@truenas/ui-components 0.4.12 → 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.
@@ -11244,10 +11244,51 @@ class TnTableColumnDirective {
11244
11244
  name = input.required({ ...(ngDevMode ? { debugName: "name" } : {}), alias: 'tnColumnDef' });
11245
11245
  sortable = input(false, ...(ngDevMode ? [{ debugName: "sortable" }] : []));
11246
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" }] : []));
11247
11288
  headerTemplate = contentChild(TnHeaderCellDefDirective, { ...(ngDevMode ? { debugName: "headerTemplate" } : {}), read: TemplateRef });
11248
11289
  cellTemplate = contentChild(TnCellDefDirective, { ...(ngDevMode ? { debugName: "cellTemplate" } : {}), read: TemplateRef });
11249
11290
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnTableColumnDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
11250
- 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 });
11251
11292
  }
11252
11293
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnTableColumnDirective, decorators: [{
11253
11294
  type: Directive,
@@ -11256,7 +11297,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
11256
11297
  standalone: true,
11257
11298
  exportAs: 'tnColumnDef',
11258
11299
  }]
11259
- }], 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), { ...{
11260
11301
  read: TemplateRef,
11261
11302
  }, isSignal: true }] }], cellTemplate: [{ type: i0.ContentChild, args: [i0.forwardRef(() => TnCellDefDirective), { ...{ read: TemplateRef }, isSignal: true }] }] } });
11262
11303
  /**
@@ -11285,6 +11326,38 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
11285
11326
  standalone: true,
11286
11327
  }]
11287
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
+ }] });
11288
11361
 
11289
11362
  /**
11290
11363
  * Animation duration for detail row expand/collapse.
@@ -11307,6 +11380,16 @@ function getExpandDuration() {
11307
11380
  }
11308
11381
  class TnTableComponent {
11309
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;
11310
11393
  // --- Core inputs ---
11311
11394
  dataSource = input([], ...(ngDevMode ? [{ debugName: "dataSource" }] : []));
11312
11395
  displayedColumns = input([], ...(ngDevMode ? [{ debugName: "displayedColumns" }] : []));
@@ -11374,6 +11457,12 @@ class TnTableComponent {
11374
11457
  * When true, shows a spinner overlay over the table. Existing rows remain
11375
11458
  * visible (dimmed) so reloads don't cause layout jumps; if there are no rows
11376
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.
11377
11466
  */
11378
11467
  loading = input(false, ...(ngDevMode ? [{ debugName: "loading" }] : []));
11379
11468
  /** Accessible label announced while loading. */
@@ -11382,6 +11471,16 @@ class TnTableComponent {
11382
11471
  * When true, rows become keyboard-focusable (tabindex=0) and clicking or
11383
11472
  * pressing Enter/Space emits `rowClick`. Use this for "click row to view
11384
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.
11385
11484
  */
11386
11485
  clickable = input(false, ...(ngDevMode ? [{ debugName: "clickable" }] : []));
11387
11486
  /**
@@ -11389,6 +11488,12 @@ class TnTableComponent {
11389
11488
  * chevron. Requires `clickable` — that is what makes rows activatable at all — and `expandable`;
11390
11489
  * rows the `isRowExpandable` predicate rejects are unaffected, since `toggleRowExpansion` gates
11391
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.
11392
11497
  */
11393
11498
  expandOnRowClick = input(false, ...(ngDevMode ? [{ debugName: "expandOnRowClick" }] : []));
11394
11499
  /**
@@ -11443,9 +11548,52 @@ class TnTableComponent {
11443
11548
  if (!perColumn || !this.fixedLayout()) {
11444
11549
  return null;
11445
11550
  }
11446
- 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})`;
11447
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" }] : []));
11448
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
+ */
11449
11597
  sortChange = output();
11450
11598
  selectionChange = output();
11451
11599
  /** Emits the row when a clickable row is activated (click or Enter/Space). */
@@ -11460,6 +11608,11 @@ class TnTableComponent {
11460
11608
  // --- Content queries ---
11461
11609
  columnDefs = contentChildren(TnTableColumnDirective, ...(ngDevMode ? [{ debugName: "columnDefs" }] : []));
11462
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;
11463
11616
  // --- Sort state ---
11464
11617
  /**
11465
11618
  * Sorted column and direction. Two-way bindable (`[(sortColumn)]`), so a consumer that owns the
@@ -11475,6 +11628,9 @@ class TnTableComponent {
11475
11628
  * approach could address this.
11476
11629
  */
11477
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++}`;
11478
11634
  // --- Selection state ---
11479
11635
  selection = new SelectionModel(true, []);
11480
11636
  selectionCount = signal(0, ...(ngDevMode ? [{ debugName: "selectionCount" }] : []));
@@ -11535,14 +11691,147 @@ class TnTableComponent {
11535
11691
  this.expandedRows.set(next);
11536
11692
  }
11537
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);
11538
11822
  }
11539
11823
  ngOnInit() {
11540
11824
  this.initialized = true;
11541
- // Clean up SelectionModel on destroy
11542
11825
  this.destroyRef.onDestroy(() => {
11543
11826
  this.selection.clear();
11827
+ this.resizeObserver?.disconnect();
11544
11828
  });
11545
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" }] : []));
11546
11835
  // --- Computed ---
11547
11836
  data = computed(() => {
11548
11837
  const source = this.dataSource();
@@ -11561,6 +11850,13 @@ class TnTableComponent {
11561
11850
  }
11562
11851
  return cols;
11563
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" }] : []));
11564
11860
  isAllSelected = computed(() => {
11565
11861
  const numSelected = this.selectionCount();
11566
11862
  const numRows = this.data().length;
@@ -11583,13 +11879,15 @@ class TnTableComponent {
11583
11879
  if (!colDef?.sortable()) {
11584
11880
  return;
11585
11881
  }
11586
- if (this.sortColumn() === column) {
11587
- const current = this.sortDirection();
11588
- if (current === 'asc') {
11589
- this.sortDirection.set('desc');
11590
- }
11591
- else if (current === 'desc') {
11592
- 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()) {
11593
11891
  this.sortColumn.set('');
11594
11892
  }
11595
11893
  }
@@ -11597,8 +11895,12 @@ class TnTableComponent {
11597
11895
  this.sortColumn.set(column);
11598
11896
  this.sortDirection.set('asc');
11599
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.
11600
11902
  this.sortChange.emit({
11601
- column: this.sortColumn() || column,
11903
+ column: this.sortColumn(),
11602
11904
  direction: this.sortDirection(),
11603
11905
  });
11604
11906
  }
@@ -11649,6 +11951,21 @@ class TnTableComponent {
11649
11951
  isRowExpandTrigger(row) {
11650
11952
  return this.expandOnRowClick() && this.clickable() && this.canExpandRow(row);
11651
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
+ }
11652
11969
  // --- Active row ---
11653
11970
  isRowActive(row) {
11654
11971
  if (this.activeWhen()?.(row)) {
@@ -11658,25 +11975,47 @@ class TnTableComponent {
11658
11975
  return active !== null && active === row;
11659
11976
  }
11660
11977
  // --- Row click ---
11661
- 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) {
11662
11985
  if (!this.clickable()) {
11663
11986
  return;
11664
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
+ }
11665
11997
  if (this.expandOnRowClick()) {
11666
11998
  this.toggleRowExpansion(row);
11667
11999
  }
11668
12000
  this.rowClick.emit(row);
11669
12001
  }
11670
- onRowDoubleClick(row) {
12002
+ /** @param event See {@link onRowClick} — same guard, same optionality. */
12003
+ onRowDoubleClick(row, event) {
11671
12004
  if (!this.clickable()) {
11672
12005
  return;
11673
12006
  }
12007
+ if (event && this.isRowControlTarget(event)) {
12008
+ return;
12009
+ }
11674
12010
  this.rowDoubleClick.emit(row);
11675
12011
  }
11676
12012
  onRowKeydown(event, row) {
11677
12013
  if (!this.clickable()) {
11678
12014
  return;
11679
12015
  }
12016
+ if (this.isRowControlTarget(event)) {
12017
+ return;
12018
+ }
11680
12019
  if (event.key === 'Enter' || event.key === ' ') {
11681
12020
  event.preventDefault();
11682
12021
  if (this.expandOnRowClick()) {
@@ -11685,6 +12024,135 @@ class TnTableComponent {
11685
12024
  this.rowClick.emit(row);
11686
12025
  }
11687
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
+ }
11688
12156
  // --- Selection methods ---
11689
12157
  toggleSelectAll() {
11690
12158
  if (this.isAllSelected()) {
@@ -11704,15 +12172,138 @@ class TnTableComponent {
11704
12172
  isRowSelected(row) {
11705
12173
  return this.selection.isSelected(row);
11706
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
+ }
11707
12290
  // --- Column helpers ---
11708
12291
  getColumnDef(columnName) {
11709
12292
  return this.columnDefMap().get(columnName);
11710
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
+ }
11711
12302
  getCellValue(row, column) {
11712
12303
  return row[column];
11713
12304
  }
11714
12305
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnTableComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
11715
- 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: [
11716
12307
  trigger('detailExpand', [
11717
12308
  state('collapsed,void', style({ height: '0px', minHeight: '0', overflow: 'hidden' })),
11718
12309
  state('expanded', style({ height: '*' })),
@@ -11733,10 +12324,17 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
11733
12324
  '[class.tn-table--bordered]': 'bordered()',
11734
12325
  '[class.tn-table--fixed-layout]': 'fixedLayout()',
11735
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()',
11736
12334
  '[style.--tn-table-active-bg]': 'activeBg()',
11737
12335
  '[style.--tn-table-active-indicator]': 'activeIndicator()',
11738
- }, 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"] }]
11739
- }], 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"] }] } });
11740
12338
 
11741
12339
  /**
11742
12340
  * Harness for interacting with `tn-table` in tests.
@@ -11770,18 +12368,59 @@ class TnTableHarness extends ComponentHarness {
11770
12368
  /**
11771
12369
  * Gets the number of data rows (excludes header and detail rows).
11772
12370
  *
11773
- * @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.
11774
12375
  */
11775
12376
  async getRowCount() {
11776
- 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)();
11777
12382
  return rows.length;
11778
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
+ }
11779
12415
  /**
11780
12416
  * Gets the text content of header cells (excludes sort icons).
11781
12417
  *
11782
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.
11783
12421
  */
11784
12422
  async getHeaderTexts() {
12423
+ await this.assertTableLayout('getHeaderTexts');
11785
12424
  const textEls = await this.locatorForAll('.tn-table__header-text')();
11786
12425
  const texts = [];
11787
12426
  for (const el of textEls) {
@@ -11794,8 +12433,10 @@ class TnTableHarness extends ComponentHarness {
11794
12433
  *
11795
12434
  * @param rowIndex Zero-based index of the data row.
11796
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.
11797
12437
  */
11798
12438
  async getRowTexts(rowIndex) {
12439
+ await this.assertTableLayout('getRowTexts');
11799
12440
  await this.assertRowExists(rowIndex);
11800
12441
  const cells = await this.locatorForAll(`.tn-table__row[data-row-index="${rowIndex}"] .tn-table__cell[data-column]`)();
11801
12442
  const texts = [];
@@ -11810,8 +12451,11 @@ class TnTableHarness extends ComponentHarness {
11810
12451
  * @param rowIndex Zero-based index of the data row.
11811
12452
  * @param columnName The column's data-column attribute value.
11812
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.
11813
12456
  */
11814
12457
  async getCellText(rowIndex, columnName) {
12458
+ await this.assertTableLayout('getCellText');
11815
12459
  await this.assertRowExists(rowIndex);
11816
12460
  const cell = await this.locatorFor(`.tn-table__row[data-row-index="${rowIndex}"] [data-column="${columnName}"]`)();
11817
12461
  return (await cell.text()).trim();
@@ -11820,8 +12464,10 @@ class TnTableHarness extends ComponentHarness {
11820
12464
  * Gets all row texts as a 2D string array (data-column cells only).
11821
12465
  *
11822
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.
11823
12468
  */
11824
12469
  async getAllRowTexts() {
12470
+ await this.assertTableLayout('getAllRowTexts');
11825
12471
  const count = await this.getRowCount();
11826
12472
  const result = [];
11827
12473
  for (let i = 0; i < count; i++) {
@@ -11834,8 +12480,11 @@ class TnTableHarness extends ComponentHarness {
11834
12480
  * Clicks a sortable column header to cycle sort direction.
11835
12481
  *
11836
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.
11837
12485
  */
11838
12486
  async clickSortHeader(columnName) {
12487
+ await this.assertTableLayout('clickSortHeader');
11839
12488
  const header = await this.locatorFor(`th[data-column="${columnName}"]`)();
11840
12489
  await header.click();
11841
12490
  }
@@ -11844,59 +12493,105 @@ class TnTableHarness extends ComponentHarness {
11844
12493
  *
11845
12494
  * @param columnName The column's data-column attribute value.
11846
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.
11847
12498
  */
11848
12499
  async isSortable(columnName) {
12500
+ await this.assertTableLayout('isSortable');
11849
12501
  const header = await this.locatorFor(`th[data-column="${columnName}"]`)();
11850
12502
  return header.hasClass('tn-table__header-cell--sortable');
11851
12503
  }
11852
12504
  /**
11853
12505
  * Gets the current sort direction for a column via aria-sort.
11854
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
+ *
11855
12511
  * @param columnName The column's data-column attribute value.
11856
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.
11857
12515
  */
11858
12516
  async getSortDirection(columnName) {
12517
+ await this.assertTableLayout('getSortDirection');
11859
12518
  const header = await this.locatorFor(`th[data-column="${columnName}"]`)();
11860
- return header.getAttribute('aria-sort');
12519
+ const sort = await header.getAttribute('aria-sort');
12520
+ return sort === 'none' ? null : sort;
11861
12521
  }
11862
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
+ }
11863
12537
  /**
11864
- * 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.
11865
12540
  */
11866
12541
  async toggleSelectAll() {
11867
- const cell = await this.locatorFor('.tn-table__header-row .tn-table__select-cell')();
11868
- 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();
11869
12547
  }
11870
12548
  /**
11871
- * 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.
11872
12551
  *
11873
12552
  * @param rowIndex Zero-based index of the data row.
11874
12553
  */
11875
12554
  async toggleRowSelection(rowIndex) {
11876
- await this.assertRowExists(rowIndex);
11877
- const cell = await this.locatorFor(`.tn-table__row[data-row-index="${rowIndex}"] .tn-table__select-cell`)();
11878
- 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();
11879
12561
  }
11880
12562
  /**
11881
- * Checks if a specific row's checkbox is checked.
12563
+ * Checks if a specific row (or its card) is selected.
11882
12564
  *
11883
12565
  * @param rowIndex Zero-based index of the data row.
11884
12566
  * @returns Promise resolving to true if the row's checkbox is checked.
11885
12567
  */
11886
12568
  async isRowSelected(rowIndex) {
11887
- await this.assertRowExists(rowIndex);
11888
- const checkbox = await this.locatorFor(TnCheckboxHarness.with({
11889
- ancestor: `.tn-table__row[data-row-index="${rowIndex}"]`,
11890
- }))();
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 }))();
11891
12579
  return checkbox.isChecked();
11892
12580
  }
11893
12581
  /**
11894
- * Gets the count of currently selected rows.
12582
+ * Gets the count of currently selected rows, in either layout.
11895
12583
  *
11896
12584
  * @returns Promise resolving to the number of checked row checkboxes.
11897
12585
  */
11898
12586
  async getSelectedRowCount() {
11899
- 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 }))();
11900
12595
  let count = 0;
11901
12596
  for (const cb of checkboxes) {
11902
12597
  if (await cb.isChecked()) {
@@ -11910,8 +12605,11 @@ class TnTableHarness extends ComponentHarness {
11910
12605
  * Clicks the expand button for a specific row.
11911
12606
  *
11912
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.
11913
12610
  */
11914
12611
  async toggleRowExpansion(rowIndex) {
12612
+ await this.assertTableLayout('toggleRowExpansion');
11915
12613
  await this.assertRowExists(rowIndex);
11916
12614
  const button = await this.locatorFor(`.tn-table__row[data-row-index="${rowIndex}"] .tn-table__expand-button`)();
11917
12615
  await button.click();
@@ -11921,8 +12619,10 @@ class TnTableHarness extends ComponentHarness {
11921
12619
  *
11922
12620
  * @param rowIndex Zero-based index of the data row.
11923
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.
11924
12623
  */
11925
12624
  async isRowExpanded(rowIndex) {
12625
+ await this.assertTableLayout('isRowExpanded');
11926
12626
  await this.assertRowExists(rowIndex);
11927
12627
  const row = await this.locatorFor(`.tn-table__row[data-row-index="${rowIndex}"]`)();
11928
12628
  return row.hasClass('tn-table__row--expanded');
@@ -11933,8 +12633,11 @@ class TnTableHarness extends ComponentHarness {
11933
12633
  *
11934
12634
  * @param rowIndex Zero-based index of the data row.
11935
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.
11936
12638
  */
11937
12639
  async hasExpandControl(rowIndex) {
12640
+ await this.assertTableLayout('hasExpandControl');
11938
12641
  await this.assertRowExists(rowIndex);
11939
12642
  const button = await this.locatorForOptional(`.tn-table__row[data-row-index="${rowIndex}"] .tn-table__expand-button`)();
11940
12643
  return button !== null;
@@ -11944,8 +12647,10 @@ class TnTableHarness extends ComponentHarness {
11944
12647
  * Clicks a row (for tables with `clickable` enabled).
11945
12648
  *
11946
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.
11947
12651
  */
11948
12652
  async clickRow(rowIndex) {
12653
+ await this.assertTableLayout('clickRow');
11949
12654
  await this.assertRowExists(rowIndex);
11950
12655
  const row = await this.locatorFor(`.tn-table__row[data-row-index="${rowIndex}"]`)();
11951
12656
  await row.click();
@@ -11956,8 +12661,10 @@ class TnTableHarness extends ComponentHarness {
11956
12661
  * clicks first; this helper dispatches only the `dblclick` event.
11957
12662
  *
11958
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.
11959
12665
  */
11960
12666
  async doubleClickRow(rowIndex) {
12667
+ await this.assertTableLayout('doubleClickRow');
11961
12668
  await this.assertRowExists(rowIndex);
11962
12669
  const row = await this.locatorFor(`.tn-table__row[data-row-index="${rowIndex}"]`)();
11963
12670
  await row.dispatchEvent('dblclick');
@@ -11967,8 +12674,10 @@ class TnTableHarness extends ComponentHarness {
11967
12674
  *
11968
12675
  * @param rowIndex Zero-based index of the data row.
11969
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.
11970
12678
  */
11971
12679
  async pressKeyOnRow(rowIndex, key) {
12680
+ await this.assertTableLayout('pressKeyOnRow');
11972
12681
  await this.assertRowExists(rowIndex);
11973
12682
  const row = await this.locatorFor(`.tn-table__row[data-row-index="${rowIndex}"]`)();
11974
12683
  await row.focus();
@@ -11983,8 +12692,10 @@ class TnTableHarness extends ComponentHarness {
11983
12692
  * Checks if a row is keyboard-focusable (tabindex=0).
11984
12693
  *
11985
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.
11986
12696
  */
11987
12697
  async isRowFocusable(rowIndex) {
12698
+ await this.assertTableLayout('isRowFocusable');
11988
12699
  await this.assertRowExists(rowIndex);
11989
12700
  const row = await this.locatorFor(`.tn-table__row[data-row-index="${rowIndex}"]`)();
11990
12701
  return (await row.getAttribute('tabindex')) === '0';
@@ -12003,10 +12714,17 @@ class TnTableHarness extends ComponentHarness {
12003
12714
  /**
12004
12715
  * Checks if a data row is currently marked active.
12005
12716
  *
12006
- * @param rowIndex Zero-based index of the data row.
12007
- * @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.
12008
12722
  */
12009
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
+ }
12010
12728
  await this.assertRowExists(rowIndex);
12011
12729
  const row = await this.locatorFor(`.tn-table__row[data-row-index="${rowIndex}"]`)();
12012
12730
  return row.hasClass('tn-table__row--active');
@@ -12017,11 +12735,19 @@ class TnTableHarness extends ComponentHarness {
12017
12735
  * @returns Promise resolving to the active row index or null.
12018
12736
  */
12019
12737
  async getActiveRowIndex() {
12020
- const row = await this.locatorForOptional('.tn-table__row--active')();
12021
- 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) {
12022
12748
  return null;
12023
12749
  }
12024
- const attr = await row.getAttribute('data-row-index');
12750
+ const attr = await active.getAttribute('data-row-index');
12025
12751
  return attr === null ? null : Number(attr);
12026
12752
  }
12027
12753
  /**
@@ -12029,8 +12755,11 @@ class TnTableHarness extends ComponentHarness {
12029
12755
  *
12030
12756
  * @param detailIndex Zero-based index among currently visible detail rows.
12031
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.
12032
12760
  */
12033
12761
  async getDetailRowContent(detailIndex) {
12762
+ await this.assertTableLayout('getDetailRowContent');
12034
12763
  const detailRows = await this.locatorForAll('.tn-table__detail-row')();
12035
12764
  if (detailIndex >= detailRows.length) {
12036
12765
  throw new Error(`Detail row index ${detailIndex} out of bounds (${detailRows.length} detail rows)`);
@@ -12040,21 +12769,355 @@ class TnTableHarness extends ComponentHarness {
12040
12769
  /**
12041
12770
  * Gets the count of currently expanded detail rows.
12042
12771
  *
12043
- * @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.
12044
12777
  */
12045
12778
  async getExpandedRowCount() {
12779
+ if (await this.isCards()) {
12780
+ const panels = await this.locatorForAll('.tn-table__card-detail')();
12781
+ return panels.length;
12782
+ }
12046
12783
  const detailRows = await this.locatorForAll('.tn-table__detail-row')();
12047
12784
  return detailRows.length;
12048
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
+ }
12049
13011
  // --- Internal helpers ---
12050
- 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) {
12051
13027
  const count = await this.getRowCount();
12052
- if (rowIndex >= count) {
12053
- 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)`);
12054
13041
  }
12055
13042
  }
12056
13043
  }
12057
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
+
12058
13121
  /** English defaults used when no `TN_TABLE_PAGER_LABELS` provider is registered. */
12059
13122
  const TN_TABLE_PAGER_DEFAULT_LABELS = {
12060
13123
  itemsPerPage: 'Items per page',
@@ -19458,7 +20521,7 @@ class TnFilePickerPopupComponent {
19458
20521
  return `${(date.getMonth() + 1).toString().padStart(2, '0')}/${date.getDate().toString().padStart(2, '0')}/${date.getFullYear()} ${timePart}`;
19459
20522
  }
19460
20523
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnFilePickerPopupComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
19461
- 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" }] });
19462
20525
  }
19463
20526
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TnFilePickerPopupComponent, decorators: [{
19464
20527
  type: Component,
@@ -21314,5 +22377,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImpor
21314
22377
  * Generated bundle index. Do not edit.
21315
22378
  */
21316
22379
 
21317
- 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 };
21318
22381
  //# sourceMappingURL=truenas-ui-components.mjs.map