Merge remote-tracking branch 'origin/staging' into develop

This commit is contained in:
Andrey Antukh 2026-04-13 18:31:50 +02:00
commit 9106a994f1
15 changed files with 764 additions and 138 deletions

View File

@ -74,6 +74,25 @@
- Fix text editor v1 focus [Taiga #13961](https://tree.taiga.io/project/penpot/issue/13961)
## 2.14.3 (Unreleased)
### :sparkles: New features & Enhancements
- Add webp export format to plugin types [Github #8870](https://github.com/penpot/penpot/pull/8870)
### :bug: Bugs fixed
- Fix component "broken" after switch variant [Taiga #12984](https://tree.taiga.io/project/penpot/issue/12984)
- Fix variants corner cases with selrect and points [Github #8882](https://github.com/penpot/penpot/pull/8882)
- Fix dashboard navigation tabs overlap with projects content when scrolling [Taiga #13962](https://tree.taiga.io/project/penpot/issue/13962)
- Fix text editor v1 focus [Taiga #13961](https://tree.taiga.io/project/penpot/issue/13961)
- Fix highlight on frames after rename [Github #8938](https://github.com/penpot/penpot/pull/8938)
- Fix TypeError in sd-token-uuid when resolving tokens interactively [Github #8929](https://github.com/penpot/penpot/pull/8929)
- Fix path drawing preview passing shape instead of content to next-node
- Fix swapped arguments in CLJS PathData `-nth` with default
- Normalize PathData coordinates to safe integer bounds on read
## 2.14.2
### :sparkles: New features & Enhancements

View File

@ -2016,14 +2016,17 @@
(defn- switch-fixed-layout-geom-change-value
[prev-shape ; The shape before the switch
current-shape ; The shape after the switch (a clean copy)
origin-shape ; The original shape
attr]
;; When there is a layout with fixed h or v sizing, we need
;; to keep the width/height (and recalculate selrect and points)
(let [prev-width (-> prev-shape :selrect :width)
current-width (-> current-shape :selrect :width)
origin-width (-> origin-shape :selrect :width)
prev-height (-> prev-shape :selrect :height)
current-height (-> current-shape :selrect :height)
origin-height (-> origin-shape :selrect :height)
x (-> current-shape :selrect :x)
y (-> current-shape :selrect :y)
@ -2034,10 +2037,16 @@
final-width (if (= :fix h-sizing)
current-width
prev-width)
(if (= origin-width current-width)
prev-width ;; same-size: preserve override
current-width)) ;; different-size: use new component's
final-height (if (= :fix v-sizing)
current-height
prev-height)
(if (= origin-height current-height)
prev-height ;; same-size: preserve override
current-height)) ;; different-size: use new component's
selrect (assoc (:selrect current-shape)
:width final-width
:height final-height
@ -2066,6 +2075,25 @@
(or (:transform current-shape) (gmt/matrix)))))))
(defn- equal-geometry?
"Returns true when the value of `attr` in `shape` is considered equal
to the corresponding value in `origin-shape`, ignoring positional
displacement (x/y).
For :selrect we compare width/height only;
for :points we normalise each vector so the first point is the
origin before comparing."
[shape origin-shape attr]
(or (and (= attr :selrect)
(= (-> shape :selrect :width) (-> origin-shape :selrect :width))
(= (-> shape :selrect :height) (-> origin-shape :selrect :height)))
(and (= attr :points)
(let [normalize-pts (fn [pts]
(when (seq pts)
(let [f (first pts)]
(mapv #(gpt/subtract % f) pts))))]
(= (normalize-pts (get shape :points))
(normalize-pts (get origin-shape :points)))))))
(defn update-attrs-on-switch
"Copy attributes that have changed in the shape previous to the switch
@ -2104,8 +2132,9 @@
;; If the values are already equal, don't copy them
(= (get previous-shape attr) (get current-shape attr))
;; If the value is the same as the origin, don't copy it
(= (get previous-shape attr) (get origin-ref-shape attr))
;; If :selrect/:points values are already equal ignoring displacement,
;; don't copy them
(equal-geometry? previous-shape origin-ref-shape attr)
;; If the attr is not touched, don't copy it
(not (touched sync-group))
@ -2154,8 +2183,21 @@
skip-operations? (or skip-operations?
;; If we are going to reset the position data, skip the selrect attr
(and reset-pos-data? (= attr :selrect)))
(and reset-pos-data? (= attr :selrect))
;; Avoid copying composite geometry attrs (:selrect/:points) when the
;; variant dimensions differ but neither sizing is :fix. Without this,
;; :width/:height are correctly skipped by the check above
;; but :selrect/:points would still carry the old override dimensions,
;; leaving the shape in an inconsistent state. When :fix sizing is
;; present, switch-fixed-layout-geom-change-value handles the composite
;; attrs and must NOT be bypassed. Path shapes are also handled
;; separately via switch-path-change-value.
(and (contains? #{:selrect :points} attr)
(not path-change?)
(not (or (= :fix (:layout-item-h-sizing previous-shape))
(= :fix (:layout-item-v-sizing previous-shape))))
(or (not= (get origin-ref-shape :width) (get current-shape :width))
(not= (get origin-ref-shape :height) (get current-shape :height)))))
attr-val
(when-not skip-operations?
(cond
@ -2179,7 +2221,7 @@
(and (or (= :fix (:layout-item-h-sizing previous-shape))
(= :fix (:layout-item-v-sizing previous-shape)))
(contains? #{:points :selrect :width :height} attr))
(switch-fixed-layout-geom-change-value previous-shape current-shape attr)
(switch-fixed-layout-geom-change-value previous-shape current-shape origin-ref-shape attr)
:else
(get previous-shape attr)))

View File

@ -23,7 +23,7 @@
#?(:clj (set! *warn-on-reflection* true))
(def ^:cosnt bool-group-style-properties bool/group-style-properties)
(def ^:const bool-group-style-properties bool/group-style-properties)
(def ^:const bool-style-properties bool/style-properties)
(defn get-default-bool-fills
@ -79,7 +79,7 @@
(defn close-subpaths
"Given a content, searches a path for possible subpaths that can
create closed loops and merge them; then return the transformed path
conten as PathData instance"
content as PathData instance"
[content]
(-> (subpath/close-subpaths content)
(impl/from-plain)))

View File

@ -30,6 +30,18 @@
#?(:clj (set! *warn-on-reflection* true))
(def ^:const SEGMENT-U8-SIZE 28)
(defn- normalize-coord
"Normalize a coordinate value to be within safe integer bounds.
Clamps values greater than max-safe-int to max-safe-int,
and values less than min-safe-int to min-safe-int.
Always returns a double."
[v]
(cond
(> v sm/max-safe-int) (double sm/max-safe-int)
(< v sm/min-safe-int) (double sm/min-safe-int)
:else (double v)))
(def ^:const SEGMENT-U32-SIZE (/ SEGMENT-U8-SIZE 4))
(defprotocol IPathData
@ -121,12 +133,12 @@
(if (< index size)
(let [offset (* index SEGMENT-U8-SIZE)
type (buf/read-short buffer offset)
c1x (buf/read-float buffer (+ offset 4))
c1y (buf/read-float buffer (+ offset 8))
c2x (buf/read-float buffer (+ offset 12))
c2y (buf/read-float buffer (+ offset 16))
x (buf/read-float buffer (+ offset 20))
y (buf/read-float buffer (+ offset 24))
c1x (normalize-coord (buf/read-float buffer (+ offset 4)))
c1y (normalize-coord (buf/read-float buffer (+ offset 8)))
c2x (normalize-coord (buf/read-float buffer (+ offset 12)))
c2y (normalize-coord (buf/read-float buffer (+ offset 16)))
x (normalize-coord (buf/read-float buffer (+ offset 20)))
y (normalize-coord (buf/read-float buffer (+ offset 24)))
type (case type
1 :move-to
2 :line-to
@ -148,12 +160,12 @@
(if (< index size)
(let [offset (* index SEGMENT-U8-SIZE)
type (buf/read-short buffer offset)
c1x (buf/read-float buffer (+ offset 4))
c1y (buf/read-float buffer (+ offset 8))
c2x (buf/read-float buffer (+ offset 12))
c2y (buf/read-float buffer (+ offset 16))
x (buf/read-float buffer (+ offset 20))
y (buf/read-float buffer (+ offset 24))
c1x (normalize-coord (buf/read-float buffer (+ offset 4)))
c1y (normalize-coord (buf/read-float buffer (+ offset 8)))
c2x (normalize-coord (buf/read-float buffer (+ offset 12)))
c2y (normalize-coord (buf/read-float buffer (+ offset 16)))
x (normalize-coord (buf/read-float buffer (+ offset 20)))
y (normalize-coord (buf/read-float buffer (+ offset 24)))
type (case type
1 :move-to
2 :line-to
@ -172,12 +184,12 @@
[buffer index f]
(let [offset (* index SEGMENT-U8-SIZE)
type (buf/read-short buffer offset)
c1x (buf/read-float buffer (+ offset 4))
c1y (buf/read-float buffer (+ offset 8))
c2x (buf/read-float buffer (+ offset 12))
c2y (buf/read-float buffer (+ offset 16))
x (buf/read-float buffer (+ offset 20))
y (buf/read-float buffer (+ offset 24))
c1x (normalize-coord (buf/read-float buffer (+ offset 4)))
c1y (normalize-coord (buf/read-float buffer (+ offset 8)))
c2x (normalize-coord (buf/read-float buffer (+ offset 12)))
c2y (normalize-coord (buf/read-float buffer (+ offset 16)))
x (normalize-coord (buf/read-float buffer (+ offset 20)))
y (normalize-coord (buf/read-float buffer (+ offset 24)))
type (case type
1 :move-to
2 :line-to
@ -252,31 +264,31 @@
(let [offset (* index SEGMENT-U8-SIZE)
type (buf/read-short buffer offset)]
(case (long type)
1 (let [x (buf/read-float buffer (+ offset 20))
y (buf/read-float buffer (+ offset 24))]
1 (let [x (normalize-coord (buf/read-float buffer (+ offset 20)))
y (normalize-coord (buf/read-float buffer (+ offset 24)))]
{:command :move-to
:params {:x (double x)
:y (double y)}})
:params {:x x
:y y}})
2 (let [x (buf/read-float buffer (+ offset 20))
y (buf/read-float buffer (+ offset 24))]
2 (let [x (normalize-coord (buf/read-float buffer (+ offset 20)))
y (normalize-coord (buf/read-float buffer (+ offset 24)))]
{:command :line-to
:params {:x (double x)
:y (double y)}})
:params {:x x
:y y}})
3 (let [c1x (buf/read-float buffer (+ offset 4))
c1y (buf/read-float buffer (+ offset 8))
c2x (buf/read-float buffer (+ offset 12))
c2y (buf/read-float buffer (+ offset 16))
x (buf/read-float buffer (+ offset 20))
y (buf/read-float buffer (+ offset 24))]
3 (let [c1x (normalize-coord (buf/read-float buffer (+ offset 4)))
c1y (normalize-coord (buf/read-float buffer (+ offset 8)))
c2x (normalize-coord (buf/read-float buffer (+ offset 12)))
c2y (normalize-coord (buf/read-float buffer (+ offset 16)))
x (normalize-coord (buf/read-float buffer (+ offset 20)))
y (normalize-coord (buf/read-float buffer (+ offset 24)))]
{:command :curve-to
:params {:x (double x)
:y (double y)
:c1x (double c1x)
:c1y (double c1y)
:c2x (double c2x)
:c2y (double c2y)}})
:params {:x x
:y y
:c1x c1x
:c1y c1y
:c2x c2x
:c2y c2y}})
4 {:command :close-path
:params {}}
@ -462,7 +474,7 @@
nil))
(-nth [_ i default]
(if (d/in-range? i size)
(if (d/in-range? size i)
(read-segment buffer i)
default))
@ -666,8 +678,6 @@
(defn from-plain
"Create a PathData instance from plain data structures"
[segments]
(assert (check-plain-content segments))
(let [total (count segments)
buffer (buf/allocate (* total SEGMENT-U8-SIZE))]
(loop [index 0]
@ -677,30 +687,28 @@
(case (get segment :command)
:move-to
(let [params (get segment :params)
x (float (get params :x))
y (float (get params :y))]
x (normalize-coord (get params :x))
y (normalize-coord (get params :y))]
(buf/write-short buffer offset 1)
(buf/write-float buffer (+ offset 20) x)
(buf/write-float buffer (+ offset 24) y))
:line-to
(let [params (get segment :params)
x (float (get params :x))
y (float (get params :y))]
x (normalize-coord (get params :x))
y (normalize-coord (get params :y))]
(buf/write-short buffer offset 2)
(buf/write-float buffer (+ offset 20) x)
(buf/write-float buffer (+ offset 24) y))
:curve-to
(let [params (get segment :params)
x (float (get params :x))
y (float (get params :y))
c1x (float (get params :c1x x))
c1y (float (get params :c1y y))
c2x (float (get params :c2x x))
c2y (float (get params :c2y y))]
x (normalize-coord (get params :x))
y (normalize-coord (get params :y))
c1x (normalize-coord (get params :c1x x))
c1y (normalize-coord (get params :c1y y))
c2x (normalize-coord (get params :c2x x))
c2y (normalize-coord (get params :c2y y))]
(buf/write-short buffer offset 3)
(buf/write-float buffer (+ offset 4) c1x)
(buf/write-float buffer (+ offset 8) c1y)

View File

@ -62,7 +62,7 @@
(map (fn [[index _]] index))))
(defn handler-indices
"Return an index where the key is the positions and the values the handlers"
"Returns [[index prefix] ...] of all handlers associated with point."
[content point]
(->> (d/with-prev content)
(d/enumerate)
@ -76,7 +76,7 @@
[])))))
(defn opposite-index
"Calculates the opposite index given a prefix and an index"
"Calculates the opposite handler index given a content, index and prefix."
[content index prefix]
(let [point (if (= prefix :c2)

View File

@ -32,7 +32,7 @@
(d/without-keys shape dissoc-attrs))
(defn- make-corner-arc
"Creates a curvle corner for border radius"
"Creates a curve corner for border radius"
[from to corner radius]
(let [x (case corner
:top-left (:x from)

View File

@ -501,17 +501,15 @@
(defn backtrace-tokens-tree
"Convert tokens into a nested tree with their name as the path.
Generates a uuid per token to backtrace a token from an external source (StyleDictionary).
Uses the existing token :id to backtrace a token from an external source (StyleDictionary).
The backtrace can't be the name as the name might not exist when the user is creating a token."
[tokens]
(reduce
(fn [acc [_ token]]
(let [temp-id (random-uuid)
token (assoc token :temp/id temp-id)
path (get-token-path token)]
(let [path (get-token-path token)]
(-> acc
(assoc-in (concat [:tokens-tree] path) token)
(assoc-in [:ids temp-id] token))))
(assoc-in [:ids (:id token)] token))))
{:tokens-tree {} :ids {}}
tokens))

View File

@ -2257,4 +2257,469 @@
;; or if it needs recalculation, the test validates the behavior
(t/is (or (nil? old-position-data)
(nil? new-position-data)
(not= old-position-data new-position-data)))))
(not= old-position-data new-position-data)))))
;; ============================================================
;; SELRECT CONSISTENCY TESTS
;; These tests verify that after a variant switch, the composite
;; geometry attributes (:selrect, :points) stay consistent with
;; the scalar attributes (:width, :height) that are kept.
;; ============================================================
(t/deftest test-switch-selrect-consistent-no-sizing-different-widths
;; When no :fix sizing and variants have different widths,
;; :width is correctly skipped (stays at new component width),
;; but :selrect was being copied from the old shape, leaving
;; selrect.width inconsistent with :width. This test verifies the fix.
(let [;; ==== Setup
file (-> (thf/sample-file :file1)
(thv/add-variant-with-child
:v01 :c01 :m01 :c02 :m02 :r01 :r02
{:child1-params {:width 100 :height 50}
:child2-params {:width 200 :height 50}})
(thc/instantiate-component :c01
:copy01
:children-labels [:copy-r01]))
page (thf/current-page file)
copy01 (ths/get-shape file :copy01)
rect01 (get-in page [:objects (-> copy01 :shapes first)])
;; Override width AND selrect consistently (simulating a real resize)
changes (cls/generate-update-shapes (pcb/empty-changes nil (:id page))
#{(:id rect01)}
(fn [shape]
(let [new-width 150
sr (:selrect shape)
new-sr (-> sr
(assoc :width new-width)
(assoc :x2 (+ (:x1 sr) new-width)))]
(-> shape
(assoc :width new-width)
(assoc :selrect new-sr))))
(:objects page)
{})
file (thf/apply-changes file changes)
page (thf/current-page file)
rect01 (get-in page [:objects (:id rect01)])
;; ==== Action
file' (tho/swap-component file copy01 :c02 {:new-shape-label :copy02 :keep-touched? true})
page' (thf/current-page file')
copy02' (ths/get-shape file' :copy02)
rect02' (get-in page' [:objects (-> copy02' :shapes first)])]
;; The rect had the width override before the switch
(t/is (= (:width rect01) 150))
(t/is (= (get-in rect01 [:selrect :width]) 150))
;; Since the variants have different widths (100 vs 200), the override is not preserved
(t/is (= (:width rect02') 200))
;; The selrect must be consistent with :width
(t/is (= (get-in rect02' [:selrect :width]) 200))))
(t/deftest test-switch-selrect-consistent-no-sizing-different-heights
;; Same as above but for height.
(let [;; ==== Setup
file (-> (thf/sample-file :file1)
(thv/add-variant-with-child
:v01 :c01 :m01 :c02 :m02 :r01 :r02
{:child1-params {:width 50 :height 100}
:child2-params {:width 50 :height 200}})
(thc/instantiate-component :c01
:copy01
:children-labels [:copy-r01]))
page (thf/current-page file)
copy01 (ths/get-shape file :copy01)
rect01 (get-in page [:objects (-> copy01 :shapes first)])
;; Override height AND selrect consistently
changes (cls/generate-update-shapes (pcb/empty-changes nil (:id page))
#{(:id rect01)}
(fn [shape]
(let [new-height 150
sr (:selrect shape)
new-sr (-> sr
(assoc :height new-height)
(assoc :y2 (+ (:y1 sr) new-height)))]
(-> shape
(assoc :height new-height)
(assoc :selrect new-sr))))
(:objects page)
{})
file (thf/apply-changes file changes)
page (thf/current-page file)
rect01 (get-in page [:objects (:id rect01)])
;; ==== Action
file' (tho/swap-component file copy01 :c02 {:new-shape-label :copy02 :keep-touched? true})
page' (thf/current-page file')
copy02' (ths/get-shape file' :copy02)
rect02' (get-in page' [:objects (-> copy02' :shapes first)])]
;; The rect had the height override before the switch
(t/is (= (:height rect01) 150))
(t/is (= (get-in rect01 [:selrect :height]) 150))
;; Since the variants have different heights (100 vs 200), the override is not preserved
(t/is (= (:height rect02') 200))
;; The selrect must be consistent with :height
(t/is (= (get-in rect02' [:selrect :height]) 200))))
(t/deftest test-switch-with-v-sizing-fix-selrect-consistent-different-widths
;; mixed-sizing scenario: v-sizing=:fix but variants differ in WIDTH.
;; switch-fixed-layout-geom-change-value is triggered (because v-sizing=:fix).
;; Without the fix, the function returned prev-width for the non-:fix dimension,
;; leaving selrect.width inconsistent with :width.
(let [;; ==== Setup
file (-> (thf/sample-file :file1)
(thv/add-variant-with-child
:v01 :c01 :m01 :c02 :m02 :r01 :r02
{:child1-params {:width 100 :height 50 :layout-item-v-sizing :fix}
:child2-params {:width 200 :height 50 :layout-item-v-sizing :fix}})
(thc/instantiate-component :c01
:copy01
:children-labels [:copy-r01]))
page (thf/current-page file)
copy01 (ths/get-shape file :copy01)
rect01 (get-in page [:objects (-> copy01 :shapes first)])
;; Override width AND selrect consistently
changes (cls/generate-update-shapes (pcb/empty-changes nil (:id page))
#{(:id rect01)}
(fn [shape]
(let [new-width 150
sr (:selrect shape)
new-sr (-> sr
(assoc :width new-width)
(assoc :x2 (+ (:x1 sr) new-width)))]
(-> shape
(assoc :width new-width)
(assoc :selrect new-sr))))
(:objects page)
{})
file (thf/apply-changes file changes)
page (thf/current-page file)
rect01 (get-in page [:objects (:id rect01)])
;; ==== Action
file' (tho/swap-component file copy01 :c02 {:new-shape-label :copy02 :keep-touched? true})
page' (thf/current-page file')
copy02' (ths/get-shape file' :copy02)
rect02' (get-in page' [:objects (-> copy02' :shapes first)])]
;; The rect had the width override before the switch
(t/is (= (:width rect01) 150))
(t/is (= (get-in rect01 [:selrect :width]) 150))
;; Since the variants have different widths (100 vs 200), the override is not preserved
;; (v-sizing=:fix does not affect the horizontal dimension)
(t/is (= (:width rect02') 200))
;; The selrect must be consistent with :width
(t/is (= (get-in rect02' [:selrect :width]) 200))
;; v-sizing is preserved
(t/is (= (:layout-item-v-sizing rect02') :fix))))
(t/deftest test-switch-with-h-sizing-fix-selrect-consistent-different-heights
;; mixed-sizing scenario: h-sizing=:fix but variants differ in HEIGHT.
;; switch-fixed-layout-geom-change-value is triggered (because h-sizing=:fix).
;; Without the fix, the function returned prev-height for the non-:fix dimension,
;; leaving selrect.height inconsistent with :height.
(let [;; ==== Setup
file (-> (thf/sample-file :file1)
(thv/add-variant-with-child
:v01 :c01 :m01 :c02 :m02 :r01 :r02
{:child1-params {:width 50 :height 100 :layout-item-h-sizing :fix}
:child2-params {:width 50 :height 200 :layout-item-h-sizing :fix}})
(thc/instantiate-component :c01
:copy01
:children-labels [:copy-r01]))
page (thf/current-page file)
copy01 (ths/get-shape file :copy01)
rect01 (get-in page [:objects (-> copy01 :shapes first)])
;; Override height AND selrect consistently
changes (cls/generate-update-shapes (pcb/empty-changes nil (:id page))
#{(:id rect01)}
(fn [shape]
(let [new-height 150
sr (:selrect shape)
new-sr (-> sr
(assoc :height new-height)
(assoc :y2 (+ (:y1 sr) new-height)))]
(-> shape
(assoc :height new-height)
(assoc :selrect new-sr))))
(:objects page)
{})
file (thf/apply-changes file changes)
page (thf/current-page file)
rect01 (get-in page [:objects (:id rect01)])
;; ==== Action
file' (tho/swap-component file copy01 :c02 {:new-shape-label :copy02 :keep-touched? true})
page' (thf/current-page file')
copy02' (ths/get-shape file' :copy02)
rect02' (get-in page' [:objects (-> copy02' :shapes first)])]
;; The rect had the height override before the switch
(t/is (= (:height rect01) 150))
(t/is (= (get-in rect01 [:selrect :height]) 150))
;; Since the variants have different heights (100 vs 200), the override is not preserved
;; (h-sizing=:fix does not affect the vertical dimension)
(t/is (= (:height rect02') 200))
;; The selrect must be consistent with :height
(t/is (= (get-in rect02' [:selrect :height]) 200))
;; h-sizing is preserved
(t/is (= (:layout-item-h-sizing rect02') :fix))))
;; ============================================================
;; FIXED-SIZING: "SAME-SIZE → PRESERVE OVERRIDE" PATH TESTS
;; These tests exercise the branch inside switch-fixed-layout-geom-change-value
;; where variants share the same value in the non-:fix dimension:
;; (if (= origin-dim current-dim) prev-dim current-dim)
;; When origin-dim == current-dim the user's override for that dimension
;; must be preserved after the switch.
;; ============================================================
(t/deftest test-switch-with-h-sizing-fix-same-height-override-preserved
;; h-sizing=:fix, variants have SAME height (non-:fix dim, same-size).
;; switch-fixed-layout-geom-change-value must return prev-height for the
;; non-:fix dimension because origin-height == current-height.
(let [;; ==== Setup
file (-> (thf/sample-file :file1)
(thv/add-variant-with-child
:v01 :c01 :m01 :c02 :m02 :r01 :r02
{:child1-params {:width 100 :height 50 :layout-item-h-sizing :fix}
:child2-params {:width 200 :height 50 :layout-item-h-sizing :fix}})
(thc/instantiate-component :c01
:copy01
:children-labels [:copy-r01]))
page (thf/current-page file)
copy01 (ths/get-shape file :copy01)
rect01 (get-in page [:objects (-> copy01 :shapes first)])
;; Override height (the non-:fix dimension) and selrect consistently
changes (cls/generate-update-shapes (pcb/empty-changes nil (:id page))
#{(:id rect01)}
(fn [shape]
(let [new-height 75
sr (:selrect shape)
new-sr (-> sr
(assoc :height new-height)
(assoc :y2 (+ (:y1 sr) new-height)))]
(-> shape
(assoc :height new-height)
(assoc :selrect new-sr))))
(:objects page)
{})
file (thf/apply-changes file changes)
page (thf/current-page file)
rect01 (get-in page [:objects (:id rect01)])
;; ==== Action
file' (tho/swap-component file copy01 :c02 {:new-shape-label :copy02 :keep-touched? true})
page' (thf/current-page file')
copy02' (ths/get-shape file' :copy02)
rect02' (get-in page' [:objects (-> copy02' :shapes first)])]
;; The rect had the height override 75 before the switch
(t/is (= (:height rect01) 75))
;; h-sizing=:fix means width always takes the new component's value
(t/is (= (:width rect02') 200))
;; Height (non-:fix dim) is preserved because both variants have same height (50)
(t/is (= (:height rect02') 75))
;; selrect must be consistent with the preserved height
(t/is (= (get-in rect02' [:selrect :height]) 75))
(t/is (= (get-in rect02' [:selrect :width]) 200))
;; h-sizing is preserved
(t/is (= (:layout-item-h-sizing rect02') :fix))))
(t/deftest test-switch-with-v-sizing-fix-same-width-override-preserved
;; v-sizing=:fix, variants have SAME width (non-:fix dim, same-size).
;; switch-fixed-layout-geom-change-value must return prev-width for the
;; non-:fix dimension because origin-width == current-width.
(let [;; ==== Setup
file (-> (thf/sample-file :file1)
(thv/add-variant-with-child
:v01 :c01 :m01 :c02 :m02 :r01 :r02
{:child1-params {:width 100 :height 50 :layout-item-v-sizing :fix}
:child2-params {:width 100 :height 100 :layout-item-v-sizing :fix}})
(thc/instantiate-component :c01
:copy01
:children-labels [:copy-r01]))
page (thf/current-page file)
copy01 (ths/get-shape file :copy01)
rect01 (get-in page [:objects (-> copy01 :shapes first)])
;; Override width (the non-:fix dimension) and selrect consistently
changes (cls/generate-update-shapes (pcb/empty-changes nil (:id page))
#{(:id rect01)}
(fn [shape]
(let [new-width 150
sr (:selrect shape)
new-sr (-> sr
(assoc :width new-width)
(assoc :x2 (+ (:x1 sr) new-width)))]
(-> shape
(assoc :width new-width)
(assoc :selrect new-sr))))
(:objects page)
{})
file (thf/apply-changes file changes)
page (thf/current-page file)
rect01 (get-in page [:objects (:id rect01)])
;; ==== Action
file' (tho/swap-component file copy01 :c02 {:new-shape-label :copy02 :keep-touched? true})
page' (thf/current-page file')
copy02' (ths/get-shape file' :copy02)
rect02' (get-in page' [:objects (-> copy02' :shapes first)])]
;; The rect had the width override 150 before the switch
(t/is (= (:width rect01) 150))
;; Width (non-:fix dim) is preserved because both variants have same width (100)
(t/is (= (:width rect02') 150))
;; selrect must be consistent with the preserved width
(t/is (= (get-in rect02' [:selrect :width]) 150))
;; v-sizing=:fix means height always takes the new component's value
(t/is (= (:height rect02') 100))
(t/is (= (get-in rect02' [:selrect :height]) 100))
;; v-sizing is preserved
(t/is (= (:layout-item-v-sizing rect02') :fix))))
(t/deftest test-switch-with-both-sizing-fix-overrides-discarded
;; When both h-sizing=:fix and v-sizing=:fix, switch-fixed-layout-geom-change-value
;; always uses current-width and current-height (the new component's values).
;; Both width and height overrides are discarded because :fix always
;; defers to the new component's dimension regardless of same-size or not.
(let [;; ==== Setup
file (-> (thf/sample-file :file1)
(thv/add-variant-with-child
:v01 :c01 :m01 :c02 :m02 :r01 :r02
{:child1-params {:width 100 :height 50
:layout-item-h-sizing :fix
:layout-item-v-sizing :fix}
:child2-params {:width 200 :height 100
:layout-item-h-sizing :fix
:layout-item-v-sizing :fix}})
(thc/instantiate-component :c01
:copy01
:children-labels [:copy-r01]))
page (thf/current-page file)
copy01 (ths/get-shape file :copy01)
rect01 (get-in page [:objects (-> copy01 :shapes first)])
;; Override both width and height (and selrect) consistently
changes (cls/generate-update-shapes (pcb/empty-changes nil (:id page))
#{(:id rect01)}
(fn [shape]
(let [new-width 150
new-height 75
sr (:selrect shape)
new-sr (-> sr
(assoc :width new-width)
(assoc :height new-height)
(assoc :x2 (+ (:x1 sr) new-width))
(assoc :y2 (+ (:y1 sr) new-height)))]
(-> shape
(assoc :width new-width)
(assoc :height new-height)
(assoc :selrect new-sr))))
(:objects page)
{})
file (thf/apply-changes file changes)
page (thf/current-page file)
rect01 (get-in page [:objects (:id rect01)])
;; ==== Action
file' (tho/swap-component file copy01 :c02 {:new-shape-label :copy02 :keep-touched? true})
page' (thf/current-page file')
copy02' (ths/get-shape file' :copy02)
rect02' (get-in page' [:objects (-> copy02' :shapes first)])]
;; The rect had both overrides before the switch
(t/is (= (:width rect01) 150))
(t/is (= (:height rect01) 75))
;; With both sizing :fix, both dimensions take the new component's values
(t/is (= (:width rect02') 200))
(t/is (= (:height rect02') 100))
;; selrect must be consistent
(t/is (= (get-in rect02' [:selrect :width]) 200))
(t/is (= (get-in rect02' [:selrect :height]) 100))
(t/is (= (:layout-item-h-sizing rect02') :fix))
(t/is (= (:layout-item-v-sizing rect02') :fix))))
(t/deftest test-switch-same-size-variants-geometry-override-preserved
;; When both variants have IDENTICAL dimensions (width=100, height=50),
;; the guard that skips :selrect/:points must NOT fire
;; (its condition `(or (not= origin.width current.width) ...)` is false).
;; A geometry override should therefore be carried through correctly.
(let [;; ==== Setup
file (-> (thf/sample-file :file1)
(thv/add-variant-with-child
:v01 :c01 :m01 :c02 :m02 :r01 :r02
{:child1-params {:width 100 :height 50}
:child2-params {:width 100 :height 50}}) ; same size!
(thc/instantiate-component :c01
:copy01
:children-labels [:copy-r01]))
page (thf/current-page file)
copy01 (ths/get-shape file :copy01)
rect01 (get-in page [:objects (-> copy01 :shapes first)])
;; Override width AND selrect consistently (simulating a real resize)
changes (cls/generate-update-shapes (pcb/empty-changes nil (:id page))
#{(:id rect01)}
(fn [shape]
(let [new-width 150
sr (:selrect shape)
new-sr (-> sr
(assoc :width new-width)
(assoc :x2 (+ (:x1 sr) new-width)))]
(-> shape
(assoc :width new-width)
(assoc :selrect new-sr))))
(:objects page)
{})
file (thf/apply-changes file changes)
page (thf/current-page file)
rect01 (get-in page [:objects (:id rect01)])
;; ==== Action
file' (tho/swap-component file copy01 :c02 {:new-shape-label :copy02 :keep-touched? true})
page' (thf/current-page file')
copy02' (ths/get-shape file' :copy02)
rect02' (get-in page' [:objects (-> copy02' :shapes first)])]
;; The rect had the width override 150 before the switch
(t/is (= (:width rect01) 150))
(t/is (= (get-in rect01 [:selrect :width]) 150))
;; Both variants are identical in size (100x50), so the override IS preserved
(t/is (= (:width rect02') 150))
;; The guard must not have suppressed :selrect — it should be consistent
(t/is (= (get-in rect02' [:selrect :width]) 150))))

View File

@ -13,6 +13,7 @@
[app.common.geom.rect :as grc]
[app.common.math :as mth]
[app.common.pprint :as pp]
[app.common.schema :as sm]
[app.common.transit :as trans]
[app.common.types.path :as path]
[app.common.types.path.bool :as path.bool]
@ -1418,3 +1419,60 @@
;; Verify first and last entries specifically
(t/is (= :move-to (first seq-types)))
(t/is (= :close-path (last seq-types))))))
(t/deftest path-data-read-normalizes-out-of-bounds-coordinates
(let [max-safe (double sm/max-safe-int)
min-safe (double sm/min-safe-int)
;; Create content with values exceeding safe bounds
content-with-out-of-bounds
[{:command :move-to :params {:x (+ max-safe 1000.0) :y (- min-safe 1000.0)}}
{:command :line-to :params {:x (- min-safe 500.0) :y (+ max-safe 500.0)}}
{:command :curve-to :params
{:c1x (+ max-safe 200.0) :c1y (- min-safe 200.0)
:c2x (+ max-safe 300.0) :c2y (- min-safe 300.0)
:x (+ max-safe 400.0) :y (- min-safe 400.0)}}
{:command :close-path :params {}}]
;; Create PathData from the content
pdata (path/content content-with-out-of-bounds)
;; Read it back
result (vec pdata)]
(t/testing "Coordinates exceeding max-safe-int are clamped to max-safe-int"
(let [move-to (first result)
line-to (second result)]
(t/is (= max-safe (:x (:params move-to))) "x in move-to should be clamped to max-safe-int")
(t/is (= min-safe (:y (:params move-to))) "y in move-to should be clamped to min-safe-int")
(t/is (= min-safe (:x (:params line-to))) "x in line-to should be clamped to min-safe-int")
(t/is (= max-safe (:y (:params line-to))) "y in line-to should be clamped to max-safe-int")))
(t/testing "Curve-to coordinates are clamped"
(let [curve-to (nth result 2)]
(t/is (= max-safe (:c1x (:params curve-to))) "c1x should be clamped")
(t/is (= min-safe (:c1y (:params curve-to))) "c1y should be clamped")
(t/is (= max-safe (:c2x (:params curve-to))) "c2x should be clamped")
(t/is (= min-safe (:c2y (:params curve-to))) "c2y should be clamped")
(t/is (= max-safe (:x (:params curve-to))) "x should be clamped")
(t/is (= min-safe (:y (:params curve-to))) "y should be clamped")))
(t/testing "-lookup normalizes coordinates"
(let [move-to (path.impl/-lookup pdata 0 (fn [_ _ _ _ _ x y] {:x x :y y}))]
(t/is (= max-safe (:x move-to)) "lookup x should be clamped")
(t/is (= min-safe (:y move-to)) "lookup y should be clamped")))
(t/testing "-walk normalizes coordinates"
(let [coords (path.impl/-walk pdata
(fn [_ _ _ _ _ x y]
(when (and x y) {:x x :y y}))
[])]
(t/is (= max-safe (:x (first coords))) "walk first x should be clamped")
(t/is (= min-safe (:y (first coords))) "walk first y should be clamped")))
(t/testing "-reduce normalizes coordinates"
(let [[move-res] (path.impl/-reduce pdata
(fn [acc _ _ _ _ _ _ x y]
(if (and x y) (conj acc {:x x :y y}) acc))
[])]
(t/is (= max-safe (:x move-res)) "reduce first x should be clamped")
(t/is (= min-safe (:y move-res)) "reduce first y should be clamped")))))

View File

@ -551,7 +551,7 @@
(.. sd-token -original -name))
(defn sd-token-uuid [^js sd-token]
(uuid (.-uuid (.-id ^js sd-token))))
(uuid (.-uuid (.. sd-token -original -id))))
(defn resolve-tokens
[tokens]
@ -560,15 +560,23 @@
(defn resolve-tokens-interactive
"Interactive check of resolving tokens.
Uses a ids map to backtrace the original token from the resolved StyleDictionary token.
Uses a ids map to backtrace the original token from the resolved
StyleDictionary token.
We have to pass in all tokens from all sets in the entire library to style dictionary
so we know if references are missing / to resolve them and possibly show interactive previews (in the tokens form) to the user.
We have to pass in all tokens from all sets in the entire library to
style dictionary so we know if references are missing / to resolve
them and possibly show interactive previews (in the tokens form) to
the user.
Since we're using the :name path as the identifier we might be throwing away or overriding tokens in the tree that we pass to StyleDictionary.
Since we're using the :name path as the identifier we might be
throwing away or overriding tokens in the tree that we pass to
StyleDictionary.
So to get back the original token from the resolved sd-token (see my updates for what an sd-token is) we include a temporary :id for the token that we pass to StyleDictionary,
this way after the resolving computation we can restore any token, even clashing ones with the same :name path by just looking up that :id in the ids map."
So to get back the original token from the resolved sd-token (see my
updates for what an sd-token is) we include a temporary :id for the
token that we pass to StyleDictionary, this way after the resolving
computation we can restore any token, even clashing ones with the
same :name path by just looking up that :id in the ids map."
[tokens]
(let [{:keys [tokens-tree ids]} (ctob/backtrace-tokens-tree tokens)]
(resolve-tokens-tree tokens-tree #(get ids (sd-token-uuid %)))))
@ -584,10 +592,11 @@
(defonce !tokens-cache (atom nil))
(defn use-resolved-tokens
"The StyleDictionary process function is async, so we can't use resolved values directly.
"The StyleDictionary process function is async, so we can't use
resolved values directly.
This hook will return the unresolved tokens as state until they are processed,
then the state will be updated with the resolved tokens."
This hook will return the unresolved tokens as state until they are
processed, then the state will be updated with the resolved tokens."
[tokens & {:keys [cache-atom interactive?]
:or {cache-atom !tokens-cache}
:as config}]

View File

@ -57,3 +57,29 @@
(t/is (= :error.token/number-too-large
(get-in resolved-tokens ["borderRadius.largeFn" :errors 0 :error/code])))
(done))))))))
(t/deftest resolve-tokens-interactive-test
(t/async
done
(t/testing "resolves tokens interactively using backtrace ids map"
(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 "borderRadius.sm"
:value "12px"
:type :border-radius}))
(ctob/add-token (cthi/id :core-set)
(ctob/make-token {:value "{borderRadius.sm} * 2"
:name "borderRadius.md"
:type :border-radius}))
(ctob/get-all-tokens-map))]
(-> (sd/resolve-tokens-interactive tokens)
(rx/sub!
(fn [resolved-tokens]
(t/is (= 12 (get-in resolved-tokens ["borderRadius.sm" :resolved-value])))
(t/is (= "px" (get-in resolved-tokens ["borderRadius.sm" :unit])))
(t/is (= 24 (get-in resolved-tokens ["borderRadius.md" :resolved-value])))
(t/is (= "px" (get-in resolved-tokens ["borderRadius.md" :unit])))
(done))))))))

View File

@ -19,6 +19,6 @@
"@github/copilot": "^1.0.21",
"@types/node": "^25.5.2",
"esbuild": "^0.28.0",
"opencode-ai": "^1.4.0"
"opencode-ai": "^1.4.3"
}
}

View File

@ -1,12 +1,13 @@
## 1.5.0 (Unreleased)
- **plugin-types**: Added a flags subcontexts with the flag `naturalChildrenOrdering`
- **plugins-runtime**: Added `version` field that returns the current version
- **plugin-types**: Added a flags subcontexts with the flag `naturalChildrenOrdering`
- **plugin-types**: Fix penpot.openPage() to navigate in same tab by default
- **plugin-types**: Added `createVariantFromComponents`
- **plugin-types**: Change return type of `combineAsVariants`
- **plugin-types**: Added `textBounds` property for text shapes
- **plugin-types**: Added flag `throwValidationErrors` to enable exceptions on validation
- **plugin-types**: Fix missing `webp` export format in `Export.type`
## 1.4.2 (2026-01-21)

View File

@ -1548,9 +1548,9 @@ export interface EventsMap {
*/
export interface Export {
/**
* Type of the file to export. Can be one of the following values: png, jpeg, svg, pdf
* Type of the file to export. Can be one of the following values: png, jpeg, webp, svg, pdf
*/
type: 'png' | 'jpeg' | 'svg' | 'pdf';
type: 'png' | 'jpeg' | 'webp' | 'svg' | 'pdf';
/**
* For bitmap formats represent the scale of the original size to resize the export
*/

106
pnpm-lock.yaml generated
View File

@ -18,8 +18,8 @@ importers:
specifier: ^0.28.0
version: 0.28.0
opencode-ai:
specifier: ^1.4.0
version: 1.4.0
specifier: ^1.4.3
version: 1.4.3
packages:
@ -227,67 +227,67 @@ packages:
engines: {node: '>=18'}
hasBin: true
opencode-ai@1.4.0:
resolution: {integrity: sha512-Cb5Vo5Rl1gvOIXC8gtgupwoa5+rufsp+6u5tIxIYLl5fReX+P2eugLSOkKH2KB5GC6BwxaEvapEZiPvQYsZSXA==}
opencode-ai@1.4.3:
resolution: {integrity: sha512-WwCSrLgJiS+sLIWoi9pa62vAw3l6VI3a+ShhjDDMUJBBG2FxU18xEhk8xhEedLMKyHo1p0nwD41+iKZ1y+rdAw==}
hasBin: true
opencode-darwin-arm64@1.4.0:
resolution: {integrity: sha512-rXdrH1Oejb+220ZCzkd1P+tCP7IhLTyfRbUr89vzvEWVRueh0vr2hvyrGDVv9LAskZAt/hwY3Wnw9CzjtxocdQ==}
opencode-darwin-arm64@1.4.3:
resolution: {integrity: sha512-d/MT28Is5yhdFY+36AqKc5r31zx8lXTQIYblfn5R8kdhamXijZVGdD0pHl3eJc1ZolUHNwzg2B+IqV22uyU9GQ==}
cpu: [arm64]
os: [darwin]
opencode-darwin-x64-baseline@1.4.0:
resolution: {integrity: sha512-5xCXF8Xn9M2WQKZATc4llm9qrAc4JodmQj88Oibbz/rXIOr/A1ejXvaeqLOQkQyQweeEABlYXOf3eCiY5hx8Gw==}
opencode-darwin-x64-baseline@1.4.3:
resolution: {integrity: sha512-WTqf7WBNRZcv6pClqnN4F7X/T/osgcPGikNHkHUSLszKWg9flqz7Z68kHR4i9ae8Bn3ke9MQRgzRdOt2PgLL0w==}
cpu: [x64]
os: [darwin]
opencode-darwin-x64@1.4.0:
resolution: {integrity: sha512-PhBfT2EtPos7jcGBtVSz3+yKv2e1nQy1UrXiH4ILdSgwzroKU/0kMsRtWJeMPHIj1imUQmSVlnDcuxiCiCkozw==}
opencode-darwin-x64@1.4.3:
resolution: {integrity: sha512-8FUHeybVmaCYt4S2YmWcf32o/xa/ahCfI258bpWssrzs7Xg51JgUB/Csoble0I1mH7RpW39SKy/hHUtHGuJfJg==}
cpu: [x64]
os: [darwin]
opencode-linux-arm64-musl@1.4.0:
resolution: {integrity: sha512-1lc0Nj6OmtJF5NJn+AhV7rXOHzw+0p7rvXQu2iqd9V7DpUEbltyF6oiyuF54gBZjHpvSzFXu8a3YeTcuMEXdNA==}
opencode-linux-arm64-musl@1.4.3:
resolution: {integrity: sha512-3Ej2klaep8+fxcc44UyEuRpb/UFiNkwfzIDLIST83hFUtjzprjpTRqg6zHmOfzyfjNAaNpB4VZw6e9y3mGBpiQ==}
cpu: [arm64]
os: [linux]
opencode-linux-arm64@1.4.0:
resolution: {integrity: sha512-XEM3tP7DTrYDEYCe9jqC/xtgzPJpCZTfinc5DjcPuh2hx+iHCGSr9+qG7tRGeCyvD9ibAFewNtpco5Is49JCrg==}
opencode-linux-arm64@1.4.3:
resolution: {integrity: sha512-9jpVSOEF7TX3gPPAHVAsBT9XEO3LgYafI+IUmOzbBB9CDiVVNJw6JmEffmSpSxY4nkAh322xnMbNjVGEyXQBRA==}
cpu: [arm64]
os: [linux]
opencode-linux-x64-baseline-musl@1.4.0:
resolution: {integrity: sha512-URg1JXIUaNz0R4TLUT98rK2jozmh5ScAkkqxPK6LWj3XwJojJx23mJRNgLb26034PgNkUwXhrtdbnyPTSVlkqQ==}
opencode-linux-x64-baseline-musl@1.4.3:
resolution: {integrity: sha512-aned/3FQTHXXQv2PPKDprJwQaQkoadriQ6AByGhRl6/bHhSkhkiVl6cHHvYMKxYEwN4bVOydWhasfgm/xru/xw==}
cpu: [x64]
os: [linux]
opencode-linux-x64-baseline@1.4.0:
resolution: {integrity: sha512-GocjLGNgs41PLgSVPWxT3Do0StZkDB9QF3e3VIIAGzPmOVcpTZLdDvJPkZdRbRGcVfUcSRGquBbBgvwK9Zsw4w==}
opencode-linux-x64-baseline@1.4.3:
resolution: {integrity: sha512-HpzdgYaI90qqt0WokcyBhadgFQ0EYMhq4TZ4EcaSPuZTssS2Drb6kp70Si54uOJL/MUAdc9+E0BYYIAdOJ6h1g==}
cpu: [x64]
os: [linux]
opencode-linux-x64-musl@1.4.0:
resolution: {integrity: sha512-yGb1uNO++BtkZ7X/LGLax9ppaEvsmn5s5GaAqcrYj/SyJA5cL2IYzEeMYRAsrb0b81fQCSq5SLEiWiMq1o59oA==}
opencode-linux-x64-musl@1.4.3:
resolution: {integrity: sha512-ibUevyDxVrwkp6FWu8UBCBsrzlKDT/uEug2NHCKaHIwo9uwVf5zsL/0ueHYqmH14SHK+M6wzWewYk6WuW9f0zQ==}
cpu: [x64]
os: [linux]
opencode-linux-x64@1.4.0:
resolution: {integrity: sha512-Ops08slOBhHbKaYhERH8zMTjlM6mearVaA0udCDIx2fGqDbZRisoRyyI6Z44GPYBH02w8eGmvOvnF5fQYyq2fw==}
opencode-linux-x64@1.4.3:
resolution: {integrity: sha512-RS6TsDqTUrW5sefxD1KD9Xy9mSYGXAlr2DlGrdi8vNm9e/Bt4r4u557VB7f/Uj2CxTt2Gf7OWl08ZoPlxMJ5Gg==}
cpu: [x64]
os: [linux]
opencode-windows-arm64@1.4.0:
resolution: {integrity: sha512-47quWER7bCGRPWRXd3fsOyu5F/T4Y65FiS05kD+PYYV4iOJymlBQ34kpcJhNBOpQLYf9HSLbJ8AaJeb5dmUi+Q==}
opencode-windows-arm64@1.4.3:
resolution: {integrity: sha512-2ViH17WpIQbRVfQaOBMi49pu73gqTQYT/4/WxFjShmRagX40/KkG18fhvyDAZrBKfkhPtdwgFsFxMSYP9F6QCQ==}
cpu: [arm64]
os: [win32]
opencode-windows-x64-baseline@1.4.0:
resolution: {integrity: sha512-eGK9lF70XKzf9zBO7xil9+Vl7ZJUAgLK6bG6kug6RKxD6FsydY3Y6q/3tIW0+YZ0wyINOtEbTRfUHbO5TxV4FQ==}
opencode-windows-x64-baseline@1.4.3:
resolution: {integrity: sha512-SWYDli9SAKQd/pS/hVfuq1KEsc+gnAJdv+YtBmxaHOw57y0euqLwbGFUYFq78GAMGt/RnTYWZIEUbRK/ZiX3UA==}
cpu: [x64]
os: [win32]
opencode-windows-x64@1.4.0:
resolution: {integrity: sha512-DQ8CoxCsmFM38U1e73+hFuB6Wu0tbn6B4R7KwcL1JhvKvQaYYiukNfuLgcjjx5D7s81NP1SWlv6lw60wN0gq8g==}
opencode-windows-x64@1.4.3:
resolution: {integrity: sha512-UxmKDIw3t4XHST6JSUWHmSrCGIEK1LRTAOpO82HBC3XkIjH78gVIeauRR6RULjWAApmy9I1C3TukO2sDUi7Gvw==}
cpu: [x64]
os: [win32]
@ -434,55 +434,55 @@ snapshots:
'@esbuild/win32-ia32': 0.28.0
'@esbuild/win32-x64': 0.28.0
opencode-ai@1.4.0:
opencode-ai@1.4.3:
optionalDependencies:
opencode-darwin-arm64: 1.4.0
opencode-darwin-x64: 1.4.0
opencode-darwin-x64-baseline: 1.4.0
opencode-linux-arm64: 1.4.0
opencode-linux-arm64-musl: 1.4.0
opencode-linux-x64: 1.4.0
opencode-linux-x64-baseline: 1.4.0
opencode-linux-x64-baseline-musl: 1.4.0
opencode-linux-x64-musl: 1.4.0
opencode-windows-arm64: 1.4.0
opencode-windows-x64: 1.4.0
opencode-windows-x64-baseline: 1.4.0
opencode-darwin-arm64: 1.4.3
opencode-darwin-x64: 1.4.3
opencode-darwin-x64-baseline: 1.4.3
opencode-linux-arm64: 1.4.3
opencode-linux-arm64-musl: 1.4.3
opencode-linux-x64: 1.4.3
opencode-linux-x64-baseline: 1.4.3
opencode-linux-x64-baseline-musl: 1.4.3
opencode-linux-x64-musl: 1.4.3
opencode-windows-arm64: 1.4.3
opencode-windows-x64: 1.4.3
opencode-windows-x64-baseline: 1.4.3
opencode-darwin-arm64@1.4.0:
opencode-darwin-arm64@1.4.3:
optional: true
opencode-darwin-x64-baseline@1.4.0:
opencode-darwin-x64-baseline@1.4.3:
optional: true
opencode-darwin-x64@1.4.0:
opencode-darwin-x64@1.4.3:
optional: true
opencode-linux-arm64-musl@1.4.0:
opencode-linux-arm64-musl@1.4.3:
optional: true
opencode-linux-arm64@1.4.0:
opencode-linux-arm64@1.4.3:
optional: true
opencode-linux-x64-baseline-musl@1.4.0:
opencode-linux-x64-baseline-musl@1.4.3:
optional: true
opencode-linux-x64-baseline@1.4.0:
opencode-linux-x64-baseline@1.4.3:
optional: true
opencode-linux-x64-musl@1.4.0:
opencode-linux-x64-musl@1.4.3:
optional: true
opencode-linux-x64@1.4.0:
opencode-linux-x64@1.4.3:
optional: true
opencode-windows-arm64@1.4.0:
opencode-windows-arm64@1.4.3:
optional: true
opencode-windows-x64-baseline@1.4.0:
opencode-windows-x64-baseline@1.4.3:
optional: true
opencode-windows-x64@1.4.0:
opencode-windows-x64@1.4.3:
optional: true
undici-types@7.18.2: {}