♻️ Use DS icons in the layer row buttons (#12477)

* ♻️ 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

* ♻️ 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
This commit is contained in:
Eva Marco 2026-10-27 16:01:53 +01:00 committed by GitHub
parent 9d5dad3de7
commit f6f9df9a1a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 154 additions and 93 deletions

View File

@ -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

View File

@ -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]))

View File

@ -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);
}
}

View File

@ -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);
}