diff --git a/common/src/app/common/files/changes.cljc b/common/src/app/common/files/changes.cljc
index 45195c48d..2adf6b559 100644
--- a/common/src/app/common/files/changes.cljc
+++ b/common/src/app/common/files/changes.cljc
@@ -415,6 +415,12 @@
[:type [:= :add-token-sets]]
[:token-sets [:sequential ::ctot/token-set]]]]
+ [:rename-token-set-group
+ [:map {:title "RenameTokenSetGroup"}
+ [:type [:= :rename-token-set-group]]
+ [:from-path-str :string]
+ [:to-path-str :string]]]
+
[:mod-token-set
[:map {:title "ModTokenSetChange"}
[:type [:= :mod-token-set]]
@@ -1063,6 +1069,13 @@
(ctob/ensure-tokens-lib)
(ctob/add-sets (map ctob/make-token-set token-sets)))))
+(defmethod process-change :rename-token-set-group
+ [data {:keys [from-path-str to-path-str]}]
+ (update data :tokens-lib (fn [lib]
+ (-> lib
+ (ctob/ensure-tokens-lib)
+ (ctob/rename-set-group from-path-str to-path-str)))))
+
(defmethod process-change :mod-token-set
[data {:keys [name token-set]}]
(update data :tokens-lib (fn [lib]
@@ -1153,7 +1166,7 @@
; We need to trigger a sync if the shape has changed any
; attribute that participates in components synchronization.
(and (= (:type operation) :set)
- (get ctk/sync-attrs (:attr operation))))
+ (ctk/component-attr? (:attr operation))))
any-sync? (some need-sync? operations)]
(when any-sync?
(if page-id
diff --git a/common/src/app/common/files/changes_builder.cljc b/common/src/app/common/files/changes_builder.cljc
index 6e6ead182..9ad9d0b28 100644
--- a/common/src/app/common/files/changes_builder.cljc
+++ b/common/src/app/common/files/changes_builder.cljc
@@ -812,6 +812,14 @@
(update :undo-changes conj {:type :del-token-set :name (:name token-set)})
(apply-changes-local)))
+(defn rename-token-set-group
+ [changes from-path-str to-path-str]
+ (-> changes
+ (update :redo-changes conj {:type :rename-token-set-group :from-path-str from-path-str :to-path-str to-path-str})
+ ;; TODO: Figure out undo
+ #_(update :undo-changes conj {:type :rename-token-set-group :name (:name token-set) :token-set (or prev-token-set token-set)})
+ (apply-changes-local)))
+
(defn update-token-set
[changes token-set prev-token-set]
(-> changes
diff --git a/common/src/app/common/logic/libraries.cljc b/common/src/app/common/logic/libraries.cljc
index bc9ab68b0..98db43816 100644
--- a/common/src/app/common/logic/libraries.cljc
+++ b/common/src/app/common/logic/libraries.cljc
@@ -27,6 +27,7 @@
[app.common.types.shape-tree :as ctst]
[app.common.types.shape.interactions :as ctsi]
[app.common.types.shape.layout :as ctl]
+ [app.common.types.token :as cto]
[app.common.types.typography :as cty]
[app.common.uuid :as uuid]
[clojure.set :as set]
@@ -1479,6 +1480,44 @@
[{:type :set-remote-synced
:remote-synced (:remote-synced shape)}]}))))))
+(defn- update-tokens
+ "Token synchronization algorithm. Copy the applied tokens that have changed
+ in the origin shape to the dest shape (applying or removing as necessary).
+
+ Only the given token attributes are synced."
+ [changes container dest-shape orig-shape token-attrs]
+ (let [orig-tokens (get orig-shape :applied-tokens {})
+ dest-tokens (get dest-shape :applied-tokens {})
+ dest-tokens' (reduce (fn [dest-tokens' token-attr]
+ (let [orig-token (get orig-tokens token-attr)
+ dest-token (get dest-tokens token-attr)]
+ (if (= orig-token dest-token)
+ dest-tokens'
+ (if (nil? orig-token)
+ (dissoc dest-tokens' token-attr)
+ (assoc dest-tokens' token-attr orig-token)))))
+ dest-tokens
+ token-attrs)]
+ (if (= dest-tokens dest-tokens')
+ changes
+ (-> changes
+ (update :redo-changes conj (make-change
+ container
+ {:type :mod-obj
+ :id (:id dest-shape)
+ :operations [{:type :set
+ :attr :applied-tokens
+ :val dest-tokens'
+ :ignore-touched true}]}))
+ (update :undo-changes conj (make-change
+ container
+ {:type :mod-obj
+ :id (:id dest-shape)
+ :operations [{:type :set
+ :attr :applied-tokens
+ :val dest-tokens
+ :ignore-touched true}]}))))))
+
(defn- update-attrs
"The main function that implements the attribute sync algorithm. Copy
attributes that have changed in the origin shape to the dest shape.
@@ -1511,37 +1550,41 @@
(loop [attrs (->> (seq (keys ctk/sync-attrs))
;; We don't update the flex-child attrs
(remove ctk/swap-keep-attrs)
-
;; We don't do automatic update of the `layout-grid-cells` property.
(remove #(= :layout-grid-cells %)))
+ applied-tokens #{}
roperations []
uoperations '()]
(let [attr (first attrs)]
(if (nil? attr)
- (if (empty? roperations)
+ (if (and (empty? roperations) (empty? applied-tokens))
changes
(let [all-parents (cfh/get-parent-ids (:objects container)
(:id dest-shape))]
- (-> changes
- (update :redo-changes conj (make-change
- container
- {:type :mod-obj
- :id (:id dest-shape)
- :operations roperations}))
- (update :redo-changes conj (make-change
- container
- {:type :reg-objects
- :shapes all-parents}))
- (update :undo-changes conj (make-change
- container
- {:type :mod-obj
- :id (:id dest-shape)
- :operations (vec uoperations)}))
- (update :undo-changes concat [(make-change
- container
- {:type :reg-objects
- :shapes all-parents})]))))
+ (cond-> changes
+ (seq roperations)
+ (-> (update :redo-changes conj (make-change
+ container
+ {:type :mod-obj
+ :id (:id dest-shape)
+ :operations roperations}))
+ (update :redo-changes conj (make-change
+ container
+ {:type :reg-objects
+ :shapes all-parents}))
+ (update :undo-changes conj (make-change
+ container
+ {:type :mod-obj
+ :id (:id dest-shape)
+ :operations (vec uoperations)}))
+ (update :undo-changes concat [(make-change
+ container
+ {:type :reg-objects
+ :shapes all-parents})]))
+ (seq applied-tokens)
+ (update-tokens container dest-shape origin-shape applied-tokens))))
+
(let [;; position-data is a special case because can be affected by :geometry-group and :content-group
;; so, if the position-data changes but the geometry is touched we need to reset the position-data
;; so it's calculated again
@@ -1564,14 +1607,21 @@
:val (get dest-shape attr)
:ignore-touched true}
- attr-group (get ctk/sync-attrs attr)]
+ attr-group (get ctk/sync-attrs attr)
+ token-attrs (cto/shape-attr->token-attrs attr)
+ applied-tokens' (cond-> applied-tokens
+ (not (and (touched attr-group)
+ omit-touched?))
+ (into token-attrs))]
(if (or (= (get origin-shape attr) (get dest-shape attr))
(and (touched attr-group) omit-touched?))
(recur (next attrs)
+ applied-tokens'
roperations
uoperations)
(recur (next attrs)
+ applied-tokens'
(conj roperations roperation)
(conj uoperations uoperation)))))))))
diff --git a/common/src/app/common/logic/shapes.cljc b/common/src/app/common/logic/shapes.cljc
index 2d9dd4464..e1e31ed30 100644
--- a/common/src/app/common/logic/shapes.cljc
+++ b/common/src/app/common/logic/shapes.cljc
@@ -14,8 +14,35 @@
[app.common.types.container :as ctn]
[app.common.types.shape.interactions :as ctsi]
[app.common.types.shape.layout :as ctl]
+ [app.common.types.token :as cto]
[app.common.uuid :as uuid]))
+(defn- generate-unapply-tokens
+ "When updating attributes that have a token applied, we must unapply it, because the value
+ of the attribute now has been given directly, and does not come from the token."
+ [changes objects]
+ (let [mod-obj-changes (->> (:redo-changes changes)
+ (filter #(= (:type %) :mod-obj)))
+
+ check-attr (fn [shape changes attr]
+ (let [tokens (get shape :applied-tokens {})
+ token-attrs (cto/shape-attr->token-attrs attr)]
+ (if (some #(contains? tokens %) token-attrs)
+ (pcb/update-shapes changes [(:id shape)] #(cto/unapply-token-id % token-attrs))
+ changes)))
+
+ check-shape (fn [changes mod-obj-change]
+ (let [shape (get objects (:id mod-obj-change))
+ xf (comp (filter #(= (:type %) :set))
+ (map :attr))
+ attrs (into [] xf (:operations mod-obj-change))]
+ (reduce (partial check-attr shape)
+ changes
+ attrs)))]
+ (reduce check-shape
+ changes
+ mod-obj-changes)))
+
(defn generate-update-shapes
[changes ids update-fn objects {:keys [attrs ignore-tree ignore-touched with-objects?]}]
(let [changes (reduce
@@ -29,8 +56,12 @@
(pcb/with-objects objects))
ids)
grid-ids (->> ids (filter (partial ctl/grid-layout? objects)))
- changes (pcb/update-shapes changes grid-ids ctl/assign-cell-positions {:with-objects? true})
- changes (pcb/reorder-grid-children changes ids)]
+ changes (-> changes
+ (pcb/update-shapes grid-ids ctl/assign-cell-positions {:with-objects? true})
+ (pcb/reorder-grid-children ids)
+ (cond->
+ (not ignore-touched)
+ (generate-unapply-tokens objects)))]
changes))
(defn- generate-update-shape-flags
diff --git a/common/src/app/common/logic/tokens.cljc b/common/src/app/common/logic/tokens.cljc
new file mode 100644
index 000000000..d1e7a49d8
--- /dev/null
+++ b/common/src/app/common/logic/tokens.cljc
@@ -0,0 +1,42 @@
+(ns app.common.logic.tokens
+ (:require
+ [app.common.files.changes-builder :as pcb]
+ [app.common.types.tokens-lib :as ctob]))
+
+(defn generate-update-active-sets
+ "Copy the active sets from the currently active themes and move them to the hidden token theme and update the theme with `update-hidden-theme-fn`.
+
+ Use this for managing sets active state without having to modify a user created theme (\"no themes selected\" state in the ui)."
+ [changes tokens-lib update-hidden-theme-fn]
+ (let [prev-active-token-themes (ctob/get-active-theme-paths tokens-lib)
+ active-token-set-names (ctob/get-active-themes-set-names tokens-lib)
+
+ prev-hidden-token-theme (ctob/get-hidden-theme tokens-lib)
+ hidden-token-theme (-> (or (some-> prev-hidden-token-theme (ctob/set-sets active-token-set-names))
+ (ctob/make-hidden-token-theme :sets active-token-set-names))
+ (update-hidden-theme-fn))
+
+ changes (-> changes
+ (pcb/update-active-token-themes #{ctob/hidden-token-theme-path} prev-active-token-themes))
+
+ changes (if prev-hidden-token-theme
+ (pcb/update-token-theme changes hidden-token-theme prev-hidden-token-theme)
+ (pcb/add-token-theme changes hidden-token-theme))]
+ changes))
+
+(defn generate-toggle-token-set
+ "Toggle a token set at `set-name` in `tokens-lib` without modifying a user theme."
+ [changes tokens-lib set-name]
+ (generate-update-active-sets changes tokens-lib #(ctob/toggle-set % set-name)))
+
+(defn generate-toggle-token-set-group
+ "Toggle a token set group at `prefixed-set-path-str` in `tokens-lib` without modifying a user theme."
+ [changes tokens-lib prefixed-set-path-str]
+ (let [deactivate? (contains? #{:all :partial} (ctob/sets-at-path-all-active? tokens-lib prefixed-set-path-str))
+ sets-names (->> (ctob/get-sets-at-prefix-path tokens-lib prefixed-set-path-str)
+ (map :name)
+ (into #{}))
+ update-fn (if deactivate?
+ #(ctob/disable-sets % sets-names)
+ #(ctob/enable-sets % sets-names))]
+ (generate-update-active-sets changes tokens-lib update-fn)))
diff --git a/common/src/app/common/test_helpers/files.cljc b/common/src/app/common/test_helpers/files.cljc
index 7d384b0ef..d7c18dcd9 100644
--- a/common/src/app/common/test_helpers/files.cljc
+++ b/common/src/app/common/test_helpers/files.cljc
@@ -58,6 +58,12 @@
(validate-file! file')
file'))
+(defn apply-undo-changes
+ [file changes]
+ (let [file' (ctf/update-file-data file #(cfc/process-changes % (:undo-changes changes) true))]
+ (validate-file! file')
+ file'))
+
;; ----- Pages
(defn sample-page
diff --git a/common/src/app/common/test_helpers/tokens.cljc b/common/src/app/common/test_helpers/tokens.cljc
new file mode 100644
index 000000000..c344f32b7
--- /dev/null
+++ b/common/src/app/common/test_helpers/tokens.cljc
@@ -0,0 +1,93 @@
+;; 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.test-helpers.tokens
+ (:require
+ [app.common.test-helpers.files :as thf]
+ [app.common.test-helpers.shapes :as ths]
+ [app.common.types.container :as ctn]
+ [app.common.types.file :as ctf]
+ [app.common.types.pages-list :as ctpl]
+ [app.common.types.shape-tree :as ctst]
+ [app.common.types.token :as cto]
+ [app.common.types.tokens-lib :as ctob]))
+
+(defn get-tokens-lib
+ [file]
+ (:tokens-lib (ctf/file-data file)))
+
+(defn add-tokens-lib
+ [file]
+ (ctf/update-file-data file #(update % :tokens-lib ctob/ensure-tokens-lib)))
+
+(defn update-tokens-lib
+ [file f]
+ (ctf/update-file-data file #(update % :tokens-lib f)))
+
+(defn get-token
+ [file set-name token-name]
+ (let [tokens-lib (:tokens-lib (:data file))]
+ (when tokens-lib
+ (-> tokens-lib
+ (ctob/get-set set-name)
+ (ctob/get-token token-name)))))
+
+(defn- set-stroke-width
+ [shape stroke-width]
+ (let [strokes (if (seq (:strokes shape))
+ (:strokes shape)
+ [{:stroke-style :solid
+ :stroke-alignment :inner
+ :stroke-width 1
+ :stroke-color "#000000"
+ :stroke-opacity 1}])
+ new-strokes (update strokes 0 assoc :stroke-width stroke-width)]
+ (ctn/set-shape-attr shape :strokes new-strokes {:ignore-touched true})))
+
+(defn- set-stroke-color
+ [shape stroke-color]
+ (let [strokes (if (seq (:strokes shape))
+ (:strokes shape)
+ [{:stroke-style :solid
+ :stroke-alignment :inner
+ :stroke-width 1
+ :stroke-color "#000000"
+ :stroke-opacity 1}])
+ new-strokes (update strokes 0 assoc :stroke-color stroke-color)]
+ (ctn/set-shape-attr shape :strokes new-strokes {:ignore-touched true})))
+
+(defn- set-fill-color
+ [shape fill-color]
+ (let [fills (if (seq (:fills shape))
+ (:fills shape)
+ [{:fill-color "#000000"
+ :fill-opacity 1}])
+ new-fills (update fills 0 assoc :fill-color fill-color)]
+ (ctn/set-shape-attr shape :fills new-fills {:ignore-touched true})))
+
+(defn apply-token-to-shape
+ [file shape-label token-name token-attrs shape-attrs resolved-value]
+ (let [page (thf/current-page file)
+ shape (ths/get-shape file shape-label)
+ shape' (as-> shape $
+ (cto/apply-token-to-shape {:shape $
+ :token {:name token-name}
+ :attributes token-attrs})
+ (reduce (fn [shape attr]
+ (case attr
+ :stroke-width (set-stroke-width shape resolved-value)
+ :stroke-color (set-stroke-color shape resolved-value)
+ :fill (set-fill-color shape resolved-value)
+ (ctn/set-shape-attr shape attr resolved-value {:ignore-touched true})))
+ $
+ shape-attrs))]
+
+ (ctf/update-file-data
+ file
+ (fn [file-data]
+ (ctpl/update-page file-data
+ (:id page)
+ #(ctst/set-shape % shape'))))))
diff --git a/common/src/app/common/types/component.cljc b/common/src/app/common/types/component.cljc
index aec70e7c2..b26461f2b 100644
--- a/common/src/app/common/types/component.cljc
+++ b/common/src/app/common/types/component.cljc
@@ -138,6 +138,14 @@
:layout-item-z-index
:layout-item-align-self})
+(defn component-attr?
+ "Check if some attribute is one that is involved in component syncrhonization.
+ Note that design tokens also are involved, although they go by an alternate
+ route and thus they are not part of :sync-attrs."
+ [attr]
+ (or (get sync-attrs attr)
+ (= :applied-tokens attr)))
+
(defn instance-root?
"Check if this shape is the head of a top instance."
[shape]
diff --git a/common/src/app/common/types/container.cljc b/common/src/app/common/types/container.cljc
index ca0181604..5c5673459 100644
--- a/common/src/app/common/types/container.cljc
+++ b/common/src/app/common/types/container.cljc
@@ -18,6 +18,7 @@
[app.common.types.plugins :as ctpg]
[app.common.types.shape-tree :as ctst]
[app.common.types.shape.layout :as ctl]
+ [app.common.types.token :as ctt]
[app.common.uuid :as uuid]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@@ -540,14 +541,28 @@
;; --- SHAPE UPDATE
+(defn- get-token-groups
+ [shape new-applied-tokens]
+ (let [old-applied-tokens (d/nilv (:applied-tokens shape) #{})
+ changed-token-attrs (filter #(not= (get old-applied-tokens %) (get new-applied-tokens %))
+ ctt/all-keys)
+ changed-groups (into #{}
+ (comp (map ctt/token-attr->shape-attr)
+ (map #(get ctk/sync-attrs %))
+ (filter some?))
+ changed-token-attrs)]
+ changed-groups))
+
(defn set-shape-attr
"Assign attribute to shape with touched logic.
The returned shape will contain a metadata associated with it
indicating if shape is touched or not."
[shape attr val & {:keys [ignore-touched ignore-geometry]}]
- (let [group (get ctk/sync-attrs attr)
- shape-val (get shape attr)
+ (let [group (get ctk/sync-attrs attr)
+ token-groups (when (= attr :applied-tokens)
+ (get-token-groups shape val))
+ shape-val (get shape attr)
ignore?
(or ignore-touched
@@ -585,9 +600,15 @@
;; set the "touched" flag for the group the attribute belongs to.
;; In some cases we need to ignore touched only if the attribute is
;; geometric (position, width or transformation).
- (and in-copy? group (not ignore?) (not equal?)
- (not (and ignore-geometry is-geometry?)))
- (-> (update :touched ctk/set-touched-group group)
+ (and in-copy?
+ (or (and group (not equal?)) (seq token-groups))
+ (not ignore?) (not (and ignore-geometry is-geometry?)))
+ (-> (update :touched (fn [touched]
+ (reduce #(ctk/set-touched-group %1 %2)
+ touched
+ (if group
+ (cons group token-groups)
+ token-groups))))
(dissoc :remote-synced))
(nil? val)
diff --git a/common/src/app/common/types/token.cljc b/common/src/app/common/types/token.cljc
index 31f0dd600..7794b45e1 100644
--- a/common/src/app/common/types/token.cljc
+++ b/common/src/app/common/types/token.cljc
@@ -6,8 +6,10 @@
(ns app.common.types.token
(:require
+ [app.common.data :as d]
[app.common.schema :as sm]
[app.common.schema.registry :as sr]
+ [clojure.data :as data]
[clojure.set :as set]
[malli.util :as mu]))
@@ -148,6 +150,15 @@
(def rotation-keys (schema-keys ::rotation))
+(def all-keys (set/union color-keys
+ border-radius-keys
+ stroke-width-keys
+ sizing-keys
+ opacity-keys
+ spacing-keys
+ dimensions-keys
+ rotation-keys))
+
(sm/register!
^{::sm/type ::tokens}
[:map {:title "Applied Tokens"}])
@@ -161,3 +172,59 @@
::spacing
::rotation
::dimensions])
+
+(defn shape-attr->token-attrs
+ [shape-attr]
+ (cond
+ (= :fills shape-attr) #{:fill}
+ (= :strokes shape-attr) #{:stroke-color :stroke-width}
+ (border-radius-keys shape-attr) #{shape-attr}
+ (sizing-keys shape-attr) #{shape-attr}
+ (opacity-keys shape-attr) #{shape-attr}
+ (spacing-keys shape-attr) #{shape-attr}
+ (rotation-keys shape-attr) #{shape-attr}))
+
+(defn token-attr->shape-attr
+ [token-attr]
+ (case token-attr
+ :fill :fills
+ :stroke-color :strokes
+ :stroke-width :strokes
+ token-attr))
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; TOKENS IN SHAPES
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(defn- toggle-or-apply-token
+ "Remove any shape attributes from token if they exists.
+ Othewise apply token attributes."
+ [shape token]
+ (let [[shape-leftover token-leftover _matching] (data/diff (:applied-tokens shape) token)]
+ (merge {} shape-leftover token-leftover)))
+
+(defn- token-from-attributes [token attributes]
+ (->> (map (fn [attr] [attr (:name token)]) attributes)
+ (into {})))
+
+(defn- apply-token-to-attributes [{:keys [shape token attributes]}]
+ (let [token (token-from-attributes token attributes)]
+ (toggle-or-apply-token shape token)))
+
+(defn apply-token-to-shape
+ [{:keys [shape token attributes] :as _props}]
+ (let [applied-tokens (apply-token-to-attributes {:shape shape
+ :token token
+ :attributes attributes})]
+ (update shape :applied-tokens #(merge % applied-tokens))))
+
+(defn 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 unapply-token-id [shape attributes]
+ (update shape :applied-tokens d/without-keys attributes))
+
diff --git a/common/src/app/common/types/tokens_lib.cljc b/common/src/app/common/types/tokens_lib.cljc
index 93148bcf9..76f653351 100644
--- a/common/src/app/common/types/tokens_lib.cljc
+++ b/common/src/app/common/types/tokens_lib.cljc
@@ -183,8 +183,38 @@
(def set-separator "/")
-(defn join-set-path [set-path]
- (join-path set-path set-separator))
+(defn join-set-path-str [& args]
+ (->> (filter some? args)
+ (str/join set-separator)))
+
+(defn join-set-path [path]
+ (join-path path set-separator))
+
+(defn split-set-str-path-prefix
+ "Split set-path
+
+ E.g.: \"S-some-set\" -> [\"S-\" \"some-set\"]
+ \"G-some-group\" -> [\"G-\" \"some-group\"]"
+ [path-str]
+ (some->> path-str
+ (re-matches #"^([SG]-)(.*)")
+ (rest)))
+
+(defn add-set-path-prefix [set-name-str]
+ (str set-prefix set-name-str))
+
+(defn add-set-path-group-prefix [group-path-str]
+ (str set-group-prefix group-path-str))
+
+(defn set-full-path->set-prefixed-full-path
+ "Returns token-set paths with prefixes to differentiate between sets and set-groups.
+
+ Sets will be prefixed with `set-prefix` (S-).
+ Set groups will be prefixed with `set-group-prefix` (G-)."
+ [full-path]
+ (let [set-path (mapv add-set-path-group-prefix (butlast full-path))
+ set-name (add-set-path-prefix (last full-path))]
+ (conj set-path set-name)))
(defn split-set-prefix [set-path]
(some->> set-path
@@ -230,6 +260,48 @@
path-part)))
(join-set-path)))
+(defn get-token-set-final-name [path]
+ (-> (split-token-set-path path)
+ (last)))
+
+(defn set-name->prefixed-full-path [name-str]
+ (-> (split-token-set-path name-str)
+ (set-full-path->set-prefixed-full-path)))
+
+(defn get-token-set-prefixed-path [token-set]
+ (let [path (get-path token-set set-separator)]
+ (set-full-path->set-prefixed-full-path path)))
+
+(defn get-prefixed-token-set-final-prefix [prefixed-path-str]
+ (some-> (get-token-set-final-name prefixed-path-str)
+ (split-set-str-path-prefix)
+ (first)))
+
+(defn set-name-string->prefixed-set-path-string [name-str]
+ (-> (set-name->prefixed-full-path name-str)
+ (join-set-path)))
+
+(defn prefixed-set-path-string->set-path [path-str]
+ (->> (split-token-set-path path-str)
+ (map (fn [path-part]
+ (or (-> (split-set-str-path-prefix path-part)
+ (second))
+ path-part)))))
+
+(defn prefixed-set-path-string->set-name-string [path-str]
+ (->> (prefixed-set-path-string->set-path path-str)
+ (join-set-path)))
+
+(defn prefixed-set-path-final-group?
+ "Predicate if the given prefixed path string ends with a group."
+ [prefixed-path-str]
+ (= (get-prefixed-token-set-final-prefix prefixed-path-str) set-group-prefix))
+
+(defn prefixed-set-path-final-set?
+ "Predicate if the given prefixed path string ends with a set."
+ [prefixed-path-str]
+ (= (get-prefixed-token-set-final-prefix prefixed-path-str) set-prefix))
+
(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."
@@ -263,7 +335,7 @@
(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-set-path [_] "returns name of set converted to the path with prefix identifiers")
+ (get-set-prefixed-path-string [_] "convert set name to prefixed full path string")
(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"))
@@ -312,8 +384,8 @@
(get-tokens [_]
(vals tokens))
- (get-set-path [_]
- (set-name->set-path-string name))
+ (get-set-prefixed-path-string [_]
+ (set-name-string->prefixed-set-path-string name))
(get-tokens-tree [_]
(tokens-tree tokens))
@@ -358,9 +430,23 @@
;; === TokenSets (collection)
(defprotocol ITokenSets
+ "Collection of sets and set groups.
+
+ Naming conventions:
+ Set name: the complete name as a string, without prefix \"some-group/some-subgroup/some-set\".
+ Set final name or fname: the last part of the name \"some-set\".
+ Set path: the groups part of the name, as a vector [\"some-group\" \"some-subgroup\"].
+ Set path str: the set path as a string \"some-group/some-subgroup\".
+ Set full path: the path including the fname, as a vector [\"some-group\", \"some-subgroup\", \"some-set\"].
+ Set full path str: the set full path as a string \"some-group/some-subgroup/some-set\".
+
+ Set prefix: the two-characters prefix added to a full path item \"G-\" / \"S-\".
+ Prefixed set path or ppath: a path wit added prefixes [\"G-some-group\", \"G-some-subgroup\"].
+ Prefixed set full path or pfpath: a full path wit prefixes [\"G-some-group\", \"G-some-subgroup\", \"S-some-set\"].
+ Prefixed set final name or pfname: a final name with prefix \"S-some-set\"."
(add-set [_ token-set] "add a set to the library, at the end")
(add-sets [_ token-set] "add a collection of sets to the library, at the end")
- (update-set [_ set-name f] "modify a set in the ilbrary")
+ (update-set [_ set-name f] "modify a set in the library")
(delete-set-path [_ set-path] "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")
@@ -369,6 +455,9 @@ When `before-set-name` is nil, move set to bottom")
(get-in-set-tree [_ path] "get `path` in nested tree of all sets in the library")
(get-sets [_] "get an ordered sequence of all sets in the library")
(get-path-sets [_ path] "get an ordered sequence of sets at `path` in the library")
+ (get-sets-at-prefix-path [_ prefixed-path-str] "get an ordered sequence of sets at `prefixed-path-str` in the library")
+ (get-sets-at-path [_ path-str] "get an ordered sequence of sets at `path` in the library")
+ (rename-set-group [_ from-path-str to-path-str] "renames set groups and all child set names from `from-path-str` to `to-path-str`")
(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`"))
@@ -415,12 +504,13 @@ When `before-set-name` is nil, move set to bottom")
(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")
+ (enable-set [_ set-name] "enable set in theme")
+ (enable-sets [_ set-names] "enable sets in theme")
(disable-set [_ set-name] "disable set in theme")
+ (disable-sets [_ set-names] "disable sets in theme")
(toggle-set [_ set-name] "toggle a set enabled / disabled in the theme")
-
(update-set-name [_ prev-set-name set-name] "update set-name from `prev-set-name` to `set-name` when it exists")
(theme-path [_] "get `token-theme-path` from theme")
(theme-matches-group-name [_ group name] "if a theme matches the given group & name")
@@ -436,13 +526,22 @@ When `before-set-name` is nil, move set to bottom")
(dt/now)
set-names))
+ (enable-set [this set-name]
+ (set-sets this (conj sets set-name)))
+
+ (enable-sets [this set-names]
+ (set-sets this (set/union sets set-names)))
+
(disable-set [this set-name]
(set-sets this (disj sets set-name)))
+ (disable-sets [this set-names]
+ (set-sets this (or (set/difference sets set-names) #{})))
+
(toggle-set [this set-name]
- (set-sets this (if (sets set-name)
- (disj sets set-name)
- (conj sets set-name))))
+ (if (sets set-name)
+ (disable-set this set-name)
+ (enable-set this set-name)))
(update-set-name [this prev-set-name set-name]
(if (get sets prev-set-name)
@@ -521,6 +620,8 @@ When `before-set-name` is nil, move set to bottom")
(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-hidden-theme [_] "get the theme hidden from the user ,
+used for managing active sets without a user created theme.")
(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")
@@ -587,6 +688,11 @@ When `before-set-name` is nil, move set to bottom")
(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")
+ (sets-at-path-all-active? [_ prefixed-path] "compute active state for child sets at `prefixed-path`.
+Will return a value that matches this schema:
+`:none` None of the nested sets are active
+`:all` All of the nested sets are active
+`:partial` Mixed active state of nested sets")
(get-active-themes-set-tokens [_] "set of set names that are active in the the active 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")
@@ -613,7 +719,7 @@ When `before-set-name` is nil, move set to bottom")
ITokenSets
(add-set [_ token-set]
(dm/assert! "expected valid token set" (check-token-set! token-set))
- (let [path (get-token-set-path token-set)]
+ (let [path (get-token-set-prefixed-path token-set)]
(TokensLib. (d/oassoc-in sets path token-set)
themes
active-themes)))
@@ -625,18 +731,18 @@ When `before-set-name` is nil, move set to bottom")
this token-sets))
(update-set [this set-name f]
- (let [path (split-token-set-name set-name)
- set (get-in sets path)]
+ (let [prefixed-full-path (set-name->prefixed-full-path set-name)
+ set (get-in sets prefixed-full-path)]
(if set
(let [set' (-> (make-token-set (f set))
(assoc :modified-at (dt/now)))
- path' (get-token-set-path set')
+ prefixed-full-path' (get-token-set-prefixed-path set')
name-changed? (not= (:name set) (:name set'))]
(check-token-set! set')
(if name-changed?
(TokensLib. (-> sets
- (d/oassoc-in-before path path' set')
- (d/dissoc-in path))
+ (d/oassoc-in-before prefixed-full-path prefixed-full-path' set')
+ (d/dissoc-in prefixed-full-path))
(walk/postwalk
(fn [form]
(if (instance? TokenTheme form)
@@ -644,33 +750,34 @@ When `before-set-name` is nil, move set to bottom")
form))
themes)
active-themes)
- (TokensLib. (d/oassoc-in sets path set')
+ (TokensLib. (d/oassoc-in sets prefixed-full-path set')
themes
active-themes)))
this)))
- (delete-set-path [_ set-path]
- (let [path (split-token-set-path set-path)
- set-node (get-in sets path)
- set-group? (not (instance? TokenSet set-node))]
- (TokensLib. (d/dissoc-in sets path)
+ (delete-set-path [_ prefixed-set-name]
+ (let [prefixed-set-path (split-token-set-path prefixed-set-name)
+ set-node (get-in sets prefixed-set-path)
+ set-group? (not (instance? TokenSet set-node))
+ set-name-string (prefixed-set-path-string->set-name-string prefixed-set-name)]
+ (TokensLib. (d/dissoc-in sets prefixed-set-path)
;; TODO: When deleting a set-group, also deactivate the child sets
(if set-group?
themes
(walk/postwalk
(fn [form]
(if (instance? TokenTheme form)
- (disable-set form set-path)
+ (disable-set form set-name-string)
form))
themes))
active-themes)))
;; TODO Handle groups and nesting
(move-set-before [this set-name before-set-name]
- (let [source-path (split-token-set-name set-name)
+ (let [source-path (set-name->prefixed-full-path set-name)
token-set (-> (get-set this set-name)
(assoc :modified-at (dt/now)))
- target-path (split-token-set-name before-set-name)]
+ target-path (set-name->prefixed-full-path before-set-name)]
(if before-set-name
(TokensLib. (d/oassoc-in-before sets target-path source-path token-set)
themes
@@ -696,6 +803,26 @@ When `before-set-name` is nil, move set to bottom")
(tree-seq d/ordered-map? vals)
(filter (partial instance? TokenSet))))
+ (get-sets-at-prefix-path [_ prefixed-path-str]
+ (some->> (get-in sets (split-token-set-path prefixed-path-str))
+ (tree-seq d/ordered-map? vals)
+ (filter (partial instance? TokenSet))))
+
+ (get-sets-at-path [_ path-str]
+ (some->> (split-token-set-path path-str)
+ (map add-set-path-group-prefix)
+ (get-in sets)
+ (tree-seq d/ordered-map? vals)
+ (filter (partial instance? TokenSet))))
+
+ (rename-set-group [this from-path-str to-path-str]
+ (->> (get-sets-at-path this from-path-str)
+ (reduce
+ (fn [lib set]
+ (update-set lib (:name set) (fn [set']
+ (update set' :name #(str to-path-str (str/strip-prefix % from-path-str))))))
+ this)))
+
(get-ordered-set-names [this]
(map :name (get-sets this)))
@@ -703,7 +830,7 @@ When `before-set-name` is nil, move set to bottom")
(count (get-sets this)))
(get-set [_ set-name]
- (let [path (split-token-set-name set-name)]
+ (let [path (set-name->prefixed-full-path set-name)]
(get-in sets path)))
(get-neighbor-set-name [this set-name index-offset]
@@ -766,6 +893,9 @@ When `before-set-name` is nil, move set to bottom")
(get-theme [_ group name]
(dm/get-in themes [group name]))
+ (get-hidden-theme [this]
+ (get-theme this hidden-token-theme-group hidden-token-theme-name))
+
(set-active-themes [_ active-themes]
(TokensLib. sets
themes
@@ -831,6 +961,19 @@ When `before-set-name` is nil, move set to bottom")
(mapcat :sets)
(get-active-themes this)))
+ (sets-at-path-all-active? [this prefixed-path-str]
+ (let [active-set-names (get-active-themes-set-names this)]
+ (if (seq active-set-names)
+ (let [path-active-set-names (->> (get-sets-at-prefix-path this prefixed-path-str)
+ (map :name)
+ (into #{}))
+ difference (set/difference path-active-set-names active-set-names)]
+ (cond
+ (empty? difference) :all
+ (seq (set/intersection path-active-set-names active-set-names)) :partial
+ :else :none))
+ :none)))
+
(get-active-themes-set-tokens [this]
(let [sets-order (get-ordered-set-names this)
active-themes (get-active-themes this)
@@ -845,15 +988,26 @@ When `before-set-name` is nil, move set to bottom")
(d/ordered-map) active-themes)))
(encode-dtcg [_]
- (into {} (comp
- (filter (partial instance? TokenSet))
- (map (fn [token-set]
- [(:name token-set) (get-dtcg-tokens-tree token-set)])))
- (tree-seq d/ordered-map? vals sets)))
+ (let [themes (into []
+ (comp
+ (filter #(and (instance? TokenTheme %)
+ (not (hidden-temporary-theme? %))))
+ (map (fn [token-theme]
+ (->> token-theme
+ (into {})
+ walk/stringify-keys))))
+ (tree-seq d/ordered-map? vals themes))
+ sets (into {} (comp
+ (filter (partial instance? TokenSet))
+ (map (fn [token-set]
+ [(:name token-set) (get-dtcg-tokens-tree token-set)])))
+ (tree-seq d/ordered-map? vals sets))]
+ (assoc sets "$themes" themes)))
(decode-dtcg-json [_ parsed-json]
(let [;; tokens-studio/plugin will add these meta properties, remove them for now
sets-data (dissoc parsed-json "$themes" "$metadata")
+ themes-data (get parsed-json "$themes")
lib (make-tokens-lib)
lib' (reduce
(fn [lib [set-name tokens]]
@@ -861,7 +1015,15 @@ When `before-set-name` is nil, move set to bottom")
:name set-name
:tokens (flatten-nested-tokens-json tokens ""))))
lib sets-data)]
- lib'))
+ (reduce
+ (fn [lib {:strs [name group description is-source modified-at sets]}]
+ (add-theme lib (TokenTheme. name
+ group
+ description
+ is-source
+ (dt/parse-instant modified-at)
+ (set sets))))
+ lib' themes-data)))
(get-all-tokens [this]
(reduce
diff --git a/common/src/app/common/types/tokens_list.cljc b/common/src/app/common/types/tokens_list.cljc
deleted file mode 100644
index b31262d4d..000000000
--- a/common/src/app/common/types/tokens_list.cljc
+++ /dev/null
@@ -1,49 +0,0 @@
-;; This Source Code Form is subject to the terms of the Mozilla Public
-;; License, v. 2.0. If a copy of the MPL was not distributed with this
-;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
-;;
-;; Copyright (c) KALEIDOS INC
-
-(ns app.common.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))
diff --git a/common/src/app/common/types/tokens_theme_list.cljc b/common/src/app/common/types/tokens_theme_list.cljc
deleted file mode 100644
index 971c96946..000000000
--- a/common/src/app/common/types/tokens_theme_list.cljc
+++ /dev/null
@@ -1,79 +0,0 @@
-;; This Source Code Form is subject to the terms of the Mozilla Public
-;; License, v. 2.0. If a copy of the MPL was not distributed with this
-;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
-;;
-;; Copyright (c) KALEIDOS INC
-
-(ns app.common.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))))))
diff --git a/common/test/common_tests/logic/comp_sync_test.cljc b/common/test/common_tests/logic/comp_sync_test.cljc
index 94f093ff3..f970f5fcb 100644
--- a/common/test/common_tests/logic/comp_sync_test.cljc
+++ b/common/test/common_tests/logic/comp_sync_test.cljc
@@ -193,7 +193,6 @@
(ths/add-sample-shape :free-shape))
page (thf/current-page file)
- main-root (ths/get-shape file :main-root)
;; ==== Action
changes1 (cls/generate-relocate (pcb/empty-changes)
@@ -203,9 +202,6 @@
0 ; to-index
#{(thi/id :free-shape)}) ; ids
-
-
-
updated-file (thf/apply-changes file changes1)
changes2 (cll/generate-sync-file-changes (pcb/empty-changes)
@@ -491,4 +487,4 @@
(t/is (= (:fill-color fill') "#fabada"))
(t/is (= (:fill-opacity fill') 1))
(t/is (= (:touched copy2-root') nil))
- (t/is (= (:touched copy2-child') nil))))
\ No newline at end of file
+ (t/is (= (:touched copy2-child') nil))))
diff --git a/common/test/common_tests/logic/token_apply_test.cljc b/common/test/common_tests/logic/token_apply_test.cljc
new file mode 100644
index 000000000..be7be1e5f
--- /dev/null
+++ b/common/test/common_tests/logic/token_apply_test.cljc
@@ -0,0 +1,202 @@
+;; 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 common-tests.logic.token-apply-test
+ (:require
+ [app.common.files.changes-builder :as pcb]
+ [app.common.logic.shapes :as cls]
+ [app.common.test-helpers.compositions :as tho]
+ [app.common.test-helpers.files :as thf]
+ [app.common.test-helpers.ids-map :as thi]
+ [app.common.test-helpers.shapes :as ths]
+ [app.common.test-helpers.tokens :as tht]
+ [app.common.types.container :as ctn]
+ [app.common.types.token :as cto]
+ [app.common.types.tokens-lib :as ctob]
+ [clojure.test :as t]))
+
+(t/use-fixtures :each thi/test-fixture)
+
+(defn- setup-file
+ []
+ (-> (thf/sample-file :file1)
+ (tht/add-tokens-lib)
+ (tht/update-tokens-lib #(-> %
+ (ctob/add-set (ctob/make-token-set :name "test-token-set"))
+ (ctob/add-theme (ctob/make-token-theme :name "test-theme"
+ :sets #{"test-token-set"}))
+ (ctob/set-active-themes #{"/test-theme"})
+ (ctob/add-token-in-set "test-token-set"
+ (ctob/make-token :name "token-radius"
+ :type :border-radius
+ :value 10))
+ (ctob/add-token-in-set "test-token-set"
+ (ctob/make-token :name "token-rotation"
+ :type :rotation
+ :value 30))
+ (ctob/add-token-in-set "test-token-set"
+ (ctob/make-token :name "token-opacity"
+ :type :opacity
+ :value 0.7))
+ (ctob/add-token-in-set "test-token-set"
+ (ctob/make-token :name "token-stroke-width"
+ :type :stroke-width
+ :value 2))
+ (ctob/add-token-in-set "test-token-set"
+ (ctob/make-token :name "token-color"
+ :type :color
+ :value "#00ff00"))
+ (ctob/add-token-in-set "test-token-set"
+ (ctob/make-token :name "token-dimensions"
+ :type :dimensions
+ :value 100))))
+ (tho/add-frame :frame1)))
+
+(defn- apply-all-tokens
+ [file]
+ (-> file
+ (tht/apply-token-to-shape :frame1 "token-radius" [:r1 :r2 :r3 :r4] [:r1 :r2 :r3 :r4] 10)
+ (tht/apply-token-to-shape :frame1 "token-rotation" [:rotation] [:rotation] 30)
+ (tht/apply-token-to-shape :frame1 "token-opacity" [:opacity] [:opacity] 0.7)
+ (tht/apply-token-to-shape :frame1 "token-stroke-width" [:stroke-width] [:stroke-width] 2)
+ (tht/apply-token-to-shape :frame1 "token-color" [:stroke-color] [:stroke-color] "#00ff00")
+ (tht/apply-token-to-shape :frame1 "token-color" [:fill] [:fill] "#00ff00")
+ (tht/apply-token-to-shape :frame1 "token-dimensions" [:width :height] [:width :height] 100)))
+
+(t/deftest apply-tokens-to-shape
+ (let [;; ==== Setup
+ file (setup-file)
+ page (thf/current-page file)
+ frame1 (ths/get-shape file :frame1)
+ token-radius (tht/get-token file "test-token-set" "token-radius")
+ token-rotation (tht/get-token file "test-token-set" "token-rotation")
+ token-opacity (tht/get-token file "test-token-set" "token-opacity")
+ token-stroke-width (tht/get-token file "test-token-set" "token-stroke-width")
+ token-color (tht/get-token file "test-token-set" "token-color")
+ token-dimensions (tht/get-token file "test-token-set" "token-dimensions")
+
+ ;; ==== Action
+ changes (-> (-> (pcb/empty-changes nil)
+ (pcb/with-page page)
+ (pcb/with-objects (:objects page)))
+ (cls/generate-update-shapes [(:id frame1)]
+ (fn [shape]
+ (as-> shape $
+ (cto/maybe-apply-token-to-shape {:token nil ; test nil case
+ :shape $
+ :attributes []})
+ (cto/maybe-apply-token-to-shape {:token token-radius
+ :shape $
+ :attributes [:r1 :r2 :r3 :r4]})
+ (cto/maybe-apply-token-to-shape {:token token-rotation
+ :shape $
+ :attributes [:rotation]})
+ (cto/maybe-apply-token-to-shape {:token token-opacity
+ :shape $
+ :attributes [:opacity]})
+ (cto/maybe-apply-token-to-shape {:token token-stroke-width
+ :shape $
+ :attributes [:stroke-width]})
+ (cto/maybe-apply-token-to-shape {:token token-color
+ :shape $
+ :attributes [:stroke-color]})
+ (cto/maybe-apply-token-to-shape {:token token-color
+ :shape $
+ :attributes [:fill]})
+ (cto/maybe-apply-token-to-shape {:token token-dimensions
+ :shape $
+ :attributes [:width :height]})))
+ (:objects page)
+ {}))
+
+ file' (thf/apply-changes file changes)
+
+ ;; ==== Get
+ frame1' (ths/get-shape file' :frame1)
+ applied-tokens' (:applied-tokens frame1')]
+
+ ;; ==== Check
+ (t/is (= (count applied-tokens') 11))
+ (t/is (= (:r1 applied-tokens') "token-radius"))
+ (t/is (= (:r2 applied-tokens') "token-radius"))
+ (t/is (= (:r3 applied-tokens') "token-radius"))
+ (t/is (= (:r4 applied-tokens') "token-radius"))
+ (t/is (= (:rotation applied-tokens') "token-rotation"))
+ (t/is (= (:opacity applied-tokens') "token-opacity"))
+ (t/is (= (:stroke-width applied-tokens') "token-stroke-width"))
+ (t/is (= (:stroke-color applied-tokens') "token-color"))
+ (t/is (= (:fill applied-tokens') "token-color"))
+ (t/is (= (:width applied-tokens') "token-dimensions"))
+ (t/is (= (:height applied-tokens') "token-dimensions"))))
+
+(t/deftest unapply-tokens-from-shape
+ (let [;; ==== Setup
+ file (-> (setup-file)
+ (apply-all-tokens))
+ page (thf/current-page file)
+ frame1 (ths/get-shape file :frame1)
+
+ ;; ==== Action
+ changes (-> (-> (pcb/empty-changes nil)
+ (pcb/with-page page)
+ (pcb/with-objects (:objects page)))
+ (cls/generate-update-shapes [(:id frame1)]
+ (fn [shape]
+ (-> shape
+ (cto/unapply-token-id [:r1 :r2 :r3 :r4])
+ (cto/unapply-token-id [:rotation])
+ (cto/unapply-token-id [:opacity])
+ (cto/unapply-token-id [:stroke-width])
+ (cto/unapply-token-id [:stroke-color])
+ (cto/unapply-token-id [:fill])
+ (cto/unapply-token-id [:width :height])))
+ (:objects page)
+ {}))
+
+ file' (thf/apply-changes file changes)
+
+ ;; ==== Get
+ frame1' (ths/get-shape file' :frame1)
+ applied-tokens' (:applied-tokens frame1')]
+
+ ;; ==== Check
+ (t/is (= (count applied-tokens') 0))))
+
+(t/deftest unapply-tokens-automatic
+ (let [;; ==== Setup
+ file (-> (setup-file)
+ (apply-all-tokens))
+ page (thf/current-page file)
+ frame1 (ths/get-shape file :frame1)
+
+ ;; ==== Action
+ changes (-> (-> (pcb/empty-changes nil)
+ (pcb/with-page page)
+ (pcb/with-objects (:objects page)))
+ (cls/generate-update-shapes [(:id frame1)]
+ (fn [shape]
+ (-> shape
+ (ctn/set-shape-attr :r1 0)
+ (ctn/set-shape-attr :r2 0)
+ (ctn/set-shape-attr :r3 0)
+ (ctn/set-shape-attr :r4 0)
+ (ctn/set-shape-attr :rotation 0)
+ (ctn/set-shape-attr :opacity 0)
+ (ctn/set-shape-attr :strokes [])
+ (ctn/set-shape-attr :fills [])
+ (ctn/set-shape-attr :width 0)
+ (ctn/set-shape-attr :height 0)))
+ (:objects page)
+ {}))
+
+ file' (thf/apply-changes file changes)
+
+ ;; ==== Get
+ frame1' (ths/get-shape file' :frame1)
+ applied-tokens' (:applied-tokens frame1')]
+
+ ;; ==== Check
+ (t/is (= (count applied-tokens') 0))))
\ No newline at end of file
diff --git a/common/test/common_tests/logic/token_test.cljc b/common/test/common_tests/logic/token_test.cljc
new file mode 100644
index 000000000..c91235d8f
--- /dev/null
+++ b/common/test/common_tests/logic/token_test.cljc
@@ -0,0 +1,117 @@
+(ns common-tests.logic.token-test
+ (:require
+ [app.common.files.changes-builder :as pcb]
+ [app.common.logic.tokens :as clt]
+ [app.common.test-helpers.files :as thf]
+ [app.common.test-helpers.ids-map :as thi]
+ [app.common.test-helpers.tokens :as tht]
+ [app.common.types.tokens-lib :as ctob]
+ [clojure.test :as t]))
+
+(t/use-fixtures :each thi/test-fixture)
+
+(defn- setup-file [lib-fn]
+ (-> (thf/sample-file :file1)
+ (tht/add-tokens-lib)
+ (tht/update-tokens-lib lib-fn)))
+
+(t/deftest generate-toggle-token-set-test
+ (t/testing "toggling an active set will switch to hidden theme without user sets"
+ (let [file (setup-file #(-> %
+ (ctob/add-set (ctob/make-token-set :name "foo/bar"))
+ (ctob/add-theme (ctob/make-token-theme :name "theme"
+ :sets #{"foo/bar"}))
+ (ctob/set-active-themes #{"/theme"})))
+ changes (clt/generate-toggle-token-set (pcb/empty-changes) (tht/get-tokens-lib file) "foo/bar")
+
+ redo (thf/apply-changes file changes)
+ redo-lib (tht/get-tokens-lib redo)
+ undo (thf/apply-undo-changes redo changes)
+ undo-lib (tht/get-tokens-lib undo)]
+ (t/is (= #{ctob/hidden-token-theme-path} (ctob/get-active-theme-paths redo-lib)))
+ (t/is (= #{} (:sets (ctob/get-hidden-theme redo-lib))))
+
+ ;; Undo
+ (t/is (nil? (ctob/get-hidden-theme undo-lib)))
+ (t/is (= #{"/theme"} (ctob/get-active-theme-paths undo-lib)))))
+
+ (t/testing "toggling an inactive set will switch to hidden theme without user sets"
+ (let [file (setup-file #(-> %
+ (ctob/add-set (ctob/make-token-set :name "foo/bar"))
+ (ctob/add-theme (ctob/make-token-theme :name "theme"
+ :sets #{"foo/bar"}))
+ (ctob/set-active-themes #{"/theme"})))
+ changes (clt/generate-toggle-token-set (pcb/empty-changes) (tht/get-tokens-lib file) "foo/bar")
+
+ redo (thf/apply-changes file changes)
+ redo-lib (tht/get-tokens-lib redo)
+ undo (thf/apply-undo-changes redo changes)
+ undo-lib (tht/get-tokens-lib undo)]
+ (t/is (= #{ctob/hidden-token-theme-path} (ctob/get-active-theme-paths redo-lib)))
+ (t/is (= #{} (:sets (ctob/get-hidden-theme redo-lib))))
+
+ ;; Undo
+ (t/is (nil? (ctob/get-hidden-theme undo-lib)))
+ (t/is (= #{"/theme"} (ctob/get-active-theme-paths undo-lib)))))
+
+ (t/testing "toggling an set with hidden theme already active will toggle set in hidden theme"
+ (let [file (setup-file #(-> %
+ (ctob/add-set (ctob/make-token-set :name "foo/bar"))
+ (ctob/add-theme (ctob/make-hidden-token-theme))
+ (ctob/set-active-themes #{ctob/hidden-token-theme-path})))
+
+ changes (clt/generate-toggle-token-set-group (pcb/empty-changes) (tht/get-tokens-lib file) "G-foo/S-bar")
+
+ redo (thf/apply-changes file changes)
+ redo-lib (tht/get-tokens-lib redo)
+ undo (thf/apply-undo-changes redo changes)
+ undo-lib (tht/get-tokens-lib undo)]
+ (t/is (= (ctob/get-active-theme-paths redo-lib) (ctob/get-active-theme-paths undo-lib)))
+
+ (t/is (= #{"foo/bar"} (:sets (ctob/get-hidden-theme redo-lib))))
+
+ ;; Undo
+ (t/is (some? (ctob/get-hidden-theme undo-lib))))))
+
+(t/deftest generate-toggle-token-set-group-test
+ (t/testing "toggling set group with no active sets inside will activate all child sets"
+ (let [file (setup-file #(-> %
+ (ctob/add-set (ctob/make-token-set :name "foo/bar"))
+ (ctob/add-set (ctob/make-token-set :name "foo/bar/baz"))
+ (ctob/add-set (ctob/make-token-set :name "foo/bar/baz/baz-child"))
+ (ctob/add-theme (ctob/make-token-theme :name "theme"))
+ (ctob/set-active-themes #{"/theme"})))
+ changes (clt/generate-toggle-token-set-group (pcb/empty-changes) (tht/get-tokens-lib file) "G-foo/G-bar")
+
+ redo (thf/apply-changes file changes)
+ redo-lib (tht/get-tokens-lib redo)
+ undo (thf/apply-undo-changes redo changes)
+ undo-lib (tht/get-tokens-lib undo)]
+ (t/is (= #{ctob/hidden-token-theme-path} (ctob/get-active-theme-paths redo-lib)))
+ (t/is (= #{"foo/bar/baz" "foo/bar/baz/baz-child"} (:sets (ctob/get-hidden-theme redo-lib))))
+
+ ;; Undo
+ (t/is (nil? (ctob/get-hidden-theme undo-lib)))
+ (t/is (= #{"/theme"} (ctob/get-active-theme-paths undo-lib)))))
+
+ (t/testing "toggling set group with partially active sets inside will deactivate all child sets"
+ (let [file (setup-file #(-> %
+ (ctob/add-set (ctob/make-token-set :name "foo/bar"))
+ (ctob/add-set (ctob/make-token-set :name "foo/bar/baz"))
+ (ctob/add-set (ctob/make-token-set :name "foo/bar/baz/baz-child"))
+ (ctob/add-theme (ctob/make-token-theme :name "theme"
+ :sets #{"foo/bar/baz"}))
+ (ctob/set-active-themes #{"/theme"})))
+
+ changes (clt/generate-toggle-token-set-group (pcb/empty-changes) (tht/get-tokens-lib file) "G-foo/G-bar")
+
+ redo (thf/apply-changes file changes)
+ redo-lib (tht/get-tokens-lib redo)
+ undo (thf/apply-undo-changes redo changes)
+ undo-lib (tht/get-tokens-lib undo)]
+ (t/is (= #{} (:sets (ctob/get-hidden-theme redo-lib))))
+ (t/is (= #{ctob/hidden-token-theme-path} (ctob/get-active-theme-paths redo-lib)))
+
+ ;; Undo
+ (t/is (nil? (ctob/get-hidden-theme undo-lib)))
+ (t/is (= #{"/theme"} (ctob/get-active-theme-paths undo-lib))))))
diff --git a/common/test/common_tests/types/data/tokens-multi-set-example.json b/common/test/common_tests/types/data/tokens-multi-set-example.json
index ca836d961..7b44af9bc 100644
--- a/common/test/common_tests/types/data/tokens-multi-set-example.json
+++ b/common/test/common_tests/types/data/tokens-multi-set-example.json
@@ -796,7 +796,14 @@
}
}
},
- "$themes": [],
+ "$themes": [ {
+ "name": "theme-1",
+ "group": "group-1",
+ "description": null,
+ "is-source": false,
+ "modified-at": "2024-01-01T00:00:00.000+00:00",
+ "sets": [ "light" ]
+ } ],
"$metadata": {
"tokenSetOrder": ["core", "light", "dark", "theme"]
}
diff --git a/common/test/common_tests/types/tokens_lib_test.cljc b/common/test/common_tests/types/tokens_lib_test.cljc
index cab60fc8f..67fd9c105 100644
--- a/common/test/common_tests/types/tokens_lib_test.cljc
+++ b/common/test/common_tests/types/tokens_lib_test.cljc
@@ -100,7 +100,6 @@
(->> (ctob/move-set-before tokens-lib set-name before-set-name)
(ctob/get-ordered-set-names)
(into [])))]
- ;; TODO Nested moving doesn't work as expected
(t/testing "regular moving"
(t/is (= ["A" "Move" "B"] (move "Move" "B")))
(t/is (= ["B" "A" "Move"] (move "A" "Move"))))
@@ -231,6 +230,24 @@
(t/is (= (:name token-set') "updated-name"))
(t/is (dt/is-after? (:modified-at token-set') (:modified-at token-set)))))
+ (t/deftest rename-token-set-group
+ (let [tokens-lib (-> (ctob/make-tokens-lib)
+ (ctob/add-set (ctob/make-token-set :name "foo/bar/baz"))
+ (ctob/add-set (ctob/make-token-set :name "foo/bar/baz/baz-child-1"))
+ (ctob/add-set (ctob/make-token-set :name "foo/bar/baz/baz-child-2"))
+ (ctob/add-theme (ctob/make-token-theme :name "theme" :sets #{"foo/bar/baz/baz-child-1"})))
+ tokens-lib' (-> tokens-lib
+ (ctob/rename-set-group "foo/bar" "foo/bar-renamed")
+ (ctob/rename-set-group "foo/bar-renamed/baz" "foo/bar-renamed/baz-renamed"))
+ expected-set-names (ctob/get-ordered-set-names tokens-lib')
+ expected-theme-sets (-> (ctob/get-theme tokens-lib' "" "theme")
+ :sets)]
+ (t/is (= expected-set-names
+ '("foo/bar-renamed/baz"
+ "foo/bar-renamed/baz-renamed/baz-child-1"
+ "foo/bar-renamed/baz-renamed/baz-child-2")))
+ (t/is (= expected-theme-sets #{"foo/bar-renamed/baz-renamed/baz-child-1"}))))
+
(t/deftest delete-token-set
(let [tokens-lib (-> (ctob/make-tokens-lib)
(ctob/add-set (ctob/make-token-set :name "test-token-set"))
@@ -241,11 +258,10 @@
(ctob/delete-set-path "S-not-existing-set"))
token-set' (ctob/get-set tokens-lib' "updated-name")
- ;;token-theme' (ctob/get-theme tokens-lib' "" "test-token-theme")
- ]
+ token-theme' (ctob/get-theme tokens-lib' "" "test-token-theme")]
(t/is (= (ctob/set-count tokens-lib') 0))
- ;; (t/is (= (:sets token-theme') #{})) TODO: fix this
+ (t/is (= (:sets token-theme') #{}))
(t/is (nil? token-set'))))
(t/deftest active-themes-set-names
@@ -401,8 +417,39 @@
expected-tokens (ctob/get-active-themes-set-tokens tokens-lib)
expected-token-names (mapv key expected-tokens)]
(t/is (= '("set-a" "set-b" "inactive-set") expected-order))
- (t/is (= ["set-a-token" "set-b-token"] expected-token-names)))))
+ (t/is (= ["set-a-token" "set-b-token"] expected-token-names))))
+ (t/testing "sets-at-path-active-state"
+ (let [tokens-lib (-> (ctob/make-tokens-lib)
+
+ (ctob/add-set (ctob/make-token-set :name "foo/bar/baz"))
+ (ctob/add-set (ctob/make-token-set :name "foo/bar/bam"))
+
+ (ctob/add-theme (ctob/make-token-theme :name "none"))
+ (ctob/add-theme (ctob/make-token-theme :name "partial"
+ :sets #{"foo/bar/baz"}))
+ (ctob/add-theme (ctob/make-token-theme :name "all"
+ :sets #{"foo/bar/baz"
+ "foo/bar/bam"}))
+ (ctob/add-theme (ctob/make-token-theme :name "invalid"
+ :sets #{"foo/missing"})))
+
+ expected-none (-> tokens-lib
+ (ctob/set-active-themes #{"/none"})
+ (ctob/sets-at-path-all-active? "G-foo"))
+ expected-all (-> tokens-lib
+ (ctob/set-active-themes #{"/all"})
+ (ctob/sets-at-path-all-active? "G-foo"))
+ expected-partial (-> tokens-lib
+ (ctob/set-active-themes #{"/partial"})
+ (ctob/sets-at-path-all-active? "G-foo"))
+ expected-invalid-none (-> tokens-lib
+ (ctob/set-active-themes #{"/invalid"})
+ (ctob/sets-at-path-all-active? "G-foo"))]
+ (t/is (= :none expected-none))
+ (t/is (= :all expected-all))
+ (t/is (= :partial expected-partial))
+ (t/is (= :none expected-invalid-none)))))
(t/deftest token-theme-in-a-lib
(t/testing "add-token-theme"
@@ -1060,8 +1107,13 @@
get-set-token (fn [set-name token-name]
(some-> (ctob/get-set lib set-name)
(ctob/get-token token-name)
- (dissoc :modified-at)))]
+ (dissoc :modified-at)))
+ token-theme (ctob/get-theme lib "group-1" "theme-1")]
(t/is (= '("core" "light" "dark" "theme") (ctob/get-ordered-set-names lib)))
+ (t/testing "set exists in theme"
+ (t/is (= (:group token-theme) "group-1"))
+ (t/is (= (:name token-theme) "theme-1"))
+ (t/is (= (:sets token-theme) #{"light"})))
(t/testing "tokens exist in core set"
(t/is (= (get-set-token "core" "colors.red.600")
{:name "colors.red.600"
@@ -1082,7 +1134,8 @@
(t/is (nil? (get-set-token "typography" "H1.Bold"))))))
(t/testing "encode-dtcg-json"
- (let [tokens-lib (-> (ctob/make-tokens-lib)
+ (let [now (dt/now)
+ tokens-lib (-> (ctob/make-tokens-lib)
(ctob/add-set (ctob/make-token-set :name "core"
:tokens {"colors.red.600"
(ctob/make-token
@@ -1099,9 +1152,19 @@
(ctob/make-token
{:name "button.primary.background"
:type :color
- :value "{accent.default}"})})))
+ :value "{accent.default}"})}))
+ (ctob/add-theme (ctob/make-token-theme :name "theme-1"
+ :group "group-1"
+ :modified-at now
+ :sets #{"core"})))
expected (ctob/encode-dtcg tokens-lib)]
- (t/is (= {"core"
+ (t/is (= {"$themes" [{"description" nil
+ "group" "group-1"
+ "is-source" false
+ "modified-at" now
+ "name" "theme-1"
+ "sets" #{"core"}}]
+ "core"
{"colors" {"red" {"600" {"$value" "#e53e3e"
"$type" "color"}}}
"spacing"
@@ -1142,4 +1205,3 @@
(t/is (= @with-prev-tokens-lib @tokens-lib)))
(t/testing "fresh tokens library is also equal"
(= @with-empty-tokens-lib @tokens-lib)))))))
-
diff --git a/frontend/package.json b/frontend/package.json
index 0f85bf1b2..acf288b23 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -122,7 +122,7 @@
"rxjs": "8.0.0-alpha.14",
"sax": "^1.4.1",
"source-map-support": "^0.5.21",
- "style-dictionary": "4.0.0-prerelease.34",
+ "style-dictionary": "4.0.0-prerelease.36",
"tdigest": "^0.1.2",
"tinycolor2": "npm:^1.6.0",
"ua-parser-js": "2.0.0-rc.1",
diff --git a/frontend/playwright/data/get-teams-tokens.json b/frontend/playwright/data/get-teams-tokens.json
new file mode 100644
index 000000000..7ec12f187
--- /dev/null
+++ b/frontend/playwright/data/get-teams-tokens.json
@@ -0,0 +1,26 @@
+[
+ {
+ "~:features": {
+ "~#set": [
+ "design-tokens/v1",
+ "layout/grid",
+ "styles/v2",
+ "fdata/pointer-map",
+ "fdata/objects-map",
+ "components/v2",
+ "fdata/shape-data-type"
+ ]
+ },
+ "~:permissions": {
+ "~:type": "~:membership",
+ "~:is-owner": true,
+ "~:is-admin": true,
+ "~:can-edit": true
+ },
+ "~:name": "Default",
+ "~:modified-at": "~m1713533116375",
+ "~:id": "~uc7ce0794-0992-8105-8004-38e630f7920a",
+ "~:created-at": "~m1713533116375",
+ "~:is-default": true
+ }
+]
diff --git a/frontend/playwright/data/workspace/get-team-tokens.json b/frontend/playwright/data/workspace/get-team-tokens.json
new file mode 100644
index 000000000..855b1506a
--- /dev/null
+++ b/frontend/playwright/data/workspace/get-team-tokens.json
@@ -0,0 +1,24 @@
+{
+ "~:features": {
+ "~#set": [
+ "design-tokens/v1",
+ "layout/grid",
+ "styles/v2",
+ "fdata/pointer-map",
+ "fdata/objects-map",
+ "components/v2",
+ "fdata/shape-data-type"
+ ]
+ },
+ "~:permissions": {
+ "~:type": "~:membership",
+ "~:is-owner": true,
+ "~:is-admin": true,
+ "~:can-edit": true
+ },
+ "~:name": "Default",
+ "~:modified-at": "~m1713533116375",
+ "~:id": "~uc7ce0794-0992-8105-8004-38e630f40f6d",
+ "~:created-at": "~m1713533116375",
+ "~:is-default": true
+}
diff --git a/frontend/playwright/ui/pages/WorkspacePage.js b/frontend/playwright/ui/pages/WorkspacePage.js
index d0d29b531..3948730c5 100644
--- a/frontend/playwright/ui/pages/WorkspacePage.js
+++ b/frontend/playwright/ui/pages/WorkspacePage.js
@@ -85,6 +85,12 @@ export class WorkspacePage extends BaseWebSocketPage {
this.togglePalettesVisibility = page.getByTestId(
"toggle-palettes-visibility",
);
+ this.tokensUpdateCreateModal = page.getByTestId(
+ "token-update-create-modal",
+ );
+ this.tokenThemesSetsSidebar = page.getByTestId("token-themes-sets-sidebar");
+ this.tokenSetItems = page.getByTestId("tokens-set-item");
+ this.tokenSetGroupItems = page.getByTestId("tokens-set-group-item");
}
async goToWorkspace({
diff --git a/frontend/playwright/ui/specs/tokens.spec.js b/frontend/playwright/ui/specs/tokens.spec.js
new file mode 100644
index 000000000..0c33b0ddd
--- /dev/null
+++ b/frontend/playwright/ui/specs/tokens.spec.js
@@ -0,0 +1,145 @@
+import { test, expect } from "@playwright/test";
+import { WorkspacePage } from "../pages/WorkspacePage";
+import { BaseWebSocketPage } from "../pages/BaseWebSocketPage";
+
+test.beforeEach(async ({ page }) => {
+ await WorkspacePage.init(page);
+ await BaseWebSocketPage.mockRPC(page, "get-teams", "get-teams-tokens.json");
+});
+
+const setupFileWithTokens = async (page) => {
+ const workspacePage = new WorkspacePage(page);
+ await workspacePage.setupEmptyFile();
+ await workspacePage.mockRPC(
+ "get-team?id=*",
+ "workspace/get-team-tokens.json",
+ );
+
+ await workspacePage.goToWorkspace();
+
+ const tokensTabButton = page.getByRole("tab", { name: "Tokens" });
+ await tokensTabButton.click();
+
+ return {
+ workspacePage,
+ tokensUpdateCreateModal: workspacePage.tokensUpdateCreateModal,
+ tokenThemesSetsSidebar: workspacePage.tokenThemesSetsSidebar,
+ tokenSetItems: workspacePage.tokenSetItems,
+ tokenSetGroupItems: workspacePage.tokenSetGroupItems,
+ };
+};
+
+test.describe("Tokens: Tokens Tab", () => {
+ test("Clicking tokens tab button opens tokens sidebar tab", async ({
+ page,
+ }) => {
+ const { workspacePage, tokensUpdateCreateModal, tokenThemesSetsSidebar } =
+ await setupFileWithTokens(page);
+
+ const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" });
+
+ await expect(tokensTabPanel).toHaveText(/TOKENS/);
+ await expect(tokensTabPanel).toHaveText(/Themes/);
+ });
+
+ test("User creates color token and auto created set show up in the sidebar", async ({
+ page,
+ }) => {
+ const { workspacePage, tokensUpdateCreateModal, tokenThemesSetsSidebar } =
+ await setupFileWithTokens(page);
+
+ const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" });
+ await tokensTabPanel.getByTitle("Add token: Color").click();
+
+ // Create color token with mouse
+
+ await expect(tokensUpdateCreateModal).toBeVisible();
+
+ const nameField = tokensUpdateCreateModal.getByLabel("Name");
+ const valueField = tokensUpdateCreateModal.getByLabel("Value");
+
+ await nameField.click();
+ await nameField.fill("color.primary");
+
+ await valueField.click();
+ await valueField.fill("red");
+
+ const submitButton = tokensUpdateCreateModal.getByRole("button", {
+ name: "Save",
+ });
+ await expect(submitButton).toBeEnabled();
+ await submitButton.click();
+
+ await expect(tokensTabPanel.getByText("color.primary")).toBeEnabled();
+
+ // Create token referencing the previous one with keyboard
+
+ await tokensTabPanel.getByTitle("Add token: Color").click();
+ await expect(tokensUpdateCreateModal).toBeVisible();
+
+ await nameField.click();
+ await nameField.fill("color.secondary");
+ await nameField.press("Tab");
+
+ await valueField.click();
+ await valueField.fill("{color.primary}");
+
+ await expect(submitButton).toBeEnabled();
+ await nameField.press("Enter");
+
+ const referenceToken = tokensTabPanel.getByText("color.secondary");
+ await expect(referenceToken).toBeEnabled();
+
+ // Tokens tab panel should have two tokens with the color red / #ff0000
+ await expect(tokensTabPanel.getByTitle("#ff0000")).toHaveCount(2);
+
+ // Global set has been auto created and is active
+ await expect(
+ tokenThemesSetsSidebar.getByRole("button", {
+ name: "Global",
+ }),
+ ).toHaveCount(1);
+ await expect(
+ tokenThemesSetsSidebar.getByRole("button", {
+ name: "Global",
+ }),
+ ).toHaveAttribute("aria-checked", "true");
+ });
+});
+
+test.describe("Tokens: Sets Tab", () => {
+ const createSet = async (sidebar, setName, finalKey = "Enter") => {
+ const tokensTabButton = sidebar
+ .getByRole("button", { name: "Add set" })
+ .click();
+
+ const setInput = sidebar.locator("input:focus");
+ await expect(setInput).toBeVisible();
+ await setInput.fill(setName);
+ await setInput.press(finalKey);
+ };
+
+ // test("User creates sets tree structure by entering a set path", async ({
+ // page,
+ // }) => {
+ // const {
+ // workspacePage,
+ // tokenThemesSetsSidebar,
+ // tokenSetItems,
+ // tokenSetGroupItems,
+ // } = await setupFileWithTokens(page);
+ //
+ // const tokensTabButton = tokenThemesSetsSidebar
+ // .getByRole("button", { name: "Add set" })
+ // .click();
+ //
+ // await createSet(tokenThemesSetsSidebar, "core/colors/light");
+ // await createSet(tokenThemesSetsSidebar, "core/colors/dark");
+ //
+ // // User cancels during editing
+ // await createSet(tokenThemesSetsSidebar, "core/colors/dark", "Escape");
+ //
+ // await expect(tokenSetItems).toHaveCount(2);
+ // await expect(tokenSetGroupItems).toHaveCount(2);
+ // });
+});
diff --git a/frontend/resources/images/icons/broken-link.svg b/frontend/resources/images/icons/broken-link.svg
new file mode 100644
index 000000000..4e6ed1273
--- /dev/null
+++ b/frontend/resources/images/icons/broken-link.svg
@@ -0,0 +1,3 @@
+
diff --git a/frontend/resources/images/icons/import-export.svg b/frontend/resources/images/icons/import-export.svg
new file mode 100644
index 000000000..26ef0f81a
--- /dev/null
+++ b/frontend/resources/images/icons/import-export.svg
@@ -0,0 +1,3 @@
+
\ No newline at end of file
diff --git a/frontend/resources/images/icons/token-status-full.svg b/frontend/resources/images/icons/token-status-full.svg
new file mode 100644
index 000000000..a24ba0ce7
--- /dev/null
+++ b/frontend/resources/images/icons/token-status-full.svg
@@ -0,0 +1,8 @@
+
\ No newline at end of file
diff --git a/frontend/resources/images/icons/token-status-non-applied.svg b/frontend/resources/images/icons/token-status-non-applied.svg
new file mode 100644
index 000000000..6c9838f0a
--- /dev/null
+++ b/frontend/resources/images/icons/token-status-non-applied.svg
@@ -0,0 +1,8 @@
+
\ No newline at end of file
diff --git a/frontend/resources/images/icons/token-status-partial.svg b/frontend/resources/images/icons/token-status-partial.svg
new file mode 100644
index 000000000..de17718d2
--- /dev/null
+++ b/frontend/resources/images/icons/token-status-partial.svg
@@ -0,0 +1,8 @@
+
\ No newline at end of file
diff --git a/frontend/resources/styles/common/refactor/spacing.scss b/frontend/resources/styles/common/refactor/spacing.scss
index fcc536563..c903c96f3 100644
--- a/frontend/resources/styles/common/refactor/spacing.scss
+++ b/frontend/resources/styles/common/refactor/spacing.scss
@@ -152,6 +152,7 @@ $s-648: #{0.25 * 162}rem;
$s-664: #{0.25 * 166}rem;
$s-688: #{0.25 * 172}rem;
$s-712: #{0.25 * 178}rem;
+$s-720: #{0.25 * 180}rem;
$s-736: #{0.25 * 184}rem;
$s-744: #{0.25 * 186}rem;
$s-800: #{0.25 * 200}rem;
diff --git a/frontend/src/app/main/data/tokens.cljs b/frontend/src/app/main/data/tokens.cljs
index dbd3358c7..e70f557bf 100644
--- a/frontend/src/app/main/data/tokens.cljs
+++ b/frontend/src/app/main/data/tokens.cljs
@@ -6,20 +6,18 @@
(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.logic.tokens :as clt]
[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]))
@@ -51,57 +49,25 @@
;; 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
+(defn set-selected-token-set-path
+ [full-path]
+ (ptk/reify ::set-selected-token-set-path
ptk/UpdateEvent
(update [_ state]
- (wtts/assoc-selected-token-set-id state id))))
+ (wtts/assoc-selected-token-set-path state full-path))))
-(defn set-selected-token-set-id-from-name
+(defn set-selected-token-set-path-from-name
[token-set-name]
- (ptk/reify ::set-selected-token-set-id-from-name
+ (ptk/reify ::set-selected-token-set-path-from-name
ptk/UpdateEvent
(update [_ state]
- (->> (ctob/set-name->set-path-string token-set-name)
- (wtts/assoc-selected-token-set-id state)))))
+ (->> (ctob/set-name-string->prefixed-set-path-string token-set-name)
+ (wtts/assoc-selected-token-set-path state)))))
(defn create-token-theme [token-theme]
(let [new-token-theme token-theme]
@@ -165,9 +131,19 @@
(let [changes (-> (pcb/empty-changes it)
(pcb/add-token-set new-token-set))]
(rx/of
- (set-selected-token-set-id-from-name (:name new-token-set))
+ (set-selected-token-set-path-from-name (:name new-token-set))
(dch/commit-changes changes)))))))
+(defn rename-token-set-group [from-path-str to-path-str]
+ (ptk/reify ::rename-token-set-group
+ ptk/WatchEvent
+ (watch [it _state _]
+ (let [changes (-> (pcb/empty-changes it)
+ (pcb/rename-token-set-group from-path-str to-path-str))]
+ (rx/of
+ (set-selected-token-set-path-from-name to-path-str)
+ (dch/commit-changes changes))))))
+
(defn update-token-set [set-name token-set]
(ptk/reify ::update-token-set
ptk/WatchEvent
@@ -177,28 +153,25 @@
changes (-> (pcb/empty-changes it)
(pcb/update-token-set token-set prev-token-set))]
(rx/of
- (set-selected-token-set-id-from-name (:name token-set))
+ (set-selected-token-set-path-from-name (: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))]
+ (watch [_ state _]
+ (let [changes (clt/generate-toggle-token-set (pcb/empty-changes) (get-tokens-lib state) token-set-name)]
(rx/of
- (dch/commit-changes changes')
+ (dch/commit-changes changes)
+ (wtu/update-workspace-tokens))))))
+
+(defn toggle-token-set-group [{:keys [prefixed-path-str]}]
+ (ptk/reify ::toggle-token-set-group
+ ptk/WatchEvent
+ (watch [_ state _]
+ (let [changes (clt/generate-toggle-token-set-group (pcb/empty-changes) (get-tokens-lib state) prefixed-path-str)]
+ (rx/of
+ (dch/commit-changes changes)
(wtu/update-workspace-tokens))))))
(defn import-tokens-lib [lib]
@@ -210,7 +183,7 @@
(ctob/get-sets)
(first)
(:name)
- (set-selected-token-set-id-from-name))
+ (set-selected-token-set-path-from-name))
changes (-> (pcb/empty-changes it)
(pcb/with-library-data data)
(pcb/set-tokens-lib lib))]
@@ -219,14 +192,14 @@
update-token-set-change
(wtu/update-workspace-tokens))))))
-(defn delete-token-set-path [token-set-path]
+(defn delete-token-set-path [prefixed-full-set-path]
(ptk/reify ::delete-token-set-path
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-path token-set-path))]
+ (pcb/delete-token-set-path prefixed-full-set-path))]
(rx/of
(dch/commit-changes changes)
(wtu/update-workspace-tokens))))))
@@ -276,7 +249,7 @@
(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-from-name token-set-name)
+ (set-selected-token-set-path-from-name token-set-name)
(dch/commit-changes changes))))))
(defn delete-token
diff --git a/frontend/src/app/main/data/workspace/colors.cljs b/frontend/src/app/main/data/workspace/colors.cljs
index 709cc739d..43425d449 100644
--- a/frontend/src/app/main/data/workspace/colors.cljs
+++ b/frontend/src/app/main/data/workspace/colors.cljs
@@ -82,45 +82,46 @@
(assoc-in [:workspace-global :picked-shift?] shift?)))))
(defn transform-fill
- [state ids color transform]
- (let [objects (wsh/lookup-page-objects state)
+ ([state ids color transform] (transform-fill state ids color transform nil))
+ ([state ids color transform options]
+ (let [objects (wsh/lookup-page-objects state)
- is-text? #(= :text (:type (get objects %)))
- text-ids (filter is-text? ids)
- shape-ids (remove is-text? ids)
+ is-text? #(= :text (:type (get objects %)))
+ text-ids (filter is-text? ids)
+ shape-ids (remove is-text? ids)
- undo-id (js/Symbol)
+ undo-id (js/Symbol)
- attrs
- (cond-> {}
- (contains? color :color)
- (assoc :fill-color (:color color))
+ attrs
+ (cond-> {}
+ (contains? color :color)
+ (assoc :fill-color (:color color))
- (contains? color :id)
- (assoc :fill-color-ref-id (:id color))
+ (contains? color :id)
+ (assoc :fill-color-ref-id (:id color))
- (contains? color :file-id)
- (assoc :fill-color-ref-file (:file-id color))
+ (contains? color :file-id)
+ (assoc :fill-color-ref-file (:file-id color))
- (contains? color :gradient)
- (assoc :fill-color-gradient (:gradient color))
+ (contains? color :gradient)
+ (assoc :fill-color-gradient (:gradient color))
- (contains? color :opacity)
- (assoc :fill-opacity (:opacity color))
+ (contains? color :opacity)
+ (assoc :fill-opacity (:opacity color))
- (contains? color :image)
- (assoc :fill-image (:image color))
+ (contains? color :image)
+ (assoc :fill-image (:image color))
- :always
- (d/without-nils))
+ :always
+ (d/without-nils))
- transform-attrs #(transform % attrs)]
+ transform-attrs #(transform % attrs)]
- (rx/concat
- (rx/of (dwu/start-undo-transaction undo-id))
- (rx/from (map #(dwt/update-text-with-function % transform-attrs) text-ids))
- (rx/of (dwsh/update-shapes shape-ids transform-attrs))
- (rx/of (dwu/commit-undo-transaction undo-id)))))
+ (rx/concat
+ (rx/of (dwu/start-undo-transaction undo-id))
+ (rx/from (map #(dwt/update-text-with-function % transform-attrs options) text-ids))
+ (rx/of (dwsh/update-shapes shape-ids transform-attrs options))
+ (rx/of (dwu/commit-undo-transaction undo-id))))))
(defn swap-attrs [shape attr index new-index]
(let [first (get-in shape [attr index])
@@ -146,81 +147,86 @@
(rx/of (dwsh/update-shapes shape-ids transform-attrs)))))))
(defn change-fill
- [ids color position]
- (ptk/reify ::change-fill
- ptk/WatchEvent
- (watch [_ state _]
- (let [change-fn (fn [shape attrs]
- (-> shape
- (cond-> (not (contains? shape :fills))
- (assoc :fills []))
- (assoc-in [:fills position] (into {} attrs))))]
- (transform-fill state ids color change-fn)))))
+ ([ids color position] (change-fill ids color position nil))
+ ([ids color position options]
+ (ptk/reify ::change-fill
+ ptk/WatchEvent
+ (watch [_ state _]
+ (let [change-fn (fn [shape attrs]
+ (-> shape
+ (cond-> (not (contains? shape :fills))
+ (assoc :fills []))
+ (assoc-in [:fills position] (into {} attrs))))]
+ (transform-fill state ids color change-fn options))))))
(defn change-fill-and-clear
- [ids color]
- (ptk/reify ::change-fill-and-clear
- ptk/WatchEvent
- (watch [_ state _]
- (let [set (fn [shape attrs] (assoc shape :fills [attrs]))]
- (transform-fill state ids color set)))))
+ ([ids color] (change-fill-and-clear ids color nil))
+ ([ids color options]
+ (ptk/reify ::change-fill-and-clear
+ ptk/WatchEvent
+ (watch [_ state _]
+ (let [set (fn [shape attrs] (assoc shape :fills [attrs]))]
+ (transform-fill state ids color set options))))))
(defn add-fill
- [ids color]
+ ([ids color] (add-fill ids color nil))
+ ([ids color options]
- (dm/assert!
- "expected a valid color struct"
- (ctc/check-color! color))
+ (dm/assert!
+ "expected a valid color struct"
+ (ctc/check-color! color))
- (dm/assert!
- "expected a valid coll of uuid's"
- (every? uuid? ids))
+ (dm/assert!
+ "expected a valid coll of uuid's"
+ (every? uuid? ids))
- (ptk/reify ::add-fill
- ptk/WatchEvent
- (watch [_ state _]
- (let [add (fn [shape attrs]
- (-> shape
- (update :fills #(into [attrs] %))))]
- (transform-fill state ids color add)))))
+ (ptk/reify ::add-fill
+ ptk/WatchEvent
+ (watch [_ state _]
+ (let [add (fn [shape attrs]
+ (-> shape
+ (update :fills #(into [attrs] %))))]
+ (transform-fill state ids color add options))))))
(defn remove-fill
- [ids color position]
+ ([ids color position] (remove-fill ids color position nil))
+ ([ids color position options]
- (dm/assert!
- "expected a valid color struct"
- (ctc/check-color! color))
+ (dm/assert!
+ "expected a valid color struct"
+ (ctc/check-color! color))
- (dm/assert!
- "expected a valid coll of uuid's"
- (every? uuid? ids))
+ (dm/assert!
+ "expected a valid coll of uuid's"
+ (every? uuid? ids))
- (ptk/reify ::remove-fill
- ptk/WatchEvent
- (watch [_ state _]
- (let [remove-fill-by-index (fn [values index] (->> (d/enumerate values)
- (filterv (fn [[idx _]] (not= idx index)))
- (mapv second)))
+ (ptk/reify ::remove-fill
+ ptk/WatchEvent
+ (watch [_ state _]
+ (let [remove-fill-by-index (fn [values index] (->> (d/enumerate values)
+ (filterv (fn [[idx _]] (not= idx index)))
+ (mapv second)))
- remove (fn [shape _] (update shape :fills remove-fill-by-index position))]
- (transform-fill state ids color remove)))))
+ remove (fn [shape _] (update shape :fills remove-fill-by-index position))]
+ (transform-fill state ids color remove options))))))
(defn remove-all-fills
- [ids color]
+ ([ids color] (remove-all-fills ids color nil))
+ ([ids color options]
- (dm/assert!
- "expected a valid color struct"
- (ctc/check-color! color))
+ (dm/assert!
+ "expected a valid color struct"
+ (ctc/check-color! color))
- (dm/assert!
- "expected a valid coll of uuid's"
- (every? uuid? ids))
+ (dm/assert!
+ "expected a valid coll of uuid's"
+ (every? uuid? ids))
- (ptk/reify ::remove-all-fills
- ptk/WatchEvent
- (watch [_ state _]
- (let [remove-all (fn [shape _] (assoc shape :fills []))]
- (transform-fill state ids color remove-all)))))
+ (ptk/reify ::remove-all-fills
+ ptk/WatchEvent
+ (watch [_ state _]
+ (let [remove-all (fn [shape _] (assoc shape :fills []))]
+ (transform-fill state ids color remove-all options))))))
(defn change-hide-fill-on-export
[ids hide-fill-on-export]
@@ -237,56 +243,58 @@
(d/merge shape attrs)
shape))))))))
(defn change-stroke
- [ids attrs index]
- (ptk/reify ::change-stroke
- ptk/WatchEvent
- (watch [_ _ _]
- (let [color-attrs (cond-> {}
- (contains? attrs :color)
- (assoc :stroke-color (:color attrs))
+ ([ids attrs index] (change-stroke ids attrs index nil))
+ ([ids attrs index options]
+ (ptk/reify ::change-stroke
+ ptk/WatchEvent
+ (watch [_ _ _]
+ (let [color-attrs (cond-> {}
+ (contains? attrs :color)
+ (assoc :stroke-color (:color attrs))
- (contains? attrs :id)
- (assoc :stroke-color-ref-id (:id attrs))
+ (contains? attrs :id)
+ (assoc :stroke-color-ref-id (:id attrs))
- (contains? attrs :file-id)
- (assoc :stroke-color-ref-file (:file-id attrs))
+ (contains? attrs :file-id)
+ (assoc :stroke-color-ref-file (:file-id attrs))
- (contains? attrs :gradient)
- (assoc :stroke-color-gradient (:gradient attrs))
+ (contains? attrs :gradient)
+ (assoc :stroke-color-gradient (:gradient attrs))
- (contains? attrs :opacity)
- (assoc :stroke-opacity (:opacity attrs))
+ (contains? attrs :opacity)
+ (assoc :stroke-opacity (:opacity attrs))
- (contains? attrs :image)
- (assoc :stroke-image (:image attrs)))
+ (contains? attrs :image)
+ (assoc :stroke-image (:image attrs)))
- attrs (->
- (merge attrs color-attrs)
- (dissoc :image)
- (dissoc :gradient))]
+ attrs (->
+ (merge attrs color-attrs)
+ (dissoc :image)
+ (dissoc :gradient))]
- (rx/of (dwsh/update-shapes
- ids
- (fn [shape]
- (let [new-attrs (merge (get-in shape [:strokes index]) attrs)
- new-attrs (cond-> new-attrs
- (not (contains? new-attrs :stroke-width))
- (assoc :stroke-width 1)
+ (rx/of (dwsh/update-shapes
+ ids
+ (fn [shape]
+ (let [new-attrs (merge (get-in shape [:strokes index]) attrs)
+ new-attrs (cond-> new-attrs
+ (not (contains? new-attrs :stroke-width))
+ (assoc :stroke-width 1)
- (not (contains? new-attrs :stroke-style))
- (assoc :stroke-style :solid)
+ (not (contains? new-attrs :stroke-style))
+ (assoc :stroke-style :solid)
- (not (contains? new-attrs :stroke-alignment))
- (assoc :stroke-alignment :center)
+ (not (contains? new-attrs :stroke-alignment))
+ (assoc :stroke-alignment :inner)
- :always
- (d/without-nils))]
- (cond-> shape
- (not (contains? shape :strokes))
- (assoc :strokes [])
+ :always
+ (d/without-nils))]
+ (cond-> shape
+ (not (contains? shape :strokes))
+ (assoc :strokes [])
- :always
- (assoc-in [:strokes index] new-attrs))))))))))
+ :always
+ (assoc-in [:strokes index] new-attrs))))
+ options)))))))
(defn change-shadow
[ids attrs index]
diff --git a/frontend/src/app/main/data/workspace/libraries.cljs b/frontend/src/app/main/data/workspace/libraries.cljs
index 6a6ac39dd..3461de888 100644
--- a/frontend/src/app/main/data/workspace/libraries.cljs
+++ b/frontend/src/app/main/data/workspace/libraries.cljs
@@ -824,7 +824,6 @@
(rx/map #(reset-component %) (rx/from ids))
(rx/of (dwu/commit-undo-transaction undo-id)))))))
-
(defn update-component
"Modify the component linked to the shape with the given id, in the
current page, so that all attributes of its shapes are equal to the
diff --git a/frontend/src/app/main/data/workspace/modifiers.cljs b/frontend/src/app/main/data/workspace/modifiers.cljs
index cde48b4d9..c2d75abf6 100644
--- a/frontend/src/app/main/data/workspace/modifiers.cljs
+++ b/frontend/src/app/main/data/workspace/modifiers.cljs
@@ -465,8 +465,10 @@
([]
(apply-modifiers nil))
- ([{:keys [modifiers undo-transation? stack-undo? ignore-constraints ignore-snap-pixel undo-group]
- :or {undo-transation? true stack-undo? false ignore-constraints false ignore-snap-pixel false}}]
+ ([{:keys [modifiers undo-transation? stack-undo? ignore-constraints
+ ignore-snap-pixel ignore-touched undo-group]
+ :or {undo-transation? true stack-undo? false ignore-constraints false
+ ignore-snap-pixel false ignore-touched false}}]
(ptk/reify ::apply-modifiers
ptk/WatchEvent
(watch [_ state _]
@@ -515,6 +517,7 @@
{:reg-objects? true
:stack-undo? stack-undo?
:ignore-tree ignore-tree
+ :ignore-touched ignore-touched
:undo-group undo-group
;; Attributes that can change in the transform. This way we don't have to check
;; all the attributes
diff --git a/frontend/src/app/main/data/workspace/shape_layout.cljs b/frontend/src/app/main/data/workspace/shape_layout.cljs
index 7a2db7bcc..e910fafec 100644
--- a/frontend/src/app/main/data/workspace/shape_layout.cljs
+++ b/frontend/src/app/main/data/workspace/shape_layout.cljs
@@ -262,15 +262,16 @@
(rx/of (with-meta event (meta it)))))))))
(defn update-layout
- [ids changes]
- (ptk/reify ::update-layout
- ptk/WatchEvent
- (watch [_ _ _]
- (let [undo-id (js/Symbol)]
- (rx/of (dwu/start-undo-transaction undo-id)
- (dwsh/update-shapes ids (d/patch-object changes))
- (ptk/data-event :layout/update {:ids ids})
- (dwu/commit-undo-transaction undo-id))))))
+ ([ids changes] (update-layout ids changes nil))
+ ([ids changes options]
+ (ptk/reify ::update-layout
+ ptk/WatchEvent
+ (watch [_ _ _]
+ (let [undo-id (js/Symbol)]
+ (rx/of (dwu/start-undo-transaction undo-id)
+ (dwsh/update-shapes ids (d/patch-object changes) options)
+ (ptk/data-event :layout/update {:ids ids})
+ (dwu/commit-undo-transaction undo-id)))))))
(defn add-layout-track
([ids type value]
@@ -518,27 +519,28 @@
(assoc :layout-item-v-sizing :fix))))
(defn update-layout-child
- [ids changes]
- (ptk/reify ::update-layout-child
- ptk/WatchEvent
- (watch [_ state _]
- (let [objects (wsh/lookup-page-objects state)
- children-ids (->> ids (mapcat #(cfh/get-children-ids objects %)))
- parent-ids (->> ids (map #(cfh/get-parent-id objects %)))
- undo-id (js/Symbol)]
- (rx/of (dwu/start-undo-transaction undo-id)
- (dwsh/update-shapes ids (d/patch-object changes))
- (dwsh/update-shapes children-ids (partial fix-child-sizing objects changes))
- (dwsh/update-shapes
- parent-ids
- (fn [parent objects]
- (-> parent
- (fix-parent-sizing objects (set ids) changes)
- (cond-> (ctl/grid-layout? parent)
- (ctl/assign-cells objects))))
- {:with-objects? true})
- (ptk/data-event :layout/update {:ids ids})
- (dwu/commit-undo-transaction undo-id))))))
+ ([ids changes] (update-layout-child ids changes nil))
+ ([ids changes options]
+ (ptk/reify ::update-layout-child
+ ptk/WatchEvent
+ (watch [_ state _]
+ (let [objects (wsh/lookup-page-objects state)
+ children-ids (->> ids (mapcat #(cfh/get-children-ids objects %)))
+ parent-ids (->> ids (map #(cfh/get-parent-id objects %)))
+ undo-id (js/Symbol)]
+ (rx/of (dwu/start-undo-transaction undo-id)
+ (dwsh/update-shapes ids (d/patch-object changes) options)
+ (dwsh/update-shapes children-ids (partial fix-child-sizing objects changes) options)
+ (dwsh/update-shapes
+ parent-ids
+ (fn [parent objects]
+ (-> parent
+ (fix-parent-sizing objects (set ids) changes)
+ (cond-> (ctl/grid-layout? parent)
+ (ctl/assign-cells objects))))
+ (merge options {:with-objects? true}))
+ (ptk/data-event :layout/update {:ids ids})
+ (dwu/commit-undo-transaction undo-id)))))))
(defn update-grid-cells
[layout-id ids props]
diff --git a/frontend/src/app/main/data/workspace/texts.cljs b/frontend/src/app/main/data/workspace/texts.cljs
index 0ff5809be..b0c69130e 100644
--- a/frontend/src/app/main/data/workspace/texts.cljs
+++ b/frontend/src/app/main/data/workspace/texts.cljs
@@ -434,49 +434,50 @@
(txt/transform-nodes (some-fn txt/is-text-node? txt/is-paragraph-node?) migrate-node content))
(defn update-text-with-function
- [id update-node-fn]
- (ptk/reify ::update-text-with-function
- ptk/UpdateEvent
- (update [_ state]
- (d/update-in-when state [:workspace-editor-state id] ted/update-editor-current-inline-styles-fn (comp update-node-fn migrate-node)))
+ ([id update-node-fn] (update-text-with-function id update-node-fn nil))
+ ([id update-node-fn options]
+ (ptk/reify ::update-text-with-function
+ ptk/UpdateEvent
+ (update [_ state]
+ (d/update-in-when state [:workspace-editor-state id] ted/update-editor-current-inline-styles-fn (comp update-node-fn migrate-node)))
- ptk/WatchEvent
- (watch [_ state _]
- (when (or
- (and (features/active-feature? state "text-editor/v2") (nil? (:workspace-editor state)))
- (and (not (features/active-feature? state "text-editor/v2")) (nil? (get-in state [:workspace-editor-state id]))))
- (let [objects (wsh/lookup-page-objects state)
- shape (get objects id)
+ ptk/WatchEvent
+ (watch [_ state _]
+ (when (or
+ (and (features/active-feature? state "text-editor/v2") (nil? (:workspace-editor state)))
+ (and (not (features/active-feature? state "text-editor/v2")) (nil? (get-in state [:workspace-editor-state id]))))
+ (let [objects (wsh/lookup-page-objects state)
+ shape (get objects id)
- update-node? (some-fn txt/is-text-node? txt/is-paragraph-node?)
+ update-node? (some-fn txt/is-text-node? txt/is-paragraph-node?)
- shape-ids
- (cond
- (cfh/text-shape? shape) [id]
- (cfh/group-shape? shape) (cfh/get-children-ids objects id))
+ shape-ids
+ (cond
+ (cfh/text-shape? shape) [id]
+ (cfh/group-shape? shape) (cfh/get-children-ids objects id))
- update-content
- (fn [content]
- (->> content
- (migrate-content)
- (txt/transform-nodes update-node? update-node-fn)))
+ update-content
+ (fn [content]
+ (->> content
+ (migrate-content)
+ (txt/transform-nodes update-node? update-node-fn)))
- update-shape
- (fn [shape]
- (-> shape
- (dissoc :fills)
- (d/update-when :content update-content)))]
- (rx/of (dwsh/update-shapes shape-ids update-shape)))))
+ update-shape
+ (fn [shape]
+ (-> shape
+ (dissoc :fills)
+ (d/update-when :content update-content)))]
+ (rx/of (dwsh/update-shapes shape-ids update-shape options)))))
- ptk/EffectEvent
- (effect [_ state _]
- (when (features/active-feature? state "text-editor/v2")
- (let [instance (:workspace-editor state)
- styles (some-> (editor.v2/getCurrentStyle instance)
- (styles/get-styles-from-style-declaration)
- ((comp update-node-fn migrate-node))
- (styles/attrs->styles))]
- (editor.v2/applyStylesToSelection instance styles))))))
+ ptk/EffectEvent
+ (effect [_ state _]
+ (when (features/active-feature? state "text-editor/v2")
+ (let [instance (:workspace-editor state)
+ styles (some-> (editor.v2/getCurrentStyle instance)
+ (styles/get-styles-from-style-declaration)
+ ((comp update-node-fn migrate-node))
+ (styles/attrs->styles))]
+ (editor.v2/applyStylesToSelection instance styles)))))))
;; --- RESIZE UTILS
diff --git a/frontend/src/app/main/data/workspace/transforms.cljs b/frontend/src/app/main/data/workspace/transforms.cljs
index c4e2a8064..3c157498f 100644
--- a/frontend/src/app/main/data/workspace/transforms.cljs
+++ b/frontend/src/app/main/data/workspace/transforms.cljs
@@ -301,30 +301,31 @@
(defn update-dimensions
"Change size of shapes, from the sideber options form.
Will ignore pixel snap used in the options side panel"
- [ids attr value]
- (dm/assert! (number? value))
- (dm/assert!
- "expected valid coll of uuids"
- (every? uuid? ids))
- (dm/assert!
- "expected valid attr"
- (contains? #{:width :height} attr))
- (ptk/reify ::update-dimensions
- ptk/UpdateEvent
- (update [_ state]
- (let [objects (wsh/lookup-page-objects state)
- get-modifier
- (fn [shape] (ctm/change-dimensions-modifiers shape attr value))
+ ([ids attr value] (update-dimensions ids attr value nil))
+ ([ids attr value options]
+ (dm/assert! (number? value))
+ (dm/assert!
+ "expected valid coll of uuids"
+ (every? uuid? ids))
+ (dm/assert!
+ "expected valid attr"
+ (contains? #{:width :height} attr))
+ (ptk/reify ::update-dimensions
+ ptk/UpdateEvent
+ (update [_ state]
+ (let [objects (wsh/lookup-page-objects state)
+ get-modifier
+ (fn [shape] (ctm/change-dimensions-modifiers shape attr value))
- modif-tree
- (-> (dwm/build-modif-tree ids objects get-modifier)
- (gm/set-objects-modifiers objects))]
+ modif-tree
+ (-> (dwm/build-modif-tree ids objects get-modifier)
+ (gm/set-objects-modifiers objects))]
- (assoc state :workspace-modifiers modif-tree)))
+ (assoc state :workspace-modifiers modif-tree)))
- ptk/WatchEvent
- (watch [_ _ _]
- (rx/of (dwm/apply-modifiers)))))
+ ptk/WatchEvent
+ (watch [_ _ _]
+ (rx/of (dwm/apply-modifiers options))))))
(defn change-orientation
"Change orientation of shapes, from the sidebar options form.
@@ -402,7 +403,7 @@
"Rotate shapes a fixed angle, from a keyboard action."
([ids rotation]
(increase-rotation ids rotation nil))
- ([ids rotation params]
+ ([ids rotation params & options]
(ptk/reify ::increase-rotation
ptk/WatchEvent
(watch [_ state _]
@@ -411,7 +412,7 @@
shapes (->> ids (map #(get objects %)))]
(rx/concat
(rx/of (dwm/set-delta-rotation-modifiers rotation shapes params))
- (rx/of (dwm/apply-modifiers))))))))
+ (rx/of (dwm/apply-modifiers options))))))))
;; -- Move ----------------------------------------------------------
diff --git a/frontend/src/app/main/refs.cljs b/frontend/src/app/main/refs.cljs
index 04a099208..e87a6370b 100644
--- a/frontend/src/app/main/refs.cljs
+++ b/frontend/src/app/main/refs.cljs
@@ -453,8 +453,8 @@
(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-selected-token-set-path
+ (l/derived wtts/get-selected-token-set-path st/state))
(def workspace-token-set-group-selected?
(l/derived wtts/token-group-selected? st/state))
@@ -468,6 +468,14 @@
(def workspace-active-theme-paths
(l/derived (d/nilf ctob/get-active-theme-paths) tokens-lib))
+(defn token-sets-at-path-all-active
+ [prefixed-path]
+ (l/derived
+ (fn [lib]
+ (when lib
+ (ctob/sets-at-path-all-active? lib prefixed-path)))
+ tokens-lib))
+
(def workspace-active-theme-paths-no-hidden
(l/derived #(disj % ctob/hidden-token-theme-path) workspace-active-theme-paths))
diff --git a/frontend/src/app/main/ui/components/select.cljs b/frontend/src/app/main/ui/components/select.cljs
index 0187499e6..fba040fbf 100644
--- a/frontend/src/app/main/ui/components/select.cljs
+++ b/frontend/src/app/main/ui/components/select.cljs
@@ -86,6 +86,11 @@
(mf/with-effect [default-value]
(swap! state* assoc :current-value default-value))
+ (mf/with-effect [is-open?]
+ (when (and (not= 0 (mf/ref-val dropdown-direction-change*)) (= false is-open?))
+ (reset! dropdown-direction* "down")
+ (mf/set-ref-val! dropdown-direction-change* 0)))
+
(mf/with-effect [is-open? dropdown-element*]
(let [dropdown-element (mf/ref-val dropdown-element*)]
(when (and (= 0 (mf/ref-val dropdown-direction-change*)) dropdown-element)
diff --git a/frontend/src/app/main/ui/ds.cljs b/frontend/src/app/main/ui/ds.cljs
index 19623bffa..b17332ab6 100644
--- a/frontend/src/app/main/ui/ds.cljs
+++ b/frontend/src/app/main/ui/ds.cljs
@@ -17,6 +17,7 @@
[app.main.ui.ds.foundations.typography :refer [typography-list]]
[app.main.ui.ds.foundations.typography.heading :refer [heading*]]
[app.main.ui.ds.foundations.typography.text :refer [text*]]
+ [app.main.ui.ds.foundations.utilities.token.token-status :refer [token-status-icon* token-status-list]]
[app.main.ui.ds.layout.tab-switcher :refer [tab-switcher*]]
[app.main.ui.ds.notifications.toast :refer [toast*]]
[app.main.ui.ds.product.empty-placeholder :refer [empty-placeholder*]]
@@ -43,9 +44,11 @@
:Text text*
:TabSwitcher tab-switcher*
:Toast toast*
+ :TokenStatusIcon token-status-icon*
:Swatch swatch*
;; meta / misc
:meta #js {:icons (clj->js (sort icon-list))
+ :tokenStatus (clj->js (sort token-status-list))
:svgs (clj->js (sort raw-svg-list))
:typography (clj->js typography-list)}
:storybook #js {:StoryGrid sb/story-grid*
diff --git a/frontend/src/app/main/ui/ds/buttons/button.cljs b/frontend/src/app/main/ui/ds/buttons/button.cljs
index 73af4824e..a7eb02621 100644
--- a/frontend/src/app/main/ui/ds/buttons/button.cljs
+++ b/frontend/src/app/main/ui/ds/buttons/button.cljs
@@ -17,20 +17,24 @@
[:class {:optional true} :string]
[:icon {:optional true}
[:and :string [:fn #(contains? icon-list %)]]]
+ [:on-ref {:optional true} fn?]
[:variant {:optional true}
[:maybe [:enum "primary" "secondary" "ghost" "destructive"]]]])
(mf/defc button*
{::mf/props :obj
::mf/schema schema:button}
- [{:keys [variant icon children class] :rest props}]
+ [{:keys [variant icon children class on-ref] :rest props}]
(let [variant (or variant "primary")
class (dm/str class " " (stl/css-case :button true
:button-primary (= variant "primary")
:button-secondary (= variant "secondary")
:button-ghost (= variant "ghost")
:button-destructive (= variant "destructive")))
- props (mf/spread-props props {:class class})]
+ props (mf/spread-props props {:class class
+ :ref (fn [node]
+ (when on-ref
+ (on-ref node)))})]
[:> "button" props
(when icon [:> icon* {:icon-id icon :size "m"}])
[:span {:class (stl/css :label-wrapper)} children]]))
\ No newline at end of file
diff --git a/frontend/src/app/main/ui/ds/colors.scss b/frontend/src/app/main/ui/ds/colors.scss
index 6d0823c93..773ee1afc 100644
--- a/frontend/src/app/main/ui/ds/colors.scss
+++ b/frontend/src/app/main/ui/ds/colors.scss
@@ -22,6 +22,7 @@ $orange-950: #440806;
$red-200: #ffcada;
$red-400: #c80857;
+$red-500: #ff3277;
$red-950: #500124;
$pink-400: #ff6fe0;
@@ -33,6 +34,16 @@ $purple-700: #6911d4;
$purple-600-10: #8c33eb1a;
$purple-700-60: #6911d499;
+$aqua-200: #ddf7ff;
+$aqua-400: #77e1f3;
+$aqua-600: #59acbb;
+$aqua-800: #1d4464;
+
+$violet-300: #a7a9ff;
+$violet-600: #6c6dad;
+$violet-700: #484c74;
+$violet-800: #272941;
+
$blue-200: #bae3fd;
$blue-500: #0e9be9;
$blue-950: #082c49;
@@ -72,6 +83,7 @@ $grayish-red: #bfbfbf;
--color-background-warning: #{$orange-200};
--color-accent-error: #{$red-400};
--color-background-error: #{$red-200};
+ --color-foreground-error: #{$red-500};
--color-accent-info: #{$blue-500};
--color-background-info: #{$blue-200};
@@ -87,6 +99,11 @@ $grayish-red: #bfbfbf;
--color-overlay-default: #{$white-60};
--color-overlay-onboarding: #{$white-90};
--color-canvas: #{$grayish-red};
+
+ --color-token-background: #{$aqua-200};
+ --color-token-border: #{$aqua-400};
+ --color-token-accent: #{$aqua-600};
+ --color-token-foreground: #{$aqua-800};
}
:global(.default) {
@@ -104,6 +121,7 @@ $grayish-red: #bfbfbf;
--color-background-warning: #{$orange-950};
--color-accent-error: #{$red-400};
--color-background-error: #{$red-950};
+ --color-foreground-error: #{$red-500};
--color-accent-info: #{$blue-500};
--color-background-info: #{$blue-950};
@@ -119,4 +137,9 @@ $grayish-red: #bfbfbf;
--color-overlay-default: #{$gray-950-60};
--color-overlay-onboarding: #{$gray-950-90};
--color-canvas: #{$grayish-red};
+
+ --color-token-background: #{$violet-800};
+ --color-token-border: #{$violet-700};
+ --color-token-accent: #{$violet-600};
+ --color-token-foreground: #{$violet-300};
}
diff --git a/frontend/src/app/main/ui/ds/controls/input.cljs b/frontend/src/app/main/ui/ds/controls/input.cljs
index 5637b287f..279e06f9a 100644
--- a/frontend/src/app/main/ui/ds/controls/input.cljs
+++ b/frontend/src/app/main/ui/ds/controls/input.cljs
@@ -19,8 +19,7 @@
[:class {:optional true} :string]
[:icon {:optional true}
[:and :string [:fn #(contains? icon-list %)]]]
- [:type {:optional true} :string]
- [:ref {:optional true} some?]])
+ [:type {:optional true} :string]])
(mf/defc input*
{::mf/props :obj
diff --git a/frontend/src/app/main/ui/ds/foundations/assets/icon.cljs b/frontend/src/app/main/ui/ds/foundations/assets/icon.cljs
index 6bd108a5a..e3b83ab97 100644
--- a/frontend/src/app/main/ui/ds/foundations/assets/icon.cljs
+++ b/frontend/src/app/main/ui/ds/foundations/assets/icon.cljs
@@ -63,6 +63,7 @@
(def ^:icon-id boolean-flatten "boolean-flatten")
(def ^:icon-id boolean-intersection "boolean-intersection")
(def ^:icon-id boolean-union "boolean-union")
+(def ^:icon-id broken-link "broken-link")
(def ^:icon-id bug "bug")
(def ^:icon-id character-a "character-a")
(def ^:icon-id character-b "character-b")
@@ -165,6 +166,7 @@
(def ^:icon-id icon "icon")
(def ^:icon-id img "img")
(def ^:icon-id info "info")
+(def ^:icon-id import-export "import-export")
(def ^:icon-id interaction "interaction")
(def ^:icon-id join-nodes "join-nodes")
(def ^:icon-id justify-content-column-around "justify-content-column-around")
diff --git a/frontend/src/app/main/ui/ds/foundations/utilities/token/token_status.cljs b/frontend/src/app/main/ui/ds/foundations/utilities/token/token_status.cljs
new file mode 100644
index 000000000..482087f0e
--- /dev/null
+++ b/frontend/src/app/main/ui/ds/foundations/utilities/token/token_status.cljs
@@ -0,0 +1,28 @@
+(ns app.main.ui.ds.foundations.utilities.token.token-status
+ (:require-macros
+ [app.common.data.macros :as dm]
+ [app.main.style :as stl])
+ (:require
+ [app.main.ui.ds.foundations.assets.icon :refer [collect-icons]]
+ [rumext.v2 :as mf]))
+
+(def ^:icon-id token-status-partial "token-status-partial")
+(def ^:icon-id token-status-full "token-status-full")
+(def ^:icon-id token-status-non-applied "token-status-non-applied")
+
+(def token-status-list "A collection of all status" (collect-icons))
+
+(def ^:private schema:token-status-icon
+ [:map
+ [:class {:optional true} :string]
+ [:id [:and :string [:fn #(contains? token-status-list %)]]]])
+
+(mf/defc token-status-icon*
+ {::mf/props :obj
+ ::mf/schema schema:token-status-icon}
+ [{:keys [id class] :rest props}]
+ (let [class (dm/str (or class "") " " (stl/css :token-icon))
+ props (mf/spread-props props {:class class :width "14px" :height "14px"})
+ offset 0]
+ [:> "svg" props
+ [:use {:href (dm/str "#icon-" id) :width "14px" :height "14px" :x offset :y offset}]]))
diff --git a/frontend/src/app/main/ui/ds/foundations/utilities/token/token_status.mdx b/frontend/src/app/main/ui/ds/foundations/utilities/token/token_status.mdx
new file mode 100644
index 000000000..5d4fe3b59
--- /dev/null
+++ b/frontend/src/app/main/ui/ds/foundations/utilities/token/token_status.mdx
@@ -0,0 +1,31 @@
+import { Canvas, Meta } from '@storybook/blocks';
+import * as TokenStatusIconStories from "./token_status.stories"
+
+
+
+# Token status icons
+
+## Technical notes
+
+There are some SVG that are not regular icons, and that are only
+meant to be used on token components.
+
+They represent the applied status of a token over a shape.
+
+The assets are located in the `frontend/resources/images/icons` folder.
+
+### Using asset IDs
+
+For convenience, icons IDs are available in the component namespace.
+
+```clj
+(ns app.main.ui.foo
+ (:require
+ [app.main.ui.ds.foundations.utilities.token.token-status :refer [token-status-icon*] :as ts]))
+```
+
+```clj
+[:> token-status-icon*
+ {:id ts/token-status-partial
+ :class (stl/css :token-pill-icon)}]
+```
diff --git a/frontend/src/app/main/ui/ds/foundations/utilities/token/token_status.scss b/frontend/src/app/main/ui/ds/foundations/utilities/token/token_status.scss
new file mode 100644
index 000000000..207b2236a
--- /dev/null
+++ b/frontend/src/app/main/ui/ds/foundations/utilities/token/token_status.scss
@@ -0,0 +1,10 @@
+// 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
+
+.token-icon {
+ fill: currentColor;
+ stroke: none;
+}
diff --git a/frontend/src/app/main/ui/ds/foundations/utilities/token/token_status.stories.jsx b/frontend/src/app/main/ui/ds/foundations/utilities/token/token_status.stories.jsx
new file mode 100644
index 000000000..84cce010c
--- /dev/null
+++ b/frontend/src/app/main/ui/ds/foundations/utilities/token/token_status.stories.jsx
@@ -0,0 +1,23 @@
+import * as React from "react";
+import Components from "@target/components";
+
+const { TokenStatusIcon } = Components;
+const { tokenStatus } = Components.meta;
+
+export default {
+ title: "Foundations/Utilities/TokenStatus",
+ component: TokenStatusIcon,
+ argTypes: {
+ id: {
+ options: tokenStatus,
+ control: { type: "select" },
+ },
+ },
+ render: ({ ...args }) => ,
+};
+
+export const Default = {
+ args: {
+ id: "token-status-full",
+ },
+};
diff --git a/frontend/src/app/main/ui/ds/utilities/swatch.cljs b/frontend/src/app/main/ui/ds/utilities/swatch.cljs
index bfce203c8..d8cbd9657 100644
--- a/frontend/src/app/main/ui/ds/utilities/swatch.cljs
+++ b/frontend/src/app/main/ui/ds/utilities/swatch.cljs
@@ -7,70 +7,77 @@
(ns app.main.ui.ds.utilities.swatch
(:require-macros
+
[app.main.style :as stl])
+
(:require
+
[app.common.data.macros :as dm]
+ [app.common.json :as json]
+ [app.common.schema :as sm]
+ [app.common.types.color :as ct]
+ [app.config :as cfg]
+ [app.util.color :as uc]
+ [app.util.i18n :refer [tr]]
[cuerdas.core :as str]
[rumext.v2 :as mf]))
(def ^:private schema:swatch
- [:map
- [:background :string]
+ [:map {:title "SchemaSwatch"}
+ [:background {:optional true} ct/schema:color]
[:class {:optional true} :string]
- [:format {:optional true} [:enum "square" "rounded"]]
[:size {:optional true} [:enum "small" "medium"]]
[:active {:optional true} :boolean]
[:on-click {:optional true} fn?]])
-(def hex-regex #"^#(?:[0-9a-fA-F]{3}){1,2}$")
-(def rgb-regex #"^rgb\((\d{1,3}),\s*(\d{1,3}),\s*(\d{1,3})\)$")
-(def hsl-regex #"^hsl\((\d{1,3}),\s*(\d{1,3})%,\s*(\d{1,3})%\)$")
-(def hsla-regex #"^hsla\((\d{1,3}),\s*(\d{1,3})%,\s*(\d{1,3})%,\s*(0|1|0?\.\d+)\)$")
-(def rgba-regex #"^rgba\((\d{1,3}),\s*(\d{1,3}),\s*(\d{1,3}),\s*(0|1|0?\.\d+)\)$")
+(defn- color-title
+ [color-item]
+ (let [name (:name color-item)
+ path (:path color-item)
+ path-and-name (if path (str path " / " name) name)
+ gradient (:gradient color-item)
+ image (:image color-item)
+ color (:color color-item)]
-(defn- gradient? [background]
- (or
- (str/starts-with? background "linear-gradient")
- (str/starts-with? background "radial-gradient")))
+ (if (some? name)
+ (cond
+ (some? color)
+ (str/ffmt "% (%)" path-and-name color)
-(defn- color-solid? [background]
- (boolean
- (or (re-matches hex-regex background)
- (or (re-matches hsl-regex background)
- (re-matches rgb-regex background)))))
+ (some? gradient)
+ (str/ffmt "% (%)" path-and-name (uc/gradient-type->string (:type gradient)))
-(defn- color-opacity? [background]
- (boolean
- (or (re-matches hsla-regex background)
- (re-matches rgba-regex background))))
+ (some? image)
+ (str/ffmt "% (%)" path-and-name (tr "media.image"))
-(defn- extract-color-and-opacity [background]
- (cond
- (re-matches rgba-regex background)
- (let [[_ r g b a] (re-matches rgba-regex background)]
- {:color (dm/str "rgb(" r ", " g ", " b ")")
- :opacity (js/parseFloat a)})
+ :else
+ path-and-name)
- (re-matches hsla-regex background)
- (let [[_ h s l a] (re-matches hsla-regex background)]
- {:color (dm/str "hsl(" h ", " s "%, " l "%)")
- :opacity (js/parseFloat a)})
+ (cond
+ (some? color)
+ color
- :else
- {:color background
- :opacity 1.0}))
+ (some? gradient)
+ (uc/gradient-type->string (:type gradient))
+
+ (some? image)
+ (tr "media.image")))))
(mf/defc swatch*
{::mf/props :obj
- ::mf/schema schema:swatch}
- [{:keys [background on-click format size active class]
+ ::mf/schema (sm/schema schema:swatch)}
+ [{:keys [background on-click size active class]
:rest props}]
- (let [element-type (if on-click "button" "div")
- button-type (if on-click "button" nil)
- format (or format "square")
+ (let [background (if (object? background) (json/->clj background) background)
+ read-only? (nil? on-click)
+ id? (some? (:id background))
+ element-type (if read-only? "div" "button")
+ button-type (if (not read-only?) "button" nil)
size (or size "small")
active (or active false)
- {:keys [color opacity]} (extract-color-and-opacity background)
+ gradient (:gradient background)
+ image (:image background)
+ format (if id? "rounded" "square")
class (dm/str class " " (stl/css-case
:swatch true
:small (= size "small")
@@ -79,25 +86,26 @@
:active (= active true)
:interactive (= element-type "button")
:rounded (= format "rounded")))
- props (mf/spread-props props {:class class :on-click on-click :type button-type})]
+ props (mf/spread-props props {:class class
+ :on-click on-click
+ :type button-type
+ :title (color-title background)})]
[:> element-type props
(cond
- (color-solid? background)
- [:span {:class (stl/css :swatch-solid)
- :style {:background background}}]
- (color-opacity? background)
- [:span {:class (stl/css :swatch-opacity)}
- [:span {:class (stl/css :swatch-solid-side)
- :style {:background color}}]
- [:span {:class (stl/css :swatch-opacity-side)
- :style {:background color :opacity opacity}}]]
-
- (gradient? background)
+ (some? gradient)
[:span {:class (stl/css :swatch-gradient)
- :style {:background-image (str background ", repeating-conic-gradient(lightgray 0% 25%, white 0% 50%)")}}]
+ :style {:background-image (str gradient ", repeating-conic-gradient(lightgray 0% 25%, white 0% 50%)")}}]
+
+ (some? image)
+ (let [uri (cfg/resolve-file-media image)]
+ [:span {:class (stl/css :swatch-image)
+ :style {:background-image (str/ffmt "url(%)" uri)}}])
:else
- [:span {:class (stl/css :swatch-image)
- :style {:background-image (str "url('" background "'), repeating-conic-gradient(lightgray 0% 25%, white 0% 50%)")}}])]))
+ [:span {:class (stl/css :swatch-opacity)}
+ [:span {:class (stl/css :swatch-solid-side)
+ :style {:background (uc/color->background (assoc background :opacity 1))}}]
+ [:span {:class (stl/css :swatch-opacity-side)
+ :style {:background (uc/color->background background)}}]])]))
diff --git a/frontend/src/app/main/ui/ds/utilities/swatch.mdx b/frontend/src/app/main/ui/ds/utilities/swatch.mdx
index a091a4e32..bef929f07 100644
--- a/frontend/src/app/main/ui/ds/utilities/swatch.mdx
+++ b/frontend/src/app/main/ui/ds/utilities/swatch.mdx
@@ -7,56 +7,47 @@ import * as SwatchStories from "./swatch.stories";
Swatches are elements that display a color, gradient or image. They can sometimes trigger an action.
+## Background Property
+
+A swatch component can receive several props. The `background` prop is the most important and must be an object. Depending on the value of the background property we will get different variants of the component.
+
## Variants
-**Color** (`"color"`), displays a solid color. It can take a hexadecimal, an rgb or an rgba.
+If the background prop has a hex `color` value it will display a full swatch with a solid color
-**WithOpacity** (`"color"`), displays a solid color on one side and the same color with its opacity applied on the other side. It can take a hexadecimal, an rgb or an rgba.
+If the background prop has a hex `color` value and an opacity value it will display a full swatch with a solid color on one side and the same color with the opacity applied on the other side. (default opacity: 1)
-**Gradient** (`"gradient"`), displays a gradient. A gradient should be a `linear-gradient` or a `conic-gradient`.
-
-
-
-**Image** (`"image"`) the swatch could display any image.
-
-
-
-**Active** (`"active"`) displays the swatch as active while an interface related action is happening.
-
-
-
-**Size** (`"size"`) shows a bigger or smaller swatch. Accepts `small` and `medium` (_default_) sizes.
+This component can take a size property to set the size of the swatch. In this case we can set it to `small` (default size: `medium`)
-**Format** (`"format"`) displays a square or rounded swatch. Accepts `square` (_default_) and `rounded` sizes.
+With the `active` property, we can display the element as being active
-
+
+
+The element can also be interactive, and execute an external function. Typically, it launches the color picker. To make it an interactive button, it accepts an onClick function.
+
+
+
+> Due to technical issues regarding the transformation between Clojurescript and Javascript, we are unable to display:
+
+ - Swatches with gradients
+ - Library Swatches
+ - Swatches with images
## Technical Notes
-### Background
-
-The `swatch*` component accepts a `background` prop, which must be:
-
-- An hexadecimal (e.g. `#996633`)
-- An RGB (e.g. `rgb(125, 125, 0)`)
-- An RGBA (e.g. `rgba(125, 125, 0, 0.3)`)
-- A linear gradient (e.g. `linear-gradient(to right, blue, pink)`)
-- A conic gradient (e.g. `conic-gradient(red, orange, yellow, green, blue)`)
-- An image (e.g. `url(https://placecats.com/100/100)`)
-
### onClick
-> Note: If the swatch is interactive, an `aria-label` is required. More on the `Accessibility` section.
+> Note: If the swatch is interactive, an `aria-label` is required. See the `Accessibility` section for more information.
-The swatch button accepts an onClick prop that expect a function on the parent context.
+The swatch button accepts an onClick prop that expects a function on the parent context.
It should be useful for launching other tools as a color picker.
-It runs when the user clics on the swatch, or presses enter or space while focusing it.
+It is executed when the user clicks on the swatch, or presses Enter or Spacebar while focused.
### Accessibility
diff --git a/frontend/src/app/main/ui/ds/utilities/swatch.stories.jsx b/frontend/src/app/main/ui/ds/utilities/swatch.stories.jsx
index 165b7c599..08f0e3d07 100644
--- a/frontend/src/app/main/ui/ds/utilities/swatch.stories.jsx
+++ b/frontend/src/app/main/ui/ds/utilities/swatch.stories.jsx
@@ -15,11 +15,7 @@ export default {
component: Swatch,
argTypes: {
background: {
- control: { type: "text" },
- },
- format: {
- control: "select",
- options: ["square", "rounded"],
+ control: "object",
},
size: {
control: "select",
@@ -30,8 +26,7 @@ export default {
},
},
args: {
- background: "#663399",
- format: "square",
+ background: { color: "#7efff5" },
size: "medium",
active: false,
},
@@ -42,28 +37,52 @@ export const Default = {};
export const WithOpacity = {
args: {
- background: "rgba(255, 0, 0, 0.5)",
+ background: {
+ color: "#7efff5",
+ opacity: 0.5,
+ },
},
};
-export const LinearGradient = {
- args: {
- background: "linear-gradient(to right, transparent, mistyrose)",
- },
-};
+// These stories are disabled because the gradient and the UUID variants cannot be translated from cljs into JS
+// When the repo is updated to use the new version of rumext, these stories should be re-enabled and tested
+//
+// export const LinearGradient = {
+// args: {
+// background: {
+// gradient: {
+// type: "linear",
+// startX: 0,
+// startY: 0,
+// endX: 1,
+// endY: 0,
+// width: 1,
+// stops: [
+// {
+// color: "#fabada",
+// opacity: 1,
+// offset: 0,
+// },
+// {
+// color: "#cc0000",
+// opacity: 0.5,
+// offset: 1,
+// },
+// ],
+// },
+// },
+// },
+// };
-export const Image = {
- args: {
- background: "images/form/never-used.png",
- size: "medium",
- },
-};
-
-export const Rounded = {
- args: {
- format: "rounded",
- },
-};
+// export const Rounded = {
+// args: {
+// background: {
+// id: crypto.randomUUID(),
+// color: "#7efff5",
+// opacity: 0.5,
+// },
+// },
+// };
export const Small = {
args: {
@@ -74,7 +93,6 @@ export const Small = {
export const Active = {
args: {
active: true,
- background: "#CC00CC",
},
};
diff --git a/frontend/src/app/main/ui/workspace/sidebar/assets/common.cljs b/frontend/src/app/main/ui/workspace/sidebar/assets/common.cljs
index 20f4bdafa..353ef6b59 100644
--- a/frontend/src/app/main/ui/workspace/sidebar/assets/common.cljs
+++ b/frontend/src/app/main/ui/workspace/sidebar/assets/common.cljs
@@ -25,7 +25,7 @@
[app.main.ui.components.context-menu-a11y :refer [context-menu*]]
[app.main.ui.components.title-bar :refer [title-bar]]
[app.main.ui.context :as ctx]
- [app.main.ui.icons :as i]
+ [app.main.ui.ds.foundations.assets.icon :refer [icon*]]
[app.util.array :as array]
[app.util.dom :as dom]
[app.util.dom.dnd :as dnd]
@@ -119,18 +119,17 @@
:left (:left state)
:options options}])
-(mf/defc section-icon
- {::mf/wrap-props false}
- [{:keys [section]}]
+(defn section-icon
+ [section]
(case section
- :colors i/drop-icon
- :components i/component
- :typographies i/text-palette
- i/add))
+ :colors "drop"
+ :components "component"
+ :typographies "text-palette"
+ "add"))
(mf/defc asset-section
{::mf/wrap-props false}
- [{:keys [children file-id title section assets-count icon open?]}]
+ [{:keys [children file-id title section assets-count icon open? on-click]}]
(let [children (-> (array/normalize-to-array children)
(array/without-nils))
@@ -151,7 +150,7 @@
(mf/html
[:span {:class (stl/css :title-name)}
[:span {:class (stl/css :section-icon)}
- [:& (or icon section-icon) {:section section}]]
+ [:> icon* {:id (or icon (section-icon section)) :size "s"}]]
[:span {:class (stl/css :section-name)}
title]
@@ -160,17 +159,20 @@
[:div {:class (stl/css-case :asset-section true
:opened (and (< 0 assets-count)
- open?))}
+ open?))
+ :on-click on-click}
[:& title-bar
{:collapsable (< 0 assets-count)
:collapsed (not open?)
:all-clickable true
:on-collapsed on-collapsed
:add-icon-gap (= 0 assets-count)
- :class (stl/css-case :title-spacing open?)
:title title}
buttons]
- (when ^boolean open? content)]))
+ (when ^boolean (and (< 0 assets-count)
+ open?)
+ [:div {:class (stl/css-case :title-spacing open?)}
+ content])]))
(mf/defc asset-section-block
{::mf/wrap-props false}
diff --git a/frontend/src/app/main/ui/workspace/sidebar/assets/common.scss b/frontend/src/app/main/ui/workspace/sidebar/assets/common.scss
index bbc0c7d70..14ae8aa10 100644
--- a/frontend/src/app/main/ui/workspace/sidebar/assets/common.scss
+++ b/frontend/src/app/main/ui/workspace/sidebar/assets/common.scss
@@ -39,7 +39,7 @@
}
.title-spacing {
- margin-bottom: $s-4;
+ padding-block-start: $s-4;
}
.asset-section.opened {
diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/border_radius.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/border_radius.cljs
index 7809c8568..d39917b1d 100644
--- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/border_radius.cljs
+++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/border_radius.cljs
@@ -7,7 +7,10 @@
[app.main.ui.components.numeric-input :refer [numeric-input*]]
[app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
[app.main.ui.ds.foundations.assets.icon :refer [icon*]]
+ [app.main.ui.hooks :as hooks]
[app.util.i18n :as i18n :refer [tr]]
+ [beicon.v2.core :as rx]
+ [potok.v2.core :as ptk]
[rumext.v2 :as mf]))
(defn all-equal?
@@ -58,7 +61,19 @@
on-radius-r1-change #(on-radius-4-change % :r1)
on-radius-r2-change #(on-radius-4-change % :r2)
on-radius-r3-change #(on-radius-4-change % :r3)
- on-radius-r4-change #(on-radius-4-change % :r4)]
+ on-radius-r4-change #(on-radius-4-change % :r4)
+
+ expand-stream
+ (mf/with-memo []
+ (->> st/stream
+ (rx/filter (ptk/type? :expand-border-radius))))]
+
+ (hooks/use-stream
+ expand-stream
+ #(reset! radius-expanded* true))
+
+ (mf/with-effect [ids]
+ (reset! radius-expanded* false))
[:div {:class (stl/css :radius)}
(if (not radius-expanded)
@@ -117,6 +132,6 @@
:variant "ghost"
:on-click toggle-radius-mode
:aria-label (if radius-expanded
- (tr "workspace.options.radius.all-corners")
- (tr "workspace.options.radius.single-corners"))
- :icon "corner-radius"}]]))
\ No newline at end of file
+ (tr "workspace.options.radius.hide-all-corners")
+ (tr "workspace.options.radius.show-single-corners"))
+ :icon "corner-radius"}]]))
diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/measures.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/measures.cljs
index 32f31027d..c23bc4307 100644
--- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/measures.cljs
+++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/measures.cljs
@@ -282,7 +282,6 @@
(st/emit! (udw/trigger-bounding-box-cloaking ids)
(udw/increase-rotation ids value)))))
-
on-width-change #(on-size-change % :width)
on-height-change #(on-size-change % :height)
on-pos-x-change #(on-position-change % :x)
diff --git a/frontend/src/app/main/ui/workspace/tokens/changes.cljs b/frontend/src/app/main/ui/workspace/tokens/changes.cljs
index c5b6b790b..163e9f068 100644
--- a/frontend/src/app/main/ui/workspace/tokens/changes.cljs
+++ b/frontend/src/app/main/ui/workspace/tokens/changes.cljs
@@ -16,6 +16,7 @@
[app.main.data.workspace.state-helpers :as wsh]
[app.main.data.workspace.transforms :as dwt]
[app.main.data.workspace.undo :as dwu]
+ [app.main.store :as st]
[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]
@@ -95,11 +96,25 @@
(when (ctsr/can-get-border-radius? shape)
(ctsr/set-radius-to-all-corners shape value)))
{:reg-objects? true
+ :ignore-touched true
+ :attrs ctt/border-radius-keys}))
+
+(defn update-shape-radius-single-corner [value shape-ids attributes]
+ ;; NOTE: This key should be namespaced on data tokens, but these events are not there.
+ (st/emit! (ptk/data-event :expand-border-radius))
+ (dwsh/update-shapes shape-ids
+ (fn [shape]
+ (when (ctsr/can-get-border-radius? shape)
+ (ctsr/set-radius-to-single-corner shape (first attributes) value)))
+ {:reg-objects? true
+ :ignore-touched true
:attrs ctt/border-radius-keys}))
(defn update-opacity [value shape-ids]
(when (<= 0 value 1)
- (dwsh/update-shapes shape-ids #(assoc % :opacity value))))
+ (dwsh/update-shapes shape-ids
+ #(assoc % :opacity value)
+ {:ignore-touched true})))
(defn update-rotation [value shape-ids]
(ptk/reify ::update-shape-rotation
@@ -107,15 +122,7 @@
(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/can-get-border-radius? shape)
- (ctsr/set-radius-to-single-corner shape (first attributes) value)))
- {:reg-objects? true
- :attrs ctt/border-radius-keys}))
+ (udw/increase-rotation shape-ids value nil :ignore-touched true)))))
(defn update-stroke-width
[value shape-ids]
@@ -124,14 +131,15 @@
(when (seq (:strokes shape))
(assoc-in shape [:strokes 0 :stroke-width] value)))
{:reg-objects? true
+ :ignore-touched true
:attrs [:strokes]}))
(defn update-color [f value shape-ids]
- (let [color (some->> value
- (tinycolor/valid-color)
- (tinycolor/->hex)
- (str "#"))]
- (f shape-ids {:color color} 0)))
+ (when-let [color (some->> value
+ (tinycolor/valid-color)
+ (tinycolor/->hex)
+ (str "#"))]
+ (f shape-ids {:color color} 0 {:ignore-touched true})))
(defn update-fill
[value shape-ids]
@@ -141,13 +149,21 @@
[value shape-ids]
(update-color wdc/change-stroke value shape-ids))
+(defn update-fill-stroke [value shape-ids attributes]
+ (ptk/reify ::update-fill-stroke
+ ptk/WatchEvent
+ (watch [_ _ _]
+ (rx/of
+ (when (:fill attributes) (update-fill value shape-ids))
+ (when (:stroke-color attributes) (update-stroke-color value shape-ids))))))
+
(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))))))
+ (when (:width attributes) (dwt/update-dimensions shape-ids :width value {:ignore-touched true}))
+ (when (:height attributes) (dwt/update-dimensions shape-ids :height value {:ignore-touched true}))))))
(defn- attributes->layout-gap [attributes value]
(let [layout-gap (-> (set/intersection attributes #{:column-gap :row-gap})
@@ -155,7 +171,9 @@
{:layout-gap layout-gap}))
(defn update-layout-padding [value shape-ids attrs]
- (dwsl/update-layout shape-ids {:layout-padding (zipmap attrs (repeat value))}))
+ (dwsl/update-layout shape-ids
+ {:layout-padding (zipmap attrs (repeat value))}
+ {:ignore-touched true}))
(defn update-layout-spacing [value shape-ids attributes]
(ptk/reify ::update-layout-spacing
@@ -167,7 +185,9 @@
(map :id)))
layout-attributes (attributes->layout-gap attributes value)]
(rx/of
- (dwsl/update-layout layout-shape-ids layout-attributes))))))
+ (dwsl/update-layout layout-shape-ids
+ layout-attributes
+ {:ignore-touched true}))))))
(defn update-shape-position [value shape-ids attributes]
(ptk/reify ::update-shape-position
@@ -185,4 +205,4 @@
:layout-item-max-w value
:layout-item-max-h value}
(select-keys attributes))]
- (dwsl/update-layout-child shape-ids props)))))
+ (dwsl/update-layout-child shape-ids props {:ignore-touched true})))))
diff --git a/frontend/src/app/main/ui/workspace/tokens/common.scss b/frontend/src/app/main/ui/workspace/tokens/common.scss
index 668db73af..bb067e683 100644
--- a/frontend/src/app/main/ui/workspace/tokens/common.scss
+++ b/frontend/src/app/main/ui/workspace/tokens/common.scss
@@ -6,6 +6,22 @@
@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;
}
diff --git a/frontend/src/app/main/ui/workspace/tokens/context_menu.cljs b/frontend/src/app/main/ui/workspace/tokens/context_menu.cljs
index 722296c00..6f350c593 100644
--- a/frontend/src/app/main/ui/workspace/tokens/context_menu.cljs
+++ b/frontend/src/app/main/ui/workspace/tokens/context_menu.cljs
@@ -8,17 +8,19 @@
(:require-macros [app.main.style :as stl])
(:require
[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.dropdown :refer [dropdown]]
- [app.main.ui.icons :as i]
+ [app.main.ui.ds.foundations.assets.icon :refer [icon*]]
[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.i18n :refer [tr]]
[app.util.timers :as timers]
[okulary.core :as l]
[rumext.v2 :as mf]))
@@ -58,7 +60,7 @@
all-action (let [props {:attributes attributes
:token token
:shape-ids shape-ids}]
- {:title "All"
+ {:title (tr "labels.all")
:selected? all-selected?
:action #(if all-selected?
(st/emit! (wtch/unapply-token props))
@@ -96,7 +98,7 @@
vertical-padding-selected? (and
(not all-selected?)
(every? selected-pred vertical-attributes))
- padding-items [{:title "All"
+ padding-items [{:title (tr "labels.all")
:selected? all-selected?
:action (fn []
(let [props {:attributes all-padding-attrs
@@ -195,22 +197,20 @@
:stroke-width stroke-width
:dimensions (fn [context-data]
(concat
- [{:title "Spacing" :submenu :spacing}
- {:title "Sizing" :submenu :sizing}
+ [{:title "Sizing" :submenu :sizing}
+ {:title "Spacing" :submenu :spacing}
:separator
{:title "Border Radius" :submenu :border-radius}]
+ [:separator]
(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]}]
+(defn default-actions [{:keys [token selected-token-set-path]}]
(let [{:keys [modal]} (wtty/get-token-properties token)]
- [{:title "Delete Token"
- :action #(st/emit! (dt/delete-token (ctob/set-path->set-name selected-token-set-id) (:name token)))}
- {:title "Duplicate Token"
- :action #(st/emit! (dt/duplicate-token (:name token)))}
- {:title "Edit Token"
+ [{:title (tr "workspace.token.edit")
+ :no-selectable true
:action (fn [event]
(let [{:keys [key fields]} modal]
(st/emit! dt/hide-token-context-menu)
@@ -220,8 +220,16 @@
:position :right
:fields fields
:action "edit"
- :selected-token-set-id selected-token-set-id
- :token token})))}]))
+ :selected-token-set-path selected-token-set-path
+ :token token})))}
+ {:title (tr "workspace.token.duplicate")
+ :no-selectable true
+ :action #(st/emit! (dt/duplicate-token (:name token)))}
+ {:title (tr "workspace.token.delete")
+ :no-selectable true
+ :action #(st/emit! (-> selected-token-set-path
+ ctob/prefixed-set-path-string->set-name-string
+ (dt/delete-token (:name token))))}]))
(defn selection-actions [{:keys [type token] :as context-data}]
(let [with-actions (get shape-attribute-actions-map (or type (:type token)))
@@ -231,6 +239,12 @@
(when (seq attribute-actions) [:separator])
(default-actions context-data))))
+(defn submenu-actions-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)))
+
;; Components ------------------------------------------------------------------
(def tokens-menu-ref
@@ -243,98 +257,146 @@
(mf/defc menu-entry
{::mf/props :obj}
- [{:keys [title value on-click selected? children submenu-offset]}]
+ [{:keys [title value on-click selected? children submenu-offset submenu-direction no-selectable]}]
(let [submenu-ref (mf/use-ref nil)
hovering? (mf/use-ref false)
on-pointer-enter
- (mf/use-callback
+ (mf/use-fn
(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
+ (mf/use-fn
(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
+ (mf/use-fn
(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 (and (some? dom) (some? submenu-node) (= submenu-direction "up"))
+ (dom/set-css-property! submenu-node "top" "unset"))
+ (when (and (some? dom) (some? submenu-node) (= submenu-direction "down"))
+ (dom/set-css-property! submenu-node "top" (dm/str (.-offsetTop dom) "px"))))))]
+
+ (mf/use-effect
+ (mf/deps submenu-direction)
+ (fn []
+ (let [submenu-node (mf/ref-val submenu-ref)]
+ (when (= submenu-direction "up")
+ (dom/set-css-property! submenu-node "top" "unset")))))
+
+ [: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]
+ [:> icon* {:id "tick" :size "s" :class (stl/css :icon-wrapper)}])
+ [:span {:class (stl/css-case :item-text true
+ :item-with-icon-space (and
+ (not selected?)
+ (not no-selectable)))}
+ title]
(when children
[:*
- [:span {:class (stl/css :submenu-icon)} i/arrow]
+ [:> icon* {:id "arrow" :size "s"}]
[:ul {:class (stl/css :token-context-submenu)
+ :data-direction submenu-direction
:ref submenu-ref
+ ;; Under review: This distances are arbitrary,
+ ;; https://tree.taiga.io/project/penpot/task/9627
:style {:display "none"
- :top 0
- :left (str submenu-offset "px")}
+ :--dist (if (= submenu-direction "down")
+ "-80px"
+ "80px")
+ :left (dm/str submenu-offset "px")}
:on-context-menu prevent-default}
children]])]))
(mf/defc menu-tree
- [{:keys [selected-shapes] :as context-data}]
+ [{:keys [selected-shapes submenu-offset submenu-direction type] :as context-data}]
(let [entries (if (seq selected-shapes)
- (selection-actions context-data)
+ (if (some? type)
+ (submenu-actions-selection-actions context-data)
+ (selection-actions context-data))
(default-actions context-data))]
- (for [[index {:keys [title action selected? submenu] :as entry}] (d/enumerate entries)]
- [:* {:key (str title " " index)}
+ (for [[index {:keys [title action selected? submenu no-selectable] :as entry}] (d/enumerate entries)]
+ [:* {:key (dm/str title " " index)}
(cond
(= :separator entry) [:li {:class (stl/css :separator)}]
submenu [:& menu-entry {:title title
- :submenu-offset (:submenu-offset context-data)}
+ :no-selectable true
+ :submenu-direction submenu-direction
+ :submenu-offset submenu-offset}
[:& menu-tree (assoc context-data :type submenu)]]
:else [:& menu-entry
{:title title
:on-click action
+ :no-selectable no-selectable
:selected? selected?}])])))
(mf/defc token-context-menu-tree
- [{:keys [width] :as mdata}]
+ [{:keys [width direction] :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)]
+ selected-token-set-path (mf/deref refs/workspace-selected-token-set-path)]
[:ul {:class (stl/css :context-list)}
[:& menu-tree {:submenu-offset width
+ :submenu-direction direction
:token token
- :selected-token-set-id selected-token-set-id
+ :selected-token-set-path selected-token-set-path
: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)]
+ (let [mdata (mf/deref tokens-menu-ref)
+ is-open? (boolean mdata)
+ width (mf/use-state 0)
+ dropdown-ref (mf/use-ref)
+ dropdown-direction* (mf/use-state "down")
+ dropdown-direction (deref dropdown-direction*)
+ dropdown-direction-change* (mf/use-ref 0)
+ top (+ (get-in mdata [:position :y]) 5)
+ left (+ (get-in mdata [:position :x]) 5)]
+
(mf/use-effect
- (mf/deps mdata)
+ (mf/deps is-open?)
(fn []
(when-let [node (mf/ref-val dropdown-ref)]
(reset! width (.-offsetWidth node)))))
- [:& dropdown {:show (boolean mdata)
+
+ (mf/with-effect [is-open?]
+ (when (and (not= 0 (mf/ref-val dropdown-direction-change*)) (= false is-open?))
+ (reset! dropdown-direction* "down")
+ (mf/set-ref-val! dropdown-direction-change* 0)))
+
+ (mf/with-effect [is-open? dropdown-ref]
+ (let [dropdown-element (mf/ref-val dropdown-ref)]
+ (when (and (= 0 (mf/ref-val dropdown-direction-change*)) dropdown-element)
+ (let [is-outside? (dom/is-element-outside? dropdown-element)]
+ (reset! dropdown-direction* (if is-outside? "up" "down"))
+ (mf/set-ref-val! dropdown-direction-change* (inc (mf/ref-val dropdown-direction-change*)))))))
+
+ [:& dropdown {:show is-open?
:on-close #(st/emit! dt/hide-token-context-menu)}
[:div {:class (stl/css :token-context-menu)
:ref dropdown-ref
- :style {:top top :left left}
+ :data-direction dropdown-direction
+ :style {:--bottom (if (= dropdown-direction "up")
+ "40px"
+ "unset")
+ :--top (dm/str top "px")
+ :left (dm/str left "px")}
:on-context-menu prevent-default}
(when mdata
- [:& token-context-menu-tree (assoc mdata :offset @width)])]]))
+ [:& token-context-menu-tree (assoc mdata :width @width :direction dropdown-direction)])]]))
diff --git a/frontend/src/app/main/ui/workspace/tokens/context_menu.scss b/frontend/src/app/main/ui/workspace/tokens/context_menu.scss
index c1d6cc573..2d3aeab92 100644
--- a/frontend/src/app/main/ui/workspace/tokens/context_menu.scss
+++ b/frontend/src/app/main/ui/workspace/tokens/context_menu.scss
@@ -4,6 +4,7 @@
//
// Copyright (c) KALEIDOS INC
+@use "../../ds/typography.scss" as *;
@import "refactor/common-refactor.scss";
.token-context-menu {
@@ -11,6 +12,14 @@
z-index: $z-index-4;
}
+.token-context-menu[data-direction="up"] {
+ bottom: var(--bottom);
+}
+
+.token-context-menu[data-direction="down"] {
+ top: var(--top);
+}
+
.context-list,
.token-context-submenu {
@include menuShadow;
@@ -18,15 +27,18 @@
width: $s-240;
padding: $s-4;
border-radius: $br-8;
- border: $s-2 solid var(--panel-border-color);
- background-color: var(--menu-background-color);
+ border: $s-2 solid var(--color-background-quaternary);
+ background-color: var(--color-background-tertiary);
max-height: 100vh;
overflow-y: auto;
+}
- li {
- @include bodySmallTypography;
- color: var(--menu-foreground-color);
- }
+.token-context-submenu[data-direction="up"] {
+ bottom: var(--dist);
+}
+
+.token-context-submenu[data-direction="down"] {
+ top: var(--dist);
}
.token-context-submenu {
@@ -36,68 +48,46 @@
}
.separator {
- @include bodySmallTypography;
margin: $s-6;
border-block-start: $s-1 solid var(--panel-border-color);
}
.context-menu-item {
+ --context-menu-item-bg-color: none;
+ --context-menu-item-fg-color: var(--color-foreground-primary);
+ --context-menu-item-border-color: none;
+ @include use-typography("body-small");
display: flex;
align-items: center;
height: $s-28;
width: 100%;
- padding: $s-6;
+ padding: $s-8;
border-radius: $br-8;
+ color: var(--context-menu-item-fg-color);
+ background-color: var(--context-menu-item-bg-color);
+ border: $s-1 solid var(--context-menu-item-border-color);
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);
- }
+ --context-menu-item-bg-color: var(--color-background-quaternary);
}
&:focus {
- border: 1px solid var(--menu-border-color-focus);
- background-color: var(--menu-background-color-focus);
+ --context-menu-item-bg-color: var(--menu-background-color-focus);
+ --context-menu-item-border-color: var(--color-background-tertiary);
}
- &[disabled] {
- pointer-events: none;
- opacity: 0.6;
+ &[aria-selected="true"] {
+ --context-menu-item-bg-color: var(--color-background-quaternary);
}
}
+
+.item-text {
+ flex-grow: 1;
+}
+
+.item-with-icon-space {
+ padding-left: $s-20;
+}
+.icon-wrapper {
+ margin-right: $s-4;
+}
diff --git a/frontend/src/app/main/ui/workspace/tokens/form.cljs b/frontend/src/app/main/ui/workspace/tokens/form.cljs
index fcb34661d..69e63ddf3 100644
--- a/frontend/src/app/main/ui/workspace/tokens/form.cljs
+++ b/frontend/src/app/main/ui/workspace/tokens/form.cljs
@@ -9,7 +9,6 @@
(:require
[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]
@@ -191,10 +190,10 @@ Token names should only contain letters and digits separated by . characters.")}
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") "-")
+ empty-message? (tr "workspace.token.resolved-value" "-")
errors (->> (wte/humanize-errors errors)
(str/join "\n"))
- :else (dm/str (tr "workspace.token.resolved-value") result-or-errors))]
+ :else (tr "workspace.token.resolved-value" result-or-errors))]
[:> text* {:as "p"
:typography "body-small"
:class (stl/css-case :resolved-value true
@@ -204,7 +203,7 @@ Token names should only contain letters and digits separated by . characters.")}
(mf/defc form
{::mf/wrap-props false}
- [{:keys [token token-type action selected-token-set-id]}]
+ [{:keys [token token-type action selected-token-set-path]}]
(let [token (or token {:type token-type})
token-properties (wtty/get-token-properties token)
color? (wtt/color-token? token)
@@ -221,6 +220,12 @@ Token names should only contain letters and digits separated by . characters.")}
(-> (ctob/tokens-tree selected-set-tokens)
;; Allow setting editing token to it's own path
(d/dissoc-in token-path))))
+ cancel-ref (mf/use-ref nil)
+
+ on-cancel-ref
+ (mf/use-fn
+ (fn [node]
+ (mf/set-ref-val! cancel-ref node)))
;; Name
touched-name? (mf/use-state false)
@@ -234,6 +239,17 @@ Token names should only contain letters and digits separated by . characters.")}
:tokens-tree selected-set-tokens-tree})]
(m/explain schema (finalize-name value)))))
+ on-blur-name
+ (mf/use-fn
+ (mf/deps cancel-ref)
+ (fn [e]
+ (let [node (dom/get-related-target e)
+ on-cancel-btn (= node (mf/ref-val cancel-ref))]
+ (when-not on-cancel-btn
+ (let [value (dom/get-target-val e)
+ errors (validate-name value)]
+ (reset! name-errors errors))))))
+
on-update-name-debounced
(mf/use-fn
(uf/debounce (fn [e]
@@ -283,6 +299,11 @@ Token names should only contain letters and digits separated by . characters.")}
(set! (.-value (mf/ref-val value-input-ref)) hex-value)
(on-update-value-debounced hex-value)))
+ on-display-colorpicker (mf/use-fn
+ (mf/deps color-ramp-open?)
+ (fn []
+ (swap! color-ramp-open? not)))
+
value-error? (seq (:errors @token-resolve-result))
valid-value-field? (and
(not value-error?)
@@ -315,6 +336,7 @@ Token names should only contain letters and digits separated by . characters.")}
(mf/deps validate-name validate-descripion token resolved-tokens)
(fn [e]
(dom/prevent-default e)
+ (mf/set-ref-val! cancel-ref nil)
;; 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,
@@ -343,20 +365,21 @@ Token names should only contain letters and digits separated by . characters.")}
(modal/hide!))))))))
on-delete-token
(mf/use-fn
- (mf/deps selected-token-set-id)
+ (mf/deps selected-token-set-path)
(fn [e]
(dom/prevent-default e)
(modal/hide!)
- (st/emit! (dt/delete-token (ctob/set-path->set-name selected-token-set-id) (:name token)))))
+ (st/emit! (dt/delete-token (ctob/prefixed-set-path-string->set-name-string selected-token-set-path) (:name token)))))
on-cancel
(mf/use-fn
(fn [e]
+ (mf/set-ref-val! cancel-ref nil)
(dom/prevent-default e)
(modal/hide!)))]
- [:form {:class (stl/css :form-wrapper)
- :on-submit on-submit}
+ [: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")
@@ -372,7 +395,7 @@ Token names should only contain letters and digits separated by . characters.")}
:auto-focus true
:label (tr "workspace.token.token-name")
:default-value @name-ref
- :on-blur on-update-name
+ :on-blur on-blur-name
:on-change on-update-name}])
(for [error (->> (:errors @name-errors)
@@ -395,7 +418,7 @@ Token names should only contain letters and digits separated by . characters.")}
:on-blur on-update-value}
(when color?
[:> input-token-color-bullet*
- {:color @color :on-click #(swap! color-ramp-open? not)}])]
+ {:color @color :on-click on-display-colorpicker}])]
(when @color-ramp-open?
[:& ramp {:color (some-> (or @token-resolve-result (:value token))
(tinycolor/valid-color))
@@ -427,6 +450,8 @@ Token names should only contain letters and digits separated by . characters.")}
(tr "labels.delete")])
[:> button* {:on-click on-cancel
:type "button"
+ :on-ref on-cancel-ref
+ :id "token-modal-cancel"
:variant "secondary"}
(tr "labels.cancel")]
[:> button* {:type "submit"
diff --git a/frontend/src/app/main/ui/workspace/tokens/modals.cljs b/frontend/src/app/main/ui/workspace/tokens/modals.cljs
index a34ccfe61..3b2dcccdc 100644
--- a/frontend/src/app/main/ui/workspace/tokens/modals.cljs
+++ b/frontend/src/app/main/ui/workspace/tokens/modals.cljs
@@ -42,13 +42,14 @@
(mf/defc token-update-create-modal
{::mf/wrap-props false}
- [{:keys [x y position token token-type action selected-token-set-id] :as _args}]
+ [{:keys [x y position token token-type action selected-token-set-path] :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}
+ :style wrapper-style
+ :data-testid "token-update-create-modal"}
[:> icon-button* {:on-click close-modal
:class (stl/css :close-btn)
:icon i/close
@@ -56,7 +57,7 @@
:aria-label (tr "labels.close")}]
[:& form {:token token
:action action
- :selected-token-set-id selected-token-set-id
+ :selected-token-set-path selected-token-set-path
:token-type token-type}]]))
;; Modals ----------------------------------------------------------------------
diff --git a/frontend/src/app/main/ui/workspace/tokens/modals/themes.cljs b/frontend/src/app/main/ui/workspace/tokens/modals/themes.cljs
index b40762495..342b1e3d4 100644
--- a/frontend/src/app/main/ui/workspace/tokens/modals/themes.cljs
+++ b/frontend/src/app/main/ui/workspace/tokens/modals/themes.cljs
@@ -173,8 +173,6 @@
(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)
@@ -188,8 +186,6 @@
(on-toggle-dropdown))}
[:> icon* {: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)}}]]]))
@@ -254,36 +250,41 @@
[{:keys [state set-state]}]
(let [{:keys [theme-path]} @state
[_ theme-group theme-name] theme-path
+ ordered-token-sets (mf/deref refs/workspace-ordered-token-sets)
token-sets (mf/deref refs/workspace-token-sets-tree)
theme (mf/deref (refs/workspace-token-theme theme-group theme-name))
+ theme-state (mf/use-state theme)
+ lib (-> (ctob/make-tokens-lib)
+ (ctob/add-theme @theme-state)
+ (ctob/add-sets ordered-token-sets)
+ (ctob/activate-theme (:group @theme-state) (:name @theme-state)))
+
+ ;; Form / Modal handlers
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)))
+
+ on-change-field
+ (mf/use-fn
+ (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]
@@ -295,13 +296,39 @@
(mf/deps theme on-back)
(fn []
(st/emit! (wdt/delete-token-theme (:group theme) (:name theme)))
- (on-back)))]
+ (on-back)))
+
+ ;; Sets tree handlers
+ token-set-group-active?
+ (mf/use-callback
+ (mf/deps theme-state)
+ (fn [prefixed-path]
+ (ctob/sets-at-path-all-active? lib prefixed-path)))
+
+ 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-click-token-set
+ (mf/use-callback
+ (mf/deps on-toggle-token-set)
+ (fn [prefixed-set-path-str]
+ (let [set-name (ctob/prefixed-set-path-string->set-name-string prefixed-set-path-str)]
+ (on-toggle-token-set set-name))))]
[: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}
+ [:form {:on-submit on-save-form :class (stl/css :edit-theme-form)}
[:div {:class (stl/css :edit-theme-wrapper)}
[:button {:on-click on-back
:class (stl/css :back-btn)
@@ -322,7 +349,8 @@
{:token-sets token-sets
:token-set-selected? (constantly false)
:token-set-active? token-set-active?
- :on-select on-toggle-token-set
+ :token-set-group-active? token-set-group-active?
+ :on-select on-click-token-set
:on-toggle-token-set on-toggle-token-set
:origin "theme-modal"
:context sets-context/static-context}]]
diff --git a/frontend/src/app/main/ui/workspace/tokens/modals/themes.scss b/frontend/src/app/main/ui/workspace/tokens/modals/themes.scss
index 0b6058a23..a3bb43803 100644
--- a/frontend/src/app/main/ui/workspace/tokens/modals/themes.scss
+++ b/frontend/src/app/main/ui/workspace/tokens/modals/themes.scss
@@ -12,10 +12,12 @@
.modal-dialog {
@extend .modal-container-base;
- display: grid;
- grid-template-rows: auto 1fr auto;
+
+ display: flex;
+ flex-direction: column;
width: 100%;
- max-width: $s-468;
+ max-width: $s-512;
+ max-height: $s-720;
user-select: none;
}
@@ -32,6 +34,12 @@
display: flex;
flex-direction: column;
gap: $s-24;
+ overflow: auto;
+}
+
+.edit-theme-form {
+ display: flex;
+ overflow-y: auto;
}
.themes-modal-title {
@@ -101,6 +109,8 @@
.theme-group-label {
color: var(--color-foreground-secondary);
+ margin: 0 0 $s-12 0;
+ padding: 0;
}
.group-title {
@@ -114,12 +124,14 @@
display: flex;
flex-direction: column;
gap: $s-6;
+ margin: 0;
}
.theme-group-wrapper {
display: flex;
flex-direction: column;
- gap: $s-8;
+ overflow-y: auto;
+ gap: $s-32;
}
.theme-row {
@@ -146,7 +158,6 @@
}
.sets-count-button {
- text-transform: lowercase;
padding: $s-6;
padding-left: $s-12;
}
@@ -166,7 +177,7 @@
.sets-list-wrapper {
border: 1px solid color-mix(in hsl, var(--color-foreground-secondary) 30%, transparent);
border-radius: $s-8;
- overflow: hidden;
+ overflow-y: auto;
}
.sets-count-empty-button {
diff --git a/frontend/src/app/main/ui/workspace/tokens/sets.cljs b/frontend/src/app/main/ui/workspace/tokens/sets.cljs
index 0c3eb6928..8baaa0555 100644
--- a/frontend/src/app/main/ui/workspace/tokens/sets.cljs
+++ b/frontend/src/app/main/ui/workspace/tokens/sets.cljs
@@ -7,6 +7,7 @@
(ns app.main.ui.workspace.tokens.sets
(:require-macros [app.main.style :as stl])
(:require
+ [app.common.data.macros :as dm]
[app.common.types.tokens-lib :as ctob]
[app.main.data.tokens :as wdt]
[app.main.refs :as refs]
@@ -24,12 +25,24 @@
(defn on-toggle-token-set-click [token-set-name]
(st/emit! (wdt/toggle-token-set {:token-set-name token-set-name})))
+(defn on-toggle-token-set-group-click [prefixed-path-str]
+ (st/emit! (wdt/toggle-token-set-group {:prefixed-path-str prefixed-path-str})))
+
(defn on-select-token-set-click [tree-path]
- (st/emit! (wdt/set-selected-token-set-id tree-path)))
+ (st/emit! (wdt/set-selected-token-set-path tree-path)))
(defn on-update-token-set [set-name token-set]
(st/emit! (wdt/update-token-set set-name token-set)))
+(defn on-update-token-set-group [from-prefixed-path-str to-path-str]
+ (st/emit!
+ (wdt/rename-token-set-group
+ (ctob/prefixed-set-path-string->set-name-string from-prefixed-path-str)
+ (-> (ctob/prefixed-set-path-string->set-path from-prefixed-path-str)
+ (butlast)
+ (ctob/join-set-path)
+ (ctob/join-set-path-str to-path-str)))))
+
(defn on-create-token-set [_ token-set]
(st/emit! (wdt/create-token-set token-set)))
@@ -59,17 +72,28 @@
:auto-focus true
:default-value default-value}]))
-(mf/defc sets-tree-set-group
- [{:keys [label tree-depth tree-path selected? collapsed? on-select editing? on-edit on-edit-reset on-edit-submit]}]
- (let [editing?' (editing? tree-path)
- on-click
- (mf/use-fn
- (mf/deps editing? tree-path)
- (fn [event]
- (dom/stop-propagation event)
- (when-not (editing? tree-path)
- (on-select tree-path))))
+(mf/defc checkbox
+ [{:keys [checked aria-label on-click]}]
+ (let [all? (true? checked)
+ mixed? (= checked "mixed")
+ checked? (or all? mixed?)]
+ [:div {:role "checkbox"
+ :aria-checked (dm/str checked)
+ :tab-index 0
+ :class (stl/css-case :checkbox-style true
+ :checkbox-checked-style checked?)
+ :on-click on-click}
+ (when checked?
+ [:> icon*
+ {:aria-label aria-label
+ :class (stl/css :check-icon)
+ :size "s"
+ :id (if mixed? ic/remove ic/tick)}])]))
+(mf/defc sets-tree-set-group
+ [{:keys [label tree-depth tree-path active? selected? collapsed? editing? on-toggle on-edit on-edit-reset on-edit-submit]}]
+ (let [editing?' (editing? tree-path)
+ active?' (active? tree-path)
on-context-menu
(mf/use-fn
(mf/deps editing? tree-path)
@@ -80,37 +104,60 @@
(st/emit!
(wdt/show-token-set-context-menu
{:position (dom/get-client-position event)
- :tree-path tree-path})))))]
- [:div {;; :ref dref
- :role "button"
+ :prefixed-set-path tree-path})))))
+ on-collapse-click
+ (mf/use-fn
+ (fn [event]
+ (dom/stop-propagation event)
+ (swap! collapsed? not)))
+ on-double-click
+ (mf/use-fn
+ (mf/deps tree-path)
+ #(on-edit tree-path))
+ on-checkbox-click
+ (mf/use-fn
+ (mf/deps on-toggle tree-path)
+ #(on-toggle tree-path))
+ on-edit-submit'
+ (mf/use-fn
+ (mf/deps tree-path on-edit-submit)
+ #(on-edit-submit tree-path %))]
+ [:div {:role "button"
+ :data-testid "tokens-set-group-item"
:style {"--tree-depth" tree-depth}
:class (stl/css-case :set-item-container true
+ :set-item-group true
:selected-set selected?)
- :on-click on-click
- :on-context-menu on-context-menu
- :on-double-click #(on-edit tree-path)}
+ :on-context-menu on-context-menu}
[:> icon-button*
- {:on-click (fn [event]
- (.stopPropagation event)
- (swap! collapsed? not))
+ {:class (stl/css :set-item-group-collapse-button)
+ :on-click on-collapse-click
:aria-label (tr "labels.collapse")
:icon (if @collapsed? "arrow-right" "arrow-down")
:variant "action"}]
- [:> icon* {:icon-id "group"
- :class (stl/css :icon)}]
(if editing?'
[:& editing-label
{:default-value label
:on-cancel on-edit-reset
:on-create on-edit-reset
- :on-submit #(on-edit-submit)}]
- [:div {:class (stl/css :set-name)} label])]))
+ :on-submit on-edit-submit'}]
+ [:*
+ [:div {:class (stl/css :set-name)
+ :on-double-click on-double-click}
+ label]
+ [:& checkbox
+ {:on-click on-checkbox-click
+ :checked (case active?'
+ :all true
+ :partial "mixed"
+ :none false)
+ :arial-label (tr "workspace.token.select-set")}]])]))
(mf/defc sets-tree-set
[{:keys [set label tree-depth tree-path selected? on-select active? on-toggle editing? on-edit on-edit-reset on-edit-submit]}]
(let [set-name (.-name set)
editing?' (editing? tree-path)
- active?' (active? set-name)
+ active?' (some? (active? set-name))
on-click
(mf/use-fn
(mf/deps editing?' tree-path)
@@ -118,7 +165,6 @@
(dom/stop-propagation event)
(when-not editing?'
(on-select tree-path))))
-
on-context-menu
(mf/use-fn
(mf/deps editing?' tree-path)
@@ -129,44 +175,66 @@
(st/emit!
(wdt/show-token-set-context-menu
{:position (dom/get-client-position event)
- :tree-path tree-path})))))]
- [:div {;; :ref dref
- :role "button"
+ :prefixed-set-path tree-path})))))
+ on-double-click (mf/use-fn
+ (mf/deps tree-path)
+ #(on-edit tree-path))
+ on-checkbox-click (mf/use-fn
+ (mf/deps set-name)
+ (fn [event]
+ (dom/stop-propagation event)
+ (on-toggle set-name)))
+ on-edit-submit' (mf/use-fn
+ (mf/deps set on-edit-submit)
+ #(on-edit-submit set-name (ctob/update-name set %)))]
+ [:div {:role "button"
+ :data-testid "tokens-set-item"
:style {"--tree-depth" tree-depth}
:class (stl/css-case :set-item-container true
:selected-set selected?)
:on-click on-click
- :on-double-click #(on-edit tree-path)
- :on-context-menu on-context-menu}
- [:> icon* {:icon-id "document"
- :class (stl/css-case :icon true
- :root-icon (not tree-depth))}]
+ :on-context-menu on-context-menu
+ :aria-checked active?'}
+ [:> icon*
+ {:icon-id "document"
+ :class (stl/css-case :icon true
+ :root-icon (not tree-depth))}]
(if editing?'
[:& editing-label
{:default-value label
:on-cancel on-edit-reset
:on-create on-edit-reset
- :on-submit #(on-edit-submit set-name (ctob/update-name set %))}]
+ :on-submit on-edit-submit'}]
[:*
- [:div {:class (stl/css :set-name)} label]
- [:button {:on-click (fn [event]
- (dom/stop-propagation event)
- (on-toggle set-name))
- :class (stl/css-case :checkbox-style true
- :checkbox-checked-style active?')}
- (when active?'
- [:> icon* {:aria-label (tr "workspace.token.select-set")
- :class (stl/css :check-icon)
- :size "s"
- :icon-id ic/tick}])]])]))
+ [:div {:class (stl/css :set-name)
+ :on-double-click on-double-click}
+ label]
+ [:& checkbox
+ {:on-click on-checkbox-click
+ :arial-label (tr "workspace.token.select-set")
+ :checked active?'}]])]))
(mf/defc sets-tree
- [{:keys [set-path set-node tree-depth tree-path on-select selected? on-toggle active? editing? on-edit on-edit-reset on-edit-submit]
+ [{:keys [active?
+ group-active?
+ editing?
+ on-edit
+ on-edit-reset
+ on-edit-submit-set
+ on-edit-submit-group
+ on-select
+ on-toggle-set
+ on-toggle-set-group
+ selected?
+ set-node
+ set-path
+ tree-depth
+ tree-path]
:or {tree-depth 0}
:as props}]
- (let [[set-prefix set-path'] (some-> set-path (ctob/split-set-prefix))
+ (let [[set-path-prefix set-fname] (some-> set-path (ctob/split-set-str-path-prefix))
set? (instance? ctob/TokenSet set-node)
- set-group? (= ctob/set-group-prefix set-prefix)
+ set-group? (= ctob/set-group-prefix set-path-prefix)
root? (= tree-depth 0)
collapsed? (mf/use-state false)
children? (and
@@ -181,29 +249,31 @@
:active? active?
:selected? (selected? tree-path)
:on-select on-select
- :label set-path'
+ :label set-fname
:tree-path (or tree-path set-path)
:tree-depth tree-depth
:editing? editing?
- :on-toggle on-toggle
+ :on-toggle on-toggle-set
:on-edit on-edit
:on-edit-reset on-edit-reset
- :on-edit-submit on-edit-submit}]
+ :on-edit-submit on-edit-submit-set}]
set-group?
[:& sets-tree-set-group
{:selected? (selected? tree-path)
+ :active? group-active?
:on-select on-select
- :label set-path'
+ :label set-fname
:collapsed? collapsed?
:tree-path (or tree-path set-path)
:tree-depth tree-depth
:editing? editing?
+ :on-toggle on-toggle-set-group
:on-edit on-edit
:on-edit-reset on-edit-reset
- :on-edit-submit on-edit-submit}])
+ :on-edit-submit on-edit-submit-group}])
(when children?
(for [[set-path set-node] set-node
- :let [tree-path' (str (when tree-path (str tree-path "/")) set-path)]]
+ :let [tree-path' (ctob/join-set-path-str tree-path set-path)]]
[:& sets-tree
{:key tree-path'
:set-path set-path
@@ -212,29 +282,34 @@
:tree-path tree-path'
:on-select on-select
:selected? selected?
- :on-toggle on-toggle
+ :on-toggle-set on-toggle-set
+ :on-toggle-set-group on-toggle-set-group
:active? active?
+ :group-active? group-active?
:editing? editing?
:on-edit on-edit
:on-edit-reset on-edit-reset
- :on-edit-submit on-edit-submit}]))]))
+ :on-edit-submit-set on-edit-submit-set
+ :on-edit-submit-group on-update-token-set-group}]))]))
(mf/defc controlled-sets-list
[{:keys [token-sets
on-update-token-set
+ on-update-token-set-group
token-set-selected?
token-set-active?
+ token-set-group-active?
on-create-token-set
on-toggle-token-set
+ on-toggle-token-set-group
origin
on-select
context]
:as _props}]
(let [{:keys [editing? new? on-edit on-reset] :as ctx} (or context (sets-context/use-context))]
- [:ul {:class (stl/css :sets-list)}
- (if (and
- (= origin "theme-modal")
- (empty? token-sets))
+ [:fieldset {: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")]
(if (and (= origin "theme-modal")
@@ -247,11 +322,14 @@
:selected? token-set-selected?
:on-select on-select
:active? token-set-active?
- :on-toggle on-toggle-token-set
+ :group-active? token-set-group-active?
+ :on-toggle-set on-toggle-token-set
+ :on-toggle-set-group on-toggle-token-set-group
:editing? editing?
:on-edit on-edit
:on-edit-reset on-reset
- :on-edit-submit on-update-token-set}]
+ :on-edit-submit-set on-update-token-set
+ :on-edit-submit-group on-update-token-set-group}]
(when new?
[:& sets-tree-set
{:set (ctob/make-token-set :name "")
@@ -267,22 +345,28 @@
(mf/defc sets-list
[{:keys []}]
(let [token-sets (mf/deref refs/workspace-token-sets-tree)
- selected-token-set-id (mf/deref refs/workspace-selected-token-set-id)
+ selected-token-set-path (mf/deref refs/workspace-selected-token-set-path)
token-set-selected? (mf/use-fn
- (mf/deps token-sets selected-token-set-id)
+ (mf/deps token-sets selected-token-set-path)
(fn [tree-path]
- (= tree-path selected-token-set-id)))
+ (= tree-path selected-token-set-path)))
active-token-set-names (mf/deref refs/workspace-active-set-names)
token-set-active? (mf/use-fn
(mf/deps active-token-set-names)
(fn [set-name]
- (get active-token-set-names set-name)))]
+ (get active-token-set-names set-name)))
+ token-set-group-active? (mf/use-fn
+ (fn [prefixed-path]
+ @(refs/token-sets-at-path-all-active prefixed-path)))]
[:& controlled-sets-list
{:token-sets token-sets
:token-set-selected? token-set-selected?
:token-set-active? token-set-active?
+ :token-set-group-active? token-set-group-active?
:on-select on-select-token-set-click
:origin "set-panel"
:on-toggle-token-set on-toggle-token-set-click
+ :on-toggle-token-set-group on-toggle-token-set-group-click
:on-update-token-set on-update-token-set
+ :on-update-token-set-group on-update-token-set-group
:on-create-token-set on-create-token-set}]))
diff --git a/frontend/src/app/main/ui/workspace/tokens/sets.scss b/frontend/src/app/main/ui/workspace/tokens/sets.scss
index 10c7c83f0..a0a84192f 100644
--- a/frontend/src/app/main/ui/workspace/tokens/sets.scss
+++ b/frontend/src/app/main/ui/workspace/tokens/sets.scss
@@ -34,6 +34,14 @@
}
}
+.set-item-group {
+ cursor: unset;
+}
+
+.set-item-group-collapse-button {
+ cursor: pointer;
+}
+
.set-name {
@include textEllipsis;
flex-grow: 1;
diff --git a/frontend/src/app/main/ui/workspace/tokens/sets_context_menu.cljs b/frontend/src/app/main/ui/workspace/tokens/sets_context_menu.cljs
index 4fb37428a..cf3c1c412 100644
--- a/frontend/src/app/main/ui/workspace/tokens/sets_context_menu.cljs
+++ b/frontend/src/app/main/ui/workspace/tokens/sets_context_menu.cljs
@@ -7,6 +7,7 @@
(ns app.main.ui.workspace.tokens.sets-context-menu
(:require-macros [app.main.style :as stl])
(:require
+ [app.common.types.tokens-lib :as ctob]
[app.main.data.tokens :as wdt]
[app.main.refs :as refs]
[app.main.store :as st]
@@ -35,11 +36,13 @@
[:span {:class (stl/css :title)} title]])
(mf/defc menu
- [{:keys [tree-path]}]
+ [{:keys [prefixed-set-path]}]
(let [{:keys [on-edit]} (sets-context/use-context)
- edit-name (mf/use-fn #(on-edit tree-path))
- delete-set (mf/use-fn #(st/emit! (wdt/delete-token-set-path tree-path)))]
+ edit-name (mf/use-fn #(on-edit prefixed-set-path))
+ delete-set (mf/use-fn #(st/emit! (wdt/delete-token-set-path prefixed-set-path)))]
[:ul {:class (stl/css :context-list)}
+ (when (ctob/prefixed-set-path-final-group? prefixed-set-path)
+ [:& menu-entry {:title "Add set to this group" :on-click js/console.log}])
[:& menu-entry {:title (tr "labels.rename") :on-click edit-name}]
[:& menu-entry {:title (tr "labels.delete") :on-click delete-set}]]))
@@ -61,4 +64,4 @@
:ref dropdown-ref
:style {:top top :left left}
:on-context-menu prevent-default}
- [:& menu {:tree-path (:tree-path mdata)}]]]))
+ [:& menu {:prefixed-set-path (:prefixed-set-path mdata)}]]]))
diff --git a/frontend/src/app/main/ui/workspace/tokens/sidebar.cljs b/frontend/src/app/main/ui/workspace/tokens/sidebar.cljs
index 8fcbab432..b516fc806 100644
--- a/frontend/src/app/main/ui/workspace/tokens/sidebar.cljs
+++ b/frontend/src/app/main/ui/workspace/tokens/sidebar.cljs
@@ -14,7 +14,6 @@
[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*]]
@@ -22,7 +21,6 @@
[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]]
@@ -33,12 +31,12 @@
[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-pill :refer [token-pill]]
[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]))
@@ -46,54 +44,31 @@
(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))
+ :border-radius "corner-radius"
+ :color "drop"
+ :boolean "boolean-difference"
+ :opacity "percentage"
+ :rotation "rotation"
+ :spacing "padding-extended"
+ :string "text-mixed"
+ :stroke-width "stroke-size"
+ :typography "text"
+ :dimensions "expand"
+ :sizing "expand"
+ "add"))
+
+(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))}))
(mf/defc token-component
[{:keys [type tokens selected-shapes token-type-props active-theme-tokens]}]
@@ -105,9 +80,10 @@
(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)}))))
+ (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)
@@ -137,28 +113,37 @@
: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}]])
+ [:& cmm/asset-section {:icon (token-section-icon 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]]
+ [:> icon-button* {:on-click on-popover-open-click
+ :variant "ghost"
+ :icon "add"
+ :aria-label (str "Add token: " title)}]]
(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))]
+ (let [theme-token (get active-theme-tokens (wtt/token-identifier token))
+ multiple-selection (< 1 (count selected-shapes))
+ full-applied (:all-selected? (attribute-actions token selected-shapes (or all-attributes attributes)))
+ applied (wtt/shapes-token-applied? token selected-shapes (or all-attributes attributes))
+ on-token-click (fn [e]
+ (on-token-pill-click e token))
+ on-context-menu (fn [e] (on-context-menu e 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)}]))]])]]))
+ :half-applied (or (and applied multiple-selection)
+ (and applied (not full-applied)))
+ :full-applied (if multiple-selection
+ false
+ applied)
+ :on-click on-token-click
+ :on-context-menu on-context-menu}]))]])]]))
(defn sorted-token-groups
"Separate token-types into groups of `:empty` or `:filled` depending if tokens exist for that type.
@@ -235,16 +220,13 @@
on-open (mf/use-fn #(reset! open? true))]
[:& sets-context/provider {}
[:& sets-context-menu]
- [:article {:class (stl/css :sets-section-wrapper)
+ [:article {:data-testid "token-themes-sets-sidebar"
+ :class (stl/css :sets-section-wrapper)
:style {"--resize-height" (str resize-height "px")}}
[: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)}
+ [:& title-bar {:title (tr "labels.sets")}
[:& add-set-button {:on-open on-open
:style "header"}]]]
[:& theme-sets-list {:on-open on-open}]]]]))
@@ -259,30 +241,24 @@
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))]
+
+ selected-token-set-tokens (mf/deref refs/workspace-selected-token-set-tokens)
+
+ token-groups (mf/with-memo [tokens selected-token-set-tokens]
+ (-> (select-keys tokens (keys selected-token-set-tokens))
+ (sorted-token-groups)))]
[:*
[:& 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"]]))
+ (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 import-export-button
{::mf/wrap-props false}
@@ -303,6 +279,10 @@
(reset! show-menu* false)))
input-ref (mf/use-ref)
+ on-option-click
+ (mf/use-fn
+ #(.click (mf/ref-val input-ref)))
+
on-import
(fn [event]
(let [file (-> event .-target .-files (aget 0))]
@@ -318,12 +298,13 @@
:timeout 9000})))))
(set! (.-value (mf/ref-val input-ref)) "")))
on-export (fn []
- (let [tokens-blob (some-> (deref refs/tokens-lib)
+ (let [tokens-json (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)))]
+ (js/JSON.stringify nil 2))]
+ (->> (wapi/create-blob (or tokens-json "{}") "application/json")
+ (dom/trigger-download "tokens.json"))))]
+
[:div {:class (stl/css :import-export-button-wrapper)}
[:input {:type "file"
:ref input-ref
@@ -331,20 +312,20 @@
:id "file-input"
:accept ".json"
:on-change on-import}]
- [:button {:class (stl/css :import-export-button)
- :on-click open-menu}
- download-icon
- "Tokens"]
+ [:> button* {:on-click open-menu
+ :icon "import-export"
+ :variant "secondary"}
+ (tr "workspace.token.tools")]
[:& 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"]
+ :on-click on-option-click}
+ (tr "labels.import")]
[:> dropdown-menu-item* {:class (stl/css :import-export-menu-item)
:on-click on-export}
- "Export"]]]))
+ (tr "labels.export")]]]))
(mf/defc tokens-sidebar-tab
{::mf/wrap [mf/memo]
diff --git a/frontend/src/app/main/ui/workspace/tokens/sidebar.scss b/frontend/src/app/main/ui/workspace/tokens/sidebar.scss
index 023534ac2..1241b2a67 100644
--- a/frontend/src/app/main/ui/workspace/tokens/sidebar.scss
+++ b/frontend/src/app/main/ui/workspace/tokens/sidebar.scss
@@ -39,8 +39,8 @@
}
.themes-header {
+ @include use-typography("headline-small");
display: block;
- @include headlineSmallTypography;
margin-bottom: $s-8;
padding-left: $s-8;
color: var(--title-foreground-color);
@@ -80,25 +80,6 @@
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;
@@ -119,8 +100,7 @@
flex-direction: row;
align-items: end;
justify-content: end;
- padding: $s-16;
- margin-top: $s-8;
+ padding: $s-8;
background-color: var(--color-background-primary);
box-shadow: var(--el-shadow-dark);
}
@@ -129,16 +109,13 @@
@extend .button-secondary;
display: flex;
align-items: center;
+ justify-content: end;
padding: $s-6 $s-8;
text-transform: uppercase;
gap: $s-8;
+ background-color: var(--color-background-primary);
- .download-icon {
- @extend .button-icon;
- stroke: var(--icon-foreground);
- width: 20px;
- height: 20px;
- }
+ box-shadow: var(--el-shadow-dark);
}
.import-export-menu {
@@ -153,23 +130,8 @@
.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);
- }
}
}
diff --git a/frontend/src/app/main/ui/workspace/tokens/style_dictionary.cljs b/frontend/src/app/main/ui/workspace/tokens/style_dictionary.cljs
index e2c77d007..25da4fbbb 100644
--- a/frontend/src/app/main/ui/workspace/tokens/style_dictionary.cljs
+++ b/frontend/src/app/main/ui/workspace/tokens/style_dictionary.cljs
@@ -252,8 +252,10 @@
@tokens-state))
(defn use-resolved-workspace-tokens []
- (-> (mf/deref refs/workspace-selected-token-set-tokens)
- (use-resolved-tokens)))
+ (let [active-theme-tokens (mf/deref refs/workspace-active-theme-sets-tokens)
+ selected-token-set-tokens (mf/deref refs/workspace-selected-token-set-tokens)
+ prefer-selected-token-set-tokens (merge active-theme-tokens selected-token-set-tokens)]
+ (use-resolved-tokens prefer-selected-token-set-tokens)))
(defn use-active-theme-sets-tokens []
(-> (mf/deref refs/workspace-active-theme-sets-tokens)
diff --git a/frontend/src/app/main/ui/workspace/tokens/token.cljs b/frontend/src/app/main/ui/workspace/tokens/token.cljs
index 215f9ca51..3fd66ed7a 100644
--- a/frontend/src/app/main/ui/workspace/tokens/token.cljs
+++ b/frontend/src/app/main/ui/workspace/tokens/token.cljs
@@ -47,12 +47,12 @@
(= (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`."
+ "Test if `token` is applied to a `shape` with at least 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`."
+ "Test if `token` is applied to to any of `shapes` with at least one of the given `token-attributes`."
[token shapes token-attributes]
(some #(token-applied? token % token-attributes) shapes))
diff --git a/frontend/src/app/main/ui/workspace/tokens/token_pill.cljs b/frontend/src/app/main/ui/workspace/tokens/token_pill.cljs
new file mode 100644
index 000000000..92217edf2
--- /dev/null
+++ b/frontend/src/app/main/ui/workspace/tokens/token_pill.cljs
@@ -0,0 +1,59 @@
+(ns app.main.ui.workspace.tokens.token-pill
+ (:require-macros [app.main.style :as stl])
+ (:require
+ [app.common.types.tokens-lib :as ctob]
+ [app.main.ui.components.color-bullet :refer [color-bullet]]
+ [app.main.ui.ds.foundations.assets.icon :refer [icon*]]
+ [app.main.ui.ds.foundations.utilities.token.token-status :refer [token-status-icon*]]
+ [app.main.ui.workspace.tokens.style-dictionary :as sd]
+ [app.main.ui.workspace.tokens.token :as wtt]
+ [app.util.i18n :refer [tr]]
+ [cuerdas.core :as str]
+ [rumext.v2 :as mf]))
+
+(mf/defc token-pill
+ {::mf/wrap-props false}
+ [{:keys [on-click token theme-token full-applied on-context-menu half-applied]}]
+ (let [{:keys [name value resolved-value errors]} token
+ errors? (or (nil? theme-token) (and (seq errors) (seq (:errors theme-token))))
+
+ color (when (seq (ctob/find-token-value-references value))
+ (wtt/resolved-value-hex theme-token))
+
+ color (or color (wtt/resolved-value-hex token))
+
+ token-status-id (cond
+ half-applied
+ "token-status-partial"
+ full-applied
+ "token-status-full"
+ :else
+ "token-status-non-applied")]
+ [:button {:class (stl/css-case :token-pill true
+ :token-pill-applied (or half-applied full-applied)
+ :token-pill-invalid errors?
+ :token-pill-invalid-applied (and full-applied errors?))
+ :type "button"
+ :title (cond
+ errors? (sd/humanize-errors token)
+ :else (->> [(str "Token: " name)
+ (tr "workspace.token.original-value" value)
+ (tr "workspace.token.resolved-value" resolved-value)]
+ (str/join "\n")))
+ :on-click on-click
+ :on-context-menu on-context-menu
+ :disabled errors?}
+ (cond
+ color
+ [:& color-bullet {:color color
+ :mini true}]
+ errors?
+ [:> icon*
+ {:id "broken-link"
+ :class (stl/css :token-pill-icon)}]
+
+ :else
+ [:> token-status-icon*
+ {:id token-status-id
+ :class (stl/css :token-pill-icon)}])
+ name]))
\ No newline at end of file
diff --git a/frontend/src/app/main/ui/workspace/tokens/token_pill.scss b/frontend/src/app/main/ui/workspace/tokens/token_pill.scss
new file mode 100644
index 000000000..3d39b8dac
--- /dev/null
+++ b/frontend/src/app/main/ui/workspace/tokens/token_pill.scss
@@ -0,0 +1,106 @@
+// 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";
+
+.token-pill {
+ --token-pill-background: var(--color-background-tertiary);
+ --token-pill-foreground: var(--color-foreground-secondary);
+ --token-pill-border: var(--color-background-tertiary);
+ --token-pill-outline: none;
+ --token-pill-accent: var(--color-background-quaternary);
+
+ @include use-typography("code-font");
+ border: none;
+ background: none;
+ cursor: pointer;
+ display: grid;
+ grid-template-columns: auto 1fr;
+ align-items: center;
+ gap: $s-6;
+ border: $s-1 solid var(--token-pill-border);
+ outline: $s-2 solid var(--token-pill-outline);
+ height: $s-24;
+ border-radius: $br-8;
+ padding: $s-2 $s-8 $s-2 $s-4;
+ color: var(--token-pill-foreground);
+ background: var(--token-pill-background);
+
+ &:hover {
+ --token-pill-background: var(--color-token-background);
+ --token-pill-foreground: var(--color-foreground-primary);
+ --token-pill-border: var(--color-token-background);
+ --token-pill-outline: none;
+ --token-pill-accent: var(--color-background-quaternary);
+ }
+
+ &:focus-visible {
+ --token-pill-outline: var(--color-background-primary);
+ --token-pill-border: var(--color-accent-primary);
+ outline-offset: -3px;
+ }
+
+ &:disabled {
+ --token-pill-background: var(--color-background-primary);
+ --token-pill-foreground: var(--color-foreground-secondary);
+ --token-pill-border: var(--color-background-tertiary);
+ --token-pill-outline: none;
+ --token-pill-accent: var(--color-background-tertiary);
+ }
+}
+
+.token-pill-applied {
+ --token-pill-background: var(--color-token-background);
+ --token-pill-foreground: var(--color-token-foreground);
+ --token-pill-border: var(--color-token-border);
+ --token-pill-accent: var(--color-token-accent);
+
+ &:hover {
+ --token-pill-background: var(--color-token-background);
+ --token-pill-foreground: var(--color-foreground-primary);
+ --token-pill-border: var(--color-token-foreground);
+ --token-pill-accent: var(--color-token-accent);
+ }
+
+ &:focus-visible {
+ --token-pill-background: var(--color-token-background);
+ --token-pill-foreground: var(--color-token-foreground);
+ --token-pill-outline: var(--color-accent-primary);
+ --token-pill-border: var(--color-token-background);
+ --token-pill-accent: var(--color-token-accent);
+ }
+
+ &:disabled {
+ --token-pill-background: var(--color-background-primary);
+ --token-pill-foreground: var(--color-token-foreground);
+ --token-pill-border: var(--color-token-accent);
+ --token-pill-outline: none;
+ --token-pill-accent: var(--color-token-accent);
+ }
+}
+
+.token-pill-invalid,
+.token-pill-invalid-applied {
+ --token-pill-background: var(--color-background-tertiary);
+ --token-pill-foreground: var(--color-foreground-error);
+ --token-pill-border: var(--color-background-tertiary);
+ --token-pill-accent: var(--color-foreground-error);
+
+ &:hover,
+ &:focus-visible,
+ &:disabled {
+ --token-pill-background: var(--color-background-tertiary);
+ --token-pill-foreground: var(--color-foreground-error);
+ --token-pill-border: var(--color-background-tertiary);
+ --token-pill-accent: var(--color-foreground-error);
+ }
+}
+
+.token-pill-icon {
+ color: var(--token-pill-accent);
+}
diff --git a/frontend/src/app/main/ui/workspace/tokens/token_set.cljs b/frontend/src/app/main/ui/workspace/tokens/token_set.cljs
index 380a6b997..9a38202d8 100644
--- a/frontend/src/app/main/ui/workspace/tokens/token_set.cljs
+++ b/frontend/src/app/main/ui/workspace/tokens/token_set.cljs
@@ -36,15 +36,15 @@
;; === Set selection
-(defn get-selected-token-set-id [state]
- (or (get-in state [:workspace-local :selected-token-set-id])
+(defn get-selected-token-set-path [state]
+ (or (get-in state [:workspace-local :selected-token-set-path])
(some-> (get-workspace-tokens-lib state)
(ctob/get-sets)
(first)
- (ctob/get-set-path))))
+ (ctob/get-set-prefixed-path-string))))
(defn get-selected-token-set-node [state]
- (when-let [path (some-> (get-selected-token-set-id state)
+ (when-let [path (some-> (get-selected-token-set-path state)
(ctob/split-token-set-path))]
(some-> (get-workspace-tokens-lib state)
(ctob/get-in-set-tree path))))
@@ -66,5 +66,5 @@
(defn token-group-selected? [state]
(some? (get-selected-token-set-group state)))
-(defn assoc-selected-token-set-id [state id]
- (assoc-in state [:workspace-local :selected-token-set-id] id))
+(defn assoc-selected-token-set-path [state id]
+ (assoc-in state [:workspace-local :selected-token-set-path] id))
diff --git a/frontend/src/app/main/ui/workspace/tokens/token_types.cljs b/frontend/src/app/main/ui/workspace/tokens/token_types.cljs
index b8247a288..f5979add4 100644
--- a/frontend/src/app/main/ui/workspace/tokens/token_types.cljs
+++ b/frontend/src/app/main/ui/workspace/tokens/token_types.cljs
@@ -23,8 +23,9 @@
:color
{:title "Color"
- :attributes ctt/color-keys
- :on-update-shape wtch/update-fill
+ :attributes #{:fill}
+ :all-attributes ctt/color-keys
+ :on-update-shape wtch/update-fill-stroke
:modal {:key :tokens/color
:fields [{:label "Color" :key :color}]}}
diff --git a/frontend/src/app/main/ui/workspace/tokens/update.cljs b/frontend/src/app/main/ui/workspace/tokens/update.cljs
index e220a07d5..ffe23f261 100644
--- a/frontend/src/app/main/ui/workspace/tokens/update.cljs
+++ b/frontend/src/app/main/ui/workspace/tokens/update.cljs
@@ -2,8 +2,8 @@
(:require
[app.common.types.token :as ctt]
[app.main.data.workspace.shape-layout :as dwsl]
+ [app.main.data.workspace.state-helpers :as wsh]
[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]
@@ -17,10 +17,8 @@
(def filter-existing-values? false)
(def attributes->shape-update
- {#{:r1 :r2 :r3 :r4} wtch/update-shape-radius-single-corner
- #_(fn [v ids _] (wtch/update-shape-radius-all v ids))
- #{:fill} wtch/update-fill
- #{:stroke-color} wtch/update-stroke-color
+ {#{:r1 :r2 :r3 :r4} wtch/update-shape-radius-all
+ ctt/color-keys wtch/update-fill-stroke
ctt/stroke-width-keys wtch/update-stroke-width
ctt/sizing-keys wtch/update-shape-dimensions
ctt/opacity-keys wtch/update-opacity
@@ -108,8 +106,8 @@
update-infos)))
shapes-update-info))
-(defn update-tokens [resolved-tokens]
- (->> @refs/workspace-page-objects
+(defn update-tokens [state resolved-tokens]
+ (->> (wsh/lookup-page-objects state)
(collect-shapes-update-info resolved-tokens)
(actionize-shapes-update-info)))
@@ -127,5 +125,5 @@
(let [undo-id (js/Symbol)]
(rx/concat
(rx/of (dwu/start-undo-transaction undo-id))
- (update-tokens sd-tokens)
+ (update-tokens state sd-tokens)
(rx/of (dwu/commit-undo-transaction undo-id))))))))))
diff --git a/frontend/src/app/util/dom.cljs b/frontend/src/app/util/dom.cljs
index 4257e8c36..2d3b6c0b9 100644
--- a/frontend/src/app/util/dom.cljs
+++ b/frontend/src/app/util/dom.cljs
@@ -135,6 +135,12 @@
(when (some? event)
(.-target event)))
+(defn get-related-target
+ "Extract the related target from a blur or focus event instance."
+ [^js event]
+ (when (some? event)
+ (.-relatedTarget event)))
+
(defn select-target
"Extract the target from event instance and select it"
[^js event]
diff --git a/frontend/src/app/worker.cljs b/frontend/src/app/worker.cljs
index 502e3d67e..213d20ae2 100644
--- a/frontend/src/app/worker.cljs
+++ b/frontend/src/app/worker.cljs
@@ -168,7 +168,6 @@
(.removeEventListener js/self "message" on-message))
(defn ^:dev/after-load start []
- []
(set! process-message-sub (subscribe-buffer-messages))
(.addEventListener js/self "message" on-message))
diff --git a/frontend/test/frontend_tests/basic_shapes_test.cljs b/frontend/test/frontend_tests/basic_shapes_test.cljs
index 301773837..158ca488c 100644
--- a/frontend/test/frontend_tests/basic_shapes_test.cljs
+++ b/frontend/test/frontend_tests/basic_shapes_test.cljs
@@ -79,4 +79,4 @@
;; ==== Check
(println stroke')
(t/is (some? shape1'))
- (t/is (= (:stroke-alignment stroke') :center))))))))
\ No newline at end of file
+ (t/is (= (:stroke-alignment stroke') :inner))))))))
diff --git a/frontend/test/frontend_tests/helpers/state.cljs b/frontend/test/frontend_tests/helpers/state.cljs
index 068a6cce9..4027ccf29 100644
--- a/frontend/test/frontend_tests/helpers/state.cljs
+++ b/frontend/test/frontend_tests/helpers/state.cljs
@@ -57,7 +57,7 @@
(fn [cause]
(js/console.log "[error]:" cause))
(fn [_]
- (js/console.log "[complete]"))))
+ #_(js/console.debug "[complete]"))))
(doseq [event events]
(ptk/emit! store event))
diff --git a/frontend/test/frontend_tests/logic/components_and_tokens.cljs b/frontend/test/frontend_tests/logic/components_and_tokens.cljs
new file mode 100644
index 000000000..d561a75e7
--- /dev/null
+++ b/frontend/test/frontend_tests/logic/components_and_tokens.cljs
@@ -0,0 +1,426 @@
+;; 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.logic.components-and-tokens
+ (:require
+ [app.common.geom.point :as geom]
+ [app.common.math :as mth]
+ [app.common.test-helpers.components :as cthc]
+ [app.common.test-helpers.compositions :as ctho]
+ [app.common.test-helpers.files :as cthf]
+ [app.common.test-helpers.ids-map :as cthi]
+ [app.common.test-helpers.shapes :as cths]
+ [app.common.test-helpers.tokens :as ctht]
+ [app.common.types.tokens-lib :as ctob]
+ [app.main.data.tokens :as dt]
+ [app.main.data.workspace.libraries :as dwl]
+ [app.main.data.workspace.selection :as dws]
+ [app.main.data.workspace.state-helpers :as wsh]
+ [app.main.ui.workspace.tokens.changes :as wtch]
+ [app.main.ui.workspace.tokens.update :as wtu]
+ [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 thp/reset-idmap!})
+
+(defn- setup-base-file
+ []
+ (-> (cthf/sample-file :file1)
+ (ctht/add-tokens-lib)
+ (ctht/update-tokens-lib #(-> %
+ (ctob/add-set (ctob/make-token-set :name "test-token-set"))
+ (ctob/add-theme (ctob/make-token-theme :name "test-theme"
+ :sets #{"test-token-set"}))
+ (ctob/set-active-themes #{"/test-theme"})
+ (ctob/add-token-in-set "test-token-set"
+ (ctob/make-token :name "test-token-1"
+ :type :border-radius
+ :value 25))
+ (ctob/add-token-in-set "test-token-set"
+ (ctob/make-token :name "test-token-2"
+ :type :border-radius
+ :value 50))
+ (ctob/add-token-in-set "test-token-set"
+ (ctob/make-token :name "test-token-3"
+ :type :border-radius
+ :value 75))))
+ (ctho/add-frame :frame1)
+ (ctht/apply-token-to-shape :frame1 "test-token-1" [:r1 :r2 :r3 :r4] [:r1 :r2 :r3 :r4] 25)))
+
+(defn- setup-file-with-main
+ []
+ (-> (setup-base-file)
+ (cthc/make-component :component1 :frame1)))
+
+(defn- setup-file-with-copy
+ []
+ (-> (setup-file-with-main)
+ (cthc/instantiate-component :component1 :c-frame1)))
+
+(t/deftest create-component-with-token
+ (t/async
+ done
+ (let [;; ==== Setup
+ file (setup-base-file)
+ store (ths/setup-store file)
+
+ ;; ==== Action
+ events
+ [(dws/select-shape (cthi/id :frame1))
+ (dwl/add-component)]]
+
+ (ths/run-store
+ store done events
+ (fn [new-state]
+ (let [;; ==== Get
+ file' (ths/get-file-from-store new-state)
+ frame1' (cths/get-shape file' :frame1)
+ tokens-frame1' (:applied-tokens frame1')]
+
+ ;; ==== Check
+ (t/is (= (count tokens-frame1') 4))
+ (t/is (= (get tokens-frame1' :r1) "test-token-1"))
+ (t/is (= (get tokens-frame1' :r2) "test-token-1"))
+ (t/is (= (get tokens-frame1' :r3) "test-token-1"))
+ (t/is (= (get tokens-frame1' :r4) "test-token-1"))
+ (t/is (= (get frame1' :r1) 25))
+ (t/is (= (get frame1' :r2) 25))
+ (t/is (= (get frame1' :r3) 25))
+ (t/is (= (get frame1' :r4) 25))))))))
+
+(t/deftest create-copy-with-token
+ (t/async
+ done
+ (let [;; ==== Setup
+ file (setup-file-with-main)
+ store (ths/setup-store file)
+
+ ;; ==== Action
+ events
+ [(dwl/instantiate-component (:id file)
+ (cthi/id :component1)
+ (geom/point 0 0))]]
+
+ (ths/run-store
+ store done events
+ (fn [new-state]
+ (let [;; ==== Get
+ selected (wsh/lookup-selected new-state)
+ c-frame1' (wsh/lookup-shape new-state (first selected))
+ tokens-frame1' (:applied-tokens c-frame1')]
+
+ ;; ==== Check
+ (t/is (= (count tokens-frame1') 4))
+ (t/is (= (get tokens-frame1' :r1) "test-token-1"))
+ (t/is (= (get tokens-frame1' :r2) "test-token-1"))
+ (t/is (= (get tokens-frame1' :r3) "test-token-1"))
+ (t/is (= (get tokens-frame1' :r4) "test-token-1"))
+ (t/is (= (get c-frame1' :r1) 25))
+ (t/is (= (get c-frame1' :r2) 25))
+ (t/is (= (get c-frame1' :r3) 25))
+ (t/is (= (get c-frame1' :r4) 25))))))))
+
+(t/deftest change-token-in-main
+ (t/async
+ done
+ (let [;; ==== Setup
+ file (setup-file-with-copy)
+ store (ths/setup-store file)
+
+ ;; ==== Action
+ events [(wtch/apply-token {:shape-ids [(cthi/id :frame1)]
+ :attributes #{:r1 :r2 :r3 :r4}
+ :token (toht/get-token file "test-token-2")
+ :on-update-shape wtch/update-shape-radius-all})]
+
+ step2 (fn [_]
+ (let [events2 [(dwl/sync-file (:id file) (:id file))]]
+ (ths/run-store
+ store done events2
+ (fn [new-state]
+ (let [;; ==== Get
+ file' (ths/get-file-from-store new-state)
+ c-frame1' (cths/get-shape file' :c-frame1)
+ tokens-frame1' (:applied-tokens c-frame1')]
+
+ ;; ==== Check
+ (t/is (= (count tokens-frame1') 4))
+ (t/is (= (get tokens-frame1' :r1) "test-token-2"))
+ (t/is (= (get tokens-frame1' :r2) "test-token-2"))
+ (t/is (= (get tokens-frame1' :r3) "test-token-2"))
+ (t/is (= (get tokens-frame1' :r4) "test-token-2"))
+ (t/is (= (get c-frame1' :r1) 50))
+ (t/is (= (get c-frame1' :r2) 50))
+ (t/is (= (get c-frame1' :r3) 50))
+ (t/is (= (get c-frame1' :r4) 50)))))))]
+
+ (tohs/run-store-async
+ store step2 events identity))))
+
+(t/deftest remove-token-in-main
+ (t/async
+ done
+ (let [;; ==== Setup
+ file (setup-file-with-copy)
+ store (ths/setup-store file)
+
+ ;; ==== Action
+ events [(wtch/unapply-token {:shape-ids [(cthi/id :frame1)]
+ :attributes #{:r1 :r2 :r3 :r4}
+ :token (toht/get-token file "test-token-1")})]
+
+ step2 (fn [_]
+ (let [events2 [(dwl/sync-file (:id file) (:id file))]]
+ (ths/run-store
+ store done events2
+ (fn [new-state]
+ (let [;; ==== Get
+ file' (ths/get-file-from-store new-state)
+ c-frame1' (cths/get-shape file' :c-frame1)
+ tokens-frame1' (:applied-tokens c-frame1')]
+
+ ;; ==== Check
+ (t/is (= (count tokens-frame1') 0))
+ (t/is (= (get c-frame1' :r1) 25))
+ (t/is (= (get c-frame1' :r2) 25))
+ (t/is (= (get c-frame1' :r3) 25))
+ (t/is (= (get c-frame1' :r4) 25)))))))]
+
+ (tohs/run-store-async
+ store step2 events identity))))
+
+(t/deftest modify-token
+ (t/async
+ done
+ (let [;; ==== Setup
+ file (setup-file-with-copy)
+ store (ths/setup-store file)
+
+ ;; ==== Action
+ events [(dt/update-create-token {:token (ctob/make-token :name "test-token-1"
+ :type :border-radius
+ :value 66)
+ :prev-token-name "test-token-1"})]
+
+ step2 (fn [_]
+ (let [events2 [(wtu/update-workspace-tokens)
+ (dwl/sync-file (:id file) (:id file))]]
+ (tohs/run-store-async
+ store done events2
+ (fn [new-state]
+ (let [;; ==== Get
+ file' (ths/get-file-from-store new-state)
+ c-frame1' (cths/get-shape file' :c-frame1)
+ tokens-frame1' (:applied-tokens c-frame1')]
+
+ ;; ==== Check
+ (t/is (= (count tokens-frame1') 4))
+ (t/is (= (get tokens-frame1' :r1) "test-token-1"))
+ (t/is (= (get tokens-frame1' :r2) "test-token-1"))
+ (t/is (= (get tokens-frame1' :r3) "test-token-1"))
+ (t/is (= (get tokens-frame1' :r4) "test-token-1"))
+ (t/is (= (get c-frame1' :r1) 66))
+ (t/is (= (get c-frame1' :r2) 66))
+ (t/is (= (get c-frame1' :r3) 66))
+ (t/is (= (get c-frame1' :r4) 66)))))))]
+
+ (tohs/run-store-async
+ store step2 events identity))))
+
+(t/deftest change-token-in-copy-then-change-main
+ (t/async
+ done
+ (let [;; ==== Setup
+ file (setup-file-with-copy)
+ store (ths/setup-store file)
+
+ ;; ==== Action
+ events [(wtch/apply-token {:shape-ids [(cthi/id :c-frame1)]
+ :attributes #{:r1 :r2 :r3 :r4}
+ :token (toht/get-token file "test-token-2")
+ :on-update-shape wtch/update-shape-radius-all})
+ (wtch/apply-token {:shape-ids [(cthi/id :frame1)]
+ :attributes #{:r1 :r2 :r3 :r4}
+ :token (toht/get-token file "test-token-3")
+ :on-update-shape wtch/update-shape-radius-all})]
+
+ step2 (fn [_]
+ (let [events2 [(dwl/sync-file (:id file) (:id file))]]
+ (ths/run-store
+ store done events2
+ (fn [new-state]
+ (let [;; ==== Get
+ file' (ths/get-file-from-store new-state)
+ c-frame1' (cths/get-shape file' :c-frame1)
+ tokens-frame1' (:applied-tokens c-frame1')]
+
+ ;; ==== Check
+ (t/is (= (count tokens-frame1') 4))
+ (t/is (= (get tokens-frame1' :r1) "test-token-2"))
+ (t/is (= (get tokens-frame1' :r2) "test-token-2"))
+ (t/is (= (get tokens-frame1' :r3) "test-token-2"))
+ (t/is (= (get tokens-frame1' :r4) "test-token-2"))
+ (t/is (= (get c-frame1' :r1) 50))
+ (t/is (= (get c-frame1' :r2) 50))
+ (t/is (= (get c-frame1' :r3) 50))
+ (t/is (= (get c-frame1' :r4) 50)))))))]
+
+ (tohs/run-store-async
+ store step2 events identity))))
+
+(t/deftest remove-token-in-copy-then-change-main
+ (t/async
+ done
+ (let [;; ==== Setup
+ file (setup-file-with-copy)
+ store (ths/setup-store file)
+
+ ;; ==== Action
+ events [(wtch/unapply-token {:shape-ids [(cthi/id :c-frame1)]
+ :attributes #{:r1 :r2 :r3 :r4}
+ :token (toht/get-token file "test-token-1")})
+ (wtch/apply-token {:shape-ids [(cthi/id :frame1)]
+ :attributes #{:r1 :r2 :r3 :r4}
+ :token (toht/get-token file "test-token-3")
+ :on-update-shape wtch/update-shape-radius-all})]
+
+ step2 (fn [_]
+ (let [events2 [(dwl/sync-file (:id file) (:id file))]]
+ (ths/run-store
+ store done events2
+ (fn [new-state]
+ (let [;; ==== Get
+ file' (ths/get-file-from-store new-state)
+ c-frame1' (cths/get-shape file' :c-frame1)
+ tokens-frame1' (:applied-tokens c-frame1')]
+
+ ;; ==== Check
+ (t/is (= (count tokens-frame1') 0))
+ (t/is (= (get c-frame1' :r1) 25))
+ (t/is (= (get c-frame1' :r2) 25))
+ (t/is (= (get c-frame1' :r3) 25))
+ (t/is (= (get c-frame1' :r4) 25)))))))]
+
+ (tohs/run-store-async
+ store step2 events identity))))
+
+(t/deftest modify-token-all-types
+ (t/async
+ done
+ (let [;; ==== Setup
+ file (-> (cthf/sample-file :file1)
+ (ctht/add-tokens-lib)
+ (ctht/update-tokens-lib #(-> %
+ (ctob/add-set (ctob/make-token-set :name "test-token-set"))
+ (ctob/add-theme (ctob/make-token-theme :name "test-theme"
+ :sets #{"test-token-set"}))
+ (ctob/set-active-themes #{"/test-theme"})
+ (ctob/add-token-in-set "test-token-set"
+ (ctob/make-token :name "token-radius"
+ :type :border-radius
+ :value 10))
+ (ctob/add-token-in-set "test-token-set"
+ (ctob/make-token :name "token-rotation"
+ :type :rotation
+ :value 30))
+ (ctob/add-token-in-set "test-token-set"
+ (ctob/make-token :name "token-opacity"
+ :type :opacity
+ :value 0.7))
+ (ctob/add-token-in-set "test-token-set"
+ (ctob/make-token :name "token-stroke-width"
+ :type :stroke-width
+ :value 2))
+ (ctob/add-token-in-set "test-token-set"
+ (ctob/make-token :name "token-color"
+ :type :color
+ :value "#00ff00"))
+ (ctob/add-token-in-set "test-token-set"
+ (ctob/make-token :name "token-dimensions"
+ :type :dimensions
+ :value 100))))
+ (ctho/add-frame :frame1)
+ (ctht/apply-token-to-shape :frame1 "token-radius" [:r1 :r2 :r3 :r4] [:r1 :r2 :r3 :r4] 10)
+ (ctht/apply-token-to-shape :frame1 "token-rotation" [:rotation] [:rotation] 30)
+ (ctht/apply-token-to-shape :frame1 "token-opacity" [:opacity] [:opacity] 0.7)
+ (ctht/apply-token-to-shape :frame1 "token-stroke-width" [:stroke-width] [:stroke-width] 2)
+ (ctht/apply-token-to-shape :frame1 "token-color" [:stroke-color] [:stroke-color] "#00ff00")
+ (ctht/apply-token-to-shape :frame1 "token-color" [:fill] [:fill] "#00ff00")
+ (ctht/apply-token-to-shape :frame1 "token-dimensions" [:width :height] [:width :height] 100)
+ (cthc/make-component :component1 :frame1)
+ (cthc/instantiate-component :component1 :c-frame1))
+ store (ths/setup-store file)
+
+ ;; ==== Action
+ events [(dt/update-create-token {:token (ctob/make-token :name "token-radius"
+ :type :border-radius
+ :value 30)
+ :prev-token-name "token-radius"})
+ (dt/update-create-token {:token (ctob/make-token :name "token-rotation"
+ :type :rotation
+ :value 45)
+ :prev-token-name "token-rotation"})
+ (dt/update-create-token {:token (ctob/make-token :name "token-opacity"
+ :type :opacity
+ :value 0.9)
+ :prev-token-name "token-opacity"})
+ (dt/update-create-token {:token (ctob/make-token :name "token-stroke-width"
+ :type :stroke-width
+ :value 8)
+ :prev-token-name "token-stroke-width"})
+ (dt/update-create-token {:token (ctob/make-token :name "token-color"
+ :type :color
+ :value "#ff0000")
+ :prev-token-name "token-color"})
+ (dt/update-create-token {:token (ctob/make-token :name "token-dimensions"
+ :type :dimensions
+ :value 200)
+ :prev-token-name "token-dimensions"})]
+
+ step2 (fn [_]
+ (let [events2 [(wtu/update-workspace-tokens)
+ (dwl/sync-file (:id file) (:id file))]]
+ (tohs/run-store-async
+ store done events2
+ (fn [new-state]
+ (let [;; ==== Get
+ file' (ths/get-file-from-store new-state)
+ frame1' (cths/get-shape file' :frame1)
+ c-frame1' (cths/get-shape file' :c-frame1)
+ tokens-frame1' (:applied-tokens c-frame1')]
+
+ ;; ==== Check
+ (t/is (= (count tokens-frame1') 11))
+ (t/is (= (get tokens-frame1' :r1) "token-radius"))
+ (t/is (= (get tokens-frame1' :r2) "token-radius"))
+ (t/is (= (get tokens-frame1' :r3) "token-radius"))
+ (t/is (= (get tokens-frame1' :r4) "token-radius"))
+ (t/is (= (get tokens-frame1' :rotation) "token-rotation"))
+ (t/is (= (get tokens-frame1' :opacity) "token-opacity"))
+ (t/is (= (get tokens-frame1' :stroke-width) "token-stroke-width"))
+ (t/is (= (get tokens-frame1' :stroke-color) "token-color"))
+ (t/is (= (get tokens-frame1' :fill) "token-color"))
+ (t/is (= (get tokens-frame1' :width) "token-dimensions"))
+ (t/is (= (get tokens-frame1' :height) "token-dimensions"))
+ (t/is (= (get c-frame1' :r1) 30))
+ (t/is (= (get c-frame1' :r2) 30))
+ (t/is (= (get c-frame1' :r3) 30))
+ (t/is (= (get c-frame1' :r4) 30))
+ (t/is (= (get c-frame1' :rotation) 45))
+ (t/is (= (get c-frame1' :opacity) 0.9))
+ (t/is (= (get-in c-frame1' [:strokes 0 :stroke-width]) 8))
+ (t/is (= (get-in c-frame1' [:strokes 0 :stroke-color]) "#ff0000"))
+ (t/is (= (get-in c-frame1' [:fills 0 :fill-color]) "#ff0000"))
+ (t/is (mth/close? (get c-frame1' :width) 200))
+ (t/is (mth/close? (get c-frame1' :height) 200))
+
+ (t/is (empty? (:touched c-frame1'))))))))]
+
+ (tohs/run-store-async
+ store step2 events identity))))
\ No newline at end of file
diff --git a/frontend/test/frontend_tests/runner.cljs b/frontend/test/frontend_tests/runner.cljs
index a42eb7203..bca0112e1 100644
--- a/frontend/test/frontend_tests/runner.cljs
+++ b/frontend/test/frontend_tests/runner.cljs
@@ -4,6 +4,7 @@
[frontend-tests.basic-shapes-test]
[frontend-tests.helpers-shapes-test]
[frontend-tests.logic.comp-remove-swap-slots-test]
+ [frontend-tests.logic.components-and-tokens]
[frontend-tests.logic.copying-and-duplicating-test]
[frontend-tests.logic.frame-guides-test]
[frontend-tests.logic.groups-test]
@@ -28,6 +29,7 @@
(t/run-tests
'frontend-tests.helpers-shapes-test
'frontend-tests.logic.comp-remove-swap-slots-test
+ 'frontend-tests.logic.components-and-tokens
'frontend-tests.logic.copying-and-duplicating-test
'frontend-tests.logic.frame-guides-test
'frontend-tests.logic.groups-test
diff --git a/frontend/test/frontend_tests/tokens/helpers/tokens.cljs b/frontend/test/frontend_tests/tokens/helpers/tokens.cljs
index 29316a1fa..5bfe1ed70 100644
--- a/frontend/test/frontend_tests/tokens/helpers/tokens.cljs
+++ b/frontend/test/frontend_tests/tokens/helpers/tokens.cljs
@@ -4,11 +4,6 @@
[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)
diff --git a/frontend/test/frontend_tests/tokens/logic/token_actions_test.cljs b/frontend/test/frontend_tests/tokens/logic/token_actions_test.cljs
index 1eae92e5f..08376d33f 100644
--- a/frontend/test/frontend_tests/tokens/logic/token_actions_test.cljs
+++ b/frontend/test/frontend_tests/tokens/logic/token_actions_test.cljs
@@ -122,6 +122,38 @@
(t/testing "while :r4 was kept with borderRadius.sm"
(t/is (= (:r4 (:applied-tokens rect-1')) (:name token-sm)))))))))))
+(t/deftest test-apply-color
+ (t/testing "applies color token and updates the shape fill and stroke-color"
+ (t/async
+ done
+ (let [color-token {:name "color.primary"
+ :value "red"
+ :type :color}
+ file (-> (setup-file-with-tokens)
+ (update-in [:data :tokens-lib]
+ #(ctob/add-token-in-set % "Set A" (ctob/make-token color-token))))
+ store (ths/setup-store file)
+ rect-1 (cths/get-shape file :rect-1)
+ events [(wtch/apply-token {:shape-ids [(:id rect-1)]
+ :attributes #{:color}
+ :token (toht/get-token file "color.primary")
+ :on-update-shape wtch/update-fill})
+ (wtch/apply-token {:shape-ids [(:id rect-1)]
+ :attributes #{:stroke-color}
+ :token (toht/get-token file "color.primary")
+ :on-update-shape wtch/update-stroke-color})]]
+ (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 (= (:fill (:applied-tokens rect-1')) (:name token-target')))
+ (t/is (= (get-in rect-1' [:fills 0 :fill-color]) "#ff0000"))
+ (t/is (= (:stroke (:applied-tokens rect-1')) (:name token-target')))
+ (t/is (= (get-in rect-1' [:strokes 0 :stroke-color]) "#ff0000")))))))))
+
(t/deftest test-apply-dimensions
(t/testing "applies dimensions token and updates the shapes width and height"
(t/async
diff --git a/frontend/translations/en.po b/frontend/translations/en.po
index a69ddf28b..14e090feb 100644
--- a/frontend/translations/en.po
+++ b/frontend/translations/en.po
@@ -1756,6 +1756,10 @@ msgstr "Expired"
msgid "labels.export"
msgstr "Export"
+#: src/app/main/ui/exports/assets.cljs:177
+msgid "labels.import"
+msgstr "Import"
+
#: src/app/main/ui/settings/feedback.cljs:48
msgid "labels.feedback-disabled"
msgstr "Feedback disabled"
@@ -5466,12 +5470,12 @@ msgid "workspace.options.radius-top-right"
msgstr "Top right"
#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs:639
-msgid "workspace.options.radius.all-corners"
-msgstr "All corners"
+msgid "workspace.options.radius.hide-all-corners"
+msgstr "Collapse independent radius"
#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs:640
-msgid "workspace.options.radius.single-corners"
-msgstr "Independent corners"
+msgid "workspace.options.radius.show-single-corners"
+msgstr "Show independent radius"
#: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs:184
msgid "workspace.options.recent-fonts"
@@ -6700,6 +6704,202 @@ msgstr "Open version menu"
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, src/app/main/ui/workspace/tokens/token_pill.cljs
+msgid "workspace.token.resolved-value"
+msgstr "Resolved value: %s"
+
+#: src/app/main/ui/workspace/tokens/form.cljs
+msgid "workspace.token.token-name"
+msgstr "Name"
+
+#: src/app/main/ui/workspace/tokens/form.cljs
+msgid "workspace.token.enter-token-name"
+msgstr "Enter %s token name"
+
+#: src/app/main/ui/workspace/tokens/form.cljs
+msgid "workspace.token.token-value"
+msgstr "Value"
+
+#: src/app/main/ui/workspace/tokens/form.cljs
+msgid "workspace.token.enter-token-value"
+msgstr "Enter token value or alias"
+
+#: src/app/main/ui/workspace/tokens/form.cljs
+msgid "workspace.token.token-description"
+msgstr "Description"
+
+#: src/app/main/ui/workspace/tokens/form.cljs
+msgid "workspace.token.enter-token-description"
+msgstr "Add a description (optional)"
+
+#: src/app/main/ui/workspace/tokens/sidebar.cljs
+msgid "workspace.token.original-value"
+msgstr "Original value: %s"
+
+#: 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/sidebar.cljs
+msgid "workspace.token.tools"
+msgstr "Tools"
+
+#: 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."
+
+#: src/app/main/ui/workspace/tokens/context_menu.cljs
+msgid "workspace.token.delete"
+msgstr "Delete token"
+
+#: src/app/main/ui/workspace/tokens/context_menu.cljs
+msgid "workspace.token.duplicate"
+msgstr "Duplicate token"
+
+#: src/app/main/ui/workspace/tokens/context_menu.cljs
+msgid "workspace.token.edit"
+msgstr "Edit token"
+
+msgid "workspace.versions.button.save"
+msgstr "Save version"
+
+msgid "workspace.versions.button.pin"
+msgstr "Pin version"
+
+msgid "workspace.versions.button.restore"
+msgstr "Restore version"
+
+msgid "workspace.versions.empty"
+msgstr "There are no versions yet"
+
+msgid "workspace.versions.autosaved.version"
+msgstr "Autosaved %s"
+
+msgid "workspace.versions.autosaved.entry"
+msgstr "%s autosave versions"
+
+msgid "workspace.versions.loading"
+msgstr "Loading..."
+
+msgid "workspace.versions.filter.label"
+msgstr "Versions filter"
+
+msgid "workspace.versions.filter.all"
+msgstr "All versions"
+
+msgid "workspace.versions.filter.mine"
+msgstr "My versions"
+
+msgid "workspace.versions.filter.user"
+msgstr "%s's versions"
+
+msgid "workspace.versions.restore-warning"
+msgstr "Do you want to restore this version?"
+
+msgid "workspace.versions.snapshot-menu"
+msgstr "Open snapshot menu"
+
+msgid "workspace.versions.version-menu"
+msgstr "Open version menu"
+
+msgid "workspace.versions.expand-snapshot"
+msgstr "Expand snapshots"
+
+msgid "workspace.versions.tab.history"
+msgstr "History"
+
msgid "dashboard.notifications.notifications-saved"
msgstr "Notification settings updated"
diff --git a/frontend/translations/es.po b/frontend/translations/es.po
index df0f5dc8e..bac438273 100644
--- a/frontend/translations/es.po
+++ b/frontend/translations/es.po
@@ -1761,6 +1761,11 @@ msgstr "Expirada"
msgid "labels.export"
msgstr "Exportar"
+
+#: src/app/main/ui/exports/assets.cljs:177
+msgid "labels.import"
+msgstr "Importar"
+
#: src/app/main/ui/settings/feedback.cljs:48
msgid "labels.feedback-disabled"
msgstr "El modulo de recepción de opiniones esta deshabilitado"
@@ -5461,12 +5466,12 @@ msgid "workspace.options.radius-top-right"
msgstr "Arriba derecha"
#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs:639
-msgid "workspace.options.radius.all-corners"
-msgstr "Todas las esquinas"
+msgid "workspace.options.radius.hide-all-corners"
+msgstr "Colapsar radios individuales"
-#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs:640
-msgid "workspace.options.radius.single-corners"
-msgstr "Esquinas individuales"
+#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs:639
+msgid "workspace.options.radius.show-single-corners"
+msgstr "Mostrar radios individuales"
#: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs:184
msgid "workspace.options.recent-fonts"
@@ -6323,10 +6328,6 @@ msgstr "%s sets"
msgid "workspace.token.original-value"
msgstr "Valor original: "
-#: src/app/main/ui/workspace/tokens/form.cljs:193, src/app/main/ui/workspace/tokens/form.cljs:196, src/app/main/ui/workspace/tokens/sidebar.cljs:67
-msgid "workspace.token.resolved-value"
-msgstr "Valor resuelto: "
-
#: src/app/main/ui/workspace/tokens/modals/themes.cljs:208
msgid "workspace.token.save-theme"
msgstr "Guardar tema"
@@ -6650,6 +6651,206 @@ msgstr "Abrir menu de versiones"
msgid "workspace.viewport.click-to-close-path"
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 ,src/app/main/ui/workspace/tokens/token_pill.cljs
+msgid "workspace.token.resolved-value"
+msgstr "Valor resuelto: %s"
+
+#: src/app/main/ui/workspace/tokens/form.cljs
+msgid "workspace.token.token-name"
+msgstr "Nombre"
+
+#: src/app/main/ui/workspace/tokens/form.cljs
+msgid "workspace.token.enter-token-name"
+msgstr "Introduce un nombre para el token %s"
+
+#: src/app/main/ui/workspace/tokens/form.cljs
+msgid "workspace.token.token-value"
+msgstr "Valor"
+
+#: src/app/main/ui/workspace/tokens/form.cljs
+msgid "workspace.token.enter-token-value"
+msgstr "Introduce un valor o alias"
+
+#: src/app/main/ui/workspace/tokens/form.cljs
+msgid "workspace.token.token-description"
+msgstr "Descripción"
+
+#: src/app/main/ui/workspace/tokens/form.cljs
+msgid "workspace.token.enter-token-description"
+msgstr "Añade una Descripción (opcional)"
+
+#: src/app/main/ui/workspace/tokens/sidebar.cljs
+msgid "workspace.token.original-value"
+msgstr "Valor original: %s"
+
+#: 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/sidebar.cljs
+msgid "workspace.token.tools"
+msgstr "Herramientas"
+
+#: 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"
+
+#: src/app/main/ui/workspace/tokens/context_menu.cljs
+msgid "workspace.token.delete"
+msgstr "Eliminar token"
+
+#: src/app/main/ui/workspace/tokens/context_menu.cljs
+msgid "workspace.token.duplicate"
+msgstr "Duplicar token"
+
+#: src/app/main/ui/workspace/tokens/context_menu.cljs
+msgid "workspace.token.edit"
+msgstr "Editar token"
+
+msgid "workspace.versions.button.save"
+msgstr "Guardar versión"
+
+msgid "workspace.versions.button.pin"
+msgstr "Fijar versión"
+
+msgid "workspace.versions.button.restore"
+msgstr "Restaurar versión"
+
+msgid "workspace.versions.empty"
+msgstr "No hay versiones aún"
+
+msgid "workspace.versions.autosaved.version"
+msgstr "Autoguardado %s"
+
+msgid "workspace.versions.autosaved.entry"
+msgstr "%s versiones de autoguardado"
+
+msgid "workspace.versions.loading"
+msgstr "Cargando..."
+
+msgid "workspace.versions.filter.label"
+msgstr "Filtro de versiones"
+
+msgid "workspace.versions.filter.all"
+msgstr "Todas las versiones"
+
+msgid "workspace.versions.filter.mine"
+msgstr "Mis versiones"
+
+msgid "workspace.versions.filter.user"
+msgstr "Versiones de %s"
+
+msgid "workspace.versions.restore-warning"
+msgstr "¿Quieres restaurar esta versión?"
+
+msgid "workspace.versions.snapshot-menu"
+msgstr "Abrir menu de versiones"
+
+msgid "workspace.versions.version-menu"
+msgstr "Abrir menu de versiones"
+
+msgid "workspace.versions.expand-snapshot"
+msgstr "Expandir versiones"
+
msgid "workspace.versions.tab.history"
msgstr "Histórico"
diff --git a/frontend/yarn.lock b/frontend/yarn.lock
index c9ed1b332..1a3cf06e9 100644
--- a/frontend/yarn.lock
+++ b/frontend/yarn.lock
@@ -4557,7 +4557,7 @@ __metadata:
shadow-cljs: "npm:2.28.18"
source-map-support: "npm:^0.5.21"
storybook: "npm:^8.4.6"
- style-dictionary: "npm:4.0.0-prerelease.34"
+ style-dictionary: "npm:4.0.0-prerelease.36"
svg-sprite: "npm:^2.0.4"
tdigest: "npm:^0.1.2"
tinycolor2: "npm:^1.6.0"
@@ -8728,9 +8728,9 @@ __metadata:
languageName: node
linkType: hard
-"style-dictionary@npm:4.0.0-prerelease.34":
- version: 4.0.0-prerelease.34
- resolution: "style-dictionary@npm:4.0.0-prerelease.34"
+"style-dictionary@npm:4.0.0-prerelease.36":
+ version: 4.0.0-prerelease.36
+ resolution: "style-dictionary@npm:4.0.0-prerelease.36"
dependencies:
"@bundled-es-modules/deepmerge": "npm:^4.3.1"
"@bundled-es-modules/glob": "npm:^10.3.13"
@@ -8746,7 +8746,7 @@ __metadata:
tinycolor2: "npm:^1.6.0"
bin:
style-dictionary: bin/style-dictionary.js
- checksum: 10c0/775d00c0e6aec7749dd5554c448550bc0793aaff9ab028d61ba219476ffa827d3e11866d326c34a27d3e848156b885e476beaade0909fe6b174a50e857dd5009
+ checksum: 10c0/8707b3cced5ee7a858c425b296b53f3b9055f388839ab77ec94f9ed012ca99db43ce28fb540cec1659b92680a2769b1ed24d9af891ea98b9b298895341781f30
languageName: node
linkType: hard
diff --git a/run-ci.sh b/run-ci.sh
new file mode 100755
index 000000000..74f0bcef9
--- /dev/null
+++ b/run-ci.sh
@@ -0,0 +1,50 @@
+#!/bin/bash
+
+set -e
+
+echo "################ test common ################"
+cd common
+yarn install
+yarn run fmt:clj:check
+yarn run lint:clj
+clojure -M:dev:test
+yarn run test
+cd ..
+
+echo "################ test frontend ################"
+cd frontend
+yarn install
+yarn run fmt:clj:check
+yarn run fmt:js:check
+yarn run lint:scss
+yarn run lint:clj
+yarn run test
+cd ..
+
+echo "################ test integration ################"
+cd frontend
+yarn install
+yarn run test:e2e -x --workers=4
+cd ..
+
+echo "################ test backend ################"
+cd backend
+yarn install
+yarn run fmt:clj:check
+yarn run lint:clj
+clojure -M:dev:test --reporter kaocha.report/documentation
+cd ..
+
+echo "################ test exporter ################"
+cd exporter
+yarn install
+yarn run fmt:clj:check
+yarn run lint:clj
+cd ..
+
+echo "################ test render-wasm ################"
+cd render-wasm
+cargo fmt --check
+./test
+cd ..
+