@icen.ai/ui 0.5.0 → 0.8.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 (138) hide show
  1. package/AGENTS.md +256 -0
  2. package/README.md +47 -13
  3. package/dist/base.css +31 -4
  4. package/dist/behaviors/accordion.d.mts +4 -0
  5. package/dist/behaviors/accordion.mjs +4 -3
  6. package/dist/behaviors/ai-chat.d.mts +57 -0
  7. package/dist/behaviors/ai-chat.mjs +557 -0
  8. package/dist/behaviors/ai-composer.d.mts +112 -0
  9. package/dist/behaviors/ai-composer.mjs +2032 -0
  10. package/dist/behaviors/ai-core.d.mts +151 -0
  11. package/dist/behaviors/ai-core.mjs +491 -0
  12. package/dist/behaviors/ai-diff.d.mts +53 -0
  13. package/dist/behaviors/ai-diff.mjs +282 -0
  14. package/dist/behaviors/ai-panel.d.mts +98 -0
  15. package/dist/behaviors/ai-panel.mjs +1012 -0
  16. package/dist/behaviors/ai-provider.d.mts +198 -0
  17. package/dist/behaviors/ai-provider.mjs +1055 -0
  18. package/dist/behaviors/ai-tool.d.mts +18 -0
  19. package/dist/behaviors/ai-tool.mjs +2125 -0
  20. package/dist/behaviors/ai-tools.d.mts +81 -0
  21. package/dist/behaviors/ai-tools.mjs +1658 -0
  22. package/dist/behaviors/back-top.d.mts +20 -0
  23. package/dist/behaviors/back-top.mjs +49 -32
  24. package/dist/behaviors/carousel.d.mts +4 -0
  25. package/dist/behaviors/carousel.mjs +34 -16
  26. package/dist/behaviors/charts.d.mts +250 -0
  27. package/dist/behaviors/charts.mjs +1301 -448
  28. package/dist/behaviors/command-palette.d.mts +8 -0
  29. package/dist/behaviors/command-palette.mjs +24 -12
  30. package/dist/behaviors/context-menu.d.mts +36 -0
  31. package/dist/behaviors/context-menu.mjs +6 -2
  32. package/dist/behaviors/copy.d.mts +4 -0
  33. package/dist/behaviors/copy.mjs +7 -6
  34. package/dist/behaviors/datatable.d.mts +78 -0
  35. package/dist/behaviors/datatable.mjs +54 -11
  36. package/dist/behaviors/date-picker.d.mts +4 -0
  37. package/dist/behaviors/date-picker.mjs +104 -36
  38. package/dist/behaviors/dropdown.d.mts +4 -0
  39. package/dist/behaviors/dropdown.mjs +7 -3
  40. package/dist/behaviors/input.d.mts +4 -0
  41. package/dist/behaviors/input.mjs +29 -22
  42. package/dist/behaviors/modal.d.mts +11 -0
  43. package/dist/behaviors/modal.mjs +11 -6
  44. package/dist/behaviors/nav.d.mts +7 -0
  45. package/dist/behaviors/nav.mjs +25 -9
  46. package/dist/behaviors/notification.d.mts +172 -0
  47. package/dist/behaviors/notification.mjs +243 -89
  48. package/dist/behaviors/popover.d.mts +46 -0
  49. package/dist/behaviors/popover.mjs +2 -1
  50. package/dist/behaviors/select.d.mts +4 -0
  51. package/dist/behaviors/select.mjs +131 -26
  52. package/dist/behaviors/sidebar.d.mts +4 -0
  53. package/dist/behaviors/sidebar.mjs +4 -3
  54. package/dist/behaviors/slider.d.mts +4 -0
  55. package/dist/behaviors/slider.mjs +4 -3
  56. package/dist/behaviors/split-pane.d.mts +4 -0
  57. package/dist/behaviors/split-pane.mjs +13 -13
  58. package/dist/behaviors/tabs.d.mts +8 -0
  59. package/dist/behaviors/tabs.mjs +44 -19
  60. package/dist/behaviors/tag-input.d.mts +4 -0
  61. package/dist/behaviors/tag-input.mjs +12 -3
  62. package/dist/behaviors/theme.d.mts +25 -0
  63. package/dist/behaviors/theme.mjs +3 -1
  64. package/dist/behaviors/toast.d.mts +26 -0
  65. package/dist/behaviors/toast.mjs +24 -18
  66. package/dist/behaviors/tree.d.mts +4 -0
  67. package/dist/behaviors/tree.mjs +4 -3
  68. package/dist/behaviors/upload.d.mts +4 -0
  69. package/dist/behaviors/upload.mjs +63 -26
  70. package/dist/components/ai-chat.css +1037 -0
  71. package/dist/components/ai-chat.mjs +3 -0
  72. package/dist/components/ai-composer.mjs +3 -0
  73. package/dist/components/ai-context.mjs +2 -0
  74. package/dist/components/ai-diff.css +311 -0
  75. package/dist/components/ai-diff.mjs +3 -0
  76. package/dist/components/ai-files.mjs +3 -0
  77. package/dist/components/ai-message.mjs +3 -0
  78. package/dist/components/ai-panel.css +682 -0
  79. package/dist/components/ai-reasoning.mjs +3 -0
  80. package/dist/components/ai-subagent.mjs +2 -0
  81. package/dist/components/ai-todo.mjs +2 -0
  82. package/dist/components/ai-tool-call.mjs +2 -0
  83. package/dist/components/ai-tool.css +532 -0
  84. package/dist/components/ai-usage.mjs +3 -0
  85. package/dist/components/badge.css +4 -8
  86. package/dist/components/breadcrumb.css +1 -5
  87. package/dist/components/btn.css +4 -7
  88. package/dist/components/card.css +3 -6
  89. package/dist/components/chart-bar.css +10 -0
  90. package/dist/components/chart-calendar.mjs +5 -0
  91. package/dist/components/chart-gauge.css +1 -1
  92. package/dist/components/chart-heatmap.css +40 -0
  93. package/dist/components/chart-scatter.css +24 -0
  94. package/dist/components/chart-scatter.mjs +4 -0
  95. package/dist/components/charts.css +86 -1
  96. package/dist/components/charts.mjs +1 -0
  97. package/dist/components/command-palette.css +2 -2
  98. package/dist/components/content.css +8 -9
  99. package/dist/components/copy.css +8 -8
  100. package/dist/components/datatable.css +4 -8
  101. package/dist/components/date-picker.css +3 -7
  102. package/dist/components/empty.css +0 -4
  103. package/dist/components/feedback.css +10 -17
  104. package/dist/components/form.css +13 -14
  105. package/dist/components/input.css +2 -2
  106. package/dist/components/layout.css +36 -36
  107. package/dist/components/media.css +12 -8
  108. package/dist/components/menu.css +2 -2
  109. package/dist/components/modal.css +1 -1
  110. package/dist/components/nav.css +11 -6
  111. package/dist/components/notification.css +28 -28
  112. package/dist/components/pagination.css +1 -1
  113. package/dist/components/panel.css +16 -16
  114. package/dist/components/pill.css +15 -15
  115. package/dist/components/segmented.css +5 -5
  116. package/dist/components/segmented.mjs +1 -0
  117. package/dist/components/select.css +12 -20
  118. package/dist/components/sidebar.css +1 -1
  119. package/dist/components/split-pane.css +0 -1
  120. package/dist/components/stat.css +14 -18
  121. package/dist/components/steps.css +2 -2
  122. package/dist/components/switch.css +8 -12
  123. package/dist/components/table.css +46 -47
  124. package/dist/components/tabs.css +35 -46
  125. package/dist/components/tag-input.css +4 -4
  126. package/dist/components/toast.css +16 -13
  127. package/dist/components/tooltip.css +2 -2
  128. package/dist/index.d.mts +37 -0
  129. package/dist/index.mjs +7890 -1199
  130. package/dist/registry.json +113 -4
  131. package/dist/retro-effects.css +21 -17
  132. package/dist/tokens/colors.css +4 -1
  133. package/dist/tokens/retro-effects.css +21 -17
  134. package/dist/tokens/style-profiles.css +21 -0
  135. package/dist/tokens.css +25 -1
  136. package/dist/ui.css +3168 -433
  137. package/docs/spec/ai-native.md +659 -0
  138. package/package.json +15 -5
@@ -1,4 +1,101 @@
1
+ // src/behaviors/popover.ts
2
+ var DEFAULT_MIN_WIDTH = 220;
3
+ var DEFAULT_MAX_WIDTH = 420;
4
+ var DEFAULT_MIN_HEIGHT = 120;
5
+ var DEFAULT_MAX_HEIGHT = 420;
6
+ function clamp(value, min, max) {
7
+ if (max < min) return min;
8
+ return Math.min(max, Math.max(min, value));
9
+ }
10
+ function chooseSide(preferredSide, availableTop, availableBottom, desiredHeight) {
11
+ const preferredSpace = preferredSide === "top" ? availableTop : availableBottom;
12
+ const oppositeSpace = preferredSide === "top" ? availableBottom : availableTop;
13
+ if (preferredSpace >= desiredHeight) return preferredSide;
14
+ if (oppositeSpace >= desiredHeight) return preferredSide === "top" ? "bottom" : "top";
15
+ if (preferredSpace >= oppositeSpace) return preferredSide;
16
+ return preferredSide === "top" ? "bottom" : "top";
17
+ }
18
+ function horizontalAnchor(anchor, width, align) {
19
+ if (align === "center") return anchor.left + anchor.width / 2 - width / 2;
20
+ if (align === "end") return anchor.right - width;
21
+ return anchor.left;
22
+ }
23
+ function computePopoverLayout(anchor, viewport, opts = {}) {
24
+ const margin = opts.margin ?? 12;
25
+ const offset = opts.offset ?? 8;
26
+ const availableWidth = Math.max(80, viewport.width - margin * 2);
27
+ const requestedMinWidth = Math.max(1, opts.minWidth ?? DEFAULT_MIN_WIDTH);
28
+ const requestedMaxWidth = Math.max(requestedMinWidth, opts.maxWidth ?? DEFAULT_MAX_WIDTH);
29
+ const maxWidth = Math.min(requestedMaxWidth, availableWidth);
30
+ const minWidth = Math.min(requestedMinWidth, maxWidth);
31
+ const preferredSide = opts.side ?? "top";
32
+ const align = opts.align ?? "start";
33
+ const availableTop = Math.max(0, anchor.top - margin - offset);
34
+ const availableBottom = Math.max(0, viewport.height - anchor.bottom - margin - offset);
35
+ const availableHeight = Math.max(80, viewport.height - margin * 2);
36
+ const minHeight = Math.min(Math.max(80, opts.minHeight ?? DEFAULT_MIN_HEIGHT), availableHeight);
37
+ const requestedMaxHeight = Math.max(minHeight, opts.maxHeight ?? DEFAULT_MAX_HEIGHT);
38
+ const fallbackHeight = Math.min(260, requestedMaxHeight);
39
+ const contentHeight = Math.max(opts.contentHeightHint ?? 0, fallbackHeight);
40
+ const desiredHeight = clamp(contentHeight, minHeight, Math.min(requestedMaxHeight, availableHeight));
41
+ const side = chooseSide(preferredSide, availableTop, availableBottom, desiredHeight);
42
+ const sideSpace = side === "top" ? availableTop : availableBottom;
43
+ const maxHeight = Math.min(
44
+ requestedMaxHeight,
45
+ Math.max(80, sideSpace || Math.max(availableTop, availableBottom)),
46
+ availableHeight
47
+ );
48
+ const positionedHeight = clamp(desiredHeight, 40, maxHeight);
49
+ const width = align === "stretch" ? clamp(anchor.width, minWidth, maxWidth) : maxWidth;
50
+ const left = clamp(horizontalAnchor(anchor, width, align), margin, viewport.width - margin - width);
51
+ const desiredTop = side === "top" ? anchor.top - offset - positionedHeight : anchor.bottom + offset;
52
+ const top = clamp(desiredTop, margin, viewport.height - margin - positionedHeight);
53
+ return { side, left, top, width, maxHeight };
54
+ }
55
+
1
56
  // src/behaviors/select.ts
57
+ var openSelect = null;
58
+ var globalBound = false;
59
+ var PANEL_OFFSET = 4;
60
+ var PANEL_MAX_HEIGHT = 240;
61
+ function onGlobalPointerDown(ev) {
62
+ const cur = openSelect;
63
+ if (!cur) return;
64
+ const t = ev.target;
65
+ if (!(t instanceof Node)) return;
66
+ if (cur.container.contains(t) || cur.panel.contains(t)) return;
67
+ cur.close();
68
+ }
69
+ function onGlobalKeyDown(ev) {
70
+ if (ev.key !== "Escape") return;
71
+ const cur = openSelect;
72
+ if (!cur) return;
73
+ cur.close();
74
+ cur.trigger.focus();
75
+ }
76
+ function onWindowChange() {
77
+ const cur = openSelect;
78
+ if (!cur) return;
79
+ if (!cur.trigger.isConnected) {
80
+ cur.close();
81
+ return;
82
+ }
83
+ cur.reposition();
84
+ }
85
+ function bindGlobalListeners() {
86
+ if (globalBound) return;
87
+ globalBound = true;
88
+ document.addEventListener("pointerdown", onGlobalPointerDown);
89
+ document.addEventListener("keydown", onGlobalKeyDown);
90
+ }
91
+ function bindOpenListeners() {
92
+ window.addEventListener("scroll", onWindowChange, true);
93
+ window.addEventListener("resize", onWindowChange);
94
+ }
95
+ function unbindOpenListeners() {
96
+ window.removeEventListener("scroll", onWindowChange, true);
97
+ window.removeEventListener("resize", onWindowChange);
98
+ }
2
99
  function setup(container) {
3
100
  const el = container;
4
101
  if (el.__icenSelectInit) return;
@@ -14,6 +111,7 @@ function setup(container) {
14
111
  const isSearchable = container.hasAttribute("data-select-search");
15
112
  const maxSelect = Number(container.getAttribute("data-select-max")) || 0;
16
113
  const placeholder = container.getAttribute("data-select-placeholder") ?? (valueEl?.classList.contains("is-empty") ? valueEl?.textContent ?? "" : "");
114
+ if (isMultiple) panel.setAttribute("data-select-multiple", "");
17
115
  let searchInput = null;
18
116
  let focusIdx = -1;
19
117
  if (isSearchable && !panel.querySelector(".select-search-input")) {
@@ -89,11 +187,37 @@ function setup(container) {
89
187
  }
90
188
  });
91
189
  }
190
+ function positionPanel() {
191
+ const rect = trigger.getBoundingClientRect();
192
+ const layout = computePopoverLayout(
193
+ rect,
194
+ { width: window.innerWidth, height: window.innerHeight },
195
+ {
196
+ side: "bottom",
197
+ align: "stretch",
198
+ offset: PANEL_OFFSET,
199
+ minWidth: rect.width,
200
+ maxWidth: rect.width,
201
+ maxHeight: PANEL_MAX_HEIGHT
202
+ }
203
+ );
204
+ panel.style.left = `${layout.left + window.scrollX}px`;
205
+ panel.style.top = `${layout.top + window.scrollY}px`;
206
+ panel.style.minWidth = `${layout.width}px`;
207
+ panel.style.width = `${layout.width}px`;
208
+ panel.style.maxHeight = `${layout.maxHeight}px`;
209
+ panel.dataset.side = layout.side;
210
+ }
92
211
  function openPanel() {
93
212
  if (isOpen()) return;
213
+ openSelect?.close();
214
+ if (panel.parentElement !== document.body) document.body.appendChild(panel);
94
215
  panel.hidden = false;
95
216
  container.classList.add("is-open");
96
217
  trigger.setAttribute("aria-expanded", "true");
218
+ positionPanel();
219
+ openSelect = { container, trigger, panel, close: closePanel, reposition: positionPanel };
220
+ bindOpenListeners();
97
221
  if (searchInput) {
98
222
  searchInput.value = "";
99
223
  filterOptions("");
@@ -109,6 +233,8 @@ function setup(container) {
109
233
  container.classList.remove("is-open");
110
234
  trigger.setAttribute("aria-expanded", "false");
111
235
  setFocus(-1);
236
+ if (openSelect?.panel === panel) openSelect = null;
237
+ unbindOpenListeners();
112
238
  }
113
239
  function getSelectedValues() {
114
240
  return allOptions().filter((o) => o.classList.contains("is-selected")).map((o) => o.getAttribute("data-value") ?? "");
@@ -188,20 +314,6 @@ function setup(container) {
188
314
  }
189
315
  return;
190
316
  }
191
- if (searchInput && document.activeElement === searchInput) {
192
- if (ev.key === "ArrowDown") {
193
- ev.preventDefault();
194
- moveFocus(1);
195
- } else if (ev.key === "ArrowUp") {
196
- ev.preventDefault();
197
- moveFocus(-1);
198
- } else if (ev.key === "Enter") {
199
- ev.preventDefault();
200
- const opt = visibleOptions()[focusIdx];
201
- if (opt) choose(opt);
202
- }
203
- return;
204
- }
205
317
  switch (ev.key) {
206
318
  case "ArrowDown":
207
319
  ev.preventDefault();
@@ -238,22 +350,15 @@ function setup(container) {
238
350
  const idx = visibleOptions().indexOf(opt);
239
351
  if (idx >= 0 && !opt.classList.contains("is-disabled")) setFocus(idx);
240
352
  });
241
- document.addEventListener("pointerdown", (ev) => {
242
- if (isOpen() && ev.target instanceof Node && !container.contains(ev.target)) closePanel();
243
- });
244
- document.addEventListener("keydown", (ev) => {
245
- if (ev.key === "Escape" && isOpen()) {
246
- closePanel();
247
- trigger.focus();
248
- }
249
- });
250
353
  if (isMultiple) updateMultiTrigger();
251
354
  }
252
- function initSelect(root = document) {
355
+ function initSelect(root) {
253
356
  if (typeof document === "undefined") return;
357
+ const scope = root ?? document;
358
+ bindGlobalListeners();
254
359
  const containers = [];
255
- if (root instanceof Element && root.matches("[data-select]")) containers.push(root);
256
- containers.push(...Array.from(root.querySelectorAll("[data-select]")));
360
+ if (scope instanceof Element && scope.matches("[data-select]")) containers.push(scope);
361
+ containers.push(...Array.from(scope.querySelectorAll("[data-select]")));
257
362
  for (const c of containers) setup(c);
258
363
  }
259
364
  export {
@@ -0,0 +1,4 @@
1
+ /** 为 root 下每个 .sidebar 初始化(root 自身是 .sidebar 也算)。 */
2
+ declare function initSidebar(root?: ParentNode): void;
3
+
4
+ export { initSidebar };
@@ -27,11 +27,12 @@ function setup(sidebar) {
27
27
  }));
28
28
  });
29
29
  }
30
- function initSidebar(root = document) {
30
+ function initSidebar(root) {
31
31
  if (typeof document === "undefined") return;
32
+ const scope = root ?? document;
32
33
  const sidebars = [];
33
- if (root instanceof Element && root.matches(".sidebar")) sidebars.push(root);
34
- sidebars.push(...Array.from(root.querySelectorAll(".sidebar")));
34
+ if (scope instanceof Element && scope.matches(".sidebar")) sidebars.push(scope);
35
+ sidebars.push(...Array.from(scope.querySelectorAll(".sidebar")));
35
36
  for (const s of sidebars) setup(s);
36
37
  }
37
38
  export {
@@ -0,0 +1,4 @@
1
+ /** 为 root 下每个 .slider 容器初始化(root 自身是 .slider 也算)。 */
2
+ declare function initSlider(root?: ParentNode): void;
3
+
4
+ export { initSlider };
@@ -92,11 +92,12 @@ function setup(slider) {
92
92
  if (slider.classList.contains("slider--dual")) setupDual(slider, natives);
93
93
  else setupSingle(slider, natives[0]);
94
94
  }
95
- function initSlider(root = document) {
95
+ function initSlider(root) {
96
96
  if (typeof document === "undefined") return;
97
+ const scope = root ?? document;
97
98
  const containers = [];
98
- if (root instanceof Element && root.matches(".slider")) containers.push(root);
99
- containers.push(...Array.from(root.querySelectorAll(".slider")));
99
+ if (scope instanceof Element && scope.matches(".slider")) containers.push(scope);
100
+ containers.push(...Array.from(scope.querySelectorAll(".slider")));
100
101
  for (const c of containers) setup(c);
101
102
  }
102
103
  export {
@@ -0,0 +1,4 @@
1
+ /** 为 root 下每个 .split-pane[data-split-pane] 初始化(root 自身匹配也算)。 */
2
+ declare function initSplitPane(root?: ParentNode): void;
3
+
4
+ export { initSplitPane };
@@ -1,5 +1,9 @@
1
1
  // src/behaviors/split-pane.ts
2
- var initialized = /* @__PURE__ */ new WeakSet();
2
+ function numOr(value, fallback) {
3
+ if (value === void 0 || value === "") return fallback;
4
+ const n = Number(value);
5
+ return Number.isFinite(n) ? n : fallback;
6
+ }
3
7
  function pct(value, min, max) {
4
8
  return Math.min(max, Math.max(min, value));
5
9
  }
@@ -10,9 +14,9 @@ function setup(container) {
10
14
  const dividerQuery = el.querySelector(".split-pane-divider");
11
15
  if (!dividerQuery) return;
12
16
  const divider = dividerQuery;
13
- const min = Number(el.dataset.splitMin ?? "10") || 10;
14
- const max = Number(el.dataset.splitMax ?? "90") || 90;
15
- const step = Number(el.dataset.splitStep ?? "2") || 2;
17
+ const min = numOr(el.dataset.splitMin, 10);
18
+ const max = numOr(el.dataset.splitMax, 90);
19
+ const step = numOr(el.dataset.splitStep, 2);
16
20
  const vertical = el.classList.contains("split-pane--vertical");
17
21
  const orientation = vertical ? "vertical" : "horizontal";
18
22
  divider.setAttribute("role", "separator");
@@ -85,17 +89,13 @@ function setup(container) {
85
89
  write(next);
86
90
  });
87
91
  }
88
- function initSplitPane(root = document) {
92
+ function initSplitPane(root) {
89
93
  if (typeof document === "undefined") return;
94
+ const scope = root ?? document;
90
95
  const panes = [];
91
- if (root instanceof Element && root.matches(".split-pane[data-split-pane]")) panes.push(root);
92
- panes.push(...Array.from(root.querySelectorAll(".split-pane[data-split-pane]")));
93
- for (const p of panes) {
94
- const el = p;
95
- if (initialized.has(el)) continue;
96
- initialized.add(el);
97
- setup(el);
98
- }
96
+ if (scope instanceof Element && scope.matches(".split-pane[data-split-pane]")) panes.push(scope);
97
+ panes.push(...Array.from(scope.querySelectorAll(".split-pane[data-split-pane]")));
98
+ for (const p of panes) setup(p);
99
99
  }
100
100
  export {
101
101
  initSplitPane
@@ -0,0 +1,8 @@
1
+ /**
2
+ * 为 root 下每个 [data-tabs] 容器初始化(root 自身是 [data-tabs] 也算)。
3
+ * 返回销毁函数:调用后移除所有容器监听与 window hashchange,并复位幂等标记
4
+ * (销毁后的容器可被再次 initTabs 重新接管)。SSR 下返回 no-op 函数。
5
+ */
6
+ declare function initTabs(root?: ParentNode): () => void;
7
+
8
+ export { initTabs };
@@ -8,28 +8,40 @@ function tabNames(container) {
8
8
  function currentHash() {
9
9
  return window.location.hash.replace(/^#/, "");
10
10
  }
11
- function activate(container, name, writeHash) {
11
+ function activate(container, name, writeHash, emit = false) {
12
12
  const buttons = tabButtons(container);
13
13
  let matched = false;
14
14
  buttons.forEach((t) => {
15
15
  const on = t.getAttribute("data-tab") === name;
16
16
  if (on) matched = true;
17
- t.classList.toggle("active", on);
17
+ t.classList.toggle("is-active", on);
18
18
  t.setAttribute("aria-selected", String(on));
19
19
  t.tabIndex = on ? 0 : -1;
20
20
  });
21
21
  if (!matched) return;
22
+ let panel = null;
22
23
  container.querySelectorAll("[data-tab-panel]").forEach((p) => {
23
24
  const isOn = p.getAttribute("data-tab-panel") === name;
24
25
  p.hidden = !isOn;
25
- if (isOn && !p.getAttribute("aria-labelledby")) {
26
- const btn = buttons.find((b) => b.getAttribute("data-tab") === name);
27
- if (btn?.id) p.setAttribute("aria-labelledby", btn.id);
26
+ if (isOn) {
27
+ panel = p;
28
+ if (!p.getAttribute("aria-labelledby")) {
29
+ const btn = buttons.find((b) => b.getAttribute("data-tab") === name);
30
+ if (btn?.id) p.setAttribute("aria-labelledby", btn.id);
31
+ }
28
32
  }
29
33
  });
30
34
  if (writeHash && typeof window !== "undefined") {
31
35
  window.history.replaceState(null, "", `#${name}`);
32
36
  }
37
+ if (emit) {
38
+ const index = buttons.findIndex((b) => b.getAttribute("data-tab") === name);
39
+ const tab = buttons[index] ?? null;
40
+ container.dispatchEvent(new CustomEvent("icen:tab-change", {
41
+ bubbles: true,
42
+ detail: { tab, panel, index }
43
+ }));
44
+ }
33
45
  }
34
46
  function isDisabled(btn) {
35
47
  return btn.hasAttribute("disabled") || btn.getAttribute("aria-disabled") === "true" || btn.classList.contains("is-disabled");
@@ -52,7 +64,7 @@ function nextEnabled(container, from, dir) {
52
64
  }
53
65
  function setup(container) {
54
66
  const el = container;
55
- if (el.__icenTabsInit) return;
67
+ if (el.__icenTabsInit) return void 0;
56
68
  el.__icenTabsInit = true;
57
69
  const useHash = container.hasAttribute("data-tabs-hash");
58
70
  const names = tabNames(container);
@@ -71,7 +83,7 @@ function setup(container) {
71
83
  if (!btn || btn.closest("[data-tabs]") !== container) return;
72
84
  if (btn instanceof HTMLElement && isDisabled(btn)) return;
73
85
  const name = btn.getAttribute("data-tab");
74
- if (name) activate(container, name, useHash);
86
+ if (name) activate(container, name, useHash, true);
75
87
  };
76
88
  const onKeyDown = (ev) => {
77
89
  const kev = ev;
@@ -99,12 +111,12 @@ function setup(container) {
99
111
  }
100
112
  case "Home": {
101
113
  kev.preventDefault();
102
- focusTab(container, 0);
114
+ focusTab(container, nextEnabled(container, -1, 1));
103
115
  break;
104
116
  }
105
117
  case "End": {
106
118
  kev.preventDefault();
107
- focusTab(container, buttonsList.length - 1);
119
+ focusTab(container, nextEnabled(container, buttonsList.length, -1));
108
120
  break;
109
121
  }
110
122
  case "Enter":
@@ -112,7 +124,7 @@ function setup(container) {
112
124
  if (btn instanceof HTMLElement && !isDisabled(btn)) {
113
125
  kev.preventDefault();
114
126
  const name = btn.getAttribute("data-tab");
115
- if (name) activate(container, name, useHash);
127
+ if (name) activate(container, name, useHash, true);
116
128
  }
117
129
  break;
118
130
  }
@@ -125,29 +137,42 @@ function setup(container) {
125
137
  const h = currentHash();
126
138
  if (names.includes(h)) initial = h;
127
139
  } else {
128
- const n = container.querySelector("[data-tab].active")?.getAttribute("data-tab");
140
+ const n = container.querySelector("[data-tab].is-active")?.getAttribute("data-tab");
129
141
  if (n && names.includes(n)) initial = n;
130
142
  }
131
143
  activate(container, initial ?? names[0], false);
132
144
  const onHash = () => {
133
145
  const h = currentHash();
134
- if (names.includes(h)) activate(container, h, false);
146
+ if (names.includes(h)) activate(container, h, false, true);
135
147
  };
136
148
  if (useHash && typeof window !== "undefined") {
137
149
  window.addEventListener("hashchange", onHash);
138
150
  }
139
- el.__icenTabsCleanup = () => {
151
+ const cleanup = () => {
140
152
  container.removeEventListener("click", onClick);
141
153
  container.removeEventListener("keydown", onKeyDown);
142
154
  window.removeEventListener("hashchange", onHash);
155
+ el.__icenTabsInit = false;
143
156
  };
157
+ el.__icenTabsCleanup = cleanup;
158
+ return cleanup;
144
159
  }
145
- function initTabs(root = document) {
146
- if (typeof document === "undefined") return;
147
- const containers = [];
148
- if (root instanceof Element && root.matches("[data-tabs]")) containers.push(root);
149
- containers.push(...Array.from(root.querySelectorAll("[data-tabs]")));
150
- for (const c of containers) setup(c);
160
+ function initTabs(root) {
161
+ const cleanups = [];
162
+ if (typeof document !== "undefined") {
163
+ const scope = root ?? document;
164
+ const containers = [];
165
+ if (scope instanceof Element && scope.matches("[data-tabs]")) containers.push(scope);
166
+ containers.push(...Array.from(scope.querySelectorAll("[data-tabs]")));
167
+ for (const c of containers) {
168
+ const cleanup = setup(c);
169
+ if (cleanup) cleanups.push(cleanup);
170
+ }
171
+ }
172
+ return () => {
173
+ for (const fn of cleanups) fn();
174
+ cleanups.length = 0;
175
+ };
151
176
  }
152
177
  export {
153
178
  initTabs
@@ -0,0 +1,4 @@
1
+ /** 为 root 下每个 .tag-input 容器初始化(root 自身匹配也算;data-tags 为可选初始值配置)。 */
2
+ declare function initTagInput(root?: ParentNode): void;
3
+
4
+ export { initTagInput };
@@ -9,6 +9,13 @@ function setup(wrap) {
9
9
  const maxAttr = wrap.getAttribute("data-max");
10
10
  const max = maxAttr ? Number(maxAttr) || Infinity : Infinity;
11
11
  const placeholder = field.placeholder;
12
+ let isComposing = false;
13
+ field.addEventListener("compositionstart", () => {
14
+ isComposing = true;
15
+ });
16
+ field.addEventListener("compositionend", () => {
17
+ isComposing = false;
18
+ });
12
19
  let tags = (wrap.getAttribute("data-tags") ?? "").split(",").map((s) => s.trim()).filter(Boolean);
13
20
  function sync() {
14
21
  wrap.setAttribute("data-tags", tags.join(","));
@@ -63,6 +70,7 @@ function setup(wrap) {
63
70
  emit();
64
71
  }
65
72
  field.addEventListener("keydown", (ev) => {
73
+ if (isComposing || ev.isComposing) return;
66
74
  if (ev.key === "Enter" || ev.key === ",") {
67
75
  ev.preventDefault();
68
76
  commit();
@@ -73,11 +81,12 @@ function setup(wrap) {
73
81
  field.addEventListener("blur", commit);
74
82
  render();
75
83
  }
76
- function initTagInput(root = document) {
84
+ function initTagInput(root) {
77
85
  if (typeof document === "undefined") return;
86
+ const scope = root ?? document;
78
87
  const containers = [];
79
- if (root instanceof HTMLElement && root.matches(".tag-input[data-tags]")) containers.push(root);
80
- containers.push(...Array.from(root.querySelectorAll(".tag-input[data-tags]")));
88
+ if (scope instanceof HTMLElement && scope.matches(".tag-input")) containers.push(scope);
89
+ containers.push(...Array.from(scope.querySelectorAll(".tag-input")));
81
90
  for (const c of containers) setup(c);
82
91
  }
83
92
  export {
@@ -0,0 +1,25 @@
1
+ interface ThemeState {
2
+ preset: string;
3
+ dark: boolean;
4
+ style: string;
5
+ }
6
+ declare const PRESETS: readonly ["clay", "piano", "art", "vangogh", "ink", "retro"];
7
+ declare const STYLES: readonly ["modern", "retro", "terminal"];
8
+ declare const STORAGE_KEY = "icen.ui.theme";
9
+ /** 默认主题:clay + 跟随系统明暗 + modern。SSR 下返回离线兜底值。 */
10
+ declare function getDefaultTheme(): ThemeState;
11
+ /** 读 localStorage 中的持久化主题;缺失或坏数据回退默认。 */
12
+ declare function getTheme(): ThemeState;
13
+ /** 把主题应用到 <html>:先清掉所有 preset/style/dark 相关类,再按 t 挂回。 */
14
+ declare function applyTheme(t: ThemeState): void;
15
+ /** 合并当前主题与 patch,应用并持久化,返回新态。patch 中的非法枚举值回退默认。 */
16
+ declare function setTheme(patch: Partial<ThemeState>): ThemeState;
17
+ /** 切换明暗,返回新态。 */
18
+ declare function toggleDark(): ThemeState;
19
+ /** 页面加载早期调用:读持久化主题并应用,防闪烁。
20
+ * 无持久化偏好时,自动跟随系统的 prefers-color-scheme 变化(实时);
21
+ * 一旦用户显式 setTheme / toggleDark,跟随停止(持久化优先)。
22
+ * 重复调用幂等(系统监听不会重复挂)。 */
23
+ declare function initTheme(): ThemeState;
24
+
25
+ export { PRESETS, STORAGE_KEY, STYLES, type ThemeState, applyTheme, getDefaultTheme, getTheme, initTheme, setTheme, toggleDark };
@@ -62,10 +62,12 @@ function setTheme(patch) {
62
62
  function toggleDark() {
63
63
  return setTheme({ dark: !getTheme().dark });
64
64
  }
65
+ var systemListenerAttached = false;
65
66
  function initTheme() {
66
67
  const t = getTheme();
67
68
  applyTheme(t);
68
- if (isBrowser() && typeof window.matchMedia === "function" && !hasPersistedPreference()) {
69
+ if (isBrowser() && typeof window.matchMedia === "function" && !systemListenerAttached && !hasPersistedPreference()) {
70
+ systemListenerAttached = true;
69
71
  let lastDark = t.dark;
70
72
  try {
71
73
  const mql = window.matchMedia("(prefers-color-scheme: dark)");
@@ -0,0 +1,26 @@
1
+ type ToastKind = 'ok' | 'err' | 'warn' | 'info';
2
+ interface ToastOptions {
3
+ /** 保留时长(毫秒),默认 2600;带 action 时默认 6000 */
4
+ ms?: number;
5
+ /** 配合 action:可撤销 / 重试 / 查看等 */
6
+ actionLabel?: string;
7
+ /** action 触发回调;返回 false 阻止 toast 关闭 */
8
+ onAction?: () => void | Promise<void> | boolean;
9
+ }
10
+ interface ToastConfig {
11
+ /** 同屏最大堆叠,默认 5;超出从最早的开始移除 */
12
+ maxStack?: number;
13
+ }
14
+ interface ToastFn {
15
+ (message: string, kind?: ToastKind, ms?: number): void;
16
+ ok(message: string, ms?: number): void;
17
+ err(message: string, ms?: number): void;
18
+ warn(message: string, ms?: number): void;
19
+ info(message: string, ms?: number): void;
20
+ action(message: string, actionLabel: string, onAction: () => void, ms?: number): void;
21
+ dismiss(): void;
22
+ config(opts: ToastConfig): void;
23
+ }
24
+ declare const toast: ToastFn;
25
+
26
+ export { type ToastConfig, type ToastKind, type ToastOptions, toast };
@@ -16,21 +16,35 @@ function ensureContainer() {
16
16
  document.body.appendChild(el);
17
17
  return el;
18
18
  }
19
+ var KIND_CLASS = {
20
+ ok: "toast--success",
21
+ err: "toast--error",
22
+ warn: "toast--warning",
23
+ info: "toast--info"
24
+ };
25
+ function scheduleRemove(el) {
26
+ el.classList.add("is-leaving");
27
+ let done = false;
28
+ const finish = () => {
29
+ if (done) return;
30
+ done = true;
31
+ el.remove();
32
+ };
33
+ el.addEventListener("transitionend", finish, { once: true });
34
+ window.setTimeout(finish, 400);
35
+ }
19
36
  function trimStack(container) {
20
37
  for (; ; ) {
21
38
  const items = Array.from(container.querySelectorAll(":scope > .toast"));
22
39
  if (items.length <= maxStack) return;
23
40
  const oldest = items[0];
24
41
  if (!oldest) return;
25
- oldest.classList.add("leaving");
26
- let done = false;
27
- const finish = () => {
28
- if (done) return;
29
- done = true;
42
+ if (oldest.classList.contains("is-leaving")) {
30
43
  oldest.remove();
31
- };
32
- oldest.addEventListener("transitionend", finish, { once: true });
33
- window.setTimeout(finish, 400);
44
+ continue;
45
+ }
46
+ scheduleRemove(oldest);
47
+ return;
34
48
  }
35
49
  }
36
50
  function show(message, kind = "info", opts = {}) {
@@ -38,7 +52,7 @@ function show(message, kind = "info", opts = {}) {
38
52
  const { ms, actionLabel, onAction } = opts;
39
53
  const container = ensureContainer();
40
54
  const el = document.createElement("div");
41
- el.className = kind === "info" ? "toast" : `toast ${kind}`;
55
+ el.className = `toast ${KIND_CLASS[kind]}`;
42
56
  el.setAttribute("role", kind === "err" ? "alert" : "status");
43
57
  const msgNode = document.createElement("span");
44
58
  msgNode.className = "toast-msg";
@@ -58,15 +72,7 @@ function show(message, kind = "info", opts = {}) {
58
72
  const remove = () => {
59
73
  if (removed) return;
60
74
  removed = true;
61
- el.classList.add("leaving");
62
- let done = false;
63
- const finish = () => {
64
- if (done) return;
65
- done = true;
66
- el.remove();
67
- };
68
- el.addEventListener("transitionend", finish, { once: true });
69
- window.setTimeout(finish, 400);
75
+ scheduleRemove(el);
70
76
  };
71
77
  const duration = ms ?? (actionLabel ? ACTION_MS : DEFAULT_MS);
72
78
  const timer = window.setTimeout(remove, duration);
@@ -0,0 +1,4 @@
1
+ /** 为 root 下每棵 .tree 初始化(root 自身是 .tree 也算)。 */
2
+ declare function initTree(root?: ParentNode): void;
3
+
4
+ export { initTree };
@@ -88,11 +88,12 @@ function setup(tree) {
88
88
  }
89
89
  });
90
90
  }
91
- function initTree(root = document) {
91
+ function initTree(root) {
92
92
  if (typeof document === "undefined") return;
93
+ const scope = root ?? document;
93
94
  const trees = [];
94
- if (root instanceof Element && root.matches(".tree")) trees.push(root);
95
- trees.push(...Array.from(root.querySelectorAll(".tree")));
95
+ if (scope instanceof Element && scope.matches(".tree")) trees.push(scope);
96
+ trees.push(...Array.from(scope.querySelectorAll(".tree")));
96
97
  for (const t of trees) setup(t);
97
98
  }
98
99
  export {
@@ -0,0 +1,4 @@
1
+ /** 为 root 下每个 .upload 容器初始化(root 自身是 .upload 也算)。 */
2
+ declare function initUpload(root?: ParentNode): void;
3
+
4
+ export { initUpload };