@nexterajs/ui 0.0.2 → 0.1.0

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.
Files changed (59) hide show
  1. package/README.md +279 -34
  2. package/fesm2022/nexterajs-ui-charts.mjs +1096 -0
  3. package/fesm2022/nexterajs-ui-charts.mjs.map +1 -0
  4. package/fesm2022/nexterajs-ui-core.mjs +652 -12
  5. package/fesm2022/nexterajs-ui-core.mjs.map +1 -1
  6. package/fesm2022/nexterajs-ui-display.mjs +1142 -0
  7. package/fesm2022/nexterajs-ui-display.mjs.map +1 -0
  8. package/fesm2022/nexterajs-ui-dropdowns.mjs +321 -33
  9. package/fesm2022/nexterajs-ui-dropdowns.mjs.map +1 -1
  10. package/fesm2022/nexterajs-ui-file-uploader.mjs +572 -10
  11. package/fesm2022/nexterajs-ui-file-uploader.mjs.map +1 -1
  12. package/fesm2022/nexterajs-ui-forms.mjs +2027 -136
  13. package/fesm2022/nexterajs-ui-forms.mjs.map +1 -1
  14. package/fesm2022/nexterajs-ui-grid.mjs +1222 -188
  15. package/fesm2022/nexterajs-ui-grid.mjs.map +1 -1
  16. package/fesm2022/nexterajs-ui-intl.mjs +103 -11
  17. package/fesm2022/nexterajs-ui-intl.mjs.map +1 -1
  18. package/fesm2022/nexterajs-ui-navigation.mjs +1054 -0
  19. package/fesm2022/nexterajs-ui-navigation.mjs.map +1 -0
  20. package/fesm2022/nexterajs-ui-notify.mjs +46 -44
  21. package/fesm2022/nexterajs-ui-notify.mjs.map +1 -1
  22. package/fesm2022/nexterajs-ui-overlay.mjs +1671 -0
  23. package/fesm2022/nexterajs-ui-overlay.mjs.map +1 -0
  24. package/fesm2022/nexterajs-ui-pivot.mjs +51 -26
  25. package/fesm2022/nexterajs-ui-pivot.mjs.map +1 -1
  26. package/fesm2022/nexterajs-ui-shell.mjs +542 -0
  27. package/fesm2022/nexterajs-ui-shell.mjs.map +1 -0
  28. package/fesm2022/nexterajs-ui-tree-view.mjs +5 -2
  29. package/fesm2022/nexterajs-ui-tree-view.mjs.map +1 -1
  30. package/fesm2022/nexterajs-ui-validation.mjs +42 -11
  31. package/fesm2022/nexterajs-ui-validation.mjs.map +1 -1
  32. package/fesm2022/nexterajs-ui.mjs +5 -0
  33. package/fesm2022/nexterajs-ui.mjs.map +1 -1
  34. package/package.json +33 -2
  35. package/styles/_dropdown.scss +5 -5
  36. package/styles/_editor.scss +52 -17
  37. package/styles/_mixins.scss +1 -1
  38. package/styles/_scrollbar.scss +5 -5
  39. package/styles/_status.scss +21 -0
  40. package/styles/_toast.scss +14 -14
  41. package/styles/_tokens.scss +420 -3
  42. package/styles/_widget.scss +14 -3
  43. package/styles/index.scss +6 -4
  44. package/types/nexterajs-ui-charts.d.ts +446 -0
  45. package/types/nexterajs-ui-core.d.ts +387 -22
  46. package/types/nexterajs-ui-display.d.ts +720 -0
  47. package/types/nexterajs-ui-dropdowns.d.ts +191 -48
  48. package/types/nexterajs-ui-file-uploader.d.ts +236 -7
  49. package/types/nexterajs-ui-forms.d.ts +675 -30
  50. package/types/nexterajs-ui-grid.d.ts +448 -93
  51. package/types/nexterajs-ui-intl.d.ts +39 -4
  52. package/types/nexterajs-ui-navigation.d.ts +531 -0
  53. package/types/nexterajs-ui-notify.d.ts +4 -2
  54. package/types/nexterajs-ui-overlay.d.ts +703 -0
  55. package/types/nexterajs-ui-pivot.d.ts +56 -48
  56. package/types/nexterajs-ui-shell.d.ts +294 -0
  57. package/types/nexterajs-ui-tree-view.d.ts +6 -3
  58. package/types/nexterajs-ui-validation.d.ts +20 -11
  59. package/types/nexterajs-ui.d.ts +5 -0
@@ -0,0 +1,1054 @@
1
+ import { NgTemplateOutlet } from '@angular/common';
2
+ import * as i0 from '@angular/core';
3
+ import { inject, TemplateRef, Directive, input, numberAttribute, booleanAttribute, contentChild, viewChild, computed, ChangeDetectionStrategy, Component, model, contentChildren, linkedSignal, signal, DestroyRef, output, ElementRef, afterNextRender, ViewEncapsulation } from '@angular/core';
4
+ import { toSignal, takeUntilDestroyed, toObservable } from '@angular/core/rxjs-interop';
5
+ import { Router, ActivatedRoute, NavigationEnd, RouterLink } from '@angular/router';
6
+ import { NxeBadgeComponent } from '@nexterajs/ui/display';
7
+ import { filter, map, switchMap, startWith, of } from 'rxjs';
8
+ import { nxeOptionalBoolean, NxeMessageService } from '@nexterajs/ui/core';
9
+ import { NxeMenuComponent, NxeMenuItemComponent, NxeMenuTriggerForDirective } from '@nexterajs/ui/overlay';
10
+
11
+ /**
12
+ * Marks the `<ng-template>` that holds a tab's lazy content. The template is stamped out the first
13
+ * time the tab is activated, not before, so a heavy panel (a grid, a chart) costs nothing until it
14
+ * is opened:
15
+ *
16
+ * ```html
17
+ * <nxe-tab label="Lines">
18
+ * <ng-template nxeTabContent><app-po-lines /></ng-template>
19
+ * </nxe-tab>
20
+ * ```
21
+ *
22
+ * Whether it is destroyed again when the tab is left is `keepAlive` on the tab or the tabs.
23
+ */
24
+ class NxeTabContentDirective {
25
+ /** The template to render. */
26
+ template = inject(TemplateRef);
27
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeTabContentDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
28
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.1.5", type: NxeTabContentDirective, isStandalone: true, selector: "ng-template[nxeTabContent]", ngImport: i0 });
29
+ }
30
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeTabContentDirective, decorators: [{
31
+ type: Directive,
32
+ args: [{
33
+ selector: 'ng-template[nxeTabContent]',
34
+ }]
35
+ }] });
36
+
37
+ /**
38
+ * One tab of an {@link NxeTabsComponent}. It draws nothing itself: the parent renders the tab
39
+ * button and the panel from the inputs and content declared here.
40
+ *
41
+ * ```html
42
+ * <nxe-tab label="Details">Plain content: created with the page, shown when active.</nxe-tab>
43
+ * <nxe-tab label="Lines" [badge]="12" key="lines">
44
+ * <ng-template nxeTabContent>Lazy: created the first time the tab opens.</ng-template>
45
+ * </nxe-tab>
46
+ * <nxe-tab label="Pending" [link]="[]" [queryParams]="{ status: 'pending' }" [badge]="4" />
47
+ * ```
48
+ *
49
+ * With `link` set the tab is an anchor and the URL decides whether it is active (router mode).
50
+ */
51
+ class NxeTabComponent {
52
+ /** The tab's text. */
53
+ label = input('', /* @ts-ignore */
54
+ ...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
55
+ /** A stable id for `selectedKey` and for telling tabs apart across reorders. */
56
+ key = input(null, /* @ts-ignore */
57
+ ...(ngDevMode ? [{ debugName: "key" }] : /* istanbul ignore next */ []));
58
+ /** A count shown after the label in an `nxe-badge`; `null` or `undefined` shows none, 0 shows 0. */
59
+ badge = input(undefined, { ...(ngDevMode ? { debugName: "badge" } : /* istanbul ignore next */ {}), transform: (v) => (v === null || v === undefined || v === '' ? undefined : numberAttribute(v, NaN)) });
60
+ /** Status family of the badge. `neutral` by default: a count in a tab is information, not an alarm. */
61
+ badgeFamily = input('neutral', /* @ts-ignore */
62
+ ...(ngDevMode ? [{ debugName: "badgeFamily" }] : /* istanbul ignore next */ []));
63
+ /** Not selectable and skipped by the arrow keys. */
64
+ disabled = input(false, { ...(ngDevMode ? { debugName: "disabled" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
65
+ /** Overrides the tabs' `keepAlive` for this tab: keep the content after leaving it, or destroy it. */
66
+ keepAlive = input(undefined, { ...(ngDevMode ? { debugName: "keepAlive" } : /* istanbul ignore next */ {}), transform: (v) => (v === undefined || v === null || v === '' ? undefined : booleanAttribute(v)) });
67
+ /**
68
+ * Router mode. Router commands (`['/pos', id]`, `[]` for the current route) or a URL string; the tab
69
+ * becomes an anchor and is active while the URL matches this link plus its `queryParams`.
70
+ */
71
+ link = input(undefined, /* @ts-ignore */
72
+ ...(ngDevMode ? [{ debugName: "link" }] : /* istanbul ignore next */ []));
73
+ /** Query params of the link, e.g. `{ status: 'pending' }`. */
74
+ queryParams = input(undefined, /* @ts-ignore */
75
+ ...(ngDevMode ? [{ debugName: "queryParams" }] : /* istanbul ignore next */ []));
76
+ /** `merge` keeps the other query params in the URL (a search term, a page) when switching tab. */
77
+ queryParamsHandling = input('', /* @ts-ignore */
78
+ ...(ngDevMode ? [{ debugName: "queryParamsHandling" }] : /* istanbul ignore next */ []));
79
+ lazy = contentChild(NxeTabContentDirective, /* @ts-ignore */
80
+ ...(ngDevMode ? [{ debugName: "lazy" }] : /* istanbul ignore next */ []));
81
+ implicit = viewChild.required('implicit', /* @ts-ignore */
82
+ ...(ngDevMode ? [{ debugName: "implicit" }] : /* istanbul ignore next */ []));
83
+ /** True when the content was declared in an `nxeTabContent` template. */
84
+ isLazy = computed(() => this.lazy() !== undefined, /* @ts-ignore */
85
+ ...(ngDevMode ? [{ debugName: "isLazy" }] : /* istanbul ignore next */ []));
86
+ /** The template the parent stamps into the panel. */
87
+ contentTemplate = computed(() => this.lazy()?.template ?? this.implicit(), /* @ts-ignore */
88
+ ...(ngDevMode ? [{ debugName: "contentTemplate" }] : /* istanbul ignore next */ []));
89
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeTabComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
90
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.2.0", version: "22.1.5", type: NxeTabComponent, isStandalone: true, selector: "nxe-tab", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, key: { classPropertyName: "key", publicName: "key", isSignal: true, isRequired: false, transformFunction: null }, badge: { classPropertyName: "badge", publicName: "badge", isSignal: true, isRequired: false, transformFunction: null }, badgeFamily: { classPropertyName: "badgeFamily", publicName: "badgeFamily", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, keepAlive: { classPropertyName: "keepAlive", publicName: "keepAlive", isSignal: true, isRequired: false, transformFunction: null }, link: { classPropertyName: "link", publicName: "link", isSignal: true, isRequired: false, transformFunction: null }, queryParams: { classPropertyName: "queryParams", publicName: "queryParams", isSignal: true, isRequired: false, transformFunction: null }, queryParamsHandling: { classPropertyName: "queryParamsHandling", publicName: "queryParamsHandling", isSignal: true, isRequired: false, transformFunction: null } }, host: { classAttribute: "nxe-tab" }, queries: [{ propertyName: "lazy", first: true, predicate: NxeTabContentDirective, descendants: true, isSignal: true }], viewQueries: [{ propertyName: "implicit", first: true, predicate: ["implicit"], descendants: true, isSignal: true }], ngImport: i0, template: '<ng-template #implicit><ng-content /></ng-template>', isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush });
91
+ }
92
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeTabComponent, decorators: [{
93
+ type: Component,
94
+ args: [{
95
+ selector: 'nxe-tab',
96
+ template: '<ng-template #implicit><ng-content /></ng-template>',
97
+ host: { class: 'nxe-tab' },
98
+ changeDetection: ChangeDetectionStrategy.OnPush,
99
+ }]
100
+ }], propDecorators: { label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], key: [{ type: i0.Input, args: [{ isSignal: true, alias: "key", required: false }] }], badge: [{ type: i0.Input, args: [{ isSignal: true, alias: "badge", required: false }] }], badgeFamily: [{ type: i0.Input, args: [{ isSignal: true, alias: "badgeFamily", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], keepAlive: [{ type: i0.Input, args: [{ isSignal: true, alias: "keepAlive", required: false }] }], link: [{ type: i0.Input, args: [{ isSignal: true, alias: "link", required: false }] }], queryParams: [{ type: i0.Input, args: [{ isSignal: true, alias: "queryParams", required: false }] }], queryParamsHandling: [{ type: i0.Input, args: [{ isSignal: true, alias: "queryParamsHandling", required: false }] }], lazy: [{ type: i0.ContentChild, args: [i0.forwardRef(() => NxeTabContentDirective), { isSignal: true }] }], implicit: [{ type: i0.ViewChild, args: ['implicit', { isSignal: true }] }] } });
101
+
102
+ let nextTabsId = 0;
103
+ /** Whether a declared query param equals what the URL carries (arrays compare element-wise). */
104
+ function sameParam(want, have) {
105
+ if (have === undefined) {
106
+ return false;
107
+ }
108
+ const norm = (v) => (Array.isArray(v) ? v.map(String).join(',') : String(v));
109
+ return norm(want) === norm(have);
110
+ }
111
+ /**
112
+ * `nxe-tabs` is a WAI-ARIA tabs widget: a `tablist` of `tab` buttons over `tabpanel`s, one tab in the
113
+ * tab order (roving `tabindex`), Left/Right (flipped in RTL), Home and End to move, and the active tab
114
+ * in `aria-selected`.
115
+ *
116
+ * ```html
117
+ * <nxe-tabs [(selectedKey)]="tab" tablistLabel="Purchase order">
118
+ * <nxe-tab key="details" label="Details">...</nxe-tab>
119
+ * <nxe-tab key="lines" label="Lines" [badge]="lines().length">
120
+ * <ng-template nxeTabContent><app-po-lines /></ng-template>
121
+ * </nxe-tab>
122
+ * <nxe-tab key="history" label="History" disabled />
123
+ * </nxe-tabs>
124
+ * ```
125
+ *
126
+ * **Selection.** `selectedIndex` and `selectedKey` are two-way models. A tab with a `key` is better
127
+ * addressed by key (it survives a tab being added or hidden); if both are set the key wins, so bind one.
128
+ *
129
+ * **Lazy content.** Plain content is part of the page from the start. Content inside
130
+ * `<ng-template nxeTabContent>` is created the first time its tab is active. `keepAlive` (default
131
+ * `true`) then keeps it, with its state, when you leave; `false` destroys it. A tab's own `keepAlive`
132
+ * overrides.
133
+ *
134
+ * **Router mode.** A tab with `link` is an anchor and the URL decides which tab is active, for a route
135
+ * path or for a query param (`[link]="[]" [queryParams]="{ status: 'pending' }"`). Query params are
136
+ * compared as each tab declares them (`queryParamsHandling` plays no part in matching): a key any link
137
+ * tab declares is a key of the group, and a tab that leaves it out is active only while the URL does not
138
+ * carry it, so an "All" tab never shares the highlight with "PO" at `?status=po`. Of several matches the
139
+ * tab declaring the most params wins. The back button moves the tab. Link tabs follow the URL, so Left/Right only move focus
140
+ * and Enter follows the link. Put a `router-outlet` (or your list) beside the tabs, or give a link tab an
141
+ * `nxeTabContent` panel.
142
+ *
143
+ * **Overflow.** More tabs than fit scroll sideways; the focused or selected tab is kept in view.
144
+ */
145
+ class NxeTabsComponent {
146
+ /** Zero-based index of the selected tab. Two-way. */
147
+ selectedIndex = model(0, /* @ts-ignore */
148
+ ...(ngDevMode ? [{ debugName: "selectedIndex" }] : /* istanbul ignore next */ []));
149
+ /** `key` of the selected tab. Two-way; wins over `selectedIndex` when it matches a tab. */
150
+ selectedKey = model(null, /* @ts-ignore */
151
+ ...(ngDevMode ? [{ debugName: "selectedKey" }] : /* istanbul ignore next */ []));
152
+ /** `automatic` (default) selects on arrow; `manual` needs Enter or Space. */
153
+ activation = input('automatic', /* @ts-ignore */
154
+ ...(ngDevMode ? [{ debugName: "activation" }] : /* istanbul ignore next */ []));
155
+ /** Keep a tab's content alive after leaving it. A tab's own `keepAlive` overrides. */
156
+ keepAlive = input(true, { ...(ngDevMode ? { debugName: "keepAlive" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
157
+ /** Accessible name of the tab list. */
158
+ tablistLabel = input('', /* @ts-ignore */
159
+ ...(ngDevMode ? [{ debugName: "tablistLabel" }] : /* istanbul ignore next */ []));
160
+ tabs = contentChildren(NxeTabComponent, /* @ts-ignore */
161
+ ...(ngDevMode ? [{ debugName: "tabs" }] : /* istanbul ignore next */ []));
162
+ router = inject(Router);
163
+ route = inject(ActivatedRoute);
164
+ list = viewChild.required('list', /* @ts-ignore */
165
+ ...(ngDevMode ? [{ debugName: "list" }] : /* istanbul ignore next */ []));
166
+ uid = `nxe-tabs-${nextTabsId++}`;
167
+ /** The URL after each navigation; what router-mode tabs are matched against. */
168
+ url = toSignal(this.router.events.pipe(filter((event) => event instanceof NavigationEnd), map(() => this.router.url)), { initialValue: this.router.url });
169
+ /** Index of the link tab that matches the URL, or -1. */
170
+ linkIndex = computed(() => {
171
+ const urlParams = this.router.parseUrl(this.url()).queryParams;
172
+ // The query keys the group discriminates on: any key a link tab declares.
173
+ const groupKeys = new Set();
174
+ for (const tab of this.tabs()) {
175
+ if (tab.link() !== null && tab.link() !== undefined) {
176
+ Object.keys(tab.queryParams() ?? {}).forEach((key) => groupKeys.add(key));
177
+ }
178
+ }
179
+ let best = -1;
180
+ let bestScore = -1;
181
+ this.tabs().forEach((tab, index) => {
182
+ const link = tab.link();
183
+ if (link === null || link === undefined || tab.disabled()) {
184
+ return;
185
+ }
186
+ const declared = tab.queryParams() ?? {};
187
+ // Path only: the tab's own declared params (not merged ones) are compared below.
188
+ const tree = this.router.createUrlTree(typeof link === 'string' ? [link] : [...link], {
189
+ relativeTo: this.route,
190
+ });
191
+ const pathActive = this.router.isActive(tree, {
192
+ paths: 'subset',
193
+ queryParams: 'ignored',
194
+ fragment: 'ignored',
195
+ matrixParams: 'ignored',
196
+ });
197
+ if (!pathActive) {
198
+ return;
199
+ }
200
+ // A group key the tab declares must equal the URL's; one it leaves out must be absent.
201
+ for (const key of groupKeys) {
202
+ const want = declared[key];
203
+ const have = urlParams[key];
204
+ if (want === null || want === undefined) {
205
+ if (have !== undefined) {
206
+ return;
207
+ }
208
+ }
209
+ else if (!sameParam(want, have)) {
210
+ return;
211
+ }
212
+ }
213
+ const score = Object.keys(declared).length;
214
+ if (score > bestScore) {
215
+ best = index;
216
+ bestScore = score;
217
+ }
218
+ });
219
+ return best;
220
+ }, /* @ts-ignore */
221
+ ...(ngDevMode ? [{ debugName: "linkIndex" }] : /* istanbul ignore next */ []));
222
+ /** Index of the active tab, or -1 when every tab is disabled. */
223
+ activeIndex = computed(() => {
224
+ const tabs = this.tabs();
225
+ const fromUrl = this.linkIndex();
226
+ if (fromUrl >= 0) {
227
+ return fromUrl;
228
+ }
229
+ const key = this.selectedKey();
230
+ if (key !== null) {
231
+ const byKey = tabs.findIndex((tab) => tab.key() === key);
232
+ if (byKey >= 0 && !tabs[byKey].disabled()) {
233
+ return byKey;
234
+ }
235
+ }
236
+ const index = this.selectedIndex();
237
+ if (index >= 0 && index < tabs.length && !tabs[index].disabled()) {
238
+ return index;
239
+ }
240
+ return tabs.findIndex((tab) => !tab.disabled());
241
+ }, /* @ts-ignore */
242
+ ...(ngDevMode ? [{ debugName: "activeIndex" }] : /* istanbul ignore next */ []));
243
+ activeTab = computed(() => this.tabs()[this.activeIndex()], /* @ts-ignore */
244
+ ...(ngDevMode ? [{ debugName: "activeTab" }] : /* istanbul ignore next */ []));
245
+ /** Every tab that has been active, for lazy content that stays alive. */
246
+ visited = linkedSignal({ ...(ngDevMode ? { debugName: "visited" } : /* istanbul ignore next */ {}), source: this.activeTab,
247
+ computation: (tab, previous) => {
248
+ const set = new Set(previous?.value);
249
+ if (tab) {
250
+ set.add(tab);
251
+ }
252
+ return set;
253
+ } });
254
+ /** The tab with focus inside the list, while it has any. */
255
+ focusedIndex = signal(undefined, /* @ts-ignore */
256
+ ...(ngDevMode ? [{ debugName: "focusedIndex" }] : /* istanbul ignore next */ []));
257
+ /** The one tab in the tab order: the focused one, else the active one. */
258
+ tabbableIndex = computed(() => this.focusedIndex() ?? Math.max(this.activeIndex(), 0), /* @ts-ignore */
259
+ ...(ngDevMode ? [{ debugName: "tabbableIndex" }] : /* istanbul ignore next */ []));
260
+ constructor() {
261
+ // Keep the models in step when the URL moves the tab (back button, a link elsewhere).
262
+ this.router.events
263
+ .pipe(filter((event) => event instanceof NavigationEnd), takeUntilDestroyed(inject(DestroyRef)))
264
+ .subscribe(() => {
265
+ const index = this.linkIndex();
266
+ if (index >= 0) {
267
+ this.commit(index);
268
+ }
269
+ });
270
+ }
271
+ tabId(index) {
272
+ return `${this.uid}-tab-${index}`;
273
+ }
274
+ panelId(index) {
275
+ return `${this.uid}-panel-${index}`;
276
+ }
277
+ isLink(tab) {
278
+ return tab.link() !== null && tab.link() !== undefined;
279
+ }
280
+ /** True when a tab has a panel to point `aria-controls` at. */
281
+ hasPanel(tab) {
282
+ return !this.isLink(tab) || tab.isLazy();
283
+ }
284
+ /** Whether the panel's content exists right now. */
285
+ isRendered(tab, index) {
286
+ if (index === this.activeIndex()) {
287
+ return true;
288
+ }
289
+ return (tab.keepAlive() ?? this.keepAlive()) && this.visited().has(tab);
290
+ }
291
+ select(index) {
292
+ const tab = this.tabs()[index];
293
+ if (!tab || tab.disabled() || this.isLink(tab)) {
294
+ // A link tab is selected by the navigation its anchor triggers, not by us.
295
+ return;
296
+ }
297
+ this.commit(index);
298
+ }
299
+ commit(index) {
300
+ const tab = this.tabs()[index];
301
+ if (!tab) {
302
+ return;
303
+ }
304
+ this.focusedIndex.set(undefined);
305
+ if (this.selectedIndex() !== index) {
306
+ this.selectedIndex.set(index);
307
+ }
308
+ const key = tab.key();
309
+ if (this.selectedKey() !== key) {
310
+ this.selectedKey.set(key);
311
+ }
312
+ }
313
+ onFocusOut(event) {
314
+ const next = event.relatedTarget;
315
+ if (!next || !this.list().nativeElement.contains(next)) {
316
+ this.focusedIndex.set(undefined);
317
+ }
318
+ }
319
+ onKeydown(event) {
320
+ const current = this.triggers().indexOf(event.target);
321
+ if (current < 0) {
322
+ // Keys typed in a panel are the panel's.
323
+ return;
324
+ }
325
+ const enabled = this.tabs()
326
+ .map((tab, index) => (tab.disabled() ? -1 : index))
327
+ .filter((index) => index >= 0);
328
+ if (enabled.length === 0) {
329
+ return;
330
+ }
331
+ const position = Math.max(enabled.indexOf(current), 0);
332
+ const rtl = getComputedStyle(this.list().nativeElement).direction === 'rtl';
333
+ const step = (by) => enabled[(position + by + enabled.length) % enabled.length];
334
+ let target;
335
+ switch (event.key) {
336
+ case 'ArrowRight':
337
+ target = step(rtl ? -1 : 1);
338
+ break;
339
+ case 'ArrowLeft':
340
+ target = step(rtl ? 1 : -1);
341
+ break;
342
+ case 'Home':
343
+ target = enabled[0];
344
+ break;
345
+ case 'End':
346
+ target = enabled[enabled.length - 1];
347
+ break;
348
+ default:
349
+ // Enter and Space select through the button's own click.
350
+ return;
351
+ }
352
+ event.preventDefault();
353
+ this.focusTab(target);
354
+ if (this.activation() === 'automatic') {
355
+ this.select(target);
356
+ }
357
+ }
358
+ triggers() {
359
+ return Array.from(this.list().nativeElement.querySelectorAll('[role="tab"]'));
360
+ }
361
+ focusTab(index) {
362
+ this.focusedIndex.set(index);
363
+ // Focusing scrolls an overflowed tab into view.
364
+ this.triggers()[index]?.focus();
365
+ }
366
+ /** Moves focus to the tab that is in the tab order (the active one). */
367
+ focus() {
368
+ this.triggers()[this.tabbableIndex()]?.focus();
369
+ }
370
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeTabsComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
371
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.5", type: NxeTabsComponent, isStandalone: true, selector: "nxe-tabs", inputs: { selectedIndex: { classPropertyName: "selectedIndex", publicName: "selectedIndex", isSignal: true, isRequired: false, transformFunction: null }, selectedKey: { classPropertyName: "selectedKey", publicName: "selectedKey", isSignal: true, isRequired: false, transformFunction: null }, activation: { classPropertyName: "activation", publicName: "activation", isSignal: true, isRequired: false, transformFunction: null }, keepAlive: { classPropertyName: "keepAlive", publicName: "keepAlive", isSignal: true, isRequired: false, transformFunction: null }, tablistLabel: { classPropertyName: "tablistLabel", publicName: "tablistLabel", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { selectedIndex: "selectedIndexChange", selectedKey: "selectedKeyChange" }, host: { listeners: { "keydown": "onKeydown($event)", "focusout": "onFocusOut($event)" }, classAttribute: "nxe-tabs" }, queries: [{ propertyName: "tabs", predicate: NxeTabComponent, isSignal: true }], viewQueries: [{ propertyName: "list", first: true, predicate: ["list"], descendants: true, isSignal: true }], ngImport: i0, template: "<div class=\"nxe-tabs-scroller\">\r\n <div\r\n #list\r\n class=\"nxe-tabs-list\"\r\n role=\"tablist\"\r\n aria-orientation=\"horizontal\"\r\n [attr.aria-label]=\"tablistLabel() || null\">\r\n @for (tab of tabs(); track tab; let i = $index) {\r\n <ng-template #content>\r\n <span class=\"nxe-tab-label\">{{ tab.label() }}</span>\r\n @if (tab.badge() !== undefined) {\r\n <nxe-badge [value]=\"tab.badge()\" [family]=\"tab.badgeFamily()\" showZero />\r\n }\r\n </ng-template>\r\n @if (isLink(tab)) {\r\n <a\r\n class=\"nxe-tab-trigger\"\r\n role=\"tab\"\r\n [id]=\"tabId(i)\"\r\n [routerLink]=\"tab.disabled() ? null : tab.link()\"\r\n [queryParams]=\"tab.queryParams()\"\r\n [queryParamsHandling]=\"tab.queryParamsHandling()\"\r\n [class.nxe-tab-active]=\"i === activeIndex()\"\r\n [attr.aria-selected]=\"i === activeIndex()\"\r\n [attr.aria-controls]=\"hasPanel(tab) ? panelId(i) : null\"\r\n [attr.aria-disabled]=\"tab.disabled() || null\"\r\n [attr.tabindex]=\"tab.disabled() ? null : i === tabbableIndex() ? 0 : -1\"\r\n (focus)=\"focusedIndex.set(i)\">\r\n <ng-container [ngTemplateOutlet]=\"content\" />\r\n </a>\r\n } @else {\r\n <button\r\n type=\"button\"\r\n class=\"nxe-tab-trigger\"\r\n role=\"tab\"\r\n [id]=\"tabId(i)\"\r\n [disabled]=\"tab.disabled()\"\r\n [class.nxe-tab-active]=\"i === activeIndex()\"\r\n [attr.aria-selected]=\"i === activeIndex()\"\r\n [attr.aria-controls]=\"panelId(i)\"\r\n [attr.aria-disabled]=\"tab.disabled() || null\"\r\n [attr.tabindex]=\"i === tabbableIndex() ? 0 : -1\"\r\n (focus)=\"focusedIndex.set(i)\"\r\n (click)=\"select(i)\">\r\n <ng-container [ngTemplateOutlet]=\"content\" />\r\n </button>\r\n }\r\n }\r\n </div>\r\n</div>\r\n@for (tab of tabs(); track tab; let i = $index) {\r\n @if (hasPanel(tab)) {\r\n <div\r\n class=\"nxe-tab-panel\"\r\n role=\"tabpanel\"\r\n tabindex=\"0\"\r\n [id]=\"panelId(i)\"\r\n [attr.aria-labelledby]=\"tabId(i)\"\r\n [hidden]=\"i !== activeIndex()\">\r\n @if (isRendered(tab, i)) {\r\n <ng-container [ngTemplateOutlet]=\"tab.contentTemplate()\" />\r\n }\r\n </div>\r\n }\r\n}\r\n", styles: [":host{display:block;color:var(--nxe-ui-text);font-family:var(--nxe-ui-font-family)}:host .nxe-tabs-scroller{overflow-x:auto;overscroll-behavior-x:contain;scrollbar-width:thin;border-bottom:1px solid var(--nxe-ui-border)}:host .nxe-tabs-list{display:flex;gap:var(--nxe-ui-space-1);min-width:max-content}:host .nxe-tab-trigger{display:inline-flex;align-items:center;gap:var(--nxe-ui-space-2);box-sizing:border-box;min-height:var(--nxe-ui-control-min-height);margin:0 0 -1px;padding:0 var(--nxe-ui-space-3);border:0;border-bottom:2px solid transparent;background:transparent;color:var(--nxe-ui-neutral-600);font:inherit;font-weight:var(--nxe-ui-font-weight-semibold);text-decoration:none;white-space:nowrap;cursor:pointer}:host .nxe-tab-trigger:hover:not([aria-disabled=true]){color:var(--nxe-ui-text);background-color:var(--nxe-ui-surface-alt)}:host .nxe-tab-trigger:focus-visible{outline:2px solid var(--nxe-ui-focus-ring-color);outline-offset:-2px}:host .nxe-tab-trigger.nxe-tab-active{color:var(--nxe-ui-accent);border-bottom-color:var(--nxe-ui-accent)}:host .nxe-tab-trigger[aria-disabled=true]{opacity:.5;cursor:not-allowed}:host .nxe-tab-panel{padding-top:var(--nxe-ui-space-4)}:host .nxe-tab-panel[hidden]{display:none}:host .nxe-tab-panel:focus-visible{outline:2px solid var(--nxe-ui-focus-ring-color);outline-offset:2px}\n"], 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", "browserUrl", "routerLink"] }, { kind: "component", type: NxeBadgeComponent, selector: "nxe-badge", inputs: ["value", "max", "dot", "showZero", "family", "overlay", "label"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
372
+ }
373
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeTabsComponent, decorators: [{
374
+ type: Component,
375
+ args: [{ selector: 'nxe-tabs', imports: [NgTemplateOutlet, RouterLink, NxeBadgeComponent], host: {
376
+ class: 'nxe-tabs',
377
+ '(keydown)': 'onKeydown($event)',
378
+ '(focusout)': 'onFocusOut($event)',
379
+ }, changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"nxe-tabs-scroller\">\r\n <div\r\n #list\r\n class=\"nxe-tabs-list\"\r\n role=\"tablist\"\r\n aria-orientation=\"horizontal\"\r\n [attr.aria-label]=\"tablistLabel() || null\">\r\n @for (tab of tabs(); track tab; let i = $index) {\r\n <ng-template #content>\r\n <span class=\"nxe-tab-label\">{{ tab.label() }}</span>\r\n @if (tab.badge() !== undefined) {\r\n <nxe-badge [value]=\"tab.badge()\" [family]=\"tab.badgeFamily()\" showZero />\r\n }\r\n </ng-template>\r\n @if (isLink(tab)) {\r\n <a\r\n class=\"nxe-tab-trigger\"\r\n role=\"tab\"\r\n [id]=\"tabId(i)\"\r\n [routerLink]=\"tab.disabled() ? null : tab.link()\"\r\n [queryParams]=\"tab.queryParams()\"\r\n [queryParamsHandling]=\"tab.queryParamsHandling()\"\r\n [class.nxe-tab-active]=\"i === activeIndex()\"\r\n [attr.aria-selected]=\"i === activeIndex()\"\r\n [attr.aria-controls]=\"hasPanel(tab) ? panelId(i) : null\"\r\n [attr.aria-disabled]=\"tab.disabled() || null\"\r\n [attr.tabindex]=\"tab.disabled() ? null : i === tabbableIndex() ? 0 : -1\"\r\n (focus)=\"focusedIndex.set(i)\">\r\n <ng-container [ngTemplateOutlet]=\"content\" />\r\n </a>\r\n } @else {\r\n <button\r\n type=\"button\"\r\n class=\"nxe-tab-trigger\"\r\n role=\"tab\"\r\n [id]=\"tabId(i)\"\r\n [disabled]=\"tab.disabled()\"\r\n [class.nxe-tab-active]=\"i === activeIndex()\"\r\n [attr.aria-selected]=\"i === activeIndex()\"\r\n [attr.aria-controls]=\"panelId(i)\"\r\n [attr.aria-disabled]=\"tab.disabled() || null\"\r\n [attr.tabindex]=\"i === tabbableIndex() ? 0 : -1\"\r\n (focus)=\"focusedIndex.set(i)\"\r\n (click)=\"select(i)\">\r\n <ng-container [ngTemplateOutlet]=\"content\" />\r\n </button>\r\n }\r\n }\r\n </div>\r\n</div>\r\n@for (tab of tabs(); track tab; let i = $index) {\r\n @if (hasPanel(tab)) {\r\n <div\r\n class=\"nxe-tab-panel\"\r\n role=\"tabpanel\"\r\n tabindex=\"0\"\r\n [id]=\"panelId(i)\"\r\n [attr.aria-labelledby]=\"tabId(i)\"\r\n [hidden]=\"i !== activeIndex()\">\r\n @if (isRendered(tab, i)) {\r\n <ng-container [ngTemplateOutlet]=\"tab.contentTemplate()\" />\r\n }\r\n </div>\r\n }\r\n}\r\n", styles: [":host{display:block;color:var(--nxe-ui-text);font-family:var(--nxe-ui-font-family)}:host .nxe-tabs-scroller{overflow-x:auto;overscroll-behavior-x:contain;scrollbar-width:thin;border-bottom:1px solid var(--nxe-ui-border)}:host .nxe-tabs-list{display:flex;gap:var(--nxe-ui-space-1);min-width:max-content}:host .nxe-tab-trigger{display:inline-flex;align-items:center;gap:var(--nxe-ui-space-2);box-sizing:border-box;min-height:var(--nxe-ui-control-min-height);margin:0 0 -1px;padding:0 var(--nxe-ui-space-3);border:0;border-bottom:2px solid transparent;background:transparent;color:var(--nxe-ui-neutral-600);font:inherit;font-weight:var(--nxe-ui-font-weight-semibold);text-decoration:none;white-space:nowrap;cursor:pointer}:host .nxe-tab-trigger:hover:not([aria-disabled=true]){color:var(--nxe-ui-text);background-color:var(--nxe-ui-surface-alt)}:host .nxe-tab-trigger:focus-visible{outline:2px solid var(--nxe-ui-focus-ring-color);outline-offset:-2px}:host .nxe-tab-trigger.nxe-tab-active{color:var(--nxe-ui-accent);border-bottom-color:var(--nxe-ui-accent)}:host .nxe-tab-trigger[aria-disabled=true]{opacity:.5;cursor:not-allowed}:host .nxe-tab-panel{padding-top:var(--nxe-ui-space-4)}:host .nxe-tab-panel[hidden]{display:none}:host .nxe-tab-panel:focus-visible{outline:2px solid var(--nxe-ui-focus-ring-color);outline-offset:2px}\n"] }]
380
+ }], ctorParameters: () => [], propDecorators: { selectedIndex: [{ type: i0.Input, args: [{ isSignal: true, alias: "selectedIndex", required: false }] }, { type: i0.Output, args: ["selectedIndexChange"] }], selectedKey: [{ type: i0.Input, args: [{ isSignal: true, alias: "selectedKey", required: false }] }, { type: i0.Output, args: ["selectedKeyChange"] }], activation: [{ type: i0.Input, args: [{ isSignal: true, alias: "activation", required: false }] }], keepAlive: [{ type: i0.Input, args: [{ isSignal: true, alias: "keepAlive", required: false }] }], tablistLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "tablistLabel", required: false }] }], tabs: [{ type: i0.ContentChildren, args: [i0.forwardRef(() => NxeTabComponent), { isSignal: true }] }], list: [{ type: i0.ViewChild, args: ['list', { isSignal: true }] }] } });
381
+
382
+ /**
383
+ * Marks the `<ng-template>` that holds a step's lazy content. The template is stamped out the first
384
+ * time the step is activated, not before, so a heavy step (a document upload, a review table) costs
385
+ * nothing until the user gets there:
386
+ *
387
+ * ```html
388
+ * <nxe-step label="Documents">
389
+ * <ng-template nxeStepContent><app-kyb-documents /></ng-template>
390
+ * </nxe-step>
391
+ * ```
392
+ *
393
+ * Whether it is destroyed again when the step is left is `keepAlive` on the step or the stepper.
394
+ */
395
+ class NxeStepContentDirective {
396
+ /** The template to render. */
397
+ template = inject(TemplateRef);
398
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeStepContentDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
399
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.1.5", type: NxeStepContentDirective, isStandalone: true, selector: "ng-template[nxeStepContent]", ngImport: i0 });
400
+ }
401
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeStepContentDirective, decorators: [{
402
+ type: Directive,
403
+ args: [{
404
+ selector: 'ng-template[nxeStepContent]',
405
+ }]
406
+ }] });
407
+
408
+ const NO_CONTROL = { hasControl: false, valid: true, pristine: true };
409
+ function snapshot(control) {
410
+ return { hasControl: true, valid: control.valid, pristine: control.pristine };
411
+ }
412
+ /**
413
+ * One step of an {@link NxeStepperComponent}. It draws nothing itself: the stepper renders the header
414
+ * and the panel from the inputs and content declared here.
415
+ *
416
+ * ```html
417
+ * <nxe-step label="Account" [stepControl]="account">
418
+ * <nxe-form-field label="Email"><nxe-text-box formControlName="email" /></nxe-form-field>
419
+ * </nxe-step>
420
+ * <nxe-step label="Documents" optional>
421
+ * <ng-template nxeStepContent><app-documents /></ng-template>
422
+ * </nxe-step>
423
+ * ```
424
+ *
425
+ * **Completed and error.** Left alone a step is completed once the user has been through it and its
426
+ * `stepControl` is valid, and shows the error state once they have been through it and the control is
427
+ * invalid. `completed` and `hasError` replace that with your own answer (a server check, say).
428
+ */
429
+ class NxeStepComponent {
430
+ /** The step's title in the header. */
431
+ label = input('', /* @ts-ignore */
432
+ ...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
433
+ /** A line under the title. */
434
+ description = input('', /* @ts-ignore */
435
+ ...(ngDevMode ? [{ debugName: "description" }] : /* istanbul ignore next */ []));
436
+ /**
437
+ * The form (a `FormGroup`, `FormArray` or `FormControl`) that has to be valid before a linear
438
+ * stepper lets the user past this step.
439
+ */
440
+ stepControl = input(undefined, /* @ts-ignore */
441
+ ...(ngDevMode ? [{ debugName: "stepControl" }] : /* istanbul ignore next */ []));
442
+ /** Overrides the computed completed state. Unset, it is "visited and `stepControl` valid". */
443
+ completed = input(undefined, { ...(ngDevMode ? { debugName: "completed" } : /* istanbul ignore next */ {}), transform: nxeOptionalBoolean });
444
+ /** Overrides the computed error state. Unset, it is "visited and `stepControl` invalid". */
445
+ hasError = input(undefined, { ...(ngDevMode ? { debugName: "hasError" } : /* istanbul ignore next */ {}), transform: nxeOptionalBoolean });
446
+ /** The step may be skipped: it does not hold a linear stepper back while its form is untouched. */
447
+ optional = input(false, { ...(ngDevMode ? { debugName: "optional" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
448
+ /** A completed step can be reopened. `false` locks it once the user has moved on. */
449
+ editable = input(true, { ...(ngDevMode ? { debugName: "editable" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
450
+ /** Overrides the stepper's `keepAlive` for this step. */
451
+ keepAlive = input(undefined, { ...(ngDevMode ? { debugName: "keepAlive" } : /* istanbul ignore next */ {}), transform: nxeOptionalBoolean });
452
+ lazy = contentChild(NxeStepContentDirective, /* @ts-ignore */
453
+ ...(ngDevMode ? [{ debugName: "lazy" }] : /* istanbul ignore next */ []));
454
+ implicit = viewChild.required('implicit', /* @ts-ignore */
455
+ ...(ngDevMode ? [{ debugName: "implicit" }] : /* istanbul ignore next */ []));
456
+ /** True when the content was declared in an `nxeStepContent` template. */
457
+ isLazy = computed(() => this.lazy() !== undefined, /* @ts-ignore */
458
+ ...(ngDevMode ? [{ debugName: "isLazy" }] : /* istanbul ignore next */ []));
459
+ /** The template the stepper stamps into the panel. */
460
+ contentTemplate = computed(() => this.lazy()?.template ?? this.implicit(), /* @ts-ignore */
461
+ ...(ngDevMode ? [{ debugName: "contentTemplate" }] : /* istanbul ignore next */ []));
462
+ /** The control's validity, kept in step with it through its `events` stream. */
463
+ controlState = toSignal(toObservable(this.stepControl).pipe(switchMap((control) => control ? control.events.pipe(startWith(null), map(() => snapshot(control))) : of(NO_CONTROL))), { initialValue: NO_CONTROL });
464
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeStepComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
465
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.2.0", version: "22.1.5", type: NxeStepComponent, isStandalone: true, selector: "nxe-step", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, description: { classPropertyName: "description", publicName: "description", isSignal: true, isRequired: false, transformFunction: null }, stepControl: { classPropertyName: "stepControl", publicName: "stepControl", isSignal: true, isRequired: false, transformFunction: null }, completed: { classPropertyName: "completed", publicName: "completed", isSignal: true, isRequired: false, transformFunction: null }, hasError: { classPropertyName: "hasError", publicName: "hasError", isSignal: true, isRequired: false, transformFunction: null }, optional: { classPropertyName: "optional", publicName: "optional", isSignal: true, isRequired: false, transformFunction: null }, editable: { classPropertyName: "editable", publicName: "editable", isSignal: true, isRequired: false, transformFunction: null }, keepAlive: { classPropertyName: "keepAlive", publicName: "keepAlive", isSignal: true, isRequired: false, transformFunction: null } }, host: { classAttribute: "nxe-step" }, queries: [{ propertyName: "lazy", first: true, predicate: NxeStepContentDirective, descendants: true, isSignal: true }], viewQueries: [{ propertyName: "implicit", first: true, predicate: ["implicit"], descendants: true, isSignal: true }], ngImport: i0, template: '<ng-template #implicit><ng-content /></ng-template>', isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush });
466
+ }
467
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeStepComponent, decorators: [{
468
+ type: Component,
469
+ args: [{
470
+ selector: 'nxe-step',
471
+ template: '<ng-template #implicit><ng-content /></ng-template>',
472
+ host: { class: 'nxe-step' },
473
+ changeDetection: ChangeDetectionStrategy.OnPush,
474
+ }]
475
+ }], propDecorators: { label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], description: [{ type: i0.Input, args: [{ isSignal: true, alias: "description", required: false }] }], stepControl: [{ type: i0.Input, args: [{ isSignal: true, alias: "stepControl", required: false }] }], completed: [{ type: i0.Input, args: [{ isSignal: true, alias: "completed", required: false }] }], hasError: [{ type: i0.Input, args: [{ isSignal: true, alias: "hasError", required: false }] }], optional: [{ type: i0.Input, args: [{ isSignal: true, alias: "optional", required: false }] }], editable: [{ type: i0.Input, args: [{ isSignal: true, alias: "editable", required: false }] }], keepAlive: [{ type: i0.Input, args: [{ isSignal: true, alias: "keepAlive", required: false }] }], lazy: [{ type: i0.ContentChild, args: [i0.forwardRef(() => NxeStepContentDirective), { isSignal: true }] }], implicit: [{ type: i0.ViewChild, args: ['implicit', { isSignal: true }] }] } });
476
+
477
+ let nextStepperId = 0;
478
+ /**
479
+ * `nxe-stepper` is a wizard: an ordered list of `nxe-step`s, one open at a time, with a header that
480
+ * shows where the user is and what is done.
481
+ *
482
+ * ```html
483
+ * <nxe-stepper linear [(selectedIndex)]="index" #wizard>
484
+ * <nxe-step label="Account" [stepControl]="account">...</nxe-step>
485
+ * <nxe-step label="Company" [stepControl]="company">...</nxe-step>
486
+ * <nxe-step label="Documents" optional>
487
+ * <ng-template nxeStepContent><app-documents /></ng-template>
488
+ * </nxe-step>
489
+ * <nxe-step label="Review">
490
+ * <nxe-button nxeStepperPrevious text="Back" />
491
+ * <nxe-button (onClick)="submit()" text="Submit" type="default" />
492
+ * </nxe-step>
493
+ * </nxe-stepper>
494
+ * ```
495
+ *
496
+ * **Linear and non-linear.** Non-linear (default) lets the user open any step. `linear` makes each step
497
+ * hold the user back until it is passable: its `stepControl` is valid (or `completed` says so). Pressing
498
+ * Next on a step that is not passable stays put, marks the step's controls as touched so `nxe-form-field`
499
+ * shows their errors, announces why, and emits `stepBlocked`. Headers of steps further on are not
500
+ * navigable until every step before them is complete. A programmatic `selectedIndex` is not checked.
501
+ *
502
+ * **API.** `selectedIndex` is a two-way model. `next()`, `previous()`, `select(i)` and `reset()` move it
503
+ * and return whether they did; `nxeStepperNext` and `nxeStepperPrevious` are directives that call
504
+ * `next()` and `previous()` from a button inside (or, given the stepper, outside) the steps.
505
+ *
506
+ * **Layout.** Horizontal puts the headers in a row over the open step; vertical stacks each header over its
507
+ * own panel. With `responsive` (default) a stepper narrower than `collapseBelow` px is vertical.
508
+ *
509
+ * **Accessibility.** The steps are an ordered list; the open step's header carries `aria-current="step"`.
510
+ * A header is a button while its step can be opened and plain text when it cannot. Arrow keys, Home and
511
+ * End move focus between headers. A polite live region announces each change of step ("Step 2 of 4:
512
+ * Company") and a blocked Next.
513
+ */
514
+ class NxeStepperComponent {
515
+ /** Zero-based index of the open step. Two-way. */
516
+ selectedIndex = model(0, /* @ts-ignore */
517
+ ...(ngDevMode ? [{ debugName: "selectedIndex" }] : /* istanbul ignore next */ []));
518
+ /** Each step must be passable before the next opens. */
519
+ linear = input(false, { ...(ngDevMode ? { debugName: "linear" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
520
+ /** `horizontal` (default) or `vertical`. */
521
+ orientation = input('horizontal', /* @ts-ignore */
522
+ ...(ngDevMode ? [{ debugName: "orientation" }] : /* istanbul ignore next */ []));
523
+ /** A horizontal stepper turns vertical when its host is narrower than {@link collapseBelow}. */
524
+ responsive = input(true, { ...(ngDevMode ? { debugName: "responsive" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
525
+ /** Host width, in px, under which a responsive horizontal stepper turns vertical. */
526
+ collapseBelow = input(560, { ...(ngDevMode ? { debugName: "collapseBelow" } : /* istanbul ignore next */ {}), transform: (value) => numberAttribute(value, 560) });
527
+ /** Keep a step's lazy content after leaving it. A step's own `keepAlive` overrides. */
528
+ keepAlive = input(true, { ...(ngDevMode ? { debugName: "keepAlive" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
529
+ /** Accessible name of the list of steps; the active language's "Steps" when unset. */
530
+ stepsLabel = input(undefined, /* @ts-ignore */
531
+ ...(ngDevMode ? [{ debugName: "stepsLabel" }] : /* istanbul ignore next */ []));
532
+ /** The stepper moved to another step. */
533
+ selectionChange = output();
534
+ /** A linear Next was refused; the value is the index of the step that is not complete. */
535
+ stepBlocked = output();
536
+ steps = contentChildren(NxeStepComponent, /* @ts-ignore */
537
+ ...(ngDevMode ? [{ debugName: "steps" }] : /* istanbul ignore next */ []));
538
+ messages = inject(NxeMessageService);
539
+ host = inject(ElementRef);
540
+ destroyRef = inject(DestroyRef);
541
+ list = viewChild.required('list', /* @ts-ignore */
542
+ ...(ngDevMode ? [{ debugName: "list" }] : /* istanbul ignore next */ []));
543
+ uid = `nxe-stepper-${nextStepperId++}`;
544
+ /** Whether the host is under {@link collapseBelow}; set from a ResizeObserver. */
545
+ narrow = signal(false, /* @ts-ignore */
546
+ ...(ngDevMode ? [{ debugName: "narrow" }] : /* istanbul ignore next */ []));
547
+ /** The orientation actually drawn. */
548
+ layout = computed(() => this.orientation() === 'vertical' || (this.responsive() && this.narrow()) ? 'vertical' : 'horizontal', /* @ts-ignore */
549
+ ...(ngDevMode ? [{ debugName: "layout" }] : /* istanbul ignore next */ []));
550
+ /** Index of the open step, kept inside the steps; -1 with no steps. */
551
+ activeIndex = computed(() => {
552
+ const count = this.steps().length;
553
+ return Math.min(Math.max(this.selectedIndex(), 0), count - 1);
554
+ }, /* @ts-ignore */
555
+ ...(ngDevMode ? [{ debugName: "activeIndex" }] : /* istanbul ignore next */ []));
556
+ activeStep = computed(() => this.steps()[this.activeIndex()], /* @ts-ignore */
557
+ ...(ngDevMode ? [{ debugName: "activeStep" }] : /* istanbul ignore next */ []));
558
+ /** Steps that have been open at some point, the open one included. Resets with the stepper. */
559
+ visited = linkedSignal({ ...(ngDevMode ? { debugName: "visited" } : /* istanbul ignore next */ {}), source: this.activeStep,
560
+ computation: (step, previous) => {
561
+ const set = new Set(previous?.value);
562
+ if (step) {
563
+ set.add(step);
564
+ }
565
+ return set;
566
+ } });
567
+ /** Steps the user tried to leave while they were not passable. */
568
+ attempted = signal(new Set(), /* @ts-ignore */
569
+ ...(ngDevMode ? [{ debugName: "attempted" }] : /* istanbul ignore next */ []));
570
+ /** The live region's text. Writable so a blocked Next can announce without moving. */
571
+ announcement = linkedSignal({ ...(ngDevMode ? { debugName: "announcement" } : /* istanbul ignore next */ {}), source: this.activeIndex,
572
+ computation: (index, previous) => (previous === undefined ? '' : this.stepOf(index)) });
573
+ blockedToggle = false;
574
+ /** True on the first step. */
575
+ isFirst = computed(() => this.activeIndex() <= 0, /* @ts-ignore */
576
+ ...(ngDevMode ? [{ debugName: "isFirst" }] : /* istanbul ignore next */ []));
577
+ /** True on the last step. */
578
+ isLast = computed(() => this.activeIndex() >= this.steps().length - 1, /* @ts-ignore */
579
+ ...(ngDevMode ? [{ debugName: "isLast" }] : /* istanbul ignore next */ []));
580
+ /** Whether Next would move: there is a step after this one and, when linear, this one is passable. */
581
+ canNext = computed(() => {
582
+ const step = this.activeStep();
583
+ return !!step && !this.isLast() && (!this.linear() || this.passable(step, true));
584
+ }, /* @ts-ignore */
585
+ ...(ngDevMode ? [{ debugName: "canNext" }] : /* istanbul ignore next */ []));
586
+ views = computed(() => {
587
+ const steps = this.steps();
588
+ const active = this.activeIndex();
589
+ return steps.map((step, index) => {
590
+ const isActive = index === active;
591
+ const error = this.hasError(step);
592
+ const completed = this.isCompleted(step);
593
+ return {
594
+ step,
595
+ index,
596
+ headerId: `${this.uid}-header-${index}`,
597
+ panelId: `${this.uid}-panel-${index}`,
598
+ active: isActive,
599
+ navigable: this.canOpen(index),
600
+ marker: error ? 'error' : completed && !isActive ? 'completed' : 'number',
601
+ completed,
602
+ error,
603
+ rendered: isActive || ((step.keepAlive() ?? this.keepAlive()) && this.visited().has(step)),
604
+ };
605
+ });
606
+ }, /* @ts-ignore */
607
+ ...(ngDevMode ? [{ debugName: "views" }] : /* istanbul ignore next */ []));
608
+ constructor() {
609
+ afterNextRender(() => {
610
+ const el = this.host.nativeElement;
611
+ this.narrow.set(el.clientWidth > 0 && el.clientWidth < this.collapseBelow());
612
+ if (typeof ResizeObserver === 'undefined') {
613
+ return;
614
+ }
615
+ const observer = new ResizeObserver(() => {
616
+ const width = el.clientWidth;
617
+ // A hidden host reports 0; that is not narrow, and it must not flip the layout.
618
+ if (width > 0) {
619
+ this.narrow.set(width < this.collapseBelow());
620
+ }
621
+ });
622
+ observer.observe(el);
623
+ this.destroyRef.onDestroy(() => observer.disconnect());
624
+ });
625
+ }
626
+ /** Moves to the next step. Linear and not passable: stays, shows the step's errors, returns false. */
627
+ next() {
628
+ const step = this.activeStep();
629
+ if (!step || this.isLast()) {
630
+ return false;
631
+ }
632
+ if (this.linear() && !this.passable(step, true)) {
633
+ this.block(step);
634
+ return false;
635
+ }
636
+ return this.go(this.activeIndex() + 1);
637
+ }
638
+ /** Moves to the previous step, which a linear stepper allows freely. False on the first step. */
639
+ previous() {
640
+ return this.isFirst() ? false : this.go(this.activeIndex() - 1);
641
+ }
642
+ /** Opens step `index` if it can be opened from here; returns whether it did. */
643
+ select(index) {
644
+ if (index === this.activeIndex()) {
645
+ return false;
646
+ }
647
+ if (!this.canOpen(index)) {
648
+ const step = this.steps()[index];
649
+ if (step && this.linear() && index > this.activeIndex()) {
650
+ this.block(this.firstBlocking(index) ?? step);
651
+ }
652
+ return false;
653
+ }
654
+ return this.go(index);
655
+ }
656
+ /**
657
+ * Back to the first step with every step unvisited. `controls` (default `true`) also resets each
658
+ * step's `stepControl`, so the wizard starts over empty.
659
+ */
660
+ reset(options = {}) {
661
+ const first = this.steps()[0];
662
+ this.attempted.set(new Set());
663
+ this.visited.set(new Set(first ? [first] : []));
664
+ if (options.controls !== false) {
665
+ this.steps().forEach((step) => step.stepControl()?.reset());
666
+ }
667
+ const previousIndex = this.activeIndex();
668
+ this.selectedIndex.set(0);
669
+ if (previousIndex !== 0) {
670
+ this.selectionChange.emit({ previousIndex, selectedIndex: 0 });
671
+ }
672
+ }
673
+ go(index) {
674
+ const previousIndex = this.activeIndex();
675
+ this.selectedIndex.set(index);
676
+ this.selectionChange.emit({ previousIndex, selectedIndex: index });
677
+ return true;
678
+ }
679
+ /** Surfaces why a linear stepper stayed: errors on the fields, a message for assistive tech. */
680
+ block(step) {
681
+ step.stepControl()?.markAllAsTouched();
682
+ this.attempted.update((set) => new Set(set).add(step));
683
+ const index = this.steps().indexOf(step);
684
+ this.blockedToggle = !this.blockedToggle;
685
+ // The trailing no-break space makes a repeat of the same message a different string.
686
+ this.announcement.set(this.messages.t('stepper.blocked', index + 1) + (this.blockedToggle ? ' ' : ''));
687
+ this.stepBlocked.emit(index);
688
+ }
689
+ stepOf(index) {
690
+ const step = this.steps()[index];
691
+ return step ? this.messages.t('stepper.stepOf', index + 1, this.steps().length, step.label()) : '';
692
+ }
693
+ interacted(step) {
694
+ return (this.visited().has(step) && step !== this.activeStep()) || this.attempted().has(step);
695
+ }
696
+ /** Whether a linear stepper lets the user leave `step` forward. */
697
+ passable(step, isActive) {
698
+ const explicit = step.completed();
699
+ if (explicit !== undefined) {
700
+ return explicit;
701
+ }
702
+ const control = step.controlState();
703
+ if (control.hasControl) {
704
+ return control.valid ? isActive || this.interacted(step) : step.optional() && control.pristine;
705
+ }
706
+ return isActive || step.optional() || this.interacted(step);
707
+ }
708
+ isCompleted(step) {
709
+ const explicit = step.completed();
710
+ if (explicit !== undefined) {
711
+ return explicit;
712
+ }
713
+ return this.interacted(step) && step.controlState().valid;
714
+ }
715
+ hasError(step) {
716
+ const explicit = step.hasError();
717
+ if (explicit !== undefined) {
718
+ return explicit;
719
+ }
720
+ const control = step.controlState();
721
+ return this.interacted(step) && control.hasControl && !control.valid && !(step.optional() && control.pristine);
722
+ }
723
+ /** The first step between here and `target` that holds a linear stepper back. */
724
+ firstBlocking(target) {
725
+ const steps = this.steps();
726
+ const active = this.activeIndex();
727
+ for (let k = active; k < target; k++) {
728
+ if (!this.passable(steps[k], k === active)) {
729
+ return steps[k];
730
+ }
731
+ }
732
+ return undefined;
733
+ }
734
+ /** Whether the user can open step `index` from the open one. */
735
+ canOpen(index) {
736
+ const steps = this.steps();
737
+ const active = this.activeIndex();
738
+ const step = steps[index];
739
+ if (!step) {
740
+ return false;
741
+ }
742
+ if (index === active) {
743
+ return true;
744
+ }
745
+ if (index < active) {
746
+ // A locked step stays locked once it is done.
747
+ return step.editable() || !this.isCompleted(step);
748
+ }
749
+ return !this.linear() || this.firstBlocking(index) === undefined;
750
+ }
751
+ headerLabel(view) {
752
+ return view.step.label();
753
+ }
754
+ stateText(view) {
755
+ if (view.error) {
756
+ return this.messages.t('stepper.error');
757
+ }
758
+ return view.completed && !view.active ? this.messages.t('stepper.completed') : '';
759
+ }
760
+ optionalText() {
761
+ return this.messages.t('stepper.optional');
762
+ }
763
+ listLabel() {
764
+ return this.stepsLabel() ?? this.messages.t('stepper.listLabel');
765
+ }
766
+ open(view) {
767
+ this.select(view.index);
768
+ }
769
+ onKeydown(event) {
770
+ const headers = Array.from(this.list().nativeElement.querySelectorAll('button.nxe-step-header'));
771
+ const current = headers.indexOf(event.target);
772
+ if (current < 0) {
773
+ return;
774
+ }
775
+ const rtl = getComputedStyle(this.list().nativeElement).direction === 'rtl';
776
+ const horizontal = this.layout() === 'horizontal';
777
+ let target;
778
+ switch (event.key) {
779
+ case 'ArrowRight':
780
+ target = current + (horizontal ? (rtl ? -1 : 1) : 0);
781
+ break;
782
+ case 'ArrowLeft':
783
+ target = current + (horizontal ? (rtl ? 1 : -1) : 0);
784
+ break;
785
+ case 'ArrowDown':
786
+ target = current + 1;
787
+ break;
788
+ case 'ArrowUp':
789
+ target = current - 1;
790
+ break;
791
+ case 'Home':
792
+ target = 0;
793
+ break;
794
+ case 'End':
795
+ target = headers.length - 1;
796
+ break;
797
+ default:
798
+ return;
799
+ }
800
+ event.preventDefault();
801
+ if (target !== current) {
802
+ headers[(target + headers.length) % headers.length]?.focus();
803
+ }
804
+ }
805
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeStepperComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
806
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.5", type: NxeStepperComponent, isStandalone: true, selector: "nxe-stepper", inputs: { selectedIndex: { classPropertyName: "selectedIndex", publicName: "selectedIndex", isSignal: true, isRequired: false, transformFunction: null }, linear: { classPropertyName: "linear", publicName: "linear", isSignal: true, isRequired: false, transformFunction: null }, orientation: { classPropertyName: "orientation", publicName: "orientation", isSignal: true, isRequired: false, transformFunction: null }, responsive: { classPropertyName: "responsive", publicName: "responsive", isSignal: true, isRequired: false, transformFunction: null }, collapseBelow: { classPropertyName: "collapseBelow", publicName: "collapseBelow", isSignal: true, isRequired: false, transformFunction: null }, keepAlive: { classPropertyName: "keepAlive", publicName: "keepAlive", isSignal: true, isRequired: false, transformFunction: null }, stepsLabel: { classPropertyName: "stepsLabel", publicName: "stepsLabel", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { selectedIndex: "selectedIndexChange", selectionChange: "selectionChange", stepBlocked: "stepBlocked" }, host: { listeners: { "keydown": "onKeydown($event)" }, properties: { "class.nxe-stepper-vertical": "layout() === 'vertical'", "class.nxe-stepper-horizontal": "layout() === 'horizontal'" }, classAttribute: "nxe-stepper" }, queries: [{ propertyName: "steps", predicate: NxeStepComponent, isSignal: true }], viewQueries: [{ propertyName: "list", first: true, predicate: ["list"], descendants: true, isSignal: true }], ngImport: i0, template: "<ol #list class=\"nxe-stepper-list\" [attr.aria-label]=\"listLabel()\">\r\n @for (view of views(); track view.step; let last = $last) {\r\n <li\r\n class=\"nxe-step-item\"\r\n [class.nxe-step-active]=\"view.active\"\r\n [class.nxe-step-completed]=\"view.completed\"\r\n [class.nxe-step-error]=\"view.error\"\r\n [class.nxe-step-optional]=\"view.step.optional()\">\r\n <ng-template #headerBody>\r\n <span class=\"nxe-step-marker\" aria-hidden=\"true\">\r\n @switch (view.marker) {\r\n @case ('completed') {\r\n <svg viewBox=\"0 0 16 16\" width=\"14\" height=\"14\" focusable=\"false\">\r\n <path d=\"M3 8.5l3.2 3.2L13 4.8\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" />\r\n </svg>\r\n }\r\n @case ('error') {\r\n <svg viewBox=\"0 0 16 16\" width=\"14\" height=\"14\" focusable=\"false\">\r\n <path d=\"M8 3.5v5.2M8 11.6v.1\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" />\r\n </svg>\r\n }\r\n @default {\r\n {{ view.index + 1 }}\r\n }\r\n }\r\n </span>\r\n <span class=\"nxe-step-text\">\r\n <span class=\"nxe-step-label\">{{ headerLabel(view) }}</span>\r\n @if (view.error) {\r\n <span class=\"nxe-step-sub nxe-step-sub-error\">{{ stateText(view) }}</span>\r\n } @else if (view.step.description()) {\r\n <span class=\"nxe-step-sub\">{{ view.step.description() }}</span>\r\n }\r\n @if (view.step.optional()) {\r\n <span class=\"nxe-step-sub\">{{ optionalText() }}</span>\r\n }\r\n @if (!view.error && stateText(view)) {\r\n <span class=\"nxe-step-hidden\">{{ stateText(view) }}</span>\r\n }\r\n </span>\r\n </ng-template>\r\n @if (view.navigable) {\r\n <button\r\n type=\"button\"\r\n class=\"nxe-step-header\"\r\n [id]=\"view.headerId\"\r\n [attr.aria-current]=\"view.active ? 'step' : null\"\r\n [attr.aria-controls]=\"view.panelId\"\r\n (click)=\"open(view)\">\r\n <ng-container [ngTemplateOutlet]=\"headerBody\" />\r\n </button>\r\n } @else {\r\n <div class=\"nxe-step-header nxe-step-header-static\" [id]=\"view.headerId\">\r\n <ng-container [ngTemplateOutlet]=\"headerBody\" />\r\n </div>\r\n }\r\n @if (!last && layout() === 'horizontal') {\r\n <span class=\"nxe-step-connector\" aria-hidden=\"true\"></span>\r\n }\r\n @if (layout() === 'vertical') {\r\n <ng-container [ngTemplateOutlet]=\"panel\" [ngTemplateOutletContext]=\"{ $implicit: view }\" />\r\n }\r\n </li>\r\n }\r\n</ol>\r\n@if (layout() === 'horizontal') {\r\n @for (view of views(); track view.step) {\r\n <ng-container [ngTemplateOutlet]=\"panel\" [ngTemplateOutletContext]=\"{ $implicit: view }\" />\r\n }\r\n}\r\n<ng-template #panel let-view>\r\n <div class=\"nxe-step-panel\" role=\"group\" [id]=\"view.panelId\" [attr.aria-labelledby]=\"view.headerId\" [hidden]=\"!view.active\">\r\n @if (view.rendered) {\r\n <ng-container [ngTemplateOutlet]=\"view.step.contentTemplate()\" />\r\n }\r\n </div>\r\n</ng-template>\r\n<div class=\"nxe-step-hidden\" role=\"status\" aria-live=\"polite\" aria-atomic=\"true\">{{ announcement() }}</div>\r\n", styles: ["nxe-stepper.nxe-stepper{--nxe-step-marker-size: calc(var(--nxe-ui-space-5) + var(--nxe-ui-space-1));display:block;position:relative;color:var(--nxe-ui-text);font-family:var(--nxe-ui-font-family)}nxe-stepper.nxe-stepper .nxe-step-hidden{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip-path:inset(50%);white-space:nowrap;border:0}nxe-stepper.nxe-stepper .nxe-stepper-list{display:flex;margin:0;padding:0;list-style:none}nxe-stepper.nxe-stepper .nxe-step-item{display:flex;align-items:center;gap:var(--nxe-ui-space-2);min-width:0}nxe-stepper.nxe-stepper .nxe-step-header{display:inline-flex;align-items:center;gap:var(--nxe-ui-space-2);box-sizing:border-box;min-height:var(--nxe-ui-control-min-height);padding:var(--nxe-ui-space-1) var(--nxe-ui-space-2);border:0;border-radius:var(--nxe-ui-radius-sm);background:transparent;color:inherit;font:inherit;text-align:start}nxe-stepper.nxe-stepper button.nxe-step-header{cursor:pointer}nxe-stepper.nxe-stepper button.nxe-step-header:hover{background-color:var(--nxe-ui-surface-alt)}nxe-stepper.nxe-stepper button.nxe-step-header:focus-visible{outline:2px solid var(--nxe-ui-focus-ring-color);outline-offset:-2px}nxe-stepper.nxe-stepper .nxe-step-header-static{cursor:default}nxe-stepper.nxe-stepper .nxe-step-marker{display:inline-flex;flex:0 0 auto;align-items:center;justify-content:center;box-sizing:border-box;width:var(--nxe-step-marker-size);height:var(--nxe-step-marker-size);border:1px solid var(--nxe-ui-status-neutral-border);border-radius:var(--nxe-ui-radius-pill);background:var(--nxe-ui-status-neutral-bg);color:var(--nxe-ui-status-neutral-fg);font-size:var(--nxe-ui-font-size-sm);font-weight:var(--nxe-ui-font-weight-semibold);line-height:1}nxe-stepper.nxe-stepper .nxe-step-text{display:inline-flex;flex-direction:column;min-width:0}nxe-stepper.nxe-stepper .nxe-step-label{color:var(--nxe-ui-neutral-600);font-weight:var(--nxe-ui-font-weight-semibold)}nxe-stepper.nxe-stepper .nxe-step-sub{color:var(--nxe-ui-text-muted);font-size:var(--nxe-ui-font-size-xs)}nxe-stepper.nxe-stepper .nxe-step-sub-error{color:var(--nxe-ui-status-danger-fg)}nxe-stepper.nxe-stepper .nxe-step-active .nxe-step-marker{border-color:var(--nxe-ui-accent);background:var(--nxe-ui-accent);color:var(--nxe-ui-accent-contrast)}nxe-stepper.nxe-stepper .nxe-step-active .nxe-step-label{color:var(--nxe-ui-text)}nxe-stepper.nxe-stepper .nxe-step-completed:not(.nxe-step-active) .nxe-step-marker{border-color:var(--nxe-ui-status-success-border);background:var(--nxe-ui-status-success-bg);color:var(--nxe-ui-status-success-fg)}nxe-stepper.nxe-stepper .nxe-step-error .nxe-step-marker{border-color:var(--nxe-ui-status-danger-border);background:var(--nxe-ui-status-danger-bg);color:var(--nxe-ui-status-danger-fg)}nxe-stepper.nxe-stepper .nxe-step-error .nxe-step-label{color:var(--nxe-ui-status-danger-fg)}nxe-stepper.nxe-stepper .nxe-step-panel{padding-top:var(--nxe-ui-space-4)}nxe-stepper.nxe-stepper .nxe-step-panel[hidden]{display:none}nxe-stepper.nxe-stepper.nxe-stepper-horizontal .nxe-step-item{flex:1 1 auto}nxe-stepper.nxe-stepper.nxe-stepper-horizontal .nxe-step-item:last-child{flex:0 0 auto}nxe-stepper.nxe-stepper.nxe-stepper-horizontal .nxe-step-connector{flex:1 1 var(--nxe-ui-space-5);min-width:var(--nxe-ui-space-4);height:1px;background:var(--nxe-ui-border)}nxe-stepper.nxe-stepper.nxe-stepper-horizontal .nxe-step-completed .nxe-step-connector{background:var(--nxe-ui-status-success-border)}nxe-stepper.nxe-stepper.nxe-stepper-vertical .nxe-stepper-list{flex-direction:column}nxe-stepper.nxe-stepper.nxe-stepper-vertical .nxe-step-item{display:grid;grid-template-columns:minmax(0,1fr);align-items:start;gap:0}nxe-stepper.nxe-stepper.nxe-stepper-vertical .nxe-step-panel{margin-inline-start:calc(var(--nxe-step-marker-size) / 2 + var(--nxe-ui-space-2));padding-inline-start:var(--nxe-ui-space-4);padding-bottom:var(--nxe-ui-space-3);border-inline-start:1px solid var(--nxe-ui-border)}nxe-stepper.nxe-stepper.nxe-stepper-vertical .nxe-step-item:last-child .nxe-step-panel{border-inline-start-color:transparent}nxe-stepper.nxe-stepper.nxe-stepper-vertical .nxe-step-item:not(.nxe-step-active) .nxe-step-panel[hidden]{display:block;min-height:var(--nxe-ui-space-3);padding-top:0;padding-bottom:0}nxe-stepper.nxe-stepper.nxe-stepper-vertical .nxe-step-item:not(.nxe-step-active) .nxe-step-panel[hidden]>*{display:none}\n"], dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
807
+ }
808
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeStepperComponent, decorators: [{
809
+ type: Component,
810
+ args: [{ selector: 'nxe-stepper', imports: [NgTemplateOutlet], host: {
811
+ class: 'nxe-stepper',
812
+ '(keydown)': 'onKeydown($event)',
813
+ '[class.nxe-stepper-vertical]': "layout() === 'vertical'",
814
+ '[class.nxe-stepper-horizontal]': "layout() === 'horizontal'",
815
+ }, changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, template: "<ol #list class=\"nxe-stepper-list\" [attr.aria-label]=\"listLabel()\">\r\n @for (view of views(); track view.step; let last = $last) {\r\n <li\r\n class=\"nxe-step-item\"\r\n [class.nxe-step-active]=\"view.active\"\r\n [class.nxe-step-completed]=\"view.completed\"\r\n [class.nxe-step-error]=\"view.error\"\r\n [class.nxe-step-optional]=\"view.step.optional()\">\r\n <ng-template #headerBody>\r\n <span class=\"nxe-step-marker\" aria-hidden=\"true\">\r\n @switch (view.marker) {\r\n @case ('completed') {\r\n <svg viewBox=\"0 0 16 16\" width=\"14\" height=\"14\" focusable=\"false\">\r\n <path d=\"M3 8.5l3.2 3.2L13 4.8\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" />\r\n </svg>\r\n }\r\n @case ('error') {\r\n <svg viewBox=\"0 0 16 16\" width=\"14\" height=\"14\" focusable=\"false\">\r\n <path d=\"M8 3.5v5.2M8 11.6v.1\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" />\r\n </svg>\r\n }\r\n @default {\r\n {{ view.index + 1 }}\r\n }\r\n }\r\n </span>\r\n <span class=\"nxe-step-text\">\r\n <span class=\"nxe-step-label\">{{ headerLabel(view) }}</span>\r\n @if (view.error) {\r\n <span class=\"nxe-step-sub nxe-step-sub-error\">{{ stateText(view) }}</span>\r\n } @else if (view.step.description()) {\r\n <span class=\"nxe-step-sub\">{{ view.step.description() }}</span>\r\n }\r\n @if (view.step.optional()) {\r\n <span class=\"nxe-step-sub\">{{ optionalText() }}</span>\r\n }\r\n @if (!view.error && stateText(view)) {\r\n <span class=\"nxe-step-hidden\">{{ stateText(view) }}</span>\r\n }\r\n </span>\r\n </ng-template>\r\n @if (view.navigable) {\r\n <button\r\n type=\"button\"\r\n class=\"nxe-step-header\"\r\n [id]=\"view.headerId\"\r\n [attr.aria-current]=\"view.active ? 'step' : null\"\r\n [attr.aria-controls]=\"view.panelId\"\r\n (click)=\"open(view)\">\r\n <ng-container [ngTemplateOutlet]=\"headerBody\" />\r\n </button>\r\n } @else {\r\n <div class=\"nxe-step-header nxe-step-header-static\" [id]=\"view.headerId\">\r\n <ng-container [ngTemplateOutlet]=\"headerBody\" />\r\n </div>\r\n }\r\n @if (!last && layout() === 'horizontal') {\r\n <span class=\"nxe-step-connector\" aria-hidden=\"true\"></span>\r\n }\r\n @if (layout() === 'vertical') {\r\n <ng-container [ngTemplateOutlet]=\"panel\" [ngTemplateOutletContext]=\"{ $implicit: view }\" />\r\n }\r\n </li>\r\n }\r\n</ol>\r\n@if (layout() === 'horizontal') {\r\n @for (view of views(); track view.step) {\r\n <ng-container [ngTemplateOutlet]=\"panel\" [ngTemplateOutletContext]=\"{ $implicit: view }\" />\r\n }\r\n}\r\n<ng-template #panel let-view>\r\n <div class=\"nxe-step-panel\" role=\"group\" [id]=\"view.panelId\" [attr.aria-labelledby]=\"view.headerId\" [hidden]=\"!view.active\">\r\n @if (view.rendered) {\r\n <ng-container [ngTemplateOutlet]=\"view.step.contentTemplate()\" />\r\n }\r\n </div>\r\n</ng-template>\r\n<div class=\"nxe-step-hidden\" role=\"status\" aria-live=\"polite\" aria-atomic=\"true\">{{ announcement() }}</div>\r\n", styles: ["nxe-stepper.nxe-stepper{--nxe-step-marker-size: calc(var(--nxe-ui-space-5) + var(--nxe-ui-space-1));display:block;position:relative;color:var(--nxe-ui-text);font-family:var(--nxe-ui-font-family)}nxe-stepper.nxe-stepper .nxe-step-hidden{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip-path:inset(50%);white-space:nowrap;border:0}nxe-stepper.nxe-stepper .nxe-stepper-list{display:flex;margin:0;padding:0;list-style:none}nxe-stepper.nxe-stepper .nxe-step-item{display:flex;align-items:center;gap:var(--nxe-ui-space-2);min-width:0}nxe-stepper.nxe-stepper .nxe-step-header{display:inline-flex;align-items:center;gap:var(--nxe-ui-space-2);box-sizing:border-box;min-height:var(--nxe-ui-control-min-height);padding:var(--nxe-ui-space-1) var(--nxe-ui-space-2);border:0;border-radius:var(--nxe-ui-radius-sm);background:transparent;color:inherit;font:inherit;text-align:start}nxe-stepper.nxe-stepper button.nxe-step-header{cursor:pointer}nxe-stepper.nxe-stepper button.nxe-step-header:hover{background-color:var(--nxe-ui-surface-alt)}nxe-stepper.nxe-stepper button.nxe-step-header:focus-visible{outline:2px solid var(--nxe-ui-focus-ring-color);outline-offset:-2px}nxe-stepper.nxe-stepper .nxe-step-header-static{cursor:default}nxe-stepper.nxe-stepper .nxe-step-marker{display:inline-flex;flex:0 0 auto;align-items:center;justify-content:center;box-sizing:border-box;width:var(--nxe-step-marker-size);height:var(--nxe-step-marker-size);border:1px solid var(--nxe-ui-status-neutral-border);border-radius:var(--nxe-ui-radius-pill);background:var(--nxe-ui-status-neutral-bg);color:var(--nxe-ui-status-neutral-fg);font-size:var(--nxe-ui-font-size-sm);font-weight:var(--nxe-ui-font-weight-semibold);line-height:1}nxe-stepper.nxe-stepper .nxe-step-text{display:inline-flex;flex-direction:column;min-width:0}nxe-stepper.nxe-stepper .nxe-step-label{color:var(--nxe-ui-neutral-600);font-weight:var(--nxe-ui-font-weight-semibold)}nxe-stepper.nxe-stepper .nxe-step-sub{color:var(--nxe-ui-text-muted);font-size:var(--nxe-ui-font-size-xs)}nxe-stepper.nxe-stepper .nxe-step-sub-error{color:var(--nxe-ui-status-danger-fg)}nxe-stepper.nxe-stepper .nxe-step-active .nxe-step-marker{border-color:var(--nxe-ui-accent);background:var(--nxe-ui-accent);color:var(--nxe-ui-accent-contrast)}nxe-stepper.nxe-stepper .nxe-step-active .nxe-step-label{color:var(--nxe-ui-text)}nxe-stepper.nxe-stepper .nxe-step-completed:not(.nxe-step-active) .nxe-step-marker{border-color:var(--nxe-ui-status-success-border);background:var(--nxe-ui-status-success-bg);color:var(--nxe-ui-status-success-fg)}nxe-stepper.nxe-stepper .nxe-step-error .nxe-step-marker{border-color:var(--nxe-ui-status-danger-border);background:var(--nxe-ui-status-danger-bg);color:var(--nxe-ui-status-danger-fg)}nxe-stepper.nxe-stepper .nxe-step-error .nxe-step-label{color:var(--nxe-ui-status-danger-fg)}nxe-stepper.nxe-stepper .nxe-step-panel{padding-top:var(--nxe-ui-space-4)}nxe-stepper.nxe-stepper .nxe-step-panel[hidden]{display:none}nxe-stepper.nxe-stepper.nxe-stepper-horizontal .nxe-step-item{flex:1 1 auto}nxe-stepper.nxe-stepper.nxe-stepper-horizontal .nxe-step-item:last-child{flex:0 0 auto}nxe-stepper.nxe-stepper.nxe-stepper-horizontal .nxe-step-connector{flex:1 1 var(--nxe-ui-space-5);min-width:var(--nxe-ui-space-4);height:1px;background:var(--nxe-ui-border)}nxe-stepper.nxe-stepper.nxe-stepper-horizontal .nxe-step-completed .nxe-step-connector{background:var(--nxe-ui-status-success-border)}nxe-stepper.nxe-stepper.nxe-stepper-vertical .nxe-stepper-list{flex-direction:column}nxe-stepper.nxe-stepper.nxe-stepper-vertical .nxe-step-item{display:grid;grid-template-columns:minmax(0,1fr);align-items:start;gap:0}nxe-stepper.nxe-stepper.nxe-stepper-vertical .nxe-step-panel{margin-inline-start:calc(var(--nxe-step-marker-size) / 2 + var(--nxe-ui-space-2));padding-inline-start:var(--nxe-ui-space-4);padding-bottom:var(--nxe-ui-space-3);border-inline-start:1px solid var(--nxe-ui-border)}nxe-stepper.nxe-stepper.nxe-stepper-vertical .nxe-step-item:last-child .nxe-step-panel{border-inline-start-color:transparent}nxe-stepper.nxe-stepper.nxe-stepper-vertical .nxe-step-item:not(.nxe-step-active) .nxe-step-panel[hidden]{display:block;min-height:var(--nxe-ui-space-3);padding-top:0;padding-bottom:0}nxe-stepper.nxe-stepper.nxe-stepper-vertical .nxe-step-item:not(.nxe-step-active) .nxe-step-panel[hidden]>*{display:none}\n"] }]
816
+ }], ctorParameters: () => [], propDecorators: { selectedIndex: [{ type: i0.Input, args: [{ isSignal: true, alias: "selectedIndex", required: false }] }, { type: i0.Output, args: ["selectedIndexChange"] }], linear: [{ type: i0.Input, args: [{ isSignal: true, alias: "linear", required: false }] }], orientation: [{ type: i0.Input, args: [{ isSignal: true, alias: "orientation", required: false }] }], responsive: [{ type: i0.Input, args: [{ isSignal: true, alias: "responsive", required: false }] }], collapseBelow: [{ type: i0.Input, args: [{ isSignal: true, alias: "collapseBelow", required: false }] }], keepAlive: [{ type: i0.Input, args: [{ isSignal: true, alias: "keepAlive", required: false }] }], stepsLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "stepsLabel", required: false }] }], selectionChange: [{ type: i0.Output, args: ["selectionChange"] }], stepBlocked: [{ type: i0.Output, args: ["stepBlocked"] }], steps: [{ type: i0.ContentChildren, args: [i0.forwardRef(() => NxeStepComponent), { isSignal: true }] }], list: [{ type: i0.ViewChild, args: ['list', { isSignal: true }] }] } });
817
+
818
+ /**
819
+ * Makes a button move its `nxe-stepper` to the next step. Put it on any element inside a step
820
+ * (`nxe-button`, a native `button`); from outside the stepper, give it the stepper:
821
+ *
822
+ * ```html
823
+ * <nxe-button nxeStepperNext text="Continue" type="default" />
824
+ * <button type="button" [nxeStepperNext]="wizard">Continue</button>
825
+ * ```
826
+ *
827
+ * It calls `next()`, so a linear stepper that is not ready to leave the step stays on it, marks the
828
+ * step's controls as touched and announces why. It does not disable the button: a blocked click is how
829
+ * the user finds out what is missing.
830
+ */
831
+ class NxeStepperNextDirective {
832
+ parent = inject(NxeStepperComponent, { optional: true });
833
+ /** The stepper to drive when the element is not inside one. */
834
+ target = input(undefined, { ...(ngDevMode ? { debugName: "target" } : /* istanbul ignore next */ {}), alias: 'nxeStepperNext' });
835
+ stepper() {
836
+ return this.target() || this.parent;
837
+ }
838
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeStepperNextDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
839
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.1.5", type: NxeStepperNextDirective, isStandalone: true, selector: "[nxeStepperNext]", inputs: { target: { classPropertyName: "target", publicName: "nxeStepperNext", isSignal: true, isRequired: false, transformFunction: null } }, host: { listeners: { "click": "stepper()?.next()" } }, ngImport: i0 });
840
+ }
841
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeStepperNextDirective, decorators: [{
842
+ type: Directive,
843
+ args: [{
844
+ selector: '[nxeStepperNext]',
845
+ host: { '(click)': 'stepper()?.next()' },
846
+ }]
847
+ }], propDecorators: { target: [{ type: i0.Input, args: [{ isSignal: true, alias: "nxeStepperNext", required: false }] }] } });
848
+
849
+ /**
850
+ * Makes a button move its `nxe-stepper` back one step; the counterpart of `nxeStepperNext`. Going
851
+ * back is never validated, and on the first step it does nothing.
852
+ *
853
+ * ```html
854
+ * <nxe-button nxeStepperPrevious text="Back" stylingMode="outlined" />
855
+ * ```
856
+ */
857
+ class NxeStepperPreviousDirective {
858
+ parent = inject(NxeStepperComponent, { optional: true });
859
+ /** The stepper to drive when the element is not inside one. */
860
+ target = input(undefined, { ...(ngDevMode ? { debugName: "target" } : /* istanbul ignore next */ {}), alias: 'nxeStepperPrevious' });
861
+ stepper() {
862
+ return this.target() || this.parent;
863
+ }
864
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeStepperPreviousDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
865
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.1.5", type: NxeStepperPreviousDirective, isStandalone: true, selector: "[nxeStepperPrevious]", inputs: { target: { classPropertyName: "target", publicName: "nxeStepperPrevious", isSignal: true, isRequired: false, transformFunction: null } }, host: { listeners: { "click": "stepper()?.previous()" } }, ngImport: i0 });
866
+ }
867
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeStepperPreviousDirective, decorators: [{
868
+ type: Directive,
869
+ args: [{
870
+ selector: '[nxeStepperPrevious]',
871
+ host: { '(click)': 'stepper()?.previous()' },
872
+ }]
873
+ }], propDecorators: { target: [{ type: i0.Input, args: [{ isSignal: true, alias: "nxeStepperPrevious", required: false }] }] } });
874
+
875
+ /**
876
+ * Marks the `<ng-template>` an `nxe-breadcrumb` draws between crumbs, for a separator that is markup
877
+ * (an icon) rather than the text of the `separator` input:
878
+ *
879
+ * ```html
880
+ * <nxe-breadcrumb [items]="items">
881
+ * <ng-template nxeBreadcrumbSeparator><svg viewBox="0 0 16 16">...</svg></ng-template>
882
+ * </nxe-breadcrumb>
883
+ * ```
884
+ *
885
+ * The separator is decoration (`aria-hidden`): the `<ol>` already says these are steps in a list.
886
+ */
887
+ class NxeBreadcrumbSeparatorDirective {
888
+ /** The template to draw. */
889
+ template = inject(TemplateRef);
890
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeBreadcrumbSeparatorDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
891
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.1.5", type: NxeBreadcrumbSeparatorDirective, isStandalone: true, selector: "ng-template[nxeBreadcrumbSeparator]", ngImport: i0 });
892
+ }
893
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeBreadcrumbSeparatorDirective, decorators: [{
894
+ type: Directive,
895
+ args: [{
896
+ selector: 'ng-template[nxeBreadcrumbSeparator]',
897
+ }]
898
+ }] });
899
+
900
+ /**
901
+ * `nxe-breadcrumb` shows where a page sits in the application, one link per level, for document pages
902
+ * that are a few levels deep.
903
+ *
904
+ * ```html
905
+ * <!-- from data -->
906
+ * <nxe-breadcrumb [items]="[
907
+ * { label: 'Pesanan', route: '/pos' },
908
+ * { label: 'PO-2026-0412', route: ['/pos', 412] },
909
+ * { label: 'Baris' }
910
+ * ]" />
911
+ *
912
+ * <!-- from the router: each route's data.breadcrumb, root to leaf -->
913
+ * <nxe-breadcrumb fromRoute />
914
+ * ```
915
+ *
916
+ * ```ts
917
+ * { path: 'pos', data: { breadcrumb: 'Pesanan' }, children: [
918
+ * { path: ':id', resolve: { po: poResolver }, data: { breadcrumb: (d: Data) => d['po'].number } },
919
+ * ] }
920
+ * ```
921
+ *
922
+ * **Sources.** `items` are the crumbs you list. With `fromRoute`, the crumbs of the active route follow
923
+ * them (so `items` can hold a fixed "Beranda"): the router state is walked from the root, every route that
924
+ * declares a `breadcrumb` (in `data` or `resolve`) adds one, linked to that route's own URL, and the trail
925
+ * is read again after every `NavigationEnd`. A `data.breadcrumb` is a string or `(data) => string`; an empty
926
+ * result drops the route.
927
+ *
928
+ * **Semantics.** A `<nav aria-label="Breadcrumb">` around an `<ol>`. The last crumb is the current page: plain
929
+ * text with `aria-current="page"`, never a link. A crumb with no `route` or `href` is text too. Separators
930
+ * are `aria-hidden`.
931
+ *
932
+ * **Long trails.** More crumbs than `maxItems` (default 5) keep the first and the last `maxItems - 1` and fold
933
+ * the middle into a "..." button that opens an `nxe-menu` of them. `maxItems` of 0 never folds.
934
+ *
935
+ * **Separator.** The `separator` input (a character, default `/`), or an `<ng-template nxeBreadcrumbSeparator>`
936
+ * for an icon.
937
+ */
938
+ class NxeBreadcrumbComponent {
939
+ router = inject(Router);
940
+ messages = inject(NxeMessageService);
941
+ /** The crumbs to show. */
942
+ items = input(undefined, /* @ts-ignore */
943
+ ...(ngDevMode ? [{ debugName: "items" }] : /* istanbul ignore next */ []));
944
+ /** Add the crumbs of the active route's `data.breadcrumb`, after `items`. */
945
+ fromRoute = input(false, { ...(ngDevMode ? { debugName: "fromRoute" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
946
+ /** Crumbs shown before the middle ones fold into a menu; 0 never folds. At least 2 otherwise. Default 5. */
947
+ maxItems = input(5, { ...(ngDevMode ? { debugName: "maxItems" } : /* istanbul ignore next */ {}), transform: (value) => Math.max(0, Math.floor(numberAttribute(value, 5))) });
948
+ /** Text between crumbs. Default `/`. */
949
+ separator = input('/', /* @ts-ignore */
950
+ ...(ngDevMode ? [{ debugName: "separator" }] : /* istanbul ignore next */ []));
951
+ /** Accessible name of the `nav`. Default "Breadcrumb" (`breadcrumb.label`). */
952
+ ariaLabel = input(undefined, { ...(ngDevMode ? { debugName: "ariaLabel" } : /* istanbul ignore next */ {}), alias: 'aria-label' });
953
+ separatorTemplate = contentChild(NxeBreadcrumbSeparatorDirective, /* @ts-ignore */
954
+ ...(ngDevMode ? [{ debugName: "separatorTemplate" }] : /* istanbul ignore next */ []));
955
+ /** Bumped after each navigation so the route trail is read again. */
956
+ navigations = signal(0, /* @ts-ignore */
957
+ ...(ngDevMode ? [{ debugName: "navigations" }] : /* istanbul ignore next */ []));
958
+ navLabel = computed(() => this.ariaLabel() || this.messages.t('breadcrumb.label'), /* @ts-ignore */
959
+ ...(ngDevMode ? [{ debugName: "navLabel" }] : /* istanbul ignore next */ []));
960
+ moreLabel = computed(() => this.messages.t('breadcrumb.more'), /* @ts-ignore */
961
+ ...(ngDevMode ? [{ debugName: "moreLabel" }] : /* istanbul ignore next */ []));
962
+ /** Every crumb, in order: `items`, then the route's. */
963
+ crumbs = computed(() => {
964
+ const listed = this.items() ?? [];
965
+ if (!this.fromRoute()) {
966
+ return listed;
967
+ }
968
+ this.navigations();
969
+ return [...listed, ...this.routeCrumbs(this.router.routerState.snapshot.root)];
970
+ }, /* @ts-ignore */
971
+ ...(ngDevMode ? [{ debugName: "crumbs" }] : /* istanbul ignore next */ []));
972
+ /** The crumbs folded into the menu: the middle ones past `maxItems`. */
973
+ folded = computed(() => {
974
+ const [from, to] = this.foldRange();
975
+ return this.crumbs().slice(from, to);
976
+ }, /* @ts-ignore */
977
+ ...(ngDevMode ? [{ debugName: "folded" }] : /* istanbul ignore next */ []));
978
+ slots = computed(() => {
979
+ const crumbs = this.crumbs();
980
+ const [from, to] = this.foldRange();
981
+ const out = [];
982
+ crumbs.forEach((crumb, index) => {
983
+ if (index >= from && index < to) {
984
+ if (index === from) {
985
+ out.push({ crumb: null, last: false });
986
+ }
987
+ return;
988
+ }
989
+ out.push({ crumb, last: index === crumbs.length - 1 });
990
+ });
991
+ return out;
992
+ }, /* @ts-ignore */
993
+ ...(ngDevMode ? [{ debugName: "slots" }] : /* istanbul ignore next */ []));
994
+ constructor() {
995
+ this.router.events
996
+ .pipe(filter((event) => event instanceof NavigationEnd), takeUntilDestroyed(inject(DestroyRef)))
997
+ .subscribe(() => this.navigations.update((n) => n + 1));
998
+ }
999
+ /** `[from, to)` of the crumbs that fold; empty when everything fits. */
1000
+ foldRange() {
1001
+ const count = this.crumbs().length;
1002
+ const max = this.maxItems();
1003
+ if (max === 0) {
1004
+ return [0, 0];
1005
+ }
1006
+ const keep = Math.max(2, max);
1007
+ return count > keep ? [1, count - (keep - 1)] : [0, 0];
1008
+ }
1009
+ /** One crumb per route, root to leaf, that declares a breadcrumb of its own. */
1010
+ routeCrumbs(root) {
1011
+ const trail = [];
1012
+ const path = [];
1013
+ for (let snapshot = root; snapshot; snapshot = snapshot.firstChild) {
1014
+ path.push(...snapshot.url.map((segment) => segment.path));
1015
+ const config = snapshot.routeConfig;
1016
+ // Inherited data is not this route's own: an empty-path child would repeat its parent's crumb.
1017
+ const declared = config?.data?.['breadcrumb'] !== undefined || config?.resolve?.['breadcrumb'] !== undefined;
1018
+ const source = snapshot.data['breadcrumb'];
1019
+ if (!declared || source === undefined || source === null) {
1020
+ continue;
1021
+ }
1022
+ const label = typeof source === 'function' ? source(snapshot.data) : source;
1023
+ if (label) {
1024
+ trail.push({ label, route: ['/', ...path] });
1025
+ }
1026
+ }
1027
+ return trail;
1028
+ }
1029
+ /** The commands of a crumb's `routerLink`. */
1030
+ commands(crumb) {
1031
+ return crumb.route ?? [];
1032
+ }
1033
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeBreadcrumbComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1034
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.5", type: NxeBreadcrumbComponent, isStandalone: true, selector: "nxe-breadcrumb", inputs: { items: { classPropertyName: "items", publicName: "items", isSignal: true, isRequired: false, transformFunction: null }, fromRoute: { classPropertyName: "fromRoute", publicName: "fromRoute", isSignal: true, isRequired: false, transformFunction: null }, maxItems: { classPropertyName: "maxItems", publicName: "maxItems", isSignal: true, isRequired: false, transformFunction: null }, separator: { classPropertyName: "separator", publicName: "separator", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "aria-label", isSignal: true, isRequired: false, transformFunction: null } }, host: { classAttribute: "nxe-breadcrumb" }, queries: [{ propertyName: "separatorTemplate", first: true, predicate: NxeBreadcrumbSeparatorDirective, descendants: true, isSignal: true }], exportAs: ["nxeBreadcrumb"], ngImport: i0, template: "<nav class=\"nxe-breadcrumb-nav\" [attr.aria-label]=\"navLabel()\">\r\n <ol class=\"nxe-breadcrumb-list\">\r\n @for (slot of slots(); track $index) {\r\n <li class=\"nxe-breadcrumb-item\">\r\n @if (slot.crumb; as crumb) {\r\n @if (slot.last) {\r\n <span class=\"nxe-breadcrumb-current\" aria-current=\"page\">{{ crumb.label }}</span>\r\n } @else if (crumb.route !== undefined) {\r\n <a class=\"nxe-breadcrumb-link\" [routerLink]=\"commands(crumb)\" [queryParams]=\"crumb.queryParams\">{{ crumb.label }}</a>\r\n } @else if (crumb.href !== undefined) {\r\n <a class=\"nxe-breadcrumb-link\" [href]=\"crumb.href\">{{ crumb.label }}</a>\r\n } @else {\r\n <span class=\"nxe-breadcrumb-text\">{{ crumb.label }}</span>\r\n }\r\n } @else {\r\n <button type=\"button\" class=\"nxe-breadcrumb-more\" [attr.aria-label]=\"moreLabel()\" [nxeMenuTriggerFor]=\"more\">\r\n <span aria-hidden=\"true\">&hellip;</span>\r\n </button>\r\n }\r\n @if (!slot.last) {\r\n <span class=\"nxe-breadcrumb-separator\" aria-hidden=\"true\">\r\n @if (separatorTemplate(); as template) {\r\n <ng-container *ngTemplateOutlet=\"template.template\" />\r\n } @else {\r\n {{ separator() }}\r\n }\r\n </span>\r\n }\r\n </li>\r\n }\r\n </ol>\r\n</nav>\r\n\r\n<nxe-menu #more panelClass=\"nxe-breadcrumb-menu\">\r\n @for (crumb of folded(); track $index) {\r\n @if (crumb.route !== undefined) {\r\n <nxe-menu-item [routerLink]=\"commands(crumb)\" [queryParams]=\"crumb.queryParams\">{{ crumb.label }}</nxe-menu-item>\r\n } @else if (crumb.href !== undefined) {\r\n <a nxe-menu-item [href]=\"crumb.href\">{{ crumb.label }}</a>\r\n } @else {\r\n <nxe-menu-item [disabled]=\"true\">{{ crumb.label }}</nxe-menu-item>\r\n }\r\n }\r\n</nxe-menu>\r\n", styles: ["nxe-breadcrumb.nxe-breadcrumb{display:block;font-family:var(--nxe-ui-font-family);font-size:var(--nxe-ui-font-size-md);line-height:var(--nxe-ui-line-height-md);color:var(--nxe-ui-text)}nxe-breadcrumb.nxe-breadcrumb .nxe-breadcrumb-list{display:flex;flex-wrap:wrap;align-items:center;gap:var(--nxe-ui-space-1) 0;margin:0;padding:0;list-style:none}nxe-breadcrumb.nxe-breadcrumb .nxe-breadcrumb-item{display:inline-flex;align-items:center;min-width:0}nxe-breadcrumb.nxe-breadcrumb .nxe-breadcrumb-link,nxe-breadcrumb.nxe-breadcrumb .nxe-breadcrumb-more{border-radius:var(--nxe-ui-radius-sm);color:var(--nxe-ui-accent-strong);text-decoration:none}nxe-breadcrumb.nxe-breadcrumb .nxe-breadcrumb-link:hover,nxe-breadcrumb.nxe-breadcrumb .nxe-breadcrumb-more:hover{text-decoration:underline}nxe-breadcrumb.nxe-breadcrumb .nxe-breadcrumb-link:focus-visible,nxe-breadcrumb.nxe-breadcrumb .nxe-breadcrumb-more:focus-visible{outline:none;box-shadow:var(--nxe-ui-focus-ring)}nxe-breadcrumb.nxe-breadcrumb .nxe-breadcrumb-link{padding:0 var(--nxe-ui-space-1)}nxe-breadcrumb.nxe-breadcrumb .nxe-breadcrumb-more{display:inline-flex;align-items:center;justify-content:center;min-width:var(--nxe-ui-space-6);height:var(--nxe-ui-space-6);padding:0 var(--nxe-ui-space-1);border:0;background:transparent;font:inherit;cursor:pointer}nxe-breadcrumb.nxe-breadcrumb .nxe-breadcrumb-more:hover{background:var(--nxe-ui-surface-alt);text-decoration:none}nxe-breadcrumb.nxe-breadcrumb .nxe-breadcrumb-current{padding:0 var(--nxe-ui-space-1);font-weight:var(--nxe-ui-font-weight-semibold);overflow-wrap:anywhere}nxe-breadcrumb.nxe-breadcrumb .nxe-breadcrumb-text{padding:0 var(--nxe-ui-space-1)}nxe-breadcrumb.nxe-breadcrumb .nxe-breadcrumb-separator{display:inline-flex;align-items:center;padding:0 var(--nxe-ui-space-1);color:var(--nxe-ui-neutral-600);-webkit-user-select:none;user-select:none}nxe-breadcrumb.nxe-breadcrumb .nxe-breadcrumb-separator svg{width:1em;height:1em;fill:none;stroke:currentColor}[dir=rtl] nxe-breadcrumb.nxe-breadcrumb .nxe-breadcrumb-separator{transform:scaleX(-1)}\n"], 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", "browserUrl", "routerLink"] }, { kind: "component", type: NxeMenuComponent, selector: "nxe-menu", inputs: ["aria-label", "panelClass"], exportAs: ["nxeMenu"] }, { kind: "component", type: NxeMenuItemComponent, selector: "nxe-menu-item, [nxe-menu-item]", inputs: ["disabled", "danger"], outputs: ["itemClick"] }, { kind: "directive", type: NxeMenuTriggerForDirective, selector: "[nxeMenuTriggerFor]", inputs: ["nxeMenuTriggerFor"], exportAs: ["nxeMenuTrigger"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
1035
+ }
1036
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeBreadcrumbComponent, decorators: [{
1037
+ type: Component,
1038
+ args: [{ selector: 'nxe-breadcrumb', exportAs: 'nxeBreadcrumb', imports: [
1039
+ NgTemplateOutlet,
1040
+ RouterLink,
1041
+ NxeMenuComponent,
1042
+ NxeMenuItemComponent,
1043
+ NxeMenuTriggerForDirective,
1044
+ ], host: { class: 'nxe-breadcrumb' }, changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, template: "<nav class=\"nxe-breadcrumb-nav\" [attr.aria-label]=\"navLabel()\">\r\n <ol class=\"nxe-breadcrumb-list\">\r\n @for (slot of slots(); track $index) {\r\n <li class=\"nxe-breadcrumb-item\">\r\n @if (slot.crumb; as crumb) {\r\n @if (slot.last) {\r\n <span class=\"nxe-breadcrumb-current\" aria-current=\"page\">{{ crumb.label }}</span>\r\n } @else if (crumb.route !== undefined) {\r\n <a class=\"nxe-breadcrumb-link\" [routerLink]=\"commands(crumb)\" [queryParams]=\"crumb.queryParams\">{{ crumb.label }}</a>\r\n } @else if (crumb.href !== undefined) {\r\n <a class=\"nxe-breadcrumb-link\" [href]=\"crumb.href\">{{ crumb.label }}</a>\r\n } @else {\r\n <span class=\"nxe-breadcrumb-text\">{{ crumb.label }}</span>\r\n }\r\n } @else {\r\n <button type=\"button\" class=\"nxe-breadcrumb-more\" [attr.aria-label]=\"moreLabel()\" [nxeMenuTriggerFor]=\"more\">\r\n <span aria-hidden=\"true\">&hellip;</span>\r\n </button>\r\n }\r\n @if (!slot.last) {\r\n <span class=\"nxe-breadcrumb-separator\" aria-hidden=\"true\">\r\n @if (separatorTemplate(); as template) {\r\n <ng-container *ngTemplateOutlet=\"template.template\" />\r\n } @else {\r\n {{ separator() }}\r\n }\r\n </span>\r\n }\r\n </li>\r\n }\r\n </ol>\r\n</nav>\r\n\r\n<nxe-menu #more panelClass=\"nxe-breadcrumb-menu\">\r\n @for (crumb of folded(); track $index) {\r\n @if (crumb.route !== undefined) {\r\n <nxe-menu-item [routerLink]=\"commands(crumb)\" [queryParams]=\"crumb.queryParams\">{{ crumb.label }}</nxe-menu-item>\r\n } @else if (crumb.href !== undefined) {\r\n <a nxe-menu-item [href]=\"crumb.href\">{{ crumb.label }}</a>\r\n } @else {\r\n <nxe-menu-item [disabled]=\"true\">{{ crumb.label }}</nxe-menu-item>\r\n }\r\n }\r\n</nxe-menu>\r\n", styles: ["nxe-breadcrumb.nxe-breadcrumb{display:block;font-family:var(--nxe-ui-font-family);font-size:var(--nxe-ui-font-size-md);line-height:var(--nxe-ui-line-height-md);color:var(--nxe-ui-text)}nxe-breadcrumb.nxe-breadcrumb .nxe-breadcrumb-list{display:flex;flex-wrap:wrap;align-items:center;gap:var(--nxe-ui-space-1) 0;margin:0;padding:0;list-style:none}nxe-breadcrumb.nxe-breadcrumb .nxe-breadcrumb-item{display:inline-flex;align-items:center;min-width:0}nxe-breadcrumb.nxe-breadcrumb .nxe-breadcrumb-link,nxe-breadcrumb.nxe-breadcrumb .nxe-breadcrumb-more{border-radius:var(--nxe-ui-radius-sm);color:var(--nxe-ui-accent-strong);text-decoration:none}nxe-breadcrumb.nxe-breadcrumb .nxe-breadcrumb-link:hover,nxe-breadcrumb.nxe-breadcrumb .nxe-breadcrumb-more:hover{text-decoration:underline}nxe-breadcrumb.nxe-breadcrumb .nxe-breadcrumb-link:focus-visible,nxe-breadcrumb.nxe-breadcrumb .nxe-breadcrumb-more:focus-visible{outline:none;box-shadow:var(--nxe-ui-focus-ring)}nxe-breadcrumb.nxe-breadcrumb .nxe-breadcrumb-link{padding:0 var(--nxe-ui-space-1)}nxe-breadcrumb.nxe-breadcrumb .nxe-breadcrumb-more{display:inline-flex;align-items:center;justify-content:center;min-width:var(--nxe-ui-space-6);height:var(--nxe-ui-space-6);padding:0 var(--nxe-ui-space-1);border:0;background:transparent;font:inherit;cursor:pointer}nxe-breadcrumb.nxe-breadcrumb .nxe-breadcrumb-more:hover{background:var(--nxe-ui-surface-alt);text-decoration:none}nxe-breadcrumb.nxe-breadcrumb .nxe-breadcrumb-current{padding:0 var(--nxe-ui-space-1);font-weight:var(--nxe-ui-font-weight-semibold);overflow-wrap:anywhere}nxe-breadcrumb.nxe-breadcrumb .nxe-breadcrumb-text{padding:0 var(--nxe-ui-space-1)}nxe-breadcrumb.nxe-breadcrumb .nxe-breadcrumb-separator{display:inline-flex;align-items:center;padding:0 var(--nxe-ui-space-1);color:var(--nxe-ui-neutral-600);-webkit-user-select:none;user-select:none}nxe-breadcrumb.nxe-breadcrumb .nxe-breadcrumb-separator svg{width:1em;height:1em;fill:none;stroke:currentColor}[dir=rtl] nxe-breadcrumb.nxe-breadcrumb .nxe-breadcrumb-separator{transform:scaleX(-1)}\n"] }]
1045
+ }], ctorParameters: () => [], propDecorators: { items: [{ type: i0.Input, args: [{ isSignal: true, alias: "items", required: false }] }], fromRoute: [{ type: i0.Input, args: [{ isSignal: true, alias: "fromRoute", required: false }] }], maxItems: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxItems", required: false }] }], separator: [{ type: i0.Input, args: [{ isSignal: true, alias: "separator", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "aria-label", required: false }] }], separatorTemplate: [{ type: i0.ContentChild, args: [i0.forwardRef(() => NxeBreadcrumbSeparatorDirective), { isSignal: true }] }] } });
1046
+
1047
+ // @nexterajs/ui/navigation — public API.
1048
+
1049
+ /**
1050
+ * Generated bundle index. Do not edit.
1051
+ */
1052
+
1053
+ export { NxeBreadcrumbComponent, NxeBreadcrumbSeparatorDirective, NxeStepComponent, NxeStepContentDirective, NxeStepperComponent, NxeStepperNextDirective, NxeStepperPreviousDirective, NxeTabComponent, NxeTabContentDirective, NxeTabsComponent };
1054
+ //# sourceMappingURL=nexterajs-ui-navigation.mjs.map