From 6acb360f4ad5dc24faec9d67f1fc8725456755de Mon Sep 17 00:00:00 2001 From: Eva Marco Date: Wed, 21 Oct 2026 15:59:31 +0200 Subject: [PATCH] :recycle: Use DS icons and an icon button in the undo history panel (#12472) * :recycle: Use DS icons and an icon button in the undo history panel The undo history panel (History > Actions) drew its entry type icons with the deprecated icon set, and the arrow that shows the details of an entry was a div with a click handler: no button role, no keyboard access and no accessible name. - The entry type icons use the DS icon (drop-icon is i/drop in the DS). - The arrow becomes a DS action icon button labeled "Show details", with aria-expanded for its state. It keeps its data attribute, its classes and its click handler, and it also shows on keyboard focus. The DS icon draws 12px inside a 16px box, so the entry icon gets a negative margin, and the button wrapper a negative end margin: icon, arrow and text stay where they were. The arrow turns through its own class instead of an svg rule. Add the workspace.undo.entry.show-details key (en, es) and a visual test that draws a rectangle, opens the actions list and expands the entry. AI-assisted-by: claude-opus-5-5 * :recycle: Move undo history panel styles off the deprecated styles Move history.scss to DS tokens, sizes, borders and logical properties, and drop the refactor import. The entry color aliases become local variables of the entry, set by its disabled and hover states, and the arrow opacity goes through one as well. AI-assisted-by: claude-opus-5-5 * :lipstick: Place the undo history icon and arrow without negative margins Drop the -2px margin of the entry icon and the -6px end margin of the details button wrapper. Keep text and arrow in place with token spacing instead: the text after the 16px icon box starts with --sp-xs instead of --sp-s, and the entry end padding is --sp-xxs, since the 24px button leaves 6px around its 12px arrow. The icon now sits 2px to the right, centered in its box. AI-assisted-by: claude-opus-5-5 --- .../ui/visual-specs/workspace.spec.js | 36 +++++++ .../main/ui/workspace/sidebar/history.cljs | 53 +++++----- .../main/ui/workspace/sidebar/history.scss | 98 +++++++++---------- frontend/translations/en.po | 4 + frontend/translations/es.po | 4 + 5 files changed, 120 insertions(+), 75 deletions(-) diff --git a/frontend/playwright/ui/visual-specs/workspace.spec.js b/frontend/playwright/ui/visual-specs/workspace.spec.js index 6a7b7325e9..92ac8dddb0 100644 --- a/frontend/playwright/ui/visual-specs/workspace.spec.js +++ b/frontend/playwright/ui/visual-specs/workspace.spec.js @@ -560,6 +560,42 @@ test.describe("Right header", () => { }); }); +test.describe("Undo history", () => { + test("Shows the actions list with an entry expanded", async ({ page }) => { + const workspace = new WorkspacePage(page); + await workspace.setupEmptyFile(); + await workspace.mockRPC( + "update-file?id=*", + "workspace/update-file-empty.json", + ); + await workspace.mockRPC( + "get-file-snapshots?file-id=*", + "workspace/versions-snapshot-1.json", + ); + await workspace.goToWorkspace(); + + await page.keyboard.press("r"); + await workspace.clickWithDragViewportAt(128, 128, 200, 100); + + await page.keyboard.press("Control+Alt+h"); + await page.getByRole("tab", { name: "Actions" }).click(); + const panel = page.getByRole("tabpanel", { name: "Actions" }); + const entry = panel.getByText("New rectangle"); + // The short id and the relative time change on every run. + const mask = [page.locator('[class*="history-entry-meta"]')]; + + // The expand arrow only shows while the entry is hovered. + await entry.hover(); + await expect(panel).toHaveScreenshot({ mask }); + + // The data attribute exists before and after the DS migration. + await panel.locator('[data-has-entry="true"]').click(); + await expect(panel.getByText("Rectangle", { exact: true })).toBeVisible(); + await entry.hover(); + await expect(panel).toHaveScreenshot({ mask }); + }); +}); + test.describe("Palette", () => { test("Shows the bottom palette expanded and collapsed", async ({ page }) => { const workspace = new WorkspacePage(page); diff --git a/frontend/src/app/main/ui/workspace/sidebar/history.cljs b/frontend/src/app/main/ui/workspace/sidebar/history.cljs index 09e8e68583..293a79ce1e 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/history.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/history.cljs @@ -13,9 +13,9 @@ [app.main.data.workspace.undo :as dwu] [app.main.refs :as refs] [app.main.store :as st] - [app.main.ui.ds.foundations.assets.icon :as i] + [app.main.ui.ds.buttons.icon-button :refer [icon-button*]] + [app.main.ui.ds.foundations.assets.icon :as i :refer [icon*]] [app.main.ui.ds.product.empty-state :refer [empty-state*]] - [app.main.ui.icons :as deprecated-icon] [app.util.dom :as dom] [app.util.i18n :refer [tr] :as i18n] [cuerdas.core :as str] @@ -155,22 +155,23 @@ :move (tr "workspace.undo.entry.move" value) (tr "workspace.undo.entry.unknown" value)))) -(defn entry->icon [{:keys [type]}] +(defn- entry->icon-id + [{:keys [type]}] (case type - :page deprecated-icon/document - :shape deprecated-icon/svg - :rect deprecated-icon/rectangle - :circle deprecated-icon/ellipse - :text deprecated-icon/text - :path deprecated-icon/path - :frame deprecated-icon/board - :group deprecated-icon/group - :color deprecated-icon/drop-icon - :typography deprecated-icon/text-palette - :component deprecated-icon/component - :media deprecated-icon/img - :image deprecated-icon/img - deprecated-icon/svg)) + :page i/document + :shape i/svg + :rect i/rectangle + :circle i/ellipse + :text i/text + :path i/path + :frame i/board + :group i/group + :color i/drop + :typography i/text-palette + :component i/component + :media i/img + :image i/img + i/svg)) (defn is-shape? [type] (contains? #{:shape :rect :circle :text :path :frame :group} type)) @@ -339,7 +340,7 @@ [:div {:class (stl/css :history-entry-summary)} [:div {:class (stl/css :history-entry-summary-icon)} - (entry->icon entry)] + [:> icon* {:icon-id (entry->icon-id entry) :size "s"}]] [:div {:class (stl/css :history-entry-summary-text)} [:div {:class (stl/css :history-entry-title)} (entry->message entry)] @@ -363,11 +364,17 @@ [:span {:class (stl/css :history-entry-author)} (tr "workspace.undo.entry.by" author)])])] (when (:detail entry) - [:div {:class (stl/css-case :history-entry-summary-button true - :button-opened @show-detail?) - :on-click toggle-show-detail - :data-has-entry (dm/str (not (nil? (:detail entry))))} - deprecated-icon/arrow])] + [:> icon-button* {:variant "action" + :class (stl/css-case :history-entry-summary-button true + :button-opened @show-detail?) + :tooltip-class (stl/css :history-entry-summary-button-wrapper) + :aria-label (tr "workspace.undo.entry.show-details") + :aria-expanded @show-detail? + :icon i/arrow + :icon-size "s" + :icon-class (stl/css :history-entry-summary-arrow) + :on-click toggle-show-detail + :data-has-entry (dm/str (not (nil? (:detail entry))))}])] (when @show-detail? [:> history-entry-details* {:entry entry}])])) diff --git a/frontend/src/app/main/ui/workspace/sidebar/history.scss b/frontend/src/app/main/ui/workspace/sidebar/history.scss index e7e8b1a2cd..59d56a64ad 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/history.scss +++ b/frontend/src/app/main/ui/workspace/sidebar/history.scss @@ -4,13 +4,14 @@ // // Copyright (c) KALEIDOS SUBSIDIARY SL +@use "ds/_borders.scss" as *; @use "ds/_sizes.scss" as *; -@use "refactor/common-refactor.scss" as deprecated; +@use "ds/_utils.scss" as *; .history-toolbox { display: flex; flex-direction: column; - background-color: var(--panel-background-color); + background-color: var(--color-background-primary); } .history-entry-empty { @@ -22,21 +23,29 @@ } .history-entries { - height: calc(100vh - deprecated.$s-100); - padding: deprecated.$s-12; + block-size: calc(100vh - #{px2rem(100)}); + padding: var(--sp-m); overflow: hidden auto; - font-size: deprecated.$fs-12; + font-size: px2rem(12); } .history-entry { + --entry-background-color: var(--color-background-tertiary); + --entry-foreground-color: var(--color-foreground-secondary); + --entry-border-color: transparent; + --entry-arrow-opacity: 0; + display: flex; justify-content: center; flex-direction: column; - min-height: deprecated.$s-32; - margin: deprecated.$s-4; - padding: deprecated.$s-4 deprecated.$s-8; - border: deprecated.$s-2 solid transparent; - border-radius: deprecated.$s-8; + min-block-size: $sz-32; + margin: var(--sp-xs); + + // The end padding is small because the details button leaves 6px + // around its 12px arrow. + padding: var(--sp-xs) var(--sp-xxs) var(--sp-xs) var(--sp-s); + border: $b-2 solid var(--entry-border-color); + border-radius: $br-8; background-color: var(--entry-background-color); cursor: pointer; transition: border 0.2s; @@ -46,18 +55,18 @@ align-items: center; .history-entry-summary-icon { - svg { - @extend %button-icon-small; - - stroke: var(--entry-foreground-color); - } + display: flex; + color: var(--entry-foreground-color); } .history-entry-summary-text { display: flex; flex-direction: column; - gap: deprecated.$s-2; - margin: 0 deprecated.$s-8; + gap: var(--sp-xxs); + + // The DS icon draws 12px inside a 16px box: a smaller start margin + // keeps the text where it was. + margin-inline: var(--sp-xs) var(--sp-s); color: var(--color-foreground-primary); min-inline-size: 0; } @@ -73,7 +82,7 @@ // template ("hash · time by Name") survive rendering. Issue #7660. .history-entry-meta { color: var(--color-foreground-secondary); - font-size: deprecated.$fs-10; + font-size: px2rem(10); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; @@ -99,28 +108,28 @@ white-space: nowrap; } - .history-entry-summary-button { - opacity: deprecated.$op-0; - margin-left: auto; + // Tooltip wrapper of the details button, pushed to the end of the row. + .history-entry-summary-button-wrapper { + margin-inline-start: auto; + } - &.button-opened { - svg { - transform: rotate(90deg); - } + .history-entry-summary-button { + opacity: var(--entry-arrow-opacity); + + &:focus-visible { + --entry-arrow-opacity: 1; } - svg { - @extend %button-icon-small; - - stroke: var(--entry-foreground-color); + &.button-opened .history-entry-summary-arrow { + transform: rotate(90deg); } } } .history-entry-detail { display: block; - padding-top: deprecated.$s-16; - color: var(--modal-text-foreground-color); + padding-block-start: var(--sp-l); + color: var(--color-foreground-secondary); .history-entry-details-list { margin: 0; @@ -128,31 +137,16 @@ } &.disabled { - border-color: var(--entry-border-color-disabled); - background-color: var(--entry-background-color-disabled); + --entry-background-color: var(--color-background-primary); + --entry-border-color: var(--color-background-quaternary); } &.hover, &:hover { - background-color: var(--entry-background-color-hover); - color: var(--entry-foreground-color-hover); + --entry-background-color: var(--color-background-quaternary); + --entry-foreground-color: var(--color-foreground-primary); + --entry-arrow-opacity: 1; - .history-entry-summary { - .history-entry-summary-icon { - svg { - stroke: var(--entry-foreground-color-hover); - } - } - - .history-entry-summary-button { - opacity: deprecated.$op-10; - - &.button-opened { - svg { - transform: rotate(90deg); - } - } - } - } + color: var(--color-foreground-primary); } } diff --git a/frontend/translations/en.po b/frontend/translations/en.po index 078f9c694b..040339f8ef 100644 --- a/frontend/translations/en.po +++ b/frontend/translations/en.po @@ -9407,6 +9407,10 @@ msgstr "There are no history changes so far" msgid "workspace.undo.entry.by" msgstr "by %s" +#: src/app/main/ui/workspace/sidebar/history.cljs +msgid "workspace.undo.entry.show-details" +msgstr "Show details" + #: src/app/main/ui/workspace/sidebar/history.cljs:154 msgid "workspace.undo.entry.delete" msgstr "Deleted %s" diff --git a/frontend/translations/es.po b/frontend/translations/es.po index 23f9f2f47c..e369ed0b12 100644 --- a/frontend/translations/es.po +++ b/frontend/translations/es.po @@ -9513,6 +9513,10 @@ msgstr "Todavía no hay cambios en el histórico" msgid "workspace.undo.entry.by" msgstr "por %s" +#: src/app/main/ui/workspace/sidebar/history.cljs +msgid "workspace.undo.entry.show-details" +msgstr "Mostrar detalles" + #: src/app/main/ui/workspace/sidebar/history.cljs:154 msgid "workspace.undo.entry.delete" msgstr "%s eliminado"