♻️ Use DS buttons and icons in the share link modal (#12562)

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

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

* ♻️ 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
This commit is contained in:
Eva Marco 2026-10-28 09:20:41 +01:00 committed by GitHub
parent ac52c41903
commit c05b2e2ead
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 173 additions and 119 deletions

View File

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

View File

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

View File

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

View File

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