From 0490a4c4941df082b054284d83ce6aa027f2f451 Mon Sep 17 00:00:00 2001 From: Florian Sylvain Date: Sun, 4 Oct 2026 17:08:43 +0200 Subject: [PATCH] fix: language buttons real design / conformance --- docs/design/components.md | 22 +- docs/design/screens/profile.md | 6 +- docs/design/source/AppFrame.dc.html | 6 +- docs/design/source/AuthCard.dc.html | 9 +- docs/design/source/RenewCMS Prototype.dc.html | 9 +- .../language_switcher_conformance.feature | 172 ++++ features/language_switcher_conformance.qa.md | 35 + features/spa_internationalization.feature | 22 +- features/spa_internationalization.qa.md | 6 +- web/src/components/AccountMenuSheet.vue | 79 +- web/src/components/AuthShell.vue | 14 +- web/src/components/LanguageChip.vue | 63 ++ web/src/components/LanguagePicker.vue | 44 - web/src/components/UserMenu.vue | 98 +- .../__tests__/DashboardShell.spec.ts | 20 +- web/src/components/__tests__/UserMenu.spec.ts | 26 +- .../__tests__/languageSwitcher.spec.ts | 840 ++++++++++++++++++ .../__tests__/loginCardConformance.spec.ts | 43 +- web/src/composables/useLanguage.ts | 15 + web/src/composables/useRadioMenuRoles.ts | 63 ++ web/src/i18n/messages.ts | 2 + web/src/i18n/spa.spec.ts | 219 +---- web/visual/states.ts | 7 + 23 files changed, 1408 insertions(+), 412 deletions(-) create mode 100644 features/language_switcher_conformance.feature create mode 100644 features/language_switcher_conformance.qa.md create mode 100644 web/src/components/LanguageChip.vue delete mode 100644 web/src/components/LanguagePicker.vue create mode 100644 web/src/components/__tests__/languageSwitcher.spec.ts create mode 100644 web/src/composables/useLanguage.ts create mode 100644 web/src/composables/useRadioMenuRoles.ts diff --git a/docs/design/components.md b/docs/design/components.md index 984fb58..8aa9bb2 100644 --- a/docs/design/components.md +++ b/docs/design/components.md @@ -47,7 +47,7 @@ These are the RenewCMS components, built from the primitives on the first page . ### AuthShell -One centered card for every public screen. The logo sits above the title, and the form or ResultPanel goes in the slot. Footer links and the theme select sit outside the card, so the card itself stays about one task. +One centered card for every public screen. The logo sits above the title, and the form or ResultPanel goes in the slot. Footer links, the language chip and the theme select sit outside the card, so the card itself stays about one task. Log in Welcome back. Log in to manage your articles. @@ -55,7 +55,7 @@ default slot Forgot password? System -Built from Custom layout component AuthShell.vue : a UPageCard variant="outline" inside a centered main , with AppLogo , and UColorModeSelect size="sm" in the footer. Forms inside it use UForm rather than UAuthForm , because every screen needs states (such as 403, 429 and an inline login during verification) that UAuthForm doesn't expose. +Built from Custom layout component AuthShell.vue : a UPageCard variant="outline" inside a centered main , with AppLogo , and UColorModeSelect size="sm" in the footer, next to the language chip (see LanguageSwitcher). Forms inside it use UForm rather than UAuthForm , because every screen needs states (such as 403, 429 and an inline login during verification) that UAuthForm doesn't expose. Props, slots Props title: string , description?: string . Slots default , notice (an alert above the title, for session expired or password changed), footer (links). No emits. @@ -383,12 +383,13 @@ The Product components board was not regenerated for these. They are written fro ### UserMenu (changed) -The chip at the foot of the sidebar no longer logs out on click. It opens the account menu, which lists Profile, the theme choice and Log out. +The chip at the foot of the sidebar no longer logs out on click. It opens the account menu, which lists Profile, the theme choice, the language choice and Log out. - **Chip:** 40 px high, an initials avatar (26 px, sand `#d4c5b0` with ink text), the name, and a chevrons-up-down icon. `aria-haspopup="menu"`, `aria-expanded`, title "Account". While the menu is open, or while `/profile` is the current page, it takes `--rc-sand` with `#111111` text. - **Name:** the display name when there is one, otherwise the username. Initials come from the first letters of the first two words of the display name, uppercased. Without a display name they come from the username. -- **Desktop menu:** a 224 px popover anchored above the chip on its left edge, with `--rc-shadow-pop`. Header: the name in semibold, the email below it in muted 12 px, a bottom border. Items are 32 px: Profile (circle-user), a separator, System, Light and Dark (14 px text, a check icon at the right edge on the current choice), a separator, Log out (log-out). The current page's item is highlighted with `--ui-bg-elevated`. The theme items are radio-style: choosing one applies at once and keeps the menu open. Nuxt UI: `type: 'checkbox'` items bound to `useColorMode().preference`. -- **Mobile:** the account row in the navigation drawer opens a bottom sheet over an `--rc-overlay` scrim, with a grab handle. Header: 36 px avatar, name in 15 px semibold, email in muted text. Items are 48 px at 15 px text; the theme items keep the same order and separators. +- **Desktop menu:** a 224 px popover anchored above the chip on its left edge, with `--rc-shadow-pop`. Header: the name in semibold, the email below it in muted 12 px, a bottom border. Items are 32 px: Profile (circle-user), a separator, a "Theme" section label, System, Light and Dark (14 px text, a check icon at the right edge on the current choice), a separator, a "Language" section label, English and Français (same row as the theme items, a check icon on the current one), a separator, Log out (log-out). Section labels are 11 px, weight 500, uppercase, letter-spacing .04em, `--ui-text-muted`, padded 6px 8px 2px. The current page's item is highlighted with `--ui-bg-elevated`. The theme items are radio-style: choosing one applies at once and keeps the menu open. Nuxt UI: `type: 'checkbox'` items bound to `useColorMode().preference`. +- **Mobile:** the account row in the navigation drawer opens a bottom sheet over an `--rc-overlay` scrim, with a grab handle. Header: 36 px avatar, name in 15 px semibold, email in muted text. Items are 48 px at 15 px text; the theme and language items keep the same order, section labels (padded 8px 16px 2px) and separators. +- **Language items:** a `menuitemradio` group named by the "Language" label. Each item is labelled with the language's own name (English, Français) and carries `lang="en"` / `lang="fr"`. Choosing one applies the language at once and keeps the menu open, like the theme items. See LanguageSwitcher. - **Keyboard:** Escape or a click outside closes it. Arrow keys move between items. - The board now draws the theme items, so the chip, menu and sheet need no visual exception. The board highlights Profile even on Home; the app highlights it only on `/profile`. @@ -734,7 +735,16 @@ Three choices: System (the default), Light and Dark. The toggle at the top of th Nuxt UI In the user menu: a UDropdownMenu group of three type: 'checkbox' items bound to useColorMode().preference . On auth screens: UColorModeSelect in the footer. Nuxt UI's UColorModeButton isn't used because it only toggles between light and dark and hides the system option. colorMode: true in the plugin handles storage and applying the .dark class before first paint. -ARIA Menu items use menuitemcheckbox , with aria-checked on the current choice. The segmented variant is a radiogroup named "Theme". +ARIA Menu items use menuitemradio , with aria-checked on the current choice. The segmented variant is a radiogroup named "Theme". + +### LanguageSwitcher + +Two choices: English and Français. Each is always written in its own language, whatever the interface language. It appears in two places and never as a form select. + +- **Account menu (signed in):** a "Language" section between the theme items and Log out. See UserMenu (changed). +- **Auth footer (signed out):** a chip beside the System theme chip, in the footer outside the card. The chip is 28 px high on desktop and 44 px on mobile, 8 px side padding, 1 px `--ui-border-accented` border, `--ui-text-toned` text, a 14 px lucide `languages` icon, then the current language's own name. `aria-haspopup="menu"`, `aria-expanded`, `aria-label` "Language: English". While open it takes `--ui-bg-elevated`. The footer sits at the right with an 8 px gap between the two chips. +- **Auth popover:** opens above the chip (bottom 36 px, right-aligned, 168 px wide), `--ui-bg`, 1 px `--ui-border`, `--rc-shadow-pop`, 4 px padding, `role="menu"` named "Language". Rows are 32 px (44 px on mobile), 14 px text, `menuitemradio` with `lang`, a 16 px check icon at the right edge on the current one. Choosing a row applies the language and closes the popover; Escape or an outside click closes it without changing anything. +- The choice persists across reloads and applies to every screen at once. ## Added with roles, invite actions and Settings diff --git a/docs/design/screens/profile.md b/docs/design/screens/profile.md index 5ade18a..3603adf 100644 --- a/docs/design/screens/profile.md +++ b/docs/design/screens/profile.md @@ -48,8 +48,12 @@ Desktop: a 224 px popover above the chip, aligned to its left. Mobile: the accou | Header | The name in semibold (display name, or the username), and the email in muted 12 px (14 px on mobile, with a 36 px initials avatar beside them) | | Item | Profile (circle-user icon), highlighted while on `/profile` | | Separator | | +| Label | "Theme": 11 px, weight 500, uppercase, letter-spacing .04em, muted | | Items | System, Light, Dark: a radio-style group, the current choice carries a check icon at the right edge; choosing one applies the theme at once and keeps the menu open | | Separator | | +| Label | "Language", styled like "Theme" | +| Items | English, Français (each in its own language, with `lang`): a radio-style group with a check on the current one; choosing one applies the language at once and keeps the menu open | +| Separator | | | Item | Log out (log-out icon) | Avatar initials come from the first letters of the first two words of the display name, uppercased ("Marta Alves" gives MA). Without a display name they come from the username. @@ -64,7 +68,7 @@ The board's menu now carries System, Light and Dark, matching the shipped UserMe - Breadcrumbs: Home (mobile title "Home") - Page behind the menu: Home, default -- Menu open, as described above, with the System, Light and Dark group between Profile and Log out. The board shows Light checked +- Menu open, as described above, with the Theme group (System, Light, Dark) and the Language group (English, Français) between Profile and Log out. The board shows Light and English checked ### Loading diff --git a/docs/design/source/AppFrame.dc.html b/docs/design/source/AppFrame.dc.html index 928e837..837a490 100644 --- a/docs/design/source/AppFrame.dc.html +++ b/docs/design/source/AppFrame.dc.html @@ -18,7 +18,7 @@
{{ ini }}{{ who }}
-
{{ who }}marta@renew.example
Profile
{{ m.t }}
Log out
+
{{ who }}marta@renew.example
Profile
Theme
{{ m.t }}
Language
{{ m.t }}
Log out
@@ -280,7 +280,7 @@
-
{{ ini }}
{{ who }}marta@renew.example
Profile
{{ m.t }}
Log out
+
{{ ini }}
{{ who }}marta@renew.example
Profile
Theme
{{ m.t }}
Language
{{ m.t }}
Log out
{{ s.overlay.title }}{{ s.overlay.text }}
{{ s.overlay.meta }}{{ s.overlay.btn }}
@@ -435,7 +435,7 @@ class Component extends DCLogic { const edTitle = e.title || 'How we price a bathroom renovation' return { cr, s, nav, crumbs, who, ini, pf, dn, dnSave, rsNote, rsBtn, - themeSeg: [['system','monitor','System'],['light','sun','Light'],['dark','moon','Dark']].map(([k,icon,tt])=>{const on=(s.mode||'light')===k;return {act:'theme-'+k,icon,t:tt,on,op:on?1:0}}), menuOpen: !!s.userMenu, deskMenu: !m && !!s.userMenu, mobMenu: m && !!s.userMenu, pfItemBg: 'var(--ui-bg-elevated)', + langSeg: [['en','English'],['fr','Français']].map(([k,tt])=>{const on=(s.lang||'en')===k;return {act:'lang-'+k,code:k,t:tt,on,op:on?1:0}}), themeSeg: [['system','monitor','System'],['light','sun','Light'],['dark','moon','Dark']].map(([k,icon,tt])=>{const on=(s.mode||'light')===k;return {act:'theme-'+k,icon,t:tt,on,op:on?1:0}}), menuOpen: !!s.userMenu, deskMenu: !m && !!s.userMenu, mobMenu: m && !!s.userMenu, pfItemBg: 'var(--ui-bg-elevated)', chipBg: s.userMenu || s.nav === 'profile' ? 'var(--rc-sand)' : 'transparent', chipFg: s.userMenu || s.nav === 'profile' ? '#111111' : 'var(--ui-text-highlighted)', isSettings: k === 'settings', isProfile: k === 'profile', pfLoading: k === 'profile' && !!pf.loading, pfReady: k === 'profile' && !pf.loading, upBtnDir: m ? 'column' : 'row', upBtnAlign: m ? 'stretch' : 'flex-start', cardPad: m ? '16px' : '20px', cardPadX: m ? '16px' : '20px', inH: m ? '44px' : '40px', pfCols: m ? 'minmax(0,1fr)' : 'minmax(0,1fr) minmax(0,1.4fr)', pwDir: m ? 'column' : 'row', pwAlign: m ? 'stretch' : 'flex-end', edTitle, edBody: e.body || '', sampleBody: !e.body, tabs, rows, save, counter, pub, mFields, up, toasts, endpoints, toolbar, diff --git a/docs/design/source/AuthCard.dc.html b/docs/design/source/AuthCard.dc.html index c657854..0bfe22b 100644 --- a/docs/design/source/AuthCard.dc.html +++ b/docs/design/source/AuthCard.dc.html @@ -54,16 +54,17 @@
{{ s.footer.pre }}{{ s.footer.link }}API pending -System +
{{ m.t }}
{{ langName }}System
- + + diff --git a/web/src/components/LanguagePicker.vue b/web/src/components/LanguagePicker.vue deleted file mode 100644 index c8ac771..0000000 --- a/web/src/components/LanguagePicker.vue +++ /dev/null @@ -1,44 +0,0 @@ - - - diff --git a/web/src/components/UserMenu.vue b/web/src/components/UserMenu.vue index 045ee59..8e7ee44 100644 --- a/web/src/components/UserMenu.vue +++ b/web/src/components/UserMenu.vue @@ -1,13 +1,17 @@