@besaitech/ng-design-system 0.0.11 → 0.0.26

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,10 +1,12 @@
1
1
  import * as i0 from '@angular/core';
2
- import { inject, input, computed, ChangeDetectionStrategy, Component, booleanAttribute, isDevMode, effect, signal, forwardRef, viewChild, numberAttribute, afterRenderEffect, ElementRef, model, output, Injector, afterNextRender, Injectable, TemplateRef, Directive, contentChildren, contentChild } from '@angular/core';
2
+ import { inject, input, computed, ChangeDetectionStrategy, Component, booleanAttribute, isDevMode, effect, output, signal, forwardRef, viewChild, numberAttribute, afterRenderEffect, ElementRef, model, Injector, afterNextRender, Injectable, TemplateRef, Directive, contentChildren, contentChild } from '@angular/core';
3
3
  import { DomSanitizer } from '@angular/platform-browser';
4
4
  import * as i1 from '@angular/forms';
5
5
  import { NG_VALUE_ACCESSOR, NG_VALIDATORS, FormsModule } from '@angular/forms';
6
6
  import { NgTemplateOutlet } from '@angular/common';
7
- import { RouterLink, RouterLinkActive } from '@angular/router';
7
+ import { toSignal } from '@angular/core/rxjs-interop';
8
+ import { Router, NavigationEnd, RouterLink, RouterLinkActive } from '@angular/router';
9
+ import { filter, map } from 'rxjs';
8
10
 
9
11
  /**
10
12
  * Curated set of line icons (Lucide-style, 24x24 viewBox, stroke-based).
@@ -12,6 +14,17 @@ import { RouterLink, RouterLinkActive } from '@angular/router';
12
14
  * Add new icons here and they become available everywhere via name="...".
13
15
  */
14
16
  const SDS_ICONS = {
17
+ // Added for the Belvigo merchant shell (nav + header). Lucide 24x24 stroke paths, matching the
18
+ // set the app already used before it moved onto this library.
19
+ 'chart-column': '<path d="M3 3v16a2 2 0 0 0 2 2h16"/><path d="M18 17V9"/><path d="M13 17V5"/><path d="M8 17v-3"/>',
20
+ 'clipboard-list': '<rect width="8" height="4" x="8" y="2" rx="1" ry="1"/><path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"/><path d="M12 11h4"/><path d="M12 16h4"/><path d="M8 11h.01"/><path d="M8 16h.01"/>',
21
+ 'credit-card': '<rect width="20" height="14" x="2" y="5" rx="2"/><line x1="2" x2="22" y1="10" y2="10"/>',
22
+ 'external-link': '<path d="M15 3h6v6"/><path d="M10 14 21 3"/><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/>',
23
+ 'languages': '<path d="m5 8 6 6"/><path d="m4 14 6-6 2-3"/><path d="M2 5h12"/><path d="M7 2h1"/><path d="m22 22-5-10-5 10"/><path d="M14 18h6"/>',
24
+ 'megaphone': '<path d="m3 11 18-5v12L3 14v-3z"/><path d="M11.6 16.8a3 3 0 1 1-5.8-1.6"/>',
25
+ 'monitor': '<rect width="20" height="14" x="2" y="3" rx="2"/><line x1="8" x2="16" y1="21" y2="21"/><line x1="12" x2="12" y1="17" y2="21"/>',
26
+ 'store': '<path d="m2 7 4.41-4.41A2 2 0 0 1 7.83 2h8.34a2 2 0 0 1 1.42.59L22 7"/><path d="M4 12v8a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-8"/><path d="M15 22v-4a2 2 0 0 0-2-2h-2a2 2 0 0 0-2 2v4"/><path d="M2 7h20"/><path d="M22 7v3a2 2 0 0 1-2 2a2.7 2.7 0 0 1-1.59-.63.7.7 0 0 0-.82 0A2.7 2.7 0 0 1 16 12a2.7 2.7 0 0 1-1.59-.63.7.7 0 0 0-.82 0A2.7 2.7 0 0 1 12 12a2.7 2.7 0 0 1-1.59-.63.7.7 0 0 0-.82 0A2.7 2.7 0 0 1 8 12a2.7 2.7 0 0 1-1.59-.63.7.7 0 0 0-.82 0A2.7 2.7 0 0 1 4 12a2 2 0 0 1-2-2V7"/>',
27
+ 'user-round': '<circle cx="12" cy="8" r="5"/><path d="M20 21a8 8 0 0 0-16 0"/>',
15
28
  home: '<path d="m3 9 9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/>',
16
29
  'user-plus': '<path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><line x1="19" x2="19" y1="8" y2="14"/><line x1="22" x2="16" y1="11" y2="11"/>',
17
30
  hand: '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-hand-icon lucide-hand"><path d="M18 11V6a2 2 0 0 0-2-2a2 2 0 0 0-2 2"/><path d="M14 10V4a2 2 0 0 0-2-2a2 2 0 0 0-2 2v2"/><path d="M10 10.5V6a2 2 0 0 0-2-2a2 2 0 0 0-2 2v8"/><path d="M18 8a2 2 0 1 1 4 0v6a8 8 0 0 1-8 8h-2c-2.8 0-4.5-.86-5.99-2.34l-3.6-3.6a2 2 0 0 1 2.83-2.82L7 15"/></svg>',
@@ -59,6 +72,9 @@ const SDS_ICONS = {
59
72
  filter: '<polygon points="22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3"/>',
60
73
  table: '<path d="M12 3v18"/><rect width="18" height="18" x="3" y="3" rx="2"/><path d="M3 9h18"/><path d="M3 15h18"/>',
61
74
  eye: '<path d="M2 12s3-7 10-7 10 7 10 7-3 7-10 7-10-7-10-7Z"/><circle cx="12" cy="12" r="3"/>',
75
+ // Pairs with `eye` for password reveal. Without it a show/hide toggle has no second state to draw,
76
+ // and aria-pressed alone leaves sighted users with no visible change.
77
+ 'eye-off': '<path d="M10.73 5.08A10.43 10.43 0 0 1 12 5c7 0 10 7 10 7a13.16 13.16 0 0 1-1.67 2.68"/><path d="M6.61 6.61A13.526 13.526 0 0 0 2 12s3 7 10 7a9.74 9.74 0 0 0 5.39-1.61"/><path d="m2 2 20 20"/><path d="M14.12 14.12a3 3 0 1 1-4.24-4.24"/>',
62
78
  user: '<path d="M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/>',
63
79
  'check-circle': '<circle cx="12" cy="12" r="10"/><path d="m9 12 2 2 4-4"/>',
64
80
  'arrows-clockwise': '<path d="M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8"/><path d="M21 3v5h-5"/><path d="M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16"/><path d="M3 21v-5h5"/>',
@@ -264,6 +280,21 @@ class SdsInput {
264
280
  clearable = input(false, ...(ngDevMode ? [{ debugName: "clearable", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
265
281
  leadingIcon = input(null, ...(ngDevMode ? [{ debugName: "leadingIcon" }] : []));
266
282
  trailingIcon = input(null, ...(ngDevMode ? [{ debugName: "trailingIcon" }] : []));
283
+ /**
284
+ * A CLICKABLE trailing icon, for the affordances that belong to the field itself — password reveal
285
+ * above all. Distinct from {@link trailingIcon}, which is decorative and explicitly
286
+ * `pointer-events-none`; a consumer who needed a working control previously had to abandon
287
+ * `sds-input` and rebuild the whole field, losing the label/error/aria wiring with it.
288
+ *
289
+ * Always pair it with {@link trailingActionLabel} — an icon-only button with no accessible name is
290
+ * unusable by a screen reader. For a toggle, also bind {@link trailingActionPressed}.
291
+ */
292
+ trailingAction = input(null, ...(ngDevMode ? [{ debugName: "trailingAction" }] : []));
293
+ /** Accessible name for {@link trailingAction}. Translate it in the consuming app. */
294
+ trailingActionLabel = input('', ...(ngDevMode ? [{ debugName: "trailingActionLabel" }] : []));
295
+ /** `aria-pressed` for a toggling action (e.g. show/hide password). Null renders no attribute. */
296
+ trailingActionPressed = input(null, ...(ngDevMode ? [{ debugName: "trailingActionPressed" }] : []));
297
+ trailingActionClick = output();
267
298
  /** Current value (driven by CVA + user input). */
268
299
  value = signal('', ...(ngDevMode ? [{ debugName: "value" }] : []));
269
300
  cvaDisabled = signal(false, ...(ngDevMode ? [{ debugName: "cvaDisabled" }] : []));
@@ -278,7 +309,7 @@ class SdsInput {
278
309
  }, ...(ngDevMode ? [{ debugName: "describedBy" }] : []));
279
310
  fieldPadding = computed(() => {
280
311
  const left = this.leadingIcon() ? 'pl-10' : 'pl-3.5';
281
- const right = this.trailingIcon() || this.clearable() ? 'pr-10' : 'pr-3.5';
312
+ const right = this.trailingIcon() || this.clearable() || this.trailingAction() ? 'pr-10' : 'pr-3.5';
282
313
  return `${left} ${right}`;
283
314
  }, ...(ngDevMode ? [{ debugName: "fieldPadding" }] : []));
284
315
  wrapperClasses = computed(() => {
@@ -325,7 +356,7 @@ class SdsInput {
325
356
  this.onTouched();
326
357
  }
327
358
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: SdsInput, deps: [], target: i0.ɵɵFactoryTarget.Component });
328
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.25", type: SdsInput, isStandalone: true, selector: "sds-input", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, hint: { classPropertyName: "hint", publicName: "hint", isSignal: true, isRequired: false, transformFunction: null }, error: { classPropertyName: "error", publicName: "error", isSignal: true, isRequired: false, transformFunction: null }, type: { classPropertyName: "type", publicName: "type", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, appearance: { classPropertyName: "appearance", publicName: "appearance", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, clearable: { classPropertyName: "clearable", publicName: "clearable", isSignal: true, isRequired: false, transformFunction: null }, leadingIcon: { classPropertyName: "leadingIcon", publicName: "leadingIcon", isSignal: true, isRequired: false, transformFunction: null }, trailingIcon: { classPropertyName: "trailingIcon", publicName: "trailingIcon", isSignal: true, isRequired: false, transformFunction: null } }, host: { classAttribute: "block" }, providers: [
359
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.25", type: SdsInput, isStandalone: true, selector: "sds-input", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, hint: { classPropertyName: "hint", publicName: "hint", isSignal: true, isRequired: false, transformFunction: null }, error: { classPropertyName: "error", publicName: "error", isSignal: true, isRequired: false, transformFunction: null }, type: { classPropertyName: "type", publicName: "type", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, appearance: { classPropertyName: "appearance", publicName: "appearance", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, clearable: { classPropertyName: "clearable", publicName: "clearable", isSignal: true, isRequired: false, transformFunction: null }, leadingIcon: { classPropertyName: "leadingIcon", publicName: "leadingIcon", isSignal: true, isRequired: false, transformFunction: null }, trailingIcon: { classPropertyName: "trailingIcon", publicName: "trailingIcon", isSignal: true, isRequired: false, transformFunction: null }, trailingAction: { classPropertyName: "trailingAction", publicName: "trailingAction", isSignal: true, isRequired: false, transformFunction: null }, trailingActionLabel: { classPropertyName: "trailingActionLabel", publicName: "trailingActionLabel", isSignal: true, isRequired: false, transformFunction: null }, trailingActionPressed: { classPropertyName: "trailingActionPressed", publicName: "trailingActionPressed", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { trailingActionClick: "trailingActionClick" }, host: { classAttribute: "block" }, providers: [
329
360
  {
330
361
  provide: NG_VALUE_ACCESSOR,
331
362
  useExisting: forwardRef(() => SdsInput),
@@ -379,6 +410,18 @@ class SdsInput {
379
410
  >
380
411
  <sds-icon name="x" [size]="15" />
381
412
  </button>
413
+ } @else if (trailingAction()) {
414
+ <button
415
+ type="button"
416
+ [attr.aria-label]="trailingActionLabel() || null"
417
+ [attr.aria-pressed]="trailingActionPressed()"
418
+ [attr.aria-controls]="inputId"
419
+ class="absolute right-2.5 inline-flex h-7 w-7 items-center justify-center rounded-full
420
+ opacity-70 hover:opacity-100 hover:bg-black/5"
421
+ (click)="trailingActionClick.emit()"
422
+ >
423
+ <sds-icon [name]="trailingAction()!" [size]="18" />
424
+ </button>
382
425
  } @else if (trailingIcon()) {
383
426
  <sds-icon
384
427
  [name]="trailingIcon()!"
@@ -465,6 +508,18 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
465
508
  >
466
509
  <sds-icon name="x" [size]="15" />
467
510
  </button>
511
+ } @else if (trailingAction()) {
512
+ <button
513
+ type="button"
514
+ [attr.aria-label]="trailingActionLabel() || null"
515
+ [attr.aria-pressed]="trailingActionPressed()"
516
+ [attr.aria-controls]="inputId"
517
+ class="absolute right-2.5 inline-flex h-7 w-7 items-center justify-center rounded-full
518
+ opacity-70 hover:opacity-100 hover:bg-black/5"
519
+ (click)="trailingActionClick.emit()"
520
+ >
521
+ <sds-icon [name]="trailingAction()!" [size]="18" />
522
+ </button>
468
523
  } @else if (trailingIcon()) {
469
524
  <sds-icon
470
525
  [name]="trailingIcon()!"
@@ -489,7 +544,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
489
544
  }
490
545
  `,
491
546
  }]
492
- }], propDecorators: { label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], hint: [{ type: i0.Input, args: [{ isSignal: true, alias: "hint", required: false }] }], error: [{ type: i0.Input, args: [{ isSignal: true, alias: "error", required: false }] }], type: [{ type: i0.Input, args: [{ isSignal: true, alias: "type", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], appearance: [{ type: i0.Input, args: [{ isSignal: true, alias: "appearance", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], clearable: [{ type: i0.Input, args: [{ isSignal: true, alias: "clearable", required: false }] }], leadingIcon: [{ type: i0.Input, args: [{ isSignal: true, alias: "leadingIcon", required: false }] }], trailingIcon: [{ type: i0.Input, args: [{ isSignal: true, alias: "trailingIcon", required: false }] }] } });
547
+ }], propDecorators: { label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], hint: [{ type: i0.Input, args: [{ isSignal: true, alias: "hint", required: false }] }], error: [{ type: i0.Input, args: [{ isSignal: true, alias: "error", required: false }] }], type: [{ type: i0.Input, args: [{ isSignal: true, alias: "type", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], appearance: [{ type: i0.Input, args: [{ isSignal: true, alias: "appearance", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], clearable: [{ type: i0.Input, args: [{ isSignal: true, alias: "clearable", required: false }] }], leadingIcon: [{ type: i0.Input, args: [{ isSignal: true, alias: "leadingIcon", required: false }] }], trailingIcon: [{ type: i0.Input, args: [{ isSignal: true, alias: "trailingIcon", required: false }] }], trailingAction: [{ type: i0.Input, args: [{ isSignal: true, alias: "trailingAction", required: false }] }], trailingActionLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "trailingActionLabel", required: false }] }], trailingActionPressed: [{ type: i0.Input, args: [{ isSignal: true, alias: "trailingActionPressed", required: false }] }], trailingActionClick: [{ type: i0.Output, args: ["trailingActionClick"] }] } });
493
548
 
494
549
  let nextId$9 = 0;
495
550
  const SIZES$7 = {
@@ -6459,29 +6514,72 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
6459
6514
  }] });
6460
6515
 
6461
6516
  /**
6462
- * Application top bar: brand gradient with a decorative wave, a logo slot,
6463
- * the app name, and the signed-in user with a logout action.
6517
+ * Application top bar: a logo slot, the app name, and three slots the app fills.
6518
+ *
6519
+ * Two appearances. `brand` is the gradient-and-wave bar an internal tool wants. `surface` is white on
6520
+ * a hairline border, for a bar that has to sit under a customer-facing page without dominating it —
6521
+ * a storefront header is a bar, but it is not a banner. The slots and the layout are identical in
6522
+ * both; only the paint differs, so a consumer app gets the same structure without the brand fill.
6523
+ *
6524
+ * It deliberately owns NO account UI. It used to render the signed-in user and a logout button, which
6525
+ * forced every consumer into one shape — no dropdown, no email, no menu, no busy state, and an
6526
+ * icon-only button whose accessible name the library had to guess a language for. What an app needs
6527
+ * beside its own brand bar differs per app, so the bar supplies the surface and the app supplies the
6528
+ * contents: project them into `[sdsActions]`.
6529
+ *
6530
+ * Slots, left to right: `[sdsLogo]`, `[sdsLead]` (beside the name — a location or workspace picker),
6531
+ * `[sdsCenter]` (the flexible middle — a search field), `[sdsActions]` (the right edge).
6464
6532
  *
6465
6533
  * @example
6466
- * <sds-topbar appName="WebApp" [user]="user" (logout)="signOut()">
6534
+ * <sds-topbar appName="WebApp">
6467
6535
  * <img sdsLogo src="logo.svg" alt="" />
6536
+ * <my-address-picker sdsLead />
6537
+ * <my-search sdsCenter />
6538
+ * <my-account-menu sdsActions />
6468
6539
  * </sds-topbar>
6469
6540
  */
6470
6541
  class SdsTopbar {
6471
6542
  store = inject(SidebarStore);
6472
6543
  appName = input('', ...(ngDevMode ? [{ debugName: "appName" }] : []));
6473
- user = input(null, ...(ngDevMode ? [{ debugName: "user" }] : []));
6474
- showLogout = input(true, ...(ngDevMode ? [{ debugName: "showLogout", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
6544
+ /**
6545
+ * Width of the bar's CONTENTS, as Tailwind width utilities — e.g. `"w-full max-w-7xl"`.
6546
+ *
6547
+ * The coloured band always spans the viewport; this constrains only what sits on it, so the logo and
6548
+ * the actions line up with a page whose own content is centred at the same width. Default is empty,
6549
+ * i.e. contents run edge to edge, which is right for a full-bleed dashboard shell.
6550
+ *
6551
+ * A class string rather than an enum on purpose: apps already state their content width as a Tailwind
6552
+ * utility, and this way the bar is given the SAME string rather than a name that has to be kept in
6553
+ * sync with it.
6554
+ */
6555
+ contentWidth = input('w-full', ...(ngDevMode ? [{ debugName: "contentWidth" }] : []));
6475
6556
  /** Show the mobile hamburger that toggles the sidebar drawer (< lg). */
6476
6557
  menuButton = input(true, ...(ngDevMode ? [{ debugName: "menuButton", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
6477
- logout = output();
6558
+ /**
6559
+ * Accessible name for the hamburger. Defaults to French so nothing changes for existing consumers,
6560
+ * but any app serving more than one language MUST pass a translated value — an icon-only control's
6561
+ * aria-label is its only name, and a hardcoded one is simply wrong in every other locale.
6562
+ */
6563
+ menuLabel = input('Ouvrir le menu', ...(ngDevMode ? [{ debugName: "menuLabel" }] : []));
6564
+ /** `brand` (default) paints the gradient and the wave; `surface` is white on a hairline border. */
6565
+ appearance = input('brand', ...(ngDevMode ? [{ debugName: "appearance" }] : []));
6566
+ shellClasses = computed(() => this.appearance() === 'brand'
6567
+ ? 'bg-linear-to-r from-brand-800 via-brand-700 to-brand-600 text-white'
6568
+ : 'border-b border-border bg-surface text-ink', ...(ngDevMode ? [{ debugName: "shellClasses" }] : []));
6569
+ menuButtonClasses = computed(() => this.appearance() === 'brand'
6570
+ ? 'text-white/85 hover:bg-white/10 hover:text-white focus-visible:ring-white/40'
6571
+ : 'text-ink-soft hover:bg-muted hover:text-ink focus-visible:ring-brand-500/40', ...(ngDevMode ? [{ debugName: "menuButtonClasses" }] : []));
6478
6572
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: SdsTopbar, deps: [], target: i0.ɵɵFactoryTarget.Component });
6479
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.25", type: SdsTopbar, isStandalone: true, selector: "sds-topbar", inputs: { appName: { classPropertyName: "appName", publicName: "appName", isSignal: true, isRequired: false, transformFunction: null }, user: { classPropertyName: "user", publicName: "user", isSignal: true, isRequired: false, transformFunction: null }, showLogout: { classPropertyName: "showLogout", publicName: "showLogout", isSignal: true, isRequired: false, transformFunction: null }, menuButton: { classPropertyName: "menuButton", publicName: "menuButton", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { logout: "logout" }, host: { classAttribute: "block" }, ngImport: i0, template: `
6480
- <header
6481
- class="relative z-30 flex items-center justify-between overflow-hidden bg-linear-to-r from-brand-800 via-brand-700 to-brand-600 px-4 text-white sm:px-6"
6482
- style="height: var(--sds-topbar-h)"
6483
- >
6484
- <!-- Decorative wave -->
6573
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.25", type: SdsTopbar, isStandalone: true, selector: "sds-topbar", inputs: { appName: { classPropertyName: "appName", publicName: "appName", isSignal: true, isRequired: false, transformFunction: null }, contentWidth: { classPropertyName: "contentWidth", publicName: "contentWidth", isSignal: true, isRequired: false, transformFunction: null }, menuButton: { classPropertyName: "menuButton", publicName: "menuButton", isSignal: true, isRequired: false, transformFunction: null }, menuLabel: { classPropertyName: "menuLabel", publicName: "menuLabel", isSignal: true, isRequired: false, transformFunction: null }, appearance: { classPropertyName: "appearance", publicName: "appearance", isSignal: true, isRequired: false, transformFunction: null } }, host: { classAttribute: "block" }, ngImport: i0, template: `
6574
+ <header class="relative z-30" [class]="shellClasses()" style="height: var(--sds-topbar-h)">
6575
+ <!--
6576
+ Decorative wave. The clip lives HERE, on the wave's own wrapper, and not on the header.
6577
+ The header used to carry overflow-hidden, which also clipped anything projected into
6578
+ [sdsActions] — so an app menu or dropdown, the very thing that slot exists for, rendered as a
6579
+ 4px sliver. The wave still needs clipping; nothing else does.
6580
+ -->
6581
+ @if (appearance() === 'brand') {
6582
+ <span class="pointer-events-none absolute inset-0 overflow-hidden" aria-hidden="true">
6485
6583
  <svg
6486
6584
  class="pointer-events-none absolute inset-y-0 right-0 h-full w-2/3 min-w-[420px]"
6487
6585
  viewBox="0 0 600 64"
@@ -6507,14 +6605,27 @@ class SdsTopbar {
6507
6605
  stroke-width="1.5"
6508
6606
  />
6509
6607
  </svg>
6608
+ </span>
6609
+ }
6510
6610
 
6611
+ <!--
6612
+ The band is always full-bleed — the gradient and the wave span the viewport — while the
6613
+ CONTENTS are constrained by [contentWidth], so a bar over a centred page lines up with it
6614
+ instead of running to the edges. Empty (the default) leaves the contents full-bleed too,
6615
+ which is what a dashboard shell wants.
6616
+ -->
6617
+ <div
6618
+ class="relative mx-auto flex h-full items-center justify-between px-4 sm:px-6"
6619
+ [class]="contentWidth()"
6620
+ >
6511
6621
  <!-- Left: mobile menu button + logo + app name -->
6512
6622
  <div class="relative flex items-center gap-3">
6513
6623
  @if (menuButton()) {
6514
6624
  <button
6515
6625
  type="button"
6516
- class="-ml-1 inline-flex h-9 w-9 items-center justify-center rounded-control text-white/85 transition-colors hover:bg-white/10 hover:text-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/40 lg:hidden"
6517
- aria-label="Ouvrir le menu"
6626
+ class="-ml-1 inline-flex h-9 w-9 items-center justify-center rounded-control transition-colors focus-visible:outline-none focus-visible:ring-2 lg:hidden"
6627
+ [class]="menuButtonClasses()"
6628
+ [attr.aria-label]="menuLabel()"
6518
6629
  (click)="store.toggleMobile()"
6519
6630
  >
6520
6631
  <sds-icon name="menu" [size]="22" />
@@ -6522,51 +6633,42 @@ class SdsTopbar {
6522
6633
  }
6523
6634
  <ng-content select="[sdsLogo]" />
6524
6635
  @if (appName()) {
6525
- <span class="h-7 w-px bg-white/30"></span>
6636
+ <span class="h-7 w-px" [class]="appearance() === 'brand' ? 'bg-white/30' : 'bg-border'"></span>
6526
6637
  <span class="text-xl font-bold tracking-tight">{{ appName() }}</span>
6527
6638
  }
6639
+ <ng-content select="[sdsLead]" />
6640
+ </div>
6641
+
6642
+ <!-- Flexible middle: takes the space the two ends leave, and is skipped entirely when empty. -->
6643
+ <div class="relative flex min-w-0 flex-1 items-center justify-center px-2">
6644
+ <ng-content select="[sdsCenter]" />
6528
6645
  </div>
6529
6646
 
6530
- <!-- Right: actions + user + logout -->
6647
+ <!-- Right: whatever the app puts here -->
6531
6648
  <div class="relative flex items-center gap-3 sm:gap-4">
6532
6649
  <ng-content select="[sdsActions]" />
6533
- @if (user(); as u) {
6534
- <span class="hidden text-sm font-medium sm:block">{{ u.name }}</span>
6535
- <sds-avatar
6536
- [name]="u.name"
6537
- [initials]="u.initials ?? ''"
6538
- [status]="u.status ?? 'online'"
6539
- [size]="38"
6540
- bgClass="bg-white/15"
6541
- />
6542
- }
6543
- @if (showLogout()) {
6544
- <button
6545
- type="button"
6546
- class="inline-flex h-9 w-9 items-center justify-center rounded-control text-white/85 transition-colors hover:bg-white/10 hover:text-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/40"
6547
- aria-label="Se déconnecter"
6548
- (click)="logout.emit()"
6549
- >
6550
- <sds-icon name="log-out" [size]="20" />
6551
- </button>
6552
- }
6650
+ </div>
6553
6651
  </div>
6554
6652
  </header>
6555
- `, isInline: true, dependencies: [{ kind: "component", type: SdsAvatar, selector: "sds-avatar", inputs: ["name", "initials", "src", "size", "status", "bgClass"] }, { kind: "component", type: SdsIcon, selector: "sds-icon", inputs: ["name", "size", "strokeWidth"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
6653
+ `, isInline: true, dependencies: [{ kind: "component", type: SdsIcon, selector: "sds-icon", inputs: ["name", "size", "strokeWidth"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
6556
6654
  }
6557
6655
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: SdsTopbar, decorators: [{
6558
6656
  type: Component,
6559
6657
  args: [{
6560
6658
  selector: 'sds-topbar',
6561
6659
  changeDetection: ChangeDetectionStrategy.OnPush,
6562
- imports: [SdsAvatar, SdsIcon],
6660
+ imports: [SdsIcon],
6563
6661
  host: { class: 'block' },
6564
6662
  template: `
6565
- <header
6566
- class="relative z-30 flex items-center justify-between overflow-hidden bg-linear-to-r from-brand-800 via-brand-700 to-brand-600 px-4 text-white sm:px-6"
6567
- style="height: var(--sds-topbar-h)"
6568
- >
6569
- <!-- Decorative wave -->
6663
+ <header class="relative z-30" [class]="shellClasses()" style="height: var(--sds-topbar-h)">
6664
+ <!--
6665
+ Decorative wave. The clip lives HERE, on the wave's own wrapper, and not on the header.
6666
+ The header used to carry overflow-hidden, which also clipped anything projected into
6667
+ [sdsActions] — so an app menu or dropdown, the very thing that slot exists for, rendered as a
6668
+ 4px sliver. The wave still needs clipping; nothing else does.
6669
+ -->
6670
+ @if (appearance() === 'brand') {
6671
+ <span class="pointer-events-none absolute inset-0 overflow-hidden" aria-hidden="true">
6570
6672
  <svg
6571
6673
  class="pointer-events-none absolute inset-y-0 right-0 h-full w-2/3 min-w-[420px]"
6572
6674
  viewBox="0 0 600 64"
@@ -6592,14 +6694,27 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
6592
6694
  stroke-width="1.5"
6593
6695
  />
6594
6696
  </svg>
6697
+ </span>
6698
+ }
6595
6699
 
6700
+ <!--
6701
+ The band is always full-bleed — the gradient and the wave span the viewport — while the
6702
+ CONTENTS are constrained by [contentWidth], so a bar over a centred page lines up with it
6703
+ instead of running to the edges. Empty (the default) leaves the contents full-bleed too,
6704
+ which is what a dashboard shell wants.
6705
+ -->
6706
+ <div
6707
+ class="relative mx-auto flex h-full items-center justify-between px-4 sm:px-6"
6708
+ [class]="contentWidth()"
6709
+ >
6596
6710
  <!-- Left: mobile menu button + logo + app name -->
6597
6711
  <div class="relative flex items-center gap-3">
6598
6712
  @if (menuButton()) {
6599
6713
  <button
6600
6714
  type="button"
6601
- class="-ml-1 inline-flex h-9 w-9 items-center justify-center rounded-control text-white/85 transition-colors hover:bg-white/10 hover:text-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/40 lg:hidden"
6602
- aria-label="Ouvrir le menu"
6715
+ class="-ml-1 inline-flex h-9 w-9 items-center justify-center rounded-control transition-colors focus-visible:outline-none focus-visible:ring-2 lg:hidden"
6716
+ [class]="menuButtonClasses()"
6717
+ [attr.aria-label]="menuLabel()"
6603
6718
  (click)="store.toggleMobile()"
6604
6719
  >
6605
6720
  <sds-icon name="menu" [size]="22" />
@@ -6607,39 +6722,26 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
6607
6722
  }
6608
6723
  <ng-content select="[sdsLogo]" />
6609
6724
  @if (appName()) {
6610
- <span class="h-7 w-px bg-white/30"></span>
6725
+ <span class="h-7 w-px" [class]="appearance() === 'brand' ? 'bg-white/30' : 'bg-border'"></span>
6611
6726
  <span class="text-xl font-bold tracking-tight">{{ appName() }}</span>
6612
6727
  }
6728
+ <ng-content select="[sdsLead]" />
6613
6729
  </div>
6614
6730
 
6615
- <!-- Right: actions + user + logout -->
6731
+ <!-- Flexible middle: takes the space the two ends leave, and is skipped entirely when empty. -->
6732
+ <div class="relative flex min-w-0 flex-1 items-center justify-center px-2">
6733
+ <ng-content select="[sdsCenter]" />
6734
+ </div>
6735
+
6736
+ <!-- Right: whatever the app puts here -->
6616
6737
  <div class="relative flex items-center gap-3 sm:gap-4">
6617
6738
  <ng-content select="[sdsActions]" />
6618
- @if (user(); as u) {
6619
- <span class="hidden text-sm font-medium sm:block">{{ u.name }}</span>
6620
- <sds-avatar
6621
- [name]="u.name"
6622
- [initials]="u.initials ?? ''"
6623
- [status]="u.status ?? 'online'"
6624
- [size]="38"
6625
- bgClass="bg-white/15"
6626
- />
6627
- }
6628
- @if (showLogout()) {
6629
- <button
6630
- type="button"
6631
- class="inline-flex h-9 w-9 items-center justify-center rounded-control text-white/85 transition-colors hover:bg-white/10 hover:text-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/40"
6632
- aria-label="Se déconnecter"
6633
- (click)="logout.emit()"
6634
- >
6635
- <sds-icon name="log-out" [size]="20" />
6636
- </button>
6637
- }
6739
+ </div>
6638
6740
  </div>
6639
6741
  </header>
6640
6742
  `,
6641
6743
  }]
6642
- }], propDecorators: { appName: [{ type: i0.Input, args: [{ isSignal: true, alias: "appName", required: false }] }], user: [{ type: i0.Input, args: [{ isSignal: true, alias: "user", required: false }] }], showLogout: [{ type: i0.Input, args: [{ isSignal: true, alias: "showLogout", required: false }] }], menuButton: [{ type: i0.Input, args: [{ isSignal: true, alias: "menuButton", required: false }] }], logout: [{ type: i0.Output, args: ["logout"] }] } });
6744
+ }], propDecorators: { appName: [{ type: i0.Input, args: [{ isSignal: true, alias: "appName", required: false }] }], contentWidth: [{ type: i0.Input, args: [{ isSignal: true, alias: "contentWidth", required: false }] }], menuButton: [{ type: i0.Input, args: [{ isSignal: true, alias: "menuButton", required: false }] }], menuLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "menuLabel", required: false }] }], appearance: [{ type: i0.Input, args: [{ isSignal: true, alias: "appearance", required: false }] }] } });
6643
6745
 
6644
6746
  /**
6645
6747
  * App layout shell: a full-height column with the top bar across the top and a
@@ -6702,6 +6804,14 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
6702
6804
  */
6703
6805
  class SdsSidebar {
6704
6806
  store = inject(SidebarStore);
6807
+ /**
6808
+ * Accessible names for the rail's three icon-only controls. French defaults keep existing consumers
6809
+ * unchanged; a multilingual app must pass translated values, since an icon-only button's aria-label
6810
+ * is its only name.
6811
+ */
6812
+ collapseLabel = input('Replier le menu', ...(ngDevMode ? [{ debugName: "collapseLabel" }] : []));
6813
+ expandLabel = input('Déplier le menu', ...(ngDevMode ? [{ debugName: "expandLabel" }] : []));
6814
+ closeLabel = input('Fermer le menu', ...(ngDevMode ? [{ debugName: "closeLabel" }] : []));
6705
6815
  collapsed = this.store.collapsed;
6706
6816
  mobileOpen = this.store.mobileOpen;
6707
6817
  /** Desktop rail width (full vs. collapsed); mobile always uses full width. */
@@ -6709,7 +6819,7 @@ class SdsSidebar {
6709
6819
  ? 'var(--sds-sidebar-w-collapsed)'
6710
6820
  : 'var(--sds-sidebar-w)', ...(ngDevMode ? [{ debugName: "railWidth" }] : []));
6711
6821
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: SdsSidebar, deps: [], target: i0.ɵɵFactoryTarget.Component });
6712
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.25", type: SdsSidebar, isStandalone: true, selector: "sds-sidebar", host: { classAttribute: "block h-full" }, ngImport: i0, template: `
6822
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.25", type: SdsSidebar, isStandalone: true, selector: "sds-sidebar", inputs: { collapseLabel: { classPropertyName: "collapseLabel", publicName: "collapseLabel", isSignal: true, isRequired: false, transformFunction: null }, expandLabel: { classPropertyName: "expandLabel", publicName: "expandLabel", isSignal: true, isRequired: false, transformFunction: null }, closeLabel: { classPropertyName: "closeLabel", publicName: "closeLabel", isSignal: true, isRequired: false, transformFunction: null } }, host: { classAttribute: "block h-full" }, ngImport: i0, template: `
6713
6823
  <!-- Mobile backdrop (only when the drawer is open, < lg) -->
6714
6824
  @if (mobileOpen()) {
6715
6825
  <div
@@ -6737,7 +6847,7 @@ class SdsSidebar {
6737
6847
  <button
6738
6848
  type="button"
6739
6849
  class="hidden h-9 w-9 items-center justify-center rounded-control text-white/70 transition-colors hover:bg-white/10 hover:text-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/40 lg:inline-flex"
6740
- [attr.aria-label]="collapsed() ? 'Déplier le menu' : 'Replier le menu'"
6850
+ [attr.aria-label]="collapsed() ? expandLabel() : collapseLabel()"
6741
6851
  [attr.aria-expanded]="!collapsed()"
6742
6852
  (click)="store.toggle()"
6743
6853
  >
@@ -6749,7 +6859,7 @@ class SdsSidebar {
6749
6859
  <button
6750
6860
  type="button"
6751
6861
  class="inline-flex h-9 w-9 items-center justify-center rounded-control text-white/70 transition-colors hover:bg-white/10 hover:text-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/40 lg:hidden"
6752
- aria-label="Fermer le menu"
6862
+ [attr.aria-label]="closeLabel()"
6753
6863
  (click)="store.closeMobile()"
6754
6864
  >
6755
6865
  <sds-icon name="x" [size]="20" />
@@ -6803,7 +6913,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
6803
6913
  <button
6804
6914
  type="button"
6805
6915
  class="hidden h-9 w-9 items-center justify-center rounded-control text-white/70 transition-colors hover:bg-white/10 hover:text-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/40 lg:inline-flex"
6806
- [attr.aria-label]="collapsed() ? 'Déplier le menu' : 'Replier le menu'"
6916
+ [attr.aria-label]="collapsed() ? expandLabel() : collapseLabel()"
6807
6917
  [attr.aria-expanded]="!collapsed()"
6808
6918
  (click)="store.toggle()"
6809
6919
  >
@@ -6815,7 +6925,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
6815
6925
  <button
6816
6926
  type="button"
6817
6927
  class="inline-flex h-9 w-9 items-center justify-center rounded-control text-white/70 transition-colors hover:bg-white/10 hover:text-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/40 lg:hidden"
6818
- aria-label="Fermer le menu"
6928
+ [attr.aria-label]="closeLabel()"
6819
6929
  (click)="store.closeMobile()"
6820
6930
  >
6821
6931
  <sds-icon name="x" [size]="20" />
@@ -6834,62 +6944,206 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
6834
6944
  </aside>
6835
6945
  `,
6836
6946
  }]
6837
- }] });
6947
+ }], propDecorators: { collapseLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "collapseLabel", required: false }] }], expandLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "expandLabel", required: false }] }], closeLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "closeLabel", required: false }] }] } });
6838
6948
 
6839
6949
  /**
6840
- * Groups nav items under an optional uppercase section label. The label is
6841
- * hidden (replaced by a divider) when the sidebar is collapsed.
6950
+ * Groups nav items under an optional uppercase section label.
6951
+ *
6952
+ * Two modes:
6953
+ * - **Section** (default) — a static label over its items. The label is replaced by a divider when
6954
+ * the rail is collapsed.
6955
+ * - **Collapsible** (`[collapsible]`) — the label becomes a button that expands and collapses the
6956
+ * group. Set `[basePath]` and the group opens itself whenever the active route is inside it, so a
6957
+ * deep link or a browser Back lands with the right section already open instead of the person
6958
+ * having to find and re-open it.
6959
+ *
6960
+ * Collapsible mode exists because a flat rail stops scaling somewhere around a dozen destinations,
6961
+ * and the alternative — showing every child of every section at once — buries the ones in use.
6962
+ *
6963
+ * On the collapsed desktop rail there is nowhere to put a header, so a collapsible group renders
6964
+ * exactly like a section: divider, children visible. Hiding them there would leave the rail with
6965
+ * unreachable destinations and no affordance to reveal them.
6842
6966
  */
6843
6967
  class SdsNavGroup {
6844
6968
  store = inject(SidebarStore);
6969
+ router = inject(Router);
6845
6970
  label = input('', ...(ngDevMode ? [{ debugName: "label" }] : []));
6971
+ /** Turn the label into an expand/collapse control. */
6972
+ collapsible = input(false, ...(ngDevMode ? [{ debugName: "collapsible", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
6973
+ /** Icon for a collapsible header, so it reads at the same weight as the items it sits among. */
6974
+ icon = input(null, ...(ngDevMode ? [{ debugName: "icon" }] : []));
6975
+ /**
6976
+ * URL prefix owned by this group, e.g. `/payments`. While the active route starts with it the
6977
+ * group counts as active and opens itself — unless the person has explicitly toggled it since.
6978
+ */
6979
+ basePath = input(null, ...(ngDevMode ? [{ debugName: "basePath" }] : []));
6980
+ /** Emits the new state on every manual toggle. Not emitted for route-driven auto-expansion. */
6981
+ expandedChange = output();
6982
+ /** Explicit user choice; null means "follow the route". */
6983
+ manual = signal(null, ...(ngDevMode ? [{ debugName: "manual" }] : []));
6984
+ url = toSignal(this.router.events.pipe(filter((e) => e instanceof NavigationEnd), map((e) => e.urlAfterRedirects)), { initialValue: this.router.url });
6985
+ /** True while the active route lives inside this group. */
6986
+ routeActive = computed(() => {
6987
+ const base = this.basePath();
6988
+ return !!base && this.url().startsWith(base);
6989
+ }, ...(ngDevMode ? [{ debugName: "routeActive" }] : []));
6990
+ expanded = computed(() => this.manual() ?? this.routeActive(), ...(ngDevMode ? [{ debugName: "expanded" }] : []));
6846
6991
  collapsed = this.store.collapsed;
6992
+ /**
6993
+ * A plain section always shows its items — they carry their own icons, so they survive the collapsed
6994
+ * rail. A collapsible group hides its children there instead: its children are sub-destinations and
6995
+ * typically have no icon, which on an icon-only rail is a row with nothing in it.
6996
+ */
6997
+ childrenVisible = computed(() => !this.collapsible() || (!this.collapsed() && this.expanded()), ...(ngDevMode ? [{ debugName: "childrenVisible" }] : []));
6998
+ /**
6999
+ * One binding rather than `[class]` alongside `[class.hidden]` — mixing a whole-list binding with a
7000
+ * single-class toggle on the same element depends on Angular's styling precedence to resolve, which
7001
+ * is a subtlety no template should rest on.
7002
+ *
7003
+ * The indent rule appears only on an expanded collapsible group on the full-width rail: it is what
7004
+ * shows the children belong to the header above them.
7005
+ */
7006
+ childrenClass = computed(() => {
7007
+ const parts = ['flex flex-col gap-1'];
7008
+ if (!this.childrenVisible()) {
7009
+ parts.push('hidden');
7010
+ }
7011
+ if (this.collapsible() && !this.collapsed()) {
7012
+ parts.push('ml-5 border-l border-white/10 pl-3');
7013
+ }
7014
+ return parts.join(' ');
7015
+ }, ...(ngDevMode ? [{ debugName: "childrenClass" }] : []));
7016
+ /**
7017
+ * On the collapsed rail there is nothing to expand into, so the toggle always reports "opening" —
7018
+ * the app's handler takes that as the cue to navigate into the section, which is the only useful
7019
+ * meaning the control can have there.
7020
+ */
7021
+ toggle() {
7022
+ const next = this.collapsed() ? true : !this.expanded();
7023
+ this.manual.set(next);
7024
+ this.expandedChange.emit(next);
7025
+ }
6847
7026
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: SdsNavGroup, deps: [], target: i0.ɵɵFactoryTarget.Component });
6848
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.25", type: SdsNavGroup, isStandalone: true, selector: "sds-nav-group", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null } }, host: { classAttribute: "block" }, ngImport: i0, template: `
7027
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.25", type: SdsNavGroup, isStandalone: true, selector: "sds-nav-group", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, collapsible: { classPropertyName: "collapsible", publicName: "collapsible", isSignal: true, isRequired: false, transformFunction: null }, icon: { classPropertyName: "icon", publicName: "icon", isSignal: true, isRequired: false, transformFunction: null }, basePath: { classPropertyName: "basePath", publicName: "basePath", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { expandedChange: "expandedChange" }, host: { classAttribute: "block" }, ngImport: i0, template: `
6849
7028
  <div class="mt-5 first:mt-3">
6850
7029
  @if (label()) {
6851
- @if (!collapsed()) {
6852
- <p
6853
- class="mb-1.5 px-3 text-[11px] font-semibold uppercase tracking-wider text-white/60"
7030
+ @if (collapsible()) {
7031
+ <!--
7032
+ A collapsible header is styled as a NAV ROW, not as a section caption. A small uppercase
7033
+ caption says "everything under me belongs to me" — which is a lie the moment the group is
7034
+ collapsed and the next section's items sit directly beneath it. At row scale, with an icon
7035
+ and a chevron, it reads as the control it is.
7036
+
7037
+ It renders on the collapsed rail too, as an icon alone, because there it is the group's
7038
+ ONLY representation: the children are hidden (a child with no icon of its own would be a
7039
+ blank row), so the app is expected to treat the toggle as "take me into this section" —
7040
+ which is what expandedChange already means.
7041
+ -->
7042
+ <button
7043
+ type="button"
7044
+ class="mb-1 flex h-10 w-full items-center gap-3 rounded-control px-3 text-sm font-medium
7045
+ text-white/70 transition-colors hover:bg-white/8 hover:text-white
7046
+ focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/40"
7047
+ [class.lg:justify-center]="collapsed()"
7048
+ [class.text-white]="routeActive()"
7049
+ [attr.aria-expanded]="collapsed() ? null : expanded()"
7050
+ [attr.title]="collapsed() ? label() : null"
7051
+ [attr.aria-label]="collapsed() ? label() : null"
7052
+ (click)="toggle()"
6854
7053
  >
7054
+ @if (icon()) {
7055
+ <sds-icon [name]="icon()!" [size]="20" class="shrink-0" />
7056
+ }
7057
+ <span class="flex-1 truncate text-left" [class.lg:hidden]="collapsed()">
7058
+ {{ label() }}
7059
+ </span>
7060
+ <sds-icon
7061
+ name="chevron-down"
7062
+ [size]="16"
7063
+ class="shrink-0 text-white/50 transition-transform"
7064
+ [class.lg:hidden]="collapsed()"
7065
+ [class.rotate-180]="expanded()"
7066
+ />
7067
+ </button>
7068
+ } @else if (collapsed()) {
7069
+ <div class="mx-3 mb-2 mt-2 h-px bg-white/10"></div>
7070
+ } @else {
7071
+ <p class="mb-1.5 px-3 text-[11px] font-semibold uppercase tracking-wider text-white/60">
6855
7072
  {{ label() }}
6856
7073
  </p>
6857
- } @else {
6858
- <div class="mx-3 mb-2 mt-2 h-px bg-white/10"></div>
6859
7074
  }
6860
7075
  }
6861
- <div class="flex flex-col gap-1">
7076
+
7077
+ <div [class]="childrenClass()">
6862
7078
  <ng-content />
6863
7079
  </div>
6864
7080
  </div>
6865
- `, isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush });
7081
+ `, isInline: true, dependencies: [{ kind: "component", type: SdsIcon, selector: "sds-icon", inputs: ["name", "size", "strokeWidth"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
6866
7082
  }
6867
7083
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: SdsNavGroup, decorators: [{
6868
7084
  type: Component,
6869
7085
  args: [{
6870
7086
  selector: 'sds-nav-group',
6871
7087
  changeDetection: ChangeDetectionStrategy.OnPush,
7088
+ imports: [SdsIcon],
6872
7089
  host: { class: 'block' },
6873
7090
  template: `
6874
7091
  <div class="mt-5 first:mt-3">
6875
7092
  @if (label()) {
6876
- @if (!collapsed()) {
6877
- <p
6878
- class="mb-1.5 px-3 text-[11px] font-semibold uppercase tracking-wider text-white/60"
7093
+ @if (collapsible()) {
7094
+ <!--
7095
+ A collapsible header is styled as a NAV ROW, not as a section caption. A small uppercase
7096
+ caption says "everything under me belongs to me" — which is a lie the moment the group is
7097
+ collapsed and the next section's items sit directly beneath it. At row scale, with an icon
7098
+ and a chevron, it reads as the control it is.
7099
+
7100
+ It renders on the collapsed rail too, as an icon alone, because there it is the group's
7101
+ ONLY representation: the children are hidden (a child with no icon of its own would be a
7102
+ blank row), so the app is expected to treat the toggle as "take me into this section" —
7103
+ which is what expandedChange already means.
7104
+ -->
7105
+ <button
7106
+ type="button"
7107
+ class="mb-1 flex h-10 w-full items-center gap-3 rounded-control px-3 text-sm font-medium
7108
+ text-white/70 transition-colors hover:bg-white/8 hover:text-white
7109
+ focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/40"
7110
+ [class.lg:justify-center]="collapsed()"
7111
+ [class.text-white]="routeActive()"
7112
+ [attr.aria-expanded]="collapsed() ? null : expanded()"
7113
+ [attr.title]="collapsed() ? label() : null"
7114
+ [attr.aria-label]="collapsed() ? label() : null"
7115
+ (click)="toggle()"
6879
7116
  >
7117
+ @if (icon()) {
7118
+ <sds-icon [name]="icon()!" [size]="20" class="shrink-0" />
7119
+ }
7120
+ <span class="flex-1 truncate text-left" [class.lg:hidden]="collapsed()">
7121
+ {{ label() }}
7122
+ </span>
7123
+ <sds-icon
7124
+ name="chevron-down"
7125
+ [size]="16"
7126
+ class="shrink-0 text-white/50 transition-transform"
7127
+ [class.lg:hidden]="collapsed()"
7128
+ [class.rotate-180]="expanded()"
7129
+ />
7130
+ </button>
7131
+ } @else if (collapsed()) {
7132
+ <div class="mx-3 mb-2 mt-2 h-px bg-white/10"></div>
7133
+ } @else {
7134
+ <p class="mb-1.5 px-3 text-[11px] font-semibold uppercase tracking-wider text-white/60">
6880
7135
  {{ label() }}
6881
7136
  </p>
6882
- } @else {
6883
- <div class="mx-3 mb-2 mt-2 h-px bg-white/10"></div>
6884
7137
  }
6885
7138
  }
6886
- <div class="flex flex-col gap-1">
7139
+
7140
+ <div [class]="childrenClass()">
6887
7141
  <ng-content />
6888
7142
  </div>
6889
7143
  </div>
6890
7144
  `,
6891
7145
  }]
6892
- }], propDecorators: { label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }] } });
7146
+ }], propDecorators: { label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], collapsible: [{ type: i0.Input, args: [{ isSignal: true, alias: "collapsible", required: false }] }], icon: [{ type: i0.Input, args: [{ isSignal: true, alias: "icon", required: false }] }], basePath: [{ type: i0.Input, args: [{ isSignal: true, alias: "basePath", required: false }] }], expandedChange: [{ type: i0.Output, args: ["expandedChange"] }] } });
6893
7147
 
6894
7148
  /**
6895
7149
  * A single sidebar navigation entry. Renders an anchor when [link] is set