@truenas/ui-components 0.1.58 → 0.1.59

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.
@@ -1,6 +1,6 @@
1
1
  import * as i0 from '@angular/core';
2
- import { InjectionToken, inject, Renderer2, ElementRef, input, effect, Directive, output, viewChild, signal, computed, forwardRef, Component, model, afterNextRender, ChangeDetectionStrategy, Injectable, isDevMode, ViewEncapsulation, contentChildren, ViewContainerRef, contentChild, ChangeDetectorRef, HostListener, TemplateRef, DestroyRef, IterableDiffers, Pipe, untracked, ApplicationRef, EnvironmentInjector, createComponent, PLATFORM_ID } from '@angular/core';
3
- import * as i1$3 from '@angular/forms';
2
+ import { InjectionToken, inject, Renderer2, ElementRef, input, effect, Directive, output, viewChild, signal, computed, forwardRef, Component, model, afterNextRender, ChangeDetectionStrategy, Injectable, isDevMode, ViewEncapsulation, contentChildren, ViewContainerRef, contentChild, ChangeDetectorRef, HostListener, TemplateRef, DestroyRef, isSignal, untracked, IterableDiffers, Pipe, ApplicationRef, EnvironmentInjector, createComponent, PLATFORM_ID } from '@angular/core';
3
+ import * as i2 from '@angular/forms';
4
4
  import { NG_VALUE_ACCESSOR, FormsModule, NgControl } from '@angular/forms';
5
5
  import { ComponentHarness, HarnessPredicate } from '@angular/cdk/testing';
6
6
  import * as i1 from '@angular/cdk/a11y';
@@ -20,7 +20,7 @@ import { trigger, state, transition, style, animate } from '@angular/animations'
20
20
  import { SPACE, ENTER, END, HOME, DOWN_ARROW, UP_ARROW, RIGHT_ARROW, LEFT_ARROW } from '@angular/cdk/keycodes';
21
21
  import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed';
22
22
  import { SelectionModel, DataSource } from '@angular/cdk/collections';
23
- import * as i2 from '@angular/cdk/tree';
23
+ import * as i2$1 from '@angular/cdk/tree';
24
24
  import { CdkTree, CdkTreeModule, CdkTreeNode, CDK_TREE_NODE_OUTLET_NODE, CdkTreeNodeOutlet, CdkNestedTreeNode } from '@angular/cdk/tree';
25
25
  export { FlatTreeControl } from '@angular/cdk/tree';
26
26
  import { map } from 'rxjs/operators';
@@ -5255,43 +5255,144 @@ class TnSelectComponent {
5255
5255
  options = input([], ...(ngDevMode ? [{ debugName: "options" }] : []));
5256
5256
  optionGroups = input([], ...(ngDevMode ? [{ debugName: "optionGroups" }] : []));
5257
5257
  placeholder = input('Select an option', ...(ngDevMode ? [{ debugName: "placeholder" }] : []));
5258
+ /**
5259
+ * Accessible label for the select trigger. When set, this is used as the
5260
+ * trigger's `aria-label` instead of the visible `placeholder` — useful in
5261
+ * contexts (e.g. a pager's page-size dropdown) where the placeholder text
5262
+ * doesn't accurately describe the field's purpose to screen readers.
5263
+ */
5264
+ ariaLabel = input(undefined, ...(ngDevMode ? [{ debugName: "ariaLabel" }] : []));
5265
+ /**
5266
+ * Message shown inside the dropdown when no options (and no option groups)
5267
+ * are available. Defaults to the English `'No options available'`; consumers
5268
+ * with i18n requirements can pass a translated string.
5269
+ */
5270
+ noOptionsLabel = input('No options available', ...(ngDevMode ? [{ debugName: "noOptionsLabel" }] : []));
5258
5271
  disabled = input(false, ...(ngDevMode ? [{ debugName: "disabled" }] : []));
5259
5272
  testId = input('', ...(ngDevMode ? [{ debugName: "testId" }] : []));
5260
5273
  multiple = input(false, ...(ngDevMode ? [{ debugName: "multiple" }] : []));
5274
+ /**
5275
+ * Custom comparator for matching option values against the selected value(s).
5276
+ *
5277
+ * When the option values are objects, **provide this** — the built-in
5278
+ * fallback uses `JSON.stringify`, which is key-order dependent and can
5279
+ * produce false negatives for structurally equal objects. For primitives the
5280
+ * default identity check is fine.
5281
+ */
5261
5282
  compareWith = input(...(ngDevMode ? [undefined, { debugName: "compareWith" }] : []));
5262
5283
  selectionChange = output();
5263
5284
  /** Emits the full array of selected values after each toggle in multiple mode. */
5264
5285
  multiSelectionChange = output();
5265
5286
  // Internal state signals
5266
5287
  isOpen = signal(false, ...(ngDevMode ? [{ debugName: "isOpen" }] : []));
5288
+ dropdownPosition = signal('below', ...(ngDevMode ? [{ debugName: "dropdownPosition" }] : []));
5267
5289
  selectedValue = signal(null, ...(ngDevMode ? [{ debugName: "selectedValue" }] : []));
5268
5290
  selectedValues = signal([], ...(ngDevMode ? [{ debugName: "selectedValues" }] : []));
5291
+ /** Index into `flatOptions` of the keyboard-focused row (-1 when none). */
5292
+ focusedIndex = signal(-1, ...(ngDevMode ? [{ debugName: "focusedIndex" }] : []));
5269
5293
  formDisabled = signal(false, ...(ngDevMode ? [{ debugName: "formDisabled" }] : []));
5294
+ // Name of the CSS custom property that defines the dropdown's max-height
5295
+ // (set in select.component.scss). Reading it via getComputedStyle keeps the
5296
+ // flip-up heuristic in sync with the stylesheet — no duplicated constant.
5297
+ static DROPDOWN_MAX_HEIGHT_VAR = '--tn-select-dropdown-max-height';
5298
+ // Fallback used when getComputedStyle can't resolve the variable (older
5299
+ // browsers, jsdom in some test configs).
5300
+ static DROPDOWN_MAX_HEIGHT_FALLBACK = 200;
5301
+ // Per-instance suffix used to namespace DOM ids when `testId` is empty.
5302
+ // Without this, two `<tn-select>` elements with no testId would emit
5303
+ // colliding option/dropdown/group ids, breaking aria-activedescendant.
5304
+ // A random suffix is preferred over a monotonic counter so id values stay
5305
+ // stable from test file to test file (a counter would grow unpredictably
5306
+ // across suites and break snapshot tests).
5307
+ fallbackId = `auto-${Math.random().toString(36).slice(2, 10)}`;
5308
+ uniqueId = computed(() => this.testId() || this.fallbackId, ...(ngDevMode ? [{ debugName: "uniqueId" }] : []));
5270
5309
  // Computed disabled state (combines input and form state)
5271
5310
  isDisabled = computed(() => this.disabled() || this.formDisabled(), ...(ngDevMode ? [{ debugName: "isDisabled" }] : []));
5311
+ /**
5312
+ * Flattened option list (ungrouped + grouped, in render order). The keyboard
5313
+ * navigation walks this list — entries from disabled groups are kept but
5314
+ * marked disabled so the cursor skips over them correctly.
5315
+ */
5316
+ flatOptions = computed(() => {
5317
+ const flat = [...this.options()];
5318
+ for (const group of this.optionGroups()) {
5319
+ for (const opt of group.options) {
5320
+ flat.push({ ...opt, disabled: opt.disabled || group.disabled });
5321
+ }
5322
+ }
5323
+ return flat;
5324
+ }, ...(ngDevMode ? [{ debugName: "flatOptions" }] : []));
5325
+ /**
5326
+ * Starting flat-index of each option group, used by the template to
5327
+ * translate a (group, option) pair into the matching `flatOptions` index.
5328
+ */
5329
+ groupOffsets = computed(() => {
5330
+ const offsets = [];
5331
+ let offset = this.options().length;
5332
+ for (const group of this.optionGroups()) {
5333
+ offsets.push(offset);
5334
+ offset += group.options.length;
5335
+ }
5336
+ return offsets;
5337
+ }, ...(ngDevMode ? [{ debugName: "groupOffsets" }] : []));
5338
+ /** `aria-activedescendant` id for the focused option (or null). */
5339
+ activeOptionId = computed(() => {
5340
+ const idx = this.focusedIndex();
5341
+ if (idx < 0 || !this.isOpen()) {
5342
+ return null;
5343
+ }
5344
+ return this.optionId(idx);
5345
+ }, ...(ngDevMode ? [{ debugName: "activeOptionId" }] : []));
5272
5346
  onChange = (_value) => { };
5273
5347
  onTouched = () => { };
5274
5348
  elementRef = inject(ElementRef);
5275
5349
  cdr = inject(ChangeDetectorRef);
5350
+ triggerEl = viewChild('trigger', ...(ngDevMode ? [{ debugName: "triggerEl" }] : []));
5276
5351
  constructor() {
5277
- // Click-outside detection using effect
5352
+ // Click-outside detection. Cleanup is registered via the `onCleanup`
5353
+ // callback (returning a function from `effect()` does *not* register one) —
5354
+ // which fires both when the effect re-runs and when the component's
5355
+ // injector is destroyed, so the listener is removed even if the host is
5356
+ // torn down while the dropdown is still open. The `disposed` flag prevents
5357
+ // the listener from being attached at all if teardown races the deferred
5358
+ // setTimeout — without it, a cleanup that fires between the timeout firing
5359
+ // and addEventListener executing could leak a permanent listener.
5360
+ effect((onCleanup) => {
5361
+ if (!this.isOpen()) {
5362
+ return;
5363
+ }
5364
+ let disposed = false;
5365
+ const clickListener = (event) => {
5366
+ if (!this.elementRef.nativeElement.contains(event.target)) {
5367
+ // Click outside → close, but don't steal focus from whatever the
5368
+ // user clicked on.
5369
+ this.closeDropdown({ restoreFocus: false });
5370
+ }
5371
+ };
5372
+ // Deferred so the click that opened the dropdown doesn't immediately
5373
+ // close it on its bubble back up to the document.
5374
+ const timeoutId = setTimeout(() => {
5375
+ if (disposed) {
5376
+ return;
5377
+ }
5378
+ document.addEventListener('click', clickListener);
5379
+ }, 0);
5380
+ onCleanup(() => {
5381
+ disposed = true;
5382
+ clearTimeout(timeoutId);
5383
+ document.removeEventListener('click', clickListener);
5384
+ });
5385
+ });
5386
+ // When the dropdown opens, scroll the focused option into view.
5278
5387
  effect(() => {
5279
- if (this.isOpen()) {
5280
- const clickListener = (event) => {
5281
- if (!this.elementRef.nativeElement.contains(event.target)) {
5282
- this.closeDropdown();
5283
- }
5284
- };
5285
- // Add listener after a small delay to avoid immediate closure
5286
- setTimeout(() => {
5287
- document.addEventListener('click', clickListener);
5288
- }, 0);
5289
- // Cleanup function
5290
- return () => {
5291
- document.removeEventListener('click', clickListener);
5292
- };
5388
+ if (!this.isOpen()) {
5389
+ return;
5293
5390
  }
5294
- return undefined;
5391
+ const idx = this.focusedIndex();
5392
+ if (idx < 0) {
5393
+ return;
5394
+ }
5395
+ queueMicrotask(() => this.scrollFocusedIntoView());
5295
5396
  });
5296
5397
  }
5297
5398
  // ControlValueAccessor implementation
@@ -5322,14 +5423,107 @@ class TnSelectComponent {
5322
5423
  if (this.isDisabled()) {
5323
5424
  return;
5324
5425
  }
5325
- this.isOpen.set(!this.isOpen());
5326
- if (!this.isOpen()) {
5327
- this.onTouched();
5426
+ if (this.isOpen()) {
5427
+ this.closeDropdown();
5428
+ }
5429
+ else {
5430
+ this.openDropdown();
5328
5431
  }
5329
5432
  }
5330
- closeDropdown() {
5433
+ /**
5434
+ * Open the dropdown, seed the keyboard cursor on the currently-selected
5435
+ * option (or the first focusable one), and decide whether to flip up.
5436
+ */
5437
+ openDropdown() {
5438
+ if (this.isDisabled()) {
5439
+ return;
5440
+ }
5441
+ this.dropdownPosition.set(this.computeDropdownPosition());
5442
+ this.isOpen.set(true);
5443
+ this.focusedIndex.set(this.initialFocusIndex());
5444
+ }
5445
+ /**
5446
+ * Close the dropdown.
5447
+ *
5448
+ * @param restoreFocus When `true` (default), return focus to the trigger so
5449
+ * keyboard users land somewhere sensible. Pass `false` for click-outside
5450
+ * so we don't steal focus from the element the user just navigated to.
5451
+ */
5452
+ closeDropdown(options = {}) {
5453
+ const restoreFocus = options.restoreFocus ?? true;
5454
+ if (!this.isOpen()) {
5455
+ return;
5456
+ }
5331
5457
  this.isOpen.set(false);
5458
+ this.focusedIndex.set(-1);
5332
5459
  this.onTouched();
5460
+ if (restoreFocus) {
5461
+ this.triggerEl()?.nativeElement.focus({ preventScroll: true });
5462
+ }
5463
+ }
5464
+ /** Picks the initial focused-row index when the dropdown opens. */
5465
+ initialFocusIndex() {
5466
+ const flat = this.flatOptions();
5467
+ if (flat.length === 0) {
5468
+ return -1;
5469
+ }
5470
+ // Prefer the currently selected option (or first selected in multi mode).
5471
+ if (this.multiple()) {
5472
+ const values = this.selectedValues();
5473
+ if (values.length > 0) {
5474
+ const idx = flat.findIndex((opt) => values.some((v) => this.compareValues(v, opt.value)));
5475
+ if (idx >= 0 && !flat[idx].disabled) {
5476
+ return idx;
5477
+ }
5478
+ }
5479
+ }
5480
+ else {
5481
+ const value = this.selectedValue();
5482
+ if (value !== null && value !== undefined) {
5483
+ const idx = flat.findIndex((opt) => this.compareValues(opt.value, value));
5484
+ if (idx >= 0 && !flat[idx].disabled) {
5485
+ return idx;
5486
+ }
5487
+ }
5488
+ }
5489
+ // Otherwise the first non-disabled option.
5490
+ return flat.findIndex((opt) => !opt.disabled);
5491
+ }
5492
+ /**
5493
+ * Decide whether the dropdown should open above or below the trigger.
5494
+ * Opens above when there isn't enough space below the trigger AND there is
5495
+ * more space above — otherwise stays below. Falls back to `'below'` when no
5496
+ * trigger element is found yet.
5497
+ */
5498
+ computeDropdownPosition() {
5499
+ if (typeof window === 'undefined') {
5500
+ return 'below';
5501
+ }
5502
+ const trigger = this.elementRef.nativeElement.querySelector('.tn-select-trigger');
5503
+ if (!trigger) {
5504
+ return 'below';
5505
+ }
5506
+ const rect = trigger.getBoundingClientRect();
5507
+ const spaceBelow = window.innerHeight - rect.bottom;
5508
+ const spaceAbove = rect.top;
5509
+ if (spaceBelow < this.readDropdownMaxHeight(trigger) && spaceAbove > spaceBelow) {
5510
+ return 'above';
5511
+ }
5512
+ return 'below';
5513
+ }
5514
+ /**
5515
+ * Reads the dropdown's max-height from the CSS custom property set in
5516
+ * select.component.scss. Single source of truth for the flip-up threshold —
5517
+ * if the stylesheet changes, the heuristic follows automatically.
5518
+ */
5519
+ readDropdownMaxHeight(trigger) {
5520
+ const raw = getComputedStyle(trigger)
5521
+ .getPropertyValue(TnSelectComponent.DROPDOWN_MAX_HEIGHT_VAR)
5522
+ .trim();
5523
+ const parsed = parseFloat(raw);
5524
+ return Number.isFinite(parsed) && parsed > 0
5525
+ ? parsed
5526
+ : TnSelectComponent.DROPDOWN_MAX_HEIGHT_FALLBACK;
5333
5527
  }
5334
5528
  onOptionClick(option, groupDisabled = false) {
5335
5529
  if (option.disabled || groupDisabled) {
@@ -5373,7 +5567,11 @@ class TnSelectComponent {
5373
5567
  }
5374
5568
  return this.compareValues(this.selectedValue(), option.value);
5375
5569
  }
5376
- getDisplayText = computed(() => {
5570
+ /** Build a stable DOM id for the option at `index` for aria-activedescendant. */
5571
+ optionId(index) {
5572
+ return `tn-select-${this.uniqueId()}-option-${index}`;
5573
+ }
5574
+ displayText = computed(() => {
5377
5575
  if (this.multiple()) {
5378
5576
  const values = this.selectedValues();
5379
5577
  if (values.length === 0) {
@@ -5390,7 +5588,7 @@ class TnSelectComponent {
5390
5588
  }
5391
5589
  const option = this.findOptionByValue(value);
5392
5590
  return option ? option.label : String(value);
5393
- }, ...(ngDevMode ? [{ debugName: "getDisplayText" }] : []));
5591
+ }, ...(ngDevMode ? [{ debugName: "displayText" }] : []));
5394
5592
  findOptionByValue(value) {
5395
5593
  // Search in regular options first
5396
5594
  const regularOption = this.options().find(opt => this.compareValues(opt.value, value));
@@ -5406,9 +5604,15 @@ class TnSelectComponent {
5406
5604
  }
5407
5605
  return undefined;
5408
5606
  }
5409
- hasAnyOptions = computed(() => {
5607
+ anyOptionsPresent = computed(() => {
5410
5608
  return this.options().length > 0 || this.optionGroups().length > 0;
5411
- }, ...(ngDevMode ? [{ debugName: "hasAnyOptions" }] : []));
5609
+ }, ...(ngDevMode ? [{ debugName: "anyOptionsPresent" }] : []));
5610
+ /**
5611
+ * Compares two option values for equality. Uses `compareWith` if provided,
5612
+ * otherwise identity (`===`). For object values it falls back to
5613
+ * `JSON.stringify`, which is key-order dependent — consumers with object
5614
+ * values should provide `compareWith` to avoid subtle bugs.
5615
+ */
5412
5616
  compareValues(a, b) {
5413
5617
  const customCompare = this.compareWith();
5414
5618
  if (customCompare) {
@@ -5422,17 +5626,31 @@ class TnSelectComponent {
5422
5626
  }
5423
5627
  return false;
5424
5628
  }
5425
- // Keyboard navigation
5426
- // TODO: Add ArrowUp/ArrowDown option navigation, Enter/Space toggle,
5427
- // and aria-activedescendant tracking for full keyboard accessibility.
5629
+ /**
5630
+ * Keyboard handling on the trigger (focus stays on the trigger while the
5631
+ * dropdown is open options use mousedown-preventDefault to avoid stealing
5632
+ * it). Implements the WAI-ARIA combobox pattern subset we need:
5633
+ *
5634
+ * - **Enter / Space**: open closed dropdown, or select the focused row
5635
+ * (toggle in multi-mode).
5636
+ * - **ArrowDown / ArrowUp**: move the focused row; opens the dropdown first
5637
+ * if it's closed.
5638
+ * - **Home / End**: jump to first / last focusable row (when open).
5639
+ * - **Escape**: close and restore focus to the trigger.
5640
+ * - **Tab**: close without preventing default so focus moves to the next
5641
+ * element naturally.
5642
+ */
5428
5643
  onKeydown(event) {
5429
5644
  switch (event.key) {
5430
5645
  case 'Enter':
5431
5646
  case ' ':
5432
- if (!this.isOpen()) {
5433
- this.toggleDropdown();
5434
- event.preventDefault();
5647
+ if (this.isOpen()) {
5648
+ this.selectFocused();
5649
+ }
5650
+ else {
5651
+ this.openDropdown();
5435
5652
  }
5653
+ event.preventDefault();
5436
5654
  break;
5437
5655
  case 'Escape':
5438
5656
  if (this.isOpen()) {
@@ -5442,20 +5660,114 @@ class TnSelectComponent {
5442
5660
  break;
5443
5661
  case 'ArrowDown':
5444
5662
  if (!this.isOpen()) {
5445
- this.toggleDropdown();
5663
+ this.openDropdown();
5664
+ }
5665
+ else {
5666
+ this.moveFocus(1);
5446
5667
  }
5447
5668
  event.preventDefault();
5448
5669
  break;
5670
+ case 'ArrowUp':
5671
+ if (!this.isOpen()) {
5672
+ this.openDropdown();
5673
+ }
5674
+ else {
5675
+ this.moveFocus(-1);
5676
+ }
5677
+ event.preventDefault();
5678
+ break;
5679
+ case 'Home':
5680
+ if (this.isOpen()) {
5681
+ this.moveFocusTo(0, 1);
5682
+ event.preventDefault();
5683
+ }
5684
+ break;
5685
+ case 'End':
5686
+ if (this.isOpen()) {
5687
+ this.moveFocusTo(this.flatOptions().length - 1, -1);
5688
+ event.preventDefault();
5689
+ }
5690
+ break;
5691
+ case 'Tab':
5692
+ // Let the browser move focus to the next element; just close.
5693
+ if (this.isOpen()) {
5694
+ this.closeDropdown({ restoreFocus: false });
5695
+ }
5696
+ break;
5697
+ }
5698
+ }
5699
+ /** Step the focused row by ±1 (or more), skipping disabled options. */
5700
+ moveFocus(delta) {
5701
+ const flat = this.flatOptions();
5702
+ if (flat.length === 0) {
5703
+ return;
5704
+ }
5705
+ let idx = this.focusedIndex();
5706
+ for (let i = 0; i < flat.length; i++) {
5707
+ idx = (idx + delta + flat.length) % flat.length;
5708
+ if (!flat[idx].disabled) {
5709
+ this.focusedIndex.set(idx);
5710
+ this.scrollFocusedIntoView();
5711
+ return;
5712
+ }
5713
+ }
5714
+ }
5715
+ /** Move focus to a specific index, scanning forward/backward to skip disabled. */
5716
+ moveFocusTo(start, step) {
5717
+ const flat = this.flatOptions();
5718
+ if (flat.length === 0) {
5719
+ return;
5720
+ }
5721
+ let idx = start;
5722
+ while (idx >= 0 && idx < flat.length) {
5723
+ if (!flat[idx].disabled) {
5724
+ this.focusedIndex.set(idx);
5725
+ this.scrollFocusedIntoView();
5726
+ return;
5727
+ }
5728
+ idx += step;
5729
+ }
5730
+ }
5731
+ /** Select (or toggle, in multi-mode) the currently keyboard-focused row. */
5732
+ selectFocused() {
5733
+ const idx = this.focusedIndex();
5734
+ const flat = this.flatOptions();
5735
+ if (idx < 0 || idx >= flat.length) {
5736
+ return;
5737
+ }
5738
+ const opt = flat[idx];
5739
+ if (opt.disabled) {
5740
+ return;
5741
+ }
5742
+ if (this.multiple()) {
5743
+ this.toggleOption(opt);
5744
+ }
5745
+ else {
5746
+ this.selectOption(opt);
5449
5747
  }
5450
5748
  }
5749
+ /** Scrolls the keyboard-focused option into view if it's outside the dropdown's viewport. */
5750
+ scrollFocusedIntoView() {
5751
+ const idx = this.focusedIndex();
5752
+ if (idx < 0) {
5753
+ return;
5754
+ }
5755
+ const host = this.elementRef.nativeElement;
5756
+ // Use attribute-equality instead of #id selectors so we don't need
5757
+ // CSS.escape (unavailable in jsdom for tests) — option ids may contain
5758
+ // characters that require escaping in #id form.
5759
+ const el = host.querySelector(`[id="${this.optionId(idx)}"]`);
5760
+ // jsdom doesn't implement scrollIntoView — guard so tests don't crash.
5761
+ el?.scrollIntoView?.({ block: 'nearest' });
5762
+ }
5451
5763
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnSelectComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
5452
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.0", type: TnSelectComponent, isStandalone: true, selector: "tn-select", inputs: { options: { classPropertyName: "options", publicName: "options", isSignal: true, isRequired: false, transformFunction: null }, optionGroups: { classPropertyName: "optionGroups", publicName: "optionGroups", 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 }, testId: { classPropertyName: "testId", publicName: "testId", isSignal: true, isRequired: false, transformFunction: null }, multiple: { classPropertyName: "multiple", publicName: "multiple", isSignal: true, isRequired: false, transformFunction: null }, compareWith: { classPropertyName: "compareWith", publicName: "compareWith", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { selectionChange: "selectionChange", multiSelectionChange: "multiSelectionChange" }, providers: [
5764
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.0", type: TnSelectComponent, isStandalone: true, selector: "tn-select", inputs: { options: { classPropertyName: "options", publicName: "options", isSignal: true, isRequired: false, transformFunction: null }, optionGroups: { classPropertyName: "optionGroups", publicName: "optionGroups", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null }, noOptionsLabel: { classPropertyName: "noOptionsLabel", publicName: "noOptionsLabel", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, testId: { classPropertyName: "testId", publicName: "testId", isSignal: true, isRequired: false, transformFunction: null }, multiple: { classPropertyName: "multiple", publicName: "multiple", isSignal: true, isRequired: false, transformFunction: null }, compareWith: { classPropertyName: "compareWith", publicName: "compareWith", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { selectionChange: "selectionChange", multiSelectionChange: "multiSelectionChange" }, providers: [
5453
5765
  {
5454
5766
  provide: NG_VALUE_ACCESSOR,
5455
5767
  useExisting: forwardRef(() => TnSelectComponent),
5456
5768
  multi: true
5457
5769
  }
5458
- ], ngImport: i0, template: "<div class=\"tn-select-container\" [tnTestId]=\"testId()\">\n <!-- Select Trigger -->\n <div\n class=\"tn-select-trigger\"\n role=\"combobox\"\n tabindex=\"0\"\n [class.disabled]=\"isDisabled()\"\n [class.open]=\"isOpen()\"\n [attr.aria-expanded]=\"isOpen()\"\n [attr.aria-haspopup]=\"true\"\n [attr.aria-controls]=\"isOpen() ? 'tn-select-dropdown-' + testId() : null\"\n [attr.aria-label]=\"placeholder()\"\n (click)=\"toggleDropdown()\"\n (keydown)=\"onKeydown($event)\">\n\n <!-- Display Text -->\n <span\n class=\"tn-select-text\"\n [class.placeholder]=\"multiple() ? selectedValues().length === 0 : (selectedValue() === null || selectedValue() === undefined)\">\n {{ getDisplayText() }}\n </span>\n\n <!-- Dropdown Arrow -->\n <div class=\"tn-select-arrow\" [class.open]=\"isOpen()\">\n <svg width=\"12\" height=\"12\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\">\n <polyline points=\"6,9 12,15 18,9\" />\n </svg>\n </div>\n </div>\n\n <!-- Dropdown Menu -->\n @if (isOpen()) {\n <div\n class=\"tn-select-dropdown\"\n role=\"listbox\"\n [attr.aria-multiselectable]=\"multiple()\"\n [attr.id]=\"'tn-select-dropdown-' + testId()\">\n\n <!-- Options List -->\n <div class=\"tn-select-options\">\n <!-- Regular Options -->\n @for (option of options(); track $index) {\n <div\n class=\"tn-select-option\"\n role=\"option\"\n [attr.tabindex]=\"option.disabled ? null : 0\"\n [class.selected]=\"isOptionSelected(option)\"\n [class.disabled]=\"option.disabled\"\n [attr.aria-selected]=\"isOptionSelected(option)\"\n [attr.aria-disabled]=\"option.disabled\"\n (mousedown)=\"$event.preventDefault()\"\n (click)=\"onOptionClick(option)\"\n (keydown.enter)=\"onOptionClick(option)\"\n (keydown.space)=\"onOptionClick(option)\">\n @if (multiple()) {\n <tn-checkbox\n class=\"tn-select-check\"\n label=\"\"\n [checked]=\"isOptionSelected(option)\"\n [disabled]=\"true\"\n [hideLabel]=\"true\" />\n }\n {{ option.label }}\n </div>\n }\n\n <!-- Option Groups -->\n @for (group of optionGroups(); track $index; let isFirst = $first) {\n <!-- Group Separator (not shown before first group if we have regular options) -->\n @if (!isFirst || options().length > 0) {\n <div\n class=\"tn-select-separator\"\n role=\"separator\">\n </div>\n }\n\n <div role=\"group\" [attr.aria-labelledby]=\"'tn-select-group-' + testId() + '-' + $index\">\n <!-- Group Label -->\n <div\n class=\"tn-select-group-label\"\n [attr.id]=\"'tn-select-group-' + testId() + '-' + $index\"\n [class.disabled]=\"group.disabled\">\n {{ group.label }}\n </div>\n\n <!-- Group Options -->\n @for (option of group.options; track $index) {\n <div\n class=\"tn-select-option\"\n role=\"option\"\n [attr.tabindex]=\"option.disabled || group.disabled ? null : 0\"\n [class.selected]=\"isOptionSelected(option)\"\n [class.disabled]=\"option.disabled || group.disabled\"\n [attr.aria-selected]=\"isOptionSelected(option)\"\n [attr.aria-disabled]=\"option.disabled || group.disabled\"\n (mousedown)=\"$event.preventDefault()\"\n (click)=\"onOptionClick(option, !!group.disabled)\"\n (keydown.enter)=\"onOptionClick(option, !!group.disabled)\"\n (keydown.space)=\"onOptionClick(option, !!group.disabled)\">\n @if (multiple()) {\n <tn-checkbox\n class=\"tn-select-check\"\n label=\"\"\n [checked]=\"isOptionSelected(option)\"\n [disabled]=\"true\"\n [hideLabel]=\"true\" />\n }\n {{ option.label }}\n </div>\n }\n </div>\n }\n\n <!-- No Options Message -->\n @if (!hasAnyOptions()) {\n <div class=\"tn-select-no-options\">\n No options available\n </div>\n }\n </div>\n </div>\n }\n</div>\n", styles: [".tn-select-container{position:relative;width:100%;font-family:var(--tn-font-family-body, \"Inter\"),sans-serif}.tn-select-label{display:block;margin-bottom:.5rem;font-size:.875rem;font-weight:500;color:var(--tn-fg1, #333);line-height:1.4}.tn-select-label.required .required-asterisk{color:var(--tn-error, #dc3545);margin-left:.25rem}.tn-select-trigger{position:relative;display:flex;align-items:center;min-height:2.5rem;padding:.5rem 2.5rem .5rem .75rem;background-color:var(--tn-bg1, #ffffff);border:1px solid var(--tn-lines, #d1d5db);border-radius:.375rem;cursor:pointer;transition:all .15s ease-in-out;outline:none;box-sizing:border-box}.tn-select-trigger:hover:not(.disabled){border-color:var(--tn-primary, #007bff)}.tn-select-trigger:focus-visible{border-color:var(--tn-primary, #007bff);box-shadow:0 0 0 2px #007bff40}.tn-select-trigger.open{border-color:var(--tn-primary, #007bff);box-shadow:0 0 0 2px #007bff40}.tn-select-trigger.error{border-color:var(--tn-error, #dc3545)}.tn-select-trigger.error:focus-visible,.tn-select-trigger.error.open{border-color:var(--tn-error, #dc3545);box-shadow:0 0 0 2px #dc354540}.tn-select-trigger.disabled{background-color:var(--tn-alt-bg1, #f8f9fa);color:var(--tn-fg2, #6c757d);cursor:not-allowed;opacity:.6}.tn-select-text{flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;color:var(--tn-fg1, #212529)}.tn-select-text.placeholder{color:var(--tn-alt-fg1, #999)}.tn-select-arrow{position:absolute;right:.75rem;top:50%;transform:translateY(-50%);color:var(--tn-fg2, #6c757d);transition:transform .15s ease-in-out;pointer-events:none}.tn-select-arrow.open{transform:translateY(-50%) rotate(180deg)}.tn-select-arrow svg{display:block}.tn-select-dropdown{position:absolute;top:100%;left:0;right:0;z-index:1000;margin-top:.25rem;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:200px;overflow:hidden}.tn-select-options{overflow-y:auto;padding:.25rem 0;max-height:200px}.tn-select-option{display:flex;align-items:center;padding:.5rem .75rem;cursor:pointer;color:var(--tn-fg1, #212529);transition:background-color .15s ease-in-out;pointer-events:auto;position:relative;z-index:1001}.tn-select-option:hover:not(.disabled){background-color:var(--tn-alt-bg2, #f8f9fa)!important}.tn-select-option.selected{background-color:var(--tn-alt-bg1, #f8f9fa)!important;color:var(--tn-fg1, #212529)}.tn-select-option.selected:hover{background-color:var(--tn-alt-bg2, #f8f9fa)!important}.tn-select-option.disabled{color:var(--tn-fg2, #6c757d);cursor:not-allowed;opacity:.6}.tn-select-check{margin-right:.5rem;flex-shrink:0;pointer-events:none}.tn-select-separator{height:1px;background:var(--tn-lines, #e0e0e0);margin:.25rem 0}.tn-select-group-label{padding:.375rem .75rem;font-size:.75rem;font-weight:600;color:var(--tn-alt-fg1, #9ca3af);text-transform:uppercase;letter-spacing:.05em;cursor:default;-webkit-user-select:none;user-select:none}.tn-select-group-label.disabled{opacity:.6}.tn-select-no-options{padding:1rem .75rem;text-align:center;color:var(--tn-fg2, #6c757d);font-style:italic}.tn-select-error{margin-top:.25rem;font-size:.75rem;color:var(--tn-error, #dc3545)}@media(prefers-reduced-motion:reduce){.tn-select-trigger,.tn-select-option,.tn-select-arrow{transition:none}}@media(prefers-contrast:high){.tn-select-trigger{border-width:2px}.tn-select-option.selected{outline:2px solid var(--tn-fg1, #000);outline-offset:-2px}}\n"], dependencies: [{ kind: "component", type: TnCheckboxComponent, selector: "tn-checkbox", inputs: ["label", "hideLabel", "disabled", "required", "indeterminate", "testId", "error", "checked"], outputs: ["change"] }, { kind: "directive", type: TnTestIdDirective, selector: "[tnTestId]", inputs: ["tnTestId"] }] });
5770
+ ], viewQueries: [{ propertyName: "triggerEl", first: true, predicate: ["trigger"], descendants: true, isSignal: true }], ngImport: i0, template: "<div class=\"tn-select-container\" [tnTestId]=\"testId()\">\n <!-- Select Trigger -->\n <div\n #trigger\n class=\"tn-select-trigger\"\n role=\"combobox\"\n [attr.tabindex]=\"isDisabled() ? -1 : 0\"\n [class.disabled]=\"isDisabled()\"\n [class.open]=\"isOpen()\"\n [attr.aria-expanded]=\"isOpen()\"\n [attr.aria-haspopup]=\"'listbox'\"\n [attr.aria-controls]=\"isOpen() ? 'tn-select-dropdown-' + uniqueId() : null\"\n [attr.aria-activedescendant]=\"activeOptionId()\"\n [attr.aria-disabled]=\"isDisabled()\"\n [attr.aria-label]=\"ariaLabel() ?? placeholder()\"\n (click)=\"toggleDropdown()\"\n (keydown)=\"onKeydown($event)\">\n\n <!-- Display Text -->\n <span\n class=\"tn-select-text\"\n [class.placeholder]=\"multiple() ? selectedValues().length === 0 : (selectedValue() === null || selectedValue() === undefined)\">\n {{ displayText() }}\n </span>\n\n <!-- Dropdown Arrow -->\n <div class=\"tn-select-arrow\" [class.open]=\"isOpen()\">\n <svg width=\"12\" height=\"12\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\">\n <polyline points=\"6,9 12,15 18,9\" />\n </svg>\n </div>\n </div>\n\n <!-- Dropdown Menu -->\n @if (isOpen()) {\n <div\n class=\"tn-select-dropdown\"\n role=\"listbox\"\n [class.tn-select-dropdown--above]=\"dropdownPosition() === 'above'\"\n [attr.aria-multiselectable]=\"multiple()\"\n [attr.id]=\"'tn-select-dropdown-' + uniqueId()\">\n\n <!-- Options List -->\n <!-- Options follow the WAI-ARIA combobox pattern: focus stays on the\n trigger, navigation is via the trigger's keydown handler and\n aria-activedescendant. Options handle click only, by design \u2014 they\n must not steal focus on mousedown. -->\n <div class=\"tn-select-options\">\n <!-- Regular Options -->\n @for (option of options(); track $index; let i = $index) {\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events, @angular-eslint/template/interactive-supports-focus -->\n <div\n class=\"tn-select-option\"\n role=\"option\"\n [id]=\"optionId(i)\"\n [class.selected]=\"isOptionSelected(option)\"\n [class.disabled]=\"option.disabled\"\n [class.focused]=\"focusedIndex() === i\"\n [attr.aria-selected]=\"isOptionSelected(option)\"\n [attr.aria-disabled]=\"option.disabled\"\n (mousedown)=\"$event.preventDefault()\"\n (click)=\"onOptionClick(option)\">\n @if (multiple()) {\n <tn-checkbox\n class=\"tn-select-check\"\n label=\"\"\n [checked]=\"isOptionSelected(option)\"\n [disabled]=\"true\"\n [hideLabel]=\"true\" />\n }\n {{ option.label }}\n </div>\n }\n\n <!-- Option Groups -->\n @for (group of optionGroups(); track $index; let groupIdx = $index; let isFirst = $first) {\n <!-- Group Separator (not shown before first group if we have regular options) -->\n @if (!isFirst || options().length > 0) {\n <div\n class=\"tn-select-separator\"\n role=\"separator\">\n </div>\n }\n\n <div role=\"group\" [attr.aria-labelledby]=\"'tn-select-group-' + uniqueId() + '-' + groupIdx\">\n <!-- Group Label -->\n <div\n class=\"tn-select-group-label\"\n [attr.id]=\"'tn-select-group-' + uniqueId() + '-' + groupIdx\"\n [class.disabled]=\"group.disabled\">\n {{ group.label }}\n </div>\n\n <!-- Group Options -->\n @for (option of group.options; track $index; let optIdx = $index) {\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events, @angular-eslint/template/interactive-supports-focus -->\n <div\n class=\"tn-select-option\"\n role=\"option\"\n [id]=\"optionId(groupOffsets()[groupIdx] + optIdx)\"\n [class.selected]=\"isOptionSelected(option)\"\n [class.disabled]=\"option.disabled || group.disabled\"\n [class.focused]=\"focusedIndex() === groupOffsets()[groupIdx] + optIdx\"\n [attr.aria-selected]=\"isOptionSelected(option)\"\n [attr.aria-disabled]=\"option.disabled || group.disabled\"\n (mousedown)=\"$event.preventDefault()\"\n (click)=\"onOptionClick(option, !!group.disabled)\">\n @if (multiple()) {\n <tn-checkbox\n class=\"tn-select-check\"\n label=\"\"\n [checked]=\"isOptionSelected(option)\"\n [disabled]=\"true\"\n [hideLabel]=\"true\" />\n }\n {{ option.label }}\n </div>\n }\n </div>\n }\n\n <!-- No Options Message -->\n @if (!anyOptionsPresent()) {\n <div class=\"tn-select-no-options\">\n {{ noOptionsLabel() }}\n </div>\n }\n </div>\n </div>\n }\n</div>\n", styles: [".tn-select-container{position:relative;width:100%;font-family:var(--tn-font-family-body, \"Inter\"),sans-serif;--tn-select-dropdown-max-height: 200px}.tn-select-label{display:block;margin-bottom:.5rem;font-size:.875rem;font-weight:500;color:var(--tn-fg1, #333);line-height:1.4}.tn-select-label.required .required-asterisk{color:var(--tn-error, #dc3545);margin-left:.25rem}.tn-select-trigger{position:relative;display:flex;align-items:center;min-height:2.5rem;padding:.5rem 2.5rem .5rem .75rem;background-color:var(--tn-bg1, #ffffff);border:1px solid var(--tn-lines, #d1d5db);border-radius:.375rem;cursor:pointer;transition:all .15s ease-in-out;outline:none;box-sizing:border-box}.tn-select-trigger:hover:not(.disabled){border-color:var(--tn-primary, #007bff)}.tn-select-trigger:focus-visible{border-color:var(--tn-primary, #007bff);box-shadow:0 0 0 2px #007bff40}.tn-select-trigger.open{border-color:var(--tn-primary, #007bff);box-shadow:0 0 0 2px #007bff40}.tn-select-trigger.error{border-color:var(--tn-error, #dc3545)}.tn-select-trigger.error:focus-visible,.tn-select-trigger.error.open{border-color:var(--tn-error, #dc3545);box-shadow:0 0 0 2px #dc354540}.tn-select-trigger.disabled{background-color:var(--tn-alt-bg1, #f8f9fa);color:var(--tn-fg2, #6c757d);cursor:not-allowed;opacity:.6}.tn-select-text{flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;color:var(--tn-fg1, #212529)}.tn-select-text.placeholder{color:var(--tn-alt-fg1, #999)}.tn-select-arrow{position:absolute;right:.75rem;top:50%;transform:translateY(-50%);color:var(--tn-fg2, #6c757d);transition:transform .15s ease-in-out;pointer-events:none}.tn-select-arrow.open{transform:translateY(-50%) rotate(180deg)}.tn-select-arrow svg{display:block}.tn-select-dropdown{position:absolute;top:100%;left:0;right:0;z-index:1000;margin-top:.25rem;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-select-dropdown-max-height);overflow:hidden}.tn-select-dropdown--above{top:auto;bottom:100%;margin-top:0;margin-bottom:.25rem}.tn-select-options{overflow-y:auto;padding:.25rem 0;max-height:var(--tn-select-dropdown-max-height)}.tn-select-option{display:flex;align-items:center;padding:.5rem .75rem;cursor:pointer;color:var(--tn-fg1, #212529);transition:background-color .15s ease-in-out;pointer-events:auto;position:relative;z-index:1001}.tn-select-option.selected{background-color:var(--tn-alt-bg1, #f8f9fa);color:var(--tn-fg1, #212529)}.tn-select-option:hover:not(.disabled):not(.focused){background-color:var(--tn-alt-bg2, #f8f9fa)}.tn-select-option.focused{background-color:var(--tn-alt-bg1, #f1f3f5);outline:2px solid var(--tn-primary, #007bff);outline-offset:-2px}.tn-select-option.disabled{color:var(--tn-fg2, #6c757d);cursor:not-allowed;opacity:.6}.tn-select-check{margin-right:.5rem;flex-shrink:0;pointer-events:none}.tn-select-separator{height:1px;background:var(--tn-lines, #e0e0e0);margin:.25rem 0}.tn-select-group-label{padding:.375rem .75rem;font-size:.75rem;font-weight:600;color:var(--tn-alt-fg1, #9ca3af);text-transform:uppercase;letter-spacing:.05em;cursor:default;-webkit-user-select:none;user-select:none}.tn-select-group-label.disabled{opacity:.6}.tn-select-no-options{padding:1rem .75rem;text-align:center;color:var(--tn-fg2, #6c757d);font-style:italic}.tn-select-error{margin-top:.25rem;font-size:.75rem;color:var(--tn-error, #dc3545)}@media(prefers-reduced-motion:reduce){.tn-select-trigger,.tn-select-option,.tn-select-arrow{transition:none}}@media(prefers-contrast:high){.tn-select-trigger{border-width:2px}.tn-select-option.selected{outline:2px solid var(--tn-fg1, #000);outline-offset:-2px}}\n"], dependencies: [{ kind: "component", type: TnCheckboxComponent, selector: "tn-checkbox", inputs: ["label", "hideLabel", "disabled", "required", "indeterminate", "testId", "error", "checked"], outputs: ["change"] }, { kind: "directive", type: TnTestIdDirective, selector: "[tnTestId]", inputs: ["tnTestId"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
5459
5771
  }
5460
5772
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnSelectComponent, decorators: [{
5461
5773
  type: Component,
@@ -5465,8 +5777,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
5465
5777
  useExisting: forwardRef(() => TnSelectComponent),
5466
5778
  multi: true
5467
5779
  }
5468
- ], template: "<div class=\"tn-select-container\" [tnTestId]=\"testId()\">\n <!-- Select Trigger -->\n <div\n class=\"tn-select-trigger\"\n role=\"combobox\"\n tabindex=\"0\"\n [class.disabled]=\"isDisabled()\"\n [class.open]=\"isOpen()\"\n [attr.aria-expanded]=\"isOpen()\"\n [attr.aria-haspopup]=\"true\"\n [attr.aria-controls]=\"isOpen() ? 'tn-select-dropdown-' + testId() : null\"\n [attr.aria-label]=\"placeholder()\"\n (click)=\"toggleDropdown()\"\n (keydown)=\"onKeydown($event)\">\n\n <!-- Display Text -->\n <span\n class=\"tn-select-text\"\n [class.placeholder]=\"multiple() ? selectedValues().length === 0 : (selectedValue() === null || selectedValue() === undefined)\">\n {{ getDisplayText() }}\n </span>\n\n <!-- Dropdown Arrow -->\n <div class=\"tn-select-arrow\" [class.open]=\"isOpen()\">\n <svg width=\"12\" height=\"12\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\">\n <polyline points=\"6,9 12,15 18,9\" />\n </svg>\n </div>\n </div>\n\n <!-- Dropdown Menu -->\n @if (isOpen()) {\n <div\n class=\"tn-select-dropdown\"\n role=\"listbox\"\n [attr.aria-multiselectable]=\"multiple()\"\n [attr.id]=\"'tn-select-dropdown-' + testId()\">\n\n <!-- Options List -->\n <div class=\"tn-select-options\">\n <!-- Regular Options -->\n @for (option of options(); track $index) {\n <div\n class=\"tn-select-option\"\n role=\"option\"\n [attr.tabindex]=\"option.disabled ? null : 0\"\n [class.selected]=\"isOptionSelected(option)\"\n [class.disabled]=\"option.disabled\"\n [attr.aria-selected]=\"isOptionSelected(option)\"\n [attr.aria-disabled]=\"option.disabled\"\n (mousedown)=\"$event.preventDefault()\"\n (click)=\"onOptionClick(option)\"\n (keydown.enter)=\"onOptionClick(option)\"\n (keydown.space)=\"onOptionClick(option)\">\n @if (multiple()) {\n <tn-checkbox\n class=\"tn-select-check\"\n label=\"\"\n [checked]=\"isOptionSelected(option)\"\n [disabled]=\"true\"\n [hideLabel]=\"true\" />\n }\n {{ option.label }}\n </div>\n }\n\n <!-- Option Groups -->\n @for (group of optionGroups(); track $index; let isFirst = $first) {\n <!-- Group Separator (not shown before first group if we have regular options) -->\n @if (!isFirst || options().length > 0) {\n <div\n class=\"tn-select-separator\"\n role=\"separator\">\n </div>\n }\n\n <div role=\"group\" [attr.aria-labelledby]=\"'tn-select-group-' + testId() + '-' + $index\">\n <!-- Group Label -->\n <div\n class=\"tn-select-group-label\"\n [attr.id]=\"'tn-select-group-' + testId() + '-' + $index\"\n [class.disabled]=\"group.disabled\">\n {{ group.label }}\n </div>\n\n <!-- Group Options -->\n @for (option of group.options; track $index) {\n <div\n class=\"tn-select-option\"\n role=\"option\"\n [attr.tabindex]=\"option.disabled || group.disabled ? null : 0\"\n [class.selected]=\"isOptionSelected(option)\"\n [class.disabled]=\"option.disabled || group.disabled\"\n [attr.aria-selected]=\"isOptionSelected(option)\"\n [attr.aria-disabled]=\"option.disabled || group.disabled\"\n (mousedown)=\"$event.preventDefault()\"\n (click)=\"onOptionClick(option, !!group.disabled)\"\n (keydown.enter)=\"onOptionClick(option, !!group.disabled)\"\n (keydown.space)=\"onOptionClick(option, !!group.disabled)\">\n @if (multiple()) {\n <tn-checkbox\n class=\"tn-select-check\"\n label=\"\"\n [checked]=\"isOptionSelected(option)\"\n [disabled]=\"true\"\n [hideLabel]=\"true\" />\n }\n {{ option.label }}\n </div>\n }\n </div>\n }\n\n <!-- No Options Message -->\n @if (!hasAnyOptions()) {\n <div class=\"tn-select-no-options\">\n No options available\n </div>\n }\n </div>\n </div>\n }\n</div>\n", styles: [".tn-select-container{position:relative;width:100%;font-family:var(--tn-font-family-body, \"Inter\"),sans-serif}.tn-select-label{display:block;margin-bottom:.5rem;font-size:.875rem;font-weight:500;color:var(--tn-fg1, #333);line-height:1.4}.tn-select-label.required .required-asterisk{color:var(--tn-error, #dc3545);margin-left:.25rem}.tn-select-trigger{position:relative;display:flex;align-items:center;min-height:2.5rem;padding:.5rem 2.5rem .5rem .75rem;background-color:var(--tn-bg1, #ffffff);border:1px solid var(--tn-lines, #d1d5db);border-radius:.375rem;cursor:pointer;transition:all .15s ease-in-out;outline:none;box-sizing:border-box}.tn-select-trigger:hover:not(.disabled){border-color:var(--tn-primary, #007bff)}.tn-select-trigger:focus-visible{border-color:var(--tn-primary, #007bff);box-shadow:0 0 0 2px #007bff40}.tn-select-trigger.open{border-color:var(--tn-primary, #007bff);box-shadow:0 0 0 2px #007bff40}.tn-select-trigger.error{border-color:var(--tn-error, #dc3545)}.tn-select-trigger.error:focus-visible,.tn-select-trigger.error.open{border-color:var(--tn-error, #dc3545);box-shadow:0 0 0 2px #dc354540}.tn-select-trigger.disabled{background-color:var(--tn-alt-bg1, #f8f9fa);color:var(--tn-fg2, #6c757d);cursor:not-allowed;opacity:.6}.tn-select-text{flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;color:var(--tn-fg1, #212529)}.tn-select-text.placeholder{color:var(--tn-alt-fg1, #999)}.tn-select-arrow{position:absolute;right:.75rem;top:50%;transform:translateY(-50%);color:var(--tn-fg2, #6c757d);transition:transform .15s ease-in-out;pointer-events:none}.tn-select-arrow.open{transform:translateY(-50%) rotate(180deg)}.tn-select-arrow svg{display:block}.tn-select-dropdown{position:absolute;top:100%;left:0;right:0;z-index:1000;margin-top:.25rem;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:200px;overflow:hidden}.tn-select-options{overflow-y:auto;padding:.25rem 0;max-height:200px}.tn-select-option{display:flex;align-items:center;padding:.5rem .75rem;cursor:pointer;color:var(--tn-fg1, #212529);transition:background-color .15s ease-in-out;pointer-events:auto;position:relative;z-index:1001}.tn-select-option:hover:not(.disabled){background-color:var(--tn-alt-bg2, #f8f9fa)!important}.tn-select-option.selected{background-color:var(--tn-alt-bg1, #f8f9fa)!important;color:var(--tn-fg1, #212529)}.tn-select-option.selected:hover{background-color:var(--tn-alt-bg2, #f8f9fa)!important}.tn-select-option.disabled{color:var(--tn-fg2, #6c757d);cursor:not-allowed;opacity:.6}.tn-select-check{margin-right:.5rem;flex-shrink:0;pointer-events:none}.tn-select-separator{height:1px;background:var(--tn-lines, #e0e0e0);margin:.25rem 0}.tn-select-group-label{padding:.375rem .75rem;font-size:.75rem;font-weight:600;color:var(--tn-alt-fg1, #9ca3af);text-transform:uppercase;letter-spacing:.05em;cursor:default;-webkit-user-select:none;user-select:none}.tn-select-group-label.disabled{opacity:.6}.tn-select-no-options{padding:1rem .75rem;text-align:center;color:var(--tn-fg2, #6c757d);font-style:italic}.tn-select-error{margin-top:.25rem;font-size:.75rem;color:var(--tn-error, #dc3545)}@media(prefers-reduced-motion:reduce){.tn-select-trigger,.tn-select-option,.tn-select-arrow{transition:none}}@media(prefers-contrast:high){.tn-select-trigger{border-width:2px}.tn-select-option.selected{outline:2px solid var(--tn-fg1, #000);outline-offset:-2px}}\n"] }]
5469
- }], ctorParameters: () => [], propDecorators: { options: [{ type: i0.Input, args: [{ isSignal: true, alias: "options", required: false }] }], optionGroups: [{ type: i0.Input, args: [{ isSignal: true, alias: "optionGroups", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], testId: [{ type: i0.Input, args: [{ isSignal: true, alias: "testId", required: false }] }], multiple: [{ type: i0.Input, args: [{ isSignal: true, alias: "multiple", required: false }] }], compareWith: [{ type: i0.Input, args: [{ isSignal: true, alias: "compareWith", required: false }] }], selectionChange: [{ type: i0.Output, args: ["selectionChange"] }], multiSelectionChange: [{ type: i0.Output, args: ["multiSelectionChange"] }] } });
5780
+ ], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"tn-select-container\" [tnTestId]=\"testId()\">\n <!-- Select Trigger -->\n <div\n #trigger\n class=\"tn-select-trigger\"\n role=\"combobox\"\n [attr.tabindex]=\"isDisabled() ? -1 : 0\"\n [class.disabled]=\"isDisabled()\"\n [class.open]=\"isOpen()\"\n [attr.aria-expanded]=\"isOpen()\"\n [attr.aria-haspopup]=\"'listbox'\"\n [attr.aria-controls]=\"isOpen() ? 'tn-select-dropdown-' + uniqueId() : null\"\n [attr.aria-activedescendant]=\"activeOptionId()\"\n [attr.aria-disabled]=\"isDisabled()\"\n [attr.aria-label]=\"ariaLabel() ?? placeholder()\"\n (click)=\"toggleDropdown()\"\n (keydown)=\"onKeydown($event)\">\n\n <!-- Display Text -->\n <span\n class=\"tn-select-text\"\n [class.placeholder]=\"multiple() ? selectedValues().length === 0 : (selectedValue() === null || selectedValue() === undefined)\">\n {{ displayText() }}\n </span>\n\n <!-- Dropdown Arrow -->\n <div class=\"tn-select-arrow\" [class.open]=\"isOpen()\">\n <svg width=\"12\" height=\"12\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\">\n <polyline points=\"6,9 12,15 18,9\" />\n </svg>\n </div>\n </div>\n\n <!-- Dropdown Menu -->\n @if (isOpen()) {\n <div\n class=\"tn-select-dropdown\"\n role=\"listbox\"\n [class.tn-select-dropdown--above]=\"dropdownPosition() === 'above'\"\n [attr.aria-multiselectable]=\"multiple()\"\n [attr.id]=\"'tn-select-dropdown-' + uniqueId()\">\n\n <!-- Options List -->\n <!-- Options follow the WAI-ARIA combobox pattern: focus stays on the\n trigger, navigation is via the trigger's keydown handler and\n aria-activedescendant. Options handle click only, by design \u2014 they\n must not steal focus on mousedown. -->\n <div class=\"tn-select-options\">\n <!-- Regular Options -->\n @for (option of options(); track $index; let i = $index) {\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events, @angular-eslint/template/interactive-supports-focus -->\n <div\n class=\"tn-select-option\"\n role=\"option\"\n [id]=\"optionId(i)\"\n [class.selected]=\"isOptionSelected(option)\"\n [class.disabled]=\"option.disabled\"\n [class.focused]=\"focusedIndex() === i\"\n [attr.aria-selected]=\"isOptionSelected(option)\"\n [attr.aria-disabled]=\"option.disabled\"\n (mousedown)=\"$event.preventDefault()\"\n (click)=\"onOptionClick(option)\">\n @if (multiple()) {\n <tn-checkbox\n class=\"tn-select-check\"\n label=\"\"\n [checked]=\"isOptionSelected(option)\"\n [disabled]=\"true\"\n [hideLabel]=\"true\" />\n }\n {{ option.label }}\n </div>\n }\n\n <!-- Option Groups -->\n @for (group of optionGroups(); track $index; let groupIdx = $index; let isFirst = $first) {\n <!-- Group Separator (not shown before first group if we have regular options) -->\n @if (!isFirst || options().length > 0) {\n <div\n class=\"tn-select-separator\"\n role=\"separator\">\n </div>\n }\n\n <div role=\"group\" [attr.aria-labelledby]=\"'tn-select-group-' + uniqueId() + '-' + groupIdx\">\n <!-- Group Label -->\n <div\n class=\"tn-select-group-label\"\n [attr.id]=\"'tn-select-group-' + uniqueId() + '-' + groupIdx\"\n [class.disabled]=\"group.disabled\">\n {{ group.label }}\n </div>\n\n <!-- Group Options -->\n @for (option of group.options; track $index; let optIdx = $index) {\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events, @angular-eslint/template/interactive-supports-focus -->\n <div\n class=\"tn-select-option\"\n role=\"option\"\n [id]=\"optionId(groupOffsets()[groupIdx] + optIdx)\"\n [class.selected]=\"isOptionSelected(option)\"\n [class.disabled]=\"option.disabled || group.disabled\"\n [class.focused]=\"focusedIndex() === groupOffsets()[groupIdx] + optIdx\"\n [attr.aria-selected]=\"isOptionSelected(option)\"\n [attr.aria-disabled]=\"option.disabled || group.disabled\"\n (mousedown)=\"$event.preventDefault()\"\n (click)=\"onOptionClick(option, !!group.disabled)\">\n @if (multiple()) {\n <tn-checkbox\n class=\"tn-select-check\"\n label=\"\"\n [checked]=\"isOptionSelected(option)\"\n [disabled]=\"true\"\n [hideLabel]=\"true\" />\n }\n {{ option.label }}\n </div>\n }\n </div>\n }\n\n <!-- No Options Message -->\n @if (!anyOptionsPresent()) {\n <div class=\"tn-select-no-options\">\n {{ noOptionsLabel() }}\n </div>\n }\n </div>\n </div>\n }\n</div>\n", styles: [".tn-select-container{position:relative;width:100%;font-family:var(--tn-font-family-body, \"Inter\"),sans-serif;--tn-select-dropdown-max-height: 200px}.tn-select-label{display:block;margin-bottom:.5rem;font-size:.875rem;font-weight:500;color:var(--tn-fg1, #333);line-height:1.4}.tn-select-label.required .required-asterisk{color:var(--tn-error, #dc3545);margin-left:.25rem}.tn-select-trigger{position:relative;display:flex;align-items:center;min-height:2.5rem;padding:.5rem 2.5rem .5rem .75rem;background-color:var(--tn-bg1, #ffffff);border:1px solid var(--tn-lines, #d1d5db);border-radius:.375rem;cursor:pointer;transition:all .15s ease-in-out;outline:none;box-sizing:border-box}.tn-select-trigger:hover:not(.disabled){border-color:var(--tn-primary, #007bff)}.tn-select-trigger:focus-visible{border-color:var(--tn-primary, #007bff);box-shadow:0 0 0 2px #007bff40}.tn-select-trigger.open{border-color:var(--tn-primary, #007bff);box-shadow:0 0 0 2px #007bff40}.tn-select-trigger.error{border-color:var(--tn-error, #dc3545)}.tn-select-trigger.error:focus-visible,.tn-select-trigger.error.open{border-color:var(--tn-error, #dc3545);box-shadow:0 0 0 2px #dc354540}.tn-select-trigger.disabled{background-color:var(--tn-alt-bg1, #f8f9fa);color:var(--tn-fg2, #6c757d);cursor:not-allowed;opacity:.6}.tn-select-text{flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;color:var(--tn-fg1, #212529)}.tn-select-text.placeholder{color:var(--tn-alt-fg1, #999)}.tn-select-arrow{position:absolute;right:.75rem;top:50%;transform:translateY(-50%);color:var(--tn-fg2, #6c757d);transition:transform .15s ease-in-out;pointer-events:none}.tn-select-arrow.open{transform:translateY(-50%) rotate(180deg)}.tn-select-arrow svg{display:block}.tn-select-dropdown{position:absolute;top:100%;left:0;right:0;z-index:1000;margin-top:.25rem;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-select-dropdown-max-height);overflow:hidden}.tn-select-dropdown--above{top:auto;bottom:100%;margin-top:0;margin-bottom:.25rem}.tn-select-options{overflow-y:auto;padding:.25rem 0;max-height:var(--tn-select-dropdown-max-height)}.tn-select-option{display:flex;align-items:center;padding:.5rem .75rem;cursor:pointer;color:var(--tn-fg1, #212529);transition:background-color .15s ease-in-out;pointer-events:auto;position:relative;z-index:1001}.tn-select-option.selected{background-color:var(--tn-alt-bg1, #f8f9fa);color:var(--tn-fg1, #212529)}.tn-select-option:hover:not(.disabled):not(.focused){background-color:var(--tn-alt-bg2, #f8f9fa)}.tn-select-option.focused{background-color:var(--tn-alt-bg1, #f1f3f5);outline:2px solid var(--tn-primary, #007bff);outline-offset:-2px}.tn-select-option.disabled{color:var(--tn-fg2, #6c757d);cursor:not-allowed;opacity:.6}.tn-select-check{margin-right:.5rem;flex-shrink:0;pointer-events:none}.tn-select-separator{height:1px;background:var(--tn-lines, #e0e0e0);margin:.25rem 0}.tn-select-group-label{padding:.375rem .75rem;font-size:.75rem;font-weight:600;color:var(--tn-alt-fg1, #9ca3af);text-transform:uppercase;letter-spacing:.05em;cursor:default;-webkit-user-select:none;user-select:none}.tn-select-group-label.disabled{opacity:.6}.tn-select-no-options{padding:1rem .75rem;text-align:center;color:var(--tn-fg2, #6c757d);font-style:italic}.tn-select-error{margin-top:.25rem;font-size:.75rem;color:var(--tn-error, #dc3545)}@media(prefers-reduced-motion:reduce){.tn-select-trigger,.tn-select-option,.tn-select-arrow{transition:none}}@media(prefers-contrast:high){.tn-select-trigger{border-width:2px}.tn-select-option.selected{outline:2px solid var(--tn-fg1, #000);outline-offset:-2px}}\n"] }]
5781
+ }], ctorParameters: () => [], propDecorators: { options: [{ type: i0.Input, args: [{ isSignal: true, alias: "options", required: false }] }], optionGroups: [{ type: i0.Input, args: [{ isSignal: true, alias: "optionGroups", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], noOptionsLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "noOptionsLabel", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], testId: [{ type: i0.Input, args: [{ isSignal: true, alias: "testId", required: false }] }], multiple: [{ type: i0.Input, args: [{ isSignal: true, alias: "multiple", required: false }] }], compareWith: [{ type: i0.Input, args: [{ isSignal: true, alias: "compareWith", required: false }] }], selectionChange: [{ type: i0.Output, args: ["selectionChange"] }], multiSelectionChange: [{ type: i0.Output, args: ["multiSelectionChange"] }], triggerEl: [{ type: i0.ViewChild, args: ['trigger', { isSignal: true }] }] } });
5470
5782
 
5471
5783
  /**
5472
5784
  * Harness for interacting with tn-select in tests.
@@ -6942,6 +7254,352 @@ class TnTableHarness extends ComponentHarness {
6942
7254
  }
6943
7255
  }
6944
7256
 
7257
+ /** English defaults used when no `TN_TABLE_PAGER_LABELS` provider is registered. */
7258
+ const TN_TABLE_PAGER_DEFAULT_LABELS = {
7259
+ itemsPerPage: 'Items per page',
7260
+ of: 'of',
7261
+ firstPage: 'First page',
7262
+ previousPage: 'Previous page',
7263
+ nextPage: 'Next page',
7264
+ lastPage: 'Last page',
7265
+ tablePagination: 'Table pagination',
7266
+ };
7267
+ /**
7268
+ * DI token for app-wide default labels. Provide either a static object or a
7269
+ * `Signal<TnTablePagerLabels>` — the latter lets the pager react to language
7270
+ * changes when the consumer wires it up to an i18n service.
7271
+ *
7272
+ * Explicit input bindings on `<tn-table-pager>` still win over these defaults.
7273
+ */
7274
+ const TN_TABLE_PAGER_LABELS = new InjectionToken('TN_TABLE_PAGER_LABELS', { providedIn: 'root', factory: () => TN_TABLE_PAGER_DEFAULT_LABELS });
7275
+ /**
7276
+ * Pagination control for the `tn-table` (or any list view that paginates).
7277
+ *
7278
+ * Works in two modes:
7279
+ *
7280
+ * 1. **Dumb mode** — bind `[currentPage]`, `[pageSize]`, `[totalItems]` and listen
7281
+ * to `pageChange` / `pageSizeChange`. The component owns no provider state.
7282
+ * 2. **Data-provider mode** — bind `[dataProvider]` and the component drives
7283
+ * `setPagination()` on the provider, mirrors `totalRows`, and reacts to
7284
+ * `currentPage$` changes (with an internal guard against feedback loops).
7285
+ *
7286
+ * @example Dumb mode
7287
+ * ```html
7288
+ * <tn-table-pager
7289
+ * [(currentPage)]="page"
7290
+ * [(pageSize)]="size"
7291
+ * [totalItems]="total()"
7292
+ * (pageChange)="loadPage($event)" />
7293
+ * ```
7294
+ *
7295
+ * @example Data-provider mode (replaces the typical ix-table-pager wrapper)
7296
+ * ```html
7297
+ * <tn-table-pager
7298
+ * [dataProvider]="dataProvider()"
7299
+ * [pageSize]="dataProvider().pagination.pageSize ?? 50"
7300
+ * [currentPage]="dataProvider().pagination.pageNumber ?? 1"
7301
+ * [itemsPerPageLabel]="'Items per page' | translate" />
7302
+ * ```
7303
+ */
7304
+ class TnTablePagerComponent {
7305
+ destroyRef = inject(DestroyRef);
7306
+ /**
7307
+ * Normalize the injected token into a Signal so consumers can supply either
7308
+ * a plain object or a reactive signal (e.g. derived from a TranslateService's
7309
+ * onLangChange) and the pager re-renders when labels change.
7310
+ */
7311
+ defaultLabels;
7312
+ /** 1-based index of the currently displayed page. */
7313
+ currentPage = model(1, ...(ngDevMode ? [{ debugName: "currentPage" }] : []));
7314
+ /** Number of items per page. */
7315
+ pageSize = model(50, ...(ngDevMode ? [{ debugName: "pageSize" }] : []));
7316
+ /** Selectable page-size values rendered in the dropdown. */
7317
+ pageSizeOptions = input([10, 20, 50, 100], ...(ngDevMode ? [{ debugName: "pageSizeOptions" }] : []));
7318
+ /**
7319
+ * Total item count across all pages — drives `totalPages` and the range
7320
+ * labels. Ignored when `dataProvider` is set (the provider's `totalRows`
7321
+ * wins, so consumers don't have to keep both in sync).
7322
+ */
7323
+ totalItems = input(0, ...(ngDevMode ? [{ debugName: "totalItems" }] : []));
7324
+ /**
7325
+ * Optional data-provider integration. When supplied, the pager initializes
7326
+ * the provider's pagination on the first effect run, mirrors `totalRows`
7327
+ * into the displayed total, and listens to `currentPage$` to sync external
7328
+ * page changes. Page/size changes from user input are pushed back via
7329
+ * `setPagination()`.
7330
+ */
7331
+ dataProvider = input(undefined, ...(ngDevMode ? [{ debugName: "dataProvider" }] : []));
7332
+ /**
7333
+ * Label inputs are nullable on purpose: the template reads the resolved
7334
+ * `*Label` computed signals below, which fall back to the DI-provided default
7335
+ * (a signal — so language changes propagate live). An explicit input binding
7336
+ * always wins.
7337
+ */
7338
+ itemsPerPageLabel = input(undefined, ...(ngDevMode ? [{ debugName: "itemsPerPageLabel" }] : []));
7339
+ ofLabel = input(undefined, ...(ngDevMode ? [{ debugName: "ofLabel" }] : []));
7340
+ firstPageLabel = input(undefined, ...(ngDevMode ? [{ debugName: "firstPageLabel" }] : []));
7341
+ previousPageLabel = input(undefined, ...(ngDevMode ? [{ debugName: "previousPageLabel" }] : []));
7342
+ nextPageLabel = input(undefined, ...(ngDevMode ? [{ debugName: "nextPageLabel" }] : []));
7343
+ lastPageLabel = input(undefined, ...(ngDevMode ? [{ debugName: "lastPageLabel" }] : []));
7344
+ tablePaginationLabel = input(undefined, ...(ngDevMode ? [{ debugName: "tablePaginationLabel" }] : []));
7345
+ /** Resolved labels: explicit input takes precedence over the DI default. */
7346
+ resolvedItemsPerPageLabel = computed(() => this.itemsPerPageLabel() ?? this.defaultLabels().itemsPerPage, ...(ngDevMode ? [{ debugName: "resolvedItemsPerPageLabel" }] : []));
7347
+ resolvedOfLabel = computed(() => this.ofLabel() ?? this.defaultLabels().of, ...(ngDevMode ? [{ debugName: "resolvedOfLabel" }] : []));
7348
+ resolvedFirstPageLabel = computed(() => this.firstPageLabel() ?? this.defaultLabels().firstPage, ...(ngDevMode ? [{ debugName: "resolvedFirstPageLabel" }] : []));
7349
+ resolvedPreviousPageLabel = computed(() => this.previousPageLabel() ?? this.defaultLabels().previousPage, ...(ngDevMode ? [{ debugName: "resolvedPreviousPageLabel" }] : []));
7350
+ resolvedNextPageLabel = computed(() => this.nextPageLabel() ?? this.defaultLabels().nextPage, ...(ngDevMode ? [{ debugName: "resolvedNextPageLabel" }] : []));
7351
+ resolvedLastPageLabel = computed(() => this.lastPageLabel() ?? this.defaultLabels().lastPage, ...(ngDevMode ? [{ debugName: "resolvedLastPageLabel" }] : []));
7352
+ resolvedTablePaginationLabel = computed(() => this.tablePaginationLabel() ?? this.defaultLabels().tablePagination, ...(ngDevMode ? [{ debugName: "resolvedTablePaginationLabel" }] : []));
7353
+ /** Emits the new 1-based page number whenever the user navigates. */
7354
+ pageChange = output();
7355
+ /** Emits the new page-size value when the dropdown changes. */
7356
+ pageSizeChange = output();
7357
+ /**
7358
+ * Total items reported by the data provider (when one is bound). Falls back
7359
+ * to `totalItems` input otherwise — see `effectiveTotalItems`.
7360
+ */
7361
+ providerTotalItems = signal(0, ...(ngDevMode ? [{ debugName: "providerTotalItems" }] : []));
7362
+ // The fields below are intentionally plain mutable state, not signals: they
7363
+ // back the provider binding's imperative lifecycle (current ref, current
7364
+ // subscription, last-pushed echo guard) and are only ever written from
7365
+ // inside the bind effect or the syncFromProvider/pushToProvider helpers.
7366
+ // Reading them never needs to participate in the reactive graph — exposing
7367
+ // them as signals would invite spurious re-evaluation without buying us
7368
+ // anything.
7369
+ /** The provider reference we're currently bound to (used to detect swaps). */
7370
+ currentProvider = null;
7371
+ /** Subscription to the current provider's `currentPage$` — torn down on swap. */
7372
+ providerSub = null;
7373
+ /**
7374
+ * Last pagination value we pushed to the provider. Used to recognize the
7375
+ * provider's resulting emission as our own echo and break the feedback loop
7376
+ * (setPagination → provider emits → syncFromProvider → setPagination …)
7377
+ * regardless of whether the provider emits synchronously or asynchronously,
7378
+ * and regardless of whether its stream replays on subscribe.
7379
+ */
7380
+ lastPushedPagination = null;
7381
+ effectiveTotalItems = computed(() => this.dataProvider() ? this.providerTotalItems() : this.totalItems(), ...(ngDevMode ? [{ debugName: "effectiveTotalItems" }] : []));
7382
+ totalPages = computed(() => {
7383
+ const size = this.pageSize();
7384
+ if (size <= 0) {
7385
+ return 0;
7386
+ }
7387
+ return Math.ceil(this.effectiveTotalItems() / size);
7388
+ }, ...(ngDevMode ? [{ debugName: "totalPages" }] : []));
7389
+ firstItemOnPage = computed(() => {
7390
+ if (this.effectiveTotalItems() === 0) {
7391
+ return 0;
7392
+ }
7393
+ return (this.currentPage() - 1) * this.pageSize() + 1;
7394
+ }, ...(ngDevMode ? [{ debugName: "firstItemOnPage" }] : []));
7395
+ lastItemOnPage = computed(() => {
7396
+ const last = this.currentPage() * this.pageSize();
7397
+ return Math.min(last, this.effectiveTotalItems());
7398
+ }, ...(ngDevMode ? [{ debugName: "lastItemOnPage" }] : []));
7399
+ isFirstPageDisabled = computed(() => this.currentPage() <= 1, ...(ngDevMode ? [{ debugName: "isFirstPageDisabled" }] : []));
7400
+ isLastPageDisabled = computed(() => {
7401
+ const total = this.totalPages();
7402
+ return total === 0 || this.currentPage() >= total;
7403
+ }, ...(ngDevMode ? [{ debugName: "isLastPageDisabled" }] : []));
7404
+ pageSizeSelectOptions = computed(() => this.pageSizeOptions().map((value) => ({ value, label: String(value) })), ...(ngDevMode ? [{ debugName: "pageSizeSelectOptions" }] : []));
7405
+ constructor() {
7406
+ const provided = inject(TN_TABLE_PAGER_LABELS);
7407
+ this.defaultLabels = isSignal(provided) ? provided : signal(provided).asReadonly();
7408
+ // Re-bind when the dataProvider reference changes (including swap to a
7409
+ // different instance or clearing back to undefined). `untracked` keeps the
7410
+ // provider's imperative reads out of the reactive graph so this effect only
7411
+ // re-runs when the provider reference itself changes.
7412
+ this.destroyRef.onDestroy(() => this.providerSub?.unsubscribe());
7413
+ effect(() => {
7414
+ const provider = this.dataProvider() ?? null;
7415
+ if (provider === this.currentProvider) {
7416
+ return;
7417
+ }
7418
+ // Tear down any previous binding before attaching to the new provider so
7419
+ // a swap doesn't leave the old subscription running against destroyRef.
7420
+ this.providerSub?.unsubscribe();
7421
+ this.providerSub = null;
7422
+ this.lastPushedPagination = null;
7423
+ this.currentProvider = provider;
7424
+ if (!provider) {
7425
+ this.providerTotalItems.set(0);
7426
+ return;
7427
+ }
7428
+ untracked(() => {
7429
+ this.pushToProvider();
7430
+ this.providerTotalItems.set(provider.totalRows);
7431
+ });
7432
+ // No skip() here: if currentPage$ replays (BehaviorSubject), the replay
7433
+ // value matches what pushToProvider() just recorded in
7434
+ // lastPushedPagination, so syncFromProvider treats it as our own echo
7435
+ // and short-circuits. A plain Subject (no replay) still gets every real
7436
+ // emission — earlier we used skip(1), which silently dropped the first
7437
+ // real emission for non-replaying streams.
7438
+ this.providerSub = provider.currentPage$.subscribe(() => this.syncFromProvider());
7439
+ });
7440
+ }
7441
+ syncFromProvider() {
7442
+ const provider = this.dataProvider();
7443
+ if (!provider) {
7444
+ return;
7445
+ }
7446
+ this.providerTotalItems.set(provider.totalRows);
7447
+ const p = provider.pagination;
7448
+ const isEcho = this.lastPushedPagination !== null
7449
+ && p.pageNumber === this.lastPushedPagination.pageNumber
7450
+ && p.pageSize === this.lastPushedPagination.pageSize;
7451
+ if (!isEcho) {
7452
+ const providerPage = p.pageNumber;
7453
+ if (providerPage !== null && providerPage !== this.currentPage()) {
7454
+ this.currentPage.set(providerPage);
7455
+ return;
7456
+ }
7457
+ }
7458
+ // Whether or not this was an echo, an updated totalRows can leave us on an
7459
+ // out-of-range page — reset, emit pageChange (so consumers see the auto-
7460
+ // correction symmetrically with user-initiated navigation), and push the
7461
+ // corrected pagination back.
7462
+ if (this.currentPage() > this.totalPages() && this.currentPage() !== 1) {
7463
+ this.currentPage.set(1);
7464
+ this.pageChange.emit(1);
7465
+ this.pushToProvider();
7466
+ }
7467
+ }
7468
+ /**
7469
+ * Public navigation API: jump to a specific 1-based page. Used both by the
7470
+ * template (first/last buttons) and by consumers who want to drive the pager
7471
+ * programmatically. Out-of-range values and no-op transitions are silently
7472
+ * ignored. Sibling helpers `previousPage` / `nextPage` are template-only and
7473
+ * therefore `protected` — `goToPage` is intentionally part of the public API.
7474
+ */
7475
+ goToPage(pageNumber) {
7476
+ const total = this.totalPages();
7477
+ if (pageNumber < 1 || (total > 0 && pageNumber > total)) {
7478
+ return;
7479
+ }
7480
+ if (pageNumber === this.currentPage()) {
7481
+ return;
7482
+ }
7483
+ this.currentPage.set(pageNumber);
7484
+ this.pageChange.emit(pageNumber);
7485
+ this.pushToProvider();
7486
+ }
7487
+ previousPage() {
7488
+ this.goToPage(this.currentPage() - 1);
7489
+ }
7490
+ nextPage() {
7491
+ this.goToPage(this.currentPage() + 1);
7492
+ }
7493
+ onPageSizeChange(value) {
7494
+ if (value === this.pageSize()) {
7495
+ return;
7496
+ }
7497
+ this.pageSize.set(value);
7498
+ this.pageSizeChange.emit(value);
7499
+ // Resetting to page 1 is a UX expectation when the page size changes —
7500
+ // otherwise the user might land on an out-of-range page.
7501
+ this.currentPage.set(1);
7502
+ this.pageChange.emit(1);
7503
+ this.pushToProvider();
7504
+ }
7505
+ /** Forwards the current page/size to the data provider, if one is bound. */
7506
+ pushToProvider() {
7507
+ const provider = this.dataProvider();
7508
+ if (!provider) {
7509
+ return;
7510
+ }
7511
+ // Recording the value before pushing lets syncFromProvider recognize the
7512
+ // resulting emission as our own echo (independent of whether the provider
7513
+ // emits synchronously from setPagination).
7514
+ const pagination = {
7515
+ pageNumber: this.currentPage(),
7516
+ pageSize: this.pageSize(),
7517
+ };
7518
+ this.lastPushedPagination = pagination;
7519
+ provider.setPagination(pagination);
7520
+ }
7521
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnTablePagerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
7522
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.0", type: TnTablePagerComponent, isStandalone: true, selector: "tn-table-pager", inputs: { currentPage: { classPropertyName: "currentPage", publicName: "currentPage", isSignal: true, isRequired: false, transformFunction: null }, pageSize: { classPropertyName: "pageSize", publicName: "pageSize", isSignal: true, isRequired: false, transformFunction: null }, pageSizeOptions: { classPropertyName: "pageSizeOptions", publicName: "pageSizeOptions", isSignal: true, isRequired: false, transformFunction: null }, totalItems: { classPropertyName: "totalItems", publicName: "totalItems", isSignal: true, isRequired: false, transformFunction: null }, dataProvider: { classPropertyName: "dataProvider", publicName: "dataProvider", isSignal: true, isRequired: false, transformFunction: null }, itemsPerPageLabel: { classPropertyName: "itemsPerPageLabel", publicName: "itemsPerPageLabel", isSignal: true, isRequired: false, transformFunction: null }, ofLabel: { classPropertyName: "ofLabel", publicName: "ofLabel", isSignal: true, isRequired: false, transformFunction: null }, firstPageLabel: { classPropertyName: "firstPageLabel", publicName: "firstPageLabel", isSignal: true, isRequired: false, transformFunction: null }, previousPageLabel: { classPropertyName: "previousPageLabel", publicName: "previousPageLabel", isSignal: true, isRequired: false, transformFunction: null }, nextPageLabel: { classPropertyName: "nextPageLabel", publicName: "nextPageLabel", isSignal: true, isRequired: false, transformFunction: null }, lastPageLabel: { classPropertyName: "lastPageLabel", publicName: "lastPageLabel", isSignal: true, isRequired: false, transformFunction: null }, tablePaginationLabel: { classPropertyName: "tablePaginationLabel", publicName: "tablePaginationLabel", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { currentPage: "currentPageChange", pageSize: "pageSizeChange", pageChange: "pageChange", pageSizeChange: "pageSizeChange" }, host: { attributes: { "role": "navigation" }, properties: { "attr.aria-label": "resolvedTablePaginationLabel()" }, classAttribute: "tn-table-pager" }, hostDirectives: [{ directive: TnTestIdDirective, inputs: ["tnTestId", "testId"] }], ngImport: i0, template: "<div class=\"tn-table-pager__page-size\">\n <span class=\"tn-table-pager__label\">{{ resolvedItemsPerPageLabel() }}:</span>\n <tn-select\n class=\"tn-table-pager__size-select\"\n [options]=\"pageSizeSelectOptions()\"\n [ariaLabel]=\"resolvedItemsPerPageLabel()\"\n [ngModel]=\"pageSize()\"\n (ngModelChange)=\"onPageSizeChange($event)\" />\n</div>\n\n<span class=\"tn-table-pager__range\">\n @if (effectiveTotalItems() === 0) {\n \u2013 {{ resolvedOfLabel() }} 0\n } @else if (lastItemOnPage() > firstItemOnPage()) {\n {{ firstItemOnPage() }} \u2013 {{ lastItemOnPage() }} {{ resolvedOfLabel() }} {{ effectiveTotalItems() }}\n } @else {\n {{ lastItemOnPage() }} {{ resolvedOfLabel() }} {{ effectiveTotalItems() }}\n }\n</span>\n\n<div class=\"tn-table-pager__buttons\">\n <tn-icon-button\n name=\"page-first\"\n library=\"mdi\"\n [ariaLabel]=\"resolvedFirstPageLabel()\"\n [disabled]=\"isFirstPageDisabled()\"\n (onClick)=\"goToPage(1)\" />\n <tn-icon-button\n name=\"chevron-left\"\n library=\"mdi\"\n [ariaLabel]=\"resolvedPreviousPageLabel()\"\n [disabled]=\"isFirstPageDisabled()\"\n (onClick)=\"previousPage()\" />\n <tn-icon-button\n name=\"chevron-right\"\n library=\"mdi\"\n [ariaLabel]=\"resolvedNextPageLabel()\"\n [disabled]=\"isLastPageDisabled()\"\n (onClick)=\"nextPage()\" />\n <tn-icon-button\n name=\"page-last\"\n library=\"mdi\"\n [ariaLabel]=\"resolvedLastPageLabel()\"\n [disabled]=\"isLastPageDisabled()\"\n (onClick)=\"goToPage(totalPages())\" />\n</div>\n", styles: [":host{display:flex;align-items:center;justify-content:flex-end;gap:16px;padding:10px;background-color:var(--tn-bg2);border:1px solid var(--tn-lines);color:var(--tn-fg2)}.tn-table-pager__page-size{display:flex;align-items:center;flex-wrap:wrap;gap:8px}.tn-table-pager__label{white-space:nowrap}.tn-table-pager__size-select{min-width:84px}.tn-table-pager__range{white-space:nowrap}.tn-table-pager__buttons{display:flex;align-items:center;gap:4px}@media(max-width:600px){:host{gap:8px}.tn-table-pager__page-size{gap:4px}}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: TnIconButtonComponent, selector: "tn-icon-button", inputs: ["disabled", "ariaLabel", "testId", "name", "size", "color", "tooltip", "library"], outputs: ["onClick"] }, { kind: "component", type: TnSelectComponent, selector: "tn-select", inputs: ["options", "optionGroups", "placeholder", "ariaLabel", "noOptionsLabel", "disabled", "testId", "multiple", "compareWith"], outputs: ["selectionChange", "multiSelectionChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
7523
+ }
7524
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnTablePagerComponent, decorators: [{
7525
+ type: Component,
7526
+ args: [{ selector: 'tn-table-pager', standalone: true, imports: [FormsModule, TnIconButtonComponent, TnSelectComponent, TnTestIdDirective], changeDetection: ChangeDetectionStrategy.OnPush, host: {
7527
+ 'class': 'tn-table-pager',
7528
+ 'role': 'navigation',
7529
+ '[attr.aria-label]': 'resolvedTablePaginationLabel()',
7530
+ }, hostDirectives: [{ directive: TnTestIdDirective, inputs: ['tnTestId: testId'] }], template: "<div class=\"tn-table-pager__page-size\">\n <span class=\"tn-table-pager__label\">{{ resolvedItemsPerPageLabel() }}:</span>\n <tn-select\n class=\"tn-table-pager__size-select\"\n [options]=\"pageSizeSelectOptions()\"\n [ariaLabel]=\"resolvedItemsPerPageLabel()\"\n [ngModel]=\"pageSize()\"\n (ngModelChange)=\"onPageSizeChange($event)\" />\n</div>\n\n<span class=\"tn-table-pager__range\">\n @if (effectiveTotalItems() === 0) {\n \u2013 {{ resolvedOfLabel() }} 0\n } @else if (lastItemOnPage() > firstItemOnPage()) {\n {{ firstItemOnPage() }} \u2013 {{ lastItemOnPage() }} {{ resolvedOfLabel() }} {{ effectiveTotalItems() }}\n } @else {\n {{ lastItemOnPage() }} {{ resolvedOfLabel() }} {{ effectiveTotalItems() }}\n }\n</span>\n\n<div class=\"tn-table-pager__buttons\">\n <tn-icon-button\n name=\"page-first\"\n library=\"mdi\"\n [ariaLabel]=\"resolvedFirstPageLabel()\"\n [disabled]=\"isFirstPageDisabled()\"\n (onClick)=\"goToPage(1)\" />\n <tn-icon-button\n name=\"chevron-left\"\n library=\"mdi\"\n [ariaLabel]=\"resolvedPreviousPageLabel()\"\n [disabled]=\"isFirstPageDisabled()\"\n (onClick)=\"previousPage()\" />\n <tn-icon-button\n name=\"chevron-right\"\n library=\"mdi\"\n [ariaLabel]=\"resolvedNextPageLabel()\"\n [disabled]=\"isLastPageDisabled()\"\n (onClick)=\"nextPage()\" />\n <tn-icon-button\n name=\"page-last\"\n library=\"mdi\"\n [ariaLabel]=\"resolvedLastPageLabel()\"\n [disabled]=\"isLastPageDisabled()\"\n (onClick)=\"goToPage(totalPages())\" />\n</div>\n", styles: [":host{display:flex;align-items:center;justify-content:flex-end;gap:16px;padding:10px;background-color:var(--tn-bg2);border:1px solid var(--tn-lines);color:var(--tn-fg2)}.tn-table-pager__page-size{display:flex;align-items:center;flex-wrap:wrap;gap:8px}.tn-table-pager__label{white-space:nowrap}.tn-table-pager__size-select{min-width:84px}.tn-table-pager__range{white-space:nowrap}.tn-table-pager__buttons{display:flex;align-items:center;gap:4px}@media(max-width:600px){:host{gap:8px}.tn-table-pager__page-size{gap:4px}}\n"] }]
7531
+ }], ctorParameters: () => [], propDecorators: { currentPage: [{ type: i0.Input, args: [{ isSignal: true, alias: "currentPage", required: false }] }, { type: i0.Output, args: ["currentPageChange"] }], pageSize: [{ type: i0.Input, args: [{ isSignal: true, alias: "pageSize", required: false }] }, { type: i0.Output, args: ["pageSizeChange"] }], pageSizeOptions: [{ type: i0.Input, args: [{ isSignal: true, alias: "pageSizeOptions", required: false }] }], totalItems: [{ type: i0.Input, args: [{ isSignal: true, alias: "totalItems", required: false }] }], dataProvider: [{ type: i0.Input, args: [{ isSignal: true, alias: "dataProvider", required: false }] }], itemsPerPageLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "itemsPerPageLabel", required: false }] }], ofLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ofLabel", required: false }] }], firstPageLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "firstPageLabel", required: false }] }], previousPageLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "previousPageLabel", required: false }] }], nextPageLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "nextPageLabel", required: false }] }], lastPageLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "lastPageLabel", required: false }] }], tablePaginationLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "tablePaginationLabel", required: false }] }], pageChange: [{ type: i0.Output, args: ["pageChange"] }], pageSizeChange: [{ type: i0.Output, args: ["pageSizeChange"] }] } });
7532
+
7533
+ /**
7534
+ * Harness for interacting with `tn-table-pager` in tests.
7535
+ *
7536
+ * @example
7537
+ * ```ts
7538
+ * const pager = await loader.getHarness(TnTablePagerHarness);
7539
+ * await pager.nextPage();
7540
+ * expect(await pager.getRangeText()).toBe('21 – 40 of 47');
7541
+ * ```
7542
+ */
7543
+ class TnTablePagerHarness extends ComponentHarness {
7544
+ static hostSelector = 'tn-table-pager';
7545
+ static with(options = {}) {
7546
+ return new HarnessPredicate(TnTablePagerHarness, options);
7547
+ }
7548
+ firstButton = this.locatorFor(TnIconButtonHarness.with({ name: 'page-first' }));
7549
+ previousButton = this.locatorFor(TnIconButtonHarness.with({ name: 'chevron-left' }));
7550
+ nextButton = this.locatorFor(TnIconButtonHarness.with({ name: 'chevron-right' }));
7551
+ lastButton = this.locatorFor(TnIconButtonHarness.with({ name: 'page-last' }));
7552
+ pageSizeSelect = this.locatorFor(TnSelectHarness);
7553
+ rangeEl = this.locatorFor('.tn-table-pager__range');
7554
+ /**
7555
+ * Returns the rendered range text (e.g. `"1 – 20 of 47"`).
7556
+ */
7557
+ async getRangeText() {
7558
+ const el = await this.rangeEl();
7559
+ return (await el.text()).replace(/\s+/g, ' ').trim();
7560
+ }
7561
+ /** Clicks the "first page" button. */
7562
+ async goToFirstPage() {
7563
+ const btn = await this.firstButton();
7564
+ await btn.click();
7565
+ }
7566
+ /** Clicks the "previous page" button. */
7567
+ async previousPage() {
7568
+ const btn = await this.previousButton();
7569
+ await btn.click();
7570
+ }
7571
+ /** Clicks the "next page" button. */
7572
+ async nextPage() {
7573
+ const btn = await this.nextButton();
7574
+ await btn.click();
7575
+ }
7576
+ /** Clicks the "last page" button. */
7577
+ async goToLastPage() {
7578
+ const btn = await this.lastButton();
7579
+ await btn.click();
7580
+ }
7581
+ async isFirstButtonDisabled() {
7582
+ const btn = await this.firstButton();
7583
+ return btn.isDisabled();
7584
+ }
7585
+ async isPreviousButtonDisabled() {
7586
+ const btn = await this.previousButton();
7587
+ return btn.isDisabled();
7588
+ }
7589
+ async isNextButtonDisabled() {
7590
+ const btn = await this.nextButton();
7591
+ return btn.isDisabled();
7592
+ }
7593
+ async isLastButtonDisabled() {
7594
+ const btn = await this.lastButton();
7595
+ return btn.isDisabled();
7596
+ }
7597
+ /** Returns the harness for the underlying page-size `tn-select`. */
7598
+ async getPageSizeSelect() {
7599
+ return this.pageSizeSelect();
7600
+ }
7601
+ }
7602
+
6945
7603
  /**
6946
7604
  * Tree flattener to convert normal type of node to node with children & level information.
6947
7605
  */
@@ -7025,7 +7683,7 @@ class TnTreeComponent extends CdkTree {
7025
7683
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnTreeComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
7026
7684
  static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.1.0", type: TnTreeComponent, isStandalone: true, selector: "tn-tree", host: { attributes: { "role": "tree" }, classAttribute: "tn-tree" }, providers: [
7027
7685
  { provide: CdkTree, useExisting: TnTreeComponent }
7028
- ], exportAs: ["tnTree"], usesInheritance: true, hostDirectives: [{ directive: TnTestIdDirective, inputs: ["tnTestId", "testId"] }], ngImport: i0, template: "<ng-container cdkTreeNodeOutlet />", styles: [":host{display:block;width:100%}.tn-tree{width:100%;background-color:var(--tn-bg1);border:1px solid var(--tn-lines);border-radius:6px;overflow:hidden}\n"], dependencies: [{ kind: "ngmodule", type: CdkTreeModule }, { kind: "directive", type: i2.CdkTreeNodeOutlet, selector: "[cdkTreeNodeOutlet]" }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
7686
+ ], exportAs: ["tnTree"], usesInheritance: true, hostDirectives: [{ directive: TnTestIdDirective, inputs: ["tnTestId", "testId"] }], ngImport: i0, template: "<ng-container cdkTreeNodeOutlet />", styles: [":host{display:block;width:100%}.tn-tree{width:100%;background-color:var(--tn-bg1);border:1px solid var(--tn-lines);border-radius:6px;overflow:hidden}\n"], dependencies: [{ kind: "ngmodule", type: CdkTreeModule }, { kind: "directive", type: i2$1.CdkTreeNodeOutlet, selector: "[cdkTreeNodeOutlet]" }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
7029
7687
  }
7030
7688
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnTreeComponent, decorators: [{
7031
7689
  type: Component,
@@ -7056,7 +7714,7 @@ class TnTreeNodeComponent extends CdkTreeNode {
7056
7714
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnTreeNodeComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
7057
7715
  static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.0", type: TnTreeNodeComponent, isStandalone: true, selector: "tn-tree-node", host: { attributes: { "role": "treeitem" }, properties: { "attr.aria-level": "level + 1", "attr.aria-expanded": "isExpandable ? isExpanded : null" }, classAttribute: "tn-tree-node-wrapper" }, providers: [
7058
7716
  { provide: CdkTreeNode, useExisting: TnTreeNodeComponent }
7059
- ], exportAs: ["tnTreeNode"], usesInheritance: true, hostDirectives: [{ directive: TnTestIdDirective, inputs: ["tnTestId", "testId"] }], ngImport: i0, template: "<div class=\"tn-tree-node\"\n cdkTreeNodeToggle\n role=\"treeitem\"\n [class.tn-tree-node--expandable]=\"isExpandable\"\n [attr.aria-level]=\"level + 1\"\n [attr.aria-expanded]=\"isExpandable ? isExpanded : null\"\n [attr.aria-selected]=\"false\"\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"], 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", "fullSize", "customSize"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
7717
+ ], exportAs: ["tnTreeNode"], usesInheritance: true, hostDirectives: [{ directive: TnTestIdDirective, inputs: ["tnTestId", "testId"] }], ngImport: i0, template: "<div class=\"tn-tree-node\"\n cdkTreeNodeToggle\n role=\"treeitem\"\n [class.tn-tree-node--expandable]=\"isExpandable\"\n [attr.aria-level]=\"level + 1\"\n [attr.aria-expanded]=\"isExpandable ? isExpanded : null\"\n [attr.aria-selected]=\"false\"\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"], dependencies: [{ kind: "ngmodule", type: CdkTreeModule }, { kind: "directive", type: i2$1.CdkTreeNodeToggle, selector: "[cdkTreeNodeToggle]", inputs: ["cdkTreeNodeToggleRecursive"] }, { kind: "component", type: TnIconComponent, selector: "tn-icon", inputs: ["name", "size", "color", "tooltip", "ariaLabel", "library", "fullSize", "customSize"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
7060
7718
  }
7061
7719
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnTreeNodeComponent, decorators: [{
7062
7720
  type: Component,
@@ -7072,7 +7730,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
7072
7730
 
7073
7731
  class TnTreeNodeOutletDirective {
7074
7732
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnTreeNodeOutletDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
7075
- static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.1.0", type: TnTreeNodeOutletDirective, isStandalone: true, selector: "[tnTreeNodeOutlet]", hostDirectives: [{ directive: i2.CdkTreeNodeOutlet }], ngImport: i0 });
7733
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.1.0", type: TnTreeNodeOutletDirective, isStandalone: true, selector: "[tnTreeNodeOutlet]", hostDirectives: [{ directive: i2$1.CdkTreeNodeOutlet }], ngImport: i0 });
7076
7734
  }
7077
7735
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnTreeNodeOutletDirective, decorators: [{
7078
7736
  type: Directive,
@@ -7137,7 +7795,7 @@ class TnNestedTreeNodeComponent extends CdkNestedTreeNode {
7137
7795
  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: [
7138
7796
  { provide: CdkNestedTreeNode, useExisting: TnNestedTreeNodeComponent },
7139
7797
  { provide: CdkTreeNode, useExisting: TnNestedTreeNodeComponent }
7140
- ], 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:.875rem;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", "fullSize", "customSize"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
7798
+ ], 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:.875rem;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$1.CdkTreeNodeToggle, selector: "[cdkTreeNodeToggle]", inputs: ["cdkTreeNodeToggleRecursive"] }, { kind: "component", type: TnIconComponent, selector: "tn-icon", inputs: ["name", "size", "color", "tooltip", "ariaLabel", "library", "fullSize", "customSize"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
7141
7799
  }
7142
7800
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnNestedTreeNodeComponent, decorators: [{
7143
7801
  type: Component,
@@ -9681,7 +10339,7 @@ class TnTimeInputComponent {
9681
10339
  useExisting: forwardRef(() => TnTimeInputComponent),
9682
10340
  multi: true
9683
10341
  }
9684
- ], ngImport: i0, template: "<tn-select\n [options]=\"timeSelectOptions()\"\n [placeholder]=\"placeholder()\"\n [disabled]=\"isDisabled()\"\n [testId]=\"testId()\"\n [ngModel]=\"_value\"\n (selectionChange)=\"onSelectionChange($event)\" />\n", styles: [":host{display:block;width:100%}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$3.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$3.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: TnSelectComponent, selector: "tn-select", inputs: ["options", "optionGroups", "placeholder", "disabled", "testId", "multiple", "compareWith"], outputs: ["selectionChange", "multiSelectionChange"] }] });
10342
+ ], ngImport: i0, template: "<tn-select\n [options]=\"timeSelectOptions()\"\n [placeholder]=\"placeholder()\"\n [disabled]=\"isDisabled()\"\n [testId]=\"testId()\"\n [ngModel]=\"_value\"\n (selectionChange)=\"onSelectionChange($event)\" />\n", styles: [":host{display:block;width:100%}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: TnSelectComponent, selector: "tn-select", inputs: ["options", "optionGroups", "placeholder", "ariaLabel", "noOptionsLabel", "disabled", "testId", "multiple", "compareWith"], outputs: ["selectionChange", "multiSelectionChange"] }] });
9685
10343
  }
9686
10344
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnTimeInputComponent, decorators: [{
9687
10345
  type: Component,
@@ -13307,5 +13965,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
13307
13965
  * Generated bundle index. Do not edit.
13308
13966
  */
13309
13967
 
13310
- export { CommonShortcuts, DEFAULT_THEME, DiskIconComponent, DiskType, FileSizePipe, InputType, LIGHT_THEME, LinuxModifierKeys, LinuxShortcuts, ModifierKeys, QuickShortcuts, ShortcutBuilder, StripMntPrefixPipe, THEME_MAP, THEME_STORAGE_KEY, TN_TEST_ATTR, TN_THEME_DEFINITIONS, TnAutocompleteComponent, TnAutocompleteHarness, TnBannerActionDirective, TnBannerComponent, TnBannerHarness, TnBrandedSpinnerComponent, TnButtonComponent, TnButtonHarness, TnButtonToggleComponent, TnButtonToggleGroupComponent, TnButtonToggleGroupHarness, TnButtonToggleHarness, TnCalendarComponent, TnCalendarHeaderComponent, TnCardComponent, TnCardHeaderDirective, TnCellDefDirective, TnCheckboxComponent, TnCheckboxHarness, TnCheckboxLabelDirective, TnChipComponent, TnConfirmDialogComponent, TnDateInputComponent, TnDateInputHarness, TnDateRangeInputComponent, TnDateRangeInputHarness, TnDetailRowDefDirective, TnDialog, TnDialogHarness, TnDialogShellComponent, TnDialogTesting, TnDividerComponent, TnDividerDirective, TnDrawerComponent, TnDrawerContainerComponent, TnDrawerContainerHarness, TnDrawerContentComponent, TnDrawerHarness, TnEmptyComponent, TnEmptyHarness, TnExpansionPanelComponent, TnExpansionPanelHarness, TnFilePickerComponent, TnFilePickerPopupComponent, TnFormFieldComponent, TnFormFieldHarness, 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, TnTabComponent, TnTabHarness, TnTabPanelComponent, TnTabPanelHarness, TnTableColumnDirective, TnTableComponent, TnTableHarness, TnTabsComponent, TnTabsHarness, TnTestIdDirective, TnTheme, TnThemeService, TnTimeInputComponent, TnToastComponent, TnToastMock, TnToastPosition, TnToastRef, TnToastService, TnToastTesting, TnToastType, TnTooltipComponent, TnTooltipDirective, TnTreeComponent, TnTreeFlatDataSource, TnTreeFlattener, TnTreeNodeComponent, TnTreeNodeOutletDirective, TruncatePathPipe, WindowsModifierKeys, WindowsShortcuts, createLucideLibrary, createShortcut, defaultSpriteBasePath, defaultSpriteConfigPath, libIconMarker, registerLucideIcons, setupLucideIntegration, tnIconMarker };
13968
+ export { CommonShortcuts, DEFAULT_THEME, DiskIconComponent, DiskType, FileSizePipe, InputType, LIGHT_THEME, LinuxModifierKeys, LinuxShortcuts, ModifierKeys, QuickShortcuts, ShortcutBuilder, StripMntPrefixPipe, THEME_MAP, THEME_STORAGE_KEY, 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, TnCardHeaderDirective, TnCellDefDirective, TnCheckboxComponent, TnCheckboxHarness, TnCheckboxLabelDirective, TnChipComponent, TnConfirmDialogComponent, TnDateInputComponent, TnDateInputHarness, TnDateRangeInputComponent, TnDateRangeInputHarness, TnDetailRowDefDirective, TnDialog, TnDialogHarness, TnDialogShellComponent, TnDialogTesting, TnDividerComponent, TnDividerDirective, TnDrawerComponent, TnDrawerContainerComponent, TnDrawerContainerHarness, TnDrawerContentComponent, TnDrawerHarness, TnEmptyComponent, TnEmptyHarness, TnExpansionPanelComponent, TnExpansionPanelHarness, TnFilePickerComponent, TnFilePickerPopupComponent, TnFormFieldComponent, TnFormFieldHarness, 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, 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, TruncatePathPipe, WindowsModifierKeys, WindowsShortcuts, createLucideLibrary, createShortcut, defaultSpriteBasePath, defaultSpriteConfigPath, libIconMarker, registerLucideIcons, setupLucideIntegration, tnIconMarker };
13311
13969
  //# sourceMappingURL=truenas-ui-components.mjs.map