0
Fork 0
mirror of https://github.com/penpot/penpot.git synced 2025-03-17 10:11:22 -05:00

Merge pull request #5242 from penpot/niwinz-design-tokens

🎉 Merge tokens-studio
This commit is contained in:
Andrey Antukh 2024-11-07 10:33:24 +01:00 committed by GitHub
commit 46a6aff4da
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
84 changed files with 10622 additions and 174 deletions

View file

@ -27,12 +27,6 @@ jobs:
yarn run fmt:clj:check
yarn run lint:clj
- run:
name: "JS tests"
working_directory: "./common"
command: |
yarn run test
- run:
name: "JVM tests"
working_directory: "./common"

View file

@ -57,6 +57,58 @@
#?(:cljs (instance? lkm/LinkedMap o)
:clj (instance? LinkedMap o)))
(defn oassoc
[o & kvs]
(apply assoc (or o (ordered-map)) kvs))
(defn oassoc-in
[o [k & ks] v]
(if ks
(oassoc o k (oassoc-in (get o k) ks v))
(oassoc o k v)))
(defn oupdate-in
[m ks f & args]
(let [up (fn up [m ks f args]
(let [[k & ks] ks]
(if ks
(oassoc m k (up (get m k) ks f args))
(oassoc m k (apply f (get m k) args)))))]
(up m ks f args)))
(declare index-of)
(defn oassoc-before
"Assoc a k v pair, in the order position just before the other key"
[o before-k k v]
(if-let [index (index-of (keys o) before-k)]
(-> (ordered-map)
(into (take index o))
(assoc k v)
(into (drop index o)))
(oassoc o k v)))
(defn oassoc-in-before
[o [before-k & before-ks] [k & ks] v]
(if-let [index (index-of (keys o) before-k)]
(let [new-v (if ks
(oassoc-in-before (get o k) before-ks ks v)
v)
current-index (index-of (keys o) k)
new-index (if (and current-index (< current-index index))
(dec index)
index)]
(if (= k before-k)
(-> (ordered-map)
(into (take new-index o))
(assoc k new-v)
(into (drop (inc new-index) o)))
(-> (ordered-map)
(into (take new-index (dissoc o k)))
(assoc k new-v)
(into (drop new-index (dissoc o k))))))
(oassoc-in o (cons k ks) v)))
(defn vec2
"Creates a optimized vector compatible type of length 2 backed
internally with MapEntry impl because it has faster access method

View file

@ -50,6 +50,7 @@
"styles/v2"
"layout/grid"
"plugins/runtime"
"design-tokens/v1"
"text-editor/v2"})
;; A set of features enabled by default
@ -84,6 +85,7 @@
"layout/grid"
"fdata/shape-data-type"
"plugins/runtime"
"design-tokens/v1"
"text-editor/v2"}
(into frontend-only-features)))
@ -104,6 +106,7 @@
:feature-fdata-objects-map "fdata/objects-map"
:feature-fdata-pointer-map "fdata/pointer-map"
:feature-plugins "plugins/runtime"
:feature-design-tokens "design-tokens/v1"
:feature-text-editor-v2 "text-editor/v2"
nil))
@ -312,5 +315,3 @@
:feature (first not-supported)
:hint (str/ffmt "paste features '%' not enabled on the application"
(str/join "," not-supported))))))

View file

@ -26,6 +26,9 @@
[app.common.types.pages-list :as ctpl]
[app.common.types.shape :as cts]
[app.common.types.shape-tree :as ctst]
[app.common.types.token :as cto]
[app.common.types.token-theme :as ctot]
[app.common.types.tokens-lib :as ctob]
[app.common.types.typographies-list :as ctyl]
[app.common.types.typography :as ctt]
[app.common.uuid :as uuid]
@ -366,7 +369,87 @@
[:del-typography
[:map {:title "DelTypogrphyChange"}
[:type [:= :del-typography]]
[:id ::sm/uuid]]]]])
[:id ::sm/uuid]]]
[:add-temporary-token-theme
[:map {:title "AddTemporaryTokenThemeChange"}
[:type [:= :add-temporary-token-theme]]
[:token-theme ::ctot/token-theme]]]
[:update-active-token-themes
[:map {:title "UpdateActiveTokenThemes"}
[:type [:= :update-active-token-themes]]
[:theme-ids [:set :string]]]]
[:delete-temporary-token-theme
[:map {:title "DeleteTemporaryTokenThemeChange"}
[:type [:= :delete-temporary-token-theme]]
[:id ::sm/uuid]
[:name :string]]]
[:add-token-theme
[:map {:title "AddTokenThemeChange"}
[:type [:= :add-token-theme]]
[:token-theme ::ctot/token-theme]]]
[:mod-token-theme
[:map {:title "ModTokenThemeChange"}
[:type [:= :mod-token-theme]]
[:group :string]
[:name :string]
[:token-theme ::ctot/token-theme]]]
[:del-token-theme
[:map {:title "DelTokenThemeChange"}
[:type [:= :del-token-theme]]
[:group :string]
[:name :string]]]
[:add-token-set
[:map {:title "AddTokenSetChange"}
[:type [:= :add-token-set]]
[:token-set ::ctot/token-set]]]
[:mod-token-set
[:map {:title "ModTokenSetChange"}
[:type [:= :mod-token-set]]
[:name :string]
[:token-set ::ctot/token-set]]]
[:move-token-set-before
[:map {:title "MoveTokenSetBefore"}
[:type [:= :move-token-set-before]]
[:set-name :string]
[:before-set-name [:maybe :string]]]]
[:del-token-set
[:map {:title "DelTokenSetChange"}
[:type [:= :del-token-set]]
[:name :string]]]
[:set-tokens-lib
[:map {:title "SetTokensLib"}
[:type [:= :set-tokens-lib]]
[:tokens-lib :any]]]
[:add-token
[:map {:title "AddTokenChange"}
[:type [:= :add-token]]
[:set-name :string]
[:token ::cto/token]]]
[:mod-token
[:map {:title "ModTokenChange"}
[:type [:= :mod-token]]
[:set-name :string]
[:name :string]
[:token ::cto/token]]]
[:del-token
[:map {:title "DelTokenChange"}
[:type [:= :del-token]]
[:set-name :string]
[:name :string]]]]])
(def schema:changes
[:sequential {:gen/max 5 :gen/min 1} schema:change])
@ -889,6 +972,103 @@
[data {:keys [id]}]
(ctyl/delete-typography data id))
;; -- Tokens
(defmethod process-change :set-tokens-lib
[data {:keys [tokens-lib]}]
(assoc data :tokens-lib tokens-lib))
(defmethod process-change :add-token
[data {:keys [set-name token]}]
(update data :tokens-lib #(-> %
(ctob/ensure-tokens-lib)
(ctob/add-token-in-set set-name (ctob/make-token token)))))
(defmethod process-change :mod-token
[data {:keys [set-name name token]}]
(update data :tokens-lib #(-> %
(ctob/ensure-tokens-lib)
(ctob/update-token-in-set
set-name
name
(fn [old-token]
(ctob/make-token (merge old-token token)))))))
(defmethod process-change :del-token
[data {:keys [set-name name]}]
(update data :tokens-lib #(-> %
(ctob/ensure-tokens-lib)
(ctob/delete-token-from-set
set-name
name))))
(defmethod process-change :add-temporary-token-theme
[data {:keys [token-theme]}]
(update data :tokens-lib #(-> %
(ctob/ensure-tokens-lib)
(ctob/add-theme (ctob/make-token-theme token-theme)))))
(defmethod process-change :update-active-token-themes
[data {:keys [theme-ids]}]
(update data :tokens-lib #(-> % (ctob/ensure-tokens-lib)
(ctob/set-active-themes theme-ids))))
(defmethod process-change :delete-temporary-token-theme
[data {:keys [group name]}]
(update data :tokens-lib #(-> %
(ctob/ensure-tokens-lib)
(ctob/delete-theme group name))))
(defmethod process-change :add-token-theme
[data {:keys [token-theme]}]
(update data :tokens-lib #(-> %
(ctob/ensure-tokens-lib)
(ctob/add-theme (-> token-theme
(ctob/make-token-theme))))))
(defmethod process-change :mod-token-theme
[data {:keys [name group token-theme]}]
(update data :tokens-lib #(-> %
(ctob/ensure-tokens-lib)
(ctob/update-theme group name
(fn [prev-theme]
(merge prev-theme token-theme))))))
(defmethod process-change :del-token-theme
[data {:keys [group name]}]
(update data :tokens-lib #(-> %
(ctob/ensure-tokens-lib)
(ctob/delete-theme group name))))
(defmethod process-change :add-token-set
[data {:keys [token-set]}]
(update data :tokens-lib #(-> %
(ctob/ensure-tokens-lib)
(ctob/add-set (ctob/make-token-set token-set)))))
(defmethod process-change :mod-token-set
[data {:keys [name token-set]}]
(update data :tokens-lib (fn [lib]
(let [path-changed? (not= name (:name token-set))
lib' (-> lib
(ctob/ensure-tokens-lib)
(ctob/update-set name (fn [prev-set]
(merge prev-set (dissoc token-set :tokens)))))]
(cond-> lib'
path-changed? (ctob/update-set-name name (:name token-set)))))))
(defmethod process-change :move-token-set-before
[data {:keys [set-name before-set-name]}]
(update data :tokens-lib #(-> %
(ctob/ensure-tokens-lib)
(ctob/move-set-before set-name before-set-name))))
(defmethod process-change :del-token-set
[data {:keys [name]}]
(update data :tokens-lib #(-> %
(ctob/ensure-tokens-lib)
(ctob/delete-set name))))
;; === Operations
(def ^:private decode-shape

View file

@ -20,6 +20,7 @@
[app.common.types.component :as ctk]
[app.common.types.file :as ctf]
[app.common.types.shape.layout :as ctl]
[app.common.types.tokens-lib :as ctob]
[app.common.uuid :as uuid]))
;; Auxiliary functions to help create a set of changes (undo + redo)
@ -760,6 +761,116 @@
(update :undo-changes conj {:type :add-typography :typography prev-typography})
(apply-changes-local))))
(defn add-temporary-token-theme
[changes token-theme]
(-> changes
(update :redo-changes conj {:type :add-temporary-token-theme :token-theme token-theme})
(update :undo-changes conj {:type :delete-temporary-token-theme :id (:id token-theme) :name (:name token-theme)})
(apply-changes-local)))
(defn update-active-token-themes
[changes token-active-theme-ids prev-token-active-theme-ids]
(-> changes
(update :redo-changes conj {:type :update-active-token-themes :theme-ids token-active-theme-ids})
(update :undo-changes conj {:type :update-active-token-themes :theme-ids prev-token-active-theme-ids})
(apply-changes-local)))
(defn add-token-theme
[changes token-theme]
(-> changes
(update :redo-changes conj {:type :add-token-theme :token-theme token-theme})
(update :undo-changes conj {:type :del-token-theme :group (:group token-theme) :name (:name token-theme)})
(apply-changes-local)))
(defn update-token-theme
[changes token-theme prev-token-theme]
(let [name (or (:name prev-token-theme)
(:name token-theme))
group (or (:group prev-token-theme)
(:group token-theme))]
(-> changes
(update :redo-changes conj {:type :mod-token-theme :group group :name name :token-theme token-theme})
(update :undo-changes conj {:type :mod-token-theme :group group :name name :token-theme (or prev-token-theme token-theme)})
(apply-changes-local))))
(defn delete-token-theme
[changes group name]
(assert-library! changes)
(let [library-data (::library-data (meta changes))
prev-token-theme (some-> (get library-data :tokens-lib)
(ctob/get-theme group name))]
(-> changes
(update :redo-changes conj {:type :del-token-theme :group group :name name})
(update :undo-changes conj {:type :add-token-theme :token-theme prev-token-theme})
(apply-changes-local))))
(defn add-token-set
[changes token-set]
(-> changes
(update :redo-changes conj {:type :add-token-set :token-set token-set})
(update :undo-changes conj {:type :del-token-set :name (:name token-set)})
(apply-changes-local)))
(defn update-token-set
[changes token-set prev-token-set]
(-> changes
(update :redo-changes conj {:type :mod-token-set :name (:name prev-token-set) :token-set token-set})
(update :undo-changes conj {:type :mod-token-set :name (:name token-set) :token-set (or prev-token-set token-set)})
(apply-changes-local)))
(defn delete-token-set
[changes token-set-name]
(assert-library! changes)
(let [library-data (::library-data (meta changes))
prev-token-theme (some-> (get library-data :tokens-lib)
(ctob/get-set token-set-name))]
(-> changes
(update :redo-changes conj {:type :del-token-set :name token-set-name})
(update :undo-changes conj {:type :add-token-set :token-set prev-token-theme})
(apply-changes-local))))
(defn move-token-set-before
[changes set-name before-set-name prev-before-set-name]
(-> changes
(update :redo-changes conj {:type :move-token-set-before :set-name set-name :before-set-name before-set-name})
(update :undo-changes conj {:type :move-token-set-before :set-name set-name :before-set-name prev-before-set-name})
(apply-changes-local)))
(defn set-tokens-lib
[changes tokens-lib]
(let [library-data (::library-data (meta changes))
prev-tokens-lib (get library-data :tokens-lib)]
(-> changes
(update :redo-changes conj {:type :set-tokens-lib :tokens-lib tokens-lib})
(update :undo-changes conj {:type :set-tokens-lib :tokens-lib prev-tokens-lib})
(apply-changes-local))))
(defn add-token
[changes set-name token]
(-> changes
(update :redo-changes conj {:type :add-token :set-name set-name :token token})
(update :undo-changes conj {:type :del-token :set-name set-name :name (:name token)})
(apply-changes-local)))
(defn update-token
[changes set-name token prev-token]
(-> changes
(update :redo-changes conj {:type :mod-token :set-name set-name :name (:name prev-token) :token token})
(update :undo-changes conj {:type :mod-token :set-name set-name :name (:name token) :token (or prev-token token)})
(apply-changes-local)))
(defn delete-token
[changes set-name token-name]
(assert-library! changes)
(let [library-data (::library-data (meta changes))
prev-token (some-> (get library-data :tokens-lib)
(ctob/get-set set-name)
(ctob/get-token token-name))]
(-> changes
(update :redo-changes conj {:type :del-token :set-name set-name :name token-name})
(update :undo-changes conj {:type :add-token :set-name set-name :token prev-token})
(apply-changes-local))))
(defn add-component
([changes id path name new-shapes updated-shapes main-instance-id main-instance-page]
(add-component changes id path name new-shapes updated-shapes main-instance-id main-instance-page nil))

View file

@ -16,6 +16,7 @@
java.time.OffsetDateTime
java.util.List
linked.map.LinkedMap
linked.set.LinkedSet
org.fressian.Reader
org.fressian.StreamingWriter
org.fressian.Writer
@ -275,7 +276,12 @@
{:name "clj/seq"
:class clojure.lang.ISeq
:wfn write-list-like
:rfn (comp sequence read-object!)})
:rfn (comp sequence read-object!)}
{:name "linked/set"
:class LinkedSet
:wfn write-list-like
:rfn (comp #(into (d/ordered-set) %) read-object!)})
;; --- PUBLIC API

View file

@ -27,6 +27,11 @@
#?(:clj (Instant/now)
:cljs (.local ^js DateTime)))
#?(:clj
(defn is-after?
[one other]
(.isAfter one other)))
(defn instant?
[o]
#?(:clj (instance? Instant o)
@ -51,6 +56,8 @@
#?(:clj (.format DateTimeFormatter/ISO_INSTANT ^Instant v)
:cljs (.toISO ^js v)))
;; To check for valid date time we can just use the core inst? function
#?(:cljs
(extend-protocol IComparable
DateTime

View file

@ -26,6 +26,7 @@
[app.common.types.pages-list :as ctpl]
[app.common.types.plugins :as ctpg]
[app.common.types.shape-tree :as ctst]
[app.common.types.tokens-lib :as ctl]
[app.common.types.typographies-list :as ctyl]
[app.common.types.typography :as cty]
[app.common.uuid :as uuid]
@ -69,7 +70,8 @@
[:colors {:optional true} schema:colors]
[:components {:optional true} schema:components]
[:typographies {:optional true} schema:typographies]
[:plugin-data {:optional true} ::ctpg/plugin-data]])
[:plugin-data {:optional true} ::ctpg/plugin-data]
[:tokens-lib {:optional true} ::ctl/tokens-lib]])
(def schema:file
"A schema for validate a file data structure; data is optional

View file

@ -27,6 +27,7 @@
[app.common.types.shape.path :as ctsp]
[app.common.types.shape.shadow :as ctss]
[app.common.types.shape.text :as ctsx]
[app.common.types.token :as cto]
[app.common.uuid :as uuid]
[clojure.set :as set]))
@ -189,6 +190,7 @@
[:blur {:optional true} ::ctsb/blur]
[:grow-type {:optional true}
[::sm/one-of grow-types]]
[:applied-tokens {:optional true} ::cto/applied-tokens]
[:plugin-data {:optional true} ::ctpg/plugin-data]])
(def schema:group-attrs

View file

@ -23,6 +23,8 @@
:show-content
:hide-in-viewer
:applied-tokens
:opacity
:blend-mode
:blocked
@ -95,6 +97,8 @@
:parent-id
:frame-id
:applied-tokens
:opacity
:blend-mode
:blocked

View file

@ -0,0 +1,151 @@
;; 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.types.token
(:require
[app.common.schema :as sm]
[app.common.schema.registry :as sr]
[clojure.set :as set]
[malli.util :as mu]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; HELPERS
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn merge-schemas
"Merge registered schemas."
[& schema-keys]
(let [schemas (map #(get @sr/registry %) schema-keys)]
(reduce sm/merge schemas)))
(defn schema-keys
"Converts registed map schema into set of keys."
[registered-schema]
(->> (get @sr/registry registered-schema)
(sm/schema)
(mu/keys)
(into #{})))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; SCHEMA
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(def token-type->dtcg-token-type
{:boolean "boolean"
:border-radius "borderRadius"
:color "color"
:dimensions "dimension"
:numeric "numeric"
:opacity "opacity"
:other "other"
:rotation "rotation"
:sizing "sizing"
:spacing "spacing"
:string "string"
:stroke-width "strokeWidth"})
(def dtcg-token-type->token-type
(set/map-invert token-type->dtcg-token-type))
(def token-types
(into #{} (keys token-type->dtcg-token-type)))
(defn valid-token-type?
[t]
(token-types t))
(def token-name-ref :string)
(defn valid-token-name-ref?
[n]
(string? n))
;; TODO Move this to tokens-lib
(sm/register! ::token
[:map {:title "Token"}
[:name token-name-ref]
[:type [::sm/one-of token-types]]
[:value :any]
[:description {:optional true} [:maybe :string]]
[:modified-at {:optional true} ::sm/inst]])
(sm/register! ::color
[:map
[:color {:optional true} token-name-ref]])
(def color-keys (schema-keys ::color))
(sm/register! ::border-radius
[:map
[:rx {:optional true} token-name-ref]
[:ry {:optional true} token-name-ref]
[: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 ::border-radius))
(sm/register! ::stroke-width
[:map
[:stroke-width {:optional true} token-name-ref]])
(def stroke-width-keys (schema-keys ::stroke-width))
(sm/register! ::sizing
[:map
[:width {:optional true} token-name-ref]
[:height {:optional true} token-name-ref]
[: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 sizing-keys (schema-keys ::sizing))
(sm/register! ::opacity
[:map
[:opacity {:optional true} token-name-ref]])
(def opacity-keys (schema-keys ::opacity))
(sm/register! ::spacing
[:map
[:row-gap {:optional true} token-name-ref]
[:column-gap {:optional true} token-name-ref]
[:p1 {:optional true} token-name-ref]
[:p2 {:optional true} token-name-ref]
[:p3 {:optional true} token-name-ref]
[:p4 {:optional true} token-name-ref]
[:x {:optional true} token-name-ref]
[:y {:optional true} token-name-ref]])
(def spacing-keys (schema-keys ::spacing))
(sm/register! ::dimensions
(merge-schemas ::sizing
::spacing
::stroke-width
::border-radius))
(def dimensions-keys (schema-keys ::dimensions))
(sm/register! ::rotation
[:map
[:rotation {:optional true} token-name-ref]])
(def rotation-keys (schema-keys ::rotation))
(sm/register! ::tokens
[:map {:title "Applied Tokens"}])
(sm/register! ::applied-tokens
(merge-schemas ::tokens
::border-radius
::sizing
::spacing
::rotation
::dimensions))

View file

@ -0,0 +1,25 @@
;; 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.types.token-theme
(:require
[app.common.schema :as sm]))
(sm/register! ::token-theme
[:map {:title "TokenTheme"}
[:name :string]
[:group :string]
[:description [:maybe :string]]
[:is-source :boolean]
[:modified-at {:optional true} ::sm/inst]
[:sets :any]])
(sm/register! ::token-set
[:map {:title "TokenSet"}
[:name :string]
[:description {:optional true} [:maybe :string]]
[:modified-at {:optional true} ::sm/inst]
[:tokens :any]])

View file

@ -0,0 +1,947 @@
;; 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.types.tokens-lib
(:require
#?(:clj [app.common.fressian :as fres])
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.schema :as sm]
[app.common.time :as dt]
[app.common.transit :as t]
[app.common.types.token :as cto]
[clojure.set :as set]
[clojure.walk :as walk]
[cuerdas.core :as str]))
;; === Groups handling
(def schema:groupable-item
[:map {:title "Groupable item"}
[:name :string]])
(def valid-groupable-item?
(sm/validator schema:groupable-item))
(defn split-path
"Decompose a string in the form 'one.two.three' into a vector of strings, removing spaces."
[path separator]
(let [xf (comp (map str/trim)
(remove str/empty?))]
(->> (str/split path separator)
(into [] xf))))
(defn join-path
"Regenerate a path as a string, from a vector."
[path separator]
(str/join separator path))
(defn group-item
"Add a group to the item name, in the form group.name."
[item group-name separator]
(dm/assert!
"expected groupable item"
(valid-groupable-item? item))
(update item :name #(str group-name separator %)))
(defn ungroup-item
"Remove the first group from the item name."
[item separator]
(dm/assert!
"expected groupable item"
(valid-groupable-item? item))
(update item :name #(-> %
(split-path separator)
(rest)
(join-path separator))))
(defn get-path
"Get the groups part of the name as a vector. E.g. group.subgroup.name -> ['group' 'subrgoup']"
[item separator]
(dm/assert!
"expected groupable item"
(valid-groupable-item? item))
(split-path (:name item) separator))
(defn get-groups-str
"Get the groups part of the name. E.g. group.subgroup.name -> group.subrgoup"
[item separator]
(-> (get-path item separator)
(butlast)
(join-path separator)))
(defn get-final-name
"Get the final part of the name. E.g. group.subgroup.name -> name"
[item separator]
(dm/assert!
"expected groupable item"
(valid-groupable-item? item))
(-> (:name item)
(split-path separator)
(last)))
(defn group?
"Check if a node of the grouping tree is a group or a final item."
[item]
(d/ordered-map? item))
(defn get-children
"Get all children of a group of a grouping tree. Each child is
a tuple [name item], where item "
[group]
(dm/assert!
"expected group node"
(group? group))
(seq group))
;; === Token
(def token-separator ".")
(defn get-token-path [path]
(get-path path token-separator))
(defn split-token-path [path]
(split-path path token-separator))
(defrecord Token [name type value description modified-at])
(def schema:token
[:and
[:map {:title "Token"}
[:name cto/token-name-ref]
[:type [::sm/one-of cto/token-types]]
[:value :any]
[:description [:maybe :string]]
[:modified-at ::sm/inst]]
[:fn (partial instance? Token)]])
(sm/register! ::token schema:token)
(def valid-token?
(sm/validator schema:token))
(def check-token!
(sm/check-fn ::token))
(defn make-token
[& {:keys [] :as params}]
(let [params (-> params
(dissoc :id) ;; we will remove this when old data structures are removed
(update :modified-at #(or % (dt/now))))
token (map->Token params)]
(dm/assert!
"expected valid token"
(check-token! token))
token))
(defn find-token-value-references
"Returns set of token references found in `token-value`.
Used for checking if a token has a reference in the value.
Token references are strings delimited by curly braces.
E.g.: {foo.bar.baz} -> foo.bar.baz"
[token-value]
(some->> (re-seq #"\{([^}]*)\}" token-value)
(map second)
(into #{})))
(defn token-value-self-reference?
"Check if the token is self referencing with its `token-name` in `token-value`.
Simple 1 level check, doesn't account for circular self refernces across multiple tokens."
[token-name token-value]
(let [token-references (find-token-value-references token-value)
self-reference? (get token-references token-name)]
self-reference?))
(defn group-by-type [tokens]
(let [tokens' (if (or (map? tokens)
(d/ordered-map? tokens))
(vals tokens)
tokens)]
(group-by :type tokens')))
(defn filter-by-type [token-type tokens]
(let [token-type? #(= token-type (:type %))]
(cond
(d/ordered-map? tokens) (into (d/ordered-map) (filter (comp token-type? val) tokens))
(map? tokens) (into {} (filter (comp token-type? val) tokens))
:else (filter token-type? tokens))))
;; === Token Set
(def set-separator "/")
(defn get-token-set-path [path]
(get-path path set-separator))
(defn get-token-set-group-str [path]
(get-groups-str path set-separator))
(defn split-token-set-path [path]
(split-path path set-separator))
(defn tokens-tree
"Convert tokens into a nested tree with their `:name` as the path.
Optionally use `update-token-fn` option to transform the token."
[tokens & {:keys [update-token-fn]
:or {update-token-fn identity}}]
(reduce
(fn [acc [_ token]]
(let [path (split-token-path (:name token))]
(assoc-in acc path (update-token-fn token))))
{} tokens))
(defn backtrace-tokens-tree
"Convert tokens into a nested tree with their `:name` as the path.
Generates a uuid per token to backtrace a token from an external source (StyleDictionary).
The backtrace can't be the name as the name might not exist when the user is creating a token."
[tokens]
(reduce
(fn [acc [_ token]]
(let [temp-id (random-uuid)
token (assoc token :temp/id temp-id)
path (split-token-path (:name token))]
(-> acc
(assoc-in (concat [:tokens-tree] path) token)
(assoc-in [:ids temp-id] token))))
{:tokens-tree {} :ids {}} tokens))
(defprotocol ITokenSet
(add-token [_ token] "add a token at the end of the list")
(update-token [_ token-name f] "update a token in the list")
(delete-token [_ token-name] "delete a token from the list")
(get-token [_ token-name] "return token by token-name")
(get-tokens [_] "return an ordered sequence of all tokens in the set")
(get-tokens-tree [_] "returns a tree of tokens split & nested by their name path")
(get-dtcg-tokens-tree [_] "returns tokens tree formated to the dtcg spec"))
(defrecord TokenSet [name description modified-at tokens]
ITokenSet
(add-token [_ token]
(dm/assert! "expected valid token" (check-token! token))
(TokenSet. name
description
(dt/now)
(assoc tokens (:name token) token)))
(update-token [this token-name f]
(if-let [token (get tokens token-name)]
(let [token' (-> (make-token (f token))
(assoc :modified-at (dt/now)))]
(check-token! token')
(TokenSet. name
description
(dt/now)
(if (= (:name token) (:name token'))
(assoc tokens (:name token') token')
(-> tokens
(d/oassoc-before (:name token) (:name token') token')
(dissoc (:name token))))))
this))
(delete-token [_ token-name]
(TokenSet. name
description
(dt/now)
(dissoc tokens token-name)))
(get-token [_ token-name]
(get tokens token-name))
(get-tokens [_]
(vals tokens))
(get-tokens-tree [_]
(tokens-tree tokens))
(get-dtcg-tokens-tree [_]
(tokens-tree tokens :update-token-fn (fn [token]
(cond-> {"$value" (:value token)
"$type" (cto/token-type->dtcg-token-type (:type token))}
(:description token) (assoc "$description" (:description token)))))))
(def schema:token-set
[:and [:map {:title "TokenSet"}
[:name :string]
[:description [:maybe :string]]
[:modified-at ::sm/inst]
[:tokens [:and [:map-of {:gen/max 5} :string ::token]
[:fn d/ordered-map?]]]]
[:fn (partial instance? TokenSet)]])
(sm/register! ::token-set schema:token-set)
(def valid-token-set?
(sm/validator schema:token-set))
(def check-token-set!
(sm/check-fn ::token-set))
(defn make-token-set
[& {:keys [] :as params}]
(let [params (-> params
(dissoc :id)
(update :modified-at #(or % (dt/now)))
(update :tokens #(into (d/ordered-map) %)))
token-set (map->TokenSet params)]
(dm/assert!
"expected valid token set"
(check-token-set! token-set))
token-set))
;; === TokenSetGroup
(defrecord TokenSetGroup [attr1 attr2])
;; TODO schema, validators, etc.
(defn make-token-set-group
[]
(TokenSetGroup. "one" "two"))
;; === TokenSets (collection)
(defprotocol ITokenSets
(add-set [_ token-set] "add a set to the library, at the end")
(update-set [_ set-name f] "modify a set in the ilbrary")
(delete-set [_ set-name] "delete a set in the library")
(move-set-before [_ set-name before-set-name] "move a set with `set-name` before a set with `before-set-name` in the library.
When `before-set-name` is nil, move set to bottom")
(set-count [_] "get the total number if sets in the library")
(get-set-tree [_] "get a nested tree of all sets in the library")
(get-sets [_] "get an ordered sequence of all sets in the library")
(get-ordered-set-names [_] "get an ordered sequence of all sets names in the library")
(get-set [_ set-name] "get one set looking for name")
(get-neighbor-set-name [_ set-name index-offset] "get neighboring set name offset by `index-offset`")
(get-set-group [_ set-group-path] "get the attributes of a set group"))
(def schema:token-set-node
[:schema {:registry {::node [:or ::token-set
[:and
[:map-of {:gen/max 5} :string [:ref ::node]]
[:fn d/ordered-map?]]]}}
[:ref ::node]])
(sm/register! ::token-set-node schema:token-set-node)
(def schema:token-sets
[:and
[:map-of {:title "TokenSets"}
:string ::token-set-node]
[:fn d/ordered-map?]])
(sm/register! ::token-sets schema:token-sets)
(def valid-token-sets?
(sm/validator schema:token-sets))
(def check-token-sets!
(sm/check-fn ::token-sets))
;; === TokenTheme
(def theme-separator "/")
(defn token-theme-path [group name]
(join-path [group name] theme-separator))
(defn split-token-theme-path [path]
(split-path path theme-separator))
(def hidden-token-theme-group
"")
(def hidden-token-theme-name
"__PENPOT__HIDDEN__TOKEN__THEME__")
(def hidden-token-theme-path
(token-theme-path hidden-token-theme-group hidden-token-theme-name))
(defprotocol ITokenTheme
(set-sets [_ set-names] "set the active token sets")
(disable-set [_ set-name] "disable set in theme")
(toggle-set [_ set-name] "toggle a set enabled / disabled in the theme")
(theme-path [_] "get `token-theme-path` from theme")
(theme-matches-group-name [_ group name] "if a theme matches the given group & name")
(hidden-temporary-theme? [_] "if a theme is the (from the user ui) hidden temporary theme"))
(defrecord TokenTheme [name group description is-source modified-at sets]
ITokenTheme
(set-sets [_ set-names]
(TokenTheme. name
group
description
is-source
(dt/now)
set-names))
(disable-set [this set-name]
(set-sets this (disj sets set-name)))
(toggle-set [this set-name]
(set-sets this (if (sets set-name)
(disj sets set-name)
(conj sets set-name))))
(theme-path [_]
(token-theme-path group name))
(theme-matches-group-name [this group name]
(and (= (:group this) group)
(= (:name this) name)))
(hidden-temporary-theme? [this]
(theme-matches-group-name this hidden-token-theme-group hidden-token-theme-name)))
(def schema:token-theme
[:and [:map {:title "TokenTheme"}
[:name :string]
[:group :string]
[:description [:maybe :string]]
[:is-source :boolean]
[:modified-at ::sm/inst]
[:sets [:set {:gen/max 5} :string]]]
[:fn (partial instance? TokenTheme)]])
(sm/register! ::token-theme schema:token-theme)
(def valid-token-theme?
(sm/validator schema:token-theme))
(def check-token-theme!
(sm/check-fn ::token-theme))
(def top-level-theme-group-name
"Top level theme groups have an empty string as the theme group."
"")
(defn top-level-theme-group? [group]
(= group top-level-theme-group-name))
(defn make-token-theme
[& {:keys [] :as params}]
(let [params (-> params
(dissoc :id)
(update :group #(or % top-level-theme-group-name))
(update :is-source #(or % false))
(update :modified-at #(or % (dt/now)))
(update :sets #(into #{} %)))
token-theme (map->TokenTheme params)]
(dm/assert!
"expected valid token theme"
(check-token-theme! token-theme))
token-theme))
(defn make-hidden-token-theme
[& {:keys [] :as params}]
(make-token-theme (assoc params
:group hidden-token-theme-group
:name hidden-token-theme-name)))
;; === TokenThemes (collection)
(defprotocol ITokenThemes
(add-theme [_ token-theme] "add a theme to the library, at the end")
(update-theme [_ group name f] "modify a theme in the ilbrary")
(delete-theme [_ group name] "delete a theme in the library")
(theme-count [_] "get the total number if themes in the library")
(get-theme-tree [_] "get a nested tree of all themes in the library")
(get-themes [_] "get an ordered sequence of all themes in the library")
(get-theme [_ group name] "get one theme looking for name")
(get-theme-groups [_] "get a sequence of group names by order")
(get-active-theme-paths [_] "get the active theme paths")
(get-active-themes [_] "get an ordered sequence of active themes in the library")
(set-active-themes [_ active-themes] "set active themes in library")
(theme-active? [_ group name] "predicate if token theme is active")
(activate-theme [_ group name] "adds theme from the active-themes")
(deactivate-theme [_ group name] "removes theme from the active-themes")
(toggle-theme-active? [_ group name] "toggles theme in the active-themes"))
(def schema:token-themes
[:and
[:map-of {:title "TokenThemes"}
:string [:and [:map-of :string ::token-theme]
[:fn d/ordered-map?]]]
[:fn d/ordered-map?]])
(sm/register! ::token-themes schema:token-themes)
(def valid-token-themes?
(sm/validator schema:token-themes))
(def check-token-themes!
(sm/check-fn ::token-themes))
(def schema:active-token-themes
[:set string?])
(def valid-active-token-themes?
(sm/validator schema:active-token-themes))
;; === Import / Export from DTCG format
(defn flatten-nested-tokens-json
"Recursively flatten the dtcg token structure, joining keys with '.'."
[tokens token-path]
(reduce-kv
(fn [acc k v]
(let [child-path (if (empty? token-path)
(name k)
(str token-path "." k))]
(if (and (map? v)
(not (contains? v "$type")))
(merge acc (flatten-nested-tokens-json v child-path))
(let [token-type (cto/dtcg-token-type->token-type (get v "$type"))]
(if token-type
(assoc acc child-path (make-token
:name child-path
:type token-type
:value (get v "$value")
:description (get v "$description")))
;; Discard unknown tokens
acc)))))
{}
tokens))
;; === Tokens Lib
(defprotocol ITokensLib
"A library of tokens, sets and themes."
(add-token-in-set [_ set-name token] "add token to a set")
(update-token-in-set [_ set-name token-name f] "update a token in a set")
(delete-token-from-set [_ set-name token-name] "delete a token from a set")
(toggle-set-in-theme [_ group-name theme-name set-name] "toggle a set used / not used in a theme")
(get-active-themes-set-names [_] "set of set names that are active in the the active themes")
(get-active-themes-set-tokens [_] "set of set names that are active in the the active themes")
(update-set-name [_ old-set-name new-set-name] "updates set name in themes")
(encode-dtcg [_] "Encodes library to a dtcg compatible json string")
(decode-dtcg-json [_ parsed-json] "Decodes parsed json containing tokens and converts to library")
(get-all-tokens [_] "all tokens in the lib")
(validate [_]))
(deftype TokensLib [sets set-groups themes active-themes]
;; NOTE: This is only for debug purposes, pending to properly
;; implement the toString and alternative printing.
#?@(:clj [clojure.lang.IDeref
(deref [_] {:sets sets
:set-groups set-groups
:themes themes
:active-themes active-themes})]
:cljs [cljs.core/IDeref
(-deref [_] {:sets sets
:set-groups set-groups
:themes themes
:active-themes active-themes})])
#?@(:cljs [cljs.core/IEncodeJS
(-clj->js [_] (js-obj "sets" (clj->js sets)
"set-groups" (clj->js set-groups)
"themes" (clj->js themes)
"active-themes" (clj->js active-themes)))])
ITokenSets
(add-set [_ token-set]
(dm/assert! "expected valid token set" (check-token-set! token-set))
(let [path (get-token-set-path token-set)
groups-str (get-token-set-group-str token-set)]
(TokensLib. (d/oassoc-in sets path token-set)
(cond-> set-groups
(not (str/empty? groups-str))
(assoc groups-str (make-token-set-group)))
themes
active-themes)))
(update-set [this set-name f]
(let [path (split-token-set-path set-name)
set (get-in sets path)]
(if set
(let [set' (-> (make-token-set (f set))
(assoc :modified-at (dt/now)))
path' (get-path set' "/")]
(check-token-set! set')
(TokensLib. (if (= (:name set) (:name set'))
(d/oassoc-in sets path set')
(-> sets
(d/oassoc-in-before path path' set')
(d/dissoc-in path)))
set-groups ;; TODO update set-groups as needed
themes
active-themes))
this)))
(delete-set [_ set-name]
(let [path (split-token-set-path set-name)]
(TokensLib. (d/dissoc-in sets path)
set-groups ;; TODO remove set-group if needed
(walk/postwalk
(fn [form]
(if (instance? TokenTheme form)
(disable-set form set-name)
form))
themes)
active-themes)))
;; TODO Handle groups and nesting
(move-set-before [this set-name before-set-name]
(let [source-path (split-token-set-path set-name)
token-set (-> (get-set this set-name)
(assoc :modified-at (dt/now)))
target-path (split-token-set-path before-set-name)]
(if before-set-name
(TokensLib. (d/oassoc-in-before sets target-path source-path token-set)
set-groups ;; TODO remove set-group if needed
themes
active-themes)
(TokensLib. (-> sets
(d/dissoc-in source-path)
(d/oassoc-in source-path token-set))
set-groups ;; TODO remove set-group if needed
themes
active-themes))))
(get-set-tree [_]
sets)
(get-sets [_]
(->> (tree-seq d/ordered-map? vals sets)
(filter (partial instance? TokenSet))))
(get-ordered-set-names [this]
(map :name (get-sets this)))
(set-count [this]
(count (get-sets this)))
(get-set [_ set-name]
(let [path (split-path set-name "/")]
(get-in sets path)))
(get-neighbor-set-name [this set-name index-offset]
(let [sets (get-ordered-set-names this)
index (d/index-of sets set-name)
neighbor-set-name (when index
(nth sets (+ index-offset index) nil))]
neighbor-set-name))
(get-set-group [_ set-group-path]
(get set-groups set-group-path))
ITokenThemes
(add-theme [_ token-theme]
(dm/assert! "expected valid token theme" (check-token-theme! token-theme))
(TokensLib. sets
set-groups
(update themes (:group token-theme) d/oassoc (:name token-theme) token-theme)
active-themes))
(update-theme [this group name f]
(let [theme (dm/get-in themes [group name])]
(if theme
(let [theme' (-> (make-token-theme (f theme))
(assoc :modified-at (dt/now)))
group' (:group theme')
name' (:name theme')
same-group? (= group group')
same-name? (= name name')
same-path? (and same-group? same-name?)]
(check-token-theme! theme')
(TokensLib. sets
set-groups
(if same-path?
(update themes group' assoc name' theme')
(-> themes
(d/oassoc-in-before [group name] [group' name'] theme')
(d/dissoc-in [group name])))
(if same-path?
active-themes
(disj active-themes (token-theme-path group name)))))
this)))
(delete-theme [_ group name]
(TokensLib. sets
set-groups
(d/dissoc-in themes [group name])
(disj active-themes (token-theme-path group name))))
(get-theme-tree [_]
themes)
(get-theme-groups [_]
(into [] (comp
(map key)
(remove top-level-theme-group?))
themes))
(get-themes [_]
(->> (tree-seq d/ordered-map? vals themes)
(filter (partial instance? TokenTheme))))
(theme-count [this]
(count (get-themes this)))
(get-theme [_ group name]
(dm/get-in themes [group name]))
(set-active-themes [_ active-themes]
(TokensLib. sets
set-groups
themes
active-themes))
(activate-theme [this group name]
(if-let [theme (get-theme this group name)]
(let [group-themes (->> (get themes group)
(map (comp theme-path val))
(into #{}))
active-themes' (-> (set/difference active-themes group-themes)
(conj (theme-path theme)))]
(TokensLib. sets
set-groups
themes
active-themes'))
this))
(deactivate-theme [_ group name]
(TokensLib. sets
set-groups
themes
(disj active-themes (token-theme-path group name))))
(theme-active? [_ group name]
(contains? active-themes (token-theme-path group name)))
(toggle-theme-active? [this group name]
(if (theme-active? this group name)
(deactivate-theme this group name)
(activate-theme this group name)))
(get-active-theme-paths [_]
active-themes)
(get-active-themes [this]
(into
(list)
(comp
(filter (partial instance? TokenTheme))
(filter #(theme-active? this (:group %) (:name %))))
(tree-seq d/ordered-map? vals themes)))
ITokensLib
(add-token-in-set [this set-name token]
(dm/assert! "expected valid token instance" (check-token! token))
(if (contains? sets set-name)
(TokensLib. (update sets set-name add-token token)
set-groups
themes
active-themes)
this))
(update-token-in-set [this set-name token-name f]
(if (contains? sets set-name)
(TokensLib. (update sets set-name
#(update-token % token-name f))
set-groups
themes
active-themes)
this))
(delete-token-from-set [this set-name token-name]
(if (contains? sets set-name)
(TokensLib. (update sets set-name
#(delete-token % token-name))
set-groups
themes
active-themes)
this))
(toggle-set-in-theme [this theme-group theme-name set-name]
(if-let [_theme (get-in themes theme-group theme-name)]
(TokensLib. sets
set-groups
(d/oupdate-in themes [theme-group theme-name]
#(toggle-set % set-name))
active-themes)
this))
(get-active-themes-set-names [this]
(into #{}
(mapcat :sets)
(get-active-themes this)))
(get-active-themes-set-tokens [this]
(let [sets-order (get-ordered-set-names this)
active-themes (get-active-themes this)
order-theme-set (fn [theme]
(filter #(contains? (set (:sets theme)) %) sets-order))]
(reduce
(fn [tokens theme]
(reduce
(fn [tokens' cur]
(merge tokens' (:tokens (get-set this cur))))
tokens (order-theme-set theme)))
(d/ordered-map) active-themes)))
;; TODO Move to `update-set`
(update-set-name [_ old-set-name new-set-name]
(TokensLib. sets
set-groups
(walk/postwalk
(fn [form]
(if (instance? TokenTheme form)
(-> form
(update :sets disj old-set-name)
(update :sets conj new-set-name))
form))
themes)
active-themes))
(encode-dtcg [_]
(into {} (map (fn [[k v]]
[k (get-dtcg-tokens-tree v)])
sets)))
(decode-dtcg-json [_ parsed-json]
(let [token-sets (into (d/ordered-map)
(map (fn [[set-name tokens]]
[set-name (make-token-set
:name set-name
:tokens (flatten-nested-tokens-json tokens ""))]))
(-> parsed-json
;; tokens-studio/plugin will add these meta properties, remove them for now
(dissoc "$themes" "$metadata")))]
(TokensLib. token-sets
set-groups
themes
active-themes)))
(get-all-tokens [this]
(reduce
(fn [tokens' set]
(into tokens' (map (fn [x] [(:name x) x]) (get-tokens set))))
{} (get-sets this)))
(validate [_]
(and (valid-token-sets? sets) ;; TODO: validate set-groups
(valid-token-themes? themes)
(valid-active-token-themes? active-themes))))
(defn valid-tokens-lib?
[o]
(and (instance? TokensLib o)
(validate o)))
(defn check-tokens-lib!
[lib]
(dm/assert!
"expected valid tokens lib"
(valid-tokens-lib? lib)))
(defn make-tokens-lib
"Create an empty or prepopulated tokens library."
([]
;; NOTE: is possible that ordered map is not the most apropriate
;; data structure and maybe we need a specific that allows us an
;; easy way to reorder it, or just store inside Tokens data
;; structure the data and the order separately as we already do
;; with pages and pages-index.
(make-tokens-lib :sets (d/ordered-map)
:set-groups {}
:themes (d/ordered-map)
:active-themes #{}))
([& {:keys [sets set-groups themes active-themes]}]
(let [tokens-lib (TokensLib. sets set-groups themes (or active-themes #{}))]
(dm/assert!
"expected valid tokens lib"
(valid-tokens-lib? tokens-lib))
tokens-lib)))
(defn ensure-tokens-lib
[tokens-lib]
(or tokens-lib (make-tokens-lib)))
(def type:tokens-lib
{:type ::tokens-lib
:pred valid-tokens-lib?})
(sm/register! ::tokens-lib type:tokens-lib)
;; === Serialization handlers for RPC API (transit) and database (fressian)
(t/add-handlers!
{:id "penpot/tokens-lib"
:class TokensLib
:wfn deref
:rfn #(make-tokens-lib %)}
{:id "penpot/token-set"
:class TokenSet
:wfn #(into {} %)
:rfn #(make-token-set %)}
{:id "penpot/token-theme"
:class TokenTheme
:wfn #(into {} %)
:rfn #(make-token-theme %)}
{:id "penpot/token"
:class Token
:wfn #(into {} %)
:rfn #(make-token %)})
#?(:clj
(fres/add-handlers!
{:name "penpot/token/v1"
:class Token
:wfn (fn [n w o]
(fres/write-tag! w n 1)
(fres/write-object! w (into {} o)))
:rfn (fn [r]
(let [obj (fres/read-object! r)]
(map->Token obj)))}
{:name "penpot/token-set/v1"
:class TokenSet
:wfn (fn [n w o]
(fres/write-tag! w n 1)
(fres/write-object! w (into {} o)))
:rfn (fn [r]
(let [obj (fres/read-object! r)]
(map->TokenSet obj)))}
{:name "penpot/token-theme/v1"
:class TokenTheme
:wfn (fn [n w o]
(fres/write-tag! w n 1)
(fres/write-object! w (into {} o)))
:rfn (fn [r]
(let [obj (fres/read-object! r)]
(map->TokenTheme obj)))}
{:name "penpot/tokens-lib/v1"
:class TokensLib
:wfn (fn [n w o]
(fres/write-tag! w n 3)
(fres/write-object! w (.-sets o))
(fres/write-object! w (.-set-groups o))
(fres/write-object! w (.-themes o))
(fres/write-object! w (.-active-themes o)))
:rfn (fn [r]
(let [sets (fres/read-object! r)
set-groups (fres/read-object! r)
themes (fres/read-object! r)
active-themes (fres/read-object! r)]
(->TokensLib sets set-groups themes active-themes)))}))

View file

@ -0,0 +1,49 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS INC
(ns app.common.types.tokens-list
(:require
[app.common.data :as d]
[app.common.time :as dt]))
(defn tokens-seq
"Returns a sequence of all tokens within the file data."
[file-data]
(vals (:tokens file-data)))
(defn- touch
"Updates the `modified-at` timestamp of a token."
[token]
(assoc token :modified-at (dt/now)))
(defn add-token
"Adds a new token to the file data, setting its `modified-at` timestamp."
[file-data token-set-id token]
(-> file-data
(update :tokens assoc (:id token) (touch token))
(d/update-in-when [:token-sets-index token-set-id] #(->
(update % :tokens conj (:id token))
(touch)))))
(defn get-token
"Retrieves a token by its ID from the file data."
[file-data token-id]
(get-in file-data [:tokens token-id]))
(defn set-token
"Sets or updates a token in the file data, updating its `modified-at` timestamp."
[file-data token]
(d/assoc-in-when file-data [:tokens (:id token)] (touch token)))
(defn update-token
"Applies a function to update a token in the file data, then touches it."
[file-data token-id f & args]
(d/update-in-when file-data [:tokens token-id] #(-> (apply f % args) (touch))))
(defn delete-token
"Removes a token from the file data by its ID."
[file-data token-id]
(update file-data :tokens dissoc token-id))

View file

@ -0,0 +1,79 @@
;; 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.types.tokens-theme-list
(:require
[app.common.data :as d]
[app.common.time :as dt]))
(defn- touch
"Updates the `modified-at` timestamp of a token set."
[token-set]
(assoc token-set :modified-at (dt/now)))
(defn assoc-active-token-themes
[file-data theme-ids]
(assoc file-data :token-active-themes theme-ids))
(defn add-temporary-token-theme
[file-data {:keys [id name] :as token-theme}]
(-> file-data
(d/dissoc-in [:token-themes-index (:token-theme-temporary-id file-data)])
(assoc :token-theme-temporary-id id)
(assoc :token-theme-temporary-name name)
(update :token-themes-index assoc id token-theme)))
(defn delete-temporary-token-theme
[file-data token-theme-id]
(cond-> file-data
(= (:token-theme-temporary-id file-data) token-theme-id) (dissoc :token-theme-temporary-id :token-theme-temporary-name)
:always (d/dissoc-in [:token-themes-index (:token-theme-temporary-id file-data)])))
(defn add-token-theme
[file-data {:keys [index id] :as token-theme}]
(-> file-data
(update :token-themes
(fn [token-themes]
(let [exists? (some (partial = id) token-themes)]
(cond
exists? token-themes
(nil? index) (conj (or token-themes []) id)
:else (d/insert-at-index token-themes index [id])))))
(update :token-themes-index assoc id token-theme)))
(defn update-token-theme
[file-data token-theme-id f & args]
(d/update-in-when file-data [:token-themes-index token-theme-id] #(-> (apply f % args) (touch))))
(defn delete-token-theme
[file-data theme-id]
(-> file-data
(update :token-themes (fn [ids] (d/removev #(= % theme-id) ids)))
(update :token-themes-index dissoc theme-id)
(update :token-active-themes disj theme-id)))
(defn add-token-set
[file-data {:keys [index id] :as token-set}]
(-> file-data
(update :token-set-groups
(fn [token-set-groups]
(let [exists? (some (partial = id) token-set-groups)]
(cond
exists? token-set-groups
(nil? index) (conj (or token-set-groups []) id)
:else (d/insert-at-index token-set-groups index [id])))))
(update :token-sets-index assoc id token-set)))
(defn update-token-set
[file-data token-set-id f & args]
(d/update-in-when file-data [:token-sets-index token-set-id] #(-> (apply f % args) (touch))))
(defn delete-token-set
[file-data token-set-id]
(-> file-data
(update :token-set-groups (fn [xs] (into [] (remove #(= (:id %) token-set-id) xs))))
(update :token-sets-index dissoc token-set-id)
(update :token-themes-index (fn [xs] (update-vals xs #(update % :sets disj token-set-id))))))

View file

@ -0,0 +1,803 @@
{
"core": {
"dimension": {
"scale": {
"$value": "2",
"$type": "dimension"
},
"xs": {
"$value": "4",
"$type": "dimension"
},
"sm": {
"$value": "{dimension.xs} * {dimension.scale}",
"$type": "dimension"
},
"md": {
"$value": "{dimension.sm} * {dimension.scale}",
"$type": "dimension"
},
"lg": {
"$value": "{dimension.md} * {dimension.scale}",
"$type": "dimension"
},
"xl": {
"$value": "{dimension.lg} * {dimension.scale}",
"$type": "dimension"
}
},
"spacing": {
"xs": {
"$value": "{dimension.xs}",
"$type": "spacing"
},
"sm": {
"$value": "{dimension.sm}",
"$type": "spacing"
},
"md": {
"$value": "{dimension.md}",
"$type": "spacing"
},
"lg": {
"$value": "{dimension.lg}",
"$type": "spacing"
},
"xl": {
"$value": "{dimension.xl}",
"$type": "spacing"
},
"multi-value": {
"$value": "{dimension.sm} {dimension.xl}",
"$type": "spacing",
"$description": "You can have multiple values in a single spacing token"
}
},
"borderRadius": {
"sm": {
"$value": "4",
"$type": "borderRadius"
},
"lg": {
"$value": "8",
"$type": "borderRadius"
},
"xl": {
"$value": "16",
"$type": "borderRadius"
},
"multi-value": {
"$value": "{borderRadius.sm} {borderRadius.lg}",
"$type": "borderRadius",
"$description": "You can have multiple values in a single radius token. Read more on these: https://docs.tokens.studio/available-tokens/border-radius-tokens#single--multiple-values"
}
},
"colors": {
"black": {
"$value": "#000000",
"$type": "color"
},
"white": {
"$value": "#ffffff",
"$type": "color"
},
"gray": {
"100": {
"$value": "#f7fafc",
"$type": "color"
},
"200": {
"$value": "#edf2f7",
"$type": "color"
},
"300": {
"$value": "#e2e8f0",
"$type": "color"
},
"400": {
"$value": "#cbd5e0",
"$type": "color"
},
"500": {
"$value": "#a0aec0",
"$type": "color"
},
"600": {
"$value": "#718096",
"$type": "color"
},
"700": {
"$value": "#4a5568",
"$type": "color"
},
"800": {
"$value": "#2d3748",
"$type": "color"
},
"900": {
"$value": "#1a202c",
"$type": "color"
}
},
"red": {
"100": {
"$value": "#fff5f5",
"$type": "color"
},
"200": {
"$value": "#fed7d7",
"$type": "color"
},
"300": {
"$value": "#feb2b2",
"$type": "color"
},
"400": {
"$value": "#fc8181",
"$type": "color"
},
"500": {
"$value": "#f56565",
"$type": "color"
},
"600": {
"$value": "#e53e3e",
"$type": "color"
},
"700": {
"$value": "#c53030",
"$type": "color"
},
"800": {
"$value": "#9b2c2c",
"$type": "color"
},
"900": {
"$value": "#742a2a",
"$type": "color"
}
},
"orange": {
"100": {
"$value": "#fffaf0",
"$type": "color"
},
"200": {
"$value": "#feebc8",
"$type": "color"
},
"300": {
"$value": "#fbd38d",
"$type": "color"
},
"400": {
"$value": "#f6ad55",
"$type": "color"
},
"500": {
"$value": "#ed8936",
"$type": "color"
},
"600": {
"$value": "#dd6b20",
"$type": "color"
},
"700": {
"$value": "#c05621",
"$type": "color"
},
"800": {
"$value": "#9c4221",
"$type": "color"
},
"900": {
"$value": "#7b341e",
"$type": "color"
}
},
"yellow": {
"100": {
"$value": "#fffff0",
"$type": "color"
},
"200": {
"$value": "#fefcbf",
"$type": "color"
},
"300": {
"$value": "#faf089",
"$type": "color"
},
"400": {
"$value": "#f6e05e",
"$type": "color"
},
"500": {
"$value": "#ecc94b",
"$type": "color"
},
"600": {
"$value": "#d69e2e",
"$type": "color"
},
"700": {
"$value": "#b7791f",
"$type": "color"
},
"800": {
"$value": "#975a16",
"$type": "color"
},
"900": {
"$value": "#744210",
"$type": "color"
}
},
"green": {
"100": {
"$value": "#f0fff4",
"$type": "color"
},
"200": {
"$value": "#c6f6d5",
"$type": "color"
},
"300": {
"$value": "#9ae6b4",
"$type": "color"
},
"400": {
"$value": "#68d391",
"$type": "color"
},
"500": {
"$value": "#48bb78",
"$type": "color"
},
"600": {
"$value": "#38a169",
"$type": "color"
},
"700": {
"$value": "#2f855a",
"$type": "color"
},
"800": {
"$value": "#276749",
"$type": "color"
},
"900": {
"$value": "#22543d",
"$type": "color"
}
},
"teal": {
"100": {
"$value": "#e6fffa",
"$type": "color"
},
"200": {
"$value": "#b2f5ea",
"$type": "color"
},
"300": {
"$value": "#81e6d9",
"$type": "color"
},
"400": {
"$value": "#4fd1c5",
"$type": "color"
},
"500": {
"$value": "#38b2ac",
"$type": "color"
},
"600": {
"$value": "#319795",
"$type": "color"
},
"700": {
"$value": "#2c7a7b",
"$type": "color"
},
"800": {
"$value": "#285e61",
"$type": "color"
},
"900": {
"$value": "#234e52",
"$type": "color"
}
},
"blue": {
"100": {
"$value": "#ebf8ff",
"$type": "color"
},
"200": {
"$value": "#bee3f8",
"$type": "color"
},
"300": {
"$value": "#90cdf4",
"$type": "color"
},
"400": {
"$value": "#63b3ed",
"$type": "color"
},
"500": {
"$value": "#4299e1",
"$type": "color"
},
"600": {
"$value": "#3182ce",
"$type": "color"
},
"700": {
"$value": "#2b6cb0",
"$type": "color"
},
"800": {
"$value": "#2c5282",
"$type": "color"
},
"900": {
"$value": "#2a4365",
"$type": "color"
}
},
"indigo": {
"100": {
"$value": "#ebf4ff",
"$type": "color"
},
"200": {
"$value": "#c3dafe",
"$type": "color"
},
"300": {
"$value": "#a3bffa",
"$type": "color"
},
"400": {
"$value": "#7f9cf5",
"$type": "color"
},
"500": {
"$value": "#667eea",
"$type": "color"
},
"600": {
"$value": "#5a67d8",
"$type": "color"
},
"700": {
"$value": "#4c51bf",
"$type": "color"
},
"800": {
"$value": "#434190",
"$type": "color"
},
"900": {
"$value": "#3c366b",
"$type": "color"
}
},
"purple": {
"100": {
"$value": "#faf5ff",
"$type": "color"
},
"200": {
"$value": "#e9d8fd",
"$type": "color"
},
"300": {
"$value": "#d6bcfa",
"$type": "color"
},
"400": {
"$value": "#b794f4",
"$type": "color"
},
"500": {
"$value": "#9f7aea",
"$type": "color"
},
"600": {
"$value": "#805ad5",
"$type": "color"
},
"700": {
"$value": "#6b46c1",
"$type": "color"
},
"800": {
"$value": "#553c9a",
"$type": "color"
},
"900": {
"$value": "#44337a",
"$type": "color"
}
},
"pink": {
"100": {
"$value": "#fff5f7",
"$type": "color"
},
"200": {
"$value": "#fed7e2",
"$type": "color"
},
"300": {
"$value": "#fbb6ce",
"$type": "color"
},
"400": {
"$value": "#f687b3",
"$type": "color"
},
"500": {
"$value": "#ed64a6",
"$type": "color"
},
"600": {
"$value": "#d53f8c",
"$type": "color"
},
"700": {
"$value": "#b83280",
"$type": "color"
},
"800": {
"$value": "#97266d",
"$type": "color"
},
"900": {
"$value": "#702459",
"$type": "color"
}
}
},
"opacity": {
"low": {
"$value": "10%",
"$type": "opacity"
},
"md": {
"$value": "50%",
"$type": "opacity"
},
"high": {
"$value": "90%",
"$type": "opacity"
}
},
"fontFamilies": {
"heading": {
"$value": "Inter",
"$type": "fontFamilies"
},
"body": {
"$value": "Roboto",
"$type": "fontFamilies"
}
},
"lineHeights": {
"heading": {
"$value": "110%",
"$type": "lineHeights"
},
"body": {
"$value": "140%",
"$type": "lineHeights"
}
},
"letterSpacing": {
"default": {
"$value": "0",
"$type": "letterSpacing"
},
"increased": {
"$value": "150%",
"$type": "letterSpacing"
},
"decreased": {
"$value": "-5%",
"$type": "letterSpacing"
}
},
"paragraphSpacing": {
"h1": {
"$value": "32",
"$type": "paragraphSpacing"
},
"h2": {
"$value": "26",
"$type": "paragraphSpacing"
}
},
"fontWeights": {
"headingRegular": {
"$value": "Regular",
"$type": "fontWeights"
},
"headingBold": {
"$value": "Bold",
"$type": "fontWeights"
},
"bodyRegular": {
"$value": "Regular",
"$type": "fontWeights"
},
"bodyBold": {
"$value": "Bold",
"$type": "fontWeights"
}
},
"fontSizes": {
"h1": {
"$value": "{fontSizes.h2} * 1.25",
"$type": "fontSizes"
},
"h2": {
"$value": "{fontSizes.h3} * 1.25",
"$type": "fontSizes"
},
"h3": {
"$value": "{fontSizes.h4} * 1.25",
"$type": "fontSizes"
},
"h4": {
"$value": "{fontSizes.h5} * 1.25",
"$type": "fontSizes"
},
"h5": {
"$value": "{fontSizes.h6} * 1.25",
"$type": "fontSizes"
},
"h6": {
"$value": "{fontSizes.body} * 1",
"$type": "fontSizes"
},
"body": {
"$value": "16",
"$type": "fontSizes"
},
"sm": {
"$value": "{fontSizes.body} * 0.85",
"$type": "fontSizes"
},
"xs": {
"$value": "{fontSizes.body} * 0.65",
"$type": "fontSizes"
}
}
},
"light": {
"fg": {
"default": {
"$value": "{colors.black}",
"$type": "color"
},
"muted": {
"$value": "{colors.gray.700}",
"$type": "color"
},
"subtle": {
"$value": "{colors.gray.500}",
"$type": "color"
}
},
"bg": {
"default": {
"$value": "{colors.white}",
"$type": "color"
},
"muted": {
"$value": "{colors.gray.100}",
"$type": "color"
},
"subtle": {
"$value": "{colors.gray.200}",
"$type": "color"
}
},
"accent": {
"default": {
"$value": "{colors.indigo.400}",
"$type": "color"
},
"onAccent": {
"$value": "{colors.white}",
"$type": "color"
},
"bg": {
"$value": "{colors.indigo.200}",
"$type": "color"
}
},
"shadows": {
"default": {
"$value": "{colors.gray.900}",
"$type": "color"
}
}
},
"dark": {
"fg": {
"default": {
"$value": "{colors.white}",
"$type": "color"
},
"muted": {
"$value": "{colors.gray.300}",
"$type": "color"
},
"subtle": {
"$value": "{colors.gray.500}",
"$type": "color"
}
},
"bg": {
"default": {
"$value": "{colors.gray.900}",
"$type": "color"
},
"muted": {
"$value": "{colors.gray.700}",
"$type": "color"
},
"subtle": {
"$value": "{colors.gray.600}",
"$type": "color"
}
},
"accent": {
"default": {
"$value": "{colors.indigo.600}",
"$type": "color"
},
"onAccent": {
"$value": "{colors.white}",
"$type": "color"
},
"bg": {
"$value": "{colors.indigo.800}",
"$type": "color"
}
},
"shadows": {
"default": {
"$value": "rgba({colors.black}, 0.3)",
"$type": "color"
}
}
},
"theme": {
"button": {
"primary": {
"background": {
"$value": "{accent.default}",
"$type": "color"
},
"text": {
"$value": "{accent.onAccent}",
"$type": "color"
}
},
"borderRadius": {
"$value": "{borderRadius.lg}",
"$type": "borderRadius"
},
"borderWidth": {
"$value": "{dimension.sm}",
"$type": "borderWidth"
}
},
"card": {
"borderRadius": {
"$value": "{borderRadius.lg}",
"$type": "borderRadius"
},
"background": {
"$value": "{bg.default}",
"$type": "color"
},
"padding": {
"$value": "{dimension.md}",
"$type": "dimension"
}
},
"boxShadow": {
"default": {
"$value": [
{
"x": 5,
"y": 5,
"spread": 3,
"color": "rgba({shadows.default}, 0.15)",
"blur": 5,
"$type": "dropShadow"
},
{
"x": 4,
"y": 4,
"spread": 6,
"color": "#00000033",
"blur": 5,
"$type": "innerShadow"
}
],
"$type": "boxShadow"
}
},
"typography": {
"H1": {
"Bold": {
"$value": {
"fontFamily": "{fontFamilies.heading}",
"fontWeight": "{fontWeights.headingBold}",
"lineHeight": "{lineHeights.heading}",
"fontSize": "{fontSizes.h1}",
"paragraphSpacing": "{paragraphSpacing.h1}",
"letterSpacing": "{letterSpacing.decreased}"
},
"$type": "typography"
},
"Regular": {
"$value": {
"fontFamily": "{fontFamilies.heading}",
"fontWeight": "{fontWeights.headingRegular}",
"lineHeight": "{lineHeights.heading}",
"fontSize": "{fontSizes.h1}",
"paragraphSpacing": "{paragraphSpacing.h1}",
"letterSpacing": "{letterSpacing.decreased}"
},
"$type": "typography"
}
},
"H2": {
"Bold": {
"$value": {
"fontFamily": "{fontFamilies.heading}",
"fontWeight": "{fontWeights.headingBold}",
"lineHeight": "{lineHeights.heading}",
"fontSize": "{fontSizes.h2}",
"paragraphSpacing": "{paragraphSpacing.h2}",
"letterSpacing": "{letterSpacing.decreased}"
},
"$type": "typography"
},
"Regular": {
"$value": {
"fontFamily": "{fontFamilies.heading}",
"fontWeight": "{fontWeights.headingRegular}",
"lineHeight": "{lineHeights.heading}",
"fontSize": "{fontSizes.h2}",
"paragraphSpacing": "{paragraphSpacing.h2}",
"letterSpacing": "{letterSpacing.decreased}"
},
"$type": "typography"
}
},
"Body": {
"$value": {
"fontFamily": "{fontFamilies.body}",
"fontWeight": "{fontWeights.bodyRegular}",
"lineHeight": "{lineHeights.heading}",
"fontSize": "{fontSizes.body}",
"paragraphSpacing": "{paragraphSpacing.h2}"
},
"$type": "typography"
}
}
},
"$themes": [],
"$metadata": {
"tokenSetOrder": ["core", "light", "dark", "theme"]
}
}

File diff suppressed because it is too large Load diff

View file

@ -32,9 +32,9 @@
"lint:clj": "clj-kondo --parallel --lint src/",
"lint:scss": "yarn run prettier -c resources/styles -c src/**/*.scss",
"lint:scss:fix": "yarn run prettier -c resources/styles -c src/**/*.scss -w",
"build:test": "clojure -M:dev:shadow-cljs compile test-esm",
"build:test": "clojure -M:dev:shadow-cljs compile test",
"test": "yarn run build:test && node target/tests/test.js",
"watch:test": "concurrently \"clojure -M:dev:shadow-cljs watch test-esm\" \"nodemon -w target/tests/test.js --exec 'sleep 2 && node target/tests/test.js'\"",
"watch:test": "concurrently \"clojure -M:dev:shadow-cljs watch test\" \"nodemon -w target/tests/test.js --exec 'sleep 2 && node target/tests/test.js'\"",
"test:e2e": "playwright test --project default",
"translations": "node ./scripts/translations.js",
"watch:app:assets": "node ./scripts/watch.js",
@ -54,6 +54,7 @@
"@storybook/react-vite": "^8.3.6",
"@types/node": "^22.7.7",
"autoprefixer": "^10.4.20",
"bun": "^1.1.25",
"concurrently": "^9.0.1",
"esbuild": "^0.24.0",
"express": "^4.21.1",
@ -100,12 +101,14 @@
"@penpot/mousetrap": "file:./vendor/mousetrap",
"@penpot/svgo": "penpot/svgo#c6fba7a4dcfbc27b643e7fc0c94fc98cf680b77b",
"@penpot/text-editor": "penpot/penpot-text-editor#449e3322f3fa40b1318c9154afbbc7932a3cb766",
"@tokens-studio/sd-transforms": "^0.16.1",
"compression": "^1.7.4",
"date-fns": "^4.1.0",
"eventsource-parser": "^3.0.0",
"js-beautify": "^1.15.1",
"jszip": "^3.10.1",
"lodash": "^4.17.21",
"lodash.debounce": "^4.0.8",
"luxon": "^3.5.0",
"opentype.js": "^1.3.4",
"postcss-modules": "^6.0.0",
@ -117,7 +120,9 @@
"rxjs": "8.0.0-alpha.14",
"sax": "^1.4.1",
"source-map-support": "^0.5.21",
"style-dictionary": "^4.1.4",
"tdigest": "^0.1.2",
"tinycolor2": "npm:^1.6.0",
"ua-parser-js": "2.0.0-rc.1",
"xregexp": "^5.1.1"
}

View file

@ -0,0 +1,3 @@
<svg viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" stroke-linecap="round" stroke-linejoin="round">
<path d="m4 6 4 4 4-4"/>
</svg>

After

Width:  |  Height:  |  Size: 144 B

View file

@ -0,0 +1,3 @@
<svg viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" stroke-linecap="round" stroke-linejoin="round">
<path d="M10 12 6 8l4-4"/>
</svg>

After

Width:  |  Height:  |  Size: 145 B

View file

@ -0,0 +1,3 @@
<svg viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" stroke-linecap="round" stroke-linejoin="round">
<path d="m6 12 4-4-4-4"/>
</svg>

After

Width:  |  Height:  |  Size: 144 B

View file

@ -0,0 +1,3 @@
<svg viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" stroke-linecap="round" stroke-linejoin="round">
<path d="m4 10 4-4 4 4"/>
</svg>

After

Width:  |  Height:  |  Size: 140 B

View file

@ -8,8 +8,7 @@
{:target :browser
:output-dir "resources/public/js/"
:asset-path "/js"
:devtools {:browser-inject :main
:watch-dir "resources/public"
:devtools {:watch-dir "resources/public"
:reload-strategy :full}
:build-options {:manifest-name "manifest.json"}
:module-loader true
@ -136,38 +135,37 @@
:output-wrapper false
:warnings {:fn-deprecated false}}}
:test-esm
{:target :esm
:output-dir "target/tests"
:runtime :custom
:test
{:target :esm
:output-dir "target/tests"
:runtime :custom
:js-options {:js-provider :import}
:modules
{:test {:init-fn frontend-tests.runner/init}}}
;; :compiler-options
;; {:output-feature-set :es2020
;; :warnings {:fn-deprecated false}}}
:modules
{:test {:init-fn frontend-tests.runner/init
:prepend-js "globalThis.navigator = {userAgent: \"\"}"}}}
:lib-penpot
{:target :esm
:output-dir "resources/public/libs"
{:target :esm
:output-dir "resources/public/libs"
:modules
{:penpot {:exports {:renderPage app.libs.render/render-page-export
:createFile app.libs.file-builder/create-file-export}}}
:modules
{:penpot {:exports {:renderPage app.libs.render/render-page-export
:createFile app.libs.file-builder/create-file-export}}}
:compiler-options
{:output-feature-set :es2020
:output-wrapper false
:warnings {:fn-deprecated false}}
:compiler-options
{:output-feature-set :es2020
:output-wrapper false
:warnings {:fn-deprecated false}}
:release
{:compiler-options
{:fn-invoke-direct true
:source-map true
:elide-asserts true
:anon-fn-naming-policy :off
:source-map-detail-level :all}}}
:release
{:compiler-options
{:fn-invoke-direct true
:source-map true
:elide-asserts true
:anon-fn-naming-policy :off
:source-map-detail-level :all}}}
:bench
{:target :node-script
@ -184,24 +182,5 @@
{:compiler-options
{:fn-invoke-direct true
:elide-asserts true
:anon-fn-naming-policy :off}}}
:anon-fn-naming-policy :off}}}}}
:test
{:target :node-test
:output-to "target/tests.cjs"
:output-dir "target/test/"
:ns-regexp "^frontend-tests.*-test$"
:autorun true
:js-options
{:entry-keys ["module" "browser" "main"]}
:compiler-options
{:output-feature-set :es2020
:output-wrapper false
:source-map true
:source-map-include-sources-content true
:source-map-detail-level :all
:warnings {:fn-deprecated false}}}
}}

View file

@ -0,0 +1,352 @@
;; 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.main.data.tokens
(:require
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.files.changes-builder :as pcb]
[app.common.geom.point :as gpt]
[app.common.types.shape :as cts]
[app.common.types.tokens-lib :as ctob]
[app.main.data.changes :as dch]
[app.main.data.workspace.shapes :as dwsh]
[app.main.refs :as refs]
[app.main.ui.workspace.tokens.token :as wtt]
[app.main.ui.workspace.tokens.token-set :as wtts]
[app.main.ui.workspace.tokens.update :as wtu]
[beicon.v2.core :as rx]
[clojure.data :as data]
[cuerdas.core :as str]
[potok.v2.core :as ptk]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Helpers
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; TODO HYMA: Copied over from workspace.cljs
(defn update-shape
[id attrs]
(dm/assert!
"expected valid parameters"
(and (cts/check-shape-attrs! attrs)
(uuid? id)))
(ptk/reify ::update-shape
ptk/WatchEvent
(watch [_ _ _]
(rx/of (dwsh/update-shapes [id] #(merge % attrs))))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; TOKENS Getters
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn get-tokens-lib [state]
(get-in state [:workspace-data :tokens-lib]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; TOKENS Actions
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(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 (wtt/token-identifier token)]) attributes)
(into {})))
(defn unapply-token-id [shape attributes]
(update shape :applied-tokens d/without-keys attributes))
(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 maybe-apply-token-to-shape
"When the passed `:token` is non-nil apply it to the `:applied-tokens` on a shape."
[{:keys [shape token _attributes] :as props}]
(if token
(apply-token-to-shape props)
shape))
(defn get-token-data-from-token-id
[id]
(let [workspace-data (deref refs/workspace-data)]
(get (:tokens workspace-data) id)))
(defn set-selected-token-set-id
[id]
(ptk/reify ::set-selected-token-set-id
ptk/UpdateEvent
(update [_ state]
(wtts/assoc-selected-token-set-id state id))))
(defn create-token-theme [token-theme]
(let [new-token-theme token-theme]
(ptk/reify ::create-token-theme
ptk/WatchEvent
(watch [it _ _]
(let [changes (-> (pcb/empty-changes it)
(pcb/add-token-theme new-token-theme))]
(rx/of
(dch/commit-changes changes)))))))
(defn update-token-theme [[group name] token-theme]
(ptk/reify ::update-token-theme
ptk/WatchEvent
(watch [it state _]
(let [tokens-lib (get-tokens-lib state)
prev-token-theme (some-> tokens-lib (ctob/get-theme group name))
changes (pcb/update-token-theme (pcb/empty-changes it) token-theme prev-token-theme)]
(rx/of
(dch/commit-changes changes))))))
(defn toggle-token-theme-active? [group name]
(ptk/reify ::toggle-token-theme-active?
ptk/WatchEvent
(watch [it state _]
(let [tokens-lib (get-tokens-lib state)
prev-active-token-themes (some-> tokens-lib
(ctob/get-active-theme-paths))
active-token-themes (some-> tokens-lib
(ctob/toggle-theme-active? group name)
(ctob/get-active-theme-paths))
active-token-themes' (if (= active-token-themes #{ctob/hidden-token-theme-path})
active-token-themes
(disj active-token-themes ctob/hidden-token-theme-path))
changes (-> (pcb/empty-changes it)
(pcb/update-active-token-themes active-token-themes' prev-active-token-themes))]
(rx/of
(dch/commit-changes changes)
(wtu/update-workspace-tokens))))))
(defn delete-token-theme [group name]
(ptk/reify ::delete-token-theme
ptk/WatchEvent
(watch [it state _]
(let [data (get state :workspace-data)
changes (-> (pcb/empty-changes it)
(pcb/with-library-data data)
(pcb/delete-token-theme group name))]
(rx/of
(dch/commit-changes changes)
(wtu/update-workspace-tokens))))))
(defn create-token-set [token-set]
(let [new-token-set (merge
{:name "Token Set"
:tokens []}
token-set)]
(ptk/reify ::create-token-set
ptk/WatchEvent
(watch [it _ _]
(let [changes (-> (pcb/empty-changes it)
(pcb/add-token-set new-token-set))]
(rx/of
(set-selected-token-set-id (:name new-token-set))
(dch/commit-changes changes)))))))
(defn update-token-set [set-name token-set]
(ptk/reify ::update-token-set
ptk/WatchEvent
(watch [it state _]
(let [prev-token-set (some-> (get-tokens-lib state)
(ctob/get-set set-name))
changes (-> (pcb/empty-changes it)
(pcb/update-token-set token-set prev-token-set))]
(rx/of
(set-selected-token-set-id (:name token-set))
(dch/commit-changes changes))))))
(defn toggle-token-set [{:keys [token-set-name]}]
(ptk/reify ::toggle-token-set
ptk/WatchEvent
(watch [it state _]
(let [tokens-lib (get-tokens-lib state)
prev-theme (ctob/get-theme tokens-lib ctob/hidden-token-theme-group ctob/hidden-token-theme-name)
active-token-set-names (ctob/get-active-themes-set-names tokens-lib)
theme (-> (or (some-> prev-theme
(ctob/set-sets active-token-set-names))
(ctob/make-hidden-token-theme :sets active-token-set-names))
(ctob/toggle-set token-set-name))
prev-active-token-themes (ctob/get-active-theme-paths tokens-lib)
changes (-> (pcb/empty-changes it)
(pcb/update-active-token-themes #{(ctob/token-theme-path ctob/hidden-token-theme-group ctob/hidden-token-theme-name)} prev-active-token-themes))
changes' (if prev-theme
(pcb/update-token-theme changes theme prev-theme)
(pcb/add-token-theme changes theme))]
(rx/of
(dch/commit-changes changes')
(wtu/update-workspace-tokens))))))
(defn import-tokens-lib [lib]
(ptk/reify ::import-tokens-lib
ptk/WatchEvent
(watch [it state _]
(let [data (get state :workspace-data)
update-token-set-change (some-> lib
(ctob/get-sets)
(first)
(:name)
(set-selected-token-set-id))
changes (-> (pcb/empty-changes it)
(pcb/with-library-data data)
(pcb/set-tokens-lib lib))]
(rx/of
(dch/commit-changes changes)
update-token-set-change
(wtu/update-workspace-tokens))))))
(defn delete-token-set [token-set-name]
(ptk/reify ::delete-token-set
ptk/WatchEvent
(watch [it state _]
(let [data (get state :workspace-data)
changes (-> (pcb/empty-changes it)
(pcb/with-library-data data)
(pcb/delete-token-set token-set-name))]
(rx/of
(dch/commit-changes changes)
(wtu/update-workspace-tokens))))))
(defn move-token-set [source-set-name dest-set-name position]
(ptk/reify ::move-token-set
ptk/WatchEvent
(watch [it state _]
(let [tokens-lib (get-tokens-lib state)
prev-before-set-name (ctob/get-neighbor-set-name tokens-lib source-set-name 1)
[source-set-name' dest-set-name'] (if (= :top position)
[source-set-name dest-set-name]
[source-set-name (ctob/get-neighbor-set-name tokens-lib dest-set-name 1)])
changes (-> (pcb/empty-changes it)
(pcb/move-token-set-before source-set-name' dest-set-name' prev-before-set-name))]
(rx/of
(dch/commit-changes changes)
(wtu/update-workspace-tokens))))))
(defn update-create-token
[{:keys [token prev-token-name]}]
(ptk/reify ::update-create-token
ptk/WatchEvent
(watch [_ state _]
(let [token-set (wtts/get-selected-token-set state)
token-set-name (or (:name token-set) "Global")
changes (if (not token-set)
;; No set created add a global set
(let [tokens-lib (get-tokens-lib state)
token-set (ctob/make-token-set :name token-set-name :tokens {(:name token) token})
hidden-theme (ctob/make-hidden-token-theme :sets [token-set-name])
active-theme-paths (some-> tokens-lib ctob/get-active-theme-paths)
add-to-hidden-theme? (= active-theme-paths #{ctob/hidden-token-theme-path})
base-changes (pcb/add-token-set (pcb/empty-changes) token-set)]
(cond
(not tokens-lib) (-> base-changes
(pcb/add-token-theme hidden-theme)
(pcb/update-active-token-themes #{ctob/hidden-token-theme-path} #{}))
add-to-hidden-theme? (let [prev-hidden-theme (ctob/get-theme tokens-lib ctob/hidden-token-theme-group ctob/hidden-token-theme-name)]
(-> base-changes
(pcb/update-token-theme (ctob/toggle-set prev-hidden-theme ctob/hidden-token-theme-path) prev-hidden-theme)))
:else base-changes))
;; Either update or add token to existing set
(if-let [prev-token (ctob/get-token token-set (or prev-token-name (:name token)))]
(pcb/update-token (pcb/empty-changes) (:name token-set) token prev-token)
(pcb/add-token (pcb/empty-changes) (:name token-set) token)))]
(rx/of
(set-selected-token-set-id token-set-name)
(dch/commit-changes changes))))))
(defn delete-token
[set-name token-name]
(dm/assert! (string? set-name))
(dm/assert! (string? token-name))
(ptk/reify ::delete-token
ptk/WatchEvent
(watch [it state _]
(let [data (get state :workspace-data)
changes (-> (pcb/empty-changes it)
(pcb/with-library-data data)
(pcb/delete-token set-name token-name))]
(rx/of (dch/commit-changes changes))))))
(defn duplicate-token
[token-name]
(dm/assert! (string? token-name))
(ptk/reify ::duplicate-token
ptk/WatchEvent
(watch [_ state _]
(when-let [token (some-> (wtts/get-selected-token-set state)
(ctob/get-token token-name)
(update :name #(str/concat % "-copy")))]
(rx/of
(update-create-token {:token token}))))))
(defn set-token-type-section-open
[token-type open?]
(ptk/reify ::set-token-type-section-open
ptk/UpdateEvent
(update [_ state]
(assoc-in state [:workspace-tokens :open-status token-type] open?))))
;; === Token Context Menu
(defn show-token-context-menu
[{:keys [position _token-name] :as params}]
(dm/assert! (gpt/point? position))
(ptk/reify ::show-token-context-menu
ptk/UpdateEvent
(update [_ state]
(assoc-in state [:workspace-local :token-context-menu] params))))
(def hide-token-context-menu
(ptk/reify ::hide-token-context-menu
ptk/UpdateEvent
(update [_ state]
(assoc-in state [:workspace-local :token-context-menu] nil))))
;; === Token Set Context Menu
(defn show-token-set-context-menu
[{:keys [position _token-set-name] :as params}]
(dm/assert! (gpt/point? position))
(ptk/reify ::show-token-set-context-menu
ptk/UpdateEvent
(update [_ state]
(assoc-in state [:workspace-local :token-set-context-menu] params))))
(def hide-token-set-context-menu
(ptk/reify ::hide-token-set-context-menu
ptk/UpdateEvent
(update [_ state]
(assoc-in state [:workspace-local :token-set-context-menu] nil))))
;; === Import Export Context Menu
(defn show-import-export-context-menu
[{:keys [position] :as params}]
(dm/assert! (gpt/point? position))
(ptk/reify ::show-import-export-context-menu
ptk/UpdateEvent
(update [_ state]
(assoc-in state [:workspace-local :import-export-context-menu] params))))
(def hide-import-export-set-context-menu
(ptk/reify ::hide-import-export-set-context-menu
ptk/UpdateEvent
(update [_ state]
(assoc-in state [:workspace-local :import-export-set-context-menu] nil))))

View file

@ -44,7 +44,11 @@
:layers
{:del #{:document-history :assets}
:add #{:sitemap :layers}}})
:add #{:sitemap :layers}}
:tokens
{:del #{:sitemap :layers :document-history :assets}
:add #{:tokens}}})
(def valid-options-mode
#{:design :prototype :inspect})

View file

@ -110,7 +110,9 @@
(when *assert*
(->> (rx/from cfeat/no-migration-features)
;; text editor v2 isn't enabled by default even in devenv
(rx/filter #(not (or (contains? cfeat/backend-only-features %) (= "text-editor/v2" %))))
(rx/filter #(not (or (contains? cfeat/backend-only-features %)
(= "text-editor/v2" %)
(= "design-tokens/v1" %))))
(rx/observe-on :async)
(rx/map enable-feature))))

View file

@ -12,8 +12,10 @@
[app.common.files.helpers :as cph]
[app.common.types.shape-tree :as ctt]
[app.common.types.shape.layout :as ctl]
[app.common.types.tokens-lib :as ctob]
[app.main.data.workspace.state-helpers :as wsh]
[app.main.store :as st]
[app.main.ui.workspace.tokens.token-set :as wtts]
[okulary.core :as l]))
;; ---- Global refs
@ -205,6 +207,9 @@
(def context-menu
(l/derived :context-menu workspace-local))
(def token-context-menu
(l/derived :token-context-menu workspace-local))
;; page item that it is being edited
(def editing-page-item
(l/derived :page-item workspace-local))
@ -448,6 +453,65 @@
ids)))
st/state =))
;; ---- Token refs
(def tokens-lib
(l/derived :tokens-lib workspace-data))
(def workspace-token-theme-groups
(l/derived (d/nilf ctob/get-theme-groups) tokens-lib))
(defn workspace-token-theme
[group name]
(l/derived
(fn [lib]
(when lib
(ctob/get-theme lib group name)))
tokens-lib))
(def workspace-token-theme-tree-no-hidden
(l/derived (fn [lib]
(or
(some-> lib
(ctob/delete-theme ctob/hidden-token-theme-group ctob/hidden-token-theme-name)
(ctob/get-theme-tree))
[]))
tokens-lib))
(def workspace-token-themes
(l/derived #(or (some-> % ctob/get-themes) []) tokens-lib))
(def workspace-token-themes-no-hidden
(l/derived #(remove ctob/hidden-temporary-theme? %) workspace-token-themes))
(def workspace-selected-token-set-id
(l/derived wtts/get-selected-token-set-id st/state))
(def workspace-ordered-token-sets
(l/derived #(or (some-> % ctob/get-sets) []) tokens-lib))
(def workspace-active-theme-paths
(l/derived (d/nilf ctob/get-active-theme-paths) tokens-lib))
(def workspace-active-theme-paths-no-hidden
(l/derived #(disj % ctob/hidden-token-theme-path) workspace-active-theme-paths))
(def workspace-active-set-names
(l/derived (d/nilf ctob/get-active-themes-set-names) tokens-lib))
(def workspace-active-theme-sets-tokens
(l/derived #(or (some-> % ctob/get-active-themes-set-tokens) {}) tokens-lib))
(def workspace-selected-token-set-token
(fn [token-name]
(l/derived
#(some-> (wtts/get-selected-token-set %)
(ctob/get-token token-name))
st/state)))
(def workspace-selected-token-set-tokens
(l/derived #(or (wtts/get-selected-token-set-tokens %) {}) st/state))
;; ---- Viewer refs
(defn lookup-viewer-objects-by-id

View file

@ -99,7 +99,7 @@
content (mf/use-state "")
disabled? (or (str/blank? @content)
(str/empty-or-nil? @content))
(str/empty? @content))
on-focus
(mf/use-fn
@ -159,7 +159,7 @@
pos-y (* (:y position) zoom)
disabled? (or (str/blank? content)
(str/empty-or-nil? content))
(str/empty? content))
on-esc
(mf/use-fn
@ -230,7 +230,7 @@
(fn [] (on-submit @content)))
disabled? (or (str/blank? @content)
(str/empty-or-nil? @content))]
(str/empty? @content))]
[:div {:class (stl/css :edit-form)}
[:& resizing-textarea {:value @content

View file

@ -16,6 +16,8 @@
[cuerdas.core :as str]
[rumext.v2 :as mf]))
(set! *warn-on-infer* false)
(mf/defc tab-element
{::mf/wrap-props false}
[{:keys [children]}]

View file

@ -24,6 +24,7 @@
(def libraries (mf/create-context nil))
(def components-v2 (mf/create-context nil))
(def design-tokens (mf/create-context nil))
(def current-scroll (mf/create-context nil))
(def current-zoom (mf/create-context nil))

View file

@ -130,3 +130,21 @@
box-shadow: inset 0 0 #{px2rem(10)} #{px2rem(2)} rgba(0, 0, 0, 0.2);
}
}
%base-button-action {
--button-bg-color: transparent;
--button-fg-color: var(--color-foreground-secondary);
--button-hover-bg-color: transparent;
--button-hover-fg-color: var(--color-accent-primary);
--button-active-bg-color: var(--color-background-quaternary);
--button-disabled-bg-color: transparent;
--button-disabled-fg-color: var(--color-accent-primary-muted);
--button-focus-bg-color: transparent;
--button-focus-fg-color: var(--color-accent-primary);
--button-focus-inner-ring-color: transparent;
--button-focus-outer-ring-color: var(--color-accent-primary);
}

View file

@ -12,9 +12,6 @@
[app.main.ui.ds.foundations.assets.icon :refer [icon* icon-list]]
[rumext.v2 :as mf]))
(def button-variants (set '("primary" "secondary" "ghost" "destructive")))
(def ^:private schema:icon-button
[:map
[:class {:optional true} :string]
@ -22,7 +19,7 @@
[:and :string [:fn #(contains? icon-list %)]]]
[:aria-label :string]
[:variant {:optional true}
[:maybe [:enum "primary" "secondary" "ghost" "destructive"]]]])
[:maybe [:enum "primary" "secondary" "ghost" "destructive" "action"]]]])
(mf/defc icon-button*
{::mf/props :obj
@ -33,6 +30,7 @@
:icon-button-primary (= variant "primary")
:icon-button-secondary (= variant "secondary")
:icon-button-ghost (= variant "ghost")
:icon-button-action (= variant "action")
:icon-button-destructive (= variant "destructive")))
props (mf/spread-props props {:class class :title aria-label})]
[:> "button" props [:> icon* {:id icon :aria-label aria-label}] children]))

View file

@ -31,3 +31,7 @@
.icon-button-destructive {
@extend %base-button-destructive;
}
.icon-button-action {
@extend %base-button-action;
}

View file

@ -26,7 +26,7 @@ export default {
},
disabled: { control: "boolean" },
variant: {
options: ["primary", "secondary", "ghost", "destructive"],
options: ["primary", "secondary", "ghost", "destructive", "action"],
control: { type: "select" },
},
},
@ -59,6 +59,12 @@ export const Ghost = {
},
};
export const Action = {
args: {
variant: "action",
},
};
export const Destructive = {
args: {
variant: "destructive",

View file

@ -50,6 +50,10 @@
(def ^:icon-id align-top "align-top")
(def ^:icon-id align-vertical-center "align-vertical-center")
(def ^:icon-id arrow "arrow")
(def ^:icon-id arrow-up "arrow-up")
(def ^:icon-id arrow-down "arrow-down")
(def ^:icon-id arrow-left "arrow-left")
(def ^:icon-id arrow-right "arrow-right")
(def ^:icon-id asc-sort "asc-sort")
(def ^:icon-id board "board")
(def ^:icon-id boards-thumbnail "boards-thumbnail")

View file

@ -32,17 +32,17 @@
(let [level (or level "1")
tag (dm/str "h" level)
class (dm/str (or class "") " " (stl/css-case :display-typography (= typography t/display)
:title-large-typography (= typography t/title-large)
:title-medium-typography (= typography t/title-medium)
:title-small-typography (= typography t/title-small)
:headline-large-typography (= typography t/headline-large)
:headline-medium-typography (= typography t/headline-medium)
:headline-small-typography (= typography t/headline-small)
:body-large-typography (= typography t/body-large)
:body-medium-typography (= typography t/body-medium)
:body-small-typography (= typography t/body-small)
:code-font-typography (= typography t/code-font)))
class (dm/str class " " (stl/css-case :display-typography (= typography t/display)
:title-large-typography (= typography t/title-large)
:title-medium-typography (= typography t/title-medium)
:title-small-typography (= typography t/title-small)
:headline-large-typography (= typography t/headline-large)
:headline-medium-typography (= typography t/headline-medium)
:headline-small-typography (= typography t/headline-small)
:body-large-typography (= typography t/body-large)
:body-medium-typography (= typography t/body-medium)
:body-small-typography (= typography t/body-small)
:code-font-typography (= typography t/code-font)))
props (mf/spread-props props {:class class})]
[:> tag props
children]))

View file

@ -30,6 +30,7 @@
[app.main.ui.workspace.sidebar :refer [left-sidebar right-sidebar]]
[app.main.ui.workspace.sidebar.collapsable-button :refer [collapsed-button]]
[app.main.ui.workspace.sidebar.history :refer [history-toolbox]]
[app.main.ui.workspace.tokens.modals]
[app.main.ui.workspace.viewport :refer [viewport]]
[app.util.debug :as dbg]
[app.util.dom :as dom]
@ -179,6 +180,7 @@
file-ready? (mf/deref file-ready*)
components-v2? (features/use-feature "components/v2")
design-tokens? (features/use-feature "design-tokens/v1")
background-color (:background-color wglobal)]
@ -207,15 +209,16 @@
[:& (mf/provider ctx/current-team-id) {:value team-id}
[:& (mf/provider ctx/current-page-id) {:value page-id}
[:& (mf/provider ctx/components-v2) {:value components-v2?}
[:& (mf/provider ctx/workspace-read-only?) {:value read-only?}
[:& (mf/provider ctx/team-permissions) {:value permissions}
[:section {:class (stl/css :workspace)
:style {:background-color background-color
:touch-action "none"}}
[:& context-menu]
(if ^boolean file-ready?
[:& workspace-page {:page-id page-id
:file file
:wglobal wglobal
:layout layout}]
[:& workspace-loader])]]]]]]]]))
[:& (mf/provider ctx/design-tokens) {:value design-tokens?}
[:& (mf/provider ctx/workspace-read-only?) {:value read-only?}
[:& (mf/provider ctx/team-permissions) {:value permissions}
[:section {:class (stl/css :workspace)
:style {:background-color background-color
:touch-action "none"}}
[:& context-menu]
(if ^boolean file-ready?
[:& workspace-page {:page-id page-id
:file file
:wglobal wglobal
:layout layout}]
[:& workspace-loader])]]]]]]]]]))

View file

@ -53,6 +53,25 @@
;; --- Color Picker Modal
(defn use-color-picker-css-variables! [node-ref current-color]
(mf/with-effect [current-color]
(let [node (mf/ref-val node-ref)
{:keys [r g b h v]} current-color
rgb [r g b]
hue-rgb (cc/hsv->rgb [h 1.0 255])
hsl-from (cc/hsv->hsl [h 0.0 v])
hsl-to (cc/hsv->hsl [h 1.0 v])
format-hsl (fn [[h s l]]
(str/fmt "hsl(%s, %s, %s)"
h
(str (* s 100) "%")
(str (* l 100) "%")))]
(dom/set-css-property! node "--color" (str/join ", " rgb))
(dom/set-css-property! node "--hue-rgb" (str/join ", " hue-rgb))
(dom/set-css-property! node "--saturation-grad-from" (format-hsl hsl-from))
(dom/set-css-property! node "--saturation-grad-to" (format-hsl hsl-to)))))
(mf/defc colorpicker
{::mf/props :obj}
[{:keys [data disable-gradient disable-opacity disable-image on-change on-accept]}]
@ -264,23 +283,7 @@
(st/emit! (dc/update-colorpicker data)))
;; Updates the CSS color variable when there is a change in the color
(mf/with-effect [current-color]
(let [node (mf/ref-val node-ref)
{:keys [r g b h v]} current-color
rgb [r g b]
hue-rgb (cc/hsv->rgb [h 1.0 255])
hsl-from (cc/hsv->hsl [h 0.0 v])
hsl-to (cc/hsv->hsl [h 1.0 v])
format-hsl (fn [[h s l]]
(str/fmt "hsl(%s, %s, %s)"
h
(str (* s 100) "%")
(str (* l 100) "%")))]
(dom/set-css-property! node "--color" (str/join ", " rgb))
(dom/set-css-property! node "--hue-rgb" (str/join ", " hue-rgb))
(dom/set-css-property! node "--saturation-grad-from" (format-hsl hsl-from))
(dom/set-css-property! node "--saturation-grad-to" (format-hsl hsl-to))))
(use-color-picker-css-variables! node-ref current-color)
;; Updates color when pixel picker is used
(mf/with-effect [picking-color? picked-color picked-color-select]

View file

@ -27,6 +27,7 @@
[app.main.ui.workspace.sidebar.shortcuts :refer [shortcuts-container]]
[app.main.ui.workspace.sidebar.sitemap :refer [sitemap]]
[app.main.ui.workspace.sidebar.versions :refer [versions-toolbox]]
[app.main.ui.workspace.tokens.sidebar :refer [tokens-sidebar-tab]]
[app.util.debug :as dbg]
[app.util.i18n :refer [tr]]
[rumext.v2 :as mf]))
@ -52,8 +53,11 @@
mode-inspect? (= options-mode :inspect)
project (mf/deref refs/workspace-project)
design-tokens? (mf/use-ctx muc/design-tokens)
section (cond (or mode-inspect? (contains? layout :layers)) :layers
(contains? layout :assets) :assets)
(contains? layout :assets) :assets
(contains? layout :tokens) :tokens)
shortcuts? (contains? layout :shortcuts)
show-debug? (contains? layout :debug-panel)
@ -97,17 +101,31 @@
assets-tab
(mf/html [:& assets-toolbox {:size (- size 58)}])
tokens-tab
(when design-tokens?
(mf/html [:& tokens-sidebar-tab]))
tabs
(if ^boolean mode-inspect?
#js [#js {:label (tr "workspace.sidebar.layers")
:id "layers"
:content layers-tab}]
#js [#js {:label (tr "workspace.sidebar.layers")
:id "layers"
:content layers-tab}
#js {:label (tr "workspace.toolbar.assets")
:id "assets"
:content assets-tab}])]
(if ^boolean design-tokens?
#js [#js {:label (tr "workspace.sidebar.layers")
:id "layers"
:content layers-tab}
#js {:label (tr "workspace.toolbar.assets")
:id "assets"
:content assets-tab}
#js {:label "Tokens"
:id "tokens"
:content tokens-tab}]
#js [#js {:label (tr "workspace.sidebar.layers")
:id "layers"
:content layers-tab}
#js {:label (tr "workspace.toolbar.assets")
:id "assets"
:content assets-tab}]))]
[:& (mf/provider muc/sidebar) {:value :left}
[:aside {:ref parent-ref

View file

@ -130,7 +130,7 @@
(mf/defc asset-section
{::mf/wrap-props false}
[{:keys [children file-id title section assets-count open?]}]
[{:keys [children file-id title section assets-count icon open?]}]
(let [children (-> (array/normalize-to-array children)
(array/without-nils))
@ -151,7 +151,7 @@
(mf/html
[:span {:class (stl/css :title-name)}
[:span {:class (stl/css :section-icon)}
[:& section-icon {:section section}]]
[:& (or icon section-icon) {:section section}]]
[:span {:class (stl/css :section-name)}
title]

View file

@ -69,11 +69,11 @@
(defn group-assets
"Convert a list of assets in a nested structure like this:
{'': [{assetA} {assetB}]
'group1': {'': [{asset1A} {asset1B}]
'subgroup11': {'': [{asset11A} {asset11B} {asset11C}]}
'subgroup12': {'': [{asset12A}]}}
'group2': {'subgroup21': {'': [{asset21A}}}}
{'': [assetA assetB]
'group1': {'': [asset1A asset1B]
'subgroup11': {'': [asset11A asset11B asset11C]}
'subgroup12': {'': [asset12A]}}
'group2': {'subgroup21': {'': [asset21A]}}}
"
[assets reverse-sort?]
(when-not (empty? assets)

View file

@ -12,7 +12,9 @@
[app.common.logic.shapes :as cls]
[app.common.types.shape.layout :as ctl]
[app.common.types.shape.radius :as ctsr]
[app.common.types.tokens-lib :as ctob]
[app.main.constants :refer [size-presets]]
[app.main.data.tokens :as dt]
[app.main.data.workspace :as udw]
[app.main.data.workspace.interactions :as dwi]
[app.main.data.workspace.shapes :as dwsh]
@ -22,8 +24,13 @@
[app.main.ui.components.dropdown :refer [dropdown]]
[app.main.ui.components.numeric-input :refer [numeric-input*]]
[app.main.ui.components.radio-buttons :refer [radio-button radio-buttons]]
[app.main.ui.context :as muc]
[app.main.ui.hooks :as hooks]
[app.main.ui.icons :as i]
[app.main.ui.workspace.tokens.core :as wtc]
[app.main.ui.workspace.tokens.editable-select :refer [editable-select]]
[app.main.ui.workspace.tokens.style-dictionary :as sd]
[app.main.ui.workspace.tokens.token-types :as wtty]
[app.util.dom :as dom]
[app.util.i18n :as i18n :refer [tr]]
[clojure.set :refer [rename-keys union]]
@ -84,6 +91,8 @@
(reduce #(union %1 %2) (map #(get type->options %) all-types))
(get type->options type))
design-tokens? (mf/use-ctx muc/design-tokens)
ids-with-children (or ids-with-children ids)
old-shapes (if (= type :multiple)
@ -96,6 +105,34 @@
selection-parents-ref (mf/use-memo (mf/deps ids) #(refs/parents-by-ids ids))
selection-parents (mf/deref selection-parents-ref)
tokens (sd/use-active-theme-sets-tokens)
tokens-by-type (mf/use-memo
(mf/deps tokens)
#(ctob/group-by-type tokens))
border-radius-tokens (:border-radius tokens-by-type)
border-radius-options (mf/use-memo
(mf/deps shape border-radius-tokens)
#(wtc/tokens->select-options
{:shape shape
:tokens border-radius-tokens
:attributes (wtty/token-attributes :border-radius)}))
sizing-tokens (:sizing tokens-by-type)
width-options (mf/use-memo
(mf/deps shape sizing-tokens)
#(wtc/tokens->select-options
{:shape shape
:tokens sizing-tokens
:attributes (wtty/token-attributes :sizing)
:selected-attributes #{:width}}))
height-options (mf/use-memo
(mf/deps shape sizing-tokens)
#(wtc/tokens->select-options
{:shape shape
:tokens sizing-tokens
:attributes (wtty/token-attributes :sizing)
:selected-attributes #{:height}}))
flex-child? (->> selection-parents (some ctl/flex-layout?))
absolute? (ctl/item-absolute? shape)
flex-container? (ctl/flex-layout? shape)
@ -209,8 +246,21 @@
(mf/use-fn
(mf/deps ids)
(fn [value attr]
(st/emit! (udw/trigger-bounding-box-cloaking ids)
(udw/update-dimensions ids attr value))))
(let [token-value (wtc/maybe-resolve-token-value value)
undo-id (js/Symbol)]
(if-not design-tokens?
(st/emit! (udw/trigger-bounding-box-cloaking ids)
(udw/update-dimensions ids attr (or token-value value)))
(st/emit! (udw/trigger-bounding-box-cloaking ids)
(dwu/start-undo-transaction undo-id)
(dwsh/update-shapes ids
(if token-value
#(assoc-in % [:applied-tokens attr] (:id value))
#(d/dissoc-in % [:applied-tokens attr]))
{:reg-objects? true
:attrs [:applied-tokens]})
(udw/update-dimensions ids attr (or token-value value))
(dwu/commit-undo-transaction undo-id))))))
on-proportion-lock-change
(mf/use-fn
@ -256,7 +306,7 @@
(update-fn shape)
shape))
{:reg-objects? true
:attrs [:rx :ry :r1 :r2 :r3 :r4]})))
:attrs [:rx :ry :r1 :r2 :r3 :r4 :applied-tokens]})))
on-switch-to-radius-1
(mf/use-fn
@ -281,11 +331,27 @@
(on-switch-to-radius-4)
(on-switch-to-radius-1))))
on-border-radius-token-unapply
(mf/use-fn
(mf/deps ids change-radius)
(fn [token]
(let [token-value (wtc/maybe-resolve-token-value token)]
(st/emit!
(change-radius (fn [shape]
(-> (dt/unapply-token-id shape (wtty/token-attributes :border-radius))
(ctsr/set-radius-1 token-value))))))))
on-radius-1-change
(mf/use-fn
(mf/deps ids change-radius)
(fn [value]
(st/emit! (change-radius #(ctsr/set-radius-1 % value)))))
(let [token-value (wtc/maybe-resolve-token-value value)]
(st/emit!
(change-radius (fn [shape]
(-> (dt/maybe-apply-token-to-shape {:token (when token-value value)
:shape shape
:attributes (wtty/token-attributes :border-radius)})
(ctsr/set-radius-1 (or token-value value)))))))))
on-radius-multi-change
(mf/use-fn
@ -394,24 +460,50 @@
:disabled disabled-width-sizing?)
:title (tr "workspace.options.width")}
[:span {:class (stl/css :icon-text)} "W"]
[:> numeric-input* {:min 0.01
:no-validate true
:placeholder (if (= :multiple (:width values)) (tr "settings.multiple") "--")
:on-change on-width-change
:disabled disabled-width-sizing?
:class (stl/css :numeric-input)
:value (:width values)}]]
(if-not design-tokens?
[:> numeric-input* {:min 0.01
:no-validate true
:placeholder (if (= :multiple (:width values)) (tr "settings.multiple") "--")
:on-change on-width-change
:disabled disabled-width-sizing?
:class (stl/css :numeric-input)
:value (:width values)}]
[:& editable-select
{:placeholder (if (= :multiple (:rx values)) (tr "settings.multiple") "--")
:class (stl/css :token-select)
:disabled disabled-width-sizing?
:on-change on-width-change
:on-token-remove #(on-width-change (wtc/maybe-resolve-token-value %))
:options width-options
:position :left
:value (:width values)
:input-props {:type "number"
:no-validate true
:min 0.01}}])]
[:div {:class (stl/css-case :height true
:disabled disabled-height-sizing?)
:title (tr "workspace.options.height")}
[:span {:class (stl/css :icon-text)} "H"]
[:> numeric-input* {:min 0.01
:no-validate true
:placeholder (if (= :multiple (:height values)) (tr "settings.multiple") "--")
:on-change on-height-change
:disabled disabled-height-sizing?
:class (stl/css :numeric-input)
:value (:height values)}]]
(if-not design-tokens?
[:> numeric-input* {:min 0.01
:no-validate true
:placeholder (if (= :multiple (:height values)) (tr "settings.multiple") "--")
:on-change on-height-change
:disabled disabled-height-sizing?
:class (stl/css :numeric-input)
:value (:height values)}]
[:& editable-select
{:placeholder (if (= :multiple (:rx values)) (tr "settings.multiple") "--")
:class (stl/css :token-select)
:disabled disabled-height-sizing?
:on-change on-height-change
:on-token-remove #(on-height-change (wtc/maybe-resolve-token-value %))
:options height-options
:position :right
:value (:height values)
:input-props {:type "number"
:no-validate true
:min 0.01}}])]
[:button {:class (stl/css-case
:lock-size-btn true
:selected (true? proportion-lock)
@ -468,13 +560,24 @@
[:div {:class (stl/css :radius-1)
:title (tr "workspace.options.radius")}
[:span {:class (stl/css :icon)} i/corner-radius]
[:> numeric-input*
{:placeholder (if (= :multiple (:rx values)) (tr "settings.multiple") "--")
:ref radius-input-ref
:min 0
:on-change on-radius-1-change
:class (stl/css :numeric-input)
:value (:rx values)}]]
(if-not design-tokens?
[:> numeric-input*
{:placeholder (if (= :multiple (:rx values)) (tr "settings.multiple") "--")
:ref radius-input-ref
:min 0
:on-change on-radius-1-change
:class (stl/css :numeric-input)
:value (:rx values)}]
[:& editable-select
{:placeholder (if (= :multiple (:rx values)) (tr "settings.multiple") "--")
:class (stl/css :token-select)
:on-change on-radius-1-change
:on-token-remove on-border-radius-token-unapply
:options border-radius-options
:position :right
:value (:rx values)
:input-props {:type "number"
:min 0}}])]
@radius-multi?
[:div {:class (stl/css :radius-1)

View file

@ -109,6 +109,7 @@
.size {
@include flexRow;
position: relative;
}
.height,
@ -186,6 +187,7 @@
@extend .input-element;
@include bodySmallTypography;
width: $s-108;
position: relative;
}
.radius-4 {

View file

@ -0,0 +1,183 @@
;; 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.main.ui.workspace.tokens.changes
(:require
[app.common.types.shape.radius :as ctsr]
[app.common.types.token :as ctt]
[app.common.types.tokens-lib :as ctob]
[app.main.data.workspace :as udw]
[app.main.data.workspace.colors :as wdc]
[app.main.data.workspace.shape-layout :as dwsl]
[app.main.data.workspace.shapes :as dwsh]
[app.main.data.workspace.state-helpers :as wsh]
[app.main.data.workspace.transforms :as dwt]
[app.main.data.workspace.undo :as dwu]
[app.main.ui.workspace.tokens.style-dictionary :as sd]
[app.main.ui.workspace.tokens.tinycolor :as tinycolor]
[app.main.ui.workspace.tokens.token :as wtt]
[beicon.v2.core :as rx]
[clojure.set :as set]
[potok.v2.core :as ptk]))
;; Token Updates ---------------------------------------------------------------
(defn apply-token
"Apply `attributes` that match `token` for `shape-ids`.
Optionally remove attributes from `attributes-to-remove`,
this is useful for applying a single attribute from an attributes set
while removing other applied tokens from this set."
[{:keys [attributes attributes-to-remove token shape-ids on-update-shape] :as _props}]
(ptk/reify ::apply-token
ptk/WatchEvent
(watch [_ state _]
(when-let [tokens (some-> (get-in state [:workspace-data :tokens-lib])
(ctob/get-active-themes-set-tokens))]
(->> (rx/from (sd/resolve-tokens+ tokens))
(rx/mapcat
(fn [resolved-tokens]
(let [undo-id (js/Symbol)
resolved-value (get-in resolved-tokens [(wtt/token-identifier token) :resolved-value])
tokenized-attributes (wtt/attributes-map attributes token)]
(rx/of
(dwu/start-undo-transaction undo-id)
(dwsh/update-shapes shape-ids (fn [shape]
(cond-> shape
attributes-to-remove (update :applied-tokens #(apply (partial dissoc %) attributes-to-remove))
:always (update :applied-tokens merge tokenized-attributes))))
(when on-update-shape
(on-update-shape resolved-value shape-ids attributes))
(dwu/commit-undo-transaction undo-id))))))))))
(defn unapply-token
"Removes `attributes` that match `token` for `shape-ids`.
Doesn't update shape attributes."
[{:keys [attributes token shape-ids] :as _props}]
(ptk/reify ::unapply-token
ptk/WatchEvent
(watch [_ _ _]
(rx/of
(let [remove-token #(when % (wtt/remove-attributes-for-token attributes token %))]
(dwsh/update-shapes
shape-ids
(fn [shape]
(update shape :applied-tokens remove-token))))))))
(defn toggle-token
[{:keys [token-type-props token shapes] :as _props}]
(ptk/reify ::on-toggle-token
ptk/WatchEvent
(watch [_ _ _]
(let [{:keys [attributes all-attributes on-update-shape]} token-type-props
unapply-tokens? (wtt/shapes-token-applied? token shapes (or all-attributes attributes))
shape-ids (map :id shapes)]
(if unapply-tokens?
(rx/of
(unapply-token {:attributes (or all-attributes attributes)
:token token
:shape-ids shape-ids}))
(rx/of
(apply-token {:attributes attributes
:token token
:shape-ids shape-ids
:on-update-shape on-update-shape})))))))
;; Shape Updates ---------------------------------------------------------------
(defn update-shape-radius-all [value shape-ids]
(dwsh/update-shapes shape-ids
(fn [shape]
(when (ctsr/has-radius? shape)
(ctsr/set-radius-1 shape value)))
{:reg-objects? true
:attrs ctt/border-radius-keys}))
(defn update-opacity [value shape-ids]
(when (<= 0 value 1)
(dwsh/update-shapes shape-ids #(assoc % :opacity value))))
(defn update-rotation [value shape-ids]
(ptk/reify ::update-shape-rotation
ptk/WatchEvent
(watch [_ _ _]
(rx/of
(udw/trigger-bounding-box-cloaking shape-ids)
(udw/increase-rotation shape-ids value)))))
(defn update-shape-radius-single-corner [value shape-ids attributes]
(dwsh/update-shapes shape-ids
(fn [shape]
(when (ctsr/has-radius? shape)
(cond-> shape
(:rx shape) (ctsr/switch-to-radius-4)
:always (ctsr/set-radius-4 (first attributes) value))))
{:reg-objects? true
:attrs [:rx :ry :r1 :r2 :r3 :r4]}))
(defn update-stroke-width
[value shape-ids]
(dwsh/update-shapes shape-ids
(fn [shape]
(when (seq (:strokes shape))
(assoc-in shape [:strokes 0 :stroke-width] value)))
{:reg-objects? true
:attrs [:strokes]}))
(defn update-color
[value shape-ids]
(let [color (some->> value
(tinycolor/valid-color)
(tinycolor/->hex)
(str "#"))]
(wdc/change-fill shape-ids {:color color} 0)))
(defn update-shape-dimensions [value shape-ids attributes]
(ptk/reify ::update-shape-dimensions
ptk/WatchEvent
(watch [_ _ _]
(rx/of
(when (:width attributes) (dwt/update-dimensions shape-ids :width value))
(when (:height attributes) (dwt/update-dimensions shape-ids :height value))))))
(defn- attributes->layout-gap [attributes value]
(let [layout-gap (-> (set/intersection attributes #{:column-gap :row-gap})
(zipmap (repeat value)))]
{:layout-gap layout-gap}))
(defn update-layout-padding [value shape-ids attrs]
(dwsl/update-layout shape-ids {:layout-padding (zipmap attrs (repeat value))}))
(defn update-layout-spacing [value shape-ids attributes]
(ptk/reify ::update-layout-spacing
ptk/WatchEvent
(watch [_ state _]
(let [layout-shape-ids (->> (wsh/lookup-shapes state shape-ids)
(eduction
(filter :layout)
(map :id)))
layout-attributes (attributes->layout-gap attributes value)]
(rx/of
(dwsl/update-layout layout-shape-ids layout-attributes))))))
(defn update-shape-position [value shape-ids attributes]
(ptk/reify ::update-shape-position
ptk/WatchEvent
(watch [_ _ _]
(rx/concat
(map #(dwt/update-position % (zipmap attributes (repeat value))) shape-ids)))))
(defn update-layout-sizing-limits [value shape-ids attributes]
(ptk/reify ::update-layout-sizing-limits
ptk/WatchEvent
(watch [_ _ _]
(let [props (-> {:layout-item-min-w value
:layout-item-min-h value
:layout-item-max-w value
:layout-item-max-h value}
(select-keys attributes))]
(dwsl/update-layout-child shape-ids props)))))

View file

@ -0,0 +1,131 @@
;; 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.main.ui.workspace.tokens.common
(:require-macros [app.main.style :as stl])
(:require
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.main.data.shortcuts :as dsc]
[app.main.store :as st]
[app.main.ui.components.dropdown :refer [dropdown]]
[app.main.ui.icons :as i]
[app.util.dom :as dom]
[app.util.globals :as globals]
[app.util.keyboard :as kbd]
[cuerdas.core :as str]
[goog.events :as events]
[rumext.v2 :as mf])
(:import goog.events.EventType))
;; Helpers ---------------------------------------------------------------------
(defn camel-keys [m]
(->> m
(d/deep-mapm
(fn [[k v]]
(if (or (keyword? k) (string? k))
[(keyword (str/camel (name k))) v]
[k v])))))
(defn direction-select
"Returns next `n` in `direction` while wrapping around at the last item at the count of `coll`.
`direction` accepts `:up` or `:down`."
[direction n coll]
(let [last-n (dec (count coll))
next-n (case direction
:up (dec n)
:down (inc n))
wrap-around-n (cond
(neg? next-n) last-n
(> next-n last-n) 0
:else next-n)]
wrap-around-n))
(defn use-arrow-highlight [{:keys [shortcuts-key options on-select]}]
(let [highlighted* (mf/use-state nil)
highlighted (deref highlighted*)
on-dehighlight #(reset! highlighted* nil)
on-keyup (fn [event]
(cond
(and (kbd/enter? event) highlighted) (on-select (nth options highlighted))
(kbd/up-arrow? event) (do
(dom/prevent-default event)
(->> (direction-select :up (or highlighted 0) options)
(reset! highlighted*)))
(kbd/down-arrow? event) (do
(dom/prevent-default event)
(->> (direction-select :down (or highlighted -1) options)
(reset! highlighted*)))))]
(mf/with-effect [highlighted]
(let [shortcuts-key shortcuts-key
keys [(events/listen globals/document EventType.KEYUP on-keyup)
(events/listen globals/document EventType.KEYDOWN dom/prevent-default)]]
(st/emit! (dsc/push-shortcuts shortcuts-key {}))
(fn []
(doseq [key keys]
(events/unlistenByKey key))
(st/emit! (dsc/pop-shortcuts shortcuts-key)))))
{:highlighted highlighted
:on-dehighlight on-dehighlight}))
(defn use-dropdown-open-state []
(let [open? (mf/use-state false)
on-open (mf/use-fn #(reset! open? true))
on-close (mf/use-fn #(reset! open? false))
on-toggle (mf/use-fn #(swap! open? not))]
{:dropdown-open? @open?
:on-open-dropdown on-open
:on-close-dropdown on-close
:on-toggle-dropdown on-toggle}))
;; Components ------------------------------------------------------------------
(mf/defc dropdown-select
[{:keys [id _shortcuts-key options on-close element-ref on-select] :as props}]
(let [{:keys [highlighted on-dehighlight]} (use-arrow-highlight props)]
[:& dropdown {:show true
:on-close on-close}
[:> :div {:class (stl/css :dropdown)
:on-mouse-enter on-dehighlight
:ref element-ref}
[:ul {:class (stl/css :dropdown-list)}
(for [[index item] (d/enumerate options)]
(cond
(= :separator item)
[:li {:class (stl/css :separator)
:key (dm/str id "-" index)}]
:else
(let [{:keys [label selected? disabled?]} item
highlighted? (= highlighted index)]
[:li
{:key (str id "-" index)
:class (stl/css-case :dropdown-element true
:is-selected selected?
:is-highlighted highlighted?)
:data-label label
:disabled disabled?
:on-click #(on-select item)}
[:span {:class (stl/css :label)} label]
[:span {:class (stl/css :check-icon)} i/tick]])))]]]))
(mf/defc labeled-input
{::mf/wrap-props false}
[{:keys [label input-props auto-complete? error? render-right]}]
(let [input-props (cond-> input-props
:always camel-keys
;; Disable auto-complete on form fields for proprietary password managers
;; https://github.com/orgs/tokens-studio/projects/69/views/11?pane=issue&itemId=63724204
(not auto-complete?) (assoc "data-1p-ignore" true
"data-lpignore" true
:auto-complete "off"))]
[:label {:class (stl/css-case :labeled-input true
:labeled-input-error error?)}
[:span {:class (stl/css :label)} label]
[:& :input input-props]
(when render-right
[:& render-right])]))

View file

@ -0,0 +1,115 @@
// 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
@import "refactor/common-refactor.scss";
.input {
@extend .input-element;
}
.labeled-input {
@extend .input-element;
.label {
width: auto;
text-wrap: nowrap;
}
}
.labeled-input-error {
border: 1px solid var(--status-color-error-500) !important;
}
.button {
@extend .button-primary;
}
.action-button {
@extend .button-tertiary;
height: $s-32;
width: $s-28;
svg {
@extend .button-icon;
}
}
.dropdown {
@extend .dropdown-wrapper;
max-height: $s-320;
width: 100%;
margin-top: $s-4;
ul {
margin: 0;
}
.separator {
margin: 0;
height: $s-12;
}
.dropdown-element {
@extend .dropdown-element-base;
color: var(--menu-foreground-color-rest);
display: flex;
& > span {
display: flex;
justify-content: flex-start;
align-content: center;
}
.label,
.value {
width: fit-content;
}
.label {
text-transform: unset;
flex: 1;
}
.value {
text-align: right;
justify-content: flex-end;
flex: 0.6;
}
.check-icon {
@include flexCenter;
translate: -$s-4 0;
svg {
@extend .button-icon-small;
visibility: hidden;
stroke: var(--icon-foreground);
}
}
&.is-selected {
color: var(--menu-foreground-color);
.check-icon svg {
stroke: var(--menu-foreground-color);
visibility: visible;
}
}
&:hover {
background-color: var(--menu-background-color-hover);
color: var(--menu-foreground-color-hover);
.check-icon svg {
stroke: var(--menu-foreground-color-hover);
}
}
&.is-highlighted {
background-color: var(--button-primary-background-color-rest);
span {
color: var(--button-primary-foreground-color-rest);
}
.check-icon svg {
stroke: var(--button-primary-foreground-color-rest);
}
}
}
}

View file

@ -0,0 +1,336 @@
;; 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.main.ui.workspace.tokens.context-menu
(:require-macros [app.main.style :as stl])
(:require
[app.common.data :as d]
[app.main.data.modal :as modal]
[app.main.data.tokens :as dt]
[app.main.refs :as refs]
[app.main.store :as st]
[app.main.ui.components.dropdown :refer [dropdown]]
[app.main.ui.icons :as i]
[app.main.ui.workspace.tokens.changes :as wtch]
[app.main.ui.workspace.tokens.token :as wtt]
[app.main.ui.workspace.tokens.token-types :as wtty]
[app.util.dom :as dom]
[app.util.timers :as timers]
[okulary.core :as l]
[rumext.v2 :as mf]))
;; Actions ---------------------------------------------------------------------
(defn attribute-actions [token selected-shapes attributes]
(let [ids-by-attributes (wtt/shapes-ids-by-applied-attributes token selected-shapes attributes)
shape-ids (into #{} (map :id selected-shapes))]
{:all-selected? (wtt/shapes-applied-all? ids-by-attributes shape-ids attributes)
:shape-ids shape-ids
:selected-pred #(seq (% ids-by-attributes))}))
(defn generic-attribute-actions [attributes title {:keys [token selected-shapes on-update-shape]}]
(let [on-update-shape-fn (or on-update-shape
(-> (wtty/get-token-properties token)
(:on-update-shape)))
{:keys [selected-pred shape-ids]} (attribute-actions token selected-shapes attributes)]
(map (fn [attribute]
(let [selected? (selected-pred attribute)
props {:attributes #{attribute}
:token token
:shape-ids shape-ids}]
{:title title
:selected? selected?
:action (fn []
(if selected?
(st/emit! (wtch/unapply-token props))
(st/emit! (wtch/apply-token (assoc props :on-update-shape on-update-shape-fn)))))}))
attributes)))
(defn all-or-sepearate-actions [{:keys [attribute-labels on-update-shape-all on-update-shape]}
{:keys [token selected-shapes]}]
(let [attributes (set (keys attribute-labels))
{:keys [all-selected? selected-pred shape-ids]} (attribute-actions token selected-shapes attributes)
all-action (let [props {:attributes attributes
:token token
:shape-ids shape-ids}]
{:title "All"
:selected? all-selected?
:action #(if all-selected?
(st/emit! (wtch/unapply-token props))
(st/emit! (wtch/apply-token (assoc props :on-update-shape (or on-update-shape-all on-update-shape)))))})
single-actions (map (fn [[attr title]]
(let [selected? (selected-pred attr)]
{:title title
:selected? (and (not all-selected?) selected?)
:action #(let [props {:attributes #{attr}
:token token
:shape-ids shape-ids}
event (cond
all-selected? (-> (assoc props :attributes-to-remove attributes)
(wtch/apply-token))
selected? (wtch/unapply-token props)
:else (-> (assoc props :on-update-shape on-update-shape)
(wtch/apply-token)))]
(st/emit! event))}))
attribute-labels)]
(concat [all-action] single-actions)))
(defn spacing-attribute-actions [{:keys [token selected-shapes] :as context-data}]
(let [on-update-shape-padding wtch/update-layout-padding
padding-attrs {:p1 "Top"
:p2 "Right"
:p3 "Bottom"
:p4 "Left"}
all-padding-attrs (into #{} (keys padding-attrs))
{:keys [all-selected? selected-pred shape-ids]} (attribute-actions token selected-shapes all-padding-attrs)
horizontal-attributes #{:p1 :p3}
horizontal-padding-selected? (and
(not all-selected?)
(every? selected-pred horizontal-attributes))
vertical-attributes #{:p2 :p4}
vertical-padding-selected? (and
(not all-selected?)
(every? selected-pred vertical-attributes))
padding-items [{:title "All"
:selected? all-selected?
:action (fn []
(let [props {:attributes all-padding-attrs
:token token
:shape-ids shape-ids}]
(if all-selected?
(st/emit! (wtch/unapply-token props))
(st/emit! (wtch/apply-token (assoc props :on-update-shape on-update-shape-padding))))))}
{:title "Horizontal"
:selected? horizontal-padding-selected?
:action (fn []
(let [props {:token token
:shape-ids shape-ids}
event (cond
all-selected? (wtch/apply-token (assoc props :attributes-to-remove vertical-attributes))
horizontal-padding-selected? (wtch/apply-token (assoc props :attributes-to-remove horizontal-attributes))
:else (wtch/apply-token (assoc props
:attributes horizontal-attributes
:on-update-shape on-update-shape-padding)))]
(st/emit! event)))}
{:title "Vertical"
:selected? vertical-padding-selected?
:action (fn []
(let [props {:token token
:shape-ids shape-ids}
event (cond
all-selected? (wtch/apply-token (assoc props :attributes-to-remove vertical-attributes))
vertical-padding-selected? (wtch/apply-token (assoc props :attributes-to-remove vertical-attributes))
:else (wtch/apply-token (assoc props
:attributes vertical-attributes
:on-update-shape on-update-shape-padding)))]
(st/emit! event)))}]
single-padding-items (->> padding-attrs
(map (fn [[attr title]]
(let [same-axis-selected? (cond
(get horizontal-attributes attr) horizontal-padding-selected?
(get vertical-attributes attr) vertical-padding-selected?
:else true)
selected? (and
(not all-selected?)
(not same-axis-selected?)
(selected-pred attr))]
{:title title
:selected? selected?
:action #(let [props {:attributes #{attr}
:token token
:shape-ids shape-ids}
event (cond
all-selected? (-> (assoc props :attributes-to-remove all-padding-attrs)
(wtch/apply-token))
selected? (wtch/unapply-token props)
:else (-> (assoc props :on-update-shape on-update-shape-padding)
(wtch/apply-token)))]
(st/emit! event))}))))
gap-items (all-or-sepearate-actions {:attribute-labels {:column-gap "Column Gap"
:row-gap "Row Gap"}
:on-update-shape wtch/update-layout-spacing}
context-data)]
(concat padding-items
single-padding-items
[:separator]
gap-items)))
(defn sizing-attribute-actions [context-data]
(concat
(all-or-sepearate-actions {:attribute-labels {:width "Width"
:height "Height"}
:on-update-shape wtch/update-shape-dimensions}
context-data)
[:separator]
(all-or-sepearate-actions {:attribute-labels {:layout-item-min-w "Min Width"
:layout-item-min-h "Min Height"}
:on-update-shape wtch/update-layout-sizing-limits}
context-data)
[:separator]
(all-or-sepearate-actions {:attribute-labels {:layout-item-max-w "Max Width"
:layout-item-max-h "Max Height"}
:on-update-shape wtch/update-layout-sizing-limits}
context-data)))
(def shape-attribute-actions-map
(let [stroke-width (partial generic-attribute-actions #{:stroke-width} "Stroke Width")]
{:border-radius (partial all-or-sepearate-actions {:attribute-labels {:r1 "Top Left"
:r2 "Top Right"
:r4 "Bottom Left"
:r3 "Bottom Right"}
:on-update-shape-all wtch/update-shape-radius-all
:on-update-shape wtch/update-shape-radius-single-corner})
:spacing spacing-attribute-actions
:sizing sizing-attribute-actions
:rotation (partial generic-attribute-actions #{:rotation} "Rotation")
:opacity (partial generic-attribute-actions #{:opacity} "Opacity")
:stroke-width stroke-width
:dimensions (fn [context-data]
(concat
[{:title "Spacing" :submenu :spacing}
{:title "Sizing" :submenu :sizing}
:separator
{:title "Border Radius" :submenu :border-radius}]
(stroke-width context-data)
[:separator]
(generic-attribute-actions #{:x} "X" (assoc context-data :on-update-shape wtch/update-shape-position))
(generic-attribute-actions #{:y} "Y" (assoc context-data :on-update-shape wtch/update-shape-position))))}))
(defn default-actions [{:keys [token selected-token-set-id]}]
(let [{:keys [modal]} (wtty/get-token-properties token)]
[{:title "Delete Token"
:action #(st/emit! (dt/delete-token selected-token-set-id (:name token)))}
{:title "Duplicate Token"
:action #(st/emit! (dt/duplicate-token (:name token)))}
{:title "Edit Token"
:action (fn [event]
(let [{:keys [key fields]} modal]
(st/emit! dt/hide-token-context-menu)
(dom/stop-propagation event)
(modal/show! key {:x (.-clientX ^js event)
:y (.-clientY ^js event)
:position :right
:fields fields
:action "edit"
:selected-token-set-id selected-token-set-id
:token token})))}]))
(defn selection-actions [{:keys [type token] :as context-data}]
(let [with-actions (get shape-attribute-actions-map (or type (:type token)))
attribute-actions (if with-actions (with-actions context-data) [])]
(concat
attribute-actions
(when (seq attribute-actions) [:separator])
(default-actions context-data))))
;; Components ------------------------------------------------------------------
(def tokens-menu-ref
(l/derived :token-context-menu refs/workspace-local))
(defn- prevent-default
[event]
(dom/prevent-default event)
(dom/stop-propagation event))
(mf/defc menu-entry
{::mf/props :obj}
[{:keys [title value on-click selected? children submenu-offset]}]
(let [submenu-ref (mf/use-ref nil)
hovering? (mf/use-ref false)
on-pointer-enter
(mf/use-callback
(fn []
(mf/set-ref-val! hovering? true)
(when-let [submenu-node (mf/ref-val submenu-ref)]
(dom/set-css-property! submenu-node "display" "block"))))
on-pointer-leave
(mf/use-callback
(fn []
(mf/set-ref-val! hovering? false)
(when-let [submenu-node (mf/ref-val submenu-ref)]
(timers/schedule 50 #(when-not (mf/ref-val hovering?)
(dom/set-css-property! submenu-node "display" "none"))))))
set-dom-node
(mf/use-callback
(fn [dom]
(let [submenu-node (mf/ref-val submenu-ref)]
(when (and (some? dom) (some? submenu-node))
(dom/set-css-property! submenu-node "top" (str (.-offsetTop dom) "px"))))))]
[:li
{:class (stl/css :context-menu-item)
:ref set-dom-node
:data-value value
:on-click on-click
:on-pointer-enter on-pointer-enter
:on-pointer-leave on-pointer-leave}
(when selected?
[:span {:class (stl/css :icon-wrapper)}
[:span {:class (stl/css :selected-icon)} i/tick]])
[:span {:class (stl/css :title)} title]
(when children
[:*
[:span {:class (stl/css :submenu-icon)} i/arrow]
[:ul {:class (stl/css :token-context-submenu)
:ref submenu-ref
:style {:display "none"
:top 0
:left (str submenu-offset "px")}
:on-context-menu prevent-default}
children]])]))
(mf/defc menu-tree
[{:keys [selected-shapes] :as context-data}]
(let [entries (if (seq selected-shapes)
(selection-actions context-data)
(default-actions context-data))]
(for [[index {:keys [title action selected? submenu] :as entry}] (d/enumerate entries)]
[:* {:key (str title " " index)}
(cond
(= :separator entry) [:li {:class (stl/css :separator)}]
submenu [:& menu-entry {:title title
:submenu-offset (:submenu-offset context-data)}
[:& menu-tree (assoc context-data :type submenu)]]
:else [:& menu-entry
{:title title
:on-click action
:selected? selected?}])])))
(mf/defc token-context-menu-tree
[{:keys [width] :as mdata}]
(let [objects (mf/deref refs/workspace-page-objects)
selected (mf/deref refs/selected-shapes)
selected-shapes (into [] (keep (d/getf objects)) selected)
token-name (:token-name mdata)
token (mf/deref (refs/workspace-selected-token-set-token token-name))
selected-token-set-id (mf/deref refs/workspace-selected-token-set-id)]
[:ul {:class (stl/css :context-list)}
[:& menu-tree {:submenu-offset width
:token token
:selected-token-set-id selected-token-set-id
:selected-shapes selected-shapes}]]))
(mf/defc token-context-menu
[]
(let [mdata (mf/deref tokens-menu-ref)
top (+ (get-in mdata [:position :y]) 5)
left (+ (get-in mdata [:position :x]) 5)
width (mf/use-state 0)
dropdown-ref (mf/use-ref)]
(mf/use-effect
(mf/deps mdata)
(fn []
(when-let [node (mf/ref-val dropdown-ref)]
(reset! width (.-offsetWidth node)))))
[:& dropdown {:show (boolean mdata)
:on-close #(st/emit! dt/hide-token-context-menu)}
[:div {:class (stl/css :token-context-menu)
:ref dropdown-ref
:style {:top top :left left}
:on-context-menu prevent-default}
(when mdata
[:& token-context-menu-tree (assoc mdata :offset @width)])]]))

View file

@ -0,0 +1,103 @@
// 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
@import "refactor/common-refactor.scss";
.token-context-menu {
position: absolute;
z-index: $z-index-4;
}
.context-list,
.token-context-submenu {
@include menuShadow;
display: grid;
width: $s-240;
padding: $s-4;
border-radius: $br-8;
border: $s-2 solid var(--panel-border-color);
background-color: var(--menu-background-color);
max-height: 100vh;
overflow-y: auto;
li {
@include bodySmallTypography;
color: var(--menu-foreground-color);
}
}
.token-context-submenu {
position: absolute;
padding: $s-4;
margin-left: $s-6;
}
.separator {
@include bodySmallTypography;
margin: $s-6;
border-block-start: $s-1 solid var(--panel-border-color);
}
.context-menu-item {
display: flex;
align-items: center;
height: $s-28;
width: 100%;
padding: $s-6;
border-radius: $br-8;
cursor: pointer;
.title {
flex-grow: 1;
@include bodySmallTypography;
color: var(--menu-foreground-color);
margin-left: calc(($s-32 + $s-28) / 2);
}
.icon-wrapper {
display: grid;
grid-template-columns: 1fr 1fr;
}
.icon-wrapper + .title {
margin-left: $s-6;
}
.selected-icon {
svg {
@extend .button-icon-small;
stroke: var(--menu-foreground-color);
}
}
.submenu-icon {
margin-left: $s-2;
svg {
@extend .button-icon-small;
stroke: var(--menu-foreground-color);
}
}
&:hover {
background-color: var(--menu-background-color-hover);
.title {
color: var(--menu-foreground-color-hover);
}
.shortcut {
color: var(--menu-shortcut-foreground-color-hover);
}
}
&:focus {
border: 1px solid var(--menu-border-color-focus);
background-color: var(--menu-background-color-focus);
}
&[disabled] {
pointer-events: none;
opacity: 0.6;
}
}

View file

@ -0,0 +1,34 @@
;; 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.main.ui.workspace.tokens.core
(:require
[app.common.data :as d]
[app.main.ui.workspace.tokens.token :as wtt]))
;; Helpers ---------------------------------------------------------------------
(defn resolve-token-value [{:keys [value resolved-value] :as _token}]
(or
resolved-value
(d/parse-double value)))
(defn maybe-resolve-token-value [{:keys [value] :as token}]
(when value (resolve-token-value token)))
(defn tokens->select-options [{:keys [shape tokens attributes selected-attributes]}]
(map
(fn [{:keys [name] :as token}]
(cond-> (assoc token :label name)
(wtt/token-applied? token shape (or selected-attributes attributes)) (assoc :selected? true)))
tokens))
(defn tokens-name-map->select-options [{:keys [shape tokens attributes selected-attributes]}]
(map
(fn [[_k {:keys [name] :as token}]]
(cond-> (assoc token :label name)
(wtt/token-applied? token shape (or selected-attributes attributes)) (assoc :selected? true)))
tokens))

View file

@ -0,0 +1,301 @@
;; 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.main.ui.workspace.tokens.editable-select
(:require-macros [app.main.style :as stl])
(:require
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.math :as mth]
[app.common.uuid :as uuid]
[app.main.data.shortcuts :as dsc]
[app.main.store :as st]
[app.main.ui.components.dropdown :refer [dropdown]]
[app.main.ui.components.numeric-input :refer [numeric-input*]]
[app.main.ui.icons :as i]
[app.main.ui.workspace.tokens.core :as wtc]
[app.util.dom :as dom]
[app.util.globals :as globals]
[app.util.keyboard :as kbd]
[app.util.timers :as timers]
[cuerdas.core :as str]
[goog.events :as events]
[rumext.v2 :as mf])
(:import goog.events.EventType))
(defn on-number-input-key-down [{:keys [event min-val max-val set-value!]}]
(let [up? (kbd/up-arrow? event)
down? (kbd/down-arrow? event)]
(when (or up? down?)
(dom/prevent-default event)
(let [value (-> event dom/get-target dom/get-value)
value (or (d/parse-double value) value)
increment (cond
(kbd/shift? event) (if up? 10 -10)
(kbd/alt? event) (if up? 0.1 -0.1)
:else (if up? 1 -1))
new-value (+ value increment)
new-value (cond
(and (d/num? min-val) (< new-value min-val)) min-val
(and (d/num? max-val) (> new-value max-val)) max-val
:else new-value)]
(set-value! new-value)))))
(defn direction-select
"Returns next `n` in `direction` while wrapping around at the last item at the count of `coll`.
`direction` accepts `:up` or `:down`."
[direction n coll]
(let [last-n (dec (count coll))
next-n (case direction
:up (dec n)
:down (inc n))
wrap-around-n (cond
(neg? next-n) last-n
(> next-n last-n) 0
:else next-n)]
wrap-around-n))
(mf/defc dropdown-select [{:keys [position on-close element-id element-ref options on-select]}]
(let [highlighted* (mf/use-state nil)
highlighted (deref highlighted*)
on-keyup (fn [event]
(cond
(and (kbd/enter? event) highlighted) (on-select (nth options highlighted))
(kbd/up-arrow? event) (do
(dom/prevent-default event)
(->> (direction-select :up (or highlighted 0) options)
(reset! highlighted*)))
(kbd/down-arrow? event) (do
(dom/prevent-default event)
(->> (direction-select :down (or highlighted -1) options)
(reset! highlighted*)))))]
(mf/with-effect [highlighted]
(let [keys [(events/listen globals/document EventType.KEYUP on-keyup)
(events/listen globals/document EventType.KEYDOWN dom/prevent-default)]]
(st/emit! (dsc/push-shortcuts :token {}))
(fn []
(doseq [key keys]
(events/unlistenByKey key))
(st/emit! (dsc/pop-shortcuts :token)))))
[:& dropdown {:show true
:on-close on-close}
[:> :div {:class (stl/css-case :custom-select-dropdown true
:custom-select-dropdown-right (= position :right)
:custom-select-dropdown-left (= position :left))
:on-mouse-enter #(reset! highlighted* nil)
:ref element-ref}
[:ul {:class (stl/css :custom-select-dropdown-list)}
(for [[index item] (d/enumerate options)]
(cond
(= :separator item) [:li {:class (stl/css :separator)
:key (dm/str element-id "-" index)}]
;; Remove items with missing references
(seq (:errors item)) nil
:else (let [{:keys [label selected? errors]} item
highlighted? (= highlighted index)]
[:li
{:key (str element-id "-" index)
:class (stl/css-case :dropdown-element true
:is-selected selected?
:is-highlighted highlighted?)
:data-label label
:disabled (seq errors)
:on-click #(on-select item)}
[:span {:class (stl/css :label)} label]
[:span {:class (stl/css :value)} (wtc/resolve-token-value item)]
[:span {:class (stl/css :check-icon)} i/tick]])))]]]))
(mf/defc editable-select
[{:keys [value options disabled class on-change placeholder on-blur on-token-remove position input-props] :as params}]
(let [{:keys [type]} input-props
input-class (:class input-props)
state* (mf/use-state {:id (uuid/next)
:is-open? false
:current-value value
:token-value nil
:current-item nil
:top nil
:left nil
:bottom nil})
state (deref state*)
is-open? (:is-open? state)
refocus? (:refocus? state)
current-value (:current-value state)
element-id (:id state)
min-val (get params :min)
max-val (get params :max)
multiple? (= :multiple value)
token (when-not multiple?
(-> (filter :selected? options) (first)))
emit-blur? (mf/use-ref nil)
select-wrapper-ref (mf/use-ref)
toggle-dropdown
(mf/use-fn
(mf/deps state)
#(swap! state* update :is-open? not))
close-dropdown
(fn [event]
(dom/stop-propagation event)
(swap! state* assoc :is-open? false))
labels-map (->> (map (fn [{:keys [label] :as item}]
[label item])
options)
(into {}))
set-token-value!
(fn [value]
(swap! state* assoc :token-value value))
set-value
(fn [value event]
(swap! state* assoc
:current-value value
:token-value value)
(when on-change (on-change value event)))
select-item
(mf/use-fn
(mf/deps on-change on-blur labels-map)
(fn [{:keys [value] :as item}]
(swap! state* assoc
:current-value value
:token-value nil
:current-item item)
(when on-change (on-change item))
(when on-blur (on-blur))))
handle-change-input
(fn [event]
(let [value (-> event dom/get-target dom/get-value)
value (or (d/parse-double value) value)]
(set-value value event)))
handle-token-change-input
(fn [event]
(let [value (-> event dom/get-target dom/get-value)
value (or (d/parse-double value) value)]
(set-token-value! value)))
handle-key-down
(mf/use-fn
(mf/deps set-value is-open? token)
(fn [^js event]
(cond
token (let [backspace? (kbd/backspace? event)
enter? (kbd/enter? event)
value (-> event dom/get-target dom/get-value)
caret-at-beginning? (zero? (.. event -target -selectionStart))
no-text-selected? (str/empty? (.toString (js/document.getSelection)))
delete-token? (and backspace? caret-at-beginning? no-text-selected?)
replace-token-with-value? (and enter? (seq (str/trim value)))]
(cond
delete-token? (do
(dom/prevent-default event)
(on-token-remove token)
;; Re-focus the input value of the newly rendered input element
(swap! state* assoc :refocus? true))
replace-token-with-value? (do
(dom/prevent-default event)
(on-token-remove token)
(handle-change-input event)
(set-token-value! nil))
:else (set-token-value! value)))
(= type "number") (on-number-input-key-down {:event event
:min-val min-val
:max-val max-val
:set-value! set-value}))))
handle-focus
(mf/use-fn
(mf/deps refocus?)
(fn []
(when refocus?
(swap! state* dissoc :refocus?))
(mf/set-ref-val! emit-blur? false)))
handle-blur
(mf/use-fn
(fn []
(mf/set-ref-val! emit-blur? true)
(swap! state* assoc :token-value nil)
(timers/schedule
200
(fn []
(when (and on-blur (mf/ref-val emit-blur?)) (on-blur))))))]
(mf/use-effect
(mf/deps value current-value)
#(when (not= (str value) current-value)
(swap! state* assoc :current-value value)))
(mf/with-effect [is-open?]
(let [wrapper-node (mf/ref-val select-wrapper-ref)
node (dom/get-element-by-class "checked-element is-selected" wrapper-node)
nodes (dom/get-elements-by-class "checked-element-value" wrapper-node)
closest (fn [a b] (first (sort-by #(mth/abs (- % b)) a)))
closest-value (str (closest options value))]
(when is-open?
(if (some? node)
(dom/scroll-into-view-if-needed! node)
(some->> nodes
(d/seek #(= closest-value (dom/get-inner-text %)))
(dom/scroll-into-view-if-needed!)))))
(mf/set-ref-val! emit-blur? (not is-open?)))
[:div {:class (dm/str class " " (stl/css-case :editable-select true
:editable-select-disabled disabled))}
(when-let [{:keys [label value]} token]
[:div {:title (str label ": " value)
:class (stl/css :token-pill)}
(wtc/resolve-token-value token)])
(cond
token [:& :input (merge input-props
{:value (or (:token-value state) "")
:type "text"
:class input-class
:onChange handle-token-change-input
:onKeyDown handle-key-down
:onFocus handle-focus
:onBlur handle-blur})]
(= type "number") [:& numeric-input* (merge input-props
{:autoFocus refocus?
:value (or current-value "")
:className input-class
:onChange set-value
:onFocus handle-focus
:onBlur handle-blur
:placeholder placeholder})]
:else [:& :input (merge input-props
{:value (or current-value "")
:class input-class
:onChange handle-change-input
:onKeyDown handle-key-down
:onFocus handle-focus
:onBlur handle-blur
:placeholder placeholder
:type type})])
(when (seq options)
[:div {:class (stl/css :dropdown-button)
:on-click toggle-dropdown}
i/arrow])
(when (and is-open? (seq options))
[:& dropdown-select {:position position
:on-close close-dropdown
:element-id element-id
:element-ref select-wrapper-ref
:options options
:on-select select-item}])]))

View file

@ -0,0 +1,155 @@
// 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
@import "refactor/common-refactor.scss";
.editable-select {
@extend .asset-element;
margin: 0;
display: flex;
height: calc($s-32 - 2px); // Fixes border being clipped by the input field
width: 100%;
padding: $s-8;
border-radius: $br-8;
position: relative;
cursor: pointer;
background: transparent;
&:hover {
background: transparent;
}
&:focus-within {
.token-pill {
background-color: var(--button-primary-background-color-rest);
color: var(--button-primary-foreground-color-rest);
}
}
.dropdown-button {
@include flexCenter;
margin-right: -$s-8;
padding-right: $s-8;
padding-left: 0;
aspect-ratio: 0.8 / 1;
width: auto;
svg {
@extend .button-icon-small;
transform: rotate(90deg);
stroke: var(--icon-foreground);
}
}
.custom-select-dropdown-list {
min-width: 150px;
width: 100%;
max-width: 200px;
margin-bottom: 0;
}
.token-pill {
background-color: rgb(94 107 120 / 25%);
border-radius: $br-4;
padding: $s-2 $s-6;
text-overflow: ellipsis;
flex: 0 0 auto;
}
.token-pill + input {
flex: 1 1 auto;
width: 0;
}
.custom-select-dropdown-left {
left: 0;
right: unset;
}
.custom-select-dropdown-right {
right: 0;
left: unset;
}
.custom-select-dropdown {
@extend .dropdown-wrapper;
max-height: $s-320;
width: auto;
margin-top: $s-4;
.separator {
margin: 0;
height: $s-12;
}
.dropdown-element {
@extend .dropdown-element-base;
color: var(--menu-foreground-color-rest);
padding: 0;
display: flex;
& > span {
display: flex;
justify-content: flex-start;
align-content: center;
}
.label,
.value {
width: fit-content;
}
.label {
text-transform: unset;
flex: 1;
}
.value {
text-align: right;
justify-content: flex-end;
flex: 0.6;
}
.check-icon {
@include flexCenter;
translate: -$s-4 0;
svg {
@extend .button-icon-small;
visibility: hidden;
stroke: var(--icon-foreground);
}
}
&.is-selected {
color: var(--menu-foreground-color);
.check-icon svg {
stroke: var(--menu-foreground-color);
visibility: visible;
}
}
&:hover {
background-color: var(--menu-background-color-hover);
color: var(--menu-foreground-color-hover);
.check-icon svg {
stroke: var(--menu-foreground-color-hover);
}
}
&.is-highlighted {
background-color: var(--button-primary-background-color-rest);
span {
color: var(--button-primary-foreground-color-rest);
}
.check-icon svg {
stroke: var(--button-primary-foreground-color-rest);
}
}
}
}
}
.editable-select-disabled {
pointer-events: none;
}

View file

@ -0,0 +1,63 @@
(ns app.main.ui.workspace.tokens.errors
(:require
[cuerdas.core :as str]))
(def error-codes
{:error.import/json-parse-error
{:error/code :error.import/json-parse-error
:error/message "Import Error: Could not parse json"}
:error.import/invalid-json-data
{:error/code :error.import/invalid-json-data
:error/message "Import Error: Invalid token data in json."}
:error.import/style-dictionary-reference-errors
{:error/code :error.import/style-dictionary-reference-errors
:error/fn #(str "Import Error:\n\n" (str/join "\n\n" %))}
:error.import/style-dictionary-unknown-error
{:error/code :error.import/style-dictionary-reference-errors
:error/message "Import Error:"}
:error.token/direct-self-reference
{:error/code :error.token/direct-self-reference
:error/message "Token has self reference"}
:error.token/invalid-color
{:error/code :error.token/invalid-color
:error/fn #(str "Invalid color value: " %)}
:error.style-dictionary/missing-reference
{:error/code :error.style-dictionary/missing-reference
:error/fn #(str "Missing token references: " (str/join " " %))}
:error.style-dictionary/invalid-token-value
{:error/code :error.style-dictionary/invalid-token-value
:error/fn #(str "Invalid token value: " %)}
:error/unknown
{:error/code :error/unknown
:error/message "Unknown error"}})
(defn get-error-code [error-key]
(get error-codes error-key (:error/unknown error-codes)))
(defn error-with-value [error-key error-value]
(-> (get-error-code error-key)
(assoc :error/value error-value)))
(defn error-ex-info [error-key error-value exception]
(let [err (-> (error-with-value error-key error-value)
(assoc :error/exception exception))]
(ex-info (:error/code err) err)))
(defn has-error-code? [error-key errors]
(some #(= (:error/code %) error-key) errors))
(defn humanize-errors [errors]
(->> errors
(map (fn [err]
(cond
(:error/fn err) ((:error/fn err) (:error/value err))
(:error/message err) (:error/message err)
:else err)))))

View file

@ -0,0 +1,431 @@
;; 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.main.ui.workspace.tokens.form
(:require-macros [app.main.style :as stl])
(:require
;; ["lodash.debounce" :as debounce]
[app.common.colors :as c]
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.types.tokens-lib :as ctob]
[app.main.data.modal :as modal]
[app.main.data.tokens :as dt]
[app.main.refs :as refs]
[app.main.store :as st]
[app.main.ui.components.color-bullet :refer [color-bullet]]
[app.main.ui.ds.buttons.button :refer [button*]]
[app.main.ui.ds.foundations.assets.icon :as i]
[app.main.ui.ds.foundations.typography.heading :refer [heading*]]
[app.main.ui.ds.foundations.typography.text :refer [text*]]
[app.main.ui.workspace.colorpicker :as colorpicker]
[app.main.ui.workspace.colorpicker.ramp :refer [ramp-selector]]
[app.main.ui.workspace.tokens.common :as tokens.common]
[app.main.ui.workspace.tokens.errors :as wte]
[app.main.ui.workspace.tokens.style-dictionary :as sd]
[app.main.ui.workspace.tokens.tinycolor :as tinycolor]
[app.main.ui.workspace.tokens.token :as wtt]
[app.main.ui.workspace.tokens.update :as wtu]
[app.util.dom :as dom]
[app.util.functions :as uf]
[app.util.i18n :refer [tr]]
[cuerdas.core :as str]
[malli.core :as m]
[malli.error :as me]
[promesa.core :as p]
[rumext.v2 :as mf]))
;; Schemas ---------------------------------------------------------------------
(def valid-token-name-regexp
"Only allow letters and digits for token names.
Also allow one `.` for a namespace separator.
Caution: This will allow a trailing dot like `token-name.`,
But we will trim that in the `finalize-name`,
to not throw too many errors while the user is editing."
#"([a-zA-Z0-9-]+\.?)*")
(def valid-token-name-schema
(m/-simple-schema
{:type :token/invalid-token-name
:pred #(re-matches valid-token-name-regexp %)
:type-properties {:error/fn #(str (:value %) " is not a valid token name.
Token names should only contain letters and digits separated by . characters.")}}))
(defn token-name-schema
"Generate a dynamic schema validation to check if a token path derived from the name already exists at `tokens-tree`."
[{:keys [tokens-tree]}]
(let [path-exists-schema
(m/-simple-schema
{:type :token/name-exists
:pred #(not (wtt/token-name-path-exists? % tokens-tree))
:type-properties {:error/fn #(str "A token already exists at the path: " (:value %))}})]
(m/schema
[:and
[:string {:min 1 :max 255}]
valid-token-name-schema
path-exists-schema])))
(def token-description-schema
(m/schema
[:string {:max 2048}]))
;; Helpers ---------------------------------------------------------------------
(defn finalize-name [name]
(-> (str/trim name)
;; Remove trailing dots
(str/replace #"\.+$" "")))
(defn valid-name? [name]
(seq (finalize-name (str name))))
(defn finalize-value [value]
(-> (str value)
(str/trim)))
(defn valid-value? [value]
(seq (finalize-value value)))
(defn schema-validation->promise [validated]
(if (:errors validated)
(p/rejected validated)
(p/resolved validated)))
;; Component -------------------------------------------------------------------
(defn validate-token-value+
"Validates token value by resolving the value `input` using `StyleDictionary`.
Returns a promise of either resolved tokens or rejects with an error state."
[{:keys [value name-value token tokens]}]
(let [;; When creating a new token we dont have a token name yet,
;; so we use a temporary token name that hopefully doesn't clash with any of the users token names
token-name (if (str/empty? name-value) "__TOKEN_STUDIO_SYSTEM.TEMP" name-value)]
(cond
(empty? (str/trim value))
(p/rejected {:errors [{:error/code :error/empty-input}]})
(ctob/token-value-self-reference? token-name value)
(p/rejected {:errors [(wte/get-error-code :error.token/direct-self-reference)]})
:else
(-> (update tokens token-name merge {:value value
:name token-name
:type (:type token)})
(sd/resolve-tokens+)
(p/then
(fn [resolved-tokens]
(let [{:keys [errors resolved-value] :as resolved-token} (get resolved-tokens token-name)]
(cond
resolved-value (p/resolved resolved-token)
:else (p/rejected {:errors (or errors (wte/get-error-code :error/unknown-error))})))))))))
(defn use-debonced-resolve-callback
"Resolves a token values using `StyleDictionary`.
This function is debounced as the resolving might be an expensive calculation.
Uses a custom debouncing logic, as the resolve function is async."
[name-ref token tokens callback & {:keys [timeout] :or {timeout 160}}]
(let [timeout-id-ref (mf/use-ref nil)
debounced-resolver-callback
(mf/use-fn
(mf/deps token callback tokens)
(fn [value]
(let [timeout-id (js/Symbol)
;; Dont execute callback when the timout-id-ref is outdated because this function got called again
timeout-outdated-cb? #(not= (mf/ref-val timeout-id-ref) timeout-id)]
(mf/set-ref-val! timeout-id-ref timeout-id)
(js/setTimeout
(fn []
(when (not (timeout-outdated-cb?))
(-> (validate-token-value+ {:value value
:name-value @name-ref
:token token
:tokens tokens})
(p/finally
(fn [x err]
(when-not (timeout-outdated-cb?)
(callback (or err x))))))))
timeout))))]
debounced-resolver-callback))
(defonce form-token-cache-atom (atom nil))
(mf/defc ramp
[{:keys [color on-change]}]
(let [wrapper-node-ref (mf/use-ref nil)
dragging? (mf/use-state)
hex->value (fn [hex]
(when-let [tc (tinycolor/valid-color hex)]
(let [hex (str "#" (tinycolor/->hex tc))
[r g b] (c/hex->rgb hex)
[h s v] (c/hex->hsv hex)]
{:hex hex
:r r :g g :b b
:h h :s s :v v
:alpha 1})))
value (mf/use-state (hex->value color))
on-change' (fn [{:keys [hex]}]
(reset! value (hex->value hex))
(when-not (and @dragging? hex)
(on-change hex)))]
(colorpicker/use-color-picker-css-variables! wrapper-node-ref @value)
[:div {:ref wrapper-node-ref}
[:& ramp-selector
{:color @value
:disable-opacity true
:on-start-drag #(reset! dragging? true)
:on-finish-drag #(reset! dragging? false)
:on-change on-change'}]]))
(mf/defc token-value-or-errors
[{:keys [result-or-errors]}]
(let [{:keys [errors]} result-or-errors
empty-message? (or (nil? result-or-errors)
(wte/has-error-code? :error/empty-input errors))
message (cond
empty-message? (dm/str (tr "workspace.token.resolved-value") "-")
errors (->> (wte/humanize-errors errors)
(str/join "\n"))
:else (dm/str (tr "workspace.token.resolved-value") result-or-errors))]
[:> text* {:as "p"
:typography "body-small"
:class (stl/css-case :resolved-value true
:resolved-value-placeholder empty-message?
:resolved-value-error (seq errors))}
message]))
(mf/defc form
{::mf/wrap-props false}
[{:keys [token token-type action selected-token-set-id]}]
(let [validate-name? (mf/use-state (not (:id token)))
token (or token {:type token-type})
color? (wtt/color-token? token)
selected-set-tokens (mf/deref refs/workspace-selected-token-set-tokens)
active-theme-tokens (mf/deref refs/workspace-active-theme-sets-tokens)
resolved-tokens (sd/use-resolved-tokens active-theme-tokens {:cache-atom form-token-cache-atom
:interactive? true})
token-path (mf/use-memo
(mf/deps (:name token))
#(wtt/token-name->path (:name token)))
selected-set-tokens-tree (mf/use-memo
(mf/deps token-path selected-set-tokens)
(fn []
(-> (ctob/tokens-tree selected-set-tokens)
;; Allow setting editing token to it's own path
(d/dissoc-in token-path))))
;; Name
name-ref (mf/use-var (:name token))
name-errors (mf/use-state nil)
validate-name
(mf/use-fn
(mf/deps selected-set-tokens-tree)
(fn [value]
(let [schema (token-name-schema {:token token
:tokens-tree selected-set-tokens-tree})]
(m/explain schema (finalize-name value)))))
on-update-name-debounced
(mf/use-fn
(uf/debounce (fn [e]
(let [value (dom/get-target-val e)
errors (validate-name value)]
;; Prevent showing error when just going to another field on a new token
(when-not (and validate-name? (str/empty? value))
(reset! validate-name? false)
(reset! name-errors errors))))))
on-update-name
(mf/use-fn
(mf/deps on-update-name-debounced)
(fn [e]
(reset! name-ref (dom/get-target-val e))
(on-update-name-debounced e)))
valid-name-field? (and
(not @name-errors)
(valid-name? @name-ref))
;; Value
color (mf/use-state (when color? (:value token)))
color-ramp-open? (mf/use-state false)
value-input-ref (mf/use-ref nil)
value-ref (mf/use-var (:value token))
token-resolve-result (mf/use-state (get-in resolved-tokens [(wtt/token-identifier token) :resolved-value]))
set-resolve-value
(mf/use-fn
(fn [token-or-err]
(let [error? (:errors token-or-err)
v (if error?
token-or-err
(:resolved-value token-or-err))]
(when color? (reset! color (if error? nil v)))
(reset! token-resolve-result v))))
on-update-value-debounced (use-debonced-resolve-callback name-ref token active-theme-tokens set-resolve-value)
on-update-value (mf/use-fn
(mf/deps on-update-value-debounced)
(fn [e]
(let [value (dom/get-target-val e)]
(reset! value-ref value)
(on-update-value-debounced value))))
on-update-color (mf/use-fn
(mf/deps on-update-value-debounced)
(fn [hex-value]
(reset! value-ref hex-value)
(set! (.-value (mf/ref-val value-input-ref)) hex-value)
(on-update-value-debounced hex-value)))
value-error? (seq (:errors @token-resolve-result))
valid-value-field? (and
(not value-error?)
(valid-value? @token-resolve-result))
;; Description
description-ref (mf/use-var (:description token))
description-errors (mf/use-state nil)
validate-descripion (mf/use-fn #(m/explain token-description-schema %))
on-update-description-debounced (mf/use-fn
(uf/debounce (fn [e]
(let [value (dom/get-target-val e)
errors (validate-descripion value)]
(reset! description-errors errors)))))
on-update-description
(mf/use-fn
(mf/deps on-update-description-debounced)
(fn [e]
(reset! description-ref (dom/get-target-val e))
(on-update-description-debounced e)))
valid-description-field? (not @description-errors)
;; Form
disabled? (or (not valid-name-field?)
(not valid-value-field?)
(not valid-description-field?))
on-submit
(mf/use-fn
(mf/deps validate-name validate-descripion token resolved-tokens)
(fn [e]
(dom/prevent-default e)
;; We have to re-validate the current form values before submitting
;; because the validation is asynchronous/debounced
;; and the user might have edited a valid form to make it invalid,
;; and press enter before the next validations could return.
(let [final-name (finalize-name @name-ref)
valid-name?+ (-> (validate-name final-name) schema-validation->promise)
final-value (finalize-value @value-ref)
final-description @description-ref
valid-description?+ (some-> final-description validate-descripion schema-validation->promise)]
(-> (p/all [valid-name?+
valid-description?+
(validate-token-value+ {:value final-value
:name-value final-name
:token token
:tokens resolved-tokens})])
(p/finally (fn [result err]
;; The result should be a vector of all resolved validations
;; We do not handle the error case as it will be handled by the components validations
(when (and (seq result) (not err))
(st/emit! (dt/update-create-token {:token (ctob/make-token :name final-name
:type (or (:type token) token-type)
:value final-value
:description final-description)
:prev-token-name (:name token)}))
(st/emit! (wtu/update-workspace-tokens))
(modal/hide!))))))))
on-delete-token
(mf/use-fn
(mf/deps selected-token-set-id)
(fn [e]
(dom/prevent-default e)
(modal/hide!)
(st/emit! (dt/delete-token selected-token-set-id (:name token)))))
on-cancel
(mf/use-fn
(fn [e]
(dom/prevent-default e)
(modal/hide!)))]
[:form {:class (stl/css :form-wrapper)
:on-submit on-submit}
[:div {:class (stl/css :token-rows)}
[:> heading* {:level 2 :typography "headline-medium" :class (stl/css :form-modal-title)}
(if (= action "edit")
(tr "workspace.token.edit-token")
(tr "workspace.token.create-token" token-type))]
[:div {:class (stl/css :input-row)}
;; This should be remove when labeled-imput is modified
[:span {:class (stl/css :labeled-input-label)} "Name"]
[:& tokens.common/labeled-input {:label "Name"
:error? @name-errors
:input-props {:default-value @name-ref
:auto-focus true
:on-blur on-update-name
:on-change on-update-name}}]
(for [error (->> (:errors @name-errors)
(map #(-> (assoc @name-errors :errors [%])
(me/humanize))))]
[:> text* {:as "p"
:key error
:typography "body-small"
:class (stl/css :error)}
error])]
[:div {:class (stl/css :input-row)}
;; This should be remove when labeled-imput is modified
[:span {:class (stl/css :labeled-input-label)} "value"]
[:& tokens.common/labeled-input {:label "Value"
:input-props {:default-value @value-ref
:on-blur on-update-value
:on-change on-update-value
:ref value-input-ref}
:render-right (when color?
(mf/fnc color-bullet []
[:div {:class (stl/css :color-bullet)
:on-click #(swap! color-ramp-open? not)}
(if-let [hex (some-> @color tinycolor/valid-color tinycolor/->hex)]
[:& color-bullet {:color hex
:mini? true}]
[:div {:class (stl/css :color-bullet-placeholder)}])]))}]
(when @color-ramp-open?
[:& ramp {:color (some-> (or @token-resolve-result (:value token))
(tinycolor/valid-color))
:on-change on-update-color}])
[:& token-value-or-errors {:result-or-errors @token-resolve-result}]]
[:div {:class (stl/css :input-row)}
;; This should be remove when labeled-imput is modified
[:span {:class (stl/css :labeled-input-label)} "Description"]
[:& tokens.common/labeled-input {:label "Description"
:input-props {:default-value @description-ref
:on-change on-update-description}}]
(when @description-errors
[:> text* {:as "p"
:typography "body-small"
:class (stl/css :error)}
(me/humanize @description-errors)])]
[:div {:class (stl/css-case :button-row true
:with-delete (= action "edit"))}
(when (= action "edit")
[:> button* {:on-click on-delete-token
:class (stl/css :delete-btn)
:type "button"
:icon i/delete
:variant "secondary"}
(tr "labels.delete")])
[:> button* {:on-click on-cancel
:type "button"
:variant "secondary"}
(tr "labels.cancel")]
[:> button* {:type "submit"
:variant "primary"
:disabled disabled?}
(tr "labels.save")]]]]))

View file

@ -0,0 +1,85 @@
// 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
@import "refactor/common-refactor.scss";
@import "./common.scss";
.form-wrapper {
width: $s-384;
}
.button-row {
display: grid;
grid-template-columns: auto auto;
justify-content: end;
gap: $s-12;
padding-block-start: $s-8;
}
.with-delete {
grid-template-columns: 1fr auto auto;
}
.delete-btn {
justify-self: start;
}
.token-rows {
display: flex;
flex-direction: column;
gap: $s-16;
}
.input-row {
display: flex;
flex-direction: column;
gap: $s-4;
}
.labeled-input-label {
color: var(--color-foreground-primary);
}
.error {
padding: $s-4 $s-6;
margin-bottom: 0;
color: var(--status-color-error-500);
}
.resolved-value {
--input-hint-color: var(--color-foreground-primary);
margin-bottom: 0;
padding: $s-4 $s-6;
color: var(--input-hint-color);
}
.resolved-value-placeholder {
--input-hint-color: var(--color-foreground-secondary);
}
.resolved-value-error {
--input-hint-color: var(--status-color-error-500);
}
.color-bullet {
margin-right: $s-8;
cursor: pointer;
}
.color-bullet-placeholder {
width: var(--bullet-size, $s-16);
height: var(--bullet-size, $s-16);
min-width: var(--bullet-size, $s-16);
min-height: var(--bullet-size, $s-16);
margin-top: 0;
background-color: color-mix(in hsl, var(--color-foreground-secondary) 30%, transparent);
border-radius: $br-4;
cursor: pointer;
}
.form-modal-title {
color: var(--color-foreground-primary);
}

View file

@ -0,0 +1,146 @@
;; 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.main.ui.workspace.tokens.modals
(:require-macros [app.main.style :as stl])
(:require
[app.main.data.modal :as modal]
[app.main.refs :as refs]
[app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
[app.main.ui.ds.foundations.assets.icon :as i]
[app.main.ui.workspace.tokens.form :refer [form]]
[app.util.i18n :refer [tr]]
[okulary.core :as l]
[rumext.v2 :as mf]))
;; Component -------------------------------------------------------------------
(defn calculate-position
"Calculates the style properties for the given coordinates and position"
[{vh :height} position x y]
(let [;; picker height in pixels
h 510
;; Checks for overflow outside the viewport height
overflow-fix (max 0 (+ y (- 50) h (- vh)))
x-pos 325]
(cond
(or (nil? x) (nil? y)) {:left "auto" :right "16rem" :top "4rem"}
(= position :left) {:left (str (- x x-pos) "px")
:top (str (- y 50 overflow-fix) "px")}
:else {:left (str (+ x 80) "px")
:top (str (- y 70 overflow-fix) "px")})))
(defn use-viewport-position-style [x y position]
(let [vport (-> (l/derived :vport refs/workspace-local)
(mf/deref))]
(-> (calculate-position vport position x y)
(clj->js))))
(mf/defc token-update-create-modal
{::mf/wrap-props false}
[{:keys [x y position token token-type action selected-token-set-id] :as _args}]
(let [wrapper-style (use-viewport-position-style x y position)
close-modal (mf/use-fn
(fn []
(modal/hide!)))]
[:div {:class (stl/css :token-modal-wrapper)
:style wrapper-style}
[:> icon-button* {:on-click close-modal
:class (stl/css :close-btn)
:icon i/close
:variant "action"
:aria-label (tr "labels.close")}]
[:& form {:token token
:action action
:selected-token-set-id selected-token-set-id
:token-type token-type}]]))
;; Modals ----------------------------------------------------------------------
(mf/defc boolean-modal
{::mf/register modal/components
::mf/register-as :tokens/boolean}
[properties]
[:& token-update-create-modal properties])
(mf/defc border-radius-modal
{::mf/register modal/components
::mf/register-as :tokens/border-radius}
[properties]
[:& token-update-create-modal properties])
(mf/defc color-modal
{::mf/register modal/components
::mf/register-as :tokens/color}
[properties]
[:& token-update-create-modal properties])
(mf/defc stroke-width-modal
{::mf/register modal/components
::mf/register-as :tokens/stroke-width}
[properties]
[:& token-update-create-modal properties])
(mf/defc box-shadow-modal
{::mf/register modal/components
::mf/register-as :tokens/box-shadow}
[properties]
[:& token-update-create-modal properties])
(mf/defc sizing-modal
{::mf/register modal/components
::mf/register-as :tokens/sizing}
[properties]
[:& token-update-create-modal properties])
(mf/defc dimensions-modal
{::mf/register modal/components
::mf/register-as :tokens/dimensions}
[properties]
[:& token-update-create-modal properties])
(mf/defc numeric-modal
{::mf/register modal/components
::mf/register-as :tokens/numeric}
[properties]
[:& token-update-create-modal properties])
(mf/defc opacity-modal
{::mf/register modal/components
::mf/register-as :tokens/opacity}
[properties]
[:& token-update-create-modal properties])
(mf/defc other-modal
{::mf/register modal/components
::mf/register-as :tokens/other}
[properties]
[:& token-update-create-modal properties])
(mf/defc rotation-modal
{::mf/register modal/components
::mf/register-as :tokens/rotation}
[properties]
[:& token-update-create-modal properties])
(mf/defc spacing-modal
{::mf/register modal/components
::mf/register-as :tokens/spacing}
[properties]
[:& token-update-create-modal properties])
(mf/defc string-modal
{::mf/register modal/components
::mf/register-as :tokens/string}
[properties]
[:& token-update-create-modal properties])
(mf/defc typography-modal
{::mf/register modal/components
::mf/register-as :tokens/typography}
[properties]
[:& token-update-create-modal properties])

View file

@ -0,0 +1,24 @@
// 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
@import "refactor/common-refactor.scss";
.token-modal-wrapper {
@extend .modal-container-base;
@include menuShadow;
position: absolute;
width: auto;
min-width: auto;
z-index: 11;
overflow-y: auto;
overflow-x: hidden;
}
.close-btn {
position: absolute;
top: $s-6;
right: $s-6;
}

View file

@ -0,0 +1,369 @@
;; 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.main.ui.workspace.tokens.modals.themes
(:require-macros [app.main.style :as stl])
(:require
[app.common.data.macros :as dm]
[app.common.types.tokens-lib :as ctob]
[app.main.data.modal :as modal]
[app.main.data.tokens :as wdt]
[app.main.refs :as refs]
[app.main.store :as st]
[app.main.ui.components.radio-buttons :refer [radio-button radio-buttons]]
[app.main.ui.ds.buttons.button :refer [button*]]
[app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
[app.main.ui.ds.foundations.assets.icon :refer [icon*] :as ic]
[app.main.ui.ds.foundations.typography.heading :refer [heading*]]
[app.main.ui.ds.foundations.typography.text :refer [text*]]
[app.main.ui.icons :as i]
[app.main.ui.workspace.tokens.common :refer [labeled-input] :as wtco]
[app.main.ui.workspace.tokens.sets :as wts]
[app.main.ui.workspace.tokens.sets-context :as sets-context]
[app.util.dom :as dom]
[app.util.i18n :refer [tr]]
[cuerdas.core :as str]
[rumext.v2 :as mf]))
(mf/defc empty-themes
[{:keys [set-state]}]
(let [create-theme
(mf/use-fn
(mf/deps set-state)
#(set-state (fn [_] {:type :create-theme})))]
[:div {:class (stl/css :themes-modal-wrapper)}
[:> heading* {:level 2 :typography "headline-medium" :class (stl/css :themes-modal-title)}
(tr "workspace.token.themes")]
[:div {:class (stl/css :empty-themes-wrapper)}
[:div {:class (stl/css :empty-themes-message)}
[:> text* {:as "span" :typography "title-medium" :class (stl/css :empty-theme-title)}
(tr "workspace.token.no-themes-currently")]
[:> text* {:as "span"
:class (stl/css :empty-theme-subtitle)
:typography "body-medium"}
(tr "workspace.token.create-new-theme")]]
[:div {:class (stl/css :button-footer)}
[:> button* {:variant "primary"
:type "button"
:on-click create-theme}
(tr "workspace.token.new-theme")]]]]))
(mf/defc switch
[{:keys [selected? name on-change]}]
(let [selected (if selected? :on :off)]
[:& radio-buttons {:selected selected
:on-change on-change
:name name}
[:& radio-button {:id :on
:value :on
:icon i/tick
:label ""}]
[:& radio-button {:id :off
:value :off
:icon i/close
:label ""}]]))
(mf/defc themes-overview
[{:keys [set-state]}]
(let [active-theme-ids (mf/deref refs/workspace-active-theme-paths)
themes-groups (mf/deref refs/workspace-token-theme-tree-no-hidden)
create-theme
(mf/use-fn
(mf/deps set-state)
(fn [e]
(dom/prevent-default e)
(dom/stop-propagation e)
(set-state (fn [_] {:type :create-theme}))))]
[:div {:class (stl/css :themes-modal-wrapper)}
[:> heading* {:level 2 :typography "headline-medium" :class (stl/css :themes-modal-title)}
(tr "workspace.token.themes")]
[:ul {:class (stl/css :theme-group-wrapper)}
(for [[group themes] themes-groups]
[:li {:key (dm/str "token-theme-group" group)}
(when (seq group)
[:> heading* {:level 3
:class (stl/css :theme-group-label)
:typography "body-large"}
[:span {:class (stl/css :group-title)}
[:> icon* {:id "group"}]
group]])
[:ul {:class (stl/css :theme-group-rows-wrapper)}
(for [[_ {:keys [group name] :as theme}] themes
:let [theme-id (ctob/theme-path theme)
selected? (some? (get active-theme-ids theme-id))
delete-theme
(fn [e]
(dom/prevent-default e)
(dom/stop-propagation e)
(st/emit! (wdt/delete-token-theme group name)))
on-edit-theme
(fn [e]
(dom/prevent-default e)
(dom/stop-propagation e)
(set-state (fn [_] {:type :edit-theme
:theme-path [(:id theme) (:group theme) (:name theme)]})))]]
[:li {:key theme-id
:class (stl/css :theme-row)}
[:div {:class (stl/css :theme-row-left)}
;; FIREEEEEEEEEE THIS
[:div {:on-click (fn [e]
(dom/prevent-default e)
(dom/stop-propagation e)
(st/emit! (wdt/toggle-token-theme-active? group name)))}
[:& switch {:name (tr "workspace.token.theme" name)
:on-change (constantly nil)
:selected? selected?}]]
[:> text* {:as "span" :typography "body-medium" :class (stl/css :theme-name)} name]]
[:div {:class (stl/css :theme-row-right)}
(if-let [sets-count (some-> theme :sets seq count)]
[:> button* {:class (stl/css :sets-count-button)
:variant "secondary"
:type "button"
:on-click on-edit-theme}
[:div {:class (stl/css :label-wrapper)}
[:> text* {:as "span" :typography "body-medium"}
(tr "workspace.token.num-sets" sets-count)]
[:> icon* {:id "arrow-right"}]]]
[:> button* {:class (stl/css :sets-count-empty-button)
:type "button"
:variant "secondary"
:on-click on-edit-theme}
[:div {:class (stl/css :label-wrapper)}
[:> text* {:as "span" :typography "body-medium"}
(tr "workspace.token.no-sets")]
[:> icon* {:id "arrow-right"}]]])
[:> icon-button* {:on-click delete-theme
:variant "ghost"
:aria-label (tr "workspace.token.delete-theme-title")
:icon "delete"}]]])]])]
[:div {:class (stl/css :button-footer)}
[:> button* {:variant "primary"
:type "button"
:icon "add"
:on-click create-theme}
(tr "workspace.token.create-theme-title")]]]))
(mf/defc theme-inputs
[{:keys [theme dropdown-open? on-close-dropdown on-toggle-dropdown on-change-field]}]
(let [theme-groups (mf/deref refs/workspace-token-theme-groups)
group-input-ref (mf/use-ref)
on-update-group (partial on-change-field :group)
on-update-name (partial on-change-field :name)]
[:div {:class (stl/css :edit-theme-inputs-wrapper)}
[:div {:class (stl/css :group-input-wrapper)}
(when dropdown-open?
[:& wtco/dropdown-select {:id ::groups-dropdown
:shortcuts-key ::groups-dropdown
:options (map (fn [group]
{:label group
:value group})
theme-groups)
:on-select (fn [{:keys [value]}]
(set! (.-value (mf/ref-val group-input-ref)) value)
(on-update-group value))
:on-close on-close-dropdown}])
;; TODO: This span should be remove when labeled-input is updated
[:span {:class (stl/css :labeled-input-label)} "Theme group"]
[:& labeled-input {:label "Group"
:input-props {:ref group-input-ref
:default-value (:group theme)
:on-change (comp on-update-group dom/get-target-val)}
:render-right (when (seq theme-groups)
(mf/fnc drop-down-button []
[:button {:class (stl/css :group-drop-down-button)
:type "button"
:on-click (fn [e]
(dom/stop-propagation e)
(on-toggle-dropdown))}
[:> icon* {:id "arrow-down"}]]))}]]
[:div {:class (stl/css :group-input-wrapper)}
;; TODO: This span should be remove when labeled-input is updated
[:span {:class (stl/css :labeled-input-label)} "Theme"]
[:& labeled-input {:label "Theme"
:input-props {:default-value (:name theme)
:on-change (comp on-update-name dom/get-target-val)}}]]]))
(mf/defc theme-modal-buttons
[{:keys [close-modal on-save-form disabled?] :as props}]
[:*
[:> button* {:variant "secondary"
:type "button"
:on-click close-modal}
(tr "labels.cancel")]
[:> button* {:variant "primary"
:type "submit"
:on-click on-save-form
:disabled disabled?}
(tr "workspace.token.save-theme")]])
(mf/defc create-theme
[{:keys [set-state]}]
(let [{:keys [dropdown-open? _on-open-dropdown on-close-dropdown on-toggle-dropdown]} (wtco/use-dropdown-open-state)
theme (ctob/make-token-theme :name "")
on-back #(set-state (constantly {:type :themes-overview}))
on-submit #(st/emit! (wdt/create-token-theme %))
theme-state (mf/use-state theme)
disabled? (-> (:name @theme-state)
(str/trim)
(str/empty?))
on-change-field (fn [field value]
(swap! theme-state #(assoc % field value)))
on-save-form (mf/use-callback
(mf/deps theme-state on-submit)
(fn [e]
(dom/prevent-default e)
(let [theme (-> @theme-state
(update :name str/trim)
(update :group str/trim)
(update :description str/trim))]
(when-not (str/empty? (:name theme))
(on-submit theme)))
(on-back)))
close-modal (mf/use-fn
(fn [e]
(dom/prevent-default e)
(st/emit! (modal/hide))))]
[:div {:class (stl/css :themes-modal-wrapper)}
[:> heading* {:level 2 :typography "headline-medium" :class (stl/css :themes-modal-title)}
(tr "workspace.token.create-theme-title")]
[:form {:on-submit on-save-form}
[:div {:class (stl/css :create-theme-wrapper)}
[:& theme-inputs {:dropdown-open? dropdown-open?
:on-close-dropdown on-close-dropdown
:on-toggle-dropdown on-toggle-dropdown
:theme theme
:on-change-field on-change-field}]
[:div {:class (stl/css :button-footer)}
[:& theme-modal-buttons {:close-modal close-modal
:on-save-form on-save-form
:disabled? disabled?}]]]]]))
(mf/defc controlled-edit-theme
[{:keys [state set-state]}]
(let [{:keys [theme-path]} @state
[_ theme-group theme-name] theme-path
token-sets (mf/deref refs/workspace-ordered-token-sets)
theme (mf/deref (refs/workspace-token-theme theme-group theme-name))
on-back #(set-state (constantly {:type :themes-overview}))
on-submit #(st/emit! (wdt/update-token-theme [(:group theme) (:name theme)] %))
{:keys [dropdown-open? _on-open-dropdown on-close-dropdown on-toggle-dropdown]} (wtco/use-dropdown-open-state)
theme-state (mf/use-state theme)
disabled? (-> (:name @theme-state)
(str/trim)
(str/empty?))
token-set-active? (mf/use-callback
(mf/deps theme-state)
(fn [set-name]
(get-in @theme-state [:sets set-name])))
on-toggle-token-set (mf/use-callback
(mf/deps theme-state)
(fn [set-name]
(swap! theme-state #(ctob/toggle-set % set-name))))
on-change-field (fn [field value]
(swap! theme-state #(assoc % field value)))
on-save-form (mf/use-callback
(mf/deps theme-state on-submit)
(fn [e]
(dom/prevent-default e)
(let [theme (-> @theme-state
(update :name str/trim)
(update :group str/trim)
(update :description str/trim))]
(when-not (str/empty? (:name theme))
(on-submit theme)))
(on-back)))
close-modal
(mf/use-fn
(fn [e]
(dom/prevent-default e)
(st/emit! (modal/hide))))
on-delete-token
(mf/use-fn
(mf/deps theme on-back)
(fn []
(st/emit! (wdt/delete-token-theme (:group theme) (:name theme)))
(on-back)))]
[:div {:class (stl/css :themes-modal-wrapper)}
[:> heading* {:level 2 :typography "headline-medium" :class (stl/css :themes-modal-title)}
(tr "workspace.token.edit-theme-title")]
[:form {:on-submit on-save-form}
[:div {:class (stl/css :edit-theme-wrapper)}
[:button {:on-click on-back
:class (stl/css :back-btn)
:type "button"}
[:> icon* {:id ic/arrow-left :aria-hidden true}]
(tr "workspace.token.back-to-themes")]
[:& theme-inputs {:dropdown-open? dropdown-open?
:on-close-dropdown on-close-dropdown
:on-toggle-dropdown on-toggle-dropdown
:theme theme
:on-change-field on-change-field}]
[:> text* {:as "span" :typography "body-small" :class (stl/css :select-sets-message)}
(tr "workspace.token.set-selection-theme")]
[:div {:class (stl/css :sets-list-wrapper)}
[:& wts/controlled-sets-list
{:token-sets token-sets
:token-set-selected? (constantly false)
:token-set-active? token-set-active?
:on-select on-toggle-token-set
:on-toggle-token-set on-toggle-token-set
:origin "theme-modal"
:context sets-context/static-context}]]
[:div {:class (stl/css :edit-theme-footer)}
[:> button* {:variant "secondary"
:type "button"
:icon "delete"
:on-click on-delete-token}
(tr "labels.delete")]
[:div {:class (stl/css :button-footer)}
[:& theme-modal-buttons {:close-modal close-modal
:on-save-form on-save-form
:disabled? disabled?}]]]]]]))
(mf/defc themes-modal-body
[_]
(let [themes (mf/deref refs/workspace-token-themes-no-hidden)
state (mf/use-state (if (empty? themes)
{:type :create-theme}
{:type :themes-overview}))
set-state (mf/use-callback #(swap! state %))
component (case (:type @state)
:empty-themes empty-themes
:themes-overview (if (empty? themes) empty-themes themes-overview)
:edit-theme controlled-edit-theme
:create-theme create-theme)]
[:& component {:state state
:set-state set-state}]))
(mf/defc token-themes-modal
{::mf/wrap-props false
::mf/register modal/components
::mf/register-as :tokens/themes}
[_args]
(let [handle-close-dialog (mf/use-callback #(st/emit! (modal/hide)))]
[:div {:class (stl/css :modal-overlay)}
[:div {:class (stl/css :modal-dialog)}
[:> icon-button* {:class (stl/css :close-btn)
:on-click handle-close-dialog
:aria-label (tr "labels.close")
:variant "action"
:icon "close"}]
[:& themes-modal-body]]]))

View file

@ -0,0 +1,198 @@
// 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
@import "refactor/common-refactor.scss";
.modal-overlay {
@extend .modal-overlay-base;
}
.modal-dialog {
@extend .modal-container-base;
display: grid;
grid-template-rows: auto 1fr auto;
width: 100%;
max-width: $s-468;
user-select: none;
}
.empty-themes-message {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
gap: $s-12;
padding: $s-72 0;
}
.themes-modal-wrapper {
display: flex;
flex-direction: column;
gap: $s-24;
}
.themes-modal-title {
color: var(--color-foreground-primary);
}
.back-btn {
background-color: transparent;
border: none;
appearance: none;
color: var(--color-foreground-secondary);
width: fit-content;
display: grid;
grid-template-columns: auto auto;
gap: $s-4;
align-items: center;
padding: 0;
&:hover {
color: var(--color-accent-primary);
}
}
.labeled-input-label {
color: var(--color-foreground-primary);
}
.button-footer {
display: flex;
justify-content: flex-end;
gap: $s-6;
}
.edit-theme-footer {
display: flex;
justify-content: space-between;
}
.empty-themes-wrapper {
display: flex;
flex-direction: column;
color: var(--color-foreground-secondary);
}
.empty-theme-subtitle {
color: var(--color-foreground-secondary);
}
.empty-theme-title {
color: var(--color-foreground-primary);
}
.select-sets-message {
color: var(--color-foreground-secondary);
}
.create-theme-wrapper {
display: flex;
flex-direction: column;
gap: $s-24;
}
.close-btn {
position: absolute;
top: $s-8;
right: $s-6;
}
.theme-group-label {
color: var(--color-foreground-secondary);
}
.group-title {
display: flex;
align-items: center;
justify-content: flex-start;
gap: $s-4;
}
.theme-group-rows-wrapper {
display: flex;
flex-direction: column;
gap: $s-6;
}
.theme-group-wrapper {
display: flex;
flex-direction: column;
gap: $s-8;
}
.theme-row {
display: flex;
align-items: center;
gap: $s-12;
justify-content: space-between;
}
.theme-row-left {
display: flex;
align-items: center;
gap: $s-16;
}
.theme-name {
color: var(--color-foreground-primary);
}
.theme-row-right {
display: flex;
align-items: center;
gap: $s-6;
}
.sets-count-button {
text-transform: lowercase;
padding: $s-6;
padding-left: $s-12;
}
.label-wrapper {
display: flex;
align-items: center;
justify-content: center;
}
.edit-theme-wrapper {
display: flex;
flex-direction: column;
gap: $s-12;
}
.sets-list-wrapper {
border: 1px solid color-mix(in hsl, var(--color-foreground-secondary) 30%, transparent);
border-radius: $s-8;
overflow: hidden;
}
.sets-count-empty-button {
text-transform: lowercase;
padding: $s-6;
padding-left: $s-12;
}
.group-input-wrapper {
position: relative;
display: flex;
flex-direction: column;
gap: $s-4;
}
.edit-theme-inputs-wrapper {
display: grid;
grid-template-columns: 0.6fr 1fr;
gap: $s-12;
}
.group-drop-down-button {
@include buttonStyle;
color: var(--color-foreground-secondary);
width: $s-24;
height: 100%;
padding: 0;
margin: 0 $s-6;
}

View file

@ -0,0 +1,270 @@
;; 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.main.ui.workspace.tokens.sets
(:require-macros [app.main.style :as stl])
(:require
[app.main.data.notifications :as ntf]
[app.main.data.tokens :as wdt]
[app.main.refs :as refs]
[app.main.store :as st]
[app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
[app.main.ui.ds.foundations.assets.icon :refer [icon*] :as ic]
[app.main.ui.ds.foundations.typography.text :refer [text*]]
[app.main.ui.hooks :as h]
[app.main.ui.workspace.tokens.sets-context :as sets-context]
[app.util.dom :as dom]
[app.util.i18n :refer [tr]]
[app.util.keyboard :as kbd]
[cuerdas.core :as str]
[rumext.v2 :as mf]))
(defn on-toggle-token-set-click [token-set-name]
(st/emit! (wdt/toggle-token-set {:token-set-name token-set-name})))
(defn on-select-token-set-click [name]
(st/emit! (wdt/set-selected-token-set-id name)))
(defn on-update-token-set [set-name token-set]
(st/emit! (wdt/update-token-set set-name token-set)))
(defn on-create-token-set [token-set]
(st/emit! (wdt/create-token-set token-set)))
(mf/defc editing-node
[{:keys [default-value on-cancel on-submit]}]
(let [ref (mf/use-ref)
on-submit-valid (mf/use-fn
(fn [event]
(let [value (str/trim (dom/get-target-val event))]
(if (or (str/empty? value)
(= value default-value))
(on-cancel)
(on-submit value)))))
on-key-down (mf/use-fn
(fn [event]
(cond
(kbd/enter? event) (on-submit-valid event)
(kbd/esc? event) (on-cancel))))]
[:input
{:class (stl/css :editing-node)
:type "text"
:ref ref
:on-blur on-submit-valid
:on-key-down on-key-down
:auto-focus true
:default-value default-value}]))
(mf/defc sets-tree
[{:keys [token-set
token-set-active?
token-set-selected?
editing?
on-select
on-toggle
on-edit
on-submit
on-cancel]
:as _props}]
(let [{:keys [name _children]} token-set
selected? (and set? (token-set-selected? name))
visible? (token-set-active? name)
collapsed? (mf/use-state false)
set? true #_(= type :set)
group? false #_(= type :group)
editing-node? (editing? name)
on-click
(mf/use-fn
(mf/deps editing-node?)
(fn [event]
(dom/stop-propagation event)
(when-not editing-node?
(on-select name))))
on-context-menu
(mf/use-fn
(mf/deps editing-node? name)
(fn [event]
(dom/prevent-default event)
(dom/stop-propagation event)
(when-not editing-node?
(st/emit!
(wdt/show-token-set-context-menu
{:position (dom/get-client-position event)
:token-set-name name})))))
on-drag
(mf/use-fn
(mf/deps name)
(fn [_]
(when-not selected?
(on-select name))))
on-drop
(mf/use-fn
(mf/deps name)
(fn [position data]
(st/emit! (wdt/move-token-set (:name data) name position))))
on-submit-edit
(mf/use-fn
(mf/deps on-submit token-set)
#(on-submit (assoc token-set :name %)))
on-edit-name
(mf/use-fn
(fn [e]
(let [name (-> (dom/get-current-target e)
(dom/get-data "name"))]
(on-edit name))))
on-toggle-set (fn [event]
(dom/stop-propagation event)
(on-toggle name))
on-collapse (mf/use-fn #(swap! collapsed? not))
[dprops dref]
(h/use-sortable
:data-type "penpot/token-set"
:on-drag on-drag
:on-drop on-drop
:data {:name name}
:draggable? true)]
[:div {:ref dref
:role "button"
:class (stl/css-case :set-item-container true
:dnd-over (= (:over dprops) :center)
:dnd-over-top (= (:over dprops) :top)
:dnd-over-bot (= (:over dprops) :bot))
:on-click on-click
:on-double-click on-edit-name
:on-context-menu on-context-menu
:data-name name}
[:div {:class (stl/css-case :set-item-group group?
:set-item-set set?
:selected-set selected?)}
(when group?
[:> icon-button* {:on-click on-collapse
:aria-label (tr "labels.collapse")
:icon (if @collapsed?
"arrow-right"
"arrow-down")
:variant "action"}])
[:> icon* {:id (if set? "document" "group")
:class (stl/css :icon)}]
(if editing-node?
[:& editing-node {:default-value name
:on-submit on-submit-edit
:on-cancel on-cancel}]
[:*
[:div {:class (stl/css :set-name)} name]
(if set?
[:button {:on-click on-toggle-set
:class (stl/css-case :checkbox-style true
:checkbox-checked-style visible?)}
(when visible?
[:> icon* {:aria-label (tr "workspace.token.select-set")
:class (stl/css :check-icon)
:size "s"
:id ic/tick}])]
nil
#_(when (and children (not @collapsed?))
[:div {:class (stl/css :set-children)}
(for [child-id children]
[:& sets-tree (assoc props :key child-id
{:key child-id}
:set-id child-id
:selected-set-id selected-token-set-id)])]))])]]))
(defn warn-on-try-create-token-set-group! []
(st/emit! (ntf/show {:content (tr "workspace.token.grouping-set-alert")
:notification-type :toast
:type :warning
:timeout 3000})))
(mf/defc controlled-sets-list
[{:keys [token-sets
on-update-token-set
token-set-selected?
token-set-active?
on-create-token-set
on-toggle-token-set
origin
on-select
context]
:as _props}]
(let [{:keys [editing? new? on-edit on-create on-reset] :as ctx} (or context (sets-context/use-context))
avoid-token-set-grouping #(str/replace % "/" "-")
submit-token
#(do
;; TODO: We don't support set grouping for now so we rename sets for now
(when (str/includes? (:name %) "/")
(warn-on-try-create-token-set-group!))
(on-create-token-set (update % :name avoid-token-set-grouping))
(on-reset))]
[:ul {:class (stl/css :sets-list)}
(if (and
(= origin "theme-modal")
(empty? token-sets))
[:> text* {:as "span" :typography "body-small" :class (stl/css :empty-state-message-sets)}
(tr "workspace.token.no-sets-create")]
(for [token-set token-sets]
(when token-set
(let [update-token
#(do
;; TODO: We don't support set grouping for now so we rename sets for now
(when (str/includes? (:name %) "/")
(warn-on-try-create-token-set-group!))
(on-update-token-set (avoid-token-set-grouping (:name token-set)) (update % :name avoid-token-set-grouping))
(on-reset))]
[:& sets-tree
{:key (:name token-set)
:token-set token-set
:token-set-selected? (if new? (constantly false) token-set-selected?)
:token-set-active? token-set-active?
:editing? editing?
:on-select on-select
:on-edit on-edit
:on-toggle on-toggle-token-set
:on-submit update-token
:on-cancel on-reset}]))))
(when new?
[:& sets-tree
{:token-set {:name ""}
:token-set-selected? (constantly true)
:token-set-active? (constantly true)
:editing? (constantly true)
:on-select (constantly nil)
:on-edit on-create
:on-submit submit-token
:on-cancel on-reset}])]))
(mf/defc sets-list
[{:keys []}]
(let [token-sets (mf/deref refs/workspace-ordered-token-sets)
selected-token-set-id (mf/deref refs/workspace-selected-token-set-id)
token-set-selected? (mf/use-fn
(mf/deps token-sets selected-token-set-id)
(fn [set-name]
(= set-name selected-token-set-id)))
active-token-set-ids (mf/deref refs/workspace-active-set-names)
token-set-active? (mf/use-fn
(mf/deps active-token-set-ids)
(fn [id]
(get active-token-set-ids id)))]
[:& controlled-sets-list
{:token-sets token-sets
:token-set-selected? token-set-selected?
:token-set-active? token-set-active?
:on-select on-select-token-set-click
:origin "set-panel"
:on-toggle-token-set on-toggle-token-set-click
:on-update-token-set on-update-token-set
:on-create-token-set on-create-token-set}]))

View file

@ -0,0 +1,125 @@
// 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
@import "refactor/common-refactor.scss";
.sets-list {
width: 100%;
margin-bottom: 0;
overflow-y: auto;
}
.set-item-container {
width: 100%;
cursor: pointer;
color: var(--layer-row-foreground-color);
padding-left: $s-20;
border: $s-2 solid transparent;
&.dnd-over-bot {
border-bottom: $s-2 solid var(--layer-row-foreground-color-hover);
}
&.dnd-over-top {
border-top: $s-2 solid var(--layer-row-foreground-color-hover);
}
&.dnd-over {
border: $s-2 solid var(--layer-row-foreground-color-hover);
}
}
.set-item-set,
.set-item-group {
@include bodySmallTypography;
display: flex;
align-items: center;
min-height: $s-32;
width: 100%;
cursor: pointer;
color: var(--layer-row-foreground-color);
}
.set-name {
@include textEllipsis;
flex-grow: 1;
padding-left: $s-2;
}
.icon {
display: flex;
align-items: center;
width: $s-20;
height: $s-20;
padding-right: $s-4;
}
.checkbox-style {
display: flex;
justify-content: center;
align-items: center;
width: $s-16;
height: $s-16;
margin-inline: $s-6;
background-color: var(--input-checkbox-background-color-rest);
border: 1px solid var(--input-checkbox-border-color-rest);
border-radius: 0.25rem;
padding: 0;
}
.checkbox-checked-style {
background-color: var(--input-border-color-active);
}
.check-icon {
color: var(--color-background-secondary);
}
.set-item-set:hover {
background-color: var(--layer-row-background-color-hover);
color: var(--layer-row-foreground-color-hover);
box-shadow: -100px 0 0 0 var(--layer-row-background-color-hover);
}
.empty-state-message-sets {
display: flex;
justify-content: center;
align-items: center;
width: 100%;
padding: $s-12;
color: var(--color-foreground-secondary);
}
.selected-set {
background-color: var(--layer-row-background-color-selected);
color: var(--layer-row-foreground-color-selected);
box-shadow: -100px 0 0 0 var(--layer-row-background-color-selected);
}
.collapsabled-icon {
@include buttonStyle;
@include flexCenter;
height: $s-24;
border-radius: $br-8;
&:hover {
color: var(--title-foreground-color-hover);
}
}
.editing-node {
@include textEllipsis;
color: var(--layer-row-foreground-color-focus);
}
.editing-node {
@include textEllipsis;
@include bodySmallTypography;
@include removeInputStyle;
flex-grow: 1;
height: $s-28;
padding-left: $s-6;
margin: 0;
border-radius: $br-8;
border: $s-1 solid var(--input-border-color-focus);
color: var(--layer-row-foreground-color);
}

View file

@ -0,0 +1,47 @@
;; 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.main.ui.workspace.tokens.sets-context
(:require
[rumext.v2 :as mf]))
(def initial {:editing-id nil
:new? false})
(def context (mf/create-context initial))
(def static-context
{:editing? (constantly false)
:new? false
:on-edit (constantly nil)
:on-create (constantly nil)
:on-reset (constantly nil)})
(mf/defc provider
{::mf/wrap-props false}
[props]
(let [children (unchecked-get props "children")
state (mf/use-state initial)]
[:& (mf/provider context) {:value state}
children]))
(defn use-context []
(let [ctx (mf/use-ctx context)
{:keys [editing-id new?]} @ctx
editing? (mf/use-callback
(mf/deps editing-id)
#(= editing-id %))
on-edit (mf/use-fn
#(swap! ctx assoc :editing-id %))
on-create (mf/use-fn
#(swap! ctx assoc :editing-id (random-uuid) :new? true))
on-reset (mf/use-fn
#(reset! ctx initial))]
{:editing? editing?
:new? new?
:on-edit on-edit
:on-create on-create
:on-reset on-reset}))

View file

@ -0,0 +1,65 @@
;; 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.main.ui.workspace.tokens.sets-context-menu
(:require-macros [app.main.style :as stl])
(:require
[app.main.data.tokens :as wdt]
[app.main.refs :as refs]
[app.main.store :as st]
[app.main.ui.components.dropdown :refer [dropdown]]
[app.main.ui.workspace.tokens.sets-context :as sets-context]
[app.util.dom :as dom]
[app.util.i18n :refer [tr]]
[okulary.core :as l]
[rumext.v2 :as mf]))
(def sets-menu-ref
(l/derived :token-set-context-menu refs/workspace-local))
(defn- prevent-default
[event]
(dom/prevent-default event)
(dom/stop-propagation event))
(mf/defc menu-entry
{::mf/props :obj}
[{:keys [title value on-click]}]
[:li
{:class (stl/css :context-menu-item)
:data-value value
:on-click on-click}
[:span {:class (stl/css :title)} title]])
(mf/defc menu
[{:keys [token-set-name]}]
(let [{:keys [on-edit]} (sets-context/use-context)
edit-name (mf/use-fn #(on-edit token-set-name))
delete-set (mf/use-fn #(st/emit! (wdt/delete-token-set token-set-name)))]
[:ul {:class (stl/css :context-list)}
[:& menu-entry {:title (tr "labels.rename") :on-click edit-name}]
[:& menu-entry {:title (tr "labels.delete") :on-click delete-set}]]))
(mf/defc sets-context-menu
[]
(let [mdata (mf/deref sets-menu-ref)
top (+ (get-in mdata [:position :y]) 5)
left (+ (get-in mdata [:position :x]) 5)
width (mf/use-state 0)
dropdown-ref (mf/use-ref)
token-set-name (:token-set-name mdata)]
(mf/use-effect
(mf/deps mdata)
(fn []
(when-let [node (mf/ref-val dropdown-ref)]
(reset! width (.-offsetWidth node)))))
[:& dropdown {:show (boolean mdata)
:on-close #(st/emit! wdt/hide-token-set-context-menu)}
[:div {:class (stl/css :token-set-context-menu)
:ref dropdown-ref
:style {:top top :left left}
:on-context-menu prevent-default}
[:& menu {:token-set-name token-set-name}]]]))

View file

@ -0,0 +1,46 @@
// 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
@import "refactor/common-refactor.scss";
.token-set-context-menu {
position: absolute;
z-index: $z-index-4;
}
.context-list {
@include menuShadow;
display: grid;
width: $s-240;
padding: $s-4;
border-radius: $br-8;
border: $s-2 solid var(--panel-border-color);
background-color: var(--menu-background-color);
max-height: 100vh;
overflow-y: auto;
li {
@include bodySmallTypography;
color: var(--menu-foreground-color);
}
}
.context-menu-item {
display: flex;
align-items: center;
height: $s-28;
width: 100%;
padding: $s-6;
border-radius: $br-8;
cursor: pointer;
&:hover {
background-color: var(--menu-background-color-hover);
.title {
color: var(--menu-foreground-color-hover);
}
}
}

View file

@ -0,0 +1,360 @@
;; 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.main.ui.workspace.tokens.sidebar
(:require-macros [app.main.style :as stl])
(:require
[app.common.data :as d]
[app.common.types.tokens-lib :as ctob]
[app.main.data.modal :as modal]
[app.main.data.notifications :as ntf]
[app.main.data.tokens :as dt]
[app.main.refs :as refs]
[app.main.store :as st]
[app.main.ui.components.color-bullet :refer [color-bullet]]
[app.main.ui.components.dropdown-menu :refer [dropdown-menu dropdown-menu-item*]]
[app.main.ui.components.title-bar :refer [title-bar]]
[app.main.ui.ds.buttons.button :refer [button*]]
[app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
[app.main.ui.ds.foundations.typography.text :refer [text*]]
[app.main.ui.hooks :as h]
[app.main.ui.hooks.resize :refer [use-resize-hook]]
[app.main.ui.icons :as i]
[app.main.ui.workspace.sidebar.assets.common :as cmm]
[app.main.ui.workspace.tokens.changes :as wtch]
[app.main.ui.workspace.tokens.context-menu :refer [token-context-menu]]
[app.main.ui.workspace.tokens.errors :as wte]
[app.main.ui.workspace.tokens.sets :refer [sets-list]]
[app.main.ui.workspace.tokens.sets-context :as sets-context]
[app.main.ui.workspace.tokens.sets-context-menu :refer [sets-context-menu]]
[app.main.ui.workspace.tokens.style-dictionary :as sd]
[app.main.ui.workspace.tokens.theme-select :refer [theme-select]]
[app.main.ui.workspace.tokens.token :as wtt]
[app.main.ui.workspace.tokens.token-types :as wtty]
[app.util.dom :as dom]
[app.util.i18n :refer [tr]]
[app.util.webapi :as wapi]
[beicon.v2.core :as rx]
[cuerdas.core :as str]
[okulary.core :as l]
[rumext.v2 :as mf]
[shadow.resource]))
(def lens:token-type-open-status
(l/derived (l/in [:workspace-tokens :open-status]) st/state))
(def ^:private download-icon
(i/icon-xref :download (stl/css :download-icon)))
;; Components ------------------------------------------------------------------
(mf/defc token-pill
{::mf/wrap-props false}
[{:keys [on-click token theme-token highlighted? on-context-menu]}]
(let [{:keys [name value resolved-value errors]} token
errors? (and (seq errors) (seq (:errors theme-token)))]
[:button
{:class (stl/css-case :token-pill true
:token-pill-highlighted highlighted?
:token-pill-invalid errors?)
:title (cond
errors? (sd/humanize-errors token)
:else (->> [(str "Token: " name)
(str (tr "workspace.token.original-value") value)
(str (tr "workspace.token.resolved-value") resolved-value)]
(str/join "\n")))
:on-click on-click
:on-context-menu on-context-menu
:disabled errors?}
(when-let [color (if (seq (ctob/find-token-value-references (:value token)))
(wtt/resolved-value-hex theme-token)
(wtt/resolved-value-hex token))]
[:& color-bullet {:color color
:mini? true}])
name]))
(mf/defc token-section-icon
{::mf/wrap-props false}
[{:keys [type]}]
(case type
:border-radius i/corner-radius
:numeric [:span {:class (stl/css :section-text-icon)} "123"]
:color i/drop-icon
:boolean i/boolean-difference
:opacity [:span {:class (stl/css :section-text-icon)} "%"]
:rotation i/rotation
:spacing i/padding-extended
:string i/text-mixed
:stroke-width i/stroke-size
:typography i/text
;; TODO: Add diagonal icon here when it's available
:dimensions [:div {:style {:rotate "45deg"}} i/constraint-horizontal]
:sizing [:div {:style {:rotate "45deg"}} i/constraint-horizontal]
i/add))
(mf/defc token-component
[{:keys [type tokens selected-shapes token-type-props active-theme-tokens]}]
(let [open? (mf/deref (-> (l/key type)
(l/derived lens:token-type-open-status)))
{:keys [modal attributes all-attributes title]} token-type-props
on-context-menu (mf/use-fn
(fn [event token]
(dom/prevent-default event)
(dom/stop-propagation event)
(st/emit! (dt/show-token-context-menu {:type :token
:position (dom/get-client-position event)
:token-name (:name token)}))))
on-toggle-open-click (mf/use-fn
(mf/deps open? tokens)
#(st/emit! (dt/set-token-type-section-open type (not open?))))
on-popover-open-click (mf/use-fn
(fn [event]
(mf/deps type title)
(let [{:keys [key fields]} modal]
(dom/stop-propagation event)
(st/emit! (dt/set-token-type-section-open type true))
(modal/show! key {:x (.-clientX ^js event)
:y (.-clientY ^js event)
:position :right
:fields fields
:title title
:action "create"
:token-type type}))))
on-token-pill-click (mf/use-fn
(mf/deps selected-shapes token-type-props)
(fn [event token]
(dom/stop-propagation event)
(when (seq selected-shapes)
(st/emit!
(wtch/toggle-token {:token token
:shapes selected-shapes
:token-type-props token-type-props})))))
tokens-count (count tokens)]
[:div {:on-click on-toggle-open-click}
[:& cmm/asset-section {:icon (mf/fnc icon-wrapper []
[:div {:class (stl/css :section-icon)}
[:& token-section-icon {:type type}]])
:title title
:assets-count tokens-count
:open? open?}
[:& cmm/asset-section-block {:role :title-button}
[:button {:class (stl/css :action-button)
:on-click on-popover-open-click}
i/add]]
(when open?
[:& cmm/asset-section-block {:role :content}
[:div {:class (stl/css :token-pills-wrapper)}
(for [token (sort-by :name tokens)]
(let [theme-token (get active-theme-tokens (wtt/token-identifier token))]
[:& token-pill
{:key (:name token)
:token token
:theme-token theme-token
:highlighted? (wtt/shapes-token-applied? token selected-shapes (or all-attributes attributes))
:on-click #(on-token-pill-click % token)
:on-context-menu #(on-context-menu % token)}]))]])]]))
(defn sorted-token-groups
"Separate token-types into groups of `:empty` or `:filled` depending if tokens exist for that type.
Sort each group alphabetically (by their `:token-key`)."
[tokens]
(let [tokens-by-type (ctob/group-by-type tokens)
{:keys [empty filled]} (->> wtty/token-types
(map (fn [[token-key token-type-props]]
{:token-key token-key
:token-type-props token-type-props
:tokens (get tokens-by-type token-key [])}))
(group-by (fn [{:keys [tokens]}]
(if (empty? tokens) :empty :filled))))]
{:empty (sort-by :token-key empty)
:filled (sort-by :token-key filled)}))
(mf/defc themes-header
[_props]
(let [ordered-themes (mf/deref refs/workspace-token-themes-no-hidden)
open-modal
(mf/use-fn
(fn [e]
(dom/stop-propagation e)
(modal/show! :tokens/themes {})))]
[:div {:class (stl/css :themes-wrapper)}
[:span {:class (stl/css :themes-header)} (tr "labels.themes")]
(if (empty? ordered-themes)
[:div {:class (stl/css :empty-theme-wrapper)}
[:> text* {:as "span" :typography "body-small" :class (stl/css :empty-state-message)}
(tr "workspace.token.no-themes")]
[:button {:on-click open-modal
:class (stl/css :create-theme-button)}
(tr "workspace.token.create-one")]]
[:div {:class (stl/css :theme-select-wrapper)}
[:& theme-select]
[:> button* {:variant "secondary"
:on-click open-modal}
(tr "labels.edit")]])]))
(mf/defc add-set-button
[{:keys [on-open style]}]
(let [{:keys [on-create]} (sets-context/use-context)
on-click #(do
(on-open)
(on-create))]
(if (= style "inline")
[:button {:on-click on-click
:class (stl/css :create-theme-button)}
(tr "workspace.token.create-one")]
[:> icon-button* {:variant "ghost"
:icon "add"
:on-click on-click
:aria-label (tr "workspace.token.add set")}])))
(mf/defc themes-sets-tab
[]
(let [token-sets (mf/deref refs/workspace-ordered-token-sets)
open? (mf/use-state true)
on-open (mf/use-fn #(reset! open? true))]
[:& sets-context/provider {}
[:& sets-context-menu]
[:div {:class (stl/css :sets-sidebar)}
[:& themes-header]
[:div {:class (stl/css :sidebar-header)}
[:& title-bar {:collapsable true
:collapsed (not @open?)
:all-clickable true
:title (tr "labels.sets")
:on-collapsed #(swap! open? not)}
[:& add-set-button {:on-open on-open
:style "header"}]]]
(when @open?
[:& h/sortable-container {}
[:*
(when (empty? token-sets)
[:div {:class (stl/css :empty-sets-wrapper)}
[:> text* {:as "span" :typography "body-small" :class (stl/css :empty-state-message)}
(tr "workspace.token.no-sets-yet")]
[:& add-set-button {:on-open on-open
:style "inline"}]])
[:& sets-list]]])]]))
(mf/defc tokens-tab
[_props]
(let [objects (mf/deref refs/workspace-page-objects)
selected (mf/deref refs/selected-shapes)
selected-shapes (into [] (keep (d/getf objects)) selected)
active-theme-tokens (sd/use-active-theme-sets-tokens)
tokens (sd/use-resolved-workspace-tokens)
token-groups (mf/with-memo [tokens]
(sorted-token-groups tokens))]
[:*
[:& token-context-menu]
[:& title-bar {:all-clickable true
:title "TOKENS"}]
[:div.assets-bar
(for [{:keys [token-key token-type-props tokens]} (concat (:filled token-groups)
(:empty token-groups))]
[:& token-component {:key token-key
:type token-key
:selected-shapes selected-shapes
:active-theme-tokens active-theme-tokens
:tokens tokens
:token-type-props token-type-props}])]]))
(mf/defc json-import-button []
(let []
[:div
[:button {:class (stl/css :download-json-button)
:on-click #(.click (js/document.getElementById "file-input"))}
download-icon
"Import JSON"]]))
(mf/defc import-export-button
{::mf/wrap-props false}
[{:keys []}]
(let [show-menu* (mf/use-state false)
show-menu? (deref show-menu*)
open-menu
(mf/use-fn
(fn [event]
(dom/stop-propagation event)
(reset! show-menu* true)))
close-menu
(mf/use-fn
(fn [event]
(dom/stop-propagation event)
(reset! show-menu* false)))
input-ref (mf/use-ref)
on-import
(fn [event]
(let [file (-> event .-target .-files (aget 0))]
(->> (wapi/read-file-as-text file)
(sd/process-json-stream)
(rx/subs! (fn [lib]
(st/emit! (dt/import-tokens-lib lib)))
(fn [err]
(js/console.error err)
(st/emit! (ntf/show {:content (wte/humanize-errors [(ex-data err)])
:type :toast
:level :warning
:timeout 9000})))))
(set! (.-value (mf/ref-val input-ref)) "")))
on-export (fn []
(let [tokens-blob (some-> (deref refs/tokens-lib)
(ctob/encode-dtcg)
(clj->js)
(js/JSON.stringify nil 2)
(wapi/create-blob "application/json"))]
(dom/trigger-download "tokens.json" tokens-blob)))]
[:div {:class (stl/css :import-export-button-wrapper)}
[:input {:type "file"
:ref input-ref
:style {:display "none"}
:id "file-input"
:accept ".json"
:on-change on-import}]
[:button {:class (stl/css :import-export-button)
:on-click open-menu}
download-icon
"Tokens"]
[:& dropdown-menu {:show show-menu?
:on-close close-menu
:list-class (stl/css :import-export-menu)}
[:> dropdown-menu-item* {:class (stl/css :import-export-menu-item)
:on-click #(.click (mf/ref-val input-ref))}
"Import"]
[:> dropdown-menu-item* {:class (stl/css :import-export-menu-item)
:on-click on-export}
"Export"]]]))
(mf/defc tokens-sidebar-tab
{::mf/wrap [mf/memo]
::mf/wrap-props false}
[_props]
(let [{on-pointer-down-pages :on-pointer-down
on-lost-pointer-capture-pages :on-lost-pointer-capture
on-pointer-move-pages :on-pointer-move
size-pages-opened :size}
(use-resize-hook :tokens 200 38 400 :y false nil)]
[:div {:class (stl/css :sidebar-wrapper)}
[:article {:class (stl/css :sets-section-wrapper)
:style {"--resize-height" (str size-pages-opened "px")}}
[:& themes-sets-tab]]
[:article {:class (stl/css :tokens-section-wrapper)}
[:div {:class (stl/css :resize-area-horiz)
:on-pointer-down on-pointer-down-pages
:on-lost-pointer-capture on-lost-pointer-capture-pages
:on-pointer-move on-pointer-move-pages}]
[:& tokens-tab]
[:& import-export-button]]]))

View file

@ -0,0 +1,196 @@
// 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/typography.scss" as *;
@import "refactor/common-refactor.scss";
@import "./common.scss";
.sidebar-wrapper {
display: grid;
grid-template-rows: auto auto 1fr;
// Overflow on the bottom section can't be done without hardcoded values for the height
// This has to be changed from the wrapping sidebar styles
height: calc(100vh - #{$s-84});
overflow: hidden;
}
.sets-section-wrapper {
position: relative;
display: flex;
flex: 1;
height: var(--resize-height);
flex-direction: column;
overflow-y: auto;
scrollbar-gutter: stable;
}
.tokens-section-wrapper {
height: 100%;
padding-left: $s-12;
overflow-y: auto;
scrollbar-gutter: stable;
}
.sets-sidebar {
position: relative;
}
.themes-header {
display: block;
@include headlineSmallTypography;
margin-bottom: $s-8;
padding-left: $s-8;
color: var(--title-foreground-color);
}
.themes-wrapper {
padding: $s-12 0 0 $s-12;
}
.empty-theme-wrapper {
padding: $s-12;
color: var(--color-foreground-secondary);
}
.empty-sets-wrapper {
padding: $s-12;
padding-inline-start: $s-24;
color: var(--color-foreground-secondary);
}
.sidebar-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-left: $s-8;
padding-top: $s-12;
color: var(--layer-row-foreground-color);
}
.empty-state-message {
color: var(--color-foreground-secondary);
}
.token-pills-wrapper {
display: flex;
gap: $s-4;
flex-wrap: wrap;
}
.token-pill {
@extend .button-secondary;
gap: $s-8;
padding: $s-4 $s-8;
border-radius: $br-6;
font-size: $fs-14;
&.token-pill-highlighted {
color: var(--button-primary-foreground-color-rest);
background: var(--button-primary-background-color-rest);
}
&.token-pill-invalid {
background-color: var(--button-secondary-background-color-rest);
color: var(--status-color-error-500);
opacity: 0.8;
}
}
.section-text-icon {
font-size: $fs-12;
width: 16px;
height: 16px;
display: flex;
place-content: center;
}
.section-icon {
margin-right: $s-4;
// Align better with the label
translate: 0px -1px;
}
.import-export-button-wrapper {
position: absolute;
bottom: $s-12;
right: $s-12;
}
.import-export-button {
@extend .button-secondary;
display: flex;
align-items: center;
padding: $s-6 $s-8;
text-transform: uppercase;
gap: $s-8;
.download-icon {
@extend .button-icon;
stroke: var(--icon-foreground);
width: 20px;
height: 20px;
}
}
.import-export-menu {
@extend .menu-dropdown;
top: -#{$s-6};
right: 0;
translate: 0 -100%;
width: $s-192;
margin: 0;
}
.import-export-menu-item {
@extend .menu-item-base;
cursor: pointer;
.open-arrow {
@include flexCenter;
svg {
@extend .button-icon;
stroke: var(--icon-foreground);
}
}
&:hover {
color: var(--menu-foreground-color-hover);
.open-arrow {
svg {
stroke: var(--menu-foreground-color-hover);
}
}
.shortcut-key {
color: var(--menu-shortcut-foreground-color-hover);
}
}
}
.theme-select-wrapper {
display: grid;
grid-template-columns: 1fr 0.28fr;
gap: $s-6;
}
.themes-button {
@extend .button-secondary;
width: auto;
}
.create-theme-button {
@include use-typography("body-small");
background-color: transparent;
border: none;
appearance: none;
color: var(--color-accent-primary);
cursor: pointer;
}
.resize-area-horiz {
position: absolute;
left: 0;
width: 100%;
border-bottom: $s-2 solid var(--resize-area-border-color);
cursor: ns-resize;
}

View file

@ -0,0 +1,261 @@
(ns app.main.ui.workspace.tokens.style-dictionary
(:require
["@tokens-studio/sd-transforms" :as sd-transforms]
["style-dictionary$default" :as sd]
[app.common.logging :as l]
[app.common.transit :as t]
[app.common.types.tokens-lib :as ctob]
[app.main.refs :as refs]
[app.main.ui.workspace.tokens.errors :as wte]
[app.main.ui.workspace.tokens.tinycolor :as tinycolor]
[app.main.ui.workspace.tokens.token :as wtt]
[beicon.v2.core :as rx]
[cuerdas.core :as str]
[promesa.core :as p]
[rumext.v2 :as mf]))
(l/set-level! "app.main.ui.workspace.tokens.style-dictionary" :warn)
;; === Style Dictionary
(def setup-style-dictionary
"Initiates the StyleDictionary instance.
Setup transforms from tokens-studio used to parse and resolved token values."
(do
(sd-transforms/registerTransforms sd)
(.registerFormat sd #js {:name "custom/json"
:format (fn [^js res]
(.-tokens (.-dictionary res)))})
sd))
(def default-config
{:platforms {:json
{:transformGroup "tokens-studio"
;; Required: The StyleDictionary API is focused on files even when working in the browser
:files [{:format "custom/json" :destination "penpot"}]}}
:preprocessors ["tokens-studio"]
;; Silences style dictionary logs and errors
;; We handle token errors in the UI
:log {:verbosity "silent"
:warnings "silent"
:errors {:brokenReferences "console"}}})
(defn parse-sd-token-color-value
"Parses `value` of a color `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]
(if-let [tc (tinycolor/valid-color value)]
{:value value :unit (tinycolor/color-format tc)}
{:errors [(wte/error-with-value :error.token/invalid-color value)]}))
(defn parse-sd-token-dimensions-value
"Parses `value` of a dimensions `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]
(or
(wtt/parse-token-value value)
(if-let [references (seq (ctob/find-token-value-references value))]
{:errors [(wte/error-with-value :error.style-dictionary/missing-reference references)]
:references references}
{:errors [(wte/error-with-value :error.style-dictionary/invalid-token-value value)]})))
(defn process-sd-tokens
"Converts a StyleDictionary dictionary with resolved tokens (aka `sd-tokens`) back to clojure.
The `get-origin-token` argument should be a function that takes an
`sd-token` and returns the original penpot token, so we can merge
the resolved attributes back in.
The `sd-token` will have references in `value` replaced with the computed value as a string.
Here's an example for a `sd-token`:
```js
{
name: 'token.with.reference',
value: '12px',
type: 'border-radius',
path: ['token', 'with', 'reference'],
// The penpot origin token converted to a js object
original: {
name: 'token.with.reference',
value: '{referenced.token}',
type: 'border-radius'
},
}
```
We also convert `sd-token` value string into a unit that can be used as penpot shape attributes.
- Dimensions like '12px' will be converted into numbers
- Colors will be validated & converted to hex
Lastly we check for errors in each token
`sd-token` will keep the missing references in the `value` (E.g \"{missing} + {existing}\" -> \"{missing} + 12px\")
So we parse out the missing references and add them to `:errors` in the final token."
[sd-tokens get-origin-token]
(reduce
(fn [acc ^js sd-token]
(let [origin-token (get-origin-token sd-token)
value (.-value sd-token)
parsed-token-value (case (:type origin-token)
:color (parse-sd-token-color-value value)
(parse-sd-token-dimensions-value value))
output-token (if (:errors parsed-token-value)
(merge origin-token parsed-token-value)
(assoc origin-token
:resolved-value (:value parsed-token-value)
:unit (:unit parsed-token-value)))]
(assoc acc (:name output-token) output-token)))
{} sd-tokens))
(defprotocol IStyleDictionary
(add-tokens [_ tokens])
(enable-debug [_])
(get-config [_])
(build-dictionary [_]))
(deftype StyleDictionary [config]
IStyleDictionary
(add-tokens [_ tokens]
(StyleDictionary. (assoc config :tokens tokens)))
(enable-debug [_]
(StyleDictionary. (update config :log merge {:verbosity "verbose"})))
(get-config [_]
config)
(build-dictionary [_]
(-> (sd. (clj->js config))
(.buildAllPlatforms "json")
(p/then #(.-allTokens ^js %)))))
(defn resolve-tokens-tree+
([tokens-tree get-token]
(resolve-tokens-tree+ tokens-tree get-token (StyleDictionary. default-config)))
([tokens-tree get-token style-dictionary]
(-> style-dictionary
(add-tokens tokens-tree)
(build-dictionary)
(p/then #(process-sd-tokens % get-token)))))
(defn sd-token-name [^js sd-token]
(.. sd-token -original -name))
(defn sd-token-uuid [^js sd-token]
(uuid (.-uuid (.-id ^js sd-token))))
(defn resolve-tokens+ [tokens]
(resolve-tokens-tree+ (ctob/tokens-tree tokens) #(get tokens (sd-token-name %))))
(defn resolve-tokens-interactive+
"Interactive check of resolving tokens.
Uses a ids map to backtrace the original token from the resolved StyleDictionary token.
We have to pass in all tokens from all sets in the entire library to style dictionary
so we know if references are missing / to resolve them and possibly show interactive previews (in the tokens form) to the user.
Since we're using the :name path as the identifier we might be throwing away or overriding tokens in the tree that we pass to StyleDictionary.
So to get back the original token from the resolved sd-token (see my updates for what an sd-token is) we include a temporary :id for the token that we pass to StyleDictionary,
this way after the resolving computation we can restore any token, even clashing ones with the same :name path by just looking up that :id in the ids map."
[tokens]
(let [{:keys [tokens-tree ids]} (ctob/backtrace-tokens-tree tokens)]
(resolve-tokens-tree+ tokens-tree #(get ids (sd-token-uuid %)))))
(defn resolve-tokens-with-errors+ [tokens]
(resolve-tokens-tree+
(ctob/tokens-tree tokens)
#(get tokens (sd-token-name %))
(StyleDictionary. (assoc default-config :log {:verbosity "verbose"}))))
;; === Import
(defn reference-errors
"Extracts reference errors from StyleDictionary."
[err]
(let [[header-1 header-2 & errors] (str/split err "\n")]
(when (and
(= header-1 "Error: ")
(= header-2 "Reference Errors:"))
errors)))
(defn process-json-stream [data-stream]
(->> data-stream
(rx/map (fn [data]
(try
(-> (str/replace data "/" "-") ;; TODO Remove when token groups work
(t/decode-str))
(catch js/Error e
(throw (wte/error-ex-info :error.import/json-parse-error data e))))))
(rx/map (fn [json-data]
(try
(ctob/decode-dtcg-json (ctob/ensure-tokens-lib nil) json-data)
(catch js/Error e
(throw (wte/error-ex-info :error.import/invalid-json-data json-data e))))))
(rx/mapcat (fn [tokens-lib]
(try
(-> (ctob/get-all-tokens tokens-lib)
(resolve-tokens-with-errors+)
(p/then (fn [_] tokens-lib))
(p/catch (fn [sd-error]
(let [reference-errors (reference-errors sd-error)
err (if reference-errors
(wte/error-ex-info :error.import/style-dictionary-reference-errors reference-errors sd-error)
(wte/error-ex-info :error.import/style-dictionary-unknown-error sd-error sd-error))]
(throw err)))))
(catch js/Error e
(p/rejected (wte/error-ex-info :error.import/style-dictionary-unknown-error "" e))))))))
;; === Errors
(defn humanize-errors [{:keys [errors value] :as _token}]
(->> (map (fn [err]
(case err
:error.style-dictionary/missing-reference (str "Could not resolve reference token with the name: " value)
nil))
errors)
(str/join "\n")))
;; === Hooks
(defonce !tokens-cache (atom nil))
(defonce !theme-tokens-cache (atom nil))
(defn use-resolved-tokens
"The StyleDictionary process function is async, so we can't use resolved values directly.
This hook will return the unresolved tokens as state until they are processed,
then the state will be updated with the resolved tokens."
[tokens & {:keys [cache-atom interactive?]
:or {cache-atom !tokens-cache}
:as config}]
(let [tokens-state (mf/use-state (get @cache-atom tokens))]
(mf/use-effect
(mf/deps tokens config)
(fn []
(let [cached (get @cache-atom tokens)]
(cond
(nil? tokens) nil
;; The tokens are already processing somewhere
(p/promise? cached) (-> cached
(p/then #(reset! tokens-state %))
#_(p/catch js/console.error))
;; Get the cached entry
(some? cached) (reset! tokens-state cached)
;; No cached entry, start processing
:else (let [promise+ (if interactive?
(resolve-tokens-interactive+ tokens)
(resolve-tokens+ tokens))]
(swap! cache-atom assoc tokens promise+)
(p/then promise+ (fn [resolved-tokens]
(swap! cache-atom assoc tokens resolved-tokens)
(reset! tokens-state resolved-tokens))))))))
@tokens-state))
(defn use-resolved-workspace-tokens []
(-> (mf/deref refs/workspace-selected-token-set-tokens)
(use-resolved-tokens)))
(defn use-active-theme-sets-tokens []
(-> (mf/deref refs/workspace-active-theme-sets-tokens)
(use-resolved-tokens {:cache-atom !theme-tokens-cache})))

View file

@ -0,0 +1,117 @@
;; 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.main.ui.workspace.tokens.theme-select
(:require-macros [app.main.style :as stl])
(:require
[app.common.data.macros :as dm]
[app.common.types.tokens-lib :as ctob]
[app.common.uuid :as uuid]
[app.main.data.modal :as modal]
[app.main.data.tokens :as wdt]
[app.main.refs :as refs]
[app.main.store :as st]
[app.main.ui.components.dropdown :refer [dropdown]]
[app.main.ui.ds.foundations.assets.icon :refer [icon*] :as i]
[app.main.ui.ds.foundations.typography.text :refer [text*]]
[app.util.dom :as dom]
[app.util.i18n :refer [tr]]
[cuerdas.core :as str]
[rumext.v2 :as mf]))
(mf/defc themes-list
[{:keys [themes active-theme-paths on-close grouped?]}]
(when (seq themes)
[:ul {:class (stl/css :theme-options)}
(for [[_ {:keys [group name] :as theme}] themes
:let [theme-id (ctob/theme-path theme)
selected? (get active-theme-paths theme-id)
select-theme (fn [e]
(dom/stop-propagation e)
(st/emit! (wdt/toggle-token-theme-active? group name))
(on-close))]]
[:li {:key theme-id
:role "option"
:aria-selected selected?
:class (stl/css-case
:checked-element true
:sub-item grouped?
:is-selected selected?)
:on-click select-theme}
[:> text* {:as "span" :typography "body-small" :class (stl/css :label)} name]
[:> icon* {:id i/tick
:aria-hidden true
:class (stl/css-case :check-icon true
:check-icon-visible selected?)}]])]))
(mf/defc theme-options
[{:keys [active-theme-paths themes on-close]}]
(let []
(let [on-edit-click #(modal/show! :tokens/themes {})]
[:ul {:class (stl/css :theme-options :custom-select-dropdown)
:role "listbox"}
(for [[group themes] themes]
[:li {:key group
:aria-labelledby (dm/str group "-label")
:role "group"}
(when (seq group)
[:> text* {:as "span" :typography "headline-small" :class (stl/css :group) :id (dm/str group "-label")} group])
[:& themes-list {:themes themes
:active-theme-paths active-theme-paths
:on-close on-close
:grouped? true}]])
[:li {:class (stl/css :separator)
:aria-hidden true}]
[:li {:class (stl/css-case :checked-element true
:checked-element-button true)
:role "option"
:on-click on-edit-click}
[:> text* {:as "span" :typography "body-small"} (tr "workspace.token.edit-themes")]
[:> icon* {:id i/arrow-right :aria-hidden true}]]])))
(mf/defc theme-select
[{:keys []}]
(let [;; Store
active-theme-paths (mf/deref refs/workspace-active-theme-paths-no-hidden)
active-themes-count (count active-theme-paths)
themes (mf/deref refs/workspace-token-theme-tree-no-hidden)
;; Data
current-label (cond
(> active-themes-count 1) (tr "workspace.token.active-themes" active-themes-count)
(= active-themes-count 1) (some->> (first active-theme-paths)
(ctob/split-token-theme-path)
(str/join " / "))
:else (tr "workspace.token.no-active-theme"))
;; State
state* (mf/use-state
{:id (uuid/next)
:is-open? false})
state (deref state*)
is-open? (:is-open? state)
;; Dropdown
dropdown-element* (mf/use-ref nil)
on-close-dropdown (mf/use-fn #(swap! state* assoc :is-open? false))
on-open-dropdown (mf/use-fn #(swap! state* assoc :is-open? true))]
;; TODO: This element should be accessible by keyboard
[:div {:on-click on-open-dropdown
:aria-expanded is-open?
:aria-haspopup "listbox"
:tab-index "0"
:role "combobox"
:class (stl/css :custom-select)}
[:> text* {:as "span" :typography "body-small" :class (stl/css :current-label)}
current-label]
[:> icon* {:id i/arrow-down :class (stl/css :dropdown-button) :aria-hidden true}]
[:& dropdown {:show is-open?
:on-close on-close-dropdown
:ref dropdown-element*}
[:& theme-options {:active-theme-paths active-theme-paths
:themes themes
:on-close on-close-dropdown}]]]))

View file

@ -0,0 +1,124 @@
// 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
@import "refactor/common-refactor.scss";
.custom-select {
--custom-select-border-color: var(--menu-background-color);
--custom-select-bg-color: var(--menu-background-color);
--custom-select-icon-color: var(--color-foreground-secondary);
--custom-select-text-color: var(--menu-foreground-color);
@extend .new-scrollbar;
position: relative;
display: grid;
grid-template-columns: 1fr auto;
align-items: center;
height: $s-32;
width: 100%;
margin: 0;
padding: $s-8;
border-radius: $br-8;
background-color: var(--custom-select-bg-color);
border: $s-1 solid var(--custom-select-border-color);
color: var(--custom-select-text-color);
cursor: pointer;
&:hover {
--custom-select-bg-color: var(--menu-background-color-hover);
--custom-select-border-color: var(--menu-background-color);
--custom-select-icon-color: var(--menu-foreground-color-hover);
}
&:focus {
--custom-select-bg-color: var(--menu-background-color-focus);
--custom-select-border-color: var(--menu-background-focus);
}
}
.theme-options {
margin-bottom: 0;
}
.group {
display: block;
padding: $s-8;
color: var(--color-foreground-secondary);
}
.disabled {
--custom-select-bg-color: var(--menu-background-color-disabled);
--custom-select-border-color: var(--menu-border-color-disabled);
--custom-select-icon-color: var(--menu-foreground-color-disabled);
--custom-select-text-color: var(--menu-foreground-color-disabled);
pointer-events: none;
cursor: default;
}
.dropdown-button {
@include flexCenter;
color: var(--color-foreground-secondary);
}
.current-icon {
@include flexCenter;
width: $s-24;
padding-right: $s-4;
}
.custom-select-dropdown {
@extend .dropdown-wrapper;
}
.separator {
margin: 0;
height: $s-2;
border-block-start: $s-1 solid color-mix(in hsl, var(--color-foreground-secondary) 20%, transparent);
}
.custom-select-dropdown[data-direction="up"] {
bottom: $s-32;
top: auto;
}
.sub-item {
padding-left: $s-16;
}
.checked-element-button {
@extend .dropdown-element-base;
position: relative;
display: flex;
justify-content: space-between;
padding-right: 0;
}
.checked-element {
@extend .dropdown-element-base;
&.is-selected {
color: var(--menu-foreground-color);
}
&.disabled {
display: none;
}
}
.check-icon {
@include flexCenter;
color: var(--icon-foreground-primary);
visibility: hidden;
}
.label {
flex-grow: 1;
width: 100%;
}
.check-icon-visible {
visibility: visible;
}
.current-label {
@include textEllipsis;
}

View file

@ -0,0 +1,27 @@
(ns app.main.ui.workspace.tokens.tinycolor
"Bindings for tinycolor2 which supports a wide range of css compatible colors.
This library was chosen as it is already used by StyleDictionary,
so there is no extra dependency cost and there was no clojure alternatives with all the necessary features."
(:require
["tinycolor2" :as tinycolor]))
(defn tinycolor? [^js x]
(and (instance? tinycolor x) (.isValid x)))
(defn valid-color [color-str]
(let [tc (tinycolor color-str)]
(when (.isValid tc) tc)))
(defn ->hex [^js tc]
(assert (tinycolor? tc))
(.toHex tc))
(defn color-format [^js tc]
(assert (tinycolor? tc))
(.getFormat tc))
(comment
(some-> (valid-color "red") ->hex)
(some-> (valid-color "red") color-format)
nil)

View file

@ -0,0 +1,142 @@
(ns app.main.ui.workspace.tokens.token
(:require
[app.common.data :as d]
[app.main.ui.workspace.tokens.tinycolor :as tinycolor]
[clojure.set :as set]
[cuerdas.core :as str]))
(def parseable-token-value-regexp
"Regexp that can be used to parse a number value out of resolved token value.
This regexp also trims whitespace around the value."
#"^\s*(-?[0-9]+\.?[0-9]*)(px|%)?\s*$")
(defn parse-token-value
"Parses a resolved value and separates the unit from the value.
Returns a map of {:value `number` :unit `string`}."
[value]
(cond
(number? value) {:value value}
(string? value) (when-let [[_ value unit] (re-find parseable-token-value-regexp value)]
(when-let [parsed-value (d/parse-double value)]
{:value parsed-value
:unit unit}))))
(defn token-identifier [{:keys [name] :as _token}]
name)
(defn attributes-map
"Creats an attributes map using collection of `attributes` for `id`."
[attributes token]
(->> (map (fn [attr] [attr (token-identifier token)]) attributes)
(into {})))
(defn remove-attributes-for-token
"Removes applied tokens with `token-id` for the given `attributes` set from `applied-tokens`."
[attributes token applied-tokens]
(let [attr? (set attributes)]
(->> (remove (fn [[k v]]
(and (attr? k)
(= v (token-identifier token))))
applied-tokens)
(into {}))))
(defn token-attribute-applied?
"Test if `token` is applied to a `shape` on single `token-attribute`."
[token shape token-attribute]
(when-let [id (get-in shape [:applied-tokens token-attribute])]
(= (token-identifier token) id)))
(defn token-applied?
"Test if `token` is applied to a `shape` with at least one of the one of the given `token-attributes`."
[token shape token-attributes]
(some #(token-attribute-applied? token shape %) token-attributes))
(defn shapes-token-applied?
"Test if `token` is applied to to any of `shapes` with at least one of the one of the given `token-attributes`."
[token shapes token-attributes]
(some #(token-applied? token % token-attributes) shapes))
(defn shapes-ids-by-applied-attributes [token shapes token-attributes]
(reduce (fn [acc shape]
(let [applied-ids-by-attribute (->> (map #(when (token-attribute-applied? token shape %)
[% #{(:id shape)}])
token-attributes)
(filter some?)
(into {}))]
(merge-with into acc applied-ids-by-attribute)))
{} shapes))
(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-names-tree-id-map [tokens]
(reduce
(fn [acc [_ {:keys [name] :as token}]]
(when (string? name)
(let [temp-id (random-uuid)
token (assoc token :temp/id temp-id)]
(-> acc
(assoc-in (concat [:tree] (token-name->path name)) token)
(assoc-in [:ids-map temp-id] token)))))
{:tree {}
:ids-map {}}
tokens))
(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))
(defn resolved-value-hex [{:keys [resolved-value] :as token}]
(when (and resolved-value (color-token? token))
(some->> (tinycolor/valid-color resolved-value)
(tinycolor/->hex)
(str "#"))))

View file

@ -0,0 +1,56 @@
(ns app.main.ui.workspace.tokens.token-set
(:require
[app.common.types.tokens-lib :as ctob]))
(defn get-workspace-tokens-lib [state]
(get-in state [:workspace-data :tokens-lib]))
;; Themes ----------------------------------------------------------------------
(defn get-active-theme-ids [state]
(get-in state [:workspace-data :token-active-themes] #{}))
(defn get-temp-theme-id [state]
(get-in state [:workspace-data :token-theme-temporary-id]))
(defn update-theme-id
[state]
(let [active-themes (get-active-theme-ids state)
temporary-theme-id (get-temp-theme-id state)]
(cond
(empty? active-themes) temporary-theme-id
(= 1 (count active-themes)) (first active-themes)
:else temporary-theme-id)))
(defn get-workspace-token-theme [id state]
(get-in state [:workspace-data :token-themes-index id]))
(defn add-token-set-to-token-theme [token-set-id token-theme]
(update token-theme :sets conj token-set-id))
;; Sets ------------------------------------------------------------------------
(defn get-active-theme-sets-tokens-names-map [state]
(when-let [lib (get-workspace-tokens-lib state)]
(ctob/get-active-themes-set-tokens lib)))
;; === Set selection
(defn get-selected-token-set-id [state]
(or (get-in state [:workspace-local :selected-token-set-id])
(some-> (get-workspace-tokens-lib state)
(ctob/get-sets)
(first)
(:name))))
(defn get-selected-token-set [state]
(when-let [id (get-selected-token-set-id state)]
(some-> (get-workspace-tokens-lib state)
(ctob/get-set id))))
(defn get-selected-token-set-tokens [state]
(some-> (get-selected-token-set state)
:tokens))
(defn assoc-selected-token-set-id [state id]
(assoc-in state [:workspace-local :selected-token-set-id] id))

View file

@ -0,0 +1,88 @@
;; 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.main.ui.workspace.tokens.token-types
(:require
[app.common.data :as d :refer [ordered-map]]
[app.common.types.token :as ctt]
[app.main.ui.workspace.tokens.changes :as wtch]
[clojure.set :as set]))
(def token-types
(ordered-map
:border-radius
{:title "Border Radius"
:attributes ctt/border-radius-keys
:on-update-shape wtch/update-shape-radius-all
:modal {:key :tokens/border-radius
:fields [{:label "Border Radius"
:key :border-radius}]}}
:color
{:title "Color"
:attributes ctt/color-keys
:on-update-shape wtch/update-color
:modal {:key :tokens/color
:fields [{:label "Color" :key :color}]}}
:stroke-width
{:title "Stroke Width"
:attributes ctt/stroke-width-keys
:on-update-shape wtch/update-stroke-width
:modal {:key :tokens/stroke-width
:fields [{:label "Stroke Width"
:key :stroke-width}]}}
:sizing
{:title "Sizing"
:attributes #{:width :height}
:all-attributes ctt/sizing-keys
:on-update-shape wtch/update-shape-dimensions
:modal {:key :tokens/sizing
:fields [{:label "Sizing"
:key :sizing}]}}
:dimensions
{:title "Dimensions"
:attributes #{:width :height}
:all-attributes (set/union
ctt/spacing-keys
ctt/sizing-keys
ctt/border-radius-keys
ctt/stroke-width-keys)
:on-update-shape wtch/update-shape-dimensions
:modal {:key :tokens/dimensions
:fields [{:label "Dimensions"
:key :dimensions}]}}
:opacity
{:title "Opacity"
:attributes ctt/opacity-keys
:on-update-shape wtch/update-opacity
:modal {:key :tokens/opacity
:fields [{:label "Opacity"
:key :opacity}]}}
:rotation
{:title "Rotation"
:attributes ctt/rotation-keys
:on-update-shape wtch/update-rotation
:modal {:key :tokens/rotation
:fields [{:label "Rotation"
:key :rotation}]}}
:spacing
{:title "Spacing"
:attributes #{:column-gap :row-gap}
:all-attributes ctt/spacing-keys
:on-update-shape wtch/update-layout-spacing
:modal {:key :tokens/spacing
:fields [{:label "Spacing"
:key :spacing}]}}))
(defn get-token-properties [token]
(get token-types (:type token)))
(defn token-attributes [token-type]
(get-in token-types [token-type :attributes]))

View file

@ -0,0 +1,135 @@
(ns app.main.ui.workspace.tokens.update
(:require
[app.common.types.token :as ctt]
[app.main.data.workspace.shape-layout :as dwsl]
[app.main.data.workspace.undo :as dwu]
[app.main.refs :as refs]
[app.main.ui.workspace.tokens.changes :as wtch]
[app.main.ui.workspace.tokens.style-dictionary :as wtsd]
[app.main.ui.workspace.tokens.token-set :as wtts]
[beicon.v2.core :as rx]
[clojure.data :as data]
[clojure.set :as set]
[potok.v2.core :as ptk]))
;; Constants -------------------------------------------------------------------
(def filter-existing-values? false)
(def attributes->shape-update
{#{:rx :ry} (fn [v ids _] (wtch/update-shape-radius-all v ids))
#{:r1 :r2 :r3 :r4} wtch/update-shape-radius-single-corner
ctt/color-keys wtch/update-color
ctt/stroke-width-keys wtch/update-stroke-width
ctt/sizing-keys wtch/update-shape-dimensions
ctt/opacity-keys wtch/update-opacity
#{:x :y} wtch/update-shape-position
#{:p1 :p2 :p3 :p4} (fn [resolved-value shape-ids attrs]
(dwsl/update-layout shape-ids {:layout-padding (zipmap attrs (repeat resolved-value))}))
#{:column-gap :row-gap} wtch/update-layout-spacing
#{:width :height} wtch/update-shape-dimensions
#{:layout-item-min-w :layout-item-min-h :layout-item-max-w :layout-item-max-h} wtch/update-layout-sizing-limits
ctt/rotation-keys wtch/update-rotation})
(def attribute-actions-map
(reduce
(fn [acc [ks action]]
(into acc (map (fn [k] [k action]) ks)))
{} attributes->shape-update))
;; Helpers ---------------------------------------------------------------------
(defn deep-merge
"Like d/deep-merge but unions set values."
([a b]
(cond
(map? a) (merge-with deep-merge a b)
(set? a) (set/union a b)
:else b))
([a b & rest]
(reduce deep-merge a (cons b rest))))
;; Data flows ------------------------------------------------------------------
(defn invert-collect-key-vals
[xs resolved-tokens shape]
(-> (reduce
(fn [acc [k v]]
(let [resolved-token (get resolved-tokens v)
resolved-value (get resolved-token :resolved-value)
skip? (or
(not (get resolved-tokens v))
(and filter-existing-values? (= (get shape k) resolved-value)))]
(if skip?
acc
(update acc resolved-value (fnil conj #{}) k))))
{} xs)))
(defn split-attribute-groups [attrs-values-map]
(reduce
(fn [acc [attrs v]]
(cond
(some attrs #{:rx :ry}) (let [[_ a b] (data/diff #{:rx :ry} attrs)]
(cond-> (assoc acc b v)
;; Exact match in attrs
a (assoc a v)))
(some attrs #{:widht :height}) (let [[_ a b] (data/diff #{:width :height} attrs)]
(cond-> (assoc acc b v)
;; Exact match in attrs
a (assoc a v)))
(some attrs ctt/spacing-keys) (let [[_ rst gap] (data/diff #{:row-gap :column-gap} attrs)
[_ position padding] (data/diff #{:p1 :p2 :p3 :p4} rst)]
(cond-> acc
(seq gap) (assoc gap v)
(seq position) (assoc position v)
(seq padding) (assoc padding v)))
attrs (assoc acc attrs v)))
{} attrs-values-map))
(defn shape-ids-by-values
[attrs-values-map object-id]
(->> (map (fn [[value attrs]] [attrs {value #{object-id}}]) attrs-values-map)
(into {})))
(defn collect-shapes-update-info [resolved-tokens shapes]
(reduce
(fn [acc [object-id {:keys [applied-tokens] :as shape}]]
(if (seq applied-tokens)
(let [applied-tokens (-> (invert-collect-key-vals applied-tokens resolved-tokens shape)
(shape-ids-by-values object-id)
(split-attribute-groups))]
(deep-merge acc applied-tokens))
acc))
{} shapes))
(defn actionize-shapes-update-info [shapes-update-info]
(mapcat (fn [[attrs update-infos]]
(let [action (some attribute-actions-map attrs)]
(map
(fn [[v shape-ids]]
(action v shape-ids attrs))
update-infos)))
shapes-update-info))
(defn update-tokens [resolved-tokens]
(->> @refs/workspace-page-objects
(collect-shapes-update-info resolved-tokens)
(actionize-shapes-update-info)))
(defn update-workspace-tokens []
(ptk/reify ::update-workspace-tokens
ptk/WatchEvent
(watch [_ state _]
(->>
(rx/from
(->
(wtts/get-active-theme-sets-tokens-names-map state)
(wtsd/resolve-tokens+)))
(rx/mapcat
(fn [sd-tokens]
(let [undo-id (js/Symbol)]
(rx/concat
(rx/of (dwu/start-undo-transaction undo-id))
(update-tokens sd-tokens)
(rx/of (dwu/commit-undo-transaction undo-id))))))))))

View file

@ -25,5 +25,7 @@
lodash-debounce))
(defn debounce
[f timeout]
(ext-debounce f timeout #{:leading false :trailing true}))
([f]
(debounce f 0))
([f timeout]
(ext-debounce f timeout #{:leading false :trailing true})))

View file

@ -8,6 +8,10 @@
[frontend-tests.logic.frame-guides-test]
[frontend-tests.logic.groups-test]
[frontend-tests.plugins.context-shapes-test]
[frontend-tests.tokens.logic.token-actions-test]
[frontend-tests.tokens.style-dictionary-test]
[frontend-tests.tokens.token-form-test]
[frontend-tests.tokens.token-test]
[frontend-tests.util-range-tree-test]
[frontend-tests.util-simple-math-test]
[frontend-tests.util-snap-data-test]))
@ -19,16 +23,20 @@
(.exit js/process 0)
(.exit js/process 1)))
(defn init
[]
(t/run-tests 'frontend-tests.helpers-shapes-test
'frontend-tests.logic.comp-remove-swap-slots-test
'frontend-tests.logic.copying-and-duplicating-test
'frontend-tests.logic.frame-guides-test
'frontend-tests.logic.groups-test
'frontend-tests.plugins.context-shapes-test
'frontend-tests.util-range-tree-test
'frontend-tests.util-snap-data-test
'frontend-tests.util-simple-math-test
'frontend-tests.basic-shapes-test))
(t/run-tests
'frontend-tests.helpers-shapes-test
'frontend-tests.logic.comp-remove-swap-slots-test
'frontend-tests.logic.copying-and-duplicating-test
'frontend-tests.logic.frame-guides-test
'frontend-tests.logic.groups-test
'frontend-tests.plugins.context-shapes-test
'frontend-tests.util-range-tree-test
'frontend-tests.util-snap-data-test
'frontend-tests.util-simple-math-test
'frontend-tests.basic-shapes-test
;; 'frontend-tests.tokens.logic.token-actions-test
;; 'frontend-tests.tokens.style-dictionary-test
'frontend-tests.tokens.token-test
'frontend-tests.tokens.token-form-test))

View file

@ -0,0 +1,74 @@
(ns frontend-tests.tokens.helpers.state
(:require
[app.common.types.tokens-lib :as ctob]
[app.main.ui.workspace.tokens.style-dictionary :as sd]
[beicon.v2.core :as rx]
[potok.v2.core :as ptk]))
(defn end
"Apply `attributes` that match `token` for `shape-ids`.
Optionally remove attributes from `attributes-to-remove`,
this is useful for applying a single attribute from an attributes set
while removing other applied tokens from this set."
[]
(ptk/reify ::end
ptk/WatchEvent
(watch [_ _ _]
(rx/empty))))
(defn end+
[]
(ptk/reify ::end+
ptk/WatchEvent
(watch [_ state _]
(->> (rx/from (-> (get-in state [:workspace-data :tokens-lib])
(ctob/get-active-themes-set-tokens)
(sd/resolve-tokens+)))
(rx/mapcat #(rx/of (end)))))))
(defn stop-on
"Helper function to be used with async version of run-store.
Will stop the execution after event with `event-type` has completed."
[event-type]
(fn [stream]
(->> stream
#_(rx/tap #(prn (ptk/type %)))
(rx/filter #(ptk/type? event-type %)))))
(def stop-on-send-update-indices
"Stops on `send-update-indices` function being called, which should be the last function of an event chain."
(stop-on ::end))
;; Support for async events in tests
;; https://chat.kaleidos.net/penpot-partners/pl/tz1yoes3w3fr9qanxqpuhoz3ch
(defn run-store
"Async version of `frontend-tests.helpers.state/run-store`."
([store done events completed-cb]
(run-store store done events completed-cb nil))
([store done events completed-cb stopper]
(let [stream (ptk/input-stream store)
stopper-s (if (fn? stopper)
(stopper stream)
(rx/filter #(= :the/end %) stream))]
(->> stream
(rx/take-until stopper-s)
(rx/last)
(rx/tap (fn [_]
(completed-cb @store)))
(rx/subs! (fn [_] (done))
(fn [cause]
(js/console.log "[error]:" cause))
(fn [_]
#_(js/console.log "[complete]"))))
(doseq [event (concat events [(end+)])]
(ptk/emit! store event))
(ptk/emit! store :the/end))))
(defn run-store-async
"Helper version of `run-store` that automatically stops on the `send-update-indices` event"
([store done events completed-cb]
(run-store store done events completed-cb stop-on-send-update-indices))
([store done events completed-cb stop-on]
(run-store store done events completed-cb stop-on)))

View file

@ -0,0 +1,26 @@
(ns frontend-tests.tokens.helpers.tokens
(:require
[app.common.test-helpers.ids-map :as thi]
[app.common.types.tokens-lib :as ctob]
[app.main.ui.workspace.tokens.token :as wtt]))
(defn add-token [state label params]
(let [id (thi/new-id! label)
token (assoc params :id id)]
(update-in state [:data :tokens] assoc id token)))
(defn get-token [file name]
(some-> (get-in file [:data :tokens-lib])
(ctob/get-active-themes-set-tokens)
(get name)))
(defn apply-token-to-shape [file shape-label token-label attributes]
(let [first-page-id (get-in file [:data :pages 0])
shape-id (thi/id shape-label)
token (get-token file token-label)
applied-attributes (wtt/attributes-map attributes token)]
(update-in file [:data
:pages-index first-page-id
:objects shape-id
:applied-tokens]
merge applied-attributes)))

View file

@ -0,0 +1,407 @@
(ns frontend-tests.tokens.logic.token-actions-test
(:require
[app.common.logging :as log]
[app.common.test-helpers.compositions :as ctho]
[app.common.test-helpers.files :as cthf]
[app.common.test-helpers.shapes :as cths]
[app.common.types.tokens-lib :as ctob]
[app.main.ui.workspace.tokens.changes :as wtch]
[app.main.ui.workspace.tokens.token :as wtt]
[cljs.test :as t :include-macros true]
[frontend-tests.helpers.pages :as thp]
[frontend-tests.helpers.state :as ths]
[frontend-tests.tokens.helpers.state :as tohs]
[frontend-tests.tokens.helpers.tokens :as toht]))
(t/use-fixtures :each
{:before (fn []
;; Ignore rxjs async errors
(log/set-level! "app.main.data.changes" :error)
(thp/reset-idmap!))})
(defn setup-file []
(cthf/sample-file :file-1 :page-label :page-1))
(def border-radius-token
{:name "borderRadius.sm"
:value "12"
:type :border-radius})
(def reference-border-radius-token
{:name "borderRadius.md"
:value "{borderRadius.sm} * 2"
:type :border-radius})
(defn setup-file-with-tokens
[& {:keys [rect-1 rect-2 rect-3]}]
(-> (setup-file)
(ctho/add-rect :rect-1 rect-1)
(ctho/add-rect :rect-2 rect-2)
(ctho/add-rect :rect-3 rect-3)
(assoc-in [:data :tokens-lib]
(-> (ctob/make-tokens-lib)
(ctob/add-theme (ctob/make-token-theme :name "Theme A" :sets #{"Set A"}))
(ctob/set-active-themes #{"/Theme A"})
(ctob/add-set (ctob/make-token-set :name "Set A"))
(ctob/add-token-in-set "Set A" (ctob/make-token border-radius-token))
(ctob/add-token-in-set "Set A" (ctob/make-token reference-border-radius-token))))))
(t/deftest test-apply-token
(t/testing "applies token to shape and updates shape attributes to resolved value"
(t/async
done
(let [file (setup-file-with-tokens)
store (ths/setup-store file)
rect-1 (cths/get-shape file :rect-1)
events [(wtch/apply-token {:shape-ids [(:id rect-1)]
:attributes #{:rx :ry}
:token (toht/get-token file "borderRadius.md")
:on-update-shape wtch/update-shape-radius-all})]]
(tohs/run-store-async
store done events
(fn [new-state]
(let [file' (ths/get-file-from-store new-state)
token (toht/get-token file' "borderRadius.md")
rect-1' (cths/get-shape file' :rect-1)]
(t/testing "shape `:applied-tokens` got updated"
(t/is (some? (:applied-tokens rect-1')))
(t/is (= (:rx (:applied-tokens rect-1')) (:name token)))
(t/is (= (:ry (:applied-tokens rect-1')) (:name token))))
(t/testing "shape radius got update to the resolved token value."
(t/is (= (:rx rect-1') 24))
(t/is (= (:ry rect-1') 24))))))))))
(t/deftest test-apply-multiple-tokens
(t/testing "applying a token twice with the same attributes will override the previously applied tokens values"
(t/async
done
(let [file (setup-file-with-tokens)
store (ths/setup-store file)
rect-1 (cths/get-shape file :rect-1)
events [(wtch/apply-token {:shape-ids [(:id rect-1)]
:attributes #{:rx :ry}
:token (toht/get-token file "borderRadius.sm")
:on-update-shape wtch/update-shape-radius-all})
(wtch/apply-token {:shape-ids [(:id rect-1)]
:attributes #{:rx :ry}
:token (toht/get-token file "borderRadius.md")
:on-update-shape wtch/update-shape-radius-all})]]
(tohs/run-store-async
store done events
(fn [new-state]
(let [file' (ths/get-file-from-store new-state)
token (toht/get-token file' "borderRadius.md")
rect-1' (cths/get-shape file' :rect-1)]
(t/testing "shape `:applied-tokens` got updated"
(t/is (some? (:applied-tokens rect-1')))
(t/is (= (:rx (:applied-tokens rect-1')) (:name token)))
(t/is (= (:ry (:applied-tokens rect-1')) (:name token))))
(t/testing "shape radius got update to the resolved token value."
(t/is (= (:rx rect-1') 24))
(t/is (= (:ry rect-1') 24))))))))))
(t/deftest test-apply-token-overwrite
(t/testing "removes old token attributes and applies only single attribute"
(t/async
done
(let [file (setup-file-with-tokens)
store (ths/setup-store file)
rect-1 (cths/get-shape file :rect-1)
events [;; Apply "borderRadius.sm" to all border radius attributes
(wtch/apply-token {:attributes #{:rx :ry :r1 :r2 :r3 :r4}
:token (toht/get-token file "borderRadius.sm")
:shape-ids [(:id rect-1)]
:on-update-shape wtch/update-shape-radius-all})
;; Apply single `:r1` attribute to same shape
;; while removing other attributes from the border-radius set
;; but keep `:r4` for testing purposes
(wtch/apply-token {:attributes #{:r1}
:attributes-to-remove #{:rx :ry :r1 :r2 :r3}
:token (toht/get-token file "borderRadius.md")
:shape-ids [(:id rect-1)]
:on-update-shape wtch/update-shape-radius-all})]]
(tohs/run-store-async
store done events
(fn [new-state]
(let [file' (ths/get-file-from-store new-state)
token-sm (toht/get-token file' "borderRadius.sm")
token-md (toht/get-token file' "borderRadius.md")
rect-1' (cths/get-shape file' :rect-1)]
(t/testing "other border-radius attributes got removed"
(t/is (nil? (:rx (:applied-tokens rect-1')))))
(t/testing "r1 got applied with borderRadius.md"
(t/is (= (:r1 (:applied-tokens rect-1')) (:name token-md))))
(t/testing "while :r4 was kept with borderRadius.sm"
(t/is (= (:r4 (:applied-tokens rect-1')) (:name token-sm)))))))))))
(t/deftest test-apply-dimensions
(t/testing "applies dimensions token and updates the shapes width and height"
(t/async
done
(let [dimensions-token {:name "dimensions.sm"
:value "100"
:type :dimensions}
file (-> (setup-file-with-tokens)
(update-in [:data :tokens-lib]
#(ctob/add-token-in-set % "Set A" (ctob/make-token dimensions-token))))
store (ths/setup-store file)
rect-1 (cths/get-shape file :rect-1)
events [(wtch/apply-token {:shape-ids [(:id rect-1)]
:attributes #{:width :height}
:token (toht/get-token file "dimensions.sm")
:on-update-shape wtch/update-shape-dimensions})]]
(tohs/run-store-async
store done events
(fn [new-state]
(let [file' (ths/get-file-from-store new-state)
token-target' (toht/get-token file' "dimensions.sm")
rect-1' (cths/get-shape file' :rect-1)]
(t/testing "shape `:applied-tokens` got updated"
(t/is (some? (:applied-tokens rect-1')))
(t/is (= (:width (:applied-tokens rect-1')) (:name token-target')))
(t/is (= (:height (:applied-tokens rect-1')) (:name token-target'))))
(t/testing "shapes width and height got updated"
(t/is (= (:width rect-1') 100))
(t/is (= (:height rect-1') 100))))))))))
(t/deftest test-apply-sizing
(t/testing "applies sizing token and updates the shapes width and height"
(t/async
done
(let [sizing-token {:name "sizing.sm"
:value "100"
:type :sizing}
file (-> (setup-file-with-tokens)
(update-in [:data :tokens-lib]
#(ctob/add-token-in-set % "Set A" (ctob/make-token sizing-token))))
store (ths/setup-store file)
rect-1 (cths/get-shape file :rect-1)
events [(wtch/apply-token {:shape-ids [(:id rect-1)]
:attributes #{:width :height}
:token (toht/get-token file "sizing.sm")
:on-update-shape wtch/update-shape-dimensions})]]
(tohs/run-store-async
store done events
(fn [new-state]
(let [file' (ths/get-file-from-store new-state)
token-target' (toht/get-token file' "sizing.sm")
rect-1' (cths/get-shape file' :rect-1)]
(t/testing "shape `:applied-tokens` got updated"
(t/is (some? (:applied-tokens rect-1')))
(t/is (= (:width (:applied-tokens rect-1')) (:name token-target')))
(t/is (= (:height (:applied-tokens rect-1')) (:name token-target'))))
(t/testing "shapes width and height got updated"
(t/is (= (:width rect-1') 100))
(t/is (= (:height rect-1') 100))))))))))
(t/deftest test-apply-opacity
(t/testing "applies opacity token and updates the shapes opacity"
(t/async
done
(let [opacity-float {:name "opacity.float"
:value "0.3"
:type :opacity}
opacity-percent {:name "opacity.percent"
:value "40%"
:type :opacity}
opacity-invalid {:name "opacity.invalid"
:value "100"
:type :opacity}
file (-> (setup-file-with-tokens)
(update-in [:data :tokens-lib]
#(-> %
(ctob/add-token-in-set "Set A" (ctob/make-token opacity-float))
(ctob/add-token-in-set "Set A" (ctob/make-token opacity-percent))
(ctob/add-token-in-set "Set A" (ctob/make-token opacity-invalid)))))
store (ths/setup-store file)
rect-1 (cths/get-shape file :rect-1)
rect-2 (cths/get-shape file :rect-2)
rect-3 (cths/get-shape file :rect-3)
events [(wtch/apply-token {:shape-ids [(:id rect-1)]
:attributes #{:opacity}
:token (toht/get-token file "opacity.float")
:on-update-shape wtch/update-opacity})
(wtch/apply-token {:shape-ids [(:id rect-2)]
:attributes #{:opacity}
:token (toht/get-token file "opacity.percent")
:on-update-shape wtch/update-opacity})
(wtch/apply-token {:shape-ids [(:id rect-3)]
:attributes #{:opacity}
:token (toht/get-token file "opacity.invalid")
:on-update-shape wtch/update-opacity})]]
(tohs/run-store-async
store done events
(fn [new-state]
(let [file' (ths/get-file-from-store new-state)
rect-1' (cths/get-shape file' :rect-1)
rect-2' (cths/get-shape file' :rect-2)
rect-3' (cths/get-shape file' :rect-3)
token-opacity-float (toht/get-token file' "opacity.float")
token-opacity-percent (toht/get-token file' "opacity.percent")
token-opacity-invalid (toht/get-token file' "opacity.invalid")]
(t/testing "float value got translated to float and applied to opacity"
(t/is (= (:opacity (:applied-tokens rect-1')) (:name token-opacity-float)))
(t/is (= (:opacity rect-1') 0.3)))
(t/testing "percentage value got translated to float and applied to opacity"
(t/is (= (:opacity (:applied-tokens rect-2')) (:name token-opacity-percent)))
(t/is (= (:opacity rect-2') 0.4)))
(t/testing "invalid opacity value got applied but did not change shape"
(t/is (= (:opacity (:applied-tokens rect-3')) (:name token-opacity-invalid)))
(t/is (nil? (:opacity rect-3')))))))))))
(t/deftest test-apply-rotation
(t/testing "applies rotation token and updates the shapes rotation"
(t/async
done
(let [rotation-token {:name "rotation.medium"
:value "120"
:type :rotation}
file (-> (setup-file-with-tokens)
(update-in [:data :tokens-lib]
#(ctob/add-token-in-set % "Set A" (ctob/make-token rotation-token))))
store (ths/setup-store file)
rect-1 (cths/get-shape file :rect-1)
events [(wtch/apply-token {:shape-ids [(:id rect-1)]
:attributes #{:rotation}
:token (toht/get-token file "rotation.medium")
:on-update-shape wtch/update-rotation})]]
(tohs/run-store-async
store done events
(fn [new-state]
(let [file' (ths/get-file-from-store new-state)
token-target' (toht/get-token file' "rotation.medium")
rect-1' (cths/get-shape file' :rect-1)]
(t/is (some? (:applied-tokens rect-1')))
(t/is (= (:rotation (:applied-tokens rect-1')) (:name token-target')))
(t/is (= (:rotation rect-1') 120)))))))))
(t/deftest test-apply-stroke-width
(t/testing "applies stroke-width token and updates the shapes with stroke"
(t/async
done
(let [stroke-width-token {:name "stroke-width.sm"
:value "10"
:type :stroke-width}
file (-> (setup-file-with-tokens {:rect-1 {:strokes [{:stroke-alignment :inner,
:stroke-style :solid,
:stroke-color "#000000",
:stroke-opacity 1,
:stroke-width 5}]}})
(update-in [:data :tokens-lib]
#(ctob/add-token-in-set % "Set A" (ctob/make-token stroke-width-token))))
store (ths/setup-store file)
rect-with-stroke (cths/get-shape file :rect-1)
rect-without-stroke (cths/get-shape file :rect-2)
events [(wtch/apply-token {:shape-ids [(:id rect-with-stroke) (:id rect-without-stroke)]
:attributes #{:stroke-width}
:token (toht/get-token file "stroke-width.sm")
:on-update-shape wtch/update-stroke-width})]]
(tohs/run-store-async
store done events
(fn [new-state]
(let [file' (ths/get-file-from-store new-state)
token-target' (toht/get-token file' "stroke-width.sm")
rect-with-stroke' (cths/get-shape file' :rect-1)
rect-without-stroke' (cths/get-shape file' :rect-2)]
(t/testing "token got applied to rect with stroke and shape stroke got updated"
(t/is (= (:stroke-width (:applied-tokens rect-with-stroke')) (:name token-target')))
(t/is (= (get-in rect-with-stroke' [:strokes 0 :stroke-width]) 10)))
(t/testing "token got applied to rect without stroke but shape didnt get updated"
(t/is (= (:stroke-width (:applied-tokens rect-without-stroke')) (:name token-target')))
(t/is (empty? (:strokes rect-without-stroke')))))))))))
(t/deftest test-toggle-token-none
(t/testing "should apply token to all selected items, where no item has the token applied"
(t/async
done
(let [file (setup-file-with-tokens)
store (ths/setup-store file)
rect-1 (cths/get-shape file :rect-1)
rect-2 (cths/get-shape file :rect-2)
events [(wtch/toggle-token {:shapes [rect-1 rect-2]
:token-type-props {:attributes #{:rx :ry}
:on-update-shape wtch/update-shape-radius-all}
:token (toht/get-token file "borderRadius.md")})]]
(tohs/run-store-async
store done events
(fn [new-state]
(let [file' (ths/get-file-from-store new-state)
token-2' (toht/get-token file' "borderRadius.md")
rect-1' (cths/get-shape file' :rect-1)
rect-2' (cths/get-shape file' :rect-2)]
(t/is (some? (:applied-tokens rect-1')))
(t/is (some? (:applied-tokens rect-2')))
(t/is (= (:rx (:applied-tokens rect-1')) (:name token-2')))
(t/is (= (:rx (:applied-tokens rect-2')) (:name token-2')))
(t/is (= (:ry (:applied-tokens rect-1')) (:name token-2')))
(t/is (= (:ry (:applied-tokens rect-2')) (:name token-2')))
(t/is (= (:rx rect-1') 24))
(t/is (= (:rx rect-2') 24)))))))))
(t/deftest test-toggle-token-mixed
(t/testing "should unapply given token if one of the selected items has the token applied while keeping other tokens with some attributes"
(t/async
done
(let [file (-> (setup-file-with-tokens)
(toht/apply-token-to-shape :rect-1 "borderRadius.sm" #{:rx :ry})
(toht/apply-token-to-shape :rect-3 "borderRadius.md" #{:rx :ry}))
store (ths/setup-store file)
rect-with-token (cths/get-shape file :rect-1)
rect-without-token (cths/get-shape file :rect-2)
rect-with-other-token (cths/get-shape file :rect-3)
events [(wtch/toggle-token {:shapes [rect-with-token rect-without-token rect-with-other-token]
:token (toht/get-token file "borderRadius.sm")
:token-type-props {:attributes #{:rx :ry}}})]]
(tohs/run-store-async
store done events
(fn [new-state]
(let [file' (ths/get-file-from-store new-state)
rect-with-token' (cths/get-shape file' :rect-1)
rect-without-token' (cths/get-shape file' :rect-2)
rect-with-other-token' (cths/get-shape file' :rect-3)]
(t/testing "rect-with-token got the token removed"
(t/is (nil? (:rx (:applied-tokens rect-with-token'))))
(t/is (nil? (:ry (:applied-tokens rect-with-token')))))
(t/testing "rect-without-token didn't get updated"
(t/is (= (:applied-tokens rect-without-token') (:applied-tokens rect-without-token))))
(t/testing "rect-with-other-token didn't get updated"
(t/is (= (:applied-tokens rect-with-other-token') (:applied-tokens rect-with-other-token)))))))))))
(t/deftest test-toggle-token-apply-to-all
(t/testing "should apply token to all if none of the shapes has it applied"
(t/async
done
(let [file (-> (setup-file-with-tokens)
(toht/apply-token-to-shape :rect-1 "borderRadius.md" #{:rx :ry})
(toht/apply-token-to-shape :rect-3 "borderRadius.md" #{:rx :ry}))
store (ths/setup-store file)
rect-with-other-token-1 (cths/get-shape file :rect-1)
rect-without-token (cths/get-shape file :rect-2)
rect-with-other-token-2 (cths/get-shape file :rect-3)
events [(wtch/toggle-token {:shapes [rect-with-other-token-1 rect-without-token rect-with-other-token-2]
:token (toht/get-token file "borderRadius.sm")
:token-type-props {:attributes #{:rx :ry}}})]]
(tohs/run-store-async
store done events
(fn [new-state]
(let [file' (ths/get-file-from-store new-state)
target-token (toht/get-token file' "borderRadius.sm")
rect-with-other-token-1' (cths/get-shape file' :rect-1)
rect-without-token' (cths/get-shape file' :rect-2)
rect-with-other-token-2' (cths/get-shape file' :rect-3)]
(t/testing "token got applied to all shapes"
(t/is (= (:rx (:applied-tokens rect-with-other-token-1')) (:name target-token)))
(t/is (= (:rx (:applied-tokens rect-without-token')) (:name target-token)))
(t/is (= (:rx (:applied-tokens rect-with-other-token-2')) (:name target-token)))
(t/is (= (:ry (:applied-tokens rect-with-other-token-1')) (:name target-token)))
(t/is (= (:ry (:applied-tokens rect-without-token')) (:name target-token)))
(t/is (= (:ry (:applied-tokens rect-with-other-token-2')) (:name target-token)))))))))))

View file

@ -0,0 +1,115 @@
(ns frontend-tests.tokens.style-dictionary-test
(:require
[app.common.transit :as tr]
[app.common.types.tokens-lib :as ctob]
[app.main.ui.workspace.tokens.style-dictionary :as sd]
[beicon.v2.core :as rx]
[cljs.test :as t :include-macros true]
[promesa.core :as p]))
(t/deftest resolve-tokens-test
(t/async
done
(t/testing "resolves tokens using style-dictionary from a ids map"
(let [tokens (-> (ctob/make-tokens-lib)
(ctob/add-set (ctob/make-token-set :name "core"))
(ctob/add-token-in-set "core" (ctob/make-token {:name "borderRadius.sm"
:value "12px"
:type :border-radius}))
(ctob/add-token-in-set "core" (ctob/make-token {:value "{borderRadius.sm} * 2"
:name "borderRadius.md-with-dashes"
:type :border-radius}))
(ctob/get-all-tokens))]
(-> (sd/resolve-tokens+ tokens)
(p/finally
(fn [resolved-tokens]
(t/is (= 12 (get-in resolved-tokens ["borderRadius.sm" :resolved-value])))
(t/is (= "px" (get-in resolved-tokens ["borderRadius.sm" :unit])))
(t/is (= 24 (get-in resolved-tokens ["borderRadius.md-with-dashes" :resolved-value])))
(t/is (= "px" (get-in resolved-tokens ["borderRadius.md-with-dashes" :unit])))
(done))))))))
(t/deftest process-json-stream-test
(t/async
done
(t/testing "processes empty json string"
(let [json (-> {"core" {"color" {"$value" "red"
"$type" "color"}}}
(tr/encode-str {:type :json-verbose}))]
(->> (rx/of json)
(sd/process-json-stream)
(rx/subs! (fn [tokens-lib]
(t/is (instance? ctob/TokensLib tokens-lib))
(t/is (= "red" (-> (ctob/get-set tokens-lib "core")
(ctob/get-token "color")
(:value))))
(done))))))))
(t/deftest reference-errros-test
(t/testing "Extracts reference errors from StyleDictionary errors"
;; Using unicode for the white-space after "Error: " as some editors might remove it and its more visible
(t/is (=
["Some token references (2) could not be found."
""
"foo.value tries to reference missing, which is not defined."
"color.value tries to reference missing, which is not defined."]
(sd/reference-errors "Error:\u0020
Reference Errors:
Some token references (2) could not be found.
foo.value tries to reference missing, which is not defined.
color.value tries to reference missing, which is not defined.")))
(t/is (nil? (sd/reference-errors nil)))
(t/is (nil? (sd/reference-errors "none")))))
(t/deftest process-empty-json-stream-test
(t/async
done
(t/testing "processes empty json string"
(->> (rx/of "{}")
(sd/process-json-stream)
(rx/subs! (fn [tokens-lib]
(t/is (instance? ctob/TokensLib tokens-lib))
(done)))))))
(t/deftest process-invalid-json-stream-test
(t/async
done
(t/testing "fails on invalid json"
(->> (rx/of "{,}")
(sd/process-json-stream)
(rx/subs!
(fn []
(throw (js/Error. "Should be an error")))
(fn [err]
(t/is (= :error.import/json-parse-error (:error/code (ex-data err))))
(done)))))))
(t/deftest process-non-token-json-stream-test
(t/async
done
(t/testing "fails on non-token json"
(->> (rx/of "{\"foo\": \"bar\"}")
(sd/process-json-stream)
(rx/subs!
(fn []
(throw (js/Error. "Should be an error")))
(fn [err]
(t/is (= :error.import/invalid-json-data (:error/code (ex-data err))))
(done)))))))
(t/deftest process-missing-references-json-test
(t/async
done
(t/testing "fails on missing references in tokens"
(let [json (-> {"core" {"color" {"$value" "{missing}"
"$type" "color"}}}
(tr/encode-str {:type :json-verbose}))]
(->> (rx/of json)
(sd/process-json-stream)
(rx/subs!
(fn []
(throw (js/Error. "Should be an error")))
(fn [err]
(t/is (= :error.import/style-dictionary-reference-errors (:error/code (ex-data err))))
(done))))))))

View file

@ -0,0 +1,26 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS INC
(ns frontend-tests.tokens.token-form-test
(:require
[app.main.ui.workspace.tokens.form :as wtf]
[cljs.test :as t :include-macros true]
[malli.core :as m]))
(t/deftest test-valid-token-name-schema
;; Allow regular namespace token names
(t/is (some? (m/validate wtf/valid-token-name-schema "Foo")))
(t/is (some? (m/validate wtf/valid-token-name-schema "foo")))
(t/is (some? (m/validate wtf/valid-token-name-schema "FOO")))
(t/is (some? (m/validate wtf/valid-token-name-schema "Foo.Bar.Baz")))
;; Allow trailing tokens
(t/is (nil? (m/validate wtf/valid-token-name-schema "Foo.Bar.Baz....")))
;; Disallow multiple separator dots
(t/is (nil? (m/validate wtf/valid-token-name-schema "Foo..Bar.Baz")))
;; Disallow any special characters
(t/is (nil? (m/validate wtf/valid-token-name-schema "Hey Foo.Bar")))
(t/is (nil? (m/validate wtf/valid-token-name-schema "Hey😈Foo.Bar")))
(t/is (nil? (m/validate wtf/valid-token-name-schema "Hey%Foo.Bar"))))

View file

@ -0,0 +1,100 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS INC
(ns frontend-tests.tokens.token-test
(:require
[app.main.ui.workspace.tokens.token :as wtt]
[cljs.test :as t :include-macros true]))
(t/deftest test-parse-token-value
(t/testing "parses double from a token value"
(t/is (= {:value 100.1 :unit nil} (wtt/parse-token-value "100.1")))
(t/is (= {:value -9 :unit nil} (wtt/parse-token-value "-9"))))
(t/testing "trims white-space"
(t/is (= {:value -1.3 :unit nil} (wtt/parse-token-value " -1.3 "))))
(t/testing "parses unit: px"
(t/is (= {:value 70.3 :unit "px"} (wtt/parse-token-value " 70.3px "))))
(t/testing "parses unit: %"
(t/is (= {:value -10 :unit "%"} (wtt/parse-token-value "-10%"))))
(t/testing "parses unit: px")
(t/testing "returns nil for any invalid characters"
(t/is (nil? (wtt/parse-token-value " -1.3a "))))
(t/testing "doesnt accept invalid double"
(t/is (nil? (wtt/parse-token-value ".3")))))
(t/deftest remove-attributes-for-token-id
(t/testing "removes attributes matching the `token`, keeps other attributes"
(t/is (= {:ry "b"}
(wtt/remove-attributes-for-token #{:rx :ry} {:name "a"} {:rx "a" :ry "b"})))))
(t/deftest token-applied-test
(t/testing "matches passed token with `:token-attributes`"
(t/is (true? (wtt/token-applied? {:name "a"} {:applied-tokens {:x "a"}} #{:x}))))
(t/testing "doesn't match empty token"
(t/is (nil? (wtt/token-applied? {} {:applied-tokens {:x "a"}} #{:x}))))
(t/testing "does't match passed token `:id`"
(t/is (nil? (wtt/token-applied? {:name "b"} {:applied-tokens {:x "a"}} #{:x}))))
(t/testing "doesn't match passed `:token-attributes`"
(t/is (nil? (wtt/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"
(let [attributes #{:x :y :z}
shape-applied-x {:id "shape-applied-x"
:applied-tokens {:x "1"}}
shape-applied-y {:id "shape-applied-y"
:applied-tokens {:y "1"}}
shape-applied-x-y {:id "shape-applied-x-y"
:applied-tokens {:x "1" :y "1"}}
shape-applied-none {:id "shape-applied-none"
:applied-tokens {}}
shape-applied-all {:id "shape-applied-all"
:applied-tokens {:x "1" :y "1" :z "1"}}
shape-ids (fn [& xs] (into #{} (map :id xs)))
shapes [shape-applied-x
shape-applied-y
shape-applied-x-y
shape-applied-all
shape-applied-none]
expected (wtt/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)))
(t/is (= (:y expected) (shape-ids shape-applied-y
shape-applied-x-y
shape-applied-all)))
(t/is (= (:z expected) (shape-ids shape-applied-all)))
(t/is (true? (wtt/shapes-applied-all? expected (shape-ids shape-applied-all) attributes)))
(t/is (false? (wtt/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? (wtt/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? (wtt/shapes-token-applied? {:name "a"} [{:applied-tokens {:x "b"}}
{:applied-tokens {:x "b"}}]
#{:x})))
(t/is (nil? (wtt/shapes-token-applied? {:name "a"} [{:applied-tokens {:x "a"}}
{:applied-tokens {:x "a"}}]
#{:y})))))
(t/deftest name->path-test
(t/is (= ["foo" "bar" "baz"] (wtt/token-name->path "foo.bar.baz")))
(t/is (= ["foo" "bar" "baz"] (wtt/token-name->path "foo..bar.baz")))
(t/is (= ["foo" "bar" "baz"] (wtt/token-name->path "foo..bar.baz...."))))
(t/deftest token-name-path-exists?-test
(t/is (true? (wtt/token-name-path-exists? "border-radius" {"border-radius" {"sm" {:name "sm"}}})))
(t/is (true? (wtt/token-name-path-exists? "border-radius" {"border-radius" {:name "sm"}})))
(t/is (true? (wtt/token-name-path-exists? "border-radius.sm" {"border-radius" {:name "sm"}})))
(t/is (true? (wtt/token-name-path-exists? "border-radius.sm.x" {"border-radius" {:name "sm"}})))
(t/is (false? (wtt/token-name-path-exists? "other" {"border-radius" {:name "sm"}})))
(t/is (false? (wtt/token-name-path-exists? "dark.border-radius.md" {"dark" {"border-radius" {"sm" {:name "sm"}}}}))))

View file

@ -1613,6 +1613,10 @@ msgstr "Canva"
msgid "labels.close"
msgstr "Close"
#: src/app/main/ui/workspace/tokens/sets.cljs
msgid "labels.collapse"
msgstr "Collapse"
#: src/app/main/ui/dashboard/comments.cljs:104, src/app/main/ui/viewer/comments.cljs:70, src/app/main/ui/workspace/comments.cljs:126
msgid "labels.comments"
msgstr "Comments"
@ -2081,6 +2085,14 @@ msgstr "Team Leader"
msgid "labels.team-member"
msgstr "Team member"
#: src/app/main/ui/workspace/tokens/sidebar.cljs
msgid "labels.themes"
msgstr "Themes"
#: src/app/main/ui/workspace/tokens/sidebar.cljs
msgid "labels.sets"
msgstr "Sets"
#: src/app/main/ui/dashboard/sidebar.cljs:992, src/app/main/ui/workspace/main_menu.cljs:118
msgid "labels.tutorials"
msgstr "Tutorials"
@ -6359,6 +6371,114 @@ msgstr "Update"
msgid "workspace.viewport.click-to-close-path"
msgstr "Click to close the path"
#: src/app/main/ui/workspace/tokens/form.cljs
msgid "workspace.token.create-token"
msgstr "Create new %s token"
#: src/app/main/ui/workspace/tokens/form.cljs
msgid "workspace.token.edit-token"
msgstr "Edit token"
#: src/app/main/ui/workspace/tokens/form.cljs
msgid "workspace.token.resolved-value"
msgstr "Resolved value: "
#: src/app/main/ui/workspace/tokens/sidebar.cljs
msgid "workspace.token.original-value"
msgstr "Original value: "
#: src/app/main/ui/workspace/tokens/sidebar.cljs
msgid "workspace.token.no-themes"
msgstr "There are no themes."
#: src/app/main/ui/workspace/tokens/sidebar.cljs
msgid "workspace.token.create-one"
msgstr "Create one."
#: src/app/main/ui/workspace/tokens/sidebar.cljs
msgid "workspace.token.add set"
msgstr "Add set"
#: src/app/main/ui/workspace/tokens/modals/themes.cljs
msgid "workspace.token.save-theme"
msgstr "Save theme"
#: src/app/main/ui/workspace/tokens/modals/themes.cljs
msgid "workspace.token.create-theme-title"
msgstr "Create theme"
#: src/app/main/ui/workspace/tokens/modals/themes.cljs
msgid "workspace.token.edit-theme-title"
msgstr "Edit theme"
#: src/app/main/ui/workspace/tokens/modals/themes.cljs
msgid "workspace.token.delete-theme-title"
msgstr "Delete theme"
#: src/app/main/ui/workspace/tokens/modals/themes.cljs
msgid "workspace.token.no-themes-currently"
msgstr "You currently have no themes."
#: src/app/main/ui/workspace/tokens/modals/themes.cljs
msgid "workspace.token.create-new-theme"
msgstr "Create your first theme now."
#: src/app/main/ui/workspace/tokens/modals/themes.cljs
msgid "workspace.token.new-theme"
msgstr "New theme"
#: src/app/main/ui/workspace/tokens/modals/themes.cljs
msgid "workspace.token.themes"
msgstr "Themes"
#: src/app/main/ui/workspace/tokens/modals/themes.cljs
msgid "workspace.token.theme-name"
msgstr "Theme %s"
#: src/app/main/ui/workspace/tokens/modals/themes.cljs
msgid "workspace.token.no-sets"
msgstr "No sets"
#: src/app/main/ui/workspace/tokens/modals/themes.cljs
msgid "workspace.token.num-sets"
msgstr "%s sets"
#: src/app/main/ui/workspace/tokens/modals/themes.cljs
msgid "workspace.token.back-to-themes"
msgstr "Back to theme list"
#: src/app/main/ui/workspace/tokens/theme_select.cljs
msgid "workspace.token.edit-themes"
msgstr "Edit themes"
#: src/app/main/ui/workspace/tokens/theme_select.cljs
msgid "workspace.token.no-active-theme"
msgstr "No theme active"
#: src/app/main/ui/workspace/tokens/theme_select.cljs
msgid "workspace.token.active-themes"
msgstr "%s active themes"
#: src/app/main/ui/workspace/tokens/sets.cljs
msgid "workspace.token.grouping-set-alert"
msgstr "Token Set grouping is not supported yet."
#: src/app/main/ui/workspace/tokens/sets.cljs
msgid "workspace.token.select-set"
msgstr "Select set."
#: src/app/main/ui/workspace/tokens/sets.cljs
msgid "workspace.token.set-selection-theme"
msgstr "Define what token sets should be used as part of this theme option:"
#: src/app/main/ui/workspace/tokens/sets.cljs
msgid "workspace.token.no-sets-yet"
msgstr "There are no sets yet."
#: src/app/main/ui/workspace/tokens/sets.cljs
msgid "workspace.token.no-sets-create"
msgstr "There are no sets defined yet. Create one first."
msgid "workspace.versions.button.save"
msgstr "Save version"

View file

@ -1613,6 +1613,10 @@ msgstr "Canva"
msgid "labels.close"
msgstr "Cerrar"
#: src/app/main/ui/workspace/tokens/sets.cljs
msgid "labels.collapse"
msgstr "Colapsar"
#: src/app/main/ui/dashboard/comments.cljs:104, src/app/main/ui/viewer/comments.cljs:70, src/app/main/ui/workspace/comments.cljs:126
msgid "labels.comments"
msgstr "Comentarios"
@ -2073,6 +2077,14 @@ msgstr "Líder de equipo"
msgid "labels.team-member"
msgstr "Miembro de equipo"
#: src/app/main/ui/workspace/tokens/sidebar.cljs
msgid "labels.themes"
msgstr "Temas"
#: src/app/main/ui/workspace/tokens/sidebar.cljs
msgid "labels.sets"
msgstr "Sets"
#: src/app/main/ui/dashboard/sidebar.cljs:992, src/app/main/ui/workspace/main_menu.cljs:118
msgid "labels.tutorials"
msgstr "Tutoriales"
@ -6341,6 +6353,118 @@ msgstr "Pulsar para cerrar la ruta"
msgid "errors.maximum-invitations-by-request-reached"
msgstr "Se ha alcanzado el número máximo (%s) de correos electrónicos que se pueden invitar en una sola solicitud"
#: src/app/main/ui/workspace/tokens/form.cljs
msgid "workspace.token.create-token"
msgstr "Crear un token de %s"
#: src/app/main/ui/workspace/tokens/form.cljs
msgid "workspace.token.edit-token"
msgstr "Editar token"
#: src/app/main/ui/workspace/tokens/form.cljs
msgid "workspace.token.resolved-value"
msgstr "Valor resuelto: "
#: src/app/main/ui/workspace/tokens/sidebar.cljs
msgid "workspace.token.original-value"
msgstr "Valor original: "
#: src/app/main/ui/workspace/tokens/sidebar.cljs
msgid "workspace.token.no-themes"
msgstr "No hay temas."
#: src/app/main/ui/workspace/tokens/sidebar.cljs
msgid "workspace.token.create-one"
msgstr "Crear uno."
#: src/app/main/ui/workspace/tokens/sidebar.cljs
msgid "workspace.token.add set"
msgstr "Añadir set"
#: src/app/main/ui/workspace/tokens/modals/themes.cljs
msgid "workspace.token.save-theme"
msgstr "Guardar tema"
#: src/app/main/ui/workspace/tokens/modals/themes.cljs
msgid "workspace.token.create-theme-title"
msgstr "Crear tema"
#: src/app/main/ui/workspace/tokens/modals/themes.cljs
msgid "workspace.token.edit-theme-title"
msgstr "Editar tema"
#: src/app/main/ui/workspace/tokens/modals/themes.cljs
msgid "workspace.token.delete-theme-title"
msgstr "Borrar theme"
#: src/app/main/ui/workspace/tokens/modals/themes.cljs
msgid "workspace.token.no-themes-currently"
msgstr "Actualmente no existen temas."
#: src/app/main/ui/workspace/tokens/modals/themes.cljs
msgid "workspace.token.create-new-theme"
msgstr "Crea un nuevo tema ahora."
#: src/app/main/ui/workspace/tokens/modals/themes.cljs
msgid "workspace.token.new-theme"
msgstr "Nuevo tema"
#: src/app/main/ui/workspace/tokens/modals/themes.cljs
msgid "workspace.token.themes"
msgstr "Temas"
#: src/app/main/ui/workspace/tokens/modals/themes.cljs
msgid "workspace.token.theme-name"
msgstr "Tema %s"
#: src/app/main/ui/workspace/tokens/modals/themes.cljs
msgid "workspace.token.no-sets"
msgstr "No hay sets"
#: src/app/main/ui/workspace/tokens/modals/themes.cljs
msgid "workspace.token.num-sets"
msgstr "%s sets"
#: src/app/main/ui/workspace/tokens/modals/themes.cljs
msgid "workspace.token.back-to-themes"
msgstr "Volver al listado de temas"
#: src/app/main/ui/workspace/tokens/theme_select.cljs
msgid "workspace.token.edit-themes"
msgstr "Editar temas"
#: src/app/main/ui/workspace/tokens/theme_select.cljs
msgid "workspace.token.no-active-theme"
msgstr "No hay temas activos"
#: src/app/main/ui/workspace/tokens/theme_select.cljs
msgid "workspace.token.active-themes"
msgstr "%s temas activos"
#: src/app/main/ui/workspace/tokens/sets.cljs
msgid "workspace.token.grouping-set-alert"
msgstr "La agrupación de sets aun no está soportada."
#: src/app/main/ui/workspace/tokens/sets.cljs
msgid "workspace.token.select-set"
msgstr "Selecciona set"
#: src/app/main/ui/workspace/tokens/sets.cljs
msgid "workspace.token.set-selection-theme"
msgstr "Define que sets de tokens deberian formar parte de este tema:"
#: src/app/main/ui/workspace/tokens/sets.cljs
msgid "workspace.token.no-sets"
msgstr "Aun no hay sets."
#: src/app/main/ui/workspace/tokens/sets.cljs
msgid "workspace.create-one"
msgstr "Crea uno."
#: src/app/main/ui/workspace/tokens/sets.cljs
msgid "workspace.token.no-sets-create"
msgstr "Aun no hay sets definidos. Crea uno primero"
msgid "workspace.versions.button.save"
msgstr "Guardar versión"

View file

@ -209,6 +209,46 @@ __metadata:
languageName: node
linkType: hard
"@bundled-es-modules/deepmerge@npm:^4.3.1":
version: 4.3.1
resolution: "@bundled-es-modules/deepmerge@npm:4.3.1"
dependencies:
deepmerge: "npm:^4.3.1"
checksum: 10c0/50493fb741d588aa358edc5e844cbf31493cb64aca0a5ca0d33d73f61eb9eb853f7038074429343afbe199e614a6be8400abfd31909f9e5f14a53a4cff39b894
languageName: node
linkType: hard
"@bundled-es-modules/glob@npm:^10.4.2":
version: 10.4.2
resolution: "@bundled-es-modules/glob@npm:10.4.2"
dependencies:
buffer: "npm:^6.0.3"
events: "npm:^3.3.0"
glob: "npm:^10.4.2"
patch-package: "npm:^8.0.0"
path: "npm:^0.12.7"
stream: "npm:^0.0.3"
string_decoder: "npm:^1.3.0"
url: "npm:^0.11.3"
checksum: 10c0/0c61907efb170750c69c7a6953d613bcbffdefca5ced668c0579baf46e28232793fb6e2ac3b736dd937f750572ef5a17483c417060df43e4be30dc4c8567aaba
languageName: node
linkType: hard
"@bundled-es-modules/memfs@npm:^4.9.4":
version: 4.9.4
resolution: "@bundled-es-modules/memfs@npm:4.9.4"
dependencies:
assert: "npm:^2.0.0"
buffer: "npm:^6.0.3"
events: "npm:^3.3.0"
memfs: "npm:^4.9.3"
path: "npm:^0.12.7"
stream: "npm:^0.0.3"
util: "npm:^0.12.5"
checksum: 10c0/e3548c14379183fb74aa9a94407c1cdb8587320216fb557c0af7277d2dccf23f10a2edf8726e99f878758730c0c8d71524f77e19b26660a067b01d9afa07c891
languageName: node
linkType: hard
"@colors/colors@npm:1.6.0, @colors/colors@npm:^1.6.0":
version: 1.6.0
resolution: "@colors/colors@npm:1.6.0"
@ -653,6 +693,38 @@ __metadata:
languageName: node
linkType: hard
"@jsonjoy.com/base64@npm:^1.1.1":
version: 1.1.2
resolution: "@jsonjoy.com/base64@npm:1.1.2"
peerDependencies:
tslib: 2
checksum: 10c0/88717945f66dc89bf58ce75624c99fe6a5c9a0c8614e26d03e406447b28abff80c69fb37dabe5aafef1862cf315071ae66e5c85f6018b437d95f8d13d235e6eb
languageName: node
linkType: hard
"@jsonjoy.com/json-pack@npm:^1.0.3":
version: 1.1.0
resolution: "@jsonjoy.com/json-pack@npm:1.1.0"
dependencies:
"@jsonjoy.com/base64": "npm:^1.1.1"
"@jsonjoy.com/util": "npm:^1.1.2"
hyperdyperid: "npm:^1.2.0"
thingies: "npm:^1.20.0"
peerDependencies:
tslib: 2
checksum: 10c0/cdf5cb567a7f2e703d4966a3e3a5f7f7b54ee40a2102aa0ede5c79bcf2060c8465d82f39de8583db4cf1d8415bec8e57dfb1156ef663567b846cdea45813d9d1
languageName: node
linkType: hard
"@jsonjoy.com/util@npm:^1.1.2, @jsonjoy.com/util@npm:^1.3.0":
version: 1.5.0
resolution: "@jsonjoy.com/util@npm:1.5.0"
peerDependencies:
tslib: 2
checksum: 10c0/0065ae12c4108d8aede01a479c8d2b5a39bce99e9a449d235befc753f57e8385d9c1115720529f26597840b7398d512898155423d9859fd638319fb0c827365d
languageName: node
linkType: hard
"@mdx-js/react@npm:^3.0.0":
version: 3.1.0
resolution: "@mdx-js/react@npm:3.1.0"
@ -694,6 +766,62 @@ __metadata:
languageName: node
linkType: hard
"@oven/bun-darwin-aarch64@npm:1.1.34":
version: 1.1.34
resolution: "@oven/bun-darwin-aarch64@npm:1.1.34"
conditions: os=darwin & cpu=arm64
languageName: node
linkType: hard
"@oven/bun-darwin-x64-baseline@npm:1.1.34":
version: 1.1.34
resolution: "@oven/bun-darwin-x64-baseline@npm:1.1.34"
conditions: os=darwin & cpu=x64
languageName: node
linkType: hard
"@oven/bun-darwin-x64@npm:1.1.34":
version: 1.1.34
resolution: "@oven/bun-darwin-x64@npm:1.1.34"
conditions: os=darwin & cpu=x64
languageName: node
linkType: hard
"@oven/bun-linux-aarch64@npm:1.1.34":
version: 1.1.34
resolution: "@oven/bun-linux-aarch64@npm:1.1.34"
conditions: os=linux & cpu=arm64
languageName: node
linkType: hard
"@oven/bun-linux-x64-baseline@npm:1.1.34":
version: 1.1.34
resolution: "@oven/bun-linux-x64-baseline@npm:1.1.34"
conditions: os=linux & cpu=x64
languageName: node
linkType: hard
"@oven/bun-linux-x64@npm:1.1.34":
version: 1.1.34
resolution: "@oven/bun-linux-x64@npm:1.1.34"
conditions: os=linux & cpu=x64
languageName: node
linkType: hard
"@oven/bun-windows-x64-baseline@npm:1.1.34":
version: 1.1.34
resolution: "@oven/bun-windows-x64-baseline@npm:1.1.34"
conditions: os=win32 & cpu=x64
languageName: node
linkType: hard
"@oven/bun-windows-x64@npm:1.1.34":
version: 1.1.34
resolution: "@oven/bun-windows-x64@npm:1.1.34"
conditions: os=win32 & cpu=x64
languageName: node
linkType: hard
"@parcel/watcher-android-arm64@npm:2.5.0":
version: 2.5.0
resolution: "@parcel/watcher-android-arm64@npm:2.5.0"
@ -1508,6 +1636,30 @@ __metadata:
languageName: node
linkType: hard
"@tokens-studio/sd-transforms@npm:^0.16.1":
version: 0.16.1
resolution: "@tokens-studio/sd-transforms@npm:0.16.1"
dependencies:
"@tokens-studio/types": "npm:^0.4.0"
color2k: "npm:^2.0.1"
colorjs.io: "npm:^0.4.3"
deepmerge: "npm:^4.3.1"
expr-eval-fork: "npm:^2.0.2"
is-mergeable-object: "npm:^1.1.1"
postcss-calc-ast-parser: "npm:^0.1.4"
peerDependencies:
style-dictionary: ^4.0.0-prerelease.27
checksum: 10c0/496a22026ffa25e3f6d8438a1fb39d67383fa55c89de9ac6759e2dce10a16268f5009e4809d03ceab38597fc02025a90eb1d32083b98a9353feded83831549c9
languageName: node
linkType: hard
"@tokens-studio/types@npm:^0.4.0":
version: 0.4.0
resolution: "@tokens-studio/types@npm:0.4.0"
checksum: 10c0/0641385653c94704f63dc5e10699c49bdbb1e1d8cba54af31bf50c3be85056123109bb2fe5091b1ccebaa9eba4c4afce3148a3b850919ed67bc81e3294ae839c
languageName: node
linkType: hard
"@trysound/sax@npm:0.2.0":
version: 0.2.0
resolution: "@trysound/sax@npm:0.2.0"
@ -1712,6 +1864,20 @@ __metadata:
languageName: node
linkType: hard
"@yarnpkg/lockfile@npm:^1.1.0":
version: 1.1.0
resolution: "@yarnpkg/lockfile@npm:1.1.0"
checksum: 10c0/0bfa50a3d756623d1f3409bc23f225a1d069424dbc77c6fd2f14fb377390cd57ec703dc70286e081c564be9051ead9ba85d81d66a3e68eeb6eb506d4e0c0fbda
languageName: node
linkType: hard
"@zip.js/zip.js@npm:^2.7.44":
version: 2.7.53
resolution: "@zip.js/zip.js@npm:2.7.53"
checksum: 10c0/883527bf09ce7c312117536c79d5f07e736d87de802a6c19e39ba2e18027499dcb9359df94dfde13c9bcf6118a20b4f26a40f9892ee82d7cac3124d6986b15c8
languageName: node
linkType: hard
"abbrev@npm:^2.0.0":
version: 2.0.0
resolution: "abbrev@npm:2.0.0"
@ -1938,6 +2104,19 @@ __metadata:
languageName: node
linkType: hard
"assert@npm:^2.0.0":
version: 2.1.0
resolution: "assert@npm:2.1.0"
dependencies:
call-bind: "npm:^1.0.2"
is-nan: "npm:^1.3.2"
object-is: "npm:^1.1.5"
object.assign: "npm:^4.1.4"
util: "npm:^0.12.5"
checksum: 10c0/7271a5da883c256a1fa690677bf1dd9d6aa882139f2bed1cd15da4f9e7459683e1da8e32a203d6cc6767e5e0f730c77a9532a87b896b4b0af0dd535f668775f0
languageName: node
linkType: hard
"assertion-error@npm:^2.0.1":
version: 2.0.1
resolution: "assertion-error@npm:2.0.1"
@ -1975,6 +2154,13 @@ __metadata:
languageName: node
linkType: hard
"at-least-node@npm:^1.0.0":
version: 1.0.0
resolution: "at-least-node@npm:1.0.0"
checksum: 10c0/4c058baf6df1bc5a1697cf182e2029c58cd99975288a13f9e70068ef5d6f4e1f1fd7c4d2c3c4912eae44797d1725be9700995736deca441b39f3e66d8dee97ef
languageName: node
linkType: hard
"atob@npm:^2.1.2":
version: 2.1.2
resolution: "atob@npm:2.1.2"
@ -2289,6 +2475,43 @@ __metadata:
languageName: node
linkType: hard
"bun@npm:^1.1.25":
version: 1.1.34
resolution: "bun@npm:1.1.34"
dependencies:
"@oven/bun-darwin-aarch64": "npm:1.1.34"
"@oven/bun-darwin-x64": "npm:1.1.34"
"@oven/bun-darwin-x64-baseline": "npm:1.1.34"
"@oven/bun-linux-aarch64": "npm:1.1.34"
"@oven/bun-linux-x64": "npm:1.1.34"
"@oven/bun-linux-x64-baseline": "npm:1.1.34"
"@oven/bun-windows-x64": "npm:1.1.34"
"@oven/bun-windows-x64-baseline": "npm:1.1.34"
dependenciesMeta:
"@oven/bun-darwin-aarch64":
optional: true
"@oven/bun-darwin-x64":
optional: true
"@oven/bun-darwin-x64-baseline":
optional: true
"@oven/bun-linux-aarch64":
optional: true
"@oven/bun-linux-x64":
optional: true
"@oven/bun-linux-x64-baseline":
optional: true
"@oven/bun-windows-x64":
optional: true
"@oven/bun-windows-x64-baseline":
optional: true
bin:
bun: bin/bun.exe
bunx: bin/bun.exe
checksum: 10c0/d7a69a3e6a7545d7c76edaf86633f23f791641732fb0f5a6378f1503d267d03a3353afcc01e735acb6981b12acc83827d73bca701f8e3f62183bb00ad7e22e9d
conditions: (os=darwin | os=linux | os=win32) & (cpu=arm64 | cpu=x64)
languageName: node
linkType: hard
"bytes@npm:3.1.2, bytes@npm:^3.0.0":
version: 3.1.2
resolution: "bytes@npm:3.1.2"
@ -2323,7 +2546,7 @@ __metadata:
languageName: node
linkType: hard
"call-bind@npm:^1.0.2, call-bind@npm:^1.0.5, call-bind@npm:^1.0.6, call-bind@npm:^1.0.7":
"call-bind@npm:^1.0.0, call-bind@npm:^1.0.2, call-bind@npm:^1.0.5, call-bind@npm:^1.0.6, call-bind@npm:^1.0.7":
version: 1.0.7
resolution: "call-bind@npm:1.0.7"
dependencies:
@ -2377,6 +2600,20 @@ __metadata:
languageName: node
linkType: hard
"chalk@npm:^5.3.0":
version: 5.3.0
resolution: "chalk@npm:5.3.0"
checksum: 10c0/8297d436b2c0f95801103ff2ef67268d362021b8210daf8ddbe349695333eb3610a71122172ff3b0272f1ef2cf7cc2c41fdaa4715f52e49ffe04c56340feed09
languageName: node
linkType: hard
"change-case@npm:^5.3.0":
version: 5.4.4
resolution: "change-case@npm:5.4.4"
checksum: 10c0/2a9c2b9c9ad6ab2491105aaf506db1a9acaf543a18967798dcce20926c6a173aa63266cb6189f3086e3c14bf7ae1f8ea4f96ecc466fcd582310efa00372f3734
languageName: node
linkType: hard
"check-error@npm:^2.1.1":
version: 2.1.1
resolution: "check-error@npm:2.1.1"
@ -2419,6 +2656,13 @@ __metadata:
languageName: node
linkType: hard
"ci-info@npm:^3.7.0":
version: 3.9.0
resolution: "ci-info@npm:3.9.0"
checksum: 10c0/6f0109e36e111684291d46123d491bc4e7b7a1934c3a20dea28cba89f1d4a03acd892f5f6a81ed3855c38647e285a150e3c9ba062e38943bef57fee6c1554c3a
languageName: node
linkType: hard
"cipher-base@npm:^1.0.0, cipher-base@npm:^1.0.1, cipher-base@npm:^1.0.3":
version: 1.0.4
resolution: "cipher-base@npm:1.0.4"
@ -2546,6 +2790,13 @@ __metadata:
languageName: node
linkType: hard
"color2k@npm:^2.0.1":
version: 2.0.3
resolution: "color2k@npm:2.0.3"
checksum: 10c0/e7c13d212c9d1abb1690e378bbc0a6fb1751e4b02e9a73ba3b2ade9d54da673834597d342791d577d1ce400ec486c7f92c5098f9fa85cd113bcfde57420a2bb9
languageName: node
linkType: hard
"color@npm:^3.1.3":
version: 3.2.1
resolution: "color@npm:3.2.1"
@ -2556,6 +2807,13 @@ __metadata:
languageName: node
linkType: hard
"colorjs.io@npm:^0.4.3":
version: 0.4.5
resolution: "colorjs.io@npm:0.4.5"
checksum: 10c0/4cc58d18223426bcb8caa558e7554002b62bf87bd20db06596abf5efe5ea65416266402db86b504ac5fa2c38360913dbb8e6ef7c4fa19a992fd1818d5710ef6f
languageName: node
linkType: hard
"colorjs.io@npm:^0.5.0":
version: 0.5.2
resolution: "colorjs.io@npm:0.5.2"
@ -2596,6 +2854,20 @@ __metadata:
languageName: node
linkType: hard
"commander@npm:^8.3.0":
version: 8.3.0
resolution: "commander@npm:8.3.0"
checksum: 10c0/8b043bb8322ea1c39664a1598a95e0495bfe4ca2fad0d84a92d7d1d8d213e2a155b441d2470c8e08de7c4a28cf2bc6e169211c49e1b21d9f7edc6ae4d9356060
languageName: node
linkType: hard
"component-emitter@npm:^2.0.0":
version: 2.0.0
resolution: "component-emitter@npm:2.0.0"
checksum: 10c0/65dfaf787ea49eb48f0ffec766bda7ec67e8dbeb3b406f08724dcae842e0aa274731fcccb9280b77d2b41693061731a9080b60d276020246a146544cd9900b83
languageName: node
linkType: hard
"compressible@npm:~2.0.18":
version: 2.0.18
resolution: "compressible@npm:2.0.18"
@ -2795,7 +3067,7 @@ __metadata:
languageName: node
linkType: hard
"cross-spawn@npm:^7.0.0":
"cross-spawn@npm:^7.0.0, cross-spawn@npm:^7.0.3":
version: 7.0.3
resolution: "cross-spawn@npm:7.0.3"
dependencies:
@ -3079,6 +3351,13 @@ __metadata:
languageName: node
linkType: hard
"deepmerge@npm:^4.3.1":
version: 4.3.1
resolution: "deepmerge@npm:4.3.1"
checksum: 10c0/e53481aaf1aa2c4082b5342be6b6d8ad9dfe387bc92ce197a66dea08bd4265904a087e75e464f14d1347cf2ac8afe1e4c16b266e0561cc5df29382d3c5f80044
languageName: node
linkType: hard
"define-data-property@npm:^1.0.1, define-data-property@npm:^1.1.4":
version: 1.1.4
resolution: "define-data-property@npm:1.1.4"
@ -3097,7 +3376,7 @@ __metadata:
languageName: node
linkType: hard
"define-properties@npm:^1.2.0, define-properties@npm:^1.2.1":
"define-properties@npm:^1.1.3, define-properties@npm:^1.2.0, define-properties@npm:^1.2.1":
version: 1.2.1
resolution: "define-properties@npm:1.2.1"
dependencies:
@ -3883,6 +4162,13 @@ __metadata:
languageName: node
linkType: hard
"expr-eval-fork@npm:^2.0.2":
version: 2.0.2
resolution: "expr-eval-fork@npm:2.0.2"
checksum: 10c0/ab5143fe65017d8811c155be55abd700321b8a32117635c35ce1309488f3263a251788f27f2e4a77425f58f7a64f99fd46d652c35a8c1668b22b4a8861702b75
languageName: node
linkType: hard
"express@npm:^4.21.1":
version: 4.21.1
resolution: "express@npm:4.21.1"
@ -4025,6 +4311,15 @@ __metadata:
languageName: node
linkType: hard
"find-yarn-workspace-root@npm:^2.0.0":
version: 2.0.0
resolution: "find-yarn-workspace-root@npm:2.0.0"
dependencies:
micromatch: "npm:^4.0.2"
checksum: 10c0/b0d3843013fbdaf4e57140e0165889d09fa61745c9e85da2af86e54974f4cc9f1967e40f0d8fc36a79d53091f0829c651d06607d552582e53976f3cd8f4e5689
languageName: node
linkType: hard
"fn.name@npm:1.x.x":
version: 1.1.0
resolution: "fn.name@npm:1.1.0"
@ -4108,8 +4403,10 @@ __metadata:
"@storybook/blocks": "npm:^8.3.6"
"@storybook/react": "npm:^8.3.6"
"@storybook/react-vite": "npm:^8.3.6"
"@tokens-studio/sd-transforms": "npm:^0.16.1"
"@types/node": "npm:^22.7.7"
autoprefixer: "npm:^10.4.20"
bun: "npm:^1.1.25"
compression: "npm:^1.7.4"
concurrently: "npm:^9.0.1"
date-fns: "npm:^4.1.0"
@ -4131,6 +4428,7 @@ __metadata:
jsdom: "npm:^25.0.1"
jszip: "npm:^3.10.1"
lodash: "npm:^4.17.21"
lodash.debounce: "npm:^4.0.8"
luxon: "npm:^3.5.0"
map-stream: "npm:0.0.7"
marked: "npm:^14.1.3"
@ -4159,8 +4457,10 @@ __metadata:
shadow-cljs: "npm:2.28.18"
source-map-support: "npm:^0.5.21"
storybook: "npm:^8.3.6"
style-dictionary: "npm:^4.1.4"
svg-sprite: "npm:^2.0.4"
tdigest: "npm:^0.1.2"
tinycolor2: "npm:^1.6.0"
typescript: "npm:^5.6.3"
ua-parser-js: "npm:2.0.0-rc.1"
vite: "npm:^5.4.9"
@ -4172,6 +4472,18 @@ __metadata:
languageName: unknown
linkType: soft
"fs-extra@npm:^9.0.0":
version: 9.1.0
resolution: "fs-extra@npm:9.1.0"
dependencies:
at-least-node: "npm:^1.0.0"
graceful-fs: "npm:^4.2.0"
jsonfile: "npm:^6.0.1"
universalify: "npm:^2.0.0"
checksum: 10c0/9b808bd884beff5cb940773018179a6b94a966381d005479f00adda6b44e5e3d4abf765135773d849cc27efe68c349e4a7b86acd7d3306d5932c14f3a4b17a92
languageName: node
linkType: hard
"fs-minipass@npm:^2.0.0":
version: 2.1.0
resolution: "fs-minipass@npm:2.1.0"
@ -4347,7 +4659,7 @@ __metadata:
languageName: node
linkType: hard
"glob@npm:^10.2.2, glob@npm:^10.3.10, glob@npm:^10.3.3":
"glob@npm:^10.2.2, glob@npm:^10.3.10, glob@npm:^10.3.3, glob@npm:^10.4.2":
version: 10.4.5
resolution: "glob@npm:10.4.5"
dependencies:
@ -4419,7 +4731,7 @@ __metadata:
languageName: node
linkType: hard
"graceful-fs@npm:^4.0.0, graceful-fs@npm:^4.1.2, graceful-fs@npm:^4.2.6":
"graceful-fs@npm:^4.0.0, graceful-fs@npm:^4.1.11, graceful-fs@npm:^4.1.2, graceful-fs@npm:^4.1.6, graceful-fs@npm:^4.2.0, graceful-fs@npm:^4.2.6":
version: 4.2.11
resolution: "graceful-fs@npm:4.2.11"
checksum: 10c0/386d011a553e02bc594ac2ca0bd6d9e4c22d7fa8cfbfc448a6d148c59ea881b092db9dbe3547ae4b88e55f1b01f7c4a2ecc53b310c042793e63aa44cf6c257f2
@ -4702,6 +5014,13 @@ __metadata:
languageName: node
linkType: hard
"hyperdyperid@npm:^1.2.0":
version: 1.2.0
resolution: "hyperdyperid@npm:1.2.0"
checksum: 10c0/885ba3177c7181d315a856ee9c0005ff8eb5dcb1ce9e9d61be70987895d934d84686c37c981cceeb53216d4c9c15c1cc25f1804e84cc6a74a16993c5d7fd0893
languageName: node
linkType: hard
"iconv-lite@npm:0.4.24":
version: 0.4.24
resolution: "iconv-lite@npm:0.4.24"
@ -4990,6 +5309,23 @@ __metadata:
languageName: node
linkType: hard
"is-mergeable-object@npm:^1.1.1":
version: 1.1.1
resolution: "is-mergeable-object@npm:1.1.1"
checksum: 10c0/ed895a17686eb88d28040e0281c507639e5a07e63ac51f033c34091c2d8679ca86775ecfe80d5f0636bc2b7c530acd731527e5a2e9c32a88f8847286451720f1
languageName: node
linkType: hard
"is-nan@npm:^1.3.2":
version: 1.3.2
resolution: "is-nan@npm:1.3.2"
dependencies:
call-bind: "npm:^1.0.0"
define-properties: "npm:^1.1.3"
checksum: 10c0/8bfb286f85763f9c2e28ea32e9127702fe980ffd15fa5d63ade3be7786559e6e21355d3625dd364c769c033c5aedf0a2ed3d4025d336abf1b9241e3d9eddc5b0
languageName: node
linkType: hard
"is-negative-zero@npm:^2.0.3":
version: 2.0.3
resolution: "is-negative-zero@npm:2.0.3"
@ -5013,6 +5349,13 @@ __metadata:
languageName: node
linkType: hard
"is-plain-obj@npm:^4.1.0":
version: 4.1.0
resolution: "is-plain-obj@npm:4.1.0"
checksum: 10c0/32130d651d71d9564dc88ba7e6fda0e91a1010a3694648e9f4f47bb6080438140696d3e3e15c741411d712e47ac9edc1a8a9de1fe76f3487b0d90be06ac9975e
languageName: node
linkType: hard
"is-plain-object@npm:^2.0.4":
version: 2.0.4
resolution: "is-plain-object@npm:2.0.4"
@ -5098,7 +5441,7 @@ __metadata:
languageName: node
linkType: hard
"is-wsl@npm:^2.2.0":
"is-wsl@npm:^2.1.1, is-wsl@npm:^2.2.0":
version: 2.2.0
resolution: "is-wsl@npm:2.2.0"
dependencies:
@ -5270,6 +5613,18 @@ __metadata:
languageName: node
linkType: hard
"json-stable-stringify@npm:^1.0.2":
version: 1.1.1
resolution: "json-stable-stringify@npm:1.1.1"
dependencies:
call-bind: "npm:^1.0.5"
isarray: "npm:^2.0.5"
jsonify: "npm:^0.0.1"
object-keys: "npm:^1.1.1"
checksum: 10c0/3801e3eeccbd030afb970f54bea690a079cfea7d9ed206a1b17ca9367f4b7772c764bf77a48f03e56b50e5f7ee7d11c52339fe20d8d7ccead003e4ca69e4cfde
languageName: node
linkType: hard
"json5@npm:^2.2.2, json5@npm:^2.2.3":
version: 2.2.3
resolution: "json5@npm:2.2.3"
@ -5279,6 +5634,26 @@ __metadata:
languageName: node
linkType: hard
"jsonfile@npm:^6.0.1":
version: 6.1.0
resolution: "jsonfile@npm:6.1.0"
dependencies:
graceful-fs: "npm:^4.1.6"
universalify: "npm:^2.0.0"
dependenciesMeta:
graceful-fs:
optional: true
checksum: 10c0/4f95b5e8a5622b1e9e8f33c96b7ef3158122f595998114d1e7f03985649ea99cb3cd99ce1ed1831ae94c8c8543ab45ebd044207612f31a56fd08462140e46865
languageName: node
linkType: hard
"jsonify@npm:^0.0.1":
version: 0.0.1
resolution: "jsonify@npm:0.0.1"
checksum: 10c0/7f5499cdd59a0967ed35bda48b7cec43d850bbc8fb955cdd3a1717bb0efadbe300724d5646de765bb7a99fc1c3ab06eb80d93503c6faaf99b4ff50a3326692f6
languageName: node
linkType: hard
"jszip@npm:^3.10.1":
version: 3.10.1
resolution: "jszip@npm:3.10.1"
@ -5291,6 +5666,15 @@ __metadata:
languageName: node
linkType: hard
"klaw-sync@npm:^6.0.0":
version: 6.0.0
resolution: "klaw-sync@npm:6.0.0"
dependencies:
graceful-fs: "npm:^4.1.11"
checksum: 10c0/00d8e4c48d0d699b743b3b028e807295ea0b225caf6179f51029e19783a93ad8bb9bccde617d169659fbe99559d73fb35f796214de031d0023c26b906cecd70a
languageName: node
linkType: hard
"kuler@npm:^2.0.0":
version: 2.0.0
resolution: "kuler@npm:2.0.0"
@ -5356,6 +5740,13 @@ __metadata:
languageName: node
linkType: hard
"lodash.debounce@npm:^4.0.8":
version: 4.0.8
resolution: "lodash.debounce@npm:4.0.8"
checksum: 10c0/762998a63e095412b6099b8290903e0a8ddcb353ac6e2e0f2d7e7d03abd4275fe3c689d88960eb90b0dde4f177554d51a690f22a343932ecbc50a5d111849987
languageName: node
linkType: hard
"lodash.escape@npm:^4.0.1":
version: 4.0.1
resolution: "lodash.escape@npm:4.0.1"
@ -5548,6 +5939,18 @@ __metadata:
languageName: node
linkType: hard
"memfs@npm:^4.9.3":
version: 4.14.0
resolution: "memfs@npm:4.14.0"
dependencies:
"@jsonjoy.com/json-pack": "npm:^1.0.3"
"@jsonjoy.com/util": "npm:^1.3.0"
tree-dump: "npm:^1.0.1"
tslib: "npm:^2.0.0"
checksum: 10c0/d1de2e4b3c269f5b5f27b63f60bb8ea9ae5800843776e0bed4548f2957dcd55237ac5eab3a5ffe0d561a6be53e42c055a7bc79efc1613563b14e14c287ef3b0a
languageName: node
linkType: hard
"memoizee@npm:0.4.X":
version: 0.4.17
resolution: "memoizee@npm:0.4.17"
@ -5594,7 +5997,7 @@ __metadata:
languageName: node
linkType: hard
"micromatch@npm:^4.0.5":
"micromatch@npm:^4.0.2, micromatch@npm:^4.0.5":
version: 4.0.8
resolution: "micromatch@npm:4.0.8"
dependencies:
@ -6072,6 +6475,16 @@ __metadata:
languageName: node
linkType: hard
"object-is@npm:^1.1.5":
version: 1.1.6
resolution: "object-is@npm:1.1.6"
dependencies:
call-bind: "npm:^1.0.7"
define-properties: "npm:^1.2.1"
checksum: 10c0/506af444c4dce7f8e31f34fc549e2fb8152d6b9c4a30c6e62852badd7f520b579c679af433e7a072f9d78eb7808d230dc12e1cf58da9154dfbf8813099ea0fe0
languageName: node
linkType: hard
"object-keys@npm:^1.1.1":
version: 1.1.1
resolution: "object-keys@npm:1.1.1"
@ -6125,6 +6538,16 @@ __metadata:
languageName: node
linkType: hard
"open@npm:^7.4.2":
version: 7.4.2
resolution: "open@npm:7.4.2"
dependencies:
is-docker: "npm:^2.0.0"
is-wsl: "npm:^2.1.1"
checksum: 10c0/77573a6a68f7364f3a19a4c80492712720746b63680ee304555112605ead196afe91052bd3c3d165efdf4e9d04d255e87de0d0a77acec11ef47fd5261251813f
languageName: node
linkType: hard
"open@npm:^8.0.4":
version: 8.4.2
resolution: "open@npm:8.4.2"
@ -6155,6 +6578,13 @@ __metadata:
languageName: node
linkType: hard
"os-tmpdir@npm:~1.0.2":
version: 1.0.2
resolution: "os-tmpdir@npm:1.0.2"
checksum: 10c0/f438450224f8e2687605a8dd318f0db694b6293c5d835ae509a69e97c8de38b6994645337e5577f5001115470414638978cc49da1cdcc25106dad8738dc69990
languageName: node
linkType: hard
"p-limit@npm:^3.0.2":
version: 3.1.0
resolution: "p-limit@npm:3.1.0"
@ -6252,6 +6682,31 @@ __metadata:
languageName: node
linkType: hard
"patch-package@npm:^8.0.0":
version: 8.0.0
resolution: "patch-package@npm:8.0.0"
dependencies:
"@yarnpkg/lockfile": "npm:^1.1.0"
chalk: "npm:^4.1.2"
ci-info: "npm:^3.7.0"
cross-spawn: "npm:^7.0.3"
find-yarn-workspace-root: "npm:^2.0.0"
fs-extra: "npm:^9.0.0"
json-stable-stringify: "npm:^1.0.2"
klaw-sync: "npm:^6.0.0"
minimist: "npm:^1.2.6"
open: "npm:^7.4.2"
rimraf: "npm:^2.6.3"
semver: "npm:^7.5.3"
slash: "npm:^2.0.0"
tmp: "npm:^0.0.33"
yaml: "npm:^2.2.2"
bin:
patch-package: index.js
checksum: 10c0/690eab0537e953a3fd7d32bb23f0e82f97cd448f8244c3227ed55933611a126f9476397325c06ad2c11d881a19b427a02bd1881bee78d89f1731373fc4fe0fee
languageName: node
linkType: hard
"path-browserify@npm:0.0.1":
version: 0.0.1
resolution: "path-browserify@npm:0.0.1"
@ -6330,6 +6785,23 @@ __metadata:
languageName: node
linkType: hard
"path-unified@npm:^0.1.0":
version: 0.1.0
resolution: "path-unified@npm:0.1.0"
checksum: 10c0/26c314221bcc0ca3ce59b67f50dffb6f37214d294fd9dfeb0219e9f12b93d8852c8525d32be9387011d902d361669a43e22ec419d522055794790222665b2de9
languageName: node
linkType: hard
"path@npm:^0.12.7":
version: 0.12.7
resolution: "path@npm:0.12.7"
dependencies:
process: "npm:^0.11.1"
util: "npm:^0.10.3"
checksum: 10c0/f795ce5438a988a590c7b6dfd450ec9baa1c391a8be4c2dea48baa6e0f5b199e56cd83b8c9ebf3991b81bea58236d2c32bdafe2c17a2e70c3a2e4c69891ade59
languageName: node
linkType: hard
"pathe@npm:^1.1.2":
version: 1.1.2
resolution: "pathe@npm:1.1.2"
@ -6462,6 +6934,15 @@ __metadata:
languageName: node
linkType: hard
"postcss-calc-ast-parser@npm:^0.1.4":
version: 0.1.4
resolution: "postcss-calc-ast-parser@npm:0.1.4"
dependencies:
postcss-value-parser: "npm:^3.3.1"
checksum: 10c0/6ab488da4024aefe749baff2ee2cd41d1a7b84611291a6fd5d220262255c86f37687b3541696cab3e4edb1b7601634719877184ee426048ad82ed15185a5f64f
languageName: node
linkType: hard
"postcss-clean@npm:^1.2.2":
version: 1.2.2
resolution: "postcss-clean@npm:1.2.2"
@ -6565,6 +7046,13 @@ __metadata:
languageName: node
linkType: hard
"postcss-value-parser@npm:^3.3.1":
version: 3.3.1
resolution: "postcss-value-parser@npm:3.3.1"
checksum: 10c0/23eed98d8eeadb1f9ef1db4a2757da0f1d8e7c1dac2a38d6b35d971aab9eb3c6d8a967d0e9f435558834ffcd966afbbe875a56bcc5bcdd09e663008c106b3e47
languageName: node
linkType: hard
"postcss-value-parser@npm:^4.1.0, postcss-value-parser@npm:^4.2.0":
version: 4.2.0
resolution: "postcss-value-parser@npm:4.2.0"
@ -6641,7 +7129,7 @@ __metadata:
languageName: node
linkType: hard
"process@npm:^0.11.10":
"process@npm:^0.11.1, process@npm:^0.11.10":
version: 0.11.10
resolution: "process@npm:0.11.10"
checksum: 10c0/40c3ce4b7e6d4b8c3355479df77aeed46f81b279818ccdc500124e6a5ab882c0cc81ff7ea16384873a95a74c4570b01b120f287abbdd4c877931460eca6084b3
@ -7063,6 +7551,17 @@ __metadata:
languageName: node
linkType: hard
"rimraf@npm:^2.6.3":
version: 2.7.1
resolution: "rimraf@npm:2.7.1"
dependencies:
glob: "npm:^7.1.3"
bin:
rimraf: ./bin.js
checksum: 10c0/4eef73d406c6940927479a3a9dee551e14a54faf54b31ef861250ac815172bade86cc6f7d64a4dc5e98b65e4b18a2e1c9ff3b68d296be0c748413f092bb0dd40
languageName: node
linkType: hard
"rimraf@npm:^3.0.2":
version: 3.0.2
resolution: "rimraf@npm:3.0.2"
@ -7717,6 +8216,13 @@ __metadata:
languageName: node
linkType: hard
"slash@npm:^2.0.0":
version: 2.0.0
resolution: "slash@npm:2.0.0"
checksum: 10c0/f83dbd3cb62c41bb8fcbbc6bf5473f3234b97fa1d008f571710a9d3757a28c7169e1811cad1554ccb1cc531460b3d221c9a7b37f549398d9a30707f0a5af9193
languageName: node
linkType: hard
"smart-buffer@npm:^4.2.0":
version: 4.2.0
resolution: "smart-buffer@npm:4.2.0"
@ -7930,6 +8436,15 @@ __metadata:
languageName: node
linkType: hard
"stream@npm:^0.0.3":
version: 0.0.3
resolution: "stream@npm:0.0.3"
dependencies:
component-emitter: "npm:^2.0.0"
checksum: 10c0/5d262408583f3d5fed8077b33ad670320d85c6b7c0fb3ab73a9a632fbad0ee36f3c66e6feb5264cb39dbee3a619174fa886b5f69f98217666d0844f6a2f6510b
languageName: node
linkType: hard
"string-hash@npm:^1.1.1":
version: 1.1.3
resolution: "string-hash@npm:1.1.3"
@ -8078,6 +8593,28 @@ __metadata:
languageName: node
linkType: hard
"style-dictionary@npm:^4.1.4":
version: 4.1.4
resolution: "style-dictionary@npm:4.1.4"
dependencies:
"@bundled-es-modules/deepmerge": "npm:^4.3.1"
"@bundled-es-modules/glob": "npm:^10.4.2"
"@bundled-es-modules/memfs": "npm:^4.9.4"
"@zip.js/zip.js": "npm:^2.7.44"
chalk: "npm:^5.3.0"
change-case: "npm:^5.3.0"
commander: "npm:^8.3.0"
is-plain-obj: "npm:^4.1.0"
json5: "npm:^2.2.2"
patch-package: "npm:^8.0.0"
path-unified: "npm:^0.1.0"
tinycolor2: "npm:^1.6.0"
bin:
style-dictionary: bin/style-dictionary.js
checksum: 10c0/b88e2f94615bc851e2e797e685863911dbb875d312bf1571a3be6b6a9dde7e0b324d83495f153446eceefe93ec119c80e2ca032a600818dcecc72174d285e429
languageName: node
linkType: hard
"supports-color@npm:^5.3.0, supports-color@npm:^5.4.0, supports-color@npm:^5.5.0":
version: 5.5.0
resolution: "supports-color@npm:5.5.0"
@ -8193,6 +8730,15 @@ __metadata:
languageName: node
linkType: hard
"thingies@npm:^1.20.0":
version: 1.21.0
resolution: "thingies@npm:1.21.0"
peerDependencies:
tslib: ^2
checksum: 10c0/7570ee855aecb73185a672ecf3eb1c287a6512bf5476449388433b2d4debcf78100bc8bfd439b0edd38d2bc3bfb8341de5ce85b8557dec66d0f27b962c9a8bc1
languageName: node
linkType: hard
"through2@npm:^2.0.0, through2@npm:^2.0.3":
version: 2.0.5
resolution: "through2@npm:2.0.5"
@ -8269,6 +8815,13 @@ __metadata:
languageName: node
linkType: hard
"tinycolor2@npm:^1.6.0":
version: 1.6.0
resolution: "tinycolor2@npm:1.6.0"
checksum: 10c0/9aa79a36ba2c2a87cb221453465cabacd04b9e35f9694373e846fdc78b1c768110f81e581ea41440106c0f24d9a023891d0887e8075885e790ac40eb0e74a5c1
languageName: node
linkType: hard
"tinyexec@npm:^0.3.1":
version: 0.3.1
resolution: "tinyexec@npm:0.3.1"
@ -8315,6 +8868,15 @@ __metadata:
languageName: node
linkType: hard
"tmp@npm:^0.0.33":
version: 0.0.33
resolution: "tmp@npm:0.0.33"
dependencies:
os-tmpdir: "npm:~1.0.2"
checksum: 10c0/69863947b8c29cabad43fe0ce65cec5bb4b481d15d4b4b21e036b060b3edbf3bc7a5541de1bacb437bb3f7c4538f669752627fdf9b4aaf034cebd172ba373408
languageName: node
linkType: hard
"to-arraybuffer@npm:^1.0.0":
version: 1.0.1
resolution: "to-arraybuffer@npm:1.0.1"
@ -8372,6 +8934,15 @@ __metadata:
languageName: node
linkType: hard
"tree-dump@npm:^1.0.1":
version: 1.0.2
resolution: "tree-dump@npm:1.0.2"
peerDependencies:
tslib: 2
checksum: 10c0/d1d180764e9c691b28332dbd74226c6b6af361dfb1e134bb11e60e17cb11c215894adee50ffc578da5dcf546006693947be8b6665eb1269b56e2f534926f1c1f
languageName: node
linkType: hard
"tree-kill@npm:^1.2.2":
version: 1.2.2
resolution: "tree-kill@npm:1.2.2"
@ -8406,7 +8977,7 @@ __metadata:
languageName: node
linkType: hard
"tslib@npm:^2.0.1, tslib@npm:^2.1.0":
"tslib@npm:^2.0.0, tslib@npm:^2.0.1, tslib@npm:^2.1.0":
version: 2.8.1
resolution: "tslib@npm:2.8.1"
checksum: 10c0/9c4759110a19c53f992d9aae23aac5ced636e99887b51b9e61def52611732872ff7668757d4e4c61f19691e36f4da981cd9485e869b4a7408d689f6bf1f14e62
@ -8580,6 +9151,13 @@ __metadata:
languageName: node
linkType: hard
"universalify@npm:^2.0.0":
version: 2.0.1
resolution: "universalify@npm:2.0.1"
checksum: 10c0/73e8ee3809041ca8b818efb141801a1004e3fc0002727f1531f4de613ea281b494a40909596dae4a042a4fb6cd385af5d4db2e137b1362e0e91384b828effd3a
languageName: node
linkType: hard
"unpipe@npm:1.0.0, unpipe@npm:~1.0.0":
version: 1.0.0
resolution: "unpipe@npm:1.0.0"
@ -8616,7 +9194,7 @@ __metadata:
languageName: node
linkType: hard
"url@npm:^0.11.0":
"url@npm:^0.11.0, url@npm:^0.11.3":
version: 0.11.4
resolution: "url@npm:0.11.4"
dependencies:
@ -8633,7 +9211,7 @@ __metadata:
languageName: node
linkType: hard
"util@npm:^0.10.4":
"util@npm:^0.10.3, util@npm:^0.10.4":
version: 0.10.4
resolution: "util@npm:0.10.4"
dependencies:
@ -9154,7 +9732,7 @@ __metadata:
languageName: node
linkType: hard
"yaml@npm:^2.4.2":
"yaml@npm:^2.2.2, yaml@npm:^2.4.2":
version: 2.6.0
resolution: "yaml@npm:2.6.0"
bin: