@truenas/ui-components 0.1.18 → 0.1.20

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.
@@ -22,7 +22,8 @@ import * as i1$2 from '@angular/cdk/tree';
22
22
  import { CdkTree, CdkTreeModule, CdkTreeNode, CDK_TREE_NODE_OUTLET_NODE, CdkTreeNodeOutlet, CdkNestedTreeNode } from '@angular/cdk/tree';
23
23
  export { FlatTreeControl } from '@angular/cdk/tree';
24
24
  import { map } from 'rxjs/operators';
25
- import { Dialog, DialogRef, DIALOG_DATA } from '@angular/cdk/dialog';
25
+ import { DialogRef, DIALOG_DATA, Dialog } from '@angular/cdk/dialog';
26
+ import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed';
26
27
  import { ScrollingModule } from '@angular/cdk/scrolling';
27
28
 
28
29
  var DiskType;
@@ -1669,6 +1670,7 @@ class TnMenuTriggerDirective {
1669
1670
  tnMenuPosition = input('below', ...(ngDevMode ? [{ debugName: "tnMenuPosition" }] : []));
1670
1671
  overlayRef;
1671
1672
  isMenuOpen = signal(false, ...(ngDevMode ? [{ debugName: "isMenuOpen" }] : []));
1673
+ itemClickSub;
1672
1674
  elementRef = inject(ElementRef);
1673
1675
  overlay = inject(Overlay);
1674
1676
  viewContainerRef = inject(ViewContainerRef);
@@ -1712,10 +1714,16 @@ class TnMenuTriggerDirective {
1712
1714
  this.overlayRef.backdropClick().subscribe(() => {
1713
1715
  this.closeMenu();
1714
1716
  });
1717
+ // Close menu when a leaf item is selected
1718
+ this.itemClickSub = menuComponent.menuItemClick.subscribe(() => {
1719
+ this.closeMenu();
1720
+ });
1715
1721
  // Notify menu component
1716
1722
  menuComponent.onMenuOpen();
1717
1723
  }
1718
1724
  closeMenu() {
1725
+ this.itemClickSub?.unsubscribe();
1726
+ this.itemClickSub = undefined;
1719
1727
  if (this.overlayRef) {
1720
1728
  this.overlayRef.dispose();
1721
1729
  this.overlayRef = undefined;
@@ -1762,6 +1770,38 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
1762
1770
  }]
1763
1771
  }], propDecorators: { menu: [{ type: i0.Input, args: [{ isSignal: true, alias: "tnMenuTriggerFor", required: true }] }], tnMenuPosition: [{ type: i0.Input, args: [{ isSignal: true, alias: "tnMenuPosition", required: false }] }] } });
1764
1772
 
1773
+ /**
1774
+ * Activates CDK menu hover-to-open behavior for menus opened via custom overlays.
1775
+ *
1776
+ * CDK's hover-to-open for submenu triggers is guarded by `!menuStack.isEmpty()`.
1777
+ * When a CdkMenu is opened via TnMenuTriggerDirective (custom overlay) instead of
1778
+ * CDK's own CdkMenuTrigger, the menu is considered "inline" and doesn't register
1779
+ * with the stack, disabling hover for its submenu triggers.
1780
+ *
1781
+ * This directive pushes the CdkMenu to its own stack and focuses the element
1782
+ * (to trigger the CdkMenu's focusin host listener, preventing the hasFocus
1783
+ * auto-close subscription from immediately clearing the stack).
1784
+ */
1785
+ class TnMenuActivateHoverDirective {
1786
+ cdkMenu = inject(CdkMenu);
1787
+ elementRef = inject(ElementRef);
1788
+ ngAfterContentInit() {
1789
+ const stack = this.cdkMenu.menuStack;
1790
+ if (stack.isEmpty()) {
1791
+ stack.push(this.cdkMenu);
1792
+ this.elementRef.nativeElement.focus({ preventScroll: true });
1793
+ }
1794
+ }
1795
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnMenuActivateHoverDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
1796
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.1.0", type: TnMenuActivateHoverDirective, isStandalone: true, selector: "[tnMenuActivateHover]", ngImport: i0 });
1797
+ }
1798
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnMenuActivateHoverDirective, decorators: [{
1799
+ type: Directive,
1800
+ args: [{
1801
+ selector: '[tnMenuActivateHover]',
1802
+ standalone: true,
1803
+ }]
1804
+ }] });
1765
1805
  class TnMenuComponent {
1766
1806
  items = input([], ...(ngDevMode ? [{ debugName: "items" }] : []));
1767
1807
  contextMenu = input(false, ...(ngDevMode ? [{ debugName: "contextMenu" }] : [])); // Enable context menu mode (right-click)
@@ -1852,11 +1892,11 @@ class TnMenuComponent {
1852
1892
  return item.id;
1853
1893
  }
1854
1894
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnMenuComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1855
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.0", type: TnMenuComponent, isStandalone: true, selector: "tn-menu", inputs: { items: { classPropertyName: "items", publicName: "items", isSignal: true, isRequired: false, transformFunction: null }, contextMenu: { classPropertyName: "contextMenu", publicName: "contextMenu", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { menuItemClick: "menuItemClick", menuOpen: "menuOpen", menuClose: "menuClose" }, viewQueries: [{ propertyName: "menuTemplate", first: true, predicate: ["menuTemplate"], descendants: true, isSignal: true }, { propertyName: "contextMenuTemplate", first: true, predicate: ["contextMenuTemplate"], descendants: true, isSignal: true }], ngImport: i0, template: "<!-- Context menu content slot -->\n@if (contextMenu()) {\n <div class=\"tn-menu-context-content\" (contextmenu)=\"onContextMenu($event)\">\n <ng-content />\n </div>\n}\n\n <!-- Context menu template for overlay -->\n <ng-template #contextMenuTemplate>\n <div class=\"tn-menu\" cdkMenu>\n @for (item of items(); track trackByItemId($index, item)) {\n @if (item.separator) {\n <div\n class=\"tn-menu-separator\"\n role=\"separator\"\n ></div>\n } @else {\n @if (!item.children || item.children.length === 0) {\n <button\n cdkMenuItem\n class=\"tn-menu-item\"\n type=\"button\"\n [disabled]=\"item.disabled\"\n [class.disabled]=\"item.disabled\"\n (click)=\"onMenuItemClick(item)\"\n >\n @if (item.icon) {\n <tn-icon size=\"sm\" class=\"tn-menu-item-icon\" [name]=\"item.icon\" [library]=\"item.iconLibrary\" />\n }\n <span class=\"tn-menu-item-label\">{{ item.label }}</span>\n @if (item.shortcut) {\n <span class=\"tn-menu-item-shortcut\">{{ item.shortcut }}</span>\n }\n </button>\n } @else {\n <button\n cdkMenuItem\n class=\"tn-menu-item tn-menu-item--nested\"\n type=\"button\"\n [cdkMenuTriggerFor]=\"nestedMenu\"\n [disabled]=\"item.disabled\"\n [class.disabled]=\"item.disabled\"\n >\n @if (item.icon) {\n <tn-icon size=\"sm\" class=\"tn-menu-item-icon\" [name]=\"item.icon\" [library]=\"item.iconLibrary\" />\n }\n <span class=\"tn-menu-item-label\">{{ item.label }}</span>\n @if (item.shortcut) {\n <span class=\"tn-menu-item-shortcut\">{{ item.shortcut }}</span>\n }\n <span class=\"tn-menu-item-arrow\">\u25B6</span>\n </button>\n\n <ng-template #nestedMenu>\n <div class=\"tn-menu tn-menu--nested\" cdkMenu>\n @for (nestedItem of item.children; track trackByItemId($index, nestedItem)) {\n @if (nestedItem.separator) {\n <div\n class=\"tn-menu-separator\"\n role=\"separator\"\n ></div>\n } @else {\n @if (!nestedItem.children || nestedItem.children.length === 0) {\n <button\n cdkMenuItem\n class=\"tn-menu-item\"\n type=\"button\"\n [disabled]=\"nestedItem.disabled\"\n [class.disabled]=\"nestedItem.disabled\"\n (click)=\"onMenuItemClick(nestedItem)\"\n >\n @if (nestedItem.icon) {\n <tn-icon size=\"sm\" class=\"tn-menu-item-icon\" [name]=\"nestedItem.icon\" [library]=\"nestedItem.iconLibrary\" />\n }\n <span class=\"tn-menu-item-label\">{{ nestedItem.label }}</span>\n @if (nestedItem.shortcut) {\n <span class=\"tn-menu-item-shortcut\">{{ nestedItem.shortcut }}</span>\n }\n </button>\n } @else {\n <button\n cdkMenuItem\n class=\"tn-menu-item tn-menu-item--nested\"\n type=\"button\"\n [cdkMenuTriggerFor]=\"deepNestedMenu\"\n [disabled]=\"nestedItem.disabled\"\n [class.disabled]=\"nestedItem.disabled\"\n >\n @if (nestedItem.icon) {\n <tn-icon size=\"sm\" class=\"tn-menu-item-icon\" [name]=\"nestedItem.icon\" [library]=\"nestedItem.iconLibrary\" />\n }\n <span class=\"tn-menu-item-label\">{{ nestedItem.label }}</span>\n @if (nestedItem.shortcut) {\n <span class=\"tn-menu-item-shortcut\">{{ nestedItem.shortcut }}</span>\n }\n <span class=\"tn-menu-item-arrow\">\u25B6</span>\n </button>\n\n <ng-template #deepNestedMenu>\n <div class=\"tn-menu tn-menu--nested\" cdkMenu>\n @for (deepNestedItem of nestedItem.children; track trackByItemId($index, deepNestedItem)) {\n @if (deepNestedItem.separator) {\n <div\n class=\"tn-menu-separator\"\n role=\"separator\"\n ></div>\n } @else {\n <button\n cdkMenuItem\n class=\"tn-menu-item\"\n type=\"button\"\n [disabled]=\"deepNestedItem.disabled\"\n [class.disabled]=\"deepNestedItem.disabled\"\n (click)=\"onMenuItemClick(deepNestedItem)\"\n >\n @if (deepNestedItem.icon) {\n <tn-icon size=\"sm\" class=\"tn-menu-item-icon\" [name]=\"deepNestedItem.icon\" [library]=\"deepNestedItem.iconLibrary\" />\n }\n <span class=\"tn-menu-item-label\">{{ deepNestedItem.label }}</span>\n @if (deepNestedItem.shortcut) {\n <span class=\"tn-menu-item-shortcut\">{{ deepNestedItem.shortcut }}</span>\n }\n </button>\n }\n }\n </div>\n </ng-template>\n }\n }\n }\n </div>\n </ng-template>\n }\n }\n }\n </div>\n </ng-template>\n\n <!-- Regular menu template -->\n <ng-template #menuTemplate>\n <div class=\"tn-menu\" cdkMenu>\n @for (item of items(); track trackByItemId($index, item)) {\n @if (item.separator) {\n <div\n class=\"tn-menu-separator\"\n role=\"separator\"\n ></div>\n } @else {\n @if (!item.children || item.children.length === 0) {\n <button\n cdkMenuItem\n class=\"tn-menu-item\"\n type=\"button\"\n [disabled]=\"item.disabled\"\n [class.disabled]=\"item.disabled\"\n (click)=\"onMenuItemClick(item)\"\n >\n @if (item.icon) {\n <tn-icon size=\"sm\" class=\"tn-menu-item-icon\" [name]=\"item.icon\" [library]=\"item.iconLibrary\" />\n }\n <span class=\"tn-menu-item-label\">{{ item.label }}</span>\n @if (item.shortcut) {\n <span class=\"tn-menu-item-shortcut\">{{ item.shortcut }}</span>\n }\n </button>\n } @else {\n <button\n cdkMenuItem\n class=\"tn-menu-item tn-menu-item--nested\"\n type=\"button\"\n [cdkMenuTriggerFor]=\"nestedMenu\"\n [disabled]=\"item.disabled\"\n [class.disabled]=\"item.disabled\"\n >\n @if (item.icon) {\n <tn-icon size=\"sm\" class=\"tn-menu-item-icon\" [name]=\"item.icon\" [library]=\"item.iconLibrary\" />\n }\n <span class=\"tn-menu-item-label\">{{ item.label }}</span>\n @if (item.shortcut) {\n <span class=\"tn-menu-item-shortcut\">{{ item.shortcut }}</span>\n }\n <span class=\"tn-menu-item-arrow\">\u25B6</span>\n </button>\n\n <ng-template #nestedMenu>\n <div class=\"tn-menu tn-menu--nested\" cdkMenu>\n @for (nestedItem of item.children; track trackByItemId($index, nestedItem)) {\n @if (nestedItem.separator) {\n <div\n class=\"tn-menu-separator\"\n role=\"separator\"\n ></div>\n } @else {\n @if (!nestedItem.children || nestedItem.children.length === 0) {\n <button\n cdkMenuItem\n class=\"tn-menu-item\"\n type=\"button\"\n [disabled]=\"nestedItem.disabled\"\n [class.disabled]=\"nestedItem.disabled\"\n (click)=\"onMenuItemClick(nestedItem)\"\n >\n @if (nestedItem.icon) {\n <tn-icon size=\"sm\" class=\"tn-menu-item-icon\" [name]=\"nestedItem.icon\" [library]=\"nestedItem.iconLibrary\" />\n }\n <span class=\"tn-menu-item-label\">{{ nestedItem.label }}</span>\n @if (nestedItem.shortcut) {\n <span class=\"tn-menu-item-shortcut\">{{ nestedItem.shortcut }}</span>\n }\n </button>\n } @else {\n <button\n cdkMenuItem\n class=\"tn-menu-item tn-menu-item--nested\"\n type=\"button\"\n [cdkMenuTriggerFor]=\"deepNestedMenu\"\n [disabled]=\"nestedItem.disabled\"\n [class.disabled]=\"nestedItem.disabled\"\n >\n @if (nestedItem.icon) {\n <tn-icon size=\"sm\" class=\"tn-menu-item-icon\" [name]=\"nestedItem.icon\" [library]=\"nestedItem.iconLibrary\" />\n }\n <span class=\"tn-menu-item-label\">{{ nestedItem.label }}</span>\n @if (nestedItem.shortcut) {\n <span class=\"tn-menu-item-shortcut\">{{ nestedItem.shortcut }}</span>\n }\n <span class=\"tn-menu-item-arrow\">\u25B6</span>\n </button>\n\n <ng-template #deepNestedMenu>\n <div class=\"tn-menu tn-menu--nested\" cdkMenu>\n @for (deepNestedItem of nestedItem.children; track trackByItemId($index, deepNestedItem)) {\n @if (deepNestedItem.separator) {\n <div\n class=\"tn-menu-separator\"\n role=\"separator\"\n ></div>\n } @else {\n <button\n cdkMenuItem\n class=\"tn-menu-item\"\n type=\"button\"\n [disabled]=\"deepNestedItem.disabled\"\n [class.disabled]=\"deepNestedItem.disabled\"\n (click)=\"onMenuItemClick(deepNestedItem)\"\n >\n @if (deepNestedItem.icon) {\n <tn-icon size=\"sm\" class=\"tn-menu-item-icon\" [name]=\"deepNestedItem.icon\" [library]=\"deepNestedItem.iconLibrary\" />\n }\n <span class=\"tn-menu-item-label\">{{ deepNestedItem.label }}</span>\n @if (deepNestedItem.shortcut) {\n <span class=\"tn-menu-item-shortcut\">{{ deepNestedItem.shortcut }}</span>\n }\n </button>\n }\n }\n </div>\n </ng-template>\n }\n }\n }\n </div>\n </ng-template>\n }\n }\n }\n </div>\n </ng-template>", styles: [".tn-menu-container{display:inline-block;position:relative}.tn-menu-container.tn-menu-container--context{display:block;width:100%;height:100%;cursor:context-menu}.tn-menu-trigger{display:flex;align-items:center;gap:8px;padding:8px 16px;background:var(--tn-bg1, #ffffff);border:1px solid var(--tn-lines, #e0e0e0);border-radius:4px;color:var(--tn-fg1, #333333);cursor:pointer;font-size:14px;transition:all .2s ease}.tn-menu-trigger:hover:not(.disabled){background:var(--tn-bg2, #f5f5f5);border-color:var(--tn-lines, #cccccc)}.tn-menu-trigger:focus{outline:2px solid var(--tn-primary, #007bff);outline-offset:2px}.tn-menu-trigger.disabled{opacity:.5;cursor:not-allowed}.tn-menu-arrow{font-size:12px;transition:transform .2s ease}.tn-menu-arrow.tn-menu-arrow--up{transform:rotate(180deg)}.tn-menu{display:flex;flex-direction:column;background:var(--tn-bg2, #f5f5f5);border:1px solid var(--tn-lines, #e0e0e0);border-radius:4px;box-shadow:0 8px 24px #00000026,0 4px 8px #0000001a;min-width:160px;max-width:300px;padding:4px 0;z-index:1000}.tn-menu-item{display:flex;align-items:center;gap:8px;width:100%;padding:8px 16px;border:none;background:transparent;color:var(--tn-fg1, #333333);cursor:pointer;font-size:14px;text-align:left;transition:background-color .2s ease}.tn-menu-item:hover:not(.disabled){background:var(--tn-alt-bg2, #e8f4fd)!important}.tn-menu-item:focus{outline:none;background:var(--tn-alt-bg2, #e8f4fd)}.tn-menu-item[aria-selected=true]{background:var(--tn-alt-bg2, #e8f4fd)}.tn-menu-item.disabled{opacity:.5;cursor:not-allowed}.tn-menu-item.disabled:hover{background:transparent!important}.tn-menu-item-icon{font-size:16px;width:16px;text-align:center}.tn-menu-item-label{flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.tn-menu-item-shortcut{font-size:12px;color:var(--tn-fg2, #666666);margin-left:auto;padding-left:16px;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica,Arial,sans-serif;font-weight:400;opacity:.7}.tn-menu-item-arrow{font-size:10px;margin-left:8px;color:var(--tn-fg2, #666666);transition:transform .2s ease;flex-shrink:0}.tn-menu-item--nested{position:relative}.tn-menu-item--nested:hover .tn-menu-item-arrow{color:var(--tn-fg1, #333333)}.tn-menu-separator{height:1px;background:var(--tn-lines, #e0e0e0);margin:4px 0}.tn-menu--nested{margin-left:4px;box-shadow:0 8px 24px #00000026,0 4px 8px #0000001a;border-radius:4px}.tn-menu-context-content{width:100%;height:100%}.tn-menu-context-content:hover:before{content:\"\";position:absolute;inset:0;background:rgba(var(--tn-primary-rgb, 0, 123, 255),.05);pointer-events:none;border:1px dashed rgba(var(--tn-primary-rgb, 0, 123, 255),.3);border-radius:4px}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: CdkMenu, selector: "[cdkMenu]", outputs: ["closed"], exportAs: ["cdkMenu"] }, { kind: "directive", type: CdkMenuItem, selector: "[cdkMenuItem]", inputs: ["cdkMenuItemDisabled", "cdkMenuitemTypeaheadLabel"], outputs: ["cdkMenuItemTriggered"], exportAs: ["cdkMenuItem"] }, { kind: "directive", type: CdkMenuTrigger, selector: "[cdkMenuTriggerFor]", inputs: ["cdkMenuTriggerFor", "cdkMenuPosition", "cdkMenuTriggerData", "cdkMenuTriggerTransformOriginOn"], outputs: ["cdkMenuOpened", "cdkMenuClosed"], exportAs: ["cdkMenuTriggerFor"] }, { kind: "component", type: TnIconComponent, selector: "tn-icon", inputs: ["name", "size", "color", "tooltip", "ariaLabel", "library", "fullSize", "customSize"] }] });
1895
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.0", type: TnMenuComponent, isStandalone: true, selector: "tn-menu", inputs: { items: { classPropertyName: "items", publicName: "items", isSignal: true, isRequired: false, transformFunction: null }, contextMenu: { classPropertyName: "contextMenu", publicName: "contextMenu", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { menuItemClick: "menuItemClick", menuOpen: "menuOpen", menuClose: "menuClose" }, viewQueries: [{ propertyName: "menuTemplate", first: true, predicate: ["menuTemplate"], descendants: true, isSignal: true }, { propertyName: "contextMenuTemplate", first: true, predicate: ["contextMenuTemplate"], descendants: true, isSignal: true }], ngImport: i0, template: "<!-- Context menu content slot -->\n@if (contextMenu()) {\n <div class=\"tn-menu-context-content\" (contextmenu)=\"onContextMenu($event)\">\n <ng-content />\n </div>\n}\n\n <!-- Context menu template for overlay -->\n <ng-template #contextMenuTemplate>\n <div class=\"tn-menu\" cdkMenu tnMenuActivateHover>\n @for (item of items(); track trackByItemId($index, item)) {\n @if (item.separator) {\n <div\n class=\"tn-menu-separator\"\n role=\"separator\"\n ></div>\n } @else {\n @if (!item.children || item.children.length === 0) {\n <button\n cdkMenuItem\n class=\"tn-menu-item\"\n type=\"button\"\n [disabled]=\"item.disabled\"\n [class.disabled]=\"item.disabled\"\n (click)=\"onMenuItemClick(item)\"\n >\n @if (item.icon) {\n <tn-icon size=\"sm\" class=\"tn-menu-item-icon\" [name]=\"item.icon\" [library]=\"item.iconLibrary\" />\n }\n <span class=\"tn-menu-item-label\">{{ item.label }}</span>\n @if (item.shortcut) {\n <span class=\"tn-menu-item-shortcut\">{{ item.shortcut }}</span>\n }\n </button>\n } @else {\n <button\n cdkMenuItem\n class=\"tn-menu-item tn-menu-item--nested\"\n type=\"button\"\n [cdkMenuTriggerFor]=\"nestedMenu\"\n [disabled]=\"item.disabled\"\n [class.disabled]=\"item.disabled\"\n >\n @if (item.icon) {\n <tn-icon size=\"sm\" class=\"tn-menu-item-icon\" [name]=\"item.icon\" [library]=\"item.iconLibrary\" />\n }\n <span class=\"tn-menu-item-label\">{{ item.label }}</span>\n @if (item.shortcut) {\n <span class=\"tn-menu-item-shortcut\">{{ item.shortcut }}</span>\n }\n <span class=\"tn-menu-item-arrow\">\u25B6</span>\n </button>\n\n <ng-template #nestedMenu>\n <div class=\"tn-menu tn-menu--nested\" cdkMenu>\n @for (nestedItem of item.children; track trackByItemId($index, nestedItem)) {\n @if (nestedItem.separator) {\n <div\n class=\"tn-menu-separator\"\n role=\"separator\"\n ></div>\n } @else {\n @if (!nestedItem.children || nestedItem.children.length === 0) {\n <button\n cdkMenuItem\n class=\"tn-menu-item\"\n type=\"button\"\n [disabled]=\"nestedItem.disabled\"\n [class.disabled]=\"nestedItem.disabled\"\n (click)=\"onMenuItemClick(nestedItem)\"\n >\n @if (nestedItem.icon) {\n <tn-icon size=\"sm\" class=\"tn-menu-item-icon\" [name]=\"nestedItem.icon\" [library]=\"nestedItem.iconLibrary\" />\n }\n <span class=\"tn-menu-item-label\">{{ nestedItem.label }}</span>\n @if (nestedItem.shortcut) {\n <span class=\"tn-menu-item-shortcut\">{{ nestedItem.shortcut }}</span>\n }\n </button>\n } @else {\n <button\n cdkMenuItem\n class=\"tn-menu-item tn-menu-item--nested\"\n type=\"button\"\n [cdkMenuTriggerFor]=\"deepNestedMenu\"\n [disabled]=\"nestedItem.disabled\"\n [class.disabled]=\"nestedItem.disabled\"\n >\n @if (nestedItem.icon) {\n <tn-icon size=\"sm\" class=\"tn-menu-item-icon\" [name]=\"nestedItem.icon\" [library]=\"nestedItem.iconLibrary\" />\n }\n <span class=\"tn-menu-item-label\">{{ nestedItem.label }}</span>\n @if (nestedItem.shortcut) {\n <span class=\"tn-menu-item-shortcut\">{{ nestedItem.shortcut }}</span>\n }\n <span class=\"tn-menu-item-arrow\">\u25B6</span>\n </button>\n\n <ng-template #deepNestedMenu>\n <div class=\"tn-menu tn-menu--nested\" cdkMenu>\n @for (deepNestedItem of nestedItem.children; track trackByItemId($index, deepNestedItem)) {\n @if (deepNestedItem.separator) {\n <div\n class=\"tn-menu-separator\"\n role=\"separator\"\n ></div>\n } @else {\n <button\n cdkMenuItem\n class=\"tn-menu-item\"\n type=\"button\"\n [disabled]=\"deepNestedItem.disabled\"\n [class.disabled]=\"deepNestedItem.disabled\"\n (click)=\"onMenuItemClick(deepNestedItem)\"\n >\n @if (deepNestedItem.icon) {\n <tn-icon size=\"sm\" class=\"tn-menu-item-icon\" [name]=\"deepNestedItem.icon\" [library]=\"deepNestedItem.iconLibrary\" />\n }\n <span class=\"tn-menu-item-label\">{{ deepNestedItem.label }}</span>\n @if (deepNestedItem.shortcut) {\n <span class=\"tn-menu-item-shortcut\">{{ deepNestedItem.shortcut }}</span>\n }\n </button>\n }\n }\n </div>\n </ng-template>\n }\n }\n }\n </div>\n </ng-template>\n }\n }\n }\n </div>\n </ng-template>\n\n <!-- Regular menu template -->\n <ng-template #menuTemplate>\n <div class=\"tn-menu\" cdkMenu tnMenuActivateHover>\n @for (item of items(); track trackByItemId($index, item)) {\n @if (item.separator) {\n <div\n class=\"tn-menu-separator\"\n role=\"separator\"\n ></div>\n } @else {\n @if (!item.children || item.children.length === 0) {\n <button\n cdkMenuItem\n class=\"tn-menu-item\"\n type=\"button\"\n [disabled]=\"item.disabled\"\n [class.disabled]=\"item.disabled\"\n (click)=\"onMenuItemClick(item)\"\n >\n @if (item.icon) {\n <tn-icon size=\"sm\" class=\"tn-menu-item-icon\" [name]=\"item.icon\" [library]=\"item.iconLibrary\" />\n }\n <span class=\"tn-menu-item-label\">{{ item.label }}</span>\n @if (item.shortcut) {\n <span class=\"tn-menu-item-shortcut\">{{ item.shortcut }}</span>\n }\n </button>\n } @else {\n <button\n cdkMenuItem\n class=\"tn-menu-item tn-menu-item--nested\"\n type=\"button\"\n [cdkMenuTriggerFor]=\"nestedMenu\"\n [disabled]=\"item.disabled\"\n [class.disabled]=\"item.disabled\"\n >\n @if (item.icon) {\n <tn-icon size=\"sm\" class=\"tn-menu-item-icon\" [name]=\"item.icon\" [library]=\"item.iconLibrary\" />\n }\n <span class=\"tn-menu-item-label\">{{ item.label }}</span>\n @if (item.shortcut) {\n <span class=\"tn-menu-item-shortcut\">{{ item.shortcut }}</span>\n }\n <span class=\"tn-menu-item-arrow\">\u25B6</span>\n </button>\n\n <ng-template #nestedMenu>\n <div class=\"tn-menu tn-menu--nested\" cdkMenu>\n @for (nestedItem of item.children; track trackByItemId($index, nestedItem)) {\n @if (nestedItem.separator) {\n <div\n class=\"tn-menu-separator\"\n role=\"separator\"\n ></div>\n } @else {\n @if (!nestedItem.children || nestedItem.children.length === 0) {\n <button\n cdkMenuItem\n class=\"tn-menu-item\"\n type=\"button\"\n [disabled]=\"nestedItem.disabled\"\n [class.disabled]=\"nestedItem.disabled\"\n (click)=\"onMenuItemClick(nestedItem)\"\n >\n @if (nestedItem.icon) {\n <tn-icon size=\"sm\" class=\"tn-menu-item-icon\" [name]=\"nestedItem.icon\" [library]=\"nestedItem.iconLibrary\" />\n }\n <span class=\"tn-menu-item-label\">{{ nestedItem.label }}</span>\n @if (nestedItem.shortcut) {\n <span class=\"tn-menu-item-shortcut\">{{ nestedItem.shortcut }}</span>\n }\n </button>\n } @else {\n <button\n cdkMenuItem\n class=\"tn-menu-item tn-menu-item--nested\"\n type=\"button\"\n [cdkMenuTriggerFor]=\"deepNestedMenu\"\n [disabled]=\"nestedItem.disabled\"\n [class.disabled]=\"nestedItem.disabled\"\n >\n @if (nestedItem.icon) {\n <tn-icon size=\"sm\" class=\"tn-menu-item-icon\" [name]=\"nestedItem.icon\" [library]=\"nestedItem.iconLibrary\" />\n }\n <span class=\"tn-menu-item-label\">{{ nestedItem.label }}</span>\n @if (nestedItem.shortcut) {\n <span class=\"tn-menu-item-shortcut\">{{ nestedItem.shortcut }}</span>\n }\n <span class=\"tn-menu-item-arrow\">\u25B6</span>\n </button>\n\n <ng-template #deepNestedMenu>\n <div class=\"tn-menu tn-menu--nested\" cdkMenu>\n @for (deepNestedItem of nestedItem.children; track trackByItemId($index, deepNestedItem)) {\n @if (deepNestedItem.separator) {\n <div\n class=\"tn-menu-separator\"\n role=\"separator\"\n ></div>\n } @else {\n <button\n cdkMenuItem\n class=\"tn-menu-item\"\n type=\"button\"\n [disabled]=\"deepNestedItem.disabled\"\n [class.disabled]=\"deepNestedItem.disabled\"\n (click)=\"onMenuItemClick(deepNestedItem)\"\n >\n @if (deepNestedItem.icon) {\n <tn-icon size=\"sm\" class=\"tn-menu-item-icon\" [name]=\"deepNestedItem.icon\" [library]=\"deepNestedItem.iconLibrary\" />\n }\n <span class=\"tn-menu-item-label\">{{ deepNestedItem.label }}</span>\n @if (deepNestedItem.shortcut) {\n <span class=\"tn-menu-item-shortcut\">{{ deepNestedItem.shortcut }}</span>\n }\n </button>\n }\n }\n </div>\n </ng-template>\n }\n }\n }\n </div>\n </ng-template>\n }\n }\n }\n </div>\n </ng-template>", styles: [".tn-menu-container{display:inline-block;position:relative}.tn-menu-container.tn-menu-container--context{display:block;width:100%;height:100%;cursor:context-menu}.tn-menu-trigger{display:flex;align-items:center;gap:8px;padding:8px 16px;background:var(--tn-bg1, #ffffff);border:1px solid var(--tn-lines, #e0e0e0);border-radius:4px;color:var(--tn-fg1, #333333);cursor:pointer;font-size:14px;transition:all .2s ease}.tn-menu-trigger:hover:not(.disabled){background:var(--tn-bg2, #f5f5f5);border-color:var(--tn-lines, #cccccc)}.tn-menu-trigger:focus{outline:2px solid var(--tn-primary, #007bff);outline-offset:2px}.tn-menu-trigger.disabled{opacity:.5;cursor:not-allowed}.tn-menu-arrow{font-size:12px;transition:transform .2s ease}.tn-menu-arrow.tn-menu-arrow--up{transform:rotate(180deg)}.tn-menu{display:flex;flex-direction:column;background:var(--tn-bg2, #f5f5f5);border:1px solid var(--tn-lines, #e0e0e0);border-radius:4px;box-shadow:0 8px 24px #00000026,0 4px 8px #0000001a;min-width:160px;max-width:300px;padding:4px 0;z-index:1000}.tn-menu-item{display:flex;align-items:center;gap:8px;width:100%;padding:8px 16px;border:none;background:transparent;color:var(--tn-fg1, #333333);cursor:pointer;font-size:14px;text-align:left;transition:background-color .2s ease}.tn-menu-item:hover:not(.disabled){background:var(--tn-alt-bg2, #e8f4fd)!important}.tn-menu-item:focus{outline:none;background:var(--tn-alt-bg2, #e8f4fd)}.tn-menu-item[aria-selected=true]{background:var(--tn-alt-bg2, #e8f4fd)}.tn-menu-item.disabled{opacity:.5;cursor:not-allowed}.tn-menu-item.disabled:hover{background:transparent!important}.tn-menu-item-icon{font-size:16px;width:16px;text-align:center}.tn-menu-item-label{flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.tn-menu-item-shortcut{font-size:12px;color:var(--tn-fg2, #666666);margin-left:auto;padding-left:16px;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica,Arial,sans-serif;font-weight:400;opacity:.7}.tn-menu-item-arrow{font-size:10px;margin-left:8px;color:var(--tn-fg2, #666666);transition:transform .2s ease;flex-shrink:0}.tn-menu-item--nested{position:relative}.tn-menu-item--nested:hover .tn-menu-item-arrow{color:var(--tn-fg1, #333333)}.tn-menu-separator{height:1px;background:var(--tn-lines, #e0e0e0);margin:4px 0}.tn-menu--nested{margin-left:4px;box-shadow:0 8px 24px #00000026,0 4px 8px #0000001a;border-radius:4px}.tn-menu-context-content{width:100%;height:100%}.tn-menu-context-content:hover:before{content:\"\";position:absolute;inset:0;background:rgba(var(--tn-primary-rgb, 0, 123, 255),.05);pointer-events:none;border:1px dashed rgba(var(--tn-primary-rgb, 0, 123, 255),.3);border-radius:4px}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: CdkMenu, selector: "[cdkMenu]", outputs: ["closed"], exportAs: ["cdkMenu"] }, { kind: "directive", type: CdkMenuItem, selector: "[cdkMenuItem]", inputs: ["cdkMenuItemDisabled", "cdkMenuitemTypeaheadLabel"], outputs: ["cdkMenuItemTriggered"], exportAs: ["cdkMenuItem"] }, { kind: "directive", type: CdkMenuTrigger, selector: "[cdkMenuTriggerFor]", inputs: ["cdkMenuTriggerFor", "cdkMenuPosition", "cdkMenuTriggerData", "cdkMenuTriggerTransformOriginOn"], outputs: ["cdkMenuOpened", "cdkMenuClosed"], exportAs: ["cdkMenuTriggerFor"] }, { kind: "component", type: TnIconComponent, selector: "tn-icon", inputs: ["name", "size", "color", "tooltip", "ariaLabel", "library", "fullSize", "customSize"] }, { kind: "directive", type: TnMenuActivateHoverDirective, selector: "[tnMenuActivateHover]" }] });
1856
1896
  }
1857
1897
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnMenuComponent, decorators: [{
1858
1898
  type: Component,
1859
- args: [{ selector: 'tn-menu', standalone: true, imports: [CommonModule, CdkMenu, CdkMenuItem, CdkMenuTrigger, TnIconComponent], template: "<!-- Context menu content slot -->\n@if (contextMenu()) {\n <div class=\"tn-menu-context-content\" (contextmenu)=\"onContextMenu($event)\">\n <ng-content />\n </div>\n}\n\n <!-- Context menu template for overlay -->\n <ng-template #contextMenuTemplate>\n <div class=\"tn-menu\" cdkMenu>\n @for (item of items(); track trackByItemId($index, item)) {\n @if (item.separator) {\n <div\n class=\"tn-menu-separator\"\n role=\"separator\"\n ></div>\n } @else {\n @if (!item.children || item.children.length === 0) {\n <button\n cdkMenuItem\n class=\"tn-menu-item\"\n type=\"button\"\n [disabled]=\"item.disabled\"\n [class.disabled]=\"item.disabled\"\n (click)=\"onMenuItemClick(item)\"\n >\n @if (item.icon) {\n <tn-icon size=\"sm\" class=\"tn-menu-item-icon\" [name]=\"item.icon\" [library]=\"item.iconLibrary\" />\n }\n <span class=\"tn-menu-item-label\">{{ item.label }}</span>\n @if (item.shortcut) {\n <span class=\"tn-menu-item-shortcut\">{{ item.shortcut }}</span>\n }\n </button>\n } @else {\n <button\n cdkMenuItem\n class=\"tn-menu-item tn-menu-item--nested\"\n type=\"button\"\n [cdkMenuTriggerFor]=\"nestedMenu\"\n [disabled]=\"item.disabled\"\n [class.disabled]=\"item.disabled\"\n >\n @if (item.icon) {\n <tn-icon size=\"sm\" class=\"tn-menu-item-icon\" [name]=\"item.icon\" [library]=\"item.iconLibrary\" />\n }\n <span class=\"tn-menu-item-label\">{{ item.label }}</span>\n @if (item.shortcut) {\n <span class=\"tn-menu-item-shortcut\">{{ item.shortcut }}</span>\n }\n <span class=\"tn-menu-item-arrow\">\u25B6</span>\n </button>\n\n <ng-template #nestedMenu>\n <div class=\"tn-menu tn-menu--nested\" cdkMenu>\n @for (nestedItem of item.children; track trackByItemId($index, nestedItem)) {\n @if (nestedItem.separator) {\n <div\n class=\"tn-menu-separator\"\n role=\"separator\"\n ></div>\n } @else {\n @if (!nestedItem.children || nestedItem.children.length === 0) {\n <button\n cdkMenuItem\n class=\"tn-menu-item\"\n type=\"button\"\n [disabled]=\"nestedItem.disabled\"\n [class.disabled]=\"nestedItem.disabled\"\n (click)=\"onMenuItemClick(nestedItem)\"\n >\n @if (nestedItem.icon) {\n <tn-icon size=\"sm\" class=\"tn-menu-item-icon\" [name]=\"nestedItem.icon\" [library]=\"nestedItem.iconLibrary\" />\n }\n <span class=\"tn-menu-item-label\">{{ nestedItem.label }}</span>\n @if (nestedItem.shortcut) {\n <span class=\"tn-menu-item-shortcut\">{{ nestedItem.shortcut }}</span>\n }\n </button>\n } @else {\n <button\n cdkMenuItem\n class=\"tn-menu-item tn-menu-item--nested\"\n type=\"button\"\n [cdkMenuTriggerFor]=\"deepNestedMenu\"\n [disabled]=\"nestedItem.disabled\"\n [class.disabled]=\"nestedItem.disabled\"\n >\n @if (nestedItem.icon) {\n <tn-icon size=\"sm\" class=\"tn-menu-item-icon\" [name]=\"nestedItem.icon\" [library]=\"nestedItem.iconLibrary\" />\n }\n <span class=\"tn-menu-item-label\">{{ nestedItem.label }}</span>\n @if (nestedItem.shortcut) {\n <span class=\"tn-menu-item-shortcut\">{{ nestedItem.shortcut }}</span>\n }\n <span class=\"tn-menu-item-arrow\">\u25B6</span>\n </button>\n\n <ng-template #deepNestedMenu>\n <div class=\"tn-menu tn-menu--nested\" cdkMenu>\n @for (deepNestedItem of nestedItem.children; track trackByItemId($index, deepNestedItem)) {\n @if (deepNestedItem.separator) {\n <div\n class=\"tn-menu-separator\"\n role=\"separator\"\n ></div>\n } @else {\n <button\n cdkMenuItem\n class=\"tn-menu-item\"\n type=\"button\"\n [disabled]=\"deepNestedItem.disabled\"\n [class.disabled]=\"deepNestedItem.disabled\"\n (click)=\"onMenuItemClick(deepNestedItem)\"\n >\n @if (deepNestedItem.icon) {\n <tn-icon size=\"sm\" class=\"tn-menu-item-icon\" [name]=\"deepNestedItem.icon\" [library]=\"deepNestedItem.iconLibrary\" />\n }\n <span class=\"tn-menu-item-label\">{{ deepNestedItem.label }}</span>\n @if (deepNestedItem.shortcut) {\n <span class=\"tn-menu-item-shortcut\">{{ deepNestedItem.shortcut }}</span>\n }\n </button>\n }\n }\n </div>\n </ng-template>\n }\n }\n }\n </div>\n </ng-template>\n }\n }\n }\n </div>\n </ng-template>\n\n <!-- Regular menu template -->\n <ng-template #menuTemplate>\n <div class=\"tn-menu\" cdkMenu>\n @for (item of items(); track trackByItemId($index, item)) {\n @if (item.separator) {\n <div\n class=\"tn-menu-separator\"\n role=\"separator\"\n ></div>\n } @else {\n @if (!item.children || item.children.length === 0) {\n <button\n cdkMenuItem\n class=\"tn-menu-item\"\n type=\"button\"\n [disabled]=\"item.disabled\"\n [class.disabled]=\"item.disabled\"\n (click)=\"onMenuItemClick(item)\"\n >\n @if (item.icon) {\n <tn-icon size=\"sm\" class=\"tn-menu-item-icon\" [name]=\"item.icon\" [library]=\"item.iconLibrary\" />\n }\n <span class=\"tn-menu-item-label\">{{ item.label }}</span>\n @if (item.shortcut) {\n <span class=\"tn-menu-item-shortcut\">{{ item.shortcut }}</span>\n }\n </button>\n } @else {\n <button\n cdkMenuItem\n class=\"tn-menu-item tn-menu-item--nested\"\n type=\"button\"\n [cdkMenuTriggerFor]=\"nestedMenu\"\n [disabled]=\"item.disabled\"\n [class.disabled]=\"item.disabled\"\n >\n @if (item.icon) {\n <tn-icon size=\"sm\" class=\"tn-menu-item-icon\" [name]=\"item.icon\" [library]=\"item.iconLibrary\" />\n }\n <span class=\"tn-menu-item-label\">{{ item.label }}</span>\n @if (item.shortcut) {\n <span class=\"tn-menu-item-shortcut\">{{ item.shortcut }}</span>\n }\n <span class=\"tn-menu-item-arrow\">\u25B6</span>\n </button>\n\n <ng-template #nestedMenu>\n <div class=\"tn-menu tn-menu--nested\" cdkMenu>\n @for (nestedItem of item.children; track trackByItemId($index, nestedItem)) {\n @if (nestedItem.separator) {\n <div\n class=\"tn-menu-separator\"\n role=\"separator\"\n ></div>\n } @else {\n @if (!nestedItem.children || nestedItem.children.length === 0) {\n <button\n cdkMenuItem\n class=\"tn-menu-item\"\n type=\"button\"\n [disabled]=\"nestedItem.disabled\"\n [class.disabled]=\"nestedItem.disabled\"\n (click)=\"onMenuItemClick(nestedItem)\"\n >\n @if (nestedItem.icon) {\n <tn-icon size=\"sm\" class=\"tn-menu-item-icon\" [name]=\"nestedItem.icon\" [library]=\"nestedItem.iconLibrary\" />\n }\n <span class=\"tn-menu-item-label\">{{ nestedItem.label }}</span>\n @if (nestedItem.shortcut) {\n <span class=\"tn-menu-item-shortcut\">{{ nestedItem.shortcut }}</span>\n }\n </button>\n } @else {\n <button\n cdkMenuItem\n class=\"tn-menu-item tn-menu-item--nested\"\n type=\"button\"\n [cdkMenuTriggerFor]=\"deepNestedMenu\"\n [disabled]=\"nestedItem.disabled\"\n [class.disabled]=\"nestedItem.disabled\"\n >\n @if (nestedItem.icon) {\n <tn-icon size=\"sm\" class=\"tn-menu-item-icon\" [name]=\"nestedItem.icon\" [library]=\"nestedItem.iconLibrary\" />\n }\n <span class=\"tn-menu-item-label\">{{ nestedItem.label }}</span>\n @if (nestedItem.shortcut) {\n <span class=\"tn-menu-item-shortcut\">{{ nestedItem.shortcut }}</span>\n }\n <span class=\"tn-menu-item-arrow\">\u25B6</span>\n </button>\n\n <ng-template #deepNestedMenu>\n <div class=\"tn-menu tn-menu--nested\" cdkMenu>\n @for (deepNestedItem of nestedItem.children; track trackByItemId($index, deepNestedItem)) {\n @if (deepNestedItem.separator) {\n <div\n class=\"tn-menu-separator\"\n role=\"separator\"\n ></div>\n } @else {\n <button\n cdkMenuItem\n class=\"tn-menu-item\"\n type=\"button\"\n [disabled]=\"deepNestedItem.disabled\"\n [class.disabled]=\"deepNestedItem.disabled\"\n (click)=\"onMenuItemClick(deepNestedItem)\"\n >\n @if (deepNestedItem.icon) {\n <tn-icon size=\"sm\" class=\"tn-menu-item-icon\" [name]=\"deepNestedItem.icon\" [library]=\"deepNestedItem.iconLibrary\" />\n }\n <span class=\"tn-menu-item-label\">{{ deepNestedItem.label }}</span>\n @if (deepNestedItem.shortcut) {\n <span class=\"tn-menu-item-shortcut\">{{ deepNestedItem.shortcut }}</span>\n }\n </button>\n }\n }\n </div>\n </ng-template>\n }\n }\n }\n </div>\n </ng-template>\n }\n }\n }\n </div>\n </ng-template>", styles: [".tn-menu-container{display:inline-block;position:relative}.tn-menu-container.tn-menu-container--context{display:block;width:100%;height:100%;cursor:context-menu}.tn-menu-trigger{display:flex;align-items:center;gap:8px;padding:8px 16px;background:var(--tn-bg1, #ffffff);border:1px solid var(--tn-lines, #e0e0e0);border-radius:4px;color:var(--tn-fg1, #333333);cursor:pointer;font-size:14px;transition:all .2s ease}.tn-menu-trigger:hover:not(.disabled){background:var(--tn-bg2, #f5f5f5);border-color:var(--tn-lines, #cccccc)}.tn-menu-trigger:focus{outline:2px solid var(--tn-primary, #007bff);outline-offset:2px}.tn-menu-trigger.disabled{opacity:.5;cursor:not-allowed}.tn-menu-arrow{font-size:12px;transition:transform .2s ease}.tn-menu-arrow.tn-menu-arrow--up{transform:rotate(180deg)}.tn-menu{display:flex;flex-direction:column;background:var(--tn-bg2, #f5f5f5);border:1px solid var(--tn-lines, #e0e0e0);border-radius:4px;box-shadow:0 8px 24px #00000026,0 4px 8px #0000001a;min-width:160px;max-width:300px;padding:4px 0;z-index:1000}.tn-menu-item{display:flex;align-items:center;gap:8px;width:100%;padding:8px 16px;border:none;background:transparent;color:var(--tn-fg1, #333333);cursor:pointer;font-size:14px;text-align:left;transition:background-color .2s ease}.tn-menu-item:hover:not(.disabled){background:var(--tn-alt-bg2, #e8f4fd)!important}.tn-menu-item:focus{outline:none;background:var(--tn-alt-bg2, #e8f4fd)}.tn-menu-item[aria-selected=true]{background:var(--tn-alt-bg2, #e8f4fd)}.tn-menu-item.disabled{opacity:.5;cursor:not-allowed}.tn-menu-item.disabled:hover{background:transparent!important}.tn-menu-item-icon{font-size:16px;width:16px;text-align:center}.tn-menu-item-label{flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.tn-menu-item-shortcut{font-size:12px;color:var(--tn-fg2, #666666);margin-left:auto;padding-left:16px;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica,Arial,sans-serif;font-weight:400;opacity:.7}.tn-menu-item-arrow{font-size:10px;margin-left:8px;color:var(--tn-fg2, #666666);transition:transform .2s ease;flex-shrink:0}.tn-menu-item--nested{position:relative}.tn-menu-item--nested:hover .tn-menu-item-arrow{color:var(--tn-fg1, #333333)}.tn-menu-separator{height:1px;background:var(--tn-lines, #e0e0e0);margin:4px 0}.tn-menu--nested{margin-left:4px;box-shadow:0 8px 24px #00000026,0 4px 8px #0000001a;border-radius:4px}.tn-menu-context-content{width:100%;height:100%}.tn-menu-context-content:hover:before{content:\"\";position:absolute;inset:0;background:rgba(var(--tn-primary-rgb, 0, 123, 255),.05);pointer-events:none;border:1px dashed rgba(var(--tn-primary-rgb, 0, 123, 255),.3);border-radius:4px}\n"] }]
1899
+ args: [{ selector: 'tn-menu', standalone: true, imports: [CommonModule, CdkMenu, CdkMenuItem, CdkMenuTrigger, TnIconComponent, TnMenuActivateHoverDirective], template: "<!-- Context menu content slot -->\n@if (contextMenu()) {\n <div class=\"tn-menu-context-content\" (contextmenu)=\"onContextMenu($event)\">\n <ng-content />\n </div>\n}\n\n <!-- Context menu template for overlay -->\n <ng-template #contextMenuTemplate>\n <div class=\"tn-menu\" cdkMenu tnMenuActivateHover>\n @for (item of items(); track trackByItemId($index, item)) {\n @if (item.separator) {\n <div\n class=\"tn-menu-separator\"\n role=\"separator\"\n ></div>\n } @else {\n @if (!item.children || item.children.length === 0) {\n <button\n cdkMenuItem\n class=\"tn-menu-item\"\n type=\"button\"\n [disabled]=\"item.disabled\"\n [class.disabled]=\"item.disabled\"\n (click)=\"onMenuItemClick(item)\"\n >\n @if (item.icon) {\n <tn-icon size=\"sm\" class=\"tn-menu-item-icon\" [name]=\"item.icon\" [library]=\"item.iconLibrary\" />\n }\n <span class=\"tn-menu-item-label\">{{ item.label }}</span>\n @if (item.shortcut) {\n <span class=\"tn-menu-item-shortcut\">{{ item.shortcut }}</span>\n }\n </button>\n } @else {\n <button\n cdkMenuItem\n class=\"tn-menu-item tn-menu-item--nested\"\n type=\"button\"\n [cdkMenuTriggerFor]=\"nestedMenu\"\n [disabled]=\"item.disabled\"\n [class.disabled]=\"item.disabled\"\n >\n @if (item.icon) {\n <tn-icon size=\"sm\" class=\"tn-menu-item-icon\" [name]=\"item.icon\" [library]=\"item.iconLibrary\" />\n }\n <span class=\"tn-menu-item-label\">{{ item.label }}</span>\n @if (item.shortcut) {\n <span class=\"tn-menu-item-shortcut\">{{ item.shortcut }}</span>\n }\n <span class=\"tn-menu-item-arrow\">\u25B6</span>\n </button>\n\n <ng-template #nestedMenu>\n <div class=\"tn-menu tn-menu--nested\" cdkMenu>\n @for (nestedItem of item.children; track trackByItemId($index, nestedItem)) {\n @if (nestedItem.separator) {\n <div\n class=\"tn-menu-separator\"\n role=\"separator\"\n ></div>\n } @else {\n @if (!nestedItem.children || nestedItem.children.length === 0) {\n <button\n cdkMenuItem\n class=\"tn-menu-item\"\n type=\"button\"\n [disabled]=\"nestedItem.disabled\"\n [class.disabled]=\"nestedItem.disabled\"\n (click)=\"onMenuItemClick(nestedItem)\"\n >\n @if (nestedItem.icon) {\n <tn-icon size=\"sm\" class=\"tn-menu-item-icon\" [name]=\"nestedItem.icon\" [library]=\"nestedItem.iconLibrary\" />\n }\n <span class=\"tn-menu-item-label\">{{ nestedItem.label }}</span>\n @if (nestedItem.shortcut) {\n <span class=\"tn-menu-item-shortcut\">{{ nestedItem.shortcut }}</span>\n }\n </button>\n } @else {\n <button\n cdkMenuItem\n class=\"tn-menu-item tn-menu-item--nested\"\n type=\"button\"\n [cdkMenuTriggerFor]=\"deepNestedMenu\"\n [disabled]=\"nestedItem.disabled\"\n [class.disabled]=\"nestedItem.disabled\"\n >\n @if (nestedItem.icon) {\n <tn-icon size=\"sm\" class=\"tn-menu-item-icon\" [name]=\"nestedItem.icon\" [library]=\"nestedItem.iconLibrary\" />\n }\n <span class=\"tn-menu-item-label\">{{ nestedItem.label }}</span>\n @if (nestedItem.shortcut) {\n <span class=\"tn-menu-item-shortcut\">{{ nestedItem.shortcut }}</span>\n }\n <span class=\"tn-menu-item-arrow\">\u25B6</span>\n </button>\n\n <ng-template #deepNestedMenu>\n <div class=\"tn-menu tn-menu--nested\" cdkMenu>\n @for (deepNestedItem of nestedItem.children; track trackByItemId($index, deepNestedItem)) {\n @if (deepNestedItem.separator) {\n <div\n class=\"tn-menu-separator\"\n role=\"separator\"\n ></div>\n } @else {\n <button\n cdkMenuItem\n class=\"tn-menu-item\"\n type=\"button\"\n [disabled]=\"deepNestedItem.disabled\"\n [class.disabled]=\"deepNestedItem.disabled\"\n (click)=\"onMenuItemClick(deepNestedItem)\"\n >\n @if (deepNestedItem.icon) {\n <tn-icon size=\"sm\" class=\"tn-menu-item-icon\" [name]=\"deepNestedItem.icon\" [library]=\"deepNestedItem.iconLibrary\" />\n }\n <span class=\"tn-menu-item-label\">{{ deepNestedItem.label }}</span>\n @if (deepNestedItem.shortcut) {\n <span class=\"tn-menu-item-shortcut\">{{ deepNestedItem.shortcut }}</span>\n }\n </button>\n }\n }\n </div>\n </ng-template>\n }\n }\n }\n </div>\n </ng-template>\n }\n }\n }\n </div>\n </ng-template>\n\n <!-- Regular menu template -->\n <ng-template #menuTemplate>\n <div class=\"tn-menu\" cdkMenu tnMenuActivateHover>\n @for (item of items(); track trackByItemId($index, item)) {\n @if (item.separator) {\n <div\n class=\"tn-menu-separator\"\n role=\"separator\"\n ></div>\n } @else {\n @if (!item.children || item.children.length === 0) {\n <button\n cdkMenuItem\n class=\"tn-menu-item\"\n type=\"button\"\n [disabled]=\"item.disabled\"\n [class.disabled]=\"item.disabled\"\n (click)=\"onMenuItemClick(item)\"\n >\n @if (item.icon) {\n <tn-icon size=\"sm\" class=\"tn-menu-item-icon\" [name]=\"item.icon\" [library]=\"item.iconLibrary\" />\n }\n <span class=\"tn-menu-item-label\">{{ item.label }}</span>\n @if (item.shortcut) {\n <span class=\"tn-menu-item-shortcut\">{{ item.shortcut }}</span>\n }\n </button>\n } @else {\n <button\n cdkMenuItem\n class=\"tn-menu-item tn-menu-item--nested\"\n type=\"button\"\n [cdkMenuTriggerFor]=\"nestedMenu\"\n [disabled]=\"item.disabled\"\n [class.disabled]=\"item.disabled\"\n >\n @if (item.icon) {\n <tn-icon size=\"sm\" class=\"tn-menu-item-icon\" [name]=\"item.icon\" [library]=\"item.iconLibrary\" />\n }\n <span class=\"tn-menu-item-label\">{{ item.label }}</span>\n @if (item.shortcut) {\n <span class=\"tn-menu-item-shortcut\">{{ item.shortcut }}</span>\n }\n <span class=\"tn-menu-item-arrow\">\u25B6</span>\n </button>\n\n <ng-template #nestedMenu>\n <div class=\"tn-menu tn-menu--nested\" cdkMenu>\n @for (nestedItem of item.children; track trackByItemId($index, nestedItem)) {\n @if (nestedItem.separator) {\n <div\n class=\"tn-menu-separator\"\n role=\"separator\"\n ></div>\n } @else {\n @if (!nestedItem.children || nestedItem.children.length === 0) {\n <button\n cdkMenuItem\n class=\"tn-menu-item\"\n type=\"button\"\n [disabled]=\"nestedItem.disabled\"\n [class.disabled]=\"nestedItem.disabled\"\n (click)=\"onMenuItemClick(nestedItem)\"\n >\n @if (nestedItem.icon) {\n <tn-icon size=\"sm\" class=\"tn-menu-item-icon\" [name]=\"nestedItem.icon\" [library]=\"nestedItem.iconLibrary\" />\n }\n <span class=\"tn-menu-item-label\">{{ nestedItem.label }}</span>\n @if (nestedItem.shortcut) {\n <span class=\"tn-menu-item-shortcut\">{{ nestedItem.shortcut }}</span>\n }\n </button>\n } @else {\n <button\n cdkMenuItem\n class=\"tn-menu-item tn-menu-item--nested\"\n type=\"button\"\n [cdkMenuTriggerFor]=\"deepNestedMenu\"\n [disabled]=\"nestedItem.disabled\"\n [class.disabled]=\"nestedItem.disabled\"\n >\n @if (nestedItem.icon) {\n <tn-icon size=\"sm\" class=\"tn-menu-item-icon\" [name]=\"nestedItem.icon\" [library]=\"nestedItem.iconLibrary\" />\n }\n <span class=\"tn-menu-item-label\">{{ nestedItem.label }}</span>\n @if (nestedItem.shortcut) {\n <span class=\"tn-menu-item-shortcut\">{{ nestedItem.shortcut }}</span>\n }\n <span class=\"tn-menu-item-arrow\">\u25B6</span>\n </button>\n\n <ng-template #deepNestedMenu>\n <div class=\"tn-menu tn-menu--nested\" cdkMenu>\n @for (deepNestedItem of nestedItem.children; track trackByItemId($index, deepNestedItem)) {\n @if (deepNestedItem.separator) {\n <div\n class=\"tn-menu-separator\"\n role=\"separator\"\n ></div>\n } @else {\n <button\n cdkMenuItem\n class=\"tn-menu-item\"\n type=\"button\"\n [disabled]=\"deepNestedItem.disabled\"\n [class.disabled]=\"deepNestedItem.disabled\"\n (click)=\"onMenuItemClick(deepNestedItem)\"\n >\n @if (deepNestedItem.icon) {\n <tn-icon size=\"sm\" class=\"tn-menu-item-icon\" [name]=\"deepNestedItem.icon\" [library]=\"deepNestedItem.iconLibrary\" />\n }\n <span class=\"tn-menu-item-label\">{{ deepNestedItem.label }}</span>\n @if (deepNestedItem.shortcut) {\n <span class=\"tn-menu-item-shortcut\">{{ deepNestedItem.shortcut }}</span>\n }\n </button>\n }\n }\n </div>\n </ng-template>\n }\n }\n }\n </div>\n </ng-template>\n }\n }\n }\n </div>\n </ng-template>", styles: [".tn-menu-container{display:inline-block;position:relative}.tn-menu-container.tn-menu-container--context{display:block;width:100%;height:100%;cursor:context-menu}.tn-menu-trigger{display:flex;align-items:center;gap:8px;padding:8px 16px;background:var(--tn-bg1, #ffffff);border:1px solid var(--tn-lines, #e0e0e0);border-radius:4px;color:var(--tn-fg1, #333333);cursor:pointer;font-size:14px;transition:all .2s ease}.tn-menu-trigger:hover:not(.disabled){background:var(--tn-bg2, #f5f5f5);border-color:var(--tn-lines, #cccccc)}.tn-menu-trigger:focus{outline:2px solid var(--tn-primary, #007bff);outline-offset:2px}.tn-menu-trigger.disabled{opacity:.5;cursor:not-allowed}.tn-menu-arrow{font-size:12px;transition:transform .2s ease}.tn-menu-arrow.tn-menu-arrow--up{transform:rotate(180deg)}.tn-menu{display:flex;flex-direction:column;background:var(--tn-bg2, #f5f5f5);border:1px solid var(--tn-lines, #e0e0e0);border-radius:4px;box-shadow:0 8px 24px #00000026,0 4px 8px #0000001a;min-width:160px;max-width:300px;padding:4px 0;z-index:1000}.tn-menu-item{display:flex;align-items:center;gap:8px;width:100%;padding:8px 16px;border:none;background:transparent;color:var(--tn-fg1, #333333);cursor:pointer;font-size:14px;text-align:left;transition:background-color .2s ease}.tn-menu-item:hover:not(.disabled){background:var(--tn-alt-bg2, #e8f4fd)!important}.tn-menu-item:focus{outline:none;background:var(--tn-alt-bg2, #e8f4fd)}.tn-menu-item[aria-selected=true]{background:var(--tn-alt-bg2, #e8f4fd)}.tn-menu-item.disabled{opacity:.5;cursor:not-allowed}.tn-menu-item.disabled:hover{background:transparent!important}.tn-menu-item-icon{font-size:16px;width:16px;text-align:center}.tn-menu-item-label{flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.tn-menu-item-shortcut{font-size:12px;color:var(--tn-fg2, #666666);margin-left:auto;padding-left:16px;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica,Arial,sans-serif;font-weight:400;opacity:.7}.tn-menu-item-arrow{font-size:10px;margin-left:8px;color:var(--tn-fg2, #666666);transition:transform .2s ease;flex-shrink:0}.tn-menu-item--nested{position:relative}.tn-menu-item--nested:hover .tn-menu-item-arrow{color:var(--tn-fg1, #333333)}.tn-menu-separator{height:1px;background:var(--tn-lines, #e0e0e0);margin:4px 0}.tn-menu--nested{margin-left:4px;box-shadow:0 8px 24px #00000026,0 4px 8px #0000001a;border-radius:4px}.tn-menu-context-content{width:100%;height:100%}.tn-menu-context-content:hover:before{content:\"\";position:absolute;inset:0;background:rgba(var(--tn-primary-rgb, 0, 123, 255),.05);pointer-events:none;border:1px dashed rgba(var(--tn-primary-rgb, 0, 123, 255),.3);border-radius:4px}\n"] }]
1860
1900
  }], propDecorators: { items: [{ type: i0.Input, args: [{ isSignal: true, alias: "items", required: false }] }], contextMenu: [{ type: i0.Input, args: [{ isSignal: true, alias: "contextMenu", required: false }] }], menuItemClick: [{ type: i0.Output, args: ["menuItemClick"] }], menuOpen: [{ type: i0.Output, args: ["menuOpen"] }], menuClose: [{ type: i0.Output, args: ["menuClose"] }], menuTemplate: [{ type: i0.ViewChild, args: ['menuTemplate', { isSignal: true }] }], contextMenuTemplate: [{ type: i0.ViewChild, args: ['contextMenuTemplate', { isSignal: true }] }] } });
1861
1901
 
1862
1902
  class TnSlideToggleComponent {
@@ -7657,78 +7697,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
7657
7697
  args: ['keydown', ['$event']]
7658
7698
  }] } });
7659
7699
 
7660
- const defaults = {
7661
- panelClass: ['tn-dialog-panel'],
7662
- maxWidth: '90vw',
7663
- maxHeight: '90vh',
7664
- role: 'dialog',
7665
- };
7666
- class TnDialog {
7667
- dialog = inject(Dialog);
7668
- /**
7669
- * Open a dialog with the given component or template.
7670
- * Applies default configuration for panel class, max dimensions, and focus behavior.
7671
- */
7672
- open(target, config) {
7673
- const merged = {
7674
- ...defaults,
7675
- ...config,
7676
- panelClass: [
7677
- ...defaults.panelClass,
7678
- ...(Array.isArray(config?.panelClass) ? config.panelClass : config?.panelClass ? [config.panelClass] : [])
7679
- ],
7680
- autoFocus: config?.autoFocus ?? true,
7681
- restoreFocus: config?.restoreFocus ?? true,
7682
- };
7683
- return this.dialog.open(target, merged);
7684
- }
7685
- /**
7686
- * Open a fullscreen dialog that takes over the entire viewport.
7687
- * Automatically applies fullscreen styling and dimensions.
7688
- */
7689
- openFullscreen(target, config) {
7690
- const merged = {
7691
- ...defaults,
7692
- ...config,
7693
- maxWidth: '100vw',
7694
- maxHeight: '100vh',
7695
- width: '100vw',
7696
- height: '100vh',
7697
- panelClass: [
7698
- ...defaults.panelClass,
7699
- ...(Array.isArray(config?.panelClass) ? config.panelClass : config?.panelClass ? [config.panelClass] : []),
7700
- 'tn-dialog--fullscreen'
7701
- ],
7702
- autoFocus: config?.autoFocus ?? true,
7703
- restoreFocus: config?.restoreFocus ?? true,
7704
- };
7705
- return this.dialog.open(target, merged);
7706
- }
7707
- /**
7708
- * Open a confirmation dialog with customizable title, message, and button labels.
7709
- * Returns a promise that resolves to an Observable of the user's choice.
7710
- */
7711
- confirm(opts) {
7712
- // Import the confirm dialog component dynamically to avoid circular dependencies
7713
- return Promise.resolve().then(function () { return confirmDialog_component; }).then(m => {
7714
- const dialogRef = this.open(m.TnConfirmDialogComponent, {
7715
- data: opts,
7716
- width: '488px',
7717
- role: 'alertdialog',
7718
- disableClose: true,
7719
- panelClass: [opts.destructive ? 'tn-dialog--destructive' : ''].filter(Boolean),
7720
- });
7721
- return dialogRef.closed;
7722
- });
7723
- }
7724
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnDialog, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
7725
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnDialog, providedIn: 'root' });
7726
- }
7727
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnDialog, decorators: [{
7728
- type: Injectable,
7729
- args: [{ providedIn: 'root' }]
7730
- }] });
7731
-
7732
7700
  class TnDialogShellComponent {
7733
7701
  title = input('', ...(ngDevMode ? [{ debugName: "title" }] : []));
7734
7702
  showFullscreenButton = input(false, ...(ngDevMode ? [{ debugName: "showFullscreenButton" }] : []));
@@ -7817,10 +7785,281 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
7817
7785
  }, template: "<tn-dialog-shell [title]=\"data.title\">\n <p style=\"margin: 0;\">{{ data.message }}</p>\n <div tnDialogAction>\n <tn-button\n type=\"button\"\n variant=\"outline\"\n [label]=\"data.cancelText || 'Cancel'\"\n (click)=\"ref.close(false)\" />\n <tn-button\n type=\"button\"\n [color]=\"data.destructive ? 'warn' : 'primary'\"\n [label]=\"data.confirmText || 'OK'\"\n (click)=\"ref.close(true)\" />\n </div>\n</tn-dialog-shell>\n" }]
7818
7786
  }] });
7819
7787
 
7820
- var confirmDialog_component = /*#__PURE__*/Object.freeze({
7821
- __proto__: null,
7822
- TnConfirmDialogComponent: TnConfirmDialogComponent
7823
- });
7788
+ const defaults = {
7789
+ panelClass: ['tn-dialog-panel'],
7790
+ maxWidth: '90vw',
7791
+ maxHeight: '90vh',
7792
+ role: 'dialog',
7793
+ };
7794
+ class TnDialog {
7795
+ dialog = inject(Dialog);
7796
+ /**
7797
+ * Open a dialog with the given component or template.
7798
+ * Applies default configuration for panel class, max dimensions, and focus behavior.
7799
+ */
7800
+ open(target, config) {
7801
+ const merged = {
7802
+ ...defaults,
7803
+ ...config,
7804
+ panelClass: [
7805
+ ...defaults.panelClass,
7806
+ ...(Array.isArray(config?.panelClass) ? config.panelClass : config?.panelClass ? [config.panelClass] : [])
7807
+ ],
7808
+ autoFocus: config?.autoFocus ?? true,
7809
+ restoreFocus: config?.restoreFocus ?? true,
7810
+ };
7811
+ return this.dialog.open(target, merged);
7812
+ }
7813
+ /**
7814
+ * Open a fullscreen dialog that takes over the entire viewport.
7815
+ * Automatically applies fullscreen styling and dimensions.
7816
+ */
7817
+ openFullscreen(target, config) {
7818
+ const merged = {
7819
+ ...defaults,
7820
+ ...config,
7821
+ maxWidth: '100vw',
7822
+ maxHeight: '100vh',
7823
+ width: '100vw',
7824
+ height: '100vh',
7825
+ panelClass: [
7826
+ ...defaults.panelClass,
7827
+ ...(Array.isArray(config?.panelClass) ? config.panelClass : config?.panelClass ? [config.panelClass] : []),
7828
+ 'tn-dialog--fullscreen'
7829
+ ],
7830
+ autoFocus: config?.autoFocus ?? true,
7831
+ restoreFocus: config?.restoreFocus ?? true,
7832
+ };
7833
+ return this.dialog.open(target, merged);
7834
+ }
7835
+ /**
7836
+ * Open a confirmation dialog. Resolves to true if confirmed, false otherwise.
7837
+ */
7838
+ async confirm(opts) {
7839
+ const dialogRef = this.open(TnConfirmDialogComponent, {
7840
+ data: opts,
7841
+ width: '488px',
7842
+ role: 'alertdialog',
7843
+ disableClose: true,
7844
+ panelClass: [opts.destructive ? 'tn-dialog--destructive' : ''].filter(Boolean),
7845
+ });
7846
+ const result = await firstValueFrom(dialogRef.closed);
7847
+ return !!result;
7848
+ }
7849
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnDialog, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
7850
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnDialog, providedIn: 'root' });
7851
+ }
7852
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnDialog, decorators: [{
7853
+ type: Injectable,
7854
+ args: [{ providedIn: 'root' }]
7855
+ }] });
7856
+
7857
+ /**
7858
+ * Harness for interacting with `tn-dialog-shell` in tests.
7859
+ *
7860
+ * Dialogs are portaled into the CDK overlay outside the component tree.
7861
+ * Use `TnDialogTesting.rootLoader(fixture)` to get a loader that can find them.
7862
+ *
7863
+ * @example
7864
+ * ```typescript
7865
+ * const dialogLoader = TnDialogTesting.rootLoader(fixture);
7866
+ *
7867
+ * // Find dialog by title
7868
+ * const dialog = await dialogLoader.getHarness(
7869
+ * TnDialogHarness.with({ title: 'Delete Dataset?' })
7870
+ * );
7871
+ *
7872
+ * // Click an action button and close
7873
+ * await dialog.clickActionButton('Delete');
7874
+ * ```
7875
+ */
7876
+ class TnDialogHarness extends ComponentHarness {
7877
+ /**
7878
+ * The selector for the host element of a dialog shell instance.
7879
+ */
7880
+ static hostSelector = 'tn-dialog-shell';
7881
+ _title = this.locatorFor('.tn-dialog__title');
7882
+ _closeButton = this.locatorFor('.tn-dialog__close');
7883
+ _fullscreenButton = this.locatorForOptional('.tn-dialog__fullscreen');
7884
+ _content = this.locatorFor('.tn-dialog__content');
7885
+ /**
7886
+ * Gets a `HarnessPredicate` that can be used to search for a dialog
7887
+ * with specific attributes.
7888
+ *
7889
+ * @param options Options for filtering which dialog instances are considered a match.
7890
+ * @returns A `HarnessPredicate` configured with the given options.
7891
+ *
7892
+ * @example
7893
+ * ```typescript
7894
+ * const dialog = await dialogLoader.getHarness(
7895
+ * TnDialogHarness.with({ title: 'Edit User' })
7896
+ * );
7897
+ *
7898
+ * // With regex
7899
+ * const dialog = await dialogLoader.getHarness(
7900
+ * TnDialogHarness.with({ title: /delete/i })
7901
+ * );
7902
+ * ```
7903
+ */
7904
+ static with(options = {}) {
7905
+ return new HarnessPredicate(TnDialogHarness, options)
7906
+ .addOption('title', options.title, (harness, title) => HarnessPredicate.stringMatches(harness.getTitle(), title));
7907
+ }
7908
+ /**
7909
+ * Gets the dialog's title text.
7910
+ *
7911
+ * @returns Promise resolving to the dialog title.
7912
+ *
7913
+ * @example
7914
+ * ```typescript
7915
+ * const dialog = await dialogLoader.getHarness(TnDialogHarness);
7916
+ * expect(await dialog.getTitle()).toBe('Edit User');
7917
+ * ```
7918
+ */
7919
+ async getTitle() {
7920
+ const title = await this._title();
7921
+ return (await title.text()).trim();
7922
+ }
7923
+ /**
7924
+ * Gets the text content of the dialog's scrollable content area.
7925
+ *
7926
+ * @returns Promise resolving to the content text.
7927
+ *
7928
+ * @example
7929
+ * ```typescript
7930
+ * const dialog = await dialogLoader.getHarness(TnDialogHarness);
7931
+ * expect(await dialog.getContentText()).toContain('Are you sure?');
7932
+ * ```
7933
+ */
7934
+ async getContentText() {
7935
+ const content = await this._content();
7936
+ return (await content.text()).trim();
7937
+ }
7938
+ /**
7939
+ * Clicks the close button (X) in the dialog header.
7940
+ *
7941
+ * @returns Promise that resolves when the click action is complete.
7942
+ *
7943
+ * @example
7944
+ * ```typescript
7945
+ * const dialog = await dialogLoader.getHarness(TnDialogHarness);
7946
+ * await dialog.close();
7947
+ * ```
7948
+ */
7949
+ async close() {
7950
+ const closeBtn = await this._closeButton();
7951
+ await closeBtn.click();
7952
+ }
7953
+ /**
7954
+ * Clicks an action button in the dialog footer by its label.
7955
+ * Only matches buttons inside the `tnDialogAction` footer area, not buttons in content.
7956
+ *
7957
+ * @param label The button label to match. Supports string or regex.
7958
+ * @throws Error if no matching button is found in the actions area.
7959
+ *
7960
+ * @example
7961
+ * ```typescript
7962
+ * const dialog = await dialogLoader.getHarness(TnDialogHarness);
7963
+ * await dialog.clickActionButton('Save');
7964
+ * await dialog.clickActionButton(/cancel/i);
7965
+ * ```
7966
+ */
7967
+ async clickActionButton(label) {
7968
+ const buttons = await this.locatorForAll(TnButtonHarness.with({ label, ancestor: '.tn-dialog__actions' }))();
7969
+ if (buttons.length === 0) {
7970
+ throw new Error(`No action button found with label matching: ${label}`);
7971
+ }
7972
+ await buttons[0].click();
7973
+ }
7974
+ /**
7975
+ * Gets all action button harnesses in the dialog footer.
7976
+ *
7977
+ * @returns Promise resolving to an array of `TnButtonHarness` instances.
7978
+ *
7979
+ * @example
7980
+ * ```typescript
7981
+ * const dialog = await dialogLoader.getHarness(TnDialogHarness);
7982
+ * const buttons = await dialog.getActionButtons();
7983
+ * expect(buttons).toHaveLength(2);
7984
+ * ```
7985
+ */
7986
+ async getActionButtons() {
7987
+ return this.locatorForAll(TnButtonHarness.with({ ancestor: '.tn-dialog__actions' }))();
7988
+ }
7989
+ /**
7990
+ * Whether the dialog has a fullscreen toggle button.
7991
+ *
7992
+ * @returns Promise resolving to true if the fullscreen button is present.
7993
+ */
7994
+ async hasFullscreenButton() {
7995
+ return (await this._fullscreenButton()) !== null;
7996
+ }
7997
+ /**
7998
+ * Clicks the fullscreen toggle button.
7999
+ *
8000
+ * @throws Error if the dialog does not have a fullscreen button.
8001
+ */
8002
+ async toggleFullscreen() {
8003
+ const btn = await this._fullscreenButton();
8004
+ if (!btn) {
8005
+ throw new Error('Dialog does not have a fullscreen button');
8006
+ }
8007
+ await btn.click();
8008
+ }
8009
+ /**
8010
+ * Whether the dialog is currently in fullscreen mode.
8011
+ * Checks the aria-label of the fullscreen button.
8012
+ *
8013
+ * @returns Promise resolving to true if fullscreen, false if not or if no fullscreen button.
8014
+ */
8015
+ async isFullscreen() {
8016
+ const btn = await this._fullscreenButton();
8017
+ if (!btn) {
8018
+ return false;
8019
+ }
8020
+ const ariaLabel = await btn.getAttribute('aria-label');
8021
+ return ariaLabel === 'Exit fullscreen';
8022
+ }
8023
+ }
8024
+
8025
+ /**
8026
+ * Test utilities for working with `TnDialogHarness`.
8027
+ *
8028
+ * Dialogs are portaled into the CDK overlay outside the component tree,
8029
+ * so a regular `TestbedHarnessEnvironment.loader()` won't find them.
8030
+ * Use `TnDialogTesting.rootLoader()` to get a loader that can.
8031
+ *
8032
+ * @example
8033
+ * ```typescript
8034
+ * import { TnDialogTesting, TnDialogHarness } from '@truenas/ui-components';
8035
+ *
8036
+ * let dialogLoader: HarnessLoader;
8037
+ *
8038
+ * beforeEach(() => {
8039
+ * fixture = TestBed.createComponent(TestHostComponent);
8040
+ * dialogLoader = TnDialogTesting.rootLoader(fixture);
8041
+ * });
8042
+ *
8043
+ * it('should open a confirm dialog', async () => {
8044
+ * const dialog = await dialogLoader.getHarness(
8045
+ * TnDialogHarness.with({ title: 'Delete?' })
8046
+ * );
8047
+ * await dialog.clickActionButton('Delete');
8048
+ * });
8049
+ * ```
8050
+ */
8051
+ class TnDialogTesting {
8052
+ /**
8053
+ * Creates a `HarnessLoader` that searches the entire document,
8054
+ * including the CDK overlay where dialogs are rendered.
8055
+ *
8056
+ * @param fixture The component fixture for the test.
8057
+ * @returns A `HarnessLoader` capable of finding dialog harnesses.
8058
+ */
8059
+ static rootLoader(fixture) {
8060
+ return TestbedHarnessEnvironment.documentRootLoader(fixture);
8061
+ }
8062
+ }
7824
8063
 
7825
8064
  /**
7826
8065
  * Directive to mark an element as a side-panel footer action.
@@ -9633,5 +9872,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
9633
9872
  * Generated bundle index. Do not edit.
9634
9873
  */
9635
9874
 
9636
- 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, TnEmptyComponent, TnEmptyHarness, TnExpansionPanelComponent, TnFilePickerComponent, TnFilePickerPopupComponent, TnFormFieldComponent, TnHeaderCellDefDirective, TnIconButtonComponent, TnIconButtonHarness, TnIconComponent, TnIconHarness, TnIconRegistryService, TnIconTesting, TnInputComponent, TnInputDirective, TnInputHarness, TnKeyboardShortcutComponent, TnKeyboardShortcutService, TnListAvatarDirective, TnListComponent, TnListIconDirective, TnListItemComponent, TnListItemLineDirective, TnListItemPrimaryDirective, TnListItemSecondaryDirective, TnListItemTitleDirective, TnListItemTrailingDirective, TnListOptionComponent, TnListSubheaderComponent, TnMenuComponent, TnMenuTriggerDirective, TnMonthViewComponent, TnMultiYearViewComponent, TnNestedTreeNodeComponent, TnParticleProgressBarComponent, TnProgressBarComponent, TnRadioComponent, TnSelectComponent, TnSelectHarness, TnSelectionListComponent, TnSidePanelActionDirective, TnSidePanelComponent, TnSidePanelHarness, TnSidePanelHeaderActionDirective, TnSlideToggleComponent, TnSliderComponent, TnSliderThumbDirective, TnSliderWithLabelDirective, TnSpinnerComponent, TnSpriteLoaderService, TnStepComponent, TnStepperComponent, TnTabComponent, TnTabHarness, TnTabPanelComponent, TnTabPanelHarness, TnTableColumnDirective, TnTableComponent, TnTabsComponent, TnTabsHarness, TnTheme, TnThemeService, TnTimeInputComponent, TnTooltipComponent, TnTooltipDirective, TnTreeComponent, TnTreeFlatDataSource, TnTreeFlattener, TnTreeNodeComponent, TnTreeNodeOutletDirective, TruncatePathPipe, WindowsModifierKeys, WindowsShortcuts, createLucideLibrary, createShortcut, defaultSpriteBasePath, defaultSpriteConfigPath, libIconMarker, registerLucideIcons, setupLucideIntegration, tnIconMarker };
9875
+ 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, TnDialogHarness, TnDialogShellComponent, TnDialogTesting, TnDividerComponent, TnDividerDirective, TnEmptyComponent, TnEmptyHarness, TnExpansionPanelComponent, TnFilePickerComponent, TnFilePickerPopupComponent, TnFormFieldComponent, 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, TnMenuTriggerDirective, TnMonthViewComponent, TnMultiYearViewComponent, TnNestedTreeNodeComponent, TnParticleProgressBarComponent, TnProgressBarComponent, TnRadioComponent, TnSelectComponent, TnSelectHarness, TnSelectionListComponent, TnSidePanelActionDirective, TnSidePanelComponent, TnSidePanelHarness, TnSidePanelHeaderActionDirective, TnSlideToggleComponent, TnSliderComponent, TnSliderThumbDirective, TnSliderWithLabelDirective, TnSpinnerComponent, TnSpriteLoaderService, TnStepComponent, TnStepperComponent, TnTabComponent, TnTabHarness, TnTabPanelComponent, TnTabPanelHarness, TnTableColumnDirective, TnTableComponent, TnTabsComponent, TnTabsHarness, TnTheme, TnThemeService, TnTimeInputComponent, TnTooltipComponent, TnTooltipDirective, TnTreeComponent, TnTreeFlatDataSource, TnTreeFlattener, TnTreeNodeComponent, TnTreeNodeOutletDirective, TruncatePathPipe, WindowsModifierKeys, WindowsShortcuts, createLucideLibrary, createShortcut, defaultSpriteBasePath, defaultSpriteConfigPath, libIconMarker, registerLucideIcons, setupLucideIntegration, tnIconMarker };
9637
9876
  //# sourceMappingURL=truenas-ui-components.mjs.map