🎉 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
@@ -762,7 +771,7 @@
(filter-active-sets active-sets-names) (filter-active-sets active-sets-names)
(filter-non-empty-sets) (filter-non-empty-sets)
(group-sets) (group-sets)
(combine-groups-with-resolved color-tokens)))] (combine-groups-with-resolved color-tokens)))]
(mf/with-effect [] (mf/with-effect []
(st/emit! (st/emit! (dsc/push-shortcuts ::colorpicker sc/shortcuts))) (st/emit! (st/emit! (dsc/push-shortcuts ::colorpicker sc/shortcuts)))
@@ -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}])
(cond
(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}]
[:div {:class (stl/css :color-info)} library-color?
[:div {:class (stl/css-case :color-name-wrapper true [:> color-info-wrapper* {:class (stl/css-case :color-name-wrapper true
:no-opacity (or disable-opacity :library-name-wrapper true)
(not opacity?)) :handle-click-color handle-click-color
:library-name-wrapper library-color? :color color}
:editing editing-text? [:*
:gradient-name-wrapper gradient-color?)} [:div {:class (stl/css :color-name)
[:div {:class (stl/css :color-bullet-wrapper)} :title (str color-name)}
[:& cb/color-bullet {:color (cond-> color (str color-name)]
(nil? color-name) (dissoc :ref-id :ref-file)) [:> icon-button*
:mini true {:variant "ghost"
:on-click handle-click-color}]] :class (stl/css :detach-btn)
(cond :aria-label (tr "settings.detach")
;; Rendering a color with ID :on-click detach-value
library-color? :icon i/detach}]]]
[:*
[:div {:class (stl/css :color-name)
:title (str color-name)}
(str color-name)] gradient-color?
(when on-detach [:> color-info-wrapper* {:class (stl/css-case :color-name-wrapper true
[:button :no-opacity disable-opacity
{:class (stl/css :detach-btn) :gradient-name-wrapper true)
:title (tr "settings.detach") :handle-click-color handle-click-color
:on-click detach-value} :color color
detach-icon])] :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)}
(uc/gradient-type->string (dm/get-in color [:gradient :type]))]]
;; Rendering a gradient image-color?
gradient-color? [:> color-info-wrapper* {:class (stl/css-case :color-name-wrapper true
[:div {:class (stl/css :color-name)} :no-opacity disable-opacity)
(uc/gradient-type->string (dm/get-in color [:gradient :type]))] :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)}
(tr "media.image")]]
;; Rendering an image :else
image-color? [:> color-info-wrapper* {:class (stl/css-case :color-name-wrapper true
[:div {:class (stl/css :color-name)} :no-opacity (or disable-opacity
(tr "media.image")] 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}
;; Rendering a plain color [:span {:class (stl/css :color-input-wrapper)}
:else [:> color-input* {:value (if has-multiple-colors
[:span {:class (stl/css :color-input-wrapper)} ""
[:> color-input* {:value (if multiple-colors? color-without-hash)
"" :placeholder (tr "settings.multiple")
(-> color :color clr/remove-hash)) :data-index index
:placeholder (tr "settings.multiple") :class (stl/css :color-input)
:data-index index :on-focus on-focus'
:class (stl/css :color-input) :on-blur on-blur'
:on-focus on-focus' :on-change on-color-change}]]])
:on-blur on-blur'
: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"
@@ -299,5 +427,4 @@
[:> icon-button* {:variant "ghost" [:> icon-button* {:variant "ghost"
: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: flex; display: grid;
} }
&.editing { &.editing {
background-color: var(--input-background-color-active); --color-name-wrapper-background-color: var(--color-background-primary);
.color-bullet-wrapper,
.color-name,
.detach-btn,
.color-input-wrapper {
background-color: var(--input-background-color-active);
}
} }
&:focus, &:focus,
&:focus-within { &:focus-within {
background-color: var(--input-background-color-focus); --color-name-wrapper-background-color: var(--color-background-tertiary);
border: deprecated.$s-1 solid var(--input-border-color-focus); --color-name-wrapper-boder-color: var(--color-accent-primary);
} }
} }
&:focus, &:focus,
&:focus-within { &:focus-within {
background-color: var(--input-background-color-focus); --color-name-wrapper-background-color: var(--color-background-tertiary);
border: deprecated.$s-1 solid var(--input-border-color-focus); --color-name-wrapper-boder-color: var(--color-accent-primary);
&:hover { &:hover {
background-color: var(--input-background-color-hover); --color-name-wrapper-background-color: var(--color-background-quaternary);
border: deprecated.$s-1 solid var(--input-border-color-focus);
} }
} }
&.editing { &.editing {
background-color: var(--input-background-color-active); --color-name-wrapper-background-color: var(--color-background-primary);
&:hover { &:hover {
border: deprecated.$s-1 solid var(--input-border-color-active); --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;
align-items: center;
flex-grow: 1;
block-size: $sz-28;
inline-size: 100%;
padding: 0;
margin: 0;
background-color: var(--color-name-wrapper-background-color);
color: var(--color-name-wrapper-foreground-color);
border-radius: 0;
}
.color-name {
@include t.use-typography("body-small");
@include textEllipsis;
flex-grow: 1;
padding-inline: px2rem(6);
border-radius: $br-8;
color: var(--color-name-wrapper-foreground-color);
}
.color-bullet-wrapper {
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 {
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);
}
} }
.icon-text {
@include deprecated.flexCenter; &:focus,
height: deprecated.$s-32; &:focus-within {
margin-inline-end: deprecated.$s-4; --opacity-input-background-color: var(--color-background-tertiary);
margin-block-start: deprecated.$s-2; --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);
}
} }
} }
.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-attr
(fn [acc applied-tokens t-attr]
"Merges a single token attribute (`t-attr`) into the accumulator map.
- If the attribute is not present, associates it with the new value.
- If the existing value equals the new value, keeps the accumulator unchanged.
- 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
(= existing ::not-found) (assoc acc t-attr new-val)
(= existing new-val) acc
:else (assoc acc t-attr :multiple))))
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 merge-token-values
(fn [acc keys attrs] (fn [acc shape-attrs applied-tokens]
(reduce "Merges token values across all shape attributes.
(fn [accum key] For each shape attribute, its corresponding token attributes are merged
(let [new-val (get attrs key) into the accumulator. If applied tokens are empty, the accumulator is returned unchanged."
existing (get accum key ::not-found)] (if (seq applied-tokens)
(cond (reduce #(merge-shape-attr %1 applied-tokens %2) acc shape-attrs)
(= existing ::not-found) (assoc accum key new-val) acc))
(= existing new-val) accum
:else (assoc accum key :multiple))))
acc
keys))
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"