@truenas/ui-components 0.3.22 → 0.3.24

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.
@@ -13,7 +13,7 @@ import { mdiCheckCircle, mdiAlertCircle, mdiAlert, mdiInformation, mdiOpenInNew,
13
13
  import * as i1$1 from '@angular/platform-browser';
14
14
  import { DomSanitizer } from '@angular/platform-browser';
15
15
  import { HttpClient } from '@angular/common/http';
16
- import { firstValueFrom, BehaviorSubject, merge, animationFrameScheduler, asapScheduler, Subject, take } from 'rxjs';
16
+ import { firstValueFrom, BehaviorSubject, Subject, merge, animationFrameScheduler, asapScheduler, take } from 'rxjs';
17
17
  import { RouterLink } from '@angular/router';
18
18
  import { filesize } from 'filesize';
19
19
  import { CdkMenu, CdkMenuItem, CdkMenuTrigger } from '@angular/cdk/menu';
@@ -23,9 +23,9 @@ import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
23
23
  import { trigger, state, transition, style, animate } from '@angular/animations';
24
24
  import { SelectionModel, DataSource } from '@angular/cdk/collections';
25
25
  import * as i2 from '@angular/cdk/tree';
26
- import { CdkTree, CdkTreeModule, CdkTreeNode, CDK_TREE_NODE_OUTLET_NODE, CdkTreeNodeOutlet, CdkNestedTreeNode, CdkTreeNodeOutletContext } from '@angular/cdk/tree';
26
+ import { CdkTree, CdkTreeModule, CdkTreeNode, CDK_TREE_NODE_OUTLET_NODE, CdkNestedTreeNode, CdkTreeNodeOutlet, FlatTreeControl, NestedTreeControl, CdkTreeNodeOutletContext } from '@angular/cdk/tree';
27
27
  export { FlatTreeControl } from '@angular/cdk/tree';
28
- import { map, auditTime } from 'rxjs/operators';
28
+ import { map, filter, debounceTime, distinctUntilChanged, takeUntil, auditTime } from 'rxjs/operators';
29
29
  import { CdkVirtualScrollViewport, CdkVirtualScrollableWindow, CdkFixedSizeVirtualScroll, CdkVirtualForOf, ScrollingModule } from '@angular/cdk/scrolling';
30
30
  import { DialogRef, DIALOG_DATA, Dialog } from '@angular/cdk/dialog';
31
31
 
@@ -245,6 +245,34 @@ function controlTestId(testId) {
245
245
  });
246
246
  }
247
247
 
248
+ /**
249
+ * Derive the test-id segments for a dropdown option row, consumed by
250
+ * `[tnTestId]` with `tnTestIdType="option"`. The component's resolved base
251
+ * (explicit `testId`, else the bound control name) scopes a per-option
252
+ * discriminator so ids stay unique across instances: base `user` + option
253
+ * value `jane-doe` → `option-user-jane-doe`; with no base → `option-jane-doe`.
254
+ *
255
+ * The discriminator comes from `extractor` when provided (a component's
256
+ * `optionTestIdKey` input), else the option's primitive `value`, else its
257
+ * `label`. Shared by `tn-select` and `tn-autocomplete` so the derivation
258
+ * rules can't drift between dropdown components; synthetic rows with fixed
259
+ * discriminators (e.g. select's `allowEmpty` option) are handled by the
260
+ * caller before delegating here.
261
+ */
262
+ function optionTestId(base, option, extractor) {
263
+ let key;
264
+ if (extractor) {
265
+ key = extractor(option);
266
+ }
267
+ else if (typeof option.value === 'string' || typeof option.value === 'number') {
268
+ key = option.value;
269
+ }
270
+ else {
271
+ key = option.label;
272
+ }
273
+ return scopeTestId(base, key);
274
+ }
275
+
248
276
  /**
249
277
  * Controls which attribute name the library renders `testId` values to.
250
278
  *
@@ -412,6 +440,20 @@ class TnAutocompleteComponent {
412
440
  ariaLabel = input(undefined, ...(ngDevMode ? [{ debugName: "ariaLabel" }] : []));
413
441
  /** Test ID attribute */
414
442
  testId = input(undefined, ...(ngDevMode ? [{ debugName: "testId" }] : []));
443
+ /** Test-id base, falling back to the bound control name when `testId` is unset. */
444
+ resolvedTestId = controlTestId(this.testId);
445
+ /**
446
+ * Optional extractor for the per-option test-id discriminator. Defaults to
447
+ * the option's `value` (when a string/number) or its `label`. Provide this
448
+ * when option values are objects, or to pick a more stable/unique key —
449
+ * mirrors `tn-select`'s input of the same name.
450
+ *
451
+ * @example
452
+ * ```html
453
+ * <tn-autocomplete testId="user" [optionTestIdKey]="(o) => o.value.id" ... />
454
+ * ```
455
+ */
456
+ optionTestIdKey = input(...(ngDevMode ? [undefined, { debugName: "optionTestIdKey" }] : []));
415
457
  /**
416
458
  * ARIA wiring from an enclosing `tn-form-field` (label, error/hint,
417
459
  * invalid, required). All-null when standalone or when `ariaLabel` overrides.
@@ -745,6 +787,25 @@ class TnAutocompleteComponent {
745
787
  }
746
788
  return this.valueMatches(option.value, value);
747
789
  }
790
+ /**
791
+ * Test-id segments for an option row, consumed by `[tnTestId]` with
792
+ * `tnTestIdType="option"` — see {@link optionTestId} for the derivation
793
+ * rules (shared with `tn-select`).
794
+ */
795
+ optionTestIdParts(option) {
796
+ return optionTestId(this.resolvedTestId(), option, this.optionTestIdKey());
797
+ }
798
+ /**
799
+ * Test-id segments for the loading / no-results status rows, consumed by
800
+ * `[tnTestId]` with `tnTestIdType="autocomplete"` so they nest under the
801
+ * input's own id: base `user` → `autocomplete-user-loading` /
802
+ * `autocomplete-user-no-results` (unscoped `autocomplete-loading` /
803
+ * `autocomplete-no-results` with no base). Mirrors webui's ix-combobox,
804
+ * which stamps its status option so automation can assert on those states.
805
+ */
806
+ statusTestIdParts(status) {
807
+ return scopeTestId(this.resolvedTestId(), status);
808
+ }
748
809
  // ── Internal ──
749
810
  /**
750
811
  * Index of the committed value's option in the visible list, or -1 when
@@ -921,13 +982,13 @@ class TnAutocompleteComponent {
921
982
  }
922
983
  }
923
984
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnAutocompleteComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
924
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.0", type: TnAutocompleteComponent, isStandalone: true, selector: "tn-autocomplete", inputs: { options: { classPropertyName: "options", publicName: "options", isSignal: true, isRequired: false, transformFunction: null }, compareWith: { classPropertyName: "compareWith", publicName: "compareWith", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, requireSelection: { classPropertyName: "requireSelection", publicName: "requireSelection", isSignal: true, isRequired: false, transformFunction: null }, allowCustomValue: { classPropertyName: "allowCustomValue", publicName: "allowCustomValue", isSignal: true, isRequired: false, transformFunction: null }, loading: { classPropertyName: "loading", publicName: "loading", isSignal: true, isRequired: false, transformFunction: null }, loadingText: { classPropertyName: "loadingText", publicName: "loadingText", isSignal: true, isRequired: false, transformFunction: null }, filterFn: { classPropertyName: "filterFn", publicName: "filterFn", isSignal: true, isRequired: false, transformFunction: null }, noResultsText: { classPropertyName: "noResultsText", publicName: "noResultsText", isSignal: true, isRequired: false, transformFunction: null }, maxResults: { classPropertyName: "maxResults", publicName: "maxResults", isSignal: true, isRequired: false, transformFunction: null }, panelMaxHeight: { classPropertyName: "panelMaxHeight", publicName: "panelMaxHeight", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null }, testId: { classPropertyName: "testId", publicName: "testId", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { optionSelected: "optionSelected", searchChange: "searchChange", loadMore: "loadMore", opened: "opened" }, providers: [
985
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.0", type: TnAutocompleteComponent, isStandalone: true, selector: "tn-autocomplete", inputs: { options: { classPropertyName: "options", publicName: "options", isSignal: true, isRequired: false, transformFunction: null }, compareWith: { classPropertyName: "compareWith", publicName: "compareWith", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, requireSelection: { classPropertyName: "requireSelection", publicName: "requireSelection", isSignal: true, isRequired: false, transformFunction: null }, allowCustomValue: { classPropertyName: "allowCustomValue", publicName: "allowCustomValue", isSignal: true, isRequired: false, transformFunction: null }, loading: { classPropertyName: "loading", publicName: "loading", isSignal: true, isRequired: false, transformFunction: null }, loadingText: { classPropertyName: "loadingText", publicName: "loadingText", isSignal: true, isRequired: false, transformFunction: null }, filterFn: { classPropertyName: "filterFn", publicName: "filterFn", isSignal: true, isRequired: false, transformFunction: null }, noResultsText: { classPropertyName: "noResultsText", publicName: "noResultsText", isSignal: true, isRequired: false, transformFunction: null }, maxResults: { classPropertyName: "maxResults", publicName: "maxResults", isSignal: true, isRequired: false, transformFunction: null }, panelMaxHeight: { classPropertyName: "panelMaxHeight", publicName: "panelMaxHeight", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null }, testId: { classPropertyName: "testId", publicName: "testId", isSignal: true, isRequired: false, transformFunction: null }, optionTestIdKey: { classPropertyName: "optionTestIdKey", publicName: "optionTestIdKey", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { optionSelected: "optionSelected", searchChange: "searchChange", loadMore: "loadMore", opened: "opened" }, providers: [
925
986
  {
926
987
  provide: NG_VALUE_ACCESSOR,
927
988
  useExisting: forwardRef(() => TnAutocompleteComponent),
928
989
  multi: true,
929
990
  },
930
- ], viewQueries: [{ propertyName: "inputEl", first: true, predicate: ["inputEl"], descendants: true, isSignal: true }, { propertyName: "dropdownTemplate", first: true, predicate: ["dropdownTemplate"], descendants: true, isSignal: true }], ngImport: i0, template: "<div class=\"tn-autocomplete\">\n <input\n #inputEl\n type=\"text\"\n class=\"tn-autocomplete__input\"\n role=\"combobox\"\n aria-haspopup=\"listbox\"\n tnTestIdType=\"autocomplete\"\n autocomplete=\"off\"\n [tnTestId]=\"testId()\"\n [class.open]=\"isOpen()\"\n [class.disabled]=\"isDisabled()\"\n [placeholder]=\"placeholder()\"\n [disabled]=\"isDisabled()\"\n [value]=\"searchTerm()\"\n [attr.aria-label]=\"ariaLabel()\"\n [attr.aria-labelledby]=\"fieldAria.labelledby()\"\n [attr.aria-describedby]=\"fieldAria.describedby()\"\n [attr.aria-invalid]=\"fieldAria.invalid()\"\n [attr.aria-required]=\"fieldAria.required()\"\n [attr.aria-expanded]=\"isOpen()\"\n [attr.aria-autocomplete]=\"'list'\"\n [attr.aria-controls]=\"isOpen() ? uid + '-dropdown' : null\"\n [attr.aria-owns]=\"isOpen() ? uid + '-dropdown' : null\"\n [attr.aria-activedescendant]=\"highlightedIndex() >= 0 ? uid + '-option-' + highlightedIndex() : null\"\n (input)=\"onInput($event)\"\n (focus)=\"onFocus()\"\n (blur)=\"onBlur()\"\n (keydown)=\"onKeydown($event)\" />\n</div>\n\n<!-- Dropdown panel \u2014 portaled via CDK Overlay so it escapes parent\n overflow/clipping. Attach/detach (driven by open()/close()) controls\n visibility; the input keeps DOM focus throughout. -->\n<ng-template #dropdownTemplate>\n <div\n class=\"tn-autocomplete__dropdown\"\n [style.--tn-autocomplete-panel-max-height]=\"panelMaxHeightValue()\"\n (scroll)=\"onPanelScroll($event)\">\n\n <!-- Per ARIA, a listbox may only contain option/group children, so the\n loading and no-results rows live OUTSIDE it as live-region siblings.\n The listbox itself stays rendered (possibly empty) the whole time the\n panel is open, so the input's aria-controls always points at a real\n element \u2014 including the loading-with-no-results-yet state. -->\n <div role=\"listbox\" [attr.id]=\"uid + '-dropdown'\" [attr.aria-busy]=\"loading() ? true : null\">\n @if (hasResults()) {\n @for (option of filteredOptions(); track $index) {\n <div\n class=\"tn-autocomplete__option\"\n role=\"option\"\n tabindex=\"-1\"\n [class.highlighted]=\"highlightedIndex() === $index\"\n [class.disabled]=\"option.disabled\"\n [attr.id]=\"uid + '-option-' + $index\"\n [attr.aria-selected]=\"isOptionSelected(option)\"\n [attr.aria-disabled]=\"option.disabled ? true : null\"\n (mousedown)=\"$event.preventDefault()\"\n (click)=\"onOptionClick(option)\"\n (keydown.enter)=\"onOptionClick(option)\">\n {{ option.label }}\n </div>\n }\n }\n </div>\n\n <div role=\"status\">\n @if (loading()) {\n <div class=\"tn-autocomplete__loading\">\n <tn-spinner [diameter]=\"16\" [strokeWidth]=\"2\" [ariaLabel]=\"loadingText()\" />\n <span>{{ loadingText() }}</span>\n </div>\n } @else if (!hasResults()) {\n <div class=\"tn-autocomplete__no-results\">\n {{ noResultsText() }}\n </div>\n }\n </div>\n </div>\n</ng-template>\n", styles: [".tn-autocomplete{position:relative;width:100%;font-family:var(--tn-font-family-body, \"Inter\"),sans-serif}.tn-autocomplete__input{display:block;width:100%;min-height:2.5rem;padding:.5rem .75rem;background-color:var(--tn-bg1, #ffffff);border:1px solid var(--tn-lines, #d1d5db);border-radius:.375rem;color:var(--tn-fg1, #212529);font-size:1rem;font-family:inherit;outline:none;box-sizing:border-box;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out}.tn-autocomplete__input::placeholder{color:var(--tn-alt-fg1, #999)}.tn-autocomplete__input:hover:not(.disabled){border-color:var(--tn-primary, #007bff)}.tn-autocomplete__input:focus-visible,.tn-autocomplete__input.open{border-color:var(--tn-primary, #007bff);box-shadow:0 0 0 2px color-mix(in srgb,var(--tn-primary, #007bff) 25%,transparent)}.tn-autocomplete__input.disabled{background-color:var(--tn-alt-bg1, #f8f9fa);color:var(--tn-fg2, #6c757d);cursor:not-allowed;opacity:.6}.tn-autocomplete__dropdown{background-color:var(--tn-bg2, #f5f5f5);border:1px solid var(--tn-lines, #d1d5db);border-radius:.375rem;box-shadow:0 4px 6px -1px #0000001a,0 2px 4px -1px #0000000f;max-height:var(--tn-autocomplete-panel-max-height, 320px);overflow-y:auto;padding:.25rem 0}.tn-autocomplete__option{display:flex;align-items:center;padding:.5rem .75rem;cursor:pointer;color:var(--tn-fg1, #212529);transition:background-color .15s ease-in-out}.tn-autocomplete__option:hover:not(.disabled){background-color:var(--tn-alt-bg2, #f8f9fa)}.tn-autocomplete__option.highlighted{background-color:var(--tn-alt-bg1, #f8f9fa)}.tn-autocomplete__option.disabled{color:var(--tn-fg2, #6c757d);cursor:not-allowed;opacity:.6}.tn-autocomplete__no-results{padding:1rem .75rem;text-align:center;color:var(--tn-fg2, #6c757d);font-style:italic}@media(prefers-reduced-motion:reduce){.tn-autocomplete__input,.tn-autocomplete__option{transition:none}}.tn-autocomplete__loading{align-items:center;color:var(--tn-fg2, #6c757d);display:flex;font-size:.875rem;gap:.5rem;padding:.5rem .75rem}\n"], dependencies: [{ kind: "component", type: TnSpinnerComponent, selector: "tn-spinner", inputs: ["mode", "value", "diameter", "strokeWidth", "ariaLabel", "ariaLabelledby"] }, { kind: "directive", type: TnTestIdDirective, selector: "[tnTestId]", inputs: ["tnTestId", "tnTestIdType"] }] });
991
+ ], viewQueries: [{ propertyName: "inputEl", first: true, predicate: ["inputEl"], descendants: true, isSignal: true }, { propertyName: "dropdownTemplate", first: true, predicate: ["dropdownTemplate"], descendants: true, isSignal: true }], ngImport: i0, template: "<div class=\"tn-autocomplete\">\n <input\n #inputEl\n type=\"text\"\n class=\"tn-autocomplete__input\"\n role=\"combobox\"\n aria-haspopup=\"listbox\"\n tnTestIdType=\"autocomplete\"\n autocomplete=\"off\"\n [tnTestId]=\"resolvedTestId()\"\n [class.open]=\"isOpen()\"\n [class.disabled]=\"isDisabled()\"\n [placeholder]=\"placeholder()\"\n [disabled]=\"isDisabled()\"\n [value]=\"searchTerm()\"\n [attr.aria-label]=\"ariaLabel()\"\n [attr.aria-labelledby]=\"fieldAria.labelledby()\"\n [attr.aria-describedby]=\"fieldAria.describedby()\"\n [attr.aria-invalid]=\"fieldAria.invalid()\"\n [attr.aria-required]=\"fieldAria.required()\"\n [attr.aria-expanded]=\"isOpen()\"\n [attr.aria-autocomplete]=\"'list'\"\n [attr.aria-controls]=\"isOpen() ? uid + '-dropdown' : null\"\n [attr.aria-owns]=\"isOpen() ? uid + '-dropdown' : null\"\n [attr.aria-activedescendant]=\"highlightedIndex() >= 0 ? uid + '-option-' + highlightedIndex() : null\"\n (input)=\"onInput($event)\"\n (focus)=\"onFocus()\"\n (blur)=\"onBlur()\"\n (keydown)=\"onKeydown($event)\" />\n</div>\n\n<!-- Dropdown panel \u2014 portaled via CDK Overlay so it escapes parent\n overflow/clipping. Attach/detach (driven by open()/close()) controls\n visibility; the input keeps DOM focus throughout. -->\n<ng-template #dropdownTemplate>\n <div\n class=\"tn-autocomplete__dropdown\"\n [style.--tn-autocomplete-panel-max-height]=\"panelMaxHeightValue()\"\n (scroll)=\"onPanelScroll($event)\">\n\n <!-- Per ARIA, a listbox may only contain option/group children, so the\n loading and no-results rows live OUTSIDE it as live-region siblings.\n The listbox itself stays rendered (possibly empty) the whole time the\n panel is open, so the input's aria-controls always points at a real\n element \u2014 including the loading-with-no-results-yet state. -->\n <div role=\"listbox\" [attr.id]=\"uid + '-dropdown'\" [attr.aria-busy]=\"loading() ? true : null\">\n @if (hasResults()) {\n @for (option of filteredOptions(); track $index) {\n <div\n class=\"tn-autocomplete__option\"\n role=\"option\"\n tabindex=\"-1\"\n tnTestIdType=\"option\"\n [tnTestId]=\"optionTestIdParts(option)\"\n [class.highlighted]=\"highlightedIndex() === $index\"\n [class.disabled]=\"option.disabled\"\n [attr.id]=\"uid + '-option-' + $index\"\n [attr.aria-selected]=\"isOptionSelected(option)\"\n [attr.aria-disabled]=\"option.disabled ? true : null\"\n (mousedown)=\"$event.preventDefault()\"\n (click)=\"onOptionClick(option)\"\n (keydown.enter)=\"onOptionClick(option)\">\n {{ option.label }}\n </div>\n }\n }\n </div>\n\n <div role=\"status\">\n @if (loading()) {\n <div\n class=\"tn-autocomplete__loading\"\n tnTestIdType=\"autocomplete\"\n [tnTestId]=\"statusTestIdParts('loading')\">\n <tn-spinner [diameter]=\"16\" [strokeWidth]=\"2\" [ariaLabel]=\"loadingText()\" />\n <span>{{ loadingText() }}</span>\n </div>\n } @else if (!hasResults()) {\n <div\n class=\"tn-autocomplete__no-results\"\n tnTestIdType=\"autocomplete\"\n [tnTestId]=\"statusTestIdParts('no-results')\">\n {{ noResultsText() }}\n </div>\n }\n </div>\n </div>\n</ng-template>\n", styles: [".tn-autocomplete{position:relative;width:100%;font-family:var(--tn-font-family-body, \"Inter\"),sans-serif}.tn-autocomplete__input{display:block;width:100%;min-height:2.5rem;padding:.5rem .75rem;background-color:var(--tn-bg1, #ffffff);border:1px solid var(--tn-lines, #d1d5db);border-radius:.375rem;color:var(--tn-fg1, #212529);font-size:1rem;font-family:inherit;outline:none;box-sizing:border-box;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out}.tn-autocomplete__input::placeholder{color:var(--tn-alt-fg1, #999)}.tn-autocomplete__input:hover:not(.disabled){border-color:var(--tn-primary, #007bff)}.tn-autocomplete__input:focus-visible,.tn-autocomplete__input.open{border-color:var(--tn-primary, #007bff);box-shadow:0 0 0 2px color-mix(in srgb,var(--tn-primary, #007bff) 25%,transparent)}.tn-autocomplete__input.disabled{background-color:var(--tn-alt-bg1, #f8f9fa);color:var(--tn-fg2, #6c757d);cursor:not-allowed;opacity:.6}.tn-autocomplete__dropdown{background-color:var(--tn-bg2, #f5f5f5);border:1px solid var(--tn-lines, #d1d5db);border-radius:.375rem;box-shadow:0 4px 6px -1px #0000001a,0 2px 4px -1px #0000000f;max-height:var(--tn-autocomplete-panel-max-height, 320px);overflow-y:auto;padding:.25rem 0}.tn-autocomplete__option{display:flex;align-items:center;padding:.5rem .75rem;cursor:pointer;color:var(--tn-fg1, #212529);transition:background-color .15s ease-in-out}.tn-autocomplete__option:hover:not(.disabled){background-color:var(--tn-alt-bg2, #f8f9fa)}.tn-autocomplete__option.highlighted{background-color:var(--tn-alt-bg1, #f8f9fa)}.tn-autocomplete__option.disabled{color:var(--tn-fg2, #6c757d);cursor:not-allowed;opacity:.6}.tn-autocomplete__no-results{padding:1rem .75rem;text-align:center;color:var(--tn-fg2, #6c757d);font-style:italic}@media(prefers-reduced-motion:reduce){.tn-autocomplete__input,.tn-autocomplete__option{transition:none}}.tn-autocomplete__loading{align-items:center;color:var(--tn-fg2, #6c757d);display:flex;font-size:.875rem;gap:.5rem;padding:.5rem .75rem}\n"], dependencies: [{ kind: "component", type: TnSpinnerComponent, selector: "tn-spinner", inputs: ["mode", "value", "diameter", "strokeWidth", "ariaLabel", "ariaLabelledby"] }, { kind: "directive", type: TnTestIdDirective, selector: "[tnTestId]", inputs: ["tnTestId", "tnTestIdType"] }] });
931
992
  }
932
993
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnAutocompleteComponent, decorators: [{
933
994
  type: Component,
@@ -937,8 +998,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
937
998
  useExisting: forwardRef(() => TnAutocompleteComponent),
938
999
  multi: true,
939
1000
  },
940
- ], template: "<div class=\"tn-autocomplete\">\n <input\n #inputEl\n type=\"text\"\n class=\"tn-autocomplete__input\"\n role=\"combobox\"\n aria-haspopup=\"listbox\"\n tnTestIdType=\"autocomplete\"\n autocomplete=\"off\"\n [tnTestId]=\"testId()\"\n [class.open]=\"isOpen()\"\n [class.disabled]=\"isDisabled()\"\n [placeholder]=\"placeholder()\"\n [disabled]=\"isDisabled()\"\n [value]=\"searchTerm()\"\n [attr.aria-label]=\"ariaLabel()\"\n [attr.aria-labelledby]=\"fieldAria.labelledby()\"\n [attr.aria-describedby]=\"fieldAria.describedby()\"\n [attr.aria-invalid]=\"fieldAria.invalid()\"\n [attr.aria-required]=\"fieldAria.required()\"\n [attr.aria-expanded]=\"isOpen()\"\n [attr.aria-autocomplete]=\"'list'\"\n [attr.aria-controls]=\"isOpen() ? uid + '-dropdown' : null\"\n [attr.aria-owns]=\"isOpen() ? uid + '-dropdown' : null\"\n [attr.aria-activedescendant]=\"highlightedIndex() >= 0 ? uid + '-option-' + highlightedIndex() : null\"\n (input)=\"onInput($event)\"\n (focus)=\"onFocus()\"\n (blur)=\"onBlur()\"\n (keydown)=\"onKeydown($event)\" />\n</div>\n\n<!-- Dropdown panel \u2014 portaled via CDK Overlay so it escapes parent\n overflow/clipping. Attach/detach (driven by open()/close()) controls\n visibility; the input keeps DOM focus throughout. -->\n<ng-template #dropdownTemplate>\n <div\n class=\"tn-autocomplete__dropdown\"\n [style.--tn-autocomplete-panel-max-height]=\"panelMaxHeightValue()\"\n (scroll)=\"onPanelScroll($event)\">\n\n <!-- Per ARIA, a listbox may only contain option/group children, so the\n loading and no-results rows live OUTSIDE it as live-region siblings.\n The listbox itself stays rendered (possibly empty) the whole time the\n panel is open, so the input's aria-controls always points at a real\n element \u2014 including the loading-with-no-results-yet state. -->\n <div role=\"listbox\" [attr.id]=\"uid + '-dropdown'\" [attr.aria-busy]=\"loading() ? true : null\">\n @if (hasResults()) {\n @for (option of filteredOptions(); track $index) {\n <div\n class=\"tn-autocomplete__option\"\n role=\"option\"\n tabindex=\"-1\"\n [class.highlighted]=\"highlightedIndex() === $index\"\n [class.disabled]=\"option.disabled\"\n [attr.id]=\"uid + '-option-' + $index\"\n [attr.aria-selected]=\"isOptionSelected(option)\"\n [attr.aria-disabled]=\"option.disabled ? true : null\"\n (mousedown)=\"$event.preventDefault()\"\n (click)=\"onOptionClick(option)\"\n (keydown.enter)=\"onOptionClick(option)\">\n {{ option.label }}\n </div>\n }\n }\n </div>\n\n <div role=\"status\">\n @if (loading()) {\n <div class=\"tn-autocomplete__loading\">\n <tn-spinner [diameter]=\"16\" [strokeWidth]=\"2\" [ariaLabel]=\"loadingText()\" />\n <span>{{ loadingText() }}</span>\n </div>\n } @else if (!hasResults()) {\n <div class=\"tn-autocomplete__no-results\">\n {{ noResultsText() }}\n </div>\n }\n </div>\n </div>\n</ng-template>\n", styles: [".tn-autocomplete{position:relative;width:100%;font-family:var(--tn-font-family-body, \"Inter\"),sans-serif}.tn-autocomplete__input{display:block;width:100%;min-height:2.5rem;padding:.5rem .75rem;background-color:var(--tn-bg1, #ffffff);border:1px solid var(--tn-lines, #d1d5db);border-radius:.375rem;color:var(--tn-fg1, #212529);font-size:1rem;font-family:inherit;outline:none;box-sizing:border-box;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out}.tn-autocomplete__input::placeholder{color:var(--tn-alt-fg1, #999)}.tn-autocomplete__input:hover:not(.disabled){border-color:var(--tn-primary, #007bff)}.tn-autocomplete__input:focus-visible,.tn-autocomplete__input.open{border-color:var(--tn-primary, #007bff);box-shadow:0 0 0 2px color-mix(in srgb,var(--tn-primary, #007bff) 25%,transparent)}.tn-autocomplete__input.disabled{background-color:var(--tn-alt-bg1, #f8f9fa);color:var(--tn-fg2, #6c757d);cursor:not-allowed;opacity:.6}.tn-autocomplete__dropdown{background-color:var(--tn-bg2, #f5f5f5);border:1px solid var(--tn-lines, #d1d5db);border-radius:.375rem;box-shadow:0 4px 6px -1px #0000001a,0 2px 4px -1px #0000000f;max-height:var(--tn-autocomplete-panel-max-height, 320px);overflow-y:auto;padding:.25rem 0}.tn-autocomplete__option{display:flex;align-items:center;padding:.5rem .75rem;cursor:pointer;color:var(--tn-fg1, #212529);transition:background-color .15s ease-in-out}.tn-autocomplete__option:hover:not(.disabled){background-color:var(--tn-alt-bg2, #f8f9fa)}.tn-autocomplete__option.highlighted{background-color:var(--tn-alt-bg1, #f8f9fa)}.tn-autocomplete__option.disabled{color:var(--tn-fg2, #6c757d);cursor:not-allowed;opacity:.6}.tn-autocomplete__no-results{padding:1rem .75rem;text-align:center;color:var(--tn-fg2, #6c757d);font-style:italic}@media(prefers-reduced-motion:reduce){.tn-autocomplete__input,.tn-autocomplete__option{transition:none}}.tn-autocomplete__loading{align-items:center;color:var(--tn-fg2, #6c757d);display:flex;font-size:.875rem;gap:.5rem;padding:.5rem .75rem}\n"] }]
941
- }], ctorParameters: () => [], propDecorators: { options: [{ type: i0.Input, args: [{ isSignal: true, alias: "options", required: false }] }], compareWith: [{ type: i0.Input, args: [{ isSignal: true, alias: "compareWith", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], requireSelection: [{ type: i0.Input, args: [{ isSignal: true, alias: "requireSelection", required: false }] }], allowCustomValue: [{ type: i0.Input, args: [{ isSignal: true, alias: "allowCustomValue", required: false }] }], loading: [{ type: i0.Input, args: [{ isSignal: true, alias: "loading", required: false }] }], loadingText: [{ type: i0.Input, args: [{ isSignal: true, alias: "loadingText", required: false }] }], filterFn: [{ type: i0.Input, args: [{ isSignal: true, alias: "filterFn", required: false }] }], noResultsText: [{ type: i0.Input, args: [{ isSignal: true, alias: "noResultsText", required: false }] }], maxResults: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxResults", required: false }] }], panelMaxHeight: [{ type: i0.Input, args: [{ isSignal: true, alias: "panelMaxHeight", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], testId: [{ type: i0.Input, args: [{ isSignal: true, alias: "testId", required: false }] }], optionSelected: [{ type: i0.Output, args: ["optionSelected"] }], searchChange: [{ type: i0.Output, args: ["searchChange"] }], loadMore: [{ type: i0.Output, args: ["loadMore"] }], opened: [{ type: i0.Output, args: ["opened"] }], inputEl: [{ type: i0.ViewChild, args: ['inputEl', { isSignal: true }] }], dropdownTemplate: [{ type: i0.ViewChild, args: ['dropdownTemplate', { isSignal: true }] }] } });
1001
+ ], template: "<div class=\"tn-autocomplete\">\n <input\n #inputEl\n type=\"text\"\n class=\"tn-autocomplete__input\"\n role=\"combobox\"\n aria-haspopup=\"listbox\"\n tnTestIdType=\"autocomplete\"\n autocomplete=\"off\"\n [tnTestId]=\"resolvedTestId()\"\n [class.open]=\"isOpen()\"\n [class.disabled]=\"isDisabled()\"\n [placeholder]=\"placeholder()\"\n [disabled]=\"isDisabled()\"\n [value]=\"searchTerm()\"\n [attr.aria-label]=\"ariaLabel()\"\n [attr.aria-labelledby]=\"fieldAria.labelledby()\"\n [attr.aria-describedby]=\"fieldAria.describedby()\"\n [attr.aria-invalid]=\"fieldAria.invalid()\"\n [attr.aria-required]=\"fieldAria.required()\"\n [attr.aria-expanded]=\"isOpen()\"\n [attr.aria-autocomplete]=\"'list'\"\n [attr.aria-controls]=\"isOpen() ? uid + '-dropdown' : null\"\n [attr.aria-owns]=\"isOpen() ? uid + '-dropdown' : null\"\n [attr.aria-activedescendant]=\"highlightedIndex() >= 0 ? uid + '-option-' + highlightedIndex() : null\"\n (input)=\"onInput($event)\"\n (focus)=\"onFocus()\"\n (blur)=\"onBlur()\"\n (keydown)=\"onKeydown($event)\" />\n</div>\n\n<!-- Dropdown panel \u2014 portaled via CDK Overlay so it escapes parent\n overflow/clipping. Attach/detach (driven by open()/close()) controls\n visibility; the input keeps DOM focus throughout. -->\n<ng-template #dropdownTemplate>\n <div\n class=\"tn-autocomplete__dropdown\"\n [style.--tn-autocomplete-panel-max-height]=\"panelMaxHeightValue()\"\n (scroll)=\"onPanelScroll($event)\">\n\n <!-- Per ARIA, a listbox may only contain option/group children, so the\n loading and no-results rows live OUTSIDE it as live-region siblings.\n The listbox itself stays rendered (possibly empty) the whole time the\n panel is open, so the input's aria-controls always points at a real\n element \u2014 including the loading-with-no-results-yet state. -->\n <div role=\"listbox\" [attr.id]=\"uid + '-dropdown'\" [attr.aria-busy]=\"loading() ? true : null\">\n @if (hasResults()) {\n @for (option of filteredOptions(); track $index) {\n <div\n class=\"tn-autocomplete__option\"\n role=\"option\"\n tabindex=\"-1\"\n tnTestIdType=\"option\"\n [tnTestId]=\"optionTestIdParts(option)\"\n [class.highlighted]=\"highlightedIndex() === $index\"\n [class.disabled]=\"option.disabled\"\n [attr.id]=\"uid + '-option-' + $index\"\n [attr.aria-selected]=\"isOptionSelected(option)\"\n [attr.aria-disabled]=\"option.disabled ? true : null\"\n (mousedown)=\"$event.preventDefault()\"\n (click)=\"onOptionClick(option)\"\n (keydown.enter)=\"onOptionClick(option)\">\n {{ option.label }}\n </div>\n }\n }\n </div>\n\n <div role=\"status\">\n @if (loading()) {\n <div\n class=\"tn-autocomplete__loading\"\n tnTestIdType=\"autocomplete\"\n [tnTestId]=\"statusTestIdParts('loading')\">\n <tn-spinner [diameter]=\"16\" [strokeWidth]=\"2\" [ariaLabel]=\"loadingText()\" />\n <span>{{ loadingText() }}</span>\n </div>\n } @else if (!hasResults()) {\n <div\n class=\"tn-autocomplete__no-results\"\n tnTestIdType=\"autocomplete\"\n [tnTestId]=\"statusTestIdParts('no-results')\">\n {{ noResultsText() }}\n </div>\n }\n </div>\n </div>\n</ng-template>\n", styles: [".tn-autocomplete{position:relative;width:100%;font-family:var(--tn-font-family-body, \"Inter\"),sans-serif}.tn-autocomplete__input{display:block;width:100%;min-height:2.5rem;padding:.5rem .75rem;background-color:var(--tn-bg1, #ffffff);border:1px solid var(--tn-lines, #d1d5db);border-radius:.375rem;color:var(--tn-fg1, #212529);font-size:1rem;font-family:inherit;outline:none;box-sizing:border-box;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out}.tn-autocomplete__input::placeholder{color:var(--tn-alt-fg1, #999)}.tn-autocomplete__input:hover:not(.disabled){border-color:var(--tn-primary, #007bff)}.tn-autocomplete__input:focus-visible,.tn-autocomplete__input.open{border-color:var(--tn-primary, #007bff);box-shadow:0 0 0 2px color-mix(in srgb,var(--tn-primary, #007bff) 25%,transparent)}.tn-autocomplete__input.disabled{background-color:var(--tn-alt-bg1, #f8f9fa);color:var(--tn-fg2, #6c757d);cursor:not-allowed;opacity:.6}.tn-autocomplete__dropdown{background-color:var(--tn-bg2, #f5f5f5);border:1px solid var(--tn-lines, #d1d5db);border-radius:.375rem;box-shadow:0 4px 6px -1px #0000001a,0 2px 4px -1px #0000000f;max-height:var(--tn-autocomplete-panel-max-height, 320px);overflow-y:auto;padding:.25rem 0}.tn-autocomplete__option{display:flex;align-items:center;padding:.5rem .75rem;cursor:pointer;color:var(--tn-fg1, #212529);transition:background-color .15s ease-in-out}.tn-autocomplete__option:hover:not(.disabled){background-color:var(--tn-alt-bg2, #f8f9fa)}.tn-autocomplete__option.highlighted{background-color:var(--tn-alt-bg1, #f8f9fa)}.tn-autocomplete__option.disabled{color:var(--tn-fg2, #6c757d);cursor:not-allowed;opacity:.6}.tn-autocomplete__no-results{padding:1rem .75rem;text-align:center;color:var(--tn-fg2, #6c757d);font-style:italic}@media(prefers-reduced-motion:reduce){.tn-autocomplete__input,.tn-autocomplete__option{transition:none}}.tn-autocomplete__loading{align-items:center;color:var(--tn-fg2, #6c757d);display:flex;font-size:.875rem;gap:.5rem;padding:.5rem .75rem}\n"] }]
1002
+ }], ctorParameters: () => [], propDecorators: { options: [{ type: i0.Input, args: [{ isSignal: true, alias: "options", required: false }] }], compareWith: [{ type: i0.Input, args: [{ isSignal: true, alias: "compareWith", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], requireSelection: [{ type: i0.Input, args: [{ isSignal: true, alias: "requireSelection", required: false }] }], allowCustomValue: [{ type: i0.Input, args: [{ isSignal: true, alias: "allowCustomValue", required: false }] }], loading: [{ type: i0.Input, args: [{ isSignal: true, alias: "loading", required: false }] }], loadingText: [{ type: i0.Input, args: [{ isSignal: true, alias: "loadingText", required: false }] }], filterFn: [{ type: i0.Input, args: [{ isSignal: true, alias: "filterFn", required: false }] }], noResultsText: [{ type: i0.Input, args: [{ isSignal: true, alias: "noResultsText", required: false }] }], maxResults: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxResults", required: false }] }], panelMaxHeight: [{ type: i0.Input, args: [{ isSignal: true, alias: "panelMaxHeight", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], testId: [{ type: i0.Input, args: [{ isSignal: true, alias: "testId", required: false }] }], optionTestIdKey: [{ type: i0.Input, args: [{ isSignal: true, alias: "optionTestIdKey", required: false }] }], optionSelected: [{ type: i0.Output, args: ["optionSelected"] }], searchChange: [{ type: i0.Output, args: ["searchChange"] }], loadMore: [{ type: i0.Output, args: ["loadMore"] }], opened: [{ type: i0.Output, args: ["opened"] }], inputEl: [{ type: i0.ViewChild, args: ['inputEl', { isSignal: true }] }], dropdownTemplate: [{ type: i0.ViewChild, args: ['dropdownTemplate', { isSignal: true }] }] } });
942
1003
 
943
1004
  /**
944
1005
  * Harness for interacting with tn-autocomplete in tests.
@@ -8981,11 +9042,8 @@ class TnSelectComponent {
8981
9042
  }
8982
9043
  /**
8983
9044
  * Test-id segments for an option row, consumed by `[tnTestId]` with
8984
- * `tnTestIdType="option"`. The select's resolved base (explicit `testId`, else
8985
- * the bound control name) scopes each option so ids stay unique across selects:
8986
- * base `quick-filters` + option value `ssd` → `option-quick-filters-ssd`; with
8987
- * no base → `option-ssd`. The discriminator comes from `optionTestIdKey` when
8988
- * provided, else the option's primitive `value`, else its `label`.
9045
+ * `tnTestIdType="option"` — see {@link optionTestId} for the derivation
9046
+ * rules (shared with `tn-autocomplete`).
8989
9047
  */
8990
9048
  optionTestIdParts(option) {
8991
9049
  // The synthetic empty option gets a fixed `empty` discriminator — its
@@ -8994,18 +9052,7 @@ class TnSelectComponent {
8994
9052
  if (this.isEmptyOption(option)) {
8995
9053
  return scopeTestId(this.resolvedTestId(), 'empty');
8996
9054
  }
8997
- const extractor = this.optionTestIdKey();
8998
- let key;
8999
- if (extractor) {
9000
- key = extractor(option);
9001
- }
9002
- else if (typeof option.value === 'string' || typeof option.value === 'number') {
9003
- key = option.value;
9004
- }
9005
- else {
9006
- key = option.label;
9007
- }
9008
- return scopeTestId(this.resolvedTestId(), key);
9055
+ return optionTestId(this.resolvedTestId(), option, this.optionTestIdKey());
9009
9056
  }
9010
9057
  onChange = (_value) => { };
9011
9058
  onTouched = () => { };
@@ -11629,23 +11676,42 @@ class TnTreeFlattener {
11629
11676
  }
11630
11677
  /**
11631
11678
  * Data source for flat tree.
11679
+ *
11680
+ * Optionally supports client-side filtering and sorting: set `filterPredicate`
11681
+ * (and call `filter(query)`) and/or `sortComparer`. Both are opt-in; when unset
11682
+ * the datasource behaves exactly as before.
11632
11683
  */
11633
11684
  class TnTreeFlatDataSource extends DataSource {
11634
11685
  _treeControl;
11635
11686
  _treeFlattener;
11687
+ /** When set, `filter(query)` re-renders the tree from `filterPredicate(data, query)`. */
11688
+ filterPredicate;
11689
+ /** When set, incoming `data` is sorted (top level) before flattening. */
11690
+ sortComparer;
11636
11691
  _flattenedData = new BehaviorSubject([]);
11637
11692
  _expandedData = new BehaviorSubject([]);
11638
11693
  _data = new BehaviorSubject([]);
11694
+ _filteredData = new BehaviorSubject([]);
11695
+ filterValue = '';
11696
+ filterChanged$ = new BehaviorSubject('');
11697
+ disconnect$ = new Subject();
11639
11698
  constructor(_treeControl, _treeFlattener) {
11640
11699
  super();
11641
11700
  this._treeControl = _treeControl;
11642
11701
  this._treeFlattener = _treeFlattener;
11702
+ this.detectFilterChanges();
11643
11703
  }
11644
11704
  get data() { return this._data.value; }
11645
11705
  set data(value) {
11646
- this._data.next(value);
11647
- this._flattenedData.next(this._treeFlattener.flattenNodes(this.data));
11648
- this._treeControl.dataNodes = this._flattenedData.value;
11706
+ this._data.next(this.sortComparer ? [...value].sort(this.sortComparer) : value);
11707
+ if (this.filterValue && this.filterPredicate) {
11708
+ this._filteredData.next(this.filterPredicate(this.data, this.filterValue));
11709
+ }
11710
+ this.flattenData();
11711
+ }
11712
+ /** The currently filtered (unflattened) data; equals `data` until `filter()` is used. */
11713
+ get filteredData() {
11714
+ return this._filteredData.value;
11649
11715
  }
11650
11716
  connect() {
11651
11717
  return merge(this._treeControl.expansionModel.changed, this._flattenedData).pipe(map(() => {
@@ -11653,7 +11719,28 @@ class TnTreeFlatDataSource extends DataSource {
11653
11719
  return this._expandedData.value;
11654
11720
  }));
11655
11721
  }
11656
- disconnect() { }
11722
+ disconnect() {
11723
+ this.disconnect$.next();
11724
+ this.disconnect$.complete();
11725
+ }
11726
+ /** Filters the tree using `filterPredicate` (debounced). No-op until `filterPredicate` is set. */
11727
+ filter(query) {
11728
+ this.filterChanged$.next(query);
11729
+ }
11730
+ flattenData() {
11731
+ this._flattenedData.next(this._treeFlattener.flattenNodes(this.filterValue ? this._filteredData.value : this.data));
11732
+ this._treeControl.dataNodes = this._flattenedData.value;
11733
+ }
11734
+ detectFilterChanges() {
11735
+ this.filterChanged$.pipe(filter(() => !!this.filterPredicate), debounceTime(200), distinctUntilChanged(), takeUntil(this.disconnect$)).subscribe((changedValue) => {
11736
+ if (this.filterValue === changedValue || !this.filterPredicate) {
11737
+ return;
11738
+ }
11739
+ this.filterValue = changedValue;
11740
+ this._filteredData.next(this.filterPredicate(this.data, changedValue));
11741
+ this.flattenData();
11742
+ });
11743
+ }
11657
11744
  _getExpandedNodesWithLevel() {
11658
11745
  const expandedNodes = [];
11659
11746
  const flatNodes = this._flattenedData.value;
@@ -11725,26 +11812,24 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
11725
11812
  }, encapsulation: ViewEncapsulation.Emulated, changeDetection: ChangeDetectionStrategy.OnPush, template: "<!-- The `role=\"treeitem\"` (plus aria-level/aria-expanded) lives on the host\n `.tn-tree-node-wrapper`, so this inner div is a presentational container \u2014\n no role or aria-* here, to avoid exposing a second, nested treeitem to AT. -->\n<div class=\"tn-tree-node\"\n cdkTreeNodeToggle\n [class.tn-tree-node--expandable]=\"isExpandable\"\n [style.cursor]=\"isExpandable ? 'pointer' : 'default'\">\n \n <div class=\"tn-tree-node__content\">\n <!-- Arrow icon for expandable nodes -->\n @if (isExpandable) {\n <div\n class=\"tn-tree-node__toggle\"\n [class.tn-tree-node__toggle--expanded]=\"isExpanded\">\n <tn-icon\n library=\"mdi\"\n size=\"sm\"\n style=\"transition: transform 0.2s ease;\"\n [name]=\"isExpanded ? 'chevron-down' : 'chevron-right'\" />\n </div>\n }\n\n <!-- Spacer for non-expandable nodes -->\n @if (!isExpandable) {\n <div class=\"tn-tree-node__spacer\"></div>\n }\n \n <!-- Node content -->\n <div class=\"tn-tree-node__text\">\n <ng-content />\n </div>\n </div>\n</div>", styles: [":host{display:block}.tn-tree-node{border-bottom:1px solid var(--tn-lines);transition:background-color .2s ease}.tn-tree-node:hover{background-color:var(--tn-alt-bg2)}.tn-tree-node:last-child{border-bottom:none}.tn-tree-node--expandable{cursor:pointer}.tn-tree-node--expandable:hover{background-color:var(--tn-alt-bg2)}.tn-tree-node--expandable:active{background-color:var(--tn-alt-bg1)}.tn-tree-node__content{display:flex;align-items:center;gap:8px;min-height:48px;padding:12px 16px}.tn-tree-node__toggle{display:flex;align-items:center;justify-content:center;width:24px;height:24px;padding:0;border:none;background:none;color:var(--tn-fg2);cursor:pointer;border-radius:3px;transition:all .2s ease;flex-shrink:0}.tn-tree-node__toggle:hover{background-color:var(--tn-alt-bg2);color:var(--tn-fg1)}.tn-tree-node__toggle:focus{outline:2px solid var(--tn-primary);outline-offset:1px}.tn-tree-node__toggle svg{transition:transform .2s ease;transform:rotate(0)}.tn-tree-node__toggle--expanded svg{transform:rotate(90deg)}.tn-tree-node__spacer{width:24px;height:24px;flex-shrink:0}.tn-tree-node__text{flex:1;min-width:0;color:var(--tn-fg1)}.tn-tree-node__children{padding-left:24px}.tn-tree-invisible{display:none}\n"] }]
11726
11813
  }], ctorParameters: () => [] });
11727
11814
 
11728
- class TnTreeNodeOutletDirective {
11729
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnTreeNodeOutletDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
11730
- static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.1.0", type: TnTreeNodeOutletDirective, isStandalone: true, selector: "[tnTreeNodeOutlet]", hostDirectives: [{ directive: i2.CdkTreeNodeOutlet }], ngImport: i0 });
11731
- }
11732
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnTreeNodeOutletDirective, decorators: [{
11733
- type: Directive,
11734
- args: [{
11735
- selector: '[tnTreeNodeOutlet]',
11736
- standalone: true,
11737
- hostDirectives: [{
11738
- directive: CdkTreeNodeOutlet,
11739
- inputs: [],
11740
- outputs: []
11741
- }]
11742
- }]
11743
- }] });
11744
-
11745
11815
  class TnNestedTreeNodeComponent extends CdkNestedTreeNode {
11816
+ /**
11817
+ * Accessible label for the built-in toggle button. Pass a translated string;
11818
+ * defaults to the previous hardcoded value.
11819
+ */
11820
+ toggleAriaLabel = input('Toggle node', ...(ngDevMode ? [{ debugName: "toggleAriaLabel" }] : []));
11821
+ /** Semantic test-id base applied to the built-in toggle button (`button-` prefixed). */
11822
+ toggleTestId = input(...(ngDevMode ? [undefined, { debugName: "toggleTestId" }] : []));
11823
+ /**
11824
+ * Suppresses the built-in toggle button (and its alignment spacer) so the
11825
+ * consumer can render a fully custom toggle in the projected content.
11826
+ */
11827
+ hideToggle = input(false, { ...(ngDevMode ? { debugName: "hideToggle" } : {}), transform: booleanAttribute });
11828
+ changeDetectorRef = inject(ChangeDetectorRef);
11829
+ destroyed = false;
11746
11830
  constructor() {
11747
11831
  super(inject((ElementRef)), inject(CdkTree, { optional: true }), inject(CDK_TREE_NODE_OUTLET_NODE, { optional: true }), inject(ChangeDetectorRef, { optional: true }));
11832
+ inject(DestroyRef).onDestroy(() => this.destroyed = true);
11748
11833
  }
11749
11834
  /** The tree node's level in the tree */
11750
11835
  get level() {
@@ -11752,11 +11837,10 @@ class TnNestedTreeNodeComponent extends CdkNestedTreeNode {
11752
11837
  // Legacy treeControl approach
11753
11838
  return this._tree.treeControl.getLevel(this.data);
11754
11839
  }
11755
- else if (this._tree && 'getLevel' in this._tree && typeof this._tree.getLevel === 'function') {
11756
- // Modern childrenAccessor approach - use tree's getLevel method
11757
- return this._tree.getLevel(this.data);
11758
- }
11759
- return 0;
11840
+ // CdkTreeNode's own getter: levelAccessor when available, else the parent
11841
+ // node's aria-level from the DOM — which is what makes nested trees (whose
11842
+ // NestedTreeControl has no getLevel) report correct levels.
11843
+ return this._tree ? super.level : 0;
11760
11844
  }
11761
11845
  /** Whether the tree node is expandable */
11762
11846
  get isExpandable() {
@@ -11764,6 +11848,12 @@ class TnNestedTreeNodeComponent extends CdkNestedTreeNode {
11764
11848
  // Legacy treeControl approach
11765
11849
  return this._tree.treeControl.isExpandable(this.data);
11766
11850
  }
11851
+ else if (this._tree?.treeControl) {
11852
+ // NestedTreeControl leaves `isExpandable` undefined unless configured —
11853
+ // fall back to its getChildren (synchronous arrays only).
11854
+ const children = this._tree.treeControl.getChildren?.(this.data);
11855
+ return Array.isArray(children) && children.length > 0;
11856
+ }
11767
11857
  else if (this._tree && 'childrenAccessor' in this._tree && this._tree.childrenAccessor) {
11768
11858
  // Modern childrenAccessor approach
11769
11859
  const childrenAccessor = this._tree.childrenAccessor;
@@ -11788,24 +11878,330 @@ class TnNestedTreeNodeComponent extends CdkNestedTreeNode {
11788
11878
  }
11789
11879
  return false;
11790
11880
  }
11881
+ /**
11882
+ * CDK inserts child node views synchronously from `ngAfterContentInit`, i.e.
11883
+ * mid-change-detection. Because the outlet here lives in PROJECTED content
11884
+ * whose view container has already been traversed for the current pass, the
11885
+ * new views would miss their first check and trip dev-mode NG0100. Defer the
11886
+ * insertion to a microtask so the views are created outside change detection
11887
+ * and get a normal first check on the pass that `markForCheck` schedules.
11888
+ */
11889
+ updateChildrenNodes(children) {
11890
+ queueMicrotask(() => {
11891
+ if (this.destroyed) {
11892
+ return;
11893
+ }
11894
+ super.updateChildrenNodes(children);
11895
+ this.changeDetectorRef.markForCheck();
11896
+ });
11897
+ }
11898
+ /**
11899
+ * Toggles the node; with the Alt key held, toggles the node together with all
11900
+ * of its descendants. Replaces `cdkTreeNodeToggle` on the built-in button so
11901
+ * the Alt behavior is deterministic (a stacked template listener would run in
11902
+ * an unspecified order relative to the CDK toggle's own click handler).
11903
+ */
11904
+ toggleNode(event) {
11905
+ if (!this._tree) {
11906
+ return;
11907
+ }
11908
+ if ('altKey' in event && event.altKey) {
11909
+ this._tree.toggleDescendants(this.data);
11910
+ }
11911
+ else {
11912
+ this._tree.toggle(this.data);
11913
+ }
11914
+ this._tree._keyManager?.focusItem(this);
11915
+ }
11791
11916
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnNestedTreeNodeComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
11792
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.0", type: TnNestedTreeNodeComponent, isStandalone: true, selector: "tn-nested-tree-node", host: { attributes: { "role": "treeitem" }, properties: { "attr.aria-level": "level + 1", "attr.aria-expanded": "isExpandable ? isExpanded : null" }, classAttribute: "tn-nested-tree-node-wrapper" }, providers: [
11917
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.0", type: TnNestedTreeNodeComponent, isStandalone: true, selector: "tn-nested-tree-node", inputs: { toggleAriaLabel: { classPropertyName: "toggleAriaLabel", publicName: "toggleAriaLabel", isSignal: true, isRequired: false, transformFunction: null }, toggleTestId: { classPropertyName: "toggleTestId", publicName: "toggleTestId", isSignal: true, isRequired: false, transformFunction: null }, hideToggle: { classPropertyName: "hideToggle", publicName: "hideToggle", isSignal: true, isRequired: false, transformFunction: null } }, host: { attributes: { "role": "treeitem" }, properties: { "attr.aria-level": "level + 1", "attr.aria-expanded": "isExpandable ? isExpanded : null" }, classAttribute: "tn-nested-tree-node-wrapper" }, providers: [
11793
11918
  { provide: CdkNestedTreeNode, useExisting: TnNestedTreeNodeComponent },
11794
11919
  { provide: CdkTreeNode, useExisting: TnNestedTreeNodeComponent }
11795
- ], exportAs: ["tnNestedTreeNode"], usesInheritance: true, ngImport: i0, template: "<div class=\"tn-nested-tree-node__content\">\n <!-- Toggle button for expandable nodes (provided by component) -->\n @if (isExpandable) {\n <button\n class=\"tn-nested-tree-node__toggle\"\n cdkTreeNodeToggle\n type=\"button\"\n [class.tn-nested-tree-node__toggle--expanded]=\"isExpanded\"\n [attr.aria-label]=\"'Toggle node'\">\n <tn-icon\n library=\"mdi\"\n size=\"sm\"\n style=\"transition: transform 0.2s ease;\"\n [name]=\"isExpanded ? 'chevron-down' : 'chevron-right'\" />\n </button>\n }\n\n <!-- Spacer for non-expandable nodes to maintain alignment -->\n @if (!isExpandable) {\n <div class=\"tn-nested-tree-node__spacer\"></div>\n }\n\n <!-- Consumer content -->\n <ng-content />\n</div>\n\n<!-- Children container -->\n@if (isExpandable) {\n <div class=\"tn-nested-tree-node-container\" role=\"group\" [class.tn-tree-invisible]=\"!isExpanded\">\n <ng-content select=\"[slot=children]\" />\n </div>\n}", styles: [".tn-nested-tree-node-wrapper{display:block;width:100%}.tn-nested-tree-node{display:block;width:100%;font-family:var(--tn-font-family-body);font-size:1rem;line-height:1.4;color:var(--tn-fg1)}.tn-nested-tree-node--expandable .tn-nested-tree-node__content{cursor:pointer}.tn-nested-tree-node__content{display:flex;align-items:center;gap:8px;min-height:48px;padding:12px 16px;border-bottom:1px solid var(--tn-lines);transition:background-color .2s ease}.tn-nested-tree-node__content:hover{background-color:var(--tn-alt-bg2)}.tn-nested-tree-node__content:focus-within{background-color:var(--tn-alt-bg2);outline:2px solid var(--tn-primary);outline-offset:-2px}.tn-tree-invisible{display:none}.tn-nested-tree-node__toggle{display:flex;align-items:center;justify-content:center;width:24px;height:24px;margin-right:8px;padding:0;border:none;background:transparent;border-radius:4px;cursor:pointer;color:var(--tn-fg2);transition:background-color .2s ease,color .2s ease}.tn-nested-tree-node__toggle:hover{background-color:var(--tn-bg3);color:var(--tn-fg1)}.tn-nested-tree-node__toggle:focus{outline:2px solid var(--tn-primary);outline-offset:2px}.tn-nested-tree-node__toggle svg{transition:transform .2s ease}.tn-nested-tree-node__toggle--expanded svg{transform:rotate(90deg)}.tn-nested-tree-node__spacer{width:24px;height:24px;flex-shrink:0}.tn-nested-tree-node__text{flex:1;display:flex;align-items:center;gap:8px;min-width:0;color:var(--tn-fg1)}div.tn-nested-tree-node-container{padding-left:40px}@media(prefers-reduced-motion:reduce){.tn-nested-tree-node__toggle svg,.tn-nested-tree-node__content,.tn-nested-tree-node__children{transition:none}}@media(prefers-contrast:high){.tn-nested-tree-node__content{border:1px solid transparent}.tn-nested-tree-node__content:hover,.tn-nested-tree-node__content:focus-within{border-color:var(--tn-fg1)}.tn-nested-tree-node__toggle{border:1px solid var(--tn-fg2)}.tn-nested-tree-node__toggle:hover,.tn-nested-tree-node__toggle:focus{border-color:var(--tn-fg1)}}\n"], dependencies: [{ kind: "ngmodule", type: CdkTreeModule }, { kind: "directive", type: i2.CdkTreeNodeToggle, selector: "[cdkTreeNodeToggle]", inputs: ["cdkTreeNodeToggleRecursive"] }, { kind: "component", type: TnIconComponent, selector: "tn-icon", inputs: ["name", "size", "color", "tooltip", "ariaLabel", "library", "testId", "fullSize", "customSize"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
11920
+ ], exportAs: ["tnNestedTreeNode"], usesInheritance: true, hostDirectives: [{ directive: TnTestIdDirective, inputs: ["tnTestId", "testId"] }], ngImport: i0, template: "<div class=\"tn-nested-tree-node__content\">\n @if (!hideToggle()) {\n <!-- Toggle button for expandable nodes (provided by component).\n Alt+click / Alt+Enter toggles the node together with all descendants.\n tabindex=\"-1\" keeps it out of the tab order \u2014 the tree's key manager\n drives focus at the treeitem level, matching cdkTreeNodeToggle. -->\n @if (isExpandable) {\n <button\n class=\"tn-nested-tree-node__toggle\"\n type=\"button\"\n tabindex=\"-1\"\n tnTestIdType=\"button\"\n [tnTestId]=\"toggleTestId()\"\n [class.tn-nested-tree-node__toggle--expanded]=\"isExpanded\"\n [attr.aria-label]=\"toggleAriaLabel()\"\n (click)=\"toggleNode($event); $event.stopPropagation()\"\n (keydown.enter)=\"toggleNode($event); $event.preventDefault()\"\n (keydown.space)=\"toggleNode($event); $event.preventDefault()\">\n <tn-icon\n library=\"mdi\"\n size=\"sm\"\n style=\"transition: transform 0.2s ease;\"\n [name]=\"isExpanded ? 'chevron-down' : 'chevron-right'\" />\n </button>\n }\n\n <!-- Spacer for non-expandable nodes to maintain alignment -->\n @if (!isExpandable) {\n <div class=\"tn-nested-tree-node__spacer\"></div>\n }\n }\n\n <!-- Consumer content -->\n <ng-content />\n</div>\n\n<!-- Children container -->\n@if (isExpandable) {\n <div class=\"tn-nested-tree-node-container\" role=\"group\" [class.tn-tree-invisible]=\"!isExpanded\">\n <ng-content select=\"[slot=children]\" />\n </div>\n}\n", styles: [".tn-nested-tree-node-wrapper{display:block;width:100%}.tn-nested-tree-node{display:block;width:100%;font-family:var(--tn-font-family-body);font-size:1rem;line-height:1.4;color:var(--tn-fg1)}.tn-nested-tree-node--expandable .tn-nested-tree-node__content{cursor:pointer}.tn-nested-tree-node__content{display:flex;align-items:center;gap:8px;min-height:48px;padding:12px 16px;border-bottom:1px solid var(--tn-lines);transition:background-color .2s ease}.tn-nested-tree-node__content:hover{background-color:var(--tn-alt-bg2)}.tn-nested-tree-node__content:focus-within{background-color:var(--tn-alt-bg2);outline:2px solid var(--tn-primary);outline-offset:-2px}.tn-tree-invisible{display:none}.tn-nested-tree-node__toggle{display:flex;align-items:center;justify-content:center;width:24px;height:24px;margin-right:8px;padding:0;border:none;background:transparent;border-radius:4px;cursor:pointer;color:var(--tn-fg2);transition:background-color .2s ease,color .2s ease}.tn-nested-tree-node__toggle:hover{background-color:var(--tn-bg3);color:var(--tn-fg1)}.tn-nested-tree-node__toggle:focus{outline:2px solid var(--tn-primary);outline-offset:2px}.tn-nested-tree-node__toggle svg{transition:transform .2s ease}.tn-nested-tree-node__toggle--expanded svg{transform:rotate(90deg)}.tn-nested-tree-node__spacer{width:24px;height:24px;flex-shrink:0}.tn-nested-tree-node__text{flex:1;display:flex;align-items:center;gap:8px;min-width:0;color:var(--tn-fg1)}div.tn-nested-tree-node-container{padding-left:40px}@media(prefers-reduced-motion:reduce){.tn-nested-tree-node__toggle svg,.tn-nested-tree-node__content,.tn-nested-tree-node__children{transition:none}}@media(prefers-contrast:high){.tn-nested-tree-node__content{border:1px solid transparent}.tn-nested-tree-node__content:hover,.tn-nested-tree-node__content:focus-within{border-color:var(--tn-fg1)}.tn-nested-tree-node__toggle{border:1px solid var(--tn-fg2)}.tn-nested-tree-node__toggle:hover,.tn-nested-tree-node__toggle:focus{border-color:var(--tn-fg1)}}\n"], dependencies: [{ kind: "component", type: TnIconComponent, selector: "tn-icon", inputs: ["name", "size", "color", "tooltip", "ariaLabel", "library", "testId", "fullSize", "customSize"] }, { kind: "directive", type: TnTestIdDirective, selector: "[tnTestId]", inputs: ["tnTestId", "tnTestIdType"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
11796
11921
  }
11797
11922
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnNestedTreeNodeComponent, decorators: [{
11798
11923
  type: Component,
11799
- args: [{ selector: 'tn-nested-tree-node', standalone: true, imports: [CdkTreeModule, TnIconComponent, TnTreeNodeOutletDirective], exportAs: 'tnNestedTreeNode', providers: [
11924
+ args: [{ selector: 'tn-nested-tree-node', standalone: true, imports: [TnIconComponent, TnTestIdDirective], exportAs: 'tnNestedTreeNode', providers: [
11800
11925
  { provide: CdkNestedTreeNode, useExisting: TnNestedTreeNodeComponent },
11801
11926
  { provide: CdkTreeNode, useExisting: TnNestedTreeNodeComponent }
11802
- ], host: {
11927
+ ], hostDirectives: [{ directive: TnTestIdDirective, inputs: ['tnTestId: testId'] }], host: {
11803
11928
  'class': 'tn-nested-tree-node-wrapper',
11804
11929
  '[attr.aria-level]': 'level + 1',
11805
11930
  '[attr.aria-expanded]': 'isExpandable ? isExpanded : null',
11806
11931
  'role': 'treeitem'
11807
- }, encapsulation: ViewEncapsulation.Emulated, changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"tn-nested-tree-node__content\">\n <!-- Toggle button for expandable nodes (provided by component) -->\n @if (isExpandable) {\n <button\n class=\"tn-nested-tree-node__toggle\"\n cdkTreeNodeToggle\n type=\"button\"\n [class.tn-nested-tree-node__toggle--expanded]=\"isExpanded\"\n [attr.aria-label]=\"'Toggle node'\">\n <tn-icon\n library=\"mdi\"\n size=\"sm\"\n style=\"transition: transform 0.2s ease;\"\n [name]=\"isExpanded ? 'chevron-down' : 'chevron-right'\" />\n </button>\n }\n\n <!-- Spacer for non-expandable nodes to maintain alignment -->\n @if (!isExpandable) {\n <div class=\"tn-nested-tree-node__spacer\"></div>\n }\n\n <!-- Consumer content -->\n <ng-content />\n</div>\n\n<!-- Children container -->\n@if (isExpandable) {\n <div class=\"tn-nested-tree-node-container\" role=\"group\" [class.tn-tree-invisible]=\"!isExpanded\">\n <ng-content select=\"[slot=children]\" />\n </div>\n}", styles: [".tn-nested-tree-node-wrapper{display:block;width:100%}.tn-nested-tree-node{display:block;width:100%;font-family:var(--tn-font-family-body);font-size:1rem;line-height:1.4;color:var(--tn-fg1)}.tn-nested-tree-node--expandable .tn-nested-tree-node__content{cursor:pointer}.tn-nested-tree-node__content{display:flex;align-items:center;gap:8px;min-height:48px;padding:12px 16px;border-bottom:1px solid var(--tn-lines);transition:background-color .2s ease}.tn-nested-tree-node__content:hover{background-color:var(--tn-alt-bg2)}.tn-nested-tree-node__content:focus-within{background-color:var(--tn-alt-bg2);outline:2px solid var(--tn-primary);outline-offset:-2px}.tn-tree-invisible{display:none}.tn-nested-tree-node__toggle{display:flex;align-items:center;justify-content:center;width:24px;height:24px;margin-right:8px;padding:0;border:none;background:transparent;border-radius:4px;cursor:pointer;color:var(--tn-fg2);transition:background-color .2s ease,color .2s ease}.tn-nested-tree-node__toggle:hover{background-color:var(--tn-bg3);color:var(--tn-fg1)}.tn-nested-tree-node__toggle:focus{outline:2px solid var(--tn-primary);outline-offset:2px}.tn-nested-tree-node__toggle svg{transition:transform .2s ease}.tn-nested-tree-node__toggle--expanded svg{transform:rotate(90deg)}.tn-nested-tree-node__spacer{width:24px;height:24px;flex-shrink:0}.tn-nested-tree-node__text{flex:1;display:flex;align-items:center;gap:8px;min-width:0;color:var(--tn-fg1)}div.tn-nested-tree-node-container{padding-left:40px}@media(prefers-reduced-motion:reduce){.tn-nested-tree-node__toggle svg,.tn-nested-tree-node__content,.tn-nested-tree-node__children{transition:none}}@media(prefers-contrast:high){.tn-nested-tree-node__content{border:1px solid transparent}.tn-nested-tree-node__content:hover,.tn-nested-tree-node__content:focus-within{border-color:var(--tn-fg1)}.tn-nested-tree-node__toggle{border:1px solid var(--tn-fg2)}.tn-nested-tree-node__toggle:hover,.tn-nested-tree-node__toggle:focus{border-color:var(--tn-fg1)}}\n"] }]
11808
- }], ctorParameters: () => [] });
11932
+ }, encapsulation: ViewEncapsulation.Emulated, changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"tn-nested-tree-node__content\">\n @if (!hideToggle()) {\n <!-- Toggle button for expandable nodes (provided by component).\n Alt+click / Alt+Enter toggles the node together with all descendants.\n tabindex=\"-1\" keeps it out of the tab order \u2014 the tree's key manager\n drives focus at the treeitem level, matching cdkTreeNodeToggle. -->\n @if (isExpandable) {\n <button\n class=\"tn-nested-tree-node__toggle\"\n type=\"button\"\n tabindex=\"-1\"\n tnTestIdType=\"button\"\n [tnTestId]=\"toggleTestId()\"\n [class.tn-nested-tree-node__toggle--expanded]=\"isExpanded\"\n [attr.aria-label]=\"toggleAriaLabel()\"\n (click)=\"toggleNode($event); $event.stopPropagation()\"\n (keydown.enter)=\"toggleNode($event); $event.preventDefault()\"\n (keydown.space)=\"toggleNode($event); $event.preventDefault()\">\n <tn-icon\n library=\"mdi\"\n size=\"sm\"\n style=\"transition: transform 0.2s ease;\"\n [name]=\"isExpanded ? 'chevron-down' : 'chevron-right'\" />\n </button>\n }\n\n <!-- Spacer for non-expandable nodes to maintain alignment -->\n @if (!isExpandable) {\n <div class=\"tn-nested-tree-node__spacer\"></div>\n }\n }\n\n <!-- Consumer content -->\n <ng-content />\n</div>\n\n<!-- Children container -->\n@if (isExpandable) {\n <div class=\"tn-nested-tree-node-container\" role=\"group\" [class.tn-tree-invisible]=\"!isExpanded\">\n <ng-content select=\"[slot=children]\" />\n </div>\n}\n", styles: [".tn-nested-tree-node-wrapper{display:block;width:100%}.tn-nested-tree-node{display:block;width:100%;font-family:var(--tn-font-family-body);font-size:1rem;line-height:1.4;color:var(--tn-fg1)}.tn-nested-tree-node--expandable .tn-nested-tree-node__content{cursor:pointer}.tn-nested-tree-node__content{display:flex;align-items:center;gap:8px;min-height:48px;padding:12px 16px;border-bottom:1px solid var(--tn-lines);transition:background-color .2s ease}.tn-nested-tree-node__content:hover{background-color:var(--tn-alt-bg2)}.tn-nested-tree-node__content:focus-within{background-color:var(--tn-alt-bg2);outline:2px solid var(--tn-primary);outline-offset:-2px}.tn-tree-invisible{display:none}.tn-nested-tree-node__toggle{display:flex;align-items:center;justify-content:center;width:24px;height:24px;margin-right:8px;padding:0;border:none;background:transparent;border-radius:4px;cursor:pointer;color:var(--tn-fg2);transition:background-color .2s ease,color .2s ease}.tn-nested-tree-node__toggle:hover{background-color:var(--tn-bg3);color:var(--tn-fg1)}.tn-nested-tree-node__toggle:focus{outline:2px solid var(--tn-primary);outline-offset:2px}.tn-nested-tree-node__toggle svg{transition:transform .2s ease}.tn-nested-tree-node__toggle--expanded svg{transform:rotate(90deg)}.tn-nested-tree-node__spacer{width:24px;height:24px;flex-shrink:0}.tn-nested-tree-node__text{flex:1;display:flex;align-items:center;gap:8px;min-width:0;color:var(--tn-fg1)}div.tn-nested-tree-node-container{padding-left:40px}@media(prefers-reduced-motion:reduce){.tn-nested-tree-node__toggle svg,.tn-nested-tree-node__content,.tn-nested-tree-node__children{transition:none}}@media(prefers-contrast:high){.tn-nested-tree-node__content{border:1px solid transparent}.tn-nested-tree-node__content:hover,.tn-nested-tree-node__content:focus-within{border-color:var(--tn-fg1)}.tn-nested-tree-node__toggle{border:1px solid var(--tn-fg2)}.tn-nested-tree-node__toggle:hover,.tn-nested-tree-node__toggle:focus{border-color:var(--tn-fg1)}}\n"] }]
11933
+ }], ctorParameters: () => [], propDecorators: { toggleAriaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "toggleAriaLabel", required: false }] }], toggleTestId: [{ type: i0.Input, args: [{ isSignal: true, alias: "toggleTestId", required: false }] }], hideToggle: [{ type: i0.Input, args: [{ isSignal: true, alias: "hideToggle", required: false }] }] } });
11934
+
11935
+ class TnTreeNodeOutletDirective {
11936
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnTreeNodeOutletDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
11937
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.1.0", type: TnTreeNodeOutletDirective, isStandalone: true, selector: "[tnTreeNodeOutlet]", hostDirectives: [{ directive: i2.CdkTreeNodeOutlet }], ngImport: i0 });
11938
+ }
11939
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnTreeNodeOutletDirective, decorators: [{
11940
+ type: Directive,
11941
+ args: [{
11942
+ selector: '[tnTreeNodeOutlet]',
11943
+ standalone: true,
11944
+ hostDirectives: [{
11945
+ directive: CdkTreeNodeOutlet,
11946
+ inputs: [],
11947
+ outputs: []
11948
+ }]
11949
+ }]
11950
+ }] });
11951
+
11952
+ /**
11953
+ * Data source for nested trees (`tn-nested-tree-node`).
11954
+ *
11955
+ * Optionally supports client-side filtering and sorting: set `filterPredicate`
11956
+ * (and call `filter(query)`) and/or `sortComparer` (applied recursively through
11957
+ * `children`). Both are opt-in.
11958
+ */
11959
+ class TnNestedTreeDataSource extends DataSource {
11960
+ /** When set, `filter(query)` re-renders the tree from `filterPredicate(data, query)`. */
11961
+ filterPredicate;
11962
+ /** When set, incoming `data` is sorted recursively before rendering. */
11963
+ sortComparer;
11964
+ filterValue = '';
11965
+ filterChanged$ = new BehaviorSubject('');
11966
+ _data = new BehaviorSubject([]);
11967
+ _filteredData = new BehaviorSubject([]);
11968
+ disconnect$ = new Subject();
11969
+ get data() {
11970
+ return this._data.value;
11971
+ }
11972
+ set data(value) {
11973
+ this._data.next(this.sortComparer ? this.sort(value) : value);
11974
+ }
11975
+ constructor(initialData) {
11976
+ super();
11977
+ if (initialData) {
11978
+ this.data = initialData;
11979
+ }
11980
+ this.detectFilterChanges();
11981
+ }
11982
+ connect(collectionViewer) {
11983
+ return merge(collectionViewer.viewChange, this._data, this._filteredData).pipe(map(() => (this.filterValue ? this._filteredData.value : this.data)));
11984
+ }
11985
+ disconnect() {
11986
+ this.disconnect$.next();
11987
+ this.disconnect$.complete();
11988
+ }
11989
+ /** Filters the tree using `filterPredicate` (debounced). No-op until `filterPredicate` is set. */
11990
+ filter(query) {
11991
+ this.filterChanged$.next(query);
11992
+ }
11993
+ detectFilterChanges() {
11994
+ this.filterChanged$.pipe(filter(() => !!this.filterPredicate), debounceTime(200), distinctUntilChanged(), takeUntil(this.disconnect$)).subscribe((changedValue) => {
11995
+ if (this.filterValue === changedValue || !this.filterPredicate) {
11996
+ return;
11997
+ }
11998
+ this.filterValue = changedValue;
11999
+ this._filteredData.next(this.filterPredicate(this.data, changedValue));
12000
+ });
12001
+ }
12002
+ sort(value) {
12003
+ return [...value].sort(this.sortComparer).map((item) => {
12004
+ if (item.children?.length) {
12005
+ item.children = this.sort(item.children);
12006
+ }
12007
+ return item;
12008
+ });
12009
+ }
12010
+ }
12011
+
12012
+ /**
12013
+ * Creates a flat tree control for managing expansion state of flat tree structures.
12014
+ *
12015
+ * @param getLevel - Function that returns the level/depth of a node
12016
+ * @param isExpandable - Function that returns whether a node can be expanded
12017
+ * @param options - Optional configuration including trackBy function
12018
+ * @returns TnTreeExpansion instance for managing tree state
12019
+ *
12020
+ * @example
12021
+ * ```ts
12022
+ * const treeControl = createFlatTreeControl<DatasetDetails, string>(
12023
+ * (dataset) => (dataset?.name?.split('/')?.length || 0) - 1,
12024
+ * (dataset) => Number(dataset?.children?.length) > 0,
12025
+ * { trackBy: (dataset) => dataset.id }
12026
+ * );
12027
+ * ```
12028
+ */
12029
+ function createFlatTreeControl(getLevel, isExpandable, options) {
12030
+ return new FlatTreeControl(getLevel, isExpandable, options);
12031
+ }
12032
+ /**
12033
+ * Creates a nested tree control for managing expansion state of nested tree structures.
12034
+ *
12035
+ * @param getChildren - Function that returns the children of a node
12036
+ * @param options - Optional configuration including isExpandable and trackBy functions
12037
+ * @returns TnTreeExpansion instance for managing tree state
12038
+ *
12039
+ * @example
12040
+ * ```ts
12041
+ * const treeControl = createNestedTreeControl<VDevNode, string>(
12042
+ * (vdev) => vdev.children,
12043
+ * { trackBy: (vdev) => vdev.guid }
12044
+ * );
12045
+ * ```
12046
+ */
12047
+ function createNestedTreeControl(getChildren, options) {
12048
+ // NestedTreeControl leaves `isExpandable` undefined unless provided, which
12049
+ // reads as "every node is a leaf" — default it from getChildren (sync arrays).
12050
+ const isExpandable = options?.isExpandable
12051
+ ?? ((dataNode) => {
12052
+ const children = getChildren(dataNode);
12053
+ return Array.isArray(children) && children.length > 0;
12054
+ });
12055
+ return new NestedTreeControl(getChildren, { ...options, isExpandable });
12056
+ }
12057
+
12058
+ /**
12059
+ * Harness for interacting with a single `tn-tree-node` or `tn-nested-tree-node`.
12060
+ *
12061
+ * Levels are 0-based (root nodes are level 0), matching the values consumers pass
12062
+ * to tree controls — the DOM's 1-based `aria-level` is converted internally.
12063
+ */
12064
+ class TnTreeNodeHarness extends ComponentHarness {
12065
+ static hostSelector = '.tn-tree-node-wrapper, .tn-nested-tree-node-wrapper';
12066
+ /** The built-in nested-node toggle button (absent on flat nodes, leaves, and `hideToggle` nodes). */
12067
+ nestedToggle = this.locatorForOptional('.tn-nested-tree-node__toggle');
12068
+ /** The flat node's row, which acts as its toggle surface. */
12069
+ flatRow = this.locatorForOptional('.tn-tree-node');
12070
+ /**
12071
+ * Gets a `HarnessPredicate` that can be used to search for a tree node with
12072
+ * specific attributes.
12073
+ *
12074
+ * @param options Options for filtering which instances are considered a match.
12075
+ * @returns A `HarnessPredicate` configured with the given options.
12076
+ */
12077
+ static with(options = {}) {
12078
+ return new HarnessPredicate(TnTreeNodeHarness, options)
12079
+ .addOption('text', options.text, (harness, text) => HarnessPredicate.stringMatches(harness.getText(), text))
12080
+ .addOption('expanded', options.expanded, async (harness, expanded) => (await harness.isExpanded()) === expanded)
12081
+ .addOption('level', options.level, async (harness, level) => (await harness.getLevel()) === level);
12082
+ }
12083
+ /**
12084
+ * Gets the node's own text, excluding any descendant tree nodes and the
12085
+ * built-in toggle button.
12086
+ *
12087
+ * @returns Promise resolving to the node's text.
12088
+ */
12089
+ async getText() {
12090
+ return (await this.host()).text({
12091
+ exclude: '.tn-tree-node-wrapper, .tn-nested-tree-node-wrapper, button',
12092
+ });
12093
+ }
12094
+ /**
12095
+ * Gets the node's 0-based level (root nodes are 0).
12096
+ *
12097
+ * @returns Promise resolving to the level.
12098
+ */
12099
+ async getLevel() {
12100
+ const ariaLevel = await (await this.host()).getAttribute('aria-level');
12101
+ return Number(ariaLevel) - 1;
12102
+ }
12103
+ /**
12104
+ * Whether the node is expandable (advertises `aria-expanded`).
12105
+ *
12106
+ * @returns Promise resolving to true if the node is expandable.
12107
+ */
12108
+ async isExpandable() {
12109
+ return (await (await this.host()).getAttribute('aria-expanded')) !== null;
12110
+ }
12111
+ /**
12112
+ * Whether an expandable node is currently expanded.
12113
+ *
12114
+ * @returns Promise resolving to true if `aria-expanded="true"`.
12115
+ */
12116
+ async isExpanded() {
12117
+ return (await (await this.host()).getAttribute('aria-expanded')) === 'true';
12118
+ }
12119
+ /**
12120
+ * Toggles the node's expansion by clicking its toggle surface — the built-in
12121
+ * toggle button on a nested node, or the row itself on a flat node. Throws for
12122
+ * nested nodes rendered with `hideToggle` (interact with the custom toggle
12123
+ * directly in that case).
12124
+ */
12125
+ async toggle() {
12126
+ await (await this.getToggle()).click();
12127
+ }
12128
+ /**
12129
+ * Toggles the node together with all of its descendants (Alt+click on the
12130
+ * built-in nested-node toggle).
12131
+ */
12132
+ async toggleWithDescendants() {
12133
+ await (await this.getToggle()).click({ alt: true });
12134
+ }
12135
+ /**
12136
+ * Expands the node if it is expandable and currently collapsed.
12137
+ */
12138
+ async expand() {
12139
+ if ((await this.isExpandable()) && !(await this.isExpanded())) {
12140
+ await this.toggle();
12141
+ }
12142
+ }
12143
+ /**
12144
+ * Collapses the node if it is currently expanded.
12145
+ */
12146
+ async collapse() {
12147
+ if (await this.isExpanded()) {
12148
+ await this.toggle();
12149
+ }
12150
+ }
12151
+ async getToggle() {
12152
+ const toggle = (await this.nestedToggle()) ?? (await this.flatRow());
12153
+ if (!toggle) {
12154
+ throw new Error('Node has no toggle surface (nested node with hideToggle?)');
12155
+ }
12156
+ return toggle;
12157
+ }
12158
+ }
12159
+ /**
12160
+ * Harness for interacting with `tn-tree` in tests.
12161
+ *
12162
+ * @example
12163
+ * ```typescript
12164
+ * const tree = await loader.getHarness(TnTreeHarness);
12165
+ * const roots = await tree.getNodes({ level: 0 });
12166
+ * await roots[0].expand();
12167
+ * expect(await tree.getNodeTexts()).toContain('child');
12168
+ * ```
12169
+ */
12170
+ class TnTreeHarness extends ComponentHarness {
12171
+ static hostSelector = 'tn-tree';
12172
+ /**
12173
+ * Gets a `HarnessPredicate` that can be used to search for a tree with
12174
+ * specific attributes.
12175
+ *
12176
+ * @param options Options for filtering which instances are considered a match.
12177
+ * @returns A `HarnessPredicate` configured with the given options.
12178
+ */
12179
+ static with(options = {}) {
12180
+ return new HarnessPredicate(TnTreeHarness, options);
12181
+ }
12182
+ /**
12183
+ * Gets the harnesses for the tree's nodes (all levels, in document order).
12184
+ *
12185
+ * Note: nested trees render collapsed subtrees into the DOM (hidden via CSS),
12186
+ * so nodes under a collapsed parent are still returned — filter by `level` or
12187
+ * check the parent's `isExpanded()` when visibility matters.
12188
+ *
12189
+ * @param filter Optional filter to narrow the nodes.
12190
+ * @returns Promise resolving to the matching node harnesses.
12191
+ */
12192
+ async getNodes(filter = {}) {
12193
+ return this.locatorForAll(TnTreeNodeHarness.with(filter))();
12194
+ }
12195
+ /**
12196
+ * Gets the text of every node currently rendered, in document order.
12197
+ *
12198
+ * @returns Promise resolving to an array of node texts.
12199
+ */
12200
+ async getNodeTexts() {
12201
+ const nodes = await this.getNodes();
12202
+ return Promise.all(nodes.map((node) => node.getText()));
12203
+ }
12204
+ }
11809
12205
 
11810
12206
  /**
11811
12207
  * Renders a single virtual tree row.
@@ -18810,5 +19206,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
18810
19206
  * Generated bundle index. Do not edit.
18811
19207
  */
18812
19208
 
18813
- export { CommonShortcuts, DEFAULT_THEME, DiskIconComponent, DiskType, FileSizePipe, InputType, LIGHT_THEME, LabelMarkupPipe, LabelTextPipe, LinuxModifierKeys, LinuxShortcuts, ModifierKeys, QuickShortcuts, ShortcutBuilder, StripMntPrefixPipe, THEME_MAP, THEME_STORAGE_KEY, TN_FORM_FIELD_CONTEXT, TN_FORM_FIELD_ERRORS, TN_TABLE_PAGER_DEFAULT_LABELS, TN_TABLE_PAGER_LABELS, TN_TEST_ATTR, TN_THEME_DEFINITIONS, TnAutocompleteComponent, TnAutocompleteHarness, TnBannerActionDirective, TnBannerComponent, TnBannerHarness, TnBrandedSpinnerComponent, TnButtonComponent, TnButtonHarness, TnButtonToggleComponent, TnButtonToggleGroupComponent, TnButtonToggleGroupHarness, TnButtonToggleHarness, TnCalendarComponent, TnCalendarHeaderComponent, TnCardComponent, TnCardFooterActionsDirective, TnCardHeaderActionsDirective, TnCardHeaderDirective, TnCellDefDirective, TnCheckboxComponent, TnCheckboxHarness, TnCheckboxLabelDirective, TnChipComponent, TnChipHarness, TnChipInputComponent, TnChipInputHarness, TnConfirmDialogComponent, TnDateInputComponent, TnDateInputHarness, TnDateRangeInputComponent, TnDateRangeInputHarness, TnDetailRowDefDirective, TnDialog, TnDialogHarness, TnDialogShellComponent, TnDialogTesting, TnDividerComponent, TnDividerDirective, TnDrawerComponent, TnDrawerContainerComponent, TnDrawerContainerHarness, TnDrawerContentComponent, TnDrawerHarness, TnEmptyComponent, TnEmptyHarness, TnExpansionPanelComponent, TnExpansionPanelHarness, TnFileInputComponent, TnFileInputHarness, TnFilePickerComponent, TnFilePickerPopupComponent, TnFormFieldComponent, TnFormFieldHarness, TnFormSectionComponent, TnFormSectionHarness, TnHeaderCellDefDirective, TnIconButtonComponent, TnIconButtonHarness, TnIconComponent, TnIconHarness, TnIconRegistryService, TnIconTesting, TnInputComponent, TnInputDirective, TnInputHarness, TnKeyboardShortcutComponent, TnKeyboardShortcutService, TnListAvatarDirective, TnListComponent, TnListIconDirective, TnListItemComponent, TnListItemLineDirective, TnListItemPrimaryDirective, TnListItemSecondaryDirective, TnListItemTitleDirective, TnListItemTrailingDirective, TnListOptionComponent, TnListSubheaderComponent, TnMenuActivateHoverDirective, TnMenuComponent, TnMenuHarness, TnMenuItemComponent, TnMenuTesting, TnMenuTriggerDirective, TnMonthViewComponent, TnMultiYearViewComponent, TnNestedTreeNodeComponent, TnParticleProgressBarComponent, TnProgressBarComponent, TnRadioComponent, TnRadioHarness, TnSelectComponent, TnSelectHarness, TnSelectionListComponent, TnSidePanelActionDirective, TnSidePanelComponent, TnSidePanelHarness, TnSidePanelHeaderActionDirective, TnSlideToggleComponent, TnSlideToggleHarness, TnSliderComponent, TnSliderThumbDirective, TnSliderWithLabelDirective, TnSpinnerComponent, TnSpriteLoaderService, TnStepComponent, TnStepperComponent, TnStepperHarness, TnStepperNextDirective, TnStepperPreviousDirective, TnTabComponent, TnTabHarness, TnTabPanelComponent, TnTabPanelHarness, TnTableColumnDirective, TnTableComponent, TnTableHarness, TnTablePagerComponent, TnTablePagerHarness, TnTabsComponent, TnTabsHarness, TnTestIdDirective, TnTheme, TnThemeService, TnTimeInputComponent, TnToastComponent, TnToastMock, TnToastPosition, TnToastRef, TnToastService, TnToastTesting, TnToastType, TnTooltipComponent, TnTooltipDirective, TnTreeComponent, TnTreeFlatDataSource, TnTreeFlattener, TnTreeNodeComponent, TnTreeNodeOutletDirective, TnTreeVirtualScrollNodeOutletDirective, TnTreeVirtualScrollViewComponent, TnTreeVirtualScrollViewHarness, TruncatePathPipe, WindowsModifierKeys, WindowsShortcuts, composeTestId, controlTestId, createLucideLibrary, createShortcut, defaultSpriteBasePath, defaultSpriteConfigPath, defaultTreeItemSize, formatSize, injectTnFormFieldAria, kebabTestSegment, labelMarkupToHtml, labelMarkupToText, libIconMarker, parseLabelMarkup, parseSize, registerLucideIcons, scopeTestId, setupLucideIntegration, tnIconMarker, writeTestId };
19209
+ export { CommonShortcuts, DEFAULT_THEME, DiskIconComponent, DiskType, FileSizePipe, InputType, LIGHT_THEME, LabelMarkupPipe, LabelTextPipe, LinuxModifierKeys, LinuxShortcuts, ModifierKeys, QuickShortcuts, ShortcutBuilder, StripMntPrefixPipe, THEME_MAP, THEME_STORAGE_KEY, TN_FORM_FIELD_CONTEXT, TN_FORM_FIELD_ERRORS, TN_TABLE_PAGER_DEFAULT_LABELS, TN_TABLE_PAGER_LABELS, TN_TEST_ATTR, TN_THEME_DEFINITIONS, TnAutocompleteComponent, TnAutocompleteHarness, TnBannerActionDirective, TnBannerComponent, TnBannerHarness, TnBrandedSpinnerComponent, TnButtonComponent, TnButtonHarness, TnButtonToggleComponent, TnButtonToggleGroupComponent, TnButtonToggleGroupHarness, TnButtonToggleHarness, TnCalendarComponent, TnCalendarHeaderComponent, TnCardComponent, TnCardFooterActionsDirective, TnCardHeaderActionsDirective, TnCardHeaderDirective, TnCellDefDirective, TnCheckboxComponent, TnCheckboxHarness, TnCheckboxLabelDirective, TnChipComponent, TnChipHarness, TnChipInputComponent, TnChipInputHarness, TnConfirmDialogComponent, TnDateInputComponent, TnDateInputHarness, TnDateRangeInputComponent, TnDateRangeInputHarness, TnDetailRowDefDirective, TnDialog, TnDialogHarness, TnDialogShellComponent, TnDialogTesting, TnDividerComponent, TnDividerDirective, TnDrawerComponent, TnDrawerContainerComponent, TnDrawerContainerHarness, TnDrawerContentComponent, TnDrawerHarness, TnEmptyComponent, TnEmptyHarness, TnExpansionPanelComponent, TnExpansionPanelHarness, TnFileInputComponent, TnFileInputHarness, TnFilePickerComponent, TnFilePickerPopupComponent, TnFormFieldComponent, TnFormFieldHarness, TnFormSectionComponent, TnFormSectionHarness, TnHeaderCellDefDirective, TnIconButtonComponent, TnIconButtonHarness, TnIconComponent, TnIconHarness, TnIconRegistryService, TnIconTesting, TnInputComponent, TnInputDirective, TnInputHarness, TnKeyboardShortcutComponent, TnKeyboardShortcutService, TnListAvatarDirective, TnListComponent, TnListIconDirective, TnListItemComponent, TnListItemLineDirective, TnListItemPrimaryDirective, TnListItemSecondaryDirective, TnListItemTitleDirective, TnListItemTrailingDirective, TnListOptionComponent, TnListSubheaderComponent, TnMenuActivateHoverDirective, TnMenuComponent, TnMenuHarness, TnMenuItemComponent, TnMenuTesting, TnMenuTriggerDirective, TnMonthViewComponent, TnMultiYearViewComponent, TnNestedTreeDataSource, TnNestedTreeNodeComponent, TnParticleProgressBarComponent, TnProgressBarComponent, TnRadioComponent, TnRadioHarness, TnSelectComponent, TnSelectHarness, TnSelectionListComponent, TnSidePanelActionDirective, TnSidePanelComponent, TnSidePanelHarness, TnSidePanelHeaderActionDirective, TnSlideToggleComponent, TnSlideToggleHarness, TnSliderComponent, TnSliderThumbDirective, TnSliderWithLabelDirective, TnSpinnerComponent, TnSpriteLoaderService, TnStepComponent, TnStepperComponent, TnStepperHarness, TnStepperNextDirective, TnStepperPreviousDirective, TnTabComponent, TnTabHarness, TnTabPanelComponent, TnTabPanelHarness, TnTableColumnDirective, TnTableComponent, TnTableHarness, TnTablePagerComponent, TnTablePagerHarness, TnTabsComponent, TnTabsHarness, TnTestIdDirective, TnTheme, TnThemeService, TnTimeInputComponent, TnToastComponent, TnToastMock, TnToastPosition, TnToastRef, TnToastService, TnToastTesting, TnToastType, TnTooltipComponent, TnTooltipDirective, TnTreeComponent, TnTreeFlatDataSource, TnTreeFlattener, TnTreeHarness, TnTreeNodeComponent, TnTreeNodeHarness, TnTreeNodeOutletDirective, TnTreeVirtualScrollNodeOutletDirective, TnTreeVirtualScrollViewComponent, TnTreeVirtualScrollViewHarness, TruncatePathPipe, WindowsModifierKeys, WindowsShortcuts, composeTestId, controlTestId, createFlatTreeControl, createLucideLibrary, createNestedTreeControl, createShortcut, defaultSpriteBasePath, defaultSpriteConfigPath, defaultTreeItemSize, formatSize, injectTnFormFieldAria, kebabTestSegment, labelMarkupToHtml, labelMarkupToText, libIconMarker, optionTestId, parseLabelMarkup, parseSize, registerLucideIcons, scopeTestId, setupLucideIntegration, tnIconMarker, writeTestId };
18814
19210
  //# sourceMappingURL=truenas-ui-components.mjs.map