diff --git a/frontend/playwright/ui/visual-specs/visual-dashboard.spec.js b/frontend/playwright/ui/visual-specs/visual-dashboard.spec.js index 2e3b7fe082..878a02d347 100644 --- a/frontend/playwright/ui/visual-specs/visual-dashboard.spec.js +++ b/frontend/playwright/ui/visual-specs/visual-dashboard.spec.js @@ -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 }; diff --git a/frontend/resources/images/icons/loader.svg b/frontend/resources/images/icons/loader.svg deleted file mode 100644 index ee1d9b96bf..0000000000 --- a/frontend/resources/images/icons/loader.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/frontend/src/app/main/ui/dashboard/fonts.cljs b/frontend/src/app/main/ui/dashboard/fonts.cljs index 5574f21572..ed27260fe9 100644 --- a/frontend/src/app/main/ui/dashboard/fonts.cljs +++ b/frontend/src/app/main/ui/dashboard/fonts.cljs @@ -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* diff --git a/frontend/src/app/main/ui/dashboard/fonts.scss b/frontend/src/app/main/ui/dashboard/fonts.scss index c8cbcdf808..e2e6430ede 100644 --- a/frontend/src/app/main/ui/dashboard/fonts.scss +++ b/frontend/src/app/main/ui/dashboard/fonts.scss @@ -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); } diff --git a/frontend/src/app/main/ui/icons.cljs b/frontend/src/app/main/ui/icons.cljs index 59708eef1a..4dc9408889 100644 --- a/frontend/src/app/main/ui/icons.cljs +++ b/frontend/src/app/main/ui/icons.cljs @@ -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)) diff --git a/frontend/translations/en.po b/frontend/translations/en.po index 3fa9a763f3..09b305cc19 100644 --- a/frontend/translations/en.po +++ b/frontend/translations/en.po @@ -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" diff --git a/frontend/translations/es.po b/frontend/translations/es.po index 431886f0d1..94ac75a0a6 100644 --- a/frontend/translations/es.po +++ b/frontend/translations/es.po @@ -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"