@alacris/ui 0.0.0 → 0.1.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.
Files changed (94) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +148 -1
  3. package/package.json +75 -3
  4. package/src/components/base.js +46 -0
  5. package/src/components/ui-accordion-item.js +157 -0
  6. package/src/components/ui-accordion.js +58 -0
  7. package/src/components/ui-alert.js +168 -0
  8. package/src/components/ui-app-bar.js +119 -0
  9. package/src/components/ui-autocomplete.js +371 -0
  10. package/src/components/ui-avatar.js +104 -0
  11. package/src/components/ui-backdrop.js +66 -0
  12. package/src/components/ui-badge.js +95 -0
  13. package/src/components/ui-bottom-app-bar.js +78 -0
  14. package/src/components/ui-bottom-nav.js +83 -0
  15. package/src/components/ui-breadcrumbs.js +100 -0
  16. package/src/components/ui-button-group.js +49 -0
  17. package/src/components/ui-button.js +143 -0
  18. package/src/components/ui-card.js +86 -0
  19. package/src/components/ui-carousel-item.js +53 -0
  20. package/src/components/ui-carousel.js +301 -0
  21. package/src/components/ui-checkbox.js +165 -0
  22. package/src/components/ui-chip-set.js +90 -0
  23. package/src/components/ui-chip.js +226 -0
  24. package/src/components/ui-container.js +59 -0
  25. package/src/components/ui-date-picker.js +774 -0
  26. package/src/components/ui-dialog.js +179 -0
  27. package/src/components/ui-divider.js +58 -0
  28. package/src/components/ui-drawer.js +169 -0
  29. package/src/components/ui-fab-menu.js +122 -0
  30. package/src/components/ui-fab.js +117 -0
  31. package/src/components/ui-icon-button.js +112 -0
  32. package/src/components/ui-icon.js +64 -0
  33. package/src/components/ui-list-item.js +166 -0
  34. package/src/components/ui-list.js +38 -0
  35. package/src/components/ui-loading-indicator.js +74 -0
  36. package/src/components/ui-menu-item.js +117 -0
  37. package/src/components/ui-menu.js +192 -0
  38. package/src/components/ui-nav-item.js +138 -0
  39. package/src/components/ui-nav-rail.js +111 -0
  40. package/src/components/ui-option.js +85 -0
  41. package/src/components/ui-pagination.js +173 -0
  42. package/src/components/ui-progress.js +100 -0
  43. package/src/components/ui-radio-group.js +104 -0
  44. package/src/components/ui-radio.js +143 -0
  45. package/src/components/ui-rating.js +126 -0
  46. package/src/components/ui-search.js +318 -0
  47. package/src/components/ui-select.js +418 -0
  48. package/src/components/ui-sheet.js +205 -0
  49. package/src/components/ui-side-sheet.js +221 -0
  50. package/src/components/ui-skeleton.js +89 -0
  51. package/src/components/ui-slider.js +266 -0
  52. package/src/components/ui-snackbar.js +233 -0
  53. package/src/components/ui-spinner.js +101 -0
  54. package/src/components/ui-split-button.js +158 -0
  55. package/src/components/ui-stack.js +37 -0
  56. package/src/components/ui-step.js +91 -0
  57. package/src/components/ui-stepper.js +86 -0
  58. package/src/components/ui-surface.js +51 -0
  59. package/src/components/ui-switch.js +153 -0
  60. package/src/components/ui-tab-panel.js +54 -0
  61. package/src/components/ui-tab.js +116 -0
  62. package/src/components/ui-table-footer.js +141 -0
  63. package/src/components/ui-table-toolbar.js +219 -0
  64. package/src/components/ui-table.js +735 -0
  65. package/src/components/ui-tabs.js +178 -0
  66. package/src/components/ui-text-field.js +319 -0
  67. package/src/components/ui-text.js +36 -0
  68. package/src/components/ui-time-picker.js +753 -0
  69. package/src/components/ui-toggle-button.js +170 -0
  70. package/src/components/ui-toggle-group.js +111 -0
  71. package/src/components/ui-toolbar.js +64 -0
  72. package/src/components/ui-tooltip.js +148 -0
  73. package/src/index.js +121 -0
  74. package/src/motion/animate.js +145 -0
  75. package/src/motion/flip.js +46 -0
  76. package/src/motion/index.js +4 -0
  77. package/src/motion/presence.js +104 -0
  78. package/src/motion/ripple.js +108 -0
  79. package/src/theme/apply-theme.js +142 -0
  80. package/src/theme/create-theme.js +70 -0
  81. package/src/theme/index.js +5 -0
  82. package/src/tokens/color.js +237 -0
  83. package/src/tokens/index.js +14 -0
  84. package/src/tokens/sys.js +76 -0
  85. package/src/tokens/system.js +114 -0
  86. package/src/tokens/typography.js +189 -0
  87. package/src/util/focus.js +89 -0
  88. package/src/util/form.js +69 -0
  89. package/src/util/icons.js +161 -0
  90. package/src/util/keys.js +110 -0
  91. package/src/util/position.js +102 -0
  92. package/src/util/table.js +230 -0
  93. package/types/elements.d.ts +78 -0
  94. package/types/index.d.ts +168 -0
@@ -0,0 +1,735 @@
1
+ // <ui-table> — Material data-table / DataGrid.
2
+ //
3
+ // Markup mode — pass a native <table> (adopted into the shadow so cells can
4
+ // be themed). `data-sortable` / `data-numeric` decorate headers.
5
+ //
6
+ // <ui-table label="Nutrition">
7
+ // <table>…</table>
8
+ // </ui-table>
9
+ //
10
+ // Data mode — `columns` + `rows`. The grid is `role="table"` (Alacris cannot
11
+ // bind inside native <tr>). Pipeline (filter → sort → group → paginate →
12
+ // aggregate) lives in `util/table.js`. Chrome is <ui-table-toolbar> and
13
+ // <ui-table-footer>, composed from search, menus, checkboxes, and pagination.
14
+ //
15
+ // <ui-table headline="Trades" columns=${cols} rows=${rows}
16
+ // selectable="multiple" group-by="commodity"
17
+ // quick-filter column-menu density-menu csv-export
18
+ // page-size="10"></ui-table>
19
+ //
20
+ // Column objects: { key, label, numeric, sortable, width, hidden, align,
21
+ // aggregate: 'sum'|'avg'|'min'|'max'|'count', render(row, col), sortValue(row) }
22
+ //
23
+ // @prop {string} label=''
24
+ // @prop {string} headline=''
25
+ // @prop {string} supporting=''
26
+ // @prop {string} variant='outlined' — outlined | standard
27
+ // @prop {string} density='standard' — compact | standard | comfortable
28
+ // @prop {boolean} dense=false — alias for density="compact"
29
+ // @prop {boolean} stickyHeader=false
30
+ // @prop {boolean} stickyFirst=false
31
+ // @prop {boolean} striped=false
32
+ // @prop {boolean} loading=false
33
+ // @prop {string} maxHeight=''
34
+ // @prop {string} selectable='none' — none | single | multiple
35
+ // @prop {Array} selected=[]
36
+ // @prop {Array} columns=[]
37
+ // @prop {Array} rows=[]
38
+ // @prop {object} getRowId=null — (row, index) => id
39
+ // @prop {string} sortBy=''
40
+ // @prop {string} sortDir='asc' — asc | desc
41
+ // @prop {string} sortMode='client' — client | server
42
+ // @prop {string} filter=''
43
+ // @prop {string} groupBy='' — column key to group rows
44
+ // @prop {Array} expandedGroups=[] — empty means all groups expanded
45
+ // @prop {Array} hiddenColumns=[]
46
+ // @prop {number} page=1
47
+ // @prop {number} pageSize=0 — 0 shows every row
48
+ // @prop {number} rowCount=0
49
+ // @prop {string} paginationMode='client' — client | server
50
+ // @prop {Array} pageSizeOptions=[]
51
+ // @prop {boolean} quickFilter=false
52
+ // @prop {boolean} columnMenu=false
53
+ // @prop {boolean} densityMenu=false
54
+ // @prop {boolean} csvExport=false
55
+ // @prop {string} csvFileName='table.csv'
56
+ // @prop {string} emptyText='No results'
57
+ // @event change — selection; detail: { selected }
58
+ // @event sort — detail: { key, dir }
59
+ // @event page — detail: { page, pageSize }
60
+ // @event filter — detail: { value }
61
+ // @event density — detail: { density }
62
+ // @event column-visibility — detail: { hidden }
63
+ // @event group — a group was toggled; detail: { key, expanded, expandedGroups }
64
+ // @event row-click — detail: { id, row }
65
+ // @event export — CSV produced; detail: { format, filename }
66
+ // @slot (default) — native <table> (markup mode)
67
+ // @slot toolbar — replaces the default <ui-table-toolbar>
68
+ // @slot headline, supporting, actions — projected into the default toolbar
69
+ // @slot footer — replaces the default <ui-table-footer>
70
+ // @slot empty
71
+ // @part container, table
72
+ // @vars see `t` below (`themeVars.names`)
73
+
74
+ import { define, html, css, vars, computed, effect, each, signal } from '@alacris/core';
75
+ import { sys } from '../tokens/sys.js';
76
+ import { base, focusRingOn } from './base.js';
77
+ import { ripple } from '../motion/ripple.js';
78
+ import {
79
+ processTable, makeIdOf, formatAgg, toCsv, downloadText, compare,
80
+ } from '../util/table.js';
81
+ import './ui-table-toolbar.js';
82
+ import './ui-table-footer.js';
83
+ import './ui-checkbox.js';
84
+ import './ui-icon.js';
85
+ import './ui-progress.js';
86
+
87
+ const t = vars('ui-table', {
88
+ rowHeight: '52px',
89
+ denseRowHeight: '40px',
90
+ comfortableRowHeight: '56px',
91
+ checkCol: '52px',
92
+ borderColor: sys.color.outlineVariant,
93
+ headerFg: sys.color.onSurfaceVariant,
94
+ headerBg: sys.color.surface,
95
+ fg: sys.color.onSurface,
96
+ bg: sys.color.surface,
97
+ hoverBg: sys.color.surfaceContainerLow,
98
+ selectedBg: `color-mix(in srgb, ${sys.color.primary} 8%, ${sys.color.surface})`,
99
+ stripeBg: sys.color.surfaceContainerLow,
100
+ groupBg: sys.color.surfaceContainerLow,
101
+ aggFg: sys.color.primary,
102
+ radius: sys.radius.md,
103
+ });
104
+
105
+ const styles = css`
106
+ :host { display: block; }
107
+ .root {
108
+ display: flex;
109
+ flex-direction: column;
110
+ position: relative;
111
+ background: ${t.bg};
112
+ color: ${t.fg};
113
+ }
114
+ .root.outlined {
115
+ border: 1px solid ${t.borderColor};
116
+ border-radius: ${t.radius};
117
+ overflow: hidden;
118
+ }
119
+ .loading {
120
+ position: absolute;
121
+ inset-inline: 0;
122
+ inset-block-start: 0;
123
+ z-index: 2;
124
+ }
125
+ .scroller { overflow: auto; max-block-size: var(--ui-table-max-height, none); }
126
+
127
+ /* Markup mode is a real <table>. Data mode is display:table — Alacris
128
+ cannot put bindings as children of <tr>/<tbody> (the parser drops them). */
129
+ table, .grid {
130
+ inline-size: 100%;
131
+ border-collapse: collapse;
132
+ color: ${t.fg};
133
+ }
134
+ .grid { display: table; }
135
+ .thead, .tbody, .tfoot { display: table-row-group; }
136
+ .tr { display: table-row; }
137
+ caption {
138
+ position: absolute;
139
+ inline-size: 1px; block-size: 1px; overflow: hidden;
140
+ clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap;
141
+ }
142
+ tr, .tr { block-size: calc(${t.rowHeight} + var(--ui-density, 0) * 4px); }
143
+ :host(.compact) tr, :host(.compact) .tr,
144
+ :host([dense]) tr, :host([dense]) .tr {
145
+ block-size: calc(${t.denseRowHeight} + var(--ui-density, 0) * 4px);
146
+ }
147
+ :host(.comfortable) tr, :host(.comfortable) .tr {
148
+ block-size: calc(${t.comfortableRowHeight} + var(--ui-density, 0) * 4px);
149
+ }
150
+ th, td, .th, .td {
151
+ display: table-cell;
152
+ padding-inline: ${sys.space(4)};
153
+ border-block-end: 1px solid ${t.borderColor};
154
+ white-space: nowrap;
155
+ vertical-align: middle;
156
+ }
157
+ tbody tr:last-child td, .tbody .tr:last-child .td { border-block-end: none; }
158
+ th, .th {
159
+ font: ${sys.type.labelLg};
160
+ letter-spacing: ${sys.tracking.labelLg};
161
+ color: ${t.headerFg};
162
+ text-align: start;
163
+ background: ${t.headerBg};
164
+ }
165
+ td, .td {
166
+ font: ${sys.type.bodyMd};
167
+ letter-spacing: ${sys.tracking.bodyMd};
168
+ }
169
+ .numeric, [data-numeric] { text-align: end; font-variant-numeric: tabular-nums; }
170
+ .center { text-align: center; }
171
+ .end { text-align: end; }
172
+ .agg-label {
173
+ display: block;
174
+ font: ${sys.type.labelSm};
175
+ letter-spacing: ${sys.tracking.labelSm};
176
+ color: ${sys.color.onSurfaceVariant};
177
+ text-transform: lowercase;
178
+ }
179
+
180
+ tbody tr, .tbody .tr {
181
+ transition: background-color ${sys.duration.short2} ${sys.easing.standard};
182
+ }
183
+ tbody tr:hover, .tbody .tr:hover { background: ${t.hoverBg}; }
184
+ tbody tr[aria-selected="true"], .tbody .tr[aria-selected="true"] { background: ${t.selectedBg}; }
185
+ tbody tr[aria-selected="true"]:hover, .tbody .tr[aria-selected="true"]:hover {
186
+ background: color-mix(in srgb, ${sys.color.primary} 12%, ${sys.color.surface});
187
+ }
188
+ :host([striped]) tbody tr:nth-child(even):not([aria-selected="true"]):not(:hover),
189
+ :host([striped]) .tbody .tr:nth-child(even):not(.group):not(.total):not([aria-selected="true"]):not(:hover) {
190
+ background: ${t.stripeBg};
191
+ }
192
+ .tr.group { background: ${t.groupBg}; font: ${sys.type.labelLg}; }
193
+ .tr.group:hover { background: ${t.hoverBg}; }
194
+ .tr.total { font: ${sys.type.labelLg}; color: ${t.aggFg}; }
195
+ .agg { color: ${t.aggFg}; font-variant-numeric: tabular-nums; }
196
+
197
+ :host([sticky-header]) thead th, :host([sticky-header]) .thead .th {
198
+ position: sticky; inset-block-start: 0; z-index: 1; background: ${t.headerBg};
199
+ }
200
+ :host([sticky-first]) .pin {
201
+ position: sticky; inset-inline-start: 0; z-index: 1; background: ${t.headerBg};
202
+ }
203
+ :host([sticky-first]) .td.pin { background: ${t.bg}; }
204
+ :host([sticky-first]) .tbody .tr:hover .td.pin { background: ${t.hoverBg}; }
205
+ :host([sticky-first]) .tbody .tr[aria-selected="true"] .td.pin { background: ${t.selectedBg}; }
206
+ :host([sticky-first]) .pin-data { inset-inline-start: ${t.checkCol}; }
207
+ :host([sticky-first]) .pin { box-shadow: 4px 0 8px rgb(var(--ui-shadow-rgb) / 0.08); }
208
+
209
+ .check {
210
+ inline-size: ${t.checkCol};
211
+ min-inline-size: ${t.checkCol};
212
+ padding-inline: ${sys.space(1)};
213
+ text-align: center;
214
+ }
215
+ .check ui-checkbox::part(label) {
216
+ position: absolute; inline-size: 1px; block-size: 1px; overflow: hidden;
217
+ clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap;
218
+ }
219
+
220
+ button.sort, button.group-toggle {
221
+ position: relative; isolation: isolate;
222
+ display: inline-flex; align-items: center; gap: ${sys.space(1)};
223
+ margin: 0; padding: ${sys.space(1)} ${sys.space(2)};
224
+ border: none; border-radius: ${sys.radius.xs};
225
+ background: transparent; color: inherit; font: inherit;
226
+ letter-spacing: inherit; cursor: pointer;
227
+ --ui-icon-size: 1.125rem;
228
+ }
229
+ button.sort { margin-inline: calc(${sys.space(2)} * -1); }
230
+ ${focusRingOn('button.sort')}
231
+ ${focusRingOn('button.group-toggle')}
232
+ button.sort .layer, button.group-toggle .layer {
233
+ position: absolute; inset: 0; z-index: -1;
234
+ border-radius: inherit; background: currentColor; opacity: 0;
235
+ transition: opacity ${sys.duration.short2} ${sys.easing.standard};
236
+ }
237
+ button.sort:hover .layer, button.group-toggle:hover .layer { opacity: ${sys.state.hover}; }
238
+ button.sort:focus-visible .layer, button.group-toggle:focus-visible .layer { opacity: ${sys.state.focus}; }
239
+ button.sort:active .layer, button.group-toggle:active .layer { opacity: ${sys.state.pressed}; }
240
+ button.sort ui-icon {
241
+ opacity: 0;
242
+ transition: opacity ${sys.duration.short2} ${sys.easing.standard},
243
+ transform ${sys.duration.short4} ${sys.easing.standard};
244
+ }
245
+ .th:hover button.sort ui-icon,
246
+ th:hover button.sort ui-icon,
247
+ button.sort:focus-visible ui-icon,
248
+ [aria-sort="ascending"] button.sort ui-icon,
249
+ [aria-sort="descending"] button.sort ui-icon { opacity: 1; }
250
+ [aria-sort="descending"] button.sort ui-icon { transform: rotate(180deg); }
251
+
252
+ .empty {
253
+ text-align: center; white-space: normal;
254
+ color: ${sys.color.onSurfaceVariant};
255
+ font: ${sys.type.bodyMd}; letter-spacing: ${sys.tracking.bodyMd};
256
+ padding-block: ${sys.space(6)};
257
+ }
258
+ .tr.empty-row, .tr.empty-row .empty { display: block; }
259
+ .tr.empty-row .empty { inline-size: 100%; }
260
+
261
+ :host([loading]) .scroller { pointer-events: none; opacity: 0.72; }
262
+ `;
263
+
264
+ const cellVal = (tr, idx) => {
265
+ const td = (tr.cells && tr.cells[idx]) || tr.children[idx];
266
+ if (!td) return '';
267
+ if (td.dataset.sort != null) return td.dataset.sort;
268
+ const text = td.textContent.trim();
269
+ const n = +text.replace(/,/g, '');
270
+ return text !== '' && !Number.isNaN(n) ? n : text;
271
+ };
272
+
273
+ const colAlign = (col) => {
274
+ if (col.numeric) return 'numeric';
275
+ if (col.align === 'center') return 'center';
276
+ if (col.align === 'end') return 'end';
277
+ return '';
278
+ };
279
+
280
+ define('ui-table', {
281
+ props: {
282
+ label: '',
283
+ headline: '',
284
+ supporting: '',
285
+ variant: 'outlined',
286
+ density: 'standard',
287
+ dense: false,
288
+ stickyHeader: false,
289
+ stickyFirst: false,
290
+ striped: false,
291
+ loading: false,
292
+ maxHeight: '',
293
+ selectable: 'none',
294
+ selected: [],
295
+ columns: [],
296
+ rows: [],
297
+ getRowId: null,
298
+ sortBy: '',
299
+ sortDir: 'asc',
300
+ sortMode: 'client',
301
+ filter: '',
302
+ groupBy: '',
303
+ expandedGroups: [],
304
+ hiddenColumns: [],
305
+ page: 1,
306
+ pageSize: 0,
307
+ rowCount: 0,
308
+ paginationMode: 'client',
309
+ pageSizeOptions: [],
310
+ quickFilter: false,
311
+ columnMenu: false,
312
+ densityMenu: false,
313
+ csvExport: false,
314
+ csvFileName: 'table.csv',
315
+ emptyText: 'No results',
316
+ },
317
+ styles: [base, styles],
318
+ setup(p, host) {
319
+ const {
320
+ label, headline, supporting, variant, density, dense, stickyHeader, stickyFirst,
321
+ striped, loading, maxHeight, selectable, selected, columns, rows, getRowId,
322
+ sortBy, sortDir, sortMode, filter, groupBy, expandedGroups, hiddenColumns,
323
+ page, pageSize, rowCount, paginationMode, pageSizeOptions,
324
+ quickFilter, columnMenu, densityMenu, csvExport, csvFileName, emptyText,
325
+ } = p;
326
+
327
+ const resolvedDensity = computed(() =>
328
+ dense() && density() === 'standard' ? 'compact' : (density() || 'standard'));
329
+
330
+ effect(() => host.toggleAttribute('dense', dense()));
331
+ effect(() => {
332
+ host.classList.toggle('compact', resolvedDensity() === 'compact');
333
+ host.classList.toggle('comfortable', resolvedDensity() === 'comfortable');
334
+ });
335
+ effect(() => host.toggleAttribute('sticky-header', stickyHeader()));
336
+ effect(() => host.toggleAttribute('sticky-first', stickyFirst()));
337
+ effect(() => host.toggleAttribute('striped', striped()));
338
+ effect(() => host.toggleAttribute('loading', loading()));
339
+ effect(() => host.setAttribute('variant', variant() || 'outlined'));
340
+ effect(() => {
341
+ const s = selectable() || 'none';
342
+ if (s === 'none') host.removeAttribute('selectable');
343
+ else host.setAttribute('selectable', s);
344
+ });
345
+ effect(() => {
346
+ if (maxHeight()) host.style.setProperty('--ui-table-max-height', maxHeight());
347
+ else host.style.removeProperty('--ui-table-max-height');
348
+ });
349
+
350
+ const idOf = makeIdOf(null);
351
+ const idOfLive = (row, i) => {
352
+ const fn = getRowId();
353
+ return typeof fn === 'function' ? fn(row, i) : idOf(row, i);
354
+ };
355
+
356
+ const dataMode = computed(() =>
357
+ (columns() && columns().length > 0) || (rows() && rows().length > 0));
358
+
359
+ const expanded = computed(() => {
360
+ const v = expandedGroups();
361
+ return v && v.length ? new Set(v) : null;
362
+ });
363
+
364
+ const model = computed(() => processTable({
365
+ rows: rows() || [],
366
+ columns: columns() || [],
367
+ hiddenColumns: hiddenColumns() || [],
368
+ filter: filter(),
369
+ sortBy: sortBy(),
370
+ sortDir: sortDir(),
371
+ sortMode: sortMode(),
372
+ groupBy: groupBy(),
373
+ expanded: expanded(),
374
+ page: page(),
375
+ pageSize: pageSize(),
376
+ paginationMode: paginationMode(),
377
+ rowCount: rowCount(),
378
+ idOf: idOfLive,
379
+ }));
380
+
381
+ const cols = computed(() => model().cols);
382
+ const visible = computed(() => model().visible);
383
+ const list = computed(() => model().list);
384
+ const totals = computed(() => model().totals);
385
+ const filteredCount = computed(() => model().filteredCount);
386
+ const hasTotals = computed(() => Object.keys(totals()).length > 0 && list().length > 0);
387
+
388
+ const multi = computed(() => selectable() === 'multiple');
389
+ const picking = computed(() => selectable() === 'multiple' || selectable() === 'single');
390
+ const selectedSet = computed(() => new Set(selected() || []));
391
+ const pageIds = computed(() =>
392
+ visible().filter((it) => it.type === 'row').map((it) => it.id));
393
+ const allOnPage = computed(() => {
394
+ const ids = pageIds();
395
+ return ids.length > 0 && ids.every((id) => selectedSet().has(id));
396
+ });
397
+ const someOnPage = computed(() => {
398
+ const ids = pageIds();
399
+ const n = ids.filter((id) => selectedSet().has(id)).length;
400
+ return n > 0 && n < ids.length;
401
+ });
402
+
403
+ const emitSelected = (next) => {
404
+ selected.set(next);
405
+ host.emit('change', { selected: next });
406
+ };
407
+ const toggleId = (id) => {
408
+ if (selectable() === 'single') {
409
+ emitSelected(selectedSet().has(id) ? [] : [id]);
410
+ return;
411
+ }
412
+ const next = new Set(selected() || []);
413
+ if (next.has(id)) next.delete(id); else next.add(id);
414
+ emitSelected([...next]);
415
+ };
416
+ const toggleAll = () => {
417
+ if (allOnPage()) {
418
+ const drop = new Set(pageIds());
419
+ emitSelected((selected() || []).filter((id) => !drop.has(id)));
420
+ } else {
421
+ const next = new Set(selected() || []);
422
+ for (const id of pageIds()) next.add(id);
423
+ emitSelected([...next]);
424
+ }
425
+ };
426
+
427
+ const applySort = (key) => {
428
+ let dir;
429
+ if (sortBy() !== key) dir = 'asc';
430
+ else if (sortDir() === 'asc') dir = 'desc';
431
+ else dir = 'none';
432
+ sortBy.set(dir === 'none' ? '' : key);
433
+ if (dir !== 'none') sortDir.set(dir);
434
+ host.emit('sort', { key: dir === 'none' ? '' : key, dir });
435
+ };
436
+
437
+ const toggleGroup = (key) => {
438
+ const groups = model().items.filter((it) => it.type === 'group').map((g) => g.key);
439
+ const current = expanded();
440
+ const set = new Set(current ? current : groups);
441
+ const open = !set.has(key);
442
+ if (open) set.add(key); else set.delete(key);
443
+ expandedGroups.set([...set]);
444
+ host.emit('group', { key, expanded: open, expandedGroups: [...set] });
445
+ };
446
+
447
+ const firstKey = computed(() => cols()[0]?.key);
448
+ const cellContent = (row, col) => {
449
+ if (typeof col.render === 'function') return col.render(row, col);
450
+ const v = row == null ? '' : row[col.key];
451
+ return v == null ? '' : v;
452
+ };
453
+ const rowName = (row) => {
454
+ const c = cols()[0];
455
+ if (!c) return String(idOfLive(row, 0));
456
+ const v = row == null ? '' : row[c.key];
457
+ return v == null || v === '' ? String(idOfLive(row, 0)) : String(v);
458
+ };
459
+
460
+ const pinClass = (col, pickingOn) => {
461
+ const pin = stickyFirst() && col.key === firstKey();
462
+ return pin ? (pickingOn ? 'pin pin-data' : 'pin') : '';
463
+ };
464
+
465
+ const adopted = signal(null);
466
+ const adopt = (slot) => {
467
+ if (dataMode.peek()) return;
468
+ const wrap = slot.parentNode;
469
+ for (const el of slot.assignedElements()) {
470
+ if (el.localName === 'table') {
471
+ wrap.append(el);
472
+ decorateMarkup(el);
473
+ adopted.set(el);
474
+ }
475
+ }
476
+ };
477
+
478
+ const decorateMarkup = (table) => {
479
+ table.querySelectorAll('th[data-numeric], td[data-numeric]').forEach((c) =>
480
+ c.classList.add('numeric'));
481
+ table.querySelectorAll('thead th[data-numeric], thead th.numeric').forEach((th) => {
482
+ const i = [...th.parentElement.children].indexOf(th);
483
+ for (const tr of table.querySelectorAll('tbody > tr')) {
484
+ tr.children[i]?.classList.add('numeric');
485
+ }
486
+ });
487
+ table.querySelectorAll('thead th[data-sortable]').forEach((th) => {
488
+ if (th.dataset.uiEnhanced) return;
489
+ th.dataset.uiEnhanced = '1';
490
+ const key = th.dataset.key || String([...th.parentElement.children].indexOf(th));
491
+ const name = th.textContent.trim();
492
+ const btn = document.createElement('button');
493
+ btn.type = 'button';
494
+ btn.className = 'sort';
495
+ btn.setAttribute('aria-label', `Sort by ${name}`);
496
+ while (th.firstChild) btn.append(th.firstChild);
497
+ const icon = document.createElement('ui-icon');
498
+ icon.setAttribute('name', 'arrow-upward');
499
+ btn.append(icon);
500
+ const layer = document.createElement('span');
501
+ layer.className = 'layer';
502
+ layer.setAttribute('aria-hidden', 'true');
503
+ btn.prepend(layer);
504
+ th.append(btn);
505
+ ripple(btn);
506
+ btn.addEventListener('click', () => {
507
+ applySort(key);
508
+ table.querySelectorAll('thead th[data-sortable]').forEach((h) => {
509
+ const k = h.dataset.key || String([...h.parentElement.children].indexOf(h));
510
+ if (k === sortBy()) h.setAttribute('aria-sort', sortDir() === 'desc' ? 'descending' : 'ascending');
511
+ else h.removeAttribute('aria-sort');
512
+ });
513
+ if (!sortBy() || sortMode() === 'server') return;
514
+ const tbody = table.querySelector('tbody');
515
+ if (!tbody) return;
516
+ const idx = [...th.parentElement.children].indexOf(th);
517
+ const dir = sortDir() === 'desc' ? -1 : 1;
518
+ const bodyRows = [...tbody.querySelectorAll('tr')];
519
+ bodyRows.sort((a, b) => dir * compare(cellVal(a, idx), cellVal(b, idx)));
520
+ for (const r of bodyRows) tbody.appendChild(r);
521
+ });
522
+ });
523
+ };
524
+
525
+ effect(() => {
526
+ const table = adopted();
527
+ if (!table) return;
528
+ const name = label();
529
+ if (name && !table.caption) table.setAttribute('aria-label', name);
530
+ else if (!name) table.removeAttribute('aria-label');
531
+ });
532
+
533
+ const tableAriaLabel = computed(() => {
534
+ const n = (selected() || []).length;
535
+ if (picking() && n) return `${n} selected`;
536
+ return headline() || label() || null;
537
+ });
538
+
539
+ const onRowClick = (e, row, id) => {
540
+ const path = e.composedPath();
541
+ if (path.some((n) => n.localName === 'button' || n.localName === 'a' ||
542
+ n.localName === 'input' || n.localName === 'ui-checkbox' ||
543
+ n.localName === 'ui-icon-button' || n.localName === 'select')) {
544
+ return;
545
+ }
546
+ host.emit('row-click', { id, row });
547
+ if (picking()) toggleId(id);
548
+ };
549
+
550
+ const empty = computed(() => dataMode() && visible().length === 0 && !loading());
551
+ const showFooter = computed(() => dataMode() && (pageSize() > 0 || filteredCount() >= 0));
552
+
553
+ const headerCell = (col) => html`
554
+ <div role="columnheader"
555
+ class=${() => ['th', colAlign(col()), pinClass(col(), picking())].filter(Boolean).join(' ')}
556
+ style=${() => (col().width ? { width: col().width } : null)}
557
+ aria-sort=${() => {
558
+ const c = col();
559
+ if (!c.sortable) return null;
560
+ if (sortBy() !== c.key) return 'none';
561
+ return sortDir() === 'desc' ? 'descending' : 'ascending';
562
+ }}>
563
+ ${() => {
564
+ const c = col();
565
+ const labelEl = c.sortable
566
+ ? html`<button type="button" class="sort"
567
+ aria-label=${() => sortBy() !== c.key
568
+ ? `Sort by ${c.label}`
569
+ : `${c.label}, sorted ${sortDir() === 'desc' ? 'descending' : 'ascending'}`}
570
+ @click=${() => applySort(c.key)}
571
+ ref=${(el) => ripple(el)}>
572
+ <span class="layer" aria-hidden="true"></span>
573
+ ${c.label}
574
+ <ui-icon name="arrow-upward"></ui-icon>
575
+ </button>`
576
+ : c.label;
577
+ return c.aggregate
578
+ ? html`<div>${labelEl}<span class="agg-label">${c.aggregate}</span></div>`
579
+ : labelEl;
580
+ }}
581
+ </div>`;
582
+
583
+ const bodyCell = (item, col) => {
584
+ const c = col();
585
+ const it = item();
586
+ let content;
587
+ let agg = false;
588
+ if (it.type === 'group') {
589
+ if (c.key === groupBy()) {
590
+ content = html`
591
+ <button type="button" class="group-toggle"
592
+ aria-expanded=${String(it.expanded)}
593
+ aria-label=${`${it.expanded ? 'Collapse' : 'Expand'} ${it.label}`}
594
+ @click=${(e) => { e.stopPropagation(); toggleGroup(it.key); }}
595
+ ref=${(el) => ripple(el)}>
596
+ <span class="layer" aria-hidden="true"></span>
597
+ <ui-icon name=${it.expanded ? 'expand-more' : 'chevron-right'}></ui-icon>
598
+ ${it.label} (${it.count})
599
+ </button>`;
600
+ } else if (c.aggregate) {
601
+ content = formatAgg(it.aggregates[c.key]);
602
+ agg = true;
603
+ } else content = '';
604
+ } else {
605
+ content = cellContent(it.row, c);
606
+ }
607
+ return html`
608
+ <div role=${it.type === 'group' ? 'rowheader' : 'cell'}
609
+ class=${() => ['td', colAlign(c), pinClass(c, picking()), agg ? 'agg' : '', it.depth ? 'nested' : '']
610
+ .filter(Boolean).join(' ')}>
611
+ ${content}
612
+ </div>`;
613
+ };
614
+
615
+ const dataTable = () => html`
616
+ <div class="grid" part="table" role="table"
617
+ aria-label=${tableAriaLabel}
618
+ aria-busy=${() => (loading() ? 'true' : null)}
619
+ aria-rowcount=${() => (pageSize() ? 1 + filteredCount() + (hasTotals() ? 1 : 0) : null)}
620
+ aria-colcount=${() => cols().length + (picking() ? 1 : 0)}>
621
+ <div class="thead" role="rowgroup">
622
+ <div class="tr" role="row" aria-rowindex="1">
623
+ ${() => (picking() ? html`
624
+ <div class=${() => (stickyFirst() ? 'th check pin' : 'th check')} role="columnheader">
625
+ ${() => (multi() ? html`
626
+ <ui-checkbox label="Select all rows on this page"
627
+ checked=${allOnPage} indeterminate=${someOnPage}
628
+ disabled=${loading}
629
+ @change=${(e) => { e.stopPropagation(); toggleAll(); }}>
630
+ </ui-checkbox>` : null)}
631
+ </div>` : null)}
632
+ ${each(() => cols(), headerCell, (c) => c.key)}
633
+ </div>
634
+ </div>
635
+ <div class="tbody" role="rowgroup">
636
+ ${() => (empty() ? html`
637
+ <div class="tr empty-row" role="row">
638
+ <div class="td empty" role="cell"><slot name="empty">${emptyText}</slot></div>
639
+ </div>` : null)}
640
+ ${each(
641
+ () => visible(),
642
+ (item) => html`
643
+ <div class=${() => `tr${item().type === 'group' ? ' group' : ''}`}
644
+ role="row"
645
+ aria-selected=${() => (item().type === 'row' && picking()
646
+ ? String(selectedSet().has(item().id)) : null)}
647
+ aria-expanded=${() => (item().type === 'group' ? String(item().expanded) : null)}
648
+ @click=${(e) => item().type === 'row' && onRowClick(e, item().row, item().id)}>
649
+ ${() => (picking() ? html`
650
+ <div class=${() => (stickyFirst() ? 'td check pin' : 'td check')} role="cell"
651
+ @click=${(e) => e.stopPropagation()}>
652
+ ${() => (item().type === 'row' ? html`
653
+ <ui-checkbox label=${() => `Select ${rowName(item().row)}`}
654
+ checked=${() => selectedSet().has(item().id)}
655
+ disabled=${loading}
656
+ @change=${(e) => { e.stopPropagation(); toggleId(item().id); }}>
657
+ </ui-checkbox>` : null)}
658
+ </div>` : null)}
659
+ ${each(() => cols(), (col) => bodyCell(item, col), (c) => c.key)}
660
+ </div>`,
661
+ (it) => String(it.id),
662
+ )}
663
+ </div>
664
+ ${() => (hasTotals() ? html`
665
+ <div class="tfoot" role="rowgroup">
666
+ <div class="tr total" role="row">
667
+ ${() => (picking() ? html`<div class="td check" role="cell"></div>` : null)}
668
+ ${each(() => cols(), (col) => html`
669
+ <div role="cell"
670
+ class=${() => ['td', colAlign(col()), col().aggregate ? 'agg' : ''].filter(Boolean).join(' ')}>
671
+ ${() => col().key === firstKey()
672
+ ? 'Total'
673
+ : (col().aggregate ? formatAgg(totals()[col().key]) : '')}
674
+ </div>`, (c) => c.key)}
675
+ </div>
676
+ </div>` : null)}
677
+ </div>`;
678
+
679
+ return html`
680
+ <div class=${() => `root ${variant() || 'outlined'}`} part="container">
681
+ <slot name="toolbar">
682
+ <ui-table-toolbar headline=${headline} supporting=${supporting}
683
+ selected-count=${() => (selected() || []).length}
684
+ filter=${filter}
685
+ quick-filter=${quickFilter}
686
+ column-menu=${columnMenu}
687
+ density-menu=${densityMenu}
688
+ csv-export=${csvExport}
689
+ columns=${() => (columns() || []).map((c) =>
690
+ typeof c === 'string' ? { key: c, label: c } : { key: c.key, label: c.label || c.key })}
691
+ hidden-columns=${hiddenColumns}
692
+ density=${resolvedDensity}
693
+ disabled=${loading}
694
+ @filter=${(e) => { e.stopPropagation(); filter.set(e.detail.value); host.emit('filter', e.detail); }}
695
+ @density=${(e) => { e.stopPropagation(); density.set(e.detail.density); dense.set(e.detail.density === 'compact'); host.emit('density', e.detail); }}
696
+ @column-visibility=${(e) => { e.stopPropagation(); hiddenColumns.set(e.detail.hidden); host.emit('column-visibility', e.detail); }}
697
+ @export=${(e) => {
698
+ e.stopPropagation();
699
+ const name = csvFileName() || 'table.csv';
700
+ downloadText(name, toCsv(list(), cols()));
701
+ host.emit('export', { format: 'csv', filename: name });
702
+ }}>
703
+ <slot name="headline" slot="headline"></slot>
704
+ <slot name="supporting" slot="supporting"></slot>
705
+ <slot name="actions" slot="actions"></slot>
706
+ </ui-table-toolbar>
707
+ </slot>
708
+ ${() => (loading() ? html`<ui-progress class="loading" label="Loading"></ui-progress>` : null)}
709
+ <div class="scroller">
710
+ ${() => (dataMode() ? dataTable() : html`
711
+ <slot ref=${(slot) => {
712
+ slot.addEventListener('slotchange', () => adopt(slot));
713
+ adopt(slot);
714
+ }}></slot>`)}
715
+ </div>
716
+ <slot name="footer">
717
+ ${() => (showFooter() ? html`
718
+ <ui-table-footer page=${page} page-size=${pageSize}
719
+ row-count=${filteredCount}
720
+ page-size-options=${pageSizeOptions}
721
+ disabled=${loading}
722
+ @page=${(e) => {
723
+ e.stopPropagation();
724
+ page.set(e.detail.page);
725
+ if (e.detail.pageSize) pageSize.set(e.detail.pageSize);
726
+ host.emit('page', e.detail);
727
+ }}>
728
+ </ui-table-footer>` : null)}
729
+ </slot>
730
+ </div>`;
731
+ },
732
+ });
733
+
734
+ export const tag = 'ui-table';
735
+ export const themeVars = t;