Compare commits

...

10 Commits

Author SHA1 Message Date
Andrés Moya
a5a0142d33 🔧 Add tokenscript flag and more validations to token values 2026-02-23 17:25:00 +01:00
Andrés Moya
a46ff9d828 🐛 Fix activeSets in themes API 2026-02-23 17:25:00 +01:00
Eva Marco
d96c6dd165 🐛 Fix input width on composite token form 2026-02-23 17:25:00 +01:00
Andrey Antukh
1b8afccba2 Remove usage of multipart body size config on backend 2026-02-23 14:44:44 +01:00
Yamila Moreno
dd856ecf50 ♻️ Deprecate PENPOT_HTTP_SERVER_MAX_MULTIPART_BODY_SIZE envvar 2026-02-23 13:48:01 +01:00
Andrey Antukh
145198c148 📎 Use proper version tag on frontend index template 2026-02-23 12:17:58 +01:00
alonso.torres
eddfc4c4b2 🐛 Fix problem with createText in plugins 2026-02-23 09:35:30 +01:00
alonso.torres
e6e34af391 🐛 Show outline on hidden paths 2026-02-23 09:34:50 +01:00
Andrés Moya
3d41dc276e 🐛 Fix resolve tokens with tokenscript when type is font family 2026-02-20 12:41:17 +01:00
alonso.torres
cee974a906 🐛 Fix problem with tokens in plugins 2026-02-18 17:20:46 +01:00
35 changed files with 244 additions and 116 deletions

View File

@@ -2,6 +2,9 @@
## 2.14.0 (Unreleased)
### :boom: Breaking changes & Deprecations
- Deprecate `PENPOT_HTTP_SERVER_MAX_MULTIPART_BODY_SIZE` in favour of `PENPOT_HTTP_SERVER_MAX_BODY_SIZE`.
### :sparkles: New features & Enhancements
- Access to design tokens in Penpot Plugins [Taiga #8990](https://tree.taiga.io/project/penpot/us/8990)

View File

@@ -28,8 +28,8 @@
com.google.guava/guava {:mvn/version "33.4.8-jre"}
funcool/yetti
{:git/tag "v11.8"
:git/sha "1d1b33f"
{:git/tag "v11.9"
:git/sha "5fad7a9"
:git/url "https://github.com/funcool/yetti.git"
:exclusions [org.slf4j/slf4j-api]}

View File

@@ -98,7 +98,6 @@
[:http-server-port {:optional true} ::sm/int]
[:http-server-host {:optional true} :string]
[:http-server-max-body-size {:optional true} ::sm/int]
[:http-server-max-multipart-body-size {:optional true} ::sm/int]
[:http-server-io-threads {:optional true} ::sm/int]
[:http-server-max-worker-threads {:optional true} ::sm/int]

View File

@@ -42,8 +42,8 @@
(def default-params
{::port 6060
::host "0.0.0.0"
::max-body-size 31457280 ; default 30 MiB
::max-multipart-body-size 367001600}) ; default 350 MiB
::max-body-size 367001600 ; default 350 MiB
})
(defmethod ig/expand-key ::server
[k v]
@@ -56,7 +56,6 @@
[::io-threads {:optional true} ::sm/int]
[::max-worker-threads {:optional true} ::sm/int]
[::max-body-size {:optional true} ::sm/int]
[::max-multipart-body-size {:optional true} ::sm/int]
[::router {:optional true} [:fn r/router?]]
[::handler {:optional true} ::sm/fn]])
@@ -79,7 +78,7 @@
{:http/port port
:http/host host
:http/max-body-size (::max-body-size cfg)
:http/max-multipart-body-size (::max-multipart-body-size cfg)
:http/max-multipart-body-size (::max-body-size cfg)
:xnio/direct-buffers false
:xnio/io-threads (::io-threads cfg)
:xnio/max-worker-threads (::max-worker-threads cfg)

View File

@@ -226,11 +226,10 @@
::http/server
{::http/port (cf/get :http-server-port)
::http/host (cf/get :http-server-host)
::http/router (ig/ref ::http/router)
::http/io-threads (cf/get :http-server-io-threads)
::http/max-worker-threads (cf/get :http-server-max-worker-threads)
::http/max-body-size (cf/get :http-server-max-body-size)
::http/max-multipart-body-size (cf/get :http-server-max-multipart-body-size)
::http/router (ig/ref ::http/router)
::mtx/metrics (ig/ref ::mtx/metrics)}
::ldap/provider

View File

@@ -760,6 +760,21 @@
default
v))))
(defn percent?
[v]
(str/numeric? (str/rtrim v "%")))
(defn parse-percent
([v]
(parse-percent v nil))
([v default]
(if (str/ends-with? v "%")
(let [v (impl-parse-double (str/trim v "%"))]
(if (or (nil? v) (nan? v))
default
(/ v 100)))
(parse-double v default))))
(defn parse-uuid
[v]
(try

View File

@@ -31,18 +31,56 @@
(def schema:token-value-generic
[::sm/text {:error/fn token-value-empty-fn}])
(def schema:token-value-numeric
[:and
[::sm/text {:error/fn token-value-empty-fn}]
[:fn {:error/fn #(tr "workspace.tokens.invalid-value" (:value %))}
(fn [value]
(if (str/numeric? value)
(let [n (d/parse-double value)]
(some? n))
true))]]) ;; Leave references or formulas to be checked by the resolver
(def schema:token-value-percent
[:and
[::sm/text {:error/fn token-value-empty-fn}]
[:fn {:error/fn #(tr "workspace.tokens.value-with-percent" (:value %))}
(fn [value]
(if (d/percent? value)
(let [v (d/parse-percent value)]
(some? v))
true))]]) ;; Leave references or formulas to be checked by the resolver
(def schema:token-value-composite-ref
[::sm/text {:error/fn token-value-empty-fn}])
(def schema:token-value-opacity
[:and
[::sm/text {:error/fn token-value-empty-fn}]
[:fn {:error/fn #(tr "workspace.tokens.opacity-range" (:value %))}
(fn [opacity]
(if (str/numeric? opacity)
(let [n (d/parse-percent opacity)]
(and (some? n) (<= 0 n 1)))
true))]]) ;; Leave references or formulas to be checked by the resolver
(def schema:token-value-font-family
[:vector ::sm/text])
[:or
[:vector ::sm/text]
cto/schema:token-ref])
(def schema:token-value-font-weight
[:or
[:fn {:error/fn #(tr "workspace.tokens.invalid-font-weight-token-value" (:value %))}
cto/valid-font-weight-variant]
::sm/text]) ;; Leave references or formulas to be checked by the resolver
(def schema:token-value-typography-map
[:map
[:font-family {:optional true} schema:token-value-font-family]
[:font-weight {:optional true} schema:token-value-generic]
[:font-size {:optional true} schema:token-value-generic]
[:line-height {:optional true} schema:token-value-generic]
[:font-size {:optional true} schema:token-value-numeric]
[:font-weight {:optional true} schema:token-value-font-weight]
[:line-height {:optional true} schema:token-value-percent]
[:letter-spacing {:optional true} schema:token-value-generic]
[:paragraph-spacing {:optional true} schema:token-value-generic]
[:text-decoration {:optional true} schema:token-value-generic]
@@ -61,14 +99,14 @@
[:blur
[:and
:string
[:fn {:error/fn #(tr "workspace.tokens.shadow-token-blur-value-error")}
[:fn {:error/fn #(tr "workspace.tokens.shadow-token-blur-value-error" (:value %))}
(fn [blur]
(let [n (d/parse-double blur)]
(or (nil? n) (not (< n 0)))))]]]
[:spread
[:and
:string
[:fn {:error/fn #(tr "workspace.tokens.shadow-token-spread-value-error")}
[:fn {:error/fn #(tr "workspace.tokens.shadow-token-spread-value-error" (:value %))}
(fn [spread]
(let [n (d/parse-double spread)]
(or (nil? n) (not (< n 0)))))]]]
@@ -84,7 +122,10 @@
[token-type]
[:multi {:dispatch (constantly token-type)
:title "Token Value"}
[:opacity schema:token-value-opacity]
[:font-family schema:token-value-font-family]
[:font-size schema:token-value-numeric]
[:font-weight schema:token-value-font-weight]
[:typography schema:token-value-typography]
[:shadow schema:token-value-shadow]
[::m/default schema:token-value-generic]])

View File

@@ -143,6 +143,15 @@
:gen/gen sg/text}
token-name-validation-regex])
(def token-ref-validation-regex
#"^\{[a-zA-Z0-9_-][a-zA-Z0-9$_-]*(\.[a-zA-Z0-9$_-]+)*\}$")
(def schema:token-ref
"A token reference is a token name enclosed in {}."
[:re {:title "TokenRef"
:gen/gen sg/text}
token-ref-validation-regex])
(def schema:token-type
[::sm/one-of {:decode/json (fn [type]
(if (string? type)

View File

@@ -50,6 +50,7 @@ services:
- 4400:4400
- 4401:4401
- 4402:4402
- 4403:4403
# Plugins
- 4200:4200

View File

@@ -30,11 +30,9 @@ x-uri: &penpot-public-uri
PENPOT_PUBLIC_URI: http://localhost:9001
x-body-size: &penpot-http-body-size
# Max body size (30MiB); Used for plain requests, should never be
# greater than multi-part size
PENPOT_HTTP_SERVER_MAX_BODY_SIZE: 31457280
# Max multipart body size (350MiB)
# Max body size
PENPOT_HTTP_SERVER_MAX_BODY_SIZE: 367001600
# Deprecation warning: this variable is deprecated. Use PENPOT_HTTP_SERVER_MAX_BODY (defaults to 367001600)
PENPOT_HTTP_SERVER_MAX_MULTIPART_BODY_SIZE: 367001600
## Penpot SECRET KEY. It serves as a master key from which other keys for subsystems

View File

@@ -30,8 +30,8 @@ update_flags /var/www/app/js/config.js
export PENPOT_BACKEND_URI=${PENPOT_BACKEND_URI:-http://penpot-backend:6060}
export PENPOT_EXPORTER_URI=${PENPOT_EXPORTER_URI:-http://penpot-exporter:6061}
export PENPOT_NITRATE_URI=${PENPOT_NITRATE_URI:-http://penpot-nitrate:3000}
export PENPOT_HTTP_SERVER_MAX_MULTIPART_BODY_SIZE=${PENPOT_HTTP_SERVER_MAX_MULTIPART_BODY_SIZE:-367001600} # Default to 350MiB
envsubst "\$PENPOT_BACKEND_URI,\$PENPOT_EXPORTER_URI,\$PENPOT_NITRATE_URI,\$PENPOT_HTTP_SERVER_MAX_MULTIPART_BODY_SIZE" \
export PENPOT_HTTP_SERVER_MAX_BODY_SIZE=${PENPOT_HTTP_SERVER_MAX_BODY_SIZE:-367001600} # Default to 350MiB
envsubst "\$PENPOT_BACKEND_URI,\$PENPOT_EXPORTER_URI,\$PENPOT_NITRATE_URI,\$PENPOT_HTTP_SERVER_MAX_BODY_SIZE" \
< /tmp/nginx.conf.template > /etc/nginx/nginx.conf
PENPOT_DEFAULT_INTERNAL_RESOLVER="$(awk 'BEGIN{ORS=" "} $1=="nameserver" { sub(/%.*$/,"",$2); print ($2 ~ ":")? "["$2"]": $2}' /etc/resolv.conf)"

View File

@@ -76,7 +76,7 @@ http {
listen [::]:8080 default_server;
server_name _;
client_max_body_size $PENPOT_HTTP_SERVER_MAX_MULTIPART_BODY_SIZE;
client_max_body_size $PENPOT_HTTP_SERVER_MAX_BODY_SIZE;
charset utf-8;
etag off;

View File

@@ -188,8 +188,8 @@ server {
server_name penpot.mycompany.com;
# This value should be in sync with the corresponding in the docker-compose.yml
# PENPOT_HTTP_SERVER_MAX_BODY_SIZE: 31457280
client_max_body_size 31457280;
# PENPOT_HTTP_SERVER_MAX_BODY_SIZE: 367001600
client_max_body_size 367001600;
# Logs: Configure your logs following the best practices inside your company
access_log /path/to/penpot.access.log;

View File

@@ -18,7 +18,7 @@
<meta name="twitter:creator" content="@penpotapp">
<meta name="theme-color" content="#FFFFFF" media="(prefers-color-scheme: light)">
<link id="theme" href="css/main.css?version={{& version_tag}}" rel="stylesheet" type="text/css" />
<link href="css/ui.css?ts={{& ts}}" rel="stylesheet" type="text/css" />
<link href="css/ui.css?ts={{& version_tag}}" rel="stylesheet" type="text/css" />
{{#isDebug}}
<link href="css/debug.css?version={{& version_tag}}" rel="stylesheet" type="text/css" />
{{/isDebug}}

View File

@@ -69,6 +69,10 @@
(and (number-with-unit-symbol? v)
(= (.-unit v) "rem")))
(defn percent-number-with-unit? [v]
(and (number-with-unit-symbol? v)
(= (.-unit v) "%")))
(defn rem->px [^js v]
(* (.-value v) 16))
@@ -79,7 +83,7 @@
Structured tokens are non-primitive token types like `typography` or `box-shadow`."
[^js token-symbol]
(if (instance? js/Array (.-value token-symbol))
(mapv structured-token->penpot-map (.-value token-symbol))
(mapv tokenscript-symbols->penpot-unit (.-value token-symbol))
(let [entries (es6-iterator-seq (.entries (.-value token-symbol)))]
(into {} (map (fn [[k v :as V]]
[(keyword k) (tokenscript-symbols->penpot-unit v)])
@@ -87,10 +91,12 @@
(defn tokenscript-symbols->penpot-unit [^js v]
(cond
(nil? v) nil
(structured-token? v) (structured-token->penpot-map v)
(list-symbol? v) (tokenscript-symbols->penpot-unit (.nth 1 v))
(list-symbol? v) (structured-token->penpot-map v)
(color-symbol? v) (.-value (.to v "hex"))
(rem-number-with-unit? v) (rem->px v)
(percent-number-with-unit? v) (/ (.-value v) 100)
:else (.-value v)))
;; Processors ------------------------------------------------------------------

View File

@@ -8,10 +8,11 @@
(:require
[app.common.json :as json]
[app.common.path-names :as cpn]
[app.common.types.tokens-lib :as ctob]
[app.config :as cf]
[app.main.data.notifications :as ntf]
[app.main.data.style-dictionary :as sd]
[app.main.data.tokenscript :as ts]
[app.main.data.workspace.tokens.errors :as wte]
[app.main.store :as st]
[app.util.i18n :refer [tr]]
@@ -74,15 +75,18 @@
(when unknown-tokens
(st/emit! (show-unknown-types-warning unknown-tokens)))
(try
(->> (ctob/get-all-tokens-map tokens-lib)
(sd/resolve-tokens-with-verbose-errors)
(rx/map (fn [_]
tokens-lib))
(rx/catch (fn [sd-error]
(let [reference-errors (extract-reference-errors sd-error)]
(if reference-errors
(rx/of tokens-lib)
(throw (wte/error-ex-info :error.import/style-dictionary-unknown-error sd-error sd-error)))))))
(let [tokens-tree (ctob/get-all-tokens-map tokens-lib)
resolved-tokens (if (contains? cf/flags :tokenscript)
(rx/of (ts/resolve-tokens tokens-tree))
(sd/resolve-tokens-with-verbose-errors tokens-tree))]
(->> resolved-tokens
(rx/map (fn [_]
tokens-lib))
(rx/catch (fn [sd-error]
(let [reference-errors (extract-reference-errors sd-error)]
(if reference-errors
(rx/of tokens-lib)
(throw (wte/error-ex-info :error.import/style-dictionary-unknown-error sd-error sd-error))))))))
(catch js/Error e
(throw (wte/error-ex-info :error.import/style-dictionary-unknown-error "" e)))))

View File

@@ -6,13 +6,16 @@
(ns app.main.data.workspace.tokens.propagation
(:require
[app.common.data :as d]
[app.common.files.helpers :as cfh]
[app.common.logging :as l]
[app.common.time :as ct]
[app.common.types.token :as ctt]
[app.common.types.tokens-lib :as ctob]
[app.config :as cf]
[app.main.data.helpers :as dsh]
[app.main.data.style-dictionary :as sd]
[app.main.data.tokenscript :as ts]
[app.main.data.workspace.shapes :as dwsh]
[app.main.data.workspace.thumbnails :as dwt]
[app.main.data.workspace.tokens.application :as dwta]
@@ -210,10 +213,13 @@
(ptk/reify ::propagate-workspace-tokens
ptk/WatchEvent
(watch [_ state _]
(when-let [tokens-lib (-> (dsh/lookup-file-data state)
(get :tokens-lib))]
(->> (ctob/get-tokens-in-active-sets tokens-lib)
(sd/resolve-tokens)
(when-let [tokens-tree (-> (dsh/lookup-file-data state)
(get :tokens-lib)
(ctob/get-tokens-in-active-sets))]
(->> (if (contains? cf/flags :tokenscript)
(rx/of (-> (ts/resolve-tokens tokens-tree)
(d/update-vals #(update % :resolved-value ts/tokenscript-symbols->penpot-unit))))
(sd/resolve-tokens tokens-tree))
(rx/mapcat (fn [sd-tokens]
(let [undo-id (js/Symbol)]
(rx/concat

View File

@@ -84,6 +84,7 @@
:on-click on-icon-click}])
(if aria-label
[:> tooltip* {:content aria-label
:class (stl/css :tooltip-wrapper)
:id tooltip-id}
[:> "input" props]]
[:> "input" props])

View File

@@ -120,3 +120,7 @@
color: var(--color-foreground-secondary);
min-inline-size: var(--sp-l);
}
.tooltip-wrapper {
inline-size: 100%;
}

View File

@@ -143,8 +143,7 @@
(let [token-ids (set tokens-in-path-ids)
remaining-tokens (filter (fn [token]
(not (contains? token-ids (:id token))))
selected-token-set-tokens)
_ (prn "Remaining tokens:" remaining-tokens)]
selected-token-set-tokens)]
(seq remaining-tokens))))
delete-token

View File

@@ -13,8 +13,10 @@
[app.common.types.color :as cl]
[app.common.types.token :as cto]
[app.common.types.tokens-lib :as ctob]
[app.config :as cf]
[app.main.data.style-dictionary :as sd]
[app.main.data.tinycolor :as tinycolor]
[app.main.data.tokenscript :as ts]
[app.main.data.workspace.tokens.format :as dwtf]
[app.main.refs :as refs]
[app.main.ui.ds.controls.input :as ds]
@@ -70,11 +72,15 @@
(dissoc (:name prev-token))
(update (:name token) #(ctob/make-token (merge % prev-token token))))]
(->> tokens
(sd/resolve-tokens-interactive)
(->> (if (contains? cf/flags :tokenscript)
(rx/of (ts/resolve-tokens tokens))
(sd/resolve-tokens-interactive tokens))
(rx/mapcat
(fn [resolved-tokens]
(let [{:keys [errors resolved-value] :as resolved-token} (get resolved-tokens (:name token))]
(let [{:keys [errors resolved-value] :as resolved-token} (get resolved-tokens (:name token))
resolved-value (if (contains? cf/flags :tokenscript)
(ts/tokenscript-symbols->penpot-unit resolved-value)
resolved-value)]
(if resolved-value
(rx/of {:value resolved-value})
(rx/of {:error (first errors)}))))))))

View File

@@ -10,7 +10,9 @@
[app.common.data :as d]
[app.common.types.token :as cto]
[app.common.types.tokens-lib :as ctob]
[app.config :as cf]
[app.main.data.style-dictionary :as sd]
[app.main.data.tokenscript :as ts]
[app.main.fonts :as fonts]
[app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
[app.main.ui.ds.controls.input :refer [input*]]
@@ -49,28 +51,30 @@
;; validate data within the form state.
(defn- resolve-value
[tokens prev-token token-name value]
(let [valid-token-name?
(and (string? token-name)
(re-matches cto/token-name-validation-regex token-name))
[tokens prev-token _token-name value]
(let [tmp-value (cto/split-font-family value)
tmp-name "__PENPOT__FONT_FAMILY__PLACEHOLDER__"
;; Create a temporary font-family token to validate the value
token
{:value (cto/split-font-family value)
:name (if (or (not valid-token-name?) (str/blank? token-name))
"__PENPOT__TOKEN__NAME__PLACEHOLDER__"
token-name)}
{:name tmp-name
:type :font-family
:value (if (= (:type prev-token) :typography)
(assoc (:value prev-token) :font-family tmp-value)
tmp-value)}
tokens
(-> tokens
;; Remove previous token when renaming a token
(dissoc (:name prev-token))
(update (:name token) #(ctob/make-token (merge % prev-token token))))]
(update tokens (:name token) #(ctob/make-token (merge % prev-token token)))]
(->> tokens
(sd/resolve-tokens-interactive)
(->> (if (contains? cf/flags :tokenscript)
(rx/of (ts/resolve-tokens tokens))
(sd/resolve-tokens-interactive tokens))
(rx/mapcat
(fn [resolved-tokens]
(let [{:keys [errors resolved-value] :as resolved-token} (get resolved-tokens (:name token))]
(let [{:keys [errors resolved-value] :as resolved-token} (get resolved-tokens (:name token))
resolved-value (if (contains? cf/flags :tokenscript)
(ts/tokenscript-symbols->penpot-unit resolved-value)
resolved-value)]
(if resolved-value
(rx/of {:value resolved-value})
(rx/of {:error (first errors)}))))))))
@@ -176,7 +180,6 @@
(let [message (tr "workspace.tokens.resolved-value" value)]
(swap! form update :extra-errors dissoc input-name)
(reset! hint* {:message message :type "hint"})))))))]
(fn []
(rx/dispose! subs))))

View File

@@ -175,7 +175,10 @@
(sd/resolve-tokens-interactive)
(rx/mapcat
(fn [resolved-tokens]
(let [{:keys [errors resolved-value] :as resolved-token} (get resolved-tokens (:name token))]
(let [{:keys [errors resolved-value] :as resolved-token} (get resolved-tokens (:name token))
resolved-value (if (contains? cf/flags :tokenscript)
(ts/tokenscript-symbols->penpot-unit resolved-value)
resolved-value)]
(if resolved-value
(rx/of {:value resolved-value})
(rx/of {:error (first errors)}))))))))

View File

@@ -36,9 +36,9 @@
[rumext.v2 :as mf]))
(defn get-value-for-validator
[active-tab value value-subfield form-type]
[active-tab value value-subfield value-type]
(case form-type
(case value-type
:indexed
(if (= active-tab :reference)
(:reference value)
@@ -62,7 +62,7 @@
make-schema
input-component
initial
type
value-type
value-subfield
input-value-placeholder] :as props}]
@@ -178,13 +178,13 @@
on-submit
(mf/use-fn
(mf/deps validate-token token tokens token-type value-subfield type active-tab on-remap-token on-rename-token is-create)
(mf/deps validate-token token tokens token-type value-subfield value-type active-tab on-remap-token on-rename-token is-create)
(fn [form _event]
(let [name (get-in @form [:clean-data :name])
path (str (d/name token-type) "." name)
description (get-in @form [:clean-data :description])
value (get-in @form [:clean-data :value])
value-for-validation (get-value-for-validator active-tab value value-subfield type)]
value-for-validation (get-value-for-validator active-tab value value-subfield value-type)]
(->> (validate-token {:token-value value-for-validation
:token-name name
:token-description description
@@ -245,7 +245,7 @@
:auto-focus true}]]
[:div {:class (stl/css :input-row)}
(case type
(case value-type
:indexed
[:> input-component
{:token token

View File

@@ -365,7 +365,7 @@
:token-type token-type
:initial initial
:make-schema make-schema
:type :indexed
:value-type :indexed
:value-subfield :shadow
:input-component tabs-wrapper*
:validator validate-shadow-token})]

View File

@@ -300,6 +300,6 @@
:make-schema make-schema
:token token
:validator validate-typography-token
:type :composite
:value-type :composite
:input-component tabs-wrapper*})]
[:> generic/form* props]))

View File

@@ -4,7 +4,9 @@
[app.common.schema :as sm]
[app.common.types.token :as cto]
[app.common.types.tokens-lib :as ctob]
[app.config :as cf]
[app.main.data.style-dictionary :as sd]
[app.main.data.tokenscript :as ts]
[app.main.data.workspace.tokens.errors :as wte]
[beicon.v2.core :as rx]
[cuerdas.core :as str]))
@@ -36,14 +38,20 @@
:always
(update (:name token) #(ctob/make-token (merge % prev-token token))))]
(->> tokens'
(sd/resolve-tokens-interactive)
(->> (if (contains? cf/flags :tokenscript)
(rx/of (ts/resolve-tokens tokens'))
(sd/resolve-tokens-interactive tokens'))
(rx/mapcat
(fn [resolved-tokens]
(let [{:keys [errors resolved-value] :as resolved-token} (get resolved-tokens (:name token))]
(let [resolved-token (cond-> (get resolved-tokens (:name token))
(contains? cf/flags :tokenscript)
(update :resolved-value ts/tokenscript-symbols->penpot-unit))]
(cond
resolved-value (rx/of resolved-token)
:else (rx/throw {:errors (or (seq errors)
(:resolved-value resolved-token)
(rx/of resolved-token)
:else (rx/throw {:errors (or (seq (:errors resolved-token))
[(wte/get-error-code :error/unknown-error)])}))))))))
(defn- validate-token-with [token validators]

View File

@@ -176,9 +176,10 @@
(mf/defc token-pill*
{::mf/wrap [mf/memo]}
[{:keys [on-click token on-context-menu selected-shapes is-selected-inside-layout active-theme-tokens]}]
(let [{:keys [name value errors type]} token
(let [{:keys [name value type]} token
resolved-token (get active-theme-tokens (:name token))
errors (:errors resolved-token)
has-selected? (pos? (count selected-shapes))
is-reference? (cfo/is-reference? token)

View File

@@ -111,11 +111,6 @@
:modifier modifier
:zoom zoom}]))))
(defn- show-outline?
[shape]
(and (not (:hidden shape))
(not (:blocked shape))))
(mf/defc shape-outlines
{::mf/wrap-props false}
[props]
@@ -133,8 +128,7 @@
shapes (-> #{}
(into (comp (remove edition?)
(keep lookup)
(filter show-outline?))
(keep lookup))
(set/union selected hover))
(into (comp (remove edition?)
(keep lookup))

View File

@@ -27,6 +27,7 @@
[app.main.data.workspace.media :as dwm]
[app.main.data.workspace.selection :as dws]
[app.main.data.workspace.wasm-text :as dwwt]
[app.main.features :as features]
[app.main.fonts :refer [fetch-font-css]]
[app.main.router :as rt]
[app.main.store :as st]
@@ -365,8 +366,10 @@
(cb/add-object shape))]
(st/emit! (ch/commit-changes changes)
(se/event plugin-id "create-shape" :type :text)
(dwwt/resize-wasm-text-debounce (:id shape)))
(se/event plugin-id "create-shape" :type :text))
(when (features/active-feature? @st/state "render-wasm/v1")
(st/emit! (dwwt/resize-wasm-text-debounce (:id shape))))
(shape/shape-proxy plugin-id (:id shape)))))

View File

@@ -1305,7 +1305,8 @@
tokens)))}
:applyToken
{:schema [:tuple
{:enumerable false
:schema [:tuple
[:fn token-proxy?]
[:maybe [:set [:and ::sm/keyword [:fn cto/token-attr?]]]]]
:fn (fn [token attrs]

View File

@@ -12,14 +12,12 @@
[app.common.types.token :as cto]
[app.common.types.tokens-lib :as ctob]
[app.common.uuid :as uuid]
[app.main.data.style-dictionary :as sd]
[app.main.data.tokenscript :as ts]
[app.main.data.workspace.tokens.application :as dwta]
[app.main.data.workspace.tokens.library-edit :as dwtl]
[app.main.store :as st]
[app.plugins.utils :as u]
[app.util.object :as obj]
[beicon.v2.core :as rx]
[clojure.datafy :refer [datafy]]))
;; === Token
@@ -144,14 +142,16 @@
(st/emit! (dwtl/delete-token set-id id)))
:applyToShapes
{:schema [:tuple
{:enumerable false
:schema [:tuple
[:vector [:fn shape-proxy?]]
[:maybe [:set [:and ::sm/keyword [:fn cto/token-attr?]]]]]
:fn (fn [shapes attrs]
(apply-token-to-shapes file-id set-id id (map #(obj/get % "$id") shapes) attrs))}
:applyToSelected
{:schema [:tuple [:maybe [:set [:and ::sm/keyword [:fn cto/token-attr?]]]]]
{:enumerable false
:schema [:tuple [:maybe [:set [:and ::sm/keyword [:fn cto/token-attr?]]]]]
:fn (fn [attrs]
(let [selected (get-in @st/state [:workspace-local :selected])]
(apply-token-to-shapes file-id set-id id selected attrs)))}))
@@ -236,14 +236,16 @@
(apply array))))}
:getTokenById
{:schema [:tuple ::sm/uuid]
{:enumerable false
:schema [:tuple ::sm/uuid]
:fn (fn [token-id]
(let [token (u/locate-token file-id id token-id)]
(when (some? token)
(token-proxy plugin-id file-id id token-id))))}
:addToken
{:schema (fn [args]
{:enumerable false
:schema (fn [args]
[:tuple (-> (cfo/make-token-schema
(-> (u/locate-tokens-lib file-id) (ctob/get-tokens id))
(cto/dtcg-token-type->token-type (-> args (first) (get "type"))))
@@ -256,20 +258,19 @@
:decode/options {:key-fn identity}
:fn (fn [attrs]
(let [tokens-lib (u/locate-tokens-lib file-id)
tokens-tree (ctob/get-tokens-in-active-sets tokens-lib)
token (ctob/make-token attrs)]
(->> (assoc tokens-tree (:name token) token)
(sd/resolve-tokens-interactive)
(rx/subs!
(fn [resolved-tokens]
(let [{:keys [errors resolved-value] :as resolved-token} (get resolved-tokens (:name token))]
(if resolved-value
(st/emit! (dwtl/create-token id token))
(u/display-not-valid :addToken (str errors)))))))
;; TODO: as the addToken function is synchronous, we must return the newly created
;; token even if the validator will throw it away if the resolution fails.
;; This will be solved with the TokenScript resolver, that is syncronous.
(token-proxy plugin-id file-id id (:id token))))}
token (ctob/make-token attrs)
tokens-tree (-> (ctob/get-tokens-in-active-sets tokens-lib)
(assoc (:name token) token))
resolved-tokens (ts/resolve-tokens tokens-tree)
{:keys [errors resolved-value] :as resolved-token}
(get resolved-tokens (:name token))]
(if resolved-value
(do (st/emit! (dwtl/create-token id token))
(token-proxy plugin-id file-id id (:id token)))
(do (u/display-not-valid :addToken (str errors))
nil))))}
:duplicate
(fn []
@@ -350,16 +351,28 @@
(st/emit! (dwtl/toggle-token-theme-active id)))
:activeSets
{:this true :get (fn [_])}
{:this true
:get (fn [_]
(let [tokens-lib (u/locate-tokens-lib file-id)
theme (u/locate-token-theme file-id id)]
(->> theme
:sets
(map #(->> %
(ctob/get-set-by-name tokens-lib)
(ctob/get-id)
(token-set-proxy plugin-id file-id)))
(apply array))))}
:addSet
{:schema [:tuple [:fn token-set-proxy?]]
{:enumerable false
:schema [:tuple [:fn token-set-proxy?]]
:fn (fn [token-set]
(let [theme (u/locate-token-theme file-id id)]
(st/emit! (dwtl/update-token-theme id (ctob/enable-set theme (obj/get token-set :name))))))}
:removeSet
{:schema [:tuple [:fn token-set-proxy?]]
{:enumerable false
:schema [:tuple [:fn token-set-proxy?]]
:fn (fn [token-set]
(let [theme (u/locate-token-theme file-id id)]
(st/emit! (dwtl/update-token-theme id (ctob/disable-set theme (obj/get token-set :name))))))}
@@ -406,7 +419,8 @@
(apply array (map #(token-set-proxy plugin-id file-id (ctob/get-id %)) sets))))}
:addTheme
{:schema (fn [attrs]
{:enumerable false
:schema (fn [attrs]
[:tuple (-> (sm/schema (cfo/make-token-theme-schema
(u/locate-tokens-lib file-id)
(or (obj/get attrs "group") "")
@@ -419,7 +433,8 @@
(token-theme-proxy plugin-id file-id (:id theme))))}
:addSet
{:schema [:tuple (-> (sm/schema (cfo/make-token-set-schema
{:enumerable false
:schema [:tuple (-> (sm/schema (cfo/make-token-set-schema
(u/locate-tokens-lib file-id)
nil))
(sm/dissoc-key :id))] ;; We don't allow plugins to set the id
@@ -431,14 +446,16 @@
(token-set-proxy plugin-id file-id (ctob/get-id set))))}
:getThemeById
{:schema [:tuple ::sm/uuid]
{:enumerable false
:schema [:tuple ::sm/uuid]
:fn (fn [theme-id]
(let [theme (u/locate-token-theme file-id theme-id)]
(when (some? theme)
(token-theme-proxy plugin-id file-id theme-id))))}
:getSetById
{:schema [:tuple ::sm/uuid]
{:enumerable false
:schema [:tuple ::sm/uuid]
:fn (fn [set-id]
(let [set (u/locate-token-set file-id set-id)]
(when (some? set)

View File

@@ -18,7 +18,12 @@
<ul data-handler="themes-list">
@for (theme of themes; track theme.id) {
<li class="body-m panel-item theme-item">
<span>{{ theme.group }} / {{ theme.name }}</span>
<span title="{{ theme.activeSets }}">
@if (theme.group) {
{{ theme.group }} /
}
{{ theme.name }}
</span>
<button
type="button"
data-appearance="secondary"

View File

@@ -10,6 +10,7 @@ type TokenTheme = {
group: string;
description: string;
active: boolean;
activeSets: string;
};
type TokenSet = {

View File

@@ -61,11 +61,13 @@ function loadLibrary() {
const themes = tokensCatalog.themes;
const themesData = themes.map((theme) => {
const activeSets = theme.activeSets.map((set) => set.name).join(', ');
return {
id: theme.id,
group: theme.group,
name: theme.name,
active: theme.active,
activeSets: activeSets,
};
});