From 5dbf0731e5b94b125f96499a37a28b2d02ebe372 Mon Sep 17 00:00:00 2001 From: Eva Marco Date: Wed, 21 Oct 2026 13:11:04 +0200 Subject: [PATCH] :recycle: Use DS buttons and icons in the plugins dialogs (#12470) * :recycle: Use DS buttons and icons in the plugins dialogs The plugins manager and the permissions, permissions update and try-out dialogs used raw buttons, input elements styled as buttons and deprecated icons. Replace them with the DS components: - The four close buttons become ghost icon buttons labeled "Close", shown in their DS tooltip. The class that places them in the corner goes on the tooltip wrapper, so the tooltip anchors to the button. - Install, and the cancel and accept buttons of the three dialogs, become DS primary and secondary buttons. - The empty list puzzle and the external link icons use the DS icon. The permission icons (oauth-*) stay: they are brand art. Add a visual test with fixtures for a team with the plugins feature and a profile with one installed plugin. It shows the empty manager, the manager with a plugin and the permissions dialog. AI-assisted-by: claude-opus-5-5 * :recycle: Move plugins dialog styles off the deprecated styles Remove the styles the DS buttons and icons no longer need (close-icon, primary-button, cancel-button, search-icon and the svg rules of the migrated icons) and move the rest of plugins.scss to DS tokens, DS typographies and logical properties. - The close button wrapper keeps only its position; the deprecated tertiary button style was landing on the tooltip wrapper. - The full-width cancel and accept buttons center their label again: the DS button does not center it when it grows. - The external link icon gets a class instead of an svg rule. - The deprecated body-small used line-height 1.4 and the DS one 1.3, so the permissions dialog is a few pixels shorter. The refactor import stays for the modal placeholders until the modals move to the DS modal. AI-assisted-by: claude-opus-5-5 --- .../get-profile-with-plugins.json | 44 ++++ .../data/workspace/get-team-plugins.json | 24 +++ .../ui/visual-specs/workspace.spec.js | 51 +++++ .../src/app/main/ui/workspace/plugins.cljs | 97 +++++---- .../src/app/main/ui/workspace/plugins.scss | 203 ++++++++---------- 5 files changed, 264 insertions(+), 155 deletions(-) create mode 100644 frontend/playwright/data/logged-in-user/get-profile-with-plugins.json create mode 100644 frontend/playwright/data/workspace/get-team-plugins.json diff --git a/frontend/playwright/data/logged-in-user/get-profile-with-plugins.json b/frontend/playwright/data/logged-in-user/get-profile-with-plugins.json new file mode 100644 index 0000000000..38dbba2b31 --- /dev/null +++ b/frontend/playwright/data/logged-in-user/get-profile-with-plugins.json @@ -0,0 +1,44 @@ +{ + "~:email": "foo@example.com", + "~:is-demo": false, + "~:auth-backend": "penpot", + "~:fullname": "Princesa Leia", + "~:modified-at": "~m1713533116365", + "~:is-active": true, + "~:default-project-id": "~uc7ce0794-0992-8105-8004-38e630f7920b", + "~:id": "~uc7ce0794-0992-8105-8004-38e630f29a9b", + "~:is-muted": false, + "~:default-team-id": "~uc7ce0794-0992-8105-8004-38e630f40f6d", + "~:created-at": "~m1713533116365", + "~:is-blocked": false, + "~:props": { + "~:nudge": { + "~:big": 10, + "~:small": 1 + }, + "~:v2-info-shown": true, + "~:viewed-tutorial?": false, + "~:viewed-walkthrough?": false, + "~:plugins": { + "~:ids": [ + "0a5b1c2d-3e4f-4a5b-8c6d-7e8f9a0b1c2d" + ], + "~:data": { + "0a5b1c2d-3e4f-4a5b-8c6d-7e8f9a0b1c2d": { + "~:plugin-id": "0a5b1c2d-3e4f-4a5b-8c6d-7e8f9a0b1c2d", + "~:version": 2, + "~:name": "Lorem Ipsum generator", + "~:description": "Fill text layers with placeholder text.", + "~:host": "https://plugin.example.com/", + "~:code": "plugin.js", + "~:permissions": { + "~#set": [ + "content:read", + "content:write" + ] + } + } + } + } + } +} diff --git a/frontend/playwright/data/workspace/get-team-plugins.json b/frontend/playwright/data/workspace/get-team-plugins.json new file mode 100644 index 0000000000..83d7cf47ce --- /dev/null +++ b/frontend/playwright/data/workspace/get-team-plugins.json @@ -0,0 +1,24 @@ +{ + "~:features": { + "~#set": [ + "layout/grid", + "styles/v2", + "fdata/pointer-map", + "fdata/objects-map", + "components/v2", + "fdata/shape-data-type", + "plugins/runtime" + ] + }, + "~:permissions": { + "~:type": "~:membership", + "~:is-owner": true, + "~:is-admin": true, + "~:can-edit": true + }, + "~:name": "Default", + "~:modified-at": "~m1713533116375", + "~:id": "~uc7ce0794-0992-8105-8004-38e630f40f6d", + "~:created-at": "~m1713533116375", + "~:is-default": true +} diff --git a/frontend/playwright/ui/visual-specs/workspace.spec.js b/frontend/playwright/ui/visual-specs/workspace.spec.js index 4ff50815a0..99a79bef99 100644 --- a/frontend/playwright/ui/visual-specs/workspace.spec.js +++ b/frontend/playwright/ui/visual-specs/workspace.spec.js @@ -509,6 +509,57 @@ test.describe("Palette", () => { }); }); +test.describe("Plugins", () => { + const setupPlugins = async (workspace, profile) => { + await workspace.setupEmptyFile(); + await workspace.mockRPC("get-team?id=*", "workspace/get-team-plugins.json"); + if (profile) { + await workspace.mockRPC("get-profile", profile); + } + await workspace.goToWorkspace(); + await workspace.page.keyboard.press("Control+Alt+P"); + }; + + test("Shows the plugins manager without plugins", async ({ page }) => { + const workspace = new WorkspacePage(page); + await setupPlugins(workspace); + + await expect(page.getByText("No plugins installed yet")).toBeVisible(); + await expect(page.getByRole("button", { name: "Close" })).toBeVisible(); + await expect(page).toHaveScreenshot(); + }); + + test("Shows the plugins manager and the permissions dialog", async ({ + page, + }) => { + const workspace = new WorkspacePage(page); + await page.route("https://plugin.example.com/manifest.json", (route) => + route.fulfill({ + json: { + name: "Contrast checker", + description: "Check the contrast of the selected layers.", + code: "plugin.js", + permissions: ["content:write", "user:read", "library:write"], + }, + }), + ); + await setupPlugins( + workspace, + "logged-in-user/get-profile-with-plugins.json", + ); + + await expect(page.getByText("Lorem Ipsum generator")).toBeVisible(); + await expect(page).toHaveScreenshot(); + + await page + .getByRole("textbox") + .fill("https://plugin.example.com/manifest.json"); + await page.getByRole("button", { name: "Install" }).click(); + await expect(page.getByText("CONTRAST CHECKER")).toBeVisible(); + await expect(page).toHaveScreenshot(); + }); +}); + test.describe("Nudge modal", () => { test("Shows the nudge amount modal", async ({ page }) => { const workspace = new WorkspacePage(page); diff --git a/frontend/src/app/main/ui/workspace/plugins.cljs b/frontend/src/app/main/ui/workspace/plugins.cljs index 478566b45a..cd73c45664 100644 --- a/frontend/src/app/main/ui/workspace/plugins.cljs +++ b/frontend/src/app/main/ui/workspace/plugins.cljs @@ -20,7 +20,7 @@ [app.main.ui.components.title-bar :refer [title-bar*]] [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.foundations.assets.icon :as i :refer [icon*]] [app.main.ui.icons :as deprecated-icon] [app.plugins.register :as preg] [app.util.avatars :as avatars] @@ -31,9 +31,6 @@ [cuerdas.core :as str] [rumext.v2 :as mf])) -(def ^:private close-icon - (deprecated-icon/icon-xref :close (stl/css :close-icon))) - (defn- normalize-plugin-url "Automatically appens manifest.json if the plugin-uri comes without it." [plugin-url] @@ -211,7 +208,11 @@ [:div {:class (stl/css :modal-overlay)} [:div {:class (stl/css :modal-dialog :plugin-management)} - [:button {:class (stl/css :close-btn) :on-click modal/hide!} close-icon] + [:> icon-button* {:variant "ghost" + :tooltip-class (stl/css :close-btn) + :aria-label (tr "labels.close") + :icon i/close + :on-click modal/hide!}] [:div {:class (stl/css :modal-title)} (tr "workspace.plugins.title")] [:div {:class (stl/css :modal-content)} @@ -222,9 +223,9 @@ :placeholder (tr "workspace.plugins.search-placeholder") :class (stl/css-case :input-error error?)}] - [:button {:class (stl/css :primary-button) - :disabled @fetching-manifest? - :on-click on-install} + [:> button* {:variant "primary" + :disabled @fetching-manifest? + :on-click on-install} (tr "workspace.plugins.install")]] (when error-url? @@ -245,13 +246,17 @@ (if (empty? plugins-state) [:div {:class (stl/css :plugins-empty)} - [:div {:class (stl/css :plugins-empty-logo)} deprecated-icon/puzzle] + [:div {:class (stl/css :plugins-empty-logo)} + [:> icon* {:icon-id i/puzzle :size "m"}]] [:div {:class (stl/css :plugins-empty-text)} (tr "workspace.plugins.empty-plugins")] [:a {:class (stl/css :plugins-link) :href cfg/plugins-list-uri :target "_blank" :on-click #(st/emit! (ev/event {::ev/name "open-plugins-list"}))} - (tr "workspace.plugins.plugin-list-link") deprecated-icon/external-link]] + (tr "workspace.plugins.plugin-list-link") + [:> icon* {:icon-id i/external-link + :size "s" + :class (stl/css :plugins-link-icon)}]]] [:* [:> title-bar* {:collapsable false @@ -411,7 +416,11 @@ [:div {:class (stl/css :modal-overlay)} [:div {:class (stl/css :modal-dialog :plugin-permissions)} - [:button {:class (stl/css :close-btn) :on-click on-close-dialog} close-icon] + [:> icon-button* {:variant "ghost" + :tooltip-class (stl/css :close-btn) + :aria-label (tr "labels.close") + :icon i/close + :on-click on-close-dialog}] [:div {:class (stl/css :modal-title)} (tr "workspace.plugins.permissions.title" (str/upper (:name plugin)))] [:div {:class (stl/css :modal-content)} @@ -423,17 +432,15 @@ [:div {:class (stl/css :modal-footer)} [:div {:class (stl/css :action-buttons)} - [:input - {:class (stl/css :cancel-button :button-expand) - :type "button" - :value (tr "ds.confirm-cancel") - :on-click on-close-dialog}] + [:> button* {:variant "secondary" + :class (stl/css :button-expand) + :on-click on-close-dialog} + (tr "ds.confirm-cancel")] - [:input - {:class (stl/css :primary-button :button-expand) - :type "button" - :value (tr "ds.confirm-allow") - :on-click on-accept-dialog}]]]]])) + [:> button* {:variant "primary" + :class (stl/css :button-expand) + :on-click on-accept-dialog} + (tr "ds.confirm-allow")]]]]])) (mf/defc plugins-permissions-updated-dialog @@ -475,7 +482,11 @@ [:div {:class (stl/css :modal-overlay)} [:div {:class (stl/css :modal-dialog :plugin-permissions)} - [:button {:class (stl/css :close-btn) :on-click on-close-dialog} close-icon] + [:> icon-button* {:variant "ghost" + :tooltip-class (stl/css :close-btn) + :aria-label (tr "labels.close") + :icon i/close + :on-click on-close-dialog}] [:div {:class (stl/css :modal-title)} (tr "workspace.plugins.permissions-update.title" (str/upper (:name plugin)))] @@ -486,17 +497,15 @@ [:div {:class (stl/css :modal-footer)} [:div {:class (stl/css :action-buttons)} - [:input - {:class (stl/css :cancel-button :button-expand) - :type "button" - :value (tr "ds.confirm-cancel") - :on-click on-close-dialog}] + [:> button* {:variant "secondary" + :class (stl/css :button-expand) + :on-click on-close-dialog} + (tr "ds.confirm-cancel")] - [:input - {:class (stl/css :primary-button :button-expand) - :type "button" - :value (tr "ds.confirm-allow") - :on-click on-accept-dialog}]]]]])) + [:> button* {:variant "primary" + :class (stl/css :button-expand) + :on-click on-accept-dialog} + (tr "ds.confirm-allow")]]]]])) (mf/defc plugins-try-out-dialog @@ -530,7 +539,11 @@ [:div {:class (stl/css :modal-overlay)} [:div {:class (stl/css :modal-dialog :plugin-try-out)} - [:button {:class (stl/css :close-btn) :on-click on-close-dialog} close-icon] + [:> icon-button* {:variant "ghost" + :tooltip-class (stl/css :close-btn) + :aria-label (tr "labels.close") + :icon i/close + :on-click on-close-dialog}] [:div {:class (stl/css :modal-title)} [:div {:class (stl/css :plugin-icon)} [:img {:src (if (some? icon) @@ -544,14 +557,12 @@ [:div {:class (stl/css :modal-footer)} [:div {:class (stl/css :action-buttons)} - [:input - {:class (stl/css :cancel-button :button-expand) - :type "button" - :value (tr "workspace.plugins.try-out.cancel") - :on-click on-close-dialog}] + [:> button* {:variant "secondary" + :class (stl/css :button-expand) + :on-click on-close-dialog} + (tr "workspace.plugins.try-out.cancel")] - [:input - {:class (stl/css :primary-button :button-expand) - :type "button" - :value (tr "workspace.plugins.try-out.try") - :on-click on-accept-dialog}]]]]])) + [:> button* {:variant "primary" + :class (stl/css :button-expand) + :on-click on-accept-dialog} + (tr "workspace.plugins.try-out.try")]]]]])) diff --git a/frontend/src/app/main/ui/workspace/plugins.scss b/frontend/src/app/main/ui/workspace/plugins.scss index c67d5ea498..40f770c57c 100644 --- a/frontend/src/app/main/ui/workspace/plugins.scss +++ b/frontend/src/app/main/ui/workspace/plugins.scss @@ -4,8 +4,15 @@ // // 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/mixins.scss" as *; +@use "ds/typography.scss" as *; + +// Only for the modal placeholders (%modal-overlay-base, +// %modal-container-base), until the modals move to the DS modal. +@use "refactor/common-refactor.scss" as deprecated; .modal-overlay { @extend %modal-overlay-base; @@ -17,49 +24,47 @@ display: grid; grid-template-rows: auto 1fr auto; grid-template-columns: minmax(0, 1fr); - max-height: initial; + max-block-size: initial; &.plugin-permissions { - width: deprecated.$s-412; - max-width: deprecated.$s-412; + inline-size: px2rem(412); + max-inline-size: px2rem(412); } &.plugin-management { - width: deprecated.$s-472; - max-width: deprecated.$s-472; + inline-size: px2rem(472); + max-inline-size: px2rem(472); } &.plugin-try-out { - width: deprecated.$s-452; - max-width: deprecated.$s-452; + inline-size: px2rem(452); + max-inline-size: px2rem(452); } + // The separator of the plugins manager has no class. hr { border-color: var(--color-background-tertiary); } } .modal-footer { - margin-top: 2rem; + margin-block-start: var(--sp-xxxl); } +// Goes on the tooltip wrapper of the close icon button. .close-btn { - @extend %modal-close-btn-base; -} - -.close-icon { - @extend %button-icon; - - stroke: var(--icon-foreground); + position: absolute; + inset-block-start: var(--sp-s); + inset-inline-end: var(--sp-xs); } .modal-title { - @include deprecated.headline-medium-typography; + @include use-typography("headline-medium"); - margin-block-end: deprecated.$s-32; - color: var(--modal-title-foreground-color); + margin-block-end: var(--sp-xxxl); + color: var(--color-foreground-primary); display: flex; - gap: deprecated.$s-12; + gap: var(--sp-m); } .modal-content { @@ -67,113 +72,92 @@ flex-direction: column; .plugin-permissions & { - gap: deprecated.$s-20; + gap: var(--sp-xl); } .plugin-management & { - height: deprecated.$s-380; - max-height: deprecated.$s-380; + block-size: px2rem(380); + max-block-size: px2rem(380); } } .modal-message { - font-size: deprecated.$fs-14; + // TODO: No exact DS typography match found. + // Closest match: body-medium. + font-size: px2rem(14); color: var(--color-foreground-secondary); } .modal-paragraph { - font-size: deprecated.$fs-14; + // TODO: No exact DS typography match found. + // Closest match: body-medium. + font-size: px2rem(14); color: var(--color-foreground-primary); } -.primary-button { - @extend %button-primary; - @include deprecated.headline-small-typography; - - padding: deprecated.$s-0 deprecated.$s-16; -} - +// The DS button does not center its label when it grows. .button-expand { - width: 100%; + inline-size: 100%; margin: 0; -} - -.cancel-button { - @extend %button-secondary; - @include deprecated.headline-small-typography; - - padding: deprecated.$s-0 deprecated.$s-16; -} - -.search-icon { - @include deprecated.flex-center; - - width: deprecated.$s-20; - padding: 0 0 0 deprecated.$s-8; - - svg { - @extend %button-icon-small; - - stroke: var(--icon-foreground); - } + justify-content: center; } .top-bar { display: flex; - gap: deprecated.$s-8; + gap: var(--sp-s); } .open-button { display: flex; justify-content: center; align-items: center; - width: deprecated.$s-68; - min-width: deprecated.$s-68; - height: deprecated.$s-32; + inline-size: px2rem(68); + min-inline-size: px2rem(68); + block-size: $sz-32; text-transform: uppercase; } .plugins-list { - padding-top: deprecated.$s-20; + padding-block-start: var(--sp-xl); overflow: hidden auto; flex: 1; display: flex; flex-direction: column; - gap: deprecated.$s-12; + gap: var(--sp-m); } .plugins-list-element { display: flex; - gap: deprecated.$s-12; + gap: var(--sp-m); } .plugin-icon { - min-width: deprecated.$s-32; - min-height: deprecated.$s-32; - width: deprecated.$s-32; - height: deprecated.$s-32; - background: var(--button-secondary-background-color-rest); - padding: deprecated.$s-2; - border-radius: deprecated.$s-4; + min-inline-size: $sz-32; + min-block-size: $sz-32; + inline-size: $sz-32; + block-size: $sz-32; + background: var(--color-background-tertiary); + padding: var(--sp-xxs); + border-radius: $br-4; } .plugin-description { display: flex; flex-direction: column; - gap: deprecated.$s-8; - width: 100%; - min-width: 0; + gap: var(--sp-s); + inline-size: 100%; + min-inline-size: 0; } .plugin-title { - @include deprecated.body-medium-typography; + @include use-typography("body-medium"); @include text-ellipsis; color: var(--color-foreground-primary); } .plugin-summary { - @include deprecated.body-small-typography; + @include use-typography("body-small"); @include two-line-text-ellipsis; overflow-wrap: anywhere; @@ -184,116 +168,111 @@ display: flex; flex-direction: column; align-items: center; - gap: deprecated.$s-20; - height: 100%; + gap: var(--sp-xl); + block-size: 100%; justify-content: center; - padding: deprecated.$s-36 0; + padding: px2rem(36) 0; } .plugins-empty-logo { - width: deprecated.$s-44; - height: deprecated.$s-44; - border-radius: 50%; + inline-size: px2rem(44); + block-size: px2rem(44); + border-radius: $br-circle; display: flex; justify-content: center; align-items: center; background: var(--color-background-tertiary); - - svg { - width: deprecated.$s-16; - height: deprecated.$s-16; - stroke: var(--color-foreground-secondary); - fill: none; - } + color: var(--color-foreground-secondary); } .plugins-empty-text { - @include deprecated.body-small-typography; + @include use-typography("body-small"); color: var(--color-foreground-primary); } div.input-error { - border: deprecated.$s-1 solid var(--input-border-color-error); + border: $b-1 solid var(--status-color-error-500); } .info { - @include deprecated.body-small-typography; + @include use-typography("body-small"); - margin-top: deprecated.$s-4; + margin-block-start: var(--sp-xs); + color: var(--info-foreground-color); &.error { - color: var(--input-border-color-error); + --info-foreground-color: var(--status-color-error-500); } &.success { - color: var(--input-border-color-success); + --info-foreground-color: var(--color-accent-primary); } } .plugins-link { + // TODO: No exact DS typography match found. + // Closest match: body-small. + font-size: px2rem(12); color: var(--color-accent-primary); - font-size: deprecated.$fs-12; display: inline-flex; align-items: center; - gap: deprecated.$s-4; + gap: var(--sp-xs); +} - svg { - margin-top: calc(-1 * deprecated.$s-2); - width: deprecated.$s-12; - height: deprecated.$s-12; - stroke: var(--color-accent-primary); - fill: none; - } +.plugins-link-icon { + margin-block-start: calc(-1 * var(--sp-xxs)); } .permissions-list { display: flex; flex-direction: column; - gap: deprecated.$s-24; + gap: var(--sp-xxl); } .permissions-list-entry { display: grid; - grid-template-columns: 24px 1fr; - gap: deprecated.$s-16; + grid-template-columns: $sz-24 1fr; + gap: var(--sp-l); align-items: center; + // The permission icons are legacy brand art with no class. svg { - width: deprecated.$s-24; - height: deprecated.$s-24; + inline-size: $sz-24; + block-size: $sz-24; stroke: var(--color-accent-primary); fill: none; } } .permissions-list-text { - @include deprecated.body-small-typography; + @include use-typography("body-small"); margin: 0; color: var(--color-foreground-secondary); } .permissions-disclaimer { - @include deprecated.body-small-typography; + @include use-typography("body-small"); - padding: deprecated.$s-16; + padding: var(--sp-l); background: var(--color-background-quaternary); color: var(--color-foreground-primary); - border-radius: deprecated.$br-4; + border-radius: $br-4; } .action-buttons { display: flex; - gap: deprecated.$s-12; + gap: var(--sp-m); } .discover { - @include deprecated.body-small-typography; + @include use-typography("body-small"); color: var(--color-foreground-secondary); - margin-top: deprecated.$s-24; + margin-block-start: var(--sp-xxl); + // The link comes from a translated HTML string, with no class. a { color: var(--color-accent-primary); }