5f5e6d8e35
PR 1 of 3 from the user-menu / profile / cross-app-link chantier. * New `UserMenu` standalone component in `libs/shared/ui`. Avatar trigger + CDK menu panel (gitea / github shape) — "Signed in as" header, configurable rows (Profile, Settings — marked Soon), separator, dedicated Sign out row at the bottom. Keyboard nav, ARIA, and overlay portal handled by `@angular/cdk/menu`, same primitives the theme + locale switchers already use. * Each app's header consumes the shared component for the authenticated state. The loading / anonymous / error widgets stay app-local because they diverge by surface (portal-shell has a search bar + notification cluster, portal-admin is leaner). * Portal-shell anonymous Sign-in button moves from `rounded-full` to `rounded-md`. The loading + error chips follow for visual consistency with the new square-ish avatar. * Portal-admin overrides the shared `--user-menu-avatar-bg` CSS custom property to a translucent white so the avatar reads on the brand-primary-600 header background. Out of scope for this PR (covered by PR 2 + 3 of the chantier): profile pages, real roles surfaced on portal-shell, cross-app links between the two surfaces. The user menu's `items` input is designed to absorb those additions without touching the component. i18n: 6 new strings added to `messages.fr.xlf` for portal-shell. portal-admin labels stay in English (no FR locale in the admin app's v1 per ADR-0020 §"No locale switcher in v1").
77 lines
2.9 KiB
TypeScript
77 lines
2.9 KiB
TypeScript
import { CdkMenu, CdkMenuItem, CdkMenuTrigger } from '@angular/cdk/menu';
|
|
import {
|
|
ChangeDetectionStrategy,
|
|
Component,
|
|
ViewEncapsulation,
|
|
input,
|
|
output,
|
|
} from '@angular/core';
|
|
import { RouterLink } from '@angular/router';
|
|
import { Icon, type IconName } from '../icon/icon';
|
|
|
|
/**
|
|
* Shape of a single row in the user menu — Profile, Settings,
|
|
* cross-app links, etc. Sign out is NOT modelled here; it is always
|
|
* rendered as the last row in a separate group and emits the
|
|
* dedicated `signOut` output.
|
|
*
|
|
* Use `routerLink` for internal navigation (Angular `RouterLink`),
|
|
* `href` for cross-origin links (other apps, external URLs). At most
|
|
* one of the two should be set.
|
|
*/
|
|
export interface UserMenuItem {
|
|
readonly label: string;
|
|
readonly icon?: IconName;
|
|
readonly routerLink?: string;
|
|
readonly href?: string;
|
|
readonly disabled?: boolean;
|
|
/** Right-aligned chip — e.g. `"Soon"` for not-yet-shipped entries. */
|
|
readonly badge?: string;
|
|
}
|
|
|
|
/**
|
|
* Authenticated user menu — the gitea/github-style avatar dropdown
|
|
* docked at the top right of both `portal-shell` and `portal-admin`
|
|
* headers. Rendered only when the session is `authenticated`; the
|
|
* loading / anonymous / error widgets stay in each app's header
|
|
* because they vary by surface (search bar, sign-up button, etc.).
|
|
*
|
|
* The component is layout-only: caller passes the principal
|
|
* (`displayName`, `username`, `initials`) plus a list of `items`
|
|
* for the navigation rows, and listens for `signOut` to actually
|
|
* destroy the session.
|
|
*
|
|
* Keyboard nav (arrow keys, Home/End, Escape, type-ahead) is
|
|
* provided by `@angular/cdk/menu` — same primitives the
|
|
* `ThemeSwitcher` and `LocaleSwitcher` already use, kept consistent
|
|
* to land familiar interactions across the chrome.
|
|
*/
|
|
@Component({
|
|
selector: 'lib-user-menu',
|
|
imports: [CdkMenu, CdkMenuItem, CdkMenuTrigger, RouterLink, Icon],
|
|
templateUrl: './user-menu.html',
|
|
styleUrl: './user-menu.scss',
|
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
// CDK Menu opens its panel in an overlay portal outside this
|
|
// component's host. Encapsulated styles would not reach the
|
|
// overlay; the BEM-prefixed class names below keep selectors
|
|
// specific enough to avoid leakage. Same pattern as
|
|
// `LocaleSwitcher` / `ThemeSwitcher`.
|
|
encapsulation: ViewEncapsulation.None,
|
|
})
|
|
export class UserMenu {
|
|
readonly displayName = input.required<string>();
|
|
readonly username = input.required<string>();
|
|
readonly initials = input.required<string>();
|
|
readonly items = input.required<readonly UserMenuItem[]>();
|
|
|
|
/** Localised label for the "Signed in as {username}" panel header. */
|
|
readonly signedInAsLabel = input<string>('Signed in as');
|
|
/** Localised label for the trailing Sign out row. */
|
|
readonly signOutLabel = input<string>('Sign out');
|
|
/** Localised aria-label for the avatar trigger button. */
|
|
readonly triggerAriaLabel = input<string>('User menu');
|
|
|
|
readonly signOut = output<void>();
|
|
}
|