@rivinia/table 0.1.0 → 1.0.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.
package/CHANGELOG.md ADDED
@@ -0,0 +1,6 @@
1
+ # Changelog
2
+
3
+ ## 1.0.0
4
+
5
+ - First stable release. Row identity must be unique and stable; ambiguous IDs cannot own selection or other identity-dependent state.
6
+
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Rivinia contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -3,12 +3,16 @@
3
3
  [![npm version](https://img.shields.io/npm/v/@rivinia/table.svg)](https://www.npmjs.com/package/@rivinia/table)
4
4
  [![license](https://img.shields.io/npm/l/@rivinia/table.svg)](https://www.npmjs.com/package/@rivinia/table)
5
5
 
6
+ <p align="center">
7
+ <img src="https://raw.githubusercontent.com/abdelfattahqandil21-oss/Revinia/table/v1.0.0/assets/rivinia-abstract-mark-light.svg" alt="Rivinia" width="340" />
8
+ </p>
9
+
6
10
  A headless Angular table with typed columns, real row and column spans,
7
11
  sorting, filtering, selection and paging — plus an optional ready-made grid.
8
12
  The core owns the data and the geometry; it never renders a cell, never knows
9
13
  what an "action" is, and never imports a stylesheet.
10
14
 
11
- - Package version: **0.1.0**
15
+ - Package version: **1.0.0**
12
16
  - Requires: **Angular `^22.0.0`** (`@angular/core` and `@angular/common`).
13
17
  - Entry points: `@rivinia/table` (headless controller + pure helpers) and
14
18
  `@rivinia/table/ui` (the `rv-table` grid, div-based, `role="grid"`).
@@ -82,7 +86,7 @@ property read at the use site.
82
86
  ```ts
83
87
  import { createTable, tableColumn } from '@rivinia/table';
84
88
 
85
- @Component({ /* … */ })
89
+ @Component({/* … */})
86
90
  export class InvoicesPage {
87
91
  private readonly table = createTable<Invoice>(() => ({
88
92
  // Columns are a FACTORY too: `options()` runs inside a computed, so a
@@ -117,19 +121,19 @@ A column is data, not a component. `tableColumn<T>()` has two builders:
117
121
  `accessor(key, def?)` reads a field of the row, `display(id, def?)` has no field
118
122
  and exists for a template or an actions column.
119
123
 
120
- | Field | Meaning |
121
- | --- | --- |
122
- | `header` | Visible header text. Omit for an icon-only column. |
123
- | `width` | A CSS length. Fixed columns take part in `table-layout: fixed`. |
124
- | `flex` | `> 0` absorbs free space; `0` stays fixed. |
125
- | `align` | `'start' \| 'center' \| 'end'` — logical, so RTL needs no override. |
126
- | `sortable` | Adds the three-state sort affordance and `aria-sort`. |
127
- | `format` | `(value, row, locale) => string` for display only. |
128
- | `comparator` | Sort with something other than the default compare. |
129
- | `pinned` | `'start' \| 'end'` — stays visible during horizontal scroll. |
130
- | `textOverflow` | `'nowrap'` (default), `'wrap'`, `'clamp'`. |
131
- | `lines` | Clamp height, default 2. |
132
- | `hidden` | Removed from the visible set without losing its definition. |
124
+ | Field | Meaning |
125
+ | -------------- | ------------------------------------------------------------------- |
126
+ | `header` | Visible header text. Omit for an icon-only column. |
127
+ | `width` | A CSS length. Fixed columns take part in `table-layout: fixed`. |
128
+ | `flex` | `> 0` absorbs free space; `0` stays fixed. |
129
+ | `align` | `'start' \| 'center' \| 'end'` — logical, so RTL needs no override. |
130
+ | `sortable` | Adds the three-state sort affordance and `aria-sort`. |
131
+ | `format` | `(value, row, locale) => string` for display only. |
132
+ | `comparator` | Sort with something other than the default compare. |
133
+ | `pinned` | `'start' \| 'end'` — stays visible during horizontal scroll. |
134
+ | `textOverflow` | `'nowrap'` (default), `'wrap'`, `'clamp'`. |
135
+ | `lines` | Clamp height, default 2. |
136
+ | `hidden` | Removed from the visible set without losing its definition. |
133
137
 
134
138
  ## Row and column spans
135
139
 
@@ -139,15 +143,15 @@ in increasing power:
139
143
 
140
144
  ```ts
141
145
  // 1. Row span by a shared key: consecutive rows with an equal value merge.
142
- tableColumn<Invoice>().accessor('department', { header: 'Department', merge: 'equal' })
146
+ tableColumn<Invoice>().accessor('department', { header: 'Department', merge: 'equal' });
143
147
 
144
148
  // 2. Column span: this cell covers N columns to its end.
145
- tableColumn<Invoice>().accessor('note', { header: 'Notes', colSpan: 2 })
149
+ tableColumn<Invoice>().accessor('note', { header: 'Notes', colSpan: 2 });
146
150
 
147
151
  // 3. Both axes, computed per cell.
148
152
  tableColumn<Invoice>().display('summary', {
149
153
  span: ({ rowIndex }) => (rowIndex === 0 ? { colSpan: 5 } : null),
150
- })
154
+ });
151
155
  ```
152
156
 
153
157
  Rules, enforced at `createTable()` rather than at render time:
@@ -181,10 +185,21 @@ createTable<Invoice>(() => ({
181
185
  ```ts
182
186
  table.selection.toggle(row.id);
183
187
  table.selection.isSelected(row.id);
184
- table.selection.selectAllOnPage(); // page scope, never "everything by accident"
188
+ table.selection.selectAllOnPage(); // page scope, never "everything by accident"
185
189
  table.selection.clear();
186
190
  ```
187
191
 
192
+ ### Row identity contract
193
+
194
+ `getRowId` must return a unique, stable, non-empty string or finite number for
195
+ every row in the full active data set. Validation runs before filtering,
196
+ sorting, paging, or selection state is committed. In development, duplicate IDs
197
+ throw an error naming the ID and source row indices; invalid values name their
198
+ source index. In production, rows with ambiguous IDs remain visible, but
199
+ selection and other identity-dependent actions are disabled for those rows.
200
+ The table never substitutes an array index for an invalid ID. If rows have no
201
+ stable `id`, provide a `getRowId` backed by a persistent domain key.
202
+
188
203
  ## Paging and state
189
204
 
190
205
  `page`, `pageSize`, `sorting`, `filters` and `globalSearch` are writable
@@ -271,3 +286,7 @@ Sizes are in `rem`, so the grid follows the browser's text size; hairlines stay
271
286
  and spans are the 0.1.0 scope; server mode (`TableRequest`, `reload()`) and
272
287
  editing (`@rivinia/table/editing`) are planned, additive, and not part of this
273
288
  version.
289
+
290
+ ## Further reading
291
+
292
+ [Package design and usage notes](https://github.com/abdelfattahqandil21-oss/Revinia/blob/table/v1.0.0/docs/specifications/table-design.md).
@@ -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
  }],