@besaitech/ng-design-system 0.0.42 → 0.0.45

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,9 +1,9 @@
1
1
  import * as i0 from '@angular/core';
2
- import { inject, input, computed, ChangeDetectionStrategy, Component, booleanAttribute, isDevMode, effect, output, signal, forwardRef, viewChild, numberAttribute, viewChildren, afterRenderEffect, ElementRef, model, 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, viewChildren, afterRenderEffect, ElementRef, model, Injector, afterNextRender, TemplateRef, Directive, contentChild, Injectable, contentChildren, untracked, DestroyRef } 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
- import { NgTemplateOutlet } from '@angular/common';
6
+ import { NgTemplateOutlet, DOCUMENT } from '@angular/common';
7
7
  import { toSignal } from '@angular/core/rxjs-interop';
8
8
  import { Router, NavigationEnd, RouterLink, RouterLinkActive } from '@angular/router';
9
9
  import { filter, map } from 'rxjs';
@@ -97,6 +97,12 @@ const SDS_ICONS = {
97
97
  'loader-circle': '<path d="M21 12a9 9 0 1 1-6.219-8.56"/>',
98
98
  bug: '<path d="M12 20v-9"/><path d="M14 7a4 4 0 0 1 4 4v3a6 6 0 0 1-12 0v-3a4 4 0 0 1 4-4z"/><path d="M14.12 3.88 16 2"/><path d="M21 21a4 4 0 0 0-3.81-4"/><path d="M21 5a4 4 0 0 1-3.55 3.97"/><path d="M22 13h-4"/><path d="M3 21a4 4 0 0 1 3.81-4"/><path d="M3 5a4 4 0 0 0 3.55 3.97"/><path d="M6 13H2"/><path d="m8 2 1.88 1.88"/><path d="M9 7.13V6a3 3 0 1 1 6 0v1.13"/>',
99
99
  'scroll-text': '<path d="M15 12h-5"/><path d="M15 8h-5"/><path d="M19 17V5a2 2 0 0 0-2-2H4"/><path d="M8 21h12a2 2 0 0 0 2-2v-1a1 1 0 0 0-1-1H11a1 1 0 0 0-1 1v1a2 2 0 1 1-4 0V5a2 2 0 1 0-4 0v2a1 1 0 0 0 1 1h3"/>',
100
+ // Added for the dark/light switcher (sds-theme-toggle) and the command palette (sds-search-command).
101
+ moon: '<path d="M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z"/>',
102
+ sun: '<circle cx="12" cy="12" r="4"/><path d="M12 2v2"/><path d="M12 20v2"/><path d="m4.93 4.93 1.41 1.41"/><path d="m17.66 17.66 1.41 1.41"/><path d="M2 12h2"/><path d="M20 12h2"/><path d="m6.34 17.66-1.41 1.41"/><path d="m19.07 4.93-1.41 1.41"/>',
103
+ 'search-x': '<path d="m13.5 8.5-5 5"/><path d="m8.5 8.5 5 5"/><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/>',
104
+ command: '<path d="M15 6v12a3 3 0 1 0 3-3H6a3 3 0 1 0 3 3V6a3 3 0 1 0-3 3h12a3 3 0 1 0-3-3"/>',
105
+ sparkles: '<path d="M9.937 15.5A2 2 0 0 0 8.5 14.063l-6.135-1.582a.5.5 0 0 1 0-.962L8.5 9.936A2 2 0 0 0 9.937 8.5l1.582-6.135a.5.5 0 0 1 .963 0L14.063 8.5A2 2 0 0 0 15.5 9.937l6.135 1.581a.5.5 0 0 1 0 .964L15.5 14.063a2 2 0 0 0-1.437 1.437l-1.582 6.135a.5.5 0 0 1-.963 0z"/><path d="M20 3v4"/><path d="M22 5h-4"/><path d="M4 17v2"/><path d="M5 18H3"/>',
100
106
  };
101
107
 
102
108
  const SIZE_PX = { sm: 16, md: 20, lg: 24, xl: 32 };
@@ -155,7 +161,7 @@ const BASE = 'inline-flex items-center justify-center gap-2 font-medium ' +
155
161
  'focus-visible:ring-brand-500 disabled:opacity-50 disabled:pointer-events-none';
156
162
  const VARIANTS$2 = {
157
163
  primary: 'bg-brand-600 text-white shadow-sm hover:bg-brand-700 active:bg-brand-800',
158
- secondary: 'bg-white text-ink border border-border shadow-sm hover:bg-muted active:bg-brand-50',
164
+ secondary: 'bg-surface text-ink border border-border shadow-sm hover:bg-muted active:bg-brand-50',
159
165
  tertiary: 'bg-brand-50 text-brand-700 hover:bg-brand-100 active:bg-brand-200/70',
160
166
  ghost: 'bg-transparent text-ink-soft hover:bg-black/5 active:bg-black/10',
161
167
  danger: 'bg-danger text-white shadow-sm hover:brightness-95 active:brightness-90',
@@ -380,7 +386,7 @@ class SdsInput {
380
386
  : 'border-white/15 focus-within:border-white/40 focus-within:ring-2 focus-within:ring-white/20');
381
387
  }
382
388
  else {
383
- parts.push('bg-white text-ink');
389
+ parts.push('bg-surface text-ink');
384
390
  parts.push(this.error()
385
391
  ? 'border-danger focus-within:border-danger focus-within:ring-2 focus-within:ring-danger/25'
386
392
  : 'border-border focus-within:border-brand-500 focus-within:ring-2 focus-within:ring-brand-500/25');
@@ -764,7 +770,7 @@ class SdsInputNumber {
764
770
  : 'focus-within:ring-2 focus-within:ring-brand-500/25');
765
771
  }
766
772
  else {
767
- parts.push('bg-white text-ink');
773
+ parts.push('bg-surface text-ink');
768
774
  parts.push(this.error()
769
775
  ? 'border-danger focus-within:border-danger focus-within:ring-2 focus-within:ring-danger/25'
770
776
  : 'border-border focus-within:border-brand-500 focus-within:ring-2 focus-within:ring-brand-500/25');
@@ -1461,7 +1467,7 @@ class SdsLocalizedInput {
1461
1467
  }
1462
1468
 
1463
1469
  <div
1464
- class="relative flex items-stretch rounded-control border bg-white text-ink transition-colors"
1470
+ class="relative flex items-stretch rounded-control border bg-surface text-ink transition-colors"
1465
1471
  [class]="wrapperClasses()"
1466
1472
  >
1467
1473
  @if (multiline()) {
@@ -1545,7 +1551,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
1545
1551
  }
1546
1552
 
1547
1553
  <div
1548
- class="relative flex items-stretch rounded-control border bg-white text-ink transition-colors"
1554
+ class="relative flex items-stretch rounded-control border bg-surface text-ink transition-colors"
1549
1555
  [class]="wrapperClasses()"
1550
1556
  >
1551
1557
  @if (multiline()) {
@@ -1850,7 +1856,7 @@ class SdsOtpInput {
1850
1856
  [disabled]="isDisabled()"
1851
1857
  [attr.aria-label]="cellLabel() + ' ' + (i + 1)"
1852
1858
  [attr.aria-invalid]="error() ? 'true' : null"
1853
- class="bg-white text-center font-semibold tabular-nums outline-none transition-colors
1859
+ class="bg-surface text-center font-semibold tabular-nums outline-none transition-colors
1854
1860
  disabled:cursor-not-allowed"
1855
1861
  [class]="cellClasses()"
1856
1862
  (input)="onInput($event, i)"
@@ -1908,7 +1914,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
1908
1914
  [disabled]="isDisabled()"
1909
1915
  [attr.aria-label]="cellLabel() + ' ' + (i + 1)"
1910
1916
  [attr.aria-invalid]="error() ? 'true' : null"
1911
- class="bg-white text-center font-semibold tabular-nums outline-none transition-colors
1917
+ class="bg-surface text-center font-semibold tabular-nums outline-none transition-colors
1912
1918
  disabled:cursor-not-allowed"
1913
1919
  [class]="cellClasses()"
1914
1920
  (input)="onInput($event, i)"
@@ -1984,7 +1990,7 @@ class SdsTextarea {
1984
1990
  }, ...(ngDevMode ? [{ debugName: "describedBy" }] : []));
1985
1991
  fieldClasses = computed(() => {
1986
1992
  const parts = [
1987
- 'block w-full rounded-control border bg-white text-ink outline-none transition-colors placeholder:text-ink-faint',
1993
+ 'block w-full rounded-control border bg-surface text-ink outline-none transition-colors placeholder:text-ink-faint',
1988
1994
  SIZES$7[this.size()],
1989
1995
  ];
1990
1996
  if (this.autoResize())
@@ -2453,7 +2459,7 @@ class SdsCheckbox {
2453
2459
  const size = this.size() === 'sm' ? 'h-4 w-4' : 'h-5 w-5';
2454
2460
  const state = this.checked()
2455
2461
  ? 'border-brand-600 bg-brand-600 text-white'
2456
- : 'border-border bg-white text-transparent';
2462
+ : 'border-border bg-surface text-transparent';
2457
2463
  const focus = 'focus-within:ring-2 focus-within:ring-brand-500/30 focus-within:border-brand-500';
2458
2464
  return `${size} ${state} ${focus}`;
2459
2465
  }, ...(ngDevMode ? [{ debugName: "boxClasses" }] : []));
@@ -3257,7 +3263,7 @@ class SdsCalendar {
3257
3263
  <div class="mt-2 flex items-center gap-2 border-t border-border pt-2">
3258
3264
  <span class="text-xs font-medium text-ink-soft">{{ timeLabel() }}</span>
3259
3265
  <div class="ml-auto inline-flex items-center gap-1.5">
3260
- <div class="inline-flex items-center rounded-control border border-border bg-white">
3266
+ <div class="inline-flex items-center rounded-control border border-border bg-surface">
3261
3267
  <input
3262
3268
  type="text"
3263
3269
  inputmode="numeric"
@@ -3297,7 +3303,7 @@ class SdsCalendar {
3297
3303
  </div>
3298
3304
  </div>
3299
3305
  <span class="text-sm font-semibold text-ink-soft">:</span>
3300
- <div class="inline-flex items-center rounded-control border border-border bg-white">
3306
+ <div class="inline-flex items-center rounded-control border border-border bg-surface">
3301
3307
  <input
3302
3308
  type="text"
3303
3309
  inputmode="numeric"
@@ -3486,7 +3492,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
3486
3492
  <div class="mt-2 flex items-center gap-2 border-t border-border pt-2">
3487
3493
  <span class="text-xs font-medium text-ink-soft">{{ timeLabel() }}</span>
3488
3494
  <div class="ml-auto inline-flex items-center gap-1.5">
3489
- <div class="inline-flex items-center rounded-control border border-border bg-white">
3495
+ <div class="inline-flex items-center rounded-control border border-border bg-surface">
3490
3496
  <input
3491
3497
  type="text"
3492
3498
  inputmode="numeric"
@@ -3526,7 +3532,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
3526
3532
  </div>
3527
3533
  </div>
3528
3534
  <span class="text-sm font-semibold text-ink-soft">:</span>
3529
- <div class="inline-flex items-center rounded-control border border-border bg-white">
3535
+ <div class="inline-flex items-center rounded-control border border-border bg-surface">
3530
3536
  <input
3531
3537
  type="text"
3532
3538
  inputmode="numeric"
@@ -3901,7 +3907,7 @@ class SdsDatePicker {
3901
3907
  <div class="relative">
3902
3908
  <div
3903
3909
  #trigger
3904
- class="relative flex items-center rounded-control border bg-white transition-colors"
3910
+ class="relative flex items-center rounded-control border bg-surface transition-colors"
3905
3911
  [class]="wrapperClasses()"
3906
3912
  (click)="toggle()"
3907
3913
  >
@@ -4036,7 +4042,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
4036
4042
  <div class="relative">
4037
4043
  <div
4038
4044
  #trigger
4039
- class="relative flex items-center rounded-control border bg-white transition-colors"
4045
+ class="relative flex items-center rounded-control border bg-surface transition-colors"
4040
4046
  [class]="wrapperClasses()"
4041
4047
  (click)="toggle()"
4042
4048
  >
@@ -4130,6 +4136,83 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
4130
4136
  }], 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 }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], min: [{ type: i0.Input, args: [{ isSignal: true, alias: "min", required: false }] }], max: [{ type: i0.Input, args: [{ isSignal: true, alias: "max", required: false }] }], firstDayOfWeek: [{ type: i0.Input, args: [{ isSignal: true, alias: "firstDayOfWeek", required: false }] }], locale: [{ type: i0.Input, args: [{ isSignal: true, alias: "locale", required: false }] }], inline: [{ type: i0.Input, args: [{ isSignal: true, alias: "inline", required: false }] }], clearable: [{ type: i0.Input, args: [{ isSignal: true, alias: "clearable", required: false }] }], showActions: [{ type: i0.Input, args: [{ isSignal: true, alias: "showActions", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], showTime: [{ type: i0.Input, args: [{ isSignal: true, alias: "showTime", required: false }] }], format: [{ type: i0.Input, args: [{ isSignal: true, alias: "format", required: false }] }], timeLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "timeLabel", required: false }] }], hoursLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "hoursLabel", required: false }] }], minutesLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "minutesLabel", required: false }] }], doneLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "doneLabel", required: false }] }], clearLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "clearLabel", required: false }] }], openCalendarLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "openCalendarLabel", required: false }] }], closeCalendarLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "closeCalendarLabel", required: false }] }], panelLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "panelLabel", required: false }] }], calendarLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "calendarLabel", required: false }] }], previousLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "previousLabel", required: false }] }], nextLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "nextLabel", required: false }] }], todayActionLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "todayActionLabel", required: false }] }], clearActionLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "clearActionLabel", required: false }] }], calendar: [{ type: i0.ViewChild, args: [i0.forwardRef(() => SdsCalendar), { isSignal: true }] }], trigger: [{ type: i0.ViewChild, args: ['trigger', { isSignal: true }] }] } });
4131
4137
  let nextId$6 = 0;
4132
4138
 
4139
+ /**
4140
+ * Supplies a custom template for each option row (and the selected value shown
4141
+ * in the trigger) of an `<sds-select>` / `<sds-multi-select>`. Use as a
4142
+ * structural template projected into the select:
4143
+ *
4144
+ * @example
4145
+ * <sds-select [options]="categories" optionLabel="name" optionValue="id" [(ngModel)]="catId">
4146
+ * <ng-template sdsSelectOption let-option let-selected="selected">
4147
+ * <span class="dot" [style.background]="option.color"></span>
4148
+ * <span class="truncate">{{ option.name }}</span>
4149
+ * </ng-template>
4150
+ * </sds-select>
4151
+ */
4152
+ class SdsSelectOption {
4153
+ /** The wrapped template, rendered by the host select via ngTemplateOutlet. */
4154
+ template = inject(TemplateRef);
4155
+ /** Lets the compiler type-check `let-*` bindings against SdsSelectOptionContext. */
4156
+ static ngTemplateContextGuard(_dir, _ctx) {
4157
+ return true;
4158
+ }
4159
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: SdsSelectOption, deps: [], target: i0.ɵɵFactoryTarget.Directive });
4160
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "20.3.25", type: SdsSelectOption, isStandalone: true, selector: "ng-template[sdsSelectOption]", ngImport: i0 });
4161
+ }
4162
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: SdsSelectOption, decorators: [{
4163
+ type: Directive,
4164
+ args: [{
4165
+ selector: 'ng-template[sdsSelectOption]',
4166
+ standalone: true,
4167
+ }]
4168
+ }] });
4169
+ /**
4170
+ * Supplies a custom template shown in place of the default "no results" message
4171
+ * when a select has no (matching) options.
4172
+ *
4173
+ * @example
4174
+ * <ng-template sdsSelectEmpty let-q>No category matches "{{ q }}".</ng-template>
4175
+ */
4176
+ class SdsSelectEmpty {
4177
+ template = inject(TemplateRef);
4178
+ static ngTemplateContextGuard(_dir, _ctx) {
4179
+ return true;
4180
+ }
4181
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: SdsSelectEmpty, deps: [], target: i0.ɵɵFactoryTarget.Directive });
4182
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "20.3.25", type: SdsSelectEmpty, isStandalone: true, selector: "ng-template[sdsSelectEmpty]", ngImport: i0 });
4183
+ }
4184
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: SdsSelectEmpty, decorators: [{
4185
+ type: Directive,
4186
+ args: [{
4187
+ selector: 'ng-template[sdsSelectEmpty]',
4188
+ standalone: true,
4189
+ }]
4190
+ }] });
4191
+ /**
4192
+ * Supplies a custom sticky footer rendered below the option list — e.g. a
4193
+ * "+ Create new" action. The `close` context callback dismisses the panel.
4194
+ *
4195
+ * @example
4196
+ * <ng-template sdsSelectFooter let-close="close">
4197
+ * <button (click)="createCategory(); close()">+ New category</button>
4198
+ * </ng-template>
4199
+ */
4200
+ class SdsSelectFooter {
4201
+ template = inject(TemplateRef);
4202
+ static ngTemplateContextGuard(_dir, _ctx) {
4203
+ return true;
4204
+ }
4205
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: SdsSelectFooter, deps: [], target: i0.ɵɵFactoryTarget.Directive });
4206
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "20.3.25", type: SdsSelectFooter, isStandalone: true, selector: "ng-template[sdsSelectFooter]", ngImport: i0 });
4207
+ }
4208
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: SdsSelectFooter, decorators: [{
4209
+ type: Directive,
4210
+ args: [{
4211
+ selector: 'ng-template[sdsSelectFooter]',
4212
+ standalone: true,
4213
+ }]
4214
+ }] });
4215
+
4133
4216
  const SIZES$4 = {
4134
4217
  sm: 'h-9 text-sm',
4135
4218
  md: 'h-10 text-sm',
@@ -4146,9 +4229,25 @@ let nextId$5 = 0;
4146
4229
  * key; otherwise selection matching falls back to reference equality (a written
4147
4230
  * copy of an option will not match).
4148
4231
  *
4232
+ * Options render as plain text by default. To render richer options (an icon,
4233
+ * an avatar, a two-line row…) project template directives — the same content
4234
+ * also renders in the trigger for the selected value:
4235
+ * - `<ng-template sdsSelectOption let-option>` — one option row (see {@link SdsSelectOption});
4236
+ * context: the raw `option` (implicit), plus `label`, `value`, `selected`, `disabled`, `index`.
4237
+ * - `<ng-template sdsSelectEmpty let-query>` — replaces the "no results" message (see {@link SdsSelectEmpty}).
4238
+ * - `<ng-template sdsSelectFooter let-close="close">` — a sticky footer action, e.g. "+ New…"
4239
+ * (see {@link SdsSelectFooter}); `close()` dismisses the panel.
4240
+ *
4149
4241
  * @example
4150
4242
  * <sds-select [options]="services" optionLabel="name" optionValue="id"
4151
4243
  * placeholder="Choisir un service" [(ngModel)]="serviceId" filter clearable />
4244
+ *
4245
+ * @example
4246
+ * <sds-select [options]="categories" optionLabel="name" optionValue="id" [(ngModel)]="categoryId">
4247
+ * <ng-template sdsSelectOption let-option>
4248
+ * <span class="dot" [style.background]="option.color"></span> {{ option.name }}
4249
+ * </ng-template>
4250
+ * </sds-select>
4152
4251
  */
4153
4252
  class SdsSelect {
4154
4253
  host = inject(ElementRef);
@@ -4173,6 +4272,12 @@ class SdsSelect {
4173
4272
  filter = input(false, ...(ngDevMode ? [{ debugName: "filter", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
4174
4273
  filterPlaceholder = input('Rechercher…', ...(ngDevMode ? [{ debugName: "filterPlaceholder" }] : []));
4175
4274
  emptyMessage = input('Aucun résultat', ...(ngDevMode ? [{ debugName: "emptyMessage" }] : []));
4275
+ /** Optional projected `<ng-template sdsSelectOption>` — custom option + selected-value rendering. */
4276
+ optionTpl = contentChild(SdsSelectOption, ...(ngDevMode ? [{ debugName: "optionTpl" }] : []));
4277
+ /** Optional projected `<ng-template sdsSelectEmpty>` — custom no-results content. */
4278
+ emptyTpl = contentChild(SdsSelectEmpty, ...(ngDevMode ? [{ debugName: "emptyTpl" }] : []));
4279
+ /** Optional projected `<ng-template sdsSelectFooter>` — a sticky footer action. */
4280
+ footerTpl = contentChild(SdsSelectFooter, ...(ngDevMode ? [{ debugName: "footerTpl" }] : []));
4176
4281
  value = signal(null, ...(ngDevMode ? [{ debugName: "value" }] : []));
4177
4282
  open = signal(false, ...(ngDevMode ? [{ debugName: "open" }] : []));
4178
4283
  filterText = signal('', ...(ngDevMode ? [{ debugName: "filterText" }] : []));
@@ -4211,7 +4316,7 @@ class SdsSelect {
4211
4316
  : 'border-white/15 focus:border-white/40 focus:ring-2 focus:ring-white/20');
4212
4317
  }
4213
4318
  else {
4214
- parts.push('bg-white text-ink');
4319
+ parts.push('bg-surface text-ink');
4215
4320
  parts.push(this.error()
4216
4321
  ? 'border-danger focus:border-danger focus:ring-2 focus:ring-danger/25'
4217
4322
  : 'border-border focus:border-brand-500 focus:ring-2 focus:ring-brand-500/25');
@@ -4229,9 +4334,30 @@ class SdsSelect {
4229
4334
  const label = labelKey && labelKey in o ? String(o[labelKey]) : String(o['label'] ?? raw);
4230
4335
  const value = this.optionValue() ? o[this.optionValue()] : raw;
4231
4336
  const disabled = this.optionDisabled() ? !!o[this.optionDisabled()] : !!o['disabled'];
4232
- return { label, value, disabled };
4337
+ return { label, value, disabled, raw };
4233
4338
  }
4234
- return { label: String(raw), value: raw, disabled: false };
4339
+ return { label: String(raw), value: raw, disabled: false, raw };
4340
+ }
4341
+ /** Context for a projected option template (an option row). */
4342
+ optionContext(opt, index) {
4343
+ return {
4344
+ $implicit: opt.raw,
4345
+ option: opt.raw,
4346
+ label: opt.label,
4347
+ value: opt.value,
4348
+ selected: this.isSelected(opt.value),
4349
+ disabled: opt.disabled,
4350
+ index,
4351
+ };
4352
+ }
4353
+ /** Context for the option template reused in the trigger to render the selected value. */
4354
+ triggerContext = computed(() => {
4355
+ const sel = this.selectedOption();
4356
+ return sel ? this.optionContext(sel, -1) : null;
4357
+ }, ...(ngDevMode ? [{ debugName: "triggerContext" }] : []));
4358
+ /** Context for a projected footer template. */
4359
+ footerContext() {
4360
+ return { $implicit: this.filterText(), filterText: this.filterText(), close: () => this.close(true) };
4235
4361
  }
4236
4362
  equals(a, b) {
4237
4363
  const k = this.dataKey();
@@ -4503,7 +4629,7 @@ class SdsSelect {
4503
4629
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: SdsSelect, deps: [], target: i0.ɵɵFactoryTarget.Component });
4504
4630
  static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.25", type: SdsSelect, isStandalone: true, selector: "sds-select", inputs: { options: { classPropertyName: "options", publicName: "options", isSignal: true, isRequired: false, transformFunction: null }, optionLabel: { classPropertyName: "optionLabel", publicName: "optionLabel", isSignal: true, isRequired: false, transformFunction: null }, optionValue: { classPropertyName: "optionValue", publicName: "optionValue", isSignal: true, isRequired: false, transformFunction: null }, optionDisabled: { classPropertyName: "optionDisabled", publicName: "optionDisabled", isSignal: true, isRequired: false, transformFunction: null }, dataKey: { classPropertyName: "dataKey", publicName: "dataKey", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", 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 }, 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 }, filter: { classPropertyName: "filter", publicName: "filter", isSignal: true, isRequired: false, transformFunction: null }, filterPlaceholder: { classPropertyName: "filterPlaceholder", publicName: "filterPlaceholder", isSignal: true, isRequired: false, transformFunction: null }, emptyMessage: { classPropertyName: "emptyMessage", publicName: "emptyMessage", isSignal: true, isRequired: false, transformFunction: null } }, host: { listeners: { "document:click": "onDocumentClick($event)" }, classAttribute: "block" }, providers: [
4505
4631
  { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => SdsSelect), multi: true },
4506
- ], viewQueries: [{ propertyName: "trigger", first: true, predicate: ["trigger"], descendants: true, isSignal: true }], ngImport: i0, template: `
4632
+ ], queries: [{ propertyName: "optionTpl", first: true, predicate: SdsSelectOption, descendants: true, isSignal: true }, { propertyName: "emptyTpl", first: true, predicate: SdsSelectEmpty, descendants: true, isSignal: true }, { propertyName: "footerTpl", first: true, predicate: SdsSelectFooter, descendants: true, isSignal: true }], viewQueries: [{ propertyName: "trigger", first: true, predicate: ["trigger"], descendants: true, isSignal: true }], ngImport: i0, template: `
4507
4633
  @if (label()) {
4508
4634
  <label [for]="selectId" class="mb-1.5 block text-sm font-medium text-ink">
4509
4635
  {{ label() }}
@@ -4526,12 +4652,21 @@ class SdsSelect {
4526
4652
  (click)="toggle()"
4527
4653
  (keydown)="onTriggerKeydown($event)"
4528
4654
  >
4529
- <span
4530
- class="flex-1 truncate pl-3.5"
4531
- [class]="selectedOption() ? 'text-ink' : 'text-ink-faint'"
4532
- >
4533
- {{ selectedOption() ? selectedOption()!.label : placeholder() }}
4534
- </span>
4655
+ @if (optionTpl() && selectedOption()) {
4656
+ <span class="flex flex-1 items-center gap-2 truncate pl-3.5 text-ink">
4657
+ <ng-container
4658
+ [ngTemplateOutlet]="optionTpl()!.template"
4659
+ [ngTemplateOutletContext]="triggerContext()!"
4660
+ />
4661
+ </span>
4662
+ } @else {
4663
+ <span
4664
+ class="flex-1 truncate pl-3.5"
4665
+ [class]="selectedOption() ? 'text-ink' : 'text-ink-faint'"
4666
+ >
4667
+ {{ selectedOption() ? selectedOption()!.label : placeholder() }}
4668
+ </span>
4669
+ }
4535
4670
 
4536
4671
  @if (showClear()) {
4537
4672
  <button
@@ -4589,15 +4724,42 @@ class SdsSelect {
4589
4724
  (click)="opt.disabled ? null : selectOption(opt)"
4590
4725
  (mouseenter)="opt.disabled ? null : highlightedIndex.set(i)"
4591
4726
  >
4592
- <span class="truncate">{{ opt.label }}</span>
4727
+ @if (optionTpl()) {
4728
+ <span class="flex min-w-0 flex-1 items-center gap-2">
4729
+ <ng-container
4730
+ [ngTemplateOutlet]="optionTpl()!.template"
4731
+ [ngTemplateOutletContext]="optionContext(opt, i)"
4732
+ />
4733
+ </span>
4734
+ } @else {
4735
+ <span class="truncate">{{ opt.label }}</span>
4736
+ }
4593
4737
  @if (isSelected(opt.value)) {
4594
4738
  <sds-icon name="check" [size]="16" class="shrink-0 text-brand-600" />
4595
4739
  }
4596
4740
  </li>
4597
4741
  } @empty {
4598
- <li class="px-3 py-2 text-sm text-ink-faint">{{ emptyMessage() }}</li>
4742
+ @if (emptyTpl()) {
4743
+ <li class="px-3 py-2 text-sm text-ink-faint">
4744
+ <ng-container
4745
+ [ngTemplateOutlet]="emptyTpl()!.template"
4746
+ [ngTemplateOutletContext]="{ $implicit: filterText(), filterText: filterText() }"
4747
+ />
4748
+ </li>
4749
+ } @else {
4750
+ <li class="px-3 py-2 text-sm text-ink-faint">{{ emptyMessage() }}</li>
4751
+ }
4599
4752
  }
4600
4753
  </ul>
4754
+
4755
+ @if (footerTpl()) {
4756
+ <div class="border-t border-border">
4757
+ <ng-container
4758
+ [ngTemplateOutlet]="footerTpl()!.template"
4759
+ [ngTemplateOutletContext]="footerContext()"
4760
+ />
4761
+ </div>
4762
+ }
4601
4763
  </div>
4602
4764
  }
4603
4765
  </div>
@@ -4608,14 +4770,14 @@ class SdsSelect {
4608
4770
  @if (error()) {
4609
4771
  <p class="mt-1.5 text-xs font-medium text-danger">{{ error() }}</p>
4610
4772
  }
4611
- `, isInline: true, dependencies: [{ kind: "component", type: SdsIcon, selector: "sds-icon", inputs: ["name", "size", "strokeWidth"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
4773
+ `, isInline: true, dependencies: [{ kind: "component", type: SdsIcon, selector: "sds-icon", inputs: ["name", "size", "strokeWidth"] }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
4612
4774
  }
4613
4775
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: SdsSelect, decorators: [{
4614
4776
  type: Component,
4615
4777
  args: [{
4616
4778
  selector: 'sds-select',
4617
4779
  changeDetection: ChangeDetectionStrategy.OnPush,
4618
- imports: [SdsIcon],
4780
+ imports: [SdsIcon, NgTemplateOutlet],
4619
4781
  providers: [
4620
4782
  { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => SdsSelect), multi: true },
4621
4783
  ],
@@ -4646,12 +4808,21 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
4646
4808
  (click)="toggle()"
4647
4809
  (keydown)="onTriggerKeydown($event)"
4648
4810
  >
4649
- <span
4650
- class="flex-1 truncate pl-3.5"
4651
- [class]="selectedOption() ? 'text-ink' : 'text-ink-faint'"
4652
- >
4653
- {{ selectedOption() ? selectedOption()!.label : placeholder() }}
4654
- </span>
4811
+ @if (optionTpl() && selectedOption()) {
4812
+ <span class="flex flex-1 items-center gap-2 truncate pl-3.5 text-ink">
4813
+ <ng-container
4814
+ [ngTemplateOutlet]="optionTpl()!.template"
4815
+ [ngTemplateOutletContext]="triggerContext()!"
4816
+ />
4817
+ </span>
4818
+ } @else {
4819
+ <span
4820
+ class="flex-1 truncate pl-3.5"
4821
+ [class]="selectedOption() ? 'text-ink' : 'text-ink-faint'"
4822
+ >
4823
+ {{ selectedOption() ? selectedOption()!.label : placeholder() }}
4824
+ </span>
4825
+ }
4655
4826
 
4656
4827
  @if (showClear()) {
4657
4828
  <button
@@ -4709,15 +4880,42 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
4709
4880
  (click)="opt.disabled ? null : selectOption(opt)"
4710
4881
  (mouseenter)="opt.disabled ? null : highlightedIndex.set(i)"
4711
4882
  >
4712
- <span class="truncate">{{ opt.label }}</span>
4883
+ @if (optionTpl()) {
4884
+ <span class="flex min-w-0 flex-1 items-center gap-2">
4885
+ <ng-container
4886
+ [ngTemplateOutlet]="optionTpl()!.template"
4887
+ [ngTemplateOutletContext]="optionContext(opt, i)"
4888
+ />
4889
+ </span>
4890
+ } @else {
4891
+ <span class="truncate">{{ opt.label }}</span>
4892
+ }
4713
4893
  @if (isSelected(opt.value)) {
4714
4894
  <sds-icon name="check" [size]="16" class="shrink-0 text-brand-600" />
4715
4895
  }
4716
4896
  </li>
4717
4897
  } @empty {
4718
- <li class="px-3 py-2 text-sm text-ink-faint">{{ emptyMessage() }}</li>
4898
+ @if (emptyTpl()) {
4899
+ <li class="px-3 py-2 text-sm text-ink-faint">
4900
+ <ng-container
4901
+ [ngTemplateOutlet]="emptyTpl()!.template"
4902
+ [ngTemplateOutletContext]="{ $implicit: filterText(), filterText: filterText() }"
4903
+ />
4904
+ </li>
4905
+ } @else {
4906
+ <li class="px-3 py-2 text-sm text-ink-faint">{{ emptyMessage() }}</li>
4907
+ }
4719
4908
  }
4720
4909
  </ul>
4910
+
4911
+ @if (footerTpl()) {
4912
+ <div class="border-t border-border">
4913
+ <ng-container
4914
+ [ngTemplateOutlet]="footerTpl()!.template"
4915
+ [ngTemplateOutletContext]="footerContext()"
4916
+ />
4917
+ </div>
4918
+ }
4721
4919
  </div>
4722
4920
  }
4723
4921
  </div>
@@ -4730,7 +4928,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
4730
4928
  }
4731
4929
  `,
4732
4930
  }]
4733
- }], propDecorators: { options: [{ type: i0.Input, args: [{ isSignal: true, alias: "options", required: false }] }], optionLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "optionLabel", required: false }] }], optionValue: [{ type: i0.Input, args: [{ isSignal: true, alias: "optionValue", required: false }] }], optionDisabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "optionDisabled", required: false }] }], dataKey: [{ type: i0.Input, args: [{ isSignal: true, alias: "dataKey", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], hint: [{ type: i0.Input, args: [{ isSignal: true, alias: "hint", required: false }] }], error: [{ type: i0.Input, args: [{ isSignal: true, alias: "error", 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 }] }], filter: [{ type: i0.Input, args: [{ isSignal: true, alias: "filter", required: false }] }], filterPlaceholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "filterPlaceholder", required: false }] }], emptyMessage: [{ type: i0.Input, args: [{ isSignal: true, alias: "emptyMessage", required: false }] }], trigger: [{ type: i0.ViewChild, args: ['trigger', { isSignal: true }] }] } });
4931
+ }], propDecorators: { options: [{ type: i0.Input, args: [{ isSignal: true, alias: "options", required: false }] }], optionLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "optionLabel", required: false }] }], optionValue: [{ type: i0.Input, args: [{ isSignal: true, alias: "optionValue", required: false }] }], optionDisabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "optionDisabled", required: false }] }], dataKey: [{ type: i0.Input, args: [{ isSignal: true, alias: "dataKey", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], hint: [{ type: i0.Input, args: [{ isSignal: true, alias: "hint", required: false }] }], error: [{ type: i0.Input, args: [{ isSignal: true, alias: "error", 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 }] }], filter: [{ type: i0.Input, args: [{ isSignal: true, alias: "filter", required: false }] }], filterPlaceholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "filterPlaceholder", required: false }] }], emptyMessage: [{ type: i0.Input, args: [{ isSignal: true, alias: "emptyMessage", required: false }] }], optionTpl: [{ type: i0.ContentChild, args: [i0.forwardRef(() => SdsSelectOption), { isSignal: true }] }], emptyTpl: [{ type: i0.ContentChild, args: [i0.forwardRef(() => SdsSelectEmpty), { isSignal: true }] }], footerTpl: [{ type: i0.ContentChild, args: [i0.forwardRef(() => SdsSelectFooter), { isSignal: true }] }], trigger: [{ type: i0.ViewChild, args: ['trigger', { isSignal: true }] }] } });
4734
4932
 
4735
4933
  const SIZES$3 = {
4736
4934
  sm: 'min-h-9 text-sm',
@@ -4748,9 +4946,24 @@ let nextId$4 = 0;
4748
4946
  * For object options, set [optionValue] or [dataKey] so written values match by
4749
4947
  * key; otherwise matching falls back to reference equality.
4750
4948
  *
4949
+ * Project template directives to customise the panel:
4950
+ * - `<ng-template sdsSelectOption let-option>` — the content of each option row, after its
4951
+ * checkbox (see {@link SdsSelectOption}); context: the raw `option` (implicit), plus `label`,
4952
+ * `value`, `selected`, `disabled`, `index`.
4953
+ * - `<ng-template sdsSelectEmpty let-query>` — replaces the "no results" message (see {@link SdsSelectEmpty}).
4954
+ * - `<ng-template sdsSelectFooter let-close="close">` — a sticky footer action (see {@link SdsSelectFooter});
4955
+ * `close()` dismisses the panel.
4956
+ *
4751
4957
  * @example
4752
4958
  * <sds-multi-select [options]="services" optionLabel="name" optionValue="code"
4753
4959
  * display="chip" filter [(ngModel)]="codes" />
4960
+ *
4961
+ * @example
4962
+ * <sds-multi-select [options]="tags" optionLabel="name" optionValue="id" [(ngModel)]="tagIds">
4963
+ * <ng-template sdsSelectOption let-option>
4964
+ * <span class="dot" [style.background]="option.color"></span> {{ option.name }}
4965
+ * </ng-template>
4966
+ * </sds-multi-select>
4754
4967
  */
4755
4968
  class SdsMultiSelect {
4756
4969
  host = inject(ElementRef);
@@ -4778,6 +4991,12 @@ class SdsMultiSelect {
4778
4991
  filter = input(false, ...(ngDevMode ? [{ debugName: "filter", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
4779
4992
  filterPlaceholder = input('Rechercher…', ...(ngDevMode ? [{ debugName: "filterPlaceholder" }] : []));
4780
4993
  emptyMessage = input('Aucun résultat', ...(ngDevMode ? [{ debugName: "emptyMessage" }] : []));
4994
+ /** Optional projected `<ng-template sdsSelectOption>` — custom option-row rendering. */
4995
+ optionTpl = contentChild(SdsSelectOption, ...(ngDevMode ? [{ debugName: "optionTpl" }] : []));
4996
+ /** Optional projected `<ng-template sdsSelectEmpty>` — custom no-results content. */
4997
+ emptyTpl = contentChild(SdsSelectEmpty, ...(ngDevMode ? [{ debugName: "emptyTpl" }] : []));
4998
+ /** Optional projected `<ng-template sdsSelectFooter>` — a sticky footer action. */
4999
+ footerTpl = contentChild(SdsSelectFooter, ...(ngDevMode ? [{ debugName: "footerTpl" }] : []));
4781
5000
  value = signal([], ...(ngDevMode ? [{ debugName: "value" }] : []));
4782
5001
  open = signal(false, ...(ngDevMode ? [{ debugName: "open" }] : []));
4783
5002
  filterText = signal('', ...(ngDevMode ? [{ debugName: "filterText" }] : []));
@@ -4843,9 +5062,25 @@ class SdsMultiSelect {
4843
5062
  const label = labelKey && labelKey in o ? String(o[labelKey]) : String(o['label'] ?? raw);
4844
5063
  const value = this.optionValue() ? o[this.optionValue()] : raw;
4845
5064
  const disabled = this.optionDisabled() ? !!o[this.optionDisabled()] : !!o['disabled'];
4846
- return { label, value, disabled };
5065
+ return { label, value, disabled, raw };
4847
5066
  }
4848
- return { label: String(raw), value: raw, disabled: false };
5067
+ return { label: String(raw), value: raw, disabled: false, raw };
5068
+ }
5069
+ /** Context for a projected option template (an option row). */
5070
+ optionContext(opt, index) {
5071
+ return {
5072
+ $implicit: opt.raw,
5073
+ option: opt.raw,
5074
+ label: opt.label,
5075
+ value: opt.value,
5076
+ selected: this.isSelected(opt.value),
5077
+ disabled: opt.disabled,
5078
+ index,
5079
+ };
5080
+ }
5081
+ /** Context for a projected footer template. */
5082
+ footerContext() {
5083
+ return { $implicit: this.filterText(), filterText: this.filterText(), close: () => this.close(true) };
4849
5084
  }
4850
5085
  equals(a, b) {
4851
5086
  const k = this.dataKey();
@@ -4874,7 +5109,7 @@ class SdsMultiSelect {
4874
5109
  checkboxClass(checked, indeterminate) {
4875
5110
  return checked || indeterminate
4876
5111
  ? 'border-brand-600 bg-brand-600 text-white'
4877
- : 'border-border bg-white';
5112
+ : 'border-border bg-surface';
4878
5113
  }
4879
5114
  // --- open / close ---
4880
5115
  toggle() {
@@ -5157,7 +5392,7 @@ class SdsMultiSelect {
5157
5392
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: SdsMultiSelect, deps: [], target: i0.ɵɵFactoryTarget.Component });
5158
5393
  static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.25", type: SdsMultiSelect, isStandalone: true, selector: "sds-multi-select", inputs: { options: { classPropertyName: "options", publicName: "options", isSignal: true, isRequired: false, transformFunction: null }, optionLabel: { classPropertyName: "optionLabel", publicName: "optionLabel", isSignal: true, isRequired: false, transformFunction: null }, optionValue: { classPropertyName: "optionValue", publicName: "optionValue", isSignal: true, isRequired: false, transformFunction: null }, optionDisabled: { classPropertyName: "optionDisabled", publicName: "optionDisabled", isSignal: true, isRequired: false, transformFunction: null }, dataKey: { classPropertyName: "dataKey", publicName: "dataKey", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", 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 }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, display: { classPropertyName: "display", publicName: "display", isSignal: true, isRequired: false, transformFunction: null }, maxSelectedLabels: { classPropertyName: "maxSelectedLabels", publicName: "maxSelectedLabels", isSignal: true, isRequired: false, transformFunction: null }, selectedItemsLabel: { classPropertyName: "selectedItemsLabel", publicName: "selectedItemsLabel", isSignal: true, isRequired: false, transformFunction: null }, showToggleAll: { classPropertyName: "showToggleAll", publicName: "showToggleAll", 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 }, filter: { classPropertyName: "filter", publicName: "filter", isSignal: true, isRequired: false, transformFunction: null }, filterPlaceholder: { classPropertyName: "filterPlaceholder", publicName: "filterPlaceholder", isSignal: true, isRequired: false, transformFunction: null }, emptyMessage: { classPropertyName: "emptyMessage", publicName: "emptyMessage", isSignal: true, isRequired: false, transformFunction: null } }, host: { listeners: { "document:click": "onDocumentClick($event)" }, classAttribute: "block" }, providers: [
5159
5394
  { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => SdsMultiSelect), multi: true },
5160
- ], viewQueries: [{ propertyName: "trigger", first: true, predicate: ["trigger"], descendants: true, isSignal: true }], ngImport: i0, template: `
5395
+ ], queries: [{ propertyName: "optionTpl", first: true, predicate: SdsSelectOption, descendants: true, isSignal: true }, { propertyName: "emptyTpl", first: true, predicate: SdsSelectEmpty, descendants: true, isSignal: true }, { propertyName: "footerTpl", first: true, predicate: SdsSelectFooter, descendants: true, isSignal: true }], viewQueries: [{ propertyName: "trigger", first: true, predicate: ["trigger"], descendants: true, isSignal: true }], ngImport: i0, template: `
5161
5396
  @if (label()) {
5162
5397
  <label [for]="selectId" class="mb-1.5 block text-sm font-medium text-ink">
5163
5398
  {{ label() }}
@@ -5175,7 +5410,7 @@ class SdsMultiSelect {
5175
5410
  [attr.aria-haspopup]="'listbox'"
5176
5411
  [attr.aria-activedescendant]="open() && !filter() ? activeDescId() : null"
5177
5412
  [attr.aria-label]="!label() && ariaLabel() ? ariaLabel() : null"
5178
- class="relative flex items-center rounded-control border bg-white transition-colors focus:outline-none"
5413
+ class="relative flex items-center rounded-control border bg-surface transition-colors focus:outline-none"
5179
5414
  [class]="wrapperClasses()"
5180
5415
  (click)="toggle()"
5181
5416
  (keydown)="onTriggerKeydown($event)"
@@ -5290,12 +5525,39 @@ class SdsMultiSelect {
5290
5525
  <sds-icon name="check" [size]="12" />
5291
5526
  }
5292
5527
  </span>
5293
- <span class="truncate">{{ opt.label }}</span>
5528
+ @if (optionTpl()) {
5529
+ <span class="flex min-w-0 flex-1 items-center gap-2">
5530
+ <ng-container
5531
+ [ngTemplateOutlet]="optionTpl()!.template"
5532
+ [ngTemplateOutletContext]="optionContext(opt, i)"
5533
+ />
5534
+ </span>
5535
+ } @else {
5536
+ <span class="truncate">{{ opt.label }}</span>
5537
+ }
5294
5538
  </li>
5295
5539
  } @empty {
5296
- <li class="px-3 py-2 text-sm text-ink-faint">{{ emptyMessage() }}</li>
5540
+ @if (emptyTpl()) {
5541
+ <li class="px-3 py-2 text-sm text-ink-faint">
5542
+ <ng-container
5543
+ [ngTemplateOutlet]="emptyTpl()!.template"
5544
+ [ngTemplateOutletContext]="{ $implicit: filterText(), filterText: filterText() }"
5545
+ />
5546
+ </li>
5547
+ } @else {
5548
+ <li class="px-3 py-2 text-sm text-ink-faint">{{ emptyMessage() }}</li>
5549
+ }
5297
5550
  }
5298
5551
  </ul>
5552
+
5553
+ @if (footerTpl()) {
5554
+ <div class="border-t border-border">
5555
+ <ng-container
5556
+ [ngTemplateOutlet]="footerTpl()!.template"
5557
+ [ngTemplateOutletContext]="footerContext()"
5558
+ />
5559
+ </div>
5560
+ }
5299
5561
  </div>
5300
5562
  }
5301
5563
  </div>
@@ -5306,14 +5568,14 @@ class SdsMultiSelect {
5306
5568
  @if (error()) {
5307
5569
  <p class="mt-1.5 text-xs font-medium text-danger">{{ error() }}</p>
5308
5570
  }
5309
- `, isInline: true, dependencies: [{ kind: "component", type: SdsIcon, selector: "sds-icon", inputs: ["name", "size", "strokeWidth"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
5571
+ `, isInline: true, dependencies: [{ kind: "component", type: SdsIcon, selector: "sds-icon", inputs: ["name", "size", "strokeWidth"] }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
5310
5572
  }
5311
5573
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: SdsMultiSelect, decorators: [{
5312
5574
  type: Component,
5313
5575
  args: [{
5314
5576
  selector: 'sds-multi-select',
5315
5577
  changeDetection: ChangeDetectionStrategy.OnPush,
5316
- imports: [SdsIcon],
5578
+ imports: [SdsIcon, NgTemplateOutlet],
5317
5579
  providers: [
5318
5580
  { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => SdsMultiSelect), multi: true },
5319
5581
  ],
@@ -5339,7 +5601,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
5339
5601
  [attr.aria-haspopup]="'listbox'"
5340
5602
  [attr.aria-activedescendant]="open() && !filter() ? activeDescId() : null"
5341
5603
  [attr.aria-label]="!label() && ariaLabel() ? ariaLabel() : null"
5342
- class="relative flex items-center rounded-control border bg-white transition-colors focus:outline-none"
5604
+ class="relative flex items-center rounded-control border bg-surface transition-colors focus:outline-none"
5343
5605
  [class]="wrapperClasses()"
5344
5606
  (click)="toggle()"
5345
5607
  (keydown)="onTriggerKeydown($event)"
@@ -5454,12 +5716,39 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
5454
5716
  <sds-icon name="check" [size]="12" />
5455
5717
  }
5456
5718
  </span>
5457
- <span class="truncate">{{ opt.label }}</span>
5719
+ @if (optionTpl()) {
5720
+ <span class="flex min-w-0 flex-1 items-center gap-2">
5721
+ <ng-container
5722
+ [ngTemplateOutlet]="optionTpl()!.template"
5723
+ [ngTemplateOutletContext]="optionContext(opt, i)"
5724
+ />
5725
+ </span>
5726
+ } @else {
5727
+ <span class="truncate">{{ opt.label }}</span>
5728
+ }
5458
5729
  </li>
5459
5730
  } @empty {
5460
- <li class="px-3 py-2 text-sm text-ink-faint">{{ emptyMessage() }}</li>
5731
+ @if (emptyTpl()) {
5732
+ <li class="px-3 py-2 text-sm text-ink-faint">
5733
+ <ng-container
5734
+ [ngTemplateOutlet]="emptyTpl()!.template"
5735
+ [ngTemplateOutletContext]="{ $implicit: filterText(), filterText: filterText() }"
5736
+ />
5737
+ </li>
5738
+ } @else {
5739
+ <li class="px-3 py-2 text-sm text-ink-faint">{{ emptyMessage() }}</li>
5740
+ }
5461
5741
  }
5462
5742
  </ul>
5743
+
5744
+ @if (footerTpl()) {
5745
+ <div class="border-t border-border">
5746
+ <ng-container
5747
+ [ngTemplateOutlet]="footerTpl()!.template"
5748
+ [ngTemplateOutletContext]="footerContext()"
5749
+ />
5750
+ </div>
5751
+ }
5463
5752
  </div>
5464
5753
  }
5465
5754
  </div>
@@ -5472,7 +5761,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
5472
5761
  }
5473
5762
  `,
5474
5763
  }]
5475
- }], propDecorators: { options: [{ type: i0.Input, args: [{ isSignal: true, alias: "options", required: false }] }], optionLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "optionLabel", required: false }] }], optionValue: [{ type: i0.Input, args: [{ isSignal: true, alias: "optionValue", required: false }] }], optionDisabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "optionDisabled", required: false }] }], dataKey: [{ type: i0.Input, args: [{ isSignal: true, alias: "dataKey", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], hint: [{ type: i0.Input, args: [{ isSignal: true, alias: "hint", required: false }] }], error: [{ type: i0.Input, args: [{ isSignal: true, alias: "error", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], display: [{ type: i0.Input, args: [{ isSignal: true, alias: "display", required: false }] }], maxSelectedLabels: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxSelectedLabels", required: false }] }], selectedItemsLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "selectedItemsLabel", required: false }] }], showToggleAll: [{ type: i0.Input, args: [{ isSignal: true, alias: "showToggleAll", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], clearable: [{ type: i0.Input, args: [{ isSignal: true, alias: "clearable", required: false }] }], filter: [{ type: i0.Input, args: [{ isSignal: true, alias: "filter", required: false }] }], filterPlaceholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "filterPlaceholder", required: false }] }], emptyMessage: [{ type: i0.Input, args: [{ isSignal: true, alias: "emptyMessage", required: false }] }], trigger: [{ type: i0.ViewChild, args: ['trigger', { isSignal: true }] }] } });
5764
+ }], propDecorators: { options: [{ type: i0.Input, args: [{ isSignal: true, alias: "options", required: false }] }], optionLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "optionLabel", required: false }] }], optionValue: [{ type: i0.Input, args: [{ isSignal: true, alias: "optionValue", required: false }] }], optionDisabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "optionDisabled", required: false }] }], dataKey: [{ type: i0.Input, args: [{ isSignal: true, alias: "dataKey", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], hint: [{ type: i0.Input, args: [{ isSignal: true, alias: "hint", required: false }] }], error: [{ type: i0.Input, args: [{ isSignal: true, alias: "error", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], display: [{ type: i0.Input, args: [{ isSignal: true, alias: "display", required: false }] }], maxSelectedLabels: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxSelectedLabels", required: false }] }], selectedItemsLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "selectedItemsLabel", required: false }] }], showToggleAll: [{ type: i0.Input, args: [{ isSignal: true, alias: "showToggleAll", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], clearable: [{ type: i0.Input, args: [{ isSignal: true, alias: "clearable", required: false }] }], filter: [{ type: i0.Input, args: [{ isSignal: true, alias: "filter", required: false }] }], filterPlaceholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "filterPlaceholder", required: false }] }], emptyMessage: [{ type: i0.Input, args: [{ isSignal: true, alias: "emptyMessage", required: false }] }], optionTpl: [{ type: i0.ContentChild, args: [i0.forwardRef(() => SdsSelectOption), { isSignal: true }] }], emptyTpl: [{ type: i0.ContentChild, args: [i0.forwardRef(() => SdsSelectEmpty), { isSignal: true }] }], footerTpl: [{ type: i0.ContentChild, args: [i0.forwardRef(() => SdsSelectFooter), { isSignal: true }] }], trigger: [{ type: i0.ViewChild, args: ['trigger', { isSignal: true }] }] } });
5476
5765
 
5477
5766
  const SDS_PHONE_COUNTRIES = [
5478
5767
  { iso2: 'BE', name: 'Belgique', dialCode: '32', lengths: [9], format: '### ## ## ##', example: '470123456' },
@@ -5990,7 +6279,7 @@ class SdsPhoneInput {
5990
6279
 
5991
6280
  <div
5992
6281
  #anchor
5993
- class="relative flex items-stretch rounded-control border bg-white transition-colors"
6282
+ class="relative flex items-stretch rounded-control border bg-surface transition-colors"
5994
6283
  [class]="wrapperClasses()"
5995
6284
  >
5996
6285
  <!-- country selector -->
@@ -6131,7 +6420,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
6131
6420
 
6132
6421
  <div
6133
6422
  #anchor
6134
- class="relative flex items-stretch rounded-control border bg-white transition-colors"
6423
+ class="relative flex items-stretch rounded-control border bg-surface transition-colors"
6135
6424
  [class]="wrapperClasses()"
6136
6425
  >
6137
6426
  <!-- country selector -->
@@ -6520,7 +6809,7 @@ class SdsPaginator {
6520
6809
  [disabled]="disabled()"
6521
6810
  [attr.aria-label]="jumpToPageAriaLabel()"
6522
6811
  (change)="onJump($event)"
6523
- class="h-9 w-16 rounded-control border border-border bg-white px-2 text-center text-sm text-ink outline-none transition-colors focus:border-brand-500 focus:ring-2 focus:ring-brand-500/25 disabled:opacity-60"
6812
+ class="h-9 w-16 rounded-control border border-border bg-surface px-2 text-center text-sm text-ink outline-none transition-colors focus:border-brand-500 focus:ring-2 focus:ring-brand-500/25 disabled:opacity-60"
6524
6813
  />
6525
6814
  </label>
6526
6815
  }
@@ -6629,7 +6918,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
6629
6918
  [disabled]="disabled()"
6630
6919
  [attr.aria-label]="jumpToPageAriaLabel()"
6631
6920
  (change)="onJump($event)"
6632
- class="h-9 w-16 rounded-control border border-border bg-white px-2 text-center text-sm text-ink outline-none transition-colors focus:border-brand-500 focus:ring-2 focus:ring-brand-500/25 disabled:opacity-60"
6921
+ class="h-9 w-16 rounded-control border border-border bg-surface px-2 text-center text-sm text-ink outline-none transition-colors focus:border-brand-500 focus:ring-2 focus:ring-brand-500/25 disabled:opacity-60"
6633
6922
  />
6634
6923
  </label>
6635
6924
  }
@@ -7805,11 +8094,11 @@ class SdsTable {
7805
8094
  checkboxClass(checked, indeterminate) {
7806
8095
  return checked || indeterminate
7807
8096
  ? 'border-brand-600 bg-brand-600 text-white'
7808
- : 'border-border bg-white';
8097
+ : 'border-border bg-surface';
7809
8098
  }
7810
8099
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: SdsTable, deps: [], target: i0.ɵɵFactoryTarget.Component });
7811
8100
  static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.25", type: SdsTable, isStandalone: true, selector: "sds-table", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, columns: { classPropertyName: "columns", publicName: "columns", isSignal: true, isRequired: false, transformFunction: null }, dataKey: { classPropertyName: "dataKey", publicName: "dataKey", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, striped: { classPropertyName: "striped", publicName: "striped", isSignal: true, isRequired: false, transformFunction: null }, gridlines: { classPropertyName: "gridlines", publicName: "gridlines", isSignal: true, isRequired: false, transformFunction: null }, hoverable: { classPropertyName: "hoverable", publicName: "hoverable", isSignal: true, isRequired: false, transformFunction: null }, sortable: { classPropertyName: "sortable", publicName: "sortable", isSignal: true, isRequired: false, transformFunction: null }, paginator: { classPropertyName: "paginator", publicName: "paginator", isSignal: true, isRequired: false, transformFunction: null }, rows: { classPropertyName: "rows", publicName: "rows", isSignal: true, isRequired: false, transformFunction: null }, rowsPerPageOptions: { classPropertyName: "rowsPerPageOptions", publicName: "rowsPerPageOptions", isSignal: true, isRequired: false, transformFunction: null }, showCurrentPageReport: { classPropertyName: "showCurrentPageReport", publicName: "showCurrentPageReport", isSignal: true, isRequired: false, transformFunction: null }, currentPageReportTemplate: { classPropertyName: "currentPageReportTemplate", publicName: "currentPageReportTemplate", isSignal: true, isRequired: false, transformFunction: null }, rowsPerPageLabel: { classPropertyName: "rowsPerPageLabel", publicName: "rowsPerPageLabel", isSignal: true, isRequired: false, transformFunction: null }, paginationLabel: { classPropertyName: "paginationLabel", publicName: "paginationLabel", isSignal: true, isRequired: false, transformFunction: null }, firstPageLabel: { classPropertyName: "firstPageLabel", publicName: "firstPageLabel", isSignal: true, isRequired: false, transformFunction: null }, previousPageLabel: { classPropertyName: "previousPageLabel", publicName: "previousPageLabel", isSignal: true, isRequired: false, transformFunction: null }, nextPageLabel: { classPropertyName: "nextPageLabel", publicName: "nextPageLabel", isSignal: true, isRequired: false, transformFunction: null }, lastPageLabel: { classPropertyName: "lastPageLabel", publicName: "lastPageLabel", isSignal: true, isRequired: false, transformFunction: null }, pageLinkLabel: { classPropertyName: "pageLinkLabel", publicName: "pageLinkLabel", isSignal: true, isRequired: false, transformFunction: null }, globalFilter: { classPropertyName: "globalFilter", publicName: "globalFilter", isSignal: true, isRequired: false, transformFunction: null }, globalFilterPlaceholder: { classPropertyName: "globalFilterPlaceholder", publicName: "globalFilterPlaceholder", isSignal: true, isRequired: false, transformFunction: null }, globalFilterAriaLabel: { classPropertyName: "globalFilterAriaLabel", publicName: "globalFilterAriaLabel", isSignal: true, isRequired: false, transformFunction: null }, globalFilterFields: { classPropertyName: "globalFilterFields", publicName: "globalFilterFields", isSignal: true, isRequired: false, transformFunction: null }, selectionMode: { classPropertyName: "selectionMode", publicName: "selectionMode", isSignal: true, isRequired: false, transformFunction: null }, selection: { classPropertyName: "selection", publicName: "selection", isSignal: true, isRequired: false, transformFunction: null }, loading: { classPropertyName: "loading", publicName: "loading", isSignal: true, isRequired: false, transformFunction: null }, emptyMessage: { classPropertyName: "emptyMessage", publicName: "emptyMessage", isSignal: true, isRequired: false, transformFunction: null }, caption: { classPropertyName: "caption", publicName: "caption", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null }, scrollHeight: { classPropertyName: "scrollHeight", publicName: "scrollHeight", isSignal: true, isRequired: false, transformFunction: null }, lazy: { classPropertyName: "lazy", publicName: "lazy", isSignal: true, isRequired: false, transformFunction: null }, totalRecords: { classPropertyName: "totalRecords", publicName: "totalRecords", isSignal: true, isRequired: false, transformFunction: null }, expandable: { classPropertyName: "expandable", publicName: "expandable", isSignal: true, isRequired: false, transformFunction: null }, resizableColumns: { classPropertyName: "resizableColumns", publicName: "resizableColumns", isSignal: true, isRequired: false, transformFunction: null }, reorderableColumns: { classPropertyName: "reorderableColumns", publicName: "reorderableColumns", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { selection: "selectionChange", sortChange: "sortChange", pageChange: "pageChange", rowSelect: "rowSelect", rowUnselect: "rowUnselect", lazyLoad: "lazyLoad", rowClick: "rowClick", rowExpand: "rowExpand", rowCollapse: "rowCollapse", colReorder: "colReorder" }, host: { classAttribute: "block" }, queries: [{ propertyName: "cellTemplates", predicate: SdsTableCell, isSignal: true }, { propertyName: "rowExpansion", first: true, predicate: SdsTableRowExpansion, descendants: true, isSignal: true }], ngImport: i0, template: `
7812
- <div class="rounded-card border border-border bg-white overflow-hidden">
8101
+ <div class="rounded-card border border-border bg-surface overflow-hidden">
7813
8102
  <!-- Toolbar: caption + global filter -->
7814
8103
  @if (caption() || globalFilter()) {
7815
8104
  <div class="flex flex-wrap items-center justify-between gap-3 border-b border-border px-4 py-3">
@@ -7821,7 +8110,7 @@ class SdsTable {
7821
8110
 
7822
8111
  @if (globalFilter()) {
7823
8112
  <div
7824
- class="relative flex h-9 items-center rounded-control border border-border bg-white transition-colors focus-within:border-brand-500 focus-within:ring-2 focus-within:ring-brand-500/25"
8113
+ class="relative flex h-9 items-center rounded-control border border-border bg-surface transition-colors focus-within:border-brand-500 focus-within:ring-2 focus-within:ring-brand-500/25"
7825
8114
  >
7826
8115
  <sds-icon
7827
8116
  name="search"
@@ -8112,7 +8401,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
8112
8401
  imports: [SdsIcon, SdsPaginator, NgTemplateOutlet],
8113
8402
  host: { class: 'block' },
8114
8403
  template: `
8115
- <div class="rounded-card border border-border bg-white overflow-hidden">
8404
+ <div class="rounded-card border border-border bg-surface overflow-hidden">
8116
8405
  <!-- Toolbar: caption + global filter -->
8117
8406
  @if (caption() || globalFilter()) {
8118
8407
  <div class="flex flex-wrap items-center justify-between gap-3 border-b border-border px-4 py-3">
@@ -8124,7 +8413,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
8124
8413
 
8125
8414
  @if (globalFilter()) {
8126
8415
  <div
8127
- class="relative flex h-9 items-center rounded-control border border-border bg-white transition-colors focus-within:border-brand-500 focus-within:ring-2 focus-within:ring-brand-500/25"
8416
+ class="relative flex h-9 items-center rounded-control border border-border bg-surface transition-colors focus-within:border-brand-500 focus-within:ring-2 focus-within:ring-brand-500/25"
8128
8417
  >
8129
8418
  <sds-icon
8130
8419
  name="search"
@@ -8560,6 +8849,153 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
8560
8849
  }]
8561
8850
  }], propDecorators: { closeLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "closeLabel", required: false }] }] } });
8562
8851
 
8852
+ const STORAGE_KEY = 'sds-theme';
8853
+ /**
8854
+ * Light/dark theme state for the whole app. It owns exactly one thing: the presence of the `.dark`
8855
+ * class on the document root, which flips every design token defined under `.dark` in the theme CSS
8856
+ * (base.css and the brand ramp). Because the library themes by ROLE, this single class re-themes the
8857
+ * entire component set — there is no per-component dark mode to wire.
8858
+ *
8859
+ * `providedIn: 'root'`, so the switcher, the shell and any feature share one source of truth:
8860
+ *
8861
+ * ```ts
8862
+ * private readonly theme = inject(SdsThemeService);
8863
+ * this.theme.toggle(); // flip
8864
+ * this.theme.isDark(); // read (a signal)
8865
+ * ```
8866
+ *
8867
+ * Initial value: a previously stored choice wins; otherwise the OS `prefers-color-scheme` is honoured,
8868
+ * so a first-time visitor already in dark at the OS level lands in dark. The choice is persisted to
8869
+ * `localStorage` and re-applied on the next visit. Every browser access is guarded, so the service is
8870
+ * safe under SSR / prerender (it simply no-ops without a DOM).
8871
+ */
8872
+ class SdsThemeService {
8873
+ document = inject(DOCUMENT);
8874
+ theme = signal(this.initialTheme(), ...(ngDevMode ? [{ debugName: "theme" }] : []));
8875
+ isDark = computed(() => this.theme() === 'dark', ...(ngDevMode ? [{ debugName: "isDark" }] : []));
8876
+ constructor() {
8877
+ // Keep the DOM class and the stored preference in lockstep with the signal — one effect, so any
8878
+ // caller that sets the signal (toggle, setTheme, or a future settings screen) applies uniformly.
8879
+ effect(() => this.apply(this.theme()));
8880
+ }
8881
+ /** Flip between light and dark. */
8882
+ toggle() {
8883
+ this.theme.update((t) => (t === 'light' ? 'dark' : 'light'));
8884
+ }
8885
+ /** Set an explicit theme. */
8886
+ setTheme(theme) {
8887
+ this.theme.set(theme);
8888
+ }
8889
+ initialTheme() {
8890
+ try {
8891
+ const stored = this.readStorage()?.getItem(STORAGE_KEY);
8892
+ if (stored === 'light' || stored === 'dark')
8893
+ return stored;
8894
+ const win = this.document.defaultView;
8895
+ if (win?.matchMedia('(prefers-color-scheme: dark)').matches)
8896
+ return 'dark';
8897
+ }
8898
+ catch {
8899
+ /* no storage / no matchMedia (SSR, privacy mode) — fall through to the light default */
8900
+ }
8901
+ return 'light';
8902
+ }
8903
+ apply(theme) {
8904
+ const root = this.document.documentElement;
8905
+ root.classList.toggle('dark', theme === 'dark');
8906
+ // Let the UA paint form controls / scrollbars to match.
8907
+ root.style.colorScheme = theme;
8908
+ try {
8909
+ this.readStorage()?.setItem(STORAGE_KEY, theme);
8910
+ }
8911
+ catch {
8912
+ /* storage unavailable — the class is still applied; only persistence is skipped */
8913
+ }
8914
+ }
8915
+ readStorage() {
8916
+ return this.document.defaultView?.localStorage ?? null;
8917
+ }
8918
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: SdsThemeService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
8919
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: SdsThemeService, providedIn: 'root' });
8920
+ }
8921
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: SdsThemeService, decorators: [{
8922
+ type: Injectable,
8923
+ args: [{ providedIn: 'root' }]
8924
+ }], ctorParameters: () => [] });
8925
+
8926
+ /**
8927
+ * Icon button that flips the app between light and dark, backed by {@link SdsThemeService}. It shows the
8928
+ * destination, not the current state — a moon while you are in light (tap to go dark), a sun while you
8929
+ * are in dark — and its accessible name names the action, because an icon-only control's label is its
8930
+ * only name.
8931
+ *
8932
+ * Two appearances so it sits correctly on either bar: `surface` (default) for a light or glass top bar,
8933
+ * `on-brand` for the dark brand bar / sidebar.
8934
+ *
8935
+ * @example
8936
+ * <sds-theme-toggle />
8937
+ * <sds-theme-toggle appearance="on-brand" [lightLabel]="'Passer en clair'" [darkLabel]="'Passer en sombre'" />
8938
+ */
8939
+ class SdsThemeToggle {
8940
+ theme = inject(SdsThemeService);
8941
+ isDark = this.theme.isDark;
8942
+ /** Button box size in px (the icon scales with it). */
8943
+ size = input(40, ...(ngDevMode ? [{ debugName: "size" }] : []));
8944
+ /** `surface` for light/glass bars, `on-brand` for the dark brand bar. */
8945
+ appearance = input('surface', ...(ngDevMode ? [{ debugName: "appearance" }] : []));
8946
+ /**
8947
+ * Accessible names for the two actions. French defaults keep existing consumers unchanged; a
8948
+ * multilingual app must pass translated values.
8949
+ */
8950
+ darkLabel = input('Passer en mode sombre', ...(ngDevMode ? [{ debugName: "darkLabel" }] : []));
8951
+ lightLabel = input('Passer en mode clair', ...(ngDevMode ? [{ debugName: "lightLabel" }] : []));
8952
+ iconSize = computed(() => Math.round(this.size() * 0.5), ...(ngDevMode ? [{ debugName: "iconSize" }] : []));
8953
+ label = computed(() => (this.isDark() ? this.lightLabel() : this.darkLabel()), ...(ngDevMode ? [{ debugName: "label" }] : []));
8954
+ buttonClasses = computed(() => this.appearance() === 'on-brand'
8955
+ ? 'text-white/85 hover:bg-white/10 hover:text-white focus-visible:ring-white/40'
8956
+ : 'bg-muted text-ink-soft hover:bg-brand-50 hover:text-brand-600 focus-visible:ring-brand-500/40', ...(ngDevMode ? [{ debugName: "buttonClasses" }] : []));
8957
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: SdsThemeToggle, deps: [], target: i0.ɵɵFactoryTarget.Component });
8958
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "20.3.25", type: SdsThemeToggle, isStandalone: true, selector: "sds-theme-toggle", inputs: { size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, appearance: { classPropertyName: "appearance", publicName: "appearance", isSignal: true, isRequired: false, transformFunction: null }, darkLabel: { classPropertyName: "darkLabel", publicName: "darkLabel", isSignal: true, isRequired: false, transformFunction: null }, lightLabel: { classPropertyName: "lightLabel", publicName: "lightLabel", isSignal: true, isRequired: false, transformFunction: null } }, host: { classAttribute: "inline-block" }, ngImport: i0, template: `
8959
+ <button
8960
+ type="button"
8961
+ class="inline-flex items-center justify-center rounded-control transition-colors focus-visible:outline-none focus-visible:ring-2"
8962
+ [class]="buttonClasses()"
8963
+ [style.width.px]="size()"
8964
+ [style.height.px]="size()"
8965
+ [attr.aria-label]="label()"
8966
+ [attr.aria-pressed]="isDark()"
8967
+ [attr.title]="label()"
8968
+ (click)="theme.toggle()"
8969
+ >
8970
+ <sds-icon [name]="isDark() ? 'sun' : 'moon'" [size]="iconSize()" />
8971
+ </button>
8972
+ `, isInline: true, dependencies: [{ kind: "component", type: SdsIcon, selector: "sds-icon", inputs: ["name", "size", "strokeWidth"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
8973
+ }
8974
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: SdsThemeToggle, decorators: [{
8975
+ type: Component,
8976
+ args: [{
8977
+ selector: 'sds-theme-toggle',
8978
+ changeDetection: ChangeDetectionStrategy.OnPush,
8979
+ imports: [SdsIcon],
8980
+ host: { class: 'inline-block' },
8981
+ template: `
8982
+ <button
8983
+ type="button"
8984
+ class="inline-flex items-center justify-center rounded-control transition-colors focus-visible:outline-none focus-visible:ring-2"
8985
+ [class]="buttonClasses()"
8986
+ [style.width.px]="size()"
8987
+ [style.height.px]="size()"
8988
+ [attr.aria-label]="label()"
8989
+ [attr.aria-pressed]="isDark()"
8990
+ [attr.title]="label()"
8991
+ (click)="theme.toggle()"
8992
+ >
8993
+ <sds-icon [name]="isDark() ? 'sun' : 'moon'" [size]="iconSize()" />
8994
+ </button>
8995
+ `,
8996
+ }]
8997
+ }], propDecorators: { size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], appearance: [{ type: i0.Input, args: [{ isSignal: true, alias: "appearance", required: false }] }], darkLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "darkLabel", required: false }] }], lightLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "lightLabel", required: false }] }] } });
8998
+
8563
8999
  /**
8564
9000
  * Shared collapse state for the sidebar and its nav items.
8565
9001
  *
@@ -8570,6 +9006,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
8570
9006
  class SidebarStore {
8571
9007
  /** Desktop rail state: collapsed to icon-only width (applies at ≥ lg). */
8572
9008
  collapsed = signal(false, ...(ngDevMode ? [{ debugName: "collapsed" }] : []));
9009
+ /**
9010
+ * The rail's paint, published by <sds-sidebar> so projected <sds-nav-item>/<sds-nav-group> pick the
9011
+ * matching state and hover colours (white-on-dark for `brand`, ink-and-brand for `surface`) without
9012
+ * every item taking an input. One rail, one appearance — set by the sidebar, read by its children.
9013
+ */
9014
+ appearance = signal('brand', ...(ngDevMode ? [{ debugName: "appearance" }] : []));
8573
9015
  /** Mobile drawer state: whether the off-canvas sidebar is slid in (< lg). */
8574
9016
  mobileOpen = signal(false, ...(ngDevMode ? [{ debugName: "mobileOpen" }] : []));
8575
9017
  /** Toggle the desktop icon-rail. */
@@ -8602,10 +9044,15 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
8602
9044
  /**
8603
9045
  * Application top bar: a logo slot, the app name, and three slots the app fills.
8604
9046
  *
8605
- * Two appearances. `brand` is the gradient-and-wave bar an internal tool wants. `surface` is white on
9047
+ * Three appearances. `brand` is the gradient-and-wave bar an internal tool wants. `surface` is white on
8606
9048
  * a hairline border, for a bar that has to sit under a customer-facing page without dominating it —
8607
- * a storefront header is a bar, but it is not a banner. The slots and the layout are identical in
8608
- * both; only the paint differs, so a consumer app gets the same structure without the brand fill.
9049
+ * a storefront header is a bar, but it is not a banner. `glass` is a translucent, brand-tinted bar with
9050
+ * a blur behind it, for a dashboard that wants the brand present but light — it reads the brand from the
9051
+ * ramp, so it tints itself to whatever theme is loaded and flips with dark mode. The slots and the
9052
+ * layout are identical across all three; only the paint differs.
9053
+ *
9054
+ * `[decorations]` adds slow-drifting, brand-tinted blobs behind the bar (opt-in; they respect
9055
+ * prefers-reduced-motion). They pair with `glass`/`surface`; on `brand` the wave already fills that role.
8609
9056
  *
8610
9057
  * It deliberately owns NO account UI. It used to render the signed-in user and a logout button, which
8611
9058
  * forced every consumer into one shape — no dropdown, no email, no menu, no busy state, and an
@@ -8617,7 +9064,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
8617
9064
  * `[sdsCenter]` (the flexible middle — a search field), `[sdsActions]` (the right edge).
8618
9065
  *
8619
9066
  * @example
8620
- * <sds-topbar appName="WebApp">
9067
+ * <sds-topbar appName="Design system">
8621
9068
  * <img sdsLogo src="logo.svg" alt="" />
8622
9069
  * <my-address-picker sdsLead />
8623
9070
  * <my-search sdsCenter />
@@ -8647,16 +9094,29 @@ class SdsTopbar {
8647
9094
  * aria-label is its only name, and a hardcoded one is simply wrong in every other locale.
8648
9095
  */
8649
9096
  menuLabel = input('Ouvrir le menu', ...(ngDevMode ? [{ debugName: "menuLabel" }] : []));
8650
- /** `brand` (default) paints the gradient and the wave; `surface` is white on a hairline border. */
9097
+ /**
9098
+ * `brand` (default) paints the gradient and the wave; `surface` is white on a hairline border;
9099
+ * `glass` is a translucent, brand-tinted bar with a backdrop blur.
9100
+ */
8651
9101
  appearance = input('brand', ...(ngDevMode ? [{ debugName: "appearance" }] : []));
8652
- shellClasses = computed(() => this.appearance() === 'brand'
8653
- ? 'bg-linear-to-r from-brand-800 via-brand-700 to-brand-600 text-white'
8654
- : 'border-b border-border bg-surface text-ink', ...(ngDevMode ? [{ debugName: "shellClasses" }] : []));
9102
+ /** Add slow-drifting brand-tinted blobs behind the bar (opt-in; pairs with `glass`/`surface`). */
9103
+ decorations = input(false, ...(ngDevMode ? [{ debugName: "decorations", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
9104
+ shellClasses = computed(() => {
9105
+ switch (this.appearance()) {
9106
+ case 'brand':
9107
+ return 'bg-linear-to-r from-brand-800 via-brand-700 to-brand-600 text-white';
9108
+ case 'glass':
9109
+ // Translucent so the blur shows the page tint through; the brand edge is read from the ramp.
9110
+ return 'border-b border-border bg-linear-to-r from-surface/85 via-surface/80 to-brand-100/70 text-ink backdrop-blur-xl';
9111
+ default:
9112
+ return 'border-b border-border bg-surface text-ink';
9113
+ }
9114
+ }, ...(ngDevMode ? [{ debugName: "shellClasses" }] : []));
8655
9115
  menuButtonClasses = computed(() => this.appearance() === 'brand'
8656
9116
  ? 'text-white/85 hover:bg-white/10 hover:text-white focus-visible:ring-white/40'
8657
9117
  : 'text-ink-soft hover:bg-muted hover:text-ink focus-visible:ring-brand-500/40', ...(ngDevMode ? [{ debugName: "menuButtonClasses" }] : []));
8658
9118
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: SdsTopbar, deps: [], target: i0.ɵɵFactoryTarget.Component });
8659
- 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: `
9119
+ 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 }, decorations: { classPropertyName: "decorations", publicName: "decorations", isSignal: true, isRequired: false, transformFunction: null } }, host: { classAttribute: "block" }, ngImport: i0, template: `
8660
9120
  <header class="relative z-30" [class]="shellClasses()" style="height: var(--sds-topbar-h)">
8661
9121
  <!--
8662
9122
  Decorative wave. The clip lives HERE, on the wave's own wrapper, and not on the header.
@@ -8694,6 +9154,18 @@ class SdsTopbar {
8694
9154
  </span>
8695
9155
  }
8696
9156
 
9157
+ <!--
9158
+ Brand-tinted ambient blobs. Behind the content and non-interactive; the clip lives on this span,
9159
+ never on the header, so a projected menu/dropdown is not sliced. Colours come from the brand ramp,
9160
+ so they follow the active theme and dark mode.
9161
+ -->
9162
+ @if (decorations()) {
9163
+ <span class="pointer-events-none absolute inset-0 overflow-hidden" aria-hidden="true">
9164
+ <span class="absolute -top-16 right-8 h-56 w-56 rounded-full bg-brand-300/25 blur-3xl animate-sds-ambient"></span>
9165
+ <span class="absolute -top-10 right-[26%] h-40 w-40 rounded-full bg-brand-400/20 blur-3xl animate-sds-ambient [animation-delay:3s]"></span>
9166
+ </span>
9167
+ }
9168
+
8697
9169
  <!--
8698
9170
  The band is always full-bleed — the gradient and the wave span the viewport — while the
8699
9171
  CONTENTS are constrained by [contentWidth], so a bar over a centred page lines up with it
@@ -8719,18 +9191,13 @@ class SdsTopbar {
8719
9191
  }
8720
9192
  <ng-content select="[sdsLogo]" />
8721
9193
  <!--
8722
- shrink-0 + whitespace-nowrap, both required. The left block is a flex row, so without them a
8723
- two-word app name wraps to a second line INSIDE a bar whose height is fixed at
8724
- --sds-topbar-h, and the name is clipped by its own bar. It surfaced at 375px on a bar
8725
- carrying a logo, a wordmark and a two-syllable name; it is wrong at every width.
8726
-
8727
- The name never truncates and never hides: it is the answer to "which app am I in", which on a
8728
- platform of five same-branded apps is the one thing the bar must not drop. An app whose left
8729
- block is too crowded for a phone hides ITS OWN wordmark under sm and keeps the mark.
9194
+ The app name is hidden on phones (< sm) and shown from sm up: on a narrow bar carrying a logo,
9195
+ a search field and the account controls, the wordmark is what gives — the logo still answers
9196
+ "which app". shrink-0 + whitespace-nowrap keep it on one line at the widths where it does show.
8730
9197
  -->
8731
9198
  @if (appName()) {
8732
- <span class="h-7 w-px shrink-0" [class]="appearance() === 'brand' ? 'bg-white/30' : 'bg-border'"></span>
8733
- <span class="shrink-0 whitespace-nowrap text-xl font-bold tracking-tight">{{ appName() }}</span>
9199
+ <span class="hidden h-7 w-px shrink-0 sm:block" [class]="appearance() === 'brand' ? 'bg-white/30' : 'bg-border'" [style.opacity]="appearance() === 'glass' ? 0.7 : null"></span>
9200
+ <span class="hidden shrink-0 whitespace-nowrap text-xl font-bold tracking-tight sm:inline-block">{{ appName() }}</span>
8734
9201
  }
8735
9202
  <ng-content select="[sdsLead]" />
8736
9203
  </div>
@@ -8793,6 +9260,18 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
8793
9260
  </span>
8794
9261
  }
8795
9262
 
9263
+ <!--
9264
+ Brand-tinted ambient blobs. Behind the content and non-interactive; the clip lives on this span,
9265
+ never on the header, so a projected menu/dropdown is not sliced. Colours come from the brand ramp,
9266
+ so they follow the active theme and dark mode.
9267
+ -->
9268
+ @if (decorations()) {
9269
+ <span class="pointer-events-none absolute inset-0 overflow-hidden" aria-hidden="true">
9270
+ <span class="absolute -top-16 right-8 h-56 w-56 rounded-full bg-brand-300/25 blur-3xl animate-sds-ambient"></span>
9271
+ <span class="absolute -top-10 right-[26%] h-40 w-40 rounded-full bg-brand-400/20 blur-3xl animate-sds-ambient [animation-delay:3s]"></span>
9272
+ </span>
9273
+ }
9274
+
8796
9275
  <!--
8797
9276
  The band is always full-bleed — the gradient and the wave span the viewport — while the
8798
9277
  CONTENTS are constrained by [contentWidth], so a bar over a centred page lines up with it
@@ -8818,18 +9297,13 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
8818
9297
  }
8819
9298
  <ng-content select="[sdsLogo]" />
8820
9299
  <!--
8821
- shrink-0 + whitespace-nowrap, both required. The left block is a flex row, so without them a
8822
- two-word app name wraps to a second line INSIDE a bar whose height is fixed at
8823
- --sds-topbar-h, and the name is clipped by its own bar. It surfaced at 375px on a bar
8824
- carrying a logo, a wordmark and a two-syllable name; it is wrong at every width.
8825
-
8826
- The name never truncates and never hides: it is the answer to "which app am I in", which on a
8827
- platform of five same-branded apps is the one thing the bar must not drop. An app whose left
8828
- block is too crowded for a phone hides ITS OWN wordmark under sm and keeps the mark.
9300
+ The app name is hidden on phones (< sm) and shown from sm up: on a narrow bar carrying a logo,
9301
+ a search field and the account controls, the wordmark is what gives — the logo still answers
9302
+ "which app". shrink-0 + whitespace-nowrap keep it on one line at the widths where it does show.
8829
9303
  -->
8830
9304
  @if (appName()) {
8831
- <span class="h-7 w-px shrink-0" [class]="appearance() === 'brand' ? 'bg-white/30' : 'bg-border'"></span>
8832
- <span class="shrink-0 whitespace-nowrap text-xl font-bold tracking-tight">{{ appName() }}</span>
9305
+ <span class="hidden h-7 w-px shrink-0 sm:block" [class]="appearance() === 'brand' ? 'bg-white/30' : 'bg-border'" [style.opacity]="appearance() === 'glass' ? 0.7 : null"></span>
9306
+ <span class="hidden shrink-0 whitespace-nowrap text-xl font-bold tracking-tight sm:inline-block">{{ appName() }}</span>
8833
9307
  }
8834
9308
  <ng-content select="[sdsLead]" />
8835
9309
  </div>
@@ -8847,28 +9321,47 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
8847
9321
  </header>
8848
9322
  `,
8849
9323
  }]
8850
- }], 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 }] }] } });
9324
+ }], 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 }] }], decorations: [{ type: i0.Input, args: [{ isSignal: true, alias: "decorations", required: false }] }] } });
8851
9325
 
8852
9326
  /**
8853
9327
  * App layout shell: a full-height column with the top bar across the top and a
8854
9328
  * row below holding the sidebar and the scrollable main content.
8855
9329
  *
9330
+ * The content area is `plain` (flat canvas) by default. Set `[background]="'gradient'"` for the soft
9331
+ * brand-tinted wash that pairs with the glass top bar and the surface sidebar, and add `[decorations]`
9332
+ * for the slow drifting brand blobs behind the page. Both read the brand ramp, so they follow the active
9333
+ * theme and dark mode, and the decorations respect prefers-reduced-motion.
9334
+ *
8856
9335
  * @example
8857
- * <sds-shell>
8858
- * <sds-topbar sdsTopbar … />
8859
- * <sds-sidebar sdsSidebar> … </sds-sidebar>
9336
+ * <sds-shell background="gradient" decorations>
9337
+ * <sds-topbar appearance="glass" sdsTopbar … />
9338
+ * <sds-sidebar appearance="surface" sdsSidebar> … </sds-sidebar>
8860
9339
  * <!-- routed page content -->
8861
9340
  * <router-outlet />
8862
9341
  * </sds-shell>
8863
9342
  */
8864
9343
  class SdsShell {
9344
+ /** `plain` flat canvas (default) or a soft brand-tinted `gradient` wash. */
9345
+ background = input('plain', ...(ngDevMode ? [{ debugName: "background" }] : []));
9346
+ /** Add slow-drifting brand-tinted blobs behind the page content (opt-in). */
9347
+ decorations = input(false, ...(ngDevMode ? [{ debugName: "decorations", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
9348
+ mainClass = computed(() => this.background() === 'gradient'
9349
+ ? 'bg-linear-to-br from-brand-50/50 via-canvas to-brand-100/40'
9350
+ : 'bg-canvas', ...(ngDevMode ? [{ debugName: "mainClass" }] : []));
8865
9351
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: SdsShell, deps: [], target: i0.ɵɵFactoryTarget.Component });
8866
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.25", type: SdsShell, isStandalone: true, selector: "sds-shell", host: { classAttribute: "block" }, ngImport: i0, template: `
9352
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.25", type: SdsShell, isStandalone: true, selector: "sds-shell", inputs: { background: { classPropertyName: "background", publicName: "background", isSignal: true, isRequired: false, transformFunction: null }, decorations: { classPropertyName: "decorations", publicName: "decorations", isSignal: true, isRequired: false, transformFunction: null } }, host: { classAttribute: "block" }, ngImport: i0, template: `
8867
9353
  <div class="flex h-screen flex-col overflow-hidden">
8868
9354
  <ng-content select="[sdsTopbar]" />
8869
9355
  <div class="flex min-h-0 flex-1">
8870
9356
  <ng-content select="[sdsSidebar]" />
8871
- <main class="min-w-0 flex-1 overflow-y-auto bg-canvas">
9357
+ <main class="relative isolate min-w-0 flex-1 overflow-y-auto" [class]="mainClass()">
9358
+ @if (decorations()) {
9359
+ <div class="pointer-events-none absolute inset-0 -z-10 overflow-hidden" aria-hidden="true">
9360
+ <div class="absolute -left-32 top-10 h-96 w-96 rounded-full bg-brand-300/15 blur-3xl animate-sds-ambient"></div>
9361
+ <div class="absolute -right-40 top-1/3 h-[28rem] w-[28rem] rounded-full bg-brand-400/12 blur-3xl animate-sds-ambient [animation-delay:4s]"></div>
9362
+ <div class="absolute bottom-10 left-1/4 h-80 w-80 rounded-full bg-brand-200/20 blur-3xl animate-sds-ambient [animation-delay:7s]"></div>
9363
+ </div>
9364
+ }
8872
9365
  <ng-content />
8873
9366
  </main>
8874
9367
  </div>
@@ -8886,22 +9379,39 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
8886
9379
  <ng-content select="[sdsTopbar]" />
8887
9380
  <div class="flex min-h-0 flex-1">
8888
9381
  <ng-content select="[sdsSidebar]" />
8889
- <main class="min-w-0 flex-1 overflow-y-auto bg-canvas">
9382
+ <main class="relative isolate min-w-0 flex-1 overflow-y-auto" [class]="mainClass()">
9383
+ @if (decorations()) {
9384
+ <div class="pointer-events-none absolute inset-0 -z-10 overflow-hidden" aria-hidden="true">
9385
+ <div class="absolute -left-32 top-10 h-96 w-96 rounded-full bg-brand-300/15 blur-3xl animate-sds-ambient"></div>
9386
+ <div class="absolute -right-40 top-1/3 h-[28rem] w-[28rem] rounded-full bg-brand-400/12 blur-3xl animate-sds-ambient [animation-delay:4s]"></div>
9387
+ <div class="absolute bottom-10 left-1/4 h-80 w-80 rounded-full bg-brand-200/20 blur-3xl animate-sds-ambient [animation-delay:7s]"></div>
9388
+ </div>
9389
+ }
8890
9390
  <ng-content />
8891
9391
  </main>
8892
9392
  </div>
8893
9393
  </div>
8894
9394
  `,
8895
9395
  }]
8896
- }] });
9396
+ }], propDecorators: { background: [{ type: i0.Input, args: [{ isSignal: true, alias: "background", required: false }] }], decorations: [{ type: i0.Input, args: [{ isSignal: true, alias: "decorations", required: false }] }] } });
8897
9397
 
8898
9398
  /**
8899
9399
  * Collapsible navigation rail. Project <sds-nav-group>/<sds-nav-item> as the
8900
9400
  * default content, and pin a footer (e.g. search + version badge) via the
8901
9401
  * [sdsFooter] slot. Collapse state is shared through SidebarStore.
8902
9402
  *
9403
+ * Two appearances. `brand` (default) is the dark brand rail. `surface` is a light rail with a faint
9404
+ * brand wash and brand-tinted active items — the counterpart to the glass top bar. The appearance is
9405
+ * published to SidebarStore so the projected nav items adopt the matching state colours from this one
9406
+ * input. `[decorations]` adds the same slow brand-tinted blobs the top bar and container use.
9407
+ *
9408
+ * Responsive: below `lg` it is a fixed off-canvas drawer opened by the top bar's hamburger; it is a
9409
+ * modal (scrim + trap), so while open Escape closes it, focus moves into it and is restored on close,
9410
+ * and Tab is kept inside. While closed it is `inert`, so its off-screen items are out of the tab order.
9411
+ * At `lg` and up it is the in-flow, collapsible rail.
9412
+ *
8903
9413
  * @example
8904
- * <sds-sidebar>
9414
+ * <sds-sidebar appearance="surface" decorations>
8905
9415
  * <sds-nav-group label="Membres">
8906
9416
  * <sds-nav-item icon="user-plus" label="Créer un membre" link="/membres/new" />
8907
9417
  * </sds-nav-group>
@@ -8910,22 +9420,101 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
8910
9420
  */
8911
9421
  class SdsSidebar {
8912
9422
  store = inject(SidebarStore);
9423
+ document = inject(DOCUMENT);
9424
+ drawer = viewChild('drawer', ...(ngDevMode ? [{ debugName: "drawer" }] : []));
9425
+ closeBtn = viewChild('closeBtn', ...(ngDevMode ? [{ debugName: "closeBtn" }] : []));
9426
+ /** True below the `lg` breakpoint — where the rail is the off-canvas modal drawer. */
9427
+ belowLg = signal(false, ...(ngDevMode ? [{ debugName: "belowLg" }] : []));
9428
+ /** The element focused before the drawer opened, restored to it on close. */
9429
+ restoreFocusTo = null;
9430
+ constructor() {
9431
+ // Publish the rail's paint so projected nav items match it.
9432
+ effect(() => this.store.appearance.set(this.appearance()));
9433
+ const view = this.document.defaultView;
9434
+ const mql = view?.matchMedia('(max-width: 1023.98px)');
9435
+ if (mql) {
9436
+ this.belowLg.set(mql.matches);
9437
+ mql.addEventListener('change', (e) => this.belowLg.set(e.matches));
9438
+ }
9439
+ // Modal focus handling for the mobile drawer: on open, remember what was focused (the hamburger) and
9440
+ // move focus to the close button; on close, restore it. A no-op at ≥ lg, where the rail is not modal.
9441
+ effect(() => {
9442
+ const open = this.mobileOpen();
9443
+ if (!this.belowLg())
9444
+ return;
9445
+ if (open) {
9446
+ this.restoreFocusTo = this.document.activeElement;
9447
+ this.closeBtn()?.nativeElement.focus();
9448
+ }
9449
+ else if (this.restoreFocusTo) {
9450
+ this.restoreFocusTo.focus();
9451
+ this.restoreFocusTo = null;
9452
+ }
9453
+ });
9454
+ }
8913
9455
  /**
8914
9456
  * Accessible names for the rail's three icon-only controls. French defaults keep existing consumers
8915
- * unchanged; a multilingual app must pass translated values, since an icon-only button's aria-label
8916
- * is its only name.
9457
+ * unchanged; a multilingual app must pass translated values.
8917
9458
  */
8918
9459
  collapseLabel = input('Replier le menu', ...(ngDevMode ? [{ debugName: "collapseLabel" }] : []));
8919
9460
  expandLabel = input('Déplier le menu', ...(ngDevMode ? [{ debugName: "expandLabel" }] : []));
8920
9461
  closeLabel = input('Fermer le menu', ...(ngDevMode ? [{ debugName: "closeLabel" }] : []));
9462
+ /** `brand` (default) is the dark rail; `surface` is a light rail with brand accents. */
9463
+ appearance = input('brand', ...(ngDevMode ? [{ debugName: "appearance" }] : []));
9464
+ /** Add slow-drifting brand-tinted blobs behind the rail (opt-in). */
9465
+ decorations = input(false, ...(ngDevMode ? [{ debugName: "decorations", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
8921
9466
  collapsed = this.store.collapsed;
8922
9467
  mobileOpen = this.store.mobileOpen;
9468
+ asideClass = computed(() => {
9469
+ // The rail must stay OPAQUE: as a mobile drawer it slides over page content, so a translucent stop
9470
+ // (e.g. to-brand-50/50) would let that content bleed through. All gradient stops are opaque; the
9471
+ // wash is a subtle surface->brand-50 tint, not a transparency.
9472
+ const paint = this.appearance() === 'surface'
9473
+ ? 'bg-surface bg-linear-to-b from-surface via-surface to-brand-50 text-ink border-r border-border'
9474
+ : 'bg-brand-900 text-white';
9475
+ const slide = this.mobileOpen() ? 'translate-x-0' : '-translate-x-full';
9476
+ return `${paint} ${slide}`;
9477
+ }, ...(ngDevMode ? [{ debugName: "asideClass" }] : []));
9478
+ /** The two header icon buttons, painted for the active rail. */
9479
+ railBtnClass = computed(() => this.appearance() === 'surface'
9480
+ ? 'text-ink-soft hover:bg-muted hover:text-ink focus-visible:ring-brand-500/40'
9481
+ : 'text-white/70 hover:bg-white/10 hover:text-white focus-visible:ring-white/40', ...(ngDevMode ? [{ debugName: "railBtnClass" }] : []));
8923
9482
  /** Desktop rail width (full vs. collapsed); mobile always uses full width. */
8924
9483
  railWidth = computed(() => this.collapsed()
8925
9484
  ? 'var(--sds-sidebar-w-collapsed)'
8926
9485
  : 'var(--sds-sidebar-w)', ...(ngDevMode ? [{ debugName: "railWidth" }] : []));
9486
+ onEscape() {
9487
+ if (this.mobileOpen())
9488
+ this.store.closeMobile();
9489
+ }
9490
+ /** Keep Tab inside the open mobile drawer (a modal must not leak focus to the page behind the scrim). */
9491
+ onDrawerKeydown(event) {
9492
+ if (event.key !== 'Tab' || !this.mobileOpen() || !this.belowLg())
9493
+ return;
9494
+ const focusables = this.focusableEls();
9495
+ if (!focusables.length)
9496
+ return;
9497
+ const first = focusables[0];
9498
+ const last = focusables[focusables.length - 1];
9499
+ const active = this.document.activeElement;
9500
+ if (event.shiftKey && active === first) {
9501
+ event.preventDefault();
9502
+ last.focus();
9503
+ }
9504
+ else if (!event.shiftKey && active === last) {
9505
+ event.preventDefault();
9506
+ first.focus();
9507
+ }
9508
+ }
9509
+ focusableEls() {
9510
+ const root = this.drawer()?.nativeElement;
9511
+ if (!root)
9512
+ return [];
9513
+ const sel = 'a[href],button:not([disabled]),input:not([disabled]),select:not([disabled]),textarea:not([disabled]),[tabindex]:not([tabindex="-1"])';
9514
+ return Array.from(root.querySelectorAll(sel)).filter((el) => el.offsetParent !== null || el === this.document.activeElement);
9515
+ }
8927
9516
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: SdsSidebar, deps: [], target: i0.ɵɵFactoryTarget.Component });
8928
- 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: `
9517
+ 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 }, appearance: { classPropertyName: "appearance", publicName: "appearance", isSignal: true, isRequired: false, transformFunction: null }, decorations: { classPropertyName: "decorations", publicName: "decorations", isSignal: true, isRequired: false, transformFunction: null } }, host: { listeners: { "document:keydown.escape": "onEscape()" }, classAttribute: "block h-full" }, viewQueries: [{ propertyName: "drawer", first: true, predicate: ["drawer"], descendants: true, isSignal: true }, { propertyName: "closeBtn", first: true, predicate: ["closeBtn"], descendants: true, isSignal: true }], ngImport: i0, template: `
8929
9518
  <!-- Mobile backdrop (only when the drawer is open, < lg) -->
8930
9519
  @if (mobileOpen()) {
8931
9520
  <div
@@ -8936,23 +9525,37 @@ class SdsSidebar {
8936
9525
  }
8937
9526
 
8938
9527
  <!--
8939
- Mobile (< lg): fixed off-canvas drawer that slides in from the left.
8940
- Desktop (≥ lg): static, collapsible icon-rail (width via --sds-rail).
9528
+ Mobile (< lg): fixed off-canvas drawer that slides in from the left, and is inert while closed so
9529
+ its off-screen controls leave the tab order. Desktop (>= lg): the in-flow collapsible icon-rail. It
9530
+ is lg:relative (not static) so the ambient blobs, which are absolutely positioned, anchor to it.
8941
9531
  -->
8942
9532
  <aside
8943
- class="fixed inset-y-0 left-0 z-50 flex h-full w-[var(--sds-sidebar-w)] flex-col bg-brand-900 text-white transition-transform duration-200 ease-out lg:static lg:z-auto lg:w-[var(--sds-rail)] lg:translate-x-0 lg:transition-[width]"
8944
- [class]="mobileOpen() ? 'translate-x-0' : '-translate-x-full'"
9533
+ #drawer
9534
+ class="fixed inset-y-0 left-0 z-50 flex h-full w-[var(--sds-sidebar-w)] flex-col transition-transform duration-200 ease-out lg:relative lg:z-auto lg:w-[var(--sds-rail)] lg:translate-x-0 lg:transition-[width]"
9535
+ [class]="asideClass()"
8945
9536
  [style.--sds-rail]="railWidth()"
9537
+ [inert]="belowLg() && !mobileOpen()"
9538
+ [attr.role]="belowLg() && mobileOpen() ? 'dialog' : null"
9539
+ [attr.aria-modal]="belowLg() && mobileOpen() ? 'true' : null"
9540
+ (keydown)="onDrawerKeydown($event)"
8946
9541
  >
9542
+ @if (decorations()) {
9543
+ <span class="pointer-events-none absolute inset-0 overflow-hidden" aria-hidden="true">
9544
+ <span class="absolute -top-16 -left-16 h-56 w-56 rounded-full bg-brand-300/20 blur-3xl animate-sds-ambient"></span>
9545
+ <span class="absolute -bottom-24 -right-16 h-64 w-64 rounded-full bg-brand-400/15 blur-3xl animate-sds-ambient [animation-delay:4s]"></span>
9546
+ </span>
9547
+ }
9548
+
8947
9549
  <!-- Header: collapse toggle (desktop) / close (mobile) -->
8948
9550
  <div
8949
- class="flex items-center px-3"
9551
+ class="relative flex items-center px-3"
8950
9552
  style="height: 52px"
8951
9553
  [class]="collapsed() ? 'justify-end lg:justify-center' : 'justify-end'"
8952
9554
  >
8953
9555
  <button
8954
9556
  type="button"
8955
- 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"
9557
+ class="hidden h-9 w-9 items-center justify-center rounded-control transition-colors focus-visible:outline-none focus-visible:ring-2 lg:inline-flex"
9558
+ [class]="railBtnClass()"
8956
9559
  [attr.aria-label]="collapsed() ? expandLabel() : collapseLabel()"
8957
9560
  [attr.aria-expanded]="!collapsed()"
8958
9561
  (click)="store.toggle()"
@@ -8963,8 +9566,10 @@ class SdsSidebar {
8963
9566
  />
8964
9567
  </button>
8965
9568
  <button
9569
+ #closeBtn
8966
9570
  type="button"
8967
- 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"
9571
+ class="inline-flex h-9 w-9 items-center justify-center rounded-control transition-colors focus-visible:outline-none focus-visible:ring-2 lg:hidden"
9572
+ [class]="railBtnClass()"
8968
9573
  [attr.aria-label]="closeLabel()"
8969
9574
  (click)="store.closeMobile()"
8970
9575
  >
@@ -8973,12 +9578,12 @@ class SdsSidebar {
8973
9578
  </div>
8974
9579
 
8975
9580
  <!-- Navigation -->
8976
- <nav class="flex-1 overflow-y-auto overflow-x-hidden px-3 pb-4">
9581
+ <nav class="relative flex-1 overflow-y-auto overflow-x-hidden px-3 pb-4">
8977
9582
  <ng-content />
8978
9583
  </nav>
8979
9584
 
8980
9585
  <!-- Footer (search, version, …) -->
8981
- <div class="px-3 pb-4 pt-2">
9586
+ <div class="relative px-3 pb-4 pt-2">
8982
9587
  <ng-content select="[sdsFooter]" />
8983
9588
  </div>
8984
9589
  </aside>
@@ -8990,7 +9595,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
8990
9595
  selector: 'sds-sidebar',
8991
9596
  changeDetection: ChangeDetectionStrategy.OnPush,
8992
9597
  imports: [SdsIcon],
8993
- host: { class: 'block h-full' },
9598
+ host: {
9599
+ class: 'block h-full',
9600
+ '(document:keydown.escape)': 'onEscape()',
9601
+ },
8994
9602
  template: `
8995
9603
  <!-- Mobile backdrop (only when the drawer is open, < lg) -->
8996
9604
  @if (mobileOpen()) {
@@ -9002,23 +9610,37 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
9002
9610
  }
9003
9611
 
9004
9612
  <!--
9005
- Mobile (< lg): fixed off-canvas drawer that slides in from the left.
9006
- Desktop (≥ lg): static, collapsible icon-rail (width via --sds-rail).
9613
+ Mobile (< lg): fixed off-canvas drawer that slides in from the left, and is inert while closed so
9614
+ its off-screen controls leave the tab order. Desktop (>= lg): the in-flow collapsible icon-rail. It
9615
+ is lg:relative (not static) so the ambient blobs, which are absolutely positioned, anchor to it.
9007
9616
  -->
9008
9617
  <aside
9009
- class="fixed inset-y-0 left-0 z-50 flex h-full w-[var(--sds-sidebar-w)] flex-col bg-brand-900 text-white transition-transform duration-200 ease-out lg:static lg:z-auto lg:w-[var(--sds-rail)] lg:translate-x-0 lg:transition-[width]"
9010
- [class]="mobileOpen() ? 'translate-x-0' : '-translate-x-full'"
9618
+ #drawer
9619
+ class="fixed inset-y-0 left-0 z-50 flex h-full w-[var(--sds-sidebar-w)] flex-col transition-transform duration-200 ease-out lg:relative lg:z-auto lg:w-[var(--sds-rail)] lg:translate-x-0 lg:transition-[width]"
9620
+ [class]="asideClass()"
9011
9621
  [style.--sds-rail]="railWidth()"
9622
+ [inert]="belowLg() && !mobileOpen()"
9623
+ [attr.role]="belowLg() && mobileOpen() ? 'dialog' : null"
9624
+ [attr.aria-modal]="belowLg() && mobileOpen() ? 'true' : null"
9625
+ (keydown)="onDrawerKeydown($event)"
9012
9626
  >
9627
+ @if (decorations()) {
9628
+ <span class="pointer-events-none absolute inset-0 overflow-hidden" aria-hidden="true">
9629
+ <span class="absolute -top-16 -left-16 h-56 w-56 rounded-full bg-brand-300/20 blur-3xl animate-sds-ambient"></span>
9630
+ <span class="absolute -bottom-24 -right-16 h-64 w-64 rounded-full bg-brand-400/15 blur-3xl animate-sds-ambient [animation-delay:4s]"></span>
9631
+ </span>
9632
+ }
9633
+
9013
9634
  <!-- Header: collapse toggle (desktop) / close (mobile) -->
9014
9635
  <div
9015
- class="flex items-center px-3"
9636
+ class="relative flex items-center px-3"
9016
9637
  style="height: 52px"
9017
9638
  [class]="collapsed() ? 'justify-end lg:justify-center' : 'justify-end'"
9018
9639
  >
9019
9640
  <button
9020
9641
  type="button"
9021
- 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"
9642
+ class="hidden h-9 w-9 items-center justify-center rounded-control transition-colors focus-visible:outline-none focus-visible:ring-2 lg:inline-flex"
9643
+ [class]="railBtnClass()"
9022
9644
  [attr.aria-label]="collapsed() ? expandLabel() : collapseLabel()"
9023
9645
  [attr.aria-expanded]="!collapsed()"
9024
9646
  (click)="store.toggle()"
@@ -9029,8 +9651,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
9029
9651
  />
9030
9652
  </button>
9031
9653
  <button
9654
+ #closeBtn
9032
9655
  type="button"
9033
- 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"
9656
+ class="inline-flex h-9 w-9 items-center justify-center rounded-control transition-colors focus-visible:outline-none focus-visible:ring-2 lg:hidden"
9657
+ [class]="railBtnClass()"
9034
9658
  [attr.aria-label]="closeLabel()"
9035
9659
  (click)="store.closeMobile()"
9036
9660
  >
@@ -9039,18 +9663,18 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
9039
9663
  </div>
9040
9664
 
9041
9665
  <!-- Navigation -->
9042
- <nav class="flex-1 overflow-y-auto overflow-x-hidden px-3 pb-4">
9666
+ <nav class="relative flex-1 overflow-y-auto overflow-x-hidden px-3 pb-4">
9043
9667
  <ng-content />
9044
9668
  </nav>
9045
9669
 
9046
9670
  <!-- Footer (search, version, …) -->
9047
- <div class="px-3 pb-4 pt-2">
9671
+ <div class="relative px-3 pb-4 pt-2">
9048
9672
  <ng-content select="[sdsFooter]" />
9049
9673
  </div>
9050
9674
  </aside>
9051
9675
  `,
9052
9676
  }]
9053
- }], 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 }] }] } });
9677
+ }], ctorParameters: () => [], propDecorators: { drawer: [{ type: i0.ViewChild, args: ['drawer', { isSignal: true }] }], closeBtn: [{ type: i0.ViewChild, args: ['closeBtn', { isSignal: true }] }], 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 }] }], appearance: [{ type: i0.Input, args: [{ isSignal: true, alias: "appearance", required: false }] }], decorations: [{ type: i0.Input, args: [{ isSignal: true, alias: "decorations", required: false }] }] } });
9054
9678
 
9055
9679
  /**
9056
9680
  * Groups nav items under an optional uppercase section label.
@@ -9088,13 +9712,39 @@ class SdsNavGroup {
9088
9712
  /** Explicit user choice; null means "follow the route". */
9089
9713
  manual = signal(null, ...(ngDevMode ? [{ debugName: "manual" }] : []));
9090
9714
  url = toSignal(this.router.events.pipe(filter((e) => e instanceof NavigationEnd), map((e) => e.urlAfterRedirects)), { initialValue: this.router.url });
9091
- /** True while the active route lives inside this group. */
9715
+ /**
9716
+ * True while the active route lives inside this group. Matches on segment boundaries, not a raw string
9717
+ * prefix, so `/users/42` does NOT activate a group whose basePath is `/user` (query/hash are stripped
9718
+ * first).
9719
+ */
9092
9720
  routeActive = computed(() => {
9093
9721
  const base = this.basePath();
9094
- return !!base && this.url().startsWith(base);
9722
+ if (!base)
9723
+ return false;
9724
+ const path = this.url().split(/[?#]/)[0];
9725
+ return path === base || path.startsWith(base + '/');
9095
9726
  }, ...(ngDevMode ? [{ debugName: "routeActive" }] : []));
9096
9727
  expanded = computed(() => this.manual() ?? this.routeActive(), ...(ngDevMode ? [{ debugName: "expanded" }] : []));
9097
9728
  collapsed = this.store.collapsed;
9729
+ /** Rail paint, shared via the store — see SidebarStore.appearance. Drives the header/caption tones. */
9730
+ surface = computed(() => this.store.appearance() === 'surface', ...(ngDevMode ? [{ debugName: "surface" }] : []));
9731
+ /** Collapsible header, painted for the active rail (ink+brand on `surface`, white on `brand`). */
9732
+ headerClass = computed(() => {
9733
+ const s = this.surface();
9734
+ const base = 'mb-1 flex h-10 w-full items-center gap-3 rounded-control px-3 text-sm font-medium ' +
9735
+ 'transition-colors focus-visible:outline-none focus-visible:ring-2 ' +
9736
+ (this.collapsed() ? 'lg:justify-center ' : '') +
9737
+ (s
9738
+ ? 'text-ink-soft hover:bg-muted hover:text-ink focus-visible:ring-brand-500/40'
9739
+ : 'text-white/70 hover:bg-white/8 hover:text-white focus-visible:ring-white/40');
9740
+ // text-ink (not brand-700) stays readable in dark, where the grayscale brand-700 is near-black.
9741
+ const active = this.routeActive() ? (s ? ' text-ink' : ' text-white') : '';
9742
+ return base + active;
9743
+ }, ...(ngDevMode ? [{ debugName: "headerClass" }] : []));
9744
+ chevronClass = computed(() => 'shrink-0 transition-transform ' + (this.surface() ? 'text-ink-faint' : 'text-white/50'), ...(ngDevMode ? [{ debugName: "chevronClass" }] : []));
9745
+ dividerClass = computed(() => 'mx-3 mb-2 mt-2 h-px ' + (this.surface() ? 'bg-border' : 'bg-white/10'), ...(ngDevMode ? [{ debugName: "dividerClass" }] : []));
9746
+ captionClass = computed(() => 'mb-1.5 px-3 text-[11px] font-semibold uppercase tracking-wider ' +
9747
+ (this.surface() ? 'text-ink-faint' : 'text-white/60'), ...(ngDevMode ? [{ debugName: "captionClass" }] : []));
9098
9748
  /**
9099
9749
  * A plain section always shows its items — they carry their own icons, so they survive the collapsed
9100
9750
  * rail. A collapsible group hides its children there instead: its children are sub-destinations and
@@ -9115,7 +9765,7 @@ class SdsNavGroup {
9115
9765
  parts.push('hidden');
9116
9766
  }
9117
9767
  if (this.collapsible() && !this.collapsed()) {
9118
- parts.push('ml-5 border-l border-white/10 pl-3');
9768
+ parts.push(this.surface() ? 'ml-5 border-l border-border pl-3' : 'ml-5 border-l border-white/10 pl-3');
9119
9769
  }
9120
9770
  return parts.join(' ');
9121
9771
  }, ...(ngDevMode ? [{ debugName: "childrenClass" }] : []));
@@ -9147,11 +9797,7 @@ class SdsNavGroup {
9147
9797
  -->
9148
9798
  <button
9149
9799
  type="button"
9150
- class="mb-1 flex h-10 w-full items-center gap-3 rounded-control px-3 text-sm font-medium
9151
- text-white/70 transition-colors hover:bg-white/8 hover:text-white
9152
- focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/40"
9153
- [class.lg:justify-center]="collapsed()"
9154
- [class.text-white]="routeActive()"
9800
+ [class]="headerClass()"
9155
9801
  [attr.aria-expanded]="collapsed() ? null : expanded()"
9156
9802
  [attr.title]="collapsed() ? label() : null"
9157
9803
  [attr.aria-label]="collapsed() ? label() : null"
@@ -9166,15 +9812,15 @@ class SdsNavGroup {
9166
9812
  <sds-icon
9167
9813
  name="chevron-down"
9168
9814
  [size]="16"
9169
- class="shrink-0 text-white/50 transition-transform"
9815
+ [class]="chevronClass()"
9170
9816
  [class.lg:hidden]="collapsed()"
9171
9817
  [class.rotate-180]="expanded()"
9172
9818
  />
9173
9819
  </button>
9174
9820
  } @else if (collapsed()) {
9175
- <div class="mx-3 mb-2 mt-2 h-px bg-white/10"></div>
9821
+ <div [class]="dividerClass()"></div>
9176
9822
  } @else {
9177
- <p class="mb-1.5 px-3 text-[11px] font-semibold uppercase tracking-wider text-white/60">
9823
+ <p [class]="captionClass()">
9178
9824
  {{ label() }}
9179
9825
  </p>
9180
9826
  }
@@ -9210,11 +9856,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
9210
9856
  -->
9211
9857
  <button
9212
9858
  type="button"
9213
- class="mb-1 flex h-10 w-full items-center gap-3 rounded-control px-3 text-sm font-medium
9214
- text-white/70 transition-colors hover:bg-white/8 hover:text-white
9215
- focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/40"
9216
- [class.lg:justify-center]="collapsed()"
9217
- [class.text-white]="routeActive()"
9859
+ [class]="headerClass()"
9218
9860
  [attr.aria-expanded]="collapsed() ? null : expanded()"
9219
9861
  [attr.title]="collapsed() ? label() : null"
9220
9862
  [attr.aria-label]="collapsed() ? label() : null"
@@ -9229,15 +9871,15 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
9229
9871
  <sds-icon
9230
9872
  name="chevron-down"
9231
9873
  [size]="16"
9232
- class="shrink-0 text-white/50 transition-transform"
9874
+ [class]="chevronClass()"
9233
9875
  [class.lg:hidden]="collapsed()"
9234
9876
  [class.rotate-180]="expanded()"
9235
9877
  />
9236
9878
  </button>
9237
9879
  } @else if (collapsed()) {
9238
- <div class="mx-3 mb-2 mt-2 h-px bg-white/10"></div>
9880
+ <div [class]="dividerClass()"></div>
9239
9881
  } @else {
9240
- <p class="mb-1.5 px-3 text-[11px] font-semibold uppercase tracking-wider text-white/60">
9882
+ <p [class]="captionClass()">
9241
9883
  {{ label() }}
9242
9884
  </p>
9243
9885
  }
@@ -9273,15 +9915,31 @@ class SdsNavItem {
9273
9915
  select = output();
9274
9916
  collapsed = this.store.collapsed;
9275
9917
  itemClass(isActive) {
9276
- const base = 'group flex w-full items-center gap-3 rounded-control px-3 text-sm font-medium ' +
9918
+ // A `surface` rail is light: an active item is a brand-tinted pill with a left accent bar, brand
9919
+ // text and a soft shadow (matching the certicheck reference); inactive is muted ink that lifts on
9920
+ // hover. A `brand` rail is dark, so an active item is a solid brand-500 pill in white. Read from the
9921
+ // shared store so the whole rail switches from one input on <sds-sidebar>.
9922
+ const surface = this.store.appearance() === 'surface';
9923
+ const base = 'group relative flex w-full items-center gap-3 rounded-control px-3 text-sm font-medium ' +
9277
9924
  'transition-colors duration-150 focus-visible:outline-none focus-visible:ring-2 ' +
9278
- 'focus-visible:ring-white/40 ' +
9925
+ (surface ? 'focus-visible:ring-brand-500/40 ' : 'focus-visible:ring-white/40 ') +
9279
9926
  (this.collapsed() ? 'h-10 lg:justify-center' : 'h-10');
9280
9927
  const state = isActive
9281
- ? 'bg-brand-500 text-white shadow-sm'
9282
- : 'text-white/70 hover:bg-white/8 hover:text-white';
9928
+ ? surface
9929
+ ? 'bg-linear-to-r from-brand-500/15 to-brand-500/5 text-ink shadow-sm'
9930
+ : 'bg-brand-500 text-white shadow-sm'
9931
+ : surface
9932
+ ? 'text-ink-soft hover:bg-brand-500/8 hover:text-ink'
9933
+ : 'text-white/70 hover:bg-white/8 hover:text-white';
9283
9934
  return `${base} ${state}`;
9284
9935
  }
9936
+ /**
9937
+ * The left accent bar on an active item — the strongest "selected" cue, and the one that survives a
9938
+ * grayscale brand where a tinted pill alone is nearly invisible. brand-600 on the light rail, white on
9939
+ * the dark one; on a coloured brand it is the brand colour, matching the certicheck accent bar.
9940
+ */
9941
+ accentBarClass = computed(() => 'pointer-events-none absolute left-1 top-1/2 h-5 w-1 -translate-y-1/2 rounded-full ' +
9942
+ (this.store.appearance() === 'surface' ? 'bg-brand-500' : 'bg-white'), ...(ngDevMode ? [{ debugName: "accentBarClass" }] : []));
9285
9943
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: SdsNavItem, deps: [], target: i0.ɵɵFactoryTarget.Component });
9286
9944
  static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.25", type: SdsNavItem, isStandalone: true, selector: "sds-nav-item", inputs: { icon: { classPropertyName: "icon", publicName: "icon", isSignal: true, isRequired: false, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: true, transformFunction: null }, link: { classPropertyName: "link", publicName: "link", isSignal: true, isRequired: false, transformFunction: null }, exact: { classPropertyName: "exact", publicName: "exact", isSignal: true, isRequired: false, transformFunction: null }, active: { classPropertyName: "active", publicName: "active", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { select: "select" }, host: { classAttribute: "block" }, ngImport: i0, template: `
9287
9945
  @if (link() !== null) {
@@ -9296,6 +9954,9 @@ class SdsNavItem {
9296
9954
  [attr.aria-current]="rla.isActive || active() ? 'page' : null"
9297
9955
  (click)="store.closeMobile()"
9298
9956
  >
9957
+ @if (rla.isActive || active()) {
9958
+ <span [class]="accentBarClass()" aria-hidden="true"></span>
9959
+ }
9299
9960
  <ng-container [ngTemplateOutlet]="content" />
9300
9961
  </a>
9301
9962
  } @else {
@@ -9307,6 +9968,9 @@ class SdsNavItem {
9307
9968
  [attr.aria-current]="active() ? 'page' : null"
9308
9969
  (click)="select.emit(); store.closeMobile()"
9309
9970
  >
9971
+ @if (active()) {
9972
+ <span [class]="accentBarClass()" aria-hidden="true"></span>
9973
+ }
9310
9974
  <ng-container [ngTemplateOutlet]="content" />
9311
9975
  </button>
9312
9976
  }
@@ -9340,6 +10004,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
9340
10004
  [attr.aria-current]="rla.isActive || active() ? 'page' : null"
9341
10005
  (click)="store.closeMobile()"
9342
10006
  >
10007
+ @if (rla.isActive || active()) {
10008
+ <span [class]="accentBarClass()" aria-hidden="true"></span>
10009
+ }
9343
10010
  <ng-container [ngTemplateOutlet]="content" />
9344
10011
  </a>
9345
10012
  } @else {
@@ -9351,6 +10018,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
9351
10018
  [attr.aria-current]="active() ? 'page' : null"
9352
10019
  (click)="select.emit(); store.closeMobile()"
9353
10020
  >
10021
+ @if (active()) {
10022
+ <span [class]="accentBarClass()" aria-hidden="true"></span>
10023
+ }
9354
10024
  <ng-container [ngTemplateOutlet]="content" />
9355
10025
  </button>
9356
10026
  }
@@ -9366,6 +10036,775 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
9366
10036
  }]
9367
10037
  }], propDecorators: { icon: [{ type: i0.Input, args: [{ isSignal: true, alias: "icon", required: false }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: true }] }], link: [{ type: i0.Input, args: [{ isSignal: true, alias: "link", required: false }] }], exact: [{ type: i0.Input, args: [{ isSignal: true, alias: "exact", required: false }] }], active: [{ type: i0.Input, args: [{ isSignal: true, alias: "active", required: false }] }], select: [{ type: i0.Output, args: ["select"] }] } });
9368
10038
 
10039
+ /**
10040
+ * The signed-in user's trigger and dropdown for the top bar: an avatar (with the name and email beside
10041
+ * it on wider screens) that opens a panel with a gradient header and whatever menu rows the app projects
10042
+ * — profile, settings, sign out. The library deliberately owns no fixed set of actions, because what
10043
+ * belongs here differs per app; project <sds-menu-item> (or any markup) into the default slot.
10044
+ *
10045
+ * It manages its own open state, closes on outside click, on Escape, and whenever a menu row is
10046
+ * activated, restoring focus to the trigger. Two appearances so the trigger sits correctly on either
10047
+ * bar: `surface` (default) for a light / glass bar, `on-brand` for the dark brand bar — the same default
10048
+ * as its top-bar siblings sds-theme-toggle and sds-search-command.
10049
+ *
10050
+ * It is a disclosure (aria-haspopup + aria-expanded on the trigger), not an ARIA menu widget: the rows
10051
+ * are ordinary links/buttons the app projects, navigated with Tab like any popover.
10052
+ *
10053
+ * @example
10054
+ * <sds-user-menu name="Souhail AIT SAID" email="souhail@example.com" status="online" appearance="surface">
10055
+ * <sds-menu-item icon="user" label="Profil" link="/profile" />
10056
+ * <sds-menu-item icon="settings" label="Paramètres" link="/settings" />
10057
+ * <sds-menu-item icon="log-out" label="Se déconnecter" tone="danger" (select)="signOut()" />
10058
+ * </sds-user-menu>
10059
+ */
10060
+ class SdsUserMenu {
10061
+ el = inject(ElementRef);
10062
+ trigger = viewChild('trigger', ...(ngDevMode ? [{ debugName: "trigger" }] : []));
10063
+ name = input('', ...(ngDevMode ? [{ debugName: "name" }] : []));
10064
+ email = input('', ...(ngDevMode ? [{ debugName: "email" }] : []));
10065
+ initials = input('', ...(ngDevMode ? [{ debugName: "initials" }] : []));
10066
+ src = input(null, ...(ngDevMode ? [{ debugName: "src" }] : []));
10067
+ status = input(null, ...(ngDevMode ? [{ debugName: "status" }] : []));
10068
+ /** `surface` (default) for a light / glass bar; `on-brand` for the dark brand bar. */
10069
+ appearance = input('surface', ...(ngDevMode ? [{ debugName: "appearance" }] : []));
10070
+ /** Which edge the panel aligns to. `end` (default) hangs the panel from the right of the trigger. */
10071
+ align = input('end', ...(ngDevMode ? [{ debugName: "align" }] : []));
10072
+ /** Show the name/email beside the avatar (≥ sm). Turn off for an avatar-only trigger. */
10073
+ showDetails = input(true, ...(ngDevMode ? [{ debugName: "showDetails", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
10074
+ /** Trigger avatar box size in px. */
10075
+ avatarSize = input(36, ...(ngDevMode ? [{ debugName: "avatarSize" }] : []));
10076
+ /** Override the trigger avatar's initials background; defaults per appearance. */
10077
+ avatarBgClass = input(null, ...(ngDevMode ? [{ debugName: "avatarBgClass" }] : []));
10078
+ /** Accessible name for the trigger (icon/avatar only when details are hidden). */
10079
+ menuLabel = input('Menu du compte', ...(ngDevMode ? [{ debugName: "menuLabel" }] : []));
10080
+ openChange = output();
10081
+ open = signal(false, ...(ngDevMode ? [{ debugName: "open" }] : []));
10082
+ toggle() {
10083
+ this.open.update((v) => !v);
10084
+ this.openChange.emit(this.open());
10085
+ }
10086
+ close() {
10087
+ if (!this.open())
10088
+ return;
10089
+ this.open.set(false);
10090
+ this.openChange.emit(false);
10091
+ }
10092
+ onDocumentClick(event) {
10093
+ if (this.open() && !this.el.nativeElement.contains(event.target)) {
10094
+ this.close();
10095
+ }
10096
+ }
10097
+ onEscape() {
10098
+ if (!this.open())
10099
+ return;
10100
+ this.close();
10101
+ // Keyboard dismissal returns focus to the trigger (a click-away deliberately does not).
10102
+ this.trigger()?.nativeElement.focus();
10103
+ }
10104
+ /** Close when any actionable row (link or button) inside the panel is activated. */
10105
+ onPanelClick(event) {
10106
+ if (event.target.closest('a,button')) {
10107
+ this.close();
10108
+ this.trigger()?.nativeElement.focus();
10109
+ }
10110
+ }
10111
+ onBrand = computed(() => this.appearance() === 'on-brand', ...(ngDevMode ? [{ debugName: "onBrand" }] : []));
10112
+ resolvedAvatarBg = computed(() => this.avatarBgClass() ?? (this.onBrand() ? 'bg-white/15' : 'bg-brand-500'), ...(ngDevMode ? [{ debugName: "resolvedAvatarBg" }] : []));
10113
+ headerAvatarBg = computed(() => 'bg-brand-500', ...(ngDevMode ? [{ debugName: "headerAvatarBg" }] : []));
10114
+ triggerClass = computed(() => this.onBrand()
10115
+ ? 'hover:bg-white/10 focus-visible:ring-white/40'
10116
+ : 'hover:bg-muted focus-visible:ring-brand-500/40', ...(ngDevMode ? [{ debugName: "triggerClass" }] : []));
10117
+ nameClass = computed(() => (this.onBrand() ? 'text-white' : 'text-ink'), ...(ngDevMode ? [{ debugName: "nameClass" }] : []));
10118
+ emailClass = computed(() => (this.onBrand() ? 'text-white/70' : 'text-ink-faint'), ...(ngDevMode ? [{ debugName: "emailClass" }] : []));
10119
+ chevronClass = computed(() => (this.onBrand() ? 'text-white/70' : 'text-ink-faint'), ...(ngDevMode ? [{ debugName: "chevronClass" }] : []));
10120
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: SdsUserMenu, deps: [], target: i0.ɵɵFactoryTarget.Component });
10121
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.25", type: SdsUserMenu, isStandalone: true, selector: "sds-user-menu", inputs: { name: { classPropertyName: "name", publicName: "name", isSignal: true, isRequired: false, transformFunction: null }, email: { classPropertyName: "email", publicName: "email", isSignal: true, isRequired: false, transformFunction: null }, initials: { classPropertyName: "initials", publicName: "initials", isSignal: true, isRequired: false, transformFunction: null }, src: { classPropertyName: "src", publicName: "src", isSignal: true, isRequired: false, transformFunction: null }, status: { classPropertyName: "status", publicName: "status", isSignal: true, isRequired: false, transformFunction: null }, appearance: { classPropertyName: "appearance", publicName: "appearance", isSignal: true, isRequired: false, transformFunction: null }, align: { classPropertyName: "align", publicName: "align", isSignal: true, isRequired: false, transformFunction: null }, showDetails: { classPropertyName: "showDetails", publicName: "showDetails", isSignal: true, isRequired: false, transformFunction: null }, avatarSize: { classPropertyName: "avatarSize", publicName: "avatarSize", isSignal: true, isRequired: false, transformFunction: null }, avatarBgClass: { classPropertyName: "avatarBgClass", publicName: "avatarBgClass", isSignal: true, isRequired: false, transformFunction: null }, menuLabel: { classPropertyName: "menuLabel", publicName: "menuLabel", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { openChange: "openChange" }, host: { listeners: { "document:click": "onDocumentClick($event)", "document:keydown.escape": "onEscape()" }, classAttribute: "relative inline-block" }, viewQueries: [{ propertyName: "trigger", first: true, predicate: ["trigger"], descendants: true, isSignal: true }], ngImport: i0, template: `
10122
+ <button
10123
+ #trigger
10124
+ type="button"
10125
+ class="flex items-center gap-2.5 rounded-control py-1.5 pl-1.5 pr-2.5 transition-colors focus-visible:outline-none focus-visible:ring-2"
10126
+ [class]="triggerClass()"
10127
+ [attr.aria-label]="menuLabel()"
10128
+ aria-haspopup="true"
10129
+ [attr.aria-expanded]="open()"
10130
+ (click)="toggle()"
10131
+ >
10132
+ <sds-avatar
10133
+ [name]="name()"
10134
+ [initials]="initials()"
10135
+ [src]="src()"
10136
+ [status]="status()"
10137
+ [size]="avatarSize()"
10138
+ [bgClass]="resolvedAvatarBg()"
10139
+ />
10140
+ @if (showDetails()) {
10141
+ <span class="hidden min-w-0 flex-col items-start text-left leading-tight sm:flex">
10142
+ <span class="max-w-40 truncate text-sm font-semibold" [class]="nameClass()">{{ name() }}</span>
10143
+ @if (email()) {
10144
+ <span class="max-w-40 truncate text-xs" [class]="emailClass()">{{ email() }}</span>
10145
+ }
10146
+ </span>
10147
+ }
10148
+ <sds-icon
10149
+ name="chevron-down"
10150
+ [size]="16"
10151
+ class="hidden shrink-0 transition-transform sm:block"
10152
+ [class]="chevronClass()"
10153
+ [class.rotate-180]="open()"
10154
+ />
10155
+ </button>
10156
+
10157
+ @if (open()) {
10158
+ <div
10159
+ class="absolute top-full z-50 mt-2 w-64 overflow-hidden rounded-card border border-border bg-surface shadow-xl animate-sds-menu"
10160
+ [class]="align() === 'end' ? 'right-0' : 'left-0'"
10161
+ (click)="onPanelClick($event)"
10162
+ >
10163
+ <!-- Header: identity, on a soft brand wash. -->
10164
+ <div class="flex items-center gap-3 border-b border-border bg-linear-to-br from-brand-50 via-surface to-brand-100/40 p-4">
10165
+ <sds-avatar
10166
+ [name]="name()"
10167
+ [initials]="initials()"
10168
+ [src]="src()"
10169
+ [size]="44"
10170
+ [bgClass]="headerAvatarBg()"
10171
+ />
10172
+ <div class="min-w-0">
10173
+ <p class="truncate text-sm font-semibold text-ink">{{ name() }}</p>
10174
+ @if (email()) {
10175
+ <p class="mt-0.5 truncate text-xs text-ink-faint">{{ email() }}</p>
10176
+ }
10177
+ </div>
10178
+ </div>
10179
+
10180
+ <!-- Actions projected by the app. -->
10181
+ <div class="py-1.5">
10182
+ <ng-content />
10183
+ </div>
10184
+ </div>
10185
+ }
10186
+ `, isInline: true, styles: [".animate-sds-menu{animation:sds-menu-in .14s ease-out}@keyframes sds-menu-in{0%{opacity:0;transform:translateY(-6px)}to{opacity:1;transform:translateY(0)}}@media(prefers-reduced-motion:reduce){.animate-sds-menu{animation:none}}\n"], 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 });
10187
+ }
10188
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: SdsUserMenu, decorators: [{
10189
+ type: Component,
10190
+ args: [{ selector: 'sds-user-menu', changeDetection: ChangeDetectionStrategy.OnPush, imports: [SdsAvatar, SdsIcon], host: {
10191
+ class: 'relative inline-block',
10192
+ '(document:click)': 'onDocumentClick($event)',
10193
+ '(document:keydown.escape)': 'onEscape()',
10194
+ }, template: `
10195
+ <button
10196
+ #trigger
10197
+ type="button"
10198
+ class="flex items-center gap-2.5 rounded-control py-1.5 pl-1.5 pr-2.5 transition-colors focus-visible:outline-none focus-visible:ring-2"
10199
+ [class]="triggerClass()"
10200
+ [attr.aria-label]="menuLabel()"
10201
+ aria-haspopup="true"
10202
+ [attr.aria-expanded]="open()"
10203
+ (click)="toggle()"
10204
+ >
10205
+ <sds-avatar
10206
+ [name]="name()"
10207
+ [initials]="initials()"
10208
+ [src]="src()"
10209
+ [status]="status()"
10210
+ [size]="avatarSize()"
10211
+ [bgClass]="resolvedAvatarBg()"
10212
+ />
10213
+ @if (showDetails()) {
10214
+ <span class="hidden min-w-0 flex-col items-start text-left leading-tight sm:flex">
10215
+ <span class="max-w-40 truncate text-sm font-semibold" [class]="nameClass()">{{ name() }}</span>
10216
+ @if (email()) {
10217
+ <span class="max-w-40 truncate text-xs" [class]="emailClass()">{{ email() }}</span>
10218
+ }
10219
+ </span>
10220
+ }
10221
+ <sds-icon
10222
+ name="chevron-down"
10223
+ [size]="16"
10224
+ class="hidden shrink-0 transition-transform sm:block"
10225
+ [class]="chevronClass()"
10226
+ [class.rotate-180]="open()"
10227
+ />
10228
+ </button>
10229
+
10230
+ @if (open()) {
10231
+ <div
10232
+ class="absolute top-full z-50 mt-2 w-64 overflow-hidden rounded-card border border-border bg-surface shadow-xl animate-sds-menu"
10233
+ [class]="align() === 'end' ? 'right-0' : 'left-0'"
10234
+ (click)="onPanelClick($event)"
10235
+ >
10236
+ <!-- Header: identity, on a soft brand wash. -->
10237
+ <div class="flex items-center gap-3 border-b border-border bg-linear-to-br from-brand-50 via-surface to-brand-100/40 p-4">
10238
+ <sds-avatar
10239
+ [name]="name()"
10240
+ [initials]="initials()"
10241
+ [src]="src()"
10242
+ [size]="44"
10243
+ [bgClass]="headerAvatarBg()"
10244
+ />
10245
+ <div class="min-w-0">
10246
+ <p class="truncate text-sm font-semibold text-ink">{{ name() }}</p>
10247
+ @if (email()) {
10248
+ <p class="mt-0.5 truncate text-xs text-ink-faint">{{ email() }}</p>
10249
+ }
10250
+ </div>
10251
+ </div>
10252
+
10253
+ <!-- Actions projected by the app. -->
10254
+ <div class="py-1.5">
10255
+ <ng-content />
10256
+ </div>
10257
+ </div>
10258
+ }
10259
+ `, styles: [".animate-sds-menu{animation:sds-menu-in .14s ease-out}@keyframes sds-menu-in{0%{opacity:0;transform:translateY(-6px)}to{opacity:1;transform:translateY(0)}}@media(prefers-reduced-motion:reduce){.animate-sds-menu{animation:none}}\n"] }]
10260
+ }], propDecorators: { trigger: [{ type: i0.ViewChild, args: ['trigger', { isSignal: true }] }], name: [{ type: i0.Input, args: [{ isSignal: true, alias: "name", required: false }] }], email: [{ type: i0.Input, args: [{ isSignal: true, alias: "email", required: false }] }], initials: [{ type: i0.Input, args: [{ isSignal: true, alias: "initials", required: false }] }], src: [{ type: i0.Input, args: [{ isSignal: true, alias: "src", required: false }] }], status: [{ type: i0.Input, args: [{ isSignal: true, alias: "status", required: false }] }], appearance: [{ type: i0.Input, args: [{ isSignal: true, alias: "appearance", required: false }] }], align: [{ type: i0.Input, args: [{ isSignal: true, alias: "align", required: false }] }], showDetails: [{ type: i0.Input, args: [{ isSignal: true, alias: "showDetails", required: false }] }], avatarSize: [{ type: i0.Input, args: [{ isSignal: true, alias: "avatarSize", required: false }] }], avatarBgClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "avatarBgClass", required: false }] }], menuLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "menuLabel", required: false }] }], openChange: [{ type: i0.Output, args: ["openChange"] }] } });
10261
+
10262
+ /**
10263
+ * A single row inside a menu surface (the {@link SdsUserMenu} dropdown, or any custom popover). Renders
10264
+ * an anchor when [link] is set — with the Router — or a button otherwise, and emits {@link select} on
10265
+ * activation. The parent menu closes itself when an item is activated, so an item never has to reach
10266
+ * back into the menu.
10267
+ *
10268
+ * `tone="danger"` paints a destructive action (sign out, delete). Trailing content (a shortcut hint, a
10269
+ * count) projects into the default slot after the label.
10270
+ *
10271
+ * @example
10272
+ * <sds-menu-item icon="user" label="Profil" link="/profile" />
10273
+ * <sds-menu-item icon="log-out" label="Se déconnecter" tone="danger" (select)="signOut()" />
10274
+ */
10275
+ class SdsMenuItem {
10276
+ icon = input(null, ...(ngDevMode ? [{ debugName: "icon" }] : []));
10277
+ label = input.required(...(ngDevMode ? [{ debugName: "label" }] : []));
10278
+ /** Router link target; when null (default) the item renders as a button. */
10279
+ link = input(null, ...(ngDevMode ? [{ debugName: "link" }] : []));
10280
+ tone = input('default', ...(ngDevMode ? [{ debugName: "tone" }] : []));
10281
+ disabled = input(false, ...(ngDevMode ? [{ debugName: "disabled", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
10282
+ select = output();
10283
+ rowClass = computed(() => {
10284
+ const base = 'flex w-full items-center gap-3 px-4 py-2.5 text-sm font-medium transition-colors ' +
10285
+ 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset ' +
10286
+ 'disabled:opacity-50 disabled:pointer-events-none';
10287
+ const tone = this.tone() === 'danger'
10288
+ ? 'text-danger hover:bg-danger/10 focus-visible:ring-danger/40'
10289
+ : 'text-ink-soft hover:bg-muted hover:text-ink focus-visible:ring-brand-500/40';
10290
+ return `${base} ${tone}`;
10291
+ }, ...(ngDevMode ? [{ debugName: "rowClass" }] : []));
10292
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: SdsMenuItem, deps: [], target: i0.ɵɵFactoryTarget.Component });
10293
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.25", type: SdsMenuItem, isStandalone: true, selector: "sds-menu-item", inputs: { icon: { classPropertyName: "icon", publicName: "icon", isSignal: true, isRequired: false, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: true, transformFunction: null }, link: { classPropertyName: "link", publicName: "link", isSignal: true, isRequired: false, transformFunction: null }, tone: { classPropertyName: "tone", publicName: "tone", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { select: "select" }, host: { classAttribute: "block" }, ngImport: i0, template: `
10294
+ @if (link() !== null) {
10295
+ <a [routerLink]="link()" [class]="rowClass()" (click)="select.emit()">
10296
+ <ng-container [ngTemplateOutlet]="content" />
10297
+ </a>
10298
+ } @else {
10299
+ <button type="button" [class]="rowClass()" [disabled]="disabled()" (click)="select.emit()">
10300
+ <ng-container [ngTemplateOutlet]="content" />
10301
+ </button>
10302
+ }
10303
+
10304
+ <ng-template #content>
10305
+ @if (icon()) {
10306
+ <sds-icon [name]="icon()!" [size]="18" class="shrink-0" />
10307
+ }
10308
+ <span class="flex-1 truncate text-left">{{ label() }}</span>
10309
+ <ng-content />
10310
+ </ng-template>
10311
+ `, isInline: true, dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "directive", type: RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "routerLink"] }, { kind: "component", type: SdsIcon, selector: "sds-icon", inputs: ["name", "size", "strokeWidth"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
10312
+ }
10313
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: SdsMenuItem, decorators: [{
10314
+ type: Component,
10315
+ args: [{
10316
+ selector: 'sds-menu-item',
10317
+ changeDetection: ChangeDetectionStrategy.OnPush,
10318
+ imports: [NgTemplateOutlet, RouterLink, SdsIcon],
10319
+ host: { class: 'block' },
10320
+ template: `
10321
+ @if (link() !== null) {
10322
+ <a [routerLink]="link()" [class]="rowClass()" (click)="select.emit()">
10323
+ <ng-container [ngTemplateOutlet]="content" />
10324
+ </a>
10325
+ } @else {
10326
+ <button type="button" [class]="rowClass()" [disabled]="disabled()" (click)="select.emit()">
10327
+ <ng-container [ngTemplateOutlet]="content" />
10328
+ </button>
10329
+ }
10330
+
10331
+ <ng-template #content>
10332
+ @if (icon()) {
10333
+ <sds-icon [name]="icon()!" [size]="18" class="shrink-0" />
10334
+ }
10335
+ <span class="flex-1 truncate text-left">{{ label() }}</span>
10336
+ <ng-content />
10337
+ </ng-template>
10338
+ `,
10339
+ }]
10340
+ }], propDecorators: { icon: [{ type: i0.Input, args: [{ isSignal: true, alias: "icon", required: false }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: true }] }], link: [{ type: i0.Input, args: [{ isSignal: true, alias: "link", required: false }] }], tone: [{ type: i0.Input, args: [{ isSignal: true, alias: "tone", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], select: [{ type: i0.Output, args: ["select"] }] } });
10341
+
10342
+ const norm = (s) => s.normalize('NFD').replace(/[̀-ͯ]/g, '').toLowerCase();
10343
+ /**
10344
+ * A command-palette search: a trigger pill (search icon, placeholder, a ⌘K hint) that opens a modal with
10345
+ * a search field, grouped results and keyboard navigation. It is data-driven — the app passes `[items]`
10346
+ * and handles `(select)` — so the library stays router-agnostic; ⌘K / Ctrl-K opens it from anywhere.
10347
+ *
10348
+ * When the field is empty the items show as quick navigation; typing filters by label and keywords
10349
+ * (accent-insensitive). Arrow keys move the highlight, Enter selects, Escape closes. Hide the built-in
10350
+ * pill with `[showTrigger]="false"` and drive it from your own control via a template ref: `cmd.open()`.
10351
+ *
10352
+ * @example
10353
+ * <sds-search-command
10354
+ * #cmd
10355
+ * appearance="surface"
10356
+ * [items]="navItems"
10357
+ * (select)="router.navigateByUrl($event.id)"
10358
+ * />
10359
+ */
10360
+ class SdsSearchCommand {
10361
+ document = inject(DOCUMENT);
10362
+ searchInput = viewChild('searchInput', ...(ngDevMode ? [{ debugName: "searchInput" }] : []));
10363
+ overlay = viewChild('overlay', ...(ngDevMode ? [{ debugName: "overlay" }] : []));
10364
+ /** The searchable entries. Order is preserved; when the field is empty they show as quick navigation. */
10365
+ items = input([], ...(ngDevMode ? [{ debugName: "items" }] : []));
10366
+ /** Placeholder on the trigger pill and the search field. */
10367
+ placeholder = input('Rechercher…', ...(ngDevMode ? [{ debugName: "placeholder" }] : []));
10368
+ /** Show the built-in trigger pill. Turn off to drive the palette from your own control (`cmd.open()`). */
10369
+ showTrigger = input(true, ...(ngDevMode ? [{ debugName: "showTrigger", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
10370
+ /** Enable the global ⌘K / Ctrl-K shortcut. */
10371
+ hotkey = input(true, ...(ngDevMode ? [{ debugName: "hotkey", transform: booleanAttribute }] : [{ transform: booleanAttribute }]));
10372
+ /** Trigger pill minimum width in px, so it does not collapse in a flexible top-bar slot. */
10373
+ triggerMinWidth = input(220, ...(ngDevMode ? [{ debugName: "triggerMinWidth" }] : []));
10374
+ /** `surface` (default) for a light / glass bar; `on-brand` for the dark brand bar. */
10375
+ appearance = input('surface', ...(ngDevMode ? [{ debugName: "appearance" }] : []));
10376
+ /**
10377
+ * User-facing copy. French defaults; a multilingual app passes translated values (the trigger and the
10378
+ * field share `placeholder`, and `searchLabel` is the accessible name for both).
10379
+ */
10380
+ searchLabel = input('Rechercher', ...(ngDevMode ? [{ debugName: "searchLabel" }] : []));
10381
+ clearLabel = input('Effacer', ...(ngDevMode ? [{ debugName: "clearLabel" }] : []));
10382
+ emptyLabel = input('Aucun résultat', ...(ngDevMode ? [{ debugName: "emptyLabel" }] : []));
10383
+ selectHint = input('Sélectionner', ...(ngDevMode ? [{ debugName: "selectHint" }] : []));
10384
+ navigateHint = input('Naviguer', ...(ngDevMode ? [{ debugName: "navigateHint" }] : []));
10385
+ closeHint = input('Fermer', ...(ngDevMode ? [{ debugName: "closeHint" }] : []));
10386
+ select = output();
10387
+ openChange = output();
10388
+ isOpen = signal(false, ...(ngDevMode ? [{ debugName: "isOpen" }] : []));
10389
+ query = signal('', ...(ngDevMode ? [{ debugName: "query" }] : []));
10390
+ /** DOM id of the highlighted option, or null. Kept as the id (not an index) so the list can regroup. */
10391
+ activeId = signal(null, ...(ngDevMode ? [{ debugName: "activeId" }] : []));
10392
+ /** The element focused before the palette opened, restored to it on close (WAI-ARIA dialog pattern). */
10393
+ restoreFocusTo = null;
10394
+ constructor() {
10395
+ // On open: move the overlay to <body> so it escapes any ancestor that is a containing block for
10396
+ // fixed positioning (a `backdrop-filter` on the glass top bar is exactly such an ancestor), then
10397
+ // focus the field and lock background scroll. Angular still owns the moved node — its bindings and
10398
+ // listeners keep working, and @if removes it on close.
10399
+ effect(() => {
10400
+ const open = this.isOpen();
10401
+ const overlay = this.overlay();
10402
+ const input = this.searchInput();
10403
+ this.document.body.style.overflow = open ? 'hidden' : '';
10404
+ if (open && overlay)
10405
+ this.document.body.appendChild(overlay.nativeElement);
10406
+ if (open && input)
10407
+ input.nativeElement.focus();
10408
+ });
10409
+ // Keep a valid highlight as the results change: default to the first option; drop it if empty.
10410
+ // activeId is read untracked so writing it here does not retrigger this effect.
10411
+ effect(() => {
10412
+ const flat = this.flat();
10413
+ const current = untracked(this.activeId);
10414
+ if (!flat.length) {
10415
+ if (current !== null)
10416
+ this.activeId.set(null);
10417
+ return;
10418
+ }
10419
+ if (!current || !flat.some((i) => this.optionId(i) === current)) {
10420
+ this.activeId.set(this.optionId(flat[0]));
10421
+ }
10422
+ });
10423
+ // A palette destroyed while open must not leave the page unscrollable.
10424
+ inject(DestroyRef).onDestroy(() => {
10425
+ this.document.body.style.overflow = '';
10426
+ });
10427
+ }
10428
+ // --- public control (also reachable via a template ref) ---
10429
+ open() {
10430
+ if (this.isOpen())
10431
+ return;
10432
+ // Remember what was focused (the trigger pill, or wherever ⌘K was pressed) to restore on close.
10433
+ this.restoreFocusTo = this.document.activeElement;
10434
+ this.isOpen.set(true);
10435
+ this.openChange.emit(true);
10436
+ }
10437
+ close() {
10438
+ if (!this.isOpen())
10439
+ return;
10440
+ this.isOpen.set(false);
10441
+ this.query.set('');
10442
+ this.openChange.emit(false);
10443
+ // Return focus to the invoker so keyboard/AT users are not dropped onto <body>.
10444
+ this.restoreFocusTo?.focus();
10445
+ this.restoreFocusTo = null;
10446
+ }
10447
+ toggle() {
10448
+ this.isOpen() ? this.close() : this.open();
10449
+ }
10450
+ // --- filtering ---
10451
+ filtered = computed(() => {
10452
+ const q = norm(this.query().trim());
10453
+ if (!q)
10454
+ return this.items();
10455
+ return this.items().filter((i) => norm(`${i.label} ${i.keywords ?? ''}`).includes(q));
10456
+ }, ...(ngDevMode ? [{ debugName: "filtered" }] : []));
10457
+ sections = computed(() => {
10458
+ const list = this.filtered();
10459
+ if (!list.length)
10460
+ return [];
10461
+ const anyGroup = list.some((i) => i.group);
10462
+ if (!anyGroup) {
10463
+ return [{ label: '', items: list }];
10464
+ }
10465
+ const order = [];
10466
+ const map = new Map();
10467
+ for (const item of list) {
10468
+ const g = item.group ?? '';
10469
+ if (!map.has(g)) {
10470
+ map.set(g, []);
10471
+ order.push(g);
10472
+ }
10473
+ map.get(g).push(item);
10474
+ }
10475
+ return order.map((label) => ({ label, items: map.get(label) }));
10476
+ }, ...(ngDevMode ? [{ debugName: "sections" }] : []));
10477
+ flat = computed(() => this.sections().flatMap((s) => s.items.filter((i) => !i.disabled)), ...(ngDevMode ? [{ debugName: "flat" }] : []));
10478
+ // --- interaction ---
10479
+ optionId(item) {
10480
+ return `sds-cmd-opt-${item.id}`;
10481
+ }
10482
+ onInput(event) {
10483
+ this.query.set(event.target.value);
10484
+ }
10485
+ clear() {
10486
+ this.query.set('');
10487
+ this.searchInput()?.nativeElement.focus();
10488
+ }
10489
+ setActiveById(id) {
10490
+ this.activeId.set(id);
10491
+ }
10492
+ choose(item) {
10493
+ if (item.disabled)
10494
+ return;
10495
+ this.select.emit(item);
10496
+ this.close();
10497
+ }
10498
+ onPanelKeydown(event) {
10499
+ switch (event.key) {
10500
+ case 'Escape':
10501
+ event.preventDefault();
10502
+ this.close();
10503
+ return;
10504
+ case 'ArrowDown':
10505
+ event.preventDefault();
10506
+ this.move(1);
10507
+ return;
10508
+ case 'ArrowUp':
10509
+ event.preventDefault();
10510
+ this.move(-1);
10511
+ return;
10512
+ case 'Enter': {
10513
+ event.preventDefault();
10514
+ const current = this.flat().find((i) => this.optionId(i) === this.activeId());
10515
+ if (current)
10516
+ this.choose(current);
10517
+ return;
10518
+ }
10519
+ case 'Tab':
10520
+ // Keep focus inside the modal (aria-modal must not leak focus to the page behind the backdrop).
10521
+ this.trapTab(event);
10522
+ return;
10523
+ }
10524
+ }
10525
+ onGlobalKeydown(event) {
10526
+ if (this.hotkey() && (event.metaKey || event.ctrlKey) && event.key.toLowerCase() === 'k') {
10527
+ event.preventDefault();
10528
+ this.toggle();
10529
+ return;
10530
+ }
10531
+ // Fallback: close on Escape even if focus somehow left the panel.
10532
+ if (this.isOpen() && event.key === 'Escape') {
10533
+ event.preventDefault();
10534
+ this.close();
10535
+ }
10536
+ }
10537
+ trapTab(event) {
10538
+ const root = this.overlay()?.nativeElement;
10539
+ if (!root)
10540
+ return;
10541
+ const sel = 'a[href],button:not([disabled]),input:not([disabled]),textarea:not([disabled]),[tabindex]:not([tabindex="-1"])';
10542
+ const focusables = Array.from(root.querySelectorAll(sel)).filter((el) => el.offsetParent !== null || el === this.document.activeElement);
10543
+ if (!focusables.length)
10544
+ return;
10545
+ const first = focusables[0];
10546
+ const last = focusables[focusables.length - 1];
10547
+ const active = this.document.activeElement;
10548
+ if (event.shiftKey && active === first) {
10549
+ event.preventDefault();
10550
+ last.focus();
10551
+ }
10552
+ else if (!event.shiftKey && active === last) {
10553
+ event.preventDefault();
10554
+ first.focus();
10555
+ }
10556
+ }
10557
+ move(delta) {
10558
+ const flat = this.flat();
10559
+ if (!flat.length)
10560
+ return;
10561
+ const currentIndex = flat.findIndex((i) => this.optionId(i) === this.activeId());
10562
+ const next = Math.min(flat.length - 1, Math.max(0, (currentIndex < 0 ? 0 : currentIndex) + delta));
10563
+ const id = this.optionId(flat[next]);
10564
+ this.activeId.set(id);
10565
+ // Positions are static, so scrolling the element into view needs no rendered highlight.
10566
+ this.document.getElementById(id)?.scrollIntoView({ block: 'nearest' });
10567
+ }
10568
+ // --- trigger appearance ---
10569
+ onBrand = computed(() => this.appearance() === 'on-brand', ...(ngDevMode ? [{ debugName: "onBrand" }] : []));
10570
+ triggerClass = computed(() => this.onBrand()
10571
+ ? 'bg-white/10 hover:bg-white/15 focus-visible:ring-white/40'
10572
+ : 'bg-muted hover:bg-brand-50 border border-transparent hover:border-brand-200 focus-visible:ring-brand-500/40', ...(ngDevMode ? [{ debugName: "triggerClass" }] : []));
10573
+ triggerTextClass = computed(() => (this.onBrand() ? 'text-white/80' : 'text-ink-faint'), ...(ngDevMode ? [{ debugName: "triggerTextClass" }] : []));
10574
+ kbdClass = computed(() => this.onBrand() ? 'border-white/25 text-white/70' : 'border-border bg-surface text-ink-faint', ...(ngDevMode ? [{ debugName: "kbdClass" }] : []));
10575
+ hotkeyLabel = computed(() => {
10576
+ const platform = this.document.defaultView?.navigator?.platform ?? '';
10577
+ return /mac|iphone|ipad/i.test(platform) ? '⌘K' : 'Ctrl K';
10578
+ }, ...(ngDevMode ? [{ debugName: "hotkeyLabel" }] : []));
10579
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: SdsSearchCommand, deps: [], target: i0.ɵɵFactoryTarget.Component });
10580
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.25", type: SdsSearchCommand, isStandalone: true, selector: "sds-search-command", inputs: { items: { classPropertyName: "items", publicName: "items", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, showTrigger: { classPropertyName: "showTrigger", publicName: "showTrigger", isSignal: true, isRequired: false, transformFunction: null }, hotkey: { classPropertyName: "hotkey", publicName: "hotkey", isSignal: true, isRequired: false, transformFunction: null }, triggerMinWidth: { classPropertyName: "triggerMinWidth", publicName: "triggerMinWidth", isSignal: true, isRequired: false, transformFunction: null }, appearance: { classPropertyName: "appearance", publicName: "appearance", isSignal: true, isRequired: false, transformFunction: null }, searchLabel: { classPropertyName: "searchLabel", publicName: "searchLabel", isSignal: true, isRequired: false, transformFunction: null }, clearLabel: { classPropertyName: "clearLabel", publicName: "clearLabel", isSignal: true, isRequired: false, transformFunction: null }, emptyLabel: { classPropertyName: "emptyLabel", publicName: "emptyLabel", isSignal: true, isRequired: false, transformFunction: null }, selectHint: { classPropertyName: "selectHint", publicName: "selectHint", isSignal: true, isRequired: false, transformFunction: null }, navigateHint: { classPropertyName: "navigateHint", publicName: "navigateHint", isSignal: true, isRequired: false, transformFunction: null }, closeHint: { classPropertyName: "closeHint", publicName: "closeHint", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { select: "select", openChange: "openChange" }, host: { listeners: { "document:keydown": "onGlobalKeydown($event)" }, classAttribute: "block" }, viewQueries: [{ propertyName: "searchInput", first: true, predicate: ["searchInput"], descendants: true, isSignal: true }, { propertyName: "overlay", first: true, predicate: ["overlay"], descendants: true, isSignal: true }], ngImport: i0, template: `
10581
+ @if (showTrigger()) {
10582
+ <!--
10583
+ Responsive trigger: a 40px icon-only button on phones (< sm), the full pill (placeholder + ⌘K
10584
+ hint, with a minimum width so it does not collapse in a flexible slot) from sm up. The min-width
10585
+ is carried in a CSS var so it can be applied at sm+ only, never squeezing the mobile square.
10586
+ -->
10587
+ <button
10588
+ type="button"
10589
+ class="flex h-10 w-10 items-center justify-center rounded-control transition-colors focus-visible:outline-none focus-visible:ring-2 sm:w-auto sm:justify-between sm:gap-2 sm:px-3 sm:min-w-[var(--sds-search-minw)]"
10590
+ [class]="triggerClass()"
10591
+ [style.--sds-search-minw]="triggerMinWidth() + 'px'"
10592
+ [attr.aria-label]="searchLabel()"
10593
+ (click)="open()"
10594
+ >
10595
+ <span class="flex min-w-0 items-center gap-2" [class]="triggerTextClass()">
10596
+ <sds-icon name="search" [size]="16" class="shrink-0" />
10597
+ <span class="hidden truncate text-sm sm:inline">{{ placeholder() }}</span>
10598
+ </span>
10599
+ <kbd class="hidden shrink-0 rounded border px-1.5 py-0.5 text-[11px] font-semibold sm:inline-block" [class]="kbdClass()">{{ hotkeyLabel() }}</kbd>
10600
+ </button>
10601
+ }
10602
+
10603
+ @if (isOpen()) {
10604
+ <div #overlay class="fixed inset-0 z-[100] flex items-start justify-center p-4 pt-[12vh]">
10605
+ <!-- Backdrop -->
10606
+ <div class="absolute inset-0 bg-black/40 backdrop-blur-sm" (click)="close()" aria-hidden="true"></div>
10607
+
10608
+ <!-- Panel -->
10609
+ <div
10610
+ role="dialog"
10611
+ aria-modal="true"
10612
+ [attr.aria-label]="searchLabel()"
10613
+ class="relative flex w-full max-w-xl flex-col overflow-hidden rounded-card border border-border bg-surface shadow-2xl animate-sds-cmd"
10614
+ (keydown)="onPanelKeydown($event)"
10615
+ >
10616
+ <!-- Search field -->
10617
+ <div class="flex items-center gap-3 border-b border-border px-4 py-3.5">
10618
+ <sds-icon name="search" [size]="20" class="shrink-0 text-ink-faint" />
10619
+ <input
10620
+ #searchInput
10621
+ type="text"
10622
+ role="combobox"
10623
+ aria-autocomplete="list"
10624
+ aria-controls="sds-cmd-list"
10625
+ [attr.aria-expanded]="true"
10626
+ [attr.aria-activedescendant]="activeId()"
10627
+ [attr.aria-label]="searchLabel()"
10628
+ [value]="query()"
10629
+ (input)="onInput($event)"
10630
+ [placeholder]="placeholder()"
10631
+ class="min-w-0 flex-1 bg-transparent text-base text-ink outline-none placeholder:text-ink-faint"
10632
+ />
10633
+ @if (query()) {
10634
+ <button
10635
+ type="button"
10636
+ class="rounded p-1 text-ink-faint transition-colors hover:bg-muted hover:text-ink focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-500/40"
10637
+ [attr.aria-label]="clearLabel()"
10638
+ (click)="clear()"
10639
+ >
10640
+ <sds-icon name="x" [size]="18" />
10641
+ </button>
10642
+ }
10643
+ <kbd class="hidden shrink-0 rounded bg-muted px-2 py-1 text-[11px] font-semibold text-ink-faint sm:inline-block">ESC</kbd>
10644
+ </div>
10645
+
10646
+ <!-- Results -->
10647
+ <div id="sds-cmd-list" #list role="listbox" [attr.aria-label]="searchLabel()" class="max-h-[min(60vh,420px)] overflow-y-auto p-2">
10648
+ @if (sections().length) {
10649
+ @for (section of sections(); track section.label) {
10650
+ @if (section.label) {
10651
+ <p class="px-3 pb-1 pt-2 text-[11px] font-semibold uppercase tracking-wider text-ink-faint">{{ section.label }}</p>
10652
+ }
10653
+ @for (item of section.items; track item.id) {
10654
+ <button
10655
+ type="button"
10656
+ role="option"
10657
+ [id]="optionId(item)"
10658
+ [attr.aria-selected]="activeId() === optionId(item)"
10659
+ [disabled]="item.disabled"
10660
+ class="flex w-full items-center gap-3 rounded-control px-3 py-2.5 text-left text-sm font-medium transition-colors focus-visible:outline-none disabled:opacity-50"
10661
+ [class]="activeId() === optionId(item) ? 'bg-brand-500/15 text-ink' : 'text-ink-soft hover:bg-muted hover:text-ink'"
10662
+ (click)="choose(item)"
10663
+ (mousemove)="setActiveById(optionId(item))"
10664
+ >
10665
+ @if (item.icon) {
10666
+ <sds-icon [name]="item.icon" [size]="18" class="shrink-0" />
10667
+ }
10668
+ <span class="flex-1 truncate">{{ item.label }}</span>
10669
+ </button>
10670
+ }
10671
+ }
10672
+ } @else {
10673
+ <div class="px-6 py-10 text-center">
10674
+ <sds-icon name="search-x" [size]="40" class="mx-auto mb-3 text-ink-faint" />
10675
+ <p class="text-sm text-ink-soft">{{ emptyLabel() }}</p>
10676
+ </div>
10677
+ }
10678
+ </div>
10679
+
10680
+ <!-- Footer hints -->
10681
+ <div class="flex items-center justify-center gap-4 border-t border-border bg-muted px-3 py-2.5 text-xs text-ink-faint">
10682
+ <span class="flex items-center gap-1.5"><kbd class="rounded bg-surface px-1.5 py-0.5">↵</kbd> {{ selectHint() }}</span>
10683
+ <span class="flex items-center gap-1.5"><kbd class="rounded bg-surface px-1.5 py-0.5">↑↓</kbd> {{ navigateHint() }}</span>
10684
+ <span class="flex items-center gap-1.5"><kbd class="rounded bg-surface px-1.5 py-0.5">ESC</kbd> {{ closeHint() }}</span>
10685
+ </div>
10686
+ </div>
10687
+ </div>
10688
+ }
10689
+ `, isInline: true, styles: [".animate-sds-cmd{animation:sds-cmd-in .16s ease-out}@keyframes sds-cmd-in{0%{opacity:0;transform:translateY(-10px) scale(.98)}to{opacity:1;transform:translateY(0) scale(1)}}@media(prefers-reduced-motion:reduce){.animate-sds-cmd{animation:none}}\n"], dependencies: [{ kind: "component", type: SdsIcon, selector: "sds-icon", inputs: ["name", "size", "strokeWidth"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
10690
+ }
10691
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: SdsSearchCommand, decorators: [{
10692
+ type: Component,
10693
+ args: [{ selector: 'sds-search-command', changeDetection: ChangeDetectionStrategy.OnPush, imports: [SdsIcon], host: {
10694
+ class: 'block',
10695
+ '(document:keydown)': 'onGlobalKeydown($event)',
10696
+ }, template: `
10697
+ @if (showTrigger()) {
10698
+ <!--
10699
+ Responsive trigger: a 40px icon-only button on phones (< sm), the full pill (placeholder + ⌘K
10700
+ hint, with a minimum width so it does not collapse in a flexible slot) from sm up. The min-width
10701
+ is carried in a CSS var so it can be applied at sm+ only, never squeezing the mobile square.
10702
+ -->
10703
+ <button
10704
+ type="button"
10705
+ class="flex h-10 w-10 items-center justify-center rounded-control transition-colors focus-visible:outline-none focus-visible:ring-2 sm:w-auto sm:justify-between sm:gap-2 sm:px-3 sm:min-w-[var(--sds-search-minw)]"
10706
+ [class]="triggerClass()"
10707
+ [style.--sds-search-minw]="triggerMinWidth() + 'px'"
10708
+ [attr.aria-label]="searchLabel()"
10709
+ (click)="open()"
10710
+ >
10711
+ <span class="flex min-w-0 items-center gap-2" [class]="triggerTextClass()">
10712
+ <sds-icon name="search" [size]="16" class="shrink-0" />
10713
+ <span class="hidden truncate text-sm sm:inline">{{ placeholder() }}</span>
10714
+ </span>
10715
+ <kbd class="hidden shrink-0 rounded border px-1.5 py-0.5 text-[11px] font-semibold sm:inline-block" [class]="kbdClass()">{{ hotkeyLabel() }}</kbd>
10716
+ </button>
10717
+ }
10718
+
10719
+ @if (isOpen()) {
10720
+ <div #overlay class="fixed inset-0 z-[100] flex items-start justify-center p-4 pt-[12vh]">
10721
+ <!-- Backdrop -->
10722
+ <div class="absolute inset-0 bg-black/40 backdrop-blur-sm" (click)="close()" aria-hidden="true"></div>
10723
+
10724
+ <!-- Panel -->
10725
+ <div
10726
+ role="dialog"
10727
+ aria-modal="true"
10728
+ [attr.aria-label]="searchLabel()"
10729
+ class="relative flex w-full max-w-xl flex-col overflow-hidden rounded-card border border-border bg-surface shadow-2xl animate-sds-cmd"
10730
+ (keydown)="onPanelKeydown($event)"
10731
+ >
10732
+ <!-- Search field -->
10733
+ <div class="flex items-center gap-3 border-b border-border px-4 py-3.5">
10734
+ <sds-icon name="search" [size]="20" class="shrink-0 text-ink-faint" />
10735
+ <input
10736
+ #searchInput
10737
+ type="text"
10738
+ role="combobox"
10739
+ aria-autocomplete="list"
10740
+ aria-controls="sds-cmd-list"
10741
+ [attr.aria-expanded]="true"
10742
+ [attr.aria-activedescendant]="activeId()"
10743
+ [attr.aria-label]="searchLabel()"
10744
+ [value]="query()"
10745
+ (input)="onInput($event)"
10746
+ [placeholder]="placeholder()"
10747
+ class="min-w-0 flex-1 bg-transparent text-base text-ink outline-none placeholder:text-ink-faint"
10748
+ />
10749
+ @if (query()) {
10750
+ <button
10751
+ type="button"
10752
+ class="rounded p-1 text-ink-faint transition-colors hover:bg-muted hover:text-ink focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-500/40"
10753
+ [attr.aria-label]="clearLabel()"
10754
+ (click)="clear()"
10755
+ >
10756
+ <sds-icon name="x" [size]="18" />
10757
+ </button>
10758
+ }
10759
+ <kbd class="hidden shrink-0 rounded bg-muted px-2 py-1 text-[11px] font-semibold text-ink-faint sm:inline-block">ESC</kbd>
10760
+ </div>
10761
+
10762
+ <!-- Results -->
10763
+ <div id="sds-cmd-list" #list role="listbox" [attr.aria-label]="searchLabel()" class="max-h-[min(60vh,420px)] overflow-y-auto p-2">
10764
+ @if (sections().length) {
10765
+ @for (section of sections(); track section.label) {
10766
+ @if (section.label) {
10767
+ <p class="px-3 pb-1 pt-2 text-[11px] font-semibold uppercase tracking-wider text-ink-faint">{{ section.label }}</p>
10768
+ }
10769
+ @for (item of section.items; track item.id) {
10770
+ <button
10771
+ type="button"
10772
+ role="option"
10773
+ [id]="optionId(item)"
10774
+ [attr.aria-selected]="activeId() === optionId(item)"
10775
+ [disabled]="item.disabled"
10776
+ class="flex w-full items-center gap-3 rounded-control px-3 py-2.5 text-left text-sm font-medium transition-colors focus-visible:outline-none disabled:opacity-50"
10777
+ [class]="activeId() === optionId(item) ? 'bg-brand-500/15 text-ink' : 'text-ink-soft hover:bg-muted hover:text-ink'"
10778
+ (click)="choose(item)"
10779
+ (mousemove)="setActiveById(optionId(item))"
10780
+ >
10781
+ @if (item.icon) {
10782
+ <sds-icon [name]="item.icon" [size]="18" class="shrink-0" />
10783
+ }
10784
+ <span class="flex-1 truncate">{{ item.label }}</span>
10785
+ </button>
10786
+ }
10787
+ }
10788
+ } @else {
10789
+ <div class="px-6 py-10 text-center">
10790
+ <sds-icon name="search-x" [size]="40" class="mx-auto mb-3 text-ink-faint" />
10791
+ <p class="text-sm text-ink-soft">{{ emptyLabel() }}</p>
10792
+ </div>
10793
+ }
10794
+ </div>
10795
+
10796
+ <!-- Footer hints -->
10797
+ <div class="flex items-center justify-center gap-4 border-t border-border bg-muted px-3 py-2.5 text-xs text-ink-faint">
10798
+ <span class="flex items-center gap-1.5"><kbd class="rounded bg-surface px-1.5 py-0.5">↵</kbd> {{ selectHint() }}</span>
10799
+ <span class="flex items-center gap-1.5"><kbd class="rounded bg-surface px-1.5 py-0.5">↑↓</kbd> {{ navigateHint() }}</span>
10800
+ <span class="flex items-center gap-1.5"><kbd class="rounded bg-surface px-1.5 py-0.5">ESC</kbd> {{ closeHint() }}</span>
10801
+ </div>
10802
+ </div>
10803
+ </div>
10804
+ }
10805
+ `, styles: [".animate-sds-cmd{animation:sds-cmd-in .16s ease-out}@keyframes sds-cmd-in{0%{opacity:0;transform:translateY(-10px) scale(.98)}to{opacity:1;transform:translateY(0) scale(1)}}@media(prefers-reduced-motion:reduce){.animate-sds-cmd{animation:none}}\n"] }]
10806
+ }], ctorParameters: () => [], propDecorators: { searchInput: [{ type: i0.ViewChild, args: ['searchInput', { isSignal: true }] }], overlay: [{ type: i0.ViewChild, args: ['overlay', { isSignal: true }] }], items: [{ type: i0.Input, args: [{ isSignal: true, alias: "items", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], showTrigger: [{ type: i0.Input, args: [{ isSignal: true, alias: "showTrigger", required: false }] }], hotkey: [{ type: i0.Input, args: [{ isSignal: true, alias: "hotkey", required: false }] }], triggerMinWidth: [{ type: i0.Input, args: [{ isSignal: true, alias: "triggerMinWidth", required: false }] }], appearance: [{ type: i0.Input, args: [{ isSignal: true, alias: "appearance", required: false }] }], searchLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "searchLabel", required: false }] }], clearLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "clearLabel", required: false }] }], emptyLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "emptyLabel", required: false }] }], selectHint: [{ type: i0.Input, args: [{ isSignal: true, alias: "selectHint", required: false }] }], navigateHint: [{ type: i0.Input, args: [{ isSignal: true, alias: "navigateHint", required: false }] }], closeHint: [{ type: i0.Input, args: [{ isSignal: true, alias: "closeHint", required: false }] }], select: [{ type: i0.Output, args: ["select"] }], openChange: [{ type: i0.Output, args: ["openChange"] }] } });
10807
+
9369
10808
  /*
9370
10809
  * Public API Surface of saitech-design-system — the Saitech / App design system.
9371
10810
  */
@@ -9375,5 +10814,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
9375
10814
  * Generated bundle index. Do not edit.
9376
10815
  */
9377
10816
 
9378
- export { SDS_ICONS, SDS_PHONE_COUNTRIES, SdsAccordion, SdsAccordionContent, SdsAccordionHeader, SdsAccordionPanel, SdsAccordionStore, SdsAvatar, SdsBadge, SdsButton, SdsCalendar, SdsCard, SdsCheckbox, SdsDatePicker, SdsIcon, SdsInput, SdsInputNumber, SdsLocalizedInput, SdsMultiSelect, SdsNavGroup, SdsNavItem, SdsOtpInput, SdsPaginator, SdsPhoneInput, SdsSelect, SdsShell, SdsSidebar, SdsTab, SdsTabList, SdsTabPanel, SdsTabPanels, SdsTable, SdsTableCell, SdsTableRowExpansion, SdsTabs, SdsTabsStore, SdsTextarea, SdsToastHost, SdsToastService, SdsToggleSwitch, SdsTopbar, SidebarStore, addDays, addMonths, addMonthsKeepDay, formatDdMmYyyy, formatHhMm, isOutOfRange, isSameDay, isoDay, pad2, sdsFormatNational, sdsMatchDialCode, sdsNormalizeSignificant, startOfDay, startOfMonth };
10817
+ export { SDS_ICONS, SDS_PHONE_COUNTRIES, SdsAccordion, SdsAccordionContent, SdsAccordionHeader, SdsAccordionPanel, SdsAccordionStore, SdsAvatar, SdsBadge, SdsButton, SdsCalendar, SdsCard, SdsCheckbox, SdsDatePicker, SdsIcon, SdsInput, SdsInputNumber, SdsLocalizedInput, SdsMenuItem, SdsMultiSelect, SdsNavGroup, SdsNavItem, SdsOtpInput, SdsPaginator, SdsPhoneInput, SdsSearchCommand, SdsSelect, SdsSelectEmpty, SdsSelectFooter, SdsSelectOption, SdsShell, SdsSidebar, SdsTab, SdsTabList, SdsTabPanel, SdsTabPanels, SdsTable, SdsTableCell, SdsTableRowExpansion, SdsTabs, SdsTabsStore, SdsTextarea, SdsThemeService, SdsThemeToggle, SdsToastHost, SdsToastService, SdsToggleSwitch, SdsTopbar, SdsUserMenu, SidebarStore, addDays, addMonths, addMonthsKeepDay, formatDdMmYyyy, formatHhMm, isOutOfRange, isSameDay, isoDay, pad2, sdsFormatNational, sdsMatchDialCode, sdsNormalizeSignificant, startOfDay, startOfMonth };
9379
10818
  //# sourceMappingURL=besaitech-ng-design-system.mjs.map