Compare commits

..

17 Commits

Author SHA1 Message Date
Andrey Antukh
9e0ba4429a Add slugify to the filename on assets exportation
Fixes https://github.com/penpot/penpot/issues/8017
2026-01-20 15:27:24 +01:00
David Barragán Merino
bbe6ee2e19 🔧 Define a different temporary config file for each execution 2026-01-20 12:59:56 +01:00
David Barragán Merino
fb6d8309b6 🔧 Prevent error 429 downloading docker images from dockerhub 2026-01-20 12:59:56 +01:00
Andrey Antukh
689467bcf9 📎 Update changelog 2026-01-20 12:25:43 +01:00
Andrey Antukh
7724450037 Merge remote-tracking branch 'origin/staging-render' into develop 2026-01-20 12:18:04 +01:00
Xaviju
368fa954ce Remove tokens tree node (#8042) 2026-01-20 11:00:13 +01:00
Andrey Antukh
6fd0f5377c Merge remote-tracking branch 'origin/staging' into staging-render 2026-01-20 10:08:58 +01:00
Elena Torró
eb54bc485e Merge pull request #8120 from penpot/alotor-fix-flex-layout
🐛 Fix problems with layout
2026-01-20 10:00:24 +01:00
Elena Torró
12c24a36b4 Merge pull request #8122 from penpot/fix-thumbnail-generation
🐛 Fix problem with thumbnail generation
2026-01-20 09:59:34 +01:00
Alejandro Alonso
324d54ad28 🐛 Fix set all rounded corners to 0 2026-01-20 09:34:06 +01:00
alonso.torres
f42ff27f3d 🐛 Fix problem with bools 2026-01-19 17:05:04 +01:00
alonso.torres
2c1cc89f53 🐛 Fix problem with thumbnail generation 2026-01-19 12:54:15 +01:00
alonso.torres
498b0b30fe 🐛 Fix problems with layout 2026-01-19 12:17:58 +01:00
Elena Torró
89f40dcda2 🔧 Move WebGL context error message to 'errors' namespace (#8117) 2026-01-19 11:24:19 +01:00
Elena Torró
ccac7bd510 Merge pull request #8108 from penpot/ladybenko-13022-blur-page
🎉 Apply blur effect when switching pages
2026-01-19 11:04:31 +01:00
Belén Albeza
43d1d127dc 🎉 Apply blur effect to previous canvas pixels while setting wasm objects 2026-01-16 13:04:59 +01:00
Belén Albeza
8bd3ef717c 🎉 Apply blur to canvas when switching pages 2026-01-16 13:04:59 +01:00
82 changed files with 28462 additions and 3409 deletions

View File

@@ -10,6 +10,11 @@ jobs:
runs-on: penpot-runner-02
steps:
- name: Set common environment variables
run: |
# Each job execution will use its own docker configuration.
echo "DOCKER_CONFIG=${{ runner.temp }}/.docker-${{ github.run_id }}-${{ github.job }}" >> $GITHUB_ENV
- name: Checkout code
uses: actions/checkout@v4

View File

@@ -22,6 +22,11 @@ jobs:
runs-on: penpot-runner-02
steps:
- name: Set common environment variables
run: |
# Each job execution will use its own docker configuration.
echo "DOCKER_CONFIG=${{ runner.temp }}/.docker-${{ github.run_id }}-${{ github.job }}" >> $GITHUB_ENV
- name: Checkout code
uses: actions/checkout@v4
with:
@@ -66,6 +71,15 @@ jobs:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
# To avoid the “429 Too Many Requests” error when downloading
# images from DockerHub for unregistered users.
# https://docs.docker.com/docker-hub/usage/
- name: Login to DockerHub Registry
uses: docker/login-action@v3
with:
username: ${{ secrets.PUB_DOCKER_USERNAME }}
password: ${{ secrets.PUB_DOCKER_PASSWORD }}
- name: Extract metadata (tags, labels)
id: meta
uses: docker/metadata-action@v5

View File

@@ -14,14 +14,16 @@
- Tokens panel nested path view [Taiga #9966](https://tree.taiga.io/project/penpot/us/9966)
- Improve usability of lock and hide buttons in the layer panel. [Taiga #12916](https://tree.taiga.io/project/penpot/issue/12916)
- Optimize sidebar performance for deeply nested shapes [Taiga #13017](https://tree.taiga.io/project/penpot/task/13017)
- Change the default option on tokens import dialog [Github #8051](https://github.com/penpot/penpot/pull/8051)
### :bug: Bugs fixed
- Remove whitespaces from asset export filename [Github #8133](https://github.com/penpot/penpot/pull/8133)
- Fix prototype connections lost when switching between variants [Taiga #12812](https://tree.taiga.io/project/penpot/issue/12812)
- Fix wrong image in the onboarding invitation block [Taiga #13040](https://tree.taiga.io/project/penpot/issue/13040)
- Fix wrong register image [Taiga #12955](https://tree.taiga.io/project/penpot/task/12955)
- Fix error message on components doesn't close automatically [Taiga #12012](https://tree.taiga.io/project/penpot/issue/12012)
- Fix incorrect default option on tokens import dialog [Github #8051](https://github.com/penpot/penpot/pull/8051)
## 2.13.0 (Unreleased)

View File

@@ -27,7 +27,6 @@
[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]
@@ -379,7 +378,7 @@
[:type [:= :set-token]]
[:set-id ::sm/uuid]
[:token-id ::sm/uuid]
[:attrs [:maybe cto/schema:token-attrs]]]]
[:attrs [:maybe ctob/schema:token-attrs]]]]
[:set-token-set
[:map {:title "SetTokenSetChange"}

View File

@@ -8,222 +8,8 @@
(: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]
[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])
(def schema:token-value
[:or
schema:token-value-font-family
schema:token-value-typography
schema:token-value-shadow
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
(-> cto/schema:token-name
(sm/update-properties assoc :error/fn #(str (:value %) (tr "workspace.tokens.token-name-validation-error"))))
[:string {:min 1 :max 255 :error/fn #(str (:value %) (tr "workspace.tokens.token-name-length-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]
[:and
(sm/merge
cto/schema:token-attrs
[:map
[:name (make-token-name-schema tokens-tree)]
[:value schema:token-value]
[: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]
(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]
(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]
(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
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
[cuerdas.core :as str]))
(def parseable-token-value-regexp
"Regexp that can be used to parse a number value out of resolved token value.
@@ -294,6 +80,56 @@
(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

@@ -1,15 +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.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-tokens-from-shape % token-attrs))
(pcb/update-shapes changes [shape-id] #(cto/unapply-token-id % token-attrs))
changes)))
check-shape

View File

@@ -92,30 +92,6 @@
[& 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)]
(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))
@@ -294,13 +270,6 @@
(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)

View File

@@ -9,13 +9,13 @@
[app.common.data :as d]
[app.common.schema :as sm]
[app.common.schema.generators :as sg]
[app.common.time :as ct]
[clojure.data :as data]
[clojure.set :as set]
[cuerdas.core :as str]
[malli.util :as mu]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; GENERAL HELPERS
;; 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)]
(boolean self-reference?)))
self-reference?))
(defn references-token?
"Recursively check if a value references the token name. Handles strings, maps, and sequences."
@@ -59,26 +59,6 @@
(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
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@@ -86,6 +66,7 @@
(def token-type->dtcg-token-type
{:boolean "boolean"
:border-radius "borderRadius"
:shadow "shadow"
:color "color"
:dimensions "dimension"
:font-family "fontFamilies"
@@ -96,7 +77,6 @@
:opacity "opacity"
:other "other"
:rotation "rotation"
:shadow "shadow"
:sizing "sizing"
:spacing "spacing"
:string "string"
@@ -114,13 +94,14 @@
"boxShadow" :shadow)))
(def composite-token-type->dtcg-token-type
"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."
"Custom set of conversion keys for composite typography token with `:line-height` available.
(Penpot doesn't support `:line-height` token)"
(assoc token-type->dtcg-token-type
:line-height "lineHeights"))
(def composite-dtcg-token-type->token-type
"Same as above, in the opposite direction."
"Custom set of conversion keys for composite typography token with `:line-height` available.
(Penpot doesn't support `:line-height` token)"
(assoc dtcg-token-type->token-type
"lineHeights" :line-height
"lineHeight" :line-height))
@@ -128,111 +109,83 @@
(def token-types
(into #{} (keys token-type->dtcg-token-type)))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; SCHEMA: Token
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(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}
(def token-name-ref
[:re {:title "TokenNameRef" :gen/gen sg/text}
#"^(?!\$)([a-zA-Z0-9-$_]+\.?)*(?<!\.)$"])
(def schema:token-type
[::sm/one-of {:decode/json (fn [type]
(if (string? type)
(dtcg-token-type->token-type type)
type))}
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} 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:color
[:map
[:fill {:optional true} schema:token-name]
[:stroke-color {:optional true} schema:token-name]])
[:fill {:optional true} token-name-ref]
[:stroke-color {:optional true} token-name-ref]])
(def color-keys (schema-keys schema:color))
(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]])
(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
[:map
[:stroke-width {:optional true} token-name-ref]])
(def stroke-width-keys (schema-keys schema:stroke-width))
(def ^:private schema:sizing-base
[:map {:title "SizingBaseTokenAttrs"}
[:width {:optional true} schema:token-name]
[:height {:optional true} schema:token-name]])
[:width {:optional true} token-name-ref]
[:height {:optional true} token-name-ref]])
(def ^:private schema:sizing-layout-item
[:map {:title "SizingLayoutItemTokenAttrs"}
[: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))
[: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]])
(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} schema:token-name]
[:column-gap {:optional true} schema:token-name]])
[:row-gap {:optional true} token-name-ref]
[:column-gap {:optional true} token-name-ref]])
(def ^:private schema:spacing-padding
[:map {:title "SpacingPaddingTokenAttrs"}
[: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
schema:spacing-padding])
(mu/update-properties assoc :title "SpacingGapPaddingTokenAttrs")))
(def spacing-gap-padding-keys (schema-keys schema:spacing-gap-padding))
[: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} 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))
[: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
@@ -240,13 +193,16 @@
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))
(def ^:private schema:stroke-width
[:map
[:stroke-width {:optional true} schema:token-name]])
(def ^:private schema:spacing-gap-padding
(-> (reduce mu/union [schema:spacing-gap
schema:spacing-padding])
(mu/update-properties assoc :title "SpacingGapPaddingTokenAttrs")))
(def stroke-width-keys (schema-keys schema:stroke-width))
(def spacing-gap-padding-keys (schema-keys schema:spacing-gap-padding))
(def ^:private schema:dimensions
(-> (reduce mu/union [schema:sizing
@@ -257,109 +213,91 @@
(def dimensions-keys (schema-keys schema:dimensions))
(def ^:private schema:font-family
(def ^:private schema:axis
[:map
[:font-family {:optional true} schema:token-name]])
[:x {:optional true} token-name-ref]
[:y {:optional true} token-name-ref]])
(def font-family-keys (schema-keys schema:font-family))
(def ^:private schema:font-size
[:map {:title "FontSizeTokenAttrs"}
[:font-size {:optional true} schema:token-name]])
(def font-size-keys (schema-keys schema:font-size))
(def ^:private schema:font-weight
[:map
[:font-weight {:optional true} schema:token-name]])
(def font-weight-keys (schema-keys schema:font-weight))
(def ^:private schema:letter-spacing
[:map {:title "LetterSpacingTokenAttrs"}
[:letter-spacing {:optional true} schema:token-name]])
(def letter-spacing-keys (schema-keys schema:letter-spacing))
(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 line-height-keys (schema-keys schema:line-height))
(def axis-keys (schema-keys schema:axis))
(def ^:private schema:rotation
[:map {:title "RotationTokenAttrs"}
[:rotation {:optional true} schema:token-name]])
[: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]])
(def font-family-keys (schema-keys schema:font-family))
(def ^:private schema:text-case
[:map
[:text-case {:optional true} token-name-ref]])
(def text-case-keys (schema-keys schema:text-case))
(def ^:private schema:font-weight
[:map
[:font-weight {:optional true} token-name-ref]])
(def font-weight-keys (schema-keys schema:font-weight))
(def ^:private schema:typography
[:map
[:typography {:optional true} token-name-ref]])
(def typography-token-keys (schema-keys schema:typography))
(def ^:private schema:text-decoration
[:map
[:text-decoration {:optional true} token-name-ref]])
(def text-decoration-keys (schema-keys schema:text-decoration))
(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:number
(-> (reduce mu/union [schema:line-height
(-> (reduce mu/union [[:map [:line-height {:optional true} token-name-ref]]
schema:rotation])
(mu/update-properties assoc :title "NumberTokenAttrs")))
(def number-keys (schema-keys schema:number))
(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
(def all-keys (set/union color-keys
border-radius-keys
color-keys
dimensions-keys
number-keys
opacity-keys
rotation-keys
shadow-keys
sizing-keys
spacing-keys
stroke-width-keys
sizing-keys
opacity-keys
spacing-keys
dimensions-keys
axis-keys
rotation-keys
typography-keys
typography-token-keys))
typography-token-keys
number-keys))
(def ^:private schema:tokens
[:map {:title "GenericTokenAttrs"}])
@@ -380,28 +318,11 @@
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
@@ -443,13 +364,21 @@
(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))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; HELPERS for token attributes by shape type
;; TOKEN SHAPE ATTRIBUTES
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(def ^:private position-attributes #{:x :y})
(def position-attributes #{:x :y})
(def ^:private generic-attributes
(def generic-attributes
(set/union color-keys
stroke-width-keys
rotation-keys
@@ -458,22 +387,20 @@
shadow-keys
position-attributes))
(def ^:private rect-attributes
(def rect-attributes
(set/union generic-attributes
border-radius-keys))
(def ^:private frame-with-layout-attributes
(def frame-with-layout-attributes
(set/union rect-attributes
spacing-gap-padding-keys))
(def ^:private text-attributes
(def 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
@@ -489,14 +416,12 @@
nil))
(defn appliable-attrs-for-shape
"Returns which ones of the given `attributes` can be applied to a shape
of type `shape-type` and `is-layout`."
"Returns intersection of shape `attributes` for `shape-type`."
[attributes shape-type is-layout]
(set/intersection attributes (shape-type->attributes shape-type is-layout)))
(defn any-appliable-attr-for-shape?
"Returns if any of the given `attributes` can be applied to a shape
of type `shape-type` and `is-layout`."
"Checks if `token-type` supports given shape `attributes`."
[attributes token-type is-layout]
(d/not-empty? (appliable-attrs-for-shape attributes token-type is-layout)))
@@ -507,6 +432,42 @@
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}
@@ -532,48 +493,7 @@
:stroke-color #{:color}})
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; 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
;; TYPOGRAPHY
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn split-font-family
@@ -636,3 +556,32 @@
(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,19 +114,25 @@
[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 cto/schema:token-attrs)
[:and {:gen/gen (->> (sg/generator schema:token-attrs)
(sg/fmap #(make-token %)))}
(sm/required-keys cto/schema:token-attrs)
(sm/required-keys schema:token-attrs)
[:fn token?]])
(def ^:private check-token-attrs
(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))
(sm/check-fn schema:token-attrs :hint "expected valid params for token"))
(def check-token
(sm/check-fn schema:token :hint "expected valid token"))
@@ -311,13 +317,10 @@
[o]
(instance? TokenSetLegacy o))
(declare make-token-set)
(declare normalized-set-name?)
(def schema:token-set-attrs
[:map {:title "TokenSet"}
[:id ::sm/uuid]
[:name [:and :string [:fn #(normalized-set-name? %)]]]
[:name :string]
[:description {:optional true} :string]
[:modified-at {:optional true} ::ct/inst]
[:tokens {:optional true
@@ -339,6 +342,8 @@
: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 %)))}
@@ -399,25 +404,12 @@
(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.
@@ -1378,13 +1370,10 @@ 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 (tokens-lib? o) (valid? o)))
(and (instance? TokensLib o)
(valid? o)))
(defn- ensure-hidden-theme
"A helper that is responsible to ensure that the hidden theme always
@@ -1446,50 +1435,6 @@ 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 cfo]
[app.common.files.tokens :as cft]
[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} (cfo/parse-token-value "100.1")))
(t/is (= {:value -9.0 :unit nil} (cfo/parse-token-value "-9"))))
(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/testing "trims white-space"
(t/is (= {:value -1.3 :unit nil} (cfo/parse-token-value " -1.3 "))))
(t/is (= {:value -1.3 :unit nil} (cft/parse-token-value " -1.3 "))))
(t/testing "parses unit: px"
(t/is (= {:value 70.3 :unit "px"} (cfo/parse-token-value " 70.3px "))))
(t/is (= {:value 70.3 :unit "px"} (cft/parse-token-value " 70.3px "))))
(t/testing "parses unit: %"
(t/is (= {:value -10.0 :unit "%"} (cfo/parse-token-value "-10%"))))
(t/is (= {:value -10.0 :unit "%"} (cft/parse-token-value "-10%"))))
(t/testing "parses unit: px")
(t/testing "returns nil for any invalid characters"
(t/is (nil? (cfo/parse-token-value " -1.3a "))))
(t/is (nil? (cft/parse-token-value " -1.3a "))))
(t/testing "doesnt accept invalid double"
(t/is (nil? (cfo/parse-token-value ".3")))))
(t/is (nil? (cft/parse-token-value ".3")))))
(t/deftest token-applied-test
(t/testing "matches passed token with `:token-attributes`"
(t/is (true? (cfo/token-applied? {:name "a"} {:applied-tokens {:x "a"}} #{:x}))))
(t/is (true? (cft/token-applied? {:name "a"} {:applied-tokens {:x "a"}} #{:x}))))
(t/testing "doesn't match empty token"
(t/is (nil? (cfo/token-applied? {} {:applied-tokens {:x "a"}} #{:x}))))
(t/is (nil? (cft/token-applied? {} {:applied-tokens {:x "a"}} #{:x}))))
(t/testing "does't match passed token `:id`"
(t/is (nil? (cfo/token-applied? {:name "b"} {:applied-tokens {:x "a"}} #{:x}))))
(t/is (nil? (cft/token-applied? {:name "b"} {:applied-tokens {:x "a"}} #{:x}))))
(t/testing "doesn't match passed `:token-attributes`"
(t/is (nil? (cfo/token-applied? {:name "a"} {:applied-tokens {:x "a"}} #{:y})))))
(t/is (nil? (cft/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 (cfo/shapes-ids-by-applied-attributes {:name "1"} shapes attributes)]
expected (cft/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,21 +62,34 @@
shape-applied-x-y
shape-applied-all)))
(t/is (= (:z expected) (shape-ids shape-applied-all)))
(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)))
(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)))
(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? (cfo/shapes-token-applied? {:name "a"} [{:applied-tokens {:x "a"}}
(t/is (true? (cft/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? (cfo/shapes-token-applied? {:name "a"} [{:applied-tokens {:x "b"}}
(t/is (nil? (cft/shapes-token-applied? {:name "a"} [{:applied-tokens {:x "b"}}
{:applied-tokens {:x "b"}}]
#{:x})))
(t/is (nil? (cfo/shapes-token-applied? {:name "a"} [{:applied-tokens {:x "a"}}
(t/is (nil? (cft/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-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])))
(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])))
(:objects page)
{})
(cls/generate-update-shapes [(:id text1)]
(fn [shape]
(-> shape
(cto/unapply-tokens-from-shape [:font-size])
(cto/unapply-tokens-from-shape [:letter-spacing])
(cto/unapply-tokens-from-shape [:font-family])))
(cto/unapply-token-id [:font-size])
(cto/unapply-token-id [:letter-spacing])
(cto/unapply-token-id [:font-family])))
(:objects page)
{})
(cls/generate-update-shapes [(:id circle1)]
(fn [shape]
(-> shape
(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])))
(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])))
(:objects page)
{}))

View File

@@ -8,19 +8,20 @@
(: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/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")))
(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")))
;; Disallow trailing tokens
(t/is (false? (sm/validate cto/schema:token-name "Foo.Bar.Baz....")))
(t/is (false? (sm/validate cto/token-name-ref "Foo.Bar.Baz....")))
;; Disallow multiple separator dots
(t/is (false? (sm/validate cto/schema:token-name "Foo..Bar.Baz")))
(t/is (false? (sm/validate cto/token-name-ref "Foo..Bar.Baz")))
;; Disallow any special characters
(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"))))
(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"))))

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,11 +2013,3 @@
(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

@@ -36,7 +36,7 @@
{:path path
:mtype (mime/get type)
:name name
:filename (str/concat name (mime/get-extension type))
:filename (str/concat (str/slug name) (mime/get-extension type))
:id task-id}))
(defn create-zip

View File

@@ -14,7 +14,7 @@ test.beforeEach(async ({ page }) => {
await BaseWebSocketPage.mockRPC(page, "get-teams", "get-teams-tokens.json");
});
test.describe("Tokens - CRUD", () => {
test.describe("Tokens - creation", () => {
test("User creates border radius token", async ({ page }) => {
await testTokenCreationFlow(page, {
tokenLabel: "Border Radius",
@@ -1256,6 +1256,91 @@ test.describe("Tokens - CRUD", () => {
).toBeEnabled();
});
test("User creates grouped color token", async ({ page }) => {
const { workspacePage, tokensUpdateCreateModal, tokensSidebar } =
await setupEmptyTokensFile(page);
await tokensSidebar
.getByRole("button", { name: "Add Token: Color" })
.click();
// Create grouped color token with mouse
await expect(tokensUpdateCreateModal).toBeVisible();
const nameField = tokensUpdateCreateModal.getByLabel("Name");
const valueField = tokensUpdateCreateModal.getByLabel("Value");
await nameField.click();
await nameField.fill("dark.primary");
await valueField.click();
await valueField.fill("red");
const submitButton = tokensUpdateCreateModal.getByRole("button", {
name: "Save",
});
await expect(submitButton).toBeEnabled();
await submitButton.click();
await unfoldTokenTree(tokensSidebar, "color", "dark.primary");
await expect(tokensSidebar.getByLabel("primary")).toBeEnabled();
});
test("User cant create regular token with value missing", async ({
page,
}) => {
const { tokensUpdateCreateModal } = await setupEmptyTokensFile(page);
const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" });
await tokensTabPanel
.getByRole("button", { name: "Add Token: Color" })
.click();
await expect(tokensUpdateCreateModal).toBeVisible();
const nameField = tokensUpdateCreateModal.getByLabel("Name");
const submitButton = tokensUpdateCreateModal.getByRole("button", {
name: "Save",
});
// Initially submit button should be disabled
await expect(submitButton).toBeDisabled();
// Fill in name but leave value empty
await nameField.click();
await nameField.fill("primary");
// Submit button should remain disabled when value is empty
await expect(submitButton).toBeDisabled();
});
test("User duplicate color token", async ({ page }) => {
const { tokensSidebar, tokenContextMenuForToken } =
await setupTokensFile(page);
await expect(tokensSidebar).toBeVisible();
unfoldTokenTree(tokensSidebar, "color", "colors.blue.100");
const colorToken = tokensSidebar.getByRole("button", {
name: "100",
});
await colorToken.click({ button: "right" });
await expect(tokenContextMenuForToken).toBeVisible();
await tokenContextMenuForToken.getByText("Duplicate token").click();
await expect(tokenContextMenuForToken).not.toBeVisible();
await expect(
tokensSidebar.getByRole("button", { name: "colors.blue.100-copy" }),
).toBeVisible();
});
});
test.describe("Tokens tab - edition", () => {
test("User edits typography token and all fields are valid", async ({
page,
}) => {
@@ -1388,67 +1473,7 @@ test.describe("Tokens - CRUD", () => {
await expect(colorTokenChanged).toBeVisible();
});
test("User creates grouped color token", async ({ page }) => {
const { workspacePage, tokensUpdateCreateModal, tokensSidebar } =
await setupEmptyTokensFile(page);
await tokensSidebar
.getByRole("button", { name: "Add Token: Color" })
.click();
// Create grouped color token with mouse
await expect(tokensUpdateCreateModal).toBeVisible();
const nameField = tokensUpdateCreateModal.getByLabel("Name");
const valueField = tokensUpdateCreateModal.getByLabel("Value");
await nameField.click();
await nameField.fill("dark.primary");
await valueField.click();
await valueField.fill("red");
const submitButton = tokensUpdateCreateModal.getByRole("button", {
name: "Save",
});
await expect(submitButton).toBeEnabled();
await submitButton.click();
await unfoldTokenTree(tokensSidebar, "color", "dark.primary");
await expect(tokensSidebar.getByLabel("primary")).toBeEnabled();
});
test("User cant create regular token with value missing", async ({
page,
}) => {
const { tokensUpdateCreateModal } = await setupEmptyTokensFile(page);
const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" });
await tokensTabPanel
.getByRole("button", { name: "Add Token: Color" })
.click();
await expect(tokensUpdateCreateModal).toBeVisible();
const nameField = tokensUpdateCreateModal.getByLabel("Name");
const submitButton = tokensUpdateCreateModal.getByRole("button", {
name: "Save",
});
// Initially submit button should be disabled
await expect(submitButton).toBeDisabled();
// Fill in name but leave value empty
await nameField.click();
await nameField.fill("primary");
// Submit button should remain disabled when value is empty
await expect(submitButton).toBeDisabled();
});
test("User changes color token color while keeping custom color space", async ({
test("User edits color token color while keeping custom color space", async ({
page,
}) => {
const { workspacePage, tokensUpdateCreateModal, tokenThemesSetsSidebar } =
@@ -1502,30 +1527,9 @@ test.describe("Tokens - CRUD", () => {
await valueSaturationSelector.click({ position: { x: 0, y: 0 } });
await expect(valueField).toHaveValue(/^rgba(.*)$/);
});
});
test("User duplicate color token", async ({ page }) => {
const { tokensSidebar, tokenContextMenuForToken } =
await setupTokensFile(page);
await expect(tokensSidebar).toBeVisible();
unfoldTokenTree(tokensSidebar, "color", "colors.blue.100");
const colorToken = tokensSidebar.getByRole("button", {
name: "100",
});
await colorToken.click({ button: "right" });
await expect(tokenContextMenuForToken).toBeVisible();
await tokenContextMenuForToken.getByText("Duplicate token").click();
await expect(tokenContextMenuForToken).not.toBeVisible();
await expect(
tokensSidebar.getByRole("button", { name: "colors.blue.100-copy" }),
).toBeVisible();
});
test.describe("Tokens tab - delete", () => {
test("User delete color token", async ({ page }) => {
const { tokensSidebar, tokenContextMenuForToken } =
await setupTokensFile(page);
@@ -1546,4 +1550,40 @@ test.describe("Tokens - CRUD", () => {
await expect(tokenContextMenuForToken).not.toBeVisible();
await expect(colorToken).not.toBeVisible();
});
test("User removes node and all child tokens", async ({ page }) => {
const { tokensSidebar, workspacePage } = await setupTokensFile(page);
await expect(tokensSidebar).toBeVisible();
// Expand color tokens
unfoldTokenTree(tokensSidebar, "color", "colors.blue.100");
// Verify that the node and child token are visible before deletion
const colorNode = tokensSidebar.getByRole("button", {
name: "blue",
exact: true,
});
const colorNodeToken = tokensSidebar.getByRole("button", {
name: "100",
});
// Select a node and right click on it to open context menu
await expect(colorNode).toBeVisible();
await expect(colorNodeToken).toBeVisible();
await colorNode.click({ button: "right" });
// select "Delete" from the context menu
const deleteNodeButton = page.getByRole("button", {
name: "Delete",
exact: true,
});
await expect(deleteNodeButton).toBeVisible();
await deleteNodeButton.click();
// Verify that the node is removed
await expect(colorNode).not.toBeVisible();
// Verify that child token is also removed
await expect(colorNodeToken).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 cfo]
[app.common.files.tokens :as cft]
[app.common.logging :as l]
[app.common.schema :as sm]
[app.common.time :as ct]
@@ -83,7 +83,7 @@
[value]
(let [number? (or (number? value)
(numeric-string? value))
parsed-value (cfo/parse-token-value value)
parsed-value (cft/parse-token-value value)
out-of-bounds (or (>= (:value parsed-value) sm/max-safe-int)
(<= (:value parsed-value) sm/min-safe-int))]
@@ -109,7 +109,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 (cfo/parse-token-value value)
(let [parsed-value (cft/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))
@@ -127,7 +127,7 @@
If the `value` is parseable but is out of range returns a map with `warnings`."
[value]
(let [missing-references? (seq (seq (cto/find-token-value-references value)))
parsed-value (cfo/parse-token-value value)
parsed-value (cft/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))
@@ -151,7 +151,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 (cfo/parse-token-value value)
parsed-value (cft/parse-token-value value)
out-of-scope (< (:value parsed-value) 0)
references (seq (cto/find-token-value-references value))]
(cond
@@ -250,7 +250,7 @@
:font-size-value font-size-value})]
(or error
(try
(when-let [{:keys [unit value]} (cfo/parse-token-value line-height-value)]
(when-let [{:keys [unit value]} (cft/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 cfo]
[app.common.files.tokens :as cft]
[app.common.types.component :as ctk]
[app.common.types.shape.layout :as ctsl]
[app.common.types.shape.radius :as ctsr]
@@ -525,8 +525,8 @@
shape-ids (d/nilv (keys shapes) [])
any-variant? (->> shapes vals (some ctk/is-variant?) boolean)
resolved-value (get-in resolved-tokens [(cfo/token-identifier token) :resolved-value])
tokenized-attributes (cfo/attributes-map attributes token)
resolved-value (get-in resolved-tokens [(cft/token-identifier token) :resolved-value])
tokenized-attributes (cft/attributes-map attributes token)
type (:type token)]
(rx/concat
(rx/of
@@ -585,7 +585,7 @@
ptk/WatchEvent
(watch [_ _ _]
(rx/of
(let [remove-token #(when % (cfo/remove-attributes-for-token attributes token %))]
(let [remove-token #(when % (cft/remove-attributes-for-token attributes token %))]
(dwsh/update-shapes
shape-ids
(fn [shape]
@@ -613,7 +613,7 @@
(get token-properties (:type token))
unapply-tokens?
(cfo/shapes-token-applied? token shapes (or attrs all-attributes attributes))
(cft/shapes-token-applied? token shapes (or attrs all-attributes attributes))
shape-ids (map :id shapes)]
@@ -655,7 +655,7 @@
(get token-properties (:type token))
unapply-tokens?
(cfo/shapes-token-applied? token shapes (or attrs all-attributes attributes))
(cft/shapes-token-applied? token shapes (or attrs all-attributes attributes))
shape-ids (map :id shapes)]

View File

@@ -6,7 +6,7 @@
(ns app.main.data.workspace.tokens.color
(:require
[app.common.files.tokens :as cfo]
[app.common.files.tokens :as cft]
[app.main.data.tinycolor :as tinycolor]))
(defn color-bullet-color [token-color-value]
@@ -17,5 +17,5 @@
(tinycolor/->hex-string tc))))
(defn resolved-token-bullet-color [{:keys [resolved-value] :as token}]
(when (and resolved-value (cfo/color-token? token))
(when (and resolved-value (cft/color-token? token))
(color-bullet-color resolved-value)))

View File

@@ -149,30 +149,27 @@
(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/WatchEvent
(watch [it state _]
(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))))))
ptk/UpdateEvent
(update [_ state]
;; Clear possible local state
(update state :workspace-tokens dissoc :token-set-new-path))
(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))))))
(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))))))))
(defn rename-token-set-group
[set-group-path set-group-fname]
@@ -184,6 +181,26 @@
(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
@@ -416,11 +433,22 @@
ptk/WatchEvent
(watch [it state _]
(let [data (dsh/lookup-file-data state)
changes (-> (pcb/empty-changes it)
(pcb/with-library-data data)
(pcb/set-token set-id token-id nil))]
(rx/of (dch/commit-changes changes))))))
(defn bulk-delete-tokens
[set-id token-ids]
(dm/assert! (uuid? set-id))
(dm/assert! (every? uuid? token-ids))
(ptk/reify ::bulk-delete-tokens
ptk/WatchEvent
(watch [_ _ _]
(apply rx/of
(map #(delete-token set-id %) token-ids)))))
(defn duplicate-token
[token-id]
(dm/assert! (uuid? token-id))
@@ -433,7 +461,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) ;; TODO: add function duplicate-token in tokens-lib
unames (map :name tokens)
suffix (tr "workspace.tokens.duplicate-suffix")
copy-name (cfh/generate-unique-name (:name token) unames :suffix suffix)
new-token (-> token
@@ -488,6 +516,19 @@
(update state :workspace-tokens assoc :token-context-menu params)
(update state :workspace-tokens dissoc :token-context-menu)))))
(defn assign-token-node-context-menu
[{:keys [position] :as params}]
(when params
(assert (gpt/point? position) "expected a point instance for `position` param"))
(ptk/reify ::show-token-node-context-menu
ptk/UpdateEvent
(update [_ state]
(if params
(update state :workspace-tokens assoc :token-node-context-menu params)
(update state :workspace-tokens dissoc :token-node-context-menu)))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; TOKEN-SET UI OPS
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

View File

@@ -19,17 +19,19 @@
[:expandable {:optional true} :boolean]
[:expanded {:optional true} :boolean]
[:icon {:optional true} :string]
[:on-toggle-expand fn?]])
[:on-toggle-expand {:optional true} fn?]
[:on-context-menu {:optional true} fn?]])
(mf/defc layer-button*
{::mf/schema schema:layer-button}
[{:keys [label description class is-expandable expanded icon on-toggle-expand children] :rest props}]
[{:keys [label description class is-expandable expanded icon on-toggle-expand on-context-menu children] :rest props}]
(let [button-props (mf/spread-props props
{:class [class (stl/css-case :layer-button true
:layer-button--expandable is-expandable
:layer-button--expanded expanded)]
:type "button"
:on-click on-toggle-expand})]
:on-click on-toggle-expand
:on-context-menu on-context-menu})]
[:div {:class (stl/css :layer-button-wrapper)}
[:> "button" button-props
[:div {:class (stl/css :layer-button-content)}

View File

@@ -312,8 +312,8 @@
[]
(let [on-reload (mf/use-fn #(js/location.reload))]
[:> error-container* {}
[:div {:class (stl/css :main-message)} (tr "labels.webgl-context-lost.main-message")]
[:div {:class (stl/css :desc-message)} (tr "labels.webgl-context-lost.desc-message")]
[:div {:class (stl/css :main-message)} (tr "errors.webgl-context-lost.main-message")]
[:div {:class (stl/css :desc-message)} (tr "errors.webgl-context-lost.desc-message")]
[:div {:class (stl/css :buttons-container)}
[:> button* {:variant "primary" :on-click on-reload}
(tr "labels.reload-page")]]]))

View File

@@ -276,7 +276,11 @@
:wglobal wglobal
:layout layout}])
(when (or (not (and file-loaded? page-id))
(and wasm-renderer-enabled? (not @first-frame-rendered?)))
;; in wasm renderer, extend the pixel loader until the first frame is rendered
;; but do not apply it when switching pages
(and wasm-renderer-enabled?
(not file-loaded?)
(not @first-frame-rendered?)))
[:> workspace-loader*])]]]]]]))
(mf/defc workspace-page*

View File

@@ -13,6 +13,7 @@
[app.main.data.helpers :as dsh]
[app.main.data.modal :as modal]
[app.main.data.workspace :as dw]
[app.main.features :as features]
[app.main.refs :as refs]
[app.main.store :as st]
[app.main.ui.components.title-bar :refer [title-bar*]]
@@ -22,9 +23,11 @@
[app.main.ui.hooks :as hooks]
[app.main.ui.icons :as deprecated-icon]
[app.main.ui.notifications.badge :refer [badge-notification]]
[app.render-wasm.api :as wasm.api]
[app.util.dom :as dom]
[app.util.i18n :as i18n :refer [tr]]
[app.util.keyboard :as kbd]
[app.util.timers :as timers]
[cuerdas.core :as str]
[okulary.core :as l]
[rumext.v2 :as mf]))
@@ -52,6 +55,8 @@
refs/workspace-data
=))
;; --- Page Item
(mf/defc page-item
@@ -63,6 +68,22 @@
navigate-fn (mf/use-fn (mf/deps id) #(st/emit! :interrupt (dcm/go-to-workspace :page-id id)))
read-only? (mf/use-ctx ctx/workspace-read-only?)
on-click
(mf/use-fn
(mf/deps id)
(fn []
;; when using the wasm renderer, apply a blur effect to the viewport canvas
(if (features/active-feature? @st/state "render-wasm/v1")
(do
(wasm.api/capture-canvas-pixels)
(wasm.api/apply-canvas-blur)
;; NOTE: it seems we need two RAF so the blur is actually applied and visible
;; in the canvas :(
(timers/raf
(fn []
(timers/raf navigate-fn))))
(navigate-fn))))
on-delete
(mf/use-fn
(mf/deps id)
@@ -155,7 +176,7 @@
:selected selected?)
:data-testid (dm/str "page-" id)
:tab-index "0"
:on-click navigate-fn
:on-click on-click
:on-double-click on-double-click
:on-context-menu on-context-menu}
[:div {:class (stl/css :page-icon)}

View File

@@ -14,8 +14,10 @@
[app.main.ui.ds.foundations.typography.text :refer [text*]]
[app.main.ui.workspace.tokens.management.context-menu :refer [token-context-menu]]
[app.main.ui.workspace.tokens.management.group :refer [token-group*]]
[app.main.ui.workspace.tokens.management.node-context-menu :refer [token-node-context-menu*]]
[app.util.array :as array]
[app.util.i18n :refer [tr]]
[cuerdas.core :as str]
[rumext.v2 :as mf]))
(defn- get-sorted-token-groups
@@ -120,7 +122,27 @@
[empty-group filled-group]
(mf/with-memo [tokens-by-type]
(get-sorted-token-groups tokens-by-type))]
(get-sorted-token-groups tokens-by-type))
;; Filter tokens by their path and return their ids
filter-tokens-by-path-ids
(mf/use-fn
(mf/deps tokens)
(fn [type path]
(->> tokens
(filter (fn [token]
(let [[_ token-value] token]
(and (= (:type token-value) type) (str/starts-with? (:name token-value) path)))))
(mapv (fn [token]
(let [[_ token-value] token]
(:id token-value)))))))
delete-node
(mf/with-memo [tokens selected-token-set-id]
(fn [node type]
(let [path (:path node)
tokens-in-path-ids (filter-tokens-by-path-ids type path)]
(st/emit! (dwtl/bulk-delete-tokens selected-token-set-id tokens-in-path-ids)))))]
(mf/with-effect [tokens-lib selected-token-set-id]
(when (and tokens-lib
@@ -134,6 +156,7 @@
[:*
[:& token-context-menu]
[:> token-node-context-menu* {:on-delete-node delete-node}]
[:& selected-set-info* {:tokens-lib tokens-lib
:selected-token-set-id selected-token-set-id}]

View File

@@ -9,7 +9,7 @@
(:require
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.files.tokens :as cfo]
[app.common.files.tokens :as cft]
[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 (cfo/shapes-ids-by-applied-attributes token selected-shapes attributes)
(let [ids-by-attributes (cft/shapes-ids-by-applied-attributes token selected-shapes attributes)
shape-ids (into #{} (map :id selected-shapes))]
{:all-selected? (cfo/shapes-applied-all? ids-by-attributes shape-ids attributes)
{:all-selected? (cft/shapes-applied-all? ids-by-attributes shape-ids attributes)
:shape-ids shape-ids
:selected-pred #(seq (% ids-by-attributes))}))

View File

@@ -6,23 +6,22 @@
(ns app.main.ui.workspace.tokens.management.forms.color
(:require
[app.common.files.tokens :as cfo]
[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.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]
[app.util.i18n :refer [tr]]
[cuerdas.core :as str]
[rumext.v2 :as mf]))
#_(defn- token-value-error-fn
(defn- token-value-error-fn
[{:keys [value]}]
(when (or (str/empty? value)
(str/blank? value))
(tr "workspace.tokens.empty-input")))
#_(defn- make-schema
(defn- make-schema
[tokens-tree _]
(sm/schema
[:and
@@ -30,9 +29,9 @@
[: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")))
(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 (ctob/token-name-path-exists? % tokens-tree))]]]
#(not (cft/token-name-path-exists? % tokens-tree))]]]
[:value [::sm/text {:error/fn token-value-error-fn}]]
@@ -49,10 +48,6 @@
(mf/defc form*
[props]
(let [props (mf/spread-props props {:make-schema #(-> (sm/merge (cfo/make-token-schema %1)
[:map [:color-result {:optional true} ::sm/any]])
(sm/dissoc-key :id)
;;(sm/assoc-key :color-result {:optional true} ::sm/any) ;; TODO WTF does this not work?
)
(let [props (mf/spread-props props {:make-schema make-schema
:input-component token.controls/color-input*})]
[:> generic/form* props]))

View File

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

View File

@@ -7,6 +7,7 @@
(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]
@@ -27,7 +28,7 @@
token-path
(mf/with-memo [token]
(ctob/get-token-path token))
(cft/token-name->path (:name token)))
tokens-tree-in-selected-set
(mf/with-memo [token-path tokens-in-selected-set]

View File

@@ -8,11 +8,10 @@
(:require-macros [app.main.style :as stl])
(:require
[app.common.data :as d]
[app.common.files.tokens :as cfo]
[app.common.files.tokens :as cft]
[app.common.schema :as sm]
#_[app.common.types.token :as cto]
[app.common.types.token :as cto]
[app.common.types.tokens-lib :as ctob]
[app.common.uuid :as uuid]
[app.main.constants :refer [max-input-length]]
[app.main.data.helpers :as dh]
[app.main.data.modal :as modal]
@@ -37,12 +36,13 @@
[cuerdas.core :as str]
[rumext.v2 :as mf]))
#_(defn- token-value-error-fn
(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,7 +59,7 @@
value))
#_(defn- default-make-schema
(defn- default-make-schema
[tokens-tree _]
(sm/schema
[:and
@@ -67,9 +67,9 @@
[: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")))
(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 (ctob/token-name-path-exists? % tokens-tree))]]]
#(not (cft/token-name-path-exists? % tokens-tree))]]]
[:value [::sm/text {:error/fn token-value-error-fn}]]
@@ -80,7 +80,7 @@
:error/fn #(tr "workspace.tokens.self-reference")}
(fn [{:keys [name value]}]
(when (and name value)
(not (cto/token-value-self-reference? name value))))]]))
(nil? (cto/token-value-self-reference? name value))))]]))
(mf/defc form*
[{:keys [token
@@ -98,13 +98,12 @@
tokens-in-selected-set
input-value-placeholder] :as props}]
(let [make-schema (or make-schema #(-> (cfo/make-token-schema %)
(sm/dissoc-key :id))) ;; TODO this does not work because the schema is no longer a :map but a :multi
(let [make-schema (or make-schema default-make-schema)
input-component (or input-component token.controls/input*)
validate-token (or validator default-validate-token)
validate-token (or validator default-validate-token)
active-tab* (mf/use-state #(if (cfo/is-reference? token) :reference :composite))
active-tab (deref active-tab*)
active-tab* (mf/use-state #(if (cft/is-reference? token) :reference :composite))
active-tab (deref active-tab*)
on-toggle-tab
(mf/use-fn
@@ -140,13 +139,10 @@
initial
(mf/with-memo [token]
(-> (or initial
{:id (uuid/next) ;; TODO it should not be necessary if the sm/dissoc-key :id worked correctly
:type token-type
:name (:name token "")
(or initial
{:name (:name token "")
:value (:value token "")
:description (:description token "")})
(d/tap-r #(prn "initial" %))))
:description (:description token "")}))
form
(fm/use-form :schema schema

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/composite-token-reference? token-value) (default-validate-token params)
(cto/shadow-composite-token-reference? token-value) (default-validate-token params)
;; Validate composite token
:else
(let [params (-> params
@@ -253,7 +253,6 @@
[:> reference-form* {:token token
:tokens tokens}])]))
;; TODO: use cfo/schema:token-value and extend it with shadow and reference fields, adding :optional when needed
(defn- make-schema
[tokens-tree active-tab]
(sm/schema
@@ -263,10 +262,10 @@
[: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
(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 (ctob/token-name-path-exists? % tokens-tree))]]]
#(not (cft/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/composite-token-reference? token-value) (default-validate-token props)
(cto/typography-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/schema:token-name assoc
(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 (ctob/token-name-path-exists? % tokens-tree))]]]
#(not (cft/token-name-path-exists? % tokens-tree))]]]
[:value
[:map

View File

@@ -1,11 +1,13 @@
(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]))
@@ -27,8 +29,7 @@
;; 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/schema:token-name (:name token)))
(assoc :name "__PENPOT__TOKEN__NAME__PLACEHOLDER__"))
(not (sm/valid? cto/token-name-ref (:name token))) (assoc :name "__PENPOT__TOKEN__NAME__PLACEHOLDER__"))
tokens' (cond-> tokens
;; Remove previous token when renaming a token
(not= (:name token) (:name prev-token))
@@ -88,3 +89,23 @@
[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

@@ -88,7 +88,7 @@
expandable? (d/nilv (seq tokens) false)
on-context-menu
on-pill-context-menu
(mf/use-fn
(fn [event token]
(dom/prevent-default event)
@@ -98,6 +98,15 @@
:errors (:errors token)
:token-id (:id token)}))))
on-node-context-menu
(mf/use-fn
(fn [event node]
(dom/prevent-default event)
(st/emit! (dwtl/assign-token-node-context-menu
{:node node
:type type
:position (dom/get-client-position event)}))))
on-toggle-open-click
(mf/use-fn
(mf/deps type expandable?)
@@ -159,4 +168,5 @@
:selected-token-set-id selected-token-set-id
:is-selected-inside-layout is-selected-inside-layout
:on-token-pill-click on-token-pill-click
:on-context-menu on-context-menu}])]))
:on-pill-context-menu on-pill-context-menu
:on-node-context-menu on-node-context-menu}])]))

View File

@@ -0,0 +1,83 @@
(ns app.main.ui.workspace.tokens.management.node-context-menu
(:require-macros [app.main.style :as stl])
(:require
[app.common.data.macros :as dm]
[app.main.data.workspace.tokens.library-edit :as dwtl]
[app.main.refs :as refs]
[app.main.store :as st]
[app.main.ui.components.dropdown :refer [dropdown]]
[app.util.dom :as dom]
[app.util.i18n :refer [tr]]
[okulary.core :as l]
[rumext.v2 :as mf]))
(def ^:private schema:token-node-context-menu
[:map
[:on-delete-node fn?]])
(def ^:private tokens-node-menu-ref
(l/derived :token-node-context-menu refs/workspace-tokens))
(defn- prevent-default
[event]
(dom/prevent-default event)
(dom/stop-propagation event))
(mf/defc token-node-context-menu*
{::mf/schema schema:token-node-context-menu}
[{:keys [on-delete-node]}]
(let [mdata (mf/deref tokens-node-menu-ref)
is-open? (boolean mdata)
dropdown-ref (mf/use-ref)
dropdown-action (mf/use-ref)
dropdown-direction* (mf/use-state "down")
dropdown-direction (deref dropdown-direction*)
dropdown-direction-change* (mf/use-ref 0)
top (+ (get-in mdata [:position :y]) 5)
left (+ (get-in mdata [:position :x]) 5)
delete-node (mf/use-fn
(mf/deps mdata)
(fn []
(let [node (get mdata :node)
type (get mdata :type)]
(when node
(on-delete-node node type)))))]
(mf/with-effect [is-open?]
(when (and (not= 0 (mf/ref-val dropdown-direction-change*)) (= false is-open?))
(reset! dropdown-direction* "down")
(mf/set-ref-val! dropdown-direction-change* 0)))
(mf/with-effect [is-open? dropdown-ref dropdown-action]
(let [dropdown-element (mf/ref-val dropdown-ref)]
(when (and (= 0 (mf/ref-val dropdown-direction-change*)) dropdown-element)
(let [is-outside? (dom/is-element-outside? dropdown-element)]
(reset! dropdown-direction* (if is-outside? "up" "down"))
(mf/set-ref-val! dropdown-direction-change* (inc (mf/ref-val dropdown-direction-change*)))))))
;; FIXME: perf optimization
(when is-open?
(mf/portal
(mf/html
[:& dropdown {:show is-open?
:on-close #(st/emit! (dwtl/assign-token-node-context-menu nil))}
[:div {:class (stl/css :token-node-context-menu)
:data-testid "tokens-context-menu-for-token-node"
:ref dropdown-ref
:data-direction dropdown-direction
:style {:--bottom (if (= dropdown-direction "up")
"40px"
"unset")
:--top (dm/str top "px")
:left (dm/str left "px")}
:on-context-menu prevent-default}
(when mdata
[:ul {:class (stl/css :token-node-context-menu-list)}
[:li {:class (stl/css :token-node-context-menu-listitem)}
[:button {:class (stl/css :token-node-context-menu-action)
:type "button"
:on-click delete-node}
(tr "labels.delete")]]])]])
(dom/get-body)))))

View File

@@ -0,0 +1,70 @@
// 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
@use "ds/_utils.scss" as *;
@use "ds/_sizes.scss" as *;
@use "ds/_borders.scss" as *;
@use "ds/typography.scss" as t;
@use "ds/spacing.scss" as *;
@use "ds/mixins.scss" as *;
.token-node-context-menu {
--menu-inline-size: #{px2rem(240)};
position: absolute;
z-index: var(--z-index-dropdown);
}
.token-node-context-menu[data-direction="up"] {
bottom: var(--bottom);
}
.token-node-context-menu[data-direction="down"] {
top: var(--top);
}
.token-node-context-menu-list {
inline-size: var(--menu-inline-size);
padding: var(--sp-xs);
border-radius: $br-8;
border: $b-2 solid var(--color-background-quaternary);
background-color: var(--color-background-tertiary);
max-block-size: 100vh;
overflow-y: auto;
box-shadow: 0px 0px $sz-12 0px var(--menu-shadow-color);
}
.token-node-context-menu-action {
--context-menu-item-bg-color: none;
--context-menu-item-fg-color: var(--color-foreground-primary);
--context-menu-item-border-color: none;
@include t.use-typography("body-small");
appearance: none;
background: var(--context-menu-item-bg-color);
border: $b-1 solid var(--context-menu-item-border-color);
color: var(--context-menu-item-fg-color);
border-radius: $br-8;
cursor: pointer;
block-size: px2rem(32);
inline-size: 100%;
display: flex;
align-items: center;
padding: var(--sp-xs);
&:hover {
--context-menu-item-bg-color: var(--color-background-quaternary);
}
&:focus {
--context-menu-item-bg-color: var(--menu-background-color-focus);
--context-menu-item-border-color: var(--color-background-tertiary);
}
&[aria-selected="true"] {
--context-menu-item-bg-color: var(--color-background-quaternary);
}
}

View File

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

View File

@@ -10,6 +10,7 @@
[app.common.path-names :as cpn]
[app.common.types.tokens-lib :as ctob]
[app.main.data.workspace.tokens.library-edit :as dwtl]
[app.main.refs :as refs]
[app.main.store :as st]
[app.main.ui.ds.layers.layer-button :refer [layer-button*]]
[app.main.ui.workspace.tokens.management.token-pill :refer [token-pill*]]
@@ -26,7 +27,8 @@
[:selected-token-set-id {:optional true} :any]
[:tokens-lib {:optional true} :any]
[:on-token-pill-click {:optional true} fn?]
[:on-context-menu {:optional true} fn?]])
[:on-pill-context-menu {:optional true} fn?]
[:on-node-context-menu {:optional true} fn?]])
(mf/defc folder-node*
{::mf/schema schema:folder-node}
@@ -39,22 +41,29 @@
selected-token-set-id
tokens-lib
on-token-pill-click
on-context-menu]}]
on-pill-context-menu
on-node-context-menu]}]
(let [full-path (str (name type) "." (:path node))
is-folder-expanded (contains? (set (or unfolded-token-paths [])) full-path)
swap-folder-expanded (mf/use-fn
(mf/deps (:path node) type)
(fn []
(let [path (str (name type) "." (:path node))]
(st/emit! (dwtl/toggle-token-path path)))))]
(st/emit! (dwtl/toggle-token-path path)))))
node-context-menu-prep (mf/use-fn
(mf/deps on-node-context-menu node)
(fn [event]
(when on-node-context-menu
(on-node-context-menu event node))))]
[:li {:class (stl/css :folder-node)}
[:> layer-button* {:label (:name node)
:expanded is-folder-expanded
:aria-expanded is-folder-expanded
:aria-controls (str "folder-children-" (:path node))
:is-expandable (not (:leaf node))
:on-toggle-expand swap-folder-expanded}]
:on-toggle-expand swap-folder-expanded
:on-context-menu node-context-menu-prep}]
(when is-folder-expanded
(let [children-fn (:children-fn node)]
[:div {:class (stl/css :folder-children-wrapper)
@@ -63,16 +72,17 @@
(let [children (children-fn)]
(for [child children]
(if (not (:leaf child))
[:ul {:class (stl/css :node-parent)}
[:> folder-node* {:key (:path child)
:type type
[:ul {:class (stl/css :node-parent)
:key (:path child)}
[:> folder-node* {:type type
:node child
:unfolded-token-paths unfolded-token-paths
:selected-shapes selected-shapes
:is-selected-inside-layout is-selected-inside-layout
:active-theme-tokens active-theme-tokens
:on-token-pill-click on-token-pill-click
:on-context-menu on-context-menu
:on-pill-context-menu on-pill-context-menu
:on-node-context-menu on-node-context-menu
:tokens-lib tokens-lib
:selected-token-set-id selected-token-set-id}]]
(let [id (:id (:leaf child))
@@ -84,7 +94,7 @@
:is-selected-inside-layout is-selected-inside-layout
:active-theme-tokens active-theme-tokens
:on-click on-token-pill-click
:on-context-menu on-context-menu}])))))]))]))
:on-context-menu on-pill-context-menu}])))))]))]))
(def ^:private schema:token-tree
[:map
@@ -97,7 +107,8 @@
[:selected-token-set-id {:optional true} :any]
[:tokens-lib {:optional true} :any]
[:on-token-pill-click {:optional true} fn?]
[:on-context-menu {:optional true} fn?]])
[:on-pill-context-menu {:optional true} fn?]
[:on-node-context-menu {:optional true} fn?]])
(mf/defc token-tree*
{::mf/schema schema:token-tree}
@@ -110,12 +121,19 @@
tokens-lib
selected-token-set-id
on-token-pill-click
on-context-menu]}]
on-pill-context-menu
on-node-context-menu]}]
(let [separator "."
tree (mf/use-memo
(mf/deps tokens)
(fn []
(cpn/build-tree-root tokens separator)))]
(cpn/build-tree-root tokens separator)))
can-edit? (:can-edit (deref refs/permissions))
on-node-context-menu (mf/use-fn
(mf/deps can-edit? on-node-context-menu)
(fn [event node]
(when can-edit?
(on-node-context-menu event node))))]
[:div {:class (stl/css :token-tree-wrapper)}
(for [node tree]
(if (:leaf node)
@@ -127,7 +145,7 @@
:is-selected-inside-layout is-selected-inside-layout
:active-theme-tokens active-theme-tokens
:on-click on-token-pill-click
:on-context-menu on-context-menu}])
:on-context-menu on-pill-context-menu}])
;; Render segment folder
[:ul {:class (stl/css :node-parent)
:key (:path node)}
@@ -138,6 +156,7 @@
:is-selected-inside-layout is-selected-inside-layout
:active-theme-tokens active-theme-tokens
:on-token-pill-click on-token-pill-click
:on-context-menu on-context-menu
:on-node-context-menu on-node-context-menu
:on-pill-context-menu on-pill-context-menu
:tokens-lib tokens-lib
:selected-token-set-id selected-token-set-id}]]))]))

View File

@@ -62,8 +62,7 @@
(st/emit! (dwtl/start-token-set-edition id)))))]
[:> controlled-sets-list*
{:tokens-lib tokens-lib
:token-sets token-sets
{:token-sets token-sets
:is-token-set-active token-set-active?
:is-token-set-group-active token-set-group-active?
@@ -80,6 +79,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 (partial sets-helpers/on-update-token-set tokens-lib)
:on-update-token-set sets-helpers/on-update-token-set
: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,13 +1,9 @@
(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]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@@ -15,18 +11,9 @@
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn on-update-token-set
[tokens-lib token-set name]
(let [name (ctob/normalize-set-name name)
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})))))
[token-set name]
(st/emit! (dwtl/clear-token-set-edition)
(dwtl/update-token-set token-set name)))
(defn on-update-token-set-group
[path name]
@@ -34,15 +21,15 @@
(dwtl/rename-token-set-group path name)))
(defn on-create-token-set
[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))]
[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)]
(st/emit! (ptk/data-event ::ev/event {::ev/name "create-token-set" :name name})
(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})))))
(dwtl/create-token-set token-set))))

View File

@@ -321,7 +321,6 @@
on-select
on-toggle-set
on-toggle-set-group
tokens-lib
token-sets
new-path
edition-id]}]
@@ -409,7 +408,7 @@
:on-drop on-drop
:on-reset-edition on-reset-edition
:on-edit-submit (partial sets-helpers/on-create-token-set tokens-lib)}]
:on-edit-submit sets-helpers/on-create-token-set}]
:else
[:> sets-tree-set*
@@ -435,8 +434,7 @@
:on-edit-submit on-edit-submit-set}])))))
(mf/defc controlled-sets-list*
[{:keys [tokens-lib
token-sets
[{:keys [token-sets
selected
on-update-token-set
on-update-token-set-group
@@ -488,7 +486,6 @@
{: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,11 +7,8 @@
(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]
@@ -33,9 +30,32 @@
[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
@@ -179,43 +199,26 @@
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 tokens-lib current-name)
(mf/deps on-change-field)
(fn [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 "")))))
(reset! current-group* value)
(on-change-field :group value)))
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 (sm/validation-errors value (cfo/make-token-theme-name-schema
tokens-lib
current-group
(ctob/get-id theme)))]
errors (validate-theme-name tokens-lib current-group (ctob/get-id theme) value)]
(reset! name-errors* errors)
(mf/set-ref-val! theme-name-ref value)
(if (empty? errors)
(do
(reset! current-name* value)
(on-change-field :name value))
(on-change-field :name value)
(on-change-field :name "")))))]
[:div {:class (stl/css :edit-theme-inputs-wrapper)}
@@ -225,7 +228,6 @@
: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)}
@@ -278,7 +280,6 @@
(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)
@@ -380,8 +381,7 @@
[:div {:class (stl/css :sets-list-wrapper)}
[:> wts/controlled-sets-list*
{:tokens-lib tokens-lib
:token-sets token-sets
{: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

@@ -312,6 +312,11 @@
(js/console.error "Error initializing canvas context:" e)
false))]
(reset! canvas-init? init?)
(when init?
;; Restore previous canvas pixels immediately after context initialization
;; This happens before initialize-viewport is called
(wasm.api/apply-canvas-blur)
(wasm.api/restore-previous-canvas-pixels))
(when-not init?
(js/alert "WebGL not supported")
(st/emit! (dcm/go-to-dashboard-recent))))))))
@@ -340,6 +345,7 @@
(mf/with-effect [@canvas-init? zoom vbox background]
(when (and @canvas-init? (not @initialized?))
(wasm.api/clear-canvas-pixels)
(wasm.api/initialize-viewport base-objects zoom vbox background)
(reset! initialized? true)))

View File

@@ -7,22 +7,18 @@
(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.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)
@@ -54,13 +50,15 @@
(ctob/get-name token)))
:set
(fn [_ value]
(let [name (u/coerce-1 value
(cfo/make-token-name-schema
(-> (u/locate-tokens-lib file-id)
(ctob/get-tokens set-id)))
:name
"Invalid token name")]
(when name
(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})))))}
:type
@@ -86,11 +84,6 @@
: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
@@ -111,13 +104,9 @@
(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
[plugin-id file-id id]
(obj/reify {:name "TokenSetProxy"
:wrap u/wrap-errors}
(obj/reify {:name "TokenSetProxy"}
:$plugin {:enumerable false :get (constantly plugin-id)}
:$file-id {:enumerable false :get (constantly file-id)}
:$id {:enumerable false :get (constantly id)}
@@ -133,15 +122,13 @@
(ctob/get-name set)))
:set
(fn [_ value]
(let [set (u/locate-token-set file-id id)
name (u/coerce-1 value
(cfo/make-token-set-name-schema
(u/locate-tokens-lib file-id)
id)
:setTokenSet
"Invalid token set name")]
(when name
(st/emit! (dwtl/rename-token-set set 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)))))}
:active
{:this true
@@ -153,13 +140,8 @@
(ctob/token-set-active? tokens-lib (ctob/get-name set))))
:set
(fn [_ value]
(let [value (u/coerce-1 value
(sm/schema [:boolean])
:setActiveSet
value)]
(when (some? value)
(let [set (u/locate-token-set file-id id)]
(st/emit! (dwtl/set-enabled-token-set (ctob/get-name set) value))))))}
(let [set (u/locate-token-set file-id id)]
(st/emit! (dwtl/set-enabled-token-set (ctob/get-name set) value))))}
:toggleActive
(fn [_]
@@ -171,7 +153,8 @@
:enumerable false
:get
(fn [_]
(let [tokens-lib (u/locate-tokens-lib file-id)]
(let [file (u/locate-file file-id)
tokens-lib (->> file :data :tokens-lib)]
(->> (ctob/get-tokens tokens-lib id)
(vals)
(map #(token-proxy plugin-id file-id id (:id %)))
@@ -182,7 +165,8 @@
:enumerable false
:get
(fn [_]
(let [tokens-lib (u/locate-tokens-lib file-id)
(let [file (u/locate-file file-id)
tokens-lib (->> file :data :tokens-lib)
tokens (ctob/get-tokens tokens-lib id)]
(->> tokens
(vals)
@@ -209,36 +193,35 @@
(token-proxy plugin-id file-id id token-id)))))
:addToken
{:schema [:tuple (-> (cfo/make-token-schema
(-> (u/locate-tokens-lib file-id)
(ctob/get-tokens id)))
;; 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)
;; 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-params {: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 set) (:id token))))}
(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))))))
:duplicate
(fn []
(st/emit! (dwtl/duplicate-token-set id)))
(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'))))
:remove
(fn []
@@ -269,15 +252,12 @@
(:group theme)))
:set
(fn [_ value]
(let [theme (u/locate-token-theme file-id id)
group (u/coerce-1 value
(cfo/make-token-theme-group-schema
(u/locate-tokens-lib file-id)
(:name theme)
(:id theme))
:group
"Invalid token theme group")]
(when 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))))))}
:name
@@ -289,14 +269,16 @@
:set
(fn [_ value]
(let [theme (u/locate-token-theme file-id id)
name (u/coerce-1 value
(cfo/make-token-theme-name-schema
(u/locate-tokens-lib file-id)
(:id theme)
(:group theme))
:name
"Invalid token theme name")]
(when name
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))))))}
:active
@@ -351,7 +333,8 @@
:enumerable false
:get
(fn [_]
(let [tokens-lib (u/locate-tokens-lib file-id)
(let [file (u/locate-file file-id)
tokens-lib (->> file :data :tokens-lib)
themes (->> (ctob/get-themes tokens-lib)
(remove #(= (:id %) uuid/zero)))]
(apply array (map #(token-theme-proxy plugin-id file-id (ctob/get-id %)) themes))))}
@@ -361,36 +344,36 @@
:enumerable false
:get
(fn [_]
(let [tokens-lib (u/locate-tokens-lib file-id)
(let [file (u/locate-file file-id)
tokens-lib (->> file :data :tokens-lib)
sets (ctob/get-sets tokens-lib)]
(apply array (map #(token-set-proxy plugin-id file-id (ctob/get-id %)) sets))))}
:addTheme
(fn [attrs]
(let [schema (-> (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
attrs (u/coerce attrs schema :addTheme "invalid theme attrs")]
(when attrs
(let [theme (ctob/make-token-theme attrs)]
(st/emit! (dwtl/create-token-theme theme))
(token-theme-proxy plugin-id file-id (:id theme))))))
(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)))))
:addSet
(fn [attrs]
(obj/update! attrs "name" ctob/normalize-set-name) ;; TODO: seems a quite weird way of doing this
(let [schema (-> (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
attrs (u/coerce attrs schema :addSet "invalid set attrs")]
(when attrs
(let [set (ctob/make-token-set attrs)]
(st/emit! (dwtl/create-token-set set))
(token-set-proxy plugin-id file-id (ctob/get-id set))))))
(fn [name]
(cond
(not (string? name))
(u/display-not-valid :addSetName name)
: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)))))
:getThemeById
(fn [theme-id]

View File

@@ -9,15 +9,12 @@
(: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]
[cuerdas.core :as str]))
[app.util.object :as obj]))
(defn locate-file
[id]
@@ -221,8 +218,7 @@
(defn display-not-valid
[code value]
(.error js/console (dm/str "[PENPOT PLUGIN] Value not valid: " value ". Code: " code))
nil)
(.error js/console (dm/str "[PENPOT PLUGIN] Value not valid: " value ". Code: " code)))
(defn reject-not-valid
[reject code value]
@@ -230,43 +226,7 @@
(.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)))
nil)))))

View File

@@ -29,6 +29,7 @@
[app.main.worker :as mw]
[app.render-wasm.api.fonts :as f]
[app.render-wasm.api.texts :as t]
[app.render-wasm.api.webgl :as webgl]
[app.render-wasm.deserializers :as dr]
[app.render-wasm.helpers :as h]
[app.render-wasm.mem :as mem]
@@ -37,7 +38,6 @@
[app.render-wasm.serializers :as sr]
[app.render-wasm.serializers.color :as sr-clr]
[app.render-wasm.svg-filters :as svg-filters]
;; FIXME: rename; confunsing name
[app.render-wasm.wasm :as wasm]
[app.util.debug :as dbg]
[app.util.dom :as dom]
@@ -279,30 +279,6 @@
[string]
(+ (count string) 1))
(defn- create-webgl-texture-from-image
"Creates a WebGL texture from an HTMLImageElement or ImageBitmap and returns the texture object"
[gl image-element]
(let [texture (.createTexture ^js gl)]
(.bindTexture ^js gl (.-TEXTURE_2D ^js gl) texture)
(.texParameteri ^js gl (.-TEXTURE_2D ^js gl) (.-TEXTURE_WRAP_S ^js gl) (.-CLAMP_TO_EDGE ^js gl))
(.texParameteri ^js gl (.-TEXTURE_2D ^js gl) (.-TEXTURE_WRAP_T ^js gl) (.-CLAMP_TO_EDGE ^js gl))
(.texParameteri ^js gl (.-TEXTURE_2D ^js gl) (.-TEXTURE_MIN_FILTER ^js gl) (.-LINEAR ^js gl))
(.texParameteri ^js gl (.-TEXTURE_2D ^js gl) (.-TEXTURE_MAG_FILTER ^js gl) (.-LINEAR ^js gl))
(.texImage2D ^js gl (.-TEXTURE_2D ^js gl) 0 (.-RGBA ^js gl) (.-RGBA ^js gl) (.-UNSIGNED_BYTE ^js gl) image-element)
(.bindTexture ^js gl (.-TEXTURE_2D ^js gl) nil)
texture))
(defn- get-webgl-context
"Gets the WebGL context from the WASM module"
[]
(when wasm/context-initialized?
(let [gl-obj (unchecked-get wasm/internal-module "GL")]
(when gl-obj
;; Get the current WebGL context from Emscripten
;; The GL object has a currentContext property that contains the context handle
(let [current-ctx (.-currentContext ^js gl-obj)]
(when current-ctx
(.-GLctx ^js current-ctx)))))))
(defn- get-texture-id-for-gl-object
"Registers a WebGL texture with Emscripten's GL object system and returns its ID"
@@ -332,8 +308,8 @@
(->> (retrieve-image url)
(rx/map
(fn [img]
(when-let [gl (get-webgl-context)]
(let [texture (create-webgl-texture-from-image gl img)
(when-let [gl (webgl/get-webgl-context)]
(let [texture (webgl/create-webgl-texture-from-image gl img)
texture-id (get-texture-id-for-gl-object texture)
width (.-width ^js img)
height (.-height ^js img)
@@ -979,6 +955,7 @@
(set-shape-grow-type grow-type))
(set-shape-layout shape)
(set-layout-data shape)
(set-shape-selrect selrect)
(let [pending_thumbnails (into [] (concat
@@ -1055,8 +1032,9 @@
(perf/end-measure "set-objects")
(process-pending shapes thumbnails full noop-fn
(fn []
(when render-callback (render-callback))
(render-finish)
(if render-callback
(render-callback)
(render-finish))
(ug/dispatch! (ug/event "penpot:wasm:set-objects")))))))
(defn clear-focus-mode
@@ -1384,8 +1362,9 @@
all-children
(->> ids
(mapcat #(cfh/get-children-with-self objects %)))]
(h/call wasm/internal-module "_init_shapes_pool" (count all-children))
(run! (partial set-object objects) all-children)
(run! set-object all-children)
(let [content (-> (calculate-bool* bool-type ids)
(path.impl/path-data))]
@@ -1448,6 +1427,12 @@
result)))
(defn apply-canvas-blur
[]
(when wasm/canvas
(dom/set-style! wasm/canvas "filter" "blur(4px)")))
(defn init-wasm-module
[module]
(let [default-fn (unchecked-get module "default")
@@ -1469,3 +1454,8 @@
(js/console.error cause)
(p/resolved false)))))
(p/resolved false))))
;; Re-export public WebGL functions
(def capture-canvas-pixels webgl/capture-canvas-pixels)
(def restore-previous-canvas-pixels webgl/restore-previous-canvas-pixels)
(def clear-canvas-pixels webgl/clear-canvas-pixels)

View File

@@ -0,0 +1,166 @@
;; 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.render-wasm.api.webgl
"WebGL utilities for pixel capture and rendering"
(:require
[app.common.logging :as log]
[app.render-wasm.wasm :as wasm]
[app.util.dom :as dom]))
(defn get-webgl-context
"Gets the WebGL context from the WASM module"
[]
(when wasm/context-initialized?
(let [gl-obj (unchecked-get wasm/internal-module "GL")]
(when gl-obj
;; Get the current WebGL context from Emscripten
;; The GL object has a currentContext property that contains the context handle
(let [current-ctx (.-currentContext ^js gl-obj)]
(when current-ctx
(.-GLctx ^js current-ctx)))))))
(defn create-webgl-texture-from-image
"Creates a WebGL texture from an HTMLImageElement or ImageBitmap and returns the texture object"
[gl image-element]
(let [texture (.createTexture ^js gl)]
(.bindTexture ^js gl (.-TEXTURE_2D ^js gl) texture)
(.texParameteri ^js gl (.-TEXTURE_2D ^js gl) (.-TEXTURE_WRAP_S ^js gl) (.-CLAMP_TO_EDGE ^js gl))
(.texParameteri ^js gl (.-TEXTURE_2D ^js gl) (.-TEXTURE_WRAP_T ^js gl) (.-CLAMP_TO_EDGE ^js gl))
(.texParameteri ^js gl (.-TEXTURE_2D ^js gl) (.-TEXTURE_MIN_FILTER ^js gl) (.-LINEAR ^js gl))
(.texParameteri ^js gl (.-TEXTURE_2D ^js gl) (.-TEXTURE_MAG_FILTER ^js gl) (.-LINEAR ^js gl))
(.texImage2D ^js gl (.-TEXTURE_2D ^js gl) 0 (.-RGBA ^js gl) (.-RGBA ^js gl) (.-UNSIGNED_BYTE ^js gl) image-element)
(.bindTexture ^js gl (.-TEXTURE_2D ^js gl) nil)
texture))
;; FIXME: temporary function until we are able to keep the same <canvas> across pages.
(defn- draw-imagedata-to-webgl
"Draws ImageData to a WebGL2 context by creating a texture"
[gl image-data]
(let [width (.-width ^js image-data)
height (.-height ^js image-data)
texture (.createTexture ^js gl)]
;; Bind texture and set parameters
(.bindTexture ^js gl (.-TEXTURE_2D ^js gl) texture)
(.texParameteri ^js gl (.-TEXTURE_2D ^js gl) (.-TEXTURE_WRAP_S ^js gl) (.-CLAMP_TO_EDGE ^js gl))
(.texParameteri ^js gl (.-TEXTURE_2D ^js gl) (.-TEXTURE_WRAP_T ^js gl) (.-CLAMP_TO_EDGE ^js gl))
(.texParameteri ^js gl (.-TEXTURE_2D ^js gl) (.-TEXTURE_MIN_FILTER ^js gl) (.-LINEAR ^js gl))
(.texParameteri ^js gl (.-TEXTURE_2D ^js gl) (.-TEXTURE_MAG_FILTER ^js gl) (.-LINEAR ^js gl))
(.texImage2D ^js gl (.-TEXTURE_2D ^js gl) 0 (.-RGBA ^js gl) (.-RGBA ^js gl) (.-UNSIGNED_BYTE ^js gl) image-data)
;; Set up viewport
(.viewport ^js gl 0 0 width height)
;; Vertex & Fragment shaders
;; Since we are only calling this function once (on page switch), we don't need
;; to cache the compiled shaders somewhere else (cannot be reused in a differen context).
(let [vertex-shader-source "#version 300 es
in vec2 a_position;
in vec2 a_texCoord;
out vec2 v_texCoord;
void main() {
gl_Position = vec4(a_position, 0.0, 1.0);
v_texCoord = a_texCoord;
}"
fragment-shader-source "#version 300 es
precision highp float;
in vec2 v_texCoord;
uniform sampler2D u_texture;
out vec4 fragColor;
void main() {
fragColor = texture(u_texture, v_texCoord);
}"
vertex-shader (.createShader ^js gl (.-VERTEX_SHADER ^js gl))
fragment-shader (.createShader ^js gl (.-FRAGMENT_SHADER ^js gl))
program (.createProgram ^js gl)]
(.shaderSource ^js gl vertex-shader vertex-shader-source)
(.compileShader ^js gl vertex-shader)
(when-not (.getShaderParameter ^js gl vertex-shader (.-COMPILE_STATUS ^js gl))
(log/error :hint "Vertex shader compilation failed"
:log (.getShaderInfoLog ^js gl vertex-shader)))
(.shaderSource ^js gl fragment-shader fragment-shader-source)
(.compileShader ^js gl fragment-shader)
(when-not (.getShaderParameter ^js gl fragment-shader (.-COMPILE_STATUS ^js gl))
(log/error :hint "Fragment shader compilation failed"
:log (.getShaderInfoLog ^js gl fragment-shader)))
(.attachShader ^js gl program vertex-shader)
(.attachShader ^js gl program fragment-shader)
(.linkProgram ^js gl program)
(if (.getProgramParameter ^js gl program (.-LINK_STATUS ^js gl))
(do
(.useProgram ^js gl program)
;; Create full-screen quad vertices (normalized device coordinates)
(let [position-location (.getAttribLocation ^js gl program "a_position")
texcoord-location (.getAttribLocation ^js gl program "a_texCoord")
position-buffer (.createBuffer ^js gl)
texcoord-buffer (.createBuffer ^js gl)
positions #js [-1.0 -1.0 1.0 -1.0 -1.0 1.0 -1.0 1.0 1.0 -1.0 1.0 1.0]
texcoords #js [0.0 0.0 1.0 0.0 0.0 1.0 0.0 1.0 1.0 0.0 1.0 1.0]]
;; Set up position buffer
(.bindBuffer ^js gl (.-ARRAY_BUFFER ^js gl) position-buffer)
(.bufferData ^js gl (.-ARRAY_BUFFER ^js gl) (js/Float32Array. positions) (.-STATIC_DRAW ^js gl))
(.enableVertexAttribArray ^js gl position-location)
(.vertexAttribPointer ^js gl position-location 2 (.-FLOAT ^js gl) false 0 0)
;; Set up texcoord buffer
(.bindBuffer ^js gl (.-ARRAY_BUFFER ^js gl) texcoord-buffer)
(.bufferData ^js gl (.-ARRAY_BUFFER ^js gl) (js/Float32Array. texcoords) (.-STATIC_DRAW ^js gl))
(.enableVertexAttribArray ^js gl texcoord-location)
(.vertexAttribPointer ^js gl texcoord-location 2 (.-FLOAT ^js gl) false 0 0)
;; Set texture uniform
(.activeTexture ^js gl (.-TEXTURE0 ^js gl))
(.bindTexture ^js gl (.-TEXTURE_2D ^js gl) texture)
(let [texture-location (.getUniformLocation ^js gl program "u_texture")]
(.uniform1i ^js gl texture-location 0))
;; draw
(.drawArrays ^js gl (.-TRIANGLES ^js gl) 0 6)
;; cleanup
(.deleteBuffer ^js gl position-buffer)
(.deleteBuffer ^js gl texcoord-buffer)
(.deleteShader ^js gl vertex-shader)
(.deleteShader ^js gl fragment-shader)
(.deleteProgram ^js gl program)))
(log/error :hint "Program linking failed"
:log (.getProgramInfoLog ^js gl program)))
(.bindTexture ^js gl (.-TEXTURE_2D ^js gl) nil)
(.deleteTexture ^js gl texture))))
(defn restore-previous-canvas-pixels
"Restores previous canvas pixels into the new canvas"
[]
(when-let [previous-canvas-pixels wasm/canvas-pixels]
(when-let [gl wasm/gl-context]
(draw-imagedata-to-webgl gl previous-canvas-pixels)
(set! wasm/canvas-pixels nil))))
(defn clear-canvas-pixels
[]
(when wasm/canvas
(let [context wasm/gl-context]
(.clearColor ^js context 0 0 0 0.0)
(.clear ^js context (.-COLOR_BUFFER_BIT ^js context))
(.clear ^js context (.-DEPTH_BUFFER_BIT ^js context))
(.clear ^js context (.-STENCIL_BUFFER_BIT ^js context)))
(dom/set-style! wasm/canvas "filter" "none")
(set! wasm/canvas-pixels nil)))
(defn capture-canvas-pixels
"Captures the pixels of the viewport canvas"
[]
(when wasm/canvas
(let [context wasm/gl-context
width (.-width wasm/canvas)
height (.-height wasm/canvas)
buffer (js/Uint8ClampedArray. (* width height 4))
_ (.readPixels ^js context 0 0 width height (.-RGBA ^js context) (.-UNSIGNED_BYTE ^js context) buffer)
image-data (js/ImageData. buffer width height)]
(set! wasm/canvas-pixels image-data))))

View File

@@ -12,6 +12,8 @@
;; Reference to the HTML canvas element.
(defonce canvas nil)
;; Reference to the captured pixels of the canvas (for page switching effect)
(defonce canvas-pixels nil)
;; Reference to the Emscripten GL context wrapper.
(defonce gl-context-handle nil)
@@ -56,3 +58,4 @@
:stroke-linecap shared/RawStrokeLineCap
:stroke-linejoin shared/RawStrokeLineJoin
:fill-rule shared/RawFillRule})

View File

@@ -19,7 +19,6 @@
(defn- translate-code
[code]
(prn "code" code)
(if (vector? code)
(tr (nth code 0) (i18n/c (nth code 1)))
(tr code)))
@@ -28,7 +27,6 @@
[props problem]
(let [v-fn (:error/fn props)
result (v-fn problem)]
(prn "result" result)
(if (string? result)
{:message result}
{:message (or (some-> (get result :code)
@@ -38,7 +36,6 @@
(defn- handle-error-message
[props]
(prn "message" (get props :error/message))
{:message (get props :error/message)})
(defn- handle-error-code
@@ -56,7 +53,6 @@
field
[field])]
(prn "problem" problem)
(if (and (= 1 (count field))
(contains? acc (first field)))
acc

View File

@@ -8,7 +8,6 @@
"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]
@@ -211,7 +210,3 @@
(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,11 +106,6 @@
(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
@@ -168,15 +163,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)
decode-params (c/get params :decode-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)
fn-sym
(-> (gensym (str "internal-fn-" (str/slug pname) "-"))
@@ -229,14 +223,14 @@
~@(if schema-1
[fn-sym `(fn* [param#]
(let [param# (json/->clj param# ~decode-params)
(let [param# (json/->clj param#)
param# (~coercer-sym param#)]
(~fn-sym param#)))]
[])
~@(if schema-n
[fn-sym `(fn* []
(let [params# (into-array (cljs.core/js-arguments))
params# (json/->clj params# ~decode-params)
params# (mfu/bean params#)
params# (~coercer-sym params#)]
(apply ~fn-sym params#)))]
[])

View File

@@ -179,6 +179,7 @@
(->> (render-canvas-blob canvas width height bgcolor)
(p/fnly (fn [data cause]
(wasm.api/clear-canvas)
(if cause
(rx/error! subs cause)
(rx/push! subs

View File

@@ -6,7 +6,7 @@
(ns frontend-tests.tokens.helpers.tokens
(:require
[app.common.files.tokens :as cfo]
[app.common.files.tokens :as cft]
[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 (cfo/attributes-map attributes token)]
applied-attributes (cft/attributes-map attributes token)]
(update-in file [:data
:pages-index first-page-id
:objects shape-id

View File

@@ -57,7 +57,8 @@
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/rename-token-set set-a "Set A updated")]]
events [(dwtl/update-token-set (ctob/rename set-a "Set A updated")
"Set A updated")]]
(tohs/run-store-async
store done events

View File

@@ -8731,10 +8731,10 @@ msgstr ""
msgid "workspace.versions.warning.text"
msgstr "Autosaved versions will be kept for %s days."
msgid "labels.webgl-context-lost.main-message"
msgid "errors.webgl-context-lost.main-message"
msgstr "Oops! The canvas context was lost"
msgid "labels.webgl-context-lost.desc-message"
msgid "errors.webgl-context-lost.desc-message"
msgstr "WebGL has stopped working. Please reload the page to reset it"
#, unused

View File

@@ -8582,8 +8582,8 @@ msgstr "Los autoguardados duran %s días."
msgid "workspace.viewport.click-to-close-path"
msgstr "Pulsar para cerrar la ruta"
msgid "labels.webgl-context-lost.main-message"
msgid "errors.webgl-context-lost.main-message"
msgstr "Ups! Se ha perdido el contexto del canvas"
msgid "labels.webgl-context-lost.desc-message"
msgstr "WebGL ha dejado de funcionar. Por favor, recarga la página para restaurarlo"
msgid "errors.webgl-context-lost.desc-message"
msgstr "WebGL ha dejado de funcionar. Por favor, recarga la página para restaurarlo"

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
```
pnpm run start:styles-example
npm 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 `pnpm install` to install the required dependencies.
Then, run `pnpm run start` to launch the plugins wrapper.
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.
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
pnpm run start:plugin:contrast
npm run start:plugin:contrast
```
Finally, open in your browser the specific port. In this specific example would be `http://localhost:4302`
@@ -49,22 +49,21 @@ A table listing the available plugins and their corresponding startup commands i
| 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 |
| 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 |
## Web Apps
| 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/ |
| 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/ |
## Creating a plugin from scratch

View File

@@ -1,51 +0,0 @@
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

@@ -1,79 +0,0 @@
{
"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

@@ -1,127 +0,0 @@
/* @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

@@ -1,144 +0,0 @@
<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

@@ -1,290 +0,0 @@
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,
// attributes: ['stroke-color'] // 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

@@ -1,11 +0,0 @@
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

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

View File

@@ -1 +0,0 @@
*

View File

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

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

View File

@@ -1,14 +0,0 @@
{
"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

@@ -1,13 +0,0 @@
<!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

@@ -1,7 +0,0 @@
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

@@ -1,112 +0,0 @@
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;
attributes?: 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

@@ -1,249 +0,0 @@
import type { PluginMessageEvent, PluginUIEvent } from './model.js';
import { TokenType, TokenProperty } 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.attributes);
}
});
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});
if (token) {
loadTokens(setId);
}
}
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;
loadTokens(setId);
}
}
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,
attributes: TokenProperty[] | undefined,
) {
const tokensCatalog = penpot.library.local.tokens;
const set = tokensCatalog?.getSetById(setId);
const token = set?.getTokenById(tokenId);
if (token) {
token.applyToSelected(attributes);
}
// Alternatve way
//
// const selection = penpot.selection;
// if (token && selection) {
// for (const shape of selection) {
// shape.applyToken(token, attributes);
// }
// }
}

View File

@@ -1,23 +0,0 @@
/* @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

@@ -1,10 +0,0 @@
{
"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

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

View File

@@ -1,33 +0,0 @@
{
"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

@@ -1,8 +0,0 @@
{
"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

27170
plugins/package-lock.json generated Normal file
View File

File diff suppressed because it is too large Load Diff

View File

@@ -16,7 +16,6 @@
"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",

View File

@@ -90,6 +90,18 @@ impl Type {
}
}
pub fn clear_corners(&mut self) {
match self {
Type::Rect(data) => {
data.corners = None;
}
Type::Frame(data) => {
data.corners = None;
}
_ => {}
}
}
pub fn path(&self) -> Option<&Path> {
match self {
Type::Path(path) => Some(path),
@@ -694,9 +706,11 @@ impl Shape {
pub fn set_corners(&mut self, raw_corners: (f32, f32, f32, f32)) {
if let Some(corners) = make_corners(raw_corners) {
self.shape_type.set_corners(corners);
self.invalidate_bounds();
self.invalidate_extrect();
} else {
self.shape_type.clear_corners();
}
self.invalidate_bounds();
self.invalidate_extrect();
}
pub fn set_svg(&mut self, svg: skia::svg::Dom) {
@@ -1590,6 +1604,13 @@ mod tests {
} else {
unreachable!();
}
shape.set_corners((0.0, 0.0, 0.0, 0.0));
if let Type::Rect(Rect { corners, .. }) = shape.shape_type {
assert_eq!(corners, None);
} else {
unreachable!();
}
}
#[test]