@truenas/ui-components 0.1.9 → 0.1.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -54,6 +54,30 @@ Or import in your main styles file:
54
54
  @import '@truenas/ui-components/src/styles/themes.css';
55
55
  ```
56
56
 
57
+ ### 3. Configure Icon System (Required)
58
+
59
+ The icon system requires the sprite to be loaded before the application renders. Add this to your `main.ts` or application bootstrap:
60
+
61
+ ```typescript
62
+ import { provideAppInitializer } from '@angular/core';
63
+ import { TnSpriteLoaderService } from '@truenas/ui-components';
64
+
65
+ bootstrapApplication(AppComponent, {
66
+ providers: [
67
+ // ... other providers
68
+ provideAppInitializer(() => {
69
+ const spriteLoader = inject(TnSpriteLoaderService);
70
+ return spriteLoader.ensureSpriteLoaded();
71
+ }),
72
+ ],
73
+ });
74
+ ```
75
+
76
+ **Why is this required?**
77
+ - Icons are rendered synchronously for optimal performance and test reliability
78
+ - The sprite must be preloaded at app startup to avoid showing fallback text
79
+ - Without this initializer, icons will display as abbreviations (e.g., "FO" for "folder") until the sprite loads
80
+
57
81
  ## Icon System
58
82
 
59
83
  ### Using Icons
@@ -324,6 +348,46 @@ projects/truenas-ui/
324
348
  - Firefox (latest)
325
349
  - Safari (latest)
326
350
 
351
+ ## Migration Guide
352
+
353
+ ### Upgrading to v0.2.0+ (Icon System Changes)
354
+
355
+ **Breaking Change:** The icon system now requires `APP_INITIALIZER` setup.
356
+
357
+ **Before (v0.1.x):**
358
+ ```typescript
359
+ // Icons worked without any special setup
360
+ bootstrapApplication(AppComponent, {
361
+ providers: [/* your providers */]
362
+ });
363
+ ```
364
+
365
+ **After (v0.2.0+):**
366
+ ```typescript
367
+ import { provideAppInitializer, inject } from '@angular/core';
368
+ import { TnSpriteLoaderService } from '@truenas/ui-components';
369
+
370
+ bootstrapApplication(AppComponent, {
371
+ providers: [
372
+ provideAppInitializer(() => {
373
+ const spriteLoader = inject(TnSpriteLoaderService);
374
+ return spriteLoader.ensureSpriteLoaded();
375
+ }),
376
+ // ... your other providers
377
+ ]
378
+ });
379
+ ```
380
+
381
+ **What changed:**
382
+ - Icons now resolve synchronously for better performance and test reliability
383
+ - Sprite must be preloaded at app startup via `APP_INITIALIZER`
384
+ - Without this setup, icons will show text fallbacks until sprite loads asynchronously
385
+
386
+ **Benefits:**
387
+ - ✅ Faster icon rendering (no async delay)
388
+ - ✅ More reliable unit tests (no timing issues)
389
+ - ✅ Cleaner code (no `whenStable()` workarounds needed)
390
+
327
391
  ## License
328
392
 
329
393
  Copyright © iXsystems, Inc.
@@ -1 +1,3 @@
1
- <svg xmlns="http://www.w3.org/2000/svg" width="43.494" height="28.99" fill="none"><g style="fill: rgb(0, 0, 0);"><path d="M34.436 10.874A12.849 12.849 0 0 0 21.755.004 14.536 14.536 0 0 0 9.073 7.25 10.404 10.404 0 0 0 .015 18.12c0 6.003 4.866 10.87 10.87 10.87h23.551a9.058 9.058 0 0 0 9.058-9.058 9.67 9.67 0 0 0-9.058-9.058Z" style="fill: rgb(255, 255, 255); fill-opacity: 1;" class="fills"/></g><g style="fill: rgb(0, 0, 0);"><path d="M34.319 14.237v5.358l-11.47 6.626v-5.358Z" class="fills"/><path d="M20.654 20.863v5.358L9.175 19.594v-5.357l5.187 2.99a.076.076 0 0 0 .019.012Z" class="fills"/><path d="m26.392 16.283-4.641 2.685-4.644-2.68 4.644-2.681Z" class="fills"/><path d="m33.226 12.339-4.643 2.68-5.734-3.314V6.343Z" class="fills"/><path d="M20.654 6.343v5.362l-5.741 3.311-4.644-2.677Z" class="fills"/></g></svg>
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 44 29">
2
+ <path fill-rule="evenodd" d="M34.436 10.874A12.849 12.849 0 0 0 21.755.004 14.536 14.536 0 0 0 9.073 7.25 10.404 10.404 0 0 0 .015 18.12c0 6.003 4.866 10.87 10.87 10.87h23.551a9.058 9.058 0 0 0 9.058-9.058 9.67 9.67 0 0 0-9.058-9.058ZM34.319 14.237v5.358l-11.47 6.626v-5.358ZM20.654 20.863v5.358L9.175 19.594v-5.357l5.187 2.99a.076.076 0 0 0 .019.012ZM26.392 16.283l-4.641 2.685-4.644-2.68 4.644-2.681ZM33.226 12.339l-4.643 2.68-5.734-3.314V6.343ZM20.654 6.343v5.362l-5.741 3.311-4.644-2.677Z"/>
3
+ </svg>
@@ -1,5 +1,5 @@
1
1
  import * as i0 from '@angular/core';
2
- import { input, ChangeDetectionStrategy, Component, inject, Injectable, viewChild, ChangeDetectorRef, effect, computed, ViewEncapsulation, Directive, contentChildren, output, signal, forwardRef, ElementRef, ViewContainerRef, contentChild, HostListener, TemplateRef, IterableDiffers, Pipe, model, PLATFORM_ID } from '@angular/core';
2
+ import { input, ChangeDetectionStrategy, Component, inject, Injectable, viewChild, signal, effect, computed, ViewEncapsulation, Directive, contentChildren, output, forwardRef, ElementRef, ViewContainerRef, contentChild, ChangeDetectorRef, HostListener, TemplateRef, IterableDiffers, Pipe, model, PLATFORM_ID } from '@angular/core';
3
3
  import * as i1$1 from '@angular/common';
4
4
  import { CommonModule, DOCUMENT, isPlatformBrowser } from '@angular/common';
5
5
  import { mdiCheckCircle, mdiAlertCircle, mdiAlert, mdiInformation, mdiDotsVertical, mdiFolderOpen, mdiLock, mdiLoading, mdiFolderPlus, mdiFolderNetwork, mdiHarddisk, mdiDatabase, mdiFile, mdiFolder } from '@mdi/js';
@@ -403,78 +403,55 @@ class TnIconComponent {
403
403
  */
404
404
  fullSize = input(false, ...(ngDevMode ? [{ debugName: "fullSize" }] : []));
405
405
  svgContainer = viewChild('svgContainer', ...(ngDevMode ? [{ debugName: "svgContainer" }] : []));
406
- iconResult = { source: 'text', content: '?' };
406
+ iconResult = signal({ source: 'text', content: '?' }, ...(ngDevMode ? [{ debugName: "iconResult" }] : []));
407
407
  iconRegistry = inject(TnIconRegistryService);
408
- cdr = inject(ChangeDetectorRef);
409
408
  sanitizer = inject(DomSanitizer);
410
409
  constructor() {
411
- // Use effect to watch for changes in name or library
410
+ // Effect 1: Resolve icons when name or library changes
412
411
  effect(() => {
413
- // Track signals to re-run effect when they change
414
- this.name();
415
- this.library();
416
- // Trigger icon resolution when name or library changes
417
- this.resolveIcon()
418
- .then(() => {
419
- this.cdr.markForCheck();
420
- setTimeout(() => this.updateSvgContent(), 0);
421
- })
422
- .catch((error) => {
423
- console.error('[TnIcon] Resolution failed (onChange)', error);
424
- this.iconResult = { source: 'text', content: '!' };
425
- this.cdr.markForCheck();
426
- });
412
+ const name = this.name();
413
+ const library = this.library();
414
+ const result = this.resolveIcon(name, library);
415
+ this.iconResult.set(result);
416
+ });
417
+ // Effect 2: Update SVG DOM when container is available and content changes
418
+ effect(() => {
419
+ const container = this.svgContainer();
420
+ const content = this.sanitizedContent();
421
+ const result = this.iconResult();
422
+ if (result.source === 'svg' && container && typeof content === 'string') {
423
+ // Bypass Angular's sanitization by setting innerHTML directly
424
+ container.nativeElement.innerHTML = content;
425
+ }
427
426
  });
428
- }
429
- ngAfterViewInit() {
430
- this.updateSvgContent();
431
427
  }
432
428
  effectiveAriaLabel = computed(() => {
433
429
  return this.ariaLabel() || this.name() || 'Icon';
434
430
  }, ...(ngDevMode ? [{ debugName: "effectiveAriaLabel" }] : []));
435
431
  sanitizedContent = computed(() => {
436
- const content = this.iconResult.content;
432
+ const content = this.iconResult().content;
437
433
  // Handle mock SafeHtml objects from Storybook
438
434
  if (content && typeof content === 'object' && content.changingThisBreaksApplicationSecurity) {
439
435
  return content.changingThisBreaksApplicationSecurity;
440
436
  }
441
437
  return content;
442
438
  }, ...(ngDevMode ? [{ debugName: "sanitizedContent" }] : []));
443
- updateSvgContent() {
444
- const svgContainer = this.svgContainer();
445
- if (this.iconResult.source === 'svg' && svgContainer) {
446
- const content = this.sanitizedContent();
447
- if (typeof content === 'string') {
448
- // Bypass Angular's sanitization by setting innerHTML directly
449
- svgContainer.nativeElement.innerHTML = content;
450
- }
451
- }
452
- }
453
- async resolveIcon() {
454
- if (!this.name()) {
455
- this.iconResult = { source: 'text', content: '?' };
456
- return;
457
- }
458
- // Wait for sprite to load (if it's being loaded)
459
- try {
460
- await this.iconRegistry.getSpriteLoader().ensureSpriteLoaded();
461
- }
462
- catch (error) {
463
- // Sprite loading failed, continue with other resolution methods
464
- console.warn('[TnIcon] Sprite loading failed, falling back to other icon sources:', error);
439
+ resolveIcon(name, library) {
440
+ if (!name) {
441
+ return { source: 'text', content: '?' };
465
442
  }
466
443
  // Construct the effective icon name based on library attribute
467
- let effectiveIconName = this.name();
468
- if (this.library() === 'mdi' && !this.name().startsWith('mdi-')) {
469
- effectiveIconName = `mdi-${this.name()}`;
444
+ let effectiveIconName = name;
445
+ if (library === 'mdi' && !name.startsWith('mdi-')) {
446
+ effectiveIconName = `mdi-${name}`;
470
447
  }
471
- else if (this.library() === 'material' && !this.name().startsWith('mat-')) {
448
+ else if (library === 'material' && !name.startsWith('mat-')) {
472
449
  // Material icons get mat- prefix in sprite
473
- effectiveIconName = `mat-${this.name()}`;
450
+ effectiveIconName = `mat-${name}`;
474
451
  }
475
- else if (this.library() === 'lucide' && !this.name().includes(':')) {
452
+ else if (library === 'lucide' && !name.includes(':')) {
476
453
  // Convert to registry format for Lucide icons
477
- effectiveIconName = `lucide:${this.name()}`;
454
+ effectiveIconName = `lucide:${name}`;
478
455
  }
479
456
  // 1. Try icon registry (libraries and custom icons)
480
457
  const iconOptions = {
@@ -490,29 +467,25 @@ class TnIconComponent {
490
467
  }
491
468
  }
492
469
  if (registryResult) {
493
- this.iconResult = registryResult;
494
- return;
470
+ return registryResult;
495
471
  }
496
472
  // 2. Try built-in third-party patterns (deprecated - use registry instead)
497
473
  const thirdPartyResult = this.tryThirdPartyIcon(effectiveIconName);
498
474
  if (thirdPartyResult) {
499
- this.iconResult = thirdPartyResult;
500
- return;
475
+ return thirdPartyResult;
501
476
  }
502
477
  // 3. Try CSS class (Font Awesome, Material Icons, etc.)
503
478
  const cssResult = this.tryCssIcon(effectiveIconName);
504
479
  if (cssResult) {
505
- this.iconResult = cssResult;
506
- return;
480
+ return cssResult;
507
481
  }
508
482
  // 4. Try Unicode mapping
509
483
  const unicodeResult = this.tryUnicodeIcon(effectiveIconName);
510
484
  if (unicodeResult) {
511
- this.iconResult = unicodeResult;
512
- return;
485
+ return unicodeResult;
513
486
  }
514
487
  // 5. Fallback to text abbreviation
515
- this.iconResult = {
488
+ return {
516
489
  source: 'text',
517
490
  content: this.generateTextAbbreviation(effectiveIconName)
518
491
  };
@@ -557,13 +530,6 @@ class TnIconComponent {
557
530
  content: `material-icons material-icons-${materialName}`
558
531
  };
559
532
  }
560
- // Check if class exists in document
561
- if (this.cssClassExists(name)) {
562
- return {
563
- source: 'css',
564
- content: name
565
- };
566
- }
567
533
  return null;
568
534
  }
569
535
  tryUnicodeIcon(name) {
@@ -604,16 +570,8 @@ class TnIconComponent {
604
570
  // Default to first 2 characters
605
571
  return name.substring(0, 2).toUpperCase();
606
572
  }
607
- cssClassExists(_className) {
608
- if (typeof document === 'undefined') {
609
- return false;
610
- }
611
- // For now, only return true for known CSS icon patterns
612
- // In real implementation, consumers would override this method
613
- return false; // Disable generic CSS class checking for now
614
- }
615
573
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnIconComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
616
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.0", type: TnIconComponent, isStandalone: true, selector: "tn-icon", inputs: { name: { classPropertyName: "name", publicName: "name", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, color: { classPropertyName: "color", publicName: "color", isSignal: true, isRequired: false, transformFunction: null }, tooltip: { classPropertyName: "tooltip", publicName: "tooltip", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null }, library: { classPropertyName: "library", publicName: "library", isSignal: true, isRequired: false, transformFunction: null }, fullSize: { classPropertyName: "fullSize", publicName: "fullSize", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "attr.name": "name()", "attr.library": "library()", "attr.size": "size()", "attr.color": "color()", "attr.full-size": "fullSize() || null" } }, viewQueries: [{ propertyName: "svgContainer", first: true, predicate: ["svgContainer"], descendants: true, isSignal: true }], ngImport: i0, template: "<div\n class=\"tn-icon\"\n role=\"img\"\n [ngClass]=\"fullSize() ? 'tn-icon--full' : 'tn-icon--' + size()\"\n [style.color]=\"color()\"\n [attr.aria-label]=\"effectiveAriaLabel()\"\n [attr.title]=\"tooltip()\">\n \n\n @switch (iconResult.source) {\n <!-- Sprite icons (from generated sprite.svg) -->\n @case ('sprite') {\n <svg\n class=\"tn-icon__sprite\"\n aria-hidden=\"true\">\n <use [attr.href]=\"iconResult.spriteUrl\" />\n </svg>\n }\n\n <!-- SVG content (from third-party libraries or assets) -->\n @case ('svg') {\n <div\n #svgContainer\n class=\"tn-icon__svg\">\n </div>\n }\n\n <!-- CSS class icons (Font Awesome, Material Icons, etc.) -->\n @case ('css') {\n <i\n aria-hidden=\"true\"\n [class]=\"'tn-icon__css ' + iconResult.content\">\n </i>\n }\n\n <!-- Unicode characters -->\n @case ('unicode') {\n <span\n class=\"tn-icon__unicode\"\n aria-hidden=\"true\">{{ iconResult.content }}</span>\n }\n\n <!-- Text abbreviation fallback -->\n @default {\n <span\n class=\"tn-icon__text\"\n aria-hidden=\"true\">{{ iconResult.content }}</span>\n }\n }\n</div>", styles: [".tn-icon{display:inline-flex;align-items:center;justify-content:center;vertical-align:middle}.tn-icon--xs{width:var(--tn-icon-xs)!important;height:var(--tn-icon-xs)!important;font-size:var(--tn-icon-xs)!important}.tn-icon--sm{width:var(--tn-icon-sm)!important;height:var(--tn-icon-sm)!important;font-size:var(--tn-icon-sm)!important}.tn-icon--md{width:var(--tn-icon-md)!important;height:var(--tn-icon-md)!important;font-size:var(--tn-icon-md)!important}.tn-icon--lg{width:var(--tn-icon-lg)!important;height:var(--tn-icon-lg)!important;font-size:var(--tn-icon-lg)!important}.tn-icon--xl{width:var(--tn-icon-xl)!important;height:var(--tn-icon-xl)!important;font-size:var(--tn-icon-xl)!important}.tn-icon--full{width:100%!important;height:100%!important}.tn-icon__sprite{width:100%;height:100%;fill:currentColor;color:inherit}.tn-icon__svg{width:100%;height:100%;display:flex;align-items:center;justify-content:center}.tn-icon__svg :global(svg){width:100%;height:100%;fill:currentColor;color:inherit}.tn-icon__css{font-size:inherit;line-height:1;color:inherit}.tn-icon__unicode{font-size:inherit;line-height:1;color:inherit;text-align:center}.tn-icon__text{font-size:.75em;font-weight:600;line-height:1;color:inherit;text-align:center;opacity:.7}.tn-icon{width:var(--tn-icon-size, var(--tn-icon-md));height:var(--tn-icon-size, var(--tn-icon-md));color:var(--tn-icon-color, currentColor)}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1$1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
574
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.0", type: TnIconComponent, isStandalone: true, selector: "tn-icon", inputs: { name: { classPropertyName: "name", publicName: "name", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, color: { classPropertyName: "color", publicName: "color", isSignal: true, isRequired: false, transformFunction: null }, tooltip: { classPropertyName: "tooltip", publicName: "tooltip", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null }, library: { classPropertyName: "library", publicName: "library", isSignal: true, isRequired: false, transformFunction: null }, fullSize: { classPropertyName: "fullSize", publicName: "fullSize", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "attr.name": "name()", "attr.library": "library()", "attr.size": "size()", "attr.color": "color()", "attr.full-size": "fullSize() || null" } }, viewQueries: [{ propertyName: "svgContainer", first: true, predicate: ["svgContainer"], descendants: true, isSignal: true }], ngImport: i0, template: "<div\n class=\"tn-icon\"\n role=\"img\"\n [ngClass]=\"fullSize() ? 'tn-icon--full' : 'tn-icon--' + size()\"\n [style.color]=\"color()\"\n [attr.aria-label]=\"effectiveAriaLabel()\"\n [attr.title]=\"tooltip()\">\n \n\n @switch (iconResult().source) {\n <!-- Sprite icons (from generated sprite.svg) -->\n @case ('sprite') {\n <svg\n class=\"tn-icon__sprite\"\n aria-hidden=\"true\">\n <use [attr.href]=\"iconResult().spriteUrl\" />\n </svg>\n }\n\n <!-- SVG content (from third-party libraries or assets) -->\n @case ('svg') {\n <div\n #svgContainer\n class=\"tn-icon__svg\">\n </div>\n }\n\n <!-- CSS class icons (Font Awesome, Material Icons, etc.) -->\n @case ('css') {\n <i\n aria-hidden=\"true\"\n [class]=\"'tn-icon__css ' + iconResult().content\">\n </i>\n }\n\n <!-- Unicode characters -->\n @case ('unicode') {\n <span\n class=\"tn-icon__unicode\"\n aria-hidden=\"true\">{{ iconResult().content }}</span>\n }\n\n <!-- Text abbreviation fallback -->\n @default {\n <span\n class=\"tn-icon__text\"\n aria-hidden=\"true\">{{ iconResult().content }}</span>\n }\n }\n</div>", styles: [".tn-icon{display:inline-flex;align-items:center;justify-content:center;vertical-align:middle}.tn-icon--xs{width:var(--tn-icon-xs)!important;height:var(--tn-icon-xs)!important;font-size:var(--tn-icon-xs)!important}.tn-icon--sm{width:var(--tn-icon-sm)!important;height:var(--tn-icon-sm)!important;font-size:var(--tn-icon-sm)!important}.tn-icon--md{width:var(--tn-icon-md)!important;height:var(--tn-icon-md)!important;font-size:var(--tn-icon-md)!important}.tn-icon--lg{width:var(--tn-icon-lg)!important;height:var(--tn-icon-lg)!important;font-size:var(--tn-icon-lg)!important}.tn-icon--xl{width:var(--tn-icon-xl)!important;height:var(--tn-icon-xl)!important;font-size:var(--tn-icon-xl)!important}.tn-icon--full{width:100%!important;height:100%!important}.tn-icon__sprite{width:100%;height:100%;fill:currentColor;color:inherit}.tn-icon__svg{width:100%;height:100%;display:flex;align-items:center;justify-content:center}.tn-icon__svg :global(svg){width:100%;height:100%;fill:currentColor;color:inherit}.tn-icon__css{font-size:inherit;line-height:1;color:inherit}.tn-icon__unicode{font-size:inherit;line-height:1;color:inherit;text-align:center}.tn-icon__text{font-size:.75em;font-weight:600;line-height:1;color:inherit;text-align:center;opacity:.7}.tn-icon{width:var(--tn-icon-size, var(--tn-icon-md));height:var(--tn-icon-size, var(--tn-icon-md));color:var(--tn-icon-color, currentColor)}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1$1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
617
575
  }
618
576
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnIconComponent, decorators: [{
619
577
  type: Component,
@@ -623,7 +581,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
623
581
  '[attr.size]': 'size()',
624
582
  '[attr.color]': 'color()',
625
583
  '[attr.full-size]': 'fullSize() || null'
626
- }, template: "<div\n class=\"tn-icon\"\n role=\"img\"\n [ngClass]=\"fullSize() ? 'tn-icon--full' : 'tn-icon--' + size()\"\n [style.color]=\"color()\"\n [attr.aria-label]=\"effectiveAriaLabel()\"\n [attr.title]=\"tooltip()\">\n \n\n @switch (iconResult.source) {\n <!-- Sprite icons (from generated sprite.svg) -->\n @case ('sprite') {\n <svg\n class=\"tn-icon__sprite\"\n aria-hidden=\"true\">\n <use [attr.href]=\"iconResult.spriteUrl\" />\n </svg>\n }\n\n <!-- SVG content (from third-party libraries or assets) -->\n @case ('svg') {\n <div\n #svgContainer\n class=\"tn-icon__svg\">\n </div>\n }\n\n <!-- CSS class icons (Font Awesome, Material Icons, etc.) -->\n @case ('css') {\n <i\n aria-hidden=\"true\"\n [class]=\"'tn-icon__css ' + iconResult.content\">\n </i>\n }\n\n <!-- Unicode characters -->\n @case ('unicode') {\n <span\n class=\"tn-icon__unicode\"\n aria-hidden=\"true\">{{ iconResult.content }}</span>\n }\n\n <!-- Text abbreviation fallback -->\n @default {\n <span\n class=\"tn-icon__text\"\n aria-hidden=\"true\">{{ iconResult.content }}</span>\n }\n }\n</div>", styles: [".tn-icon{display:inline-flex;align-items:center;justify-content:center;vertical-align:middle}.tn-icon--xs{width:var(--tn-icon-xs)!important;height:var(--tn-icon-xs)!important;font-size:var(--tn-icon-xs)!important}.tn-icon--sm{width:var(--tn-icon-sm)!important;height:var(--tn-icon-sm)!important;font-size:var(--tn-icon-sm)!important}.tn-icon--md{width:var(--tn-icon-md)!important;height:var(--tn-icon-md)!important;font-size:var(--tn-icon-md)!important}.tn-icon--lg{width:var(--tn-icon-lg)!important;height:var(--tn-icon-lg)!important;font-size:var(--tn-icon-lg)!important}.tn-icon--xl{width:var(--tn-icon-xl)!important;height:var(--tn-icon-xl)!important;font-size:var(--tn-icon-xl)!important}.tn-icon--full{width:100%!important;height:100%!important}.tn-icon__sprite{width:100%;height:100%;fill:currentColor;color:inherit}.tn-icon__svg{width:100%;height:100%;display:flex;align-items:center;justify-content:center}.tn-icon__svg :global(svg){width:100%;height:100%;fill:currentColor;color:inherit}.tn-icon__css{font-size:inherit;line-height:1;color:inherit}.tn-icon__unicode{font-size:inherit;line-height:1;color:inherit;text-align:center}.tn-icon__text{font-size:.75em;font-weight:600;line-height:1;color:inherit;text-align:center;opacity:.7}.tn-icon{width:var(--tn-icon-size, var(--tn-icon-md));height:var(--tn-icon-size, var(--tn-icon-md));color:var(--tn-icon-color, currentColor)}\n"] }]
584
+ }, template: "<div\n class=\"tn-icon\"\n role=\"img\"\n [ngClass]=\"fullSize() ? 'tn-icon--full' : 'tn-icon--' + size()\"\n [style.color]=\"color()\"\n [attr.aria-label]=\"effectiveAriaLabel()\"\n [attr.title]=\"tooltip()\">\n \n\n @switch (iconResult().source) {\n <!-- Sprite icons (from generated sprite.svg) -->\n @case ('sprite') {\n <svg\n class=\"tn-icon__sprite\"\n aria-hidden=\"true\">\n <use [attr.href]=\"iconResult().spriteUrl\" />\n </svg>\n }\n\n <!-- SVG content (from third-party libraries or assets) -->\n @case ('svg') {\n <div\n #svgContainer\n class=\"tn-icon__svg\">\n </div>\n }\n\n <!-- CSS class icons (Font Awesome, Material Icons, etc.) -->\n @case ('css') {\n <i\n aria-hidden=\"true\"\n [class]=\"'tn-icon__css ' + iconResult().content\">\n </i>\n }\n\n <!-- Unicode characters -->\n @case ('unicode') {\n <span\n class=\"tn-icon__unicode\"\n aria-hidden=\"true\">{{ iconResult().content }}</span>\n }\n\n <!-- Text abbreviation fallback -->\n @default {\n <span\n class=\"tn-icon__text\"\n aria-hidden=\"true\">{{ iconResult().content }}</span>\n }\n }\n</div>", styles: [".tn-icon{display:inline-flex;align-items:center;justify-content:center;vertical-align:middle}.tn-icon--xs{width:var(--tn-icon-xs)!important;height:var(--tn-icon-xs)!important;font-size:var(--tn-icon-xs)!important}.tn-icon--sm{width:var(--tn-icon-sm)!important;height:var(--tn-icon-sm)!important;font-size:var(--tn-icon-sm)!important}.tn-icon--md{width:var(--tn-icon-md)!important;height:var(--tn-icon-md)!important;font-size:var(--tn-icon-md)!important}.tn-icon--lg{width:var(--tn-icon-lg)!important;height:var(--tn-icon-lg)!important;font-size:var(--tn-icon-lg)!important}.tn-icon--xl{width:var(--tn-icon-xl)!important;height:var(--tn-icon-xl)!important;font-size:var(--tn-icon-xl)!important}.tn-icon--full{width:100%!important;height:100%!important}.tn-icon__sprite{width:100%;height:100%;fill:currentColor;color:inherit}.tn-icon__svg{width:100%;height:100%;display:flex;align-items:center;justify-content:center}.tn-icon__svg :global(svg){width:100%;height:100%;fill:currentColor;color:inherit}.tn-icon__css{font-size:inherit;line-height:1;color:inherit}.tn-icon__unicode{font-size:inherit;line-height:1;color:inherit;text-align:center}.tn-icon__text{font-size:.75em;font-weight:600;line-height:1;color:inherit;text-align:center;opacity:.7}.tn-icon{width:var(--tn-icon-size, var(--tn-icon-md));height:var(--tn-icon-size, var(--tn-icon-md));color:var(--tn-icon-color, currentColor)}\n"] }]
627
585
  }], ctorParameters: () => [], propDecorators: { name: [{ type: i0.Input, args: [{ isSignal: true, alias: "name", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], color: [{ type: i0.Input, args: [{ isSignal: true, alias: "color", required: false }] }], tooltip: [{ type: i0.Input, args: [{ isSignal: true, alias: "tooltip", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], library: [{ type: i0.Input, args: [{ isSignal: true, alias: "library", required: false }] }], fullSize: [{ type: i0.Input, args: [{ isSignal: true, alias: "fullSize", required: false }] }], svgContainer: [{ type: i0.ViewChild, args: ['svgContainer', { isSignal: true }] }] } });
628
586
 
629
587
  /**
@@ -2587,6 +2545,196 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
2587
2545
  ], template: "<div class=\"tn-select-container\" [attr.data-testid]=\"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]=\"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.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]=\"isSelected()(option)\"\n [class.disabled]=\"option.disabled\"\n [attr.aria-selected]=\"isSelected()(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\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 <!-- Group Label -->\n <div\n class=\"tn-select-group-label\"\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]=\"isSelected()(option)\"\n [class.disabled]=\"option.disabled || group.disabled\"\n [attr.aria-selected]=\"isSelected()(option)\"\n [attr.aria-disabled]=\"option.disabled || group.disabled\"\n (mousedown)=\"$event.preventDefault()\"\n (click)=\"onOptionClick(option)\"\n (keydown.enter)=\"onOptionClick(option)\"\n (keydown.space)=\"onOptionClick(option)\">\n\n {{ option.label }}\n </div>\n }\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>", 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-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"] }]
2588
2546
  }], 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 }] }], selectionChange: [{ type: i0.Output, args: ["selectionChange"] }] } });
2589
2547
 
2548
+ /**
2549
+ * Harness for interacting with tn-select in tests.
2550
+ * Provides methods for querying select state, opening/closing the dropdown,
2551
+ * and selecting options.
2552
+ *
2553
+ * @example
2554
+ * ```typescript
2555
+ * // Find a select and choose an option
2556
+ * const select = await loader.getHarness(TnSelectHarness);
2557
+ * await select.selectOption('Option 2');
2558
+ *
2559
+ * // Check the displayed text
2560
+ * expect(await select.getDisplayText()).toBe('Option 2');
2561
+ *
2562
+ * // Filter by displayed text
2563
+ * const fruitSelect = await loader.getHarness(
2564
+ * TnSelectHarness.with({ displayText: /Apple/i })
2565
+ * );
2566
+ *
2567
+ * // Check disabled state
2568
+ * const disabledSelect = await loader.getHarness(TnSelectHarness);
2569
+ * expect(await disabledSelect.isDisabled()).toBe(true);
2570
+ * ```
2571
+ */
2572
+ class TnSelectHarness extends ComponentHarness {
2573
+ /**
2574
+ * The selector for the host element of a `TnSelectComponent` instance.
2575
+ */
2576
+ static hostSelector = 'tn-select';
2577
+ _trigger = this.locatorFor('.tn-select-trigger');
2578
+ _text = this.locatorFor('.tn-select-text');
2579
+ /**
2580
+ * Gets a `HarnessPredicate` that can be used to search for a select
2581
+ * with specific attributes.
2582
+ *
2583
+ * @param options Options for filtering which select instances are considered a match.
2584
+ * @returns A `HarnessPredicate` configured with the given options.
2585
+ *
2586
+ * @example
2587
+ * ```typescript
2588
+ * // Find select by exact display text
2589
+ * const select = await loader.getHarness(TnSelectHarness.with({ displayText: 'Apple' }));
2590
+ *
2591
+ * // Find select by regex pattern
2592
+ * const select = await loader.getHarness(TnSelectHarness.with({ displayText: /Option/i }));
2593
+ * ```
2594
+ */
2595
+ static with(options = {}) {
2596
+ return new HarnessPredicate(TnSelectHarness, options)
2597
+ .addOption('displayText', options.displayText, (harness, text) => HarnessPredicate.stringMatches(harness.getDisplayText(), text));
2598
+ }
2599
+ /**
2600
+ * Gets the currently displayed text (selected label or placeholder).
2601
+ *
2602
+ * @returns Promise resolving to the display text content.
2603
+ *
2604
+ * @example
2605
+ * ```typescript
2606
+ * const select = await loader.getHarness(TnSelectHarness);
2607
+ * const text = await select.getDisplayText();
2608
+ * expect(text).toBe('Select an option');
2609
+ * ```
2610
+ */
2611
+ async getDisplayText() {
2612
+ const text = await this._text();
2613
+ return (await text.text()).trim();
2614
+ }
2615
+ /**
2616
+ * Checks whether the select is disabled.
2617
+ *
2618
+ * @returns Promise resolving to true if the select trigger has the disabled class.
2619
+ *
2620
+ * @example
2621
+ * ```typescript
2622
+ * const select = await loader.getHarness(TnSelectHarness);
2623
+ * expect(await select.isDisabled()).toBe(false);
2624
+ * ```
2625
+ */
2626
+ async isDisabled() {
2627
+ const trigger = await this._trigger();
2628
+ return trigger.hasClass('disabled');
2629
+ }
2630
+ /**
2631
+ * Checks whether the dropdown is currently open.
2632
+ *
2633
+ * @returns Promise resolving to true if the dropdown is open.
2634
+ *
2635
+ * @example
2636
+ * ```typescript
2637
+ * const select = await loader.getHarness(TnSelectHarness);
2638
+ * expect(await select.isOpen()).toBe(false);
2639
+ * await select.open();
2640
+ * expect(await select.isOpen()).toBe(true);
2641
+ * ```
2642
+ */
2643
+ async isOpen() {
2644
+ const trigger = await this._trigger();
2645
+ return trigger.hasClass('open');
2646
+ }
2647
+ /**
2648
+ * Opens the dropdown. No-op if already open.
2649
+ *
2650
+ * @returns Promise that resolves when the dropdown is open.
2651
+ *
2652
+ * @example
2653
+ * ```typescript
2654
+ * const select = await loader.getHarness(TnSelectHarness);
2655
+ * await select.open();
2656
+ * ```
2657
+ */
2658
+ async open() {
2659
+ if (!(await this.isOpen())) {
2660
+ const trigger = await this._trigger();
2661
+ await trigger.click();
2662
+ }
2663
+ }
2664
+ /**
2665
+ * Closes the dropdown. No-op if already closed.
2666
+ *
2667
+ * @returns Promise that resolves when the dropdown is closed.
2668
+ *
2669
+ * @example
2670
+ * ```typescript
2671
+ * const select = await loader.getHarness(TnSelectHarness);
2672
+ * await select.open();
2673
+ * await select.close();
2674
+ * expect(await select.isOpen()).toBe(false);
2675
+ * ```
2676
+ */
2677
+ async close() {
2678
+ if (await this.isOpen()) {
2679
+ const trigger = await this._trigger();
2680
+ await trigger.click();
2681
+ }
2682
+ }
2683
+ /**
2684
+ * Selects an option by its label text. Opens the dropdown if needed.
2685
+ *
2686
+ * @param filter The text to match against option labels. Supports string or RegExp.
2687
+ * @returns Promise that resolves when the option has been selected.
2688
+ *
2689
+ * @example
2690
+ * ```typescript
2691
+ * const select = await loader.getHarness(TnSelectHarness);
2692
+ *
2693
+ * // Select by exact text
2694
+ * await select.selectOption('Option 2');
2695
+ *
2696
+ * // Select by regex
2697
+ * await select.selectOption(/option 2/i);
2698
+ * ```
2699
+ */
2700
+ async selectOption(filter) {
2701
+ await this.open();
2702
+ const options = await this.locatorForAll('.tn-select-option')();
2703
+ for (const option of options) {
2704
+ const text = (await option.text()).trim();
2705
+ const matches = filter instanceof RegExp
2706
+ ? filter.test(text)
2707
+ : text === filter;
2708
+ if (matches) {
2709
+ await option.click();
2710
+ return;
2711
+ }
2712
+ }
2713
+ throw new Error(`Could not find option matching "${filter}"`);
2714
+ }
2715
+ /**
2716
+ * Gets the labels of all available options. Opens the dropdown if needed.
2717
+ *
2718
+ * @returns Promise resolving to an array of option label strings.
2719
+ *
2720
+ * @example
2721
+ * ```typescript
2722
+ * const select = await loader.getHarness(TnSelectHarness);
2723
+ * const options = await select.getOptions();
2724
+ * expect(options).toEqual(['Option 1', 'Option 2', 'Option 3']);
2725
+ * ```
2726
+ */
2727
+ async getOptions() {
2728
+ await this.open();
2729
+ const options = await this.locatorForAll('.tn-select-option')();
2730
+ const labels = [];
2731
+ for (const option of options) {
2732
+ labels.push((await option.text()).trim());
2733
+ }
2734
+ return labels;
2735
+ }
2736
+ }
2737
+
2590
2738
  /**
2591
2739
  * Harness for interacting with tn-icon in tests.
2592
2740
  * Provides filtering by icon name and library for existence checks, as well as click interaction.
@@ -8501,5 +8649,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
8501
8649
  * Generated bundle index. Do not edit.
8502
8650
  */
8503
8651
 
8504
- export { CommonShortcuts, DEFAULT_THEME, DiskIconComponent, DiskType, FileSizePipe, InputType, LinuxModifierKeys, LinuxShortcuts, ModifierKeys, QuickShortcuts, ShortcutBuilder, StripMntPrefixPipe, THEME_MAP, THEME_STORAGE_KEY, TN_THEME_DEFINITIONS, TnBannerActionDirective, TnBannerComponent, TnBannerHarness, TnBrandedSpinnerComponent, TnButtonComponent, TnButtonHarness, TnButtonToggleComponent, TnButtonToggleGroupComponent, TnCalendarComponent, TnCalendarHeaderComponent, TnCardComponent, TnCellDefDirective, TnCheckboxComponent, TnChipComponent, TnConfirmDialogComponent, TnDateInputComponent, TnDateRangeInputComponent, TnDialog, TnDialogShellComponent, TnDividerComponent, TnDividerDirective, TnExpansionPanelComponent, TnFilePickerComponent, TnFilePickerPopupComponent, TnFormFieldComponent, TnHeaderCellDefDirective, TnIconButtonComponent, TnIconButtonHarness, TnIconComponent, TnIconHarness, TnIconRegistryService, TnIconTesting, TnInputComponent, TnInputDirective, TnKeyboardShortcutComponent, TnKeyboardShortcutService, TnListAvatarDirective, TnListComponent, TnListIconDirective, TnListItemComponent, TnListItemLineDirective, TnListItemPrimaryDirective, TnListItemSecondaryDirective, TnListItemTitleDirective, TnListItemTrailingDirective, TnListOptionComponent, TnListSubheaderComponent, TnMenuComponent, TnMenuTriggerDirective, TnMonthViewComponent, TnMultiYearViewComponent, TnNestedTreeNodeComponent, TnParticleProgressBarComponent, TnProgressBarComponent, TnRadioComponent, TnSelectComponent, TnSelectionListComponent, TnSlideToggleComponent, TnSliderComponent, TnSliderThumbDirective, TnSliderWithLabelDirective, TnSpinnerComponent, TnSpriteLoaderService, TnStepComponent, TnStepperComponent, TnTabComponent, TnTabPanelComponent, TnTableColumnDirective, TnTableComponent, TnTabsComponent, TnTheme, TnThemeService, TnTimeInputComponent, TnTooltipComponent, TnTooltipDirective, TnTreeComponent, TnTreeFlatDataSource, TnTreeFlattener, TnTreeNodeComponent, TnTreeNodeOutletDirective, TruncatePathPipe, WindowsModifierKeys, WindowsShortcuts, createLucideLibrary, createShortcut, defaultSpriteBasePath, defaultSpriteConfigPath, libIconMarker, registerLucideIcons, setupLucideIntegration, tnIconMarker };
8652
+ export { CommonShortcuts, DEFAULT_THEME, DiskIconComponent, DiskType, FileSizePipe, InputType, LinuxModifierKeys, LinuxShortcuts, ModifierKeys, QuickShortcuts, ShortcutBuilder, StripMntPrefixPipe, THEME_MAP, THEME_STORAGE_KEY, TN_THEME_DEFINITIONS, TnBannerActionDirective, TnBannerComponent, TnBannerHarness, TnBrandedSpinnerComponent, TnButtonComponent, TnButtonHarness, TnButtonToggleComponent, TnButtonToggleGroupComponent, TnCalendarComponent, TnCalendarHeaderComponent, TnCardComponent, TnCellDefDirective, TnCheckboxComponent, TnChipComponent, TnConfirmDialogComponent, TnDateInputComponent, TnDateRangeInputComponent, TnDialog, TnDialogShellComponent, TnDividerComponent, TnDividerDirective, TnExpansionPanelComponent, TnFilePickerComponent, TnFilePickerPopupComponent, TnFormFieldComponent, TnHeaderCellDefDirective, TnIconButtonComponent, TnIconButtonHarness, TnIconComponent, TnIconHarness, TnIconRegistryService, TnIconTesting, TnInputComponent, TnInputDirective, TnKeyboardShortcutComponent, TnKeyboardShortcutService, TnListAvatarDirective, TnListComponent, TnListIconDirective, TnListItemComponent, TnListItemLineDirective, TnListItemPrimaryDirective, TnListItemSecondaryDirective, TnListItemTitleDirective, TnListItemTrailingDirective, TnListOptionComponent, TnListSubheaderComponent, TnMenuComponent, TnMenuTriggerDirective, TnMonthViewComponent, TnMultiYearViewComponent, TnNestedTreeNodeComponent, TnParticleProgressBarComponent, TnProgressBarComponent, TnRadioComponent, TnSelectComponent, TnSelectHarness, TnSelectionListComponent, TnSlideToggleComponent, TnSliderComponent, TnSliderThumbDirective, TnSliderWithLabelDirective, TnSpinnerComponent, TnSpriteLoaderService, TnStepComponent, TnStepperComponent, TnTabComponent, TnTabPanelComponent, TnTableColumnDirective, TnTableComponent, TnTabsComponent, TnTheme, TnThemeService, TnTimeInputComponent, TnTooltipComponent, TnTooltipDirective, TnTreeComponent, TnTreeFlatDataSource, TnTreeFlattener, TnTreeNodeComponent, TnTreeNodeOutletDirective, TruncatePathPipe, WindowsModifierKeys, WindowsShortcuts, createLucideLibrary, createShortcut, defaultSpriteBasePath, defaultSpriteConfigPath, libIconMarker, registerLucideIcons, setupLucideIntegration, tnIconMarker };
8505
8653
  //# sourceMappingURL=truenas-ui-components.mjs.map