@icen.ai/ui 0.2.1 → 0.4.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 (121) 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 +356 -0
  6. package/dist/behaviors/copy.mjs +4 -3
  7. package/dist/behaviors/datatable.mjs +968 -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/chart-area.css +15 -0
  31. package/dist/components/chart-area.mjs +4 -0
  32. package/dist/components/chart-bar.css +79 -0
  33. package/dist/components/chart-bar.mjs +4 -0
  34. package/dist/components/chart-gauge.css +38 -0
  35. package/dist/components/chart-gauge.mjs +4 -0
  36. package/dist/components/chart-heatmap.css +52 -0
  37. package/dist/components/chart-heatmap.mjs +4 -0
  38. package/dist/components/chart-line.css +15 -0
  39. package/dist/components/chart-line.mjs +4 -0
  40. package/dist/components/chart-pie.css +26 -0
  41. package/dist/components/chart-pie.mjs +4 -0
  42. package/dist/components/chart-radar.css +19 -0
  43. package/dist/components/chart-radar.mjs +4 -0
  44. package/dist/components/chart-sparkline.css +14 -0
  45. package/dist/components/chart-sparkline.mjs +4 -0
  46. package/dist/components/chart-stack.css +30 -0
  47. package/dist/components/chart-stack.mjs +4 -0
  48. package/dist/components/charts.css +66 -74
  49. package/dist/components/charts.mjs +12 -0
  50. package/dist/components/content.css +256 -74
  51. package/dist/components/context-menu.mjs +3 -0
  52. package/dist/components/datatable.css +429 -0
  53. package/dist/components/datatable.mjs +4 -0
  54. package/dist/components/desc.mjs +2 -0
  55. package/dist/components/dropdown.mjs +3 -0
  56. package/dist/components/empty.css +50 -10
  57. package/dist/components/empty.mjs +2 -0
  58. package/dist/components/feedback.css +158 -47
  59. package/dist/components/form.css +139 -22
  60. package/dist/components/input.css +165 -45
  61. package/dist/components/input.mjs +3 -0
  62. package/dist/components/kbd.mjs +2 -0
  63. package/dist/components/layout.mjs +2 -0
  64. package/dist/components/list.mjs +2 -0
  65. package/dist/components/media-card.mjs +2 -0
  66. package/dist/components/media.css +187 -40
  67. package/dist/components/menu.css +159 -32
  68. package/dist/components/modal.css +140 -34
  69. package/dist/components/modal.mjs +3 -0
  70. package/dist/components/nav.css +134 -32
  71. package/dist/components/nav.mjs +3 -0
  72. package/dist/components/pagination.css +105 -19
  73. package/dist/components/pagination.mjs +2 -0
  74. package/dist/components/panel.css +61 -6
  75. package/dist/components/panel.mjs +2 -0
  76. package/dist/components/pill.css +74 -14
  77. package/dist/components/pill.mjs +2 -0
  78. package/dist/components/popover.css +109 -7
  79. package/dist/components/popover.mjs +3 -0
  80. package/dist/components/progress.mjs +2 -0
  81. package/dist/components/rating.mjs +2 -0
  82. package/dist/components/result.mjs +2 -0
  83. package/dist/components/segmented.css +199 -50
  84. package/dist/components/segmented.mjs +2 -0
  85. package/dist/components/select.css +140 -39
  86. package/dist/components/select.mjs +3 -0
  87. package/dist/components/sidebar.css +162 -34
  88. package/dist/components/sidebar.mjs +3 -0
  89. package/dist/components/skeleton.mjs +2 -0
  90. package/dist/components/slider.css +169 -31
  91. package/dist/components/slider.mjs +3 -0
  92. package/dist/components/spinner.mjs +2 -0
  93. package/dist/components/stat.css +103 -8
  94. package/dist/components/stat.mjs +2 -0
  95. package/dist/components/steps.css +105 -31
  96. package/dist/components/steps.mjs +2 -0
  97. package/dist/components/switch.css +171 -54
  98. package/dist/components/switch.mjs +2 -0
  99. package/dist/components/table.css +114 -12
  100. package/dist/components/table.mjs +2 -0
  101. package/dist/components/tabs.css +104 -17
  102. package/dist/components/tabs.mjs +3 -0
  103. package/dist/components/tag-input.css +119 -23
  104. package/dist/components/tag-input.mjs +3 -0
  105. package/dist/components/timeline.mjs +2 -0
  106. package/dist/components/toast.css +85 -12
  107. package/dist/components/toast.mjs +3 -0
  108. package/dist/components/tooltip.css +86 -26
  109. package/dist/components/tooltip.mjs +2 -0
  110. package/dist/components/tree.mjs +3 -0
  111. package/dist/components/upload.css +144 -17
  112. package/dist/components/upload.mjs +3 -0
  113. package/dist/index.mjs +811 -42
  114. package/dist/registry.json +310 -2
  115. package/dist/retro-effects.css +264 -0
  116. package/dist/tokens/colors.css +18 -90
  117. package/dist/tokens/retro-effects.css +264 -0
  118. package/dist/tokens/style-profiles.css +47 -47
  119. package/dist/tokens.css +65 -137
  120. package/dist/ui.css +5069 -1026
  121. package/package.json +9 -1
@@ -0,0 +1,968 @@
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
+ var ICON_COLUMNS = "M3 3h7v18H3zM14 3h7v18h-7z";
159
+ var ICON_DOWNLOAD = "M12 3v12m0 0-4-4m4 4 4-4M5 21h14";
160
+ function defaultCompare(a, b) {
161
+ if (typeof a === "number" && typeof b === "number") return a - b;
162
+ return String(a ?? "").localeCompare(String(b ?? ""), "zh-CN", { numeric: true });
163
+ }
164
+ function noopHandle() {
165
+ const noop = () => void 0;
166
+ return {
167
+ setData: noop,
168
+ getSelected: () => [],
169
+ clearSelection: noop,
170
+ setSearch: noop,
171
+ setFilter: noop,
172
+ setSort: noop,
173
+ refresh: noop,
174
+ destroy: noop,
175
+ getData: () => [],
176
+ exportCSV: noop,
177
+ showColumn: noop,
178
+ hideColumn: noop
179
+ };
180
+ }
181
+ function createTable(el, opts) {
182
+ if (typeof document === "undefined") return noopHandle();
183
+ let data = opts.data;
184
+ let search = "";
185
+ let page = 1;
186
+ let pageSize = opts.pageSize ?? 10;
187
+ let sortKey = null;
188
+ let sortDir = "asc";
189
+ let scrollTop = 0;
190
+ const filters = /* @__PURE__ */ new Map();
191
+ const selected = /* @__PURE__ */ new Set();
192
+ const expanded = /* @__PURE__ */ new Set();
193
+ const colWidths = /* @__PURE__ */ new Map();
194
+ const colHidden = /* @__PURE__ */ new Map();
195
+ let lastClickedIndex = -1;
196
+ let destroyed = false;
197
+ const virtual = opts.virtual === true;
198
+ const compact = opts.density === "compact";
199
+ const rowH = opts.rowHeight ?? (compact ? 32 : 44);
200
+ const pageSizes = opts.pageSizes ?? [10, 20, 50];
201
+ const selectable = opts.selectable === true;
202
+ const expandable = !virtual ? opts.expandable : void 0;
203
+ const resizable = opts.resizable !== false;
204
+ const cols = () => opts.columns.filter((c) => !(c.hidden || colHidden.get(c.key)));
205
+ const keyOf = (row, index) => {
206
+ if (typeof opts.rowKey === "function") return opts.rowKey(row);
207
+ if (typeof opts.rowKey === "string") return String(row[opts.rowKey]);
208
+ return String(index);
209
+ };
210
+ let keyCache = /* @__PURE__ */ new WeakMap();
211
+ const keyOfRow = (row) => keyCache.get(row) ?? keyOf(row, data.indexOf(row));
212
+ function rekey() {
213
+ keyCache = /* @__PURE__ */ new WeakMap();
214
+ data.forEach((r, i) => keyCache.set(r, keyOf(r, i)));
215
+ }
216
+ let pipelineCache = null;
217
+ function invalidate() {
218
+ pipelineCache = null;
219
+ }
220
+ function pipeline() {
221
+ if (pipelineCache) return pipelineCache;
222
+ let rows = data;
223
+ if (search) {
224
+ const q = search.toLowerCase();
225
+ rows = rows.filter(
226
+ (row) => cols().some((c) => String(row[c.key] ?? "").toLowerCase().includes(q))
227
+ );
228
+ }
229
+ if (filters.size > 0) {
230
+ rows = rows.filter((row) => {
231
+ for (const [key, allowed] of filters) {
232
+ if (!allowed.has(String(row[key] ?? ""))) return false;
233
+ }
234
+ return true;
235
+ });
236
+ }
237
+ if (sortKey) {
238
+ const col = opts.columns.find((c) => c.key === sortKey);
239
+ const cmp = col?.sortFn ? (a, b) => col.sortFn(a, b) : (a, b) => defaultCompare(a[sortKey], b[sortKey]);
240
+ rows = [...rows].sort((a, b) => sortDir === "asc" ? cmp(a, b) : -cmp(a, b));
241
+ }
242
+ pipelineCache = rows;
243
+ return rows;
244
+ }
245
+ el.textContent = "";
246
+ const root = document.createElement("div");
247
+ root.className = "datatable";
248
+ if (opts.striped) root.classList.add("datatable--striped");
249
+ if (virtual) root.classList.add("datatable--virtual");
250
+ if (compact) root.classList.add("datatable--compact");
251
+ if (opts.columns.some((c) => c.frozen)) root.classList.add("datatable--frozen");
252
+ let searchInput = null;
253
+ const needsToolbar = opts.searchable || opts.columnVisibility || opts.exportable;
254
+ let toolbar = null;
255
+ if (needsToolbar) {
256
+ toolbar = document.createElement("div");
257
+ toolbar.className = "dt-toolbar";
258
+ if (opts.searchable) {
259
+ const box = document.createElement("div");
260
+ box.className = "dt-search";
261
+ box.appendChild(svgIcon(ICON_SEARCH, 14));
262
+ searchInput = document.createElement("input");
263
+ searchInput.className = "dt-search-input";
264
+ searchInput.type = "search";
265
+ searchInput.placeholder = opts.searchPlaceholder ?? "\u641C\u7D22\u2026";
266
+ searchInput.setAttribute("aria-label", "\u641C\u7D22\u8868\u683C");
267
+ box.appendChild(searchInput);
268
+ toolbar.appendChild(box);
269
+ }
270
+ const actions = document.createElement("div");
271
+ actions.className = "dt-toolbar-actions";
272
+ if (opts.columnVisibility) {
273
+ const colBtn = document.createElement("button");
274
+ colBtn.type = "button";
275
+ colBtn.className = "dt-tool-btn";
276
+ colBtn.setAttribute("aria-label", "\u5217\u8BBE\u7F6E");
277
+ colBtn.appendChild(svgIcon(ICON_COLUMNS, 14));
278
+ colBtn.addEventListener("click", (ev) => {
279
+ ev.stopPropagation();
280
+ if (openPanel?.key === "__cols") closeFilterPanel();
281
+ else {
282
+ closeFilterPanel();
283
+ openColumnPanel(colBtn);
284
+ }
285
+ });
286
+ actions.appendChild(colBtn);
287
+ }
288
+ if (opts.exportable) {
289
+ const expBtn = document.createElement("button");
290
+ expBtn.type = "button";
291
+ expBtn.className = "dt-tool-btn";
292
+ expBtn.setAttribute("aria-label", "\u5BFC\u51FA CSV");
293
+ expBtn.appendChild(svgIcon(ICON_DOWNLOAD, 14));
294
+ expBtn.addEventListener("click", () => exportCSV());
295
+ actions.appendChild(expBtn);
296
+ }
297
+ if (actions.children.length > 0) toolbar.appendChild(actions);
298
+ root.appendChild(toolbar);
299
+ }
300
+ const scroll = document.createElement("div");
301
+ scroll.className = "dt-scroll";
302
+ if (virtual) scroll.style.maxHeight = opts.height ?? "420px";
303
+ const canvas = document.createElement("div");
304
+ canvas.className = "dt-canvas";
305
+ canvas.setAttribute("role", "grid");
306
+ const head = document.createElement("div");
307
+ head.className = "dt-head";
308
+ head.setAttribute("role", "rowgroup");
309
+ const headRow = document.createElement("div");
310
+ headRow.className = "dt-row dt-row--head";
311
+ headRow.setAttribute("role", "row");
312
+ head.appendChild(headRow);
313
+ const body = document.createElement("div");
314
+ body.className = "dt-body";
315
+ body.setAttribute("role", "rowgroup");
316
+ canvas.append(head, body);
317
+ scroll.appendChild(canvas);
318
+ root.appendChild(scroll);
319
+ const foot = document.createElement("div");
320
+ foot.className = "dt-foot";
321
+ root.appendChild(foot);
322
+ el.appendChild(root);
323
+ function frozenOffsets() {
324
+ const offsets = /* @__PURE__ */ new Map();
325
+ if (!opts.columns.some((c) => c.frozen)) return offsets;
326
+ const visible = cols();
327
+ let acc = 0;
328
+ if (selectable) acc += 40;
329
+ if (expandable) acc += 34;
330
+ for (const c of visible) {
331
+ if (c.frozen) offsets.set(c.key, acc);
332
+ const w = colWidths.get(c.key) ?? c.width ?? "minmax(0,1fr)";
333
+ acc += parsePx(w);
334
+ }
335
+ return offsets;
336
+ }
337
+ function parsePx(w) {
338
+ const m = /^(\d+(?:\.\d+)?)px/.exec(w);
339
+ return m ? Number(m[1]) : 0;
340
+ }
341
+ function template() {
342
+ const parts = [];
343
+ if (selectable) parts.push("40px");
344
+ if (expandable) parts.push("34px");
345
+ for (const c of cols()) parts.push(colWidths.get(c.key) ?? c.width ?? "minmax(0,1fr)");
346
+ return parts.join(" ");
347
+ }
348
+ function fillCell(cell, col, row, rowIndex) {
349
+ const value = row[col.key];
350
+ if (col.render) {
351
+ const out = col.render(value, row, rowIndex);
352
+ if (out instanceof Node) cell.appendChild(out);
353
+ else if (out !== null && out !== void 0) cell.textContent = String(out);
354
+ } else {
355
+ cell.textContent = String(value ?? "");
356
+ }
357
+ }
358
+ function makeCheck(checked, label) {
359
+ const input = document.createElement("input");
360
+ input.type = "checkbox";
361
+ input.className = "dt-check";
362
+ input.checked = checked;
363
+ input.setAttribute("aria-label", label);
364
+ return input;
365
+ }
366
+ let openPanel = null;
367
+ function onPanelScroll(ev) {
368
+ if (openPanel && ev.target instanceof Node && openPanel.el.contains(ev.target)) return;
369
+ closeFilterPanel();
370
+ }
371
+ function closeFilterPanel() {
372
+ if (!openPanel) return;
373
+ openPanel.el.remove();
374
+ openPanel = null;
375
+ headRow.querySelectorAll(".dt-filter-btn").forEach((b) => b.classList.remove("is-open"));
376
+ window.removeEventListener("scroll", onPanelScroll, true);
377
+ window.removeEventListener("resize", onPanelScroll);
378
+ }
379
+ function openFilterPanel(col, btn) {
380
+ const panel2 = buildFilterPanel(col);
381
+ document.body.appendChild(panel2);
382
+ openPanel = { key: col.key, el: panel2 };
383
+ btn.classList.add("is-open");
384
+ const rect = btn.getBoundingClientRect();
385
+ const pr = panel2.getBoundingClientRect();
386
+ let x = rect.right - pr.width;
387
+ let y = rect.bottom + 4;
388
+ if (x < 8) x = 8;
389
+ if (y + pr.height > window.innerHeight - 8) y = rect.top - pr.height - 4;
390
+ panel2.style.left = `${x}px`;
391
+ panel2.style.top = `${y}px`;
392
+ panel2.hidden = false;
393
+ window.addEventListener("scroll", onPanelScroll, true);
394
+ window.addEventListener("resize", onPanelScroll);
395
+ }
396
+ function renderHead() {
397
+ closeFilterPanel();
398
+ headRow.textContent = "";
399
+ headRow.style.gridTemplateColumns = template();
400
+ if (selectable) {
401
+ const cell = document.createElement("div");
402
+ cell.className = "dt-cell dt-cell--check";
403
+ const rows = pipeline();
404
+ const allKeys = rows.map((r) => keyOfRow(r));
405
+ const selCount = allKeys.filter((k) => selected.has(k)).length;
406
+ const box = makeCheck(selCount > 0 && selCount === allKeys.length, "\u5168\u9009");
407
+ box.indeterminate = selCount > 0 && selCount < allKeys.length;
408
+ box.addEventListener("change", () => {
409
+ if (box.checked) allKeys.forEach((k) => selected.add(k));
410
+ else allKeys.forEach((k) => selected.delete(k));
411
+ emitSelection();
412
+ renderBody();
413
+ renderHead();
414
+ });
415
+ cell.appendChild(box);
416
+ headRow.appendChild(cell);
417
+ }
418
+ if (expandable) {
419
+ const cell = document.createElement("div");
420
+ cell.className = "dt-cell dt-cell--expand";
421
+ headRow.appendChild(cell);
422
+ }
423
+ for (const col of cols()) {
424
+ const cell = document.createElement("div");
425
+ cell.className = "dt-cell dt-cell--head";
426
+ if (col.align) cell.dataset.align = col.align;
427
+ if (col.frozen) cell.classList.add("dt-cell--frozen");
428
+ const title = document.createElement("span");
429
+ title.className = "dt-head-title";
430
+ title.textContent = col.title;
431
+ cell.appendChild(title);
432
+ if (col.sortable) {
433
+ cell.classList.add("is-sortable");
434
+ cell.setAttribute("role", "columnheader");
435
+ cell.tabIndex = 0;
436
+ const active = sortKey === col.key;
437
+ cell.setAttribute("aria-sort", active ? sortDir === "asc" ? "ascending" : "descending" : "none");
438
+ const arrow = document.createElement("span");
439
+ arrow.className = "dt-sort";
440
+ arrow.textContent = active ? sortDir === "asc" ? "\u2191" : "\u2193" : "\u2195";
441
+ if (active) arrow.classList.add("is-active");
442
+ cell.appendChild(arrow);
443
+ const toggleSort = () => {
444
+ if (sortKey !== col.key) {
445
+ sortKey = col.key;
446
+ sortDir = "asc";
447
+ } else if (sortDir === "asc") sortDir = "desc";
448
+ else sortKey = null;
449
+ invalidate();
450
+ opts.onSortChange?.(sortKey, sortDir);
451
+ renderAll();
452
+ };
453
+ cell.addEventListener("click", (ev) => {
454
+ if (ev.target.closest(".dt-filter-btn, .dt-resize")) return;
455
+ toggleSort();
456
+ });
457
+ cell.addEventListener("keydown", (ev) => {
458
+ if (ev.key === "Enter" || ev.key === " ") {
459
+ ev.preventDefault();
460
+ toggleSort();
461
+ }
462
+ });
463
+ }
464
+ if (col.filterable) {
465
+ const hasFilter = filters.has(col.key);
466
+ const btn = document.createElement("button");
467
+ btn.type = "button";
468
+ btn.className = "dt-filter-btn";
469
+ if (hasFilter) btn.classList.add("is-active");
470
+ btn.setAttribute("aria-label", `\u7B5B\u9009\u300C${col.title}\u300D`);
471
+ btn.appendChild(svgIcon(ICON_FUNNEL, 12));
472
+ btn.addEventListener("click", (ev) => {
473
+ ev.stopPropagation();
474
+ if (openPanel?.key === col.key) closeFilterPanel();
475
+ else {
476
+ closeFilterPanel();
477
+ openFilterPanel(col, btn);
478
+ }
479
+ });
480
+ cell.appendChild(btn);
481
+ }
482
+ if (resizable && col !== cols()[cols().length - 1]) {
483
+ const handle = document.createElement("div");
484
+ handle.className = "dt-resize";
485
+ handle.setAttribute("role", "separator");
486
+ handle.setAttribute("aria-orientation", "vertical");
487
+ handle.setAttribute("aria-label", `\u8C03\u6574\u300C${col.title}\u300D\u5217\u5BBD`);
488
+ attachResize(handle, col);
489
+ cell.appendChild(handle);
490
+ }
491
+ headRow.appendChild(cell);
492
+ }
493
+ }
494
+ function attachResize(handle, col) {
495
+ handle.addEventListener("pointerdown", (ev) => {
496
+ ev.preventDefault();
497
+ ev.stopPropagation();
498
+ const startX = ev.clientX;
499
+ const startW = handle.closest(".dt-cell").getBoundingClientRect().width;
500
+ handle.setPointerCapture(ev.pointerId);
501
+ const onMove = (e) => {
502
+ const delta = e.clientX - startX;
503
+ const next = Math.max(48, Math.round(startW + delta));
504
+ colWidths.set(col.key, `${next}px`);
505
+ headRow.style.gridTemplateColumns = template();
506
+ body.querySelectorAll(".dt-row").forEach((r) => {
507
+ r.style.gridTemplateColumns = template();
508
+ });
509
+ };
510
+ const onUp = (e) => {
511
+ handle.releasePointerCapture(e.pointerId);
512
+ handle.removeEventListener("pointermove", onMove);
513
+ handle.removeEventListener("pointerup", onUp);
514
+ };
515
+ handle.addEventListener("pointermove", onMove);
516
+ handle.addEventListener("pointerup", onUp);
517
+ });
518
+ }
519
+ function openColumnPanel(btn) {
520
+ const panel2 = document.createElement("div");
521
+ panel2.className = "dt-filter-panel dt-col-panel";
522
+ panel2.hidden = true;
523
+ panel2.addEventListener("click", (ev) => ev.stopPropagation());
524
+ const list = document.createElement("div");
525
+ list.className = "dt-filter-list";
526
+ for (const col of opts.columns) {
527
+ if (col.key === "__select" || col.key === "__expand") continue;
528
+ const row = document.createElement("label");
529
+ row.className = "dt-filter-item";
530
+ const isHidden = colHidden.get(col.key) ?? col.hidden ?? false;
531
+ const box = makeCheck(!isHidden, col.title);
532
+ box.addEventListener("change", () => {
533
+ if (box.checked) colHidden.delete(col.key);
534
+ else colHidden.set(col.key, true);
535
+ invalidate();
536
+ renderAll();
537
+ });
538
+ const text = document.createElement("span");
539
+ text.textContent = col.title;
540
+ row.append(box, text);
541
+ list.appendChild(row);
542
+ }
543
+ panel2.appendChild(list);
544
+ document.body.appendChild(panel2);
545
+ openPanel = { key: "__cols", el: panel2 };
546
+ const rect = btn.getBoundingClientRect();
547
+ const pr = panel2.getBoundingClientRect();
548
+ let x = rect.right - pr.width;
549
+ let y = rect.bottom + 4;
550
+ if (x < 8) x = 8;
551
+ if (y + pr.height > window.innerHeight - 8) y = rect.top - pr.height - 4;
552
+ panel2.style.left = `${x}px`;
553
+ panel2.style.top = `${y}px`;
554
+ panel2.hidden = false;
555
+ window.addEventListener("scroll", onPanelScroll, true);
556
+ window.addEventListener("resize", onPanelScroll);
557
+ }
558
+ function exportCSV(filename) {
559
+ const rows = pipeline();
560
+ const visible = cols();
561
+ const header = visible.map((c) => csvEscape(c.title)).join(",");
562
+ const lines = rows.map(
563
+ (row) => visible.map((c) => csvEscape(String(row[c.key] ?? ""))).join(",")
564
+ );
565
+ const csv = "\uFEFF" + [header, ...lines].join("\r\n");
566
+ const blob = new Blob([csv], { type: "text/csv;charset=utf-8;" });
567
+ const url = URL.createObjectURL(blob);
568
+ const a = document.createElement("a");
569
+ a.href = url;
570
+ a.download = filename ?? "export.csv";
571
+ a.click();
572
+ URL.revokeObjectURL(url);
573
+ }
574
+ function csvEscape(s) {
575
+ if (/[",\r\n]/.test(s)) return `"${s.replace(/"/g, '""')}"`;
576
+ return s;
577
+ }
578
+ function buildFilterPanel(col) {
579
+ const panel2 = document.createElement("div");
580
+ panel2.className = "dt-filter-panel";
581
+ panel2.hidden = true;
582
+ panel2.addEventListener("click", (ev) => ev.stopPropagation());
583
+ const list = document.createElement("div");
584
+ list.className = "dt-filter-list";
585
+ const values = Array.from(new Set(data.map((r) => String(r[col.key] ?? "")))).sort((a, b) => a.localeCompare(b, "zh-CN", { numeric: true }));
586
+ for (const v of values) {
587
+ const row = document.createElement("label");
588
+ row.className = "dt-filter-item";
589
+ const box = makeCheck(filters.get(col.key)?.has(v) ?? true, v);
590
+ box.addEventListener("change", () => {
591
+ const cur = new Set(filters.get(col.key) ?? values);
592
+ if (box.checked) cur.add(v);
593
+ else cur.delete(v);
594
+ if (cur.size === values.length) filters.delete(col.key);
595
+ else filters.set(col.key, cur);
596
+ invalidate();
597
+ page = 1;
598
+ renderAll();
599
+ });
600
+ const text = document.createElement("span");
601
+ text.textContent = v === "" ? "\uFF08\u7A7A\uFF09" : v;
602
+ row.append(box, text);
603
+ list.appendChild(row);
604
+ }
605
+ const footRow = document.createElement("div");
606
+ footRow.className = "dt-filter-foot";
607
+ const reset = document.createElement("button");
608
+ reset.type = "button";
609
+ reset.className = "dt-filter-reset";
610
+ reset.textContent = "\u91CD\u7F6E";
611
+ reset.addEventListener("click", () => {
612
+ filters.delete(col.key);
613
+ invalidate();
614
+ page = 1;
615
+ closeFilterPanel();
616
+ renderAll();
617
+ });
618
+ footRow.appendChild(reset);
619
+ panel2.append(list, footRow);
620
+ return panel2;
621
+ }
622
+ function makeRow(row, rowIndex, key) {
623
+ const tr = document.createElement("div");
624
+ tr.className = "dt-row";
625
+ tr.setAttribute("role", "row");
626
+ tr.style.gridTemplateColumns = template();
627
+ if (virtual) tr.style.height = `${rowH}px`;
628
+ if (selected.has(key)) tr.classList.add("is-selected");
629
+ if (opts.rowClassName) {
630
+ const extra = opts.rowClassName(row, rowIndex);
631
+ if (extra) tr.classList.add(...extra.split(/\s+/).filter(Boolean));
632
+ }
633
+ if (selectable) {
634
+ const cell = document.createElement("div");
635
+ cell.className = "dt-cell dt-cell--check";
636
+ if (frozenOffsets().size > 0 && selectable) {
637
+ cell.classList.add("dt-cell--frozen");
638
+ cell.style.left = "0px";
639
+ }
640
+ const box = makeCheck(selected.has(key), "\u9009\u62E9\u8BE5\u884C");
641
+ box.addEventListener("click", (ev) => {
642
+ box._shift = ev.shiftKey;
643
+ ev.stopPropagation();
644
+ });
645
+ box.addEventListener("change", () => {
646
+ const shift = box._shift === true;
647
+ if (shift && lastClickedIndex >= 0 && lastClickedIndex !== rowIndex) {
648
+ const rows = pipeline();
649
+ const [lo, hi] = lastClickedIndex < rowIndex ? [lastClickedIndex, rowIndex] : [rowIndex, lastClickedIndex];
650
+ const willSelect = box.checked;
651
+ for (let i = lo; i <= hi; i++) {
652
+ const k = keyOfRow(rows[i]);
653
+ if (willSelect) selected.add(k);
654
+ else selected.delete(k);
655
+ }
656
+ renderBody();
657
+ } else {
658
+ if (box.checked) selected.add(key);
659
+ else selected.delete(key);
660
+ tr.classList.toggle("is-selected", box.checked);
661
+ }
662
+ lastClickedIndex = rowIndex;
663
+ emitSelection();
664
+ renderHead();
665
+ renderFoot(pipeline().length);
666
+ });
667
+ cell.appendChild(box);
668
+ tr.appendChild(cell);
669
+ }
670
+ if (expandable) {
671
+ const cell = document.createElement("div");
672
+ cell.className = "dt-cell dt-cell--expand";
673
+ const btn = document.createElement("button");
674
+ btn.type = "button";
675
+ btn.className = "dt-expand-btn";
676
+ btn.setAttribute("aria-label", "\u5C55\u5F00\u884C");
677
+ btn.setAttribute("aria-expanded", String(expanded.has(key)));
678
+ btn.appendChild(svgIcon(ICON_CHEVRON, 14));
679
+ btn.addEventListener("click", (ev) => {
680
+ ev.stopPropagation();
681
+ if (expanded.has(key)) expanded.delete(key);
682
+ else expanded.add(key);
683
+ renderBody();
684
+ });
685
+ cell.appendChild(btn);
686
+ tr.appendChild(cell);
687
+ }
688
+ const offsets = frozenOffsets();
689
+ for (const col of cols()) {
690
+ const cell = document.createElement("div");
691
+ cell.className = "dt-cell";
692
+ if (col.align) cell.dataset.align = col.align;
693
+ if (col.frozen) {
694
+ cell.classList.add("dt-cell--frozen");
695
+ const off = offsets.get(col.key);
696
+ if (off != null) cell.style.left = `${off}px`;
697
+ }
698
+ fillCell(cell, col, row, rowIndex);
699
+ tr.appendChild(cell);
700
+ }
701
+ if (opts.onRowClick) {
702
+ tr.classList.add("is-clickable");
703
+ tr.addEventListener("click", () => opts.onRowClick(row));
704
+ }
705
+ if (opts.contextMenu) {
706
+ tr.addEventListener("contextmenu", (ev) => {
707
+ ev.preventDefault();
708
+ ev.stopPropagation();
709
+ openContextMenu(ev.clientX, ev.clientY, opts.contextMenu(row));
710
+ });
711
+ }
712
+ return tr;
713
+ }
714
+ function makeExpandRow(row) {
715
+ const wrap = document.createElement("div");
716
+ wrap.className = "dt-expand-row";
717
+ const out = expandable(row);
718
+ if (out instanceof Node) wrap.appendChild(out);
719
+ else wrap.textContent = String(out);
720
+ return wrap;
721
+ }
722
+ function visibleRange(total) {
723
+ if (!virtual) {
724
+ if (opts.pagination === false) return [0, total];
725
+ const start2 = (page - 1) * pageSize;
726
+ return [start2, Math.min(total, start2 + pageSize)];
727
+ }
728
+ const viewH = scroll.clientHeight || 420;
729
+ const overscan = 6;
730
+ const start = Math.max(0, Math.floor(scrollTop / rowH) - overscan);
731
+ const end = Math.min(total, Math.ceil((scrollTop + viewH) / rowH) + overscan);
732
+ return [start, end];
733
+ }
734
+ function renderBody() {
735
+ const rows = pipeline();
736
+ const total = rows.length;
737
+ const keepTop = virtual ? scroll.scrollTop : 0;
738
+ body.textContent = "";
739
+ if (opts.loading) {
740
+ for (let i = 0; i < Math.min(pageSize, 8); i++) {
741
+ const sk = document.createElement("div");
742
+ sk.className = "dt-skeleton-row";
743
+ const bar = document.createElement("div");
744
+ bar.className = "skeleton";
745
+ bar.style.height = "14px";
746
+ bar.style.width = `${88 - i % 3 * 14}%`;
747
+ sk.appendChild(bar);
748
+ body.appendChild(sk);
749
+ }
750
+ renderFoot(total);
751
+ return;
752
+ }
753
+ if (total === 0) {
754
+ const empty = document.createElement("div");
755
+ empty.className = "dt-empty";
756
+ empty.textContent = opts.emptyLabel ?? "\u6682\u65E0\u6570\u636E";
757
+ body.appendChild(empty);
758
+ renderFoot(total);
759
+ return;
760
+ }
761
+ const [start, end] = visibleRange(total);
762
+ const frag = document.createDocumentFragment();
763
+ if (virtual && start > 0) {
764
+ const top = document.createElement("div");
765
+ top.style.height = `${start * rowH}px`;
766
+ frag.appendChild(top);
767
+ }
768
+ for (let i = start; i < end; i++) {
769
+ const row = rows[i];
770
+ const key = keyOfRow(row);
771
+ frag.appendChild(makeRow(row, i, key));
772
+ if (expandable && expanded.has(key)) frag.appendChild(makeExpandRow(row));
773
+ }
774
+ if (virtual && end < total) {
775
+ const bottom = document.createElement("div");
776
+ bottom.style.height = `${(total - end) * rowH}px`;
777
+ frag.appendChild(bottom);
778
+ }
779
+ body.appendChild(frag);
780
+ if (virtual) scroll.scrollTop = keepTop;
781
+ renderFoot(total);
782
+ }
783
+ function renderFoot(total) {
784
+ foot.textContent = "";
785
+ const stat = document.createElement("span");
786
+ stat.className = "dt-stat";
787
+ stat.textContent = `\u5171 ${total} \u6761`;
788
+ foot.appendChild(stat);
789
+ if (selectable && selected.size > 0) {
790
+ const sel = document.createElement("span");
791
+ sel.className = "dt-stat dt-stat--sel";
792
+ sel.textContent = `\u5DF2\u9009 ${selected.size} \u9879`;
793
+ const clear = document.createElement("button");
794
+ clear.type = "button";
795
+ clear.className = "dt-clear-sel";
796
+ clear.textContent = "\u6E05\u7A7A";
797
+ clear.addEventListener("click", () => {
798
+ selected.clear();
799
+ emitSelection();
800
+ renderAll();
801
+ });
802
+ sel.appendChild(clear);
803
+ foot.appendChild(sel);
804
+ }
805
+ if (virtual || opts.pagination === false || total <= pageSize) return;
806
+ const pages = Math.ceil(total / pageSize);
807
+ const pager = document.createElement("nav");
808
+ pager.className = "dt-pager";
809
+ pager.setAttribute("aria-label", "\u5206\u9875");
810
+ const sizeSel = document.createElement("select");
811
+ sizeSel.className = "dt-page-size";
812
+ sizeSel.setAttribute("aria-label", "\u6BCF\u9875\u6761\u6570");
813
+ for (const s of pageSizes) {
814
+ const o = document.createElement("option");
815
+ o.value = String(s);
816
+ o.textContent = `${s} \u6761/\u9875`;
817
+ if (s === pageSize) o.selected = true;
818
+ sizeSel.appendChild(o);
819
+ }
820
+ sizeSel.addEventListener("change", () => {
821
+ pageSize = Number(sizeSel.value);
822
+ page = 1;
823
+ renderAll();
824
+ });
825
+ pager.appendChild(sizeSel);
826
+ const pageBtn = (label, target, opts2 = {}) => {
827
+ const b = document.createElement("button");
828
+ b.type = "button";
829
+ b.className = "dt-page-btn";
830
+ if (opts2.active) b.classList.add("is-active");
831
+ b.disabled = opts2.disabled === true;
832
+ b.textContent = label;
833
+ if (opts2.aria) b.setAttribute("aria-label", opts2.aria);
834
+ b.addEventListener("click", () => {
835
+ page = target;
836
+ renderAll();
837
+ });
838
+ return b;
839
+ };
840
+ pager.appendChild(pageBtn("\u2039", page - 1, { disabled: page <= 1, aria: "\u4E0A\u4E00\u9875" }));
841
+ const win = [];
842
+ for (let p = 1; p <= pages; p++) {
843
+ if (p === 1 || p === pages || Math.abs(p - page) <= 2) win.push(p);
844
+ }
845
+ let last = 0;
846
+ for (const p of win) {
847
+ if (p - last > 1) {
848
+ const dots = document.createElement("span");
849
+ dots.className = "dt-page-dots";
850
+ dots.textContent = "\u2026";
851
+ pager.appendChild(dots);
852
+ }
853
+ pager.appendChild(pageBtn(String(p), p, { active: p === page }));
854
+ last = p;
855
+ }
856
+ pager.appendChild(pageBtn("\u203A", page + 1, { disabled: page >= pages, aria: "\u4E0B\u4E00\u9875" }));
857
+ foot.appendChild(pager);
858
+ }
859
+ let searchTimer = 0;
860
+ function onSearch() {
861
+ window.clearTimeout(searchTimer);
862
+ searchTimer = window.setTimeout(() => {
863
+ search = searchInput?.value.trim() ?? "";
864
+ invalidate();
865
+ page = 1;
866
+ renderAll();
867
+ }, 160);
868
+ }
869
+ searchInput?.addEventListener("input", onSearch);
870
+ function onScroll() {
871
+ scrollTop = scroll.scrollTop;
872
+ renderBody();
873
+ }
874
+ if (virtual) scroll.addEventListener("scroll", onScroll, { passive: true });
875
+ function onDocPointerDown(ev) {
876
+ if (!openPanel) return;
877
+ const t = ev.target;
878
+ if (t instanceof Node && (openPanel.el.contains(t) || headRow.contains(t) || toolbar?.contains(t))) return;
879
+ closeFilterPanel();
880
+ }
881
+ document.addEventListener("pointerdown", onDocPointerDown, true);
882
+ function onDocKeydown(ev) {
883
+ if (ev.key === "Escape") closeFilterPanel();
884
+ }
885
+ document.addEventListener("keydown", onDocKeydown);
886
+ function emitSelection() {
887
+ if (!opts.onSelectionChange) return;
888
+ const rows = data.filter((r) => selected.has(keyOfRow(r)));
889
+ opts.onSelectionChange(rows);
890
+ }
891
+ function renderAll() {
892
+ if (destroyed) return;
893
+ renderHead();
894
+ renderBody();
895
+ }
896
+ rekey();
897
+ renderAll();
898
+ return {
899
+ setData(next) {
900
+ data = next;
901
+ rekey();
902
+ invalidate();
903
+ page = 1;
904
+ renderAll();
905
+ },
906
+ getSelected() {
907
+ return data.filter((r) => selected.has(keyOfRow(r)));
908
+ },
909
+ clearSelection() {
910
+ selected.clear();
911
+ emitSelection();
912
+ renderAll();
913
+ },
914
+ setSearch(q) {
915
+ search = q;
916
+ if (searchInput) searchInput.value = q;
917
+ invalidate();
918
+ page = 1;
919
+ renderAll();
920
+ },
921
+ setFilter(key, values) {
922
+ if (values === null) filters.delete(key);
923
+ else filters.set(key, new Set(values));
924
+ invalidate();
925
+ page = 1;
926
+ renderAll();
927
+ },
928
+ setSort(key, dir = "asc") {
929
+ sortKey = key;
930
+ sortDir = dir;
931
+ invalidate();
932
+ opts.onSortChange?.(sortKey, sortDir);
933
+ renderAll();
934
+ },
935
+ getData() {
936
+ return pipeline();
937
+ },
938
+ exportCSV,
939
+ showColumn(key) {
940
+ colHidden.delete(key);
941
+ opts.columns.find((c) => c.key === key).hidden = false;
942
+ invalidate();
943
+ renderAll();
944
+ },
945
+ hideColumn(key) {
946
+ colHidden.set(key, true);
947
+ invalidate();
948
+ renderAll();
949
+ },
950
+ refresh() {
951
+ invalidate();
952
+ renderAll();
953
+ },
954
+ destroy() {
955
+ destroyed = true;
956
+ window.clearTimeout(searchTimer);
957
+ closeFilterPanel();
958
+ searchInput?.removeEventListener("input", onSearch);
959
+ if (virtual) scroll.removeEventListener("scroll", onScroll);
960
+ document.removeEventListener("pointerdown", onDocPointerDown, true);
961
+ document.removeEventListener("keydown", onDocKeydown);
962
+ el.textContent = "";
963
+ }
964
+ };
965
+ }
966
+ export {
967
+ createTable
968
+ };