From c6ed081a0b3692d08ec8661b378ec8be356c8f3c Mon Sep 17 00:00:00 2001 From: Florian Schroedl Date: Thu, 10 Oct 2024 13:08:35 +0200 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20Implement=20token=20import=20/=20ex?= =?UTF-8?q?port?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- common/src/app/common/files/changes.cljc | 11 +- .../src/app/common/files/changes_builder.cljc | 9 + common/src/app/common/types/token.cljc | 34 +- common/src/app/common/types/tokens_lib.cljc | 80 +- .../types/data/tokens-multi-set-example.json | 803 ++++++++++++++++++ .../common_tests/types/tokens_lib_test.cljc | 112 ++- frontend/src/app/main/data/tokens.cljs | 38 +- .../app/main/ui/workspace/tokens/core.cljs | 33 +- .../tokens/data/example-tokens-set.json | 580 ------------- .../app/main/ui/workspace/tokens/form.cljs | 2 +- .../app/main/ui/workspace/tokens/sidebar.cljs | 187 ++-- .../app/main/ui/workspace/tokens/sidebar.scss | 75 +- .../app/main/ui/workspace/tokens/token.cljs | 26 - .../token_tests/logic/token_actions_test.cljs | 29 +- .../token_tests/style_dictionary_test.cljs | 19 - frontend/test/token_tests/token_test.cljs | 20 - 16 files changed, 1248 insertions(+), 810 deletions(-) create mode 100644 common/test/common_tests/types/data/tokens-multi-set-example.json delete mode 100644 frontend/src/app/main/ui/workspace/tokens/data/example-tokens-set.json diff --git a/common/src/app/common/files/changes.cljc b/common/src/app/common/files/changes.cljc index 827d4331a..3e19a8037 100644 --- a/common/src/app/common/files/changes.cljc +++ b/common/src/app/common/files/changes.cljc @@ -26,8 +26,6 @@ [app.common.types.token :as cto] [app.common.types.token-theme :as ctot] [app.common.types.tokens-lib :as ctob] - [app.common.types.tokens-list :as ctol] - [app.common.types.tokens-theme-list :as ctotl] [app.common.types.typographies-list :as ctyl] [app.common.types.typography :as ctt] [clojure.set :as set])) @@ -306,6 +304,11 @@ [:type [:= :del-token-set]] [:name :string]]] + [:set-tokens-lib + [:map {:title "SetTokensLib"} + [:type [:= :set-tokens-lib]] + [:tokens-lib :any]]] + [:add-token [:map {:title "AddTokenChange"} [:type [:= :add-token]] @@ -792,6 +795,10 @@ ;; -- Tokens +(defmethod process-change :set-tokens-lib + [data {:keys [tokens-lib]}] + (assoc data :tokens-lib tokens-lib)) + (defmethod process-change :add-token [data {:keys [set-name token]}] (update data :tokens-lib #(-> % diff --git a/common/src/app/common/files/changes_builder.cljc b/common/src/app/common/files/changes_builder.cljc index 1c11f58bc..27f30b344 100644 --- a/common/src/app/common/files/changes_builder.cljc +++ b/common/src/app/common/files/changes_builder.cljc @@ -771,6 +771,15 @@ (update :undo-changes conj {:type :move-token-set-before :set-name set-name :before-set-name prev-before-set-name}) (apply-changes-local))) +(defn set-tokens-lib + [changes tokens-lib] + (let [library-data (::library-data (meta changes)) + prev-tokens-lib (get library-data :tokens-lib)] + (-> changes + (update :redo-changes conj {:type :set-tokens-lib :tokens-lib tokens-lib}) + (update :undo-changes conj {:type :set-tokens-lib :tokens-lib prev-tokens-lib}) + (apply-changes-local)))) + (defn add-token [changes set-name token] (-> changes diff --git a/common/src/app/common/types/token.cljc b/common/src/app/common/types/token.cljc index ea8744d68..0e95d1a9b 100644 --- a/common/src/app/common/types/token.cljc +++ b/common/src/app/common/types/token.cljc @@ -8,6 +8,7 @@ (:require [app.common.schema :as sm] [app.common.schema.registry :as sr] + [clojure.set :as set] [malli.util :as mu])) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; @@ -32,21 +33,26 @@ ;; SCHEMA ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +(def token-type->dtcg-token-type + {:boolean "boolean" + :border-radius "borderRadius" + :box-shadow "boxShadow" + :color "color" + :dimensions "dimension" + :numeric "numeric" + :opacity "opacity" + :other "other" + :rotation "rotation" + :sizing "sizing" + :spacing "spacing" + :string "string" + :stroke-width "strokeWidth"}) + +(def dtcg-token-type->token-type + (set/map-invert token-type->dtcg-token-type)) + (def token-types - #{:boolean - :border-radius - :box-shadow - :color - :dimensions - :numeric - :opacity - :other - :rotation - :sizing - :spacing - :string - :stroke-width - :typography}) + (into #{} (keys token-type->dtcg-token-type))) (defn valid-token-type? [t] diff --git a/common/src/app/common/types/tokens_lib.cljc b/common/src/app/common/types/tokens_lib.cljc index 3c2a55ee1..94657b299 100644 --- a/common/src/app/common/types/tokens_lib.cljc +++ b/common/src/app/common/types/tokens_lib.cljc @@ -99,6 +99,14 @@ ;; === Token +(def token-separator ".") + +(defn get-token-path [path] + (get-path path token-separator)) + +(defn split-token-path [path] + (split-path path token-separator)) + (defrecord Token [name type value description modified-at]) (def schema:token @@ -178,12 +186,25 @@ (defn split-token-set-path [path] (split-path path set-separator)) +(defn tokens-tree + "Convert tokens into a nested tree with their `:name` as the path. + Optionally use `update-token-fn` option to transform the token." + [tokens & {:keys [update-token-fn] + :or {update-token-fn identity}}] + (reduce + (fn [acc [_ token]] + (let [path (split-token-path (:name token))] + (assoc-in acc path (update-token-fn token)))) + {} tokens)) + (defprotocol ITokenSet (add-token [_ token] "add a token at the end of the list") (update-token [_ token-name f] "update a token in the list") (delete-token [_ token-name] "delete a token from the list") (get-token [_ token-name] "return token by token-name") - (get-tokens [_] "return an ordered sequence of all tokens in the set")) + (get-tokens [_] "return an ordered sequence of all tokens in the set") + (get-tokens-tree [_] "returns a tree of tokens split & nested by their name path") + (get-dtcg-tokens-tree [_] "returns tokens tree formated to the dtcg spec")) (defrecord TokenSet [name description modified-at tokens] ITokenSet @@ -219,7 +240,16 @@ (get tokens token-name)) (get-tokens [_] - (vals tokens))) + (vals tokens)) + + (get-tokens-tree [_] + (tokens-tree tokens)) + + (get-dtcg-tokens-tree [_] + (tokens-tree tokens :update-token-fn (fn [token] + (cond-> {"$value" (:value token) + "$type" (cto/token-type->dtcg-token-type (:type token))} + (:description token) (assoc "$description" (:description token))))))) (def schema:token-set [:and [:map {:title "TokenSet"} @@ -440,6 +470,31 @@ When `before-set-name` is nil, move set to bottom") (def valid-active-token-themes? (sm/validator schema:active-token-themes)) +;; === Import / Export from DTCG format + +(defn flatten-nested-tokens-json + "Recursively flatten the dtcg token structure, joining keys with '.'." + [tokens token-path] + (reduce-kv + (fn [acc k v] + (let [child-path (if (empty? token-path) + (name k) + (str token-path "." k))] + (if (and (map? v) + (not (contains? v "$type"))) + (merge acc (flatten-nested-tokens-json v child-path)) + (let [token-type (cto/dtcg-token-type->token-type (get v "$type"))] + (if token-type + (assoc acc child-path (make-token + :name child-path + :type token-type + :value (get v "$value") + :description (get v "$description"))) + ;; Discard unknown tokens + acc))))) + {} + tokens)) + ;; === Tokens Lib (defprotocol ITokensLib @@ -451,6 +506,8 @@ When `before-set-name` is nil, move set to bottom") (get-active-themes-set-names [_] "set of set names that are active in the the active themes") (get-active-themes-set-tokens [_] "set of set names that are active in the the active themes") (update-set-name [_ old-set-name new-set-name] "updates set name in themes") + (encode-dtcg [_] "Encodes library to a dtcg compatible json string") + (decode-dtcg-json [_ parsed-json] "Decodes parsed json containing tokens and converts to library") (validate [_])) (deftype TokensLib [sets set-groups themes active-themes] @@ -724,6 +781,25 @@ When `before-set-name` is nil, move set to bottom") themes) active-themes)) + (encode-dtcg [_] + (into {} (map (fn [[k v]] + [k (get-dtcg-tokens-tree v)]) + sets))) + + (decode-dtcg-json [_ parsed-json] + (let [token-sets (into (d/ordered-map) + (map (fn [[set-name tokens]] + [set-name (make-token-set + :name set-name + :tokens (flatten-nested-tokens-json tokens ""))])) + (-> parsed-json + ;; tokens-studio/plugin will add these meta properties, remove them for now + (dissoc "$themes" "$metadata")))] + (TokensLib. token-sets + set-groups + themes + active-themes))) + (validate [_] (and (valid-token-sets? sets) ;; TODO: validate set-groups (valid-token-themes? themes) 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 new file mode 100644 index 000000000..ca836d961 --- /dev/null +++ b/common/test/common_tests/types/data/tokens-multi-set-example.json @@ -0,0 +1,803 @@ +{ + "core": { + "dimension": { + "scale": { + "$value": "2", + "$type": "dimension" + }, + "xs": { + "$value": "4", + "$type": "dimension" + }, + "sm": { + "$value": "{dimension.xs} * {dimension.scale}", + "$type": "dimension" + }, + "md": { + "$value": "{dimension.sm} * {dimension.scale}", + "$type": "dimension" + }, + "lg": { + "$value": "{dimension.md} * {dimension.scale}", + "$type": "dimension" + }, + "xl": { + "$value": "{dimension.lg} * {dimension.scale}", + "$type": "dimension" + } + }, + "spacing": { + "xs": { + "$value": "{dimension.xs}", + "$type": "spacing" + }, + "sm": { + "$value": "{dimension.sm}", + "$type": "spacing" + }, + "md": { + "$value": "{dimension.md}", + "$type": "spacing" + }, + "lg": { + "$value": "{dimension.lg}", + "$type": "spacing" + }, + "xl": { + "$value": "{dimension.xl}", + "$type": "spacing" + }, + "multi-value": { + "$value": "{dimension.sm} {dimension.xl}", + "$type": "spacing", + "$description": "You can have multiple values in a single spacing token" + } + }, + "borderRadius": { + "sm": { + "$value": "4", + "$type": "borderRadius" + }, + "lg": { + "$value": "8", + "$type": "borderRadius" + }, + "xl": { + "$value": "16", + "$type": "borderRadius" + }, + "multi-value": { + "$value": "{borderRadius.sm} {borderRadius.lg}", + "$type": "borderRadius", + "$description": "You can have multiple values in a single radius token. Read more on these: https://docs.tokens.studio/available-tokens/border-radius-tokens#single--multiple-values" + } + }, + "colors": { + "black": { + "$value": "#000000", + "$type": "color" + }, + "white": { + "$value": "#ffffff", + "$type": "color" + }, + "gray": { + "100": { + "$value": "#f7fafc", + "$type": "color" + }, + "200": { + "$value": "#edf2f7", + "$type": "color" + }, + "300": { + "$value": "#e2e8f0", + "$type": "color" + }, + "400": { + "$value": "#cbd5e0", + "$type": "color" + }, + "500": { + "$value": "#a0aec0", + "$type": "color" + }, + "600": { + "$value": "#718096", + "$type": "color" + }, + "700": { + "$value": "#4a5568", + "$type": "color" + }, + "800": { + "$value": "#2d3748", + "$type": "color" + }, + "900": { + "$value": "#1a202c", + "$type": "color" + } + }, + "red": { + "100": { + "$value": "#fff5f5", + "$type": "color" + }, + "200": { + "$value": "#fed7d7", + "$type": "color" + }, + "300": { + "$value": "#feb2b2", + "$type": "color" + }, + "400": { + "$value": "#fc8181", + "$type": "color" + }, + "500": { + "$value": "#f56565", + "$type": "color" + }, + "600": { + "$value": "#e53e3e", + "$type": "color" + }, + "700": { + "$value": "#c53030", + "$type": "color" + }, + "800": { + "$value": "#9b2c2c", + "$type": "color" + }, + "900": { + "$value": "#742a2a", + "$type": "color" + } + }, + "orange": { + "100": { + "$value": "#fffaf0", + "$type": "color" + }, + "200": { + "$value": "#feebc8", + "$type": "color" + }, + "300": { + "$value": "#fbd38d", + "$type": "color" + }, + "400": { + "$value": "#f6ad55", + "$type": "color" + }, + "500": { + "$value": "#ed8936", + "$type": "color" + }, + "600": { + "$value": "#dd6b20", + "$type": "color" + }, + "700": { + "$value": "#c05621", + "$type": "color" + }, + "800": { + "$value": "#9c4221", + "$type": "color" + }, + "900": { + "$value": "#7b341e", + "$type": "color" + } + }, + "yellow": { + "100": { + "$value": "#fffff0", + "$type": "color" + }, + "200": { + "$value": "#fefcbf", + "$type": "color" + }, + "300": { + "$value": "#faf089", + "$type": "color" + }, + "400": { + "$value": "#f6e05e", + "$type": "color" + }, + "500": { + "$value": "#ecc94b", + "$type": "color" + }, + "600": { + "$value": "#d69e2e", + "$type": "color" + }, + "700": { + "$value": "#b7791f", + "$type": "color" + }, + "800": { + "$value": "#975a16", + "$type": "color" + }, + "900": { + "$value": "#744210", + "$type": "color" + } + }, + "green": { + "100": { + "$value": "#f0fff4", + "$type": "color" + }, + "200": { + "$value": "#c6f6d5", + "$type": "color" + }, + "300": { + "$value": "#9ae6b4", + "$type": "color" + }, + "400": { + "$value": "#68d391", + "$type": "color" + }, + "500": { + "$value": "#48bb78", + "$type": "color" + }, + "600": { + "$value": "#38a169", + "$type": "color" + }, + "700": { + "$value": "#2f855a", + "$type": "color" + }, + "800": { + "$value": "#276749", + "$type": "color" + }, + "900": { + "$value": "#22543d", + "$type": "color" + } + }, + "teal": { + "100": { + "$value": "#e6fffa", + "$type": "color" + }, + "200": { + "$value": "#b2f5ea", + "$type": "color" + }, + "300": { + "$value": "#81e6d9", + "$type": "color" + }, + "400": { + "$value": "#4fd1c5", + "$type": "color" + }, + "500": { + "$value": "#38b2ac", + "$type": "color" + }, + "600": { + "$value": "#319795", + "$type": "color" + }, + "700": { + "$value": "#2c7a7b", + "$type": "color" + }, + "800": { + "$value": "#285e61", + "$type": "color" + }, + "900": { + "$value": "#234e52", + "$type": "color" + } + }, + "blue": { + "100": { + "$value": "#ebf8ff", + "$type": "color" + }, + "200": { + "$value": "#bee3f8", + "$type": "color" + }, + "300": { + "$value": "#90cdf4", + "$type": "color" + }, + "400": { + "$value": "#63b3ed", + "$type": "color" + }, + "500": { + "$value": "#4299e1", + "$type": "color" + }, + "600": { + "$value": "#3182ce", + "$type": "color" + }, + "700": { + "$value": "#2b6cb0", + "$type": "color" + }, + "800": { + "$value": "#2c5282", + "$type": "color" + }, + "900": { + "$value": "#2a4365", + "$type": "color" + } + }, + "indigo": { + "100": { + "$value": "#ebf4ff", + "$type": "color" + }, + "200": { + "$value": "#c3dafe", + "$type": "color" + }, + "300": { + "$value": "#a3bffa", + "$type": "color" + }, + "400": { + "$value": "#7f9cf5", + "$type": "color" + }, + "500": { + "$value": "#667eea", + "$type": "color" + }, + "600": { + "$value": "#5a67d8", + "$type": "color" + }, + "700": { + "$value": "#4c51bf", + "$type": "color" + }, + "800": { + "$value": "#434190", + "$type": "color" + }, + "900": { + "$value": "#3c366b", + "$type": "color" + } + }, + "purple": { + "100": { + "$value": "#faf5ff", + "$type": "color" + }, + "200": { + "$value": "#e9d8fd", + "$type": "color" + }, + "300": { + "$value": "#d6bcfa", + "$type": "color" + }, + "400": { + "$value": "#b794f4", + "$type": "color" + }, + "500": { + "$value": "#9f7aea", + "$type": "color" + }, + "600": { + "$value": "#805ad5", + "$type": "color" + }, + "700": { + "$value": "#6b46c1", + "$type": "color" + }, + "800": { + "$value": "#553c9a", + "$type": "color" + }, + "900": { + "$value": "#44337a", + "$type": "color" + } + }, + "pink": { + "100": { + "$value": "#fff5f7", + "$type": "color" + }, + "200": { + "$value": "#fed7e2", + "$type": "color" + }, + "300": { + "$value": "#fbb6ce", + "$type": "color" + }, + "400": { + "$value": "#f687b3", + "$type": "color" + }, + "500": { + "$value": "#ed64a6", + "$type": "color" + }, + "600": { + "$value": "#d53f8c", + "$type": "color" + }, + "700": { + "$value": "#b83280", + "$type": "color" + }, + "800": { + "$value": "#97266d", + "$type": "color" + }, + "900": { + "$value": "#702459", + "$type": "color" + } + } + }, + "opacity": { + "low": { + "$value": "10%", + "$type": "opacity" + }, + "md": { + "$value": "50%", + "$type": "opacity" + }, + "high": { + "$value": "90%", + "$type": "opacity" + } + }, + "fontFamilies": { + "heading": { + "$value": "Inter", + "$type": "fontFamilies" + }, + "body": { + "$value": "Roboto", + "$type": "fontFamilies" + } + }, + "lineHeights": { + "heading": { + "$value": "110%", + "$type": "lineHeights" + }, + "body": { + "$value": "140%", + "$type": "lineHeights" + } + }, + "letterSpacing": { + "default": { + "$value": "0", + "$type": "letterSpacing" + }, + "increased": { + "$value": "150%", + "$type": "letterSpacing" + }, + "decreased": { + "$value": "-5%", + "$type": "letterSpacing" + } + }, + "paragraphSpacing": { + "h1": { + "$value": "32", + "$type": "paragraphSpacing" + }, + "h2": { + "$value": "26", + "$type": "paragraphSpacing" + } + }, + "fontWeights": { + "headingRegular": { + "$value": "Regular", + "$type": "fontWeights" + }, + "headingBold": { + "$value": "Bold", + "$type": "fontWeights" + }, + "bodyRegular": { + "$value": "Regular", + "$type": "fontWeights" + }, + "bodyBold": { + "$value": "Bold", + "$type": "fontWeights" + } + }, + "fontSizes": { + "h1": { + "$value": "{fontSizes.h2} * 1.25", + "$type": "fontSizes" + }, + "h2": { + "$value": "{fontSizes.h3} * 1.25", + "$type": "fontSizes" + }, + "h3": { + "$value": "{fontSizes.h4} * 1.25", + "$type": "fontSizes" + }, + "h4": { + "$value": "{fontSizes.h5} * 1.25", + "$type": "fontSizes" + }, + "h5": { + "$value": "{fontSizes.h6} * 1.25", + "$type": "fontSizes" + }, + "h6": { + "$value": "{fontSizes.body} * 1", + "$type": "fontSizes" + }, + "body": { + "$value": "16", + "$type": "fontSizes" + }, + "sm": { + "$value": "{fontSizes.body} * 0.85", + "$type": "fontSizes" + }, + "xs": { + "$value": "{fontSizes.body} * 0.65", + "$type": "fontSizes" + } + } + }, + "light": { + "fg": { + "default": { + "$value": "{colors.black}", + "$type": "color" + }, + "muted": { + "$value": "{colors.gray.700}", + "$type": "color" + }, + "subtle": { + "$value": "{colors.gray.500}", + "$type": "color" + } + }, + "bg": { + "default": { + "$value": "{colors.white}", + "$type": "color" + }, + "muted": { + "$value": "{colors.gray.100}", + "$type": "color" + }, + "subtle": { + "$value": "{colors.gray.200}", + "$type": "color" + } + }, + "accent": { + "default": { + "$value": "{colors.indigo.400}", + "$type": "color" + }, + "onAccent": { + "$value": "{colors.white}", + "$type": "color" + }, + "bg": { + "$value": "{colors.indigo.200}", + "$type": "color" + } + }, + "shadows": { + "default": { + "$value": "{colors.gray.900}", + "$type": "color" + } + } + }, + "dark": { + "fg": { + "default": { + "$value": "{colors.white}", + "$type": "color" + }, + "muted": { + "$value": "{colors.gray.300}", + "$type": "color" + }, + "subtle": { + "$value": "{colors.gray.500}", + "$type": "color" + } + }, + "bg": { + "default": { + "$value": "{colors.gray.900}", + "$type": "color" + }, + "muted": { + "$value": "{colors.gray.700}", + "$type": "color" + }, + "subtle": { + "$value": "{colors.gray.600}", + "$type": "color" + } + }, + "accent": { + "default": { + "$value": "{colors.indigo.600}", + "$type": "color" + }, + "onAccent": { + "$value": "{colors.white}", + "$type": "color" + }, + "bg": { + "$value": "{colors.indigo.800}", + "$type": "color" + } + }, + "shadows": { + "default": { + "$value": "rgba({colors.black}, 0.3)", + "$type": "color" + } + } + }, + "theme": { + "button": { + "primary": { + "background": { + "$value": "{accent.default}", + "$type": "color" + }, + "text": { + "$value": "{accent.onAccent}", + "$type": "color" + } + }, + "borderRadius": { + "$value": "{borderRadius.lg}", + "$type": "borderRadius" + }, + "borderWidth": { + "$value": "{dimension.sm}", + "$type": "borderWidth" + } + }, + "card": { + "borderRadius": { + "$value": "{borderRadius.lg}", + "$type": "borderRadius" + }, + "background": { + "$value": "{bg.default}", + "$type": "color" + }, + "padding": { + "$value": "{dimension.md}", + "$type": "dimension" + } + }, + "boxShadow": { + "default": { + "$value": [ + { + "x": 5, + "y": 5, + "spread": 3, + "color": "rgba({shadows.default}, 0.15)", + "blur": 5, + "$type": "dropShadow" + }, + { + "x": 4, + "y": 4, + "spread": 6, + "color": "#00000033", + "blur": 5, + "$type": "innerShadow" + } + ], + "$type": "boxShadow" + } + }, + "typography": { + "H1": { + "Bold": { + "$value": { + "fontFamily": "{fontFamilies.heading}", + "fontWeight": "{fontWeights.headingBold}", + "lineHeight": "{lineHeights.heading}", + "fontSize": "{fontSizes.h1}", + "paragraphSpacing": "{paragraphSpacing.h1}", + "letterSpacing": "{letterSpacing.decreased}" + }, + "$type": "typography" + }, + "Regular": { + "$value": { + "fontFamily": "{fontFamilies.heading}", + "fontWeight": "{fontWeights.headingRegular}", + "lineHeight": "{lineHeights.heading}", + "fontSize": "{fontSizes.h1}", + "paragraphSpacing": "{paragraphSpacing.h1}", + "letterSpacing": "{letterSpacing.decreased}" + }, + "$type": "typography" + } + }, + "H2": { + "Bold": { + "$value": { + "fontFamily": "{fontFamilies.heading}", + "fontWeight": "{fontWeights.headingBold}", + "lineHeight": "{lineHeights.heading}", + "fontSize": "{fontSizes.h2}", + "paragraphSpacing": "{paragraphSpacing.h2}", + "letterSpacing": "{letterSpacing.decreased}" + }, + "$type": "typography" + }, + "Regular": { + "$value": { + "fontFamily": "{fontFamilies.heading}", + "fontWeight": "{fontWeights.headingRegular}", + "lineHeight": "{lineHeights.heading}", + "fontSize": "{fontSizes.h2}", + "paragraphSpacing": "{paragraphSpacing.h2}", + "letterSpacing": "{letterSpacing.decreased}" + }, + "$type": "typography" + } + }, + "Body": { + "$value": { + "fontFamily": "{fontFamilies.body}", + "fontWeight": "{fontWeights.bodyRegular}", + "lineHeight": "{lineHeights.heading}", + "fontSize": "{fontSizes.body}", + "paragraphSpacing": "{paragraphSpacing.h2}" + }, + "$type": "typography" + } + } + }, + "$themes": [], + "$metadata": { + "tokenSetOrder": ["core", "light", "dark", "theme"] + } +} diff --git a/common/test/common_tests/types/tokens_lib_test.cljc b/common/test/common_tests/types/tokens_lib_test.cljc index 64c164fe5..e1061939c 100644 --- a/common/test/common_tests/types/tokens_lib_test.cljc +++ b/common/test/common_tests/types/tokens_lib_test.cljc @@ -11,6 +11,7 @@ [app.common.time :as dt] [app.common.transit :as tr] [app.common.types.tokens-lib :as ctob] + [clojure.data.json :as json] [clojure.test :as t])) (t/testing "token" @@ -104,7 +105,25 @@ (t/testing "ignore invalid moves" (t/is (= original-order (move "A" "foo/bar/baz"))) - (t/is (= original-order (move "Missing" "Move"))))))) + (t/is (= original-order (move "Missing" "Move")))))) + + (t/deftest tokens-tree + (let [tokens-lib (-> (ctob/make-tokens-lib) + (ctob/add-set (ctob/make-token-set :name "A" + :tokens {"foo.bar.baz" (ctob/make-token :name "foo.bar.baz" + :type :boolean + :value true) + "foo.bar.bam" (ctob/make-token :name "foo.bar.bam" + :type :boolean + :value true) + "baz.boo" (ctob/make-token :name "baz.boo" + :type :boolean + :value true)}))) + expected (-> (ctob/get-set tokens-lib "A") + (ctob/get-tokens-tree))] + (t/is (= (get-in expected ["foo" "bar" "baz" :name]) "foo.bar.baz")) + (t/is (= (get-in expected ["foo" "bar" "bam" :name]) "foo.bar.bam")) + (t/is (= (get-in expected ["baz" "boo" :name]) "baz.boo"))))) (t/testing "token-theme" (t/deftest make-token-theme @@ -1026,3 +1045,94 @@ (t/is (= (ctob/theme-count tokens-lib') 1)) (t/is (= (count themes-tree') 1)) (t/is (nil? token-theme')))))) + +(t/testing "dtcg encoding/decoding" + (t/deftest decode-dtcg-json + (let [json (-> (slurp "test/common_tests/types/data/tokens-multi-set-example.json") + (tr/decode-str)) + lib (ctob/decode-dtcg-json (ctob/ensure-tokens-lib nil) json) + get-set-token (fn [set-name token-name] + (some-> (ctob/get-set lib set-name) + (ctob/get-token token-name) + (dissoc :modified-at)))] + (t/is (= '("core" "light" "dark" "theme") (ctob/get-ordered-set-names lib))) + (t/testing "tokens exist in core set" + (t/is (= (get-set-token "core" "colors.red.600") + {:name "colors.red.600" + :type :color + :value "#e53e3e" + :description nil})) + (t/is (= (get-set-token "core" "spacing.multi-value") + {:name "spacing.multi-value" + :type :spacing + :value "{dimension.sm} {dimension.xl}" + :description "You can have multiple values in a single spacing token"})) + (t/is (= (get-set-token "theme" "button.primary.background") + {:name "button.primary.background" + :type :color + :value "{accent.default}" + :description nil}))) + (t/testing "invalid tokens got discarded" + (t/is (nil? (get-set-token "typography" "H1.Bold")))))) + + (t/deftest encode-dtcg-json + (let [tokens-lib (-> (ctob/make-tokens-lib) + (ctob/add-set (ctob/make-token-set :name "core" + :tokens {"colors.red.600" + (ctob/make-token + {:name "colors.red.600" + :type :color + :value "#e53e3e"}) + "spacing.multi-value" + (ctob/make-token + {:name "spacing.multi-value" + :type :spacing + :value "{dimension.sm} {dimension.xl}" + :description "You can have multiple values in a single spacing token"}) + "button.primary.background" + (ctob/make-token + {:name "button.primary.background" + :type :color + :value "{accent.default}"})}))) + expected (ctob/encode-dtcg tokens-lib)] + (t/is (= {"core" + {"colors" {"red" {"600" {"$value" "#e53e3e" + "$type" "color"}}} + "spacing" + {"multi-value" + {"$value" "{dimension.sm} {dimension.xl}" + "$type" "spacing" + "$description" "You can have multiple values in a single spacing token"}} + "button" + {"primary" {"background" {"$value" "{accent.default}" + "$type" "color"}}}}} + expected)))) + + (t/deftest encode-decode-dtcg-json + (with-redefs [dt/now (constantly #inst "2024-10-16T12:01:20.257840055-00:00")] + (let [tokens-lib (-> (ctob/make-tokens-lib) + (ctob/add-set (ctob/make-token-set :name "core" + :tokens {"colors.red.600" + (ctob/make-token + {:name "colors.red.600" + :type :color + :value "#e53e3e"}) + "spacing.multi-value" + (ctob/make-token + {:name "spacing.multi-value" + :type :spacing + :value "{dimension.sm} {dimension.xl}" + :description "You can have multiple values in a single spacing token"}) + "button.primary.background" + (ctob/make-token + {:name "button.primary.background" + :type :color + :value "{accent.default}"})}))) + encoded (ctob/encode-dtcg tokens-lib) + with-prev-tokens-lib (ctob/decode-dtcg-json tokens-lib encoded) + with-empty-tokens-lib (ctob/decode-dtcg-json (ctob/ensure-tokens-lib nil) encoded)] + (t/testing "library got updated but data is equal" + (t/is (not= with-prev-tokens-lib tokens-lib)) + (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/src/app/main/data/tokens.cljs b/frontend/src/app/main/data/tokens.cljs index a6f1c8b41..fa4700501 100644 --- a/frontend/src/app/main/data/tokens.cljs +++ b/frontend/src/app/main/data/tokens.cljs @@ -192,6 +192,24 @@ (dch/commit-changes changes') (wtu/update-workspace-tokens)))))) +(defn import-tokens-lib [lib] + (ptk/reify ::import-tokens-lib + ptk/WatchEvent + (watch [it state _] + (let [data (get state :workspace-data) + update-token-set-change (some-> lib + (ctob/get-sets) + (first) + (:name) + (set-selected-token-set-id)) + changes (-> (pcb/empty-changes it) + (pcb/with-library-data data) + (pcb/set-tokens-lib lib))] + (rx/of + (dch/commit-changes changes) + update-token-set-change + (wtu/update-workspace-tokens)))))) + (defn delete-token-set [token-set-name] (ptk/reify ::delete-token-set ptk/WatchEvent @@ -284,7 +302,7 @@ (update [_ state] (assoc-in state [:workspace-tokens :open-status token-type] open?)))) -;; Token Context Menu Functions ------------------------------------------------- +;; === Token Context Menu (defn show-token-context-menu [{:keys [position _token-name] :as params}] @@ -300,6 +318,8 @@ (update [_ state] (assoc-in state [:workspace-local :token-context-menu] nil)))) +;; === Token Set Context Menu + (defn show-token-set-context-menu [{:keys [position _token-set-name] :as params}] (dm/assert! (gpt/point? position)) @@ -313,3 +333,19 @@ ptk/UpdateEvent (update [_ state] (assoc-in state [:workspace-local :token-set-context-menu] nil)))) + +;; === Import Export Context Menu + +(defn show-import-export-context-menu + [{:keys [position] :as params}] + (dm/assert! (gpt/point? position)) + (ptk/reify ::show-import-export-context-menu + ptk/UpdateEvent + (update [_ state] + (assoc-in state [:workspace-local :import-export-context-menu] params)))) + +(def hide-import-export-set-context-menu + (ptk/reify ::hide-import-export-set-context-menu + ptk/UpdateEvent + (update [_ state] + (assoc-in state [:workspace-local :import-export-set-context-menu] nil)))) diff --git a/frontend/src/app/main/ui/workspace/tokens/core.cljs b/frontend/src/app/main/ui/workspace/tokens/core.cljs index 14c2df725..c61cf0e40 100644 --- a/frontend/src/app/main/ui/workspace/tokens/core.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/core.cljs @@ -7,11 +7,7 @@ (ns app.main.ui.workspace.tokens.core (:require [app.common.data :as d] - [app.main.refs :as refs] - [app.main.ui.workspace.tokens.token :as wtt] - [app.util.dom :as dom] - [app.util.webapi :as wapi] - [cuerdas.core :as str])) + [app.main.ui.workspace.tokens.token :as wtt])) ;; Helpers --------------------------------------------------------------------- @@ -36,30 +32,3 @@ (cond-> (assoc token :label name) (wtt/token-applied? token shape (or selected-attributes attributes)) (assoc :selected? true))) tokens)) - -;; JSON export functions ------------------------------------------------------- - -(defn encode-tokens - [data] - (-> data - (clj->js) - (js/JSON.stringify nil 2))) - -(defn export-tokens-file [tokens-json] - (let [file-name "tokens.json" - file-content (encode-tokens tokens-json) - blob (wapi/create-blob file-content "application/json")] - (dom/trigger-download file-name blob))) - -(defn tokens->dtcg-map [tokens] - (let [global (reduce - (fn [acc [_ {:keys [name value type]}]] - (assoc acc name {"$value" value - "$type" (str/camel type)})) - (d/ordered-map) tokens)] - {:global global})) - -(defn download-tokens-as-json [] - (let [tokens (deref refs/workspace-active-theme-sets-tokens) - dtcg-format-tokens-map (tokens->dtcg-map tokens)] - (export-tokens-file dtcg-format-tokens-map))) diff --git a/frontend/src/app/main/ui/workspace/tokens/data/example-tokens-set.json b/frontend/src/app/main/ui/workspace/tokens/data/example-tokens-set.json deleted file mode 100644 index 90ae54e75..000000000 --- a/frontend/src/app/main/ui/workspace/tokens/data/example-tokens-set.json +++ /dev/null @@ -1,580 +0,0 @@ -{ - "core": { - "dimension": { - "scale": { - "value": "2", - "type": "dimension" - }, - "xs": { - "value": "4", - "type": "dimension" - }, - "sm": { - "value": "{dimension.xs} * {dimension.scale}", - "type": "dimension" - }, - "md": { - "value": "{dimension.sm} * {dimension.scale}", - "type": "dimension" - }, - "lg": { - "value": "{dimension.md} * {dimension.scale}", - "type": "dimension" - }, - "xl": { - "value": "{dimension.lg} * {dimension.scale}", - "type": "dimension" - } - }, - "spacing": { - "xs": { - "value": "{dimension.xs}", - "type": "spacing" - }, - "sm": { - "value": "{dimension.sm}", - "type": "spacing" - }, - "md": { - "value": "{dimension.md}", - "type": "spacing" - }, - "lg": { - "value": "{dimension.lg}", - "type": "spacing" - }, - "xl": { - "value": "{dimension.xl}", - "type": "spacing" - }, - "multi-value": { - "value": "{dimension.sm} {dimension.xl}", - "type": "spacing", - "description": "You can have multiple values in a single spacing token. Read more on these: https://docs.tokens.studio/available-tokens/spacing-tokens#multi-value-spacing-tokens" - } - }, - "borderRadius": { - "sm": { - "value": "4", - "type": "borderRadius" - }, - "lg": { - "value": "8", - "type": "borderRadius" - }, - "xl": { - "value": "16", - "type": "borderRadius" - }, - "multi-value": { - "value": "{borderRadius.sm} {borderRadius.lg}", - "type": "borderRadius", - "description": "You can have multiple values in a single radius token. Read more on these: https://docs.tokens.studio/available-tokens/border-radius-tokens#single--multiple-values" - } - }, - "colors": { - "black": { - "value": "#000000", - "type": "color" - }, - "white": { - "value": "#ffffff", - "type": "color" - }, - "gray": { - "100": { - "value": "#f7fafc", - "type": "color" - }, - "200": { - "value": "#edf2f7", - "type": "color" - }, - "300": { - "value": "#e2e8f0", - "type": "color" - }, - "400": { - "value": "#cbd5e0", - "type": "color" - }, - "500": { - "value": "#a0aec0", - "type": "color" - }, - "600": { - "value": "#718096", - "type": "color" - }, - "700": { - "value": "#4a5568", - "type": "color" - }, - "800": { - "value": "#2d3748", - "type": "color" - }, - "900": { - "value": "#1a202c", - "type": "color" - } - }, - "red": { - "100": { - "value": "#fff5f5", - "type": "color" - }, - "200": { - "value": "#fed7d7", - "type": "color" - }, - "300": { - "value": "#feb2b2", - "type": "color" - }, - "400": { - "value": "#fc8181", - "type": "color" - }, - "500": { - "value": "#f56565", - "type": "color" - }, - "600": { - "value": "#e53e3e", - "type": "color" - }, - "700": { - "value": "#c53030", - "type": "color" - }, - "800": { - "value": "#9b2c2c", - "type": "color" - }, - "900": { - "value": "#742a2a", - "type": "color" - } - }, - "orange": { - "100": { - "value": "#fffaf0", - "type": "color" - }, - "200": { - "value": "#feebc8", - "type": "color" - }, - "300": { - "value": "#fbd38d", - "type": "color" - }, - "400": { - "value": "#f6ad55", - "type": "color" - }, - "500": { - "value": "#ed8936", - "type": "color" - }, - "600": { - "value": "#dd6b20", - "type": "color" - }, - "700": { - "value": "#c05621", - "type": "color" - }, - "800": { - "value": "#9c4221", - "type": "color" - }, - "900": { - "value": "#7b341e", - "type": "color" - } - }, - "yellow": { - "100": { - "value": "#fffff0", - "type": "color" - }, - "200": { - "value": "#fefcbf", - "type": "color" - }, - "300": { - "value": "#faf089", - "type": "color" - }, - "400": { - "value": "#f6e05e", - "type": "color" - }, - "500": { - "value": "#ecc94b", - "type": "color" - }, - "600": { - "value": "#d69e2e", - "type": "color" - }, - "700": { - "value": "#b7791f", - "type": "color" - }, - "800": { - "value": "#975a16", - "type": "color" - }, - "900": { - "value": "#744210", - "type": "color" - } - }, - "green": { - "100": { - "value": "#f0fff4", - "type": "color" - }, - "200": { - "value": "#c6f6d5", - "type": "color" - }, - "300": { - "value": "#9ae6b4", - "type": "color" - }, - "400": { - "value": "#68d391", - "type": "color" - }, - "500": { - "value": "#48bb78", - "type": "color" - }, - "600": { - "value": "#38a169", - "type": "color" - }, - "700": { - "value": "#2f855a", - "type": "color" - }, - "800": { - "value": "#276749", - "type": "color" - }, - "900": { - "value": "#22543d", - "type": "color" - } - }, - "teal": { - "100": { - "value": "#e6fffa", - "type": "color" - }, - "200": { - "value": "#b2f5ea", - "type": "color" - }, - "300": { - "value": "#81e6d9", - "type": "color" - }, - "400": { - "value": "#4fd1c5", - "type": "color" - }, - "500": { - "value": "#38b2ac", - "type": "color" - }, - "600": { - "value": "#319795", - "type": "color" - }, - "700": { - "value": "#2c7a7b", - "type": "color" - }, - "800": { - "value": "#285e61", - "type": "color" - }, - "900": { - "value": "#234e52", - "type": "color" - } - }, - "blue": { - "100": { - "value": "#ebf8ff", - "type": "color" - }, - "200": { - "value": "#bee3f8", - "type": "color" - }, - "300": { - "value": "#90cdf4", - "type": "color" - }, - "400": { - "value": "#63b3ed", - "type": "color" - }, - "500": { - "value": "#4299e1", - "type": "color" - }, - "600": { - "value": "#3182ce", - "type": "color" - }, - "700": { - "value": "#2b6cb0", - "type": "color" - }, - "800": { - "value": "#2c5282", - "type": "color" - }, - "900": { - "value": "#2a4365", - "type": "color" - } - }, - "indigo": { - "100": { - "value": "#ebf4ff", - "type": "color" - }, - "200": { - "value": "#c3dafe", - "type": "color" - }, - "300": { - "value": "#a3bffa", - "type": "color" - }, - "400": { - "value": "#7f9cf5", - "type": "color" - }, - "500": { - "value": "#667eea", - "type": "color" - }, - "600": { - "value": "#5a67d8", - "type": "color" - }, - "700": { - "value": "#4c51bf", - "type": "color" - }, - "800": { - "value": "#434190", - "type": "color" - }, - "900": { - "value": "#3c366b", - "type": "color" - } - }, - "purple": { - "100": { - "value": "#faf5ff", - "type": "color" - }, - "200": { - "value": "#e9d8fd", - "type": "color" - }, - "300": { - "value": "#d6bcfa", - "type": "color" - }, - "400": { - "value": "#b794f4", - "type": "color" - }, - "500": { - "value": "#9f7aea", - "type": "color" - }, - "600": { - "value": "#805ad5", - "type": "color" - }, - "700": { - "value": "#6b46c1", - "type": "color" - }, - "800": { - "value": "#553c9a", - "type": "color" - }, - "900": { - "value": "#44337a", - "type": "color" - } - }, - "pink": { - "100": { - "value": "#fff5f7", - "type": "color" - }, - "200": { - "value": "#fed7e2", - "type": "color" - }, - "300": { - "value": "#fbb6ce", - "type": "color" - }, - "400": { - "value": "#f687b3", - "type": "color" - }, - "500": { - "value": "#ed64a6", - "type": "color" - }, - "600": { - "value": "#d53f8c", - "type": "color" - }, - "700": { - "value": "#b83280", - "type": "color" - }, - "800": { - "value": "#97266d", - "type": "color" - }, - "900": { - "value": "#702459", - "type": "color" - } - } - }, - "opacity": { - "low": { - "value": "10%", - "type": "opacity" - }, - "md": { - "value": "50%", - "type": "opacity" - }, - "high": { - "value": "90%", - "type": "opacity" - } - }, - "fontFamilies": { - "heading": { - "value": "Inter", - "type": "fontFamilies" - }, - "body": { - "value": "Roboto", - "type": "fontFamilies" - } - }, - "lineHeights": { - "heading": { - "value": "110%", - "type": "lineHeights" - }, - "body": { - "value": "140%", - "type": "lineHeights" - } - }, - "letterSpacing": { - "default": { - "value": "0", - "type": "letterSpacing" - }, - "increased": { - "value": "150%", - "type": "letterSpacing" - }, - "decreased": { - "value": "-5%", - "type": "letterSpacing" - } - }, - "paragraphSpacing": { - "h1": { - "value": "32", - "type": "paragraphSpacing" - }, - "h2": { - "value": "26", - "type": "paragraphSpacing" - } - }, - "fontWeights": { - "headingRegular": { - "value": "Regular", - "type": "fontWeights" - }, - "headingBold": { - "value": "Bold", - "type": "fontWeights" - }, - "bodyRegular": { - "value": "Regular", - "type": "fontWeights" - }, - "bodyBold": { - "value": "Bold", - "type": "fontWeights" - } - }, - "fontSizes": { - "h1": { - "value": "{fontSizes.h2} * 1.25", - "type": "fontSizes" - }, - "h2": { - "value": "{fontSizes.h3} * 1.25", - "type": "fontSizes" - }, - "h3": { - "value": "{fontSizes.h4} * 1.25", - "type": "fontSizes" - }, - "h4": { - "value": "{fontSizes.h5} * 1.25", - "type": "fontSizes" - }, - "h5": { - "value": "{fontSizes.h6} * 1.25", - "type": "fontSizes" - }, - "h6": { - "value": "{fontSizes.body} * 1", - "type": "fontSizes" - }, - "body": { - "value": "16", - "type": "fontSizes" - }, - "sm": { - "value": "{fontSizes.body} * 0.85", - "type": "fontSizes" - }, - "xs": { - "value": "{fontSizes.body} * 0.65", - "type": "fontSizes" - } - } - } -} \ No newline at end of file diff --git a/frontend/src/app/main/ui/workspace/tokens/form.cljs b/frontend/src/app/main/ui/workspace/tokens/form.cljs index 188cc88a8..dee3b0215 100644 --- a/frontend/src/app/main/ui/workspace/tokens/form.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/form.cljs @@ -205,7 +205,7 @@ Token names should only contain letters and digits separated by . characters.")} selected-set-tokens-tree (mf/use-memo (mf/deps token-path selected-set-tokens) (fn [] - (-> (wtt/token-names-tree selected-set-tokens) + (-> (ctob/tokens-tree selected-set-tokens) ;; Allow setting editing token to it's own path (d/dissoc-in token-path)))) diff --git a/frontend/src/app/main/ui/workspace/tokens/sidebar.cljs b/frontend/src/app/main/ui/workspace/tokens/sidebar.cljs index bb6150ae7..39fea9768 100644 --- a/frontend/src/app/main/ui/workspace/tokens/sidebar.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/sidebar.cljs @@ -8,12 +8,16 @@ (:require-macros [app.main.style :as stl]) (:require [app.common.data :as d] + [app.common.transit :as t] [app.common.types.tokens-lib :as ctob] + [app.main.data.messages :as msg] [app.main.data.modal :as modal] [app.main.data.tokens :as dt] [app.main.refs :as refs] [app.main.store :as st] [app.main.ui.components.color-bullet :refer [color-bullet]] + [app.main.ui.components.dropdown-menu :refer [dropdown-menu + dropdown-menu-item*]] [app.main.ui.components.title-bar :refer [title-bar]] [app.main.ui.hooks :as h] [app.main.ui.hooks.resize :refer [use-resize-hook]] @@ -21,7 +25,6 @@ [app.main.ui.workspace.sidebar.assets.common :as cmm] [app.main.ui.workspace.tokens.changes :as wtch] [app.main.ui.workspace.tokens.context-menu :refer [token-context-menu]] - [app.main.ui.workspace.tokens.core :as wtc] [app.main.ui.workspace.tokens.sets :refer [sets-list]] [app.main.ui.workspace.tokens.sets-context :as sets-context] [app.main.ui.workspace.tokens.sets-context-menu :refer [sets-context-menu]] @@ -30,7 +33,8 @@ [app.main.ui.workspace.tokens.token :as wtt] [app.main.ui.workspace.tokens.token-types :as wtty] [app.util.dom :as dom] - [app.util.storage :refer [storage]] + [app.util.webapi :as wapi] + [beicon.v2.core :as rx] [cuerdas.core :as str] [okulary.core :as l] [rumext.v2 :as mf] @@ -140,7 +144,7 @@ (when open? [:& cmm/asset-section-block {:role :content} [:div {:class (stl/css :token-pills-wrapper)} - (for [token (sort-by :modified-at tokens)] + (for [token (sort-by :name tokens)] (let [theme-token (get active-theme-tokens (wtt/token-identifier token))] [:& token-pill {:key (:name token) @@ -173,10 +177,10 @@ (modal/show! :tokens/themes {}))} (if create? "Create" "Edit")]) -(mf/defc themes-sidebar +(mf/defc themes-header [_props] (let [ordered-themes (mf/deref refs/workspace-token-themes-no-hidden)] - [:div {:class (stl/css :theme-sidebar)} + [:div {:class (stl/css :themes-wrapper)} [:span {:class (stl/css :themes-header)} "Themes"] [:div {:class (stl/css :theme-select-wrapper)} [:& theme-select] @@ -191,13 +195,14 @@ (on-create))} i/add])) -(mf/defc sets-sidebar +(mf/defc themes-sets-tab [] (let [open? (mf/use-state true) on-open (mf/use-fn #(reset! open? true))] [:& sets-context/provider {} [:& sets-context-menu] [:div {:class (stl/css :sets-sidebar)} + [:& themes-header] [:div {:class (stl/css :sidebar-header)} [:& title-bar {:collapsable true :collapsed (not @open?) @@ -209,10 +214,9 @@ [:& h/sortable-container {} [:& sets-list]])]])) -(mf/defc tokens-explorer +(mf/defc tokens-tab [_props] - (let [open? (mf/use-state true) - objects (mf/deref refs/workspace-page-objects) + (let [objects (mf/deref refs/workspace-page-objects) selected (mf/deref refs/selected-shapes) selected-shapes (into [] (keep (d/getf objects)) selected) @@ -222,70 +226,125 @@ tokens (sd/use-resolved-workspace-tokens) token-groups (mf/with-memo [tokens] (sorted-token-groups tokens))] - [:article + [:* [:& token-context-menu] - [:& title-bar {:collapsable true - :collapsed (not @open?) - :all-clickable true - :title "TOKENS" - :on-collapsed #(swap! open? not)}] - (when @open? - [: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}])])])) + [:& 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}])]])) -(defn dev-or-preview-url? [url] - (let [host (-> url js/URL. .-host) - localhost? (= "localhost" (first (str/split host #":"))) - pr? (str/ends-with? host "penpot.alpha.tokens.studio")] - (or localhost? pr?))) +(mf/defc json-import-button [] + (let [] + [:div -(defn location-url-dev-or-preview-url!? [] - (dev-or-preview-url? js/window.location.href)) + [:button {:class (stl/css :download-json-button) + :on-click #(.click (js/document.getElementById "file-input"))} + download-icon + "Import JSON"]])) -(defn temp-use-themes-flag [] - (let [show? (mf/use-state (or - (location-url-dev-or-preview-url!?) - (get @storage ::show-token-themes-sets?) - true))] - (mf/use-effect - (fn [] - (letfn [(toggle! [] - (swap! storage update ::show-token-themes-sets? not) - (reset! show? (get @storage ::show-token-themes-sets?)))] - (set! js/window.toggleThemes toggle!)))) - show?)) +(mf/defc import-export-button + {::mf/wrap-props false} + [{:keys []}] + (let [show-menu* (mf/use-state false) + show-menu? (deref show-menu*) + + open-menu + (mf/use-fn + (fn [event] + (dom/stop-propagation event) + (reset! show-menu* true))) + + close-menu + (mf/use-fn + (fn [event] + (dom/stop-propagation event) + (reset! show-menu* false))) + + input-ref (mf/use-ref) + on-import + (fn [event] + (let [file (-> event .-target .-files (aget 0))] + (->> (wapi/read-file-as-text file) + (rx/map (fn [data] + (try + (t/decode-str data) + (catch js/Error e + (throw (ex-info "Json parse error" + {:user-error "Import Error: Could not parse json" + :type :json-parse-error + :data data + :exception e})))))) + (rx/map (fn [json-data] + (try + (ctob/decode-dtcg-json (ctob/ensure-tokens-lib nil) json-data) + (catch js/Error e + (throw (ex-info "invalid token data" + {:user-error "Import Error: Invalid token data in json." + :type :invalid-token-data + :data json-data + :exception e})))))) + (rx/subs! (fn [lib] + (st/emit! (dt/import-tokens-lib lib))) + (fn [err] + (let [{:keys [user-error]} (ex-data err)] + (st/emit! (msg/show {:content user-error + :notification-type :toast + :type :warning + :timeout 3000})))))) + (set! (.-value (mf/ref-val input-ref)) ""))) + on-export (fn [] + (let [tokens-blob (some-> (deref refs/tokens-lib) + (ctob/encode-dtcg) + (clj->js) + (js/JSON.stringify nil 2) + (wapi/create-blob "application/json"))] + (dom/trigger-download "tokens.json" tokens-blob)))] + [:div {:class (stl/css :import-export-button-wrapper)} + [:input {:type "file" + :ref input-ref + :style {:display "none"} + :id "file-input" + :accept ".json" + :on-change on-import}] + [:button {:class (stl/css :import-export-button) + :on-click open-menu} + download-icon + "Tokens"] + [:& dropdown-menu {:show show-menu? + :on-close close-menu + :list-class (stl/css :import-export-menu)} + [:> dropdown-menu-item* {:class (stl/css :import-export-menu-item) + :on-click #(.click (mf/ref-val input-ref))} + "Import"] + + [:> dropdown-menu-item* {:class (stl/css :import-export-menu-item) + :on-click on-export} + "Export"]]])) (mf/defc tokens-sidebar-tab {::mf/wrap [mf/memo] ::mf/wrap-props false} [_props] - (let [show-sets-section? (deref (temp-use-themes-flag)) - {on-pointer-down-pages :on-pointer-down + (let [{on-pointer-down-pages :on-pointer-down on-lost-pointer-capture-pages :on-lost-pointer-capture on-pointer-move-pages :on-pointer-move size-pages-opened :size} - (use-resize-hook :sitemap 200 38 400 :y false nil)] - [:div {:class (stl/css :sidebar-tab-wrapper)} - (when show-sets-section? - [:div {:class (stl/css :sets-section-wrapper) - :style {:height (str size-pages-opened "px")}} - [:& themes-sidebar] - [:& sets-sidebar]]) - [:div {:class (stl/css :tokens-section-wrapper)} - (when show-sets-section? - [:div {:class (stl/css :resize-area-horiz) - :on-pointer-down on-pointer-down-pages - :on-lost-pointer-capture on-lost-pointer-capture-pages - :on-pointer-move on-pointer-move-pages}]) - [:& tokens-explorer]] - [:button {:class (stl/css :download-json-button) - :on-click wtc/download-tokens-as-json} - download-icon - "Export JSON"]])) + (use-resize-hook :tokens 200 38 400 :y false nil)] + [:div {:class (stl/css :sidebar-wrapper)} + [:article {:class (stl/css :sets-section-wrapper) + :style {"--resize-height" (str size-pages-opened "px")}} + [:& themes-sets-tab]] + [:article {:class (stl/css :tokens-section-wrapper)} + [:div {:class (stl/css :resize-area-horiz) + :on-pointer-down on-pointer-down-pages + :on-lost-pointer-capture on-lost-pointer-capture-pages + :on-pointer-move on-pointer-move-pages}] + [:& tokens-tab] + [:& import-export-button]]])) diff --git a/frontend/src/app/main/ui/workspace/tokens/sidebar.scss b/frontend/src/app/main/ui/workspace/tokens/sidebar.scss index ed2f1a9b7..aede52260 100644 --- a/frontend/src/app/main/ui/workspace/tokens/sidebar.scss +++ b/frontend/src/app/main/ui/workspace/tokens/sidebar.scss @@ -7,25 +7,37 @@ @import "refactor/common-refactor.scss"; @import "./common.scss"; -.sidebar-tab-wrapper { - display: flex; - flex-direction: column; - height: 100%; +.sidebar-wrapper { + display: grid; + grid-template-rows: auto auto 1fr; + // Overflow on the bottom section can't be done without hardcoded values for the height + // This has to be changed from the wrapping sidebar styles + height: calc(100vh - #{$s-84}); overflow: hidden; } .sets-section-wrapper { + position: relative; display: flex; + flex: 1; + height: var(--resize-height); flex-direction: column; - margin-bottom: $s-8; overflow-y: auto; + scrollbar-gutter: stable; +} + +.tokens-section-wrapper { + height: 100%; + padding-left: $s-12; + overflow-y: auto; + scrollbar-gutter: stable; } .sets-sidebar { position: relative; } -.theme-sidebar { +.themes-wrapper { padding: $s-12; padding-bottom: 0; } @@ -52,18 +64,6 @@ } } -.tokens-section-wrapper { - flex: 1; - padding-top: $s-12; - padding-left: $s-12; - overflow-y: auto; -} - -// TODO Remove once sets are available to public -.sets-section-wrapper + .tokens-section-wrapper { - padding-top: 0; -} - .token-pills-wrapper { display: flex; gap: $s-4; @@ -103,11 +103,14 @@ translate: 0px -1px; } -.download-json-button { - @extend .button-secondary; +.import-export-button-wrapper { position: absolute; bottom: $s-12; right: $s-12; +} + +.import-export-button { + @extend .button-secondary; display: flex; align-items: center; padding: $s-6 $s-8; @@ -122,6 +125,38 @@ } } +.import-export-menu { + @extend .menu-dropdown; + top: -#{$s-6}; + right: 0; + translate: 0 -100%; + width: $s-192; + margin: 0; +} + +.import-export-menu-item { + @extend .menu-item-base; + cursor: pointer; + .open-arrow { + @include flexCenter; + svg { + @extend .button-icon; + stroke: var(--icon-foreground); + } + } + &:hover { + color: var(--menu-foreground-color-hover); + .open-arrow { + svg { + stroke: var(--menu-foreground-color-hover); + } + } + .shortcut-key { + color: var(--menu-shortcut-foreground-color-hover); + } + } +} + .theme-select-wrapper { display: grid; grid-template-columns: 1fr 0.28fr; diff --git a/frontend/src/app/main/ui/workspace/tokens/token.cljs b/frontend/src/app/main/ui/workspace/tokens/token.cljs index b12ce4e55..8783f4732 100644 --- a/frontend/src/app/main/ui/workspace/tokens/token.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/token.cljs @@ -1,7 +1,6 @@ (ns app.main.ui.workspace.tokens.token (:require [app.common.data :as d] - [app.common.data.macros :as dm] [app.main.ui.workspace.tokens.tinycolor :as tinycolor] [clojure.set :as set] [cuerdas.core :as str])) @@ -22,13 +21,6 @@ {:value parsed-value :unit unit})))) -(defn find-token-references - "Finds token reference values in `value-string` and returns a set with all contained namespaces." - [value-string] - (some->> (re-seq #"\{([^}]*)\}" value-string) - (map second) - (into #{}))) - (defn token-identifier [{:keys [name] :as _token}] name) @@ -96,14 +88,6 @@ {:path (seq path) :selector selector})) -(defn token-names-map - "Convert tokens into a map with their `:name` as the key. - - E.g.: {\"sm\" {:token-type :border-radius :id #uuid \"000\" ...}}" - [tokens] - (->> (map (fn [{:keys [name] :as token}] [name token]) tokens) - (into {}))) - (defn token-names-tree-id-map [tokens] (reduce (fn [acc [_ {:keys [name] :as token}]] @@ -117,16 +101,6 @@ :ids-map {}} tokens)) -(defn token-names-tree - "Convert tokens into a nested tree with their `:name` as the path." - [tokens] - (reduce - (fn [acc [_ {:keys [name] :as token}]] - (when (string? name) - (let [path (token-name->path name)] - (assoc-in acc path token)))) - {} tokens)) - (defn token-name-path-exists? "Traverses the path from `token-name` down a `token-tree` and checks if a token at that path exists. diff --git a/frontend/test/token_tests/logic/token_actions_test.cljs b/frontend/test/token_tests/logic/token_actions_test.cljs index d710c9bf1..9c79dcb90 100644 --- a/frontend/test/token_tests/logic/token_actions_test.cljs +++ b/frontend/test/token_tests/logic/token_actions_test.cljs @@ -6,6 +6,7 @@ [app.common.test-helpers.shapes :as cths] [app.common.types.tokens-lib :as ctob] [app.main.ui.workspace.tokens.changes :as wtch] + [app.main.ui.workspace.tokens.token :as wtt] [cljs.test :as t :include-macros true] [frontend-tests.helpers.pages :as thp] [frontend-tests.helpers.state :as ths] @@ -133,34 +134,6 @@ (t/testing "while :r4 was kept with borderRadius.sm" (t/is (= (:r4 (:applied-tokens rect-1')) (:name token-sm))))))))))) -(t/deftest test-apply-dimensions - (t/testing "applies dimensions token and updates the shapes width and height" - (t/async - done - (let [file (-> (setup-file-with-tokens) - (toht/add-token :token-target {:value "100" - :name "dimensions.sm" - :type :dimensions})) - store (ths/setup-store file) - rect-1 (cths/get-shape file :rect-1) - events [(wtch/apply-token {:shape-ids [(:id rect-1)] - :attributes #{:width :height} - :token (toht/get-token file :token-target) - :on-update-shape wtch/update-shape-dimensions})]] - (tohs/run-store-async - store done events - (fn [new-state] - (let [file' (ths/get-file-from-store new-state) - token-target' (toht/get-token file' :token-target) - rect-1' (cths/get-shape file' :rect-1)] - (t/testing "shape `:applied-tokens` got updated" - (t/is (some? (:applied-tokens rect-1'))) - (t/is (= (:width (:applied-tokens rect-1')) (wtt/token-identifier token-target'))) - (t/is (= (:height (:applied-tokens rect-1')) (wtt/token-identifier token-target')))) - (t/testing "shapes width and height got updated" - (t/is (= (:width rect-1') 100)) - (t/is (= (:height rect-1') 100)))))))))) - (t/deftest test-apply-dimensions (t/testing "applies dimensions token and updates the shapes width and height" (t/async diff --git a/frontend/test/token_tests/style_dictionary_test.cljs b/frontend/test/token_tests/style_dictionary_test.cljs index 07b407e13..6ec097138 100644 --- a/frontend/test/token_tests/style_dictionary_test.cljs +++ b/frontend/test/token_tests/style_dictionary_test.cljs @@ -39,22 +39,3 @@ (t/is (= 24 (get-in resolved-tokens ["borderRadius.md-with-dashes" :resolved-value]))) (t/is (= "px" (get-in resolved-tokens ["borderRadius.md-with-dashes" :unit]))) (done)))))))) - -(t/deftest resolve-tokens-names-map-test - (t/async - done - (t/testing "resolves tokens using style-dictionary from a names map" - (-> (vals tokens) - (wtt/token-names-map) - (sd/resolve-tokens+ {:names-map? true}) - (p/finally (fn [resolved-tokens] - (let [expected-tokens {"borderRadius.sm" - (assoc border-radius-token - :resolved-value 12 - :unit "px") - "borderRadius.md-with-dashes" - (assoc reference-border-radius-token - :resolved-value 24 - :unit "px")}] - (t/is (= expected-tokens resolved-tokens)) - (done)))))))) diff --git a/frontend/test/token_tests/token_test.cljs b/frontend/test/token_tests/token_test.cljs index 1157125ad..2aca8fda2 100644 --- a/frontend/test/token_tests/token_test.cljs +++ b/frontend/test/token_tests/token_test.cljs @@ -91,26 +91,6 @@ (t/is (= ["foo" "bar" "baz"] (wtt/token-name->path "foo..bar.baz"))) (t/is (= ["foo" "bar" "baz"] (wtt/token-name->path "foo..bar.baz....")))) -(t/deftest tokens-name-map-test - (t/testing "creates a a names map from tokens" - (t/is (= {"border-radius.sm" {:name "border-radius.sm", :value "10"} - "border-radius.md" {:name "border-radius.md", :value "20"}} - (wtt/token-names-map [{:name "border-radius.sm" :value "10"} - {:name "border-radius.md" :value "20"}]))))) - -(t/deftest tokens-name-tree-test - (t/is (= {"foo" - {"bar" - {"baz" {:name "foo.bar.baz", :value "a"}, - "bam" {:name "foo.bar.bam", :value "b"}}}, - "baz" {"bar" {"foo" {:name "baz.bar.foo", :value "{foo.bar.baz}"}}}} - (wtt/token-names-tree {:a {:name "foo.bar.baz" - :value "a"} - :b {:name "foo.bar.bam" - :value "b"} - :c {:name "baz.bar.foo" - :value "{foo.bar.baz}"}})))) - (t/deftest token-name-path-exists?-test (t/is (true? (wtt/token-name-path-exists? "border-radius" {"border-radius" {"sm" {:name "sm"}}}))) (t/is (true? (wtt/token-name-path-exists? "border-radius" {"border-radius" {:name "sm"}})))