@icen.ai/ui 0.2.1 → 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 (103) hide show
  1. package/README.md +29 -16
  2. package/dist/base.css +389 -48
  3. package/dist/behaviors/accordion.mjs +20 -0
  4. package/dist/behaviors/carousel.mjs +30 -1
  5. package/dist/behaviors/charts.mjs +168 -0
  6. package/dist/behaviors/copy.mjs +4 -3
  7. package/dist/behaviors/datatable.mjs +748 -0
  8. package/dist/behaviors/input.mjs +20 -1
  9. package/dist/behaviors/modal.mjs +14 -0
  10. package/dist/behaviors/nav.mjs +31 -4
  11. package/dist/behaviors/sidebar.mjs +12 -0
  12. package/dist/behaviors/slider.mjs +35 -3
  13. package/dist/behaviors/tabs.mjs +100 -9
  14. package/dist/behaviors/tag-input.mjs +11 -0
  15. package/dist/behaviors/theme.mjs +26 -0
  16. package/dist/behaviors/toast.mjs +83 -15
  17. package/dist/behaviors/tree.mjs +57 -1
  18. package/dist/behaviors/upload.mjs +2 -3
  19. package/dist/cli.mjs +74 -0
  20. package/dist/components/accordion.mjs +3 -0
  21. package/dist/components/alert.mjs +2 -0
  22. package/dist/components/avatar.mjs +2 -0
  23. package/dist/components/breadcrumb.css +91 -18
  24. package/dist/components/breadcrumb.mjs +2 -0
  25. package/dist/components/btn.css +174 -14
  26. package/dist/components/btn.mjs +2 -0
  27. package/dist/components/card.css +118 -18
  28. package/dist/components/card.mjs +2 -0
  29. package/dist/components/carousel.mjs +3 -0
  30. package/dist/components/charts.css +225 -33
  31. package/dist/components/charts.mjs +3 -0
  32. package/dist/components/content.css +256 -74
  33. package/dist/components/context-menu.mjs +3 -0
  34. package/dist/components/datatable.css +343 -0
  35. package/dist/components/datatable.mjs +4 -0
  36. package/dist/components/desc.mjs +2 -0
  37. package/dist/components/dropdown.mjs +3 -0
  38. package/dist/components/empty.css +50 -10
  39. package/dist/components/empty.mjs +2 -0
  40. package/dist/components/feedback.css +158 -47
  41. package/dist/components/form.css +139 -22
  42. package/dist/components/input.css +165 -45
  43. package/dist/components/input.mjs +3 -0
  44. package/dist/components/kbd.mjs +2 -0
  45. package/dist/components/layout.mjs +2 -0
  46. package/dist/components/list.mjs +2 -0
  47. package/dist/components/media-card.mjs +2 -0
  48. package/dist/components/media.css +187 -40
  49. package/dist/components/menu.css +159 -32
  50. package/dist/components/modal.css +140 -34
  51. package/dist/components/modal.mjs +3 -0
  52. package/dist/components/nav.css +134 -32
  53. package/dist/components/nav.mjs +3 -0
  54. package/dist/components/pagination.css +105 -19
  55. package/dist/components/pagination.mjs +2 -0
  56. package/dist/components/panel.css +61 -6
  57. package/dist/components/panel.mjs +2 -0
  58. package/dist/components/pill.css +74 -14
  59. package/dist/components/pill.mjs +2 -0
  60. package/dist/components/popover.css +109 -7
  61. package/dist/components/popover.mjs +3 -0
  62. package/dist/components/progress.mjs +2 -0
  63. package/dist/components/rating.mjs +2 -0
  64. package/dist/components/result.mjs +2 -0
  65. package/dist/components/segmented.css +199 -50
  66. package/dist/components/segmented.mjs +2 -0
  67. package/dist/components/select.css +140 -39
  68. package/dist/components/select.mjs +3 -0
  69. package/dist/components/sidebar.css +162 -34
  70. package/dist/components/sidebar.mjs +3 -0
  71. package/dist/components/skeleton.mjs +2 -0
  72. package/dist/components/slider.css +169 -31
  73. package/dist/components/slider.mjs +3 -0
  74. package/dist/components/spinner.mjs +2 -0
  75. package/dist/components/stat.css +103 -8
  76. package/dist/components/stat.mjs +2 -0
  77. package/dist/components/steps.css +105 -31
  78. package/dist/components/steps.mjs +2 -0
  79. package/dist/components/switch.css +171 -54
  80. package/dist/components/switch.mjs +2 -0
  81. package/dist/components/table.css +114 -12
  82. package/dist/components/table.mjs +2 -0
  83. package/dist/components/tabs.css +104 -17
  84. package/dist/components/tabs.mjs +3 -0
  85. package/dist/components/tag-input.css +119 -23
  86. package/dist/components/tag-input.mjs +3 -0
  87. package/dist/components/timeline.mjs +2 -0
  88. package/dist/components/toast.css +85 -12
  89. package/dist/components/toast.mjs +3 -0
  90. package/dist/components/tooltip.css +86 -26
  91. package/dist/components/tooltip.mjs +2 -0
  92. package/dist/components/tree.mjs +3 -0
  93. package/dist/components/upload.css +144 -17
  94. package/dist/components/upload.mjs +3 -0
  95. package/dist/index.mjs +623 -42
  96. package/dist/registry.json +197 -2
  97. package/dist/retro-effects.css +264 -0
  98. package/dist/tokens/colors.css +18 -90
  99. package/dist/tokens/retro-effects.css +264 -0
  100. package/dist/tokens/style-profiles.css +47 -47
  101. package/dist/tokens.css +65 -137
  102. package/dist/ui.css +4861 -1019
  103. package/package.json +9 -1
@@ -0,0 +1,748 @@
1
+ // src/behaviors/context-menu.ts
2
+ var FLIP_MARGIN = 10;
3
+ var CLAMP_MARGIN = 8;
4
+ var panel = null;
5
+ var focusableItems = [];
6
+ var focusedIndex = -1;
7
+ function buildItem(item) {
8
+ if ("type" in item && item.type === "separator") {
9
+ const sep = document.createElement("div");
10
+ sep.className = "menu-separator";
11
+ sep.setAttribute("role", "separator");
12
+ return sep;
13
+ }
14
+ if ("type" in item && item.type === "header") {
15
+ const header = document.createElement("div");
16
+ header.className = "menu-header";
17
+ const label2 = document.createElement("span");
18
+ label2.className = "menu-header-label";
19
+ label2.textContent = item.label;
20
+ header.appendChild(label2);
21
+ if (item.description) {
22
+ const desc = document.createElement("span");
23
+ desc.className = "menu-header-desc";
24
+ desc.textContent = item.description;
25
+ header.appendChild(desc);
26
+ }
27
+ return header;
28
+ }
29
+ const entry = item;
30
+ const btn = document.createElement("button");
31
+ btn.type = "button";
32
+ btn.className = entry.danger ? "menu-item pressable menu-item--danger" : "menu-item pressable";
33
+ if (entry.disabled) btn.disabled = true;
34
+ btn.setAttribute("role", "menuitem");
35
+ btn.tabIndex = -1;
36
+ const icon = document.createElement("span");
37
+ icon.className = "menu-item-icon";
38
+ if (entry.icon) icon.innerHTML = entry.icon;
39
+ btn.appendChild(icon);
40
+ const label = document.createElement("span");
41
+ label.className = "menu-item-label";
42
+ label.textContent = entry.label;
43
+ if (entry.subLabel) {
44
+ const sub = document.createElement("span");
45
+ sub.className = "menu-item-sub";
46
+ sub.textContent = entry.subLabel;
47
+ label.appendChild(sub);
48
+ }
49
+ btn.appendChild(label);
50
+ if (entry.shortcut) {
51
+ const shortcut = document.createElement("span");
52
+ shortcut.className = "menu-item-shortcut";
53
+ shortcut.textContent = entry.shortcut;
54
+ btn.appendChild(shortcut);
55
+ }
56
+ btn.addEventListener("click", () => {
57
+ entry.onClick?.();
58
+ closeMenu();
59
+ });
60
+ return btn;
61
+ }
62
+ function openContextMenu(x, y, items) {
63
+ if (typeof document === "undefined") return;
64
+ closeMenu();
65
+ panel = document.createElement("div");
66
+ panel.className = "menu menu--context";
67
+ panel.setAttribute("role", "menu");
68
+ panel.style.visibility = "hidden";
69
+ for (const item of items) panel.appendChild(buildItem(item));
70
+ document.body.appendChild(panel);
71
+ const rect = panel.getBoundingClientRect();
72
+ const vw = window.innerWidth;
73
+ const vh = window.innerHeight;
74
+ let nx = x;
75
+ let ny = y;
76
+ if (x + rect.width > vw - FLIP_MARGIN) nx = x - rect.width;
77
+ if (y + rect.height > vh - FLIP_MARGIN) ny = y - rect.height;
78
+ if (nx < CLAMP_MARGIN) nx = CLAMP_MARGIN;
79
+ if (ny < CLAMP_MARGIN) ny = CLAMP_MARGIN;
80
+ panel.style.left = `${nx}px`;
81
+ panel.style.top = `${ny}px`;
82
+ panel.style.visibility = "";
83
+ focusableItems = Array.from(panel.querySelectorAll(".menu-item:not(:disabled)"));
84
+ focusedIndex = -1;
85
+ requestAnimationFrame(() => {
86
+ if (!panel || focusableItems.length === 0) return;
87
+ setFocused(0);
88
+ });
89
+ document.addEventListener("pointerdown", onOutsidePointerDown, true);
90
+ document.addEventListener("scroll", onCloseSignal, true);
91
+ window.addEventListener("resize", onCloseSignal);
92
+ window.addEventListener("keydown", onKeyDown);
93
+ }
94
+ function closeMenu() {
95
+ if (!panel) return;
96
+ panel.remove();
97
+ panel = null;
98
+ focusableItems = [];
99
+ focusedIndex = -1;
100
+ document.removeEventListener("pointerdown", onOutsidePointerDown, true);
101
+ document.removeEventListener("scroll", onCloseSignal, true);
102
+ window.removeEventListener("resize", onCloseSignal);
103
+ window.removeEventListener("keydown", onKeyDown);
104
+ }
105
+ function setFocused(next) {
106
+ if (!panel || focusableItems.length === 0) return;
107
+ focusedIndex = (next % focusableItems.length + focusableItems.length) % focusableItems.length;
108
+ focusableItems[focusedIndex].focus();
109
+ }
110
+ function onOutsidePointerDown(e) {
111
+ if (!panel) return;
112
+ const target = e.target;
113
+ if (!(target instanceof Node) || !panel.contains(target)) closeMenu();
114
+ }
115
+ function onCloseSignal() {
116
+ closeMenu();
117
+ }
118
+ function onKeyDown(e) {
119
+ if (!panel) return;
120
+ if (e.key === "Escape") {
121
+ e.preventDefault();
122
+ closeMenu();
123
+ return;
124
+ }
125
+ if (focusableItems.length === 0) return;
126
+ if (e.key === "ArrowDown") {
127
+ e.preventDefault();
128
+ setFocused(focusedIndex + 1);
129
+ } else if (e.key === "ArrowUp") {
130
+ e.preventDefault();
131
+ setFocused(focusedIndex - 1);
132
+ } else if (e.key === "Enter" && focusedIndex >= 0) {
133
+ e.preventDefault();
134
+ focusableItems[focusedIndex].click();
135
+ }
136
+ }
137
+
138
+ // src/behaviors/datatable.ts
139
+ var SVG_NS = "http://www.w3.org/2000/svg";
140
+ function svgIcon(d, size = 12) {
141
+ const svg = document.createElementNS(SVG_NS, "svg");
142
+ svg.setAttribute("width", String(size));
143
+ svg.setAttribute("height", String(size));
144
+ svg.setAttribute("viewBox", "0 0 24 24");
145
+ svg.setAttribute("fill", "none");
146
+ svg.setAttribute("stroke", "currentColor");
147
+ svg.setAttribute("stroke-width", "2");
148
+ svg.setAttribute("stroke-linecap", "round");
149
+ svg.setAttribute("stroke-linejoin", "round");
150
+ const path = document.createElementNS(SVG_NS, "path");
151
+ path.setAttribute("d", d);
152
+ svg.appendChild(path);
153
+ return svg;
154
+ }
155
+ var ICON_SEARCH = "M11 19a8 8 0 1 0 0-16 8 8 0 0 0 0 16Zm10 2-4.3-4.3";
156
+ var ICON_FUNNEL = "M22 3H2l8 9.46V19l4 2v-8.54L22 3Z";
157
+ var ICON_CHEVRON = "m6 9 6 6 6-6";
158
+ function defaultCompare(a, b) {
159
+ if (typeof a === "number" && typeof b === "number") return a - b;
160
+ return String(a ?? "").localeCompare(String(b ?? ""), "zh-CN", { numeric: true });
161
+ }
162
+ function noopHandle() {
163
+ const noop = () => void 0;
164
+ return {
165
+ setData: noop,
166
+ getSelected: () => [],
167
+ clearSelection: noop,
168
+ setSearch: noop,
169
+ setFilter: noop,
170
+ setSort: noop,
171
+ refresh: noop,
172
+ destroy: noop
173
+ };
174
+ }
175
+ function createTable(el, opts) {
176
+ if (typeof document === "undefined") return noopHandle();
177
+ let data = opts.data;
178
+ let search = "";
179
+ let page = 1;
180
+ let pageSize = opts.pageSize ?? 10;
181
+ let sortKey = null;
182
+ let sortDir = "asc";
183
+ let scrollTop = 0;
184
+ const filters = /* @__PURE__ */ new Map();
185
+ const selected = /* @__PURE__ */ new Set();
186
+ const expanded = /* @__PURE__ */ new Set();
187
+ let destroyed = false;
188
+ const virtual = opts.virtual === true;
189
+ const rowH = opts.rowHeight ?? 44;
190
+ const pageSizes = opts.pageSizes ?? [10, 20, 50];
191
+ const selectable = opts.selectable === true;
192
+ const expandable = !virtual ? opts.expandable : void 0;
193
+ const cols = () => opts.columns.filter((c) => !c.hidden);
194
+ const keyOf = (row, index) => {
195
+ if (typeof opts.rowKey === "function") return opts.rowKey(row);
196
+ if (typeof opts.rowKey === "string") return String(row[opts.rowKey]);
197
+ return String(index);
198
+ };
199
+ let keyCache = /* @__PURE__ */ new WeakMap();
200
+ const keyOfRow = (row) => keyCache.get(row) ?? keyOf(row, data.indexOf(row));
201
+ function rekey() {
202
+ keyCache = /* @__PURE__ */ new WeakMap();
203
+ data.forEach((r, i) => keyCache.set(r, keyOf(r, i)));
204
+ }
205
+ function pipeline() {
206
+ let rows = data;
207
+ if (search) {
208
+ const q = search.toLowerCase();
209
+ rows = rows.filter(
210
+ (row) => cols().some((c) => String(row[c.key] ?? "").toLowerCase().includes(q))
211
+ );
212
+ }
213
+ if (filters.size > 0) {
214
+ rows = rows.filter((row) => {
215
+ for (const [key, allowed] of filters) {
216
+ if (!allowed.has(String(row[key] ?? ""))) return false;
217
+ }
218
+ return true;
219
+ });
220
+ }
221
+ if (sortKey) {
222
+ const col = opts.columns.find((c) => c.key === sortKey);
223
+ const cmp = col?.sortFn ? (a, b) => col.sortFn(a, b) : (a, b) => defaultCompare(a[sortKey], b[sortKey]);
224
+ rows = [...rows].sort((a, b) => sortDir === "asc" ? cmp(a, b) : -cmp(a, b));
225
+ }
226
+ return rows;
227
+ }
228
+ el.textContent = "";
229
+ const root = document.createElement("div");
230
+ root.className = "datatable";
231
+ if (opts.striped) root.classList.add("datatable--striped");
232
+ if (virtual) root.classList.add("datatable--virtual");
233
+ let searchInput = null;
234
+ if (opts.searchable) {
235
+ const bar = document.createElement("div");
236
+ bar.className = "dt-toolbar";
237
+ const box = document.createElement("div");
238
+ box.className = "dt-search";
239
+ box.appendChild(svgIcon(ICON_SEARCH, 14));
240
+ searchInput = document.createElement("input");
241
+ searchInput.className = "dt-search-input";
242
+ searchInput.type = "search";
243
+ searchInput.placeholder = opts.searchPlaceholder ?? "\u641C\u7D22\u2026";
244
+ searchInput.setAttribute("aria-label", "\u641C\u7D22\u8868\u683C");
245
+ box.appendChild(searchInput);
246
+ bar.appendChild(box);
247
+ root.appendChild(bar);
248
+ }
249
+ const scroll = document.createElement("div");
250
+ scroll.className = "dt-scroll";
251
+ if (virtual) scroll.style.maxHeight = opts.height ?? "420px";
252
+ const canvas = document.createElement("div");
253
+ canvas.className = "dt-canvas";
254
+ canvas.setAttribute("role", "grid");
255
+ const head = document.createElement("div");
256
+ head.className = "dt-head";
257
+ head.setAttribute("role", "rowgroup");
258
+ const headRow = document.createElement("div");
259
+ headRow.className = "dt-row dt-row--head";
260
+ headRow.setAttribute("role", "row");
261
+ head.appendChild(headRow);
262
+ const body = document.createElement("div");
263
+ body.className = "dt-body";
264
+ body.setAttribute("role", "rowgroup");
265
+ canvas.append(head, body);
266
+ scroll.appendChild(canvas);
267
+ root.appendChild(scroll);
268
+ const foot = document.createElement("div");
269
+ foot.className = "dt-foot";
270
+ root.appendChild(foot);
271
+ el.appendChild(root);
272
+ function template() {
273
+ const parts = [];
274
+ if (selectable) parts.push("40px");
275
+ if (expandable) parts.push("34px");
276
+ for (const c of cols()) parts.push(c.width ?? "minmax(0,1fr)");
277
+ return parts.join(" ");
278
+ }
279
+ function fillCell(cell, col, row, rowIndex) {
280
+ const value = row[col.key];
281
+ if (col.render) {
282
+ const out = col.render(value, row, rowIndex);
283
+ if (out instanceof Node) cell.appendChild(out);
284
+ else if (out !== null && out !== void 0) cell.textContent = String(out);
285
+ } else {
286
+ cell.textContent = String(value ?? "");
287
+ }
288
+ }
289
+ function makeCheck(checked, label) {
290
+ const input = document.createElement("input");
291
+ input.type = "checkbox";
292
+ input.className = "dt-check";
293
+ input.checked = checked;
294
+ input.setAttribute("aria-label", label);
295
+ return input;
296
+ }
297
+ let openPanel = null;
298
+ function onPanelScroll(ev) {
299
+ if (openPanel && ev.target instanceof Node && openPanel.el.contains(ev.target)) return;
300
+ closeFilterPanel();
301
+ }
302
+ function closeFilterPanel() {
303
+ if (!openPanel) return;
304
+ openPanel.el.remove();
305
+ openPanel = null;
306
+ headRow.querySelectorAll(".dt-filter-btn").forEach((b) => b.classList.remove("is-open"));
307
+ window.removeEventListener("scroll", onPanelScroll, true);
308
+ window.removeEventListener("resize", onPanelScroll);
309
+ }
310
+ function openFilterPanel(col, btn) {
311
+ const panel2 = buildFilterPanel(col);
312
+ document.body.appendChild(panel2);
313
+ openPanel = { key: col.key, el: panel2 };
314
+ btn.classList.add("is-open");
315
+ const rect = btn.getBoundingClientRect();
316
+ const pr = panel2.getBoundingClientRect();
317
+ let x = rect.right - pr.width;
318
+ let y = rect.bottom + 4;
319
+ if (x < 8) x = 8;
320
+ if (y + pr.height > window.innerHeight - 8) y = rect.top - pr.height - 4;
321
+ panel2.style.left = `${x}px`;
322
+ panel2.style.top = `${y}px`;
323
+ panel2.hidden = false;
324
+ window.addEventListener("scroll", onPanelScroll, true);
325
+ window.addEventListener("resize", onPanelScroll);
326
+ }
327
+ function renderHead() {
328
+ closeFilterPanel();
329
+ headRow.textContent = "";
330
+ headRow.style.gridTemplateColumns = template();
331
+ if (selectable) {
332
+ const cell = document.createElement("div");
333
+ cell.className = "dt-cell dt-cell--check";
334
+ const rows = pipeline();
335
+ const allKeys = rows.map((r) => keyOfRow(r));
336
+ const selCount = allKeys.filter((k) => selected.has(k)).length;
337
+ const box = makeCheck(selCount > 0 && selCount === allKeys.length, "\u5168\u9009");
338
+ box.indeterminate = selCount > 0 && selCount < allKeys.length;
339
+ box.addEventListener("change", () => {
340
+ if (box.checked) allKeys.forEach((k) => selected.add(k));
341
+ else allKeys.forEach((k) => selected.delete(k));
342
+ emitSelection();
343
+ renderBody();
344
+ renderHead();
345
+ });
346
+ cell.appendChild(box);
347
+ headRow.appendChild(cell);
348
+ }
349
+ if (expandable) {
350
+ const cell = document.createElement("div");
351
+ cell.className = "dt-cell dt-cell--expand";
352
+ headRow.appendChild(cell);
353
+ }
354
+ for (const col of cols()) {
355
+ const cell = document.createElement("div");
356
+ cell.className = "dt-cell dt-cell--head";
357
+ if (col.align) cell.dataset.align = col.align;
358
+ const title = document.createElement("span");
359
+ title.className = "dt-head-title";
360
+ title.textContent = col.title;
361
+ cell.appendChild(title);
362
+ if (col.sortable) {
363
+ cell.classList.add("is-sortable");
364
+ cell.setAttribute("role", "columnheader");
365
+ cell.tabIndex = 0;
366
+ const active = sortKey === col.key;
367
+ cell.setAttribute("aria-sort", active ? sortDir === "asc" ? "ascending" : "descending" : "none");
368
+ const arrow = document.createElement("span");
369
+ arrow.className = "dt-sort";
370
+ arrow.textContent = active ? sortDir === "asc" ? "\u2191" : "\u2193" : "\u2195";
371
+ if (active) arrow.classList.add("is-active");
372
+ cell.appendChild(arrow);
373
+ const toggleSort = () => {
374
+ if (sortKey !== col.key) {
375
+ sortKey = col.key;
376
+ sortDir = "asc";
377
+ } else if (sortDir === "asc") sortDir = "desc";
378
+ else sortKey = null;
379
+ renderAll();
380
+ };
381
+ cell.addEventListener("click", (ev) => {
382
+ if (ev.target.closest(".dt-filter-btn")) return;
383
+ toggleSort();
384
+ });
385
+ cell.addEventListener("keydown", (ev) => {
386
+ if (ev.key === "Enter" || ev.key === " ") {
387
+ ev.preventDefault();
388
+ toggleSort();
389
+ }
390
+ });
391
+ }
392
+ if (col.filterable) {
393
+ const hasFilter = filters.has(col.key);
394
+ const btn = document.createElement("button");
395
+ btn.type = "button";
396
+ btn.className = "dt-filter-btn";
397
+ if (hasFilter) btn.classList.add("is-active");
398
+ btn.setAttribute("aria-label", `\u7B5B\u9009\u300C${col.title}\u300D`);
399
+ btn.appendChild(svgIcon(ICON_FUNNEL, 12));
400
+ btn.addEventListener("click", (ev) => {
401
+ ev.stopPropagation();
402
+ if (openPanel?.key === col.key) closeFilterPanel();
403
+ else {
404
+ closeFilterPanel();
405
+ openFilterPanel(col, btn);
406
+ }
407
+ });
408
+ cell.appendChild(btn);
409
+ }
410
+ headRow.appendChild(cell);
411
+ }
412
+ }
413
+ function buildFilterPanel(col) {
414
+ const panel2 = document.createElement("div");
415
+ panel2.className = "dt-filter-panel";
416
+ panel2.hidden = true;
417
+ panel2.addEventListener("click", (ev) => ev.stopPropagation());
418
+ const list = document.createElement("div");
419
+ list.className = "dt-filter-list";
420
+ const values = Array.from(new Set(data.map((r) => String(r[col.key] ?? "")))).sort((a, b) => a.localeCompare(b, "zh-CN", { numeric: true }));
421
+ for (const v of values) {
422
+ const row = document.createElement("label");
423
+ row.className = "dt-filter-item";
424
+ const box = makeCheck(filters.get(col.key)?.has(v) ?? true, v);
425
+ box.addEventListener("change", () => {
426
+ const cur = new Set(filters.get(col.key) ?? values);
427
+ if (box.checked) cur.add(v);
428
+ else cur.delete(v);
429
+ if (cur.size === values.length) filters.delete(col.key);
430
+ else filters.set(col.key, cur);
431
+ page = 1;
432
+ renderAll();
433
+ });
434
+ const text = document.createElement("span");
435
+ text.textContent = v === "" ? "\uFF08\u7A7A\uFF09" : v;
436
+ row.append(box, text);
437
+ list.appendChild(row);
438
+ }
439
+ const footRow = document.createElement("div");
440
+ footRow.className = "dt-filter-foot";
441
+ const reset = document.createElement("button");
442
+ reset.type = "button";
443
+ reset.className = "dt-filter-reset";
444
+ reset.textContent = "\u91CD\u7F6E";
445
+ reset.addEventListener("click", () => {
446
+ filters.delete(col.key);
447
+ page = 1;
448
+ closeFilterPanel();
449
+ renderAll();
450
+ });
451
+ footRow.appendChild(reset);
452
+ panel2.append(list, footRow);
453
+ return panel2;
454
+ }
455
+ function makeRow(row, rowIndex, key) {
456
+ const tr = document.createElement("div");
457
+ tr.className = "dt-row";
458
+ tr.setAttribute("role", "row");
459
+ tr.style.gridTemplateColumns = template();
460
+ if (virtual) tr.style.height = `${rowH}px`;
461
+ if (selected.has(key)) tr.classList.add("is-selected");
462
+ if (selectable) {
463
+ const cell = document.createElement("div");
464
+ cell.className = "dt-cell dt-cell--check";
465
+ const box = makeCheck(selected.has(key), "\u9009\u62E9\u8BE5\u884C");
466
+ box.addEventListener("click", (ev) => ev.stopPropagation());
467
+ box.addEventListener("change", () => {
468
+ if (box.checked) selected.add(key);
469
+ else selected.delete(key);
470
+ tr.classList.toggle("is-selected", box.checked);
471
+ emitSelection();
472
+ renderHead();
473
+ renderFoot(pipeline().length);
474
+ });
475
+ cell.appendChild(box);
476
+ tr.appendChild(cell);
477
+ }
478
+ if (expandable) {
479
+ const cell = document.createElement("div");
480
+ cell.className = "dt-cell dt-cell--expand";
481
+ const btn = document.createElement("button");
482
+ btn.type = "button";
483
+ btn.className = "dt-expand-btn";
484
+ btn.setAttribute("aria-label", "\u5C55\u5F00\u884C");
485
+ btn.setAttribute("aria-expanded", String(expanded.has(key)));
486
+ btn.appendChild(svgIcon(ICON_CHEVRON, 14));
487
+ btn.addEventListener("click", (ev) => {
488
+ ev.stopPropagation();
489
+ if (expanded.has(key)) expanded.delete(key);
490
+ else expanded.add(key);
491
+ renderBody();
492
+ });
493
+ cell.appendChild(btn);
494
+ tr.appendChild(cell);
495
+ }
496
+ for (const col of cols()) {
497
+ const cell = document.createElement("div");
498
+ cell.className = "dt-cell";
499
+ if (col.align) cell.dataset.align = col.align;
500
+ fillCell(cell, col, row, rowIndex);
501
+ tr.appendChild(cell);
502
+ }
503
+ if (opts.onRowClick) {
504
+ tr.classList.add("is-clickable");
505
+ tr.addEventListener("click", () => opts.onRowClick(row));
506
+ }
507
+ if (opts.contextMenu) {
508
+ tr.addEventListener("contextmenu", (ev) => {
509
+ ev.preventDefault();
510
+ ev.stopPropagation();
511
+ openContextMenu(ev.clientX, ev.clientY, opts.contextMenu(row));
512
+ });
513
+ }
514
+ return tr;
515
+ }
516
+ function makeExpandRow(row) {
517
+ const wrap = document.createElement("div");
518
+ wrap.className = "dt-expand-row";
519
+ const out = expandable(row);
520
+ if (out instanceof Node) wrap.appendChild(out);
521
+ else wrap.textContent = String(out);
522
+ return wrap;
523
+ }
524
+ function visibleRange(total) {
525
+ if (!virtual) {
526
+ if (opts.pagination === false) return [0, total];
527
+ const start2 = (page - 1) * pageSize;
528
+ return [start2, Math.min(total, start2 + pageSize)];
529
+ }
530
+ const viewH = scroll.clientHeight || 420;
531
+ const overscan = 6;
532
+ const start = Math.max(0, Math.floor(scrollTop / rowH) - overscan);
533
+ const end = Math.min(total, Math.ceil((scrollTop + viewH) / rowH) + overscan);
534
+ return [start, end];
535
+ }
536
+ function renderBody() {
537
+ const rows = pipeline();
538
+ const total = rows.length;
539
+ const keepTop = virtual ? scroll.scrollTop : 0;
540
+ body.textContent = "";
541
+ if (opts.loading) {
542
+ for (let i = 0; i < Math.min(pageSize, 8); i++) {
543
+ const sk = document.createElement("div");
544
+ sk.className = "dt-skeleton-row";
545
+ const bar = document.createElement("div");
546
+ bar.className = "skeleton";
547
+ bar.style.height = "14px";
548
+ bar.style.width = `${88 - i % 3 * 14}%`;
549
+ sk.appendChild(bar);
550
+ body.appendChild(sk);
551
+ }
552
+ renderFoot(total);
553
+ return;
554
+ }
555
+ if (total === 0) {
556
+ const empty = document.createElement("div");
557
+ empty.className = "dt-empty";
558
+ empty.textContent = opts.emptyLabel ?? "\u6682\u65E0\u6570\u636E";
559
+ body.appendChild(empty);
560
+ renderFoot(total);
561
+ return;
562
+ }
563
+ const [start, end] = visibleRange(total);
564
+ const frag = document.createDocumentFragment();
565
+ if (virtual && start > 0) {
566
+ const top = document.createElement("div");
567
+ top.style.height = `${start * rowH}px`;
568
+ frag.appendChild(top);
569
+ }
570
+ for (let i = start; i < end; i++) {
571
+ const row = rows[i];
572
+ const key = keyOfRow(row);
573
+ frag.appendChild(makeRow(row, i, key));
574
+ if (expandable && expanded.has(key)) frag.appendChild(makeExpandRow(row));
575
+ }
576
+ if (virtual && end < total) {
577
+ const bottom = document.createElement("div");
578
+ bottom.style.height = `${(total - end) * rowH}px`;
579
+ frag.appendChild(bottom);
580
+ }
581
+ body.appendChild(frag);
582
+ if (virtual) scroll.scrollTop = keepTop;
583
+ renderFoot(total);
584
+ }
585
+ function renderFoot(total) {
586
+ foot.textContent = "";
587
+ const stat = document.createElement("span");
588
+ stat.className = "dt-stat";
589
+ stat.textContent = `\u5171 ${total} \u6761`;
590
+ foot.appendChild(stat);
591
+ if (selectable && selected.size > 0) {
592
+ const sel = document.createElement("span");
593
+ sel.className = "dt-stat dt-stat--sel";
594
+ sel.textContent = `\u5DF2\u9009 ${selected.size} \u9879`;
595
+ const clear = document.createElement("button");
596
+ clear.type = "button";
597
+ clear.className = "dt-clear-sel";
598
+ clear.textContent = "\u6E05\u7A7A";
599
+ clear.addEventListener("click", () => {
600
+ selected.clear();
601
+ emitSelection();
602
+ renderAll();
603
+ });
604
+ sel.appendChild(clear);
605
+ foot.appendChild(sel);
606
+ }
607
+ if (virtual || opts.pagination === false || total <= pageSize) return;
608
+ const pages = Math.ceil(total / pageSize);
609
+ const pager = document.createElement("nav");
610
+ pager.className = "dt-pager";
611
+ pager.setAttribute("aria-label", "\u5206\u9875");
612
+ const sizeSel = document.createElement("select");
613
+ sizeSel.className = "dt-page-size";
614
+ sizeSel.setAttribute("aria-label", "\u6BCF\u9875\u6761\u6570");
615
+ for (const s of pageSizes) {
616
+ const o = document.createElement("option");
617
+ o.value = String(s);
618
+ o.textContent = `${s} \u6761/\u9875`;
619
+ if (s === pageSize) o.selected = true;
620
+ sizeSel.appendChild(o);
621
+ }
622
+ sizeSel.addEventListener("change", () => {
623
+ pageSize = Number(sizeSel.value);
624
+ page = 1;
625
+ renderAll();
626
+ });
627
+ pager.appendChild(sizeSel);
628
+ const pageBtn = (label, target, opts2 = {}) => {
629
+ const b = document.createElement("button");
630
+ b.type = "button";
631
+ b.className = "dt-page-btn";
632
+ if (opts2.active) b.classList.add("is-active");
633
+ b.disabled = opts2.disabled === true;
634
+ b.textContent = label;
635
+ if (opts2.aria) b.setAttribute("aria-label", opts2.aria);
636
+ b.addEventListener("click", () => {
637
+ page = target;
638
+ renderAll();
639
+ });
640
+ return b;
641
+ };
642
+ pager.appendChild(pageBtn("\u2039", page - 1, { disabled: page <= 1, aria: "\u4E0A\u4E00\u9875" }));
643
+ const win = [];
644
+ for (let p = 1; p <= pages; p++) {
645
+ if (p === 1 || p === pages || Math.abs(p - page) <= 2) win.push(p);
646
+ }
647
+ let last = 0;
648
+ for (const p of win) {
649
+ if (p - last > 1) {
650
+ const dots = document.createElement("span");
651
+ dots.className = "dt-page-dots";
652
+ dots.textContent = "\u2026";
653
+ pager.appendChild(dots);
654
+ }
655
+ pager.appendChild(pageBtn(String(p), p, { active: p === page }));
656
+ last = p;
657
+ }
658
+ pager.appendChild(pageBtn("\u203A", page + 1, { disabled: page >= pages, aria: "\u4E0B\u4E00\u9875" }));
659
+ foot.appendChild(pager);
660
+ }
661
+ let searchTimer = 0;
662
+ function onSearch() {
663
+ window.clearTimeout(searchTimer);
664
+ searchTimer = window.setTimeout(() => {
665
+ search = searchInput?.value.trim() ?? "";
666
+ page = 1;
667
+ renderAll();
668
+ }, 160);
669
+ }
670
+ searchInput?.addEventListener("input", onSearch);
671
+ function onScroll() {
672
+ scrollTop = scroll.scrollTop;
673
+ renderBody();
674
+ }
675
+ if (virtual) scroll.addEventListener("scroll", onScroll, { passive: true });
676
+ function onDocPointerDown(ev) {
677
+ if (!openPanel) return;
678
+ const t = ev.target;
679
+ if (t instanceof Node && (openPanel.el.contains(t) || headRow.contains(t))) return;
680
+ closeFilterPanel();
681
+ }
682
+ document.addEventListener("pointerdown", onDocPointerDown, true);
683
+ function onDocKeydown(ev) {
684
+ if (ev.key === "Escape") closeFilterPanel();
685
+ }
686
+ document.addEventListener("keydown", onDocKeydown);
687
+ function emitSelection() {
688
+ if (!opts.onSelectionChange) return;
689
+ const rows = data.filter((r) => selected.has(keyOfRow(r)));
690
+ opts.onSelectionChange(rows);
691
+ }
692
+ function renderAll() {
693
+ if (destroyed) return;
694
+ renderHead();
695
+ renderBody();
696
+ }
697
+ rekey();
698
+ renderAll();
699
+ return {
700
+ setData(next) {
701
+ data = next;
702
+ rekey();
703
+ page = 1;
704
+ renderAll();
705
+ },
706
+ getSelected() {
707
+ return data.filter((r) => selected.has(keyOfRow(r)));
708
+ },
709
+ clearSelection() {
710
+ selected.clear();
711
+ emitSelection();
712
+ renderAll();
713
+ },
714
+ setSearch(q) {
715
+ search = q;
716
+ if (searchInput) searchInput.value = q;
717
+ page = 1;
718
+ renderAll();
719
+ },
720
+ setFilter(key, values) {
721
+ if (values === null) filters.delete(key);
722
+ else filters.set(key, new Set(values));
723
+ page = 1;
724
+ renderAll();
725
+ },
726
+ setSort(key, dir = "asc") {
727
+ sortKey = key;
728
+ sortDir = dir;
729
+ renderAll();
730
+ },
731
+ refresh() {
732
+ renderAll();
733
+ },
734
+ destroy() {
735
+ destroyed = true;
736
+ window.clearTimeout(searchTimer);
737
+ closeFilterPanel();
738
+ searchInput?.removeEventListener("input", onSearch);
739
+ if (virtual) scroll.removeEventListener("scroll", onScroll);
740
+ document.removeEventListener("pointerdown", onDocPointerDown, true);
741
+ document.removeEventListener("keydown", onDocKeydown);
742
+ el.textContent = "";
743
+ }
744
+ };
745
+ }
746
+ export {
747
+ createTable
748
+ };