♻️ Use DS buttons and icons in the dashboard fonts page (#12533)

* ♻️ Use DS buttons and icons in the dashboard fonts page

The fonts page moves off the deprecated icons and raw buttons:

- "Add custom font", "Upload all", "Upload" and "Save" become DS primary
  buttons and "Dismiss all" a secondary one.
- The remove, cancel and menu controls of the font rows become ghost DS
  icon buttons named "Remove", "Cancel" and "Options"; the menu one
  says whether its menu is open. Two of them were spans.
- The delete control of a font style was a span; it is now a button
  named "Delete font style" with the DS close icon, still shown only
  on hover.
- The warning and empty list icons use the DS icons. The loading
  placeholder keeps the legacy loader: there is no DS one.

The font menu offset grows by the 6px the taller menu button added, so
the menu opens where it did. New key: dashboard.fonts.delete-variant
(en, es). A visual test covers a font ready to upload, the style hover,
the font menu and the edition row.

AI-assisted-by: claude-opus-5-5

* ♻️ Move dashboard fonts styles to logical properties

Physical sizes, offsets, margins and paddings become logical properties
and the DS typography module is used without a namespace. The element
selectors (section title, inputs, hero text) stay, with a comment: they
have no class.

The shared dashboard header styles (common-dashboard) still come from
refactor/: ten dashboard modules use them, so they move in their own
change. No visual change.

AI-assisted-by: claude-opus-5-5

* ♻️ Use the DS loader and heading in the dashboard fonts page

The fonts loading placeholder used the legacy loader icon, drawn as an
outline in a 32px box. It now uses the DS loader*, the same animated
pencil as the other dashboard loading screens. Its box is 9px tall,
so the label moves 6px up.

The section title h3 is now a heading* with the title-small typography
and its own class, so the SCSS no longer styles the bare element.

The legacy loader icon has no other use, so its SVG and def are gone.

AI-assisted-by: claude-opus-5-5

* ♻️ Give the dashboard fonts inputs and hero text classes

The font name and search inputs and the hero text get their own
classes instead of bare input and p selectors, and the hero title uses
the DS heading with title-large instead of the global h2 rule. The
title keeps its 24px size and weight; the DS line height (1.4 instead
of 1.15) moves the hero content about 6px down.

AI-assisted-by: claude-opus-5-5

* 🐛 Give the fonts placeholder icon its color back

Removing the legacy loader also removed the .icon svg rule, which gave
the empty placeholder text icon its color, so the icon had none. The
icon now has its own class with the secondary foreground color, and the
wrapper div without styles goes.

AI-assisted-by: claude-opus-5-5
This commit is contained in:
Eva Marco 2026-10-28 16:11:52 +01:00 committed by GitHub
parent 66c67660f9
commit 8951c57ee6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 265 additions and 193 deletions

View File

@ -338,6 +338,74 @@ test.describe("Delete account modal", () => {
});
});
test.describe("Fonts page controls", () => {
const shot = { maxDiffPixelRatio: 0, threshold: 0 };
test("Installed font variant, menu and edition", async ({ page }) => {
const dashboardPage = new DashboardPage(page);
await dashboardPage.setupDashboardFull();
await dashboardPage.goToDashboard();
await dashboardPage.fontsLink.click();
await expect(dashboardPage.mainHeading).toHaveText("Fonts");
const installed = page.locator("[class*='dashboard-installed-fonts']");
const row = installed.locator("[class*='font-item']").first();
await expect(row).toBeVisible();
// The variant delete icon only shows on hover.
await row.locator("[class$='__variant'], [class*='__variant ']").hover();
await expect(installed).toHaveScreenshot(shot);
// The menu button: a span before the DS migration, a DS icon button
// named "Options" after it.
const options = row.locator("[class*='options']");
await options
.locator("span[class*='icon']")
.or(options.getByRole("button", { name: "Options" }))
.first()
.click();
await expect(page.getByText("Download")).toBeVisible();
await expect(page).toHaveScreenshot(shot);
await page.getByText("Edit", { exact: true }).click();
await expect(row.locator("input")).toBeVisible();
await page.mouse.move(10, 700);
await expect(installed).toHaveScreenshot(shot);
});
test("Fonts ready to upload", async ({ page }) => {
const dashboardPage = new DashboardPage(page);
await dashboardPage.setupDashboardFull();
await dashboardPage.goToDashboard();
await dashboardPage.fontsLink.click();
await expect(dashboardPage.mainHeading).toHaveText("Fonts");
await page
.locator("#font-upload")
.setInputFiles("playwright/data/render-wasm/assets/ebgaramond.ttf");
await expect(page.getByTestId("upload-all")).toBeVisible();
await page.mouse.move(10, 700);
await expect(
page.locator("[class*='dashboard-fonts-upload']"),
).toHaveScreenshot(shot);
});
test("Fonts loading placeholder", async ({ page }) => {
const dashboardPage = new DashboardPage(page);
await dashboardPage.setupDashboardFull();
// Never answer, so the page keeps showing the loader.
await page.route(/get-font-variants/, () => {});
await dashboardPage.goToDashboard();
await dashboardPage.fontsLink.click();
await expect(dashboardPage.mainHeading).toHaveText("Fonts");
const placeholder = page.locator("[class*='fonts-placeholder']");
await expect(placeholder).toBeVisible();
await page.mouse.move(10, 700);
await expect(placeholder).toHaveScreenshot(shot);
});
});
test.describe("Subscription controls", () => {
const shot = { maxDiffPixelRatio: 0, threshold: 0 };

View File

@ -1 +0,0 @@
<svg height="182" viewBox="0 0 677.34762 182.15429" width="667" xmlns="http://www.w3.org/2000/svg"><path d="m128.273 0-3.9 2.77-124.373 88.308 128.273 91.076 549.075-.006v-182.14zm20.852 30 498.223.006v122.144l-498.223.007zm-25 9.74v102.678l-49.033-34.813-.578-32.64 49.61-35.225z"/><path d="m134.482 157.147v25l518.57.008.002-25z"/></svg>

Before

Width:  |  Height:  |  Size: 339 B

View File

@ -21,8 +21,13 @@
[app.main.store :as st]
[app.main.ui.components.context-menu-a11y :refer [context-menu*]]
[app.main.ui.components.file-uploader :refer [file-uploader]]
[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.ds.foundations.typography :as t]
[app.main.ui.ds.foundations.typography.heading :refer [heading*]]
[app.main.ui.ds.product.empty-placeholder :refer [empty-placeholder*]]
[app.main.ui.icons :as deprecated-icon]
[app.main.ui.ds.product.loader :refer [loader*]]
[app.main.ui.notifications.context-notification :refer [context-notification]]
[app.util.dom :as dom]
[app.util.http :as http]
@ -195,13 +200,18 @@
[:div {:class (stl/css :dashboard-fonts-upload)}
[:div {:class (stl/css :dashboard-fonts-hero)}
[:div {:class (stl/css :desc)}
[:h2 (tr "labels.upload-custom-fonts")]
[:> i18n/tr-html* {:content (tr "dashboard.fonts.hero-text1")}]
[:> heading* {:level 2
:typography t/title-large
:class (stl/css :hero-title)}
(tr "labels.upload-custom-fonts")]
[:> i18n/tr-html* {:content (tr "dashboard.fonts.hero-text1")
:class (stl/css :hero-text)}]
[:button {:class (stl/css :btn-primary)
:on-click on-click
:tab-index "0"}
[:span (tr "labels.add-custom-font")]
[:> button* {:variant "primary"
:type "button"
:class (stl/css :add-font-button)
:on-click on-click}
(tr "labels.add-custom-font")
[:& file-uploader {:input-id "font-upload"
:accept accept-font-types
:multi true
@ -223,17 +233,19 @@
[:div {:class (stl/css :font-item :table-row)}
[:span (tr "dashboard.fonts.fonts-added" (i18n/c (count fonts)))]
[:div {:class (stl/css :table-field :options)}
[:button {:class (stl/css-case
:btn-primary true
:disabled disable-upload-all?)
:on-click on-upload-all
:data-testid "upload-all"
:disabled disable-upload-all?}
[:span (tr "dashboard.fonts.upload-all")]]
[:button {:class (stl/css :btn-secondary)
:on-click on-dismis-all
:data-testid "dismiss-all"}
[:span (tr "dashboard.fonts.dismiss-all")]]]])
[:> button* {:variant "primary"
:type "button"
:class (stl/css :upload-button)
:on-click on-upload-all
:data-testid "upload-all"
:disabled disable-upload-all?}
(tr "dashboard.fonts.upload-all")]
[:> button* {:variant "secondary"
:type "button"
:class (stl/css :dismiss-button)
:on-click on-dismis-all
:data-testid "dismiss-all"}
(tr "dashboard.fonts.dismiss-all")]]])
(for [{:keys [id] :as item} (sort-by :font-family font-vals)]
(let [uploading? (contains? uploading id)
@ -242,6 +254,7 @@
:key (dm/str id)}
[:div {:class (stl/css :table-field :family)}
[:input {:type "text"
:class (stl/css :name-input)
:data-id (dm/str id)
:on-blur on-blur-name
:on-change on-change-name
@ -256,23 +269,23 @@
[:div {:class (stl/css :table-field :options)}
(when (:height-warning? item)
[:span {:class (stl/css :icon :failure)}
deprecated-icon/msg-neutral])
[:> icon* {:icon-id i/msg-neutral
:class (stl/css :warning-icon)}])
[:button {:on-click on-upload
:data-id (dm/str id)
:class (stl/css-case
:btn-primary true
:upload-button true
:disabled disable-upload?)
:disabled disable-upload?}
[:> button* {:variant "primary"
:type "button"
:on-click on-upload
:data-id (dm/str id)
:class (stl/css :upload-button)
:disabled disable-upload?}
(if ^boolean uploading?
(tr "labels.uploading")
(tr "labels.upload"))]
[:span {:class (stl/css :icon :close)
:data-id (dm/str id)
:on-click on-delete}
deprecated-icon/close]]]))]]))
[:> icon-button* {:variant "ghost"
:aria-label (tr "labels.remove")
:icon i/close
:data-id (dm/str id)
:on-click on-delete}]]]))]]))
(mf/defc installed-font-context-menu*
{::mf/private true}
@ -292,7 +305,7 @@
:show is-open
:fixed false
:min-width true
:top -15
:top -21
:left -115
:options options}]))
@ -406,6 +419,7 @@
:is-edition edition?)}
(if ^boolean edition?
[:input {:type "text"
:class (stl/css :name-input)
:auto-focus true
:default-value font-family
:on-key-down on-key-down
@ -420,29 +434,37 @@
[:span {:class (stl/css :label)}
[:> font-variant-display-name* {:variant item}]]
(when can-edit
[:span
{:class (stl/css :icon :close)
:data-id (dm/str id)
:on-click on-delete-variant}
deprecated-icon/add])])]
[:button {:type "button"
:class (stl/css :variant-delete)
:aria-label (tr "dashboard.fonts.delete-variant")
:title (tr "dashboard.fonts.delete-variant")
:data-id (dm/str id)
:on-click on-delete-variant}
[:> icon* {:icon-id i/close
:size "s"
:aria-hidden true}]])])]
(if ^boolean edition?
[:div {:class (stl/css :table-field :options)}
[:button
{:disabled (str/blank? font-family)
:on-click on-save
:class (stl/css-case :btn-primary true
:btn-disabled (str/blank? font-family))}
[:> button* {:variant "primary"
:type "button"
:class (stl/css :upload-button)
:disabled (str/blank? font-family)
:on-click on-save}
(tr "labels.save")]
[:button {:class (stl/css :icon :close)
:on-click on-cancel}
deprecated-icon/close]]
[:> icon-button* {:variant "ghost"
:aria-label (tr "labels.cancel")
:icon i/close
:on-click on-cancel}]]
(when can-edit
[:div {:class (stl/css :table-field :options)}
[:span {:class (stl/css :icon)
:on-click on-menu-open}
deprecated-icon/menu]
[:> icon-button* {:variant "ghost"
:aria-label (tr "labels.options")
:aria-haspopup "menu"
:aria-expanded menu-open?
:icon i/menu
:on-click on-menu-open}]
[:> installed-font-context-menu*
{:on-close on-menu-close
@ -468,12 +490,16 @@
(cond
(seq fonts)
[:*
[:h3 (tr "labels.installed-fonts")]
[:> heading* {:level 3
:typography t/title-small
:class (stl/css :installed-fonts-title)}
(tr "labels.installed-fonts")]
[:div {:class (stl/css :installed-fonts-header)}
[:div {:class (stl/css :table-field :family)} (tr "labels.font-family")]
[:div {:class (stl/css :table-field :variants)} (tr "labels.font-variants")]
[:div {:class (stl/css :table-field :search-input)}
[:input {:placeholder (tr "labels.search-font")
[:input {:class (stl/css :search-font-input)
:placeholder (tr "labels.search-font")
:default-value ""
:on-change on-change}]]]
(for [[font-id variants] (->> (vals fonts)
@ -486,13 +512,16 @@
(nil? fonts)
[:div {:class (stl/css :fonts-placeholder)}
[:div {:class (stl/css :icon)} deprecated-icon/loader]
[:> loader* {:width 32
:title (tr "labels.loading")}]
[:div {:class (stl/css :label)} (tr "dashboard.loading-fonts")]]
:else
(if ^boolean can-edit
[:div {:class (stl/css :fonts-placeholder)}
[:div {:class (stl/css :icon)} deprecated-icon/text]
[:> icon* {:icon-id i/text
:size "xl"
:class (stl/css :placeholder-icon)}]
[:div {:class (stl/css :label)} (tr "dashboard.fonts.empty-placeholder")]]
[:> empty-placeholder*

View File

@ -4,66 +4,60 @@
//
// Copyright (c) KALEIDOS SUBSIDIARY SL
// The shared dashboard header styles move in their own change: they are
// used by ten dashboard modules.
@use "common/refactor/common-dashboard";
@use "ds/_utils.scss" as *;
@use "ds/_sizes.scss" as *;
@use "ds/_borders.scss" as *;
@use "ds/typography.scss" as t;
@use "ds/typography.scss" as *;
@use "ds/spacing.scss" as *;
@use "ds/mixins.scss" as *;
.dashboard-fonts {
border-top: $b-1 solid var(--color-background-quaternary);
border-block-start: $b-1 solid var(--color-background-quaternary);
display: flex;
flex-direction: column;
padding-left: px2rem(120);
padding-bottom: px2rem(120);
padding-inline-start: px2rem(120);
padding-block-end: px2rem(120);
overflow-y: auto;
.btn-primary {
font-size: px2rem(11);
height: $sz-32;
min-width: px2rem(100);
}
}
.dashboard-installed-fonts {
max-width: px2rem(1000);
width: 100%;
max-inline-size: px2rem(1000);
inline-size: 100%;
display: flex;
margin-top: var(--sp-xxl);
margin-block-start: var(--sp-xxl);
flex-direction: column;
h3 {
@include t.use-typography("title-small");
color: var(--color-foreground-secondary);
margin: var(--sp-xs);
}
.font-item {
color: var(--color-background-secondary);
}
}
.installed-fonts-title {
color: var(--color-foreground-secondary);
margin: var(--sp-xs);
}
.installed-fonts-header {
@include t.use-typography("headline-small");
@include use-typography("headline-small");
align-items: center;
color: var(--color-foreground-secondary);
display: flex;
height: $sz-40;
padding-left: var(--sp-xxl);
block-size: $sz-40;
padding-inline-start: var(--sp-xxl);
> .family {
@include two-line-text-ellipsis;
min-width: $sz-200;
width: $sz-200;
min-inline-size: $sz-200;
inline-size: $sz-200;
}
> .variants {
padding-left: var(--sp-m);
padding-inline-start: var(--sp-m);
}
}
@ -71,31 +65,31 @@
display: flex;
flex-grow: 1;
justify-content: flex-end;
}
input {
@include t.use-typography("body-medium");
.search-font-input {
@include use-typography("body-medium");
background-color: var(--color-background-tertiary);
border-radius: $br-8;
border: $b-1 solid transparent;
color: var(--color-foreground-primary);
height: $sz-32;
margin: 0;
padding: 0 var(--sp-s);
width: px2rem(152);
background-color: var(--color-background-tertiary);
border-radius: $br-8;
border: $b-1 solid transparent;
color: var(--color-foreground-primary);
block-size: $sz-32;
margin: 0;
padding: 0 var(--sp-s);
inline-size: px2rem(152);
&:focus {
outline: $b-1 solid var(--color-accent-primary);
}
&:focus {
outline: $b-1 solid var(--color-accent-primary);
}
&::placeholder {
color: var(--color-foreground-secondary);
}
&::placeholder {
color: var(--color-foreground-secondary);
}
}
.font-item {
@include t.use-typography("body-medium");
@include use-typography("body-medium");
align-items: center;
background-color: var(--color-background-tertiary);
@ -103,13 +97,13 @@
color: var(--color-foreground-secondary);
display: flex;
justify-content: space-between;
margin-top: var(--sp-xs);
max-width: px2rem(1000);
margin-block-start: var(--sp-xs);
max-inline-size: px2rem(1000);
padding: var(--sp-m) var(--sp-xxl);
width: 100%;
inline-size: 100%;
input {
@include t.use-typography("body-medium");
.name-input {
@include use-typography("body-medium");
@include text-ellipsis;
border: $b-1 solid transparent;
@ -129,8 +123,8 @@
> .family {
@include two-line-text-ellipsis;
min-width: $sz-200;
width: $sz-200;
min-inline-size: $sz-200;
inline-size: $sz-200;
&.is-edition {
overflow: visible;
@ -140,16 +134,16 @@
> .filenames {
@include text-ellipsis;
min-width: $sz-200;
min-inline-size: $sz-200;
}
> .variants {
@include t.use-typography("body-medium");
@include use-typography("body-medium");
display: flex;
flex-wrap: wrap;
flex-grow: 1;
padding-left: var(--sp-l);
padding-inline-start: var(--sp-l);
gap: $sz-6;
.variant {
@ -160,25 +154,11 @@
cursor: pointer;
gap: var(--sp-xs);
.icon {
display: flex;
align-items: center;
justify-content: center;
height: $sz-16;
width: $sz-16;
svg {
fill: none;
width: $sz-12;
height: $sz-12;
transform: rotate(45deg);
}
}
// The delete button only shows its icon while the variant is hovered.
--variant-delete-color: transparent;
&:hover {
.icon svg {
stroke: var(--color-foreground-secondary);
}
--variant-delete-color: var(--color-foreground-secondary);
}
}
@ -197,79 +177,76 @@
}
.filenames {
@include t.use-typography("body-small");
@include use-typography("body-small");
@include text-ellipsis;
min-width: $sz-400;
padding-left: var(--sp-xxxl);
min-inline-size: $sz-400;
padding-inline-start: var(--sp-xxxl);
}
.options {
display: flex;
justify-content: flex-end;
min-width: px2rem(180);
.icon {
width: $sz-24;
cursor: pointer;
display: flex;
margin-left: var(--sp-m);
justify-content: center;
align-items: center;
svg {
width: $sz-16;
height: $sz-16;
stroke: var(--color-foreground-secondary);
fill: none;
}
&.failure {
margin-right: var(--sp-m);
svg {
stroke: var(--element-foreground-warning);
}
}
&.close {
background: none;
border: none;
svg {
stroke: var(--color-foreground-secondary);
}
}
}
min-inline-size: px2rem(180);
align-items: center;
gap: var(--sp-s);
}
}
.variant-delete {
display: flex;
align-items: center;
justify-content: center;
block-size: $sz-16;
inline-size: $sz-16;
padding: 0;
border: none;
background: none;
cursor: pointer;
color: var(--variant-delete-color);
&:focus-visible {
--variant-delete-color: var(--color-foreground-secondary);
}
}
// With the 8px gap of .options it keeps 12px on each side.
.warning-icon {
margin-inline: var(--sp-xs);
color: var(--color-accent-warning);
}
// A DS button keeps its label at the start when it grows.
.upload-button {
min-inline-size: px2rem(100);
justify-content: center;
}
.dashboard-fonts-upload {
max-width: px2rem(1000);
width: 100%;
max-inline-size: px2rem(1000);
inline-size: 100%;
display: flex;
flex-direction: column;
.upload-button {
width: px2rem(100);
}
.btn-secondary {
margin-left: var(--sp-m);
.dismiss-button {
margin-inline-start: var(--sp-xs);
}
}
.dashboard-fonts-hero {
@include t.use-typography("body-medium");
@include use-typography("body-medium");
padding: var(--sp-xxxl) 0;
margin-top: px2rem(80);
margin-block-start: px2rem(80);
display: flex;
justify-content: space-between;
.btn-primary {
height: $sz-40;
width: 100%;
.add-font-button {
--button-height: #{$sz-40};
inline-size: 100%;
flex-shrink: 0;
justify-content: center;
}
.desc {
@ -277,23 +254,18 @@
flex-direction: column;
gap: var(--sp-xxl);
color: var(--color-background-secondary);
width: $sz-500;
inline-size: $sz-500;
h2 {
.hero-title {
color: var(--color-foreground-primary);
font-weight: 400;
}
p {
@include t.use-typography("body-large");
.hero-text {
@include use-typography("body-large");
color: var(--color-foreground-secondary);
}
}
.btn-primary {
flex-shrink: 0;
}
}
.fonts-placeholder {
@ -302,21 +274,18 @@
border: $b-1 solid var(--color-background-quaternary);
display: flex;
flex-direction: column;
height: $sz-160;
block-size: $sz-160;
justify-content: center;
margin-top: var(--sp-l);
max-width: px2rem(1000);
width: 100%;
margin-block-start: var(--sp-l);
max-inline-size: px2rem(1000);
inline-size: 100%;
.icon svg {
stroke: var(--color-foreground-secondary);
fill: none;
width: $sz-32;
height: $sz-32;
.placeholder-icon {
color: var(--color-foreground-secondary);
}
.label {
@include t.use-typography("body-medium");
@include use-typography("body-medium");
color: var(--color-foreground-secondary);
}

View File

@ -13,7 +13,6 @@
;; Keep the list of icons sorted
(def ^:icon icon-verify (icon-xref :icon-verify))
(def ^:icon loader (icon-xref :loader))
(def ^:icon logo (icon-xref :penpot-logo))
(def ^:icon logo-icon (icon-xref :penpot-logo-icon))
(def ^:icon logo-error-screen (icon-xref :logo-error-screen))

View File

@ -579,6 +579,10 @@ msgstr "Grid view"
msgid "dashboard.files-layout.list"
msgstr "List view"
#: src/app/main/ui/dashboard/fonts.cljs:431
msgid "dashboard.fonts.delete-variant"
msgstr "Delete font style"
#: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs:608
msgid "dashboard.fonts.deleted-placeholder"
msgstr "Missing font"

View File

@ -587,6 +587,10 @@ msgstr "Vista de cuadrícula"
msgid "dashboard.files-layout.list"
msgstr "Vista de lista"
#: src/app/main/ui/dashboard/fonts.cljs:431
msgid "dashboard.fonts.delete-variant"
msgstr "Eliminar estilo de fuente"
#: src/app/main/ui/workspace/sidebar/options/menus/typography.cljs:608
msgid "dashboard.fonts.deleted-placeholder"
msgstr "Fuente no encontrada"