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

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

View File

@ -24,6 +24,25 @@
- Fix text editor v1 focus [Taiga #13961](https://tree.taiga.io/project/penpot/issue/13961) - 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 ## 2.14.2
### :sparkles: New features & Enhancements ### :sparkles: New features & Enhancements

View File

@ -2016,14 +2016,17 @@
(defn- switch-fixed-layout-geom-change-value (defn- switch-fixed-layout-geom-change-value
[prev-shape ; The shape before the switch [prev-shape ; The shape before the switch
current-shape ; The shape after the switch (a clean copy) current-shape ; The shape after the switch (a clean copy)
origin-shape ; The original shape
attr] attr]
;; When there is a layout with fixed h or v sizing, we need ;; When there is a layout with fixed h or v sizing, we need
;; to keep the width/height (and recalculate selrect and points) ;; to keep the width/height (and recalculate selrect and points)
(let [prev-width (-> prev-shape :selrect :width) (let [prev-width (-> prev-shape :selrect :width)
current-width (-> current-shape :selrect :width) current-width (-> current-shape :selrect :width)
origin-width (-> origin-shape :selrect :width)
prev-height (-> prev-shape :selrect :height) prev-height (-> prev-shape :selrect :height)
current-height (-> current-shape :selrect :height) current-height (-> current-shape :selrect :height)
origin-height (-> origin-shape :selrect :height)
x (-> current-shape :selrect :x) x (-> current-shape :selrect :x)
y (-> current-shape :selrect :y) y (-> current-shape :selrect :y)
@ -2034,10 +2037,16 @@
final-width (if (= :fix h-sizing) final-width (if (= :fix h-sizing)
current-width 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) final-height (if (= :fix v-sizing)
current-height 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) selrect (assoc (:selrect current-shape)
:width final-width :width final-width
:height final-height :height final-height
@ -2066,6 +2075,25 @@
(or (:transform current-shape) (gmt/matrix))))))) (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 (defn update-attrs-on-switch
"Copy attributes that have changed in the shape previous to the 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 ;; If the values are already equal, don't copy them
(= (get previous-shape attr) (get current-shape attr)) (= (get previous-shape attr) (get current-shape attr))
;; If the value is the same as the origin, don't copy it ;; If :selrect/:points values are already equal ignoring displacement,
(= (get previous-shape attr) (get origin-ref-shape attr)) ;; don't copy them
(equal-geometry? previous-shape origin-ref-shape attr)
;; If the attr is not touched, don't copy it ;; If the attr is not touched, don't copy it
(not (touched sync-group)) (not (touched sync-group))
@ -2154,8 +2183,21 @@
skip-operations? (or skip-operations? skip-operations? (or skip-operations?
;; If we are going to reset the position data, skip the selrect attr ;; 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 attr-val
(when-not skip-operations? (when-not skip-operations?
(cond (cond
@ -2179,7 +2221,7 @@
(and (or (= :fix (:layout-item-h-sizing previous-shape)) (and (or (= :fix (:layout-item-h-sizing previous-shape))
(= :fix (:layout-item-v-sizing previous-shape))) (= :fix (:layout-item-v-sizing previous-shape)))
(contains? #{:points :selrect :width :height} attr)) (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 :else
(get previous-shape attr))) (get previous-shape attr)))

View File

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

View File

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

View File

@ -62,7 +62,7 @@
(map (fn [[index _]] index)))) (map (fn [[index _]] index))))
(defn handler-indices (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] [content point]
(->> (d/with-prev content) (->> (d/with-prev content)
(d/enumerate) (d/enumerate)
@ -76,7 +76,7 @@
[]))))) [])))))
(defn opposite-index (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] [content index prefix]
(let [point (if (= prefix :c2) (let [point (if (= prefix :c2)

View File

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

View File

@ -489,17 +489,15 @@
(defn backtrace-tokens-tree (defn backtrace-tokens-tree
"Convert tokens into a nested tree with their name as the path. "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." The backtrace can't be the name as the name might not exist when the user is creating a token."
[tokens] [tokens]
(reduce (reduce
(fn [acc [_ token]] (fn [acc [_ token]]
(let [temp-id (random-uuid) (let [path (get-token-path token)]
token (assoc token :temp/id temp-id)
path (get-token-path token)]
(-> acc (-> acc
(assoc-in (concat [:tokens-tree] path) token) (assoc-in (concat [:tokens-tree] path) token)
(assoc-in [:ids temp-id] token)))) (assoc-in [:ids (:id token)] token))))
{:tokens-tree {} :ids {}} {:tokens-tree {} :ids {}}
tokens)) tokens))

View File

@ -2257,4 +2257,469 @@
;; or if it needs recalculation, the test validates the behavior ;; or if it needs recalculation, the test validates the behavior
(t/is (or (nil? old-position-data) (t/is (or (nil? old-position-data)
(nil? new-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.geom.rect :as grc]
[app.common.math :as mth] [app.common.math :as mth]
[app.common.pprint :as pp] [app.common.pprint :as pp]
[app.common.schema :as sm]
[app.common.transit :as trans] [app.common.transit :as trans]
[app.common.types.path :as path] [app.common.types.path :as path]
[app.common.types.path.bool :as path.bool] [app.common.types.path.bool :as path.bool]
@ -1418,3 +1419,60 @@
;; Verify first and last entries specifically ;; Verify first and last entries specifically
(t/is (= :move-to (first seq-types))) (t/is (= :move-to (first seq-types)))
(t/is (= :close-path (last 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)) (.. sd-token -original -name))
(defn sd-token-uuid [^js sd-token] (defn sd-token-uuid [^js sd-token]
(uuid (.-uuid (.-id ^js sd-token)))) (uuid (.-uuid (.. sd-token -original -id))))
(defn resolve-tokens (defn resolve-tokens
[tokens] [tokens]
@ -560,15 +560,23 @@
(defn resolve-tokens-interactive (defn resolve-tokens-interactive
"Interactive check of resolving tokens. "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 We have to pass in all tokens from all sets in the entire library to
so we know if references are missing / to resolve them and possibly show interactive previews (in the tokens form) to the user. 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, So to get back the original token from the resolved sd-token (see my
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." 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] [tokens]
(let [{:keys [tokens-tree ids]} (ctob/backtrace-tokens-tree tokens)] (let [{:keys [tokens-tree ids]} (ctob/backtrace-tokens-tree tokens)]
(resolve-tokens-tree tokens-tree #(get ids (sd-token-uuid %))))) (resolve-tokens-tree tokens-tree #(get ids (sd-token-uuid %)))))
@ -584,10 +592,11 @@
(defonce !tokens-cache (atom nil)) (defonce !tokens-cache (atom nil))
(defn use-resolved-tokens (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, This hook will return the unresolved tokens as state until they are
then the state will be updated with the resolved tokens." processed, then the state will be updated with the resolved tokens."
[tokens & {:keys [cache-atom interactive?] [tokens & {:keys [cache-atom interactive?]
:or {cache-atom !tokens-cache} :or {cache-atom !tokens-cache}
:as config}] :as config}]

View File

@ -57,3 +57,29 @@
(t/is (= :error.token/number-too-large (t/is (= :error.token/number-too-large
(get-in resolved-tokens ["borderRadius.largeFn" :errors 0 :error/code]))) (get-in resolved-tokens ["borderRadius.largeFn" :errors 0 :error/code])))
(done)))))))) (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", "@github/copilot": "^1.0.21",
"@types/node": "^25.5.2", "@types/node": "^25.5.2",
"esbuild": "^0.28.0", "esbuild": "^0.28.0",
"opencode-ai": "^1.4.0" "opencode-ai": "^1.4.3"
} }
} }

View File

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

View File

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