🎉 Show tokens on color inputs (#7377)

* 🎉 Add tokens to color row

* 🎉 Add color-token to stroke input

* 🐛 FIx change token on multiselection with groups

* 🔧 Add config flag

* 🐛 Fix comments
This commit is contained in:
Eva Marco
2025-10-03 11:19:01 +02:00
committed by GitHub
parent a4f20564af
commit 44f6c2f83c
31 changed files with 789 additions and 315 deletions

View File

@@ -534,4 +534,6 @@
:sided-margins #{:spacing :dimensions} :sided-margins #{:spacing :dimensions}
:line-height #{:line-height :number} :line-height #{:line-height :number}
:font-size #{:font-size} :font-size #{:font-size}
:letter-spacing #{:letter-spacing}}) :letter-spacing #{:letter-spacing}
:fill #{:color}
:stroke-color #{:color}})

View File

@@ -93,7 +93,7 @@ test("Create a LINEAR gradient", async ({ page }) => {
await expect(inputOpacityGlobal).toHaveValue("50"); await expect(inputOpacityGlobal).toHaveValue("50");
await expect(inputOpacityGlobal).toBeVisible(); await expect(inputOpacityGlobal).toBeVisible();
await expect(workspacePage.page.getByText("Linear gradient")).toBeVisible(); await expect(workspacePage.page.getByText("Linear gradient").nth(1)).toBeVisible();
}); });
test("Create a RADIAL gradient", async ({ page }) => { test("Create a RADIAL gradient", async ({ page }) => {
@@ -175,7 +175,7 @@ test("Create a RADIAL gradient", async ({ page }) => {
await expect(inputOpacityGlobal).toHaveValue("50"); await expect(inputOpacityGlobal).toHaveValue("50");
await expect(inputOpacityGlobal).toBeVisible(); await expect(inputOpacityGlobal).toBeVisible();
await expect(workspacePage.page.getByText("Radial gradient")).toBeVisible(); await expect(workspacePage.page.getByText("Radial gradient").nth(1)).toBeVisible();
}); });
test("Gradient stops limit", async ({ page }) => { test("Gradient stops limit", async ({ page }) => {

View File

@@ -46,7 +46,7 @@ const setupTokensFile = async (page, options = {}) => {
} = options; } = options;
const workspacePage = new WorkspacePage(page); const workspacePage = new WorkspacePage(page);
if (flags.length) { if (flags.length > 0) {
await workspacePage.mockConfigFlags(flags); await workspacePage.mockConfigFlags(flags);
} }
@@ -879,7 +879,10 @@ test.describe("Tokens: Themes modal", () => {
}); });
test.describe("Tokens: Apply token", () => { test.describe("Tokens: Apply token", () => {
test("User applies color token to a shape", async ({ page }) => { // When deleting the "enable-token-color" flag, permanently remove this test.
test("User applies color token to a shape without tokens on design-tab", async ({
page,
}) => {
const { workspacePage, tokensSidebar, tokenContextMenuForToken } = const { workspacePage, tokensSidebar, tokenContextMenuForToken } =
await setupTokensFile(page); await setupTokensFile(page);
@@ -909,6 +912,35 @@ test.describe("Tokens: Themes modal", () => {
await expect(inputColor).toHaveValue("000000"); await expect(inputColor).toHaveValue("000000");
}); });
test("User applies color token to a shape", async ({ page }) => {
const { workspacePage, tokensSidebar, tokenContextMenuForToken } =
await setupTokensFile(page, { flags: ["enable-token-color"] });
await page.getByRole("tab", { name: "Layers" }).click();
await workspacePage.layers
.getByTestId("layer-row")
.filter({ hasText: "Button" })
.click();
const tokensTabButton = page.getByRole("tab", { name: "Tokens" });
await tokensTabButton.click();
await tokensSidebar
.getByRole("button")
.filter({ hasText: "Color" })
.click();
await tokensSidebar
.getByRole("button", { name: "colors.black" })
.click({ button: "right" });
await tokenContextMenuForToken.getByText("Fill").click();
await expect(
workspacePage.page.getByLabel("Name: colors.black"),
).toBeVisible();
});
test("User applies typography token to a text shape", async ({ page }) => { test("User applies typography token to a text shape", async ({ page }) => {
const { workspacePage, tokensSidebar, tokenContextMenuForToken } = const { workspacePage, tokensSidebar, tokenContextMenuForToken } =
await setupTypographyTokensFile(page); await setupTypographyTokensFile(page);
@@ -1024,9 +1056,7 @@ test.describe("Tokens: Themes modal", () => {
await expect(letterSpacingField).toHaveValue( await expect(letterSpacingField).toHaveValue(
originalValues.letterSpacing, originalValues.letterSpacing,
); );
await expect(lineHeightField).toHaveValue( await expect(lineHeightField).toHaveValue(originalValues.lineHeight);
originalValues.lineHeight,
);
await expect(textCaseField).toHaveValue(originalValues.textCase); await expect(textCaseField).toHaveValue(originalValues.textCase);
await expect(textDecorationField).toHaveValue( await expect(textDecorationField).toHaveValue(
originalValues.textDecoration, originalValues.textDecoration,

View File

@@ -8,16 +8,16 @@
cursor: grab; cursor: grab;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
height: calc(100% + var(--sp-m)); block-size: calc(100% + var(--sp-m));
justify-content: center; justify-content: center;
left: var(--reorder-left-position, calc(-1 * var(--sp-l))); inset-inline-start: var(--reorder-left-position, -11px);
position: absolute; position: absolute;
top: calc(-1 * (var(--sp-m) / 2)); inset-block-start: calc(-1 * (var(--sp-m) / 2));
z-index: var(--z-index-panels); z-index: var(--z-index-panels);
} }
.reorder-icon { .reorder-icon {
height: var(--sp-l); block-size: var(--sp-l);
pointer-events: none; pointer-events: none;
visibility: var(--reorder-icon-visibility, hidden); visibility: var(--reorder-icon-visibility, hidden);
--icon-stroke-color: var(--color-foreground-secondary); --icon-stroke-color: var(--color-foreground-secondary);
@@ -28,15 +28,15 @@
border-color: var(--color-accent-primary); border-color: var(--color-accent-primary);
margin: 0; margin: 0;
position: absolute; position: absolute;
width: 100%; inline-size: 100%;
} }
.reorder-separator-top { .reorder-separator-top {
display: var(--reorder-top-display, none); display: var(--reorder-top-display, none);
top: calc(-1 * var(--sp-xxs)); inset-block-start: calc(-1 * var(--sp-xxs));
} }
.reorder-separator-bottom { .reorder-separator-bottom {
display: var(--reorder-bottom-display, none); display: var(--reorder-bottom-display, none);
bottom: calc(-1 * var(--sp-xxs)); inset-block-end: calc(-1 * var(--sp-xxs));
} }

View File

@@ -18,7 +18,6 @@ $sz-32: px2rem(32);
$sz-36: px2rem(36); $sz-36: px2rem(36);
$sz-40: px2rem(40); $sz-40: px2rem(40);
$sz-48: px2rem(48); $sz-48: px2rem(48);
$sz-80: px2rem(80);
$sz-88: px2rem(88); $sz-88: px2rem(88);
$sz-120: px2rem(120); $sz-120: px2rem(120);
$sz-154: px2rem(154); $sz-154: px2rem(154);

View File

@@ -4,6 +4,8 @@
// //
// Copyright (c) KALEIDOS INC // Copyright (c) KALEIDOS INC
@use "ds/_utils.scss" as *;
@keyframes line-pencil { @keyframes line-pencil {
0% { 0% {
transform: translateY(0); transform: translateY(0);
@@ -44,8 +46,8 @@
// Tips container // Tips container
.tips-container { .tips-container {
text-align: center; text-align: center;
min-width: var(--sp-600); min-width: px2rem(600);
max-width: var(--sp-800); max-width: px2rem(800);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: var(--sp-s); gap: var(--sp-s);

View File

@@ -14,8 +14,6 @@ $_sp-16: px2rem(16);
$_sp-20: px2rem(20); $_sp-20: px2rem(20);
$_sp-24: px2rem(24); $_sp-24: px2rem(24);
$_sp-32: px2rem(32); $_sp-32: px2rem(32);
$_sp-600: px2rem(600);
$_sp-800: px2rem(800);
:global(:root) { :global(:root) {
--sp-xxs: #{$_sp-2}; --sp-xxs: #{$_sp-2};
@@ -26,6 +24,4 @@ $_sp-800: px2rem(800);
--sp-xl: #{$_sp-20}; --sp-xl: #{$_sp-20};
--sp-xxl: #{$_sp-24}; --sp-xxl: #{$_sp-24};
--sp-xxxl: #{$_sp-32}; --sp-xxxl: #{$_sp-32};
--sp-600: #{$_sp-600};
--sp-800: #{$_sp-800};
} }

View File

@@ -63,11 +63,12 @@
[:class {:optional true} :string] [:class {:optional true} :string]
[:size {:optional true} [:enum "small" "medium" "large"]] [:size {:optional true} [:enum "small" "medium" "large"]]
[:active {:optional true} ::sm/boolean] [:active {:optional true} ::sm/boolean]
[:has-errors {:optional true} [:maybe ::sm/boolean]]
[:on-click {:optional true} ::sm/fn]]) [:on-click {:optional true} ::sm/fn]])
(mf/defc swatch* (mf/defc swatch*
{::mf/schema (sm/schema schema:swatch)} {::mf/schema (sm/schema schema:swatch)}
[{:keys [background on-click size active class tooltip-content] [{:keys [background on-click size active class tooltip-content has-errors]
:rest props}] :rest props}]
(let [;; NOTE: this code is only relevant for storybook, because (let [;; NOTE: this code is only relevant for storybook, because
;; storybook is unable to pass in a comfortable way a complex ;; storybook is unable to pass in a comfortable way a complex
@@ -92,6 +93,12 @@
image (:image background) image (:image background)
format (if id? "rounded" "square") format (if id? "rounded" "square")
element-id (mf/use-id) element-id (mf/use-id)
on-click
(mf/use-fn
(mf/deps background on-click)
(fn [event]
(when (fn? on-click)
(^function on-click background event))))
class class
(dm/str class " " (stl/css-case (dm/str class " " (stl/css-case
@@ -124,6 +131,8 @@
(let [uri (cfg/resolve-file-media image)] (let [uri (cfg/resolve-file-media image)]
[:span {:class (stl/css :swatch-image) [:span {:class (stl/css :swatch-image)
:style {:background-image (str/ffmt "url(%)" uri)}}]) :style {:background-image (str/ffmt "url(%)" uri)}}])
has-errors
[:span {:class (stl/css :swatch-error)}]
:else :else
[:span {:class (stl/css :swatch-opacity)} [:span {:class (stl/css :swatch-opacity)}

View File

@@ -20,7 +20,6 @@
border: $b-1 solid var(--border-color); border: $b-1 solid var(--border-color);
border-radius: var(--border-radius); border-radius: var(--border-radius);
overflow: hidden; overflow: hidden;
&:focus { &:focus {
--border-color: var(--color-accent-primary); --border-color: var(--color-accent-primary);
} }
@@ -109,3 +108,7 @@
flex: 1; flex: 1;
display: block; display: block;
} }
.swatch-error {
background: var(--color-background-primary);
}

View File

@@ -47,7 +47,8 @@
[cuerdas.core :as str] [cuerdas.core :as str]
[okulary.core :as l] [okulary.core :as l]
[potok.v2.core :as ptk] [potok.v2.core :as ptk]
[rumext.v2 :as mf])) [rumext.v2 :as mf]
[rumext.v2.util :as mfu]))
;; --- Refs ;; --- Refs
@@ -93,13 +94,13 @@
(dom/set-css-property! node "--saturation-grad-to" (format-hsl hsl-to))))) (dom/set-css-property! node "--saturation-grad-to" (format-hsl hsl-to)))))
(mf/defc colorpicker (mf/defc colorpicker
[{:keys [data disable-gradient disable-opacity disable-image on-change on-accept origin combined-tokens color-origin on-token-change]}] [{:keys [data disable-gradient disable-opacity disable-image on-change on-accept origin combined-tokens color-origin on-token-change tab]}]
(let [state (mf/deref refs/colorpicker) (let [state (mf/deref refs/colorpicker)
node-ref (mf/use-ref) node-ref (mf/use-ref)
should-update? (mf/use-var true) should-update? (mf/use-var true)
token-color (contains? cfg/flags :token-color) token-color (contains? cfg/flags :token-color)
color-style* (mf/use-state :direct-color) color-style* (mf/use-state (d/nilv tab :direct-color))
color-style (deref color-style*) color-style (deref color-style*)
toggle-token-color toggle-token-color
(mf/use-fn (mf/use-fn
@@ -364,7 +365,7 @@
:icon i/hsva :icon i/hsva
:id "hsva"}]) :id "hsva"}])
show-tokens? (contains? #{:fill :stroke :color-selection} color-origin)] show-tokens? (contains? #{:fill :stroke-color :color-selection} color-origin)]
;; Initialize colorpicker state ;; Initialize colorpicker state
(mf/with-effect [] (mf/with-effect []
@@ -726,12 +727,16 @@
color-origin color-origin
on-token-change on-token-change
on-close on-close
tab
on-accept]}] on-accept]}]
(let [vport (mf/deref viewport) (let [vport (mf/deref viewport)
dirty? (mf/use-var false) dirty? (mf/use-var false)
last-change (mf/use-var nil) last-change (mf/use-var nil)
position (d/nilv position :left) position (d/nilv position :left)
style (calculate-position vport position x y (some? (:gradient data))) style (calculate-position vport position x y (some? (:gradient data)))
active-tokens (if (object? active-tokens)
(mfu/bean active-tokens)
active-tokens)
on-change' on-change'
(mf/use-fn (mf/use-fn
@@ -752,6 +757,10 @@
(some-> tokens-lib (some-> tokens-lib
(ctob/get-active-themes-set-names))) (ctob/get-active-themes-set-names)))
active-tokens (if (delay? active-tokens)
@active-tokens
active-tokens)
color-tokens (:color active-tokens) color-tokens (:color active-tokens)
grouped-tokens-by-set grouped-tokens-by-set
@@ -783,5 +792,6 @@
:on-token-change on-token-change :on-token-change on-token-change
:on-change on-change' :on-change on-change'
:origin origin :origin origin
:tab tab
:color-origin color-origin :color-origin color-origin
:on-accept on-accept}]])) :on-accept on-accept}]]))

View File

@@ -135,24 +135,11 @@
on-token-pill-click on-token-pill-click
(mf/use-fn (mf/use-fn
(mf/deps selected-shapes selected color-origin) (mf/deps selected-shapes)
(fn [event token] (fn [event token]
(dom/stop-propagation event) (dom/stop-propagation event)
(when (seq selected-shapes) (when (seq selected-shapes)
(if (= :color-selection color-origin) (on-token-change event token))))
(on-token-change event token)
(let [attributes (if (= color-origin :stroke) #{:stroke-color} #{:fill})
shape-ids (into #{} (map :id selected-shapes))]
(if (or
(and (= (:name token) has-stroke-tokens?) (= color-origin :stroke))
(and (= (:name token) has-color-tokens?) (= color-origin :fill)))
(st/emit! (dwta/unapply-token {:attributes attributes
:token token
:shape-ids shape-ids}))
(st/emit! (dwta/apply-token {:shape-ids shape-ids
:attributes attributes
:token token
:on-update-shape dwta/update-fill-stroke}))))))))
create-token-on-set create-token-on-set
(mf/use-fn (mf/use-fn

View File

@@ -52,6 +52,10 @@
(str/join ", ") (str/join ", ")
(str/ffmt "linear-gradient(90deg, %1)"))) (str/ffmt "linear-gradient(90deg, %1)")))
(defn- stop->hex-color
[stop]
(select-keys stop [:color :opacity]))
(mf/defc stop-input-row* (mf/defc stop-input-row*
{::mf/private true} {::mf/private true}
[{:keys [stop [{:keys [stop
@@ -156,7 +160,7 @@
[:> color-row* [:> color-row*
{:disable-gradient true {:disable-gradient true
:disable-picker true :disable-picker true
:color stop :color (stop->hex-color stop)
:index index :index index
:origin :gradient :origin :gradient
:on-change handle-change-stop-color :on-change handle-change-stop-color

View File

@@ -454,7 +454,7 @@
.sample-library-button { .sample-library-button {
@include t.use-typography("headline-small"); @include t.use-typography("headline-small");
height: $sz-32; height: $sz-32;
width: $sz-80; width: px2rem(80);
margin: 0; margin: 0;
border-radius: $br-8; border-radius: $br-8;
white-space: nowrap; white-space: nowrap;

View File

@@ -46,7 +46,7 @@
(mf/defc single-shape-options* (mf/defc single-shape-options*
{::mf/private true} {::mf/private true}
[{:keys [shape page-id file-id libraries] :as props}] [{:keys [shape page-id file-id libraries] :rest props}]
(let [shape-type (dm/get-prop shape :type) (let [shape-type (dm/get-prop shape :type)
shape-id (dm/get-prop shape :id) shape-id (dm/get-prop shape :id)

View File

@@ -108,6 +108,7 @@
color-operations (get groups color) color-operations (get groups color)
ids (into (d/ordered-set) xf:map-shape-id color-operations)] ids (into (d/ordered-set) xf:map-shape-id color-operations)]
(st/emit! (dws/select-shapes ids))))) (st/emit! (dws/select-shapes ids)))))
on-token-change on-token-change
(mf/use-fn (mf/use-fn
(fn [_ token old-color] (fn [_ token old-color]

View File

@@ -13,6 +13,7 @@
[app.config :as cfg] [app.config :as cfg]
[app.main.data.workspace :as udw] [app.main.data.workspace :as udw]
[app.main.data.workspace.colors :as dc] [app.main.data.workspace.colors :as dc]
[app.main.data.workspace.tokens.application :as dwta]
[app.main.store :as st] [app.main.store :as st]
[app.main.ui.components.title-bar :refer [title-bar*]] [app.main.ui.components.title-bar :refer [title-bar*]]
[app.main.ui.ds.buttons.icon-button :refer [icon-button*]] [app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
@@ -68,16 +69,24 @@
n-vals (unchecked-get n-props "values") n-vals (unchecked-get n-props "values")
o-fills (get o-vals :fills) o-fills (get o-vals :fills)
n-fills (get n-vals :fills) n-fills (get n-vals :fills)
o-objects (get o-vals :objects)
n-objects (get n-vals :objects)
o-applied-tokens (get o-vals :applied-tokens)
n-applied-tokens (get n-vals :applied-tokens)
o-hide (get o-vals :hide-fill-on-export) o-hide (get o-vals :hide-fill-on-export)
n-hide (get n-vals :hide-fill-on-export)] n-hide (get n-vals :hide-fill-on-export)]
(and (identical? o-hide n-hide) (and (identical? o-hide n-hide)
(identical? o-fills n-fills))))) (identical? o-applied-tokens n-applied-tokens)
(identical? o-fills n-fills)
(identical? o-objects n-objects)))))
(mf/defc fill-menu* (mf/defc fill-menu*
{::mf/wrap [#(mf/memo' % check-props)]} {::mf/wrap [#(mf/memo' % check-props)]}
[{:keys [ids type values]}] [{:keys [ids type values applied-tokens shapes objects]}]
(let [fills (get values :fills) (let [fills (get values :fills)
hide-on-export (get values :hide-fill-on-export false) hide-on-export (get values :hide-fill-on-export false)
fill-token-applied (:fill applied-tokens)
^boolean ^boolean
multiple? (= :multiple fills) multiple? (= :multiple fills)
@@ -172,7 +181,37 @@
#(reset! disable-drag* true)) #(reset! disable-drag* true))
on-blur on-blur
(mf/use-fn #(reset! disable-drag* false))] (mf/use-fn #(reset! disable-drag* false))
on-token-change
(mf/use-fn
(mf/deps shapes objects)
(fn [_ token]
(let [expanded-shapes
(if (= 1 (count shapes))
(let [shape (first shapes)]
(if (= (:type shape) :group)
(keep objects (:shapes shape))
[shape]))
(mapcat (fn [shape]
(if (= (:type shape) :group)
(keep objects (:shapes shape))
[shape]))
shapes))]
(st/emit!
(dwta/toggle-token {:token token
:attrs #{:fill}
:shapes expanded-shapes})))))
on-detach-token
(mf/use-fn
(mf/deps ids)
(fn [token]
(st/emit! (dwta/unapply-token {:attributes #{:fill}
:token token
:shape-ids ids}))))]
(mf/with-layout-effect [hide-on-export] (mf/with-layout-effect [hide-on-export]
(when-let [checkbox (mf/ref-val checkbox-ref)] (when-let [checkbox (mf/ref-val checkbox-ref)]
@@ -223,9 +262,12 @@
:on-change on-change :on-change on-change
:on-reorder on-reorder :on-reorder on-reorder
:on-detach on-detach :on-detach on-detach
:on-detach-token on-detach-token
:on-remove on-remove :on-remove on-remove
:disable-drag disable-drag? :disable-drag disable-drag?
:on-focus on-focus :on-focus on-focus
:applied-token fill-token-applied
:on-token-change on-token-change
:origin :fill :origin :fill
:select-on-focus (not disable-drag?) :select-on-focus (not disable-drag?)
:on-blur on-blur}]))]) :on-blur on-blur}]))])

View File

@@ -12,6 +12,7 @@
[app.common.types.stroke :as cts] [app.common.types.stroke :as cts]
[app.main.data.workspace :as udw] [app.main.data.workspace :as udw]
[app.main.data.workspace.colors :as dc] [app.main.data.workspace.colors :as dc]
[app.main.data.workspace.tokens.application :as dwta]
[app.main.store :as st] [app.main.store :as st]
[app.main.ui.components.title-bar :refer [title-bar*]] [app.main.ui.components.title-bar :refer [title-bar*]]
[app.main.ui.ds.buttons.icon-button :refer [icon-button*]] [app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
@@ -37,8 +38,8 @@
:stroke-cap-end]) :stroke-cap-end])
(mf/defc stroke-menu (mf/defc stroke-menu
{::mf/wrap [#(mf/memo' % (mf/check-props ["ids" "values" "type" "show-caps"]))]} {::mf/wrap [#(mf/memo' % (mf/check-props ["ids" "values" "type" "show-caps" "applied-tokens" "shapes" "objects"]))]}
[{:keys [ids type values show-caps disable-stroke-style] :as props}] [{:keys [ids type values show-caps disable-stroke-style applied-tokens shapes objects] :as props}]
(let [label (case type (let [label (case type
:multiple (tr "workspace.options.selection-stroke") :multiple (tr "workspace.options.selection-stroke")
:group (tr "workspace.options.group-stroke") :group (tr "workspace.options.group-stroke")
@@ -167,7 +168,14 @@
(reset! disable-drag true)) (reset! disable-drag true))
on-blur (fn [_] on-blur (fn [_]
(reset! disable-drag false))] (reset! disable-drag false))
on-detach-token
(mf/use-fn
(mf/deps ids)
(fn [token attrs]
(st/emit! (dwta/unapply-token {:attributes attrs
:token token
:shape-ids ids}))))]
[:div {:class (stl/css :element-set)} [:div {:class (stl/css :element-set)}
[:div {:class (stl/css :element-title)} [:div {:class (stl/css :element-title)}
@@ -201,6 +209,8 @@
:stroke value :stroke value
:title (tr "workspace.options.stroke-color") :title (tr "workspace.options.stroke-color")
:index index :index index
:shapes shapes
:objects objects
:show-caps show-caps :show-caps show-caps
:on-color-change on-color-change :on-color-change on-color-change
:on-color-detach on-color-detach :on-color-detach on-color-detach
@@ -212,6 +222,8 @@
:on-stroke-cap-start-change on-stroke-cap-start-change :on-stroke-cap-start-change on-stroke-cap-start-change
:on-stroke-cap-end-change on-stroke-cap-end-change :on-stroke-cap-end-change on-stroke-cap-end-change
:on-stroke-cap-switch on-stroke-cap-switch :on-stroke-cap-switch on-stroke-cap-switch
:applied-tokens applied-tokens
:on-detach-token on-detach-token
:on-remove on-remove :on-remove on-remove
:on-reorder (handle-reorder index) :on-reorder (handle-reorder index)
:disable-drag disable-drag :disable-drag disable-drag

View File

@@ -11,28 +11,27 @@
[app.common.data.macros :as dm] [app.common.data.macros :as dm]
[app.common.types.color :as clr] [app.common.types.color :as clr]
[app.common.types.shape.attrs :refer [default-color]] [app.common.types.shape.attrs :refer [default-color]]
[app.common.types.token :as tk]
[app.config :as cfg]
[app.main.data.modal :as modal] [app.main.data.modal :as modal]
[app.main.data.workspace.colors :as dwc] [app.main.data.workspace.colors :as dwc]
[app.main.refs :as refs] [app.main.refs :as refs]
[app.main.store :as st] [app.main.store :as st]
[app.main.ui.components.color-bullet :as cb]
[app.main.ui.components.color-input :refer [color-input*]] [app.main.ui.components.color-input :refer [color-input*]]
[app.main.ui.components.numeric-input :refer [numeric-input*]] [app.main.ui.components.numeric-input :refer [numeric-input*]]
[app.main.ui.components.reorder-handler :refer [reorder-handler*]] [app.main.ui.components.reorder-handler :refer [reorder-handler*]]
[app.main.ui.context :as ctx] [app.main.ui.context :as ctx]
[app.main.ui.ds.buttons.icon-button :refer [icon-button*]] [app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
[app.main.ui.ds.foundations.assets.icon :as i] [app.main.ui.ds.foundations.assets.icon :as i]
[app.main.ui.ds.tooltip.tooltip :refer [tooltip*]]
[app.main.ui.ds.utilities.swatch :refer [swatch*]]
[app.main.ui.formats :as fmt] [app.main.ui.formats :as fmt]
[app.main.ui.hooks :as h] [app.main.ui.hooks :as h]
[app.main.ui.icons :as deprecated-icon]
[app.util.color :as uc] [app.util.color :as uc]
[app.util.dom :as dom] [app.util.dom :as dom]
[app.util.i18n :as i18n :refer [tr]] [app.util.i18n :as i18n :refer [tr]]
[rumext.v2 :as mf])) [rumext.v2 :as mf]))
(def ^:private detach-icon
(deprecated-icon/icon-xref :detach (stl/css :detach-icon)))
(defn opacity->string (defn opacity->string
[opacity] [opacity]
(if (= opacity :multiple) (if (= opacity :multiple)
@@ -42,45 +41,139 @@
(* 100) (* 100)
(fmt/format-number))))) (fmt/format-number)))))
(defn remove-multiple (mf/defc color-info-wrapper*
[v] {::mf/private true}
(if (= v :multiple) nil v)) [{:keys [color class handle-click-color children select-on-focus opacity on-focus on-blur on-opacity-change]}]
[:div {:class (stl/css :color-info)}
[:div {:class class}
[:div {:class (stl/css :color-bullet-wrapper)}
[:> swatch* {:background color
:on-click handle-click-color
:size "small"}]]
children]
(when opacity
[:div {:class (stl/css :opacity-element-wrapper)}
[:span {:class (stl/css :icon-text)} "%"]
[:> numeric-input* {:value (-> color :opacity opacity->string)
:class (stl/css :opacity-input)
:placeholder "--"
:select-on-focus select-on-focus
:on-focus on-focus
:on-blur on-blur
:on-change on-opacity-change
:data-testid "opacity-input"
:default 100
:min 0
:max 100}]])])
(mf/defc color-token-row*
{::mf/private true}
[{:keys [active-tokens color-token color on-swatch-click-token detach-token open-modal-from-token]}]
(let [;; `active-tokens` may be provided as a `delay` (lazy computation).
;; In that case we must deref it (`@active-tokens`) to force evaluation
;; and obtain the actual value. If its already realized (not a delay),
;; we just use it directly.
active-tokens (if (delay? active-tokens)
@active-tokens
active-tokens)
color-tokens (:color active-tokens)
token (some #(when (= (:name %) color-token) %) color-tokens)
on-detach-token
(mf/use-fn
(mf/deps detach-token token)
#(detach-token token))
has-errors (some? (:errors token))
token-name (:name token)
resolved (:resolved-value token)
not-active (and (some? active-tokens) (nil? token))
id (dm/str (:id token) "-name")
swatch-tooltip-content (cond
not-active
(tr "ds.inputs.token-field.no-active-token-option")
has-errors
(tr "color-row.token-color-row.deleted-token")
:else
(tr "workspace.tokens.resolved-value" resolved))
name-tooltip-content (cond
not-active
(tr "ds.inputs.token-field.no-active-token-option")
has-errors
(tr "color-row.token-color-row.deleted-token")
:else
(mf/html
[:div
[:span (dm/str (tr "workspace.tokens.token-name") ": ")]
[:span {:class (stl/css :token-name-tooltip)} color-token]]))]
[:div {:class (stl/css :color-info)}
[:div {:class (stl/css-case :token-color-wrapper true
:token-color-with-errors has-errors
:token-color-not-active not-active)}
[:div {:class (stl/css :color-bullet-wrapper)}
(when (or has-errors not-active)
[:div {:class (stl/css :error-dot)}])
[:> swatch* {:background color
:tooltip-content swatch-tooltip-content
:on-click on-swatch-click-token
:has-errors (or has-errors not-active)
:size "small"}]]
[:> tooltip* {:content name-tooltip-content
:id id
:class (stl/css :token-tooltip)}
[:div {:class (stl/css :token-name)
:aria-labelledby id}
(or token-name color-token)]]
[:div {:class (stl/css :token-actions)}
[:> icon-button*
{:variant "action"
:aria-label (tr "ds.inputs.token-field.detach-token")
:on-click on-detach-token
:icon i/detach}]
[:> icon-button*
{:variant "action"
:aria-label (tr "ds.inputs.numeric-input.open-token-list-dropdown")
:on-click open-modal-from-token
:icon i/tokens}]]]]))
(mf/defc color-row* (mf/defc color-row*
[{:keys [index color class disable-gradient disable-opacity disable-image disable-picker hidden [{:keys [index color class disable-gradient disable-opacity disable-image disable-picker hidden
on-change on-reorder on-detach on-open on-close on-remove origin on-change on-reorder on-detach on-open on-close on-remove origin on-detach-token
disable-drag on-focus on-blur select-only select-on-focus on-token-change]}] disable-drag on-focus on-blur select-only select-on-focus on-token-change applied-token]}]
(let [libraries (mf/deref refs/files) (let [token-color (contains? cfg/flags :token-color)
libraries (mf/deref refs/files)
on-change (h/use-ref-callback on-change) on-change (h/use-ref-callback on-change)
on-token-change (h/use-ref-callback on-token-change) on-token-change (h/use-ref-callback on-token-change)
color-without-hash (mf/use-memo
(mf/deps color)
#(-> color :color clr/remove-hash))
file-id (or (:ref-file color) (:file-id color)) file-id (or (:ref-file color) (:file-id color))
color-id (or (:ref-id color) (:id color)) color-id (or (:ref-id color) (:id color))
src-colors (dm/get-in libraries [file-id :data :colors]) src-colors (dm/get-in libraries [file-id :data :colors])
color-name (dm/get-in src-colors [color-id :name]) color-name (dm/get-in src-colors [color-id :name])
multiple-colors? (uc/multiple? color) has-multiple-colors (uc/multiple? color)
library-color? (and (or (:id color) (:ref-id color)) color-name (not multiple-colors?)) library-color? (and (or (:id color) (:ref-id color)) color-name (not has-multiple-colors))
gradient-color? (and (not multiple-colors?) gradient-color? (and (not has-multiple-colors)
(:gradient color) (:gradient color)
(dm/get-in color [:gradient :type])) (dm/get-in color [:gradient :type]))
image-color? (and (not multiple-colors?) image-color? (and (not has-multiple-colors)
(:image color)) (:image color))
editing-text* (mf/use-state false) editing-text* (mf/use-state false)
editing-text? (deref editing-text*) is-editing-text (deref editing-text*)
class (if (some? class) (dm/str class " ") "")
active-tokens* (mf/use-ctx ctx/active-tokens-by-type) active-tokens* (mf/use-ctx ctx/active-tokens-by-type)
active-tokens (if active-tokens*
@active-tokens*
{})
opacity? tokens (mf/with-memo [active-tokens* origin]
(and (not multiple-colors?) (delay
(not library-color?) (-> (deref active-tokens*)
(not disable-opacity)) (select-keys (get tk/tokens-by-input origin))
(not-empty))))
on-focus' on-focus'
(mf/use-fn (mf/use-fn
@@ -138,12 +231,12 @@
(st/emit! (dwc/add-recent-color color) (st/emit! (dwc/add-recent-color color)
(on-change color index))))) (on-change color index)))))
handle-click-color open-modal
(mf/use-fn (mf/use-fn
(mf/deps disable-gradient disable-opacity disable-image disable-picker on-change on-close on-open active-tokens) (mf/deps disable-gradient disable-opacity disable-image disable-picker on-change on-close on-open tokens)
(fn [color event] (fn [color pos tab]
(let [color (cond (let [color (cond
multiple-colors? has-multiple-colors
{:color default-color {:color default-color
:opacity 1} :opacity 1}
@@ -152,13 +245,8 @@
:else :else
color) color)
props {:x (:x pos)
cpos (dom/get-client-position event) :y (:y pos)
x (dm/get-prop cpos :x)
y (dm/get-prop cpos :y)
props {:x x
:y y
:disable-gradient disable-gradient :disable-gradient disable-gradient
:disable-opacity disable-opacity :disable-opacity disable-opacity
:disable-image disable-image :disable-image disable-image
@@ -168,8 +256,9 @@
:on-close (fn [value opacity id file-id] :on-close (fn [value opacity id file-id]
(when on-close (when on-close
(on-close value opacity id file-id))) (on-close value opacity id file-id)))
:active-tokens active-tokens :active-tokens tokens
:color-origin origin :color-origin origin
:tab tab
:origin :sidebar :origin :sidebar
:data color}] :data color}]
@@ -179,6 +268,38 @@
(when-not disable-picker (when-not disable-picker
(modal/show! :colorpicker props))))) (modal/show! :colorpicker props)))))
handle-click-color
(mf/use-fn
(mf/deps open-modal)
(fn [color event]
(let [cpos (dom/get-client-position event)]
(open-modal color cpos nil))))
open-modal-from-token
(mf/use-fn
(mf/deps open-modal color)
(fn [event]
(let [cpos (dom/get-client-position event)
x (:x cpos)
y (:y cpos)
pos {:x (- x 215)
:y y}]
(open-modal color pos :token-color))))
on-swatch-click-token
(mf/use-fn
(mf/deps open-modal)
(fn [color event]
(let [cpos (dom/get-client-position event)]
(open-modal color cpos :token-color))))
detach-token
(mf/use-fn
(mf/deps on-detach-token)
(fn [token]
(when on-detach-token
(on-detach-token token))))
on-remove' on-remove'
(mf/use-fn (mf/use-fn
(mf/deps index) (mf/deps index)
@@ -207,88 +328,95 @@
:name (str "Color row" index)}) :name (str "Color row" index)})
[nil nil]) [nil nil])
row-class row-class
(stl/css-case :color-data true (stl/css-case :color-data true
:hidden hidden :hidden hidden
:dnd-over-top (= (:over dprops) :top) :dnd-over-top (= (:over dprops) :top)
:dnd-over-bot (= (:over dprops) :bot))] :dnd-over-bot (= (:over dprops) :bot))]
(mf/with-effect [color prev-color disable-picker] (mf/with-effect [color prev-color disable-picker]
(when (and (not disable-picker) (not= prev-color color)) (when (and (not disable-picker) (not= prev-color color))
(modal/update-props! :colorpicker {:data (parse-color color)}))) (modal/update-props! :colorpicker {:data (parse-color color)})))
[:div {:class [class row-class]} [:div {:class [class row-class]}
;; Drag handler ;; Drag handler
(when (some? on-reorder) (when (some? on-reorder)
[:> reorder-handler* {:ref dref}]) [:> reorder-handler* {:ref dref}])
[:div {:class (stl/css :color-info)}
[:div {:class (stl/css-case :color-name-wrapper true
:no-opacity (or disable-opacity
(not opacity?))
:library-name-wrapper library-color?
:editing editing-text?
:gradient-name-wrapper gradient-color?)}
[:div {:class (stl/css :color-bullet-wrapper)}
[:& cb/color-bullet {:color (cond-> color
(nil? color-name) (dissoc :ref-id :ref-file))
:mini true
:on-click handle-click-color}]]
(cond (cond
;; Rendering a color with ID (and token-color applied-token)
[:> color-token-row* {:active-tokens tokens
:color-token applied-token
:color (dissoc color :ref-id :ref-file)
:on-swatch-click-token on-swatch-click-token
:detach-token detach-token
:open-modal-from-token open-modal-from-token}]
library-color? library-color?
[:> color-info-wrapper* {:class (stl/css-case :color-name-wrapper true
:library-name-wrapper true)
:handle-click-color handle-click-color
:color color}
[:* [:*
[:div {:class (stl/css :color-name) [:div {:class (stl/css :color-name)
:title (str color-name)} :title (str color-name)}
(str color-name)] (str color-name)]
(when on-detach [:> icon-button*
[:button {:variant "ghost"
{:class (stl/css :detach-btn) :class (stl/css :detach-btn)
:title (tr "settings.detach") :aria-label (tr "settings.detach")
:on-click detach-value} :on-click detach-value
detach-icon])] :icon i/detach}]]]
;; Rendering a gradient
gradient-color? gradient-color?
[:> color-info-wrapper* {:class (stl/css-case :color-name-wrapper true
:no-opacity disable-opacity
:gradient-name-wrapper true)
:handle-click-color handle-click-color
:color color
:opacity true
:select-on-focus select-on-focus
:on-focus on-focus'
:on-blur on-blur'
:on-opacity-change on-opacity-change}
[:div {:class (stl/css :color-name)} [:div {:class (stl/css :color-name)}
(uc/gradient-type->string (dm/get-in color [:gradient :type]))] (uc/gradient-type->string (dm/get-in color [:gradient :type]))]]
;; Rendering an image
image-color? image-color?
[:> color-info-wrapper* {:class (stl/css-case :color-name-wrapper true
:no-opacity disable-opacity)
:handle-click-color handle-click-color
:color color
:opacity true
:select-on-focus select-on-focus
:on-focus on-focus'
:on-blur on-blur'
:on-opacity-change on-opacity-change}
[:div {:class (stl/css :color-name)} [:div {:class (stl/css :color-name)}
(tr "media.image")] (tr "media.image")]]
;; Rendering a plain color
:else :else
[:> color-info-wrapper* {:class (stl/css-case :color-name-wrapper true
:no-opacity (or disable-opacity
has-multiple-colors)
:editing is-editing-text)
:handle-click-color handle-click-color
:color color
:opacity true
:select-on-focus select-on-focus
:on-focus on-focus'
:on-blur on-blur'
:on-opacity-change on-opacity-change}
[:span {:class (stl/css :color-input-wrapper)} [:span {:class (stl/css :color-input-wrapper)}
[:> color-input* {:value (if multiple-colors? [:> color-input* {:value (if has-multiple-colors
"" ""
(-> color :color clr/remove-hash)) color-without-hash)
:placeholder (tr "settings.multiple") :placeholder (tr "settings.multiple")
:data-index index :data-index index
:class (stl/css :color-input) :class (stl/css :color-input)
:on-focus on-focus' :on-focus on-focus'
:on-blur on-blur' :on-blur on-blur'
:on-change on-color-change}]])] :on-change on-color-change}]]])
(when opacity?
[:div {:class (stl/css :opacity-element-wrapper)}
[:span {:class (stl/css :icon-text)} "%"]
[:> numeric-input* {:value (-> color :opacity opacity->string)
:class (stl/css :opacity-input)
:placeholder "--"
:select-on-focus select-on-focus
:on-focus on-focus'
:on-blur on-blur'
:on-change on-opacity-change
:data-testid "opacity-input"
:default 100
:min 0
:max 100}]])]
(when (some? on-remove) (when (some? on-remove)
[:> icon-button* {:variant "ghost" [:> icon-button* {:variant "ghost"
@@ -300,4 +428,3 @@
:aria-label (tr "settings.select-this-color") :aria-label (tr "settings.select-this-color")
:on-click handle-select :on-click handle-select
:icon i/move}])])) :icon i/move}])]))

View File

@@ -4,15 +4,18 @@
// //
// Copyright (c) KALEIDOS INC // Copyright (c) KALEIDOS INC
@use "refactor/common-refactor.scss" as deprecated; @use "ds/typography.scss" as t;
@use "ds/_sizes.scss" as *;
@use "ds/_borders.scss" as *;
@use "ds/mixins.scss" as *;
@use "ds/_utils.scss" as *;
.color-data { .color-data {
@include deprecated.flexRow; display: flex;
align-items: center;
gap: var(--sp-xs);
position: relative; position: relative;
--reorder-left-position: calc(-1 * var(--sp-m) - var(--sp-xxs));
&:hover { &:hover {
--reorder-icon-visibility: visible; --reorder-icon-visibility: visible;
} }
@@ -34,156 +37,289 @@
--detach-icon-foreground-color: none; --detach-icon-foreground-color: none;
display: grid; display: grid;
flex: 1;
grid-template-columns: 1fr auto; grid-template-columns: 1fr auto;
align-items: center; align-items: center;
gap: deprecated.$s-2; flex: 1;
border-radius: deprecated.$s-8; gap: var(--sp-xxs);
background-color: var(--input-details-color); block-size: $sz-32;
height: deprecated.$s-32; border-radius: $br-8;
background-color: var(--color-background-primary);
&:hover { &:hover {
--detach-icon-foreground-color: var(--input-foreground-color-active); --detach-icon-foreground-color: var(--color-foreground-primary);
.detach-btn,
.select-btn {
background-color: transparent;
}
} }
} }
.color-name-wrapper { .color-name-wrapper {
@extend .input-element; --color-name-wrapper-background-color: var(--color-background-tertiary);
@include deprecated.bodySmallTypography; --color-name-wrapper-foreground-color: var(--color-foreground-primary);
--color-name-wrapper-boder-color: var(--color-background-tertiary);
@include t.use-typography("body-small");
@include textEllipsis;
display: flex;
align-items: center;
flex-grow: 1; flex-grow: 1;
width: 100%; gap: var(--sp-xs);
min-width: 0; block-size: $sz-32;
border-radius: deprecated.$br-8 0 0 deprecated.$br-8; min-inline-size: 0;
inline-size: 100%;
padding: 0; padding: 0;
margin-inline-end: 0; margin-inline-end: 0;
gap: deprecated.$s-4; border: $b-1 solid var(--color-name-wrapper-boder-color);
border-radius: $br-8;
background-color: var(--color-name-wrapper-background-color);
color: var(--color-name-wrapper-foreground-color);
border-radius: $br-8 0 0 $br-8;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
input {
padding: 0;
}
.color-bullet-wrapper {
height: deprecated.$s-28;
padding: 0 deprecated.$s-2 0 deprecated.$s-8;
border-radius: deprecated.$br-8 0 0 deprecated.$br-8;
background-color: transparent;
display: flex;
align-items: center;
&:hover {
background-color: transparent;
}
}
.color-name {
@include deprecated.bodySmallTypography;
@include deprecated.textEllipsis;
padding-inline: deprecated.$s-6;
border-radius: deprecated.$br-8;
color: var(--input-foreground-color-active);
}
.detach-btn {
@extend .button-tertiary;
height: deprecated.$s-28;
width: deprecated.$s-28;
margin-inline-start: auto;
border-radius: 0 deprecated.$br-8 deprecated.$br-8 0;
display: none;
}
.detach-icon {
@extend .button-icon;
stroke: var(--detach-icon-foreground-color);
}
.color-input-wrapper {
@include deprecated.bodySmallTypography;
display: flex;
align-items: center;
height: deprecated.$s-28;
padding: 0 deprecated.$s-0;
width: 100%;
margin: 0;
flex-grow: 1;
background-color: var(--input-background-color);
color: var(--input-foreground-color);
border-radius: deprecated.$br-0;
}
&.no-opacity { &.no-opacity {
border-radius: deprecated.$br-8; border-radius: $br-8;
.color-input-wrapper { .color-input-wrapper {
border-radius: deprecated.$br-8; border-radius: $br-8;
} }
} }
&:hover { &:hover {
--detach-icon-foreground-color: var(--input-foreground-color-active); --detach-icon-foreground-color: var(--input-foreground-color-active);
--color-name-wrapper-background-color: var(--color-background-quaternary);
--color-name-wrapper-boder-color: var(--color-background-quaternary);
background-color: var(--input-background-color-hover);
border: deprecated.$s-1 solid var(--input-border-color-hover);
.color-bullet-wrapper,
.color-name,
.detach-btn,
.color-input-wrapper {
background-color: var(--input-background-color-hover);
}
.detach-btn { .detach-btn {
display: grid;
}
&.editing {
--color-name-wrapper-background-color: var(--color-background-primary);
}
&:focus,
&:focus-within {
--color-name-wrapper-background-color: var(--color-background-tertiary);
--color-name-wrapper-boder-color: var(--color-accent-primary);
}
}
&:focus,
&:focus-within {
--color-name-wrapper-background-color: var(--color-background-tertiary);
--color-name-wrapper-boder-color: var(--color-accent-primary);
&:hover {
--color-name-wrapper-background-color: var(--color-background-quaternary);
}
}
&.editing {
--color-name-wrapper-background-color: var(--color-background-primary);
&:hover {
--color-name-wrapper-boder-color: var(--color-accent-primary);
}
}
}
.detach-btn {
display: none;
background: var(--color-name-wrapper-background-color);
}
.color-input-wrapper {
@include t.use-typography("body-small");
display: flex; display: flex;
} align-items: center;
&.editing { flex-grow: 1;
background-color: var(--input-background-color-active); block-size: $sz-28;
.color-bullet-wrapper, inline-size: 100%;
.color-name, padding: 0;
.detach-btn, margin: 0;
.color-input-wrapper { background-color: var(--color-name-wrapper-background-color);
background-color: var(--input-background-color-active); color: var(--color-name-wrapper-foreground-color);
} border-radius: 0;
} }
&:focus,
&:focus-within {
background-color: var(--input-background-color-focus);
border: deprecated.$s-1 solid var(--input-border-color-focus);
}
}
&:focus, .color-name {
&:focus-within { @include t.use-typography("body-small");
background-color: var(--input-background-color-focus); @include textEllipsis;
border: deprecated.$s-1 solid var(--input-border-color-focus); flex-grow: 1;
&:hover { padding-inline: px2rem(6);
background-color: var(--input-background-color-hover); border-radius: $br-8;
border: deprecated.$s-1 solid var(--input-border-color-focus); color: var(--color-name-wrapper-foreground-color);
} }
}
&.editing { .color-bullet-wrapper {
background-color: var(--input-background-color-active); display: flex;
align-items: center;
position: relative;
block-size: $sz-28;
padding: 0 var(--sp-xxs) 0 var(--sp-s);
border-radius: $br-8 0 0 $br-8;
background-color: transparent;
&:hover { &:hover {
border: deprecated.$s-1 solid var(--input-border-color-active); background-color: transparent;
} }
}
.color-input {
@include textEllipsis;
border: none;
background: none;
outline: none;
block-size: $sz-28;
inline-size: 100%;
flex-grow: 1;
margin: var(--sp-xxs) 0;
padding: 0 0 0 px2rem(6);
border-radius: $br-8;
color: var(--input-foreground-color-active);
&[disabled] {
opacity: 0.5;
pointer-events: none;
} }
} }
.library-name-wrapper { .library-name-wrapper {
border-radius: deprecated.$br-8; border-radius: $br-8;
} }
.opacity-element-wrapper { .opacity-element-wrapper {
@extend .input-element; --opacity-input-background-color: var(--color-background-tertiary);
@include deprecated.bodySmallTypography; --opacity-input-boder-color: var(--color-background-tertiary);
width: deprecated.$s-60;
border-radius: 0 deprecated.$br-8 deprecated.$br-8 0; @include t.use-typography("body-small");
.opacity-input { display: flex;
padding: 0; align-items: center;
border-radius: 0 deprecated.$br-8 deprecated.$br-8 0; block-size: $sz-32;
min-width: deprecated.$s-28; inline-size: px2rem(60);
border-radius: 0 $br-8 $br-8 0;
border: $b-1 solid var(--opacity-input-boder-color);
background-color: var(--opacity-input-background-color);
&:hover {
--opacity-input-background-color: var(--color-background-quaternary);
--opacity-input-boder-color: var(--color-background-quaternary);
.detach-btn {
display: grid;
}
&.editing {
--opacity-input-background-color: var(--color-background-primary);
}
&:focus,
&:focus-within {
--opacity-input-background-color: var(--color-background-tertiary);
--opacity-input-boder-color: var(--color-accent-primary);
}
}
&:focus,
&:focus-within {
--opacity-input-background-color: var(--color-background-tertiary);
--opacity-input-boder-color: var(--color-accent-primary);
&:hover {
--opacity-input-background-color: var(--color-background-quaternary);
}
}
&.editing {
--opacity-input-background-color: var(--color-background-primary);
&:hover {
--opacity-input-boder-color: var(--color-accent-primary);
} }
.icon-text {
@include deprecated.flexCenter;
height: deprecated.$s-32;
margin-inline-end: deprecated.$s-4;
margin-block-start: deprecated.$s-2;
} }
} }
.opacity-input {
@include textEllipsis;
block-size: $sz-28;
min-inline-size: $sz-28;
flex-grow: 1;
inline-size: 100%;
padding: 0;
border-radius: 0 $br-8 $br-8 0;
border: none;
background: none;
outline: none;
margin: var(--sp-xxs) 0;
padding: 0 0 0 px2rem(6);
color: var(--color-foreground-primary);
&[disabled] {
opacity: 0.5;
pointer-events: none;
}
}
.icon-text {
display: flex;
justify-content: center;
align-items: center;
block-size: $sz-32;
margin-inline-end: var(--sp-xs);
margin-block-start: var(--sp-xxs);
color: var(--color-foreground-secondary);
}
// TOKEN ROW
.token-color-wrapper {
--token-color-wrapper-background-color: var(--color-background-tertiary);
--token-color-wrapper-foreground-color: var(--color-token-foreground);
--token-color-wrapper-border-color: var(--color-token-border);
--token-actions-display: none;
display: grid;
grid-template-columns: auto 1fr auto;
gap: var(--sp-xs);
block-size: $sz-32;
min-inline-size: 0;
inline-size: 100%;
padding: 0;
margin-inline-end: 0;
background: var(--token-color-wrapper-background-color);
border: $b-1 solid var(--token-color-wrapper-border-color);
border-radius: $br-8;
&:hover {
--token-color-wrapper-background-color: var(--color-token-background);
--token-color-wrapper-foreground-color: var(--color-foreground-primary);
--token-color-wrapper-border-color: var(--color-token-background);
--token-actions-display: flex;
}
}
.token-color-with-errors,
.token-color-not-active {
--token-color-wrapper-background-color: var(--color-background-primary);
--token-color-wrapper-foreground-color: var(--color-foreground-secondary);
--token-color-wrapper-border-color: var(--color-token-border);
&:hover {
--token-color-wrapper-background-color: var(--color-background-primary);
--token-color-wrapper-foreground-color: var(--color-foreground-secondary);
--token-color-wrapper-border-color: var(--color-token-background);
--token-actions-display: flex;
}
}
.token-name {
@include t.use-typography("body-small");
@include textEllipsis;
color: var(--token-color-wrapper-foreground-color);
block-size: $sz-32;
display: flex;
align-items: center;
}
.token-name-tooltip {
color: var(--color-foreground-primary);
}
.token-actions {
display: var(--token-actions-display);
justify-self: flex-end;
align-items: center;
}
.error-dot {
inline-size: px2rem(4);
block-size: px2rem(4);
border-radius: 50%;
background-color: var(--color-foreground-error);
margin-inline-start: var(--sp-xs);
position: absolute;
inset-inline-end: px2rem(1);
inset-block-start: px2rem(5);
}

View File

@@ -9,6 +9,8 @@
(:require (:require
[app.common.data :as d] [app.common.data :as d]
[app.common.types.color :as ctc] [app.common.types.color :as ctc]
[app.main.data.workspace.tokens.application :as dwta]
[app.main.store :as st]
[app.main.ui.components.numeric-input :refer [numeric-input*]] [app.main.ui.components.numeric-input :refer [numeric-input*]]
[app.main.ui.components.reorder-handler :refer [reorder-handler*]] [app.main.ui.components.reorder-handler :refer [reorder-handler*]]
[app.main.ui.components.select :refer [select]] [app.main.ui.components.select :refer [select]]
@@ -37,8 +39,12 @@
disable-drag disable-drag
on-focus on-focus
on-blur on-blur
applied-tokens
on-detach-token
disable-stroke-style disable-stroke-style
select-on-focus]}] select-on-focus
shapes
objects]}]
(let [on-drop (let [on-drop
(fn [_ data] (fn [_ data]
@@ -55,27 +61,29 @@
:name (str "Border row" index)}) :name (str "Border row" index)})
[nil nil]) [nil nil])
stroke-color-token (:stroke-color applied-tokens)
on-color-change-refactor on-color-change-refactor
(mf/use-callback (mf/use-fn
(mf/deps index on-color-change) (mf/deps index on-color-change)
(fn [color] (fn [color]
(on-color-change index color))) (on-color-change index color)))
on-color-detach on-color-detach
(mf/use-callback (mf/use-fn
(mf/deps index on-color-detach) (mf/deps index on-color-detach)
(fn [color] (fn [color]
(on-color-detach index color))) (on-color-detach index color)))
on-remove on-remove
(mf/use-callback (mf/use-fn
(mf/deps index on-remove) (mf/deps index on-remove)
#(on-remove index)) #(on-remove index))
stroke-width (:stroke-width stroke) stroke-width (:stroke-width stroke)
on-width-change on-width-change
(mf/use-callback (mf/use-fn
(mf/deps index on-stroke-width-change) (mf/deps index on-stroke-width-change)
#(on-stroke-width-change index %)) #(on-stroke-width-change index %))
@@ -91,12 +99,35 @@
{:value :outer :label (tr "workspace.options.stroke.outer")}])) {:value :outer :label (tr "workspace.options.stroke.outer")}]))
on-alignment-change on-alignment-change
(mf/use-callback (mf/use-fn
(mf/deps index on-stroke-alignment-change) (mf/deps index on-stroke-alignment-change)
#(on-stroke-alignment-change index (keyword %))) #(on-stroke-alignment-change index (keyword %)))
on-token-change
(mf/use-fn
(mf/deps shapes objects)
(fn [_ token]
(let [expanded-shapes
(if (= 1 (count shapes))
(let [shape (first shapes)]
(if (= (:type shape) :group)
(keep objects (:shapes shape))
[shape]))
(mapcat (fn [shape]
(if (= (:type shape) :group)
(keep objects (:shapes shape))
[shape]))
shapes))]
(st/emit!
(dwta/toggle-token {:token token
:attrs #{:stroke-color}
:shapes expanded-shapes})))))
stroke-style (or (:stroke-style stroke) :solid) stroke-style (or (:stroke-style stroke) :solid)
stroke-style-options stroke-style-options
(mf/with-memo [stroke-style] (mf/with-memo [stroke-style]
(d/concat-vec (d/concat-vec
@@ -108,20 +139,26 @@
{:value :mixed :label (tr "workspace.options.stroke.mixed")}])) {:value :mixed :label (tr "workspace.options.stroke.mixed")}]))
on-style-change on-style-change
(mf/use-callback (mf/use-fn
(mf/deps index on-stroke-style-change) (mf/deps index on-stroke-style-change)
#(on-stroke-style-change index (keyword %))) #(on-stroke-style-change index (keyword %)))
on-caps-start-change on-caps-start-change
(mf/use-callback (mf/use-fn
(mf/deps index on-stroke-cap-start-change) (mf/deps index on-stroke-cap-start-change)
#(on-stroke-cap-start-change index (keyword %))) #(on-stroke-cap-start-change index (keyword %)))
on-caps-end-change on-caps-end-change
(mf/use-callback (mf/use-fn
(mf/deps index on-stroke-cap-end-change) (mf/deps index on-stroke-cap-end-change)
#(on-stroke-cap-end-change index (keyword %))) #(on-stroke-cap-end-change index (keyword %)))
on-detach-token-color
(mf/use-fn
(mf/deps on-detach-token)
(fn [token]
(on-detach-token token #{:stroke-color})))
stroke-caps-options stroke-caps-options
[{:value nil :label (tr "workspace.options.stroke-cap.none")} [{:value nil :label (tr "workspace.options.stroke-cap.none")}
:separator :separator
@@ -135,7 +172,7 @@
{:value :square :label (tr "workspace.options.stroke-cap.square") :icon :stroke-squared}] {:value :square :label (tr "workspace.options.stroke-cap.square") :icon :stroke-squared}]
on-cap-switch on-cap-switch
(mf/use-callback (mf/use-fn
(mf/deps index on-stroke-cap-switch) (mf/deps index on-stroke-cap-switch)
#(on-stroke-cap-switch index))] #(on-stroke-cap-switch index))]
@@ -156,8 +193,11 @@
:on-detach on-color-detach :on-detach on-color-detach
:on-remove on-remove :on-remove on-remove
:disable-drag disable-drag :disable-drag disable-drag
:applied-token stroke-color-token
:on-detach-token on-detach-token-color
:on-token-change on-token-change
:on-focus on-focus :on-focus on-focus
:origin :stroke :origin :stroke-color
:select-on-focus select-on-focus :select-on-focus select-on-focus
:on-blur on-blur}] :on-blur on-blur}]

View File

@@ -30,7 +30,8 @@
shapes (mf/with-memo [shape] [shape]) shapes (mf/with-memo [shape] [shape])
applied-tokens applied-tokens
(get shape :applied-tokens) (when (seq (get shape :applied-tokens))
(get shape :applied-tokens))
measure-values measure-values
(select-keys shape measure-attrs) (select-keys shape measure-attrs)
@@ -120,12 +121,16 @@
[:> fill/fill-menu* [:> fill/fill-menu*
{:ids ids {:ids ids
:type type :type type
:values shape}] :values shape
:shapes shapes
:applied-tokens applied-tokens}]
[:& stroke-menu {:ids ids [:& stroke-menu {:ids ids
:type type :type type
:show-caps true :show-caps true
:values stroke-values}] :values stroke-values
:shapes shapes
:applied-tokens applied-tokens}]
[:> shadow-menu* {:ids ids :values (get shape :shadow)}] [:> shadow-menu* {:ids ids :values (get shape :shadow)}]
[:& blur-menu {:ids ids [:& blur-menu {:ids ids

View File

@@ -120,10 +120,15 @@
[:> fill/fill-menu* [:> fill/fill-menu*
{:ids ids {:ids ids
:type type :type type
:values shape}] :values shape
:shapes shapes
:applied-tokens applied-tokens}]
[:& stroke-menu {:ids ids [:& stroke-menu {:ids ids
:type type :type type
:values stroke-values}] :values stroke-values
:shapes shapes
:applied-tokens applied-tokens}]
[:> shadow-menu* {:ids ids :values (get shape :shadow)}] [:> shadow-menu* {:ids ids :values (get shape :shadow)}]
[:& blur-menu {:ids ids [:& blur-menu {:ids ids
:values (select-keys shape [:blur])}] :values (select-keys shape [:blur])}]

View File

@@ -143,11 +143,15 @@
[:> fill/fill-menu* [:> fill/fill-menu*
{:ids ids {:ids ids
:type shape-type :type shape-type
:values shape}] :values shape
:shapes shapes
:applied-tokens applied-tokens}]
[:& stroke-menu {:ids ids [:& stroke-menu {:ids ids
:type shape-type :type shape-type
:values stroke-values}] :values stroke-values
:shapes shapes
:applied-tokens applied-tokens}]
[:> color-selection-menu* {:type shape-type [:> color-selection-menu* {:type shape-type
:shapes shapes-with-children :shapes shapes-with-children
:file-id file-id :file-id file-id

View File

@@ -90,7 +90,7 @@
[constraint-ids constraint-values] [constraint-ids constraint-values]
(get-attrs shapes objects :constraint) (get-attrs shapes objects :constraint)
[fill-ids fill-values] [fill-ids fill-values fill-tokens]
(get-attrs shapes objects :fill) (get-attrs shapes objects :fill)
[shadow-ids] [shadow-ids]
@@ -99,7 +99,7 @@
[blur-ids blur-values] [blur-ids blur-values]
(get-attrs shapes objects :blur) (get-attrs shapes objects :blur)
[stroke-ids stroke-values] [stroke-ids stroke-values stroke-tokens]
(get-attrs shapes objects :stroke) (get-attrs shapes objects :stroke)
[text-ids text-values] [text-ids text-values]
@@ -143,10 +143,21 @@
[:& constraints-menu {:ids constraint-ids :values constraint-values}]) [:& constraints-menu {:ids constraint-ids :values constraint-values}])
(when-not (empty? fill-ids) (when-not (empty? fill-ids)
[:> fill/fill-menu* {:type type :ids fill-ids :values fill-values}]) [:> fill/fill-menu*
{:type type
:ids fill-ids
:values fill-values
:shapes shapes
:objects objects
:applied-tokens fill-tokens}])
(when-not (empty? stroke-ids) (when-not (empty? stroke-ids)
[:& stroke-menu {:type type :ids stroke-ids :values stroke-values}]) [:& stroke-menu {:type type
:ids stroke-ids
:values stroke-values
:shapes shapes
:objects objects
:applied-tokens stroke-tokens}])
[:> color-selection-menu* [:> color-selection-menu*
{:type type {:type type

View File

@@ -17,6 +17,7 @@
[app.common.types.shape.attrs :refer [editable-attrs]] [app.common.types.shape.attrs :refer [editable-attrs]]
[app.common.types.shape.layout :as ctl] [app.common.types.shape.layout :as ctl]
[app.common.types.text :as txt] [app.common.types.text :as txt]
[app.common.types.token :as tt]
[app.common.weak :as weak] [app.common.weak :as weak]
[app.main.refs :as refs] [app.main.refs :as refs]
[app.main.ui.workspace.sidebar.options.menus.blur :refer [blur-attrs blur-menu]] [app.main.ui.workspace.sidebar.options.menus.blur :refer [blur-attrs blur-menu]]
@@ -211,18 +212,34 @@
(= attr-group :blur) (attrs/get-attrs-multi [v1 v2] attrs blur-eq blur-sel) (= attr-group :blur) (attrs/get-attrs-multi [v1 v2] attrs blur-eq blur-sel)
:else (attrs/get-attrs-multi [v1 v2] attrs))) :else (attrs/get-attrs-multi [v1 v2] attrs)))
merge-token-values merge-attr
(fn [acc keys attrs] (fn [acc applied-tokens t-attr]
(reduce "Merges a single token attribute (`t-attr`) into the accumulator map.
(fn [accum key] - If the attribute is not present, associates it with the new value.
(let [new-val (get attrs key) - If the existing value equals the new value, keeps the accumulator unchanged.
existing (get accum key ::not-found)] - If there is a conflict, sets the value to `:multiple`."
(let [new-val (get applied-tokens t-attr)
existing (get acc t-attr ::not-found)]
(cond (cond
(= existing ::not-found) (assoc accum key new-val) (= existing ::not-found) (assoc acc t-attr new-val)
(= existing new-val) accum (= existing new-val) acc
:else (assoc accum key :multiple)))) :else (assoc acc t-attr :multiple))))
acc
keys)) merge-shape-attr
(fn [acc applied-tokens shape-attr]
"Merges all token attributes derived from a single shape attribute
into the accumulator map using `merge-attr`."
(let [token-attrs (tt/shape-attr->token-attrs shape-attr)]
(reduce #(merge-attr %1 applied-tokens %2) acc token-attrs)))
merge-token-values
(fn [acc shape-attrs applied-tokens]
"Merges token values across all shape attributes.
For each shape attribute, its corresponding token attributes are merged
into the accumulator. If applied tokens are empty, the accumulator is returned unchanged."
(if (seq applied-tokens)
(reduce #(merge-shape-attr %1 applied-tokens %2) acc shape-attrs)
acc))
extract-attrs extract-attrs
(fn [[ids values token-acc] {:keys [id type applied-tokens] :as shape}] (fn [[ids values token-acc] {:keys [id type applied-tokens] :as shape}]
@@ -263,8 +280,8 @@
:children :children
(let [children (->> (:shapes shape []) (map #(get objects %))) (let [children (->> (:shapes shape []) (map #(get objects %)))
[new-ids new-values] (get-attrs* children objects attr-group)] [new-ids new-values tokens] (get-attrs* children objects attr-group)]
[(d/concat-vec ids new-ids) (merge-attrs values new-values) {}]) [(d/concat-vec ids new-ids) (merge-attrs values new-values) tokens])
[])))] [])))]
@@ -376,7 +393,7 @@
[constraint-ids constraint-values] [constraint-ids constraint-values]
(get-attrs shapes objects :constraint) (get-attrs shapes objects :constraint)
[fill-ids fill-values] [fill-ids fill-values fill-tokens]
(get-attrs shapes objects :fill) (get-attrs shapes objects :fill)
[shadow-ids shadow-values] [shadow-ids shadow-values]
@@ -385,7 +402,7 @@
[blur-ids blur-values] [blur-ids blur-values]
(get-attrs shapes objects :blur) (get-attrs shapes objects :blur)
[stroke-ids stroke-values] [stroke-ids stroke-values stroke-tokens]
(get-attrs shapes objects :stroke) (get-attrs shapes objects :stroke)
[exports-ids exports-values] [exports-ids exports-values]
@@ -463,14 +480,22 @@
[:& ot/text-menu {:type type :ids text-ids :values text-values}]) [:& ot/text-menu {:type type :ids text-ids :values text-values}])
(when-not (empty? fill-ids) (when-not (empty? fill-ids)
[:> fill/fill-menu* {:type type :ids fill-ids :values fill-values}]) [:> fill/fill-menu* {:type type
:ids fill-ids
:values fill-values
:shapes shapes
:objects objects
:applied-tokens fill-tokens}])
(when-not (empty? stroke-ids) (when-not (empty? stroke-ids)
[:& stroke-menu {:type type [:& stroke-menu {:type type
:ids stroke-ids :ids stroke-ids
:show-caps show-caps? :show-caps show-caps?
:values stroke-values :values stroke-values
:disable-stroke-style has-text?}]) :shapes shapes
:objects objects
:disable-stroke-style has-text?
:applied-tokens stroke-tokens}])
(when-not (empty? shapes) (when-not (empty? shapes)
[:> color-selection-menu* [:> color-selection-menu*

View File

@@ -120,12 +120,16 @@
[:> fill/fill-menu* [:> fill/fill-menu*
{:ids ids {:ids ids
:type type :type type
:values shape}] :values shape
:shapes shapes
:applied-tokens applied-tokens}]
[:& stroke-menu {:ids ids [:& stroke-menu {:ids ids
:type type :type type
:show-caps true :show-caps true
:values stroke-values}] :values stroke-values
:shapes shapes
:applied-tokens applied-tokens}]
[:> shadow-menu* {:ids ids :values (get shape :shadow)}] [:> shadow-menu* {:ids ids :values (get shape :shadow)}]
[:& blur-menu {:ids ids [:& blur-menu {:ids ids
:values (select-keys shape [:blur])}] :values (select-keys shape [:blur])}]

View File

@@ -120,11 +120,15 @@
[:> fill/fill-menu* [:> fill/fill-menu*
{:ids ids {:ids ids
:type type :type type
:values shape}] :shapes shapes
:values shape
:applied-tokens applied-tokens}]
[:& stroke-menu {:ids ids [:& stroke-menu {:ids ids
:type type :type type
:values stroke-values}] :shapes shapes
:values stroke-values
:applied-tokens applied-tokens}]
[:> shadow-menu* {:ids ids :values (get shape :shadow)}] [:> shadow-menu* {:ids ids :values (get shape :shadow)}]

View File

@@ -188,11 +188,15 @@
[:> fill/fill-menu* [:> fill/fill-menu*
{:ids ids {:ids ids
:type type :type type
:values fill-values}] :values fill-values
:shapes shapes
:applied-tokens applied-tokens}]
[:& stroke-menu {:ids ids [:& stroke-menu {:ids ids
:type type :type type
:values stroke-values}] :values stroke-values
:shapes shapes
:applied-tokens applied-tokens}]
[:> shadow-menu* {:ids ids :values (get shape :shadow)}] [:> shadow-menu* {:ids ids :values (get shape :shadow)}]

View File

@@ -173,12 +173,16 @@
[:> fill/fill-menu* [:> fill/fill-menu*
{:ids ids {:ids ids
:type type :type type
:values fill-values}] :values fill-values
:shapes shapes
:applied-tokens applied-tokens}]
[:& stroke-menu {:ids ids [:& stroke-menu {:ids ids
:type type :type type
:values stroke-values :values stroke-values
:disable-stroke-style true}] :shapes shapes
:disable-stroke-style true
:applied-tokens applied-tokens}]
(when (= :multiple (:fills fill-values)) (when (= :multiple (:fills fill-values))
[:> color-selection-menu* [:> color-selection-menu*

View File

@@ -1184,7 +1184,11 @@ msgstr "Detach token"
#: src/app/main/ui/ds/controls/utilities/token_field.cljs:39 #: src/app/main/ui/ds/controls/utilities/token_field.cljs:39
msgid "ds.inputs.token-field.no-active-token-option" msgid "ds.inputs.token-field.no-active-token-option"
msgstr "This token is not available in any active set or theme." msgstr "This token is not in any active set or has an invalid value."
#: src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs
msgid "color-row.token-color-row.deleted-token"
msgstr "This token does not exists or has been deleted."
#: src/app/main/data/auth.cljs:314 #: src/app/main/data/auth.cljs:314
msgid "errors.auth-provider-not-allowed" msgid "errors.auth-provider-not-allowed"

View File

@@ -1196,7 +1196,11 @@ msgstr "Desvincular token"
#: src/app/main/ui/ds/controls/utilities/token_field.cljs:39 #: src/app/main/ui/ds/controls/utilities/token_field.cljs:39
msgid "ds.inputs.token-field.no-active-token-option" msgid "ds.inputs.token-field.no-active-token-option"
msgstr "Este token no está disponible en ningún set ni tema activo." msgstr "Este token no está disponible en ningún set o tiene un valor inválido."
#: src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs
msgid "color-row.token-color-row.deleted-token"
msgstr "Este token no existe o ha sido borrado."
#: src/app/main/data/auth.cljs:314 #: src/app/main/data/auth.cljs:314
msgid "errors.auth-provider-not-allowed" msgid "errors.auth-provider-not-allowed"