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)
|
||||
(let [legacy-shadow-type (get "type" shadow)]
|
||||
(-> shadow
|
||||
(set/rename-keys {"x" :offsetX
|
||||
"offsetX" :offsetX
|
||||
"y" :offsetY
|
||||
"offsetY" :offsetY
|
||||
(set/rename-keys {"x" :offset-x
|
||||
"offsetX" :offset-x
|
||||
"y" :offset-y
|
||||
"offsetY" :offset-y
|
||||
"blur" :blur
|
||||
"spread" :spread
|
||||
"color" :color
|
||||
@@ -1589,7 +1589,7 @@ Will return a value that matches this schema:
|
||||
(= "false" %) false
|
||||
(= legacy-shadow-type "innerShadow") true
|
||||
:else false))
|
||||
(select-keys [:offsetX :offsetY :blur :spread :color :inset])))
|
||||
(select-keys [:offset-x :offset-y :blur :spread :color :inset])))
|
||||
shadow))]
|
||||
(cond
|
||||
;; Reference value - keep as string
|
||||
@@ -1860,8 +1860,8 @@ Will return a value that matches this schema:
|
||||
(mapv (fn [shadow]
|
||||
(if (map? shadow)
|
||||
(-> shadow
|
||||
(set/rename-keys {:offsetX "offsetX"
|
||||
:offsetY "offsetY"
|
||||
(set/rename-keys {:offset-x "offsetX"
|
||||
:offset-y "offsetY"
|
||||
:blur "blur"
|
||||
:spread "spread"
|
||||
:color "color"
|
||||
|
||||
@@ -1897,15 +1897,15 @@
|
||||
(let [token (ctob/get-token-by-name lib "shadow-test" "test.shadow-single")]
|
||||
(t/is (some? token))
|
||||
(t/is (= :shadow (:type token)))
|
||||
(t/is (= [{:offsetX "0", :offsetY "2px", :blur "4px", :spread "0", :color "#000", :inset false}]
|
||||
(t/is (= [{:offset-x "0", :offset-y "2px", :blur "4px", :spread "0", :color "#000", :inset false}]
|
||||
(:value token)))))
|
||||
|
||||
(t/testing "multiple shadow token"
|
||||
(let [token (ctob/get-token-by-name lib "shadow-test" "test.shadow-multiple")]
|
||||
(t/is (some? token))
|
||||
(t/is (= :shadow (:type token)))
|
||||
(t/is (= [{:offsetX "0", :offsetY "2px", :blur "4px", :spread "0", :color "#000", :inset true}
|
||||
{:offsetX "0", :offsetY "8px", :blur "16px", :spread "0", :color "#000", :inset true}]
|
||||
(t/is (= [{:offset-x "0", :offset-y "2px", :blur "4px", :spread "0", :color "#000", :inset true}
|
||||
{:offset-x "0", :offset-y "8px", :blur "16px", :spread "0", :color "#000", :inset true}]
|
||||
(:value token)))))
|
||||
|
||||
(t/testing "shadow token with reference"
|
||||
@@ -1918,7 +1918,7 @@
|
||||
(let [token (ctob/get-token-by-name lib "shadow-test" "test.shadow-with-type")]
|
||||
(t/is (some? token))
|
||||
(t/is (= :shadow (:type token)))
|
||||
(t/is (= [{:offsetX "0", :offsetY "4px", :blur "8px", :spread "0", :color "rgba(0,0,0,0.2)", :inset false}]
|
||||
(t/is (= [{:offset-x "0", :offset-y "4px", :blur "8px", :spread "0", :color "rgba(0,0,0,0.2)", :inset false}]
|
||||
(:value token)))))
|
||||
|
||||
(t/testing "shadow token with description"
|
||||
@@ -1937,14 +1937,14 @@
|
||||
(ctob/make-token
|
||||
{:name "shadow.single"
|
||||
:type :shadow
|
||||
:value [{:offsetX "0" :offsetY "2px" :blur "4px" :spread "0" :color "#0000001A"}]
|
||||
:value [{:offset-x "0" :offset-y "2px" :blur "4px" :spread "0" :color "#0000001A"}]
|
||||
:description "A single shadow"})
|
||||
"shadow.multiple"
|
||||
(ctob/make-token
|
||||
{:name "shadow.multiple"
|
||||
:type :shadow
|
||||
:value [{:offsetX "0" :offsetY "2px" :blur "4px" :spread "0" :color "#0000001A"}
|
||||
{:offsetX "0" :offsetY "8px" :blur "16px" :spread "0" :color "#0000001A"}]})
|
||||
:value [{:offset-x "0" :offset-y "2px" :blur "4px" :spread "0" :color "#0000001A"}
|
||||
{:offset-x "0" :offset-y "8px" :blur "16px" :spread "0" :color "#0000001A"}]})
|
||||
"shadow.ref"
|
||||
(ctob/make-token
|
||||
{:name "shadow.ref"
|
||||
@@ -1991,7 +1991,7 @@
|
||||
(ctob/make-token
|
||||
{:name "shadow.test"
|
||||
:type :shadow
|
||||
:value [{:offsetX "1" :offsetY "1" :blur "1" :spread "1" :color "red" :inset true}]
|
||||
:value [{:offset-x "1" :offset-y "1" :blur "1" :spread "1" :color "red" :inset true}]
|
||||
:description "Round trip test"})
|
||||
"shadow.ref"
|
||||
(ctob/make-token
|
||||
|
||||
@@ -5947,8 +5947,8 @@
|
||||
"~:spread": "10",
|
||||
"~:color": "rgb(160, 73, 73)",
|
||||
"~:inset": true,
|
||||
"~:offsetX": "10",
|
||||
"~:offsetY": "10"
|
||||
"~:offset-x": "10",
|
||||
"~:offset-y": "10"
|
||||
}
|
||||
],
|
||||
"~:description": "",
|
||||
|
||||
@@ -1239,8 +1239,12 @@ test.describe("Tokens: Apply token", () => {
|
||||
// Fill in the shadow values
|
||||
const offsetXInput = firstShadowFields.getByLabel("X");
|
||||
const offsetYInput = firstShadowFields.getByLabel("Y");
|
||||
const blurInput = firstShadowFields.getByLabel("Blur");
|
||||
const spreadInput = firstShadowFields.getByLabel("Spread");
|
||||
const blurInput = firstShadowFields.getByRole("textbox", {
|
||||
name: "Blur",
|
||||
});
|
||||
const spreadInput = firstShadowFields.getByRole("textbox", {
|
||||
name: "Spread",
|
||||
});
|
||||
|
||||
await offsetXInput.fill("2");
|
||||
await offsetYInput.fill("2");
|
||||
@@ -1261,9 +1265,10 @@ test.describe("Tokens: Apply token", () => {
|
||||
await valueSaturationSelector.click({ position: { x: 50, y: 50 } });
|
||||
|
||||
// Verify that a color value was set
|
||||
const colorInput = firstShadowFields.getByLabel("Color");
|
||||
const firstColorValue = await colorInput.inputValue();
|
||||
await expect(firstColorValue).toMatch(/^rgb(.*)$/);
|
||||
const colorInput = firstShadowFields.getByRole("textbox", {
|
||||
name: "Color",
|
||||
});
|
||||
await expect(colorInput).toHaveValue(/^rgb(.*)$/);
|
||||
|
||||
// Wait for validation to complete
|
||||
await expect(
|
||||
@@ -1281,11 +1286,15 @@ test.describe("Tokens: Apply token", () => {
|
||||
const firstShadowFields = tokensUpdateCreateModal.getByTestId(
|
||||
"shadow-input-fields-0",
|
||||
);
|
||||
const colorInput = firstShadowFields.getByLabel("Color");
|
||||
const colorInput = firstShadowFields.getByRole("textbox", {
|
||||
name: "Color",
|
||||
});
|
||||
const firstColorValue = await colorInput.inputValue();
|
||||
|
||||
// User adds a second shadow
|
||||
const addButton = firstShadowFields.getByTestId("shadow-add-button-0");
|
||||
const addButton = tokensUpdateCreateModal.getByRole("button", {
|
||||
name: "Add Shadow",
|
||||
});
|
||||
await addButton.click();
|
||||
|
||||
const secondShadowFields = tokensUpdateCreateModal.getByTestId(
|
||||
@@ -1294,8 +1303,7 @@ test.describe("Tokens: Apply token", () => {
|
||||
await expect(secondShadowFields).toBeVisible();
|
||||
|
||||
// User adds a third shadow
|
||||
const addButton2 = secondShadowFields.getByTestId("shadow-add-button-1");
|
||||
await addButton2.click();
|
||||
await addButton.click();
|
||||
|
||||
const thirdShadowFields = tokensUpdateCreateModal.getByTestId(
|
||||
"shadow-input-fields-2",
|
||||
@@ -1305,9 +1313,15 @@ test.describe("Tokens: Apply token", () => {
|
||||
// User adds values for the third shadow
|
||||
const thirdOffsetXInput = thirdShadowFields.getByLabel("X");
|
||||
const thirdOffsetYInput = thirdShadowFields.getByLabel("Y");
|
||||
const thirdBlurInput = thirdShadowFields.getByLabel("Blur");
|
||||
const thirdSpreadInput = thirdShadowFields.getByLabel("Spread");
|
||||
const thirdColorInput = thirdShadowFields.getByLabel("Color");
|
||||
const thirdBlurInput = thirdShadowFields.getByRole("textbox", {
|
||||
name: "Blur",
|
||||
});
|
||||
const thirdSpreadInput = thirdShadowFields.getByRole("textbox", {
|
||||
name: "Spread",
|
||||
});
|
||||
const thirdColorInput = thirdShadowFields.getByRole("textbox", {
|
||||
name: "Color",
|
||||
});
|
||||
|
||||
await thirdOffsetXInput.fill("10");
|
||||
await thirdOffsetYInput.fill("10");
|
||||
@@ -1316,15 +1330,13 @@ test.describe("Tokens: Apply token", () => {
|
||||
await thirdColorInput.fill("#FF0000");
|
||||
|
||||
// User removes the 2nd shadow
|
||||
const removeButton2 = secondShadowFields.getByTestId(
|
||||
"shadow-remove-button-1",
|
||||
);
|
||||
const removeButton2 = secondShadowFields.getByRole("button", {
|
||||
name: "Remove Shadow",
|
||||
});
|
||||
await removeButton2.click();
|
||||
|
||||
// Verify second shadow is removed
|
||||
await expect(
|
||||
secondShadowFields.getByTestId("shadow-add-button-3"),
|
||||
).not.toBeVisible();
|
||||
// Verify that we have only two shadow fields
|
||||
await expect(thirdShadowFields).not.toBeVisible();
|
||||
|
||||
// Verify that the first shadow kept its values
|
||||
const firstOffsetXValue = await firstShadowFields
|
||||
@@ -1334,13 +1346,13 @@ test.describe("Tokens: Apply token", () => {
|
||||
.getByLabel("Y")
|
||||
.inputValue();
|
||||
const firstBlurValue = await firstShadowFields
|
||||
.getByLabel("Blur")
|
||||
.getByRole("textbox", { name: "Blur" })
|
||||
.inputValue();
|
||||
const firstSpreadValue = await firstShadowFields
|
||||
.getByLabel("Spread")
|
||||
.getByRole("textbox", { name: "Spread" })
|
||||
.inputValue();
|
||||
const firstColorValueAfter = await firstShadowFields
|
||||
.getByLabel("Color")
|
||||
.getByRole("textbox", { name: "Color" })
|
||||
.inputValue();
|
||||
|
||||
await expect(firstOffsetXValue).toBe("2");
|
||||
@@ -1349,7 +1361,7 @@ test.describe("Tokens: Apply token", () => {
|
||||
await expect(firstSpreadValue).toBe("0");
|
||||
await expect(firstColorValueAfter).toBe(firstColorValue);
|
||||
|
||||
// Verify that the third shadow (now second) kept its values
|
||||
// Verify that the second kept its values (after shadow 3)
|
||||
// After removing index 1, the third shadow becomes the second shadow at index 1
|
||||
const newSecondShadowFields = tokensUpdateCreateModal.getByTestId(
|
||||
"shadow-input-fields-1",
|
||||
@@ -1363,13 +1375,13 @@ test.describe("Tokens: Apply token", () => {
|
||||
.getByLabel("Y")
|
||||
.inputValue();
|
||||
const secondBlurValue = await newSecondShadowFields
|
||||
.getByLabel("Blur")
|
||||
.getByRole("textbox", { name: "Blur" })
|
||||
.inputValue();
|
||||
const secondSpreadValue = await newSecondShadowFields
|
||||
.getByLabel("Spread")
|
||||
.getByRole("textbox", { name: "Spread" })
|
||||
.inputValue();
|
||||
const secondColorValue = await newSecondShadowFields
|
||||
.getByLabel("Color")
|
||||
.getByRole("textbox", { name: "Color" })
|
||||
.inputValue();
|
||||
|
||||
await expect(secondOffsetXValue).toBe("10");
|
||||
@@ -1386,7 +1398,9 @@ test.describe("Tokens: Apply token", () => {
|
||||
const newSecondShadowFields = tokensUpdateCreateModal.getByTestId(
|
||||
"shadow-input-fields-1",
|
||||
);
|
||||
const colorInput = firstShadowFields.getByLabel("Color");
|
||||
const colorInput = firstShadowFields.getByRole("textbox", {
|
||||
name: "Color",
|
||||
});
|
||||
const firstColorValue = await colorInput.inputValue();
|
||||
|
||||
// Switch to reference tab
|
||||
@@ -1414,13 +1428,13 @@ test.describe("Tokens: Apply token", () => {
|
||||
.getByLabel("Y")
|
||||
.inputValue();
|
||||
const restoredFirstBlur = await firstShadowFields
|
||||
.getByLabel("Blur")
|
||||
.getByRole("textbox", { name: "Blur" })
|
||||
.inputValue();
|
||||
const restoredFirstSpread = await firstShadowFields
|
||||
.getByLabel("Spread")
|
||||
.getByRole("textbox", { name: "Spread" })
|
||||
.inputValue();
|
||||
const restoredFirstColor = await firstShadowFields
|
||||
.getByLabel("Color")
|
||||
.getByRole("textbox", { name: "Color" })
|
||||
.inputValue();
|
||||
|
||||
await expect(restoredFirstOffsetX).toBe("2");
|
||||
@@ -1437,13 +1451,13 @@ test.describe("Tokens: Apply token", () => {
|
||||
.getByLabel("Y")
|
||||
.inputValue();
|
||||
const restoredSecondBlur = await newSecondShadowFields
|
||||
.getByLabel("Blur")
|
||||
.getByRole("textbox", { name: "Blur" })
|
||||
.inputValue();
|
||||
const restoredSecondSpread = await newSecondShadowFields
|
||||
.getByLabel("Spread")
|
||||
.getByRole("textbox", { name: "Spread" })
|
||||
.inputValue();
|
||||
const restoredSecondColor = await newSecondShadowFields
|
||||
.getByLabel("Color")
|
||||
.getByRole("textbox", { name: "Color" })
|
||||
.inputValue();
|
||||
|
||||
await expect(restoredSecondOffsetX).toBe("10");
|
||||
|
||||
@@ -59,9 +59,15 @@
|
||||
"Parses `value` of a color `sd-token` into a map like `{:value 1 :unit \"px\"}`.
|
||||
If the value is not parseable and/or has missing references returns a map with `:errors`."
|
||||
[value]
|
||||
(let [missing-references (seq (cto/find-token-value-references value))]
|
||||
(if-let [tc (tinycolor/valid-color value)]
|
||||
{:value value :unit (tinycolor/color-format tc)}
|
||||
{:errors [(wte/error-with-value :error.token/invalid-color value)]}))
|
||||
(cond
|
||||
missing-references
|
||||
{:errors [(wte/error-with-value :error.style-dictionary/missing-reference missing-references)]
|
||||
:references missing-references}
|
||||
:else
|
||||
{:errors [(wte/error-with-value :error.token/invalid-color value)]}))))
|
||||
|
||||
(defn- numeric-string? [s]
|
||||
(and (string? s)
|
||||
@@ -120,7 +126,7 @@
|
||||
If the `value` is not parseable and/or has missing references returns a map with `:errors`.
|
||||
If the `value` is parseable but is out of range returns a map with `warnings`."
|
||||
[value]
|
||||
(let [missing-references? (seq (cto/find-token-value-references value))
|
||||
(let [missing-references? (seq (seq (cto/find-token-value-references value)))
|
||||
parsed-value (cft/parse-token-value value)
|
||||
out-of-scope (not (<= 0 (:value parsed-value) 1))
|
||||
references (seq (cto/find-token-value-references value))]
|
||||
@@ -368,8 +374,8 @@
|
||||
(let [add-keyed-errors (fn [shadow-result k errors]
|
||||
(update shadow-result :errors concat
|
||||
(map #(assoc % :shadow-key k :shadow-index shadow-index) errors)))
|
||||
parsers {:offsetX parse-sd-token-general-value
|
||||
:offsetY parse-sd-token-general-value
|
||||
parsers {:offset-x parse-sd-token-general-value
|
||||
:offset-y parse-sd-token-general-value
|
||||
:blur parse-sd-token-shadow-blur
|
||||
:spread parse-sd-token-shadow-spread
|
||||
:color parse-sd-token-color-value
|
||||
@@ -389,9 +395,16 @@
|
||||
(defn- parse-sd-token-shadow-value
|
||||
"Parses shadow value and validates it."
|
||||
[value]
|
||||
(let [missing-references
|
||||
(when (string? value)
|
||||
(seq (cto/find-token-value-references value)))]
|
||||
(cond
|
||||
;; Reference value (string)
|
||||
(string? value) {:value value}
|
||||
missing-references
|
||||
{:errors [(wte/error-with-value :error.style-dictionary/missing-reference missing-references)]
|
||||
:references missing-references}
|
||||
|
||||
(string? value)
|
||||
{:errors [(wte/error-with-value :error.style-dictionary/invalid-token-value-shadow value)]}
|
||||
|
||||
;; Empty value
|
||||
(nil? value) {:errors [(wte/get-error-code :error.token/empty-input)]}
|
||||
@@ -417,7 +430,7 @@
|
||||
(if (seq all-errors)
|
||||
{:errors all-errors
|
||||
:value all-values}
|
||||
{:value all-values}))))
|
||||
{:value all-values})))))
|
||||
|
||||
(defn collect-shadow-errors [token shadow-index]
|
||||
(group-by :shadow-key
|
||||
|
||||
@@ -153,11 +153,11 @@
|
||||
(defn value->shadow
|
||||
"Transform a token shadow value into penpot shadow data structure"
|
||||
[value]
|
||||
(mapv (fn [{:keys [offsetX offsetY blur spread color inset]}]
|
||||
(mapv (fn [{:keys [offset-x offset-y blur spread color inset]}]
|
||||
{:id (random-uuid)
|
||||
:hidden false
|
||||
:offset-x offsetX
|
||||
:offset-y offsetY
|
||||
:offset-x offset-x
|
||||
:offset-y offset-y
|
||||
:blur blur
|
||||
:color (value->color color)
|
||||
:spread spread
|
||||
|
||||
@@ -108,6 +108,10 @@
|
||||
{:error/code :error.style-dictionary/invalid-token-value-shadow-spread
|
||||
:error/fn #(tr "workspace.tokens.shadow-spread-range")}
|
||||
|
||||
:error.style-dictionary/invalid-token-value-shadow
|
||||
{:error/code :error.style-dictionary/invalid-token-value-shadow
|
||||
:error/fn #(tr "workspace.tokens.invalid-token-value-shadow" %)}
|
||||
|
||||
:error/unknown
|
||||
{:error/code :error/unknown
|
||||
:error/fn #(tr "labels.unknown-error")}})
|
||||
|
||||
@@ -16,8 +16,8 @@
|
||||
:letter-spacing "Letter Spacing"
|
||||
:text-case "Text Case"
|
||||
:text-decoration "Text Decoration"
|
||||
:offsetX "X"
|
||||
:offsetY "Y"
|
||||
:offset-x "X"
|
||||
:offset-y "Y"
|
||||
:blur "Blur"
|
||||
:spread "Spread"
|
||||
:color "Color"
|
||||
|
||||
@@ -118,6 +118,7 @@
|
||||
(mf/use-fn
|
||||
(mf/deps disabled)
|
||||
(fn [event]
|
||||
(dom/prevent-default event)
|
||||
(dom/stop-propagation event)
|
||||
(when-not disabled
|
||||
(swap! is-open* not))))
|
||||
|
||||
@@ -53,10 +53,15 @@
|
||||
"true")
|
||||
:aria-describedby (when has-hint
|
||||
(str id "-hint"))
|
||||
:aria-labelledby tooltip-id
|
||||
:type (d/nilv type "text")
|
||||
:id id
|
||||
:max-length (d/nilv max-length max-input-length)})
|
||||
|
||||
props (if (and aria-label (not (some? icon)))
|
||||
(mf/spread-props props
|
||||
{:aria-label aria-label})
|
||||
(mf/spread-props props
|
||||
{:aria-labelledby tooltip-id}))
|
||||
inside-class (stl/css-case :input-wrapper true
|
||||
:has-hint has-hint
|
||||
:hint-type-hint (= hint-type "hint")
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
}
|
||||
|
||||
.swatch {
|
||||
--border-color: var(--color-background-quaternary);
|
||||
--border-color: var(--color-accent-primary-muted);
|
||||
--border-radius: #{$br-4};
|
||||
--border-color-active: var(--color-foreground-primary);
|
||||
--border-color-active-inset: var(--color-background-primary);
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
[app.main.ui.workspace.tokens.export.modal]
|
||||
[app.main.ui.workspace.tokens.import]
|
||||
[app.main.ui.workspace.tokens.import.modal]
|
||||
[app.main.ui.workspace.tokens.management.create.modals]
|
||||
[app.main.ui.workspace.tokens.management.forms.modals]
|
||||
[app.main.ui.workspace.tokens.settings]
|
||||
[app.main.ui.workspace.tokens.themes.create-modal]
|
||||
[app.main.ui.workspace.viewport :refer [viewport*]]
|
||||
|
||||
@@ -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
|
||||
|
||||
(ns app.main.ui.workspace.tokens.management.create.combobox-token-fonts
|
||||
(ns app.main.ui.workspace.tokens.management.forms.controls.fonts-combobox
|
||||
(:require-macros [app.main.style :as stl])
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
@@ -24,6 +24,29 @@
|
||||
[cuerdas.core :as str]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
;; --- Font Picker Inputs -------------------------------------------------------
|
||||
;;
|
||||
;; We provide two versions of the font picker: the normal (primitive) input and
|
||||
;; the composite input. Both allow selecting a font-family either by typing free
|
||||
;; text or by choosing from the list of fonts loaded in the application. Comma-
|
||||
;; separated values are treated as literal font-family lists.
|
||||
;;
|
||||
;; 1) Normal Font Picker (primitive)
|
||||
;; - Used when the token’s value *is itself* a font-family (string) or a
|
||||
;; reference to another token.
|
||||
;; - The input writes directly to `:value`.
|
||||
;; - Validation ensures the string is a valid font-family or a valid token
|
||||
;; reference.
|
||||
;;
|
||||
;; 2) Composite Font Picker
|
||||
;; - Used inside typography tokens, where `:value` is a map (e.g. contains
|
||||
;; :font-family, :font-weight, :letter-spacing, etc.).
|
||||
;; - The input writes to the specific subfield `[:value :font-family]`.
|
||||
;; - Only this field is validated and updated—other typography fields remain
|
||||
;; untouched.
|
||||
;;
|
||||
;; Both modes share the same UI behaviour, but differ in how they store and
|
||||
;; validate data within the form state.
|
||||
|
||||
(defn- resolve-value
|
||||
[tokens prev-token value]
|
||||
@@ -46,7 +69,7 @@
|
||||
(rx/of {:value resolved-value})
|
||||
(rx/of {:error (first errors)}))))))))
|
||||
|
||||
(mf/defc font-picker-combobox*
|
||||
(mf/defc fonts-combobox*
|
||||
[{:keys [token tokens name] :rest props}]
|
||||
(let [form (mf/use-ctx fc/context)
|
||||
input-name name
|
||||
@@ -117,7 +140,6 @@
|
||||
|
||||
props
|
||||
(mf/spread-props props {:on-change on-change
|
||||
;; TODO: Review this value vs default-value
|
||||
:value (or value "")
|
||||
:hint-message (:message hint)
|
||||
:slot-end font-selector-button
|
||||
@@ -174,7 +196,7 @@
|
||||
(update :errors clean-errors)
|
||||
(update :extra-errors clean-errors)))))))
|
||||
|
||||
(mf/defc font-picker-composite-combobox*
|
||||
(mf/defc composite-fonts-combobox*
|
||||
[{:keys [token tokens name] :rest props}]
|
||||
(let [form (mf/use-ctx fc/context)
|
||||
input-name name
|
||||
@@ -242,7 +264,6 @@
|
||||
|
||||
props
|
||||
(mf/spread-props props {:on-change on-change
|
||||
;; TODO: Review this value vs default-value
|
||||
:value (or value "")
|
||||
:hint-message (:message hint)
|
||||
:slot-end font-selector-button
|
||||
@@ -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
|
||||
|
||||
(ns app.main.ui.workspace.tokens.management.create.border-radius
|
||||
(ns app.main.ui.workspace.tokens.management.forms.generic-form
|
||||
(:require-macros [app.main.style :as stl])
|
||||
(:require
|
||||
[app.common.files.tokens :as cft]
|
||||
@@ -23,7 +23,8 @@
|
||||
[app.main.ui.ds.foundations.typography.heading :refer [heading*]]
|
||||
[app.main.ui.ds.notifications.context-notification :refer [context-notification*]]
|
||||
[app.main.ui.forms :as fc]
|
||||
[app.main.ui.workspace.tokens.management.create.form-input-token :refer [form-input-token*]]
|
||||
[app.main.ui.workspace.tokens.management.forms.controls :as token.controls]
|
||||
[app.main.ui.workspace.tokens.management.forms.validators :refer [default-validate-token]]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.forms :as fm]
|
||||
[app.util.i18n :refer [tr]]
|
||||
@@ -38,8 +39,25 @@
|
||||
(str/blank? value))
|
||||
(tr "workspace.tokens.empty-input")))
|
||||
|
||||
(defn- make-schema
|
||||
[tokens-tree]
|
||||
|
||||
(defn get-value-for-validator
|
||||
[active-tab value value-subfield form-type]
|
||||
|
||||
(case form-type
|
||||
:indexed
|
||||
(if (= active-tab :reference)
|
||||
(:reference value)
|
||||
(value-subfield value))
|
||||
|
||||
:composite
|
||||
(if (= active-tab :reference)
|
||||
(get value :reference)
|
||||
value)
|
||||
|
||||
value))
|
||||
|
||||
(defn- default-make-schema
|
||||
[tokens-tree _]
|
||||
(sm/schema
|
||||
[:and
|
||||
[:map
|
||||
@@ -62,14 +80,37 @@
|
||||
(nil? (cto/token-value-self-reference? name value))))]]))
|
||||
|
||||
(mf/defc form*
|
||||
[{:keys [token validate-token action is-create selected-token-set-id tokens-tree-in-selected-set] :as props}]
|
||||
|
||||
(let [token
|
||||
(mf/with-memo [token]
|
||||
(or token {:type :border-radius}))
|
||||
|
||||
[{:keys [token
|
||||
validator
|
||||
action
|
||||
is-create
|
||||
selected-token-set-id
|
||||
tokens-tree-in-selected-set
|
||||
token-type
|
||||
(get token :type)
|
||||
make-schema
|
||||
input-component
|
||||
initial
|
||||
type
|
||||
value-subfield
|
||||
input-value-placeholder] :as props}]
|
||||
|
||||
(let [make-schema (or make-schema default-make-schema)
|
||||
input-component (or input-component token.controls/input*)
|
||||
validate-token (or validator default-validate-token)
|
||||
|
||||
active-tab* (mf/use-state #(if (cft/is-reference? token) :reference :composite))
|
||||
active-tab (deref active-tab*)
|
||||
|
||||
on-toggle-tab
|
||||
(mf/use-fn
|
||||
(mf/deps)
|
||||
(fn [new-tab]
|
||||
(let [new-tab (keyword new-tab)]
|
||||
(reset! active-tab* new-tab))))
|
||||
|
||||
token
|
||||
(mf/with-memo [token]
|
||||
(or token {:type token-type}))
|
||||
|
||||
token-properties
|
||||
(dwta/get-token-properties token)
|
||||
@@ -89,14 +130,15 @@
|
||||
(assoc (:name token) token)))
|
||||
|
||||
schema
|
||||
(mf/with-memo [tokens-tree-in-selected-set]
|
||||
(make-schema tokens-tree-in-selected-set))
|
||||
(mf/with-memo [tokens-tree-in-selected-set active-tab]
|
||||
(make-schema tokens-tree-in-selected-set active-tab))
|
||||
|
||||
initial
|
||||
(mf/with-memo [token]
|
||||
(or initial
|
||||
{:name (:name token "")
|
||||
:value (:value token "")
|
||||
:description (:description token "")})
|
||||
:description (:description token "")}))
|
||||
|
||||
form
|
||||
(fm/use-form :schema schema
|
||||
@@ -136,12 +178,13 @@
|
||||
|
||||
on-submit
|
||||
(mf/use-fn
|
||||
(mf/deps validate-token token tokens token-type)
|
||||
(mf/deps validate-token token tokens token-type value-subfield type active-tab)
|
||||
(fn [form _event]
|
||||
(let [name (get-in @form [:clean-data :name])
|
||||
description (get-in @form [:clean-data :description])
|
||||
value (get-in @form [:clean-data :value])]
|
||||
(->> (validate-token {:token-value value
|
||||
value (get-in @form [:clean-data :value])
|
||||
value-for-validation (get-value-for-validator active-tab value value-subfield type)]
|
||||
(->> (validate-token {:token-value value-for-validation
|
||||
:token-name name
|
||||
:token-description description
|
||||
:prev-token token
|
||||
@@ -168,7 +211,9 @@
|
||||
[:div {:class (stl/css :token-rows)}
|
||||
|
||||
[:> heading* {:level 2 :typography "headline-medium" :class (stl/css :form-modal-title)}
|
||||
(tr "workspace.tokens.create-token" token-type)]
|
||||
(if (= action "edit")
|
||||
(tr "workspace.tokens.edit-token" token-type)
|
||||
(tr "workspace.tokens.create-token" token-type))]
|
||||
|
||||
[:div {:class (stl/css :input-row)}
|
||||
[:> fc/form-input* {:id "token-name"
|
||||
@@ -185,12 +230,16 @@
|
||||
{:level :warning :appearance :ghost} (tr "workspace.tokens.warning-name-change")]])]
|
||||
|
||||
[:div {:class (stl/css :input-row)}
|
||||
[:> form-input-token*
|
||||
{:placeholder (tr "workspace.tokens.token-value-enter")
|
||||
[:> input-component
|
||||
{:placeholder (or input-value-placeholder (tr "workspace.tokens.token-value-enter"))
|
||||
:label (tr "workspace.tokens.token-value")
|
||||
:name :value
|
||||
:form form
|
||||
:token token
|
||||
:tokens tokens}]]
|
||||
:tokens tokens
|
||||
:tab active-tab
|
||||
:subfield value-subfield
|
||||
:toggle on-toggle-tab}]]
|
||||
|
||||
[:div {:class (stl/css :input-row)}
|
||||
[:> fc/form-input* {:id "token-description"
|
||||
@@ -4,7 +4,7 @@
|
||||
;;
|
||||
;; Copyright (c) KALEIDOS INC
|
||||
|
||||
(ns app.main.ui.workspace.tokens.management.create.modals
|
||||
(ns app.main.ui.workspace.tokens.management.forms.modals
|
||||
(:require-macros [app.main.style :as stl])
|
||||
(:require
|
||||
[app.common.data.macros :as dm]
|
||||
@@ -13,22 +13,28 @@
|
||||
[app.main.refs :as refs]
|
||||
[app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
|
||||
[app.main.ui.ds.foundations.assets.icon :as i]
|
||||
[app.main.ui.workspace.tokens.management.create.form :refer [form-wrapper*]]
|
||||
[app.main.ui.workspace.tokens.management.forms.form-container :refer [form-container*]]
|
||||
[app.util.i18n :refer [tr]]
|
||||
[okulary.core :as l]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
;; Component -------------------------------------------------------------------
|
||||
|
||||
(defn calculate-position
|
||||
(defn- calculate-position
|
||||
"Calculates the style properties for the given coordinates and position"
|
||||
[{vh :height} position x y color?]
|
||||
(let [;; picker height in pixels
|
||||
;; TODO: Revisit these harcoded values
|
||||
h (if color? 610 510)
|
||||
[{vh :height} position x y token-type]
|
||||
(let [; TODO: Revisit these harcoded values
|
||||
modal-height (case token-type
|
||||
:color
|
||||
500
|
||||
:typography
|
||||
660
|
||||
:shadow
|
||||
660
|
||||
400)
|
||||
;; Checks for overflow outside the viewport height
|
||||
max-y (- vh h)
|
||||
overflow-fix (max 0 (+ y (- 50) h (- vh)))
|
||||
max-y (- vh modal-height)
|
||||
overflow-fix (max 0 (+ y (- 50) modal-height (- vh)))
|
||||
bottom-offset "1rem"
|
||||
top-offset (dm/str (- y 70) "px")
|
||||
max-height-top (str "calc(100vh - " top-offset)
|
||||
@@ -61,17 +67,19 @@
|
||||
:top (dm/str (- y 70 overflow-fix) "px")
|
||||
:maxHeight max-height-top}))))
|
||||
|
||||
(defn use-viewport-position-style [x y position color?]
|
||||
(defn- use-viewport-position-style [x y position token-type]
|
||||
(let [vport (-> (l/derived :vport refs/workspace-local)
|
||||
(mf/deref))]
|
||||
(-> (calculate-position vport position x y color?)
|
||||
(-> (calculate-position vport position x y token-type)
|
||||
(clj->js))))
|
||||
|
||||
(mf/defc token-update-create-modal
|
||||
{::mf/wrap-props false}
|
||||
[{:keys [x y position token token-type action selected-token-set-id] :as _args}]
|
||||
(let [wrapper-style (use-viewport-position-style x y position (= token-type :color))
|
||||
modal-size-large* (mf/use-state (= token-type :typography))
|
||||
(let [wrapper-style (use-viewport-position-style x y position token-type)
|
||||
modal-size-large* (mf/use-state (or (= token-type :typography)
|
||||
(= token-type :color)
|
||||
(= token-type :shadow)))
|
||||
modal-size-large? (deref modal-size-large*)
|
||||
close-modal (mf/use-fn
|
||||
(fn []
|
||||
@@ -89,7 +97,7 @@
|
||||
:icon i/close
|
||||
:variant "action"
|
||||
:aria-label (tr "labels.close")}]
|
||||
[:> form-wrapper* {:is-create (not (ctob/token? token))
|
||||
[:> form-container* {:is-create (not (ctob/token? token))
|
||||
:token token
|
||||
:action action
|
||||
:selected-token-set-id selected-token-set-id
|
||||
@@ -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/_borders.scss" as *;
|
||||
|
||||
.form-wrapper {
|
||||
width: $sz-384;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.token-rows {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--sp-l);
|
||||
}
|
||||
|
||||
.inputs-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -34,9 +23,19 @@
|
||||
gap: var(--sp-xs);
|
||||
}
|
||||
|
||||
.input-row-reference {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--sp-xs);
|
||||
border-inline-start: $b-1 solid var(--color-accent-primary-muted);
|
||||
padding-inline-start: var(--sp-m);
|
||||
}
|
||||
|
||||
.title-bar {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto;
|
||||
grid-template-columns: 1fr auto auto;
|
||||
gap: var(--sp-xs);
|
||||
}
|
||||
|
||||
.title {
|
||||
@@ -46,29 +45,17 @@
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.form-modal-title {
|
||||
@include t.use-typography("headline-medium");
|
||||
color: var(--color-foreground-primary);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.button-row {
|
||||
display: grid;
|
||||
grid-template-columns: auto auto;
|
||||
justify-content: end;
|
||||
gap: var(--sp-m);
|
||||
padding-block-start: var(--sp-s);
|
||||
}
|
||||
|
||||
.with-delete {
|
||||
grid-template-columns: 1fr auto auto;
|
||||
}
|
||||
|
||||
.warning-name-change-notification-wrapper {
|
||||
margin-block-start: var(--sp-l);
|
||||
}
|
||||
|
||||
.delete-btn {
|
||||
justify-self: start;
|
||||
}
|
||||
|
||||
.visible-label {
|
||||
@include t.use-typography("headline-small");
|
||||
color: var(--color-foreground-secondary);
|
||||
line-height: $sz-32;
|
||||
}
|
||||
|
||||
.select-wrapper {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto;
|
||||
}
|
||||
@@ -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/_borders.scss" as *;
|
||||
|
||||
.form-wrapper {
|
||||
width: $sz-384;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.token-rows {
|
||||
.inputs-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--sp-l);
|
||||
gap: var(--sp-m);
|
||||
border-inline-start: $b-1 solid var(--color-accent-primary-muted);
|
||||
padding-inline-start: var(--sp-m);
|
||||
}
|
||||
|
||||
.input-row {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--sp-xs);
|
||||
@@ -30,29 +28,9 @@
|
||||
grid-template-columns: 1fr auto;
|
||||
}
|
||||
|
||||
.form-modal-title {
|
||||
@include t.use-typography("headline-medium");
|
||||
.title {
|
||||
@include t.use-typography("body-small");
|
||||
color: var(--color-foreground-primary);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.button-row {
|
||||
display: grid;
|
||||
grid-template-columns: auto auto;
|
||||
justify-content: end;
|
||||
gap: var(--sp-m);
|
||||
padding-block-start: var(--sp-s);
|
||||
}
|
||||
|
||||
.with-delete {
|
||||
grid-template-columns: 1fr auto auto;
|
||||
}
|
||||
|
||||
.warning-name-change-notification-wrapper {
|
||||
margin-block-start: var(--sp-l);
|
||||
}
|
||||
|
||||
.delete-btn {
|
||||
justify-self: start;
|
||||
}
|
||||
@@ -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"
|
||||
:dimensions "expand"
|
||||
:sizing "expand"
|
||||
:shadow "drop-shadow"
|
||||
"add"))
|
||||
|
||||
(mf/defc token-group*
|
||||
|
||||
@@ -151,8 +151,8 @@
|
||||
;; export interface Shadow {
|
||||
;; id?: string;
|
||||
;; style?: 'drop-shadow' | 'inner-shadow';
|
||||
;; offsetX?: number;
|
||||
;; offsetY?: number;
|
||||
;; offset-x?: number;
|
||||
;; offset-y?: number;
|
||||
;; blur?: number;
|
||||
;; spread?: number;
|
||||
;; hidden?: boolean;
|
||||
@@ -164,8 +164,8 @@
|
||||
(obj/without-empty
|
||||
#js {:id (-> id format-id)
|
||||
:style (-> style format-key)
|
||||
:offsetX offset-x
|
||||
:offsetY offset-y
|
||||
:offset-x offset-x
|
||||
:offset-y offset-y
|
||||
:blur blur
|
||||
:spread spread
|
||||
:hidden hidden
|
||||
|
||||
@@ -147,7 +147,7 @@
|
||||
;; export interface Shadow {
|
||||
;; id?: string;
|
||||
;; style?: 'drop-shadow' | 'inner-shadow';
|
||||
;; offsetX?: number;
|
||||
;; offset--y?: number;
|
||||
;; offsetY?: number;
|
||||
;; blur?: number;
|
||||
;; spread?: number;
|
||||
@@ -160,8 +160,8 @@
|
||||
(d/without-nils
|
||||
{:id (-> (obj/get shadow "id") parse-id)
|
||||
:style (-> (obj/get shadow "style") parse-keyword)
|
||||
:offset-x (obj/get shadow "offsetX")
|
||||
:offset-y (obj/get shadow "offsetY")
|
||||
:offset-x (obj/get shadow "offset-x")
|
||||
:offset-y (obj/get shadow "offset-y")
|
||||
:blur (obj/get shadow "blur")
|
||||
:spread (obj/get shadow "spread")
|
||||
:hidden (obj/get shadow "hidden")
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
[app.main.data.workspace.tokens.application :as dwta]
|
||||
[app.main.data.workspace.tokens.library-edit :as dwtl]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.workspace.tokens.management.create.form :as token-form]
|
||||
[app.main.ui.workspace.tokens.management.forms.validators :as form-validator]
|
||||
[app.main.ui.workspace.tokens.themes.create-modal :as theme-form]
|
||||
[app.plugins.utils :as u]
|
||||
[app.util.object :as obj]
|
||||
@@ -51,7 +51,7 @@
|
||||
:set
|
||||
(fn [_ value]
|
||||
(let [tokens-lib (u/locate-tokens-lib file-id)
|
||||
errors (token-form/validate-token-name
|
||||
errors (form-validator/validate-token-name
|
||||
(ctob/get-tokens tokens-lib set-id)
|
||||
value)]
|
||||
(cond
|
||||
|
||||
@@ -474,8 +474,8 @@
|
||||
(t/async
|
||||
done
|
||||
(let [shadow-token {:name "shadow.sm"
|
||||
:value [{:offsetX 10
|
||||
:offsetY 10
|
||||
:value [{:offset-x 10
|
||||
:offset-y 10
|
||||
:blur 10
|
||||
:spread 10
|
||||
:color "rgba(0,0,0,0.5)"
|
||||
|
||||
@@ -7753,6 +7753,10 @@ msgstr "Invalid token value: only none, underline and strike-through are accepte
|
||||
msgid "workspace.tokens.invalid-token-value-typography"
|
||||
msgstr "Invalid value: must reference a composite typography token."
|
||||
|
||||
#: src/app/main/data/workspace/tokens/errors.cljs
|
||||
msgid "workspace.tokens.invalid-token-value-shadow"
|
||||
msgstr "Invalid value: must reference a composite shadow token."
|
||||
|
||||
#: src/app/main/data/workspace/tokens/errors.cljs:61, src/app/main/data/workspace/tokens/errors.cljs:73, src/app/main/data/workspace/tokens/errors.cljs:77
|
||||
msgid "workspace.tokens.invalid-value"
|
||||
msgstr "Invalid token value: %s"
|
||||
@@ -7870,6 +7874,10 @@ msgstr "Reference is not valid or is not in any active set"
|
||||
msgid "workspace.tokens.reference-composite"
|
||||
msgstr "Enter a token typography alias"
|
||||
|
||||
#: src/app/main/ui/workspace/tokens/management/create/form.cljs:775
|
||||
msgid "workspace.tokens.reference-composite-shadow"
|
||||
msgstr "Enter a token shadow alias"
|
||||
|
||||
#: src/app/main/ui/workspace/tokens/style_dictionary.cljs
|
||||
#, unused
|
||||
msgid "workspace.tokens.reference-error"
|
||||
|
||||
@@ -7674,6 +7674,10 @@ msgstr "Tipo de sombra no válida: solo se aceptan 'innerShadow' o 'dropShadow'"
|
||||
msgid "workspace.tokens.invalid-token-value-typography"
|
||||
msgstr "Valor no válido: debe hacer referencia a un token tipográfico compuesto."
|
||||
|
||||
#: src/app/main/data/workspace/tokens/errors.cljs
|
||||
msgid "workspace.tokens.invalid-token-value-shadow"
|
||||
msgstr "Valor no válido: debe hacer referencia a un token de sombra compuesto."
|
||||
|
||||
#: src/app/main/data/workspace/tokens/errors.cljs:61, src/app/main/data/workspace/tokens/errors.cljs:73, src/app/main/data/workspace/tokens/errors.cljs:77
|
||||
msgid "workspace.tokens.invalid-value"
|
||||
msgstr "Valor de token no válido: %s"
|
||||
|
||||
Reference in New Issue
Block a user