From e24e9dc747cc20b8f342731dac927fa6574595a9 Mon Sep 17 00:00:00 2001 From: Eva Marco Date: Mon, 26 Oct 2026 14:39:06 +0100 Subject: [PATCH] :recycle: Use DS buttons and icons in the viewer header (#12523) * :recycle: Use DS buttons and icons in the viewer header The section buttons, full screen and edit become ghost DS icon buttons (the last two were spans), with aria-pressed where they toggle. Share and "Log in or sign up" become DS buttons. The zoom widget becomes a button that toggles its menu, like the workspace one, with DS zoom buttons inside. The breadcrumb and the current board name become buttons with aria-expanded, and the page menu moves next to its button. The arrow and tick icons use DS icons. The logo stays: it is brand art. A visual test covers the header in two sections and both menus. AI-assisted-by: claude-opus-5-5 * :recycle: Move viewer header styles off the deprecated styles Spacing, sizes, borders, typographies and color aliases move to DS tokens and logical properties, and the button placeholders go away. The section buttons column goes from a fixed 92px to auto, since the three 32px DS buttons no longer fit; the 1fr side columns keep it centered. The zoom widget follows the workspace header one. The 11px project name becomes the 12px DS headline-small. The global .selected rule also highlighted the current page in the page menu: that row keeps its highlight through its own rule. The refactor/ import stays only for the shared menu placeholders; the options z-index (10) has no DS token and keeps a TODO. AI-assisted-by: claude-opus-5-5 --- .../ui/visual-specs/visual-viewer.spec.js | 56 +++ frontend/src/app/main/ui/viewer/header.cljs | 130 ++++--- frontend/src/app/main/ui/viewer/header.scss | 329 +++++++----------- 3 files changed, 258 insertions(+), 257 deletions(-) diff --git a/frontend/playwright/ui/visual-specs/visual-viewer.spec.js b/frontend/playwright/ui/visual-specs/visual-viewer.spec.js index 65ffbe4af8..432b3df9c6 100644 --- a/frontend/playwright/ui/visual-specs/visual-viewer.spec.js +++ b/frontend/playwright/ui/visual-specs/visual-viewer.spec.js @@ -266,3 +266,59 @@ test.describe("Board navigation", () => { }); }); }); + +test.describe("Viewer header", () => { + test("Shows the header buttons and its menus", async ({ page }) => { + const viewerPage = new ViewerPage(page); + await viewerPage.setupLoggedInUser(); + await viewerPage.setupFileWithComments(); + await viewerPage.goToViewer({ + fileId: singleBoardFileId, + pageId: singleBoardPageId, + }); + + const header = page.locator("header").first(); + await expect(header).toBeVisible(); + await page.mouse.move(640, 600); + // Interactions section. + await expect(header).toHaveScreenshot({ + maxDiffPixelRatio: 0, + threshold: 0, + }); + + // Zoom menu. + await page.getByTitle("Zoom").click(); + await expect(page.getByText("Fit - Scale down to fit")).toBeVisible(); + await page.mouse.move(640, 600); + await expect(page).toHaveScreenshot({ + clip: { x: 760, y: 0, width: 520, height: 260 }, + maxDiffPixelRatio: 0, + threshold: 0, + }); + await page.keyboard.press("Escape"); + await page.mouse.click(640, 600); + + // Sitemap menu. + await page + .locator("#current-frame") + .locator("..") + .locator("div") + .first() + .click(); + await page.mouse.move(640, 600); + await expect(page).toHaveScreenshot({ + clip: { x: 0, y: 0, width: 520, height: 200 }, + maxDiffPixelRatio: 0, + threshold: 0, + }); + await page.mouse.click(640, 600); + + // Comments section, with its button selected. + await viewerPage.showComments(); + await page.mouse.move(640, 600); + await expect(header).toHaveScreenshot({ + maxDiffPixelRatio: 0, + threshold: 0, + }); + }); +}); diff --git a/frontend/src/app/main/ui/viewer/header.cljs b/frontend/src/app/main/ui/viewer/header.cljs index 3d1404373a..e0907d26aa 100644 --- a/frontend/src/app/main/ui/viewer/header.cljs +++ b/frontend/src/app/main/ui/viewer/header.cljs @@ -14,6 +14,9 @@ [app.main.data.viewer.shortcuts :as sc] [app.main.store :as st] [app.main.ui.components.dropdown :refer [dropdown]] + [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 :refer [icon*]] [app.main.ui.exports.assets :refer [progress-widget]] [app.main.ui.formats :as fmt] [app.main.ui.icons :as deprecated-icon] @@ -47,11 +50,11 @@ (let [open* (mf/use-state false) open? (deref open*) - open-dropdown + toggle-dropdown (mf/use-fn (fn [event] (dom/stop-propagation event) - (reset! open* true))) + (swap! open* not))) close-dropdown (mf/use-fn @@ -73,28 +76,31 @@ (dom/stop-propagation event) (on-decrease)))] - [:div {:class (stl/css-case :zoom-widget true - :selected open?) - :on-click open-dropdown - :title (tr "workspace.header.zoom")} - [:span {:class (stl/css :label)} (fmt/format-percent zoom)] + [:div {:class (stl/css :zoom-widget-wrapper)} + [:button {:type "button" + :class (stl/css-case :zoom-widget true + :selected open?) + :on-click toggle-dropdown + :title (tr "workspace.header.zoom") + :aria-expanded open?} + [:span {:class (stl/css :label)} (fmt/format-percent zoom)]] [:& dropdown {:show open? :on-close close-dropdown} [:ul {:class (stl/css :dropdown)} [:li {:class (stl/css :basic-zoom-bar)} [:span {:class (stl/css :zoom-btns)} - [:button {:class (stl/css :zoom-btn) - :on-click on-decrease} - [:span {:class (stl/css :zoom-icon)} - deprecated-icon/remove-icon]] + [:> icon-button* {:variant "ghost" + :aria-label (tr "shortcuts.decrease-zoom") + :on-click on-decrease + :icon i/remove}] [:p {:class (stl/css :zoom-text)} (fmt/format-percent zoom)] - [:button {:class (stl/css :zoom-btn) - :on-click on-increase} - [:span {:class (stl/css :zoom-icon)} - deprecated-icon/add]]] - [:button {:class (stl/css :reset-btn) - :on-click on-zoom-reset} + [:> icon-button* {:variant "ghost" + :aria-label (tr "shortcuts.increase-zoom") + :on-click on-increase + :icon i/add}]] + [:> button* {:variant "ghost" + :on-click on-zoom-reset} (tr "workspace.header.reset-zoom")]] [:li {:class (stl/css :zoom-option) @@ -188,27 +194,30 @@ :on-fullscreen toggle-fullscreen}] (when (:in-team permissions) - [:span {:on-click go-to-workspace - :class (stl/css :edit-btn)} - deprecated-icon/curve]) + [:> icon-button* {:variant "ghost" + :aria-label (tr "labels.edit") + :icon i/curve + :on-click go-to-workspace}]) - [:span {:title (tr "viewer.header.fullscreen") - :class (stl/css-case :fullscreen-btn true - :selected fullscreen?) - :on-click toggle-fullscreen} - deprecated-icon/expand] + [:> icon-button* {:variant "ghost" + :aria-label (tr "viewer.header.fullscreen") + :aria-pressed (boolean fullscreen?) + :icon i/expand + :on-click toggle-fullscreen}] (when (:in-team permissions) - [:button {:on-click open-share-dialog - :class (stl/css :share-btn)} + [:> button* {:variant "primary" + :class (stl/css :share-btn) + :on-click open-share-dialog} (tr "labels.share")]) (when-not (:is-logged permissions) - [:span {:on-click open-login-dialog - :class (stl/css :go-log-btn)} (tr "labels.log-or-sign")])])) + [:> button* {:variant "ghost" + :on-click open-login-dialog} + (tr "labels.log-or-sign")])])) (mf/defc header-sitemap - [{:keys [project file page frame toggle-thumbnails] :as props}] + [{:keys [project file page frame toggle-thumbnails shown-thumbnails] :as props}] (let [project-name (:name project) file-name (:name file) page-name (:name page) @@ -236,11 +245,15 @@ :title (tr "viewer.header.sitemap")} [:span {:class (stl/css :project-name)} project-name] [:div {:class (stl/css :sitemap-text)} - [:div {:class (stl/css :breadcrumb) - :on-click open-dropdown} - [:span {:class (stl/css :breadcrumb-text)} - (dm/str file-name " / " page-name)] - [:span {:class (stl/css :icon)} deprecated-icon/arrow] + [:div {:class (stl/css :breadcrumb)} + [:button {:type "button" + :class (stl/css :breadcrumb-btn) + :aria-expanded (deref show-dropdown?) + :aria-haspopup "menu" + :on-click open-dropdown} + [:span {:class (stl/css :breadcrumb-text)} + (dm/str file-name " / " page-name)] + [:> icon* {:icon-id i/arrow-down :size "s" :class (stl/css :icon)}]] [:span "/"] [:& dropdown {:show @show-dropdown? :on-close close-dropdown} @@ -254,12 +267,14 @@ [:span {:class (stl/css :label)} (get-in file [:data :pages-index id :name])] (when (= page-id id) - [:span {:class (stl/css :icon-check)} deprecated-icon/tick])])]]] - [:div {:class (stl/css :current-frame) - :id "current-frame" - :on-click toggle-thumbnails} + [:> icon* {:icon-id i/tick :size "s" :class (stl/css :icon-check)}])])]]] + [:button {:type "button" + :class (stl/css :current-frame) + :id "current-frame" + :aria-expanded (boolean shown-thumbnails) + :on-click toggle-thumbnails} [:span {:class (stl/css :frame-name)} frame-name] - [:span {:class (stl/css :icon)} deprecated-icon/arrow]]]])) + [:> icon* {:icon-id i/arrow-down :size "s" :class (stl/css :icon)}]]]])) (def ^:private penpot-logo-icon (deprecated-icon/icon-xref :penpot-logo-icon (stl/css :logo-icon))) @@ -322,33 +337,34 @@ :page page :frame frame :toggle-thumbnails toggle-thumbnails + :shown-thumbnails shown-thumbnails :index index}]] [:div {:class (stl/css :mode-zone)} - [:button {:on-click navigate - :data-value "interactions" - :class (stl/css-case :mode-zone-btn true - :selected (= section :interactions)) - :title (tr "viewer.header.interactions-section" (sc/get-tooltip :open-interactions))} - deprecated-icon/play] + [:> icon-button* {:variant "ghost" + :on-click navigate + :data-value "interactions" + :aria-pressed (= section :interactions) + :aria-label (tr "viewer.header.interactions-section" (sc/get-tooltip :open-interactions)) + :icon i/play}] (when (or (:in-team permissions) (= (:who-comment permissions) "all")) - [:button {:on-click navigate - :data-value "comments" - :class (stl/css-case :mode-zone-btn true - :selected (= section :comments)) - :title (tr "viewer.header.comments-section" (sc/get-tooltip :open-comments))} - deprecated-icon/comments]) + [:> icon-button* {:variant "ghost" + :on-click navigate + :data-value "comments" + :aria-pressed (= section :comments) + :aria-label (tr "viewer.header.comments-section" (sc/get-tooltip :open-comments)) + :icon i/comments}]) (when (or (:in-team permissions) (and (= (:type permissions) :share-link) (= (:who-inspect permissions) "all"))) - [:button {:on-click go-to-inspect - :class (stl/css-case :mode-zone-btn true - :selected (= section :inspect)) - :title (tr "viewer.header.inspect-section" (sc/get-tooltip :open-inspect))} - deprecated-icon/code])] + [:> icon-button* {:variant "ghost" + :on-click go-to-inspect + :aria-pressed (= section :inspect) + :aria-label (tr "viewer.header.inspect-section" (sc/get-tooltip :open-inspect)) + :icon i/code}])] [:& header-options {:section section :permissions permissions diff --git a/frontend/src/app/main/ui/viewer/header.scss b/frontend/src/app/main/ui/viewer/header.scss index ffe476cbda..aee1220748 100644 --- a/frontend/src/app/main/ui/viewer/header.scss +++ b/frontend/src/app/main/ui/viewer/header.scss @@ -4,22 +4,29 @@ // // Copyright (c) KALEIDOS SUBSIDIARY SL +// Only for the shared menu placeholders, which move to the DS with the +// menus and dropdowns. @use "refactor/common-refactor.scss" as deprecated; +@use "ds/_borders.scss" as *; +@use "ds/_sizes.scss" as *; +@use "ds/_utils.scss" as *; +@use "ds/mixins.scss" as *; +@use "ds/typography.scss" as *; .viewer-header { position: absolute; - top: 0; + inset-block-start: 0; grid-column: 1 / span 1; grid-row: 1 / span 1; display: grid; - grid-template-columns: 1fr deprecated.$s-92 1fr; + grid-template-columns: 1fr auto 1fr; justify-content: space-between; align-items: center; - height: deprecated.$s-48; - width: 100vw; - padding: deprecated.$s-8 deprecated.$s-12; + block-size: $sz-48; + inline-size: 100vw; + padding: var(--sp-s) var(--sp-m); transition: transform 400ms ease 300ms; - background-color: var(--panel-background-color); + background-color: var(--color-background-primary); } // FILE NAVIGATION @@ -28,8 +35,8 @@ display: flex; justify-content: flex-start; flex-basis: min-content; - width: 100%; - gap: deprecated.$s-12; + inline-size: 100%; + gap: var(--sp-m); } .home-link { @@ -39,143 +46,137 @@ } .logo-icon { - width: deprecated.$s-28; - height: deprecated.$s-28; - fill: var(--icon-foreground-hover); + inline-size: $sz-28; + block-size: $sz-28; + fill: var(--color-foreground-primary); } .sitemap-zone { - @include deprecated.flex-column; - + display: flex; + flex-direction: column; + gap: var(--sp-xs); position: relative; - width: 100%; + inline-size: 100%; } .project-name { - @include deprecated.uppercase-title-typography; + @include use-typography("headline-small"); - color: var(--title-foreground-color); + color: var(--color-foreground-secondary); } .sitemap-text { - @include deprecated.flex-row; + display: flex; + align-items: center; + gap: var(--sp-xs); } .breadcrumb { - @include deprecated.body-small-typography; - @include deprecated.flex-row; + @include use-typography("body-small"); - color: var(--title-foreground-color); + display: flex; + align-items: center; + gap: var(--sp-xs); + color: var(--color-foreground-secondary); +} + +// Text and arrow that open the page menu: not a DS button. +.breadcrumb-btn { + @include use-typography("body-small"); + + display: flex; + align-items: center; + gap: var(--sp-xs); + padding: 0; + border: none; + background: none; + color: inherit; cursor: pointer; } .breadcrumb-text { - @include deprecated.text-ellipsis; + @include text-ellipsis; - max-width: 12vw; // This is a fallback - max-width: 12cqw; // This is a unit refered to container + max-inline-size: 12vw; // This is a fallback + max-inline-size: 12cqw; // This is a unit refered to container } -.icon { - @include deprecated.flex-center; - - height: deprecated.$s-16; - width: deprecated.$s-16; - - svg { - @extend %button-icon-small; - - transform: rotate(90deg); - stroke: var(--icon-foreground); - } -} - -.dropdown { - position: absolute; +.icon, +.icon-check { + flex-shrink: 0; + color: var(--color-foreground-secondary); } .dropdown-sitemap { @extend %menu-dropdown; - left: 0; - top: calc(deprecated.$s-2 + deprecated.$s-48); - width: deprecated.$s-272; - padding: deprecated.$s-6; + inset-inline-start: 0; + inset-block-start: px2rem(50); + inline-size: px2rem(272); + padding: px2rem(6); } .dropdown-element { @extend %dropdown-element-base; - .icon-check { - @include deprecated.flex-center; - - height: 100%; - width: deprecated.$s-16; - - svg { - @extend %button-icon-small; - - stroke: var(--icon-foreground); - } + &:hover .label { + color: var(--color-foreground-primary); } - &:hover .label { - color: var(--input-foreground-color-active); + // The current page. + &.selected { + outline: none; + border-color: var(--color-background-quaternary); + background-color: var(--color-background-quaternary); + color: var(--color-accent-primary); } } +// Board name that toggles the thumbnails: not a DS button. .current-frame { - @include deprecated.body-small-typography; - @include deprecated.flex-row; + @include use-typography("body-small"); + display: flex; + align-items: center; + gap: var(--sp-xs); flex-grow: 1; - color: var(--title-foreground-color-hover); + padding: 0; + border: none; + background: none; + color: var(--color-foreground-primary); cursor: pointer; - .icon svg { - stroke: var(--title-foreground-color-hover); + .icon { + color: var(--color-foreground-primary); } } .frame-name { - @include deprecated.text-ellipsis; + @include text-ellipsis; - max-width: 17vw; // This is a fallback - max-width: 17cqw; // This is a unit refered to container + max-inline-size: 17vw; // This is a fallback + max-inline-size: 17cqw; // This is a unit refered to container } // SECTION BUTTONS .mode-zone { - @include deprecated.flex-row; - - height: 100%; -} - -.mode-zone-btn { - @extend %button-tertiary; - @include deprecated.flex-center; - - height: deprecated.$s-32; - width: deprecated.$s-28; - padding: 0; - - svg { - @extend %button-icon; - } -} - -.selected { - @extend %button-icon-selected; + display: flex; + align-items: center; + gap: var(--sp-xs); + block-size: 100%; } // OPTION AREA .options-zone { - @include deprecated.flex-row; - + display: flex; + align-items: center; position: relative; justify-content: flex-end; - gap: deprecated.$s-8; - z-index: deprecated.$z-index-10; + gap: var(--sp-s); + + // TODO: no DS z-index token for 10; it keeps the menus above the + // viewer content. + z-index: 10; } .view-options { @@ -185,92 +186,54 @@ cursor: pointer; } -.fullscreen-btn { - @extend %button-tertiary; - @include deprecated.flex-center; - - height: deprecated.$s-32; - width: deprecated.$s-28; - - svg { - @extend %button-icon; - - stroke: var(--icon-foreground); - } -} - .share-btn { - @extend %button-primary; - - height: deprecated.$s-32; - min-width: deprecated.$s-72; - margin-left: deprecated.$s-4; -} - -.edit-btn { - @extend %button-tertiary; - @include deprecated.flex-center; - - height: deprecated.$s-32; - width: deprecated.$s-28; - - svg { - @extend %button-icon; - - stroke: var(--icon-foreground); - } -} - -.go-log-btn { - @extend %button-tertiary; - @include deprecated.body-small-typography; - - height: deprecated.$s-32; - padding: 0 deprecated.$s-8; - border-radius: deprecated.$br-8; - color: var(--button-tertiary-foreground-color-rest); + min-inline-size: px2rem(72); + margin-inline-start: var(--sp-xs); } // ZOOM WIDGET -.zoom-widget { - @include deprecated.button-style; - @include deprecated.flex-center; +.zoom-widget-wrapper { + display: flex; +} - height: deprecated.$s-28; - min-width: deprecated.$s-64; - border-radius: deprecated.$br-8; +.zoom-widget { + --zoom-widget-foreground-color: var(--color-foreground-secondary); + + display: flex; + align-items: center; + justify-content: center; + block-size: $sz-28; + min-inline-size: $sz-64; + padding: 0; + border: none; + border-radius: $br-8; + background: none; + cursor: pointer; .label { - @include deprecated.body-small-typography; + @include use-typography("body-small"); - color: var(--button-tertiary-foreground-color-rest); - } - - &:hover { - .label { - color: var(--button-tertiary-foreground-color-focus); - } + color: var(--zoom-widget-foreground-color); } + &:hover, &.selected { - .label { - color: var(--button-tertiary-foreground-color-focus); - } + --zoom-widget-foreground-color: var(--color-foreground-primary); } } .dropdown { @extend %menu-dropdown; - right: deprecated.$s-2; - top: calc(deprecated.$s-2 + deprecated.$s-48); - width: deprecated.$s-272; + inset-inline-end: px2rem(2); + inset-block-start: px2rem(50); + inline-size: px2rem(272); } .basic-zoom-bar { display: flex; justify-content: space-between; - padding: deprecated.$s-6; + padding: px2rem(6); cursor: auto; } @@ -278,49 +241,15 @@ display: flex; } -.zoom-btn { - @extend %button-tertiary; - - height: deprecated.$s-28; - width: deprecated.$s-28; - border-radius: deprecated.$br-8; - - .zoom-icon { - @include deprecated.flex-center; - - width: deprecated.$s-24; - height: deprecated.$s-32; - - svg { - @extend %button-icon; - - stroke: var(--icon-foreground); - } - } - - &:hover { - .zoom-icon svg { - stroke: var(--button-tertiary-foreground-color-hover); - } - } -} - .zoom-text { - @include deprecated.flex-center; - - height: 100%; - min-width: deprecated.$s-64; + display: flex; + justify-content: center; + align-items: center; + block-size: 100%; + min-inline-size: $sz-64; padding: 0; - margin: 0 deprecated.$s-2; - color: var(--modal-title-foreground-color); -} - -.reset-btn { - @extend %button-tertiary; - - color: var(--button-tertiary-foreground-color-hover); - height: deprecated.$s-28; - border-radius: deprecated.$br-8; + margin: 0 px2rem(2); + color: var(--color-foreground-primary); } .zoom-option { @@ -335,11 +264,11 @@ } &:hover { - color: var(--menu-foreground-color-hover); + color: var(--color-foreground-primary); .shortcuts { .shortcut-key { - color: var(--menu-foreground-color-hover); + color: var(--color-foreground-primary); } } } @@ -349,14 +278,14 @@ [data-fullscreen="true"] .viewer-header::after { content: " "; position: absolute; - width: 100%; - height: deprecated.$s-48; - left: 0; - top: deprecated.$s-48; + inline-size: 100%; + block-size: $sz-48; + inset-inline-start: 0; + inset-block-start: $sz-48; } [data-fullscreen="true"] .viewer-header { - transform: translateY(-#{deprecated.$s-48}); + transform: translateY(-#{$sz-48}); } [data-force-visible="true"] .viewer-header,