🎉 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}
:line-height #{:line-height :number}
: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).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 }) => {
@@ -175,7 +175,7 @@ test("Create a RADIAL gradient", async ({ page }) => {
await expect(inputOpacityGlobal).toHaveValue("50");
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 }) => {

View File

@@ -46,7 +46,7 @@ const setupTokensFile = async (page, options = {}) => {
} = options;
const workspacePage = new WorkspacePage(page);
if (flags.length) {
if (flags.length > 0) {
await workspacePage.mockConfigFlags(flags);
}
@@ -879,7 +879,10 @@ test.describe("Tokens: Themes modal", () => {
});
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 } =
await setupTokensFile(page);
@@ -909,6 +912,35 @@ test.describe("Tokens: Themes modal", () => {
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 }) => {
const { workspacePage, tokensSidebar, tokenContextMenuForToken } =
await setupTypographyTokensFile(page);
@@ -1024,9 +1056,7 @@ test.describe("Tokens: Themes modal", () => {
await expect(letterSpacingField).toHaveValue(
originalValues.letterSpacing,
);
await expect(lineHeightField).toHaveValue(
originalValues.lineHeight,
);
await expect(lineHeightField).toHaveValue(originalValues.lineHeight);
await expect(textCaseField).toHaveValue(originalValues.textCase);
await expect(textDecorationField).toHaveValue(
originalValues.textDecoration,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -47,7 +47,8 @@
[cuerdas.core :as str]
[okulary.core :as l]
[potok.v2.core :as ptk]
[rumext.v2 :as mf]))
[rumext.v2 :as mf]
[rumext.v2.util :as mfu]))
;; --- Refs
@@ -93,13 +94,13 @@
(dom/set-css-property! node "--saturation-grad-to" (format-hsl hsl-to)))))
(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)
node-ref (mf/use-ref)
should-update? (mf/use-var true)
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*)
toggle-token-color
(mf/use-fn
@@ -364,7 +365,7 @@
:icon i/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
(mf/with-effect []
@@ -726,12 +727,16 @@
color-origin
on-token-change
on-close
tab
on-accept]}]
(let [vport (mf/deref viewport)
dirty? (mf/use-var false)
last-change (mf/use-var nil)
position (d/nilv position :left)
style (calculate-position vport position x y (some? (:gradient data)))
active-tokens (if (object? active-tokens)
(mfu/bean active-tokens)
active-tokens)
on-change'
(mf/use-fn
@@ -752,6 +757,10 @@
(some-> tokens-lib
(ctob/get-active-themes-set-names)))
active-tokens (if (delay? active-tokens)
@active-tokens
active-tokens)
color-tokens (:color active-tokens)
grouped-tokens-by-set
@@ -762,7 +771,7 @@
(filter-active-sets active-sets-names)
(filter-non-empty-sets)
(group-sets)
(combine-groups-with-resolved color-tokens)))]
(combine-groups-with-resolved color-tokens)))]
(mf/with-effect []
(st/emit! (st/emit! (dsc/push-shortcuts ::colorpicker sc/shortcuts)))
@@ -783,5 +792,6 @@
:on-token-change on-token-change
:on-change on-change'
:origin origin
:tab tab
:color-origin color-origin
:on-accept on-accept}]]))

View File

@@ -135,24 +135,11 @@
on-token-pill-click
(mf/use-fn
(mf/deps selected-shapes selected color-origin)
(mf/deps selected-shapes)
(fn [event token]
(dom/stop-propagation event)
(when (seq selected-shapes)
(if (= :color-selection color-origin)
(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}))))))))
(on-token-change event token))))
create-token-on-set
(mf/use-fn

View File

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

View File

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

View File

@@ -46,7 +46,7 @@
(mf/defc single-shape-options*
{::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)
shape-id (dm/get-prop shape :id)

View File

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

View File

@@ -13,6 +13,7 @@
[app.config :as cfg]
[app.main.data.workspace :as udw]
[app.main.data.workspace.colors :as dc]
[app.main.data.workspace.tokens.application :as dwta]
[app.main.store :as st]
[app.main.ui.components.title-bar :refer [title-bar*]]
[app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
@@ -68,16 +69,24 @@
n-vals (unchecked-get n-props "values")
o-fills (get o-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)
n-hide (get n-vals :hide-fill-on-export)]
(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/wrap [#(mf/memo' % check-props)]}
[{:keys [ids type values]}]
[{:keys [ids type values applied-tokens shapes objects]}]
(let [fills (get values :fills)
hide-on-export (get values :hide-fill-on-export false)
fill-token-applied (:fill applied-tokens)
^boolean
multiple? (= :multiple fills)
@@ -172,7 +181,37 @@
#(reset! disable-drag* true))
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]
(when-let [checkbox (mf/ref-val checkbox-ref)]
@@ -223,9 +262,12 @@
:on-change on-change
:on-reorder on-reorder
:on-detach on-detach
:on-detach-token on-detach-token
:on-remove on-remove
:disable-drag disable-drag?
:on-focus on-focus
:applied-token fill-token-applied
:on-token-change on-token-change
:origin :fill
:select-on-focus (not disable-drag?)
:on-blur on-blur}]))])

View File

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

View File

@@ -11,28 +11,27 @@
[app.common.data.macros :as dm]
[app.common.types.color :as clr]
[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.workspace.colors :as dwc]
[app.main.refs :as refs]
[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.numeric-input :refer [numeric-input*]]
[app.main.ui.components.reorder-handler :refer [reorder-handler*]]
[app.main.ui.context :as ctx]
[app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
[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.hooks :as h]
[app.main.ui.icons :as deprecated-icon]
[app.util.color :as uc]
[app.util.dom :as dom]
[app.util.i18n :as i18n :refer [tr]]
[rumext.v2 :as mf]))
(def ^:private detach-icon
(deprecated-icon/icon-xref :detach (stl/css :detach-icon)))
(defn opacity->string
[opacity]
(if (= opacity :multiple)
@@ -42,45 +41,139 @@
(* 100)
(fmt/format-number)))))
(defn remove-multiple
[v]
(if (= v :multiple) nil v))
(mf/defc color-info-wrapper*
{::mf/private true}
[{: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*
[{: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
disable-drag on-focus on-blur select-only select-on-focus on-token-change]}]
(let [libraries (mf/deref refs/files)
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 applied-token]}]
(let [token-color (contains? cfg/flags :token-color)
libraries (mf/deref refs/files)
on-change (h/use-ref-callback on-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))
color-id (or (:ref-id color) (:id color))
src-colors (dm/get-in libraries [file-id :data :colors])
color-name (dm/get-in src-colors [color-id :name])
multiple-colors? (uc/multiple? color)
library-color? (and (or (:id color) (:ref-id color)) color-name (not multiple-colors?))
gradient-color? (and (not multiple-colors?)
has-multiple-colors (uc/multiple? color)
library-color? (and (or (:id color) (:ref-id color)) color-name (not has-multiple-colors))
gradient-color? (and (not has-multiple-colors)
(:gradient color)
(dm/get-in color [:gradient :type]))
image-color? (and (not multiple-colors?)
image-color? (and (not has-multiple-colors)
(:image color))
editing-text* (mf/use-state false)
editing-text? (deref editing-text*)
class (if (some? class) (dm/str class " ") "")
is-editing-text (deref editing-text*)
active-tokens* (mf/use-ctx ctx/active-tokens-by-type)
active-tokens (if active-tokens*
@active-tokens*
{})
opacity?
(and (not multiple-colors?)
(not library-color?)
(not disable-opacity))
tokens (mf/with-memo [active-tokens* origin]
(delay
(-> (deref active-tokens*)
(select-keys (get tk/tokens-by-input origin))
(not-empty))))
on-focus'
(mf/use-fn
@@ -138,12 +231,12 @@
(st/emit! (dwc/add-recent-color color)
(on-change color index)))))
handle-click-color
open-modal
(mf/use-fn
(mf/deps disable-gradient disable-opacity disable-image disable-picker on-change on-close on-open active-tokens)
(fn [color event]
(mf/deps disable-gradient disable-opacity disable-image disable-picker on-change on-close on-open tokens)
(fn [color pos tab]
(let [color (cond
multiple-colors?
has-multiple-colors
{:color default-color
:opacity 1}
@@ -152,13 +245,8 @@
:else
color)
cpos (dom/get-client-position event)
x (dm/get-prop cpos :x)
y (dm/get-prop cpos :y)
props {:x x
:y y
props {:x (:x pos)
:y (:y pos)
:disable-gradient disable-gradient
:disable-opacity disable-opacity
:disable-image disable-image
@@ -168,8 +256,9 @@
:on-close (fn [value opacity id file-id]
(when on-close
(on-close value opacity id file-id)))
:active-tokens active-tokens
:active-tokens tokens
:color-origin origin
:tab tab
:origin :sidebar
:data color}]
@@ -179,6 +268,38 @@
(when-not disable-picker
(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'
(mf/use-fn
(mf/deps index)
@@ -207,88 +328,95 @@
:name (str "Color row" index)})
[nil nil])
row-class
(stl/css-case :color-data true
:hidden hidden
:dnd-over-top (= (:over dprops) :top)
:dnd-over-bot (= (:over dprops) :bot))]
(mf/with-effect [color prev-color disable-picker]
(when (and (not disable-picker) (not= prev-color color))
(modal/update-props! :colorpicker {:data (parse-color color)})))
[:div {:class [class row-class]}
;; Drag handler
(when (some? on-reorder)
[:> 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)}
[: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
;; Rendering a color with ID
library-color?
[:*
[:div {:class (stl/css :color-name)
:title (str color-name)}
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)
:title (str color-name)}
(str color-name)]
[:> icon-button*
{:variant "ghost"
:class (stl/css :detach-btn)
:aria-label (tr "settings.detach")
:on-click detach-value
:icon i/detach}]]]
(str color-name)]
(when on-detach
[:button
{:class (stl/css :detach-btn)
:title (tr "settings.detach")
:on-click detach-value}
detach-icon])]
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)}
(uc/gradient-type->string (dm/get-in color [:gradient :type]))]]
;; Rendering a gradient
gradient-color?
[:div {:class (stl/css :color-name)}
(uc/gradient-type->string (dm/get-in color [:gradient :type]))]
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)}
(tr "media.image")]]
;; Rendering an image
image-color?
[:div {:class (stl/css :color-name)}
(tr "media.image")]
: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}
;; Rendering a plain color
:else
[:span {:class (stl/css :color-input-wrapper)}
[:> color-input* {:value (if multiple-colors?
""
(-> color :color clr/remove-hash))
:placeholder (tr "settings.multiple")
:data-index index
:class (stl/css :color-input)
:on-focus on-focus'
: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}]])]
[:span {:class (stl/css :color-input-wrapper)}
[:> color-input* {:value (if has-multiple-colors
""
color-without-hash)
:placeholder (tr "settings.multiple")
:data-index index
:class (stl/css :color-input)
:on-focus on-focus'
:on-blur on-blur'
:on-change on-color-change}]]])
(when (some? on-remove)
[:> icon-button* {:variant "ghost"
@@ -299,5 +427,4 @@
[:> icon-button* {:variant "ghost"
:aria-label (tr "settings.select-this-color")
:on-click handle-select
:icon i/move}])]))
:icon i/move}])]))

View File

@@ -4,15 +4,18 @@
//
// 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 {
@include deprecated.flexRow;
display: flex;
align-items: center;
gap: var(--sp-xs);
position: relative;
--reorder-left-position: calc(-1 * var(--sp-m) - var(--sp-xxs));
&:hover {
--reorder-icon-visibility: visible;
}
@@ -34,156 +37,289 @@
--detach-icon-foreground-color: none;
display: grid;
flex: 1;
grid-template-columns: 1fr auto;
align-items: center;
gap: deprecated.$s-2;
border-radius: deprecated.$s-8;
background-color: var(--input-details-color);
height: deprecated.$s-32;
flex: 1;
gap: var(--sp-xxs);
block-size: $sz-32;
border-radius: $br-8;
background-color: var(--color-background-primary);
&:hover {
--detach-icon-foreground-color: var(--input-foreground-color-active);
.detach-btn,
.select-btn {
background-color: transparent;
}
--detach-icon-foreground-color: var(--color-foreground-primary);
}
}
.color-name-wrapper {
@extend .input-element;
@include deprecated.bodySmallTypography;
--color-name-wrapper-background-color: var(--color-background-tertiary);
--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;
width: 100%;
min-width: 0;
border-radius: deprecated.$br-8 0 0 deprecated.$br-8;
gap: var(--sp-xs);
block-size: $sz-32;
min-inline-size: 0;
inline-size: 100%;
padding: 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 {
border-radius: deprecated.$br-8;
border-radius: $br-8;
.color-input-wrapper {
border-radius: deprecated.$br-8;
border-radius: $br-8;
}
}
&:hover {
--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 {
display: flex;
display: grid;
}
&.editing {
background-color: var(--input-background-color-active);
.color-bullet-wrapper,
.color-name,
.detach-btn,
.color-input-wrapper {
background-color: var(--input-background-color-active);
}
--color-name-wrapper-background-color: var(--color-background-primary);
}
&:focus,
&:focus-within {
background-color: var(--input-background-color-focus);
border: deprecated.$s-1 solid var(--input-border-color-focus);
--color-name-wrapper-background-color: var(--color-background-tertiary);
--color-name-wrapper-boder-color: var(--color-accent-primary);
}
}
&:focus,
&:focus-within {
background-color: var(--input-background-color-focus);
border: deprecated.$s-1 solid var(--input-border-color-focus);
--color-name-wrapper-background-color: var(--color-background-tertiary);
--color-name-wrapper-boder-color: var(--color-accent-primary);
&:hover {
background-color: var(--input-background-color-hover);
border: deprecated.$s-1 solid var(--input-border-color-focus);
--color-name-wrapper-background-color: var(--color-background-quaternary);
}
}
&.editing {
background-color: var(--input-background-color-active);
--color-name-wrapper-background-color: var(--color-background-primary);
&: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 {
border-radius: deprecated.$br-8;
border-radius: $br-8;
}
.opacity-element-wrapper {
@extend .input-element;
@include deprecated.bodySmallTypography;
width: deprecated.$s-60;
border-radius: 0 deprecated.$br-8 deprecated.$br-8 0;
.opacity-input {
padding: 0;
border-radius: 0 deprecated.$br-8 deprecated.$br-8 0;
min-width: deprecated.$s-28;
--opacity-input-background-color: var(--color-background-tertiary);
--opacity-input-boder-color: var(--color-background-tertiary);
@include t.use-typography("body-small");
display: flex;
align-items: center;
block-size: $sz-32;
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;
height: deprecated.$s-32;
margin-inline-end: deprecated.$s-4;
margin-block-start: deprecated.$s-2;
&: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);
}
}
}
.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
[app.common.data :as d]
[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.reorder-handler :refer [reorder-handler*]]
[app.main.ui.components.select :refer [select]]
@@ -37,8 +39,12 @@
disable-drag
on-focus
on-blur
applied-tokens
on-detach-token
disable-stroke-style
select-on-focus]}]
select-on-focus
shapes
objects]}]
(let [on-drop
(fn [_ data]
@@ -55,27 +61,29 @@
:name (str "Border row" index)})
[nil nil])
stroke-color-token (:stroke-color applied-tokens)
on-color-change-refactor
(mf/use-callback
(mf/use-fn
(mf/deps index on-color-change)
(fn [color]
(on-color-change index color)))
on-color-detach
(mf/use-callback
(mf/use-fn
(mf/deps index on-color-detach)
(fn [color]
(on-color-detach index color)))
on-remove
(mf/use-callback
(mf/use-fn
(mf/deps index on-remove)
#(on-remove index))
stroke-width (:stroke-width stroke)
on-width-change
(mf/use-callback
(mf/use-fn
(mf/deps index on-stroke-width-change)
#(on-stroke-width-change index %))
@@ -91,12 +99,35 @@
{:value :outer :label (tr "workspace.options.stroke.outer")}]))
on-alignment-change
(mf/use-callback
(mf/use-fn
(mf/deps index on-stroke-alignment-change)
#(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-options
(mf/with-memo [stroke-style]
(d/concat-vec
@@ -108,20 +139,26 @@
{:value :mixed :label (tr "workspace.options.stroke.mixed")}]))
on-style-change
(mf/use-callback
(mf/use-fn
(mf/deps index on-stroke-style-change)
#(on-stroke-style-change index (keyword %)))
on-caps-start-change
(mf/use-callback
(mf/use-fn
(mf/deps index on-stroke-cap-start-change)
#(on-stroke-cap-start-change index (keyword %)))
on-caps-end-change
(mf/use-callback
(mf/use-fn
(mf/deps index on-stroke-cap-end-change)
#(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
[{:value nil :label (tr "workspace.options.stroke-cap.none")}
:separator
@@ -135,7 +172,7 @@
{:value :square :label (tr "workspace.options.stroke-cap.square") :icon :stroke-squared}]
on-cap-switch
(mf/use-callback
(mf/use-fn
(mf/deps index on-stroke-cap-switch)
#(on-stroke-cap-switch index))]
@@ -156,8 +193,11 @@
:on-detach on-color-detach
:on-remove on-remove
: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
:origin :stroke
:origin :stroke-color
:select-on-focus select-on-focus
:on-blur on-blur}]

View File

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

View File

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

View File

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

View File

@@ -90,7 +90,7 @@
[constraint-ids constraint-values]
(get-attrs shapes objects :constraint)
[fill-ids fill-values]
[fill-ids fill-values fill-tokens]
(get-attrs shapes objects :fill)
[shadow-ids]
@@ -99,7 +99,7 @@
[blur-ids blur-values]
(get-attrs shapes objects :blur)
[stroke-ids stroke-values]
[stroke-ids stroke-values stroke-tokens]
(get-attrs shapes objects :stroke)
[text-ids text-values]
@@ -143,10 +143,21 @@
[:& constraints-menu {:ids constraint-ids :values constraint-values}])
(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)
[:& 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*
{:type type

View File

@@ -17,6 +17,7 @@
[app.common.types.shape.attrs :refer [editable-attrs]]
[app.common.types.shape.layout :as ctl]
[app.common.types.text :as txt]
[app.common.types.token :as tt]
[app.common.weak :as weak]
[app.main.refs :as refs]
[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)
: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
(fn [acc keys attrs]
(reduce
(fn [accum key]
(let [new-val (get attrs key)
existing (get accum key ::not-found)]
(cond
(= existing ::not-found) (assoc accum key new-val)
(= existing new-val) accum
:else (assoc accum key :multiple))))
acc
keys))
(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
(fn [[ids values token-acc] {:keys [id type applied-tokens] :as shape}]
@@ -263,8 +280,8 @@
:children
(let [children (->> (:shapes shape []) (map #(get objects %)))
[new-ids new-values] (get-attrs* children objects attr-group)]
[(d/concat-vec ids new-ids) (merge-attrs values new-values) {}])
[new-ids new-values tokens] (get-attrs* children objects attr-group)]
[(d/concat-vec ids new-ids) (merge-attrs values new-values) tokens])
[])))]
@@ -376,7 +393,7 @@
[constraint-ids constraint-values]
(get-attrs shapes objects :constraint)
[fill-ids fill-values]
[fill-ids fill-values fill-tokens]
(get-attrs shapes objects :fill)
[shadow-ids shadow-values]
@@ -385,7 +402,7 @@
[blur-ids blur-values]
(get-attrs shapes objects :blur)
[stroke-ids stroke-values]
[stroke-ids stroke-values stroke-tokens]
(get-attrs shapes objects :stroke)
[exports-ids exports-values]
@@ -463,14 +480,22 @@
[:& ot/text-menu {:type type :ids text-ids :values text-values}])
(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)
[:& stroke-menu {:type type
:ids stroke-ids
:show-caps show-caps?
: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)
[:> color-selection-menu*

View File

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

View File

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

View File

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

View File

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

View File

@@ -1184,7 +1184,11 @@ msgstr "Detach token"
#: src/app/main/ui/ds/controls/utilities/token_field.cljs:39
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
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
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
msgid "errors.auth-provider-not-allowed"