@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.
- package/CHANGELOG.md +10 -0
- package/LICENSE +21 -0
- package/README.md +41 -245
- package/fesm2022/rivinia-table-ui.mjs +31 -17
- package/fesm2022/rivinia-table-ui.mjs.map +1 -1
- package/fesm2022/rivinia-table.mjs +40 -10
- package/fesm2022/rivinia-table.mjs.map +1 -1
- package/package.json +13 -1
- package/types/rivinia-table-ui.d.ts +2 -8
- package/types/rivinia-table.d.ts +12 -16
|
@@ -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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
|
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]
|
|
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(
|
|
506
|
-
if (!this.selectable()) return;
|
|
507
|
-
const id =
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
}],
|