@truenas/ui-components 0.4.4 → 0.4.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,19 +1,19 @@
1
1
  import { Overlay, OverlayPositionBuilder, OverlayModule } from '@angular/cdk/overlay';
2
2
  import { TemplatePortal, ComponentPortal, PortalModule } from '@angular/cdk/portal';
3
3
  import * as i0 from '@angular/core';
4
- import { InjectionToken, inject, computed, input, ViewEncapsulation, ChangeDetectionStrategy, Component, Injector, Renderer2, ElementRef, effect, Directive, ViewContainerRef, output, viewChild, signal, untracked, isDevMode, forwardRef, model, afterNextRender, Injectable, contentChildren, Pipe, HostListener, linkedSignal, contentChild, TemplateRef, ChangeDetectorRef, DestroyRef, isSignal, IterableDiffers, booleanAttribute, ApplicationRef, EnvironmentInjector, createComponent, PLATFORM_ID } from '@angular/core';
4
+ import { InjectionToken, inject, computed, input, ViewEncapsulation, ChangeDetectionStrategy, Component, Injector, Renderer2, ElementRef, effect, Directive, ViewContainerRef, output, viewChild, signal, untracked, isDevMode, forwardRef, model, afterNextRender, Injectable, contentChildren, Pipe, HostListener, linkedSignal, contentChild, TemplateRef, ChangeDetectorRef, DestroyRef, isSignal, IterableDiffers, booleanAttribute, LOCALE_ID, ApplicationRef, EnvironmentInjector, createComponent, PLATFORM_ID } from '@angular/core';
5
5
  import * as i1$3 from '@angular/forms';
6
6
  import { NgControl, NG_VALUE_ACCESSOR, FormsModule, Validators } from '@angular/forms';
7
- import { ComponentHarness, HarnessPredicate, TestKey } from '@angular/cdk/testing';
7
+ import { ComponentHarness, HarnessPredicate, TestKey, parallel } from '@angular/cdk/testing';
8
8
  import * as i1 from '@angular/cdk/a11y';
9
- import { A11yModule, FocusMonitor } from '@angular/cdk/a11y';
9
+ import { A11yModule, FocusMonitor, _IdGenerator } from '@angular/cdk/a11y';
10
10
  import * as i1$2 from '@angular/common';
11
11
  import { DOCUMENT, NgTemplateOutlet, CommonModule, AsyncPipe, isPlatformBrowser } from '@angular/common';
12
12
  import { mdiCheckCircle, mdiAlertCircle, mdiAlert, mdiInformation, mdiOpenInNew, mdiHelpCircle, mdiDotsVertical, mdiClose } from '@mdi/js';
13
13
  import * as i1$1 from '@angular/platform-browser';
14
14
  import { DomSanitizer } from '@angular/platform-browser';
15
15
  import { HttpClient } from '@angular/common/http';
16
- import { firstValueFrom, BehaviorSubject, merge, animationFrameScheduler, asapScheduler, Subject, take } from 'rxjs';
16
+ import { firstValueFrom, BehaviorSubject, merge, animationFrameScheduler, asapScheduler, take, Subject } from 'rxjs';
17
17
  import { RouterLink } from '@angular/router';
18
18
  import { filesize } from 'filesize';
19
19
  import { CdkMenu, CdkMenuItem, CdkMenuTrigger } from '@angular/cdk/menu';
@@ -2504,6 +2504,11 @@ class TnButtonComponent {
2504
2504
  */
2505
2505
  iconPosition = input('left', ...(ngDevMode ? [{ debugName: "iconPosition" }] : []));
2506
2506
  disabled = input(false, ...(ngDevMode ? [{ debugName: "disabled" }] : []));
2507
+ /**
2508
+ * Stretches the button to the full width of its container instead of
2509
+ * shrink-wrapping the label. Use for buttons that head or foot a form column.
2510
+ */
2511
+ fullWidth = input(false, ...(ngDevMode ? [{ debugName: "fullWidth" }] : []));
2507
2512
  /**
2508
2513
  * Native `type` of the rendered `<button>`. Defaults to `button` so stray
2509
2514
  * clicks never submit an enclosing form. Set to `submit` for a form's save
@@ -2614,12 +2619,14 @@ class TnButtonComponent {
2614
2619
  host.focus = (options) => inner.focus(options);
2615
2620
  }
2616
2621
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnButtonComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
2617
- 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:1px solid transparent;font-weight:500;line-height:1;font-family:IBM Plex Sans,Helvetica Neue,Helvetica,Arial,sans-serif;white-space:nowrap}.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" }] });
2622
+ 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 }, fullWidth: { classPropertyName: "fullWidth", publicName: "fullWidth", 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" }, host: { properties: { "class.tn-button--full-width": "fullWidth()" } }, 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}:host(.tn-button--full-width){display:block;width:100%;justify-self:stretch}:host(.tn-button--full-width) .storybook-button{width:100%}.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:1px solid transparent;font-weight:500;line-height:1;font-family:IBM Plex Sans,Helvetica Neue,Helvetica,Arial,sans-serif;white-space:nowrap}.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" }] });
2618
2623
  }
2619
2624
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnButtonComponent, decorators: [{
2620
2625
  type: Component,
2621
- 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:1px solid transparent;font-weight:500;line-height:1;font-family:IBM Plex Sans,Helvetica Neue,Helvetica,Arial,sans-serif;white-space:nowrap}.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"] }]
2622
- }], 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 }] }] } });
2626
+ args: [{ selector: 'tn-button', standalone: true, imports: [CommonModule, RouterLink, TnTestIdDirective, LabelMarkupPipe, TnIconComponent], host: {
2627
+ '[class.tn-button--full-width]': 'fullWidth()',
2628
+ }, 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}:host(.tn-button--full-width){display:block;width:100%;justify-self:stretch}:host(.tn-button--full-width) .storybook-button{width:100%}.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:1px solid transparent;font-weight:500;line-height:1;font-family:IBM Plex Sans,Helvetica Neue,Helvetica,Arial,sans-serif;white-space:nowrap}.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"] }]
2629
+ }], 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 }] }], fullWidth: [{ type: i0.Input, args: [{ isSignal: true, alias: "fullWidth", 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 }] }] } });
2623
2630
 
2624
2631
  /**
2625
2632
  * Harness for interacting with tn-button in tests.
@@ -4500,7 +4507,7 @@ class TnFileInputComponent {
4500
4507
  useExisting: forwardRef(() => TnFileInputComponent),
4501
4508
  multi: true
4502
4509
  }
4503
- ], viewQueries: [{ propertyName: "fileInputEl", first: true, predicate: ["fileInput"], descendants: true, isSignal: true }], ngImport: i0, template: "<div class=\"tn-file-input__container\" tnTestIdType=\"file-input\" [tnTestId]=\"testId()\" (focusout)=\"onBlur()\">\n <input\n #fileInput\n type=\"file\"\n class=\"tn-file-input__native\"\n tabindex=\"-1\"\n aria-hidden=\"true\"\n [accept]=\"accept()\"\n [multiple]=\"multiple()\"\n [disabled]=\"isDisabled()\"\n (change)=\"onFilesSelected($event)\">\n\n <tn-button\n class=\"tn-file-input__button\"\n color=\"default\"\n [label]=\"buttonLabel()\"\n [disabled]=\"isDisabled()\"\n [ariaLabel]=\"ariaLabel()\"\n (onClick)=\"openFileDialog()\" />\n\n @if (showFileName()) {\n <span class=\"tn-file-input__filename\" [class.tn-file-input__filename--empty]=\"!hasFiles()\">\n {{ hasFiles() ? fileNames() : noFileText() }}\n </span>\n }\n\n <!--\n Always-present live region so screen readers are told what was picked even\n when the visible file name is suppressed (showFileName=false).\n -->\n <span class=\"tn-file-input__announcer\" aria-live=\"polite\">\n {{ hasFiles() ? fileNames() : '' }}\n </span>\n</div>\n", styles: [":host{display:inline-block}.tn-file-input__container{display:inline-flex;align-items:center;gap:12px}.tn-file-input__native,.tn-file-input__announcer{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}.tn-file-input__button{flex-shrink:0}.tn-file-input__filename{font-size:14px;color:var(--tn-fg1);max-width:30ch;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.tn-file-input__filename--empty{color:var(--tn-fg2)}\n"], dependencies: [{ 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: "directive", type: TnTestIdDirective, selector: "[tnTestId]", inputs: ["tnTestId", "tnTestIdType"] }] });
4510
+ ], viewQueries: [{ propertyName: "fileInputEl", first: true, predicate: ["fileInput"], descendants: true, isSignal: true }], ngImport: i0, template: "<div class=\"tn-file-input__container\" tnTestIdType=\"file-input\" [tnTestId]=\"testId()\" (focusout)=\"onBlur()\">\n <input\n #fileInput\n type=\"file\"\n class=\"tn-file-input__native\"\n tabindex=\"-1\"\n aria-hidden=\"true\"\n [accept]=\"accept()\"\n [multiple]=\"multiple()\"\n [disabled]=\"isDisabled()\"\n (change)=\"onFilesSelected($event)\">\n\n <tn-button\n class=\"tn-file-input__button\"\n color=\"default\"\n [label]=\"buttonLabel()\"\n [disabled]=\"isDisabled()\"\n [ariaLabel]=\"ariaLabel()\"\n (onClick)=\"openFileDialog()\" />\n\n @if (showFileName()) {\n <span class=\"tn-file-input__filename\" [class.tn-file-input__filename--empty]=\"!hasFiles()\">\n {{ hasFiles() ? fileNames() : noFileText() }}\n </span>\n }\n\n <!--\n Always-present live region so screen readers are told what was picked even\n when the visible file name is suppressed (showFileName=false).\n -->\n <span class=\"tn-file-input__announcer\" aria-live=\"polite\">\n {{ hasFiles() ? fileNames() : '' }}\n </span>\n</div>\n", styles: [":host{display:inline-block}.tn-file-input__container{display:inline-flex;align-items:center;gap:12px}.tn-file-input__native,.tn-file-input__announcer{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}.tn-file-input__button{flex-shrink:0}.tn-file-input__filename{font-size:14px;color:var(--tn-fg1);max-width:30ch;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.tn-file-input__filename--empty{color:var(--tn-fg2)}\n"], dependencies: [{ kind: "component", type: TnButtonComponent, selector: "tn-button", inputs: ["primary", "color", "variant", "backgroundColor", "label", "icon", "iconPosition", "disabled", "fullWidth", "type", "testId", "href", "routerLink", "queryParams", "fragment", "target", "rel", "ariaLabel"], outputs: ["onClick"] }, { kind: "directive", type: TnTestIdDirective, selector: "[tnTestId]", inputs: ["tnTestId", "tnTestIdType"] }] });
4504
4511
  }
4505
4512
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnFileInputComponent, decorators: [{
4506
4513
  type: Component,
@@ -6057,6 +6064,12 @@ class TnSlideToggleComponent {
6057
6064
  toggleEl = viewChild.required('toggleEl');
6058
6065
  labelPosition = input('after', ...(ngDevMode ? [{ debugName: "labelPosition" }] : []));
6059
6066
  label = input(undefined, ...(ngDevMode ? [{ debugName: "label" }] : []));
6067
+ /**
6068
+ * Stretches the toggle to the full width of its container and pushes the label
6069
+ * and the track to opposite ends of the row, instead of shrink-wrapping them
6070
+ * side by side. Use for settings rows and option lists.
6071
+ */
6072
+ fullWidth = input(false, ...(ngDevMode ? [{ debugName: "fullWidth" }] : []));
6060
6073
  disabled = input(false, ...(ngDevMode ? [{ debugName: "disabled" }] : []));
6061
6074
  required = input(false, ...(ngDevMode ? [{ debugName: "required" }] : []));
6062
6075
  color = input('primary', ...(ngDevMode ? [{ debugName: "color" }] : []));
@@ -6137,6 +6150,9 @@ class TnSlideToggleComponent {
6137
6150
  if (this.effectiveChecked()) {
6138
6151
  classes.push('tn-slide-toggle--checked');
6139
6152
  }
6153
+ if (this.fullWidth()) {
6154
+ classes.push('tn-slide-toggle--full-width');
6155
+ }
6140
6156
  classes.push(`tn-slide-toggle--${this.color()}`);
6141
6157
  classes.push(`tn-slide-toggle--label-${this.labelPosition()}`);
6142
6158
  return classes;
@@ -6145,24 +6161,29 @@ class TnSlideToggleComponent {
6145
6161
  return this.ariaLabel() || (this.label() ? undefined : 'Toggle');
6146
6162
  }, ...(ngDevMode ? [{ debugName: "effectiveAriaLabel" }] : []));
6147
6163
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnSlideToggleComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
6148
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.0", type: TnSlideToggleComponent, isStandalone: true, selector: "tn-slide-toggle", inputs: { labelPosition: { classPropertyName: "labelPosition", publicName: "labelPosition", 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 }, required: { classPropertyName: "required", publicName: "required", isSignal: true, isRequired: false, transformFunction: null }, color: { classPropertyName: "color", publicName: "color", isSignal: true, isRequired: false, transformFunction: null }, testId: { classPropertyName: "testId", publicName: "testId", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null }, ariaLabelledby: { classPropertyName: "ariaLabelledby", publicName: "ariaLabelledby", isSignal: true, isRequired: false, transformFunction: null }, checked: { classPropertyName: "checked", publicName: "checked", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { change: "change", toggleChange: "toggleChange" }, providers: [
6164
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.0", type: TnSlideToggleComponent, isStandalone: true, selector: "tn-slide-toggle", inputs: { labelPosition: { classPropertyName: "labelPosition", publicName: "labelPosition", isSignal: true, isRequired: false, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, fullWidth: { classPropertyName: "fullWidth", publicName: "fullWidth", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, required: { classPropertyName: "required", publicName: "required", isSignal: true, isRequired: false, transformFunction: null }, color: { classPropertyName: "color", publicName: "color", isSignal: true, isRequired: false, transformFunction: null }, testId: { classPropertyName: "testId", publicName: "testId", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null }, ariaLabelledby: { classPropertyName: "ariaLabelledby", publicName: "ariaLabelledby", isSignal: true, isRequired: false, transformFunction: null }, checked: { classPropertyName: "checked", publicName: "checked", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { change: "change", toggleChange: "toggleChange" }, host: { properties: { "class.tn-slide-toggle-host--full-width": "fullWidth()" } }, providers: [
6149
6165
  {
6150
6166
  provide: NG_VALUE_ACCESSOR,
6151
6167
  useExisting: forwardRef(() => TnSlideToggleComponent),
6152
6168
  multi: true
6153
6169
  }
6154
- ], viewQueries: [{ propertyName: "toggleEl", first: true, predicate: ["toggleEl"], descendants: true, isSignal: true }], ngImport: i0, template: "<div [ngClass]=\"classes()\">\n <label class=\"tn-slide-toggle__label\" tnTestIdType=\"toggle\" [for]=\"id\" [tnTestId]=\"resolvedTestId()\">\n\n <!-- Label before toggle -->\n @if (label() && labelPosition() === 'before') {\n <span\n class=\"tn-slide-toggle__label-text tn-slide-toggle__label-text--before\"\n tabindex=\"0\"\n role=\"button\"\n [innerHTML]=\"label() | tnLabelMarkup\"\n (click)=\"onLabelClick()\"\n (keydown.enter)=\"onLabelClick()\"\n (keydown.space)=\"onLabelClick()\">\n </span>\n }\n\n <!-- Toggle track and thumb -->\n <div class=\"tn-slide-toggle__bar\">\n <input\n #toggleEl\n type=\"checkbox\"\n class=\"tn-slide-toggle__input\"\n [id]=\"id\"\n [checked]=\"effectiveChecked()\"\n [disabled]=\"isDisabled()\"\n [required]=\"required()\"\n [attr.aria-label]=\"effectiveAriaLabel()\"\n [attr.aria-labelledby]=\"ariaLabelledby()\"\n (change)=\"onToggleChange($event)\"\n />\n\n <div class=\"tn-slide-toggle__track\">\n <div class=\"tn-slide-toggle__track-fill\"></div>\n </div>\n\n <div class=\"tn-slide-toggle__thumb-container\">\n <div class=\"tn-slide-toggle__thumb\">\n <div class=\"tn-slide-toggle__ripple\"></div>\n <!-- State icon -->\n <div class=\"tn-slide-toggle__icon\">\n @if (effectiveChecked()) {\n <svg\n class=\"tn-slide-toggle__check-icon\"\n viewBox=\"0 0 24 24\"\n width=\"16\"\n height=\"16\">\n <path d=\"M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41L9 16.17z\" fill=\"currentColor\"/>\n </svg>\n }\n @if (!effectiveChecked()) {\n <svg\n class=\"tn-slide-toggle__minus-icon\"\n viewBox=\"0 0 24 24\"\n width=\"16\"\n height=\"16\">\n <path d=\"M19 13H5v-2h14v2z\" fill=\"currentColor\"/>\n </svg>\n }\n </div>\n </div>\n </div>\n </div>\n\n <!-- Label after toggle -->\n @if (label() && labelPosition() === 'after') {\n <span\n class=\"tn-slide-toggle__label-text tn-slide-toggle__label-text--after\"\n tabindex=\"0\"\n role=\"button\"\n [innerHTML]=\"label() | tnLabelMarkup\"\n (click)=\"onLabelClick()\"\n (keydown.enter)=\"onLabelClick()\"\n (keydown.space)=\"onLabelClick()\">\n </span>\n }\n\n </label>\n</div>", styles: [".tn-slide-toggle{display:inline-flex;align-items:center;cursor:pointer;-webkit-user-select:none;user-select:none;line-height:1;font-family:inherit}.tn-slide-toggle__label{display:flex;align-items:center;cursor:inherit}.tn-slide-toggle__label-text ::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}.tn-slide-toggle__label-text{font-size:14px;line-height:1.4;color:var(--tn-fg1);transition:color .2s ease}.tn-slide-toggle__label-text--before{margin-right:8px}.tn-slide-toggle__label-text--after{margin-left:8px}.tn-slide-toggle__input{position:absolute;opacity:0;width:0;height:0;margin:0;pointer-events:none}.tn-slide-toggle__input:focus+.tn-slide-toggle__track{box-shadow:0 0 0 2px rgba(var(--tn-primary-rgb, 0, 123, 255),.2)}.tn-slide-toggle__bar{position:relative;display:flex;align-items:center;flex-shrink:0}.tn-slide-toggle__track{position:relative;width:52px;height:32px;border-radius:16px;background-color:var(--tn-lines);border:1px solid transparent;transition:background-color .2s ease,border-color .2s ease;overflow:hidden}.tn-slide-toggle__track-fill{position:absolute;inset:0;border-radius:inherit;background-color:var(--tn-primary, #007cba);opacity:0;transform:scaleX(0);transform-origin:left center;transition:opacity .2s ease,transform .2s ease}.tn-slide-toggle__thumb-container{position:absolute;top:50%;left:4px;transform:translateY(-50%);width:24px;height:24px;transition:transform .2s ease;z-index:1}.tn-slide-toggle__thumb{position:relative;width:24px;height:24px;border-radius:50%;background-color:var(--tn-bg2);border:1px solid var(--tn-lines);box-shadow:0 2px 4px #0003;transition:background-color .2s ease,border-color .2s ease,box-shadow .2s ease;display:flex;align-items:center;justify-content:center}.tn-slide-toggle__ripple{position:absolute;width:40px;height:40px;border-radius:50%;background-color:transparent;top:50%;left:50%;transform:translate(-50%,-50%);opacity:0;transition:opacity .2s ease,background-color .2s ease}.tn-slide-toggle__icon{position:relative;display:flex;align-items:center;justify-content:center;z-index:2;pointer-events:none}.tn-slide-toggle__check-icon,.tn-slide-toggle__minus-icon{transition:opacity .2s ease,transform .2s ease;color:var(--tn-fg2)}.tn-slide-toggle:hover:not(.tn-slide-toggle--disabled) .tn-slide-toggle__ripple{background-color:var(--tn-primary);opacity:.08}.tn-slide-toggle--checked .tn-slide-toggle__track{background-color:var(--tn-primary);opacity:.5}.tn-slide-toggle--checked .tn-slide-toggle__track-fill{opacity:1;transform:scaleX(1)}.tn-slide-toggle--checked .tn-slide-toggle__thumb-container{transform:translateY(-50%) translate(24px)}.tn-slide-toggle--checked .tn-slide-toggle__thumb{background-color:var(--tn-bg2);border-color:var(--tn-primary)}.tn-slide-toggle--checked .tn-slide-toggle__check-icon{color:var(--tn-primary)}.tn-slide-toggle--checked:hover:not(.tn-slide-toggle--disabled) .tn-slide-toggle__ripple{background-color:var(--tn-primary);opacity:.12}.tn-slide-toggle--disabled{cursor:not-allowed;opacity:.6}.tn-slide-toggle--disabled .tn-slide-toggle__label-text{color:var(--tn-alt-fg1)}.tn-slide-toggle--disabled .tn-slide-toggle__track{background-color:var(--tn-alt-bg1)}.tn-slide-toggle--disabled .tn-slide-toggle__thumb{background-color:var(--tn-alt-bg2);border-color:var(--tn-alt-bg1);box-shadow:none}.tn-slide-toggle--disabled.tn-slide-toggle--checked .tn-slide-toggle__track,.tn-slide-toggle--disabled.tn-slide-toggle--checked .tn-slide-toggle__track-fill{background-color:var(--tn-alt-bg1)}.tn-slide-toggle--disabled.tn-slide-toggle--checked .tn-slide-toggle__thumb{background-color:var(--tn-alt-bg2);border-color:var(--tn-alt-bg1)}.tn-slide-toggle--disabled.tn-slide-toggle--checked .tn-slide-toggle__check-icon{color:var(--tn-alt-fg1)}.tn-slide-toggle--disabled:hover .tn-slide-toggle__ripple{opacity:0}.tn-slide-toggle--accent{--tn-primary: var(--tn-accent)}.tn-slide-toggle--warn{--tn-primary: var(--tn-red)}.tn-slide-toggle .tn-slide-toggle__input:focus-visible+.tn-slide-toggle__track{outline:2px solid var(--tn-primary);outline-offset:2px}.tn-slide-toggle:active:not(.tn-slide-toggle--disabled) .tn-slide-toggle__ripple{background-color:var(--tn-primary);opacity:.16;transform:translate(-50%,-50%) scale(1.1)}@media(prefers-contrast:high){.tn-slide-toggle .tn-slide-toggle__track{border-color:var(--tn-fg1)}.tn-slide-toggle .tn-slide-toggle__thumb{border-width:2px}.tn-slide-toggle--disabled .tn-slide-toggle__track,.tn-slide-toggle--disabled .tn-slide-toggle__thumb{border-color:var(--tn-alt-fg1)}}@media(prefers-reduced-motion:reduce){.tn-slide-toggle .tn-slide-toggle__track,.tn-slide-toggle .tn-slide-toggle__track-fill,.tn-slide-toggle .tn-slide-toggle__thumb-container,.tn-slide-toggle .tn-slide-toggle__thumb,.tn-slide-toggle .tn-slide-toggle__ripple{transition:none}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1$2.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "ngmodule", type: FormsModule }, { kind: "ngmodule", type: A11yModule }, { kind: "directive", type: TnTestIdDirective, selector: "[tnTestId]", inputs: ["tnTestId", "tnTestIdType"] }, { kind: "pipe", type: LabelMarkupPipe, name: "tnLabelMarkup" }] });
6170
+ ], viewQueries: [{ propertyName: "toggleEl", first: true, predicate: ["toggleEl"], descendants: true, isSignal: true }], ngImport: i0, template: "<div [ngClass]=\"classes()\">\n <label class=\"tn-slide-toggle__label\" tnTestIdType=\"toggle\" [for]=\"id\" [tnTestId]=\"resolvedTestId()\">\n\n <!-- Label before toggle -->\n @if (label() && labelPosition() === 'before') {\n <span\n class=\"tn-slide-toggle__label-text tn-slide-toggle__label-text--before\"\n tabindex=\"0\"\n role=\"button\"\n [innerHTML]=\"label() | tnLabelMarkup\"\n (click)=\"onLabelClick()\"\n (keydown.enter)=\"onLabelClick()\"\n (keydown.space)=\"onLabelClick()\">\n </span>\n }\n\n <!-- Toggle track and thumb -->\n <div class=\"tn-slide-toggle__bar\">\n <input\n #toggleEl\n type=\"checkbox\"\n class=\"tn-slide-toggle__input\"\n [id]=\"id\"\n [checked]=\"effectiveChecked()\"\n [disabled]=\"isDisabled()\"\n [required]=\"required()\"\n [attr.aria-label]=\"effectiveAriaLabel()\"\n [attr.aria-labelledby]=\"ariaLabelledby()\"\n (change)=\"onToggleChange($event)\"\n />\n\n <div class=\"tn-slide-toggle__track\">\n <div class=\"tn-slide-toggle__track-fill\"></div>\n </div>\n\n <div class=\"tn-slide-toggle__thumb-container\">\n <div class=\"tn-slide-toggle__thumb\">\n <div class=\"tn-slide-toggle__ripple\"></div>\n <!-- State icon -->\n <div class=\"tn-slide-toggle__icon\">\n @if (effectiveChecked()) {\n <svg\n class=\"tn-slide-toggle__check-icon\"\n viewBox=\"0 0 24 24\"\n width=\"16\"\n height=\"16\">\n <path d=\"M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41L9 16.17z\" fill=\"currentColor\"/>\n </svg>\n }\n @if (!effectiveChecked()) {\n <svg\n class=\"tn-slide-toggle__minus-icon\"\n viewBox=\"0 0 24 24\"\n width=\"16\"\n height=\"16\">\n <path d=\"M19 13H5v-2h14v2z\" fill=\"currentColor\"/>\n </svg>\n }\n </div>\n </div>\n </div>\n </div>\n\n <!-- Label after toggle -->\n @if (label() && labelPosition() === 'after') {\n <span\n class=\"tn-slide-toggle__label-text tn-slide-toggle__label-text--after\"\n tabindex=\"0\"\n role=\"button\"\n [innerHTML]=\"label() | tnLabelMarkup\"\n (click)=\"onLabelClick()\"\n (keydown.enter)=\"onLabelClick()\"\n (keydown.space)=\"onLabelClick()\">\n </span>\n }\n\n </label>\n</div>", styles: [":host(.tn-slide-toggle-host--full-width){display:block;width:100%}.tn-slide-toggle{display:inline-flex;align-items:center;cursor:pointer;-webkit-user-select:none;user-select:none;line-height:1;font-family:inherit}.tn-slide-toggle__label{display:flex;align-items:center;cursor:inherit}.tn-slide-toggle--full-width{display:flex;width:100%}.tn-slide-toggle--full-width .tn-slide-toggle__label{justify-content:space-between;width:100%}.tn-slide-toggle__label-text ::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}.tn-slide-toggle__label-text{font-size:14px;line-height:1.4;color:var(--tn-fg1);transition:color .2s ease}.tn-slide-toggle__label-text--before{margin-right:8px}.tn-slide-toggle__label-text--after{margin-left:8px}.tn-slide-toggle__input{position:absolute;opacity:0;width:0;height:0;margin:0;pointer-events:none}.tn-slide-toggle__input:focus+.tn-slide-toggle__track{box-shadow:0 0 0 2px rgba(var(--tn-primary-rgb, 0, 123, 255),.2)}.tn-slide-toggle__bar{position:relative;display:flex;align-items:center;flex-shrink:0}.tn-slide-toggle__track{position:relative;width:52px;height:32px;border-radius:16px;background-color:var(--tn-lines);border:1px solid transparent;transition:background-color .2s ease,border-color .2s ease;overflow:hidden}.tn-slide-toggle__track-fill{position:absolute;inset:0;border-radius:inherit;background-color:var(--tn-primary, #007cba);opacity:0;transform:scaleX(0);transform-origin:left center;transition:opacity .2s ease,transform .2s ease}.tn-slide-toggle__thumb-container{position:absolute;top:50%;left:4px;transform:translateY(-50%);width:24px;height:24px;transition:transform .2s ease;z-index:1}.tn-slide-toggle__thumb{position:relative;width:24px;height:24px;border-radius:50%;background-color:var(--tn-bg2);border:1px solid var(--tn-lines);box-shadow:0 2px 4px #0003;transition:background-color .2s ease,border-color .2s ease,box-shadow .2s ease;display:flex;align-items:center;justify-content:center}.tn-slide-toggle__ripple{position:absolute;width:40px;height:40px;border-radius:50%;background-color:transparent;top:50%;left:50%;transform:translate(-50%,-50%);opacity:0;transition:opacity .2s ease,background-color .2s ease}.tn-slide-toggle__icon{position:relative;display:flex;align-items:center;justify-content:center;z-index:2;pointer-events:none}.tn-slide-toggle__check-icon,.tn-slide-toggle__minus-icon{transition:opacity .2s ease,transform .2s ease;color:var(--tn-fg2)}.tn-slide-toggle:hover:not(.tn-slide-toggle--disabled) .tn-slide-toggle__ripple{background-color:var(--tn-primary);opacity:.08}.tn-slide-toggle--checked .tn-slide-toggle__track{background-color:var(--tn-primary);opacity:.5}.tn-slide-toggle--checked .tn-slide-toggle__track-fill{opacity:1;transform:scaleX(1)}.tn-slide-toggle--checked .tn-slide-toggle__thumb-container{transform:translateY(-50%) translate(24px)}.tn-slide-toggle--checked .tn-slide-toggle__thumb{background-color:var(--tn-bg2);border-color:var(--tn-primary)}.tn-slide-toggle--checked .tn-slide-toggle__check-icon{color:var(--tn-primary)}.tn-slide-toggle--checked:hover:not(.tn-slide-toggle--disabled) .tn-slide-toggle__ripple{background-color:var(--tn-primary);opacity:.12}.tn-slide-toggle--disabled{cursor:not-allowed;opacity:.6}.tn-slide-toggle--disabled .tn-slide-toggle__label-text{color:var(--tn-alt-fg1)}.tn-slide-toggle--disabled .tn-slide-toggle__track{background-color:var(--tn-alt-bg1)}.tn-slide-toggle--disabled .tn-slide-toggle__thumb{background-color:var(--tn-alt-bg2);border-color:var(--tn-alt-bg1);box-shadow:none}.tn-slide-toggle--disabled.tn-slide-toggle--checked .tn-slide-toggle__track,.tn-slide-toggle--disabled.tn-slide-toggle--checked .tn-slide-toggle__track-fill{background-color:var(--tn-alt-bg1)}.tn-slide-toggle--disabled.tn-slide-toggle--checked .tn-slide-toggle__thumb{background-color:var(--tn-alt-bg2);border-color:var(--tn-alt-bg1)}.tn-slide-toggle--disabled.tn-slide-toggle--checked .tn-slide-toggle__check-icon{color:var(--tn-alt-fg1)}.tn-slide-toggle--disabled:hover .tn-slide-toggle__ripple{opacity:0}.tn-slide-toggle--accent{--tn-primary: var(--tn-accent)}.tn-slide-toggle--warn{--tn-primary: var(--tn-red)}.tn-slide-toggle .tn-slide-toggle__input:focus-visible+.tn-slide-toggle__track{outline:2px solid var(--tn-primary);outline-offset:2px}.tn-slide-toggle:active:not(.tn-slide-toggle--disabled) .tn-slide-toggle__ripple{background-color:var(--tn-primary);opacity:.16;transform:translate(-50%,-50%) scale(1.1)}@media(prefers-contrast:high){.tn-slide-toggle .tn-slide-toggle__track{border-color:var(--tn-fg1)}.tn-slide-toggle .tn-slide-toggle__thumb{border-width:2px}.tn-slide-toggle--disabled .tn-slide-toggle__track,.tn-slide-toggle--disabled .tn-slide-toggle__thumb{border-color:var(--tn-alt-fg1)}}@media(prefers-reduced-motion:reduce){.tn-slide-toggle .tn-slide-toggle__track,.tn-slide-toggle .tn-slide-toggle__track-fill,.tn-slide-toggle .tn-slide-toggle__thumb-container,.tn-slide-toggle .tn-slide-toggle__thumb,.tn-slide-toggle .tn-slide-toggle__ripple{transition:none}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1$2.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "ngmodule", type: FormsModule }, { kind: "ngmodule", type: A11yModule }, { kind: "directive", type: TnTestIdDirective, selector: "[tnTestId]", inputs: ["tnTestId", "tnTestIdType"] }, { kind: "pipe", type: LabelMarkupPipe, name: "tnLabelMarkup" }] });
6155
6171
  }
6156
6172
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnSlideToggleComponent, decorators: [{
6157
6173
  type: Component,
6158
- args: [{ selector: 'tn-slide-toggle', standalone: true, imports: [CommonModule, FormsModule, A11yModule, TnTestIdDirective, LabelMarkupPipe], providers: [
6174
+ args: [{ selector: 'tn-slide-toggle', standalone: true, imports: [CommonModule, FormsModule, A11yModule, TnTestIdDirective, LabelMarkupPipe], host: {
6175
+ // The `.tn-slide-toggle` class sits on an inner <div>; the host element is
6176
+ // inline by default, so it has to be stretched too or the inner width has
6177
+ // nothing to fill.
6178
+ '[class.tn-slide-toggle-host--full-width]': 'fullWidth()',
6179
+ }, providers: [
6159
6180
  {
6160
6181
  provide: NG_VALUE_ACCESSOR,
6161
6182
  useExisting: forwardRef(() => TnSlideToggleComponent),
6162
6183
  multi: true
6163
6184
  }
6164
- ], template: "<div [ngClass]=\"classes()\">\n <label class=\"tn-slide-toggle__label\" tnTestIdType=\"toggle\" [for]=\"id\" [tnTestId]=\"resolvedTestId()\">\n\n <!-- Label before toggle -->\n @if (label() && labelPosition() === 'before') {\n <span\n class=\"tn-slide-toggle__label-text tn-slide-toggle__label-text--before\"\n tabindex=\"0\"\n role=\"button\"\n [innerHTML]=\"label() | tnLabelMarkup\"\n (click)=\"onLabelClick()\"\n (keydown.enter)=\"onLabelClick()\"\n (keydown.space)=\"onLabelClick()\">\n </span>\n }\n\n <!-- Toggle track and thumb -->\n <div class=\"tn-slide-toggle__bar\">\n <input\n #toggleEl\n type=\"checkbox\"\n class=\"tn-slide-toggle__input\"\n [id]=\"id\"\n [checked]=\"effectiveChecked()\"\n [disabled]=\"isDisabled()\"\n [required]=\"required()\"\n [attr.aria-label]=\"effectiveAriaLabel()\"\n [attr.aria-labelledby]=\"ariaLabelledby()\"\n (change)=\"onToggleChange($event)\"\n />\n\n <div class=\"tn-slide-toggle__track\">\n <div class=\"tn-slide-toggle__track-fill\"></div>\n </div>\n\n <div class=\"tn-slide-toggle__thumb-container\">\n <div class=\"tn-slide-toggle__thumb\">\n <div class=\"tn-slide-toggle__ripple\"></div>\n <!-- State icon -->\n <div class=\"tn-slide-toggle__icon\">\n @if (effectiveChecked()) {\n <svg\n class=\"tn-slide-toggle__check-icon\"\n viewBox=\"0 0 24 24\"\n width=\"16\"\n height=\"16\">\n <path d=\"M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41L9 16.17z\" fill=\"currentColor\"/>\n </svg>\n }\n @if (!effectiveChecked()) {\n <svg\n class=\"tn-slide-toggle__minus-icon\"\n viewBox=\"0 0 24 24\"\n width=\"16\"\n height=\"16\">\n <path d=\"M19 13H5v-2h14v2z\" fill=\"currentColor\"/>\n </svg>\n }\n </div>\n </div>\n </div>\n </div>\n\n <!-- Label after toggle -->\n @if (label() && labelPosition() === 'after') {\n <span\n class=\"tn-slide-toggle__label-text tn-slide-toggle__label-text--after\"\n tabindex=\"0\"\n role=\"button\"\n [innerHTML]=\"label() | tnLabelMarkup\"\n (click)=\"onLabelClick()\"\n (keydown.enter)=\"onLabelClick()\"\n (keydown.space)=\"onLabelClick()\">\n </span>\n }\n\n </label>\n</div>", styles: [".tn-slide-toggle{display:inline-flex;align-items:center;cursor:pointer;-webkit-user-select:none;user-select:none;line-height:1;font-family:inherit}.tn-slide-toggle__label{display:flex;align-items:center;cursor:inherit}.tn-slide-toggle__label-text ::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}.tn-slide-toggle__label-text{font-size:14px;line-height:1.4;color:var(--tn-fg1);transition:color .2s ease}.tn-slide-toggle__label-text--before{margin-right:8px}.tn-slide-toggle__label-text--after{margin-left:8px}.tn-slide-toggle__input{position:absolute;opacity:0;width:0;height:0;margin:0;pointer-events:none}.tn-slide-toggle__input:focus+.tn-slide-toggle__track{box-shadow:0 0 0 2px rgba(var(--tn-primary-rgb, 0, 123, 255),.2)}.tn-slide-toggle__bar{position:relative;display:flex;align-items:center;flex-shrink:0}.tn-slide-toggle__track{position:relative;width:52px;height:32px;border-radius:16px;background-color:var(--tn-lines);border:1px solid transparent;transition:background-color .2s ease,border-color .2s ease;overflow:hidden}.tn-slide-toggle__track-fill{position:absolute;inset:0;border-radius:inherit;background-color:var(--tn-primary, #007cba);opacity:0;transform:scaleX(0);transform-origin:left center;transition:opacity .2s ease,transform .2s ease}.tn-slide-toggle__thumb-container{position:absolute;top:50%;left:4px;transform:translateY(-50%);width:24px;height:24px;transition:transform .2s ease;z-index:1}.tn-slide-toggle__thumb{position:relative;width:24px;height:24px;border-radius:50%;background-color:var(--tn-bg2);border:1px solid var(--tn-lines);box-shadow:0 2px 4px #0003;transition:background-color .2s ease,border-color .2s ease,box-shadow .2s ease;display:flex;align-items:center;justify-content:center}.tn-slide-toggle__ripple{position:absolute;width:40px;height:40px;border-radius:50%;background-color:transparent;top:50%;left:50%;transform:translate(-50%,-50%);opacity:0;transition:opacity .2s ease,background-color .2s ease}.tn-slide-toggle__icon{position:relative;display:flex;align-items:center;justify-content:center;z-index:2;pointer-events:none}.tn-slide-toggle__check-icon,.tn-slide-toggle__minus-icon{transition:opacity .2s ease,transform .2s ease;color:var(--tn-fg2)}.tn-slide-toggle:hover:not(.tn-slide-toggle--disabled) .tn-slide-toggle__ripple{background-color:var(--tn-primary);opacity:.08}.tn-slide-toggle--checked .tn-slide-toggle__track{background-color:var(--tn-primary);opacity:.5}.tn-slide-toggle--checked .tn-slide-toggle__track-fill{opacity:1;transform:scaleX(1)}.tn-slide-toggle--checked .tn-slide-toggle__thumb-container{transform:translateY(-50%) translate(24px)}.tn-slide-toggle--checked .tn-slide-toggle__thumb{background-color:var(--tn-bg2);border-color:var(--tn-primary)}.tn-slide-toggle--checked .tn-slide-toggle__check-icon{color:var(--tn-primary)}.tn-slide-toggle--checked:hover:not(.tn-slide-toggle--disabled) .tn-slide-toggle__ripple{background-color:var(--tn-primary);opacity:.12}.tn-slide-toggle--disabled{cursor:not-allowed;opacity:.6}.tn-slide-toggle--disabled .tn-slide-toggle__label-text{color:var(--tn-alt-fg1)}.tn-slide-toggle--disabled .tn-slide-toggle__track{background-color:var(--tn-alt-bg1)}.tn-slide-toggle--disabled .tn-slide-toggle__thumb{background-color:var(--tn-alt-bg2);border-color:var(--tn-alt-bg1);box-shadow:none}.tn-slide-toggle--disabled.tn-slide-toggle--checked .tn-slide-toggle__track,.tn-slide-toggle--disabled.tn-slide-toggle--checked .tn-slide-toggle__track-fill{background-color:var(--tn-alt-bg1)}.tn-slide-toggle--disabled.tn-slide-toggle--checked .tn-slide-toggle__thumb{background-color:var(--tn-alt-bg2);border-color:var(--tn-alt-bg1)}.tn-slide-toggle--disabled.tn-slide-toggle--checked .tn-slide-toggle__check-icon{color:var(--tn-alt-fg1)}.tn-slide-toggle--disabled:hover .tn-slide-toggle__ripple{opacity:0}.tn-slide-toggle--accent{--tn-primary: var(--tn-accent)}.tn-slide-toggle--warn{--tn-primary: var(--tn-red)}.tn-slide-toggle .tn-slide-toggle__input:focus-visible+.tn-slide-toggle__track{outline:2px solid var(--tn-primary);outline-offset:2px}.tn-slide-toggle:active:not(.tn-slide-toggle--disabled) .tn-slide-toggle__ripple{background-color:var(--tn-primary);opacity:.16;transform:translate(-50%,-50%) scale(1.1)}@media(prefers-contrast:high){.tn-slide-toggle .tn-slide-toggle__track{border-color:var(--tn-fg1)}.tn-slide-toggle .tn-slide-toggle__thumb{border-width:2px}.tn-slide-toggle--disabled .tn-slide-toggle__track,.tn-slide-toggle--disabled .tn-slide-toggle__thumb{border-color:var(--tn-alt-fg1)}}@media(prefers-reduced-motion:reduce){.tn-slide-toggle .tn-slide-toggle__track,.tn-slide-toggle .tn-slide-toggle__track-fill,.tn-slide-toggle .tn-slide-toggle__thumb-container,.tn-slide-toggle .tn-slide-toggle__thumb,.tn-slide-toggle .tn-slide-toggle__ripple{transition:none}}\n"] }]
6165
- }], propDecorators: { toggleEl: [{ type: i0.ViewChild, args: ['toggleEl', { isSignal: true }] }], labelPosition: [{ type: i0.Input, args: [{ isSignal: true, alias: "labelPosition", required: false }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], required: [{ type: i0.Input, args: [{ isSignal: true, alias: "required", required: false }] }], color: [{ type: i0.Input, args: [{ isSignal: true, alias: "color", required: false }] }], testId: [{ type: i0.Input, args: [{ isSignal: true, alias: "testId", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], ariaLabelledby: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabelledby", required: false }] }], checked: [{ type: i0.Input, args: [{ isSignal: true, alias: "checked", required: false }] }], change: [{ type: i0.Output, args: ["change"] }], toggleChange: [{ type: i0.Output, args: ["toggleChange"] }] } });
6185
+ ], template: "<div [ngClass]=\"classes()\">\n <label class=\"tn-slide-toggle__label\" tnTestIdType=\"toggle\" [for]=\"id\" [tnTestId]=\"resolvedTestId()\">\n\n <!-- Label before toggle -->\n @if (label() && labelPosition() === 'before') {\n <span\n class=\"tn-slide-toggle__label-text tn-slide-toggle__label-text--before\"\n tabindex=\"0\"\n role=\"button\"\n [innerHTML]=\"label() | tnLabelMarkup\"\n (click)=\"onLabelClick()\"\n (keydown.enter)=\"onLabelClick()\"\n (keydown.space)=\"onLabelClick()\">\n </span>\n }\n\n <!-- Toggle track and thumb -->\n <div class=\"tn-slide-toggle__bar\">\n <input\n #toggleEl\n type=\"checkbox\"\n class=\"tn-slide-toggle__input\"\n [id]=\"id\"\n [checked]=\"effectiveChecked()\"\n [disabled]=\"isDisabled()\"\n [required]=\"required()\"\n [attr.aria-label]=\"effectiveAriaLabel()\"\n [attr.aria-labelledby]=\"ariaLabelledby()\"\n (change)=\"onToggleChange($event)\"\n />\n\n <div class=\"tn-slide-toggle__track\">\n <div class=\"tn-slide-toggle__track-fill\"></div>\n </div>\n\n <div class=\"tn-slide-toggle__thumb-container\">\n <div class=\"tn-slide-toggle__thumb\">\n <div class=\"tn-slide-toggle__ripple\"></div>\n <!-- State icon -->\n <div class=\"tn-slide-toggle__icon\">\n @if (effectiveChecked()) {\n <svg\n class=\"tn-slide-toggle__check-icon\"\n viewBox=\"0 0 24 24\"\n width=\"16\"\n height=\"16\">\n <path d=\"M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41L9 16.17z\" fill=\"currentColor\"/>\n </svg>\n }\n @if (!effectiveChecked()) {\n <svg\n class=\"tn-slide-toggle__minus-icon\"\n viewBox=\"0 0 24 24\"\n width=\"16\"\n height=\"16\">\n <path d=\"M19 13H5v-2h14v2z\" fill=\"currentColor\"/>\n </svg>\n }\n </div>\n </div>\n </div>\n </div>\n\n <!-- Label after toggle -->\n @if (label() && labelPosition() === 'after') {\n <span\n class=\"tn-slide-toggle__label-text tn-slide-toggle__label-text--after\"\n tabindex=\"0\"\n role=\"button\"\n [innerHTML]=\"label() | tnLabelMarkup\"\n (click)=\"onLabelClick()\"\n (keydown.enter)=\"onLabelClick()\"\n (keydown.space)=\"onLabelClick()\">\n </span>\n }\n\n </label>\n</div>", styles: [":host(.tn-slide-toggle-host--full-width){display:block;width:100%}.tn-slide-toggle{display:inline-flex;align-items:center;cursor:pointer;-webkit-user-select:none;user-select:none;line-height:1;font-family:inherit}.tn-slide-toggle__label{display:flex;align-items:center;cursor:inherit}.tn-slide-toggle--full-width{display:flex;width:100%}.tn-slide-toggle--full-width .tn-slide-toggle__label{justify-content:space-between;width:100%}.tn-slide-toggle__label-text ::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}.tn-slide-toggle__label-text{font-size:14px;line-height:1.4;color:var(--tn-fg1);transition:color .2s ease}.tn-slide-toggle__label-text--before{margin-right:8px}.tn-slide-toggle__label-text--after{margin-left:8px}.tn-slide-toggle__input{position:absolute;opacity:0;width:0;height:0;margin:0;pointer-events:none}.tn-slide-toggle__input:focus+.tn-slide-toggle__track{box-shadow:0 0 0 2px rgba(var(--tn-primary-rgb, 0, 123, 255),.2)}.tn-slide-toggle__bar{position:relative;display:flex;align-items:center;flex-shrink:0}.tn-slide-toggle__track{position:relative;width:52px;height:32px;border-radius:16px;background-color:var(--tn-lines);border:1px solid transparent;transition:background-color .2s ease,border-color .2s ease;overflow:hidden}.tn-slide-toggle__track-fill{position:absolute;inset:0;border-radius:inherit;background-color:var(--tn-primary, #007cba);opacity:0;transform:scaleX(0);transform-origin:left center;transition:opacity .2s ease,transform .2s ease}.tn-slide-toggle__thumb-container{position:absolute;top:50%;left:4px;transform:translateY(-50%);width:24px;height:24px;transition:transform .2s ease;z-index:1}.tn-slide-toggle__thumb{position:relative;width:24px;height:24px;border-radius:50%;background-color:var(--tn-bg2);border:1px solid var(--tn-lines);box-shadow:0 2px 4px #0003;transition:background-color .2s ease,border-color .2s ease,box-shadow .2s ease;display:flex;align-items:center;justify-content:center}.tn-slide-toggle__ripple{position:absolute;width:40px;height:40px;border-radius:50%;background-color:transparent;top:50%;left:50%;transform:translate(-50%,-50%);opacity:0;transition:opacity .2s ease,background-color .2s ease}.tn-slide-toggle__icon{position:relative;display:flex;align-items:center;justify-content:center;z-index:2;pointer-events:none}.tn-slide-toggle__check-icon,.tn-slide-toggle__minus-icon{transition:opacity .2s ease,transform .2s ease;color:var(--tn-fg2)}.tn-slide-toggle:hover:not(.tn-slide-toggle--disabled) .tn-slide-toggle__ripple{background-color:var(--tn-primary);opacity:.08}.tn-slide-toggle--checked .tn-slide-toggle__track{background-color:var(--tn-primary);opacity:.5}.tn-slide-toggle--checked .tn-slide-toggle__track-fill{opacity:1;transform:scaleX(1)}.tn-slide-toggle--checked .tn-slide-toggle__thumb-container{transform:translateY(-50%) translate(24px)}.tn-slide-toggle--checked .tn-slide-toggle__thumb{background-color:var(--tn-bg2);border-color:var(--tn-primary)}.tn-slide-toggle--checked .tn-slide-toggle__check-icon{color:var(--tn-primary)}.tn-slide-toggle--checked:hover:not(.tn-slide-toggle--disabled) .tn-slide-toggle__ripple{background-color:var(--tn-primary);opacity:.12}.tn-slide-toggle--disabled{cursor:not-allowed;opacity:.6}.tn-slide-toggle--disabled .tn-slide-toggle__label-text{color:var(--tn-alt-fg1)}.tn-slide-toggle--disabled .tn-slide-toggle__track{background-color:var(--tn-alt-bg1)}.tn-slide-toggle--disabled .tn-slide-toggle__thumb{background-color:var(--tn-alt-bg2);border-color:var(--tn-alt-bg1);box-shadow:none}.tn-slide-toggle--disabled.tn-slide-toggle--checked .tn-slide-toggle__track,.tn-slide-toggle--disabled.tn-slide-toggle--checked .tn-slide-toggle__track-fill{background-color:var(--tn-alt-bg1)}.tn-slide-toggle--disabled.tn-slide-toggle--checked .tn-slide-toggle__thumb{background-color:var(--tn-alt-bg2);border-color:var(--tn-alt-bg1)}.tn-slide-toggle--disabled.tn-slide-toggle--checked .tn-slide-toggle__check-icon{color:var(--tn-alt-fg1)}.tn-slide-toggle--disabled:hover .tn-slide-toggle__ripple{opacity:0}.tn-slide-toggle--accent{--tn-primary: var(--tn-accent)}.tn-slide-toggle--warn{--tn-primary: var(--tn-red)}.tn-slide-toggle .tn-slide-toggle__input:focus-visible+.tn-slide-toggle__track{outline:2px solid var(--tn-primary);outline-offset:2px}.tn-slide-toggle:active:not(.tn-slide-toggle--disabled) .tn-slide-toggle__ripple{background-color:var(--tn-primary);opacity:.16;transform:translate(-50%,-50%) scale(1.1)}@media(prefers-contrast:high){.tn-slide-toggle .tn-slide-toggle__track{border-color:var(--tn-fg1)}.tn-slide-toggle .tn-slide-toggle__thumb{border-width:2px}.tn-slide-toggle--disabled .tn-slide-toggle__track,.tn-slide-toggle--disabled .tn-slide-toggle__thumb{border-color:var(--tn-alt-fg1)}}@media(prefers-reduced-motion:reduce){.tn-slide-toggle .tn-slide-toggle__track,.tn-slide-toggle .tn-slide-toggle__track-fill,.tn-slide-toggle .tn-slide-toggle__thumb-container,.tn-slide-toggle .tn-slide-toggle__thumb,.tn-slide-toggle .tn-slide-toggle__ripple{transition:none}}\n"] }]
6186
+ }], propDecorators: { toggleEl: [{ type: i0.ViewChild, args: ['toggleEl', { isSignal: true }] }], labelPosition: [{ type: i0.Input, args: [{ isSignal: true, alias: "labelPosition", required: false }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], fullWidth: [{ type: i0.Input, args: [{ isSignal: true, alias: "fullWidth", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], required: [{ type: i0.Input, args: [{ isSignal: true, alias: "required", required: false }] }], color: [{ type: i0.Input, args: [{ isSignal: true, alias: "color", required: false }] }], testId: [{ type: i0.Input, args: [{ isSignal: true, alias: "testId", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], ariaLabelledby: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabelledby", required: false }] }], checked: [{ type: i0.Input, args: [{ isSignal: true, alias: "checked", required: false }] }], change: [{ type: i0.Output, args: ["change"] }], toggleChange: [{ type: i0.Output, args: ["toggleChange"] }] } });
6166
6187
 
6167
6188
  class TnCardComponent {
6168
6189
  iconRegistry = inject(TnIconRegistryService);
@@ -6271,7 +6292,7 @@ class TnCardComponent {
6271
6292
  return type ? `tn-card__status--${type}` : 'tn-card__status--neutral';
6272
6293
  }
6273
6294
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnCardComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
6274
- 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 }, titleRouterLink: { classPropertyName: "titleRouterLink", publicName: "titleRouterLink", isSignal: true, isRequired: false, transformFunction: null }, titleQueryParams: { classPropertyName: "titleQueryParams", publicName: "titleQueryParams", isSignal: true, isRequired: false, transformFunction: null }, titleTooltip: { classPropertyName: "titleTooltip", publicName: "titleTooltip", 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 class=\"tn-card__title\">\n @if (isTitleRouterLink()) {\n <a\n class=\"tn-card__title-link\"\n [routerLink]=\"titleRouterLink()\"\n [queryParams]=\"titleQueryParams()\">\n {{ title() }}\n <tn-icon class=\"tn-card__title-link-icon\" name=\"open-in-new\" library=\"mdi\" size=\"sm\" [attr.aria-hidden]=\"true\" />\n </a>\n } @else if (titleLink()) {\n <span\n class=\"tn-card__title-link\"\n role=\"button\"\n tabindex=\"0\"\n (click)=\"onTitleClick()\"\n (keydown.enter)=\"onTitleClick()\"\n (keydown.space)=\"onTitleClick(); $event.preventDefault()\">\n {{ title() }}\n <tn-icon class=\"tn-card__title-link-icon\" name=\"open-in-new\" library=\"mdi\" size=\"sm\" [attr.aria-hidden]=\"true\" />\n </span>\n } @else {\n {{ title() }}\n }\n\n @if (titleTooltip(); as tooltip) {\n <button\n type=\"button\"\n class=\"tn-card__title-tooltip\"\n aria-label=\"More information\"\n [tnTooltip]=\"tooltip\">\n <tn-icon name=\"help-circle\" library=\"mdi\" size=\"sm\" [attr.aria-hidden]=\"true\" />\n </button>\n }\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{display:inline-flex;align-items:center;gap:8px;margin:0;font-size:1.125rem;font-weight:600;color:var(--tn-fg1, #1f2937);line-height:1.5}.tn-card__title-link{display:inline-flex;align-items:center;gap:4px;color:inherit;cursor:pointer;text-decoration:none;transition:color .2s ease}.tn-card__title-link:hover{color:var(--tn-primary, #2563eb)}.tn-card__title-link-icon{flex-shrink:0}.tn-card__title-tooltip{display:inline-flex;align-items:center;justify-content:center;padding:0;border:none;background:transparent;color:var(--tn-fg2, #6c757d);cursor:help;line-height:0}.tn-card__title-tooltip:hover,.tn-card__title-tooltip:focus-visible{color:var(--tn-fg1, #1f2937)}.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: "directive", type: RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "routerLink"] }, { 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: TnIconComponent, selector: "tn-icon", inputs: ["name", "size", "color", "tooltip", "ariaLabel", "library", "testId", "fullSize", "customSize"] }, { 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"] }, { kind: "directive", type: TnTooltipDirective, selector: "[tnTooltip]", inputs: ["tnTooltip", "tnTooltipPosition", "tnTooltipDisabled", "tnTooltipShowDelay", "tnTooltipHideDelay", "tnTooltipClass"] }] });
6295
+ 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 }, titleRouterLink: { classPropertyName: "titleRouterLink", publicName: "titleRouterLink", isSignal: true, isRequired: false, transformFunction: null }, titleQueryParams: { classPropertyName: "titleQueryParams", publicName: "titleQueryParams", isSignal: true, isRequired: false, transformFunction: null }, titleTooltip: { classPropertyName: "titleTooltip", publicName: "titleTooltip", 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 class=\"tn-card__title\">\n @if (isTitleRouterLink()) {\n <a\n class=\"tn-card__title-link\"\n [routerLink]=\"titleRouterLink()\"\n [queryParams]=\"titleQueryParams()\">\n {{ title() }}\n <tn-icon class=\"tn-card__title-link-icon\" name=\"open-in-new\" library=\"mdi\" size=\"sm\" [attr.aria-hidden]=\"true\" />\n </a>\n } @else if (titleLink()) {\n <span\n class=\"tn-card__title-link\"\n role=\"button\"\n tabindex=\"0\"\n (click)=\"onTitleClick()\"\n (keydown.enter)=\"onTitleClick()\"\n (keydown.space)=\"onTitleClick(); $event.preventDefault()\">\n {{ title() }}\n <tn-icon class=\"tn-card__title-link-icon\" name=\"open-in-new\" library=\"mdi\" size=\"sm\" [attr.aria-hidden]=\"true\" />\n </span>\n } @else {\n {{ title() }}\n }\n\n @if (titleTooltip(); as tooltip) {\n <button\n type=\"button\"\n class=\"tn-card__title-tooltip\"\n aria-label=\"More information\"\n [tnTooltip]=\"tooltip\">\n <tn-icon name=\"help-circle\" library=\"mdi\" size=\"sm\" [attr.aria-hidden]=\"true\" />\n </button>\n }\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{display:inline-flex;align-items:center;gap:8px;margin:0;font-size:1.125rem;font-weight:600;color:var(--tn-fg1, #1f2937);line-height:1.5}.tn-card__title-link{display:inline-flex;align-items:center;gap:4px;color:inherit;cursor:pointer;text-decoration:none;transition:color .2s ease}.tn-card__title-link:hover{color:var(--tn-primary, #2563eb)}.tn-card__title-link-icon{flex-shrink:0}.tn-card__title-tooltip{display:inline-flex;align-items:center;justify-content:center;padding:0;border:none;background:transparent;color:var(--tn-fg2, #6c757d);cursor:help;line-height:0}.tn-card__title-tooltip:hover,.tn-card__title-tooltip:focus-visible{color:var(--tn-fg1, #1f2937)}.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: "directive", type: RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "routerLink"] }, { kind: "component", type: TnButtonComponent, selector: "tn-button", inputs: ["primary", "color", "variant", "backgroundColor", "label", "icon", "iconPosition", "disabled", "fullWidth", "type", "testId", "href", "routerLink", "queryParams", "fragment", "target", "rel", "ariaLabel"], outputs: ["onClick"] }, { kind: "component", type: TnIconComponent, selector: "tn-icon", inputs: ["name", "size", "color", "tooltip", "ariaLabel", "library", "testId", "fullSize", "customSize"] }, { 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", "fullWidth", "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"] }, { kind: "directive", type: TnTooltipDirective, selector: "[tnTooltip]", inputs: ["tnTooltip", "tnTooltipPosition", "tnTooltipDisabled", "tnTooltipShowDelay", "tnTooltipHideDelay", "tnTooltipClass"] }] });
6275
6296
  }
6276
6297
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnCardComponent, decorators: [{
6277
6298
  type: Component,
@@ -10151,77 +10172,24 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
10151
10172
  }, template: "<ng-content />", styles: [".tn-list{display:block;padding:8px 0;margin:0;background-color:var(--tn-bg2);border-radius:4px}.tn-list--dense{padding:4px 0}.tn-list--dense ::ng-deep .tn-list-item{min-height:40px;padding:4px 16px}.tn-list--disabled{opacity:.6;pointer-events:none}::ng-deep .tn-divider{border-top:1px solid var(--tn-lines);margin:0;height:1px}\n"] }]
10152
10173
  }], propDecorators: { dense: [{ type: i0.Input, args: [{ isSignal: true, alias: "dense", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }] } });
10153
10174
 
10154
- class TnListItemComponent {
10155
- disabled = input(false, ...(ngDevMode ? [{ debugName: "disabled" }] : []));
10156
- clickable = input(false, ...(ngDevMode ? [{ debugName: "clickable" }] : []));
10157
- itemClick = output();
10158
- hasLeadingContent = signal(false, ...(ngDevMode ? [{ debugName: "hasLeadingContent" }] : []));
10159
- hasSecondaryTextContent = signal(false, ...(ngDevMode ? [{ debugName: "hasSecondaryTextContent" }] : []));
10160
- hasTrailingContent = signal(false, ...(ngDevMode ? [{ debugName: "hasTrailingContent" }] : []));
10161
- hasPrimaryTextDirective = signal(false, ...(ngDevMode ? [{ debugName: "hasPrimaryTextDirective" }] : []));
10162
- elementRef = inject(ElementRef);
10163
- ngAfterContentInit() {
10164
- this.checkContentProjection();
10165
- }
10166
- checkContentProjection() {
10167
- const element = this.elementRef.nativeElement;
10168
- // Check for leading content (icons/avatars)
10169
- this.hasLeadingContent.set(!!(element.querySelector('[tnListIcon]') ||
10170
- element.querySelector('[tnListAvatar]')));
10171
- // Check for secondary text content
10172
- this.hasSecondaryTextContent.set(!!(element.querySelector('[tnListItemLine]') ||
10173
- element.querySelector('[tnListItemSecondary]')));
10174
- // Check for trailing content
10175
- this.hasTrailingContent.set(!!element.querySelector('[tnListItemTrailing]'));
10176
- // Check for primary text directive
10177
- this.hasPrimaryTextDirective.set(!!(element.querySelector('[tnListItemTitle]') ||
10178
- element.querySelector('[tnListItemPrimary]')));
10179
- }
10180
- hasSecondaryText = computed(() => {
10181
- return this.hasSecondaryTextContent();
10182
- }, ...(ngDevMode ? [{ debugName: "hasSecondaryText" }] : []));
10183
- hasThirdText = computed(() => {
10184
- // For now, we'll consider third line as having more than one secondary line
10185
- const element = this.elementRef.nativeElement;
10186
- const secondaryElements = element.querySelectorAll('[tnListItemLine], [tnListItemSecondary]');
10187
- return secondaryElements.length > 1;
10188
- }, ...(ngDevMode ? [{ debugName: "hasThirdText" }] : []));
10189
- onClick(event) {
10190
- if (!this.disabled() && this.clickable()) {
10191
- this.itemClick.emit(event);
10192
- }
10193
- }
10194
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnListItemComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
10195
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.0", type: TnListItemComponent, isStandalone: true, selector: "tn-list-item", inputs: { disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, clickable: { classPropertyName: "clickable", publicName: "clickable", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { itemClick: "itemClick" }, host: { attributes: { "role": "listitem" }, listeners: { "click": "onClick($event)" }, properties: { "class.tn-list-item--disabled": "disabled()", "class.tn-list-item--clickable": "clickable()", "class.tn-list-item--two-line": "hasSecondaryText()", "class.tn-list-item--three-line": "hasThirdText()" }, classAttribute: "tn-list-item" }, ngImport: i0, template: "<div class=\"tn-list-item__content\">\n <!-- Leading icon/avatar slot -->\n @if (hasLeadingContent()) {\n <div class=\"tn-list-item__leading\">\n <ng-content select=\"[tnListIcon], [tnListAvatar]\" />\n </div>\n }\n\n <!-- Text content -->\n <div class=\"tn-list-item__text\">\n <!-- Primary text -->\n <div class=\"tn-list-item__primary-text\">\n <ng-content select=\"[tnListItemTitle], [tnListItemPrimary]\" />\n @if (!hasPrimaryTextDirective()) {\n <ng-content />\n }\n </div>\n\n <!-- Secondary text -->\n @if (hasSecondaryTextContent()) {\n <div class=\"tn-list-item__secondary-text\">\n <ng-content select=\"[tnListItemLine], [tnListItemSecondary]\" />\n </div>\n }\n </div>\n\n <!-- Trailing content slot -->\n @if (hasTrailingContent()) {\n <div class=\"tn-list-item__trailing\">\n <ng-content select=\"[tnListItemTrailing]\" />\n </div>\n }\n</div>", styles: [".tn-list-item{display:block;position:relative;min-height:48px;padding:0;cursor:default;text-decoration:none;color:var(--tn-fg1);transition:background-color .2s ease}.tn-list-item__content{display:flex;align-items:center;padding:8px 16px;min-height:inherit;box-sizing:border-box}.tn-list-item__leading{flex-shrink:0;margin-right:16px;display:flex;align-items:center;justify-content:center}.tn-list-item__text{flex:1;min-width:0;display:flex;flex-direction:column;justify-content:center}.tn-list-item__primary-text{font-size:1rem;font-weight:400;line-height:1.5;color:var(--tn-fg1);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.tn-list-item__secondary-text{font-size:14px;font-weight:400;line-height:1.4;color:var(--tn-fg2);margin-top:2px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.tn-list-item__trailing{flex-shrink:0;margin-left:16px;display:flex;align-items:center}.tn-list-item--clickable{cursor:pointer}.tn-list-item--clickable:hover:not(.tn-list-item--disabled){background-color:var(--tn-alt-bg1)}.tn-list-item--clickable:focus{outline:none;background-color:var(--tn-alt-bg1)}.tn-list-item--clickable:active:not(.tn-list-item--disabled){background-color:var(--tn-alt-bg2)}.tn-list-item--two-line{min-height:64px}.tn-list-item--two-line .tn-list-item__primary-text{white-space:normal;line-height:1.4}.tn-list-item--two-line .tn-list-item__secondary-text{white-space:normal;line-height:1.3}.tn-list-item--three-line{min-height:88px}.tn-list-item--three-line .tn-list-item__text{align-items:flex-start;padding:8px 0}.tn-list-item--three-line .tn-list-item__primary-text{white-space:normal;line-height:1.4}.tn-list-item--three-line .tn-list-item__secondary-text{white-space:normal;line-height:1.3;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}.tn-list-item--disabled{opacity:.6;cursor:not-allowed;pointer-events:none}::ng-deep [ixListIcon]{width:24px;height:24px;font-size:24px;color:var(--tn-fg2);display:flex;align-items:center;justify-content:center}::ng-deep [ixListAvatar]{width:40px;height:40px;border-radius:50%;object-fit:cover;background-color:var(--tn-alt-bg1);color:var(--tn-fg1);display:flex;align-items:center;justify-content:center;font-weight:500}::ng-deep [ixListItemTitle]{font-weight:400}::ng-deep [ixListItemLine]{display:block;margin-top:4px;font-size:14px;color:var(--tn-fg2);line-height:1.4}::ng-deep [ixListItemTrailing]{color:var(--tn-fg2);font-size:14px}\n"] });
10196
- }
10197
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnListItemComponent, decorators: [{
10198
- type: Component,
10199
- args: [{ selector: 'tn-list-item', standalone: true, imports: [], host: {
10200
- 'class': 'tn-list-item',
10201
- '[class.tn-list-item--disabled]': 'disabled()',
10202
- '[class.tn-list-item--clickable]': 'clickable()',
10203
- '[class.tn-list-item--two-line]': 'hasSecondaryText()',
10204
- '[class.tn-list-item--three-line]': 'hasThirdText()',
10205
- 'role': 'listitem',
10206
- '(click)': 'onClick($event)'
10207
- }, template: "<div class=\"tn-list-item__content\">\n <!-- Leading icon/avatar slot -->\n @if (hasLeadingContent()) {\n <div class=\"tn-list-item__leading\">\n <ng-content select=\"[tnListIcon], [tnListAvatar]\" />\n </div>\n }\n\n <!-- Text content -->\n <div class=\"tn-list-item__text\">\n <!-- Primary text -->\n <div class=\"tn-list-item__primary-text\">\n <ng-content select=\"[tnListItemTitle], [tnListItemPrimary]\" />\n @if (!hasPrimaryTextDirective()) {\n <ng-content />\n }\n </div>\n\n <!-- Secondary text -->\n @if (hasSecondaryTextContent()) {\n <div class=\"tn-list-item__secondary-text\">\n <ng-content select=\"[tnListItemLine], [tnListItemSecondary]\" />\n </div>\n }\n </div>\n\n <!-- Trailing content slot -->\n @if (hasTrailingContent()) {\n <div class=\"tn-list-item__trailing\">\n <ng-content select=\"[tnListItemTrailing]\" />\n </div>\n }\n</div>", styles: [".tn-list-item{display:block;position:relative;min-height:48px;padding:0;cursor:default;text-decoration:none;color:var(--tn-fg1);transition:background-color .2s ease}.tn-list-item__content{display:flex;align-items:center;padding:8px 16px;min-height:inherit;box-sizing:border-box}.tn-list-item__leading{flex-shrink:0;margin-right:16px;display:flex;align-items:center;justify-content:center}.tn-list-item__text{flex:1;min-width:0;display:flex;flex-direction:column;justify-content:center}.tn-list-item__primary-text{font-size:1rem;font-weight:400;line-height:1.5;color:var(--tn-fg1);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.tn-list-item__secondary-text{font-size:14px;font-weight:400;line-height:1.4;color:var(--tn-fg2);margin-top:2px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.tn-list-item__trailing{flex-shrink:0;margin-left:16px;display:flex;align-items:center}.tn-list-item--clickable{cursor:pointer}.tn-list-item--clickable:hover:not(.tn-list-item--disabled){background-color:var(--tn-alt-bg1)}.tn-list-item--clickable:focus{outline:none;background-color:var(--tn-alt-bg1)}.tn-list-item--clickable:active:not(.tn-list-item--disabled){background-color:var(--tn-alt-bg2)}.tn-list-item--two-line{min-height:64px}.tn-list-item--two-line .tn-list-item__primary-text{white-space:normal;line-height:1.4}.tn-list-item--two-line .tn-list-item__secondary-text{white-space:normal;line-height:1.3}.tn-list-item--three-line{min-height:88px}.tn-list-item--three-line .tn-list-item__text{align-items:flex-start;padding:8px 0}.tn-list-item--three-line .tn-list-item__primary-text{white-space:normal;line-height:1.4}.tn-list-item--three-line .tn-list-item__secondary-text{white-space:normal;line-height:1.3;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}.tn-list-item--disabled{opacity:.6;cursor:not-allowed;pointer-events:none}::ng-deep [ixListIcon]{width:24px;height:24px;font-size:24px;color:var(--tn-fg2);display:flex;align-items:center;justify-content:center}::ng-deep [ixListAvatar]{width:40px;height:40px;border-radius:50%;object-fit:cover;background-color:var(--tn-alt-bg1);color:var(--tn-fg1);display:flex;align-items:center;justify-content:center;font-weight:500}::ng-deep [ixListItemTitle]{font-weight:400}::ng-deep [ixListItemLine]{display:block;margin-top:4px;font-size:14px;color:var(--tn-fg2);line-height:1.4}::ng-deep [ixListItemTrailing]{color:var(--tn-fg2);font-size:14px}\n"] }]
10208
- }], propDecorators: { disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], clickable: [{ type: i0.Input, args: [{ isSignal: true, alias: "clickable", required: false }] }], itemClick: [{ type: i0.Output, args: ["itemClick"] }] } });
10209
-
10210
- class TnListSubheaderComponent {
10211
- inset = input(false, ...(ngDevMode ? [{ debugName: "inset" }] : []));
10212
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnListSubheaderComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
10213
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.1.0", type: TnListSubheaderComponent, isStandalone: true, selector: "tn-list-subheader", inputs: { inset: { classPropertyName: "inset", publicName: "inset", isSignal: true, isRequired: false, transformFunction: null } }, host: { attributes: { "role": "heading", "aria-level": "3" }, properties: { "class.tn-list-subheader--inset": "inset()" }, classAttribute: "tn-list-subheader" }, ngImport: i0, template: "<ng-content />", styles: [":host{display:block;margin:.75rem 16px;font-size:14px;font-weight:600;color:var(--tn-fg2);text-transform:uppercase;letter-spacing:.5px;line-height:1.4}:host-context(.tn-list--dense) :host{font-size:13px}\n"] });
10214
- }
10215
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnListSubheaderComponent, decorators: [{
10216
- type: Component,
10217
- args: [{ selector: 'tn-list-subheader', standalone: true, imports: [], host: {
10218
- 'class': 'tn-list-subheader',
10219
- '[class.tn-list-subheader--inset]': 'inset()',
10220
- 'role': 'heading',
10221
- 'aria-level': '3'
10222
- }, template: "<ng-content />", styles: [":host{display:block;margin:.75rem 16px;font-size:14px;font-weight:600;color:var(--tn-fg2);text-transform:uppercase;letter-spacing:.5px;line-height:1.4}:host-context(.tn-list--dense) :host{font-size:13px}\n"] }]
10223
- }], propDecorators: { inset: [{ type: i0.Input, args: [{ isSignal: true, alias: "inset", required: false }] }] } });
10224
-
10175
+ /*
10176
+ * Content directives for `tn-list-item` / `tn-list-option`.
10177
+ *
10178
+ * Each of these must be imported by the component whose template writes the
10179
+ * attribute. `tn-list-item` renders its leading, secondary and trailing slots
10180
+ * only when a matching directive *instance* is present, and a directive applies
10181
+ * only in the template that declares it — importing the list-item component
10182
+ * does not bring these into scope. An attribute written without its directive
10183
+ * imported is inert: the slot stays closed and the content silently disappears.
10184
+ * `[tnListItemTitle]` and `[tnListItemPrimary]` are the exception; their slot is
10185
+ * ungated, so they project either way (they still need importing for the class
10186
+ * and styling).
10187
+ */
10188
+ /**
10189
+ * Leading icon of a list row.
10190
+ *
10191
+ * Requires importing `TnListIconDirective`, or the leading slot never renders.
10192
+ */
10225
10193
  class TnListIconDirective {
10226
10194
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnListIconDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
10227
10195
  static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.1.0", type: TnListIconDirective, isStandalone: true, selector: "[tnListIcon]", host: { classAttribute: "tn-list-icon" }, ngImport: i0 });
@@ -10236,6 +10204,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
10236
10204
  }
10237
10205
  }]
10238
10206
  }] });
10207
+ /**
10208
+ * Leading avatar of a list row.
10209
+ *
10210
+ * Requires importing `TnListAvatarDirective`, or the leading slot never renders.
10211
+ */
10239
10212
  class TnListAvatarDirective {
10240
10213
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnListAvatarDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
10241
10214
  static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.1.0", type: TnListAvatarDirective, isStandalone: true, selector: "[tnListAvatar]", host: { classAttribute: "tn-list-avatar" }, ngImport: i0 });
@@ -10250,6 +10223,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
10250
10223
  }
10251
10224
  }]
10252
10225
  }] });
10226
+ /**
10227
+ * Primary text of a list row. Projects whether or not this directive is
10228
+ * imported — the primary slot is ungated — but import it for the styling.
10229
+ */
10253
10230
  class TnListItemTitleDirective {
10254
10231
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnListItemTitleDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
10255
10232
  static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.1.0", type: TnListItemTitleDirective, isStandalone: true, selector: "[tnListItemTitle]", host: { classAttribute: "tn-list-item-title" }, ngImport: i0 });
@@ -10264,6 +10241,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
10264
10241
  }
10265
10242
  }]
10266
10243
  }] });
10244
+ /**
10245
+ * Secondary line of a list row. Two or more make the row three-line.
10246
+ *
10247
+ * Requires importing `TnListItemLineDirective`, or the secondary slot never
10248
+ * renders and the row is not marked as multi-line.
10249
+ */
10267
10250
  class TnListItemLineDirective {
10268
10251
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnListItemLineDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
10269
10252
  static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.1.0", type: TnListItemLineDirective, isStandalone: true, selector: "[tnListItemLine]", host: { classAttribute: "tn-list-item-line" }, ngImport: i0 });
@@ -10278,6 +10261,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
10278
10261
  }
10279
10262
  }]
10280
10263
  }] });
10264
+ /**
10265
+ * Primary text of a list row. Projects whether or not this directive is
10266
+ * imported — the primary slot is ungated — but import it for the styling.
10267
+ */
10281
10268
  class TnListItemPrimaryDirective {
10282
10269
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnListItemPrimaryDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
10283
10270
  static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.1.0", type: TnListItemPrimaryDirective, isStandalone: true, selector: "[tnListItemPrimary]", host: { classAttribute: "tn-list-item-primary" }, ngImport: i0 });
@@ -10292,6 +10279,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
10292
10279
  }
10293
10280
  }]
10294
10281
  }] });
10282
+ /**
10283
+ * Secondary text of a list row.
10284
+ *
10285
+ * Requires importing `TnListItemSecondaryDirective`, or the secondary slot
10286
+ * never renders and the row is not marked as multi-line.
10287
+ */
10295
10288
  class TnListItemSecondaryDirective {
10296
10289
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnListItemSecondaryDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
10297
10290
  static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.1.0", type: TnListItemSecondaryDirective, isStandalone: true, selector: "[tnListItemSecondary]", host: { classAttribute: "tn-list-item-secondary" }, ngImport: i0 });
@@ -10306,6 +10299,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
10306
10299
  }
10307
10300
  }]
10308
10301
  }] });
10302
+ /**
10303
+ * Trailing content of a list row — a control, a badge, a chevron.
10304
+ *
10305
+ * Requires importing `TnListItemTrailingDirective`, or the trailing slot never
10306
+ * renders.
10307
+ */
10309
10308
  class TnListItemTrailingDirective {
10310
10309
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnListItemTrailingDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
10311
10310
  static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.1.0", type: TnListItemTrailingDirective, isStandalone: true, selector: "[tnListItemTrailing]", host: { classAttribute: "tn-list-item-trailing" }, ngImport: i0 });
@@ -10336,6 +10335,108 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
10336
10335
  }]
10337
10336
  }] });
10338
10337
 
10338
+ /**
10339
+ * A single row of a `tn-list`.
10340
+ *
10341
+ * **The content directives must be imported by the component that declares the
10342
+ * row.** Every slot but the primary text is rendered only when a matching
10343
+ * directive *instance* is found, and directives apply only in the template that
10344
+ * declares them — importing `TnListItemComponent` alone does not bring them
10345
+ * into scope. Write `<span tnListIcon>` without `TnListIconDirective` in that
10346
+ * component's `imports` and the attribute is inert: no instance, so the leading
10347
+ * slot never renders and the icon silently disappears. The same holds for
10348
+ * `[tnListAvatar]`, `[tnListItemLine]`, `[tnListItemSecondary]` and
10349
+ * `[tnListItemTrailing]`. Import them alongside the component:
10350
+ *
10351
+ * ```ts
10352
+ * import {
10353
+ * TnListItemComponent,
10354
+ * TnListIconDirective,
10355
+ * TnListItemLineDirective,
10356
+ * TnListItemTrailingDirective,
10357
+ * } from '@truenas/ui-components';
10358
+ * ```
10359
+ *
10360
+ * The primary-text slot is the deliberate exception — it has no gate, so
10361
+ * `[tnListItemTitle]` and `[tnListItemPrimary]` render either way. See the
10362
+ * comment in `list-item.component.html` for why the asymmetry is there.
10363
+ */
10364
+ class TnListItemComponent {
10365
+ disabled = input(false, ...(ngDevMode ? [{ debugName: "disabled" }] : []));
10366
+ clickable = input(false, ...(ngDevMode ? [{ debugName: "clickable" }] : []));
10367
+ /** Compact row: smaller minimum height and tighter vertical padding. */
10368
+ dense = input(false, ...(ngDevMode ? [{ debugName: "dense" }] : []));
10369
+ /**
10370
+ * Lets the primary and secondary text wrap onto multiple lines instead of
10371
+ * being truncated with an ellipsis. Use for rows whose content is a path, a
10372
+ * sentence, or anything else that should stay fully readable.
10373
+ */
10374
+ wrap = input(false, ...(ngDevMode ? [{ debugName: "wrap" }] : []));
10375
+ itemClick = output();
10376
+ // Content queries, not a DOM query: projected content is instantiated in the
10377
+ // *declaring* view, so these resolve even while the matching <ng-content> slot
10378
+ // is still hidden behind the flag they feed. A `querySelector` cannot — the
10379
+ // element is not in the DOM until its slot renders, and its slot does not
10380
+ // render until the flag is set, so every gated slot stayed empty forever.
10381
+ //
10382
+ // These match directive *instances* while the slots they gate project by
10383
+ // *attribute*, so the two disagree for a consumer who writes the attribute
10384
+ // without importing the directive: nothing matches here, the slot stays
10385
+ // closed, and the content vanishes. The primary-text slot avoids this by not
10386
+ // gating at all — see the comment in the template. The side slots cannot
10387
+ // follow it: their wrapper elements carry their own gutters, so rendering
10388
+ // them unconditionally would leave empty ones spacing out every row, and the
10389
+ // --two-line/--three-line host classes need the counts regardless. So the
10390
+ // requirement is documented on the class and on each directive instead. It
10391
+ // cannot be checked at runtime: content that never projects is never in the
10392
+ // DOM to be found.
10393
+ leadingIcons = contentChildren(TnListIconDirective, { ...(ngDevMode ? { debugName: "leadingIcons" } : {}), descendants: true });
10394
+ leadingAvatars = contentChildren(TnListAvatarDirective, { ...(ngDevMode ? { debugName: "leadingAvatars" } : {}), descendants: true });
10395
+ secondaryLines = contentChildren(TnListItemLineDirective, { ...(ngDevMode ? { debugName: "secondaryLines" } : {}), descendants: true });
10396
+ secondaryTexts = contentChildren(TnListItemSecondaryDirective, { ...(ngDevMode ? { debugName: "secondaryTexts" } : {}), descendants: true });
10397
+ trailing = contentChildren(TnListItemTrailingDirective, { ...(ngDevMode ? { debugName: "trailing" } : {}), descendants: true });
10398
+ hasLeadingContent = computed(() => this.leadingIcons().length > 0 || this.leadingAvatars().length > 0, ...(ngDevMode ? [{ debugName: "hasLeadingContent" }] : []));
10399
+ hasTrailingContent = computed(() => this.trailing().length > 0, ...(ngDevMode ? [{ debugName: "hasTrailingContent" }] : []));
10400
+ hasSecondaryText = computed(() => this.secondaryLines().length > 0 || this.secondaryTexts().length > 0, ...(ngDevMode ? [{ debugName: "hasSecondaryText" }] : []));
10401
+ hasThirdText = computed(() => this.secondaryLines().length + this.secondaryTexts().length > 1, ...(ngDevMode ? [{ debugName: "hasThirdText" }] : []));
10402
+ onClick(event) {
10403
+ if (!this.disabled() && this.clickable()) {
10404
+ this.itemClick.emit(event);
10405
+ }
10406
+ }
10407
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnListItemComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
10408
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.0", type: TnListItemComponent, isStandalone: true, selector: "tn-list-item", inputs: { disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, clickable: { classPropertyName: "clickable", publicName: "clickable", isSignal: true, isRequired: false, transformFunction: null }, dense: { classPropertyName: "dense", publicName: "dense", isSignal: true, isRequired: false, transformFunction: null }, wrap: { classPropertyName: "wrap", publicName: "wrap", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { itemClick: "itemClick" }, host: { attributes: { "role": "listitem" }, listeners: { "click": "onClick($event)" }, properties: { "class.tn-list-item--disabled": "disabled()", "class.tn-list-item--clickable": "clickable()", "class.tn-list-item--dense": "dense()", "class.tn-list-item--wrap": "wrap()", "class.tn-list-item--two-line": "hasSecondaryText()", "class.tn-list-item--three-line": "hasThirdText()" }, classAttribute: "tn-list-item" }, queries: [{ propertyName: "leadingIcons", predicate: TnListIconDirective, descendants: true, isSignal: true }, { propertyName: "leadingAvatars", predicate: TnListAvatarDirective, descendants: true, isSignal: true }, { propertyName: "secondaryLines", predicate: TnListItemLineDirective, descendants: true, isSignal: true }, { propertyName: "secondaryTexts", predicate: TnListItemSecondaryDirective, descendants: true, isSignal: true }, { propertyName: "trailing", predicate: TnListItemTrailingDirective, descendants: true, isSignal: true }], ngImport: i0, template: "<div class=\"tn-list-item__content\">\n <!-- Leading icon/avatar slot -->\n @if (hasLeadingContent()) {\n <div class=\"tn-list-item__leading\">\n <ng-content select=\"[tnListIcon], [tnListAvatar]\" />\n </div>\n }\n\n <!-- Text content -->\n <div class=\"tn-list-item__text\">\n <!--\n Primary text. The catch-all slot needs no guard: a projected node is\n assigned to exactly one slot, so anything carrying [tnListItemTitle] or\n [tnListItemPrimary] lands in the slot above and can never reach the\n catch-all. Gating this on a directive query would instead key projection\n and rendering off two different things \u2014 the slot matches the attribute,\n a contentChildren query matches the directive instance \u2014 so an attribute\n written without its directive imported would toggle the guard while\n projecting exactly as before.\n\n The leading, secondary and trailing slots below are still gated that way,\n deliberately: their wrappers carry gutters that would space out every row\n if rendered empty. The cost is that they require the directive to be\n imported, which is documented on the component and on each directive.\n -->\n <div class=\"tn-list-item__primary-text\">\n <ng-content select=\"[tnListItemTitle], [tnListItemPrimary]\" />\n <ng-content />\n </div>\n\n <!-- Secondary text -->\n @if (hasSecondaryText()) {\n <div class=\"tn-list-item__secondary-text\">\n <ng-content select=\"[tnListItemLine], [tnListItemSecondary]\" />\n </div>\n }\n </div>\n\n <!-- Trailing content slot -->\n @if (hasTrailingContent()) {\n <div class=\"tn-list-item__trailing\">\n <ng-content select=\"[tnListItemTrailing]\" />\n </div>\n }\n</div>", styles: [".tn-list-item{display:block;position:relative;min-height:48px;padding:0;cursor:default;text-decoration:none;color:var(--tn-fg1);transition:background-color .2s ease}.tn-list-item__content{display:flex;align-items:center;padding:8px 16px;min-height:inherit;box-sizing:border-box}.tn-list-item__leading{flex-shrink:0;margin-right:16px;display:flex;align-items:center;justify-content:center}.tn-list-item__text{flex:1;min-width:0;display:flex;flex-direction:column;justify-content:center}.tn-list-item__primary-text{font-size:1rem;font-weight:400;line-height:1.5;color:var(--tn-fg1);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.tn-list-item__secondary-text{font-size:14px;font-weight:400;line-height:1.4;color:var(--tn-fg2);margin-top:2px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.tn-list-item__trailing{flex-shrink:0;margin-left:16px;display:flex;align-items:center}.tn-list-item--clickable{cursor:pointer}.tn-list-item--clickable:hover:not(.tn-list-item--disabled){background-color:var(--tn-alt-bg1)}.tn-list-item--clickable:focus{outline:none;background-color:var(--tn-alt-bg1)}.tn-list-item--clickable:active:not(.tn-list-item--disabled){background-color:var(--tn-alt-bg2)}.tn-list-item--two-line{min-height:64px}.tn-list-item--two-line .tn-list-item__primary-text{white-space:normal;line-height:1.4}.tn-list-item--two-line .tn-list-item__secondary-text{white-space:normal;line-height:1.3}.tn-list-item--three-line{min-height:88px}.tn-list-item--three-line .tn-list-item__text{align-items:flex-start;padding:8px 0}.tn-list-item--three-line .tn-list-item__primary-text{white-space:normal;line-height:1.4}.tn-list-item--three-line .tn-list-item__secondary-text{white-space:normal;line-height:1.3;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}.tn-list-item--dense{min-height:32px}.tn-list-item--dense .tn-list-item__content{padding:4px 16px}.tn-list-item--dense .tn-list-item__leading{margin-right:8px}.tn-list-item--dense .tn-list-item__trailing{margin-left:8px}.tn-list-item--wrap .tn-list-item__primary-text,.tn-list-item--wrap .tn-list-item__secondary-text{overflow:visible;text-overflow:clip;white-space:normal}.tn-list-item--disabled{opacity:.6;cursor:not-allowed;pointer-events:none}::ng-deep [tnListIcon]{width:24px;height:24px;font-size:24px;color:var(--tn-fg2);display:flex;align-items:center;justify-content:center}::ng-deep [tnListAvatar]{width:40px;height:40px;border-radius:50%;object-fit:cover;background-color:var(--tn-alt-bg1);color:var(--tn-fg1);display:flex;align-items:center;justify-content:center;font-weight:500}::ng-deep [tnListItemTitle]{font-weight:400}::ng-deep [tnListItemLine]{display:block;margin-top:4px;font-size:14px;color:var(--tn-fg2);line-height:1.4}::ng-deep [tnListItemTrailing]{color:var(--tn-fg2);font-size:14px}\n"] });
10409
+ }
10410
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnListItemComponent, decorators: [{
10411
+ type: Component,
10412
+ args: [{ selector: 'tn-list-item', standalone: true, imports: [], host: {
10413
+ 'class': 'tn-list-item',
10414
+ '[class.tn-list-item--disabled]': 'disabled()',
10415
+ '[class.tn-list-item--clickable]': 'clickable()',
10416
+ '[class.tn-list-item--dense]': 'dense()',
10417
+ '[class.tn-list-item--wrap]': 'wrap()',
10418
+ '[class.tn-list-item--two-line]': 'hasSecondaryText()',
10419
+ '[class.tn-list-item--three-line]': 'hasThirdText()',
10420
+ 'role': 'listitem',
10421
+ '(click)': 'onClick($event)'
10422
+ }, template: "<div class=\"tn-list-item__content\">\n <!-- Leading icon/avatar slot -->\n @if (hasLeadingContent()) {\n <div class=\"tn-list-item__leading\">\n <ng-content select=\"[tnListIcon], [tnListAvatar]\" />\n </div>\n }\n\n <!-- Text content -->\n <div class=\"tn-list-item__text\">\n <!--\n Primary text. The catch-all slot needs no guard: a projected node is\n assigned to exactly one slot, so anything carrying [tnListItemTitle] or\n [tnListItemPrimary] lands in the slot above and can never reach the\n catch-all. Gating this on a directive query would instead key projection\n and rendering off two different things \u2014 the slot matches the attribute,\n a contentChildren query matches the directive instance \u2014 so an attribute\n written without its directive imported would toggle the guard while\n projecting exactly as before.\n\n The leading, secondary and trailing slots below are still gated that way,\n deliberately: their wrappers carry gutters that would space out every row\n if rendered empty. The cost is that they require the directive to be\n imported, which is documented on the component and on each directive.\n -->\n <div class=\"tn-list-item__primary-text\">\n <ng-content select=\"[tnListItemTitle], [tnListItemPrimary]\" />\n <ng-content />\n </div>\n\n <!-- Secondary text -->\n @if (hasSecondaryText()) {\n <div class=\"tn-list-item__secondary-text\">\n <ng-content select=\"[tnListItemLine], [tnListItemSecondary]\" />\n </div>\n }\n </div>\n\n <!-- Trailing content slot -->\n @if (hasTrailingContent()) {\n <div class=\"tn-list-item__trailing\">\n <ng-content select=\"[tnListItemTrailing]\" />\n </div>\n }\n</div>", styles: [".tn-list-item{display:block;position:relative;min-height:48px;padding:0;cursor:default;text-decoration:none;color:var(--tn-fg1);transition:background-color .2s ease}.tn-list-item__content{display:flex;align-items:center;padding:8px 16px;min-height:inherit;box-sizing:border-box}.tn-list-item__leading{flex-shrink:0;margin-right:16px;display:flex;align-items:center;justify-content:center}.tn-list-item__text{flex:1;min-width:0;display:flex;flex-direction:column;justify-content:center}.tn-list-item__primary-text{font-size:1rem;font-weight:400;line-height:1.5;color:var(--tn-fg1);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.tn-list-item__secondary-text{font-size:14px;font-weight:400;line-height:1.4;color:var(--tn-fg2);margin-top:2px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.tn-list-item__trailing{flex-shrink:0;margin-left:16px;display:flex;align-items:center}.tn-list-item--clickable{cursor:pointer}.tn-list-item--clickable:hover:not(.tn-list-item--disabled){background-color:var(--tn-alt-bg1)}.tn-list-item--clickable:focus{outline:none;background-color:var(--tn-alt-bg1)}.tn-list-item--clickable:active:not(.tn-list-item--disabled){background-color:var(--tn-alt-bg2)}.tn-list-item--two-line{min-height:64px}.tn-list-item--two-line .tn-list-item__primary-text{white-space:normal;line-height:1.4}.tn-list-item--two-line .tn-list-item__secondary-text{white-space:normal;line-height:1.3}.tn-list-item--three-line{min-height:88px}.tn-list-item--three-line .tn-list-item__text{align-items:flex-start;padding:8px 0}.tn-list-item--three-line .tn-list-item__primary-text{white-space:normal;line-height:1.4}.tn-list-item--three-line .tn-list-item__secondary-text{white-space:normal;line-height:1.3;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}.tn-list-item--dense{min-height:32px}.tn-list-item--dense .tn-list-item__content{padding:4px 16px}.tn-list-item--dense .tn-list-item__leading{margin-right:8px}.tn-list-item--dense .tn-list-item__trailing{margin-left:8px}.tn-list-item--wrap .tn-list-item__primary-text,.tn-list-item--wrap .tn-list-item__secondary-text{overflow:visible;text-overflow:clip;white-space:normal}.tn-list-item--disabled{opacity:.6;cursor:not-allowed;pointer-events:none}::ng-deep [tnListIcon]{width:24px;height:24px;font-size:24px;color:var(--tn-fg2);display:flex;align-items:center;justify-content:center}::ng-deep [tnListAvatar]{width:40px;height:40px;border-radius:50%;object-fit:cover;background-color:var(--tn-alt-bg1);color:var(--tn-fg1);display:flex;align-items:center;justify-content:center;font-weight:500}::ng-deep [tnListItemTitle]{font-weight:400}::ng-deep [tnListItemLine]{display:block;margin-top:4px;font-size:14px;color:var(--tn-fg2);line-height:1.4}::ng-deep [tnListItemTrailing]{color:var(--tn-fg2);font-size:14px}\n"] }]
10423
+ }], propDecorators: { disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], clickable: [{ type: i0.Input, args: [{ isSignal: true, alias: "clickable", required: false }] }], dense: [{ type: i0.Input, args: [{ isSignal: true, alias: "dense", required: false }] }], wrap: [{ type: i0.Input, args: [{ isSignal: true, alias: "wrap", required: false }] }], itemClick: [{ type: i0.Output, args: ["itemClick"] }], leadingIcons: [{ type: i0.ContentChildren, args: [i0.forwardRef(() => TnListIconDirective), { ...{ descendants: true }, isSignal: true }] }], leadingAvatars: [{ type: i0.ContentChildren, args: [i0.forwardRef(() => TnListAvatarDirective), { ...{ descendants: true }, isSignal: true }] }], secondaryLines: [{ type: i0.ContentChildren, args: [i0.forwardRef(() => TnListItemLineDirective), { ...{ descendants: true }, isSignal: true }] }], secondaryTexts: [{ type: i0.ContentChildren, args: [i0.forwardRef(() => TnListItemSecondaryDirective), { ...{ descendants: true }, isSignal: true }] }], trailing: [{ type: i0.ContentChildren, args: [i0.forwardRef(() => TnListItemTrailingDirective), { ...{ descendants: true }, isSignal: true }] }] } });
10424
+
10425
+ class TnListSubheaderComponent {
10426
+ inset = input(false, ...(ngDevMode ? [{ debugName: "inset" }] : []));
10427
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnListSubheaderComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
10428
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.1.0", type: TnListSubheaderComponent, isStandalone: true, selector: "tn-list-subheader", inputs: { inset: { classPropertyName: "inset", publicName: "inset", isSignal: true, isRequired: false, transformFunction: null } }, host: { attributes: { "role": "heading", "aria-level": "3" }, properties: { "class.tn-list-subheader--inset": "inset()" }, classAttribute: "tn-list-subheader" }, ngImport: i0, template: "<ng-content />", styles: [":host{display:block;margin:.75rem 16px;font-size:14px;font-weight:600;color:var(--tn-fg2);text-transform:uppercase;letter-spacing:.5px;line-height:1.4}:host-context(.tn-list--dense) :host{font-size:13px}\n"] });
10429
+ }
10430
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnListSubheaderComponent, decorators: [{
10431
+ type: Component,
10432
+ args: [{ selector: 'tn-list-subheader', standalone: true, imports: [], host: {
10433
+ 'class': 'tn-list-subheader',
10434
+ '[class.tn-list-subheader--inset]': 'inset()',
10435
+ 'role': 'heading',
10436
+ 'aria-level': '3'
10437
+ }, template: "<ng-content />", styles: [":host{display:block;margin:.75rem 16px;font-size:14px;font-weight:600;color:var(--tn-fg2);text-transform:uppercase;letter-spacing:.5px;line-height:1.4}:host-context(.tn-list--dense) :host{font-size:13px}\n"] }]
10438
+ }], propDecorators: { inset: [{ type: i0.Input, args: [{ isSignal: true, alias: "inset", required: false }] }] } });
10439
+
10339
10440
  class TnDividerComponent {
10340
10441
  vertical = input(false, ...(ngDevMode ? [{ debugName: "vertical" }] : []));
10341
10442
  inset = input(false, ...(ngDevMode ? [{ debugName: "inset" }] : []));
@@ -10556,7 +10657,7 @@ class TnEmptyComponent {
10556
10657
  return this.size() === 'compact' ? 'lg' : 'xl';
10557
10658
  }, ...(ngDevMode ? [{ debugName: "iconSizePreset" }] : []));
10558
10659
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnEmptyComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
10559
- 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"] }] });
10660
+ 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", "fullWidth", "type", "testId", "href", "routerLink", "queryParams", "fragment", "target", "rel", "ariaLabel"], outputs: ["onClick"] }] });
10560
10661
  }
10561
10662
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnEmptyComponent, decorators: [{
10562
10663
  type: Component,
@@ -13830,43 +13931,251 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
13830
13931
  }, template: "<svg [attr.width]=\"width()\" [attr.height]=\"height()\">\n <defs>\n <linearGradient id=\"fadeToPrimary\" x1=\"0%\" y1=\"0%\" x2=\"100%\" y2=\"0%\">\n <stop offset=\"0%\" stop-color=\"transparent\" />\n <stop stop-color=\"transparent\" [attr.offset]=\"gradientTransitionStart() + '%'\" />\n <stop offset=\"100%\" [attr.stop-color]=\"progressBarColor()\" />\n </linearGradient>\n </defs>\n\n <!-- Background bar -->\n <rect x=\"50\" fill=\"rgba(0,0,0,0.1)\" [attr.y]=\"height() / 2 - height() / 4\" [attr.width]=\"width() - 100\" [attr.height]=\"height() / 2\" [attr.rx]=\"height() / 4\" />\n\n <!-- Fill bar -->\n <rect\n x=\"50\"\n fill=\"url(#fadeToPrimary)\"\n [attr.y]=\"height() / 2 - height() / 4\"\n [attr.width]=\"fill()\"\n [attr.height]=\"height() / 2\"\n [attr.rx]=\"height() / 4\"\n />\n\n <!-- Particle canvas -->\n <foreignObject x=\"0\" y=\"0\" [attr.width]=\"width()\" [attr.height]=\"height()\">\n <canvas #canvas style=\"pointer-events: none;\" [attr.width]=\"width()\" [attr.height]=\"height()\"></canvas>\n </foreignObject>\n</svg>\n", styles: [":host{display:block}\n"] }]
13831
13932
  }], propDecorators: { speed: [{ type: i0.Input, args: [{ isSignal: true, alias: "speed", required: false }] }], color: [{ type: i0.Input, args: [{ isSignal: true, alias: "color", required: false }] }], height: [{ type: i0.Input, args: [{ isSignal: true, alias: "height", required: false }] }], width: [{ type: i0.Input, args: [{ isSignal: true, alias: "width", required: false }] }], fill: [{ type: i0.Input, args: [{ isSignal: true, alias: "fill", required: false }] }], canvasRef: [{ type: i0.ViewChild, args: ['canvas', { isSignal: true }] }] } });
13832
13933
 
13934
+ /**
13935
+ * Day-granularity date helpers shared by the calendar views.
13936
+ *
13937
+ * The calendar deals in calendar days, but the dates a caller binds are ordinary
13938
+ * `Date`s and routinely carry a time of day — `new Date()` for a default, or a value
13939
+ * round-tripped through an API. Comparing those against the midnight dates the grid
13940
+ * builds gives answers that are off by up to a day, so every comparison the calendar
13941
+ * makes goes through here.
13942
+ */
13943
+ /**
13944
+ * Orders two dates by calendar day: negative when `a` falls on an earlier day than `b`,
13945
+ * zero on the same day, positive when later. Time of day is ignored on both sides.
13946
+ */
13947
+ function compareDays(a, b) {
13948
+ return (a.getFullYear() - b.getFullYear()
13949
+ || a.getMonth() - b.getMonth()
13950
+ || a.getDate() - b.getDate());
13951
+ }
13952
+ /** Whether two dates fall on the same calendar day, ignoring time of day. */
13953
+ function isSameDay(a, b) {
13954
+ return compareDays(a, b) === 0;
13955
+ }
13956
+ /** Stable per-day key, for lookups and change tracking. */
13957
+ function dateKey(date) {
13958
+ return `${date.getFullYear()}-${date.getMonth()}-${date.getDate()}`;
13959
+ }
13960
+ /**
13961
+ * `YYYY-MM-DD` for the day this date falls on, built from its local parts.
13962
+ *
13963
+ * Rendered onto each day cell as `data-tn-date`, which gives tests a handle on a day
13964
+ * that survives translation — cell text is formatted for the locale, right down to the
13965
+ * numerals, so matching on it only works in English.
13966
+ *
13967
+ * Deliberately not `toISOString()`: that converts to UTC first, so anywhere east or west
13968
+ * of Greenwich it can name the day before or after the one shown.
13969
+ */
13970
+ function isoDateString(date) {
13971
+ const month = `${date.getMonth() + 1}`.padStart(2, '0');
13972
+ const day = `${date.getDate()}`.padStart(2, '0');
13973
+ return `${date.getFullYear()}-${month}-${day}`;
13974
+ }
13975
+ /**
13976
+ * Adds whole months, clamping to the last day when the target month is shorter, so
13977
+ * navigating never silently lands in the month after the one asked for. `new Date` rolls
13978
+ * 31 January + 1 month over into early March; this returns 28 (or 29) February.
13979
+ */
13980
+ function addMonths(date, delta) {
13981
+ return atMonthAndDay(date, date.getFullYear(), date.getMonth() + delta);
13982
+ }
13983
+ /**
13984
+ * The same month and day in another year, clamped the same way — 29 February in a
13985
+ * non-leap year is 28 February, not 1 March. Without the clamp, paging the year grid off
13986
+ * a leap day quietly changes the month as well as the year.
13987
+ */
13988
+ function withYear(date, year) {
13989
+ return atMonthAndDay(date, year, date.getMonth());
13990
+ }
13991
+ /** Keeps `date`'s day-of-month where the target month is long enough to hold it. */
13992
+ function atMonthAndDay(date, year, month) {
13993
+ const lastDay = new Date(year, month + 1, 0).getDate();
13994
+ return new Date(year, month, Math.min(date.getDate(), lastDay));
13995
+ }
13996
+ /** How many years one page of the year grid shows. */
13997
+ const YEARS_PER_PAGE = 24;
13998
+ /**
13999
+ * The first year of the page containing `year`.
14000
+ *
14001
+ * Pages are fixed blocks counted from year 0, not windows centred on the year asked
14002
+ * about — 2020 sits near the top of 2016–2039 rather than in the middle of 2008–2031.
14003
+ * The year view and the header both need this answer and must agree on it, or the
14004
+ * header labels a span the grid isn't showing.
14005
+ */
14006
+ function yearPageStart(year) {
14007
+ return Math.floor(year / YEARS_PER_PAGE) * YEARS_PER_PAGE;
14008
+ }
14009
+ /**
14010
+ * The day a week starts on in the given locale, as a `Date.getDay()` index — 0 for
14011
+ * Sunday through 6 for Saturday. Sunday in the US, Monday across most of Europe,
14012
+ * Saturday in much of the Middle East.
14013
+ *
14014
+ * Read from `Intl`, which reports it ISO-style (1 Monday … 7 Sunday). Not every engine
14015
+ * exposes it — Firefox notably lagged — so anything that cannot answer falls back to
14016
+ * Sunday, which is what the grid did for every locale before.
14017
+ */
14018
+ function firstDayOfWeek(locale) {
14019
+ try {
14020
+ const info = new Intl.Locale(locale);
14021
+ const firstDay = info.getWeekInfo?.().firstDay ?? info.weekInfo?.firstDay;
14022
+ // ISO counts Sunday as 7; `Date.getDay()` counts it as 0.
14023
+ return firstDay === undefined ? 0 : firstDay % 7;
14024
+ }
14025
+ catch {
14026
+ // A malformed locale tag shouldn't take the calendar down with it.
14027
+ return 0;
14028
+ }
14029
+ }
14030
+
14031
+ /** The cell holding the grid's roving tabindex — the one cell focus belongs on. */
14032
+ const ACTIVE_CELL = '.tn-calendar-body-cell[tabindex="0"]';
14033
+ /**
14034
+ * Follows the roving tabindex with real focus, once the grid it names has rendered.
14035
+ *
14036
+ * Two situations need it, and both leave focus somewhere wrong until it runs. Arrowing
14037
+ * within a grid re-renders cells that persist as elements, so the browser keeps focus on
14038
+ * the cell just left. Switching between the day and year grids destroys the cell that
14039
+ * was focused outright, which drops focus to `<body>` and restarts the tab order.
14040
+ *
14041
+ * Tied to the render rather than a zero timeout: the target cell doesn't exist until the
14042
+ * new grid is painted.
14043
+ *
14044
+ * Does nothing when no cell can take focus — a month or page with every day disabled has
14045
+ * nothing worth focusing.
14046
+ */
14047
+ function focusActiveCellAfterRender(host, injector) {
14048
+ afterNextRender(() => {
14049
+ host.nativeElement.querySelector(ACTIVE_CELL)?.focus();
14050
+ }, { injector });
14051
+ }
14052
+
14053
+ /** The wording used when an app provides none of its own. */
14054
+ const TN_CALENDAR_INTL_DEFAULTS = {
14055
+ marked: '(marked)',
14056
+ rangeStart: '(range start)',
14057
+ rangeEnd: '(range end)',
14058
+ inRange: '(in range)',
14059
+ currentYear: '(current year)',
14060
+ monthGridLabel: (period) => period,
14061
+ yearGridLabel: (startYear, endYear) => `Years ${startYear} to ${endYear}`,
14062
+ chooseMonthAndYear: 'Choose month and year',
14063
+ previousMonth: 'Previous month',
14064
+ nextMonth: 'Next month',
14065
+ previousYears: `Previous ${YEARS_PER_PAGE} years`,
14066
+ nextYears: `Next ${YEARS_PER_PAGE} years`,
14067
+ };
14068
+ /**
14069
+ * Injection token for the calendar's wording.
14070
+ *
14071
+ * Because the library ships no localized strings, this is the recommended hook for
14072
+ * wiring a translation service so every `tn-calendar` reads the same way. Anything left
14073
+ * out falls back to {@link TN_CALENDAR_INTL_DEFAULTS}, so an app can translate one
14074
+ * string without restating the rest.
14075
+ *
14076
+ * Dates themselves are not here — they follow `LOCALE_ID`, or the calendar's `locale`
14077
+ * input. See `TnCalendarComponent.locale`.
14078
+ *
14079
+ * @example
14080
+ * ```ts
14081
+ * // Fixed for the lifetime of the app — the language is chosen at build or bootstrap.
14082
+ * providers: [
14083
+ * {
14084
+ * provide: TN_CALENDAR_INTL,
14085
+ * useFactory: (translate: TranslateService): TnCalendarIntlInput => ({
14086
+ * marked: translate.instant('calendar.marked'),
14087
+ * previousMonth: translate.instant('calendar.previousMonth'),
14088
+ * yearGridLabel: (from, to) => translate.instant('calendar.years', { from, to }),
14089
+ * }),
14090
+ * deps: [TranslateService],
14091
+ * },
14092
+ * ];
14093
+ * ```
14094
+ *
14095
+ * @example
14096
+ * ```ts
14097
+ * // Switchable at runtime: a signal, so calendars already on screen follow along.
14098
+ * providers: [
14099
+ * {
14100
+ * provide: TN_CALENDAR_INTL,
14101
+ * useFactory: (): TnCalendarIntlInput => {
14102
+ * const translate = inject(TranslateService);
14103
+ * const lang = toSignal(translate.onLangChange, { initialValue: null });
14104
+ * return computed(() => {
14105
+ * lang(); // Re-read every string when the language changes.
14106
+ * return { marked: translate.instant('calendar.marked') };
14107
+ * });
14108
+ * },
14109
+ * },
14110
+ * ];
14111
+ * ```
14112
+ */
14113
+ const TN_CALENDAR_INTL = new InjectionToken('TN_CALENDAR_INTL');
14114
+ /**
14115
+ * Resolves the wording for a calendar component: whatever the app provided, laid over
14116
+ * the built-in defaults.
14117
+ *
14118
+ * Returns a signal rather than a plain object so a language switch reaches calendars that
14119
+ * are already on screen. Read it inside a `computed` — as every caller here does — and
14120
+ * the labels recompute on their own.
14121
+ */
14122
+ function injectTnCalendarIntl() {
14123
+ const provided = inject(TN_CALENDAR_INTL, { optional: true });
14124
+ return computed(() => {
14125
+ // Signals are functions; a plain object of wording is not.
14126
+ const wording = typeof provided === 'function' ? provided() : provided;
14127
+ return { ...TN_CALENDAR_INTL_DEFAULTS, ...(wording ?? {}) };
14128
+ });
14129
+ }
14130
+
13833
14131
  class TnCalendarHeaderComponent {
13834
14132
  currentDate = input(new Date(), ...(ngDevMode ? [{ debugName: "currentDate" }] : []));
13835
14133
  currentView = input('month', ...(ngDevMode ? [{ debugName: "currentView" }] : []));
13836
- monthSelected = output();
13837
- yearSelected = output();
14134
+ /**
14135
+ * Locale for the month abbreviation. Falls back to the app's `LOCALE_ID`;
14136
+ * `tn-calendar` binds it from its own `locale` input.
14137
+ */
14138
+ locale = input(undefined, ...(ngDevMode ? [{ debugName: "locale" }] : []));
13838
14139
  viewChanged = output();
13839
14140
  previousClicked = output();
13840
14141
  nextClicked = output();
13841
- months = [
13842
- 'JAN', 'FEB', 'MAR', 'APR', 'MAY', 'JUN',
13843
- 'JUL', 'AUG', 'SEP', 'OCT', 'NOV', 'DEC'
13844
- ];
13845
- periodLabelId = `tn-calendar-period-label-${Math.floor(Math.random() * 10000)}`;
14142
+ appLocale = inject(LOCALE_ID);
14143
+ intl = injectTnCalendarIntl();
14144
+ resolvedLocale = computed(() => this.locale() ?? this.appLocale, ...(ngDevMode ? [{ debugName: "resolvedLocale" }] : []));
14145
+ /**
14146
+ * Ties the period button to its live region through `aria-describedby`. Counted rather
14147
+ * than drawn at random: two calendars on one page — a start and an end picker, say —
14148
+ * had a real chance of minting the same id out of only 10,000, and a duplicate id
14149
+ * silently unhooks the association with nothing to show for it.
14150
+ */
14151
+ periodLabelId = inject(_IdGenerator).getId('tn-calendar-period-label-');
13846
14152
  periodLabel = computed(() => {
13847
14153
  const date = this.currentDate();
13848
14154
  if (!date) {
13849
14155
  return '';
13850
14156
  }
14157
+ const locale = this.resolvedLocale();
13851
14158
  if (this.currentView() === 'month') {
13852
- const month = this.months[date.getMonth()];
13853
- const year = date.getFullYear();
13854
- return `${month} ${year}`;
14159
+ // Formatted as one unit so the locale decides the order and the numerals, not a
14160
+ // template string. Upper-cased after the fact: no locale offers a short month name
14161
+ // already capitalised the way this header wants it.
14162
+ return date.toLocaleDateString(locale, { month: 'short', year: 'numeric' }).toUpperCase();
13855
14163
  }
13856
14164
  else {
13857
- // For year view, show the year range (24 years like Material)
13858
- const currentYear = date.getFullYear();
13859
- const startYear = Math.floor(currentYear / 24) * 24;
13860
- const endYear = startYear + 23;
13861
- return `${startYear} – ${endYear}`;
14165
+ // The span the year grid is showing. Shared with the view so the two cannot
14166
+ // disagree about which page is on screen.
14167
+ const startYear = yearPageStart(date.getFullYear());
14168
+ const yearFormat = new Intl.NumberFormat(locale, { useGrouping: false });
14169
+ return `${yearFormat.format(startYear)} – ${yearFormat.format(startYear + YEARS_PER_PAGE - 1)}`;
13862
14170
  }
13863
14171
  }, ...(ngDevMode ? [{ debugName: "periodLabel" }] : []));
13864
14172
  previousLabel = computed(() => {
13865
- return this.currentView() === 'month' ? 'Previous month' : 'Previous 24 years';
14173
+ return this.currentView() === 'month' ? this.intl().previousMonth : this.intl().previousYears;
13866
14174
  }, ...(ngDevMode ? [{ debugName: "previousLabel" }] : []));
13867
14175
  nextLabel = computed(() => {
13868
- return this.currentView() === 'month' ? 'Next month' : 'Next 24 years';
14176
+ return this.currentView() === 'month' ? this.intl().nextMonth : this.intl().nextYears;
13869
14177
  }, ...(ngDevMode ? [{ debugName: "nextLabel" }] : []));
14178
+ periodButtonLabel = computed(() => this.intl().chooseMonthAndYear, ...(ngDevMode ? [{ debugName: "periodButtonLabel" }] : []));
13870
14179
  toggleView() {
13871
14180
  const newView = this.currentView() === 'month' ? 'year' : 'month';
13872
14181
  this.viewChanged.emit(newView);
@@ -13878,12 +14187,12 @@ class TnCalendarHeaderComponent {
13878
14187
  this.nextClicked.emit();
13879
14188
  }
13880
14189
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnCalendarHeaderComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
13881
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.1.0", type: TnCalendarHeaderComponent, isStandalone: true, selector: "tn-calendar-header", inputs: { currentDate: { classPropertyName: "currentDate", publicName: "currentDate", isSignal: true, isRequired: false, transformFunction: null }, currentView: { classPropertyName: "currentView", publicName: "currentView", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { monthSelected: "monthSelected", yearSelected: "yearSelected", viewChanged: "viewChanged", previousClicked: "previousClicked", nextClicked: "nextClicked" }, ngImport: i0, template: "<div class=\"tn-calendar-header\">\n <div class=\"tn-calendar-controls\">\n <!-- Period label (visually hidden for screen readers) -->\n <span aria-live=\"polite\" class=\"cdk-visually-hidden\" [id]=\"periodLabelId\">\n {{ periodLabel() }}\n </span>\n\n <!-- Period button (month/year selector) -->\n <button\n type=\"button\"\n class=\"tn-calendar-period-button\"\n [attr.aria-label]=\"'Choose month and year'\"\n [attr.aria-describedby]=\"periodLabelId\"\n (click)=\"toggleView()\">\n <span [attr.aria-hidden]=\"true\">{{ periodLabel() }}</span>\n <svg viewBox=\"0 0 10 5\" focusable=\"false\" aria-hidden=\"true\" class=\"tn-calendar-arrow\">\n <polygon points=\"0,0 5,5 10,0\" />\n </svg>\n </button>\n\n <!-- Spacer -->\n <div class=\"tn-calendar-spacer\"></div>\n\n <!-- Previous button -->\n <button\n type=\"button\"\n class=\"tn-calendar-previous-button\"\n [attr.aria-label]=\"previousLabel()\"\n (click)=\"onPreviousClick()\">\n <svg viewBox=\"0 0 24 24\" focusable=\"false\" aria-hidden=\"true\">\n <path d=\"M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z\" />\n </svg>\n </button>\n\n <!-- Next button -->\n <button\n type=\"button\"\n class=\"tn-calendar-next-button\"\n [attr.aria-label]=\"nextLabel()\"\n (click)=\"onNextClick()\">\n <svg viewBox=\"0 0 24 24\" focusable=\"false\" aria-hidden=\"true\">\n <path d=\"M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z\" />\n </svg>\n </button>\n </div>\n</div>\n", styles: [".tn-calendar-header{display:flex;padding:16px}.tn-calendar-controls{display:flex;align-items:center;width:100%}.cdk-visually-hidden{position:absolute!important;width:1px!important;height:1px!important;padding:0!important;margin:-1px!important;overflow:hidden!important;clip:rect(0,0,0,0)!important;white-space:nowrap!important;border:0!important}.tn-calendar-period-button{background:none;border:none;font-weight:600;font-size:1rem;color:var(--tn-fg1, #333);padding:8px 12px;border-radius:4px;cursor:pointer;display:flex;align-items:center;gap:4px;transition:background-color .2s ease}.tn-calendar-period-button:hover{background:var(--tn-alt-bg2, #e8f4fd)}.tn-calendar-period-button:focus{outline:2px solid var(--tn-primary, #007bff);outline-offset:2px;background:var(--tn-alt-bg2, #e8f4fd)}.tn-calendar-arrow{width:10px;height:5px;fill:currentColor}.tn-calendar-spacer{flex:1}.tn-calendar-previous-button,.tn-calendar-next-button{background:none;border:none;width:40px;height:40px;border-radius:50%;cursor:pointer;display:flex;align-items:center;justify-content:center;color:var(--tn-fg1, #333);transition:background-color .2s ease}.tn-calendar-previous-button svg,.tn-calendar-next-button svg{width:24px;height:24px;fill:currentColor}.tn-calendar-previous-button:hover:not(:disabled),.tn-calendar-next-button:hover:not(:disabled){background:var(--tn-alt-bg2, #e8f4fd)}.tn-calendar-previous-button:focus,.tn-calendar-next-button:focus{outline:2px solid var(--tn-primary, #007bff);outline-offset:2px;background:var(--tn-alt-bg2, #e8f4fd)}.tn-calendar-previous-button:disabled,.tn-calendar-next-button:disabled{color:var(--tn-fg2, #666);opacity:.5;cursor:not-allowed}\n"] });
14190
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.1.0", type: TnCalendarHeaderComponent, isStandalone: true, selector: "tn-calendar-header", inputs: { currentDate: { classPropertyName: "currentDate", publicName: "currentDate", isSignal: true, isRequired: false, transformFunction: null }, currentView: { classPropertyName: "currentView", publicName: "currentView", isSignal: true, isRequired: false, transformFunction: null }, locale: { classPropertyName: "locale", publicName: "locale", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { viewChanged: "viewChanged", previousClicked: "previousClicked", nextClicked: "nextClicked" }, ngImport: i0, template: "<div class=\"tn-calendar-header\">\n <div class=\"tn-calendar-controls\">\n <!-- Period label (visually hidden for screen readers) -->\n <span aria-live=\"polite\" class=\"cdk-visually-hidden\" [id]=\"periodLabelId\">\n {{ periodLabel() }}\n </span>\n\n <!-- Period button (month/year selector) -->\n <button\n type=\"button\"\n class=\"tn-calendar-period-button\"\n [attr.aria-label]=\"periodButtonLabel()\"\n [attr.aria-describedby]=\"periodLabelId\"\n (click)=\"toggleView()\">\n <span [attr.aria-hidden]=\"true\">{{ periodLabel() }}</span>\n <svg viewBox=\"0 0 10 5\" focusable=\"false\" aria-hidden=\"true\" class=\"tn-calendar-arrow\">\n <polygon points=\"0,0 5,5 10,0\" />\n </svg>\n </button>\n\n <!-- Spacer -->\n <div class=\"tn-calendar-spacer\"></div>\n\n <!-- Previous button -->\n <button\n type=\"button\"\n class=\"tn-calendar-previous-button\"\n [attr.aria-label]=\"previousLabel()\"\n (click)=\"onPreviousClick()\">\n <svg viewBox=\"0 0 24 24\" focusable=\"false\" aria-hidden=\"true\">\n <path d=\"M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z\" />\n </svg>\n </button>\n\n <!-- Next button -->\n <button\n type=\"button\"\n class=\"tn-calendar-next-button\"\n [attr.aria-label]=\"nextLabel()\"\n (click)=\"onNextClick()\">\n <svg viewBox=\"0 0 24 24\" focusable=\"false\" aria-hidden=\"true\">\n <path d=\"M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z\" />\n </svg>\n </button>\n </div>\n</div>\n", styles: [".tn-calendar-header{display:flex;padding:16px}.tn-calendar-controls{display:flex;align-items:center;width:100%}.cdk-visually-hidden{position:absolute!important;width:1px!important;height:1px!important;padding:0!important;margin:-1px!important;overflow:hidden!important;clip:rect(0,0,0,0)!important;white-space:nowrap!important;border:0!important}.tn-calendar-period-button{background:none;border:none;font-weight:600;font-size:1rem;color:var(--tn-fg1, #333);padding:8px 12px;border-radius:4px;cursor:pointer;display:flex;align-items:center;gap:4px;transition:background-color .2s ease}.tn-calendar-period-button:hover{background:var(--tn-alt-bg2, #e8f4fd)}.tn-calendar-period-button:focus{outline:2px solid var(--tn-primary, #007bff);outline-offset:2px;background:var(--tn-alt-bg2, #e8f4fd)}.tn-calendar-arrow{width:10px;height:5px;fill:currentColor}.tn-calendar-spacer{flex:1}.tn-calendar-previous-button,.tn-calendar-next-button{background:none;border:none;width:40px;height:40px;border-radius:50%;cursor:pointer;display:flex;align-items:center;justify-content:center;color:var(--tn-fg1, #333);transition:background-color .2s ease}.tn-calendar-previous-button svg,.tn-calendar-next-button svg{width:24px;height:24px;fill:currentColor}.tn-calendar-previous-button:hover:not(:disabled),.tn-calendar-next-button:hover:not(:disabled){background:var(--tn-alt-bg2, #e8f4fd)}.tn-calendar-previous-button:focus,.tn-calendar-next-button:focus{outline:2px solid var(--tn-primary, #007bff);outline-offset:2px;background:var(--tn-alt-bg2, #e8f4fd)}.tn-calendar-previous-button:disabled,.tn-calendar-next-button:disabled{color:var(--tn-fg2, #666);opacity:.5;cursor:not-allowed}\n"] });
13882
14191
  }
13883
14192
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnCalendarHeaderComponent, decorators: [{
13884
14193
  type: Component,
13885
- args: [{ selector: 'tn-calendar-header', standalone: true, imports: [], template: "<div class=\"tn-calendar-header\">\n <div class=\"tn-calendar-controls\">\n <!-- Period label (visually hidden for screen readers) -->\n <span aria-live=\"polite\" class=\"cdk-visually-hidden\" [id]=\"periodLabelId\">\n {{ periodLabel() }}\n </span>\n\n <!-- Period button (month/year selector) -->\n <button\n type=\"button\"\n class=\"tn-calendar-period-button\"\n [attr.aria-label]=\"'Choose month and year'\"\n [attr.aria-describedby]=\"periodLabelId\"\n (click)=\"toggleView()\">\n <span [attr.aria-hidden]=\"true\">{{ periodLabel() }}</span>\n <svg viewBox=\"0 0 10 5\" focusable=\"false\" aria-hidden=\"true\" class=\"tn-calendar-arrow\">\n <polygon points=\"0,0 5,5 10,0\" />\n </svg>\n </button>\n\n <!-- Spacer -->\n <div class=\"tn-calendar-spacer\"></div>\n\n <!-- Previous button -->\n <button\n type=\"button\"\n class=\"tn-calendar-previous-button\"\n [attr.aria-label]=\"previousLabel()\"\n (click)=\"onPreviousClick()\">\n <svg viewBox=\"0 0 24 24\" focusable=\"false\" aria-hidden=\"true\">\n <path d=\"M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z\" />\n </svg>\n </button>\n\n <!-- Next button -->\n <button\n type=\"button\"\n class=\"tn-calendar-next-button\"\n [attr.aria-label]=\"nextLabel()\"\n (click)=\"onNextClick()\">\n <svg viewBox=\"0 0 24 24\" focusable=\"false\" aria-hidden=\"true\">\n <path d=\"M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z\" />\n </svg>\n </button>\n </div>\n</div>\n", styles: [".tn-calendar-header{display:flex;padding:16px}.tn-calendar-controls{display:flex;align-items:center;width:100%}.cdk-visually-hidden{position:absolute!important;width:1px!important;height:1px!important;padding:0!important;margin:-1px!important;overflow:hidden!important;clip:rect(0,0,0,0)!important;white-space:nowrap!important;border:0!important}.tn-calendar-period-button{background:none;border:none;font-weight:600;font-size:1rem;color:var(--tn-fg1, #333);padding:8px 12px;border-radius:4px;cursor:pointer;display:flex;align-items:center;gap:4px;transition:background-color .2s ease}.tn-calendar-period-button:hover{background:var(--tn-alt-bg2, #e8f4fd)}.tn-calendar-period-button:focus{outline:2px solid var(--tn-primary, #007bff);outline-offset:2px;background:var(--tn-alt-bg2, #e8f4fd)}.tn-calendar-arrow{width:10px;height:5px;fill:currentColor}.tn-calendar-spacer{flex:1}.tn-calendar-previous-button,.tn-calendar-next-button{background:none;border:none;width:40px;height:40px;border-radius:50%;cursor:pointer;display:flex;align-items:center;justify-content:center;color:var(--tn-fg1, #333);transition:background-color .2s ease}.tn-calendar-previous-button svg,.tn-calendar-next-button svg{width:24px;height:24px;fill:currentColor}.tn-calendar-previous-button:hover:not(:disabled),.tn-calendar-next-button:hover:not(:disabled){background:var(--tn-alt-bg2, #e8f4fd)}.tn-calendar-previous-button:focus,.tn-calendar-next-button:focus{outline:2px solid var(--tn-primary, #007bff);outline-offset:2px;background:var(--tn-alt-bg2, #e8f4fd)}.tn-calendar-previous-button:disabled,.tn-calendar-next-button:disabled{color:var(--tn-fg2, #666);opacity:.5;cursor:not-allowed}\n"] }]
13886
- }], propDecorators: { currentDate: [{ type: i0.Input, args: [{ isSignal: true, alias: "currentDate", required: false }] }], currentView: [{ type: i0.Input, args: [{ isSignal: true, alias: "currentView", required: false }] }], monthSelected: [{ type: i0.Output, args: ["monthSelected"] }], yearSelected: [{ type: i0.Output, args: ["yearSelected"] }], viewChanged: [{ type: i0.Output, args: ["viewChanged"] }], previousClicked: [{ type: i0.Output, args: ["previousClicked"] }], nextClicked: [{ type: i0.Output, args: ["nextClicked"] }] } });
14194
+ args: [{ selector: 'tn-calendar-header', standalone: true, imports: [], template: "<div class=\"tn-calendar-header\">\n <div class=\"tn-calendar-controls\">\n <!-- Period label (visually hidden for screen readers) -->\n <span aria-live=\"polite\" class=\"cdk-visually-hidden\" [id]=\"periodLabelId\">\n {{ periodLabel() }}\n </span>\n\n <!-- Period button (month/year selector) -->\n <button\n type=\"button\"\n class=\"tn-calendar-period-button\"\n [attr.aria-label]=\"periodButtonLabel()\"\n [attr.aria-describedby]=\"periodLabelId\"\n (click)=\"toggleView()\">\n <span [attr.aria-hidden]=\"true\">{{ periodLabel() }}</span>\n <svg viewBox=\"0 0 10 5\" focusable=\"false\" aria-hidden=\"true\" class=\"tn-calendar-arrow\">\n <polygon points=\"0,0 5,5 10,0\" />\n </svg>\n </button>\n\n <!-- Spacer -->\n <div class=\"tn-calendar-spacer\"></div>\n\n <!-- Previous button -->\n <button\n type=\"button\"\n class=\"tn-calendar-previous-button\"\n [attr.aria-label]=\"previousLabel()\"\n (click)=\"onPreviousClick()\">\n <svg viewBox=\"0 0 24 24\" focusable=\"false\" aria-hidden=\"true\">\n <path d=\"M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z\" />\n </svg>\n </button>\n\n <!-- Next button -->\n <button\n type=\"button\"\n class=\"tn-calendar-next-button\"\n [attr.aria-label]=\"nextLabel()\"\n (click)=\"onNextClick()\">\n <svg viewBox=\"0 0 24 24\" focusable=\"false\" aria-hidden=\"true\">\n <path d=\"M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z\" />\n </svg>\n </button>\n </div>\n</div>\n", styles: [".tn-calendar-header{display:flex;padding:16px}.tn-calendar-controls{display:flex;align-items:center;width:100%}.cdk-visually-hidden{position:absolute!important;width:1px!important;height:1px!important;padding:0!important;margin:-1px!important;overflow:hidden!important;clip:rect(0,0,0,0)!important;white-space:nowrap!important;border:0!important}.tn-calendar-period-button{background:none;border:none;font-weight:600;font-size:1rem;color:var(--tn-fg1, #333);padding:8px 12px;border-radius:4px;cursor:pointer;display:flex;align-items:center;gap:4px;transition:background-color .2s ease}.tn-calendar-period-button:hover{background:var(--tn-alt-bg2, #e8f4fd)}.tn-calendar-period-button:focus{outline:2px solid var(--tn-primary, #007bff);outline-offset:2px;background:var(--tn-alt-bg2, #e8f4fd)}.tn-calendar-arrow{width:10px;height:5px;fill:currentColor}.tn-calendar-spacer{flex:1}.tn-calendar-previous-button,.tn-calendar-next-button{background:none;border:none;width:40px;height:40px;border-radius:50%;cursor:pointer;display:flex;align-items:center;justify-content:center;color:var(--tn-fg1, #333);transition:background-color .2s ease}.tn-calendar-previous-button svg,.tn-calendar-next-button svg{width:24px;height:24px;fill:currentColor}.tn-calendar-previous-button:hover:not(:disabled),.tn-calendar-next-button:hover:not(:disabled){background:var(--tn-alt-bg2, #e8f4fd)}.tn-calendar-previous-button:focus,.tn-calendar-next-button:focus{outline:2px solid var(--tn-primary, #007bff);outline-offset:2px;background:var(--tn-alt-bg2, #e8f4fd)}.tn-calendar-previous-button:disabled,.tn-calendar-next-button:disabled{color:var(--tn-fg2, #666);opacity:.5;cursor:not-allowed}\n"] }]
14195
+ }], propDecorators: { currentDate: [{ type: i0.Input, args: [{ isSignal: true, alias: "currentDate", required: false }] }], currentView: [{ type: i0.Input, args: [{ isSignal: true, alias: "currentView", required: false }] }], locale: [{ type: i0.Input, args: [{ isSignal: true, alias: "locale", required: false }] }], viewChanged: [{ type: i0.Output, args: ["viewChanged"] }], previousClicked: [{ type: i0.Output, args: ["previousClicked"] }], nextClicked: [{ type: i0.Output, args: ["nextClicked"] }] } });
13887
14196
 
13888
14197
  class TnMonthViewComponent {
13889
14198
  activeDate = input(new Date(), ...(ngDevMode ? [{ debugName: "activeDate" }] : []));
@@ -13891,25 +14200,114 @@ class TnMonthViewComponent {
13891
14200
  minDate = input(undefined, ...(ngDevMode ? [{ debugName: "minDate" }] : []));
13892
14201
  maxDate = input(undefined, ...(ngDevMode ? [{ debugName: "maxDate" }] : []));
13893
14202
  dateFilter = input(undefined, ...(ngDevMode ? [{ debugName: "dateFilter" }] : []));
14203
+ /**
14204
+ * Dates to flag as noteworthy — days a task runs, days with events, and the like.
14205
+ * Order and time-of-day are ignored; only the calendar day is compared. The
14206
+ * calendar owns how a marked day looks, so callers pass dates rather than styles.
14207
+ */
14208
+ markedDates = input(undefined, ...(ngDevMode ? [{ debugName: "markedDates" }] : []));
13894
14209
  // Range mode inputs
13895
14210
  rangeMode = input(false, ...(ngDevMode ? [{ debugName: "rangeMode" }] : []));
13896
14211
  selectedRange = input(undefined, ...(ngDevMode ? [{ debugName: "selectedRange" }] : []));
13897
14212
  selectedChange = output();
13898
14213
  activeDateChange = output();
13899
- weekdays = [
13900
- { long: 'Sunday', short: 'S' },
13901
- { long: 'Monday', short: 'M' },
13902
- { long: 'Tuesday', short: 'T' },
13903
- { long: 'Wednesday', short: 'W' },
13904
- { long: 'Thursday', short: 'T' },
13905
- { long: 'Friday', short: 'F' },
13906
- { long: 'Saturday', short: 'S' },
13907
- ];
14214
+ /**
14215
+ * Locale for dates, weekday names and the first day of the week. Falls back to the
14216
+ * app's `LOCALE_ID`; `tn-calendar` binds it from its own `locale` input.
14217
+ */
14218
+ locale = input(undefined, ...(ngDevMode ? [{ debugName: "locale" }] : []));
14219
+ host = inject(ElementRef);
14220
+ injector = inject(Injector);
14221
+ appLocale = inject(LOCALE_ID);
14222
+ intl = injectTnCalendarIntl();
14223
+ resolvedLocale = computed(() => this.locale() ?? this.appLocale, ...(ngDevMode ? [{ debugName: "resolvedLocale" }] : []));
14224
+ /**
14225
+ * Which day the week starts on in this locale, as a `Date.getDay()` index. Everything
14226
+ * about the grid's shape hangs off it: the order of the headings and how many blank
14227
+ * cells precede the 1st.
14228
+ */
14229
+ weekStart = computed(() => firstDayOfWeek(this.resolvedLocale()), ...(ngDevMode ? [{ debugName: "weekStart" }] : []));
14230
+ /** Day numbers in the locale's own numerals, without a thousands separator. */
14231
+ dayFormat = computed(() => {
14232
+ return new Intl.NumberFormat(this.resolvedLocale(), { useGrouping: false });
14233
+ }, ...(ngDevMode ? [{ debugName: "dayFormat" }] : []));
14234
+ /**
14235
+ * Column headings, named and ordered by the locale — Sunday first in the US, Monday
14236
+ * across most of Europe. The dates are an arbitrary week, used only to name the days.
14237
+ */
14238
+ weekdays = computed(() => {
14239
+ const locale = this.resolvedLocale();
14240
+ const long = new Intl.DateTimeFormat(locale, { weekday: 'long' });
14241
+ const narrow = new Intl.DateTimeFormat(locale, { weekday: 'narrow' });
14242
+ const weekStart = this.weekStart();
14243
+ return Array.from({ length: 7 }, (_, column) => {
14244
+ // 7 Jan 2024 was a Sunday, so adding a getDay() index lands on that weekday.
14245
+ const date = new Date(2024, 0, 7 + ((weekStart + column) % 7));
14246
+ return { long: long.format(date), short: narrow.format(date) };
14247
+ });
14248
+ }, ...(ngDevMode ? [{ debugName: "weekdays" }] : []));
13908
14249
  // Cell sizing now controlled via CSS custom properties in the SCSS file
14250
+ markedDateKeys = computed(() => {
14251
+ return new Set((this.markedDates() ?? []).map((date) => dateKey(date)));
14252
+ }, ...(ngDevMode ? [{ debugName: "markedDateKeys" }] : []));
14253
+ /**
14254
+ * Names the grid after the month it shows. Without it a screen-reader user arrowing
14255
+ * around hears each day but never which month they are in — the period only appears in
14256
+ * the header, outside the grid.
14257
+ */
14258
+ gridLabel = computed(() => {
14259
+ const period = this.activeDate()
14260
+ .toLocaleDateString(this.resolvedLocale(), { month: 'long', year: 'numeric' });
14261
+ return this.intl().monthGridLabel(period);
14262
+ }, ...(ngDevMode ? [{ debugName: "gridLabel" }] : []));
14263
+ /**
14264
+ * The day of the month that carries `tabindex="0"` — the grid's single keyboard entry
14265
+ * point, per the roving tabindex pattern.
14266
+ *
14267
+ * It follows `activeDate`, but falls back to the nearest enabled day when that day
14268
+ * can't take focus. Without the fallback a month whose active day is disabled (say,
14269
+ * anything before `minDate`) would have no tabbable cell at all, leaving the grid
14270
+ * unreachable by keyboard. `null` only when the whole month is disabled, where there
14271
+ * is nothing worth focusing anyway.
14272
+ *
14273
+ * Derived from the month rather than from `calendarRows()`, which would be circular.
14274
+ */
14275
+ activeDay = computed(() => {
14276
+ const activeDate = this.activeDate();
14277
+ if (!activeDate) {
14278
+ return null;
14279
+ }
14280
+ const year = activeDate.getFullYear();
14281
+ const month = activeDate.getMonth();
14282
+ const lastDay = new Date(year, month + 1, 0).getDate();
14283
+ const isEnabled = (day) => this.isDateEnabled(new Date(year, month, day));
14284
+ // Always within the rendered month: both come off the same Date, and `Date` has
14285
+ // already rolled over any overflow the caller handed us.
14286
+ const wanted = activeDate.getDate();
14287
+ if (isEnabled(wanted)) {
14288
+ return wanted;
14289
+ }
14290
+ // Spiral outwards from the day we wanted, forward first.
14291
+ for (let offset = 1; offset < lastDay; offset++) {
14292
+ if (wanted + offset <= lastDay && isEnabled(wanted + offset)) {
14293
+ return wanted + offset;
14294
+ }
14295
+ if (wanted - offset >= 1 && isEnabled(wanted - offset)) {
14296
+ return wanted - offset;
14297
+ }
14298
+ }
14299
+ return null;
14300
+ }, ...(ngDevMode ? [{ debugName: "activeDay" }] : []));
14301
+ activeCellDate = computed(() => {
14302
+ const day = this.activeDay();
14303
+ if (day === null) {
14304
+ return null;
14305
+ }
14306
+ const activeDate = this.activeDate();
14307
+ return new Date(activeDate.getFullYear(), activeDate.getMonth(), day);
14308
+ }, ...(ngDevMode ? [{ debugName: "activeCellDate" }] : []));
13909
14309
  calendarRows = computed(() => {
13910
14310
  const activeDate = this.activeDate();
13911
- // Track selectedRange signal so computed recalculates when range changes
13912
- this.selectedRange();
13913
14311
  if (!activeDate) {
13914
14312
  return [];
13915
14313
  }
@@ -13917,7 +14315,13 @@ class TnMonthViewComponent {
13917
14315
  const month = activeDate.getMonth();
13918
14316
  const firstDate = new Date(year, month, 1);
13919
14317
  const lastDate = new Date(year, month + 1, 0);
13920
- const startDayOfWeek = firstDate.getDay(); // 0 = Sunday, 1 = Monday, etc.
14318
+ // How far into the week the 1st falls, counted from wherever this locale's week
14319
+ // begins — that many blanks lead the grid.
14320
+ const startDayOfWeek = (firstDate.getDay() - this.weekStart() + 7) % 7;
14321
+ // Read once for the whole grid rather than per cell, so every day is measured
14322
+ // against the same instant as well as allocating a fortieth as much.
14323
+ const today = new Date();
14324
+ const dayFormat = this.dayFormat();
13921
14325
  const rows = [];
13922
14326
  let currentRow = [];
13923
14327
  // Add empty cells for days before month starts
@@ -13927,7 +14331,7 @@ class TnMonthViewComponent {
13927
14331
  // Add all days of the month
13928
14332
  for (let day = 1; day <= lastDate.getDate(); day++) {
13929
14333
  const date = new Date(year, month, day);
13930
- currentRow.push(this.createCell(date, day));
14334
+ currentRow.push(this.createCell(date, day, today, dayFormat));
13931
14335
  // If we have 7 cells, complete the row
13932
14336
  if (currentRow.length === 7) {
13933
14337
  rows.push(currentRow);
@@ -13943,10 +14347,10 @@ class TnMonthViewComponent {
13943
14347
  }
13944
14348
  return rows;
13945
14349
  }, ...(ngDevMode ? [{ debugName: "calendarRows" }] : []));
13946
- createCell(date, value) {
13947
- const today = new Date();
13948
- const isToday = this.isSameDate(date, today);
13949
- const isSelected = this.selected() ? this.isSameDate(date, this.selected()) : false;
14350
+ createCell(date, value, today, dayFormat) {
14351
+ const isToday = isSameDay(date, today);
14352
+ const isSelected = this.selected() ? isSameDay(date, this.selected()) : false;
14353
+ const isMarked = this.markedDateKeys().has(dateKey(date));
13950
14354
  const enabled = this.isDateEnabled(date);
13951
14355
  // Range mode calculations
13952
14356
  let rangeStart = false;
@@ -13955,24 +14359,36 @@ class TnMonthViewComponent {
13955
14359
  const currentRange = this.selectedRange();
13956
14360
  if (this.rangeMode() && currentRange) {
13957
14361
  const { start, end } = currentRange;
13958
- if (start && this.isSameDate(date, start)) {
14362
+ if (start && isSameDay(date, start)) {
13959
14363
  rangeStart = true;
13960
14364
  }
13961
- if (end && this.isSameDate(date, end)) {
14365
+ if (end && isSameDay(date, end)) {
13962
14366
  rangeEnd = true;
13963
14367
  }
13964
- if (start && end && date > start && date < end) {
14368
+ // Strictly between the ends. Compared by day so a start or end carrying a time of
14369
+ // day can't come out as both a cap and an in-between day.
14370
+ if (start && end && compareDays(date, start) > 0 && compareDays(date, end) < 0) {
13965
14371
  inRange = true;
13966
14372
  }
13967
14373
  }
14374
+ // Selection and the range caps share one solid fill; marking sits below them and
14375
+ // shows only where neither applies. A cap is a selection too, so `today` must not
14376
+ // paint its outline over it.
14377
+ const isPrimary = isSelected || rangeStart || rangeEnd;
14378
+ const fill = isPrimary ? 'primary' : isMarked ? 'marked' : 'none';
13968
14379
  return {
13969
14380
  value,
13970
14381
  date: new Date(date),
13971
- label: date.getDate().toString(),
13972
- ariaLabel: this.formatAriaLabel(date, isSelected, isToday, rangeStart, rangeEnd, inRange),
14382
+ isoDate: isoDateString(date),
14383
+ label: dayFormat.format(value),
14384
+ ariaLabel: this.formatAriaLabel(date, isMarked, rangeStart, rangeEnd, inRange),
13973
14385
  enabled,
13974
14386
  selected: isSelected,
13975
14387
  today: isToday,
14388
+ marked: isMarked,
14389
+ inSelection: isSelected || rangeStart || rangeEnd || inRange,
14390
+ fill,
14391
+ todayOutline: isToday && !isPrimary,
13976
14392
  rangeStart,
13977
14393
  rangeEnd,
13978
14394
  inRange,
@@ -13982,21 +14398,28 @@ class TnMonthViewComponent {
13982
14398
  return {
13983
14399
  value: 0,
13984
14400
  date: new Date(),
14401
+ isoDate: '',
13985
14402
  label: '',
13986
14403
  ariaLabel: '',
13987
14404
  enabled: false,
13988
14405
  selected: false,
13989
14406
  today: false,
14407
+ marked: false,
14408
+ inSelection: false,
14409
+ fill: 'none',
14410
+ todayOutline: false,
13990
14411
  };
13991
14412
  }
13992
14413
  isDateEnabled(date) {
13993
14414
  const minDate = this.minDate();
13994
14415
  const maxDate = this.maxDate();
13995
14416
  const dateFilter = this.dateFilter();
13996
- if (minDate && date < minDate) {
14417
+ // Bounds are inclusive of their own day: a `minDate` of "today, 14:30" means today
14418
+ // is selectable, not that today is half past the deadline.
14419
+ if (minDate && compareDays(date, minDate) < 0) {
13997
14420
  return false;
13998
14421
  }
13999
- if (maxDate && date > maxDate) {
14422
+ if (maxDate && compareDays(date, maxDate) > 0) {
14000
14423
  return false;
14001
14424
  }
14002
14425
  if (dateFilter && !dateFilter(date)) {
@@ -14004,32 +14427,31 @@ class TnMonthViewComponent {
14004
14427
  }
14005
14428
  return true;
14006
14429
  }
14007
- isSameDate(date1, date2) {
14008
- return date1.getFullYear() === date2.getFullYear() &&
14009
- date1.getMonth() === date2.getMonth() &&
14010
- date1.getDate() === date2.getDate();
14011
- }
14012
- formatAriaLabel(date, isSelected, isToday, rangeStart, rangeEnd, inRange) {
14013
- let label = date.toLocaleDateString('en', {
14430
+ /**
14431
+ * Spells out the state a day is in, but only the parts no ARIA attribute already
14432
+ * carries. Selection lives on the button's `aria-pressed` and today on `aria-current`,
14433
+ * so repeating them here had screen readers announce each one twice. Marking and the
14434
+ * range positions have no attribute equivalent, and would otherwise be conveyed by
14435
+ * background colour alone.
14436
+ */
14437
+ formatAriaLabel(date, isMarked, rangeStart, rangeEnd, inRange) {
14438
+ let label = date.toLocaleDateString(this.resolvedLocale(), {
14014
14439
  weekday: 'long',
14015
14440
  year: 'numeric',
14016
14441
  month: 'long',
14017
14442
  day: 'numeric'
14018
14443
  });
14019
- if (isSelected) {
14020
- label += ' (selected)';
14021
- }
14022
- if (isToday) {
14023
- label += ' (today)';
14444
+ if (isMarked) {
14445
+ label += ` ${this.intl().marked}`;
14024
14446
  }
14025
14447
  if (rangeStart) {
14026
- label += ' (range start)';
14448
+ label += ` ${this.intl().rangeStart}`;
14027
14449
  }
14028
14450
  if (rangeEnd) {
14029
- label += ' (range end)';
14451
+ label += ` ${this.intl().rangeEnd}`;
14030
14452
  }
14031
14453
  if (inRange) {
14032
- label += ' (in range)';
14454
+ label += ` ${this.intl().inRange}`;
14033
14455
  }
14034
14456
  return label;
14035
14457
  }
@@ -14037,23 +14459,107 @@ class TnMonthViewComponent {
14037
14459
  if (cell.value === 0) {
14038
14460
  return `empty-${index}`;
14039
14461
  }
14040
- return `${cell.date.getFullYear()}-${cell.date.getMonth()}-${cell.date.getDate()}`;
14462
+ return dateKey(cell.date);
14041
14463
  }
14042
14464
  trackByRow(index) {
14043
14465
  return index;
14044
14466
  }
14045
14467
  onCellClicked(cell) {
14046
- if (cell.enabled && cell.value > 0) {
14047
- this.selectedChange.emit(cell.date);
14468
+ if (!cell.enabled || cell.value === 0) {
14469
+ return;
14470
+ }
14471
+ // The click already moved browser focus here, so the roving tabindex has to come
14472
+ // along or the two diverge: arrowing on from a clicked day would carry on from
14473
+ // wherever the active day happened to be, jumping the focus somewhere unrelated.
14474
+ this.activeDateChange.emit(cell.date);
14475
+ this.selectedChange.emit(cell.date);
14476
+ }
14477
+ /**
14478
+ * Moves the roving tabindex. Selection stays on click/Enter/Space, which the cells
14479
+ * are already buttons for, so this only ever changes which day is active.
14480
+ *
14481
+ * Moves that leave the displayed month emit an `activeDateChange` like any other
14482
+ * navigation; the grid re-renders on the new month with the target day active.
14483
+ */
14484
+ onKeydown(event) {
14485
+ const from = this.activeCellDate();
14486
+ if (!from) {
14487
+ return;
14488
+ }
14489
+ const move = this.targetForKey(event, from);
14490
+ if (!move) {
14491
+ return;
14492
+ }
14493
+ // Claim the key before the browser scrolls the page with it.
14494
+ event.preventDefault();
14495
+ const landing = this.nearestEnabled(move.date, move.search);
14496
+ if (!landing || isSameDay(landing, from)) {
14497
+ return;
14048
14498
  }
14499
+ this.activeDateChange.emit(landing);
14500
+ focusActiveCellAfterRender(this.host, this.injector);
14501
+ }
14502
+ /**
14503
+ * The day a key aims at, and which way to look from there when that day is disabled.
14504
+ *
14505
+ * The search direction belongs to the key, not to where the target happens to fall
14506
+ * relative to the current day. Home and End mean "the ends of *this* month", so they
14507
+ * search inward: looking outward would step straight into the neighbouring month over
14508
+ * a single filtered-out boundary day and page the view, which is not what either key
14509
+ * promises. The arrows and the paging keys keep travelling the way they were already
14510
+ * headed, and crossing into the next month is the point of those.
14511
+ */
14512
+ targetForKey(event, from) {
14513
+ const year = from.getFullYear();
14514
+ const month = from.getMonth();
14515
+ const day = from.getDate();
14516
+ // Overshooting a month's length is fine — the Date constructor rolls it over, which
14517
+ // is exactly what arrowing off the end of a month should do.
14518
+ switch (event.key) {
14519
+ case 'ArrowLeft': return { date: new Date(year, month, day - 1), search: -1 };
14520
+ case 'ArrowRight': return { date: new Date(year, month, day + 1), search: 1 };
14521
+ case 'ArrowUp': return { date: new Date(year, month, day - 7), search: -1 };
14522
+ case 'ArrowDown': return { date: new Date(year, month, day + 7), search: 1 };
14523
+ case 'Home': return { date: new Date(year, month, 1), search: 1 };
14524
+ case 'End': return { date: new Date(year, month + 1, 0), search: -1 };
14525
+ // Shift pages by a year, matching Material and the wider grid convention.
14526
+ case 'PageUp': return { date: addMonths(from, event.shiftKey ? -12 : -1), search: -1 };
14527
+ case 'PageDown': return { date: addMonths(from, event.shiftKey ? 12 : 1), search: 1 };
14528
+ default: return null;
14529
+ }
14530
+ }
14531
+ /**
14532
+ * Steps past days the caller has disabled, so a gap in `dateFilter` or the far side of
14533
+ * `minDate`/`maxDate` doesn't park focus somewhere unusable.
14534
+ *
14535
+ * Searches the way the key asked for (see `targetForKey`), then doubles back if that
14536
+ * finds nothing. The fallback is for the ends of the allowed range: arrowing right at
14537
+ * `maxDate` finds nothing ahead, doubles back onto the day we started from, and the
14538
+ * caller reads that as "don't move".
14539
+ */
14540
+ nearestEnabled(target, preferred) {
14541
+ return this.scanForEnabled(target, preferred)
14542
+ ?? this.scanForEnabled(target, preferred === 1 ? -1 : 1);
14543
+ }
14544
+ /** Bounded, so an everything-disabled filter stops the move instead of spinning. */
14545
+ scanForEnabled(from, direction) {
14546
+ const maxSteps = 62;
14547
+ const candidate = new Date(from);
14548
+ for (let step = 0; step <= maxSteps; step++) {
14549
+ if (this.isDateEnabled(candidate)) {
14550
+ return new Date(candidate);
14551
+ }
14552
+ candidate.setDate(candidate.getDate() + direction);
14553
+ }
14554
+ return null;
14049
14555
  }
14050
14556
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnMonthViewComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
14051
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.0", type: TnMonthViewComponent, isStandalone: true, selector: "tn-month-view", inputs: { activeDate: { classPropertyName: "activeDate", publicName: "activeDate", isSignal: true, isRequired: false, transformFunction: null }, selected: { classPropertyName: "selected", publicName: "selected", isSignal: true, isRequired: false, transformFunction: null }, minDate: { classPropertyName: "minDate", publicName: "minDate", isSignal: true, isRequired: false, transformFunction: null }, maxDate: { classPropertyName: "maxDate", publicName: "maxDate", isSignal: true, isRequired: false, transformFunction: null }, dateFilter: { classPropertyName: "dateFilter", publicName: "dateFilter", isSignal: true, isRequired: false, transformFunction: null }, rangeMode: { classPropertyName: "rangeMode", publicName: "rangeMode", isSignal: true, isRequired: false, transformFunction: null }, selectedRange: { classPropertyName: "selectedRange", publicName: "selectedRange", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { selectedChange: "selectedChange", activeDateChange: "activeDateChange" }, ngImport: i0, template: "<table role=\"grid\" class=\"tn-calendar-table\">\n <!-- Table header with day names -->\n <thead class=\"tn-calendar-table-header\">\n <tr>\n @for (day of weekdays; track day) {\n <th scope=\"col\">\n <span class=\"cdk-visually-hidden\">{{ day.long }}</span>\n <span aria-hidden=\"true\">{{ day.short }}</span>\n </th>\n }\n </tr>\n </thead>\n\n <!-- Table body with calendar cells -->\n <tbody class=\"tn-calendar-body\">\n <!-- Calendar rows -->\n @for (row of calendarRows(); track trackByRow($index); let rowIndex = $index) {\n <tr role=\"row\">\n @for (cell of row; track trackByDate($index, cell); let colIndex = $index) {\n <td\n role=\"gridcell\"\n class=\"tn-calendar-body-cell-container\"\n [attr.data-tn-row]=\"rowIndex\"\n [attr.data-tn-col]=\"colIndex\">\n @if (cell.value > 0) {\n <button\n type=\"button\"\n class=\"tn-calendar-body-cell\"\n [class.tn-calendar-body-selected]=\"cell.selected\"\n [class.tn-calendar-body-today]=\"cell.today\"\n [class.tn-calendar-body-active]=\"cell.selected\"\n [class.tn-calendar-body-range-start]=\"cell.rangeStart\"\n [class.tn-calendar-body-range-end]=\"cell.rangeEnd\"\n [class.tn-calendar-body-in-range]=\"cell.inRange\"\n [disabled]=\"!cell.enabled\"\n [attr.tabindex]=\"cell.selected ? 0 : -1\"\n [attr.aria-label]=\"cell.ariaLabel\"\n [attr.aria-pressed]=\"cell.selected\"\n [attr.aria-current]=\"cell.today ? 'date' : null\"\n (click)=\"onCellClicked(cell)\">\n <span class=\"tn-calendar-body-cell-content tn-focus-indicator\"\n [class.tn-calendar-body-selected]=\"cell.selected\"\n [class.tn-calendar-body-today]=\"cell.today\"\n [class.tn-calendar-body-range-start]=\"cell.rangeStart\"\n [class.tn-calendar-body-range-end]=\"cell.rangeEnd\"\n [class.tn-calendar-body-in-range]=\"cell.inRange\">\n {{ cell.value }}\n </span>\n <span aria-hidden=\"true\" class=\"tn-calendar-body-cell-preview\"></span>\n </button>\n }\n </td>\n }\n </tr>\n }\n </tbody>\n</table>\n", styles: [":host{--calendar-cell-size: 48px;--calendar-header-height: 40px;--calendar-cell-font-size: 1rem;--calendar-header-font-size: 14px}.tn-calendar-table{width:calc(7 * var(--calendar-cell-size));border-spacing:0;border-collapse:separate}.tn-calendar-table-header th{text-align:center;height:var(--calendar-header-height);padding:8px 0;font-size:var(--calendar-header-font-size);font-weight:500;color:var(--tn-fg2, #666)}.tn-calendar-table-header-divider{height:1px;border:0}.tn-calendar-body tr{border:0}.tn-calendar-body-cell-container{position:relative;border:0;outline:0;height:var(--calendar-cell-size);width:14.2857142857%}.tn-calendar-body-cell{position:absolute;inset:0;margin:auto;background:transparent;border:0;outline:0;cursor:pointer;color:var(--tn-fg1, #333);width:var(--calendar-cell-size);height:var(--calendar-cell-size)}.tn-calendar-body-cell:not(:disabled):hover:not(.tn-calendar-body-selected):not(.tn-calendar-body-range-start):not(.tn-calendar-body-range-end):not(.tn-calendar-body-in-range){background:var(--tn-alt-bg2, #e8f4fd)}.tn-calendar-body-cell:focus{outline:2px solid var(--tn-primary, #007bff);outline-offset:2px}.tn-calendar-body-cell:focus .tn-calendar-body-cell-content.tn-focus-indicator{background:var(--tn-alt-bg2, #e8f4fd)}.tn-calendar-body-cell:disabled{color:var(--tn-fg2, #666);opacity:.5;cursor:default}.tn-calendar-body-cell.tn-calendar-body-today:not(.tn-calendar-body-selected){border:1px solid var(--tn-primary, #007bff);color:var(--tn-primary, #007bff)}.tn-calendar-body-cell.tn-calendar-body-selected,.tn-calendar-body-cell.tn-calendar-body-range-start,.tn-calendar-body-cell.tn-calendar-body-range-end,.tn-calendar-body-cell.tn-calendar-body-in-range{background:var(--tn-primary, #007bff);color:#fff}.tn-calendar-body-cell-content{position:relative;display:flex;align-items:center;justify-content:center;box-sizing:border-box;width:100%;height:100%;font-size:var(--calendar-cell-font-size);font-weight:400;transition:background-color .2s cubic-bezier(.25,.8,.25,1)}.tn-calendar-body-cell-preview{position:absolute;inset:0;background:transparent}.cdk-visually-hidden{position:absolute!important;width:1px!important;height:1px!important;padding:0!important;margin:-1px!important;overflow:hidden!important;clip:rect(0,0,0,0)!important;white-space:nowrap!important;border:0!important}.tn-focus-indicator{position:relative}.tn-focus-indicator:before{content:\"\";position:absolute;inset:0;opacity:0;background:currentColor;transition:opacity .2s cubic-bezier(.25,.8,.25,1)}\n"] });
14557
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.0", type: TnMonthViewComponent, isStandalone: true, selector: "tn-month-view", inputs: { activeDate: { classPropertyName: "activeDate", publicName: "activeDate", isSignal: true, isRequired: false, transformFunction: null }, selected: { classPropertyName: "selected", publicName: "selected", isSignal: true, isRequired: false, transformFunction: null }, minDate: { classPropertyName: "minDate", publicName: "minDate", isSignal: true, isRequired: false, transformFunction: null }, maxDate: { classPropertyName: "maxDate", publicName: "maxDate", isSignal: true, isRequired: false, transformFunction: null }, dateFilter: { classPropertyName: "dateFilter", publicName: "dateFilter", isSignal: true, isRequired: false, transformFunction: null }, markedDates: { classPropertyName: "markedDates", publicName: "markedDates", isSignal: true, isRequired: false, transformFunction: null }, rangeMode: { classPropertyName: "rangeMode", publicName: "rangeMode", isSignal: true, isRequired: false, transformFunction: null }, selectedRange: { classPropertyName: "selectedRange", publicName: "selectedRange", isSignal: true, isRequired: false, transformFunction: null }, locale: { classPropertyName: "locale", publicName: "locale", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { selectedChange: "selectedChange", activeDateChange: "activeDateChange" }, ngImport: i0, template: "<table role=\"grid\" class=\"tn-calendar-table\" [attr.aria-label]=\"gridLabel()\">\n <!-- Table header with day names -->\n <thead class=\"tn-calendar-table-header\">\n <tr>\n @for (day of weekdays(); track day.long) {\n <th scope=\"col\">\n <span class=\"cdk-visually-hidden\">{{ day.long }}</span>\n <span aria-hidden=\"true\">{{ day.short }}</span>\n </th>\n }\n </tr>\n </thead>\n\n <!-- Table body with calendar cells -->\n <tbody class=\"tn-calendar-body\">\n <!-- Calendar rows -->\n @for (row of calendarRows(); track trackByRow($index); let rowIndex = $index) {\n <tr role=\"row\">\n @for (cell of row; track trackByDate($index, cell); let colIndex = $index) {\n <td\n role=\"gridcell\"\n class=\"tn-calendar-body-cell-container\"\n [attr.data-tn-row]=\"rowIndex\"\n [attr.data-tn-col]=\"colIndex\">\n @if (cell.value > 0) {\n <button\n type=\"button\"\n class=\"tn-calendar-body-cell\"\n [attr.data-tn-date]=\"cell.isoDate\"\n [class.tn-calendar-body-selected]=\"cell.selected\"\n [class.tn-calendar-body-today]=\"cell.today\"\n [class.tn-calendar-body-marked]=\"cell.marked\"\n [class.tn-calendar-body-active]=\"cell.value === activeDay()\"\n [class.tn-calendar-body-range-start]=\"cell.rangeStart\"\n [class.tn-calendar-body-range-end]=\"cell.rangeEnd\"\n [class.tn-calendar-body-in-range]=\"cell.inRange\"\n [disabled]=\"!cell.enabled\"\n [attr.tabindex]=\"cell.value === activeDay() ? 0 : -1\"\n [attr.aria-label]=\"cell.ariaLabel\"\n [attr.aria-pressed]=\"cell.inSelection\"\n [attr.aria-current]=\"cell.today ? 'date' : null\"\n (click)=\"onCellClicked(cell)\"\n (keydown)=\"onKeydown($event)\">\n <span class=\"tn-calendar-body-cell-content\"\n [class.tn-calendar-body-fill-primary]=\"cell.fill === 'primary'\"\n [class.tn-calendar-body-fill-marked]=\"cell.fill === 'marked'\"\n [class.tn-calendar-body-today-outline]=\"cell.todayOutline\">\n {{ cell.label }}\n </span>\n </button>\n }\n </td>\n }\n </tr>\n }\n </tbody>\n</table>\n", styles: [":host{display:block;--calendar-cell-size: 48px;--calendar-header-height: 40px;--calendar-cell-font-size: 1rem;--calendar-header-font-size: 14px;--calendar-indicator-size: var(--calendar-cell-size);--calendar-radius: 4px}.tn-calendar-table{width:100%;min-width:min(100%,7 * var(--calendar-cell-size));table-layout:fixed;border-spacing:0;border-collapse:separate}.tn-calendar-table-header th{text-align:center;height:var(--calendar-header-height);padding:8px 0;font-size:var(--calendar-header-font-size);font-weight:500;color:var(--tn-fg2, #666)}.tn-calendar-table-header th:first-child{border-start-start-radius:var(--calendar-radius);border-end-start-radius:var(--calendar-radius)}.tn-calendar-table-header th:last-child{border-start-end-radius:var(--calendar-radius);border-end-end-radius:var(--calendar-radius)}.tn-calendar-table-header-divider{height:1px;border:0}.tn-calendar-body tr{border:0}.tn-calendar-body-cell-container{position:relative;border:0;outline:0;height:var(--calendar-cell-size);width:14.2857142857%}.tn-calendar-body-cell{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;width:100%;height:100%;padding:0;background:transparent;border:0;outline:0;cursor:pointer;color:var(--tn-fg1, #333)}.tn-calendar-body-cell:focus-visible{outline:none}.tn-calendar-body-cell:focus-visible .tn-calendar-body-cell-content{outline:2px solid var(--tn-primary, #007bff);outline-offset:-2px}.tn-calendar-body-cell:disabled{color:var(--tn-fg2, #666);opacity:.5;cursor:default}.tn-calendar-body-cell.tn-calendar-body-in-range:before,.tn-calendar-body-cell.tn-calendar-body-range-start:before,.tn-calendar-body-cell.tn-calendar-body-range-end:before{content:\"\";position:absolute;top:50%;transform:translateY(-50%);height:var(--calendar-indicator-size);background:var(--tn-alt-bg2, #e8f4fd)}.tn-calendar-body-cell.tn-calendar-body-in-range:before{inset-inline:0}.tn-calendar-body-cell.tn-calendar-body-range-start:before{inset-inline-start:50%;inset-inline-end:0}.tn-calendar-body-cell.tn-calendar-body-range-end:before{inset-inline-start:0;inset-inline-end:50%}.tn-calendar-body-cell.tn-calendar-body-range-start.tn-calendar-body-range-end:before{content:none}.tn-calendar-body-cell-content{position:relative;display:flex;align-items:center;justify-content:center;box-sizing:border-box;width:var(--calendar-indicator-size);height:var(--calendar-indicator-size);max-width:100%;border-radius:var(--calendar-radius);font-size:var(--calendar-cell-font-size);font-weight:400;transition:background-color .2s cubic-bezier(.25,.8,.25,1)}.tn-calendar-body-cell:not(:disabled):hover .tn-calendar-body-cell-content:not(.tn-calendar-body-fill-primary){background:var(--tn-alt-bg2, #e8f4fd)}.tn-calendar-body-cell-content.tn-calendar-body-fill-marked{background:var(--tn-alt-bg1, #f0f0f0)}.tn-calendar-body-cell-content.tn-calendar-body-fill-primary{background:var(--tn-primary, #007bff);color:var(--tn-primary-txt, #fff)}.tn-calendar-body-cell-content.tn-calendar-body-today-outline{border:1px solid var(--tn-primary, #007bff);color:var(--tn-primary, #007bff)}.cdk-visually-hidden{position:absolute!important;width:1px!important;height:1px!important;padding:0!important;margin:-1px!important;overflow:hidden!important;clip:rect(0,0,0,0)!important;white-space:nowrap!important;border:0!important}\n"] });
14052
14558
  }
14053
14559
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnMonthViewComponent, decorators: [{
14054
14560
  type: Component,
14055
- args: [{ selector: 'tn-month-view', standalone: true, imports: [], template: "<table role=\"grid\" class=\"tn-calendar-table\">\n <!-- Table header with day names -->\n <thead class=\"tn-calendar-table-header\">\n <tr>\n @for (day of weekdays; track day) {\n <th scope=\"col\">\n <span class=\"cdk-visually-hidden\">{{ day.long }}</span>\n <span aria-hidden=\"true\">{{ day.short }}</span>\n </th>\n }\n </tr>\n </thead>\n\n <!-- Table body with calendar cells -->\n <tbody class=\"tn-calendar-body\">\n <!-- Calendar rows -->\n @for (row of calendarRows(); track trackByRow($index); let rowIndex = $index) {\n <tr role=\"row\">\n @for (cell of row; track trackByDate($index, cell); let colIndex = $index) {\n <td\n role=\"gridcell\"\n class=\"tn-calendar-body-cell-container\"\n [attr.data-tn-row]=\"rowIndex\"\n [attr.data-tn-col]=\"colIndex\">\n @if (cell.value > 0) {\n <button\n type=\"button\"\n class=\"tn-calendar-body-cell\"\n [class.tn-calendar-body-selected]=\"cell.selected\"\n [class.tn-calendar-body-today]=\"cell.today\"\n [class.tn-calendar-body-active]=\"cell.selected\"\n [class.tn-calendar-body-range-start]=\"cell.rangeStart\"\n [class.tn-calendar-body-range-end]=\"cell.rangeEnd\"\n [class.tn-calendar-body-in-range]=\"cell.inRange\"\n [disabled]=\"!cell.enabled\"\n [attr.tabindex]=\"cell.selected ? 0 : -1\"\n [attr.aria-label]=\"cell.ariaLabel\"\n [attr.aria-pressed]=\"cell.selected\"\n [attr.aria-current]=\"cell.today ? 'date' : null\"\n (click)=\"onCellClicked(cell)\">\n <span class=\"tn-calendar-body-cell-content tn-focus-indicator\"\n [class.tn-calendar-body-selected]=\"cell.selected\"\n [class.tn-calendar-body-today]=\"cell.today\"\n [class.tn-calendar-body-range-start]=\"cell.rangeStart\"\n [class.tn-calendar-body-range-end]=\"cell.rangeEnd\"\n [class.tn-calendar-body-in-range]=\"cell.inRange\">\n {{ cell.value }}\n </span>\n <span aria-hidden=\"true\" class=\"tn-calendar-body-cell-preview\"></span>\n </button>\n }\n </td>\n }\n </tr>\n }\n </tbody>\n</table>\n", styles: [":host{--calendar-cell-size: 48px;--calendar-header-height: 40px;--calendar-cell-font-size: 1rem;--calendar-header-font-size: 14px}.tn-calendar-table{width:calc(7 * var(--calendar-cell-size));border-spacing:0;border-collapse:separate}.tn-calendar-table-header th{text-align:center;height:var(--calendar-header-height);padding:8px 0;font-size:var(--calendar-header-font-size);font-weight:500;color:var(--tn-fg2, #666)}.tn-calendar-table-header-divider{height:1px;border:0}.tn-calendar-body tr{border:0}.tn-calendar-body-cell-container{position:relative;border:0;outline:0;height:var(--calendar-cell-size);width:14.2857142857%}.tn-calendar-body-cell{position:absolute;inset:0;margin:auto;background:transparent;border:0;outline:0;cursor:pointer;color:var(--tn-fg1, #333);width:var(--calendar-cell-size);height:var(--calendar-cell-size)}.tn-calendar-body-cell:not(:disabled):hover:not(.tn-calendar-body-selected):not(.tn-calendar-body-range-start):not(.tn-calendar-body-range-end):not(.tn-calendar-body-in-range){background:var(--tn-alt-bg2, #e8f4fd)}.tn-calendar-body-cell:focus{outline:2px solid var(--tn-primary, #007bff);outline-offset:2px}.tn-calendar-body-cell:focus .tn-calendar-body-cell-content.tn-focus-indicator{background:var(--tn-alt-bg2, #e8f4fd)}.tn-calendar-body-cell:disabled{color:var(--tn-fg2, #666);opacity:.5;cursor:default}.tn-calendar-body-cell.tn-calendar-body-today:not(.tn-calendar-body-selected){border:1px solid var(--tn-primary, #007bff);color:var(--tn-primary, #007bff)}.tn-calendar-body-cell.tn-calendar-body-selected,.tn-calendar-body-cell.tn-calendar-body-range-start,.tn-calendar-body-cell.tn-calendar-body-range-end,.tn-calendar-body-cell.tn-calendar-body-in-range{background:var(--tn-primary, #007bff);color:#fff}.tn-calendar-body-cell-content{position:relative;display:flex;align-items:center;justify-content:center;box-sizing:border-box;width:100%;height:100%;font-size:var(--calendar-cell-font-size);font-weight:400;transition:background-color .2s cubic-bezier(.25,.8,.25,1)}.tn-calendar-body-cell-preview{position:absolute;inset:0;background:transparent}.cdk-visually-hidden{position:absolute!important;width:1px!important;height:1px!important;padding:0!important;margin:-1px!important;overflow:hidden!important;clip:rect(0,0,0,0)!important;white-space:nowrap!important;border:0!important}.tn-focus-indicator{position:relative}.tn-focus-indicator:before{content:\"\";position:absolute;inset:0;opacity:0;background:currentColor;transition:opacity .2s cubic-bezier(.25,.8,.25,1)}\n"] }]
14056
- }], propDecorators: { activeDate: [{ type: i0.Input, args: [{ isSignal: true, alias: "activeDate", required: false }] }], selected: [{ type: i0.Input, args: [{ isSignal: true, alias: "selected", required: false }] }], minDate: [{ type: i0.Input, args: [{ isSignal: true, alias: "minDate", required: false }] }], maxDate: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxDate", required: false }] }], dateFilter: [{ type: i0.Input, args: [{ isSignal: true, alias: "dateFilter", required: false }] }], rangeMode: [{ type: i0.Input, args: [{ isSignal: true, alias: "rangeMode", required: false }] }], selectedRange: [{ type: i0.Input, args: [{ isSignal: true, alias: "selectedRange", required: false }] }], selectedChange: [{ type: i0.Output, args: ["selectedChange"] }], activeDateChange: [{ type: i0.Output, args: ["activeDateChange"] }] } });
14561
+ args: [{ selector: 'tn-month-view', standalone: true, imports: [], template: "<table role=\"grid\" class=\"tn-calendar-table\" [attr.aria-label]=\"gridLabel()\">\n <!-- Table header with day names -->\n <thead class=\"tn-calendar-table-header\">\n <tr>\n @for (day of weekdays(); track day.long) {\n <th scope=\"col\">\n <span class=\"cdk-visually-hidden\">{{ day.long }}</span>\n <span aria-hidden=\"true\">{{ day.short }}</span>\n </th>\n }\n </tr>\n </thead>\n\n <!-- Table body with calendar cells -->\n <tbody class=\"tn-calendar-body\">\n <!-- Calendar rows -->\n @for (row of calendarRows(); track trackByRow($index); let rowIndex = $index) {\n <tr role=\"row\">\n @for (cell of row; track trackByDate($index, cell); let colIndex = $index) {\n <td\n role=\"gridcell\"\n class=\"tn-calendar-body-cell-container\"\n [attr.data-tn-row]=\"rowIndex\"\n [attr.data-tn-col]=\"colIndex\">\n @if (cell.value > 0) {\n <button\n type=\"button\"\n class=\"tn-calendar-body-cell\"\n [attr.data-tn-date]=\"cell.isoDate\"\n [class.tn-calendar-body-selected]=\"cell.selected\"\n [class.tn-calendar-body-today]=\"cell.today\"\n [class.tn-calendar-body-marked]=\"cell.marked\"\n [class.tn-calendar-body-active]=\"cell.value === activeDay()\"\n [class.tn-calendar-body-range-start]=\"cell.rangeStart\"\n [class.tn-calendar-body-range-end]=\"cell.rangeEnd\"\n [class.tn-calendar-body-in-range]=\"cell.inRange\"\n [disabled]=\"!cell.enabled\"\n [attr.tabindex]=\"cell.value === activeDay() ? 0 : -1\"\n [attr.aria-label]=\"cell.ariaLabel\"\n [attr.aria-pressed]=\"cell.inSelection\"\n [attr.aria-current]=\"cell.today ? 'date' : null\"\n (click)=\"onCellClicked(cell)\"\n (keydown)=\"onKeydown($event)\">\n <span class=\"tn-calendar-body-cell-content\"\n [class.tn-calendar-body-fill-primary]=\"cell.fill === 'primary'\"\n [class.tn-calendar-body-fill-marked]=\"cell.fill === 'marked'\"\n [class.tn-calendar-body-today-outline]=\"cell.todayOutline\">\n {{ cell.label }}\n </span>\n </button>\n }\n </td>\n }\n </tr>\n }\n </tbody>\n</table>\n", styles: [":host{display:block;--calendar-cell-size: 48px;--calendar-header-height: 40px;--calendar-cell-font-size: 1rem;--calendar-header-font-size: 14px;--calendar-indicator-size: var(--calendar-cell-size);--calendar-radius: 4px}.tn-calendar-table{width:100%;min-width:min(100%,7 * var(--calendar-cell-size));table-layout:fixed;border-spacing:0;border-collapse:separate}.tn-calendar-table-header th{text-align:center;height:var(--calendar-header-height);padding:8px 0;font-size:var(--calendar-header-font-size);font-weight:500;color:var(--tn-fg2, #666)}.tn-calendar-table-header th:first-child{border-start-start-radius:var(--calendar-radius);border-end-start-radius:var(--calendar-radius)}.tn-calendar-table-header th:last-child{border-start-end-radius:var(--calendar-radius);border-end-end-radius:var(--calendar-radius)}.tn-calendar-table-header-divider{height:1px;border:0}.tn-calendar-body tr{border:0}.tn-calendar-body-cell-container{position:relative;border:0;outline:0;height:var(--calendar-cell-size);width:14.2857142857%}.tn-calendar-body-cell{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;width:100%;height:100%;padding:0;background:transparent;border:0;outline:0;cursor:pointer;color:var(--tn-fg1, #333)}.tn-calendar-body-cell:focus-visible{outline:none}.tn-calendar-body-cell:focus-visible .tn-calendar-body-cell-content{outline:2px solid var(--tn-primary, #007bff);outline-offset:-2px}.tn-calendar-body-cell:disabled{color:var(--tn-fg2, #666);opacity:.5;cursor:default}.tn-calendar-body-cell.tn-calendar-body-in-range:before,.tn-calendar-body-cell.tn-calendar-body-range-start:before,.tn-calendar-body-cell.tn-calendar-body-range-end:before{content:\"\";position:absolute;top:50%;transform:translateY(-50%);height:var(--calendar-indicator-size);background:var(--tn-alt-bg2, #e8f4fd)}.tn-calendar-body-cell.tn-calendar-body-in-range:before{inset-inline:0}.tn-calendar-body-cell.tn-calendar-body-range-start:before{inset-inline-start:50%;inset-inline-end:0}.tn-calendar-body-cell.tn-calendar-body-range-end:before{inset-inline-start:0;inset-inline-end:50%}.tn-calendar-body-cell.tn-calendar-body-range-start.tn-calendar-body-range-end:before{content:none}.tn-calendar-body-cell-content{position:relative;display:flex;align-items:center;justify-content:center;box-sizing:border-box;width:var(--calendar-indicator-size);height:var(--calendar-indicator-size);max-width:100%;border-radius:var(--calendar-radius);font-size:var(--calendar-cell-font-size);font-weight:400;transition:background-color .2s cubic-bezier(.25,.8,.25,1)}.tn-calendar-body-cell:not(:disabled):hover .tn-calendar-body-cell-content:not(.tn-calendar-body-fill-primary){background:var(--tn-alt-bg2, #e8f4fd)}.tn-calendar-body-cell-content.tn-calendar-body-fill-marked{background:var(--tn-alt-bg1, #f0f0f0)}.tn-calendar-body-cell-content.tn-calendar-body-fill-primary{background:var(--tn-primary, #007bff);color:var(--tn-primary-txt, #fff)}.tn-calendar-body-cell-content.tn-calendar-body-today-outline{border:1px solid var(--tn-primary, #007bff);color:var(--tn-primary, #007bff)}.cdk-visually-hidden{position:absolute!important;width:1px!important;height:1px!important;padding:0!important;margin:-1px!important;overflow:hidden!important;clip:rect(0,0,0,0)!important;white-space:nowrap!important;border:0!important}\n"] }]
14562
+ }], propDecorators: { activeDate: [{ type: i0.Input, args: [{ isSignal: true, alias: "activeDate", required: false }] }], selected: [{ type: i0.Input, args: [{ isSignal: true, alias: "selected", required: false }] }], minDate: [{ type: i0.Input, args: [{ isSignal: true, alias: "minDate", required: false }] }], maxDate: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxDate", required: false }] }], dateFilter: [{ type: i0.Input, args: [{ isSignal: true, alias: "dateFilter", required: false }] }], markedDates: [{ type: i0.Input, args: [{ isSignal: true, alias: "markedDates", required: false }] }], rangeMode: [{ type: i0.Input, args: [{ isSignal: true, alias: "rangeMode", required: false }] }], selectedRange: [{ type: i0.Input, args: [{ isSignal: true, alias: "selectedRange", required: false }] }], selectedChange: [{ type: i0.Output, args: ["selectedChange"] }], activeDateChange: [{ type: i0.Output, args: ["activeDateChange"] }], locale: [{ type: i0.Input, args: [{ isSignal: true, alias: "locale", required: false }] }] } });
14057
14563
 
14058
14564
  class TnMultiYearViewComponent {
14059
14565
  activeDate = input(new Date(), ...(ngDevMode ? [{ debugName: "activeDate" }] : []));
@@ -14061,46 +14567,91 @@ class TnMultiYearViewComponent {
14061
14567
  minDate = input(undefined, ...(ngDevMode ? [{ debugName: "minDate" }] : []));
14062
14568
  maxDate = input(undefined, ...(ngDevMode ? [{ debugName: "maxDate" }] : []));
14063
14569
  dateFilter = input(undefined, ...(ngDevMode ? [{ debugName: "dateFilter" }] : []));
14570
+ /**
14571
+ * Locale for the year numerals — Arabic-Indic digits in `ar`, Western in `en`. Falls
14572
+ * back to the app's `LOCALE_ID`; `tn-calendar` binds it from its own `locale` input.
14573
+ */
14574
+ locale = input(undefined, ...(ngDevMode ? [{ debugName: "locale" }] : []));
14064
14575
  selectedChange = output();
14065
14576
  activeDateChange = output();
14066
- cellWidth = 25; // 100/4 for 4 columns
14067
- cellAspectRatio = 7.14286; // Same as Material
14577
+ host = inject(ElementRef);
14578
+ injector = inject(Injector);
14579
+ appLocale = inject(LOCALE_ID);
14580
+ intl = injectTnCalendarIntl();
14581
+ /** Years read out in the locale's own numerals, without a thousands separator. */
14582
+ yearFormat = computed(() => {
14583
+ return new Intl.NumberFormat(this.locale() ?? this.appLocale, { useGrouping: false });
14584
+ }, ...(ngDevMode ? [{ debugName: "yearFormat" }] : []));
14068
14585
  yearsPerRow = 4;
14069
- yearRowCount = 6; // Shows 24 years total (6 rows x 4 columns)
14070
- // Calculate the year range to display
14586
+ yearRowCount = YEARS_PER_PAGE / this.yearsPerRow;
14587
+ /**
14588
+ * The page of years on screen. Pages are fixed blocks, so the active year sits
14589
+ * wherever it falls within one rather than in the middle — see `yearPageStart`.
14590
+ */
14071
14591
  yearRange = computed(() => {
14072
- const activeDate = this.activeDate();
14073
- const currentYear = activeDate.getFullYear();
14074
- // Calculate the starting year for a 24-year range
14075
- // We want the active year to be roughly in the middle
14076
- const startYear = Math.floor(currentYear / 24) * 24;
14077
- return { start: startYear, end: startYear + 23 };
14592
+ const start = yearPageStart(this.activeDate().getFullYear());
14593
+ return { start, end: start + YEARS_PER_PAGE - 1 };
14078
14594
  }, ...(ngDevMode ? [{ debugName: "yearRange" }] : []));
14595
+ /**
14596
+ * Names the grid after the span of years it shows, so a screen-reader user arrowing
14597
+ * around knows which page they are on rather than just hearing bare years.
14598
+ */
14599
+ gridLabel = computed(() => {
14600
+ const range = this.yearRange();
14601
+ return this.intl().yearGridLabel(range.start, range.end);
14602
+ }, ...(ngDevMode ? [{ debugName: "gridLabel" }] : []));
14079
14603
  yearRows = computed(() => {
14080
14604
  const range = this.yearRange();
14605
+ const format = this.yearFormat();
14606
+ const formatYear = (year) => format.format(year);
14607
+ // Read once for the whole grid rather than per cell, matching the month view: every
14608
+ // year is then measured against the same instant, and 24 allocations become one.
14609
+ const thisYear = new Date().getFullYear();
14081
14610
  const rows = [];
14082
14611
  for (let row = 0; row < this.yearRowCount; row++) {
14083
14612
  const yearRow = [];
14084
14613
  for (let col = 0; col < this.yearsPerRow; col++) {
14085
14614
  const year = range.start + (row * this.yearsPerRow) + col;
14086
- yearRow.push(this.createYearCell(year));
14615
+ yearRow.push(this.createYearCell(year, formatYear, thisYear));
14087
14616
  }
14088
14617
  rows.push(yearRow);
14089
14618
  }
14090
14619
  return rows;
14091
14620
  }, ...(ngDevMode ? [{ debugName: "yearRows" }] : []));
14092
- createYearCell(year) {
14093
- const today = new Date();
14094
- const currentYear = today.getFullYear();
14621
+ /**
14622
+ * The year that carries `tabindex="0"` — the grid's single keyboard entry point, per
14623
+ * the roving tabindex pattern. Follows `activeDate`, falling back to the nearest
14624
+ * enabled year in the page so a range whose active year is disabled still has a
14625
+ * tabbable cell. `null` only when every year on the page is disabled.
14626
+ */
14627
+ activeYear = computed(() => {
14628
+ const range = this.yearRange();
14629
+ // Always inside the page: the range is derived from this same year.
14630
+ const wanted = this.activeDate().getFullYear();
14631
+ if (this.isYearEnabled(wanted)) {
14632
+ return wanted;
14633
+ }
14634
+ // Spiral outwards from the year we wanted, forward first.
14635
+ const span = range.end - range.start;
14636
+ for (let offset = 1; offset <= span; offset++) {
14637
+ if (wanted + offset <= range.end && this.isYearEnabled(wanted + offset)) {
14638
+ return wanted + offset;
14639
+ }
14640
+ if (wanted - offset >= range.start && this.isYearEnabled(wanted - offset)) {
14641
+ return wanted - offset;
14642
+ }
14643
+ }
14644
+ return null;
14645
+ }, ...(ngDevMode ? [{ debugName: "activeYear" }] : []));
14646
+ createYearCell(year, formatYear, thisYear) {
14095
14647
  const selectedYear = this.selected()?.getFullYear();
14096
- const isToday = year === currentYear;
14648
+ const isToday = year === thisYear;
14097
14649
  const isSelected = year === selectedYear;
14098
14650
  const enabled = this.isYearEnabled(year);
14099
14651
  return {
14100
- value: year,
14101
- year: year,
14102
- label: year.toString(),
14103
- ariaLabel: this.formatYearAriaLabel(year, isSelected, isToday),
14652
+ year,
14653
+ label: formatYear(year),
14654
+ ariaLabel: this.formatYearAriaLabel(formatYear(year), isToday),
14104
14655
  enabled,
14105
14656
  selected: isSelected,
14106
14657
  today: isToday,
@@ -14116,7 +14667,11 @@ class TnMultiYearViewComponent {
14116
14667
  if (maxDate && year > maxDate.getFullYear()) {
14117
14668
  return false;
14118
14669
  }
14119
- // If we have a date filter, test January 1st of that year
14670
+ // Deliberately coarse: a year is a single cell, so it gets a single verdict, and
14671
+ // January 1st stands in for the whole year. A filter that rules out January 1st
14672
+ // while allowing the rest of the year therefore disables the year outright. Material
14673
+ // approximates the same way; testing all 365 days per cell is not worth it, and the
14674
+ // month view applies the filter exactly once the user drills in.
14120
14675
  if (dateFilter) {
14121
14676
  const testDate = new Date(year, 0, 1);
14122
14677
  if (!dateFilter(testDate)) {
@@ -14125,13 +14680,15 @@ class TnMultiYearViewComponent {
14125
14680
  }
14126
14681
  return true;
14127
14682
  }
14128
- formatYearAriaLabel(year, isSelected, isToday) {
14129
- let label = year.toString();
14130
- if (isSelected) {
14131
- label += ' (selected)';
14132
- }
14683
+ /**
14684
+ * Selection is carried by the button's `aria-pressed`, so it is deliberately not
14685
+ * repeated here. "Current year" stays: `aria-current="date"` on a year cell is vague
14686
+ * enough that spelling it out earns its place.
14687
+ */
14688
+ formatYearAriaLabel(year, isToday) {
14689
+ let label = year;
14133
14690
  if (isToday) {
14134
- label += ' (current year)';
14691
+ label += ` ${this.intl().currentYear}`;
14135
14692
  }
14136
14693
  return label;
14137
14694
  }
@@ -14143,19 +14700,92 @@ class TnMultiYearViewComponent {
14143
14700
  }
14144
14701
  onYearClicked(cell) {
14145
14702
  if (cell.enabled) {
14146
- // Create a new date with the selected year, keeping current month and day
14147
- const currentDate = this.activeDate();
14148
- const newDate = new Date(cell.year, currentDate.getMonth(), currentDate.getDate());
14149
- this.selectedChange.emit(newDate);
14703
+ // Keeps the month and day, clamped: moving off 29 February into a non-leap year
14704
+ // would otherwise roll over into March and change the month too.
14705
+ this.selectedChange.emit(withYear(this.activeDate(), cell.year));
14150
14706
  }
14151
14707
  }
14708
+ /**
14709
+ * Moves the roving tabindex across the year grid. Picking a year stays on
14710
+ * click/Enter/Space, which the cells are already buttons for.
14711
+ *
14712
+ * Moves past either end of the 24-year page emit an `activeDateChange` like the
14713
+ * header's paging buttons do, and the grid re-renders on the neighbouring page.
14714
+ */
14715
+ onKeydown(event) {
14716
+ const from = this.activeYear();
14717
+ if (from === null) {
14718
+ return;
14719
+ }
14720
+ const move = this.targetForKey(event, from);
14721
+ if (move === null) {
14722
+ return;
14723
+ }
14724
+ // Claim the key before the browser scrolls the page with it.
14725
+ event.preventDefault();
14726
+ const landing = this.nearestEnabledYear(move.year, move.search);
14727
+ if (landing === null || landing === from) {
14728
+ return;
14729
+ }
14730
+ this.activeDateChange.emit(withYear(this.activeDate(), landing));
14731
+ focusActiveCellAfterRender(this.host, this.injector);
14732
+ }
14733
+ /**
14734
+ * The year a key aims at, and which way to look from there when that year is disabled.
14735
+ *
14736
+ * The search direction belongs to the key, not to where the target happens to fall
14737
+ * relative to the current year. Home and End mean "the ends of *this* page", so they
14738
+ * search inward: looking outward would step onto the neighbouring 24-year page over a
14739
+ * single ruled-out boundary year and turn the page, which is not what either key
14740
+ * promises. The arrows and the paging keys keep travelling the way they were already
14741
+ * headed, and moving onto the next page is the point of those.
14742
+ */
14743
+ targetForKey(event, from) {
14744
+ const range = this.yearRange();
14745
+ const yearsPerPage = range.end - range.start + 1;
14746
+ switch (event.key) {
14747
+ case 'ArrowLeft': return { year: from - 1, search: -1 };
14748
+ case 'ArrowRight': return { year: from + 1, search: 1 };
14749
+ case 'ArrowUp': return { year: from - this.yearsPerRow, search: -1 };
14750
+ case 'ArrowDown': return { year: from + this.yearsPerRow, search: 1 };
14751
+ case 'Home': return { year: range.start, search: 1 };
14752
+ case 'End': return { year: range.end, search: -1 };
14753
+ case 'PageUp': return { year: from - yearsPerPage, search: -1 };
14754
+ case 'PageDown': return { year: from + yearsPerPage, search: 1 };
14755
+ default: return null;
14756
+ }
14757
+ }
14758
+ /**
14759
+ * Steps past years the caller has disabled, so `minDate`/`maxDate` don't park focus
14760
+ * somewhere unusable.
14761
+ *
14762
+ * Searches the way the key asked for (see `targetForKey`), then doubles back if that
14763
+ * finds nothing. The fallback is for the ends of the allowed range: arrowing right at
14764
+ * `maxDate` finds nothing ahead, doubles back onto the year we started from, and the
14765
+ * caller reads that as "don't move".
14766
+ */
14767
+ nearestEnabledYear(target, preferred) {
14768
+ return this.scanForEnabledYear(target, preferred)
14769
+ ?? this.scanForEnabledYear(target, preferred === 1 ? -1 : 1);
14770
+ }
14771
+ /** Bounded, so an everything-disabled range stops the move instead of spinning. */
14772
+ scanForEnabledYear(from, direction) {
14773
+ const maxSteps = 48;
14774
+ for (let step = 0; step <= maxSteps; step++) {
14775
+ const candidate = from + (step * direction);
14776
+ if (this.isYearEnabled(candidate)) {
14777
+ return candidate;
14778
+ }
14779
+ }
14780
+ return null;
14781
+ }
14152
14782
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnMultiYearViewComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
14153
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.0", type: TnMultiYearViewComponent, isStandalone: true, selector: "tn-multi-year-view", inputs: { activeDate: { classPropertyName: "activeDate", publicName: "activeDate", isSignal: true, isRequired: false, transformFunction: null }, selected: { classPropertyName: "selected", publicName: "selected", isSignal: true, isRequired: false, transformFunction: null }, minDate: { classPropertyName: "minDate", publicName: "minDate", isSignal: true, isRequired: false, transformFunction: null }, maxDate: { classPropertyName: "maxDate", publicName: "maxDate", isSignal: true, isRequired: false, transformFunction: null }, dateFilter: { classPropertyName: "dateFilter", publicName: "dateFilter", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { selectedChange: "selectedChange", activeDateChange: "activeDateChange" }, ngImport: i0, template: "<table role=\"grid\" class=\"tn-calendar-table\">\n <!-- Table body with year cells -->\n <tbody class=\"tn-calendar-body\">\n @for (row of yearRows(); track trackByRow($index, row); let rowIndex = $index) {\n <tr role=\"row\">\n @for (cell of row; track trackByYear($index, cell); let colIndex = $index) {\n <td\n role=\"gridcell\"\n class=\"tn-calendar-body-cell-container\"\n [attr.data-tn-row]=\"rowIndex\"\n [attr.data-tn-col]=\"colIndex\"\n [style.width.%]=\"cellWidth\"\n [style.padding-top.%]=\"cellAspectRatio\"\n [style.padding-bottom.%]=\"cellAspectRatio\">\n <button\n type=\"button\"\n class=\"tn-calendar-body-cell\"\n [class.tn-calendar-body-selected]=\"cell.selected\"\n [class.tn-calendar-body-today]=\"cell.today\"\n [class.tn-calendar-body-active]=\"cell.selected\"\n [disabled]=\"!cell.enabled\"\n [attr.tabindex]=\"cell.selected ? 0 : -1\"\n [attr.aria-label]=\"cell.ariaLabel\"\n [attr.aria-pressed]=\"cell.selected\"\n [attr.aria-current]=\"cell.today ? 'date' : null\"\n (click)=\"onYearClicked(cell)\">\n <span class=\"tn-calendar-body-cell-content tn-focus-indicator\"\n [class.tn-calendar-body-selected]=\"cell.selected\"\n [class.tn-calendar-body-today]=\"cell.today\">\n {{ cell.value }}\n </span>\n <span aria-hidden=\"true\" class=\"tn-calendar-body-cell-preview\"></span>\n </button>\n </td>\n }\n </tr>\n }\n </tbody>\n</table>\n", styles: [".tn-calendar-table{width:100%;border-spacing:0;border-collapse:separate}.tn-calendar-table-header-divider{height:16px;border:none}.tn-calendar-body{min-width:224px}.tn-calendar-body-cell-container{position:relative;border:none}.tn-calendar-body-cell{position:absolute;top:5%;left:5%;width:90%;height:90%;border:none;background:transparent;color:var(--tn-fg1, #333);cursor:pointer;font-size:14px;font-weight:500;transition:background-color .2s ease}.tn-calendar-body-cell:hover:not(:disabled){background:var(--tn-alt-bg2, #e8f4fd)}.tn-calendar-body-cell:focus{outline:2px solid var(--tn-primary, #007bff);outline-offset:2px;background:var(--tn-alt-bg2, #e8f4fd)}.tn-calendar-body-cell:disabled{color:var(--tn-fg2, #666);opacity:.5;cursor:default}.tn-calendar-body-cell.tn-calendar-body-today:not(.tn-calendar-body-selected){border:1px solid var(--tn-primary, #007bff);color:var(--tn-primary, #007bff)}.tn-calendar-body-cell.tn-calendar-body-selected{background:var(--tn-primary, #007bff);color:#fff}.tn-calendar-body-cell-content{position:relative;display:flex;align-items:center;justify-content:center;width:100%;height:100%;transition:background-color .2s cubic-bezier(.25,.8,.25,1)}.tn-focus-indicator{position:relative}.tn-focus-indicator:before{content:\"\";position:absolute;inset:0;opacity:0;background:currentColor;transition:opacity .2s cubic-bezier(.25,.8,.25,1)}.tn-calendar-body-cell-preview{position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none}\n"] });
14783
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.0", type: TnMultiYearViewComponent, isStandalone: true, selector: "tn-multi-year-view", inputs: { activeDate: { classPropertyName: "activeDate", publicName: "activeDate", isSignal: true, isRequired: false, transformFunction: null }, selected: { classPropertyName: "selected", publicName: "selected", isSignal: true, isRequired: false, transformFunction: null }, minDate: { classPropertyName: "minDate", publicName: "minDate", isSignal: true, isRequired: false, transformFunction: null }, maxDate: { classPropertyName: "maxDate", publicName: "maxDate", isSignal: true, isRequired: false, transformFunction: null }, dateFilter: { classPropertyName: "dateFilter", publicName: "dateFilter", isSignal: true, isRequired: false, transformFunction: null }, locale: { classPropertyName: "locale", publicName: "locale", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { selectedChange: "selectedChange", activeDateChange: "activeDateChange" }, ngImport: i0, template: "<table role=\"grid\" class=\"tn-calendar-table\" [attr.aria-label]=\"gridLabel()\">\n <!-- Table body with year cells -->\n <tbody class=\"tn-calendar-body\">\n @for (row of yearRows(); track trackByRow($index, row); let rowIndex = $index) {\n <tr role=\"row\">\n @for (cell of row; track trackByYear($index, cell); let colIndex = $index) {\n <td\n role=\"gridcell\"\n class=\"tn-calendar-body-cell-container\"\n [attr.data-tn-row]=\"rowIndex\"\n [attr.data-tn-col]=\"colIndex\">\n <button\n type=\"button\"\n class=\"tn-calendar-body-cell\"\n [attr.data-tn-year]=\"cell.year\"\n [class.tn-calendar-body-selected]=\"cell.selected\"\n [class.tn-calendar-body-today]=\"cell.today\"\n [class.tn-calendar-body-active]=\"cell.year === activeYear()\"\n [disabled]=\"!cell.enabled\"\n [attr.tabindex]=\"cell.year === activeYear() ? 0 : -1\"\n [attr.aria-label]=\"cell.ariaLabel\"\n [attr.aria-pressed]=\"cell.selected\"\n [attr.aria-current]=\"cell.today ? 'date' : null\"\n (click)=\"onYearClicked(cell)\"\n (keydown)=\"onKeydown($event)\">\n <span class=\"tn-calendar-body-cell-content\">\n {{ cell.label }}\n </span>\n </button>\n </td>\n }\n </tr>\n }\n </tbody>\n</table>\n", styles: [":host{--calendar-radius: 4px;--calendar-year-row-height: 48px;--calendar-year-indicator-width: 72px;--calendar-year-indicator-height: 36px}.tn-calendar-table{width:100%;min-width:min(100%,224px);table-layout:fixed;border-spacing:0;border-collapse:separate}.tn-calendar-table-header-divider{height:16px;border:none}.tn-calendar-body-cell-container{position:relative;border:none;height:var(--calendar-year-row-height);width:25%}.tn-calendar-body-cell{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;width:100%;height:100%;padding:0;border:none;background:transparent;color:var(--tn-fg1, #333);cursor:pointer;font-size:14px;font-weight:500}.tn-calendar-body-cell:focus-visible{outline:none}.tn-calendar-body-cell:focus-visible .tn-calendar-body-cell-content{outline:2px solid var(--tn-primary, #007bff);outline-offset:-2px}.tn-calendar-body-cell:disabled{color:var(--tn-fg2, #666);opacity:.5;cursor:default}.tn-calendar-body-cell-content{position:relative;display:flex;align-items:center;justify-content:center;box-sizing:border-box;width:var(--calendar-year-indicator-width);height:var(--calendar-year-indicator-height);max-width:100%;border-radius:var(--calendar-radius);transition:background-color .2s cubic-bezier(.25,.8,.25,1)}.tn-calendar-body-cell:not(:disabled):not(.tn-calendar-body-selected):hover .tn-calendar-body-cell-content{background:var(--tn-alt-bg2, #e8f4fd)}.tn-calendar-body-cell.tn-calendar-body-today:not(.tn-calendar-body-selected) .tn-calendar-body-cell-content{border:1px solid var(--tn-primary, #007bff);color:var(--tn-primary, #007bff)}.tn-calendar-body-cell.tn-calendar-body-selected .tn-calendar-body-cell-content{background:var(--tn-primary, #007bff);color:var(--tn-primary-txt, #fff)}\n"] });
14154
14784
  }
14155
14785
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnMultiYearViewComponent, decorators: [{
14156
14786
  type: Component,
14157
- args: [{ selector: 'tn-multi-year-view', standalone: true, imports: [], template: "<table role=\"grid\" class=\"tn-calendar-table\">\n <!-- Table body with year cells -->\n <tbody class=\"tn-calendar-body\">\n @for (row of yearRows(); track trackByRow($index, row); let rowIndex = $index) {\n <tr role=\"row\">\n @for (cell of row; track trackByYear($index, cell); let colIndex = $index) {\n <td\n role=\"gridcell\"\n class=\"tn-calendar-body-cell-container\"\n [attr.data-tn-row]=\"rowIndex\"\n [attr.data-tn-col]=\"colIndex\"\n [style.width.%]=\"cellWidth\"\n [style.padding-top.%]=\"cellAspectRatio\"\n [style.padding-bottom.%]=\"cellAspectRatio\">\n <button\n type=\"button\"\n class=\"tn-calendar-body-cell\"\n [class.tn-calendar-body-selected]=\"cell.selected\"\n [class.tn-calendar-body-today]=\"cell.today\"\n [class.tn-calendar-body-active]=\"cell.selected\"\n [disabled]=\"!cell.enabled\"\n [attr.tabindex]=\"cell.selected ? 0 : -1\"\n [attr.aria-label]=\"cell.ariaLabel\"\n [attr.aria-pressed]=\"cell.selected\"\n [attr.aria-current]=\"cell.today ? 'date' : null\"\n (click)=\"onYearClicked(cell)\">\n <span class=\"tn-calendar-body-cell-content tn-focus-indicator\"\n [class.tn-calendar-body-selected]=\"cell.selected\"\n [class.tn-calendar-body-today]=\"cell.today\">\n {{ cell.value }}\n </span>\n <span aria-hidden=\"true\" class=\"tn-calendar-body-cell-preview\"></span>\n </button>\n </td>\n }\n </tr>\n }\n </tbody>\n</table>\n", styles: [".tn-calendar-table{width:100%;border-spacing:0;border-collapse:separate}.tn-calendar-table-header-divider{height:16px;border:none}.tn-calendar-body{min-width:224px}.tn-calendar-body-cell-container{position:relative;border:none}.tn-calendar-body-cell{position:absolute;top:5%;left:5%;width:90%;height:90%;border:none;background:transparent;color:var(--tn-fg1, #333);cursor:pointer;font-size:14px;font-weight:500;transition:background-color .2s ease}.tn-calendar-body-cell:hover:not(:disabled){background:var(--tn-alt-bg2, #e8f4fd)}.tn-calendar-body-cell:focus{outline:2px solid var(--tn-primary, #007bff);outline-offset:2px;background:var(--tn-alt-bg2, #e8f4fd)}.tn-calendar-body-cell:disabled{color:var(--tn-fg2, #666);opacity:.5;cursor:default}.tn-calendar-body-cell.tn-calendar-body-today:not(.tn-calendar-body-selected){border:1px solid var(--tn-primary, #007bff);color:var(--tn-primary, #007bff)}.tn-calendar-body-cell.tn-calendar-body-selected{background:var(--tn-primary, #007bff);color:#fff}.tn-calendar-body-cell-content{position:relative;display:flex;align-items:center;justify-content:center;width:100%;height:100%;transition:background-color .2s cubic-bezier(.25,.8,.25,1)}.tn-focus-indicator{position:relative}.tn-focus-indicator:before{content:\"\";position:absolute;inset:0;opacity:0;background:currentColor;transition:opacity .2s cubic-bezier(.25,.8,.25,1)}.tn-calendar-body-cell-preview{position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none}\n"] }]
14158
- }], propDecorators: { activeDate: [{ type: i0.Input, args: [{ isSignal: true, alias: "activeDate", required: false }] }], selected: [{ type: i0.Input, args: [{ isSignal: true, alias: "selected", required: false }] }], minDate: [{ type: i0.Input, args: [{ isSignal: true, alias: "minDate", required: false }] }], maxDate: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxDate", required: false }] }], dateFilter: [{ type: i0.Input, args: [{ isSignal: true, alias: "dateFilter", required: false }] }], selectedChange: [{ type: i0.Output, args: ["selectedChange"] }], activeDateChange: [{ type: i0.Output, args: ["activeDateChange"] }] } });
14787
+ args: [{ selector: 'tn-multi-year-view', standalone: true, imports: [], template: "<table role=\"grid\" class=\"tn-calendar-table\" [attr.aria-label]=\"gridLabel()\">\n <!-- Table body with year cells -->\n <tbody class=\"tn-calendar-body\">\n @for (row of yearRows(); track trackByRow($index, row); let rowIndex = $index) {\n <tr role=\"row\">\n @for (cell of row; track trackByYear($index, cell); let colIndex = $index) {\n <td\n role=\"gridcell\"\n class=\"tn-calendar-body-cell-container\"\n [attr.data-tn-row]=\"rowIndex\"\n [attr.data-tn-col]=\"colIndex\">\n <button\n type=\"button\"\n class=\"tn-calendar-body-cell\"\n [attr.data-tn-year]=\"cell.year\"\n [class.tn-calendar-body-selected]=\"cell.selected\"\n [class.tn-calendar-body-today]=\"cell.today\"\n [class.tn-calendar-body-active]=\"cell.year === activeYear()\"\n [disabled]=\"!cell.enabled\"\n [attr.tabindex]=\"cell.year === activeYear() ? 0 : -1\"\n [attr.aria-label]=\"cell.ariaLabel\"\n [attr.aria-pressed]=\"cell.selected\"\n [attr.aria-current]=\"cell.today ? 'date' : null\"\n (click)=\"onYearClicked(cell)\"\n (keydown)=\"onKeydown($event)\">\n <span class=\"tn-calendar-body-cell-content\">\n {{ cell.label }}\n </span>\n </button>\n </td>\n }\n </tr>\n }\n </tbody>\n</table>\n", styles: [":host{--calendar-radius: 4px;--calendar-year-row-height: 48px;--calendar-year-indicator-width: 72px;--calendar-year-indicator-height: 36px}.tn-calendar-table{width:100%;min-width:min(100%,224px);table-layout:fixed;border-spacing:0;border-collapse:separate}.tn-calendar-table-header-divider{height:16px;border:none}.tn-calendar-body-cell-container{position:relative;border:none;height:var(--calendar-year-row-height);width:25%}.tn-calendar-body-cell{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;width:100%;height:100%;padding:0;border:none;background:transparent;color:var(--tn-fg1, #333);cursor:pointer;font-size:14px;font-weight:500}.tn-calendar-body-cell:focus-visible{outline:none}.tn-calendar-body-cell:focus-visible .tn-calendar-body-cell-content{outline:2px solid var(--tn-primary, #007bff);outline-offset:-2px}.tn-calendar-body-cell:disabled{color:var(--tn-fg2, #666);opacity:.5;cursor:default}.tn-calendar-body-cell-content{position:relative;display:flex;align-items:center;justify-content:center;box-sizing:border-box;width:var(--calendar-year-indicator-width);height:var(--calendar-year-indicator-height);max-width:100%;border-radius:var(--calendar-radius);transition:background-color .2s cubic-bezier(.25,.8,.25,1)}.tn-calendar-body-cell:not(:disabled):not(.tn-calendar-body-selected):hover .tn-calendar-body-cell-content{background:var(--tn-alt-bg2, #e8f4fd)}.tn-calendar-body-cell.tn-calendar-body-today:not(.tn-calendar-body-selected) .tn-calendar-body-cell-content{border:1px solid var(--tn-primary, #007bff);color:var(--tn-primary, #007bff)}.tn-calendar-body-cell.tn-calendar-body-selected .tn-calendar-body-cell-content{background:var(--tn-primary, #007bff);color:var(--tn-primary-txt, #fff)}\n"] }]
14788
+ }], propDecorators: { activeDate: [{ type: i0.Input, args: [{ isSignal: true, alias: "activeDate", required: false }] }], selected: [{ type: i0.Input, args: [{ isSignal: true, alias: "selected", required: false }] }], minDate: [{ type: i0.Input, args: [{ isSignal: true, alias: "minDate", required: false }] }], maxDate: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxDate", required: false }] }], dateFilter: [{ type: i0.Input, args: [{ isSignal: true, alias: "dateFilter", required: false }] }], locale: [{ type: i0.Input, args: [{ isSignal: true, alias: "locale", required: false }] }], selectedChange: [{ type: i0.Output, args: ["selectedChange"] }], activeDateChange: [{ type: i0.Output, args: ["activeDateChange"] }] } });
14159
14789
 
14160
14790
  class TnCalendarComponent {
14161
14791
  startView = input('month', ...(ngDevMode ? [{ debugName: "startView" }] : []));
@@ -14163,116 +14793,112 @@ class TnCalendarComponent {
14163
14793
  minDate = input(undefined, ...(ngDevMode ? [{ debugName: "minDate" }] : []));
14164
14794
  maxDate = input(undefined, ...(ngDevMode ? [{ debugName: "maxDate" }] : []));
14165
14795
  dateFilter = input(undefined, ...(ngDevMode ? [{ debugName: "dateFilter" }] : []));
14796
+ /**
14797
+ * Dates to flag as noteworthy — days a task runs, days with events, and the like.
14798
+ * Order and time-of-day are ignored; only the calendar day is compared. The
14799
+ * calendar owns how a marked day looks, so callers pass dates rather than styles.
14800
+ * Marking is independent of `selected`/`selectedRange`, which stay authoritative
14801
+ * where they overlap.
14802
+ */
14803
+ markedDates = input(undefined, ...(ngDevMode ? [{ debugName: "markedDates" }] : []));
14804
+ /**
14805
+ * Which month — or which page of years, in the year view — is on screen.
14806
+ *
14807
+ * Optional. Left unbound, the calendar opens on the month the bound value lives in and
14808
+ * drives navigation itself. Bind it, alongside `activeDateChange`, to drive the view
14809
+ * from outside: useful for a calendar that stays mounted while its value jumps to
14810
+ * another month, which the open-on-init behaviour alone won't follow.
14811
+ *
14812
+ * Unlike `selected`, this isn't strictly controlled — the calendar still navigates on
14813
+ * its own when the user pages or arrows around, so binding a constant here won't
14814
+ * freeze the view. Changing what you bind always wins.
14815
+ */
14816
+ activeDate = input(undefined, ...(ngDevMode ? [{ debugName: "activeDate" }] : []));
14817
+ /**
14818
+ * Locale for dates, month and weekday names, and which day the week starts on.
14819
+ *
14820
+ * Defaults to the app's `LOCALE_ID`, which is where an Angular app already declares
14821
+ * its locale — so a `ng build --localize` app is right without touching this. Bind it
14822
+ * only when one calendar needs a different locale from the rest of the app.
14823
+ *
14824
+ * Nothing here reads the browser's language on its own: doing so would quietly
14825
+ * disagree with `DatePipe` and every other locale-aware part of the app. An app that
14826
+ * wants that behaviour opts into it once, at bootstrap:
14827
+ * `{ provide: LOCALE_ID, useValue: navigator.language }`.
14828
+ *
14829
+ * Wording that isn't a date — "(marked)", the header button labels — comes from
14830
+ * `TN_CALENDAR_INTL` instead.
14831
+ */
14832
+ locale = input(undefined, ...(ngDevMode ? [{ debugName: "locale" }] : []));
14833
+ appLocale = inject(LOCALE_ID);
14834
+ host = inject(ElementRef);
14835
+ injector = inject(Injector);
14836
+ /** The locale actually in force: the input when bound, the app's otherwise. */
14837
+ resolvedLocale = computed(() => this.locale() ?? this.appLocale, ...(ngDevMode ? [{ debugName: "resolvedLocale" }] : []));
14166
14838
  // Range mode inputs
14167
14839
  rangeMode = input(false, ...(ngDevMode ? [{ debugName: "rangeMode" }] : []));
14168
14840
  selectedRange = input(undefined, ...(ngDevMode ? [{ debugName: "selectedRange" }] : []));
14169
14841
  selectedChange = output();
14842
+ /**
14843
+ * The date the calendar is now sitting on — the month it shows, and the day holding
14844
+ * the grid's roving tabindex.
14845
+ *
14846
+ * This is navigation, not selection: it fires on paging, on arrow keys, and on
14847
+ * clicking a day (a click moves the active cell as well as choosing the date, so the
14848
+ * two never drift apart). Treat it as "the view moved", not "the user picked a date" —
14849
+ * `selectedChange` is the one that means a choice was made.
14850
+ */
14170
14851
  activeDateChange = output();
14171
14852
  viewChanged = output();
14172
14853
  // Range mode outputs
14173
14854
  selectedRangeChange = output();
14174
- currentDate = signal(new Date(), ...(ngDevMode ? [{ debugName: "currentDate" }] : []));
14855
+ /**
14856
+ * Which month/year grid is on screen. Navigation state, not selection state.
14857
+ *
14858
+ * Follows the `activeDate` input whenever the caller changes it, but stays writable so
14859
+ * paging and arrow keys keep working with no echo back from the caller.
14860
+ */
14861
+ currentDate = linkedSignal({ ...(ngDevMode ? { debugName: "currentDate" } : {}), source: () => this.activeDate(),
14862
+ computation: (activeDate, previous) => activeDate ?? previous?.value ?? new Date() });
14175
14863
  currentView = signal('month', ...(ngDevMode ? [{ debugName: "currentView" }] : []));
14176
- // Range selection state - this is the authoritative source for calendar display
14177
- rangeState = signal({
14178
- start: null,
14179
- end: null,
14180
- selecting: 'start'
14181
- }, ...(ngDevMode ? [{ debugName: "rangeState" }] : []));
14182
- // Track if user has interacted with calendar - once true, ignore external selectedRange
14183
- userHasInteracted = false;
14184
- constructor() {
14185
- // Watch for changes to selectedRange input
14186
- effect(() => {
14187
- // Track signals to re-run effect when they change
14188
- this.selectedRange();
14189
- const rangeMode = this.rangeMode();
14190
- // Only update range state from external selectedRange if user hasn't interacted yet
14191
- if (!this.userHasInteracted && rangeMode) {
14192
- this.initializeRangeState();
14193
- }
14194
- });
14195
- }
14196
14864
  ngOnInit() {
14197
14865
  this.currentView.set(this.startView());
14198
- // Initialize range state if in range mode (this also handles currentDate)
14199
- if (this.rangeMode()) {
14200
- this.initializeRangeState();
14201
- }
14202
- else {
14203
- const selected = this.selected();
14204
- if (selected) {
14205
- // For single date mode, navigate to the selected date's month
14206
- this.currentDate.set(new Date(selected));
14207
- }
14866
+ // A bound activeDate already says what should be on screen.
14867
+ if (this.activeDate()) {
14868
+ return;
14208
14869
  }
14209
- }
14210
- initializeRangeState() {
14211
- if (this.rangeMode()) {
14212
- const selectedRange = this.selectedRange();
14213
- if (selectedRange) {
14214
- this.rangeState.set({
14215
- start: selectedRange.start,
14216
- end: selectedRange.end,
14217
- selecting: selectedRange.start && selectedRange.end ? 'start' :
14218
- selectedRange.start ? 'end' : 'start'
14219
- });
14220
- // Navigate to the month of the selected start date, or end date if no start date
14221
- const dateToShow = selectedRange.start || selectedRange.end;
14222
- if (dateToShow) {
14223
- this.currentDate.set(new Date(dateToShow));
14224
- }
14225
- }
14226
- else {
14227
- // No selected range - initialize empty range state
14228
- this.rangeState.set({
14229
- start: null,
14230
- end: null,
14231
- selecting: 'start'
14232
- });
14233
- }
14870
+ // Otherwise open on the month the caller's value lives in.
14871
+ const initialDate = this.rangeMode()
14872
+ ? (this.selectedRange()?.start ?? this.selectedRange()?.end ?? null)
14873
+ : (this.selected() ?? null);
14874
+ if (initialDate) {
14875
+ this.currentDate.set(new Date(initialDate));
14234
14876
  }
14235
14877
  }
14236
- onMonthSelected(month) {
14237
- const newDate = new Date(this.currentDate());
14238
- newDate.setMonth(month);
14239
- this.currentDate.set(newDate);
14240
- this.currentView.set('month');
14241
- this.viewChanged.emit('month');
14242
- }
14243
- onYearSelected(year) {
14244
- const newDate = new Date(this.currentDate());
14245
- newDate.setFullYear(year);
14246
- this.currentDate.set(newDate);
14247
- this.activeDateChange.emit(newDate);
14248
- }
14249
14878
  onViewChanged(view) {
14250
14879
  this.currentView.set(view);
14251
14880
  this.viewChanged.emit(view);
14252
14881
  }
14253
14882
  onPreviousClicked() {
14254
- const current = this.currentDate();
14255
- let newDate;
14256
- if (this.currentView() === 'month') {
14257
- newDate = new Date(current.getFullYear(), current.getMonth() - 1, 1);
14258
- }
14259
- else {
14260
- // For year view, navigate by 24-year ranges (like Material)
14261
- newDate = new Date(current.getFullYear() - 24, current.getMonth(), 1);
14262
- }
14263
- this.currentDate.set(newDate);
14264
- this.activeDateChange.emit(newDate);
14883
+ this.page(-1);
14265
14884
  }
14266
14885
  onNextClicked() {
14886
+ this.page(1);
14887
+ }
14888
+ /**
14889
+ * Steps the view one page in `direction`: a month in the day view, a page of years in
14890
+ * the year view — the same spans `PageUp`/`PageDown` move, and the same the header
14891
+ * labels.
14892
+ *
14893
+ * Carries the day across, clamped, rather than dropping to the 1st. The keyboard has
14894
+ * always done this, so resetting here left the roving tabindex somewhere different
14895
+ * depending on whether the user clicked the arrow or pressed the key.
14896
+ */
14897
+ page(direction) {
14267
14898
  const current = this.currentDate();
14268
- let newDate;
14269
- if (this.currentView() === 'month') {
14270
- newDate = new Date(current.getFullYear(), current.getMonth() + 1, 1);
14271
- }
14272
- else {
14273
- // For year view, navigate by 24-year ranges (like Material)
14274
- newDate = new Date(current.getFullYear() + 24, current.getMonth(), 1);
14275
- }
14899
+ const newDate = this.currentView() === 'month'
14900
+ ? addMonths(current, direction)
14901
+ : withYear(current, current.getFullYear() + (direction * YEARS_PER_PAGE));
14276
14902
  this.currentDate.set(newDate);
14277
14903
  this.activeDateChange.emit(newDate);
14278
14904
  }
@@ -14284,83 +14910,346 @@ class TnCalendarComponent {
14284
14910
  this.selectedChange.emit(date);
14285
14911
  }
14286
14912
  }
14913
+ /**
14914
+ * Works out the next range from the one the caller currently owns and emits it. The
14915
+ * calendar keeps no copy — what it renders next is whatever the caller binds back.
14916
+ */
14287
14917
  handleRangeSelection(date) {
14288
- // Mark that user has interacted - calendar is now authoritative
14289
- this.userHasInteracted = true;
14290
- const currentRange = this.rangeState();
14291
- // If we already have a complete range (both start and end), clear and start fresh
14292
- if (currentRange.start && currentRange.end && currentRange.selecting === 'start') {
14293
- const newRangeState = {
14294
- start: date,
14295
- end: null,
14296
- selecting: 'end'
14297
- };
14298
- this.rangeState.set(newRangeState);
14918
+ const current = this.selectedRange();
14919
+ const start = current?.start ?? null;
14920
+ const end = current?.end ?? null;
14921
+ // Nothing pending, or the range is already complete: begin a new one.
14922
+ if (!start || end) {
14299
14923
  this.selectedRangeChange.emit({ start: date, end: null });
14300
14924
  return;
14301
14925
  }
14302
- if (currentRange.selecting === 'start' || !currentRange.start) {
14303
- // First click or selecting start date - clear any previous range immediately
14304
- const newRangeState = {
14305
- start: date,
14306
- end: null,
14307
- selecting: 'end'
14308
- };
14309
- this.rangeState.set(newRangeState);
14926
+ // A second pick before the first restarts the range rather than inverting it.
14927
+ // Compared by day: the clicked date is midnight, but the caller's start may carry a
14928
+ // time of day, which would otherwise make re-picking the same day restart the range.
14929
+ if (compareDays(date, start) < 0) {
14310
14930
  this.selectedRangeChange.emit({ start: date, end: null });
14931
+ return;
14311
14932
  }
14312
- else {
14313
- // Setting end date
14314
- const start = currentRange.start;
14315
- // If second date is earlier than first, treat it as new start date
14316
- if (date < start) {
14317
- const newRangeState = {
14318
- start: date,
14319
- end: null,
14320
- selecting: 'end'
14321
- };
14322
- this.rangeState.set(newRangeState);
14323
- this.selectedRangeChange.emit({ start: date, end: null });
14324
- }
14325
- else {
14326
- // Valid end date - complete the range
14327
- const newRangeState = {
14328
- start: start,
14329
- end: date,
14330
- selecting: 'start'
14331
- };
14332
- this.rangeState.set(newRangeState);
14333
- this.selectedRangeChange.emit({ start: start, end: date });
14334
- }
14335
- }
14933
+ this.selectedRangeChange.emit({ start, end: date });
14336
14934
  }
14337
14935
  onActiveDateChange(date) {
14338
14936
  this.currentDate.set(date);
14339
14937
  this.activeDateChange.emit(date);
14340
14938
  }
14939
+ /**
14940
+ * A year picked in the year grid moves the view to that year and returns to the day
14941
+ * grid. It is navigation, not selection — no `selectedChange` — so the user lands on
14942
+ * the month they asked for and picks a day from there.
14943
+ */
14341
14944
  onYearSelectedFromView(date) {
14342
- // When a year is selected from the multi-year view, update the current date
14343
- // and switch back to month view
14344
14945
  this.currentDate.set(date);
14345
14946
  this.currentView.set('month');
14346
14947
  this.viewChanged.emit('month');
14347
14948
  this.activeDateChange.emit(date);
14348
- }
14349
- /**
14350
- * Reset the calendar to accept external range values - called when calendar reopens
14351
- */
14352
- resetInteractionState() {
14353
- this.userHasInteracted = false;
14354
- // Reinitialize range state from selectedRange if provided
14355
- this.initializeRangeState();
14949
+ // Switching grids destroys the year cell the user just activated, which drops focus
14950
+ // to `<body>` and restarts the tab order. Carry it over to the day now active.
14951
+ focusActiveCellAfterRender(this.host, this.injector);
14356
14952
  }
14357
14953
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnCalendarComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
14358
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.0", type: TnCalendarComponent, isStandalone: true, selector: "tn-calendar", inputs: { startView: { classPropertyName: "startView", publicName: "startView", isSignal: true, isRequired: false, transformFunction: null }, selected: { classPropertyName: "selected", publicName: "selected", isSignal: true, isRequired: false, transformFunction: null }, minDate: { classPropertyName: "minDate", publicName: "minDate", isSignal: true, isRequired: false, transformFunction: null }, maxDate: { classPropertyName: "maxDate", publicName: "maxDate", isSignal: true, isRequired: false, transformFunction: null }, dateFilter: { classPropertyName: "dateFilter", publicName: "dateFilter", isSignal: true, isRequired: false, transformFunction: null }, rangeMode: { classPropertyName: "rangeMode", publicName: "rangeMode", isSignal: true, isRequired: false, transformFunction: null }, selectedRange: { classPropertyName: "selectedRange", publicName: "selectedRange", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { selectedChange: "selectedChange", activeDateChange: "activeDateChange", viewChanged: "viewChanged", selectedRangeChange: "selectedRangeChange" }, hostDirectives: [{ directive: TnTestIdDirective, inputs: ["tnTestId", "testId"] }], ngImport: i0, template: "<tn-calendar-header\n [currentDate]=\"currentDate()\"\n [currentView]=\"currentView()\"\n (monthSelected)=\"onMonthSelected($event)\"\n (yearSelected)=\"onYearSelected($event)\"\n (viewChanged)=\"onViewChanged($event)\"\n (previousClicked)=\"onPreviousClicked()\"\n (nextClicked)=\"onNextClicked()\" />\n\n<div class=\"tn-calendar-content\" cdkMonitorSubtreeFocus tabindex=\"-1\">\n @if (currentView() === 'month') {\n <tn-month-view\n [activeDate]=\"currentDate()\"\n [selected]=\"selected()\"\n [dateFilter]=\"dateFilter()\"\n [minDate]=\"minDate()\"\n [maxDate]=\"maxDate()\"\n [rangeMode]=\"rangeMode()\"\n [selectedRange]=\"rangeMode() ? rangeState() : undefined\"\n (selectedChange)=\"onSelectedChange($event)\"\n (activeDateChange)=\"onActiveDateChange($event)\" />\n }\n\n <!-- Multi-year view -->\n @if (currentView() === 'year') {\n <tn-multi-year-view\n [activeDate]=\"currentDate()\"\n [selected]=\"selected()\"\n [dateFilter]=\"dateFilter()\"\n [minDate]=\"minDate()\"\n [maxDate]=\"maxDate()\"\n (selectedChange)=\"onYearSelectedFromView($event)\"\n (activeDateChange)=\"onActiveDateChange($event)\" />\n }\n</div>\n", styles: [":host{display:block;background:var(--tn-bg2, #f5f5f5);color:var(--tn-fg1, #333);padding:0 8px 8px;box-shadow:0 4px 16px #0000001f,0 1px 4px #00000014}.tn-calendar-content{padding:8px;outline:none}.tn-year-view{text-align:center;padding:20px;color:var(--tn-fg2, #666)}\n"], dependencies: [{ kind: "component", type: TnCalendarHeaderComponent, selector: "tn-calendar-header", inputs: ["currentDate", "currentView"], outputs: ["monthSelected", "yearSelected", "viewChanged", "previousClicked", "nextClicked"] }, { kind: "component", type: TnMonthViewComponent, selector: "tn-month-view", inputs: ["activeDate", "selected", "minDate", "maxDate", "dateFilter", "rangeMode", "selectedRange"], outputs: ["selectedChange", "activeDateChange"] }, { kind: "component", type: TnMultiYearViewComponent, selector: "tn-multi-year-view", inputs: ["activeDate", "selected", "minDate", "maxDate", "dateFilter"], outputs: ["selectedChange", "activeDateChange"] }] });
14954
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.0", type: TnCalendarComponent, isStandalone: true, selector: "tn-calendar", inputs: { startView: { classPropertyName: "startView", publicName: "startView", isSignal: true, isRequired: false, transformFunction: null }, selected: { classPropertyName: "selected", publicName: "selected", isSignal: true, isRequired: false, transformFunction: null }, minDate: { classPropertyName: "minDate", publicName: "minDate", isSignal: true, isRequired: false, transformFunction: null }, maxDate: { classPropertyName: "maxDate", publicName: "maxDate", isSignal: true, isRequired: false, transformFunction: null }, dateFilter: { classPropertyName: "dateFilter", publicName: "dateFilter", isSignal: true, isRequired: false, transformFunction: null }, markedDates: { classPropertyName: "markedDates", publicName: "markedDates", isSignal: true, isRequired: false, transformFunction: null }, activeDate: { classPropertyName: "activeDate", publicName: "activeDate", isSignal: true, isRequired: false, transformFunction: null }, locale: { classPropertyName: "locale", publicName: "locale", isSignal: true, isRequired: false, transformFunction: null }, rangeMode: { classPropertyName: "rangeMode", publicName: "rangeMode", isSignal: true, isRequired: false, transformFunction: null }, selectedRange: { classPropertyName: "selectedRange", publicName: "selectedRange", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { selectedChange: "selectedChange", activeDateChange: "activeDateChange", viewChanged: "viewChanged", selectedRangeChange: "selectedRangeChange" }, hostDirectives: [{ directive: TnTestIdDirective, inputs: ["tnTestId", "testId"] }], ngImport: i0, template: "<tn-calendar-header\n [currentDate]=\"currentDate()\"\n [locale]=\"resolvedLocale()\"\n [currentView]=\"currentView()\"\n (viewChanged)=\"onViewChanged($event)\"\n (previousClicked)=\"onPreviousClicked()\"\n (nextClicked)=\"onNextClicked()\" />\n\n<div class=\"tn-calendar-content\">\n @if (currentView() === 'month') {\n <tn-month-view\n [activeDate]=\"currentDate()\"\n [locale]=\"resolvedLocale()\"\n [selected]=\"selected()\"\n [dateFilter]=\"dateFilter()\"\n [minDate]=\"minDate()\"\n [maxDate]=\"maxDate()\"\n [markedDates]=\"markedDates()\"\n [rangeMode]=\"rangeMode()\"\n [selectedRange]=\"rangeMode() ? selectedRange() : undefined\"\n (selectedChange)=\"onSelectedChange($event)\"\n (activeDateChange)=\"onActiveDateChange($event)\" />\n }\n\n <!-- Multi-year view -->\n @if (currentView() === 'year') {\n <tn-multi-year-view\n [activeDate]=\"currentDate()\"\n [locale]=\"resolvedLocale()\"\n [selected]=\"selected()\"\n [dateFilter]=\"dateFilter()\"\n [minDate]=\"minDate()\"\n [maxDate]=\"maxDate()\"\n (selectedChange)=\"onYearSelectedFromView($event)\"\n (activeDateChange)=\"onActiveDateChange($event)\" />\n }\n</div>\n", styles: [":host{display:block;background:var(--tn-bg2, #f5f5f5);color:var(--tn-fg1, #333);padding:0 8px 8px;box-shadow:0 4px 16px #0000001f,0 1px 4px #00000014}.tn-calendar-content{padding:8px;outline:none}.tn-year-view{text-align:center;padding:20px;color:var(--tn-fg2, #666)}\n"], dependencies: [{ kind: "component", type: TnCalendarHeaderComponent, selector: "tn-calendar-header", inputs: ["currentDate", "currentView", "locale"], outputs: ["viewChanged", "previousClicked", "nextClicked"] }, { kind: "component", type: TnMonthViewComponent, selector: "tn-month-view", inputs: ["activeDate", "selected", "minDate", "maxDate", "dateFilter", "markedDates", "rangeMode", "selectedRange", "locale"], outputs: ["selectedChange", "activeDateChange"] }, { kind: "component", type: TnMultiYearViewComponent, selector: "tn-multi-year-view", inputs: ["activeDate", "selected", "minDate", "maxDate", "dateFilter", "locale"], outputs: ["selectedChange", "activeDateChange"] }] });
14359
14955
  }
14360
14956
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnCalendarComponent, decorators: [{
14361
14957
  type: Component,
14362
- args: [{ selector: 'tn-calendar', standalone: true, imports: [TnCalendarHeaderComponent, TnMonthViewComponent, TnMultiYearViewComponent], hostDirectives: [{ directive: TnTestIdDirective, inputs: ['tnTestId: testId'] }], template: "<tn-calendar-header\n [currentDate]=\"currentDate()\"\n [currentView]=\"currentView()\"\n (monthSelected)=\"onMonthSelected($event)\"\n (yearSelected)=\"onYearSelected($event)\"\n (viewChanged)=\"onViewChanged($event)\"\n (previousClicked)=\"onPreviousClicked()\"\n (nextClicked)=\"onNextClicked()\" />\n\n<div class=\"tn-calendar-content\" cdkMonitorSubtreeFocus tabindex=\"-1\">\n @if (currentView() === 'month') {\n <tn-month-view\n [activeDate]=\"currentDate()\"\n [selected]=\"selected()\"\n [dateFilter]=\"dateFilter()\"\n [minDate]=\"minDate()\"\n [maxDate]=\"maxDate()\"\n [rangeMode]=\"rangeMode()\"\n [selectedRange]=\"rangeMode() ? rangeState() : undefined\"\n (selectedChange)=\"onSelectedChange($event)\"\n (activeDateChange)=\"onActiveDateChange($event)\" />\n }\n\n <!-- Multi-year view -->\n @if (currentView() === 'year') {\n <tn-multi-year-view\n [activeDate]=\"currentDate()\"\n [selected]=\"selected()\"\n [dateFilter]=\"dateFilter()\"\n [minDate]=\"minDate()\"\n [maxDate]=\"maxDate()\"\n (selectedChange)=\"onYearSelectedFromView($event)\"\n (activeDateChange)=\"onActiveDateChange($event)\" />\n }\n</div>\n", styles: [":host{display:block;background:var(--tn-bg2, #f5f5f5);color:var(--tn-fg1, #333);padding:0 8px 8px;box-shadow:0 4px 16px #0000001f,0 1px 4px #00000014}.tn-calendar-content{padding:8px;outline:none}.tn-year-view{text-align:center;padding:20px;color:var(--tn-fg2, #666)}\n"] }]
14363
- }], ctorParameters: () => [], propDecorators: { startView: [{ type: i0.Input, args: [{ isSignal: true, alias: "startView", required: false }] }], selected: [{ type: i0.Input, args: [{ isSignal: true, alias: "selected", required: false }] }], minDate: [{ type: i0.Input, args: [{ isSignal: true, alias: "minDate", required: false }] }], maxDate: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxDate", required: false }] }], dateFilter: [{ type: i0.Input, args: [{ isSignal: true, alias: "dateFilter", required: false }] }], rangeMode: [{ type: i0.Input, args: [{ isSignal: true, alias: "rangeMode", required: false }] }], selectedRange: [{ type: i0.Input, args: [{ isSignal: true, alias: "selectedRange", required: false }] }], selectedChange: [{ type: i0.Output, args: ["selectedChange"] }], activeDateChange: [{ type: i0.Output, args: ["activeDateChange"] }], viewChanged: [{ type: i0.Output, args: ["viewChanged"] }], selectedRangeChange: [{ type: i0.Output, args: ["selectedRangeChange"] }] } });
14958
+ args: [{ selector: 'tn-calendar', standalone: true, imports: [TnCalendarHeaderComponent, TnMonthViewComponent, TnMultiYearViewComponent], hostDirectives: [{ directive: TnTestIdDirective, inputs: ['tnTestId: testId'] }], template: "<tn-calendar-header\n [currentDate]=\"currentDate()\"\n [locale]=\"resolvedLocale()\"\n [currentView]=\"currentView()\"\n (viewChanged)=\"onViewChanged($event)\"\n (previousClicked)=\"onPreviousClicked()\"\n (nextClicked)=\"onNextClicked()\" />\n\n<div class=\"tn-calendar-content\">\n @if (currentView() === 'month') {\n <tn-month-view\n [activeDate]=\"currentDate()\"\n [locale]=\"resolvedLocale()\"\n [selected]=\"selected()\"\n [dateFilter]=\"dateFilter()\"\n [minDate]=\"minDate()\"\n [maxDate]=\"maxDate()\"\n [markedDates]=\"markedDates()\"\n [rangeMode]=\"rangeMode()\"\n [selectedRange]=\"rangeMode() ? selectedRange() : undefined\"\n (selectedChange)=\"onSelectedChange($event)\"\n (activeDateChange)=\"onActiveDateChange($event)\" />\n }\n\n <!-- Multi-year view -->\n @if (currentView() === 'year') {\n <tn-multi-year-view\n [activeDate]=\"currentDate()\"\n [locale]=\"resolvedLocale()\"\n [selected]=\"selected()\"\n [dateFilter]=\"dateFilter()\"\n [minDate]=\"minDate()\"\n [maxDate]=\"maxDate()\"\n (selectedChange)=\"onYearSelectedFromView($event)\"\n (activeDateChange)=\"onActiveDateChange($event)\" />\n }\n</div>\n", styles: [":host{display:block;background:var(--tn-bg2, #f5f5f5);color:var(--tn-fg1, #333);padding:0 8px 8px;box-shadow:0 4px 16px #0000001f,0 1px 4px #00000014}.tn-calendar-content{padding:8px;outline:none}.tn-year-view{text-align:center;padding:20px;color:var(--tn-fg2, #666)}\n"] }]
14959
+ }], propDecorators: { startView: [{ type: i0.Input, args: [{ isSignal: true, alias: "startView", required: false }] }], selected: [{ type: i0.Input, args: [{ isSignal: true, alias: "selected", required: false }] }], minDate: [{ type: i0.Input, args: [{ isSignal: true, alias: "minDate", required: false }] }], maxDate: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxDate", required: false }] }], dateFilter: [{ type: i0.Input, args: [{ isSignal: true, alias: "dateFilter", required: false }] }], markedDates: [{ type: i0.Input, args: [{ isSignal: true, alias: "markedDates", required: false }] }], activeDate: [{ type: i0.Input, args: [{ isSignal: true, alias: "activeDate", required: false }] }], locale: [{ type: i0.Input, args: [{ isSignal: true, alias: "locale", required: false }] }], rangeMode: [{ type: i0.Input, args: [{ isSignal: true, alias: "rangeMode", required: false }] }], selectedRange: [{ type: i0.Input, args: [{ isSignal: true, alias: "selectedRange", required: false }] }], selectedChange: [{ type: i0.Output, args: ["selectedChange"] }], activeDateChange: [{ type: i0.Output, args: ["activeDateChange"] }], viewChanged: [{ type: i0.Output, args: ["viewChanged"] }], selectedRangeChange: [{ type: i0.Output, args: ["selectedRangeChange"] }] } });
14960
+
14961
+ /**
14962
+ * Harness for interacting with a single day (or year) cell inside `tn-calendar`.
14963
+ *
14964
+ * Modelled on Angular Material's `MatCalendarCellHarness` so specs can move across
14965
+ * with the method names intact. Three differences worth knowing:
14966
+ *
14967
+ * - `isMarked()` is new — it reports the `markedDates` state, which Material has no
14968
+ * equivalent for.
14969
+ * - `getDate()` and the `date` filter are new, and are the reliable way to name a cell:
14970
+ * the text is formatted for the app's locale, so `'14'` misses a calendar rendering
14971
+ * `١٤`.
14972
+ * - There are no comparison-range or preview-range methods (`isComparisonRangeStart()`
14973
+ * and friends); `tn-calendar` has no comparison range to report.
14974
+ *
14975
+ * @example
14976
+ * ```typescript
14977
+ * const calendar = await loader.getHarness(TnCalendarHarness);
14978
+ * const [first] = await calendar.getCells({ date: new Date(2031, 4, 14) });
14979
+ * await first.select();
14980
+ * expect(await first.isSelected()).toBe(true);
14981
+ * ```
14982
+ */
14983
+ class TnCalendarCellHarness extends ComponentHarness {
14984
+ /** The selector for a calendar cell. Padding cells render no button, so only real days match. */
14985
+ static hostSelector = '.tn-calendar-body-cell';
14986
+ _content = this.locatorFor('.tn-calendar-body-cell-content');
14987
+ /**
14988
+ * Gets a `HarnessPredicate` that can be used to search for a cell with specific attributes.
14989
+ *
14990
+ * @param options Options for filtering which cell instances are considered a match.
14991
+ * @returns A `HarnessPredicate` configured with the given options.
14992
+ *
14993
+ * @example
14994
+ * ```typescript
14995
+ * // Every day the schedule runs on
14996
+ * const runDays = await calendar.getCells({ marked: true });
14997
+ *
14998
+ * // The disabled days before minDate
14999
+ * const blocked = await calendar.getCells({ disabled: true });
15000
+ * ```
15001
+ */
15002
+ static with(options = {}) {
15003
+ return new HarnessPredicate(TnCalendarCellHarness, options)
15004
+ .addOption('text', options.text, (harness, text) => HarnessPredicate.stringMatches(harness.getText(), text))
15005
+ .addOption('date', options.date, async (harness, date) => (await harness.getDateStamp()) === isoDateString(date))
15006
+ .addOption('year', options.year, async (harness, year) => (await harness.getYear()) === year)
15007
+ .addOption('selected', options.selected, async (harness, selected) => (await harness.isSelected()) === selected)
15008
+ .addOption('active', options.active, async (harness, active) => (await harness.isActive()) === active)
15009
+ .addOption('disabled', options.disabled, async (harness, disabled) => (await harness.isDisabled()) === disabled)
15010
+ .addOption('today', options.today, async (harness, today) => (await harness.isToday()) === today)
15011
+ .addOption('inRange', options.inRange, async (harness, inRange) => (await harness.isInRange()) === inRange)
15012
+ .addOption('marked', options.marked, async (harness, marked) => (await harness.isMarked()) === marked);
15013
+ }
15014
+ /** Gets the text of the calendar cell. */
15015
+ async getText() {
15016
+ return (await this._content()).text();
15017
+ }
15018
+ /** Gets the aria-label of the calendar cell. */
15019
+ async getAriaLabel() {
15020
+ return (await this.host()).getAttribute('aria-label').then((label) => label ?? '');
15021
+ }
15022
+ /**
15023
+ * Gets the calendar day this cell represents, independent of how it is rendered —
15024
+ * `getText()` is formatted for the locale, so it reads `١٤` in Arabic while this still
15025
+ * answers the 14th. Midnight local time. `null` on a year cell, which has no day.
15026
+ */
15027
+ async getDate() {
15028
+ const stamp = await this.getDateStamp();
15029
+ if (!stamp) {
15030
+ return null;
15031
+ }
15032
+ const [year, month, day] = stamp.split('-').map(Number);
15033
+ return new Date(year, month - 1, day);
15034
+ }
15035
+ /**
15036
+ * Gets the year this cell represents, independent of how it is rendered. Works on year
15037
+ * cells; `null` on a day cell.
15038
+ */
15039
+ async getYear() {
15040
+ const year = await (await this.host()).getAttribute('data-tn-year');
15041
+ return year === null ? null : Number(year);
15042
+ }
15043
+ /** The raw `YYYY-MM-DD` stamp, for comparing without rebuilding a `Date`. */
15044
+ async getDateStamp() {
15045
+ return (await this.host()).getAttribute('data-tn-date');
15046
+ }
15047
+ /** Whether the cell is selected. */
15048
+ async isSelected() {
15049
+ return (await this.host()).hasClass('tn-calendar-body-selected');
15050
+ }
15051
+ /** Whether the cell is disabled. */
15052
+ async isDisabled() {
15053
+ return (await this.host()).getProperty('disabled');
15054
+ }
15055
+ /**
15056
+ * Whether the cell holds the grid's roving tabindex — the one cell reachable with Tab,
15057
+ * which the arrow keys move. Independent of selection: arrowing around changes which
15058
+ * cell is active without selecting anything.
15059
+ */
15060
+ async isActive() {
15061
+ return (await this.host()).hasClass('tn-calendar-body-active');
15062
+ }
15063
+ /** Whether the cell represents today's date. */
15064
+ async isToday() {
15065
+ return (await this.host()).hasClass('tn-calendar-body-today');
15066
+ }
15067
+ /** Whether the cell was flagged through the calendar's `markedDates` input. */
15068
+ async isMarked() {
15069
+ return (await this.host()).hasClass('tn-calendar-body-marked');
15070
+ }
15071
+ /** Whether the cell is the start of the selected range. */
15072
+ async isRangeStart() {
15073
+ return (await this.host()).hasClass('tn-calendar-body-range-start');
15074
+ }
15075
+ /** Whether the cell is the end of the selected range. */
15076
+ async isRangeEnd() {
15077
+ return (await this.host()).hasClass('tn-calendar-body-range-end');
15078
+ }
15079
+ /**
15080
+ * Whether the cell is part of the selected range, ends included — matching Material's
15081
+ * `isInRange()`. Use `isRangeStart()`/`isRangeEnd()` to single out the ends.
15082
+ */
15083
+ async isInRange() {
15084
+ const host = await this.host();
15085
+ const [between, start, end] = await parallel(() => [
15086
+ host.hasClass('tn-calendar-body-in-range'),
15087
+ host.hasClass('tn-calendar-body-range-start'),
15088
+ host.hasClass('tn-calendar-body-range-end'),
15089
+ ]);
15090
+ return between || start || end;
15091
+ }
15092
+ /** Selects the calendar cell. Won't do anything if the cell is disabled. */
15093
+ async select() {
15094
+ return (await this.host()).click();
15095
+ }
15096
+ /**
15097
+ * Presses a key on the cell. The grid handles its keys on the cells themselves, so
15098
+ * this is how the roving tabindex is driven — arrows, Home/End and PageUp/PageDown
15099
+ * move the active cell, while Enter and Space select.
15100
+ *
15101
+ * @param key The key to press, e.g. `TestKey.RIGHT_ARROW`.
15102
+ * @param modifiers Modifier keys held while pressing, e.g. `{ shift: true }`.
15103
+ *
15104
+ * @example
15105
+ * ```typescript
15106
+ * const [cell] = await calendar.getCells({ active: true });
15107
+ * await cell.press(TestKey.PAGE_DOWN, { shift: true }); // a year on
15108
+ * ```
15109
+ */
15110
+ async press(key, modifiers) {
15111
+ const host = await this.host();
15112
+ return modifiers ? host.sendKeys(modifiers, key) : host.sendKeys(key);
15113
+ }
15114
+ /** Hovers over the calendar cell. */
15115
+ async hover() {
15116
+ return (await this.host()).hover();
15117
+ }
15118
+ /** Moves the mouse away from the calendar cell. */
15119
+ async mouseAway() {
15120
+ return (await this.host()).mouseAway();
15121
+ }
15122
+ /** Focuses the calendar cell. */
15123
+ async focus() {
15124
+ return (await this.host()).focus();
15125
+ }
15126
+ /** Removes focus from the calendar cell. */
15127
+ async blur() {
15128
+ return (await this.host()).blur();
15129
+ }
15130
+ }
15131
+ /**
15132
+ * Harness for interacting with `tn-calendar` in tests.
15133
+ *
15134
+ * The method names mirror Angular Material's `MatCalendarHarness`, so a spec written
15135
+ * against the Material calendar moves over unchanged. Differences to know:
15136
+ *
15137
+ * - `getCurrentView()` resolves to `'month' | 'year'`. `tn-calendar` has no separate
15138
+ * multi-year view — its year view *is* the multi-year grid.
15139
+ * - Cell filters gain `marked` for the `markedDates` state and `date`/`year` for naming
15140
+ * a cell without going through its rendered text, and drop Material's
15141
+ * `inComparisonRange`, which has no equivalent here.
15142
+ *
15143
+ * @example
15144
+ * ```typescript
15145
+ * const calendar = await loader.getHarness(TnCalendarHarness);
15146
+ *
15147
+ * // Which days does the schedule run on this month?
15148
+ * const marked = await calendar.getCells({ marked: true });
15149
+ * expect(await parallel(() => marked.map((cell) => cell.getText()))).toEqual(['24', '25']);
15150
+ *
15151
+ * // Page forward a month
15152
+ * await calendar.next();
15153
+ * expect(await calendar.getCurrentViewLabel()).toBe('MAR 2022');
15154
+ *
15155
+ * // Pick a day
15156
+ * await calendar.selectCell({ date: new Date(2022, 2, 14) });
15157
+ * ```
15158
+ */
15159
+ class TnCalendarHarness extends ComponentHarness {
15160
+ /** The selector for the host element of a `TnCalendarComponent` instance. */
15161
+ static hostSelector = 'tn-calendar';
15162
+ _periodButton = this.locatorFor('.tn-calendar-period-button');
15163
+ _previousButton = this.locatorFor('.tn-calendar-previous-button');
15164
+ _nextButton = this.locatorFor('.tn-calendar-next-button');
15165
+ _monthView = this.locatorForOptional('tn-month-view');
15166
+ /**
15167
+ * Gets a `HarnessPredicate` that can be used to search for a calendar with specific
15168
+ * attributes.
15169
+ *
15170
+ * @param options Options for filtering which calendar instances are considered a match.
15171
+ * @returns A `HarnessPredicate` configured with the given options.
15172
+ */
15173
+ static with(options = {}) {
15174
+ return new HarnessPredicate(TnCalendarHarness, options);
15175
+ }
15176
+ /**
15177
+ * Gets a list of cells inside the calendar. Padding cells for days outside the month
15178
+ * are not included.
15179
+ *
15180
+ * @param filter Optionally filters which cells are included.
15181
+ */
15182
+ async getCells(filter = {}) {
15183
+ return this.locatorForAll(TnCalendarCellHarness.with(filter))();
15184
+ }
15185
+ /** Gets the current view that is being shown inside the calendar. */
15186
+ async getCurrentView() {
15187
+ return (await this._monthView()) ? 'month' : 'year';
15188
+ }
15189
+ /** Gets the label of the current calendar view, e.g. `'MAR 2022'`. */
15190
+ async getCurrentViewLabel() {
15191
+ return (await this._periodButton()).text();
15192
+ }
15193
+ /** Changes the calendar view by clicking on the view toggle button. */
15194
+ async changeView() {
15195
+ return (await this._periodButton()).click();
15196
+ }
15197
+ /** Goes to the next page of the current view (e.g. next month when inside the month view). */
15198
+ async next() {
15199
+ return (await this._nextButton()).click();
15200
+ }
15201
+ /** Goes to the previous page of the current view (e.g. previous month in the month view). */
15202
+ async previous() {
15203
+ return (await this._previousButton()).click();
15204
+ }
15205
+ /**
15206
+ * Selects a cell in the current calendar view.
15207
+ *
15208
+ * @param filter An optional filter to apply to the cells. The first cell matching the
15209
+ * filter will be selected.
15210
+ */
15211
+ async selectCell(filter = {}) {
15212
+ const cells = await this.getCells(filter);
15213
+ if (!cells.length) {
15214
+ throw Error(`Cannot find calendar cell matching filter ${JSON.stringify(filter)}`);
15215
+ }
15216
+ await cells[0].select();
15217
+ }
15218
+ /**
15219
+ * Gets the cell holding the grid's roving tabindex — the one cell Tab reaches, and the
15220
+ * one the arrow keys move.
15221
+ *
15222
+ * `null` only when every cell in the view is disabled, where there is nothing to focus.
15223
+ */
15224
+ async getActiveCell() {
15225
+ const [active] = await this.getCells({ active: true });
15226
+ return active ?? null;
15227
+ }
15228
+ /**
15229
+ * Moves the grid's roving tabindex with the keyboard, without going through the DOM.
15230
+ *
15231
+ * Arrows move by a day or a week (a year or a row of years); Home and End reach the
15232
+ * ends of the month or year page; PageUp and PageDown page, shifted by a year. Moving
15233
+ * never changes the selection — use `selectCell()` or `TnCalendarCellHarness.select()`
15234
+ * for that.
15235
+ *
15236
+ * @param key The key to press, e.g. `TestKey.RIGHT_ARROW`.
15237
+ * @param modifiers Modifier keys held while pressing, e.g. `{ shift: true }`.
15238
+ *
15239
+ * @example
15240
+ * ```typescript
15241
+ * await calendar.moveActiveCell(TestKey.DOWN_ARROW);
15242
+ * expect(await (await calendar.getActiveCell())!.getText()).toBe('17');
15243
+ * ```
15244
+ */
15245
+ async moveActiveCell(key, modifiers) {
15246
+ const active = await this.getActiveCell();
15247
+ if (!active) {
15248
+ throw Error('Cannot move the active cell: the calendar has no cell able to take focus.');
15249
+ }
15250
+ await active.press(key, modifiers);
15251
+ }
15252
+ }
14364
15253
 
14365
15254
  class TnDateInputComponent {
14366
15255
  overlay = inject(Overlay);
@@ -14381,9 +15270,7 @@ class TnDateInputComponent {
14381
15270
  dayRef = viewChild.required('dayInput');
14382
15271
  yearRef = viewChild.required('yearInput');
14383
15272
  calendarTemplate = viewChild.required('calendarTemplate');
14384
- calendar = viewChild.required(TnCalendarComponent);
14385
15273
  wrapperEl = viewChild.required('wrapper');
14386
- destroy$ = new Subject();
14387
15274
  overlayRef;
14388
15275
  portal;
14389
15276
  isOpen = signal(false, ...(ngDevMode ? [{ debugName: "isOpen" }] : []));
@@ -14399,8 +15286,6 @@ class TnDateInputComponent {
14399
15286
  this.updateDisplayValues();
14400
15287
  }
14401
15288
  ngOnDestroy() {
14402
- this.destroy$.next();
14403
- this.destroy$.complete();
14404
15289
  this.close();
14405
15290
  }
14406
15291
  // ControlValueAccessor implementation
@@ -14543,11 +15428,8 @@ class TnDateInputComponent {
14543
15428
  }
14544
15429
  this.createOverlay();
14545
15430
  this.isOpen.set(true);
14546
- // Reset calendar interaction state when opening
14547
- const cal = this.calendar();
14548
- if (cal) {
14549
- setTimeout(() => cal.resetInteractionState(), 0);
14550
- }
15431
+ // No calendar state to reset — the calendar renders whatever date this component
15432
+ // binds to it, so reopening already shows the current value.
14551
15433
  }
14552
15434
  close() {
14553
15435
  if (this.overlayRef) {
@@ -14604,6 +15486,8 @@ class TnDateInputComponent {
14604
15486
  panelClass: 'tn-datepicker-overlay'
14605
15487
  });
14606
15488
  // Close datepicker when backdrop is clicked
15489
+ // No teardown to track: `dispose()` in close() ends this subscription with the
15490
+ // overlay, and ngOnDestroy closes.
14607
15491
  this.overlayRef.backdropClick().subscribe(() => {
14608
15492
  this.close();
14609
15493
  });
@@ -14617,7 +15501,7 @@ class TnDateInputComponent {
14617
15501
  useExisting: forwardRef(() => TnDateInputComponent),
14618
15502
  multi: true
14619
15503
  }
14620
- ], viewQueries: [{ propertyName: "monthRef", first: true, predicate: ["monthInput"], descendants: true, isSignal: true }, { propertyName: "dayRef", first: true, predicate: ["dayInput"], descendants: true, isSignal: true }, { propertyName: "yearRef", first: true, predicate: ["yearInput"], descendants: true, isSignal: true }, { propertyName: "calendarTemplate", first: true, predicate: ["calendarTemplate"], descendants: true, isSignal: true }, { propertyName: "calendar", first: true, predicate: TnCalendarComponent, descendants: true, isSignal: true }, { propertyName: "wrapperEl", first: true, predicate: ["wrapper"], descendants: true, isSignal: true }], ngImport: i0, template: "<div class=\"tn-date-input-container\" tnTestIdType=\"date-input\" [tnTestId]=\"testId()\">\n <div #wrapper ixInput class=\"tn-date-input-wrapper\" style=\"padding-right: 40px;\">\n <!-- Date segments MM/DD/YYYY -->\n <div class=\"tn-date-segment-group\">\n <input\n #monthInput\n type=\"text\"\n class=\"tn-date-segment tn-date-segment-month\"\n placeholder=\"MM\"\n maxlength=\"2\"\n [disabled]=\"isDisabled()\"\n (focus)=\"onSegmentFocus('month')\"\n (blur)=\"onSegmentBlur('month')\"\n (keydown)=\"onSegmentKeydown($event, 'month')\">\n <span class=\"tn-date-segment-separator\">/</span>\n <input\n #dayInput\n type=\"text\"\n class=\"tn-date-segment tn-date-segment-day\"\n placeholder=\"DD\"\n maxlength=\"2\"\n [disabled]=\"isDisabled()\"\n (focus)=\"onSegmentFocus('day')\"\n (blur)=\"onSegmentBlur('day')\"\n (keydown)=\"onSegmentKeydown($event, 'day')\">\n <span class=\"tn-date-segment-separator\">/</span>\n <input\n #yearInput\n type=\"text\"\n class=\"tn-date-segment tn-date-segment-year\"\n placeholder=\"YYYY\"\n maxlength=\"4\"\n [disabled]=\"isDisabled()\"\n (focus)=\"onSegmentFocus('year')\"\n (blur)=\"onSegmentBlur('year')\"\n (keydown)=\"onSegmentKeydown($event, 'year')\">\n </div>\n\n <button\n type=\"button\"\n class=\"tn-date-input-toggle\"\n aria-label=\"Open calendar\"\n [disabled]=\"isDisabled()\"\n (click)=\"openDatepicker()\">\n <span aria-hidden=\"true\">\uD83D\uDCC5</span>\n </button>\n </div>\n\n <ng-template #calendarTemplate>\n <tn-calendar\n class=\"tn-calendar\"\n [startView]=\"'month'\"\n [rangeMode]=\"false\"\n [selected]=\"value()\"\n (selectedChange)=\"onDateSelected($event)\" />\n </ng-template>\n</div>\n", styles: [":host{display:block;width:100%}.tn-date-input-container{position:relative;display:flex;align-items:center}.tn-date-input-wrapper{display:flex;align-items:center;width:100%;position:relative}.tn-date-segment-group{display:flex;align-items:center}.tn-date-segment{background:transparent;border:none;outline:none;font:inherit;color:inherit;padding:0;min-width:0;text-align:center;width:2.6ch}.tn-date-segment::placeholder{color:var(--tn-alt-fg1, #999);opacity:1}.tn-date-segment:focus{outline:none;background:var(--tn-bg2, rgba(0, 0, 0, .05));border-radius:2px}.tn-date-segment:focus::placeholder{opacity:0}.tn-date-segment.tn-date-segment-year{width:4ch}.tn-date-segment-separator{padding:0 2px;-webkit-user-select:none;user-select:none;color:var(--tn-alt-fg1, #999)}.tn-date-input-toggle{position:absolute;right:8px;z-index:2;pointer-events:auto;background:transparent;border:none;cursor:pointer;padding:4px;font-size:1rem}.tn-date-input-toggle:hover{background:var(--tn-bg2, #f0f0f0);border-radius:4px}.tn-date-input-toggle:disabled{cursor:not-allowed;opacity:.5}:host ::ng-deep .tn-datepicker-overlay .tn-calendar{background:var(--tn-bg1, white);border:1px solid var(--tn-lines, #e0e0e0);border-radius:8px;box-shadow:0 4px 12px #00000026;padding:24px;min-width:380px;--calendar-cell-size: 48px;--calendar-header-height: 44px;--calendar-cell-font-size: 1rem;--calendar-header-font-size: 14px}:host ::ng-deep .tn-datepicker-overlay .tn-calendar .tn-calendar-content{padding:0}\n"], dependencies: [{ kind: "component", type: TnCalendarComponent, selector: "tn-calendar", inputs: ["startView", "selected", "minDate", "maxDate", "dateFilter", "rangeMode", "selectedRange"], outputs: ["selectedChange", "activeDateChange", "viewChanged", "selectedRangeChange"] }, { kind: "ngmodule", type: OverlayModule }, { kind: "ngmodule", type: PortalModule }, { kind: "ngmodule", type: A11yModule }, { kind: "directive", type: TnTestIdDirective, selector: "[tnTestId]", inputs: ["tnTestId", "tnTestIdType"] }] });
15504
+ ], viewQueries: [{ propertyName: "monthRef", first: true, predicate: ["monthInput"], descendants: true, isSignal: true }, { propertyName: "dayRef", first: true, predicate: ["dayInput"], descendants: true, isSignal: true }, { propertyName: "yearRef", first: true, predicate: ["yearInput"], descendants: true, isSignal: true }, { propertyName: "calendarTemplate", first: true, predicate: ["calendarTemplate"], descendants: true, isSignal: true }, { propertyName: "wrapperEl", first: true, predicate: ["wrapper"], descendants: true, isSignal: true }], ngImport: i0, template: "<div class=\"tn-date-input-container\" tnTestIdType=\"date-input\" [tnTestId]=\"testId()\">\n <div #wrapper ixInput class=\"tn-date-input-wrapper\" style=\"padding-right: 40px;\">\n <!-- Date segments MM/DD/YYYY -->\n <div class=\"tn-date-segment-group\">\n <input\n #monthInput\n type=\"text\"\n class=\"tn-date-segment tn-date-segment-month\"\n placeholder=\"MM\"\n maxlength=\"2\"\n [disabled]=\"isDisabled()\"\n (focus)=\"onSegmentFocus('month')\"\n (blur)=\"onSegmentBlur('month')\"\n (keydown)=\"onSegmentKeydown($event, 'month')\">\n <span class=\"tn-date-segment-separator\">/</span>\n <input\n #dayInput\n type=\"text\"\n class=\"tn-date-segment tn-date-segment-day\"\n placeholder=\"DD\"\n maxlength=\"2\"\n [disabled]=\"isDisabled()\"\n (focus)=\"onSegmentFocus('day')\"\n (blur)=\"onSegmentBlur('day')\"\n (keydown)=\"onSegmentKeydown($event, 'day')\">\n <span class=\"tn-date-segment-separator\">/</span>\n <input\n #yearInput\n type=\"text\"\n class=\"tn-date-segment tn-date-segment-year\"\n placeholder=\"YYYY\"\n maxlength=\"4\"\n [disabled]=\"isDisabled()\"\n (focus)=\"onSegmentFocus('year')\"\n (blur)=\"onSegmentBlur('year')\"\n (keydown)=\"onSegmentKeydown($event, 'year')\">\n </div>\n\n <button\n type=\"button\"\n class=\"tn-date-input-toggle\"\n aria-label=\"Open calendar\"\n [disabled]=\"isDisabled()\"\n (click)=\"openDatepicker()\">\n <span aria-hidden=\"true\">\uD83D\uDCC5</span>\n </button>\n </div>\n\n <ng-template #calendarTemplate>\n <tn-calendar\n class=\"tn-calendar\"\n [startView]=\"'month'\"\n [rangeMode]=\"false\"\n [selected]=\"value()\"\n (selectedChange)=\"onDateSelected($event)\" />\n </ng-template>\n</div>\n", styles: [":host{display:block;width:100%}.tn-date-input-container{position:relative;display:flex;align-items:center}.tn-date-input-wrapper{display:flex;align-items:center;width:100%;position:relative}.tn-date-segment-group{display:flex;align-items:center}.tn-date-segment{background:transparent;border:none;outline:none;font:inherit;color:inherit;padding:0;min-width:0;text-align:center;width:2.6ch}.tn-date-segment::placeholder{color:var(--tn-alt-fg1, #999);opacity:1}.tn-date-segment:focus{outline:none;background:var(--tn-bg2, rgba(0, 0, 0, .05));border-radius:2px}.tn-date-segment:focus::placeholder{opacity:0}.tn-date-segment.tn-date-segment-year{width:4ch}.tn-date-segment-separator{padding:0 2px;-webkit-user-select:none;user-select:none;color:var(--tn-alt-fg1, #999)}.tn-date-input-toggle{position:absolute;right:8px;z-index:2;pointer-events:auto;background:transparent;border:none;cursor:pointer;padding:4px;font-size:1rem}.tn-date-input-toggle:hover{background:var(--tn-bg2, #f0f0f0);border-radius:4px}.tn-date-input-toggle:disabled{cursor:not-allowed;opacity:.5}:host ::ng-deep .tn-datepicker-overlay .tn-calendar{background:var(--tn-bg1, white);border:1px solid var(--tn-lines, #e0e0e0);border-radius:8px;box-shadow:0 4px 12px #00000026;padding:24px;min-width:380px;--calendar-cell-size: 48px;--calendar-header-height: 44px;--calendar-cell-font-size: 1rem;--calendar-header-font-size: 14px}:host ::ng-deep .tn-datepicker-overlay .tn-calendar .tn-calendar-content{padding:0}\n"], dependencies: [{ kind: "component", type: TnCalendarComponent, selector: "tn-calendar", inputs: ["startView", "selected", "minDate", "maxDate", "dateFilter", "markedDates", "activeDate", "locale", "rangeMode", "selectedRange"], outputs: ["selectedChange", "activeDateChange", "viewChanged", "selectedRangeChange"] }, { kind: "ngmodule", type: OverlayModule }, { kind: "ngmodule", type: PortalModule }, { kind: "ngmodule", type: A11yModule }, { kind: "directive", type: TnTestIdDirective, selector: "[tnTestId]", inputs: ["tnTestId", "tnTestIdType"] }] });
14621
15505
  }
14622
15506
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnDateInputComponent, decorators: [{
14623
15507
  type: Component,
@@ -14630,7 +15514,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
14630
15514
  ], host: {
14631
15515
  'class': 'tn-date-input'
14632
15516
  }, template: "<div class=\"tn-date-input-container\" tnTestIdType=\"date-input\" [tnTestId]=\"testId()\">\n <div #wrapper ixInput class=\"tn-date-input-wrapper\" style=\"padding-right: 40px;\">\n <!-- Date segments MM/DD/YYYY -->\n <div class=\"tn-date-segment-group\">\n <input\n #monthInput\n type=\"text\"\n class=\"tn-date-segment tn-date-segment-month\"\n placeholder=\"MM\"\n maxlength=\"2\"\n [disabled]=\"isDisabled()\"\n (focus)=\"onSegmentFocus('month')\"\n (blur)=\"onSegmentBlur('month')\"\n (keydown)=\"onSegmentKeydown($event, 'month')\">\n <span class=\"tn-date-segment-separator\">/</span>\n <input\n #dayInput\n type=\"text\"\n class=\"tn-date-segment tn-date-segment-day\"\n placeholder=\"DD\"\n maxlength=\"2\"\n [disabled]=\"isDisabled()\"\n (focus)=\"onSegmentFocus('day')\"\n (blur)=\"onSegmentBlur('day')\"\n (keydown)=\"onSegmentKeydown($event, 'day')\">\n <span class=\"tn-date-segment-separator\">/</span>\n <input\n #yearInput\n type=\"text\"\n class=\"tn-date-segment tn-date-segment-year\"\n placeholder=\"YYYY\"\n maxlength=\"4\"\n [disabled]=\"isDisabled()\"\n (focus)=\"onSegmentFocus('year')\"\n (blur)=\"onSegmentBlur('year')\"\n (keydown)=\"onSegmentKeydown($event, 'year')\">\n </div>\n\n <button\n type=\"button\"\n class=\"tn-date-input-toggle\"\n aria-label=\"Open calendar\"\n [disabled]=\"isDisabled()\"\n (click)=\"openDatepicker()\">\n <span aria-hidden=\"true\">\uD83D\uDCC5</span>\n </button>\n </div>\n\n <ng-template #calendarTemplate>\n <tn-calendar\n class=\"tn-calendar\"\n [startView]=\"'month'\"\n [rangeMode]=\"false\"\n [selected]=\"value()\"\n (selectedChange)=\"onDateSelected($event)\" />\n </ng-template>\n</div>\n", styles: [":host{display:block;width:100%}.tn-date-input-container{position:relative;display:flex;align-items:center}.tn-date-input-wrapper{display:flex;align-items:center;width:100%;position:relative}.tn-date-segment-group{display:flex;align-items:center}.tn-date-segment{background:transparent;border:none;outline:none;font:inherit;color:inherit;padding:0;min-width:0;text-align:center;width:2.6ch}.tn-date-segment::placeholder{color:var(--tn-alt-fg1, #999);opacity:1}.tn-date-segment:focus{outline:none;background:var(--tn-bg2, rgba(0, 0, 0, .05));border-radius:2px}.tn-date-segment:focus::placeholder{opacity:0}.tn-date-segment.tn-date-segment-year{width:4ch}.tn-date-segment-separator{padding:0 2px;-webkit-user-select:none;user-select:none;color:var(--tn-alt-fg1, #999)}.tn-date-input-toggle{position:absolute;right:8px;z-index:2;pointer-events:auto;background:transparent;border:none;cursor:pointer;padding:4px;font-size:1rem}.tn-date-input-toggle:hover{background:var(--tn-bg2, #f0f0f0);border-radius:4px}.tn-date-input-toggle:disabled{cursor:not-allowed;opacity:.5}:host ::ng-deep .tn-datepicker-overlay .tn-calendar{background:var(--tn-bg1, white);border:1px solid var(--tn-lines, #e0e0e0);border-radius:8px;box-shadow:0 4px 12px #00000026;padding:24px;min-width:380px;--calendar-cell-size: 48px;--calendar-header-height: 44px;--calendar-cell-font-size: 1rem;--calendar-header-font-size: 14px}:host ::ng-deep .tn-datepicker-overlay .tn-calendar .tn-calendar-content{padding:0}\n"] }]
14633
- }], propDecorators: { disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], min: [{ type: i0.Input, args: [{ isSignal: true, alias: "min", required: false }] }], max: [{ type: i0.Input, args: [{ isSignal: true, alias: "max", required: false }] }], dateFilter: [{ type: i0.Input, args: [{ isSignal: true, alias: "dateFilter", required: false }] }], testId: [{ type: i0.Input, args: [{ isSignal: true, alias: "testId", required: false }] }], monthRef: [{ type: i0.ViewChild, args: ['monthInput', { isSignal: true }] }], dayRef: [{ type: i0.ViewChild, args: ['dayInput', { isSignal: true }] }], yearRef: [{ type: i0.ViewChild, args: ['yearInput', { isSignal: true }] }], calendarTemplate: [{ type: i0.ViewChild, args: ['calendarTemplate', { isSignal: true }] }], calendar: [{ type: i0.ViewChild, args: [i0.forwardRef(() => TnCalendarComponent), { isSignal: true }] }], wrapperEl: [{ type: i0.ViewChild, args: ['wrapper', { isSignal: true }] }] } });
15517
+ }], propDecorators: { disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], min: [{ type: i0.Input, args: [{ isSignal: true, alias: "min", required: false }] }], max: [{ type: i0.Input, args: [{ isSignal: true, alias: "max", required: false }] }], dateFilter: [{ type: i0.Input, args: [{ isSignal: true, alias: "dateFilter", required: false }] }], testId: [{ type: i0.Input, args: [{ isSignal: true, alias: "testId", required: false }] }], monthRef: [{ type: i0.ViewChild, args: ['monthInput', { isSignal: true }] }], dayRef: [{ type: i0.ViewChild, args: ['dayInput', { isSignal: true }] }], yearRef: [{ type: i0.ViewChild, args: ['yearInput', { isSignal: true }] }], calendarTemplate: [{ type: i0.ViewChild, args: ['calendarTemplate', { isSignal: true }] }], wrapperEl: [{ type: i0.ViewChild, args: ['wrapper', { isSignal: true }] }] } });
14634
15518
 
14635
15519
  /**
14636
15520
  * Formats a Date into zero-padded month, day, and year strings.
@@ -14667,12 +15551,8 @@ async function setInputValue(el, value) {
14667
15551
  }
14668
15552
  await el.blur();
14669
15553
  }
14670
- const MONTHS = [
14671
- 'JAN', 'FEB', 'MAR', 'APR', 'MAY', 'JUN',
14672
- 'JUL', 'AUG', 'SEP', 'OCT', 'NOV', 'DEC',
14673
- ];
14674
15554
  /**
14675
- * Navigates the calendar overlay to the target month/year, then clicks the day cell.
15555
+ * Navigates the calendar overlay to the target month, then clicks that day.
14676
15556
  * Assumes the calendar popup is already open.
14677
15557
  *
14678
15558
  * @param rootLocator The document root locator factory (for finding overlay elements).
@@ -14680,45 +15560,48 @@ const MONTHS = [
14680
15560
  */
14681
15561
  async function selectCalendarDate(rootLocator, date) {
14682
15562
  await navigateCalendarTo(rootLocator, date);
14683
- const dayStr = date.getDate().toString();
14684
- const cells = await rootLocator.locatorForAll('.tn-calendar-body-cell:not([disabled])')();
14685
- for (const cell of cells) {
14686
- if ((await cell.text()).trim() === dayStr) {
14687
- await cell.click();
14688
- return;
14689
- }
14690
- }
14691
- throw new Error(`Could not find enabled calendar cell for day ${dayStr}`);
15563
+ // Matched on `data-tn-date` rather than the cell's text: the text is formatted for the
15564
+ // locale, right down to the numerals, so "15" finds nothing in an Arabic calendar.
15565
+ const target = isoDateString(date);
15566
+ const cell = await rootLocator.locatorForOptional(`.tn-calendar-body-cell[data-tn-date="${target}"]:not([disabled])`)();
15567
+ if (!cell) {
15568
+ throw new Error(`Could not find an enabled calendar cell for ${target}`);
15569
+ }
15570
+ await cell.click();
14692
15571
  }
14693
15572
  /**
14694
- * Navigates the calendar to show the month/year of the target date by
14695
- * clicking previous/next buttons as needed.
15573
+ * Pages the calendar to the month the target date falls in.
15574
+ *
15575
+ * Reads where the calendar currently sits from the days it is rendering rather than from
15576
+ * the header, which is written in the app's language and numerals. Every day cell in the
15577
+ * grid belongs to the displayed month, so the first one answers the question.
14696
15578
  */
14697
15579
  async function navigateCalendarTo(rootLocator, date) {
14698
- const targetLabel = `${MONTHS[date.getMonth()]} ${date.getFullYear()}`;
14699
- // Safety limit to prevent infinite loops
14700
- for (let i = 0; i < 120; i++) {
14701
- const periodButton = await rootLocator.locatorFor('.tn-calendar-period-button')();
14702
- const currentLabel = (await periodButton.text()).trim();
14703
- if (currentLabel === targetLabel) {
15580
+ const monthsFrom = (from) => {
15581
+ return (date.getFullYear() * 12 + date.getMonth()) - (from.year * 12 + from.month);
15582
+ };
15583
+ // A generous bound on the paging loop: a decade either way, and far more than the
15584
+ // handful of clicks any real navigation needs.
15585
+ for (let step = 0; step < 240; step++) {
15586
+ const shown = await displayedMonth(rootLocator);
15587
+ const delta = monthsFrom(shown);
15588
+ if (delta === 0) {
14704
15589
  return;
14705
15590
  }
14706
- const currentDate = parseCalendarLabel(currentLabel);
14707
- const targetTime = date.getFullYear() * 12 + date.getMonth();
14708
- const currentTime = currentDate.year * 12 + currentDate.month;
14709
- const navSelector = targetTime > currentTime
14710
- ? '.tn-calendar-next-button'
14711
- : '.tn-calendar-previous-button';
14712
- const navButton = await rootLocator.locatorFor(navSelector)();
14713
- await navButton.click();
15591
+ const button = await rootLocator.locatorFor(delta > 0 ? '.tn-calendar-next-button' : '.tn-calendar-previous-button')();
15592
+ await button.click();
14714
15593
  }
14715
- throw new Error(`Could not navigate calendar to ${targetLabel}`);
15594
+ throw new Error(`Could not navigate the calendar to ${isoDateString(date)}`);
14716
15595
  }
14717
- function parseCalendarLabel(label) {
14718
- const parts = label.split(' ');
14719
- const monthIndex = MONTHS.indexOf(parts[0]);
14720
- const year = parseInt(parts[1], 10);
14721
- return { year, month: monthIndex >= 0 ? monthIndex : 0 };
15596
+ /** The month on screen, read off the first day cell the grid rendered. */
15597
+ async function displayedMonth(rootLocator) {
15598
+ const cell = await rootLocator.locatorForOptional('.tn-calendar-body-cell[data-tn-date]')();
15599
+ const stamp = await cell?.getAttribute('data-tn-date');
15600
+ if (!stamp) {
15601
+ throw new Error('Could not read the displayed month: the calendar rendered no day cells.');
15602
+ }
15603
+ const [year, month] = stamp.split('-').map(Number);
15604
+ return { year, month: month - 1 };
14722
15605
  }
14723
15606
 
14724
15607
  /**
@@ -14850,9 +15733,7 @@ class TnDateRangeInputComponent {
14850
15733
  endDayRef = viewChild.required('endDayInput');
14851
15734
  endYearRef = viewChild.required('endYearInput');
14852
15735
  calendarTemplate = viewChild.required('calendarTemplate');
14853
- calendar = viewChild.required(TnCalendarComponent);
14854
15736
  wrapperEl = viewChild.required('wrapper');
14855
- destroy$ = new Subject();
14856
15737
  overlayRef;
14857
15738
  portal;
14858
15739
  isOpen = signal(false, ...(ngDevMode ? [{ debugName: "isOpen" }] : []));
@@ -14867,19 +15748,14 @@ class TnDateRangeInputComponent {
14867
15748
  endDay = signal('', ...(ngDevMode ? [{ debugName: "endDay" }] : []));
14868
15749
  endYear = signal('', ...(ngDevMode ? [{ debugName: "endYear" }] : []));
14869
15750
  currentFocus = 'start';
14870
- // Always provide current range to calendar for initial display
14871
- initialRange = computed(() => {
14872
- return this.value();
14873
- }, ...(ngDevMode ? [{ debugName: "initialRange" }] : []));
14874
15751
  overlay = inject(Overlay);
14875
15752
  viewContainerRef = inject(ViewContainerRef);
15753
+ injector = inject(Injector);
14876
15754
  ngOnInit() {
14877
15755
  // Initialize display values
14878
15756
  this.updateDisplayValues();
14879
15757
  }
14880
15758
  ngOnDestroy() {
14881
- this.destroy$.next();
14882
- this.destroy$.complete();
14883
15759
  this.close();
14884
15760
  }
14885
15761
  // ControlValueAccessor implementation
@@ -14947,8 +15823,9 @@ class TnDateRangeInputComponent {
14947
15823
  if (this.endYearRef()?.nativeElement) {
14948
15824
  this.endYearRef().nativeElement.value = '';
14949
15825
  }
14950
- // Focus end month for next selection
14951
- setTimeout(() => this.endMonthRef()?.nativeElement?.focus(), 0);
15826
+ // Move to the end date once the cleared segments have been painted. Tied to the
15827
+ // render rather than to a zero timeout, as the calendar's own focus handling is.
15828
+ afterNextRender(() => this.endMonthRef()?.nativeElement?.focus(), { injector: this.injector });
14952
15829
  }
14953
15830
  else if (range.start && range.end) {
14954
15831
  // Both dates selected - close calendar
@@ -15108,11 +15985,8 @@ class TnDateRangeInputComponent {
15108
15985
  }
15109
15986
  this.createOverlay();
15110
15987
  this.isOpen.set(true);
15111
- // Reset calendar interaction state when opening
15112
- const cal = this.calendar();
15113
- if (cal) {
15114
- setTimeout(() => cal.resetInteractionState(), 0);
15115
- }
15988
+ // No calendar state to reset — the calendar renders whatever range this component
15989
+ // binds to it, so reopening already shows the current value.
15116
15990
  }
15117
15991
  close() {
15118
15992
  if (this.overlayRef) {
@@ -15169,6 +16043,8 @@ class TnDateRangeInputComponent {
15169
16043
  panelClass: 'tn-datepicker-overlay'
15170
16044
  });
15171
16045
  // Close datepicker when backdrop is clicked
16046
+ // No teardown to track: `dispose()` in close() ends this subscription with the
16047
+ // overlay, and ngOnDestroy closes.
15172
16048
  this.overlayRef.backdropClick().subscribe(() => {
15173
16049
  this.close();
15174
16050
  });
@@ -15182,7 +16058,7 @@ class TnDateRangeInputComponent {
15182
16058
  useExisting: forwardRef(() => TnDateRangeInputComponent),
15183
16059
  multi: true
15184
16060
  }
15185
- ], viewQueries: [{ propertyName: "startMonthRef", first: true, predicate: ["startMonthInput"], descendants: true, isSignal: true }, { propertyName: "startDayRef", first: true, predicate: ["startDayInput"], descendants: true, isSignal: true }, { propertyName: "startYearRef", first: true, predicate: ["startYearInput"], descendants: true, isSignal: true }, { propertyName: "endMonthRef", first: true, predicate: ["endMonthInput"], descendants: true, isSignal: true }, { propertyName: "endDayRef", first: true, predicate: ["endDayInput"], descendants: true, isSignal: true }, { propertyName: "endYearRef", first: true, predicate: ["endYearInput"], descendants: true, isSignal: true }, { propertyName: "calendarTemplate", first: true, predicate: ["calendarTemplate"], descendants: true, isSignal: true }, { propertyName: "calendar", first: true, predicate: TnCalendarComponent, descendants: true, isSignal: true }, { propertyName: "wrapperEl", first: true, predicate: ["wrapper"], descendants: true, isSignal: true }], ngImport: i0, template: "<div class=\"tn-date-range-container\" tnTestIdType=\"date-range\" [tnTestId]=\"testId()\">\n <div #wrapper ixInput class=\"tn-date-range-wrapper\" style=\"padding-right: 40px;\">\n <!-- Start date segments -->\n <div class=\"tn-date-segment-group\">\n <input\n #startMonthInput\n type=\"text\"\n class=\"tn-date-segment tn-date-segment-month\"\n placeholder=\"MM\"\n maxlength=\"2\"\n [disabled]=\"isDisabled()\"\n (focus)=\"onSegmentFocus('start', 'month')\"\n (blur)=\"onSegmentBlur('start', 'month')\"\n (keydown)=\"onSegmentKeydown($event, 'start', 'month')\">\n <span class=\"tn-date-segment-separator\">/</span>\n <input\n #startDayInput\n type=\"text\"\n class=\"tn-date-segment tn-date-segment-day\"\n placeholder=\"DD\"\n maxlength=\"2\"\n [disabled]=\"isDisabled()\"\n (focus)=\"onSegmentFocus('start', 'day')\"\n (blur)=\"onSegmentBlur('start', 'day')\"\n (keydown)=\"onSegmentKeydown($event, 'start', 'day')\">\n <span class=\"tn-date-segment-separator\">/</span>\n <input\n #startYearInput\n type=\"text\"\n class=\"tn-date-segment tn-date-segment-year\"\n placeholder=\"YYYY\"\n maxlength=\"4\"\n [disabled]=\"isDisabled()\"\n (focus)=\"onSegmentFocus('start', 'year')\"\n (blur)=\"onSegmentBlur('start', 'year')\"\n (keydown)=\"onSegmentKeydown($event, 'start', 'year')\">\n </div>\n\n <span class=\"tn-date-range-separator\">\u2013</span>\n\n <!-- End date segments -->\n <div class=\"tn-date-segment-group\">\n <input\n #endMonthInput\n type=\"text\"\n class=\"tn-date-segment tn-date-segment-month\"\n placeholder=\"MM\"\n maxlength=\"2\"\n [disabled]=\"isDisabled()\"\n (focus)=\"onSegmentFocus('end', 'month')\"\n (blur)=\"onSegmentBlur('end', 'month')\"\n (keydown)=\"onSegmentKeydown($event, 'end', 'month')\">\n <span class=\"tn-date-segment-separator\">/</span>\n <input\n #endDayInput\n type=\"text\"\n class=\"tn-date-segment tn-date-segment-day\"\n placeholder=\"DD\"\n maxlength=\"2\"\n [disabled]=\"isDisabled()\"\n (focus)=\"onSegmentFocus('end', 'day')\"\n (blur)=\"onSegmentBlur('end', 'day')\"\n (keydown)=\"onSegmentKeydown($event, 'end', 'day')\">\n <span class=\"tn-date-segment-separator\">/</span>\n <input\n #endYearInput\n type=\"text\"\n class=\"tn-date-segment tn-date-segment-year\"\n placeholder=\"YYYY\"\n maxlength=\"4\"\n [disabled]=\"isDisabled()\"\n (focus)=\"onSegmentFocus('end', 'year')\"\n (blur)=\"onSegmentBlur('end', 'year')\"\n (keydown)=\"onSegmentKeydown($event, 'end', 'year')\">\n </div>\n\n <button\n type=\"button\"\n class=\"tn-date-range-toggle\"\n aria-label=\"Open calendar\"\n [disabled]=\"isDisabled()\"\n (click)=\"openDatepicker()\">\n <span aria-hidden=\"true\">\uD83D\uDCC5</span>\n </button>\n </div>\n\n <ng-template #calendarTemplate>\n <tn-calendar\n class=\"tn-calendar\"\n [startView]=\"'month'\"\n [rangeMode]=\"true\"\n [selectedRange]=\"initialRange()\"\n (selectedRangeChange)=\"onRangeSelected($event)\" />\n </ng-template>\n</div>\n", styles: [":host{display:block;width:100%}.tn-date-range-container{position:relative;display:flex;align-items:center}.tn-date-range-wrapper{display:flex;align-items:center;width:100%;position:relative}.tn-date-segment-group{display:flex;align-items:center}.tn-date-segment{background:transparent;border:none;outline:none;font:inherit;color:inherit;padding:0;min-width:0;text-align:center;width:2.6ch}.tn-date-segment::placeholder{color:var(--tn-alt-fg1, #999);opacity:1}.tn-date-segment:focus{outline:none;background:var(--tn-bg2, rgba(0, 0, 0, .05));border-radius:2px}.tn-date-segment:focus::placeholder{opacity:0}.tn-date-segment.tn-date-segment-year{width:4ch}.tn-date-segment-separator{padding:0 2px;-webkit-user-select:none;user-select:none;color:var(--tn-alt-fg1, #999)}.tn-date-range-separator{padding:0 .25em;-webkit-user-select:none;user-select:none;color:var(--tn-fg2, #666);flex-shrink:0}.tn-date-range-toggle{position:absolute;right:8px;z-index:2;pointer-events:auto;background:transparent;border:none;cursor:pointer;padding:4px;font-size:1rem}.tn-date-range-toggle:hover{background:var(--tn-bg2, #f0f0f0);border-radius:4px}.tn-date-range-toggle:disabled{cursor:not-allowed;opacity:.5}:host ::ng-deep .tn-datepicker-overlay .tn-calendar{background:var(--tn-bg1, white);border:1px solid var(--tn-lines, #e0e0e0);border-radius:8px;box-shadow:0 4px 12px #00000026;padding:24px;min-width:380px;--calendar-cell-size: 48px;--calendar-header-height: 44px;--calendar-cell-font-size: 1rem;--calendar-header-font-size: 14px}:host ::ng-deep .tn-datepicker-overlay .tn-calendar .tn-calendar-content{padding:0}\n"], dependencies: [{ kind: "component", type: TnCalendarComponent, selector: "tn-calendar", inputs: ["startView", "selected", "minDate", "maxDate", "dateFilter", "rangeMode", "selectedRange"], outputs: ["selectedChange", "activeDateChange", "viewChanged", "selectedRangeChange"] }, { kind: "ngmodule", type: OverlayModule }, { kind: "ngmodule", type: PortalModule }, { kind: "ngmodule", type: A11yModule }, { kind: "directive", type: TnTestIdDirective, selector: "[tnTestId]", inputs: ["tnTestId", "tnTestIdType"] }] });
16061
+ ], viewQueries: [{ propertyName: "startMonthRef", first: true, predicate: ["startMonthInput"], descendants: true, isSignal: true }, { propertyName: "startDayRef", first: true, predicate: ["startDayInput"], descendants: true, isSignal: true }, { propertyName: "startYearRef", first: true, predicate: ["startYearInput"], descendants: true, isSignal: true }, { propertyName: "endMonthRef", first: true, predicate: ["endMonthInput"], descendants: true, isSignal: true }, { propertyName: "endDayRef", first: true, predicate: ["endDayInput"], descendants: true, isSignal: true }, { propertyName: "endYearRef", first: true, predicate: ["endYearInput"], descendants: true, isSignal: true }, { propertyName: "calendarTemplate", first: true, predicate: ["calendarTemplate"], descendants: true, isSignal: true }, { propertyName: "wrapperEl", first: true, predicate: ["wrapper"], descendants: true, isSignal: true }], ngImport: i0, template: "<div class=\"tn-date-range-container\" tnTestIdType=\"date-range\" [tnTestId]=\"testId()\">\n <div #wrapper ixInput class=\"tn-date-range-wrapper\" style=\"padding-right: 40px;\">\n <!-- Start date segments -->\n <div class=\"tn-date-segment-group\">\n <input\n #startMonthInput\n type=\"text\"\n class=\"tn-date-segment tn-date-segment-month\"\n placeholder=\"MM\"\n maxlength=\"2\"\n [disabled]=\"isDisabled()\"\n (focus)=\"onSegmentFocus('start', 'month')\"\n (blur)=\"onSegmentBlur('start', 'month')\"\n (keydown)=\"onSegmentKeydown($event, 'start', 'month')\">\n <span class=\"tn-date-segment-separator\">/</span>\n <input\n #startDayInput\n type=\"text\"\n class=\"tn-date-segment tn-date-segment-day\"\n placeholder=\"DD\"\n maxlength=\"2\"\n [disabled]=\"isDisabled()\"\n (focus)=\"onSegmentFocus('start', 'day')\"\n (blur)=\"onSegmentBlur('start', 'day')\"\n (keydown)=\"onSegmentKeydown($event, 'start', 'day')\">\n <span class=\"tn-date-segment-separator\">/</span>\n <input\n #startYearInput\n type=\"text\"\n class=\"tn-date-segment tn-date-segment-year\"\n placeholder=\"YYYY\"\n maxlength=\"4\"\n [disabled]=\"isDisabled()\"\n (focus)=\"onSegmentFocus('start', 'year')\"\n (blur)=\"onSegmentBlur('start', 'year')\"\n (keydown)=\"onSegmentKeydown($event, 'start', 'year')\">\n </div>\n\n <span class=\"tn-date-range-separator\">\u2013</span>\n\n <!-- End date segments -->\n <div class=\"tn-date-segment-group\">\n <input\n #endMonthInput\n type=\"text\"\n class=\"tn-date-segment tn-date-segment-month\"\n placeholder=\"MM\"\n maxlength=\"2\"\n [disabled]=\"isDisabled()\"\n (focus)=\"onSegmentFocus('end', 'month')\"\n (blur)=\"onSegmentBlur('end', 'month')\"\n (keydown)=\"onSegmentKeydown($event, 'end', 'month')\">\n <span class=\"tn-date-segment-separator\">/</span>\n <input\n #endDayInput\n type=\"text\"\n class=\"tn-date-segment tn-date-segment-day\"\n placeholder=\"DD\"\n maxlength=\"2\"\n [disabled]=\"isDisabled()\"\n (focus)=\"onSegmentFocus('end', 'day')\"\n (blur)=\"onSegmentBlur('end', 'day')\"\n (keydown)=\"onSegmentKeydown($event, 'end', 'day')\">\n <span class=\"tn-date-segment-separator\">/</span>\n <input\n #endYearInput\n type=\"text\"\n class=\"tn-date-segment tn-date-segment-year\"\n placeholder=\"YYYY\"\n maxlength=\"4\"\n [disabled]=\"isDisabled()\"\n (focus)=\"onSegmentFocus('end', 'year')\"\n (blur)=\"onSegmentBlur('end', 'year')\"\n (keydown)=\"onSegmentKeydown($event, 'end', 'year')\">\n </div>\n\n <button\n type=\"button\"\n class=\"tn-date-range-toggle\"\n aria-label=\"Open calendar\"\n [disabled]=\"isDisabled()\"\n (click)=\"openDatepicker()\">\n <span aria-hidden=\"true\">\uD83D\uDCC5</span>\n </button>\n </div>\n\n <ng-template #calendarTemplate>\n <tn-calendar\n class=\"tn-calendar\"\n [startView]=\"'month'\"\n [rangeMode]=\"true\"\n [selectedRange]=\"value()\"\n (selectedRangeChange)=\"onRangeSelected($event)\" />\n </ng-template>\n</div>\n", styles: [":host{display:block;width:100%}.tn-date-range-container{position:relative;display:flex;align-items:center}.tn-date-range-wrapper{display:flex;align-items:center;width:100%;position:relative}.tn-date-segment-group{display:flex;align-items:center}.tn-date-segment{background:transparent;border:none;outline:none;font:inherit;color:inherit;padding:0;min-width:0;text-align:center;width:2.6ch}.tn-date-segment::placeholder{color:var(--tn-alt-fg1, #999);opacity:1}.tn-date-segment:focus{outline:none;background:var(--tn-bg2, rgba(0, 0, 0, .05));border-radius:2px}.tn-date-segment:focus::placeholder{opacity:0}.tn-date-segment.tn-date-segment-year{width:4ch}.tn-date-segment-separator{padding:0 2px;-webkit-user-select:none;user-select:none;color:var(--tn-alt-fg1, #999)}.tn-date-range-separator{padding:0 .25em;-webkit-user-select:none;user-select:none;color:var(--tn-fg2, #666);flex-shrink:0}.tn-date-range-toggle{position:absolute;right:8px;z-index:2;pointer-events:auto;background:transparent;border:none;cursor:pointer;padding:4px;font-size:1rem}.tn-date-range-toggle:hover{background:var(--tn-bg2, #f0f0f0);border-radius:4px}.tn-date-range-toggle:disabled{cursor:not-allowed;opacity:.5}:host ::ng-deep .tn-datepicker-overlay .tn-calendar{background:var(--tn-bg1, white);border:1px solid var(--tn-lines, #e0e0e0);border-radius:8px;box-shadow:0 4px 12px #00000026;padding:24px;min-width:380px;--calendar-cell-size: 48px;--calendar-header-height: 44px;--calendar-cell-font-size: 1rem;--calendar-header-font-size: 14px}:host ::ng-deep .tn-datepicker-overlay .tn-calendar .tn-calendar-content{padding:0}\n"], dependencies: [{ kind: "component", type: TnCalendarComponent, selector: "tn-calendar", inputs: ["startView", "selected", "minDate", "maxDate", "dateFilter", "markedDates", "activeDate", "locale", "rangeMode", "selectedRange"], outputs: ["selectedChange", "activeDateChange", "viewChanged", "selectedRangeChange"] }, { kind: "ngmodule", type: OverlayModule }, { kind: "ngmodule", type: PortalModule }, { kind: "ngmodule", type: A11yModule }, { kind: "directive", type: TnTestIdDirective, selector: "[tnTestId]", inputs: ["tnTestId", "tnTestIdType"] }] });
15186
16062
  }
15187
16063
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnDateRangeInputComponent, decorators: [{
15188
16064
  type: Component,
@@ -15194,8 +16070,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
15194
16070
  }
15195
16071
  ], host: {
15196
16072
  'class': 'tn-date-range-input'
15197
- }, template: "<div class=\"tn-date-range-container\" tnTestIdType=\"date-range\" [tnTestId]=\"testId()\">\n <div #wrapper ixInput class=\"tn-date-range-wrapper\" style=\"padding-right: 40px;\">\n <!-- Start date segments -->\n <div class=\"tn-date-segment-group\">\n <input\n #startMonthInput\n type=\"text\"\n class=\"tn-date-segment tn-date-segment-month\"\n placeholder=\"MM\"\n maxlength=\"2\"\n [disabled]=\"isDisabled()\"\n (focus)=\"onSegmentFocus('start', 'month')\"\n (blur)=\"onSegmentBlur('start', 'month')\"\n (keydown)=\"onSegmentKeydown($event, 'start', 'month')\">\n <span class=\"tn-date-segment-separator\">/</span>\n <input\n #startDayInput\n type=\"text\"\n class=\"tn-date-segment tn-date-segment-day\"\n placeholder=\"DD\"\n maxlength=\"2\"\n [disabled]=\"isDisabled()\"\n (focus)=\"onSegmentFocus('start', 'day')\"\n (blur)=\"onSegmentBlur('start', 'day')\"\n (keydown)=\"onSegmentKeydown($event, 'start', 'day')\">\n <span class=\"tn-date-segment-separator\">/</span>\n <input\n #startYearInput\n type=\"text\"\n class=\"tn-date-segment tn-date-segment-year\"\n placeholder=\"YYYY\"\n maxlength=\"4\"\n [disabled]=\"isDisabled()\"\n (focus)=\"onSegmentFocus('start', 'year')\"\n (blur)=\"onSegmentBlur('start', 'year')\"\n (keydown)=\"onSegmentKeydown($event, 'start', 'year')\">\n </div>\n\n <span class=\"tn-date-range-separator\">\u2013</span>\n\n <!-- End date segments -->\n <div class=\"tn-date-segment-group\">\n <input\n #endMonthInput\n type=\"text\"\n class=\"tn-date-segment tn-date-segment-month\"\n placeholder=\"MM\"\n maxlength=\"2\"\n [disabled]=\"isDisabled()\"\n (focus)=\"onSegmentFocus('end', 'month')\"\n (blur)=\"onSegmentBlur('end', 'month')\"\n (keydown)=\"onSegmentKeydown($event, 'end', 'month')\">\n <span class=\"tn-date-segment-separator\">/</span>\n <input\n #endDayInput\n type=\"text\"\n class=\"tn-date-segment tn-date-segment-day\"\n placeholder=\"DD\"\n maxlength=\"2\"\n [disabled]=\"isDisabled()\"\n (focus)=\"onSegmentFocus('end', 'day')\"\n (blur)=\"onSegmentBlur('end', 'day')\"\n (keydown)=\"onSegmentKeydown($event, 'end', 'day')\">\n <span class=\"tn-date-segment-separator\">/</span>\n <input\n #endYearInput\n type=\"text\"\n class=\"tn-date-segment tn-date-segment-year\"\n placeholder=\"YYYY\"\n maxlength=\"4\"\n [disabled]=\"isDisabled()\"\n (focus)=\"onSegmentFocus('end', 'year')\"\n (blur)=\"onSegmentBlur('end', 'year')\"\n (keydown)=\"onSegmentKeydown($event, 'end', 'year')\">\n </div>\n\n <button\n type=\"button\"\n class=\"tn-date-range-toggle\"\n aria-label=\"Open calendar\"\n [disabled]=\"isDisabled()\"\n (click)=\"openDatepicker()\">\n <span aria-hidden=\"true\">\uD83D\uDCC5</span>\n </button>\n </div>\n\n <ng-template #calendarTemplate>\n <tn-calendar\n class=\"tn-calendar\"\n [startView]=\"'month'\"\n [rangeMode]=\"true\"\n [selectedRange]=\"initialRange()\"\n (selectedRangeChange)=\"onRangeSelected($event)\" />\n </ng-template>\n</div>\n", styles: [":host{display:block;width:100%}.tn-date-range-container{position:relative;display:flex;align-items:center}.tn-date-range-wrapper{display:flex;align-items:center;width:100%;position:relative}.tn-date-segment-group{display:flex;align-items:center}.tn-date-segment{background:transparent;border:none;outline:none;font:inherit;color:inherit;padding:0;min-width:0;text-align:center;width:2.6ch}.tn-date-segment::placeholder{color:var(--tn-alt-fg1, #999);opacity:1}.tn-date-segment:focus{outline:none;background:var(--tn-bg2, rgba(0, 0, 0, .05));border-radius:2px}.tn-date-segment:focus::placeholder{opacity:0}.tn-date-segment.tn-date-segment-year{width:4ch}.tn-date-segment-separator{padding:0 2px;-webkit-user-select:none;user-select:none;color:var(--tn-alt-fg1, #999)}.tn-date-range-separator{padding:0 .25em;-webkit-user-select:none;user-select:none;color:var(--tn-fg2, #666);flex-shrink:0}.tn-date-range-toggle{position:absolute;right:8px;z-index:2;pointer-events:auto;background:transparent;border:none;cursor:pointer;padding:4px;font-size:1rem}.tn-date-range-toggle:hover{background:var(--tn-bg2, #f0f0f0);border-radius:4px}.tn-date-range-toggle:disabled{cursor:not-allowed;opacity:.5}:host ::ng-deep .tn-datepicker-overlay .tn-calendar{background:var(--tn-bg1, white);border:1px solid var(--tn-lines, #e0e0e0);border-radius:8px;box-shadow:0 4px 12px #00000026;padding:24px;min-width:380px;--calendar-cell-size: 48px;--calendar-header-height: 44px;--calendar-cell-font-size: 1rem;--calendar-header-font-size: 14px}:host ::ng-deep .tn-datepicker-overlay .tn-calendar .tn-calendar-content{padding:0}\n"] }]
15198
- }], propDecorators: { disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], testId: [{ type: i0.Input, args: [{ isSignal: true, alias: "testId", required: false }] }], startMonthRef: [{ type: i0.ViewChild, args: ['startMonthInput', { isSignal: true }] }], startDayRef: [{ type: i0.ViewChild, args: ['startDayInput', { isSignal: true }] }], startYearRef: [{ type: i0.ViewChild, args: ['startYearInput', { isSignal: true }] }], endMonthRef: [{ type: i0.ViewChild, args: ['endMonthInput', { isSignal: true }] }], endDayRef: [{ type: i0.ViewChild, args: ['endDayInput', { isSignal: true }] }], endYearRef: [{ type: i0.ViewChild, args: ['endYearInput', { isSignal: true }] }], calendarTemplate: [{ type: i0.ViewChild, args: ['calendarTemplate', { isSignal: true }] }], calendar: [{ type: i0.ViewChild, args: [i0.forwardRef(() => TnCalendarComponent), { isSignal: true }] }], wrapperEl: [{ type: i0.ViewChild, args: ['wrapper', { isSignal: true }] }] } });
16073
+ }, template: "<div class=\"tn-date-range-container\" tnTestIdType=\"date-range\" [tnTestId]=\"testId()\">\n <div #wrapper ixInput class=\"tn-date-range-wrapper\" style=\"padding-right: 40px;\">\n <!-- Start date segments -->\n <div class=\"tn-date-segment-group\">\n <input\n #startMonthInput\n type=\"text\"\n class=\"tn-date-segment tn-date-segment-month\"\n placeholder=\"MM\"\n maxlength=\"2\"\n [disabled]=\"isDisabled()\"\n (focus)=\"onSegmentFocus('start', 'month')\"\n (blur)=\"onSegmentBlur('start', 'month')\"\n (keydown)=\"onSegmentKeydown($event, 'start', 'month')\">\n <span class=\"tn-date-segment-separator\">/</span>\n <input\n #startDayInput\n type=\"text\"\n class=\"tn-date-segment tn-date-segment-day\"\n placeholder=\"DD\"\n maxlength=\"2\"\n [disabled]=\"isDisabled()\"\n (focus)=\"onSegmentFocus('start', 'day')\"\n (blur)=\"onSegmentBlur('start', 'day')\"\n (keydown)=\"onSegmentKeydown($event, 'start', 'day')\">\n <span class=\"tn-date-segment-separator\">/</span>\n <input\n #startYearInput\n type=\"text\"\n class=\"tn-date-segment tn-date-segment-year\"\n placeholder=\"YYYY\"\n maxlength=\"4\"\n [disabled]=\"isDisabled()\"\n (focus)=\"onSegmentFocus('start', 'year')\"\n (blur)=\"onSegmentBlur('start', 'year')\"\n (keydown)=\"onSegmentKeydown($event, 'start', 'year')\">\n </div>\n\n <span class=\"tn-date-range-separator\">\u2013</span>\n\n <!-- End date segments -->\n <div class=\"tn-date-segment-group\">\n <input\n #endMonthInput\n type=\"text\"\n class=\"tn-date-segment tn-date-segment-month\"\n placeholder=\"MM\"\n maxlength=\"2\"\n [disabled]=\"isDisabled()\"\n (focus)=\"onSegmentFocus('end', 'month')\"\n (blur)=\"onSegmentBlur('end', 'month')\"\n (keydown)=\"onSegmentKeydown($event, 'end', 'month')\">\n <span class=\"tn-date-segment-separator\">/</span>\n <input\n #endDayInput\n type=\"text\"\n class=\"tn-date-segment tn-date-segment-day\"\n placeholder=\"DD\"\n maxlength=\"2\"\n [disabled]=\"isDisabled()\"\n (focus)=\"onSegmentFocus('end', 'day')\"\n (blur)=\"onSegmentBlur('end', 'day')\"\n (keydown)=\"onSegmentKeydown($event, 'end', 'day')\">\n <span class=\"tn-date-segment-separator\">/</span>\n <input\n #endYearInput\n type=\"text\"\n class=\"tn-date-segment tn-date-segment-year\"\n placeholder=\"YYYY\"\n maxlength=\"4\"\n [disabled]=\"isDisabled()\"\n (focus)=\"onSegmentFocus('end', 'year')\"\n (blur)=\"onSegmentBlur('end', 'year')\"\n (keydown)=\"onSegmentKeydown($event, 'end', 'year')\">\n </div>\n\n <button\n type=\"button\"\n class=\"tn-date-range-toggle\"\n aria-label=\"Open calendar\"\n [disabled]=\"isDisabled()\"\n (click)=\"openDatepicker()\">\n <span aria-hidden=\"true\">\uD83D\uDCC5</span>\n </button>\n </div>\n\n <ng-template #calendarTemplate>\n <tn-calendar\n class=\"tn-calendar\"\n [startView]=\"'month'\"\n [rangeMode]=\"true\"\n [selectedRange]=\"value()\"\n (selectedRangeChange)=\"onRangeSelected($event)\" />\n </ng-template>\n</div>\n", styles: [":host{display:block;width:100%}.tn-date-range-container{position:relative;display:flex;align-items:center}.tn-date-range-wrapper{display:flex;align-items:center;width:100%;position:relative}.tn-date-segment-group{display:flex;align-items:center}.tn-date-segment{background:transparent;border:none;outline:none;font:inherit;color:inherit;padding:0;min-width:0;text-align:center;width:2.6ch}.tn-date-segment::placeholder{color:var(--tn-alt-fg1, #999);opacity:1}.tn-date-segment:focus{outline:none;background:var(--tn-bg2, rgba(0, 0, 0, .05));border-radius:2px}.tn-date-segment:focus::placeholder{opacity:0}.tn-date-segment.tn-date-segment-year{width:4ch}.tn-date-segment-separator{padding:0 2px;-webkit-user-select:none;user-select:none;color:var(--tn-alt-fg1, #999)}.tn-date-range-separator{padding:0 .25em;-webkit-user-select:none;user-select:none;color:var(--tn-fg2, #666);flex-shrink:0}.tn-date-range-toggle{position:absolute;right:8px;z-index:2;pointer-events:auto;background:transparent;border:none;cursor:pointer;padding:4px;font-size:1rem}.tn-date-range-toggle:hover{background:var(--tn-bg2, #f0f0f0);border-radius:4px}.tn-date-range-toggle:disabled{cursor:not-allowed;opacity:.5}:host ::ng-deep .tn-datepicker-overlay .tn-calendar{background:var(--tn-bg1, white);border:1px solid var(--tn-lines, #e0e0e0);border-radius:8px;box-shadow:0 4px 12px #00000026;padding:24px;min-width:380px;--calendar-cell-size: 48px;--calendar-header-height: 44px;--calendar-cell-font-size: 1rem;--calendar-header-font-size: 14px}:host ::ng-deep .tn-datepicker-overlay .tn-calendar .tn-calendar-content{padding:0}\n"] }]
16074
+ }], propDecorators: { disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], testId: [{ type: i0.Input, args: [{ isSignal: true, alias: "testId", required: false }] }], startMonthRef: [{ type: i0.ViewChild, args: ['startMonthInput', { isSignal: true }] }], startDayRef: [{ type: i0.ViewChild, args: ['startDayInput', { isSignal: true }] }], startYearRef: [{ type: i0.ViewChild, args: ['startYearInput', { isSignal: true }] }], endMonthRef: [{ type: i0.ViewChild, args: ['endMonthInput', { isSignal: true }] }], endDayRef: [{ type: i0.ViewChild, args: ['endDayInput', { isSignal: true }] }], endYearRef: [{ type: i0.ViewChild, args: ['endYearInput', { isSignal: true }] }], calendarTemplate: [{ type: i0.ViewChild, args: ['calendarTemplate', { isSignal: true }] }], wrapperEl: [{ type: i0.ViewChild, args: ['wrapper', { isSignal: true }] }] } });
15199
16075
 
15200
16076
  /**
15201
16077
  * Harness for interacting with `tn-date-range-input` in tests.
@@ -16619,7 +17495,7 @@ class TnConfirmDialogComponent {
16619
17495
  ref = inject((DialogRef));
16620
17496
  data = inject(DIALOG_DATA);
16621
17497
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnConfirmDialogComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
16622
- 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"] }] });
17498
+ 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", "fullWidth", "type", "testId", "href", "routerLink", "queryParams", "fragment", "target", "rel", "ariaLabel"], outputs: ["onClick"] }] });
16623
17499
  }
16624
17500
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnConfirmDialogComponent, decorators: [{
16625
17501
  type: Component,
@@ -17937,7 +18813,7 @@ class TnFilePickerPopupComponent {
17937
18813
  return `${(date.getMonth() + 1).toString().padStart(2, '0')}/${date.getDate().toString().padStart(2, '0')}/${date.getFullYear()} ${timePart}`;
17938
18814
  }
17939
18815
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnFilePickerPopupComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
17940
- 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 }, createActions: { classPropertyName: "createActions", publicName: "createActions", isSignal: true, isRequired: false, transformFunction: null }, currentPath: { classPropertyName: "currentPath", publicName: "currentPath", isSignal: true, isRequired: false, transformFunction: null }, rootPath: { classPropertyName: "rootPath", publicName: "rootPath", 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 }, fileExtensions: { classPropertyName: "fileExtensions", publicName: "fileExtensions", isSignal: true, isRequired: false, transformFunction: null }, testIdBase: { classPropertyName: "testIdBase", publicName: "testIdBase", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { itemClick: "itemClick", itemDoubleClick: "itemDoubleClick", pathNavigate: "pathNavigate", createAction: "createAction", created: "created", clearSelection: "clearSelection", close: "close", submit: "submit", cancel: "cancel" }, 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 : rootPath(); track $index; let last = $last) {\n <button\n class=\"breadcrumb-segment\"\n tnTestIdType=\"button\"\n [tnTestId]=\"breadcrumbTestId(segment.name)\"\n [class.current]=\"last\"\n [class.nameless]=\"!segment.name\"\n [disabled]=\"last\"\n [attr.aria-label]=\"segment.name === '\u2026' || !segment.name ? 'Navigate to ' + segment.path : null\"\n (click)=\"navigateToPath(segment.path)\">\n {{ segment.name }}\n </button>\n }\n </nav>\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\n <tn-table\n [dataSource]=\"filteredFileItems()\"\n [displayedColumns]=\"multiSelect() ? displayedColumns : displayedColumns.slice(1)\"\n [clickable]=\"true\"\n [activeWhen]=\"isRowSelected\"\n (rowClick)=\"onItemClick($event)\"\n (rowDoubleClick)=\"onItemDoubleClick($event)\">\n\n <!-- Selection column: explicit width so the auto table layout doesn't\n hand the near-empty column a share of the spare width -->\n @if (multiSelect()) {\n <ng-container tnColumnDef=\"select\" width=\"20px\">\n <ng-template tnHeaderCellDef>\n <!-- Select all checkbox -->\n </ng-template>\n <ng-template let-item tnCellDef>\n @if (!item.inlineCreate) {\n <tn-checkbox\n [hideLabel]=\"true\"\n [label]=\"'Select ' + item.name\"\n [testId]=\"itemTestId(item)\"\n [checked]=\"isSelected(item)\"\n [disabled]=\"!!item.disabled\"\n (click)=\"$event.stopPropagation()\"\n (change)=\"onItemClick(item)\" />\n }\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 <!-- Inline creation row for create actions with a `create` callback -->\n @if (item.inlineCreate) {\n @if (inlineCreation(); as creation) {\n <div class=\"inline-create-cell\">\n <div class=\"inline-create-row\" [class.has-error]=\"!!creation.error\">\n <tn-icon class=\"file-icon\" [name]=\"creation.action.icon || 'mdi-folder'\" />\n <input\n type=\"text\"\n class=\"inline-create-input\"\n spellcheck=\"false\"\n autocomplete=\"off\"\n tnTestIdType=\"input\"\n [tnTestId]=\"inlineCreateTestId()\"\n [attr.aria-label]=\"creation.action.label + ' name'\"\n [attr.aria-invalid]=\"creation.error ? true : null\"\n [attr.aria-describedby]=\"creation.error ? inlineErrorId : null\"\n [class.error]=\"!!creation.error\"\n [disabled]=\"creation.loading\"\n [value]=\"creation.name\"\n (input)=\"onInlineCreationInput($event)\"\n (keydown)=\"onInlineCreationKeyDown($event)\"\n (blur)=\"onInlineCreationBlur()\">\n @if (creation.loading) {\n <tn-icon name=\"mdi-loading\" class=\"creation-loading-icon\" aria-hidden=\"true\" />\n }\n </div>\n @if (creation.error) {\n <div class=\"inline-create-error\" role=\"alert\" [id]=\"inlineErrorId\">\n <tn-icon name=\"mdi-alert-circle\" class=\"error-icon\" aria-hidden=\"true\" />\n <span class=\"error-text\">{{ creation.error }}</span>\n </div>\n }\n </div>\n }\n } @else {\n <div\n class=\"file-name-cell\"\n tnTestIdType=\"option\"\n [tnTestId]=\"itemTestId(item)\"\n [class.selected]=\"isSelected(item)\"\n [class.disabled]=\"!!item.dimmed\"\n [class.zfs-object]=\"isZfsObject(item)\">\n <tn-icon\n [name]=\"getItemIcon(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\n <!-- Navigation affordance: single click opens the directory -->\n @if (isNavigatable(item)) {\n <button\n type=\"button\"\n class=\"navigate-button\"\n tnTestIdType=\"button\"\n [tnTestId]=\"navigateTestId(item)\"\n [attr.aria-label]=\"'Open ' + item.name\"\n [attr.title]=\"'Open ' + item.name\"\n (click)=\"onNavigateClick($event, item)\"\n (dblclick)=\"$event.stopPropagation()\">\n <tn-icon name=\"mdi-chevron-right\" size=\"sm\" />\n </button>\n }\n </div>\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 <div class=\"footer-selection\">\n @if (selectedItems().length > 0) {\n <span class=\"selection-count\">\n {{ selectedItems().length }} item{{ selectedItems().length !== 1 ? 's' : '' }} selected\n </span>\n <tn-button\n variant=\"outline\"\n label=\"Clear Selection\"\n [testId]=\"clearSelectionTestId()\"\n (onClick)=\"onClearSelection()\" />\n } @else if (allowCurrentDirectorySelection()) {\n <span class=\"selection-count current-directory-selection\">\n Current directory selected\n </span>\n } @else {\n <span class=\"selection-count\">\n No items selected\n </span>\n }\n </div>\n <div class=\"footer-actions\">\n @for (action of createActions(); track action.id) {\n <tn-button\n variant=\"outline\"\n [label]=\"action.label\"\n [testId]=\"createActionTestId(action)\"\n [disabled]=\"!!inlineCreation()\"\n (onClick)=\"onCreateAction(action)\" />\n }\n <!-- Disabled while the inline creation row is open: its blur auto-submit\n would race the selection submit -->\n <tn-button\n label=\"Select\"\n [testId]=\"selectTestId()\"\n [disabled]=\"(selectedItems().length === 0 && !allowCurrentDirectorySelection()) || !!inlineCreation()\"\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);width:520px;max-width:calc(100vw - 32px);min-height:min(500px,100vh - 16px);max-height:min(600px,100vh - 16px);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;flex:1;min-width:0}.tn-file-picker-breadcrumb .breadcrumb-segment{background:transparent;border:none;color:var(--tn-primary);cursor:pointer;padding:4px 2px;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:2px;color:var(--tn-alt-fg1)}.tn-file-picker-breadcrumb .breadcrumb-segment:first-child:before{content:\"/\";margin-right:2px;color:var(--tn-alt-fg1)}.tn-file-picker-breadcrumb .breadcrumb-segment.nameless:not(:last-child):after{content:none}.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}.inline-create-cell{display:flex;flex-direction:column;gap:4px;flex:1;min-width:0}.inline-create-row{display:flex;align-items:center;gap:8px}.inline-create-row .inline-create-input{flex:1;padding:4px 8px;border:2px solid var(--tn-primary);border-radius:3px;background:var(--tn-bg1);color:var(--tn-fg1);font-size:inherit;font-family:inherit;outline:none;min-width:0}.inline-create-row .inline-create-input.error{border-color:var(--tn-error, #d32f2f)}.inline-create-row .inline-create-input:disabled{opacity:.6;cursor:not-allowed;background:var(--tn-bg2)}.inline-create-row .creation-loading-icon{animation:spin 1s linear infinite;color:var(--tn-primary);flex-shrink:0}.inline-create-error{display:flex;align-items:center;gap:6px;margin-top:4px;padding:4px 8px;background:color-mix(in srgb,var(--tn-error, #d32f2f) 10%,transparent);border-left:3px solid var(--tn-error, #d32f2f);border-radius:3px;font-size:1rem;color:var(--tn-error, #d32f2f)}.inline-create-error .error-icon{flex-shrink:0}.inline-create-error .error-text{flex:1}.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-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-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}.navigate-button{display:flex;align-items:center;justify-content:center;margin-left:auto;padding:2px;background:none;border:none;border-radius:4px;color:var(--tn-fg2);cursor:pointer}.navigate-button:hover,.navigate-button:focus-visible{background:var(--tn-bg3);color:var(--tn-primary)}.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;gap:16px;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}.tn-file-picker-footer .footer-selection{display:flex;align-items:center;gap:12px}.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: TnCheckboxComponent, selector: "tn-checkbox", inputs: ["label", "hideLabel", "disabled", "required", "indeterminate", "testId", "error", "checked"], outputs: ["change"] }, { kind: "component", type: TnTableComponent, selector: "tn-table", inputs: ["dataSource", "displayedColumns", "trackBy", "emptyMessage", "emptyIcon", "selectable", "expandable", "bordered", "isRowExpandable", "activeRow", "activeWhen", "activeBg", "activeIndicator", "loading", "loadingMessage", "clickable"], outputs: ["sortChange", "selectionChange", "rowClick", "rowDoubleClick"] }, { 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: "directive", type: TnTestIdDirective, selector: "[tnTestId]", inputs: ["tnTestId", "tnTestIdType"] }, { kind: "pipe", type: FileSizePipe, name: "tnFileSize" }, { kind: "pipe", type: TruncatePathPipe, name: "tnTruncatePath" }] });
18816
+ 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 }, createActions: { classPropertyName: "createActions", publicName: "createActions", isSignal: true, isRequired: false, transformFunction: null }, currentPath: { classPropertyName: "currentPath", publicName: "currentPath", isSignal: true, isRequired: false, transformFunction: null }, rootPath: { classPropertyName: "rootPath", publicName: "rootPath", 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 }, fileExtensions: { classPropertyName: "fileExtensions", publicName: "fileExtensions", isSignal: true, isRequired: false, transformFunction: null }, testIdBase: { classPropertyName: "testIdBase", publicName: "testIdBase", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { itemClick: "itemClick", itemDoubleClick: "itemDoubleClick", pathNavigate: "pathNavigate", createAction: "createAction", created: "created", clearSelection: "clearSelection", close: "close", submit: "submit", cancel: "cancel" }, 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 : rootPath(); track $index; let last = $last) {\n <button\n class=\"breadcrumb-segment\"\n tnTestIdType=\"button\"\n [tnTestId]=\"breadcrumbTestId(segment.name)\"\n [class.current]=\"last\"\n [class.nameless]=\"!segment.name\"\n [disabled]=\"last\"\n [attr.aria-label]=\"segment.name === '\u2026' || !segment.name ? 'Navigate to ' + segment.path : null\"\n (click)=\"navigateToPath(segment.path)\">\n {{ segment.name }}\n </button>\n }\n </nav>\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\n <tn-table\n [dataSource]=\"filteredFileItems()\"\n [displayedColumns]=\"multiSelect() ? displayedColumns : displayedColumns.slice(1)\"\n [clickable]=\"true\"\n [activeWhen]=\"isRowSelected\"\n (rowClick)=\"onItemClick($event)\"\n (rowDoubleClick)=\"onItemDoubleClick($event)\">\n\n <!-- Selection column: explicit width so the auto table layout doesn't\n hand the near-empty column a share of the spare width -->\n @if (multiSelect()) {\n <ng-container tnColumnDef=\"select\" width=\"20px\">\n <ng-template tnHeaderCellDef>\n <!-- Select all checkbox -->\n </ng-template>\n <ng-template let-item tnCellDef>\n @if (!item.inlineCreate) {\n <tn-checkbox\n [hideLabel]=\"true\"\n [label]=\"'Select ' + item.name\"\n [testId]=\"itemTestId(item)\"\n [checked]=\"isSelected(item)\"\n [disabled]=\"!!item.disabled\"\n (click)=\"$event.stopPropagation()\"\n (change)=\"onItemClick(item)\" />\n }\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 <!-- Inline creation row for create actions with a `create` callback -->\n @if (item.inlineCreate) {\n @if (inlineCreation(); as creation) {\n <div class=\"inline-create-cell\">\n <div class=\"inline-create-row\" [class.has-error]=\"!!creation.error\">\n <tn-icon class=\"file-icon\" [name]=\"creation.action.icon || 'mdi-folder'\" />\n <input\n type=\"text\"\n class=\"inline-create-input\"\n spellcheck=\"false\"\n autocomplete=\"off\"\n tnTestIdType=\"input\"\n [tnTestId]=\"inlineCreateTestId()\"\n [attr.aria-label]=\"creation.action.label + ' name'\"\n [attr.aria-invalid]=\"creation.error ? true : null\"\n [attr.aria-describedby]=\"creation.error ? inlineErrorId : null\"\n [class.error]=\"!!creation.error\"\n [disabled]=\"creation.loading\"\n [value]=\"creation.name\"\n (input)=\"onInlineCreationInput($event)\"\n (keydown)=\"onInlineCreationKeyDown($event)\"\n (blur)=\"onInlineCreationBlur()\">\n @if (creation.loading) {\n <tn-icon name=\"mdi-loading\" class=\"creation-loading-icon\" aria-hidden=\"true\" />\n }\n </div>\n @if (creation.error) {\n <div class=\"inline-create-error\" role=\"alert\" [id]=\"inlineErrorId\">\n <tn-icon name=\"mdi-alert-circle\" class=\"error-icon\" aria-hidden=\"true\" />\n <span class=\"error-text\">{{ creation.error }}</span>\n </div>\n }\n </div>\n }\n } @else {\n <div\n class=\"file-name-cell\"\n tnTestIdType=\"option\"\n [tnTestId]=\"itemTestId(item)\"\n [class.selected]=\"isSelected(item)\"\n [class.disabled]=\"!!item.dimmed\"\n [class.zfs-object]=\"isZfsObject(item)\">\n <tn-icon\n [name]=\"getItemIcon(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\n <!-- Navigation affordance: single click opens the directory -->\n @if (isNavigatable(item)) {\n <button\n type=\"button\"\n class=\"navigate-button\"\n tnTestIdType=\"button\"\n [tnTestId]=\"navigateTestId(item)\"\n [attr.aria-label]=\"'Open ' + item.name\"\n [attr.title]=\"'Open ' + item.name\"\n (click)=\"onNavigateClick($event, item)\"\n (dblclick)=\"$event.stopPropagation()\">\n <tn-icon name=\"mdi-chevron-right\" size=\"sm\" />\n </button>\n }\n </div>\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 <div class=\"footer-selection\">\n @if (selectedItems().length > 0) {\n <span class=\"selection-count\">\n {{ selectedItems().length }} item{{ selectedItems().length !== 1 ? 's' : '' }} selected\n </span>\n <tn-button\n variant=\"outline\"\n label=\"Clear Selection\"\n [testId]=\"clearSelectionTestId()\"\n (onClick)=\"onClearSelection()\" />\n } @else if (allowCurrentDirectorySelection()) {\n <span class=\"selection-count current-directory-selection\">\n Current directory selected\n </span>\n } @else {\n <span class=\"selection-count\">\n No items selected\n </span>\n }\n </div>\n <div class=\"footer-actions\">\n @for (action of createActions(); track action.id) {\n <tn-button\n variant=\"outline\"\n [label]=\"action.label\"\n [testId]=\"createActionTestId(action)\"\n [disabled]=\"!!inlineCreation()\"\n (onClick)=\"onCreateAction(action)\" />\n }\n <!-- Disabled while the inline creation row is open: its blur auto-submit\n would race the selection submit -->\n <tn-button\n label=\"Select\"\n [testId]=\"selectTestId()\"\n [disabled]=\"(selectedItems().length === 0 && !allowCurrentDirectorySelection()) || !!inlineCreation()\"\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);width:520px;max-width:calc(100vw - 32px);min-height:min(500px,100vh - 16px);max-height:min(600px,100vh - 16px);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;flex:1;min-width:0}.tn-file-picker-breadcrumb .breadcrumb-segment{background:transparent;border:none;color:var(--tn-primary);cursor:pointer;padding:4px 2px;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:2px;color:var(--tn-alt-fg1)}.tn-file-picker-breadcrumb .breadcrumb-segment:first-child:before{content:\"/\";margin-right:2px;color:var(--tn-alt-fg1)}.tn-file-picker-breadcrumb .breadcrumb-segment.nameless:not(:last-child):after{content:none}.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}.inline-create-cell{display:flex;flex-direction:column;gap:4px;flex:1;min-width:0}.inline-create-row{display:flex;align-items:center;gap:8px}.inline-create-row .inline-create-input{flex:1;padding:4px 8px;border:2px solid var(--tn-primary);border-radius:3px;background:var(--tn-bg1);color:var(--tn-fg1);font-size:inherit;font-family:inherit;outline:none;min-width:0}.inline-create-row .inline-create-input.error{border-color:var(--tn-error, #d32f2f)}.inline-create-row .inline-create-input:disabled{opacity:.6;cursor:not-allowed;background:var(--tn-bg2)}.inline-create-row .creation-loading-icon{animation:spin 1s linear infinite;color:var(--tn-primary);flex-shrink:0}.inline-create-error{display:flex;align-items:center;gap:6px;margin-top:4px;padding:4px 8px;background:color-mix(in srgb,var(--tn-error, #d32f2f) 10%,transparent);border-left:3px solid var(--tn-error, #d32f2f);border-radius:3px;font-size:1rem;color:var(--tn-error, #d32f2f)}.inline-create-error .error-icon{flex-shrink:0}.inline-create-error .error-text{flex:1}.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-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-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}.navigate-button{display:flex;align-items:center;justify-content:center;margin-left:auto;padding:2px;background:none;border:none;border-radius:4px;color:var(--tn-fg2);cursor:pointer}.navigate-button:hover,.navigate-button:focus-visible{background:var(--tn-bg3);color:var(--tn-primary)}.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;gap:16px;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}.tn-file-picker-footer .footer-selection{display:flex;align-items:center;gap:12px}.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", "fullWidth", "type", "testId", "href", "routerLink", "queryParams", "fragment", "target", "rel", "ariaLabel"], outputs: ["onClick"] }, { kind: "component", type: TnCheckboxComponent, selector: "tn-checkbox", inputs: ["label", "hideLabel", "disabled", "required", "indeterminate", "testId", "error", "checked"], outputs: ["change"] }, { kind: "component", type: TnTableComponent, selector: "tn-table", inputs: ["dataSource", "displayedColumns", "trackBy", "emptyMessage", "emptyIcon", "selectable", "expandable", "bordered", "isRowExpandable", "activeRow", "activeWhen", "activeBg", "activeIndicator", "loading", "loadingMessage", "clickable"], outputs: ["sortChange", "selectionChange", "rowClick", "rowDoubleClick"] }, { 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: "directive", type: TnTestIdDirective, selector: "[tnTestId]", inputs: ["tnTestId", "tnTestIdType"] }, { kind: "pipe", type: FileSizePipe, name: "tnFileSize" }, { kind: "pipe", type: TruncatePathPipe, name: "tnTruncatePath" }] });
17941
18817
  }
17942
18818
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnFilePickerPopupComponent, decorators: [{
17943
18819
  type: Component,
@@ -18948,14 +19824,15 @@ var TnToastPosition;
18948
19824
  TnToastPosition["Bottom"] = "bottom";
18949
19825
  })(TnToastPosition || (TnToastPosition = {}));
18950
19826
 
18951
- // Material icons render via the `material-icons` CSS font (see icon.component.ts),
18952
- // so they don't need sprite scanning — the literal `mat-` prefix matches what
18953
- // the runtime icon resolver would produce from a Material library name.
19827
+ // The sprite generator only sees icon names in templates and in `tnIconMarker()` calls.
19828
+ // These live in a map, so without the marker they'd be dropped from the sprite on the
19829
+ // next regeneration — and there is no `material-icons` font in this library to fall
19830
+ // back to, so the toast would render an empty icon.
18954
19831
  const TOAST_ICONS = {
18955
- [TnToastType.Info]: 'mat-info',
18956
- [TnToastType.Success]: 'mat-check_circle',
18957
- [TnToastType.Warning]: 'mat-warning',
18958
- [TnToastType.Error]: 'mat-error',
19832
+ [TnToastType.Info]: tnIconMarker('info', 'material'),
19833
+ [TnToastType.Success]: tnIconMarker('check_circle', 'material'),
19834
+ [TnToastType.Warning]: tnIconMarker('warning', 'material'),
19835
+ [TnToastType.Error]: tnIconMarker('error', 'material'),
18959
19836
  };
18960
19837
  class TnToastComponent {
18961
19838
  message = signal('', ...(ngDevMode ? [{ debugName: "message" }] : []));
@@ -19792,5 +20669,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
19792
20669
  * Generated bundle index. Do not edit.
19793
20670
  */
19794
20671
 
19795
- export { CommonShortcuts, DEFAULT_THEME, DiskIconComponent, DiskType, FileSizePipe, InputType, LIGHT_THEME, LabelMarkupPipe, LabelTextPipe, LinuxModifierKeys, LinuxShortcuts, ModifierKeys, QuickShortcuts, ShortcutBuilder, THEME_MAP, THEME_STORAGE_KEY, TN_FORM_FIELD_CONTEXT, TN_FORM_FIELD_ERRORS, TN_TABLE_PAGER_DEFAULT_LABELS, TN_TABLE_PAGER_LABELS, TN_TEST_ATTR, TN_THEME_DEFINITIONS, TnAutocompleteComponent, TnAutocompleteHarness, TnBannerActionDirective, TnBannerComponent, TnBannerHarness, TnBrandedSpinnerComponent, TnButtonComponent, TnButtonHarness, TnButtonToggleComponent, TnButtonToggleGroupComponent, TnButtonToggleGroupHarness, TnButtonToggleHarness, TnCalendarComponent, TnCalendarHeaderComponent, TnCardComponent, TnCardFooterActionsDirective, TnCardHeaderActionsDirective, TnCardHeaderDirective, TnCellDefDirective, TnCheckboxComponent, TnCheckboxHarness, TnCheckboxLabelDirective, TnChipComponent, TnChipHarness, TnChipInputComponent, TnChipInputHarness, TnConfirmDialogComponent, TnDateInputComponent, TnDateInputHarness, TnDateRangeInputComponent, TnDateRangeInputHarness, TnDetailRowDefDirective, TnDialog, TnDialogHarness, TnDialogShellComponent, TnDialogTesting, TnDividerComponent, TnDividerDirective, TnDrawerComponent, TnDrawerContainerComponent, TnDrawerContainerHarness, TnDrawerContentComponent, TnDrawerHarness, TnEmptyComponent, TnEmptyHarness, TnExpansionPanelComponent, TnExpansionPanelHarness, TnFileInputComponent, TnFileInputHarness, TnFilePickerComponent, TnFilePickerHarness, TnFilePickerPopupComponent, TnFormFieldComponent, TnFormFieldHarness, TnFormSectionComponent, TnFormSectionHarness, TnHeaderCellDefDirective, TnIconButtonComponent, TnIconButtonHarness, TnIconComponent, TnIconHarness, TnIconRegistryService, TnIconTesting, TnInputComponent, TnInputDirective, TnInputHarness, TnKeyboardShortcutComponent, TnKeyboardShortcutService, TnListAvatarDirective, TnListComponent, TnListIconDirective, TnListItemComponent, TnListItemLineDirective, TnListItemPrimaryDirective, TnListItemSecondaryDirective, TnListItemTitleDirective, TnListItemTrailingDirective, TnListOptionComponent, TnListSubheaderComponent, TnMenuActivateHoverDirective, TnMenuComponent, TnMenuHarness, TnMenuItemComponent, TnMenuTesting, TnMenuTriggerDirective, TnMonthViewComponent, TnMultiYearViewComponent, TnNestedTreeDataSource, TnNestedTreeNodeComponent, TnParticleProgressBarComponent, TnProgressBarComponent, TnRadioComponent, TnRadioHarness, TnSelectComponent, TnSelectHarness, TnSelectionListComponent, TnSidePanelActionDirective, TnSidePanelComponent, TnSidePanelHarness, TnSidePanelHeaderActionDirective, TnSlideToggleComponent, TnSlideToggleHarness, TnSliderComponent, TnSliderThumbDirective, TnSliderWithLabelDirective, TnSpinnerComponent, TnSpriteLoaderService, TnStepComponent, TnStepperComponent, TnStepperHarness, TnStepperNextDirective, TnStepperPreviousDirective, TnTabComponent, TnTabHarness, TnTabPanelComponent, TnTabPanelHarness, TnTableColumnDirective, TnTableComponent, TnTableHarness, TnTablePagerComponent, TnTablePagerHarness, TnTabsComponent, TnTabsHarness, TnTestIdDirective, TnTheme, TnThemeService, TnTimeInputComponent, TnToastComponent, TnToastMock, TnToastPosition, TnToastRef, TnToastService, TnToastTesting, TnToastType, TnTooltipComponent, TnTooltipDirective, TnTreeComponent, TnTreeFlatDataSource, TnTreeFlattener, TnTreeHarness, TnTreeNodeComponent, TnTreeNodeHarness, TnTreeNodeOutletDirective, TnTreeVirtualScrollNodeOutletDirective, TnTreeVirtualScrollViewComponent, TnTreeVirtualScrollViewHarness, TruncatePathPipe, WindowsModifierKeys, WindowsShortcuts, allowsCurrentDirectorySelection, composeTestId, controlTestId, createFlatTreeControl, createLucideLibrary, createNestedTreeControl, createShortcut, defaultSpriteBasePath, defaultSpriteConfigPath, defaultTreeItemSize, formatSize, getSelectableTypes, injectTnFormFieldAria, isPathWithinRoot, kebabTestSegment, labelMarkupToHtml, labelMarkupToText, libIconMarker, normalizeRootPath, optionTestId, parseLabelMarkup, parseSize, registerLucideIcons, scopeTestId, setupLucideIntegration, tnIconMarker, writeTestId };
20672
+ export { CommonShortcuts, DEFAULT_THEME, DiskIconComponent, DiskType, FileSizePipe, InputType, LIGHT_THEME, LabelMarkupPipe, LabelTextPipe, LinuxModifierKeys, LinuxShortcuts, ModifierKeys, QuickShortcuts, ShortcutBuilder, THEME_MAP, THEME_STORAGE_KEY, TN_CALENDAR_INTL, TN_CALENDAR_INTL_DEFAULTS, TN_FORM_FIELD_CONTEXT, TN_FORM_FIELD_ERRORS, TN_TABLE_PAGER_DEFAULT_LABELS, TN_TABLE_PAGER_LABELS, TN_TEST_ATTR, TN_THEME_DEFINITIONS, TnAutocompleteComponent, TnAutocompleteHarness, TnBannerActionDirective, TnBannerComponent, TnBannerHarness, TnBrandedSpinnerComponent, TnButtonComponent, TnButtonHarness, TnButtonToggleComponent, TnButtonToggleGroupComponent, TnButtonToggleGroupHarness, TnButtonToggleHarness, TnCalendarCellHarness, TnCalendarComponent, TnCalendarHarness, TnCalendarHeaderComponent, TnCardComponent, TnCardFooterActionsDirective, TnCardHeaderActionsDirective, TnCardHeaderDirective, TnCellDefDirective, TnCheckboxComponent, TnCheckboxHarness, TnCheckboxLabelDirective, TnChipComponent, TnChipHarness, TnChipInputComponent, TnChipInputHarness, TnConfirmDialogComponent, TnDateInputComponent, TnDateInputHarness, TnDateRangeInputComponent, TnDateRangeInputHarness, TnDetailRowDefDirective, TnDialog, TnDialogHarness, TnDialogShellComponent, TnDialogTesting, TnDividerComponent, TnDividerDirective, TnDrawerComponent, TnDrawerContainerComponent, TnDrawerContainerHarness, TnDrawerContentComponent, TnDrawerHarness, TnEmptyComponent, TnEmptyHarness, TnExpansionPanelComponent, TnExpansionPanelHarness, TnFileInputComponent, TnFileInputHarness, TnFilePickerComponent, TnFilePickerHarness, TnFilePickerPopupComponent, TnFormFieldComponent, TnFormFieldHarness, TnFormSectionComponent, TnFormSectionHarness, TnHeaderCellDefDirective, TnIconButtonComponent, TnIconButtonHarness, TnIconComponent, TnIconHarness, TnIconRegistryService, TnIconTesting, TnInputComponent, TnInputDirective, TnInputHarness, TnKeyboardShortcutComponent, TnKeyboardShortcutService, TnListAvatarDirective, TnListComponent, TnListIconDirective, TnListItemComponent, TnListItemLineDirective, TnListItemPrimaryDirective, TnListItemSecondaryDirective, TnListItemTitleDirective, TnListItemTrailingDirective, TnListOptionComponent, TnListSubheaderComponent, TnMenuActivateHoverDirective, TnMenuComponent, TnMenuHarness, TnMenuItemComponent, TnMenuTesting, TnMenuTriggerDirective, TnMonthViewComponent, TnMultiYearViewComponent, TnNestedTreeDataSource, TnNestedTreeNodeComponent, TnParticleProgressBarComponent, TnProgressBarComponent, TnRadioComponent, TnRadioHarness, TnSelectComponent, TnSelectHarness, TnSelectionListComponent, TnSidePanelActionDirective, TnSidePanelComponent, TnSidePanelHarness, TnSidePanelHeaderActionDirective, TnSlideToggleComponent, TnSlideToggleHarness, TnSliderComponent, TnSliderThumbDirective, TnSliderWithLabelDirective, TnSpinnerComponent, TnSpriteLoaderService, TnStepComponent, TnStepperComponent, TnStepperHarness, TnStepperNextDirective, TnStepperPreviousDirective, TnTabComponent, TnTabHarness, TnTabPanelComponent, TnTabPanelHarness, TnTableColumnDirective, TnTableComponent, TnTableHarness, TnTablePagerComponent, TnTablePagerHarness, TnTabsComponent, TnTabsHarness, TnTestIdDirective, TnTheme, TnThemeService, TnTimeInputComponent, TnToastComponent, TnToastMock, TnToastPosition, TnToastRef, TnToastService, TnToastTesting, TnToastType, TnTooltipComponent, TnTooltipDirective, TnTreeComponent, TnTreeFlatDataSource, TnTreeFlattener, TnTreeHarness, TnTreeNodeComponent, TnTreeNodeHarness, TnTreeNodeOutletDirective, TnTreeVirtualScrollNodeOutletDirective, TnTreeVirtualScrollViewComponent, TnTreeVirtualScrollViewHarness, TruncatePathPipe, WindowsModifierKeys, WindowsShortcuts, allowsCurrentDirectorySelection, composeTestId, controlTestId, createFlatTreeControl, createLucideLibrary, createNestedTreeControl, createShortcut, defaultSpriteBasePath, defaultSpriteConfigPath, defaultTreeItemSize, formatSize, getSelectableTypes, injectTnCalendarIntl, injectTnFormFieldAria, isPathWithinRoot, kebabTestSegment, labelMarkupToHtml, labelMarkupToText, libIconMarker, normalizeRootPath, optionTestId, parseLabelMarkup, parseSize, registerLucideIcons, scopeTestId, setupLucideIntegration, tnIconMarker, writeTestId };
19796
20673
  //# sourceMappingURL=truenas-ui-components.mjs.map