@truenas/ui-components 0.3.8 → 0.3.10

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.
@@ -2319,6 +2319,17 @@ class TnButtonComponent {
2319
2319
  variant = input('filled', ...(ngDevMode ? [{ debugName: "variant" }] : []));
2320
2320
  backgroundColor = input(undefined, ...(ngDevMode ? [{ debugName: "backgroundColor" }] : []));
2321
2321
  label = input('Button', ...(ngDevMode ? [{ debugName: "label" }] : []));
2322
+ /**
2323
+ * Optional icon rendered alongside the label. Accepts any name resolvable by
2324
+ * `tn-icon` (sprite/registry/library). Use `iconPosition` to place it before
2325
+ * or after the label.
2326
+ */
2327
+ icon = input(undefined, ...(ngDevMode ? [{ debugName: "icon" }] : []));
2328
+ /**
2329
+ * Side of the label the `icon` sits on. `left` (default) renders the icon
2330
+ * before the label; `right` renders it after. No effect when `icon` is unset.
2331
+ */
2332
+ iconPosition = input('left', ...(ngDevMode ? [{ debugName: "iconPosition" }] : []));
2322
2333
  disabled = input(false, ...(ngDevMode ? [{ debugName: "disabled" }] : []));
2323
2334
  /**
2324
2335
  * Native `type` of the rendered `<button>`. Defaults to `button` so stray
@@ -2385,7 +2396,11 @@ class TnButtonComponent {
2385
2396
  mode = 'button-default';
2386
2397
  }
2387
2398
  }
2388
- return ['storybook-button', `storybook-button--${this.size}`, mode];
2399
+ const classes = ['storybook-button', `storybook-button--${this.size}`, mode];
2400
+ if (this.icon()) {
2401
+ classes.push('storybook-button--has-icon');
2402
+ }
2403
+ return classes;
2389
2404
  }, ...(ngDevMode ? [{ debugName: "classes" }] : []));
2390
2405
  handleAnchorClick(event) {
2391
2406
  if (this.disabled()) {
@@ -2426,12 +2441,12 @@ class TnButtonComponent {
2426
2441
  host.focus = (options) => inner.focus(options);
2427
2442
  }
2428
2443
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnButtonComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
2429
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.0", type: TnButtonComponent, isStandalone: true, selector: "tn-button", inputs: { primary: { classPropertyName: "primary", publicName: "primary", isSignal: true, isRequired: false, transformFunction: null }, color: { classPropertyName: "color", publicName: "color", isSignal: true, isRequired: false, transformFunction: null }, variant: { classPropertyName: "variant", publicName: "variant", isSignal: true, isRequired: false, transformFunction: null }, backgroundColor: { classPropertyName: "backgroundColor", publicName: "backgroundColor", isSignal: true, isRequired: false, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, type: { classPropertyName: "type", publicName: "type", isSignal: true, isRequired: false, transformFunction: null }, testId: { classPropertyName: "testId", publicName: "testId", isSignal: true, isRequired: false, transformFunction: null }, href: { classPropertyName: "href", publicName: "href", isSignal: true, isRequired: false, transformFunction: null }, routerLink: { classPropertyName: "routerLink", publicName: "routerLink", isSignal: true, isRequired: false, transformFunction: null }, queryParams: { classPropertyName: "queryParams", publicName: "queryParams", isSignal: true, isRequired: false, transformFunction: null }, fragment: { classPropertyName: "fragment", publicName: "fragment", isSignal: true, isRequired: false, transformFunction: null }, target: { classPropertyName: "target", publicName: "target", isSignal: true, isRequired: false, transformFunction: null }, rel: { classPropertyName: "rel", publicName: "rel", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { onClick: "onClick" }, viewQueries: [{ propertyName: "innerRef", first: true, predicate: ["button"], descendants: true, isSignal: true }], ngImport: i0, template: "@if (isRouterLink()) {\n <a\n #button\n tnTestIdType=\"button\"\n [ngClass]=\"classes()\"\n [ngStyle]=\"{ 'background-color': backgroundColor() }\"\n [routerLink]=\"disabled() ? null : routerLink()\"\n [queryParams]=\"queryParams()\"\n [fragment]=\"fragment()\"\n [target]=\"target()\"\n [rel]=\"rel()\"\n [attr.aria-disabled]=\"disabled() ? 'true' : null\"\n [attr.aria-label]=\"ariaLabel()\"\n [attr.tabindex]=\"disabled() ? -1 : null\"\n [class.is-disabled]=\"disabled()\"\n [tnTestId]=\"testId()\"\n (click)=\"handleAnchorClick($event)\"\n >\n <span [innerHTML]=\"label() | tnLabelMarkup\"></span>\n </a>\n} @else if (isAnchor()) {\n <a\n #button\n tnTestIdType=\"button\"\n [ngClass]=\"classes()\"\n [ngStyle]=\"{ 'background-color': backgroundColor() }\"\n [attr.href]=\"disabled() ? null : href()\"\n [target]=\"target()\"\n [rel]=\"rel()\"\n [attr.aria-disabled]=\"disabled() ? 'true' : null\"\n [attr.aria-label]=\"ariaLabel()\"\n [attr.tabindex]=\"disabled() ? -1 : null\"\n [class.is-disabled]=\"disabled()\"\n [tnTestId]=\"testId()\"\n (click)=\"handleAnchorClick($event)\"\n >\n <span [innerHTML]=\"label() | tnLabelMarkup\"></span>\n </a>\n} @else {\n <button\n #button\n tnTestIdType=\"button\"\n [attr.type]=\"type()\"\n [ngClass]=\"classes()\"\n [ngStyle]=\"{ 'background-color': backgroundColor() }\"\n [disabled]=\"disabled()\"\n [attr.aria-label]=\"ariaLabel()\"\n [tnTestId]=\"testId()\"\n (click)=\"onClick.emit($event)\"\n >\n <span [innerHTML]=\"label() | tnLabelMarkup\"></span>\n </button>\n}\n", styles: [":host{display:inline-block;width:fit-content;justify-self:center}.storybook-button ::ng-deep code{font-family:var(--tn-font-family-monospace, monospace);font-size:.875em;background:var(--tn-bg2, #f5f5f5);border:1px solid var(--tn-lines, #ccc);border-radius:4px;padding:.125em .45em}.storybook-button{display:inline-block;cursor:pointer;border:0;font-weight:500;line-height:1;font-family:IBM Plex Sans,Helvetica Neue,Helvetica,Arial,sans-serif}.storybook-button:disabled,.storybook-button.is-disabled{opacity:.5;cursor:not-allowed;pointer-events:none}a.storybook-button{text-decoration:none;text-align:center}.button-primary{background-color:var(--tn-primary);color:var(--tn-primary-txt)}.button-default{box-shadow:#00000026 0 0 0 1px inset;background-color:var(--tn-btn-default-bg);color:var(--tn-btn-default-txt)}.button-outline-primary{background-color:transparent;border:1px solid var(--tn-primary);color:var(--tn-primary);transition:background-color .2s ease-in-out,border-color .2s ease-in-out,color .2s ease-in-out}.button-outline-primary:hover{background-color:var(--tn-primary);border:1px solid var(--tn-primary);color:var(--tn-primary-txt)}.button-outline-default{background-color:transparent;border:1px solid var(--tn-lines, #e5e7eb);color:var(--tn-fg1, #000000);transition:background-color .2s ease-in-out,border-color .2s ease-in-out,color .2s ease-in-out}.button-outline-default:hover{background-color:var(--tn-btn-default-bg);border:1px solid var(--tn-btn-default-bg);color:var(--tn-btn-default-txt);box-shadow:#00000026 0 0 0 1px inset}.button-warn{background-color:var(--tn-red);color:#fff}.button-outline-warn{background-color:transparent;border:1px solid var(--tn-red);color:var(--tn-red);transition:background-color .2s ease-in-out,border-color .2s ease-in-out,color .2s ease-in-out}.button-outline-warn:hover{background-color:var(--tn-red);border:1px solid var(--tn-red);color:#fff}.storybook-button--small{padding:10px 16px;font-size:12px}.storybook-button--medium{padding:11px 20px;font-size:14px}.storybook-button--large{padding:12px 24px;font-size:1rem}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1$2.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1$2.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "directive", type: RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "routerLink"] }, { kind: "directive", type: TnTestIdDirective, selector: "[tnTestId]", inputs: ["tnTestId", "tnTestIdType"] }, { kind: "pipe", type: LabelMarkupPipe, name: "tnLabelMarkup" }] });
2444
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.0", type: TnButtonComponent, isStandalone: true, selector: "tn-button", inputs: { primary: { classPropertyName: "primary", publicName: "primary", isSignal: true, isRequired: false, transformFunction: null }, color: { classPropertyName: "color", publicName: "color", isSignal: true, isRequired: false, transformFunction: null }, variant: { classPropertyName: "variant", publicName: "variant", isSignal: true, isRequired: false, transformFunction: null }, backgroundColor: { classPropertyName: "backgroundColor", publicName: "backgroundColor", isSignal: true, isRequired: false, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, icon: { classPropertyName: "icon", publicName: "icon", isSignal: true, isRequired: false, transformFunction: null }, iconPosition: { classPropertyName: "iconPosition", publicName: "iconPosition", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, type: { classPropertyName: "type", publicName: "type", isSignal: true, isRequired: false, transformFunction: null }, testId: { classPropertyName: "testId", publicName: "testId", isSignal: true, isRequired: false, transformFunction: null }, href: { classPropertyName: "href", publicName: "href", isSignal: true, isRequired: false, transformFunction: null }, routerLink: { classPropertyName: "routerLink", publicName: "routerLink", isSignal: true, isRequired: false, transformFunction: null }, queryParams: { classPropertyName: "queryParams", publicName: "queryParams", isSignal: true, isRequired: false, transformFunction: null }, fragment: { classPropertyName: "fragment", publicName: "fragment", isSignal: true, isRequired: false, transformFunction: null }, target: { classPropertyName: "target", publicName: "target", isSignal: true, isRequired: false, transformFunction: null }, rel: { classPropertyName: "rel", publicName: "rel", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { onClick: "onClick" }, viewQueries: [{ propertyName: "innerRef", first: true, predicate: ["button"], descendants: true, isSignal: true }], ngImport: i0, template: "<ng-template #content>\n @if (icon() && iconPosition() === 'left') {\n <tn-icon class=\"storybook-button__icon\" size=\"sm\" aria-hidden=\"true\" [name]=\"icon()!\" />\n }\n <span class=\"storybook-button__label\" [innerHTML]=\"label() | tnLabelMarkup\"></span>\n @if (icon() && iconPosition() === 'right') {\n <tn-icon class=\"storybook-button__icon\" size=\"sm\" aria-hidden=\"true\" [name]=\"icon()!\" />\n }\n</ng-template>\n\n@if (isRouterLink()) {\n <a\n #button\n tnTestIdType=\"button\"\n [ngClass]=\"classes()\"\n [ngStyle]=\"{ 'background-color': backgroundColor() }\"\n [routerLink]=\"disabled() ? null : routerLink()\"\n [queryParams]=\"queryParams()\"\n [fragment]=\"fragment()\"\n [target]=\"target()\"\n [rel]=\"rel()\"\n [attr.aria-disabled]=\"disabled() ? 'true' : null\"\n [attr.aria-label]=\"ariaLabel()\"\n [attr.tabindex]=\"disabled() ? -1 : null\"\n [class.is-disabled]=\"disabled()\"\n [tnTestId]=\"testId()\"\n (click)=\"handleAnchorClick($event)\"\n >\n <ng-container [ngTemplateOutlet]=\"content\" />\n </a>\n} @else if (isAnchor()) {\n <a\n #button\n tnTestIdType=\"button\"\n [ngClass]=\"classes()\"\n [ngStyle]=\"{ 'background-color': backgroundColor() }\"\n [attr.href]=\"disabled() ? null : href()\"\n [target]=\"target()\"\n [rel]=\"rel()\"\n [attr.aria-disabled]=\"disabled() ? 'true' : null\"\n [attr.aria-label]=\"ariaLabel()\"\n [attr.tabindex]=\"disabled() ? -1 : null\"\n [class.is-disabled]=\"disabled()\"\n [tnTestId]=\"testId()\"\n (click)=\"handleAnchorClick($event)\"\n >\n <ng-container [ngTemplateOutlet]=\"content\" />\n </a>\n} @else {\n <button\n #button\n tnTestIdType=\"button\"\n [attr.type]=\"type()\"\n [ngClass]=\"classes()\"\n [ngStyle]=\"{ 'background-color': backgroundColor() }\"\n [disabled]=\"disabled()\"\n [attr.aria-label]=\"ariaLabel()\"\n [tnTestId]=\"testId()\"\n (click)=\"onClick.emit($event)\"\n >\n <ng-container [ngTemplateOutlet]=\"content\" />\n </button>\n}\n", styles: [":host{display:inline-block;width:fit-content;justify-self:center}.storybook-button ::ng-deep code{font-family:var(--tn-font-family-monospace, monospace);font-size:.875em;background:var(--tn-bg2, #f5f5f5);border:1px solid var(--tn-lines, #ccc);border-radius:4px;padding:.125em .45em}.storybook-button{display:inline-block;cursor:pointer;border:0;font-weight:500;line-height:1;font-family:IBM Plex Sans,Helvetica Neue,Helvetica,Arial,sans-serif}.storybook-button:disabled,.storybook-button.is-disabled{opacity:.5;cursor:not-allowed;pointer-events:none}.storybook-button--has-icon{display:inline-flex;align-items:center;justify-content:center;gap:8px}.storybook-button__icon{display:inline-flex;align-items:center;justify-content:center;flex-shrink:0}a.storybook-button{text-decoration:none;text-align:center}.button-primary{background-color:var(--tn-primary);color:var(--tn-primary-txt)}.button-default{box-shadow:#00000026 0 0 0 1px inset;background-color:var(--tn-btn-default-bg);color:var(--tn-btn-default-txt)}.button-outline-primary{background-color:transparent;border:1px solid var(--tn-primary);color:var(--tn-primary);transition:background-color .2s ease-in-out,border-color .2s ease-in-out,color .2s ease-in-out}.button-outline-primary:hover{background-color:var(--tn-primary);border:1px solid var(--tn-primary);color:var(--tn-primary-txt)}.button-outline-default{background-color:transparent;border:1px solid var(--tn-lines, #e5e7eb);color:var(--tn-fg1, #000000);transition:background-color .2s ease-in-out,border-color .2s ease-in-out,color .2s ease-in-out}.button-outline-default:hover{background-color:var(--tn-btn-default-bg);border:1px solid var(--tn-btn-default-bg);color:var(--tn-btn-default-txt);box-shadow:#00000026 0 0 0 1px inset}.button-warn{background-color:var(--tn-red);color:#fff}.button-outline-warn{background-color:transparent;border:1px solid var(--tn-red);color:var(--tn-red);transition:background-color .2s ease-in-out,border-color .2s ease-in-out,color .2s ease-in-out}.button-outline-warn:hover{background-color:var(--tn-red);border:1px solid var(--tn-red);color:#fff}.storybook-button--small{padding:10px 16px;font-size:12px}.storybook-button--medium{padding:11px 20px;font-size:14px}.storybook-button--large{padding:12px 24px;font-size:1rem}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1$2.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1$2.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "directive", type: i1$2.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "directive", type: RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "routerLink"] }, { kind: "directive", type: TnTestIdDirective, selector: "[tnTestId]", inputs: ["tnTestId", "tnTestIdType"] }, { kind: "component", type: TnIconComponent, selector: "tn-icon", inputs: ["name", "size", "color", "tooltip", "ariaLabel", "library", "testId", "fullSize", "customSize"] }, { kind: "pipe", type: LabelMarkupPipe, name: "tnLabelMarkup" }] });
2430
2445
  }
2431
2446
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnButtonComponent, decorators: [{
2432
2447
  type: Component,
2433
- args: [{ selector: 'tn-button', standalone: true, imports: [CommonModule, RouterLink, TnTestIdDirective, LabelMarkupPipe], template: "@if (isRouterLink()) {\n <a\n #button\n tnTestIdType=\"button\"\n [ngClass]=\"classes()\"\n [ngStyle]=\"{ 'background-color': backgroundColor() }\"\n [routerLink]=\"disabled() ? null : routerLink()\"\n [queryParams]=\"queryParams()\"\n [fragment]=\"fragment()\"\n [target]=\"target()\"\n [rel]=\"rel()\"\n [attr.aria-disabled]=\"disabled() ? 'true' : null\"\n [attr.aria-label]=\"ariaLabel()\"\n [attr.tabindex]=\"disabled() ? -1 : null\"\n [class.is-disabled]=\"disabled()\"\n [tnTestId]=\"testId()\"\n (click)=\"handleAnchorClick($event)\"\n >\n <span [innerHTML]=\"label() | tnLabelMarkup\"></span>\n </a>\n} @else if (isAnchor()) {\n <a\n #button\n tnTestIdType=\"button\"\n [ngClass]=\"classes()\"\n [ngStyle]=\"{ 'background-color': backgroundColor() }\"\n [attr.href]=\"disabled() ? null : href()\"\n [target]=\"target()\"\n [rel]=\"rel()\"\n [attr.aria-disabled]=\"disabled() ? 'true' : null\"\n [attr.aria-label]=\"ariaLabel()\"\n [attr.tabindex]=\"disabled() ? -1 : null\"\n [class.is-disabled]=\"disabled()\"\n [tnTestId]=\"testId()\"\n (click)=\"handleAnchorClick($event)\"\n >\n <span [innerHTML]=\"label() | tnLabelMarkup\"></span>\n </a>\n} @else {\n <button\n #button\n tnTestIdType=\"button\"\n [attr.type]=\"type()\"\n [ngClass]=\"classes()\"\n [ngStyle]=\"{ 'background-color': backgroundColor() }\"\n [disabled]=\"disabled()\"\n [attr.aria-label]=\"ariaLabel()\"\n [tnTestId]=\"testId()\"\n (click)=\"onClick.emit($event)\"\n >\n <span [innerHTML]=\"label() | tnLabelMarkup\"></span>\n </button>\n}\n", styles: [":host{display:inline-block;width:fit-content;justify-self:center}.storybook-button ::ng-deep code{font-family:var(--tn-font-family-monospace, monospace);font-size:.875em;background:var(--tn-bg2, #f5f5f5);border:1px solid var(--tn-lines, #ccc);border-radius:4px;padding:.125em .45em}.storybook-button{display:inline-block;cursor:pointer;border:0;font-weight:500;line-height:1;font-family:IBM Plex Sans,Helvetica Neue,Helvetica,Arial,sans-serif}.storybook-button:disabled,.storybook-button.is-disabled{opacity:.5;cursor:not-allowed;pointer-events:none}a.storybook-button{text-decoration:none;text-align:center}.button-primary{background-color:var(--tn-primary);color:var(--tn-primary-txt)}.button-default{box-shadow:#00000026 0 0 0 1px inset;background-color:var(--tn-btn-default-bg);color:var(--tn-btn-default-txt)}.button-outline-primary{background-color:transparent;border:1px solid var(--tn-primary);color:var(--tn-primary);transition:background-color .2s ease-in-out,border-color .2s ease-in-out,color .2s ease-in-out}.button-outline-primary:hover{background-color:var(--tn-primary);border:1px solid var(--tn-primary);color:var(--tn-primary-txt)}.button-outline-default{background-color:transparent;border:1px solid var(--tn-lines, #e5e7eb);color:var(--tn-fg1, #000000);transition:background-color .2s ease-in-out,border-color .2s ease-in-out,color .2s ease-in-out}.button-outline-default:hover{background-color:var(--tn-btn-default-bg);border:1px solid var(--tn-btn-default-bg);color:var(--tn-btn-default-txt);box-shadow:#00000026 0 0 0 1px inset}.button-warn{background-color:var(--tn-red);color:#fff}.button-outline-warn{background-color:transparent;border:1px solid var(--tn-red);color:var(--tn-red);transition:background-color .2s ease-in-out,border-color .2s ease-in-out,color .2s ease-in-out}.button-outline-warn:hover{background-color:var(--tn-red);border:1px solid var(--tn-red);color:#fff}.storybook-button--small{padding:10px 16px;font-size:12px}.storybook-button--medium{padding:11px 20px;font-size:14px}.storybook-button--large{padding:12px 24px;font-size:1rem}\n"] }]
2434
- }], propDecorators: { primary: [{ type: i0.Input, args: [{ isSignal: true, alias: "primary", required: false }] }], color: [{ type: i0.Input, args: [{ isSignal: true, alias: "color", required: false }] }], variant: [{ type: i0.Input, args: [{ isSignal: true, alias: "variant", required: false }] }], backgroundColor: [{ type: i0.Input, args: [{ isSignal: true, alias: "backgroundColor", required: false }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], type: [{ type: i0.Input, args: [{ isSignal: true, alias: "type", required: false }] }], testId: [{ type: i0.Input, args: [{ isSignal: true, alias: "testId", required: false }] }], href: [{ type: i0.Input, args: [{ isSignal: true, alias: "href", required: false }] }], routerLink: [{ type: i0.Input, args: [{ isSignal: true, alias: "routerLink", required: false }] }], queryParams: [{ type: i0.Input, args: [{ isSignal: true, alias: "queryParams", required: false }] }], fragment: [{ type: i0.Input, args: [{ isSignal: true, alias: "fragment", required: false }] }], target: [{ type: i0.Input, args: [{ isSignal: true, alias: "target", required: false }] }], rel: [{ type: i0.Input, args: [{ isSignal: true, alias: "rel", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], onClick: [{ type: i0.Output, args: ["onClick"] }], innerRef: [{ type: i0.ViewChild, args: ['button', { isSignal: true }] }] } });
2448
+ args: [{ selector: 'tn-button', standalone: true, imports: [CommonModule, RouterLink, TnTestIdDirective, LabelMarkupPipe, TnIconComponent], template: "<ng-template #content>\n @if (icon() && iconPosition() === 'left') {\n <tn-icon class=\"storybook-button__icon\" size=\"sm\" aria-hidden=\"true\" [name]=\"icon()!\" />\n }\n <span class=\"storybook-button__label\" [innerHTML]=\"label() | tnLabelMarkup\"></span>\n @if (icon() && iconPosition() === 'right') {\n <tn-icon class=\"storybook-button__icon\" size=\"sm\" aria-hidden=\"true\" [name]=\"icon()!\" />\n }\n</ng-template>\n\n@if (isRouterLink()) {\n <a\n #button\n tnTestIdType=\"button\"\n [ngClass]=\"classes()\"\n [ngStyle]=\"{ 'background-color': backgroundColor() }\"\n [routerLink]=\"disabled() ? null : routerLink()\"\n [queryParams]=\"queryParams()\"\n [fragment]=\"fragment()\"\n [target]=\"target()\"\n [rel]=\"rel()\"\n [attr.aria-disabled]=\"disabled() ? 'true' : null\"\n [attr.aria-label]=\"ariaLabel()\"\n [attr.tabindex]=\"disabled() ? -1 : null\"\n [class.is-disabled]=\"disabled()\"\n [tnTestId]=\"testId()\"\n (click)=\"handleAnchorClick($event)\"\n >\n <ng-container [ngTemplateOutlet]=\"content\" />\n </a>\n} @else if (isAnchor()) {\n <a\n #button\n tnTestIdType=\"button\"\n [ngClass]=\"classes()\"\n [ngStyle]=\"{ 'background-color': backgroundColor() }\"\n [attr.href]=\"disabled() ? null : href()\"\n [target]=\"target()\"\n [rel]=\"rel()\"\n [attr.aria-disabled]=\"disabled() ? 'true' : null\"\n [attr.aria-label]=\"ariaLabel()\"\n [attr.tabindex]=\"disabled() ? -1 : null\"\n [class.is-disabled]=\"disabled()\"\n [tnTestId]=\"testId()\"\n (click)=\"handleAnchorClick($event)\"\n >\n <ng-container [ngTemplateOutlet]=\"content\" />\n </a>\n} @else {\n <button\n #button\n tnTestIdType=\"button\"\n [attr.type]=\"type()\"\n [ngClass]=\"classes()\"\n [ngStyle]=\"{ 'background-color': backgroundColor() }\"\n [disabled]=\"disabled()\"\n [attr.aria-label]=\"ariaLabel()\"\n [tnTestId]=\"testId()\"\n (click)=\"onClick.emit($event)\"\n >\n <ng-container [ngTemplateOutlet]=\"content\" />\n </button>\n}\n", styles: [":host{display:inline-block;width:fit-content;justify-self:center}.storybook-button ::ng-deep code{font-family:var(--tn-font-family-monospace, monospace);font-size:.875em;background:var(--tn-bg2, #f5f5f5);border:1px solid var(--tn-lines, #ccc);border-radius:4px;padding:.125em .45em}.storybook-button{display:inline-block;cursor:pointer;border:0;font-weight:500;line-height:1;font-family:IBM Plex Sans,Helvetica Neue,Helvetica,Arial,sans-serif}.storybook-button:disabled,.storybook-button.is-disabled{opacity:.5;cursor:not-allowed;pointer-events:none}.storybook-button--has-icon{display:inline-flex;align-items:center;justify-content:center;gap:8px}.storybook-button__icon{display:inline-flex;align-items:center;justify-content:center;flex-shrink:0}a.storybook-button{text-decoration:none;text-align:center}.button-primary{background-color:var(--tn-primary);color:var(--tn-primary-txt)}.button-default{box-shadow:#00000026 0 0 0 1px inset;background-color:var(--tn-btn-default-bg);color:var(--tn-btn-default-txt)}.button-outline-primary{background-color:transparent;border:1px solid var(--tn-primary);color:var(--tn-primary);transition:background-color .2s ease-in-out,border-color .2s ease-in-out,color .2s ease-in-out}.button-outline-primary:hover{background-color:var(--tn-primary);border:1px solid var(--tn-primary);color:var(--tn-primary-txt)}.button-outline-default{background-color:transparent;border:1px solid var(--tn-lines, #e5e7eb);color:var(--tn-fg1, #000000);transition:background-color .2s ease-in-out,border-color .2s ease-in-out,color .2s ease-in-out}.button-outline-default:hover{background-color:var(--tn-btn-default-bg);border:1px solid var(--tn-btn-default-bg);color:var(--tn-btn-default-txt);box-shadow:#00000026 0 0 0 1px inset}.button-warn{background-color:var(--tn-red);color:#fff}.button-outline-warn{background-color:transparent;border:1px solid var(--tn-red);color:var(--tn-red);transition:background-color .2s ease-in-out,border-color .2s ease-in-out,color .2s ease-in-out}.button-outline-warn:hover{background-color:var(--tn-red);border:1px solid var(--tn-red);color:#fff}.storybook-button--small{padding:10px 16px;font-size:12px}.storybook-button--medium{padding:11px 20px;font-size:14px}.storybook-button--large{padding:12px 24px;font-size:1rem}\n"] }]
2449
+ }], propDecorators: { primary: [{ type: i0.Input, args: [{ isSignal: true, alias: "primary", required: false }] }], color: [{ type: i0.Input, args: [{ isSignal: true, alias: "color", required: false }] }], variant: [{ type: i0.Input, args: [{ isSignal: true, alias: "variant", required: false }] }], backgroundColor: [{ type: i0.Input, args: [{ isSignal: true, alias: "backgroundColor", required: false }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], icon: [{ type: i0.Input, args: [{ isSignal: true, alias: "icon", required: false }] }], iconPosition: [{ type: i0.Input, args: [{ isSignal: true, alias: "iconPosition", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], type: [{ type: i0.Input, args: [{ isSignal: true, alias: "type", required: false }] }], testId: [{ type: i0.Input, args: [{ isSignal: true, alias: "testId", required: false }] }], href: [{ type: i0.Input, args: [{ isSignal: true, alias: "href", required: false }] }], routerLink: [{ type: i0.Input, args: [{ isSignal: true, alias: "routerLink", required: false }] }], queryParams: [{ type: i0.Input, args: [{ isSignal: true, alias: "queryParams", required: false }] }], fragment: [{ type: i0.Input, args: [{ isSignal: true, alias: "fragment", required: false }] }], target: [{ type: i0.Input, args: [{ isSignal: true, alias: "target", required: false }] }], rel: [{ type: i0.Input, args: [{ isSignal: true, alias: "rel", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], onClick: [{ type: i0.Output, args: ["onClick"] }], innerRef: [{ type: i0.ViewChild, args: ['button', { isSignal: true }] }] } });
2435
2450
 
2436
2451
  /**
2437
2452
  * Harness for interacting with tn-button in tests.
@@ -2457,6 +2472,8 @@ class TnButtonHarness extends ComponentHarness {
2457
2472
  */
2458
2473
  static hostSelector = 'tn-button';
2459
2474
  _button = this.locatorFor('button, a');
2475
+ _label = this.locatorForOptional('.storybook-button__label');
2476
+ _icon = this.locatorForOptional('tn-icon');
2460
2477
  /**
2461
2478
  * Gets a `HarnessPredicate` that can be used to search for a button
2462
2479
  * with specific attributes.
@@ -2490,9 +2507,45 @@ class TnButtonHarness extends ComponentHarness {
2490
2507
  * ```
2491
2508
  */
2492
2509
  async getLabel() {
2510
+ // Prefer the dedicated label span so a rendered icon's text content (sprite
2511
+ // fallback glyphs) never leaks into the label. Fall back to the full
2512
+ // element text for resilience if the span is ever absent.
2513
+ const label = await this._label();
2514
+ if (label) {
2515
+ return (await label.text()).trim();
2516
+ }
2493
2517
  const button = await this._button();
2494
2518
  return (await button.text()).trim();
2495
2519
  }
2520
+ /**
2521
+ * Gets the `name` of the icon rendered inside the button, or `null` when the
2522
+ * button has no icon.
2523
+ *
2524
+ * @example
2525
+ * ```typescript
2526
+ * const saveBtn = await loader.getHarness(TnButtonHarness.with({ label: 'Save' }));
2527
+ * expect(await saveBtn.getIconName()).toBe('check');
2528
+ * ```
2529
+ */
2530
+ async getIconName() {
2531
+ const icon = await this._icon();
2532
+ if (!icon) {
2533
+ return null;
2534
+ }
2535
+ return icon.getAttribute('name');
2536
+ }
2537
+ /**
2538
+ * Reports whether the button renders an icon.
2539
+ *
2540
+ * @example
2541
+ * ```typescript
2542
+ * const saveBtn = await loader.getHarness(TnButtonHarness.with({ label: 'Save' }));
2543
+ * expect(await saveBtn.hasIcon()).toBe(true);
2544
+ * ```
2545
+ */
2546
+ async hasIcon() {
2547
+ return (await this._icon()) !== null;
2548
+ }
2496
2549
  /**
2497
2550
  * Checks whether the button is disabled.
2498
2551
  *
@@ -5750,7 +5803,7 @@ class TnCardComponent {
5750
5803
  return type ? `tn-card__status--${type}` : 'tn-card__status--neutral';
5751
5804
  }
5752
5805
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnCardComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
5753
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.0", type: TnCardComponent, isStandalone: true, selector: "tn-card", inputs: { title: { classPropertyName: "title", publicName: "title", isSignal: true, isRequired: false, transformFunction: null }, titleLink: { classPropertyName: "titleLink", publicName: "titleLink", isSignal: true, isRequired: false, transformFunction: null }, elevation: { classPropertyName: "elevation", publicName: "elevation", isSignal: true, isRequired: false, transformFunction: null }, padding: { classPropertyName: "padding", publicName: "padding", isSignal: true, isRequired: false, transformFunction: null }, padContent: { classPropertyName: "padContent", publicName: "padContent", isSignal: true, isRequired: false, transformFunction: null }, bordered: { classPropertyName: "bordered", publicName: "bordered", isSignal: true, isRequired: false, transformFunction: null }, background: { classPropertyName: "background", publicName: "background", isSignal: true, isRequired: false, transformFunction: null }, headerStatus: { classPropertyName: "headerStatus", publicName: "headerStatus", isSignal: true, isRequired: false, transformFunction: null }, headerControl: { classPropertyName: "headerControl", publicName: "headerControl", isSignal: true, isRequired: false, transformFunction: null }, headerMenu: { classPropertyName: "headerMenu", publicName: "headerMenu", isSignal: true, isRequired: false, transformFunction: null }, headerMenuTriggerTestId: { classPropertyName: "headerMenuTriggerTestId", publicName: "headerMenuTriggerTestId", isSignal: true, isRequired: false, transformFunction: null }, primaryAction: { classPropertyName: "primaryAction", publicName: "primaryAction", isSignal: true, isRequired: false, transformFunction: null }, secondaryAction: { classPropertyName: "secondaryAction", publicName: "secondaryAction", isSignal: true, isRequired: false, transformFunction: null }, footerLink: { classPropertyName: "footerLink", publicName: "footerLink", isSignal: true, isRequired: false, transformFunction: null } }, queries: [{ propertyName: "projectedHeader", first: true, predicate: TnCardHeaderDirective, descendants: true, isSignal: true }, { propertyName: "headerActions", first: true, predicate: TnCardHeaderActionsDirective, descendants: true, read: TemplateRef, isSignal: true }, { propertyName: "footerActions", first: true, predicate: TnCardFooterActionsDirective, descendants: true, read: TemplateRef, isSignal: true }], ngImport: i0, template: "<div [ngClass]=\"classes()\">\n <!-- Header section -->\n @if (hasHeader()) {\n <div class=\"tn-card__header\">\n <div class=\"tn-card__header-left\">\n <ng-content select=\"[tnCardHeader]\" />\n @if (!projectedHeader() && title()) {\n <h3\n class=\"tn-card__title\"\n [class.tn-card__title--link]=\"titleLink()\"\n [attr.tabindex]=\"titleLink() ? 0 : null\"\n [attr.role]=\"titleLink() ? 'button' : null\"\n (click)=\"onTitleClick()\"\n (keydown.enter)=\"onTitleClick()\"\n (keydown.space)=\"onTitleClick()\">\n {{ title() }}\n </h3>\n }\n </div>\n\n @if (hasHeaderRight()) {\n <div class=\"tn-card__header-right\">\n <!-- Header Status -->\n @if (headerStatus(); as status) {\n <div\n class=\"tn-card__status\"\n [ngClass]=\"getStatusClass(status?.type)\"\n [tnTestId]=\"status.testId\">\n {{ status.label }}\n </div>\n }\n\n <!-- Header Control (Slide Toggle) -->\n @if (headerControl(); as control) {\n <div class=\"tn-card__control\">\n <tn-slide-toggle\n [label]=\"control.label\"\n [checked]=\"control.checked\"\n [disabled]=\"control.disabled || false\"\n [testId]=\"control.testId\"\n (change)=\"onControlChange($event)\" />\n </div>\n }\n\n <!-- Projected header actions (escape hatch for custom/permission-gated controls) -->\n @if (headerActions(); as actions) {\n <ng-container [ngTemplateOutlet]=\"actions\" />\n }\n\n <!-- Header Menu -->\n @if (headerMenu(); as menu) {\n @if (menu.length) {\n <div class=\"tn-card__menu\">\n <tn-icon-button\n name=\"dots-vertical\"\n library=\"mdi\"\n size=\"md\"\n ariaLabel=\"Card menu\"\n [testId]=\"headerMenuTriggerTestId()\"\n [tnMenuTriggerFor]=\"cardMenu\" />\n <tn-menu\n #cardMenu\n [items]=\"menu\"\n (menuItemClick)=\"onHeaderMenuItemClick($event)\" />\n </div>\n }\n }\n </div>\n }\n </div>\n }\n\n <!-- Content section -->\n <div class=\"tn-card__content\">\n <ng-content />\n </div>\n\n <!-- Footer section -->\n @if (hasFooter()) {\n <div class=\"tn-card__footer\">\n <div class=\"tn-card__footer-left\">\n @if (footerLink(); as link) {\n <button\n type=\"button\"\n class=\"tn-card__footer-link\"\n tnTestIdType=\"link\"\n [tnTestId]=\"link.testId\"\n (click)=\"link.handler()\">\n {{ link.label }}\n </button>\n }\n </div>\n\n <div class=\"tn-card__footer-right\">\n @if (secondaryAction(); as action) {\n <tn-button\n variant=\"outline\"\n color=\"default\"\n [label]=\"action.label\"\n [disabled]=\"action.disabled || false\"\n [testId]=\"action.testId\"\n (click)=\"action.handler()\" />\n }\n\n @if (primaryAction(); as action) {\n <tn-button\n variant=\"filled\"\n color=\"primary\"\n [label]=\"action.label\"\n [disabled]=\"action.disabled || false\"\n [testId]=\"action.testId\"\n (click)=\"action.handler()\" />\n }\n\n <!-- Projected footer actions (escape hatch for custom/permission-gated controls) -->\n @if (footerActions(); as actions) {\n <ng-container [ngTemplateOutlet]=\"actions\" />\n }\n </div>\n </div>\n }\n</div>", styles: [".tn-card{height:100%;display:flex;flex-direction:column;border-radius:8px;transition:box-shadow .3s ease;overflow:hidden}.tn-card--elevation-none{box-shadow:none}.tn-card--elevation-low{box-shadow:0 1px 3px #0000001a}.tn-card--elevation-medium{box-shadow:0 4px 6px #0000001a}.tn-card--elevation-high{box-shadow:0 10px 15px #0000001a}.tn-card--bordered{border:1px solid var(--tn-lines, #e5e7eb)}.tn-card--background{background-color:var(--tn-bg2, #ffffff)}.tn-card--padding-small .tn-card__header{padding:12px 16px}.tn-card--padding-medium .tn-card__header{padding:16px 24px}.tn-card--padding-large .tn-card__header{padding:24px 32px}.tn-card--content-padding-none .tn-card__content{padding:0}.tn-card--content-padding-small .tn-card__content{padding:16px}.tn-card--content-padding-medium .tn-card__content{padding:24px}.tn-card--content-padding-large .tn-card__content{padding:32px}.tn-card__content{flex:1;min-height:0;font-size:14px}.tn-card__header{display:flex;align-items:center;justify-content:space-between;gap:16px;border-bottom:1px solid var(--tn-lines, #e5e7eb)}.tn-card__header-left{flex:1;min-width:0}.tn-card__header-right{display:flex;align-items:center;gap:12px;flex-shrink:0}.tn-card__title{margin:0;font-size:1.125rem;font-weight:600;color:var(--tn-fg1, #1f2937);line-height:1.5}.tn-card__title--link{cursor:pointer;transition:color .2s ease}.tn-card__title--link:hover{color:var(--tn-primary, #2563eb)}.tn-card__status{display:inline-flex;align-items:center;padding:4px 12px;border-radius:12px;font-size:.75rem;font-weight:600;text-transform:uppercase;letter-spacing:.5px}.tn-card__status--success{background-color:#10b9811a;color:var(--tn-success, #10b981)}.tn-card__status--warning{background-color:#f59e0b1a;color:var(--tn-warning, #f59e0b)}.tn-card__status--error{background-color:#ef44441a;color:var(--tn-error, #ef4444)}.tn-card__status--info{background-color:#3b82f61a;color:var(--tn-info, #3b82f6)}.tn-card__status--neutral{background-color:#6b72801a;color:var(--tn-fg2, #6b7280)}.tn-card__control,.tn-card__menu{display:flex;align-items:center}.tn-card__footer{display:flex;align-items:center;justify-content:space-between;gap:16px;border-top:1px solid var(--tn-lines, #e5e7eb);padding:16px 24px}.tn-card--padding-small .tn-card__footer{padding:12px 16px}.tn-card--padding-large .tn-card__footer{padding:24px 32px}.tn-card__footer-left{flex:1;min-width:0}.tn-card__footer-right{display:flex;align-items:center;gap:8px;flex-shrink:0}.tn-card__footer-link{border:none;background:transparent;color:var(--tn-primary, #2563eb);font-size:1rem;font-weight:600;cursor:pointer;padding:0;text-decoration:none;transition:color .2s ease}.tn-card__footer-link:hover{color:var(--tn-primary-dark, #1d4ed8);text-decoration:underline}.tn-card__footer-link:focus{outline:2px solid var(--tn-primary, #2563eb);outline-offset:2px;border-radius:2px}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1$2.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1$2.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: TnButtonComponent, selector: "tn-button", inputs: ["primary", "color", "variant", "backgroundColor", "label", "disabled", "type", "testId", "href", "routerLink", "queryParams", "fragment", "target", "rel", "ariaLabel"], outputs: ["onClick"] }, { kind: "component", type: TnIconButtonComponent, selector: "tn-icon-button", inputs: ["disabled", "dense", "ariaLabel", "ariaExpanded", "testId", "name", "size", "color", "tooltip", "tooltipPosition", "library", "iconClass"], outputs: ["onClick"] }, { kind: "component", type: TnSlideToggleComponent, selector: "tn-slide-toggle", inputs: ["labelPosition", "label", "disabled", "required", "color", "testId", "ariaLabel", "ariaLabelledby", "checked"], outputs: ["change", "toggleChange"] }, { kind: "component", type: TnMenuComponent, selector: "tn-menu", inputs: ["items", "contextMenu", "testId"], outputs: ["menuItemClick", "menuOpen", "menuClose"] }, { kind: "directive", type: TnMenuTriggerDirective, selector: "[tnMenuTriggerFor]", inputs: ["tnMenuTriggerFor", "tnMenuPosition"], exportAs: ["tnMenuTrigger"] }, { kind: "directive", type: TnTestIdDirective, selector: "[tnTestId]", inputs: ["tnTestId", "tnTestIdType"] }] });
5806
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.0", type: TnCardComponent, isStandalone: true, selector: "tn-card", inputs: { title: { classPropertyName: "title", publicName: "title", isSignal: true, isRequired: false, transformFunction: null }, titleLink: { classPropertyName: "titleLink", publicName: "titleLink", isSignal: true, isRequired: false, transformFunction: null }, elevation: { classPropertyName: "elevation", publicName: "elevation", isSignal: true, isRequired: false, transformFunction: null }, padding: { classPropertyName: "padding", publicName: "padding", isSignal: true, isRequired: false, transformFunction: null }, padContent: { classPropertyName: "padContent", publicName: "padContent", isSignal: true, isRequired: false, transformFunction: null }, bordered: { classPropertyName: "bordered", publicName: "bordered", isSignal: true, isRequired: false, transformFunction: null }, background: { classPropertyName: "background", publicName: "background", isSignal: true, isRequired: false, transformFunction: null }, headerStatus: { classPropertyName: "headerStatus", publicName: "headerStatus", isSignal: true, isRequired: false, transformFunction: null }, headerControl: { classPropertyName: "headerControl", publicName: "headerControl", isSignal: true, isRequired: false, transformFunction: null }, headerMenu: { classPropertyName: "headerMenu", publicName: "headerMenu", isSignal: true, isRequired: false, transformFunction: null }, headerMenuTriggerTestId: { classPropertyName: "headerMenuTriggerTestId", publicName: "headerMenuTriggerTestId", isSignal: true, isRequired: false, transformFunction: null }, primaryAction: { classPropertyName: "primaryAction", publicName: "primaryAction", isSignal: true, isRequired: false, transformFunction: null }, secondaryAction: { classPropertyName: "secondaryAction", publicName: "secondaryAction", isSignal: true, isRequired: false, transformFunction: null }, footerLink: { classPropertyName: "footerLink", publicName: "footerLink", isSignal: true, isRequired: false, transformFunction: null } }, queries: [{ propertyName: "projectedHeader", first: true, predicate: TnCardHeaderDirective, descendants: true, isSignal: true }, { propertyName: "headerActions", first: true, predicate: TnCardHeaderActionsDirective, descendants: true, read: TemplateRef, isSignal: true }, { propertyName: "footerActions", first: true, predicate: TnCardFooterActionsDirective, descendants: true, read: TemplateRef, isSignal: true }], ngImport: i0, template: "<div [ngClass]=\"classes()\">\n <!-- Header section -->\n @if (hasHeader()) {\n <div class=\"tn-card__header\">\n <div class=\"tn-card__header-left\">\n <ng-content select=\"[tnCardHeader]\" />\n @if (!projectedHeader() && title()) {\n <h3\n class=\"tn-card__title\"\n [class.tn-card__title--link]=\"titleLink()\"\n [attr.tabindex]=\"titleLink() ? 0 : null\"\n [attr.role]=\"titleLink() ? 'button' : null\"\n (click)=\"onTitleClick()\"\n (keydown.enter)=\"onTitleClick()\"\n (keydown.space)=\"onTitleClick()\">\n {{ title() }}\n </h3>\n }\n </div>\n\n @if (hasHeaderRight()) {\n <div class=\"tn-card__header-right\">\n <!-- Header Status -->\n @if (headerStatus(); as status) {\n <div\n class=\"tn-card__status\"\n [ngClass]=\"getStatusClass(status?.type)\"\n [tnTestId]=\"status.testId\">\n {{ status.label }}\n </div>\n }\n\n <!-- Header Control (Slide Toggle) -->\n @if (headerControl(); as control) {\n <div class=\"tn-card__control\">\n <tn-slide-toggle\n [label]=\"control.label\"\n [checked]=\"control.checked\"\n [disabled]=\"control.disabled || false\"\n [testId]=\"control.testId\"\n (change)=\"onControlChange($event)\" />\n </div>\n }\n\n <!-- Projected header actions (escape hatch for custom/permission-gated controls) -->\n @if (headerActions(); as actions) {\n <ng-container [ngTemplateOutlet]=\"actions\" />\n }\n\n <!-- Header Menu -->\n @if (headerMenu(); as menu) {\n @if (menu.length) {\n <div class=\"tn-card__menu\">\n <tn-icon-button\n name=\"dots-vertical\"\n library=\"mdi\"\n size=\"md\"\n ariaLabel=\"Card menu\"\n [testId]=\"headerMenuTriggerTestId()\"\n [tnMenuTriggerFor]=\"cardMenu\" />\n <tn-menu\n #cardMenu\n [items]=\"menu\"\n (menuItemClick)=\"onHeaderMenuItemClick($event)\" />\n </div>\n }\n }\n </div>\n }\n </div>\n }\n\n <!-- Content section -->\n <div class=\"tn-card__content\">\n <ng-content />\n </div>\n\n <!-- Footer section -->\n @if (hasFooter()) {\n <div class=\"tn-card__footer\">\n <div class=\"tn-card__footer-left\">\n @if (footerLink(); as link) {\n <button\n type=\"button\"\n class=\"tn-card__footer-link\"\n tnTestIdType=\"link\"\n [tnTestId]=\"link.testId\"\n (click)=\"link.handler()\">\n {{ link.label }}\n </button>\n }\n </div>\n\n <div class=\"tn-card__footer-right\">\n @if (secondaryAction(); as action) {\n <tn-button\n variant=\"outline\"\n color=\"default\"\n [label]=\"action.label\"\n [disabled]=\"action.disabled || false\"\n [testId]=\"action.testId\"\n (click)=\"action.handler()\" />\n }\n\n @if (primaryAction(); as action) {\n <tn-button\n variant=\"filled\"\n color=\"primary\"\n [label]=\"action.label\"\n [disabled]=\"action.disabled || false\"\n [testId]=\"action.testId\"\n (click)=\"action.handler()\" />\n }\n\n <!-- Projected footer actions (escape hatch for custom/permission-gated controls) -->\n @if (footerActions(); as actions) {\n <ng-container [ngTemplateOutlet]=\"actions\" />\n }\n </div>\n </div>\n }\n</div>", styles: [".tn-card{height:100%;display:flex;flex-direction:column;border-radius:8px;transition:box-shadow .3s ease;overflow:hidden}.tn-card--elevation-none{box-shadow:none}.tn-card--elevation-low{box-shadow:0 1px 3px #0000001a}.tn-card--elevation-medium{box-shadow:0 4px 6px #0000001a}.tn-card--elevation-high{box-shadow:0 10px 15px #0000001a}.tn-card--bordered{border:1px solid var(--tn-lines, #e5e7eb)}.tn-card--background{background-color:var(--tn-bg2, #ffffff)}.tn-card--padding-small .tn-card__header{padding:12px 16px}.tn-card--padding-medium .tn-card__header{padding:16px 24px}.tn-card--padding-large .tn-card__header{padding:24px 32px}.tn-card--content-padding-none .tn-card__content{padding:0}.tn-card--content-padding-small .tn-card__content{padding:16px}.tn-card--content-padding-medium .tn-card__content{padding:24px}.tn-card--content-padding-large .tn-card__content{padding:32px}.tn-card__content{flex:1;min-height:0;font-size:14px}.tn-card__header{display:flex;align-items:center;justify-content:space-between;gap:16px;border-bottom:1px solid var(--tn-lines, #e5e7eb)}.tn-card__header-left{flex:1;min-width:0}.tn-card__header-right{display:flex;align-items:center;gap:12px;flex-shrink:0}.tn-card__title{margin:0;font-size:1.125rem;font-weight:600;color:var(--tn-fg1, #1f2937);line-height:1.5}.tn-card__title--link{cursor:pointer;transition:color .2s ease}.tn-card__title--link:hover{color:var(--tn-primary, #2563eb)}.tn-card__status{display:inline-flex;align-items:center;padding:4px 12px;border-radius:12px;font-size:.75rem;font-weight:600;text-transform:uppercase;letter-spacing:.5px}.tn-card__status--success{background-color:#10b9811a;color:var(--tn-success, #10b981)}.tn-card__status--warning{background-color:#f59e0b1a;color:var(--tn-warning, #f59e0b)}.tn-card__status--error{background-color:#ef44441a;color:var(--tn-error, #ef4444)}.tn-card__status--info{background-color:#3b82f61a;color:var(--tn-info, #3b82f6)}.tn-card__status--neutral{background-color:#6b72801a;color:var(--tn-fg2, #6b7280)}.tn-card__control,.tn-card__menu{display:flex;align-items:center}.tn-card__footer{display:flex;align-items:center;justify-content:space-between;gap:16px;border-top:1px solid var(--tn-lines, #e5e7eb);padding:16px 24px}.tn-card--padding-small .tn-card__footer{padding:12px 16px}.tn-card--padding-large .tn-card__footer{padding:24px 32px}.tn-card__footer-left{flex:1;min-width:0}.tn-card__footer-right{display:flex;align-items:center;gap:8px;flex-shrink:0}.tn-card__footer-link{border:none;background:transparent;color:var(--tn-primary, #2563eb);font-size:1rem;font-weight:600;cursor:pointer;padding:0;text-decoration:none;transition:color .2s ease}.tn-card__footer-link:hover{color:var(--tn-primary-dark, #1d4ed8);text-decoration:underline}.tn-card__footer-link:focus{outline:2px solid var(--tn-primary, #2563eb);outline-offset:2px;border-radius:2px}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1$2.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1$2.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: TnButtonComponent, selector: "tn-button", inputs: ["primary", "color", "variant", "backgroundColor", "label", "icon", "iconPosition", "disabled", "type", "testId", "href", "routerLink", "queryParams", "fragment", "target", "rel", "ariaLabel"], outputs: ["onClick"] }, { kind: "component", type: TnIconButtonComponent, selector: "tn-icon-button", inputs: ["disabled", "dense", "ariaLabel", "ariaExpanded", "testId", "name", "size", "color", "tooltip", "tooltipPosition", "library", "iconClass"], outputs: ["onClick"] }, { kind: "component", type: TnSlideToggleComponent, selector: "tn-slide-toggle", inputs: ["labelPosition", "label", "disabled", "required", "color", "testId", "ariaLabel", "ariaLabelledby", "checked"], outputs: ["change", "toggleChange"] }, { kind: "component", type: TnMenuComponent, selector: "tn-menu", inputs: ["items", "contextMenu", "testId"], outputs: ["menuItemClick", "menuOpen", "menuClose"] }, { kind: "directive", type: TnMenuTriggerDirective, selector: "[tnMenuTriggerFor]", inputs: ["tnMenuTriggerFor", "tnMenuPosition"], exportAs: ["tnMenuTrigger"] }, { kind: "directive", type: TnTestIdDirective, selector: "[tnTestId]", inputs: ["tnTestId", "tnTestIdType"] }] });
5754
5807
  }
5755
5808
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnCardComponent, decorators: [{
5756
5809
  type: Component,
@@ -9793,7 +9846,7 @@ class TnEmptyComponent {
9793
9846
  return this.size() === 'compact' ? 'lg' : 'xl';
9794
9847
  }, ...(ngDevMode ? [{ debugName: "iconSizePreset" }] : []));
9795
9848
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnEmptyComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
9796
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.0", type: TnEmptyComponent, isStandalone: true, selector: "tn-empty", inputs: { title: { classPropertyName: "title", publicName: "title", isSignal: true, isRequired: true, transformFunction: null }, description: { classPropertyName: "description", publicName: "description", isSignal: true, isRequired: false, transformFunction: null }, icon: { classPropertyName: "icon", publicName: "icon", isSignal: true, isRequired: false, transformFunction: null }, iconLibrary: { classPropertyName: "iconLibrary", publicName: "iconLibrary", isSignal: true, isRequired: false, transformFunction: null }, iconSize: { classPropertyName: "iconSize", publicName: "iconSize", isSignal: true, isRequired: false, transformFunction: null }, actionText: { classPropertyName: "actionText", publicName: "actionText", isSignal: true, isRequired: false, transformFunction: null }, bordered: { classPropertyName: "bordered", publicName: "bordered", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { onAction: "onAction" }, host: { attributes: { "role": "status" }, properties: { "class.tn-empty--compact": "size() === \"compact\"", "class.tn-empty--bordered": "bordered()" }, classAttribute: "tn-empty" }, ngImport: i0, template: "@if (icon()) {\n <div class=\"tn-empty__icon\">\n <tn-icon\n aria-hidden=\"true\"\n [name]=\"icon()!\"\n [library]=\"iconLibrary()\"\n [size]=\"iconSizePreset()\"\n [customSize]=\"iconSize()\"\n />\n </div>\n}\n\n<div class=\"tn-empty__title\">\n {{ title() }}\n</div>\n\n@if (description()) {\n <div class=\"tn-empty__description\">\n {{ description() }}\n </div>\n}\n\n@if (hasAction()) {\n <div class=\"tn-empty__action\">\n <tn-button\n color=\"primary\"\n variant=\"outline\"\n [label]=\"actionText()!\"\n (onClick)=\"onAction.emit()\"\n />\n </div>\n}\n", styles: [":host{display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;gap:8px;padding:48px 24px;border-radius:8px}:host(.tn-empty--bordered){border:1px solid var(--tn-lines)}:host(.tn-empty--compact){padding:24px 16px}:host(.tn-empty--compact) .tn-empty__title{font-size:1rem}:host(.tn-empty--compact) .tn-empty__description{font-size:.8125rem}:host(.tn-empty--compact) .tn-empty__icon{margin-bottom:4px}:host(.tn-empty--compact) .tn-empty__action{margin-top:4px}.tn-empty__icon{color:var(--tn-fg2, #6b7280);margin-bottom:8px}.tn-empty__title{font-size:1.125rem;font-weight:600;color:var(--tn-fg1, #e5e7eb);line-height:1.4}.tn-empty__description{font-size:1rem;color:var(--tn-fg2, #6b7280);line-height:1.5;max-width:420px}.tn-empty__action{margin-top:8px}\n"], dependencies: [{ kind: "component", type: TnIconComponent, selector: "tn-icon", inputs: ["name", "size", "color", "tooltip", "ariaLabel", "library", "testId", "fullSize", "customSize"] }, { kind: "component", type: TnButtonComponent, selector: "tn-button", inputs: ["primary", "color", "variant", "backgroundColor", "label", "disabled", "type", "testId", "href", "routerLink", "queryParams", "fragment", "target", "rel", "ariaLabel"], outputs: ["onClick"] }] });
9849
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.0", type: TnEmptyComponent, isStandalone: true, selector: "tn-empty", inputs: { title: { classPropertyName: "title", publicName: "title", isSignal: true, isRequired: true, transformFunction: null }, description: { classPropertyName: "description", publicName: "description", isSignal: true, isRequired: false, transformFunction: null }, icon: { classPropertyName: "icon", publicName: "icon", isSignal: true, isRequired: false, transformFunction: null }, iconLibrary: { classPropertyName: "iconLibrary", publicName: "iconLibrary", isSignal: true, isRequired: false, transformFunction: null }, iconSize: { classPropertyName: "iconSize", publicName: "iconSize", isSignal: true, isRequired: false, transformFunction: null }, actionText: { classPropertyName: "actionText", publicName: "actionText", isSignal: true, isRequired: false, transformFunction: null }, bordered: { classPropertyName: "bordered", publicName: "bordered", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { onAction: "onAction" }, host: { attributes: { "role": "status" }, properties: { "class.tn-empty--compact": "size() === \"compact\"", "class.tn-empty--bordered": "bordered()" }, classAttribute: "tn-empty" }, ngImport: i0, template: "@if (icon()) {\n <div class=\"tn-empty__icon\">\n <tn-icon\n aria-hidden=\"true\"\n [name]=\"icon()!\"\n [library]=\"iconLibrary()\"\n [size]=\"iconSizePreset()\"\n [customSize]=\"iconSize()\"\n />\n </div>\n}\n\n<div class=\"tn-empty__title\">\n {{ title() }}\n</div>\n\n@if (description()) {\n <div class=\"tn-empty__description\">\n {{ description() }}\n </div>\n}\n\n@if (hasAction()) {\n <div class=\"tn-empty__action\">\n <tn-button\n color=\"primary\"\n variant=\"outline\"\n [label]=\"actionText()!\"\n (onClick)=\"onAction.emit()\"\n />\n </div>\n}\n", styles: [":host{display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;gap:8px;padding:48px 24px;border-radius:8px}:host(.tn-empty--bordered){border:1px solid var(--tn-lines)}:host(.tn-empty--compact){padding:24px 16px}:host(.tn-empty--compact) .tn-empty__title{font-size:1rem}:host(.tn-empty--compact) .tn-empty__description{font-size:.8125rem}:host(.tn-empty--compact) .tn-empty__icon{margin-bottom:4px}:host(.tn-empty--compact) .tn-empty__action{margin-top:4px}.tn-empty__icon{color:var(--tn-fg2, #6b7280);margin-bottom:8px}.tn-empty__title{font-size:1.125rem;font-weight:600;color:var(--tn-fg1, #e5e7eb);line-height:1.4}.tn-empty__description{font-size:1rem;color:var(--tn-fg2, #6b7280);line-height:1.5;max-width:420px}.tn-empty__action{margin-top:8px}\n"], dependencies: [{ kind: "component", type: TnIconComponent, selector: "tn-icon", inputs: ["name", "size", "color", "tooltip", "ariaLabel", "library", "testId", "fullSize", "customSize"] }, { kind: "component", type: TnButtonComponent, selector: "tn-button", inputs: ["primary", "color", "variant", "backgroundColor", "label", "icon", "iconPosition", "disabled", "type", "testId", "href", "routerLink", "queryParams", "fragment", "target", "rel", "ariaLabel"], outputs: ["onClick"] }] });
9797
9850
  }
9798
9851
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnEmptyComponent, decorators: [{
9799
9852
  type: Component,
@@ -13563,11 +13616,33 @@ class TnSliderThumbDirective {
13563
13616
  slider; // Will be set by parent slider component
13564
13617
  onTouched = () => { };
13565
13618
  onChangeCallback = (_value) => { };
13619
+ // Pointer is held down on the thumb (set on mousedown/touchstart). Drives the
13620
+ // global move/up listeners.
13621
+ isPointerDown = false;
13622
+ // The pointer has actually moved since going down — i.e. a genuine drag. Only
13623
+ // set once movement occurs (onGlobalMouseMove/onGlobalTouchMove), never on the
13624
+ // initial press, so a plain click still commits through onInput. See onInput.
13566
13625
  isDragging = false;
13626
+ // Last value written by the form. Retained so the parent slider can pick it up
13627
+ // once it links the thumb (ngAfterContentInit) — writeValue can run before the
13628
+ // slider sets `this.slider`.
13629
+ currentValue = 0;
13630
+ // Whether the form has actually written a value to this thumb. Lets the parent
13631
+ // slider distinguish "form wrote 0" from "thumb never bound" so it doesn't
13632
+ // clobber its own form value with this default. See getValue()/hasFormValue().
13633
+ hasWrittenValue = false;
13567
13634
  elementRef = inject((ElementRef));
13635
+ // An accessible name set directly on the <input tnSliderThumb> (e.g.
13636
+ // aria-label="Volume"). Captured up front so the host's [attr.aria-label]
13637
+ // binding can fall back to it instead of clobbering it when the parent slider
13638
+ // doesn't supply one. See ariaLabel()/ariaLabelledby().
13639
+ fallbackAriaLabel = null;
13640
+ fallbackAriaLabelledby = null;
13568
13641
  ngOnInit() {
13569
13642
  // Make the native input visually hidden but still accessible
13570
13643
  const input = this.elementRef.nativeElement;
13644
+ this.fallbackAriaLabel = input.getAttribute('aria-label');
13645
+ this.fallbackAriaLabelledby = input.getAttribute('aria-labelledby');
13571
13646
  input.style.opacity = '0';
13572
13647
  input.style.position = 'absolute';
13573
13648
  input.style.width = '100%';
@@ -13580,10 +13655,46 @@ class TnSliderThumbDirective {
13580
13655
  ngOnDestroy() {
13581
13656
  this.cleanup();
13582
13657
  }
13658
+ /** Value last written by the form, for the slider to read once linked. */
13659
+ getValue() {
13660
+ return this.currentValue;
13661
+ }
13662
+ /**
13663
+ * Whether the form has written a value to this thumb. The slider checks this
13664
+ * before adopting getValue() so an unbound thumb's default 0 never overwrites
13665
+ * a value bound directly on the slider.
13666
+ */
13667
+ hasFormValue() {
13668
+ return this.hasWrittenValue;
13669
+ }
13583
13670
  // ControlValueAccessor implementation
13584
13671
  writeValue(value) {
13585
- if (this.elementRef.nativeElement) {
13586
- this.elementRef.nativeElement.value = value?.toString() || '0';
13672
+ const nextValue = value ?? 0;
13673
+ this.hasWrittenValue = true;
13674
+ // Propagate to the parent slider so its value signal (and thus the thumb
13675
+ // position / track fill) reflects the form value, not just the native input.
13676
+ // When linked, mirror the slider's clamped/stepped result so currentValue and
13677
+ // the native input agree with it; otherwise retain the raw value for the
13678
+ // slider to adopt (and clamp) once it links the thumb in ngAfterContentInit.
13679
+ if (this.slider) {
13680
+ this.slider.updateValue(nextValue);
13681
+ // Note: we don't emit the clamped result back to the form control, so an
13682
+ // out-of-range write (e.g. setValue(143) on a 0–100 slider) leaves the model
13683
+ // holding 143 while the slider renders 100. This mirrors native
13684
+ // <input type="range">, which also doesn't reconcile the bound value.
13685
+ this.currentValue = this.slider.value();
13686
+ // When linked, the host `[value]="slider?.value()"` binding writes the
13687
+ // native input on the next change-detection pass, so a manual write here
13688
+ // would only be re-overwritten — leave it to the binding to keep one source
13689
+ // of truth.
13690
+ }
13691
+ else {
13692
+ this.currentValue = nextValue;
13693
+ // Unlinked: no host binding value yet (slider?.value() is undefined), so set
13694
+ // the native input directly until the slider links and takes over.
13695
+ if (this.elementRef.nativeElement) {
13696
+ this.elementRef.nativeElement.value = this.currentValue.toString();
13697
+ }
13587
13698
  }
13588
13699
  }
13589
13700
  registerOnChange(fn) {
@@ -13599,21 +13710,45 @@ class TnSliderThumbDirective {
13599
13710
  }
13600
13711
  }
13601
13712
  onInput(event) {
13713
+ // Once a drag is underway, the global pointer handlers
13714
+ // (updateValueFromPosition) own value commits; the native range also fires
13715
+ // input here, so skip it to avoid a redundant double emit. A plain click sets
13716
+ // isPointerDown but not isDragging (no movement), so its native input commits
13717
+ // here; keyboard changes set neither and also flow through.
13718
+ if (this.isDragging) {
13719
+ return;
13720
+ }
13602
13721
  const input = event.target;
13603
13722
  const value = parseFloat(input.value);
13723
+ // An empty/garbage value parses to NaN, which clampValue can't sanitize
13724
+ // (Math.max/min with NaN stay NaN). Native range inputs shouldn't produce
13725
+ // this, but guard so a NaN never reaches the form or slider value.
13726
+ if (!Number.isFinite(value)) {
13727
+ return;
13728
+ }
13604
13729
  if (this.slider) {
13605
13730
  this.slider.updateValue(value);
13606
13731
  }
13607
13732
  this.onChangeCallback(value);
13608
13733
  }
13609
13734
  onChange(_event) {
13735
+ this.notifyTouched();
13736
+ }
13737
+ /**
13738
+ * Marks the bound control touched. Calls the thumb's own `onTouched` (for a
13739
+ * thumb-bound control) and forwards to the linked slider (for a
13740
+ * slider-host-bound control) — the thumb is the only interactive element, so
13741
+ * the slider relies on it to ever become touched.
13742
+ */
13743
+ notifyTouched() {
13610
13744
  this.onTouched();
13745
+ this.slider?.markTouched();
13611
13746
  }
13612
13747
  onMouseDown(event) {
13613
13748
  if (this.disabled()) {
13614
13749
  return;
13615
13750
  }
13616
- this.isDragging = true;
13751
+ this.isPointerDown = true;
13617
13752
  this.addGlobalListeners();
13618
13753
  event.stopPropagation(); // Prevent track click
13619
13754
  }
@@ -13621,7 +13756,7 @@ class TnSliderThumbDirective {
13621
13756
  if (this.disabled()) {
13622
13757
  return;
13623
13758
  }
13624
- this.isDragging = true;
13759
+ this.isPointerDown = true;
13625
13760
  this.addGlobalListeners();
13626
13761
  event.stopPropagation(); // Prevent track click
13627
13762
  }
@@ -13638,31 +13773,35 @@ class TnSliderThumbDirective {
13638
13773
  document.removeEventListener('touchend', this.onGlobalTouchEnd);
13639
13774
  }
13640
13775
  onGlobalMouseMove = (event) => {
13641
- if (!this.isDragging || this.disabled()) {
13776
+ if (!this.isPointerDown || this.disabled()) {
13642
13777
  return;
13643
13778
  }
13779
+ this.isDragging = true;
13644
13780
  event.preventDefault();
13645
13781
  this.updateValueFromPosition(event.clientX);
13646
13782
  };
13647
13783
  onGlobalMouseUp = () => {
13648
- if (this.isDragging) {
13784
+ if (this.isPointerDown) {
13785
+ this.isPointerDown = false;
13649
13786
  this.isDragging = false;
13650
- this.onTouched();
13787
+ this.notifyTouched();
13651
13788
  this.removeGlobalListeners();
13652
13789
  }
13653
13790
  };
13654
13791
  onGlobalTouchMove = (event) => {
13655
- if (!this.isDragging || this.disabled()) {
13792
+ if (!this.isPointerDown || this.disabled()) {
13656
13793
  return;
13657
13794
  }
13795
+ this.isDragging = true;
13658
13796
  event.preventDefault();
13659
13797
  const touch = event.touches[0];
13660
13798
  this.updateValueFromPosition(touch.clientX);
13661
13799
  };
13662
13800
  onGlobalTouchEnd = () => {
13663
- if (this.isDragging) {
13801
+ if (this.isPointerDown) {
13802
+ this.isPointerDown = false;
13664
13803
  this.isDragging = false;
13665
- this.onTouched();
13804
+ this.notifyTouched();
13666
13805
  this.removeGlobalListeners();
13667
13806
  }
13668
13807
  };
@@ -13676,12 +13815,58 @@ class TnSliderThumbDirective {
13676
13815
  const maxVal = this.slider.max();
13677
13816
  const newValue = minVal + (percentage * (maxVal - minVal));
13678
13817
  this.slider.updateValue(newValue);
13818
+ // updateValue only moves the visual thumb; commit the (clamped/stepped) value
13819
+ // to the form and native input so dragging actually emits changes — otherwise
13820
+ // only click (native input event) updates the model.
13821
+ this.commit(this.slider.value());
13822
+ }
13823
+ /**
13824
+ * Resolve the accessible name for the range input: the parent slider's
13825
+ * `aria-label`/`aria-labelledby` input when set, otherwise a value placed
13826
+ * directly on the `<input tnSliderThumb>`. Returning the fallback keeps the
13827
+ * host binding from wiping a directly-set label. Null removes the attribute.
13828
+ */
13829
+ ariaLabel() {
13830
+ return this.slider?.ariaLabel() ?? this.fallbackAriaLabel;
13831
+ }
13832
+ ariaLabelledby() {
13833
+ return this.slider?.ariaLabelledby() ?? this.fallbackAriaLabelledby;
13834
+ }
13835
+ /**
13836
+ * Commit a value that originated outside the native input (a thumb drag).
13837
+ * Syncs `currentValue`, the native input, and emits to the form. The slider's
13838
+ * own `onChange` only reaches a slider-bound control, so a thumb-bound control
13839
+ * relies on this to stay in sync. Expects an already clamped/stepped value
13840
+ * (slider.value()).
13841
+ */
13842
+ commit(value) {
13843
+ this.currentValue = value;
13844
+ if (this.elementRef.nativeElement) {
13845
+ this.elementRef.nativeElement.value = value.toString();
13846
+ }
13847
+ this.onChangeCallback(value);
13848
+ }
13849
+ /**
13850
+ * Builds an aria-valuetext when the slider has a label prefix/suffix so screen
13851
+ * readers announce "50 km/h" rather than the bare number. Returns null when
13852
+ * neither is set, letting the native range's valuenow announcement stand.
13853
+ */
13854
+ ariaValueText() {
13855
+ if (!this.slider) {
13856
+ return null;
13857
+ }
13858
+ const prefix = this.slider.labelPrefix();
13859
+ const suffix = this.slider.labelSuffix();
13860
+ if (!prefix && !suffix) {
13861
+ return null;
13862
+ }
13863
+ return `${prefix}${this.slider.value()}${suffix}`;
13679
13864
  }
13680
13865
  cleanup() {
13681
13866
  this.removeGlobalListeners();
13682
13867
  }
13683
13868
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnSliderThumbDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
13684
- static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.1.0", type: TnSliderThumbDirective, isStandalone: true, selector: "input[tnSliderThumb]", host: { attributes: { "type": "range" }, listeners: { "input": "onInput($event)", "change": "onChange($event)", "blur": "onTouched()", "mousedown": "onMouseDown($event)", "touchstart": "onTouchStart($event)" }, properties: { "disabled": "slider?.isDisabled()", "attr.min": "slider?.min()", "attr.max": "slider?.max()", "attr.step": "slider?.step()", "value": "slider?.value()" }, classAttribute: "tn-slider-thumb" }, providers: [
13869
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.1.0", type: TnSliderThumbDirective, isStandalone: true, selector: "input[tnSliderThumb]", host: { attributes: { "type": "range" }, listeners: { "input": "onInput($event)", "change": "onChange($event)", "blur": "notifyTouched()", "mousedown": "onMouseDown($event)", "touchstart": "onTouchStart($event)" }, properties: { "disabled": "slider?.isDisabled()", "attr.min": "slider?.min()", "attr.max": "slider?.max()", "attr.step": "slider?.step()", "value": "slider?.value()", "attr.aria-valuetext": "ariaValueText()", "attr.aria-label": "ariaLabel()", "attr.aria-labelledby": "ariaLabelledby()" }, classAttribute: "tn-slider-thumb" }, providers: [
13685
13870
  {
13686
13871
  provide: NG_VALUE_ACCESSOR,
13687
13872
  useExisting: forwardRef(() => TnSliderThumbDirective),
@@ -13709,15 +13894,28 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
13709
13894
  '[attr.max]': 'slider?.max()',
13710
13895
  '[attr.step]': 'slider?.step()',
13711
13896
  '[value]': 'slider?.value()',
13897
+ '[attr.aria-valuetext]': 'ariaValueText()',
13898
+ '[attr.aria-label]': 'ariaLabel()',
13899
+ '[attr.aria-labelledby]': 'ariaLabelledby()',
13712
13900
  '(input)': 'onInput($event)',
13713
13901
  '(change)': 'onChange($event)',
13714
- '(blur)': 'onTouched()',
13902
+ '(blur)': 'notifyTouched()',
13715
13903
  '(mousedown)': 'onMouseDown($event)',
13716
13904
  '(touchstart)': 'onTouchStart($event)'
13717
13905
  }
13718
13906
  }]
13719
13907
  }] });
13720
13908
 
13909
+ /**
13910
+ * Range slider with an optional value label.
13911
+ *
13912
+ * Form binding: both this component and the inner `input[tnSliderThumb]`
13913
+ * directive are `NG_VALUE_ACCESSOR` providers, so a `formControl`/`ngModel` can
13914
+ * be attached to either element. Bind to the `tn-slider` host for the simplest
13915
+ * usage; binding to the inner thumb input also works and the slider adopts that
13916
+ * value on init (see {@link ngAfterContentInit}). Avoid binding to both at once —
13917
+ * pick one element per control to keep a single source of truth.
13918
+ */
13721
13919
  class TnSliderComponent {
13722
13920
  min = input(0, ...(ngDevMode ? [{ debugName: "min" }] : []));
13723
13921
  max = input(100, ...(ngDevMode ? [{ debugName: "max" }] : []));
@@ -13731,9 +13929,18 @@ class TnSliderComponent {
13731
13929
  * is configured via `TN_TEST_ATTR` (default `data-testid`).
13732
13930
  */
13733
13931
  testId = input(undefined, ...(ngDevMode ? [{ debugName: "testId" }] : []));
13932
+ /**
13933
+ * Accessible name forwarded to the inner range input — the focusable element
13934
+ * screen readers actually announce. Set this (or `aria-labelledby`) when the
13935
+ * slider isn't already labelled by a `tn-form-field`/`<label>`, otherwise a
13936
+ * standalone `<tn-slider><input tnSliderThumb></tn-slider>` announces only
13937
+ * "slider". A label set directly on the `input[tnSliderThumb]` is used as a
13938
+ * fallback when neither is provided here.
13939
+ */
13940
+ ariaLabel = input(undefined, { ...(ngDevMode ? { debugName: "ariaLabel" } : {}), alias: 'aria-label' });
13941
+ ariaLabelledby = input(undefined, { ...(ngDevMode ? { debugName: "ariaLabelledby" } : {}), alias: 'aria-labelledby' });
13734
13942
  thumbDirective = contentChild.required(TnSliderThumbDirective);
13735
13943
  sliderContainer = viewChild.required('sliderContainer');
13736
- thumbVisual = viewChild.required('thumbVisual');
13737
13944
  onChange = (_value) => { };
13738
13945
  onTouched = () => { };
13739
13946
  value = signal(0, ...(ngDevMode ? [{ debugName: "value" }] : []));
@@ -13754,13 +13961,6 @@ class TnSliderComponent {
13754
13961
  fillScale = computed(() => {
13755
13962
  return this.fillPercentage() / 100;
13756
13963
  }, ...(ngDevMode ? [{ debugName: "fillScale" }] : []));
13757
- // Computed position for thumb (in pixels from left)
13758
- thumbPosition = computed(() => {
13759
- const containerWidth = this.sliderContainer()?.nativeElement?.offsetWidth || 0;
13760
- const percentage = this.fillPercentage();
13761
- // Center the thumb (20px width, so -10px offset)
13762
- return (containerWidth * percentage / 100) - 10;
13763
- }, ...(ngDevMode ? [{ debugName: "thumbPosition" }] : []));
13764
13964
  // Public signals for label management
13765
13965
  showLabel = this._showLabel.asReadonly();
13766
13966
  labelVisible = this._labelVisible.asReadonly();
@@ -13782,13 +13982,24 @@ class TnSliderComponent {
13782
13982
  }
13783
13983
  });
13784
13984
  }
13785
- ngAfterViewInit() {
13786
- // Initialize thumb directive if present
13985
+ ngAfterContentInit() {
13986
+ // Link the projected thumb directive. Done in AfterContentInit (not
13987
+ // AfterViewInit) so the link exists before the thumb's host bindings settle,
13988
+ // avoiding a null→value flip on its [disabled]/[min]/[value] bindings.
13787
13989
  const thumbDirective = this.thumbDirective();
13788
13990
  if (thumbDirective) {
13789
13991
  thumbDirective.slider = this;
13992
+ // When the form is bound to the inner thumb input, its initial writeValue()
13993
+ // may run before this link exists, so adopt the value the thumb received —
13994
+ // otherwise the slider keeps its default 0 and the thumb/fill render at the
13995
+ // wrong position. Only adopt when the thumb was actually written to, so a
13996
+ // value bound directly on the slider isn't clobbered by the thumb's default.
13997
+ if (thumbDirective.hasFormValue()) {
13998
+ this.value.set(this.clampValue(thumbDirective.getValue()));
13999
+ }
13790
14000
  }
13791
- this.updateThumbPosition();
14001
+ }
14002
+ ngAfterViewInit() {
13792
14003
  // Set up handle interaction listeners if labelType is handle or both
13793
14004
  const currentLabelType = this.labelType();
13794
14005
  if ((currentLabelType === 'handle' || currentLabelType === 'both') && this._showLabel()) {
@@ -13810,6 +14021,15 @@ class TnSliderComponent {
13810
14021
  registerOnTouched(fn) {
13811
14022
  this.onTouched = fn;
13812
14023
  }
14024
+ /**
14025
+ * Marks a slider-host-bound control as touched. The inner thumb is the only
14026
+ * interactive element, so it forwards its touch events here (on blur / pointer
14027
+ * release) — otherwise a control bound to the `tn-slider` host would never
14028
+ * transition to touched and touched-gated validation would never show.
14029
+ */
14030
+ markTouched() {
14031
+ this.onTouched();
14032
+ }
13813
14033
  setDisabledState(isDisabled) {
13814
14034
  this.formDisabled.set(isDisabled);
13815
14035
  }
@@ -13818,7 +14038,6 @@ class TnSliderComponent {
13818
14038
  const clampedValue = this.clampValue(newValue);
13819
14039
  this.value.set(clampedValue);
13820
14040
  this.onChange(clampedValue);
13821
- this.updateThumbPosition();
13822
14041
  }
13823
14042
  enableLabel() {
13824
14043
  this._showLabel.set(true);
@@ -13832,24 +14051,6 @@ class TnSliderComponent {
13832
14051
  getSliderRect() {
13833
14052
  return this.sliderContainer().nativeElement.getBoundingClientRect();
13834
14053
  }
13835
- onTrackClick(event) {
13836
- if (this.isDisabled()) {
13837
- return;
13838
- }
13839
- event.preventDefault();
13840
- const rect = this.getSliderRect();
13841
- const clientX = event instanceof MouseEvent ? event.clientX : event.touches[0].clientX;
13842
- const percentage = (clientX - rect.left) / rect.width;
13843
- const minVal = this.min();
13844
- const maxVal = this.max();
13845
- const newValue = minVal + (percentage * (maxVal - minVal));
13846
- this.updateValue(newValue);
13847
- this.onTouched();
13848
- }
13849
- updateThumbPosition() {
13850
- // Thumb position is now handled by computed signal and template binding
13851
- // No manual DOM manipulation needed
13852
- }
13853
14054
  clampValue(value) {
13854
14055
  const minVal = this.min();
13855
14056
  const maxVal = this.max();
@@ -13907,13 +14108,13 @@ class TnSliderComponent {
13907
14108
  }
13908
14109
  };
13909
14110
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnSliderComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
13910
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.0", type: TnSliderComponent, isStandalone: true, selector: "tn-slider", inputs: { min: { classPropertyName: "min", publicName: "min", isSignal: true, isRequired: false, transformFunction: null }, max: { classPropertyName: "max", publicName: "max", isSignal: true, isRequired: false, transformFunction: null }, step: { classPropertyName: "step", publicName: "step", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, labelPrefix: { classPropertyName: "labelPrefix", publicName: "labelPrefix", isSignal: true, isRequired: false, transformFunction: null }, labelSuffix: { classPropertyName: "labelSuffix", publicName: "labelSuffix", isSignal: true, isRequired: false, transformFunction: null }, labelType: { classPropertyName: "labelType", publicName: "labelType", isSignal: true, isRequired: false, transformFunction: null }, testId: { classPropertyName: "testId", publicName: "testId", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "attr.aria-disabled": "isDisabled()" }, classAttribute: "tn-slider" }, providers: [
14111
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.0", type: TnSliderComponent, isStandalone: true, selector: "tn-slider", inputs: { min: { classPropertyName: "min", publicName: "min", isSignal: true, isRequired: false, transformFunction: null }, max: { classPropertyName: "max", publicName: "max", isSignal: true, isRequired: false, transformFunction: null }, step: { classPropertyName: "step", publicName: "step", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, labelPrefix: { classPropertyName: "labelPrefix", publicName: "labelPrefix", isSignal: true, isRequired: false, transformFunction: null }, labelSuffix: { classPropertyName: "labelSuffix", publicName: "labelSuffix", isSignal: true, isRequired: false, transformFunction: null }, labelType: { classPropertyName: "labelType", publicName: "labelType", isSignal: true, isRequired: false, transformFunction: null }, testId: { classPropertyName: "testId", publicName: "testId", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "aria-label", isSignal: true, isRequired: false, transformFunction: null }, ariaLabelledby: { classPropertyName: "ariaLabelledby", publicName: "aria-labelledby", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "attr.aria-disabled": "isDisabled()" }, classAttribute: "tn-slider" }, providers: [
13911
14112
  {
13912
14113
  provide: NG_VALUE_ACCESSOR,
13913
14114
  useExisting: forwardRef(() => TnSliderComponent),
13914
14115
  multi: true
13915
14116
  }
13916
- ], queries: [{ propertyName: "thumbDirective", first: true, predicate: TnSliderThumbDirective, descendants: true, isSignal: true }], viewQueries: [{ propertyName: "sliderContainer", first: true, predicate: ["sliderContainer"], descendants: true, isSignal: true }, { propertyName: "thumbVisual", first: true, predicate: ["thumbVisual"], descendants: true, isSignal: true }], ngImport: i0, template: "<div\n #sliderContainer\n class=\"tn-slider-container\"\n tnTestIdType=\"slider\"\n [tnTestId]=\"testId()\"\n [attr.aria-disabled]=\"isDisabled()\"\n [attr.data-disabled]=\"isDisabled()\"\n (mousedown)=\"onTrackClick($event)\"\n (touchstart)=\"onTrackClick($event)\">\n\n <div class=\"tn-slider-track\">\n <div class=\"tn-slider-track-inactive\"></div>\n <div class=\"tn-slider-track-active\">\n <div\n class=\"tn-slider-track-active-fill\"\n [style.transform]=\"'scaleX(' + fillScale() + ')'\">\n </div>\n </div>\n @if ((labelType() === 'track' || labelType() === 'both') && showLabel()) {\n <div class=\"tn-slider-track-label\">\n {{ labelPrefix() }}{{ value() }}{{ labelSuffix() }}\n </div>\n }\n </div>\n\n <div\n #thumbVisual\n class=\"tn-slider-thumb-visual\"\n [style.transform]=\"'translateX(' + thumbPosition() + 'px)'\">\n <div class=\"tn-slider-thumb-knob\"></div>\n @if ((labelType() === 'handle' || labelType() === 'both') && showLabel()) {\n <div\n class=\"tn-slider-thumb-label\"\n [class.visible]=\"labelVisible()\">\n {{ labelPrefix() }}{{ value() }}{{ labelSuffix() }}\n </div>\n }\n </div>\n\n <ng-content />\n</div>\n", styles: [":host{display:block;width:100%;height:48px;position:relative;touch-action:pan-x}.tn-slider-container{position:relative;width:100%;height:100%;display:flex;align-items:center;cursor:pointer}.tn-slider-container[data-disabled=true]{cursor:not-allowed;opacity:.6}.tn-slider-track{position:relative;width:100%;height:4px}.tn-slider-track-inactive{position:absolute;top:0;left:0;width:100%;height:100%;background:var(--tn-lines, #e0e0e0);border-radius:2px}.tn-slider-track-active{position:absolute;top:0;left:0;width:100%;height:100%;overflow:hidden;border-radius:2px}.tn-slider-track-active-fill{position:absolute;top:0;left:0;width:100%;height:100%;background:var(--tn-primary, #007bff);border-radius:2px;transform-origin:left center;transition:transform .1s ease-out}:host ::ng-deep .tn-slider-thumb{position:absolute;top:0;left:0;width:100%;height:100%;opacity:0;margin:0;padding:0;cursor:pointer;z-index:2;-webkit-appearance:none;appearance:none;background:none;border:none;outline:none}:host ::ng-deep .tn-slider-thumb:disabled{cursor:not-allowed}:host ::ng-deep .tn-slider-thumb:focus{outline:none}:host ::ng-deep .tn-slider-thumb:focus-visible{outline:none}:host ::ng-deep .tn-slider-thumb:focus-visible+.tn-slider-visual-thumb{outline:2px solid var(--tn-primary, #007bff);outline-offset:2px}.tn-slider-thumb-visual{position:absolute;top:50%;left:0;pointer-events:none;z-index:3;transition:transform .1s ease-out}.tn-slider-thumb-knob{width:20px;height:24px;background:var(--tn-primary, #007bff);border:2px solid var(--tn-bg1, white);border-radius:4px;box-shadow:0 2px 4px #0003;transition:box-shadow .15s ease;transform:translateY(-50%)}.tn-slider-container:hover .tn-slider-thumb-knob{box-shadow:0 4px 8px #0000004d}.tn-slider-container[data-disabled=true] .tn-slider-thumb-knob{background:var(--tn-fg2, #999);box-shadow:0 1px 2px #0000001a}.tn-slider-thumb-label{position:absolute;bottom:calc(100% + 16px);left:50%;transform:translate(-50%) translateY(-50%);padding:8px 12px;background:var(--tn-primary, #007bff);color:var(--tn-primary-txt, white);border-radius:6px;font-size:12px;font-weight:500;line-height:1.2;white-space:nowrap;opacity:0;pointer-events:none;-webkit-user-select:none;user-select:none;transition:opacity .15s ease;z-index:4;box-shadow:0 2px 8px #00000026}.tn-slider-thumb-label:after{content:\"\";position:absolute;top:100%;left:50%;transform:translate(-50%);width:0;height:0;border-left:8px solid transparent;border-right:8px solid transparent;border-top:8px solid var(--tn-primary, #007bff)}.tn-slider-thumb-label.visible{opacity:1}.tn-slider-track-label{position:absolute;top:-28px;right:0;padding:4px 0;background:transparent;color:var(--tn-fg1, #000);font-size:12px;font-weight:500;line-height:1.2;white-space:nowrap;-webkit-user-select:none;user-select:none;z-index:2}@media(prefers-reduced-motion:reduce){.tn-slider-track-active-fill,.tn-slider-thumb-visual,.tn-slider-thumb-knob,.tn-slider-thumb-label{transition:none}}@media(prefers-contrast:high){.tn-slider-track-inactive{border:1px solid var(--tn-fg1, #000)}.tn-slider-thumb-knob{border-width:3px;border-color:var(--tn-fg1, #000)}}\n"], dependencies: [{ kind: "ngmodule", type: A11yModule }, { kind: "directive", type: TnTestIdDirective, selector: "[tnTestId]", inputs: ["tnTestId", "tnTestIdType"] }] });
14117
+ ], queries: [{ propertyName: "thumbDirective", first: true, predicate: TnSliderThumbDirective, descendants: true, isSignal: true }], viewQueries: [{ propertyName: "sliderContainer", first: true, predicate: ["sliderContainer"], descendants: true, isSignal: true }], ngImport: i0, template: "<!--\n Pointer interaction (click-to-position and press-drag, anywhere on the track)\n is handled by the projected native range input, which overlays the whole\n container (see TnSliderThumbDirective): a click jumps the thumb and fires\n `input` \u2192 onInput; a press-and-move runs the directive's global drag handlers.\n No container-level click handler is needed \u2014 and one wouldn't fire anyway, as\n the overlay calls stopPropagation() on its own pointer events.\n-->\n<div\n #sliderContainer\n class=\"tn-slider-container\"\n tnTestIdType=\"slider\"\n [tnTestId]=\"testId()\"\n [attr.aria-disabled]=\"isDisabled()\"\n [attr.data-disabled]=\"isDisabled()\">\n\n <div class=\"tn-slider-track\">\n <div class=\"tn-slider-track-inactive\"></div>\n <div class=\"tn-slider-track-active\">\n <div\n class=\"tn-slider-track-active-fill\"\n [style.transform]=\"'scaleX(' + fillScale() + ')'\">\n </div>\n </div>\n @if ((labelType() === 'track' || labelType() === 'both') && showLabel()) {\n <div class=\"tn-slider-track-label\">\n {{ labelPrefix() }}{{ value() }}{{ labelSuffix() }}\n </div>\n }\n </div>\n\n <div\n class=\"tn-slider-thumb-visual\"\n [style.left.%]=\"fillPercentage()\">\n <div class=\"tn-slider-thumb-knob\"></div>\n @if ((labelType() === 'handle' || labelType() === 'both') && showLabel()) {\n <div\n class=\"tn-slider-thumb-label\"\n [class.visible]=\"labelVisible()\">\n {{ labelPrefix() }}{{ value() }}{{ labelSuffix() }}\n </div>\n }\n </div>\n\n <ng-content />\n</div>\n", styles: [":host{display:block;width:100%;height:48px;position:relative;touch-action:pan-x}.tn-slider-container{position:relative;width:100%;height:100%;display:flex;align-items:center;cursor:pointer}.tn-slider-container[data-disabled=true]{cursor:not-allowed;opacity:.6}.tn-slider-track{position:relative;width:100%;height:4px}.tn-slider-track-inactive{position:absolute;top:0;left:0;width:100%;height:100%;background:var(--tn-lines, #e0e0e0);border-radius:2px}.tn-slider-track-active{position:absolute;top:0;left:0;width:100%;height:100%;overflow:hidden;border-radius:2px}.tn-slider-track-active-fill{position:absolute;top:0;left:0;width:100%;height:100%;background:var(--tn-primary, #007bff);border-radius:2px;transform-origin:left center;transition:transform .1s ease-out}:host ::ng-deep .tn-slider-thumb{position:absolute;top:0;left:0;width:100%;height:100%;opacity:0;margin:0;padding:0;cursor:pointer;z-index:2;-webkit-appearance:none;appearance:none;background:none;border:none;outline:none}:host ::ng-deep .tn-slider-thumb:disabled{cursor:not-allowed}:host ::ng-deep .tn-slider-thumb:focus{outline:none}:host ::ng-deep .tn-slider-thumb:focus-visible{outline:none}:host ::ng-deep .tn-slider-container:has(.tn-slider-thumb:focus-visible) .tn-slider-thumb-knob{outline:2px solid var(--tn-primary, #007bff);outline-offset:2px}.tn-slider-thumb-visual{position:absolute;top:50%;left:0;margin-left:-10px;pointer-events:none;z-index:3;transition:left .1s ease-out}.tn-slider-thumb-knob{width:20px;height:24px;background:var(--tn-primary, #007bff);border:2px solid var(--tn-bg1, white);border-radius:4px;box-shadow:0 2px 4px #0003;transition:box-shadow .15s ease;transform:translateY(-50%)}.tn-slider-container:hover .tn-slider-thumb-knob{box-shadow:0 4px 8px #0000004d}.tn-slider-container[data-disabled=true] .tn-slider-thumb-knob{background:var(--tn-fg2, #999);box-shadow:0 1px 2px #0000001a}.tn-slider-thumb-label{position:absolute;bottom:calc(100% + 16px);left:50%;transform:translate(-50%) translateY(-50%);padding:8px 12px;background:var(--tn-primary, #007bff);color:var(--tn-primary-txt, white);border-radius:6px;font-size:12px;font-weight:500;line-height:1.2;white-space:nowrap;opacity:0;pointer-events:none;-webkit-user-select:none;user-select:none;transition:opacity .15s ease;z-index:4;box-shadow:0 2px 8px #00000026}.tn-slider-thumb-label:after{content:\"\";position:absolute;top:100%;left:50%;transform:translate(-50%);width:0;height:0;border-left:8px solid transparent;border-right:8px solid transparent;border-top:8px solid var(--tn-primary, #007bff)}.tn-slider-thumb-label.visible{opacity:1}.tn-slider-track-label{position:absolute;top:-28px;right:0;padding:4px 0;background:transparent;color:var(--tn-fg1, #000);font-size:12px;font-weight:500;line-height:1.2;white-space:nowrap;-webkit-user-select:none;user-select:none;z-index:2}@media(prefers-reduced-motion:reduce){.tn-slider-track-active-fill,.tn-slider-thumb-visual,.tn-slider-thumb-knob,.tn-slider-thumb-label{transition:none}}@media(prefers-contrast:high){.tn-slider-track-inactive{border:1px solid var(--tn-fg1, #000)}.tn-slider-thumb-knob{border-width:3px;border-color:var(--tn-fg1, #000)}}\n"], dependencies: [{ kind: "ngmodule", type: A11yModule }, { kind: "directive", type: TnTestIdDirective, selector: "[tnTestId]", inputs: ["tnTestId", "tnTestIdType"] }] });
13917
14118
  }
13918
14119
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnSliderComponent, decorators: [{
13919
14120
  type: Component,
@@ -13926,8 +14127,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
13926
14127
  ], host: {
13927
14128
  'class': 'tn-slider',
13928
14129
  '[attr.aria-disabled]': 'isDisabled()'
13929
- }, template: "<div\n #sliderContainer\n class=\"tn-slider-container\"\n tnTestIdType=\"slider\"\n [tnTestId]=\"testId()\"\n [attr.aria-disabled]=\"isDisabled()\"\n [attr.data-disabled]=\"isDisabled()\"\n (mousedown)=\"onTrackClick($event)\"\n (touchstart)=\"onTrackClick($event)\">\n\n <div class=\"tn-slider-track\">\n <div class=\"tn-slider-track-inactive\"></div>\n <div class=\"tn-slider-track-active\">\n <div\n class=\"tn-slider-track-active-fill\"\n [style.transform]=\"'scaleX(' + fillScale() + ')'\">\n </div>\n </div>\n @if ((labelType() === 'track' || labelType() === 'both') && showLabel()) {\n <div class=\"tn-slider-track-label\">\n {{ labelPrefix() }}{{ value() }}{{ labelSuffix() }}\n </div>\n }\n </div>\n\n <div\n #thumbVisual\n class=\"tn-slider-thumb-visual\"\n [style.transform]=\"'translateX(' + thumbPosition() + 'px)'\">\n <div class=\"tn-slider-thumb-knob\"></div>\n @if ((labelType() === 'handle' || labelType() === 'both') && showLabel()) {\n <div\n class=\"tn-slider-thumb-label\"\n [class.visible]=\"labelVisible()\">\n {{ labelPrefix() }}{{ value() }}{{ labelSuffix() }}\n </div>\n }\n </div>\n\n <ng-content />\n</div>\n", styles: [":host{display:block;width:100%;height:48px;position:relative;touch-action:pan-x}.tn-slider-container{position:relative;width:100%;height:100%;display:flex;align-items:center;cursor:pointer}.tn-slider-container[data-disabled=true]{cursor:not-allowed;opacity:.6}.tn-slider-track{position:relative;width:100%;height:4px}.tn-slider-track-inactive{position:absolute;top:0;left:0;width:100%;height:100%;background:var(--tn-lines, #e0e0e0);border-radius:2px}.tn-slider-track-active{position:absolute;top:0;left:0;width:100%;height:100%;overflow:hidden;border-radius:2px}.tn-slider-track-active-fill{position:absolute;top:0;left:0;width:100%;height:100%;background:var(--tn-primary, #007bff);border-radius:2px;transform-origin:left center;transition:transform .1s ease-out}:host ::ng-deep .tn-slider-thumb{position:absolute;top:0;left:0;width:100%;height:100%;opacity:0;margin:0;padding:0;cursor:pointer;z-index:2;-webkit-appearance:none;appearance:none;background:none;border:none;outline:none}:host ::ng-deep .tn-slider-thumb:disabled{cursor:not-allowed}:host ::ng-deep .tn-slider-thumb:focus{outline:none}:host ::ng-deep .tn-slider-thumb:focus-visible{outline:none}:host ::ng-deep .tn-slider-thumb:focus-visible+.tn-slider-visual-thumb{outline:2px solid var(--tn-primary, #007bff);outline-offset:2px}.tn-slider-thumb-visual{position:absolute;top:50%;left:0;pointer-events:none;z-index:3;transition:transform .1s ease-out}.tn-slider-thumb-knob{width:20px;height:24px;background:var(--tn-primary, #007bff);border:2px solid var(--tn-bg1, white);border-radius:4px;box-shadow:0 2px 4px #0003;transition:box-shadow .15s ease;transform:translateY(-50%)}.tn-slider-container:hover .tn-slider-thumb-knob{box-shadow:0 4px 8px #0000004d}.tn-slider-container[data-disabled=true] .tn-slider-thumb-knob{background:var(--tn-fg2, #999);box-shadow:0 1px 2px #0000001a}.tn-slider-thumb-label{position:absolute;bottom:calc(100% + 16px);left:50%;transform:translate(-50%) translateY(-50%);padding:8px 12px;background:var(--tn-primary, #007bff);color:var(--tn-primary-txt, white);border-radius:6px;font-size:12px;font-weight:500;line-height:1.2;white-space:nowrap;opacity:0;pointer-events:none;-webkit-user-select:none;user-select:none;transition:opacity .15s ease;z-index:4;box-shadow:0 2px 8px #00000026}.tn-slider-thumb-label:after{content:\"\";position:absolute;top:100%;left:50%;transform:translate(-50%);width:0;height:0;border-left:8px solid transparent;border-right:8px solid transparent;border-top:8px solid var(--tn-primary, #007bff)}.tn-slider-thumb-label.visible{opacity:1}.tn-slider-track-label{position:absolute;top:-28px;right:0;padding:4px 0;background:transparent;color:var(--tn-fg1, #000);font-size:12px;font-weight:500;line-height:1.2;white-space:nowrap;-webkit-user-select:none;user-select:none;z-index:2}@media(prefers-reduced-motion:reduce){.tn-slider-track-active-fill,.tn-slider-thumb-visual,.tn-slider-thumb-knob,.tn-slider-thumb-label{transition:none}}@media(prefers-contrast:high){.tn-slider-track-inactive{border:1px solid var(--tn-fg1, #000)}.tn-slider-thumb-knob{border-width:3px;border-color:var(--tn-fg1, #000)}}\n"] }]
13930
- }], ctorParameters: () => [], propDecorators: { min: [{ type: i0.Input, args: [{ isSignal: true, alias: "min", required: false }] }], max: [{ type: i0.Input, args: [{ isSignal: true, alias: "max", required: false }] }], step: [{ type: i0.Input, args: [{ isSignal: true, alias: "step", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], labelPrefix: [{ type: i0.Input, args: [{ isSignal: true, alias: "labelPrefix", required: false }] }], labelSuffix: [{ type: i0.Input, args: [{ isSignal: true, alias: "labelSuffix", required: false }] }], labelType: [{ type: i0.Input, args: [{ isSignal: true, alias: "labelType", required: false }] }], testId: [{ type: i0.Input, args: [{ isSignal: true, alias: "testId", required: false }] }], thumbDirective: [{ type: i0.ContentChild, args: [i0.forwardRef(() => TnSliderThumbDirective), { isSignal: true }] }], sliderContainer: [{ type: i0.ViewChild, args: ['sliderContainer', { isSignal: true }] }], thumbVisual: [{ type: i0.ViewChild, args: ['thumbVisual', { isSignal: true }] }] } });
14130
+ }, template: "<!--\n Pointer interaction (click-to-position and press-drag, anywhere on the track)\n is handled by the projected native range input, which overlays the whole\n container (see TnSliderThumbDirective): a click jumps the thumb and fires\n `input` \u2192 onInput; a press-and-move runs the directive's global drag handlers.\n No container-level click handler is needed \u2014 and one wouldn't fire anyway, as\n the overlay calls stopPropagation() on its own pointer events.\n-->\n<div\n #sliderContainer\n class=\"tn-slider-container\"\n tnTestIdType=\"slider\"\n [tnTestId]=\"testId()\"\n [attr.aria-disabled]=\"isDisabled()\"\n [attr.data-disabled]=\"isDisabled()\">\n\n <div class=\"tn-slider-track\">\n <div class=\"tn-slider-track-inactive\"></div>\n <div class=\"tn-slider-track-active\">\n <div\n class=\"tn-slider-track-active-fill\"\n [style.transform]=\"'scaleX(' + fillScale() + ')'\">\n </div>\n </div>\n @if ((labelType() === 'track' || labelType() === 'both') && showLabel()) {\n <div class=\"tn-slider-track-label\">\n {{ labelPrefix() }}{{ value() }}{{ labelSuffix() }}\n </div>\n }\n </div>\n\n <div\n class=\"tn-slider-thumb-visual\"\n [style.left.%]=\"fillPercentage()\">\n <div class=\"tn-slider-thumb-knob\"></div>\n @if ((labelType() === 'handle' || labelType() === 'both') && showLabel()) {\n <div\n class=\"tn-slider-thumb-label\"\n [class.visible]=\"labelVisible()\">\n {{ labelPrefix() }}{{ value() }}{{ labelSuffix() }}\n </div>\n }\n </div>\n\n <ng-content />\n</div>\n", styles: [":host{display:block;width:100%;height:48px;position:relative;touch-action:pan-x}.tn-slider-container{position:relative;width:100%;height:100%;display:flex;align-items:center;cursor:pointer}.tn-slider-container[data-disabled=true]{cursor:not-allowed;opacity:.6}.tn-slider-track{position:relative;width:100%;height:4px}.tn-slider-track-inactive{position:absolute;top:0;left:0;width:100%;height:100%;background:var(--tn-lines, #e0e0e0);border-radius:2px}.tn-slider-track-active{position:absolute;top:0;left:0;width:100%;height:100%;overflow:hidden;border-radius:2px}.tn-slider-track-active-fill{position:absolute;top:0;left:0;width:100%;height:100%;background:var(--tn-primary, #007bff);border-radius:2px;transform-origin:left center;transition:transform .1s ease-out}:host ::ng-deep .tn-slider-thumb{position:absolute;top:0;left:0;width:100%;height:100%;opacity:0;margin:0;padding:0;cursor:pointer;z-index:2;-webkit-appearance:none;appearance:none;background:none;border:none;outline:none}:host ::ng-deep .tn-slider-thumb:disabled{cursor:not-allowed}:host ::ng-deep .tn-slider-thumb:focus{outline:none}:host ::ng-deep .tn-slider-thumb:focus-visible{outline:none}:host ::ng-deep .tn-slider-container:has(.tn-slider-thumb:focus-visible) .tn-slider-thumb-knob{outline:2px solid var(--tn-primary, #007bff);outline-offset:2px}.tn-slider-thumb-visual{position:absolute;top:50%;left:0;margin-left:-10px;pointer-events:none;z-index:3;transition:left .1s ease-out}.tn-slider-thumb-knob{width:20px;height:24px;background:var(--tn-primary, #007bff);border:2px solid var(--tn-bg1, white);border-radius:4px;box-shadow:0 2px 4px #0003;transition:box-shadow .15s ease;transform:translateY(-50%)}.tn-slider-container:hover .tn-slider-thumb-knob{box-shadow:0 4px 8px #0000004d}.tn-slider-container[data-disabled=true] .tn-slider-thumb-knob{background:var(--tn-fg2, #999);box-shadow:0 1px 2px #0000001a}.tn-slider-thumb-label{position:absolute;bottom:calc(100% + 16px);left:50%;transform:translate(-50%) translateY(-50%);padding:8px 12px;background:var(--tn-primary, #007bff);color:var(--tn-primary-txt, white);border-radius:6px;font-size:12px;font-weight:500;line-height:1.2;white-space:nowrap;opacity:0;pointer-events:none;-webkit-user-select:none;user-select:none;transition:opacity .15s ease;z-index:4;box-shadow:0 2px 8px #00000026}.tn-slider-thumb-label:after{content:\"\";position:absolute;top:100%;left:50%;transform:translate(-50%);width:0;height:0;border-left:8px solid transparent;border-right:8px solid transparent;border-top:8px solid var(--tn-primary, #007bff)}.tn-slider-thumb-label.visible{opacity:1}.tn-slider-track-label{position:absolute;top:-28px;right:0;padding:4px 0;background:transparent;color:var(--tn-fg1, #000);font-size:12px;font-weight:500;line-height:1.2;white-space:nowrap;-webkit-user-select:none;user-select:none;z-index:2}@media(prefers-reduced-motion:reduce){.tn-slider-track-active-fill,.tn-slider-thumb-visual,.tn-slider-thumb-knob,.tn-slider-thumb-label{transition:none}}@media(prefers-contrast:high){.tn-slider-track-inactive{border:1px solid var(--tn-fg1, #000)}.tn-slider-thumb-knob{border-width:3px;border-color:var(--tn-fg1, #000)}}\n"] }]
14131
+ }], ctorParameters: () => [], propDecorators: { min: [{ type: i0.Input, args: [{ isSignal: true, alias: "min", required: false }] }], max: [{ type: i0.Input, args: [{ isSignal: true, alias: "max", required: false }] }], step: [{ type: i0.Input, args: [{ isSignal: true, alias: "step", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], labelPrefix: [{ type: i0.Input, args: [{ isSignal: true, alias: "labelPrefix", required: false }] }], labelSuffix: [{ type: i0.Input, args: [{ isSignal: true, alias: "labelSuffix", required: false }] }], labelType: [{ type: i0.Input, args: [{ isSignal: true, alias: "labelType", required: false }] }], testId: [{ type: i0.Input, args: [{ isSignal: true, alias: "testId", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "aria-label", required: false }] }], ariaLabelledby: [{ type: i0.Input, args: [{ isSignal: true, alias: "aria-labelledby", required: false }] }], thumbDirective: [{ type: i0.ContentChild, args: [i0.forwardRef(() => TnSliderThumbDirective), { isSignal: true }] }], sliderContainer: [{ type: i0.ViewChild, args: ['sliderContainer', { isSignal: true }] }] } });
13931
14132
 
13932
14133
  class TnSliderWithLabelDirective {
13933
14134
  enabled = input(true, { ...(ngDevMode ? { debugName: "enabled" } : {}), alias: 'tnSliderWithLabel' });
@@ -14606,7 +14807,7 @@ class TnConfirmDialogComponent {
14606
14807
  ref = inject((DialogRef));
14607
14808
  data = inject(DIALOG_DATA);
14608
14809
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnConfirmDialogComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
14609
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.1.0", type: TnConfirmDialogComponent, isStandalone: true, selector: "tn-confirm-dialog", host: { properties: { "class.tn-dialog--destructive": "data.destructive" }, classAttribute: "tn-dialog-shell" }, ngImport: i0, 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 [testId]=\"data.cancelTestId || 'cancel'\"\n (click)=\"ref.close(false)\" />\n <tn-button\n type=\"button\"\n [color]=\"data.destructive ? 'warn' : 'primary'\"\n [label]=\"data.confirmText || 'OK'\"\n [testId]=\"data.confirmTestId || 'confirm'\"\n (click)=\"ref.close(true)\" />\n </div>\n</tn-dialog-shell>\n", dependencies: [{ kind: "component", type: TnDialogShellComponent, selector: "tn-dialog-shell", inputs: ["title", "showFullscreenButton", "showCloseButton", "hideContent", "hideActions", "testId"] }, { kind: "component", type: TnButtonComponent, selector: "tn-button", inputs: ["primary", "color", "variant", "backgroundColor", "label", "disabled", "type", "testId", "href", "routerLink", "queryParams", "fragment", "target", "rel", "ariaLabel"], outputs: ["onClick"] }] });
14810
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.1.0", type: TnConfirmDialogComponent, isStandalone: true, selector: "tn-confirm-dialog", host: { properties: { "class.tn-dialog--destructive": "data.destructive" }, classAttribute: "tn-dialog-shell" }, ngImport: i0, 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 [testId]=\"data.cancelTestId || 'cancel'\"\n (click)=\"ref.close(false)\" />\n <tn-button\n type=\"button\"\n [color]=\"data.destructive ? 'warn' : 'primary'\"\n [label]=\"data.confirmText || 'OK'\"\n [testId]=\"data.confirmTestId || 'confirm'\"\n (click)=\"ref.close(true)\" />\n </div>\n</tn-dialog-shell>\n", dependencies: [{ kind: "component", type: TnDialogShellComponent, selector: "tn-dialog-shell", inputs: ["title", "showFullscreenButton", "showCloseButton", "hideContent", "hideActions", "testId"] }, { kind: "component", type: TnButtonComponent, selector: "tn-button", inputs: ["primary", "color", "variant", "backgroundColor", "label", "icon", "iconPosition", "disabled", "type", "testId", "href", "routerLink", "queryParams", "fragment", "target", "rel", "ariaLabel"], outputs: ["onClick"] }] });
14610
14811
  }
14611
14812
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnConfirmDialogComponent, decorators: [{
14612
14813
  type: Component,
@@ -15615,7 +15816,7 @@ class TnFilePickerPopupComponent {
15615
15816
  return `${(date.getMonth() + 1).toString().padStart(2, '0')}/${date.getDate().toString().padStart(2, '0')}/${date.getFullYear()} ${timePart}`;
15616
15817
  }
15617
15818
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnFilePickerPopupComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
15618
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.0", type: TnFilePickerPopupComponent, isStandalone: true, selector: "tn-file-picker-popup", inputs: { mode: { classPropertyName: "mode", publicName: "mode", isSignal: true, isRequired: false, transformFunction: null }, multiSelect: { classPropertyName: "multiSelect", publicName: "multiSelect", isSignal: true, isRequired: false, transformFunction: null }, allowCreate: { classPropertyName: "allowCreate", publicName: "allowCreate", isSignal: true, isRequired: false, transformFunction: null }, allowDatasetCreate: { classPropertyName: "allowDatasetCreate", publicName: "allowDatasetCreate", isSignal: true, isRequired: false, transformFunction: null }, allowZvolCreate: { classPropertyName: "allowZvolCreate", publicName: "allowZvolCreate", isSignal: true, isRequired: false, transformFunction: null }, currentPath: { classPropertyName: "currentPath", publicName: "currentPath", isSignal: true, isRequired: false, transformFunction: null }, fileItems: { classPropertyName: "fileItems", publicName: "fileItems", isSignal: true, isRequired: false, transformFunction: null }, selectedItems: { classPropertyName: "selectedItems", publicName: "selectedItems", isSignal: true, isRequired: false, transformFunction: null }, loading: { classPropertyName: "loading", publicName: "loading", isSignal: true, isRequired: false, transformFunction: null }, creationLoading: { classPropertyName: "creationLoading", publicName: "creationLoading", isSignal: true, isRequired: false, transformFunction: null }, fileExtensions: { classPropertyName: "fileExtensions", publicName: "fileExtensions", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { itemClick: "itemClick", itemDoubleClick: "itemDoubleClick", pathNavigate: "pathNavigate", createFolder: "createFolder", clearSelection: "clearSelection", close: "close", submit: "submit", cancel: "cancel", submitFolderName: "submitFolderName", cancelFolderCreation: "cancelFolderCreation" }, host: { classAttribute: "tn-file-picker-popup" }, ngImport: i0, template: "<!-- Header with breadcrumb navigation -->\n<div class=\"tn-file-picker-header\">\n <nav class=\"tn-file-picker-breadcrumb\" aria-label=\"File path\">\n @for (segment of currentPath() | tnTruncatePath; track $index; let last = $last) {\n <button\n class=\"breadcrumb-segment\"\n [class.current]=\"last\"\n [class.parent-nav]=\"segment.name === '..'\"\n [disabled]=\"last\"\n (click)=\"navigateToPath(segment.path)\">\n {{ segment.name }}\n </button>\n }\n </nav>\n\n <div class=\"tn-file-picker-actions\">\n @if (allowCreate()) {\n <tn-button\n variant=\"outline\"\n label=\"New Folder\"\n [disabled]=\"isCreateDisabled()\"\n (onClick)=\"onCreateFolder()\" />\n }\n </div>\n</div>\n\n<!-- Loading indicator -->\n@if (loading()) {\n <div class=\"tn-file-picker-loading\">\n <tn-icon name=\"loading\" library=\"mdi\" />\n <span>Loading...</span>\n </div>\n}\n\n<!-- File table -->\n@if (!loading()) {\n <div class=\"tn-file-picker-content\">\n <tn-table\n [dataSource]=\"filteredFileItems()\"\n [displayedColumns]=\"multiSelect() ? displayedColumns : displayedColumns.slice(1)\">\n\n <!-- Selection column -->\n @if (multiSelect()) {\n <ng-container tnColumnDef=\"select\">\n <ng-template tnHeaderCellDef>\n <!-- Select all checkbox -->\n </ng-template>\n <ng-template let-item tnCellDef>\n <input \n type=\"checkbox\" \n [checked]=\"isSelected(item)\"\n [disabled]=\"!!item.disabled\"\n (click)=\"$event.stopPropagation()\"\n (change)=\"onItemClick(item)\">\n </ng-template>\n </ng-container>\n }\n\n <!-- Name column -->\n <ng-container tnColumnDef=\"name\">\n <ng-template tnHeaderCellDef>Name</ng-template>\n <ng-template let-item tnCellDef>\n\n <!-- NORMAL MODE: Display name -->\n @if (!item.isCreating) {\n <div\n class=\"file-name-cell\"\n [class.disabled]=\"!!item.disabled\"\n [class.zfs-object]=\"isZfsObject(item)\"\n [attr.tabindex]=\"item.disabled ? null : 0\"\n [attr.role]=\"'button'\"\n (click)=\"onItemClick(item)\"\n (dblclick)=\"onItemDoubleClick(item)\"\n (keydown.enter)=\"onItemDoubleClick(item)\"\n (keydown.space)=\"onItemClick(item)\">\n <tn-icon\n [name]=\"getItemIcon(item)\"\n [library]=\"getItemIconLibrary(item)\"\n [class]=\"'file-icon file-icon-' + item.type\" />\n <span class=\"file-name\">{{ item.name }}</span>\n\n <!-- ZFS badge -->\n @if (isZfsObject(item)) {\n <span\n [class]=\"'zfs-badge zfs-badge-' + item.type\">\n {{ getZfsBadge(item) }}\n </span>\n }\n\n <!-- Permission indicator -->\n @if (item.permissions === 'none') {\n <tn-icon\n name=\"lock\"\n library=\"mdi\"\n class=\"permission-icon\" />\n }\n </div>\n }\n\n <!-- EDIT MODE: Inline name input with error display -->\n @if (item.isCreating) {\n <div class=\"file-name-cell-wrapper\">\n <div class=\"file-name-cell editing\" [class.has-error]=\"!!item.creationError\">\n <tn-icon\n name=\"folder\"\n library=\"mdi\"\n class=\"file-icon file-icon-folder\" />\n <input\n #folderNameInput\n type=\"text\"\n role=\"textbox\"\n aria-label=\"Folder name\"\n class=\"folder-name-input\"\n spellcheck=\"false\"\n autocomplete=\"off\"\n [class.error]=\"!!item.creationError\"\n [value]=\"item.name\"\n [disabled]=\"creationLoading()\"\n [attr.data-autofocus]=\"true\"\n (keydown)=\"onFolderNameKeyDown($event, item)\"\n (blur)=\"onFolderNameInputBlur($event, item)\">\n\n <!-- Loading indicator during submission -->\n @if (creationLoading()) {\n <tn-icon\n name=\"loading\"\n library=\"mdi\"\n class=\"creation-loading-icon\" />\n }\n </div>\n\n <!-- Inline error message -->\n @if (item.creationError) {\n <div class=\"folder-creation-error\">\n <tn-icon name=\"alert-circle\" library=\"mdi\" class=\"error-icon\" />\n <span class=\"error-text\">{{ item.creationError }}</span>\n </div>\n }\n </div>\n }\n\n </ng-template>\n </ng-container>\n\n <!-- Size column -->\n <ng-container tnColumnDef=\"size\">\n <ng-template tnHeaderCellDef>Size</ng-template>\n <ng-template let-item tnCellDef>\n @if (item.size !== undefined) {\n <span>{{ item.size | tnFileSize }}</span>\n }\n @if (item.size === undefined && item.type === 'folder') {\n <span class=\"folder-indicator\">--</span>\n }\n </ng-template>\n </ng-container>\n\n <!-- Modified column -->\n <ng-container tnColumnDef=\"modified\">\n <ng-template tnHeaderCellDef>Modified</ng-template>\n <ng-template let-item tnCellDef>\n @if (item.modified) {\n <span>{{ formatDate(item.modified) }}</span>\n }\n </ng-template>\n </ng-container>\n\n\n </tn-table>\n\n <!-- Empty state -->\n @if (filteredFileItems().length === 0) {\n <div class=\"empty-state\">\n <tn-icon name=\"folder-open\" library=\"mdi\" customSize=\"48px\" />\n <p>No items found</p>\n </div>\n }\n </div>\n}\n\n<!-- Footer -->\n@if (!loading()) {\n <div class=\"tn-file-picker-footer\">\n @if (selectedItems().length > 0) {\n <span class=\"selection-count\">\n {{ selectedItems().length }} item{{ selectedItems().length !== 1 ? 's' : '' }} selected\n </span>\n }\n @if (selectedItems().length === 0) {\n <span class=\"selection-count\">\n No items selected\n </span>\n }\n <div class=\"footer-actions\">\n <tn-button\n label=\"Select\"\n [disabled]=\"selectedItems().length === 0\"\n (onClick)=\"onSubmit()\" />\n </div>\n </div>\n}", styles: [":host{display:block;background:var(--tn-bg1, white);color:var(--tn-fg1, #333);padding:0;box-shadow:0 4px 16px #0000001f,0 1px 4px #00000014;border-radius:8px;border:1px solid var(--tn-lines, #e0e0e0);min-width:400px;max-width:600px;min-height:500px;max-height:600px;font-family:var(--tn-font-family-body);display:flex;flex-direction:column;overflow:hidden}.tn-file-picker-header{display:flex;align-items:center;justify-content:space-between;padding:var(--tn-content-padding, 24px);padding-bottom:16px;border-bottom:1px solid var(--tn-lines)}.tn-file-picker-breadcrumb{display:flex;align-items:center;gap:4px;flex:1;min-width:0}.tn-file-picker-breadcrumb .breadcrumb-segment{background:transparent;border:none;color:var(--tn-primary);cursor:pointer;padding:4px 8px;border-radius:4px;font-size:1rem;white-space:nowrap;transition:background-color .15s ease-in-out}.tn-file-picker-breadcrumb .breadcrumb-segment:hover:not(:disabled){background:var(--tn-bg2)}.tn-file-picker-breadcrumb .breadcrumb-segment:disabled,.tn-file-picker-breadcrumb .breadcrumb-segment.current{color:var(--tn-fg1);cursor:default;font-weight:500}.tn-file-picker-breadcrumb .breadcrumb-segment:not(:last-child):after{content:\"/\";margin-left:8px;color:var(--tn-alt-fg1)}.tn-file-picker-actions{display:flex;align-items:center;gap:8px}.tn-file-picker-actions tn-button{font-size:1rem}.tn-file-picker-loading{display:flex;align-items:center;justify-content:center;gap:8px;padding:40px;color:var(--tn-fg2)}.tn-file-picker-loading tn-icon{animation:spin 1s linear infinite}@keyframes spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.tn-file-picker-content{flex:1;min-height:0;overflow-y:auto}.file-list-viewport{width:100%;height:100%}.file-list-viewport .cdk-virtual-scroll-content-wrapper{width:100%}tn-table{width:100%}tn-table th,tn-table .tn-table__header-cell{font-weight:600;color:var(--tn-fg1);padding:12px 16px;border-bottom:2px solid var(--tn-lines)}tn-table td,tn-table .tn-table__cell{padding:8px 16px;border-bottom:1px solid var(--tn-lines)}.file-checkbox{display:flex;align-items:center}.file-checkbox input[type=checkbox]{margin:0;width:16px;height:16px}.file-name-cell{display:flex;align-items:center;gap:8px;cursor:pointer}.file-name-cell.disabled{opacity:.5;color:var(--tn-fg2, #757575)}.file-name-cell.disabled .file-name{color:var(--tn-fg2, #757575)}.file-name-cell.disabled .file-icon{opacity:.6}.file-name-cell.disabled:has(.file-icon-folder),.file-name-cell.disabled:has(.file-icon-dataset),.file-name-cell.disabled:has(.file-icon-mountpoint){cursor:pointer}.file-name-cell.disabled:not(:has(.file-icon-folder)):not(:has(.file-icon-dataset)):not(:has(.file-icon-mountpoint)){cursor:not-allowed}.file-name-cell.editing{display:flex;align-items:center;gap:8px;padding:2px;cursor:default}.file-name-cell.editing .folder-name-input{flex:1;border:2px solid var(--tn-primary, #0066cc);padding:4px 8px;font-size:inherit;font-family:inherit;background:var(--tn-bg1, white);color:var(--tn-fg1, black);outline:none;border-radius:3px;min-width:200px}.file-name-cell.editing .folder-name-input:focus{border-color:var(--tn-primary, #0066cc);box-shadow:0 0 0 3px #0066cc1a}.file-name-cell.editing .folder-name-input.error{border-color:var(--tn-error, #d32f2f)}.file-name-cell.editing .folder-name-input:disabled{opacity:.6;cursor:not-allowed;background:var(--tn-bg2, #f5f5f5)}.file-name-cell.editing .creation-loading-icon{animation:spin 1s linear infinite;color:var(--tn-primary, #0066cc);flex-shrink:0}.file-name-cell-wrapper{display:flex;flex-direction:column;gap:4px}.folder-creation-error{display:flex;align-items:center;gap:6px;padding:4px 8px 4px 36px;margin-bottom:12px;background:#d32f2f1a;border-left:3px solid var(--tn-error, #d32f2f);border-radius:3px;font-size:1rem;color:var(--tn-error, #d32f2f)}.folder-creation-error .error-icon{flex-shrink:0;width:20px;height:20px}.folder-creation-error .error-text{flex:1}.file-icon{display:flex;align-items:center;justify-content:center;font-size:var(--tn-icon-md, 20px);flex-shrink:0;line-height:1}.file-icon.file-icon-folder{color:var(--tn-primary)}.file-icon.file-icon-dataset{color:var(--tn-blue, #007db3)}.file-icon.file-icon-zvol{color:var(--tn-green, #71BF44)}.file-icon.file-icon-mountpoint{color:var(--tn-orange, #E68D37)}.file-name{flex:1;font-weight:500;line-height:1.4}.zfs-badge{display:inline-flex;align-items:center;background:var(--tn-alt-bg2);color:var(--tn-alt-fg2);font-size:.625rem;font-weight:600;padding:2px 6px;border-radius:12px;text-transform:uppercase;letter-spacing:.5px;line-height:1}.zfs-badge.zfs-badge-dataset{background:var(--tn-blue);color:#fff}.zfs-badge.zfs-badge-zvol{background:var(--tn-green);color:#fff}.zfs-badge.zfs-badge-mountpoint{background:var(--tn-orange);color:#fff}.permission-icon{display:flex;align-items:center;justify-content:center;color:var(--tn-red);font-size:var(--tn-icon-sm, 16px);line-height:1}.file-type{font-size:1rem;padding:2px 8px;border-radius:12px}.file-type.type-folder{background:var(--tn-alt-bg1);color:var(--tn-alt-fg2)}.file-type.type-file{background:var(--tn-bg2);color:var(--tn-fg2)}.file-type.type-dataset{background:#007db31a;color:var(--tn-blue)}.file-type.type-zvol{background:#71bf441a;color:var(--tn-green)}.file-type.type-mountpoint{background:#e68d371a;color:var(--tn-orange)}.folder-indicator{color:var(--tn-alt-fg1);font-style:italic}.empty-state{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:40px;color:var(--tn-alt-fg1);text-align:center}.empty-state tn-icon{margin-bottom:16px;opacity:.5}.empty-state p{margin:0;font-size:1rem}.tn-file-picker-footer{display:flex;align-items:center;justify-content:space-between;padding:16px var(--tn-content-padding, 24px);border-top:1px solid var(--tn-lines);background:var(--tn-bg2);border-bottom-left-radius:8px;border-bottom-right-radius:8px}.selection-count{font-size:1rem;color:var(--tn-fg2);font-weight:500}.footer-actions{display:flex;gap:8px}@media(prefers-reduced-motion:reduce){.file-item,.breadcrumb-segment{transition:none}.tn-file-picker-loading tn-icon{animation:none}}@media(prefers-contrast:high){:host{border-width:2px}.file-item:hover,.file-item.selected{border:2px solid var(--tn-fg1)}.zfs-badge{border:1px solid var(--tn-fg1)}}@media(max-width:768px){:host{min-width:300px;max-width:calc(100vw - 32px);max-height:calc(100vh - 64px)}.tn-file-picker-header{flex-direction:column;gap:12px;align-items:stretch}.tn-file-picker-breadcrumb{overflow-x:auto;scrollbar-width:none;-ms-overflow-style:none}.tn-file-picker-breadcrumb::-webkit-scrollbar{display:none}.file-item{padding:12px;min-height:56px}.file-info{font-size:1rem}}\n"], dependencies: [{ kind: "component", type: TnIconComponent, selector: "tn-icon", inputs: ["name", "size", "color", "tooltip", "ariaLabel", "library", "testId", "fullSize", "customSize"] }, { kind: "component", type: TnButtonComponent, selector: "tn-button", inputs: ["primary", "color", "variant", "backgroundColor", "label", "disabled", "type", "testId", "href", "routerLink", "queryParams", "fragment", "target", "rel", "ariaLabel"], outputs: ["onClick"] }, { kind: "component", type: TnTableComponent, selector: "tn-table", inputs: ["dataSource", "displayedColumns", "trackBy", "emptyMessage", "emptyIcon", "selectable", "expandable", "bordered", "activeRow", "activeBg", "activeIndicator", "loading", "loadingMessage", "clickable"], outputs: ["sortChange", "selectionChange", "rowClick"] }, { kind: "directive", type: TnTableColumnDirective, selector: "[tnColumnDef]", inputs: ["tnColumnDef", "sortable", "width"], exportAs: ["tnColumnDef"] }, { kind: "directive", type: TnHeaderCellDefDirective, selector: "[tnHeaderCellDef]" }, { kind: "directive", type: TnCellDefDirective, selector: "[tnCellDef]" }, { kind: "ngmodule", type: ScrollingModule }, { kind: "ngmodule", type: A11yModule }, { kind: "pipe", type: FileSizePipe, name: "tnFileSize" }, { kind: "pipe", type: TruncatePathPipe, name: "tnTruncatePath" }] });
15819
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.0", type: TnFilePickerPopupComponent, isStandalone: true, selector: "tn-file-picker-popup", inputs: { mode: { classPropertyName: "mode", publicName: "mode", isSignal: true, isRequired: false, transformFunction: null }, multiSelect: { classPropertyName: "multiSelect", publicName: "multiSelect", isSignal: true, isRequired: false, transformFunction: null }, allowCreate: { classPropertyName: "allowCreate", publicName: "allowCreate", isSignal: true, isRequired: false, transformFunction: null }, allowDatasetCreate: { classPropertyName: "allowDatasetCreate", publicName: "allowDatasetCreate", isSignal: true, isRequired: false, transformFunction: null }, allowZvolCreate: { classPropertyName: "allowZvolCreate", publicName: "allowZvolCreate", isSignal: true, isRequired: false, transformFunction: null }, currentPath: { classPropertyName: "currentPath", publicName: "currentPath", isSignal: true, isRequired: false, transformFunction: null }, fileItems: { classPropertyName: "fileItems", publicName: "fileItems", isSignal: true, isRequired: false, transformFunction: null }, selectedItems: { classPropertyName: "selectedItems", publicName: "selectedItems", isSignal: true, isRequired: false, transformFunction: null }, loading: { classPropertyName: "loading", publicName: "loading", isSignal: true, isRequired: false, transformFunction: null }, creationLoading: { classPropertyName: "creationLoading", publicName: "creationLoading", isSignal: true, isRequired: false, transformFunction: null }, fileExtensions: { classPropertyName: "fileExtensions", publicName: "fileExtensions", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { itemClick: "itemClick", itemDoubleClick: "itemDoubleClick", pathNavigate: "pathNavigate", createFolder: "createFolder", clearSelection: "clearSelection", close: "close", submit: "submit", cancel: "cancel", submitFolderName: "submitFolderName", cancelFolderCreation: "cancelFolderCreation" }, host: { classAttribute: "tn-file-picker-popup" }, ngImport: i0, template: "<!-- Header with breadcrumb navigation -->\n<div class=\"tn-file-picker-header\">\n <nav class=\"tn-file-picker-breadcrumb\" aria-label=\"File path\">\n @for (segment of currentPath() | tnTruncatePath; track $index; let last = $last) {\n <button\n class=\"breadcrumb-segment\"\n [class.current]=\"last\"\n [class.parent-nav]=\"segment.name === '..'\"\n [disabled]=\"last\"\n (click)=\"navigateToPath(segment.path)\">\n {{ segment.name }}\n </button>\n }\n </nav>\n\n <div class=\"tn-file-picker-actions\">\n @if (allowCreate()) {\n <tn-button\n variant=\"outline\"\n label=\"New Folder\"\n [disabled]=\"isCreateDisabled()\"\n (onClick)=\"onCreateFolder()\" />\n }\n </div>\n</div>\n\n<!-- Loading indicator -->\n@if (loading()) {\n <div class=\"tn-file-picker-loading\">\n <tn-icon name=\"loading\" library=\"mdi\" />\n <span>Loading...</span>\n </div>\n}\n\n<!-- File table -->\n@if (!loading()) {\n <div class=\"tn-file-picker-content\">\n <tn-table\n [dataSource]=\"filteredFileItems()\"\n [displayedColumns]=\"multiSelect() ? displayedColumns : displayedColumns.slice(1)\">\n\n <!-- Selection column -->\n @if (multiSelect()) {\n <ng-container tnColumnDef=\"select\">\n <ng-template tnHeaderCellDef>\n <!-- Select all checkbox -->\n </ng-template>\n <ng-template let-item tnCellDef>\n <input \n type=\"checkbox\" \n [checked]=\"isSelected(item)\"\n [disabled]=\"!!item.disabled\"\n (click)=\"$event.stopPropagation()\"\n (change)=\"onItemClick(item)\">\n </ng-template>\n </ng-container>\n }\n\n <!-- Name column -->\n <ng-container tnColumnDef=\"name\">\n <ng-template tnHeaderCellDef>Name</ng-template>\n <ng-template let-item tnCellDef>\n\n <!-- NORMAL MODE: Display name -->\n @if (!item.isCreating) {\n <div\n class=\"file-name-cell\"\n [class.disabled]=\"!!item.disabled\"\n [class.zfs-object]=\"isZfsObject(item)\"\n [attr.tabindex]=\"item.disabled ? null : 0\"\n [attr.role]=\"'button'\"\n (click)=\"onItemClick(item)\"\n (dblclick)=\"onItemDoubleClick(item)\"\n (keydown.enter)=\"onItemDoubleClick(item)\"\n (keydown.space)=\"onItemClick(item)\">\n <tn-icon\n [name]=\"getItemIcon(item)\"\n [library]=\"getItemIconLibrary(item)\"\n [class]=\"'file-icon file-icon-' + item.type\" />\n <span class=\"file-name\">{{ item.name }}</span>\n\n <!-- ZFS badge -->\n @if (isZfsObject(item)) {\n <span\n [class]=\"'zfs-badge zfs-badge-' + item.type\">\n {{ getZfsBadge(item) }}\n </span>\n }\n\n <!-- Permission indicator -->\n @if (item.permissions === 'none') {\n <tn-icon\n name=\"lock\"\n library=\"mdi\"\n class=\"permission-icon\" />\n }\n </div>\n }\n\n <!-- EDIT MODE: Inline name input with error display -->\n @if (item.isCreating) {\n <div class=\"file-name-cell-wrapper\">\n <div class=\"file-name-cell editing\" [class.has-error]=\"!!item.creationError\">\n <tn-icon\n name=\"folder\"\n library=\"mdi\"\n class=\"file-icon file-icon-folder\" />\n <input\n #folderNameInput\n type=\"text\"\n role=\"textbox\"\n aria-label=\"Folder name\"\n class=\"folder-name-input\"\n spellcheck=\"false\"\n autocomplete=\"off\"\n [class.error]=\"!!item.creationError\"\n [value]=\"item.name\"\n [disabled]=\"creationLoading()\"\n [attr.data-autofocus]=\"true\"\n (keydown)=\"onFolderNameKeyDown($event, item)\"\n (blur)=\"onFolderNameInputBlur($event, item)\">\n\n <!-- Loading indicator during submission -->\n @if (creationLoading()) {\n <tn-icon\n name=\"loading\"\n library=\"mdi\"\n class=\"creation-loading-icon\" />\n }\n </div>\n\n <!-- Inline error message -->\n @if (item.creationError) {\n <div class=\"folder-creation-error\">\n <tn-icon name=\"alert-circle\" library=\"mdi\" class=\"error-icon\" />\n <span class=\"error-text\">{{ item.creationError }}</span>\n </div>\n }\n </div>\n }\n\n </ng-template>\n </ng-container>\n\n <!-- Size column -->\n <ng-container tnColumnDef=\"size\">\n <ng-template tnHeaderCellDef>Size</ng-template>\n <ng-template let-item tnCellDef>\n @if (item.size !== undefined) {\n <span>{{ item.size | tnFileSize }}</span>\n }\n @if (item.size === undefined && item.type === 'folder') {\n <span class=\"folder-indicator\">--</span>\n }\n </ng-template>\n </ng-container>\n\n <!-- Modified column -->\n <ng-container tnColumnDef=\"modified\">\n <ng-template tnHeaderCellDef>Modified</ng-template>\n <ng-template let-item tnCellDef>\n @if (item.modified) {\n <span>{{ formatDate(item.modified) }}</span>\n }\n </ng-template>\n </ng-container>\n\n\n </tn-table>\n\n <!-- Empty state -->\n @if (filteredFileItems().length === 0) {\n <div class=\"empty-state\">\n <tn-icon name=\"folder-open\" library=\"mdi\" customSize=\"48px\" />\n <p>No items found</p>\n </div>\n }\n </div>\n}\n\n<!-- Footer -->\n@if (!loading()) {\n <div class=\"tn-file-picker-footer\">\n @if (selectedItems().length > 0) {\n <span class=\"selection-count\">\n {{ selectedItems().length }} item{{ selectedItems().length !== 1 ? 's' : '' }} selected\n </span>\n }\n @if (selectedItems().length === 0) {\n <span class=\"selection-count\">\n No items selected\n </span>\n }\n <div class=\"footer-actions\">\n <tn-button\n label=\"Select\"\n [disabled]=\"selectedItems().length === 0\"\n (onClick)=\"onSubmit()\" />\n </div>\n </div>\n}", styles: [":host{display:block;background:var(--tn-bg1, white);color:var(--tn-fg1, #333);padding:0;box-shadow:0 4px 16px #0000001f,0 1px 4px #00000014;border-radius:8px;border:1px solid var(--tn-lines, #e0e0e0);min-width:400px;max-width:600px;min-height:500px;max-height:600px;font-family:var(--tn-font-family-body);display:flex;flex-direction:column;overflow:hidden}.tn-file-picker-header{display:flex;align-items:center;justify-content:space-between;padding:var(--tn-content-padding, 24px);padding-bottom:16px;border-bottom:1px solid var(--tn-lines)}.tn-file-picker-breadcrumb{display:flex;align-items:center;gap:4px;flex:1;min-width:0}.tn-file-picker-breadcrumb .breadcrumb-segment{background:transparent;border:none;color:var(--tn-primary);cursor:pointer;padding:4px 8px;border-radius:4px;font-size:1rem;white-space:nowrap;transition:background-color .15s ease-in-out}.tn-file-picker-breadcrumb .breadcrumb-segment:hover:not(:disabled){background:var(--tn-bg2)}.tn-file-picker-breadcrumb .breadcrumb-segment:disabled,.tn-file-picker-breadcrumb .breadcrumb-segment.current{color:var(--tn-fg1);cursor:default;font-weight:500}.tn-file-picker-breadcrumb .breadcrumb-segment:not(:last-child):after{content:\"/\";margin-left:8px;color:var(--tn-alt-fg1)}.tn-file-picker-actions{display:flex;align-items:center;gap:8px}.tn-file-picker-actions tn-button{font-size:1rem}.tn-file-picker-loading{display:flex;align-items:center;justify-content:center;gap:8px;padding:40px;color:var(--tn-fg2)}.tn-file-picker-loading tn-icon{animation:spin 1s linear infinite}@keyframes spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.tn-file-picker-content{flex:1;min-height:0;overflow-y:auto}.file-list-viewport{width:100%;height:100%}.file-list-viewport .cdk-virtual-scroll-content-wrapper{width:100%}tn-table{width:100%}tn-table th,tn-table .tn-table__header-cell{font-weight:600;color:var(--tn-fg1);padding:12px 16px;border-bottom:2px solid var(--tn-lines)}tn-table td,tn-table .tn-table__cell{padding:8px 16px;border-bottom:1px solid var(--tn-lines)}.file-checkbox{display:flex;align-items:center}.file-checkbox input[type=checkbox]{margin:0;width:16px;height:16px}.file-name-cell{display:flex;align-items:center;gap:8px;cursor:pointer}.file-name-cell.disabled{opacity:.5;color:var(--tn-fg2, #757575)}.file-name-cell.disabled .file-name{color:var(--tn-fg2, #757575)}.file-name-cell.disabled .file-icon{opacity:.6}.file-name-cell.disabled:has(.file-icon-folder),.file-name-cell.disabled:has(.file-icon-dataset),.file-name-cell.disabled:has(.file-icon-mountpoint){cursor:pointer}.file-name-cell.disabled:not(:has(.file-icon-folder)):not(:has(.file-icon-dataset)):not(:has(.file-icon-mountpoint)){cursor:not-allowed}.file-name-cell.editing{display:flex;align-items:center;gap:8px;padding:2px;cursor:default}.file-name-cell.editing .folder-name-input{flex:1;border:2px solid var(--tn-primary, #0066cc);padding:4px 8px;font-size:inherit;font-family:inherit;background:var(--tn-bg1, white);color:var(--tn-fg1, black);outline:none;border-radius:3px;min-width:200px}.file-name-cell.editing .folder-name-input:focus{border-color:var(--tn-primary, #0066cc);box-shadow:0 0 0 3px #0066cc1a}.file-name-cell.editing .folder-name-input.error{border-color:var(--tn-error, #d32f2f)}.file-name-cell.editing .folder-name-input:disabled{opacity:.6;cursor:not-allowed;background:var(--tn-bg2, #f5f5f5)}.file-name-cell.editing .creation-loading-icon{animation:spin 1s linear infinite;color:var(--tn-primary, #0066cc);flex-shrink:0}.file-name-cell-wrapper{display:flex;flex-direction:column;gap:4px}.folder-creation-error{display:flex;align-items:center;gap:6px;padding:4px 8px 4px 36px;margin-bottom:12px;background:#d32f2f1a;border-left:3px solid var(--tn-error, #d32f2f);border-radius:3px;font-size:1rem;color:var(--tn-error, #d32f2f)}.folder-creation-error .error-icon{flex-shrink:0;width:20px;height:20px}.folder-creation-error .error-text{flex:1}.file-icon{display:flex;align-items:center;justify-content:center;font-size:var(--tn-icon-md, 20px);flex-shrink:0;line-height:1}.file-icon.file-icon-folder{color:var(--tn-primary)}.file-icon.file-icon-dataset{color:var(--tn-blue, #007db3)}.file-icon.file-icon-zvol{color:var(--tn-green, #71BF44)}.file-icon.file-icon-mountpoint{color:var(--tn-orange, #E68D37)}.file-name{flex:1;font-weight:500;line-height:1.4}.zfs-badge{display:inline-flex;align-items:center;background:var(--tn-alt-bg2);color:var(--tn-alt-fg2);font-size:.625rem;font-weight:600;padding:2px 6px;border-radius:12px;text-transform:uppercase;letter-spacing:.5px;line-height:1}.zfs-badge.zfs-badge-dataset{background:var(--tn-blue);color:#fff}.zfs-badge.zfs-badge-zvol{background:var(--tn-green);color:#fff}.zfs-badge.zfs-badge-mountpoint{background:var(--tn-orange);color:#fff}.permission-icon{display:flex;align-items:center;justify-content:center;color:var(--tn-red);font-size:var(--tn-icon-sm, 16px);line-height:1}.file-type{font-size:1rem;padding:2px 8px;border-radius:12px}.file-type.type-folder{background:var(--tn-alt-bg1);color:var(--tn-alt-fg2)}.file-type.type-file{background:var(--tn-bg2);color:var(--tn-fg2)}.file-type.type-dataset{background:#007db31a;color:var(--tn-blue)}.file-type.type-zvol{background:#71bf441a;color:var(--tn-green)}.file-type.type-mountpoint{background:#e68d371a;color:var(--tn-orange)}.folder-indicator{color:var(--tn-alt-fg1);font-style:italic}.empty-state{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:40px;color:var(--tn-alt-fg1);text-align:center}.empty-state tn-icon{margin-bottom:16px;opacity:.5}.empty-state p{margin:0;font-size:1rem}.tn-file-picker-footer{display:flex;align-items:center;justify-content:space-between;padding:16px var(--tn-content-padding, 24px);border-top:1px solid var(--tn-lines);background:var(--tn-bg2);border-bottom-left-radius:8px;border-bottom-right-radius:8px}.selection-count{font-size:1rem;color:var(--tn-fg2);font-weight:500}.footer-actions{display:flex;gap:8px}@media(prefers-reduced-motion:reduce){.file-item,.breadcrumb-segment{transition:none}.tn-file-picker-loading tn-icon{animation:none}}@media(prefers-contrast:high){:host{border-width:2px}.file-item:hover,.file-item.selected{border:2px solid var(--tn-fg1)}.zfs-badge{border:1px solid var(--tn-fg1)}}@media(max-width:768px){:host{min-width:300px;max-width:calc(100vw - 32px);max-height:calc(100vh - 64px)}.tn-file-picker-header{flex-direction:column;gap:12px;align-items:stretch}.tn-file-picker-breadcrumb{overflow-x:auto;scrollbar-width:none;-ms-overflow-style:none}.tn-file-picker-breadcrumb::-webkit-scrollbar{display:none}.file-item{padding:12px;min-height:56px}.file-info{font-size:1rem}}\n"], dependencies: [{ kind: "component", type: TnIconComponent, selector: "tn-icon", inputs: ["name", "size", "color", "tooltip", "ariaLabel", "library", "testId", "fullSize", "customSize"] }, { kind: "component", type: TnButtonComponent, selector: "tn-button", inputs: ["primary", "color", "variant", "backgroundColor", "label", "icon", "iconPosition", "disabled", "type", "testId", "href", "routerLink", "queryParams", "fragment", "target", "rel", "ariaLabel"], outputs: ["onClick"] }, { kind: "component", type: TnTableComponent, selector: "tn-table", inputs: ["dataSource", "displayedColumns", "trackBy", "emptyMessage", "emptyIcon", "selectable", "expandable", "bordered", "activeRow", "activeBg", "activeIndicator", "loading", "loadingMessage", "clickable"], outputs: ["sortChange", "selectionChange", "rowClick"] }, { kind: "directive", type: TnTableColumnDirective, selector: "[tnColumnDef]", inputs: ["tnColumnDef", "sortable", "width"], exportAs: ["tnColumnDef"] }, { kind: "directive", type: TnHeaderCellDefDirective, selector: "[tnHeaderCellDef]" }, { kind: "directive", type: TnCellDefDirective, selector: "[tnCellDef]" }, { kind: "ngmodule", type: ScrollingModule }, { kind: "ngmodule", type: A11yModule }, { kind: "pipe", type: FileSizePipe, name: "tnFileSize" }, { kind: "pipe", type: TruncatePathPipe, name: "tnTruncatePath" }] });
15619
15820
  }
15620
15821
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnFilePickerPopupComponent, decorators: [{
15621
15822
  type: Component,