feat(portal-shell): ghost-style Sign-in button with log-in icon (#189)
## Summary The anonymous "Sign in" CTA in `portal-shell`'s header was a filled brand-primary block sitting next to two round icon-only buttons (Notifications, Help). The contrast was off — the filled rectangle visually dominated the row even though it's the *third* action in the strip. This PR turns it into a ghost-style button (no fill, no border) with a `log-in` icon ahead of the label, matching the quiet posture of its neighbours while still reading as the primary CTA for unauthenticated users. ## What lands `apps/portal-shell/src/app/components/header/header.html` — the anonymous-state button: | Aspect | Before | After | |---|---|---| | Fill | `bg-brand-primary-500` (filled) | `bg-transparent` (ghost) | | Text colour | `text-white` | `text-brand-primary-500` (`-300` in dark) | | Border | none | none — pure text-only style | | Hover | darker fill | light `bg-brand-primary-50` tint + `text-brand-primary-600` | | Icon | (none) | `<lib-icon name="log-in" [size]="16" aria-hidden="true" />` ahead of the label | | Padding / gap | `px-4 gap-2` | `px-3 gap-1.5` (slightly tighter, makes room for the icon without growing the chrome) | | Height | `h-11` | `h-11` (unchanged — 44 × 44 touch target per [ADR-0016](docs/decisions/0016-accessibility-baseline-wcag-aa-targeted-aaa.md) holds) | `libs/shared/ui/src/lib/icon/icon.ts` — adds the missing pair of the existing `log-out` icon: - Imports `LogIn` from `lucide-angular`. - Registers `'log-in': LogIn,` in the alphabetical registry between `'layout-dashboard'` and `'log-out'`. ## Notes for the reviewer - **Ghost vs. outline vs. filled — why ghost.** Tried two intermediate iterations during the design pass (outline with brand border, then a more compact outline). The user preferred the ghost rendering once we removed the border — the header strip is the right surface for an "always-quiet, surfaces on hover" CTA, since the user typically scans for the search bar first, not the auth state. Filled buttons are the right call inside content where the CTA *is* the focal point (forms, modals). - **Touch-target stays at 44 × 44.** `h-11` is kept on purpose. The CI a11y gate from ADR-0016 (`touch-target check (44×44 min)`) is non-negotiable for interactive controls; visually shrinking horizontal padding + reducing visual weight is the right way to "compact" a button without breaking the target rule. - **`aria-hidden="true"` on the icon.** The adjacent `<span>` carries the localised label, and the screen-reader contract is "the button announces 'Sign in', not 'log-in icon Sign in'". The icon is decorative reinforcement. - **Label still wrapped in `<span i18n="@@header.signIn">` rather than directly on the button.** Required because the button now contains both an icon child and the text — Angular's `i18n` on the button itself would extract the icon's rendered SVG into the translation unit, which is not what translators want to see. Wrapping the text isolates the translation unit cleanly. - **`log-in` belongs in `shared-ui`, not portal-shell.** Even though portal-shell is the only consumer today, the icon registry is by contract the single point of truth for both apps — `portal-admin`'s eventual sign-in surface will use the same icon, so registering it once in the shared lib is the right boundary. ## Test plan - [x] `pnpm nx test portal-shell` — green. The existing spec asserts `btn.textContent.trim() === 'Sign in'`; the `<lib-icon>` renders to an SVG (no text content) and the label is now inside a `<span>`, so the text-trim check still holds. - [x] `pnpm nx test shared-ui` — green. Icon registry's exhaustive-key spec picks up the new entry automatically (it iterates `Object.keys(ICON_REGISTRY)`). - [x] `pnpm nx build portal-shell` — clean, no bundle-size deltas worth flagging (`log-in` is tree-shaken alongside the rest of lucide-angular). - [x] `pnpm nx lint portal-shell shared-ui` — clean. - [ ] **Manual smoke** — `pnpm nx serve portal-shell`, signed-out, header visible: - Anonymous state: ghost "Sign in" button with the log-in icon. Hover surfaces a faint fill; focus shows the brand outline ring. - Switch to authenticated: button is replaced by `<lib-user-menu>` (unchanged). - `error` state: amber "Can't reach the server" badge (unchanged). - Toggle dark mode: text shifts to `brand-primary-300`, hover surfaces a `gray-800` fill; still readable. - Tab from the address bar into the header — focus order: search input → bell → help → Sign in. Focus ring on the ghost button matches the other icon buttons (`outline-brand-primary-500 outline-offset-2`). ## What's next - `portal-admin` will get the same `log-in` icon for its own (still skeleton) sign-in surface once that wiring lands — single shared registry means no further change here. - If the marketing folks ever ask the sign-in CTA to come back forward visually (festival days, post-incident push), the ghost class block can flip to a filled variant locally without touching the icon or i18n contract. --------- Co-authored-by: Julien Gautier <julien.gautier@apf.asso.fr> Reviewed-on: #189
This commit was merged in pull request #189.
This commit is contained in:
@@ -73,11 +73,11 @@
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
(click)="signIn()"
|
(click)="signIn()"
|
||||||
class="ml-1 inline-flex h-11 items-center gap-2 rounded-md bg-brand-primary-500 px-4 text-sm font-semibold text-white transition-colors hover:bg-brand-primary-600 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary-500"
|
class="ml-1 inline-flex h-11 items-center gap-1.5 rounded-md bg-transparent px-3 text-sm font-semibold text-brand-primary-500 transition-colors hover:bg-brand-primary-50 hover:text-brand-primary-600 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-primary-500 dark:text-brand-primary-300 dark:hover:bg-gray-800 dark:hover:text-brand-primary-200"
|
||||||
data-testid="sign-in-button"
|
data-testid="sign-in-button"
|
||||||
i18n="@@header.signIn"
|
|
||||||
>
|
>
|
||||||
Sign in
|
<lib-icon name="log-in" [size]="16" aria-hidden="true" />
|
||||||
|
<span i18n="@@header.signIn">Sign in</span>
|
||||||
</button>
|
</button>
|
||||||
} @case ('authenticated') { @if (authState(); as state) { @if (state.kind === 'authenticated')
|
} @case ('authenticated') { @if (authState(); as state) { @if (state.kind === 'authenticated')
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
Grid2x2,
|
Grid2x2,
|
||||||
House,
|
House,
|
||||||
LayoutDashboard,
|
LayoutDashboard,
|
||||||
|
LogIn,
|
||||||
LogOut,
|
LogOut,
|
||||||
type LucideIconData,
|
type LucideIconData,
|
||||||
LucideAngularModule,
|
LucideAngularModule,
|
||||||
@@ -67,6 +68,7 @@ const ICON_REGISTRY = {
|
|||||||
'grid-2x2': Grid2x2,
|
'grid-2x2': Grid2x2,
|
||||||
home: House,
|
home: House,
|
||||||
'layout-dashboard': LayoutDashboard,
|
'layout-dashboard': LayoutDashboard,
|
||||||
|
'log-in': LogIn,
|
||||||
'log-out': LogOut,
|
'log-out': LogOut,
|
||||||
mail: Mail,
|
mail: Mail,
|
||||||
monitor: Monitor,
|
monitor: Monitor,
|
||||||
|
|||||||
Reference in New Issue
Block a user