From f6f9df9a1a94087ecc9fd30337f908598ada983c Mon Sep 17 00:00:00 2001 From: Eva Marco Date: Tue, 27 Oct 2026 16:01:53 +0100 Subject: [PATCH] :recycle: Use DS icons in the layer row buttons (#12477) * :recycle: Use DS icons in the layer row buttons The expand, visibility and lock buttons of the layer rows drew their icons with the deprecated icon set (arrow, shown, hide, unlock, lock). Use the DS icon instead. The buttons stay raw: they are segments of the layer row, with backgrounds that follow the row state. The icons get a class each, so their colors move from svg rules to those classes, and they keep the old stroke weight. The DS icon draws 12px inside a 16px box: - The expand button start padding goes from 8px to 6px, so the arrow stays in place. - The visibility and lock buttons leave 12px for the icon, so their icons do not shrink (flex-shrink: 0) and stay centered, uncut. The rows look the same as before, pixel for pixel. Add a visual test with a selected, hidden and blocked layer and a hovered row. AI-assisted-by: claude-opus-5-5 * :recycle: Move the layer name styles off the deprecated styles The layer name and its rename input used the deprecated body-small typography, mixins, spacing and token aliases. They now use the DS body-small typography, sizes, borders and px2rem(), and the layer row color aliases resolve to their DS tokens, in layer_item.scss too. The DS body-small has a 1.3 line-height instead of 1.4, so the layer names sit 1px higher. The name color is one local variable for every state; the rename input sets it too, since it also has .element-name. A visual test now covers the rename input. AI-assisted-by: claude-opus-5-5 --- .../ui/visual-specs/workspace.spec.js | 62 +++++++++ .../main/ui/workspace/sidebar/layer_item.cljs | 15 ++- .../main/ui/workspace/sidebar/layer_item.scss | 118 +++++++++--------- .../main/ui/workspace/sidebar/layer_name.scss | 52 ++++---- 4 files changed, 154 insertions(+), 93 deletions(-) diff --git a/frontend/playwright/ui/visual-specs/workspace.spec.js b/frontend/playwright/ui/visual-specs/workspace.spec.js index a9a4ef2bf3..5893ae0d80 100644 --- a/frontend/playwright/ui/visual-specs/workspace.spec.js +++ b/frontend/playwright/ui/visual-specs/workspace.spec.js @@ -599,6 +599,68 @@ test.describe("Layers panel", () => { }); }); +test.describe("Layers", () => { + test("Shows the buttons of the layer rows", async ({ page }) => { + const workspace = new WorkspacePage(page); + await workspace.setupEmptyFile(); + await workspace.mockRPC(/get\-file\?/, "workspace/get-file-9066.json"); + await workspace.mockRPC( + "update-file?id=*", + "workspace/update-file-empty.json", + ); + await workspace.goToWorkspace({ + fileId: "e179d9df-de35-80bf-8005-283bbd5516b0", + pageId: "e179d9df-de35-80bf-8005-283bbd5516b1", + }); + + await workspace.clickToggableLayer("Board"); + await workspace.clickToggableLayer("Group"); + const layer = workspace.layers.getByText("A", { exact: true }); + await layer.click(); + await layer.hover(); + await workspace.layers.getByRole("button", { name: "Hide" }).click(); + await workspace.layers.getByRole("button", { name: "Lock" }).click(); + await expect( + workspace.layers.getByRole("button", { name: "Show" }), + ).toBeVisible(); + await expect( + workspace.layers.getByRole("button", { name: "Unlock" }), + ).toBeVisible(); + + // Selected, hidden and blocked layer, with the pointer on its row. + await expect(workspace.layers).toHaveScreenshot({ + maxDiffPixelRatio: 0, + threshold: 0, + }); + + // The pointer on a collapsed group, away from the selected layer. + await workspace.layers.getByText("Board", { exact: true }).hover(); + await expect(workspace.layers).toHaveScreenshot({ + maxDiffPixelRatio: 0, + threshold: 0, + }); + }); + + test("Shows the rename input of a layer", async ({ page }) => { + const workspace = new WorkspacePage(page); + await workspace.setupEmptyFile(); + await workspace.mockRPC(/get\-file\?/, "workspace/get-file-9066.json"); + await workspace.goToWorkspace({ + fileId: "e179d9df-de35-80bf-8005-283bbd5516b0", + pageId: "e179d9df-de35-80bf-8005-283bbd5516b1", + }); + + await workspace.layers.getByText("Board", { exact: true }).dblclick(); + const input = workspace.layers.getByRole("textbox"); + await expect(input).toBeVisible(); + await page.mouse.move(0, 0); + await expect(workspace.layers).toHaveScreenshot({ + maxDiffPixelRatio: 0, + threshold: 0, + }); + }); +}); + test.describe("Guides", () => { test("Shows the advanced options of a square guide", async ({ page }) => { // A taller window keeps the whole Design tab visible: a scrolled diff --git a/frontend/src/app/main/ui/workspace/sidebar/layer_item.cljs b/frontend/src/app/main/ui/workspace/sidebar/layer_item.cljs index 5733b8c3ce..6438e63914 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/layer_item.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/layer_item.cljs @@ -21,9 +21,8 @@ [app.main.refs :as refs] [app.main.store :as st] [app.main.ui.context :as ctx] - [app.main.ui.ds.foundations.assets.icon :refer [icon*]] + [app.main.ui.ds.foundations.assets.icon :as i :refer [icon*]] [app.main.ui.hooks :as hooks] - [app.main.ui.icons :as deprecated-icon] [app.main.ui.workspace.sidebar.layer-name :refer [layer-name*]] [app.util.dom :as dom] [app.util.i18n :refer [tr]] @@ -138,7 +137,9 @@ :data-testid "toggle-content" :aria-expanded is-expanded :on-click on-toggle-collapse} - deprecated-icon/arrow]) + [:> icon* {:icon-id i/arrow + :size "s" + :class (stl/css :toggle-content-icon)}]]) [:div {:class (stl/css :icon-shape) :on-double-click on-zoom-to-selected} @@ -189,7 +190,9 @@ (tr "workspace.shape.menu.show") (tr "workspace.shape.menu.hide")) :on-click on-toggle-visibility} - (if ^boolean hidden? deprecated-icon/hide deprecated-icon/shown)] + [:> icon* {:icon-id (if ^boolean hidden? i/hide i/shown) + :size "s" + :class (stl/css :element-action-icon)}]] [:button {:class (stl/css-case :block-element true :selected blocked?) @@ -197,7 +200,9 @@ (tr "workspace.shape.menu.unlock") (tr "workspace.shape.menu.lock")) :on-click on-toggle-blocking} - (if ^boolean blocked? deprecated-icon/lock deprecated-icon/unlock)]])] + [:> icon* {:icon-id (if ^boolean blocked? i/lock i/unlock) + :size "s" + :class (stl/css :element-action-icon)}]]])] children])) diff --git a/frontend/src/app/main/ui/workspace/sidebar/layer_item.scss b/frontend/src/app/main/ui/workspace/sidebar/layer_item.scss index 445b8e5bc9..d7cbad6289 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/layer_item.scss +++ b/frontend/src/app/main/ui/workspace/sidebar/layer_item.scss @@ -27,9 +27,9 @@ &:hover { --layer-background-color: var(--color-background-secondary); --shadow-color: var(--color-background-secondary); - --context-hover-color: var(--layer-row-foreground-color-hover); + --context-hover-color: var(--color-accent-primary); --context-hover-opacity: 1; - --layer-foreground-color: var(--layer-row-foreground-color-hover); + --layer-foreground-color: var(--color-accent-primary); &.hidden { opacity: 1; @@ -200,43 +200,40 @@ align-items: center; block-size: 100%; inline-size: $sz-24; - padding-inline-start: var(--sp-s); - svg { - display: flex; - justify-content: center; - align-items: center; - color: transparent; - fill: none; - block-size: $sz-12; - inline-size: $sz-12; - stroke-width: 1.33px; - stroke: var(--color-foreground-secondary); - - .layer-row.hidden & { - opacity: 0.7; - } - - .layer-row.selected & { - stroke: var(--color-accent-primary); - } - - .layer-row.type-comp & { - stroke: var(--color-accent-secondary); - } - - .layer-row.highlight &, - .layer-row:hover & { - opacity: 1; - stroke: var(--color-accent-primary); - } - } + // The DS icon draws 12px inside a 16px box: 2px less start padding + // keeps the arrow where the old 12px icon was. + padding-inline-start: px2rem(6); .layer-row.selected & { background-color: var(--color-background-quaternary); } +} - &.inverse svg { +.toggle-content-icon { + // Same stroke weight as the old 12px icons. + stroke-width: 1.33px; + color: var(--color-foreground-secondary); + + .layer-row.hidden & { + opacity: 0.7; + } + + .layer-row.selected & { + color: var(--color-accent-primary); + } + + .layer-row.type-comp & { + color: var(--color-accent-secondary); + } + + .layer-row.highlight &, + .layer-row:hover & { + opacity: 1; + color: var(--color-accent-primary); + } + + .toggle-content.inverse & { transform: rotate(90deg); } } @@ -256,26 +253,6 @@ background: var(--layer-row-action-btn-background); padding-inline-end: px2rem(6); - svg { - display: flex; - justify-content: center; - align-items: center; - color: transparent; - fill: none; - block-size: $sz-12; - inline-size: $sz-12; - stroke-width: 1.33px; - stroke: var(--color-foreground-secondary); - - .layer-row.hidden & { - opacity: 0.7; - } - - .type-comp & { - stroke: var(--color-accent-secondary); - } - } - .element-actions.selected & { display: flex; opacity: 0; @@ -294,21 +271,40 @@ display: flex; --layer-row-action-btn-background: var(--color-background-secondary); - - svg { - opacity: 1; - stroke: var(--color-accent-primary); - } } .layer-row.selected & { display: flex; --layer-row-action-btn-background: var(--color-background-quaternary); + } +} - svg { - stroke: var(--color-accent-primary); - } +.element-action-icon { + // The DS icon box is 16px and the button leaves 12px: keep the box + // whole, centered, instead of letting it shrink and cut the icon. + flex-shrink: 0; + + // Same stroke weight as the old 12px icons. + stroke-width: 1.33px; + color: var(--color-foreground-secondary); + + .layer-row.hidden & { + opacity: 0.7; + } + + .type-comp & { + color: var(--color-accent-secondary); + } + + .layer-row.highlight &, + .layer-row:hover & { + opacity: 1; + color: var(--color-accent-primary); + } + + .layer-row.selected & { + color: var(--color-accent-primary); } } diff --git a/frontend/src/app/main/ui/workspace/sidebar/layer_name.scss b/frontend/src/app/main/ui/workspace/sidebar/layer_name.scss index 23a36b1d1b..6d6fc10fff 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/layer_name.scss +++ b/frontend/src/app/main/ui/workspace/sidebar/layer_name.scss @@ -4,16 +4,22 @@ // // Copyright (c) KALEIDOS SUBSIDIARY SL -@use "refactor/common-refactor.scss" as deprecated; +@use "ds/_utils.scss" as *; +@use "ds/_borders.scss" as *; +@use "ds/_sizes.scss" as *; +@use "ds/typography.scss" as *; .element-name { - --element-name-color: var(--context-hover-color, var(--layer-row-foreground-color)); - --element-name-comp-color: var(--context-hover-color, var(--layer-row-component-foreground-color)); - --element-name-opacity: var(--context-hover-opacity, deprecated.$op-7); + --element-name-color: var(--context-hover-color, var(--color-foreground-secondary)); + --element-name-opacity: var(--context-hover-opacity, 0.7); - @include deprecated.text-ellipsis; - @include deprecated.body-small-typography; + @include use-typography("body-small"); + display: block; + max-inline-size: 99%; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; color: var(--element-name-color); flex-grow: 1; block-size: 100%; @@ -21,15 +27,11 @@ min-inline-size: var(--sp-xl); &.selected { - color: var(--layer-row-foreground-color-selected); + --element-name-color: var(--color-accent-primary); } &.type-comp { - color: var(--element-name-comp-color); - - &.hidden { - opacity: var(--element-name-opacity); - } + --element-name-color: var(--context-hover-color, var(--color-accent-secondary)); } &.hidden { @@ -37,30 +39,26 @@ } } +// The rename input also carries .element-name, so it takes its typography, +// ellipsis and color from there. .element-name-input { - --element-name-input-border-color: var(--input-border-color-focus); - --element-name-input-color: var(--layer-row-foreground-color); - - @include deprecated.text-ellipsis; - @include deprecated.body-small-typography; - @include deprecated.remove-input-style; + --element-name-color: var(--color-foreground-secondary); + background: none; + outline: none; flex-grow: 1; - height: deprecated.$s-28; + block-size: $sz-28; max-inline-size: calc(var(--parent-size) - (var(--depth) * var(--layer-indentation-size))); margin: 0; - padding-inline-start: deprecated.$s-6; - border-radius: deprecated.$br-8; - border: deprecated.$s-1 solid var(--element-name-input-border-color); - color: var(--element-name-input-color); + padding-inline-start: px2rem(6); + border-radius: $br-8; + border: $b-1 solid var(--color-accent-primary); &.selected { - min-inline-size: deprecated.$s-100; + min-inline-size: px2rem(100); } } .element-name-touched { - --element-name-touched-color: var(--layer-row-component-foreground-color); - - color: var(--element-name-touched-color); + color: var(--color-accent-secondary); }