Compare commits

..

7 Commits

Author SHA1 Message Date
Andrés Moya
ce776cb5f4 🔧 Use schema in token api methods 2026-02-05 10:12:24 +01:00
Andrés Moya
3de4498851 🔧 Use multi schema for token value 2026-02-05 10:11:19 +01:00
Andrés Moya
b156a917a7 🔧 Use schemas to validate token creation 2026-02-05 10:11:19 +01:00
Andrés Moya
5273791464 🔧 Use automatic validation in token proxies 2026-02-05 10:11:19 +01:00
Andrey Antukh
3fd6c3305a 🔧 Refactor token validation schemas 2026-02-05 10:11:17 +01:00
Andrés Moya
3ddfb5cce5 📚 Document better the tokens value in plugins API 2026-02-05 10:06:58 +01:00
Andrey Antukh
6e48e73b40 🎉 Add tokens to plugins API documentation
And add poc plugin example
2026-02-05 10:06:58 +01:00
80 changed files with 3807 additions and 1409 deletions

View File

@@ -10,7 +10,6 @@
[app.common.logging :as l]
[app.db :as db]
[app.migrations.clj.migration-0023 :as mg0023]
[app.migrations.clj.migration-0145 :as mg0145]
[app.util.migrations :as mg]
[integrant.core :as ig]))
@@ -460,11 +459,7 @@
:fn (mg/resource "app/migrations/sql/0143-add-http-session-v2-table.sql")}
{:name "0144-mod-server-error-report-table"
:fn (mg/resource "app/migrations/sql/0144-mod-server-error-report-table.sql")}
{:name "0145-fix-plugins-uri-on-profile"
:fn mg0145/migrate}])
:fn (mg/resource "app/migrations/sql/0144-mod-server-error-report-table.sql")}])
(defn apply-migrations!
[pool name migrations]

View File

@@ -58,3 +58,4 @@
(when (nil? (:data file))
(migrate-file conn file)))
(db/exec-one! conn ["drop table page cascade;"])))

View File

@@ -1,83 +0,0 @@
;; 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 app.migrations.clj.migration-0145
"Migrate plugins references on profiles"
(:require
[app.common.data :as d]
[app.common.logging :as l]
[app.db :as db]
[cuerdas.core :as str]))
(def ^:private replacements
{"https://colors-to-tokens-plugin.pages.dev"
"https://colors-to-tokens.plugins.penpot.app"
"https://contrast-penpot-plugin.pages.dev"
"https://contrast.plugins.penpot.app"
"https://create-palette-penpot-plugin.pages.dev"
"https://create-palette.plugins.penpot.app"
"https://icons-penpot-plugin.pages.dev"
"https://icons.plugins.penpot.app"
"https://lorem-ipsum-penpot-plugin.pages.dev"
"https://lorem-ipsum.plugins.penpot.app"
"https://rename-layers-penpot-plugin.pages.dev"
"https://rename-layers.plugins.penpot.app"
"https://table-penpot-plugin.pages.dev"
"https://table.plugins.penpot.app"})
(defn- fix-url
[url]
(reduce-kv (fn [url prefix replacement]
(if (str/starts-with? url prefix)
(reduced (str replacement (subs url (count prefix))))
url))
url
replacements))
(defn- fix-manifest
[manifest]
(-> manifest
(d/update-when :url fix-url)
(d/update-when :host fix-url)))
(defn- fix-plugins-data
[props]
(d/update-in-when props [:plugins :data]
(fn [data]
(reduce-kv (fn [data id manifest]
(let [manifest' (fix-manifest manifest)]
(if (= manifest manifest')
data
(assoc data id manifest'))))
data
data))))
(def ^:private sql:get-profiles
"SELECT id, props FROM profile
WHERE props ?? '~:plugins'
ORDER BY created_at
FOR UPDATE")
(defn migrate
[conn]
(->> (db/plan conn [sql:get-profiles])
(run! (fn [{:keys [id props]}]
(when-let [props (some-> props db/decode-transit-pgobject)]
(let [props' (fix-plugins-data props)]
(when (not= props props')
(l/inf :hint "fixing plugins data on profile props" :profile-id (str id))
(db/update! conn :profile
{:props (db/tjson props')}
{:id id}
{::db/return-keys false}))))))))

View File

@@ -27,6 +27,7 @@
[app.common.types.path :as path]
[app.common.types.shape :as cts]
[app.common.types.shape-tree :as ctst]
[app.common.types.token :as cto]
[app.common.types.tokens-lib :as ctob]
[app.common.types.typographies-list :as ctyl]
[app.common.types.typography :as ctt]
@@ -378,7 +379,7 @@
[:type [:= :set-token]]
[:set-id ::sm/uuid]
[:token-id ::sm/uuid]
[:attrs [:maybe ctob/schema:token-attrs]]]]
[:attrs [:maybe cto/schema:token-attrs]]]]
[:set-token-set
[:map {:title "SetTokenSetChange"}

View File

@@ -8,8 +8,227 @@
(:require
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.i18n :refer [tr]]
[app.common.schema :as sm]
[app.common.types.token :as cto]
[app.common.types.tokens-lib :as ctob]
[clojure.set :as set]
[cuerdas.core :as str]))
[cuerdas.core :as str]
[malli.core :as m]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; HIGH LEVEL SCHEMAS
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Token value
(defn- token-value-empty-fn
[{:keys [value]}]
(when (or (str/empty? value)
(str/blank? value))
(tr "workspace.tokens.empty-input")))
(def schema:token-value-generic
[::sm/text {:error/fn token-value-empty-fn}])
(def schema:token-value-composite-ref
[::sm/text {:error/fn token-value-empty-fn}])
(def schema:token-value-font-family
[:vector :string])
(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]
[:letter-spacing {:optional true} schema:token-value-generic]
[:paragraph-spacing {:optional true} schema:token-value-generic]
[:text-decoration {:optional true} schema:token-value-generic]
[:text-case {:optional true} schema:token-value-generic]])
(def schema:token-value-typography
[:or
schema:token-value-typography-map
schema:token-value-composite-ref])
(def schema:token-value-shadow-vector
[:vector
[:map
[:offset-x :string]
[:offset-y :string]
[:blur
[:and
:string
[:fn {:error/fn #(tr "workspace.tokens.shadow-token-blur-value-error")}
(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 [spread]
(let [n (d/parse-double spread)]
(or (nil? n) (not (< n 0)))))]]]
[:color :string]
[:inset {:optional true} :boolean]]])
(def schema:token-value-shadow
[:or
schema:token-value-shadow-vector
schema:token-value-composite-ref])
(defn make-token-value-schema
[token-type]
[:multi {:dispatch (constantly token-type)
:title "Token Value"}
[:font-family schema:token-value-font-family]
[:typography schema:token-value-typography]
[:shadow schema:token-value-shadow]
[::m/default schema:token-value-generic]])
;; Token
(defn make-token-name-schema
"Dynamically generates a schema to check a token name, adding translated error messages
and two additional validations:
- Min and max length.
- Checks if other token with a path derived from the name already exists at `tokens-tree`.
e.g. it's not allowed to create a token `foo.bar` if a token `foo` already exists."
[tokens-tree]
[:and
[:string {:min 1 :max 255 :error/fn #(str (:value %) (tr "workspace.tokens.token-name-length-validation-error"))}]
(-> cto/schema:token-name
(sm/update-properties assoc :error/fn #(str (:value %) (tr "workspace.tokens.token-name-validation-error"))))
[:fn {:error/fn #(tr "workspace.tokens.token-name-duplication-validation-error" (:value %))}
#(not (ctob/token-name-path-exists? % tokens-tree))]])
(def schema:token-description
[:string {:max 2048 :error/fn #(tr "errors.field-max-length" 2048)}])
(defn make-token-schema
[tokens-tree token-type]
[:and
(sm/merge
cto/schema:token-attrs
[:map
[:name (make-token-name-schema tokens-tree)]
[:value (make-token-value-schema token-type)]
[:description {:optional true} schema:token-description]])
[:fn {:error/field :value
:error/fn #(tr "workspace.tokens.self-reference")}
(fn [{:keys [name value]}]
(when (and name value)
(not (cto/token-value-self-reference? name value))))]])
(defn convert-dtcg-token
"Convert token attributes as they come from a decoded json, with DTCG types, to internal types.
Eg. From this:
{'name' 'body-text'
'type' 'typography'
'value' {
'fontFamilies' ['Arial' 'Helvetica' 'sans-serif']
'fontSize' '16px'
'fontWeights' 'normal'}}
to this
{:name 'body-text'
:type :typography
:value {
:font-family ['Arial' 'Helvetica' 'sans-serif']
:font-size '16px'
:font-weight 'normal'}}"
[token-attrs]
(let [name (get token-attrs "name")
type (get token-attrs "type")
value (get token-attrs "value")
description (get token-attrs "description")
type (cto/dtcg-token-type->token-type type)
value (case type
:font-family (ctob/convert-dtcg-font-family value)
:typography (ctob/convert-dtcg-typography-composite value)
:shadow (ctob/convert-dtcg-shadow-composite value)
value)]
(d/without-nils {:name name
:type type
:value value
:description description})))
;; Token set
(defn make-token-set-name-schema
"Generates a dynamic schema to check a token set name:
- Validate name length.
- Checks if other token set with a path derived from the name already exists in the tokens lib."
[tokens-lib set-id]
[:and
[:string {:min 1 :max 255 :error/fn #(str (:value %) (tr "workspace.tokens.token-name-length-validation-error"))}]
[:fn {:error/fn #(tr "errors.token-set-already-exists" (:value %))}
(fn [name]
(or (nil? tokens-lib)
(let [set (ctob/get-set-by-name tokens-lib name)]
(or (nil? set) (= (ctob/get-id set) set-id)))))]])
(def schema:token-set-description
[:string {:max 2048 :error/fn #(tr "errors.field-max-length" 2048)}])
(defn make-token-set-schema
[tokens-lib set-id]
(sm/merge
ctob/schema:token-set-attrs
[:map
[:name [:and (make-token-set-name-schema tokens-lib set-id)
[:fn #(ctob/normalized-set-name? %)]]]
[:description {:optional true} schema:token-set-description]]))
;; Token theme
(defn make-token-theme-group-schema
"Generates a dynamic schema to check a token theme group:
- Validate group length.
- Checks if other token theme with the same name already exists in the new group in the tokens lib."
[tokens-lib name theme-id]
[:and
[:string {:min 0 :max 255 :error/fn #(str (:value %) (tr "workspace.tokens.token-name-length-validation-error"))}]
[:fn {:error/fn #(tr "errors.token-theme-already-exists" (:value %))}
(fn [group]
(or (nil? tokens-lib)
(let [theme (ctob/get-theme-by-name tokens-lib group name)]
(or (nil? theme) (= (:id theme) theme-id)))))]])
(defn make-token-theme-name-schema
"Generates a dynamic schema to check a token theme name:
- Validate name length.
- Checks if other token theme with the same name already exists in the same group in the tokens lib."
[tokens-lib group theme-id]
[:and
[:string {:min 1 :max 255 :error/fn #(str (:value %) (tr "workspace.tokens.token-name-length-validation-error"))}]
[:fn {:error/fn #(tr "errors.token-theme-already-exists" (str group "/" (:value %)))}
(fn [name]
(or (nil? tokens-lib)
(let [theme (ctob/get-theme-by-name tokens-lib group name)]
(or (nil? theme) (= (:id theme) theme-id)))))]])
(def schema:token-theme-description
[:string {:max 2048 :error/fn #(tr "errors.field-max-length" 2048)}])
(defn make-token-theme-schema
[tokens-lib group name theme-id]
(sm/merge
ctob/schema:token-theme-attrs
[:map
[:group (make-token-theme-group-schema tokens-lib name theme-id)] ;; TODO how to keep error-fn from here?
[:name (make-token-theme-name-schema tokens-lib group theme-id)]
[:description {:optional true} schema:token-theme-description]]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; HELPERS
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(def parseable-token-value-regexp
"Regexp that can be used to parse a number value out of resolved token value.
@@ -39,12 +258,12 @@
(into {})))
(defn remove-attributes-for-token
"Removes applied tokens with `token-name` for the given `attributes` set from `applied-tokens`."
[attributes token-name applied-tokens]
"Removes applied tokens with `token-id` for the given `attributes` set from `applied-tokens`."
[attributes token applied-tokens]
(let [attr? (set attributes)]
(->> (remove (fn [[k v]]
(and (attr? k)
(= v token-name)))
(= v (or (token-identifier token) token))))
applied-tokens)
(into {}))))
@@ -80,56 +299,6 @@
(defn shapes-applied-all? [ids-by-attributes shape-ids attributes]
(every? #(set/superset? (get ids-by-attributes %) shape-ids) attributes))
(defn token-name->path
"Splits token-name into a path vector split by `.` characters.
Will concatenate multiple `.` characters into one."
[token-name]
(str/split token-name #"\.+"))
(defn token-name->path-selector
"Splits token-name into map with `:path` and `:selector` using `token-name->path`.
`:selector` is the last item of the names path
`:path` is everything leading up the the `:selector`."
[token-name]
(let [path-segments (token-name->path token-name)
last-idx (dec (count path-segments))
[path [selector]] (split-at last-idx path-segments)]
{:path (seq path)
:selector selector}))
(defn token-name-path-exists?
"Traverses the path from `token-name` down a `token-tree` and checks if a token at that path exists.
It's not allowed to create a token inside a token. E.g.:
Creating a token with
{:name \"foo.bar\"}
in the tokens tree:
{\"foo\" {:name \"other\"}}"
[token-name token-names-tree]
(let [{:keys [path selector]} (token-name->path-selector token-name)
path-target (reduce
(fn [acc cur]
(let [target (get acc cur)]
(cond
;; Path segment doesn't exist yet
(nil? target) (reduced false)
;; A token exists at this path
(:name target) (reduced true)
;; Continue traversing the true
:else target)))
token-names-tree path)]
(cond
(boolean? path-target) path-target
(get path-target :name) true
:else (-> (get path-target selector)
(seq)
(boolean)))))
(defn color-token? [token]
(= (:type token) :color))

View File

@@ -0,0 +1,15 @@
;; 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 app.common.i18n
"Dummy i18n functions, to be used by code in common that needs translations.")
(defn tr
"This function will be monkeypatched at runtime with the real function in frontend i18n.
Here it just returns the key passed as argument. This way the result can be used in
unit tests or backend code for logs or error messages."
[key & _args]
key)

View File

@@ -58,7 +58,7 @@
(cto/shape-attr->token-attrs attr changed-sub-attr))]
(if (some #(contains? tokens %) token-attrs)
(pcb/update-shapes changes [shape-id] #(cto/unapply-token-id % token-attrs))
(pcb/update-shapes changes [shape-id] #(cto/unapply-tokens-from-shape % token-attrs))
changes)))
check-shape

View File

@@ -11,6 +11,7 @@
#?(:clj [malli.dev.pretty :as mdp])
#?(:clj [malli.dev.virhe :as v])
[app.common.data :as d]
[app.common.json :as json]
[app.common.math :as mth]
[app.common.pprint :as pp]
[app.common.schema.generators :as sg]
@@ -92,6 +93,31 @@
[& items]
(apply mu/merge (map schema items)))
(defn assoc-key
"Add a key & value to a schema of type [:map]. If the first level node of the schema
is not a map, will do a depth search to find the first map node and add the key there."
([s k v]
(assoc-key s k {} v))
([s k opts v] ;; change order of opts and v to match static schema defintions (e.g. [:something {:optional true} ::sm/integer])
(let [s (schema s)
v (schema v)]
(if (= (m/type s) :map)
(mu/assoc s k v opts)
(if-let [path (mu/find-first s (fn [s' path _] (when (= (m/type s') :map) path)))]
(mu/assoc-in s (conj path k) v opts)
s)))))
(defn dissoc-key
"Remove a key from a schema of type [:map]. If the first level node of the schema
is not a map, will do a depth search to find the first map node and remove the key there."
[s k]
(let [s (schema s)]
(if (= (m/type s) :map)
(mu/dissoc s k)
(if-let [path (mu/find-first s (fn [s' path _] (when (= (m/type s') :map) path)))]
(mu/update-in s path mu/dissoc k)
s))))
(defn ref?
[s]
(m/-ref-schema? s))
@@ -270,6 +296,13 @@
(let [explain (fn [] (me/with-error-messages explain))]
((mdp/prettifier variant message explain default-options)))))
(defn validation-errors
"Checks a value against a schema. If valid, returns nil. If not, returns a list
of english error messages."
[value schema]
(let [explainer (explainer schema)]
(-> value explainer simplify not-empty)))
(defmacro ignoring
[expr]
(if (:ns &env)
@@ -850,6 +883,32 @@
:encode/string str
::oapi/type "boolean"}})
(defn parse-keyword
[v]
(if (string? v)
(-> v (json/read-kebab-key) (keyword))
v))
(defn format-keyword
[v]
(if (keyword? v)
(-> v (name) (json/write-camel-key))
v))
(register!
{:type ::keyword
:pred keyword?
:type-properties
{:title "keyword"
:description "keyword"
:error/message "expected keyword"
:error/code "errors.invalid-keyword"
:gen/gen sg/keyword
:decode/string parse-keyword
:decode/json parse-keyword
:encode/string format-keyword
::oapi/type "string"}})
(register!
{:type ::contains-any
:min 1

View File

@@ -9,13 +9,13 @@
[app.common.data :as d]
[app.common.schema :as sm]
[app.common.schema.generators :as sg]
[clojure.data :as data]
[app.common.time :as ct]
[clojure.set :as set]
[cuerdas.core :as str]
[malli.util :as mu]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; HELPERS
;; GENERAL HELPERS
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn- schema-keys
@@ -45,7 +45,7 @@
[token-name token-value]
(let [token-references (find-token-value-references token-value)
self-reference? (get token-references token-name)]
self-reference?))
(boolean self-reference?)))
(defn references-token?
"Recursively check if a value references the token name. Handles strings, maps, and sequences."
@@ -59,6 +59,26 @@
(some true? (map #(references-token? % token-name) value))
:else false))
(defn composite-token-reference?
"Predicate if a composite token is a reference value - a string pointing to another token."
[token-value]
(string? token-value))
(defn update-token-value-references
"Recursively update token references within a token value, supporting complex token values (maps, sequences, strings)."
[value old-name new-name]
(cond
(string? value)
(str/replace value
(re-pattern (str "\\{" (str/replace old-name "." "\\.") "\\}"))
(str "{" new-name "}"))
(map? value)
(d/update-vals value #(update-token-value-references % old-name new-name))
(sequential? value)
(mapv #(update-token-value-references % old-name new-name) value)
:else
value))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; SCHEMA
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@@ -66,7 +86,6 @@
(def token-type->dtcg-token-type
{:boolean "boolean"
:border-radius "borderRadius"
:shadow "shadow"
:color "color"
:dimensions "dimension"
:font-family "fontFamilies"
@@ -77,6 +96,7 @@
:opacity "opacity"
:other "other"
:rotation "rotation"
:shadow "shadow"
:sizing "sizing"
:spacing "spacing"
:string "string"
@@ -94,14 +114,13 @@
"boxShadow" :shadow)))
(def composite-token-type->dtcg-token-type
"Custom set of conversion keys for composite typography token with `:line-height` available.
(Penpot doesn't support `:line-height` token)"
"When converting the type of one element inside a composite token, an additional type
:line-height is available, that is not allowed for a standalone token."
(assoc token-type->dtcg-token-type
:line-height "lineHeights"))
(def composite-dtcg-token-type->token-type
"Custom set of conversion keys for composite typography token with `:line-height` available.
(Penpot doesn't support `:line-height` token)"
"Same as above, in the opposite direction."
(assoc dtcg-token-type->token-type
"lineHeights" :line-height
"lineHeight" :line-height))
@@ -109,96 +128,98 @@
(def token-types
(into #{} (keys token-type->dtcg-token-type)))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; SCHEMA: Token
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(def token-name-validation-regex
#"^[a-zA-Z0-9_-][a-zA-Z0-9$_-]*(\.[a-zA-Z0-9$_-]+)*$")
(def token-name-ref
[:re {:title "TokenNameRef" :gen/gen sg/text}
(def schema:token-name
"A token name can contains letters, numbers, underscores the character $ and dots, but
not start with $ or end with a dot. The $ character does not have any special meaning,
but dots separate token groups (e.g. color.primary.background)."
[:re {:title "TokenName"
:gen/gen sg/text}
token-name-validation-regex])
(def ^:private schema:color
[:map
[:fill {:optional true} token-name-ref]
[:stroke-color {:optional true} token-name-ref]])
(def schema:token-type
[::sm/one-of {:decode/json (fn [type]
(if (string? type)
(dtcg-token-type->token-type type)
type))}
(def color-keys (schema-keys schema:color))
token-types])
(def schema:token-attrs
[:map {:title "Token"}
[:id ::sm/uuid]
[:name schema:token-name]
[:type schema:token-type]
[:value ::sm/any]
[:description {:optional true} :string]
[:modified-at {:optional true} ::ct/inst]])
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; SCHEMA: Token application to shape
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; All the following schemas define the `:applied-tokens` attribute of a shape.
;; This attribute is a map <token-attribute> -> <token-name>.
;; Token attributes approximately match shape attributes, but not always.
;; For each schema there is a `*keys` set including all the possible token attributes
;; to which a token of the corresponding type can be applied.
;; Some token types can be applied to some attributes only if the shape has a
;; particular condition (i.e. has a layout itself or is a layout item).
(def ^:private schema:border-radius
[:map {:title "BorderRadiusTokenAttrs"}
[:r1 {:optional true} token-name-ref]
[:r2 {:optional true} token-name-ref]
[:r3 {:optional true} token-name-ref]
[:r4 {:optional true} token-name-ref]])
[:r1 {:optional true} schema:token-name]
[:r2 {:optional true} schema:token-name]
[:r3 {:optional true} schema:token-name]
[:r4 {:optional true} schema:token-name]])
(def border-radius-keys (schema-keys schema:border-radius))
(def ^:private schema:shadow
[:map {:title "ShadowTokenAttrs"}
[:shadow {:optional true} token-name-ref]])
(def shadow-keys (schema-keys schema:shadow))
(def ^:private schema:stroke-width
(def ^:private schema:color
[:map
[:stroke-width {:optional true} token-name-ref]])
[:fill {:optional true} schema:token-name]
[:stroke-color {:optional true} schema:token-name]])
(def stroke-width-keys (schema-keys schema:stroke-width))
(def color-keys (schema-keys schema:color))
(def ^:private schema:sizing-base
[:map {:title "SizingBaseTokenAttrs"}
[:width {:optional true} token-name-ref]
[:height {:optional true} token-name-ref]])
[:width {:optional true} schema:token-name]
[:height {:optional true} schema:token-name]])
(def ^:private schema:sizing-layout-item
[:map {:title "SizingLayoutItemTokenAttrs"}
[:layout-item-min-w {:optional true} token-name-ref]
[:layout-item-max-w {:optional true} token-name-ref]
[:layout-item-min-h {:optional true} token-name-ref]
[:layout-item-max-h {:optional true} token-name-ref]])
[:layout-item-min-w {:optional true} schema:token-name]
[:layout-item-max-w {:optional true} schema:token-name]
[:layout-item-min-h {:optional true} schema:token-name]
[:layout-item-max-h {:optional true} schema:token-name]])
(def sizing-layout-item-keys (schema-keys schema:sizing-layout-item))
(def ^:private schema:sizing
(-> (reduce mu/union [schema:sizing-base
schema:sizing-layout-item])
(mu/update-properties assoc :title "SizingTokenAttrs")))
(def sizing-layout-item-keys (schema-keys schema:sizing-layout-item))
(def sizing-keys (schema-keys schema:sizing))
(def ^:private schema:opacity
[:map {:title "OpacityTokenAttrs"}
[:opacity {:optional true} token-name-ref]])
(def opacity-keys (schema-keys schema:opacity))
(def ^:private schema:spacing-gap
[:map {:title "SpacingGapTokenAttrs"}
[:row-gap {:optional true} token-name-ref]
[:column-gap {:optional true} token-name-ref]])
[:row-gap {:optional true} schema:token-name]
[:column-gap {:optional true} schema:token-name]])
(def ^:private schema:spacing-padding
[:map {:title "SpacingPaddingTokenAttrs"}
[:p1 {:optional true} token-name-ref]
[:p2 {:optional true} token-name-ref]
[:p3 {:optional true} token-name-ref]
[:p4 {:optional true} token-name-ref]])
(def ^:private schema:spacing-margin
[:map {:title "SpacingMarginTokenAttrs"}
[:m1 {:optional true} token-name-ref]
[:m2 {:optional true} token-name-ref]
[:m3 {:optional true} token-name-ref]
[:m4 {:optional true} token-name-ref]])
(def ^:private schema:spacing
(-> (reduce mu/union [schema:spacing-gap
schema:spacing-padding
schema:spacing-margin])
(mu/update-properties assoc :title "SpacingTokenAttrs")))
(def spacing-margin-keys (schema-keys schema:spacing-margin))
(def spacing-keys (schema-keys schema:spacing))
[:p1 {:optional true} schema:token-name]
[:p2 {:optional true} schema:token-name]
[:p3 {:optional true} schema:token-name]
[:p4 {:optional true} schema:token-name]])
(def ^:private schema:spacing-gap-padding
(-> (reduce mu/union [schema:spacing-gap
@@ -207,6 +228,29 @@
(def spacing-gap-padding-keys (schema-keys schema:spacing-gap-padding))
(def ^:private schema:spacing-margin
[:map {:title "SpacingMarginTokenAttrs"}
[:m1 {:optional true} schema:token-name]
[:m2 {:optional true} schema:token-name]
[:m3 {:optional true} schema:token-name]
[:m4 {:optional true} schema:token-name]])
(def spacing-margin-keys (schema-keys schema:spacing-margin))
(def ^:private schema:spacing
(-> (reduce mu/union [schema:spacing-gap
schema:spacing-padding
schema:spacing-margin])
(mu/update-properties assoc :title "SpacingTokenAttrs")))
(def spacing-keys (schema-keys schema:spacing))
(def ^:private schema:stroke-width
[:map
[:stroke-width {:optional true} schema:token-name]])
(def stroke-width-keys (schema-keys schema:stroke-width))
(def ^:private schema:dimensions
(-> (reduce mu/union [schema:sizing
schema:spacing
@@ -216,91 +260,109 @@
(def dimensions-keys (schema-keys schema:dimensions))
(def ^:private schema:axis
[:map
[:x {:optional true} token-name-ref]
[:y {:optional true} token-name-ref]])
(def axis-keys (schema-keys schema:axis))
(def ^:private schema:rotation
[:map {:title "RotationTokenAttrs"}
[:rotation {:optional true} token-name-ref]])
(def rotation-keys (schema-keys schema:rotation))
(def ^:private schema:font-size
[:map {:title "FontSizeTokenAttrs"}
[:font-size {:optional true} token-name-ref]])
(def font-size-keys (schema-keys schema:font-size))
(def ^:private schema:letter-spacing
[:map {:title "LetterSpacingTokenAttrs"}
[:letter-spacing {:optional true} token-name-ref]])
(def letter-spacing-keys (schema-keys schema:letter-spacing))
(def ^:private schema:font-family
[:map
[:font-family {:optional true} token-name-ref]])
[:font-family {:optional true} schema:token-name]])
(def font-family-keys (schema-keys schema:font-family))
(def ^:private schema:text-case
[:map
[:text-case {:optional true} token-name-ref]])
(def ^:private schema:font-size
[:map {:title "FontSizeTokenAttrs"}
[:font-size {:optional true} schema:token-name]])
(def text-case-keys (schema-keys schema:text-case))
(def font-size-keys (schema-keys schema:font-size))
(def ^:private schema:font-weight
[:map
[:font-weight {:optional true} token-name-ref]])
[:font-weight {:optional true} schema:token-name]])
(def font-weight-keys (schema-keys schema:font-weight))
(def ^:private schema:typography
[:map
[:typography {:optional true} token-name-ref]])
(def ^:private schema:letter-spacing
[:map {:title "LetterSpacingTokenAttrs"}
[:letter-spacing {:optional true} schema:token-name]])
(def typography-token-keys (schema-keys schema:typography))
(def letter-spacing-keys (schema-keys schema:letter-spacing))
(def ^:private schema:text-decoration
[:map
[:text-decoration {:optional true} token-name-ref]])
(def ^:private schema:line-height ;; This is not available for standalone tokens, only typography
[:map {:title "LineHeightTokenAttrs"}
[:line-height {:optional true} schema:token-name]])
(def text-decoration-keys (schema-keys schema:text-decoration))
(def line-height-keys (schema-keys schema:line-height))
(def typography-keys (set/union font-size-keys
letter-spacing-keys
font-family-keys
font-weight-keys
text-case-keys
text-decoration-keys
font-weight-keys
typography-token-keys
#{:line-height}))
(def ^:private schema:rotation
[:map {:title "RotationTokenAttrs"}
[:rotation {:optional true} schema:token-name]])
(def rotation-keys (schema-keys schema:rotation))
(def ^:private schema:number
(-> (reduce mu/union [[:map [:line-height {:optional true} token-name-ref]]
(-> (reduce mu/union [schema:line-height
schema:rotation])
(mu/update-properties assoc :title "NumberTokenAttrs")))
(def number-keys (schema-keys schema:number))
(def all-keys (set/union color-keys
(def ^:private schema:opacity
[:map {:title "OpacityTokenAttrs"}
[:opacity {:optional true} schema:token-name]])
(def opacity-keys (schema-keys schema:opacity))
(def ^:private schema:shadow
[:map {:title "ShadowTokenAttrs"}
[:shadow {:optional true} schema:token-name]])
(def shadow-keys (schema-keys schema:shadow))
(def ^:private schema:text-case
[:map
[:text-case {:optional true} schema:token-name]])
(def text-case-keys (schema-keys schema:text-case))
(def ^:private schema:text-decoration
[:map
[:text-decoration {:optional true} schema:token-name]])
(def text-decoration-keys (schema-keys schema:text-decoration))
(def ^:private schema:typography
[:map
[:typography {:optional true} schema:token-name]])
(def typography-token-keys (schema-keys schema:typography))
(def typography-keys (set/union font-family-keys
font-size-keys
font-weight-keys
font-weight-keys
letter-spacing-keys
line-height-keys
text-case-keys
text-decoration-keys
typography-token-keys))
(def ^:private schema:axis
[:map
[:x {:optional true} schema:token-name]
[:y {:optional true} schema:token-name]])
(def axis-keys (schema-keys schema:axis))
(def all-keys (set/union axis-keys
border-radius-keys
shadow-keys
stroke-width-keys
sizing-keys
opacity-keys
spacing-keys
color-keys
dimensions-keys
axis-keys
number-keys
opacity-keys
rotation-keys
shadow-keys
sizing-keys
spacing-keys
stroke-width-keys
typography-keys
typography-token-keys
number-keys))
typography-token-keys))
(def ^:private schema:tokens
[:map {:title "GenericTokenAttrs"}])
@@ -321,11 +383,28 @@
schema:text-decoration
schema:dimensions])
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; HELPERS for conversion between token attrs and shape attrs
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn token-attr?
[attr]
(contains? all-keys attr))
(defn token-attr->shape-attr
"Returns the actual shape attribute affected when a token have been applied
to a given `token-attr`."
[token-attr]
(case token-attr
:fill :fills
:stroke-color :strokes
:stroke-width :strokes
token-attr))
(defn shape-attr->token-attrs
"Returns the token-attr affected when a given attribute in a shape is changed.
The sub-attr is for attributes that may have multiple values, like strokes
(may be width or color) and layout padding & margin (may have 4 edges)."
([shape-attr] (shape-attr->token-attrs shape-attr nil))
([shape-attr changed-sub-attr]
(cond
@@ -367,21 +446,13 @@
(number-keys shape-attr) #{shape-attr}
(axis-keys shape-attr) #{shape-attr})))
(defn token-attr->shape-attr
[token-attr]
(case token-attr
:fill :fills
:stroke-color :strokes
:stroke-width :strokes
token-attr))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; TOKEN SHAPE ATTRIBUTES
;; HELPERS for token attributes by shape type
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(def position-attributes #{:x :y})
(def ^:private position-attributes #{:x :y})
(def generic-attributes
(def ^:private generic-attributes
(set/union color-keys
stroke-width-keys
rotation-keys
@@ -390,20 +461,22 @@
shadow-keys
position-attributes))
(def rect-attributes
(def ^:private rect-attributes
(set/union generic-attributes
border-radius-keys))
(def frame-with-layout-attributes
(def ^:private frame-with-layout-attributes
(set/union rect-attributes
spacing-gap-padding-keys))
(def text-attributes
(def ^:private text-attributes
(set/union generic-attributes
typography-keys
number-keys))
(defn shape-type->attributes
"Returns what token attributes may be applied to a shape depending on its type
and if it is a frame with a layout."
[type is-layout]
(case type
:bool generic-attributes
@@ -419,12 +492,14 @@
nil))
(defn appliable-attrs-for-shape
"Returns intersection of shape `attributes` for `shape-type`."
"Returns which ones of the given `attributes` can be applied to a shape
of type `shape-type` and `is-layout`."
[attributes shape-type is-layout]
(set/intersection attributes (shape-type->attributes shape-type is-layout)))
(defn any-appliable-attr-for-shape?
"Checks if `token-type` supports given shape `attributes`."
"Returns if any of the given `attributes` can be applied to a shape
of type `shape-type` and `is-layout`."
[attributes token-type is-layout]
(d/not-empty? (appliable-attrs-for-shape attributes token-type is-layout)))
@@ -435,42 +510,6 @@
typography-keys
#{:fill}))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; TOKENS IN SHAPES
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn- toggle-or-apply-token
"Remove any shape attributes from token if they exists.
Othewise apply token attributes."
[shape token]
(let [[shape-leftover token-leftover _matching] (data/diff (:applied-tokens shape) token)]
(merge {} shape-leftover token-leftover)))
(defn- token-from-attributes [token attributes]
(->> (map (fn [attr] [attr (:name token)]) attributes)
(into {})))
(defn- apply-token-to-attributes [{:keys [shape token attributes]}]
(let [token (token-from-attributes token attributes)]
(toggle-or-apply-token shape token)))
(defn apply-token-to-shape
[{:keys [shape token attributes] :as _props}]
(let [applied-tokens (apply-token-to-attributes {:shape shape
:token token
:attributes attributes})]
(update shape :applied-tokens #(merge % applied-tokens))))
(defn unapply-token-id [shape attributes]
(update shape :applied-tokens d/without-keys attributes))
(defn unapply-layout-item-tokens
"Unapplies all layout item related tokens from shape."
[shape]
(let [layout-item-attrs (set/union sizing-layout-item-keys
spacing-margin-keys)]
(unapply-token-id shape layout-item-attrs)))
(def tokens-by-input
"A map from input name to applicable token for that input."
{:width #{:sizing :dimensions}
@@ -500,7 +539,48 @@
:stroke-color #{:color}})
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; TYPOGRAPHY
;; HELPERS for tokens application
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; TODO it seems that this function is redundant, maybe?
;; (defn- toggle-or-apply-token
;; "Remove any shape attributes from token if they exists.
;; Othewise apply token attributes."
;; [shape token]
;; (let [[only-in-shape only-in-token _matching] (data/diff (:applied-tokens shape) token)]
;; (merge {} only-in-shape only-in-token)))
(defn- generate-attr-map [token attributes]
(->> (map (fn [attr] [attr (:name token)]) attributes)
(into {})))
(defn apply-token-to-shape
"Applies the token to the given attributes in the shape."
[{:keys [shape token attributes] :as _props}]
(let [map-to-apply (generate-attr-map token attributes)]
(update shape :applied-tokens #(merge % map-to-apply))))
;; (defn apply-token-to-shape
;; [{:keys [shape token attributes] :as _props}]
;; (let [map-to-apply (generate-attr-map token attributes)
;; applied-tokens (toggle-or-apply-token shape map-to-apply)]
;; (update shape :applied-tokens #(merge % applied-tokens))))
(defn unapply-tokens-from-shape
"Removes any token applied to the given attributes in the shape."
[shape attributes]
(update shape :applied-tokens d/without-keys attributes))
(defn unapply-layout-item-tokens
"Unapplies all layout item related tokens from shape."
[shape]
(let [layout-item-attrs (set/union sizing-layout-item-keys
spacing-margin-keys)]
(unapply-tokens-from-shape shape layout-item-attrs)))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; HELPERS for typography tokens
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn split-font-family
@@ -563,32 +643,3 @@
(when (font-weight-values weight)
(cond-> {:weight weight}
italic? (assoc :style "italic")))))
(defn typography-composite-token-reference?
"Predicate if a typography composite token is a reference value - a string pointing to another reference token."
[token-value]
(string? token-value))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; SHADOW
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn shadow-composite-token-reference?
"Predicate if a shadow composite token is a reference value - a string pointing to another reference token."
[token-value]
(string? token-value))
(defn update-token-value-references
"Recursively update token references within a token value, supporting complex token values (maps, sequences, strings)."
[value old-name new-name]
(cond
(string? value)
(str/replace value
(re-pattern (str "\\{" (str/replace old-name "." "\\.") "\\}"))
(str "{" new-name "}"))
(map? value)
(d/update-vals value #(update-token-value-references % old-name new-name))
(sequential? value)
(mapv #(update-token-value-references % old-name new-name) value)
:else
value))

View File

@@ -114,25 +114,19 @@
[o]
(instance? Token o))
(def schema:token-attrs
[:map {:title "Token"}
[:id ::sm/uuid]
[:name cto/token-name-ref]
[:type [::sm/one-of cto/token-types]]
[:value ::sm/any]
[:description {:optional true} :string]
[:modified-at {:optional true} ::ct/inst]])
(declare make-token)
(def schema:token
[:and {:gen/gen (->> (sg/generator schema:token-attrs)
[:and {:gen/gen (->> (sg/generator cto/schema:token-attrs)
(sg/fmap #(make-token %)))}
(sm/required-keys schema:token-attrs)
(sm/required-keys cto/schema:token-attrs)
[:fn token?]])
(def ^:private check-token-attrs
(sm/check-fn schema:token-attrs :hint "expected valid params for token"))
(sm/check-fn cto/schema:token-attrs :hint "expected valid params for token"))
(def decode-token-attrs
(sm/lazy-decoder cto/schema:token-attrs sm/json-transformer))
(def check-token
(sm/check-fn schema:token :hint "expected valid token"))
@@ -317,10 +311,18 @@
[o]
(instance? TokenSetLegacy o))
(declare make-token-set)
(declare normalized-set-name?)
(def schema:token-set-name
[:and
:string
[:fn #(normalized-set-name? %)]]) ;; The #() is necessary because the function is only declared, not defined
(def schema:token-set-attrs
[:map {:title "TokenSet"}
[:id ::sm/uuid]
[:name :string]
[:name schema:token-set-name]
[:description {:optional true} :string]
[:modified-at {:optional true} ::ct/inst]
[:tokens {:optional true
@@ -342,8 +344,6 @@
:string schema:token]
[:fn d/ordered-map?]]]])
(declare make-token-set)
(def schema:token-set
[:schema {:gen/gen (->> (sg/generator schema:token-set-attrs)
(sg/fmap #(make-token-set %)))}
@@ -404,12 +404,25 @@
(split-set-name name))
(cpn/join-path :separator set-separator :with-spaces? false))))
(defn normalized-set-name?
"Check if a set name is normalized (no extra spaces)."
[name]
(= name (normalize-set-name name)))
(defn replace-last-path-name
"Replaces the last element in a `path` vector with `name`."
[path name]
(-> (into [] (drop-last path))
(conj name)))
(defn make-child-name
"Generate the name of a set child of `parent-set` adding the name `name`."
[parent-set name]
(if-let [parent-path (get-set-path parent-set)]
(->> (concat parent-path (split-set-name name))
(join-set-path))
(normalize-set-name name)))
;; The following functions will be removed after refactoring the internal structure of TokensLib,
;; since we'll no longer need group prefixes to differentiate between sets and set-groups.
@@ -1370,10 +1383,13 @@ Will return a value that matches this schema:
(def ^:private check-tokens-lib-map
(sm/check-fn schema:tokens-lib-map :hint "invalid tokens-lib internal data structure"))
(defn tokens-lib?
[o]
(instance? TokensLib o))
(defn valid-tokens-lib?
[o]
(and (instance? TokensLib o)
(valid? o)))
(and (tokens-lib? o) (valid? o)))
(defn- ensure-hidden-theme
"A helper that is responsible to ensure that the hidden theme always
@@ -1435,6 +1451,50 @@ Will return a value that matches this schema:
(rename copy-name)
(reid (uuid/next))))))
(defn- token-name->path-selector
"Splits token-name into map with `:path` and `:selector` using `token-name->path`.
`:selector` is the last item of the names path
`:path` is everything leading up the the `:selector`."
[token-name]
(let [path-segments (get-token-path {:name token-name})
last-idx (dec (count path-segments))
[path [selector]] (split-at last-idx path-segments)]
{:path (seq path)
:selector selector}))
(defn token-name-path-exists?
"Traverses the path from `token-name` down a `tokens-tree` and checks if a token at that path exists.
It's not allowed to create a token inside a token. E.g.:
Creating a token with
{:name \"foo.bar\"}
in the tokens tree:
{\"foo\" {:name \"other\"}}"
[token-name tokens-tree]
(let [{:keys [path selector]} (token-name->path-selector token-name)
path-target (reduce
(fn [acc cur]
(let [target (get acc cur)]
(cond
;; Path segment doesn't exist yet
(nil? target) (reduced false)
;; A token exists at this path
(:name target) (reduced true)
;; Continue traversing the true
:else target)))
tokens-tree
path)]
(cond
(boolean? path-target) path-target
(get path-target :name) true
:else (-> (get path-target selector)
(seq)
(boolean)))))
;; === Import / Export from JSON format
;; Supported formats:

View File

@@ -6,34 +6,34 @@
(ns common-tests.files.tokens-test
(:require
[app.common.files.tokens :as cft]
[app.common.files.tokens :as cfo]
[clojure.test :as t]))
(t/deftest test-parse-token-value
(t/testing "parses double from a token value"
(t/is (= {:value 100.1 :unit nil} (cft/parse-token-value "100.1")))
(t/is (= {:value -9.0 :unit nil} (cft/parse-token-value "-9"))))
(t/is (= {:value 100.1 :unit nil} (cfo/parse-token-value "100.1")))
(t/is (= {:value -9.0 :unit nil} (cfo/parse-token-value "-9"))))
(t/testing "trims white-space"
(t/is (= {:value -1.3 :unit nil} (cft/parse-token-value " -1.3 "))))
(t/is (= {:value -1.3 :unit nil} (cfo/parse-token-value " -1.3 "))))
(t/testing "parses unit: px"
(t/is (= {:value 70.3 :unit "px"} (cft/parse-token-value " 70.3px "))))
(t/is (= {:value 70.3 :unit "px"} (cfo/parse-token-value " 70.3px "))))
(t/testing "parses unit: %"
(t/is (= {:value -10.0 :unit "%"} (cft/parse-token-value "-10%"))))
(t/is (= {:value -10.0 :unit "%"} (cfo/parse-token-value "-10%"))))
(t/testing "parses unit: px")
(t/testing "returns nil for any invalid characters"
(t/is (nil? (cft/parse-token-value " -1.3a "))))
(t/is (nil? (cfo/parse-token-value " -1.3a "))))
(t/testing "doesnt accept invalid double"
(t/is (nil? (cft/parse-token-value ".3")))))
(t/is (nil? (cfo/parse-token-value ".3")))))
(t/deftest token-applied-test
(t/testing "matches passed token with `:token-attributes`"
(t/is (true? (cft/token-applied? {:name "a"} {:applied-tokens {:x "a"}} #{:x}))))
(t/is (true? (cfo/token-applied? {:name "a"} {:applied-tokens {:x "a"}} #{:x}))))
(t/testing "doesn't match empty token"
(t/is (nil? (cft/token-applied? {} {:applied-tokens {:x "a"}} #{:x}))))
(t/is (nil? (cfo/token-applied? {} {:applied-tokens {:x "a"}} #{:x}))))
(t/testing "does't match passed token `:id`"
(t/is (nil? (cft/token-applied? {:name "b"} {:applied-tokens {:x "a"}} #{:x}))))
(t/is (nil? (cfo/token-applied? {:name "b"} {:applied-tokens {:x "a"}} #{:x}))))
(t/testing "doesn't match passed `:token-attributes`"
(t/is (nil? (cft/token-applied? {:name "a"} {:applied-tokens {:x "a"}} #{:y})))))
(t/is (nil? (cfo/token-applied? {:name "a"} {:applied-tokens {:x "a"}} #{:y})))))
(t/deftest shapes-ids-by-applied-attributes
(t/testing "Returns set of matched attributes that fit the applied token"
@@ -54,7 +54,7 @@
shape-applied-x-y
shape-applied-all
shape-applied-none]
expected (cft/shapes-ids-by-applied-attributes {:name "1"} shapes attributes)]
expected (cfo/shapes-ids-by-applied-attributes {:name "1"} shapes attributes)]
(t/is (= (:x expected) (shape-ids shape-applied-x
shape-applied-x-y
shape-applied-all)))
@@ -62,34 +62,21 @@
shape-applied-x-y
shape-applied-all)))
(t/is (= (:z expected) (shape-ids shape-applied-all)))
(t/is (true? (cft/shapes-applied-all? expected (shape-ids shape-applied-all) attributes)))
(t/is (false? (cft/shapes-applied-all? expected (apply shape-ids shapes) attributes)))
(t/is (true? (cfo/shapes-applied-all? expected (shape-ids shape-applied-all) attributes)))
(t/is (false? (cfo/shapes-applied-all? expected (apply shape-ids shapes) attributes)))
(shape-ids shape-applied-x
shape-applied-x-y
shape-applied-all))))
(t/deftest tokens-applied-test
(t/testing "is true when single shape matches the token and attributes"
(t/is (true? (cft/shapes-token-applied? {:name "a"} [{:applied-tokens {:x "a"}}
(t/is (true? (cfo/shapes-token-applied? {:name "a"} [{:applied-tokens {:x "a"}}
{:applied-tokens {:x "b"}}]
#{:x}))))
(t/testing "is false when no shape matches the token or attributes"
(t/is (nil? (cft/shapes-token-applied? {:name "a"} [{:applied-tokens {:x "b"}}
(t/is (nil? (cfo/shapes-token-applied? {:name "a"} [{:applied-tokens {:x "b"}}
{:applied-tokens {:x "b"}}]
#{:x})))
(t/is (nil? (cft/shapes-token-applied? {:name "a"} [{:applied-tokens {:x "a"}}
(t/is (nil? (cfo/shapes-token-applied? {:name "a"} [{:applied-tokens {:x "a"}}
{:applied-tokens {:x "a"}}]
#{:y})))))
(t/deftest name->path-test
(t/is (= ["foo" "bar" "baz"] (cft/token-name->path "foo.bar.baz")))
(t/is (= ["foo" "bar" "baz"] (cft/token-name->path "foo..bar.baz")))
(t/is (= ["foo" "bar" "baz"] (cft/token-name->path "foo..bar.baz...."))))
(t/deftest token-name-path-exists?-test
(t/is (true? (cft/token-name-path-exists? "border-radius" {"border-radius" {"sm" {:name "sm"}}})))
(t/is (true? (cft/token-name-path-exists? "border-radius" {"border-radius" {:name "sm"}})))
(t/is (true? (cft/token-name-path-exists? "border-radius.sm" {"border-radius" {:name "sm"}})))
(t/is (true? (cft/token-name-path-exists? "border-radius.sm.x" {"border-radius" {:name "sm"}})))
(t/is (false? (cft/token-name-path-exists? "other" {"border-radius" {:name "sm"}})))
(t/is (false? (cft/token-name-path-exists? "dark.border-radius.md" {"dark" {"border-radius" {"sm" {:name "sm"}}}}))))

View File

@@ -255,28 +255,28 @@
(cls/generate-update-shapes [(:id frame1)]
(fn [shape]
(-> shape
(cto/unapply-token-id [:r1 :r2 :r3 :r4])
(cto/unapply-token-id [:rotation])
(cto/unapply-token-id [:opacity])
(cto/unapply-token-id [:stroke-width])
(cto/unapply-token-id [:stroke-color])
(cto/unapply-token-id [:fill])
(cto/unapply-token-id [:width :height])))
(cto/unapply-tokens-from-shape [:r1 :r2 :r3 :r4])
(cto/unapply-tokens-from-shape [:rotation])
(cto/unapply-tokens-from-shape [:opacity])
(cto/unapply-tokens-from-shape [:stroke-width])
(cto/unapply-tokens-from-shape [:stroke-color])
(cto/unapply-tokens-from-shape [:fill])
(cto/unapply-tokens-from-shape [:width :height])))
(:objects page)
{})
(cls/generate-update-shapes [(:id text1)]
(fn [shape]
(-> shape
(cto/unapply-token-id [:font-size])
(cto/unapply-token-id [:letter-spacing])
(cto/unapply-token-id [:font-family])))
(cto/unapply-tokens-from-shape [:font-size])
(cto/unapply-tokens-from-shape [:letter-spacing])
(cto/unapply-tokens-from-shape [:font-family])))
(:objects page)
{})
(cls/generate-update-shapes [(:id circle1)]
(fn [shape]
(-> shape
(cto/unapply-token-id [:layout-item-max-h :layout-item-min-h :layout-item-max-w :layout-item-min-w])
(cto/unapply-token-id [:m1 :m2 :m3 :m4])))
(cto/unapply-tokens-from-shape [:layout-item-max-h :layout-item-min-h :layout-item-max-w :layout-item-min-w])
(cto/unapply-tokens-from-shape [:m1 :m2 :m3 :m4])))
(:objects page)
{}))

View File

@@ -8,20 +8,19 @@
(:require
[app.common.schema :as sm]
[app.common.types.token :as cto]
[app.common.uuid :as uuid]
[clojure.test :as t]))
(t/deftest test-valid-token-name-schema
;; Allow regular namespace token names
(t/is (true? (sm/validate cto/token-name-ref "Foo")))
(t/is (true? (sm/validate cto/token-name-ref "foo")))
(t/is (true? (sm/validate cto/token-name-ref "FOO")))
(t/is (true? (sm/validate cto/token-name-ref "Foo.Bar.Baz")))
(t/is (true? (sm/validate cto/schema:token-name "Foo")))
(t/is (true? (sm/validate cto/schema:token-name "foo")))
(t/is (true? (sm/validate cto/schema:token-name "FOO")))
(t/is (true? (sm/validate cto/schema:token-name "Foo.Bar.Baz")))
;; Disallow trailing tokens
(t/is (false? (sm/validate cto/token-name-ref "Foo.Bar.Baz....")))
(t/is (false? (sm/validate cto/schema:token-name "Foo.Bar.Baz....")))
;; Disallow multiple separator dots
(t/is (false? (sm/validate cto/token-name-ref "Foo..Bar.Baz")))
(t/is (false? (sm/validate cto/schema:token-name "Foo..Bar.Baz")))
;; Disallow any special characters
(t/is (false? (sm/validate cto/token-name-ref "Hey Foo.Bar")))
(t/is (false? (sm/validate cto/token-name-ref "Hey😈Foo.Bar")))
(t/is (false? (sm/validate cto/token-name-ref "Hey%Foo.Bar"))))
(t/is (false? (sm/validate cto/schema:token-name "Hey Foo.Bar")))
(t/is (false? (sm/validate cto/schema:token-name "Hey😈Foo.Bar")))
(t/is (false? (sm/validate cto/schema:token-name "Hey%Foo.Bar"))))

View File

@@ -678,35 +678,35 @@
(t/deftest list-active-themes-tokens-bug-taiga-10617
(let [tokens-lib (-> (ctob/make-tokens-lib)
(ctob/add-set (ctob/make-token-set :name "Mode / Dark"
(ctob/add-set (ctob/make-token-set :name "Mode/Dark"
:tokens {"red"
(ctob/make-token :name "red"
:type :color
:value "#700000")}))
(ctob/add-set (ctob/make-token-set :name "Mode / Light"
(ctob/add-set (ctob/make-token-set :name "Mode/Light"
:tokens {"red"
(ctob/make-token :name "red"
:type :color
:value "#ff0000")}))
(ctob/add-set (ctob/make-token-set :name "Device / Desktop"
(ctob/add-set (ctob/make-token-set :name "Device/Desktop"
:tokens {"border1"
(ctob/make-token :name "border1"
:type :border-radius
:value 30)}))
(ctob/add-set (ctob/make-token-set :name "Device / Mobile"
(ctob/add-set (ctob/make-token-set :name "Device/Mobile"
:tokens {"border1"
(ctob/make-token :name "border1"
:type :border-radius
:value 50)}))
(ctob/add-theme (ctob/make-token-theme :group "App"
:name "Mobile"
:sets #{"Mode / Dark" "Device / Mobile"}))
:sets #{"Mode/Dark" "Device/Mobile"}))
(ctob/add-theme (ctob/make-token-theme :group "App"
:name "Web"
:sets #{"Mode / Dark" "Mode / Light" "Device / Desktop"}))
:sets #{"Mode/Dark" "Mode/Light" "Device/Desktop"}))
(ctob/add-theme (ctob/make-token-theme :group "Brand"
:name "Brand A"
:sets #{"Mode / Dark" "Mode / Light" "Device / Desktop" "Device / Mobile"}))
:sets #{"Mode/Dark" "Mode/Light" "Device/Desktop" "Device/Mobile"}))
(ctob/add-theme (ctob/make-token-theme :group "Brand"
:name "Brand B"
:sets #{}))
@@ -2013,3 +2013,11 @@
(t/is (some? imported-ref))
(t/is (= (:type original-ref) (:type imported-ref)))
(t/is (= (:value imported-ref) (:value original-ref))))))))
(t/deftest token-name-path-exists?-test
(t/is (true? (ctob/token-name-path-exists? "border-radius" {"border-radius" {"sm" {:name "sm"}}})))
(t/is (true? (ctob/token-name-path-exists? "border-radius" {"border-radius" {:name "sm"}})))
(t/is (true? (ctob/token-name-path-exists? "border-radius.sm" {"border-radius" {:name "sm"}})))
(t/is (true? (ctob/token-name-path-exists? "border-radius.sm.x" {"border-radius" {:name "sm"}})))
(t/is (false? (ctob/token-name-path-exists? "other" {"border-radius" {:name "sm"}})))
(t/is (false? (ctob/token-name-path-exists? "dark.border-radius.md" {"dark" {"border-radius" {"sm" {:name "sm"}}}}))))

View File

@@ -831,102 +831,15 @@ test.describe("Tokens: Apply token", () => {
});
await detachButton.click();
await expect(marginPillXL).not.toBeVisible();
const horizontalMarginInput = layoutItemSectionSidebar.getByText(
"Horizontal marginOpen token",
);
const horizontalMarginInput = layoutItemSectionSidebar.getByText('Horizontal marginOpen token');
await expect(horizontalMarginInput).toBeVisible();
const tokenDropdown = horizontalMarginInput.getByRole("button", {
name: "Open token list",
});
const tokenDropdown = horizontalMarginInput.getByRole('button', { name: 'Open token list' });
await tokenDropdown.click();
await expect(dimensionTokenOptionXl).toBeVisible();
await dimensionTokenOptionXl.click();
await expect(marginPillXL).toBeVisible();
});
});
test.describe("Tokens: Detach token", () => {
test("User applies border-radius token to a shape from sidebar", async ({
page,
}) => {
const { workspacePage, tokensSidebar, tokenContextMenuForToken } =
await setupTokensFile(page);
await page.getByRole("tab", { name: "Layers" }).click();
await workspacePage.layers.getByTestId("layer-row").nth(1).click();
// Open tokens sections on left sidebar
const tokensTabButton = page.getByRole("tab", { name: "Tokens" });
await tokensTabButton.click();
// Unfold border radius tokens
await page.getByRole("button", { name: "Border Radius 3" }).click();
await expect(
tokensSidebar.getByRole("button", { name: "borderRadius" }),
).toBeVisible();
await tokensSidebar.getByRole("button", { name: "borderRadius" }).click();
await expect(
tokensSidebar.getByRole("button", { name: "borderRadius.sm" }),
).toBeVisible();
// Apply border radius token from token panels
await tokensSidebar
.getByRole("button", { name: "borderRadius.sm" })
.click();
// Check if border radius sections is visible on right sidebar
const borderRadiusSection = page.getByRole("region", {
name: "border-radius-section",
});
await expect(borderRadiusSection).toBeVisible();
// Check if token pill is visible on design tab on right sidebar
const brTokenPillSM = borderRadiusSection.getByRole("button", {
name: "borderRadius.sm",
});
await expect(brTokenPillSM).toBeVisible();
await brTokenPillSM.click();
// Rename token
await tokensSidebar
.getByRole("button", { name: "borderRadius.sm" })
.click({ button: "right" });
await expect(page.getByText("Edit token")).toBeVisible();
await page.getByText("Edit token").click();
const editModal = page.getByTestId("token-update-create-modal");
await expect(editModal).toBeVisible();
await expect(
editModal.getByRole("textbox", { name: "Name" }),
).toBeVisible();
await editModal
.getByRole("textbox", { name: "Name" })
.fill("BorderRadius.smBis");
const submitButton = editModal.getByRole("button", { name: "Save" });
await expect(submitButton).toBeEnabled();
await submitButton.click();
await expect(page.getByText("Don't remap")).toBeVisible();
await page.getByText("Don't remap").click();
const brokenPill = borderRadiusSection.getByRole("button", {
name: "This token is not in any",
});
await expect(brokenPill).toBeVisible();
// Detach broken token
const detachButton = borderRadiusSection.getByRole("button", {
name: "Detach token",
});
await detachButton.click();
await expect(brokenPill).not.toBeVisible();
//De-select and select shape again to double check token is detached
await page.getByRole("tab", { name: "Layers" }).click();
await workspacePage.layers.getByTestId("layer-row").nth(0).click();
await workspacePage.layers.getByTestId("layer-row").nth(1).click();
await expect(brokenPill).not.toBeVisible();
});
});

View File

@@ -8,7 +8,7 @@
(:require
["@tokens-studio/sd-transforms" :as sd-transforms]
["style-dictionary$default" :as sd]
[app.common.files.tokens :as cft]
[app.common.files.tokens :as cfo]
[app.common.logging :as l]
[app.common.schema :as sm]
[app.common.time :as ct]
@@ -85,7 +85,7 @@
[value]
(let [number? (or (number? value)
(numeric-string? value))
parsed-value (cft/parse-token-value value)
parsed-value (cfo/parse-token-value value)
out-of-bounds (or (>= (:value parsed-value) sm/max-safe-int)
(<= (:value parsed-value) sm/min-safe-int))]
@@ -111,7 +111,7 @@
"Parses `value` of a number `sd-token` into a map like `{:value 1 :unit \"px\"}`.
If the `value` is not parseable and/or has missing references returns a map with `:errors`."
[value]
(let [parsed-value (cft/parse-token-value value)
(let [parsed-value (cfo/parse-token-value value)
out-of-bounds (or (>= (:value parsed-value) sm/max-safe-int)
(<= (:value parsed-value) sm/min-safe-int))]
(if (and parsed-value (not out-of-bounds))
@@ -129,7 +129,7 @@
If the `value` is parseable but is out of range returns a map with `warnings`."
[value]
(let [missing-references? (seq (cto/find-token-value-references value))
parsed-value (cft/parse-token-value value)
parsed-value (cfo/parse-token-value value)
out-of-scope (not (<= 0 (:value parsed-value) 1))
references (seq (cto/find-token-value-references value))]
(cond (and parsed-value (not out-of-scope))
@@ -153,7 +153,7 @@
If the `value` is parseable but is out of range returns a map with `warnings`."
[value]
(let [missing-references? (seq (cto/find-token-value-references value))
parsed-value (cft/parse-token-value value)
parsed-value (cfo/parse-token-value value)
out-of-scope (< (:value parsed-value) 0)]
(cond
(and parsed-value (not out-of-scope))
@@ -251,7 +251,7 @@
:font-size-value font-size-value})]
(or error
(try
(when-let [{:keys [unit value]} (cft/parse-token-value line-height-value)]
(when-let [{:keys [unit value]} (cfo/parse-token-value line-height-value)]
(case unit
"%" (/ value 100)
"px" (/ value font-size-value)

View File

@@ -7,7 +7,7 @@
(ns app.main.data.workspace.tokens.application
(:require
[app.common.data :as d]
[app.common.files.tokens :as cft]
[app.common.files.tokens :as cfo]
[app.common.types.component :as ctk]
[app.common.types.shape.layout :as ctsl]
[app.common.types.shape.radius :as ctsr]
@@ -648,11 +648,11 @@
shape-ids (d/nilv (keys shapes) [])
any-variant? (->> shapes vals (some ctk/is-variant?) boolean)
resolved-value (get-in resolved-tokens [(cft/token-identifier token) :resolved-value])
resolved-value (get-in resolved-tokens [(cfo/token-identifier token) :resolved-value])
resolved-value (if (contains? cf/flags :tokenscript)
(ts/tokenscript-symbols->penpot-unit resolved-value)
resolved-value)
tokenized-attributes (cft/attributes-map attributes token)
tokenized-attributes (cfo/attributes-map attributes token)
type (:type token)]
(rx/concat
(rx/of
@@ -706,18 +706,17 @@
"Removes `attributes` that match `token` for `shape-ids`.
Doesn't update shape attributes."
[{:keys [attributes token-name shape-ids] :as _props}]
[{:keys [attributes token shape-ids] :as _props}]
(ptk/reify ::unapply-token
ptk/WatchEvent
(watch [_ _ _]
(rx/of
(let [remove-token #(when % (cft/remove-attributes-for-token attributes token-name %))]
(let [remove-token #(when % (cfo/remove-attributes-for-token attributes token %))]
(dwsh/update-shapes
shape-ids
(fn [shape]
(update shape :applied-tokens remove-token))))))))
(defn toggle-token
[{:keys [token attrs shape-ids expand-with-children]}]
(ptk/reify ::on-toggle-token
@@ -740,14 +739,14 @@
(get token-properties (:type token))
unapply-tokens?
(cft/shapes-token-applied? token shapes (or attrs all-attributes attributes))
(cfo/shapes-token-applied? token shapes (or attrs all-attributes attributes))
shape-ids (map :id shapes)]
(if unapply-tokens?
(rx/of
(unapply-token {:attributes (or attrs all-attributes attributes)
:token-name (:name token)
:token token
:shape-ids shape-ids}))
(rx/of
(cond
@@ -763,6 +762,43 @@
:shape-ids shape-ids
:on-update-shape on-update-shape}))))))))
(defn toggle-border-radius-token
[{:keys [token attrs shape-ids expand-with-children]}]
(ptk/reify ::on-toggle-border-radius-token
ptk/WatchEvent
(watch [_ state _]
(let [objects (dsh/lookup-page-objects state)
shapes (into [] (keep (d/getf objects)) shape-ids)
shapes
(if expand-with-children
(into []
(mapcat (fn [shape]
(if (= (:type shape) :group)
(keep objects (:shapes shape))
[shape])))
shapes)
shapes)
{:keys [attributes all-attributes]}
(get token-properties (:type token))
unapply-tokens?
(cfo/shapes-token-applied? token shapes (or attrs all-attributes attributes))
shape-ids (map :id shapes)]
(if unapply-tokens?
(rx/of
(unapply-token {:attributes (or attrs all-attributes attributes)
:token token
:shape-ids shape-ids}))
(rx/of
(apply-token {:attributes attrs
:token token
:shape-ids shape-ids
:on-update-shape update-shape-radius-for-corners})))))))
(defn apply-token-on-selected
[color-operations token]
(ptk/reify ::apply-token-on-selected

View File

@@ -6,7 +6,7 @@
(ns app.main.data.workspace.tokens.color
(:require
[app.common.files.tokens :as cft]
[app.common.files.tokens :as cfo]
[app.config :as cf]
[app.main.data.tinycolor :as tinycolor]
[app.main.data.tokenscript :as ts]))
@@ -22,5 +22,5 @@
(if (contains? cf/flags :tokenscript)
(when (and resolved-value (ts/color-symbol? resolved-value))
(ts/color-symbol->penpot-color resolved-value))
(when (and resolved-value (cft/color-token? token))
(when (and resolved-value (cfo/color-token? token))
(color-bullet-color resolved-value))))

View File

@@ -195,27 +195,30 @@
(defn create-token-set
[token-set]
(assert (ctob/token-set? token-set) "a token set is required") ;; TODO should check token-set-schema?
(ptk/reify ::create-token-set
ptk/UpdateEvent
(update [_ state]
;; Clear possible local state
(update state :workspace-tokens dissoc :token-set-new-path))
ptk/WatchEvent
(watch [it state _]
(let [data (dsh/lookup-file-data state)
tokens-lib (get data :tokens-lib)
token-set (ctob/rename token-set (ctob/normalize-set-name (ctob/get-name token-set)))]
(if (and tokens-lib (ctob/get-set-by-name tokens-lib (ctob/get-name token-set)))
(rx/of (ntf/show {:content (tr "errors.token-set-already-exists")
:type :toast
:level :error
:timeout 9000}))
(let [changes (-> (pcb/empty-changes it)
(pcb/with-library-data data)
(pcb/set-token-set (ctob/get-id token-set) token-set))]
(rx/of (set-selected-token-set-id (ctob/get-id token-set))
(dch/commit-changes changes))))))))
(let [data (dsh/lookup-file-data state)
changes (-> (pcb/empty-changes it)
(pcb/with-library-data data)
(pcb/set-token-set (ctob/get-id token-set) token-set))]
(rx/of (set-selected-token-set-id (ctob/get-id token-set))
(dch/commit-changes changes))))))
(defn rename-token-set
[token-set new-name]
(assert (ctob/token-set? token-set) "a token set is required") ;; TODO should check token-set-schema after renaming?
(assert (string? new-name) "a new name is required") ;; TODO should assert normalized-set-name?
(ptk/reify ::update-token-set
ptk/WatchEvent
(watch [it state _]
(let [data (dsh/lookup-file-data state)
changes (-> (pcb/empty-changes it)
(pcb/with-library-data data)
(pcb/rename-token-set (ctob/get-id token-set) new-name))]
(rx/of (set-selected-token-set-id (ctob/get-id token-set))
(dch/commit-changes changes))))))
(defn rename-token-set-group
[set-group-path set-group-fname]
@@ -227,26 +230,6 @@
(rx/of
(dch/commit-changes changes))))))
(defn update-token-set
[token-set name]
(ptk/reify ::update-token-set
ptk/WatchEvent
(watch [it state _]
(let [data (dsh/lookup-file-data state)
name (ctob/normalize-set-name name (ctob/get-name token-set))
tokens-lib (get data :tokens-lib)]
(if (ctob/get-set-by-name tokens-lib name)
(rx/of (ntf/show {:content (tr "errors.token-set-already-exists")
:type :toast
:level :error
:timeout 9000}))
(let [changes (-> (pcb/empty-changes it)
(pcb/with-library-data data)
(pcb/rename-token-set (ctob/get-id token-set) name))]
(rx/of (set-selected-token-set-id (ctob/get-id token-set))
(dch/commit-changes changes))))))))
(defn duplicate-token-set
[id]
(ptk/reify ::duplicate-token-set
@@ -522,7 +505,7 @@
(ctob/get-id token-set)
token-id)]
(let [tokens (vals (ctob/get-tokens tokens-lib (ctob/get-id token-set)))
unames (map :name tokens)
unames (map :name tokens) ;; TODO: add function duplicate-token in tokens-lib
suffix (tr "workspace.tokens.duplicate-suffix")
copy-name (cfh/generate-unique-name (:name token) unames :suffix suffix)
new-token (-> token

View File

@@ -183,6 +183,9 @@
[id]
(l/derived #(contains? % id) selected-shapes))
(def highlighted-shapes
(l/derived :highlighted workspace-local))
(def export-in-progress?
(l/derived :export-in-progress? export))

View File

@@ -564,17 +564,18 @@
(mf/use-fn
(mf/deps on-detach tokens disabled token-applied)
(fn [event]
(when-not disabled
(dom/prevent-default event)
(dom/stop-propagation event)
(reset! token-applied* nil)
(reset! selected-id* nil)
(reset! focused-id* nil)
(when on-detach
(on-detach token-applied))
(ts/schedule-on-idle
(fn []
(dom/focus! (mf/ref-val ref)))))))
(let [token (get-token-op tokens token-applied)]
(when-not disabled
(dom/prevent-default event)
(dom/stop-propagation event)
(reset! token-applied* nil)
(reset! selected-id* nil)
(reset! focused-id* nil)
(when on-detach
(on-detach token))
(ts/schedule-on-idle
(fn []
(dom/focus! (mf/ref-val ref))))))))
on-token-key-down
(mf/use-fn

View File

@@ -49,7 +49,6 @@
(mf/defc form-submit*
[{:keys [disabled on-submit] :rest props}]
(let [form (mf/use-ctx context)
disabled? (or (and (some? form)
(or (not (:valid @form))

View File

@@ -12,7 +12,7 @@
[app.common.types.component :as ctk]
[app.main.data.viewer :as dv]
[app.main.store :as st]
[app.main.ui.workspace.sidebar.layer-item :refer [layer-item-inner*]]
[app.main.ui.workspace.sidebar.layer-item :refer [layer-item-inner]]
[app.util.dom :as dom]
[app.util.keyboard :as kbd]
[okulary.core :as l]
@@ -26,6 +26,7 @@
(mf/defc layer-item
[{:keys [item selected objects depth component-child? hide-toggle?] :as props}]
(let [id (:id item)
hidden? (:hidden item)
selected? (contains? selected id)
item-ref (mf/use-ref nil)
depth (+ depth 1)
@@ -67,17 +68,18 @@
(when (and (= (count selected) 1) selected?)
(dom/scroll-into-view-if-needed! (mf/ref-val item-ref) true))))
[:> layer-item-inner*
[:& layer-item-inner
{:ref item-ref
:item item
:depth depth
:is-read-only true
:is-highlighted false
:is-selected selected?
:is-component-tree component-tree?
:is-filtered false
:is-expanded expanded?
:hide-toggle hide-toggle?
:read-only? true
:highlighted? false
:selected? selected?
:component-tree? component-tree?
:hidden? hidden?
:filtered? false
:expanded? expanded?
:hide-toggle? hide-toggle?
:on-select-shape select-shape
:on-toggle-collapse toggle-collapse}

View File

@@ -10,13 +10,11 @@
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.files.helpers :as cfh]
[app.common.math :as mth]
[app.common.types.component :as ctk]
[app.common.types.components-list :as ctkl]
[app.common.types.container :as ctn]
[app.common.types.shape.layout :as ctl]
[app.common.uuid :as uuid]
[app.common.weak :as weak]
[app.main.data.workspace :as dw]
[app.main.data.workspace.collapse :as dwc]
[app.main.refs :as refs]
@@ -39,8 +37,6 @@
(defonce ^:private sidebar-hover-queue (atom {:enter #{} :leave #{}}))
(defonce ^:private sidebar-hover-pending? (atom false))
(def ^:const default-chunk-size 50)
(defn- schedule-sidebar-hover-flush []
(when (compare-and-set! sidebar-hover-pending? false true)
(ts/raf
@@ -52,11 +48,12 @@
(when (seq enter)
(apply st/emit! (map dw/highlight-shape enter))))))))
(mf/defc layer-item-inner*
[{:keys [item depth parent-size name-ref children ref style rename-id
(mf/defc layer-item-inner
{::mf/wrap-props false}
[{:keys [item depth parent-size name-ref children ref style
;; Flags
is-read-only is-highlighted is-selected is-component-tree
is-filtered is-expanded dnd-over dnd-over-top dnd-over-bot hide-toggle
read-only? highlighted? selected? component-tree?
filtered? expanded? dnd-over? dnd-over-top? dnd-over-bot? hide-toggle?
;; Callbacks
on-select-shape on-context-menu on-pointer-enter on-pointer-leave on-zoom-to-selected
on-toggle-collapse on-enable-drag on-disable-drag on-toggle-visibility on-toggle-blocking]}]
@@ -67,7 +64,7 @@
hidden? (:hidden item)
has-shapes? (-> item :shapes seq boolean)
touched? (-> item :touched seq boolean)
root-board? (and (cfh/frame-shape? item)
parent-board? (and (cfh/frame-shape? item)
(= uuid/zero (:parent-id item)))
absolute? (ctl/item-absolute? item)
is-variant? (ctk/is-variant? item)
@@ -76,12 +73,9 @@
variant-name (when is-variant? (:variant-name item))
variant-error (when is-variant? (:variant-error item))
component-id (get item :component-id)
variant-properties (mf/with-memo [component-id]
;; We use non-reactive deref explicitly here
(let [data (deref refs/workspace-data)]
(-> (ctkl/get-component data component-id)
(get :variant-properties))))
data (deref refs/workspace-data)
component (ctkl/get-component data (:component-id item))
variant-properties (:variant-properties component)
icon-shape (usi/get-shape-icon item)]
[:*
@@ -92,27 +86,27 @@
:data-testid "layer-row"
:class (stl/css-case
:layer-row true
:highlight is-highlighted
:highlight highlighted?
:component (ctk/instance-head? item)
:masked (:masked-group item)
:selected is-selected
:selected selected?
:type-frame (cfh/frame-shape? item)
:type-bool (cfh/bool-shape? item)
:type-comp (or is-component-tree is-variant-container?)
:type-comp (or component-tree? is-variant-container?)
:hidden hidden?
:dnd-over dnd-over
:dnd-over-top dnd-over-top
:dnd-over-bot dnd-over-bot
:root-board root-board?)
:dnd-over dnd-over?
:dnd-over-top dnd-over-top?
:dnd-over-bot dnd-over-bot?
:root-board parent-board?)
:style style}
[:span {:class (stl/css-case
:tab-indentation true
:filtered is-filtered)
:filtered filtered?)
:style {"--depth" depth}}]
[:div {:class (stl/css-case
:element-list-body true
:filtered is-filtered
:selected is-selected
:filtered filtered?
:selected selected?
:icon-layer (= (:type item) :icon))
:style {"--depth" depth}
:on-pointer-enter on-pointer-enter
@@ -121,12 +115,12 @@
(if (< 0 (count (:shapes item)))
[:div {:class (stl/css :button-content)}
(when (and (not hide-toggle) (not is-filtered))
(when (and (not hide-toggle?) (not filtered?))
[:button {:class (stl/css-case
:toggle-content true
:inverse is-expanded)
:inverse expanded?)
:data-testid "toggle-content"
:aria-expanded is-expanded
:aria-expanded expanded?
:on-click on-toggle-collapse}
deprecated-icon/arrow])
@@ -137,7 +131,7 @@
[:> icon* {:icon-id icon-shape :size "s" :data-testid (str "icon-" icon-shape)}]]]
[:div {:class (stl/css :button-content)}
(when (not ^boolean is-filtered)
(when (not ^boolean filtered?)
[:span {:class (stl/css :toggle-content)}])
[:div {:class (stl/css :icon-shape)
:on-double-click on-zoom-to-selected}
@@ -146,26 +140,25 @@
[:> icon* {:icon-id icon-shape :size "s" :data-testid (str "icon-" icon-shape)}]]])
[:> layer-name* {:ref name-ref
:rename-id rename-id
:shape-id id
:shape-name name
:is-shape-touched touched?
:disabled-double-click is-read-only
:disabled-double-click read-only?
:on-start-edit on-disable-drag
:on-stop-edit on-enable-drag
:depth depth
:is-blocked blocked?
:parent-size parent-size
:is-selected is-selected
:type-comp (or is-component-tree is-variant-container?)
:is-selected selected?
:type-comp (or component-tree? is-variant-container?)
:type-frame (cfh/frame-shape? item)
:variant-id variant-id
:variant-name variant-name
:variant-properties variant-properties
:variant-error variant-error
:component-id component-id
:component-id (:id component)
:is-hidden hidden?}]]
(when (not ^boolean is-read-only)
(when (not read-only?)
[:div {:class (stl/css-case
:element-actions true
:is-parent has-shapes?
@@ -190,87 +183,41 @@
children]))
(mf/defc layer-item*
{::mf/wrap [mf/memo]}
[{:keys [index item selected objects objects-ref rename-id
is-sortable is-filtered depth is-component-child
highlighted style render-children parent-size]
:or {render-children true}}]
(let [id (get item :id)
blocked? (get item :blocked)
hidden? (get item :hidden)
shapes (get item :shapes)
shapes (mf/with-memo [shapes]
(loop [counter 0
shapes (seq shapes)
result (list)]
(if-let [id (first shapes)]
(if-let [obj (-> (mf/ref-val objects-ref)
(get id))]
(do
;; NOTE: this is a bit hacky, but reduces substantially
;; the allocation; If we use enumeration, we allocate
;; new sequence and add one iteration on each render,
;; independently if objects are changed or not. If we
;; store counter on metadata, we still need to create a
;; new allocation for each shape; with this method we
;; bypass this by mutating a private property on the
;; object removing extra allocation and extra iteration
;; on every request.
(unchecked-set obj "__$__counter" counter)
(recur (inc counter)
(rest shapes)
(conj result obj)))
(recur (inc counter)
(rest shapes)
result))
(-> result vec not-empty))))
;; Memoized for performance
(mf/defc layer-item
{::mf/props :obj
::mf/wrap [mf/memo]}
[{:keys [index item selected objects sortable? filtered? depth parent-size component-child? highlighted style render-children?]
:or {render-children? true}}]
(let [id (:id item)
blocked? (:blocked item)
hidden? (:hidden item)
drag-disabled* (mf/use-state false)
drag-disabled? (deref drag-disabled*)
scroll-middle-ref (mf/use-ref true)
scroll-to-middle? (mf/use-var true)
expanded-iref (mf/with-memo [id]
(l/derived #(dm/get-in % [:expanded id]) refs/workspace-local))
is-expanded (mf/deref expanded-iref)
(-> (l/in [:expanded id])
(l/derived refs/workspace-local)))
expanded? (mf/deref expanded-iref)
is-selected (contains? selected id)
is-highlighted (contains? highlighted id)
selected? (contains? selected id)
highlighted? (contains? highlighted id)
container? (or (cfh/frame-shape? item)
(cfh/group-shape? item))
is-read-only (mf/use-ctx ctx/workspace-read-only?)
root-board? (and (cfh/frame-shape? item)
read-only? (mf/use-ctx ctx/workspace-read-only?)
parent-board? (and (cfh/frame-shape? item)
(= uuid/zero (:parent-id item)))
name-node-ref (mf/use-ref)
depth (+ depth 1)
is-component-tree (or ^boolean is-component-child
^boolean (ctk/instance-root? item)
^boolean (ctk/instance-head? item))
enable-drag (mf/use-fn #(reset! drag-disabled* false))
disable-drag (mf/use-fn #(reset! drag-disabled* true))
;; Lazy loading of child elements via IntersectionObserver
children-count* (mf/use-state 0)
children-count (deref children-count*)
lazy-ref (mf/use-ref nil)
observer-ref (mf/use-ref nil)
toggle-collapse
(mf/use-fn
(mf/deps is-expanded)
(mf/deps expanded?)
(fn [event]
(dom/stop-propagation event)
(if (and is-expanded (kbd/shift? event))
(if (and expanded? (kbd/shift? event))
(st/emit! (dwc/collapse-all))
(st/emit! (dwc/toggle-collapse id)))))
@@ -295,16 +242,15 @@
select-shape
(mf/use-fn
(mf/deps id is-filtered)
(mf/deps id filtered? objects)
(fn [event]
(dom/prevent-default event)
(mf/set-ref-val! scroll-middle-ref false)
(reset! scroll-to-middle? false)
(cond
(kbd/shift? event)
(let [objects (mf/ref-val objects-ref)]
(if is-filtered
(st/emit! (dw/shift-select-shapes id objects))
(st/emit! (dw/shift-select-shapes id))))
(if filtered?
(st/emit! (dw/shift-select-shapes id objects))
(st/emit! (dw/shift-select-shapes id)))
(kbd/mod? event)
(st/emit! (dw/select-shape id true))
@@ -337,11 +283,11 @@
on-context-menu
(mf/use-fn
(mf/deps item is-read-only)
(mf/deps item read-only?)
(fn [event]
(dom/prevent-default event)
(dom/stop-propagation event)
(when-not is-read-only
(when-not read-only?
(let [pos (dom/get-client-position event)]
(st/emit! (dw/show-shape-context-menu {:position pos :shape item}))))))
@@ -354,10 +300,9 @@
on-drop
(mf/use-fn
(mf/deps id index is-expanded selected)
(mf/deps id index objects expanded? selected)
(fn [side _data]
(let [objects (mf/ref-val objects-ref)
single? (= (count selected) 1)
(let [single? (= (count selected) 1)
same? (and single? (= (first selected) id))]
(when-not same?
(let [files (deref refs/files)
@@ -368,32 +313,28 @@
(= side :center)
id
(and is-expanded (= side :bot) (d/not-empty? (:shapes shape)))
(and expanded? (= side :bot) (d/not-empty? (:shapes shape)))
id
:else
(cfh/get-parent-id objects id))
[parent-id _]
(ctn/find-valid-parent-and-frame-ids parent-id objects (map #(get objects %) selected) false files)
[parent-id _] (ctn/find-valid-parent-and-frame-ids parent-id objects (map #(get objects %) selected) false files)
parent
(get objects parent-id)
to-index
(cond
(= side :center) 0
(and is-expanded (= side :bot) (d/not-empty? (:shapes shape))) (count (:shapes parent))
(= side :top) (inc index)
:else index)]
parent (get objects parent-id)
to-index (cond
(= side :center) 0
(and expanded? (= side :bot) (d/not-empty? (:shapes shape))) (count (:shapes parent))
(= side :top) (inc index)
:else index)]
(st/emit! (dw/relocate-selected-shapes parent-id to-index)))))))
on-hold
(mf/use-fn
(mf/deps id is-expanded)
(mf/deps id expanded?)
(fn []
(when-not is-expanded
(when-not expanded?
(st/emit! (dwc/toggle-collapse id)))))
zoom-to-selected
@@ -414,114 +355,116 @@
:data {:id (:id item)
:index index
:name (:name item)}
;; We don't want to change the structure of component copies
:draggable? (and ^boolean is-sortable
^boolean (not is-read-only)
^boolean (not (ctn/has-any-copy-parent? objects item))))]
:draggable? (and
sortable?
(not read-only?)
(not (ctn/has-any-copy-parent? objects item)))) ;; We don't want to change the structure of component copies
(mf/with-effect [is-selected selected]
ref (mf/use-ref)
depth (+ depth 1)
component-tree? (or component-child? (ctk/instance-root? item) (ctk/instance-head? item))
enable-drag (mf/use-fn #(reset! drag-disabled* false))
disable-drag (mf/use-fn #(reset! drag-disabled* true))
;; Lazy loading of child elements via IntersectionObserver
children-count* (mf/use-state 0)
children-count (deref children-count*)
lazy-ref (mf/use-ref nil)
observer-var (mf/use-var nil)
chunk-size 50]
(mf/with-effect [selected? selected]
(let [single? (= (count selected) 1)
node (mf/ref-val name-node-ref)
scroll-node (dom/get-parent-with-data node "scroll-container")
parent-node (dom/get-parent-at node 2)
first-child-node (dom/get-first-child parent-node)
scroll-to-middle? (mf/ref-val scroll-middle-ref)
node (mf/ref-val ref)
scroll-node (dom/get-parent-with-data node "scroll-container")
parent-node (dom/get-parent-at node 2)
first-child-node (dom/get-first-child parent-node)
subid
(when (and ^boolean single?
^boolean is-selected
^boolean scroll-to-middle?)
(when (and single? selected? @scroll-to-middle?)
(ts/schedule
100
#(when (and node scroll-node)
(let [scroll-distance-ratio (dom/get-scroll-distance-ratio node scroll-node)
scroll-behavior (if (> scroll-distance-ratio 1) "instant" "smooth")]
(dom/scroll-into-view-if-needed! first-child-node #js {:block "center" :behavior scroll-behavior :inline "start"})
(mf/set-ref-val! scroll-middle-ref true)))))]
(reset! scroll-to-middle? true)))))]
#(when (some? subid)
(rx/dispose! subid))))
;; Setup scroll-driven lazy loading when expanded
;; and ensures selected children are loaded immediately
(mf/with-effect [is-expanded shapes selected]
(let [total (count shapes)]
(if ^boolean is-expanded
(mf/with-effect [expanded? (:shapes item) selected]
(let [shapes-vec (:shapes item)
total (count shapes-vec)]
(if expanded?
(let [;; Children are rendered in reverse order, so index 0 in render = last in shapes-vec
;; Find if any selected id is a direct child and get its render index
selected-child-render-idx
(when (> total default-chunk-size)
(some (fn [sel-id]
(let [idx (.indexOf shapes sel-id)]
(when (>= idx 0) idx)))
selected))
(when (and (> total chunk-size) (seq selected))
(let [shapes-reversed (vec (reverse shapes-vec))]
(some (fn [sel-id]
(let [idx (.indexOf shapes-reversed sel-id)]
(when (>= idx 0) idx)))
selected)))
;; Load at least enough to include the selected child plus extra
;; for context (so it can be centered in the scroll view)
min-count
(if selected-child-render-idx
(+ selected-child-render-idx default-chunk-size)
default-chunk-size)
current-count
@children-count*
new-count
(mth/min total (mth/max current-count default-chunk-size min-count))]
min-count (if selected-child-render-idx
(+ selected-child-render-idx chunk-size)
chunk-size)
current @children-count*
new-count (min total (max current chunk-size min-count))]
(reset! children-count* new-count))
(reset! children-count* 0))
(fn []
(when-let [obs (mf/ref-val observer-ref)]
(.disconnect obs)
(mf/set-ref-val! obs nil)))))
(reset! children-count* 0)))
(fn []
(when-let [obs ^js @observer-var]
(.disconnect obs)
(reset! observer-var nil))))
;; Re-observe sentinel whenever children-count changes (sentinel moves)
;; and (shapes item) to reconnect observer after shape changes
(mf/with-effect [children-count is-expanded shapes]
(let [total (count shapes)
name-node (mf/ref-val name-node-ref)
scroll-node (dom/get-parent-with-data name-node "scroll-container")
lazy-node (mf/ref-val lazy-ref)]
(mf/with-effect [children-count expanded? (:shapes item)]
(let [total (count (:shapes item))
node (mf/ref-val ref)
scroll-node (dom/get-parent-with-data node "scroll-container")
lazy-node (mf/ref-val lazy-ref)]
;; Disconnect previous observer
(when-let [obs (mf/ref-val observer-ref)]
(when-let [obs ^js @observer-var]
(.disconnect obs)
(mf/set-ref-val! observer-ref nil))
(reset! observer-var nil))
;; Setup new observer if there are more children to load
(when (and ^boolean is-expanded
^boolean (< children-count total)
^boolean scroll-node
^boolean lazy-node)
(when (and expanded?
(< children-count total)
scroll-node
lazy-node)
(let [cb (fn [entries]
(when (and (pos? (alength entries))
(.-isIntersecting ^js (aget entries 0)))
(when (and (seq entries)
(.-isIntersecting (first entries)))
;; Load next chunk when sentinel intersects
(let [next-count (mth/min total (+ children-count default-chunk-size))]
(let [current @children-count*
next-count (min total (+ current chunk-size))]
(reset! children-count* next-count))))
observer (js/IntersectionObserver. cb #js {:root scroll-node})]
(.observe observer lazy-node)
(mf/set-ref-val! observer-ref observer)))))
(reset! observer-var observer)))))
[:> layer-item-inner*
[:& layer-item-inner
{:ref dref
:item item
:depth depth
:parent-size parent-size
:name-ref name-node-ref
:rename-id rename-id
:is-read-only is-read-only
:is-highlighted is-highlighted
:is-selected is-selected
:is-component-tree is-component-tree
:is-filtered is-filtered
:is-expanded is-expanded
:dnd-over (= (:over dprops) :center)
:dnd-over-top (= (:over dprops) :top)
:dnd-over-bot (= (:over dprops) :bot)
:name-ref ref
:read-only? read-only?
:highlighted? highlighted?
:selected? selected?
:component-tree? component-tree?
:filtered? filtered?
:expanded? expanded?
:dnd-over? (= (:over dprops) :center)
:dnd-over-top? (= (:over dprops) :top)
:dnd-over-bot? (= (:over dprops) :bot)
:on-select-shape select-shape
:on-context-menu on-context-menu
:on-pointer-enter on-pointer-enter
@@ -534,29 +477,29 @@
:on-toggle-blocking toggle-blocking
:style style}
(when (and ^boolean render-children
^boolean shapes
^boolean is-expanded)
(when (and render-children?
(:shapes item)
expanded?)
[:div {:class (stl/css-case
:element-children true
:parent-selected is-selected
:sticky-children root-board?)
:parent-selected selected?
:sticky-children parent-board?)
:data-testid (dm/str "children-" id)}
(for [item (take children-count shapes)]
[:> layer-item*
{:item item
:rename-id rename-id
:highlighted highlighted
:selected selected
:index (unchecked-get item "__$__counter")
:objects objects
:objects-ref objects-ref
:key (weak/weak-key item)
:is-sortable is-sortable
:depth depth
:parent-size parent-size
:is-component-child is-component-tree}])
(when (< children-count (count shapes))
(let [all-children (reverse (d/enumerate (:shapes item)))
visible (take children-count all-children)]
(for [[index id] visible]
(when-let [item (get objects id)]
[:& layer-item
{:item item
:highlighted highlighted
:selected selected
:index index
:objects objects
:key (dm/str id)
:sortable? sortable?
:depth depth
:parent-size parent-size
:component-child? component-tree?}])))
(when (< children-count (count (:shapes item)))
[:div {:ref lazy-ref
:style {:min-height 1}}])])]))

View File

@@ -16,35 +16,39 @@
[app.util.dom :as dom]
[app.util.keyboard :as kbd]
[cuerdas.core :as str]
[okulary.core :as l]
[rumext.v2 :as mf]))
(def ^:private ^:const space-for-icons 110)
(def ^:private space-for-icons 110)
(def lens:shape-for-rename
(-> (l/in [:workspace-local :shape-for-rename])
(l/derived st/state)))
(mf/defc layer-name*
[{:keys [shape-id rename-id shape-name is-shape-touched disabled-double-click
{::mf/wrap-props false
::mf/forward-ref true}
[{:keys [shape-id shape-name is-shape-touched disabled-double-click
on-start-edit on-stop-edit depth parent-size is-selected
type-comp type-frame component-id is-hidden is-blocked
variant-id variant-name variant-properties variant-error ref]}]
variant-id variant-name variant-properties variant-error]} external-ref]
(let [edition* (mf/use-state false)
edition? (deref edition*)
local-ref (mf/use-ref)
ref (d/nilv ref local-ref)
ref (d/nilv external-ref local-ref)
shape-name
(if variant-id
(d/nilv variant-error variant-name)
shape-name)
shape-for-rename (mf/deref lens:shape-for-rename)
default-value
(mf/with-memo [variant-id variant-error variant-properties]
(if variant-id
(or variant-error (ctv/properties-map->formula variant-properties))
shape-name))
shape-name (if variant-id
(d/nilv variant-error variant-name)
shape-name)
has-path?
(str/includes? shape-name "/")
default-value (if variant-id
(or variant-error (ctv/properties-map->formula variant-properties))
shape-name)
has-path? (str/includes? shape-name "/")
start-edit
(mf/use-fn
@@ -81,11 +85,10 @@
(when (kbd/enter? event) (accept-edit))
(when (kbd/esc? event) (cancel-edit))))
parent-size
(dm/str (- parent-size space-for-icons) "px")]
parent-size (dm/str (- parent-size space-for-icons) "px")]
(mf/with-effect [rename-id edition? start-edit shape-id]
(when (and (= rename-id shape-id)
(mf/with-effect [shape-for-rename edition? start-edit shape-id]
(when (and (= shape-for-rename shape-id)
(not ^boolean edition?))
(start-edit)))
@@ -107,24 +110,21 @@
:auto-focus true
:id (dm/str "layer-name-" shape-id)
:default-value (d/nilv default-value "")}]
[:*
[:span {:class (stl/css-case
:element-name true
:left-ellipsis has-path?
:selected is-selected
:hidden is-hidden
:type-comp type-comp
:type-frame type-frame)
:id (dm/str "layer-name-" shape-id)
:style {"--depth" depth "--parent-size" parent-size}
:ref ref
:on-double-click start-edit}
(if ^boolean (dbg/enabled? :show-ids)
(dm/str (d/nilv shape-name "") " | " (str/slice (str shape-id) 24))
[:span
{:class (stl/css-case
:element-name true
:left-ellipsis has-path?
:selected is-selected
:hidden is-hidden
:type-comp type-comp
:type-frame type-frame)
:id (dm/str "layer-name-" shape-id)
:style {"--depth" depth "--parent-size" parent-size}
:ref ref
:on-double-click start-edit}
(if (dbg/enabled? :show-ids)
(str (d/nilv shape-name "") " | " (str/slice (str shape-id) 24))
(d/nilv shape-name ""))]
(when (and ^boolean (dbg/enabled? :show-touched)
^boolean is-shape-touched)
(when (and (dbg/enabled? :show-touched) ^boolean is-shape-touched)
[:span {:class (stl/css :element-name-touched)} "*"])])))

View File

@@ -12,7 +12,6 @@
[app.common.files.helpers :as cfh]
[app.common.types.shape :as cts]
[app.common.uuid :as uuid]
[app.common.weak :as weak]
[app.main.data.workspace :as dw]
[app.main.refs :as refs]
[app.main.store :as st]
@@ -22,7 +21,7 @@
[app.main.ui.ds.foundations.assets.icon :refer [icon*] :as i]
[app.main.ui.hooks :as hooks]
[app.main.ui.notifications.badge :refer [badge-notification]]
[app.main.ui.workspace.sidebar.layer-item :refer [layer-item*]]
[app.main.ui.workspace.sidebar.layer-item :refer [layer-item]]
[app.util.dom :as dom]
[app.util.globals :as globals]
[app.util.i18n :as i18n :refer [tr]]
@@ -32,173 +31,92 @@
[beicon.v2.core :as rx]
[cuerdas.core :as str]
[goog.events :as events]
[okulary.core :as l]
[rumext.v2 :as mf]))
(def ^:private ref:highlighted-shapes
(l/derived (fn [local]
(-> local
(get :highlighted)
(not-empty)))
refs/workspace-local))
(def ^:private ref:shape-for-rename
(l/derived (l/key :shape-for-rename) refs/workspace-local))
(defn- use-selected-shapes
"A convencience hook wrapper for get selected shapes"
[]
(let [selected (mf/deref refs/selected-shapes)]
(hooks/use-equal-memo selected)))
[rumext.v2 :as mf])
(:import
goog.events.EventType))
;; This components is a piece for sharding equality check between top
;; level frames and try to avoid rerender frames that are does not
;; affected by the selected set.
(mf/defc frame-wrapper*
(mf/defc frame-wrapper
{::mf/props :obj}
[{:keys [selected] :as props}]
(let [pending-selected-ref
(mf/use-ref selected)
current-selected
(mf/use-state selected)
props
(mf/spread-object props {:selected @current-selected})
(let [pending-selected (mf/use-var selected)
current-selected (mf/use-state selected)
props (mf/spread-object props {:selected @current-selected})
set-selected
(mf/with-memo []
(throttle-fn 50 #(when-let [pending-selected (mf/ref-val pending-selected-ref)]
(reset! current-selected pending-selected))))]
(mf/use-memo
(fn []
(throttle-fn
50
#(when-let [pending-selected @pending-selected]
(reset! current-selected pending-selected)))))]
(mf/with-effect [selected set-selected]
(mf/set-ref-val! pending-selected-ref selected)
(^function set-selected)
(reset! pending-selected selected)
(set-selected)
(fn []
(mf/set-ref-val! pending-selected-ref nil)
(rx/dispose! set-selected)))
(reset! pending-selected nil)
#(rx/dispose! set-selected)))
[:> layer-item* props]))
(mf/defc layers-tree*
{::mf/wrap [mf/memo]}
[{:keys [objects is-filtered parent-size] :as props}]
(let [selected (use-selected-shapes)
highlighted (mf/deref ref:highlighted-shapes)
root (get objects uuid/zero)
rename-id (mf/deref ref:shape-for-rename)
shapes (get root :shapes)
shapes (mf/with-memo [shapes objects]
(loop [counter 0
shapes (seq shapes)
result (list)]
(if-let [id (first shapes)]
(if-let [obj (get objects id)]
(do
;; NOTE: this is a bit hacky, but reduces substantially
;; the allocation; If we use enumeration, we allocate
;; new sequence and add one iteration on each render,
;; independently if objects are changed or not. If we
;; store counter on metadata, we still need to create a
;; new allocation for each shape; with this method we
;; bypass this by mutating a private property on the
;; object removing extra allocation and extra iteration
;; on every request.
(unchecked-set obj "__$__counter" counter)
(recur (inc counter)
(rest shapes)
(conj result obj)))
(recur (inc counter)
(rest shapes)
result))
result)))
;; We pass objects just for trigger rerender but we internally use objects-ref
;; for make all callbacks and memoized blocks do not depen strictly on objects
;; and do not perform calculation if the local related shapes are not
;; changed. For this purpose we created a single objects-ref var for not doing
;; this on each layer item.
objects-ref (mf/use-ref objects)]
(mf/with-effect [objects]
(mf/set-ref-val! objects-ref objects))
[:> layer-item props]))
(mf/defc layers-tree
{::mf/wrap [mf/memo #(mf/throttle % 200)]
::mf/wrap-props false}
[{:keys [objects filtered? parent-size] :as props}]
(let [selected (mf/deref refs/selected-shapes)
selected (hooks/use-equal-memo selected)
highlighted (mf/deref refs/highlighted-shapes)
highlighted (hooks/use-equal-memo highlighted)
root (get objects uuid/zero)]
[:div {:class (stl/css :element-list) :data-testid "layer-item"}
[:> hooks/sortable-container* {}
(for [obj shapes]
(if (cfh/frame-shape? obj)
[:> frame-wrapper*
{:item obj
:rename-id rename-id
:selected selected
:highlighted highlighted
:index (unchecked-get obj "__$__counter")
:objects objects
:objects-ref objects-ref
:key (weak/weak-key obj)
:is-sortable true
:is-filtered is-filtered
:parent-size parent-size
:depth -1}]
[:> layer-item*
{:item obj
:rename-id rename-id
:selected selected
:highlighted highlighted
:index (unchecked-get obj "__$__counter")
:objects objects
:objects-ref objects-ref
:key (weak/weak-key obj)
:is-sortable true
:is-filtered is-filtered
:depth -1
:parent-size parent-size}]))]]))
(for [[index id] (reverse (d/enumerate (:shapes root)))]
(when-let [obj (get objects id)]
(if (cfh/frame-shape? obj)
[:& frame-wrapper
{:item obj
:selected selected
:highlighted highlighted
:index index
:objects objects
:key id
:sortable? true
:filtered? filtered?
:parent-size parent-size
:depth -1}]
[:& layer-item
{:item obj
:selected selected
:highlighted highlighted
:index index
:objects objects
:key id
:sortable? true
:filtered? filtered?
:depth -1
:parent-size parent-size}])))]]))
(mf/defc layers-tree-wrapper*
{::mf/private true}
[{:keys [objects] :as props}]
;; This is a performance sensitive componet, so we use lower-level primitives for
;; reduce residual allocation for this specific case
(let [state-tmp (mf/useState objects)
objects' (aget state-tmp 0)
set-objects (aget state-tmp 1)
subject-s (mf/with-memo []
(rx/subject))
changes-s (mf/with-memo [subject-s]
(->> subject-s
(rx/debounce 500)))
props (mf/spread-props props {:objects objects'})]
(mf/with-effect [objects subject-s]
(rx/push! subject-s objects))
(mf/with-effect [changes-s]
(let [sub (rx/subscribe changes-s set-objects)]
#(rx/dispose! sub)))
[:> layers-tree* props]))
(mf/defc filters-tree*
{::mf/wrap [mf/memo #(mf/throttle % 300)]
::mf/private true}
(mf/defc filters-tree
{::mf/wrap [mf/memo #(mf/throttle % 200)]
::mf/wrap-props false}
[{:keys [objects parent-size]}]
(let [selected (use-selected-shapes)
root (get objects uuid/zero)]
(let [selected (mf/deref refs/selected-shapes)
selected (hooks/use-equal-memo selected)
root (get objects uuid/zero)]
[:ul {:class (stl/css :element-list)}
(for [[index id] (d/enumerate (:shapes root))]
(when-let [obj (get objects id)]
[:> layer-item*
[:& layer-item
{:item obj
:selected selected
:index index
:objects objects
:key id
:is-sortable false
:is-filtered true
:sortable? false
:filtered? true
:depth -1
:parent-size parent-size}]))]))
@@ -214,7 +132,6 @@
keys
(filter #(not= uuid/zero %))
vec)]
(update reparented-objects uuid/zero assoc :shapes reparented-shapes)))
;; --- Layers Toolbox
@@ -360,11 +277,9 @@
(swap! state* update :num-items + 100))))]
(mf/with-effect []
(let [key1 (events/listen globals/document "keydown" on-key-down)
key2 (events/listen globals/document "click" hide-menu)]
(fn []
(events/unlistenByKey key1)
(events/unlistenByKey key2))))
(let [keys [(events/listen globals/document EventType.KEYDOWN on-key-down)
(events/listen globals/document EventType.CLICK hide-menu)]]
(fn [] (doseq [key keys] (events/unlistenByKey key)))))
[filtered-objects
handle-show-more
@@ -548,8 +463,6 @@
{::mf/wrap [mf/memo]}
[{:keys [size-parent]}]
(let [page (mf/deref refs/workspace-page)
page-id (get page :id)
focus (mf/deref refs/workspace-focus-selected)
objects (hooks/with-focus-objects (:objects page) focus)
@@ -559,8 +472,7 @@
observer-var (mf/use-var nil)
lazy-load-ref (mf/use-ref nil)
[filtered-objects show-more filter-component]
(use-search page objects)
[filtered-objects show-more filter-component] (use-search page objects)
intersection-callback
(fn [entries]
@@ -606,9 +518,9 @@
[:div {:class (stl/css :tool-window-content)
:data-scroll-container true
:ref on-render-container}
[:> filters-tree* {:objects filtered-objects
:key (dm/str page-id)
:parent-size size-parent}]
[:& filters-tree {:objects filtered-objects
:key (dm/str (:id page))
:parent-size size-parent}]
[:div {:ref lazy-load-ref
:style {:min-height 16}}]]
[:div {:on-scroll on-scroll
@@ -616,16 +528,16 @@
:data-scroll-container true
:style {:display (when (some? filtered-objects) "none")}}
[:> layers-tree-wrapper* {:objects filtered-objects
:key (dm/str page-id)
:is-filtered true
:parent-size size-parent}]]]
[:& layers-tree {:objects filtered-objects
:key (dm/str (:id page))
:filtered? true
:parent-size size-parent}]]]
[:div {:on-scroll on-scroll
:class (stl/css :tool-window-content)
:data-scroll-container true
:style {:display (when (some? filtered-objects) "none")}}
[:> layers-tree-wrapper* {:objects objects
:key (dm/str page-id)
:is-filtered false
:parent-size size-parent}]])]))
[:& layers-tree {:objects objects
:key (dm/str (:id page))
:filtered? false
:parent-size size-parent}]])]))

View File

@@ -60,8 +60,8 @@
on-detach-token
(mf/use-fn
(mf/deps ids)
(fn [token-name attr]
(st/emit! (dwta/unapply-token {:token-name token-name
(fn [token attr]
(st/emit! (dwta/unapply-token {:token (first token)
:attributes #{attr}
:shape-ids ids}))))

View File

@@ -152,9 +152,9 @@
on-detach-token
(mf/use-fn
(mf/deps token-colors groups)
(fn [token-name]
(fn [token]
(let [prev-colors (mf/ref-val prev-colors-ref)
token-color (some #(when (= (:token-name %) token-name) %) token-colors)
token-color (some #(when (= (:token-name %) (:name token)) %) token-colors)
[color-operations _] (retrieve-color-operations groups token-color prev-colors)]
(doseq [op color-operations]
@@ -166,8 +166,8 @@
(d/without-nils))]
(mf/set-ref-val! prev-colors-ref
(conj prev-colors color))
(st/emit! (dwta/unapply-token {:token-name token-name
:attributes attr
(st/emit! (dwta/unapply-token {:attributes attr
:token token
:shape-ids [(:shape-id op)]})))))))
select-only

View File

@@ -74,6 +74,7 @@
render-wasm? (feat/use-feature "render-wasm/v1")
^boolean
multiple? (= :multiple fills)
@@ -182,9 +183,9 @@
on-detach-token
(mf/use-fn
(mf/deps ids)
(fn [token-name]
(st/emit! (dwta/unapply-token {:token-name token-name
:attributes #{:fill}
(fn [token]
(st/emit! (dwta/unapply-token {:attributes #{:fill}
:token token
:shape-ids ids}))))]
(mf/with-layout-effect [hide-on-export]
@@ -214,8 +215,7 @@
(when open?
[:div {:class (stl/css :fill-content)}
(cond
(or (= :multiple fills)
(= :multiple fill-token-applied))
(= :multiple fills)
[:div {:class (stl/css :fill-multiple)}
[:div {:class (stl/css :fill-multiple-label)}
(tr "settings.multiple")]

View File

@@ -72,8 +72,8 @@
on-detach-token
(mf/use-fn
(mf/deps ids)
(fn [token-name attr]
(st/emit! (dwta/unapply-token {:token-name token-name
(fn [token attr]
(st/emit! (dwta/unapply-token {:token (first token)
:attributes #{attr}
:shape-ids ids}))))
@@ -229,13 +229,13 @@
:property (tr "workspace.options.opacity")
:applied-token (get applied-tokens :opacity)
:placeholder (if (or (= :multiple (get applied-tokens :opacity))
(= :multiple (or (get values :opacity) 1)))
(= :multiple (or (get values name) 1)))
(tr "settings.multiple")
"--")
:align :right
:class (stl/css :numeric-input-wrapper)
:value (* 100
(or (get values :opacity) 1))}]
(or (get values name) 1))}]
[:div {:class (stl/css :input)
:title (tr "workspace.options.opacity")}
@@ -248,6 +248,7 @@
:max 100
:className (stl/css :numeric-input)}]])
[:div {:class (stl/css :actions)}
(cond
(or (= :multiple hidden?) (not hidden?))

View File

@@ -339,8 +339,8 @@
on-detach-token
(mf/use-fn
(mf/deps ids)
(fn [token-name attr]
(st/emit! (dwta/unapply-token {:token-name token-name
(fn [token attr]
(st/emit! (dwta/unapply-token {:token (first token)
:attributes #{attr}
:shape-ids ids}))))
@@ -475,7 +475,7 @@
(mf/use-fn
(mf/deps ids)
(fn [token attr]
(st/emit! (dwta/unapply-token {:token-name token
(st/emit! (dwta/unapply-token {:token (first token)
:attributes #{attr}
:shape-ids ids}))))
@@ -722,7 +722,7 @@
(mf/use-fn
(mf/deps ids)
(fn [token attr]
(st/emit! (dwta/unapply-token {:token-name token
(st/emit! (dwta/unapply-token {:token (first token)
:attributes #{attr}
:shape-ids ids}))))

View File

@@ -97,8 +97,8 @@
on-detach-token
(mf/use-fn
(mf/deps ids)
(fn [token-name attr]
(st/emit! (dwta/unapply-token {:token-name token-name
(fn [token attr]
(st/emit! (dwta/unapply-token {:token (first token)
:attributes #{attr}
:shape-ids ids}))))
@@ -220,8 +220,8 @@
on-detach-token
(mf/use-fn
(mf/deps ids)
(fn [token-name attr]
(st/emit! (dwta/unapply-token {:token-name token-name
(fn [token attr]
(st/emit! (dwta/unapply-token {:token (first token)
:attributes #{attr}
:shape-ids ids}))))
@@ -550,8 +550,8 @@
on-detach-token
(mf/use-fn
(mf/deps ids)
(fn [token-name attr]
(st/emit! (dwta/unapply-token {:token-name token-name
(fn [token attr]
(st/emit! (dwta/unapply-token {:token (first token)
:attributes #{attr}
:shape-ids ids}))))

View File

@@ -319,8 +319,8 @@
on-detach-token
(mf/use-fn
(mf/deps ids)
(fn [token-name attr]
(st/emit! (dwta/unapply-token {:token-name token-name
(fn [token attr]
(st/emit! (dwta/unapply-token {:token (first token)
:attributes #{attr}
:shape-ids ids}))))

View File

@@ -171,9 +171,9 @@
on-detach-token
(mf/use-fn
(mf/deps ids)
(fn [token-name attrs]
(st/emit! (dwta/unapply-token {:token-name token-name
:attributes attrs
(fn [token attrs]
(st/emit! (dwta/unapply-token {:attributes attrs
:token token
:shape-ids ids}))))]
[:section {:class (stl/css :stroke-section)

View File

@@ -85,14 +85,14 @@
(mf/use-fn
(mf/deps detach-token token applied-token-name)
(fn []
(let [token-name (or (:name token) applied-token-name)]
(detach-token token-name))))
(let [token (or token applied-token-name)]
(detach-token token))))
has-errors (some? (:errors token))
token-name (:name token)
resolved (:resolved-value token)
not-active (or (empty? active-tokens)
(nil? token))
not-active (and (empty? active-tokens)
(nil? token))
id (dm/str (:id token) "-name")
swatch-tooltip-content (cond
not-active
@@ -344,6 +344,7 @@
(mf/with-effect [color prev-color disable-picker]
(when (and (not disable-picker) (not= prev-color color))
(modal/update-props! :colorpicker {:data (parse-color color)})))
[:div {:class [class row-class]}
;; Drag handler
(when (some? on-reorder)

View File

@@ -9,7 +9,7 @@
(:require
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.files.tokens :as cft]
[app.common.files.tokens :as cfo]
[app.common.types.shape.layout :as ctsl]
[app.common.types.token :as ctt]
[app.main.data.modal :as modal]
@@ -47,9 +47,9 @@
;; Actions ---------------------------------------------------------------------
(defn attribute-actions [token selected-shapes attributes]
(let [ids-by-attributes (cft/shapes-ids-by-applied-attributes token selected-shapes attributes)
(let [ids-by-attributes (cfo/shapes-ids-by-applied-attributes token selected-shapes attributes)
shape-ids (into #{} (map :id selected-shapes))]
{:all-selected? (cft/shapes-applied-all? ids-by-attributes shape-ids attributes)
{:all-selected? (cfo/shapes-applied-all? ids-by-attributes shape-ids attributes)
:shape-ids shape-ids
:selected-pred #(seq (% ids-by-attributes))}))
@@ -64,17 +64,14 @@
(let [selected? (selected-pred attribute)
props {:attributes #{attribute}
:token token
:shape-ids shape-ids}
unnaply-props {:token-name (:name token)
:attributes #{attribute}
:shape-ids shape-ids}]
:shape-ids shape-ids}]
{:title title
:hint hint
:selected? selected?
:action (fn []
(if selected?
(st/emit! (dwta/unapply-token unnaply-props))
(st/emit! (dwta/unapply-token props))
(st/emit! (dwta/apply-token (assoc props :on-update-shape on-update-shape-fn)))))}))
allowed-attributes)))
@@ -85,15 +82,12 @@
{:keys [all-selected? selected-pred shape-ids]} (attribute-actions token selected-shapes attributes)
all-action (let [props {:attributes attributes
:token token
:shape-ids shape-ids}
unnaply-props {:token-name (:name token)
:attributes attributes
:shape-ids shape-ids}]
:shape-ids shape-ids}]
{:title (tr "labels.all")
:selected? all-selected?
:hint hint
:action #(if all-selected?
(st/emit! (dwta/unapply-token unnaply-props))
(st/emit! (dwta/unapply-token props))
(st/emit! (dwta/apply-token (assoc props :on-update-shape (or on-update-shape-all on-update-shape)))))})
single-actions (map (fn [[attr title]]
(let [selected? (selected-pred attr)]
@@ -102,13 +96,10 @@
:action #(let [props {:attributes #{attr}
:token token
:shape-ids shape-ids}
unnaply-props {:token-name (:name token)
:attributes #{attr}
:shape-ids shape-ids}
event (cond
all-selected? (-> (assoc props :attributes-to-remove attributes)
(dwta/apply-token))
selected? (dwta/unapply-token unnaply-props)
selected? (dwta/unapply-token props)
:else (-> (assoc props :on-update-shape on-update-shape)
(dwta/apply-token)))]
(st/emit! event))}))
@@ -132,12 +123,9 @@
:action (fn []
(let [props {:attributes attrs
:token token
:shape-ids shape-ids}
unnaply-props {:token-name (:name token)
:attributes attrs
:shape-ids shape-ids}]
:shape-ids shape-ids}]
(if all-selected?
(st/emit! (dwta/unapply-token unnaply-props))
(st/emit! (dwta/unapply-token props))
(st/emit! (dwta/apply-token (assoc props :on-update-shape on-update-shape))))))}
{:title "Horizontal"
:selected? horizontal-selected?
@@ -177,13 +165,10 @@
:action #(let [props {:attributes #{attr}
:token token
:shape-ids shape-ids}
unnaply-props {:token-name (:name token)
:attributes #{attr}
:shape-ids shape-ids}
event (cond
all-selected? (-> (assoc props :attributes-to-remove attrs)
(dwta/apply-token))
selected? (dwta/unapply-token unnaply-props)
selected? (dwta/unapply-token props)
:else (-> (assoc props :on-update-shape on-update-shape)
(dwta/apply-token)))]
(st/emit! event))}))

View File

@@ -6,48 +6,56 @@
(ns app.main.ui.workspace.tokens.management.forms.color
(:require
[app.common.files.tokens :as cft]
#_[app.common.types.token :as cto]
#_[app.common.types.tokens-lib :as ctob]
#_[app.util.i18n :refer [tr]]
#_[cuerdas.core :as str]
[app.common.files.tokens :as cfo]
[app.common.schema :as sm]
[app.common.types.token :as cto]
[app.main.ui.workspace.tokens.management.forms.controls :as token.controls]
[app.main.ui.workspace.tokens.management.forms.generic-form :as generic]
[app.util.i18n :refer [tr]]
[cuerdas.core :as str]
[rumext.v2 :as mf]))
(defn- token-value-error-fn
[{:keys [value]}]
(when (or (str/empty? value)
(str/blank? value))
(tr "workspace.tokens.empty-input")))
#_(defn- token-value-error-fn
[{:keys [value]}]
(when (or (str/empty? value)
(str/blank? value))
(tr "workspace.tokens.empty-input")))
(defn- make-schema
[tokens-tree _]
(sm/schema
[:and
[:map
[:name
[:and
[:string {:min 1 :max 255 :error/fn #(str (:value %) (tr "workspace.tokens.token-name-length-validation-error"))}]
(sm/update-properties cto/token-name-ref assoc :error/fn #(str (:value %) (tr "workspace.tokens.token-name-validation-error")))
[:fn {:error/fn #(tr "workspace.tokens.token-name-duplication-validation-error" (:value %))}
#(not (cft/token-name-path-exists? % tokens-tree))]]]
#_(defn- make-schema
[tokens-tree _]
(sm/schema
[:and
[:map
[:name
[:and
[:string {:min 1 :max 255 :error/fn #(str (:value %) (tr "workspace.tokens.token-name-length-validation-error"))}]
(sm/update-properties cto/schema:token-name assoc :error/fn #(str (:value %) (tr "workspace.tokens.token-name-validation-error")))
[:fn {:error/fn #(tr "workspace.tokens.token-name-duplication-validation-error" (:value %))}
#(not (ctob/token-name-path-exists? % tokens-tree))]]]
[:value [::sm/text {:error/fn token-value-error-fn}]]
[:value [::sm/text {:error/fn token-value-error-fn}]]
[:color-result {:optional true} ::sm/any]
[:color-result {:optional true} ::sm/any]
[:description {:optional true}
[:string {:max 2048 :error/fn #(tr "errors.field-max-length" 2048)}]]]
[:description {:optional true}
[:string {:max 2048 :error/fn #(tr "errors.field-max-length" 2048)}]]]
[:fn {:error/field :value
:error/fn #(tr "workspace.tokens.self-reference")}
(fn [{:keys [name value]}]
(when (and name value)
(nil? (cto/token-value-self-reference? name value))))]]))
[:fn {:error/field :value
:error/fn #(tr "workspace.tokens.self-reference")}
(fn [{:keys [name value]}]
(when (and name value)
(nil? (cto/token-value-self-reference? name value))))]]))
(mf/defc form*
[props]
(let [props (mf/spread-props props {:make-schema make-schema
[{:keys [token token-type] :as props}]
(let [props (mf/spread-props props {:make-schema #(-> (cfo/make-token-schema %1 token-type)
(sm/dissoc-key :id)
(sm/assoc-key :color-result :string))
:initial {:type token-type
:name (:name token "")
:value (:value token "")
:description (:description token "")
:color-result ""}
:input-component token.controls/color-input*})]
[:> generic/form* props]))

View File

@@ -156,7 +156,6 @@
color-resolved
(get-in @form [:data :color-result] "")
valid-color (or (tinycolor/valid-color value)
(tinycolor/valid-color color-resolved))

View File

@@ -6,6 +6,8 @@
(ns app.main.ui.workspace.tokens.management.forms.font-family
(:require
[app.common.files.tokens :as cfo]
[app.common.schema :as sm]
[app.common.types.token :as cto]
[app.main.data.workspace.tokens.errors :as wte]
[app.main.ui.workspace.tokens.management.forms.controls :as token.controls]
@@ -35,6 +37,11 @@
{:type token-type}))
props (mf/spread-props props {:token token
:token-type token-type
:make-schema #(-> (cfo/make-token-schema %1 token-type)
(sm/dissoc-key :id)
;; The value as edited in the form is a simple stirng.
;; It's converted to vector in the validator.
(sm/assoc-key :value cfo/schema:token-value-generic))
:validator validate-font-family-token
:input-component token.controls/fonts-combobox*})]
[:> generic/form* props]))

View File

@@ -7,7 +7,6 @@
(ns app.main.ui.workspace.tokens.management.forms.form-container
(:require
[app.common.data :as d]
[app.common.files.tokens :as cft]
[app.common.types.tokens-lib :as ctob]
[app.main.refs :as refs]
[app.main.ui.workspace.tokens.management.forms.color :as color]
@@ -28,7 +27,7 @@
token-path
(mf/with-memo [token]
(cft/token-name->path (:name token)))
(ctob/get-token-path token))
tokens-tree-in-selected-set
(mf/with-memo [token-path tokens-in-selected-set]

View File

@@ -7,10 +7,11 @@
(ns app.main.ui.workspace.tokens.management.forms.generic-form
(:require-macros [app.main.style :as stl])
(:require
#_[app.common.types.token :as cto]
#_[app.common.uuid :as uuid]
[app.common.data :as d]
[app.common.files.tokens :as cft]
[app.common.files.tokens :as cfo]
[app.common.schema :as sm]
[app.common.types.token :as cto]
[app.common.types.tokens-lib :as ctob]
[app.main.constants :refer [max-input-length]]
[app.main.data.helpers :as dh]
@@ -36,12 +37,11 @@
[cuerdas.core :as str]
[rumext.v2 :as mf]))
(defn- token-value-error-fn
[{:keys [value]}]
(when (or (str/empty? value)
(str/blank? value))
(tr "workspace.tokens.empty-input")))
#_(defn- token-value-error-fn
[{:keys [value]}]
(when (or (str/empty? value)
(str/blank? value))
(tr "workspace.tokens.empty-input")))
(defn get-value-for-validator
[active-tab value value-subfield form-type]
@@ -59,28 +59,28 @@
value))
(defn- default-make-schema
[tokens-tree _]
(sm/schema
[:and
[:map
[:name
[:and
[:string {:min 1 :max 255 :error/fn #(str (:value %) (tr "workspace.tokens.token-name-length-validation-error"))}]
(sm/update-properties cto/token-name-ref assoc :error/fn #(str (:value %) (tr "workspace.tokens.token-name-validation-error")))
[:fn {:error/fn #(tr "workspace.tokens.token-name-duplication-validation-error" (:value %))}
#(not (cft/token-name-path-exists? % tokens-tree))]]]
#_(defn- default-make-schema
[tokens-tree _]
(sm/schema
[:and
[:map
[:name
[:and
[:string {:min 1 :max 255 :error/fn #(str (:value %) (tr "workspace.tokens.token-name-length-validation-error"))}]
(sm/update-properties cto/schema:token-name assoc :error/fn #(str (:value %) (tr "workspace.tokens.token-name-validation-error")))
[:fn {:error/fn #(tr "workspace.tokens.token-name-duplication-validation-error" (:value %))}
#(not (ctob/token-name-path-exists? % tokens-tree))]]]
[:value [::sm/text {:error/fn token-value-error-fn}]]
[:value [::sm/text {:error/fn token-value-error-fn}]]
[:description {:optional true}
[:string {:max 2048 :error/fn #(tr "errors.field-max-length" 2048)}]]]
[:description {:optional true}
[:string {:max 2048 :error/fn #(tr "errors.field-max-length" 2048)}]]]
[:fn {:error/field :value
:error/fn #(tr "workspace.tokens.self-reference")}
(fn [{:keys [name value]}]
(when (and name value)
(nil? (cto/token-value-self-reference? name value))))]]))
[:fn {:error/field :value
:error/fn #(tr "workspace.tokens.self-reference")}
(fn [{:keys [name value]}]
(when (and name value)
(not (cto/token-value-self-reference? name value))))]]))
(mf/defc form*
[{:keys [token
@@ -97,11 +97,12 @@
value-subfield
input-value-placeholder] :as props}]
(let [make-schema (or make-schema default-make-schema)
(let [make-schema (or make-schema #(-> (cfo/make-token-schema % token-type)
(sm/dissoc-key :id)))
input-component (or input-component token.controls/input*)
validate-token (or validator default-validate-token)
active-tab* (mf/use-state #(if (cft/is-reference? token) :reference :composite))
active-tab* (mf/use-state #(if (cfo/is-reference? token) :reference :composite))
active-tab (deref active-tab*)
token
@@ -134,7 +135,8 @@
initial
(mf/with-memo [token]
(or initial
{:name (:name token "")
{:type token-type
:name (:name token "")
:value (:value token "")
:description (:description token "")}))

View File

@@ -8,9 +8,9 @@
(:require-macros [app.main.style :as stl])
(:require
[app.common.data :as d]
[app.common.files.tokens :as cft]
[app.common.schema :as sm]
[app.common.types.token :as cto]
[app.common.types.tokens-lib :as ctob]
[app.main.data.workspace.tokens.errors :as wte]
[app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
[app.main.ui.ds.controls.radio-buttons :refer [radio-buttons*]]
@@ -51,7 +51,7 @@
;; Entering form without a value - show no error just resolve nil
(nil? token-value) (rx/of nil)
;; Validate refrence string
(cto/shadow-composite-token-reference? token-value) (default-validate-token params)
(cto/composite-token-reference? token-value) (default-validate-token params)
;; Validate composite token
:else
(let [params (-> params
@@ -253,6 +253,7 @@
[:> reference-form* {:token token
:tokens tokens}])]))
;; TODO: use cfo/make-schema:token-value and extend it with shadow and reference fields, adding :optional when needed
(defn- make-schema
[tokens-tree active-tab]
(sm/schema
@@ -262,10 +263,10 @@
[:and
[:string {:min 1 :max 255
:error/fn #(str (:value %) (tr "workspace.tokens.token-name-length-validation-error"))}]
(sm/update-properties cto/token-name-ref assoc
(sm/update-properties cto/schema:token-name assoc
:error/fn #(str (:value %) (tr "workspace.tokens.token-name-validation-error")))
[:fn {:error/fn #(tr "workspace.tokens.token-name-duplication-validation-error" (:value %))}
#(not (cft/token-name-path-exists? % tokens-tree))]]]
#(not (ctob/token-name-path-exists? % tokens-tree))]]]
[:value
[:map

View File

@@ -8,9 +8,9 @@
(:require-macros [app.main.style :as stl])
(:require
[app.common.data :as d]
[app.common.files.tokens :as cft]
[app.common.schema :as sm]
[app.common.types.token :as cto]
[app.common.types.tokens-lib :as ctob]
[app.main.data.workspace.tokens.errors :as wte]
[app.main.ui.ds.controls.radio-buttons :refer [radio-buttons*]]
[app.main.ui.ds.foundations.assets.icon :as i]
@@ -48,7 +48,7 @@
;; Entering form without a value - show no error just resolve nil
(nil? token-value) (rx/of nil)
;; Validate refrence string
(cto/typography-composite-token-reference? token-value) (default-validate-token props)
(cto/composite-token-reference? token-value) (default-validate-token props)
;; Validate composite token
:else
(-> props
@@ -217,10 +217,10 @@
[:and
[:string {:min 1 :max 255
:error/fn #(str (:value %) (tr "workspace.tokens.token-name-length-validation-error"))}]
(sm/update-properties cto/token-name-ref assoc
(sm/update-properties cto/schema:token-name assoc
:error/fn #(str (:value %) (tr "workspace.tokens.token-name-validation-error")))
[:fn {:error/fn #(tr "workspace.tokens.token-name-duplication-validation-error" (:value %))}
#(not (cft/token-name-path-exists? % tokens-tree))]]]
#(not (ctob/token-name-path-exists? % tokens-tree))]]]
[:value
[:map

View File

@@ -1,13 +1,11 @@
(ns app.main.ui.workspace.tokens.management.forms.validators
(:require
[app.common.data :as d]
[app.common.files.tokens :as cft]
[app.common.schema :as sm]
[app.common.types.token :as cto]
[app.common.types.tokens-lib :as ctob]
[app.main.data.style-dictionary :as sd]
[app.main.data.workspace.tokens.errors :as wte]
[app.util.i18n :refer [tr]]
[beicon.v2.core :as rx]
[cuerdas.core :as str]))
@@ -29,7 +27,8 @@
;; When creating a new token we dont have a name yet or invalid name,
;; but we still want to resolve the value to show in the form.
;; So we use a temporary token name that hopefully doesn't clash with any of the users token names
(not (sm/valid? cto/token-name-ref (:name token))) (assoc :name "__PENPOT__TOKEN__NAME__PLACEHOLDER__"))
(not (sm/valid? cto/schema:token-name (:name token)))
(assoc :name "__PENPOT__TOKEN__NAME__PLACEHOLDER__"))
tokens' (cond-> tokens
;; Remove previous token when renaming a token
(not= (:name token) (:name prev-token))
@@ -89,23 +88,3 @@
[token-name token-vals]
(when (some #(cto/token-value-self-reference? token-name %) token-vals)
(wte/get-error-code :error.token/direct-self-reference)))
;; This is used in plugins
(defn- make-token-name-schema
"Generate a dynamic schema validation to check if a token path derived
from the name already exists at `tokens-tree`."
[tokens-tree]
[:and
[:string {:min 1 :max 255 :error/fn #(str (:value %) (tr "workspace.tokens.token-name-length-validation-error"))}]
(sm/update-properties cto/token-name-ref assoc :error/fn #(str (:value %) (tr "workspace.tokens.token-name-validation-error")))
[:fn {:error/fn #(tr "workspace.tokens.token-name-duplication-validation-error" (:value %))}
#(not (cft/token-name-path-exists? % tokens-tree))]])
(defn validate-token-name
[tokens-tree name]
(let [schema (make-token-name-schema tokens-tree)
explainer (sm/explainer schema)]
(-> name explainer sm/simplify not-empty)))

View File

@@ -10,7 +10,7 @@
[app.main.style :as stl])
(:require
[app.common.data :as d]
[app.common.files.tokens :as cft]
[app.common.files.tokens :as cfo]
[app.common.path-names :as cpn]
[app.common.types.token :as ctt]
[app.config :as cf]
@@ -157,9 +157,9 @@
(defn- applied-all-attributes?
[token selected-shapes attributes]
(let [ids-by-attributes (cft/shapes-ids-by-applied-attributes token selected-shapes attributes)
(let [ids-by-attributes (cfo/shapes-ids-by-applied-attributes token selected-shapes attributes)
shape-ids (into #{} xf:map-id selected-shapes)]
(cft/shapes-applied-all? ids-by-attributes shape-ids attributes)))
(cfo/shapes-applied-all? ids-by-attributes shape-ids attributes)))
(defn attributes-match-selection?
[selected-shapes attrs & {:keys [selected-inside-layout?]}]
@@ -181,7 +181,7 @@
resolved-token (get active-theme-tokens (:name token))
has-selected? (pos? (count selected-shapes))
is-reference? (cft/is-reference? token)
is-reference? (cfo/is-reference? token)
contains-path? (str/includes? name ".")
attributes (as-> (get dwta/token-properties type) $
@@ -194,7 +194,7 @@
applied?
(if has-selected?
(cft/shapes-token-applied? token selected-shapes attributes)
(cfo/shapes-token-applied? token selected-shapes attributes)
false)
half-applied?
@@ -224,7 +224,7 @@
no-valid-value)
color
(when (cft/color-token? token)
(when (cfo/color-token? token)
(or (dwtc/resolved-token-bullet-color resolved-token)
(dwtc/resolved-token-bullet-color token)))

View File

@@ -63,7 +63,8 @@
(st/emit! (dwtl/start-token-set-edition id)))))]
[:> controlled-sets-list*
{:token-sets token-sets
{:tokens-lib tokens-lib
:token-sets token-sets
:is-token-set-active token-set-active?
:is-token-set-group-active token-set-group-active?
@@ -80,6 +81,6 @@
:on-toggle-token-set on-toggle-token-set-click
:on-toggle-token-set-group on-toggle-token-set-group-click
:on-update-token-set sets-helpers/on-update-token-set
:on-update-token-set (partial sets-helpers/on-update-token-set tokens-lib)
:on-update-token-set-group sets-helpers/on-update-token-set-group
:on-create-token-set sets-helpers/on-create-token-set}]))

View File

@@ -1,9 +1,13 @@
(ns app.main.ui.workspace.tokens.sets.helpers
(:require
[app.common.files.tokens :as cfo]
[app.common.schema :as sm]
[app.common.types.tokens-lib :as ctob]
[app.main.data.event :as ev]
[app.main.data.notifications :as ntf]
[app.main.data.workspace.tokens.library-edit :as dwtl]
[app.main.store :as st]
[app.util.i18n :refer [tr]]
[potok.v2.core :as ptk]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@@ -11,9 +15,18 @@
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn on-update-token-set
[token-set name]
(st/emit! (dwtl/clear-token-set-edition)
(dwtl/update-token-set token-set name)))
[tokens-lib token-set name]
(let [name (ctob/normalize-set-name name (ctob/get-name token-set))
errors (sm/validation-errors name (cfo/make-token-set-name-schema
tokens-lib
(ctob/get-id token-set)))]
(st/emit! (dwtl/clear-token-set-edition))
(if (empty? errors)
(st/emit! (dwtl/rename-token-set token-set name))
(st/emit! (ntf/show {:content (tr "errors.token-set-already-exists")
:type :toast
:level :error
:timeout 9000})))))
(defn on-update-token-set-group
[path name]
@@ -21,15 +34,15 @@
(dwtl/rename-token-set-group path name)))
(defn on-create-token-set
[parent-set name]
(let [;; FIXME: this code should be reusable under helper under
;; common types namespace
name
(if-let [parent-path (ctob/get-set-path parent-set)]
(->> (concat parent-path (ctob/split-set-name name))
(ctob/join-set-path))
(ctob/normalize-set-name name))
token-set (ctob/make-token-set :name name)]
[tokens-lib parent-set name]
(let [name (ctob/make-child-name parent-set name)
errors (sm/validation-errors name (cfo/make-token-set-name-schema tokens-lib nil))]
(st/emit! (ptk/data-event ::ev/event {::ev/name "create-token-set" :name name})
(dwtl/create-token-set token-set))))
(dwtl/clear-token-set-creation))
(if (empty? errors)
(let [token-set (ctob/make-token-set :name name)]
(st/emit! (dwtl/create-token-set token-set)))
(st/emit! (ntf/show {:content (tr "errors.token-set-already-exists")
:type :toast
:level :error
:timeout 9000})))))

View File

@@ -321,6 +321,7 @@
on-select
on-toggle-set
on-toggle-set-group
tokens-lib
token-sets
new-path
edition-id]}]
@@ -408,7 +409,7 @@
:on-drop on-drop
:on-reset-edition on-reset-edition
:on-edit-submit sets-helpers/on-create-token-set}]
:on-edit-submit (partial sets-helpers/on-create-token-set tokens-lib)}]
:else
[:> sets-tree-set*
@@ -434,7 +435,8 @@
:on-edit-submit on-edit-submit-set}])))))
(mf/defc controlled-sets-list*
[{:keys [token-sets
[{:keys [tokens-lib
token-sets
selected
on-update-token-set
on-update-token-set-group
@@ -486,6 +488,7 @@
{:is-draggable draggable?
:new-path new-path
:edition-id edition-id
:tokens-lib tokens-lib
:token-sets token-sets
:selected selected
:on-select on-select

View File

@@ -7,8 +7,11 @@
(ns app.main.ui.workspace.tokens.themes.create-modal
(:require-macros [app.main.style :as stl])
(:require
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.files.tokens :as cfo]
[app.common.logic.tokens :as clt]
[app.common.schema :as sm]
[app.common.types.tokens-lib :as ctob]
[app.main.constants :refer [max-input-length]]
[app.main.data.event :as ev]
@@ -30,32 +33,9 @@
[app.util.i18n :refer [tr]]
[app.util.keyboard :as k]
[cuerdas.core :as str]
[malli.core :as m]
[malli.error :as me]
[potok.v2.core :as ptk]
[rumext.v2 :as mf]))
;; Schemas ---------------------------------------------------------------------
(defn- theme-name-schema
"Generate a dynamic schema validation to check if a theme path derived from the name already exists at `tokens-tree`."
[{:keys [group theme-id tokens-lib]}]
(m/-simple-schema
{:type :token/name-exists
:pred (fn [name]
(if tokens-lib
(let [theme (ctob/get-theme-by-name tokens-lib group name)]
(or (nil? theme)
(= (ctob/get-id theme) theme-id)))
true)) ;; if still no library exists, cannot be duplicate
:type-properties {:error/fn #(tr "workspace.tokens.theme-name-already-exists")}}))
(defn validate-theme-name
[tokens-lib group theme-id name]
(let [schema (theme-name-schema {:tokens-lib tokens-lib :theme-id theme-id :group group})
validation (m/explain schema (str/trim name))]
(me/humanize validation)))
;; Form Component --------------------------------------------------------------
(mf/defc empty-themes
@@ -181,26 +161,43 @@
theme-groups)
current-group* (mf/use-state (:group theme))
current-group (deref current-group*)
current-name* (mf/use-state (:name theme))
current-name (deref current-name*)
group-errors* (mf/use-state nil)
group-errors (deref group-errors*)
name-errors* (mf/use-state nil)
name-errors (deref name-errors*)
on-update-group
(mf/use-fn
(mf/deps on-change-field)
(mf/deps on-change-field tokens-lib current-name)
(fn [value]
(reset! current-group* value)
(on-change-field :group value)))
(let [errors (sm/validation-errors value (cfo/make-token-theme-group-schema
tokens-lib
current-name
(ctob/get-id theme)))]
(reset! group-errors* errors)
(if (empty? errors)
(do
(reset! current-group* value)
(on-change-field :group value))
(on-change-field :group "")))))
on-update-name
(mf/use-fn
(mf/deps on-change-field tokens-lib current-group)
(fn [event]
(let [value (-> event dom/get-target dom/get-value)
errors (validate-theme-name tokens-lib current-group (ctob/get-id theme) value)]
errors (sm/validation-errors value (cfo/make-token-theme-name-schema
tokens-lib
current-group
(ctob/get-id theme)))]
(reset! name-errors* errors)
(mf/set-ref-val! theme-name-ref value)
(if (empty? errors)
(on-change-field :name value)
(do
(reset! current-name* value)
(on-change-field :name value))
(on-change-field :name "")))))]
[:div {:class (stl/css :edit-theme-inputs-wrapper)}
@@ -210,6 +207,7 @@
:placeholder (tr "workspace.tokens.label.group-placeholder")
:default-selected (:group theme)
:options (clj->js options)
:has-error (d/not-empty? group-errors)
:on-change on-update-group}]]
[:div {:class (stl/css :group-input-wrapper)}
@@ -262,6 +260,7 @@
(mf/defc edit-create-theme*
[{:keys [change-view theme on-save is-editing has-prev-view]}]
(let [ordered-token-sets (mf/deref refs/workspace-ordered-token-sets)
tokens-lib (mf/deref refs/tokens-lib)
token-sets (mf/deref refs/workspace-token-sets-tree)
current-theme* (mf/use-state theme)
@@ -363,7 +362,8 @@
[:div {:class (stl/css :sets-list-wrapper)}
[:> wts/controlled-sets-list*
{:token-sets token-sets
{:tokens-lib tokens-lib
:token-sets token-sets
:is-token-set-active token-set-active?
:is-token-set-group-active token-set-group-active?
:on-select on-click-token-set

View File

@@ -7,33 +7,41 @@
(ns app.plugins.tokens
(:require
[app.common.data.macros :as dm]
[app.common.files.tokens :as cfo]
[app.common.schema :as sm]
[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.workspace.tokens.application :as dwta]
[app.main.data.workspace.tokens.library-edit :as dwtl]
[app.main.store :as st]
[app.main.ui.workspace.tokens.management.forms.validators :as form-validator]
[app.main.ui.workspace.tokens.themes.create-modal :as theme-form]
[app.plugins.shape :as shape]
[app.plugins.utils :as u]
[app.util.object :as obj]
[beicon.v2.core :as rx]
[clojure.datafy :refer [datafy]]))
;; === Token
(defn- apply-token-to-shapes
[file-id set-id id shape-ids attrs]
(let [token (u/locate-token file-id set-id id)
kw-attrs (into #{} (map keyword attrs))]
(if (some #(not (cto/token-attr? %)) kw-attrs)
(let [token (u/locate-token file-id set-id id)]
(if (some #(not (cto/token-attr? %)) attrs)
(u/display-not-valid :applyToSelected attrs)
(st/emit!
(dwta/toggle-token {:token token
:attrs kw-attrs
:attrs attrs
:shape-ids shape-ids
:expand-with-children false})))))
(defn token-proxy? [p]
(obj/type-of? p "TokenProxy"))
(defn token-proxy
[plugin-id file-id set-id id]
(obj/reify {:name "TokenSetProxy"}
(obj/reify {:name "TokenProxy"
:wrap u/wrap-errors}
:$plugin {:enumerable false :get (constantly plugin-id)}
:$file-id {:enumerable false :get (constantly file-id)}
:$set-id {:enumerable false :get (constantly set-id)}
@@ -48,18 +56,12 @@
(fn [_]
(let [token (u/locate-token file-id set-id id)]
(ctob/get-name token)))
:schema (cfo/make-token-name-schema
(-> (u/locate-tokens-lib file-id)
(ctob/get-tokens set-id)))
:set
(fn [_ value]
(let [tokens-lib (u/locate-tokens-lib file-id)
errors (form-validator/validate-token-name
(ctob/get-tokens tokens-lib set-id)
value)]
(cond
(some? errors)
(u/display-not-valid :name (first errors))
:else
(st/emit! (dwtl/update-token set-id id {:name value})))))}
(st/emit! (dwtl/update-token set-id id {:name value})))}
:type
{:this true
@@ -73,17 +75,31 @@
:get
(fn [_]
(let [token (u/locate-token file-id set-id id)]
(:value token)))}
(:value token)))
:schema (let [token (u/locate-token file-id set-id id)]
(cfo/make-token-value-schema (:type token)))
:set
(fn [_ value]
(st/emit! (dwtl/update-token set-id id {:value value})))}
:description
{:this true
:get
(fn [_]
(let [token (u/locate-token file-id set-id id)]
(ctob/get-description token)))}
(ctob/get-description token)))
:schema cfo/schema:token-description
:set
(fn [_ value]
(st/emit! (dwtl/update-token set-id id {:description value})))}
:duplicate
(fn []
;; TODO:
;; - add function duplicate-token in tokens-lib, that allows to specify the new id
;; - use this function in dwtl/duplicate-token
;; - return the new token proxy using the locally forced id
;; - do the same with sets and themes
(let [token (u/locate-token file-id set-id id)
token' (ctob/make-token (-> (datafy token)
(dissoc :id
@@ -96,17 +112,27 @@
(st/emit! (dwtl/delete-token set-id id)))
:applyToShapes
(fn [shapes attrs]
(apply-token-to-shapes file-id set-id id (map :id shapes) attrs))
{:schema [:tuple
[:vector [:fn shape/shape-proxy?]]
[:maybe [:set ::sm/keyword]]]
:fn (fn [shapes attrs]
(apply-token-to-shapes file-id set-id id (map :id shapes) attrs))}
:applyToSelected
(fn [attrs]
(let [selected (get-in @st/state [:workspace-local :selected])]
(apply-token-to-shapes file-id set-id id selected attrs)))))
{:schema [:tuple [:maybe [:set ::sm/keyword]]]
:fn (fn [attrs]
(let [selected (get-in @st/state [:workspace-local :selected])]
(apply-token-to-shapes file-id set-id id selected attrs)))}))
;; === Token Set
(defn token-set-proxy? [p]
(obj/type-of? p "TokenSetProxy"))
(defn token-set-proxy
[plugin-id file-id id]
(obj/reify {:name "TokenSetProxy"}
(obj/reify {:name "TokenSetProxy"
:wrap u/wrap-errors}
:$plugin {:enumerable false :get (constantly plugin-id)}
:$file-id {:enumerable false :get (constantly file-id)}
:$id {:enumerable false :get (constantly id)}
@@ -120,15 +146,13 @@
(fn [_]
(let [set (u/locate-token-set file-id id)]
(ctob/get-name set)))
:schema (cfo/make-token-set-name-schema
(u/locate-tokens-lib file-id)
id)
:set
(fn [_ value]
(fn [_ name]
(let [set (u/locate-token-set file-id id)]
(cond
(not (string? value))
(u/display-not-valid :name value)
:else
(st/emit! (dwtl/update-token-set set value)))))}
(st/emit! (dwtl/rename-token-set set name))))}
:active
{:this true
@@ -138,6 +162,7 @@
(let [tokens-lib (u/locate-tokens-lib file-id)
set (u/locate-token-set file-id id)]
(ctob/token-set-active? tokens-lib (ctob/get-name set))))
:schema ::sm/boolean
:set
(fn [_ value]
(let [set (u/locate-token-set file-id id)]
@@ -153,8 +178,7 @@
:enumerable false
:get
(fn [_]
(let [file (u/locate-file file-id)
tokens-lib (->> file :data :tokens-lib)]
(let [tokens-lib (u/locate-tokens-lib file-id)]
(->> (ctob/get-tokens tokens-lib id)
(vals)
(map #(token-proxy plugin-id file-id id (:id %)))
@@ -165,8 +189,7 @@
:enumerable false
:get
(fn [_]
(let [file (u/locate-file file-id)
tokens-lib (->> file :data :tokens-lib)
(let [tokens-lib (u/locate-tokens-lib file-id)
tokens (ctob/get-tokens tokens-lib id)]
(->> tokens
(vals)
@@ -181,55 +204,56 @@
(apply array))))}
:getTokenById
(fn [token-id]
(cond
(not (string? token-id))
(u/display-not-valid :getTokenById token-id)
:else
(let [token-id (uuid/parse token-id)
token (u/locate-token file-id id token-id)]
(when (some? token)
(token-proxy plugin-id file-id id token-id)))))
{: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
(fn [type-str name value]
(let [type (cto/dtcg-token-type->token-type type-str)
value (case type
:font-family (ctob/convert-dtcg-font-family (js->clj value))
:typography (ctob/convert-dtcg-typography-composite (js->clj value))
:shadow (ctob/convert-dtcg-shadow-composite (js->clj value))
(js->clj value))]
(cond
(nil? type)
(u/display-not-valid :addTokenType type-str)
(not (string? name))
(u/display-not-valid :addTokenName name)
:else
(let [token (ctob/make-token {:type type
:name name
:value value})]
(st/emit! (dwtl/create-token id token))
(token-proxy plugin-id file-id (:id set) (:id token))))))
{: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"))))
;; Don't allow plugins to set the id
(sm/dissoc-key :id)
;; Instruct the json decoder in obj/reify not to process map keys (:key-fn below)
;; and set a converter that changes DTCG types to internal types (:decode/json).
;; E.g. "FontFamilies" -> :font-family or "BorderWidth" -> :stroke-width
(sm/update-properties assoc :decode/json cfo/convert-dtcg-token))])
: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))))}
:duplicate
(fn []
(let [set (u/locate-token-set file-id id)
set' (ctob/make-token-set (-> (datafy set)
(dissoc :id
:modified-at)))]
(st/emit! (dwtl/create-token-set set'))
(token-set-proxy plugin-id file-id (:id set'))))
(st/emit! (dwtl/duplicate-token-set id)))
:remove
(fn []
(st/emit! (dwtl/delete-token-set id)))))
(defn token-theme-proxy? [p]
(obj/type-of? p "TokenThemeProxy"))
(defn token-theme-proxy
[plugin-id file-id id]
(obj/reify {:name "TokenThemeProxy"}
(obj/reify {:name "TokenThemeProxy"
:wrap u/wrap-errors}
:$plugin {:enumerable false :get (constantly plugin-id)}
:$file-id {:enumerable false :get (constantly file-id)}
:$id {:enumerable false :get (constantly id)}
@@ -250,15 +274,15 @@
(fn [_]
(let [theme (u/locate-token-theme file-id id)]
(:group theme)))
:schema (let [theme (u/locate-token-theme file-id id)]
(cfo/make-token-theme-group-schema
(u/locate-tokens-lib file-id)
(:name theme)
(:id theme)))
:set
(fn [_ value]
(fn [_ group]
(let [theme (u/locate-token-theme file-id id)]
(cond
(not (string? value))
(u/display-not-valid :group value)
:else
(st/emit! (dwtl/update-token-theme id (assoc theme :group value))))))}
(st/emit! (dwtl/update-token-theme id (assoc theme :group group)))))}
:name
{:this true
@@ -266,20 +290,16 @@
(fn [_]
(let [theme (u/locate-token-theme file-id id)]
(:name theme)))
:schema (let [theme (u/locate-token-theme file-id id)]
(cfo/make-token-theme-name-schema
(u/locate-tokens-lib file-id)
(:id theme)
(:group theme)))
:set
(fn [_ value]
(let [theme (u/locate-token-theme file-id id)
errors (theme-form/validate-theme-name
(u/locate-tokens-lib file-id)
(:group theme)
id
value)]
(cond
(some? errors)
(u/display-not-valid :name (first errors))
:else
(st/emit! (dwtl/update-token-theme id (assoc theme :name value))))))}
(fn [_ name]
(let [theme (u/locate-token-theme file-id id)]
(when name
(st/emit! (dwtl/update-token-theme id (assoc theme :name name))))))}
:active
{:this true
@@ -288,6 +308,7 @@
(fn [_]
(let [tokens-lib (u/locate-tokens-lib file-id)]
(ctob/theme-active? tokens-lib id)))
:schema ::sm/boolean
:set
(fn [_ value]
(st/emit! (dwtl/set-token-theme-active id value)))}
@@ -300,14 +321,16 @@
{:this true :get (fn [_])}
:addSet
(fn [tokenSet]
(let [theme (u/locate-token-theme file-id id)]
(st/emit! (dwtl/update-token-theme id (ctob/enable-set theme (obj/get tokenSet :name))))))
{:schema [:tuple [:fn token-set-proxy?]]
:fn (fn [tokenSet]
(let [theme (u/locate-token-theme file-id id)]
(st/emit! (dwtl/update-token-theme id (ctob/enable-set theme (obj/get tokenSet :name))))))}
:removeSet
(fn [tokenSet]
(let [theme (u/locate-token-theme file-id id)]
(st/emit! (dwtl/update-token-theme id (ctob/disable-set theme (obj/get tokenSet :name))))))
{:schema [:tuple [:fn token-set-proxy?]]
:fn (fn [tokenSet]
(let [theme (u/locate-token-theme file-id id)]
(st/emit! (dwtl/update-token-theme id (ctob/disable-set theme (obj/get tokenSet :name))))))}
:duplicate
(fn []
@@ -324,7 +347,8 @@
(defn tokens-catalog
[plugin-id file-id]
(obj/reify {:name "TokensCatalog"}
(obj/reify {:name "TokensCatalog"
:wrap u/wrap-errors}
:$plugin {:enumerable false :get (constantly plugin-id)}
:$id {:enumerable false :get (constantly file-id)}
@@ -333,10 +357,10 @@
:enumerable false
:get
(fn [_]
(let [file (u/locate-file file-id)
tokens-lib (->> file :data :tokens-lib)
themes (->> (ctob/get-themes tokens-lib)
(remove #(= (:id %) uuid/zero)))]
(let [tokens-lib (u/locate-tokens-lib file-id)
themes (when tokens-lib
(->> (ctob/get-themes tokens-lib)
(remove #(= (:id %) uuid/zero))))]
(apply array (map #(token-theme-proxy plugin-id file-id (ctob/get-id %)) themes))))}
:sets
@@ -344,58 +368,47 @@
:enumerable false
:get
(fn [_]
(let [file (u/locate-file file-id)
tokens-lib (->> file :data :tokens-lib)
sets (ctob/get-sets tokens-lib)]
(let [tokens-lib (u/locate-tokens-lib file-id)
sets (when tokens-lib
(ctob/get-sets tokens-lib))]
(apply array (map #(token-set-proxy plugin-id file-id (ctob/get-id %)) sets))))}
:addTheme
(fn [group name]
(cond
(not (string? group))
(u/display-not-valid :addThemeGroup group)
(not (string? name))
(u/display-not-valid :addThemeName name)
:else
(let [theme (ctob/make-token-theme {:group group
:name name})]
(st/emit! (dwtl/create-token-theme theme))
(token-theme-proxy plugin-id file-id (:id theme)))))
{:schema (fn [attrs]
[:tuple (-> (sm/schema (cfo/make-token-theme-schema
(u/locate-tokens-lib file-id)
(or (obj/get attrs "group") "")
(or (obj/get attrs "name") "")
nil))
(sm/dissoc-key :id))]) ;; We don't allow plugins to set the id
:fn (fn [attrs]
(let [theme (ctob/make-token-theme attrs)]
(st/emit! (dwtl/create-token-theme theme))
(token-theme-proxy plugin-id file-id (:id theme))))}
:addSet
(fn [name]
(cond
(not (string? name))
(u/display-not-valid :addSetName name)
{: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
:else
(let [set (ctob/make-token-set {:name name})]
(st/emit! (dwtl/create-token-set set))
(token-set-proxy plugin-id file-id (:id set)))))
:fn (fn [attrs]
(let [attrs (update attrs :name ctob/normalize-set-name)
set (ctob/make-token-set attrs)]
(st/emit! (dwtl/create-token-set set))
(token-set-proxy plugin-id file-id (ctob/get-id set))))}
:getThemeById
(fn [theme-id]
(cond
(not (string? theme-id))
(u/display-not-valid :getThemeById theme-id)
:else
(let [theme-id (uuid/parse theme-id)
theme (u/locate-token-theme file-id theme-id)]
(when (some? theme)
(token-theme-proxy plugin-id file-id theme-id)))))
{: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
(fn [set-id]
(cond
(not (string? set-id))
(u/display-not-valid :getSetById set-id)
:else
(let [set-id (uuid/parse set-id)
set (u/locate-token-set file-id set-id)]
(when (some? set)
(token-set-proxy plugin-id file-id set-id)))))))
{:schema [:tuple ::sm/uuid]
:fn (fn [set-id]
(let [set (u/locate-token-set file-id set-id)]
(when (some? set)
(token-set-proxy plugin-id file-id set-id))))}))

View File

@@ -9,12 +9,15 @@
(:require
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.json :as json]
[app.common.schema :as sm]
[app.common.types.container :as ctn]
[app.common.types.file :as ctf]
[app.common.types.tokens-lib :as ctob]
[app.main.data.helpers :as dsh]
[app.main.store :as st]
[app.util.object :as obj]))
[app.util.object :as obj]
[cuerdas.core :as str]))
(defn locate-file
[id]
@@ -61,17 +64,20 @@
(defn locate-token-theme
[file-id id]
(let [tokens-lib (locate-tokens-lib file-id)]
(ctob/get-theme tokens-lib id)))
(when (some? tokens-lib)
(ctob/get-theme tokens-lib id))))
(defn locate-token-set
[file-id set-id]
(let [tokens-lib (locate-tokens-lib file-id)]
(ctob/get-set tokens-lib set-id)))
(when (some? tokens-lib)
(ctob/get-set tokens-lib set-id))))
(defn locate-token
[file-id set-id token-id]
(let [tokens-lib (locate-tokens-lib file-id)]
(ctob/get-token tokens-lib set-id token-id)))
(when (some? tokens-lib)
(ctob/get-token tokens-lib set-id token-id))))
(defn locate-presence
[session-id]
@@ -218,7 +224,8 @@
(defn display-not-valid
[code value]
(.error js/console (dm/str "[PENPOT PLUGIN] Value not valid: " value ". Code: " code)))
(.error js/console (dm/str "[PENPOT PLUGIN] Value not valid: " value ". Code: " code))
nil)
(defn reject-not-valid
[reject code value]
@@ -226,7 +233,44 @@
(.error js/console msg)
(reject msg)))
(defn coerce
"Decodes a javascript object into clj and check against schema. If schema validation fails,
displays a not-valid message with the code and hint provided and returns nil."
[attrs schema code hint]
(let [decoder (sm/decoder schema sm/json-transformer)
explainer (sm/explainer schema)
attrs (-> attrs json/->clj decoder)]
(if-let [explain (explainer attrs)]
(display-not-valid code (str hint " " (sm/humanize-explain explain)))
attrs)))
(defn coerce-1
"Checks a single javascript value against schema. If schema validation fails,
displays a not-valid message with the code and hint provided and returns nil."
[value schema code hint]
(let [errors (sm/validation-errors value schema)]
(if (d/not-empty? errors)
(display-not-valid code (str hint " " (str/join ", " errors)))
value)))
(defn mixed-value
[values]
(let [s (set values)]
(if (= (count s) 1) (first s) "mixed")))
(defn wrap-errors
"Function wrapper to be used in plugin proxies methods to handle errors.
When an exception is thrown, a readable error message is output to the console
and the exception is captured."
[f]
(fn []
(let [args (js-arguments)]
(try
(.apply f nil args)
(catch :default cause
(display-not-valid (ex-message cause) (obj/stringify args))
(if-let [explain (-> cause ex-data ::sm/explain)]
(println (sm/humanize-explain explain))
(js/console.log (ex-data cause)))
(js/console.log (.-stack cause))
nil)))))

View File

@@ -8,6 +8,7 @@
"A i18n foundation."
(:require
[app.common.data :as d]
[app.common.i18n]
[app.common.logging :as log]
[app.common.time :as ct]
[app.config :as cf]
@@ -210,3 +211,7 @@
(fn [_ _ pv cv]
(when (not= pv cv)
(ct/set-default-locale cv))))
;; We set the real translation function in the common i18n namespace,
;; so that when common code calls (tr ...) it uses this function.
(set! app.common.i18n/tr tr)

View File

@@ -106,6 +106,11 @@
(identical? (.getPrototypeOf js/Object o)
(.-prototype js/Object)))))
#?(:cljs
(defn stringify
[obj]
(js/JSON.stringify obj)))
;; EXPERIMENTAL: unsafe, does not checks and not validates the input,
;; should be improved over time, for now it works for define a class
;; extending js/Error that is more than enought for a first, quick and
@@ -163,14 +168,14 @@
bindings
(->> properties
(mapcat (fn [params]
(let [pname (c/get params :name)
get-expr (c/get params :get)
set-expr (c/get params :set)
fn-expr (c/get params :fn)
schema-n (c/get params :schema)
wrap (c/get params :wrap)
schema-1 (c/get params :schema-1)
this? (c/get params :this false)
(let [pname (c/get params :name)
get-expr (c/get params :get)
set-expr (c/get params :set)
fn-expr (c/get params :fn)
schema-n (c/get params :schema)
wrap (c/get params :wrap)
schema-1 (c/get params :schema-1)
this? (c/get params :this false)
decode-expr
(c/get params :decode/fn)
@@ -253,7 +258,11 @@
`(~decode-sym ~val-sym)
`(~decode-sym ~val-sym ~decode-options))
val-sym `(~coercer-sym ~val-sym)]
[])]
[])
~fn-sym ~(if wrap
`(~wrap-sym ~fn-sym)
fn-sym)]
~(if this?
`(.call ~fn-sym ~this-sym ~this-sym ~val-sym)

View File

@@ -177,7 +177,7 @@
;; ==== Action
events [(dwta/unapply-token {:shape-ids [(cthi/id :frame1)]
:attributes #{:r1 :r2 :r3 :r4}
:token-name "test-token-1"})]
:token (toht/get-token file "test-token-1")})]
step2 (fn [_]
(let [events2 [(dwl/sync-file (:id file) (:id file))]]
@@ -289,7 +289,7 @@
;; ==== Action
events [(dwta/unapply-token {:shape-ids [(cthi/id :c-frame1)]
:attributes #{:r1 :r2 :r3 :r4}
:token-name "test-token-1"})
:token (toht/get-token file "test-token-1")})
(dwta/apply-token {:shape-ids [(cthi/id :frame1)]
:attributes #{:r1 :r2 :r3 :r4}
:token (toht/get-token file "test-token-3")

View File

@@ -6,7 +6,7 @@
(ns frontend-tests.tokens.helpers.tokens
(:require
[app.common.files.tokens :as cft]
[app.common.files.tokens :as cfo]
[app.common.test-helpers.ids-map :as thi]
[app.common.types.tokens-lib :as ctob]))
@@ -20,7 +20,7 @@
(let [first-page-id (get-in file [:data :pages 0])
shape-id (thi/id shape-label)
token (get-token file token-label)
applied-attributes (cft/attributes-map attributes token)]
applied-attributes (cfo/attributes-map attributes token)]
(update-in file [:data
:pages-index first-page-id
:objects shape-id

View File

@@ -57,8 +57,7 @@
store (ths/setup-store file)
tokens-lib (toht/get-tokens-lib file)
set-a (ctob/get-set-by-name tokens-lib "Set A")
events [(dwtl/update-token-set (ctob/rename set-a "Set A updated")
"Set A updated")]]
events [(dwtl/rename-token-set set-a "Set A updated")]]
(tohs/run-store-async
store done events

View File

@@ -19,7 +19,7 @@ In the `apps` folder you'll find some examples that use the libraries mentioned
- example-styles: to run this example you should run
```
npm run start:styles-example
pnpm run start:styles-example
```
Open in your browser: `http://localhost:4202/`
@@ -28,8 +28,8 @@ Open in your browser: `http://localhost:4202/`
This guide will help you launch a Penpot plugin from the penpot-plugins repository. Before proceeding, ensure that you have Penpot running locally by following the [setup instructions](https://help.penpot.app/technical-guide/developer/devenv/).
In the terminal, navigate to the **penpot-plugins** repository and run `npm install` to install the required dependencies.
Then, run `npm start` to launch the plugins wrapper.
In the terminal, navigate to the **penpot-plugins** repository and run `pnpm install` to install the required dependencies.
Then, run `pnpm run start` to launch the plugins wrapper.
After installing the dependencies, choose a plugin to launch. You can either run one of the provided examples or create your own (see "Creating a plugin from scratch" below).
To launch a plugin, Open a new terminal tab and run the appropriate startup script for the chosen plugin.
@@ -38,7 +38,7 @@ For instance, to launch the Contrast plugin, use the following command:
```
// for the contrast plugin
npm run start:plugin:contrast
pnpm run start:plugin:contrast
```
Finally, open in your browser the specific port. In this specific example would be `http://localhost:4302`
@@ -47,23 +47,24 @@ A table listing the available plugins and their corresponding startup commands i
## Sample plugins
| Plugin | Description | PORT | Start command | Manifest URL |
| ----------------------- | ----------------------------------------------------------- | ---- | ------------------------------------- | ------------------------------------------ |
| poc-state-plugin | Sandbox plugin to test new plugins api functionality | 4301 | npm run start:plugin:poc-state | http://localhost:4301/assets/manifest.json |
| contrast-plugin | Sample plugin that gives you color contrast information | 4302 | npm run start:plugin:contrast | http://localhost:4302/assets/manifest.json |
| icons-plugin | Tool to add icons from [Feather](https://feathericons.com/) | 4303 | npm run start:plugin:icons | http://localhost:4303/assets/manifest.json |
| lorem-ipsum-plugin | Generate Lorem ipsum text | 4304 | npm run start:plugin:loremipsum | http://localhost:4304/assets/manifest.json |
| create-palette-plugin | Creates a board with all the palette colors | 4305 | npm run start:plugin:palette | http://localhost:4305/assets/manifest.json |
| table-plugin | Create or import table | 4306 | npm run start:table-plugin | http://localhost:4306/assets/manifest.json |
| rename-layers-plugin | Rename layers in bulk | 4307 | npm run start:plugin:renamelayers | http://localhost:4307/assets/manifest.json |
| colors-to-tokens-plugin | Generate tokens JSON file | 4308 | npm run start:plugin:colors-to-tokens | http://localhost:4308/assets/manifest.json |
| Plugin | Description | PORT | Start command | Manifest URL |
| ----------------------- | ----------------------------------------------------------- | ---- | -------------------------------------- | ------------------------------------------ |
| poc-state-plugin | Sandbox plugin to test new plugins api functionality | 4301 | pnpm run start:plugin:poc-state | http://localhost:4301/assets/manifest.json |
| contrast-plugin | Sample plugin that gives you color contrast information | 4302 | pnpm run start:plugin:contrast | http://localhost:4302/assets/manifest.json |
| icons-plugin | Tool to add icons from [Feather](https://feathericons.com/) | 4303 | pnpm run start:plugin:icons | http://localhost:4303/assets/manifest.json |
| lorem-ipsum-plugin | Generate Lorem ipsum text | 4304 | pnpm run start:plugin:loremipsum | http://localhost:4304/assets/manifest.json |
| create-palette-plugin | Creates a board with all the palette colors | 4305 | pnpm run start:plugin:palette | http://localhost:4305/assets/manifest.json |
| table-plugin | Create or import table | 4306 | pnpm run start:table-plugin | http://localhost:4306/assets/manifest.json |
| rename-layers-plugin | Rename layers in bulk | 4307 | pnpm run start:plugin:renamelayers | http://localhost:4307/assets/manifest.json |
| colors-to-tokens-plugin | Generate tokens JSON file | 4308 | pnpm run start:plugin:colors-to-tokens | http://localhost:4308/assets/manifest.json |
| poc-tokens-plugin | Sandbox plugin to test tokens functionality | 4309 | pnpm run start:plugin:poc-tokens | http://localhost:4309/assets/manifest.json |
## Web Apps
| App | Description | PORT | Start command | URL |
| --------------- | ----------------------------------------------------------------- | ---- | -------------------------------- | ---------------------- |
| plugins-runtime | Runtime for the plugins subsystem | 4200 | npm run start:app:runtime | |
| example-styles | Showcase of some of the Penpot styles that can be used in plugins | 4201 | npm run start:app:styles-example | http://localhost:4201/ |
| App | Description | PORT | Start command | URL |
| --------------- | ----------------------------------------------------------------- | ---- | --------------------------------- | ---------------------- |
| plugins-runtime | Runtime for the plugins subsystem | 4200 | pnpm run start:app:runtime | |
| example-styles | Showcase of some of the Penpot styles that can be used in plugins | 4201 | pnpm run start:app:styles-example | http://localhost:4201/ |
## Creating a plugin from scratch

View File

@@ -0,0 +1,51 @@
import baseConfig from '../../eslint.config.js';
import { compat } from '../../eslint.base.config.js';
export default [
...baseConfig,
...compat
.config({
extends: [
'plugin:@nx/angular',
'plugin:@angular-eslint/template/process-inline-templates',
],
})
.map((config) => ({
...config,
files: ['**/*.ts'],
rules: {
'@angular-eslint/directive-selector': [
'error',
{
type: 'attribute',
prefix: 'app',
style: 'camelCase',
},
],
'@angular-eslint/component-selector': [
'error',
{
type: 'element',
prefix: 'app',
style: 'kebab-case',
},
],
},
})),
...compat
.config({ extends: ['plugin:@nx/angular-template'] })
.map((config) => ({
...config,
files: ['**/*.html'],
rules: {},
})),
{ ignores: ['**/assets/*.js'] },
{
languageOptions: {
parserOptions: {
project: './tsconfig.*?.json',
tsconfigRootDir: import.meta.dirname,
},
},
},
];

View File

@@ -0,0 +1,79 @@
{
"name": "poc-tokens-plugin",
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"projectType": "application",
"prefix": "app",
"sourceRoot": "apps/poc-tokens-plugin/src",
"tags": ["type:plugin"],
"targets": {
"build": {
"executor": "@angular-devkit/build-angular:application",
"outputs": ["{options.outputPath}"],
"options": {
"outputPath": "dist/apps/poc-tokens-plugin",
"index": "apps/poc-tokens-plugin/src/index.html",
"browser": "apps/poc-tokens-plugin/src/main.ts",
"polyfills": ["zone.js"],
"tsConfig": "apps/poc-tokens-plugin/tsconfig.app.json",
"assets": [
"apps/poc-tokens-plugin/src/favicon.ico",
"apps/poc-tokens-plugin/src/assets"
],
"styles": [
"libs/plugins-styles/src/lib/styles.css",
"apps/poc-tokens-plugin/src/styles.css"
],
"scripts": [],
"optimization": {
"scripts": true,
"styles": true,
"fonts": false
}
},
"configurations": {
"production": {
"budgets": [
{
"type": "initial",
"maximumWarning": "500kb",
"maximumError": "1mb"
},
{
"type": "anyComponentStyle",
"maximumWarning": "2kb",
"maximumError": "4kb"
}
],
"outputHashing": "all"
},
"development": {
"optimization": false,
"extractLicenses": false,
"sourceMap": true
}
},
"defaultConfiguration": "production",
"dependsOn": ["buildPlugin"]
},
"serve": {
"executor": "@angular-devkit/build-angular:dev-server",
"configurations": {
"production": {
"buildTarget": "poc-tokens-plugin:build:production"
},
"development": {
"buildTarget": "poc-tokens-plugin:build:development",
"port": 4309,
"host": "0.0.0.0"
}
},
"defaultConfiguration": "development"
},
"extract-i18n": {
"executor": "@angular-devkit/build-angular:extract-i18n",
"options": {
"buildTarget": "poc-tokens-plugin:build"
}
}
}
}

View File

@@ -0,0 +1,127 @@
/* @import "@penpot/plugin-styles/styles.css"; */
.container {
display: flex;
flex-direction: column;
height: 100%;
}
.title-l {
margin: var(--spacing-16) 0;
}
.columns {
display: grid;
grid-template-columns: 50% 50%;
flex-grow: 1;
margin-block-end: var(--spacing-16);
}
.panels {
display: flex;
flex-direction: column;
flex-grow: 1;
padding: 0 var(--spacing-8);
}
.panel {
padding: var(--spacing-8);
display: flex;
flex-basis: 0;
flex-grow: 1;
flex-direction: column;
overflow: auto;
}
.panel:not(:first-child) {
border-block-start: 1px solid var(--df-secondary);
padding-block-start: var(--spacing-16);
}
.panel-heading,
.token-group {
display: flex;
flex-direction: row;
padding-inline-end: var(--spacing-8);
}
.panel-heading p,
.token-group span {
flex-grow: 1;
}
.panel-heading button,
.token-group button {
background: none;
padding: var(--spacing-4) calc(var(--spacing-12) / 2);
}
.panel-heading button:focus,
.token-group button:focus {
padding: calc(var(--spacing-4) - 2px) calc(var(--spacing-12) / 2 - 2px);
}
.panel-item button {
opacity: 0;
margin-inline-end: var(--spacing-8);
padding: var(--spacing-4) calc(var(--spacing-12) / 2);
}
.panel-item button:hover {
opacity: 1;
}
.panel-item button:focus {
opacity: 1;
padding: calc(var(--spacing-4) - 2px) calc(var(--spacing-12) / 2 - 2px);
}
.panel ul {
/* flex-grow: 1; */
overflow-y: auto;
padding-inline-end: var(--spacing-8);
}
.panel-item {
display: flex;
flex-direction: row;
}
.panel-item span {
flex-grow: 1;
}
.set-item {
cursor: pointer;
}
.set-item.selected {
background-color: var(--db-quaternary);
}
.set-item:hover {
color: var(--da-primary);
background-color: var(--db-secondary);
}
.token-group:not(:first-child) {
margin-top: var(--spacing-8);
}
.token-group {
border-block-end: 1px solid var(--df-secondary);
text-transform: capitalize;
}
.token-item {
cursor: pointer;
}
.token-item:hover {
color: var(--da-primary);
}
.buttons {
display: flex;
flex-direction: row-reverse;
}

View File

@@ -0,0 +1,144 @@
<div class="container">
<p class="title-l">Design tokens plugin POC</p>
<div class="columns">
<div class="panels">
<div class="panel">
<div class="panel-heading">
<p class="headline-m">THEMES</p>
<button
type="button"
data-appearance="secondary"
(click)="addTheme()"
>
+
</button>
</div>
<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>
<button
type="button"
data-appearance="secondary"
(click)="renameTheme(theme.id, theme.name)"
>
🖊️
</button>
<button
type="button"
data-appearance="secondary"
(click)="deleteTheme(theme.id)"
>
</button>
<div class="checkbox-container">
<input
class="checkbox-input"
type="checkbox"
id="checkbox1"
[checked]="isThemeActive(theme.id)"
(change)="toggleTheme(theme.id)"
/>
</div>
</li>
}
</ul>
</div>
<div class="panel">
<div class="panel-heading">
<p class="headline-m">SETS</p>
<button type="button" data-appearance="secondary" (click)="addSet()">
+
</button>
</div>
<ul data-handler="sets-list">
@for (set of sets; track set.id) {
<li
class="body-m panel-item set-item"
[class.selected]="set.id === currentSetId"
>
<span (click)="loadTokens(set.id)">
{{ set.name }}
</span>
<button
type="button"
data-appearance="secondary"
(click)="renameSet(set.id, set.name)"
>
🖊️
</button>
<button
type="button"
data-appearance="secondary"
(click)="deleteSet(set.id)"
>
</button>
<div class="checkbox-container">
<input
class="checkbox-input"
type="checkbox"
id="checkbox1"
[checked]="isSetActive(set.id)"
(change)="toggleSet(set.id)"
/>
</div>
</li>
}
</ul>
</div>
</div>
<div class="panels">
<div class="panel">
<p class="headline-m">TOKENS</p>
<ul data-handler="tokens-list">
@for (group of tokenGroups; track group[0]) {
<li class="body-m token-group">
<span>{{ group[0] }}</span>
<button
type="button"
data-appearance="secondary"
(click)="addToken(group[0])"
>
+
</button>
</li>
@for (token of group[1]; track token.id) {
<li
class="body-m panel-item token-item"
(click)="applyToken(token.id)"
>
<span>{{ token.name }}</span>
<button
type="button"
data-appearance="secondary"
(click)="renameToken(token.id, token.name)"
>
🖊️
</button>
<button
type="button"
data-appearance="secondary"
(click)="deleteToken(token.id)"
>
</button>
</li>
}
}
</ul>
</div>
</div>
</div>
<div class="buttons">
<button type="button" data-appearance="primary" (click)="loadLibrary()">
Load
</button>
</div>
</div>

View File

@@ -0,0 +1,290 @@
import { Component, inject } from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
import { ActivatedRoute } from '@angular/router';
import { fromEvent, map, filter, take, merge } from 'rxjs';
import { PluginMessageEvent, PluginUIEvent } from '../model';
type TokenTheme = {
id: string;
name: string;
group: string;
description: string;
active: boolean;
};
type TokenSet = {
id: string;
name: string;
description: string;
active: boolean;
};
type Token = {
id: string;
name: string;
description: string;
};
type TokensGroup = [string, Token[]];
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrl: './app.component.css',
host: {
'[attr.data-theme]': 'theme()',
},
})
export class AppComponent {
public route = inject(ActivatedRoute);
public messages$ = fromEvent<MessageEvent<PluginMessageEvent>>(
window,
'message',
);
public initialTheme$ = this.route.queryParamMap.pipe(
map((params) => params.get('theme')),
filter((theme) => !!theme),
take(1),
);
public theme = toSignal(
merge(
this.initialTheme$,
this.messages$.pipe(
filter((event) => event.data.type === 'theme'),
map((event) => {
return event.data.content;
}),
),
),
);
public themes: TokenTheme[] = [];
public sets: TokenSet[] = [];
public tokenGroups: TokensGroup[] = [];
public currentSetId: string | undefined = undefined;
constructor() {
window.addEventListener('message', (event) => {
if (event.data.type === 'set-themes') {
this.#setThemes(event.data.themesData);
} else if (event.data.type === 'set-sets') {
this.#setSets(event.data.setsData);
} else if (event.data.type === 'set-tokens') {
this.#setTokens(event.data.tokenGroupsData);
}
});
}
loadLibrary() {
this.#sendMessage({ type: 'load-library' });
}
loadTokens(setId: string) {
this.currentSetId = setId;
this.#sendMessage({ type: 'load-tokens', setId });
}
addTheme() {
this.#sendMessage({
type: 'add-theme',
themeGroup: this.#randomString(),
themeName: this.#randomString(),
});
}
addSet() {
this.#sendMessage({ type: 'add-set', setName: this.#randomString() });
}
addToken(tokenType: string) {
let tokenValue;
switch (tokenType) {
case 'borderRadius':
tokenValue = '25';
break;
case 'shadow':
tokenValue = [
{
color: '#123456',
inset: 'false',
offsetX: '6',
offsetY: '6',
spread: '0',
blur: '4',
},
];
break;
case 'color':
tokenValue = '#fabada';
break;
case 'dimension':
tokenValue = '100';
break;
case 'fontFamilies':
tokenValue = ['Source Sans Pro', 'Sans serif'];
break;
case 'fontSizes':
tokenValue = '24';
break;
case 'fontWeights':
tokenValue = 'bold';
break;
case 'letterSpacing':
tokenValue = '0.5';
break;
case 'number':
tokenValue = '33';
break;
case 'opacity':
tokenValue = '0.6';
break;
case 'rotation':
tokenValue = '45';
break;
case 'sizing':
tokenValue = '200';
break;
case 'spacing':
tokenValue = '16';
break;
case 'borderWidth':
tokenValue = '3';
break;
case 'textCase':
tokenValue = 'lowercase';
break;
case 'textDecoration':
tokenValue = 'underline';
break;
case 'typography':
tokenValue = {
fontFamilies: ['Acme', 'Arial', 'Sans Serif'],
fontSizes: '36',
letterSpacing: '0.8',
textCase: 'uppercase',
textDecoration: 'none',
fontWeights: '600',
lineHeight: '1.5',
};
break;
}
if (this.currentSetId && tokenValue) {
this.#sendMessage({
type: 'add-token',
setId: this.currentSetId,
tokenType,
tokenName: this.#randomString(),
tokenValue,
});
} else {
console.log('Invalid token type');
}
}
renameTheme(themeId: string, themeName: string) {
const newName = prompt('Rename theme', themeName);
if (newName && newName !== '') {
this.#sendMessage({ type: 'rename-theme', themeId, newName });
}
}
renameSet(setId: string, setName: string) {
const newName = prompt('Rename set', setName);
if (newName && newName !== '') {
this.#sendMessage({ type: 'rename-set', setId, newName });
}
}
renameToken(tokenId: string, tokenName: string) {
const newName = prompt('Rename token', tokenName);
if (this.currentSetId && newName && newName !== '') {
this.#sendMessage({
type: 'rename-token',
setId: this.currentSetId,
tokenId,
newName,
});
}
}
deleteTheme(themeId: string) {
this.#sendMessage({ type: 'delete-theme', themeId });
}
deleteSet(setId: string) {
this.#sendMessage({ type: 'delete-set', setId });
}
deleteToken(tokenId: string) {
if (this.currentSetId) {
this.#sendMessage({
type: 'delete-token',
setId: this.currentSetId,
tokenId,
});
}
}
isThemeActive(themeId: string) {
for (const theme of this.themes) {
if (theme.id === themeId) {
return theme.active;
}
}
return false;
}
toggleTheme(themeId: string) {
this.#sendMessage({ type: 'toggle-theme', themeId });
}
isSetActive(setId: string) {
for (const set of this.sets) {
if (set.id === setId) {
return set.active;
}
}
return false;
}
toggleSet(setId: string) {
this.#sendMessage({ type: 'toggle-set', setId });
}
applyToken(tokenId: string) {
if (this.currentSetId) {
this.#sendMessage({
type: 'apply-token',
setId: this.currentSetId,
tokenId,
// properties: ['strokeColor'] // Uncomment to choose attribute to apply
}); // (incompatible attributes will have no effect)
}
}
#sendMessage(message: PluginUIEvent) {
parent.postMessage(message, '*');
}
#setThemes(themes: TokenTheme[]) {
this.themes = themes;
}
#setSets(sets: TokenSet[]) {
this.sets = sets;
}
#setTokens(tokenGroups: TokensGroup[]) {
this.tokenGroups = tokenGroups;
}
#randomString() {
// Generate a big random number and convert it to string using base 36
// (the number of letters in the ascii alphabet)
return Math.floor(Math.random() * Date.now()).toString(36);
}
}

View File

@@ -0,0 +1,11 @@
import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
import { provideRouter } from '@angular/router';
import { routes } from './app.routes';
export const appConfig: ApplicationConfig = {
providers: [
provideZoneChangeDetection({ eventCoalescing: true }),
provideRouter(routes),
],
};

View File

@@ -0,0 +1,3 @@
import { Routes } from '@angular/router';
export const routes: Routes = [];

View File

@@ -0,0 +1 @@
*

View File

@@ -0,0 +1,2 @@
/*
Access-Control-Allow-Origin: *

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

View File

@@ -0,0 +1,14 @@
{
"name": "Design tokens plugin POC",
"description": "This is a plugin to try Design Tokens in Penpot API",
"code": "/assets/plugin.js",
"permissions": [
"page:read",
"content:read",
"file:read",
"selection:read",
"content:write",
"library:read",
"library:write"
]
}

View File

@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Angular example plugin</title>
<base href="/" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" type="image/x-icon" href="favicon.ico" />
</head>
<body>
<app-root></app-root>
</body>
</html>

View File

@@ -0,0 +1,7 @@
import { bootstrapApplication } from '@angular/platform-browser';
import { appConfig } from './app/app.config';
import { AppComponent } from './app/app.component';
bootstrapApplication(AppComponent, appConfig).catch((err) =>
console.error(err),
);

View File

@@ -0,0 +1,112 @@
import { TokenProperty } from '@penpot/plugin-types';
/**
* This file contains the typescript interfaces for the plugin events.
*/
// Events sent from the ui to the plugin
export interface LoadLibraryEvent {
type: 'load-library';
}
export interface LoadTokensEvent {
type: 'load-tokens';
setId: string;
}
export interface AddThemeEvent {
type: 'add-theme';
themeGroup: string;
themeName: string;
}
export interface AddSetEvent {
type: 'add-set';
setName: string;
}
export interface AddTokenEvent {
type: 'add-token';
setId: string;
tokenType: string;
tokenName: string;
tokenValue: unknown;
}
export interface RenameThemeEvent {
type: 'rename-theme';
themeId: string;
newName: string;
}
export interface RenameSetEvent {
type: 'rename-set';
setId: string;
newName: string;
}
export interface RenameTokenEvent {
type: 'rename-token';
setId: string;
tokenId: string;
newName: string;
}
export interface DeleteThemeEvent {
type: 'delete-theme';
themeId: string;
}
export interface DeleteSetEvent {
type: 'delete-set';
setId: string;
}
export interface DeleteTokenEvent {
type: 'delete-token';
setId: string;
tokenId: string;
}
export interface ToggleThemeEvent {
type: 'toggle-theme';
themeId: string;
}
export interface ToggleSetEvent {
type: 'toggle-set';
setId: string;
}
export interface ApplyTokenEvent {
type: 'apply-token';
setId: string;
tokenId: string;
properties?: TokenProperty[];
}
export type PluginUIEvent =
| LoadLibraryEvent
| LoadTokensEvent
| AddThemeEvent
| AddSetEvent
| AddTokenEvent
| RenameThemeEvent
| RenameSetEvent
| RenameTokenEvent
| DeleteThemeEvent
| DeleteSetEvent
| DeleteTokenEvent
| ToggleThemeEvent
| ToggleSetEvent
| ApplyTokenEvent;
// Events sent from the plugin to the ui
export interface ThemePluginEvent {
type: 'theme';
content: string;
}
export type PluginMessageEvent = ThemePluginEvent;

View File

@@ -0,0 +1,262 @@
import type { PluginMessageEvent, PluginUIEvent } from './model.js';
import {
TokenType,
TokenProperty,
TokenValueString,
} from '@penpot/plugin-types';
penpot.ui.open('Design Tokens test', `?theme=${penpot.theme}`, {
width: 1000,
height: 800,
});
penpot.on('themechange', (theme) => {
sendMessage({ type: 'theme', content: theme });
});
penpot.ui.onMessage<PluginUIEvent>(async (message) => {
if (message.type === 'load-library') {
loadLibrary();
} else if (message.type === 'load-tokens') {
loadTokens(message.setId);
} else if (message.type === 'add-theme') {
addTheme(message.themeGroup, message.themeName);
} else if (message.type === 'add-set') {
addSet(message.setName);
} else if (message.type === 'add-token') {
addToken(
message.setId,
message.tokenType,
message.tokenName,
message.tokenValue,
);
} else if (message.type === 'rename-theme') {
renameTheme(message.themeId, message.newName);
} else if (message.type === 'rename-set') {
renameSet(message.setId, message.newName);
} else if (message.type === 'rename-token') {
renameToken(message.setId, message.tokenId, message.newName);
} else if (message.type === 'delete-theme') {
deleteTheme(message.themeId);
} else if (message.type === 'delete-set') {
deleteSet(message.setId);
} else if (message.type === 'delete-token') {
deleteToken(message.setId, message.tokenId);
} else if (message.type === 'toggle-theme') {
toggleTheme(message.themeId);
} else if (message.type === 'toggle-set') {
toggleSet(message.setId);
} else if (message.type === 'apply-token') {
applyToken(message.setId, message.tokenId, message.properties);
}
});
function sendMessage(message: PluginMessageEvent) {
penpot.ui.sendMessage(message);
}
function loadLibrary() {
const tokensCatalog = penpot.library.local.tokens;
const themes = tokensCatalog.themes;
const themesData = themes.map((theme) => {
return {
id: theme.id,
group: theme.group,
name: theme.name,
active: theme.active,
};
});
penpot.ui.sendMessage({
source: 'penpot',
type: 'set-themes',
themesData,
});
const sets = tokensCatalog.sets;
const setsData = sets.map((set) => {
return {
id: set.id,
name: set.name,
active: set.active,
};
});
penpot.ui.sendMessage({
source: 'penpot',
type: 'set-sets',
setsData,
});
}
function loadTokens(setId: string) {
const tokensCatalog = penpot.library.local.tokens;
const set = tokensCatalog?.getSetById(setId);
const tokensByType = set?.tokensByType;
const tokenGroupsData = [];
if (tokensByType) {
for (const group of tokensByType) {
const type = group[0];
const tokens = group[1];
tokenGroupsData.push([
type,
tokens.map((token) => {
return {
id: token.id,
name: token.name,
description: token.description,
};
}),
]);
}
penpot.ui.sendMessage({
source: 'penpot',
type: 'set-tokens',
tokenGroupsData,
});
}
}
function addTheme(themeGroup: string, themeName: string) {
const tokensCatalog = penpot.library.local.tokens;
const theme = tokensCatalog?.addTheme({ group: themeGroup, name: themeName });
if (theme) {
loadLibrary();
}
}
function addSet(setName: string) {
const tokensCatalog = penpot.library.local.tokens;
const set = tokensCatalog?.addSet({ name: setName });
if (set) {
loadLibrary();
}
}
function addToken(
setId: string,
tokenType: string,
tokenName: string,
tokenValue: unknown,
) {
const tokensCatalog = penpot.library.local.tokens;
const set = tokensCatalog?.getSetById(setId);
const token = set?.addToken({
type: tokenType as TokenType,
name: tokenName,
value: tokenValue as TokenValueString,
});
if (token) {
// TODO: remove this timeout when styleDictionary is replaced
// with tokenScript and the token validation is syncrhronous.
setTimeout(() => {
loadTokens(setId);
}, 0);
}
}
function renameTheme(themeId: string, newName: string) {
const tokensCatalog = penpot.library.local.tokens;
const theme = tokensCatalog?.getThemeById(themeId);
if (theme) {
theme.name = newName;
loadLibrary();
}
}
function renameSet(setId: string, newName: string) {
const tokensCatalog = penpot.library.local.tokens;
const set = tokensCatalog?.getSetById(setId);
if (set) {
set.name = newName;
loadLibrary();
}
}
function renameToken(setId: string, tokenId: string, newName: string) {
const tokensCatalog = penpot.library.local.tokens;
const set = tokensCatalog?.getSetById(setId);
const token = set?.getTokenById(tokenId);
if (token) {
token.name = newName;
// TODO: remove this timeout when styleDictionary is replaced
// with tokenScript and the token validation is syncrhronous.
setTimeout(() => {
loadTokens(setId);
}, 0);
}
}
function deleteTheme(themeId: string) {
const tokensCatalog = penpot.library.local.tokens;
const theme = tokensCatalog?.getThemeById(themeId);
if (theme) {
theme.remove();
loadLibrary();
}
}
function deleteSet(setId: string) {
const tokensCatalog = penpot.library.local.tokens;
const set = tokensCatalog?.getSetById(setId);
if (set) {
set.remove();
loadLibrary();
}
}
function deleteToken(setId: string, tokenId: string) {
const tokensCatalog = penpot.library.local.tokens;
const set = tokensCatalog?.getSetById(setId);
const token = set?.getTokenById(tokenId);
if (token) {
token.remove();
loadTokens(setId);
}
}
function toggleTheme(themeId: string) {
const tokensCatalog = penpot.library.local.tokens;
const theme = tokensCatalog?.getThemeById(themeId);
if (theme) {
theme.toggleActive();
loadLibrary();
}
}
function toggleSet(setId: string) {
const tokensCatalog = penpot.library.local.tokens;
const set = tokensCatalog?.getSetById(setId);
if (set) {
set.toggleActive();
loadLibrary();
}
}
function applyToken(
setId: string,
tokenId: string,
properties: TokenProperty[] | undefined,
) {
const tokensCatalog = penpot.library.local.tokens;
const set = tokensCatalog?.getSetById(setId);
const token = set?.getTokenById(tokenId);
if (token) {
token.applyToSelected(properties);
}
// Alternatve way
//
// const selection = penpot.selection;
// if (token && selection) {
// for (const shape of selection) {
// shape.applyToken(token, properties);
// }
// }
}

View File

@@ -0,0 +1,23 @@
/* @import "@penpot/plugin-styles/styles.css"; */
html {
height: 100%;
}
body {
height: 100%;
line-height: 1.5;
padding: 10px;
}
ul {
margin-block-start: var(--spacing-12);
}
.title-l {
text-align: center;
}
.headline-l {
margin-block-start: var(--spacing-8);
}

View File

@@ -0,0 +1,10 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../dist/out-tsc",
"types": []
},
"files": ["src/main.ts"],
"include": ["src/**/*.d.ts"],
"exclude": ["src/**/*.test.ts", "src/**/*.spec.ts"]
}

View File

@@ -0,0 +1,7 @@
{
"extends": "./tsconfig.json",
"include": ["src/**/*.ts"],
"compilerOptions": {
"types": ["node"]
}
}

View File

@@ -0,0 +1,33 @@
{
"compilerOptions": {
"target": "es2022",
"useDefineForClassFields": false,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"noImplicitOverride": true,
"noPropertyAccessFromIndexSignature": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true
},
"files": [],
"include": [],
"references": [
{
"path": "./tsconfig.app.json"
},
{
"path": "./tsconfig.editor.json"
},
{
"path": "./tsconfig.plugin.json"
}
],
"extends": "../../tsconfig.base.json",
"angularCompilerOptions": {
"enableI18nLegacyMessageIdFormat": false,
"strictInjectionParameters": true,
"strictInputAccessModifiers": true,
"strictTemplates": true
}
}

View File

@@ -0,0 +1,8 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"types": []
},
"files": ["src/plugin.ts"],
"include": ["../../libs/plugin-types/index.d.ts"]
}

View File

File diff suppressed because it is too large Load Diff

View File

@@ -16,6 +16,7 @@
"start:plugin:table": "nx run table-plugin:init",
"start:plugin:renamelayers": "nx run rename-layers-plugin:init",
"start:plugin:colors-to-tokens": "nx run colors-to-tokens-plugin:init",
"start:plugin:poc-tokens": "nx run poc-tokens-plugin:init",
"build": "nx build plugins-runtime --emptyOutDir=true",
"build:plugins": "nx run-many -t build --parallel -p tag:type:plugin --exclude=poc-state-plugin",
"build:styles-example": "nx run example-styles:build",