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 .. +