diff --git a/common/src/app/common/types/tokens_lib.cljc b/common/src/app/common/types/tokens_lib.cljc index 7213dca372..2e52588800 100644 --- a/common/src/app/common/types/tokens_lib.cljc +++ b/common/src/app/common/types/tokens_lib.cljc @@ -670,6 +670,57 @@ used for managing active sets without a user created theme.") ;; === Import / Export from DTCG format +(def ^:private legacy-node? + (sm/validator + [:or + [:map + ["value" :string] + ["type" :string]] + [:map + ["value" [:sequential [:map ["type" :string]]]] + ["type" :string]] + [:map + ["value" :map] + ["type" :string]]])) + +(def ^:private dtcg-node? + (sm/validator + [:or + [:map + ["$value" :string] + ["$type" :string]] + [:map + ["$value" [:sequential [:map ["$type" :string]]]] + ["$type" :string]] + [:map + ["$value" :map] + ["$type" :string]]])) + +(defn has-legacy-format? + "Searches through parsed token file and returns: + - true when first node satisfies `legacy-node?` predicate + - false when first node satisfies `dtcg-node?` predicate + - nil if neither combination is found" + ([data] + (has-legacy-format? data legacy-node? dtcg-node?)) + ([data legacy-node? dtcg-node?] + (let [branch? map? + children (fn [node] (vals node)) + check-node (fn [node] + (cond + (legacy-node? node) true + (dtcg-node? node) false + :else nil)) + walk (fn walk [node] + (lazy-seq + (cons + (check-node node) + (when (branch? node) + (mapcat walk (children node))))))] + (->> (walk data) + (filter some?) + first)))) + (defn walk-sets-tree-seq "Walk sets tree as a flat list. @@ -762,6 +813,7 @@ Will return a value that matches this schema: (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") + (decode-legacy-json [_ parsed-json] "Decodes parsed legacy json containing tokens and converts to library") (get-all-tokens [_] "all tokens in the lib") (validate [_])) @@ -1187,6 +1239,27 @@ Will return a value that matches this schema: lib' themes-data) lib'))) + (decode-legacy-json [this parsed-legacy-json] + (let [other-data (select-keys parsed-legacy-json ["$themes" "$metadata"]) + sets-data (dissoc parsed-legacy-json "$themes" "$metadata") + dtcg-sets-data (walk/postwalk + (fn [node] + (cond-> node + (and (map? node) + (contains? node "value") + (sequential? (get node "value"))) + (update "value" + (fn [seq-value] + (map #(set/rename-keys % {"type" "$type"}) seq-value))) + + (and (map? node) + (and (contains? node "type") + (contains? node "value"))) + (set/rename-keys {"value" "$value" + "type" "$type"}))) + sets-data)] + (decode-dtcg-json this (merge other-data + dtcg-sets-data)))) (get-all-tokens [this] (reduce (fn [tokens' set] diff --git a/common/test/common_tests/types/data/tokens-multi-set-legacy-example.json b/common/test/common_tests/types/data/tokens-multi-set-legacy-example.json new file mode 100644 index 0000000000..18c8b665a2 --- /dev/null +++ b/common/test/common_tests/types/data/tokens-multi-set-legacy-example.json @@ -0,0 +1,810 @@ +{ + "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": [ { + "name": "theme-1", + "group": "group-1", + "description": null, + "is-source": false, + "modified-at": "2024-01-01T00:00:00.000+00:00", + "selectedTokenSets": {"light": "enabled"} + } ], + "$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 66b43e112e..ada669d7a6 100644 --- a/common/test/common_tests/types/tokens_lib_test.cljc +++ b/common/test/common_tests/types/tokens_lib_test.cljc @@ -1145,6 +1145,41 @@ (t/is (= (count themes-tree') 1)) (t/is (nil? token-theme')))))) +#?(:clj + (t/deftest legacy-json-decoding + (t/testing "decode-legacy-json" + (let [json (-> (slurp "test/common_tests/types/data/tokens-multi-set-legacy-example.json") + (tr/decode-str)) + lib (ctob/decode-legacy-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))) + 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" + :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")))))))) + #?(:clj (t/deftest dtcg-encoding-decoding (t/testing "decode-dtcg-json" diff --git a/frontend/src/app/main/ui/workspace/tokens/sidebar.cljs b/frontend/src/app/main/ui/workspace/tokens/sidebar.cljs index 6d41eb28ee..975a31bd14 100644 --- a/frontend/src/app/main/ui/workspace/tokens/sidebar.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/sidebar.cljs @@ -324,6 +324,7 @@ :type :toast :level :error}))))) (set! (.-value (mf/ref-val input-ref)) ""))) + on-export (fn [] (st/emit! (ptk/event ::ev/event {::ev/name "export-tokens"})) (let [tokens-json (some-> (deref refs/tokens-lib) @@ -352,7 +353,6 @@ [:> dropdown-menu-item* {:class (stl/css :import-export-menu-item) :on-click on-display-file-explorer} (tr "labels.import")]) - [:> dropdown-menu-item* {:class (stl/css :import-export-menu-item) :on-click on-export} (tr "labels.export")]]])) 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 8383573cd6..005e7ee65b 100644 --- a/frontend/src/app/main/ui/workspace/tokens/style_dictionary.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/style_dictionary.cljs @@ -188,7 +188,9 @@ (throw (wte/error-ex-info :error.import/json-parse-error data e)))))) (rx/map (fn [json-data] (try - (ctob/decode-dtcg-json (ctob/ensure-tokens-lib nil) json-data) + (if-not (ctob/has-legacy-format? json-data) + (ctob/decode-dtcg-json (ctob/ensure-tokens-lib nil) json-data) + (ctob/decode-legacy-json (ctob/ensure-tokens-lib nil) json-data)) (catch js/Error e (throw (wte/error-ex-info :error.import/invalid-json-data json-data e)))))) (rx/mapcat (fn [tokens-lib] diff --git a/frontend/translations/en.po b/frontend/translations/en.po index 4d3b52e31e..e10f34d0c3 100644 --- a/frontend/translations/en.po +++ b/frontend/translations/en.po @@ -1886,6 +1886,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"