@adia-ai/web-components 0.4.5 → 0.4.7

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 (316) hide show
  1. package/README.md +63 -24
  2. package/USAGE.md +604 -0
  3. package/components/accordion/accordion.d.ts +17 -0
  4. package/components/accordion/accordion.js +10 -117
  5. package/components/accordion/class.js +132 -0
  6. package/components/action-list/action-list.d.ts +15 -0
  7. package/components/action-list/action-list.js +9 -140
  8. package/components/action-list/class.js +156 -0
  9. package/components/agent-artifact/agent-artifact.d.ts +25 -0
  10. package/components/agent-artifact/agent-artifact.js +8 -181
  11. package/components/agent-artifact/class.js +200 -0
  12. package/components/agent-feedback-bar/agent-feedback-bar.d.ts +21 -0
  13. package/components/agent-feedback-bar/agent-feedback-bar.js +8 -143
  14. package/components/agent-feedback-bar/class.js +162 -0
  15. package/components/agent-questions/agent-questions.d.ts +23 -0
  16. package/components/agent-questions/agent-questions.js +8 -180
  17. package/components/agent-questions/class.js +199 -0
  18. package/components/agent-reasoning/agent-reasoning.d.ts +23 -0
  19. package/components/agent-reasoning/agent-reasoning.js +8 -494
  20. package/components/agent-reasoning/class.js +513 -0
  21. package/components/agent-suggestions/agent-suggestions.d.ts +21 -0
  22. package/components/agent-suggestions/agent-suggestions.js +8 -78
  23. package/components/agent-suggestions/class.js +97 -0
  24. package/components/agent-trace/agent-trace.d.ts +19 -0
  25. package/components/alert/alert.d.ts +29 -0
  26. package/components/alert/alert.js +8 -175
  27. package/components/alert/class.js +194 -0
  28. package/components/avatar/avatar.d.ts +27 -0
  29. package/components/avatar/avatar.js +9 -159
  30. package/components/avatar/class.js +173 -0
  31. package/components/badge/badge.d.ts +27 -0
  32. package/components/badge/badge.js +9 -75
  33. package/components/badge/class.js +93 -0
  34. package/components/block/block.d.ts +19 -0
  35. package/components/block/block.js +9 -15
  36. package/components/block/class.js +33 -0
  37. package/components/breadcrumb/breadcrumb.d.ts +23 -0
  38. package/components/breadcrumb/breadcrumb.js +8 -113
  39. package/components/breadcrumb/class.js +132 -0
  40. package/components/button/button.d.ts +34 -0
  41. package/components/button/button.js +15 -66
  42. package/components/button/class.js +80 -0
  43. package/components/calendar-picker/calendar-picker.a2ui.json +6 -1
  44. package/components/calendar-picker/calendar-picker.d.ts +27 -0
  45. package/components/calendar-picker/calendar-picker.js +8 -332
  46. package/components/calendar-picker/calendar-picker.yaml +51 -177
  47. package/components/calendar-picker/class.js +351 -0
  48. package/components/canvas/canvas.a2ui.json +6 -1
  49. package/components/canvas/canvas.d.ts +17 -0
  50. package/components/canvas/canvas.yaml +19 -36
  51. package/components/card/card.a2ui.json +3 -0
  52. package/components/card/card.d.ts +27 -0
  53. package/components/card/card.js +9 -50
  54. package/components/card/card.yaml +171 -433
  55. package/components/card/class.js +68 -0
  56. package/components/chart/chart.d.ts +41 -0
  57. package/components/chart/chart.js +8 -2131
  58. package/components/chart/class.js +2150 -0
  59. package/components/chart-legend/chart-legend.d.ts +27 -0
  60. package/components/chart-legend/chart-legend.js +8 -197
  61. package/components/chart-legend/class.js +215 -0
  62. package/components/chat-thread/chat-thread.d.ts +17 -0
  63. package/components/chat-thread/chat-thread.js +8 -157
  64. package/components/chat-thread/class.js +176 -0
  65. package/components/check/check.d.ts +30 -0
  66. package/components/check/check.js +11 -52
  67. package/components/check/class.js +68 -0
  68. package/components/code/class.js +501 -0
  69. package/components/code/code.d.ts +39 -0
  70. package/components/code/code.js +8 -482
  71. package/components/col/class.js +30 -0
  72. package/components/col/col.d.ts +23 -0
  73. package/components/col/col.js +10 -13
  74. package/components/color-picker/class.js +550 -0
  75. package/components/color-picker/color-picker.d.ts +37 -0
  76. package/components/color-picker/color-picker.js +8 -531
  77. package/components/command/class.js +364 -0
  78. package/components/command/command.a2ui.json +3 -0
  79. package/components/command/command.d.ts +19 -0
  80. package/components/command/command.js +8 -345
  81. package/components/command/command.yaml +105 -124
  82. package/components/demo-toggle/class.js +153 -0
  83. package/components/demo-toggle/demo-toggle.d.ts +23 -0
  84. package/components/demo-toggle/demo-toggle.js +8 -135
  85. package/components/description-list/class.js +86 -0
  86. package/components/description-list/description-list.d.ts +21 -0
  87. package/components/description-list/description-list.js +8 -67
  88. package/components/divider/class.js +57 -0
  89. package/components/divider/divider.d.ts +19 -0
  90. package/components/divider/divider.js +10 -40
  91. package/components/drawer/class.js +306 -0
  92. package/components/drawer/drawer.d.ts +25 -0
  93. package/components/drawer/drawer.js +8 -287
  94. package/components/embed/class.js +73 -0
  95. package/components/embed/embed.d.ts +23 -0
  96. package/components/embed/embed.js +9 -55
  97. package/components/empty-state/class.js +108 -0
  98. package/components/empty-state/empty-state.d.ts +21 -0
  99. package/components/empty-state/empty-state.js +9 -90
  100. package/components/feed/class.js +381 -0
  101. package/components/feed/feed.d.ts +19 -0
  102. package/components/feed/feed.js +9 -367
  103. package/components/field/class.js +266 -0
  104. package/components/field/field.d.ts +23 -0
  105. package/components/field/field.js +8 -247
  106. package/components/fields/class.js +106 -0
  107. package/components/fields/fields.d.ts +19 -0
  108. package/components/fields/fields.js +8 -87
  109. package/components/grid/class.js +31 -0
  110. package/components/grid/grid.d.ts +23 -0
  111. package/components/grid/grid.js +10 -14
  112. package/components/heatmap/class.js +305 -0
  113. package/components/heatmap/heatmap.d.ts +31 -0
  114. package/components/heatmap/heatmap.js +8 -286
  115. package/components/icon/class.js +54 -0
  116. package/components/icon/icon.d.ts +23 -0
  117. package/components/icon/icon.js +13 -40
  118. package/components/image/class.js +112 -0
  119. package/components/image/image.d.ts +33 -0
  120. package/components/image/image.js +9 -94
  121. package/components/index.js +1 -0
  122. package/components/input/class.js +773 -0
  123. package/components/input/input.a2ui.json +3 -0
  124. package/components/input/input.d.ts +61 -0
  125. package/components/input/input.js +8 -755
  126. package/components/input/input.yaml +171 -442
  127. package/components/inspector/class.js +142 -0
  128. package/components/inspector/inspector.a2ui.json +8 -1
  129. package/components/inspector/inspector.d.ts +17 -0
  130. package/components/inspector/inspector.js +8 -124
  131. package/components/inspector/inspector.yaml +15 -30
  132. package/components/kbd/class.js +34 -0
  133. package/components/kbd/kbd.a2ui.json +3 -0
  134. package/components/kbd/kbd.d.ts +17 -0
  135. package/components/kbd/kbd.js +10 -17
  136. package/components/kbd/kbd.yaml +54 -185
  137. package/components/link/class.js +187 -0
  138. package/components/link/link.d.ts +55 -0
  139. package/components/link/link.js +8 -168
  140. package/components/list/class.js +249 -0
  141. package/components/list/list.d.ts +23 -0
  142. package/components/list/list.js +9 -231
  143. package/components/menu/class.js +332 -0
  144. package/components/menu/menu.d.ts +21 -0
  145. package/components/menu/menu.js +11 -316
  146. package/components/modal/class.js +231 -0
  147. package/components/modal/modal.a2ui.json +5 -1
  148. package/components/modal/modal.d.ts +23 -0
  149. package/components/modal/modal.js +8 -212
  150. package/components/modal/modal.yaml +19 -39
  151. package/components/nav/class.js +150 -0
  152. package/components/nav/nav.d.ts +31 -0
  153. package/components/nav/nav.js +8 -131
  154. package/components/nav-group/class.js +152 -0
  155. package/components/nav-group/nav-group.d.ts +35 -0
  156. package/components/nav-group/nav-group.js +9 -134
  157. package/components/nav-item/class.js +86 -0
  158. package/components/nav-item/nav-item.d.ts +37 -0
  159. package/components/nav-item/nav-item.js +10 -69
  160. package/components/noodles/class.js +510 -0
  161. package/components/noodles/noodles.d.ts +33 -0
  162. package/components/noodles/noodles.js +9 -493
  163. package/components/option-card/class.js +167 -0
  164. package/components/option-card/option-card.d.ts +30 -0
  165. package/components/option-card/option-card.js +8 -149
  166. package/components/otp-input/class.js +180 -0
  167. package/components/otp-input/otp-input.a2ui.json +5 -1
  168. package/components/otp-input/otp-input.d.ts +25 -0
  169. package/components/otp-input/otp-input.js +9 -162
  170. package/components/otp-input/otp-input.yaml +45 -174
  171. package/components/page/class.js +97 -0
  172. package/components/page/page.d.ts +46 -0
  173. package/components/page/page.js +8 -79
  174. package/components/pagination/class.js +195 -0
  175. package/components/pagination/pagination.d.ts +23 -0
  176. package/components/pagination/pagination.js +9 -177
  177. package/components/pane/class.js +186 -0
  178. package/components/pane/pane.a2ui.json +12 -1
  179. package/components/pane/pane.css +10 -0
  180. package/components/pane/pane.d.ts +31 -0
  181. package/components/pane/pane.js +8 -143
  182. package/components/pane/pane.yaml +57 -157
  183. package/components/pipeline-status/class.js +189 -0
  184. package/components/pipeline-status/pipeline-status.a2ui.json +7 -1
  185. package/components/pipeline-status/pipeline-status.d.ts +21 -0
  186. package/components/pipeline-status/pipeline-status.js +9 -172
  187. package/components/pipeline-status/pipeline-status.yaml +34 -72
  188. package/components/popover/class.js +194 -0
  189. package/components/popover/popover.d.ts +23 -0
  190. package/components/popover/popover.js +9 -176
  191. package/components/progress/class.js +74 -0
  192. package/components/progress/progress.a2ui.json +3 -0
  193. package/components/progress/progress.d.ts +19 -0
  194. package/components/progress/progress.js +10 -57
  195. package/components/progress/progress.yaml +124 -287
  196. package/components/progress-row/class.js +110 -0
  197. package/components/progress-row/progress-row.d.ts +23 -0
  198. package/components/progress-row/progress-row.js +8 -92
  199. package/components/radio/class.js +83 -0
  200. package/components/radio/radio.d.ts +28 -0
  201. package/components/radio/radio.js +11 -67
  202. package/components/range/class.js +194 -0
  203. package/components/range/range.d.ts +31 -0
  204. package/components/range/range.js +9 -176
  205. package/components/rating/class.js +148 -0
  206. package/components/rating/rating.d.ts +33 -0
  207. package/components/rating/rating.js +9 -130
  208. package/components/richtext/class.js +87 -0
  209. package/components/richtext/richtext.a2ui.json +7 -1
  210. package/components/richtext/richtext.d.ts +19 -0
  211. package/components/richtext/richtext.js +8 -68
  212. package/components/richtext/richtext.yaml +30 -65
  213. package/components/row/class.js +50 -0
  214. package/components/row/row.d.ts +27 -0
  215. package/components/row/row.js +10 -33
  216. package/components/search/class.js +134 -0
  217. package/components/search/search.d.ts +35 -0
  218. package/components/search/search.js +10 -117
  219. package/components/segment/class.js +62 -0
  220. package/components/segment/segment.d.ts +25 -0
  221. package/components/segment/segment.js +10 -45
  222. package/components/segmented/class.js +165 -0
  223. package/components/segmented/segmented.a2ui.json +4 -0
  224. package/components/segmented/segmented.d.ts +24 -0
  225. package/components/segmented/segmented.js +10 -148
  226. package/components/segmented/segmented.yaml +41 -59
  227. package/components/select/class.js +408 -0
  228. package/components/select/select.d.ts +57 -0
  229. package/components/select/select.js +15 -396
  230. package/components/skeleton/class.js +52 -0
  231. package/components/skeleton/skeleton.d.ts +23 -0
  232. package/components/skeleton/skeleton.js +8 -34
  233. package/components/slider/class.js +184 -0
  234. package/components/slider/slider.d.ts +31 -0
  235. package/components/slider/slider.js +9 -166
  236. package/components/stack/class.js +28 -0
  237. package/components/stack/stack.d.ts +17 -0
  238. package/components/stack/stack.js +10 -11
  239. package/components/step-progress/class.js +98 -0
  240. package/components/step-progress/step-progress.d.ts +27 -0
  241. package/components/step-progress/step-progress.js +8 -79
  242. package/components/stepper/class.js +126 -0
  243. package/components/stepper/stepper.d.ts +19 -0
  244. package/components/stepper/stepper.js +9 -112
  245. package/components/stream/class.js +109 -0
  246. package/components/stream/stream.d.ts +19 -0
  247. package/components/stream/stream.js +8 -90
  248. package/components/swatch/class.js +131 -0
  249. package/components/swatch/swatch.d.ts +28 -0
  250. package/components/swatch/swatch.js +8 -112
  251. package/components/swiper/class.js +373 -0
  252. package/components/swiper/swiper.a2ui.json +4 -0
  253. package/components/swiper/swiper.d.ts +31 -0
  254. package/components/swiper/swiper.js +8 -354
  255. package/components/swiper/swiper.yaml +68 -212
  256. package/components/switch/class.js +63 -0
  257. package/components/switch/switch.a2ui.json +6 -1
  258. package/components/switch/switch.d.ts +30 -0
  259. package/components/switch/switch.js +11 -47
  260. package/components/switch/switch.yaml +70 -265
  261. package/components/table/class.js +1453 -0
  262. package/components/table/table.d.ts +37 -0
  263. package/components/table/table.js +8 -1435
  264. package/components/table-toolbar/class.js +680 -0
  265. package/components/table-toolbar/table-toolbar.d.ts +33 -0
  266. package/components/table-toolbar/table-toolbar.js +8 -689
  267. package/components/tabs/class.js +242 -0
  268. package/components/tabs/tabs.d.ts +21 -0
  269. package/components/tabs/tabs.js +8 -223
  270. package/components/tag/class.js +99 -0
  271. package/components/tag/tag.d.ts +27 -0
  272. package/components/tag/tag.js +8 -80
  273. package/components/text/class.js +46 -0
  274. package/components/text/text.d.ts +25 -0
  275. package/components/text/text.js +9 -28
  276. package/components/textarea/class.js +134 -0
  277. package/components/textarea/textarea.d.ts +31 -0
  278. package/components/textarea/textarea.js +11 -118
  279. package/components/timeline/class.js +176 -0
  280. package/components/timeline/timeline.d.ts +19 -0
  281. package/components/timeline/timeline.js +9 -162
  282. package/components/toast/class.js +92 -0
  283. package/components/toast/toast.d.ts +23 -0
  284. package/components/toast/toast.js +9 -76
  285. package/components/toggle-group/class.js +154 -0
  286. package/components/toggle-group/toggle-group.d.ts +19 -0
  287. package/components/toggle-group/toggle-group.js +11 -140
  288. package/components/toggle-scheme/class.js +286 -0
  289. package/components/toggle-scheme/toggle-scheme.a2ui.json +197 -0
  290. package/components/toggle-scheme/toggle-scheme.css +20 -0
  291. package/components/toggle-scheme/toggle-scheme.d.ts +41 -0
  292. package/components/toggle-scheme/toggle-scheme.js +17 -0
  293. package/components/toggle-scheme/toggle-scheme.yaml +173 -0
  294. package/components/toolbar/class.js +388 -0
  295. package/components/toolbar/toolbar.d.ts +23 -0
  296. package/components/toolbar/toolbar.js +10 -376
  297. package/components/tooltip/class.js +299 -0
  298. package/components/tooltip/tooltip.d.ts +27 -0
  299. package/components/tooltip/tooltip.js +8 -280
  300. package/components/tree/class.js +245 -0
  301. package/components/tree/tree.d.ts +15 -0
  302. package/components/tree/tree.js +9 -244
  303. package/components/upload/class.js +199 -0
  304. package/components/upload/upload.d.ts +27 -0
  305. package/components/upload/upload.js +11 -183
  306. package/core/element.d.ts +174 -0
  307. package/core/form.d.ts +108 -0
  308. package/core/index.d.ts +11 -0
  309. package/core/index.js +1 -0
  310. package/core/register.d.ts +25 -0
  311. package/core/register.js +58 -0
  312. package/core/signals.d.ts +94 -0
  313. package/core/template.d.ts +70 -0
  314. package/index.d.ts +315 -0
  315. package/package.json +25 -6
  316. package/traits/CATEGORIES.md +1 -1
@@ -1,1444 +1,17 @@
1
1
  /**
2
- * <table-ui>Data table with sorting, selection, pagination, search,
3
- * column resize, keyboard navigation, cell types, and CSV export.
2
+ * `<table-ui>`auto-registers the tag on import.
4
3
  *
5
- * Renders as CSS grid + subgrid rows with ARIA grid roles (no <table> element).
4
+ * For non-side-effect class import (test isolation, tag override), use
5
+ * the `class` subpath:
6
6
  *
7
- * Attributes:
8
- * sortable — enable column sorting on header click
9
- * selectable — enable row selection via checkboxes
10
- * striped — alternate row background
11
- * density — 'compact' | 'standard' | 'comfortable'
12
- * paginate — rows per page (0 = no pagination)
13
- * loading — show loading overlay
14
- * search — global search filter string
7
+ * import { UITable } from '@adia-ai/web-components/components/table/class';
15
8
  *
16
- * Declarative columns via <col-def> children:
17
- * <col-def key="name" label="Name" type="text" width="200"
18
- * min-width="100" max-width="400" flex="2" pinned="left"
19
- * sortable hidden filter-type="text"></col-def>
20
- *
21
- * JS API:
22
- * .columns = [{key, label, type, width, minWidth, maxWidth, flex,
23
- * sortable, filterable, resizable, pinned, hidden,
24
- * accessor, format, render, sortFn, sortDescFirst,
25
- * filterType, filterFn, aggregate, meta}]
26
- * .data = [{...}, ...]
27
- * .selected → [...indices] (read-only)
28
- * .sortState → [{key, dir}] (read-only)
29
- * .exportCSV(filename?)
30
- *
31
- * Events:
32
- * sort — { detail: { key, dir, sortState } }
33
- * select — { detail: { selected: [...indices] } }
34
- * page — { detail: { page } }
35
- * resize — { detail: { key, width } }
36
- * cell-click — { detail: { key, row, value, dataIndex } }
37
- */
38
-
39
- import { UIElement } from '../../core/element.js';
40
- import { cellTypes, sortFns } from './cell-types.js';
41
-
42
- // ── Helpers ──────────────────────────────────────────────────────────────────
43
-
44
- /**
45
- * Resolve a dot-notation path on an object.
46
- * getNestedValue({user: {name: 'Jo'}}, 'user.name') → 'Jo'
9
+ * @see ../../USAGE.md#registration--auto-vs-explicit
47
10
  */
48
- function getNestedValue(obj, path) {
49
- if (!path || !obj) return undefined;
50
- const parts = path.split('.');
51
- let cur = obj;
52
- for (const p of parts) {
53
- if (cur == null) return undefined;
54
- cur = cur[p];
55
- }
56
- return cur;
57
- }
58
-
59
- /**
60
- * Get cell value for a column from a row, respecting accessor and dot notation.
61
- */
62
- function getCellValue(row, col) {
63
- if (typeof col.accessor === 'function') return col.accessor(row);
64
- return getNestedValue(row, col.key);
65
- }
66
-
67
- /**
68
- * Escape a value for CSV output.
69
- */
70
- function csvEscape(val) {
71
- const str = String(val ?? '');
72
- if (str.includes(',') || str.includes('"') || str.includes('\n')) {
73
- return '"' + str.replace(/"/g, '""') + '"';
74
- }
75
- return str;
76
- }
77
-
78
- // ── Component ────────────────────────────────────────────────────────────────
79
-
80
- class UITable extends UIElement {
81
- static properties = {
82
- sortable: { type: Boolean, default: false, reflect: true },
83
- selectable: { type: Boolean, default: false, reflect: true },
84
- expandable: { type: Boolean, default: false, reflect: true },
85
- striped: { type: Boolean, default: false, reflect: true },
86
- raw: { type: Boolean, default: false, reflect: true },
87
- density: { type: String, default: 'standard', reflect: true },
88
- paginate: { type: Number, default: 0, reflect: true },
89
- loading: { type: Boolean, default: false, reflect: true },
90
- search: { type: String, default: '', reflect: true },
91
- };
92
-
93
- static template = () => null;
94
-
95
- // ── Private state ──
96
-
97
- #columns = [];
98
- #data = [];
99
- #sortState = []; // [{key, dir}] for multi-sort
100
- #filters = new Map(); // key → { op, value }
101
- #expanded = new Set(); // row indices
102
- #editingCell = null; // {rowIndex, colKey} or null
103
- #page = 0;
104
- #selected = new Set();
105
- #columnWidths = new Map(); // key → current width in px
106
- #focusedCell = null; // {row, col}
107
- #bound = false;
108
- #lastSelectedIndex = -1; // for shift-click range select
109
- #openFilter = null; // column key of open filter dropdown
110
- #filterFocusRaf = null;
111
-
112
- // ── Public API: columns ──
113
-
114
- set columns(arr) {
115
- this.#columns = Array.isArray(arr) ? arr : [];
116
- this.#requestRender();
117
- }
118
-
119
- get columns() { return this.#columns; }
120
-
121
- // ── Public API: data ──
122
-
123
- set data(arr) {
124
- this.#data = Array.isArray(arr) ? arr : [];
125
- this.#selected.clear();
126
- this.#page = 0;
127
- this.#requestRender();
128
- }
129
-
130
- get data() { return this.#data; }
131
-
132
- // ── Public API: selection ──
133
-
134
- /**
135
- * Indices of currently-selected rows, ascending.
136
- * @returns {number[]}
137
- */
138
- get selected() { return [...this.#selected].sort((a, b) => a - b); }
139
-
140
- /**
141
- * Replace the selection set programmatically. Out-of-range indices are
142
- * silently dropped. Pair with `selectable` mode; on a non-selectable
143
- * table the indices are stored but not rendered as checked rows.
144
- * @param {Iterable<number>} indices
145
- */
146
- set selected(indices) {
147
- this.#selected.clear();
148
- if (indices) {
149
- for (const i of indices) {
150
- if (Number.isInteger(i) && i >= 0 && i < this.#data.length) {
151
- this.#selected.add(i);
152
- }
153
- }
154
- }
155
- this.#requestRender();
156
- this.dispatchEvent(new CustomEvent('select', {
157
- detail: { selected: this.selected },
158
- bubbles: true,
159
- }));
160
- }
161
-
162
- /**
163
- * Empty the selection set. Equivalent to `el.selected = []` but doesn't
164
- * require constructing an empty array; the most common selection-write
165
- * call by far (after a "delete" or "archive" bulk action), so worth
166
- * having a one-token verb form.
167
- */
168
- clearSelection() {
169
- if (this.#selected.size === 0) return;
170
- this.#selected.clear();
171
- this.#requestRender();
172
- this.dispatchEvent(new CustomEvent('select', {
173
- detail: { selected: [] },
174
- bubbles: true,
175
- }));
176
- }
177
-
178
- // ── Public API: read-only getters ──
179
-
180
- get sortState() { return this.#sortState.map(s => ({ ...s })); }
181
-
182
- // ── Public API: filters ──
183
-
184
- setFilter(key, value, op = 'contains') {
185
- if (value === null || value === undefined || value === '') {
186
- this.#filters.delete(key);
187
- } else {
188
- this.#filters.set(key, { op, value });
189
- }
190
- this.#page = 0;
191
- this.#requestRender();
192
- this.dispatchEvent(new CustomEvent('filter-change', {
193
- detail: { filters: Object.fromEntries(this.#filters) },
194
- bubbles: true,
195
- }));
196
- }
197
-
198
- clearFilters() {
199
- this.#filters.clear();
200
- this.#openFilter = null;
201
- this.#page = 0;
202
- this.#requestRender();
203
- this.dispatchEvent(new CustomEvent('filter-change', {
204
- detail: { filters: {} },
205
- bubbles: true,
206
- }));
207
- }
208
-
209
- get filters() { return Object.fromEntries(this.#filters); }
210
-
211
- // ── Public API: expansion ──
212
-
213
- toggleExpand(index) {
214
- if (this.#expanded.has(index)) {
215
- this.#expanded.delete(index);
216
- this.dispatchEvent(new CustomEvent('row-collapse', { detail: { index, row: this.#data[index] }, bubbles: true }));
217
- } else {
218
- this.#expanded.add(index);
219
- this.dispatchEvent(new CustomEvent('row-expand', { detail: { index, row: this.#data[index] }, bubbles: true }));
220
- }
221
- this.#requestRender();
222
- }
223
-
224
- get expanded() { return [...this.#expanded]; }
225
-
226
- /** @type {((row: object, index: number) => HTMLElement)|null} */
227
- expandRenderer = null;
228
-
229
- // ── Public API: state persistence ──
230
-
231
- getState() {
232
- return {
233
- sort: this.#sortState.map(s => ({ ...s })),
234
- filters: Object.fromEntries(this.#filters),
235
- columnWidths: Object.fromEntries(this.#columnWidths),
236
- hiddenColumns: this.#columns.filter(c => c.hidden).map(c => c.key),
237
- page: this.#page,
238
- density: this.density,
239
- };
240
- }
241
-
242
- setState(state) {
243
- if (!state) return;
244
- if (state.sort) this.#sortState = state.sort;
245
- if (state.filters) {
246
- this.#filters.clear();
247
- for (const [key, val] of Object.entries(state.filters)) {
248
- this.#filters.set(key, val);
249
- }
250
- }
251
- if (state.columnWidths) {
252
- this.#columnWidths.clear();
253
- for (const [key, val] of Object.entries(state.columnWidths)) {
254
- this.#columnWidths.set(key, val);
255
- }
256
- }
257
- if (state.hiddenColumns) {
258
- for (const col of this.#columns) {
259
- col.hidden = state.hiddenColumns.includes(col.key);
260
- }
261
- }
262
- if (state.page != null) this.#page = state.page;
263
- if (state.density) this.density = state.density;
264
- this.#requestRender();
265
- }
266
-
267
- #persistState() {
268
- const key = this.getAttribute('state-key');
269
- if (!key) return;
270
- try {
271
- localStorage.setItem(`table-state:${key}`, JSON.stringify(this.getState()));
272
- } catch {}
273
- }
274
-
275
- #restoreState() {
276
- const key = this.getAttribute('state-key');
277
- if (!key) return;
278
- try {
279
- const saved = localStorage.getItem(`table-state:${key}`);
280
- if (saved) this.setState(JSON.parse(saved));
281
- } catch {}
282
- }
283
-
284
- // ── Lifecycle ──────────────────────────────────────────────────────────────
285
-
286
- connected() {
287
- // Parse declarative <col-def> children
288
- this.#parseColDefs();
289
-
290
- this.setAttribute('role', 'grid');
291
- this.setAttribute('tabindex', '0');
292
-
293
- // Restore persisted state
294
- this.#restoreState();
295
-
296
- if (!this.#bound) {
297
- this.#bound = true;
298
- this.addEventListener('click', this.#onClick);
299
- this.addEventListener('keydown', this.#onKeydown);
300
- }
301
- }
302
-
303
- disconnected() {
304
- this.removeEventListener('click', this.#onClick);
305
- this.removeEventListener('keydown', this.#onKeydown);
306
- this.#bound = false;
307
- this.#cleanupResize();
308
- if (this.#renderRaf) {
309
- cancelAnimationFrame(this.#renderRaf);
310
- this.#renderRaf = null;
311
- }
312
- if (this.#filterFocusRaf != null) {
313
- cancelAnimationFrame(this.#filterFocusRaf);
314
- this.#filterFocusRaf = null;
315
- }
316
- }
317
-
318
- // ── <col-def> Parsing ──────────────────────────────────────────────────────
319
-
320
- #parseColDefs() {
321
- const defs = this.querySelectorAll(':scope > col-def');
322
- if (!defs.length) return;
323
-
324
- const cols = [];
325
- for (const el of defs) {
326
- const col = {
327
- key: el.getAttribute('key') || '',
328
- label: el.getAttribute('label') || el.getAttribute('key') || '',
329
- type: el.getAttribute('type') || 'text',
330
- pinned: el.getAttribute('pinned') || null, // 'left' | 'right' | null
331
- hidden: el.hasAttribute('hidden'),
332
- sortable: el.hasAttribute('sortable'),
333
- resizable: el.hasAttribute('resizable'),
334
- filterable: el.hasAttribute('filterable'),
335
- filterType: el.getAttribute('filter-type') || null,
336
- };
337
-
338
- const w = el.getAttribute('width');
339
- if (w) col.width = Number(w);
340
-
341
- const min = el.getAttribute('min-width');
342
- if (min) col.minWidth = Number(min);
343
-
344
- const max = el.getAttribute('max-width');
345
- if (max) col.maxWidth = Number(max);
346
-
347
- const flex = el.getAttribute('flex');
348
- if (flex) col.flex = Number(flex);
349
-
350
- cols.push(col);
351
- el.remove();
352
- }
353
-
354
- // Only set if we actually found col-defs and columns aren't already set via JS
355
- if (cols.length && !this.#columns.length) {
356
- this.#columns = cols;
357
- }
358
- }
359
-
360
- // ── Render Batching ────────────────────────────────────────────────────────
361
-
362
- #renderRaf = null;
363
-
364
- #requestRender() {
365
- if (!this.isConnected || this.#renderRaf) return;
366
- this.#renderRaf = requestAnimationFrame(() => {
367
- this.#renderRaf = null;
368
- this.render();
369
- this.#persistState();
370
- });
371
- }
372
-
373
- // ── Visible Columns Helper ─────────────────────────────────────────────────
374
-
375
- get #visibleColumns() {
376
- return this.#columns.filter(c => !c.hidden);
377
- }
378
-
379
- // ── Grid Template Columns ──────────────────────────────────────────────────
380
-
381
- #buildGridTemplate() {
382
- const parts = [];
383
-
384
- if (this.expandable) {
385
- parts.push('2.5rem');
386
- }
387
-
388
- if (this.selectable) {
389
- parts.push('2.5rem');
390
- }
391
-
392
- for (const col of this.#visibleColumns) {
393
- // User-resized width takes priority
394
- if (this.#columnWidths.has(col.key)) {
395
- parts.push(`${this.#columnWidths.get(col.key)}px`);
396
- } else if (col.width) {
397
- parts.push(`${col.width}px`);
398
- } else if (col.flex) {
399
- parts.push(`minmax(6rem, ${col.flex}fr)`);
400
- } else {
401
- parts.push('minmax(6rem, 1fr)');
402
- }
403
- }
404
-
405
- return parts.join(' ');
406
- }
407
-
408
- // ── Data Processing Pipeline ───────────────────────────────────────────────
409
-
410
- /**
411
- * Apply search → column filters → sort → return array of original data indices.
412
- */
413
- #getProcessedIndices() {
414
- let indices = this.#data.map((_, i) => i);
415
-
416
- // 1. Global search filter
417
- if (this.search) {
418
- const q = this.search.toLowerCase();
419
- indices = indices.filter(i => {
420
- const row = this.#data[i];
421
- return this.#visibleColumns.some(col => {
422
- const val = getCellValue(row, col);
423
- return val != null && String(val).toLowerCase().includes(q);
424
- });
425
- });
426
- }
427
-
428
- // 2. Column filters
429
- if (this.#filters.size) {
430
- indices = indices.filter(i => {
431
- const row = this.#data[i];
432
- for (const [key, { op, value }] of this.#filters) {
433
- const col = this.#columns.find(c => c.key === key);
434
- const cellVal = getCellValue(row, col || { key });
435
- const str = String(cellVal ?? '').toLowerCase();
436
- const target = String(value).toLowerCase();
437
- switch (op) {
438
- case 'contains': if (!str.includes(target)) return false; break;
439
- case 'equals': if (str !== target) return false; break;
440
- case 'startsWith': if (!str.startsWith(target)) return false; break;
441
- case 'gt': if (Number(cellVal) <= Number(value)) return false; break;
442
- case 'lt': if (Number(cellVal) >= Number(value)) return false; break;
443
- case 'gte': if (Number(cellVal) < Number(value)) return false; break;
444
- case 'lte': if (Number(cellVal) > Number(value)) return false; break;
445
- case 'between': {
446
- const [lo, hi] = String(value).split(',').map(Number);
447
- const n = Number(cellVal);
448
- if (n < lo || n > hi) return false;
449
- break;
450
- }
451
- case 'select': {
452
- const selected = target.split(',').map(s => s.trim());
453
- if (!selected.includes(str)) return false;
454
- break;
455
- }
456
- default: if (!str.includes(target)) return false;
457
- }
458
- }
459
- return true;
460
- });
461
- }
462
-
463
- // 3. Multi-sort
464
- if (this.#sortState.length) {
465
- indices.sort((a, b) => {
466
- for (const { key, dir } of this.#sortState) {
467
- const col = this.#columns.find(c => c.key === key);
468
- const va = getCellValue(this.#data[a], col || { key });
469
- const vb = getCellValue(this.#data[b], col || { key });
470
-
471
- // Determine sort function
472
- let fn;
473
- if (typeof col?.sortFn === 'function') {
474
- fn = col.sortFn;
475
- } else {
476
- const typeDef = cellTypes[col?.type || 'text'];
477
- const fnName = col?.sortFn || typeDef?.sortFn || 'alphanumeric';
478
- fn = sortFns[fnName] || sortFns.alphanumeric;
479
- }
480
-
481
- // Nulls always sort last
482
- if (va == null && vb == null) continue;
483
- if (va == null) return 1;
484
- if (vb == null) return -1;
485
-
486
- const cmp = fn(va, vb);
487
- if (cmp !== 0) return dir === 'asc' ? cmp : -cmp;
488
- }
489
- return 0;
490
- });
491
- }
492
-
493
- return indices;
494
- }
495
-
496
- /**
497
- * Paginate a set of indices.
498
- */
499
- #getPageSlice(indices) {
500
- if (!this.paginate || this.paginate <= 0) return indices;
501
- const start = this.#page * this.paginate;
502
- return indices.slice(start, start + this.paginate);
503
- }
504
-
505
- get #pageCount() {
506
- if (!this.paginate || this.paginate <= 0) return 1;
507
- // pageCount should reflect filtered data, not raw data
508
- const filteredCount = this.#getProcessedIndices().length;
509
- return Math.max(1, Math.ceil(filteredCount / this.paginate));
510
- }
511
-
512
- // ── Render ─────────────────────────────────────────────────────────────────
513
-
514
- render() {
515
- const visCols = this.#visibleColumns;
516
-
517
- // Set grid template
518
- this.style.gridTemplateColumns = this.#buildGridTemplate();
519
-
520
- // ── Header row ──
521
-
522
- let header = this.querySelector(':scope > [data-header]');
523
- if (!header) {
524
- header = document.createElement('div');
525
- header.setAttribute('role', 'row');
526
- header.setAttribute('data-header', '');
527
- this.prepend(header);
528
- }
529
-
530
- this.#renderHeader(header, visCols);
531
-
532
- // ── Filter chips bar ──
533
-
534
- this.#renderFilterChips();
535
-
536
- // ── Body rowgroup ──
537
-
538
- let body = this.querySelector(':scope > [data-body]');
539
- if (!body) {
540
- body = document.createElement('div');
541
- body.setAttribute('role', 'rowgroup');
542
- body.setAttribute('data-body', '');
543
- header.after(body);
544
- }
545
-
546
- // Process data: search → sort → paginate
547
- const allProcessed = this.#getProcessedIndices();
548
- const pageIndices = this.#getPageSlice(allProcessed);
549
-
550
- // Reconcile body rows (with expansion support)
551
- const bodyChildren = [];
552
- for (const idx of pageIndices) {
553
- const existing = body.querySelector(`:scope > [role="row"][data-index="${idx}"]`);
554
- const row = existing || this.#createRow(idx, visCols);
555
- if (existing) this.#updateRow(existing, idx, visCols);
556
-
557
- // Expand toggle in first cell
558
- if (this.expandable) {
559
- const isExpanded = this.#expanded.has(idx);
560
- if (isExpanded) row.setAttribute('data-expanded', '');
561
- else row.removeAttribute('data-expanded');
562
- }
563
-
564
- bodyChildren.push(row);
565
-
566
- // Detail row (expansion)
567
- if (this.expandable && this.#expanded.has(idx)) {
568
- let detail = body.querySelector(`:scope > [data-detail-row][data-for="${idx}"]`);
569
- if (!detail) {
570
- detail = document.createElement('div');
571
- detail.setAttribute('data-detail-row', '');
572
- detail.dataset.for = idx;
573
- if (this.expandRenderer) {
574
- const content = this.expandRenderer(this.#data[idx], idx);
575
- if (content instanceof Node) detail.appendChild(content);
576
- else detail.textContent = String(content);
577
- }
578
- }
579
- bodyChildren.push(detail);
580
- }
581
- }
582
-
583
- // Reconcile: replace body contents
584
- const existingChildren = [...body.children];
585
- for (const child of existingChildren) {
586
- if (!bodyChildren.includes(child)) child.remove();
587
- }
588
- for (let i = 0; i < bodyChildren.length; i++) {
589
- if (body.children[i] !== bodyChildren[i]) {
590
- if (body.children[i]) body.insertBefore(bodyChildren[i], body.children[i]);
591
- else body.appendChild(bodyChildren[i]);
592
- }
593
- }
594
-
595
- // ── Overlays ──
596
-
597
- this.#renderOverlays(body);
598
-
599
- // ── Aggregation footer ──
600
-
601
- this.#renderAggregation(body, allProcessed, visCols);
602
-
603
- // ── Pagination footer ──
604
-
605
- const showPagination = this.paginate > 0 && this.#data.length > 0;
606
- let footer = this.querySelector(':scope > [data-footer]');
607
-
608
- if (showPagination) {
609
- if (!footer) {
610
- footer = document.createElement('div');
611
- footer.setAttribute('data-footer', '');
612
- this.appendChild(footer);
613
- }
614
- this.#renderPagination(footer, allProcessed.length);
615
- } else if (footer) {
616
- footer.remove();
617
- }
618
- }
619
-
620
- // ── Header Rendering ───────────────────────────────────────────────────────
621
-
622
- #renderHeader(header, visCols) {
623
- const allSelected = this.#data.length > 0 && this.#selected.size === this.#data.length;
624
- const cells = [];
625
-
626
- // Checkbox column header
627
- if (this.selectable) {
628
- const cell = document.createElement('div');
629
- cell.setAttribute('role', 'columnheader');
630
- cell.setAttribute('data-check-col', '');
631
- const check = document.createElement('check-ui');
632
- if (allSelected) check.setAttribute('checked', '');
633
- check.setAttribute('aria-label', 'Select all rows');
634
- cell.appendChild(check);
635
- cells.push(cell);
636
- }
637
-
638
- // Data columns
639
- for (const col of visCols) {
640
- const cell = document.createElement('div');
641
- cell.setAttribute('role', 'columnheader');
642
-
643
- // Label
644
- const label = document.createElement('span');
645
- label.textContent = col.label || col.key;
646
- cell.appendChild(label);
647
-
648
- // Sortable
649
- const colSortable = col.sortable !== false && this.sortable;
650
- if (colSortable) {
651
- cell.dataset.sortKey = col.key;
652
- cell.setAttribute('aria-label', `Sort by ${col.label || col.key}`);
653
-
654
- const icon = document.createElement('icon-ui');
655
- icon.setAttribute('data-sort-icon', '');
656
-
657
- const sortEntry = this.#sortState.find(s => s.key === col.key);
658
- if (sortEntry) {
659
- cell.setAttribute('aria-sort', sortEntry.dir === 'asc' ? 'ascending' : 'descending');
660
- icon.setAttribute('name', sortEntry.dir === 'asc' ? 'arrow-up' : 'arrow-down');
661
- } else {
662
- icon.setAttribute('name', 'caret-up-down');
663
- }
664
-
665
- cell.appendChild(icon);
666
- }
667
-
668
- // Pinned
669
- if (col.pinned) {
670
- cell.setAttribute('data-pinned', col.pinned);
671
- }
672
-
673
- // Resize handle
674
- if (col.resizable !== false) {
675
- const handle = document.createElement('div');
676
- handle.setAttribute('data-resize-handle', '');
677
- handle.dataset.resizeKey = col.key;
678
- cell.appendChild(handle);
679
- }
680
-
681
- // Filter button
682
- if (col.filter) {
683
- const filterBtn = document.createElement('button');
684
- filterBtn.setAttribute('data-filter-btn', '');
685
- filterBtn.dataset.filterKey = col.key;
686
- filterBtn.setAttribute('aria-label', `Filter ${col.label || col.key}`);
687
- const filterIcon = document.createElement('icon-ui');
688
- filterIcon.setAttribute('name', this.#filters.has(col.key) ? 'funnel-simple-fill' : 'funnel-simple');
689
- filterIcon.setAttribute('size', 'xs');
690
- filterBtn.appendChild(filterIcon);
691
- cell.appendChild(filterBtn);
692
-
693
- // Open filter dropdown
694
- if (this.#openFilter === col.key) {
695
- const dropdown = this.#buildFilterDropdown(col);
696
- cell.appendChild(dropdown);
697
- cell.setAttribute('data-filter-open', '');
698
- }
699
- }
700
-
701
- cells.push(cell);
702
- }
703
-
704
- // Reconcile header cells
705
- while (header.children.length > cells.length) header.lastChild.remove();
706
- for (let i = 0; i < cells.length; i++) {
707
- if (header.children[i]) header.replaceChild(cells[i], header.children[i]);
708
- else header.appendChild(cells[i]);
709
- }
710
- }
711
-
712
- // ── Row Builders ───────────────────────────────────────────────────────────
713
-
714
- #createRow(dataIndex, visCols) {
715
- const row = document.createElement('div');
716
- row.setAttribute('role', 'row');
717
- this.#updateRow(row, dataIndex, visCols);
718
- return row;
719
- }
720
-
721
- #updateRow(row, dataIndex, visCols) {
722
- const data = this.#data[dataIndex];
723
- const isSelected = this.#selected.has(dataIndex);
724
- row.dataset.index = dataIndex;
725
-
726
- if (isSelected) {
727
- row.setAttribute('data-selected', '');
728
- row.setAttribute('aria-selected', 'true');
729
- } else {
730
- row.removeAttribute('data-selected');
731
- row.removeAttribute('aria-selected');
732
- }
733
-
734
- const cells = [];
735
-
736
- // Expand toggle cell
737
- if (this.expandable) {
738
- const cell = document.createElement('div');
739
- cell.setAttribute('role', 'gridcell');
740
- cell.setAttribute('data-expand-col', '');
741
- const btn = document.createElement('button');
742
- btn.setAttribute('data-expand-toggle', '');
743
- btn.setAttribute('aria-label', 'Expand row');
744
- const icon = document.createElement('icon-ui');
745
- icon.setAttribute('name', 'caret-right');
746
- icon.setAttribute('size', 'xs');
747
- btn.appendChild(icon);
748
- cell.appendChild(btn);
749
- cells.push(cell);
750
- }
751
-
752
- // Checkbox cell
753
- if (this.selectable) {
754
- const cell = document.createElement('div');
755
- cell.setAttribute('role', 'gridcell');
756
- cell.setAttribute('data-check-col', '');
757
- const check = document.createElement('check-ui');
758
- if (isSelected) check.setAttribute('checked', '');
759
- check.setAttribute('aria-label', `Select row ${dataIndex + 1}`);
760
- cell.appendChild(check);
761
- cells.push(cell);
762
- }
763
-
764
- // Data cells
765
- for (const col of visCols) {
766
- const cell = document.createElement('div');
767
- cell.setAttribute('role', 'gridcell');
768
- cell.dataset.key = col.key;
769
-
770
- const value = getCellValue(data, col);
771
-
772
- // Render priority: column.render > column.format > cellType.render > text
773
- if (typeof col.render === 'function') {
774
- const result = col.render(value, data, cell, dataIndex);
775
- if (result instanceof Node) {
776
- cell.replaceChildren(result);
777
- } else if (typeof result === 'string') {
778
- cell.innerHTML = result;
779
- }
780
- } else if (typeof col.format === 'function') {
781
- cell.textContent = col.format(value, data);
782
- } else {
783
- const typeDef = cellTypes[col.type || 'text'];
784
- if (typeDef?.render) {
785
- typeDef.render(value, data, cell, col.meta);
786
- } else {
787
- cell.textContent = value != null ? value : '';
788
- }
789
- }
790
-
791
- // Alignment from cell type
792
- const typeDef = cellTypes[col.type || 'text'];
793
- if (typeDef?.align) {
794
- cell.dataset.align = typeDef.align;
795
- }
796
-
797
- // Pinned
798
- if (col.pinned) {
799
- cell.setAttribute('data-pinned', col.pinned);
800
- }
801
-
802
- cells.push(cell);
803
- }
804
-
805
- // Reconcile cells in row
806
- while (row.children.length > cells.length) row.lastChild.remove();
807
- for (let i = 0; i < cells.length; i++) {
808
- if (row.children[i]) row.replaceChild(cells[i], row.children[i]);
809
- else row.appendChild(cells[i]);
810
- }
811
- }
812
-
813
- // ── Overlays ───────────────────────────────────────────────────────────────
814
-
815
- #renderOverlays(body) {
816
- let emptyEl = this.querySelector(':scope > [data-empty]');
817
- let loadingEl = this.querySelector(':scope > [data-loading]');
818
-
819
- if (this.loading) {
820
- // Show loading overlay
821
- if (!loadingEl) {
822
- loadingEl = document.createElement('div');
823
- loadingEl.setAttribute('data-loading', '');
824
- const prog = document.createElement('progress-ui');
825
- prog.setAttribute('indeterminate', '');
826
- loadingEl.appendChild(prog);
827
- body.after(loadingEl);
828
- }
829
- if (emptyEl) emptyEl.remove();
830
- } else if (this.#data.length === 0) {
831
- // Show empty state
832
- if (!emptyEl) {
833
- emptyEl = document.createElement('div');
834
- emptyEl.setAttribute('data-empty', '');
835
- const icon = document.createElement('icon-ui');
836
- icon.setAttribute('name', 'table');
837
- const span = document.createElement('span');
838
- span.textContent = 'No data';
839
- emptyEl.appendChild(icon);
840
- emptyEl.appendChild(span);
841
- body.after(emptyEl);
842
- }
843
- if (loadingEl) loadingEl.remove();
844
- } else {
845
- // Remove both overlays
846
- if (emptyEl) emptyEl.remove();
847
- if (loadingEl) loadingEl.remove();
848
- }
849
- }
850
-
851
- // ── Aggregation ────────────────────────────────────────────────────────────
852
-
853
- #renderAggregation(body, processedIndices, visCols) {
854
- const hasAgg = visCols.some(c => c.aggregate);
855
- let aggRow = this.querySelector(':scope > [data-agg-row]');
856
-
857
- if (!hasAgg || this.#data.length === 0) {
858
- if (aggRow) aggRow.remove();
859
- return;
860
- }
861
-
862
- if (!aggRow) {
863
- aggRow = document.createElement('div');
864
- aggRow.setAttribute('role', 'row');
865
- aggRow.setAttribute('data-agg-row', '');
866
- body.after(aggRow);
867
- }
868
-
869
- const cells = [];
870
-
871
- // Expand column spacer
872
- if (this.expandable) {
873
- const spacer = document.createElement('div');
874
- spacer.setAttribute('role', 'gridcell');
875
- cells.push(spacer);
876
- }
877
-
878
- // Checkbox column spacer
879
- if (this.selectable) {
880
- const spacer = document.createElement('div');
881
- spacer.setAttribute('role', 'gridcell');
882
- cells.push(spacer);
883
- }
884
-
885
- for (const col of visCols) {
886
- const cell = document.createElement('div');
887
- cell.setAttribute('role', 'gridcell');
888
-
889
- if (col.aggregate) {
890
- const values = processedIndices.map(i => {
891
- const val = getCellValue(this.#data[i], col);
892
- return val != null ? Number(val) : NaN;
893
- }).filter(n => !isNaN(n));
894
-
895
- let result;
896
- switch (col.aggregate) {
897
- case 'sum': result = values.reduce((a, b) => a + b, 0); break;
898
- case 'avg': result = values.length ? values.reduce((a, b) => a + b, 0) / values.length : 0; break;
899
- case 'min': result = values.length ? Math.min(...values) : 0; break;
900
- case 'max': result = values.length ? Math.max(...values) : 0; break;
901
- case 'count': result = processedIndices.length; break;
902
- default: result = '';
903
- }
904
-
905
- // Format using cell type if available
906
- const typeDef = cellTypes[col.type || 'text'];
907
- if (typeof result === 'number' && typeDef?.render) {
908
- typeDef.render(result, {}, cell, col.meta);
909
- } else {
910
- cell.textContent = typeof result === 'number' ? result.toLocaleString() : result;
911
- }
912
- }
913
-
914
- cells.push(cell);
915
- }
916
-
917
- // Reconcile cells
918
- while (aggRow.children.length > cells.length) aggRow.lastChild.remove();
919
- for (let i = 0; i < cells.length; i++) {
920
- if (aggRow.children[i]) aggRow.replaceChild(cells[i], aggRow.children[i]);
921
- else aggRow.appendChild(cells[i]);
922
- }
923
- }
924
-
925
- // ── Pagination ─────────────────────────────────────────────────────────────
926
-
927
- #renderPagination(footer, filteredTotal) {
928
- let pag = footer.querySelector('pagination-ui');
929
- if (!pag) {
930
- footer.innerHTML = '';
931
- pag = document.createElement('pagination-ui');
932
- pag.addEventListener('page-change', (e) => {
933
- this.#page = e.detail.page - 1; // pagination-ui is 1-based
934
- this.render();
935
- this.dispatchEvent(new CustomEvent('page', {
936
- bubbles: true,
937
- detail: { page: this.#page },
938
- }));
939
- });
940
- footer.appendChild(pag);
941
- }
942
- const pageCount = this.paginate > 0
943
- ? Math.max(1, Math.ceil(filteredTotal / this.paginate))
944
- : 1;
945
- pag.setAttribute('page', String(this.#page + 1));
946
- pag.setAttribute('total', String(pageCount));
947
- }
948
-
949
- // ── Filter UI ──────────────────────────────────────────────────────────────
950
-
951
- #buildFilterDropdown(col) {
952
- const dropdown = document.createElement('div');
953
- dropdown.setAttribute('data-filter-dropdown', '');
954
-
955
- const currentFilter = this.#filters.get(col.key);
956
-
957
- if (col.filter === 'select') {
958
- // Build checkbox list from unique values
959
- const uniqueVals = [...new Set(this.#data.map(row => {
960
- const val = getCellValue(row, col);
961
- return val != null ? String(val) : '';
962
- }))].filter(Boolean).sort();
963
-
964
- const selectedSet = new Set(
965
- currentFilter?.op === 'select' ? String(currentFilter.value).split(',') : []
966
- );
967
-
968
- for (const val of uniqueVals) {
969
- const label = document.createElement('label');
970
- label.setAttribute('data-filter-option', '');
971
- const check = document.createElement('input');
972
- check.type = 'checkbox';
973
- check.value = val;
974
- check.checked = selectedSet.has(val.toLowerCase());
975
- check.addEventListener('change', () => {
976
- const checked = [...dropdown.querySelectorAll('input[type=checkbox]:checked')].map(c => c.value);
977
- if (checked.length) {
978
- this.setFilter(col.key, checked.join(','), 'select');
979
- } else {
980
- this.setFilter(col.key, null);
981
- }
982
- });
983
- label.appendChild(check);
984
- label.appendChild(document.createTextNode(` ${val}`));
985
- dropdown.appendChild(label);
986
- }
987
- } else if (col.filter === 'number') {
988
- const input = document.createElement('input');
989
- input.type = 'number';
990
- input.placeholder = 'Filter...';
991
- input.value = currentFilter?.value ?? '';
992
- input.setAttribute('data-filter-input', '');
993
-
994
- const opSelect = document.createElement('select');
995
- opSelect.setAttribute('data-filter-op', '');
996
- for (const [val, label] of [['gte', '≥'], ['lte', '≤'], ['gt', '>'], ['lt', '<'], ['equals', '=']]) {
997
- const opt = document.createElement('option');
998
- opt.value = val;
999
- opt.textContent = label;
1000
- if (currentFilter?.op === val) opt.selected = true;
1001
- opSelect.appendChild(opt);
1002
- }
1003
-
1004
- const apply = () => {
1005
- if (input.value) this.setFilter(col.key, input.value, opSelect.value);
1006
- else this.setFilter(col.key, null);
1007
- };
1008
- input.addEventListener('input', apply);
1009
- opSelect.addEventListener('change', apply);
1010
-
1011
- dropdown.appendChild(opSelect);
1012
- dropdown.appendChild(input);
1013
- } else {
1014
- // Default: text filter
1015
- const input = document.createElement('input');
1016
- input.type = 'text';
1017
- input.placeholder = `Filter ${col.label || col.key}...`;
1018
- input.value = currentFilter?.value ?? '';
1019
- input.setAttribute('data-filter-input', '');
1020
- input.addEventListener('input', () => {
1021
- if (input.value) this.setFilter(col.key, input.value, 'contains');
1022
- else this.setFilter(col.key, null);
1023
- });
1024
- dropdown.appendChild(input);
1025
- }
1026
-
1027
- // Clear button
1028
- if (currentFilter) {
1029
- const clearBtn = document.createElement('button');
1030
- clearBtn.textContent = 'Clear';
1031
- clearBtn.setAttribute('data-filter-clear', '');
1032
- clearBtn.addEventListener('click', () => {
1033
- this.setFilter(col.key, null);
1034
- this.#openFilter = null;
1035
- this.#requestRender();
1036
- });
1037
- dropdown.appendChild(clearBtn);
1038
- }
1039
-
1040
- // Auto-focus input
1041
- if (this.#filterFocusRaf != null) cancelAnimationFrame(this.#filterFocusRaf);
1042
- this.#filterFocusRaf = requestAnimationFrame(() => {
1043
- this.#filterFocusRaf = null;
1044
- if (!this.isConnected) return;
1045
- const input = dropdown.querySelector('input');
1046
- if (input) input.focus();
1047
- });
1048
-
1049
- return dropdown;
1050
- }
1051
-
1052
- #renderFilterChips() {
1053
- let bar = this.querySelector(':scope > [data-filter-bar]');
1054
-
1055
- if (this.#filters.size === 0) {
1056
- if (bar) bar.remove();
1057
- return;
1058
- }
1059
-
1060
- if (!bar) {
1061
- bar = document.createElement('div');
1062
- bar.setAttribute('data-filter-bar', '');
1063
- bar.setAttribute('role', 'status');
1064
- // Insert after header
1065
- const header = this.querySelector(':scope > [data-header]');
1066
- if (header) header.after(bar);
1067
- else this.prepend(bar);
1068
- }
1069
-
1070
- bar.innerHTML = '';
1071
- for (const [key, { op, value }] of this.#filters) {
1072
- const col = this.#columns.find(c => c.key === key);
1073
- const label = col?.label || key;
1074
- const chip = document.createElement('badge-ui');
1075
- const displayVal = op === 'select' ? `${value.split(',').length} selected` : value;
1076
- chip.setAttribute('text', `${label}: ${displayVal}`);
1077
- chip.setAttribute('size', 'xs');
1078
- chip.setAttribute('data-filter-chip', key);
1079
- // Dismiss button
1080
- const dismiss = document.createElement('button');
1081
- dismiss.textContent = '×';
1082
- dismiss.setAttribute('data-chip-dismiss', '');
1083
- dismiss.addEventListener('click', (e) => {
1084
- e.stopPropagation();
1085
- this.setFilter(key, null);
1086
- });
1087
- chip.appendChild(dismiss);
1088
- bar.appendChild(chip);
1089
- }
1090
-
1091
- // Clear all button
1092
- const clearAll = document.createElement('button');
1093
- clearAll.textContent = 'Clear all';
1094
- clearAll.setAttribute('data-filter-clear-all', '');
1095
- clearAll.addEventListener('click', () => this.clearFilters());
1096
- bar.appendChild(clearAll);
1097
- }
1098
-
1099
- // ── Event Handling: Click ──────────────────────────────────────────────────
1100
-
1101
- #onClick = (e) => {
1102
- const target = e.target;
1103
-
1104
- // ── Filter button click ──
1105
- const filterBtn = target.closest('[data-filter-btn]');
1106
- if (filterBtn && this.contains(filterBtn)) {
1107
- const key = filterBtn.dataset.filterKey;
1108
- this.#openFilter = this.#openFilter === key ? null : key;
1109
- this.#requestRender();
1110
- return;
1111
- }
1112
-
1113
- // ── Click outside filter dropdown closes it ──
1114
- if (this.#openFilter && !target.closest('[data-filter-dropdown]') && !target.closest('[data-filter-btn]')) {
1115
- this.#openFilter = null;
1116
- this.#requestRender();
1117
- }
1118
-
1119
- // ── Expand toggle click ──
1120
- const expandToggle = target.closest('[data-expand-toggle]');
1121
- if (expandToggle && this.expandable && this.contains(expandToggle)) {
1122
- const row = expandToggle.closest('[role="row"]');
1123
- if (row?.dataset?.index != null) {
1124
- this.toggleExpand(Number(row.dataset.index));
1125
- }
1126
- return;
1127
- }
1128
-
1129
- // ── Resize handle mousedown is handled separately ──
1130
- const resizeHandle = target.closest('[data-resize-handle]');
1131
- if (resizeHandle && this.contains(resizeHandle)) {
1132
- this.#startResize(e, resizeHandle);
1133
- return;
1134
- }
1135
-
1136
- // ── Header sort click ──
1137
- const headerCell = target.closest('[data-sort-key]');
1138
- if (headerCell && this.sortable && this.contains(headerCell)) {
1139
- const key = headerCell.dataset.sortKey;
1140
- const existing = this.#sortState.findIndex(s => s.key === key);
1141
- const col = this.#columns.find(c => c.key === key);
1142
- const defaultDir = col?.sortDescFirst ? 'desc' : 'asc';
1143
- const altDir = defaultDir === 'asc' ? 'desc' : 'asc';
1144
-
1145
- if (e.shiftKey) {
1146
- // Multi-sort: add or toggle
1147
- if (existing >= 0) {
1148
- const cur = this.#sortState[existing];
1149
- if (cur.dir === altDir) {
1150
- // Remove from sort state (third click)
1151
- this.#sortState.splice(existing, 1);
1152
- } else {
1153
- cur.dir = cur.dir === defaultDir ? altDir : defaultDir;
1154
- }
1155
- } else {
1156
- this.#sortState.push({ key, dir: defaultDir });
1157
- }
1158
- } else {
1159
- // Single sort: replace
1160
- if (existing >= 0 && this.#sortState.length === 1) {
1161
- const cur = this.#sortState[0];
1162
- if (cur.dir === altDir) {
1163
- this.#sortState = [];
1164
- } else {
1165
- cur.dir = cur.dir === defaultDir ? altDir : defaultDir;
1166
- }
1167
- } else {
1168
- this.#sortState = [{ key, dir: defaultDir }];
1169
- }
1170
- }
1171
-
1172
- this.render();
1173
- this.dispatchEvent(new CustomEvent('sort', {
1174
- bubbles: true,
1175
- detail: {
1176
- key,
1177
- dir: this.#sortState.find(s => s.key === key)?.dir || null,
1178
- sortState: this.sortState,
1179
- },
1180
- }));
1181
- return;
1182
- }
1183
-
1184
- // ── Header checkbox (select all) ──
1185
- const headerCheck = target.closest('[data-header] check-ui');
1186
- if (headerCheck && this.selectable) {
1187
- const checked = headerCheck.hasAttribute('checked');
1188
- if (checked) {
1189
- for (let i = 0; i < this.#data.length; i++) this.#selected.add(i);
1190
- } else {
1191
- this.#selected.clear();
1192
- }
1193
- this.render();
1194
- this.dispatchEvent(new CustomEvent('select', {
1195
- bubbles: true, detail: { selected: this.selected },
1196
- }));
1197
- return;
1198
- }
1199
-
1200
- // ── Row checkbox ──
1201
- const rowCheck = target.closest('[role="row"]:not([data-header]) check-ui');
1202
- if (rowCheck && this.selectable) {
1203
- const row = rowCheck.closest('[role="row"]');
1204
- const idx = row ? +row.dataset.index : -1;
1205
- if (idx >= 0) {
1206
- const checked = rowCheck.hasAttribute('checked');
1207
-
1208
- if (e.shiftKey && this.#lastSelectedIndex >= 0) {
1209
- // Range select
1210
- const from = Math.min(this.#lastSelectedIndex, idx);
1211
- const to = Math.max(this.#lastSelectedIndex, idx);
1212
- for (let i = from; i <= to; i++) {
1213
- if (checked) this.#selected.add(i);
1214
- else this.#selected.delete(i);
1215
- }
1216
- } else {
1217
- if (checked) this.#selected.add(idx);
1218
- else this.#selected.delete(idx);
1219
- }
1220
-
1221
- this.#lastSelectedIndex = idx;
1222
- this.render();
1223
- this.dispatchEvent(new CustomEvent('select', {
1224
- bubbles: true, detail: { selected: this.selected },
1225
- }));
1226
- }
1227
- return;
1228
- }
1229
-
1230
- // ── Cell click ──
1231
- const gridcell = target.closest('[role="gridcell"][data-key]');
1232
- if (gridcell && this.contains(gridcell)) {
1233
- const row = gridcell.closest('[role="row"]');
1234
- const dataIndex = row ? +row.dataset.index : -1;
1235
- if (dataIndex >= 0) {
1236
- const key = gridcell.dataset.key;
1237
- const rowData = this.#data[dataIndex];
1238
- const col = this.#columns.find(c => c.key === key);
1239
- const value = col ? getCellValue(rowData, col) : undefined;
1240
- this.dispatchEvent(new CustomEvent('cell-click', {
1241
- bubbles: true,
1242
- detail: { key, row: rowData, value, dataIndex },
1243
- }));
1244
- }
1245
- }
1246
- };
1247
-
1248
- // ── Column Resize ──────────────────────────────────────────────────────────
1249
-
1250
- #resizeState = null;
1251
-
1252
- #startResize(e, handle) {
1253
- e.preventDefault();
1254
- e.stopPropagation();
1255
-
1256
- const key = handle.dataset.resizeKey;
1257
- const col = this.#columns.find(c => c.key === key);
1258
- if (!col) return;
1259
-
1260
- // Get current column width
1261
- const headerCell = handle.closest('[role="columnheader"]');
1262
- const startWidth = headerCell ? headerCell.getBoundingClientRect().width : 100;
1263
- const startX = e.clientX;
1264
-
1265
- this.setAttribute('data-resizing', '');
1266
-
1267
- this.#resizeState = { key, col, startX, startWidth };
1268
-
1269
- document.addEventListener('mousemove', this.#onResizeMove);
1270
- document.addEventListener('mouseup', this.#onResizeEnd);
1271
- }
1272
-
1273
- #onResizeMove = (e) => {
1274
- if (!this.#resizeState) return;
1275
- const { key, col, startX, startWidth } = this.#resizeState;
1276
- const delta = e.clientX - startX;
1277
- let newWidth = startWidth + delta;
1278
-
1279
- // Clamp to min/max
1280
- const min = col.minWidth || 48;
1281
- const max = col.maxWidth || Infinity;
1282
- newWidth = Math.max(min, Math.min(max, newWidth));
1283
-
1284
- this.#columnWidths.set(key, Math.round(newWidth));
1285
- this.style.gridTemplateColumns = this.#buildGridTemplate();
1286
- };
1287
-
1288
- #onResizeEnd = (e) => {
1289
- if (!this.#resizeState) return;
1290
- const { key } = this.#resizeState;
1291
-
1292
- document.removeEventListener('mousemove', this.#onResizeMove);
1293
- document.removeEventListener('mouseup', this.#onResizeEnd);
1294
-
1295
- this.removeAttribute('data-resizing');
1296
-
1297
- const width = this.#columnWidths.get(key);
1298
- this.#resizeState = null;
1299
-
1300
- this.dispatchEvent(new CustomEvent('resize', {
1301
- bubbles: true,
1302
- detail: { key, width },
1303
- }));
1304
-
1305
- this.render();
1306
- };
1307
-
1308
- #cleanupResize() {
1309
- document.removeEventListener('mousemove', this.#onResizeMove);
1310
- document.removeEventListener('mouseup', this.#onResizeEnd);
1311
- this.#resizeState = null;
1312
- }
1313
-
1314
- // ── Keyboard Navigation ────────────────────────────────────────────────────
1315
-
1316
- #onKeydown = (e) => {
1317
- const visCols = this.#visibleColumns;
1318
- const totalCols = visCols.length + (this.selectable ? 1 : 0);
1319
- const body = this.querySelector(':scope > [data-body]');
1320
- const totalRows = body ? body.children.length : 0;
1321
-
1322
- // Include header as row -1 conceptually, body rows 0..n-1
1323
- if (!this.#focusedCell) {
1324
- this.#focusedCell = { row: 0, col: 0 };
1325
- }
1326
-
1327
- let { row, col } = this.#focusedCell;
1328
- let handled = true;
1329
-
1330
- switch (e.key) {
1331
- case 'ArrowDown':
1332
- row = Math.min(row + 1, totalRows - 1);
1333
- break;
1334
- case 'ArrowUp':
1335
- row = Math.max(row - 1, -1); // -1 = header
1336
- break;
1337
- case 'ArrowRight':
1338
- col = Math.min(col + 1, totalCols - 1);
1339
- break;
1340
- case 'ArrowLeft':
1341
- col = Math.max(col - 1, 0);
1342
- break;
1343
- case 'Tab':
1344
- if (e.shiftKey) {
1345
- col--;
1346
- if (col < 0) { col = totalCols - 1; row--; }
1347
- } else {
1348
- col++;
1349
- if (col >= totalCols) { col = 0; row++; }
1350
- }
1351
- if (row < -1 || row >= totalRows) { handled = false; break; }
1352
- break;
1353
- case 'Enter':
1354
- // If on header row, trigger sort
1355
- if (row === -1) {
1356
- const header = this.querySelector(':scope > [data-header]');
1357
- const cell = header?.children[col];
1358
- const sortKey = cell?.dataset.sortKey;
1359
- if (sortKey) cell.click();
1360
- }
1361
- break;
1362
- default:
1363
- handled = false;
1364
- }
1365
-
1366
- if (handled) {
1367
- e.preventDefault();
1368
- this.#focusedCell = { row, col };
1369
- this.#updateFocus();
1370
- }
1371
- };
1372
-
1373
- #updateFocus() {
1374
- // Remove old focus
1375
- const oldFocused = this.querySelector('[data-focused]');
1376
- if (oldFocused) oldFocused.removeAttribute('data-focused');
1377
-
1378
- if (!this.#focusedCell) return;
1379
- const { row, col } = this.#focusedCell;
1380
-
1381
- let cell;
1382
- if (row === -1) {
1383
- // Header
1384
- const header = this.querySelector(':scope > [data-header]');
1385
- cell = header?.children[col];
1386
- } else {
1387
- // Body
1388
- const body = this.querySelector(':scope > [data-body]');
1389
- const rowEl = body?.children[row];
1390
- cell = rowEl?.children[col];
1391
- }
1392
-
1393
- if (cell) {
1394
- cell.setAttribute('data-focused', '');
1395
- cell.scrollIntoView?.({ block: 'nearest', inline: 'nearest' });
1396
- }
1397
- }
1398
-
1399
- // ── CSV Export ─────────────────────────────────────────────────────────────
1400
-
1401
- exportCSV(filename = 'export.csv') {
1402
- const visCols = this.#visibleColumns;
1403
- const processed = this.#getProcessedIndices();
1404
-
1405
- // Header row
1406
- const headerRow = visCols.map(c => csvEscape(c.label || c.key));
1407
-
1408
- // Data rows
1409
- const dataRows = processed.map(idx => {
1410
- const row = this.#data[idx];
1411
- return visCols.map(col => {
1412
- const value = getCellValue(row, col);
1413
-
1414
- // Use column format, then cell type format, then String
1415
- if (typeof col.format === 'function') {
1416
- return csvEscape(col.format(value, row));
1417
- }
1418
- const typeDef = cellTypes[col.type || 'text'];
1419
- if (typeDef?.format) {
1420
- return csvEscape(typeDef.format(value, row, col.meta));
1421
- }
1422
- return csvEscape(value);
1423
- });
1424
- });
1425
-
1426
- const csv = [headerRow.join(','), ...dataRows.map(r => r.join(','))].join('\n');
1427
11
 
1428
- // Trigger download
1429
- const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
1430
- const url = URL.createObjectURL(blob);
1431
- const link = document.createElement('a');
1432
- link.href = url;
1433
- link.download = filename;
1434
- link.style.display = 'none';
1435
- document.body.appendChild(link);
1436
- link.click();
1437
- document.body.removeChild(link);
1438
- URL.revokeObjectURL(url);
1439
- }
1440
- }
12
+ import { defineIfFree } from '../../core/register.js';
13
+ import { UITable } from './class.js';
1441
14
 
1442
- customElements.define('table-ui', UITable);
15
+ defineIfFree('table-ui', UITable);
1443
16
 
1444
17
  export { UITable };