@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.
- package/fesm2022/besaitech-ng-design-system.mjs +1586 -147
- package/fesm2022/besaitech-ng-design-system.mjs.map +1 -1
- package/index.d.ts +518 -34
- package/package.json +1 -1
- package/themes/base.css +77 -0
- package/themes/belvigo.css +19 -0
- package/themes/saitech.css +24 -0
|
@@ -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,
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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
|
-
|
|
4530
|
-
class="flex-1 truncate pl-3.5"
|
|
4531
|
-
|
|
4532
|
-
|
|
4533
|
-
|
|
4534
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4650
|
-
class="flex-1 truncate pl-3.5"
|
|
4651
|
-
|
|
4652
|
-
|
|
4653
|
-
|
|
4654
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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-
|
|
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-
|
|
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
|
-
|
|
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
|
-
|
|
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-
|
|
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
|
-
|
|
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
|
-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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
|
-
*
|
|
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.
|
|
8608
|
-
*
|
|
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="
|
|
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
|
-
/**
|
|
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
|
-
|
|
8653
|
-
|
|
8654
|
-
|
|
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
|
-
|
|
8723
|
-
|
|
8724
|
-
|
|
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
|
-
|
|
8822
|
-
|
|
8823
|
-
|
|
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: "
|
|
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
|
|
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
|
|
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
|
|
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 (
|
|
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
|
-
|
|
8944
|
-
|
|
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
|
|
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
|
|
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: {
|
|
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 (
|
|
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
|
-
|
|
9010
|
-
|
|
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
|
|
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
|
|
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
|
-
/**
|
|
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
|
-
|
|
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="
|
|
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="
|
|
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="
|
|
9821
|
+
<div [class]="dividerClass()"></div>
|
|
9176
9822
|
} @else {
|
|
9177
|
-
<p class="
|
|
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="
|
|
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="
|
|
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="
|
|
9880
|
+
<div [class]="dividerClass()"></div>
|
|
9239
9881
|
} @else {
|
|
9240
|
-
<p class="
|
|
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
|
-
|
|
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
|
-
?
|
|
9282
|
-
|
|
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
|