🐛 Fix problems with plugins API (#10412)

*  Adds static dispatch safe stubs in tests

* 🐛 Fix shapesColors metadata key to match ColorShapeInfo

* 🐛 Fix CommentThread.remove rejecting the owner's own threads

* 🐛 Fix page.removeCommentThread throwing on a spurious Promise

*  Implement ShapeBase.swapComponent in the plugin API

*  Expose File.revn in the plugin API

* 🐛 Fix FileVersion.createdAt calling Luxon method on a js/Date

* 🐛 Fix plugin font/typography application to text and ranges

* 🐛 Default plugin overlay interaction position for non-manual types

* 🐛 Fix plugin interaction setters passing an id-only shape

* 🐛 Fix grid addColumnAtIndex rejecting valid track types

* 🐛 Expose libraryId on library color/typography/component proxies

*  Implement LibraryTypography.setFont in the plugin API

* 🐛 Fix typography.applyToTextRange reading unexposed range bounds

* 🐛 Fix utils.geometry.center argument mismatch

* 🐛 Fix localStorage.removeItem calling getItem

* 🐛 Fix shape backgroundBlur proxy key casing

* 🐛 Report boolean shape type as 'boolean' in the plugin API

* 🐛 Return the resulting paths from plugin flatten

* 🐛 Make plugin z-order methods act on the target shape

* 🐛 Make is-variant-container? return a boolean

*  Implement Group.isMask in the plugin API

* 🐛 Return a shape proxy from TextRange.shape

* 🐛 Return the duplicated set from TokenSet.duplicate

* 🐛 Fix theme addSet/removeSet reading set name with a keyword

* 🐛 Accept string fontFamilies token value in the plugin API

* 🐛 Fix combineAsVariants ignoring the passed component ids

* 🐛 Fix board removeRulerGuide ignoring its argument

* 🐛 Fix board guides setter schema and parser

* 🐛 Avoid 0-byte allocation when syncing empty grid tracks

* 🐛 Validate grid track indices in the plugin API

* 🐛 Return null for empty input in group() and centerShapes()

* 🐛 Return TokenTypographyValue[] from a typography token's resolvedValue

* 🐛 Return TokenShadowValue[] from a shadow token's resolvedValue

* 🐛 Return string[] from a fontFamilies token's resolvedValue

* 🐛 Clear mutually-exclusive reps when setting LibraryColor gradient/image

* 🐛 Add readonly tags to types, deprecate Image type

* 📚 Update plugins changelog
This commit is contained in:
Alonso Torres 2026-06-29 17:32:15 +02:00 committed by GitHub
parent 99638fa60c
commit f993f203bd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
34 changed files with 1226 additions and 250 deletions

View File

@ -249,7 +249,7 @@
(defn is-variant-container? (defn is-variant-container?
"Check if this shape is a variant container" "Check if this shape is a variant container"
[shape] [shape]
(:is-variant-container shape)) (boolean (:is-variant-container shape)))
(defn set-touched-group (defn set-touched-group
[touched group] [touched group]

View File

@ -772,44 +772,46 @@
#{:up :down :bottom :top}) #{:up :down :bottom :top})
(defn vertical-order-selected (defn vertical-order-selected
[loc] ([loc]
(dm/assert! (vertical-order-selected loc nil))
"expected valid location" ([loc ids]
(contains? valid-vertical-locations loc)) (dm/assert!
(ptk/reify ::vertical-order-selected "expected valid location"
ptk/WatchEvent (contains? valid-vertical-locations loc))
(watch [it state _] (ptk/reify ::vertical-order-selected
(let [page-id (:current-page-id state) ptk/WatchEvent
objects (dsh/lookup-page-objects state page-id) (watch [it state _]
selected-ids (dsh/lookup-selected state) (let [page-id (:current-page-id state)
selected-shapes (map (d/getf objects) selected-ids) objects (dsh/lookup-page-objects state page-id)
undo-id (js/Symbol) selected-ids (or ids (dsh/lookup-selected state))
selected-shapes (map (d/getf objects) selected-ids)
undo-id (js/Symbol)
move-shape move-shape
(fn [changes shape] (fn [changes shape]
(let [parent (get objects (:parent-id shape)) (let [parent (get objects (:parent-id shape))
sibling-ids (:shapes parent) sibling-ids (:shapes parent)
current-index (d/index-of sibling-ids (:id shape)) current-index (d/index-of sibling-ids (:id shape))
index-in-selection (d/index-of selected-ids (:id shape)) index-in-selection (d/index-of selected-ids (:id shape))
new-index (case loc new-index (case loc
:top (count sibling-ids) :top (count sibling-ids)
:down (max 0 (- current-index 1)) :down (max 0 (- current-index 1))
:up (min (count sibling-ids) (+ (inc current-index) 1)) :up (min (count sibling-ids) (+ (inc current-index) 1))
:bottom index-in-selection)] :bottom index-in-selection)]
(pcb/change-parent changes (pcb/change-parent changes
(:id parent) (:id parent)
[shape] [shape]
new-index))) new-index)))
changes (reduce move-shape changes (reduce move-shape
(-> (pcb/empty-changes it page-id) (-> (pcb/empty-changes it page-id)
(pcb/with-objects objects)) (pcb/with-objects objects))
selected-shapes)] selected-shapes)]
(rx/of (dwu/start-undo-transaction undo-id) (rx/of (dwu/start-undo-transaction undo-id)
(dch/commit-changes changes) (dch/commit-changes changes)
(ptk/data-event :layout/update {:ids selected-ids}) (ptk/data-event :layout/update {:ids selected-ids})
(dwu/commit-undo-transaction undo-id)))))) (dwu/commit-undo-transaction undo-id)))))))
(defn set-shape-index (defn set-shape-index
[file-id page-id id new-index] [file-id page-id id new-index]

View File

@ -343,20 +343,23 @@
(dch/commit-changes changes)))))) (dch/commit-changes changes))))))
(defn duplicate-token-set (defn duplicate-token-set
[id] ([id]
(ptk/reify ::duplicate-token-set (duplicate-token-set id nil))
ptk/WatchEvent ([id {:keys [id-ref]}]
(watch [it state _] (ptk/reify ::duplicate-token-set
(let [data (dsh/lookup-file-data state) ptk/WatchEvent
tokens-lib (get data :tokens-lib) (watch [it state _]
suffix (tr "workspace.tokens.duplicate-suffix")] (let [data (dsh/lookup-file-data state)
tokens-lib (get data :tokens-lib)
suffix (tr "workspace.tokens.duplicate-suffix")]
(when-let [token-set (ctob/duplicate-set id tokens-lib {:suffix suffix})] (when-let [token-set (ctob/duplicate-set id tokens-lib {:suffix suffix})]
(let [changes (-> (pcb/empty-changes it) (when id-ref (reset! id-ref (ctob/get-id token-set)))
(pcb/with-library-data data) (let [changes (-> (pcb/empty-changes it)
(pcb/set-token-set (ctob/get-id token-set) token-set))] (pcb/with-library-data data)
(rx/of (set-selected-token-set-id (ctob/get-id token-set)) (pcb/set-token-set (ctob/get-id token-set) token-set))]
(dch/commit-changes changes)))))))) (rx/of (set-selected-token-set-id (ctob/get-id token-set))
(dch/commit-changes changes)))))))))
(defn set-enabled-token-set (defn set-enabled-token-set
[name enabled?] [name enabled?]

View File

@ -317,6 +317,11 @@
(or (not (array? shapes)) (not (every? shape/shape-proxy? shapes))) (or (not (array? shapes)) (not (every? shape/shape-proxy? shapes)))
(u/not-valid plugin-id :group-shapes shapes) (u/not-valid plugin-id :group-shapes shapes)
;; A group cannot be created from no shapes; per the documented contract
;; return null instead of a proxy pointing at a shape that never exists.
(zero? (alength shapes))
nil
(some #(not (u/page-active? (obj/get % "$page"))) shapes) (some #(not (u/page-active? (obj/get % "$page"))) shapes)
(u/not-valid plugin-id :group "Cannot modify a page that is not currently active") (u/not-valid plugin-id :group "Cannot modify a page that is not currently active")
@ -664,8 +669,13 @@
(u/not-valid plugin-id :flatten-shapes "Not valid shapes") (u/not-valid plugin-id :flatten-shapes "Not valid shapes")
:else :else
(let [ids (into #{} (map #(obj/get % "$id")) shapes)] ;; convert-selected-to-path converts the shapes in place (keeping their
(st/emit! (dw/convert-selected-to-path ids))))) ;; ids), so return proxies for the same ids, now resolving as paths.
(let [file-id (:current-file-id @st/state)
page-id (:current-page-id @st/state)
ids (mapv #(obj/get % "$id") shapes)]
(st/emit! (dw/convert-selected-to-path (into #{} ids)))
(apply array (map #(shape/shape-proxy plugin-id file-id page-id %) ids)))))
:createVariantFromComponents :createVariantFromComponents
(fn [shapes] (fn [shapes]

View File

@ -9,7 +9,6 @@
[app.common.geom.point :as gpt] [app.common.geom.point :as gpt]
[app.common.schema :as sm] [app.common.schema :as sm]
[app.main.data.comments :as dc] [app.main.data.comments :as dc]
[app.main.data.helpers :as dsh]
[app.main.data.workspace.comments :as dwc] [app.main.data.workspace.comments :as dwc]
[app.main.repo :as rp] [app.main.repo :as rp]
[app.main.store :as st] [app.main.store :as st]
@ -203,13 +202,12 @@
:remove :remove
(fn [] (fn []
(let [profile (:profile @st/state) (let [profile (:profile @st/state)]
owner (dsh/lookup-profile @st/state (:owner-id data))]
(cond (cond
(not (r/check-permission plugin-id "comment:write")) (not (r/check-permission plugin-id "comment:write"))
(u/not-valid plugin-id :remove "Plugin doesn't have 'comment:write' permission") (u/not-valid plugin-id :remove "Plugin doesn't have 'comment:write' permission")
(not= (:id profile) owner) (not= (:id profile) (:owner-id data))
(u/not-valid plugin-id :remove "Cannot change content from another user's comments") (u/not-valid plugin-id :remove "Cannot change content from another user's comments")
:else :else

View File

@ -64,7 +64,7 @@
(user/user-proxy plugin-id user-data)))} (user/user-proxy plugin-id user-data)))}
:createdAt :createdAt
{:get #(.toJSDate ^js (:created-at @data))} {:get #(:created-at @data)}
:isAutosave :isAutosave
{:get #(= "system" (:created-by @data))} {:get #(= "system" (:created-by @data))}
@ -136,6 +136,9 @@
:name :name
{:get #(-> (u/locate-file id) :name)} {:get #(-> (u/locate-file id) :name)}
:revn
{:get #(-> (u/locate-file id) :revn)}
:pages :pages
{:this true {:this true
:get #(.getPages ^js %)} :get #(.getPages ^js %)}

View File

@ -64,13 +64,13 @@
(u/not-valid plugin-id :applyToText "Cannot modify a page that is not currently active") (u/not-valid plugin-id :applyToText "Cannot modify a page that is not currently active")
:else :else
(let [id (obj/get text "$id") (let [text-id (obj/get text "$id")
values {:font-id id values {:font-id id
:font-family family :font-family family
:font-style (d/nilv (obj/get variant "fontStyle") (:style default-variant)) :font-style (d/nilv (obj/get variant "fontStyle") (:style default-variant))
:font-variant-id (d/nilv (obj/get variant "fontVariantId") (:id default-variant)) :font-variant-id (d/nilv (obj/get variant "fontVariantId") (:id default-variant))
:font-weight (d/nilv (obj/get variant "fontWeight") (:weight default-variant))}] :font-weight (d/nilv (obj/get variant "fontWeight") (:weight default-variant))}]
(st/emit! (dwt/update-attrs id values))))) (st/emit! (dwt/update-attrs text-id values)))))
:applyToRange :applyToRange
(fn [range variant] (fn [range variant]
@ -85,15 +85,15 @@
(u/not-valid plugin-id :applyToRange "Cannot modify a page that is not currently active") (u/not-valid plugin-id :applyToRange "Cannot modify a page that is not currently active")
:else :else
(let [id (obj/get range "$id") (let [range-id (obj/get range "$id")
start (obj/get range "start") start (obj/get range "$start")
end (obj/get range "end") end (obj/get range "$end")
values {:font-id id values {:font-id id
:font-family family :font-family family
:font-style (d/nilv (obj/get variant "fontStyle") (:style default-variant)) :font-style (d/nilv (obj/get variant "fontStyle") (:style default-variant))
:font-variant-id (d/nilv (obj/get variant "fontVariantId") (:id default-variant)) :font-variant-id (d/nilv (obj/get variant "fontVariantId") (:id default-variant))
:font-weight (d/nilv (obj/get variant "fontWeight") (:weight default-variant))}] :font-weight (d/nilv (obj/get variant "fontWeight") (:weight default-variant))}]
(st/emit! (dwt/update-text-range id start end values))))))))) (st/emit! (dwt/update-text-range range-id start end values)))))))))
(defn fonts-subcontext (defn fonts-subcontext
[plugin-id] [plugin-id]

View File

@ -47,6 +47,7 @@
:frame "board" :frame "board"
:rect "rectangle" :rect "rectangle"
:circle "ellipse" :circle "ellipse"
:bool "boolean"
(d/name type))) (d/name type)))
;;export type Bounds = { ;;export type Bounds = {
@ -146,7 +147,7 @@
[[color attrs]] [[color attrs]]
(let [shapes-info (apply array (map format-shape-info attrs)) (let [shapes-info (apply array (map format-shape-info attrs))
color (format-color color)] color (format-color color)]
(obj/set! color "shapeInfo" shapes-info) (obj/set! color "shapesInfo" shapes-info)
color)) color))

View File

@ -301,11 +301,15 @@
:addRowAtIndex :addRowAtIndex
(fn [index type value] (fn [index type value]
(let [type (keyword type)] (let [type (keyword type)
num-rows (-> (u/locate-shape file-id page-id id) :layout-grid-rows count)]
(cond (cond
(not (sm/valid-safe-int? index)) (not (sm/valid-safe-int? index))
(u/not-valid plugin-id :addRowAtIndex-index index) (u/not-valid plugin-id :addRowAtIndex-index index)
(or (< index 0) (> index num-rows))
(u/not-valid plugin-id :addRowAtIndex-index index)
(not (contains? ctl/grid-track-types type)) (not (contains? ctl/grid-track-types type))
(u/not-valid plugin-id :addRowAtIndex-type type) (u/not-valid plugin-id :addRowAtIndex-type type)
@ -344,64 +348,80 @@
:addColumnAtIndex :addColumnAtIndex
(fn [index type value] (fn [index type value]
(cond (let [type (keyword type)
(not (sm/valid-safe-int? index)) num-columns (-> (u/locate-shape file-id page-id id) :layout-grid-columns count)]
(u/not-valid plugin-id :addColumnAtIndex-index index) (cond
(not (sm/valid-safe-int? index))
(u/not-valid plugin-id :addColumnAtIndex-index index)
(not (contains? ctl/grid-track-types type)) (or (< index 0) (> index num-columns))
(u/not-valid plugin-id :addColumnAtIndex-type type) (u/not-valid plugin-id :addColumnAtIndex-index index)
(and (or (= :percent type) (= :flex type) (= :fixed type)) (not (contains? ctl/grid-track-types type))
(not (sm/valid-safe-number? value))) (u/not-valid plugin-id :addColumnAtIndex-type type)
(u/not-valid plugin-id :addColumnAtIndex-value value)
(not (r/check-permission plugin-id "content:write")) (and (or (= :percent type) (= :flex type) (= :fixed type))
(u/not-valid plugin-id :addColumnAtIndex "Plugin doesn't have 'content:write' permission") (not (sm/valid-safe-number? value)))
(u/not-valid plugin-id :addColumnAtIndex-value value)
(not (u/page-active? page-id)) (not (r/check-permission plugin-id "content:write"))
(u/not-valid plugin-id :addColumnAtIndex "Cannot modify a page that is not currently active") (u/not-valid plugin-id :addColumnAtIndex "Plugin doesn't have 'content:write' permission")
:else (not (u/page-active? page-id))
(let [type (keyword type)] (u/not-valid plugin-id :addColumnAtIndex "Cannot modify a page that is not currently active")
:else
(st/emit! (dwsl/add-layout-track #{id} :column {:type type :value value} index))))) (st/emit! (dwsl/add-layout-track #{id} :column {:type type :value value} index)))))
:removeRow :removeRow
(fn [index] (fn [index]
(cond (let [num-rows (-> (u/locate-shape file-id page-id id) :layout-grid-rows count)]
(not (sm/valid-safe-int? index)) (cond
(u/not-valid plugin-id :removeRow index) (not (sm/valid-safe-int? index))
(u/not-valid plugin-id :removeRow index)
(not (r/check-permission plugin-id "content:write")) (or (< index 0) (>= index num-rows))
(u/not-valid plugin-id :removeRow "Plugin doesn't have 'content:write' permission") (u/not-valid plugin-id :removeRow index)
(not (u/page-active? page-id)) (not (r/check-permission plugin-id "content:write"))
(u/not-valid plugin-id :removeRow "Cannot modify a page that is not currently active") (u/not-valid plugin-id :removeRow "Plugin doesn't have 'content:write' permission")
:else (not (u/page-active? page-id))
(st/emit! (dwsl/remove-layout-track #{id} :row index)))) (u/not-valid plugin-id :removeRow "Cannot modify a page that is not currently active")
:else
(st/emit! (dwsl/remove-layout-track #{id} :row index)))))
:removeColumn :removeColumn
(fn [index] (fn [index]
(cond (let [num-columns (-> (u/locate-shape file-id page-id id) :layout-grid-columns count)]
(not (sm/valid-safe-int? index)) (cond
(u/not-valid plugin-id :removeColumn index) (not (sm/valid-safe-int? index))
(u/not-valid plugin-id :removeColumn index)
(not (r/check-permission plugin-id "content:write")) (or (< index 0) (>= index num-columns))
(u/not-valid plugin-id :removeColumn "Plugin doesn't have 'content:write' permission") (u/not-valid plugin-id :removeColumn index)
(not (u/page-active? page-id)) (not (r/check-permission plugin-id "content:write"))
(u/not-valid plugin-id :removeColumn "Cannot modify a page that is not currently active") (u/not-valid plugin-id :removeColumn "Plugin doesn't have 'content:write' permission")
:else (not (u/page-active? page-id))
(st/emit! (dwsl/remove-layout-track #{id} :column index)))) (u/not-valid plugin-id :removeColumn "Cannot modify a page that is not currently active")
:else
(st/emit! (dwsl/remove-layout-track #{id} :column index)))))
:setColumn :setColumn
(fn [index type value] (fn [index type value]
(let [type (keyword type)] (let [type (keyword type)
num-columns (-> (u/locate-shape file-id page-id id) :layout-grid-columns count)]
(cond (cond
(not (sm/valid-safe-int? index)) (not (sm/valid-safe-int? index))
(u/not-valid plugin-id :setColumn-index index) (u/not-valid plugin-id :setColumn-index index)
(or (< index 0) (>= index num-columns))
(u/not-valid plugin-id :setColumn-index index)
(not (contains? ctl/grid-track-types type)) (not (contains? ctl/grid-track-types type))
(u/not-valid plugin-id :setColumn-type type) (u/not-valid plugin-id :setColumn-type type)
@ -420,11 +440,15 @@
:setRow :setRow
(fn [index type value] (fn [index type value]
(let [type (keyword type)] (let [type (keyword type)
num-rows (-> (u/locate-shape file-id page-id id) :layout-grid-rows count)]
(cond (cond
(not (sm/valid-safe-int? index)) (not (sm/valid-safe-int? index))
(u/not-valid plugin-id :setRow-index index) (u/not-valid plugin-id :setRow-index index)
(or (< index 0) (>= index num-rows))
(u/not-valid plugin-id :setRow-index index)
(not (contains? ctl/grid-track-types type)) (not (contains? ctl/grid-track-types type))
(u/not-valid plugin-id :setRow-type type) (u/not-valid plugin-id :setRow-type type)

View File

@ -51,6 +51,7 @@
:$file {:enumerable false :get (constantly file-id)} :$file {:enumerable false :get (constantly file-id)}
:id {:get (fn [] (dm/str id))} :id {:get (fn [] (dm/str id))}
:libraryId {:get (fn [] (dm/str file-id))}
:fileId {:get #(dm/str file-id)} :fileId {:get #(dm/str file-id)}
:name :name
@ -101,7 +102,8 @@
:else :else
(let [color (-> (u/proxy->library-color self) (let [color (-> (u/proxy->library-color self)
(assoc :color value))] (assoc :color value)
(dissoc :gradient :image))]
(st/emit! (dwl/update-color-data color file-id)))))} (st/emit! (dwl/update-color-data color file-id)))))}
:opacity :opacity
@ -136,7 +138,8 @@
:else :else
(let [color (-> (u/proxy->library-color self) (let [color (-> (u/proxy->library-color self)
(assoc :gradient value))] (assoc :gradient value)
(dissoc :color :image))]
(st/emit! (dwl/update-color-data color file-id))))))} (st/emit! (dwl/update-color-data color file-id))))))}
:image :image
@ -154,7 +157,8 @@
:else :else
(let [color (-> (u/proxy->library-color self) (let [color (-> (u/proxy->library-color self)
(assoc :image value))] (assoc :image value)
(dissoc :color :gradient))]
(st/emit! (dwl/update-color-data color file-id))))))} (st/emit! (dwl/update-color-data color file-id))))))}
:remove :remove
@ -295,6 +299,7 @@
:$id {:enumerable false :get (constantly id)} :$id {:enumerable false :get (constantly id)}
:$file {:enumerable false :get (constantly file-id)} :$file {:enumerable false :get (constantly file-id)}
:id {:get (fn [] (dm/str id))} :id {:get (fn [] (dm/str id))}
:libraryId {:get (fn [] (dm/str file-id))}
:name :name
{:this true {:this true
@ -484,6 +489,27 @@
(assoc :text-transform value))] (assoc :text-transform value))]
(st/emit! (dwl/update-typography typo file-id)))))} (st/emit! (dwl/update-typography typo file-id)))))}
:setFont
(fn [font variant]
(cond
(not (obj/type-of? font "FontProxy"))
(u/not-valid plugin-id :setFont font)
(not (r/check-permission plugin-id "library:write"))
(u/not-valid plugin-id :setFont "Plugin doesn't have 'library:write' permission")
:else
;; When a variant is given read the variant-specific fields from it;
;; otherwise the FontProxy exposes the font's default variant fields.
(let [source (if (obj/type-of? variant "FontVariantProxy") variant font)
typo (-> (u/locate-library-typography file-id id)
(assoc :font-id (obj/get font "fontId")
:font-family (obj/get font "fontFamily")
:font-variant-id (obj/get source "fontVariantId")
:font-style (obj/get source "fontStyle")
:font-weight (obj/get source "fontWeight")))]
(st/emit! (dwl/update-typography typo file-id)))))
:remove :remove
(fn [] (fn []
(cond (cond
@ -539,8 +565,8 @@
:else :else
(let [shape-id (obj/get range "$id") (let [shape-id (obj/get range "$id")
start (obj/get range "start") start (obj/get range "$start")
end (obj/get range "end") end (obj/get range "$end")
typography (u/locate-library-typography file-id id) typography (u/locate-library-typography file-id id)
attrs (-> typography attrs (-> typography
(assoc :typography-ref-file file-id) (assoc :typography-ref-file file-id)
@ -718,6 +744,7 @@
:$id {:enumerable false :get (constantly id)} :$id {:enumerable false :get (constantly id)}
:$file {:enumerable false :get (constantly file-id)} :$file {:enumerable false :get (constantly file-id)}
:id {:get (fn [] (dm/str id))} :id {:get (fn [] (dm/str id))}
:libraryId {:get (fn [] (dm/str file-id))}
:name :name
{:this true {:this true

View File

@ -60,7 +60,7 @@
(u/not-valid plugin-id :removeItem "The key must be a string") (u/not-valid plugin-id :removeItem "The key must be a string")
:else :else
(.getItem ^js local-storage (prefix-key plugin-id key)))) (.removeItem ^js local-storage (prefix-key plugin-id key))))
:getKeys :getKeys
(fn [] (fn []

View File

@ -412,8 +412,7 @@
(js/Promise. (js/Promise.
(fn [resolve] (fn [resolve]
(let [thread-id (obj/get thread "$id")] (let [thread-id (obj/get thread "$id")]
(js/Promise. (st/emit! (dc/delete-comment-thread-on-workspace {:id thread-id} #(resolve))))))))
(st/emit! (dc/delete-comment-thread-on-workspace {:id thread-id} #(resolve)))))))))
:findCommentThreads :findCommentThreads
(fn [criteria] (fn [criteria]

View File

@ -343,10 +343,10 @@
(when (some? guide) (when (some? guide)
(case (obj/get guide "type") (case (obj/get guide "type")
"column" "column"
parse-frame-guide-column (parse-frame-guide-column guide)
"row" "row"
parse-frame-guide-row (parse-frame-guide-row guide)
"square" "square"
(parse-frame-guide-square guide)))) (parse-frame-guide-square guide))))
@ -489,7 +489,7 @@
:destination (-> (obj/get action "destination") (obj/get "$id")) :destination (-> (obj/get action "destination") (obj/get "$id"))
:relative-to (-> (obj/get action "relativeTo") (obj/get "$id")) :relative-to (-> (obj/get action "relativeTo") (obj/get "$id"))
:overlay-pos-type (-> (obj/get action "position") parse-keyword) :overlay-pos-type (-> (obj/get action "position") parse-keyword)
:overlay-position (-> (obj/get action "manualPositionLocation") parse-point) :overlay-position (-> (obj/get action "manualPositionLocation") parse-point (d/nilv (gpt/point 0 0)))
:close-click-outside (obj/get action "closeWhenClickOutside") :close-click-outside (obj/get action "closeWhenClickOutside")
:background-overlay (obj/get action "addBackgroundOverlay") :background-overlay (obj/get action "addBackgroundOverlay")
:animation (-> (obj/get action "animation") parse-animation)} :animation (-> (obj/get action "animation") parse-animation)}

View File

@ -14,10 +14,15 @@
[app.plugins.utils :as u])) [app.plugins.utils :as u]))
(defn ^:export centerShapes (defn ^:export centerShapes
[plugin-id shapes] [shapes]
(cond (cond
(not (every? shape/shape-proxy? shapes)) (not (every? shape/shape-proxy? shapes))
(u/not-valid plugin-id :centerShapes shapes) (u/not-valid nil :centerShapes shapes)
;; The documented contract returns null for an empty array; without this
;; guard `shapes->rect` yields a non-rect and `rect->center` asserts.
(empty? shapes)
nil
:else :else
(let [shapes (->> shapes (map u/proxy->shape))] (let [shapes (->> shapes (map u/proxy->shape))]

View File

@ -101,7 +101,7 @@
:else :else
(st/emit! (dwi/update-interaction (st/emit! (dwi/update-interaction
{:id shape-id} (u/locate-shape file-id page-id shape-id)
index index
#(assoc % :event-type value) #(assoc % :event-type value)
{:page-id page-id})))))} {:page-id page-id})))))}
@ -117,7 +117,7 @@
:else :else
(st/emit! (dwi/update-interaction (st/emit! (dwi/update-interaction
{:id shape-id} (u/locate-shape file-id page-id shape-id)
index index
#(assoc % :delay value) #(assoc % :delay value)
{:page-id page-id}))))} {:page-id page-id}))))}
@ -137,7 +137,7 @@
:else :else
(st/emit! (dwi/update-interaction (st/emit! (dwi/update-interaction
{:id shape-id} (u/locate-shape file-id page-id shape-id)
index index
#(d/patch-object % params) #(d/patch-object % params)
{:page-id page-id})))))} {:page-id page-id})))))}
@ -592,7 +592,7 @@
:else :else
(st/emit! (dwsh/update-shapes [id] #(assoc % :blur value)))))))} (st/emit! (dwsh/update-shapes [id] #(assoc % :blur value)))))))}
:background-blur :backgroundBlur
{:this true {:this true
:get #(-> % u/proxy->shape :background-blur format/format-blur) :get #(-> % u/proxy->shape :background-blur format/format-blur)
:set :set
@ -1249,6 +1249,11 @@
:else :else
(st/emit! (dwg/unmask-group #{id}))))) (st/emit! (dwg/unmask-group #{id})))))
:isMask
(fn []
(let [shape (u/locate-shape file-id page-id id)]
(boolean (cfh/mask-shape? shape))))
;; Only for path and bool shapes ;; Only for path and bool shapes
:toD :toD
(fn [] (fn []
@ -1315,19 +1320,19 @@
:bringForward :bringForward
(fn [] (fn []
(st/emit! (dw/vertical-order-selected :up))) (st/emit! (dw/vertical-order-selected :up [id])))
:sendBackward :sendBackward
(fn [] (fn []
(st/emit! (dw/vertical-order-selected :down))) (st/emit! (dw/vertical-order-selected :down [id])))
:bringToFront :bringToFront
(fn [] (fn []
(st/emit! (dw/vertical-order-selected :top))) (st/emit! (dw/vertical-order-selected :top [id])))
:sendToBack :sendToBack
(fn [] (fn []
(st/emit! (dw/vertical-order-selected :bottom))) (st/emit! (dw/vertical-order-selected :bottom [id])))
;; COMPONENTS ;; COMPONENTS
:isComponentInstance :isComponentInstance
@ -1402,6 +1407,28 @@
:else :else
(st/emit! (dwl/detach-component id)))) (st/emit! (dwl/detach-component id))))
:swapComponent
(fn [component]
(let [shape (u/locate-shape file-id page-id id)]
(cond
(not (u/page-active? page-id))
(u/not-valid plugin-id :swapComponent "Cannot modify a page that is not currently active")
(not (r/check-permission plugin-id "content:write"))
(u/not-valid plugin-id :swapComponent "Plugin doesn't have 'content:write' permission")
(not (obj/type-of? component "LibraryComponentProxy"))
(u/not-valid plugin-id :swapComponent "Component not valid")
(not (ctk/in-component-copy? shape))
(u/not-valid plugin-id :swapComponent "The shape is not a component copy instance")
:else
(st/emit! (dwl/component-swap shape
(obj/get component "$file")
(obj/get component "$id")
true)))))
;; Export ;; Export
:export :export
(fn [value] (fn [value]
@ -1536,7 +1563,7 @@
(rg/ruler-guide-proxy plugin-id file-id page-id ruler-id))))) (rg/ruler-guide-proxy plugin-id file-id page-id ruler-id)))))
:removeRulerGuide :removeRulerGuide
(fn [_ value] (fn [value]
(cond (cond
(not (rg/ruler-guide-proxy? value)) (not (rg/ruler-guide-proxy? value))
(u/not-valid plugin-id :removeRulerGuide "Guide not provided") (u/not-valid plugin-id :removeRulerGuide "Guide not provided")
@ -1618,7 +1645,7 @@
:else :else
(let [ids (let [ids
(into #{id} (keep uuid/parse*) id) (into #{id} (keep uuid/parse*) ids)
valid? valid?
(every? (every?
@ -1740,7 +1767,7 @@
(let [id (obj/get self "$id") (let [id (obj/get self "$id")
value (parser/parse-frame-guides value)] value (parser/parse-frame-guides value)]
(cond (cond
(not (sm/validate [:vector ::ctg/grid] value)) (not (sm/validate [:vector ctg/schema:grid] value))
(u/not-valid plugin-id :guides value) (u/not-valid plugin-id :guides value)
(not (r/check-permission plugin-id "content:write")) (not (r/check-permission plugin-id "content:write"))

View File

@ -78,7 +78,7 @@
taking? (or taking? (and (<= from start) (< start to))) taking? (or taking? (and (<= from start) (< start to)))
text (subs text (max 0 (- start acc)) (- end acc)) text (subs text (max 0 (- start acc)) (- end acc))
result (cond-> result result (cond-> result
(and taking? (d/not-empty? text)) (and taking? (seq text))
(conj (assoc node-style :text text))) (conj (assoc node-style :text text)))
continue? (or (> from end) (>= end to))] continue? (or (> from end) (>= end to))]
(recur (when continue? (rest styles)) taking? to result)) (recur (when continue? (rest styles)) taking? to result))
@ -95,10 +95,11 @@
:$id {:enumerable false :get (constantly id)} :$id {:enumerable false :get (constantly id)}
:$file {:enumerable false :get (constantly file-id)} :$file {:enumerable false :get (constantly file-id)}
:$page {:enumerable false :get (constantly page-id)} :$page {:enumerable false :get (constantly page-id)}
:$start {:enumerable false :get (constantly start)}
:$end {:enumerable false :get (constantly end)}
:shape :shape
{:this true {:get (fn [] (format/shape-proxy plugin-id file-id page-id id))}
:get #(-> % u/proxy->shape)}
:characters :characters
{:this true {:this true

View File

@ -96,13 +96,83 @@
:expand-with-children false}) :expand-with-children false})
(se/add-event plugin-id)))))) (se/add-event plugin-id))))))
(defn- typography-resolved-value->js
"Converts a resolved typography composite (a Clojure map keyed by the
tokenscript field names) into the plugin's `TokenTypographyValue[]` shape: a
JS array with a single object using the public camelCase member names."
[m]
(when (map? m)
#js [#js {"fontFamilies" (clj->js (:font-family m))
"fontSizes" (:font-size m)
"fontWeights" (some-> (:font-weight m) str)
"letterSpacing" (:letter-spacing m)
"lineHeight" (:line-height m)
"textCase" (:text-case m)
"textDecoration" (:text-decoration m)}]))
(defn- shadow-key->camel
"Renames a shadow composite field name (kebab string) to its public camelCase
member name. The shadow schema is closed; offset-x/offset-y are its only
multi-word fields, so the rest (blur, spread, color, inset) pass through."
[k]
(case k
"offset-x" "offsetX"
"offset-y" "offsetY"
k))
(defn- shadow-entry->js
"Converts one resolved shadow entry (a JS Map of field name -> tokenscript
symbol) into a plain JS object using the public member names and the
unit-converted values."
[^js m]
(let [out #js {}]
(.forEach m (fn [sym k]
(obj/set! out (shadow-key->camel k)
(ts/tokenscript-symbols->penpot-unit sym))))
out))
(defn- shadow-resolved-value->js
"Converts a resolved shadow composite (a sequence of shadow entries) into the
plugin's `TokenShadowValue[]` shape."
[entries]
(when (some? entries)
(into-array (map shadow-entry->js entries))))
(defn- font-families-resolved-value->js
"Converts a resolved fontFamilies value (a tokenscript list symbol) into the
documented `string[]` shape rather than leaking the raw tokenscript structure."
[resolved-value]
(let [v (ts/tokenscript-symbols->penpot-unit resolved-value)]
(cond
(nil? v) nil
(sequential? v) (clj->js v)
:else #js [v])))
(defn- get-resolved-value (defn- get-resolved-value
[token tokens-tree] [token tokens-tree]
(let [resolved-tokens (ts/resolve-tokens tokens-tree) (let [resolved-tokens (ts/resolve-tokens tokens-tree)
resolved-value (-> resolved-tokens resolved-value (dm/get-in resolved-tokens [(:name token) :resolved-value])]
(dm/get-in [(:name token) :resolved-value]) (cond
(ts/tokenscript-symbols->penpot-unit))] (= :font-family (:type token))
resolved-value)) ;; A fontFamilies token resolves to a list of families; expose it as the
;; documented `string[]` rather than the raw tokenscript list symbol.
(font-families-resolved-value->js resolved-value)
(= :typography (:type token))
;; A typography token resolves to a composite; expose it as the documented
;; `TokenTypographyValue[]` rather than the raw tokenscript structure.
(typography-resolved-value->js
(ts/tokenscript-symbols->penpot-unit resolved-value))
(= :shadow (:type token))
;; A shadow token resolves to a list of composites whose entries the
;; tokenscript unit conversion leaves as raw symbols; expose them as the
;; documented `TokenShadowValue[]`.
(shadow-resolved-value->js
(ts/tokenscript-symbols->penpot-unit resolved-value))
:else
(ts/tokenscript-symbols->penpot-unit resolved-value))))
(defn token-proxy? [p] (defn token-proxy? [p]
(obj/type-of? p "TokenProxy")) (obj/type-of? p "TokenProxy"))
@ -150,11 +220,21 @@
(fn [_] (fn [_]
(let [token (u/locate-token file-id set-id id)] (let [token (u/locate-token file-id set-id id)]
(json/->js (:value token)))) (json/->js (:value token))))
:schema (let [token (u/locate-token file-id set-id id)] :schema (let [token (u/locate-token file-id set-id id)
(cfo/make-token-value-schema (:type token))) base (cfo/make-token-value-schema (:type token))]
;; plugin-types declares the fontFamilies value as
;; `string | string[]`, but the core schema only accepts a
;; vector/ref; also accept a plain string (normalized in :set).
(if (= :font-family (:type token))
[:or :string base]
base))
:set :set
(fn [_ value] (fn [_ value]
(st/emit! (dwtl/update-token set-id id {:value value})))} (let [token (u/locate-token file-id set-id id)
value (cond-> value
(= :font-family (:type token))
(ctob/convert-dtcg-font-family))]
(st/emit! (dwtl/update-token set-id id {:value value}))))}
:resolvedValue :resolvedValue
{:this true {:this true
@ -361,7 +441,10 @@
:duplicate :duplicate
(fn [] (fn []
(st/emit! (dwtl/duplicate-token-set id))) (let [id-ref (atom nil)]
(st/emit! (dwtl/duplicate-token-set id {:id-ref id-ref}))
(when (some? @id-ref)
(token-set-proxy plugin-id file-id @id-ref))))
:remove :remove
(fn [] (fn []
@ -460,7 +543,7 @@
;; Guard against nil to prevent `enable-set` from conj'ing nil ;; Guard against nil to prevent `enable-set` from conj'ing nil
;; into the theme's :sets — which would send `:sets #{nil}` to the ;; into the theme's :sets — which would send `:sets #{nil}` to the
;; backend and crash the workspace. ;; backend and crash the workspace.
(let [set-name (obj/get token-set :name) (let [set-name (obj/get token-set "name")
theme (u/locate-token-theme file-id id)] theme (u/locate-token-theme file-id id)]
(when (and (some? set-name) (some? theme)) (when (and (some? set-name) (some? theme))
(st/emit! (dwtl/update-token-theme id (ctob/enable-set theme set-name))))))} (st/emit! (dwtl/update-token-theme id (ctob/enable-set theme set-name))))))}
@ -470,7 +553,7 @@
:schema [:tuple [:fn token-set-proxy?]] :schema [:tuple [:fn token-set-proxy?]]
:fn (fn [token-set] :fn (fn [token-set]
;; Same nil guard as addSet — see comment above. ;; Same nil guard as addSet — see comment above.
(let [set-name (obj/get token-set :name) (let [set-name (obj/get token-set "name")
theme (u/locate-token-theme file-id id)] theme (u/locate-token-theme file-id id)]
(when (and (some? set-name) (some? theme)) (when (and (some? set-name) (some? theme))
(st/emit! (dwtl/update-token-theme id (ctob/disable-set theme set-name))))))} (st/emit! (dwtl/update-token-theme id (ctob/disable-set theme set-name))))))}

View File

@ -1074,66 +1074,75 @@
(defn set-grid-layout-rows (defn set-grid-layout-rows
[entries] [entries]
(let [size (mem/get-alloc-size entries GRID-LAYOUT-ROW-U8-SIZE) ;; Only allocate when there are entries; an empty list would alloc 0 bytes.
offset (mem/alloc size) ;; The wasm side reads an empty buffer as zero rows.
dview (mem/get-data-view)] (when (seq entries)
(let [size (mem/get-alloc-size entries GRID-LAYOUT-ROW-U8-SIZE)
offset (mem/alloc size)
dview (mem/get-data-view)]
(reduce (fn [offset {:keys [type value]}] (reduce (fn [offset {:keys [type value]}]
(-> offset (-> offset
(mem/write-u8 dview (sr/translate-grid-track-type type)) (mem/write-u8 dview (sr/translate-grid-track-type type))
(+ 3) ;; padding (+ 3) ;; padding
(mem/write-f32 dview value) (mem/write-f32 dview value)
(mem/assert-written offset GRID-LAYOUT-ROW-U8-SIZE))) (mem/assert-written offset GRID-LAYOUT-ROW-U8-SIZE)))
offset offset
entries) entries)))
(h/call wasm/internal-module "_set_grid_rows"))) (h/call wasm/internal-module "_set_grid_rows"))
(defn set-grid-layout-columns (defn set-grid-layout-columns
[entries] [entries]
(let [size (mem/get-alloc-size entries GRID-LAYOUT-COLUMN-U8-SIZE) ;; Only allocate when there are entries; an empty list would alloc 0 bytes.
offset (mem/alloc size) ;; The wasm side reads an empty buffer as zero columns.
dview (mem/get-data-view)] (when (seq entries)
(let [size (mem/get-alloc-size entries GRID-LAYOUT-COLUMN-U8-SIZE)
offset (mem/alloc size)
dview (mem/get-data-view)]
(reduce (fn [offset {:keys [type value]}] (reduce (fn [offset {:keys [type value]}]
(-> offset (-> offset
(mem/write-u8 dview (sr/translate-grid-track-type type)) (mem/write-u8 dview (sr/translate-grid-track-type type))
(+ 3) ;; padding (+ 3) ;; padding
(mem/write-f32 dview value) (mem/write-f32 dview value)
(mem/assert-written offset GRID-LAYOUT-COLUMN-U8-SIZE))) (mem/assert-written offset GRID-LAYOUT-COLUMN-U8-SIZE)))
offset offset
entries) entries)))
(h/call wasm/internal-module "_set_grid_columns"))) (h/call wasm/internal-module "_set_grid_columns"))
(defn set-grid-layout-cells (defn set-grid-layout-cells
[cells] [cells]
(let [size (mem/get-alloc-size cells GRID-LAYOUT-CELL-U8-SIZE) ;; Only allocate when there are cells; an empty collection would alloc 0
offset (mem/alloc size) ;; bytes. The wasm side reads an empty buffer as zero cells.
dview (mem/get-data-view)] (when (seq cells)
(let [size (mem/get-alloc-size cells GRID-LAYOUT-CELL-U8-SIZE)
offset (mem/alloc size)
dview (mem/get-data-view)]
(reduce-kv (fn [offset _ cell] (reduce-kv (fn [offset _ cell]
(let [shape-id (-> (get cell :shapes) first)] (let [shape-id (-> (get cell :shapes) first)]
(-> offset (-> offset
(mem/write-i32 dview (get cell :row)) (mem/write-i32 dview (get cell :row))
(mem/write-i32 dview (get cell :row-span)) (mem/write-i32 dview (get cell :row-span))
(mem/write-i32 dview (get cell :column)) (mem/write-i32 dview (get cell :column))
(mem/write-i32 dview (get cell :column-span)) (mem/write-i32 dview (get cell :column-span))
(mem/write-u8 dview (sr/translate-align-self (get cell :align-self))) (mem/write-u8 dview (sr/translate-align-self (get cell :align-self)))
(mem/write-u8 dview (sr/translate-justify-self (get cell :justify-self))) (mem/write-u8 dview (sr/translate-justify-self (get cell :justify-self)))
;; padding ;; padding
(+ 2) (+ 2)
(mem/write-uuid dview (d/nilv shape-id uuid/zero)) (mem/write-uuid dview (d/nilv shape-id uuid/zero))
(mem/assert-written offset GRID-LAYOUT-CELL-U8-SIZE)))) (mem/assert-written offset GRID-LAYOUT-CELL-U8-SIZE))))
offset offset
cells) cells)))
(h/call wasm/internal-module "_set_grid_cells"))) (h/call wasm/internal-module "_set_grid_cells"))
(defn set-grid-layout (defn set-grid-layout
[shape] [shape]

View File

@ -126,6 +126,39 @@
[_ms] [_ms]
(rx/of :immediate)) (rx/of :immediate))
;; Static-dispatch-safe stubs
;; ═══════════════════════════════════════════════════════════════
;;
;; The `:esm` test build compiles calls to a *multi-arity* var as
;; `f.cljs$core$IFn$_invoke$arity$N(...)`. A plain single-arity `fn`
;; (including `identity`) does not expose that property, so using one
;; to redefine such a var throws "arity$N is not a function". Multi-arity
;; fns do expose the property, hence the helpers below.
(defn noop
"Multi-arity no-op. Use to stub static-dispatched multi-arity vars
such as `st/emit!` (replacing `identity`, which is single-arity)."
([] nil)
([_] nil)
([_ _] nil)
([_ _ _] nil)
([_ _ _ _] nil)
([_ _ _ _ & _] nil))
(defn stub
"Wraps `f` in a multi-arity fn (arities 0-6) delegating to `f`, so the
result exposes `cljs$core$IFn$_invoke$arity$N`. Required when replacing
a multi-arity var in a `with-redefs`/`set!` mock with a capturing fn."
[f]
(fn
([] (f))
([a] (f a))
([a b] (f a b))
([a b c] (f a b c))
([a b c d] (f a b c d))
([a b c d e] (f a b c d e))
([a b c d e g] (f a b c d e g))))
;; Lifecycle ;; Lifecycle
;; ═══════════════════════════════════════════════════════════════ ;; ═══════════════════════════════════════════════════════════════

View File

@ -0,0 +1,60 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS INC Sucursal en España SL
(ns frontend-tests.plugins.comments-test
(:require
[app.main.data.comments :as dc]
[app.main.store :as st]
[app.plugins.comments :as comments]
[app.plugins.page :as page]
[app.plugins.register :as r]
[cljs.test :as t :include-macros true]
[frontend-tests.helpers.mock :as mock]))
(def ^:private plugin-id "00000000-0000-0000-0000-000000000000")
(t/deftest comment-thread-remove-allows-the-owner
(let [owner-id (random-uuid)
file-id (random-uuid)
page-id (random-uuid)
thread-id (random-uuid)
emitted (atom nil)
thread (comments/comment-thread-proxy
plugin-id
file-id
page-id
{:id thread-id :owner-id owner-id})]
(set! st/state (atom {:profile {:id owner-id}}))
(with-redefs [r/check-permission (constantly true)
dc/delete-comment-thread-on-workspace
(mock/stub (fn [params callback]
(callback)
[:delete-thread params]))
st/emit! (mock/stub (fn [event] (reset! emitted event)))]
(let [result (.remove thread)]
(t/is (instance? js/Promise result))
(t/is (= [:delete-thread {:id thread-id}] @emitted))))))
(t/deftest page-remove-comment-thread-emits-delete-event
(let [file-id (random-uuid)
page-id (random-uuid)
thread-id (random-uuid)
emitted (atom nil)
page (page/page-proxy plugin-id file-id page-id)
thread (comments/comment-thread-proxy
plugin-id
file-id
page-id
{:id thread-id :owner-id (random-uuid)})]
(with-redefs [r/check-permission (constantly true)
dc/delete-comment-thread-on-workspace
(mock/stub (fn [params callback]
(callback)
[:delete-thread params]))
st/emit! (mock/stub (fn [event] (reset! emitted event)))]
(let [result (.removeCommentThread page thread)]
(t/is (instance? js/Promise result))
(t/is (= [:delete-thread {:id thread-id}] @emitted))))))

View File

@ -0,0 +1,21 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS INC Sucursal en España SL
(ns frontend-tests.plugins.file-test
(:require
[app.plugins.file :as file]
[cljs.test :as t :include-macros true]))
(t/deftest file-version-created-at-returns-stored-date
(let [created-at (js/Date.)
version (file/file-version-proxy
"00000000-0000-0000-0000-000000000000"
(random-uuid)
{}
{:id (random-uuid)
:label "Version"
:created-at created-at})]
(t/is (identical? created-at (.-createdAt version)))))

View File

@ -38,3 +38,17 @@
(format/format-frame-guides nil) (format/format-frame-guides nil)
(format/format-tracks nil) (format/format-tracks nil)
(format/format-path-content nil))) (format/format-path-content nil)))
(t/deftest test-format-color-result-uses-shapes-info-key
(let [shape-id (random-uuid)
result (format/format-color-result
[{:color "#fabada"}
[{:prop :fill :shape-id shape-id :index 0}]])
info (aget result "shapesInfo")]
(t/is (array? info))
(t/is (nil? (aget result "shapesColors")))
(t/is (= "fill" (aget (aget info 0) "property")))
(t/is (= (str shape-id) (aget (aget info 0) "shapeId")))))
(t/deftest test-shape-type-reports-boolean
(t/is (= "boolean" (format/shape-type :bool))))

View File

@ -0,0 +1,49 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS INC Sucursal en España SL
(ns frontend-tests.plugins.grid-test
(:require
[app.common.test-helpers.files :as cthf]
[app.main.store :as st]
[app.plugins.api :as api]
[cljs.test :as t :include-macros true]
[frontend-tests.helpers.state :as ths]
[frontend-tests.helpers.wasm :as thw]
[potok.v2.core :as ptk]))
(def ^:private plugin-id "00000000-0000-0000-0000-000000000000")
(defn- setup-grid []
(let [store (ths/setup-store (cthf/sample-file :file1 :page-label :page1))
_ (set! st/state store)
_ (set! st/stream (ptk/input-stream store))
context (api/create-context plugin-id)
board (.createBoard ^js context)
grid (.addGridLayout ^js board)]
{:store store :context context :board board :grid grid}))
(t/deftest add-column-at-index-accepts-fixed-track-type
(thw/with-wasm-mocks*
(fn []
(let [{:keys [^js grid]} (setup-grid)]
(.addColumn grid "flex" 1)
(.addColumnAtIndex grid 0 "fixed" 100)
(t/is (= "fixed" (aget (aget (.-columns grid) 0) "type")))
(t/is (= 100 (aget (aget (.-columns grid) 0) "value")))))))
(t/deftest grid-track-methods-reject-out-of-range-indices
(thw/with-wasm-mocks*
(fn []
(let [{:keys [store ^js grid]} (setup-grid)]
(swap! store assoc-in [:plugins :flags plugin-id :throw-validation-errors] true)
(.addRow grid "flex" 1)
(.addColumn grid "flex" 1)
(t/is (thrown? js/Error (.addRowAtIndex grid -1 "fixed" 10)))
(t/is (thrown? js/Error (.addColumnAtIndex grid 2 "fixed" 10)))
(t/is (thrown? js/Error (.setRow grid 1 "fixed" 10)))
(t/is (thrown? js/Error (.setColumn grid 1 "fixed" 10)))
(t/is (thrown? js/Error (.removeRow grid 1)))
(t/is (thrown? js/Error (.removeColumn grid 1)))))))

View File

@ -0,0 +1,95 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS INC Sucursal en España SL
(ns frontend-tests.plugins.library-test
(:require
[app.main.data.workspace.libraries :as dwl]
[app.main.data.workspace.texts :as dwt]
[app.main.store :as st]
[app.plugins.library :as library]
[app.plugins.register :as r]
[app.plugins.text :as text]
[app.plugins.utils :as u]
[cljs.test :as t :include-macros true]
[frontend-tests.helpers.mock :as mock]))
(def ^:private plugin-id "00000000-0000-0000-0000-000000000000")
(t/deftest library-asset-proxies-expose-library-id
(let [file-id (random-uuid)
id (random-uuid)]
(t/is (= (str file-id) (.-libraryId (library/lib-color-proxy plugin-id file-id id))))
(t/is (= (str file-id) (.-libraryId (library/lib-typography-proxy plugin-id file-id id))))
(t/is (= (str file-id) (.-libraryId (library/lib-component-proxy plugin-id file-id id))))))
(t/deftest typography-apply-to-text-range-uses-hidden-range-bounds
(let [file-id (random-uuid)
page-id (random-uuid)
shape-id (random-uuid)
typography-id (random-uuid)
typography (library/lib-typography-proxy plugin-id file-id typography-id)
text-range (text/text-range-proxy plugin-id file-id page-id shape-id 2 5)
captured (atom nil)]
(with-redefs [r/check-permission (constantly true)
u/page-active? (constantly true)
u/locate-library-typography
(constantly {:id typography-id
:name "Body"
:font-size "14"})
dwt/update-text-range
(fn [shape-id start end attrs]
(reset! captured {:shape-id shape-id
:start start
:end end
:attrs attrs})
:update-text-range)
st/emit! mock/noop]
(.applyToTextRange typography text-range)
(t/is (= shape-id (:shape-id @captured)))
(t/is (= 2 (:start @captured)))
(t/is (= 5 (:end @captured)))
(t/is (= file-id (get-in @captured [:attrs :typography-ref-file])))
(t/is (= typography-id (get-in @captured [:attrs :typography-ref-id]))))))
(t/deftest library-color-gradient-and-image-clear-exclusive-representations
(let [file-id (random-uuid)
color-id (random-uuid)
proxy (library/lib-color-proxy plugin-id file-id color-id)
captured (atom nil)
base {:id color-id
:name "Brand"
:color "#fabada"
:opacity 1
:gradient {:type :linear}
:image {:id (random-uuid) :width 1 :height 1}}]
(with-redefs [r/check-permission (constantly true)
u/proxy->library-color (constantly base)
dwl/update-color-data (fn [color file-id]
(reset! captured {:color color :file-id file-id})
:update-color-data)
st/emit! mock/noop]
(set! (.-gradient proxy)
#js {:type "linear"
:startX 0
:startY 0
:endX 1
:endY 1
:width 1
:stops #js [#js {:color "#000000"
:opacity 1
:offset 0}]})
(t/is (contains? (:color @captured) :gradient))
(t/is (not (contains? (:color @captured) :color)))
(t/is (not (contains? (:color @captured) :image)))
(set! (.-image proxy)
#js {:id (str (random-uuid))
:width 10
:height 20
:mtype "image/png"})
(t/is (contains? (:color @captured) :image))
(t/is (not (contains? (:color @captured) :color)))
(t/is (not (contains? (:color @captured) :gradient))))))

View File

@ -0,0 +1,28 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS INC Sucursal en España SL
(ns frontend-tests.plugins.local-storage-test
(:require
[app.plugins.local-storage :as storage]
[app.plugins.register :as r]
[cljs.test :as t :include-macros true]))
(t/deftest remove-item-removes-the-prefixed-key
(let [data (atom {})
fake #js {}
plugin-id "plugin-a"]
(set! (.-getItem fake) (fn [key] (get @data key)))
(set! (.-setItem fake) (fn [key value] (swap! data assoc key value)))
(set! (.-removeItem fake) (fn [key] (swap! data dissoc key)))
(set! (.-keys fake) (fn [] (to-array (keys @data))))
(with-redefs [r/check-permission (constantly true)
storage/local-storage fake]
(let [proxy (storage/local-storage-proxy plugin-id)]
(.setItem proxy "key" "value")
(t/is (= "value" (.getItem proxy "key")))
(.removeItem proxy "key")
(t/is (nil? (.getItem proxy "key")))
(t/is (empty? @data))))))

View File

@ -31,3 +31,33 @@
(t/is (gpt/point? result)) (t/is (gpt/point? result))
(t/is (= 0 (:x result))) (t/is (= 0 (:x result)))
(t/is (= 0 (:y result)))))) (t/is (= 0 (:y result))))))
(t/deftest test-parse-overlay-action-defaults-manual-position
(let [destination #js {"$id" (random-uuid)}
action (parser/parse-action
#js {:type "open-overlay"
:destination destination
:position "center"})]
(t/is (= :open-overlay (:action-type action)))
(t/is (= :center (:overlay-pos-type action)))
(t/is (gpt/point? (:overlay-position action)))
(t/is (= 0 (:x (:overlay-position action))))
(t/is (= 0 (:y (:overlay-position action))))))
(t/deftest test-parse-frame-guide-calls-guide-parser
(let [column (parser/parse-frame-guide
#js {:type "column"
:display true
:params #js {:type "stretch"
:size 12}})
row (parser/parse-frame-guide
#js {:type "row"
:display false
:params #js {:type "center"
:margin 4}})]
(t/is (= :column (:type column)))
(t/is (= true (:display column)))
(t/is (= :stretch (get-in column [:params :type])))
(t/is (= :row (:type row)))
(t/is (= false (:display row)))
(t/is (= :center (get-in row [:params :type])))))

View File

@ -0,0 +1,202 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS INC Sucursal en España SL
(ns frontend-tests.plugins.shape-bugfixes-test
(:require
[app.common.data :as d]
[app.common.test-helpers.files :as cthf]
[app.common.types.component :as ctk]
[app.common.uuid :as uuid]
[app.main.data.workspace :as dw]
[app.main.data.workspace.variants :as dwv]
[app.main.store :as st]
[app.plugins.api :as api]
[app.plugins.public-utils :as public-utils]
[app.plugins.shape :as shape]
[app.plugins.utils :as u]
[cljs.test :as t :include-macros true]
[frontend-tests.helpers.mock :as mock]
[frontend-tests.helpers.state :as ths]
[frontend-tests.helpers.wasm :as thw]))
(def ^:private plugin-id "00000000-0000-0000-0000-000000000000")
;; ---------------------------------------------------------------------------
;; Helpers
;; ---------------------------------------------------------------------------
(defn- child-shapes
"Ordered child shape ids of `board`, read back from the live store
(the observable result of a z-order operation)."
[store ^js context ^js board]
(let [file-id (aget (. context -currentFile) "$id")
page-id (aget (. context -currentPage) "$id")
board-id (aget board "$id")]
(get-in @store [:files file-id :data :pages-index page-id
:objects board-id :shapes])))
(defn- page-guides
"The guides map of the current page, read back from the live store."
[store ^js context]
(let [file-id (aget (. context -currentFile) "$id")
page-id (aget (. context -currentPage) "$id")]
(get-in @store [:files file-id :data :pages-index page-id :guides])))
;; ---------------------------------------------------------------------------
;; Tests
;; ---------------------------------------------------------------------------
(t/deftest trigger-setter-updates-the-interaction-event-type
;; Regression: the `trigger` setter must update the interaction of the
;; located shape. Asserting on the observable interaction (read back through
;; the proxy from the live store) covers that without coupling to which
;; internal action gets emitted.
(thw/with-wasm-mocks*
(fn []
(let [store (ths/setup-store (cthf/sample-file :file1 :page-label :page1))
^js context (api/create-context plugin-id)
_ (set! st/state store)
^js board (.createBoard context)]
(.addInteraction board "click" #js {:type "open-url" :url "https://example.com"})
(let [^js interaction (aget (.-interactions board) 0)]
(t/is (= "click" (.-trigger interaction))
"the interaction starts with the click trigger")
(set! (.-trigger interaction) "mouse-over")
(t/is (= "mouse-over" (.-trigger interaction))
"the trigger setter updates the interaction event-type"))))))
(t/deftest center-shapes-empty-input-returns-nil
(t/is (nil? (public-utils/centerShapes #js []))))
(t/deftest background-blur-reads-background-blur-key
(let [file-id (uuid/next)
page-id (uuid/next)
shape-id (uuid/next)
blur-id (uuid/next)
proxy (shape/shape-proxy plugin-id file-id page-id shape-id)]
(with-redefs [u/proxy->shape (constantly {:background-blur {:id blur-id
:value 12
:hidden false}})]
(let [blur (.-backgroundBlur proxy)]
(t/is (= (str blur-id) (aget blur "id")))
(t/is (= 12 (aget blur "value")))))))
(t/deftest flatten-returns-proxies-for-converted-shapes
;; `convert-selected-to-path` runs the WASM boolean/path pipeline, so this
;; test stays at the proxy boundary: it verifies `flatten` forwards the
;; selected ids to the conversion and wraps the result back into proxies.
(let [file-id (uuid/next)
page-id (uuid/next)
shape-id (uuid/next)
input (shape/shape-proxy plugin-id file-id page-id shape-id)
emitted (atom nil)
context (api/create-context plugin-id)]
(set! st/state (atom {:current-file-id file-id
:current-page-id page-id}))
(with-redefs [dw/convert-selected-to-path
(mock/stub (fn [ids]
(reset! emitted ids)
:convert-selected-to-path))
st/emit! mock/noop
shape/shape-proxy
(mock/stub (fn [_plugin file page id]
#js {"$file" file "$page" page "$id" id}))]
(let [result (.flatten context #js [input])]
(t/is (= #{shape-id} @emitted))
(t/is (array? result))
(t/is (= shape-id (aget result 0 "$id")))
(t/is (= file-id (aget result 0 "$file")))
(t/is (= page-id (aget result 0 "$page")))))))
(t/deftest z-order-methods-reorder-the-shape-within-its-parent
;; Asserts the observable child order in the parent after each z-order
;; method, instead of merely checking which location keyword was emitted.
;; The assertions are independent of the parent's `:shapes` ordering
;; convention: a reorder is verified by relative movement and extremes.
(thw/with-wasm-mocks*
(fn []
(let [store (ths/setup-store (cthf/sample-file :file1 :page-label :page1))
^js context (api/create-context plugin-id)
_ (set! st/state store)
^js board (.createBoard context)
children (mapv (fn [_] (.createRectangle context)) (range 4))
ids (mapv #(aget % "$id") children)
order #(child-shapes store context board)]
(doseq [^js c children] (.appendChild board c))
;; Operate on a shape that is currently interior (so both a forward
;; and a backward step are observable).
(let [mid-id (nth (order) 1)
^js mid (nth children (d/index-of ids mid-id))]
(t/testing "bringForward and sendBackward move in opposite directions"
(let [i0 (d/index-of (order) mid-id)
_ (.bringForward mid)
i1 (d/index-of (order) mid-id)
_ (.sendBackward mid)
i2 (d/index-of (order) mid-id)]
(t/is (not= i0 i1) "bringForward changes the order")
(t/is (not= i1 i2) "sendBackward changes the order")
(t/is (= (pos? (- i1 i0)) (neg? (- i2 i1)))
"the two steps move the shape in opposite directions")))
(t/testing "bringToFront and sendToBack move to opposite extremes"
(let [n (count (order))
_ (.bringToFront mid)
p1 (d/index-of (order) mid-id)
_ (.sendToBack mid)
p2 (d/index-of (order) mid-id)]
(t/is (contains? #{0 (dec n)} p1) "bringToFront moves to an extreme")
(t/is (contains? #{0 (dec n)} p2) "sendToBack moves to an extreme")
(t/is (not= p1 p2) "front and back are opposite extremes"))))))))
(t/deftest is-variant-container-predicate-returns-boolean
(t/is (false? (ctk/is-variant-container? {})))
(t/is (true? (ctk/is-variant-container? {:is-variant-container true}))))
(t/deftest combine-as-variants-uses-the-passed-component-ids
;; `combine-as-variants` needs real main components and the variant pipeline,
;; so this stays at the proxy boundary and verifies the component ids that
;; the head proxy collects from its argument before delegating.
(let [file-id (uuid/next)
page-id (uuid/next)
head-id (uuid/next)
other-id (uuid/next)
proxy (shape/shape-proxy plugin-id file-id page-id head-id)
captured (atom nil)]
(with-redefs [u/locate-shape (fn [_file _page id] {:id id :component-id id})
u/locate-library-component (constantly {:id (uuid/next)})
ctk/is-variant? (constantly false)
dwv/combine-as-variants
(fn [ids opts]
(reset! captured {:ids ids :opts opts})
;; return value flows through `se/add-event` (which
;; calls `with-meta`), so it must support metadata
{:event :combine-as-variants})
st/emit! mock/noop
shape/shape-proxy (mock/stub (fn [& _] #js {}))]
(.combineAsVariants proxy #js [(str other-id)])
(t/is (= #{head-id other-id} (:ids @captured))))))
(t/deftest remove-ruler-guide-deletes-the-guide-from-the-page
;; Adds a real ruler guide through the API and asserts it is gone from the
;; page guides after removeRulerGuide, rather than checking the removal call.
(thw/with-wasm-mocks*
(fn []
(let [store (ths/setup-store (cthf/sample-file :file1 :page-label :page1))
^js context (api/create-context plugin-id)
_ (set! st/state store)
^js board (.createBoard context)
^js guide (.addRulerGuide board "horizontal" 10)]
(t/is (= 1 (count (page-guides store context)))
"addRulerGuide stores one guide on the page")
(.removeRulerGuide board guide)
(t/is (empty? (page-guides store context))
"removeRulerGuide deletes the guide from the page")))))
(t/deftest group-empty-input-returns-nil
(let [context (api/create-context plugin-id)]
(t/is (nil? (.group context #js [])))))

View File

@ -0,0 +1,87 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS INC
(ns frontend-tests.plugins.text-test
(:require
[app.main.data.workspace.texts :as dwt]
[app.main.store :as st]
[app.plugins.fonts :as fonts]
[app.plugins.format :as format]
[app.plugins.register :as r]
[app.plugins.shape :as shape]
[app.plugins.text :as plugins.text]
[app.plugins.utils :as u]
[cljs.test :as t :include-macros true]
[frontend-tests.helpers.mock :as mock]))
(def ^:private plugin-id "00000000-0000-0000-0000-000000000000")
(t/deftest font-apply-to-text-uses-font-id-not-shape-id
(let [file-id (random-uuid)
page-id (random-uuid)
shape-id (random-uuid)
font (fonts/font-proxy
plugin-id
{:id "font-id"
:family "Inter"
:name "Inter"
:variants [{:id "regular"
:name "Regular"
:weight "400"
:style "normal"}]})
text (shape/shape-proxy plugin-id file-id page-id shape-id)
captured (atom nil)]
(with-redefs [r/check-permission (constantly true)
u/page-active? (constantly true)
dwt/update-attrs
(fn [id attrs]
(reset! captured {:id id :attrs attrs})
:update-attrs)
st/emit! mock/noop]
(.applyToText font text nil)
(t/is (= shape-id (:id @captured)))
(t/is (= "font-id" (get-in @captured [:attrs :font-id]))))))
(t/deftest font-apply-to-range-uses-hidden-range-bounds
(let [file-id (random-uuid)
page-id (random-uuid)
shape-id (random-uuid)
font (fonts/font-proxy
plugin-id
{:id "font-id"
:family "Inter"
:name "Inter"
:variants [{:id "regular"
:name "Regular"
:weight "400"
:style "normal"}]})
range (plugins.text/text-range-proxy plugin-id file-id page-id shape-id 1 4)
captured (atom nil)]
(with-redefs [r/check-permission (constantly true)
u/page-active? (constantly true)
dwt/update-text-range
(fn [id start end attrs]
(reset! captured {:id id
:start start
:end end
:attrs attrs})
:update-text-range)
st/emit! mock/noop]
(.applyToRange font range nil)
(t/is (= shape-id (:id @captured)))
(t/is (= 1 (:start @captured)))
(t/is (= 4 (:end @captured)))
(t/is (= "font-id" (get-in @captured [:attrs :font-id]))))))
(t/deftest text-range-shape-returns-a-shape-proxy
(let [file-id (random-uuid)
page-id (random-uuid)
shape-id (random-uuid)
range (plugins.text/text-range-proxy plugin-id file-id page-id shape-id 0 3)]
(with-redefs [format/shape-proxy shape/shape-proxy]
(let [text-shape (.-shape range)]
(t/is (shape/shape-proxy? text-shape))
(t/is (= shape-id (aget text-shape "$id")))))))

View File

@ -12,15 +12,20 @@
[app.common.test-helpers.tokens :as ctht] [app.common.test-helpers.tokens :as ctht]
[app.common.types.tokens-lib :as ctob] [app.common.types.tokens-lib :as ctob]
[app.main.data.tokenscript :as ts] [app.main.data.tokenscript :as ts]
[app.main.data.workspace.tokens.library-edit :as dwtl]
[app.main.store :as st] [app.main.store :as st]
[app.plugins.api :as api] [app.plugins.api :as api]
[app.plugins.tokens :as ptok] [app.plugins.tokens :as ptok]
[app.plugins.utils :as u]
[cljs.test :as t :include-macros true] [cljs.test :as t :include-macros true]
[frontend-tests.helpers.mock :as mock]
[frontend-tests.helpers.state :as ths] [frontend-tests.helpers.state :as ths]
[potok.v2.core :as ptk])) [potok.v2.core :as ptk]))
(t/use-fixtures :each {:before cthi/reset-idmap!}) (t/use-fixtures :each {:before cthi/reset-idmap!})
(def ^:private get-resolved-value @#'ptok/get-resolved-value)
;; Regression coverage for issue #9162. ;; Regression coverage for issue #9162.
;; ;;
;; Plugin code calling `shape.applyToken(token, ["fill"])` or ;; Plugin code calling `shape.applyToken(token, ["fill"])` or
@ -226,3 +231,110 @@
{:keys [errors resolved-value]} (get resolved (:name token))] {:keys [errors resolved-value]} (get resolved (:name token))]
(t/is (nil? resolved-value)) (t/is (nil? resolved-value))
(t/is (seq errors)))) (t/is (seq errors))))
(t/deftest token-set-duplicate-returns-the-duplicated-set
(let [file-id (cthi/new-id! :file)
set-id (cthi/new-id! :set)
dup-id (cthi/new-id! :dup)
proxy (ptok/token-set-proxy "plugin-id" file-id set-id)]
(with-redefs [dwtl/duplicate-token-set
(mock/stub (fn [id {:keys [id-ref]}]
(t/is (= set-id id))
(reset! id-ref dup-id)
:duplicate-token-set))
st/emit! mock/noop]
(let [dup (.duplicate proxy)]
(t/is (ptok/token-set-proxy? dup))
(t/is (= (str dup-id) (.-id dup)))))))
(t/deftest theme-add-set-and-remove-set-use-the-set-name
(let [file-id (cthi/new-id! :file)
theme-id (cthi/new-id! :theme)
set-id (cthi/new-id! :set)
set (ptok/token-set-proxy "plugin-id" file-id set-id "Primitives")
theme (ptok/token-theme-proxy "plugin-id" file-id theme-id)
captured (atom [])]
(with-redefs [u/locate-token-theme
(fn [_file _theme]
(ctob/make-token-theme :id theme-id
:name "Theme"
:sets #{"Primitives"}))
dwtl/update-token-theme
(fn [id theme]
(swap! captured conj {:id id :theme theme})
:update-token-theme)
st/emit! identity]
(.addSet theme set)
(.removeSet theme set)
(t/is (= [theme-id theme-id] (mapv :id @captured)))
(t/is (contains? (-> @captured first :theme :sets) "Primitives"))
(t/is (not (contains? (-> @captured second :theme :sets) "Primitives"))))))
(t/deftest font-family-token-value-accepts-a-string
(let [file-id (cthi/new-id! :file)
set-id (cthi/new-id! :set)
token-id (cthi/new-id! :token)
captured (atom nil)]
(with-redefs [u/locate-token (constantly {:id token-id
:name "font.primary"
:type :font-family
:value ["Inter"]})
dwtl/update-token (mock/stub (fn [set-id token-id attrs]
(reset! captured {:set-id set-id
:token-id token-id
:attrs attrs})
:update-token))
st/emit! mock/noop]
(let [token (ptok/token-proxy "plugin-id" file-id set-id token-id)]
(set! (.-value token) "Inter, Arial")
(t/is (= set-id (:set-id @captured)))
(t/is (= token-id (:token-id @captured)))
(t/is (= ["Inter" "Arial"] (get-in @captured [:attrs :value])))))))
(t/deftest typography-token-resolved-value-is-plugin-array-shape
(let [token (ctob/make-token
{:name "type.body"
:type :typography
:value {:font-family ["Inter" "Arial"]
:font-size "16px"
:font-weight "600"
:line-height "20px"
:letter-spacing "1"
:text-case "uppercase"
:text-decoration "underline"}})
result (get-resolved-value token {(:name token) token})
entry (aget result 0)]
(t/is (array? result))
(t/is (= ["Inter" "Arial"] (vec (aget entry "fontFamilies"))))
(t/is (= 16 (aget entry "fontSizes")))
(t/is (= "600" (aget entry "fontWeights")))
(t/is (= 20 (aget entry "lineHeight")))
(t/is (= "uppercase" (aget entry "textCase")))
(t/is (= "underline" (aget entry "textDecoration")))))
(t/deftest shadow-token-resolved-value-is-plugin-array-shape
(let [token (ctob/make-token
{:name "shadow.card"
:type :shadow
:value [{:offset-x "1px"
:offset-y "2px"
:blur "3px"
:spread "4px"
:color "#000000"
:inset false}]})
result (get-resolved-value token {(:name token) token})
entry (aget result 0)]
(t/is (array? result))
(t/is (= 1 (aget entry "offsetX")))
(t/is (= 2 (aget entry "offsetY")))
(t/is (= 3 (aget entry "blur")))
(t/is (= 4 (aget entry "spread")))))
(t/deftest font-family-token-resolved-value-is-string-array
(let [token (ctob/make-token
{:name "font.primary"
:type :font-family
:value ["Inter" "Arial"]})
result (get-resolved-value token {(:name token) token})]
(t/is (array? result))
(t/is (= ["Inter" "Arial"] (vec result)))))

View File

@ -16,6 +16,8 @@
in :fetching) and are permanently stuck with fallback-font layout metrics." in :fetching) and are permanently stuck with fallback-font layout metrics."
(:require (:require
[app.render-wasm.api :as wasm.api] [app.render-wasm.api :as wasm.api]
[app.render-wasm.mem :as mem]
[app.render-wasm.wasm :as wasm]
[beicon.v2.core :as rx] [beicon.v2.core :as rx]
[cljs.test :as t :include-macros true])) [cljs.test :as t :include-macros true]))
@ -108,3 +110,21 @@
;; process-pending fires update-text-layouts, it covers shape-b too. ;; process-pending fires update-text-layouts, it covers shape-b too.
(t/is (= 2 (count (:shapes @captured))) (t/is (= 2 (count (:shapes @captured)))
"Both shapes are in process-pending so font-load covers all of them"))) "Both shapes are in process-pending so font-load covers all of them")))
(t/deftest empty-grid-tracks-do-not-allocate-zero-bytes
(let [calls (atom [])
;; `h/call` is a macro that resolves the wasm function off the module
;; via `unchecked-get`, so it cannot be redefined. Mock the module
;; itself with recording stubs and let the real macro expansion run.
module #js {"_set_grid_rows" (fn [& _] (swap! calls conj [:call "_set_grid_rows"]) nil)
"_set_grid_columns" (fn [& _] (swap! calls conj [:call "_set_grid_columns"]) nil)}]
(with-redefs [mem/alloc (fn [size]
(swap! calls conj [:alloc size])
0)
wasm/internal-module module]
(wasm.api/set-grid-layout-rows [])
(wasm.api/set-grid-layout-columns []))
(t/is (not-any? #(= :alloc (first %)) @calls))
(t/is (= [[:call "_set_grid_rows"]
[:call "_set_grid_columns"]]
@calls))))

View File

@ -27,12 +27,19 @@
[frontend-tests.logic.groups-test] [frontend-tests.logic.groups-test]
[frontend-tests.logic.pasting-in-containers-test] [frontend-tests.logic.pasting-in-containers-test]
[frontend-tests.main-errors-test] [frontend-tests.main-errors-test]
[frontend-tests.plugins.comments-test]
[frontend-tests.plugins.context-shapes-test] [frontend-tests.plugins.context-shapes-test]
[frontend-tests.plugins.file-test]
[frontend-tests.plugins.format-test] [frontend-tests.plugins.format-test]
[frontend-tests.plugins.grid-test]
[frontend-tests.plugins.interactions-test] [frontend-tests.plugins.interactions-test]
[frontend-tests.plugins.library-test]
[frontend-tests.plugins.local-storage-test]
[frontend-tests.plugins.page-active-validation-test] [frontend-tests.plugins.page-active-validation-test]
[frontend-tests.plugins.page-test] [frontend-tests.plugins.page-test]
[frontend-tests.plugins.parser-test] [frontend-tests.plugins.parser-test]
[frontend-tests.plugins.shape-bugfixes-test]
[frontend-tests.plugins.text-test]
[frontend-tests.plugins.tokens-test] [frontend-tests.plugins.tokens-test]
[frontend-tests.plugins.utils-test] [frontend-tests.plugins.utils-test]
[frontend-tests.render-wasm.process-objects-test] [frontend-tests.render-wasm.process-objects-test]
@ -65,7 +72,8 @@
(.exit js/process 1))) (.exit js/process 1)))
(def test-namespaces (def test-namespaces
['frontend-tests.basic-shapes-test ['frontend-tests.plugins.text-test
'frontend-tests.basic-shapes-test
'frontend-tests.code-gen-style-test 'frontend-tests.code-gen-style-test
'frontend-tests.copy-as-svg-test 'frontend-tests.copy-as-svg-test
'frontend-tests.data.nitrate-test 'frontend-tests.data.nitrate-test
@ -89,11 +97,20 @@
'frontend-tests.logic.groups-test 'frontend-tests.logic.groups-test
'frontend-tests.logic.pasting-in-containers-test 'frontend-tests.logic.pasting-in-containers-test
'frontend-tests.plugins.context-shapes-test 'frontend-tests.plugins.context-shapes-test
'frontend-tests.plugins.comments-test
'frontend-tests.plugins.file-test
'frontend-tests.plugins.format-test
'frontend-tests.plugins.grid-test
'frontend-tests.plugins.interactions-test
'frontend-tests.plugins.library-test
'frontend-tests.plugins.local-storage-test
'frontend-tests.plugins.page-active-validation-test 'frontend-tests.plugins.page-active-validation-test
'frontend-tests.plugins.interactions-test 'frontend-tests.plugins.interactions-test
'frontend-tests.plugins.format-test 'frontend-tests.plugins.format-test
'frontend-tests.plugins.page-test 'frontend-tests.plugins.page-test
'frontend-tests.plugins.parser-test 'frontend-tests.plugins.parser-test
'frontend-tests.plugins.shape-bugfixes-test
'frontend-tests.plugins.text-test
'frontend-tests.plugins.tokens-test 'frontend-tests.plugins.tokens-test
'frontend-tests.plugins.utils-test 'frontend-tests.plugins.utils-test
'frontend-tests.svg-fills-test 'frontend-tests.svg-fills-test

View File

@ -1,22 +1,36 @@
## 1.5.0 (Unreleased) ## 1.5.0 (Unreleased)
### 💣 Breaking changes & Deprecations
- **plugins-runtime**: changes outside the current page now raise a validation error when the target belongs to a page that is not currently active, instead of silently operating on the active page. - **plugins-runtime**: changes outside the current page now raise a validation error when the target belongs to a page that is not currently active, instead of silently operating on the active page.
- **plugins-runtime**: Fix inverted validation that rejected valid values (and accepted invalid ones) on text range `align`, `direction`, `textDecoration`, `letterSpacing` and on layout child `zIndex`. - **plugin-types**: Change return type of `combineAsVariants`
- **plugins-runtime**: Array-typed properties (e.g. `page.flows`, `shape.exports`, `shape.shadows`, layout `rows`/`columns`, ruler guides, path `commands`) now always return an array, returning an empty array instead of `null` when there are no items - **plugin-types:** Deprecate the legacy `Image` shape interface — image shapes exist only for backward compatibility with old files; new images are embedded in a `Fill` via its `fillImage` (an `ImageData`).
- We've solved several inconsistencies accross the API, if you relied on an undocumented property or method be aware that might have changed.
### 🚀 Features
- **plugins-runtime**: Added `version` field that returns the current version - **plugins-runtime**: Added `version` field that returns the current version
- **plugins-runtime**: Added optional parameter `throwOnError` to `penpot.ui.sendMessage` (default false, backwards-compatible) - **plugins-runtime**: Added optional parameter `throwOnError` to `penpot.ui.sendMessage` (default false, backwards-compatible)
- **plugin-types**: Added a flags subcontexts with the flag `naturalChildrenOrdering` - **plugin-types**: Added a flags subcontexts with the flag `naturalChildrenOrdering`
- **plugin-types**: Added flag `throwValidationErrors` to enable exceptions on validation
- **plugin-types**: `penpot.openPage()` now returns `Promise<void>` and should be awaited before performing operations on the new page - **plugin-types**: `penpot.openPage()` now returns `Promise<void>` and should be awaited before performing operations on the new page
- **plugin-types**: Fix penpot.openPage() to navigate in same tab by default
- **plugin-types:** Change `LibraryComponent.isVariant()` return type to type guard `this is LibraryVariantComponent` - **plugin-types:** Change `LibraryComponent.isVariant()` return type to type guard `this is LibraryVariantComponent`
- **plugin-types**: Added `createVariantFromComponents` - **plugin-types**: Added `createVariantFromComponents`
- **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**: Fix missing `webp` export format in `Export.type` - **plugin-types**: Fix missing `webp` export format in `Export.type`
- **plugin-types**: Added `fixedWhenScrolling` property for shapes - **plugin-types**: Added `fixedWhenScrolling` property for shapes
- **plugin-runtime:** `addToken` now resolves references against all token sets, allowing references to tokens in inactive sets - **plugin-runtime:** `addToken` now resolves references against all token sets, allowing references to tokens in inactive sets
- **plugin-types:** `TokenCatalog.addSet` now accepts an optional `active` flag to create an already-active set (sets are inactive by default) - **plugin-types:** `TokenCatalog.addSet` now accepts an optional `active` flag to create an already-active set (sets are inactive by default)
- **plugin-runtime:** A `fontFamilies` token's `resolvedValue` now returns the documented `string[]` (the resolved family list) instead of leaking the raw tokenscript list symbol
### 🩹 Fixes
- **plugins-runtime**: Fix inverted validation that rejected valid values (and accepted invalid ones) on text range `align`, `direction`, `textDecoration`, `letterSpacing` and on layout child `zIndex`.
- **plugins-runtime**: Array-typed properties (e.g. `page.flows`, `shape.exports`, `shape.shadows`, layout `rows`/`columns`, ruler guides, path `commands`) now always return an array, returning an empty array instead of `null` when there are no items
- **plugin-types**: Fix penpot.openPage() to navigate in same tab by default
- **plugin-types**: Rename `LibraryTypography.fontFamilies` to `fontFamily` to match the runtime (it holds a single font family, not an array)
- **plugin-runtime:** Setting a `LibraryColor`'s `gradient` or `image` now clears the other color representations (solid/gradient/image are mutually exclusive), so the result is a valid color instead of being rejected with "expected valid color"
- **plugin-types:** Mark members that have no runtime setter as `readonly`, fixing a mismatch where they were typed as writable: font metadata (`Font.*`, `FontVariant.*`, `FontsContext.all`), the `Ellipse`/`Image`/`SvgRaw` `type` discriminants (now consistent with the other shapes), `File.name`/`pages`/`revn`, `Page.root`, `TokenTheme.activeSets`, `Variants.properties`, `ImageData.*`, the board guide value objects (`GuideColumn`/`GuideRow`/`GuideSquare` and their params — `board.guides` returns a formatted snapshot, so reconfiguring means reassigning the whole array), the `Point` and `Bounds` value objects, the `Penpot.ui`/`Penpot.utils` subcontexts, the derived `Boolean` path data (`d`/`content`/`commands` are computed from the operands; `Boolean` is not editable like a `Path`), and the `EventsMap` event entries (a type-only event→callback map, never assigned). Members that do expose a setter stay writable: `Board.children`, `Path.d`/`content`/`commands` and `FileVersion.label`.
## 1.4.2 (2026-01-21) ## 1.4.2 (2026-01-21)

View File

@ -6,7 +6,7 @@ export interface Penpot extends Omit<
Context, Context,
'addListener' | 'removeListener' 'addListener' | 'removeListener'
> { > {
ui: { readonly ui: {
/** /**
* Opens the plugin UI. It is possible to develop a plugin without interface (see Palette color example) but if you need, the way to open this UI is using `penpot.ui.open`. * Opens the plugin UI. It is possible to develop a plugin without interface (see Palette color example) but if you need, the way to open this UI is using `penpot.ui.open`.
* There is a minimum and maximum size for this modal and a default size but it's possible to customize it anyway with the options parameter. * There is a minimum and maximum size for this modal and a default size but it's possible to customize it anyway with the options parameter.
@ -84,7 +84,7 @@ export interface Penpot extends Omit<
/** /**
* Provides access to utility functions and context-specific operations. * Provides access to utility functions and context-specific operations.
*/ */
utils: ContextUtils; readonly utils: ContextUtils;
/** /**
* Closes the plugin. When this method is called the UI will be closed. * Closes the plugin. When this method is called the UI will be closed.
* *
@ -390,17 +390,17 @@ export interface Boolean extends ShapeBase {
* The content of the boolean shape, defined as the path string. * The content of the boolean shape, defined as the path string.
* @deprecated Use either `d` or `commands`. * @deprecated Use either `d` or `commands`.
*/ */
content: string; readonly content: string;
/** /**
* The content of the boolean shape, defined as the path string. * The content of the boolean shape, defined as the path string.
*/ */
d: string; readonly d: string;
/** /**
* The content of the boolean shape, defined as an array of path commands. * The content of the boolean shape, defined as an array of path commands.
*/ */
commands: Array<PathCommand>; readonly commands: Array<PathCommand>;
/** /**
* The fills applied to the shape. * The fills applied to the shape.
@ -455,19 +455,19 @@ export type Bounds = {
/** /**
* Top-left x position of the rectangular area defined * Top-left x position of the rectangular area defined
*/ */
x: number; readonly x: number;
/** /**
* Top-left y position of the rectangular area defined * Top-left y position of the rectangular area defined
*/ */
y: number; readonly y: number;
/** /**
* Width of the represented area * Width of the represented area
*/ */
width: number; readonly width: number;
/** /**
* Height of the represented area * Height of the represented area
*/ */
height: number; readonly height: number;
}; };
/** /**
@ -1517,7 +1517,7 @@ export interface Ellipse extends ShapeBase {
/** /**
* The type of the shape, which is always 'ellipse' for ellipse shapes. * The type of the shape, which is always 'ellipse' for ellipse shapes.
*/ */
type: 'ellipse'; readonly type: 'ellipse';
/** /**
* The fills applied to the shape. * The fills applied to the shape.
@ -1540,37 +1540,37 @@ export interface EventsMap {
/** /**
* The `pagechange` event is triggered when the active page in the project is changed. * The `pagechange` event is triggered when the active page in the project is changed.
*/ */
pagechange: Page; readonly pagechange: Page;
/** /**
* The `filechange` event is triggered when a different file is opened. * The `filechange` event is triggered when a different file is opened.
* The callback will receive the new file. * The callback will receive the new file.
*/ */
filechange: File; readonly filechange: File;
/** /**
* The `selectionchange` event is triggered when the selection of elements changes. * The `selectionchange` event is triggered when the selection of elements changes.
* This event passes a list of identifiers of the selected elements. * This event passes a list of identifiers of the selected elements.
*/ */
selectionchange: string[]; readonly selectionchange: string[];
/** /**
* The `themechange` event is triggered when the application theme is changed. * The `themechange` event is triggered when the application theme is changed.
*/ */
themechange: Theme; readonly themechange: Theme;
/** /**
* The `finish` event is triggered when the current file is closed. * The `finish` event is triggered when the current file is closed.
* The callback will receive the id of the closed file. * The callback will receive the id of the closed file.
*/ */
finish: string; readonly finish: string;
/** /**
* This event will trigger whenever the shape in the props change. It's mandatory to send * This event will trigger whenever the shape in the props change. It's mandatory to send
* with the props an object like `{ shapeId: '<id>' }` * with the props an object like `{ shapeId: '<id>' }`
*/ */
shapechange: Shape; readonly shapechange: Shape;
/** /**
* The `contentsave` event will trigger after the file content is saved in the backend. * The `contentsave` event will trigger after the file content is saved in the backend.
*/ */
contentsave: void; readonly contentsave: void;
} }
/** /**
@ -1609,17 +1609,17 @@ export interface File extends PluginData {
/** /**
* The `name` for the file * The `name` for the file
*/ */
name: string; readonly name: string;
/** /**
* The `revn` will change for every document update * The `revn` will change for every document update
*/ */
revn: number; readonly revn: number;
/** /**
* List all the pages for the current file * List all the pages for the current file
*/ */
pages: Page[]; readonly pages: Page[];
/** /**
* Export the current file to an archive. * Export the current file to an archive.
@ -1819,37 +1819,37 @@ export interface Font {
/** /**
* This property holds the human-readable name of the font. * This property holds the human-readable name of the font.
*/ */
name: string; readonly name: string;
/** /**
* The unique identifier of the font. * The unique identifier of the font.
*/ */
fontId: string; readonly fontId: string;
/** /**
* The font family of the font. * The font family of the font.
*/ */
fontFamily: string; readonly fontFamily: string;
/** /**
* The default font style of the font. * The default font style of the font.
*/ */
fontStyle?: 'normal' | 'italic' | null; readonly fontStyle?: 'normal' | 'italic' | null;
/** /**
* The default font variant ID of the font. * The default font variant ID of the font.
*/ */
fontVariantId: string; readonly fontVariantId: string;
/** /**
* The default font weight of the font. * The default font weight of the font.
*/ */
fontWeight: string; readonly fontWeight: string;
/** /**
* An array of font variants available for the font. * An array of font variants available for the font.
*/ */
variants: FontVariant[]; readonly variants: FontVariant[];
/** /**
* Applies the font styles to a text shape. * Applies the font styles to a text shape.
@ -1884,22 +1884,22 @@ export interface FontVariant {
/** /**
* The name of the font variant. * The name of the font variant.
*/ */
name: string; readonly name: string;
/** /**
* The unique identifier of the font variant. * The unique identifier of the font variant.
*/ */
fontVariantId: string; readonly fontVariantId: string;
/** /**
* The font weight of the font variant. * The font weight of the font variant.
*/ */
fontWeight: string; readonly fontWeight: string;
/** /**
* The font style of the font variant. * The font style of the font variant.
*/ */
fontStyle: 'normal' | 'italic'; readonly fontStyle: 'normal' | 'italic';
} }
/** /**
@ -1910,7 +1910,7 @@ export interface FontsContext {
/** /**
* An array containing all available fonts. * An array containing all available fonts.
*/ */
all: Font[]; readonly all: Font[];
/** /**
* Finds a font by its unique identifier. * Finds a font by its unique identifier.
@ -2208,15 +2208,15 @@ export interface GuideColumn {
/** /**
* The type of the guide, which is always 'column' for column guides. * The type of the guide, which is always 'column' for column guides.
*/ */
type: 'column'; readonly type: 'column';
/** /**
* Specifies whether the column guide is displayed. * Specifies whether the column guide is displayed.
*/ */
display: boolean; readonly display: boolean;
/** /**
* The parameters defining the appearance and layout of the column guides. * The parameters defining the appearance and layout of the column guides.
*/ */
params: GuideColumnParams; readonly params: GuideColumnParams;
} }
/** /**
@ -2227,7 +2227,7 @@ export interface GuideColumnParams {
/** /**
* The color configuration for the column guides. * The color configuration for the column guides.
*/ */
color: { color: string; opacity: number }; readonly color: { color: string; opacity: number };
/** /**
* The optional alignment type of the column guides. * The optional alignment type of the column guides.
* - 'stretch': Columns stretch to fit the available space. * - 'stretch': Columns stretch to fit the available space.
@ -2235,23 +2235,23 @@ export interface GuideColumnParams {
* - 'center': Columns align to the center. * - 'center': Columns align to the center.
* - 'right': Columns align to the right. * - 'right': Columns align to the right.
*/ */
type?: 'stretch' | 'left' | 'center' | 'right'; readonly type?: 'stretch' | 'left' | 'center' | 'right';
/** /**
* The optional size of each column. * The optional size of each column.
*/ */
size?: number; readonly size?: number;
/** /**
* The optional margin between the columns and the board edges. * The optional margin between the columns and the board edges.
*/ */
margin?: number; readonly margin?: number;
/** /**
* The optional length of each item within the columns. * The optional length of each item within the columns.
*/ */
itemLength?: number; readonly itemLength?: number;
/** /**
* The optional gutter width between columns. * The optional gutter width between columns.
*/ */
gutter?: number; readonly gutter?: number;
} }
/** /**
@ -2262,16 +2262,16 @@ export interface GuideRow {
/** /**
* The type of the guide, which is always 'row' for row guides. * The type of the guide, which is always 'row' for row guides.
*/ */
type: 'row'; readonly type: 'row';
/** /**
* Specifies whether the row guide is displayed. * Specifies whether the row guide is displayed.
*/ */
display: boolean; readonly display: boolean;
/** /**
* The parameters defining the appearance and layout of the row guides. * The parameters defining the appearance and layout of the row guides.
* Note: This reuses the same parameter structure as column guides. * Note: This reuses the same parameter structure as column guides.
*/ */
params: GuideColumnParams; readonly params: GuideColumnParams;
} }
/** /**
@ -2282,15 +2282,15 @@ export interface GuideSquare {
/** /**
* The type of the guide, which is always 'square' for square guides. * The type of the guide, which is always 'square' for square guides.
*/ */
type: 'square'; readonly type: 'square';
/** /**
* Specifies whether the square guide is displayed. * Specifies whether the square guide is displayed.
*/ */
display: boolean; readonly display: boolean;
/** /**
* The parameters defining the appearance and layout of the square guides. * The parameters defining the appearance and layout of the square guides.
*/ */
params: GuideSquareParams; readonly params: GuideSquareParams;
} }
/** /**
@ -2301,11 +2301,11 @@ export interface GuideSquareParams {
/** /**
* The color configuration for the square guides. * The color configuration for the square guides.
*/ */
color: { color: string; opacity: number }; readonly color: { color: string; opacity: number };
/** /**
* The optional size of each square guide. * The optional size of each square guide.
*/ */
size?: number; readonly size?: number;
} }
/** /**
@ -2334,12 +2334,14 @@ export interface HistoryContext {
/** /**
* Represents an image shape in Penpot. * Represents an image shape in Penpot.
* This interface extends `ShapeBase` and includes properties specific to image shapes. * This interface extends `ShapeBase` and includes properties specific to image shapes.
* @deprecated Image shapes exist only for backward compatibility with old files.
* New images are embedded in a `Fill` via its `fillImage` (an `ImageData`).
*/ */
export interface Image extends ShapeBase { export interface Image extends ShapeBase {
/** /**
* The type of the shape, which is always 'image' for image shapes. * The type of the shape, which is always 'image' for image shapes.
*/ */
type: 'image'; readonly type: 'image';
/** /**
* The fills applied to the shape. * The fills applied to the shape.
@ -2355,28 +2357,28 @@ export type ImageData = {
/** /**
* The optional name of the image. * The optional name of the image.
*/ */
name?: string; readonly name?: string;
/** /**
* The width of the image. * The width of the image.
*/ */
width: number; readonly width: number;
/** /**
* The height of the image. * The height of the image.
*/ */
height: number; readonly height: number;
/** /**
* The optional media type of the image (e.g., 'image/png', 'image/jpeg'). * The optional media type of the image (e.g., 'image/png', 'image/jpeg').
*/ */
mtype?: string; readonly mtype?: string;
/** /**
* The unique identifier for the image. * The unique identifier for the image.
*/ */
id: string; readonly id: string;
/** /**
* Whether to keep the aspect ratio of the image when resizing. * Whether to keep the aspect ratio of the image when resizing.
* Defaults to false if omitted. * Defaults to false if omitted.
*/ */
keepAspectRatio?: boolean; readonly keepAspectRatio?: boolean;
/** /**
* Returns the image data as a byte array. * Returns the image data as a byte array.
@ -2870,9 +2872,9 @@ export interface LibraryTypography extends LibraryElement {
fontId: string; fontId: string;
/** /**
* The font families of the typography element. * The font family of the typography element.
*/ */
fontFamilies: string; fontFamily: string;
/** /**
* The unique identifier of the font variant used in the typography element. * The unique identifier of the font variant used in the typography element.
@ -3097,7 +3099,7 @@ export interface Page extends PluginData {
* The root shape of the current page. Will be the parent shape of all the shapes inside the document. * The root shape of the current page. Will be the parent shape of all the shapes inside the document.
* Requires `content:read` permission. * Requires `content:read` permission.
*/ */
root: Shape; readonly root: Shape;
/** /**
* Retrieves a shape by its unique identifier. * Retrieves a shape by its unique identifier.
@ -3437,7 +3439,7 @@ export interface PluginData {
/** /**
* Point represents a point in 2D space, typically with x and y coordinates. * Point represents a point in 2D space, typically with x and y coordinates.
*/ */
export type Point = { x: number; y: number }; export type Point = { readonly x: number; readonly y: number };
/** /**
* It takes back to the last board shown. * It takes back to the last board shown.
@ -4126,7 +4128,7 @@ export interface SvgRaw extends ShapeBase {
/** /**
* The type of the shape, which is always 'svg-raw' for raw SVG shapes. * The type of the shape, which is always 'svg-raw' for raw SVG shapes.
*/ */
type: 'svg-raw'; readonly type: 'svg-raw';
} }
/** /**
@ -5297,7 +5299,7 @@ export interface TokenTheme {
/** /**
* The sets that will be activated if this theme is activated. * The sets that will be activated if this theme is activated.
*/ */
activeSets: TokenSet[]; readonly activeSets: TokenSet[];
/** /**
* Adds a set to the list of the theme. * Adds a set to the list of the theme.
@ -5563,7 +5565,7 @@ export interface Variants {
/** /**
* A list with the names of the properties of the Variant * A list with the names of the properties of the Variant
*/ */
properties: string[]; readonly properties: string[];
/** /**
* A list of all the values of a property across all the VariantComponents of this Variant * A list of all the values of a property across all the VariantComponents of this Variant

View File

@ -174,7 +174,7 @@ pub extern "C" fn set_grid_layout_data(
#[no_mangle] #[no_mangle]
#[wasm_error] #[wasm_error]
pub extern "C" fn set_grid_columns() -> Result<()> { pub extern "C" fn set_grid_columns() -> Result<()> {
let bytes = mem::bytes(); let bytes = mem::bytes_or_empty();
let entries: Vec<GridTrack> = bytes let entries: Vec<GridTrack> = bytes
.chunks(size_of::<RawGridTrack>()) .chunks(size_of::<RawGridTrack>())
@ -197,7 +197,7 @@ pub extern "C" fn set_grid_columns() -> Result<()> {
#[no_mangle] #[no_mangle]
#[wasm_error] #[wasm_error]
pub extern "C" fn set_grid_rows() -> Result<()> { pub extern "C" fn set_grid_rows() -> Result<()> {
let bytes = mem::bytes(); let bytes = mem::bytes_or_empty();
let entries: Vec<GridTrack> = bytes let entries: Vec<GridTrack> = bytes
.chunks(size_of::<RawGridTrack>()) .chunks(size_of::<RawGridTrack>())
@ -220,7 +220,7 @@ pub extern "C" fn set_grid_rows() -> Result<()> {
#[no_mangle] #[no_mangle]
#[wasm_error] #[wasm_error]
pub extern "C" fn set_grid_cells() -> Result<()> { pub extern "C" fn set_grid_cells() -> Result<()> {
let bytes = mem::bytes(); let bytes = mem::bytes_or_empty();
let cells: Vec<RawGridCell> = bytes let cells: Vec<RawGridCell> = bytes
.chunks(size_of::<RawGridCell>()) .chunks(size_of::<RawGridCell>())