From 81e0e4f222f3d2562aa014e984c2e8f89d4ce9c5 Mon Sep 17 00:00:00 2001 From: Eva Marco Date: Fri, 5 Dec 2025 17:04:07 +0100 Subject: [PATCH] :recycle: Replace token form files (#7896) * :recycle: Replace shadow form * :recycle: Rename files and components * :recycle: Replace offsetx and offsety names * :recycle: Replace form file for new form component using new form system * :recycle: Rename files and props --- common/src/app/common/types/tokens_lib.cljc | 14 +- .../common_tests/types/tokens_lib_test.cljc | 16 +- .../data/workspace/get-file-inspect-tab.json | 4 +- frontend/playwright/ui/specs/tokens.spec.js | 80 +- .../src/app/main/data/style_dictionary.cljs | 59 +- .../data/workspace/tokens/application.cljs | 6 +- .../main/data/workspace/tokens/errors.cljs | 4 + .../main/data/workspace/tokens/format.cljs | 4 +- .../src/app/main/ui/ds/controls/select.cljs | 1 + .../ui/ds/controls/utilities/input_field.cljs | 7 +- .../src/app/main/ui/ds/utilities/swatch.scss | 2 +- frontend/src/app/main/ui/workspace.cljs | 2 +- .../management/create/border_radius.scss | 58 - .../tokens/management/create/color.cljs | 224 --- .../tokens/management/create/color.scss | 58 - .../tokens/management/create/dimensions.cljs | 223 --- .../tokens/management/create/font_family.cljs | 221 --- .../tokens/management/create/form.cljs | 1455 ----------------- .../tokens/management/create/form.scss | 105 -- .../create/form_color_input_token.cljs | 237 --- .../management/create/form_input_token.cljs | 211 --- .../create/input_token_color_bullet.cljs | 28 - .../create/input_token_color_bullet.scss | 28 - .../management/create/input_tokens_value.cljs | 78 - .../management/create/input_tokens_value.scss | 36 - .../tokens/management/create/text_case.cljs | 224 --- .../tokens/management/create/text_case.scss | 58 - .../tokens/management/create/typography.cljs | 427 ----- .../tokens/management/forms/color.cljs | 53 + .../tokens/management/forms/controls.cljs | 19 + .../forms/controls/color_input.cljs | 459 ++++++ .../controls/fonts_combobox.cljs} | 31 +- .../controls/fonts_combobox.scss} | 0 .../management/forms/controls/input.cljs | 430 +++++ .../management/forms/controls/select.cljs | 45 + .../tokens/management/forms/font_family.cljs | 40 + .../management/forms/form_container.cljs | 53 + .../generic_form.cljs} | 93 +- .../generic_form.scss} | 0 .../management/{create => forms}/modals.cljs | 46 +- .../management/{create => forms}/modals.scss | 0 .../tokens/management/forms/shadow.cljs | 362 ++++ .../typography.scss => forms/shadow.scss} | 57 +- .../tokens/management/forms/typography.cljs | 304 ++++ .../dimensions.scss => forms/typography.scss} | 36 +- .../tokens/management/forms/validators.cljs | 111 ++ .../ui/workspace/tokens/management/group.cljs | 1 + frontend/src/app/plugins/format.cljs | 8 +- frontend/src/app/plugins/parser.cljs | 6 +- frontend/src/app/plugins/tokens.cljs | 4 +- .../tokens/logic/token_actions_test.cljs | 4 +- frontend/translations/en.po | 8 + frontend/translations/es.po | 4 + 53 files changed, 2171 insertions(+), 3873 deletions(-) delete mode 100644 frontend/src/app/main/ui/workspace/tokens/management/create/border_radius.scss delete mode 100644 frontend/src/app/main/ui/workspace/tokens/management/create/color.cljs delete mode 100644 frontend/src/app/main/ui/workspace/tokens/management/create/color.scss delete mode 100644 frontend/src/app/main/ui/workspace/tokens/management/create/dimensions.cljs delete mode 100644 frontend/src/app/main/ui/workspace/tokens/management/create/font_family.cljs delete mode 100644 frontend/src/app/main/ui/workspace/tokens/management/create/form.cljs delete mode 100644 frontend/src/app/main/ui/workspace/tokens/management/create/form.scss delete mode 100644 frontend/src/app/main/ui/workspace/tokens/management/create/form_color_input_token.cljs delete mode 100644 frontend/src/app/main/ui/workspace/tokens/management/create/form_input_token.cljs delete mode 100644 frontend/src/app/main/ui/workspace/tokens/management/create/input_token_color_bullet.cljs delete mode 100644 frontend/src/app/main/ui/workspace/tokens/management/create/input_token_color_bullet.scss delete mode 100644 frontend/src/app/main/ui/workspace/tokens/management/create/input_tokens_value.cljs delete mode 100644 frontend/src/app/main/ui/workspace/tokens/management/create/input_tokens_value.scss delete mode 100644 frontend/src/app/main/ui/workspace/tokens/management/create/text_case.cljs delete mode 100644 frontend/src/app/main/ui/workspace/tokens/management/create/text_case.scss delete mode 100644 frontend/src/app/main/ui/workspace/tokens/management/create/typography.cljs create mode 100644 frontend/src/app/main/ui/workspace/tokens/management/forms/color.cljs create mode 100644 frontend/src/app/main/ui/workspace/tokens/management/forms/controls.cljs create mode 100644 frontend/src/app/main/ui/workspace/tokens/management/forms/controls/color_input.cljs rename frontend/src/app/main/ui/workspace/tokens/management/{create/combobox_token_fonts.cljs => forms/controls/fonts_combobox.cljs} (88%) rename frontend/src/app/main/ui/workspace/tokens/management/{create/combobox_token_fonts.scss => forms/controls/fonts_combobox.scss} (100%) create mode 100644 frontend/src/app/main/ui/workspace/tokens/management/forms/controls/input.cljs create mode 100644 frontend/src/app/main/ui/workspace/tokens/management/forms/controls/select.cljs create mode 100644 frontend/src/app/main/ui/workspace/tokens/management/forms/font_family.cljs create mode 100644 frontend/src/app/main/ui/workspace/tokens/management/forms/form_container.cljs rename frontend/src/app/main/ui/workspace/tokens/management/{create/border_radius.cljs => forms/generic_form.cljs} (75%) rename frontend/src/app/main/ui/workspace/tokens/management/{create/font_family.scss => forms/generic_form.scss} (100%) rename frontend/src/app/main/ui/workspace/tokens/management/{create => forms}/modals.cljs (82%) rename frontend/src/app/main/ui/workspace/tokens/management/{create => forms}/modals.scss (100%) create mode 100644 frontend/src/app/main/ui/workspace/tokens/management/forms/shadow.cljs rename frontend/src/app/main/ui/workspace/tokens/management/{create/typography.scss => forms/shadow.scss} (68%) create mode 100644 frontend/src/app/main/ui/workspace/tokens/management/forms/typography.cljs rename frontend/src/app/main/ui/workspace/tokens/management/{create/dimensions.scss => forms/typography.scss} (59%) create mode 100644 frontend/src/app/main/ui/workspace/tokens/management/forms/validators.cljs diff --git a/common/src/app/common/types/tokens_lib.cljc b/common/src/app/common/types/tokens_lib.cljc index 70e36bfc2f..930b1f8e05 100644 --- a/common/src/app/common/types/tokens_lib.cljc +++ b/common/src/app/common/types/tokens_lib.cljc @@ -1575,10 +1575,10 @@ Will return a value that matches this schema: (if (map? shadow) (let [legacy-shadow-type (get "type" shadow)] (-> shadow - (set/rename-keys {"x" :offsetX - "offsetX" :offsetX - "y" :offsetY - "offsetY" :offsetY + (set/rename-keys {"x" :offset-x + "offsetX" :offset-x + "y" :offset-y + "offsetY" :offset-y "blur" :blur "spread" :spread "color" :color @@ -1589,7 +1589,7 @@ Will return a value that matches this schema: (= "false" %) false (= legacy-shadow-type "innerShadow") true :else false)) - (select-keys [:offsetX :offsetY :blur :spread :color :inset]))) + (select-keys [:offset-x :offset-y :blur :spread :color :inset]))) shadow))] (cond ;; Reference value - keep as string @@ -1860,8 +1860,8 @@ Will return a value that matches this schema: (mapv (fn [shadow] (if (map? shadow) (-> shadow - (set/rename-keys {:offsetX "offsetX" - :offsetY "offsetY" + (set/rename-keys {:offset-x "offsetX" + :offset-y "offsetY" :blur "blur" :spread "spread" :color "color" diff --git a/common/test/common_tests/types/tokens_lib_test.cljc b/common/test/common_tests/types/tokens_lib_test.cljc index d34888cf91..30bd570860 100644 --- a/common/test/common_tests/types/tokens_lib_test.cljc +++ b/common/test/common_tests/types/tokens_lib_test.cljc @@ -1897,15 +1897,15 @@ (let [token (ctob/get-token-by-name lib "shadow-test" "test.shadow-single")] (t/is (some? token)) (t/is (= :shadow (:type token))) - (t/is (= [{:offsetX "0", :offsetY "2px", :blur "4px", :spread "0", :color "#000", :inset false}] + (t/is (= [{:offset-x "0", :offset-y "2px", :blur "4px", :spread "0", :color "#000", :inset false}] (:value token))))) (t/testing "multiple shadow token" (let [token (ctob/get-token-by-name lib "shadow-test" "test.shadow-multiple")] (t/is (some? token)) (t/is (= :shadow (:type token))) - (t/is (= [{:offsetX "0", :offsetY "2px", :blur "4px", :spread "0", :color "#000", :inset true} - {:offsetX "0", :offsetY "8px", :blur "16px", :spread "0", :color "#000", :inset true}] + (t/is (= [{:offset-x "0", :offset-y "2px", :blur "4px", :spread "0", :color "#000", :inset true} + {:offset-x "0", :offset-y "8px", :blur "16px", :spread "0", :color "#000", :inset true}] (:value token))))) (t/testing "shadow token with reference" @@ -1918,7 +1918,7 @@ (let [token (ctob/get-token-by-name lib "shadow-test" "test.shadow-with-type")] (t/is (some? token)) (t/is (= :shadow (:type token))) - (t/is (= [{:offsetX "0", :offsetY "4px", :blur "8px", :spread "0", :color "rgba(0,0,0,0.2)", :inset false}] + (t/is (= [{:offset-x "0", :offset-y "4px", :blur "8px", :spread "0", :color "rgba(0,0,0,0.2)", :inset false}] (:value token))))) (t/testing "shadow token with description" @@ -1937,14 +1937,14 @@ (ctob/make-token {:name "shadow.single" :type :shadow - :value [{:offsetX "0" :offsetY "2px" :blur "4px" :spread "0" :color "#0000001A"}] + :value [{:offset-x "0" :offset-y "2px" :blur "4px" :spread "0" :color "#0000001A"}] :description "A single shadow"}) "shadow.multiple" (ctob/make-token {:name "shadow.multiple" :type :shadow - :value [{:offsetX "0" :offsetY "2px" :blur "4px" :spread "0" :color "#0000001A"} - {:offsetX "0" :offsetY "8px" :blur "16px" :spread "0" :color "#0000001A"}]}) + :value [{:offset-x "0" :offset-y "2px" :blur "4px" :spread "0" :color "#0000001A"} + {:offset-x "0" :offset-y "8px" :blur "16px" :spread "0" :color "#0000001A"}]}) "shadow.ref" (ctob/make-token {:name "shadow.ref" @@ -1991,7 +1991,7 @@ (ctob/make-token {:name "shadow.test" :type :shadow - :value [{:offsetX "1" :offsetY "1" :blur "1" :spread "1" :color "red" :inset true}] + :value [{:offset-x "1" :offset-y "1" :blur "1" :spread "1" :color "red" :inset true}] :description "Round trip test"}) "shadow.ref" (ctob/make-token diff --git a/frontend/playwright/data/workspace/get-file-inspect-tab.json b/frontend/playwright/data/workspace/get-file-inspect-tab.json index 765631ae8a..37220dad65 100644 --- a/frontend/playwright/data/workspace/get-file-inspect-tab.json +++ b/frontend/playwright/data/workspace/get-file-inspect-tab.json @@ -5947,8 +5947,8 @@ "~:spread": "10", "~:color": "rgb(160, 73, 73)", "~:inset": true, - "~:offsetX": "10", - "~:offsetY": "10" + "~:offset-x": "10", + "~:offset-y": "10" } ], "~:description": "", diff --git a/frontend/playwright/ui/specs/tokens.spec.js b/frontend/playwright/ui/specs/tokens.spec.js index c2d2796caf..941e901f71 100644 --- a/frontend/playwright/ui/specs/tokens.spec.js +++ b/frontend/playwright/ui/specs/tokens.spec.js @@ -1239,8 +1239,12 @@ test.describe("Tokens: Apply token", () => { // Fill in the shadow values const offsetXInput = firstShadowFields.getByLabel("X"); const offsetYInput = firstShadowFields.getByLabel("Y"); - const blurInput = firstShadowFields.getByLabel("Blur"); - const spreadInput = firstShadowFields.getByLabel("Spread"); + const blurInput = firstShadowFields.getByRole("textbox", { + name: "Blur", + }); + const spreadInput = firstShadowFields.getByRole("textbox", { + name: "Spread", + }); await offsetXInput.fill("2"); await offsetYInput.fill("2"); @@ -1261,9 +1265,10 @@ test.describe("Tokens: Apply token", () => { await valueSaturationSelector.click({ position: { x: 50, y: 50 } }); // Verify that a color value was set - const colorInput = firstShadowFields.getByLabel("Color"); - const firstColorValue = await colorInput.inputValue(); - await expect(firstColorValue).toMatch(/^rgb(.*)$/); + const colorInput = firstShadowFields.getByRole("textbox", { + name: "Color", + }); + await expect(colorInput).toHaveValue(/^rgb(.*)$/); // Wait for validation to complete await expect( @@ -1281,11 +1286,15 @@ test.describe("Tokens: Apply token", () => { const firstShadowFields = tokensUpdateCreateModal.getByTestId( "shadow-input-fields-0", ); - const colorInput = firstShadowFields.getByLabel("Color"); + const colorInput = firstShadowFields.getByRole("textbox", { + name: "Color", + }); const firstColorValue = await colorInput.inputValue(); // User adds a second shadow - const addButton = firstShadowFields.getByTestId("shadow-add-button-0"); + const addButton = tokensUpdateCreateModal.getByRole("button", { + name: "Add Shadow", + }); await addButton.click(); const secondShadowFields = tokensUpdateCreateModal.getByTestId( @@ -1294,8 +1303,7 @@ test.describe("Tokens: Apply token", () => { await expect(secondShadowFields).toBeVisible(); // User adds a third shadow - const addButton2 = secondShadowFields.getByTestId("shadow-add-button-1"); - await addButton2.click(); + await addButton.click(); const thirdShadowFields = tokensUpdateCreateModal.getByTestId( "shadow-input-fields-2", @@ -1305,9 +1313,15 @@ test.describe("Tokens: Apply token", () => { // User adds values for the third shadow const thirdOffsetXInput = thirdShadowFields.getByLabel("X"); const thirdOffsetYInput = thirdShadowFields.getByLabel("Y"); - const thirdBlurInput = thirdShadowFields.getByLabel("Blur"); - const thirdSpreadInput = thirdShadowFields.getByLabel("Spread"); - const thirdColorInput = thirdShadowFields.getByLabel("Color"); + const thirdBlurInput = thirdShadowFields.getByRole("textbox", { + name: "Blur", + }); + const thirdSpreadInput = thirdShadowFields.getByRole("textbox", { + name: "Spread", + }); + const thirdColorInput = thirdShadowFields.getByRole("textbox", { + name: "Color", + }); await thirdOffsetXInput.fill("10"); await thirdOffsetYInput.fill("10"); @@ -1316,15 +1330,13 @@ test.describe("Tokens: Apply token", () => { await thirdColorInput.fill("#FF0000"); // User removes the 2nd shadow - const removeButton2 = secondShadowFields.getByTestId( - "shadow-remove-button-1", - ); + const removeButton2 = secondShadowFields.getByRole("button", { + name: "Remove Shadow", + }); await removeButton2.click(); - // Verify second shadow is removed - await expect( - secondShadowFields.getByTestId("shadow-add-button-3"), - ).not.toBeVisible(); + // Verify that we have only two shadow fields + await expect(thirdShadowFields).not.toBeVisible(); // Verify that the first shadow kept its values const firstOffsetXValue = await firstShadowFields @@ -1334,13 +1346,13 @@ test.describe("Tokens: Apply token", () => { .getByLabel("Y") .inputValue(); const firstBlurValue = await firstShadowFields - .getByLabel("Blur") + .getByRole("textbox", { name: "Blur" }) .inputValue(); const firstSpreadValue = await firstShadowFields - .getByLabel("Spread") + .getByRole("textbox", { name: "Spread" }) .inputValue(); const firstColorValueAfter = await firstShadowFields - .getByLabel("Color") + .getByRole("textbox", { name: "Color" }) .inputValue(); await expect(firstOffsetXValue).toBe("2"); @@ -1349,7 +1361,7 @@ test.describe("Tokens: Apply token", () => { await expect(firstSpreadValue).toBe("0"); await expect(firstColorValueAfter).toBe(firstColorValue); - // Verify that the third shadow (now second) kept its values + // Verify that the second kept its values (after shadow 3) // After removing index 1, the third shadow becomes the second shadow at index 1 const newSecondShadowFields = tokensUpdateCreateModal.getByTestId( "shadow-input-fields-1", @@ -1363,13 +1375,13 @@ test.describe("Tokens: Apply token", () => { .getByLabel("Y") .inputValue(); const secondBlurValue = await newSecondShadowFields - .getByLabel("Blur") + .getByRole("textbox", { name: "Blur" }) .inputValue(); const secondSpreadValue = await newSecondShadowFields - .getByLabel("Spread") + .getByRole("textbox", { name: "Spread" }) .inputValue(); const secondColorValue = await newSecondShadowFields - .getByLabel("Color") + .getByRole("textbox", { name: "Color" }) .inputValue(); await expect(secondOffsetXValue).toBe("10"); @@ -1386,7 +1398,9 @@ test.describe("Tokens: Apply token", () => { const newSecondShadowFields = tokensUpdateCreateModal.getByTestId( "shadow-input-fields-1", ); - const colorInput = firstShadowFields.getByLabel("Color"); + const colorInput = firstShadowFields.getByRole("textbox", { + name: "Color", + }); const firstColorValue = await colorInput.inputValue(); // Switch to reference tab @@ -1414,13 +1428,13 @@ test.describe("Tokens: Apply token", () => { .getByLabel("Y") .inputValue(); const restoredFirstBlur = await firstShadowFields - .getByLabel("Blur") + .getByRole("textbox", { name: "Blur" }) .inputValue(); const restoredFirstSpread = await firstShadowFields - .getByLabel("Spread") + .getByRole("textbox", { name: "Spread" }) .inputValue(); const restoredFirstColor = await firstShadowFields - .getByLabel("Color") + .getByRole("textbox", { name: "Color" }) .inputValue(); await expect(restoredFirstOffsetX).toBe("2"); @@ -1437,13 +1451,13 @@ test.describe("Tokens: Apply token", () => { .getByLabel("Y") .inputValue(); const restoredSecondBlur = await newSecondShadowFields - .getByLabel("Blur") + .getByRole("textbox", { name: "Blur" }) .inputValue(); const restoredSecondSpread = await newSecondShadowFields - .getByLabel("Spread") + .getByRole("textbox", { name: "Spread" }) .inputValue(); const restoredSecondColor = await newSecondShadowFields - .getByLabel("Color") + .getByRole("textbox", { name: "Color" }) .inputValue(); await expect(restoredSecondOffsetX).toBe("10"); diff --git a/frontend/src/app/main/data/style_dictionary.cljs b/frontend/src/app/main/data/style_dictionary.cljs index 5005dedeed..3c869e17c2 100644 --- a/frontend/src/app/main/data/style_dictionary.cljs +++ b/frontend/src/app/main/data/style_dictionary.cljs @@ -59,9 +59,15 @@ "Parses `value` of a color `sd-token` into a map like `{:value 1 :unit \"px\"}`. If the value is not parseable and/or has missing references returns a map with `:errors`." [value] - (if-let [tc (tinycolor/valid-color value)] - {:value value :unit (tinycolor/color-format tc)} - {:errors [(wte/error-with-value :error.token/invalid-color value)]})) + (let [missing-references (seq (cto/find-token-value-references value))] + (if-let [tc (tinycolor/valid-color value)] + {:value value :unit (tinycolor/color-format tc)} + (cond + missing-references + {:errors [(wte/error-with-value :error.style-dictionary/missing-reference missing-references)] + :references missing-references} + :else + {:errors [(wte/error-with-value :error.token/invalid-color value)]})))) (defn- numeric-string? [s] (and (string? s) @@ -120,7 +126,7 @@ If the `value` is not parseable and/or has missing references returns a map with `:errors`. If the `value` is parseable but is out of range returns a map with `warnings`." [value] - (let [missing-references? (seq (cto/find-token-value-references value)) + (let [missing-references? (seq (seq (cto/find-token-value-references value))) parsed-value (cft/parse-token-value value) out-of-scope (not (<= 0 (:value parsed-value) 1)) references (seq (cto/find-token-value-references value))] @@ -368,8 +374,8 @@ (let [add-keyed-errors (fn [shadow-result k errors] (update shadow-result :errors concat (map #(assoc % :shadow-key k :shadow-index shadow-index) errors))) - parsers {:offsetX parse-sd-token-general-value - :offsetY parse-sd-token-general-value + parsers {:offset-x parse-sd-token-general-value + :offset-y parse-sd-token-general-value :blur parse-sd-token-shadow-blur :spread parse-sd-token-shadow-spread :color parse-sd-token-color-value @@ -389,35 +395,42 @@ (defn- parse-sd-token-shadow-value "Parses shadow value and validates it." [value] - (cond - ;; Reference value (string) - (string? value) {:value value} + (let [missing-references + (when (string? value) + (seq (cto/find-token-value-references value)))] + (cond + missing-references + {:errors [(wte/error-with-value :error.style-dictionary/missing-reference missing-references)] + :references missing-references} + + (string? value) + {:errors [(wte/error-with-value :error.style-dictionary/invalid-token-value-shadow value)]} ;; Empty value - (nil? value) {:errors [(wte/get-error-code :error.token/empty-input)]} + (nil? value) {:errors [(wte/get-error-code :error.token/empty-input)]} ;; Invalid value - (not (js/Array.isArray value)) {:errors [(wte/error-with-value :error.style-dictionary/invalid-token-value value)]} + (not (js/Array.isArray value)) {:errors [(wte/error-with-value :error.style-dictionary/invalid-token-value value)]} ;; Array of shadows - :else - (let [converted (js->clj value :keywordize-keys true) + :else + (let [converted (js->clj value :keywordize-keys true) ;; Parse each shadow with its index - parsed-shadows (map-indexed - (fn [idx shadow-map] - (parse-single-shadow shadow-map idx)) - converted) + parsed-shadows (map-indexed + (fn [idx shadow-map] + (parse-single-shadow shadow-map idx)) + converted) ;; Collect all errors from all shadows - all-errors (mapcat :errors parsed-shadows) + all-errors (mapcat :errors parsed-shadows) ;; Collect all values from shadows that have values - all-values (into [] (keep :value parsed-shadows))] + all-values (into [] (keep :value parsed-shadows))] - (if (seq all-errors) - {:errors all-errors - :value all-values} - {:value all-values})))) + (if (seq all-errors) + {:errors all-errors + :value all-values} + {:value all-values}))))) (defn collect-shadow-errors [token shadow-index] (group-by :shadow-key diff --git a/frontend/src/app/main/data/workspace/tokens/application.cljs b/frontend/src/app/main/data/workspace/tokens/application.cljs index d0f8fb61b4..c58b8c8135 100644 --- a/frontend/src/app/main/data/workspace/tokens/application.cljs +++ b/frontend/src/app/main/data/workspace/tokens/application.cljs @@ -153,11 +153,11 @@ (defn value->shadow "Transform a token shadow value into penpot shadow data structure" [value] - (mapv (fn [{:keys [offsetX offsetY blur spread color inset]}] + (mapv (fn [{:keys [offset-x offset-y blur spread color inset]}] {:id (random-uuid) :hidden false - :offset-x offsetX - :offset-y offsetY + :offset-x offset-x + :offset-y offset-y :blur blur :color (value->color color) :spread spread diff --git a/frontend/src/app/main/data/workspace/tokens/errors.cljs b/frontend/src/app/main/data/workspace/tokens/errors.cljs index 0be73b58a8..ac733c52ac 100644 --- a/frontend/src/app/main/data/workspace/tokens/errors.cljs +++ b/frontend/src/app/main/data/workspace/tokens/errors.cljs @@ -108,6 +108,10 @@ {:error/code :error.style-dictionary/invalid-token-value-shadow-spread :error/fn #(tr "workspace.tokens.shadow-spread-range")} + :error.style-dictionary/invalid-token-value-shadow + {:error/code :error.style-dictionary/invalid-token-value-shadow + :error/fn #(tr "workspace.tokens.invalid-token-value-shadow" %)} + :error/unknown {:error/code :error/unknown :error/fn #(tr "labels.unknown-error")}}) diff --git a/frontend/src/app/main/data/workspace/tokens/format.cljs b/frontend/src/app/main/data/workspace/tokens/format.cljs index b832ebb365..733619d249 100644 --- a/frontend/src/app/main/data/workspace/tokens/format.cljs +++ b/frontend/src/app/main/data/workspace/tokens/format.cljs @@ -16,8 +16,8 @@ :letter-spacing "Letter Spacing" :text-case "Text Case" :text-decoration "Text Decoration" - :offsetX "X" - :offsetY "Y" + :offset-x "X" + :offset-y "Y" :blur "Blur" :spread "Spread" :color "Color" diff --git a/frontend/src/app/main/ui/ds/controls/select.cljs b/frontend/src/app/main/ui/ds/controls/select.cljs index a8572dd8b9..32676bcd36 100644 --- a/frontend/src/app/main/ui/ds/controls/select.cljs +++ b/frontend/src/app/main/ui/ds/controls/select.cljs @@ -118,6 +118,7 @@ (mf/use-fn (mf/deps disabled) (fn [event] + (dom/prevent-default event) (dom/stop-propagation event) (when-not disabled (swap! is-open* not)))) diff --git a/frontend/src/app/main/ui/ds/controls/utilities/input_field.cljs b/frontend/src/app/main/ui/ds/controls/utilities/input_field.cljs index 4a8bcb1554..1b3c9514c7 100644 --- a/frontend/src/app/main/ui/ds/controls/utilities/input_field.cljs +++ b/frontend/src/app/main/ui/ds/controls/utilities/input_field.cljs @@ -53,10 +53,15 @@ "true") :aria-describedby (when has-hint (str id "-hint")) - :aria-labelledby tooltip-id :type (d/nilv type "text") :id id :max-length (d/nilv max-length max-input-length)}) + + props (if (and aria-label (not (some? icon))) + (mf/spread-props props + {:aria-label aria-label}) + (mf/spread-props props + {:aria-labelledby tooltip-id})) inside-class (stl/css-case :input-wrapper true :has-hint has-hint :hint-type-hint (= hint-type "hint") diff --git a/frontend/src/app/main/ui/ds/utilities/swatch.scss b/frontend/src/app/main/ui/ds/utilities/swatch.scss index 4743517165..a9eb3b6936 100644 --- a/frontend/src/app/main/ui/ds/utilities/swatch.scss +++ b/frontend/src/app/main/ui/ds/utilities/swatch.scss @@ -15,7 +15,7 @@ } .swatch { - --border-color: var(--color-background-quaternary); + --border-color: var(--color-accent-primary-muted); --border-radius: #{$br-4}; --border-color-active: var(--color-foreground-primary); --border-color-active-inset: var(--color-background-primary); diff --git a/frontend/src/app/main/ui/workspace.cljs b/frontend/src/app/main/ui/workspace.cljs index 97c89d1392..a2d74157b5 100644 --- a/frontend/src/app/main/ui/workspace.cljs +++ b/frontend/src/app/main/ui/workspace.cljs @@ -35,7 +35,7 @@ [app.main.ui.workspace.tokens.export.modal] [app.main.ui.workspace.tokens.import] [app.main.ui.workspace.tokens.import.modal] - [app.main.ui.workspace.tokens.management.create.modals] + [app.main.ui.workspace.tokens.management.forms.modals] [app.main.ui.workspace.tokens.settings] [app.main.ui.workspace.tokens.themes.create-modal] [app.main.ui.workspace.viewport :refer [viewport*]] diff --git a/frontend/src/app/main/ui/workspace/tokens/management/create/border_radius.scss b/frontend/src/app/main/ui/workspace/tokens/management/create/border_radius.scss deleted file mode 100644 index 6593b3c30a..0000000000 --- a/frontend/src/app/main/ui/workspace/tokens/management/create/border_radius.scss +++ /dev/null @@ -1,58 +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 - -@use "ds/typography.scss" as t; -@use "ds/_sizes.scss" as *; -@use "ds/_borders.scss" as *; - -.form-wrapper { - width: $sz-384; - position: relative; -} - -.token-rows { - display: flex; - flex-direction: column; - gap: var(--sp-l); -} - -.input-row { - display: flex; - flex-direction: column; - gap: var(--sp-xs); -} - -.title-bar { - display: grid; - grid-template-columns: 1fr auto; -} - -.form-modal-title { - @include t.use-typography("headline-medium"); - color: var(--color-foreground-primary); - display: flex; - align-items: center; -} - -.button-row { - display: grid; - grid-template-columns: auto auto; - justify-content: end; - gap: var(--sp-m); - padding-block-start: var(--sp-s); -} - -.with-delete { - grid-template-columns: 1fr auto auto; -} - -.warning-name-change-notification-wrapper { - margin-block-start: var(--sp-l); -} - -.delete-btn { - justify-self: start; -} diff --git a/frontend/src/app/main/ui/workspace/tokens/management/create/color.cljs b/frontend/src/app/main/ui/workspace/tokens/management/create/color.cljs deleted file mode 100644 index e953d8206e..0000000000 --- a/frontend/src/app/main/ui/workspace/tokens/management/create/color.cljs +++ /dev/null @@ -1,224 +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.main.ui.workspace.tokens.management.create.color - (:require-macros [app.main.style :as stl]) - (:require - [app.common.files.tokens :as cft] - [app.common.schema :as sm] - [app.common.types.token :as cto] - [app.common.types.tokens-lib :as ctob] - [app.main.constants :refer [max-input-length]] - [app.main.data.modal :as modal] - [app.main.data.workspace.tokens.application :as dwta] - [app.main.data.workspace.tokens.library-edit :as dwtl] - [app.main.data.workspace.tokens.propagation :as dwtp] - [app.main.refs :as refs] - [app.main.store :as st] - [app.main.ui.ds.buttons.button :refer [button*]] - [app.main.ui.ds.foundations.assets.icon :as i] - [app.main.ui.ds.foundations.typography.heading :refer [heading*]] - [app.main.ui.ds.notifications.context-notification :refer [context-notification*]] - [app.main.ui.forms :as fc] - [app.main.ui.workspace.tokens.management.create.form-color-input-token :refer [form-color-input-token*]] - [app.util.dom :as dom] - [app.util.forms :as fm] - [app.util.i18n :refer [tr]] - [app.util.keyboard :as k] - [beicon.v2.core :as rx] - [cuerdas.core :as str] - [rumext.v2 :as mf])) - -(defn- token-value-error-fn - [{:keys [value]}] - (when (or (str/empty? value) - (str/blank? value)) - (tr "workspace.tokens.empty-input"))) - -(defn- make-schema - [tokens-tree] - (sm/schema - [:and - [:map - [:name - [:and - [:string {:min 1 :max 255 :error/fn #(str (:value %) (tr "workspace.tokens.token-name-length-validation-error"))}] - (sm/update-properties cto/token-name-ref assoc :error/fn #(str (:value %) (tr "workspace.tokens.token-name-validation-error"))) - [:fn {:error/fn #(tr "workspace.tokens.token-name-duplication-validation-error" (:value %))} - #(not (cft/token-name-path-exists? % tokens-tree))]]] - - [:value [::sm/text {:error/fn token-value-error-fn}]] - - [:description {:optional true} - [:string {:max 2048 :error/fn #(tr "errors.field-max-length" 2048)}]]] - - [:fn {:error/field :value - :error/fn #(tr "workspace.tokens.self-reference")} - (fn [{:keys [name value]}] - (when (and name value) - (nil? (cto/token-value-self-reference? name value))))]])) - -(mf/defc form* - [{:keys [token validate-token action is-create selected-token-set-id tokens-tree-in-selected-set] :as props}] - - (let [token - (mf/with-memo [token] - (or token {:type :color})) - - token-type - (get token :type) - - token-properties - (dwta/get-token-properties token) - - token-title (str/lower (:title token-properties)) - - tokens - (mf/deref refs/workspace-active-theme-sets-tokens) - - tokens - (mf/with-memo [tokens token] - ;; Ensure that the resolved value uses the currently editing token - ;; even if the name has been overriden by a token with the same name - ;; in another set below. - (cond-> tokens - (and (:name token) (:value token)) - (assoc (:name token) token))) - - schema - (mf/with-memo [tokens-tree-in-selected-set] - (make-schema tokens-tree-in-selected-set)) - - initial - (mf/with-memo [token] - {:name (:name token "") - :value (:value token "") - :description (:description token "")}) - - form - (fm/use-form :schema schema - :initial initial) - - warning-name-change? - (not= (get-in @form [:data :name]) - (:name initial)) - - on-cancel - (mf/use-fn - (fn [e] - (dom/prevent-default e) - (modal/hide!))) - - on-delete-token - (mf/use-fn - (mf/deps selected-token-set-id token) - (fn [e] - (dom/prevent-default e) - (modal/hide!) - (st/emit! (dwtl/delete-token selected-token-set-id (:id token))))) - - handle-key-down-delete - (mf/use-fn - (mf/deps on-delete-token) - (fn [e] - (when (or (k/enter? e) (k/space? e)) - (on-delete-token e)))) - - handle-key-down-cancel - (mf/use-fn - (mf/deps on-cancel) - (fn [e] - (when (or (k/enter? e) (k/space? e)) - (on-cancel e)))) - - on-submit - (mf/use-fn - (mf/deps validate-token token tokens token-type) - (fn [form _event] - (let [name (get-in @form [:clean-data :name]) - description (get-in @form [:clean-data :description]) - value (get-in @form [:clean-data :value])] - (->> (validate-token {:token-value value - :token-name name - :token-description description - :prev-token token - :tokens tokens}) - (rx/subs! - (fn [valid-token] - (st/emit! - (if is-create - (dwtl/create-token (ctob/make-token {:name name - :type token-type - :value (:value valid-token) - :description description})) - - (dwtl/update-token (:id token) - {:name name - :value (:value valid-token) - :description description})) - (dwtp/propagate-workspace-tokens) - (modal/hide))))))))] - - [:> fc/form* {:class (stl/css :form-wrapper) - :form form - :on-submit on-submit} - [:div {:class (stl/css :token-rows)} - - [:> heading* {:level 2 :typography "headline-medium" :class (stl/css :form-modal-title)} - (tr "workspace.tokens.create-token" token-type)] - - [:div {:class (stl/css :input-row)} - [:> fc/form-input* {:id "token-name" - :name :name - :label (tr "workspace.tokens.token-name") - :placeholder (tr "workspace.tokens.enter-token-name" token-title) - :max-length max-input-length - :variant "comfortable" - :auto-focus true}] - - (when (and warning-name-change? (= action "edit")) - [:div {:class (stl/css :warning-name-change-notification-wrapper)} - [:> context-notification* - {:level :warning :appearance :ghost} (tr "workspace.tokens.warning-name-change")]])] - - [:div {:class (stl/css :input-row)} - [:> form-color-input-token* - {:placeholder (tr "workspace.tokens.token-value-enter") - :label (tr "workspace.tokens.token-value") - :name :value - :token token - :tokens tokens}]] - - [:div {:class (stl/css :input-row)} - [:> fc/form-input* {:id "token-description" - :name :description - :label (tr "workspace.tokens.token-description") - :placeholder (tr "workspace.tokens.token-description") - :max-length max-input-length - :variant "comfortable" - :is-optional true}]] - - [:div {:class (stl/css-case :button-row true - :with-delete (= action "edit"))} - (when (= action "edit") - [:> button* {:on-click on-delete-token - :on-key-down handle-key-down-delete - :class (stl/css :delete-btn) - :type "button" - :icon i/delete - :variant "secondary"} - (tr "labels.delete")]) - - [:> button* {:on-click on-cancel - :on-key-down handle-key-down-cancel - :type "button" - :id "token-modal-cancel" - :variant "secondary"} - (tr "labels.cancel")] - - [:> fc/form-submit* {:variant "primary" - :on-submit on-submit} - (tr "labels.save")]]]])) diff --git a/frontend/src/app/main/ui/workspace/tokens/management/create/color.scss b/frontend/src/app/main/ui/workspace/tokens/management/create/color.scss deleted file mode 100644 index 6593b3c30a..0000000000 --- a/frontend/src/app/main/ui/workspace/tokens/management/create/color.scss +++ /dev/null @@ -1,58 +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 - -@use "ds/typography.scss" as t; -@use "ds/_sizes.scss" as *; -@use "ds/_borders.scss" as *; - -.form-wrapper { - width: $sz-384; - position: relative; -} - -.token-rows { - display: flex; - flex-direction: column; - gap: var(--sp-l); -} - -.input-row { - display: flex; - flex-direction: column; - gap: var(--sp-xs); -} - -.title-bar { - display: grid; - grid-template-columns: 1fr auto; -} - -.form-modal-title { - @include t.use-typography("headline-medium"); - color: var(--color-foreground-primary); - display: flex; - align-items: center; -} - -.button-row { - display: grid; - grid-template-columns: auto auto; - justify-content: end; - gap: var(--sp-m); - padding-block-start: var(--sp-s); -} - -.with-delete { - grid-template-columns: 1fr auto auto; -} - -.warning-name-change-notification-wrapper { - margin-block-start: var(--sp-l); -} - -.delete-btn { - justify-self: start; -} diff --git a/frontend/src/app/main/ui/workspace/tokens/management/create/dimensions.cljs b/frontend/src/app/main/ui/workspace/tokens/management/create/dimensions.cljs deleted file mode 100644 index fa0c64b501..0000000000 --- a/frontend/src/app/main/ui/workspace/tokens/management/create/dimensions.cljs +++ /dev/null @@ -1,223 +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.main.ui.workspace.tokens.management.create.dimensions - (:require-macros [app.main.style :as stl]) - (:require - [app.common.files.tokens :as cft] - [app.common.schema :as sm] - [app.common.types.token :as cto] - [app.common.types.tokens-lib :as ctob] - [app.main.constants :refer [max-input-length]] - [app.main.data.modal :as modal] - [app.main.data.workspace.tokens.application :as dwta] - [app.main.data.workspace.tokens.library-edit :as dwtl] - [app.main.data.workspace.tokens.propagation :as dwtp] - [app.main.refs :as refs] - [app.main.store :as st] - [app.main.ui.ds.buttons.button :refer [button*]] - [app.main.ui.ds.foundations.assets.icon :as i] - [app.main.ui.ds.foundations.typography.heading :refer [heading*]] - [app.main.ui.ds.notifications.context-notification :refer [context-notification*]] - [app.main.ui.forms :as fc] - [app.main.ui.workspace.tokens.management.create.form-input-token :refer [form-input-token*]] - [app.util.dom :as dom] - [app.util.forms :as fm] - [app.util.i18n :refer [tr]] - [app.util.keyboard :as k] - [beicon.v2.core :as rx] - [cuerdas.core :as str] - [rumext.v2 :as mf])) - -(defn- token-value-error-fn - [{:keys [value]}] - (when (or (str/empty? value) - (str/blank? value)) - (tr "workspace.tokens.empty-input"))) - -(defn- make-schema - [tokens-tree] - (sm/schema - [:and - [:map - [:name - [:and - [:string {:min 1 :max 255 :error/fn #(str (:value %) (tr "workspace.tokens.token-name-length-validation-error"))}] - (sm/update-properties cto/token-name-ref assoc :error/fn #(str (:value %) (tr "workspace.tokens.token-name-validation-error"))) - [:fn {:error/fn #(tr "workspace.tokens.token-name-duplication-validation-error" (:value %))} - #(not (cft/token-name-path-exists? % tokens-tree))]]] - - [:value [::sm/text {:error/fn token-value-error-fn}]] - - [:description {:optional true} - [:string {:max 2048 :error/fn #(tr "errors.field-max-length" 2048)}]]] - - [:fn {:error/field :value - :error/fn #(tr "workspace.tokens.self-reference")} - (fn [{:keys [name value]}] - (when (and name value) - (nil? (cto/token-value-self-reference? name value))))]])) - -(mf/defc form* - [{:keys [token validate-token action is-create selected-token-set-id tokens-tree-in-selected-set] :as props}] - - (let [token - (mf/with-memo [token] - (or token {:type :dimensions})) - - token-type - (get token :type) - - token-properties - (dwta/get-token-properties token) - - token-title (str/lower (:title token-properties)) - - tokens - (mf/deref refs/workspace-active-theme-sets-tokens) - - tokens - (mf/with-memo [tokens token] - ;; Ensure that the resolved value uses the currently editing token - ;; even if the name has been overriden by a token with the same name - ;; in another set below. - (cond-> tokens - (and (:name token) (:value token)) - (assoc (:name token) token))) - - schema - (mf/with-memo [tokens-tree-in-selected-set] - (make-schema tokens-tree-in-selected-set)) - - initial - (mf/with-memo [token] - {:name (:name token "") - :value (:value token "") - :description (:description token "")}) - - form - (fm/use-form :schema schema - :initial initial) - - warning-name-change? - (not= (get-in @form [:data :name]) - (:name initial)) - - on-cancel - (mf/use-fn - (fn [e] - (dom/prevent-default e) - (modal/hide!))) - - on-delete-token - (mf/use-fn - (mf/deps selected-token-set-id token) - (fn [e] - (dom/prevent-default e) - (modal/hide!) - (st/emit! (dwtl/delete-token selected-token-set-id (:id token))))) - - handle-key-down-delete - (mf/use-fn - (mf/deps on-delete-token) - (fn [e] - (when (or (k/enter? e) (k/space? e)) - (on-delete-token e)))) - - handle-key-down-cancel - (mf/use-fn - (mf/deps on-cancel) - (fn [e] - (when (or (k/enter? e) (k/space? e)) - (on-cancel e)))) - - on-submit - (mf/use-fn - (mf/deps validate-token token tokens token-type) - (fn [form _event] - (let [name (get-in @form [:clean-data :name]) - description (get-in @form [:clean-data :description]) - value (get-in @form [:clean-data :value])] - (->> (validate-token {:token-value value - :token-name name - :token-description description - :prev-token token - :tokens tokens}) - (rx/subs! - (fn [valid-token] - (st/emit! - (if is-create - (dwtl/create-token (ctob/make-token {:name name - :type token-type - :value (:value valid-token) - :description description})) - - (dwtl/update-token (:id token) - {:name name - :value (:value valid-token) - :description description})) - (dwtp/propagate-workspace-tokens) - (modal/hide))))))))] - - [:> fc/form* {:class (stl/css :form-wrapper) - :form form - :on-submit on-submit} - [:div {:class (stl/css :token-rows)} - [:> heading* {:level 2 :typography "headline-medium" :class (stl/css :form-modal-title)} - (tr "workspace.tokens.create-token" token-type)] - - [:div {:class (stl/css :input-row)} - [:> fc/form-input* {:id "token-name" - :name :name - :label (tr "workspace.tokens.token-name") - :placeholder (tr "workspace.tokens.enter-token-name" token-title) - :max-length max-input-length - :variant "comfortable" - :auto-focus true}] - - (when (and warning-name-change? (= action "edit")) - [:div {:class (stl/css :warning-name-change-notification-wrapper)} - [:> context-notification* - {:level :warning :appearance :ghost} (tr "workspace.tokens.warning-name-change")]])] - - [:div {:class (stl/css :input-row)} - [:> form-input-token* - {:placeholder (tr "workspace.tokens.token-value-enter") - :label (tr "workspace.tokens.token-value") - :name :value - :token token - :tokens tokens}]] - - [:div {:class (stl/css :input-row)} - [:> fc/form-input* {:id "token-description" - :name :description - :label (tr "workspace.tokens.token-description") - :placeholder (tr "workspace.tokens.token-description") - :max-length max-input-length - :variant "comfortable" - :is-optional true}]] - - [:div {:class (stl/css-case :button-row true - :with-delete (= action "edit"))} - (when (= action "edit") - [:> button* {:on-click on-delete-token - :on-key-down handle-key-down-delete - :class (stl/css :delete-btn) - :type "button" - :icon i/delete - :variant "secondary"} - (tr "labels.delete")]) - - [:> button* {:on-click on-cancel - :on-key-down handle-key-down-cancel - :type "button" - :id "token-modal-cancel" - :variant "secondary"} - (tr "labels.cancel")] - - [:> fc/form-submit* {:variant "primary" - :on-submit on-submit} - (tr "labels.save")]]]])) diff --git a/frontend/src/app/main/ui/workspace/tokens/management/create/font_family.cljs b/frontend/src/app/main/ui/workspace/tokens/management/create/font_family.cljs deleted file mode 100644 index 7e104e8f0f..0000000000 --- a/frontend/src/app/main/ui/workspace/tokens/management/create/font_family.cljs +++ /dev/null @@ -1,221 +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.main.ui.workspace.tokens.management.create.font-family - (:require-macros [app.main.style :as stl]) - (:require - [app.common.files.tokens :as cft] - [app.common.schema :as sm] - [app.common.types.token :as cto] - [app.common.types.tokens-lib :as ctob] - [app.main.constants :refer [max-input-length]] - [app.main.data.modal :as modal] - [app.main.data.workspace.tokens.application :as dwta] - [app.main.data.workspace.tokens.library-edit :as dwtl] - [app.main.data.workspace.tokens.propagation :as dwtp] - [app.main.refs :as refs] - [app.main.store :as st] - [app.main.ui.ds.buttons.button :refer [button*]] - [app.main.ui.ds.foundations.assets.icon :as i] - [app.main.ui.ds.foundations.typography.heading :refer [heading*]] - [app.main.ui.ds.notifications.context-notification :refer [context-notification*]] - [app.main.ui.forms :as fc] - [app.main.ui.workspace.tokens.management.create.combobox-token-fonts :refer [font-picker-combobox*]] - [app.util.dom :as dom] - [app.util.forms :as fm] - [app.util.i18n :refer [tr]] - [app.util.keyboard :as k] - [beicon.v2.core :as rx] - [cuerdas.core :as str] - [rumext.v2 :as mf])) - -(defn- make-schema - [tokens-tree] - (sm/schema - [:and - [:map - [:name - [:and - [:string {:min 1 :max 255 :error/fn #(str (:value %) (tr "workspace.tokens.token-name-length-validation-error"))}] - (sm/update-properties cto/token-name-ref assoc :error/fn #(str (:value %) (tr "workspace.tokens.token-name-validation-error"))) - [:fn {:error/fn #(tr "workspace.tokens.token-name-duplication-validation-error" (:value %))} - #(not (cft/token-name-path-exists? % tokens-tree))]]] - - [:value ::sm/text] - - [:description {:optional true} - [:string {:max 2048 :error/fn #(tr "errors.field-max-length" 2048)}]]] - - [:fn {:error/field :value - :error/fn #(tr "workspace.tokens.self-reference")} - (fn [{:keys [name value]}] - (when (and name value) - (nil? (cto/token-value-self-reference? name value))))]])) - - -(mf/defc form* - [{:keys [token validate-token action is-create selected-token-set-id tokens-tree-in-selected-set] :as props}] - - (let [token - (mf/with-memo [token] - (if token - (update token :value cto/join-font-family) - {:type :font-family})) - - token-type - (get token :type) - - token-properties - (dwta/get-token-properties token) - - token-title (str/lower (:title token-properties)) - - tokens - (mf/deref refs/workspace-active-theme-sets-tokens) - - tokens - (mf/with-memo [tokens token] - ;; Ensure that the resolved value uses the currently editing token - ;; even if the name has been overriden by a token with the same name - ;; in another set below. - (cond-> tokens - (and (:name token) (:value token)) - (assoc (:name token) token))) - - schema - (mf/with-memo [tokens-tree-in-selected-set] - (make-schema tokens-tree-in-selected-set)) - - initial - (mf/with-memo [token] - {:name (:name token "") - :value (:value token "") - :description (:description token "")}) - - form - (fm/use-form :schema schema - :initial initial) - - warning-name-change? - (not= (get-in @form [:data :name]) - (:name initial)) - - on-cancel - (mf/use-fn - (fn [e] - (dom/prevent-default e) - (modal/hide!))) - - on-delete-token - (mf/use-fn - (mf/deps selected-token-set-id token) - (fn [e] - (dom/prevent-default e) - (modal/hide!) - (st/emit! (dwtl/delete-token selected-token-set-id (:id token))))) - - handle-key-down-delete - (mf/use-fn - (mf/deps on-delete-token) - (fn [e] - (when (or (k/enter? e) (k/space? e)) - (on-delete-token e)))) - - handle-key-down-cancel - (mf/use-fn - (mf/deps on-cancel) - (fn [e] - (when (or (k/enter? e) (k/space? e)) - (on-cancel e)))) - - on-submit - (mf/use-fn - (mf/deps validate-token token tokens token-type) - (fn [form _event] - (let [name (get-in @form [:clean-data :name]) - description (get-in @form [:clean-data :description]) - value (get-in @form [:clean-data :value])] - (->> (validate-token {:token-value value - :token-name name - :token-description description - :prev-token token - :tokens tokens}) - (rx/subs! - (fn [valid-token] - (st/emit! - (if is-create - (dwtl/create-token (ctob/make-token {:name name - :type token-type - :value (:value valid-token) - :description description})) - - (dwtl/update-token (:id token) - {:name name - :value (:value valid-token) - :description description})) - (dwtp/propagate-workspace-tokens) - (modal/hide))))))))] - - [:> fc/form* {:class (stl/css :form-wrapper) - :form form - :on-submit on-submit} - [:div {:class (stl/css :token-rows)} - - [:> heading* {:level 2 :typography "headline-medium" :class (stl/css :form-modal-title)} - (tr "workspace.tokens.create-token" token-type)] - - [:div {:class (stl/css :input-row)} - [:> fc/form-input* {:id "token-name" - :name :name - :label (tr "workspace.tokens.token-name") - :placeholder (tr "workspace.tokens.enter-token-name" token-title) - :max-length max-input-length - :variant "comfortable" - :auto-focus true}] - - (when (and warning-name-change? (= action "edit")) - [:div {:class (stl/css :warning-name-change-notification-wrapper)} - [:> context-notification* - {:level :warning :appearance :ghost} (tr "workspace.tokens.warning-name-change")]])] - - [:div {:class (stl/css :input-row)} - [:> font-picker-combobox* - {:placeholder (tr "workspace.tokens.token-value-enter") - :label (tr "workspace.tokens.token-value") - :name :value - :token token - :tokens tokens}]] - - [:div {:class (stl/css :input-row)} - [:> fc/form-input* {:id "token-description" - :name :description - :label (tr "workspace.tokens.token-description") - :placeholder (tr "workspace.tokens.token-description") - :max-length max-input-length - :variant "comfortable" - :is-optional true}]] - - [:div {:class (stl/css-case :button-row true - :with-delete (= action "edit"))} - (when (= action "edit") - [:> button* {:on-click on-delete-token - :on-key-down handle-key-down-delete - :class (stl/css :delete-btn) - :type "button" - :icon i/delete - :variant "secondary"} - (tr "labels.delete")]) - - [:> button* {:on-click on-cancel - :on-key-down handle-key-down-cancel - :type "button" - :id "token-modal-cancel" - :variant "secondary"} - (tr "labels.cancel")] - - [:> fc/form-submit* {:variant "primary" - :on-submit on-submit} - (tr "labels.save")]]]])) diff --git a/frontend/src/app/main/ui/workspace/tokens/management/create/form.cljs b/frontend/src/app/main/ui/workspace/tokens/management/create/form.cljs deleted file mode 100644 index 49e9c1e202..0000000000 --- a/frontend/src/app/main/ui/workspace/tokens/management/create/form.cljs +++ /dev/null @@ -1,1455 +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.main.ui.workspace.tokens.management.create.form - (:require-macros [app.main.style :as stl]) - (:require - [app.common.data :as d] - [app.common.files.tokens :as cft] - [app.common.schema :as sm] - [app.common.types.color :as c] - [app.common.types.token :as cto] - [app.common.types.tokens-lib :as ctob] - [app.main.constants :refer [max-input-length]] - [app.main.data.modal :as modal] - [app.main.data.style-dictionary :as sd] - [app.main.data.tinycolor :as tinycolor] - [app.main.data.workspace.tokens.application :as dwta] - [app.main.data.workspace.tokens.errors :as wte] - [app.main.data.workspace.tokens.library-edit :as dwtl] - [app.main.data.workspace.tokens.propagation :as dwtp] - [app.main.fonts :as fonts] - [app.main.refs :as refs] - [app.main.store :as st] - [app.main.ui.components.radio-buttons :refer [radio-button radio-buttons]] - [app.main.ui.ds.buttons.button :refer [button*]] - [app.main.ui.ds.buttons.icon-button :refer [icon-button*]] - [app.main.ui.ds.controls.input :refer [input*]] - [app.main.ui.ds.foundations.assets.icon :as i] - [app.main.ui.ds.foundations.typography.heading :refer [heading*]] - [app.main.ui.ds.notifications.context-notification :refer [context-notification*]] - [app.main.ui.workspace.colorpicker :as colorpicker] - [app.main.ui.workspace.colorpicker.ramp :refer [ramp-selector*]] - [app.main.ui.workspace.sidebar.options.menus.typography :refer [font-selector*]] - [app.main.ui.workspace.tokens.management.create.border-radius :as border-radius] - [app.main.ui.workspace.tokens.management.create.color :as color] - [app.main.ui.workspace.tokens.management.create.dimensions :as dimensions] - [app.main.ui.workspace.tokens.management.create.font-family :as font-family] - [app.main.ui.workspace.tokens.management.create.input-token-color-bullet :refer [input-token-color-bullet*]] - [app.main.ui.workspace.tokens.management.create.input-tokens-value :refer [input-token* token-value-hint*]] - [app.main.ui.workspace.tokens.management.create.text-case :as text-case] - [app.main.ui.workspace.tokens.management.create.typography :as typography] - [app.util.dom :as dom] - [app.util.functions :as uf] - [app.util.i18n :refer [tr]] - [app.util.keyboard :as k] - [app.util.object :as obj] - [beicon.v2.core :as rx] - [cuerdas.core :as str] - [rumext.v2 :as mf])) - -;; Helpers --------------------------------------------------------------------- - -(defn- clean-name [name] - (-> (str/trim name) - ;; Remove trailing dots - (str/replace #"\.+$" ""))) - -(defn- valid-name? [name] - (seq (clean-name (str name)))) - -;; Schemas --------------------------------------------------------------------- - -(defn- make-token-name-schema - "Generate a dynamic schema validation to check if a token path derived - from the name already exists at `tokens-tree`." - [tokens-tree] - [:and - [:string {:min 1 :max 255 :error/fn #(str (:value %) (tr "workspace.tokens.token-name-length-validation-error"))}] - (sm/update-properties cto/token-name-ref assoc :error/fn #(str (:value %) (tr "workspace.tokens.token-name-validation-error"))) - [:fn {:error/fn #(tr "workspace.tokens.token-name-duplication-validation-error" (:value %))} - #(not (cft/token-name-path-exists? % tokens-tree))]]) - -(defn validate-token-name - [tokens-tree name] - (let [schema (make-token-name-schema tokens-tree) - explainer (sm/explainer schema)] - (-> name explainer sm/simplify not-empty))) - -(def ^:private schema:token-description - [:string {:max 2048 :error/fn #(tr "errors.field-max-length" 2048)}]) - -(def ^:private validate-token-description - (let [explainer (sm/lazy-explainer schema:token-description)] - (fn [description] - (-> description explainer sm/simplify not-empty)))) - -;; Value Validation ------------------------------------------------------------- - -(defn check-empty-value [token-value] - (when (empty? (str/trim token-value)) - (wte/get-error-code :error.token/empty-input))) - -(defn check-token-empty-value [token] - (check-empty-value (:value token))) - -(defn check-self-reference [token-name token-value] - (when (cto/token-value-self-reference? token-name token-value) - (wte/get-error-code :error.token/direct-self-reference))) - -(defn validate-resolve-token - [token prev-token tokens] - (let [token (cond-> token - ;; When creating a new token we dont have a name yet or invalid name, - ;; but we still want to resolve the value to show in the form. - ;; So we use a temporary token name that hopefully doesn't clash with any of the users token names - (not (sm/valid? cto/token-name-ref (:name token))) (assoc :name "__PENPOT__TOKEN__NAME__PLACEHOLDER__")) - tokens' (cond-> tokens - ;; Remove previous token when renaming a token - (not= (:name token) (:name prev-token)) - (dissoc (:name prev-token)) - - :always - (update (:name token) #(ctob/make-token (merge % prev-token token))))] - (->> tokens' - (sd/resolve-tokens-interactive) - (rx/mapcat - (fn [resolved-tokens] - (let [{:keys [errors resolved-value] :as resolved-token} (get resolved-tokens (:name token))] - (cond - resolved-value (rx/of resolved-token) - :else (rx/throw {:errors (or (seq errors) - [(wte/get-error-code :error/unknown-error)])})))))))) - -(defn- validate-token-with [token validators] - (if-let [error (some (fn [validate] (validate token)) validators)] - (rx/throw {:errors [error]}) - (rx/of token))) - -(def ^:private default-validators - [check-token-empty-value check-self-reference]) - -(defn- default-validate-token - "Validates a token by confirming a list of `validator` predicates and resolving the token using `tokens` with StyleDictionary. - Returns rx stream of either a valid resolved token or an errors map. - - Props: - token-name, token-value, token-description: Values from the form inputs - prev-token: The existing token currently being edited - tokens: tokens map keyed by token-name - Used to look up the editing token & resolving step. - - validators: A list of predicates that will be used to do simple validation on the unresolved token map. - The validators get the token map as input and should either return: - - An errors map .e.g: {:errors []} - - nil (valid token predicate) - Mostly used to do simple checks like invalidating empy token `:name`. - Will default to `default-validators`." - [{:keys [token-name token-value token-description prev-token tokens validators] - :or {validators default-validators}}] - (let [token (-> {:name token-name - :value token-value - :description token-description} - (d/without-nils))] - (->> (rx/of token) - ;; Simple validation of the editing token - (rx/mapcat #(validate-token-with % validators)) - ;; Resolving token via StyleDictionary - (rx/mapcat #(validate-resolve-token % prev-token tokens))))) - -(defn- check-coll-self-reference - "Invalidate a collection of `token-vals` for a self-refernce against `token-name`.," - [token-name token-vals] - (when (some #(cto/token-value-self-reference? token-name %) token-vals) - (wte/get-error-code :error.token/direct-self-reference))) - -(defn- check-font-family-token-self-reference [token] - (check-coll-self-reference (:name token) (:value token))) - -(defn- validate-font-family-token - [props] - (-> props - (update :token-value cto/split-font-family) - (assoc :validators [(fn [token] - (when (empty? (:value token)) - (wte/get-error-code :error.token/empty-input))) - check-font-family-token-self-reference]) - (default-validate-token))) - -(defn- check-typography-token-self-reference - "Check token when any of the attributes in token value have a self-reference." - [token] - (let [token-name (:name token) - token-values (:value token)] - (some (fn [[k v]] - (when-let [err (case k - :font-family (check-coll-self-reference token-name v) - (check-self-reference token-name v))] - (assoc err :typography-key k))) - token-values))) - -(defn- check-empty-typography-token [token] - (when (empty? (:value token)) - (wte/get-error-code :error.token/empty-input))) - -(defn- check-shadow-token-self-reference - "Check token when any of the attributes in a shadow's value have a self-reference." - [token] - (let [token-name (:name token) - shadow-values (:value token)] - (some (fn [[shadow-idx shadow-map]] - (some (fn [[k v]] - (when-let [err (check-self-reference token-name v)] - (assoc err :shadow-key k :shadow-index shadow-idx))) - shadow-map)) - (d/enumerate shadow-values)))) - -(defn- check-empty-shadow-token [token] - (when (or (empty? (:value token)) - (some (fn [shadow] (not-every? #(contains? shadow %) [:offsetX :offsetY :blur :spread :color])) - (:value token))) - (wte/get-error-code :error.token/empty-input))) - -(defn- validate-typography-token - [{:keys [token-value] :as props}] - (cond - ;; Entering form without a value - show no error just resolve nil - (nil? token-value) (rx/of nil) - ;; Validate refrence string - (cto/typography-composite-token-reference? token-value) (default-validate-token props) - ;; Validate composite token - :else - (-> props - (update :token-value - (fn [v] - (-> (or v {}) - (d/update-when :font-family #(if (string? %) (cto/split-font-family %) %))))) - (assoc :validators [check-empty-typography-token - check-typography-token-self-reference]) - (default-validate-token)))) - -(defn- validate-shadow-token - [{:keys [token-value] :as props}] - (cond - ;; Entering form without a value - show no error just resolve nil - (nil? token-value) (rx/of nil) - ;; Validate refrence string - (cto/shadow-composite-token-reference? token-value) (default-validate-token props) - ;; Validate composite token - :else - (-> props - (update :token-value (fn [value] - (->> (or value []) - (mapv (fn [shadow] - (d/update-when shadow :inset #(cond - (boolean? %) % - (= "true" %) true - :else false))))))) - (assoc :validators [check-empty-shadow-token - check-shadow-token-self-reference]) - (default-validate-token)))) - -(defn- use-debonced-resolve-callback - "Resolves a token values using `StyleDictionary`. - This function is debounced as the resolving might be an expensive calculation. - Uses a custom debouncing logic, as the resolve function is async." - [{:keys [timeout name-ref token tokens callback validate-token] - :or {timeout 160}}] - (let [timeout-id-ref (mf/use-ref nil)] - (mf/use-fn - (mf/deps token callback tokens) - (fn [value] - (let [timeout-id (js/Symbol) - ;; Dont execute callback when the timout-id-ref is outdated because this function got called again - timeout-outdated-cb? #(not= (mf/ref-val timeout-id-ref) timeout-id)] - (mf/set-ref-val! timeout-id-ref timeout-id) - (js/setTimeout - (fn [] - (when (not (timeout-outdated-cb?)) - (->> (validate-token {:token-value value - :token-name (mf/ref-val name-ref) - :prev-token token - :tokens tokens}) - (rx/filter #(not (timeout-outdated-cb?))) - (rx/subs! callback callback)))) - timeout)))))) - -(defonce form-token-cache-atom (atom nil)) - -;; Form Component -------------------------------------------------------------- - -(mf/defc form* - "Form component to edit or create a token of any token type. - - Callback props: - validate-token: Function to validate and resolve an editing token, see `default-validate-token`. - on-value-resolve: Will be called when a token value is resolved - Used to sync external state (like color picker) - on-get-token-value: Custom function to get the input value from the dom - (As there might be multiple inputs passed for `custom-input-token-value`) - Can also be used to manipulate the value (E.g.: Auto-prepending # for hex colors) - - Custom component props: - custom-input-token-value: Custom component for editing/displaying the token value - custom-input-token-value-props: Custom props passed to the custom-input-token-value merged with the default props" - [{:keys [is-create - token - token-type - selected-token-set-id - action - input-value-placeholder - - ;; Callbacks - validate-token - on-value-resolve - on-get-token-value - - ;; Custom component props - custom-input-token-value - custom-input-token-value-props] - :or {validate-token default-validate-token}}] - - (let [token - (mf/with-memo [token] - (or token {:type token-type})) - - token-name (get token :name) - token-description (get token :description) - token-name-ref (mf/use-ref token-name) - - name-ref (mf/use-ref nil) - - name-errors* (mf/use-state nil) - name-errors (deref name-errors*) - - touched-name* (mf/use-state false) - touched-name? (deref touched-name*) - - warning-name-change* - (mf/use-state false) - - warning-name-change? - (deref warning-name-change*) - - token-properties - (dwta/get-token-properties token) - - tokens-in-selected-set - (mf/deref refs/workspace-all-tokens-in-selected-set) - - active-theme-tokens - (cond-> (mf/deref refs/workspace-active-theme-sets-tokens) - ;; Ensure that the resolved value uses the currently editing token - ;; even if the name has been overriden by a token with the same name - ;; in another set below. - (and (:name token) (:value token)) - (assoc (:name token) token) - - ;; Style dictionary resolver needs font families to be an array of strings - (= :font-family (or (:type token) token-type)) - (update-in [(:name token) :value] cto/split-font-family) - - (= :typography (or (:type token) token-type)) - (d/update-in-when [(:name token) :font-family :value] cto/split-font-family)) - - resolved-tokens - (sd/use-resolved-tokens active-theme-tokens - {:cache-atom form-token-cache-atom - :interactive? true}) - - token-path - (mf/with-memo [token-name] - (cft/token-name->path token-name)) - - tokens-tree-in-selected-set - (mf/with-memo [token-path tokens-in-selected-set] - (-> (ctob/tokens-tree tokens-in-selected-set) - ;; Allow setting editing token to it's own path - (d/dissoc-in token-path))) - - on-blur-name - (mf/use-fn - (mf/deps touched-name?) - (fn [e] - (let [value (dom/get-target-val e) - errors (validate-token-name tokens-tree-in-selected-set value)] - (when touched-name? (reset! warning-name-change* true)) - (reset! name-errors* errors)))) - - on-update-name-debounced - (mf/with-memo [touched-name?] - (uf/debounce (fn [token-name] - (when touched-name? - (reset! name-errors* (validate-token-name tokens-tree-in-selected-set token-name)))) - 300)) - - on-update-name - (mf/use-fn - (mf/deps on-update-name-debounced name-ref) - (fn [] - (let [ref (mf/ref-val name-ref) - token-name (dom/get-value ref)] - (reset! touched-name* true) - - (mf/set-ref-val! token-name-ref token-name) - (on-update-name-debounced token-name)))) - - valid-name-field? - (and - (not name-errors) - (valid-name? (mf/ref-val token-name-ref))) - - ;; Value - value-input-ref (mf/use-ref nil) - value-ref (mf/use-ref (:value token)) - - token-resolve-result* - (mf/use-state #(get resolved-tokens (cft/token-identifier token))) - - token-resolve-result - (deref token-resolve-result*) - - clear-resolve-value - (mf/use-fn - (fn [] - (reset! token-resolve-result* nil))) - - set-resolve-value - (mf/use-fn - (mf/deps on-value-resolve) - (fn [token-or-err] - (when on-value-resolve - (cond - (:errors token-or-err) (on-value-resolve nil) - :else (on-value-resolve (:resolved-value token-or-err)))) - (reset! token-resolve-result* token-or-err))) - - on-update-value-debounced - (use-debonced-resolve-callback - {:name-ref token-name-ref - :token token - :tokens active-theme-tokens - :callback set-resolve-value - :validate-token validate-token}) - - ;; Callback to update the value state via on of the inputs :on-change - on-update-value - (mf/use-fn - (mf/deps on-update-value-debounced on-get-token-value) - (fn [e] - (let [value (if (fn? on-get-token-value) - (on-get-token-value e (mf/ref-val value-ref)) - (dom/get-target-val e))] - (mf/set-ref-val! value-ref value) - (on-update-value-debounced value)))) - - ;; Calback to update the value state from the outside (e.g.: color picker) - on-external-update-value - (mf/use-fn - (mf/deps on-update-value-debounced) - (fn [next-value] - (mf/set-ref-val! value-ref next-value) - (on-update-value-debounced next-value))) - - value-error? (seq (:errors token-resolve-result)) - valid-value-field? (and token-resolve-result (not value-error?)) - - ;; Description - description-ref (mf/use-ref token-description) - description-errors* (mf/use-state nil) - description-errors (deref description-errors*) - - on-update-description-debounced - (mf/with-memo [] - (uf/debounce (fn [e] - (let [value (dom/get-target-val e) - errors (validate-token-description value)] - (reset! description-errors* errors))))) - - on-update-description - (mf/use-fn - (mf/deps on-update-description-debounced) - (fn [e] - (mf/set-ref-val! description-ref (dom/get-target-val e)) - (on-update-description-debounced e))) - - valid-description-field? - (empty? description-errors) - - ;; Form - disabled? - (or (not valid-name-field?) - (not valid-value-field?) - (not valid-description-field?)) - - on-submit - (mf/use-fn - (mf/deps is-create token active-theme-tokens validate-token validate-token-description) - (fn [e] - (dom/prevent-default e) - ;; We have to re-validate the current form values before submitting - ;; because the validation is asynchronous/debounced - ;; and the user might have edited a valid form to make it invalid, - ;; and press enter before the next validations could return. - - (let [clean-name (clean-name (mf/ref-val token-name-ref)) - valid-name? (empty? (validate-token-name tokens-tree-in-selected-set clean-name)) - - value (mf/ref-val value-ref) - clean-description (mf/ref-val description-ref) - valid-description? (or (some-> clean-description validate-token-description empty?) true)] - - (when (and valid-name? valid-description?) - (->> (validate-token {:token-value value - :token-name clean-name - :token-description clean-description - :prev-token token - :tokens active-theme-tokens}) - (rx/subs! - (fn [valid-token] - (st/emit! - (if is-create - (dwtl/create-token (ctob/make-token {:name clean-name - :type token-type - :value (:value valid-token) - :description clean-description})) - - (dwtl/update-token (:id token) - {:name clean-name - :value (:value valid-token) - :description clean-description})) - (dwtp/propagate-workspace-tokens) - (modal/hide))))))))) - - on-delete-token - (mf/use-fn - (mf/deps selected-token-set-id) - (fn [e] - (dom/prevent-default e) - (modal/hide!) - (st/emit! (dwtl/delete-token selected-token-set-id (:id token))))) - - on-cancel - (mf/use-fn - (fn [e] - (dom/prevent-default e) - (modal/hide!))) - - handle-key-down-delete - (mf/use-fn - (mf/deps on-delete-token) - (fn [e] - (when (k/enter? e) - (on-delete-token e)))) - - handle-key-down-cancel - (mf/use-fn - (mf/deps on-cancel) - (fn [e] - (when (k/enter? e) - (on-cancel e)))) - - handle-key-down-save - (mf/use-fn - (fn [e] - (mf/deps on-submit) - (when (k/enter? e) - (on-submit e))))] - - ;; Clear form token cache on unmount - (mf/with-effect [] - #(reset! form-token-cache-atom nil)) - - ;; Update the value when editing an existing token - ;; so the user doesn't have to interact with the form to validate the token - (mf/with-effect [is-create token resolved-tokens token-resolve-result set-resolve-value] - (when (and (not is-create) - (:value token) ;; Don't retrigger this effect when switching tabs on composite tokens - (not token-resolve-result) - resolved-tokens) - (-> (get resolved-tokens (mf/ref-val token-name-ref)) - (set-resolve-value)))) - - [:form {:class (stl/css :form-wrapper) - :on-submit on-submit} - [:div {:class (stl/css :token-rows)} - [:> heading* {:level 2 :typography "headline-medium" :class (stl/css :form-modal-title)} - (if (= action "edit") - (tr "workspace.tokens.edit-token" token-type) - (tr "workspace.tokens.create-token" token-type))] - - [:div {:class (stl/css :input-row)} - (let [token-title (str/lower (:title token-properties))] - [:> input* {:id "token-name" - :label (tr "workspace.tokens.token-name") - :placeholder (tr "workspace.tokens.enter-token-name", token-title) - :max-length max-input-length - :variant "comfortable" - :auto-focus true - :default-value (mf/ref-val token-name-ref) - :hint-type (when-not (empty? name-errors) "error") - :hint-message (first name-errors) - :ref name-ref - :on-blur on-blur-name - :on-change on-update-name}]) - - (when (and warning-name-change? (= action "edit")) - [:div {:class (stl/css :warning-name-change-notification-wrapper)} - [:> context-notification* - {:level :warning :appearance :ghost} (tr "workspace.tokens.warning-name-change")]])] - - [:div {:class (stl/css :input-row)} - (let [placeholder (or input-value-placeholder (tr "workspace.tokens.token-value-enter")) - label (tr "workspace.tokens.token-value") - default-value (mf/ref-val value-ref) - ref value-input-ref] - (if (fn? custom-input-token-value) - [:> custom-input-token-value - {:placeholder placeholder - :label label - :default-value default-value - :input-ref ref - :on-update-value on-update-value - :on-external-update-value on-external-update-value - :custom-input-token-value-props custom-input-token-value-props - :token-resolve-result token-resolve-result - :clear-resolve-value clear-resolve-value}] - [:> input-token* - {:placeholder placeholder - :label label - :default-value default-value - :ref ref - :on-blur on-update-value - :on-change on-update-value - :token-resolve-result token-resolve-result}]))] - [:div {:class (stl/css :input-row)} - [:> input* {:label (tr "workspace.tokens.token-description") - :placeholder (tr "workspace.tokens.token-description") - :is-optional true - :max-length max-input-length - :variant "comfortable" - :default-value (mf/ref-val description-ref) - :hint-type (when-not (empty? description-errors) "error") - :hint-message (first description-errors) - :on-blur on-update-description - :on-change on-update-description}]] - - [:div {:class (stl/css-case :button-row true - :with-delete (= action "edit"))} - (when (= action "edit") - [:> button* {:on-click on-delete-token - :on-key-down handle-key-down-delete - :class (stl/css :delete-btn) - :type "button" - :icon i/delete - :variant "secondary"} - (tr "labels.delete")]) - [:> button* {:on-click on-cancel - :on-key-down handle-key-down-cancel - :type "button" - :id "token-modal-cancel" - :variant "secondary"} - (tr "labels.cancel")] - [:> button* {:type "submit" - :on-key-down handle-key-down-save - :variant "primary" - :disabled disabled?} - (tr "labels.save")]]]])) - -;; Tabs Component -------------------------------------------------------------- - -(mf/defc composite-reference-input* - [{:keys [default-value on-blur on-update-value token-resolve-result reference-label reference-icon is-reference-fn]}] - [:> input-token* - {:aria-label (tr "labels.reference") - :placeholder reference-label - :icon reference-icon - :default-value (when (is-reference-fn default-value) default-value) - :on-blur on-blur - :on-change on-update-value - :token-resolve-result (when (or - (:errors token-resolve-result) - (string? (:value token-resolve-result))) - token-resolve-result)}]) - -(mf/defc composite-tabs* - [{:keys [default-value - on-update-value - on-external-update-value - on-value-resolve - clear-resolve-value - custom-input-token-value-props] - :rest props}] - (let [;; Active Tab State - {:keys [active-tab - composite-tab - is-reference-fn - reference-icon - reference-label - set-active-tab - title - update-composite-backup-value]} custom-input-token-value-props - reference-tab-active? (= :reference active-tab) - ;; Backup value ref - ;; Used to restore the previously entered value when switching tabs - ;; Uses ref to not trigger state updates during update - backup-state-ref (mf/use-var - (if reference-tab-active? - {:reference default-value} - {:composite default-value})) - default-value (get @backup-state-ref active-tab) - - on-toggle-tab - (mf/use-fn - (mf/deps active-tab on-external-update-value on-value-resolve clear-resolve-value) - (fn [] - (let [next-tab (if (= active-tab :composite) :reference :composite)] - ;; Clear the resolved value so it wont show up before the next-tab value has resolved - (clear-resolve-value) - ;; Restore the internal value from backup - (on-external-update-value (get @backup-state-ref next-tab)) - (set-active-tab next-tab)))) - - update-composite-value - (mf/use-fn - (fn [f] - (clear-resolve-value) - (swap! backup-state-ref f) - (on-external-update-value (get @backup-state-ref :composite)))) - - ;; Store updated value in backup-state-ref - on-update-value' - (mf/use-fn - (mf/deps on-update-value reference-tab-active? update-composite-backup-value) - (fn [e] - (if reference-tab-active? - (swap! backup-state-ref assoc :reference (dom/get-target-val e)) - (swap! backup-state-ref update :composite #(update-composite-backup-value % e))) - (on-update-value e)))] - [:div {:class (stl/css :typography-inputs-row)} - [:div {:class (stl/css :title-bar)} - [:div {:class (stl/css :title)} title] - [:& radio-buttons {:class (stl/css :listing-options) - :selected (if reference-tab-active? "reference" "composite") - :on-change on-toggle-tab - :name "reference-composite-tab"} - [:& radio-button {:icon i/layers - :value "composite" - :title (tr "workspace.tokens.individual-tokens") - :id "composite-opt"}] - [:& radio-button {:icon i/tokens - :value "reference" - :title (tr "workspace.tokens.use-reference") - :id "reference-opt"}]]] - [:div {:class (stl/css :typography-inputs)} - (if reference-tab-active? - [:> composite-reference-input* - (mf/spread-props props {:default-value default-value - :on-update-value on-update-value' - :reference-icon reference-icon - :reference-label reference-label - :is-reference-fn is-reference-fn})] - [:> composite-tab - (mf/spread-props props {:default-value default-value - :on-update-value on-update-value' - :update-composite-value update-composite-value})])]])) - -(mf/defc composite-form* - "Wrapper around form* that manages composite/reference tab state. - Takes the same props as form* plus a function to determine if a token value is a reference." - [{:keys [token is-reference-fn composite-tab reference-icon title update-composite-backup-value] :rest props}] - (let [active-tab* (mf/use-state (if (is-reference-fn (:value token)) :reference :composite)) - active-tab (deref active-tab*) - - custom-input-token-value-props - (mf/use-memo - (mf/deps active-tab composite-tab reference-icon title update-composite-backup-value is-reference-fn) - (fn [] - {:active-tab active-tab - :set-active-tab #(reset! active-tab* %) - :composite-tab composite-tab - :reference-icon reference-icon - :reference-label (tr "workspace.tokens.reference-composite") - :title title - :update-composite-backup-value update-composite-backup-value - :is-reference-fn is-reference-fn})) - - ;; Remove the value from a stored token when it doesn't match the tab type - ;; We need this to keep the form disabled when there's an existing value that doesn't match the tab type - token - (mf/use-memo - (mf/deps token active-tab is-reference-fn) - (fn [] - (let [token-tab-type (if (is-reference-fn (:value token)) :reference :composite)] - (cond-> token - (not= token-tab-type active-tab) (dissoc :value token)))))] - [:> form* - (mf/spread-props props {:token token - :custom-input-token-value composite-tabs* - :custom-input-token-value-props custom-input-token-value-props})])) - -;; Token Type Forms ------------------------------------------------------------ - -;; FIXME: this function has confusing name -(defn- hex->value - [hex] - (when-let [tc (tinycolor/valid-color hex)] - (let [hex (tinycolor/->hex-string tc) - alpha (tinycolor/alpha tc) - [r g b] (c/hex->rgb hex) - [h s v] (c/hex->hsv hex)] - {:hex hex - :r r :g g :b b - :h h :s s :v v - :alpha alpha}))) - -(mf/defc ramp* - [{:keys [color on-change]}] - (let [wrapper-node-ref (mf/use-ref nil) - dragging-ref (mf/use-ref false) - - on-start-drag - (mf/use-fn #(mf/set-ref-val! dragging-ref true)) - - on-finish-drag - (mf/use-fn #(mf/set-ref-val! dragging-ref false)) - - internal-color* - (mf/use-state #(hex->value color)) - - internal-color - (deref internal-color*) - - on-change' - (mf/use-fn - (mf/deps on-change) - (fn [{:keys [hex alpha] :as selector-color}] - (let [dragging? (mf/ref-val dragging-ref)] - (when-not (and dragging? hex) - (reset! internal-color* selector-color) - (on-change hex alpha)))))] - (mf/use-effect - (mf/deps color) - (fn [] - ;; Update internal color when user changes input value - (when-let [color (tinycolor/valid-color color)] - (when-not (= (tinycolor/->hex-string color) (:hex internal-color)) - (reset! internal-color* (hex->value color)))))) - - (colorpicker/use-color-picker-css-variables! wrapper-node-ref internal-color) - [:div {:ref wrapper-node-ref} - [:> ramp-selector* - {:color internal-color - :on-start-drag on-start-drag - :on-finish-drag on-finish-drag - :on-change on-change'}]])) - -(mf/defc color-picker* - [{:keys [placeholder label default-value input-ref on-blur on-update-value on-external-update-value custom-input-token-value-props token-resolve-result]}] - (let [{:keys [color on-display-colorpicker]} custom-input-token-value-props - color-ramp-open* (mf/use-state false) - color-ramp-open? (deref color-ramp-open*) - - on-click-swatch - (mf/use-fn - (mf/deps color-ramp-open? on-display-colorpicker) - (fn [] - (let [open? (not color-ramp-open?)] - (reset! color-ramp-open* open?) - (when on-display-colorpicker - (on-display-colorpicker open?))))) - - swatch - (mf/html - [:> input-token-color-bullet* - {:color color - :class (stl/css :slot-start) - :on-click on-click-swatch}]) - - on-change' - (mf/use-fn - (mf/deps color on-external-update-value) - (fn [hex-value alpha] - (let [;; StyleDictionary will always convert to hex/rgba, so we take the format from the value input field - prev-input-color (some-> (dom/get-value (mf/ref-val input-ref)) - (tinycolor/valid-color)) - ;; If the input is a reference we will take the format from the computed value - prev-computed-color (when-not prev-input-color - (some-> color (tinycolor/valid-color))) - prev-format (some-> (or prev-input-color prev-computed-color) - (tinycolor/color-format)) - to-rgba? (and - (< alpha 1) - (or (= prev-format "hex") (not prev-format))) - to-hex? (and (not prev-format) (= alpha 1)) - format (cond - to-rgba? "rgba" - to-hex? "hex" - prev-format prev-format - :else "hex") - color-value (-> (tinycolor/valid-color hex-value) - (tinycolor/set-alpha (or alpha 1)) - (tinycolor/->string format))] - (dom/set-value! (mf/ref-val input-ref) color-value) - (on-external-update-value color-value))))] - - [:* - [:> input-token* - {:placeholder placeholder - :label label - :default-value default-value - :ref input-ref - :on-blur on-blur - :on-change on-update-value - :slot-start swatch}] - (when color-ramp-open? - [:> ramp* - {:color (some-> color (tinycolor/valid-color)) - :on-change on-change'}]) - [:> token-value-hint* {:result token-resolve-result}]])) - -(mf/defc shadow-color-picker-wrapper* - "Wrapper for color-picker* that passes shadow color state from parent. - Similar to color-form* but receives color state from shadow-value-inputs*." - [{:keys [placeholder label default-value input-ref on-update-value on-external-update-value token-resolve-result shadow-color]}] - (let [;; Use the color state passed from parent (shadow-value-inputs*) - resolved-color (get token-resolve-result :resolved-value) - color (or shadow-color resolved-color default-value "") - - custom-input-token-value-props - (mf/use-memo - (mf/deps color) - (fn [] - {:color color}))] - - [:> color-picker* - {:placeholder placeholder - :label label - :default-value default-value - :input-ref input-ref - :on-update-value on-update-value - :on-external-update-value on-external-update-value - :custom-input-token-value-props custom-input-token-value-props - :token-resolve-result token-resolve-result}])) - -(def ^:private shadow-inputs - #(d/ordered-map - :offsetX - {:label (tr "workspace.tokens.shadow-x") - :placeholder (tr "workspace.tokens.shadow-x")} - :offsetY - {:label (tr "workspace.tokens.shadow-y") - :placeholder (tr "workspace.tokens.shadow-y")} - :blur - {:label (tr "workspace.tokens.shadow-blur") - :placeholder (tr "workspace.tokens.shadow-blur")} - :spread - {:label (tr "workspace.tokens.shadow-spread") - :placeholder (tr "workspace.tokens.shadow-spread")} - :color - {:label (tr "workspace.tokens.shadow-color") - :placeholder (tr "workspace.tokens.shadow-color")} - :inset - {:label (tr "workspace.tokens.shadow-inset") - :placeholder (tr "workspace.tokens.shadow-inset")})) - -(mf/defc inset-type-select* - [{:keys [default-value shadow-idx label on-change]}] - (let [selected* (mf/use-state (or (str default-value) "false")) - selected (deref selected*) - - on-change - (mf/use-fn - (mf/deps on-change selected shadow-idx) - (fn [value e] - (obj/set! e "tokenValue" (if (= "true" value) true false)) - (on-change e) - (reset! selected* (str value))))] - [:div {:class (stl/css :input-row)} - [:div {:class (stl/css :inset-label)} label] - [:& radio-buttons {:selected selected - :on-change on-change - :name (str "inset-select-" shadow-idx)} - [:& radio-button {:value "false" - :title "false" - :icon i/close - :id (str "inset-default-" shadow-idx)}] - [:& radio-button {:value "true" - :title "true" - :icon i/tick - :id (str "inset-false-" shadow-idx)}]]])) - -(mf/defc shadow-input* - [{:keys [default-value label placeholder shadow-idx input-type on-update-value on-external-update-value token-resolve-result errors-by-key shadow-color]}] - (let [color-input-ref (mf/use-ref) - - on-change - (mf/use-fn - (mf/deps shadow-idx input-type on-update-value) - (fn [e] - (-> (obj/set! e "tokenTypeAtIndex" [shadow-idx input-type]) - (on-update-value)))) - - on-external-update-value' - (mf/use-fn - (mf/deps shadow-idx input-type on-external-update-value) - (fn [v] - (on-external-update-value [shadow-idx input-type] v))) - - resolved (get-in token-resolve-result [:resolved-value shadow-idx input-type]) - - errors (get errors-by-key input-type) - - should-show? (or (some? resolved) (seq errors)) - - token-prop (when should-show? - (d/without-nils - {:resolved-value resolved - :errors errors}))] - (case input-type - :inset - [:> inset-type-select* - {:default-value default-value - :shadow-idx shadow-idx - :label label - :on-change on-change}] - :color - [:> shadow-color-picker-wrapper* - {:placeholder placeholder - :label label - :default-value default-value - :input-ref color-input-ref - :on-update-value on-change - :on-external-update-value on-external-update-value' - :token-resolve-result token-prop - :shadow-color shadow-color - :data-testid (str "shadow-color-input-" shadow-idx)}] - [:div {:class (stl/css :input-row) - :data-testid (str "shadow-" (name input-type) "-input-" shadow-idx)} - [:> input-token* - {:label label - :placeholder placeholder - :default-value default-value - :on-change on-change - :token-resolve-result token-prop}]]))) - -(mf/defc shadow-input-fields* - [{:keys [shadow shadow-idx on-remove-shadow on-add-shadow is-remove-disabled on-update-value token-resolve-result errors-by-key on-external-update-value shadow-color] :as props}] - (let [on-remove-shadow - (mf/use-fn - (mf/deps shadow-idx on-remove-shadow) - #(on-remove-shadow shadow-idx))] - [:div {:data-testid (str "shadow-input-fields-" shadow-idx)} - [:> icon-button* {:icon i/add - :type "button" - :on-click on-add-shadow - :data-testid (str "shadow-add-button-" shadow-idx) - :aria-label (tr "workspace.tokens.shadow-add-shadow")}] - [:> icon-button* {:variant "ghost" - :type "button" - :icon i/remove - :on-click on-remove-shadow - :disabled is-remove-disabled - :data-testid (str "shadow-remove-button-" shadow-idx) - :aria-label (tr "workspace.tokens.shadow-remove-shadow")}] - (for [[input-type {:keys [label placeholder]}] (shadow-inputs)] - [:> shadow-input* - {:key (str input-type shadow-idx) - :input-type input-type - :label label - :placeholder placeholder - :shadow-idx shadow-idx - :default-value (get shadow input-type) - :on-update-value on-update-value - :token-resolve-result token-resolve-result - :errors-by-key errors-by-key - :on-external-update-value on-external-update-value - :shadow-color shadow-color}])])) - -(mf/defc shadow-value-inputs* - [{:keys [default-value on-update-value token-resolve-result update-composite-value] :as props}] - (let [shadows* (mf/use-state (or default-value [{}])) - shadows (deref shadows*) - shadows-count (count shadows) - composite-token? (not (cto/typography-composite-token-reference? (:value token-resolve-result))) - - ;; Maintain a map of color states for each shadow to prevent reset on add/remove - shadow-colors* (mf/use-state {}) - shadow-colors (deref shadow-colors*) - - ;; Initialize color states for each shadow index - _ (mf/use-effect - (mf/deps shadows) - (fn [] - (doseq [[idx shadow] (d/enumerate shadows)] - (when-not (contains? shadow-colors idx) - (let [resolved-color (get-in token-resolve-result [:resolved-value idx :color]) - initial-color (or resolved-color (get shadow :color) "")] - (swap! shadow-colors* assoc idx initial-color)))))) - - ;; Define on-external-update-value here where we have access to on-update-value - on-external-update-value - (mf/use-callback - (mf/deps on-update-value shadow-colors*) - (fn [token-type-at-index value] - (let [[idx token-type] token-type-at-index - e (js-obj)] - ;; Update shadow color state if this is a color update - (when (= token-type :color) - (swap! shadow-colors* assoc idx value)) - (obj/set! e "tokenTypeAtIndex" token-type-at-index) - (obj/set! e "target" #js {:value value}) - (on-update-value e)))) - - on-add-shadow - (mf/use-fn - (mf/deps shadows update-composite-value) - (fn [] - (update-composite-value - (fn [state] - (let [new-state (update state :composite (fnil conj []) {})] - (reset! shadows* (:composite new-state)) - new-state))))) - - on-remove-shadow - (mf/use-fn - (mf/deps shadows update-composite-value) - (fn [idx] - (update-composite-value - (fn [state] - (let [new-state (update state :composite d/remove-at-index idx)] - (reset! shadows* (:composite new-state)) - new-state)))))] - [:div {:class (stl/css :nested-input-row)} - (for [[shadow-idx shadow] (d/enumerate shadows) - :let [is-remove-disabled (= shadows-count 1) - key (str shadows-count shadow-idx) - errors-by-key (when composite-token? - (sd/collect-shadow-errors token-resolve-result shadow-idx))]] - [:div {:key key - :class (stl/css :nested-input-row)} - [:> shadow-input-fields* - {:is-remove-disabled is-remove-disabled - :shadow-idx shadow-idx - :on-add-shadow on-add-shadow - :on-remove-shadow on-remove-shadow - :shadow shadow - :on-update-value on-update-value - :token-resolve-result token-resolve-result - :errors-by-key errors-by-key - :on-external-update-value on-external-update-value - :shadow-color (get shadow-colors shadow-idx "")}]])])) - -(mf/defc shadow-form* - [{:keys [token] :rest props}] - (let [on-get-token-value - (mf/use-callback - (fn [e prev-composite-value] - (let [prev-composite-value (or prev-composite-value []) - [idx token-type :as token-type-at-index] (obj/get e "tokenTypeAtIndex") - input-value (case token-type - :inset (obj/get e "tokenValue") - (dom/get-target-val e)) - reference-value-input? (not token-type-at-index)] - (cond - reference-value-input? input-value - (and (string? input-value) (empty? input-value)) (update prev-composite-value idx dissoc token-type) - :else (assoc-in prev-composite-value token-type-at-index input-value))))) - - update-composite-backup-value - (mf/use-callback - (fn [prev-composite-value e] - (let [[idx token-type :as token-type-at-index] (obj/get e "tokenTypeAtIndex") - token-value (case token-type - :inset (obj/get e "tokenValue") - (dom/get-target-val e)) - valid? (case token-type - :inset (boolean? token-value) - (seq token-value))] - (if valid? - (assoc-in (or prev-composite-value []) token-type-at-index token-value) - ;; Remove empty values so they don't retrigger validation when switching tabs - (update prev-composite-value idx dissoc token-type)))))] - [:> composite-form* - (mf/spread-props props {:token token - :composite-tab shadow-value-inputs* - :reference-icon i/text-typography - :is-reference-fn cto/typography-composite-token-reference? - :title (tr "workspace.tokens.shadow-title") - :validate-token validate-shadow-token - :on-get-token-value on-get-token-value - :update-composite-backup-value update-composite-backup-value})])) - -(mf/defc font-selector-wrapper* - [{:keys [font input-ref on-select-font on-close-font-selector]}] - (let [current-font* (mf/use-state (or font - (some-> (mf/ref-val input-ref) - (dom/get-value) - (cto/split-font-family) - (first) - (fonts/find-font-family)))) - current-font (deref current-font*)] - [:div {:class (stl/css :font-select-wrapper)} - [:> font-selector* {:current-font current-font - :on-select on-select-font - :on-close on-close-font-selector - :full-size true}]])) - -(mf/defc font-picker-combobox* - [{:keys [default-value label aria-label input-ref on-blur on-update-value on-external-update-value token-resolve-result placeholder]}] - (let [font* (mf/use-state (fonts/find-font-family default-value)) - font (deref font*) - set-font (mf/use-fn - (mf/deps font) - #(reset! font* %)) - - font-selector-open* (mf/use-state false) - font-selector-open? (deref font-selector-open*) - - on-close-font-selector - (mf/use-fn - (fn [] - (reset! font-selector-open* false))) - - on-click-dropdown-button - (mf/use-fn - (mf/deps font-selector-open?) - (fn [e] - (dom/prevent-default e) - (reset! font-selector-open* (not font-selector-open?)))) - - on-select-font - (mf/use-fn - (mf/deps on-external-update-value set-font font) - (fn [{:keys [family] :as font}] - (when font - (set-font font) - (on-external-update-value family)))) - - on-update-value' - (mf/use-fn - (mf/deps on-update-value set-font) - (fn [value] - (set-font nil) - (on-update-value value))) - - font-selector-button - (mf/html - [:> icon-button* - {:on-click on-click-dropdown-button - :aria-label (tr "workspace.tokens.token-font-family-select") - :icon i/arrow-down - :variant "action" - :type "button"}])] - [:* - [:> input-token* - {:placeholder (or placeholder (tr "workspace.tokens.token-font-family-value-enter")) - :label label - :aria-label aria-label - :default-value (or (:name font) default-value) - :ref input-ref - :on-blur on-blur - :on-change on-update-value' - :icon i/text-font-family - :slot-end font-selector-button - :token-resolve-result token-resolve-result}] - (when font-selector-open? - [:> font-selector-wrapper* {:font font - :input-ref input-ref - :on-select-font on-select-font - :on-close-font-selector on-close-font-selector}])])) - -(mf/defc font-family-form* - [{:keys [token] :rest props}] - (let [on-value-resolve - (mf/use-fn - (fn [value] - (when value - (cto/join-font-family value))))] - [:> form* - (mf/spread-props props {:token (when token (update token :value cto/join-font-family)) - :custom-input-token-value font-picker-combobox* - :on-value-resolve on-value-resolve - :validate-token validate-font-family-token})])) - -(mf/defc text-decoration-form* - [{:keys [token] :rest props}] - [:> form* - (mf/spread-props props {:token token - :input-value-placeholder (tr "workspace.tokens.text-decoration-value-enter")})]) - -(mf/defc font-weight-form* - [{:keys [token] :rest props}] - [:> form* - (mf/spread-props props {:token token - :input-value-placeholder (tr "workspace.tokens.font-weight-value-enter")})]) - -(def ^:private typography-inputs - #(d/ordered-map - :font-family - {:label (tr "workspace.tokens.token-font-family-value") - :icon i/text-font-family - :placeholder (tr "workspace.tokens.token-font-family-value-enter")} - :font-size - {:label "Font Size" - :icon i/text-font-size - :placeholder (tr "workspace.tokens.font-size-value-enter")} - :font-weight - {:label "Font Weight" - :icon i/text-font-weight - :placeholder (tr "workspace.tokens.font-weight-value-enter")} - :line-height - {:label "Line Height" - :icon i/text-lineheight - :placeholder (tr "workspace.tokens.line-height-value-enter")} - :letter-spacing - {:label "Letter Spacing" - :icon i/text-letterspacing - :placeholder (tr "workspace.tokens.letter-spacing-value-enter-composite")} - :text-case - {:label "Text Case" - :icon i/text-mixed - :placeholder (tr "workspace.tokens.text-case-value-enter")} - :text-decoration - {:label "Text Decoration" - :icon i/text-underlined - :placeholder (tr "workspace.tokens.text-decoration-value-enter")})) - -(mf/defc typography-value-inputs* - [{:keys [default-value on-blur on-update-value token-resolve-result]}] - (let [composite-token? (not (cto/typography-composite-token-reference? (:value token-resolve-result))) - typography-inputs (mf/use-memo typography-inputs) - errors-by-key (sd/collect-typography-errors token-resolve-result)] - [:div {:class (stl/css :nested-input-row)} - (for [[token-type {:keys [label placeholder icon]}] typography-inputs] - (let [value (get default-value token-type) - resolved (get-in token-resolve-result [:resolved-value token-type]) - errors (get errors-by-key token-type) - - should-show? (or (and (some? resolved) - (not= value (str resolved))) - (seq errors)) - - token-prop (when (and composite-token? should-show?) - (d/without-nils - {:resolved-value (when-not (str/empty? resolved) resolved) - :errors errors})) - - input-ref (mf/use-ref) - - on-external-update-value - (mf/use-fn - (mf/deps on-update-value) - (fn [next-value] - (let [element (mf/ref-val input-ref)] - (dom/set-value! element next-value) - (on-update-value #js {:target element - :tokenType :font-family})))) - - on-change - (mf/use-fn - (mf/deps token-type) - ;; Passing token-type via event to prevent deep function adapting & passing of type - (fn [event] - (-> (obj/set! event "tokenType" token-type) - (on-update-value))))] - - [:div {:key (str token-type) - :class (stl/css :input-row)} - (case token-type - :font-family - [:> font-picker-combobox* - {:aria-label label - :placeholder placeholder - :input-ref input-ref - :default-value (when value (cto/join-font-family value)) - :on-blur on-blur - :on-update-value on-change - :on-external-update-value on-external-update-value - :token-resolve-result token-prop}] - [:> input-token* - {:aria-label label - :placeholder placeholder - :default-value value - :on-blur on-blur - :icon icon - :on-change on-change - :token-resolve-result token-prop}])]))])) - -(mf/defc typography-form* - [{:keys [token] :rest props}] - (let [on-get-token-value - (mf/use-fn - (fn [e prev-composite-value] - (let [token-type (obj/get e "tokenType") - input-value (dom/get-target-val e) - reference-value-input? (not token-type)] - (cond - reference-value-input? input-value - - (empty? input-value) (dissoc prev-composite-value token-type) - :else (assoc prev-composite-value token-type input-value))))) - - update-composite-backup-value - (mf/use-fn - (fn [prev-composite-value e] - (let [token-type (obj/get e "tokenType") - token-value (dom/get-target-val e) - token-value (cond-> token-value - (= :font-family token-type) (cto/split-font-family))] - (if (seq token-value) - (assoc prev-composite-value token-type token-value) - ;; Remove empty values so they don't retrigger validation when switching tabs - (dissoc prev-composite-value token-type)))))] - [:> composite-form* - (mf/spread-props props {:token token - :composite-tab typography-value-inputs* - :reference-icon i/text-typography - :is-reference-fn cto/typography-composite-token-reference? - :title (tr "labels.typography") - :validate-token validate-typography-token - :on-get-token-value on-get-token-value - :update-composite-backup-value update-composite-backup-value})])) - -(mf/defc form-wrapper* - [{:keys [token token-type] :rest props}] - (let [token-type - (or (:type token) token-type) - ;; NOTE: All this references to tokens can be - ;; provided via context for - ;; avoid duplicate code among each form, this is because it is - ;; a common code and is probably will be needed on all forms - - tokens-in-selected-set - (mf/deref refs/workspace-all-tokens-in-selected-set) - - token-path - (mf/with-memo [token] - (cft/token-name->path (:name token))) - - tokens-tree-in-selected-set - (mf/with-memo [token-path tokens-in-selected-set] - (-> (ctob/tokens-tree tokens-in-selected-set) - (d/dissoc-in token-path))) - props - (mf/spread-props props {:token-type token-type - :validate-token default-validate-token - :tokens-tree-in-selected-set tokens-tree-in-selected-set - :token token}) - font-family-props (mf/spread-props props {:validate-token validate-font-family-token}) - typography-props (mf/spread-props props {:validate-token validate-typography-token})] - - (case token-type - :color [:> color/form* props] - :typography [:> typography/form* typography-props] - :shadow [:> shadow-form* props] - :font-family [:> font-family/form* font-family-props] - :text-case [:> text-case/form* props] - :text-decoration [:> text-decoration-form* props] - :font-weight [:> font-weight-form* props] - :border-radius [:> border-radius/form* props] - :dimensions [:> dimensions/form* props] - [:> form* props]))) diff --git a/frontend/src/app/main/ui/workspace/tokens/management/create/form.scss b/frontend/src/app/main/ui/workspace/tokens/management/create/form.scss deleted file mode 100644 index 08164ffa83..0000000000 --- a/frontend/src/app/main/ui/workspace/tokens/management/create/form.scss +++ /dev/null @@ -1,105 +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 - -@use "ds/typography.scss" as t; -@use "ds/_sizes.scss" as *; -@use "ds/_borders.scss" as *; - -.form-wrapper { - width: $sz-384; - position: relative; -} - -.button-row { - display: grid; - grid-template-columns: auto auto; - justify-content: end; - gap: var(--sp-m); - padding-block-start: var(--sp-s); -} - -.with-delete { - grid-template-columns: 1fr auto auto; -} - -.delete-btn { - justify-self: start; -} - -.token-rows { - display: flex; - flex-direction: column; - gap: var(--sp-l); -} - -.input-row { - display: flex; - flex-direction: column; - gap: var(--sp-xs); -} - -.nested-input-row { - display: flex; - flex-direction: column; - gap: var(--sp-m); -} - -.typography-inputs-row { - display: flex; - flex-direction: column; - gap: var(--sp-m); -} - -.typography-inputs { - border-inline-start: $b-1 solid var(--color-accent-primary-muted); - padding-inline-start: var(--sp-m); -} - -.title-bar { - display: grid; - grid-template-columns: 1fr auto; -} -.title { - @include t.use-typography("body-small"); - color: var(--color-foreground-primary); - display: flex; - align-items: center; -} - -.warning-name-change-notification-wrapper { - margin-block-start: var(--sp-l); -} - -.error { - padding: var(--sp-xs) $sz-6; - margin-block-end: 0; - color: var(--status-color-error-500); -} - -.resolved-value { - --input-hint-color: var(--color-foreground-primary); - color: var(--input-hint-color); -} - -.resolved-value-error { - --input-hint-color: var(--status-color-error-500); -} - -.resolved-value-warning { - --input-hint-color: var(--status-color-warning-500); -} - -.form-modal-title { - color: var(--color-foreground-primary); -} - -.font-select-wrapper { - position: absolute; - inset: 0; - // This padding from the modal should be shared as a variable - // Need to set this or the font-select will cause scroll - bottom: var(--sp-xxxl); -} diff --git a/frontend/src/app/main/ui/workspace/tokens/management/create/form_color_input_token.cljs b/frontend/src/app/main/ui/workspace/tokens/management/create/form_color_input_token.cljs deleted file mode 100644 index c6c667281e..0000000000 --- a/frontend/src/app/main/ui/workspace/tokens/management/create/form_color_input_token.cljs +++ /dev/null @@ -1,237 +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.main.ui.workspace.tokens.management.create.form-color-input-token - (:require-macros [app.main.style :as stl]) - (:require - [app.common.data :as d] - [app.common.data.macros :as dm] - [app.common.types.color :as cl] - [app.common.types.tokens-lib :as ctob] - [app.main.data.style-dictionary :as sd] - [app.main.data.tinycolor :as tinycolor] - [app.main.ui.ds.controls.input :refer [input*]] - [app.main.ui.ds.utilities.swatch :refer [swatch*]] - [app.main.ui.forms :as fc] - [app.main.ui.workspace.colorpicker :as colorpicker] - [app.main.ui.workspace.colorpicker.ramp :refer [ramp-selector*]] - [app.util.dom :as dom] - [app.util.forms :as fm] - [app.util.i18n :refer [tr]] - [beicon.v2.core :as rx] - [rumext.v2 :as mf])) - -(defn- resolve-value - [tokens prev-token value] - (let [token - {:value value - :name "__PENPOT__TOKEN__NAME__PLACEHOLDER__"} - - tokens - (-> tokens - ;; Remove previous token when renaming a token - (dissoc (:name prev-token)) - (update (:name token) #(ctob/make-token (merge % prev-token token))))] - - (->> tokens - (sd/resolve-tokens-interactive) - (rx/mapcat - (fn [resolved-tokens] - (let [{:keys [errors resolved-value] :as resolved-token} (get resolved-tokens (:name token))] - (if resolved-value - (rx/of {:value resolved-value}) - (rx/of {:error (first errors)})))))))) - -(defn- hex->color-obj - [hex] - (when-let [tc (tinycolor/valid-color hex)] - (let [hex (tinycolor/->hex-string tc) - alpha (tinycolor/alpha tc) - [r g b] (cl/hex->rgb hex) - [h s v] (cl/hex->hsv hex)] - {:hex hex - :r r :g g :b b - :h h :s s :v v - :alpha alpha}))) - -(mf/defc ramp* - [{:keys [color on-change]}] - (let [wrapper-node-ref (mf/use-ref nil) - dragging-ref (mf/use-ref false) - - on-start-drag - (mf/use-fn #(mf/set-ref-val! dragging-ref true)) - - on-finish-drag - (mf/use-fn #(mf/set-ref-val! dragging-ref false)) - - internal-color* - (mf/use-state #(hex->color-obj color)) - - internal-color - (deref internal-color*) - - on-change' - (mf/use-fn - (mf/deps on-change) - (fn [{:keys [hex alpha] :as selector-color}] - (let [dragging? (mf/ref-val dragging-ref)] - (when-not (and dragging? hex) - (reset! internal-color* selector-color) - (on-change hex alpha)))))] - (mf/use-effect - (mf/deps color) - (fn [] - ;; Update internal color when user changes input value - (when-let [color (tinycolor/valid-color color)] - (when-not (= (tinycolor/->hex-string color) (:hex internal-color)) - (reset! internal-color* (hex->color-obj color)))))) - - (colorpicker/use-color-picker-css-variables! wrapper-node-ref internal-color) - [:div {:ref wrapper-node-ref} - [:> ramp-selector* - {:color internal-color - :on-start-drag on-start-drag - :on-finish-drag on-finish-drag - :on-change on-change'}]])) - -(mf/defc form-color-input-token* - [{:keys [name tokens token] :rest props}] - - (let [form (mf/use-ctx fc/context) - input-name name - - - touched? - (and (contains? (:data @form) input-name) - (get-in @form [:touched input-name])) - - error - (get-in @form [:errors input-name]) - - value - (get-in @form [:data input-name] "") - - hex (if (tinycolor/valid-color value) - (tinycolor/->hex-string (tinycolor/valid-color value)) - "#8f9da3") - - alpha (if (tinycolor/valid-color value) - (tinycolor/alpha (tinycolor/valid-color value)) - 1) - - resolve-stream - (mf/with-memo [token] - (if-let [value (:value token)] - (rx/behavior-subject value) - (rx/subject))) - - hint* - (mf/use-state {}) - - hint - (deref hint*) - - color-ramp-open* (mf/use-state false) - color-ramp-open? (deref color-ramp-open*) - - on-click-swatch - (mf/use-fn - (mf/deps color-ramp-open?) - (fn [] - (let [open? (not color-ramp-open?)] - (reset! color-ramp-open* open?)))) - - swatch - (mf/html - [:> swatch* - {:background {:color hex :opacity alpha} - :show-tooltip false - :data-testid "token-form-color-bullet" - :class (stl/css :slot-start) - :on-click on-click-swatch}]) - - on-change-value - (mf/use-fn - (mf/deps resolve-stream input-name value) - (fn [hex alpha] - (let [;; StyleDictionary will always convert to hex/rgba, so we take the format from the value input field - prev-input-color (some-> value - (tinycolor/valid-color)) - ;; If the input is a reference we will take the format from the computed value - prev-computed-color (when-not prev-input-color - (some-> value (tinycolor/valid-color))) - prev-format (some-> (or prev-input-color prev-computed-color) - (tinycolor/color-format)) - to-rgba? (and - (< alpha 1) - (or (= prev-format "hex") (not prev-format))) - to-hex? (and (not prev-format) (= alpha 1)) - format (cond - to-rgba? "rgba" - to-hex? "hex" - prev-format prev-format - :else "hex") - color-value (-> (tinycolor/valid-color hex) - (tinycolor/set-alpha (or alpha 1)) - (tinycolor/->string format))] - (when (not= value color-value) - (fm/on-input-change form input-name color-value true) - (rx/push! resolve-stream color-value))))) - - on-change - (mf/use-fn - (mf/deps resolve-stream input-name) - (fn [event] - (let [raw-value (-> event dom/get-target dom/get-input-value) - value (if (tinycolor/hex-without-hash-prefix? raw-value) - (dm/str "#" raw-value) - raw-value)] - (fm/on-input-change form input-name value true) - (rx/push! resolve-stream value)))) - - props - (mf/spread-props props {:on-change on-change - ;; TODO: Review this value vs default-value - :value (or value "") - :hint-message (:message hint) - :variant "comfortable" - :slot-start swatch - :hint-type (:type hint)}) - - props - (if (and error touched?) - (mf/spread-props props {:hint-type "error" - :hint-message (:message error)}) - props)] - - (mf/with-effect [resolve-stream tokens token input-name] - (let [subs (->> resolve-stream - (rx/debounce 300) - (rx/mapcat (partial resolve-value tokens token)) - (rx/map (fn [result] - (d/update-when result :error - (fn [error] - ((:error/fn error) (:error/value error)))))) - (rx/subs! (fn [{:keys [error value]}] - (let [touched? (get-in @form [:touched input-name])] - (when touched? - (if error - (do - (swap! form assoc-in [:extra-errors input-name] {:message error}) - (reset! hint* {:message error :type "error"})) - (let [message (tr "workspace.tokens.resolved-value" value)] - (swap! form update :extra-errors dissoc input-name) - (reset! hint* {:message message :type "hint"}))))))))] - - (fn [] - (rx/dispose! subs)))) - - [:* - [:> input* props] - - (when color-ramp-open? - [:> ramp* {:color value :on-change on-change-value}])])) \ No newline at end of file diff --git a/frontend/src/app/main/ui/workspace/tokens/management/create/form_input_token.cljs b/frontend/src/app/main/ui/workspace/tokens/management/create/form_input_token.cljs deleted file mode 100644 index 90c681beb6..0000000000 --- a/frontend/src/app/main/ui/workspace/tokens/management/create/form_input_token.cljs +++ /dev/null @@ -1,211 +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.main.ui.workspace.tokens.management.create.form-input-token - (:require - [app.common.data :as d] - [app.common.types.tokens-lib :as ctob] - [app.main.data.style-dictionary :as sd] - [app.main.data.workspace.tokens.format :as dwtf] - [app.main.ui.ds.controls.input :refer [input*]] - [app.main.ui.forms :as fc] - [app.util.dom :as dom] - [app.util.forms :as fm] - [app.util.i18n :refer [tr]] - [beicon.v2.core :as rx] - [cuerdas.core :as str] - [rumext.v2 :as mf])) - -(defn- resolve-value - [tokens prev-token value] - (let [token - {:value value - :name "__PENPOT__TOKEN__NAME__PLACEHOLDER__"} - tokens - (-> tokens - ;; Remove previous token when renaming a token - (dissoc (:name prev-token)) - (update (:name token) #(ctob/make-token (merge % prev-token token))))] - - (->> tokens - (sd/resolve-tokens-interactive) - (rx/mapcat - (fn [resolved-tokens] - (let [{:keys [errors resolved-value] :as resolved-token} (get resolved-tokens (:name token))] - (if resolved-value - (rx/of {:value resolved-value}) - (rx/of {:error (first errors)})))))))) - -(mf/defc form-input-token* - [{:keys [name tokens token] :rest props}] - - (let [form (mf/use-ctx fc/context) - input-name name - - touched? - (and (contains? (:data @form) input-name) - (get-in @form [:touched input-name])) - - error - (get-in @form [:errors input-name]) - - value - (get-in @form [:data input-name] "") - - resolve-stream - (mf/with-memo [token] - (if (contains? token :value) - (rx/behavior-subject (:value token)) - (rx/subject))) - - hint* - (mf/use-state {}) - - hint - (deref hint*) - - on-change - (mf/use-fn - (mf/deps resolve-stream input-name) - (fn [event] - (let [value (-> event dom/get-target dom/get-input-value)] - (fm/on-input-change form input-name value true) - (rx/push! resolve-stream value)))) - - props - (mf/spread-props props {:on-change on-change - :default-value value - :variant "comfortable" - :hint-message (:message hint) - :hint-type (:type hint)}) - props - (if (and error touched?) - (mf/spread-props props {:hint-type "error" - :hint-message (:message error)}) - props)] - - (mf/with-effect [resolve-stream tokens token input-name] - (let [subs (->> resolve-stream - (rx/debounce 300) - (rx/mapcat (partial resolve-value tokens token)) - (rx/map (fn [result] - (d/update-when result :error - (fn [error] - ((:error/fn error) (:error/value error)))))) - (rx/subs! (fn [{:keys [error value]}] - (let [touched? (get-in @form [:touched input-name])] - (when touched? - (if error - (do - (swap! form assoc-in [:extra-errors input-name] {:message error}) - (reset! hint* {:message error :type "error"})) - (let [message (tr "workspace.tokens.resolved-value" value)] - (swap! form update :extra-errors dissoc input-name) - (reset! hint* {:message message :type "hint"}))))))))] - - (fn [] - (rx/dispose! subs)))) - - [:> input* props])) - -(defn- on-composite-input-token-change - ([form field value] - (on-composite-input-token-change form field value false)) - ([form field value trim?] - (letfn [(clean-errors [errors] - (-> errors - (dissoc field) - (not-empty)))] - (swap! form (fn [state] - (-> state - (assoc-in [:data :value field] (if trim? (str/trim value) value)) - (update :errors clean-errors) - (update :extra-errors clean-errors))))))) - -(mf/defc token-composite-value-input* - [{:keys [name tokens token] :rest props}] - - (let [form (mf/use-ctx fc/context) - input-name name - - error - (get-in @form [:errors :value input-name]) - - value - (get-in @form [:data :value input-name] "") - - resolve-stream - (mf/with-memo [token] - (if-let [value (get-in token [:value input-name])] - (rx/behavior-subject value) - (rx/subject))) - - hint* - (mf/use-state {}) - - hint - (deref hint*) - - on-change - (mf/use-fn - (mf/deps resolve-stream input-name) - (fn [event] - (let [value (-> event dom/get-target dom/get-input-value)] - (on-composite-input-token-change form input-name value true) - (rx/push! resolve-stream value)))) - - props - (mf/spread-props props {:on-change on-change - :default-value value - :variant "comfortable" - :hint-message (:message hint) - :hint-type (:type hint)}) - props - (if error - (mf/spread-props props {:hint-type "error" - :hint-message (:message error)}) - props) - - props (if (and (not error) (= input-name :reference)) - (mf/spread-props props {:hint-formated true}) - props)] - - (mf/with-effect [resolve-stream tokens token input-name] - (let [subs (->> resolve-stream - (rx/debounce 300) - (rx/mapcat (partial resolve-value tokens token)) - (rx/map (fn [result] - (d/update-when result :error - (fn [error] - (assoc error :message ((:error/fn error) (:error/value error))))))) - - (rx/subs! - (fn [{:keys [error value]}] - (cond - (and error (str/empty? (:error/value error))) - (do - (swap! form update-in [:errors :value] dissoc input-name) - (swap! form update-in [:data :value] dissoc input-name) - (swap! form update :extra-errors dissoc :value) - (reset! hint* {})) - - (some? error) - (let [error' (:message error)] - (swap! form assoc-in [:extra-errors :value input-name] {:message error'}) - (reset! hint* {:message error' :type "error"})) - - :else - (let [message (tr "workspace.tokens.resolved-value" (dwtf/format-token-value value)) - input-value (get-in @form [:data :value input-name] "")] - (swap! form update :errors dissoc :value) - (swap! form update :extra-errors dissoc :value) - (if (= input-value (str value)) - (reset! hint* {}) - (reset! hint* {:message message :type "hint"})))))))] - (fn [] - (rx/dispose! subs)))) - - [:> input* props])) diff --git a/frontend/src/app/main/ui/workspace/tokens/management/create/input_token_color_bullet.cljs b/frontend/src/app/main/ui/workspace/tokens/management/create/input_token_color_bullet.cljs deleted file mode 100644 index 18ed631f9c..0000000000 --- a/frontend/src/app/main/ui/workspace/tokens/management/create/input_token_color_bullet.cljs +++ /dev/null @@ -1,28 +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.main.ui.workspace.tokens.management.create.input-token-color-bullet - (:require-macros [app.main.style :as stl]) - (:require - [app.main.data.workspace.tokens.color :as dwtc] - [app.main.ui.components.color-bullet :refer [color-bullet]] - [rumext.v2 :as mf])) - -(def ^:private schema::input-token-color-bullet - [:map - [:color [:maybe :string]] - [:on-click fn?]]) - -(mf/defc input-token-color-bullet* - {::mf/props :obj - ::mf/schema schema::input-token-color-bullet} - [{:keys [color on-click]}] - [:div {:data-testid "token-form-color-bullet" - :class (stl/css :input-token-color-bullet) - :on-click on-click} - (if-let [color' (dwtc/color-bullet-color color)] - [:> color-bullet {:color color' :mini true}] - [:div {:class (stl/css :input-token-color-bullet-placeholder)}])]) diff --git a/frontend/src/app/main/ui/workspace/tokens/management/create/input_token_color_bullet.scss b/frontend/src/app/main/ui/workspace/tokens/management/create/input_token_color_bullet.scss deleted file mode 100644 index 7ce04f34ca..0000000000 --- a/frontend/src/app/main/ui/workspace/tokens/management/create/input_token_color_bullet.scss +++ /dev/null @@ -1,28 +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 - -@use "ds/_sizes.scss" as *; -@use "ds/_borders.scss" as *; - -.input-token-color-bullet { - --bullet-size: var(--sp-l); - --bullet-default-color: var(--color-foreground-secondary); - --bullet-radius: var(--br-4); - - margin-inline-end: var(--sp-s); - cursor: pointer; -} - -.input-token-color-bullet-placeholder { - width: var(--bullet-size, --sp-l); - height: var(--bullet-size, --sp-l); - min-width: var(--bullet-size, --sp-l); - min-height: var(--bullet-size, --sp-l); - margin-top: 0; - background-color: color-mix(in hsl, var(--bullet-default-color) 30%, transparent); - border-radius: $br-4; - cursor: pointer; -} diff --git a/frontend/src/app/main/ui/workspace/tokens/management/create/input_tokens_value.cljs b/frontend/src/app/main/ui/workspace/tokens/management/create/input_tokens_value.cljs deleted file mode 100644 index 9ab8d6f4ab..0000000000 --- a/frontend/src/app/main/ui/workspace/tokens/management/create/input_tokens_value.cljs +++ /dev/null @@ -1,78 +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.main.ui.workspace.tokens.management.create.input-tokens-value - (:require-macros [app.main.style :as stl]) - (:require - [app.common.data.macros :as dm] - [app.main.data.workspace.tokens.errors :as wte] - [app.main.data.workspace.tokens.format :as dwtf] - [app.main.data.workspace.tokens.warnings :as wtw] - [app.main.ui.ds.controls.utilities.hint-message :refer [hint-message*]] - [app.main.ui.ds.controls.utilities.input-field :refer [input-field*]] - [app.main.ui.ds.controls.utilities.label :refer [label*]] - [app.main.ui.ds.foundations.assets.icon :refer [icon-list]] - [app.util.i18n :refer [tr]] - [cuerdas.core :as str] - [rumext.v2 :as mf])) - -(def ^:private schema::input-token - [:map - [:label {:optional true} [:maybe :string]] - [:aria-label {:optional true} [:maybe :string]] - [:placeholder {:optional true} :string] - [:value {:optional true} [:maybe :string]] - [:class {:optional true} :string] - [:error {:optional true} :boolean] - [:slot-start {:optional true} [:maybe some?]] - [:icon {:optional true} - [:maybe [:and :string [:fn #(contains? icon-list %)]]]] - [:token-resolve-result {:optional true} :any]]) - -(mf/defc token-value-hint* - [{:keys [result]}] - (let [{:keys [errors warnings resolved-value]} result - empty-message? (nil? result) - - message (cond - empty-message? (tr "workspace.tokens.resolved-value" "-") - warnings (->> (wtw/humanize-warnings warnings) - (str/join "\n")) - errors (->> (wte/humanize-errors errors) - (str/join "\n")) - :else (tr "workspace.tokens.resolved-value" (dwtf/format-token-value (or resolved-value result)))) - type (cond - empty-message? "hint" - errors "error" - warnings "warning" - :else "hint")] - [:> hint-message* - {:id "token-value-hint" - :message message - :class (stl/css-case :resolved-value (not (or empty-message? (seq warnings) (seq errors)))) - :type type}])) - -(mf/defc input-token* - {::mf/forward-ref true - ::mf/schema schema::input-token} - [{:keys [class label token-resolve-result] :rest props} ref] - (let [error (not (nil? (:errors token-resolve-result))) - id (mf/use-id) - input-ref (mf/use-ref) - props (mf/spread-props props {:id id - :type "text" - :class (stl/css :input) - :variant "comfortable" - :hint-type (when error "error") - :ref (or ref input-ref)})] - [:* - [:div {:class (dm/str class " " (stl/css-case :wrapper true - :input-error error))} - (when label - [:> label* {:for id} label]) - [:> input-field* props]] - (when token-resolve-result - [:> token-value-hint* {:result token-resolve-result}])])) diff --git a/frontend/src/app/main/ui/workspace/tokens/management/create/input_tokens_value.scss b/frontend/src/app/main/ui/workspace/tokens/management/create/input_tokens_value.scss deleted file mode 100644 index 8b47b91bdd..0000000000 --- a/frontend/src/app/main/ui/workspace/tokens/management/create/input_tokens_value.scss +++ /dev/null @@ -1,36 +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 - -@use "ds/typography.scss" as *; -@use "ds/_sizes.scss" as *; - -@use "refactor/common-refactor.scss" as deprecated; - -.wrapper { - --label-color: var(--color-foreground-primary); - --input-bg-color: var(--color-background-tertiary); - --input-fg-color: var(--color-foreground-primary); - --input-icon-color: var(--color-foreground-secondary); - --input-outline-color: none; - - display: flex; - flex-direction: column; - gap: var(--sp-xs); - - &.input-error { - --input-outline-color: var(--color-accent-error); - } -} - -.label { - @include use-typography("body-small"); - @include deprecated.textEllipsis; - color: var(--label-color); -} - -.resolved-value { - white-space: pre; -} diff --git a/frontend/src/app/main/ui/workspace/tokens/management/create/text_case.cljs b/frontend/src/app/main/ui/workspace/tokens/management/create/text_case.cljs deleted file mode 100644 index 82a2d5ba2c..0000000000 --- a/frontend/src/app/main/ui/workspace/tokens/management/create/text_case.cljs +++ /dev/null @@ -1,224 +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.main.ui.workspace.tokens.management.create.text-case - (:require-macros [app.main.style :as stl]) - (:require - [app.common.files.tokens :as cft] - [app.common.schema :as sm] - [app.common.types.token :as cto] - [app.common.types.tokens-lib :as ctob] - [app.main.constants :refer [max-input-length]] - [app.main.data.modal :as modal] - [app.main.data.workspace.tokens.application :as dwta] - [app.main.data.workspace.tokens.library-edit :as dwtl] - [app.main.data.workspace.tokens.propagation :as dwtp] - [app.main.refs :as refs] - [app.main.store :as st] - [app.main.ui.ds.buttons.button :refer [button*]] - [app.main.ui.ds.foundations.assets.icon :as i] - [app.main.ui.ds.foundations.typography.heading :refer [heading*]] - [app.main.ui.ds.notifications.context-notification :refer [context-notification*]] - [app.main.ui.forms :as fc] - [app.main.ui.workspace.tokens.management.create.form-input-token :refer [form-input-token*]] - [app.util.dom :as dom] - [app.util.forms :as fm] - [app.util.i18n :refer [tr]] - [app.util.keyboard :as k] - [beicon.v2.core :as rx] - [cuerdas.core :as str] - [rumext.v2 :as mf])) - -(defn- token-value-error-fn - [{:keys [value]}] - (when (or (str/empty? value) - (str/blank? value)) - (tr "workspace.tokens.empty-input"))) - -(defn- make-schema - [tokens-tree] - (sm/schema - [:and - [:map - [:name - [:and - [:string {:min 1 :max 255 :error/fn #(str (:value %) (tr "workspace.tokens.token-name-length-validation-error"))}] - (sm/update-properties cto/token-name-ref assoc :error/fn #(str (:value %) (tr "workspace.tokens.token-name-validation-error"))) - [:fn {:error/fn #(tr "workspace.tokens.token-name-duplication-validation-error" (:value %))} - #(not (cft/token-name-path-exists? % tokens-tree))]]] - - [:value [::sm/text {:error/fn token-value-error-fn}]] - - [:description {:optional true} - [:string {:max 2048 :error/fn #(tr "errors.field-max-length" 2048)}]]] - - [:fn {:error/field :value - :error/fn #(tr "workspace.tokens.self-reference")} - (fn [{:keys [name value]}] - (when (and name value) - (nil? (cto/token-value-self-reference? name value))))]])) - -(mf/defc form* - [{:keys [token validate-token action is-create selected-token-set-id tokens-tree-in-selected-set] :as props}] - - (let [token - (mf/with-memo [token] - (or token {:type :text-case})) - - token-type - (get token :type) - - token-properties - (dwta/get-token-properties token) - - token-title (str/lower (:title token-properties)) - - tokens - (mf/deref refs/workspace-active-theme-sets-tokens) - - tokens - (mf/with-memo [tokens token] - ;; Ensure that the resolved value uses the currently editing token - ;; even if the name has been overriden by a token with the same name - ;; in another set below. - (cond-> tokens - (and (:name token) (:value token)) - (assoc (:name token) token))) - - schema - (mf/with-memo [tokens-tree-in-selected-set] - (make-schema tokens-tree-in-selected-set)) - - initial - (mf/with-memo [token] - {:name (:name token "") - :value (:value token "") - :description (:description token "")}) - - form - (fm/use-form :schema schema - :initial initial) - - warning-name-change? - (not= (get-in @form [:data :name]) - (:name initial)) - - on-cancel - (mf/use-fn - (fn [e] - (dom/prevent-default e) - (modal/hide!))) - - on-delete-token - (mf/use-fn - (mf/deps selected-token-set-id token) - (fn [e] - (dom/prevent-default e) - (modal/hide!) - (st/emit! (dwtl/delete-token selected-token-set-id (:id token))))) - - handle-key-down-delete - (mf/use-fn - (mf/deps on-delete-token) - (fn [e] - (when (or (k/enter? e) (k/space? e)) - (on-delete-token e)))) - - handle-key-down-cancel - (mf/use-fn - (mf/deps on-cancel) - (fn [e] - (when (or (k/enter? e) (k/space? e)) - (on-cancel e)))) - - on-submit - (mf/use-fn - (mf/deps validate-token token tokens token-type) - (fn [form _event] - (let [name (get-in @form [:clean-data :name]) - description (get-in @form [:clean-data :description]) - value (get-in @form [:clean-data :value])] - (->> (validate-token {:token-value value - :token-name name - :token-description description - :prev-token token - :tokens tokens}) - (rx/subs! - (fn [valid-token] - (st/emit! - (if is-create - (dwtl/create-token (ctob/make-token {:name name - :type token-type - :value (:value valid-token) - :description description})) - - (dwtl/update-token (:id token) - {:name name - :value (:value valid-token) - :description description})) - (dwtp/propagate-workspace-tokens) - (modal/hide))))))))] - - [:> fc/form* {:class (stl/css :form-wrapper) - :form form - :on-submit on-submit} - [:div {:class (stl/css :token-rows)} - - [:> heading* {:level 2 :typography "headline-medium" :class (stl/css :form-modal-title)} - (tr "workspace.tokens.create-token" token-type)] - - [:div {:class (stl/css :input-row)} - [:> fc/form-input* {:id "token-name" - :name :name - :label (tr "workspace.tokens.token-name") - :placeholder (tr "workspace.tokens.enter-token-name" token-title) - :max-length max-input-length - :variant "comfortable" - :auto-focus true}] - - (when (and warning-name-change? (= action "edit")) - [:div {:class (stl/css :warning-name-change-notification-wrapper)} - [:> context-notification* - {:level :warning :appearance :ghost} (tr "workspace.tokens.warning-name-change")]])] - - [:div {:class (stl/css :input-row)} - [:> form-input-token* - {:placeholder (tr "workspace.tokens.text-case-value-enter") - :label (tr "workspace.tokens.token-value") - :name :value - :token token - :tokens tokens}]] - - [:div {:class (stl/css :input-row)} - [:> fc/form-input* {:id "token-description" - :name :description - :label (tr "workspace.tokens.token-description") - :placeholder (tr "workspace.tokens.token-description") - :max-length max-input-length - :variant "comfortable" - :is-optional true}]] - - [:div {:class (stl/css-case :button-row true - :with-delete (= action "edit"))} - (when (= action "edit") - [:> button* {:on-click on-delete-token - :on-key-down handle-key-down-delete - :class (stl/css :delete-btn) - :type "button" - :icon i/delete - :variant "secondary"} - (tr "labels.delete")]) - - [:> button* {:on-click on-cancel - :on-key-down handle-key-down-cancel - :type "button" - :id "token-modal-cancel" - :variant "secondary"} - (tr "labels.cancel")] - - [:> fc/form-submit* {:variant "primary" - :on-submit on-submit} - (tr "labels.save")]]]])) diff --git a/frontend/src/app/main/ui/workspace/tokens/management/create/text_case.scss b/frontend/src/app/main/ui/workspace/tokens/management/create/text_case.scss deleted file mode 100644 index 6593b3c30a..0000000000 --- a/frontend/src/app/main/ui/workspace/tokens/management/create/text_case.scss +++ /dev/null @@ -1,58 +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 - -@use "ds/typography.scss" as t; -@use "ds/_sizes.scss" as *; -@use "ds/_borders.scss" as *; - -.form-wrapper { - width: $sz-384; - position: relative; -} - -.token-rows { - display: flex; - flex-direction: column; - gap: var(--sp-l); -} - -.input-row { - display: flex; - flex-direction: column; - gap: var(--sp-xs); -} - -.title-bar { - display: grid; - grid-template-columns: 1fr auto; -} - -.form-modal-title { - @include t.use-typography("headline-medium"); - color: var(--color-foreground-primary); - display: flex; - align-items: center; -} - -.button-row { - display: grid; - grid-template-columns: auto auto; - justify-content: end; - gap: var(--sp-m); - padding-block-start: var(--sp-s); -} - -.with-delete { - grid-template-columns: 1fr auto auto; -} - -.warning-name-change-notification-wrapper { - margin-block-start: var(--sp-l); -} - -.delete-btn { - justify-self: start; -} diff --git a/frontend/src/app/main/ui/workspace/tokens/management/create/typography.cljs b/frontend/src/app/main/ui/workspace/tokens/management/create/typography.cljs deleted file mode 100644 index ce39829c71..0000000000 --- a/frontend/src/app/main/ui/workspace/tokens/management/create/typography.cljs +++ /dev/null @@ -1,427 +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.main.ui.workspace.tokens.management.create.typography - (:require-macros [app.main.style :as stl]) - (:require - [app.common.data :as d] - [app.common.files.tokens :as cft] - [app.common.schema :as sm] - [app.common.types.token :as cto] - [app.common.types.tokens-lib :as ctob] - [app.main.constants :refer [max-input-length]] - [app.main.data.modal :as modal] - [app.main.data.workspace.tokens.application :as dwta] - [app.main.data.workspace.tokens.library-edit :as dwtl] - [app.main.data.workspace.tokens.propagation :as dwtp] - [app.main.refs :as refs] - [app.main.store :as st] - [app.main.ui.components.radio-buttons :refer [radio-button radio-buttons]] - [app.main.ui.ds.buttons.button :refer [button*]] - [app.main.ui.ds.foundations.assets.icon :as i] - [app.main.ui.ds.foundations.typography.heading :refer [heading*]] - [app.main.ui.ds.notifications.context-notification :refer [context-notification*]] - [app.main.ui.forms :as forms] - [app.main.ui.workspace.tokens.management.create.combobox-token-fonts :refer [font-picker-composite-combobox*]] - [app.main.ui.workspace.tokens.management.create.form-input-token :refer [token-composite-value-input*]] - [app.util.dom :as dom] - [app.util.forms :as fm] - [app.util.i18n :refer [tr]] - [app.util.keyboard :as k] - [beicon.v2.core :as rx] - [cuerdas.core :as str] - [rumext.v2 :as mf])) - -(mf/defc composite-form* - [{:keys [token tokens] :as props}] - (let [letter-spacing-sub-token - (mf/with-memo [token] - (if-let [value (get token :value)] - {:type :letter-spacing - :value (cto/join-font-family (get value :letter-spacing))} - {:type :letter-spacing})) - - font-family-sub-token - (mf/with-memo [token] - (if-let [value (get token :value)] - {:type :font-family - :value (get value :font-family)} - {:type :font-family})) - - font-size-sub-token - (mf/with-memo [token] - (if-let [value (get token :value)] - {:type :font-size - :value (get value :font-size)} - {:type :font-size})) - - font-weight-sub-token - (mf/with-memo [token] - (if-let [value (get token :value)] - {:type :font-weight - :value (get value :font-weight)} - {:type :font-weight})) - - ;; TODO: Review this type - line-height-sub-token - (mf/with-memo [token] - (if-let [value (get token :value)] - {:type :number - :value (get value :line-height)} - {:type :number})) - - text-case-sub-token - (mf/with-memo [token] - (if-let [value (get token :value)] - {:type :text-case - :value (get value :text-case)} - {:type :text-case})) - - text-decoration-sub-token - (mf/with-memo [token] - (if-let [value (get token :value)] - {:type :text-decoration - :value (get value :text-decoration)} - {:type :text-decoration}))] - - [:* - [:div {:class (stl/css :input-row)} - [:> font-picker-composite-combobox* - {:icon i/text-font-family - :placeholder (tr "workspace.tokens.token-font-family-value-enter") - :aria-label (tr "workspace.tokens.token-font-family-value") - :name :font-family - :token font-family-sub-token - :tokens tokens}]] - [:div {:class (stl/css :input-row)} - [:> token-composite-value-input* - {:aria-label "Font Size" - :icon i/text-font-size - :placeholder (tr "workspace.tokens.font-size-value-enter") - :name :font-size - :token font-size-sub-token - :tokens tokens}]] - [:div {:class (stl/css :input-row)} - [:> token-composite-value-input* - {:aria-label "Font Weight" - :icon i/text-font-weight - :placeholder (tr "workspace.tokens.font-weight-value-enter") - :name :font-weight - :token font-weight-sub-token - :tokens tokens}]] - [:div {:class (stl/css :input-row)} - [:> token-composite-value-input* - {:aria-label "Line Height" - :icon i/text-lineheight - :placeholder (tr "workspace.tokens.line-height-value-enter") - :name :line-height - :token line-height-sub-token - :tokens tokens}]] - [:div {:class (stl/css :input-row)} - [:> token-composite-value-input* - {:aria-label "Letter Spacing" - :icon i/text-letterspacing - :placeholder (tr "workspace.tokens.letter-spacing-value-enter-composite") - :name :letter-spacing - :token letter-spacing-sub-token - :tokens tokens}]] - [:div {:class (stl/css :input-row)} - [:> token-composite-value-input* - {:aria-label "Text Case" - :icon i/text-mixed - :placeholder (tr "workspace.tokens.text-case-value-enter") - :name :text-case - :token text-case-sub-token - :tokens tokens}]] - [:div {:class (stl/css :input-row)} - [:> token-composite-value-input* - {:aria-label "Text Decoration" - :icon i/text-underlined - :placeholder (tr "workspace.tokens.text-decoration-value-enter") - :name :text-decoration - :token text-decoration-sub-token - :tokens tokens}]]])) - -(mf/defc reference-form* - [{:keys [token tokens] :as props}] - [:div {:class (stl/css :input-row)} - [:> token-composite-value-input* - {:placeholder (tr "workspace.tokens.reference-composite") - :aria-label (tr "labels.reference") - :icon i/text-typography - :name :reference - :token token - :tokens tokens}]]) - -(defn- make-schema - [tokens-tree active-tab] - (sm/schema - [:and - [:map - [:name - [:and - [:string {:min 1 :max 255 - :error/fn #(str (:value %) (tr "workspace.tokens.token-name-length-validation-error"))}] - (sm/update-properties cto/token-name-ref assoc - :error/fn #(str (:value %) (tr "workspace.tokens.token-name-validation-error"))) - [:fn {:error/fn #(tr "workspace.tokens.token-name-duplication-validation-error" (:value %))} - #(not (cft/token-name-path-exists? % tokens-tree))]]] - - [:value - [:map - [:font-family {:optional true} [:maybe :string]] - [:font-size {:optional true} [:maybe :string]] - [:font-weight {:optional true} [:maybe :string]] - [:line-height {:optional true} [:maybe :string]] - [:letter-spacing {:optional true} [:maybe :string]] - [:text-case {:optional true} [:maybe :string]] - [:text-decoration {:optional true} [:maybe :string]] - (if (= active-tab :reference) - [:reference {:optional false} ::sm/text] - [:reference {:optional true} [:maybe :string]])]] - - [:description {:optional true} - [:string {:max 2048 :error/fn #(tr "errors.field-max-length" 2048)}]]] - - [:fn {:error/field [:value :reference] - :error/fn #(tr "workspace.tokens.self-reference")} - (fn [{:keys [name value]}] - (let [reference (get value :reference)] - (if (and reference name) - (not (cto/token-value-self-reference? name reference)) - true)))] - - [:fn {:error/field [:value :line-height] - :error/fn #(tr "workspace.tokens.composite-line-height-needs-font-size")} - (fn [{:keys [value]}] - (let [line-heigh (get value :line-height) - font-size (get value :font-size)] - (if (and line-heigh (not font-size)) - false - true)))] - - ;; This error does not shown on interface, it's just to avoid saving empty composite tokens - ;; We don't need to translate it. - [:fn {:error/fn (fn [_] "At least one composite field must be set") - :error/field :value} - (fn [attrs] - (let [result (reduce-kv (fn [_ _ v] - (if (str/empty? v) - false - (reduced true))) - false - (get attrs :value))] - result))]])) - -(mf/defc form* - [{:keys [token validate-token action is-create selected-token-set-id tokens-tree-in-selected-set] :as props}] - - (let [token - (mf/with-memo [token] - (or token {:type :typography})) - - active-tab* (mf/use-state #(if (cft/is-reference? token) :reference :composite)) - active-tab (deref active-tab*) - - token-type - (get token :type) - - token-properties - (dwta/get-token-properties token) - - token-title (str/lower (:title token-properties)) - - tokens - (mf/deref refs/workspace-active-theme-sets-tokens) - - tokens - (mf/with-memo [tokens token] - ;; Ensure that the resolved value uses the currently editing token - ;; even if the name has been overriden by a token with the same name - ;; in another set below. - (cond-> tokens - (and (:name token) (:value token)) - (assoc (:name token) token))) - - schema - (mf/with-memo [tokens-tree-in-selected-set active-tab] - (make-schema tokens-tree-in-selected-set active-tab)) - - initial - (mf/with-memo [token] - (let [value (:value token) - processed-value - (cond - (string? value) - {:reference value} - - (map? value) - (let [value (cond-> value - (:font-family value) - (update :font-family cto/join-font-family))] - (select-keys value - [:font-family - :font-size - :font-weight - :line-height - :letter-spacing - :text-case - :text-decoration])) - :else - {})] - - {:name (:name token "") - :value processed-value - :description (:description token "")})) - - form - (fm/use-form :schema schema - :initial initial) - - warning-name-change? - (not= (get-in @form [:data :name]) - (:name initial)) - - on-toggle-tab - (mf/use-fn - (mf/deps) - (fn [new-tab] - (let [new-tab (keyword new-tab)] - (reset! active-tab* new-tab)))) - - on-cancel - (mf/use-fn - (fn [e] - (dom/prevent-default e) - (modal/hide!))) - - on-delete-token - (mf/use-fn - (mf/deps selected-token-set-id token) - (fn [e] - (dom/prevent-default e) - (modal/hide!) - (st/emit! (dwtl/delete-token selected-token-set-id (:id token))))) - - handle-key-down-delete - (mf/use-fn - (mf/deps on-delete-token) - (fn [e] - (when (or (k/enter? e) (k/space? e)) - (on-delete-token e)))) - - handle-key-down-cancel - (mf/use-fn - (mf/deps on-cancel) - (fn [e] - (when (or (k/enter? e) (k/space? e)) - (on-cancel e)))) - - on-submit - (mf/use-fn - (mf/deps validate-token token tokens token-type) - (fn [form _event] - (let [name (get-in @form [:clean-data :name]) - description (get-in @form [:clean-data :description]) - value (get-in @form [:clean-data :value])] - - (->> (validate-token {:token-value (if (contains? value :reference) - (get value :reference) - value) - :token-name name - :token-description description - :prev-token token - :tokens tokens}) - (rx/subs! - (fn [valid-token] - (st/emit! - (if is-create - (dwtl/create-token (ctob/make-token {:name name - :type token-type - :value (:value valid-token) - :description description})) - - (dwtl/update-token (:id token) - {:name name - :value (:value valid-token) - :description description})) - (dwtp/propagate-workspace-tokens) - (modal/hide))))))))] - - [:> forms/form* {:class (stl/css :form-wrapper) - :form form - :on-submit on-submit} - [:div {:class (stl/css :token-rows)} - - [:> heading* {:level 2 :typography "headline-medium" :class (stl/css :form-modal-title)} - (tr "workspace.tokens.create-token" token-type)] - - [:div {:class (stl/css :input-row)} - [:> forms/form-input* {:id "token-name" - :name :name - :label (tr "workspace.tokens.token-name") - :placeholder (tr "workspace.tokens.enter-token-name" token-title) - :max-length max-input-length - :variant "comfortable" - :auto-focus true}] - - (when (and warning-name-change? (= action "edit")) - [:div {:class (stl/css :warning-name-change-notification-wrapper)} - [:> context-notification* - {:level :warning :appearance :ghost} (tr "workspace.tokens.warning-name-change")]])] - - [:div {:class (stl/css :title-bar)} - [:div {:class (stl/css :title)} (tr "labels.typography")] - [:& radio-buttons {:class (stl/css :listing-options) - :selected (d/name active-tab) - :on-change on-toggle-tab - :name "reference-composite-tab"} - [:& radio-button {:icon i/layers - :value "composite" - :title (tr "workspace.tokens.individual-tokens") - :id "composite-opt"}] - [:& radio-button {:icon i/tokens - :value "reference" - :title (tr "workspace.tokens.use-reference") - :id "reference-opt"}]]] - [:div {:class (stl/css :inputs-wrapper)} - (if (= active-tab :composite) - [:> composite-form* {:token token - :tokens tokens}] - - [:> reference-form* {:token token - :tokens tokens}])] - - [:div {:class (stl/css :input-row)} - [:> forms/form-input* {:id "token-description" - :name :description - :label (tr "workspace.tokens.token-description") - :placeholder (tr "workspace.tokens.token-description") - :max-length max-input-length - :variant "comfortable" - :is-optional true}]] - - [:div {:class (stl/css-case :button-row true - :with-delete (= action "edit"))} - (when (= action "edit") - [:> button* {:on-click on-delete-token - :on-key-down handle-key-down-delete - :class (stl/css :delete-btn) - :type "button" - :icon i/delete - :variant "secondary"} - (tr "labels.delete")]) - - [:> button* {:on-click on-cancel - :on-key-down handle-key-down-cancel - :type "button" - :id "token-modal-cancel" - :variant "secondary"} - (tr "labels.cancel")] - - [:> forms/form-submit* {:variant "primary" - :on-submit on-submit} - (tr "labels.save")]]]])) diff --git a/frontend/src/app/main/ui/workspace/tokens/management/forms/color.cljs b/frontend/src/app/main/ui/workspace/tokens/management/forms/color.cljs new file mode 100644 index 0000000000..ee1de768ec --- /dev/null +++ b/frontend/src/app/main/ui/workspace/tokens/management/forms/color.cljs @@ -0,0 +1,53 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC + +(ns app.main.ui.workspace.tokens.management.forms.color + (:require + [app.common.files.tokens :as cft] + [app.common.schema :as sm] + [app.common.types.token :as cto] + [app.main.ui.workspace.tokens.management.forms.controls :as token.controls] + [app.main.ui.workspace.tokens.management.forms.generic-form :as generic] + [app.util.i18n :refer [tr]] + [cuerdas.core :as str] + [rumext.v2 :as mf])) + +(defn- token-value-error-fn + [{:keys [value]}] + (when (or (str/empty? value) + (str/blank? value)) + (tr "workspace.tokens.empty-input"))) + +(defn- make-schema + [tokens-tree _] + (sm/schema + [:and + [:map + [:name + [:and + [:string {:min 1 :max 255 :error/fn #(str (:value %) (tr "workspace.tokens.token-name-length-validation-error"))}] + (sm/update-properties cto/token-name-ref assoc :error/fn #(str (:value %) (tr "workspace.tokens.token-name-validation-error"))) + [:fn {:error/fn #(tr "workspace.tokens.token-name-duplication-validation-error" (:value %))} + #(not (cft/token-name-path-exists? % tokens-tree))]]] + + [:value [::sm/text {:error/fn token-value-error-fn}]] + + [:color-result {:optional true} ::sm/any] + + [:description {:optional true} + [:string {:max 2048 :error/fn #(tr "errors.field-max-length" 2048)}]]] + + [:fn {:error/field :value + :error/fn #(tr "workspace.tokens.self-reference")} + (fn [{:keys [name value]}] + (when (and name value) + (nil? (cto/token-value-self-reference? name value))))]])) + +(mf/defc form* + [props] + (let [props (mf/spread-props props {:make-schema make-schema + :input-component token.controls/color-input*})] + [:> generic/form* props])) diff --git a/frontend/src/app/main/ui/workspace/tokens/management/forms/controls.cljs b/frontend/src/app/main/ui/workspace/tokens/management/forms/controls.cljs new file mode 100644 index 0000000000..70feee56ca --- /dev/null +++ b/frontend/src/app/main/ui/workspace/tokens/management/forms/controls.cljs @@ -0,0 +1,19 @@ +(ns app.main.ui.workspace.tokens.management.forms.controls + (:require + [app.common.data.macros :as dm] + [app.main.ui.workspace.tokens.management.forms.controls.color-input :as color] + [app.main.ui.workspace.tokens.management.forms.controls.fonts-combobox :as fonts] + [app.main.ui.workspace.tokens.management.forms.controls.input :as input] + [app.main.ui.workspace.tokens.management.forms.controls.select :as select])) + +(dm/export color/color-input*) +(dm/export color/indexed-color-input*) + +(dm/export input/input*) +(dm/export input/input-indexed*) +(dm/export input/input-composite*) + +(dm/export fonts/fonts-combobox*) +(dm/export fonts/composite-fonts-combobox*) + +(dm/export select/select-indexed*) \ No newline at end of file diff --git a/frontend/src/app/main/ui/workspace/tokens/management/forms/controls/color_input.cljs b/frontend/src/app/main/ui/workspace/tokens/management/forms/controls/color_input.cljs new file mode 100644 index 0000000000..654429dd2a --- /dev/null +++ b/frontend/src/app/main/ui/workspace/tokens/management/forms/controls/color_input.cljs @@ -0,0 +1,459 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC + +(ns app.main.ui.workspace.tokens.management.forms.controls.color-input + (:require-macros [app.main.style :as stl]) + (:require + [app.common.colors :as color] + [app.common.data :as d] + [app.common.data.macros :as dm] + [app.common.types.color :as cl] + [app.common.types.tokens-lib :as ctob] + [app.main.data.style-dictionary :as sd] + [app.main.data.tinycolor :as tinycolor] + [app.main.data.workspace.tokens.format :as dwtf] + [app.main.refs :as refs] + [app.main.ui.ds.controls.input :as ds] + [app.main.ui.ds.utilities.swatch :refer [swatch*]] + [app.main.ui.forms :as fc] + [app.main.ui.workspace.colorpicker :as colorpicker] + [app.main.ui.workspace.colorpicker.ramp :refer [ramp-selector*]] + [app.util.dom :as dom] + [app.util.forms :as fm] + [app.util.i18n :refer [tr]] + [beicon.v2.core :as rx] + [cuerdas.core :as str] + [rumext.v2 :as mf])) + +;; --- Color Inputs ------------------------------------------------------------- +;; +;; The color input exists in two variants: the normal (primitive) input and the +;; indexed input. Both support hex, rgb(), hsl(), named colors, opacity changes, +;; color-picker selection, and token references. +;; +;; 1) Normal Color Input (primitive) +;; - Used when the token’s `:value` *is a single color* or a reference. +;; - Writes directly to `:value`. +;; - Validation ensures the color is in an accepted format or is a valid +;; color token reference. + +;; 2) Indexed Color Input +;; - Used when the token’s value stores an array of items (e.g. inside +;; shadows, where each shadow layer has its own :color field). +;; - The input writes to a nested subfield: +;; [:value :color] +;; - Only that specific color entry is validated. +;; - Other properties (offsets, blur, inset, etc.) remain untouched. +;; +;; Both variants provide identical color-picker and text-input behavior, but +;; differ in how they persist the value within the form’s nested structure. + + +(defn- resolve-value + [tokens prev-token value] + (let [token + {:value value + :name "__PENPOT__TOKEN__NAME__PLACEHOLDER__"} + + tokens + (-> tokens + ;; Remove previous token when renaming a token + (dissoc (:name prev-token)) + (update (:name token) #(ctob/make-token (merge % prev-token token))))] + + (->> tokens + (sd/resolve-tokens-interactive) + (rx/mapcat + (fn [resolved-tokens] + (let [{:keys [errors resolved-value] :as resolved-token} (get resolved-tokens (:name token))] + (if resolved-value + (rx/of {:value resolved-value}) + (rx/of {:error (first errors)})))))))) + +(defn- hex->color-obj + [hex] + (when-let [tc (tinycolor/valid-color hex)] + (let [hex (tinycolor/->hex-string tc) + alpha (tinycolor/alpha tc) + [r g b] (cl/hex->rgb hex) + [h s v] (cl/hex->hsv hex)] + {:hex hex + :r r :g g :b b + :h h :s s :v v + :alpha alpha}))) + +(mf/defc ramp* + [{:keys [color on-change]}] + (let [wrapper-node-ref (mf/use-ref nil) + dragging-ref (mf/use-ref false) + + on-start-drag + (mf/use-fn #(mf/set-ref-val! dragging-ref true)) + + on-finish-drag + (mf/use-fn #(mf/set-ref-val! dragging-ref false)) + + internal-color* + (mf/use-state #(hex->color-obj color)) + + internal-color + (deref internal-color*) + + on-change' + (mf/use-fn + (mf/deps on-change) + (fn [{:keys [hex alpha] :as selector-color}] + (let [dragging? (mf/ref-val dragging-ref)] + (when-not (and dragging? hex) + (reset! internal-color* selector-color) + (on-change hex alpha)))))] + (mf/use-effect + (mf/deps color) + (fn [] + ;; Update internal color when user changes input value + (when-let [color (tinycolor/valid-color color)] + (when-not (= (tinycolor/->hex-string color) (:hex internal-color)) + (reset! internal-color* (hex->color-obj color)))))) + + (colorpicker/use-color-picker-css-variables! wrapper-node-ref internal-color) + [:div {:ref wrapper-node-ref} + [:> ramp-selector* + {:color internal-color + :on-start-drag on-start-drag + :on-finish-drag on-finish-drag + :on-change on-change'}]])) + +(mf/defc color-input* + [{:keys [name tokens token] :rest props}] + + (let [form (mf/use-ctx fc/context) + input-name name + + + touched? + (and (contains? (:data @form) input-name) + (get-in @form [:touched input-name])) + + error + (get-in @form [:errors input-name]) + + value + (get-in @form [:data input-name] "") + + color-resolved + (get-in @form [:data :color-result] "") + + + valid-color (or (tinycolor/valid-color value) + (tinycolor/valid-color color-resolved)) + + profile (mf/deref refs/profile) + + default-bullet-color + (case (:theme profile) + "light" + color/background-quaternary-light + color/background-quaternary) + hex + (if valid-color + (tinycolor/->hex-string (tinycolor/valid-color valid-color)) + default-bullet-color) + + alpha + (if (tinycolor/valid-color valid-color) + (tinycolor/alpha (tinycolor/valid-color valid-color)) + 1) + + resolve-stream + (mf/with-memo [token] + (if-let [value (:value token)] + (rx/behavior-subject value) + (rx/subject))) + + hint* + (mf/use-state {}) + + hint + (deref hint*) + + color-ramp-open* (mf/use-state false) + color-ramp-open? (deref color-ramp-open*) + + on-click-swatch + (mf/use-fn + (mf/deps color-ramp-open?) + (fn [] + (let [open? (not color-ramp-open?)] + (reset! color-ramp-open* open?)))) + + swatch + (mf/html + [:> swatch* + {:background {:color hex :opacity alpha} + :show-tooltip false + :data-testid "token-form-color-bullet" + :class (stl/css :slot-start) + :on-click on-click-swatch}]) + + on-change-value + (mf/use-fn + (mf/deps resolve-stream input-name value) + (fn [hex alpha] + (let [;; StyleDictionary will always convert to hex/rgba, so we take the format from the value input field + prev-input-color (some-> value + (tinycolor/valid-color)) + ;; If the input is a reference we will take the format from the computed value + prev-computed-color (when-not prev-input-color + (some-> value (tinycolor/valid-color))) + prev-format (some-> (or prev-input-color prev-computed-color) + (tinycolor/color-format)) + to-rgba? (and + (< alpha 1) + (or (= prev-format "hex") (not prev-format))) + to-hex? (and (not prev-format) (= alpha 1)) + format (cond + to-rgba? "rgba" + to-hex? "hex" + prev-format prev-format + :else "hex") + color-value (-> (tinycolor/valid-color hex) + (tinycolor/set-alpha (or alpha 1)) + (tinycolor/->string format))] + (when (not= value color-value) + (fm/on-input-change form input-name color-value true) + (rx/push! resolve-stream color-value))))) + + on-change + (mf/use-fn + (mf/deps resolve-stream input-name) + (fn [event] + (let [raw-value (-> event dom/get-target dom/get-input-value) + value (if (tinycolor/hex-without-hash-prefix? raw-value) + (dm/str "#" raw-value) + raw-value)] + (fm/on-input-change form input-name value true) + (rx/push! resolve-stream value)))) + + props + (mf/spread-props props {:on-change on-change + ;; TODO: Review this value vs default-value + :value (or value "") + :hint-message (:message hint) + :variant "comfortable" + :slot-start swatch + :hint-type (:type hint)}) + + props + (if (and error touched?) + (mf/spread-props props {:hint-type "error" + :hint-message (:message error)}) + props)] + + (mf/with-effect [resolve-stream tokens token input-name] + (let [subs (->> resolve-stream + (rx/debounce 300) + (rx/mapcat (partial resolve-value tokens token)) + (rx/map (fn [result] + (d/update-when result :error + (fn [error] + ((:error/fn error) (:error/value error)))))) + (rx/subs! (fn [{:keys [error value]}] + (let [touched? (get-in @form [:touched input-name])] + (when touched? + (if error + (do + (swap! form assoc-in [:extra-errors input-name] {:message error}) + (swap! form assoc-in [:data :color-result] "") + (reset! hint* {:message error :type "error"})) + (let [message (tr "workspace.tokens.resolved-value" (dwtf/format-token-value value))] + (swap! form update :extra-errors dissoc input-name) + (swap! form assoc-in [:data :color-result] value) + (reset! hint* {:message message :type "hint"}))))))))] + + (fn [] + (rx/dispose! subs)))) + + [:* + [:> ds/input* props] + + (when color-ramp-open? + [:> ramp* {:color value :on-change on-change-value}])])) + +(defn- on-indexed-input-change + ([form field index value value-subfield] + (on-indexed-input-change form field index value value-subfield false)) + ([form field index value value-subfield trim?] + (letfn [(clean-errors [errors] + (-> errors + (dissoc field) + (not-empty)))] + (swap! form (fn [state] + (-> state + (assoc-in [:data :value value-subfield index field] (if trim? (str/trim value) value)) + (update :errors clean-errors) + (update :extra-errors clean-errors))))))) + +(mf/defc indexed-color-input* + [{:keys [name tokens token index value-subfield] :rest props}] + + (let [form (mf/use-ctx fc/context) + input-name name + + error + (get-in @form [:errors :value value-subfield index input-name]) + + value + (get-in @form [:data :value value-subfield index input-name] "") + + color-resolved + (get-in @form [:data :value value-subfield index :color-result] "") + + valid-color (or (tinycolor/valid-color value) + (tinycolor/valid-color color-resolved)) + profile (mf/deref refs/profile) + + default-bullet-color + (case (:theme profile) + "light" + color/background-quaternary-light + color/background-quaternary) + + hex + (if valid-color + (tinycolor/->hex-string (tinycolor/valid-color valid-color)) + default-bullet-color) + + alpha + (if (tinycolor/valid-color valid-color) + (tinycolor/alpha (tinycolor/valid-color valid-color)) + 1) + + resolve-stream + (mf/with-memo [token] + (if-let [value (get-in token [:value value-subfield index input-name])] + (rx/behavior-subject value) + (rx/subject))) + + hint* + (mf/use-state {}) + + hint + (deref hint*) + + color-ramp-open* (mf/use-state false) + color-ramp-open? (deref color-ramp-open*) + + on-click-swatch + (mf/use-fn + (mf/deps color-ramp-open?) + (fn [] + (let [open? (not color-ramp-open?)] + (reset! color-ramp-open* open?)))) + + swatch + (mf/html + [:> swatch* + {:background {:color hex :opacity alpha} + :show-tooltip false + :data-testid "token-form-color-bullet" + :class (stl/css :slot-start) + :on-click on-click-swatch}]) + + on-change-value + (mf/use-fn + (mf/deps resolve-stream input-name value index) + (fn [hex alpha] + (let [;; StyleDictionary will always convert to hex/rgba, so we take the format from the value input field + prev-input-color (some-> value + (tinycolor/valid-color)) + ;; If the input is a reference we will take the format from the computed value + prev-computed-color (when-not prev-input-color + (some-> value (tinycolor/valid-color))) + prev-format (some-> (or prev-input-color prev-computed-color) + (tinycolor/color-format)) + to-rgba? (and + (< alpha 1) + (or (= prev-format "hex") (not prev-format))) + to-hex? (and (not prev-format) (= alpha 1)) + format (cond + to-rgba? "rgba" + to-hex? "hex" + prev-format prev-format + :else "hex") + color-value (-> (tinycolor/valid-color hex) + (tinycolor/set-alpha (or alpha 1)) + (tinycolor/->string format))] + (when (not= value color-value) + (on-indexed-input-change form input-name index color-value value-subfield true) + (rx/push! resolve-stream color-value))))) + + on-change + (mf/use-fn + (mf/deps resolve-stream input-name index) + (fn [event] + (let [raw-value (-> event dom/get-target dom/get-input-value) + value (if (tinycolor/hex-without-hash-prefix? raw-value) + (dm/str "#" raw-value) + raw-value)] + (on-indexed-input-change form input-name index value value-subfield true) + (rx/push! resolve-stream value)))) + + props + (mf/spread-props props {:on-change on-change + :value (or value "") + :hint-message (:message hint) + :slot-start swatch + :hint-type (:type hint)}) + + props + (if error + (mf/spread-props props {:hint-type "error" + :hint-message (:message error)}) + props)] + + (mf/with-effect [resolve-stream tokens token input-name index value-subfield] + (let [subs (->> resolve-stream + (rx/debounce 300) + (rx/mapcat (partial resolve-value tokens token)) + (rx/map (fn [result] + (d/update-when result :error + (fn [error] + (assoc error :message ((:error/fn error) (:error/value error))))))) + + (rx/subs! + (fn [{:keys [error value]}] + (cond + (and error (str/empty? (:error/value error))) + (do + (swap! form update-in [:errors :value value-subfield index] dissoc input-name) + (swap! form update-in [:data :value value-subfield index] dissoc input-name) + (swap! form assoc-in [:data :value value-subfield index :color-result] "") + (swap! form update :extra-errors dissoc :value) + (reset! hint* {})) + + (some? error) + (let [error' (:message error)] + (swap! form assoc-in [:extra-errors :value value-subfield index input-name] {:message error'}) + (swap! form assoc-in [:data :value value-subfield index :color-result] "") + (reset! hint* {:message error' :type "error"})) + + :else + (let [message (tr "workspace.tokens.resolved-value" (dwtf/format-token-value value)) + input-value (get-in @form [:data :value value-subfield index input-name] "")] + (swap! form update :errors dissoc :value) + (swap! form update :extra-errors dissoc :value) + (swap! form assoc-in [:data :value value-subfield index :color-result] (dwtf/format-token-value value)) + (if (= input-value (str value)) + (reset! hint* {}) + (reset! hint* {:message message :type "hint"})))))))] + (fn [] + (rx/dispose! subs)))) + + [:* + [:> ds/input* props] + + (when color-ramp-open? + [:> ramp* {:color value :on-change on-change-value}])])) diff --git a/frontend/src/app/main/ui/workspace/tokens/management/create/combobox_token_fonts.cljs b/frontend/src/app/main/ui/workspace/tokens/management/forms/controls/fonts_combobox.cljs similarity index 88% rename from frontend/src/app/main/ui/workspace/tokens/management/create/combobox_token_fonts.cljs rename to frontend/src/app/main/ui/workspace/tokens/management/forms/controls/fonts_combobox.cljs index 712b244ed3..420c2c1dd6 100644 --- a/frontend/src/app/main/ui/workspace/tokens/management/create/combobox_token_fonts.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/management/forms/controls/fonts_combobox.cljs @@ -4,7 +4,7 @@ ;; ;; Copyright (c) KALEIDOS INC -(ns app.main.ui.workspace.tokens.management.create.combobox-token-fonts +(ns app.main.ui.workspace.tokens.management.forms.controls.fonts-combobox (:require-macros [app.main.style :as stl]) (:require [app.common.data :as d] @@ -24,6 +24,29 @@ [cuerdas.core :as str] [rumext.v2 :as mf])) +;; --- Font Picker Inputs ------------------------------------------------------- +;; +;; We provide two versions of the font picker: the normal (primitive) input and +;; the composite input. Both allow selecting a font-family either by typing free +;; text or by choosing from the list of fonts loaded in the application. Comma- +;; separated values are treated as literal font-family lists. +;; +;; 1) Normal Font Picker (primitive) +;; - Used when the token’s value *is itself* a font-family (string) or a +;; reference to another token. +;; - The input writes directly to `:value`. +;; - Validation ensures the string is a valid font-family or a valid token +;; reference. +;; +;; 2) Composite Font Picker +;; - Used inside typography tokens, where `:value` is a map (e.g. contains +;; :font-family, :font-weight, :letter-spacing, etc.). +;; - The input writes to the specific subfield `[:value :font-family]`. +;; - Only this field is validated and updated—other typography fields remain +;; untouched. +;; +;; Both modes share the same UI behaviour, but differ in how they store and +;; validate data within the form state. (defn- resolve-value [tokens prev-token value] @@ -46,7 +69,7 @@ (rx/of {:value resolved-value}) (rx/of {:error (first errors)})))))))) -(mf/defc font-picker-combobox* +(mf/defc fonts-combobox* [{:keys [token tokens name] :rest props}] (let [form (mf/use-ctx fc/context) input-name name @@ -117,7 +140,6 @@ props (mf/spread-props props {:on-change on-change - ;; TODO: Review this value vs default-value :value (or value "") :hint-message (:message hint) :slot-end font-selector-button @@ -174,7 +196,7 @@ (update :errors clean-errors) (update :extra-errors clean-errors))))))) -(mf/defc font-picker-composite-combobox* +(mf/defc composite-fonts-combobox* [{:keys [token tokens name] :rest props}] (let [form (mf/use-ctx fc/context) input-name name @@ -242,7 +264,6 @@ props (mf/spread-props props {:on-change on-change - ;; TODO: Review this value vs default-value :value (or value "") :hint-message (:message hint) :slot-end font-selector-button diff --git a/frontend/src/app/main/ui/workspace/tokens/management/create/combobox_token_fonts.scss b/frontend/src/app/main/ui/workspace/tokens/management/forms/controls/fonts_combobox.scss similarity index 100% rename from frontend/src/app/main/ui/workspace/tokens/management/create/combobox_token_fonts.scss rename to frontend/src/app/main/ui/workspace/tokens/management/forms/controls/fonts_combobox.scss diff --git a/frontend/src/app/main/ui/workspace/tokens/management/forms/controls/input.cljs b/frontend/src/app/main/ui/workspace/tokens/management/forms/controls/input.cljs new file mode 100644 index 0000000000..29a8145095 --- /dev/null +++ b/frontend/src/app/main/ui/workspace/tokens/management/forms/controls/input.cljs @@ -0,0 +1,430 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC + +(ns app.main.ui.workspace.tokens.management.forms.controls.input + (:require + [app.common.data :as d] + [app.common.types.tokens-lib :as ctob] + [app.main.data.style-dictionary :as sd] + [app.main.data.workspace.tokens.format :as dwtf] + [app.main.ui.ds.controls.input :as ds] + [app.main.ui.forms :as fc] + [app.util.dom :as dom] + [app.util.forms :as fm] + [app.util.i18n :refer [tr]] + [beicon.v2.core :as rx] + [cuerdas.core :as str] + [rumext.v2 :as mf])) + +;; ----------------------------------------------------------------------------- +;; WHY WE HAVE THREE INPUT TYPES: INPUT, COMPOSITE, AND INDEXED +;; +;; Our token editor supports multiple token categories (colors, distances, +;; shadows, typography, etc.). Each category stores its data inside the token’s +;; :value field, but not all tokens have the same internal structure. To keep the +;; UI consistent and predictable, we define three input architectures: +;; +;; 1) INPUTS +;; ---------------------------------------------------------- +;; Used for tokens where the entire :value is just a single, +;; atomic field. Examples: +;; - :distance → a number or a reference +;; - :text-case → one of {none, uppercase, lowercase, capitalize} +;; - :color → a single color value or a reference +;; +;; Characteristics: +;; * The input writes directly to :value. +;; * Validation logic is simple because :value is a single unit. +;; * Switching to "reference mode" simply replaces :value. +;; +;; Data shape example: +;; {:value "16px"} +;; {:value "{spacing.sm}"} +;; +;; +;; 2) COMPOSITE INPUTS +;; ---------------------------------------------------------- +;; Used when the token contains a set of *named fields* inside :value. +;; The UI must write into a specific subfield inside the :value map. +;; +;; Example: typography tokens +;; {:value {:font-family "Inter" +;; :font-weight 600 +;; :letter-spacing -0.5 +;; :line-height 1.4}} +;; +;; Why this type exists: +;; * Each input (font-family, weight, spacing, etc.) maps to a specific +;; key inside :value. +;; * The UI must update these fields individually without replacing the +;; entire value. +;; * Validation rules apply per-field. +;; +;; In practice: +;; - The component knows which subfield to update. +;; - The form accumulates multiple fields into a single map under :value. +;; +;; +;; 3) INDEXED INPUTS +;; ---------------------------------------------------------- +;; Used for tokens where the :value can be in TWO possible shapes: +;; A) A direct reference to another token +;; B) A list (array/vector) of maps describing multiple structured items +;; +;; Main example: shadow tokens +;; +;; ;; Option A — reference mode +;; {:value {:reference "{shadow.soft}"}} +;; +;; ;; Option B — full definition mode +;; {:value {:shadow +;; [{:color "#0003" +;; :offset-x 4 +;; :offset-y 6 +;; :blur 12 +;; :spread 0 +;; :inset? false} +;; +;; {:color "rgba(0,0,0,0.1)" +;; :offset-x 0 +;; :offset-y 1 +;; :blur 3 +;; :spread 0 +;; :inset? false}]}} +;; +;; Why this type exists: +;; * The UI must handle multiple items (indexed layers). +;; * Each layer has its own internal fields (color, offsets, blur, etc.). +;; * The user can add/remove layers dynamically. +;; * Reference mode must disable/remove the structured mode. +;; * Both shapes are valid, but they cannot coexist at the same time. +;; +;; Indexed inputs therefore need: +;; * A tab system ("Shadow" vs "Reference") +;; * Clear logic to ensure :shadow XOR :reference +;; * Repetition UI (add/remove layer groups) +;; +;; +;; SUMMARY +;; ----------------------------------------------------------------------------- +;; - Plain inputs operate on :value itself. +;; - Composite inputs operate on a map stored under :value, writing into +;; predefined keys. +;; - Indexed inputs operate on either: +;; • a reference stored in :value :reference, or +;; • an array of structured items stored in :value :shadow (or similar), +;; but never both at the same time. +;; +;; This 3-tiered input system keeps the editor flexible, predictable, +;; and compatible with the many different token types supported by the app. +;; ----------------------------------------------------------------------------- + +;; +;; Summary: +;; ------- +;; - `input*` → single flat value tokens +;; - `input-composite*` → structured tokens with multiple named fields +;; (e.g. typography tokens with font-family, +;; font-weight, letter-spacing, line-height…) +;; - `input-indexed*` → array-based tokens where each entry is a map +;; (e.g. multiple shadow layers) +;; +;; This separation ensures each form input mirrors the actual structure of the +;; token data model, keeping validation and updates correctly scoped. +;; ----------------------------------------------------------------------------- + + +(defn- resolve-value + [tokens prev-token value] + (let [token + {:value value + :name "__PENPOT__TOKEN__NAME__PLACEHOLDER__"} + tokens + (-> tokens + ;; Remove previous token when renaming a token + (dissoc (:name prev-token)) + (update (:name token) #(ctob/make-token (merge % prev-token token))))] + + (->> tokens + (sd/resolve-tokens-interactive) + (rx/mapcat + (fn [resolved-tokens] + (let [{:keys [errors resolved-value] :as resolved-token} (get resolved-tokens (:name token))] + (if resolved-value + (rx/of {:value resolved-value}) + (rx/of {:error (first errors)})))))))) + +(mf/defc input* + [{:keys [name tokens token] :rest props}] + + (let [form (mf/use-ctx fc/context) + input-name name + + touched? + (and (contains? (:data @form) input-name) + (get-in @form [:touched input-name])) + + error + (get-in @form [:errors input-name]) + + value + (get-in @form [:data input-name] "") + + resolve-stream + (mf/with-memo [token] + (if (contains? token :value) + (rx/behavior-subject (:value token)) + (rx/subject))) + + hint* + (mf/use-state {}) + + hint + (deref hint*) + + on-change + (mf/use-fn + (mf/deps resolve-stream input-name) + (fn [event] + (let [value (-> event dom/get-target dom/get-input-value)] + (fm/on-input-change form input-name value true) + (rx/push! resolve-stream value)))) + + props + (mf/spread-props props {:on-change on-change + :default-value value + :variant "comfortable" + :hint-message (:message hint) + :hint-type (:type hint)}) + props + (if (and error touched?) + (mf/spread-props props {:hint-type "error" + :hint-message (:message error)}) + props)] + + (mf/with-effect [resolve-stream tokens token input-name] + + (let [subs (->> resolve-stream + (rx/debounce 300) + (rx/mapcat (partial resolve-value tokens token)) + (rx/map (fn [result] + (d/update-when result :error + (fn [error] + ((:error/fn error) (:error/value error)))))) + (rx/subs! (fn [{:keys [error value]}] + (let [touched? (get-in @form [:touched input-name])] + (when touched? + (if error + (do + (swap! form assoc-in [:extra-errors input-name] {:message error}) + (reset! hint* {:message error :type "error"})) + (let [message (tr "workspace.tokens.resolved-value" value)] + (swap! form update :extra-errors dissoc input-name) + (reset! hint* {:message message :type "hint"}))))))))] + + (fn [] + (rx/dispose! subs)))) + + [:> ds/input* props])) + +(defn- on-composite-input-change + ([form field value] + (on-composite-input-change form field value false)) + ([form field value trim?] + (letfn [(clean-errors [errors] + (-> errors + (dissoc field) + (not-empty)))] + (swap! form (fn [state] + (-> state + (assoc-in [:data :value field] (if trim? (str/trim value) value)) + (update :errors clean-errors) + (update :extra-errors clean-errors))))))) + +(mf/defc input-composite* + [{:keys [name tokens token] :rest props}] + + (let [form (mf/use-ctx fc/context) + input-name name + + error + (get-in @form [:errors :value input-name]) + + value + (get-in @form [:data :value input-name] "") + + resolve-stream + (mf/with-memo [token] + (if-let [value (get-in token [:value input-name])] + (rx/behavior-subject value) + (rx/subject))) + + hint* + (mf/use-state {}) + + hint + (deref hint*) + + on-change + (mf/use-fn + (mf/deps resolve-stream input-name) + (fn [event] + (let [value (-> event dom/get-target dom/get-input-value)] + (on-composite-input-change form input-name value true) + (rx/push! resolve-stream value)))) + + props + (mf/spread-props props {:on-change on-change + :default-value value + :variant "comfortable" + :hint-message (:message hint) + :hint-type (:type hint)}) + props + (if error + (mf/spread-props props {:hint-type "error" + :hint-message (:message error)}) + props) + + props (if (and (not error) (= input-name :reference)) + (mf/spread-props props {:hint-formated true}) + props)] + + (mf/with-effect [resolve-stream tokens token input-name] + (let [subs (->> resolve-stream + (rx/debounce 300) + (rx/mapcat (partial resolve-value tokens token)) + (rx/map (fn [result] + (d/update-when result :error + (fn [error] + (assoc error :message ((:error/fn error) (:error/value error))))))) + + (rx/subs! + (fn [{:keys [error value]}] + (cond + (and error (str/empty? (:error/value error))) + (do + (swap! form update-in [:errors :value] dissoc input-name) + (swap! form update-in [:data :value] dissoc input-name) + (swap! form update :extra-errors dissoc :value) + (reset! hint* {})) + + (some? error) + (let [error' (:message error)] + (swap! form assoc-in [:extra-errors :value input-name] {:message error'}) + (reset! hint* {:message error' :type "error"})) + + :else + (let [message (tr "workspace.tokens.resolved-value" (dwtf/format-token-value value)) + input-value (get-in @form [:data :value input-name] "")] + (swap! form update :errors dissoc :value) + (swap! form update :extra-errors dissoc :value) + (if (= input-value (str value)) + (reset! hint* {}) + (reset! hint* {:message message :type "hint"})))))))] + (fn [] + (rx/dispose! subs)))) + + [:> ds/input* props])) + +(defn- on-indexed-input-change + ([form field index value value-subfield] + (on-indexed-input-change form field index value value-subfield false)) + ([form field index value value-subfield trim?] + (letfn [(clean-errors [errors] + (-> errors + (dissoc field) + (not-empty)))] + (swap! form (fn [state] + (-> state + (assoc-in [:data :value value-subfield index field] (if trim? (str/trim value) value)) + (update :errors clean-errors) + (update :extra-errors clean-errors))))))) + +(mf/defc input-indexed* + [{:keys [name tokens token index value-subfield] :rest props}] + + (let [form (mf/use-ctx fc/context) + input-name name + + error + (get-in @form [:errors :value value-subfield index input-name]) + + value-from-form + (get-in @form [:data :value value-subfield index input-name] "") + + resolve-stream + (mf/with-memo [token index input-name] + (if-let [value (get-in token [:value value-subfield index input-name])] + (rx/behavior-subject value) + (rx/subject))) + + hint* + (mf/use-state {}) + + hint + (deref hint*) + + on-change + (mf/use-fn + (mf/deps resolve-stream input-name index) + (fn [event] + (let [value (-> event dom/get-target dom/get-input-value)] + (on-indexed-input-change form input-name index value value-subfield true) + (rx/push! resolve-stream value)))) + + props + (mf/spread-props props {:on-change on-change + :value value-from-form + :variant "comfortable" + :hint-message (:message hint) + :hint-type (:type hint)}) + props + (if error + (mf/spread-props props {:hint-type "error" + :hint-message (:message error)}) + props) + + props + (if (and (not error) (= input-name :reference)) + (mf/spread-props props {:hint-formated true}) + props)] + + (mf/with-effect [resolve-stream tokens token input-name index value-subfield] + (let [subs (->> resolve-stream + (rx/debounce 300) + (rx/mapcat (partial resolve-value tokens token)) + (rx/map (fn [result] + (d/update-when result :error + (fn [error] + (assoc error :message ((:error/fn error) (:error/value error))))))) + + (rx/subs! + (fn [{:keys [error value]}] + (cond + (and error (str/empty? (:error/value error))) + (do + (swap! form update-in [:errors :value value-subfield index] dissoc input-name) + (swap! form update-in [:data :value value-subfield index] dissoc input-name) + (swap! form update :extra-errors dissoc :value) + (reset! hint* {})) + + (some? error) + (let [error' (:message error)] + (swap! form assoc-in [:extra-errors :value value-subfield index input-name] {:message error'}) + (reset! hint* {:message error' :type "error"})) + + :else + (let [message (tr "workspace.tokens.resolved-value" (dwtf/format-token-value value)) + input-value (get-in @form [:data :value value-subfield index input-name] "")] + (swap! form update :errors dissoc :value) + (swap! form update :extra-errors dissoc :value) + (if (= input-value (str value)) + (reset! hint* {}) + (reset! hint* {:message message :type "hint"})))))))] + (fn [] + (rx/dispose! subs)))) + + [:> ds/input* props])) diff --git a/frontend/src/app/main/ui/workspace/tokens/management/forms/controls/select.cljs b/frontend/src/app/main/ui/workspace/tokens/management/forms/controls/select.cljs new file mode 100644 index 0000000000..4bbede130a --- /dev/null +++ b/frontend/src/app/main/ui/workspace/tokens/management/forms/controls/select.cljs @@ -0,0 +1,45 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC + +(ns app.main.ui.workspace.tokens.management.forms.controls.select + (:require + [app.main.ui.ds.controls.select :refer [select*]] + [app.main.ui.forms :as fc] + [rumext.v2 :as mf])) + +;; --- Select Input (Indexed) -------------------------------------------------- +;; +;; This input type is part of the indexed system, used for fields that exist +;; inside an array of maps stored in a subfield of :value. +;; +;; - Writes to a nested location: +;; [:value ] +;; - Each item in the array has its own select input, independent of others. +;; - Validation ensures the selected value is valid for that field. +;; - Changing one item does not affect the other items in the array. +;; - Ideal for properties with predefined options (boolean, enum, etc.) where +;; multiple instances exist. + + +(mf/defc select-indexed* + [{:keys [name index indexed-type] :rest props}] + (let [form (mf/use-ctx fc/context) + input-name name + + value + (get-in @form [:data :value indexed-type index input-name] false) + + on-change + (mf/use-fn + (mf/deps input-name) + (fn [type] + (let [is-inner? (= type "inner")] + (swap! form assoc-in [:data :value indexed-type index input-name] is-inner?)))) + + props (mf/spread-props props {:default-selected (if value "inner" "drop") + :variant "ghost" + :on-change on-change})] + [:> select* props])) \ No newline at end of file diff --git a/frontend/src/app/main/ui/workspace/tokens/management/forms/font_family.cljs b/frontend/src/app/main/ui/workspace/tokens/management/forms/font_family.cljs new file mode 100644 index 0000000000..729d35e764 --- /dev/null +++ b/frontend/src/app/main/ui/workspace/tokens/management/forms/font_family.cljs @@ -0,0 +1,40 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC + +(ns app.main.ui.workspace.tokens.management.forms.font-family + (:require + [app.common.types.token :as cto] + [app.main.data.workspace.tokens.errors :as wte] + [app.main.ui.workspace.tokens.management.forms.controls :as token.controls] + [app.main.ui.workspace.tokens.management.forms.generic-form :as generic] + [app.main.ui.workspace.tokens.management.forms.validators :refer [check-coll-self-reference default-validate-token]] + [rumext.v2 :as mf])) + +(defn- check-font-family-token-self-reference [token] + (check-coll-self-reference (:name token) (:value token))) + +(defn- validate-font-family-token + [props] + (-> props + (update :token-value cto/split-font-family) + (assoc :validators [(fn [token] + (when (empty? (:value token)) + (wte/get-error-code :error.token/empty-input))) + check-font-family-token-self-reference]) + (default-validate-token))) + +(mf/defc form* + [{:keys [token token-type] :rest props}] + (let [token + (mf/with-memo [token] + (if token + (update token :value cto/join-font-family) + {:type token-type})) + props (mf/spread-props props {:token token + :token-type token-type + :validator validate-font-family-token + :input-component token.controls/fonts-combobox*})] + [:> generic/form* props])) diff --git a/frontend/src/app/main/ui/workspace/tokens/management/forms/form_container.cljs b/frontend/src/app/main/ui/workspace/tokens/management/forms/form_container.cljs new file mode 100644 index 0000000000..70797979c6 --- /dev/null +++ b/frontend/src/app/main/ui/workspace/tokens/management/forms/form_container.cljs @@ -0,0 +1,53 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC + +(ns app.main.ui.workspace.tokens.management.forms.form-container + (:require + [app.common.data :as d] + [app.common.files.tokens :as cft] + [app.common.types.tokens-lib :as ctob] + [app.main.refs :as refs] + [app.main.ui.workspace.tokens.management.forms.color :as color] + [app.main.ui.workspace.tokens.management.forms.font-family :as font-family] + [app.main.ui.workspace.tokens.management.forms.generic-form :as generic] + [app.main.ui.workspace.tokens.management.forms.shadow :as shadow] + [app.main.ui.workspace.tokens.management.forms.typography :as typography] + [app.util.i18n :refer [tr]] + [rumext.v2 :as mf])) + +(mf/defc form-container* + [{:keys [token token-type] :rest props}] + (let [token-type + (or (:type token) token-type) + + tokens-in-selected-set + (mf/deref refs/workspace-all-tokens-in-selected-set) + + token-path + (mf/with-memo [token] + (cft/token-name->path (:name token))) + + tokens-tree-in-selected-set + (mf/with-memo [token-path tokens-in-selected-set] + (-> (ctob/tokens-tree tokens-in-selected-set) + (d/dissoc-in token-path))) + props + (mf/spread-props props {:token-type token-type + :tokens-tree-in-selected-set tokens-tree-in-selected-set + :token token}) + text-case-props (mf/spread-props props {:input-value-placeholder (tr "workspace.tokens.text-case-value-enter")}) + text-decoration-props (mf/spread-props props {:input-value-placeholder (tr "workspace.tokens.text-decoration-value-enter")}) + font-weight-props (mf/spread-props props {:input-value-placeholder (tr "workspace.tokens.font-weight-value-enter")})] + + (case token-type + :color [:> color/form* props] + :typography [:> typography/form* props] + :shadow [:> shadow/form* props] + :font-family [:> font-family/form* props] + :text-case [:> generic/form* text-case-props] + :text-decoration [:> generic/form* text-decoration-props] + :font-weight [:> generic/form* font-weight-props] + [:> generic/form* props]))) \ No newline at end of file diff --git a/frontend/src/app/main/ui/workspace/tokens/management/create/border_radius.cljs b/frontend/src/app/main/ui/workspace/tokens/management/forms/generic_form.cljs similarity index 75% rename from frontend/src/app/main/ui/workspace/tokens/management/create/border_radius.cljs rename to frontend/src/app/main/ui/workspace/tokens/management/forms/generic_form.cljs index d3f5842390..098f738010 100644 --- a/frontend/src/app/main/ui/workspace/tokens/management/create/border_radius.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/management/forms/generic_form.cljs @@ -4,7 +4,7 @@ ;; ;; Copyright (c) KALEIDOS INC -(ns app.main.ui.workspace.tokens.management.create.border-radius +(ns app.main.ui.workspace.tokens.management.forms.generic-form (:require-macros [app.main.style :as stl]) (:require [app.common.files.tokens :as cft] @@ -23,7 +23,8 @@ [app.main.ui.ds.foundations.typography.heading :refer [heading*]] [app.main.ui.ds.notifications.context-notification :refer [context-notification*]] [app.main.ui.forms :as fc] - [app.main.ui.workspace.tokens.management.create.form-input-token :refer [form-input-token*]] + [app.main.ui.workspace.tokens.management.forms.controls :as token.controls] + [app.main.ui.workspace.tokens.management.forms.validators :refer [default-validate-token]] [app.util.dom :as dom] [app.util.forms :as fm] [app.util.i18n :refer [tr]] @@ -38,8 +39,25 @@ (str/blank? value)) (tr "workspace.tokens.empty-input"))) -(defn- make-schema - [tokens-tree] + +(defn get-value-for-validator + [active-tab value value-subfield form-type] + + (case form-type + :indexed + (if (= active-tab :reference) + (:reference value) + (value-subfield value)) + + :composite + (if (= active-tab :reference) + (get value :reference) + value) + + value)) + +(defn- default-make-schema + [tokens-tree _] (sm/schema [:and [:map @@ -62,14 +80,37 @@ (nil? (cto/token-value-self-reference? name value))))]])) (mf/defc form* - [{:keys [token validate-token action is-create selected-token-set-id tokens-tree-in-selected-set] :as props}] + [{:keys [token + validator + action + is-create + selected-token-set-id + tokens-tree-in-selected-set + token-type + make-schema + input-component + initial + type + value-subfield + input-value-placeholder] :as props}] - (let [token + (let [make-schema (or make-schema default-make-schema) + input-component (or input-component token.controls/input*) + validate-token (or validator default-validate-token) + + active-tab* (mf/use-state #(if (cft/is-reference? token) :reference :composite)) + active-tab (deref active-tab*) + + on-toggle-tab + (mf/use-fn + (mf/deps) + (fn [new-tab] + (let [new-tab (keyword new-tab)] + (reset! active-tab* new-tab)))) + + token (mf/with-memo [token] - (or token {:type :border-radius})) - - token-type - (get token :type) + (or token {:type token-type})) token-properties (dwta/get-token-properties token) @@ -89,14 +130,15 @@ (assoc (:name token) token))) schema - (mf/with-memo [tokens-tree-in-selected-set] - (make-schema tokens-tree-in-selected-set)) + (mf/with-memo [tokens-tree-in-selected-set active-tab] + (make-schema tokens-tree-in-selected-set active-tab)) initial (mf/with-memo [token] - {:name (:name token "") - :value (:value token "") - :description (:description token "")}) + (or initial + {:name (:name token "") + :value (:value token "") + :description (:description token "")})) form (fm/use-form :schema schema @@ -136,12 +178,13 @@ on-submit (mf/use-fn - (mf/deps validate-token token tokens token-type) + (mf/deps validate-token token tokens token-type value-subfield type active-tab) (fn [form _event] (let [name (get-in @form [:clean-data :name]) description (get-in @form [:clean-data :description]) - value (get-in @form [:clean-data :value])] - (->> (validate-token {:token-value value + value (get-in @form [:clean-data :value]) + value-for-validation (get-value-for-validator active-tab value value-subfield type)] + (->> (validate-token {:token-value value-for-validation :token-name name :token-description description :prev-token token @@ -168,7 +211,9 @@ [:div {:class (stl/css :token-rows)} [:> heading* {:level 2 :typography "headline-medium" :class (stl/css :form-modal-title)} - (tr "workspace.tokens.create-token" token-type)] + (if (= action "edit") + (tr "workspace.tokens.edit-token" token-type) + (tr "workspace.tokens.create-token" token-type))] [:div {:class (stl/css :input-row)} [:> fc/form-input* {:id "token-name" @@ -185,12 +230,16 @@ {:level :warning :appearance :ghost} (tr "workspace.tokens.warning-name-change")]])] [:div {:class (stl/css :input-row)} - [:> form-input-token* - {:placeholder (tr "workspace.tokens.token-value-enter") + [:> input-component + {:placeholder (or input-value-placeholder (tr "workspace.tokens.token-value-enter")) :label (tr "workspace.tokens.token-value") :name :value + :form form :token token - :tokens tokens}]] + :tokens tokens + :tab active-tab + :subfield value-subfield + :toggle on-toggle-tab}]] [:div {:class (stl/css :input-row)} [:> fc/form-input* {:id "token-description" diff --git a/frontend/src/app/main/ui/workspace/tokens/management/create/font_family.scss b/frontend/src/app/main/ui/workspace/tokens/management/forms/generic_form.scss similarity index 100% rename from frontend/src/app/main/ui/workspace/tokens/management/create/font_family.scss rename to frontend/src/app/main/ui/workspace/tokens/management/forms/generic_form.scss diff --git a/frontend/src/app/main/ui/workspace/tokens/management/create/modals.cljs b/frontend/src/app/main/ui/workspace/tokens/management/forms/modals.cljs similarity index 82% rename from frontend/src/app/main/ui/workspace/tokens/management/create/modals.cljs rename to frontend/src/app/main/ui/workspace/tokens/management/forms/modals.cljs index 7a706976ed..9356d45725 100644 --- a/frontend/src/app/main/ui/workspace/tokens/management/create/modals.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/management/forms/modals.cljs @@ -4,7 +4,7 @@ ;; ;; Copyright (c) KALEIDOS INC -(ns app.main.ui.workspace.tokens.management.create.modals +(ns app.main.ui.workspace.tokens.management.forms.modals (:require-macros [app.main.style :as stl]) (:require [app.common.data.macros :as dm] @@ -13,22 +13,28 @@ [app.main.refs :as refs] [app.main.ui.ds.buttons.icon-button :refer [icon-button*]] [app.main.ui.ds.foundations.assets.icon :as i] - [app.main.ui.workspace.tokens.management.create.form :refer [form-wrapper*]] + [app.main.ui.workspace.tokens.management.forms.form-container :refer [form-container*]] [app.util.i18n :refer [tr]] [okulary.core :as l] [rumext.v2 :as mf])) ;; Component ------------------------------------------------------------------- -(defn calculate-position +(defn- calculate-position "Calculates the style properties for the given coordinates and position" - [{vh :height} position x y color?] - (let [;; picker height in pixels - ;; TODO: Revisit these harcoded values - h (if color? 610 510) + [{vh :height} position x y token-type] + (let [; TODO: Revisit these harcoded values + modal-height (case token-type + :color + 500 + :typography + 660 + :shadow + 660 + 400) ;; Checks for overflow outside the viewport height - max-y (- vh h) - overflow-fix (max 0 (+ y (- 50) h (- vh))) + max-y (- vh modal-height) + overflow-fix (max 0 (+ y (- 50) modal-height (- vh))) bottom-offset "1rem" top-offset (dm/str (- y 70) "px") max-height-top (str "calc(100vh - " top-offset) @@ -61,17 +67,19 @@ :top (dm/str (- y 70 overflow-fix) "px") :maxHeight max-height-top})))) -(defn use-viewport-position-style [x y position color?] +(defn- use-viewport-position-style [x y position token-type] (let [vport (-> (l/derived :vport refs/workspace-local) (mf/deref))] - (-> (calculate-position vport position x y color?) + (-> (calculate-position vport position x y token-type) (clj->js)))) (mf/defc token-update-create-modal {::mf/wrap-props false} [{:keys [x y position token token-type action selected-token-set-id] :as _args}] - (let [wrapper-style (use-viewport-position-style x y position (= token-type :color)) - modal-size-large* (mf/use-state (= token-type :typography)) + (let [wrapper-style (use-viewport-position-style x y position token-type) + modal-size-large* (mf/use-state (or (= token-type :typography) + (= token-type :color) + (= token-type :shadow))) modal-size-large? (deref modal-size-large*) close-modal (mf/use-fn (fn [] @@ -89,12 +97,12 @@ :icon i/close :variant "action" :aria-label (tr "labels.close")}] - [:> form-wrapper* {:is-create (not (ctob/token? token)) - :token token - :action action - :selected-token-set-id selected-token-set-id - :token-type token-type - :on-display-colorpicker update-modal-size}]])) + [:> form-container* {:is-create (not (ctob/token? token)) + :token token + :action action + :selected-token-set-id selected-token-set-id + :token-type token-type + :on-display-colorpicker update-modal-size}]])) ;; Modals ---------------------------------------------------------------------- diff --git a/frontend/src/app/main/ui/workspace/tokens/management/create/modals.scss b/frontend/src/app/main/ui/workspace/tokens/management/forms/modals.scss similarity index 100% rename from frontend/src/app/main/ui/workspace/tokens/management/create/modals.scss rename to frontend/src/app/main/ui/workspace/tokens/management/forms/modals.scss diff --git a/frontend/src/app/main/ui/workspace/tokens/management/forms/shadow.cljs b/frontend/src/app/main/ui/workspace/tokens/management/forms/shadow.cljs new file mode 100644 index 0000000000..880d957167 --- /dev/null +++ b/frontend/src/app/main/ui/workspace/tokens/management/forms/shadow.cljs @@ -0,0 +1,362 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC + +(ns app.main.ui.workspace.tokens.management.forms.shadow + (:require-macros [app.main.style :as stl]) + (:require + [app.common.data :as d] + [app.common.files.tokens :as cft] + [app.common.schema :as sm] + [app.common.types.token :as cto] + [app.main.data.workspace.tokens.errors :as wte] + [app.main.ui.components.radio-buttons :refer [radio-button radio-buttons]] + [app.main.ui.ds.buttons.icon-button :refer [icon-button*]] + [app.main.ui.ds.foundations.assets.icon :as i] + [app.main.ui.forms :as forms] + [app.main.ui.hooks :as hooks] + [app.main.ui.workspace.tokens.management.forms.controls :as token.controls] + [app.main.ui.workspace.tokens.management.forms.generic-form :as generic] + [app.main.ui.workspace.tokens.management.forms.validators :refer [check-self-reference default-validate-token]] + [app.util.dom :as dom] + [app.util.i18n :refer [tr]] + [beicon.v2.core :as rx] + [cuerdas.core :as str] + [rumext.v2 :as mf])) + +(defn- check-shadow-token-self-reference + "Check token when any of the attributes in a shadow's value have a self-reference." + [token] + (let [token-name (:name token) + shadow-values (:value token)] + (some (fn [[shadow-idx shadow-map]] + (some (fn [[k v]] + (when-let [err (check-self-reference token-name v)] + (assoc err :shadow-key k :shadow-index shadow-idx))) + shadow-map)) + (d/enumerate shadow-values)))) + +(defn- check-empty-shadow-token [token] + (when (or (empty? (:value token)) + (some (fn [shadow] (not-every? #(contains? shadow %) [:offset-x :offset-y :blur :spread :color])) + (:value token))) + (wte/get-error-code :error.token/empty-input))) + +(defn- validate-shadow-token + [{:keys [token-value] :as params}] + (cond + ;; Entering form without a value - show no error just resolve nil + (nil? token-value) (rx/of nil) + ;; Validate refrence string + (cto/shadow-composite-token-reference? token-value) (default-validate-token params) + ;; Validate composite token + :else + (let [params (-> params + (update :token-value (fn [value] + (->> (or value []) + (mapv (fn [shadow] + (d/update-when shadow :inset #(cond + (boolean? %) % + (= "true" %) true + :else false))))))) + (assoc :validators [check-empty-shadow-token + check-shadow-token-self-reference]))] + + (default-validate-token params)))) + +(def ^:private default-token-shadow + {:offset-x "4" + :offset-y "4" + :blur "4" + :spread "0"}) + +(defn get-subtoken + [token index prop value-subfield] + (let [value (get-in token [:value value-subfield index prop])] + (d/without-nils + {:type (if (= prop :color) :color :number) + :value value}))) + +(mf/defc shadow-formset* + [{:keys [index token tokens remove-shadow-block show-button value-subfield] :as props}] + (let [inset-token (get-subtoken token index :inset value-subfield) + inset-token (hooks/use-equal-memo inset-token) + + color-token (get-subtoken token index :color value-subfield) + color-token (hooks/use-equal-memo color-token) + + offset-x-token (get-subtoken token index :offset-x value-subfield) + offset-x-token (hooks/use-equal-memo offset-x-token) + + offset-y-token (get-subtoken token index :offset-y value-subfield) + offset-y-token (hooks/use-equal-memo offset-y-token) + + blur-token (get-subtoken token index :blur value-subfield) + blur-token (hooks/use-equal-memo blur-token) + + spread-token (get-subtoken token index :spread value-subfield) + spread-token (hooks/use-equal-memo spread-token) + + on-button-click + (mf/use-fn + (mf/deps index) + (fn [event] + (remove-shadow-block index event)))] + + [:div {:class (stl/css :shadow-block) + :data-testid (str "shadow-input-fields-" index)} + [:div {:class (stl/css :select-wrapper)} + [:> token.controls/select-indexed* {:options [{:id "drop" :label "drop shadow" :icon i/drop-shadow} + {:id "inner" :label "inner shadow" :icon i/inner-shadow}] + :aria-label (tr "workspace.tokens.shadow-inset") + :token inset-token + :tokens tokens + :index index + :value-subfield value-subfield + :name :inset}] + (when show-button + [:> icon-button* {:variant "ghost" + :type "button" + :aria-label (tr "workspace.tokens.shadow-remove-shadow") + :on-click on-button-click + :icon i/remove}])] + [:div {:class (stl/css :inputs-wrapper)} + [:div {:class (stl/css :input-row)} + [:> token.controls/indexed-color-input* + {:placeholder (tr "workspace.tokens.token-value-enter") + :aria-label (tr "workspace.tokens.color") + :name :color + :token color-token + :value-subfield value-subfield + :index index + :tokens tokens}]] + + [:div {:class (stl/css :input-row)} + [:> token.controls/input-indexed* + {:aria-label (tr "workspace.tokens.shadow-x") + :icon i/character-x + :placeholder (tr "workspace.tokens.shadow-x") + :name :offset-x + :token offset-x-token + :index index + :value-subfield value-subfield + :tokens tokens}]] + + [:div {:class (stl/css :input-row)} + [:> token.controls/input-indexed* + {:aria-label (tr "workspace.tokens.shadow-y") + :icon i/character-y + :placeholder (tr "workspace.tokens.shadow-y") + :name :offset-y + :token offset-y-token + :index index + :value-subfield value-subfield + :tokens tokens}]] + + [:div {:class (stl/css :input-row)} + [:> token.controls/input-indexed* + {:aria-label (tr "workspace.tokens.shadow-blur") + :placeholder (tr "workspace.tokens.shadow-blur") + :name :blur + :slot-start (mf/html [:span {:class (stl/css :visible-label)} + (str (tr "workspace.tokens.shadow-blur") ":")]) + :token blur-token + :index index + :value-subfield value-subfield + :tokens tokens}]] + + [:div {:class (stl/css :input-row)} + [:> token.controls/input-indexed* + {:aria-label (tr "workspace.tokens.shadow-spread") + :placeholder (tr "workspace.tokens.shadow-spread") + :name :spread + :slot-start (mf/html [:span {:class (stl/css :visible-label)} + (str (tr "workspace.tokens.shadow-spread") ":")]) + :token spread-token + :value-subfield value-subfield + :index index + :tokens tokens}]]]])) + +(mf/defc composite-form* + [{:keys [token tokens remove-shadow-block value-subfield] :as props}] + (let [form + (mf/use-ctx forms/context) + + length + (-> form deref :data :value value-subfield count)] + + (for [index (range length)] + [:> shadow-formset* {:key index + :index index + :token token + :tokens tokens + :value-subfield value-subfield + :remove-shadow-block remove-shadow-block + :show-button (> length 1)}]))) + +(mf/defc reference-form* + [{:keys [token tokens] :as props}] + [:div {:class (stl/css :input-row-reference)} + [:> token.controls/input-composite* + {:placeholder (tr "workspace.tokens.reference-composite-shadow") + :aria-label (tr "labels.reference") + :icon i/drop-shadow + :name :reference + :token token + :tokens tokens}]]) + +(mf/defc tabs-wrapper* + [{:keys [token tokens form tab toggle subfield] :rest props}] + (let [on-add-shadow-block + (mf/use-fn + (mf/deps subfield) + (fn [] + (swap! form update-in [:data :value subfield] conj default-token-shadow))) + + remove-shadow-block + (mf/use-fn + (mf/deps subfield) + (fn [index event] + (dom/prevent-default event) + (swap! form update-in [:data :value subfield] #(d/remove-at-index % index))))] + + [:* + [:div {:class (stl/css :title-bar)} + [:div {:class (stl/css :title)} (tr "labels.shadow")] + [:> icon-button* {:variant "ghost" + :type "button" + :aria-label (tr "workspace.tokens.shadow-add-shadow") + :on-click on-add-shadow-block + :icon i/add}] + [:& radio-buttons {:class (stl/css :listing-options) + :selected (d/name tab) + :on-change toggle + :name "reference-composite-tab"} + [:& radio-button {:icon i/layers + :value "composite" + :title (tr "workspace.tokens.individual-tokens") + :id "composite-opt"}] + [:& radio-button {:icon i/tokens + :value "reference" + :title (tr "workspace.tokens.use-reference") + :id "reference-opt"}]]] + + (if (= tab :composite) + [:> composite-form* {:token token + :tokens tokens + :remove-shadow-block remove-shadow-block + :value-subfield subfield}] + + [:> reference-form* {:token token + :tokens tokens}])])) + +(defn- make-schema + [tokens-tree active-tab] + (sm/schema + [:and + [:map + [:name + [:and + [:string {:min 1 :max 255 + :error/fn #(str (:value %) (tr "workspace.tokens.token-name-length-validation-error"))}] + (sm/update-properties cto/token-name-ref assoc + :error/fn #(str (:value %) (tr "workspace.tokens.token-name-validation-error"))) + [:fn {:error/fn #(tr "workspace.tokens.token-name-duplication-validation-error" (:value %))} + #(not (cft/token-name-path-exists? % tokens-tree))]]] + + [:value + [:map + [:shadow {:optinal true} + [:vector + [:map + [:offset-x {:optional true} [:maybe :string]] + [:offset-y {:optional true} [:maybe :string]] + [:blur {:optional true} [:maybe :string]] + [:spread {:optional true} [:maybe :string]] + [:color {:optional true} [:maybe :string]] + [:color-result {:optional true} ::sm/any] + [:inset {:optional true} [:maybe :boolean]]]]] + (if (= active-tab :reference) + [:reference {:optional false} ::sm/text] + [:reference {:optional true} [:maybe :string]])]] + + [:description {:optional true} + [:string {:max 2048 :error/fn #(tr "errors.field-max-length" 2048)}]]] + + [:fn {:error/field [:value :reference] + :error/fn #(tr "workspace.tokens.self-reference")} + (fn [{:keys [name value]}] + (let [reference (get value :reference)] + (if (and reference name) + (not (cto/token-value-self-reference? name reference)) + true)))] + + [:fn {:error/fn (fn [_] "Must be a valid shadow or reference") + :error/field :value} + (fn [{:keys [value]}] + (let [reference (get value :reference) + ref-valid? (and reference (not (str/blank? reference))) + + shadows (get value :shadow) + ;; To be a valid shadow it must contain one on each valid values + valid-composite-shadow? + (and (seq shadows) + (every? + (fn [{:keys [offset-x offset-y blur spread color]}] + (and (not (str/blank? offset-x)) + (not (str/blank? offset-y)) + (not (str/blank? blur)) + (not (str/blank? spread)) + (not (str/blank? color)))) + shadows))] + + (or ref-valid? valid-composite-shadow?)))]])) + +(defn- make-default-value + [value] + (cond + (string? value) + {:reference value + :shadow []} + + (vector? value) + {:reference nil + :shadow value} + + :else + {:reference nil + :shadow [default-token-shadow]})) + +(mf/defc form* + [{:keys [token + token-type] :as props}] + (let [token + (mf/with-memo [token] + (or token + (if-let [value (get token :value)] + {:type token-type + :value (make-default-value value)} + + {:type token-type + :value {:reference nil + :shadow [default-token-shadow]}}))) + initial + (mf/with-memo [token] + (let [raw-value (:value token) + value (make-default-value raw-value)] + + {:name (:name token "") + :description (:description token "") + :value value})) + + props (mf/spread-props props {:token token + :token-type token-type + :initial initial + :make-schema make-schema + :type :indexed + :value-subfield :shadow + :input-component tabs-wrapper* + :validator validate-shadow-token})] + [:> generic/form* props])) diff --git a/frontend/src/app/main/ui/workspace/tokens/management/create/typography.scss b/frontend/src/app/main/ui/workspace/tokens/management/forms/shadow.scss similarity index 68% rename from frontend/src/app/main/ui/workspace/tokens/management/create/typography.scss rename to frontend/src/app/main/ui/workspace/tokens/management/forms/shadow.scss index d129320f10..f0a973f0b5 100644 --- a/frontend/src/app/main/ui/workspace/tokens/management/create/typography.scss +++ b/frontend/src/app/main/ui/workspace/tokens/management/forms/shadow.scss @@ -8,17 +8,6 @@ @use "ds/_sizes.scss" as *; @use "ds/_borders.scss" as *; -.form-wrapper { - width: $sz-384; - position: relative; -} - -.token-rows { - display: flex; - flex-direction: column; - gap: var(--sp-l); -} - .inputs-wrapper { display: flex; flex-direction: column; @@ -34,9 +23,19 @@ gap: var(--sp-xs); } +.input-row-reference { + position: relative; + display: flex; + flex-direction: column; + gap: var(--sp-xs); + border-inline-start: $b-1 solid var(--color-accent-primary-muted); + padding-inline-start: var(--sp-m); +} + .title-bar { display: grid; - grid-template-columns: 1fr auto; + grid-template-columns: 1fr auto auto; + gap: var(--sp-xs); } .title { @@ -46,29 +45,17 @@ align-items: center; } -.form-modal-title { - @include t.use-typography("headline-medium"); - color: var(--color-foreground-primary); - display: flex; - align-items: center; -} - -.button-row { - display: grid; - grid-template-columns: auto auto; - justify-content: end; - gap: var(--sp-m); - padding-block-start: var(--sp-s); -} - -.with-delete { - grid-template-columns: 1fr auto auto; -} - -.warning-name-change-notification-wrapper { - margin-block-start: var(--sp-l); -} - .delete-btn { justify-self: start; } + +.visible-label { + @include t.use-typography("headline-small"); + color: var(--color-foreground-secondary); + line-height: $sz-32; +} + +.select-wrapper { + display: grid; + grid-template-columns: 1fr auto; +} diff --git a/frontend/src/app/main/ui/workspace/tokens/management/forms/typography.cljs b/frontend/src/app/main/ui/workspace/tokens/management/forms/typography.cljs new file mode 100644 index 0000000000..1db82e390d --- /dev/null +++ b/frontend/src/app/main/ui/workspace/tokens/management/forms/typography.cljs @@ -0,0 +1,304 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC + +(ns app.main.ui.workspace.tokens.management.forms.typography + (:require-macros [app.main.style :as stl]) + (:require + [app.common.data :as d] + [app.common.files.tokens :as cft] + [app.common.schema :as sm] + [app.common.types.token :as cto] + [app.main.data.workspace.tokens.errors :as wte] + [app.main.ui.components.radio-buttons :refer [radio-button radio-buttons]] + [app.main.ui.ds.foundations.assets.icon :as i] + [app.main.ui.workspace.tokens.management.forms.controls :as token.controls] + [app.main.ui.workspace.tokens.management.forms.generic-form :as generic] + [app.main.ui.workspace.tokens.management.forms.validators :refer [check-coll-self-reference check-self-reference default-validate-token]] + [app.util.i18n :refer [tr]] + [beicon.v2.core :as rx] + [cuerdas.core :as str] + [rumext.v2 :as mf])) + +;; VALIDATORS + +(defn- check-typography-token-self-reference + "Check token when any of the attributes in token value have a self-reference." + [token] + (let [token-name (:name token) + token-values (:value token)] + (some (fn [[k v]] + (when-let [err (case k + :font-family (check-coll-self-reference token-name v) + (check-self-reference token-name v))] + (assoc err :typography-key k))) + token-values))) + +(defn- check-empty-typography-token [token] + (when (empty? (:value token)) + (wte/get-error-code :error.token/empty-input))) + +(defn- validate-typography-token + [{:keys [token-value] :as props}] + (cond + ;; Entering form without a value - show no error just resolve nil + (nil? token-value) (rx/of nil) + ;; Validate refrence string + (cto/typography-composite-token-reference? token-value) (default-validate-token props) + ;; Validate composite token + :else + (-> props + (update :token-value + (fn [v] + (-> (or v {}) + (d/update-when :font-family #(if (string? %) (cto/split-font-family %) %))))) + (assoc :validators [check-empty-typography-token + check-typography-token-self-reference]) + (default-validate-token)))) + +;; COMPONENTS + +(mf/defc composite-form* + [{:keys [token tokens] :as props}] + (let [letter-spacing-sub-token + (mf/with-memo [token] + (if-let [value (get token :value)] + {:type :letter-spacing + :value (cto/join-font-family (get value :letter-spacing))} + {:type :letter-spacing})) + + font-family-sub-token + (mf/with-memo [token] + (if-let [value (get token :value)] + {:type :font-family + :value (get value :font-family)} + {:type :font-family})) + + font-size-sub-token + (mf/with-memo [token] + (if-let [value (get token :value)] + {:type :font-size + :value (get value :font-size)} + {:type :font-size})) + + font-weight-sub-token + (mf/with-memo [token] + (if-let [value (get token :value)] + {:type :font-weight + :value (get value :font-weight)} + {:type :font-weight})) + + line-height-sub-token + (mf/with-memo [token] + (if-let [value (get token :value)] + {:type :number + :value (get value :line-height)} + {:type :number})) + + text-case-sub-token + (mf/with-memo [token] + (if-let [value (get token :value)] + {:type :text-case + :value (get value :text-case)} + {:type :text-case})) + + text-decoration-sub-token + (mf/with-memo [token] + (if-let [value (get token :value)] + {:type :text-decoration + :value (get value :text-decoration)} + {:type :text-decoration}))] + + [:* + [:div {:class (stl/css :input-row)} + [:> token.controls/composite-fonts-combobox* + {:icon i/text-font-family + :placeholder (tr "workspace.tokens.token-font-family-value-enter") + :aria-label (tr "workspace.tokens.token-font-family-value") + :name :font-family + :token font-family-sub-token + :tokens tokens}]] + [:div {:class (stl/css :input-row)} + [:> token.controls/input-composite* + {:aria-label "Font Size" + :icon i/text-font-size + :placeholder (tr "workspace.tokens.font-size-value-enter") + :name :font-size + :token font-size-sub-token + :tokens tokens}]] + [:div {:class (stl/css :input-row)} + [:> token.controls/input-composite* + {:aria-label "Font Weight" + :icon i/text-font-weight + :placeholder (tr "workspace.tokens.font-weight-value-enter") + :name :font-weight + :token font-weight-sub-token + :tokens tokens}]] + [:div {:class (stl/css :input-row)} + [:> token.controls/input-composite* + {:aria-label "Line Height" + :icon i/text-lineheight + :placeholder (tr "workspace.tokens.line-height-value-enter") + :name :line-height + :token line-height-sub-token + :tokens tokens}]] + [:div {:class (stl/css :input-row)} + [:> token.controls/input-composite* + {:aria-label "Letter Spacing" + :icon i/text-letterspacing + :placeholder (tr "workspace.tokens.letter-spacing-value-enter-composite") + :name :letter-spacing + :token letter-spacing-sub-token + :tokens tokens}]] + [:div {:class (stl/css :input-row)} + [:> token.controls/input-composite* + {:aria-label "Text Case" + :icon i/text-mixed + :placeholder (tr "workspace.tokens.text-case-value-enter") + :name :text-case + :token text-case-sub-token + :tokens tokens}]] + [:div {:class (stl/css :input-row)} + [:> token.controls/input-composite* + {:aria-label "Text Decoration" + :icon i/text-underlined + :placeholder (tr "workspace.tokens.text-decoration-value-enter") + :name :text-decoration + :token text-decoration-sub-token + :tokens tokens}]]])) + +(mf/defc reference-form* + [{:keys [token tokens] :as props}] + [:div {:class (stl/css :input-row)} + [:> token.controls/input-composite* + {:placeholder (tr "workspace.tokens.reference-composite") + :aria-label (tr "labels.reference") + :icon i/text-typography + :name :reference + :token token + :tokens tokens}]]) + +(mf/defc tabs-wrapper* + [{:keys [token tokens tab toggle] :rest props}] + [:* + [:div {:class (stl/css :title-bar)} + [:div {:class (stl/css :title)} (tr "labels.typography")] + [:& radio-buttons {:class (stl/css :listing-options) + :selected (d/name tab) + :on-change toggle + :name "reference-composite-tab"} + [:& radio-button {:icon i/layers + :value "composite" + :title (tr "workspace.tokens.individual-tokens") + :id "composite-opt"}] + [:& radio-button {:icon i/tokens + :value "reference" + :title (tr "workspace.tokens.use-reference") + :id "reference-opt"}]]] + [:div {:class (stl/css :inputs-wrapper)} + (if (= tab :composite) + [:> composite-form* {:token token + :tokens tokens}] + + [:> reference-form* {:token token + :tokens tokens}])]]) + +;; SCHEMA + +(defn- make-schema + [tokens-tree active-tab] + (sm/schema + [:and + [:map + [:name + [:and + [:string {:min 1 :max 255 + :error/fn #(str (:value %) (tr "workspace.tokens.token-name-length-validation-error"))}] + (sm/update-properties cto/token-name-ref assoc + :error/fn #(str (:value %) (tr "workspace.tokens.token-name-validation-error"))) + [:fn {:error/fn #(tr "workspace.tokens.token-name-duplication-validation-error" (:value %))} + #(not (cft/token-name-path-exists? % tokens-tree))]]] + + [:value + [:map + [:font-family {:optional true} [:maybe :string]] + [:font-size {:optional true} [:maybe :string]] + [:font-weight {:optional true} [:maybe :string]] + [:line-height {:optional true} [:maybe :string]] + [:letter-spacing {:optional true} [:maybe :string]] + [:text-case {:optional true} [:maybe :string]] + [:text-decoration {:optional true} [:maybe :string]] + (if (= active-tab :reference) + [:reference {:optional false} ::sm/text] + [:reference {:optional true} [:maybe :string]])]] + + [:description {:optional true} + [:string {:max 2048 :error/fn #(tr "errors.field-max-length" 2048)}]]] + + [:fn {:error/field [:value :reference] + :error/fn #(tr "workspace.tokens.self-reference")} + (fn [{:keys [name value]}] + (let [reference (get value :reference)] + (if (and reference name) + (not (cto/token-value-self-reference? name reference)) + true)))] + + [:fn {:error/field [:value :line-height] + :error/fn #(tr "workspace.tokens.composite-line-height-needs-font-size")} + (fn [{:keys [value]}] + (let [line-heigh (get value :line-height) + font-size (get value :font-size)] + (if (and line-heigh (not font-size)) + false + true)))] + + ;; This error does not shown on interface, it's just to avoid saving empty composite tokens + ;; We don't need to translate it. + [:fn {:error/fn (fn [_] "At least one composite field must be set") + :error/field :value} + (fn [attrs] + (let [result (reduce-kv (fn [_ _ v] + (if (str/empty? v) + false + (reduced true))) + false + (get attrs :value))] + result))]])) + +(mf/defc form* + [{:keys [token] :as props}] + (let [initial + (mf/with-memo [token] + (let [value (:value token) + processed-value + (cond + (string? value) + {:reference value} + + (map? value) + (let [value (cond-> value + (:font-family value) + (update :font-family cto/join-font-family))] + (select-keys value + [:font-family + :font-size + :font-weight + :line-height + :letter-spacing + :text-case + :text-decoration])) + :else + {})] + + {:name (:name token "") + :value processed-value + :description (:description token "")})) + props (mf/spread-props props {:initial initial + :make-schema make-schema + :token token + :validator validate-typography-token + :type :composite + :input-component tabs-wrapper*})] + [:> generic/form* props])) diff --git a/frontend/src/app/main/ui/workspace/tokens/management/create/dimensions.scss b/frontend/src/app/main/ui/workspace/tokens/management/forms/typography.scss similarity index 59% rename from frontend/src/app/main/ui/workspace/tokens/management/create/dimensions.scss rename to frontend/src/app/main/ui/workspace/tokens/management/forms/typography.scss index 6593b3c30a..5bac11ad27 100644 --- a/frontend/src/app/main/ui/workspace/tokens/management/create/dimensions.scss +++ b/frontend/src/app/main/ui/workspace/tokens/management/forms/typography.scss @@ -8,18 +8,16 @@ @use "ds/_sizes.scss" as *; @use "ds/_borders.scss" as *; -.form-wrapper { - width: $sz-384; - position: relative; -} - -.token-rows { +.inputs-wrapper { display: flex; flex-direction: column; - gap: var(--sp-l); + gap: var(--sp-m); + border-inline-start: $b-1 solid var(--color-accent-primary-muted); + padding-inline-start: var(--sp-m); } .input-row { + position: relative; display: flex; flex-direction: column; gap: var(--sp-xs); @@ -30,29 +28,9 @@ grid-template-columns: 1fr auto; } -.form-modal-title { - @include t.use-typography("headline-medium"); +.title { + @include t.use-typography("body-small"); color: var(--color-foreground-primary); display: flex; align-items: center; } - -.button-row { - display: grid; - grid-template-columns: auto auto; - justify-content: end; - gap: var(--sp-m); - padding-block-start: var(--sp-s); -} - -.with-delete { - grid-template-columns: 1fr auto auto; -} - -.warning-name-change-notification-wrapper { - margin-block-start: var(--sp-l); -} - -.delete-btn { - justify-self: start; -} diff --git a/frontend/src/app/main/ui/workspace/tokens/management/forms/validators.cljs b/frontend/src/app/main/ui/workspace/tokens/management/forms/validators.cljs new file mode 100644 index 0000000000..f17156dd45 --- /dev/null +++ b/frontend/src/app/main/ui/workspace/tokens/management/forms/validators.cljs @@ -0,0 +1,111 @@ +(ns app.main.ui.workspace.tokens.management.forms.validators + (:require + [app.common.data :as d] + [app.common.files.tokens :as cft] + [app.common.schema :as sm] + [app.common.types.token :as cto] + [app.common.types.tokens-lib :as ctob] + [app.main.data.style-dictionary :as sd] + [app.main.data.workspace.tokens.errors :as wte] + [app.util.i18n :refer [tr]] + [beicon.v2.core :as rx] + [cuerdas.core :as str])) + +;; Value Validation ------------------------------------------------------------- + + +(defn check-empty-value [token] + (let [token-value (:value token)] + (when (empty? (str/trim token-value)) + (wte/get-error-code :error.token/empty-input)))) + +(defn check-self-reference [token-name token-value] + (when (cto/token-value-self-reference? token-name token-value) + (wte/get-error-code :error.token/direct-self-reference))) + +(defn validate-resolve-token + [token prev-token tokens] + (let [token (cond-> token + ;; When creating a new token we dont have a name yet or invalid name, + ;; but we still want to resolve the value to show in the form. + ;; So we use a temporary token name that hopefully doesn't clash with any of the users token names + (not (sm/valid? cto/token-name-ref (:name token))) (assoc :name "__PENPOT__TOKEN__NAME__PLACEHOLDER__")) + tokens' (cond-> tokens + ;; Remove previous token when renaming a token + (not= (:name token) (:name prev-token)) + (dissoc (:name prev-token)) + + :always + (update (:name token) #(ctob/make-token (merge % prev-token token))))] + (->> tokens' + (sd/resolve-tokens-interactive) + (rx/mapcat + (fn [resolved-tokens] + (let [{:keys [errors resolved-value] :as resolved-token} (get resolved-tokens (:name token))] + (cond + resolved-value (rx/of resolved-token) + :else (rx/throw {:errors (or (seq errors) + [(wte/get-error-code :error/unknown-error)])})))))))) + +(defn- validate-token-with [token validators] + (if-let [error (some (fn [validate] (validate token)) validators)] + (rx/throw {:errors [error]}) + (rx/of token))) + +(def ^:private default-validators + [check-empty-value check-self-reference]) + +(defn default-validate-token + "Validates a token by confirming a list of `validator` predicates and + resolving the token using `tokens` with StyleDictionary. Returns rx + stream of either a valid resolved token or an errors map. + + Props: + token-name, token-value, token-description: Values from the form inputs + prev-token: The existing token currently being edited + tokens: tokens map keyed by token-name + Used to look up the editing token & resolving step. + + validators: A list of predicates that will be used to do simple validation on the unresolved token map. + The validators get the token map as input and should either return: + - An errors map .e.g: {:errors []} + - nil (valid token predicate) + Mostly used to do simple checks like invalidating empy token `:name`. + Will default to `default-validators`." + [{:keys [token-name token-value token-description prev-token tokens validators] + :or {validators default-validators}}] + (let [token (-> {:name token-name + :value token-value + :description token-description} + (d/without-nils))] + (->> (rx/of token) + ;; Simple validation of the editing token + (rx/mapcat #(validate-token-with % validators)) + ;; Resolving token via StyleDictionary + (rx/mapcat #(validate-resolve-token % prev-token tokens))))) + +(defn check-coll-self-reference + "Invalidate a collection of `token-vals` for a self-refernce against `token-name`.," + [token-name token-vals] + (when (some #(cto/token-value-self-reference? token-name %) token-vals) + (wte/get-error-code :error.token/direct-self-reference))) + + + +;; This is used in plugins + +(defn- make-token-name-schema + "Generate a dynamic schema validation to check if a token path derived + from the name already exists at `tokens-tree`." + [tokens-tree] + [:and + [:string {:min 1 :max 255 :error/fn #(str (:value %) (tr "workspace.tokens.token-name-length-validation-error"))}] + (sm/update-properties cto/token-name-ref assoc :error/fn #(str (:value %) (tr "workspace.tokens.token-name-validation-error"))) + [:fn {:error/fn #(tr "workspace.tokens.token-name-duplication-validation-error" (:value %))} + #(not (cft/token-name-path-exists? % tokens-tree))]]) + +(defn validate-token-name + [tokens-tree name] + (let [schema (make-token-name-schema tokens-tree) + explainer (sm/explainer schema)] + (-> name explainer sm/simplify not-empty))) \ No newline at end of file diff --git a/frontend/src/app/main/ui/workspace/tokens/management/group.cljs b/frontend/src/app/main/ui/workspace/tokens/management/group.cljs index d496e88f52..8dd73d5fce 100644 --- a/frontend/src/app/main/ui/workspace/tokens/management/group.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/management/group.cljs @@ -43,6 +43,7 @@ :stroke-width "stroke-size" :dimensions "expand" :sizing "expand" + :shadow "drop-shadow" "add")) (mf/defc token-group* diff --git a/frontend/src/app/plugins/format.cljs b/frontend/src/app/plugins/format.cljs index 2b892a65e1..72652d2574 100644 --- a/frontend/src/app/plugins/format.cljs +++ b/frontend/src/app/plugins/format.cljs @@ -151,8 +151,8 @@ ;; export interface Shadow { ;; id?: string; ;; style?: 'drop-shadow' | 'inner-shadow'; -;; offsetX?: number; -;; offsetY?: number; +;; offset-x?: number; +;; offset-y?: number; ;; blur?: number; ;; spread?: number; ;; hidden?: boolean; @@ -164,8 +164,8 @@ (obj/without-empty #js {:id (-> id format-id) :style (-> style format-key) - :offsetX offset-x - :offsetY offset-y + :offset-x offset-x + :offset-y offset-y :blur blur :spread spread :hidden hidden diff --git a/frontend/src/app/plugins/parser.cljs b/frontend/src/app/plugins/parser.cljs index 5d4148662d..9b8d811d1e 100644 --- a/frontend/src/app/plugins/parser.cljs +++ b/frontend/src/app/plugins/parser.cljs @@ -147,7 +147,7 @@ ;; export interface Shadow { ;; id?: string; ;; style?: 'drop-shadow' | 'inner-shadow'; -;; offsetX?: number; +;; offset--y?: number; ;; offsetY?: number; ;; blur?: number; ;; spread?: number; @@ -160,8 +160,8 @@ (d/without-nils {:id (-> (obj/get shadow "id") parse-id) :style (-> (obj/get shadow "style") parse-keyword) - :offset-x (obj/get shadow "offsetX") - :offset-y (obj/get shadow "offsetY") + :offset-x (obj/get shadow "offset-x") + :offset-y (obj/get shadow "offset-y") :blur (obj/get shadow "blur") :spread (obj/get shadow "spread") :hidden (obj/get shadow "hidden") diff --git a/frontend/src/app/plugins/tokens.cljs b/frontend/src/app/plugins/tokens.cljs index fc0d0bb11b..a3737d706d 100644 --- a/frontend/src/app/plugins/tokens.cljs +++ b/frontend/src/app/plugins/tokens.cljs @@ -13,7 +13,7 @@ [app.main.data.workspace.tokens.application :as dwta] [app.main.data.workspace.tokens.library-edit :as dwtl] [app.main.store :as st] - [app.main.ui.workspace.tokens.management.create.form :as token-form] + [app.main.ui.workspace.tokens.management.forms.validators :as form-validator] [app.main.ui.workspace.tokens.themes.create-modal :as theme-form] [app.plugins.utils :as u] [app.util.object :as obj] @@ -51,7 +51,7 @@ :set (fn [_ value] (let [tokens-lib (u/locate-tokens-lib file-id) - errors (token-form/validate-token-name + errors (form-validator/validate-token-name (ctob/get-tokens tokens-lib set-id) value)] (cond 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 9ef62e7379..0b8dd4247a 100644 --- a/frontend/test/frontend_tests/tokens/logic/token_actions_test.cljs +++ b/frontend/test/frontend_tests/tokens/logic/token_actions_test.cljs @@ -474,8 +474,8 @@ (t/async done (let [shadow-token {:name "shadow.sm" - :value [{:offsetX 10 - :offsetY 10 + :value [{:offset-x 10 + :offset-y 10 :blur 10 :spread 10 :color "rgba(0,0,0,0.5)" diff --git a/frontend/translations/en.po b/frontend/translations/en.po index 0a5c4a727f..775deb2de8 100644 --- a/frontend/translations/en.po +++ b/frontend/translations/en.po @@ -7753,6 +7753,10 @@ msgstr "Invalid token value: only none, underline and strike-through are accepte msgid "workspace.tokens.invalid-token-value-typography" msgstr "Invalid value: must reference a composite typography token." +#: src/app/main/data/workspace/tokens/errors.cljs +msgid "workspace.tokens.invalid-token-value-shadow" +msgstr "Invalid value: must reference a composite shadow token." + #: src/app/main/data/workspace/tokens/errors.cljs:61, src/app/main/data/workspace/tokens/errors.cljs:73, src/app/main/data/workspace/tokens/errors.cljs:77 msgid "workspace.tokens.invalid-value" msgstr "Invalid token value: %s" @@ -7870,6 +7874,10 @@ msgstr "Reference is not valid or is not in any active set" msgid "workspace.tokens.reference-composite" msgstr "Enter a token typography alias" +#: src/app/main/ui/workspace/tokens/management/create/form.cljs:775 +msgid "workspace.tokens.reference-composite-shadow" +msgstr "Enter a token shadow alias" + #: src/app/main/ui/workspace/tokens/style_dictionary.cljs #, unused msgid "workspace.tokens.reference-error" diff --git a/frontend/translations/es.po b/frontend/translations/es.po index e58facf94b..c0ed714dad 100644 --- a/frontend/translations/es.po +++ b/frontend/translations/es.po @@ -7674,6 +7674,10 @@ msgstr "Tipo de sombra no válida: solo se aceptan 'innerShadow' o 'dropShadow'" msgid "workspace.tokens.invalid-token-value-typography" msgstr "Valor no válido: debe hacer referencia a un token tipográfico compuesto." +#: src/app/main/data/workspace/tokens/errors.cljs +msgid "workspace.tokens.invalid-token-value-shadow" +msgstr "Valor no válido: debe hacer referencia a un token de sombra compuesto." + #: src/app/main/data/workspace/tokens/errors.cljs:61, src/app/main/data/workspace/tokens/errors.cljs:73, src/app/main/data/workspace/tokens/errors.cljs:77 msgid "workspace.tokens.invalid-value" msgstr "Valor de token no válido: %s"