@xmesh/system-design 0.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/README.md +472 -0
- package/assets/brand-lockup-dark.svg +9 -0
- package/assets/brand-lockup-light.svg +9 -0
- package/assets/brand-mark.svg +9 -0
- package/colors_and_type.css +11 -0
- package/dist/lit/components/alert/index.css +201 -0
- package/dist/lit/components/alert/index.d.ts +25 -0
- package/dist/lit/components/alert/index.js +191 -0
- package/dist/lit/components/app-bar/index.css +80 -0
- package/dist/lit/components/app-bar/index.d.ts +19 -0
- package/dist/lit/components/app-bar/index.js +120 -0
- package/dist/lit/components/artifact/index.css +166 -0
- package/dist/lit/components/artifact/index.d.ts +37 -0
- package/dist/lit/components/artifact/index.js +294 -0
- package/dist/lit/components/autocomplete/index.css +171 -0
- package/dist/lit/components/autocomplete/index.d.ts +47 -0
- package/dist/lit/components/autocomplete/index.js +404 -0
- package/dist/lit/components/avatar/index.css +62 -0
- package/dist/lit/components/avatar/index.d.ts +19 -0
- package/dist/lit/components/avatar/index.js +112 -0
- package/dist/lit/components/avatar-group/index.css +60 -0
- package/dist/lit/components/avatar-group/index.d.ts +19 -0
- package/dist/lit/components/avatar-group/index.js +97 -0
- package/dist/lit/components/badge/index.css +72 -0
- package/dist/lit/components/badge/index.d.ts +18 -0
- package/dist/lit/components/badge/index.js +115 -0
- package/dist/lit/components/brand-mark/index.css +109 -0
- package/dist/lit/components/brand-mark/index.d.ts +24 -0
- package/dist/lit/components/brand-mark/index.js +116 -0
- package/dist/lit/components/breadcrumbs/index.css +91 -0
- package/dist/lit/components/breadcrumbs/index.d.ts +19 -0
- package/dist/lit/components/breadcrumbs/index.js +104 -0
- package/dist/lit/components/bubble/index.css +182 -0
- package/dist/lit/components/bubble/index.d.ts +72 -0
- package/dist/lit/components/bubble/index.js +617 -0
- package/dist/lit/components/button/index.css +342 -0
- package/dist/lit/components/button/index.d.ts +32 -0
- package/dist/lit/components/button/index.js +202 -0
- package/dist/lit/components/card/index.css +99 -0
- package/dist/lit/components/card/index.d.ts +20 -0
- package/dist/lit/components/card/index.js +133 -0
- package/dist/lit/components/chat/index.css +292 -0
- package/dist/lit/components/chat/index.d.ts +74 -0
- package/dist/lit/components/chat/index.js +589 -0
- package/dist/lit/components/checkbox/index.css +126 -0
- package/dist/lit/components/checkbox/index.d.ts +21 -0
- package/dist/lit/components/checkbox/index.js +138 -0
- package/dist/lit/components/chip/index.css +145 -0
- package/dist/lit/components/chip/index.d.ts +30 -0
- package/dist/lit/components/chip/index.js +230 -0
- package/dist/lit/components/chip-group/index.css +19 -0
- package/dist/lit/components/chip-group/index.d.ts +24 -0
- package/dist/lit/components/chip-group/index.js +171 -0
- package/dist/lit/components/code/index.css +42 -0
- package/dist/lit/components/code/index.d.ts +12 -0
- package/dist/lit/components/code/index.js +68 -0
- package/dist/lit/components/composer/index.css +548 -0
- package/dist/lit/components/composer/index.d.ts +67 -0
- package/dist/lit/components/composer/index.js +713 -0
- package/dist/lit/components/data-table/index.css +166 -0
- package/dist/lit/components/data-table/index.d.ts +55 -0
- package/dist/lit/components/data-table/index.js +390 -0
- package/dist/lit/components/dialog/index.css +124 -0
- package/dist/lit/components/dialog/index.d.ts +24 -0
- package/dist/lit/components/dialog/index.js +199 -0
- package/dist/lit/components/divider/index.css +27 -0
- package/dist/lit/components/divider/index.d.ts +13 -0
- package/dist/lit/components/divider/index.js +67 -0
- package/dist/lit/components/empty-state/index.css +69 -0
- package/dist/lit/components/empty-state/index.d.ts +21 -0
- package/dist/lit/components/empty-state/index.js +123 -0
- package/dist/lit/components/expansion-panel/index.css +120 -0
- package/dist/lit/components/expansion-panel/index.d.ts +22 -0
- package/dist/lit/components/expansion-panel/index.js +174 -0
- package/dist/lit/components/field/index.css +223 -0
- package/dist/lit/components/field/index.d.ts +106 -0
- package/dist/lit/components/field/index.js +388 -0
- package/dist/lit/components/file-input/index.css +257 -0
- package/dist/lit/components/file-input/index.d.ts +30 -0
- package/dist/lit/components/file-input/index.js +298 -0
- package/dist/lit/components/form/index.css +29 -0
- package/dist/lit/components/form/index.d.ts +38 -0
- package/dist/lit/components/form/index.js +192 -0
- package/dist/lit/components/grid/index.css +53 -0
- package/dist/lit/components/grid/index.d.ts +14 -0
- package/dist/lit/components/grid/index.js +82 -0
- package/dist/lit/components/kbd/index.css +35 -0
- package/dist/lit/components/kbd/index.d.ts +11 -0
- package/dist/lit/components/kbd/index.js +43 -0
- package/dist/lit/components/list/index.css +15 -0
- package/dist/lit/components/list/index.d.ts +28 -0
- package/dist/lit/components/list/index.js +188 -0
- package/dist/lit/components/list-item/index.css +119 -0
- package/dist/lit/components/list-item/index.d.ts +20 -0
- package/dist/lit/components/list-item/index.js +127 -0
- package/dist/lit/components/menu/index.css +94 -0
- package/dist/lit/components/menu/index.d.ts +47 -0
- package/dist/lit/components/menu/index.js +386 -0
- package/dist/lit/components/navigation-drawer/index.css +114 -0
- package/dist/lit/components/navigation-drawer/index.d.ts +29 -0
- package/dist/lit/components/navigation-drawer/index.js +218 -0
- package/dist/lit/components/overlay/index.css +171 -0
- package/dist/lit/components/overlay/index.d.ts +65 -0
- package/dist/lit/components/overlay/index.js +566 -0
- package/dist/lit/components/pagination/index.css +102 -0
- package/dist/lit/components/pagination/index.d.ts +22 -0
- package/dist/lit/components/pagination/index.js +184 -0
- package/dist/lit/components/primitives/index.css +504 -0
- package/dist/lit/components/primitives/index.d.ts +25 -0
- package/dist/lit/components/primitives/index.js +283 -0
- package/dist/lit/components/progress/index.css +143 -0
- package/dist/lit/components/progress/index.d.ts +23 -0
- package/dist/lit/components/progress/index.js +180 -0
- package/dist/lit/components/radio-group/index.css +178 -0
- package/dist/lit/components/radio-group/index.d.ts +35 -0
- package/dist/lit/components/radio-group/index.js +292 -0
- package/dist/lit/components/select/index.css +151 -0
- package/dist/lit/components/select/index.d.ts +50 -0
- package/dist/lit/components/select/index.js +390 -0
- package/dist/lit/components/sidebar-item/index.css +133 -0
- package/dist/lit/components/sidebar-item/index.d.ts +20 -0
- package/dist/lit/components/sidebar-item/index.js +105 -0
- package/dist/lit/components/skeleton/index.css +81 -0
- package/dist/lit/components/skeleton/index.d.ts +19 -0
- package/dist/lit/components/skeleton/index.js +119 -0
- package/dist/lit/components/slider/index.css +171 -0
- package/dist/lit/components/slider/index.d.ts +36 -0
- package/dist/lit/components/slider/index.js +302 -0
- package/dist/lit/components/snackbar/index.css +279 -0
- package/dist/lit/components/snackbar/index.d.ts +33 -0
- package/dist/lit/components/snackbar/index.js +195 -0
- package/dist/lit/components/stack/index.css +41 -0
- package/dist/lit/components/stack/index.d.ts +20 -0
- package/dist/lit/components/stack/index.js +103 -0
- package/dist/lit/components/switch/index.css +126 -0
- package/dist/lit/components/switch/index.d.ts +17 -0
- package/dist/lit/components/switch/index.js +116 -0
- package/dist/lit/components/table/index.css +85 -0
- package/dist/lit/components/table/index.d.ts +25 -0
- package/dist/lit/components/table/index.js +139 -0
- package/dist/lit/components/tabs/index.css +116 -0
- package/dist/lit/components/tabs/index.d.ts +49 -0
- package/dist/lit/components/tabs/index.js +320 -0
- package/dist/lit/components/text-field/index.css +90 -0
- package/dist/lit/components/text-field/index.d.ts +17 -0
- package/dist/lit/components/text-field/index.js +101 -0
- package/dist/lit/components/textarea/index.css +55 -0
- package/dist/lit/components/textarea/index.d.ts +26 -0
- package/dist/lit/components/textarea/index.js +124 -0
- package/dist/lit/components/tooltip/index.css +37 -0
- package/dist/lit/components/tooltip/index.d.ts +31 -0
- package/dist/lit/components/tooltip/index.js +196 -0
- package/dist/lit/components/validation/index.css +386 -0
- package/dist/lit/components/validation/index.d.ts +45 -0
- package/dist/lit/components/validation/index.js +318 -0
- package/dist/lit/index.d.ts +50 -0
- package/dist/lit/index.js +59 -0
- package/package.json +81 -0
- package/styles/README.md +346 -0
- package/styles/_elevation.css +24 -0
- package/styles/_fonts.css +6 -0
- package/styles/_layout.css +37 -0
- package/styles/_primitives.css +154 -0
- package/styles/_scroll.css +75 -0
- package/styles/_semantic.css +146 -0
- package/styles/_space.css +61 -0
- package/styles/_type.css +139 -0
- package/styles/_xmesh-extensions.css +232 -0
- package/styles/index.css +44 -0
- package/styles/md3/_color.css +102 -0
- package/styles/md3/_elevation.css +26 -0
- package/styles/md3/_motion.css +35 -0
- package/styles/md3/_shape.css +22 -0
- package/styles/md3/_state.css +22 -0
- package/styles/md3/_type.css +111 -0
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/* ============================================
|
|
2
|
+
Data table — the interactive sibling of <xm-table>.
|
|
3
|
+
|
|
4
|
+
Mirrors xm-table's static chrome (1px --md-sys-color-outline-variant
|
|
5
|
+
hairlines, sentence-case headers, numeric right-align + tabular figures,
|
|
6
|
+
number + unit carry a space "412 KB"), then layers in sort, paging, loading,
|
|
7
|
+
and empty states.
|
|
8
|
+
|
|
9
|
+
It sits on ONE surface family — surface* with ink --md-sys-color-on-surface
|
|
10
|
+
(AD-13). Region differentiation is the hairline rule + an optional
|
|
11
|
+
surface-container raise on the header, never a status hue (AD-11). No
|
|
12
|
+
--md-sys-color-error* anywhere; the single coral accent
|
|
13
|
+
(--md-sys-color-primary) marks the active sort.
|
|
14
|
+
|
|
15
|
+
BEM block: `data-table`. Registered in scripts/check-bem.sh STRICT_BLOCKS.
|
|
16
|
+
Elements: __head, __row, __cell, __header-cell, __header-label, __sort,
|
|
17
|
+
__sort-icon, __footer, __pagination, __empty. Modifiers: __cell--end,
|
|
18
|
+
__cell--muted, __header-cell--end / --sortable / --active,
|
|
19
|
+
__row--skeleton / --empty.
|
|
20
|
+
============================================ */
|
|
21
|
+
|
|
22
|
+
.data-table {
|
|
23
|
+
width: 100%;
|
|
24
|
+
border-collapse: collapse;
|
|
25
|
+
background: transparent;
|
|
26
|
+
color: var(--md-sys-color-on-surface);
|
|
27
|
+
font-family: var(--md-sys-typescale-body-medium-font);
|
|
28
|
+
font-size: var(--md-sys-typescale-body-medium-size);
|
|
29
|
+
line-height: var(--md-sys-typescale-body-medium-line-height);
|
|
30
|
+
border: 1px solid var(--md-sys-color-outline-variant);
|
|
31
|
+
border-radius: var(--md-sys-shape-corner-medium);
|
|
32
|
+
overflow: hidden;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.data-table__head {
|
|
36
|
+
background: var(--md-sys-color-surface-container-low);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.data-table__header-cell {
|
|
40
|
+
text-align: start;
|
|
41
|
+
padding: 0;
|
|
42
|
+
border-bottom: 1px solid var(--md-sys-color-outline-variant);
|
|
43
|
+
color: var(--md-sys-color-on-surface);
|
|
44
|
+
font-family: var(--md-sys-typescale-label-large-font);
|
|
45
|
+
font-size: var(--md-sys-typescale-label-medium-size);
|
|
46
|
+
font-weight: 600;
|
|
47
|
+
letter-spacing: 0;
|
|
48
|
+
white-space: nowrap;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.data-table__header-label {
|
|
52
|
+
display: inline-block;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/* The static label sits with the same padding the sortable hit-area carries,
|
|
56
|
+
so sortable and inert headers align on a shared baseline. */
|
|
57
|
+
.data-table__header-cell > .data-table__header-label {
|
|
58
|
+
padding: var(--s-3) var(--s-4);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/* Sortable header: a focusable in-cell control that fills the cell so the whole
|
|
62
|
+
header is the hit target. Hover/active swap real tokens — no filter/opacity. */
|
|
63
|
+
.data-table__sort {
|
|
64
|
+
display: flex;
|
|
65
|
+
align-items: center;
|
|
66
|
+
gap: var(--s-2);
|
|
67
|
+
box-sizing: border-box;
|
|
68
|
+
width: 100%;
|
|
69
|
+
padding: var(--s-3) var(--s-4);
|
|
70
|
+
cursor: pointer;
|
|
71
|
+
user-select: none;
|
|
72
|
+
color: inherit;
|
|
73
|
+
border-radius: var(--md-sys-shape-corner-small);
|
|
74
|
+
outline: none;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.data-table__header-cell--sortable .data-table__sort:hover {
|
|
78
|
+
background: color-mix(
|
|
79
|
+
in oklab,
|
|
80
|
+
var(--md-sys-color-on-surface) var(--md-sys-state-hover-state-layer-opacity),
|
|
81
|
+
transparent
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
.data-table__header-cell--sortable .data-table__sort:active {
|
|
86
|
+
background: color-mix(
|
|
87
|
+
in oklab,
|
|
88
|
+
var(--md-sys-color-on-surface) var(--md-sys-state-pressed-state-layer-opacity),
|
|
89
|
+
transparent
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.data-table__header-cell--sortable .data-table__sort:focus-visible {
|
|
94
|
+
box-shadow: var(--xm-state-focus-ring);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/* The active sorted column reads in the coral accent — the only emphasis (AD-11). */
|
|
98
|
+
.data-table__header-cell--active {
|
|
99
|
+
color: var(--md-sys-color-primary);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
.data-table__sort-icon {
|
|
103
|
+
display: inline-flex;
|
|
104
|
+
align-items: center;
|
|
105
|
+
color: var(--md-sys-color-primary);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/* End-aligned (numeric) headers push their content to the trailing edge. */
|
|
109
|
+
.data-table__header-cell--end .data-table__sort,
|
|
110
|
+
.data-table__header-cell--end > .data-table__header-label {
|
|
111
|
+
justify-content: flex-end;
|
|
112
|
+
text-align: end;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
.data-table__row + .data-table__row .data-table__cell {
|
|
116
|
+
border-top: 1px solid var(--md-sys-color-outline-variant);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
.data-table__cell {
|
|
120
|
+
padding: var(--s-3) var(--s-4);
|
|
121
|
+
text-align: start;
|
|
122
|
+
vertical-align: middle;
|
|
123
|
+
color: var(--md-sys-color-on-surface);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
.data-table__cell--muted {
|
|
127
|
+
color: var(--md-sys-color-on-surface-variant);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/* Numeric — right-aligned, tabular figures so digits line up; the unit carries
|
|
131
|
+
a space in the value ("412 KB"). Matches xm-table (NFR-22 / UX-DR4). */
|
|
132
|
+
.data-table__header-cell--end,
|
|
133
|
+
.data-table__cell--end {
|
|
134
|
+
text-align: end;
|
|
135
|
+
font-variant-numeric: tabular-nums;
|
|
136
|
+
font-feature-settings: "tnum" 1;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
.data-table__row:not(.data-table__row--skeleton):not(.data-table__row--empty):hover
|
|
140
|
+
.data-table__cell {
|
|
141
|
+
background: color-mix(
|
|
142
|
+
in oklab,
|
|
143
|
+
var(--md-sys-color-on-surface) var(--md-sys-state-hover-state-layer-opacity),
|
|
144
|
+
transparent
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/* Loading rows host composed <xm-skeleton> lines on the same column grid; the
|
|
149
|
+
skeleton owns the gradient-free pulse (AD-12). No row hover under load. */
|
|
150
|
+
.data-table__row--skeleton .data-table__cell {
|
|
151
|
+
padding-block: calc(var(--s-3) + 1px);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/* Empty: a single spanning cell hosting the composed <xm-empty-state>; never a
|
|
155
|
+
blank box (FR-161). The empty-state inherits the host on-* ink (AD-13). */
|
|
156
|
+
.data-table__empty {
|
|
157
|
+
padding: var(--s-8) var(--s-4);
|
|
158
|
+
text-align: center;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/* Footer carries the composed <xm-pagination>, right-aligned under the grid. */
|
|
162
|
+
.data-table__footer {
|
|
163
|
+
display: flex;
|
|
164
|
+
justify-content: flex-end;
|
|
165
|
+
padding-top: var(--s-3);
|
|
166
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { LitElement } from "lit";
|
|
2
|
+
import type { TemplateResult } from "lit";
|
|
3
|
+
import type { TableColumn, TableRow } from "../table/index.js";
|
|
4
|
+
export interface DataTableColumn extends TableColumn {
|
|
5
|
+
sortable?: boolean;
|
|
6
|
+
/** Raw value to sort this column by, when the displayed cell is a formatted
|
|
7
|
+
string the table can't compare correctly on its own (e.g. "412 KB" must
|
|
8
|
+
sort as 421888, "2 min ago" as a timestamp). Returns the primitive used
|
|
9
|
+
for comparison; the displayed cell (`row[key]`) is unchanged. Without it,
|
|
10
|
+
number cells sort by magnitude and string cells sort lexically. */
|
|
11
|
+
sortValue?: (row: TableRow) => string | number | null | undefined;
|
|
12
|
+
}
|
|
13
|
+
export type { TableRow };
|
|
14
|
+
export type SortDirection = "asc" | "desc";
|
|
15
|
+
export interface DataTableSortChangeDetail {
|
|
16
|
+
key: string;
|
|
17
|
+
direction: SortDirection;
|
|
18
|
+
}
|
|
19
|
+
export interface DataTablePageChangeDetail {
|
|
20
|
+
page: number;
|
|
21
|
+
}
|
|
22
|
+
export declare class XmDataTable extends LitElement {
|
|
23
|
+
columns: DataTableColumn[];
|
|
24
|
+
rows: TableRow[];
|
|
25
|
+
loading: boolean;
|
|
26
|
+
pageSize: number;
|
|
27
|
+
emptyHeading: string;
|
|
28
|
+
emptyText: string;
|
|
29
|
+
private _sortKey;
|
|
30
|
+
private _sortDirection;
|
|
31
|
+
private _currentPage;
|
|
32
|
+
willUpdate(changed: Map<string, unknown>): void;
|
|
33
|
+
render(): TemplateResult;
|
|
34
|
+
private _renderTable;
|
|
35
|
+
private _renderHeaderCell;
|
|
36
|
+
private _renderDataRows;
|
|
37
|
+
private _renderSkeletonRows;
|
|
38
|
+
private _renderEmptyRow;
|
|
39
|
+
private _renderFooter;
|
|
40
|
+
private _isEmpty;
|
|
41
|
+
private _isNumericCol;
|
|
42
|
+
private _effectivePageSize;
|
|
43
|
+
private _pageCount;
|
|
44
|
+
private _sortedRows;
|
|
45
|
+
private _pagedRows;
|
|
46
|
+
private _compare;
|
|
47
|
+
private _toggleSort;
|
|
48
|
+
private _onHeaderKey;
|
|
49
|
+
private _onPageChange;
|
|
50
|
+
}
|
|
51
|
+
declare global {
|
|
52
|
+
interface HTMLElementTagNameMap {
|
|
53
|
+
"xm-data-table": XmDataTable;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
/*
|
|
2
|
+
data-table/index.ts — <xm-data-table>, the interactive sibling of <xm-table>.
|
|
3
|
+
|
|
4
|
+
<xm-table> (Story 5.5) is the dumb, static, hairline-ruled grid. <xm-data-table>
|
|
5
|
+
is the interactive layer that wraps the SAME column/row model with:
|
|
6
|
+
|
|
7
|
+
• header-click + keyboard sort (Story 6.1)
|
|
8
|
+
• client-side paging via composed <xm-pagination> (Story 6.2)
|
|
9
|
+
• a loading state of composed <xm-skeleton> rows (Story 6.2)
|
|
10
|
+
• an empty state of a composed <xm-empty-state> (Story 6.2)
|
|
11
|
+
|
|
12
|
+
It consumes the EXACT TableColumn / TableRow shapes that <xm-table> exports, so a
|
|
13
|
+
data-table and a static table read as one system. The cell rendering mirrors
|
|
14
|
+
xm-table's numeric discipline (right-align + tabular figures, unit carries a
|
|
15
|
+
space "412 KB") rather than fighting it.
|
|
16
|
+
|
|
17
|
+
Authoring:
|
|
18
|
+
const dt = document.querySelector("xm-data-table");
|
|
19
|
+
dt.columns = [
|
|
20
|
+
{ key: "name", label: "File name" },
|
|
21
|
+
{ key: "size", label: "Size", numeric: true }, // or align: "end"
|
|
22
|
+
{ key: "calls", label: "Calls", numeric: true, sortable: true },
|
|
23
|
+
];
|
|
24
|
+
dt.rows = [ { name: "schema.json", size: "412 KB", calls: 412 }, … ];
|
|
25
|
+
dt.addEventListener("xm-data-table-sort-change", (e) => e.detail); // {key, direction}
|
|
26
|
+
dt.addEventListener("xm-data-table-page-change", (e) => e.detail); // {page}
|
|
27
|
+
|
|
28
|
+
Properties:
|
|
29
|
+
columns DataTableColumn[] — { key, label, sortable?, align?, numeric?, muted? }
|
|
30
|
+
rows TableRow[] — plain objects keyed by column key
|
|
31
|
+
loading boolean — render skeleton rows (overrides empty + data)
|
|
32
|
+
page-size number — rows per page (default 10)
|
|
33
|
+
empty-heading / empty-text — copy for the composed empty-state
|
|
34
|
+
|
|
35
|
+
Events (both bubbles + composed; AD-8 / AD-8a typed payloads):
|
|
36
|
+
xm-data-table-sort-change detail = { key: string, direction: 'asc' | 'desc' }
|
|
37
|
+
xm-data-table-page-change detail = { page: number }
|
|
38
|
+
|
|
39
|
+
Server-mode boundary (FR-162, DEFERRED): v1 sorts and pages a COPY of the
|
|
40
|
+
in-memory `rows` array, smooth up to the ~500-row client ceiling (NFR-18). The
|
|
41
|
+
props-in / events-out API is deliberately uncontrolled-first so a future server
|
|
42
|
+
mode can take over: a consumer reacting to sort-change / page-change and swapping
|
|
43
|
+
`rows` drives server ordering/paging WITHOUT an API break. Virtualization is also
|
|
44
|
+
deferred — no virtual-list dependency here.
|
|
45
|
+
|
|
46
|
+
Composition (AD-12): the sort indicator is a `primitives` icon; paging, loading,
|
|
47
|
+
and empty chrome are nested <xm-pagination> / <xm-skeleton> / <xm-empty-state>
|
|
48
|
+
elements. This component never re-implements their markup and never reaches into
|
|
49
|
+
their shadow roots. Shadow DOM; Lit from lit.
|
|
50
|
+
*/
|
|
51
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
52
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
53
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
54
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
55
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
56
|
+
};
|
|
57
|
+
import { LitElement, html, nothing } from "lit";
|
|
58
|
+
import { customElement, property, state } from "lit/decorators.js";
|
|
59
|
+
const DATA_TABLE_CSS = new URL("../data-table/index.css", import.meta.url).href;
|
|
60
|
+
const PRIMITIVES_CSS = new URL("../primitives/index.css", import.meta.url).href;
|
|
61
|
+
const DEFAULT_PAGE_SIZE = 10;
|
|
62
|
+
let XmDataTable = class XmDataTable extends LitElement {
|
|
63
|
+
constructor() {
|
|
64
|
+
super(...arguments);
|
|
65
|
+
this.columns = [];
|
|
66
|
+
this.rows = [];
|
|
67
|
+
this.loading = false;
|
|
68
|
+
this.pageSize = DEFAULT_PAGE_SIZE;
|
|
69
|
+
this.emptyHeading = "No data";
|
|
70
|
+
this.emptyText = "";
|
|
71
|
+
this._sortKey = null;
|
|
72
|
+
this._sortDirection = "asc";
|
|
73
|
+
this._currentPage = 1;
|
|
74
|
+
this._onPageChange = (e) => {
|
|
75
|
+
const detail = e.detail;
|
|
76
|
+
const page = detail?.page ?? 1;
|
|
77
|
+
if (page === this._currentPage)
|
|
78
|
+
return;
|
|
79
|
+
this._currentPage = page;
|
|
80
|
+
this.dispatchEvent(new CustomEvent("xm-data-table-page-change", {
|
|
81
|
+
detail: { page },
|
|
82
|
+
bubbles: true,
|
|
83
|
+
composed: true,
|
|
84
|
+
}));
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
willUpdate(changed) {
|
|
88
|
+
// Allow declarative JSON in attributes for static HTML authoring (mirrors
|
|
89
|
+
// xm-table); the @property is attribute:false, so set .columns/.rows as a
|
|
90
|
+
// string and we parse it here. A malformed string must not throw out of the
|
|
91
|
+
// reactive update and break render — keep the prior value on a parse error.
|
|
92
|
+
if (changed.has("columns") && typeof this.columns === "string") {
|
|
93
|
+
try {
|
|
94
|
+
this.columns = JSON.parse(this.columns);
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
this.columns = changed.get("columns") ?? [];
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
if (changed.has("rows") && typeof this.rows === "string") {
|
|
101
|
+
try {
|
|
102
|
+
this.rows = JSON.parse(this.rows);
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
this.rows = changed.get("rows") ?? [];
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
// Re-clamp the active page whenever the dataset or page size shrinks so the
|
|
109
|
+
// current slice is never out of bounds (e.g. after a filter swaps `rows`).
|
|
110
|
+
// The clamp moves the page silently from the consumer's view, so emit a
|
|
111
|
+
// page-change so a server-mode consumer can reconcile (uncontrolled-out).
|
|
112
|
+
if (changed.has("rows") || changed.has("pageSize")) {
|
|
113
|
+
const pages = this._pageCount();
|
|
114
|
+
const clamped = Math.min(Math.max(this._currentPage, 1), pages);
|
|
115
|
+
if (clamped !== this._currentPage) {
|
|
116
|
+
this._currentPage = clamped;
|
|
117
|
+
this.dispatchEvent(new CustomEvent("xm-data-table-page-change", {
|
|
118
|
+
detail: { page: clamped },
|
|
119
|
+
bubbles: true,
|
|
120
|
+
composed: true,
|
|
121
|
+
}));
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
render() {
|
|
126
|
+
return html `
|
|
127
|
+
<link rel="stylesheet" href="${PRIMITIVES_CSS}" />
|
|
128
|
+
<link rel="stylesheet" href="${DATA_TABLE_CSS}" />
|
|
129
|
+
<style>
|
|
130
|
+
:host {
|
|
131
|
+
display: block;
|
|
132
|
+
}
|
|
133
|
+
:host([hidden]) {
|
|
134
|
+
display: none;
|
|
135
|
+
}
|
|
136
|
+
</style>
|
|
137
|
+
${this._renderTable()} ${this._renderFooter()}
|
|
138
|
+
`;
|
|
139
|
+
}
|
|
140
|
+
// ── Body-state resolver (FR-161): loading wins, else empty, else paged data.
|
|
141
|
+
_renderTable() {
|
|
142
|
+
return html `
|
|
143
|
+
<table class="data-table" aria-busy=${this.loading ? "true" : nothing}>
|
|
144
|
+
<thead class="data-table__head">
|
|
145
|
+
<tr class="data-table__row">
|
|
146
|
+
${this.columns.map((col) => this._renderHeaderCell(col))}
|
|
147
|
+
</tr>
|
|
148
|
+
</thead>
|
|
149
|
+
<tbody>
|
|
150
|
+
${this.loading
|
|
151
|
+
? this._renderSkeletonRows()
|
|
152
|
+
: this._isEmpty()
|
|
153
|
+
? this._renderEmptyRow()
|
|
154
|
+
: this._renderDataRows()}
|
|
155
|
+
</tbody>
|
|
156
|
+
</table>
|
|
157
|
+
`;
|
|
158
|
+
}
|
|
159
|
+
_renderHeaderCell(col) {
|
|
160
|
+
const numeric = this._isNumericCol(col);
|
|
161
|
+
const sortable = Boolean(col.sortable) && !this.loading;
|
|
162
|
+
const active = this._sortKey === col.key;
|
|
163
|
+
const cls = [
|
|
164
|
+
"data-table__header-cell",
|
|
165
|
+
numeric && "data-table__header-cell--end",
|
|
166
|
+
sortable && "data-table__header-cell--sortable",
|
|
167
|
+
sortable && active && "data-table__header-cell--active",
|
|
168
|
+
]
|
|
169
|
+
.filter(Boolean)
|
|
170
|
+
.join(" ");
|
|
171
|
+
const ariaSort = !active
|
|
172
|
+
? "none"
|
|
173
|
+
: this._sortDirection === "asc"
|
|
174
|
+
? "ascending"
|
|
175
|
+
: "descending";
|
|
176
|
+
if (!sortable) {
|
|
177
|
+
// No aria-sort on a non-sortable header — aria-sort="none" would tell AT
|
|
178
|
+
// the column IS sortable but currently unsorted, which is misleading.
|
|
179
|
+
return html `<th class="${cls}" scope="col">
|
|
180
|
+
<span class="data-table__header-label">${col.label}</span>
|
|
181
|
+
</th>`;
|
|
182
|
+
}
|
|
183
|
+
return html `<th class="${cls}" scope="col" aria-sort=${ariaSort}>
|
|
184
|
+
<span
|
|
185
|
+
class="data-table__sort"
|
|
186
|
+
role="button"
|
|
187
|
+
tabindex="0"
|
|
188
|
+
@click=${() => this._toggleSort(col)}
|
|
189
|
+
@keydown=${(e) => this._onHeaderKey(e, col)}
|
|
190
|
+
>
|
|
191
|
+
<span class="data-table__header-label">${col.label}</span>
|
|
192
|
+
${active
|
|
193
|
+
? html `<span class="data-table__sort-icon" aria-hidden="true">
|
|
194
|
+
${this._sortDirection === "asc"
|
|
195
|
+
? html `<xm-sort-asc-icon size="14"></xm-sort-asc-icon>`
|
|
196
|
+
: html `<xm-sort-desc-icon size="14"></xm-sort-desc-icon>`}
|
|
197
|
+
</span>`
|
|
198
|
+
: nothing}
|
|
199
|
+
</span>
|
|
200
|
+
</th>`;
|
|
201
|
+
}
|
|
202
|
+
_renderDataRows() {
|
|
203
|
+
const slice = this._pagedRows();
|
|
204
|
+
return html `${slice.map((row) => html `
|
|
205
|
+
<tr class="data-table__row">
|
|
206
|
+
${this.columns.map((col) => {
|
|
207
|
+
const numeric = this._isNumericCol(col);
|
|
208
|
+
const cls = [
|
|
209
|
+
"data-table__cell",
|
|
210
|
+
numeric && "data-table__cell--end",
|
|
211
|
+
col.muted && "data-table__cell--muted",
|
|
212
|
+
]
|
|
213
|
+
.filter(Boolean)
|
|
214
|
+
.join(" ");
|
|
215
|
+
return html `<td class="${cls}">${row[col.key] ?? ""}</td>`;
|
|
216
|
+
})}
|
|
217
|
+
</tr>
|
|
218
|
+
`)}`;
|
|
219
|
+
}
|
|
220
|
+
// Loading: one skeleton row per page-size, each cell a composed <xm-skeleton>
|
|
221
|
+
// line laid on the same column grid. The skeleton owns its gradient-free pulse;
|
|
222
|
+
// the data-table only places the elements (AD-12).
|
|
223
|
+
_renderSkeletonRows() {
|
|
224
|
+
const rowCount = Math.max(1, Math.min(this._effectivePageSize(), 8));
|
|
225
|
+
const cols = this.columns.length || 1;
|
|
226
|
+
const rows = Array.from({ length: rowCount }, () => 0);
|
|
227
|
+
return html `${rows.map(() => html `
|
|
228
|
+
<tr class="data-table__row data-table__row--skeleton">
|
|
229
|
+
${Array.from({ length: cols }, () => 0).map(() => html `<td class="data-table__cell">
|
|
230
|
+
<xm-skeleton variant="line" width="72%"></xm-skeleton>
|
|
231
|
+
</td>`)}
|
|
232
|
+
</tr>
|
|
233
|
+
`)}`;
|
|
234
|
+
}
|
|
235
|
+
// Empty: a single full-width cell hosting the composed <xm-empty-state>. Never
|
|
236
|
+
// a blank box (FR-161). Consumers can override copy/action via the `empty` /
|
|
237
|
+
// `empty-action` slots without forking the component.
|
|
238
|
+
_renderEmptyRow() {
|
|
239
|
+
const span = this.columns.length || 1;
|
|
240
|
+
return html `
|
|
241
|
+
<tr class="data-table__row data-table__row--empty">
|
|
242
|
+
<td class="data-table__cell data-table__empty" colspan=${span}>
|
|
243
|
+
<slot name="empty">
|
|
244
|
+
<xm-empty-state heading=${this.emptyHeading} icon="xm-search-icon">
|
|
245
|
+
${this.emptyText
|
|
246
|
+
? html `${this.emptyText}`
|
|
247
|
+
: nothing}
|
|
248
|
+
<slot name="empty-action" slot="action"></slot>
|
|
249
|
+
</xm-empty-state>
|
|
250
|
+
</slot>
|
|
251
|
+
</td>
|
|
252
|
+
</tr>
|
|
253
|
+
`;
|
|
254
|
+
}
|
|
255
|
+
// Compose <xm-pagination> when there is more than one page (and not loading /
|
|
256
|
+
// empty). Re-surface its inner event as the table's own xm-data-table-page-change.
|
|
257
|
+
_renderFooter() {
|
|
258
|
+
if (this.loading || this._isEmpty())
|
|
259
|
+
return nothing;
|
|
260
|
+
const pages = this._pageCount();
|
|
261
|
+
if (pages <= 1)
|
|
262
|
+
return nothing;
|
|
263
|
+
return html `
|
|
264
|
+
<div class="data-table__footer">
|
|
265
|
+
<xm-pagination
|
|
266
|
+
class="data-table__pagination"
|
|
267
|
+
page=${this._currentPage}
|
|
268
|
+
total=${pages}
|
|
269
|
+
@xm-pagination-page-change=${this._onPageChange}
|
|
270
|
+
></xm-pagination>
|
|
271
|
+
</div>
|
|
272
|
+
`;
|
|
273
|
+
}
|
|
274
|
+
// ── State helpers ───────────────────────────────────────────────
|
|
275
|
+
_isEmpty() {
|
|
276
|
+
return !this.loading && this.rows.length === 0;
|
|
277
|
+
}
|
|
278
|
+
_isNumericCol(col) {
|
|
279
|
+
if (col.align === "end")
|
|
280
|
+
return true;
|
|
281
|
+
if (col.align === "start")
|
|
282
|
+
return false;
|
|
283
|
+
return Boolean(col.numeric);
|
|
284
|
+
}
|
|
285
|
+
_effectivePageSize() {
|
|
286
|
+
const n = Math.floor(this.pageSize);
|
|
287
|
+
return Number.isFinite(n) && n > 0 ? n : DEFAULT_PAGE_SIZE;
|
|
288
|
+
}
|
|
289
|
+
_pageCount() {
|
|
290
|
+
return Math.max(1, Math.ceil(this.rows.length / this._effectivePageSize()));
|
|
291
|
+
}
|
|
292
|
+
// Sort the FULL dataset (non-destructive copy), THEN slice the active page.
|
|
293
|
+
_sortedRows() {
|
|
294
|
+
if (this._sortKey === null)
|
|
295
|
+
return this.rows;
|
|
296
|
+
const key = this._sortKey;
|
|
297
|
+
const col = this.columns.find((c) => c.key === key);
|
|
298
|
+
const dir = this._sortDirection === "asc" ? 1 : -1;
|
|
299
|
+
// Use the column's sortValue accessor when provided (the only correct way to
|
|
300
|
+
// sort a formatted string like "412 KB"); otherwise compare the raw cell.
|
|
301
|
+
const valueOf = col?.sortValue
|
|
302
|
+
? (row) => col.sortValue(row)
|
|
303
|
+
: (row) => row[key];
|
|
304
|
+
return [...this.rows].sort((a, b) => this._compare(valueOf(a), valueOf(b)) * dir);
|
|
305
|
+
}
|
|
306
|
+
_pagedRows() {
|
|
307
|
+
const size = this._effectivePageSize();
|
|
308
|
+
const start = (this._currentPage - 1) * size;
|
|
309
|
+
return this._sortedRows().slice(start, start + size);
|
|
310
|
+
}
|
|
311
|
+
// Comparison: real numbers compare by magnitude; everything else compares as
|
|
312
|
+
// text with locale-numeric collation (so "item 2" < "item 10"). A formatted
|
|
313
|
+
// string like "412 KB" is NOT reverse-engineered into a number — that is the
|
|
314
|
+
// column's `sortValue` accessor's job (a unit-blind parse mis-orders "88 MB"
|
|
315
|
+
// vs "412 KB"). Nullish/empty values sort to the end of an ascending order.
|
|
316
|
+
_compare(a, b) {
|
|
317
|
+
const aNil = a === null || a === undefined || a === "";
|
|
318
|
+
const bNil = b === null || b === undefined || b === "";
|
|
319
|
+
if (aNil && bNil)
|
|
320
|
+
return 0;
|
|
321
|
+
if (aNil)
|
|
322
|
+
return 1;
|
|
323
|
+
if (bNil)
|
|
324
|
+
return -1;
|
|
325
|
+
if (typeof a === "number" && typeof b === "number")
|
|
326
|
+
return a - b;
|
|
327
|
+
return String(a).localeCompare(String(b), undefined, {
|
|
328
|
+
numeric: true,
|
|
329
|
+
sensitivity: "base",
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
// ── Interaction ─────────────────────────────────────────────────
|
|
333
|
+
_toggleSort(col) {
|
|
334
|
+
if (!col.sortable)
|
|
335
|
+
return;
|
|
336
|
+
if (this._sortKey === col.key) {
|
|
337
|
+
this._sortDirection = this._sortDirection === "asc" ? "desc" : "asc";
|
|
338
|
+
}
|
|
339
|
+
else {
|
|
340
|
+
this._sortKey = col.key;
|
|
341
|
+
this._sortDirection = "asc";
|
|
342
|
+
}
|
|
343
|
+
const detail = {
|
|
344
|
+
key: this._sortKey,
|
|
345
|
+
direction: this._sortDirection,
|
|
346
|
+
};
|
|
347
|
+
this.dispatchEvent(new CustomEvent("xm-data-table-sort-change", {
|
|
348
|
+
detail,
|
|
349
|
+
bubbles: true,
|
|
350
|
+
composed: true,
|
|
351
|
+
}));
|
|
352
|
+
}
|
|
353
|
+
_onHeaderKey(e, col) {
|
|
354
|
+
if (e.key === "Enter" || e.key === " " || e.key === "Spacebar") {
|
|
355
|
+
e.preventDefault();
|
|
356
|
+
this._toggleSort(col);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
};
|
|
360
|
+
__decorate([
|
|
361
|
+
property({ attribute: false })
|
|
362
|
+
], XmDataTable.prototype, "columns", void 0);
|
|
363
|
+
__decorate([
|
|
364
|
+
property({ attribute: false })
|
|
365
|
+
], XmDataTable.prototype, "rows", void 0);
|
|
366
|
+
__decorate([
|
|
367
|
+
property({ type: Boolean, reflect: true })
|
|
368
|
+
], XmDataTable.prototype, "loading", void 0);
|
|
369
|
+
__decorate([
|
|
370
|
+
property({ type: Number, attribute: "page-size" })
|
|
371
|
+
], XmDataTable.prototype, "pageSize", void 0);
|
|
372
|
+
__decorate([
|
|
373
|
+
property({ type: String, attribute: "empty-heading" })
|
|
374
|
+
], XmDataTable.prototype, "emptyHeading", void 0);
|
|
375
|
+
__decorate([
|
|
376
|
+
property({ type: String, attribute: "empty-text" })
|
|
377
|
+
], XmDataTable.prototype, "emptyText", void 0);
|
|
378
|
+
__decorate([
|
|
379
|
+
state()
|
|
380
|
+
], XmDataTable.prototype, "_sortKey", void 0);
|
|
381
|
+
__decorate([
|
|
382
|
+
state()
|
|
383
|
+
], XmDataTable.prototype, "_sortDirection", void 0);
|
|
384
|
+
__decorate([
|
|
385
|
+
state()
|
|
386
|
+
], XmDataTable.prototype, "_currentPage", void 0);
|
|
387
|
+
XmDataTable = __decorate([
|
|
388
|
+
customElement("xm-data-table")
|
|
389
|
+
], XmDataTable);
|
|
390
|
+
export { XmDataTable };
|