♻️ Move the profile page to the DS and make the Update overlay a button (#12651)

* ♻️ Make the profile photo Update overlay a button

The "Update" overlay on the profile photo was a span with an on-click,
so the keyboard could not reach it. It is now a button: it shows on
keyboard focus as well as on hover.

The button centers its line in the circle, so a 4px bottom padding keeps
the label where the span drew it (0px difference).

New ds visual test "Profile page controls": the page at rest, the
overlay on hover and on focus, Save enabled, and the Change email link
on hover.

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

* ♻️ Move profile page styles off the deprecated styles

profile.scss imported the whole deprecated module and kept a long copy
of shared settings rules. It now uses the DS tokens, sizes, borders,
z-index and typography. The deprecated import stays only for
%button-primary: the save button is the shared forms submit button.

Deleted selectors that match nothing in profile.cljs: .btn-secondary
(both), .field, .field-title, .field-text, .custom-input,
.custom-select, .two-columns, .subtitle, .notification-icon,
.notification-text, .notification-text-email, .newsletter-subs, the h1,
h3, p and hr rules, the form rule (both forms set their own flex) and the
link hover underline (the global link rule already does it).

Typographies: the page title uses title-large (line-height 1.4 instead
of 1.15), the Change email link title-small (line-height 1.2 instead of
the default), and the Update overlay title-large with its own
line-height.

Visible change: the taller title line moves the title 3px down and the
rest of the page 6px down. Nothing else changes.

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

* ♻️ Give the profile page links, title and photo classes

The links and the profile photo get their own classes instead of bare
a and img selectors, and the page title uses the DS heading with
title-large (it already used that typography). The link wrapper drops
a class with no styles, and the rule for the file input goes: the
shared uploader hides that input inline, so the rule did nothing. No
visual change.

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

* 💄 Use one DS typography and the accent color for the profile links

"Change email" took title-small from its container, overridden by the
global link styles (14px, weight 500), and "Want to remove your account?"
had no typography and inherited 16px. Both links now use body-medium and
--color-accent-primary, as design asked in the review.

The "Change email" line is taller with the 1.4 line height, so Save
settings and the link below it sit 3px lower.

AI-assisted-by: claude-opus-5-5
This commit is contained in:
Eva Marco 2026-10-29 12:09:56 +01:00 committed by GitHub
parent c0f0b14540
commit 2607ec89fa
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 168 additions and 336 deletions

View File

@ -833,6 +833,42 @@ test.describe("Onboarding questions controls", () => {
});
});
test.describe("Profile page controls", () => {
const shot = { maxDiffPixelRatio: 0, threshold: 0 };
test("Profile page avatar, form and links", async ({ page }) => {
const dashboardPage = new DashboardPage(page);
await dashboardPage.goToDashboard();
await dashboardPage.goToAccount();
const container = page.locator(
"[class*='settings_profile__form-container']",
);
await expect(page.getByText("Change email")).toBeVisible();
await page.mouse.move(10, 700);
await expect(container).toHaveScreenshot(shot);
// The "Update" overlay shows when the pointer is on the avatar.
await page.locator("[class*='image-change-field']").hover();
await expect(container).toHaveScreenshot(shot);
// Save turns enabled once the name changes.
await page.getByLabel("Your name").fill("Renamed user");
await page.mouse.move(10, 700);
await expect(container).toHaveScreenshot(shot);
await page.getByText("Change email").hover();
await expect(container).toHaveScreenshot(shot);
// The "Update" overlay is a button: it shows on keyboard focus too.
await page.mouse.move(10, 700);
await page.getByRole("button", { name: "Update" }).focus();
await page.keyboard.press("Shift+Tab");
await page.keyboard.press("Tab");
await expect(container).toHaveScreenshot(shot);
});
});
test.describe("Templates section controls", () => {
const shot = { maxDiffPixelRatio: 0, threshold: 0 };

View File

@ -17,6 +17,8 @@
[app.main.ui.components.file-uploader :refer [file-uploader]]
[app.main.ui.components.forms :as fm]
[app.main.ui.ds.foundations.assets.icon :as i :refer [icon*]]
[app.main.ui.ds.foundations.typography :as t]
[app.main.ui.ds.foundations.typography.heading :refer [heading*]]
[app.util.dom :as dom]
[app.util.i18n :as i18n :refer [tr]]
[rumext.v2 :as mf]))
@ -71,7 +73,8 @@
[:div {:class (stl/css :options)}
[:div.change-email
[:a {:on-click on-show-change-email}
[:a {:class (stl/css :link)
:on-click on-show-change-email}
(tr "dashboard.change-email")]]]]
[:> fm/submit-button*
@ -80,8 +83,9 @@
:class (stl/css :btn-primary)}]
[:div {:class (stl/css :links)}
[:div {:class (stl/css :link-item)}
[:a {:on-click on-show-delete-account
[:div
[:a {:class (stl/css :link)
:on-click on-show-delete-account
:data-testid "remove-acount-btn"}
(tr "dashboard.remove-account")]]]]))
@ -119,9 +123,12 @@
[:form {:class (stl/css :avatar-form)}
[:div {:class (stl/css :image-change-field)}
[:span {:class (stl/css :update-overlay)
:on-click on-image-click} (tr "labels.update")]
[:img {:src photo}]
[:button {:type "button"
:class (stl/css :update-overlay)
:on-click on-image-click}
(tr "labels.update")]
[:img {:class (stl/css :avatar-image)
:src photo}]
(when has-photo?
[:button {:type "button"
:class (stl/css :delete-overlay)
@ -146,6 +153,10 @@
[:section {:class (stl/css :dashboard-settings)
:aria-labelledby "profile-section-title"}
[:div {:class (stl/css :form-container)}
[:h2 {:id "profile-section-title"} (tr "labels.profile")]
[:> heading* {:level 2
:typography t/title-large
:id "profile-section-title"
:class (stl/css :page-title)}
(tr "labels.profile")]
[:> profile-photo-form*]
[:> profile-form*]]])

View File

@ -4,316 +4,83 @@
//
// Copyright (c) KALEIDOS SUBSIDIARY SL
@use "common/refactor/common-refactor" as *;
// Only for %button-primary: the save button is the shared forms submit
// button, which moves to the DS with the forms component.
@use "refactor/common-refactor.scss" as deprecated;
@use "ds/_borders.scss" as *;
@use "ds/_sizes.scss" as *;
@use "ds/_utils.scss" as *;
@use "ds/typography.scss" as *;
.dashboard-settings {
display: flex;
width: 100%;
inline-size: 100%;
justify-content: center;
align-items: center;
}
a:not(.button-primary, .link) {
color: var(--color-foreground-secondary);
}
.link {
@include use-typography("body-medium");
color: var(--color-accent-primary);
}
.form-container {
display: flex;
flex-direction: column;
max-width: $s-500;
width: $s-580;
margin: $s-80 auto $s-120 auto;
max-inline-size: $sz-500;
inline-size: px2rem(580);
margin-block: px2rem(80) px2rem(120);
margin-inline: auto;
justify-content: center;
form {
display: flex;
flex-direction: column;
.btn-secondary {
width: 100%;
font-size: $fs-11;
text-transform: uppercase;
background-color: var(--color-background-tertiary);
color: var(--color-foreground-primary);
&:hover {
color: var(--color-accent-primary);
background-color: var(--color-background-quaternary);
}
}
hr {
display: none;
}
}
.fields-row {
--input-height: #{$s-40};
margin-bottom: $s-20;
flex-direction: column;
.options {
display: flex;
justify-content: flex-end;
font-size: $fs-14;
margin-top: $s-12;
}
}
.field {
margin-bottom: $s-20;
}
.field-title {
color: var(--color-foreground-primary);
&:not(:first-child) {
margin-top: $s-64;
}
}
.field-text {
color: var(--color-foreground-secondary);
}
.custom-input,
.custom-select {
flex-direction: column-reverse;
label {
position: relative;
text-transform: uppercase;
color: var(--color-foreground-primary);
font-size: $fs-11;
margin-bottom: $s-12;
margin-left: calc(-1 * $s-4);
}
input,
select {
background-color: var(--color-background-tertiary);
border-radius: $br-8;
border-color: transparent;
color: var(--color-foreground-primary);
padding: 0 $s-16;
&:focus {
outline: $s-1 solid var(--color-accent-primary);
}
::placeholder {
color: var(--color-foreground-secondary);
}
}
.help-icon {
bottom: $s-12;
top: auto;
svg {
fill: var(--color-foreground-secondary);
}
}
&.disabled {
input {
background-color: var(--input-background-color-disabled);
border-color: var(--color-background-quaternary);
color: var(--color-foreground-secondary);
}
}
.input-container {
background-color: var(--color-background-tertiary);
border-radius: $br-8;
border-color: transparent;
margin-top: $s-24;
.main-content {
label {
position: absolute;
top: calc(-1 * $s-24);
}
span {
color: var(--color-foreground-primary);
}
}
&:focus {
border: $s-1 solid var(--color-accent-primary);
}
}
textarea {
border-radius: $br-8;
padding: $s-12 $s-16;
background-color: var(--color-background-tertiary);
color: var(--color-foreground-primary);
border: none;
&:focus {
outline: $s-1 solid var(--color-accent-primary);
}
}
}
&.two-columns {
max-width: $s-520;
justify-content: space-between;
flex-direction: row;
}
h1 {
font-size: $fs-36;
color: var(--color-background-tertiary);
margin-bottom: $s-20;
}
.subtitle {
font-size: $fs-24;
color: var(--color-background-tertiary);
margin-bottom: $s-20;
}
.notification-icon {
justify-content: center;
display: flex;
margin-bottom: $s-48;
svg {
fill: var(--color-background-primary);
height: 40%;
width: 40%;
}
}
.notification-text {
font-size: $fs-16;
color: var(--color-background-primary);
margin-bottom: $s-20;
}
.notification-text-email {
background: var(--color-foreground-primary);
border-radius: $br-4;
color: var(--color-background-primary);
font-size: $fs-16;
font-weight: $fw500;
margin: $s-24 0 $s-40 0;
padding: $s-16;
text-align: center;
}
h2 {
font-size: $fs-24;
font-weight: $fw400;
color: var(--color-foreground-primary);
display: flex;
align-items: center;
margin: $s-16 0;
}
h3 {
font-size: $fs-12;
font-weight: $fw400;
color: var(--color-foreground-primary);
display: flex;
align-items: center;
margin: $s-8 0;
text-transform: uppercase;
}
a:not(.button-primary) {
&:hover {
text-decoration: underline;
}
}
p {
color: var(--color-background-primary);
}
hr {
border-color: var(--color-foreground-secondary);
}
.links {
margin-top: $s-12;
}
}
form.avatar-form {
.page-title {
color: var(--color-foreground-primary);
display: flex;
align-items: center;
margin-block: var(--sp-l);
margin-inline: 0;
}
.fields-row {
--input-height: #{$sz-40};
margin-block-end: var(--sp-xl);
flex-direction: column;
}
.options {
display: flex;
justify-content: flex-end;
margin-block-start: var(--sp-m);
}
.links {
margin-block-start: var(--sp-m);
}
.avatar-form {
display: flex;
flex-direction: column;
width: $s-148;
height: $s-148;
margin: $s-16 0;
inline-size: px2rem(148);
block-size: px2rem(148);
margin-block: var(--sp-l);
margin-inline: 0;
}
img {
border-radius: 50%;
flex-shrink: 0;
height: 100%;
margin-right: $s-16;
width: 100%;
}
.avatar-image {
border-radius: $br-circle;
flex-shrink: 0;
block-size: 100%;
margin-inline-end: var(--sp-l);
inline-size: 100%;
}
.image-change-field {
position: relative;
width: 100%;
height: 100%;
.update-overlay {
opacity: 0;
cursor: pointer;
position: absolute;
width: 100%;
height: 100%;
border-radius: 50%;
font-size: $fs-24;
color: var(--color-foreground-primary);
line-height: 6;
text-align: center;
background: var(--color-accent-tertiary);
z-index: $z-index-modal;
}
.delete-overlay {
position: absolute;
top: $s-4;
inset-inline-end: $s-4;
display: flex;
align-items: center;
justify-content: center;
width: $s-32;
height: $s-32;
padding: 0;
border: none;
border-radius: 50%;
background: var(--color-background-primary);
color: var(--color-foreground-primary);
cursor: pointer;
opacity: 0;
transition: opacity 0.15s ease-in-out;
z-index: calc(#{$z-index-modal} + 1);
&:hover {
background: var(--color-background-quaternary);
color: var(--color-accent-primary);
}
}
input[type="file"] {
width: 100%;
height: 100%;
position: absolute;
opacity: 0;
cursor: pointer;
top: 0;
z-index: $z-index-modal;
}
inline-size: 100%;
block-size: 100%;
&:hover {
.update-overlay {
@ -326,52 +93,70 @@ form.avatar-form {
}
}
.update-overlay {
@include use-typography("title-large");
opacity: 0;
cursor: pointer;
position: absolute;
inline-size: 100%;
block-size: 100%;
border: none;
border-radius: $br-circle;
color: var(--color-foreground-primary);
// Draws the label in the middle of the circle.
line-height: 6;
text-align: center;
background: var(--color-accent-tertiary);
// The button centers its 144px line in the 148px circle; the bottom
// padding keeps the label where the old span drew it.
padding: 0 0 var(--sp-xs);
z-index: var(--z-index-set);
&:focus-visible {
opacity: 0.8;
}
}
.delete-overlay {
--delete-overlay-background-color: var(--color-background-primary);
--delete-overlay-foreground-color: var(--color-foreground-primary);
position: absolute;
inset-block-start: var(--sp-xs);
inset-inline-end: var(--sp-xs);
display: flex;
align-items: center;
justify-content: center;
inline-size: $sz-32;
block-size: $sz-32;
padding: 0;
border: none;
border-radius: $br-circle;
background: var(--delete-overlay-background-color);
color: var(--delete-overlay-foreground-color);
cursor: pointer;
opacity: 0;
transition: opacity 0.15s ease-in-out;
z-index: calc(var(--z-index-set) + 1);
&:hover {
--delete-overlay-background-color: var(--color-background-quaternary);
--delete-overlay-foreground-color: var(--color-accent-primary);
}
}
.profile-form {
display: flex;
flex-direction: column;
max-width: $s-368;
width: 100%;
}
.newsletter-subs {
border-bottom: $s-1 solid var(--color-foreground-secondary);
border-top: $s-1 solid var(--color-foreground-secondary);
padding: $s-32 0;
margin-bottom: $s-32;
.newsletter-title {
font-family: "worksans", "vazirmatn", sans-serif;
color: var(--color-foreground-secondary);
font-size: $fs-14;
}
label {
font-family: "worksans", "vazirmatn", sans-serif;
color: var(--color-background-primary);
font-size: $fs-12;
margin-right: calc(-1 * $s-16);
margin-bottom: $s-12;
}
.info {
color: var(--color-foreground-secondary);
font-size: $fs-12;
margin-bottom: $s-8;
}
.input-checkbox label {
align-items: flex-start;
}
}
.btn-secondary {
@extend %button-secondary;
height: $s-32;
max-inline-size: px2rem(368);
inline-size: 100%;
}
.btn-primary {
@extend %button-primary;
height: $s-32;
block-size: $sz-32;
}