feat(shared-ui): user menu dropdown + integration on portal-shell + portal-admin
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").
This commit is contained in:
@@ -0,0 +1,76 @@
|
||||
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>();
|
||||
}
|
||||
Reference in New Issue
Block a user