@hyperpackai/hyperui 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. package/README.md +13 -0
  2. package/dist/components/Accordion/index.d.ts +6 -0
  3. package/dist/components/Accordion/index.d.ts.map +1 -1
  4. package/dist/components/Accordion/index.js +65 -9
  5. package/dist/components/Autocomplete/index.d.ts +12 -2
  6. package/dist/components/Autocomplete/index.d.ts.map +1 -1
  7. package/dist/components/Autocomplete/index.js +148 -24
  8. package/dist/components/Backdrop/index.d.ts +2 -1
  9. package/dist/components/Backdrop/index.d.ts.map +1 -1
  10. package/dist/components/Backdrop/index.js +6 -3
  11. package/dist/components/Checkbox/index.d.ts +1 -0
  12. package/dist/components/Checkbox/index.d.ts.map +1 -1
  13. package/dist/components/Checkbox/index.js +6 -2
  14. package/dist/components/DashboardLayout/index.d.ts +13 -0
  15. package/dist/components/DashboardLayout/index.d.ts.map +1 -1
  16. package/dist/components/DashboardLayout/index.js +50 -7
  17. package/dist/components/DataTable/index.d.ts +43 -0
  18. package/dist/components/DataTable/index.d.ts.map +1 -1
  19. package/dist/components/DataTable/index.js +126 -21
  20. package/dist/components/Dialog/index.d.ts +9 -3
  21. package/dist/components/Dialog/index.d.ts.map +1 -1
  22. package/dist/components/Dialog/index.js +46 -30
  23. package/dist/components/Drawer/index.d.ts +11 -3
  24. package/dist/components/Drawer/index.d.ts.map +1 -1
  25. package/dist/components/Drawer/index.js +66 -11
  26. package/dist/components/DropdownMenu/index.d.ts +5 -3
  27. package/dist/components/DropdownMenu/index.d.ts.map +1 -1
  28. package/dist/components/DropdownMenu/index.js +56 -13
  29. package/dist/components/FocusTrap/index.d.ts.map +1 -1
  30. package/dist/components/FocusTrap/index.js +34 -32
  31. package/dist/components/Input/index.d.ts +2 -0
  32. package/dist/components/Input/index.d.ts.map +1 -1
  33. package/dist/components/Input/index.js +18 -4
  34. package/dist/components/Menu/index.d.ts +6 -2
  35. package/dist/components/Menu/index.d.ts.map +1 -1
  36. package/dist/components/Menu/index.js +50 -15
  37. package/dist/components/Modal/index.d.ts +3 -1
  38. package/dist/components/Modal/index.d.ts.map +1 -1
  39. package/dist/components/Modal/index.js +27 -9
  40. package/dist/components/NestedNavbar/index.d.ts +33 -0
  41. package/dist/components/NestedNavbar/index.d.ts.map +1 -0
  42. package/dist/components/NestedNavbar/index.js +435 -0
  43. package/dist/components/NestedSidebar/index.d.ts +48 -0
  44. package/dist/components/NestedSidebar/index.d.ts.map +1 -0
  45. package/dist/components/NestedSidebar/index.js +368 -0
  46. package/dist/components/Popover/index.d.ts +11 -3
  47. package/dist/components/Popover/index.d.ts.map +1 -1
  48. package/dist/components/Popover/index.js +45 -9
  49. package/dist/components/Radio/index.d.ts +26 -1
  50. package/dist/components/Radio/index.d.ts.map +1 -1
  51. package/dist/components/Radio/index.js +61 -2
  52. package/dist/components/Select/index.d.ts +5 -0
  53. package/dist/components/Select/index.d.ts.map +1 -1
  54. package/dist/components/Select/index.js +22 -5
  55. package/dist/components/Sheet/index.d.ts +9 -3
  56. package/dist/components/Sheet/index.d.ts.map +1 -1
  57. package/dist/components/Sheet/index.js +48 -23
  58. package/dist/components/Sidebar/index.d.ts +20 -1
  59. package/dist/components/Sidebar/index.d.ts.map +1 -1
  60. package/dist/components/Sidebar/index.js +285 -8
  61. package/dist/components/SpeedDial/index.d.ts +10 -0
  62. package/dist/components/SpeedDial/index.d.ts.map +1 -1
  63. package/dist/components/SpeedDial/index.js +61 -11
  64. package/dist/components/Switch/index.d.ts +2 -0
  65. package/dist/components/Switch/index.d.ts.map +1 -1
  66. package/dist/components/Switch/index.js +6 -2
  67. package/dist/components/Tabs/index.d.ts +3 -0
  68. package/dist/components/Tabs/index.d.ts.map +1 -1
  69. package/dist/components/Tabs/index.js +47 -8
  70. package/dist/components/TextField/index.d.ts +2 -0
  71. package/dist/components/TextField/index.d.ts.map +1 -1
  72. package/dist/components/TextField/index.js +12 -4
  73. package/dist/components/Textarea/index.d.ts +5 -0
  74. package/dist/components/Textarea/index.d.ts.map +1 -1
  75. package/dist/components/Textarea/index.js +21 -4
  76. package/dist/components/Transition/index.d.ts +14 -0
  77. package/dist/components/Transition/index.d.ts.map +1 -0
  78. package/dist/components/Transition/index.js +49 -0
  79. package/dist/components/TransitionGroup/index.d.ts +16 -0
  80. package/dist/components/TransitionGroup/index.d.ts.map +1 -0
  81. package/dist/components/TransitionGroup/index.js +95 -0
  82. package/dist/components/data.d.ts +81 -16
  83. package/dist/components/data.d.ts.map +1 -1
  84. package/dist/components/data.js +163 -31
  85. package/dist/components/enterprise.d.ts +85 -26
  86. package/dist/components/enterprise.d.ts.map +1 -1
  87. package/dist/components/enterprise.js +211 -36
  88. package/dist/components/index.d.ts +21 -13
  89. package/dist/components/index.d.ts.map +1 -1
  90. package/dist/components/index.js +7 -2
  91. package/dist/portal.d.ts.map +1 -1
  92. package/dist/portal.js +3 -0
  93. package/dist/theme/index.d.ts +5 -6
  94. package/dist/theme/index.d.ts.map +1 -1
  95. package/dist/theme/index.js +30 -0
  96. package/dist/tokens/index.d.ts.map +1 -1
  97. package/dist/tokens/index.js +11 -0
  98. package/package.json +6 -1
@@ -6,6 +6,18 @@ import { injectCSS, cn, h } from "../theme/index.js";
6
6
  import { Button } from "./Button/index.js";
7
7
  import { Input } from "./Input/index.js";
8
8
  import { Select } from "./Select/index.js";
9
+ function rootProps(props, className, extra) {
10
+ return {
11
+ ...extra,
12
+ id: props.id,
13
+ class: cn(className, props.class),
14
+ style: mergeStyle(props.style, extra?.style),
15
+ "data-testid": props.testId
16
+ };
17
+ }
18
+ function mergeStyle(base, extra) {
19
+ return [base, extra].filter(Boolean).join(";") || undefined;
20
+ }
9
21
  const DATA_CSS = `
10
22
  .hu-virtual-table {
11
23
  border: 1px solid var(--hu-border); border-radius: var(--hu-radius-md);
@@ -14,6 +26,11 @@ const DATA_CSS = `
14
26
  .hu-virtual-table__inner { position: relative; min-width: 100%; }
15
27
  .hu-virtual-table table { width: 100%; border-collapse: collapse; table-layout: fixed; }
16
28
  .hu-virtual-table thead { position: sticky; inset-block-start: 0; z-index: 1; background: var(--hu-bg-2); }
29
+ .hu-virtual-table--no-sticky thead { position: static; }
30
+ .hu-virtual-table__caption {
31
+ padding: var(--hu-space-2) var(--hu-space-3); color: var(--hu-text-2);
32
+ font-size: var(--hu-font-size-sm); text-align: start; caption-side: top;
33
+ }
17
34
  .hu-virtual-table th {
18
35
  padding: var(--hu-space-2) var(--hu-space-3); border-block-end: 1px solid var(--hu-border);
19
36
  color: var(--hu-text-2); font-size: var(--hu-font-size-xs); font-weight: var(--hu-font-weight-semibold);
@@ -23,7 +40,14 @@ const DATA_CSS = `
23
40
  padding: var(--hu-space-2) var(--hu-space-3); border-block-end: 1px solid var(--hu-border);
24
41
  color: var(--hu-text); font-size: var(--hu-font-size-sm); overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
25
42
  }
43
+ .hu-virtual-table--compact th,
44
+ .hu-virtual-table--compact td { padding: var(--hu-space-1) var(--hu-space-2); }
45
+ .hu-virtual-table--comfortable th,
46
+ .hu-virtual-table--comfortable td { padding: var(--hu-space-3) var(--hu-space-4); }
26
47
  .hu-virtual-table tr:hover td { background: var(--hu-bg-2); }
48
+ .hu-virtual-table tr[aria-selected="true"] td { background: var(--hu-primary-bg); }
49
+ .hu-virtual-table tr[aria-disabled="true"] td { color: var(--hu-text-3); cursor: not-allowed; opacity: .72; }
50
+ .hu-virtual-table tr[aria-disabled="true"]:hover td { background: transparent; }
27
51
  .hu-virtual-table__empty { padding: var(--hu-space-8); text-align: center; color: var(--hu-text-2); }
28
52
 
29
53
  .hu-advanced-filters {
@@ -59,6 +83,12 @@ const DATA_CSS = `
59
83
  border: 1px solid var(--hu-border); border-radius: var(--hu-radius-md);
60
84
  background: var(--hu-bg); padding: var(--hu-space-4);
61
85
  }
86
+ .hu-data-grid__header { display: flex; flex-direction: column; gap: var(--hu-space-1); }
87
+ .hu-data-grid__title {
88
+ margin: 0; color: var(--hu-text); font-size: var(--hu-font-size-lg);
89
+ font-weight: var(--hu-font-weight-semibold);
90
+ }
91
+ .hu-data-grid__description { margin: 0; color: var(--hu-text-2); font-size: var(--hu-font-size-sm); }
62
92
  .hu-data-grid__toolbar {
63
93
  display: flex; align-items: center; justify-content: space-between;
64
94
  gap: var(--hu-space-3); flex-wrap: wrap;
@@ -143,12 +173,21 @@ const DATA_CSS = `
143
173
  .hu-pivot-grid__meta { font-size: var(--hu-font-size-xs); color: var(--hu-text-2); }
144
174
  .hu-pivot-grid__table { overflow: auto; border: 1px solid var(--hu-border); border-radius: var(--hu-radius); }
145
175
  .hu-pivot-grid__table table { width: 100%; border-collapse: collapse; table-layout: fixed; }
176
+ .hu-pivot-grid__table--sticky thead { position: sticky; inset-block-start: 0; z-index: 1; }
177
+ .hu-pivot-grid__caption {
178
+ padding: var(--hu-space-2) var(--hu-space-3); color: var(--hu-text-2);
179
+ font-size: var(--hu-font-size-sm); text-align: start; caption-side: top;
180
+ }
146
181
  .hu-pivot-grid__table th,
147
182
  .hu-pivot-grid__table td {
148
183
  padding: var(--hu-space-2) var(--hu-space-3); border-block-end: 1px solid var(--hu-border);
149
184
  border-inline-end: 1px solid var(--hu-border); font-size: var(--hu-font-size-xs);
150
185
  color: var(--hu-text); text-align: end; white-space: nowrap;
151
186
  }
187
+ .hu-pivot-grid__table--compact th,
188
+ .hu-pivot-grid__table--compact td { padding: var(--hu-space-1) var(--hu-space-2); }
189
+ .hu-pivot-grid__table--comfortable th,
190
+ .hu-pivot-grid__table--comfortable td { padding: var(--hu-space-3) var(--hu-space-4); }
152
191
  .hu-pivot-grid__table th { background: var(--hu-bg-2); color: var(--hu-text-2); font-weight: var(--hu-font-weight-semibold); text-align: start; }
153
192
  .hu-pivot-grid__total { font-weight: var(--hu-font-weight-semibold); background: var(--hu-bg-2); }
154
193
  @media (max-width: 720px) {
@@ -168,14 +207,18 @@ export function VirtualTable(props) {
168
207
  const visibleRows = props.rows.slice(startIndex, startIndex + visibleCount);
169
208
  const totalHeight = props.rows.length * rowHeight;
170
209
  const offset = startIndex * rowHeight;
171
- return h("div", {
172
- class: cn("hu-virtual-table", props.class),
210
+ const selectedRows = props.selectedRows ? new Set(props.selectedRows) : undefined;
211
+ const stickyHeader = props.stickyHeader !== false;
212
+ return h("div", rootProps(props, cn("hu-virtual-table", props.density && props.density !== "default" && `hu-virtual-table--${props.density}`, !stickyHeader && "hu-virtual-table--no-sticky"), {
173
213
  style: props.height !== undefined ? `height:${toCssUnit(props.height)}` : undefined,
174
214
  role: "region",
175
- "aria-label": "Virtualized table"
176
- }, h("div", { class: "hu-virtual-table__inner", style: `height:${totalHeight}px` }, h("table", { role: "table", "aria-rowcount": props.rows.length }, h("thead", {}, h("tr", { role: "row" }, ...props.columns.map((column) => h("th", {
215
+ "aria-label": props["aria-label"] ?? "Virtualized table",
216
+ "aria-labelledby": props["aria-labelledby"]
217
+ }), h("div", { class: "hu-virtual-table__inner", style: `height:${totalHeight}px` }, h("table", { role: "table", "aria-rowcount": props.rows.length }, props.caption && h("caption", { class: "hu-virtual-table__caption" }, props.caption), h("thead", {}, h("tr", { role: "row" }, ...props.columns.map((column) => h("th", {
177
218
  key: column.key,
219
+ id: getColumnId(column),
178
220
  role: "columnheader",
221
+ "aria-label": column.headerLabel,
179
222
  style: [
180
223
  column.width !== undefined && `width:${toCssUnit(column.width)}`,
181
224
  column.align && `text-align:${column.align}`
@@ -185,15 +228,21 @@ export function VirtualTable(props) {
185
228
  : visibleRows.map((row, localIndex) => {
186
229
  const index = startIndex + localIndex;
187
230
  const key = props.rowKey?.(row, index) ?? String(row["id"] ?? index);
231
+ const disabled = props.isRowDisabled?.(row, index) ?? false;
188
232
  return h("tr", {
189
233
  key,
190
234
  role: "row",
191
235
  "aria-rowindex": index + 1,
236
+ "aria-label": props.getRowLabel?.(row, index),
237
+ "aria-selected": selectedRows?.has(key) ? "true" : undefined,
238
+ "aria-disabled": disabled ? "true" : undefined,
192
239
  style: `height:${rowHeight}px`,
193
- onClick: props.onRowClick ? () => props.onRowClick?.(row, index) : undefined
240
+ onClick: props.onRowClick && !disabled ? () => props.onRowClick?.(row, index) : undefined
194
241
  }, ...props.columns.map((column) => h("td", {
195
242
  key: column.key,
196
243
  role: "cell",
244
+ headers: getColumnId(column),
245
+ "aria-label": column.cellLabel?.(row, index),
197
246
  style: column.align ? `text-align:${column.align}` : undefined
198
247
  }, column.render ? column.render(row, index) : row[column.key])));
199
248
  })))));
@@ -214,7 +263,7 @@ export function AdvancedFilters(props) {
214
263
  injectCSS("hu-data", DATA_CSS);
215
264
  const fieldOptions = props.fields.map((field) => ({ value: field.key, label: field.label }));
216
265
  const operatorOptions = FILTER_OPERATORS.map((operator) => ({ value: operator.value, label: operator.label }));
217
- return h("section", { class: cn("hu-advanced-filters", props.class), "aria-label": props.title ?? "Advanced filters" }, h("div", { class: "hu-advanced-filters__header" }, h("div", { class: "hu-advanced-filters__title" }, props.title ?? "Advanced filters"), Button({ type: "button", variant: "secondary", size: "sm", children: "Add filter", onClick: props.onAddRule })), h("div", { class: "hu-advanced-filters__rules" }, (props.rules?.length ?? 0) === 0
266
+ return h("section", rootProps(props, "hu-advanced-filters", { "aria-label": props.title ?? "Advanced filters" }), h("div", { class: "hu-advanced-filters__header" }, h("div", { class: "hu-advanced-filters__title" }, props.title ?? "Advanced filters"), Button({ type: "button", variant: "secondary", size: "sm", children: "Add filter", onClick: props.onAddRule })), h("div", { class: "hu-advanced-filters__rules" }, (props.rules?.length ?? 0) === 0
218
267
  ? h("div", { class: "hu-advanced-filters__empty" }, "No filters applied.")
219
268
  : props.rules.map((rule) => h("div", { key: rule.id, class: "hu-advanced-filters__rule", role: "group", "aria-label": `Filter ${rule.id}` }, Select({
220
269
  label: "Field",
@@ -236,7 +285,7 @@ export function SearchBuilder(props) {
236
285
  injectCSS("hu-data", DATA_CSS);
237
286
  const tokens = props.tokens ?? [];
238
287
  const query = props.query ?? "";
239
- return h("section", { class: cn("hu-search-builder", props.class), "aria-label": "Search builder" }, h("div", { class: "hu-search-builder__bar", role: "search" }, h("div", { class: "hu-search-builder__query" }, Input({
288
+ return h("section", rootProps(props, "hu-search-builder", { "aria-label": "Search builder" }), h("div", { class: "hu-search-builder__bar", role: "search" }, h("div", { class: "hu-search-builder__query" }, Input({
240
289
  value: query,
241
290
  placeholder: props.placeholder ?? "Search…",
242
291
  onInput: (e) => props.onQueryChange?.(e.target.value)
@@ -260,14 +309,19 @@ export function DataGrid(props) {
260
309
  key: "__select",
261
310
  header: "",
262
311
  width: 44,
312
+ headerLabel: "Select rows",
263
313
  render: (row, index) => {
264
314
  const key = rowKey(row, index);
265
315
  const checked = selected.has(key);
316
+ const disabled = props.isRowDisabled?.(row, index) ?? false;
266
317
  return h("input", {
267
318
  type: "checkbox",
268
319
  checked,
320
+ disabled,
269
321
  "aria-label": `Select row ${index + 1}`,
270
322
  onChange: (e) => {
323
+ if (disabled)
324
+ return;
271
325
  const next = new Set(selected);
272
326
  if (e.target.checked)
273
327
  next.add(key);
@@ -280,11 +334,14 @@ export function DataGrid(props) {
280
334
  }
281
335
  : undefined;
282
336
  const tableColumns = selectionColumn ? [selectionColumn, ...visibleColumns] : visibleColumns;
283
- return h("section", { class: cn("hu-data-grid", props.class), "aria-label": "Data grid" }, h("div", { class: "hu-data-grid__toolbar" }, h("div", { class: "hu-data-grid__search" }, Input({
337
+ return h("section", rootProps(props, "hu-data-grid", {
338
+ "aria-label": props["aria-label"] ?? props.title ?? "Data grid",
339
+ "aria-labelledby": props["aria-labelledby"]
340
+ }), (props.title || props.description) && h("div", { class: "hu-data-grid__header" }, props.title && h("h2", { class: "hu-data-grid__title" }, props.title), props.description && h("p", { class: "hu-data-grid__description" }, props.description)), h("div", { class: "hu-data-grid__toolbar" }, h("div", { class: "hu-data-grid__search" }, Input({
284
341
  value: props.query ?? "",
285
- placeholder: "Search rows...",
342
+ placeholder: props.searchPlaceholder ?? "Search rows...",
286
343
  onInput: (e) => props.onQueryChange?.(e.target.value)
287
- })), h("div", { class: "hu-data-grid__actions" }, props.toolbarActions, props.filters && Button({ type: "button", variant: "secondary", size: "sm", children: "Add filter", onClick: props.onFilterAdd }))), props.showColumnControls && h("div", { class: "hu-data-grid__columns", "aria-label": "Column visibility" }, ...props.columns.map((column) => h("label", { class: "hu-data-grid__column-toggle" }, h("input", {
344
+ })), h("div", { class: "hu-data-grid__actions" }, props.toolbarActions, props.filters && Button({ type: "button", variant: "secondary", size: "sm", children: "Add filter", onClick: props.onFilterAdd }))), props.showColumnControls && h("div", { class: "hu-data-grid__columns", "aria-label": props.columnControlsLabel ?? "Column visibility" }, ...props.columns.map((column) => h("label", { class: "hu-data-grid__column-toggle" }, h("input", {
288
345
  type: "checkbox",
289
346
  checked: column.visible !== false,
290
347
  onChange: (e) => props.onColumnVisibilityChange?.(column.key, e.target.checked)
@@ -294,10 +351,18 @@ export function DataGrid(props) {
294
351
  onAddRule: props.onFilterAdd,
295
352
  onRemoveRule: props.onFilterRemove,
296
353
  onRuleChange: props.onFilterChange
297
- }), props.selectedRows && h("div", { class: "hu-data-grid__selection", "aria-live": "polite" }, `${props.selectedRows.length} selected`), VirtualTable({
354
+ }), props.selectedRows && h("div", { class: "hu-data-grid__selection", "aria-live": "polite" }, props.selectedRowsLabel?.(props.selectedRows.length) ?? `${props.selectedRows.length} selected`), VirtualTable({
298
355
  columns: tableColumns,
299
356
  rows: props.rows,
357
+ caption: props.caption,
358
+ "aria-label": props.tableLabel,
359
+ "aria-labelledby": props.tableLabelledBy,
360
+ density: props.density,
361
+ stickyHeader: props.stickyHeader,
300
362
  rowKey,
363
+ selectedRows: props.selectedRows,
364
+ getRowLabel: props.getRowLabel,
365
+ isRowDisabled: props.isRowDisabled,
301
366
  rowHeight: props.rowHeight,
302
367
  height: props.height,
303
368
  startIndex: props.visibleStartIndex,
@@ -324,8 +389,11 @@ export function DataExporter(props) {
324
389
  const format = props.format ?? "csv";
325
390
  const filename = props.filename ?? `export.${format}`;
326
391
  const selectedColumns = getSelectedExportColumns(props.columns, props.selectedColumns);
327
- return h("section", { class: cn("hu-data-transfer", props.class), "aria-label": props.title ?? "Data exporter" }, h("div", { class: "hu-data-transfer__header" }, h("div", {}, h("div", { class: "hu-data-transfer__title" }, props.title ?? "Export data"), h("div", { class: "hu-data-transfer__meta" }, props.description ?? `${props.rows.length} rows available`)), h("div", { class: "hu-data-transfer__meta", "aria-live": "polite" }, `${selectedColumns.length} columns selected`)), h("div", { class: "hu-data-transfer__controls" }, Select({
328
- label: "Format",
392
+ return h("section", rootProps(props, "hu-data-transfer", {
393
+ "aria-label": props["aria-label"] ?? props.title ?? "Data exporter",
394
+ "aria-labelledby": props["aria-labelledby"]
395
+ }), h("div", { class: "hu-data-transfer__header" }, h("div", {}, h("div", { class: "hu-data-transfer__title" }, props.title ?? "Export data"), h("div", { class: "hu-data-transfer__meta" }, props.description ?? `${props.rows.length} rows available`)), h("div", { class: "hu-data-transfer__meta", "aria-live": "polite" }, props.selectedColumnsLabel?.(selectedColumns.length) ?? `${selectedColumns.length} columns selected`)), h("div", { class: "hu-data-transfer__controls" }, Select({
396
+ label: props.formatLabel ?? "Format",
329
397
  value: format,
330
398
  options: [
331
399
  { label: "CSV", value: "csv" },
@@ -333,12 +401,12 @@ export function DataExporter(props) {
333
401
  ],
334
402
  onChange: (e) => props.onFormatChange?.(e.target.value)
335
403
  }), Input({
336
- label: "Filename",
404
+ label: props.filenameLabel ?? "Filename",
337
405
  value: filename,
338
406
  onInput: (e) => props.onFilenameChange?.(e.target.value)
339
407
  }), Button({
340
408
  type: "button",
341
- children: "Export",
409
+ children: props.exportLabel ?? "Export",
342
410
  disabled: selectedColumns.length === 0,
343
411
  onClick: () => props.onExport?.({
344
412
  format,
@@ -347,12 +415,18 @@ export function DataExporter(props) {
347
415
  columns: selectedColumns,
348
416
  content: serializeExport(props.rows, selectedColumns, format, props.includeHeaders !== false)
349
417
  })
350
- })), h("div", { class: "hu-data-transfer__columns", "aria-label": "Export columns" }, ...props.columns.map((column) => {
418
+ })), h("div", { class: "hu-data-transfer__columns", "aria-label": props.columnsLabel ?? "Export columns" }, ...props.columns.map((column) => {
351
419
  const checked = selectedColumns.some((selected) => selected.key === column.key);
352
420
  return h("label", { class: "hu-data-transfer__column" }, h("input", {
353
421
  type: "checkbox",
354
422
  checked,
355
- onChange: (e) => props.onColumnToggle?.(column.key, e.target.checked)
423
+ disabled: column.disabled,
424
+ "aria-label": column.headerLabel ?? `Export ${column.header}`,
425
+ onChange: (e) => {
426
+ if (column.disabled)
427
+ return;
428
+ props.onColumnToggle?.(column.key, e.target.checked);
429
+ }
356
430
  }), column.header);
357
431
  })));
358
432
  }
@@ -363,19 +437,23 @@ export function DataImporter(props) {
363
437
  const previewRows = props.previewRows ?? [];
364
438
  const columns = props.columns ?? inferImportColumns(previewRows);
365
439
  const status = props.status ?? (props.fileName ? "ready" : "idle");
366
- return h("section", { class: cn("hu-data-transfer", props.class), "aria-label": props.title ?? "Data importer" }, h("div", { class: "hu-data-transfer__header" }, h("div", {}, h("div", { class: "hu-data-transfer__title" }, props.title ?? "Import data"), h("div", { class: "hu-data-transfer__meta" }, props.description ?? "Upload a file, validate mapped columns, then import.")), h("div", { class: "hu-data-transfer__meta", "aria-live": "polite" }, `${previewRows.length} preview rows`)), h("div", { class: "hu-data-transfer__controls" }, Select({
367
- label: "Format",
440
+ return h("section", rootProps(props, "hu-data-transfer", {
441
+ "aria-label": props["aria-label"] ?? props.title ?? "Data importer",
442
+ "aria-labelledby": props["aria-labelledby"]
443
+ }), h("div", { class: "hu-data-transfer__header" }, h("div", {}, h("div", { class: "hu-data-transfer__title" }, props.title ?? "Import data"), h("div", { class: "hu-data-transfer__meta" }, props.description ?? "Upload a file, validate mapped columns, then import.")), h("div", { class: "hu-data-transfer__meta", "aria-live": "polite" }, props.previewRowsLabel?.(previewRows.length) ?? `${previewRows.length} preview rows`)), h("div", { class: "hu-data-transfer__controls" }, Select({
444
+ label: props.formatLabel ?? "Format",
368
445
  value: format,
369
446
  options: [
370
447
  { label: "CSV", value: "csv" },
371
448
  { label: "JSON", value: "json" }
372
449
  ],
373
450
  onChange: (e) => props.onFormatChange?.(e.target.value)
374
- }), h("label", { class: "hu-data-transfer__dropzone" }, h("span", { class: "hu-data-transfer__meta" }, props.fileName ?? "Choose import file"), h("input", {
451
+ }), h("label", { class: "hu-data-transfer__dropzone" }, h("span", { class: "hu-data-transfer__meta" }, props.fileName ?? props.fileLabel ?? "Choose import file"), h("input", {
375
452
  class: "hu-data-transfer__file",
376
453
  type: "file",
377
454
  accept,
378
455
  multiple: props.multiple,
456
+ "aria-label": props.fileLabel ?? "Choose import file",
379
457
  onChange: (e) => {
380
458
  const file = e.target.files?.[0];
381
459
  if (file)
@@ -383,10 +461,21 @@ export function DataImporter(props) {
383
461
  }
384
462
  })), Button({
385
463
  type: "button",
386
- children: "Import",
464
+ children: props.importLabel ?? "Import",
387
465
  disabled: previewRows.length === 0 || status === "error" || status === "parsing",
388
466
  onClick: () => props.onImport?.(previewRows)
389
- })), props.fileName && h("div", { class: "hu-data-transfer__status", role: "status" }, `${props.fileName}${props.maxFileSize ? ` · max ${formatBytes(props.maxFileSize)}` : ""}`), props.error && h("div", { class: "hu-data-transfer__error", role: "alert" }, props.error), previewRows.length > 0 && h("div", { class: "hu-data-transfer__preview", "aria-label": "Import preview" }, h("table", {}, h("thead", {}, h("tr", {}, ...columns.map((column) => h("th", { key: column.key, scope: "col" }, `${column.header}${column.required ? " *" : ""}`)))), h("tbody", {}, ...previewRows.slice(0, 5).map((row, index) => h("tr", { key: String(row["id"] ?? index) }, ...columns.map((column) => h("td", { key: column.key }, row[column.key] ?? ""))))))), props.onClear && Button({ type: "button", variant: "ghost", size: "sm", children: "Clear import", onClick: props.onClear }));
467
+ })), props.fileName && h("div", { class: "hu-data-transfer__status", role: "status" }, props.statusLabel?.(status, props.fileName) ?? `${props.fileName}${props.maxFileSize ? ` · max ${formatBytes(props.maxFileSize)}` : ""}`), props.error && h("div", { class: "hu-data-transfer__error", role: "alert" }, props.error), previewRows.length > 0 && h("div", { class: "hu-data-transfer__preview", "aria-label": props.previewLabel ?? "Import preview" }, h("table", { role: "table", "aria-rowcount": previewRows.length }, props.previewCaption && h("caption", { class: "hu-virtual-table__caption" }, props.previewCaption), h("thead", {}, h("tr", {}, ...columns.map((column) => h("th", {
468
+ key: column.key,
469
+ id: getImportColumnId(column),
470
+ scope: "col",
471
+ role: "columnheader",
472
+ "aria-label": column.headerLabel
473
+ }, `${column.header}${column.required ? " *" : ""}`)))), h("tbody", {}, ...previewRows.slice(0, 5).map((row, index) => h("tr", { key: String(row["id"] ?? index), role: "row", "aria-rowindex": index + 1 }, ...columns.map((column) => h("td", {
474
+ key: column.key,
475
+ role: "cell",
476
+ headers: getImportColumnId(column),
477
+ "aria-label": column.cellLabel?.(row, index)
478
+ }, row[column.key] ?? ""))))))), props.onClear && Button({ type: "button", variant: "ghost", size: "sm", children: props.clearLabel ?? "Clear import", onClick: props.onClear }));
390
479
  }
391
480
  export function TreeGrid(props) {
392
481
  injectCSS("hu-data", DATA_CSS);
@@ -394,14 +483,17 @@ export function TreeGrid(props) {
394
483
  const flatRows = flattenTreeGridRows(props.rows, expanded);
395
484
  const visibleColumns = props.columns.filter((column) => column.visible !== false);
396
485
  const firstColumn = visibleColumns[0]?.key;
397
- return h("div", {
398
- class: cn("hu-virtual-table hu-tree-grid", props.class),
486
+ const stickyHeader = props.stickyHeader !== false;
487
+ return h("div", rootProps(props, cn("hu-virtual-table hu-tree-grid", props.density && props.density !== "default" && `hu-virtual-table--${props.density}`, !stickyHeader && "hu-virtual-table--no-sticky"), {
399
488
  style: props.height !== undefined ? `height:${toCssUnit(props.height)}` : undefined,
400
489
  role: "region",
401
- "aria-label": "Tree grid"
402
- }, h("table", { role: "treegrid", "aria-rowcount": flatRows.length }, h("thead", {}, h("tr", { role: "row" }, ...visibleColumns.map((column) => h("th", {
490
+ "aria-label": props["aria-label"] ?? "Tree grid",
491
+ "aria-labelledby": props["aria-labelledby"]
492
+ }), h("table", { role: "treegrid", "aria-rowcount": flatRows.length }, props.caption && h("caption", { class: "hu-virtual-table__caption" }, props.caption), h("thead", {}, h("tr", { role: "row" }, ...visibleColumns.map((column) => h("th", {
403
493
  key: column.key,
494
+ id: getColumnId(column),
404
495
  role: "columnheader",
496
+ "aria-label": column.headerLabel,
405
497
  style: [
406
498
  column.width !== undefined && `width:${toCssUnit(column.width)}`,
407
499
  column.align && `text-align:${column.align}`
@@ -411,18 +503,23 @@ export function TreeGrid(props) {
411
503
  : flatRows.map(({ row, level }, index) => {
412
504
  const hasChildren = (row.children?.length ?? 0) > 0;
413
505
  const isExpanded = expanded.has(row.id);
506
+ const disabled = row.disabled || (props.isRowDisabled?.(row, index, level) ?? false);
414
507
  return h("tr", {
415
508
  key: row.id,
416
509
  role: "row",
417
510
  "aria-level": level,
418
511
  "aria-expanded": hasChildren ? String(isExpanded) : undefined,
419
512
  "aria-selected": props.selected === row.id ? "true" : undefined,
513
+ "aria-disabled": disabled ? "true" : undefined,
514
+ "aria-label": props.getRowLabel?.(row, index, level),
420
515
  "aria-rowindex": index + 1,
421
516
  style: props.rowHeight ? `height:${props.rowHeight}px` : undefined,
422
- onClick: props.onRowClick ? () => props.onRowClick?.(row) : undefined
517
+ onClick: props.onRowClick && !disabled ? () => props.onRowClick?.(row) : undefined
423
518
  }, ...visibleColumns.map((column) => h("td", {
424
519
  key: column.key,
425
520
  role: "gridcell",
521
+ headers: getColumnId(column),
522
+ "aria-label": column.cellLabel?.(row.data, index),
426
523
  style: column.align ? `text-align:${column.align}` : undefined
427
524
  }, column.key === firstColumn
428
525
  ? h("span", {
@@ -434,8 +531,10 @@ export function TreeGrid(props) {
434
531
  class: "hu-tree-grid__toggle",
435
532
  "aria-label": `${isExpanded ? "Collapse" : "Expand"} ${row.id}`,
436
533
  "aria-expanded": String(isExpanded),
437
- disabled: row.disabled,
534
+ disabled,
438
535
  onClick: (e) => {
536
+ if (disabled)
537
+ return;
439
538
  e.stopPropagation?.();
440
539
  props.onToggle?.(row.id, !isExpanded);
441
540
  }
@@ -448,13 +547,46 @@ export function PivotGrid(props) {
448
547
  injectCSS("hu-data", DATA_CSS);
449
548
  const aggregation = props.aggregation ?? "sum";
450
549
  const matrix = buildPivotMatrix(props.rows, props.rowFields, props.columnField, props.valueField, aggregation);
451
- return h("section", { class: cn("hu-pivot-grid", props.class), "aria-label": props.title ?? "Pivot grid" }, h("div", { class: "hu-pivot-grid__header" }, h("div", {}, h("div", { class: "hu-pivot-grid__title" }, props.title ?? "Pivot grid"), h("div", { class: "hu-pivot-grid__meta" }, `${props.rows.length} rows · ${aggregation}`)), h("div", { class: "hu-pivot-grid__meta", "aria-live": "polite" }, `${matrix.rowKeys.length} groups · ${matrix.columnKeys.length} columns`)), h("div", { class: "hu-pivot-grid__table", role: "region", "aria-label": "Pivot results" }, h("table", { role: "table" }, h("thead", {}, h("tr", { role: "row" }, h("th", { role: "columnheader", scope: "col" }, props.rowFields.map((field) => field.header).join(" / ") || "Group"), ...matrix.columnKeys.map((columnKey) => h("th", { key: columnKey, role: "columnheader", scope: "col" }, columnKey)), h("th", { role: "columnheader", scope: "col" }, "Total"))), h("tbody", {}, matrix.rowKeys.length === 0
452
- ? h("tr", {}, h("td", { colspan: matrix.columnKeys.length + 2, class: "hu-virtual-table__empty" }, "No pivot data."))
453
- : matrix.rowKeys.map((rowKey) => h("tr", { key: rowKey, role: "row" }, h("th", { role: "rowheader", scope: "row" }, rowKey), ...matrix.columnKeys.map((columnKey) => h("td", { key: columnKey, role: "cell" }, formatPivotValue(matrix.values.get(`${rowKey}\u0000${columnKey}`) ?? 0, props, rowKey, columnKey, aggregation))), h("td", { class: "hu-pivot-grid__total", role: "cell" }, formatPivotValue(matrix.rowTotals.get(rowKey) ?? 0, props, rowKey, "__total", aggregation))))), matrix.rowKeys.length > 0 && h("tfoot", {}, h("tr", { role: "row" }, h("th", { scope: "row" }, "Total"), ...matrix.columnKeys.map((columnKey) => h("td", { key: columnKey, class: "hu-pivot-grid__total" }, formatPivotValue(matrix.columnTotals.get(columnKey) ?? 0, props, "__total", columnKey, aggregation))), h("td", { class: "hu-pivot-grid__total" }, formatPivotValue(matrix.grandTotal, props, "__total", "__total", aggregation)))))));
550
+ const groupHeaderId = getPivotFieldId(props.rowFields[0], "group");
551
+ const totalHeaderId = "hu-pivot-total";
552
+ return h("section", rootProps(props, "hu-pivot-grid", {
553
+ "aria-label": props["aria-label"] ?? props.title ?? "Pivot grid",
554
+ "aria-labelledby": props["aria-labelledby"]
555
+ }), h("div", { class: "hu-pivot-grid__header" }, h("div", {}, h("div", { class: "hu-pivot-grid__title" }, props.title ?? "Pivot grid"), h("div", { class: "hu-pivot-grid__meta" }, `${props.rows.length} rows · ${aggregation}`)), h("div", { class: "hu-pivot-grid__meta", "aria-live": "polite" }, `${matrix.rowKeys.length} groups · ${matrix.columnKeys.length} columns`)), h("div", {
556
+ class: cn("hu-pivot-grid__table", props.density && props.density !== "default" && `hu-pivot-grid__table--${props.density}`, props.stickyHeader && "hu-pivot-grid__table--sticky"),
557
+ role: "region",
558
+ "aria-label": props.resultsLabel ?? "Pivot results",
559
+ "aria-labelledby": props.resultsLabelledBy
560
+ }, h("table", { role: "table" }, props.caption && h("caption", { class: "hu-pivot-grid__caption" }, props.caption), h("thead", {}, h("tr", { role: "row" }, h("th", {
561
+ id: groupHeaderId,
562
+ role: "columnheader",
563
+ scope: "col",
564
+ "aria-label": props.rowFields.map((field) => field.headerLabel).filter(Boolean).join(" / ") || undefined
565
+ }, props.rowFields.map((field) => field.header).join(" / ") || "Group"), ...matrix.columnKeys.map((columnKey) => h("th", { key: columnKey, id: getPivotColumnId(columnKey), role: "columnheader", scope: "col" }, columnKey)), h("th", { id: totalHeaderId, role: "columnheader", scope: "col" }, "Total"))), h("tbody", {}, matrix.rowKeys.length === 0
566
+ ? h("tr", {}, h("td", { colspan: matrix.columnKeys.length + 2, class: "hu-virtual-table__empty" }, props.emptyMessage ?? "No pivot data."))
567
+ : matrix.rowKeys.map((rowKey) => h("tr", { key: rowKey, role: "row" }, h("th", { id: getPivotRowId(rowKey), role: "rowheader", scope: "row", headers: groupHeaderId }, rowKey), ...matrix.columnKeys.map((columnKey) => h("td", { key: columnKey, role: "cell", headers: `${getPivotRowId(rowKey)} ${getPivotColumnId(columnKey)}` }, formatPivotValue(matrix.values.get(`${rowKey}\u0000${columnKey}`) ?? 0, props, rowKey, columnKey, aggregation))), h("td", { class: "hu-pivot-grid__total", role: "cell", headers: `${getPivotRowId(rowKey)} ${totalHeaderId}` }, formatPivotValue(matrix.rowTotals.get(rowKey) ?? 0, props, rowKey, "__total", aggregation))))), matrix.rowKeys.length > 0 && h("tfoot", {}, h("tr", { role: "row" }, h("th", { id: "hu-pivot-footer-total", role: "rowheader", scope: "row", headers: groupHeaderId }, "Total"), ...matrix.columnKeys.map((columnKey) => h("td", { key: columnKey, class: "hu-pivot-grid__total", role: "cell", headers: `hu-pivot-footer-total ${getPivotColumnId(columnKey)}` }, formatPivotValue(matrix.columnTotals.get(columnKey) ?? 0, props, "__total", columnKey, aggregation))), h("td", { class: "hu-pivot-grid__total", role: "cell", headers: `hu-pivot-footer-total ${totalHeaderId}` }, formatPivotValue(matrix.grandTotal, props, "__total", "__total", aggregation)))))));
454
568
  }
455
569
  function toCssUnit(value) {
456
570
  return typeof value === "number" ? `${value}px` : value;
457
571
  }
572
+ function getColumnId(column) {
573
+ return column.id ?? `hu-virtual-col-${column.key}`;
574
+ }
575
+ function getImportColumnId(column) {
576
+ return column.id ?? `hu-import-col-${column.key}`;
577
+ }
578
+ function getPivotFieldId(field, fallback) {
579
+ return field?.id ?? `hu-pivot-${fallback}`;
580
+ }
581
+ function getPivotColumnId(value) {
582
+ return `hu-pivot-col-${toDomId(value)}`;
583
+ }
584
+ function getPivotRowId(value) {
585
+ return `hu-pivot-row-${toDomId(value)}`;
586
+ }
587
+ function toDomId(value) {
588
+ return value.toLowerCase().replace(/[^a-z0-9_-]+/g, "-").replace(/^-+|-+$/g, "") || "value";
589
+ }
458
590
  function getSelectedExportColumns(columns, selectedColumns) {
459
591
  if (selectedColumns) {
460
592
  const selected = new Set(selectedColumns);