mirror of
https://github.com/penpot/penpot.git
synced 2026-09-08 13:09:22 +00:00
Merge remote-tracking branch 'origin/staging' into develop
This commit is contained in:
commit
1dfa2cd9f2
1
.gitignore
vendored
1
.gitignore
vendored
@ -99,6 +99,7 @@ opencode.json
|
|||||||
/.idea
|
/.idea
|
||||||
*.iml
|
*.iml
|
||||||
/.claude
|
/.claude
|
||||||
|
/CLAUDE.md
|
||||||
/.playwright-mcp
|
/.playwright-mcp
|
||||||
/.devenv/mcp/
|
/.devenv/mcp/
|
||||||
/opencode.json
|
/opencode.json
|
||||||
|
|||||||
@ -166,7 +166,6 @@
|
|||||||
(not (ctob/token-name-path-exists? token-name tokens-tree)))
|
(not (ctob/token-name-path-exists? token-name tokens-tree)))
|
||||||
new-tokens))))]])
|
new-tokens))))]])
|
||||||
(defn find-refs [value]
|
(defn find-refs [value]
|
||||||
(prn value)
|
|
||||||
(cond
|
(cond
|
||||||
(string? value)
|
(string? value)
|
||||||
(cto/find-token-value-references value)
|
(cto/find-token-value-references value)
|
||||||
|
|||||||
@ -78,7 +78,7 @@ services:
|
|||||||
# - "443:443"
|
# - "443:443"
|
||||||
|
|
||||||
penpot-frontend:
|
penpot-frontend:
|
||||||
image: "penpotapp/frontend:${PENPOT_VERSION:-2.16}"
|
image: "penpotapp/frontend:${PENPOT_VERSION:-2.17}"
|
||||||
restart: always
|
restart: always
|
||||||
ports:
|
ports:
|
||||||
- 9001:8080
|
- 9001:8080
|
||||||
@ -111,7 +111,7 @@ services:
|
|||||||
# PENPOT_DISABLE_IPV6_LISTEN: "true"
|
# PENPOT_DISABLE_IPV6_LISTEN: "true"
|
||||||
|
|
||||||
penpot-backend:
|
penpot-backend:
|
||||||
image: "penpotapp/backend:${PENPOT_VERSION:-2.16}"
|
image: "penpotapp/backend:${PENPOT_VERSION:-2.17}"
|
||||||
restart: always
|
restart: always
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
@ -180,13 +180,13 @@ services:
|
|||||||
PENPOT_SMTP_SSL: "false"
|
PENPOT_SMTP_SSL: "false"
|
||||||
|
|
||||||
penpot-mcp:
|
penpot-mcp:
|
||||||
image: "penpotapp/mcp:${PENPOT_VERSION:-2.16}"
|
image: "penpotapp/mcp:${PENPOT_VERSION:-2.17}"
|
||||||
restart: always
|
restart: always
|
||||||
networks:
|
networks:
|
||||||
- penpot
|
- penpot
|
||||||
|
|
||||||
penpot-exporter:
|
penpot-exporter:
|
||||||
image: "penpotapp/exporter:${PENPOT_VERSION:-2.16}"
|
image: "penpotapp/exporter:${PENPOT_VERSION:-2.17}"
|
||||||
restart: always
|
restart: always
|
||||||
|
|
||||||
depends_on:
|
depends_on:
|
||||||
|
|||||||
@ -26,7 +26,7 @@ Penpot MCP enables **multi-directional workflows** between design and code. Beca
|
|||||||
title="Quick demo: Penpot MCP server in action"
|
title="Quick demo: Penpot MCP server in action"
|
||||||
width="100%"
|
width="100%"
|
||||||
height="480"
|
height="480"
|
||||||
src="https://www.youtube.com/embed/CfvcgMQEmLk?rel=0"
|
src="https://www.youtube.com/embed/7V01SKVG6PQ?rel=0"
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
|
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
|
||||||
referrerpolicy="strict-origin-when-cross-origin"
|
referrerpolicy="strict-origin-when-cross-origin"
|
||||||
|
|||||||
@ -31,4 +31,10 @@ desc: Begin with the Penpot user guide! Get quickstarts, shortcuts, and tutorial
|
|||||||
<p>Useful resources to better understand Penpot</p>
|
<p>Useful resources to better understand Penpot</p>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="/user-guide/first-steps/migration-guide">
|
||||||
|
<h2>Migration Guide →</h2>
|
||||||
|
<p>Move a design system from Figma to Penpot</p>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
31
docs/user-guide/first-steps/migration-guide.njk
Normal file
31
docs/user-guide/first-steps/migration-guide.njk
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
---
|
||||||
|
title: Migration Guide
|
||||||
|
order: 6
|
||||||
|
desc: Move a design system from Figma to Penpot. Read a short summary of the enterprise migration guide and open the full PDF.
|
||||||
|
---
|
||||||
|
|
||||||
|
<h1 id="migration-guide">Migration Guide</h1>
|
||||||
|
|
||||||
|
<p class="main-paragraph">If you are moving a design system to Penpot, especially from Figma, start with the enterprise migration guide. It covers file and library migration, tokens, validation, dual-tool workflows, and how different roles can run a pilot.</p>
|
||||||
|
|
||||||
|
<div class="advice">
|
||||||
|
<p><strong>Open the full guide (PDF)</strong></p>
|
||||||
|
<p><a href="https://nextcloud.kaleidos.net/index.php/s/mKordyz62QF3PQ4?dir=/&editing=false&openfile=true" target="_blank" rel="noopener"><strong>The Enterprise Guide to Migrating Design Systems from Figma to Penpot</strong></a></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2 id="what-the-guide-covers">What the guide covers</h2>
|
||||||
|
<p>The document is written for teams that need to move more than a few mockups: libraries, tokens, variants, and the workflows around them. It focuses on Figma, but the same audit, pilot, and validation steps apply if you are coming from another tool.</p>
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
<li><strong>Before you export:</strong> audit critical files, component chains, token usage, and plugins that will not come along. Split oversized files and clean unused libraries while you are still in Figma.</li>
|
||||||
|
<li><strong>Static assets:</strong> export SVG, PNG, or JPG from Figma and place them in Penpot.</li>
|
||||||
|
<li><strong>Complex files and libraries:</strong> use the Penpot Exporter plugin for Figma (design files, slides, components, variants, auto layout, styles, variables, and libraries). Expect some layout cleanup, Figma Auto Layout becomes Flex and Grid in Penpot.</li>
|
||||||
|
<li><strong>Tokens:</strong> if you already use Tokens Studio, export JSON and import it in Penpot. Native Figma Variables can go through Tokens Studio, or through the Exporter plugin.</li>
|
||||||
|
<li><strong>Validate before you scale:</strong> migrate one representative file (or a sandbox library), write down recurring cleanup, then roll the same checklist out to the rest of the workspace.</li>
|
||||||
|
<li><strong>People and pilots:</strong> the second half of the guide has paths for designers, frontend developers, DesignOps, design-system leads, and product/engineering pilots, including how Penpot MCP can help with post-import cleanup.</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<p>The guide also covers running Figma and Penpot in parallel for a while. The exporter is for one-off migration, not continuous sync.</p>
|
||||||
|
|
||||||
|
<h2 id="discuss-the-guide">Questions and discussion</h2>
|
||||||
|
<p>If you want to ask about a migration, or share how yours is going, use the Community post <a href="https://community.penpot.app/t/the-enterprise-guide-to-migrating-design-systems-to-penpot/10768" target="_blank" rel="noopener">The Enterprise Guide to Migrating Design Systems to Penpot</a>.</p>
|
||||||
@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
title: Troubleshooting WebGL
|
title: Troubleshooting WebGL
|
||||||
order: 5
|
order: 7
|
||||||
desc: Diagnose and fix common WebGL issues in Penpot, enable WebGL rendering (Beta), and troubleshoot browser, GPU, and system checks.
|
desc: Diagnose and fix common WebGL issues in Penpot, enable WebGL rendering (Beta), and troubleshoot browser, GPU, and system checks.
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@ -584,11 +584,38 @@
|
|||||||
(into {}))]
|
(into {}))]
|
||||||
(merge resolved dropped)))
|
(merge resolved dropped)))
|
||||||
|
|
||||||
|
(defn- valid-token-value?
|
||||||
|
[[_ token]]
|
||||||
|
(some? (:value token)))
|
||||||
|
|
||||||
|
(def ^:private xform-invalid-value-tokens
|
||||||
|
(comp
|
||||||
|
(remove valid-token-value?)
|
||||||
|
(map (fn [[k token]]
|
||||||
|
[k (assoc token :errors [(wte/get-error-code :error.token/empty-input)])]))))
|
||||||
|
|
||||||
|
(defn- merge-invalid-value-tokens
|
||||||
|
"Tokens with a `nil` value (e.g. a composite typography token saved with
|
||||||
|
no fields filled in) must never reach StyleDictionary: some of its
|
||||||
|
preprocessors (`@tokens-studio/sd-transforms`'s font-styles preprocessor,
|
||||||
|
in particular) assume a typography token's value is never null and throw
|
||||||
|
an uncaught exception when it is, taking down token resolution for the
|
||||||
|
whole file.
|
||||||
|
|
||||||
|
`tokens` is the full, unfiltered token map; `resolved` only contains the
|
||||||
|
valid subset that was actually sent to StyleDictionary. Tag the invalid
|
||||||
|
ones with the same \"empty value\" error the token forms already use
|
||||||
|
instead of ever letting them reach the resolver."
|
||||||
|
[tokens resolved]
|
||||||
|
(into resolved xform-invalid-value-tokens tokens))
|
||||||
|
|
||||||
(defn resolve-tokens
|
(defn resolve-tokens
|
||||||
[tokens]
|
[tokens]
|
||||||
(let [tokens-tree (ctob/tokens-tree tokens)]
|
(let [valid-tokens (into {} (filter valid-token-value?) tokens)
|
||||||
(->> (resolve-tokens-tree tokens-tree #(get tokens (sd-token-name %)))
|
tokens-tree (ctob/tokens-tree valid-tokens)]
|
||||||
(rx/map #(merge-name-collisions tokens %)))))
|
(->> (resolve-tokens-tree tokens-tree #(get valid-tokens (sd-token-name %)))
|
||||||
|
(rx/map #(merge-name-collisions valid-tokens %))
|
||||||
|
(rx/map #(merge-invalid-value-tokens tokens %)))))
|
||||||
|
|
||||||
(defn resolve-tokens-interactive
|
(defn resolve-tokens-interactive
|
||||||
"Interactive check of resolving tokens.
|
"Interactive check of resolving tokens.
|
||||||
@ -610,15 +637,18 @@
|
|||||||
computation we can restore any token, even clashing ones with the
|
computation we can restore any token, even clashing ones with the
|
||||||
same :name path by just looking up that :id in the ids map."
|
same :name path by just looking up that :id in the ids map."
|
||||||
[tokens]
|
[tokens]
|
||||||
(let [{:keys [tokens-tree ids]} (ctob/backtrace-tokens-tree tokens)]
|
(let [valid-tokens (into {} (filter valid-token-value?) tokens)
|
||||||
(->> (resolve-tokens-tree tokens-tree #(get ids (sd-token-uuid %)))
|
{:keys [tokens-tree ids]} (ctob/backtrace-tokens-tree valid-tokens)]
|
||||||
(rx/map #(merge-name-collisions tokens %)))))
|
(->> (resolve-tokens-tree tokens-tree #(get ids (sd-token-uuid %)))
|
||||||
|
(rx/map #(merge-name-collisions valid-tokens %))
|
||||||
|
(rx/map #(merge-invalid-value-tokens tokens %)))))
|
||||||
|
|
||||||
(defn resolve-tokens-with-verbose-errors [tokens]
|
(defn resolve-tokens-with-verbose-errors [tokens]
|
||||||
(resolve-tokens-tree
|
(let [valid-tokens (into {} (filter valid-token-value?) tokens)]
|
||||||
(ctob/tokens-tree tokens)
|
(resolve-tokens-tree
|
||||||
#(get tokens (sd-token-name %))
|
(ctob/tokens-tree valid-tokens)
|
||||||
(StyleDictionary. (assoc default-config :log {:verbosity "verbose"}))))
|
#(get valid-tokens (sd-token-name %))
|
||||||
|
(StyleDictionary. (assoc default-config :log {:verbosity "verbose"})))))
|
||||||
|
|
||||||
;; === Hooks
|
;; === Hooks
|
||||||
|
|
||||||
|
|||||||
@ -15,6 +15,7 @@
|
|||||||
[app.common.geom.point :as gpt]
|
[app.common.geom.point :as gpt]
|
||||||
[app.common.geom.rect :as grc]
|
[app.common.geom.rect :as grc]
|
||||||
[app.common.geom.shapes :as gsh]
|
[app.common.geom.shapes :as gsh]
|
||||||
|
[app.common.logging :as log]
|
||||||
[app.common.math :as mth]
|
[app.common.math :as mth]
|
||||||
[app.common.types.component :as ctk]
|
[app.common.types.component :as ctk]
|
||||||
[app.common.types.container :as ctn]
|
[app.common.types.container :as ctn]
|
||||||
@ -579,44 +580,57 @@
|
|||||||
modifiers (calculate-modifiers state ignore-constraints ignore-snap-pixel modif-tree page-id params)]
|
modifiers (calculate-modifiers state ignore-constraints ignore-snap-pixel modif-tree page-id params)]
|
||||||
(assoc state :workspace-modifiers modifiers))))))
|
(assoc state :workspace-modifiers modifiers))))))
|
||||||
|
|
||||||
|
(defn- without-nil-ids
|
||||||
|
"Drop nil-keyed entries from a modif-tree. A nil shape id (possible in
|
||||||
|
production builds, where the upstream asserts are elided) would crash
|
||||||
|
the WASM heap write with `uuid/get-u32` being called on nil."
|
||||||
|
[modif-tree]
|
||||||
|
(if (contains? modif-tree nil)
|
||||||
|
(do (log/warn :hint "modif-tree contains a nil shape id; ignoring entry")
|
||||||
|
(dissoc modif-tree nil))
|
||||||
|
modif-tree))
|
||||||
|
|
||||||
(defn- parse-structure-modifiers
|
(defn- parse-structure-modifiers
|
||||||
[modif-tree]
|
[modif-tree]
|
||||||
(into
|
(into
|
||||||
[]
|
[]
|
||||||
(mapcat
|
(comp
|
||||||
(fn [[parent-id data]]
|
(mapcat
|
||||||
(when (ctm/has-structure? (:modifiers data))
|
(fn [[parent-id data]]
|
||||||
(->> (concat
|
(when (ctm/has-structure? (:modifiers data))
|
||||||
(get-in data [:modifiers :structure-parent])
|
(->> (concat
|
||||||
(get-in data [:modifiers :structure-child]))
|
(get-in data [:modifiers :structure-parent])
|
||||||
(mapcat
|
(get-in data [:modifiers :structure-child]))
|
||||||
(fn [modifier]
|
(mapcat
|
||||||
(case (:type modifier)
|
(fn [modifier]
|
||||||
:remove-children
|
(case (:type modifier)
|
||||||
(->> (:value modifier)
|
:remove-children
|
||||||
(map (fn [child-id]
|
(->> (:value modifier)
|
||||||
{:type :remove-children
|
(map (fn [child-id]
|
||||||
:parent parent-id
|
{:type :remove-children
|
||||||
:id child-id
|
:parent parent-id
|
||||||
:index 0
|
:id child-id
|
||||||
:value 0})))
|
:index 0
|
||||||
|
:value 0})))
|
||||||
|
|
||||||
:add-children
|
:add-children
|
||||||
(->> (:value modifier)
|
(->> (:value modifier)
|
||||||
(map (fn [child-id]
|
(map (fn [child-id]
|
||||||
{:type :add-children
|
{:type :add-children
|
||||||
:parent parent-id
|
:parent parent-id
|
||||||
:id child-id
|
:id child-id
|
||||||
:index (:index modifier)
|
:index (:index modifier)
|
||||||
:value 0})))
|
:value 0})))
|
||||||
|
|
||||||
:scale-content
|
:scale-content
|
||||||
[{:type :scale-content
|
[{:type :scale-content
|
||||||
:parent parent-id
|
:parent parent-id
|
||||||
:id parent-id
|
:id parent-id
|
||||||
:index 0
|
:index 0
|
||||||
:value (:value modifier)}]
|
:value (:value modifier)}]
|
||||||
nil)))))))
|
nil)))))))
|
||||||
|
(filter (fn [{:keys [id parent]}]
|
||||||
|
(and (some? id) (some? parent)))))
|
||||||
modif-tree))
|
modif-tree))
|
||||||
|
|
||||||
|
|
||||||
@ -624,7 +638,7 @@
|
|||||||
(let [default-transform (gmt/matrix)]
|
(let [default-transform (gmt/matrix)]
|
||||||
(keep (fn [[id data]]
|
(keep (fn [[id data]]
|
||||||
(cond
|
(cond
|
||||||
(= id uuid/zero)
|
(or (nil? id) (= id uuid/zero))
|
||||||
nil
|
nil
|
||||||
|
|
||||||
(ctm/has-geometry? (:modifiers data))
|
(ctm/has-geometry? (:modifiers data))
|
||||||
@ -693,65 +707,66 @@
|
|||||||
subtree-ids-by-id selection-rect-cache]
|
subtree-ids-by-id selection-rect-cache]
|
||||||
:or {ignore-constraints false ignore-snap-pixel false}
|
:or {ignore-constraints false ignore-snap-pixel false}
|
||||||
:as params}]
|
:as params}]
|
||||||
(ptk/reify ::set-wasm-modifiers
|
(let [modif-tree (without-nil-ids modif-tree)]
|
||||||
ptk/UpdateEvent
|
(ptk/reify ::set-wasm-modifiers
|
||||||
(update [_ state]
|
ptk/UpdateEvent
|
||||||
(let [property-changes (extract-property-changes modif-tree)]
|
(update [_ state]
|
||||||
(if (d/not-empty? property-changes)
|
(let [property-changes (extract-property-changes modif-tree)]
|
||||||
(-> state
|
(if (d/not-empty? property-changes)
|
||||||
(assoc :prev-wasm-props (:wasm-props state))
|
(-> state
|
||||||
(assoc :wasm-props property-changes))
|
(assoc :prev-wasm-props (:wasm-props state))
|
||||||
state)))
|
(assoc :wasm-props property-changes))
|
||||||
|
state)))
|
||||||
|
|
||||||
ptk/WatchEvent
|
ptk/WatchEvent
|
||||||
(watch [_ state _]
|
(watch [_ state _]
|
||||||
;; Entering an interactive transform (drag/resize/rotate). Flip
|
;; Entering an interactive transform (drag/resize/rotate). Flip
|
||||||
;; the renderer into fast + atlas-backdrop mode so the live
|
;; the renderer into fast + atlas-backdrop mode so the live
|
||||||
;; preview is cheap, tiles never appear sequentially and the main
|
;; preview is cheap, tiles never appear sequentially and the main
|
||||||
;; thread is not blocked. The pair is closed in
|
;; thread is not blocked. The pair is closed in
|
||||||
;; `clear-local-transform`.
|
;; `clear-local-transform`.
|
||||||
(ensure-interactive-transform-start!)
|
(ensure-interactive-transform-start!)
|
||||||
(let [snap-pixel? (and (not ignore-snap-pixel) (contains? (:workspace-layout state) :snap-pixel-grid))
|
(let [snap-pixel? (and (not ignore-snap-pixel) (contains? (:workspace-layout state) :snap-pixel-grid))
|
||||||
translation? (every? #(ctm/only-move? (:modifiers %)) (vals modif-tree))]
|
translation? (every? #(ctm/only-move? (:modifiers %)) (vals modif-tree))]
|
||||||
|
|
||||||
(if translation?
|
(if translation?
|
||||||
;; Pure translation: no structure changes needed. If structure
|
;; Pure translation: no structure changes needed. If structure
|
||||||
;; modifiers were active from a previous non-translation frame
|
;; modifiers were active from a previous non-translation frame
|
||||||
;; (e.g. shape hovered over a frame then dragged back out),
|
;; (e.g. shape hovered over a frame then dragged back out),
|
||||||
;; clear them now so the shape is not clipped by the old frame.
|
;; clear them now so the shape is not clipped by the old frame.
|
||||||
(when @wasm-structure-modifiers-active?
|
(when @wasm-structure-modifiers-active?
|
||||||
(wasm.api/clean-modifiers)
|
(wasm.api/clean-modifiers)
|
||||||
(vreset! wasm-structure-modifiers-active? false))
|
(vreset! wasm-structure-modifiers-active? false))
|
||||||
(let [objects (dsh/lookup-page-objects state)]
|
(let [objects (dsh/lookup-page-objects state)]
|
||||||
(set-wasm-props! objects (:prev-wasm-props state) (:wasm-props state))
|
(set-wasm-props! objects (:prev-wasm-props state) (:wasm-props state))
|
||||||
(wasm.api/clean-modifiers)
|
(wasm.api/clean-modifiers)
|
||||||
(wasm.api/set-structure-modifiers (parse-structure-modifiers modif-tree))
|
(wasm.api/set-structure-modifiers (parse-structure-modifiers modif-tree))
|
||||||
(vreset! wasm-structure-modifiers-active? true)))
|
(vreset! wasm-structure-modifiers-active? true)))
|
||||||
(let [geometry-entries (parse-geometry-modifiers modif-tree)
|
(let [geometry-entries (parse-geometry-modifiers modif-tree)
|
||||||
root-modifiers (into [] (map (fn [[id data]] [id (:transform data)])) geometry-entries)
|
root-modifiers (into [] (map (fn [[id data]] [id (:transform data)])) geometry-entries)
|
||||||
wasm-ready? (wasm.api/initialized?)
|
wasm-ready? (wasm.api/initialized?)
|
||||||
;; While the GL context is down (lost / mid-reload), keep the
|
;; While the GL context is down (lost / mid-reload), keep the
|
||||||
;; root transforms so SVG selection/preview can still move.
|
;; root transforms so SVG selection/preview can still move.
|
||||||
;; `propagate-modifiers` returns [] when not ready, do not
|
;; `propagate-modifiers` returns [] when not ready, do not
|
||||||
;; treat that as "no modifiers".
|
;; treat that as "no modifiers".
|
||||||
modifiers
|
modifiers
|
||||||
(cond
|
(cond
|
||||||
(or (not wasm-ready?)
|
(or (not wasm-ready?)
|
||||||
(and translation? (not snap-pixel?)))
|
(and translation? (not snap-pixel?)))
|
||||||
root-modifiers
|
root-modifiers
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(let [propagated (wasm.api/propagate-modifiers geometry-entries snap-pixel?)]
|
(let [propagated (wasm.api/propagate-modifiers geometry-entries snap-pixel?)]
|
||||||
(if (seq propagated) propagated root-modifiers)))]
|
(if (seq propagated) propagated root-modifiers)))]
|
||||||
(when wasm-ready?
|
(when wasm-ready?
|
||||||
(wasm.api/set-modifiers modifiers))
|
(wasm.api/set-modifiers modifiers))
|
||||||
(let [ids (into [] xf:map-key geometry-entries)
|
(let [ids (into [] xf:map-key geometry-entries)
|
||||||
selrect (when wasm-ready?
|
selrect (when wasm-ready?
|
||||||
(if (and translation? (not snap-pixel?) selection-rect-cache (seq modifiers))
|
(if (and translation? (not snap-pixel?) selection-rect-cache (seq modifiers))
|
||||||
(cached-translation-selrect ids (second (first modifiers)) selection-rect-cache)
|
(cached-translation-selrect ids (second (first modifiers)) selection-rect-cache)
|
||||||
(wasm.api/get-selection-rect ids)))]
|
(wasm.api/get-selection-rect ids)))]
|
||||||
(rx/of (set-temporary-selrect selrect)
|
(rx/of (set-temporary-selrect selrect)
|
||||||
(set-temporary-modifiers modifiers))))))))
|
(set-temporary-modifiers modifiers)))))))))
|
||||||
|
|
||||||
(defn propagate-structure-modifiers
|
(defn propagate-structure-modifiers
|
||||||
[modif-tree objects]
|
[modif-tree objects]
|
||||||
@ -782,58 +797,44 @@
|
|||||||
subtree-ids-by-id]
|
subtree-ids-by-id]
|
||||||
:or {ignore-constraints false ignore-snap-pixel false snap-ignore-axis nil undo-transation? true}
|
:or {ignore-constraints false ignore-snap-pixel false snap-ignore-axis nil undo-transation? true}
|
||||||
:as params}]
|
:as params}]
|
||||||
(ptk/reify ::apply-wasm-modifiers
|
(let [modif-tree (without-nil-ids modif-tree)]
|
||||||
ptk/WatchEvent
|
(ptk/reify ::apply-wasm-modifiers
|
||||||
(watch [_ state _]
|
ptk/WatchEvent
|
||||||
(let [translation?
|
(watch [_ state _]
|
||||||
(every? #(ctm/only-move? (:modifiers %)) (vals modif-tree))]
|
(let [translation?
|
||||||
(wasm.api/clean-modifiers)
|
(every? #(ctm/only-move? (:modifiers %)) (vals modif-tree))]
|
||||||
(when-not translation?
|
(wasm.api/clean-modifiers)
|
||||||
(wasm.api/set-structure-modifiers (parse-structure-modifiers modif-tree)))
|
(when-not translation?
|
||||||
|
(wasm.api/set-structure-modifiers (parse-structure-modifiers modif-tree)))
|
||||||
|
|
||||||
;; Apply property changes (e.g. grow-type) to WASM shapes before
|
;; Apply property changes (e.g. grow-type) to WASM shapes before
|
||||||
;; propagating geometry, so propagate_modifiers sees the updated state.
|
;; propagating geometry, so propagate_modifiers sees the updated state.
|
||||||
(doseq [[id {:keys [property value]}] (extract-property-changes modif-tree)]
|
(doseq [[id {:keys [property value]}] (extract-property-changes modif-tree)]
|
||||||
(when (= property :grow-type)
|
(when (= property :grow-type)
|
||||||
(wasm.api/use-shape id)
|
(wasm.api/use-shape id)
|
||||||
(wasm.api/set-shape-grow-type value)))
|
(wasm.api/set-shape-grow-type value)))
|
||||||
|
|
||||||
(let [objects (dsh/lookup-page-objects state)
|
(let [objects (dsh/lookup-page-objects state)
|
||||||
|
|
||||||
geometry-entries
|
geometry-entries
|
||||||
(parse-geometry-modifiers modif-tree)
|
(parse-geometry-modifiers modif-tree)
|
||||||
|
|
||||||
snap-pixel?
|
snap-pixel?
|
||||||
(and (not ignore-snap-pixel) (contains? (:workspace-layout state) :snap-pixel-grid))
|
(and (not ignore-snap-pixel) (contains? (:workspace-layout state) :snap-pixel-grid))
|
||||||
|
|
||||||
transforms
|
transforms
|
||||||
(cond
|
(cond
|
||||||
(and translation? (not snap-pixel?))
|
(and translation? (not snap-pixel?))
|
||||||
;; Mirror WASM `propagate_modifiers` in CLJS: splat the
|
;; Mirror WASM `propagate_modifiers` in CLJS: splat the
|
||||||
;; translation matrix onto every descendant. Without
|
;; translation matrix onto every descendant. Without
|
||||||
;; this step the commit would only touch the dragged
|
;; this step the commit would only touch the dragged
|
||||||
;; primaries and descendants would snap back to their
|
;; primaries and descendants would snap back to their
|
||||||
;; pre-drag positions on drop.
|
;; pre-drag positions on drop.
|
||||||
;;
|
;;
|
||||||
;; Skipped when `snap-pixel?` is on: WASM applies
|
;; Skipped when `snap-pixel?` is on: WASM applies
|
||||||
;; per-shape pixel correction (different scale/translate
|
;; per-shape pixel correction (different scale/translate
|
||||||
;; per descendant) which we can't replicate cheaply on
|
;; per descendant) which we can't replicate cheaply on
|
||||||
;; the CLJS side.
|
;; the CLJS side.
|
||||||
(reduce
|
|
||||||
(fn [acc [id data]]
|
|
||||||
(let [t (:transform data)
|
|
||||||
subtree-ids
|
|
||||||
(or (get subtree-ids-by-id id)
|
|
||||||
(cfh/get-children-ids-with-self objects id))]
|
|
||||||
(reduce (fn [a sid] (assoc a sid t)) acc subtree-ids)))
|
|
||||||
{}
|
|
||||||
geometry-entries)
|
|
||||||
|
|
||||||
;; Context lost / mid-reload: do not call into WASM. Use
|
|
||||||
;; root transforms (and splat translation onto descendants
|
|
||||||
;; when we can) so the commit still lands in file data.
|
|
||||||
(not (wasm.api/initialized?))
|
|
||||||
(if translation?
|
|
||||||
(reduce
|
(reduce
|
||||||
(fn [acc [id data]]
|
(fn [acc [id data]]
|
||||||
(let [t (:transform data)
|
(let [t (:transform data)
|
||||||
@ -843,71 +844,87 @@
|
|||||||
(reduce (fn [a sid] (assoc a sid t)) acc subtree-ids)))
|
(reduce (fn [a sid] (assoc a sid t)) acc subtree-ids)))
|
||||||
{}
|
{}
|
||||||
geometry-entries)
|
geometry-entries)
|
||||||
(into {}
|
|
||||||
(map (fn [[id data]] [id (:transform data)]))
|
|
||||||
geometry-entries))
|
|
||||||
|
|
||||||
:else
|
;; Context lost / mid-reload: do not call into WASM. Use
|
||||||
(into {} (wasm.api/propagate-modifiers geometry-entries snap-pixel?)))
|
;; root transforms (and splat translation onto descendants
|
||||||
|
;; when we can) so the commit still lands in file data.
|
||||||
|
(not (wasm.api/initialized?))
|
||||||
|
(if translation?
|
||||||
|
(reduce
|
||||||
|
(fn [acc [id data]]
|
||||||
|
(let [t (:transform data)
|
||||||
|
subtree-ids
|
||||||
|
(or (get subtree-ids-by-id id)
|
||||||
|
(cfh/get-children-ids-with-self objects id))]
|
||||||
|
(reduce (fn [a sid] (assoc a sid t)) acc subtree-ids)))
|
||||||
|
{}
|
||||||
|
geometry-entries)
|
||||||
|
(into {}
|
||||||
|
(map (fn [[id data]] [id (:transform data)]))
|
||||||
|
geometry-entries))
|
||||||
|
|
||||||
ignore-tree
|
:else
|
||||||
(calculate-ignore-tree-wasm transforms objects)
|
(into {} (wasm.api/propagate-modifiers geometry-entries snap-pixel?)))
|
||||||
|
|
||||||
options
|
ignore-tree
|
||||||
(-> params
|
(calculate-ignore-tree-wasm transforms objects)
|
||||||
(assoc :reg-objects? true)
|
|
||||||
(assoc :ignore-tree ignore-tree)
|
|
||||||
(assoc :translation? translation?)
|
|
||||||
;; Attributes that can change in the transform. This
|
|
||||||
;; way we don't have to check all the attributes
|
|
||||||
(assoc :attrs transform-attrs))
|
|
||||||
|
|
||||||
modif-tree
|
options
|
||||||
(propagate-structure-modifiers modif-tree (dsh/lookup-page-objects state))
|
(-> params
|
||||||
|
(assoc :reg-objects? true)
|
||||||
|
(assoc :ignore-tree ignore-tree)
|
||||||
|
(assoc :translation? translation?)
|
||||||
|
;; Attributes that can change in the transform. This
|
||||||
|
;; way we don't have to check all the attributes
|
||||||
|
(assoc :attrs transform-attrs))
|
||||||
|
|
||||||
ids
|
modif-tree
|
||||||
(into (set (keys modif-tree)) xf:without-uuid-zero (keys transforms))
|
(propagate-structure-modifiers modif-tree (dsh/lookup-page-objects state))
|
||||||
|
|
||||||
update-shape
|
ids
|
||||||
(fn [shape]
|
(into (set (keys modif-tree)) xf:without-uuid-zero (keys transforms))
|
||||||
(let [shape-id (dm/get-prop shape :id)
|
|
||||||
transform (get transforms shape-id)
|
|
||||||
modifiers (dm/get-in modif-tree [shape-id :modifiers])]
|
|
||||||
(-> shape
|
|
||||||
(gsh/apply-transform transform)
|
|
||||||
(ctm/apply-structure-modifiers modifiers))))
|
|
||||||
|
|
||||||
bool-ids
|
update-shape
|
||||||
(into #{}
|
(fn [shape]
|
||||||
(comp
|
(let [shape-id (dm/get-prop shape :id)
|
||||||
(mapcat (partial cfh/get-parents-with-self objects))
|
transform (get transforms shape-id)
|
||||||
(filter cfh/bool-shape?)
|
modifiers (dm/get-in modif-tree [shape-id :modifiers])]
|
||||||
(map :id))
|
(-> shape
|
||||||
ids)
|
(gsh/apply-transform transform)
|
||||||
|
(ctm/apply-structure-modifiers modifiers))))
|
||||||
|
|
||||||
undo-id (js/Symbol)]
|
bool-ids
|
||||||
(rx/concat
|
(into #{}
|
||||||
(if undo-transation?
|
(comp
|
||||||
(rx/of (dwu/start-undo-transaction undo-id))
|
(mapcat (partial cfh/get-parents-with-self objects))
|
||||||
(rx/empty))
|
(filter cfh/bool-shape?)
|
||||||
(rx/of
|
(map :id))
|
||||||
(clear-local-transform)
|
ids)
|
||||||
(ptk/event ::dwg/move-frame-guides {:ids ids :transforms transforms})
|
|
||||||
(ptk/event ::dwcm/move-frame-comment-threads transforms)
|
|
||||||
(dwsh/update-shapes ids update-shape options)
|
|
||||||
|
|
||||||
;; The update to the bool path needs to be in a different operation because it
|
undo-id (js/Symbol)]
|
||||||
;; needs to have the updated children info.
|
|
||||||
;; `update-layout? false`: recalculating a bool path can never change
|
|
||||||
;; `:hidden`, and the layout check would recompute the whole boolean
|
|
||||||
;; path in WASM once per bool shape just to find that out.
|
|
||||||
(dwsh/update-shapes bool-ids path/update-bool-shape (assoc options
|
|
||||||
:with-objects? true
|
|
||||||
:update-layout? false)))
|
|
||||||
|
|
||||||
(if undo-transation?
|
(rx/concat
|
||||||
(rx/of (dwu/commit-undo-transaction undo-id))
|
(if undo-transation?
|
||||||
(rx/empty))))))))
|
(rx/of (dwu/start-undo-transaction undo-id))
|
||||||
|
(rx/empty))
|
||||||
|
(rx/of
|
||||||
|
(clear-local-transform)
|
||||||
|
(ptk/event ::dwg/move-frame-guides {:ids ids :transforms transforms})
|
||||||
|
(ptk/event ::dwcm/move-frame-comment-threads transforms)
|
||||||
|
(dwsh/update-shapes ids update-shape options)
|
||||||
|
|
||||||
|
;; The update to the bool path needs to be in a different operation because it
|
||||||
|
;; needs to have the updated children info.
|
||||||
|
;; `update-layout? false`: recalculating a bool path can never change
|
||||||
|
;; `:hidden`, and the layout check would recompute the whole boolean
|
||||||
|
;; path in WASM once per bool shape just to find that out.
|
||||||
|
(dwsh/update-shapes bool-ids path/update-bool-shape (assoc options
|
||||||
|
:with-objects? true
|
||||||
|
:update-layout? false)))
|
||||||
|
|
||||||
|
(if undo-transation?
|
||||||
|
(rx/of (dwu/commit-undo-transaction undo-id))
|
||||||
|
(rx/empty)))))))))
|
||||||
|
|
||||||
(def ^:private
|
(def ^:private
|
||||||
xf-rotation-shape
|
xf-rotation-shape
|
||||||
|
|||||||
@ -335,14 +335,15 @@
|
|||||||
(let [page-id (:current-page-id state)
|
(let [page-id (:current-page-id state)
|
||||||
objects (dsh/lookup-page-objects state page-id)
|
objects (dsh/lookup-page-objects state page-id)
|
||||||
shape (get objects shape-id)
|
shape (get objects shape-id)
|
||||||
container (get objects (:parent-id shape))
|
container (get objects (:parent-id shape))]
|
||||||
width (+ (:width container) (:width shape) 20) ;; 20 is the default gap for variants
|
(when (and (some? shape) (some? container))
|
||||||
x (- width (+ (:width shape) 30))] ;; 30 is the default margin for variants
|
(let [width (+ (:width container) (:width shape) 20) ;; 20 is the default gap for variants
|
||||||
(rx/of
|
x (- width (+ (:width shape) 30))] ;; 30 is the default margin for variants
|
||||||
(dwt/update-dimensions [(:parent-id shape)] :width width)
|
(rx/of
|
||||||
(dwt/update-position shape-id
|
(dwt/update-dimensions [(:parent-id shape)] :width width)
|
||||||
{:x x}
|
(dwt/update-position shape-id
|
||||||
{:absolute? false}))))))
|
{:x x}
|
||||||
|
{:absolute? false}))))))))
|
||||||
|
|
||||||
(defn add-new-variant
|
(defn add-new-variant
|
||||||
"Create a new variant and add it to the variant-container"
|
"Create a new variant and add it to the variant-container"
|
||||||
@ -359,39 +360,40 @@
|
|||||||
shape (get objects shape-id)
|
shape (get objects shape-id)
|
||||||
shape (if (ctc/is-variant-container? shape)
|
shape (if (ctc/is-variant-container? shape)
|
||||||
(get objects (last (:shapes shape)))
|
(get objects (last (:shapes shape)))
|
||||||
shape)
|
shape)]
|
||||||
component-id (:component-id shape)
|
(when (some? shape)
|
||||||
component (ctkl/get-component data component-id)
|
(let [component-id (:component-id shape)
|
||||||
|
component (ctkl/get-component data component-id)
|
||||||
|
|
||||||
container-id (:parent-id shape)
|
container-id (:parent-id shape)
|
||||||
variant-container (get objects container-id)
|
variant-container (get objects container-id)
|
||||||
has-layout? (ctsl/any-layout? variant-container)
|
has-layout? (ctsl/any-layout? variant-container)
|
||||||
|
|
||||||
new-component-id (uuid/next)
|
new-component-id (uuid/next)
|
||||||
new-shape-id (uuid/next)
|
new-shape-id (uuid/next)
|
||||||
|
|
||||||
prop-num (dec (count (:variant-properties component)))
|
prop-num (dec (count (:variant-properties component)))
|
||||||
|
|
||||||
changes (-> (pcb/empty-changes it page-id)
|
changes (-> (pcb/empty-changes it page-id)
|
||||||
(pcb/with-library-data data)
|
(pcb/with-library-data data)
|
||||||
(pcb/with-objects objects)
|
(pcb/with-objects objects)
|
||||||
(pcb/with-page-id page-id)
|
(pcb/with-page-id page-id)
|
||||||
(clv/generate-add-new-variant shape (:variant-id component) new-component-id new-shape-id prop-num))
|
(clv/generate-add-new-variant shape (:variant-id component) new-component-id new-shape-id prop-num))
|
||||||
|
|
||||||
undo-id (js/Symbol)]
|
undo-id (js/Symbol)]
|
||||||
(rx/concat
|
(rx/concat
|
||||||
(rx/of
|
(rx/of
|
||||||
(dwu/start-undo-transaction undo-id)
|
(dwu/start-undo-transaction undo-id)
|
||||||
(dch/commit-changes changes)
|
(dch/commit-changes changes)
|
||||||
(when-not has-layout?
|
(when-not has-layout?
|
||||||
(resposition-and-resize-variant new-shape-id))
|
(resposition-and-resize-variant new-shape-id))
|
||||||
(dwu/commit-undo-transaction undo-id)
|
(dwu/commit-undo-transaction undo-id)
|
||||||
(ptk/data-event :layout/update {:ids [(:parent-id shape)]})
|
(ptk/data-event :layout/update {:ids [(:parent-id shape)]})
|
||||||
(if multiselect?
|
(if multiselect?
|
||||||
(dws/shift-select-shapes new-shape-id)
|
(dws/shift-select-shapes new-shape-id)
|
||||||
(dws/select-shape new-shape-id)))
|
(dws/select-shape new-shape-id)))
|
||||||
(->> (rx/of (focus-property (:id variant-container)))
|
(->> (rx/of (focus-property (:id variant-container)))
|
||||||
(rx/delay 250))))))))
|
(rx/delay 250))))))))))
|
||||||
|
|
||||||
(defn transform-in-variant
|
(defn transform-in-variant
|
||||||
"Given the id of a main shape of a component, creates a variant structure for
|
"Given the id of a main shape of a component, creates a variant structure for
|
||||||
|
|||||||
@ -118,8 +118,8 @@
|
|||||||
(tr "nitrate.form.contact-upgrade")
|
(tr "nitrate.form.contact-upgrade")
|
||||||
(tr "nitrate.form.contact-trial"))]
|
(tr "nitrate.form.contact-trial"))]
|
||||||
[:p {:class (stl/css :modal-text-large)}
|
[:p {:class (stl/css :modal-text-large)}
|
||||||
[:a {:class (stl/css :link) :href "mailto:sales@penpot.net"}
|
[:a {:class (stl/css :link) :href "mailto:sales@penpot.app"}
|
||||||
"sales@penpot.net"]]
|
"sales@penpot.app"]]
|
||||||
[:div {:class (stl/css :activation-code)}
|
[:div {:class (stl/css :activation-code)}
|
||||||
[:p {:class (stl/css :modal-text-large)}
|
[:p {:class (stl/css :modal-text-large)}
|
||||||
[:a {:class (stl/css :link)
|
[:a {:class (stl/css :link)
|
||||||
|
|||||||
@ -848,8 +848,8 @@
|
|||||||
[:div {:class (stl/css :modal-text)}
|
[:div {:class (stl/css :modal-text)}
|
||||||
(tr "nitrate.form.enterprise-intro" ".") " " (if nitrate-license (tr "nitrate.form.contact-us-upgrade") (tr "nitrate.form.contact-us-free-trial"))]
|
(tr "nitrate.form.enterprise-intro" ".") " " (if nitrate-license (tr "nitrate.form.contact-us-upgrade") (tr "nitrate.form.contact-us-free-trial"))]
|
||||||
[:div {:class (stl/css :modal-text)}
|
[:div {:class (stl/css :modal-text)}
|
||||||
[:a {:class (stl/css :cta-button) :href "mailto:sales@penpot.net"}
|
[:a {:class (stl/css :cta-button) :href "mailto:sales@penpot.app"}
|
||||||
"sales@penpot.net"]]])]]))
|
"sales@penpot.app"]]])]]))
|
||||||
|
|
||||||
(mf/defc nitrate-contact-sales-dialog
|
(mf/defc nitrate-contact-sales-dialog
|
||||||
{::mf/register modal/components
|
{::mf/register modal/components
|
||||||
@ -898,7 +898,7 @@
|
|||||||
:on-click handle-close-dialog} (tr "ds.confirm-cancel")]
|
:on-click handle-close-dialog} (tr "ds.confirm-cancel")]
|
||||||
[:> button* {:variant "primary"
|
[:> button* {:variant "primary"
|
||||||
:type "button"
|
:type "button"
|
||||||
:on-click #(dom/open-new-window (dm/str "mailto:sales@penpot.net?subject=Switch%20to%20the%20" subscription-type "%20plan"))} (tr "nitrate.contact-sales.button")]]])]]]))
|
:on-click #(dom/open-new-window (dm/str "mailto:sales@penpot.app?subject=Switch%20to%20the%20" subscription-type "%20plan"))} (tr "nitrate.contact-sales.button")]]])]]]))
|
||||||
|
|
||||||
(mf/defc nitrate-cancel-contact-sales-dialog
|
(mf/defc nitrate-cancel-contact-sales-dialog
|
||||||
{::mf/register modal/components
|
{::mf/register modal/components
|
||||||
@ -908,7 +908,7 @@
|
|||||||
(js/encodeURIComponent email)
|
(js/encodeURIComponent email)
|
||||||
|
|
||||||
mailto-url
|
mailto-url
|
||||||
(dm/str "mailto:sales@penpot.net"
|
(dm/str "mailto:sales@penpot.app"
|
||||||
"?subject=Request%20to%20Cancel%20Enterprise%20Subscription"
|
"?subject=Request%20to%20Cancel%20Enterprise%20Subscription"
|
||||||
"&body=Hello%2C%0A%0A"
|
"&body=Hello%2C%0A%0A"
|
||||||
"I%20would%20like%20to%20cancel%20my%20Enterprise%20subscription.%0A"
|
"I%20would%20like%20to%20cancel%20my%20Enterprise%20subscription.%0A"
|
||||||
@ -930,8 +930,8 @@
|
|||||||
[:div {:class (stl/css :modal-content)}
|
[:div {:class (stl/css :modal-content)}
|
||||||
[:div {:class (stl/css :modal-text-medium)}
|
[:div {:class (stl/css :modal-text-medium)}
|
||||||
(tr "nitrate.subscription.settings.manual-contact-us")]
|
(tr "nitrate.subscription.settings.manual-contact-us")]
|
||||||
[:a {:class (stl/css :cta-link) :href "mailto:sales@penpot.net"}
|
[:a {:class (stl/css :cta-link) :href "mailto:sales@penpot.app"}
|
||||||
"sales@penpot.net"]
|
"sales@penpot.app"]
|
||||||
[:div {:class (stl/css :action-buttons)}
|
[:div {:class (stl/css :action-buttons)}
|
||||||
[:> button* {:class (stl/css :button-full-width)
|
[:> button* {:class (stl/css :button-full-width)
|
||||||
:variant "primary"
|
:variant "primary"
|
||||||
|
|||||||
@ -276,7 +276,7 @@
|
|||||||
(seq strokes)
|
(seq strokes)
|
||||||
[:> h/sortable-container* {}
|
[:> h/sortable-container* {}
|
||||||
(for [[index value] (d/enumerate (:strokes values []))]
|
(for [[index value] (d/enumerate (:strokes values []))]
|
||||||
[:> stroke-row* {:key (dm/str "stroke-" index "-" (hash applied-tokens))
|
[:> stroke-row* {:key (dm/str "stroke-" index)
|
||||||
:index index
|
:index index
|
||||||
:stroke value
|
:stroke value
|
||||||
:title (tr "workspace.options.stroke-color")
|
:title (tr "workspace.options.stroke-color")
|
||||||
|
|||||||
@ -18,7 +18,6 @@
|
|||||||
[app.main.ui.workspace.tokens.management.forms.generic-form :as generic]
|
[app.main.ui.workspace.tokens.management.forms.generic-form :as generic]
|
||||||
[app.main.ui.workspace.tokens.management.forms.validators :refer [check-coll-self-reference check-self-reference default-validate-token]]
|
[app.main.ui.workspace.tokens.management.forms.validators :refer [check-coll-self-reference check-self-reference default-validate-token]]
|
||||||
[app.util.i18n :refer [tr]]
|
[app.util.i18n :refer [tr]]
|
||||||
[beicon.v2.core :as rx]
|
|
||||||
[cuerdas.core :as str]
|
[cuerdas.core :as str]
|
||||||
[rumext.v2 :as mf]))
|
[rumext.v2 :as mf]))
|
||||||
|
|
||||||
@ -43,11 +42,14 @@
|
|||||||
(defn- validate-typography-token
|
(defn- validate-typography-token
|
||||||
[{:keys [token-value] :as props}]
|
[{:keys [token-value] :as props}]
|
||||||
(cond
|
(cond
|
||||||
;; Entering form without a value - show no error just resolve nil
|
|
||||||
(nil? token-value) (rx/of nil)
|
|
||||||
;; Validate refrence string
|
;; Validate refrence string
|
||||||
(cto/composite-token-reference? token-value) (default-validate-token props)
|
(cto/composite-token-reference? token-value) (default-validate-token props)
|
||||||
;; Validate composite token
|
;; Validate composite token. `token-value` may be nil when the form is
|
||||||
|
;; submitted without any composite field filled in — normalize it to `{}`
|
||||||
|
;; so `check-empty-typography-token` catches it and rejects the submit,
|
||||||
|
;; instead of silently saving a token with a `nil` value (which later
|
||||||
|
;; crashes token resolution: the tokens-studio StyleDictionary
|
||||||
|
;; preprocessor assumes a typography token's value is never null).
|
||||||
:else
|
:else
|
||||||
(-> props
|
(-> props
|
||||||
(update :token-value
|
(update :token-value
|
||||||
|
|||||||
@ -0,0 +1,109 @@
|
|||||||
|
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
;;
|
||||||
|
;; Copyright (c) KALEIDOS INC Sucursal en España SL
|
||||||
|
|
||||||
|
(ns frontend-tests.logic.wasm-modifiers-nil-id-test
|
||||||
|
"Reproduces the production crash \"Cannot read properties of null
|
||||||
|
(reading '__u32_buffer')\".
|
||||||
|
|
||||||
|
A modif-tree containing a nil shape id (production builds elide the
|
||||||
|
asserts that catch this upstream, e.g. `update-dimensions` called
|
||||||
|
with `[(:parent-id shape)]` when `shape` is missing) reached
|
||||||
|
`wasm.api/propagate-modifiers` / `wasm.api/set-structure-modifiers`,
|
||||||
|
and `mem.h32/write-uuid` crashed calling `uuid/get-u32` on nil while
|
||||||
|
writing to the WASM heap.
|
||||||
|
|
||||||
|
These tests assert that no nil id ever crosses the WASM boundary and
|
||||||
|
that valid shapes in the same modif-tree are still processed."
|
||||||
|
(:require
|
||||||
|
[app.common.geom.rect :as grc]
|
||||||
|
[app.common.math :as mth]
|
||||||
|
[app.common.test-helpers.compositions :as ctho]
|
||||||
|
[app.common.test-helpers.files :as cthf]
|
||||||
|
[app.common.test-helpers.ids-map :as cthi]
|
||||||
|
[app.common.test-helpers.shapes :as cths]
|
||||||
|
[app.common.types.modifiers :as ctm]
|
||||||
|
[app.common.uuid :as uuid]
|
||||||
|
[app.main.data.workspace.modifiers :as dwm]
|
||||||
|
[app.render-wasm.api :as wasm.api]
|
||||||
|
[cljs.test :as t :include-macros true]
|
||||||
|
[frontend-tests.helpers.state :as ths]
|
||||||
|
[frontend-tests.helpers.wasm :as thw]))
|
||||||
|
|
||||||
|
(def ^:private captured-geometry-entries
|
||||||
|
"Entries passed to `wasm.api/propagate-modifiers` during a test."
|
||||||
|
(atom []))
|
||||||
|
|
||||||
|
(def ^:private captured-structure-entries
|
||||||
|
"Entries passed to `wasm.api/set-structure-modifiers` during a test."
|
||||||
|
(atom []))
|
||||||
|
|
||||||
|
(defn- install-capturing-spies!
|
||||||
|
"Replace the plain WASM mocks with variants that record their input.
|
||||||
|
Must run after `thw/setup-wasm-mocks!` so teardown still restores
|
||||||
|
the real implementations."
|
||||||
|
[]
|
||||||
|
(set! wasm.api/propagate-modifiers
|
||||||
|
(fn [entries _pixel-precision]
|
||||||
|
(swap! captured-geometry-entries into entries)
|
||||||
|
(into []
|
||||||
|
(map (fn [[id data]] [id (:transform data)]))
|
||||||
|
entries)))
|
||||||
|
(set! wasm.api/set-structure-modifiers
|
||||||
|
(fn [entries]
|
||||||
|
(swap! captured-structure-entries into entries)
|
||||||
|
nil)))
|
||||||
|
|
||||||
|
(t/use-fixtures :each
|
||||||
|
{:before (fn []
|
||||||
|
(cthi/reset-idmap!)
|
||||||
|
(reset! captured-geometry-entries [])
|
||||||
|
(reset! captured-structure-entries [])
|
||||||
|
(thw/setup-wasm-mocks!)
|
||||||
|
(install-capturing-spies!))
|
||||||
|
:after (fn []
|
||||||
|
(thw/teardown-wasm-mocks!))})
|
||||||
|
|
||||||
|
(t/deftest nil-id-does-not-reach-propagate-modifiers
|
||||||
|
;; A nil-keyed entry must be dropped before the WASM heap write while
|
||||||
|
;; the valid entry is still resized.
|
||||||
|
(t/async
|
||||||
|
done
|
||||||
|
(let [file (-> (cthf/sample-file :file1)
|
||||||
|
(ctho/add-rect :rect1 :x 10 :y 20 :width 100 :height 50))
|
||||||
|
store (ths/setup-store file)
|
||||||
|
rect (cths/get-shape file :rect1)
|
||||||
|
resize (ctm/change-dimensions-modifiers rect :width 200)
|
||||||
|
modif-tree {nil {:modifiers resize}
|
||||||
|
(:id rect) {:modifiers resize}}
|
||||||
|
events [(dwm/apply-wasm-modifiers modif-tree {:ignore-snap-pixel true})]]
|
||||||
|
(ths/run-store
|
||||||
|
store done events
|
||||||
|
(fn [new-state]
|
||||||
|
(let [entry-ids (into #{} (map first) @captured-geometry-entries)
|
||||||
|
file' (ths/get-file-from-state new-state)
|
||||||
|
rect' (cths/get-shape file' :rect1)
|
||||||
|
width (-> rect' :points grc/points->rect :width)]
|
||||||
|
(t/is (not (contains? entry-ids nil)))
|
||||||
|
(t/is (contains? entry-ids (:id rect)))
|
||||||
|
(t/is (mth/close? 200 width))))))))
|
||||||
|
|
||||||
|
(t/deftest nil-id-does-not-reach-set-structure-modifiers
|
||||||
|
;; A nil-keyed entry with structure modifiers must not produce
|
||||||
|
;; structure entries with a nil :parent or :id.
|
||||||
|
(t/async
|
||||||
|
done
|
||||||
|
(let [file (-> (cthf/sample-file :file1)
|
||||||
|
(ctho/add-rect :rect1 :x 10 :y 20 :width 100 :height 50))
|
||||||
|
store (ths/setup-store file)
|
||||||
|
rect (cths/get-shape file :rect1)
|
||||||
|
modif-tree {nil {:modifiers (ctm/add-children nil [(uuid/next)] 0)}
|
||||||
|
(:id rect) {:modifiers (ctm/change-dimensions-modifiers rect :width 200)}}
|
||||||
|
events [(dwm/apply-wasm-modifiers modif-tree {:ignore-snap-pixel true})]]
|
||||||
|
(ths/run-store
|
||||||
|
store done events
|
||||||
|
(fn [_new-state]
|
||||||
|
(t/is (every? #(some? (:parent %)) @captured-structure-entries))
|
||||||
|
(t/is (every? #(some? (:id %)) @captured-structure-entries)))))))
|
||||||
@ -44,6 +44,7 @@
|
|||||||
[frontend-tests.logic.path-tools-test]
|
[frontend-tests.logic.path-tools-test]
|
||||||
[frontend-tests.logic.sidebar-transform-coalescing-test]
|
[frontend-tests.logic.sidebar-transform-coalescing-test]
|
||||||
[frontend-tests.logic.update-position-test]
|
[frontend-tests.logic.update-position-test]
|
||||||
|
[frontend-tests.logic.wasm-modifiers-nil-id-test]
|
||||||
[frontend-tests.main-errors-test]
|
[frontend-tests.main-errors-test]
|
||||||
[frontend-tests.plugins.comments-test]
|
[frontend-tests.plugins.comments-test]
|
||||||
[frontend-tests.plugins.context-shapes-test]
|
[frontend-tests.plugins.context-shapes-test]
|
||||||
@ -150,6 +151,7 @@
|
|||||||
'frontend-tests.main-errors-test
|
'frontend-tests.main-errors-test
|
||||||
'frontend-tests.logic.sidebar-transform-coalescing-test
|
'frontend-tests.logic.sidebar-transform-coalescing-test
|
||||||
'frontend-tests.logic.update-position-test
|
'frontend-tests.logic.update-position-test
|
||||||
|
'frontend-tests.logic.wasm-modifiers-nil-id-test
|
||||||
'frontend-tests.plugins.comments-test
|
'frontend-tests.plugins.comments-test
|
||||||
'frontend-tests.plugins.context-shapes-test
|
'frontend-tests.plugins.context-shapes-test
|
||||||
'frontend-tests.plugins.file-test
|
'frontend-tests.plugins.file-test
|
||||||
|
|||||||
@ -122,6 +122,43 @@
|
|||||||
(get-in resolved-tokens ["typography.bad" :errors 0 :error/code])))
|
(get-in resolved-tokens ["typography.bad" :errors 0 :error/code])))
|
||||||
(done))))))))
|
(done))))))))
|
||||||
|
|
||||||
|
;; Regression: a token with a `nil` value (e.g. a composite typography
|
||||||
|
;; token saved via the workspace form with no fields filled in) must never
|
||||||
|
;; reach StyleDictionary — its `tokens-studio` preprocessor assumes a
|
||||||
|
;; typography token's value is never null and throws an uncaught exception
|
||||||
|
;; on it, which used to take down resolution for every other token in the
|
||||||
|
;; file. It should be tagged with an empty-input error instead.
|
||||||
|
(t/deftest resolve-tokens-nil-value-test
|
||||||
|
(t/async
|
||||||
|
done
|
||||||
|
(let [tokens (-> (ctob/make-tokens-lib)
|
||||||
|
(ctob/add-set (ctob/make-token-set :id (cthi/new-id! :core-set)
|
||||||
|
:name "core"))
|
||||||
|
(ctob/add-token (cthi/id :core-set)
|
||||||
|
(ctob/make-token {:name "typography.empty"
|
||||||
|
:value nil
|
||||||
|
:type :typography}))
|
||||||
|
(ctob/add-token (cthi/id :core-set)
|
||||||
|
(ctob/make-token {:name "borderRadius.sm"
|
||||||
|
:value "12px"
|
||||||
|
:type :border-radius}))
|
||||||
|
(ctob/get-all-tokens-map))]
|
||||||
|
(->> (sd/resolve-tokens tokens)
|
||||||
|
(rx/subs!
|
||||||
|
(fn [resolved-tokens]
|
||||||
|
(t/testing "the nil-value token is tagged with an error instead of crashing"
|
||||||
|
(t/is (contains? resolved-tokens "typography.empty"))
|
||||||
|
(t/is (nil? (get-in resolved-tokens ["typography.empty" :resolved-value])))
|
||||||
|
(t/is (= :error.token/empty-input
|
||||||
|
(get-in resolved-tokens ["typography.empty" :errors 0 :error/code]))))
|
||||||
|
(t/testing "other tokens still resolve normally"
|
||||||
|
(t/is (= 12 (get-in resolved-tokens ["borderRadius.sm" :resolved-value])))))
|
||||||
|
(fn [err]
|
||||||
|
(t/do-report {:type :error :message "Stream error" :actual err})
|
||||||
|
(done))
|
||||||
|
(fn []
|
||||||
|
(done)))))))
|
||||||
|
|
||||||
(t/deftest resolve-tokens-interactive-test
|
(t/deftest resolve-tokens-interactive-test
|
||||||
(t/async
|
(t/async
|
||||||
done
|
done
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user