From c05b2e2ead3b3a8173c2a43d296f12dab01e9bc3 Mon Sep 17 00:00:00 2001 From: Eva Marco Date: Wed, 28 Oct 2026 09:20:41 +0100 Subject: [PATCH] :recycle: Use DS buttons and icons in the share link modal (#12562) * :recycle: Use DS buttons and icons in the share link modal - The close button becomes a ghost DS icon button named "Close"; the class that places it goes on the tooltip wrapper. - "Get link", "Destroy link" and the confirmation buttons were raw inputs; they become DS primary, destructive and secondary buttons. - The copy link button stays a raw segment of the link field; its icon becomes the DS clipboard icon, a bit larger than the legacy drawing. - "Manage permissions" stays a link-style toggle; its arrow becomes the DS icon (the rotation still shows the state) and it says whether the permissions are expanded. The page checkboxes and the permission selects stay for their own groups. A new create share link fixture and a visual test cover the modal before and after getting a link, with the permissions expanded, and the destroy confirmation. AI-assisted-by: claude-opus-5-5 * :recycle: Move share link modal styles closer to the DS The share link stylesheet resolves its legacy sizes, spacing, mixins, z-index and color aliases to DS values and tokens, with logical properties. The copy button inlines %button-secondary with local color variables, so it looks the same at rest, on hover, on focus and while pressed; the visual test now also covers its hover. The legacy typographies become the DS ones, each differing in one detail: the title and "Manage permissions" use headline-small (12px instead of 11px) and the texts body-small (line height 1.3 instead of 1.4), so the modal changes height by about 1px. refactor/ stays only for the modal container, the link input and the page checkboxes. AI-assisted-by: claude-opus-5-5 * :recycle: Use a DS icon button for the share link copy button The copy button next to the link was a raw button with the secondary button styles inlined in share_link.scss. It is now a secondary icon-button* named "Copy link", so the .copy-button block goes away (review feedback). Visible changes: the DS secondary look (grey icon at rest, no lighter background on hover) and a 32px wide button instead of 28px, so the link text ends 4px earlier. AI-assisted-by: claude-opus-5-5 --- .../data/viewer/create-share-link.json | 8 + .../ui/visual-specs/visual-viewer.spec.js | 47 +++++ .../src/app/main/ui/viewer/share_link.cljs | 59 +++--- .../src/app/main/ui/viewer/share_link.scss | 178 +++++++++--------- 4 files changed, 173 insertions(+), 119 deletions(-) create mode 100644 frontend/playwright/data/viewer/create-share-link.json diff --git a/frontend/playwright/data/viewer/create-share-link.json b/frontend/playwright/data/viewer/create-share-link.json new file mode 100644 index 0000000000..2d92a75545 --- /dev/null +++ b/frontend/playwright/data/viewer/create-share-link.json @@ -0,0 +1,8 @@ +{ + "~:id": "~u0c7a1f2e-1b2c-8180-8007-0000000000a1", + "~:file-id": "~udd5cc0bb-91ff-81b9-8004-77df9cd3edb1", + "~:who-comment": "team", + "~:who-inspect": "team", + "~:pages": { "~#set": ["~udd5cc0bb-91ff-81b9-8004-77df9cd3edb2"] }, + "~:owner-id": "~uc7ce0794-0992-8105-8004-38e630f29a9b" +} diff --git a/frontend/playwright/ui/visual-specs/visual-viewer.spec.js b/frontend/playwright/ui/visual-specs/visual-viewer.spec.js index 5dac8b7377..450bba4a2e 100644 --- a/frontend/playwright/ui/visual-specs/visual-viewer.spec.js +++ b/frontend/playwright/ui/visual-specs/visual-viewer.spec.js @@ -442,3 +442,50 @@ test.describe("Copy buttons", () => { await expect(copy).toHaveScreenshot(shot); }); }); + +test.describe("Share link modal controls", () => { + const shot = { maxDiffPixelRatio: 0, threshold: 0 }; + + test("Get, copy and destroy link", async ({ page }) => { + const viewerPage = new ViewerPage(page); + await viewerPage.setupLoggedInUser(); + await viewerPage.setupFileWithSingleBoard(); + await viewerPage.mockRPC( + "create-share-link", + "viewer/create-share-link.json", + ); + await viewerPage.goToViewer({ + fileId: singleBoardFileId, + pageId: singleBoardPageId, + }); + + await page.getByRole("button", { name: "Share" }).click(); + const getLink = page.getByRole("button", { name: "Get link" }); + await expect(getLink).toBeVisible(); + const modal = page.locator("[class*='share-link-dialog']"); + await page.mouse.move(10, 700); + await expect(modal).toHaveScreenshot(shot); + + await getLink.click(); + const destroy = page.getByRole("button", { name: "Destroy link" }); + await expect(destroy).toBeVisible(); + await page.mouse.move(10, 700); + await expect(modal).toHaveScreenshot(shot); + + // A raw button with a title before, a DS icon button named "Copy link" after. + await modal + .getByTitle("Copy link") + .or(modal.getByRole("button", { name: "Copy link" })) + .hover(); + await expect(modal).toHaveScreenshot(shot); + + await page.getByText("Manage permissions").click(); + await page.mouse.move(10, 700); + await expect(modal).toHaveScreenshot(shot); + + await destroy.click(); + await expect(page.getByRole("button", { name: "Cancel" })).toBeVisible(); + await page.mouse.move(10, 700); + await expect(modal).toHaveScreenshot(shot); + }); +}); diff --git a/frontend/src/app/main/ui/viewer/share_link.cljs b/frontend/src/app/main/ui/viewer/share_link.cljs index f0a4c3de22..32e9117908 100644 --- a/frontend/src/app/main/ui/viewer/share_link.cljs +++ b/frontend/src/app/main/ui/viewer/share_link.cljs @@ -19,6 +19,9 @@ [app.main.router :as rt] [app.main.store :as st] [app.main.ui.components.select :refer [select]] + [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 :refer [icon*] :as i] [app.main.ui.icons :as deprecated-icon] [app.util.clipboard :as clipboard] [app.util.dom :as dom] @@ -169,10 +172,11 @@ [:div {:class (stl/css :share-link-header)} [:h2 {:class (stl/css :share-link-title)} (tr "common.share-link.title")] - [:button {:class (stl/css :modal-close-button) - :on-click on-close - :title (tr "labels.close")} - deprecated-icon/close]] + [:> icon-button* {:variant "ghost" + :tooltip-class (stl/css :modal-close-button) + :aria-label (tr "labels.close") + :icon i/close + :on-click on-close}]] [:div {:class (stl/css :modal-content)} [:div {:class (stl/css :share-link-section)} (when (and (not confirm?) (some? current-link)) @@ -183,10 +187,10 @@ :placeholder (tr "common.share-link.placeholder") :read-only true}] - [:button {:class (stl/css :copy-button) - :title (tr "viewer.header.share.copy-link") - :on-click copy-link} - deprecated-icon/clipboard]]) + [:> icon-button* {:variant "secondary" + :aria-label (tr "viewer.header.share.copy-link") + :icon i/clipboard + :on-click copy-link}]]) [:div {:class (stl/css :hint-wrapper)} (when (not ^boolean confirm?) @@ -197,37 +201,38 @@ [:div {:class (stl/css :description)} (tr "common.share-link.confirm-deletion-link-description")] [:div {:class (stl/css :actions)} - [:input {:type "button" - :class (stl/css :button-cancel) - :on-click #(reset! confirm* false) - :value (tr "labels.cancel")}] - [:input {:type "button" - :class (stl/css :button-danger) - :on-click delete-link - :value (tr "common.share-link.destroy-link")}]]] + [:> button* {:variant "secondary" + :type "button" + :on-click #(reset! confirm* false)} + (tr "labels.cancel")] + [:> button* {:variant "destructive" + :type "button" + :on-click delete-link} + (tr "common.share-link.destroy-link")]]] (some? current-link) - [:input - {:type "button" - :class (stl/css :button-danger) - :on-click try-delete-link - :value (tr "common.share-link.destroy-link")}] + [:> button* {:variant "destructive" + :type "button" + :on-click try-delete-link} + (tr "common.share-link.destroy-link")] :else - [:input - {:type "button" - :class (stl/css :button-active) - :on-click create-link - :value (tr "common.share-link.get-link")}])]] + [:> button* {:variant "primary" + :type "button" + :on-click create-link} + (tr "common.share-link.get-link")])]] (when (not ^boolean confirm?) [:div {:class (stl/css :permissions-section)} [:button {:class (stl/css :manage-permissions) + :type "button" + :aria-expanded perms-visible? :on-click toggle-perms-visibility} [:span {:class (stl/css-case :icon true :rotated perms-visible?)} - deprecated-icon/arrow] + [:> icon* {:icon-id i/arrow + :class (stl/css :arrow-icon)}]] (tr "common.share-link.manage-ops")] (when ^boolean perms-visible? diff --git a/frontend/src/app/main/ui/viewer/share_link.scss b/frontend/src/app/main/ui/viewer/share_link.scss index 43e2ba8910..336ad5ede3 100644 --- a/frontend/src/app/main/ui/viewer/share_link.scss +++ b/frontend/src/app/main/ui/viewer/share_link.scss @@ -4,149 +4,140 @@ // // Copyright (c) KALEIDOS SUBSIDIARY SL +@use "ds/_borders.scss" as *; +@use "ds/_sizes.scss" as *; +@use "ds/_utils.scss" as *; +@use "ds/typography.scss" as *; + +// refactor/ stays for the modal placeholder (%modal-container-base), the +// link input (%input-element) and the page checkboxes (%input-checkbox, +// %button-icon-small for their tick), which move with their own groups. @use "refactor/common-refactor.scss" as deprecated; .share-modal { display: block; position: absolute; - top: deprecated.$s-52; - right: deprecated.$s-12; - left: calc(100vw - deprecated.$s-512); - z-index: deprecated.$z-index-modal; + inset-block-start: px2rem(52); + inset-inline: calc(100vw - #{$sz-512}) var(--sp-m); + z-index: var(--z-index-set); } .share-link-dialog { @extend %modal-container-base; - min-height: unset; + min-block-size: unset; } .share-link-header { - margin-bottom: deprecated.$s-24; + margin-block-end: var(--sp-xxl); } .share-link-title { - @include deprecated.uppercase-title-typography; + @include use-typography("headline-small"); - color: var(--modal-title-foreground-color); + color: var(--color-foreground-primary); } +// Tooltip wrapper of the close button: it places it in the corner. .modal-close-button { - @extend %modal-close-btn-base; + position: absolute; + inset-block-start: var(--sp-s); + inset-inline-end: var(--sp-xs); } .modal-content { - @include deprecated.body-small-typography; - @include deprecated.flex-column; + @include use-typography("body-small"); - gap: deprecated.$s-24; + display: flex; + flex-direction: column; + gap: var(--sp-xxl); } .share-link-section { - @include deprecated.flex-column; - - gap: deprecated.$s-8; + display: flex; + flex-direction: column; + gap: var(--sp-s); } .hint-wrapper { - @include deprecated.flex-row; + display: flex; + align-items: center; + gap: var(--sp-xs); } .hint { flex-grow: 1; - color: var(--modal-text-foreground-color); + color: var(--color-foreground-secondary); } .custon-input-wrapper { - @include deprecated.flex-row; - - border-radius: deprecated.$br-8; - height: deprecated.$s-32; - width: 100%; - background-color: var(--input-background-color); + display: flex; + align-items: center; + gap: var(--sp-xs); + border-radius: $br-8; + block-size: $sz-32; + inline-size: 100%; + background-color: var(--color-background-tertiary); } .input-text { @extend %input-element; - @include deprecated.body-small-typography; + @include use-typography("body-small"); - color: var(--input-foreground-color-active); - padding-left: deprecated.$s-8; + color: var(--color-foreground-primary); + padding-inline-start: var(--sp-s); margin: 0; flex-grow: 1; &:focus { outline: none; - border: deprecated.$s-1 solid var(--input-border-color-active); - } -} - -.copy-button { - @extend %button-secondary; - @include deprecated.flex-row; - - gap: deprecated.$s-8; - height: deprecated.$s-32; - width: deprecated.$s-28; - - svg { - @extend %button-icon; - - stroke: var(--icon-foreground-hover); + border: $b-1 solid var(--color-accent-primary); } } .description { - @include deprecated.body-small-typography; + @include use-typography("body-small"); - color: var(--modal-text-foreground-color); - margin-bottom: deprecated.$s-24; + color: var(--color-foreground-secondary); + margin-block-end: var(--sp-xxl); } .actions { - @include deprecated.flex-row; - + display: flex; + align-items: center; + gap: var(--sp-xs); justify-content: flex-end; } -.button-active { - @extend %modal-accept-btn; -} - -.button-cancel { - @extend %modal-cancel-btn; -} - -.button-danger { - @extend %modal-danger-btn; -} - .permissions-section { - @include deprecated.flex-column; - - gap: deprecated.$s-8; + display: flex; + flex-direction: column; + gap: var(--sp-s); } .manage-permissions { - @include deprecated.button-style; - @include deprecated.uppercase-title-typography; + @include use-typography("headline-small"); - color: var(--menu-foreground-color-rest); - height: deprecated.$s-32; + border: none; + background: none; + cursor: pointer; + color: var(--color-foreground-secondary); + block-size: $sz-32; display: flex; align-items: center; padding: 0; } .icon { - @include deprecated.flex-center; + display: flex; + justify-content: center; + align-items: center; + margin-inline-end: px2rem(6); - margin-right: deprecated.$s-6; - - svg { - @extend %button-icon; - - stroke: var(--icon-foreground); + .arrow-icon { + flex-shrink: 0; + stroke-width: 1px; + color: var(--color-foreground-secondary); } &.rotated { @@ -158,74 +149,77 @@ .access-mode, .inspect-mode { display: flex; - width: 100%; + inline-size: 100%; } .view-mode { - max-height: deprecated.$s-216; + max-block-size: px2rem(216); overflow: hidden auto; scrollbar-gutter: stable; } .subtitle { - color: var(--modal-text-foreground-color); + color: var(--color-foreground-secondary); display: flex; align-items: center; justify-content: flex-start; - width: deprecated.$s-136; - height: deprecated.$s-32; + inline-size: px2rem(136); + block-size: $sz-32; } .items { flex-grow: 1; - color: var(--input-foreground-color-active); + color: var(--color-foreground-primary); } .select-all-row { - @include deprecated.flex-row; - + display: flex; + align-items: center; + gap: var(--sp-xs); justify-content: space-between; - height: deprecated.$s-32; - border-bottom: deprecated.$s-1 solid var(--input-border-color-disabled); + block-size: $sz-32; + border-block-end: $b-1 solid var(--color-background-quaternary); } .select-all-label { - color: var(--input-foreground-color-active); + color: var(--color-foreground-primary); } .pages-selection { margin: 0; + // The page rows have no class of their own. li { - border-bottom: deprecated.$s-1 solid var(--input-border-color-disabled); + border-block-end: $b-1 solid var(--color-background-quaternary); } li:last-child { - border-bottom: none; + border-block-end: none; } } .count-pages, .current-tag { - @include deprecated.body-small-typography; + @include use-typography("body-small"); - color: var(--input-foreground-color); + color: var(--color-foreground-secondary); } .checkbox-wrapper { @extend %input-checkbox; - height: deprecated.$s-32; + block-size: $sz-32; padding: 0; span.checked { - background-color: var(--input-checkbox-background-color-active); - border: deprecated.$s-1 solid var(--input-checkbox-background-color-active); + background-color: var(--color-accent-primary); + border: $b-1 solid var(--color-accent-primary); + // The legacy tick icon has no class; it moves with the checkboxes. svg { @extend %button-icon-small; - stroke: var(--input-checkbox-foreground-color-active); + stroke: var(--color-background-primary); } } }