mirror of
https://github.com/penpot/penpot.git
synced 2025-12-11 22:14:05 +01:00
♻️ Replace token form files (#7896)
Some checks failed
Commit Message Check / Check Commit Message (push) Has been cancelled
CI / Linter (push) Has been cancelled
CI / Common Tests (push) Has been cancelled
CI / Frontend Tests (push) Has been cancelled
CI / Render WASM Tests (push) Has been cancelled
CI / Backend Tests (push) Has been cancelled
CI / Library Tests (push) Has been cancelled
CI / Build Integration Bundle (push) Has been cancelled
CI / Integration Tests 1/4 (push) Has been cancelled
CI / Integration Tests 2/4 (push) Has been cancelled
CI / Integration Tests 3/4 (push) Has been cancelled
CI / Integration Tests 4/4 (push) Has been cancelled
_DEVELOP / build-bundle (push) Has been cancelled
_DEVELOP / build-docker (push) Has been cancelled
_STAGING / build-bundle (push) Has been cancelled
_STAGING / build-docker (push) Has been cancelled
_NITRATE MODULE / build-bundle (push) Has been cancelled
_NITRATE MODULE / build-docker (push) Has been cancelled
Some checks failed
Commit Message Check / Check Commit Message (push) Has been cancelled
CI / Linter (push) Has been cancelled
CI / Common Tests (push) Has been cancelled
CI / Frontend Tests (push) Has been cancelled
CI / Render WASM Tests (push) Has been cancelled
CI / Backend Tests (push) Has been cancelled
CI / Library Tests (push) Has been cancelled
CI / Build Integration Bundle (push) Has been cancelled
CI / Integration Tests 1/4 (push) Has been cancelled
CI / Integration Tests 2/4 (push) Has been cancelled
CI / Integration Tests 3/4 (push) Has been cancelled
CI / Integration Tests 4/4 (push) Has been cancelled
_DEVELOP / build-bundle (push) Has been cancelled
_DEVELOP / build-docker (push) Has been cancelled
_STAGING / build-bundle (push) Has been cancelled
_STAGING / build-docker (push) Has been cancelled
_NITRATE MODULE / build-bundle (push) Has been cancelled
_NITRATE MODULE / build-docker (push) Has been cancelled
* ♻️ Replace shadow form * ♻️ Rename files and components * ♻️ Replace offsetx and offsety names * ♻️ Replace form file for new form component using new form system * ♻️ Rename files and props
This commit is contained in:
@@ -1575,10 +1575,10 @@ Will return a value that matches this schema:
|
|||||||
(if (map? shadow)
|
(if (map? shadow)
|
||||||
(let [legacy-shadow-type (get "type" shadow)]
|
(let [legacy-shadow-type (get "type" shadow)]
|
||||||
(-> shadow
|
(-> shadow
|
||||||
(set/rename-keys {"x" :offsetX
|
(set/rename-keys {"x" :offset-x
|
||||||
"offsetX" :offsetX
|
"offsetX" :offset-x
|
||||||
"y" :offsetY
|
"y" :offset-y
|
||||||
"offsetY" :offsetY
|
"offsetY" :offset-y
|
||||||
"blur" :blur
|
"blur" :blur
|
||||||
"spread" :spread
|
"spread" :spread
|
||||||
"color" :color
|
"color" :color
|
||||||
@@ -1589,7 +1589,7 @@ Will return a value that matches this schema:
|
|||||||
(= "false" %) false
|
(= "false" %) false
|
||||||
(= legacy-shadow-type "innerShadow") true
|
(= legacy-shadow-type "innerShadow") true
|
||||||
:else false))
|
:else false))
|
||||||
(select-keys [:offsetX :offsetY :blur :spread :color :inset])))
|
(select-keys [:offset-x :offset-y :blur :spread :color :inset])))
|
||||||
shadow))]
|
shadow))]
|
||||||
(cond
|
(cond
|
||||||
;; Reference value - keep as string
|
;; Reference value - keep as string
|
||||||
@@ -1860,8 +1860,8 @@ Will return a value that matches this schema:
|
|||||||
(mapv (fn [shadow]
|
(mapv (fn [shadow]
|
||||||
(if (map? shadow)
|
(if (map? shadow)
|
||||||
(-> shadow
|
(-> shadow
|
||||||
(set/rename-keys {:offsetX "offsetX"
|
(set/rename-keys {:offset-x "offsetX"
|
||||||
:offsetY "offsetY"
|
:offset-y "offsetY"
|
||||||
:blur "blur"
|
:blur "blur"
|
||||||
:spread "spread"
|
:spread "spread"
|
||||||
:color "color"
|
:color "color"
|
||||||
|
|||||||
@@ -1897,15 +1897,15 @@
|
|||||||
(let [token (ctob/get-token-by-name lib "shadow-test" "test.shadow-single")]
|
(let [token (ctob/get-token-by-name lib "shadow-test" "test.shadow-single")]
|
||||||
(t/is (some? token))
|
(t/is (some? token))
|
||||||
(t/is (= :shadow (:type 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)))))
|
(:value token)))))
|
||||||
|
|
||||||
(t/testing "multiple shadow token"
|
(t/testing "multiple shadow token"
|
||||||
(let [token (ctob/get-token-by-name lib "shadow-test" "test.shadow-multiple")]
|
(let [token (ctob/get-token-by-name lib "shadow-test" "test.shadow-multiple")]
|
||||||
(t/is (some? token))
|
(t/is (some? token))
|
||||||
(t/is (= :shadow (:type token)))
|
(t/is (= :shadow (:type token)))
|
||||||
(t/is (= [{:offsetX "0", :offsetY "2px", :blur "4px", :spread "0", :color "#000", :inset true}
|
(t/is (= [{:offset-x "0", :offset-y "2px", :blur "4px", :spread "0", :color "#000", :inset true}
|
||||||
{:offsetX "0", :offsetY "8px", :blur "16px", :spread "0", :color "#000", :inset true}]
|
{:offset-x "0", :offset-y "8px", :blur "16px", :spread "0", :color "#000", :inset true}]
|
||||||
(:value token)))))
|
(:value token)))))
|
||||||
|
|
||||||
(t/testing "shadow token with reference"
|
(t/testing "shadow token with reference"
|
||||||
@@ -1918,7 +1918,7 @@
|
|||||||
(let [token (ctob/get-token-by-name lib "shadow-test" "test.shadow-with-type")]
|
(let [token (ctob/get-token-by-name lib "shadow-test" "test.shadow-with-type")]
|
||||||
(t/is (some? token))
|
(t/is (some? token))
|
||||||
(t/is (= :shadow (:type 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)))))
|
(:value token)))))
|
||||||
|
|
||||||
(t/testing "shadow token with description"
|
(t/testing "shadow token with description"
|
||||||
@@ -1937,14 +1937,14 @@
|
|||||||
(ctob/make-token
|
(ctob/make-token
|
||||||
{:name "shadow.single"
|
{:name "shadow.single"
|
||||||
:type :shadow
|
: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"})
|
:description "A single shadow"})
|
||||||
"shadow.multiple"
|
"shadow.multiple"
|
||||||
(ctob/make-token
|
(ctob/make-token
|
||||||
{:name "shadow.multiple"
|
{:name "shadow.multiple"
|
||||||
:type :shadow
|
: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"}
|
||||||
{:offsetX "0" :offsetY "8px" :blur "16px" :spread "0" :color "#0000001A"}]})
|
{:offset-x "0" :offset-y "8px" :blur "16px" :spread "0" :color "#0000001A"}]})
|
||||||
"shadow.ref"
|
"shadow.ref"
|
||||||
(ctob/make-token
|
(ctob/make-token
|
||||||
{:name "shadow.ref"
|
{:name "shadow.ref"
|
||||||
@@ -1991,7 +1991,7 @@
|
|||||||
(ctob/make-token
|
(ctob/make-token
|
||||||
{:name "shadow.test"
|
{:name "shadow.test"
|
||||||
:type :shadow
|
: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"})
|
:description "Round trip test"})
|
||||||
"shadow.ref"
|
"shadow.ref"
|
||||||
(ctob/make-token
|
(ctob/make-token
|
||||||
|
|||||||
@@ -5947,8 +5947,8 @@
|
|||||||
"~:spread": "10",
|
"~:spread": "10",
|
||||||
"~:color": "rgb(160, 73, 73)",
|
"~:color": "rgb(160, 73, 73)",
|
||||||
"~:inset": true,
|
"~:inset": true,
|
||||||
"~:offsetX": "10",
|
"~:offset-x": "10",
|
||||||
"~:offsetY": "10"
|
"~:offset-y": "10"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"~:description": "",
|
"~:description": "",
|
||||||
|
|||||||
@@ -1239,8 +1239,12 @@ test.describe("Tokens: Apply token", () => {
|
|||||||
// Fill in the shadow values
|
// Fill in the shadow values
|
||||||
const offsetXInput = firstShadowFields.getByLabel("X");
|
const offsetXInput = firstShadowFields.getByLabel("X");
|
||||||
const offsetYInput = firstShadowFields.getByLabel("Y");
|
const offsetYInput = firstShadowFields.getByLabel("Y");
|
||||||
const blurInput = firstShadowFields.getByLabel("Blur");
|
const blurInput = firstShadowFields.getByRole("textbox", {
|
||||||
const spreadInput = firstShadowFields.getByLabel("Spread");
|
name: "Blur",
|
||||||
|
});
|
||||||
|
const spreadInput = firstShadowFields.getByRole("textbox", {
|
||||||
|
name: "Spread",
|
||||||
|
});
|
||||||
|
|
||||||
await offsetXInput.fill("2");
|
await offsetXInput.fill("2");
|
||||||
await offsetYInput.fill("2");
|
await offsetYInput.fill("2");
|
||||||
@@ -1261,9 +1265,10 @@ test.describe("Tokens: Apply token", () => {
|
|||||||
await valueSaturationSelector.click({ position: { x: 50, y: 50 } });
|
await valueSaturationSelector.click({ position: { x: 50, y: 50 } });
|
||||||
|
|
||||||
// Verify that a color value was set
|
// Verify that a color value was set
|
||||||
const colorInput = firstShadowFields.getByLabel("Color");
|
const colorInput = firstShadowFields.getByRole("textbox", {
|
||||||
const firstColorValue = await colorInput.inputValue();
|
name: "Color",
|
||||||
await expect(firstColorValue).toMatch(/^rgb(.*)$/);
|
});
|
||||||
|
await expect(colorInput).toHaveValue(/^rgb(.*)$/);
|
||||||
|
|
||||||
// Wait for validation to complete
|
// Wait for validation to complete
|
||||||
await expect(
|
await expect(
|
||||||
@@ -1281,11 +1286,15 @@ test.describe("Tokens: Apply token", () => {
|
|||||||
const firstShadowFields = tokensUpdateCreateModal.getByTestId(
|
const firstShadowFields = tokensUpdateCreateModal.getByTestId(
|
||||||
"shadow-input-fields-0",
|
"shadow-input-fields-0",
|
||||||
);
|
);
|
||||||
const colorInput = firstShadowFields.getByLabel("Color");
|
const colorInput = firstShadowFields.getByRole("textbox", {
|
||||||
|
name: "Color",
|
||||||
|
});
|
||||||
const firstColorValue = await colorInput.inputValue();
|
const firstColorValue = await colorInput.inputValue();
|
||||||
|
|
||||||
// User adds a second shadow
|
// User adds a second shadow
|
||||||
const addButton = firstShadowFields.getByTestId("shadow-add-button-0");
|
const addButton = tokensUpdateCreateModal.getByRole("button", {
|
||||||
|
name: "Add Shadow",
|
||||||
|
});
|
||||||
await addButton.click();
|
await addButton.click();
|
||||||
|
|
||||||
const secondShadowFields = tokensUpdateCreateModal.getByTestId(
|
const secondShadowFields = tokensUpdateCreateModal.getByTestId(
|
||||||
@@ -1294,8 +1303,7 @@ test.describe("Tokens: Apply token", () => {
|
|||||||
await expect(secondShadowFields).toBeVisible();
|
await expect(secondShadowFields).toBeVisible();
|
||||||
|
|
||||||
// User adds a third shadow
|
// User adds a third shadow
|
||||||
const addButton2 = secondShadowFields.getByTestId("shadow-add-button-1");
|
await addButton.click();
|
||||||
await addButton2.click();
|
|
||||||
|
|
||||||
const thirdShadowFields = tokensUpdateCreateModal.getByTestId(
|
const thirdShadowFields = tokensUpdateCreateModal.getByTestId(
|
||||||
"shadow-input-fields-2",
|
"shadow-input-fields-2",
|
||||||
@@ -1305,9 +1313,15 @@ test.describe("Tokens: Apply token", () => {
|
|||||||
// User adds values for the third shadow
|
// User adds values for the third shadow
|
||||||
const thirdOffsetXInput = thirdShadowFields.getByLabel("X");
|
const thirdOffsetXInput = thirdShadowFields.getByLabel("X");
|
||||||
const thirdOffsetYInput = thirdShadowFields.getByLabel("Y");
|
const thirdOffsetYInput = thirdShadowFields.getByLabel("Y");
|
||||||
const thirdBlurInput = thirdShadowFields.getByLabel("Blur");
|
const thirdBlurInput = thirdShadowFields.getByRole("textbox", {
|
||||||
const thirdSpreadInput = thirdShadowFields.getByLabel("Spread");
|
name: "Blur",
|
||||||
const thirdColorInput = thirdShadowFields.getByLabel("Color");
|
});
|
||||||
|
const thirdSpreadInput = thirdShadowFields.getByRole("textbox", {
|
||||||
|
name: "Spread",
|
||||||
|
});
|
||||||
|
const thirdColorInput = thirdShadowFields.getByRole("textbox", {
|
||||||
|
name: "Color",
|
||||||
|
});
|
||||||
|
|
||||||
await thirdOffsetXInput.fill("10");
|
await thirdOffsetXInput.fill("10");
|
||||||
await thirdOffsetYInput.fill("10");
|
await thirdOffsetYInput.fill("10");
|
||||||
@@ -1316,15 +1330,13 @@ test.describe("Tokens: Apply token", () => {
|
|||||||
await thirdColorInput.fill("#FF0000");
|
await thirdColorInput.fill("#FF0000");
|
||||||
|
|
||||||
// User removes the 2nd shadow
|
// User removes the 2nd shadow
|
||||||
const removeButton2 = secondShadowFields.getByTestId(
|
const removeButton2 = secondShadowFields.getByRole("button", {
|
||||||
"shadow-remove-button-1",
|
name: "Remove Shadow",
|
||||||
);
|
});
|
||||||
await removeButton2.click();
|
await removeButton2.click();
|
||||||
|
|
||||||
// Verify second shadow is removed
|
// Verify that we have only two shadow fields
|
||||||
await expect(
|
await expect(thirdShadowFields).not.toBeVisible();
|
||||||
secondShadowFields.getByTestId("shadow-add-button-3"),
|
|
||||||
).not.toBeVisible();
|
|
||||||
|
|
||||||
// Verify that the first shadow kept its values
|
// Verify that the first shadow kept its values
|
||||||
const firstOffsetXValue = await firstShadowFields
|
const firstOffsetXValue = await firstShadowFields
|
||||||
@@ -1334,13 +1346,13 @@ test.describe("Tokens: Apply token", () => {
|
|||||||
.getByLabel("Y")
|
.getByLabel("Y")
|
||||||
.inputValue();
|
.inputValue();
|
||||||
const firstBlurValue = await firstShadowFields
|
const firstBlurValue = await firstShadowFields
|
||||||
.getByLabel("Blur")
|
.getByRole("textbox", { name: "Blur" })
|
||||||
.inputValue();
|
.inputValue();
|
||||||
const firstSpreadValue = await firstShadowFields
|
const firstSpreadValue = await firstShadowFields
|
||||||
.getByLabel("Spread")
|
.getByRole("textbox", { name: "Spread" })
|
||||||
.inputValue();
|
.inputValue();
|
||||||
const firstColorValueAfter = await firstShadowFields
|
const firstColorValueAfter = await firstShadowFields
|
||||||
.getByLabel("Color")
|
.getByRole("textbox", { name: "Color" })
|
||||||
.inputValue();
|
.inputValue();
|
||||||
|
|
||||||
await expect(firstOffsetXValue).toBe("2");
|
await expect(firstOffsetXValue).toBe("2");
|
||||||
@@ -1349,7 +1361,7 @@ test.describe("Tokens: Apply token", () => {
|
|||||||
await expect(firstSpreadValue).toBe("0");
|
await expect(firstSpreadValue).toBe("0");
|
||||||
await expect(firstColorValueAfter).toBe(firstColorValue);
|
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
|
// After removing index 1, the third shadow becomes the second shadow at index 1
|
||||||
const newSecondShadowFields = tokensUpdateCreateModal.getByTestId(
|
const newSecondShadowFields = tokensUpdateCreateModal.getByTestId(
|
||||||
"shadow-input-fields-1",
|
"shadow-input-fields-1",
|
||||||
@@ -1363,13 +1375,13 @@ test.describe("Tokens: Apply token", () => {
|
|||||||
.getByLabel("Y")
|
.getByLabel("Y")
|
||||||
.inputValue();
|
.inputValue();
|
||||||
const secondBlurValue = await newSecondShadowFields
|
const secondBlurValue = await newSecondShadowFields
|
||||||
.getByLabel("Blur")
|
.getByRole("textbox", { name: "Blur" })
|
||||||
.inputValue();
|
.inputValue();
|
||||||
const secondSpreadValue = await newSecondShadowFields
|
const secondSpreadValue = await newSecondShadowFields
|
||||||
.getByLabel("Spread")
|
.getByRole("textbox", { name: "Spread" })
|
||||||
.inputValue();
|
.inputValue();
|
||||||
const secondColorValue = await newSecondShadowFields
|
const secondColorValue = await newSecondShadowFields
|
||||||
.getByLabel("Color")
|
.getByRole("textbox", { name: "Color" })
|
||||||
.inputValue();
|
.inputValue();
|
||||||
|
|
||||||
await expect(secondOffsetXValue).toBe("10");
|
await expect(secondOffsetXValue).toBe("10");
|
||||||
@@ -1386,7 +1398,9 @@ test.describe("Tokens: Apply token", () => {
|
|||||||
const newSecondShadowFields = tokensUpdateCreateModal.getByTestId(
|
const newSecondShadowFields = tokensUpdateCreateModal.getByTestId(
|
||||||
"shadow-input-fields-1",
|
"shadow-input-fields-1",
|
||||||
);
|
);
|
||||||
const colorInput = firstShadowFields.getByLabel("Color");
|
const colorInput = firstShadowFields.getByRole("textbox", {
|
||||||
|
name: "Color",
|
||||||
|
});
|
||||||
const firstColorValue = await colorInput.inputValue();
|
const firstColorValue = await colorInput.inputValue();
|
||||||
|
|
||||||
// Switch to reference tab
|
// Switch to reference tab
|
||||||
@@ -1414,13 +1428,13 @@ test.describe("Tokens: Apply token", () => {
|
|||||||
.getByLabel("Y")
|
.getByLabel("Y")
|
||||||
.inputValue();
|
.inputValue();
|
||||||
const restoredFirstBlur = await firstShadowFields
|
const restoredFirstBlur = await firstShadowFields
|
||||||
.getByLabel("Blur")
|
.getByRole("textbox", { name: "Blur" })
|
||||||
.inputValue();
|
.inputValue();
|
||||||
const restoredFirstSpread = await firstShadowFields
|
const restoredFirstSpread = await firstShadowFields
|
||||||
.getByLabel("Spread")
|
.getByRole("textbox", { name: "Spread" })
|
||||||
.inputValue();
|
.inputValue();
|
||||||
const restoredFirstColor = await firstShadowFields
|
const restoredFirstColor = await firstShadowFields
|
||||||
.getByLabel("Color")
|
.getByRole("textbox", { name: "Color" })
|
||||||
.inputValue();
|
.inputValue();
|
||||||
|
|
||||||
await expect(restoredFirstOffsetX).toBe("2");
|
await expect(restoredFirstOffsetX).toBe("2");
|
||||||
@@ -1437,13 +1451,13 @@ test.describe("Tokens: Apply token", () => {
|
|||||||
.getByLabel("Y")
|
.getByLabel("Y")
|
||||||
.inputValue();
|
.inputValue();
|
||||||
const restoredSecondBlur = await newSecondShadowFields
|
const restoredSecondBlur = await newSecondShadowFields
|
||||||
.getByLabel("Blur")
|
.getByRole("textbox", { name: "Blur" })
|
||||||
.inputValue();
|
.inputValue();
|
||||||
const restoredSecondSpread = await newSecondShadowFields
|
const restoredSecondSpread = await newSecondShadowFields
|
||||||
.getByLabel("Spread")
|
.getByRole("textbox", { name: "Spread" })
|
||||||
.inputValue();
|
.inputValue();
|
||||||
const restoredSecondColor = await newSecondShadowFields
|
const restoredSecondColor = await newSecondShadowFields
|
||||||
.getByLabel("Color")
|
.getByRole("textbox", { name: "Color" })
|
||||||
.inputValue();
|
.inputValue();
|
||||||
|
|
||||||
await expect(restoredSecondOffsetX).toBe("10");
|
await expect(restoredSecondOffsetX).toBe("10");
|
||||||
|
|||||||
@@ -59,9 +59,15 @@
|
|||||||
"Parses `value` of a color `sd-token` into a map like `{:value 1 :unit \"px\"}`.
|
"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`."
|
If the value is not parseable and/or has missing references returns a map with `:errors`."
|
||||||
[value]
|
[value]
|
||||||
(if-let [tc (tinycolor/valid-color value)]
|
(let [missing-references (seq (cto/find-token-value-references value))]
|
||||||
{:value value :unit (tinycolor/color-format tc)}
|
(if-let [tc (tinycolor/valid-color value)]
|
||||||
{:errors [(wte/error-with-value :error.token/invalid-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]
|
(defn- numeric-string? [s]
|
||||||
(and (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 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`."
|
If the `value` is parseable but is out of range returns a map with `warnings`."
|
||||||
[value]
|
[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)
|
parsed-value (cft/parse-token-value value)
|
||||||
out-of-scope (not (<= 0 (:value parsed-value) 1))
|
out-of-scope (not (<= 0 (:value parsed-value) 1))
|
||||||
references (seq (cto/find-token-value-references value))]
|
references (seq (cto/find-token-value-references value))]
|
||||||
@@ -368,8 +374,8 @@
|
|||||||
(let [add-keyed-errors (fn [shadow-result k errors]
|
(let [add-keyed-errors (fn [shadow-result k errors]
|
||||||
(update shadow-result :errors concat
|
(update shadow-result :errors concat
|
||||||
(map #(assoc % :shadow-key k :shadow-index shadow-index) errors)))
|
(map #(assoc % :shadow-key k :shadow-index shadow-index) errors)))
|
||||||
parsers {:offsetX parse-sd-token-general-value
|
parsers {:offset-x parse-sd-token-general-value
|
||||||
:offsetY parse-sd-token-general-value
|
:offset-y parse-sd-token-general-value
|
||||||
:blur parse-sd-token-shadow-blur
|
:blur parse-sd-token-shadow-blur
|
||||||
:spread parse-sd-token-shadow-spread
|
:spread parse-sd-token-shadow-spread
|
||||||
:color parse-sd-token-color-value
|
:color parse-sd-token-color-value
|
||||||
@@ -389,35 +395,42 @@
|
|||||||
(defn- parse-sd-token-shadow-value
|
(defn- parse-sd-token-shadow-value
|
||||||
"Parses shadow value and validates it."
|
"Parses shadow value and validates it."
|
||||||
[value]
|
[value]
|
||||||
(cond
|
(let [missing-references
|
||||||
;; Reference value (string)
|
(when (string? value)
|
||||||
(string? value) {:value 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
|
;; 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
|
;; 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
|
;; Array of shadows
|
||||||
:else
|
:else
|
||||||
(let [converted (js->clj value :keywordize-keys true)
|
(let [converted (js->clj value :keywordize-keys true)
|
||||||
;; Parse each shadow with its index
|
;; Parse each shadow with its index
|
||||||
parsed-shadows (map-indexed
|
parsed-shadows (map-indexed
|
||||||
(fn [idx shadow-map]
|
(fn [idx shadow-map]
|
||||||
(parse-single-shadow shadow-map idx))
|
(parse-single-shadow shadow-map idx))
|
||||||
converted)
|
converted)
|
||||||
|
|
||||||
;; Collect all errors from all shadows
|
;; 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
|
;; 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)
|
(if (seq all-errors)
|
||||||
{:errors all-errors
|
{:errors all-errors
|
||||||
:value all-values}
|
:value all-values}
|
||||||
{:value all-values}))))
|
{:value all-values})))))
|
||||||
|
|
||||||
(defn collect-shadow-errors [token shadow-index]
|
(defn collect-shadow-errors [token shadow-index]
|
||||||
(group-by :shadow-key
|
(group-by :shadow-key
|
||||||
|
|||||||
@@ -153,11 +153,11 @@
|
|||||||
(defn value->shadow
|
(defn value->shadow
|
||||||
"Transform a token shadow value into penpot shadow data structure"
|
"Transform a token shadow value into penpot shadow data structure"
|
||||||
[value]
|
[value]
|
||||||
(mapv (fn [{:keys [offsetX offsetY blur spread color inset]}]
|
(mapv (fn [{:keys [offset-x offset-y blur spread color inset]}]
|
||||||
{:id (random-uuid)
|
{:id (random-uuid)
|
||||||
:hidden false
|
:hidden false
|
||||||
:offset-x offsetX
|
:offset-x offset-x
|
||||||
:offset-y offsetY
|
:offset-y offset-y
|
||||||
:blur blur
|
:blur blur
|
||||||
:color (value->color color)
|
:color (value->color color)
|
||||||
:spread spread
|
:spread spread
|
||||||
|
|||||||
@@ -108,6 +108,10 @@
|
|||||||
{:error/code :error.style-dictionary/invalid-token-value-shadow-spread
|
{:error/code :error.style-dictionary/invalid-token-value-shadow-spread
|
||||||
:error/fn #(tr "workspace.tokens.shadow-spread-range")}
|
: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/unknown
|
||||||
{:error/code :error/unknown
|
{:error/code :error/unknown
|
||||||
:error/fn #(tr "labels.unknown-error")}})
|
:error/fn #(tr "labels.unknown-error")}})
|
||||||
|
|||||||
@@ -16,8 +16,8 @@
|
|||||||
:letter-spacing "Letter Spacing"
|
:letter-spacing "Letter Spacing"
|
||||||
:text-case "Text Case"
|
:text-case "Text Case"
|
||||||
:text-decoration "Text Decoration"
|
:text-decoration "Text Decoration"
|
||||||
:offsetX "X"
|
:offset-x "X"
|
||||||
:offsetY "Y"
|
:offset-y "Y"
|
||||||
:blur "Blur"
|
:blur "Blur"
|
||||||
:spread "Spread"
|
:spread "Spread"
|
||||||
:color "Color"
|
:color "Color"
|
||||||
|
|||||||
@@ -118,6 +118,7 @@
|
|||||||
(mf/use-fn
|
(mf/use-fn
|
||||||
(mf/deps disabled)
|
(mf/deps disabled)
|
||||||
(fn [event]
|
(fn [event]
|
||||||
|
(dom/prevent-default event)
|
||||||
(dom/stop-propagation event)
|
(dom/stop-propagation event)
|
||||||
(when-not disabled
|
(when-not disabled
|
||||||
(swap! is-open* not))))
|
(swap! is-open* not))))
|
||||||
|
|||||||
@@ -53,10 +53,15 @@
|
|||||||
"true")
|
"true")
|
||||||
:aria-describedby (when has-hint
|
:aria-describedby (when has-hint
|
||||||
(str id "-hint"))
|
(str id "-hint"))
|
||||||
:aria-labelledby tooltip-id
|
|
||||||
:type (d/nilv type "text")
|
:type (d/nilv type "text")
|
||||||
:id id
|
:id id
|
||||||
:max-length (d/nilv max-length max-input-length)})
|
: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
|
inside-class (stl/css-case :input-wrapper true
|
||||||
:has-hint has-hint
|
:has-hint has-hint
|
||||||
:hint-type-hint (= hint-type "hint")
|
:hint-type-hint (= hint-type "hint")
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.swatch {
|
.swatch {
|
||||||
--border-color: var(--color-background-quaternary);
|
--border-color: var(--color-accent-primary-muted);
|
||||||
--border-radius: #{$br-4};
|
--border-radius: #{$br-4};
|
||||||
--border-color-active: var(--color-foreground-primary);
|
--border-color-active: var(--color-foreground-primary);
|
||||||
--border-color-active-inset: var(--color-background-primary);
|
--border-color-active-inset: var(--color-background-primary);
|
||||||
|
|||||||
@@ -35,7 +35,7 @@
|
|||||||
[app.main.ui.workspace.tokens.export.modal]
|
[app.main.ui.workspace.tokens.export.modal]
|
||||||
[app.main.ui.workspace.tokens.import]
|
[app.main.ui.workspace.tokens.import]
|
||||||
[app.main.ui.workspace.tokens.import.modal]
|
[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.settings]
|
||||||
[app.main.ui.workspace.tokens.themes.create-modal]
|
[app.main.ui.workspace.tokens.themes.create-modal]
|
||||||
[app.main.ui.workspace.viewport :refer [viewport*]]
|
[app.main.ui.workspace.viewport :refer [viewport*]]
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -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")]]]]))
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -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")]]]]))
|
|
||||||
@@ -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")]]]]))
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -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);
|
|
||||||
}
|
|
||||||
@@ -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}])]))
|
|
||||||
@@ -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]))
|
|
||||||
@@ -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)}])])
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -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}])]))
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -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")]]]]))
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -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")]]]]))
|
|
||||||
@@ -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]))
|
||||||
@@ -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*)
|
||||||
@@ -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 <subfield> <index> :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}])]))
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
;;
|
;;
|
||||||
;; Copyright (c) KALEIDOS INC
|
;; 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-macros [app.main.style :as stl])
|
||||||
(:require
|
(:require
|
||||||
[app.common.data :as d]
|
[app.common.data :as d]
|
||||||
@@ -24,6 +24,29 @@
|
|||||||
[cuerdas.core :as str]
|
[cuerdas.core :as str]
|
||||||
[rumext.v2 :as mf]))
|
[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
|
(defn- resolve-value
|
||||||
[tokens prev-token value]
|
[tokens prev-token value]
|
||||||
@@ -46,7 +69,7 @@
|
|||||||
(rx/of {:value resolved-value})
|
(rx/of {:value resolved-value})
|
||||||
(rx/of {:error (first errors)}))))))))
|
(rx/of {:error (first errors)}))))))))
|
||||||
|
|
||||||
(mf/defc font-picker-combobox*
|
(mf/defc fonts-combobox*
|
||||||
[{:keys [token tokens name] :rest props}]
|
[{:keys [token tokens name] :rest props}]
|
||||||
(let [form (mf/use-ctx fc/context)
|
(let [form (mf/use-ctx fc/context)
|
||||||
input-name name
|
input-name name
|
||||||
@@ -117,7 +140,6 @@
|
|||||||
|
|
||||||
props
|
props
|
||||||
(mf/spread-props props {:on-change on-change
|
(mf/spread-props props {:on-change on-change
|
||||||
;; TODO: Review this value vs default-value
|
|
||||||
:value (or value "")
|
:value (or value "")
|
||||||
:hint-message (:message hint)
|
:hint-message (:message hint)
|
||||||
:slot-end font-selector-button
|
:slot-end font-selector-button
|
||||||
@@ -174,7 +196,7 @@
|
|||||||
(update :errors clean-errors)
|
(update :errors clean-errors)
|
||||||
(update :extra-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}]
|
[{:keys [token tokens name] :rest props}]
|
||||||
(let [form (mf/use-ctx fc/context)
|
(let [form (mf/use-ctx fc/context)
|
||||||
input-name name
|
input-name name
|
||||||
@@ -242,7 +264,6 @@
|
|||||||
|
|
||||||
props
|
props
|
||||||
(mf/spread-props props {:on-change on-change
|
(mf/spread-props props {:on-change on-change
|
||||||
;; TODO: Review this value vs default-value
|
|
||||||
:value (or value "")
|
:value (or value "")
|
||||||
:hint-message (:message hint)
|
:hint-message (:message hint)
|
||||||
:slot-end font-selector-button
|
:slot-end font-selector-button
|
||||||
@@ -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]))
|
||||||
@@ -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 <subfield> <index> <field>]
|
||||||
|
;; - 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]))
|
||||||
@@ -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]))
|
||||||
@@ -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])))
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
;;
|
;;
|
||||||
;; Copyright (c) KALEIDOS INC
|
;; 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-macros [app.main.style :as stl])
|
||||||
(:require
|
(:require
|
||||||
[app.common.files.tokens :as cft]
|
[app.common.files.tokens :as cft]
|
||||||
@@ -23,7 +23,8 @@
|
|||||||
[app.main.ui.ds.foundations.typography.heading :refer [heading*]]
|
[app.main.ui.ds.foundations.typography.heading :refer [heading*]]
|
||||||
[app.main.ui.ds.notifications.context-notification :refer [context-notification*]]
|
[app.main.ui.ds.notifications.context-notification :refer [context-notification*]]
|
||||||
[app.main.ui.forms :as fc]
|
[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.dom :as dom]
|
||||||
[app.util.forms :as fm]
|
[app.util.forms :as fm]
|
||||||
[app.util.i18n :refer [tr]]
|
[app.util.i18n :refer [tr]]
|
||||||
@@ -38,8 +39,25 @@
|
|||||||
(str/blank? value))
|
(str/blank? value))
|
||||||
(tr "workspace.tokens.empty-input")))
|
(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
|
(sm/schema
|
||||||
[:and
|
[:and
|
||||||
[:map
|
[:map
|
||||||
@@ -62,14 +80,37 @@
|
|||||||
(nil? (cto/token-value-self-reference? name value))))]]))
|
(nil? (cto/token-value-self-reference? name value))))]]))
|
||||||
|
|
||||||
(mf/defc form*
|
(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]
|
(mf/with-memo [token]
|
||||||
(or token {:type :border-radius}))
|
(or token {:type token-type}))
|
||||||
|
|
||||||
token-type
|
|
||||||
(get token :type)
|
|
||||||
|
|
||||||
token-properties
|
token-properties
|
||||||
(dwta/get-token-properties token)
|
(dwta/get-token-properties token)
|
||||||
@@ -89,14 +130,15 @@
|
|||||||
(assoc (:name token) token)))
|
(assoc (:name token) token)))
|
||||||
|
|
||||||
schema
|
schema
|
||||||
(mf/with-memo [tokens-tree-in-selected-set]
|
(mf/with-memo [tokens-tree-in-selected-set active-tab]
|
||||||
(make-schema tokens-tree-in-selected-set))
|
(make-schema tokens-tree-in-selected-set active-tab))
|
||||||
|
|
||||||
initial
|
initial
|
||||||
(mf/with-memo [token]
|
(mf/with-memo [token]
|
||||||
{:name (:name token "")
|
(or initial
|
||||||
:value (:value token "")
|
{:name (:name token "")
|
||||||
:description (:description token "")})
|
:value (:value token "")
|
||||||
|
:description (:description token "")}))
|
||||||
|
|
||||||
form
|
form
|
||||||
(fm/use-form :schema schema
|
(fm/use-form :schema schema
|
||||||
@@ -136,12 +178,13 @@
|
|||||||
|
|
||||||
on-submit
|
on-submit
|
||||||
(mf/use-fn
|
(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]
|
(fn [form _event]
|
||||||
(let [name (get-in @form [:clean-data :name])
|
(let [name (get-in @form [:clean-data :name])
|
||||||
description (get-in @form [:clean-data :description])
|
description (get-in @form [:clean-data :description])
|
||||||
value (get-in @form [:clean-data :value])]
|
value (get-in @form [:clean-data :value])
|
||||||
(->> (validate-token {:token-value 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-name name
|
||||||
:token-description description
|
:token-description description
|
||||||
:prev-token token
|
:prev-token token
|
||||||
@@ -168,7 +211,9 @@
|
|||||||
[:div {:class (stl/css :token-rows)}
|
[:div {:class (stl/css :token-rows)}
|
||||||
|
|
||||||
[:> heading* {:level 2 :typography "headline-medium" :class (stl/css :form-modal-title)}
|
[:> 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)}
|
[:div {:class (stl/css :input-row)}
|
||||||
[:> fc/form-input* {:id "token-name"
|
[:> fc/form-input* {:id "token-name"
|
||||||
@@ -185,12 +230,16 @@
|
|||||||
{:level :warning :appearance :ghost} (tr "workspace.tokens.warning-name-change")]])]
|
{:level :warning :appearance :ghost} (tr "workspace.tokens.warning-name-change")]])]
|
||||||
|
|
||||||
[:div {:class (stl/css :input-row)}
|
[:div {:class (stl/css :input-row)}
|
||||||
[:> form-input-token*
|
[:> input-component
|
||||||
{:placeholder (tr "workspace.tokens.token-value-enter")
|
{:placeholder (or input-value-placeholder (tr "workspace.tokens.token-value-enter"))
|
||||||
:label (tr "workspace.tokens.token-value")
|
:label (tr "workspace.tokens.token-value")
|
||||||
:name :value
|
:name :value
|
||||||
|
:form form
|
||||||
:token token
|
:token token
|
||||||
:tokens tokens}]]
|
:tokens tokens
|
||||||
|
:tab active-tab
|
||||||
|
:subfield value-subfield
|
||||||
|
:toggle on-toggle-tab}]]
|
||||||
|
|
||||||
[:div {:class (stl/css :input-row)}
|
[:div {:class (stl/css :input-row)}
|
||||||
[:> fc/form-input* {:id "token-description"
|
[:> fc/form-input* {:id "token-description"
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
;;
|
;;
|
||||||
;; Copyright (c) KALEIDOS INC
|
;; 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-macros [app.main.style :as stl])
|
||||||
(:require
|
(:require
|
||||||
[app.common.data.macros :as dm]
|
[app.common.data.macros :as dm]
|
||||||
@@ -13,22 +13,28 @@
|
|||||||
[app.main.refs :as refs]
|
[app.main.refs :as refs]
|
||||||
[app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
|
[app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
|
||||||
[app.main.ui.ds.foundations.assets.icon :as i]
|
[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]]
|
[app.util.i18n :refer [tr]]
|
||||||
[okulary.core :as l]
|
[okulary.core :as l]
|
||||||
[rumext.v2 :as mf]))
|
[rumext.v2 :as mf]))
|
||||||
|
|
||||||
;; Component -------------------------------------------------------------------
|
;; Component -------------------------------------------------------------------
|
||||||
|
|
||||||
(defn calculate-position
|
(defn- calculate-position
|
||||||
"Calculates the style properties for the given coordinates and position"
|
"Calculates the style properties for the given coordinates and position"
|
||||||
[{vh :height} position x y color?]
|
[{vh :height} position x y token-type]
|
||||||
(let [;; picker height in pixels
|
(let [; TODO: Revisit these harcoded values
|
||||||
;; TODO: Revisit these harcoded values
|
modal-height (case token-type
|
||||||
h (if color? 610 510)
|
:color
|
||||||
|
500
|
||||||
|
:typography
|
||||||
|
660
|
||||||
|
:shadow
|
||||||
|
660
|
||||||
|
400)
|
||||||
;; Checks for overflow outside the viewport height
|
;; Checks for overflow outside the viewport height
|
||||||
max-y (- vh h)
|
max-y (- vh modal-height)
|
||||||
overflow-fix (max 0 (+ y (- 50) h (- vh)))
|
overflow-fix (max 0 (+ y (- 50) modal-height (- vh)))
|
||||||
bottom-offset "1rem"
|
bottom-offset "1rem"
|
||||||
top-offset (dm/str (- y 70) "px")
|
top-offset (dm/str (- y 70) "px")
|
||||||
max-height-top (str "calc(100vh - " top-offset)
|
max-height-top (str "calc(100vh - " top-offset)
|
||||||
@@ -61,17 +67,19 @@
|
|||||||
:top (dm/str (- y 70 overflow-fix) "px")
|
:top (dm/str (- y 70 overflow-fix) "px")
|
||||||
:maxHeight max-height-top}))))
|
: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)
|
(let [vport (-> (l/derived :vport refs/workspace-local)
|
||||||
(mf/deref))]
|
(mf/deref))]
|
||||||
(-> (calculate-position vport position x y color?)
|
(-> (calculate-position vport position x y token-type)
|
||||||
(clj->js))))
|
(clj->js))))
|
||||||
|
|
||||||
(mf/defc token-update-create-modal
|
(mf/defc token-update-create-modal
|
||||||
{::mf/wrap-props false}
|
{::mf/wrap-props false}
|
||||||
[{:keys [x y position token token-type action selected-token-set-id] :as _args}]
|
[{:keys [x y position token token-type action selected-token-set-id] :as _args}]
|
||||||
(let [wrapper-style (use-viewport-position-style x y position (= token-type :color))
|
(let [wrapper-style (use-viewport-position-style x y position token-type)
|
||||||
modal-size-large* (mf/use-state (= token-type :typography))
|
modal-size-large* (mf/use-state (or (= token-type :typography)
|
||||||
|
(= token-type :color)
|
||||||
|
(= token-type :shadow)))
|
||||||
modal-size-large? (deref modal-size-large*)
|
modal-size-large? (deref modal-size-large*)
|
||||||
close-modal (mf/use-fn
|
close-modal (mf/use-fn
|
||||||
(fn []
|
(fn []
|
||||||
@@ -89,12 +97,12 @@
|
|||||||
:icon i/close
|
:icon i/close
|
||||||
:variant "action"
|
:variant "action"
|
||||||
:aria-label (tr "labels.close")}]
|
:aria-label (tr "labels.close")}]
|
||||||
[:> form-wrapper* {:is-create (not (ctob/token? token))
|
[:> form-container* {:is-create (not (ctob/token? token))
|
||||||
:token token
|
:token token
|
||||||
:action action
|
:action action
|
||||||
:selected-token-set-id selected-token-set-id
|
:selected-token-set-id selected-token-set-id
|
||||||
:token-type token-type
|
:token-type token-type
|
||||||
:on-display-colorpicker update-modal-size}]]))
|
:on-display-colorpicker update-modal-size}]]))
|
||||||
|
|
||||||
;; Modals ----------------------------------------------------------------------
|
;; Modals ----------------------------------------------------------------------
|
||||||
|
|
||||||
@@ -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]))
|
||||||
@@ -8,17 +8,6 @@
|
|||||||
@use "ds/_sizes.scss" as *;
|
@use "ds/_sizes.scss" as *;
|
||||||
@use "ds/_borders.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 {
|
.inputs-wrapper {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -34,9 +23,19 @@
|
|||||||
gap: var(--sp-xs);
|
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 {
|
.title-bar {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr auto;
|
grid-template-columns: 1fr auto auto;
|
||||||
|
gap: var(--sp-xs);
|
||||||
}
|
}
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
@@ -46,29 +45,17 @@
|
|||||||
align-items: center;
|
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 {
|
.delete-btn {
|
||||||
justify-self: start;
|
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;
|
||||||
|
}
|
||||||
@@ -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]))
|
||||||
@@ -8,18 +8,16 @@
|
|||||||
@use "ds/_sizes.scss" as *;
|
@use "ds/_sizes.scss" as *;
|
||||||
@use "ds/_borders.scss" as *;
|
@use "ds/_borders.scss" as *;
|
||||||
|
|
||||||
.form-wrapper {
|
.inputs-wrapper {
|
||||||
width: $sz-384;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.token-rows {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
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 {
|
.input-row {
|
||||||
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: var(--sp-xs);
|
gap: var(--sp-xs);
|
||||||
@@ -30,29 +28,9 @@
|
|||||||
grid-template-columns: 1fr auto;
|
grid-template-columns: 1fr auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-modal-title {
|
.title {
|
||||||
@include t.use-typography("headline-medium");
|
@include t.use-typography("body-small");
|
||||||
color: var(--color-foreground-primary);
|
color: var(--color-foreground-primary);
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
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;
|
|
||||||
}
|
|
||||||
@@ -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)))
|
||||||
@@ -43,6 +43,7 @@
|
|||||||
:stroke-width "stroke-size"
|
:stroke-width "stroke-size"
|
||||||
:dimensions "expand"
|
:dimensions "expand"
|
||||||
:sizing "expand"
|
:sizing "expand"
|
||||||
|
:shadow "drop-shadow"
|
||||||
"add"))
|
"add"))
|
||||||
|
|
||||||
(mf/defc token-group*
|
(mf/defc token-group*
|
||||||
|
|||||||
@@ -151,8 +151,8 @@
|
|||||||
;; export interface Shadow {
|
;; export interface Shadow {
|
||||||
;; id?: string;
|
;; id?: string;
|
||||||
;; style?: 'drop-shadow' | 'inner-shadow';
|
;; style?: 'drop-shadow' | 'inner-shadow';
|
||||||
;; offsetX?: number;
|
;; offset-x?: number;
|
||||||
;; offsetY?: number;
|
;; offset-y?: number;
|
||||||
;; blur?: number;
|
;; blur?: number;
|
||||||
;; spread?: number;
|
;; spread?: number;
|
||||||
;; hidden?: boolean;
|
;; hidden?: boolean;
|
||||||
@@ -164,8 +164,8 @@
|
|||||||
(obj/without-empty
|
(obj/without-empty
|
||||||
#js {:id (-> id format-id)
|
#js {:id (-> id format-id)
|
||||||
:style (-> style format-key)
|
:style (-> style format-key)
|
||||||
:offsetX offset-x
|
:offset-x offset-x
|
||||||
:offsetY offset-y
|
:offset-y offset-y
|
||||||
:blur blur
|
:blur blur
|
||||||
:spread spread
|
:spread spread
|
||||||
:hidden hidden
|
:hidden hidden
|
||||||
|
|||||||
@@ -147,7 +147,7 @@
|
|||||||
;; export interface Shadow {
|
;; export interface Shadow {
|
||||||
;; id?: string;
|
;; id?: string;
|
||||||
;; style?: 'drop-shadow' | 'inner-shadow';
|
;; style?: 'drop-shadow' | 'inner-shadow';
|
||||||
;; offsetX?: number;
|
;; offset--y?: number;
|
||||||
;; offsetY?: number;
|
;; offsetY?: number;
|
||||||
;; blur?: number;
|
;; blur?: number;
|
||||||
;; spread?: number;
|
;; spread?: number;
|
||||||
@@ -160,8 +160,8 @@
|
|||||||
(d/without-nils
|
(d/without-nils
|
||||||
{:id (-> (obj/get shadow "id") parse-id)
|
{:id (-> (obj/get shadow "id") parse-id)
|
||||||
:style (-> (obj/get shadow "style") parse-keyword)
|
:style (-> (obj/get shadow "style") parse-keyword)
|
||||||
:offset-x (obj/get shadow "offsetX")
|
:offset-x (obj/get shadow "offset-x")
|
||||||
:offset-y (obj/get shadow "offsetY")
|
:offset-y (obj/get shadow "offset-y")
|
||||||
:blur (obj/get shadow "blur")
|
:blur (obj/get shadow "blur")
|
||||||
:spread (obj/get shadow "spread")
|
:spread (obj/get shadow "spread")
|
||||||
:hidden (obj/get shadow "hidden")
|
:hidden (obj/get shadow "hidden")
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
[app.main.data.workspace.tokens.application :as dwta]
|
[app.main.data.workspace.tokens.application :as dwta]
|
||||||
[app.main.data.workspace.tokens.library-edit :as dwtl]
|
[app.main.data.workspace.tokens.library-edit :as dwtl]
|
||||||
[app.main.store :as st]
|
[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.main.ui.workspace.tokens.themes.create-modal :as theme-form]
|
||||||
[app.plugins.utils :as u]
|
[app.plugins.utils :as u]
|
||||||
[app.util.object :as obj]
|
[app.util.object :as obj]
|
||||||
@@ -51,7 +51,7 @@
|
|||||||
:set
|
:set
|
||||||
(fn [_ value]
|
(fn [_ value]
|
||||||
(let [tokens-lib (u/locate-tokens-lib file-id)
|
(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)
|
(ctob/get-tokens tokens-lib set-id)
|
||||||
value)]
|
value)]
|
||||||
(cond
|
(cond
|
||||||
|
|||||||
@@ -474,8 +474,8 @@
|
|||||||
(t/async
|
(t/async
|
||||||
done
|
done
|
||||||
(let [shadow-token {:name "shadow.sm"
|
(let [shadow-token {:name "shadow.sm"
|
||||||
:value [{:offsetX 10
|
:value [{:offset-x 10
|
||||||
:offsetY 10
|
:offset-y 10
|
||||||
:blur 10
|
:blur 10
|
||||||
:spread 10
|
:spread 10
|
||||||
:color "rgba(0,0,0,0.5)"
|
:color "rgba(0,0,0,0.5)"
|
||||||
|
|||||||
@@ -7753,6 +7753,10 @@ msgstr "Invalid token value: only none, underline and strike-through are accepte
|
|||||||
msgid "workspace.tokens.invalid-token-value-typography"
|
msgid "workspace.tokens.invalid-token-value-typography"
|
||||||
msgstr "Invalid value: must reference a composite typography token."
|
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
|
#: 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"
|
msgid "workspace.tokens.invalid-value"
|
||||||
msgstr "Invalid token value: %s"
|
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"
|
msgid "workspace.tokens.reference-composite"
|
||||||
msgstr "Enter a token typography alias"
|
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
|
#: src/app/main/ui/workspace/tokens/style_dictionary.cljs
|
||||||
#, unused
|
#, unused
|
||||||
msgid "workspace.tokens.reference-error"
|
msgid "workspace.tokens.reference-error"
|
||||||
|
|||||||
@@ -7674,6 +7674,10 @@ msgstr "Tipo de sombra no válida: solo se aceptan 'innerShadow' o 'dropShadow'"
|
|||||||
msgid "workspace.tokens.invalid-token-value-typography"
|
msgid "workspace.tokens.invalid-token-value-typography"
|
||||||
msgstr "Valor no válido: debe hacer referencia a un token tipográfico compuesto."
|
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
|
#: 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"
|
msgid "workspace.tokens.invalid-value"
|
||||||
msgstr "Valor de token no válido: %s"
|
msgstr "Valor de token no válido: %s"
|
||||||
|
|||||||
Reference in New Issue
Block a user