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