✨ Add token source to plugins (#12183)

* ✨ Add token source to plugins

* ✨ Changes after review

* ✨ Move plugins code to library-edit
This commit is contained in:
Alonso Torres 2026-10-16 17:58:51 +02:00 committed by GitHub
parent 01aacde0a8
commit 02e10a005a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 717 additions and 45 deletions

View File

@ -523,7 +523,9 @@
(effective-tokens-source? file-data (:id file-data))))
(defn editable-tokens?
"Returns true if the file-data is its own tokens source."
"Returns true if the file-data is its own tokens source.
Call only with the current local file's data. This predicate does not check
which file is open; callers must reject connected libraries first."
[file-data]
(effective-tokens-source? file-data (:id file-data)))
@ -810,4 +812,4 @@
{:active-theme-ids active-theme-ids
:active-set-ids active-set-ids})
{:active-theme-ids #{}
:active-set-ids #{}}))
:active-set-ids #{}}))

View File

@ -15,15 +15,34 @@
;; Tokens source
(defn- own-tokens-status
"The current tokens status of `file-data`, keeping only the themes and sets
that exist in its own tokens lib."
[file-data]
(let [tokens-status (cfo/get-tokens-status file-data)
tokens-lib (cfo/get-tokens-lib file-data)]
(if (and tokens-status tokens-lib)
(cfo/sync-tokens-status-with-lib tokens-status tokens-lib)
(ctos/make-tokens-status))))
(defn generate-set-tokens-source
"Create changes for setting the tokens source of a file to `library`,
copying the library's tokens status. If the library is nil, the tokens source
is removed and the tokens status is cleared."
copying the library's tokens status. If the library is the file itself,
the themes and sets that don't exist in its own tokens lib are deactivated.
If the library is nil, the tokens source is removed and the tokens status
is cleared."
[changes library]
(let [library-id (:id library)
library-tokens-status (if library
(-> library ctf/file-data cfo/get-tokens-status)
(ctos/make-tokens-status))]
file-data (pcb/get-library-data changes)
library-tokens-status (cond
(nil? library)
(ctos/make-tokens-status)
(= library-id (:id file-data))
(own-tokens-status file-data)
:else
(-> library ctf/file-data cfo/get-tokens-status))]
(-> changes
(pcb/set-tokens-source library-id)
(pcb/set-tokens-status library-tokens-status))))

View File

@ -177,7 +177,60 @@
(t/is (= (ctos/get-active-theme-ids file-tokens-status)
(ctos/get-active-theme-ids undo-tokens-status)))
(t/is (= (ctos/get-active-set-ids file-tokens-status)
(ctos/get-active-set-ids undo-tokens-status))))))
(ctos/get-active-set-ids undo-tokens-status)))))
(t/testing "if the library is the same file, themes and sets of the previous source are deactivated"
(let [set-id (uuid/next)
external-set-id (uuid/next)
external-theme-id (uuid/next)
library-id (uuid/next)
file (-> (tht/sample-file-with-tokens
:file-id :file1
:lib-fn #(-> %
(ctob/add-set (ctob/make-token-set :id set-id
:name "one set")))
:status-fn #(ctos/set-tokens-status % #{external-theme-id}
#{set-id external-set-id}))
(assoc-in [:data :tokens-source] library-id))
changes (-> (pcb/empty-changes)
(pcb/with-library-data (ctf/file-data file))
(clt/generate-set-tokens-source file))
redo (thf/apply-changes file changes)
undo (thf/apply-undo-changes redo changes)
file-tokens-status (tht/get-tokens-status file)
redo-tokens-source (tht/get-tokens-source redo)
redo-tokens-status (tht/get-tokens-status redo)
undo-tokens-source (tht/get-tokens-source undo)
undo-tokens-status (tht/get-tokens-status undo)]
;; Redo: only the themes and sets of the file's own lib stay active
(t/is (nil? redo-tokens-source))
(t/is (= #{} (ctos/get-active-theme-ids redo-tokens-status)))
(t/is (= #{set-id} (ctos/get-active-set-ids redo-tokens-status)))
;; Undo: source and status restored to the original state
(t/is (= library-id undo-tokens-source))
(t/is (= (ctos/get-active-theme-ids file-tokens-status)
(ctos/get-active-theme-ids undo-tokens-status)))
(t/is (= (ctos/get-active-set-ids file-tokens-status)
(ctos/get-active-set-ids undo-tokens-status)))))
(t/testing "if the library is the same file and it has no own tokens lib, the tokens status is cleared"
(let [set-id (uuid/next)
file (-> (thf/sample-file :file1)
(update :data dissoc :tokens-lib)
(assoc-in [:data :tokens-source] (uuid/next))
(assoc-in [:data :tokens-status] (ctos/set-tokens-status (ctos/make-tokens-status)
#{} #{set-id})))
changes (-> (pcb/empty-changes)
(pcb/with-library-data (ctf/file-data file))
(clt/generate-set-tokens-source file))
redo (thf/apply-changes file changes)
redo-tokens-status (tht/get-tokens-status redo)]
(t/is (nil? (tht/get-tokens-source redo)))
(t/is (= #{} (ctos/get-active-theme-ids redo-tokens-status)))
(t/is (= #{} (ctos/get-active-set-ids redo-tokens-status))))))
;; Tokens lib

View File

@ -7,6 +7,7 @@
(ns app.main.data.workspace.tokens.library-edit
(:require
[app.common.data.macros :as dm]
[app.common.exceptions :as ex]
[app.common.files.changes-builder :as pcb]
[app.common.files.helpers :as cfh]
[app.common.files.tokens :as cfo]
@ -48,6 +49,28 @@
;; Helpers
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn editable-tokens-status?
"True when file-id is the current file. Its activation status is editable
even when its tokens come from an external source."
[state file-id]
(= file-id (:current-file-id state)))
(defn editable-tokens?
"True when file-id is the current file and uses its own tokens source.
Use this predicate when the file may be a connected library."
[state file-id]
(and (editable-tokens-status? state file-id)
(cfo/editable-tokens? (dsh/lookup-file-data state file-id))))
(defn- check-editable-tokens!
"Throws when the tokens of the current file come from an external library,
so they can't be modified from this file."
[state]
(when-not (editable-tokens? state (:current-file-id state))
(ex/raise :type :assertion
:code :tokens-not-editable
:hint "cannot modify the tokens of an external library")))
;; TODO HYMA: Copied over from workspace.cljs
(defn update-shape
[id attrs]
@ -268,6 +291,7 @@
(ptk/reify ::create-token-theme
ptk/WatchEvent
(watch [it state _]
(check-editable-tokens! state)
(let [data (dsh/lookup-file-data state)
tokens-lib (dsh/lookup-tokens-lib state)]
(if (and tokens-lib (ctob/get-theme tokens-lib (ctob/get-id token-theme)))
@ -286,6 +310,7 @@
(ptk/reify ::update-token-theme
ptk/WatchEvent
(watch [it state _]
(check-editable-tokens! state)
(let [data (dsh/lookup-file-data state)
tokens-lib (dsh/lookup-tokens-lib state)]
(if (and (not= id (ctob/get-id token-theme))
@ -322,7 +347,7 @@
(ptk/reify ::toggle-token-theme-active
ptk/WatchEvent
(watch [it state _]
(let [data (dsh/lookup-tokens-source-data state)
(let [data (dsh/lookup-file-data state)
tokens-status (dsh/lookup-tokens-status state)
tokens-lib (dsh/lookup-tokens-lib state)
changes (-> (pcb/empty-changes it)
@ -338,6 +363,7 @@
(ptk/reify ::delete-token-theme
ptk/WatchEvent
(watch [it state _]
(check-editable-tokens! state)
(let [data (dsh/lookup-file-data state)
changes (-> (pcb/empty-changes it)
(pcb/with-library-data data)
@ -352,6 +378,7 @@
(ptk/reify ::create-token-set
ptk/WatchEvent
(watch [it state _]
(check-editable-tokens! state)
(let [data (dsh/lookup-file-data state)
changes (-> (pcb/empty-changes it)
(pcb/with-library-data data)
@ -366,6 +393,7 @@
(ptk/reify ::update-token-set
ptk/WatchEvent
(watch [it state _]
(check-editable-tokens! state)
(let [data (dsh/lookup-file-data state)
changes (-> (pcb/empty-changes it)
(pcb/with-library-data data)
@ -377,7 +405,8 @@
[set-group-path set-group-fname]
(ptk/reify ::rename-token-set-group
ptk/WatchEvent
(watch [it _state _]
(watch [it state _]
(check-editable-tokens! state)
(let [changes (-> (pcb/empty-changes it)
(pcb/rename-token-set-group set-group-path set-group-fname))]
(rx/of
@ -390,6 +419,7 @@
(ptk/reify ::duplicate-token-set
ptk/WatchEvent
(watch [it state _]
(check-editable-tokens! state)
(let [data (dsh/lookup-file-data state)
tokens-lib (dsh/lookup-tokens-lib state)
suffix (tr "workspace.tokens.duplicate-suffix")]
@ -458,6 +488,7 @@
(ptk/reify ::import-tokens-lib
ptk/WatchEvent
(watch [it state _]
(check-editable-tokens! state)
(let [data (dsh/lookup-file-data state)
status (cfo/make-tokens-status-from-lib lib)
changes (-> (pcb/empty-changes it)
@ -486,6 +517,7 @@
(ptk/reify ::delete-token-set
ptk/WatchEvent
(watch [it state _]
(check-editable-tokens! state)
(let [data (dsh/lookup-file-data state)
changes (-> (pcb/empty-changes it)
(pcb/with-library-data data)
@ -498,6 +530,7 @@
(ptk/reify ::delete-token-set-group
ptk/WatchEvent
(watch [it state _]
(check-editable-tokens! state)
(let [data (dsh/lookup-file-data state)
changes (-> (pcb/empty-changes it)
(pcb/with-library-data data)
@ -528,6 +561,7 @@
(ptk/reify ::drop-token-set-group
ptk/WatchEvent
(watch [it state _]
(check-editable-tokens! state)
(try
(when-let [changes (clo/generate-move-token-set-group (pcb/empty-changes it) (dsh/lookup-tokens-lib state) drop-opts)]
(rx/of
@ -544,6 +578,7 @@
(ptk/reify ::drop-token-set
ptk/WatchEvent
(watch [it state _]
(check-editable-tokens! state)
(try
(let [tokens-lib (dsh/lookup-tokens-lib state)
changes (-> (pcb/empty-changes it)
@ -559,6 +594,7 @@
(ptk/reify ::create-token-and-set
ptk/WatchEvent
(watch [_ state _]
(check-editable-tokens! state)
(let [data
(dsh/lookup-file-data state)
@ -594,6 +630,7 @@
(ptk/reify ::create-token
ptk/WatchEvent
(watch [it state _]
(check-editable-tokens! state)
(if-let [token-set (if set-id
(lookup-token-set state set-id)
(lookup-token-set state))]
@ -621,6 +658,7 @@
(ptk/reify ::bulk-create-tokens
ptk/WatchEvent
(watch [it state _]
(check-editable-tokens! state)
(let [token-set (lookup-token-set state set-id)
data (dsh/lookup-file-data state)
changes (reduce (fn [changes token-id]
@ -650,6 +688,7 @@
(ptk/reify ::update-token
ptk/WatchEvent
(watch [it state _]
(check-editable-tokens! state)
(let [token-set (if set-id
(lookup-token-set state set-id)
(lookup-token-set state))
@ -678,6 +717,7 @@
(ptk/reify ::bulk-update-tokens
ptk/WatchEvent
(watch [it state _]
(check-editable-tokens! state)
(let [token-set (if set-id
(lookup-token-set state set-id)
(lookup-token-set state))
@ -708,6 +748,7 @@
(ptk/reify ::delete-token
ptk/WatchEvent
(watch [it state _]
(check-editable-tokens! state)
(let [data (dsh/lookup-file-data state)
tokens-lib (dsh/lookup-tokens-lib state)
token-set (if set-id
@ -731,6 +772,7 @@
(ptk/reify ::bulk-delete-tokens
ptk/WatchEvent
(watch [it state _]
(check-editable-tokens! state)
(let [data (dsh/lookup-file-data state)
changes (reduce (fn [changes token-id]
(pcb/set-token changes set-id token-id nil))
@ -746,6 +788,7 @@
(ptk/reify ::duplicate-token
ptk/WatchEvent
(watch [_ state _]
(check-editable-tokens! state)
(when-let [token-set (lookup-token-set state)]
(when-let [tokens-lib (dsh/lookup-tokens-lib state)]
(when-let [token (ctob/get-token tokens-lib

View File

@ -489,9 +489,10 @@
(tr "common.publish")])]
(for [{:keys [id name data connected-to connected-to-names] :as library} linked-libraries]
(let [disabled? (some #(contains? linked-libraries-ids %) connected-to)
has-tokens? (and (has-tokens? library)
(contains? cf/flags :token-import-from-library))]
(let [disabled? (some #(contains? linked-libraries-ids %) connected-to)
can-import-tokens? (and (has-tokens? library)
(cfo/editable-tokens? local-library)
(contains? cf/flags :token-import-from-library))]
[:div {:class (stl/css :section-list-item-legacy)
:key (dm/str id)
:data-testid "library-item"}
@ -508,7 +509,7 @@
[:span ")"]])])]]
[:div {:class (stl/css :library-actions)}
(when ^boolean has-tokens?
(when ^boolean can-import-tokens?
[:> icon-button*
{:type "button"
:aria-label (tr "workspace.tokens.import-tokens")
@ -865,9 +866,10 @@
:title-class (stl/css :connected-libraries-title)}]
(if (seq linked-libraries)
(for [{:keys [id name data connected-to nested? parent-name] :as library} linked-libraries]
(let [disabled? (some #(contains? linked-libraries-ids %) connected-to)
has-tokens? (and (has-tokens? library)
(contains? cf/flags :token-import-from-library))]
(let [disabled? (some #(contains? linked-libraries-ids %) connected-to)
can-import-tokens? (and (has-tokens? library)
(cfo/editable-tokens? local-library)
(contains? cf/flags :token-import-from-library))]
[:div {:class (stl/css-case :section-list-item true
:section-list-item-nested nested?)
:key (dm/str id)
@ -893,7 +895,7 @@
:data-library-id (dm/str id)
:on-click set-as-tokens-source}
(tr "workspace.libraries.set-as-tokens-source")])
(when ^boolean has-tokens?
(when ^boolean can-import-tokens?
[:> icon-button*
{:type "button"
:aria-label (tr "workspace.tokens.import-tokens")

View File

@ -8,6 +8,7 @@
(:require
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.files.tokens :as cfo]
[app.common.files.variant :as cfv]
[app.common.geom.point :as gpt]
[app.common.schema :as sm]
@ -22,6 +23,7 @@
[app.main.data.workspace :as dw]
[app.main.data.workspace.libraries :as dwl]
[app.main.data.workspace.texts :as dwt]
[app.main.data.workspace.tokens.library-edit :as dwtl]
[app.main.data.workspace.variants :as dwv]
[app.main.fonts :as fonts]
[app.main.repo :as rp]
@ -1300,6 +1302,37 @@
(let [libraries (get @st/state :files)]
(apply array (->> libraries keys (map (partial library-proxy plugin-id))))))}
:tokensSource
{:get
(fn []
(let [file-id (:current-file-id @st/state)
file-data (ctf/file-data (u/locate-file file-id))]
(library-proxy plugin-id (cfo/get-effective-tokens-source file-data))))
:set
(fn [value]
(let [file-id (:current-file-id @st/state)
library-id (when (library-proxy? value) (obj/get value "$id"))
library (some-> library-id u/locate-file)
library-data (some-> library ctf/file-data)
file-data (ctf/file-data (u/locate-file file-id))]
(cond
(not (r/check-permission plugin-id "content:write"))
(u/not-valid plugin-id :tokensSource "Plugin doesn't have 'content:write' permission")
(not (library-proxy? value))
(u/not-valid plugin-id :tokensSource value)
(nil? library)
(u/not-valid plugin-id :tokensSource "Library is not connected")
(and (not= library-id file-id)
(not (cfo/tokens-provider? library-data)))
(u/not-valid plugin-id :tokensSource "Library must have its own tokens")
(not (cfo/effective-tokens-source? file-data library-id))
(st/emit! (-> (dwtl/set-tokens-source library-id)
(se/add-event plugin-id))))))}
:availableLibraries
(fn []
(let [team-id (:current-team-id @st/state)]

View File

@ -11,7 +11,6 @@
[app.common.files.tokens :as cfo]
[app.common.json :as json]
[app.common.schema :as sm]
[app.common.types.file :as ctf]
[app.common.types.token :as cto]
[app.common.types.tokens-lib :as ctob]
[app.common.types.tokens-status :as ctos]
@ -512,8 +511,11 @@
(not (r/check-permission plugin-id "content:write"))
(u/not-valid plugin-id :active "Plugin doesn't have 'content:write' permission")
(not (dwtl/editable-tokens-status? @st/state file-id))
(u/not-valid plugin-id :active "Cannot change activation through a connected library; use penpot.library.local.tokens")
:else
(st/emit! (dwtl/set-enabled-token-set id value))))} ;; This can be done even with tokens in an external library
(st/emit! (dwtl/set-enabled-token-set id value))))}
:toggleActive
(fn []
@ -521,6 +523,9 @@
(not (r/check-permission plugin-id "content:write"))
(u/not-valid plugin-id :toggleActive "Plugin doesn't have 'content:write' permission")
(not (dwtl/editable-tokens-status? @st/state file-id))
(u/not-valid plugin-id :toggleActive "Cannot change activation through a connected library; use penpot.library.local.tokens")
:else
(st/emit! (dwtl/toggle-token-set id))))
@ -737,6 +742,9 @@
(not (r/check-permission plugin-id "content:write"))
(u/not-valid plugin-id :active "Plugin doesn't have 'content:write' permission")
(not (dwtl/editable-tokens-status? @st/state file-id))
(u/not-valid plugin-id :active "Cannot change activation through a connected library; use penpot.library.local.tokens")
:else
(st/emit! (dwtl/set-token-theme-active id value))))}
@ -746,6 +754,9 @@
(not (r/check-permission plugin-id "content:write"))
(u/not-valid plugin-id :toggleActive "Plugin doesn't have 'content:write' permission")
(not (dwtl/editable-tokens-status? @st/state file-id))
(u/not-valid plugin-id :toggleActive "Cannot change activation through a connected library; use penpot.library.local.tokens")
:else
(st/emit! (dwtl/toggle-token-theme-active id))))
@ -839,8 +850,7 @@
:enumerable false
:get
(fn []
(let [file (u/locate-file file-id)]
(cfo/editable-tokens? (ctf/file-data file))))}
(dwtl/editable-tokens? @st/state file-id))}
:themes
{:this true

View File

@ -18,6 +18,7 @@
[app.common.types.file :as ctf]
[app.common.types.tokens-lib :as ctob]
[app.main.data.helpers :as dsh]
[app.main.data.workspace.tokens.library-edit :as dwtl]
[app.main.store :as st]
[app.util.object :as obj]
[cuerdas.core :as str]))
@ -288,9 +289,8 @@
(defn check-editable-tokens
[file-id]
(let [file (locate-file file-id)]
(when-not (cfo/editable-tokens? (ctf/file-data file))
(throw (js/Error. (dm/str "[PENPOT PLUGIN] Cannot modify tokens in an external library"))))))
(when-not (dwtl/editable-tokens? @st/state file-id)
(throw (js/Error. (dm/str "[PENPOT PLUGIN] Cannot modify tokens in an external library")))))
(defn throw-validation-errors?
[plugin-id]

View File

@ -25,7 +25,7 @@
;; profile renderer preference instead of the global feature flag alone.
:profile {:props {:renderer :wasm}}})
(defn- on-error
(defn- report-error
[cause]
(js/console.error "STORE ERROR" (.-stack cause))
(t/do-report {:type :error :message "Store error" :actual cause})
@ -34,7 +34,7 @@
(defn setup-store
([file] (setup-store file nil))
([file {:keys [renderer libraries] :as _opts}]
([file {:keys [renderer libraries on-error] :or {on-error report-error} :as _opts}]
(let [state (-> initial-state
(assoc :current-file-id (:id file)
:current-page-id (cthf/current-page-id file)

View File

@ -18,17 +18,21 @@
[app.main.data.tokenscript :as ts]
[app.main.data.workspace.shapes :as dwsh]
[app.main.data.workspace.tokens.library-edit :as dwtl]
[app.main.data.workspace.undo :as dwu]
[app.main.store :as st]
[app.plugins.api :as api]
[app.plugins.library :as library]
[app.plugins.register :as r]
[app.plugins.tokens :as ptok]
[app.plugins.utils :as u]
[app.util.object :as obj]
[cljs.test :as t :include-macros true]
[frontend-tests.helpers.async :as tha]
[clojure.datafy :refer [datafy]]
[frontend-tests.helpers.async :as async]
[frontend-tests.helpers.mock :as mock]
[frontend-tests.helpers.state :as ths]
[frontend-tests.helpers.wasm :as thw]
[frontend-tests.tokens.helpers.state :as tohs]
[potok.v2.core :as ptk]))
(t/use-fixtures :each
@ -71,7 +75,7 @@
#js {:strokeColor "#000000" :strokeWidth 1}])
(ptk/emit! store (dwsh/update-shapes [id] #(assoc % :applied-tokens tokens)))
(obj/set! (aget (.-strokes rect) index) property value)
(await (tha/settle))
(await (async/settle))
(t/is (= (apply dissoc tokens removed) (get-in @store path)))
(t/is (= value (obj/get (aget (.-strokes rect) index) property))))))))
@ -97,15 +101,344 @@
(ptk/emit! store (dwsh/update-shapes [id] #(assoc % :applied-tokens tokens)))
(let [^js gradient (obj/get (aget (.-strokes rect) 0) "strokeColorGradient")]
(set! (.-startX gradient) 0.5)
(await (tha/settle))
(await (async/settle))
(t/is (= (dissoc tokens :stroke-color) (get-in @store path)))
(t/is (= 0.5 (obj/get (obj/get (aget (.-strokes rect) 0) "strokeColorGradient") "startX")))
(ptk/emit! store (dwsh/update-shapes [id] #(assoc % :applied-tokens tokens)))
(obj/set! (aget (.-stops gradient) 0) "color" "#ff0000")
(await (tha/settle))
(await (async/settle))
(t/is (= (dissoc tokens :stroke-color) (get-in @store path)))
(t/is (= "#ff0000" (obj/get (aget (obj/get (obj/get (aget (.-strokes rect) 0) "strokeColorGradient") "stops") 0) "color"))))))
(defn- token-source-file
[file-label]
(let [set-id (uuid/next)]
(ctht/sample-file-with-tokens
:file-id file-label
:lib-fn #(-> %
(ctob/add-set (ctob/make-token-set :id set-id :name "Core"))
(ctob/add-theme (ctob/make-token-theme :name "Light"
:group "Mode"
:sets #{"Core"}))
(ctob/add-token set-id (ctob/make-token :name "color.primary"
:type :color
:value "#FF0000")))
:status-fn #(ctos/set-tokens-status % #{} #{set-id}))))
(defn- with-active-theme
"The tokens status of `file` with its first visible theme activated."
[file]
(let [tokens-lib (ctht/get-tokens-lib file)
theme-id (:id (first (remove ctob/hidden-theme? (ctob/get-themes tokens-lib))))]
(cfo/activate-theme (ctht/get-tokens-status file) tokens-lib theme-id)))
(defn- tokens-unchanged?
"True when the token attributes of `file` in the store are the original ones."
[store file]
(let [attrs [:tokens-lib :tokens-status :tokens-source]]
(= (select-keys (:data file) attrs)
(select-keys (get-in @store [:files (:id file) :data]) attrs))))
(defn- record-not-valid
"A `u/not-valid` stub that collects each call into `errors`."
[errors]
(mock/stub (fn [plugin-id code value] (swap! errors conj [plugin-id code value]))))
(t/deftest ^:async library-tokens-source-switches-to-connected-library
(let [file (token-source-file :local)
source (token-source-file :source)
store (ths/setup-store file {:libraries [source]})
^js context (library/library-subcontext (str uuid/zero))
source-proxy (library/library-proxy (str uuid/zero) (:id source))]
(await
(mock/with-mocks* {st/state store st/stream (ptk/input-stream store)}
(t/is (= (str (:id file)) (.. context -tokensSource -id)))
(set! (.-tokensSource context) source-proxy)
(await (async/wait-for #(= (:id source) (get-in @store [:files (:id file) :data :tokens-source]))
"library token source"))
(t/is (= (str (:id source)) (.. context -tokensSource -id)))
(t/is (= (datafy (ctht/get-tokens-status source))
(datafy (get-in @store [:files (:id file) :data :tokens-status]))))
(t/is (= (ctht/get-tokens-lib source) (u/locate-tokens-lib (:id file))))
(t/is (false? (.. context -local -tokens -isEditableTokens)))
(t/is (= (ctht/get-tokens-lib file)
(get-in @store [:files (:id file) :data :tokens-lib])))
(t/is (tokens-unchanged? store source))))))
(t/deftest ^:async library-tokens-source-restores-local-tokens
(let [source (token-source-file :source)
file (-> (token-source-file :local)
(assoc-in [:data :tokens-source] (:id source))
(assoc-in [:data :tokens-status] (with-active-theme source)))
store (ths/setup-store file {:libraries [source]})
^js context (library/library-subcontext (str uuid/zero))]
(await
(mock/with-mocks* {st/state store st/stream (ptk/input-stream store)}
(set! (.-tokensSource context) (.-local context))
(await (async/wait-for #(nil? (get-in @store [:files (:id file) :data :tokens-source]))
"local token source"))
(let [^js catalog (.. context -local -tokens)]
(t/is (= (str (:id file)) (.. context -tokensSource -id)))
(t/is (true? (.-isEditableTokens catalog)))
(t/is (= (ctht/get-tokens-lib file) (u/locate-tokens-lib (:id file))))
(t/is (= #{} (ctos/get-active-set-ids (get-in @store [:files (:id file) :data :tokens-status]))))
(t/is (not-any? #(.-active ^js %) (.-themes catalog))))))))
(t/deftest ^:async library-tokens-source-supports-undo-and-redo
(let [file (token-source-file :local)
source (token-source-file :source)
store (ths/setup-store file {:libraries [source]})
^js context (library/library-subcontext (str uuid/zero))
source-proxy (library/library-proxy (str uuid/zero) (:id source))]
(await
(mock/with-mocks* {st/state store st/stream (ptk/input-stream store)}
(st/emit! (tohs/watch-undo-stack))
(set! (.-tokensSource context) source-proxy)
(await (async/wait-for #(seq (get-in @store [:workspace-undo :items]))
"token source undo entry"))
(st/emit! dwu/undo)
(await (async/wait-for #(nil? (get-in @store [:files (:id file) :data :tokens-source]))
"undo token source"))
(t/is (= (datafy (ctht/get-tokens-status file))
(datafy (get-in @store [:files (:id file) :data :tokens-status]))))
(st/emit! dwu/redo)
(await (async/wait-for #(= (:id source) (get-in @store [:files (:id file) :data :tokens-source]))
"redo token source"))
(t/is (= (datafy (ctht/get-tokens-status source))
(datafy (get-in @store [:files (:id file) :data :tokens-status]))))))))
(t/deftest ^:async library-tokens-source-keeps-status-when-source-is-unchanged
(let [source (token-source-file :source)
file (-> (token-source-file :local)
(assoc-in [:data :tokens-source] (:id source)))
store (ths/setup-store file {:libraries [source]})
^js context (library/library-subcontext (str uuid/zero))
source-proxy (library/library-proxy (str uuid/zero) (:id source))]
(await
(mock/with-mocks* {st/state store st/stream (ptk/input-stream store)}
(set! (.-tokensSource context) source-proxy)
(await (async/settle))
(t/is (tokens-unchanged? store file))))))
(t/deftest ^:async library-tokens-source-rejects-invalid-libraries
(let [file (token-source-file :local)
empty-lib (ctht/sample-file-with-tokens :file-id :empty)
indirect (-> (token-source-file :indirect)
(assoc-in [:data :tokens-source] (uuid/next)))
store (ths/setup-store file {:libraries [empty-lib indirect]})
plugin-id (str uuid/zero)
^js context (library/library-subcontext plugin-id)
errors (atom [])]
(await
(mock/with-mocks* {st/state store
st/stream (ptk/input-stream store)
u/not-valid (record-not-valid errors)}
(doseq [[label value message]
[["not a library" "invalid" "invalid"]
["not connected" (library/library-proxy plugin-id (uuid/next)) "Library is not connected"]
["without own tokens" (library/library-proxy plugin-id (:id empty-lib)) "Library must have its own tokens"]
["with another tokens source" (library/library-proxy plugin-id (:id indirect)) "Library must have its own tokens"]]]
(t/testing label
(reset! errors [])
(set! (.-tokensSource context) value)
(t/is (= [[plugin-id :tokensSource message]] @errors))))
(await (async/settle))
(t/is (tokens-unchanged? store file))))))
(t/deftest ^:async external-catalogs-reject-tokens-changes
(let [source (token-source-file :source)
other (token-source-file :other)
file (-> (token-source-file :local)
(assoc-in [:data :tokens-source] (:id source)))
store (ths/setup-store file {:libraries [source other]})
plugin-id (str uuid/zero)]
(swap! store assoc-in [:plugins :flags plugin-id :throw-validation-errors] true)
(await
(mock/with-mocks* {st/state store st/stream (ptk/input-stream store)}
(doseq [[catalog-label ^js catalog]
[["local catalog" (.. (library/library-subcontext plugin-id) -local -tokens)]
["source catalog" (.. (library/library-subcontext plugin-id) -tokensSource -tokens)]
["other connected catalog" (.-tokens (library/library-proxy plugin-id (:id other)))]]]
(t/testing catalog-label
(let [^js token-set (aget (.-sets catalog) 0)
^js token (aget (.-tokens token-set) 0)
^js theme (aget (.-themes catalog) 0)]
(t/is (false? (.-isEditableTokens catalog)))
(doseq [[label mutate]
[["create set" #(.addSet catalog #js {:name "New"})]
["rename set" #(set! (.-name token-set) "Renamed")]
["duplicate set" #(.duplicate token-set)]
["remove set" #(.remove token-set)]
["create token" #(.addToken token-set #js {:name "color.new" :type "color" :value "#00FF00"})]
["rename token" #(set! (.-name token) "color.renamed")]
["change token value" #(set! (.-value token) "#00FF00")]
["change token description" #(set! (.-description token) "New description")]
["duplicate token" #(.duplicate token)]
["remove token" #(.remove token)]
["create theme" #(.addTheme catalog #js {:name "Dark" :group "Mode"})]
["rename theme" #(set! (.-name theme) "Dark")]
["change theme group" #(set! (.-group theme) "Other")]
["add set to theme" #(.addSet theme token-set)]
["remove set from theme" #(.removeSet theme token-set)]
["duplicate theme" #(.duplicate theme)]
["remove theme" #(.remove theme)]]]
(t/testing label
(t/is (thrown-with-msg? js/Error #"Cannot modify tokens in an external library"
(mutate)))
(await (async/settle))
(t/is (tokens-unchanged? store file))
(t/is (tokens-unchanged? store source))
(t/is (tokens-unchanged? store other)))))))))))
(t/deftest ^:async external-catalogs-reject-status-changes
(let [source (token-source-file :source)
other (token-source-file :other)
file (-> (token-source-file :local)
(assoc-in [:data :tokens-source] (:id source))
(assoc-in [:data :tokens-status] (with-active-theme source)))
store (ths/setup-store file {:libraries [source other]})
plugin-id (str uuid/zero)
message "Cannot change activation through a connected library; use penpot.library.local.tokens"
errors (atom [])]
(await
(mock/with-mocks* {st/state store
st/stream (ptk/input-stream store)
u/not-valid (record-not-valid errors)}
(let [^js context (library/library-subcontext plugin-id)]
(t/is (true? (.-active ^js (aget (.. context -local -tokens -themes) 0))))
(doseq [[catalog-label ^js catalog]
[["source catalog" (.. context -tokensSource -tokens)]
["other connected catalog" (.-tokens (library/library-proxy plugin-id (:id other)))]]]
(t/testing catalog-label
(let [^js token-set (aget (.-sets catalog) 0)
^js theme (aget (.-themes catalog) 0)]
(t/testing "reports the status of its own file"
(t/is (true? (.-active token-set)))
(t/is (false? (.-active theme))))
(reset! errors [])
(set! (.-active token-set) false)
(.toggleActive token-set)
(set! (.-active theme) true)
(.toggleActive theme)
(await (async/settle))
(t/is (= [[plugin-id :active message]
[plugin-id :toggleActive message]
[plugin-id :active message]
[plugin-id :toggleActive message]]
@errors))
(t/is (tokens-unchanged? store file))
(t/is (tokens-unchanged? store source))
(t/is (tokens-unchanged? store other))))))))))
(t/deftest ^:async external-token-source-allows-set-activation
(let [source (token-source-file :source)
file (-> (token-source-file :local)
(assoc-in [:data :tokens-source] (:id source))
(assoc-in [:data :tokens-status] (ctht/get-tokens-status source)))
store (ths/setup-store file {:libraries [source]})]
(await
(mock/with-mocks* {st/state store st/stream (ptk/input-stream store)}
(let [^js catalog (.. (library/library-subcontext (str uuid/zero)) -local -tokens)
^js token-set (aget (.-sets catalog) 0)]
(t/is (true? (.-active token-set)))
(set! (.-active token-set) false)
(await (async/wait-for #(false? (.-active token-set)) "deactivate set"))
(.toggleActive token-set)
(await (async/wait-for #(true? (.-active token-set)) "toggle set"))
(t/is (= (ctht/get-tokens-lib file) (get-in @store [:files (:id file) :data :tokens-lib])))
(t/is (tokens-unchanged? store source)))))))
(t/deftest ^:async external-token-source-allows-theme-activation
(let [source (token-source-file :source)
file (-> (token-source-file :local)
(assoc-in [:data :tokens-source] (:id source))
(assoc-in [:data :tokens-status] (ctht/get-tokens-status source)))
store (ths/setup-store file {:libraries [source]})]
(await
(mock/with-mocks* {st/state store st/stream (ptk/input-stream store)}
(let [^js catalog (.. (library/library-subcontext (str uuid/zero)) -local -tokens)
^js theme (aget (.-themes catalog) 0)]
(t/is (false? (.-active theme)))
(set! (.-active theme) true)
(await (async/wait-for #(true? (.-active theme)) "activate theme"))
(.toggleActive theme)
(await (async/wait-for #(false? (.-active theme)) "toggle theme"))
(t/is (= (ctht/get-tokens-lib file) (get-in @store [:files (:id file) :data :tokens-lib])))
(t/is (tokens-unchanged? store source)))))))
(t/deftest ^:async external-token-source-theme-toggle-supports-undo
(let [source (token-source-file :source)
status (with-active-theme source)
file (-> (token-source-file :local)
(assoc-in [:data :tokens-source] (:id source))
(assoc-in [:data :tokens-status] status))
store (ths/setup-store file {:libraries [source]})]
(await
(mock/with-mocks* {st/state store st/stream (ptk/input-stream store)}
(let [^js catalog (.. (library/library-subcontext (str uuid/zero)) -local -tokens)
^js theme (aget (.-themes catalog) 0)]
(st/emit! (tohs/watch-undo-stack))
(.toggleActive theme)
(await (async/wait-for #(seq (get-in @store [:workspace-undo :items]))
"theme toggle undo entry"))
(t/is (false? (.-active theme)))
(st/emit! dwu/undo)
(await (async/wait-for #(= (datafy status)
(datafy (get-in @store [:files (:id file) :data :tokens-status])))
"undo external theme toggle"))
(t/is (tokens-unchanged? store source)))))))
(t/deftest ^:async external-token-source-allows-applying-tokens-to-shapes
(let [source (token-source-file :source)
file (-> (token-source-file :local)
(assoc-in [:data :tokens-source] (:id source))
(assoc-in [:data :tokens-status] (ctht/get-tokens-status source))
(ctho/add-rect :rect1)
(ctho/add-rect :rect2)
(ctho/add-rect :rect3))
store (ths/setup-store file {:libraries [source]})
page-id (cthf/current-page-id file)]
(swap! store assoc-in [:workspace-local :selected] #{(cthi/id :rect3)})
(await
(mock/with-mocks* {st/state store st/stream (ptk/input-stream store)}
(let [^js context (api/create-context (str uuid/zero))
^js page (.-currentPage context)
^js catalog (.. context -library -local -tokens)
^js token-set (aget (.-sets catalog) 0)
^js token (aget (.-tokens token-set) 0)
^js rect1 (.getShapeById page (str (cthi/id :rect1)))
^js rect2 (.getShapeById page (str (cthi/id :rect2)))]
(.applyToken rect1 token #js ["fill"])
(.applyToShapes token #js [rect2] #js ["fill"])
(.applyToSelected token #js ["fill"])
(doseq [label [:rect1 :rect2 :rect3]]
(let [shape-path [:files (:id file) :data :pages-index page-id :objects (cthi/id label)]]
(await (async/wait-for #(= "color.primary" (get-in @store (conj shape-path :applied-tokens :fill)))
(str "apply external token to " label)))
(await (async/wait-for #(= "#ff0000" (get-in @store (conj shape-path :fills 0 :fill-color)))
(str "resolve external token on " label)))))
(t/is (= (ctht/get-tokens-lib file) (get-in @store [:files (:id file) :data :tokens-lib])))
(t/is (= (datafy (ctht/get-tokens-status file))
(datafy (get-in @store [:files (:id file) :data :tokens-status]))))
(t/is (tokens-unchanged? store source)))))))
(t/deftest ^:async library-tokens-source-requires-content-write-permission
(let [file (token-source-file :local)
source (token-source-file :source)
plugin-id "read-only-plugin"
store (ths/setup-store file {:libraries [source]})
^js context (library/library-subcontext plugin-id)
source-proxy (library/library-proxy plugin-id (:id source))
errors (atom [])]
(await
(mock/with-mocks* {st/state store
st/stream (ptk/input-stream store)
u/not-valid (record-not-valid errors)}
(set! (.-tokensSource context) source-proxy)
(t/is (= [[plugin-id :tokensSource "Plugin doesn't have 'content:write' permission"]] @errors))
(await (async/settle))
(t/is (tokens-unchanged? store file))))))
;; Regression coverage for issue #9162.
;;
;; Plugin code calling `shape.applyToken(token, ["fill"])` or

View File

@ -15,10 +15,12 @@
[app.main.data.workspace.tokens.library-edit :as dwtl]
[app.main.data.workspace.undo :as dwu]
[cljs.test :as t :include-macros true]
[frontend-tests.helpers.async :as async]
[frontend-tests.helpers.pages :as thp]
[frontend-tests.helpers.state :as ths]
[frontend-tests.tokens.helpers.state :as tohs]
[frontend-tests.tokens.helpers.tokens :as toht]))
[frontend-tests.tokens.helpers.tokens :as toht]
[potok.v2.core :as ptk]))
(t/use-fixtures :each
{:before thp/reset-idmap!})
@ -895,3 +897,64 @@
(t/testing "Tokens lib has been imported into file without existing lib"
(t/is (some? tokens-lib'))
(t/is (some? (ctob/get-set-by-name tokens-lib' "Imported Set"))))))))))
(defn- setup-tokens-source-library
[]
(ctho/sample-file-with-tokens
:file-id :library
:lib-fn #(-> %
(ctob/add-set (ctob/make-token-set :id (cthi/new-id! :source-set)
:name "Source"))
(ctob/add-token (cthi/id :source-set)
(ctob/make-token :id (cthi/new-id! :source-token)
:name "color.source"
:type :color
:value "#FF0000"))
(ctob/add-theme (ctob/make-token-theme :id (cthi/new-id! :source-theme)
:name "Light"
:group "Mode"
:sets #{"Source"})))
:status-fn #(ctos/set-tokens-status % #{} #{(cthi/id :source-set)})))
(t/deftest ^:async external-tokens-source-rejects-tokens-lib-changes
(let [source (setup-tokens-source-library)
file (-> (setup-file-with-token-lib)
(assoc-in [:data :tokens-source] (:id source)))
errors (atom [])
store (ths/setup-store file {:libraries [source]
:on-error #(swap! errors conj %)})
tokens-lib (ctho/get-tokens-lib source)
set-id (cthi/id :source-set)
token-id (cthi/id :source-token)
theme-id (cthi/id :source-theme)]
(doseq [[label event]
[["create theme" (dwtl/create-token-theme (ctob/make-token-theme :name "Dark" :group "Mode"))]
["update theme" (dwtl/update-token-theme theme-id (ctob/rename (ctob/get-theme tokens-lib theme-id) "Dark"))]
["delete theme" (dwtl/delete-token-theme theme-id)]
["create set" (dwtl/create-token-set (ctob/make-token-set :name "New"))]
["rename set" (dwtl/rename-token-set (ctob/get-set tokens-lib set-id) "Renamed")]
["rename set group" (dwtl/rename-token-set-group ["Source"] "Renamed")]
["duplicate set" (dwtl/duplicate-token-set set-id)]
["delete set" (dwtl/delete-token-set set-id)]
["delete set group" (dwtl/delete-token-set-group ["Source"])]
["drop set group" (dwtl/drop-token-set-group {:from-index 0 :to-index 0 :position :top})]
["drop set" (dwtl/drop-token-set {:from-index 0 :to-index 0 :position :top})]
["import tokens lib" (dwtl/import-tokens-lib (ctob/make-tokens-lib))]
["create token" (dwtl/create-token set-id (ctob/make-token :name "color.new"
:type :color
:value "#00FF00"))]
["bulk create tokens" (dwtl/bulk-create-tokens set-id [token-id] :color {:depth 0} "brand")]
["update token" (dwtl/update-token set-id token-id {:value "#00FF00"})]
["bulk update tokens" (dwtl/bulk-update-tokens set-id [token-id] :color "color" "brand")]
["delete token" (dwtl/delete-token set-id token-id)]
["bulk delete tokens" (dwtl/bulk-delete-tokens set-id [token-id])]
["duplicate token" (dwtl/duplicate-token token-id)]]]
(t/testing label
(reset! errors [])
(ptk/emit! store event)
(await (async/wait-for #(seq @errors) label))
(t/is (= [{:type :assertion :code :tokens-not-editable}]
(map #(select-keys (ex-data %) [:type :code]) @errors)))))
(await (async/settle))
(t/is (= file (get-in @store [:files (:id file)])))
(t/is (= source (get-in @store [:files (:id source)])))))

View File

@ -22426,10 +22426,12 @@ TokenCatalog:
readonly isEditableTokens: boolean
```
Returns true if the tokens can be edited (it can't
if the tokens of the file are in an external library).
Note that even in this case, Themes and Sets status still
can be changed and tokens can be applied to shapes.
Returns true if the tokens in this catalog can be edited. Only the
catalog of the current file (`penpot.library.local.tokens`) can be
edited, and only when the file is its own tokens source.
When the tokens come from an external library, the local catalog
can still change the active themes and sets, and its tokens can
still be applied to shapes.
themes: |-
```
readonly themes: TokenTheme[]
@ -22564,7 +22566,8 @@ TokenSet:
active: boolean
```
Indicates if the set is currently active.
Indicates if the set is active in the file that owns this catalog.
Only sets from `penpot.library.local.tokens` can be changed.
tokens: |-
```
readonly tokens: Token[]
@ -22584,6 +22587,7 @@ TokenSet:
```
Toggles the active status of this set.
Only sets from `penpot.library.local.tokens` can be changed.
Returns void
getTokenById: |-
@ -22707,7 +22711,8 @@ TokenTheme:
active: boolean
```
Indicates if the theme is currently active.
Indicates if the theme is active in the file that owns this catalog.
Only themes from `penpot.library.local.tokens` can be changed.
activeSets: |-
```
readonly activeSets: TokenSet[]
@ -22721,6 +22726,7 @@ TokenTheme:
```
Toggles the active status of this theme.
Only themes from `penpot.library.local.tokens` can be changed.
Returns void
addSet: |-
@ -23297,6 +23303,7 @@ LibraryContext:
type LibraryContext = {
local: Library;
connected: Library[];
tokensSource: Library;
availableLibraries(): Promise<LibrarySummary[]>;
connectLibrary(libraryId: string): Promise<Library>;
}
@ -23327,6 +23334,33 @@ LibraryContext:
```
const connectedLibraries = libraryContext.connected;
```
tokensSource: |-
```
tokensSource: Library
```
The library that supplies tokens to the current file.
Defaults to the local library.
Assign a connected library that has its own tokens to use it as the
source. This copies its active themes and sets to the current file.
Assign `local` to use the file's own tokens again. Themes and sets
that don't exist in the local tokens are deactivated.
Setting the source requires `content:write` permission and supports undo.
Assigning the current source again keeps the active themes and sets.
The catalog of an external `tokensSource` is read-only and reports the
library's own active themes and sets. To change which ones are active
in the current file, use `penpot.library.local.tokens`, which lists the
tokens of the source.
Example
```
const source = await penpot.library.connectLibrary('library-id');
penpot.library.tokensSource = source;
// Restore local tokens:
penpot.library.tokensSource = penpot.library.local;
```
Methods:
availableLibraries: |-
```

View File

@ -63,6 +63,8 @@ class PenpotAPIContentMarkdownConverter(MarkdownConverter):
if node.name == "pre":
for button in soup.find_all("button"):
button.decompose()
for br in soup.find_all("br"):
br.replace_with("\n")
return f"\n```\n{soup.get_text().strip()}\n```\n\n"
# convert tsd-signature elements to code blocks, converting <br> to newlines

View File

@ -0,0 +1,31 @@
import unittest
from prepare_api_docs import PenpotAPIContentMarkdownConverter
class PenpotAPIContentMarkdownConverterTest(unittest.TestCase):
def test_preserves_line_breaks_in_code_examples(self):
html = (
'<pre><code>'
'const source = await penpot.library.connectLibrary("library-id");<br>'
'penpot.library.tokensSource = source;<br>'
'// Restore local tokens:<br>'
'penpot.library.tokensSource = penpot.library.local;'
'</code><button>Copy</button></pre>'
)
result = PenpotAPIContentMarkdownConverter().convert(html).strip()
self.assertEqual(
result,
'```\n'
'const source = await penpot.library.connectLibrary("library-id");\n'
'penpot.library.tokensSource = source;\n'
'// Restore local tokens:\n'
'penpot.library.tokensSource = penpot.library.local;\n'
'```',
)
if __name__ == '__main__':
unittest.main()

View File

@ -4,6 +4,7 @@
- **plugin-types:** Added `LibraryComponent.restore()` to restore a deleted component and its main instance without changing the current page, selection, zoom, or viewport. Requires `library:write` permission.
- **plugin-types:** Added `Shape.updateMain()`, `LibraryComponent.duplicate()`, writable component `annotation`, and `Library.deletedComponents`. Declared the existing `LibraryComponent.remove()` method.
- **plugin-types:** Added `LibraryContext.tokensSource` to read or set the current file's token source to a connected library, or restore local tokens.
- **plugin-types:** Added `paddingType` (`'simple' | 'multiple'`) to flex and grid layouts and `marginType` (`'simple' | 'multiple'`) to layout children, exposing whether the four padding/margin sides are mirrored or honoured independently.
- **plugin-types**: Added `waitForLayoutUpdate` to wait until pending layout updates have finished. It rejects when the optional timeout elapses, defaulting to 30 seconds so a wait never hangs.
- **plugin-types**: Added `waitForLayoutUpdate` to the `Shape` interface to wait until the pending layout updates of a shape and its children have finished

View File

@ -301,7 +301,8 @@
"availableLibraries",
"connectLibrary",
"connected",
"local"
"local",
"tokensSource"
],
"LibraryElement": ["id", "libraryId", "name", "path"],
"LibrarySummary": [
@ -544,6 +545,7 @@
"addTheme",
"getSetById",
"getThemeById",
"isEditableTokens",
"sets",
"themes"
],
@ -5318,6 +5320,12 @@
"type": "Library",
"array": true
},
"tokensSource": {
"decl": "LibraryContext",
"kind": "getset",
"type": "Library",
"array": false
},
"availableLibraries": {
"decl": "LibraryContext",
"kind": "method",
@ -9394,6 +9402,12 @@
}
},
"TokenCatalog": {
"isEditableTokens": {
"decl": "TokenCatalog",
"kind": "get",
"type": null,
"array": false
},
"themes": {
"decl": "TokenCatalog",
"kind": "get",

View File

@ -2892,6 +2892,32 @@ export type LibraryContext = {
*/
readonly connected: Library[];
/**
* The library that supplies tokens to the current file.
* Defaults to the local library.
*
* Assign a connected library that has its own tokens to use it as the
* source. This copies its active themes and sets to the current file.
* Assign `local` to use the file's own tokens again. Themes and sets
* that don't exist in the local tokens are deactivated.
* Setting the source requires `content:write` permission and supports undo.
* Assigning the current source again keeps the active themes and sets.
*
* The catalog of an external `tokensSource` is read-only and reports the
* library's own active themes and sets. To change which ones are active
* in the current file, use `penpot.library.local.tokens`, which lists the
* tokens of the source.
*
* @example
* ```js
* const source = await penpot.library.connectLibrary('library-id');
* penpot.library.tokensSource = source;
* // Restore local tokens:
* penpot.library.tokensSource = penpot.library.local;
* ```
*/
tokensSource: Library;
/**
* Retrieves a summary of available libraries that can be connected to.
* @return Returns a promise that resolves to an array of `LibrarySummary` objects representing available libraries.
@ -5293,10 +5319,12 @@ export type Token =
*/
export interface TokenCatalog {
/**
* Returns true if the tokens can be edited (it can't
* if the tokens of the file are in an external library).
* Note that even in this case, Themes and Sets status still
* can be changed and tokens can be applied to shapes.
* Returns true if the tokens in this catalog can be edited. Only the
* catalog of the current file (`penpot.library.local.tokens`) can be
* edited, and only when the file is its own tokens source.
* When the tokens come from an external library, the local catalog
* can still change the active themes and sets, and its tokens can
* still be applied to shapes.
*/
readonly isEditableTokens: boolean;
@ -5370,7 +5398,8 @@ export interface TokenSet {
name: string;
/**
* Indicates if the set is currently active.
* Indicates if the set is active in the file that owns this catalog.
* Only sets from `penpot.library.local.tokens` can be changed.
*/
active: boolean;
@ -5386,6 +5415,7 @@ export interface TokenSet {
/**
* Toggles the active status of this set.
* Only sets from `penpot.library.local.tokens` can be changed.
*/
toggleActive(): void;
@ -5470,12 +5500,14 @@ export interface TokenTheme {
name: string;
/**
* Indicates if the theme is currently active.
* Indicates if the theme is active in the file that owns this catalog.
* Only themes from `penpot.library.local.tokens` can be changed.
*/
active: boolean;
/**
* Toggles the active status of this theme.
* Only themes from `penpot.library.local.tokens` can be changed.
*/
toggleActive(): void;