@truenas/ui-components 0.4.11 → 0.5.0

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.
@@ -6,7 +6,7 @@ import * as i1$3 from '@angular/forms';
6
6
  import { NgControl, NG_VALUE_ACCESSOR, FormsModule, Validators } from '@angular/forms';
7
7
  import { ComponentHarness, HarnessPredicate, TestKey, parallel } from '@angular/cdk/testing';
8
8
  import * as i1 from '@angular/cdk/a11y';
9
- import { A11yModule, FocusMonitor, _IdGenerator } from '@angular/cdk/a11y';
9
+ import { A11yModule, AriaDescriber, 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';
@@ -2589,6 +2589,22 @@ class TnButtonComponent {
2589
2589
  this.onClick.emit(event);
2590
2590
  }
2591
2591
  hostRef = inject((ElementRef));
2592
+ constructor() {
2593
+ // A disabled native <button> suppresses its own click events, but its
2594
+ // pointer-events: none style retargets clicks over it to this host element —
2595
+ // where consumers commonly bind (click). Halt those here so a disabled
2596
+ // tn-button is inert for every consumer, matching what handleAnchorClick
2597
+ // does for the anchor variants. Registered in the constructor (before any
2598
+ // template listener on the host) and capture-phase, so it runs first and
2599
+ // stopImmediatePropagation() reliably blocks host (click) handlers.
2600
+ const host = this.hostRef.nativeElement;
2601
+ host.addEventListener('click', (event) => {
2602
+ if (this.disabled()) {
2603
+ event.preventDefault();
2604
+ event.stopImmediatePropagation();
2605
+ }
2606
+ }, { capture: true });
2607
+ }
2592
2608
  // The template renders exactly one of `<a [routerLink]>`, `<a [href]>`, or
2593
2609
  // `<button>` via `@if/@else`, and each carries the `#button` ref — so this
2594
2610
  // resolves to whichever variant is active. Using a viewChild instead of a
@@ -2626,7 +2642,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
2626
2642
  args: [{ selector: 'tn-button', standalone: true, imports: [CommonModule, RouterLink, TnTestIdDirective, LabelMarkupPipe, TnIconComponent], host: {
2627
2643
  '[class.tn-button--full-width]': 'fullWidth()',
2628
2644
  }, 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 }] }] } });
2645
+ }], ctorParameters: () => [], 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 }] }] } });
2630
2646
 
2631
2647
  /**
2632
2648
  * Harness for interacting with tn-button in tests.
@@ -2815,6 +2831,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
2815
2831
  /* eslint-disable @angular-eslint/no-input-rename */
2816
2832
  // Input aliasing is intentional for directive API consistency (e.g., ixTooltip, ixTooltipPosition)
2817
2833
  // This follows the standard Angular pattern used by Material and other directive-based components
2834
+ /**
2835
+ * Elements that can receive focus / are read by assistive tech. Used to decide whether
2836
+ * the directive's host is itself the interactive element, or a wrapper (e.g. `<tn-button>`)
2837
+ * whose inner control should carry `aria-describedby`.
2838
+ */
2839
+ const INTERACTIVE_SELECTOR = 'button, a[href], input, select, textarea, [tabindex]';
2818
2840
  class TnTooltipDirective {
2819
2841
  message = input('', { ...(ngDevMode ? { debugName: "message" } : {}), alias: 'tnTooltip' });
2820
2842
  position = input('above', { ...(ngDevMode ? { debugName: "position" } : {}), alias: 'tnTooltipPosition' });
@@ -2828,27 +2850,99 @@ class TnTooltipDirective {
2828
2850
  _hideTimeout = null;
2829
2851
  _isTooltipVisible = false;
2830
2852
  _positionSub = null;
2831
- _tooltipId = '';
2832
- /**
2833
- * Only point `aria-describedby` at the tooltip when there is actually a message to
2834
- * describe. Otherwise every host (e.g. an icon button with no tooltip) would carry a
2835
- * dangling reference to a tooltip element that is never rendered.
2836
- */
2837
- get _ariaDescribedBy() {
2838
- return !this.disabled() && this.message() ? this._tooltipId : null;
2839
- }
2853
+ // Unique ID for the overlay tooltip element
2854
+ _tooltipId = `tn-tooltip-${Math.random().toString(36).substr(2, 9)}`;
2840
2855
  _overlay = inject(Overlay);
2841
2856
  _elementRef = inject((ElementRef));
2842
2857
  _viewContainerRef = inject(ViewContainerRef);
2843
2858
  _overlayPositionBuilder = inject(OverlayPositionBuilder);
2844
- ngOnInit() {
2845
- // Generate unique ID for aria-describedby
2846
- this._tooltipId = `tn-tooltip-${Math.random().toString(36).substr(2, 9)}`;
2859
+ _ariaDescriber = inject(AriaDescriber);
2860
+ _viewInitialized = false;
2861
+ _describedTarget = null;
2862
+ _describedMessage = '';
2863
+ _innerObserver = null;
2864
+ /**
2865
+ * Re-sync the description (see ngAfterViewInit) when the inputs change. The initial
2866
+ * write cannot happen here: on the first run the host's child components (e.g.
2867
+ * tn-button's inner `<button>`) have not rendered yet.
2868
+ */
2869
+ _syncDescriptionOnInputChange = effect(() => {
2870
+ this.disabled();
2871
+ this.message();
2872
+ if (this._viewInitialized) {
2873
+ this._syncAriaDescription();
2874
+ }
2875
+ }, ...(ngDevMode ? [{ debugName: "_syncDescriptionOnInputChange" }] : []));
2876
+ /**
2877
+ * Expose the message to assistive tech via CDK's AriaDescriber: it keeps the text in
2878
+ * a persistent visually-hidden element (so `aria-describedby` never dangles — the
2879
+ * overlay tooltip only exists while shown) and adds/removes only its own id token,
2880
+ * leaving descriptions the control already carries (form hints, error ids) intact.
2881
+ * The description goes on the interactive element itself: the host when it is one,
2882
+ * otherwise the first interactive descendant (e.g. `<tn-button>`'s inner `<button>`),
2883
+ * because screen readers read descriptions off the focused control, not wrappers.
2884
+ */
2885
+ ngAfterViewInit() {
2886
+ this._viewInitialized = true;
2887
+ this._syncAriaDescription();
2888
+ const host = this._elementRef.nativeElement;
2889
+ if (!host.matches(INTERACTIVE_SELECTOR) && typeof MutationObserver !== 'undefined') {
2890
+ // The inner control can render after view init (@if branches inside the wrapper
2891
+ // swapping, deferred content) — re-describe when the host's subtree changes.
2892
+ // AriaDescriber writes attributes only, which produces no childList mutations,
2893
+ // so this cannot loop.
2894
+ this._innerObserver = new MutationObserver(() => this._syncAriaDescription());
2895
+ this._innerObserver.observe(host, { childList: true, subtree: true });
2896
+ }
2897
+ }
2898
+ _syncAriaDescription() {
2899
+ const host = this._elementRef.nativeElement;
2900
+ let target = host;
2901
+ if (!host.matches(INTERACTIVE_SELECTOR)) {
2902
+ // Forward the description only when it is unambiguous: a wrapper with exactly
2903
+ // one interactive descendant (e.g. tn-button's inner <button>). A container
2904
+ // holding several controls keeps the description on the host — describing an
2905
+ // arbitrary first control would attach the text to the wrong element.
2906
+ const candidates = host.querySelectorAll(INTERACTIVE_SELECTOR);
2907
+ if (candidates.length === 1) {
2908
+ target = candidates[0];
2909
+ }
2910
+ }
2911
+ const message = !this.disabled() ? this._plainTextMessage(this.message()) : '';
2912
+ if (this._describedTarget === target && this._describedMessage === message) {
2913
+ return;
2914
+ }
2915
+ this._removeAriaDescription();
2916
+ if (message) {
2917
+ this._ariaDescriber.describe(target, message);
2918
+ this._describedTarget = target;
2919
+ this._describedMessage = message;
2920
+ }
2921
+ }
2922
+ /**
2923
+ * The overlay tooltip renders its message as HTML (`[innerHTML]`), but AriaDescriber
2924
+ * writes the description as plain text — strip any markup so screen readers never
2925
+ * announce literal tags. DOMParser parses inert markup (no script execution).
2926
+ */
2927
+ _plainTextMessage(message) {
2928
+ if (!message.includes('<') && !message.includes('&')) {
2929
+ return message;
2930
+ }
2931
+ return new DOMParser().parseFromString(message, 'text/html').body.textContent ?? '';
2932
+ }
2933
+ _removeAriaDescription() {
2934
+ if (this._describedTarget && this._describedMessage) {
2935
+ this._ariaDescriber.removeDescription(this._describedTarget, this._describedMessage);
2936
+ }
2937
+ this._describedTarget = null;
2938
+ this._describedMessage = '';
2847
2939
  }
2848
2940
  ngOnDestroy() {
2849
2941
  this._clearTimeouts();
2850
2942
  this.hide(0);
2851
2943
  this._positionSub?.unsubscribe();
2944
+ this._innerObserver?.disconnect();
2945
+ this._removeAriaDescription();
2852
2946
  if (this._overlayRef) {
2853
2947
  this._overlayRef.dispose();
2854
2948
  this._overlayRef = null;
@@ -2862,12 +2956,22 @@ class TnTooltipDirective {
2862
2956
  _onMouseLeave() {
2863
2957
  this.hide(this.hideDelay());
2864
2958
  }
2865
- _onFocus() {
2959
+ // focusin/focusout (not focus/blur): focus does not bubble, so on a wrapper host
2960
+ // (e.g. `<tn-button>`) a focus listener never fires when the inner control is
2961
+ // focused via keyboard. focusin/focusout bubble and cover both shapes.
2962
+ _onFocusIn() {
2866
2963
  if (!this.disabled() && this.message()) {
2867
2964
  this.show(this.showDelay());
2868
2965
  }
2869
2966
  }
2870
- _onBlur() {
2967
+ _onFocusOut(event) {
2968
+ // focusout bubbles for every focus move inside the host too — only hide when
2969
+ // focus actually leaves the host, or a wrapper-internal move would tear down
2970
+ // the visible tooltip via the armed hide timeout.
2971
+ const next = event.relatedTarget;
2972
+ if (next && this._elementRef.nativeElement.contains(next)) {
2973
+ return;
2974
+ }
2871
2975
  this.hide(this.hideDelay());
2872
2976
  }
2873
2977
  _onKeydown(event) {
@@ -2877,10 +2981,16 @@ class TnTooltipDirective {
2877
2981
  }
2878
2982
  /** Shows the tooltip */
2879
2983
  show(delay = 0) {
2880
- if (this.disabled() || !this.message() || this._isTooltipVisible) {
2984
+ if (this.disabled() || !this.message()) {
2881
2985
  return;
2882
2986
  }
2987
+ // Cancel any pending hide even when already visible: a show intent while the
2988
+ // tooltip is up (e.g. focus or pointer re-entering) must keep it up, not let a
2989
+ // previously armed hide timeout tear it down.
2883
2990
  this._clearTimeouts();
2991
+ if (this._isTooltipVisible) {
2992
+ return;
2993
+ }
2884
2994
  this._showTimeout = setTimeout(() => {
2885
2995
  if (!this._overlayRef) {
2886
2996
  this._createOverlay();
@@ -3001,16 +3111,13 @@ class TnTooltipDirective {
3001
3111
  }
3002
3112
  }
3003
3113
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnTooltipDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
3004
- static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.1.0", type: TnTooltipDirective, isStandalone: true, selector: "[tnTooltip]", inputs: { message: { classPropertyName: "message", publicName: "tnTooltip", isSignal: true, isRequired: false, transformFunction: null }, position: { classPropertyName: "position", publicName: "tnTooltipPosition", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "tnTooltipDisabled", isSignal: true, isRequired: false, transformFunction: null }, showDelay: { classPropertyName: "showDelay", publicName: "tnTooltipShowDelay", isSignal: true, isRequired: false, transformFunction: null }, hideDelay: { classPropertyName: "hideDelay", publicName: "tnTooltipHideDelay", isSignal: true, isRequired: false, transformFunction: null }, tooltipClass: { classPropertyName: "tooltipClass", publicName: "tnTooltipClass", isSignal: true, isRequired: false, transformFunction: null } }, host: { listeners: { "mouseenter": "_onMouseEnter()", "mouseleave": "_onMouseLeave()", "focus": "_onFocus()", "blur": "_onBlur()", "keydown": "_onKeydown($event)" }, properties: { "attr.aria-describedby": "_ariaDescribedBy" } }, ngImport: i0 });
3114
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.1.0", type: TnTooltipDirective, isStandalone: true, selector: "[tnTooltip]", inputs: { message: { classPropertyName: "message", publicName: "tnTooltip", isSignal: true, isRequired: false, transformFunction: null }, position: { classPropertyName: "position", publicName: "tnTooltipPosition", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "tnTooltipDisabled", isSignal: true, isRequired: false, transformFunction: null }, showDelay: { classPropertyName: "showDelay", publicName: "tnTooltipShowDelay", isSignal: true, isRequired: false, transformFunction: null }, hideDelay: { classPropertyName: "hideDelay", publicName: "tnTooltipHideDelay", isSignal: true, isRequired: false, transformFunction: null }, tooltipClass: { classPropertyName: "tooltipClass", publicName: "tnTooltipClass", isSignal: true, isRequired: false, transformFunction: null } }, host: { listeners: { "mouseenter": "_onMouseEnter()", "mouseleave": "_onMouseLeave()", "focusin": "_onFocusIn()", "focusout": "_onFocusOut($event)", "keydown": "_onKeydown($event)" } }, ngImport: i0 });
3005
3115
  }
3006
3116
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnTooltipDirective, decorators: [{
3007
3117
  type: Directive,
3008
3118
  args: [{
3009
3119
  selector: '[tnTooltip]',
3010
3120
  standalone: true,
3011
- host: {
3012
- '[attr.aria-describedby]': '_ariaDescribedBy',
3013
- }
3014
3121
  }]
3015
3122
  }], propDecorators: { message: [{ type: i0.Input, args: [{ isSignal: true, alias: "tnTooltip", required: false }] }], position: [{ type: i0.Input, args: [{ isSignal: true, alias: "tnTooltipPosition", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "tnTooltipDisabled", required: false }] }], showDelay: [{ type: i0.Input, args: [{ isSignal: true, alias: "tnTooltipShowDelay", required: false }] }], hideDelay: [{ type: i0.Input, args: [{ isSignal: true, alias: "tnTooltipHideDelay", required: false }] }], tooltipClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "tnTooltipClass", required: false }] }], _onMouseEnter: [{
3016
3123
  type: HostListener,
@@ -3018,12 +3125,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
3018
3125
  }], _onMouseLeave: [{
3019
3126
  type: HostListener,
3020
3127
  args: ['mouseleave']
3021
- }], _onFocus: [{
3128
+ }], _onFocusIn: [{
3022
3129
  type: HostListener,
3023
- args: ['focus']
3024
- }], _onBlur: [{
3130
+ args: ['focusin']
3131
+ }], _onFocusOut: [{
3025
3132
  type: HostListener,
3026
- args: ['blur']
3133
+ args: ['focusout', ['$event']]
3027
3134
  }], _onKeydown: [{
3028
3135
  type: HostListener,
3029
3136
  args: ['keydown', ['$event']]
@@ -6314,7 +6421,7 @@ class TnCardComponent {
6314
6421
  return type ? `tn-card__status--${type}` : 'tn-card__status--neutral';
6315
6422
  }
6316
6423
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnCardComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
6317
- 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 }, fillHeight: { classPropertyName: "fillHeight", publicName: "fillHeight", 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 } }, host: { properties: { "class.tn-card-host--content-height": "!fillHeight()" } }, 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: [":host{display:block}:host(.tn-card-host--content-height){height:auto}:host(.tn-card-host--content-height) .tn-card{height:auto}.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"] }] });
6424
+ 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 }, fillHeight: { classPropertyName: "fillHeight", publicName: "fillHeight", 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 } }, host: { properties: { "class.tn-card-host--content-height": "!fillHeight()" } }, 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 [tnTooltip]=\"action.tooltip || ''\"\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 [tnTooltip]=\"action.tooltip || ''\"\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: [":host{display:block}:host(.tn-card-host--content-height){height:auto}:host(.tn-card-host--content-height) .tn-card{height:auto}.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"] }] });
6318
6425
  }
6319
6426
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnCardComponent, decorators: [{
6320
6427
  type: Component,
@@ -6331,7 +6438,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
6331
6438
  TnTooltipDirective,
6332
6439
  ], host: {
6333
6440
  '[class.tn-card-host--content-height]': '!fillHeight()',
6334
- }, 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: [":host{display:block}:host(.tn-card-host--content-height){height:auto}:host(.tn-card-host--content-height) .tn-card{height:auto}.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"] }]
6441
+ }, 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 [tnTooltip]=\"action.tooltip || ''\"\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 [tnTooltip]=\"action.tooltip || ''\"\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: [":host{display:block}:host(.tn-card-host--content-height){height:auto}:host(.tn-card-host--content-height) .tn-card{height:auto}.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"] }]
6335
6442
  }], ctorParameters: () => [], propDecorators: { projectedHeader: [{ type: i0.ContentChild, args: [i0.forwardRef(() => TnCardHeaderDirective), { isSignal: true }] }], headerActions: [{ type: i0.ContentChild, args: [i0.forwardRef(() => TnCardHeaderActionsDirective), { ...{ read: TemplateRef }, isSignal: true }] }], footerActions: [{ type: i0.ContentChild, args: [i0.forwardRef(() => TnCardFooterActionsDirective), { ...{ read: TemplateRef }, isSignal: true }] }], title: [{ type: i0.Input, args: [{ isSignal: true, alias: "title", required: false }] }], titleLink: [{ type: i0.Input, args: [{ isSignal: true, alias: "titleLink", required: false }] }], titleRouterLink: [{ type: i0.Input, args: [{ isSignal: true, alias: "titleRouterLink", required: false }] }], titleQueryParams: [{ type: i0.Input, args: [{ isSignal: true, alias: "titleQueryParams", required: false }] }], titleTooltip: [{ type: i0.Input, args: [{ isSignal: true, alias: "titleTooltip", required: false }] }], elevation: [{ type: i0.Input, args: [{ isSignal: true, alias: "elevation", required: false }] }], padding: [{ type: i0.Input, args: [{ isSignal: true, alias: "padding", required: false }] }], padContent: [{ type: i0.Input, args: [{ isSignal: true, alias: "padContent", required: false }] }], fillHeight: [{ type: i0.Input, args: [{ isSignal: true, alias: "fillHeight", required: false }] }], bordered: [{ type: i0.Input, args: [{ isSignal: true, alias: "bordered", required: false }] }], background: [{ type: i0.Input, args: [{ isSignal: true, alias: "background", required: false }] }], headerStatus: [{ type: i0.Input, args: [{ isSignal: true, alias: "headerStatus", required: false }] }], headerControl: [{ type: i0.Input, args: [{ isSignal: true, alias: "headerControl", required: false }] }], headerMenu: [{ type: i0.Input, args: [{ isSignal: true, alias: "headerMenu", required: false }] }], headerMenuTriggerTestId: [{ type: i0.Input, args: [{ isSignal: true, alias: "headerMenuTriggerTestId", required: false }] }], primaryAction: [{ type: i0.Input, args: [{ isSignal: true, alias: "primaryAction", required: false }] }], secondaryAction: [{ type: i0.Input, args: [{ isSignal: true, alias: "secondaryAction", required: false }] }], footerLink: [{ type: i0.Input, args: [{ isSignal: true, alias: "footerLink", required: false }] }] } });
6336
6443
 
6337
6444
  class TnExpansionPanelComponent {
@@ -11137,10 +11244,51 @@ class TnTableColumnDirective {
11137
11244
  name = input.required({ ...(ngDevMode ? { debugName: "name" } : {}), alias: 'tnColumnDef' });
11138
11245
  sortable = input(false, ...(ngDevMode ? [{ debugName: "sortable" }] : []));
11139
11246
  width = input(undefined, ...(ngDevMode ? [{ debugName: "width" }] : []));
11247
+ /**
11248
+ * Human-readable label for the column. Used as the default header text in the
11249
+ * regular table layout AND the default field label in card mode, so a single
11250
+ * annotation serves both — prefer this over a text-only `tnHeaderCellDef`.
11251
+ * A `tnHeaderCellDef` template still overrides the header; `cardLabel` still
11252
+ * overrides the card field label. Falls back to the column `name` when unset.
11253
+ */
11254
+ label = input(undefined, ...(ngDevMode ? [{ debugName: "label" }] : []));
11255
+ /**
11256
+ * Relative importance of this column in card mode (see `mobileLayout` on
11257
+ * `tn-table`). Higher numbers render first; fields ranked beyond
11258
+ * `cardPrimaryCount` fold under a "More fields" disclosure. Defaults to `0`,
11259
+ * in which case columns fall back to their `displayedColumns` order. Has no
11260
+ * effect in the regular (wide) table layout.
11261
+ */
11262
+ cardPriority = input(0, ...(ngDevMode ? [{ debugName: "cardPriority" }] : []));
11263
+ /**
11264
+ * Marks this column as the card's title in card mode — rendered prominently
11265
+ * in the card header alongside any row actions, never as a label/value field.
11266
+ * If no column sets this, the first displayed column that isn't `cardHidden` is
11267
+ * used, falling back to the first displayed column when every column is hidden —
11268
+ * a column deliberately kept off the card is not promoted to its most prominent
11269
+ * slot.
11270
+ */
11271
+ cardTitle = input(false, ...(ngDevMode ? [{ debugName: "cardTitle" }] : []));
11272
+ /**
11273
+ * When true, this column is omitted entirely from the card layout (e.g. a
11274
+ * redundant id, or a column whose meaning is already conveyed by the title).
11275
+ * Ignored in the regular table layout.
11276
+ */
11277
+ cardHidden = input(false, ...(ngDevMode ? [{ debugName: "cardHidden" }] : []));
11278
+ /**
11279
+ * Overrides the field label shown for this column in card mode. Defaults to the
11280
+ * column's `label`, then to the column name.
11281
+ *
11282
+ * A `tnHeaderCellDef` template is NOT used here — it is an `ng-template`, not
11283
+ * text, so there is nothing to read out. A column whose header comes only from
11284
+ * that template renders its bare column name as the card field label; set
11285
+ * `label` (or `cardLabel`) to give the card something readable.
11286
+ */
11287
+ cardLabel = input(undefined, ...(ngDevMode ? [{ debugName: "cardLabel" }] : []));
11140
11288
  headerTemplate = contentChild(TnHeaderCellDefDirective, { ...(ngDevMode ? { debugName: "headerTemplate" } : {}), read: TemplateRef });
11141
11289
  cellTemplate = contentChild(TnCellDefDirective, { ...(ngDevMode ? { debugName: "cellTemplate" } : {}), read: TemplateRef });
11142
11290
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnTableColumnDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
11143
- static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.2.0", version: "21.1.0", type: TnTableColumnDirective, isStandalone: true, selector: "[tnColumnDef]", inputs: { name: { classPropertyName: "name", publicName: "tnColumnDef", isSignal: true, isRequired: true, transformFunction: null }, sortable: { classPropertyName: "sortable", publicName: "sortable", isSignal: true, isRequired: false, transformFunction: null }, width: { classPropertyName: "width", publicName: "width", isSignal: true, isRequired: false, transformFunction: null } }, queries: [{ propertyName: "headerTemplate", first: true, predicate: TnHeaderCellDefDirective, descendants: true, read: TemplateRef, isSignal: true }, { propertyName: "cellTemplate", first: true, predicate: TnCellDefDirective, descendants: true, read: TemplateRef, isSignal: true }], exportAs: ["tnColumnDef"], ngImport: i0 });
11291
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.2.0", version: "21.1.0", type: TnTableColumnDirective, isStandalone: true, selector: "[tnColumnDef]", inputs: { name: { classPropertyName: "name", publicName: "tnColumnDef", isSignal: true, isRequired: true, transformFunction: null }, sortable: { classPropertyName: "sortable", publicName: "sortable", isSignal: true, isRequired: false, transformFunction: null }, width: { classPropertyName: "width", publicName: "width", isSignal: true, isRequired: false, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, cardPriority: { classPropertyName: "cardPriority", publicName: "cardPriority", isSignal: true, isRequired: false, transformFunction: null }, cardTitle: { classPropertyName: "cardTitle", publicName: "cardTitle", isSignal: true, isRequired: false, transformFunction: null }, cardHidden: { classPropertyName: "cardHidden", publicName: "cardHidden", isSignal: true, isRequired: false, transformFunction: null }, cardLabel: { classPropertyName: "cardLabel", publicName: "cardLabel", isSignal: true, isRequired: false, transformFunction: null } }, queries: [{ propertyName: "headerTemplate", first: true, predicate: TnHeaderCellDefDirective, descendants: true, read: TemplateRef, isSignal: true }, { propertyName: "cellTemplate", first: true, predicate: TnCellDefDirective, descendants: true, read: TemplateRef, isSignal: true }], exportAs: ["tnColumnDef"], ngImport: i0 });
11144
11292
  }
11145
11293
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnTableColumnDirective, decorators: [{
11146
11294
  type: Directive,
@@ -11149,7 +11297,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
11149
11297
  standalone: true,
11150
11298
  exportAs: 'tnColumnDef',
11151
11299
  }]
11152
- }], propDecorators: { name: [{ type: i0.Input, args: [{ isSignal: true, alias: "tnColumnDef", required: true }] }], sortable: [{ type: i0.Input, args: [{ isSignal: true, alias: "sortable", required: false }] }], width: [{ type: i0.Input, args: [{ isSignal: true, alias: "width", required: false }] }], headerTemplate: [{ type: i0.ContentChild, args: [i0.forwardRef(() => TnHeaderCellDefDirective), { ...{
11300
+ }], propDecorators: { name: [{ type: i0.Input, args: [{ isSignal: true, alias: "tnColumnDef", required: true }] }], sortable: [{ type: i0.Input, args: [{ isSignal: true, alias: "sortable", required: false }] }], width: [{ type: i0.Input, args: [{ isSignal: true, alias: "width", required: false }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], cardPriority: [{ type: i0.Input, args: [{ isSignal: true, alias: "cardPriority", required: false }] }], cardTitle: [{ type: i0.Input, args: [{ isSignal: true, alias: "cardTitle", required: false }] }], cardHidden: [{ type: i0.Input, args: [{ isSignal: true, alias: "cardHidden", required: false }] }], cardLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "cardLabel", required: false }] }], headerTemplate: [{ type: i0.ContentChild, args: [i0.forwardRef(() => TnHeaderCellDefDirective), { ...{
11153
11301
  read: TemplateRef,
11154
11302
  }, isSignal: true }] }], cellTemplate: [{ type: i0.ContentChild, args: [i0.forwardRef(() => TnCellDefDirective), { ...{ read: TemplateRef }, isSignal: true }] }] } });
11155
11303
  /**
@@ -11178,6 +11326,38 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
11178
11326
  standalone: true,
11179
11327
  }]
11180
11328
  }] });
11329
+ /**
11330
+ * Directive to define a row's action controls (e.g. edit/delete buttons).
11331
+ *
11332
+ * Place inside `tn-table` to render a trailing actions column, pinned to the right edge
11333
+ * in `scroll` mode (see `mobileLayout` on `tn-table`) and scrolling with the rest of the
11334
+ * table above `cardBreakpoint`. In card mode the same template is rendered inline beside
11335
+ * the card title, so row actions stay reachable at every viewport width. The template
11336
+ * receives the row as its implicit context.
11337
+ *
11338
+ * @example
11339
+ * ```html
11340
+ * <tn-table [dataSource]="data" [displayedColumns]="columns">
11341
+ * ...column defs...
11342
+ * <ng-template tnRowActionsDef let-row>
11343
+ * <tn-icon-button name="mat-edit" ariaLabel="Edit" (click)="edit(row)" />
11344
+ * <tn-icon-button name="mat-delete" ariaLabel="Delete" (click)="remove(row)" />
11345
+ * </ng-template>
11346
+ * </tn-table>
11347
+ * ```
11348
+ */
11349
+ class TnRowActionsDefDirective {
11350
+ template = inject((TemplateRef));
11351
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnRowActionsDefDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
11352
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.1.0", type: TnRowActionsDefDirective, isStandalone: true, selector: "[tnRowActionsDef]", ngImport: i0 });
11353
+ }
11354
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnRowActionsDefDirective, decorators: [{
11355
+ type: Directive,
11356
+ args: [{
11357
+ selector: '[tnRowActionsDef]',
11358
+ standalone: true,
11359
+ }]
11360
+ }] });
11181
11361
 
11182
11362
  /**
11183
11363
  * Animation duration for detail row expand/collapse.
@@ -11200,6 +11380,16 @@ function getExpandDuration() {
11200
11380
  }
11201
11381
  class TnTableComponent {
11202
11382
  destroyRef = inject(DestroyRef);
11383
+ elementRef = inject((ElementRef));
11384
+ injector = inject(Injector);
11385
+ /**
11386
+ * Element that had focus when `loading` went true, so it can be restored afterwards.
11387
+ *
11388
+ * `inert` blurs whatever is focused inside it and refuses focus while set, so without this
11389
+ * a keyboard user who pressed Enter on a sortable header — the flow that *starts* most
11390
+ * loads — was dropped to `<body>` and had to tab back from the top of the document.
11391
+ */
11392
+ focusBeforeLoading = null;
11203
11393
  // --- Core inputs ---
11204
11394
  dataSource = input([], ...(ngDevMode ? [{ debugName: "dataSource" }] : []));
11205
11395
  displayedColumns = input([], ...(ngDevMode ? [{ debugName: "displayedColumns" }] : []));
@@ -11267,6 +11457,12 @@ class TnTableComponent {
11267
11457
  * When true, shows a spinner overlay over the table. Existing rows remain
11268
11458
  * visible (dimmed) so reloads don't cause layout jumps; if there are no rows
11269
11459
  * yet, the spinner replaces the empty state.
11460
+ *
11461
+ * The rendered surfaces are `inert` while this is true, so they leave the tab order *and*
11462
+ * the accessibility tree — a screen-reader user hears the overlay's "Loading..." status
11463
+ * rather than the stale rows. That is deliberate: `pointer-events` alone left every row,
11464
+ * header and projected control activatable by keyboard against data being replaced.
11465
+ * Focus is captured on the way in and restored on the way out.
11270
11466
  */
11271
11467
  loading = input(false, ...(ngDevMode ? [{ debugName: "loading" }] : []));
11272
11468
  /** Accessible label announced while loading. */
@@ -11275,6 +11471,16 @@ class TnTableComponent {
11275
11471
  * When true, rows become keyboard-focusable (tabindex=0) and clicking or
11276
11472
  * pressing Enter/Space emits `rowClick`. Use this for "click row to view
11277
11473
  * details" patterns. Independent of `selectable` (checkbox) and `expandable`.
11474
+ *
11475
+ * Accessibility limitation in card mode: a card is `role="listitem"`, and the
11476
+ * roles that would announce it as activatable (`button`, `option`) have
11477
+ * presentational children, which would hide the card's own checkbox, row
11478
+ * actions and "Details" toggle from assistive tech. So a clickable card is
11479
+ * focusable and responds to Enter/Space, but is not announced as a control.
11480
+ * Active state is conveyed with `aria-current`, and expansion with
11481
+ * `aria-expanded` when `expandOnRowClick` is set. If an action must be
11482
+ * discoverable by screen-reader users, project an explicit control through
11483
+ * `[tnRowActionsDef]` rather than relying on card activation alone.
11278
11484
  */
11279
11485
  clickable = input(false, ...(ngDevMode ? [{ debugName: "clickable" }] : []));
11280
11486
  /**
@@ -11282,6 +11488,12 @@ class TnTableComponent {
11282
11488
  * chevron. Requires `clickable` — that is what makes rows activatable at all — and `expandable`;
11283
11489
  * rows the `isRowExpandable` predicate rejects are unaffected, since `toggleRowExpansion` gates
11284
11490
  * on it. `rowClick` still emits, so a consumer can both expand and react to the click.
11491
+ *
11492
+ * Applies in card mode too, where activating the card toggles its detail section. Both controls
11493
+ * report the state: the card carries `aria-expanded` while it is the trigger (`listitem` does
11494
+ * permit it, unlike `aria-selected`), and the "Details" button carries it unconditionally,
11495
+ * because that button is what a screen-reader user reaches by tabbing. The redundancy is
11496
+ * deliberate — see the comment above the button in the template.
11285
11497
  */
11286
11498
  expandOnRowClick = input(false, ...(ngDevMode ? [{ debugName: "expandOnRowClick" }] : []));
11287
11499
  /**
@@ -11336,9 +11548,52 @@ class TnTableComponent {
11336
11548
  if (!perColumn || !this.fixedLayout()) {
11337
11549
  return null;
11338
11550
  }
11339
- return `calc(${perColumn} * ${this.effectiveDisplayedColumns().length})`;
11551
+ // The actions column adds its own fixed width rather than a `minColumnWidth` share, because
11552
+ // that is exactly what it claims under `fixedLayout` — and the cell is `border-box`, so the
11553
+ // value added here is its real outer width. (`--tn-table-select-width` and
11554
+ // `--tn-table-expand-width` are treated as ordinary shares instead: they over-reserve
11555
+ // slightly, which is the behavior this component has always had and has tests for. Unifying
11556
+ // all three belongs in its own change.)
11557
+ //
11558
+ // No `var()` fallback on purpose. A duplicated default here is a second source of truth for a
11559
+ // number the stylesheet owns, and it drifted the moment that default changed — leaving the
11560
+ // exact value whose shortfall clipped the actions column sitting in code labelled "the
11561
+ // default". `:host` always defines the property, so the only way to reach a fallback is to
11562
+ // unset it deliberately, and an invalid calc() (no floor) is a more debuggable outcome than a
11563
+ // floor computed from a stale constant.
11564
+ const columns = `${perColumn} * ${this.effectiveDisplayedColumns().length}`;
11565
+ return this.rowActionsDef()
11566
+ ? `calc(${columns} + var(--tn-table-actions-width))`
11567
+ : `calc(${columns})`;
11340
11568
  }, ...(ngDevMode ? [{ debugName: "resolvedMinWidth" }] : []));
11569
+ // --- Responsive (card) inputs ---
11570
+ /**
11571
+ * How the table adapts when its container is narrower than `cardBreakpoint`.
11572
+ * See {@link TnTableMobileLayout}. Defaults to `scroll`, which preserves the
11573
+ * existing horizontal-scroll behavior; set to `cards` to opt into the stacked
11574
+ * card layout.
11575
+ */
11576
+ mobileLayout = input('scroll', ...(ngDevMode ? [{ debugName: "mobileLayout" }] : []));
11577
+ /**
11578
+ * Container width (px) below which `mobileLayout` takes effect. The component
11579
+ * observes its own host width (via `ResizeObserver`), so this responds to the
11580
+ * available container — a table in a narrow sidebar adapts on a wide screen.
11581
+ *
11582
+ * Measured against the host's **content box** — its border and padding are excluded — so a
11583
+ * `[bordered]` table crosses the threshold at the same content width as an unbordered one.
11584
+ */
11585
+ cardBreakpoint = input(640, ...(ngDevMode ? [{ debugName: "cardBreakpoint" }] : []));
11586
+ /**
11587
+ * Number of fields shown directly on each card before the rest fold under a
11588
+ * "More fields" disclosure. The title column is not counted. Defaults to `3`.
11589
+ */
11590
+ cardPrimaryCount = input(3, ...(ngDevMode ? [{ debugName: "cardPrimaryCount" }] : []));
11341
11591
  // --- Outputs ---
11592
+ /**
11593
+ * Emits the resulting sort state whenever the user changes it, from either layout.
11594
+ * Clearing a sort emits `{ column: '', direction: '' }` rather than naming the column
11595
+ * that was cleared — see {@link TnSortEvent}.
11596
+ */
11342
11597
  sortChange = output();
11343
11598
  selectionChange = output();
11344
11599
  /** Emits the row when a clickable row is activated (click or Enter/Space). */
@@ -11353,6 +11608,11 @@ class TnTableComponent {
11353
11608
  // --- Content queries ---
11354
11609
  columnDefs = contentChildren(TnTableColumnDirective, ...(ngDevMode ? [{ debugName: "columnDefs" }] : []));
11355
11610
  detailRowDef = contentChild(TnDetailRowDefDirective, ...(ngDevMode ? [{ debugName: "detailRowDef" }] : []));
11611
+ rowActionsDef = contentChild(TnRowActionsDefDirective, ...(ngDevMode ? [{ debugName: "rowActionsDef" }] : []));
11612
+ // --- Responsive state ---
11613
+ /** Observed host width in px; drives the switch into card mode. */
11614
+ containerWidth = signal(Infinity, ...(ngDevMode ? [{ debugName: "containerWidth" }] : []));
11615
+ resizeObserver;
11356
11616
  // --- Sort state ---
11357
11617
  /**
11358
11618
  * Sorted column and direction. Two-way bindable (`[(sortColumn)]`), so a consumer that owns the
@@ -11368,6 +11628,9 @@ class TnTableComponent {
11368
11628
  * approach could address this.
11369
11629
  */
11370
11630
  expandedRows = signal(new Set(), ...(ngDevMode ? [{ debugName: "expandedRows" }] : []));
11631
+ // Per-instance prefix for generated DOM ids, so two tables on a page can't collide.
11632
+ static instanceCount = 0;
11633
+ instanceId = `tn-table-${TnTableComponent.instanceCount++}`;
11371
11634
  // --- Selection state ---
11372
11635
  selection = new SelectionModel(true, []);
11373
11636
  selectionCount = signal(0, ...(ngDevMode ? [{ debugName: "selectionCount" }] : []));
@@ -11428,14 +11691,147 @@ class TnTableComponent {
11428
11691
  this.expandedRows.set(next);
11429
11692
  }
11430
11693
  });
11694
+ // Save focus on the way into `loading`, restore it on the way out. The restore is
11695
+ // deferred to after the next render because the surfaces only drop `inert` when the
11696
+ // template updates, and focusing an element still inside an inert subtree is a no-op.
11697
+ effect(() => {
11698
+ const host = this.elementRef.nativeElement;
11699
+ if (typeof document === 'undefined') {
11700
+ return;
11701
+ }
11702
+ if (this.loading()) {
11703
+ const active = document.activeElement;
11704
+ // `<body>` is what `inert` leaves behind, not evidence the user moved on. Signals are
11705
+ // glitch-free, so a false -> true flip inside one task (a store that clears loading on
11706
+ // a cache hit and re-sets it for the network request) runs this effect once, seeing
11707
+ // only `true` — and overwriting a still-valid saved element with null there would lose
11708
+ // the focus permanently, which is the symptom this whole effect exists to prevent.
11709
+ if (active && active !== document.body) {
11710
+ this.focusBeforeLoading = host.contains(active) ? active : null;
11711
+ }
11712
+ return;
11713
+ }
11714
+ const previous = this.focusBeforeLoading;
11715
+ if (!previous) {
11716
+ return;
11717
+ }
11718
+ this.focusBeforeLoading = null;
11719
+ afterNextRender(() => {
11720
+ // Never steal focus back from wherever the user moved on to.
11721
+ if (document.activeElement !== document.body) {
11722
+ return;
11723
+ }
11724
+ if (previous.isConnected) {
11725
+ previous.focus();
11726
+ if (document.activeElement === previous) {
11727
+ return;
11728
+ }
11729
+ // Focus was refused. `loading()` discriminates why: true means a second load
11730
+ // re-applied `inert` before this hook ran, so keep the element for next time.
11731
+ // False means it is simply no longer focusable — a `<th>` reused for a
11732
+ // non-sortable column (the header `@for` tracks `$index`), a projected action
11733
+ // that came back `disabled` — and re-arming there would retry the same dead
11734
+ // element on every load, parking the user on `<body>` indefinitely. Reading the
11735
+ // signal here is untracked, so it costs nothing.
11736
+ if (this.loading()) {
11737
+ this.focusBeforeLoading = previous;
11738
+ return;
11739
+ }
11740
+ }
11741
+ // The remembered element is gone: a reload dropped the focused row under an
11742
+ // id-based `trackBy`, or the container flipped to cards mid-load. Keep focus in the
11743
+ // region rather than leaving the user at the top of the document.
11744
+ if (!host.hasAttribute('tabindex')) {
11745
+ host.setAttribute('tabindex', '-1');
11746
+ }
11747
+ host.focus();
11748
+ }, { injector: this.injector });
11749
+ });
11750
+ // Measure the host width to drive card/scroll mode. The initial read is
11751
+ // taken in `afterNextRender` (guaranteed post-layout, so we get the real
11752
+ // width rather than a pre-layout 0), then a `ResizeObserver` keeps it in
11753
+ // sync as the container resizes. Both are browser-only — `afterNextRender`
11754
+ // does not run during SSR and `ResizeObserver` is feature-detected.
11755
+ afterNextRender(() => {
11756
+ const host = this.elementRef.nativeElement;
11757
+ this.measureContainer(host);
11758
+ if (typeof ResizeObserver !== 'undefined') {
11759
+ this.resizeObserver = new ResizeObserver((entries) => {
11760
+ const width = entries[0]?.contentRect.width;
11761
+ if (typeof width === 'number') {
11762
+ // `|| Infinity` for the same reason `measureContainer` applies it: a 0 width
11763
+ // means unmeasurable, not narrow. An element inside a `display: none`
11764
+ // container reports 0x0, so without this a table in a tab that gets hidden
11765
+ // flips to card mode and tears down anything keyed off `isCardMode()` — the
11766
+ // open `<details>` state, for one — for a resize that never happened.
11767
+ this.containerWidth.set(width || Infinity);
11768
+ }
11769
+ });
11770
+ this.resizeObserver.observe(host);
11771
+ }
11772
+ });
11773
+ }
11774
+ /**
11775
+ * Reads the host's current **content-box** width into `containerWidth`.
11776
+ *
11777
+ * Content box specifically, to match the `ResizeObserver` callback's `contentRect`. The
11778
+ * initial read used `getBoundingClientRect()` — the border box — so the two paths measured
11779
+ * different quantities, and `[bordered]` puts a 1px border on each side: a bordered table
11780
+ * whose content box is 639px rendered as a table on first paint (641 is not < 640) and
11781
+ * flipped to cards on the observer's first callback, with no resize having happened. Host
11782
+ * padding widens the gap further.
11783
+ */
11784
+ measureContainer(host) {
11785
+ // The computed `width` is the *used* content-box width — content box even under
11786
+ // `box-sizing: border-box`, already net of the border and of any vertical scrollbar
11787
+ // gutter, and untransformed. That matches `contentRect.width` down to the fraction,
11788
+ // which is the point: `:host` sets `overflow-x: auto` (which promotes `overflow-y` to
11789
+ // auto), so on a platform with classic scrollbars a border-box width measured ~15px
11790
+ // wider here than the observer did, and `getBoundingClientRect()` reports the
11791
+ // *transformed* size, so a table inside a scaled dialog mis-measured as well.
11792
+ //
11793
+ // `clientWidth` minus horizontal padding gets all of that right too, but it is
11794
+ // integer-rounded, and the rounding is not neutral at the boundary: a 639.6px content
11795
+ // box against the default 640 breakpoint reads as 640 here and 639.6 on the observer's
11796
+ // first callback, so the table renders for a frame and then flips to cards with no
11797
+ // resize having happened — the first-paint flip this measurement exists to remove.
11798
+ //
11799
+ // Only a `px` value is a used width, and the unit check is load-bearing rather than
11800
+ // defensive. `width` resolves to the *used* value only for an element that has a layout
11801
+ // box; without one it resolves to the computed value, and `:host` declares `width: 100%`
11802
+ // — so a table built inside a `display: none` container (an inactive tab body, a
11803
+ // collapsed panel) resolves to the string '100%'. Parsed bare that is the number 100,
11804
+ // which is under every sane `cardBreakpoint`: a container that was never measured would
11805
+ // render as cards. Rejecting the non-px value falls through to `clientWidth`, which is 0
11806
+ // there, and on to the 0 -> Infinity guard below — the same answer the `ResizeObserver`
11807
+ // callback gives a 0x0 contentRect. jsdom takes this path too: with no rule matching the
11808
+ // host it resolves `width` to the empty string rather than filling in an initial value
11809
+ // (checked against jsdom 26.1.0, the version jest-environment-jsdom resolves here).
11810
+ const hostStyle = getComputedStyle(host);
11811
+ const resolvedWidth = hostStyle.width;
11812
+ const usedWidth = resolvedWidth.endsWith('px') ? parseFloat(resolvedWidth) : NaN;
11813
+ let contentWidth;
11814
+ if (Number.isFinite(usedWidth)) {
11815
+ contentWidth = usedWidth;
11816
+ }
11817
+ else {
11818
+ const padding = parseFloat(hostStyle.paddingLeft || '0') + parseFloat(hostStyle.paddingRight || '0');
11819
+ contentWidth = host.clientWidth - (Number.isNaN(padding) ? 0 : padding);
11820
+ }
11821
+ this.containerWidth.set(contentWidth > 0 ? contentWidth : Infinity);
11431
11822
  }
11432
11823
  ngOnInit() {
11433
11824
  this.initialized = true;
11434
- // Clean up SelectionModel on destroy
11435
11825
  this.destroyRef.onDestroy(() => {
11436
11826
  this.selection.clear();
11827
+ this.resizeObserver?.disconnect();
11437
11828
  });
11438
11829
  }
11830
+ // --- Responsive computeds ---
11831
+ /** True when the layout should collapse rows into cards. */
11832
+ isCardMode = computed(() => this.mobileLayout() === 'cards' && this.containerWidth() < this.cardBreakpoint(), ...(ngDevMode ? [{ debugName: "isCardMode" }] : []));
11833
+ /** True when the layout should keep the table but pin edge columns and scroll. */
11834
+ isScrollMode = computed(() => this.mobileLayout() === 'scroll' && this.containerWidth() < this.cardBreakpoint(), ...(ngDevMode ? [{ debugName: "isScrollMode" }] : []));
11439
11835
  // --- Computed ---
11440
11836
  data = computed(() => {
11441
11837
  const source = this.dataSource();
@@ -11454,6 +11850,13 @@ class TnTableComponent {
11454
11850
  }
11455
11851
  return cols;
11456
11852
  }, ...(ngDevMode ? [{ debugName: "effectiveDisplayedColumns" }] : []));
11853
+ /**
11854
+ * Every column the table actually renders, including the trailing actions column, which
11855
+ * `effectiveDisplayedColumns` does not track because it comes from a content template rather
11856
+ * than `displayedColumns`. Used for the detail row's `colspan`, so a detail row spans the full
11857
+ * width no matter which structural columns are on.
11858
+ */
11859
+ totalColumnCount = computed(() => this.effectiveDisplayedColumns().length + (this.rowActionsDef() ? 1 : 0), ...(ngDevMode ? [{ debugName: "totalColumnCount" }] : []));
11457
11860
  isAllSelected = computed(() => {
11458
11861
  const numSelected = this.selectionCount();
11459
11862
  const numRows = this.data().length;
@@ -11476,13 +11879,15 @@ class TnTableComponent {
11476
11879
  if (!colDef?.sortable()) {
11477
11880
  return;
11478
11881
  }
11479
- if (this.sortColumn() === column) {
11480
- const current = this.sortDirection();
11481
- if (current === 'asc') {
11482
- this.sortDirection.set('desc');
11483
- }
11484
- else if (current === 'desc') {
11485
- this.sortDirection.set('');
11882
+ // `&& this.sortDirection()` matters: a column set with an empty direction is *not* sorted, and
11883
+ // that state is reachable through the two-way `[(sortColumn)]` binding — a consumer restoring
11884
+ // only the column lands in it. Without the guard neither inner branch matched, so the click
11885
+ // mutated nothing and still emitted a "cleared" event, leaving a header the user could click
11886
+ // forever with no arrow to explain it. Folding it into the not-sorted branch starts a fresh
11887
+ // ascending sort, which is what clicking an unsorted header does everywhere else.
11888
+ if (this.sortColumn() === column && this.sortDirection()) {
11889
+ this.sortDirection.set(this.sortDirection() === 'asc' ? 'desc' : '');
11890
+ if (!this.sortDirection()) {
11486
11891
  this.sortColumn.set('');
11487
11892
  }
11488
11893
  }
@@ -11490,8 +11895,12 @@ class TnTableComponent {
11490
11895
  this.sortColumn.set(column);
11491
11896
  this.sortDirection.set('asc');
11492
11897
  }
11898
+ // Emits the *resulting* sort state, so clearing a sort reports `{ column: '', direction: '' }`
11899
+ // rather than naming the column that was just cleared. Card mode's `setSortColumn('')` has
11900
+ // always emitted the empty column, and a consumer keying off `event.column` should not get a
11901
+ // different answer depending on which layout the container width happens to be showing.
11493
11902
  this.sortChange.emit({
11494
- column: this.sortColumn() || column,
11903
+ column: this.sortColumn(),
11495
11904
  direction: this.sortDirection(),
11496
11905
  });
11497
11906
  }
@@ -11542,6 +11951,21 @@ class TnTableComponent {
11542
11951
  isRowExpandTrigger(row) {
11543
11952
  return this.expandOnRowClick() && this.clickable() && this.canExpandRow(row);
11544
11953
  }
11954
+ /**
11955
+ * Card-mode counterpart of {@link isRowExpandTrigger}, additionally requiring a detail
11956
+ * template. Card mode renders no expansion affordance at all without one, so `aria-expanded`
11957
+ * on the card would advertise a state change that produces nothing.
11958
+ *
11959
+ * Table mode deliberately keeps the looser check: its rows are asserted to carry
11960
+ * `aria-expanded` from `expandOnRowClick` alone, and that predates this layout.
11961
+ */
11962
+ isCardExpandTrigger(row) {
11963
+ return this.isRowExpandTrigger(row) && !!this.detailRowDef();
11964
+ }
11965
+ /** DOM id for a row's detail panel, so the expand trigger can point `aria-controls` at it. */
11966
+ detailPanelId(rowIndex) {
11967
+ return `${this.instanceId}-detail-${rowIndex}`;
11968
+ }
11545
11969
  // --- Active row ---
11546
11970
  isRowActive(row) {
11547
11971
  if (this.activeWhen()?.(row)) {
@@ -11551,25 +11975,47 @@ class TnTableComponent {
11551
11975
  return active !== null && active === row;
11552
11976
  }
11553
11977
  // --- Row click ---
11554
- onRowClick(row) {
11978
+ /**
11979
+ * @param row The activated row.
11980
+ * @param event The originating DOM event, when there is one. Optional so a consumer (or a
11981
+ * test) can still activate a row programmatically; the control-target guard only applies
11982
+ * to real events, which is the only case where a projected control can be the source.
11983
+ */
11984
+ onRowClick(row, event) {
11555
11985
  if (!this.clickable()) {
11556
11986
  return;
11557
11987
  }
11988
+ // Mirrors `onCardClick`. Only the select, expand and actions cells stop `click`
11989
+ // themselves, so a control projected into an ordinary `tnCellDef` — a checkbox, a
11990
+ // link — used to activate the row as well: `rowClick` fired and, under
11991
+ // `expandOnRowClick`, the detail panel toggled out from under the user. Card mode
11992
+ // never did that, so the same consumer template behaved differently either side of
11993
+ // `cardBreakpoint`.
11994
+ if (event && this.isRowControlTarget(event)) {
11995
+ return;
11996
+ }
11558
11997
  if (this.expandOnRowClick()) {
11559
11998
  this.toggleRowExpansion(row);
11560
11999
  }
11561
12000
  this.rowClick.emit(row);
11562
12001
  }
11563
- onRowDoubleClick(row) {
12002
+ /** @param event See {@link onRowClick} — same guard, same optionality. */
12003
+ onRowDoubleClick(row, event) {
11564
12004
  if (!this.clickable()) {
11565
12005
  return;
11566
12006
  }
12007
+ if (event && this.isRowControlTarget(event)) {
12008
+ return;
12009
+ }
11567
12010
  this.rowDoubleClick.emit(row);
11568
12011
  }
11569
12012
  onRowKeydown(event, row) {
11570
12013
  if (!this.clickable()) {
11571
12014
  return;
11572
12015
  }
12016
+ if (this.isRowControlTarget(event)) {
12017
+ return;
12018
+ }
11573
12019
  if (event.key === 'Enter' || event.key === ' ') {
11574
12020
  event.preventDefault();
11575
12021
  if (this.expandOnRowClick()) {
@@ -11578,6 +12024,135 @@ class TnTableComponent {
11578
12024
  this.rowClick.emit(row);
11579
12025
  }
11580
12026
  }
12027
+ // --- Card activation ---
12028
+ // Cards reuse `rowClick`, but the card header embeds its own controls
12029
+ // (selection checkbox, row actions, "more"/detail toggles). Activating the
12030
+ // card must ignore clicks/keys that originate from those controls so a tap on
12031
+ // an action button doesn't also fire `rowClick`.
12032
+ onCardClick(event, row) {
12033
+ if (!this.clickable() || this.isCardControlTarget(event)) {
12034
+ return;
12035
+ }
12036
+ if (this.expandOnRowClick()) {
12037
+ this.toggleRowExpansion(row);
12038
+ }
12039
+ this.rowClick.emit(row);
12040
+ }
12041
+ /** Card-mode counterpart of {@link onRowDoubleClick}, with the same guard. */
12042
+ onCardDoubleClick(event, row) {
12043
+ if (!this.clickable() || this.isCardControlTarget(event)) {
12044
+ return;
12045
+ }
12046
+ this.rowDoubleClick.emit(row);
12047
+ }
12048
+ onCardKeydown(event, row) {
12049
+ if (!this.clickable()) {
12050
+ return;
12051
+ }
12052
+ if (event.key !== 'Enter' && event.key !== ' ') {
12053
+ return;
12054
+ }
12055
+ if (this.isCardControlTarget(event)) {
12056
+ return;
12057
+ }
12058
+ event.preventDefault();
12059
+ if (this.expandOnRowClick()) {
12060
+ this.toggleRowExpansion(row);
12061
+ }
12062
+ this.rowClick.emit(row);
12063
+ }
12064
+ /** Anything focusable or otherwise interactive, regardless of layout. */
12065
+ static INTERACTIVE_SELECTOR = 'a[href], button, input, select, textarea, summary,'
12066
+ + ' [contenteditable]:not([contenteditable="false"]), [tabindex]:not([tabindex="-1"])';
12067
+ /**
12068
+ * Whether an event originated on a control *within* the activation surface rather than on
12069
+ * the surface itself.
12070
+ *
12071
+ * @param event The DOM event; its `currentTarget` is the activation surface.
12072
+ * @param containerSelectors Layout-specific wrappers that count as controls.
12073
+ */
12074
+ isControlTarget(event, containerSelectors) {
12075
+ const target = event.target;
12076
+ const selector = containerSelectors
12077
+ ? `${TnTableComponent.INTERACTIVE_SELECTOR}, ${containerSelectors}`
12078
+ : TnTableComponent.INTERACTIVE_SELECTOR;
12079
+ const match = target?.closest(selector);
12080
+ // The row/card itself is focusable when `clickable`, so it matches the focusable clause.
12081
+ // It is the activation surface, not a control within it.
12082
+ return !!match && match !== event.currentTarget;
12083
+ }
12084
+ /**
12085
+ * Interactive elements plus the card's own controls and its projected detail panel.
12086
+ *
12087
+ * A class allowlist is not enough: the detail panel renders consumer content inside the
12088
+ * clickable card, so an allowlist lets a click on a projected button bubble up and fire
12089
+ * `rowClick` — and with `expandOnRowClick`, collapse the very panel being used. Table mode
12090
+ * never had that problem, because there the detail row is a sibling `<tr>` outside the
12091
+ * clickable row.
12092
+ *
12093
+ * The card's own field values are deliberately not interactive, so they still activate the
12094
+ * card whether primary or folded under "More fields".
12095
+ */
12096
+ isCardControlTarget(event) {
12097
+ return this.isControlTarget(event, '.tn-table__card-actions, .tn-table__card-select, .tn-table__card-more-summary,'
12098
+ + ' .tn-table__card-detail-toggle, .tn-table__card-detail');
12099
+ }
12100
+ /**
12101
+ * Table-mode counterpart. The cells stop `click` themselves, but nothing stopped `keydown`,
12102
+ * so Enter on a projected action button bubbled to the row and got `preventDefault()`d —
12103
+ * which *is* how Enter activates a `<button>`, so the consumer's handler never ran and
12104
+ * `rowClick` fired for the row instead. Card mode has always guarded this; the two layouts
12105
+ * disagreed about what a projected control does under the keyboard.
12106
+ */
12107
+ isRowControlTarget(event) {
12108
+ return this.isControlTarget(event, '.tn-table__select-cell, .tn-table__expand-cell, .tn-table__actions-cell');
12109
+ }
12110
+ /**
12111
+ * Header-cell activation, guarded like the row and the card.
12112
+ *
12113
+ * A `<th>` is an activation surface too, and a control projected through
12114
+ * `tnHeaderCellDef` broke both ways without this: clicking a filter button re-sorted the
12115
+ * table as well as running the button's own handler, and Space on a checkbox in a
12116
+ * *non-sortable* header was swallowed entirely — `onSortClick` returned early on the
12117
+ * missing `sortable()`, but the template's `preventDefault()` ran regardless, so the
12118
+ * control was simply dead.
12119
+ *
12120
+ * @param column The column the header belongs to.
12121
+ * @param event The originating DOM event.
12122
+ */
12123
+ onSortHeaderClick(column, event) {
12124
+ if (this.isControlTarget(event, '')) {
12125
+ return;
12126
+ }
12127
+ if (!this.getColumnDef(column)?.sortable()) {
12128
+ return;
12129
+ }
12130
+ this.onSortClick(column);
12131
+ }
12132
+ /**
12133
+ * Keyboard counterpart of {@link onSortHeaderClick}.
12134
+ *
12135
+ * Takes `Event` rather than `KeyboardEvent` because Angular types `$event` as `Event` for
12136
+ * the `keydown.enter` / `keydown.space` pseudo-events; narrowing happens here.
12137
+ */
12138
+ onSortHeaderKeydown(column, event) {
12139
+ if (this.isControlTarget(event, '')) {
12140
+ return;
12141
+ }
12142
+ if (!this.getColumnDef(column)?.sortable()) {
12143
+ return;
12144
+ }
12145
+ // Only swallow the key once we know this header owns it, so Space on a projected
12146
+ // control still reaches that control.
12147
+ if (event.key === ' ') {
12148
+ event.preventDefault();
12149
+ }
12150
+ this.onSortClick(column);
12151
+ }
12152
+ /** Handles the card-mode sort `<select>` change. */
12153
+ onSortSelectChange(event) {
12154
+ this.setSortColumn(event.target.value);
12155
+ }
11581
12156
  // --- Selection methods ---
11582
12157
  toggleSelectAll() {
11583
12158
  if (this.isAllSelected()) {
@@ -11597,15 +12172,138 @@ class TnTableComponent {
11597
12172
  isRowSelected(row) {
11598
12173
  return this.selection.isSelected(row);
11599
12174
  }
12175
+ // --- Card-mode computeds ---
12176
+ /**
12177
+ * The column rendered as the card title. The first `displayedColumns` entry
12178
+ * whose def sets `cardTitle`, else the first column not marked `cardHidden` —
12179
+ * a column the consumer asked to keep off the card must not be promoted to its
12180
+ * most prominent slot. Falls back to the first displayed column only when every
12181
+ * column is hidden, since a card still needs a title.
12182
+ */
12183
+ cardTitleColumn = computed(() => {
12184
+ const cols = this.displayedColumns();
12185
+ const explicit = cols.find((c) => this.getColumnDef(c)?.cardTitle());
12186
+ const visible = cols.find((c) => !this.getColumnDef(c)?.cardHidden());
12187
+ return explicit ?? visible ?? cols[0] ?? '';
12188
+ }, ...(ngDevMode ? [{ debugName: "cardTitleColumn" }] : []));
12189
+ /**
12190
+ * Columns rendered as label/value fields in a card, ordered by descending
12191
+ * `cardPriority` (ties keep `displayedColumns` order). Excludes the title column
12192
+ * and any `cardHidden` columns.
12193
+ */
12194
+ cardFieldColumns = computed(() => {
12195
+ const title = this.cardTitleColumn();
12196
+ const fields = this.displayedColumns()
12197
+ .map((name, index) => ({ name, index }))
12198
+ .filter(({ name }) => name !== title && !this.getColumnDef(name)?.cardHidden());
12199
+ fields.sort((a, b) => {
12200
+ const pa = this.getColumnDef(a.name)?.cardPriority() ?? 0;
12201
+ const pb = this.getColumnDef(b.name)?.cardPriority() ?? 0;
12202
+ return pb - pa || a.index - b.index;
12203
+ });
12204
+ return fields.map((f) => f.name);
12205
+ }, ...(ngDevMode ? [{ debugName: "cardFieldColumns" }] : []));
12206
+ /** Fields shown directly on the card (up to `cardPrimaryCount`). */
12207
+ cardPrimaryColumns = computed(() => this.cardFieldColumns().slice(0, this.cardPrimaryCount()), ...(ngDevMode ? [{ debugName: "cardPrimaryColumns" }] : []));
12208
+ /** Fields tucked behind the "More fields" disclosure. */
12209
+ cardSecondaryColumns = computed(() => this.cardFieldColumns().slice(this.cardPrimaryCount()), ...(ngDevMode ? [{ debugName: "cardSecondaryColumns" }] : []));
12210
+ /**
12211
+ * Displayed columns that are sortable — populates the card-mode sort menu.
12212
+ *
12213
+ * `cardHidden` columns are excluded, because this menu *is* part of the card layout
12214
+ * and `cardHidden` promises the column is "omitted entirely" from it. Offering one
12215
+ * here would let a user reorder the cards by a value no card shows, with the
12216
+ * direction toggle giving no clue what changed — and such a column often has no
12217
+ * readable label either, since `getCardLabel()` falls back to the raw column name
12218
+ * when the header lives only in a `tnHeaderCellDef` template.
12219
+ *
12220
+ * The title column stays eligible: it is excluded from the *fields*, but it is
12221
+ * displayed, so sorting by it is meaningful.
12222
+ */
12223
+ sortableColumns = computed(() => {
12224
+ const listed = this.displayedColumns().filter((c) => {
12225
+ const def = this.getColumnDef(c);
12226
+ return !!def?.sortable() && !def.cardHidden();
12227
+ });
12228
+ // Reconciled against the rendered option set, not against one reason for exclusion.
12229
+ // Whatever the active column is, it must have an `<option>`: with none matching, the
12230
+ // browser resets the picker to "Unsorted" while the direction toggle still renders an
12231
+ // arrow beside it, and picking "Unsorted" fires no `change` — it is already the
12232
+ // selected option — so the sort becomes unclearable from card mode. `cardHidden` was
12233
+ // only one way in; `sortColumn` is a two-way `model()`, and a dynamic
12234
+ // `displayedColumns` (a column-visibility toggle) can drop a sorted column or leave
12235
+ // one that was never `sortable()`.
12236
+ const active = this.sortColumn();
12237
+ return active && !listed.includes(active) ? [...listed, active] : listed;
12238
+ }, ...(ngDevMode ? [{ debugName: "sortableColumns" }] : []));
12239
+ /**
12240
+ * Whether the active sort column is one the table can actually sort by.
12241
+ *
12242
+ * `sortableColumns` rescues an active column into the menu so it stays clearable, even
12243
+ * when it is `cardHidden`, absent from `displayedColumns`, or never `sortable()`. That
12244
+ * escape hatch must not hand card mode a capability table mode refuses: clicking a
12245
+ * non-sortable header does nothing, so the direction toggle is hidden — and
12246
+ * {@link toggleSortDirection} refuses — for the same column.
12247
+ */
12248
+ canSortActiveColumn = computed(() => {
12249
+ const column = this.sortColumn();
12250
+ return !!column && !!this.getColumnDef(column)?.sortable();
12251
+ }, ...(ngDevMode ? [{ debugName: "canSortActiveColumn" }] : []));
12252
+ // --- Card-mode sort ---
12253
+ /**
12254
+ * Sets (or clears, when passed `''`) the active sort column for card mode.
12255
+ * Switching columns resets to ascending, and clearing emits an empty `column` —
12256
+ * both matching {@link onSortClick}, since the same `sortChange` contract should
12257
+ * not depend on which layout the container width happens to be showing.
12258
+ */
12259
+ setSortColumn(column) {
12260
+ if (!column) {
12261
+ this.sortColumn.set('');
12262
+ this.sortDirection.set('');
12263
+ }
12264
+ else {
12265
+ const changed = this.sortColumn() !== column;
12266
+ this.sortColumn.set(column);
12267
+ if (changed || this.sortDirection() === '') {
12268
+ this.sortDirection.set('asc');
12269
+ }
12270
+ }
12271
+ this.sortChange.emit({ column: this.sortColumn(), direction: this.sortDirection() });
12272
+ }
12273
+ /**
12274
+ * Flips the active sort direction between ascending and descending.
12275
+ *
12276
+ * Starts from ascending, so a column carrying an empty direction — reachable by restoring only
12277
+ * `sortColumn` through its two-way binding — sorts ascending rather than skipping straight to
12278
+ * descending. Same reading of "empty direction means not sorted" as {@link onSortClick}.
12279
+ */
12280
+ toggleSortDirection() {
12281
+ // Guarded, not just hidden: the control is gone from the template for a non-sortable
12282
+ // active column, and the method refuses too, so the API can't reorder by a column the
12283
+ // table header would ignore.
12284
+ if (!this.canSortActiveColumn()) {
12285
+ return;
12286
+ }
12287
+ this.sortDirection.set(this.sortDirection() === 'asc' ? 'desc' : 'asc');
12288
+ this.sortChange.emit({ column: this.sortColumn(), direction: this.sortDirection() });
12289
+ }
11600
12290
  // --- Column helpers ---
11601
12291
  getColumnDef(columnName) {
11602
12292
  return this.columnDefMap().get(columnName);
11603
12293
  }
12294
+ /**
12295
+ * Field label for a column in card mode. Precedence: `cardLabel` override →
12296
+ * shared `label` → the column name.
12297
+ */
12298
+ getCardLabel(column) {
12299
+ const def = this.getColumnDef(column);
12300
+ return def?.cardLabel() ?? def?.label() ?? column;
12301
+ }
11604
12302
  getCellValue(row, column) {
11605
12303
  return row[column];
11606
12304
  }
11607
12305
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnTableComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
11608
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.0", type: TnTableComponent, isStandalone: true, selector: "tn-table", inputs: { dataSource: { classPropertyName: "dataSource", publicName: "dataSource", isSignal: true, isRequired: false, transformFunction: null }, displayedColumns: { classPropertyName: "displayedColumns", publicName: "displayedColumns", isSignal: true, isRequired: false, transformFunction: null }, trackBy: { classPropertyName: "trackBy", publicName: "trackBy", isSignal: true, isRequired: false, transformFunction: null }, emptyMessage: { classPropertyName: "emptyMessage", publicName: "emptyMessage", isSignal: true, isRequired: false, transformFunction: null }, emptyDescription: { classPropertyName: "emptyDescription", publicName: "emptyDescription", isSignal: true, isRequired: false, transformFunction: null }, emptyIcon: { classPropertyName: "emptyIcon", publicName: "emptyIcon", isSignal: true, isRequired: false, transformFunction: null }, selectable: { classPropertyName: "selectable", publicName: "selectable", isSignal: true, isRequired: false, transformFunction: null }, expandable: { classPropertyName: "expandable", publicName: "expandable", isSignal: true, isRequired: false, transformFunction: null }, bordered: { classPropertyName: "bordered", publicName: "bordered", isSignal: true, isRequired: false, transformFunction: null }, isRowExpandable: { classPropertyName: "isRowExpandable", publicName: "isRowExpandable", isSignal: true, isRequired: false, transformFunction: null }, activeRow: { classPropertyName: "activeRow", publicName: "activeRow", isSignal: true, isRequired: false, transformFunction: null }, activeWhen: { classPropertyName: "activeWhen", publicName: "activeWhen", isSignal: true, isRequired: false, transformFunction: null }, activeBg: { classPropertyName: "activeBg", publicName: "activeBg", isSignal: true, isRequired: false, transformFunction: null }, activeIndicator: { classPropertyName: "activeIndicator", publicName: "activeIndicator", isSignal: true, isRequired: false, transformFunction: null }, loading: { classPropertyName: "loading", publicName: "loading", isSignal: true, isRequired: false, transformFunction: null }, loadingMessage: { classPropertyName: "loadingMessage", publicName: "loadingMessage", isSignal: true, isRequired: false, transformFunction: null }, clickable: { classPropertyName: "clickable", publicName: "clickable", isSignal: true, isRequired: false, transformFunction: null }, expandOnRowClick: { classPropertyName: "expandOnRowClick", publicName: "expandOnRowClick", isSignal: true, isRequired: false, transformFunction: null }, singleExpand: { classPropertyName: "singleExpand", publicName: "singleExpand", isSignal: true, isRequired: false, transformFunction: null }, fixedLayout: { classPropertyName: "fixedLayout", publicName: "fixedLayout", isSignal: true, isRequired: false, transformFunction: null }, minColumnWidth: { classPropertyName: "minColumnWidth", publicName: "minColumnWidth", isSignal: true, isRequired: false, transformFunction: null }, minWidth: { classPropertyName: "minWidth", publicName: "minWidth", isSignal: true, isRequired: false, transformFunction: null }, sortColumn: { classPropertyName: "sortColumn", publicName: "sortColumn", isSignal: true, isRequired: false, transformFunction: null }, sortDirection: { classPropertyName: "sortDirection", publicName: "sortDirection", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { sortChange: "sortChange", selectionChange: "selectionChange", rowClick: "rowClick", rowDoubleClick: "rowDoubleClick", sortColumn: "sortColumnChange", sortDirection: "sortDirectionChange" }, host: { properties: { "class.tn-table--bordered": "bordered()", "class.tn-table--fixed-layout": "fixedLayout()", "class.tn-table--loading": "loading()", "style.--tn-table-active-bg": "activeBg()", "style.--tn-table-active-indicator": "activeIndicator()" }, classAttribute: "tn-table" }, queries: [{ propertyName: "columnDefs", predicate: TnTableColumnDirective, isSignal: true }, { propertyName: "detailRowDef", first: true, predicate: TnDetailRowDefDirective, descendants: true, isSignal: true }], hostDirectives: [{ directive: TnTestIdDirective, inputs: ["tnTestId", "testId"] }], ngImport: i0, template: "<table class=\"tn-table__table\" [style.min-width]=\"resolvedMinWidth()\">\n <!-- Header Row -->\n <thead class=\"tn-table__header\">\n <tr class=\"tn-table__header-row\">\n @for (column of effectiveDisplayedColumns(); track $index) {\n @if (column === '__select') {\n <th class=\"tn-table__header-cell tn-table__select-cell\"\n role=\"checkbox\"\n tabindex=\"0\"\n [attr.aria-checked]=\"isAllSelected()\"\n (click)=\"toggleSelectAll()\"\n (keydown.enter)=\"toggleSelectAll()\"\n (keydown.space)=\"toggleSelectAll(); $event.preventDefault()\">\n <tn-checkbox\n class=\"tn-table__checkbox\"\n label=\"Select all rows\"\n [hideLabel]=\"true\"\n [checked]=\"isAllSelected()\"\n [indeterminate]=\"isIndeterminate()\" />\n </th>\n } @else if (column === '__expand') {\n <th class=\"tn-table__header-cell tn-table__expand-cell\">\n <span class=\"cdk-visually-hidden\">Expand</span>\n </th>\n } @else {\n <th\n class=\"tn-table__header-cell\"\n [class.tn-table__header-cell--sortable]=\"getColumnDef(column)?.sortable()\"\n [class.tn-table__header-cell--sorted]=\"isSorted(column)\"\n [style.width]=\"getColumnDef(column)?.width()\"\n [attr.aria-sort]=\"\n isSorted(column)\n ? sortDirection() === 'asc' ? 'ascending' : 'descending'\n : null\n \"\n [attr.tabindex]=\"getColumnDef(column)?.sortable() ? 0 : null\"\n [attr.data-column]=\"column\"\n (click)=\"getColumnDef(column)?.sortable() && onSortClick(column)\"\n (keydown.enter)=\"onSortClick(column)\"\n (keydown.space)=\"onSortClick(column); $event.preventDefault()\">\n <span class=\"tn-table__sort-container\">\n <span class=\"tn-table__header-text\">\n @if (getColumnDef(column)?.headerTemplate(); as tmpl) {\n <ng-container [ngTemplateOutlet]=\"tmpl\" />\n } @else {\n {{ column }}\n }\n </span>\n @if (getColumnDef(column)?.sortable()) {\n <tn-icon\n class=\"tn-table__sort-icon\"\n size=\"sm\"\n [name]=\"\n isSorted(column)\n ? (sortDirection() === 'asc' ? 'mat-arrow_upward' : 'mat-arrow_downward')\n : 'mat-unfold_more'\n \" />\n }\n </span>\n </th>\n }\n }\n </tr>\n </thead>\n\n <!-- Data Rows -->\n <tbody class=\"tn-table__body\">\n @for (row of data(); track trackByFn()($index, row); let rowIdx = $index) {\n <tr\n class=\"tn-table__row\"\n [attr.data-row-index]=\"rowIdx\"\n [class.tn-table__row--expandable]=\"canExpandRow(row)\"\n [class.tn-table__row--expanded]=\"canExpandRow(row) && isRowExpanded(row)\"\n [class.tn-table__row--active]=\"isRowActive(row)\"\n [class.tn-table__row--clickable]=\"clickable()\"\n [attr.tabindex]=\"clickable() ? 0 : null\"\n [attr.aria-selected]=\"clickable() ? isRowActive(row) : null\"\n [attr.aria-expanded]=\"isRowExpandTrigger(row) ? isRowExpanded(row) : null\"\n (click)=\"onRowClick(row)\"\n (dblclick)=\"onRowDoubleClick(row)\"\n (keydown)=\"onRowKeydown($event, row)\">\n @for (column of effectiveDisplayedColumns(); track $index) {\n @if (column === '__select') {\n <td class=\"tn-table__cell tn-table__select-cell\"\n (click)=\"toggleRowSelection(row); $event.stopPropagation()\">\n <tn-checkbox\n class=\"tn-table__checkbox\"\n [label]=\"'Select row ' + (rowIdx + 1)\"\n [hideLabel]=\"true\"\n [checked]=\"isRowSelected(row)\" />\n </td>\n } @else if (column === '__expand') {\n <td class=\"tn-table__cell tn-table__expand-cell\"\n (click)=\"$event.stopPropagation()\">\n @if (canExpandRow(row)) {\n <button\n type=\"button\"\n class=\"tn-table__expand-button\"\n [attr.aria-expanded]=\"isRowExpanded(row)\"\n [attr.aria-label]=\"isRowExpanded(row) ? 'Collapse row' : 'Expand row'\"\n (click)=\"toggleRowExpansion(row)\">\n <tn-icon\n class=\"tn-table__expand-icon\"\n [name]=\"isRowExpanded(row) ? 'mat-keyboard_arrow_up' : 'mat-keyboard_arrow_down'\" />\n </button>\n }\n </td>\n } @else {\n <td\n class=\"tn-table__cell\"\n [style.width]=\"getColumnDef(column)?.width()\"\n [attr.data-column]=\"column\">\n <div class=\"tn-table__cell-content\">\n @if (getColumnDef(column)?.cellTemplate(); as tmpl) {\n <ng-container\n [ngTemplateOutlet]=\"tmpl\"\n [ngTemplateOutletContext]=\"{ $implicit: row, column: column }\" />\n } @else {\n <span>{{ getCellValue(row, column) }}</span>\n }\n </div>\n </td>\n }\n }\n </tr>\n\n <!-- Detail / Expanded Row -->\n @if (detailRowDef() && canExpandRow(row) && isRowExpanded(row)) {\n <tr class=\"tn-table__detail-row\" [@detailExpand]=\"'expanded'\">\n <td\n class=\"tn-table__detail-cell\"\n [attr.colspan]=\"effectiveDisplayedColumns().length\">\n <ng-container\n [ngTemplateOutlet]=\"detailRowDef()!.template\"\n [ngTemplateOutletContext]=\"{ $implicit: row }\" />\n </td>\n </tr>\n }\n }\n </tbody>\n</table>\n\n@if (data().length === 0 && !loading()) {\n <tn-empty\n size=\"compact\"\n [title]=\"emptyMessage()\"\n [description]=\"emptyDescription()\"\n [icon]=\"emptyIcon()\" />\n}\n\n@if (loading()) {\n <div\n class=\"tn-table__loading-overlay\"\n role=\"status\"\n aria-live=\"polite\"\n [attr.aria-label]=\"loadingMessage()\">\n <tn-spinner [ariaLabel]=\"loadingMessage()\" />\n </div>\n}\n", styles: [":host{display:block;position:relative;width:100%;overflow-x:auto}:host(.tn-table--fixed-layout) table{table-layout:fixed;width:100%}:host(.tn-table--bordered){border:1px solid var(--tn-lines);border-radius:4px}.tn-table__table{width:100%;border-collapse:collapse;border-spacing:0;background-color:var(--tn-bg2);border-radius:4px;overflow:hidden}.tn-table__header{background-color:var(--tn-topbar);color:var(--tn-topbar-txt)}.tn-table__header-row{height:56px}.tn-table__header-cell{padding:0 16px;text-align:left;font-weight:600;font-size:14px;border-bottom:1px solid var(--tn-lines);white-space:nowrap;vertical-align:middle}.tn-table__header-cell--sortable{cursor:pointer;-webkit-user-select:none;user-select:none}.tn-table__header-cell--sortable:hover{background-color:color-mix(in srgb,var(--tn-topbar) 85%,var(--tn-topbar-txt))}.tn-table__sort-container{display:inline-flex;align-items:center;gap:4px}.tn-table__sort-icon{opacity:0;transition:opacity .2s ease}.tn-table__header-cell--sortable:hover .tn-table__sort-icon{opacity:.5}.tn-table__header-cell--sorted .tn-table__sort-icon{opacity:1}.tn-table__body{background-color:var(--tn-bg2)}.tn-table__row{height:48px;transition:background-color .2s ease}.tn-table__row:hover{background-color:var(--tn-alt-bg1)}.tn-table__row:not(:last-child){border-bottom:1px solid var(--tn-lines)}.tn-table__row--expanded{background-color:var(--tn-alt-bg1)}.tn-table__row--active{background-color:var(--tn-table-active-bg, var(--tn-bg3, #333333))}.tn-table__row--active>.tn-table__cell:first-child{box-shadow:inset 3px 0 0 0 var(--tn-table-active-indicator, var(--tn-primary, #007db3))}.tn-table__row--clickable{cursor:pointer}.tn-table__row--clickable:focus-visible{outline:none}.tn-table__row--clickable:focus-visible>.tn-table__cell:first-child{box-shadow:inset 3px 0 0 0 var(--tn-primary),inset 0 0 0 2px var(--tn-primary)}.tn-table__row--active.tn-table__row--clickable:focus-visible>.tn-table__cell:first-child{box-shadow:inset 3px 0 0 0 var(--tn-table-active-indicator, var(--tn-primary, #007db3)),inset 0 0 0 2px var(--tn-primary)}.tn-table__cell{padding:0 16px;font-size:14px;color:var(--tn-fg1);vertical-align:middle;overflow:hidden}.tn-table__cell-content{display:flex;align-items:center;min-height:24px;white-space:normal;overflow-wrap:anywhere;overflow:hidden;padding:4px;margin:-4px}.tn-table__cell-content>*{min-width:0}.tn-table__select-cell{width:48px;padding:0 8px 0 16px;cursor:pointer}.tn-table__checkbox{pointer-events:none}.tn-table__expand-cell{width:48px;padding:0 16px 0 8px;text-align:center}.tn-table__expand-button{background:none;border:none;padding:4px;cursor:pointer;border-radius:4px;display:inline-flex;align-items:center;color:var(--tn-fg2)}.tn-table__expand-button:hover{background-color:var(--tn-alt-bg1)}.tn-table__expand-button:focus-visible{outline:2px solid var(--tn-primary);outline-offset:2px}.tn-table__expand-icon{transition:transform .2s ease}.tn-table__detail-row{background-color:var(--tn-alt-bg1);border-bottom:1px solid var(--tn-lines)}.tn-table__detail-cell{padding:16px}.tn-table__loading-overlay{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;background-color:color-mix(in srgb,var(--tn-bg2, #282828) 65%,transparent);-webkit-backdrop-filter:blur(1px);backdrop-filter:blur(1px);pointer-events:all;z-index:1;border-radius:4px}.tn-table--loading .tn-table__table{pointer-events:none;-webkit-user-select:none;user-select:none}.tn-table--dense .tn-table__header-row{height:40px}.tn-table--dense .tn-table__row{height:32px}.tn-table--dense .tn-table__header-cell,.tn-table--dense .tn-table__cell{padding:0 12px;font-size:13px}@media(max-width:768px){.tn-table__table{font-size:12px}.tn-table__header-cell,.tn-table__cell{padding:0 8px}}@media(prefers-reduced-motion:reduce){.tn-table__sort-icon,.tn-table__expand-icon,.tn-table__row{transition:none}.tn-table__detail-row{animation:none}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1$2.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: TnCheckboxComponent, selector: "tn-checkbox", inputs: ["label", "hideLabel", "disabled", "required", "indeterminate", "testId", "error", "checked"], outputs: ["change"] }, { kind: "component", type: TnEmptyComponent, selector: "tn-empty", inputs: ["title", "description", "icon", "iconLibrary", "iconSize", "actionText", "bordered", "size"], outputs: ["onAction"] }, { kind: "component", type: TnIconComponent, selector: "tn-icon", inputs: ["name", "size", "color", "tooltip", "ariaLabel", "library", "testId", "fullSize", "customSize"] }, { kind: "component", type: TnSpinnerComponent, selector: "tn-spinner", inputs: ["mode", "value", "diameter", "strokeWidth", "ariaLabel", "ariaLabelledby"] }], animations: [
12306
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.0", type: TnTableComponent, isStandalone: true, selector: "tn-table", inputs: { dataSource: { classPropertyName: "dataSource", publicName: "dataSource", isSignal: true, isRequired: false, transformFunction: null }, displayedColumns: { classPropertyName: "displayedColumns", publicName: "displayedColumns", isSignal: true, isRequired: false, transformFunction: null }, trackBy: { classPropertyName: "trackBy", publicName: "trackBy", isSignal: true, isRequired: false, transformFunction: null }, emptyMessage: { classPropertyName: "emptyMessage", publicName: "emptyMessage", isSignal: true, isRequired: false, transformFunction: null }, emptyDescription: { classPropertyName: "emptyDescription", publicName: "emptyDescription", isSignal: true, isRequired: false, transformFunction: null }, emptyIcon: { classPropertyName: "emptyIcon", publicName: "emptyIcon", isSignal: true, isRequired: false, transformFunction: null }, selectable: { classPropertyName: "selectable", publicName: "selectable", isSignal: true, isRequired: false, transformFunction: null }, expandable: { classPropertyName: "expandable", publicName: "expandable", isSignal: true, isRequired: false, transformFunction: null }, bordered: { classPropertyName: "bordered", publicName: "bordered", isSignal: true, isRequired: false, transformFunction: null }, isRowExpandable: { classPropertyName: "isRowExpandable", publicName: "isRowExpandable", isSignal: true, isRequired: false, transformFunction: null }, activeRow: { classPropertyName: "activeRow", publicName: "activeRow", isSignal: true, isRequired: false, transformFunction: null }, activeWhen: { classPropertyName: "activeWhen", publicName: "activeWhen", isSignal: true, isRequired: false, transformFunction: null }, activeBg: { classPropertyName: "activeBg", publicName: "activeBg", isSignal: true, isRequired: false, transformFunction: null }, activeIndicator: { classPropertyName: "activeIndicator", publicName: "activeIndicator", isSignal: true, isRequired: false, transformFunction: null }, loading: { classPropertyName: "loading", publicName: "loading", isSignal: true, isRequired: false, transformFunction: null }, loadingMessage: { classPropertyName: "loadingMessage", publicName: "loadingMessage", isSignal: true, isRequired: false, transformFunction: null }, clickable: { classPropertyName: "clickable", publicName: "clickable", isSignal: true, isRequired: false, transformFunction: null }, expandOnRowClick: { classPropertyName: "expandOnRowClick", publicName: "expandOnRowClick", isSignal: true, isRequired: false, transformFunction: null }, singleExpand: { classPropertyName: "singleExpand", publicName: "singleExpand", isSignal: true, isRequired: false, transformFunction: null }, fixedLayout: { classPropertyName: "fixedLayout", publicName: "fixedLayout", isSignal: true, isRequired: false, transformFunction: null }, minColumnWidth: { classPropertyName: "minColumnWidth", publicName: "minColumnWidth", isSignal: true, isRequired: false, transformFunction: null }, minWidth: { classPropertyName: "minWidth", publicName: "minWidth", isSignal: true, isRequired: false, transformFunction: null }, mobileLayout: { classPropertyName: "mobileLayout", publicName: "mobileLayout", isSignal: true, isRequired: false, transformFunction: null }, cardBreakpoint: { classPropertyName: "cardBreakpoint", publicName: "cardBreakpoint", isSignal: true, isRequired: false, transformFunction: null }, cardPrimaryCount: { classPropertyName: "cardPrimaryCount", publicName: "cardPrimaryCount", isSignal: true, isRequired: false, transformFunction: null }, sortColumn: { classPropertyName: "sortColumn", publicName: "sortColumn", isSignal: true, isRequired: false, transformFunction: null }, sortDirection: { classPropertyName: "sortDirection", publicName: "sortDirection", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { sortChange: "sortChange", selectionChange: "selectionChange", rowClick: "rowClick", rowDoubleClick: "rowDoubleClick", sortColumn: "sortColumnChange", sortDirection: "sortDirectionChange" }, host: { properties: { "class.tn-table--bordered": "bordered()", "class.tn-table--fixed-layout": "fixedLayout()", "class.tn-table--loading": "loading()", "class.tn-table--cards": "isCardMode()", "class.tn-table--scroll": "isScrollMode()", "style.--tn-table-active-bg": "activeBg()", "style.--tn-table-active-indicator": "activeIndicator()" }, classAttribute: "tn-table" }, queries: [{ propertyName: "columnDefs", predicate: TnTableColumnDirective, isSignal: true }, { propertyName: "detailRowDef", first: true, predicate: TnDetailRowDefDirective, descendants: true, isSignal: true }, { propertyName: "rowActionsDef", first: true, predicate: TnRowActionsDefDirective, descendants: true, isSignal: true }], hostDirectives: [{ directive: TnTestIdDirective, inputs: ["tnTestId", "testId"] }], ngImport: i0, template: "@if (isCardMode()) {\n <!-- Card layout (narrow container) -->\n @if (selectable() || sortableColumns().length > 0) {\n <div class=\"tn-table__cards-toolbar\" [attr.inert]=\"loading() ? '' : null\">\n @if (selectable()) {\n <!--\n A plain <div>, not a <label>: tn-checkbox renders its own <label for>,\n and nesting one label inside another is invalid. The visible text is\n passed through the checkbox's own `label` input so it stays associated.\n\n The handler goes on THIS wrapper, not on <tn-checkbox>: the checkbox is\n `pointer-events: none` (see `__checkbox` in the stylesheet), so it and its\n subtree are outside hit testing and a real click lands here instead. A\n handler bound on the checkbox is never in the event path \u2014 which is how the\n label -> div change silently killed mouse and touch selection while keyboard\n kept working, since Space on the hidden input dispatches its own bubbling\n click. Table mode binds the same handler on its <td> for this reason.\n\n The click/key and focusable lint rules are suppressed rather than satisfied:\n this wrapper is a pointer hit area around a control that is already\n focusable and already handles Space. Giving it `tabindex` and a key handler\n would add a second tab stop and a second activation path for the same\n checkbox, which is worse than the rule it silences.\n -->\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events, @angular-eslint/template/interactive-supports-focus -->\n <div\n class=\"tn-table__cards-selectall\"\n (click)=\"toggleSelectAll(); $event.preventDefault()\">\n <tn-checkbox\n class=\"tn-table__checkbox\"\n label=\"Select all\"\n [checked]=\"isAllSelected()\"\n [indeterminate]=\"isIndeterminate()\" />\n </div>\n }\n @if (sortableColumns().length > 0) {\n <div class=\"tn-table__cards-sort\">\n <tn-icon name=\"mat-sort\" size=\"sm\" aria-hidden=\"true\" />\n <select\n class=\"tn-table__cards-sort-select\"\n aria-label=\"Sort by\"\n [value]=\"sortColumn()\"\n (change)=\"onSortSelectChange($event)\">\n <!--\n `[selected]` on the options, not just `[value]` on the <select>:\n the select's value is bound before the @for options exist, so the\n browser discards it and the binding is never dirty again \u2014 a table\n sorted before card mode engaged would render as \"Unsorted\".\n -->\n <option value=\"\" [selected]=\"!sortColumn()\">Unsorted</option>\n @for (column of sortableColumns(); track column) {\n <option [value]=\"column\" [selected]=\"column === sortColumn()\">\n {{ getCardLabel(column) }}\n </option>\n }\n </select>\n @if (canSortActiveColumn()) {\n <!--\n Gated on the column being genuinely sortable, not merely active:\n `sortableColumns` keeps a non-sortable active column listed so the sort stays\n clearable, and this toggle must not ride along \u2014 card mode would otherwise\n reorder by a column whose table header ignores clicks.\n\n Three states, not two. A column with an empty direction is not sorted \u2014\n reachable through the two-way binding \u2014 and this control used to render an\n up arrow labelled \"Sort descending\" for it, while table mode showed no\n arrow and no `aria-sort` for the same state. It keeps rendering, because\n it is how the user escapes that state, but shows the neutral\n `mat-unfold_more` the table header already uses for unsorted columns.\n\n The label names the action, so it is \"Sort ascending\" both when unsorted\n and when descending \u2014 `toggleSortDirection()` starts at ascending.\n -->\n <button\n type=\"button\"\n class=\"tn-table__cards-sort-dir\"\n [attr.aria-label]=\"sortDirection() === 'asc' ? 'Sort descending' : 'Sort ascending'\"\n [attr.data-sort-direction]=\"sortDirection()\"\n (click)=\"toggleSortDirection()\">\n @if (sortDirection() === 'asc') {\n <tn-icon size=\"sm\" name=\"mat-arrow_upward\" aria-hidden=\"true\" />\n } @else if (sortDirection() === 'desc') {\n <tn-icon size=\"sm\" name=\"mat-arrow_downward\" aria-hidden=\"true\" />\n } @else {\n <tn-icon size=\"sm\" name=\"mat-unfold_more\" aria-hidden=\"true\" />\n }\n </button>\n }\n </div>\n }\n </div>\n }\n\n <!--\n Decorative `tn-icon`s carry `aria-hidden=\"true\"`. tn-icon renders `role=\"img\"` and\n falls back to its `name` for the accessible name, so without this the raw token\n leaks: a standalone \"mat-sort\" image beside Select all, and disclosures announced\n as \"More fields mat-keyboard_arrow_down\". The repo convention is the same \u2014 see\n button, card, input and file-picker-popup.\n -->\n <ng-template #cardField let-column let-row=\"row\">\n <div class=\"tn-table__card-field\" [attr.data-column]=\"column\">\n <dt class=\"tn-table__card-field-label\">{{ getCardLabel(column) }}</dt>\n <dd class=\"tn-table__card-field-value\">\n @if (getColumnDef(column)?.cellTemplate(); as tmpl) {\n <ng-container\n [ngTemplateOutlet]=\"tmpl\"\n [ngTemplateOutletContext]=\"{ $implicit: row, column: column }\" />\n } @else {\n {{ getCellValue(row, column) }}\n }\n </dd>\n </div>\n </ng-template>\n\n <div class=\"tn-table__cards\" role=\"list\" [attr.inert]=\"loading() ? '' : null\">\n <!--\n Card ARIA differs from the table row on purpose, because `listitem` supports\n far less than `row` does:\n - Active state uses `aria-current`, not `aria-selected`, which `listitem`\n does not support (an earlier revision set it here and it was dropped for\n that reason; axe reports it as a critical aria-allowed-attr violation).\n - Selection state stays on the checkbox, which is a real control.\n - `aria-expanded` IS permitted here \u2014 `listitem` is a subclass of the\n abstract `section` role, and `section` supports `aria-expanded`, so\n `listitem` inherits it (axe-core's own role table lists it in\n `listitem`'s allowedAttrs alongside aria-level/posinset/setsize). So the\n card carries it whenever the card itself acts as the expand trigger,\n matching what table mode does for its rows. The \"Details\" button reports\n the same state unconditionally, because it is the control a screen-reader\n user actually tabs to \u2014 see the note above it.\n The card is deliberately NOT `role=\"button\"`: that role has presentational\n children, which would hide the card's checkbox, row actions and \"Details\"\n toggle from assistive tech entirely.\n -->\n @for (row of data(); track trackByFn()($index, row); let rowIdx = $index) {\n <div\n class=\"tn-table__card\"\n role=\"listitem\"\n [attr.data-row-index]=\"rowIdx\"\n [class.tn-table__card--active]=\"isRowActive(row)\"\n [class.tn-table__card--clickable]=\"clickable()\"\n [attr.tabindex]=\"clickable() ? 0 : null\"\n [attr.aria-current]=\"isRowActive(row) ? 'true' : null\"\n [attr.aria-expanded]=\"isCardExpandTrigger(row) ? isRowExpanded(row) : null\"\n [attr.aria-controls]=\"isCardExpandTrigger(row) && isRowExpanded(row) ? detailPanelId(rowIdx) : null\"\n (click)=\"onCardClick($event, row)\"\n (dblclick)=\"onCardDoubleClick($event, row)\"\n (keydown)=\"onCardKeydown($event, row)\">\n <div class=\"tn-table__card-header\">\n @if (selectable()) {\n <!--\n <div>, not <label>, and the handler is on the wrapper rather than the\n checkbox \u2014 see the toolbar's select-all above for both reasons.\n -->\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events, @angular-eslint/template/interactive-supports-focus -->\n <div\n class=\"tn-table__card-select\"\n (click)=\"toggleRowSelection(row); $event.preventDefault(); $event.stopPropagation()\">\n <!--\n Named from the card's title, not \"Select row N\": with `hideLabel` this is the\n checkbox's entire accessible name, and a card list has no visible row numbers\n to match it against. Falls back to the index when the title column renders\n derived data through a `tnCellDef` and `getCellValue` has nothing to return \u2014\n the same case `getCardLabel()` already falls back for.\n -->\n <tn-checkbox\n class=\"tn-table__checkbox\"\n [label]=\"\n 'Select ' + (getCellValue(row, cardTitleColumn()) ?? 'row ' + (rowIdx + 1))\n \"\n [hideLabel]=\"true\"\n [checked]=\"isRowSelected(row)\" />\n </div>\n }\n <span class=\"tn-table__card-title\">\n @if (getColumnDef(cardTitleColumn())?.cellTemplate(); as tmpl) {\n <ng-container\n [ngTemplateOutlet]=\"tmpl\"\n [ngTemplateOutletContext]=\"{ $implicit: row, column: cardTitleColumn() }\" />\n } @else {\n {{ getCellValue(row, cardTitleColumn()) }}\n }\n </span>\n @if (rowActionsDef()) {\n <div class=\"tn-table__card-actions\">\n <ng-container\n [ngTemplateOutlet]=\"rowActionsDef()!.template\"\n [ngTemplateOutletContext]=\"{ $implicit: row }\" />\n </div>\n }\n </div>\n\n <dl class=\"tn-table__card-fields\">\n @for (column of cardPrimaryColumns(); track column) {\n <ng-container\n [ngTemplateOutlet]=\"cardField\"\n [ngTemplateOutletContext]=\"{ $implicit: column, row: row }\" />\n }\n </dl>\n\n <!--\n The disclosure sits outside the primary <dl> and wraps its fields in a\n <dl> of its own: <details> is not a permitted child of <dl>, so nesting\n it there detached the folded dt/dd pairs from any description list.\n -->\n @if (cardSecondaryColumns().length > 0) {\n <details class=\"tn-table__card-more\">\n <summary class=\"tn-table__card-more-summary\">\n <span>More fields</span>\n <tn-icon size=\"sm\" name=\"mat-keyboard_arrow_down\" aria-hidden=\"true\" />\n </summary>\n <dl class=\"tn-table__card-fields\">\n @for (column of cardSecondaryColumns(); track column) {\n <ng-container\n [ngTemplateOutlet]=\"cardField\"\n [ngTemplateOutletContext]=\"{ $implicit: column, row: row }\" />\n }\n </dl>\n </details>\n }\n\n @if (canExpandRow(row) && detailRowDef()) {\n <!--\n The button always carries the state, even when the card is also a trigger.\n An earlier revision suppressed it here to avoid announcing the same panel\n twice, but that muted the primary control: this button is what a\n screen-reader user reaches by tabbing through controls, and it was\n announced as a bare \"Details, button\" with no expanded state, before or\n after activation. The state sat on the `role=\"listitem\"` card, heard only\n if the user happened to focus the card itself.\n\n Two controls that each toggle the same region and each report its state is\n the disclosure pattern applied twice \u2014 redundant, not invalid. Redundancy\n is the cheaper failure.\n -->\n <button\n type=\"button\"\n class=\"tn-table__card-detail-toggle\"\n [attr.aria-expanded]=\"isRowExpanded(row)\"\n [attr.aria-controls]=\"isRowExpanded(row) ? detailPanelId(rowIdx) : null\"\n (click)=\"toggleRowExpansion(row); $event.stopPropagation()\">\n <span>Details</span>\n <tn-icon\n size=\"sm\"\n aria-hidden=\"true\"\n [name]=\"isRowExpanded(row) ? 'mat-keyboard_arrow_up' : 'mat-keyboard_arrow_down'\" />\n </button>\n @if (isRowExpanded(row)) {\n <div class=\"tn-table__card-detail\" [attr.id]=\"detailPanelId(rowIdx)\">\n <ng-container\n [ngTemplateOutlet]=\"detailRowDef()!.template\"\n [ngTemplateOutletContext]=\"{ $implicit: row }\" />\n </div>\n }\n }\n </div>\n }\n </div>\n} @else {\n <table\n class=\"tn-table__table\"\n [style.min-width]=\"resolvedMinWidth()\"\n [attr.inert]=\"loading() ? '' : null\">\n <!-- Header Row -->\n <thead class=\"tn-table__header\">\n <tr class=\"tn-table__header-row\">\n @for (column of effectiveDisplayedColumns(); track $index) {\n @if (column === '__select') {\n <th class=\"tn-table__header-cell tn-table__select-cell\"\n role=\"checkbox\"\n tabindex=\"0\"\n [attr.aria-checked]=\"isAllSelected()\"\n (click)=\"toggleSelectAll()\"\n (keydown.enter)=\"toggleSelectAll()\"\n (keydown.space)=\"toggleSelectAll(); $event.preventDefault()\">\n <tn-checkbox\n class=\"tn-table__checkbox\"\n label=\"Select all rows\"\n [hideLabel]=\"true\"\n [checked]=\"isAllSelected()\"\n [indeterminate]=\"isIndeterminate()\" />\n </th>\n } @else if (column === '__expand') {\n <th class=\"tn-table__header-cell tn-table__expand-cell\">\n <span class=\"cdk-visually-hidden\">Expand</span>\n </th>\n } @else {\n <th\n class=\"tn-table__header-cell\"\n [class.tn-table__header-cell--sortable]=\"getColumnDef(column)?.sortable()\"\n [class.tn-table__header-cell--sorted]=\"isSorted(column)\"\n [style.width]=\"getColumnDef(column)?.width()\"\n [attr.aria-sort]=\"\n isSorted(column)\n ? (sortDirection() === 'asc' ? 'ascending' : 'descending')\n : (getColumnDef(column)?.sortable() ? 'none' : null)\n \"\n [attr.tabindex]=\"getColumnDef(column)?.sortable() ? 0 : null\"\n [attr.data-column]=\"column\"\n (click)=\"onSortHeaderClick(column, $event)\"\n (keydown.enter)=\"onSortHeaderKeydown(column, $event)\"\n (keydown.space)=\"onSortHeaderKeydown(column, $event)\">\n <span class=\"tn-table__sort-container\">\n <span class=\"tn-table__header-text\">\n @if (getColumnDef(column)?.headerTemplate(); as tmpl) {\n <ng-container [ngTemplateOutlet]=\"tmpl\" />\n } @else {\n {{ getColumnDef(column)?.label() ?? column }}\n }\n </span>\n @if (getColumnDef(column)?.sortable()) {\n <!--\n aria-hidden: the <th> has no aria-label, so its accessible name is\n computed from its contents \u2014 without this, every sortable column\n announced as \"Name mat-unfold_more\". `aria-sort` on the <th> already\n carries the state properly, so the token is pure noise.\n -->\n <tn-icon\n class=\"tn-table__sort-icon\"\n size=\"sm\"\n aria-hidden=\"true\"\n [name]=\"\n isSorted(column)\n ? (sortDirection() === 'asc' ? 'mat-arrow_upward' : 'mat-arrow_downward')\n : 'mat-unfold_more'\n \" />\n }\n </span>\n </th>\n }\n }\n @if (rowActionsDef()) {\n <th class=\"tn-table__header-cell tn-table__actions-cell\">\n <span class=\"cdk-visually-hidden\">Actions</span>\n </th>\n }\n </tr>\n </thead>\n\n <!-- Data Rows -->\n <tbody class=\"tn-table__body\">\n @for (row of data(); track trackByFn()($index, row); let rowIdx = $index) {\n <tr\n class=\"tn-table__row\"\n [attr.data-row-index]=\"rowIdx\"\n [class.tn-table__row--expandable]=\"canExpandRow(row)\"\n [class.tn-table__row--expanded]=\"canExpandRow(row) && isRowExpanded(row)\"\n [class.tn-table__row--active]=\"isRowActive(row)\"\n [class.tn-table__row--clickable]=\"clickable()\"\n [attr.tabindex]=\"clickable() ? 0 : null\"\n [attr.aria-selected]=\"clickable() ? isRowActive(row) : null\"\n [attr.aria-expanded]=\"isRowExpandTrigger(row) ? isRowExpanded(row) : null\"\n (click)=\"onRowClick(row, $event)\"\n (dblclick)=\"onRowDoubleClick(row, $event)\"\n (keydown)=\"onRowKeydown($event, row)\">\n @for (column of effectiveDisplayedColumns(); track $index) {\n @if (column === '__select') {\n <td class=\"tn-table__cell tn-table__select-cell\"\n (click)=\"toggleRowSelection(row); $event.stopPropagation()\">\n <tn-checkbox\n class=\"tn-table__checkbox\"\n [label]=\"'Select row ' + (rowIdx + 1)\"\n [hideLabel]=\"true\"\n [checked]=\"isRowSelected(row)\" />\n </td>\n } @else if (column === '__expand') {\n <td class=\"tn-table__cell tn-table__expand-cell\"\n (click)=\"$event.stopPropagation()\">\n @if (canExpandRow(row)) {\n <button\n type=\"button\"\n class=\"tn-table__expand-button\"\n [attr.aria-expanded]=\"isRowExpanded(row)\"\n [attr.aria-label]=\"isRowExpanded(row) ? 'Collapse row' : 'Expand row'\"\n (click)=\"toggleRowExpansion(row)\">\n <tn-icon\n class=\"tn-table__expand-icon\"\n aria-hidden=\"true\"\n [name]=\"isRowExpanded(row) ? 'mat-keyboard_arrow_up' : 'mat-keyboard_arrow_down'\" />\n </button>\n }\n </td>\n } @else {\n <td\n class=\"tn-table__cell\"\n [style.width]=\"getColumnDef(column)?.width()\"\n [attr.data-column]=\"column\">\n <div class=\"tn-table__cell-content\">\n @if (getColumnDef(column)?.cellTemplate(); as tmpl) {\n <ng-container\n [ngTemplateOutlet]=\"tmpl\"\n [ngTemplateOutletContext]=\"{ $implicit: row, column: column }\" />\n } @else {\n <span>{{ getCellValue(row, column) }}</span>\n }\n </div>\n </td>\n }\n }\n @if (rowActionsDef()) {\n <td class=\"tn-table__cell tn-table__actions-cell\"\n (click)=\"$event.stopPropagation()\">\n <div class=\"tn-table__actions-content\">\n <ng-container\n [ngTemplateOutlet]=\"rowActionsDef()!.template\"\n [ngTemplateOutletContext]=\"{ $implicit: row }\" />\n </div>\n </td>\n }\n </tr>\n\n <!-- Detail / Expanded Row -->\n @if (detailRowDef() && canExpandRow(row) && isRowExpanded(row)) {\n <tr class=\"tn-table__detail-row\" [@detailExpand]=\"'expanded'\">\n <td\n class=\"tn-table__detail-cell\"\n [attr.colspan]=\"totalColumnCount()\">\n <ng-container\n [ngTemplateOutlet]=\"detailRowDef()!.template\"\n [ngTemplateOutletContext]=\"{ $implicit: row }\" />\n </td>\n </tr>\n }\n }\n </tbody>\n </table>\n}\n\n@if (data().length === 0 && !loading()) {\n <tn-empty\n size=\"compact\"\n [title]=\"emptyMessage()\"\n [description]=\"emptyDescription()\"\n [icon]=\"emptyIcon()\" />\n}\n\n@if (loading()) {\n <div\n class=\"tn-table__loading-overlay\"\n role=\"status\"\n aria-live=\"polite\"\n [attr.aria-label]=\"loadingMessage()\">\n <tn-spinner [ariaLabel]=\"loadingMessage()\" />\n </div>\n}\n", styles: [":host{display:block;position:relative;width:100%;overflow-x:auto;border-radius:4px}:host{--tn-table-select-width: 72px;--tn-table-actions-width: 128px;--tn-table-expand-width: 56px}.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}:host(.tn-table--fixed-layout) table{table-layout:fixed;width:100%}:host(.tn-table--fixed-layout) .tn-table__actions-cell{box-sizing:border-box;width:var(--tn-table-actions-width)}:host(.tn-table--bordered){border:1px solid var(--tn-lines);border-radius:4px}:host(.tn-table--loading) .tn-table__table,:host(.tn-table--loading) .tn-table__cards,:host(.tn-table--loading) .tn-table__cards-toolbar{pointer-events:none;-webkit-user-select:none;user-select:none}:host(.tn-table--scroll) .tn-table__select-cell{position:sticky;left:0}:host(.tn-table--scroll) .tn-table__header-cell:first-child,:host(.tn-table--scroll) .tn-table__row>.tn-table__cell:first-child,:host(.tn-table--scroll) .tn-table__select-cell+.tn-table__header-cell,:host(.tn-table--scroll) .tn-table__row>.tn-table__select-cell+.tn-table__cell{position:sticky;left:0;z-index:1}:host(.tn-table--scroll) .tn-table__actions-cell{position:sticky;right:0;z-index:3}:host(.tn-table--scroll) .tn-table__header-row>.tn-table__header-cell.tn-table__select-cell,:host(.tn-table--scroll) .tn-table__row>.tn-table__cell.tn-table__select-cell{z-index:2}:host(.tn-table--scroll) .tn-table__select-cell+.tn-table__header-cell,:host(.tn-table--scroll) .tn-table__row>.tn-table__select-cell+.tn-table__cell{left:var(--tn-table-select-width)}:host(.tn-table--scroll) .tn-table__row>.tn-table__cell:first-child,:host(.tn-table--scroll) .tn-table__row>.tn-table__select-cell+.tn-table__cell{background-color:inherit}:host(.tn-table--scroll) .tn-table__header-row .tn-table__header-cell:first-child:not(.tn-table__header-cell--sortable:hover),:host(.tn-table--scroll) .tn-table__header-row .tn-table__select-cell+.tn-table__header-cell:not(.tn-table__header-cell--sortable:hover){background-color:var(--tn-topbar)}.tn-table__table{width:100%;border-collapse:collapse;border-spacing:0;background-color:var(--tn-bg2)}.tn-table__header{background-color:var(--tn-topbar);color:var(--tn-topbar-txt)}.tn-table__header-row{height:56px}.tn-table__header-cell{padding:0 16px;text-align:left;font-weight:600;font-size:14px;border-bottom:1px solid var(--tn-lines);white-space:nowrap;vertical-align:middle}.tn-table__header-cell--sortable{cursor:pointer;-webkit-user-select:none;user-select:none}.tn-table__header-cell--sortable:hover{background-color:color-mix(in srgb,var(--tn-topbar) 85%,var(--tn-topbar-txt))}.tn-table__sort-container{display:inline-flex;align-items:center;gap:4px}.tn-table__sort-icon{opacity:0;transition:opacity .2s ease}.tn-table__header-cell--sortable:hover .tn-table__sort-icon,.tn-table__header-cell--sortable:focus-visible .tn-table__sort-icon{opacity:.5}.tn-table__header-cell--sorted .tn-table__sort-icon{opacity:1}.tn-table__body{background-color:var(--tn-bg2)}.tn-table__row{height:48px;transition:background-color .2s ease;background-color:var(--tn-bg2)}.tn-table__row:hover{background-color:var(--tn-alt-bg1)}.tn-table__row:not(:last-child){border-bottom:1px solid var(--tn-lines)}.tn-table__row--expanded{background-color:var(--tn-alt-bg1)}.tn-table__row--active{background-color:var(--tn-table-active-bg, var(--tn-bg3, #333333))}.tn-table__row--active>.tn-table__cell:first-child{box-shadow:inset 3px 0 0 0 var(--tn-table-active-indicator, var(--tn-primary, #007db3))}.tn-table__row--clickable{cursor:pointer}.tn-table__row--clickable:focus-visible{outline:none}.tn-table__row--clickable:focus-visible>.tn-table__cell:first-child{box-shadow:inset 3px 0 0 0 var(--tn-primary),inset 0 0 0 2px var(--tn-primary)}.tn-table__row--active.tn-table__row--clickable:focus-visible>.tn-table__cell:first-child{box-shadow:inset 3px 0 0 0 var(--tn-table-active-indicator, var(--tn-primary, #007db3)),inset 0 0 0 2px var(--tn-primary)}.tn-table__cell{padding:0 16px;font-size:14px;color:var(--tn-fg1);vertical-align:middle;overflow:hidden}.tn-table__cell-content{display:flex;align-items:center;min-height:24px;white-space:normal;overflow-wrap:anywhere;overflow:hidden;padding:4px;margin:-4px}.tn-table__cell-content>*{min-width:0}.tn-table__select-cell{box-sizing:border-box;width:var(--tn-table-select-width);padding:0 8px 0 16px;cursor:pointer}.tn-table__checkbox{pointer-events:none}.tn-table__expand-cell{box-sizing:border-box;width:var(--tn-table-expand-width);padding:0 16px 0 8px;text-align:center}.tn-table__expand-button{background:none;border:none;padding:4px;cursor:pointer;border-radius:4px;display:inline-flex;align-items:center;color:var(--tn-fg2)}.tn-table__expand-button:hover{background-color:var(--tn-alt-bg1)}.tn-table__expand-button:focus-visible{outline:2px solid var(--tn-primary);outline-offset:2px}.tn-table__expand-icon{transition:transform .2s ease}.tn-table__detail-row{background-color:var(--tn-alt-bg1);border-bottom:1px solid var(--tn-lines)}.tn-table__detail-cell{padding:16px}.tn-table__actions-cell{width:1%;white-space:nowrap;text-align:right;background-color:inherit;border-left:1px solid var(--tn-lines)}.tn-table__header .tn-table__actions-cell{background-color:var(--tn-topbar)}.tn-table__actions-content{display:inline-flex;align-items:center;gap:4px}.tn-table__loading-overlay{position:absolute;inset:0;z-index:4;display:flex;align-items:center;justify-content:center;background-color:color-mix(in srgb,var(--tn-bg2, #282828) 65%,transparent);-webkit-backdrop-filter:blur(1px);backdrop-filter:blur(1px);pointer-events:all;border-radius:4px}.tn-table--dense .tn-table__header-row{height:40px}.tn-table--dense .tn-table__row{height:32px}.tn-table--dense .tn-table__header-cell,.tn-table--dense .tn-table__cell{padding:0 12px;font-size:13px}.tn-table__cards-toolbar{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:8px 4px 12px}.tn-table__cards-selectall{display:inline-flex;align-items:center;gap:8px;font-size:14px;color:var(--tn-fg1);cursor:pointer}.tn-table__cards-sort{display:inline-flex;align-items:center;gap:6px;margin-left:auto;color:var(--tn-fg2)}.tn-table__cards-sort-select{height:32px;padding:0 8px;border:1px solid var(--tn-lines);border-radius:4px;background-color:var(--tn-bg2);color:var(--tn-fg1);font-size:14px}.tn-table__cards-sort-select:focus-visible{outline:2px solid var(--tn-primary);outline-offset:2px}.tn-table__cards-sort-dir{display:inline-flex;align-items:center;justify-content:center;width:32px;height:32px;border:1px solid var(--tn-lines);border-radius:4px;background:none;color:var(--tn-fg2);cursor:pointer}.tn-table__cards-sort-dir:hover{background-color:var(--tn-alt-bg1)}.tn-table__cards-sort-dir:focus-visible{outline:2px solid var(--tn-primary);outline-offset:2px}.tn-table__cards{display:flex;flex-direction:column;gap:8px}.tn-table__card{background-color:var(--tn-bg2);border:1px solid var(--tn-lines);border-radius:6px;padding:12px}.tn-table__card--active{background-color:var(--tn-table-active-bg, var(--tn-bg3, #333333));box-shadow:inset 3px 0 0 0 var(--tn-table-active-indicator, var(--tn-primary, #007db3))}.tn-table__card--clickable{cursor:pointer}.tn-table__card--clickable:focus-visible{outline:none;box-shadow:inset 0 0 0 2px var(--tn-primary)}.tn-table__card--active.tn-table__card--clickable:focus-visible{box-shadow:inset 3px 0 0 0 var(--tn-table-active-indicator, var(--tn-primary, #007db3)),inset 0 0 0 2px var(--tn-primary)}.tn-table__card-header{display:flex;align-items:center;gap:10px}.tn-table__card-select{display:inline-flex;align-items:center;flex-shrink:0;cursor:pointer}.tn-table__card-title{flex:1;min-width:0;font-weight:600;font-size:15px;color:var(--tn-fg1);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.tn-table__card-actions{display:inline-flex;align-items:center;gap:4px;flex-shrink:0}.tn-table__card-fields{margin:0;padding:10px 0 0}.tn-table__card-field{display:flex;align-items:baseline;justify-content:space-between;gap:12px;padding:3px 0;font-size:13px}.tn-table__card-field-label{flex-shrink:0;color:var(--tn-fg2)}.tn-table__card-field-value{margin:0;min-width:0;text-align:right;color:var(--tn-fg1);overflow-wrap:anywhere}.tn-table__card-more[open]>.tn-table__card-more-summary tn-icon{transform:rotate(180deg)}.tn-table__card-more{margin-top:2px}.tn-table__card-more-summary{display:inline-flex;align-items:center;gap:4px;padding:4px 0;font-size:13px;color:var(--tn-fg1);cursor:pointer;list-style:none}.tn-table__card-more-summary::-webkit-details-marker{display:none}.tn-table__card-more-summary tn-icon{transition:transform .2s ease}.tn-table__card-more-summary:focus-visible{outline:2px solid var(--tn-primary);outline-offset:2px}.tn-table__card-detail-toggle{display:inline-flex;align-items:center;gap:4px;margin-top:8px;padding:4px 0;background:none;border:none;color:var(--tn-fg1);font-size:13px;cursor:pointer}.tn-table__card-detail-toggle:focus-visible{outline:2px solid var(--tn-primary);outline-offset:2px}.tn-table__card-detail{margin-top:8px;padding-top:8px;border-top:1px solid var(--tn-lines)}@media(max-width:768px){.tn-table__table{font-size:12px}.tn-table__header-cell,.tn-table__cell{padding:0 8px}}@media(prefers-reduced-motion:reduce){.tn-table__card-more-summary tn-icon,.tn-table__sort-icon,.tn-table__expand-icon,.tn-table__row{transition:none}.tn-table__detail-row{animation:none}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1$2.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: TnCheckboxComponent, selector: "tn-checkbox", inputs: ["label", "hideLabel", "disabled", "required", "indeterminate", "testId", "error", "checked"], outputs: ["change"] }, { kind: "component", type: TnEmptyComponent, selector: "tn-empty", inputs: ["title", "description", "icon", "iconLibrary", "iconSize", "actionText", "bordered", "size"], outputs: ["onAction"] }, { kind: "component", type: TnIconComponent, selector: "tn-icon", inputs: ["name", "size", "color", "tooltip", "ariaLabel", "library", "testId", "fullSize", "customSize"] }, { kind: "component", type: TnSpinnerComponent, selector: "tn-spinner", inputs: ["mode", "value", "diameter", "strokeWidth", "ariaLabel", "ariaLabelledby"] }], animations: [
11609
12307
  trigger('detailExpand', [
11610
12308
  state('collapsed,void', style({ height: '0px', minHeight: '0', overflow: 'hidden' })),
11611
12309
  state('expanded', style({ height: '*' })),
@@ -11626,10 +12324,17 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
11626
12324
  '[class.tn-table--bordered]': 'bordered()',
11627
12325
  '[class.tn-table--fixed-layout]': 'fixedLayout()',
11628
12326
  '[class.tn-table--loading]': 'loading()',
12327
+ // `--cards` is a state hook only: card mode is styled through the
12328
+ // `__cards*` element classes, not from the host. It is kept as the
12329
+ // documented way for consumers and tests to detect the active layout
12330
+ // without reaching for internal element classes. `--scroll` is both a
12331
+ // hook and the selector the pinned-column rules key off.
12332
+ '[class.tn-table--cards]': 'isCardMode()',
12333
+ '[class.tn-table--scroll]': 'isScrollMode()',
11629
12334
  '[style.--tn-table-active-bg]': 'activeBg()',
11630
12335
  '[style.--tn-table-active-indicator]': 'activeIndicator()',
11631
- }, template: "<table class=\"tn-table__table\" [style.min-width]=\"resolvedMinWidth()\">\n <!-- Header Row -->\n <thead class=\"tn-table__header\">\n <tr class=\"tn-table__header-row\">\n @for (column of effectiveDisplayedColumns(); track $index) {\n @if (column === '__select') {\n <th class=\"tn-table__header-cell tn-table__select-cell\"\n role=\"checkbox\"\n tabindex=\"0\"\n [attr.aria-checked]=\"isAllSelected()\"\n (click)=\"toggleSelectAll()\"\n (keydown.enter)=\"toggleSelectAll()\"\n (keydown.space)=\"toggleSelectAll(); $event.preventDefault()\">\n <tn-checkbox\n class=\"tn-table__checkbox\"\n label=\"Select all rows\"\n [hideLabel]=\"true\"\n [checked]=\"isAllSelected()\"\n [indeterminate]=\"isIndeterminate()\" />\n </th>\n } @else if (column === '__expand') {\n <th class=\"tn-table__header-cell tn-table__expand-cell\">\n <span class=\"cdk-visually-hidden\">Expand</span>\n </th>\n } @else {\n <th\n class=\"tn-table__header-cell\"\n [class.tn-table__header-cell--sortable]=\"getColumnDef(column)?.sortable()\"\n [class.tn-table__header-cell--sorted]=\"isSorted(column)\"\n [style.width]=\"getColumnDef(column)?.width()\"\n [attr.aria-sort]=\"\n isSorted(column)\n ? sortDirection() === 'asc' ? 'ascending' : 'descending'\n : null\n \"\n [attr.tabindex]=\"getColumnDef(column)?.sortable() ? 0 : null\"\n [attr.data-column]=\"column\"\n (click)=\"getColumnDef(column)?.sortable() && onSortClick(column)\"\n (keydown.enter)=\"onSortClick(column)\"\n (keydown.space)=\"onSortClick(column); $event.preventDefault()\">\n <span class=\"tn-table__sort-container\">\n <span class=\"tn-table__header-text\">\n @if (getColumnDef(column)?.headerTemplate(); as tmpl) {\n <ng-container [ngTemplateOutlet]=\"tmpl\" />\n } @else {\n {{ column }}\n }\n </span>\n @if (getColumnDef(column)?.sortable()) {\n <tn-icon\n class=\"tn-table__sort-icon\"\n size=\"sm\"\n [name]=\"\n isSorted(column)\n ? (sortDirection() === 'asc' ? 'mat-arrow_upward' : 'mat-arrow_downward')\n : 'mat-unfold_more'\n \" />\n }\n </span>\n </th>\n }\n }\n </tr>\n </thead>\n\n <!-- Data Rows -->\n <tbody class=\"tn-table__body\">\n @for (row of data(); track trackByFn()($index, row); let rowIdx = $index) {\n <tr\n class=\"tn-table__row\"\n [attr.data-row-index]=\"rowIdx\"\n [class.tn-table__row--expandable]=\"canExpandRow(row)\"\n [class.tn-table__row--expanded]=\"canExpandRow(row) && isRowExpanded(row)\"\n [class.tn-table__row--active]=\"isRowActive(row)\"\n [class.tn-table__row--clickable]=\"clickable()\"\n [attr.tabindex]=\"clickable() ? 0 : null\"\n [attr.aria-selected]=\"clickable() ? isRowActive(row) : null\"\n [attr.aria-expanded]=\"isRowExpandTrigger(row) ? isRowExpanded(row) : null\"\n (click)=\"onRowClick(row)\"\n (dblclick)=\"onRowDoubleClick(row)\"\n (keydown)=\"onRowKeydown($event, row)\">\n @for (column of effectiveDisplayedColumns(); track $index) {\n @if (column === '__select') {\n <td class=\"tn-table__cell tn-table__select-cell\"\n (click)=\"toggleRowSelection(row); $event.stopPropagation()\">\n <tn-checkbox\n class=\"tn-table__checkbox\"\n [label]=\"'Select row ' + (rowIdx + 1)\"\n [hideLabel]=\"true\"\n [checked]=\"isRowSelected(row)\" />\n </td>\n } @else if (column === '__expand') {\n <td class=\"tn-table__cell tn-table__expand-cell\"\n (click)=\"$event.stopPropagation()\">\n @if (canExpandRow(row)) {\n <button\n type=\"button\"\n class=\"tn-table__expand-button\"\n [attr.aria-expanded]=\"isRowExpanded(row)\"\n [attr.aria-label]=\"isRowExpanded(row) ? 'Collapse row' : 'Expand row'\"\n (click)=\"toggleRowExpansion(row)\">\n <tn-icon\n class=\"tn-table__expand-icon\"\n [name]=\"isRowExpanded(row) ? 'mat-keyboard_arrow_up' : 'mat-keyboard_arrow_down'\" />\n </button>\n }\n </td>\n } @else {\n <td\n class=\"tn-table__cell\"\n [style.width]=\"getColumnDef(column)?.width()\"\n [attr.data-column]=\"column\">\n <div class=\"tn-table__cell-content\">\n @if (getColumnDef(column)?.cellTemplate(); as tmpl) {\n <ng-container\n [ngTemplateOutlet]=\"tmpl\"\n [ngTemplateOutletContext]=\"{ $implicit: row, column: column }\" />\n } @else {\n <span>{{ getCellValue(row, column) }}</span>\n }\n </div>\n </td>\n }\n }\n </tr>\n\n <!-- Detail / Expanded Row -->\n @if (detailRowDef() && canExpandRow(row) && isRowExpanded(row)) {\n <tr class=\"tn-table__detail-row\" [@detailExpand]=\"'expanded'\">\n <td\n class=\"tn-table__detail-cell\"\n [attr.colspan]=\"effectiveDisplayedColumns().length\">\n <ng-container\n [ngTemplateOutlet]=\"detailRowDef()!.template\"\n [ngTemplateOutletContext]=\"{ $implicit: row }\" />\n </td>\n </tr>\n }\n }\n </tbody>\n</table>\n\n@if (data().length === 0 && !loading()) {\n <tn-empty\n size=\"compact\"\n [title]=\"emptyMessage()\"\n [description]=\"emptyDescription()\"\n [icon]=\"emptyIcon()\" />\n}\n\n@if (loading()) {\n <div\n class=\"tn-table__loading-overlay\"\n role=\"status\"\n aria-live=\"polite\"\n [attr.aria-label]=\"loadingMessage()\">\n <tn-spinner [ariaLabel]=\"loadingMessage()\" />\n </div>\n}\n", styles: [":host{display:block;position:relative;width:100%;overflow-x:auto}:host(.tn-table--fixed-layout) table{table-layout:fixed;width:100%}:host(.tn-table--bordered){border:1px solid var(--tn-lines);border-radius:4px}.tn-table__table{width:100%;border-collapse:collapse;border-spacing:0;background-color:var(--tn-bg2);border-radius:4px;overflow:hidden}.tn-table__header{background-color:var(--tn-topbar);color:var(--tn-topbar-txt)}.tn-table__header-row{height:56px}.tn-table__header-cell{padding:0 16px;text-align:left;font-weight:600;font-size:14px;border-bottom:1px solid var(--tn-lines);white-space:nowrap;vertical-align:middle}.tn-table__header-cell--sortable{cursor:pointer;-webkit-user-select:none;user-select:none}.tn-table__header-cell--sortable:hover{background-color:color-mix(in srgb,var(--tn-topbar) 85%,var(--tn-topbar-txt))}.tn-table__sort-container{display:inline-flex;align-items:center;gap:4px}.tn-table__sort-icon{opacity:0;transition:opacity .2s ease}.tn-table__header-cell--sortable:hover .tn-table__sort-icon{opacity:.5}.tn-table__header-cell--sorted .tn-table__sort-icon{opacity:1}.tn-table__body{background-color:var(--tn-bg2)}.tn-table__row{height:48px;transition:background-color .2s ease}.tn-table__row:hover{background-color:var(--tn-alt-bg1)}.tn-table__row:not(:last-child){border-bottom:1px solid var(--tn-lines)}.tn-table__row--expanded{background-color:var(--tn-alt-bg1)}.tn-table__row--active{background-color:var(--tn-table-active-bg, var(--tn-bg3, #333333))}.tn-table__row--active>.tn-table__cell:first-child{box-shadow:inset 3px 0 0 0 var(--tn-table-active-indicator, var(--tn-primary, #007db3))}.tn-table__row--clickable{cursor:pointer}.tn-table__row--clickable:focus-visible{outline:none}.tn-table__row--clickable:focus-visible>.tn-table__cell:first-child{box-shadow:inset 3px 0 0 0 var(--tn-primary),inset 0 0 0 2px var(--tn-primary)}.tn-table__row--active.tn-table__row--clickable:focus-visible>.tn-table__cell:first-child{box-shadow:inset 3px 0 0 0 var(--tn-table-active-indicator, var(--tn-primary, #007db3)),inset 0 0 0 2px var(--tn-primary)}.tn-table__cell{padding:0 16px;font-size:14px;color:var(--tn-fg1);vertical-align:middle;overflow:hidden}.tn-table__cell-content{display:flex;align-items:center;min-height:24px;white-space:normal;overflow-wrap:anywhere;overflow:hidden;padding:4px;margin:-4px}.tn-table__cell-content>*{min-width:0}.tn-table__select-cell{width:48px;padding:0 8px 0 16px;cursor:pointer}.tn-table__checkbox{pointer-events:none}.tn-table__expand-cell{width:48px;padding:0 16px 0 8px;text-align:center}.tn-table__expand-button{background:none;border:none;padding:4px;cursor:pointer;border-radius:4px;display:inline-flex;align-items:center;color:var(--tn-fg2)}.tn-table__expand-button:hover{background-color:var(--tn-alt-bg1)}.tn-table__expand-button:focus-visible{outline:2px solid var(--tn-primary);outline-offset:2px}.tn-table__expand-icon{transition:transform .2s ease}.tn-table__detail-row{background-color:var(--tn-alt-bg1);border-bottom:1px solid var(--tn-lines)}.tn-table__detail-cell{padding:16px}.tn-table__loading-overlay{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;background-color:color-mix(in srgb,var(--tn-bg2, #282828) 65%,transparent);-webkit-backdrop-filter:blur(1px);backdrop-filter:blur(1px);pointer-events:all;z-index:1;border-radius:4px}.tn-table--loading .tn-table__table{pointer-events:none;-webkit-user-select:none;user-select:none}.tn-table--dense .tn-table__header-row{height:40px}.tn-table--dense .tn-table__row{height:32px}.tn-table--dense .tn-table__header-cell,.tn-table--dense .tn-table__cell{padding:0 12px;font-size:13px}@media(max-width:768px){.tn-table__table{font-size:12px}.tn-table__header-cell,.tn-table__cell{padding:0 8px}}@media(prefers-reduced-motion:reduce){.tn-table__sort-icon,.tn-table__expand-icon,.tn-table__row{transition:none}.tn-table__detail-row{animation:none}}\n"] }]
11632
- }], ctorParameters: () => [], propDecorators: { dataSource: [{ type: i0.Input, args: [{ isSignal: true, alias: "dataSource", required: false }] }], displayedColumns: [{ type: i0.Input, args: [{ isSignal: true, alias: "displayedColumns", required: false }] }], trackBy: [{ type: i0.Input, args: [{ isSignal: true, alias: "trackBy", required: false }] }], emptyMessage: [{ type: i0.Input, args: [{ isSignal: true, alias: "emptyMessage", required: false }] }], emptyDescription: [{ type: i0.Input, args: [{ isSignal: true, alias: "emptyDescription", required: false }] }], emptyIcon: [{ type: i0.Input, args: [{ isSignal: true, alias: "emptyIcon", required: false }] }], selectable: [{ type: i0.Input, args: [{ isSignal: true, alias: "selectable", required: false }] }], expandable: [{ type: i0.Input, args: [{ isSignal: true, alias: "expandable", required: false }] }], bordered: [{ type: i0.Input, args: [{ isSignal: true, alias: "bordered", required: false }] }], isRowExpandable: [{ type: i0.Input, args: [{ isSignal: true, alias: "isRowExpandable", required: false }] }], activeRow: [{ type: i0.Input, args: [{ isSignal: true, alias: "activeRow", required: false }] }], activeWhen: [{ type: i0.Input, args: [{ isSignal: true, alias: "activeWhen", required: false }] }], activeBg: [{ type: i0.Input, args: [{ isSignal: true, alias: "activeBg", required: false }] }], activeIndicator: [{ type: i0.Input, args: [{ isSignal: true, alias: "activeIndicator", required: false }] }], loading: [{ type: i0.Input, args: [{ isSignal: true, alias: "loading", required: false }] }], loadingMessage: [{ type: i0.Input, args: [{ isSignal: true, alias: "loadingMessage", required: false }] }], clickable: [{ type: i0.Input, args: [{ isSignal: true, alias: "clickable", required: false }] }], expandOnRowClick: [{ type: i0.Input, args: [{ isSignal: true, alias: "expandOnRowClick", required: false }] }], singleExpand: [{ type: i0.Input, args: [{ isSignal: true, alias: "singleExpand", required: false }] }], fixedLayout: [{ type: i0.Input, args: [{ isSignal: true, alias: "fixedLayout", required: false }] }], minColumnWidth: [{ type: i0.Input, args: [{ isSignal: true, alias: "minColumnWidth", required: false }] }], minWidth: [{ type: i0.Input, args: [{ isSignal: true, alias: "minWidth", required: false }] }], sortChange: [{ type: i0.Output, args: ["sortChange"] }], selectionChange: [{ type: i0.Output, args: ["selectionChange"] }], rowClick: [{ type: i0.Output, args: ["rowClick"] }], rowDoubleClick: [{ type: i0.Output, args: ["rowDoubleClick"] }], columnDefs: [{ type: i0.ContentChildren, args: [i0.forwardRef(() => TnTableColumnDirective), { isSignal: true }] }], detailRowDef: [{ type: i0.ContentChild, args: [i0.forwardRef(() => TnDetailRowDefDirective), { isSignal: true }] }], sortColumn: [{ type: i0.Input, args: [{ isSignal: true, alias: "sortColumn", required: false }] }, { type: i0.Output, args: ["sortColumnChange"] }], sortDirection: [{ type: i0.Input, args: [{ isSignal: true, alias: "sortDirection", required: false }] }, { type: i0.Output, args: ["sortDirectionChange"] }] } });
12336
+ }, template: "@if (isCardMode()) {\n <!-- Card layout (narrow container) -->\n @if (selectable() || sortableColumns().length > 0) {\n <div class=\"tn-table__cards-toolbar\" [attr.inert]=\"loading() ? '' : null\">\n @if (selectable()) {\n <!--\n A plain <div>, not a <label>: tn-checkbox renders its own <label for>,\n and nesting one label inside another is invalid. The visible text is\n passed through the checkbox's own `label` input so it stays associated.\n\n The handler goes on THIS wrapper, not on <tn-checkbox>: the checkbox is\n `pointer-events: none` (see `__checkbox` in the stylesheet), so it and its\n subtree are outside hit testing and a real click lands here instead. A\n handler bound on the checkbox is never in the event path \u2014 which is how the\n label -> div change silently killed mouse and touch selection while keyboard\n kept working, since Space on the hidden input dispatches its own bubbling\n click. Table mode binds the same handler on its <td> for this reason.\n\n The click/key and focusable lint rules are suppressed rather than satisfied:\n this wrapper is a pointer hit area around a control that is already\n focusable and already handles Space. Giving it `tabindex` and a key handler\n would add a second tab stop and a second activation path for the same\n checkbox, which is worse than the rule it silences.\n -->\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events, @angular-eslint/template/interactive-supports-focus -->\n <div\n class=\"tn-table__cards-selectall\"\n (click)=\"toggleSelectAll(); $event.preventDefault()\">\n <tn-checkbox\n class=\"tn-table__checkbox\"\n label=\"Select all\"\n [checked]=\"isAllSelected()\"\n [indeterminate]=\"isIndeterminate()\" />\n </div>\n }\n @if (sortableColumns().length > 0) {\n <div class=\"tn-table__cards-sort\">\n <tn-icon name=\"mat-sort\" size=\"sm\" aria-hidden=\"true\" />\n <select\n class=\"tn-table__cards-sort-select\"\n aria-label=\"Sort by\"\n [value]=\"sortColumn()\"\n (change)=\"onSortSelectChange($event)\">\n <!--\n `[selected]` on the options, not just `[value]` on the <select>:\n the select's value is bound before the @for options exist, so the\n browser discards it and the binding is never dirty again \u2014 a table\n sorted before card mode engaged would render as \"Unsorted\".\n -->\n <option value=\"\" [selected]=\"!sortColumn()\">Unsorted</option>\n @for (column of sortableColumns(); track column) {\n <option [value]=\"column\" [selected]=\"column === sortColumn()\">\n {{ getCardLabel(column) }}\n </option>\n }\n </select>\n @if (canSortActiveColumn()) {\n <!--\n Gated on the column being genuinely sortable, not merely active:\n `sortableColumns` keeps a non-sortable active column listed so the sort stays\n clearable, and this toggle must not ride along \u2014 card mode would otherwise\n reorder by a column whose table header ignores clicks.\n\n Three states, not two. A column with an empty direction is not sorted \u2014\n reachable through the two-way binding \u2014 and this control used to render an\n up arrow labelled \"Sort descending\" for it, while table mode showed no\n arrow and no `aria-sort` for the same state. It keeps rendering, because\n it is how the user escapes that state, but shows the neutral\n `mat-unfold_more` the table header already uses for unsorted columns.\n\n The label names the action, so it is \"Sort ascending\" both when unsorted\n and when descending \u2014 `toggleSortDirection()` starts at ascending.\n -->\n <button\n type=\"button\"\n class=\"tn-table__cards-sort-dir\"\n [attr.aria-label]=\"sortDirection() === 'asc' ? 'Sort descending' : 'Sort ascending'\"\n [attr.data-sort-direction]=\"sortDirection()\"\n (click)=\"toggleSortDirection()\">\n @if (sortDirection() === 'asc') {\n <tn-icon size=\"sm\" name=\"mat-arrow_upward\" aria-hidden=\"true\" />\n } @else if (sortDirection() === 'desc') {\n <tn-icon size=\"sm\" name=\"mat-arrow_downward\" aria-hidden=\"true\" />\n } @else {\n <tn-icon size=\"sm\" name=\"mat-unfold_more\" aria-hidden=\"true\" />\n }\n </button>\n }\n </div>\n }\n </div>\n }\n\n <!--\n Decorative `tn-icon`s carry `aria-hidden=\"true\"`. tn-icon renders `role=\"img\"` and\n falls back to its `name` for the accessible name, so without this the raw token\n leaks: a standalone \"mat-sort\" image beside Select all, and disclosures announced\n as \"More fields mat-keyboard_arrow_down\". The repo convention is the same \u2014 see\n button, card, input and file-picker-popup.\n -->\n <ng-template #cardField let-column let-row=\"row\">\n <div class=\"tn-table__card-field\" [attr.data-column]=\"column\">\n <dt class=\"tn-table__card-field-label\">{{ getCardLabel(column) }}</dt>\n <dd class=\"tn-table__card-field-value\">\n @if (getColumnDef(column)?.cellTemplate(); as tmpl) {\n <ng-container\n [ngTemplateOutlet]=\"tmpl\"\n [ngTemplateOutletContext]=\"{ $implicit: row, column: column }\" />\n } @else {\n {{ getCellValue(row, column) }}\n }\n </dd>\n </div>\n </ng-template>\n\n <div class=\"tn-table__cards\" role=\"list\" [attr.inert]=\"loading() ? '' : null\">\n <!--\n Card ARIA differs from the table row on purpose, because `listitem` supports\n far less than `row` does:\n - Active state uses `aria-current`, not `aria-selected`, which `listitem`\n does not support (an earlier revision set it here and it was dropped for\n that reason; axe reports it as a critical aria-allowed-attr violation).\n - Selection state stays on the checkbox, which is a real control.\n - `aria-expanded` IS permitted here \u2014 `listitem` is a subclass of the\n abstract `section` role, and `section` supports `aria-expanded`, so\n `listitem` inherits it (axe-core's own role table lists it in\n `listitem`'s allowedAttrs alongside aria-level/posinset/setsize). So the\n card carries it whenever the card itself acts as the expand trigger,\n matching what table mode does for its rows. The \"Details\" button reports\n the same state unconditionally, because it is the control a screen-reader\n user actually tabs to \u2014 see the note above it.\n The card is deliberately NOT `role=\"button\"`: that role has presentational\n children, which would hide the card's checkbox, row actions and \"Details\"\n toggle from assistive tech entirely.\n -->\n @for (row of data(); track trackByFn()($index, row); let rowIdx = $index) {\n <div\n class=\"tn-table__card\"\n role=\"listitem\"\n [attr.data-row-index]=\"rowIdx\"\n [class.tn-table__card--active]=\"isRowActive(row)\"\n [class.tn-table__card--clickable]=\"clickable()\"\n [attr.tabindex]=\"clickable() ? 0 : null\"\n [attr.aria-current]=\"isRowActive(row) ? 'true' : null\"\n [attr.aria-expanded]=\"isCardExpandTrigger(row) ? isRowExpanded(row) : null\"\n [attr.aria-controls]=\"isCardExpandTrigger(row) && isRowExpanded(row) ? detailPanelId(rowIdx) : null\"\n (click)=\"onCardClick($event, row)\"\n (dblclick)=\"onCardDoubleClick($event, row)\"\n (keydown)=\"onCardKeydown($event, row)\">\n <div class=\"tn-table__card-header\">\n @if (selectable()) {\n <!--\n <div>, not <label>, and the handler is on the wrapper rather than the\n checkbox \u2014 see the toolbar's select-all above for both reasons.\n -->\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events, @angular-eslint/template/interactive-supports-focus -->\n <div\n class=\"tn-table__card-select\"\n (click)=\"toggleRowSelection(row); $event.preventDefault(); $event.stopPropagation()\">\n <!--\n Named from the card's title, not \"Select row N\": with `hideLabel` this is the\n checkbox's entire accessible name, and a card list has no visible row numbers\n to match it against. Falls back to the index when the title column renders\n derived data through a `tnCellDef` and `getCellValue` has nothing to return \u2014\n the same case `getCardLabel()` already falls back for.\n -->\n <tn-checkbox\n class=\"tn-table__checkbox\"\n [label]=\"\n 'Select ' + (getCellValue(row, cardTitleColumn()) ?? 'row ' + (rowIdx + 1))\n \"\n [hideLabel]=\"true\"\n [checked]=\"isRowSelected(row)\" />\n </div>\n }\n <span class=\"tn-table__card-title\">\n @if (getColumnDef(cardTitleColumn())?.cellTemplate(); as tmpl) {\n <ng-container\n [ngTemplateOutlet]=\"tmpl\"\n [ngTemplateOutletContext]=\"{ $implicit: row, column: cardTitleColumn() }\" />\n } @else {\n {{ getCellValue(row, cardTitleColumn()) }}\n }\n </span>\n @if (rowActionsDef()) {\n <div class=\"tn-table__card-actions\">\n <ng-container\n [ngTemplateOutlet]=\"rowActionsDef()!.template\"\n [ngTemplateOutletContext]=\"{ $implicit: row }\" />\n </div>\n }\n </div>\n\n <dl class=\"tn-table__card-fields\">\n @for (column of cardPrimaryColumns(); track column) {\n <ng-container\n [ngTemplateOutlet]=\"cardField\"\n [ngTemplateOutletContext]=\"{ $implicit: column, row: row }\" />\n }\n </dl>\n\n <!--\n The disclosure sits outside the primary <dl> and wraps its fields in a\n <dl> of its own: <details> is not a permitted child of <dl>, so nesting\n it there detached the folded dt/dd pairs from any description list.\n -->\n @if (cardSecondaryColumns().length > 0) {\n <details class=\"tn-table__card-more\">\n <summary class=\"tn-table__card-more-summary\">\n <span>More fields</span>\n <tn-icon size=\"sm\" name=\"mat-keyboard_arrow_down\" aria-hidden=\"true\" />\n </summary>\n <dl class=\"tn-table__card-fields\">\n @for (column of cardSecondaryColumns(); track column) {\n <ng-container\n [ngTemplateOutlet]=\"cardField\"\n [ngTemplateOutletContext]=\"{ $implicit: column, row: row }\" />\n }\n </dl>\n </details>\n }\n\n @if (canExpandRow(row) && detailRowDef()) {\n <!--\n The button always carries the state, even when the card is also a trigger.\n An earlier revision suppressed it here to avoid announcing the same panel\n twice, but that muted the primary control: this button is what a\n screen-reader user reaches by tabbing through controls, and it was\n announced as a bare \"Details, button\" with no expanded state, before or\n after activation. The state sat on the `role=\"listitem\"` card, heard only\n if the user happened to focus the card itself.\n\n Two controls that each toggle the same region and each report its state is\n the disclosure pattern applied twice \u2014 redundant, not invalid. Redundancy\n is the cheaper failure.\n -->\n <button\n type=\"button\"\n class=\"tn-table__card-detail-toggle\"\n [attr.aria-expanded]=\"isRowExpanded(row)\"\n [attr.aria-controls]=\"isRowExpanded(row) ? detailPanelId(rowIdx) : null\"\n (click)=\"toggleRowExpansion(row); $event.stopPropagation()\">\n <span>Details</span>\n <tn-icon\n size=\"sm\"\n aria-hidden=\"true\"\n [name]=\"isRowExpanded(row) ? 'mat-keyboard_arrow_up' : 'mat-keyboard_arrow_down'\" />\n </button>\n @if (isRowExpanded(row)) {\n <div class=\"tn-table__card-detail\" [attr.id]=\"detailPanelId(rowIdx)\">\n <ng-container\n [ngTemplateOutlet]=\"detailRowDef()!.template\"\n [ngTemplateOutletContext]=\"{ $implicit: row }\" />\n </div>\n }\n }\n </div>\n }\n </div>\n} @else {\n <table\n class=\"tn-table__table\"\n [style.min-width]=\"resolvedMinWidth()\"\n [attr.inert]=\"loading() ? '' : null\">\n <!-- Header Row -->\n <thead class=\"tn-table__header\">\n <tr class=\"tn-table__header-row\">\n @for (column of effectiveDisplayedColumns(); track $index) {\n @if (column === '__select') {\n <th class=\"tn-table__header-cell tn-table__select-cell\"\n role=\"checkbox\"\n tabindex=\"0\"\n [attr.aria-checked]=\"isAllSelected()\"\n (click)=\"toggleSelectAll()\"\n (keydown.enter)=\"toggleSelectAll()\"\n (keydown.space)=\"toggleSelectAll(); $event.preventDefault()\">\n <tn-checkbox\n class=\"tn-table__checkbox\"\n label=\"Select all rows\"\n [hideLabel]=\"true\"\n [checked]=\"isAllSelected()\"\n [indeterminate]=\"isIndeterminate()\" />\n </th>\n } @else if (column === '__expand') {\n <th class=\"tn-table__header-cell tn-table__expand-cell\">\n <span class=\"cdk-visually-hidden\">Expand</span>\n </th>\n } @else {\n <th\n class=\"tn-table__header-cell\"\n [class.tn-table__header-cell--sortable]=\"getColumnDef(column)?.sortable()\"\n [class.tn-table__header-cell--sorted]=\"isSorted(column)\"\n [style.width]=\"getColumnDef(column)?.width()\"\n [attr.aria-sort]=\"\n isSorted(column)\n ? (sortDirection() === 'asc' ? 'ascending' : 'descending')\n : (getColumnDef(column)?.sortable() ? 'none' : null)\n \"\n [attr.tabindex]=\"getColumnDef(column)?.sortable() ? 0 : null\"\n [attr.data-column]=\"column\"\n (click)=\"onSortHeaderClick(column, $event)\"\n (keydown.enter)=\"onSortHeaderKeydown(column, $event)\"\n (keydown.space)=\"onSortHeaderKeydown(column, $event)\">\n <span class=\"tn-table__sort-container\">\n <span class=\"tn-table__header-text\">\n @if (getColumnDef(column)?.headerTemplate(); as tmpl) {\n <ng-container [ngTemplateOutlet]=\"tmpl\" />\n } @else {\n {{ getColumnDef(column)?.label() ?? column }}\n }\n </span>\n @if (getColumnDef(column)?.sortable()) {\n <!--\n aria-hidden: the <th> has no aria-label, so its accessible name is\n computed from its contents \u2014 without this, every sortable column\n announced as \"Name mat-unfold_more\". `aria-sort` on the <th> already\n carries the state properly, so the token is pure noise.\n -->\n <tn-icon\n class=\"tn-table__sort-icon\"\n size=\"sm\"\n aria-hidden=\"true\"\n [name]=\"\n isSorted(column)\n ? (sortDirection() === 'asc' ? 'mat-arrow_upward' : 'mat-arrow_downward')\n : 'mat-unfold_more'\n \" />\n }\n </span>\n </th>\n }\n }\n @if (rowActionsDef()) {\n <th class=\"tn-table__header-cell tn-table__actions-cell\">\n <span class=\"cdk-visually-hidden\">Actions</span>\n </th>\n }\n </tr>\n </thead>\n\n <!-- Data Rows -->\n <tbody class=\"tn-table__body\">\n @for (row of data(); track trackByFn()($index, row); let rowIdx = $index) {\n <tr\n class=\"tn-table__row\"\n [attr.data-row-index]=\"rowIdx\"\n [class.tn-table__row--expandable]=\"canExpandRow(row)\"\n [class.tn-table__row--expanded]=\"canExpandRow(row) && isRowExpanded(row)\"\n [class.tn-table__row--active]=\"isRowActive(row)\"\n [class.tn-table__row--clickable]=\"clickable()\"\n [attr.tabindex]=\"clickable() ? 0 : null\"\n [attr.aria-selected]=\"clickable() ? isRowActive(row) : null\"\n [attr.aria-expanded]=\"isRowExpandTrigger(row) ? isRowExpanded(row) : null\"\n (click)=\"onRowClick(row, $event)\"\n (dblclick)=\"onRowDoubleClick(row, $event)\"\n (keydown)=\"onRowKeydown($event, row)\">\n @for (column of effectiveDisplayedColumns(); track $index) {\n @if (column === '__select') {\n <td class=\"tn-table__cell tn-table__select-cell\"\n (click)=\"toggleRowSelection(row); $event.stopPropagation()\">\n <tn-checkbox\n class=\"tn-table__checkbox\"\n [label]=\"'Select row ' + (rowIdx + 1)\"\n [hideLabel]=\"true\"\n [checked]=\"isRowSelected(row)\" />\n </td>\n } @else if (column === '__expand') {\n <td class=\"tn-table__cell tn-table__expand-cell\"\n (click)=\"$event.stopPropagation()\">\n @if (canExpandRow(row)) {\n <button\n type=\"button\"\n class=\"tn-table__expand-button\"\n [attr.aria-expanded]=\"isRowExpanded(row)\"\n [attr.aria-label]=\"isRowExpanded(row) ? 'Collapse row' : 'Expand row'\"\n (click)=\"toggleRowExpansion(row)\">\n <tn-icon\n class=\"tn-table__expand-icon\"\n aria-hidden=\"true\"\n [name]=\"isRowExpanded(row) ? 'mat-keyboard_arrow_up' : 'mat-keyboard_arrow_down'\" />\n </button>\n }\n </td>\n } @else {\n <td\n class=\"tn-table__cell\"\n [style.width]=\"getColumnDef(column)?.width()\"\n [attr.data-column]=\"column\">\n <div class=\"tn-table__cell-content\">\n @if (getColumnDef(column)?.cellTemplate(); as tmpl) {\n <ng-container\n [ngTemplateOutlet]=\"tmpl\"\n [ngTemplateOutletContext]=\"{ $implicit: row, column: column }\" />\n } @else {\n <span>{{ getCellValue(row, column) }}</span>\n }\n </div>\n </td>\n }\n }\n @if (rowActionsDef()) {\n <td class=\"tn-table__cell tn-table__actions-cell\"\n (click)=\"$event.stopPropagation()\">\n <div class=\"tn-table__actions-content\">\n <ng-container\n [ngTemplateOutlet]=\"rowActionsDef()!.template\"\n [ngTemplateOutletContext]=\"{ $implicit: row }\" />\n </div>\n </td>\n }\n </tr>\n\n <!-- Detail / Expanded Row -->\n @if (detailRowDef() && canExpandRow(row) && isRowExpanded(row)) {\n <tr class=\"tn-table__detail-row\" [@detailExpand]=\"'expanded'\">\n <td\n class=\"tn-table__detail-cell\"\n [attr.colspan]=\"totalColumnCount()\">\n <ng-container\n [ngTemplateOutlet]=\"detailRowDef()!.template\"\n [ngTemplateOutletContext]=\"{ $implicit: row }\" />\n </td>\n </tr>\n }\n }\n </tbody>\n </table>\n}\n\n@if (data().length === 0 && !loading()) {\n <tn-empty\n size=\"compact\"\n [title]=\"emptyMessage()\"\n [description]=\"emptyDescription()\"\n [icon]=\"emptyIcon()\" />\n}\n\n@if (loading()) {\n <div\n class=\"tn-table__loading-overlay\"\n role=\"status\"\n aria-live=\"polite\"\n [attr.aria-label]=\"loadingMessage()\">\n <tn-spinner [ariaLabel]=\"loadingMessage()\" />\n </div>\n}\n", styles: [":host{display:block;position:relative;width:100%;overflow-x:auto;border-radius:4px}:host{--tn-table-select-width: 72px;--tn-table-actions-width: 128px;--tn-table-expand-width: 56px}.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}:host(.tn-table--fixed-layout) table{table-layout:fixed;width:100%}:host(.tn-table--fixed-layout) .tn-table__actions-cell{box-sizing:border-box;width:var(--tn-table-actions-width)}:host(.tn-table--bordered){border:1px solid var(--tn-lines);border-radius:4px}:host(.tn-table--loading) .tn-table__table,:host(.tn-table--loading) .tn-table__cards,:host(.tn-table--loading) .tn-table__cards-toolbar{pointer-events:none;-webkit-user-select:none;user-select:none}:host(.tn-table--scroll) .tn-table__select-cell{position:sticky;left:0}:host(.tn-table--scroll) .tn-table__header-cell:first-child,:host(.tn-table--scroll) .tn-table__row>.tn-table__cell:first-child,:host(.tn-table--scroll) .tn-table__select-cell+.tn-table__header-cell,:host(.tn-table--scroll) .tn-table__row>.tn-table__select-cell+.tn-table__cell{position:sticky;left:0;z-index:1}:host(.tn-table--scroll) .tn-table__actions-cell{position:sticky;right:0;z-index:3}:host(.tn-table--scroll) .tn-table__header-row>.tn-table__header-cell.tn-table__select-cell,:host(.tn-table--scroll) .tn-table__row>.tn-table__cell.tn-table__select-cell{z-index:2}:host(.tn-table--scroll) .tn-table__select-cell+.tn-table__header-cell,:host(.tn-table--scroll) .tn-table__row>.tn-table__select-cell+.tn-table__cell{left:var(--tn-table-select-width)}:host(.tn-table--scroll) .tn-table__row>.tn-table__cell:first-child,:host(.tn-table--scroll) .tn-table__row>.tn-table__select-cell+.tn-table__cell{background-color:inherit}:host(.tn-table--scroll) .tn-table__header-row .tn-table__header-cell:first-child:not(.tn-table__header-cell--sortable:hover),:host(.tn-table--scroll) .tn-table__header-row .tn-table__select-cell+.tn-table__header-cell:not(.tn-table__header-cell--sortable:hover){background-color:var(--tn-topbar)}.tn-table__table{width:100%;border-collapse:collapse;border-spacing:0;background-color:var(--tn-bg2)}.tn-table__header{background-color:var(--tn-topbar);color:var(--tn-topbar-txt)}.tn-table__header-row{height:56px}.tn-table__header-cell{padding:0 16px;text-align:left;font-weight:600;font-size:14px;border-bottom:1px solid var(--tn-lines);white-space:nowrap;vertical-align:middle}.tn-table__header-cell--sortable{cursor:pointer;-webkit-user-select:none;user-select:none}.tn-table__header-cell--sortable:hover{background-color:color-mix(in srgb,var(--tn-topbar) 85%,var(--tn-topbar-txt))}.tn-table__sort-container{display:inline-flex;align-items:center;gap:4px}.tn-table__sort-icon{opacity:0;transition:opacity .2s ease}.tn-table__header-cell--sortable:hover .tn-table__sort-icon,.tn-table__header-cell--sortable:focus-visible .tn-table__sort-icon{opacity:.5}.tn-table__header-cell--sorted .tn-table__sort-icon{opacity:1}.tn-table__body{background-color:var(--tn-bg2)}.tn-table__row{height:48px;transition:background-color .2s ease;background-color:var(--tn-bg2)}.tn-table__row:hover{background-color:var(--tn-alt-bg1)}.tn-table__row:not(:last-child){border-bottom:1px solid var(--tn-lines)}.tn-table__row--expanded{background-color:var(--tn-alt-bg1)}.tn-table__row--active{background-color:var(--tn-table-active-bg, var(--tn-bg3, #333333))}.tn-table__row--active>.tn-table__cell:first-child{box-shadow:inset 3px 0 0 0 var(--tn-table-active-indicator, var(--tn-primary, #007db3))}.tn-table__row--clickable{cursor:pointer}.tn-table__row--clickable:focus-visible{outline:none}.tn-table__row--clickable:focus-visible>.tn-table__cell:first-child{box-shadow:inset 3px 0 0 0 var(--tn-primary),inset 0 0 0 2px var(--tn-primary)}.tn-table__row--active.tn-table__row--clickable:focus-visible>.tn-table__cell:first-child{box-shadow:inset 3px 0 0 0 var(--tn-table-active-indicator, var(--tn-primary, #007db3)),inset 0 0 0 2px var(--tn-primary)}.tn-table__cell{padding:0 16px;font-size:14px;color:var(--tn-fg1);vertical-align:middle;overflow:hidden}.tn-table__cell-content{display:flex;align-items:center;min-height:24px;white-space:normal;overflow-wrap:anywhere;overflow:hidden;padding:4px;margin:-4px}.tn-table__cell-content>*{min-width:0}.tn-table__select-cell{box-sizing:border-box;width:var(--tn-table-select-width);padding:0 8px 0 16px;cursor:pointer}.tn-table__checkbox{pointer-events:none}.tn-table__expand-cell{box-sizing:border-box;width:var(--tn-table-expand-width);padding:0 16px 0 8px;text-align:center}.tn-table__expand-button{background:none;border:none;padding:4px;cursor:pointer;border-radius:4px;display:inline-flex;align-items:center;color:var(--tn-fg2)}.tn-table__expand-button:hover{background-color:var(--tn-alt-bg1)}.tn-table__expand-button:focus-visible{outline:2px solid var(--tn-primary);outline-offset:2px}.tn-table__expand-icon{transition:transform .2s ease}.tn-table__detail-row{background-color:var(--tn-alt-bg1);border-bottom:1px solid var(--tn-lines)}.tn-table__detail-cell{padding:16px}.tn-table__actions-cell{width:1%;white-space:nowrap;text-align:right;background-color:inherit;border-left:1px solid var(--tn-lines)}.tn-table__header .tn-table__actions-cell{background-color:var(--tn-topbar)}.tn-table__actions-content{display:inline-flex;align-items:center;gap:4px}.tn-table__loading-overlay{position:absolute;inset:0;z-index:4;display:flex;align-items:center;justify-content:center;background-color:color-mix(in srgb,var(--tn-bg2, #282828) 65%,transparent);-webkit-backdrop-filter:blur(1px);backdrop-filter:blur(1px);pointer-events:all;border-radius:4px}.tn-table--dense .tn-table__header-row{height:40px}.tn-table--dense .tn-table__row{height:32px}.tn-table--dense .tn-table__header-cell,.tn-table--dense .tn-table__cell{padding:0 12px;font-size:13px}.tn-table__cards-toolbar{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:8px 4px 12px}.tn-table__cards-selectall{display:inline-flex;align-items:center;gap:8px;font-size:14px;color:var(--tn-fg1);cursor:pointer}.tn-table__cards-sort{display:inline-flex;align-items:center;gap:6px;margin-left:auto;color:var(--tn-fg2)}.tn-table__cards-sort-select{height:32px;padding:0 8px;border:1px solid var(--tn-lines);border-radius:4px;background-color:var(--tn-bg2);color:var(--tn-fg1);font-size:14px}.tn-table__cards-sort-select:focus-visible{outline:2px solid var(--tn-primary);outline-offset:2px}.tn-table__cards-sort-dir{display:inline-flex;align-items:center;justify-content:center;width:32px;height:32px;border:1px solid var(--tn-lines);border-radius:4px;background:none;color:var(--tn-fg2);cursor:pointer}.tn-table__cards-sort-dir:hover{background-color:var(--tn-alt-bg1)}.tn-table__cards-sort-dir:focus-visible{outline:2px solid var(--tn-primary);outline-offset:2px}.tn-table__cards{display:flex;flex-direction:column;gap:8px}.tn-table__card{background-color:var(--tn-bg2);border:1px solid var(--tn-lines);border-radius:6px;padding:12px}.tn-table__card--active{background-color:var(--tn-table-active-bg, var(--tn-bg3, #333333));box-shadow:inset 3px 0 0 0 var(--tn-table-active-indicator, var(--tn-primary, #007db3))}.tn-table__card--clickable{cursor:pointer}.tn-table__card--clickable:focus-visible{outline:none;box-shadow:inset 0 0 0 2px var(--tn-primary)}.tn-table__card--active.tn-table__card--clickable:focus-visible{box-shadow:inset 3px 0 0 0 var(--tn-table-active-indicator, var(--tn-primary, #007db3)),inset 0 0 0 2px var(--tn-primary)}.tn-table__card-header{display:flex;align-items:center;gap:10px}.tn-table__card-select{display:inline-flex;align-items:center;flex-shrink:0;cursor:pointer}.tn-table__card-title{flex:1;min-width:0;font-weight:600;font-size:15px;color:var(--tn-fg1);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.tn-table__card-actions{display:inline-flex;align-items:center;gap:4px;flex-shrink:0}.tn-table__card-fields{margin:0;padding:10px 0 0}.tn-table__card-field{display:flex;align-items:baseline;justify-content:space-between;gap:12px;padding:3px 0;font-size:13px}.tn-table__card-field-label{flex-shrink:0;color:var(--tn-fg2)}.tn-table__card-field-value{margin:0;min-width:0;text-align:right;color:var(--tn-fg1);overflow-wrap:anywhere}.tn-table__card-more[open]>.tn-table__card-more-summary tn-icon{transform:rotate(180deg)}.tn-table__card-more{margin-top:2px}.tn-table__card-more-summary{display:inline-flex;align-items:center;gap:4px;padding:4px 0;font-size:13px;color:var(--tn-fg1);cursor:pointer;list-style:none}.tn-table__card-more-summary::-webkit-details-marker{display:none}.tn-table__card-more-summary tn-icon{transition:transform .2s ease}.tn-table__card-more-summary:focus-visible{outline:2px solid var(--tn-primary);outline-offset:2px}.tn-table__card-detail-toggle{display:inline-flex;align-items:center;gap:4px;margin-top:8px;padding:4px 0;background:none;border:none;color:var(--tn-fg1);font-size:13px;cursor:pointer}.tn-table__card-detail-toggle:focus-visible{outline:2px solid var(--tn-primary);outline-offset:2px}.tn-table__card-detail{margin-top:8px;padding-top:8px;border-top:1px solid var(--tn-lines)}@media(max-width:768px){.tn-table__table{font-size:12px}.tn-table__header-cell,.tn-table__cell{padding:0 8px}}@media(prefers-reduced-motion:reduce){.tn-table__card-more-summary tn-icon,.tn-table__sort-icon,.tn-table__expand-icon,.tn-table__row{transition:none}.tn-table__detail-row{animation:none}}\n"] }]
12337
+ }], ctorParameters: () => [], propDecorators: { dataSource: [{ type: i0.Input, args: [{ isSignal: true, alias: "dataSource", required: false }] }], displayedColumns: [{ type: i0.Input, args: [{ isSignal: true, alias: "displayedColumns", required: false }] }], trackBy: [{ type: i0.Input, args: [{ isSignal: true, alias: "trackBy", required: false }] }], emptyMessage: [{ type: i0.Input, args: [{ isSignal: true, alias: "emptyMessage", required: false }] }], emptyDescription: [{ type: i0.Input, args: [{ isSignal: true, alias: "emptyDescription", required: false }] }], emptyIcon: [{ type: i0.Input, args: [{ isSignal: true, alias: "emptyIcon", required: false }] }], selectable: [{ type: i0.Input, args: [{ isSignal: true, alias: "selectable", required: false }] }], expandable: [{ type: i0.Input, args: [{ isSignal: true, alias: "expandable", required: false }] }], bordered: [{ type: i0.Input, args: [{ isSignal: true, alias: "bordered", required: false }] }], isRowExpandable: [{ type: i0.Input, args: [{ isSignal: true, alias: "isRowExpandable", required: false }] }], activeRow: [{ type: i0.Input, args: [{ isSignal: true, alias: "activeRow", required: false }] }], activeWhen: [{ type: i0.Input, args: [{ isSignal: true, alias: "activeWhen", required: false }] }], activeBg: [{ type: i0.Input, args: [{ isSignal: true, alias: "activeBg", required: false }] }], activeIndicator: [{ type: i0.Input, args: [{ isSignal: true, alias: "activeIndicator", required: false }] }], loading: [{ type: i0.Input, args: [{ isSignal: true, alias: "loading", required: false }] }], loadingMessage: [{ type: i0.Input, args: [{ isSignal: true, alias: "loadingMessage", required: false }] }], clickable: [{ type: i0.Input, args: [{ isSignal: true, alias: "clickable", required: false }] }], expandOnRowClick: [{ type: i0.Input, args: [{ isSignal: true, alias: "expandOnRowClick", required: false }] }], singleExpand: [{ type: i0.Input, args: [{ isSignal: true, alias: "singleExpand", required: false }] }], fixedLayout: [{ type: i0.Input, args: [{ isSignal: true, alias: "fixedLayout", required: false }] }], minColumnWidth: [{ type: i0.Input, args: [{ isSignal: true, alias: "minColumnWidth", required: false }] }], minWidth: [{ type: i0.Input, args: [{ isSignal: true, alias: "minWidth", required: false }] }], mobileLayout: [{ type: i0.Input, args: [{ isSignal: true, alias: "mobileLayout", required: false }] }], cardBreakpoint: [{ type: i0.Input, args: [{ isSignal: true, alias: "cardBreakpoint", required: false }] }], cardPrimaryCount: [{ type: i0.Input, args: [{ isSignal: true, alias: "cardPrimaryCount", required: false }] }], sortChange: [{ type: i0.Output, args: ["sortChange"] }], selectionChange: [{ type: i0.Output, args: ["selectionChange"] }], rowClick: [{ type: i0.Output, args: ["rowClick"] }], rowDoubleClick: [{ type: i0.Output, args: ["rowDoubleClick"] }], columnDefs: [{ type: i0.ContentChildren, args: [i0.forwardRef(() => TnTableColumnDirective), { isSignal: true }] }], detailRowDef: [{ type: i0.ContentChild, args: [i0.forwardRef(() => TnDetailRowDefDirective), { isSignal: true }] }], rowActionsDef: [{ type: i0.ContentChild, args: [i0.forwardRef(() => TnRowActionsDefDirective), { isSignal: true }] }], sortColumn: [{ type: i0.Input, args: [{ isSignal: true, alias: "sortColumn", required: false }] }, { type: i0.Output, args: ["sortColumnChange"] }], sortDirection: [{ type: i0.Input, args: [{ isSignal: true, alias: "sortDirection", required: false }] }, { type: i0.Output, args: ["sortDirectionChange"] }] } });
11633
12338
 
11634
12339
  /**
11635
12340
  * Harness for interacting with `tn-table` in tests.
@@ -11663,18 +12368,59 @@ class TnTableHarness extends ComponentHarness {
11663
12368
  /**
11664
12369
  * Gets the number of data rows (excludes header and detail rows).
11665
12370
  *
11666
- * @returns Promise resolving to the row count.
12371
+ * Layout-aware: counts rendered cards in the card layout, so the count tracks what the
12372
+ * user sees rather than reporting 0 for a container that happens to be narrow.
12373
+ *
12374
+ * @returns Promise resolving to the row count, or the card count in the card layout.
11667
12375
  */
11668
12376
  async getRowCount() {
11669
- const rows = await this.locatorForAll('.tn-table__row')();
12377
+ // Counts cards in card mode. Answering 0 there was the silent-wrong-answer shape the
12378
+ // selection block rejects, and this is the harness's own headline example — a consumer
12379
+ // that narrows the container and asserts emptiness would pass vacuously.
12380
+ const selector = (await this.isCards()) ? '.tn-table__card' : '.tn-table__row';
12381
+ const rows = await this.locatorForAll(selector)();
11670
12382
  return rows.length;
11671
12383
  }
12384
+ /**
12385
+ * Throws when the rendered layout is `cards`, for queries that have no card equivalent.
12386
+ * The alternative is an empty array or a 0, which reads as "the table is empty" and
12387
+ * greens a test over a rendered card list — the failure mode the selection and
12388
+ * active-row methods were made layout-aware to avoid.
12389
+ */
12390
+ async assertTableLayout(method) {
12391
+ if (await this.isCards()) {
12392
+ throw new Error(`TnTableHarness.${method}() has no meaning in the card layout. `
12393
+ + 'Use the card API instead — getCardCount(), getCardTitle(), getCardFieldValue(), '
12394
+ + 'toggleCardDetail(), expandCardMoreFields(), getCardSortColumn(), '
12395
+ + 'getCardSortDirection(), toggleCardSortDirection(), clickCard(), '
12396
+ + 'doubleClickCard(), pressKeyOnCard(), isCardFocusable() — or widen the container '
12397
+ + 'above cardBreakpoint.');
12398
+ }
12399
+ }
12400
+ /**
12401
+ * Mirror of {@link assertTableLayout} for the card API. `getCardCount()` answering 0 in
12402
+ * table mode is the same silent-wrong-answer shape rejected for `getRowCount()`: a spec
12403
+ * that forgot to push a sub-breakpoint width would assert emptiness and pass over 20
12404
+ * rendered rows.
12405
+ */
12406
+ async assertCardLayout(method) {
12407
+ if (!(await this.isCards())) {
12408
+ throw new Error(`TnTableHarness.${method}() has no meaning in the table layout. `
12409
+ + 'Use the row API instead — getRowCount(), getCellText(), getRowTexts(), '
12410
+ + 'toggleRowExpansion(), clickSortHeader(), clickRow(), doubleClickRow(), '
12411
+ + 'pressKeyOnRow(), isRowFocusable() — or narrow the container below '
12412
+ + 'cardBreakpoint.');
12413
+ }
12414
+ }
11672
12415
  /**
11673
12416
  * Gets the text content of header cells (excludes sort icons).
11674
12417
  *
11675
12418
  * @returns Promise resolving to an array of header text strings.
12419
+ * @throws Error in the card layout, which renders no header row;
12420
+ * the thrown message names the card API.
11676
12421
  */
11677
12422
  async getHeaderTexts() {
12423
+ await this.assertTableLayout('getHeaderTexts');
11678
12424
  const textEls = await this.locatorForAll('.tn-table__header-text')();
11679
12425
  const texts = [];
11680
12426
  for (const el of textEls) {
@@ -11687,8 +12433,10 @@ class TnTableHarness extends ComponentHarness {
11687
12433
  *
11688
12434
  * @param rowIndex Zero-based index of the data row.
11689
12435
  * @returns Promise resolving to an array of cell text strings.
12436
+ * @throws Error in the card layout, which renders no rows; the thrown message names the card API.
11690
12437
  */
11691
12438
  async getRowTexts(rowIndex) {
12439
+ await this.assertTableLayout('getRowTexts');
11692
12440
  await this.assertRowExists(rowIndex);
11693
12441
  const cells = await this.locatorForAll(`.tn-table__row[data-row-index="${rowIndex}"] .tn-table__cell[data-column]`)();
11694
12442
  const texts = [];
@@ -11703,8 +12451,11 @@ class TnTableHarness extends ComponentHarness {
11703
12451
  * @param rowIndex Zero-based index of the data row.
11704
12452
  * @param columnName The column's data-column attribute value.
11705
12453
  * @returns Promise resolving to the cell text.
12454
+ * @throws Error in the card layout, which renders no cells;
12455
+ * the thrown message names the card API.
11706
12456
  */
11707
12457
  async getCellText(rowIndex, columnName) {
12458
+ await this.assertTableLayout('getCellText');
11708
12459
  await this.assertRowExists(rowIndex);
11709
12460
  const cell = await this.locatorFor(`.tn-table__row[data-row-index="${rowIndex}"] [data-column="${columnName}"]`)();
11710
12461
  return (await cell.text()).trim();
@@ -11713,8 +12464,10 @@ class TnTableHarness extends ComponentHarness {
11713
12464
  * Gets all row texts as a 2D string array (data-column cells only).
11714
12465
  *
11715
12466
  * @returns Promise resolving to an array of row text arrays.
12467
+ * @throws Error in the card layout, which renders no rows; the thrown message names the card API.
11716
12468
  */
11717
12469
  async getAllRowTexts() {
12470
+ await this.assertTableLayout('getAllRowTexts');
11718
12471
  const count = await this.getRowCount();
11719
12472
  const result = [];
11720
12473
  for (let i = 0; i < count; i++) {
@@ -11727,8 +12480,11 @@ class TnTableHarness extends ComponentHarness {
11727
12480
  * Clicks a sortable column header to cycle sort direction.
11728
12481
  *
11729
12482
  * @param columnName The column's data-column attribute value.
12483
+ * @throws Error in the card layout, which renders no column headers;
12484
+ * the thrown message names the card API.
11730
12485
  */
11731
12486
  async clickSortHeader(columnName) {
12487
+ await this.assertTableLayout('clickSortHeader');
11732
12488
  const header = await this.locatorFor(`th[data-column="${columnName}"]`)();
11733
12489
  await header.click();
11734
12490
  }
@@ -11737,59 +12493,105 @@ class TnTableHarness extends ComponentHarness {
11737
12493
  *
11738
12494
  * @param columnName The column's data-column attribute value.
11739
12495
  * @returns Promise resolving to true if the column is sortable.
12496
+ * @throws Error in the card layout, which renders no column headers;
12497
+ * the thrown message names the card API.
11740
12498
  */
11741
12499
  async isSortable(columnName) {
12500
+ await this.assertTableLayout('isSortable');
11742
12501
  const header = await this.locatorFor(`th[data-column="${columnName}"]`)();
11743
12502
  return header.hasClass('tn-table__header-cell--sortable');
11744
12503
  }
11745
12504
  /**
11746
12505
  * Gets the current sort direction for a column via aria-sort.
11747
12506
  *
12507
+ * `aria-sort="none"` — which a sortable-but-unsorted header now carries, so it is
12508
+ * discoverable as sortable — is normalised to null, keeping "not sorted" a single value
12509
+ * for callers. Only a non-sortable header has no attribute at all, and both answer null.
12510
+ *
11748
12511
  * @param columnName The column's data-column attribute value.
11749
12512
  * @returns Promise resolving to 'ascending', 'descending', or null.
12513
+ * @throws Error in the card layout, which renders no column headers;
12514
+ * the thrown message names the card API.
11750
12515
  */
11751
12516
  async getSortDirection(columnName) {
12517
+ await this.assertTableLayout('getSortDirection');
11752
12518
  const header = await this.locatorFor(`th[data-column="${columnName}"]`)();
11753
- return header.getAttribute('aria-sort');
12519
+ const sort = await header.getAttribute('aria-sort');
12520
+ return sort === 'none' ? null : sort;
11754
12521
  }
11755
12522
  // --- Selection ---
12523
+ //
12524
+ // Layout-aware, all four of them. Card mode renders no `.tn-table__row` at all —
12525
+ // selection lives in `.tn-table__card .tn-table__card-select` — so a row-based
12526
+ // locator either throws or, worse, answers 0 for a table with rows selected.
12527
+ // Consumers shouldn't have to branch on the rendered layout to ask "is this row
12528
+ // selected", so these resolve the right selectors themselves.
12529
+ //
12530
+ // The click target is the WRAPPER in both layouts (the `<td>`, or the card's
12531
+ // `<div>`): `.tn-table__checkbox` is `pointer-events: none`, so clicking the
12532
+ // checkbox itself exercises a path no user can take.
12533
+ /** True when the container is narrow enough that card mode is rendered. */
12534
+ async isCards() {
12535
+ return (await this.getLayoutMode()) === 'cards';
12536
+ }
11756
12537
  /**
11757
- * Clicks the select-all checkbox in the header.
12538
+ * Clicks the select-all control — the header checkbox in table mode, the card
12539
+ * toolbar's "Select all" in card mode.
11758
12540
  */
11759
12541
  async toggleSelectAll() {
11760
- const cell = await this.locatorFor('.tn-table__header-row .tn-table__select-cell')();
11761
- await cell.click();
12542
+ const selector = (await this.isCards())
12543
+ ? '.tn-table__cards-selectall'
12544
+ : '.tn-table__header-row .tn-table__select-cell';
12545
+ const target = await this.locatorFor(selector)();
12546
+ await target.click();
11762
12547
  }
11763
12548
  /**
11764
- * Toggles selection for a specific row by clicking its checkbox cell.
12549
+ * Toggles selection for a specific row (or its card) by clicking its checkbox
12550
+ * wrapper.
11765
12551
  *
11766
12552
  * @param rowIndex Zero-based index of the data row.
11767
12553
  */
11768
12554
  async toggleRowSelection(rowIndex) {
11769
- await this.assertRowExists(rowIndex);
11770
- const cell = await this.locatorFor(`.tn-table__row[data-row-index="${rowIndex}"] .tn-table__select-cell`)();
11771
- await cell.click();
12555
+ await this.assertIndexExists(rowIndex);
12556
+ const selector = (await this.isCards())
12557
+ ? `.tn-table__card[data-row-index="${rowIndex}"] .tn-table__card-select`
12558
+ : `.tn-table__row[data-row-index="${rowIndex}"] .tn-table__select-cell`;
12559
+ const target = await this.locatorFor(selector)();
12560
+ await target.click();
11772
12561
  }
11773
12562
  /**
11774
- * Checks if a specific row's checkbox is checked.
12563
+ * Checks if a specific row (or its card) is selected.
11775
12564
  *
11776
12565
  * @param rowIndex Zero-based index of the data row.
11777
12566
  * @returns Promise resolving to true if the row's checkbox is checked.
11778
12567
  */
11779
12568
  async isRowSelected(rowIndex) {
11780
- await this.assertRowExists(rowIndex);
11781
- const checkbox = await this.locatorFor(TnCheckboxHarness.with({
11782
- ancestor: `.tn-table__row[data-row-index="${rowIndex}"]`,
11783
- }))();
12569
+ await this.assertIndexExists(rowIndex);
12570
+ // Scoped to the select cell, matching `toggleRowSelection` and `getSelectedRowCount`.
12571
+ // Resolving against the whole row/card takes the first checkbox in document order, which
12572
+ // is the selection one only while `selectable` is on — with it off and a `tn-checkbox`
12573
+ // projected through `[tnRowActionsDef]`, this reported that checkbox's state instead of
12574
+ // failing to find a selection control.
12575
+ const ancestor = (await this.isCards())
12576
+ ? `.tn-table__card[data-row-index="${rowIndex}"] .tn-table__card-select`
12577
+ : `.tn-table__row[data-row-index="${rowIndex}"] .tn-table__select-cell`;
12578
+ const checkbox = await this.locatorFor(TnCheckboxHarness.with({ ancestor }))();
11784
12579
  return checkbox.isChecked();
11785
12580
  }
11786
12581
  /**
11787
- * Gets the count of currently selected rows.
12582
+ * Gets the count of currently selected rows, in either layout.
11788
12583
  *
11789
12584
  * @returns Promise resolving to the number of checked row checkboxes.
11790
12585
  */
11791
12586
  async getSelectedRowCount() {
11792
- const checkboxes = await this.locatorForAll(TnCheckboxHarness.with({ ancestor: '.tn-table__row' }))();
12587
+ // Scoped to the select cell, not the whole row/card: a checkbox projected through
12588
+ // `[tnRowActionsDef]` sits inside the row, and in card mode the detail panel is a
12589
+ // descendant of the card, so a checkbox in a detail template would inflate the count.
12590
+ // (Table mode escaped the second case only because its detail row is a sibling `<tr>`.)
12591
+ const ancestor = (await this.isCards())
12592
+ ? '.tn-table__card-select'
12593
+ : '.tn-table__row .tn-table__select-cell';
12594
+ const checkboxes = await this.locatorForAll(TnCheckboxHarness.with({ ancestor }))();
11793
12595
  let count = 0;
11794
12596
  for (const cb of checkboxes) {
11795
12597
  if (await cb.isChecked()) {
@@ -11803,8 +12605,11 @@ class TnTableHarness extends ComponentHarness {
11803
12605
  * Clicks the expand button for a specific row.
11804
12606
  *
11805
12607
  * @param rowIndex Zero-based index of the data row.
12608
+ * @throws Error in the card layout — use `toggleCardDetail()`;
12609
+ * the thrown message names the card API.
11806
12610
  */
11807
12611
  async toggleRowExpansion(rowIndex) {
12612
+ await this.assertTableLayout('toggleRowExpansion');
11808
12613
  await this.assertRowExists(rowIndex);
11809
12614
  const button = await this.locatorFor(`.tn-table__row[data-row-index="${rowIndex}"] .tn-table__expand-button`)();
11810
12615
  await button.click();
@@ -11814,8 +12619,10 @@ class TnTableHarness extends ComponentHarness {
11814
12619
  *
11815
12620
  * @param rowIndex Zero-based index of the data row.
11816
12621
  * @returns Promise resolving to true if the row has the expanded class.
12622
+ * @throws Error in the card layout, which renders no rows; the thrown message names the card API.
11817
12623
  */
11818
12624
  async isRowExpanded(rowIndex) {
12625
+ await this.assertTableLayout('isRowExpanded');
11819
12626
  await this.assertRowExists(rowIndex);
11820
12627
  const row = await this.locatorFor(`.tn-table__row[data-row-index="${rowIndex}"]`)();
11821
12628
  return row.hasClass('tn-table__row--expanded');
@@ -11826,8 +12633,11 @@ class TnTableHarness extends ComponentHarness {
11826
12633
  *
11827
12634
  * @param rowIndex Zero-based index of the data row.
11828
12635
  * @returns Promise resolving to true if the row has an expand button.
12636
+ * @throws Error in the card layout, whose expand control is the card's Details button;
12637
+ * the thrown message names the card API.
11829
12638
  */
11830
12639
  async hasExpandControl(rowIndex) {
12640
+ await this.assertTableLayout('hasExpandControl');
11831
12641
  await this.assertRowExists(rowIndex);
11832
12642
  const button = await this.locatorForOptional(`.tn-table__row[data-row-index="${rowIndex}"] .tn-table__expand-button`)();
11833
12643
  return button !== null;
@@ -11837,8 +12647,10 @@ class TnTableHarness extends ComponentHarness {
11837
12647
  * Clicks a row (for tables with `clickable` enabled).
11838
12648
  *
11839
12649
  * @param rowIndex Zero-based index of the data row.
12650
+ * @throws Error in the card layout, which renders no rows; use {@link clickCard} there.
11840
12651
  */
11841
12652
  async clickRow(rowIndex) {
12653
+ await this.assertTableLayout('clickRow');
11842
12654
  await this.assertRowExists(rowIndex);
11843
12655
  const row = await this.locatorFor(`.tn-table__row[data-row-index="${rowIndex}"]`)();
11844
12656
  await row.click();
@@ -11849,8 +12661,10 @@ class TnTableHarness extends ComponentHarness {
11849
12661
  * clicks first; this helper dispatches only the `dblclick` event.
11850
12662
  *
11851
12663
  * @param rowIndex Zero-based index of the data row.
12664
+ * @throws Error in the card layout, which renders no rows; use {@link doubleClickCard} there.
11852
12665
  */
11853
12666
  async doubleClickRow(rowIndex) {
12667
+ await this.assertTableLayout('doubleClickRow');
11854
12668
  await this.assertRowExists(rowIndex);
11855
12669
  const row = await this.locatorFor(`.tn-table__row[data-row-index="${rowIndex}"]`)();
11856
12670
  await row.dispatchEvent('dblclick');
@@ -11860,8 +12674,10 @@ class TnTableHarness extends ComponentHarness {
11860
12674
  *
11861
12675
  * @param rowIndex Zero-based index of the data row.
11862
12676
  * @param key Which key to press — Enter or Space.
12677
+ * @throws Error in the card layout, which renders no rows; use {@link pressKeyOnCard} there.
11863
12678
  */
11864
12679
  async pressKeyOnRow(rowIndex, key) {
12680
+ await this.assertTableLayout('pressKeyOnRow');
11865
12681
  await this.assertRowExists(rowIndex);
11866
12682
  const row = await this.locatorFor(`.tn-table__row[data-row-index="${rowIndex}"]`)();
11867
12683
  await row.focus();
@@ -11876,8 +12692,10 @@ class TnTableHarness extends ComponentHarness {
11876
12692
  * Checks if a row is keyboard-focusable (tabindex=0).
11877
12693
  *
11878
12694
  * @param rowIndex Zero-based index of the data row.
12695
+ * @throws Error in the card layout, which renders no rows; use {@link isCardFocusable} there.
11879
12696
  */
11880
12697
  async isRowFocusable(rowIndex) {
12698
+ await this.assertTableLayout('isRowFocusable');
11881
12699
  await this.assertRowExists(rowIndex);
11882
12700
  const row = await this.locatorFor(`.tn-table__row[data-row-index="${rowIndex}"]`)();
11883
12701
  return (await row.getAttribute('tabindex')) === '0';
@@ -11896,10 +12714,17 @@ class TnTableHarness extends ComponentHarness {
11896
12714
  /**
11897
12715
  * Checks if a data row is currently marked active.
11898
12716
  *
11899
- * @param rowIndex Zero-based index of the data row.
11900
- * @returns Promise resolving to true if the row has the active class.
12717
+ * Layout-aware: reads the card's active class in the card layout, which marks the same
12718
+ * state with `aria-current` rather than the row's `aria-selected`.
12719
+ *
12720
+ * @param rowIndex Zero-based index of the data row (or card).
12721
+ * @returns Promise resolving to true if the row — or card — has the active class.
11901
12722
  */
11902
12723
  async isRowActive(rowIndex) {
12724
+ if (await this.isCards()) {
12725
+ const card = await this.locatorFor(`.tn-table__card[data-row-index="${rowIndex}"]`)();
12726
+ return card.hasClass('tn-table__card--active');
12727
+ }
11903
12728
  await this.assertRowExists(rowIndex);
11904
12729
  const row = await this.locatorFor(`.tn-table__row[data-row-index="${rowIndex}"]`)();
11905
12730
  return row.hasClass('tn-table__row--active');
@@ -11910,11 +12735,19 @@ class TnTableHarness extends ComponentHarness {
11910
12735
  * @returns Promise resolving to the active row index or null.
11911
12736
  */
11912
12737
  async getActiveRowIndex() {
11913
- const row = await this.locatorForOptional('.tn-table__row--active')();
11914
- if (!row) {
12738
+ // Layout-aware for the same reason as the selection block: card mode marks the active
12739
+ // row as `.tn-table__card--active` and renders no `.tn-table__row` at all, so a
12740
+ // row-only locator answered null — "nothing is active" — over a visibly active card.
12741
+ // Silent wrong answers are worse than a throw; this one could green a test after a
12742
+ // resize.
12743
+ const selector = (await this.isCards())
12744
+ ? '.tn-table__card--active'
12745
+ : '.tn-table__row--active';
12746
+ const active = await this.locatorForOptional(selector)();
12747
+ if (!active) {
11915
12748
  return null;
11916
12749
  }
11917
- const attr = await row.getAttribute('data-row-index');
12750
+ const attr = await active.getAttribute('data-row-index');
11918
12751
  return attr === null ? null : Number(attr);
11919
12752
  }
11920
12753
  /**
@@ -11922,8 +12755,11 @@ class TnTableHarness extends ComponentHarness {
11922
12755
  *
11923
12756
  * @param detailIndex Zero-based index among currently visible detail rows.
11924
12757
  * @returns Promise resolving to the detail row text.
12758
+ * @throws Error in the card layout — use the card detail panel;
12759
+ * the thrown message names the card API.
11925
12760
  */
11926
12761
  async getDetailRowContent(detailIndex) {
12762
+ await this.assertTableLayout('getDetailRowContent');
11927
12763
  const detailRows = await this.locatorForAll('.tn-table__detail-row')();
11928
12764
  if (detailIndex >= detailRows.length) {
11929
12765
  throw new Error(`Detail row index ${detailIndex} out of bounds (${detailRows.length} detail rows)`);
@@ -11933,21 +12769,355 @@ class TnTableHarness extends ComponentHarness {
11933
12769
  /**
11934
12770
  * Gets the count of currently expanded detail rows.
11935
12771
  *
11936
- * @returns Promise resolving to the number of visible detail rows.
12772
+ * Layout-aware: counts open card detail panels in the card layout, which renders the same
12773
+ * expansion state without a detail `<tr>`.
12774
+ *
12775
+ * @returns Promise resolving to the number of visible detail rows, or open card detail
12776
+ * panels in the card layout.
11937
12777
  */
11938
12778
  async getExpandedRowCount() {
12779
+ if (await this.isCards()) {
12780
+ const panels = await this.locatorForAll('.tn-table__card-detail')();
12781
+ return panels.length;
12782
+ }
11939
12783
  const detailRows = await this.locatorForAll('.tn-table__detail-row')();
11940
12784
  return detailRows.length;
11941
12785
  }
12786
+ // --- Card layout (responsive) ---
12787
+ /**
12788
+ * Reports the currently rendered layout: `cards` when the container is narrow
12789
+ * enough that `mobileLayout="cards"` has taken effect, otherwise `table`.
12790
+ *
12791
+ * @returns Promise resolving to 'cards' or 'table'.
12792
+ */
12793
+ async getLayoutMode() {
12794
+ const cards = await this.locatorForOptional('.tn-table__cards')();
12795
+ return cards ? 'cards' : 'table';
12796
+ }
12797
+ /**
12798
+ * Gets the number of rendered cards (card layout only).
12799
+ *
12800
+ * @returns Promise resolving to the card count.
12801
+ */
12802
+ async getCardCount() {
12803
+ await this.assertCardLayout('getCardCount');
12804
+ const cards = await this.locatorForAll('.tn-table__card')();
12805
+ return cards.length;
12806
+ }
12807
+ /**
12808
+ * Gets the title text of a card.
12809
+ *
12810
+ * @param cardIndex Zero-based index of the card.
12811
+ * @returns Promise resolving to the card title text.
12812
+ */
12813
+ async getCardTitle(cardIndex) {
12814
+ await this.assertCardLayout('getCardTitle');
12815
+ const title = await this.locatorFor(`.tn-table__card[data-row-index="${cardIndex}"] .tn-table__card-title`)();
12816
+ return (await title.text()).trim();
12817
+ }
12818
+ /**
12819
+ * Gets the value text of a field within a card, by column name. The field
12820
+ * may be a primary field or one tucked under "More fields".
12821
+ *
12822
+ * @param cardIndex Zero-based index of the card.
12823
+ * @param columnName The column's data-column attribute value.
12824
+ * @returns Promise resolving to the field's value text.
12825
+ */
12826
+ async getCardFieldValue(cardIndex, columnName) {
12827
+ await this.assertCardLayout('getCardFieldValue');
12828
+ const value = await this.locatorFor(`.tn-table__card[data-row-index="${cardIndex}"] .tn-table__card-field[data-column="${columnName}"] .tn-table__card-field-value`)();
12829
+ return (await value.text()).trim();
12830
+ }
12831
+ /**
12832
+ * Gets the column names of the fields shown directly on a card (i.e. not
12833
+ * those hidden behind the "More fields" disclosure).
12834
+ *
12835
+ * @param cardIndex Zero-based index of the card.
12836
+ * @returns Promise resolving to an array of column names.
12837
+ */
12838
+ async getCardPrimaryFieldColumns(cardIndex) {
12839
+ await this.assertCardLayout('getCardPrimaryFieldColumns');
12840
+ const fields = await this.locatorForAll(`.tn-table__card[data-row-index="${cardIndex}"] > .tn-table__card-fields > .tn-table__card-field[data-column]`)();
12841
+ const columns = [];
12842
+ for (const field of fields) {
12843
+ const col = await field.getAttribute('data-column');
12844
+ if (col !== null) {
12845
+ columns.push(col);
12846
+ }
12847
+ }
12848
+ return columns;
12849
+ }
12850
+ /**
12851
+ * Expands the "More fields" disclosure on a card to reveal lower-priority
12852
+ * fields. No-op if the card has no secondary fields.
12853
+ *
12854
+ * @param cardIndex Zero-based index of the card.
12855
+ */
12856
+ async expandCardMoreFields(cardIndex) {
12857
+ await this.assertCardLayout('expandCardMoreFields');
12858
+ const disclosure = await this.locatorForOptional(`.tn-table__card[data-row-index="${cardIndex}"] .tn-table__card-more`)();
12859
+ if (!disclosure) {
12860
+ return;
12861
+ }
12862
+ // Idempotent, because the name and the doc promise an expand while `<summary>` toggles.
12863
+ // A second call used to collapse it — and under a real WebDriver, where `text()` returns
12864
+ // only visible text, `getCardFieldValue()` on a folded field would then come back empty.
12865
+ if (await disclosure.getProperty('open')) {
12866
+ return;
12867
+ }
12868
+ const summary = await this.locatorFor(`.tn-table__card[data-row-index="${cardIndex}"] .tn-table__card-more-summary`)();
12869
+ await summary.click();
12870
+ }
12871
+ /**
12872
+ * Toggles a card's detail section (card layout equivalent of row expansion).
12873
+ *
12874
+ * @param cardIndex Zero-based index of the card.
12875
+ */
12876
+ async toggleCardDetail(cardIndex) {
12877
+ await this.assertCardLayout('toggleCardDetail');
12878
+ const toggle = await this.locatorFor(`.tn-table__card[data-row-index="${cardIndex}"] .tn-table__card-detail-toggle`)();
12879
+ await toggle.click();
12880
+ }
12881
+ /**
12882
+ * Gets the currently selected sort column in the card-layout sort menu, or
12883
+ * `''` when unsorted. Returns null if the sort menu isn't rendered.
12884
+ *
12885
+ * @returns Promise resolving to the selected column name, '', or null.
12886
+ */
12887
+ async getCardSortColumn() {
12888
+ const select = await this.locatorForOptional('.tn-table__cards-sort-select')();
12889
+ if (!select) {
12890
+ return null;
12891
+ }
12892
+ return select.getProperty('value');
12893
+ }
12894
+ /**
12895
+ * Gets the active sort direction in the card layout, read from the direction toggle's
12896
+ * `data-sort-direction`. Deliberately not derived from the button's `aria-label`: matching
12897
+ * display text would silently report one direction forever if the wording or locale changed.
12898
+ *
12899
+ * @returns Promise resolving to 'asc' or 'desc'; `''` both when sorted by nothing and when the
12900
+ * active column isn't sortable (the toggle isn't rendered in either case, and this reads the
12901
+ * rendered control — so a non-sortable active column reports `''` even though the component's
12902
+ * `sortDirection()` holds a value); or null when the card sort menu isn't rendered at all
12903
+ * (table layout, or no sortable columns) — the same null-vs-empty distinction
12904
+ * {@link getCardSortColumn} makes.
12905
+ */
12906
+ async getCardSortDirection() {
12907
+ const menu = await this.locatorForOptional('.tn-table__cards-sort')();
12908
+ if (!menu) {
12909
+ return null;
12910
+ }
12911
+ const button = await this.locatorForOptional('.tn-table__cards-sort-dir')();
12912
+ if (!button) {
12913
+ return '';
12914
+ }
12915
+ const direction = await button.getAttribute('data-sort-direction');
12916
+ // Report the empty direction rather than rounding it up to 'asc'. A column set with
12917
+ // no direction is not sorted, and claiming otherwise made this getter contradict its
12918
+ // own contract — and hid the fact that the button rendered a direction for it.
12919
+ if (direction === 'asc' || direction === 'desc') {
12920
+ return direction;
12921
+ }
12922
+ return '';
12923
+ }
12924
+ /**
12925
+ * Clicks the sort-direction toggle in the card-layout sort menu. No-op when the toggle
12926
+ * isn't rendered, which is three cases: the table layout is showing (the whole card sort
12927
+ * menu is absent — so a call made before the container narrowed does nothing and reports
12928
+ * success), no active sort column, or an active column that isn't `sortable()` — card mode
12929
+ * deliberately declines to reorder by a column whose table header ignores clicks.
12930
+ */
12931
+ async toggleCardSortDirection() {
12932
+ const button = await this.locatorForOptional('.tn-table__cards-sort-dir')();
12933
+ if (button) {
12934
+ await button.click();
12935
+ }
12936
+ }
12937
+ // --- Card activation ---
12938
+ //
12939
+ // Mirrors of the four clickable-row methods. Without them the row methods' `@throws` sent a
12940
+ // card-mode spec to a card API that could not activate anything, and the only way through
12941
+ // was raw DOM — which is what the harness exists to spare a consumer.
12942
+ /**
12943
+ * Clicks a card body (card mirror of {@link clickRow}, for tables with `clickable`).
12944
+ *
12945
+ * Targets the card's title rather than the card box, because the card box's centre can sit
12946
+ * on a projected row action or the selection checkbox, and the activation guard ignores
12947
+ * clicks that originate on a control — the click would land and nothing would happen.
12948
+ *
12949
+ * @param cardIndex Zero-based index of the card.
12950
+ * @throws Error in the table layout; use {@link clickRow} there.
12951
+ */
12952
+ async clickCard(cardIndex) {
12953
+ await this.assertCardLayout('clickCard');
12954
+ await this.assertIndexExists(cardIndex);
12955
+ const target = await this.cardActivationTarget(cardIndex);
12956
+ await target.click();
12957
+ }
12958
+ /**
12959
+ * Double-clicks a card, triggering `rowDoubleClick` (card mirror of
12960
+ * {@link doubleClickRow}). As there, only the `dblclick` event is dispatched — a real
12961
+ * double-click also fires two single clicks first.
12962
+ *
12963
+ * Dispatched on the card rather than on the title {@link clickCard} aims at, because a
12964
+ * synthesised event does not bubble: sent to the title it would never reach the card's
12965
+ * handler. There is no cost to it here — the target is set explicitly, so unlike a real
12966
+ * pointer click it cannot land on a projected control.
12967
+ *
12968
+ * @param cardIndex Zero-based index of the card.
12969
+ * @throws Error in the table layout; use {@link doubleClickRow} there.
12970
+ */
12971
+ async doubleClickCard(cardIndex) {
12972
+ await this.assertCardLayout('doubleClickCard');
12973
+ await this.assertIndexExists(cardIndex);
12974
+ const card = await this.cardAt(cardIndex);
12975
+ await card.dispatchEvent('dblclick');
12976
+ }
12977
+ /**
12978
+ * Sends a keyboard event to a card (Enter/Space activate clickable cards). Card mirror of
12979
+ * {@link pressKeyOnRow}; the key goes to the card itself, which is the element that carries
12980
+ * `tabindex` and the keydown handler.
12981
+ *
12982
+ * @param cardIndex Zero-based index of the card.
12983
+ * @param key Which key to press — Enter or Space.
12984
+ * @throws Error in the table layout; use {@link pressKeyOnRow} there.
12985
+ */
12986
+ async pressKeyOnCard(cardIndex, key) {
12987
+ await this.assertCardLayout('pressKeyOnCard');
12988
+ await this.assertIndexExists(cardIndex);
12989
+ const card = await this.cardAt(cardIndex);
12990
+ await card.focus();
12991
+ if (key === 'enter') {
12992
+ await card.sendKeys(TestKey.ENTER);
12993
+ }
12994
+ else {
12995
+ await card.sendKeys(' ');
12996
+ }
12997
+ }
12998
+ /**
12999
+ * Checks if a card is keyboard-focusable (tabindex=0). Card mirror of
13000
+ * {@link isRowFocusable}.
13001
+ *
13002
+ * @param cardIndex Zero-based index of the card.
13003
+ * @throws Error in the table layout; use {@link isRowFocusable} there.
13004
+ */
13005
+ async isCardFocusable(cardIndex) {
13006
+ await this.assertCardLayout('isCardFocusable');
13007
+ await this.assertIndexExists(cardIndex);
13008
+ const card = await this.cardAt(cardIndex);
13009
+ return (await card.getAttribute('tabindex')) === '0';
13010
+ }
11942
13011
  // --- Internal helpers ---
11943
- async assertRowExists(rowIndex) {
13012
+ /** The card box itself — the element carrying `tabindex`, the handlers and the ARIA state. */
13013
+ async cardAt(cardIndex) {
13014
+ return this.locatorFor(`.tn-table__card[data-row-index="${cardIndex}"]`)();
13015
+ }
13016
+ /** The surface pointer activation is aimed at — see {@link clickCard} for why not the card. */
13017
+ async cardActivationTarget(cardIndex) {
13018
+ const title = await this.locatorForOptional(`.tn-table__card[data-row-index="${cardIndex}"] .tn-table__card-title`)();
13019
+ return title ?? (await this.cardAt(cardIndex));
13020
+ }
13021
+ /**
13022
+ * Bounds check for the layout-aware methods. Counts whatever the rendered layout shows, via
13023
+ * `getRowCount()` — unlike {@link assertRowExists}, which deliberately counts rows only,
13024
+ * because its callers refuse card mode outright.
13025
+ */
13026
+ async assertIndexExists(index) {
11944
13027
  const count = await this.getRowCount();
11945
- if (rowIndex >= count) {
11946
- throw new Error(`Row index ${rowIndex} out of bounds (${count} rows)`);
13028
+ if (index >= count) {
13029
+ const noun = (await this.isCards()) ? 'cards' : 'rows';
13030
+ throw new Error(`Row index ${index} out of bounds (${count} ${noun})`);
13031
+ }
13032
+ }
13033
+ async assertRowExists(rowIndex) {
13034
+ // Counts `.tn-table__row` directly rather than calling `getRowCount()`, which is
13035
+ // layout-aware: in card mode that counts cards, so this guard passed for every valid
13036
+ // index and stopped catching the very mismatch it exists for. Row-only methods pair it
13037
+ // with `assertTableLayout()`, which reports the layout mismatch first.
13038
+ const rows = await this.locatorForAll('.tn-table__row')();
13039
+ if (rowIndex >= rows.length) {
13040
+ throw new Error(`Row index ${rowIndex} out of bounds (${rows.length} rows)`);
11947
13041
  }
11948
13042
  }
11949
13043
  }
11950
13044
 
13045
+ /**
13046
+ * Stand-in for `ResizeObserver`, which jsdom does not implement — so a `tn-table` under
13047
+ * test can never measure its own container and never leaves the table layout on its own.
13048
+ * Installing this lets a spec push a width through the component's real callback path
13049
+ * rather than reaching for its private state.
13050
+ *
13051
+ * Module-private on purpose. `MockResizeObserver` is a name a consumer's own test helpers
13052
+ * plausibly already define — this repo's own `tree-virtual-scroll-view.component.spec.ts`
13053
+ * declares one — and a wildcard import of the library would collide with it. Everything a
13054
+ * caller needs is reachable through {@link TnTableTesting}.
13055
+ */
13056
+ class MockResizeObserver {
13057
+ cb;
13058
+ static instances = [];
13059
+ constructor(cb) {
13060
+ this.cb = cb;
13061
+ MockResizeObserver.instances.push(this);
13062
+ }
13063
+ observe() { }
13064
+ unobserve() { }
13065
+ disconnect() { }
13066
+ /** Fires the observed callback with `width` as the element's content-box width. */
13067
+ emitWidth(width) {
13068
+ this.cb([{ contentRect: { width } }], this);
13069
+ }
13070
+ }
13071
+ /**
13072
+ * Testing utilities for `tn-table`'s container-driven responsive layout.
13073
+ *
13074
+ * Part of the public API because `TnTableHarness`'s layout-mismatch messages tell the caller
13075
+ * to widen or narrow the container past `cardBreakpoint`, and jsdom implements no
13076
+ * `ResizeObserver` — so without a stand-in a consumer's own spec has no supported way to
13077
+ * follow that advice.
13078
+ *
13079
+ * Namespaced rather than exported as loose functions, matching `TnIconTesting` and
13080
+ * `TnToastTesting`: these names live at the top level of the package, and `installObserver`
13081
+ * or `emitContainerWidth` on their own give no hint that they concern tables.
13082
+ *
13083
+ * @example
13084
+ * ```typescript
13085
+ * let restoreResizeObserver: () => void;
13086
+ *
13087
+ * beforeEach(() => { restoreResizeObserver = TnTableTesting.installResizeObserver(); });
13088
+ * afterEach(() => { restoreResizeObserver(); });
13089
+ *
13090
+ * // ...then, inside a test:
13091
+ * TnTableTesting.emitContainerWidth(320);
13092
+ * fixture.detectChanges();
13093
+ * ```
13094
+ */
13095
+ const TnTableTesting = {
13096
+ /**
13097
+ * Swaps `globalThis.ResizeObserver` for a stand-in and clears any instances left by a
13098
+ * previous test.
13099
+ *
13100
+ * @returns A function that restores the original global — call it in `afterEach`.
13101
+ */
13102
+ installResizeObserver() {
13103
+ const original = globalThis.ResizeObserver;
13104
+ MockResizeObserver.instances = [];
13105
+ globalThis.ResizeObserver = MockResizeObserver;
13106
+ return () => {
13107
+ globalThis.ResizeObserver = original;
13108
+ };
13109
+ },
13110
+ /**
13111
+ * Pushes `width` through every observer the component under test registered, as its
13112
+ * content-box width — the same quantity `measureContainer` reads on the initial pass.
13113
+ *
13114
+ * @param width Content-box width in px. A 0 reads as unmeasurable, not narrow.
13115
+ */
13116
+ emitContainerWidth(width) {
13117
+ MockResizeObserver.instances.forEach((observer) => observer.emitWidth(width));
13118
+ },
13119
+ };
13120
+
11951
13121
  /** English defaults used when no `TN_TABLE_PAGER_LABELS` provider is registered. */
11952
13122
  const TN_TABLE_PAGER_DEFAULT_LABELS = {
11953
13123
  itemsPerPage: 'Items per page',
@@ -19351,7 +20521,7 @@ class TnFilePickerPopupComponent {
19351
20521
  return `${(date.getMonth() + 1).toString().padStart(2, '0')}/${date.getDate().toString().padStart(2, '0')}/${date.getFullYear()} ${timePart}`;
19352
20522
  }
19353
20523
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnFilePickerPopupComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
19354
- 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", "emptyDescription", "emptyIcon", "selectable", "expandable", "bordered", "isRowExpandable", "activeRow", "activeWhen", "activeBg", "activeIndicator", "loading", "loadingMessage", "clickable", "expandOnRowClick", "singleExpand", "fixedLayout", "minColumnWidth", "minWidth", "sortColumn", "sortDirection"], outputs: ["sortChange", "selectionChange", "rowClick", "rowDoubleClick", "sortColumnChange", "sortDirectionChange"] }, { 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" }] });
20524
+ 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", "emptyDescription", "emptyIcon", "selectable", "expandable", "bordered", "isRowExpandable", "activeRow", "activeWhen", "activeBg", "activeIndicator", "loading", "loadingMessage", "clickable", "expandOnRowClick", "singleExpand", "fixedLayout", "minColumnWidth", "minWidth", "mobileLayout", "cardBreakpoint", "cardPrimaryCount", "sortColumn", "sortDirection"], outputs: ["sortChange", "selectionChange", "rowClick", "rowDoubleClick", "sortColumnChange", "sortDirectionChange"] }, { kind: "directive", type: TnTableColumnDirective, selector: "[tnColumnDef]", inputs: ["tnColumnDef", "sortable", "width", "label", "cardPriority", "cardTitle", "cardHidden", "cardLabel"], 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" }] });
19355
20525
  }
19356
20526
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnFilePickerPopupComponent, decorators: [{
19357
20527
  type: Component,
@@ -21207,5 +22377,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
21207
22377
  * Generated bundle index. Do not edit.
21208
22378
  */
21209
22379
 
21210
- 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_RADIO_GROUP, 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, TnRadioGroupComponent, TnRadioGroupHarness, 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 };
22380
+ 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_RADIO_GROUP, 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, TnRadioGroupComponent, TnRadioGroupHarness, TnRadioHarness, TnRowActionsDefDirective, 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, TnTableTesting, 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 };
21211
22381
  //# sourceMappingURL=truenas-ui-components.mjs.map