@rivinia/table 0.1.0 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  import { NgTemplateOutlet } from "@angular/common";
2
2
  import * as i0 from "@angular/core";
3
- import { ChangeDetectionStrategy, Component, Directive, ElementRef, TemplateRef, afterRenderEffect, computed, contentChildren, inject, input, model, output, signal, viewChild } from "@angular/core";
3
+ import { ChangeDetectionStrategy, Component, Directive, ElementRef, TemplateRef, afterRenderEffect, computed, contentChildren, inject, input, isDevMode, model, output, signal, viewChild } from "@angular/core";
4
4
  const TABLE_UI_LABELS_EN = {
5
5
  grid: "Data grid",
6
6
  loading: "Loading…",
@@ -26,7 +26,7 @@ var RvTableCell = class RvTableCell {
26
26
  template = inject(TemplateRef);
27
27
  static ɵfac = i0.ɵɵngDeclareFactory({
28
28
  minVersion: "12.0.0",
29
- version: "22.2.2",
29
+ version: "22.2.3",
30
30
  ngImport: i0,
31
31
  type: RvTableCell,
32
32
  deps: [],
@@ -34,7 +34,7 @@ var RvTableCell = class RvTableCell {
34
34
  });
35
35
  static ɵdir = i0.ɵɵngDeclareDirective({
36
36
  minVersion: "17.1.0",
37
- version: "22.2.2",
37
+ version: "22.2.3",
38
38
  type: RvTableCell,
39
39
  isStandalone: true,
40
40
  selector: "[rvTableCell]",
@@ -50,7 +50,7 @@ var RvTableCell = class RvTableCell {
50
50
  };
51
51
  i0.ɵɵngDeclareClassMetadata({
52
52
  minVersion: "12.0.0",
53
- version: "22.2.2",
53
+ version: "22.2.3",
54
54
  ngImport: i0,
55
55
  type: RvTableCell,
56
56
  decorators: [{
@@ -135,17 +135,31 @@ var RiviniaTableComponent = class RiviniaTableComponent {
135
135
  renderedRows = computed(() => {
136
136
  const templates = this.cellTemplates();
137
137
  const selected = this.selected();
138
- return this.rows().map((row, rowIndex) => {
138
+ const ids = /* @__PURE__ */ new Map();
139
+ const rendered = this.rows().map((row, rowIndex) => {
139
140
  const id = this.rowId(row);
141
+ const validId = typeof id === "string" && id.trim().length > 0 || typeof id === "number" && Number.isFinite(id);
142
+ const positions = validId ? ids.get(id) ?? [] : [];
143
+ if (validId) {
144
+ positions.push(rowIndex);
145
+ ids.set(id, positions);
146
+ } else if (isDevMode()) throw new Error(`[rivinia/table/ui] Invalid row ID ${String(id)} at row index ${rowIndex}.`);
140
147
  return {
141
148
  id,
149
+ identityValid: row.identityValid !== false && validId,
142
150
  row,
143
151
  rowIndex,
144
152
  ariaRowIndex: (Number.isFinite(row.index) ? row.index : rowIndex) + 2,
145
- selected: selected.has(id),
153
+ selected: row.identityValid !== false && validId && selected.has(id),
146
154
  cells: this.buildCells(row, rowIndex, templates)
147
155
  };
148
156
  });
157
+ for (const [id, positions] of ids) {
158
+ if (positions.length < 2) continue;
159
+ if (isDevMode()) throw new Error(`[rivinia/table/ui] Duplicate row ID ${JSON.stringify(id)} at row indices ${positions.join(", ")}.`);
160
+ for (const index of positions) rendered[index].identityValid = false;
161
+ }
162
+ return rendered;
149
163
  }, ...ngDevMode ? [{ debugName: "renderedRows" }] : /* istanbul ignore next */ []);
150
164
  skeletonList = computed(() => {
151
165
  const requested = this.loading() ? this.skeletonRows() : 0;
@@ -255,7 +269,7 @@ var RiviniaTableComponent = class RiviniaTableComponent {
255
269
  });
256
270
  this.pendingFocus = false;
257
271
  this.focusCell(item.rowIndex, cell.columnIndex);
258
- if (this.selectable()) this.toggleRow(item.row);
272
+ if (this.selectable()) this.toggleRow(item);
259
273
  }
260
274
  onCellFocus(row, column) {
261
275
  const active = this.active();
@@ -316,7 +330,7 @@ var RiviniaTableComponent = class RiviniaTableComponent {
316
330
  break;
317
331
  case " ":
318
332
  if (!this.selectable()) return;
319
- this.toggleRow(rows[active.row].row);
333
+ this.toggleRow(rows[active.row]);
320
334
  break;
321
335
  default: return;
322
336
  }
@@ -502,16 +516,16 @@ var RiviniaTableComponent = class RiviniaTableComponent {
502
516
  const record = data;
503
517
  return record === null || record === void 0 ? void 0 : record[accessor];
504
518
  }
505
- toggleRow(row) {
506
- if (!this.selectable()) return;
507
- const id = this.rowId(row);
519
+ toggleRow(item) {
520
+ if (!this.selectable() || !item.identityValid) return;
521
+ const id = item.id;
508
522
  const selected = new Set(this.selected());
509
523
  if (selected.has(id)) selected.delete(id);
510
524
  else selected.add(id);
511
525
  this.selected.set(selected);
512
526
  this.selectionChange.emit({
513
527
  selected,
514
- row
528
+ row: item.row
515
529
  });
516
530
  }
517
531
  errorText(error) {
@@ -521,7 +535,7 @@ var RiviniaTableComponent = class RiviniaTableComponent {
521
535
  }
522
536
  static ɵfac = i0.ɵɵngDeclareFactory({
523
537
  minVersion: "12.0.0",
524
- version: "22.2.2",
538
+ version: "22.2.3",
525
539
  ngImport: i0,
526
540
  type: RiviniaTableComponent,
527
541
  deps: [],
@@ -529,7 +543,7 @@ var RiviniaTableComponent = class RiviniaTableComponent {
529
543
  });
530
544
  static ɵcmp = i0.ɵɵngDeclareComponent({
531
545
  minVersion: "17.0.0",
532
- version: "22.2.2",
546
+ version: "22.2.3",
533
547
  type: RiviniaTableComponent,
534
548
  isStandalone: true,
535
549
  selector: "rv-table",
@@ -672,7 +686,7 @@ var RiviniaTableComponent = class RiviniaTableComponent {
672
686
  isSignal: true
673
687
  }],
674
688
  ngImport: i0,
675
- template: "<!--\n The screen-reader instructions live OUTSIDE the grid and are referenced by\n `aria-describedby`: a helper paragraph INSIDE a role=\"grid\" would be content\n the grid did not declare and would be announced as a row.\n-->\n<p class=\"rv-table-sr-only\" [id]=\"instructionsId\">{{ effectiveLabels().keyboardHelp }}</p>\n\n<div class=\"rv-table-scroller\" #scroll>\n <!--\n `min-inline-size` is the sum of the declared column widths and the column\n template is one custom property, so the header and the body cannot drift:\n they read the same value from this element.\n -->\n <div\n class=\"rv-table-grid\"\n role=\"grid\"\n [attr.aria-label]=\"ariaLabel() ?? hostLabel ?? effectiveLabels().grid\"\n [attr.aria-describedby]=\"instructionsId\"\n [attr.aria-rowcount]=\"rowCount()\"\n [attr.aria-colcount]=\"columnCount()\"\n [attr.aria-multiselectable]=\"selectable() ? 'true' : null\"\n [attr.aria-busy]=\"loading() ? 'true' : null\"\n [style.--rv-table-columns]=\"columnTemplate()\"\n [style.min-inline-size]=\"minWidth()\"\n (keydown)=\"onKeydown($event)\"\n >\n <!--\n The header is its own rowgroup, so it can be sticky without the body\n participating in the same scroll offset. The row is the CSS grid: each\n header cell is a direct grid item at its column.\n -->\n <div class=\"rv-table-head\" role=\"rowgroup\">\n <div class=\"rv-table-row\" role=\"row\" aria-rowindex=\"1\">\n @for (column of visibleColumns(); track column.id; let index = $index) {\n <div\n class=\"rv-table-cell rv-table-head-cell\"\n role=\"columnheader\"\n [attr.aria-colindex]=\"index + 1\"\n [attr.aria-sort]=\"ariaSort(column)\"\n [attr.tabindex]=\"isActive(-1, index) ? 0 : -1\"\n [attr.data-rv-cell]=\"'-1:' + index\"\n [attr.data-sortable]=\"isSortable(column) ? 'true' : null\"\n [attr.data-sorted]=\"sortRank(column) > 0 ? 'true' : null\"\n [attr.data-sort-direction]=\"sortDirection(column)\"\n [attr.data-sort-rank]=\"sortRank(column) > 1 ? sortRank(column) : null\"\n [attr.data-align]=\"column.align ?? 'start'\"\n (click)=\"onHeaderClick(column, index, $event)\"\n (focus)=\"onCellFocus(-1, index)\"\n >\n <!--\n A real button, but NOT a tab stop: the cell owns the roving\n tabindex, so the grid keeps exactly one. Enter/Space on the cell\n sorts; the button keeps pointer and AT \"click\" working.\n -->\n <button type=\"button\" class=\"rv-table-head-button\" tabindex=\"-1\">\n <span class=\"rv-table-text rv-table-head-label\" dir=\"auto\">{{ column.header }}</span>\n @if (isSortable(column)) {\n <span\n class=\"rv-table-sort\"\n aria-hidden=\"true\"\n [attr.data-direction]=\"sortDirection(column) ?? 'none'\"\n >\n <svg viewBox=\"0 0 10 6\" width=\"10\" height=\"6\" focusable=\"false\">\n <path\n d=\"M1 5 5 1l4 4\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"1.5\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n />\n </svg>\n </span>\n @if (sortRank(column) > 1) {\n <span class=\"rv-table-sort-rank\" aria-hidden=\"true\">{{ sortRank(column) }}</span>\n }\n }\n </button>\n </div>\n }\n </div>\n </div>\n\n <!--\n One CSS grid for the whole body. A row wrapper is `display: contents`,\n so its cells are grid items of THIS grid and a real `grid-row: span N`\n can cross rows at any row height. Covered positions render no element.\n -->\n <div class=\"rv-table-body\" role=\"rowgroup\">\n @if (loading()) {\n @for (skeleton of skeletonList(); track $index; let rowIndex = $index) {\n <!-- Decorative: the polite status below announces the load once. -->\n <div class=\"rv-table-row rv-table-row--skeleton\" aria-hidden=\"true\">\n @for (column of visibleColumns(); track column.id; let columnIndex = $index) {\n <div\n class=\"rv-table-cell rv-table-skeleton\"\n [style.grid-row]=\"(rowIndex + 1) + ' / span 1'\"\n [style.grid-column]=\"(columnIndex + 1) + ' / span 1'\"\n [attr.data-col]=\"columnIndex\"\n >\n <span class=\"rv-table-skeleton-bar\"></span>\n </div>\n }\n </div>\n }\n } @else {\n @for (item of bodyRows(); track item.id) {\n <div\n class=\"rv-table-row\"\n role=\"row\"\n [attr.aria-rowindex]=\"item.ariaRowIndex\"\n [attr.aria-selected]=\"selectable() ? (item.selected ? 'true' : 'false') : null\"\n [attr.data-selected]=\"item.selected ? 'true' : null\"\n [attr.data-row]=\"item.rowIndex\"\n (dblclick)=\"onRowDoubleClick(item, $event)\"\n >\n @for (cell of item.cells; track cell.columnIndex; let isFirst = $first) {\n <div\n class=\"rv-table-cell\"\n role=\"gridcell\"\n [attr.aria-colindex]=\"cell.columnIndex + 1\"\n [attr.aria-rowspan]=\"cell.rowSpan > 1 ? cell.rowSpan : null\"\n [attr.aria-colspan]=\"cell.colSpan > 1 ? cell.colSpan : null\"\n [attr.aria-describedby]=\"cell.describedBy\"\n [attr.tabindex]=\"isActive(item.rowIndex, cell.columnIndex) ? 0 : -1\"\n [attr.data-rv-cell]=\"item.rowIndex + ':' + cell.columnIndex\"\n [attr.data-row]=\"item.rowIndex\"\n [attr.data-col]=\"cell.columnIndex\"\n [attr.data-span]=\"cell.spanned ? 'true' : null\"\n [attr.data-align]=\"cell.column.align ?? 'start'\"\n [attr.data-overflow]=\"cell.overflow\"\n [style.grid-row]=\"cell.gridRow\"\n [style.grid-column]=\"cell.gridColumn\"\n (click)=\"onCellClick(item, cell, $event)\"\n (focus)=\"onCellFocus(item.rowIndex, cell.columnIndex)\"\n >\n @if (selectable() && isFirst) {\n <!--\n Decorative, deliberately: `aria-selected` on the row is the\n semantics, and a second real checkbox would announce the\n same state twice. Clicking it bubbles to the cell, which is\n what toggles the row.\n -->\n <span\n class=\"rv-table-select\"\n aria-hidden=\"true\"\n [attr.data-checked]=\"item.selected ? 'true' : null\"\n ></span>\n }\n @if (cell.template; as template) {\n <ng-container\n [ngTemplateOutlet]=\"template\"\n [ngTemplateOutletContext]=\"cell.context\"\n />\n } @else {\n <!--\n `dir=\"auto\"`: the grid's direction is the page's, but the\n VALUE's direction is the value's. Without it an English\n client name in an Arabic table renders with its full stop\n moved to the front (\".Nile Trading Co\"), which is correct\n bidi for an RTL paragraph and wrong for the data.\n -->\n <span\n class=\"rv-table-text\"\n dir=\"auto\"\n [attr.data-overflow]=\"cell.overflow\"\n [style.--rv-table-clamp-lines]=\"cell.lines\"\n >{{ cell.text }}</span\n >\n }\n <!--\n The untruncated text, exposed through `aria-describedby` and\n hidden from the browse-order so it is not read twice. There is\n deliberately no `title`: the visual tooltip is the consumer's.\n -->\n @if (cell.describedBy) {\n <span class=\"rv-table-sr-only\" [id]=\"cell.describedBy\" aria-hidden=\"true\">{{\n cell.text\n }}</span>\n }\n </div>\n }\n </div>\n }\n }\n </div>\n </div>\n\n <!--\n Inside the scroller (it belongs to the frame) but outside the grid (a grid\n may only own rows). `position: sticky` on the inline axis keeps it in view\n while the header scrolls horizontally.\n -->\n @if (state(); as current) {\n <div\n class=\"rv-table-state\"\n [attr.role]=\"current.kind === 'error' ? 'alert' : 'status'\"\n [attr.data-state]=\"current.kind\"\n >\n <span class=\"rv-table-state-art\" aria-hidden=\"true\">\n @if (current.kind === 'error') {\n <svg viewBox=\"0 0 24 24\" width=\"24\" height=\"24\" focusable=\"false\">\n <path\n d=\"M12 7v6m0 4h.01M12 3l9 16H3l9-16Z\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"1.6\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n />\n </svg>\n } @else {\n <svg viewBox=\"0 0 24 24\" width=\"24\" height=\"24\" focusable=\"false\">\n <path\n d=\"M4 6h16M4 12h16M4 18h10\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"1.6\"\n stroke-linecap=\"round\"\n />\n </svg>\n }\n </span>\n <p class=\"rv-table-state-text\">{{ current.message }}</p>\n </div>\n }\n</div>\n\n<!-- Always present, so the text change is announced by the live region. -->\n<p class=\"rv-table-sr-only\" role=\"status\">{{ loading() ? effectiveLabels().loading : '' }}</p>\n\n<!--\n The cell templates are never rendered here: they are looked up by column id\n and instantiated per row. Projecting them explicitly keeps the contract\n visible in the DOM (and avoids keeping unprojected content around).\n-->\n<ng-content select=\"[rvTableCell]\" />\n",
689
+ template: "<!--\n The screen-reader instructions live OUTSIDE the grid and are referenced by\n `aria-describedby`: a helper paragraph INSIDE a role=\"grid\" would be content\n the grid did not declare and would be announced as a row.\n-->\n<p class=\"rv-table-sr-only\" [id]=\"instructionsId\">{{ effectiveLabels().keyboardHelp }}</p>\n\n<div class=\"rv-table-scroller\" #scroll>\n <!--\n `min-inline-size` is the sum of the declared column widths and the column\n template is one custom property, so the header and the body cannot drift:\n they read the same value from this element.\n -->\n <div\n class=\"rv-table-grid\"\n role=\"grid\"\n [attr.aria-label]=\"ariaLabel() ?? hostLabel ?? effectiveLabels().grid\"\n [attr.aria-describedby]=\"instructionsId\"\n [attr.aria-rowcount]=\"rowCount()\"\n [attr.aria-colcount]=\"columnCount()\"\n [attr.aria-multiselectable]=\"selectable() ? 'true' : null\"\n [attr.aria-busy]=\"loading() ? 'true' : null\"\n [style.--rv-table-columns]=\"columnTemplate()\"\n [style.min-inline-size]=\"minWidth()\"\n (keydown)=\"onKeydown($event)\"\n >\n <!--\n The header is its own rowgroup, so it can be sticky without the body\n participating in the same scroll offset. The row is the CSS grid: each\n header cell is a direct grid item at its column.\n -->\n <div class=\"rv-table-head\" role=\"rowgroup\">\n <div class=\"rv-table-row\" role=\"row\" aria-rowindex=\"1\">\n @for (column of visibleColumns(); track column.id; let index = $index) {\n <div\n class=\"rv-table-cell rv-table-head-cell\"\n role=\"columnheader\"\n [attr.aria-colindex]=\"index + 1\"\n [attr.aria-sort]=\"ariaSort(column)\"\n [attr.tabindex]=\"isActive(-1, index) ? 0 : -1\"\n [attr.data-rv-cell]=\"'-1:' + index\"\n [attr.data-sortable]=\"isSortable(column) ? 'true' : null\"\n [attr.data-sorted]=\"sortRank(column) > 0 ? 'true' : null\"\n [attr.data-sort-direction]=\"sortDirection(column)\"\n [attr.data-sort-rank]=\"sortRank(column) > 1 ? sortRank(column) : null\"\n [attr.data-align]=\"column.align ?? 'start'\"\n (click)=\"onHeaderClick(column, index, $event)\"\n (focus)=\"onCellFocus(-1, index)\"\n >\n <!--\n A real button, but NOT a tab stop: the cell owns the roving\n tabindex, so the grid keeps exactly one. Enter/Space on the cell\n sorts; the button keeps pointer and AT \"click\" working.\n -->\n <button type=\"button\" class=\"rv-table-head-button\" tabindex=\"-1\">\n <span class=\"rv-table-text rv-table-head-label\" dir=\"auto\">{{ column.header }}</span>\n @if (isSortable(column)) {\n <span\n class=\"rv-table-sort\"\n aria-hidden=\"true\"\n [attr.data-direction]=\"sortDirection(column) ?? 'none'\"\n >\n <svg viewBox=\"0 0 10 6\" width=\"10\" height=\"6\" focusable=\"false\">\n <path\n d=\"M1 5 5 1l4 4\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"1.5\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n />\n </svg>\n </span>\n @if (sortRank(column) > 1) {\n <span class=\"rv-table-sort-rank\" aria-hidden=\"true\">{{ sortRank(column) }}</span>\n }\n }\n </button>\n </div>\n }\n </div>\n </div>\n\n <!--\n One CSS grid for the whole body. A row wrapper is `display: contents`,\n so its cells are grid items of THIS grid and a real `grid-row: span N`\n can cross rows at any row height. Covered positions render no element.\n -->\n <div class=\"rv-table-body\" role=\"rowgroup\">\n @if (loading()) {\n @for (skeleton of skeletonList(); track $index; let rowIndex = $index) {\n <!-- Decorative: the polite status below announces the load once. -->\n <div class=\"rv-table-row rv-table-row--skeleton\" aria-hidden=\"true\">\n @for (column of visibleColumns(); track column.id; let columnIndex = $index) {\n <div\n class=\"rv-table-cell rv-table-skeleton\"\n [style.grid-row]=\"(rowIndex + 1) + ' / span 1'\"\n [style.grid-column]=\"(columnIndex + 1) + ' / span 1'\"\n [attr.data-col]=\"columnIndex\"\n >\n <span class=\"rv-table-skeleton-bar\"></span>\n </div>\n }\n </div>\n }\n } @else {\n @for (item of bodyRows(); track item.row) {\n <div\n class=\"rv-table-row\"\n role=\"row\"\n [attr.aria-rowindex]=\"item.ariaRowIndex\"\n [attr.aria-selected]=\"selectable() && item.identityValid ? (item.selected ? 'true' : 'false') : null\"\n [attr.data-selected]=\"item.selected ? 'true' : null\"\n [attr.data-row]=\"item.rowIndex\"\n (dblclick)=\"onRowDoubleClick(item, $event)\"\n >\n @for (cell of item.cells; track cell.columnIndex; let isFirst = $first) {\n <div\n class=\"rv-table-cell\"\n role=\"gridcell\"\n [attr.aria-colindex]=\"cell.columnIndex + 1\"\n [attr.aria-rowspan]=\"cell.rowSpan > 1 ? cell.rowSpan : null\"\n [attr.aria-colspan]=\"cell.colSpan > 1 ? cell.colSpan : null\"\n [attr.aria-describedby]=\"cell.describedBy\"\n [attr.tabindex]=\"isActive(item.rowIndex, cell.columnIndex) ? 0 : -1\"\n [attr.data-rv-cell]=\"item.rowIndex + ':' + cell.columnIndex\"\n [attr.data-row]=\"item.rowIndex\"\n [attr.data-col]=\"cell.columnIndex\"\n [attr.data-span]=\"cell.spanned ? 'true' : null\"\n [attr.data-align]=\"cell.column.align ?? 'start'\"\n [attr.data-overflow]=\"cell.overflow\"\n [style.grid-row]=\"cell.gridRow\"\n [style.grid-column]=\"cell.gridColumn\"\n (click)=\"onCellClick(item, cell, $event)\"\n (focus)=\"onCellFocus(item.rowIndex, cell.columnIndex)\"\n >\n @if (selectable() && isFirst) {\n <!--\n Decorative, deliberately: `aria-selected` on the row is the\n semantics, and a second real checkbox would announce the\n same state twice. Clicking it bubbles to the cell, which is\n what toggles the row.\n -->\n <span\n class=\"rv-table-select\"\n aria-hidden=\"true\"\n [attr.data-checked]=\"item.selected ? 'true' : null\"\n ></span>\n }\n @if (cell.template; as template) {\n <ng-container\n [ngTemplateOutlet]=\"template\"\n [ngTemplateOutletContext]=\"cell.context\"\n />\n } @else {\n <!--\n `dir=\"auto\"`: the grid's direction is the page's, but the\n VALUE's direction is the value's. Without it an English\n client name in an Arabic table renders with its full stop\n moved to the front (\".Nile Trading Co\"), which is correct\n bidi for an RTL paragraph and wrong for the data.\n -->\n <span\n class=\"rv-table-text\"\n dir=\"auto\"\n [attr.data-overflow]=\"cell.overflow\"\n [style.--rv-table-clamp-lines]=\"cell.lines\"\n >{{ cell.text }}</span\n >\n }\n <!--\n The untruncated text, exposed through `aria-describedby` and\n hidden from the browse-order so it is not read twice. There is\n deliberately no `title`: the visual tooltip is the consumer's.\n -->\n @if (cell.describedBy) {\n <span class=\"rv-table-sr-only\" [id]=\"cell.describedBy\" aria-hidden=\"true\">{{\n cell.text\n }}</span>\n }\n </div>\n }\n </div>\n }\n }\n </div>\n </div>\n\n <!--\n Inside the scroller (it belongs to the frame) but outside the grid (a grid\n may only own rows). `position: sticky` on the inline axis keeps it in view\n while the header scrolls horizontally.\n -->\n @if (state(); as current) {\n <div\n class=\"rv-table-state\"\n [attr.role]=\"current.kind === 'error' ? 'alert' : 'status'\"\n [attr.data-state]=\"current.kind\"\n >\n <span class=\"rv-table-state-art\" aria-hidden=\"true\">\n @if (current.kind === 'error') {\n <svg viewBox=\"0 0 24 24\" width=\"24\" height=\"24\" focusable=\"false\">\n <path\n d=\"M12 7v6m0 4h.01M12 3l9 16H3l9-16Z\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"1.6\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n />\n </svg>\n } @else {\n <svg viewBox=\"0 0 24 24\" width=\"24\" height=\"24\" focusable=\"false\">\n <path\n d=\"M4 6h16M4 12h16M4 18h10\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"1.6\"\n stroke-linecap=\"round\"\n />\n </svg>\n }\n </span>\n <p class=\"rv-table-state-text\">{{ current.message }}</p>\n </div>\n }\n</div>\n\n<!-- Always present, so the text change is announced by the live region. -->\n<p class=\"rv-table-sr-only\" role=\"status\">{{ loading() ? effectiveLabels().loading : '' }}</p>\n\n<!--\n The cell templates are never rendered here: they are looked up by column id\n and instantiated per row. Projecting them explicitly keeps the contract\n visible in the DOM (and avoids keeping unprojected content around).\n-->\n<ng-content select=\"[rvTableCell]\" />\n",
676
690
  styles: [":host{display:block;font-size:var(--rv-table-font-size, .875rem);color:var(--rv-table-fg, var(--rv-fg, light-dark(#111827, #f4f4f5)));-webkit-text-size-adjust:100%}.rv-table-scroller,.rv-table-scroller *,.rv-table-scroller *:before,.rv-table-scroller *:after{box-sizing:border-box}.rv-table-scroller{position:relative;overflow:auto;max-block-size:var(--rv-table-max-height, none);overscroll-behavior:contain;border:1px solid var(--rv-table-border, var(--rv-border, light-dark(#e5e7eb, #2f2f36)));border-radius:var(--rv-table-radius, var(--rv-radius, .5rem));background:var(--rv-table-bg, var(--rv-bg, light-dark(#ffffff, #17171b)));scrollbar-width:thin;scrollbar-color:var(--rv-table-scrollbar, light-dark(#cbd5e1, #3f3f46)) transparent}.rv-table-grid{display:block}.rv-table-head{position:relative;z-index:2;background:var( --rv-table-header-bg, color-mix( in oklab, var(--rv-table-bg, var(--rv-bg, light-dark(#ffffff, #17171b))) 92%, var(--rv-table-fg, var(--rv-fg, light-dark(#111827, #f4f4f5))) ) );border-block-end:1px solid var(--rv-table-header-border, var(--rv-border, light-dark(#d1d5db, #3f3f46)))}:host([data-sticky=\"true\"]) .rv-table-head{position:sticky;inset-block-start:0}.rv-table-head .rv-table-row{display:grid;grid-template-columns:var(--rv-table-columns, 1fr)}.rv-table-head-cell{min-block-size:var(--rv-table-head-height, 2.75rem);border-block-end:0;color:var(--rv-table-header-fg, var(--rv-muted, light-dark(#52525b, #a1a1aa)));font-size:var(--rv-table-header-font-size, .75rem);font-weight:var(--rv-table-header-font-weight, 700);letter-spacing:.02em;-webkit-user-select:none;user-select:none}.rv-table-head-cell[data-sortable=true]{cursor:pointer}.rv-table-head-cell[data-sortable=true]:hover{background:var( --rv-table-header-hover, color-mix( in oklab, var(--rv-table-bg, var(--rv-bg, light-dark(#ffffff, #17171b))) 86%, var(--rv-table-fg, var(--rv-fg, light-dark(#111827, #f4f4f5))) ) );color:var(--rv-table-header-fg-active, var(--rv-fg, light-dark(#111827, #f4f4f5)))}.rv-table-head-cell[data-sorted=true]{color:var(--rv-table-header-fg-active, var(--rv-fg, light-dark(#111827, #f4f4f5)))}.rv-table-head-button{display:flex;align-items:center;gap:.375rem;inline-size:100%;min-inline-size:0;margin:0;padding:0;border:0;background:none;color:inherit;font:inherit;letter-spacing:inherit;text-align:inherit;cursor:inherit}.rv-table-head-button:focus{outline:none}.rv-table-head-label{min-inline-size:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.rv-table-sort{display:inline-flex;flex:0 0 auto;align-items:center;justify-content:center;inline-size:1rem;block-size:1rem;color:var(--rv-table-sort-fg, var(--rv-accent, light-dark(#5b5bd6, #8b8bf0)));opacity:0;transition:opacity .12s ease,transform .12s ease}.rv-table-head-cell[data-sortable=true]:hover .rv-table-sort{opacity:.45}.rv-table-head-cell[data-sorted=true] .rv-table-sort{opacity:1}.rv-table-sort[data-direction=desc]{transform:rotate(180deg)}.rv-table-sort-rank{display:inline-grid;flex:0 0 auto;place-items:center;min-inline-size:1rem;block-size:1rem;padding-inline:.2rem;border-radius:999px;background:color-mix(in oklab,var(--rv-table-sort-fg, var(--rv-accent, light-dark(#5b5bd6, #8b8bf0))) 16%,transparent);color:var(--rv-table-sort-fg, var(--rv-accent, light-dark(#5b5bd6, #8b8bf0)));font-size:.6875rem;font-weight:700;font-variant-numeric:tabular-nums;line-height:1}.rv-table-body{display:grid;grid-template-columns:var(--rv-table-columns, 1fr);grid-auto-rows:minmax(var(--rv-table-row-height, 2.5rem),auto)}.rv-table-row{display:contents}.rv-table-cell{position:relative;display:flex;align-items:center;gap:.375rem;min-inline-size:0;padding:var(--rv-table-cell-padding, .5rem .75rem);border-block-end:1px solid var(--rv-table-border, var(--rv-border, light-dark(#e5e7eb, #2f2f36)));background:transparent;transition:background-color .12s ease}.rv-table-row:hover>.rv-table-cell{background:var( --rv-table-row-hover, color-mix( in oklab, var(--rv-table-bg, var(--rv-bg, light-dark(#ffffff, #17171b))) 94%, var(--rv-table-fg, var(--rv-fg, light-dark(#111827, #f4f4f5))) ) )}.rv-table-select{display:inline-grid;place-items:center;flex:none;inline-size:1rem;block-size:1rem;border:1px solid var(--rv-table-select-border, var(--rv-border, light-dark(#9ca3af, #52525b)));border-radius:var(--rv-table-select-radius, .25rem);background:var(--rv-table-select-bg, var(--rv-bg, light-dark(#ffffff, #17171b)));transition:background-color .12s ease,border-color .12s ease}.rv-table-select:after{content:\"\";inline-size:.5rem;block-size:.3rem;border-inline-start:2px solid var(--rv-table-select-mark, var(--rv-bg, light-dark(#ffffff, #17171b)));border-block-end:2px solid var(--rv-table-select-mark, var(--rv-bg, light-dark(#ffffff, #17171b)));transform:translateY(-.06rem) rotate(-45deg) scale(0);transition:transform .12s ease}.rv-table-select[data-checked=true]{border-color:var(--rv-table-accent, var(--rv-accent, light-dark(#5b5bd6, #8b8bf0)));background:var(--rv-table-accent, var(--rv-accent, light-dark(#5b5bd6, #8b8bf0)))}.rv-table-select[data-checked=true]:after{transform:translateY(-.06rem) rotate(-45deg) scale(1)}@media(prefers-reduced-motion:reduce){.rv-table-select,.rv-table-select:after{transition:none}}.rv-table-row[aria-selected=true]>.rv-table-cell{background:var( --rv-table-row-selected, color-mix( in oklab, var(--rv-table-accent, var(--rv-accent, light-dark(#5b5bd6, #8b8bf0))) 12%, var(--rv-table-bg, var(--rv-bg, light-dark(#ffffff, #17171b))) ) )}.rv-table-row[aria-selected=true]>.rv-table-cell[data-col=\"0\"]:before{content:\"\";position:absolute;inset-block:0;inset-inline-start:0;inline-size:var(--rv-table-selected-rail, 3px);background:var(--rv-table-accent, var(--rv-accent, light-dark(#5b5bd6, #8b8bf0)))}.rv-table-cell[data-align=center]{justify-content:center;text-align:center}.rv-table-cell[data-align=end]{justify-content:flex-end;text-align:end}.rv-table-cell[data-span]{align-items:flex-start}.rv-table-cell:focus{outline:none}.rv-table-cell:focus-visible{outline:2px solid var(--rv-table-focus, var(--rv-accent, light-dark(#5b5bd6, #8b8bf0)));outline-offset:-2px;z-index:1}.rv-table-text{min-inline-size:0;max-inline-size:100%}.rv-table-text[data-overflow=nowrap]{overflow:hidden;white-space:nowrap;text-overflow:ellipsis}.rv-table-text[data-overflow=wrap]{overflow-wrap:anywhere;white-space:normal}.rv-table-text[data-overflow=clamp]{display:-webkit-box;overflow:hidden;overflow-wrap:anywhere;-webkit-box-orient:vertical;-webkit-line-clamp:var(--rv-table-clamp-lines, 2);line-clamp:var(--rv-table-clamp-lines, 2)}.rv-table-skeleton{pointer-events:none}.rv-table-skeleton-bar{display:block;inline-size:64%;block-size:.625rem;border-radius:.3125rem;background:linear-gradient(90deg,var(--rv-table-skeleton, light-dark(#eceff3, #26262d)) 25%,var(--rv-table-skeleton-highlight, light-dark(#f6f8fa, #31313a)) 37%,var(--rv-table-skeleton, light-dark(#eceff3, #26262d)) 63%);background-size:400% 100%;animation:rv-table-shimmer 1.4s ease-in-out infinite}.rv-table-skeleton:nth-child(3n+2) .rv-table-skeleton-bar{inline-size:82%}.rv-table-skeleton:nth-child(3n) .rv-table-skeleton-bar{inline-size:46%}@keyframes rv-table-shimmer{0%{background-position:100% 0}to{background-position:0 0}}.rv-table-state{position:sticky;inset-inline-start:0;display:flex;flex-direction:column;align-items:center;gap:.5rem;inline-size:100%;padding:var(--rv-table-state-padding, 3rem 1.25rem);text-align:center;background:var(--rv-table-bg, var(--rv-bg, light-dark(#ffffff, #17171b)));color:var(--rv-table-state-fg, var(--rv-muted, light-dark(#52525b, #a1a1aa)))}.rv-table-state-art{display:grid;place-items:center;inline-size:3rem;block-size:3rem;border-radius:1rem;background:color-mix(in oklab,var(--rv-table-state-fg, var(--rv-muted, light-dark(#52525b, #a1a1aa))) 10%,transparent)}.rv-table-state[data-state=error] .rv-table-state-art{background:color-mix(in oklab,var(--rv-table-state-error, var(--rv-danger, light-dark(#dc2626, #f87171))) 14%,transparent);color:var(--rv-table-state-error, var(--rv-danger, light-dark(#dc2626, #f87171)))}.rv-table-state-text{margin:0;max-inline-size:34rem;color:var(--rv-table-state-text, var(--rv-fg, light-dark(#111827, #f4f4f5)));font-size:.9375rem;line-height:1.5}.rv-table-sr-only{position:absolute;inline-size:1px;block-size:1px;margin:-1px;padding:0;overflow:hidden;clip-path:inset(50%);white-space:nowrap;border:0}:host([data-density=\"compact\"]) .rv-table-body{grid-auto-rows:minmax(var(--rv-table-row-height, 2rem),auto)}:host([data-density=\"compact\"]) .rv-table-cell{padding:var(--rv-table-cell-padding, .25rem .5rem)}:host([data-density=\"compact\"]) .rv-table-head-cell{min-block-size:var(--rv-table-head-height, 2.25rem)}:host([data-density=\"compact\"]) .rv-table-skeleton-bar{block-size:.5rem}:host([data-density=\"comfortable\"]) .rv-table-body{grid-auto-rows:minmax(var(--rv-table-row-height, 3rem),auto)}:host([data-density=\"comfortable\"]) .rv-table-cell{padding:var(--rv-table-cell-padding, .75rem 1rem)}:host([data-density=\"comfortable\"]) .rv-table-head-cell{min-block-size:var(--rv-table-head-height, 3.25rem)}@media(prefers-reduced-motion:reduce){.rv-table-cell,.rv-table-sort{transition:none}.rv-table-skeleton-bar{animation:none}}@media(forced-colors:active){.rv-table-cell:focus-visible{outline-color:Highlight}.rv-table-skeleton-bar{background:GrayText}.rv-table-row[aria-selected=true]>.rv-table-cell[data-col=\"0\"]:before{background:Highlight}}\n"],
677
691
  dependencies: [{
678
692
  kind: "directive",
@@ -689,7 +703,7 @@ var RiviniaTableComponent = class RiviniaTableComponent {
689
703
  };
690
704
  i0.ɵɵngDeclareClassMetadata({
691
705
  minVersion: "12.0.0",
692
- version: "22.2.2",
706
+ version: "22.2.3",
693
707
  ngImport: i0,
694
708
  type: RiviniaTableComponent,
695
709
  decorators: [{
@@ -704,7 +718,7 @@ i0.ɵɵngDeclareClassMetadata({
704
718
  "[attr.data-sticky]": "stickyHeader() ? \"true\" : \"false\"",
705
719
  "[attr.data-selectable]": "selectable() ? \"true\" : null"
706
720
  },
707
- template: "<!--\n The screen-reader instructions live OUTSIDE the grid and are referenced by\n `aria-describedby`: a helper paragraph INSIDE a role=\"grid\" would be content\n the grid did not declare and would be announced as a row.\n-->\n<p class=\"rv-table-sr-only\" [id]=\"instructionsId\">{{ effectiveLabels().keyboardHelp }}</p>\n\n<div class=\"rv-table-scroller\" #scroll>\n <!--\n `min-inline-size` is the sum of the declared column widths and the column\n template is one custom property, so the header and the body cannot drift:\n they read the same value from this element.\n -->\n <div\n class=\"rv-table-grid\"\n role=\"grid\"\n [attr.aria-label]=\"ariaLabel() ?? hostLabel ?? effectiveLabels().grid\"\n [attr.aria-describedby]=\"instructionsId\"\n [attr.aria-rowcount]=\"rowCount()\"\n [attr.aria-colcount]=\"columnCount()\"\n [attr.aria-multiselectable]=\"selectable() ? 'true' : null\"\n [attr.aria-busy]=\"loading() ? 'true' : null\"\n [style.--rv-table-columns]=\"columnTemplate()\"\n [style.min-inline-size]=\"minWidth()\"\n (keydown)=\"onKeydown($event)\"\n >\n <!--\n The header is its own rowgroup, so it can be sticky without the body\n participating in the same scroll offset. The row is the CSS grid: each\n header cell is a direct grid item at its column.\n -->\n <div class=\"rv-table-head\" role=\"rowgroup\">\n <div class=\"rv-table-row\" role=\"row\" aria-rowindex=\"1\">\n @for (column of visibleColumns(); track column.id; let index = $index) {\n <div\n class=\"rv-table-cell rv-table-head-cell\"\n role=\"columnheader\"\n [attr.aria-colindex]=\"index + 1\"\n [attr.aria-sort]=\"ariaSort(column)\"\n [attr.tabindex]=\"isActive(-1, index) ? 0 : -1\"\n [attr.data-rv-cell]=\"'-1:' + index\"\n [attr.data-sortable]=\"isSortable(column) ? 'true' : null\"\n [attr.data-sorted]=\"sortRank(column) > 0 ? 'true' : null\"\n [attr.data-sort-direction]=\"sortDirection(column)\"\n [attr.data-sort-rank]=\"sortRank(column) > 1 ? sortRank(column) : null\"\n [attr.data-align]=\"column.align ?? 'start'\"\n (click)=\"onHeaderClick(column, index, $event)\"\n (focus)=\"onCellFocus(-1, index)\"\n >\n <!--\n A real button, but NOT a tab stop: the cell owns the roving\n tabindex, so the grid keeps exactly one. Enter/Space on the cell\n sorts; the button keeps pointer and AT \"click\" working.\n -->\n <button type=\"button\" class=\"rv-table-head-button\" tabindex=\"-1\">\n <span class=\"rv-table-text rv-table-head-label\" dir=\"auto\">{{ column.header }}</span>\n @if (isSortable(column)) {\n <span\n class=\"rv-table-sort\"\n aria-hidden=\"true\"\n [attr.data-direction]=\"sortDirection(column) ?? 'none'\"\n >\n <svg viewBox=\"0 0 10 6\" width=\"10\" height=\"6\" focusable=\"false\">\n <path\n d=\"M1 5 5 1l4 4\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"1.5\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n />\n </svg>\n </span>\n @if (sortRank(column) > 1) {\n <span class=\"rv-table-sort-rank\" aria-hidden=\"true\">{{ sortRank(column) }}</span>\n }\n }\n </button>\n </div>\n }\n </div>\n </div>\n\n <!--\n One CSS grid for the whole body. A row wrapper is `display: contents`,\n so its cells are grid items of THIS grid and a real `grid-row: span N`\n can cross rows at any row height. Covered positions render no element.\n -->\n <div class=\"rv-table-body\" role=\"rowgroup\">\n @if (loading()) {\n @for (skeleton of skeletonList(); track $index; let rowIndex = $index) {\n <!-- Decorative: the polite status below announces the load once. -->\n <div class=\"rv-table-row rv-table-row--skeleton\" aria-hidden=\"true\">\n @for (column of visibleColumns(); track column.id; let columnIndex = $index) {\n <div\n class=\"rv-table-cell rv-table-skeleton\"\n [style.grid-row]=\"(rowIndex + 1) + ' / span 1'\"\n [style.grid-column]=\"(columnIndex + 1) + ' / span 1'\"\n [attr.data-col]=\"columnIndex\"\n >\n <span class=\"rv-table-skeleton-bar\"></span>\n </div>\n }\n </div>\n }\n } @else {\n @for (item of bodyRows(); track item.id) {\n <div\n class=\"rv-table-row\"\n role=\"row\"\n [attr.aria-rowindex]=\"item.ariaRowIndex\"\n [attr.aria-selected]=\"selectable() ? (item.selected ? 'true' : 'false') : null\"\n [attr.data-selected]=\"item.selected ? 'true' : null\"\n [attr.data-row]=\"item.rowIndex\"\n (dblclick)=\"onRowDoubleClick(item, $event)\"\n >\n @for (cell of item.cells; track cell.columnIndex; let isFirst = $first) {\n <div\n class=\"rv-table-cell\"\n role=\"gridcell\"\n [attr.aria-colindex]=\"cell.columnIndex + 1\"\n [attr.aria-rowspan]=\"cell.rowSpan > 1 ? cell.rowSpan : null\"\n [attr.aria-colspan]=\"cell.colSpan > 1 ? cell.colSpan : null\"\n [attr.aria-describedby]=\"cell.describedBy\"\n [attr.tabindex]=\"isActive(item.rowIndex, cell.columnIndex) ? 0 : -1\"\n [attr.data-rv-cell]=\"item.rowIndex + ':' + cell.columnIndex\"\n [attr.data-row]=\"item.rowIndex\"\n [attr.data-col]=\"cell.columnIndex\"\n [attr.data-span]=\"cell.spanned ? 'true' : null\"\n [attr.data-align]=\"cell.column.align ?? 'start'\"\n [attr.data-overflow]=\"cell.overflow\"\n [style.grid-row]=\"cell.gridRow\"\n [style.grid-column]=\"cell.gridColumn\"\n (click)=\"onCellClick(item, cell, $event)\"\n (focus)=\"onCellFocus(item.rowIndex, cell.columnIndex)\"\n >\n @if (selectable() && isFirst) {\n <!--\n Decorative, deliberately: `aria-selected` on the row is the\n semantics, and a second real checkbox would announce the\n same state twice. Clicking it bubbles to the cell, which is\n what toggles the row.\n -->\n <span\n class=\"rv-table-select\"\n aria-hidden=\"true\"\n [attr.data-checked]=\"item.selected ? 'true' : null\"\n ></span>\n }\n @if (cell.template; as template) {\n <ng-container\n [ngTemplateOutlet]=\"template\"\n [ngTemplateOutletContext]=\"cell.context\"\n />\n } @else {\n <!--\n `dir=\"auto\"`: the grid's direction is the page's, but the\n VALUE's direction is the value's. Without it an English\n client name in an Arabic table renders with its full stop\n moved to the front (\".Nile Trading Co\"), which is correct\n bidi for an RTL paragraph and wrong for the data.\n -->\n <span\n class=\"rv-table-text\"\n dir=\"auto\"\n [attr.data-overflow]=\"cell.overflow\"\n [style.--rv-table-clamp-lines]=\"cell.lines\"\n >{{ cell.text }}</span\n >\n }\n <!--\n The untruncated text, exposed through `aria-describedby` and\n hidden from the browse-order so it is not read twice. There is\n deliberately no `title`: the visual tooltip is the consumer's.\n -->\n @if (cell.describedBy) {\n <span class=\"rv-table-sr-only\" [id]=\"cell.describedBy\" aria-hidden=\"true\">{{\n cell.text\n }}</span>\n }\n </div>\n }\n </div>\n }\n }\n </div>\n </div>\n\n <!--\n Inside the scroller (it belongs to the frame) but outside the grid (a grid\n may only own rows). `position: sticky` on the inline axis keeps it in view\n while the header scrolls horizontally.\n -->\n @if (state(); as current) {\n <div\n class=\"rv-table-state\"\n [attr.role]=\"current.kind === 'error' ? 'alert' : 'status'\"\n [attr.data-state]=\"current.kind\"\n >\n <span class=\"rv-table-state-art\" aria-hidden=\"true\">\n @if (current.kind === 'error') {\n <svg viewBox=\"0 0 24 24\" width=\"24\" height=\"24\" focusable=\"false\">\n <path\n d=\"M12 7v6m0 4h.01M12 3l9 16H3l9-16Z\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"1.6\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n />\n </svg>\n } @else {\n <svg viewBox=\"0 0 24 24\" width=\"24\" height=\"24\" focusable=\"false\">\n <path\n d=\"M4 6h16M4 12h16M4 18h10\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"1.6\"\n stroke-linecap=\"round\"\n />\n </svg>\n }\n </span>\n <p class=\"rv-table-state-text\">{{ current.message }}</p>\n </div>\n }\n</div>\n\n<!-- Always present, so the text change is announced by the live region. -->\n<p class=\"rv-table-sr-only\" role=\"status\">{{ loading() ? effectiveLabels().loading : '' }}</p>\n\n<!--\n The cell templates are never rendered here: they are looked up by column id\n and instantiated per row. Projecting them explicitly keeps the contract\n visible in the DOM (and avoids keeping unprojected content around).\n-->\n<ng-content select=\"[rvTableCell]\" />\n",
721
+ template: "<!--\n The screen-reader instructions live OUTSIDE the grid and are referenced by\n `aria-describedby`: a helper paragraph INSIDE a role=\"grid\" would be content\n the grid did not declare and would be announced as a row.\n-->\n<p class=\"rv-table-sr-only\" [id]=\"instructionsId\">{{ effectiveLabels().keyboardHelp }}</p>\n\n<div class=\"rv-table-scroller\" #scroll>\n <!--\n `min-inline-size` is the sum of the declared column widths and the column\n template is one custom property, so the header and the body cannot drift:\n they read the same value from this element.\n -->\n <div\n class=\"rv-table-grid\"\n role=\"grid\"\n [attr.aria-label]=\"ariaLabel() ?? hostLabel ?? effectiveLabels().grid\"\n [attr.aria-describedby]=\"instructionsId\"\n [attr.aria-rowcount]=\"rowCount()\"\n [attr.aria-colcount]=\"columnCount()\"\n [attr.aria-multiselectable]=\"selectable() ? 'true' : null\"\n [attr.aria-busy]=\"loading() ? 'true' : null\"\n [style.--rv-table-columns]=\"columnTemplate()\"\n [style.min-inline-size]=\"minWidth()\"\n (keydown)=\"onKeydown($event)\"\n >\n <!--\n The header is its own rowgroup, so it can be sticky without the body\n participating in the same scroll offset. The row is the CSS grid: each\n header cell is a direct grid item at its column.\n -->\n <div class=\"rv-table-head\" role=\"rowgroup\">\n <div class=\"rv-table-row\" role=\"row\" aria-rowindex=\"1\">\n @for (column of visibleColumns(); track column.id; let index = $index) {\n <div\n class=\"rv-table-cell rv-table-head-cell\"\n role=\"columnheader\"\n [attr.aria-colindex]=\"index + 1\"\n [attr.aria-sort]=\"ariaSort(column)\"\n [attr.tabindex]=\"isActive(-1, index) ? 0 : -1\"\n [attr.data-rv-cell]=\"'-1:' + index\"\n [attr.data-sortable]=\"isSortable(column) ? 'true' : null\"\n [attr.data-sorted]=\"sortRank(column) > 0 ? 'true' : null\"\n [attr.data-sort-direction]=\"sortDirection(column)\"\n [attr.data-sort-rank]=\"sortRank(column) > 1 ? sortRank(column) : null\"\n [attr.data-align]=\"column.align ?? 'start'\"\n (click)=\"onHeaderClick(column, index, $event)\"\n (focus)=\"onCellFocus(-1, index)\"\n >\n <!--\n A real button, but NOT a tab stop: the cell owns the roving\n tabindex, so the grid keeps exactly one. Enter/Space on the cell\n sorts; the button keeps pointer and AT \"click\" working.\n -->\n <button type=\"button\" class=\"rv-table-head-button\" tabindex=\"-1\">\n <span class=\"rv-table-text rv-table-head-label\" dir=\"auto\">{{ column.header }}</span>\n @if (isSortable(column)) {\n <span\n class=\"rv-table-sort\"\n aria-hidden=\"true\"\n [attr.data-direction]=\"sortDirection(column) ?? 'none'\"\n >\n <svg viewBox=\"0 0 10 6\" width=\"10\" height=\"6\" focusable=\"false\">\n <path\n d=\"M1 5 5 1l4 4\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"1.5\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n />\n </svg>\n </span>\n @if (sortRank(column) > 1) {\n <span class=\"rv-table-sort-rank\" aria-hidden=\"true\">{{ sortRank(column) }}</span>\n }\n }\n </button>\n </div>\n }\n </div>\n </div>\n\n <!--\n One CSS grid for the whole body. A row wrapper is `display: contents`,\n so its cells are grid items of THIS grid and a real `grid-row: span N`\n can cross rows at any row height. Covered positions render no element.\n -->\n <div class=\"rv-table-body\" role=\"rowgroup\">\n @if (loading()) {\n @for (skeleton of skeletonList(); track $index; let rowIndex = $index) {\n <!-- Decorative: the polite status below announces the load once. -->\n <div class=\"rv-table-row rv-table-row--skeleton\" aria-hidden=\"true\">\n @for (column of visibleColumns(); track column.id; let columnIndex = $index) {\n <div\n class=\"rv-table-cell rv-table-skeleton\"\n [style.grid-row]=\"(rowIndex + 1) + ' / span 1'\"\n [style.grid-column]=\"(columnIndex + 1) + ' / span 1'\"\n [attr.data-col]=\"columnIndex\"\n >\n <span class=\"rv-table-skeleton-bar\"></span>\n </div>\n }\n </div>\n }\n } @else {\n @for (item of bodyRows(); track item.row) {\n <div\n class=\"rv-table-row\"\n role=\"row\"\n [attr.aria-rowindex]=\"item.ariaRowIndex\"\n [attr.aria-selected]=\"selectable() && item.identityValid ? (item.selected ? 'true' : 'false') : null\"\n [attr.data-selected]=\"item.selected ? 'true' : null\"\n [attr.data-row]=\"item.rowIndex\"\n (dblclick)=\"onRowDoubleClick(item, $event)\"\n >\n @for (cell of item.cells; track cell.columnIndex; let isFirst = $first) {\n <div\n class=\"rv-table-cell\"\n role=\"gridcell\"\n [attr.aria-colindex]=\"cell.columnIndex + 1\"\n [attr.aria-rowspan]=\"cell.rowSpan > 1 ? cell.rowSpan : null\"\n [attr.aria-colspan]=\"cell.colSpan > 1 ? cell.colSpan : null\"\n [attr.aria-describedby]=\"cell.describedBy\"\n [attr.tabindex]=\"isActive(item.rowIndex, cell.columnIndex) ? 0 : -1\"\n [attr.data-rv-cell]=\"item.rowIndex + ':' + cell.columnIndex\"\n [attr.data-row]=\"item.rowIndex\"\n [attr.data-col]=\"cell.columnIndex\"\n [attr.data-span]=\"cell.spanned ? 'true' : null\"\n [attr.data-align]=\"cell.column.align ?? 'start'\"\n [attr.data-overflow]=\"cell.overflow\"\n [style.grid-row]=\"cell.gridRow\"\n [style.grid-column]=\"cell.gridColumn\"\n (click)=\"onCellClick(item, cell, $event)\"\n (focus)=\"onCellFocus(item.rowIndex, cell.columnIndex)\"\n >\n @if (selectable() && isFirst) {\n <!--\n Decorative, deliberately: `aria-selected` on the row is the\n semantics, and a second real checkbox would announce the\n same state twice. Clicking it bubbles to the cell, which is\n what toggles the row.\n -->\n <span\n class=\"rv-table-select\"\n aria-hidden=\"true\"\n [attr.data-checked]=\"item.selected ? 'true' : null\"\n ></span>\n }\n @if (cell.template; as template) {\n <ng-container\n [ngTemplateOutlet]=\"template\"\n [ngTemplateOutletContext]=\"cell.context\"\n />\n } @else {\n <!--\n `dir=\"auto\"`: the grid's direction is the page's, but the\n VALUE's direction is the value's. Without it an English\n client name in an Arabic table renders with its full stop\n moved to the front (\".Nile Trading Co\"), which is correct\n bidi for an RTL paragraph and wrong for the data.\n -->\n <span\n class=\"rv-table-text\"\n dir=\"auto\"\n [attr.data-overflow]=\"cell.overflow\"\n [style.--rv-table-clamp-lines]=\"cell.lines\"\n >{{ cell.text }}</span\n >\n }\n <!--\n The untruncated text, exposed through `aria-describedby` and\n hidden from the browse-order so it is not read twice. There is\n deliberately no `title`: the visual tooltip is the consumer's.\n -->\n @if (cell.describedBy) {\n <span class=\"rv-table-sr-only\" [id]=\"cell.describedBy\" aria-hidden=\"true\">{{\n cell.text\n }}</span>\n }\n </div>\n }\n </div>\n }\n }\n </div>\n </div>\n\n <!--\n Inside the scroller (it belongs to the frame) but outside the grid (a grid\n may only own rows). `position: sticky` on the inline axis keeps it in view\n while the header scrolls horizontally.\n -->\n @if (state(); as current) {\n <div\n class=\"rv-table-state\"\n [attr.role]=\"current.kind === 'error' ? 'alert' : 'status'\"\n [attr.data-state]=\"current.kind\"\n >\n <span class=\"rv-table-state-art\" aria-hidden=\"true\">\n @if (current.kind === 'error') {\n <svg viewBox=\"0 0 24 24\" width=\"24\" height=\"24\" focusable=\"false\">\n <path\n d=\"M12 7v6m0 4h.01M12 3l9 16H3l9-16Z\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"1.6\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n />\n </svg>\n } @else {\n <svg viewBox=\"0 0 24 24\" width=\"24\" height=\"24\" focusable=\"false\">\n <path\n d=\"M4 6h16M4 12h16M4 18h10\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"1.6\"\n stroke-linecap=\"round\"\n />\n </svg>\n }\n </span>\n <p class=\"rv-table-state-text\">{{ current.message }}</p>\n </div>\n }\n</div>\n\n<!-- Always present, so the text change is announced by the live region. -->\n<p class=\"rv-table-sr-only\" role=\"status\">{{ loading() ? effectiveLabels().loading : '' }}</p>\n\n<!--\n The cell templates are never rendered here: they are looked up by column id\n and instantiated per row. Projecting them explicitly keeps the contract\n visible in the DOM (and avoids keeping unprojected content around).\n-->\n<ng-content select=\"[rvTableCell]\" />\n",
708
722
  styles: [":host{display:block;font-size:var(--rv-table-font-size, .875rem);color:var(--rv-table-fg, var(--rv-fg, light-dark(#111827, #f4f4f5)));-webkit-text-size-adjust:100%}.rv-table-scroller,.rv-table-scroller *,.rv-table-scroller *:before,.rv-table-scroller *:after{box-sizing:border-box}.rv-table-scroller{position:relative;overflow:auto;max-block-size:var(--rv-table-max-height, none);overscroll-behavior:contain;border:1px solid var(--rv-table-border, var(--rv-border, light-dark(#e5e7eb, #2f2f36)));border-radius:var(--rv-table-radius, var(--rv-radius, .5rem));background:var(--rv-table-bg, var(--rv-bg, light-dark(#ffffff, #17171b)));scrollbar-width:thin;scrollbar-color:var(--rv-table-scrollbar, light-dark(#cbd5e1, #3f3f46)) transparent}.rv-table-grid{display:block}.rv-table-head{position:relative;z-index:2;background:var( --rv-table-header-bg, color-mix( in oklab, var(--rv-table-bg, var(--rv-bg, light-dark(#ffffff, #17171b))) 92%, var(--rv-table-fg, var(--rv-fg, light-dark(#111827, #f4f4f5))) ) );border-block-end:1px solid var(--rv-table-header-border, var(--rv-border, light-dark(#d1d5db, #3f3f46)))}:host([data-sticky=\"true\"]) .rv-table-head{position:sticky;inset-block-start:0}.rv-table-head .rv-table-row{display:grid;grid-template-columns:var(--rv-table-columns, 1fr)}.rv-table-head-cell{min-block-size:var(--rv-table-head-height, 2.75rem);border-block-end:0;color:var(--rv-table-header-fg, var(--rv-muted, light-dark(#52525b, #a1a1aa)));font-size:var(--rv-table-header-font-size, .75rem);font-weight:var(--rv-table-header-font-weight, 700);letter-spacing:.02em;-webkit-user-select:none;user-select:none}.rv-table-head-cell[data-sortable=true]{cursor:pointer}.rv-table-head-cell[data-sortable=true]:hover{background:var( --rv-table-header-hover, color-mix( in oklab, var(--rv-table-bg, var(--rv-bg, light-dark(#ffffff, #17171b))) 86%, var(--rv-table-fg, var(--rv-fg, light-dark(#111827, #f4f4f5))) ) );color:var(--rv-table-header-fg-active, var(--rv-fg, light-dark(#111827, #f4f4f5)))}.rv-table-head-cell[data-sorted=true]{color:var(--rv-table-header-fg-active, var(--rv-fg, light-dark(#111827, #f4f4f5)))}.rv-table-head-button{display:flex;align-items:center;gap:.375rem;inline-size:100%;min-inline-size:0;margin:0;padding:0;border:0;background:none;color:inherit;font:inherit;letter-spacing:inherit;text-align:inherit;cursor:inherit}.rv-table-head-button:focus{outline:none}.rv-table-head-label{min-inline-size:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.rv-table-sort{display:inline-flex;flex:0 0 auto;align-items:center;justify-content:center;inline-size:1rem;block-size:1rem;color:var(--rv-table-sort-fg, var(--rv-accent, light-dark(#5b5bd6, #8b8bf0)));opacity:0;transition:opacity .12s ease,transform .12s ease}.rv-table-head-cell[data-sortable=true]:hover .rv-table-sort{opacity:.45}.rv-table-head-cell[data-sorted=true] .rv-table-sort{opacity:1}.rv-table-sort[data-direction=desc]{transform:rotate(180deg)}.rv-table-sort-rank{display:inline-grid;flex:0 0 auto;place-items:center;min-inline-size:1rem;block-size:1rem;padding-inline:.2rem;border-radius:999px;background:color-mix(in oklab,var(--rv-table-sort-fg, var(--rv-accent, light-dark(#5b5bd6, #8b8bf0))) 16%,transparent);color:var(--rv-table-sort-fg, var(--rv-accent, light-dark(#5b5bd6, #8b8bf0)));font-size:.6875rem;font-weight:700;font-variant-numeric:tabular-nums;line-height:1}.rv-table-body{display:grid;grid-template-columns:var(--rv-table-columns, 1fr);grid-auto-rows:minmax(var(--rv-table-row-height, 2.5rem),auto)}.rv-table-row{display:contents}.rv-table-cell{position:relative;display:flex;align-items:center;gap:.375rem;min-inline-size:0;padding:var(--rv-table-cell-padding, .5rem .75rem);border-block-end:1px solid var(--rv-table-border, var(--rv-border, light-dark(#e5e7eb, #2f2f36)));background:transparent;transition:background-color .12s ease}.rv-table-row:hover>.rv-table-cell{background:var( --rv-table-row-hover, color-mix( in oklab, var(--rv-table-bg, var(--rv-bg, light-dark(#ffffff, #17171b))) 94%, var(--rv-table-fg, var(--rv-fg, light-dark(#111827, #f4f4f5))) ) )}.rv-table-select{display:inline-grid;place-items:center;flex:none;inline-size:1rem;block-size:1rem;border:1px solid var(--rv-table-select-border, var(--rv-border, light-dark(#9ca3af, #52525b)));border-radius:var(--rv-table-select-radius, .25rem);background:var(--rv-table-select-bg, var(--rv-bg, light-dark(#ffffff, #17171b)));transition:background-color .12s ease,border-color .12s ease}.rv-table-select:after{content:\"\";inline-size:.5rem;block-size:.3rem;border-inline-start:2px solid var(--rv-table-select-mark, var(--rv-bg, light-dark(#ffffff, #17171b)));border-block-end:2px solid var(--rv-table-select-mark, var(--rv-bg, light-dark(#ffffff, #17171b)));transform:translateY(-.06rem) rotate(-45deg) scale(0);transition:transform .12s ease}.rv-table-select[data-checked=true]{border-color:var(--rv-table-accent, var(--rv-accent, light-dark(#5b5bd6, #8b8bf0)));background:var(--rv-table-accent, var(--rv-accent, light-dark(#5b5bd6, #8b8bf0)))}.rv-table-select[data-checked=true]:after{transform:translateY(-.06rem) rotate(-45deg) scale(1)}@media(prefers-reduced-motion:reduce){.rv-table-select,.rv-table-select:after{transition:none}}.rv-table-row[aria-selected=true]>.rv-table-cell{background:var( --rv-table-row-selected, color-mix( in oklab, var(--rv-table-accent, var(--rv-accent, light-dark(#5b5bd6, #8b8bf0))) 12%, var(--rv-table-bg, var(--rv-bg, light-dark(#ffffff, #17171b))) ) )}.rv-table-row[aria-selected=true]>.rv-table-cell[data-col=\"0\"]:before{content:\"\";position:absolute;inset-block:0;inset-inline-start:0;inline-size:var(--rv-table-selected-rail, 3px);background:var(--rv-table-accent, var(--rv-accent, light-dark(#5b5bd6, #8b8bf0)))}.rv-table-cell[data-align=center]{justify-content:center;text-align:center}.rv-table-cell[data-align=end]{justify-content:flex-end;text-align:end}.rv-table-cell[data-span]{align-items:flex-start}.rv-table-cell:focus{outline:none}.rv-table-cell:focus-visible{outline:2px solid var(--rv-table-focus, var(--rv-accent, light-dark(#5b5bd6, #8b8bf0)));outline-offset:-2px;z-index:1}.rv-table-text{min-inline-size:0;max-inline-size:100%}.rv-table-text[data-overflow=nowrap]{overflow:hidden;white-space:nowrap;text-overflow:ellipsis}.rv-table-text[data-overflow=wrap]{overflow-wrap:anywhere;white-space:normal}.rv-table-text[data-overflow=clamp]{display:-webkit-box;overflow:hidden;overflow-wrap:anywhere;-webkit-box-orient:vertical;-webkit-line-clamp:var(--rv-table-clamp-lines, 2);line-clamp:var(--rv-table-clamp-lines, 2)}.rv-table-skeleton{pointer-events:none}.rv-table-skeleton-bar{display:block;inline-size:64%;block-size:.625rem;border-radius:.3125rem;background:linear-gradient(90deg,var(--rv-table-skeleton, light-dark(#eceff3, #26262d)) 25%,var(--rv-table-skeleton-highlight, light-dark(#f6f8fa, #31313a)) 37%,var(--rv-table-skeleton, light-dark(#eceff3, #26262d)) 63%);background-size:400% 100%;animation:rv-table-shimmer 1.4s ease-in-out infinite}.rv-table-skeleton:nth-child(3n+2) .rv-table-skeleton-bar{inline-size:82%}.rv-table-skeleton:nth-child(3n) .rv-table-skeleton-bar{inline-size:46%}@keyframes rv-table-shimmer{0%{background-position:100% 0}to{background-position:0 0}}.rv-table-state{position:sticky;inset-inline-start:0;display:flex;flex-direction:column;align-items:center;gap:.5rem;inline-size:100%;padding:var(--rv-table-state-padding, 3rem 1.25rem);text-align:center;background:var(--rv-table-bg, var(--rv-bg, light-dark(#ffffff, #17171b)));color:var(--rv-table-state-fg, var(--rv-muted, light-dark(#52525b, #a1a1aa)))}.rv-table-state-art{display:grid;place-items:center;inline-size:3rem;block-size:3rem;border-radius:1rem;background:color-mix(in oklab,var(--rv-table-state-fg, var(--rv-muted, light-dark(#52525b, #a1a1aa))) 10%,transparent)}.rv-table-state[data-state=error] .rv-table-state-art{background:color-mix(in oklab,var(--rv-table-state-error, var(--rv-danger, light-dark(#dc2626, #f87171))) 14%,transparent);color:var(--rv-table-state-error, var(--rv-danger, light-dark(#dc2626, #f87171)))}.rv-table-state-text{margin:0;max-inline-size:34rem;color:var(--rv-table-state-text, var(--rv-fg, light-dark(#111827, #f4f4f5)));font-size:.9375rem;line-height:1.5}.rv-table-sr-only{position:absolute;inline-size:1px;block-size:1px;margin:-1px;padding:0;overflow:hidden;clip-path:inset(50%);white-space:nowrap;border:0}:host([data-density=\"compact\"]) .rv-table-body{grid-auto-rows:minmax(var(--rv-table-row-height, 2rem),auto)}:host([data-density=\"compact\"]) .rv-table-cell{padding:var(--rv-table-cell-padding, .25rem .5rem)}:host([data-density=\"compact\"]) .rv-table-head-cell{min-block-size:var(--rv-table-head-height, 2.25rem)}:host([data-density=\"compact\"]) .rv-table-skeleton-bar{block-size:.5rem}:host([data-density=\"comfortable\"]) .rv-table-body{grid-auto-rows:minmax(var(--rv-table-row-height, 3rem),auto)}:host([data-density=\"comfortable\"]) .rv-table-cell{padding:var(--rv-table-cell-padding, .75rem 1rem)}:host([data-density=\"comfortable\"]) .rv-table-head-cell{min-block-size:var(--rv-table-head-height, 3.25rem)}@media(prefers-reduced-motion:reduce){.rv-table-cell,.rv-table-sort{transition:none}.rv-table-skeleton-bar{animation:none}}@media(forced-colors:active){.rv-table-cell:focus-visible{outline-color:Highlight}.rv-table-skeleton-bar{background:GrayText}.rv-table-row[aria-selected=true]>.rv-table-cell[data-col=\"0\"]:before{background:Highlight}}\n"]
709
723
  }]
710
724
  }],