@besaitech/ng-design-system 0.0.44 → 0.0.46
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 +1706 -143
- package/fesm2022/besaitech-ng-design-system.mjs.map +1 -1
- package/index.d.ts +468 -31
- 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, TemplateRef, Directive, contentChild, Injectable, contentChildren } from '@angular/core';
|
|
2
|
+
import { inject, input, computed, ChangeDetectionStrategy, Component, booleanAttribute, isDevMode, effect, output, signal, forwardRef, viewChild, numberAttribute, viewChildren, afterRenderEffect, ElementRef, model, Injector, afterNextRender, TemplateRef, Directive, contentChild, Injectable, contentChildren, untracked, DestroyRef } from '@angular/core';
|
|
3
3
|
import { DomSanitizer } from '@angular/platform-browser';
|
|
4
4
|
import * as i1 from '@angular/forms';
|
|
5
5
|
import { NG_VALUE_ACCESSOR, NG_VALIDATORS, FormsModule } from '@angular/forms';
|
|
6
|
-
import { NgTemplateOutlet } from '@angular/common';
|
|
6
|
+
import { NgTemplateOutlet, DOCUMENT } from '@angular/common';
|
|
7
7
|
import { toSignal } from '@angular/core/rxjs-interop';
|
|
8
8
|
import { Router, NavigationEnd, RouterLink, RouterLinkActive } from '@angular/router';
|
|
9
9
|
import { filter, map } from 'rxjs';
|
|
@@ -97,6 +97,12 @@ const SDS_ICONS = {
|
|
|
97
97
|
'loader-circle': '<path d="M21 12a9 9 0 1 1-6.219-8.56"/>',
|
|
98
98
|
bug: '<path d="M12 20v-9"/><path d="M14 7a4 4 0 0 1 4 4v3a6 6 0 0 1-12 0v-3a4 4 0 0 1 4-4z"/><path d="M14.12 3.88 16 2"/><path d="M21 21a4 4 0 0 0-3.81-4"/><path d="M21 5a4 4 0 0 1-3.55 3.97"/><path d="M22 13h-4"/><path d="M3 21a4 4 0 0 1 3.81-4"/><path d="M3 5a4 4 0 0 0 3.55 3.97"/><path d="M6 13H2"/><path d="m8 2 1.88 1.88"/><path d="M9 7.13V6a3 3 0 1 1 6 0v1.13"/>',
|
|
99
99
|
'scroll-text': '<path d="M15 12h-5"/><path d="M15 8h-5"/><path d="M19 17V5a2 2 0 0 0-2-2H4"/><path d="M8 21h12a2 2 0 0 0 2-2v-1a1 1 0 0 0-1-1H11a1 1 0 0 0-1 1v1a2 2 0 1 1-4 0V5a2 2 0 1 0-4 0v2a1 1 0 0 0 1 1h3"/>',
|
|
100
|
+
// Added for the dark/light switcher (sds-theme-toggle) and the command palette (sds-search-command).
|
|
101
|
+
moon: '<path d="M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z"/>',
|
|
102
|
+
sun: '<circle cx="12" cy="12" r="4"/><path d="M12 2v2"/><path d="M12 20v2"/><path d="m4.93 4.93 1.41 1.41"/><path d="m17.66 17.66 1.41 1.41"/><path d="M2 12h2"/><path d="M20 12h2"/><path d="m6.34 17.66-1.41 1.41"/><path d="m19.07 4.93-1.41 1.41"/>',
|
|
103
|
+
'search-x': '<path d="m13.5 8.5-5 5"/><path d="m8.5 8.5 5 5"/><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/>',
|
|
104
|
+
command: '<path d="M15 6v12a3 3 0 1 0 3-3H6a3 3 0 1 0 3 3V6a3 3 0 1 0-3 3h12a3 3 0 1 0-3-3"/>',
|
|
105
|
+
sparkles: '<path d="M9.937 15.5A2 2 0 0 0 8.5 14.063l-6.135-1.582a.5.5 0 0 1 0-.962L8.5 9.936A2 2 0 0 0 9.937 8.5l1.582-6.135a.5.5 0 0 1 .963 0L14.063 8.5A2 2 0 0 0 15.5 9.937l6.135 1.581a.5.5 0 0 1 0 .964L15.5 14.063a2 2 0 0 0-1.437 1.437l-1.582 6.135a.5.5 0 0 1-.963 0z"/><path d="M20 3v4"/><path d="M22 5h-4"/><path d="M4 17v2"/><path d="M5 18H3"/>',
|
|
100
106
|
};
|
|
101
107
|
|
|
102
108
|
const SIZE_PX = { sm: 16, md: 20, lg: 24, xl: 32 };
|
|
@@ -155,7 +161,7 @@ const BASE = 'inline-flex items-center justify-center gap-2 font-medium ' +
|
|
|
155
161
|
'focus-visible:ring-brand-500 disabled:opacity-50 disabled:pointer-events-none';
|
|
156
162
|
const VARIANTS$2 = {
|
|
157
163
|
primary: 'bg-brand-600 text-white shadow-sm hover:bg-brand-700 active:bg-brand-800',
|
|
158
|
-
secondary: 'bg-
|
|
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',
|
|
@@ -272,7 +278,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
|
|
|
272
278
|
`, styles: [".sds-spinner{width:1em;height:1em;border:2px solid currentColor;border-right-color:transparent;border-radius:9999px;animation:sds-spin .6s linear infinite}@keyframes sds-spin{to{transform:rotate(360deg)}}\n"] }]
|
|
273
279
|
}], ctorParameters: () => [], propDecorators: { variant: [{ type: i0.Input, args: [{ isSignal: true, alias: "variant", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], shape: [{ type: i0.Input, args: [{ isSignal: true, alias: "shape", required: false }] }], type: [{ type: i0.Input, args: [{ isSignal: true, alias: "type", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], loading: [{ type: i0.Input, args: [{ isSignal: true, alias: "loading", required: false }] }], block: [{ type: i0.Input, args: [{ isSignal: true, alias: "block", required: false }] }], iconOnly: [{ type: i0.Input, args: [{ isSignal: true, alias: "iconOnly", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }] } });
|
|
274
280
|
|
|
275
|
-
let nextId$
|
|
281
|
+
let nextId$e = 0;
|
|
276
282
|
const SIZES$a = {
|
|
277
283
|
sm: 'h-9 text-sm',
|
|
278
284
|
md: 'h-10 text-sm',
|
|
@@ -285,7 +291,7 @@ const SIZES$a = {
|
|
|
285
291
|
* sidebar search field).
|
|
286
292
|
*/
|
|
287
293
|
class SdsInput {
|
|
288
|
-
inputId = `sds-input-${nextId$
|
|
294
|
+
inputId = `sds-input-${nextId$e++}`;
|
|
289
295
|
label = input('', ...(ngDevMode ? [{ debugName: "label" }] : []));
|
|
290
296
|
/** Accessible name used when no visible [label] is provided. */
|
|
291
297
|
ariaLabel = input('', ...(ngDevMode ? [{ debugName: "ariaLabel" }] : []));
|
|
@@ -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');
|
|
@@ -606,7 +612,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
|
|
|
606
612
|
`, styles: ["input:-webkit-autofill,input:-webkit-autofill:hover,input:-webkit-autofill:focus,input:-webkit-autofill:active{transition:background-color 100000s ease-in-out 0s;-webkit-text-fill-color:currentColor;caret-color:currentColor}\n"] }]
|
|
607
613
|
}], propDecorators: { label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], hint: [{ type: i0.Input, args: [{ isSignal: true, alias: "hint", required: false }] }], error: [{ type: i0.Input, args: [{ isSignal: true, alias: "error", required: false }] }], type: [{ type: i0.Input, args: [{ isSignal: true, alias: "type", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], appearance: [{ type: i0.Input, args: [{ isSignal: true, alias: "appearance", required: false }] }], shape: [{ type: i0.Input, args: [{ isSignal: true, alias: "shape", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], clearable: [{ type: i0.Input, args: [{ isSignal: true, alias: "clearable", required: false }] }], clearLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "clearLabel", required: false }] }], leadingIcon: [{ type: i0.Input, args: [{ isSignal: true, alias: "leadingIcon", required: false }] }], trailingIcon: [{ type: i0.Input, args: [{ isSignal: true, alias: "trailingIcon", required: false }] }], trailingAction: [{ type: i0.Input, args: [{ isSignal: true, alias: "trailingAction", required: false }] }], trailingActionLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "trailingActionLabel", required: false }] }], trailingActionPressed: [{ type: i0.Input, args: [{ isSignal: true, alias: "trailingActionPressed", required: false }] }], trailingActionClick: [{ type: i0.Output, args: ["trailingActionClick"] }] } });
|
|
608
614
|
|
|
609
|
-
let nextId$
|
|
615
|
+
let nextId$d = 0;
|
|
610
616
|
const HEIGHTS = {
|
|
611
617
|
sm: 'h-9',
|
|
612
618
|
md: 'h-10',
|
|
@@ -629,7 +635,7 @@ const TEXT = {
|
|
|
629
635
|
* {@link min}/{@link max} on blur and on every spinner step; ArrowUp / ArrowDown also step it.
|
|
630
636
|
*/
|
|
631
637
|
class SdsInputNumber {
|
|
632
|
-
inputId = `sds-input-number-${nextId$
|
|
638
|
+
inputId = `sds-input-number-${nextId$d++}`;
|
|
633
639
|
fieldRef = viewChild('field', ...(ngDevMode ? [{ debugName: "fieldRef" }] : []));
|
|
634
640
|
label = input('', ...(ngDevMode ? [{ debugName: "label" }] : []));
|
|
635
641
|
/** Accessible name used when no visible [label] is provided. */
|
|
@@ -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');
|
|
@@ -1336,7 +1342,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
|
|
|
1336
1342
|
}]
|
|
1337
1343
|
}], ctorParameters: () => [], propDecorators: { fieldRef: [{ type: i0.ViewChild, args: ['field', { isSignal: true }] }], 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 }] }], appearance: [{ type: i0.Input, args: [{ isSignal: true, alias: "appearance", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], readonly: [{ type: i0.Input, args: [{ isSignal: true, alias: "readonly", required: false }] }], mode: [{ type: i0.Input, args: [{ isSignal: true, alias: "mode", required: false }] }], locale: [{ type: i0.Input, args: [{ isSignal: true, alias: "locale", required: false }] }], currency: [{ type: i0.Input, args: [{ isSignal: true, alias: "currency", required: false }] }], currencyDisplay: [{ type: i0.Input, args: [{ isSignal: true, alias: "currencyDisplay", required: false }] }], useGrouping: [{ type: i0.Input, args: [{ isSignal: true, alias: "useGrouping", required: false }] }], minFractionDigits: [{ type: i0.Input, args: [{ isSignal: true, alias: "minFractionDigits", required: false }] }], maxFractionDigits: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxFractionDigits", required: false }] }], prefix: [{ type: i0.Input, args: [{ isSignal: true, alias: "prefix", required: false }] }], suffix: [{ type: i0.Input, args: [{ isSignal: true, alias: "suffix", required: false }] }], min: [{ type: i0.Input, args: [{ isSignal: true, alias: "min", required: false }] }], max: [{ type: i0.Input, args: [{ isSignal: true, alias: "max", required: false }] }], step: [{ type: i0.Input, args: [{ isSignal: true, alias: "step", required: false }] }], showButtons: [{ type: i0.Input, args: [{ isSignal: true, alias: "showButtons", required: false }] }], buttonLayout: [{ type: i0.Input, args: [{ isSignal: true, alias: "buttonLayout", required: false }] }], incrementIcon: [{ type: i0.Input, args: [{ isSignal: true, alias: "incrementIcon", required: false }] }], decrementIcon: [{ type: i0.Input, args: [{ isSignal: true, alias: "decrementIcon", required: false }] }], allowEmpty: [{ type: i0.Input, args: [{ isSignal: true, alias: "allowEmpty", required: false }] }], clearable: [{ type: i0.Input, args: [{ isSignal: true, alias: "clearable", required: false }] }], clearLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "clearLabel", required: false }] }], incrementLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "incrementLabel", required: false }] }], decrementLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "decrementLabel", required: false }] }] } });
|
|
1338
1344
|
|
|
1339
|
-
let nextId$
|
|
1345
|
+
let nextId$c = 0;
|
|
1340
1346
|
const SIZES$9 = {
|
|
1341
1347
|
sm: 'min-h-9 text-sm',
|
|
1342
1348
|
md: 'min-h-10 text-sm',
|
|
@@ -1356,7 +1362,7 @@ const SIZES$9 = {
|
|
|
1356
1362
|
* `<sds-localized-input [languages]="['fr','nl','en']" [(ngModel)]="title" />`.
|
|
1357
1363
|
*/
|
|
1358
1364
|
class SdsLocalizedInput {
|
|
1359
|
-
inputId = `sds-localized-input-${nextId$
|
|
1365
|
+
inputId = `sds-localized-input-${nextId$c++}`;
|
|
1360
1366
|
label = input('', ...(ngDevMode ? [{ debugName: "label" }] : []));
|
|
1361
1367
|
hint = input('', ...(ngDevMode ? [{ debugName: "hint" }] : []));
|
|
1362
1368
|
placeholder = input('', ...(ngDevMode ? [{ debugName: "placeholder" }] : []));
|
|
@@ -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()) {
|
|
@@ -1612,7 +1618,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
|
|
|
1612
1618
|
}]
|
|
1613
1619
|
}], propDecorators: { label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], hint: [{ type: i0.Input, args: [{ isSignal: true, alias: "hint", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], languages: [{ type: i0.Input, args: [{ isSignal: true, alias: "languages", required: false }] }], currentLanguage: [{ type: i0.Input, args: [{ isSignal: true, alias: "currentLanguage", required: false }] }], multiline: [{ type: i0.Input, args: [{ isSignal: true, alias: "multiline", required: false }] }], rows: [{ type: i0.Input, args: [{ isSignal: true, alias: "rows", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], languageSwitcherLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "languageSwitcherLabel", required: false }] }], untranslatedLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "untranslatedLabel", required: false }] }] } });
|
|
1614
1620
|
|
|
1615
|
-
let nextId$
|
|
1621
|
+
let nextId$b = 0;
|
|
1616
1622
|
const SIZES$8 = {
|
|
1617
1623
|
md: 'h-12 w-11 text-lg',
|
|
1618
1624
|
lg: 'h-14 w-12 text-xl',
|
|
@@ -1629,7 +1635,7 @@ const SIZES$8 = {
|
|
|
1629
1635
|
* first box and have it land correctly.
|
|
1630
1636
|
*/
|
|
1631
1637
|
class SdsOtpInput {
|
|
1632
|
-
groupId = `sds-otp-${nextId$
|
|
1638
|
+
groupId = `sds-otp-${nextId$b++}`;
|
|
1633
1639
|
/** How many boxes / characters the code has. */
|
|
1634
1640
|
length = input(6, ...(ngDevMode ? [{ debugName: "length", transform: numberAttribute }] : [{ transform: numberAttribute }]));
|
|
1635
1641
|
label = input('', ...(ngDevMode ? [{ debugName: "label" }] : []));
|
|
@@ -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)"
|
|
@@ -1929,7 +1935,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
|
|
|
1929
1935
|
}]
|
|
1930
1936
|
}], ctorParameters: () => [], propDecorators: { length: [{ type: i0.Input, args: [{ isSignal: true, alias: "length", 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 }] }], inputMode: [{ type: i0.Input, args: [{ isSignal: true, alias: "inputMode", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], autoFocus: [{ type: i0.Input, args: [{ isSignal: true, alias: "autoFocus", required: false }] }], cellLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "cellLabel", required: false }] }], completed: [{ type: i0.Output, args: ["completed"] }], cells: [{ type: i0.ViewChildren, args: ['cell', { isSignal: true }] }] } });
|
|
1931
1937
|
|
|
1932
|
-
let nextId$
|
|
1938
|
+
let nextId$a = 0;
|
|
1933
1939
|
const SIZES$7 = {
|
|
1934
1940
|
sm: 'text-sm px-3 py-2',
|
|
1935
1941
|
md: 'text-sm px-3.5 py-2.5',
|
|
@@ -1941,7 +1947,7 @@ const SIZES$7 = {
|
|
|
1941
1947
|
* [(ngModel)].
|
|
1942
1948
|
*/
|
|
1943
1949
|
class SdsTextarea {
|
|
1944
|
-
fieldId = `sds-textarea-${nextId$
|
|
1950
|
+
fieldId = `sds-textarea-${nextId$a++}`;
|
|
1945
1951
|
field = viewChild('field', ...(ngDevMode ? [{ debugName: "field" }] : []));
|
|
1946
1952
|
label = input('', ...(ngDevMode ? [{ debugName: "label" }] : []));
|
|
1947
1953
|
/** Accessible name used when no visible [label] is provided. */
|
|
@@ -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())
|
|
@@ -2179,7 +2185,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
|
|
|
2179
2185
|
}]
|
|
2180
2186
|
}], ctorParameters: () => [], propDecorators: { field: [{ type: i0.ViewChild, args: ['field', { isSignal: true }] }], 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 }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], readonly: [{ type: i0.Input, args: [{ isSignal: true, alias: "readonly", required: false }] }], required: [{ type: i0.Input, args: [{ isSignal: true, alias: "required", required: false }] }], rows: [{ type: i0.Input, args: [{ isSignal: true, alias: "rows", required: false }] }], autoResize: [{ type: i0.Input, args: [{ isSignal: true, alias: "autoResize", required: false }] }], maxLength: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxLength", required: false }] }], showCount: [{ type: i0.Input, args: [{ isSignal: true, alias: "showCount", required: false }] }], resize: [{ type: i0.Input, args: [{ isSignal: true, alias: "resize", required: false }] }] } });
|
|
2181
2187
|
|
|
2182
|
-
let nextId$
|
|
2188
|
+
let nextId$9 = 0;
|
|
2183
2189
|
const SIZES$6 = {
|
|
2184
2190
|
sm: { track: 'h-5 w-9', knob: 'h-4 w-4', on: 'translate-x-4', icon: 11 },
|
|
2185
2191
|
md: { track: 'h-6 w-11', knob: 'h-5 w-5', on: 'translate-x-5', icon: 13 },
|
|
@@ -2192,7 +2198,7 @@ const SIZES$6 = {
|
|
|
2192
2198
|
* states.
|
|
2193
2199
|
*/
|
|
2194
2200
|
class SdsToggleSwitch {
|
|
2195
|
-
fieldId = `sds-toggle-switch-${nextId$
|
|
2201
|
+
fieldId = `sds-toggle-switch-${nextId$9++}`;
|
|
2196
2202
|
label = input('', ...(ngDevMode ? [{ debugName: "label" }] : []));
|
|
2197
2203
|
/** Accessible name used when no visible [label] is provided. */
|
|
2198
2204
|
ariaLabel = input('', ...(ngDevMode ? [{ debugName: "ariaLabel" }] : []));
|
|
@@ -2429,7 +2435,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
|
|
|
2429
2435
|
}]
|
|
2430
2436
|
}], ctorParameters: () => [], propDecorators: { label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], required: [{ type: i0.Input, args: [{ isSignal: true, alias: "required", required: false }] }], trueValue: [{ type: i0.Input, args: [{ isSignal: true, alias: "trueValue", required: false }] }], falseValue: [{ type: i0.Input, args: [{ isSignal: true, alias: "falseValue", required: false }] }], onIcon: [{ type: i0.Input, args: [{ isSignal: true, alias: "onIcon", required: false }] }], offIcon: [{ type: i0.Input, args: [{ isSignal: true, alias: "offIcon", required: false }] }], hint: [{ type: i0.Input, args: [{ isSignal: true, alias: "hint", required: false }] }], error: [{ type: i0.Input, args: [{ isSignal: true, alias: "error", required: false }] }] } });
|
|
2431
2437
|
|
|
2432
|
-
let nextId$
|
|
2438
|
+
let nextId$8 = 0;
|
|
2433
2439
|
/**
|
|
2434
2440
|
* Boolean checkbox. Implements ControlValueAccessor — use with reactive forms
|
|
2435
2441
|
* or `[(ngModel)]`. Renders a styled box (brand-filled with a check when on)
|
|
@@ -2439,7 +2445,7 @@ let nextId$7 = 0;
|
|
|
2439
2445
|
* <sds-checkbox label="Fusion automatique" [(ngModel)]="autoMerge" />
|
|
2440
2446
|
*/
|
|
2441
2447
|
class SdsCheckbox {
|
|
2442
|
-
id = `sds-checkbox-${nextId$
|
|
2448
|
+
id = `sds-checkbox-${nextId$8++}`;
|
|
2443
2449
|
label = input('', ...(ngDevMode ? [{ debugName: "label" }] : []));
|
|
2444
2450
|
/** Accessible name used when no visible [label] is provided. */
|
|
2445
2451
|
ariaLabel = input('', ...(ngDevMode ? [{ debugName: "ariaLabel" }] : []));
|
|
@@ -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"
|
|
@@ -3613,7 +3619,7 @@ const SIZES$5 = {
|
|
|
3613
3619
|
class SdsDatePicker {
|
|
3614
3620
|
host = inject(ElementRef);
|
|
3615
3621
|
injector = inject(Injector);
|
|
3616
|
-
fieldId = `sds-date-${nextId$
|
|
3622
|
+
fieldId = `sds-date-${nextId$7++}`;
|
|
3617
3623
|
label = input('', ...(ngDevMode ? [{ debugName: "label" }] : []));
|
|
3618
3624
|
ariaLabel = input('', ...(ngDevMode ? [{ debugName: "ariaLabel" }] : []));
|
|
3619
3625
|
placeholder = input('jj/mm/aaaa', ...(ngDevMode ? [{ debugName: "placeholder" }] : []));
|
|
@@ -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
|
>
|
|
@@ -4128,7 +4134,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
|
|
|
4128
4134
|
`,
|
|
4129
4135
|
}]
|
|
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
|
-
let nextId$
|
|
4137
|
+
let nextId$7 = 0;
|
|
4132
4138
|
|
|
4133
4139
|
/**
|
|
4134
4140
|
* Supplies a custom template for each option row (and the selected value shown
|
|
@@ -4212,7 +4218,7 @@ const SIZES$4 = {
|
|
|
4212
4218
|
md: 'h-10 text-sm',
|
|
4213
4219
|
lg: 'h-12 text-base',
|
|
4214
4220
|
};
|
|
4215
|
-
let nextId$
|
|
4221
|
+
let nextId$6 = 0;
|
|
4216
4222
|
/**
|
|
4217
4223
|
* Single-select dropdown. Options may be primitives or objects (configure via
|
|
4218
4224
|
* [optionLabel]/[optionValue]/[optionDisabled]). The overlay is appended to
|
|
@@ -4246,7 +4252,7 @@ let nextId$5 = 0;
|
|
|
4246
4252
|
class SdsSelect {
|
|
4247
4253
|
host = inject(ElementRef);
|
|
4248
4254
|
injector = inject(Injector);
|
|
4249
|
-
selectId = `sds-select-${nextId$
|
|
4255
|
+
selectId = `sds-select-${nextId$6++}`;
|
|
4250
4256
|
listId = `${this.selectId}-list`;
|
|
4251
4257
|
options = input([], ...(ngDevMode ? [{ debugName: "options" }] : []));
|
|
4252
4258
|
optionLabel = input('label', ...(ngDevMode ? [{ debugName: "optionLabel" }] : []));
|
|
@@ -4310,7 +4316,7 @@ class SdsSelect {
|
|
|
4310
4316
|
: 'border-white/15 focus:border-white/40 focus:ring-2 focus:ring-white/20');
|
|
4311
4317
|
}
|
|
4312
4318
|
else {
|
|
4313
|
-
parts.push('bg-
|
|
4319
|
+
parts.push('bg-surface text-ink');
|
|
4314
4320
|
parts.push(this.error()
|
|
4315
4321
|
? 'border-danger focus:border-danger focus:ring-2 focus:ring-danger/25'
|
|
4316
4322
|
: 'border-border focus:border-brand-500 focus:ring-2 focus:ring-brand-500/25');
|
|
@@ -4929,7 +4935,7 @@ const SIZES$3 = {
|
|
|
4929
4935
|
md: 'min-h-10 text-sm',
|
|
4930
4936
|
lg: 'min-h-12 text-base',
|
|
4931
4937
|
};
|
|
4932
|
-
let nextId$
|
|
4938
|
+
let nextId$5 = 0;
|
|
4933
4939
|
/**
|
|
4934
4940
|
* Multi-select dropdown (value is an array). Options may be primitives or
|
|
4935
4941
|
* objects (configure via [optionLabel]/[optionValue]/[optionDisabled]). The
|
|
@@ -4962,7 +4968,7 @@ let nextId$4 = 0;
|
|
|
4962
4968
|
class SdsMultiSelect {
|
|
4963
4969
|
host = inject(ElementRef);
|
|
4964
4970
|
injector = inject(Injector);
|
|
4965
|
-
selectId = `sds-multiselect-${nextId$
|
|
4971
|
+
selectId = `sds-multiselect-${nextId$5++}`;
|
|
4966
4972
|
listId = `${this.selectId}-list`;
|
|
4967
4973
|
options = input([], ...(ngDevMode ? [{ debugName: "options" }] : []));
|
|
4968
4974
|
optionLabel = input('label', ...(ngDevMode ? [{ debugName: "optionLabel" }] : []));
|
|
@@ -5103,7 +5109,7 @@ class SdsMultiSelect {
|
|
|
5103
5109
|
checkboxClass(checked, indeterminate) {
|
|
5104
5110
|
return checked || indeterminate
|
|
5105
5111
|
? 'border-brand-600 bg-brand-600 text-white'
|
|
5106
|
-
: 'border-border bg-
|
|
5112
|
+
: 'border-border bg-surface';
|
|
5107
5113
|
}
|
|
5108
5114
|
// --- open / close ---
|
|
5109
5115
|
toggle() {
|
|
@@ -5404,7 +5410,7 @@ class SdsMultiSelect {
|
|
|
5404
5410
|
[attr.aria-haspopup]="'listbox'"
|
|
5405
5411
|
[attr.aria-activedescendant]="open() && !filter() ? activeDescId() : null"
|
|
5406
5412
|
[attr.aria-label]="!label() && ariaLabel() ? ariaLabel() : null"
|
|
5407
|
-
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"
|
|
5408
5414
|
[class]="wrapperClasses()"
|
|
5409
5415
|
(click)="toggle()"
|
|
5410
5416
|
(keydown)="onTriggerKeydown($event)"
|
|
@@ -5595,7 +5601,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
|
|
|
5595
5601
|
[attr.aria-haspopup]="'listbox'"
|
|
5596
5602
|
[attr.aria-activedescendant]="open() && !filter() ? activeDescId() : null"
|
|
5597
5603
|
[attr.aria-label]="!label() && ariaLabel() ? ariaLabel() : null"
|
|
5598
|
-
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"
|
|
5599
5605
|
[class]="wrapperClasses()"
|
|
5600
5606
|
(click)="toggle()"
|
|
5601
5607
|
(keydown)="onTriggerKeydown($event)"
|
|
@@ -5824,7 +5830,7 @@ const SIZES$2 = {
|
|
|
5824
5830
|
md: 'h-10 text-sm',
|
|
5825
5831
|
lg: 'h-12 text-base',
|
|
5826
5832
|
};
|
|
5827
|
-
let nextId$
|
|
5833
|
+
let nextId$4 = 0;
|
|
5828
5834
|
/**
|
|
5829
5835
|
* International phone number input: a country selector (ISO chip + dial code)
|
|
5830
5836
|
* with a searchable, body-appended dropdown, plus a formatted national-number
|
|
@@ -5839,7 +5845,7 @@ let nextId$3 = 0;
|
|
|
5839
5845
|
class SdsPhoneInput {
|
|
5840
5846
|
host = inject(ElementRef);
|
|
5841
5847
|
injector = inject(Injector);
|
|
5842
|
-
fieldId = `sds-phone-${nextId$
|
|
5848
|
+
fieldId = `sds-phone-${nextId$4++}`;
|
|
5843
5849
|
listId = `${this.fieldId}-list`;
|
|
5844
5850
|
label = input('', ...(ngDevMode ? [{ debugName: "label" }] : []));
|
|
5845
5851
|
hint = input('', ...(ngDevMode ? [{ debugName: "hint" }] : []));
|
|
@@ -6273,7 +6279,7 @@ class SdsPhoneInput {
|
|
|
6273
6279
|
|
|
6274
6280
|
<div
|
|
6275
6281
|
#anchor
|
|
6276
|
-
class="relative flex items-stretch rounded-control border bg-
|
|
6282
|
+
class="relative flex items-stretch rounded-control border bg-surface transition-colors"
|
|
6277
6283
|
[class]="wrapperClasses()"
|
|
6278
6284
|
>
|
|
6279
6285
|
<!-- country selector -->
|
|
@@ -6414,7 +6420,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
|
|
|
6414
6420
|
|
|
6415
6421
|
<div
|
|
6416
6422
|
#anchor
|
|
6417
|
-
class="relative flex items-stretch rounded-control border bg-
|
|
6423
|
+
class="relative flex items-stretch rounded-control border bg-surface transition-colors"
|
|
6418
6424
|
[class]="wrapperClasses()"
|
|
6419
6425
|
>
|
|
6420
6426
|
<!-- country selector -->
|
|
@@ -6803,7 +6809,7 @@ class SdsPaginator {
|
|
|
6803
6809
|
[disabled]="disabled()"
|
|
6804
6810
|
[attr.aria-label]="jumpToPageAriaLabel()"
|
|
6805
6811
|
(change)="onJump($event)"
|
|
6806
|
-
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"
|
|
6807
6813
|
/>
|
|
6808
6814
|
</label>
|
|
6809
6815
|
}
|
|
@@ -6912,7 +6918,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
|
|
|
6912
6918
|
[disabled]="disabled()"
|
|
6913
6919
|
[attr.aria-label]="jumpToPageAriaLabel()"
|
|
6914
6920
|
(change)="onJump($event)"
|
|
6915
|
-
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"
|
|
6916
6922
|
/>
|
|
6917
6923
|
</label>
|
|
6918
6924
|
}
|
|
@@ -6921,7 +6927,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
|
|
|
6921
6927
|
}]
|
|
6922
6928
|
}], propDecorators: { totalRecords: [{ type: i0.Input, args: [{ isSignal: true, alias: "totalRecords", required: false }] }], first: [{ type: i0.Input, args: [{ isSignal: true, alias: "first", required: false }] }, { type: i0.Output, args: ["firstChange"] }], rows: [{ type: i0.Input, args: [{ isSignal: true, alias: "rows", required: false }] }, { type: i0.Output, args: ["rowsChange"] }], rowsPerPageOptions: [{ type: i0.Input, args: [{ isSignal: true, alias: "rowsPerPageOptions", required: false }] }], pageLinkSize: [{ type: i0.Input, args: [{ isSignal: true, alias: "pageLinkSize", required: false }] }], showFirstLastIcon: [{ type: i0.Input, args: [{ isSignal: true, alias: "showFirstLastIcon", required: false }] }], showPageLinks: [{ type: i0.Input, args: [{ isSignal: true, alias: "showPageLinks", required: false }] }], showCurrentPageReport: [{ type: i0.Input, args: [{ isSignal: true, alias: "showCurrentPageReport", required: false }] }], currentPageReportTemplate: [{ type: i0.Input, args: [{ isSignal: true, alias: "currentPageReportTemplate", required: false }] }], showJumpToPageInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "showJumpToPageInput", required: false }] }], rowsPerPageLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "rowsPerPageLabel", required: false }] }], jumpToPageLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "jumpToPageLabel", required: false }] }], paginationLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "paginationLabel", required: false }] }], firstPageLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "firstPageLabel", required: false }] }], previousPageLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "previousPageLabel", required: false }] }], nextPageLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "nextPageLabel", required: false }] }], lastPageLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "lastPageLabel", required: false }] }], pageLinkLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "pageLinkLabel", required: false }] }], jumpToPageAriaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "jumpToPageAriaLabel", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], pageChange: [{ type: i0.Output, args: ["pageChange"] }] } });
|
|
6923
6929
|
|
|
6924
|
-
let nextId$
|
|
6930
|
+
let nextId$3 = 0;
|
|
6925
6931
|
/**
|
|
6926
6932
|
* Coordinates a single <sds-tabs> instance: holds the active value (shared from
|
|
6927
6933
|
* the host's two-way model), tracks registered tabs for default selection and
|
|
@@ -6931,7 +6937,7 @@ let nextId$2 = 0;
|
|
|
6931
6937
|
*/
|
|
6932
6938
|
class SdsTabsStore {
|
|
6933
6939
|
/** Unique base id used to link tabs and panels via ARIA. */
|
|
6934
|
-
id = `sds-tabs-${nextId$
|
|
6940
|
+
id = `sds-tabs-${nextId$3++}`;
|
|
6935
6941
|
/** Whether the tab list scrolls horizontally (mirrors [scrollable]). */
|
|
6936
6942
|
scrollable = signal(false, ...(ngDevMode ? [{ debugName: "scrollable" }] : []));
|
|
6937
6943
|
value;
|
|
@@ -7213,7 +7219,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
|
|
|
7213
7219
|
}]
|
|
7214
7220
|
}], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: true }] }] } });
|
|
7215
7221
|
|
|
7216
|
-
let nextId$
|
|
7222
|
+
let nextId$2 = 0;
|
|
7217
7223
|
/**
|
|
7218
7224
|
* Coordinates a single <sds-accordion>: holds the open value(s) (shared from the
|
|
7219
7225
|
* host's two-way model), the multiple-open flag, and generates the ARIA ids that
|
|
@@ -7223,7 +7229,7 @@ let nextId$1 = 0;
|
|
|
7223
7229
|
*/
|
|
7224
7230
|
class SdsAccordionStore {
|
|
7225
7231
|
/** Unique base id used to link headers and content regions via ARIA. */
|
|
7226
|
-
id = `sds-accordion-${nextId$
|
|
7232
|
+
id = `sds-accordion-${nextId$2++}`;
|
|
7227
7233
|
/** Whether more than one panel may be open at a time (mirrors [multiple]). */
|
|
7228
7234
|
multiple = signal(false, ...(ngDevMode ? [{ debugName: "multiple" }] : []));
|
|
7229
7235
|
value;
|
|
@@ -7561,7 +7567,7 @@ const ALIGN = {
|
|
|
7561
7567
|
center: 'text-center',
|
|
7562
7568
|
right: 'text-right',
|
|
7563
7569
|
};
|
|
7564
|
-
let nextId = 0;
|
|
7570
|
+
let nextId$1 = 0;
|
|
7565
7571
|
/**
|
|
7566
7572
|
* Client-side data table mirroring PrimeNG's table, scoped to a strong local
|
|
7567
7573
|
* feature set: sorting, pagination (reuses {@link SdsPaginator}), global filter,
|
|
@@ -7590,7 +7596,7 @@ let nextId = 0;
|
|
|
7590
7596
|
* </sds-table>
|
|
7591
7597
|
*/
|
|
7592
7598
|
class SdsTable {
|
|
7593
|
-
tableId = `sds-table-${nextId++}`;
|
|
7599
|
+
tableId = `sds-table-${nextId$1++}`;
|
|
7594
7600
|
// --- inputs ---------------------------------------------------------------
|
|
7595
7601
|
value = input([], ...(ngDevMode ? [{ debugName: "value" }] : []));
|
|
7596
7602
|
columns = input([], ...(ngDevMode ? [{ debugName: "columns" }] : []));
|
|
@@ -8088,11 +8094,11 @@ class SdsTable {
|
|
|
8088
8094
|
checkboxClass(checked, indeterminate) {
|
|
8089
8095
|
return checked || indeterminate
|
|
8090
8096
|
? 'border-brand-600 bg-brand-600 text-white'
|
|
8091
|
-
: 'border-border bg-
|
|
8097
|
+
: 'border-border bg-surface';
|
|
8092
8098
|
}
|
|
8093
8099
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: SdsTable, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
8094
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: `
|
|
8095
|
-
<div class="rounded-card border border-border bg-
|
|
8101
|
+
<div class="rounded-card border border-border bg-surface overflow-hidden">
|
|
8096
8102
|
<!-- Toolbar: caption + global filter -->
|
|
8097
8103
|
@if (caption() || globalFilter()) {
|
|
8098
8104
|
<div class="flex flex-wrap items-center justify-between gap-3 border-b border-border px-4 py-3">
|
|
@@ -8104,7 +8110,7 @@ class SdsTable {
|
|
|
8104
8110
|
|
|
8105
8111
|
@if (globalFilter()) {
|
|
8106
8112
|
<div
|
|
8107
|
-
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"
|
|
8108
8114
|
>
|
|
8109
8115
|
<sds-icon
|
|
8110
8116
|
name="search"
|
|
@@ -8395,7 +8401,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
|
|
|
8395
8401
|
imports: [SdsIcon, SdsPaginator, NgTemplateOutlet],
|
|
8396
8402
|
host: { class: 'block' },
|
|
8397
8403
|
template: `
|
|
8398
|
-
<div class="rounded-card border border-border bg-
|
|
8404
|
+
<div class="rounded-card border border-border bg-surface overflow-hidden">
|
|
8399
8405
|
<!-- Toolbar: caption + global filter -->
|
|
8400
8406
|
@if (caption() || globalFilter()) {
|
|
8401
8407
|
<div class="flex flex-wrap items-center justify-between gap-3 border-b border-border px-4 py-3">
|
|
@@ -8407,7 +8413,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
|
|
|
8407
8413
|
|
|
8408
8414
|
@if (globalFilter()) {
|
|
8409
8415
|
<div
|
|
8410
|
-
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"
|
|
8411
8417
|
>
|
|
8412
8418
|
<sds-icon
|
|
8413
8419
|
name="search"
|
|
@@ -8843,6 +8849,153 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
|
|
|
8843
8849
|
}]
|
|
8844
8850
|
}], propDecorators: { closeLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "closeLabel", required: false }] }] } });
|
|
8845
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
|
+
|
|
8846
8999
|
/**
|
|
8847
9000
|
* Shared collapse state for the sidebar and its nav items.
|
|
8848
9001
|
*
|
|
@@ -8853,6 +9006,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
|
|
|
8853
9006
|
class SidebarStore {
|
|
8854
9007
|
/** Desktop rail state: collapsed to icon-only width (applies at ≥ lg). */
|
|
8855
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" }] : []));
|
|
8856
9015
|
/** Mobile drawer state: whether the off-canvas sidebar is slid in (< lg). */
|
|
8857
9016
|
mobileOpen = signal(false, ...(ngDevMode ? [{ debugName: "mobileOpen" }] : []));
|
|
8858
9017
|
/** Toggle the desktop icon-rail. */
|
|
@@ -8885,10 +9044,15 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
|
|
|
8885
9044
|
/**
|
|
8886
9045
|
* Application top bar: a logo slot, the app name, and three slots the app fills.
|
|
8887
9046
|
*
|
|
8888
|
-
*
|
|
9047
|
+
* Three appearances. `brand` is the gradient-and-wave bar an internal tool wants. `surface` is white on
|
|
8889
9048
|
* a hairline border, for a bar that has to sit under a customer-facing page without dominating it —
|
|
8890
|
-
* a storefront header is a bar, but it is not a banner.
|
|
8891
|
-
*
|
|
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.
|
|
8892
9056
|
*
|
|
8893
9057
|
* It deliberately owns NO account UI. It used to render the signed-in user and a logout button, which
|
|
8894
9058
|
* forced every consumer into one shape — no dropdown, no email, no menu, no busy state, and an
|
|
@@ -8900,7 +9064,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
|
|
|
8900
9064
|
* `[sdsCenter]` (the flexible middle — a search field), `[sdsActions]` (the right edge).
|
|
8901
9065
|
*
|
|
8902
9066
|
* @example
|
|
8903
|
-
* <sds-topbar appName="
|
|
9067
|
+
* <sds-topbar appName="Design system">
|
|
8904
9068
|
* <img sdsLogo src="logo.svg" alt="" />
|
|
8905
9069
|
* <my-address-picker sdsLead />
|
|
8906
9070
|
* <my-search sdsCenter />
|
|
@@ -8930,16 +9094,29 @@ class SdsTopbar {
|
|
|
8930
9094
|
* aria-label is its only name, and a hardcoded one is simply wrong in every other locale.
|
|
8931
9095
|
*/
|
|
8932
9096
|
menuLabel = input('Ouvrir le menu', ...(ngDevMode ? [{ debugName: "menuLabel" }] : []));
|
|
8933
|
-
/**
|
|
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
|
+
*/
|
|
8934
9101
|
appearance = input('brand', ...(ngDevMode ? [{ debugName: "appearance" }] : []));
|
|
8935
|
-
|
|
8936
|
-
|
|
8937
|
-
|
|
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" }] : []));
|
|
8938
9115
|
menuButtonClasses = computed(() => this.appearance() === 'brand'
|
|
8939
9116
|
? 'text-white/85 hover:bg-white/10 hover:text-white focus-visible:ring-white/40'
|
|
8940
9117
|
: 'text-ink-soft hover:bg-muted hover:text-ink focus-visible:ring-brand-500/40', ...(ngDevMode ? [{ debugName: "menuButtonClasses" }] : []));
|
|
8941
9118
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: SdsTopbar, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
8942
|
-
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: `
|
|
8943
9120
|
<header class="relative z-30" [class]="shellClasses()" style="height: var(--sds-topbar-h)">
|
|
8944
9121
|
<!--
|
|
8945
9122
|
Decorative wave. The clip lives HERE, on the wave's own wrapper, and not on the header.
|
|
@@ -8977,6 +9154,18 @@ class SdsTopbar {
|
|
|
8977
9154
|
</span>
|
|
8978
9155
|
}
|
|
8979
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
|
+
|
|
8980
9169
|
<!--
|
|
8981
9170
|
The band is always full-bleed — the gradient and the wave span the viewport — while the
|
|
8982
9171
|
CONTENTS are constrained by [contentWidth], so a bar over a centred page lines up with it
|
|
@@ -9002,18 +9191,13 @@ class SdsTopbar {
|
|
|
9002
9191
|
}
|
|
9003
9192
|
<ng-content select="[sdsLogo]" />
|
|
9004
9193
|
<!--
|
|
9005
|
-
|
|
9006
|
-
|
|
9007
|
-
|
|
9008
|
-
carrying a logo, a wordmark and a two-syllable name; it is wrong at every width.
|
|
9009
|
-
|
|
9010
|
-
The name never truncates and never hides: it is the answer to "which app am I in", which on a
|
|
9011
|
-
platform of five same-branded apps is the one thing the bar must not drop. An app whose left
|
|
9012
|
-
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.
|
|
9013
9197
|
-->
|
|
9014
9198
|
@if (appName()) {
|
|
9015
|
-
<span class="h-7 w-px shrink-0" [class]="appearance() === 'brand' ? 'bg-white/30' : 'bg-border'"></span>
|
|
9016
|
-
<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>
|
|
9017
9201
|
}
|
|
9018
9202
|
<ng-content select="[sdsLead]" />
|
|
9019
9203
|
</div>
|
|
@@ -9076,6 +9260,18 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
|
|
|
9076
9260
|
</span>
|
|
9077
9261
|
}
|
|
9078
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
|
+
|
|
9079
9275
|
<!--
|
|
9080
9276
|
The band is always full-bleed — the gradient and the wave span the viewport — while the
|
|
9081
9277
|
CONTENTS are constrained by [contentWidth], so a bar over a centred page lines up with it
|
|
@@ -9101,18 +9297,13 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
|
|
|
9101
9297
|
}
|
|
9102
9298
|
<ng-content select="[sdsLogo]" />
|
|
9103
9299
|
<!--
|
|
9104
|
-
|
|
9105
|
-
|
|
9106
|
-
|
|
9107
|
-
carrying a logo, a wordmark and a two-syllable name; it is wrong at every width.
|
|
9108
|
-
|
|
9109
|
-
The name never truncates and never hides: it is the answer to "which app am I in", which on a
|
|
9110
|
-
platform of five same-branded apps is the one thing the bar must not drop. An app whose left
|
|
9111
|
-
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.
|
|
9112
9303
|
-->
|
|
9113
9304
|
@if (appName()) {
|
|
9114
|
-
<span class="h-7 w-px shrink-0" [class]="appearance() === 'brand' ? 'bg-white/30' : 'bg-border'"></span>
|
|
9115
|
-
<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>
|
|
9116
9307
|
}
|
|
9117
9308
|
<ng-content select="[sdsLead]" />
|
|
9118
9309
|
</div>
|
|
@@ -9130,28 +9321,47 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
|
|
|
9130
9321
|
</header>
|
|
9131
9322
|
`,
|
|
9132
9323
|
}]
|
|
9133
|
-
}], 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 }] }] } });
|
|
9134
9325
|
|
|
9135
9326
|
/**
|
|
9136
9327
|
* App layout shell: a full-height column with the top bar across the top and a
|
|
9137
9328
|
* row below holding the sidebar and the scrollable main content.
|
|
9138
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
|
+
*
|
|
9139
9335
|
* @example
|
|
9140
|
-
* <sds-shell>
|
|
9141
|
-
* <sds-topbar sdsTopbar … />
|
|
9142
|
-
* <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>
|
|
9143
9339
|
* <!-- routed page content -->
|
|
9144
9340
|
* <router-outlet />
|
|
9145
9341
|
* </sds-shell>
|
|
9146
9342
|
*/
|
|
9147
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" }] : []));
|
|
9148
9351
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: SdsShell, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
9149
|
-
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: `
|
|
9150
9353
|
<div class="flex h-screen flex-col overflow-hidden">
|
|
9151
9354
|
<ng-content select="[sdsTopbar]" />
|
|
9152
9355
|
<div class="flex min-h-0 flex-1">
|
|
9153
9356
|
<ng-content select="[sdsSidebar]" />
|
|
9154
|
-
<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
|
+
}
|
|
9155
9365
|
<ng-content />
|
|
9156
9366
|
</main>
|
|
9157
9367
|
</div>
|
|
@@ -9169,22 +9379,39 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
|
|
|
9169
9379
|
<ng-content select="[sdsTopbar]" />
|
|
9170
9380
|
<div class="flex min-h-0 flex-1">
|
|
9171
9381
|
<ng-content select="[sdsSidebar]" />
|
|
9172
|
-
<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
|
+
}
|
|
9173
9390
|
<ng-content />
|
|
9174
9391
|
</main>
|
|
9175
9392
|
</div>
|
|
9176
9393
|
</div>
|
|
9177
9394
|
`,
|
|
9178
9395
|
}]
|
|
9179
|
-
}] });
|
|
9396
|
+
}], propDecorators: { background: [{ type: i0.Input, args: [{ isSignal: true, alias: "background", required: false }] }], decorations: [{ type: i0.Input, args: [{ isSignal: true, alias: "decorations", required: false }] }] } });
|
|
9180
9397
|
|
|
9181
9398
|
/**
|
|
9182
9399
|
* Collapsible navigation rail. Project <sds-nav-group>/<sds-nav-item> as the
|
|
9183
9400
|
* default content, and pin a footer (e.g. search + version badge) via the
|
|
9184
9401
|
* [sdsFooter] slot. Collapse state is shared through SidebarStore.
|
|
9185
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
|
+
*
|
|
9186
9413
|
* @example
|
|
9187
|
-
* <sds-sidebar>
|
|
9414
|
+
* <sds-sidebar appearance="surface" decorations>
|
|
9188
9415
|
* <sds-nav-group label="Membres">
|
|
9189
9416
|
* <sds-nav-item icon="user-plus" label="Créer un membre" link="/membres/new" />
|
|
9190
9417
|
* </sds-nav-group>
|
|
@@ -9193,22 +9420,101 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
|
|
|
9193
9420
|
*/
|
|
9194
9421
|
class SdsSidebar {
|
|
9195
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
|
+
}
|
|
9196
9455
|
/**
|
|
9197
9456
|
* Accessible names for the rail's three icon-only controls. French defaults keep existing consumers
|
|
9198
|
-
* unchanged; a multilingual app must pass translated values
|
|
9199
|
-
* is its only name.
|
|
9457
|
+
* unchanged; a multilingual app must pass translated values.
|
|
9200
9458
|
*/
|
|
9201
9459
|
collapseLabel = input('Replier le menu', ...(ngDevMode ? [{ debugName: "collapseLabel" }] : []));
|
|
9202
9460
|
expandLabel = input('Déplier le menu', ...(ngDevMode ? [{ debugName: "expandLabel" }] : []));
|
|
9203
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 }]));
|
|
9204
9466
|
collapsed = this.store.collapsed;
|
|
9205
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" }] : []));
|
|
9206
9482
|
/** Desktop rail width (full vs. collapsed); mobile always uses full width. */
|
|
9207
9483
|
railWidth = computed(() => this.collapsed()
|
|
9208
9484
|
? 'var(--sds-sidebar-w-collapsed)'
|
|
9209
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
|
+
}
|
|
9210
9516
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: SdsSidebar, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
9211
|
-
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: `
|
|
9212
9518
|
<!-- Mobile backdrop (only when the drawer is open, < lg) -->
|
|
9213
9519
|
@if (mobileOpen()) {
|
|
9214
9520
|
<div
|
|
@@ -9219,23 +9525,37 @@ class SdsSidebar {
|
|
|
9219
9525
|
}
|
|
9220
9526
|
|
|
9221
9527
|
<!--
|
|
9222
|
-
Mobile (< lg): fixed off-canvas drawer that slides in from the left
|
|
9223
|
-
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.
|
|
9224
9531
|
-->
|
|
9225
9532
|
<aside
|
|
9226
|
-
|
|
9227
|
-
|
|
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()"
|
|
9228
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)"
|
|
9229
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
|
+
|
|
9230
9549
|
<!-- Header: collapse toggle (desktop) / close (mobile) -->
|
|
9231
9550
|
<div
|
|
9232
|
-
class="flex items-center px-3"
|
|
9551
|
+
class="relative flex items-center px-3"
|
|
9233
9552
|
style="height: 52px"
|
|
9234
9553
|
[class]="collapsed() ? 'justify-end lg:justify-center' : 'justify-end'"
|
|
9235
9554
|
>
|
|
9236
9555
|
<button
|
|
9237
9556
|
type="button"
|
|
9238
|
-
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()"
|
|
9239
9559
|
[attr.aria-label]="collapsed() ? expandLabel() : collapseLabel()"
|
|
9240
9560
|
[attr.aria-expanded]="!collapsed()"
|
|
9241
9561
|
(click)="store.toggle()"
|
|
@@ -9246,8 +9566,10 @@ class SdsSidebar {
|
|
|
9246
9566
|
/>
|
|
9247
9567
|
</button>
|
|
9248
9568
|
<button
|
|
9569
|
+
#closeBtn
|
|
9249
9570
|
type="button"
|
|
9250
|
-
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()"
|
|
9251
9573
|
[attr.aria-label]="closeLabel()"
|
|
9252
9574
|
(click)="store.closeMobile()"
|
|
9253
9575
|
>
|
|
@@ -9256,12 +9578,12 @@ class SdsSidebar {
|
|
|
9256
9578
|
</div>
|
|
9257
9579
|
|
|
9258
9580
|
<!-- Navigation -->
|
|
9259
|
-
<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">
|
|
9260
9582
|
<ng-content />
|
|
9261
9583
|
</nav>
|
|
9262
9584
|
|
|
9263
9585
|
<!-- Footer (search, version, …) -->
|
|
9264
|
-
<div class="px-3 pb-4 pt-2">
|
|
9586
|
+
<div class="relative px-3 pb-4 pt-2">
|
|
9265
9587
|
<ng-content select="[sdsFooter]" />
|
|
9266
9588
|
</div>
|
|
9267
9589
|
</aside>
|
|
@@ -9273,7 +9595,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
|
|
|
9273
9595
|
selector: 'sds-sidebar',
|
|
9274
9596
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
9275
9597
|
imports: [SdsIcon],
|
|
9276
|
-
host: {
|
|
9598
|
+
host: {
|
|
9599
|
+
class: 'block h-full',
|
|
9600
|
+
'(document:keydown.escape)': 'onEscape()',
|
|
9601
|
+
},
|
|
9277
9602
|
template: `
|
|
9278
9603
|
<!-- Mobile backdrop (only when the drawer is open, < lg) -->
|
|
9279
9604
|
@if (mobileOpen()) {
|
|
@@ -9285,23 +9610,37 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
|
|
|
9285
9610
|
}
|
|
9286
9611
|
|
|
9287
9612
|
<!--
|
|
9288
|
-
Mobile (< lg): fixed off-canvas drawer that slides in from the left
|
|
9289
|
-
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.
|
|
9290
9616
|
-->
|
|
9291
9617
|
<aside
|
|
9292
|
-
|
|
9293
|
-
|
|
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()"
|
|
9294
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)"
|
|
9295
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
|
+
|
|
9296
9634
|
<!-- Header: collapse toggle (desktop) / close (mobile) -->
|
|
9297
9635
|
<div
|
|
9298
|
-
class="flex items-center px-3"
|
|
9636
|
+
class="relative flex items-center px-3"
|
|
9299
9637
|
style="height: 52px"
|
|
9300
9638
|
[class]="collapsed() ? 'justify-end lg:justify-center' : 'justify-end'"
|
|
9301
9639
|
>
|
|
9302
9640
|
<button
|
|
9303
9641
|
type="button"
|
|
9304
|
-
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()"
|
|
9305
9644
|
[attr.aria-label]="collapsed() ? expandLabel() : collapseLabel()"
|
|
9306
9645
|
[attr.aria-expanded]="!collapsed()"
|
|
9307
9646
|
(click)="store.toggle()"
|
|
@@ -9312,8 +9651,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
|
|
|
9312
9651
|
/>
|
|
9313
9652
|
</button>
|
|
9314
9653
|
<button
|
|
9654
|
+
#closeBtn
|
|
9315
9655
|
type="button"
|
|
9316
|
-
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()"
|
|
9317
9658
|
[attr.aria-label]="closeLabel()"
|
|
9318
9659
|
(click)="store.closeMobile()"
|
|
9319
9660
|
>
|
|
@@ -9322,18 +9663,18 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
|
|
|
9322
9663
|
</div>
|
|
9323
9664
|
|
|
9324
9665
|
<!-- Navigation -->
|
|
9325
|
-
<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">
|
|
9326
9667
|
<ng-content />
|
|
9327
9668
|
</nav>
|
|
9328
9669
|
|
|
9329
9670
|
<!-- Footer (search, version, …) -->
|
|
9330
|
-
<div class="px-3 pb-4 pt-2">
|
|
9671
|
+
<div class="relative px-3 pb-4 pt-2">
|
|
9331
9672
|
<ng-content select="[sdsFooter]" />
|
|
9332
9673
|
</div>
|
|
9333
9674
|
</aside>
|
|
9334
9675
|
`,
|
|
9335
9676
|
}]
|
|
9336
|
-
}], 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 }] }] } });
|
|
9337
9678
|
|
|
9338
9679
|
/**
|
|
9339
9680
|
* Groups nav items under an optional uppercase section label.
|
|
@@ -9371,13 +9712,39 @@ class SdsNavGroup {
|
|
|
9371
9712
|
/** Explicit user choice; null means "follow the route". */
|
|
9372
9713
|
manual = signal(null, ...(ngDevMode ? [{ debugName: "manual" }] : []));
|
|
9373
9714
|
url = toSignal(this.router.events.pipe(filter((e) => e instanceof NavigationEnd), map((e) => e.urlAfterRedirects)), { initialValue: this.router.url });
|
|
9374
|
-
/**
|
|
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
|
+
*/
|
|
9375
9720
|
routeActive = computed(() => {
|
|
9376
9721
|
const base = this.basePath();
|
|
9377
|
-
|
|
9722
|
+
if (!base)
|
|
9723
|
+
return false;
|
|
9724
|
+
const path = this.url().split(/[?#]/)[0];
|
|
9725
|
+
return path === base || path.startsWith(base + '/');
|
|
9378
9726
|
}, ...(ngDevMode ? [{ debugName: "routeActive" }] : []));
|
|
9379
9727
|
expanded = computed(() => this.manual() ?? this.routeActive(), ...(ngDevMode ? [{ debugName: "expanded" }] : []));
|
|
9380
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" }] : []));
|
|
9381
9748
|
/**
|
|
9382
9749
|
* A plain section always shows its items — they carry their own icons, so they survive the collapsed
|
|
9383
9750
|
* rail. A collapsible group hides its children there instead: its children are sub-destinations and
|
|
@@ -9398,7 +9765,7 @@ class SdsNavGroup {
|
|
|
9398
9765
|
parts.push('hidden');
|
|
9399
9766
|
}
|
|
9400
9767
|
if (this.collapsible() && !this.collapsed()) {
|
|
9401
|
-
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');
|
|
9402
9769
|
}
|
|
9403
9770
|
return parts.join(' ');
|
|
9404
9771
|
}, ...(ngDevMode ? [{ debugName: "childrenClass" }] : []));
|
|
@@ -9430,11 +9797,7 @@ class SdsNavGroup {
|
|
|
9430
9797
|
-->
|
|
9431
9798
|
<button
|
|
9432
9799
|
type="button"
|
|
9433
|
-
class="
|
|
9434
|
-
text-white/70 transition-colors hover:bg-white/8 hover:text-white
|
|
9435
|
-
focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/40"
|
|
9436
|
-
[class.lg:justify-center]="collapsed()"
|
|
9437
|
-
[class.text-white]="routeActive()"
|
|
9800
|
+
[class]="headerClass()"
|
|
9438
9801
|
[attr.aria-expanded]="collapsed() ? null : expanded()"
|
|
9439
9802
|
[attr.title]="collapsed() ? label() : null"
|
|
9440
9803
|
[attr.aria-label]="collapsed() ? label() : null"
|
|
@@ -9449,15 +9812,15 @@ class SdsNavGroup {
|
|
|
9449
9812
|
<sds-icon
|
|
9450
9813
|
name="chevron-down"
|
|
9451
9814
|
[size]="16"
|
|
9452
|
-
class="
|
|
9815
|
+
[class]="chevronClass()"
|
|
9453
9816
|
[class.lg:hidden]="collapsed()"
|
|
9454
9817
|
[class.rotate-180]="expanded()"
|
|
9455
9818
|
/>
|
|
9456
9819
|
</button>
|
|
9457
9820
|
} @else if (collapsed()) {
|
|
9458
|
-
<div class="
|
|
9821
|
+
<div [class]="dividerClass()"></div>
|
|
9459
9822
|
} @else {
|
|
9460
|
-
<p class="
|
|
9823
|
+
<p [class]="captionClass()">
|
|
9461
9824
|
{{ label() }}
|
|
9462
9825
|
</p>
|
|
9463
9826
|
}
|
|
@@ -9493,11 +9856,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
|
|
|
9493
9856
|
-->
|
|
9494
9857
|
<button
|
|
9495
9858
|
type="button"
|
|
9496
|
-
class="
|
|
9497
|
-
text-white/70 transition-colors hover:bg-white/8 hover:text-white
|
|
9498
|
-
focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/40"
|
|
9499
|
-
[class.lg:justify-center]="collapsed()"
|
|
9500
|
-
[class.text-white]="routeActive()"
|
|
9859
|
+
[class]="headerClass()"
|
|
9501
9860
|
[attr.aria-expanded]="collapsed() ? null : expanded()"
|
|
9502
9861
|
[attr.title]="collapsed() ? label() : null"
|
|
9503
9862
|
[attr.aria-label]="collapsed() ? label() : null"
|
|
@@ -9512,15 +9871,15 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
|
|
|
9512
9871
|
<sds-icon
|
|
9513
9872
|
name="chevron-down"
|
|
9514
9873
|
[size]="16"
|
|
9515
|
-
class="
|
|
9874
|
+
[class]="chevronClass()"
|
|
9516
9875
|
[class.lg:hidden]="collapsed()"
|
|
9517
9876
|
[class.rotate-180]="expanded()"
|
|
9518
9877
|
/>
|
|
9519
9878
|
</button>
|
|
9520
9879
|
} @else if (collapsed()) {
|
|
9521
|
-
<div class="
|
|
9880
|
+
<div [class]="dividerClass()"></div>
|
|
9522
9881
|
} @else {
|
|
9523
|
-
<p class="
|
|
9882
|
+
<p [class]="captionClass()">
|
|
9524
9883
|
{{ label() }}
|
|
9525
9884
|
</p>
|
|
9526
9885
|
}
|
|
@@ -9556,15 +9915,31 @@ class SdsNavItem {
|
|
|
9556
9915
|
select = output();
|
|
9557
9916
|
collapsed = this.store.collapsed;
|
|
9558
9917
|
itemClass(isActive) {
|
|
9559
|
-
|
|
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 ' +
|
|
9560
9924
|
'transition-colors duration-150 focus-visible:outline-none focus-visible:ring-2 ' +
|
|
9561
|
-
'focus-visible:ring-white/40 ' +
|
|
9925
|
+
(surface ? 'focus-visible:ring-brand-500/40 ' : 'focus-visible:ring-white/40 ') +
|
|
9562
9926
|
(this.collapsed() ? 'h-10 lg:justify-center' : 'h-10');
|
|
9563
9927
|
const state = isActive
|
|
9564
|
-
?
|
|
9565
|
-
|
|
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';
|
|
9566
9934
|
return `${base} ${state}`;
|
|
9567
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" }] : []));
|
|
9568
9943
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: SdsNavItem, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
9569
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: `
|
|
9570
9945
|
@if (link() !== null) {
|
|
@@ -9579,6 +9954,9 @@ class SdsNavItem {
|
|
|
9579
9954
|
[attr.aria-current]="rla.isActive || active() ? 'page' : null"
|
|
9580
9955
|
(click)="store.closeMobile()"
|
|
9581
9956
|
>
|
|
9957
|
+
@if (rla.isActive || active()) {
|
|
9958
|
+
<span [class]="accentBarClass()" aria-hidden="true"></span>
|
|
9959
|
+
}
|
|
9582
9960
|
<ng-container [ngTemplateOutlet]="content" />
|
|
9583
9961
|
</a>
|
|
9584
9962
|
} @else {
|
|
@@ -9590,6 +9968,9 @@ class SdsNavItem {
|
|
|
9590
9968
|
[attr.aria-current]="active() ? 'page' : null"
|
|
9591
9969
|
(click)="select.emit(); store.closeMobile()"
|
|
9592
9970
|
>
|
|
9971
|
+
@if (active()) {
|
|
9972
|
+
<span [class]="accentBarClass()" aria-hidden="true"></span>
|
|
9973
|
+
}
|
|
9593
9974
|
<ng-container [ngTemplateOutlet]="content" />
|
|
9594
9975
|
</button>
|
|
9595
9976
|
}
|
|
@@ -9623,6 +10004,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
|
|
|
9623
10004
|
[attr.aria-current]="rla.isActive || active() ? 'page' : null"
|
|
9624
10005
|
(click)="store.closeMobile()"
|
|
9625
10006
|
>
|
|
10007
|
+
@if (rla.isActive || active()) {
|
|
10008
|
+
<span [class]="accentBarClass()" aria-hidden="true"></span>
|
|
10009
|
+
}
|
|
9626
10010
|
<ng-container [ngTemplateOutlet]="content" />
|
|
9627
10011
|
</a>
|
|
9628
10012
|
} @else {
|
|
@@ -9634,6 +10018,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
|
|
|
9634
10018
|
[attr.aria-current]="active() ? 'page' : null"
|
|
9635
10019
|
(click)="select.emit(); store.closeMobile()"
|
|
9636
10020
|
>
|
|
10021
|
+
@if (active()) {
|
|
10022
|
+
<span [class]="accentBarClass()" aria-hidden="true"></span>
|
|
10023
|
+
}
|
|
9637
10024
|
<ng-container [ngTemplateOutlet]="content" />
|
|
9638
10025
|
</button>
|
|
9639
10026
|
}
|
|
@@ -9649,6 +10036,1182 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
|
|
|
9649
10036
|
}]
|
|
9650
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"] }] } });
|
|
9651
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
|
+
|
|
10808
|
+
let nextId = 0;
|
|
10809
|
+
/**
|
|
10810
|
+
* The language picker for the top bar: a compact pill (globe icon + the active code) that opens a panel
|
|
10811
|
+
* listing exactly the languages the app supports, each row marked selected for the current one.
|
|
10812
|
+
*
|
|
10813
|
+
* The library owns no i18n engine — every app uses a different one (ngx-translate, Transloco, per-locale
|
|
10814
|
+
* URLs). So this is a pure, controlled control: the app passes the supported [languages] and the [current]
|
|
10815
|
+
* code, and reacts to the choice. `current` is a two-way `model`, so bind `[(current)]="lang"` for optimistic
|
|
10816
|
+
* echo, or just listen to `(currentChange)` and call your own `translate.use($event)`. Rows show each
|
|
10817
|
+
* language's human label (pass it in the option, or let it default to the uppercased code).
|
|
10818
|
+
*
|
|
10819
|
+
* It is a sibling of sds-user-menu / sds-theme-toggle / sds-search-command and shares their chrome: the same
|
|
10820
|
+
* two appearances (`surface` default for a light/glass bar, `on-brand` for the dark brand bar), the same
|
|
10821
|
+
* disclosure mechanics (closes on outside click, on Escape, and on selection, restoring focus to the trigger),
|
|
10822
|
+
* and the same panel styling and open animation — so the three top-bar controls read as one set.
|
|
10823
|
+
*
|
|
10824
|
+
* The panel is appended to `document.body` while open and positioned against the trigger (like sds-select),
|
|
10825
|
+
* so a clipping ancestor — a bar with `overflow:hidden`, or a `glass` bar whose `backdrop-filter` is a
|
|
10826
|
+
* containing block — can never cut it off or paint over it. It repositions on scroll/resize and is removed
|
|
10827
|
+
* on close. Accessibility follows the APG select-only combobox: focus stays on the trigger, and Arrow/Home/
|
|
10828
|
+
* End/Enter/Escape drive a highlighted option surfaced via `aria-activedescendant` (works even though the
|
|
10829
|
+
* listbox lives in `<body>`).
|
|
10830
|
+
*
|
|
10831
|
+
* @example
|
|
10832
|
+
* <sds-language-switcher
|
|
10833
|
+
* [languages]="[{ code: 'fr', label: 'Français' }, { code: 'de', label: 'Deutsch' }, { code: 'en', label: 'English' }]"
|
|
10834
|
+
* [current]="lang()"
|
|
10835
|
+
* menuLabel="Changer de langue"
|
|
10836
|
+
* (currentChange)="translate.use($event)"
|
|
10837
|
+
* />
|
|
10838
|
+
*
|
|
10839
|
+
* @example
|
|
10840
|
+
* <!-- Bare codes are fine; rows fall back to the uppercased code. -->
|
|
10841
|
+
* <sds-language-switcher [languages]="['fr', 'de', 'en']" [(current)]="lang" />
|
|
10842
|
+
*/
|
|
10843
|
+
class SdsLanguageSwitcher {
|
|
10844
|
+
el = inject(ElementRef);
|
|
10845
|
+
injector = inject(Injector);
|
|
10846
|
+
trigger = viewChild('trigger', ...(ngDevMode ? [{ debugName: "trigger" }] : []));
|
|
10847
|
+
panelId = `sds-language-panel-${nextId++}`;
|
|
10848
|
+
/** The supported languages — either `{ code, label }` options or bare code strings. */
|
|
10849
|
+
languages = input([], ...(ngDevMode ? [{ debugName: "languages" }] : []));
|
|
10850
|
+
/** The active language code. Two-way: `[(current)]` echoes the choice, or listen to `(currentChange)`. */
|
|
10851
|
+
current = model('', ...(ngDevMode ? [{ debugName: "current" }] : []));
|
|
10852
|
+
/** `surface` (default) for a light / glass bar; `on-brand` for the dark brand bar. */
|
|
10853
|
+
appearance = input('surface', ...(ngDevMode ? [{ debugName: "appearance" }] : []));
|
|
10854
|
+
/** Which edge the panel aligns to. `end` (default) hangs it from the right of the trigger. */
|
|
10855
|
+
align = input('end', ...(ngDevMode ? [{ debugName: "align" }] : []));
|
|
10856
|
+
/** `up` prefers opening the panel above the trigger — for a switcher near the bottom of the page. */
|
|
10857
|
+
direction = input('down', ...(ngDevMode ? [{ debugName: "direction" }] : []));
|
|
10858
|
+
/** Accessible name for the trigger and panel. */
|
|
10859
|
+
menuLabel = input('Choisir la langue', ...(ngDevMode ? [{ debugName: "menuLabel" }] : []));
|
|
10860
|
+
/** Optional visible header above the list (e.g. "Langue"). Omitted when empty. */
|
|
10861
|
+
heading = input('', ...(ngDevMode ? [{ debugName: "heading" }] : []));
|
|
10862
|
+
openChange = output();
|
|
10863
|
+
open = signal(false, ...(ngDevMode ? [{ debugName: "open" }] : []));
|
|
10864
|
+
highlightedIndex = signal(-1, ...(ngDevMode ? [{ debugName: "highlightedIndex" }] : []));
|
|
10865
|
+
panelEl = null;
|
|
10866
|
+
/** Normalise the mixed `languages` input to `{ code, label }`, defaulting the label to the code. */
|
|
10867
|
+
options = computed(() => this.languages().map((l) => typeof l === 'string'
|
|
10868
|
+
? { code: l, label: l.toUpperCase() }
|
|
10869
|
+
: { code: l.code, label: l.label ?? l.code.toUpperCase() }), ...(ngDevMode ? [{ debugName: "options" }] : []));
|
|
10870
|
+
/**
|
|
10871
|
+
* The code treated as active everywhere — trigger label AND the selected row. Falls back to the first
|
|
10872
|
+
* option when `current` is unset, so the pill and the checked row can never disagree.
|
|
10873
|
+
*/
|
|
10874
|
+
effectiveCode = computed(() => this.current() || this.options()[0]?.code || '', ...(ngDevMode ? [{ debugName: "effectiveCode" }] : []));
|
|
10875
|
+
/** Uppercased code shown in the trigger. */
|
|
10876
|
+
currentCode = computed(() => this.effectiveCode().toUpperCase(), ...(ngDevMode ? [{ debugName: "currentCode" }] : []));
|
|
10877
|
+
activeOptionId = computed(() => this.highlightedIndex() >= 0 ? this.optionId(this.highlightedIndex()) : null, ...(ngDevMode ? [{ debugName: "activeOptionId" }] : []));
|
|
10878
|
+
optionId(i) {
|
|
10879
|
+
return `${this.panelId}-opt-${i}`;
|
|
10880
|
+
}
|
|
10881
|
+
toggle() {
|
|
10882
|
+
this.open() ? this.close(true) : this.openPanel();
|
|
10883
|
+
}
|
|
10884
|
+
openPanel() {
|
|
10885
|
+
this.open.set(true);
|
|
10886
|
+
this.openChange.emit(true);
|
|
10887
|
+
this.initHighlight();
|
|
10888
|
+
// After the panel renders inside the host, portal it to <body> and anchor it to the trigger, so no
|
|
10889
|
+
// clipping/stacking ancestor (an overflow:hidden bar, a backdrop-filter glass bar) can cut it off.
|
|
10890
|
+
// Focus stays on the trigger (combobox model); the active row is surfaced via aria-activedescendant.
|
|
10891
|
+
afterNextRender(() => {
|
|
10892
|
+
this.panelEl = this.el.nativeElement.querySelector('[data-sds-language-panel]');
|
|
10893
|
+
this.positionPanel();
|
|
10894
|
+
this.addReflowListeners();
|
|
10895
|
+
this.trigger()?.nativeElement.focus();
|
|
10896
|
+
this.scrollHighlightedIntoView();
|
|
10897
|
+
}, { injector: this.injector });
|
|
10898
|
+
}
|
|
10899
|
+
close(returnFocus = false) {
|
|
10900
|
+
if (!this.open())
|
|
10901
|
+
return;
|
|
10902
|
+
this.removeReflowListeners();
|
|
10903
|
+
// Remove the portalled node ourselves before Angular tears the view down (it lives in <body> now).
|
|
10904
|
+
this.panelEl?.remove();
|
|
10905
|
+
this.panelEl = null;
|
|
10906
|
+
this.open.set(false);
|
|
10907
|
+
this.openChange.emit(false);
|
|
10908
|
+
if (returnFocus)
|
|
10909
|
+
this.trigger()?.nativeElement.focus();
|
|
10910
|
+
}
|
|
10911
|
+
choose(code) {
|
|
10912
|
+
this.current.set(code);
|
|
10913
|
+
// Pointer/keyboard selection returns focus to the trigger, like the other top-bar disclosures.
|
|
10914
|
+
this.close(true);
|
|
10915
|
+
}
|
|
10916
|
+
onDocumentClick(event) {
|
|
10917
|
+
if (!this.open())
|
|
10918
|
+
return;
|
|
10919
|
+
const target = event.target;
|
|
10920
|
+
const inHost = this.el.nativeElement.contains(target);
|
|
10921
|
+
const inPanel = !!this.panelEl && this.panelEl.contains(target);
|
|
10922
|
+
if (!inHost && !inPanel)
|
|
10923
|
+
this.close();
|
|
10924
|
+
}
|
|
10925
|
+
onEscape() {
|
|
10926
|
+
if (!this.open())
|
|
10927
|
+
return;
|
|
10928
|
+
this.close(true);
|
|
10929
|
+
}
|
|
10930
|
+
// --- keyboard (APG select-only combobox: focus stays on the trigger) ---
|
|
10931
|
+
onTriggerKeydown(event) {
|
|
10932
|
+
if (!this.open()) {
|
|
10933
|
+
if (['ArrowDown', 'ArrowUp', 'Enter', ' '].includes(event.key)) {
|
|
10934
|
+
event.preventDefault();
|
|
10935
|
+
this.openPanel();
|
|
10936
|
+
}
|
|
10937
|
+
return;
|
|
10938
|
+
}
|
|
10939
|
+
switch (event.key) {
|
|
10940
|
+
case 'ArrowDown':
|
|
10941
|
+
event.preventDefault();
|
|
10942
|
+
this.move(1);
|
|
10943
|
+
break;
|
|
10944
|
+
case 'ArrowUp':
|
|
10945
|
+
event.preventDefault();
|
|
10946
|
+
this.move(-1);
|
|
10947
|
+
break;
|
|
10948
|
+
case 'Home':
|
|
10949
|
+
event.preventDefault();
|
|
10950
|
+
this.highlight(0);
|
|
10951
|
+
break;
|
|
10952
|
+
case 'End':
|
|
10953
|
+
event.preventDefault();
|
|
10954
|
+
this.highlight(this.options().length - 1);
|
|
10955
|
+
break;
|
|
10956
|
+
case 'Enter':
|
|
10957
|
+
case ' ':
|
|
10958
|
+
event.preventDefault();
|
|
10959
|
+
this.chooseHighlighted();
|
|
10960
|
+
break;
|
|
10961
|
+
case 'Escape':
|
|
10962
|
+
event.preventDefault();
|
|
10963
|
+
this.close(true);
|
|
10964
|
+
break;
|
|
10965
|
+
case 'Tab':
|
|
10966
|
+
this.close(false);
|
|
10967
|
+
break;
|
|
10968
|
+
default: break;
|
|
10969
|
+
}
|
|
10970
|
+
}
|
|
10971
|
+
initHighlight() {
|
|
10972
|
+
const idx = this.options().findIndex((o) => o.code === this.effectiveCode());
|
|
10973
|
+
this.highlightedIndex.set(idx >= 0 ? idx : this.options().length ? 0 : -1);
|
|
10974
|
+
}
|
|
10975
|
+
move(delta) {
|
|
10976
|
+
const n = this.options().length;
|
|
10977
|
+
if (!n)
|
|
10978
|
+
return;
|
|
10979
|
+
const from = this.highlightedIndex();
|
|
10980
|
+
this.highlight(((from < 0 ? 0 : from) + delta + n) % n);
|
|
10981
|
+
}
|
|
10982
|
+
highlight(i) {
|
|
10983
|
+
if (i < 0 || i >= this.options().length)
|
|
10984
|
+
return;
|
|
10985
|
+
this.highlightedIndex.set(i);
|
|
10986
|
+
this.scrollHighlightedIntoView();
|
|
10987
|
+
}
|
|
10988
|
+
chooseHighlighted() {
|
|
10989
|
+
const opt = this.options()[this.highlightedIndex()];
|
|
10990
|
+
if (opt)
|
|
10991
|
+
this.choose(opt.code);
|
|
10992
|
+
}
|
|
10993
|
+
scrollHighlightedIntoView() {
|
|
10994
|
+
setTimeout(() => {
|
|
10995
|
+
this.panelEl
|
|
10996
|
+
?.querySelector(`[data-index="${this.highlightedIndex()}"]`)
|
|
10997
|
+
?.scrollIntoView({ block: 'nearest' });
|
|
10998
|
+
});
|
|
10999
|
+
}
|
|
11000
|
+
// --- body-appended overlay positioning (mirrors sds-select / sds-date-picker) ---
|
|
11001
|
+
reflow = () => this.positionPanel();
|
|
11002
|
+
listening = false;
|
|
11003
|
+
addReflowListeners() {
|
|
11004
|
+
if (this.listening)
|
|
11005
|
+
return;
|
|
11006
|
+
window.addEventListener('scroll', this.reflow, true);
|
|
11007
|
+
window.addEventListener('resize', this.reflow);
|
|
11008
|
+
this.listening = true;
|
|
11009
|
+
}
|
|
11010
|
+
removeReflowListeners() {
|
|
11011
|
+
if (!this.listening)
|
|
11012
|
+
return;
|
|
11013
|
+
window.removeEventListener('scroll', this.reflow, true);
|
|
11014
|
+
window.removeEventListener('resize', this.reflow);
|
|
11015
|
+
this.listening = false;
|
|
11016
|
+
}
|
|
11017
|
+
positionPanel() {
|
|
11018
|
+
const triggerEl = this.trigger()?.nativeElement;
|
|
11019
|
+
const panelEl = this.panelEl;
|
|
11020
|
+
if (!triggerEl || !panelEl)
|
|
11021
|
+
return;
|
|
11022
|
+
if (panelEl.parentElement !== document.body)
|
|
11023
|
+
document.body.appendChild(panelEl);
|
|
11024
|
+
const r = triggerEl.getBoundingClientRect();
|
|
11025
|
+
const gap = 8;
|
|
11026
|
+
const margin = 8;
|
|
11027
|
+
const pw = panelEl.offsetWidth || 176;
|
|
11028
|
+
const ph = panelEl.offsetHeight || 200;
|
|
11029
|
+
// Vertical: below by default; open above when asked (direction='up') and there is room, or when there
|
|
11030
|
+
// isn't room below but there is above. The panel height is bounded by the list's max-h, but clamp
|
|
11031
|
+
// defensively so top can never be pushed off the top edge.
|
|
11032
|
+
const roomBelow = window.innerHeight - r.bottom - gap;
|
|
11033
|
+
const roomAbove = r.top - gap;
|
|
11034
|
+
let top = (this.direction() === 'up' && roomAbove >= ph) || (roomBelow < ph && roomAbove > roomBelow)
|
|
11035
|
+
? r.top - gap - ph
|
|
11036
|
+
: r.bottom + gap;
|
|
11037
|
+
if (top + ph > window.innerHeight - margin)
|
|
11038
|
+
top = window.innerHeight - margin - ph;
|
|
11039
|
+
if (top < margin)
|
|
11040
|
+
top = margin;
|
|
11041
|
+
// Horizontal: align the panel's end (right) or start (left) edge to the trigger, then clamp.
|
|
11042
|
+
let left = this.align() === 'end' ? r.right - pw : r.left;
|
|
11043
|
+
left = Math.min(left, window.innerWidth - margin - pw);
|
|
11044
|
+
left = Math.max(margin, left);
|
|
11045
|
+
panelEl.style.top = `${Math.round(top)}px`;
|
|
11046
|
+
panelEl.style.left = `${Math.round(left)}px`;
|
|
11047
|
+
}
|
|
11048
|
+
ngOnDestroy() {
|
|
11049
|
+
this.removeReflowListeners();
|
|
11050
|
+
this.panelEl?.remove();
|
|
11051
|
+
}
|
|
11052
|
+
onBrand = computed(() => this.appearance() === 'on-brand', ...(ngDevMode ? [{ debugName: "onBrand" }] : []));
|
|
11053
|
+
triggerClass = computed(() => this.onBrand()
|
|
11054
|
+
? 'text-white/85 hover:bg-white/10 hover:text-white focus-visible:ring-white/40'
|
|
11055
|
+
: 'bg-muted text-ink-soft hover:bg-brand-50 hover:text-brand-600 focus-visible:ring-brand-500/40', ...(ngDevMode ? [{ debugName: "triggerClass" }] : []));
|
|
11056
|
+
chevronClass = computed(() => (this.onBrand() ? 'text-white/70' : 'text-ink-faint'), ...(ngDevMode ? [{ debugName: "chevronClass" }] : []));
|
|
11057
|
+
rowClass(index, selected) {
|
|
11058
|
+
const base = 'flex w-full cursor-pointer items-center gap-3 px-4 py-2.5 text-sm font-medium transition-colors';
|
|
11059
|
+
const highlighted = this.highlightedIndex() === index;
|
|
11060
|
+
let state;
|
|
11061
|
+
if (selected) {
|
|
11062
|
+
state = highlighted ? 'bg-brand-500/25 text-ink' : 'bg-brand-500/15 text-ink';
|
|
11063
|
+
}
|
|
11064
|
+
else {
|
|
11065
|
+
state = highlighted ? 'bg-muted text-ink' : 'text-ink-soft';
|
|
11066
|
+
}
|
|
11067
|
+
return `${base} ${state}`;
|
|
11068
|
+
}
|
|
11069
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: SdsLanguageSwitcher, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
11070
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.25", type: SdsLanguageSwitcher, isStandalone: true, selector: "sds-language-switcher", inputs: { languages: { classPropertyName: "languages", publicName: "languages", isSignal: true, isRequired: false, transformFunction: null }, current: { classPropertyName: "current", publicName: "current", 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 }, direction: { classPropertyName: "direction", publicName: "direction", isSignal: true, isRequired: false, transformFunction: null }, menuLabel: { classPropertyName: "menuLabel", publicName: "menuLabel", isSignal: true, isRequired: false, transformFunction: null }, heading: { classPropertyName: "heading", publicName: "heading", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { current: "currentChange", 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: `
|
|
11071
|
+
<button
|
|
11072
|
+
#trigger
|
|
11073
|
+
type="button"
|
|
11074
|
+
role="combobox"
|
|
11075
|
+
class="flex h-10 items-center gap-1.5 rounded-control px-3 text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2"
|
|
11076
|
+
[class]="triggerClass()"
|
|
11077
|
+
[attr.aria-label]="menuLabel()"
|
|
11078
|
+
aria-haspopup="listbox"
|
|
11079
|
+
[attr.aria-expanded]="open()"
|
|
11080
|
+
[attr.aria-controls]="open() ? panelId : null"
|
|
11081
|
+
[attr.aria-activedescendant]="open() ? activeOptionId() : null"
|
|
11082
|
+
(click)="toggle()"
|
|
11083
|
+
(keydown)="onTriggerKeydown($event)"
|
|
11084
|
+
>
|
|
11085
|
+
<sds-icon name="languages" [size]="16" class="shrink-0" />
|
|
11086
|
+
<span>{{ currentCode() }}</span>
|
|
11087
|
+
<sds-icon
|
|
11088
|
+
name="chevron-down"
|
|
11089
|
+
[size]="14"
|
|
11090
|
+
class="shrink-0 transition-transform"
|
|
11091
|
+
[class]="chevronClass()"
|
|
11092
|
+
[class.rotate-180]="open()"
|
|
11093
|
+
/>
|
|
11094
|
+
</button>
|
|
11095
|
+
|
|
11096
|
+
@if (open()) {
|
|
11097
|
+
<!-- Appended to document.body on open (see positionPanel) so no ancestor can clip it. -->
|
|
11098
|
+
<div
|
|
11099
|
+
data-sds-language-panel
|
|
11100
|
+
[id]="panelId"
|
|
11101
|
+
role="listbox"
|
|
11102
|
+
[attr.aria-label]="menuLabel()"
|
|
11103
|
+
class="fixed z-[1000] min-w-44 overflow-hidden rounded-card border border-border bg-surface shadow-xl animate-sds-menu"
|
|
11104
|
+
>
|
|
11105
|
+
@if (heading(); as h) {
|
|
11106
|
+
<p class="border-b border-border px-4 pb-2 pt-3 text-[0.68rem] font-semibold uppercase tracking-wider text-ink-faint">
|
|
11107
|
+
{{ h }}
|
|
11108
|
+
</p>
|
|
11109
|
+
}
|
|
11110
|
+
<div class="max-h-[60vh] overflow-auto py-1.5">
|
|
11111
|
+
@for (opt of options(); track opt.code; let i = $index) {
|
|
11112
|
+
<div
|
|
11113
|
+
[id]="optionId(i)"
|
|
11114
|
+
role="option"
|
|
11115
|
+
[attr.data-index]="i"
|
|
11116
|
+
[attr.aria-selected]="opt.code === effectiveCode()"
|
|
11117
|
+
[class]="rowClass(i, opt.code === effectiveCode())"
|
|
11118
|
+
(click)="choose(opt.code)"
|
|
11119
|
+
(mouseenter)="highlightedIndex.set(i)"
|
|
11120
|
+
>
|
|
11121
|
+
<span class="flex-1 truncate text-left">{{ opt.label }}</span>
|
|
11122
|
+
<span
|
|
11123
|
+
class="shrink-0 text-[0.7rem] font-semibold uppercase tracking-wide"
|
|
11124
|
+
[class]="opt.code === effectiveCode() ? 'text-brand-600' : 'text-ink-faint'"
|
|
11125
|
+
>{{ opt.code }}</span
|
|
11126
|
+
>
|
|
11127
|
+
@if (opt.code === effectiveCode()) {
|
|
11128
|
+
<sds-icon name="check" [size]="16" class="shrink-0 text-brand-600" />
|
|
11129
|
+
} @else {
|
|
11130
|
+
<span class="w-4 shrink-0" aria-hidden="true"></span>
|
|
11131
|
+
}
|
|
11132
|
+
</div>
|
|
11133
|
+
}
|
|
11134
|
+
</div>
|
|
11135
|
+
</div>
|
|
11136
|
+
}
|
|
11137
|
+
`, 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: SdsIcon, selector: "sds-icon", inputs: ["name", "size", "strokeWidth"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
11138
|
+
}
|
|
11139
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImport: i0, type: SdsLanguageSwitcher, decorators: [{
|
|
11140
|
+
type: Component,
|
|
11141
|
+
args: [{ selector: 'sds-language-switcher', changeDetection: ChangeDetectionStrategy.OnPush, imports: [SdsIcon], host: {
|
|
11142
|
+
class: 'relative inline-block',
|
|
11143
|
+
'(document:click)': 'onDocumentClick($event)',
|
|
11144
|
+
'(document:keydown.escape)': 'onEscape()',
|
|
11145
|
+
}, template: `
|
|
11146
|
+
<button
|
|
11147
|
+
#trigger
|
|
11148
|
+
type="button"
|
|
11149
|
+
role="combobox"
|
|
11150
|
+
class="flex h-10 items-center gap-1.5 rounded-control px-3 text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2"
|
|
11151
|
+
[class]="triggerClass()"
|
|
11152
|
+
[attr.aria-label]="menuLabel()"
|
|
11153
|
+
aria-haspopup="listbox"
|
|
11154
|
+
[attr.aria-expanded]="open()"
|
|
11155
|
+
[attr.aria-controls]="open() ? panelId : null"
|
|
11156
|
+
[attr.aria-activedescendant]="open() ? activeOptionId() : null"
|
|
11157
|
+
(click)="toggle()"
|
|
11158
|
+
(keydown)="onTriggerKeydown($event)"
|
|
11159
|
+
>
|
|
11160
|
+
<sds-icon name="languages" [size]="16" class="shrink-0" />
|
|
11161
|
+
<span>{{ currentCode() }}</span>
|
|
11162
|
+
<sds-icon
|
|
11163
|
+
name="chevron-down"
|
|
11164
|
+
[size]="14"
|
|
11165
|
+
class="shrink-0 transition-transform"
|
|
11166
|
+
[class]="chevronClass()"
|
|
11167
|
+
[class.rotate-180]="open()"
|
|
11168
|
+
/>
|
|
11169
|
+
</button>
|
|
11170
|
+
|
|
11171
|
+
@if (open()) {
|
|
11172
|
+
<!-- Appended to document.body on open (see positionPanel) so no ancestor can clip it. -->
|
|
11173
|
+
<div
|
|
11174
|
+
data-sds-language-panel
|
|
11175
|
+
[id]="panelId"
|
|
11176
|
+
role="listbox"
|
|
11177
|
+
[attr.aria-label]="menuLabel()"
|
|
11178
|
+
class="fixed z-[1000] min-w-44 overflow-hidden rounded-card border border-border bg-surface shadow-xl animate-sds-menu"
|
|
11179
|
+
>
|
|
11180
|
+
@if (heading(); as h) {
|
|
11181
|
+
<p class="border-b border-border px-4 pb-2 pt-3 text-[0.68rem] font-semibold uppercase tracking-wider text-ink-faint">
|
|
11182
|
+
{{ h }}
|
|
11183
|
+
</p>
|
|
11184
|
+
}
|
|
11185
|
+
<div class="max-h-[60vh] overflow-auto py-1.5">
|
|
11186
|
+
@for (opt of options(); track opt.code; let i = $index) {
|
|
11187
|
+
<div
|
|
11188
|
+
[id]="optionId(i)"
|
|
11189
|
+
role="option"
|
|
11190
|
+
[attr.data-index]="i"
|
|
11191
|
+
[attr.aria-selected]="opt.code === effectiveCode()"
|
|
11192
|
+
[class]="rowClass(i, opt.code === effectiveCode())"
|
|
11193
|
+
(click)="choose(opt.code)"
|
|
11194
|
+
(mouseenter)="highlightedIndex.set(i)"
|
|
11195
|
+
>
|
|
11196
|
+
<span class="flex-1 truncate text-left">{{ opt.label }}</span>
|
|
11197
|
+
<span
|
|
11198
|
+
class="shrink-0 text-[0.7rem] font-semibold uppercase tracking-wide"
|
|
11199
|
+
[class]="opt.code === effectiveCode() ? 'text-brand-600' : 'text-ink-faint'"
|
|
11200
|
+
>{{ opt.code }}</span
|
|
11201
|
+
>
|
|
11202
|
+
@if (opt.code === effectiveCode()) {
|
|
11203
|
+
<sds-icon name="check" [size]="16" class="shrink-0 text-brand-600" />
|
|
11204
|
+
} @else {
|
|
11205
|
+
<span class="w-4 shrink-0" aria-hidden="true"></span>
|
|
11206
|
+
}
|
|
11207
|
+
</div>
|
|
11208
|
+
}
|
|
11209
|
+
</div>
|
|
11210
|
+
</div>
|
|
11211
|
+
}
|
|
11212
|
+
`, 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"] }]
|
|
11213
|
+
}], propDecorators: { trigger: [{ type: i0.ViewChild, args: ['trigger', { isSignal: true }] }], languages: [{ type: i0.Input, args: [{ isSignal: true, alias: "languages", required: false }] }], current: [{ type: i0.Input, args: [{ isSignal: true, alias: "current", required: false }] }, { type: i0.Output, args: ["currentChange"] }], appearance: [{ type: i0.Input, args: [{ isSignal: true, alias: "appearance", required: false }] }], align: [{ type: i0.Input, args: [{ isSignal: true, alias: "align", required: false }] }], direction: [{ type: i0.Input, args: [{ isSignal: true, alias: "direction", required: false }] }], menuLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "menuLabel", required: false }] }], heading: [{ type: i0.Input, args: [{ isSignal: true, alias: "heading", required: false }] }], openChange: [{ type: i0.Output, args: ["openChange"] }] } });
|
|
11214
|
+
|
|
9652
11215
|
/*
|
|
9653
11216
|
* Public API Surface of saitech-design-system — the Saitech / App design system.
|
|
9654
11217
|
*/
|
|
@@ -9658,5 +11221,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.25", ngImpo
|
|
|
9658
11221
|
* Generated bundle index. Do not edit.
|
|
9659
11222
|
*/
|
|
9660
11223
|
|
|
9661
|
-
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, SdsSelectEmpty, SdsSelectFooter, SdsSelectOption, 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 };
|
|
11224
|
+
export { SDS_ICONS, SDS_PHONE_COUNTRIES, SdsAccordion, SdsAccordionContent, SdsAccordionHeader, SdsAccordionPanel, SdsAccordionStore, SdsAvatar, SdsBadge, SdsButton, SdsCalendar, SdsCard, SdsCheckbox, SdsDatePicker, SdsIcon, SdsInput, SdsInputNumber, SdsLanguageSwitcher, 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 };
|
|
9662
11225
|
//# sourceMappingURL=besaitech-ng-design-system.mjs.map
|