♻️ Use DS icons in the dashboard lists and placeholders (#12536)

* ♻️ Use DS icons in the dashboard lists and placeholders

The trash project options button becomes a ghost DS icon button, as in
the projects list, and says whether its menu is open. The search
placeholders, the empty project "create new file" button, the comments
placeholder and the legacy import library tag use the DS icons. The
legacy search icon had no fill rule, so its lens was filled black; the
DS one is not.

Two fixtures make the trash testable: a deleted project, and deleted
files due in 2100 (the existing ones expired in 2024, so the trash
dropped them). A visual test covers the trash row, the empty project
button, the project rename field and the comments placeholder.

AI-assisted-by: claude-opus-5-5

* 🐛 Remove the hidden close control from the dashboard rename field

The rename field of dashboard projects and files had a close control
next to the input that never showed: its legacy icon is drawn with a
stroke, but the styles only set a fill. It could not be reached with
the keyboard or a screen reader, and a click on it did what leaving
the field does: the blur saves the name first, and its handler saved
it too. It only left a pointer cursor over an empty spot. Enter, Escape
and leaving the field keep working as before. No visual change.

AI-assisted-by: claude-opus-5-5

* ♻️ Move dashboard trash, comments and rename styles to the DS

The trash, comments panel and rename field stylesheets resolve their
legacy color aliases, sizes, font sizes, mixins and the menu shadow to
DS tokens and values, and use logical properties. The comments panel
and the rename field no longer import refactor/; the trash keeps only
the shared dashboard header styles (common-dashboard), used by ten
dashboard modules.

Four selectors that the modules do not use are commented out with a
TODO. The comments dropdown z-index (4) has no DS token and keeps a
TODO. No visual change.

AI-assisted-by: claude-opus-5-5

* 🔥 Remove unused styles from the dashboard trash and comments

The SCSS refactor of the dashboard trash and comments left rules
commented out with a TODO because the module does not use them, or
because they target an svg inside a DS icon that is the svg itself. CSS
modules scope the classes to this module and no other source uses them,
so they go: .thread-group, .comments-icon-small, .add-file-btn,
.add-icon. No visual change.

AI-assisted-by: claude-opus-5-5

* 🐛 Use px2rem for the unread dot size in dashboard comments

The unread dot used $sz-8, which the DS sizes do not define, so the
integration bundle build failed. It now uses px2rem(8), the same 8px.

AI-assisted-by: claude-opus-5-5

* ♻️ Use DS typography and z-index in the dashboard comments

Review fixes for the dashboard lists styles:

- The comments dropdown title uses the headline-small typography. It
  was 11px regular, so it is now 12px medium (larger and bolder).
- The comments dropdown uses --z-index-dropdown instead of a raw 4.
- The rename field selector is the class alone; the input type is not
  needed to win over the input defaults (0px change).

The visual test now also captures the whole comments dropdown, so the
title change shows.

AI-assisted-by: claude-opus-5-5

* 🔥 Remove dead rules from the dashboard comments styles

The comments dropdown has no separators, so its hr rule matched
nothing, and the comments icon set the default stroke width. No visual
change.

AI-assisted-by: claude-opus-5-5
This commit is contained in:
Eva Marco 2026-10-28 12:10:47 +01:00 committed by GitHub
parent 4dc684dd9c
commit 1109e38852
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 246 additions and 147 deletions

View File

@ -0,0 +1,32 @@
[
{
"~:id": "~ue5a24d1b-ef1e-812f-8004-52bab84be6f7",
"~:team-id": "~uc7ce0794-0992-8105-8004-38e630f40f6d",
"~:created-at": "~m1715266551088",
"~:modified-at": "~m1715266551088",
"~:is-default": false,
"~:name": "New Project 1",
"~:is-pinned": false,
"~:count": 1
},
{
"~:id": "~uc7ce0794-0992-8105-8004-38e630f7920b",
"~:team-id": "~uc7ce0794-0992-8105-8004-38e630f40f6d",
"~:created-at": "~m1713533116382",
"~:modified-at": "~m1713873823633",
"~:is-default": true,
"~:name": "Drafts",
"~:count": 1
},
{
"~:id": "~uc7ce0794-0992-8105-8004-38e630f7920c",
"~:team-id": "~uc7ce0794-0992-8105-8004-38e630f40f6d",
"~:created-at": "~m1715266551088",
"~:modified-at": "~m1715266551088",
"~:is-default": false,
"~:name": "Old Project",
"~:is-pinned": false,
"~:count": 1,
"~:deleted-at": "~m1732111500000"
}
]

View File

@ -0,0 +1,47 @@
[
{
"~:id": "~uc7ce0794-0992-8105-8004-38e630f41234",
"~:revn": 1,
"~:vern": 1,
"~:project-id": "~uc7ce0794-0992-8105-8004-38e630f7920b",
"~:created-at": "~m1705307400000",
"~:modified-at": "~m1732111500000",
"~:deleted-at": "~m1732111500000",
"~:name": "Deleted Design File 1",
"~:is-shared": false,
"~:will-be-deleted-at": "~m4102444800000",
"~:thumbnail-id": null,
"~:row-num": 1,
"~:team-id": "~uc7ce0794-0992-8105-8004-38e630f40f6d"
},
{
"~:id": "~uc7ce0794-0992-8105-8004-38e630f41235",
"~:revn": 2,
"~:vern": 2,
"~:project-id": "~uc7ce0794-0992-8105-8004-38e630f7920b",
"~:created-at": "~m1704875700000",
"~:modified-at": "~m1732025400000",
"~:deleted-at": "~m1732025400000",
"~:name": "Deleted Design File 2",
"~:is-shared": true,
"~:will-be-deleted-at": "~m4102444800000",
"~:thumbnail-id": null,
"~:row-num": 2,
"~:team-id": "~uc7ce0794-0992-8105-8004-38e630f40f6d"
},
{
"~:id": "~uc7ce0794-0992-8105-8004-38e630f41236",
"~:revn": 3,
"~:vern": 3,
"~:project-id": "~uc7ce0794-0992-8105-8004-38e630f7920c",
"~:created-at": "~m1706792400000",
"~:modified-at": "~m1731939600000",
"~:deleted-at": "~m1731939600000",
"~:name": "Old Project Design",
"~:is-shared": false,
"~:will-be-deleted-at": "~m4102444800000",
"~:thumbnail-id": null,
"~:row-num": 3,
"~:team-id": "~uc7ce0794-0992-8105-8004-38e630f40f6d"
}
]

View File

@ -439,6 +439,78 @@ test.describe("Subscription controls", () => {
});
});
test.describe("Dashboard list controls", () => {
const shot = { maxDiffPixelRatio: 0, threshold: 0 };
test("Trash project options", async ({ page }) => {
const dashboardPage = new DashboardPage(page);
await dashboardPage.setupDashboardFull();
// Deleted files due in 2100, so the trash keeps them.
await dashboardPage.mockRPC(
"get-team-deleted-files?team-id=*",
"dashboard/get-team-deleted-files-pending.json",
);
// A deleted project, so its row shows the options button.
await dashboardPage.mockRPC(
"get-projects?team-id=*",
"dashboard/get-projects-deleted.json",
);
await dashboardPage.goToDeleted();
const options = page.getByTestId("project-options").first();
await expect(options).toBeVisible();
const row = page.locator("[class*='dashboard-project-row']").first();
await page.mouse.move(640, 700);
await expect(row.locator("header")).toHaveScreenshot(shot);
await options.hover();
await expect(row.locator("header")).toHaveScreenshot(shot);
});
test("Empty project create button", async ({ page }) => {
const dashboardPage = new DashboardPage(page);
await dashboardPage.setupNewProject();
await dashboardPage.goToDashboard();
await expect(dashboardPage.projectName).toBeVisible();
const create = page.locator("[class*='create-new']").first();
await expect(create).toBeVisible();
await page.mouse.move(640, 700);
await expect(create).toHaveScreenshot(shot);
await create.hover();
await expect(create).toHaveScreenshot(shot);
});
test("Project rename", async ({ page }) => {
const dashboardPage = new DashboardPage(page);
await dashboardPage.setupDashboardFull();
await dashboardPage.goToDashboard();
await page.getByTestId("project-options").first().click();
await page.getByText("Rename", { exact: true }).click();
const edit = page.locator("[class*='edit-wrapper']").first();
await expect(edit).toBeVisible();
await expect(edit).toHaveScreenshot(shot);
});
test("Comments placeholder", async ({ page }) => {
const dashboardPage = new DashboardPage(page);
await dashboardPage.mockRPC(
"get-unread-comment-threads?team-id=*",
"logged-in-user/get-unread-comment-threads-empty.json",
);
await dashboardPage.goToDashboard();
await page.getByTestId("open-comments").click();
const placeholder = page.locator("[class*='thread-groups-placeholder']");
await expect(placeholder).toBeVisible();
await expect(placeholder).toHaveScreenshot(shot);
// The whole dropdown, with the header title.
await expect(
page.locator("[class*='comment-threads-section']"),
).toHaveScreenshot(shot);
});
});
test.describe("Team modals controls", () => {
const shot = { maxDiffPixelRatio: 0, threshold: 0 };

View File

@ -15,14 +15,10 @@
[app.main.ui.comments :as cmt]
[app.main.ui.components.dropdown :refer [dropdown]]
[app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
[app.main.ui.ds.foundations.assets.icon :as i]
[app.main.ui.icons :as deprecated-icon]
[app.main.ui.ds.foundations.assets.icon :refer [icon*] :as i]
[app.util.i18n :as i18n :refer [tr]]
[rumext.v2 :as mf]))
(def ^:private comments-icon-svg
(deprecated-icon/icon-xref :comments (stl/css :comments-icon)))
(mf/defc comments-icon*
[{:keys [profile on-show-comments]}]
@ -116,5 +112,7 @@
:key (:page-id tgroup)}])]
[:div {:class (stl/css :thread-groups-placeholder)}
comments-icon-svg
[:> icon* {:icon-id i/comments
:size "l"
:class (stl/css :comments-icon)}]
(tr "labels.no-comments-available")])]]]))

View File

@ -4,44 +4,39 @@
//
// Copyright (c) KALEIDOS SUBSIDIARY SL
@use "refactor/common-refactor.scss" as deprecated;
@use "ds/_borders.scss" as *;
@use "ds/_sizes.scss" as *;
@use "ds/_utils.scss" as *;
@use "ds/typography.scss" as *;
.dashboard-comments-section {
@include deprecated.flex-center;
display: flex;
justify-content: center;
align-items: center;
position: relative;
border-radius: deprecated.$br-8;
border-radius: $br-8;
}
.thread-groups {
height: calc(100% - deprecated.$s-32);
max-height: deprecated.$s-440;
block-size: calc(100% - #{$sz-32});
max-block-size: px2rem(440);
overflow: auto;
}
.thread-group {
display: flex;
flex-direction: column;
font-size: deprecated.$fs-12;
}
.thread-groups-placeholder {
align-items: center;
display: flex;
flex-direction: column;
font-size: deprecated.$fs-12;
padding: deprecated.$s-24;
font-size: px2rem(12);
padding: var(--sp-xxl);
text-align: center;
color: var(--color-foreground-secondary);
}
.comments-icon {
@extend %button-icon;
stroke: var(--icon-foreground);
height: deprecated.$s-24;
width: deprecated.$s-24;
margin-bottom: deprecated.$s-24;
flex-shrink: 0;
color: var(--color-foreground-secondary);
margin-block-end: var(--sp-xxl);
}
.comment-button {
@ -49,57 +44,44 @@
.unread {
position: absolute;
width: deprecated.$s-8;
height: deprecated.$s-8;
border: deprecated.$s-2 solid var(--color-background-tertiary);
inline-size: px2rem(8);
block-size: px2rem(8);
border: $b-2 solid var(--color-background-tertiary);
border-radius: 50%;
background: var(--color-foreground-error);
top: deprecated.$s-6;
right: deprecated.$s-6;
inset-block-start: px2rem(6);
inset-inline-end: px2rem(6);
}
}
.comments-icon-small {
@extend %button-icon;
stroke: var(--comment-icon-small-foreground-color);
}
.dropdown {
@include deprecated.menu-shadow;
box-shadow: 0 0 $sz-12 0 var(--color-shadow-dark);
background-color: var(--color-background-tertiary);
border-radius: deprecated.$br-8;
border: deprecated.$s-1 solid transparent;
bottom: deprecated.$s-4;
height: 40vh;
max-height: deprecated.$s-480;
min-height: deprecated.$s-200;
border-radius: $br-8;
border: $b-1 solid transparent;
inset-block-end: var(--sp-xs);
block-size: 40vh;
max-block-size: $sz-480;
min-block-size: $sz-200;
position: absolute;
overflow: hidden;
width: 100%;
z-index: deprecated.$z-index-4;
hr {
margin: 0;
border-color: var(--color-foreground-secondary);
}
inline-size: 100%;
z-index: var(--z-index-dropdown);
}
.header {
display: flex;
height: deprecated.$s-40;
block-size: $sz-40;
align-items: center;
padding: 0 deprecated.$s-12;
gap: deprecated.$s-4;
padding: 0 var(--sp-m);
gap: var(--sp-xs);
}
.header-title {
@include use-typography("headline-small");
color: var(--color-foreground-secondary);
font-size: deprecated.$fs-11;
line-height: 1.28;
flex-grow: 1;
text-transform: uppercase;
}
.notifications-button {
@ -108,7 +90,7 @@
}
&.mark-all-as-read-button {
border-radius: deprecated.$s-8;
border: deprecated.$s-1 solid;
border-radius: $br-8;
border: $b-1 solid;
}
}

View File

@ -21,9 +21,10 @@
[app.main.ui.dashboard.layout-toggle :as lt :refer [layout-toggle*]]
[app.main.ui.dashboard.subscription :refer [get-subscription-type]]
[app.main.ui.ds.buttons.button :refer [button*]]
[app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
[app.main.ui.ds.foundations.assets.icon :as i]
[app.main.ui.ds.product.empty-placeholder :refer [empty-placeholder*]]
[app.main.ui.hooks :as hooks]
[app.main.ui.icons :as deprecated-icon]
[app.util.dom :as dom]
[app.util.i18n :as i18n :refer [tr]]
[app.util.keyboard :as kbd]
@ -33,9 +34,6 @@
(def ^:private ref:deleted-files
(l/derived :deleted-files st/state))
(def ^:private menu-icon
(deprecated-icon/icon-xref :menu (stl/css :menu-icon)))
(defn- on-restore-project
[project]
(let [on-accept #(st/emit! (dd/restore-project-immediately project))]
@ -158,13 +156,14 @@
[:div {:class (stl/css :info-wrapper)}
[:div {:class (stl/css-case :project-actions true)}
[:button {:class (stl/css :options-btn)
:on-click on-menu-click
:title (tr "dashboard.options")
:aria-label (tr "dashboard.options")
:data-testid "project-options"
:on-key-down handle-menu-click}
menu-icon]]
[:> icon-button* {:variant "ghost"
:class (stl/css :options-btn)
:icon i/menu
:aria-label (tr "dashboard.options")
:aria-pressed (boolean (:menu-open @local))
:data-testid "project-options"
:on-click on-menu-click
:on-key-down handle-menu-click}]]
(when (:menu-open @local)
[:> project-context-menu*

View File

@ -4,19 +4,21 @@
//
// Copyright (c) KALEIDOS SUBSIDIARY SL
@use "refactor/common-refactor.scss" as deprecated;
// The shared dashboard header styles move in their own change: they are
// used by ten dashboard modules.
@use "common/refactor/common-dashboard";
@use "ds/typography.scss" as t;
@use "ds/typography.scss" as *;
@use "ds/spacing.scss" as *;
@use "ds/z-index.scss" as *;
@use "ds/_borders.scss" as *;
@use "ds/_sizes.scss" as *;
@use "ds/_utils.scss" as *;
.dashboard-container {
flex: 1 0 0;
width: 100%;
inline-size: 100%;
margin-inline-end: var(--sp-l);
border-top: $b-1 solid var(--panel-border-color);
border-block-start: $b-1 solid var(--color-background-quaternary);
overflow-y: auto;
padding-block-end: var(--sp-xxxl);
}
@ -31,10 +33,10 @@
display: block;
color: var(--color-foreground-secondary);
@include t.use-typography("body-large");
@include use-typography("body-large");
line-height: 0.8;
height: var(--sp-xl);
block-size: var(--sp-xl);
}
.info-text-highlight {
@ -43,7 +45,7 @@
.deleted-options {
display: flex;
gap: 5px;
gap: px2rem(5);
flex-shrink: 0;
}
@ -51,12 +53,12 @@
background: var(--color-background-default);
padding: var(--sp-xxl) var(--sp-xxl) var(--sp-s) var(--sp-xxl);
position: sticky;
top: 0;
inset-block-start: 0;
z-index: var(--z-index-panels);
}
.nav-inside {
border-bottom: $b-1 solid var(--panel-border-color);
border-block-end: $b-1 solid var(--color-background-quaternary);
display: flex;
gap: var(--sp-l);
justify-content: space-between;
@ -74,7 +76,7 @@
.selected {
color: var(--color-foreground-primary);
border-bottom: $b-1 solid var(--color-foreground-primary);
border-block-end: $b-1 solid var(--color-foreground-primary);
}
.project {
@ -83,8 +85,8 @@
align-items: center;
justify-content: space-between;
gap: var(--sp-s);
width: 99%;
max-height: $sz-40;
inline-size: 99%;
max-block-size: $sz-40;
padding: var(--sp-s) var(--sp-s) var(--sp-s) var(--sp-l);
margin-block-start: var(--sp-l);
border-radius: $br-4;
@ -94,19 +96,19 @@
display: flex;
align-items: center;
justify-content: flex-start;
width: 100%;
min-height: var(--sp-xxxl);
inline-size: 100%;
min-block-size: var(--sp-xxxl);
margin-inline-start: var(--sp-s);
}
.project-name {
@include t.use-typography("body-large");
@include use-typography("body-large");
width: fit-content;
inline-size: fit-content;
margin-inline-end: var(--sp-m);
line-height: 0.8;
color: var(--title-foreground-color-hover);
height: var(--sp-l);
color: var(--color-foreground-primary);
block-size: var(--sp-l);
}
.project-actions {
@ -115,14 +117,8 @@
margin-inline-start: var(--sp-xxxl);
}
.add-file-btn,
.options-btn {
@extend %button-tertiary;
height: var(--sp-xxxl);
width: var(--sp-xxxl);
margin: 0 var(--sp-s);
padding: var(--sp-s);
}
.info-wrapper {
@ -130,10 +126,3 @@
align-items: center;
gap: var(--sp-s);
}
.add-icon,
.menu-icon {
@extend %button-icon;
stroke: var(--icon-foreground);
}

View File

@ -30,7 +30,6 @@
[app.main.ui.ds.foundations.typography.text :refer [text*]]
[app.main.ui.ds.notifications.context-notification :refer [context-notification*]]
[app.main.ui.ds.product.loader :refer [loader*]]
[app.main.ui.icons :as deprecated-icon]
[app.main.ui.jobs.progress :as jp]
[app.util.dom :as dom]
[app.util.i18n :as i18n :refer [tr]]
@ -356,7 +355,7 @@
[:span {:class (stl/css-case
:linked-library-tag true
:error error?)}
deprecated-icon/detach]])))]]))
[:> icon* {:icon-id i/detach}]]])))]]))
(defn initialize-state
[entries]

View File

@ -7,7 +7,6 @@
(ns app.main.ui.dashboard.inline-edition
(:require-macros [app.main.style :as stl])
(:require
[app.main.ui.icons :as deprecated-icon]
[app.util.dom :as dom]
[app.util.keyboard :as kbd]
[rumext.v2 :as mf]))
@ -68,7 +67,5 @@
:on-change on-input
:on-key-down on-keyup
:on-blur on-blur
:max-length max-length}]
[:span {:class (stl/css :close)
:on-click on-cancel} deprecated-icon/close]]))
:max-length max-length}]]))

View File

@ -4,50 +4,31 @@
//
// Copyright (c) KALEIDOS SUBSIDIARY SL
@use "refactor/common-refactor.scss" as deprecated;
@use "ds/_borders.scss" as *;
@use "ds/_sizes.scss" as *;
@use "ds/_utils.scss" as *;
.edit-wrapper {
border-radius: deprecated.$br-4;
border-radius: $br-4;
display: flex;
padding-right: deprecated.$s-24;
padding-inline-end: var(--sp-xxl);
position: relative;
margin-right: deprecated.$s-24;
margin-inline-end: var(--sp-xxl);
}
input.element-title {
background-color: var(--input-background-color-active);
border-radius: deprecated.$br-8;
.element-title {
background-color: var(--color-background-primary);
border-radius: $br-8;
color: var(--color-foreground-primary);
font-size: deprecated.$fs-16;
height: deprecated.$s-32;
font-size: px2rem(16);
block-size: $sz-32;
margin: 0;
border: none;
padding: deprecated.$s-6;
width: 100%;
padding: px2rem(6);
inline-size: 100%;
&:focus-visible {
border: deprecated.$s-1 solid var(--color-accent-primary);
border: $b-1 solid var(--color-accent-primary);
outline: none;
}
}
.close {
cursor: pointer;
position: absolute;
top: deprecated.$s-1;
right: calc(-1 * deprecated.$s-8);
svg {
fill: var(--color-foreground-secondary);
height: deprecated.$s-16;
transform: rotate(45deg) translateY(7px);
width: deprecated.$s-16;
margin: 0;
}
&:hover {
svg {
fill: var(--element-foreground-warning);
}
}
}

View File

@ -10,9 +10,9 @@
[app.main.data.event :as ev]
[app.main.store :as st]
[app.main.ui.dashboard.import :as udi]
[app.main.ui.ds.foundations.assets.icon :refer [icon*] :as i]
[app.main.ui.ds.product.empty-placeholder :refer [empty-placeholder*]]
[app.main.ui.ds.product.loader :refer [loader*]]
[app.main.ui.icons :as deprecated-icon]
[app.util.dom :as dom]
[app.util.i18n :as i18n :refer [tr]]
[okulary.core :as l]
@ -131,7 +131,7 @@
:on-mouse-leave on-mouse-leave}
(if show-text?
(tr "dashboard.empty-project.create")
deprecated-icon/add)]]))))
[:> icon* {:icon-id i/add :size "xl"}])]]))))
(mf/defc loading-placeholder*
[]

View File

@ -12,8 +12,8 @@
[app.main.refs :as refs]
[app.main.store :as st]
[app.main.ui.dashboard.grid :refer [grid*]]
[app.main.ui.ds.foundations.assets.icon :refer [icon*] :as i]
[app.main.ui.hooks :as hooks]
[app.main.ui.icons :as deprecated-icon]
[app.util.dom :as dom]
[app.util.i18n :as i18n :refer [tr]]
[okulary.core :as l]
@ -62,17 +62,20 @@
(cond
(empty? search-term)
[:div {:class (stl/css :grid-empty-placeholder :search)}
[:div {:class (stl/css :icon)} deprecated-icon/search]
[:div {:class (stl/css :icon)}
[:> icon* {:icon-id i/search :size "xl"}]]
[:div {:class (stl/css :text)} (tr "dashboard.type-something")]]
(nil? result)
[:div {:class (stl/css :grid-empty-placeholder :search)}
[:div {:class (stl/css :icon)} deprecated-icon/search]
[:div {:class (stl/css :icon)}
[:> icon* {:icon-id i/search :size "xl"}]]
[:div {:class (stl/css :text)} (tr "dashboard.searching-for" search-term)]]
(empty? result)
[:div {:class (stl/css :grid-empty-placeholder :search)}
[:div {:class (stl/css :icon)} deprecated-icon/search]
[:div {:class (stl/css :icon)}
[:> icon* {:icon-id i/search :size "xl"}]]
[:div {:class (stl/css :text)} (tr "dashboard.no-matches-for" search-term)]]
:else