@icen.ai/ui 0.8.0 → 0.9.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 (228) hide show
  1. package/AGENTS.md +63 -14
  2. package/README.md +17 -9
  3. package/dist/base.css +4 -3
  4. package/dist/behaviors/accordion.d.mts +5 -2
  5. package/dist/behaviors/accordion.mjs +36 -11
  6. package/dist/behaviors/ai-branch.d.mts +19 -0
  7. package/dist/behaviors/ai-branch.mjs +217 -0
  8. package/dist/behaviors/ai-chat.d.mts +11 -7
  9. package/dist/behaviors/ai-chat.mjs +134 -51
  10. package/dist/behaviors/ai-composer.d.mts +28 -3
  11. package/dist/behaviors/ai-composer.mjs +399 -229
  12. package/dist/behaviors/ai-core.d.mts +9 -4
  13. package/dist/behaviors/ai-core.mjs +21 -8
  14. package/dist/behaviors/ai-diff.d.mts +2 -2
  15. package/dist/behaviors/ai-diff.mjs +172 -41
  16. package/dist/behaviors/ai-feedback.d.mts +26 -0
  17. package/dist/behaviors/ai-feedback.mjs +303 -0
  18. package/dist/behaviors/ai-panel.d.mts +26 -9
  19. package/dist/behaviors/ai-panel.mjs +211 -110
  20. package/dist/behaviors/ai-provider.mjs +54 -48
  21. package/dist/behaviors/ai-threads.d.mts +38 -0
  22. package/dist/behaviors/ai-threads.mjs +319 -0
  23. package/dist/behaviors/ai-tool.d.mts +12 -4
  24. package/dist/behaviors/ai-tool.mjs +316 -263
  25. package/dist/behaviors/ai-tools.d.mts +9 -6
  26. package/dist/behaviors/ai-tools.mjs +264 -155
  27. package/dist/behaviors/back-top.mjs +16 -1
  28. package/dist/behaviors/carousel.d.mts +5 -2
  29. package/dist/behaviors/carousel.mjs +53 -26
  30. package/dist/behaviors/charts.d.mts +170 -22
  31. package/dist/behaviors/charts.mjs +1016 -150
  32. package/dist/behaviors/command-palette.d.mts +40 -6
  33. package/dist/behaviors/command-palette.mjs +143 -15
  34. package/dist/behaviors/context-menu.d.mts +21 -4
  35. package/dist/behaviors/context-menu.mjs +122 -38
  36. package/dist/behaviors/controls.d.mts +59 -0
  37. package/dist/behaviors/controls.mjs +601 -0
  38. package/dist/behaviors/copy.d.mts +5 -2
  39. package/dist/behaviors/copy.mjs +37 -12
  40. package/dist/behaviors/datatable.d.mts +12 -0
  41. package/dist/behaviors/datatable.mjs +226 -43
  42. package/dist/behaviors/date-picker.d.mts +2 -2
  43. package/dist/behaviors/date-picker.mjs +171 -32
  44. package/dist/behaviors/dropdown.d.mts +6 -2
  45. package/dist/behaviors/dropdown.mjs +164 -25
  46. package/dist/behaviors/events.d.mts +523 -0
  47. package/dist/behaviors/events.mjs +208 -0
  48. package/dist/behaviors/input.mjs +25 -12
  49. package/dist/behaviors/kb-access.d.mts +34 -0
  50. package/dist/behaviors/kb-access.mjs +569 -0
  51. package/dist/behaviors/kb-acl.d.mts +50 -0
  52. package/dist/behaviors/kb-acl.mjs +405 -0
  53. package/dist/behaviors/kb-answer.d.mts +38 -0
  54. package/dist/behaviors/kb-answer.mjs +2055 -0
  55. package/dist/behaviors/kb-audit.d.mts +23 -0
  56. package/dist/behaviors/kb-audit.mjs +375 -0
  57. package/dist/behaviors/kb-canvas.d.mts +44 -0
  58. package/dist/behaviors/kb-canvas.mjs +589 -0
  59. package/dist/behaviors/kb-chain.d.mts +15 -0
  60. package/dist/behaviors/kb-chain.mjs +270 -0
  61. package/dist/behaviors/kb-checkpoint.d.mts +16 -0
  62. package/dist/behaviors/kb-checkpoint.mjs +342 -0
  63. package/dist/behaviors/kb-chunks.d.mts +31 -0
  64. package/dist/behaviors/kb-chunks.mjs +475 -0
  65. package/dist/behaviors/kb-citation.d.mts +38 -0
  66. package/dist/behaviors/kb-citation.mjs +677 -0
  67. package/dist/behaviors/kb-clarify.d.mts +20 -0
  68. package/dist/behaviors/kb-clarify.mjs +248 -0
  69. package/dist/behaviors/kb-conflict.d.mts +22 -0
  70. package/dist/behaviors/kb-conflict.mjs +284 -0
  71. package/dist/behaviors/kb-connector.d.mts +17 -0
  72. package/dist/behaviors/kb-connector.mjs +375 -0
  73. package/dist/behaviors/kb-core.d.mts +529 -0
  74. package/dist/behaviors/kb-core.mjs +890 -0
  75. package/dist/behaviors/kb-eval.d.mts +22 -0
  76. package/dist/behaviors/kb-eval.mjs +408 -0
  77. package/dist/behaviors/kb-explain.d.mts +11 -0
  78. package/dist/behaviors/kb-explain.mjs +243 -0
  79. package/dist/behaviors/kb-filter.d.mts +31 -0
  80. package/dist/behaviors/kb-filter.mjs +587 -0
  81. package/dist/behaviors/kb-gap.d.mts +27 -0
  82. package/dist/behaviors/kb-gap.mjs +273 -0
  83. package/dist/behaviors/kb-hittest.d.mts +34 -0
  84. package/dist/behaviors/kb-hittest.mjs +409 -0
  85. package/dist/behaviors/kb-metadata.d.mts +29 -0
  86. package/dist/behaviors/kb-metadata.mjs +326 -0
  87. package/dist/behaviors/kb-passage.d.mts +41 -0
  88. package/dist/behaviors/kb-passage.mjs +529 -0
  89. package/dist/behaviors/kb-pipeline.d.mts +18 -0
  90. package/dist/behaviors/kb-pipeline.mjs +381 -0
  91. package/dist/behaviors/kb-qa.d.mts +26 -0
  92. package/dist/behaviors/kb-qa.mjs +332 -0
  93. package/dist/behaviors/kb-rerank.d.mts +20 -0
  94. package/dist/behaviors/kb-rerank.mjs +315 -0
  95. package/dist/behaviors/kb-retrieval.d.mts +49 -0
  96. package/dist/behaviors/kb-retrieval.mjs +746 -0
  97. package/dist/behaviors/kb-review.d.mts +44 -0
  98. package/dist/behaviors/kb-review.mjs +451 -0
  99. package/dist/behaviors/kb-sandbox.d.mts +33 -0
  100. package/dist/behaviors/kb-sandbox.mjs +325 -0
  101. package/dist/behaviors/kb-segment.d.mts +32 -0
  102. package/dist/behaviors/kb-segment.mjs +499 -0
  103. package/dist/behaviors/kb-sources.d.mts +27 -0
  104. package/dist/behaviors/kb-sources.mjs +373 -0
  105. package/dist/behaviors/kb-sql.d.mts +43 -0
  106. package/dist/behaviors/kb-sql.mjs +948 -0
  107. package/dist/behaviors/kb-trace.d.mts +16 -0
  108. package/dist/behaviors/kb-trace.mjs +414 -0
  109. package/dist/behaviors/kb-visibility.d.mts +44 -0
  110. package/dist/behaviors/kb-visibility.mjs +403 -0
  111. package/dist/behaviors/kb-who-can.d.mts +32 -0
  112. package/dist/behaviors/kb-who-can.mjs +379 -0
  113. package/dist/behaviors/modal.d.mts +24 -5
  114. package/dist/behaviors/modal.mjs +76 -10
  115. package/dist/behaviors/nav.d.mts +1 -1
  116. package/dist/behaviors/nav.mjs +33 -7
  117. package/dist/behaviors/notification.d.mts +20 -4
  118. package/dist/behaviors/notification.mjs +53 -73
  119. package/dist/behaviors/popover.d.mts +39 -3
  120. package/dist/behaviors/popover.mjs +90 -4
  121. package/dist/behaviors/select.d.mts +4 -2
  122. package/dist/behaviors/select.mjs +161 -24
  123. package/dist/behaviors/sidebar.d.mts +5 -2
  124. package/dist/behaviors/sidebar.mjs +43 -13
  125. package/dist/behaviors/slider.d.mts +5 -2
  126. package/dist/behaviors/slider.mjs +54 -43
  127. package/dist/behaviors/split-pane.d.mts +6 -2
  128. package/dist/behaviors/split-pane.mjs +62 -21
  129. package/dist/behaviors/tabs.d.mts +3 -2
  130. package/dist/behaviors/tabs.mjs +65 -14
  131. package/dist/behaviors/tag-input.d.mts +5 -2
  132. package/dist/behaviors/tag-input.mjs +61 -21
  133. package/dist/behaviors/theme.d.mts +13 -2
  134. package/dist/behaviors/theme.mjs +48 -4
  135. package/dist/behaviors/toast.d.mts +30 -8
  136. package/dist/behaviors/toast.mjs +74 -7
  137. package/dist/behaviors/tree.d.mts +5 -2
  138. package/dist/behaviors/tree.mjs +38 -15
  139. package/dist/behaviors/upload.mjs +21 -23
  140. package/dist/cli.mjs +1 -1
  141. package/dist/components/ai-branch.mjs +3 -0
  142. package/dist/components/ai-chat.css +295 -0
  143. package/dist/components/ai-feedback.mjs +3 -0
  144. package/dist/components/ai-panel.css +218 -1
  145. package/dist/components/ai-threads.mjs +3 -0
  146. package/dist/components/ai-tool-call.mjs +1 -1
  147. package/dist/components/badge.css +1 -1
  148. package/dist/components/breadcrumb.css +0 -32
  149. package/dist/components/chart-calendar.mjs +1 -2
  150. package/dist/components/chart-graph.css +57 -0
  151. package/dist/components/chart-graph.mjs +4 -0
  152. package/dist/components/chart-map.mjs +3 -0
  153. package/dist/components/chart-pie.css +0 -6
  154. package/dist/components/chart-scatter.mjs +1 -1
  155. package/dist/components/charts.css +33 -1
  156. package/dist/components/charts.mjs +1 -0
  157. package/dist/components/content.css +3 -16
  158. package/dist/components/datatable.css +1 -1
  159. package/dist/components/date-picker.css +1 -1
  160. package/dist/components/kb-access.mjs +4 -0
  161. package/dist/components/kb-acl.mjs +4 -0
  162. package/dist/components/kb-agent.css +481 -0
  163. package/dist/components/kb-answer.mjs +4 -0
  164. package/dist/components/kb-audit.mjs +4 -0
  165. package/dist/components/kb-canvas.mjs +4 -0
  166. package/dist/components/kb-chain.mjs +4 -0
  167. package/dist/components/kb-checkpoint.mjs +4 -0
  168. package/dist/components/kb-chunks.mjs +4 -0
  169. package/dist/components/kb-citation.mjs +4 -0
  170. package/dist/components/kb-clarify.mjs +4 -0
  171. package/dist/components/kb-conflict.mjs +4 -0
  172. package/dist/components/kb-connector.mjs +4 -0
  173. package/dist/components/kb-data.css +649 -0
  174. package/dist/components/kb-eval.mjs +4 -0
  175. package/dist/components/kb-explain.mjs +4 -0
  176. package/dist/components/kb-filter.mjs +4 -0
  177. package/dist/components/kb-gap.mjs +4 -0
  178. package/dist/components/kb-ground.css +566 -0
  179. package/dist/components/kb-hittest.mjs +4 -0
  180. package/dist/components/kb-ingest.css +898 -0
  181. package/dist/components/kb-metadata.mjs +4 -0
  182. package/dist/components/kb-ops.css +548 -0
  183. package/dist/components/kb-passage.mjs +4 -0
  184. package/dist/components/kb-perm.css +444 -0
  185. package/dist/components/kb-pipeline.mjs +4 -0
  186. package/dist/components/kb-qa.mjs +4 -0
  187. package/dist/components/kb-rerank.mjs +4 -0
  188. package/dist/components/kb-retrieval.mjs +4 -0
  189. package/dist/components/kb-review.mjs +4 -0
  190. package/dist/components/kb-sandbox.mjs +4 -0
  191. package/dist/components/kb-search.css +940 -0
  192. package/dist/components/kb-segment.mjs +4 -0
  193. package/dist/components/kb-sources.mjs +4 -0
  194. package/dist/components/kb-sql.mjs +4 -0
  195. package/dist/components/kb-trace.mjs +4 -0
  196. package/dist/components/kb-visibility.mjs +4 -0
  197. package/dist/components/kb-who-can.mjs +4 -0
  198. package/dist/components/kb.css +252 -0
  199. package/dist/components/kb.mjs +9 -0
  200. package/dist/components/notification.css +2 -3
  201. package/dist/components/pagination.css +3 -1
  202. package/dist/components/pagination.mjs +1 -0
  203. package/dist/components/rating.mjs +1 -0
  204. package/dist/components/segmented.css +9 -1
  205. package/dist/components/segmented.mjs +1 -1
  206. package/dist/components/select.css +67 -4
  207. package/dist/components/steps.css +13 -3
  208. package/dist/components/steps.mjs +1 -0
  209. package/dist/components/switch.css +3 -1
  210. package/dist/components/switch.mjs +1 -0
  211. package/dist/components/table.mjs +1 -0
  212. package/dist/components/tabs.css +23 -0
  213. package/dist/components/tag-input.css +1 -6
  214. package/dist/components/upload.css +0 -15
  215. package/dist/index.d.mts +48 -11
  216. package/dist/index.mjs +13205 -3615
  217. package/dist/registry.json +366 -15
  218. package/dist/retro-effects.css +3 -16
  219. package/dist/tokens/retro-effects.css +3 -16
  220. package/dist/tokens/style-profiles.css +3 -2
  221. package/dist/tokens/typography.css +2 -0
  222. package/dist/tokens.css +5 -2
  223. package/dist/ui.css +5713 -267
  224. package/docs/research/2026-10-03-ai-kb-components.md +392 -0
  225. package/docs/research/2026-10-03-kb-permissions.md +992 -0
  226. package/docs/spec/ai-native.md +7 -4
  227. package/docs/spec/kb-family.md +443 -0
  228. package/package.json +2 -2
@@ -1,3 +1,10 @@
1
+ // src/behaviors/events.ts
2
+ function emitIcen(source, type, detail) {
3
+ return source.dispatchEvent(
4
+ new CustomEvent(type, { detail, bubbles: true, composed: true, cancelable: true })
5
+ );
6
+ }
7
+
1
8
  // src/behaviors/split-pane.ts
2
9
  function numOr(value, fallback) {
3
10
  if (value === void 0 || value === "") return fallback;
@@ -9,11 +16,10 @@ function pct(value, min, max) {
9
16
  }
10
17
  function setup(container) {
11
18
  const el = container;
12
- if (el.__icenSplitPaneInit) return;
19
+ if (el.__icenSplitPaneInit) return void 0;
20
+ const divider = el.querySelector(".split-pane-divider");
21
+ if (!divider) return void 0;
13
22
  el.__icenSplitPaneInit = true;
14
- const dividerQuery = el.querySelector(".split-pane-divider");
15
- if (!dividerQuery) return;
16
- const divider = dividerQuery;
17
23
  const min = numOr(el.dataset.splitMin, 10);
18
24
  const max = numOr(el.dataset.splitMax, 90);
19
25
  const step = numOr(el.dataset.splitStep, 2);
@@ -28,20 +34,25 @@ function setup(container) {
28
34
  const v = parseFloat(getComputedStyle(el).getPropertyValue("--split"));
29
35
  return Number.isFinite(v) ? v : 50;
30
36
  }
31
- function write(p) {
37
+ let cur = readCurrent();
38
+ const write = (p) => {
32
39
  const v = pct(p, min, max);
40
+ cur = v;
33
41
  el.style.setProperty("--split", v + "%");
34
42
  divider.setAttribute("aria-valuenow", String(Math.round(v)));
43
+ };
44
+ function emitChange() {
45
+ emitIcen(el, "icen:split-change", { value: Math.round(cur * 10) / 10 });
35
46
  }
36
47
  if (!el.style.getPropertyValue("--split")) write(50);
37
48
  divider.setAttribute("aria-valuenow", String(Math.round(readCurrent())));
38
49
  let dragging = false;
39
- divider.addEventListener("pointerdown", (ev) => {
50
+ const onDown = (ev) => {
40
51
  dragging = true;
41
52
  divider.classList.add("is-dragging");
42
53
  divider.setPointerCapture(ev.pointerId);
43
54
  el.style.userSelect = "none";
44
- });
55
+ };
45
56
  const onUp = (ev) => {
46
57
  if (!dragging) return;
47
58
  dragging = false;
@@ -51,14 +62,16 @@ function setup(container) {
51
62
  } catch {
52
63
  }
53
64
  el.style.userSelect = "";
65
+ emitChange();
54
66
  };
55
- divider.addEventListener("pointerup", onUp);
56
- divider.addEventListener("pointercancel", () => {
67
+ const onCancel = () => {
68
+ if (!dragging) return;
57
69
  dragging = false;
58
70
  divider.classList.remove("is-dragging");
59
71
  el.style.userSelect = "";
60
- });
61
- divider.addEventListener("pointermove", (ev) => {
72
+ emitChange();
73
+ };
74
+ const onMove = (ev) => {
62
75
  if (!dragging) return;
63
76
  const rect = el.getBoundingClientRect();
64
77
  if (vertical) {
@@ -70,9 +83,12 @@ function setup(container) {
70
83
  const p = (ev.clientX - rect.left) / rect.width * 100;
71
84
  write(p);
72
85
  }
73
- });
74
- divider.addEventListener("dblclick", () => write(50));
75
- divider.addEventListener("keydown", (ev) => {
86
+ };
87
+ const onDblClick = () => {
88
+ write(50);
89
+ emitChange();
90
+ };
91
+ const onKeyDown = (ev) => {
76
92
  const current = readCurrent();
77
93
  let next = null;
78
94
  if (vertical) {
@@ -87,15 +103,40 @@ function setup(container) {
87
103
  if (next === null) return;
88
104
  ev.preventDefault();
89
105
  write(next);
90
- });
106
+ emitChange();
107
+ };
108
+ divider.addEventListener("pointerdown", onDown);
109
+ divider.addEventListener("pointerup", onUp);
110
+ divider.addEventListener("pointercancel", onCancel);
111
+ divider.addEventListener("pointermove", onMove);
112
+ divider.addEventListener("dblclick", onDblClick);
113
+ divider.addEventListener("keydown", onKeyDown);
114
+ return () => {
115
+ divider.removeEventListener("pointerdown", onDown);
116
+ divider.removeEventListener("pointerup", onUp);
117
+ divider.removeEventListener("pointercancel", onCancel);
118
+ divider.removeEventListener("pointermove", onMove);
119
+ divider.removeEventListener("dblclick", onDblClick);
120
+ divider.removeEventListener("keydown", onKeyDown);
121
+ el.__icenSplitPaneInit = false;
122
+ };
91
123
  }
92
124
  function initSplitPane(root) {
93
- if (typeof document === "undefined") return;
94
- const scope = root ?? document;
95
- const panes = [];
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);
125
+ const cleanups = [];
126
+ if (typeof document !== "undefined") {
127
+ const scope = root ?? document;
128
+ const panes = [];
129
+ if (scope instanceof Element && scope.matches(".split-pane[data-split-pane]")) panes.push(scope);
130
+ panes.push(...Array.from(scope.querySelectorAll(".split-pane[data-split-pane]")));
131
+ for (const p of panes) {
132
+ const cleanup = setup(p);
133
+ if (cleanup) cleanups.push(cleanup);
134
+ }
135
+ }
136
+ return () => {
137
+ for (const fn of cleanups) fn();
138
+ cleanups.length = 0;
139
+ };
99
140
  }
100
141
  export {
101
142
  initSplitPane
@@ -1,7 +1,8 @@
1
1
  /**
2
2
  * 为 root 下每个 [data-tabs] 容器初始化(root 自身是 [data-tabs] 也算)。
3
- * 返回销毁函数:调用后移除所有容器监听与 window hashchange,并复位幂等标记
4
- * (销毁后的容器可被再次 initTabs 重新接管)。SSR 下返回 no-op 函数。
3
+ * 返回销毁函数:调用后移除所有容器监听、window hashchange/resize 监听与行为层创建的
4
+ * .tabs-ink 元素,并复位幂等标记(销毁后的容器可被再次 initTabs 重新接管)。
5
+ * SSR 下返回 no-op 函数。
5
6
  */
6
7
  declare function initTabs(root?: ParentNode): () => void;
7
8
 
@@ -1,3 +1,10 @@
1
+ // src/behaviors/events.ts
2
+ function emitIcen(source, type, detail) {
3
+ return source.dispatchEvent(
4
+ new CustomEvent(type, { detail, bubbles: true, composed: true, cancelable: true })
5
+ );
6
+ }
7
+
1
8
  // src/behaviors/tabs.ts
2
9
  function tabButtons(container) {
3
10
  return Array.from(container.querySelectorAll("[data-tab]"));
@@ -8,6 +15,20 @@ function tabNames(container) {
8
15
  function currentHash() {
9
16
  return window.location.hash.replace(/^#/, "");
10
17
  }
18
+ function hashWriteMethod(container) {
19
+ const raw = container.getAttribute("data-tabs-hash");
20
+ if (raw === null || raw === "false") return null;
21
+ return raw === "push" ? "push" : "replace";
22
+ }
23
+ function syncInk(container, name) {
24
+ const btn = tabButtons(container).find((b) => b.getAttribute("data-tab") === name);
25
+ const tablist = btn?.parentElement;
26
+ if (!btn || !tablist) return;
27
+ const ink = tablist.querySelector(":scope > .tabs-ink");
28
+ if (!ink) return;
29
+ ink.style.width = `${btn.offsetWidth}px`;
30
+ ink.style.transform = `translateX(${btn.offsetLeft}px)`;
31
+ }
11
32
  function activate(container, name, writeHash, emit = false) {
12
33
  const buttons = tabButtons(container);
13
34
  let matched = false;
@@ -32,15 +53,14 @@ function activate(container, name, writeHash, emit = false) {
32
53
  }
33
54
  });
34
55
  if (writeHash && typeof window !== "undefined") {
35
- window.history.replaceState(null, "", `#${name}`);
56
+ const method = hashWriteMethod(container);
57
+ if (method) window.history[method === "push" ? "pushState" : "replaceState"](null, "", `#${name}`);
36
58
  }
59
+ syncInk(container, name);
37
60
  if (emit) {
38
61
  const index = buttons.findIndex((b) => b.getAttribute("data-tab") === name);
39
62
  const tab = buttons[index] ?? null;
40
- container.dispatchEvent(new CustomEvent("icen:tab-change", {
41
- bubbles: true,
42
- detail: { tab, panel, index }
43
- }));
63
+ emitIcen(container, "icen:tab-change", { tab, panel, index });
44
64
  }
45
65
  }
46
66
  function isDisabled(btn) {
@@ -65,10 +85,11 @@ function nextEnabled(container, from, dir) {
65
85
  function setup(container) {
66
86
  const el = container;
67
87
  if (el.__icenTabsInit) return void 0;
68
- el.__icenTabsInit = true;
69
- const useHash = container.hasAttribute("data-tabs-hash");
88
+ const useHash = hashWriteMethod(container) !== null;
89
+ const autoActivate = container.getAttribute("data-tabs-activation") === "auto";
70
90
  const names = tabNames(container);
71
91
  if (names.length === 0) return;
92
+ el.__icenTabsInit = true;
72
93
  const buttons = tabButtons(container);
73
94
  buttons.forEach((b) => {
74
95
  if (!b.getAttribute("role")) b.setAttribute("role", "tab");
@@ -76,6 +97,18 @@ function setup(container) {
76
97
  });
77
98
  const tablist = buttons[0]?.parentElement;
78
99
  if (tablist && !tablist.getAttribute("role")) tablist.setAttribute("role", "tablist");
100
+ let ink = null;
101
+ if (tablist instanceof HTMLElement && tablist.hasAttribute("data-tabs-ink")) {
102
+ ink = document.createElement("div");
103
+ ink.className = "tabs-ink";
104
+ ink.setAttribute("aria-hidden", "true");
105
+ const initialBtn = buttons.find((b) => b.classList.contains("is-active")) ?? buttons.find((b) => b.getAttribute("data-tab") === names[0]);
106
+ if (initialBtn) {
107
+ ink.style.width = `${initialBtn.offsetWidth}px`;
108
+ ink.style.transform = `translateX(${initialBtn.offsetLeft}px)`;
109
+ }
110
+ tablist.appendChild(ink);
111
+ }
79
112
  const onClick = (ev) => {
80
113
  const target = ev.target;
81
114
  if (!(target instanceof Element)) return;
@@ -85,6 +118,14 @@ function setup(container) {
85
118
  const name = btn.getAttribute("data-tab");
86
119
  if (name) activate(container, name, useHash, true);
87
120
  };
121
+ const followFocus = (idx) => {
122
+ focusTab(container, idx);
123
+ if (!autoActivate) return;
124
+ const target = tabButtons(container)[idx];
125
+ if (!target || isDisabled(target)) return;
126
+ const name = target.getAttribute("data-tab");
127
+ if (name) activate(container, name, useHash, true);
128
+ };
88
129
  const onKeyDown = (ev) => {
89
130
  const kev = ev;
90
131
  const target = kev.target;
@@ -98,25 +139,23 @@ function setup(container) {
98
139
  case "ArrowRight":
99
140
  case "ArrowDown": {
100
141
  kev.preventDefault();
101
- const next = nextEnabled(container, currentIdx, 1);
102
- if (next >= 0) focusTab(container, next);
142
+ followFocus(nextEnabled(container, currentIdx, 1));
103
143
  break;
104
144
  }
105
145
  case "ArrowLeft":
106
146
  case "ArrowUp": {
107
147
  kev.preventDefault();
108
- const prev = nextEnabled(container, currentIdx, -1);
109
- if (prev >= 0) focusTab(container, prev);
148
+ followFocus(nextEnabled(container, currentIdx, -1));
110
149
  break;
111
150
  }
112
151
  case "Home": {
113
152
  kev.preventDefault();
114
- focusTab(container, nextEnabled(container, -1, 1));
153
+ followFocus(nextEnabled(container, -1, 1));
115
154
  break;
116
155
  }
117
156
  case "End": {
118
157
  kev.preventDefault();
119
- focusTab(container, nextEnabled(container, buttonsList.length, -1));
158
+ followFocus(nextEnabled(container, buttonsList.length, -1));
120
159
  break;
121
160
  }
122
161
  case "Enter":
@@ -148,13 +187,25 @@ function setup(container) {
148
187
  if (useHash && typeof window !== "undefined") {
149
188
  window.addEventListener("hashchange", onHash);
150
189
  }
190
+ const onResize = () => {
191
+ const active = tabButtons(container).find((b) => b.classList.contains("is-active"));
192
+ const name = active?.getAttribute("data-tab");
193
+ if (name) syncInk(container, name);
194
+ };
195
+ if (ink && typeof window !== "undefined") {
196
+ window.addEventListener("resize", onResize, { passive: true });
197
+ }
151
198
  const cleanup = () => {
152
199
  container.removeEventListener("click", onClick);
153
200
  container.removeEventListener("keydown", onKeyDown);
154
201
  window.removeEventListener("hashchange", onHash);
202
+ if (ink) {
203
+ window.removeEventListener("resize", onResize);
204
+ ink.remove();
205
+ ink = null;
206
+ }
155
207
  el.__icenTabsInit = false;
156
208
  };
157
- el.__icenTabsCleanup = cleanup;
158
209
  return cleanup;
159
210
  }
160
211
  function initTabs(root) {
@@ -1,4 +1,7 @@
1
- /** 为 root 下每个 .tag-input 容器初始化(root 自身匹配也算;data-tags 为可选初始值配置)。 */
2
- declare function initTagInput(root?: ParentNode): void;
1
+ /**
2
+ * 为 root 下每个 .tag-input 容器初始化(root 自身匹配也算;data-tags 为可选初始值配置)。
3
+ * 返回销毁函数:移除本次挂上的全部监听并复位幂等标记(销毁后可重新 init)。SSR 下返回 no-op。
4
+ */
5
+ declare function initTagInput(root?: ParentNode): () => void;
3
6
 
4
7
  export { initTagInput };
@@ -1,37 +1,54 @@
1
+ // src/behaviors/events.ts
2
+ function emitIcen(source, type, detail) {
3
+ return source.dispatchEvent(
4
+ new CustomEvent(type, { detail, bubbles: true, composed: true, cancelable: true })
5
+ );
6
+ }
7
+
1
8
  // src/behaviors/tag-input.ts
2
9
  function setup(wrap) {
3
10
  const el = wrap;
4
- if (el.__icenTagInit) return;
11
+ if (el.__icenTagInit) return void 0;
5
12
  el.__icenTagInit = true;
6
13
  const fieldEl = wrap.querySelector(".tag-input-field");
7
- if (!fieldEl) return;
14
+ if (!fieldEl) return void 0;
8
15
  const field = fieldEl;
9
16
  const maxAttr = wrap.getAttribute("data-max");
10
17
  const max = maxAttr ? Number(maxAttr) || Infinity : Infinity;
11
18
  const placeholder = field.placeholder;
19
+ let alive = true;
12
20
  let isComposing = false;
13
- field.addEventListener("compositionstart", () => {
21
+ const onCompositionStart = () => {
14
22
  isComposing = true;
15
- });
16
- field.addEventListener("compositionend", () => {
23
+ };
24
+ const onCompositionEnd = () => {
17
25
  isComposing = false;
18
- });
26
+ };
19
27
  let tags = (wrap.getAttribute("data-tags") ?? "").split(",").map((s) => s.trim()).filter(Boolean);
28
+ const autoLabel = !wrap.hasAttribute("aria-label");
20
29
  function sync() {
21
30
  wrap.setAttribute("data-tags", tags.join(","));
22
31
  wrap.classList.toggle("is-max", tags.length >= max);
23
32
  field.placeholder = tags.length === 0 ? placeholder : "";
24
- if (!wrap.hasAttribute("aria-label")) {
25
- wrap.setAttribute("aria-label", `\u6807\u7B7E\u8F93\u5165\uFF0C\u5171 ${tags.length} \u4E2A`);
26
- }
33
+ if (autoLabel) wrap.setAttribute("aria-label", `\u6807\u7B7E\u8F93\u5165\uFF0C\u5171 ${tags.length} \u4E2A`);
27
34
  }
28
35
  function emit() {
29
- wrap.dispatchEvent(new CustomEvent("icen:tags-change", {
30
- bubbles: true,
31
- detail: { tags: [...tags] }
32
- }));
36
+ if (!alive) return;
37
+ emitIcen(wrap, "icen:tags-change", { tags: [...tags] });
38
+ }
39
+ let shakeTimer = null;
40
+ function shake() {
41
+ el.classList.remove("is-shake");
42
+ void el.offsetWidth;
43
+ el.classList.add("is-shake");
44
+ if (shakeTimer) clearTimeout(shakeTimer);
45
+ shakeTimer = setTimeout(() => {
46
+ el.classList.remove("is-shake");
47
+ shakeTimer = null;
48
+ }, 320);
33
49
  }
34
50
  function remove(index) {
51
+ if (!alive) return;
35
52
  tags.splice(index, 1);
36
53
  render();
37
54
  emit();
@@ -57,11 +74,13 @@ function setup(wrap) {
57
74
  sync();
58
75
  }
59
76
  function commit() {
77
+ if (!alive) return;
60
78
  const v = field.value.trim();
61
79
  if (!v) return;
62
80
  if (tags.length >= max) return;
63
81
  if (tags.includes(v)) {
64
82
  field.value = "";
83
+ shake();
65
84
  return;
66
85
  }
67
86
  tags.push(v);
@@ -69,7 +88,7 @@ function setup(wrap) {
69
88
  render();
70
89
  emit();
71
90
  }
72
- field.addEventListener("keydown", (ev) => {
91
+ const onKeyDown = (ev) => {
73
92
  if (isComposing || ev.isComposing) return;
74
93
  if (ev.key === "Enter" || ev.key === ",") {
75
94
  ev.preventDefault();
@@ -77,17 +96,38 @@ function setup(wrap) {
77
96
  } else if (ev.key === "Backspace" && !field.value && tags.length > 0) {
78
97
  remove(tags.length - 1);
79
98
  }
80
- });
99
+ };
100
+ field.addEventListener("compositionstart", onCompositionStart);
101
+ field.addEventListener("compositionend", onCompositionEnd);
102
+ field.addEventListener("keydown", onKeyDown);
81
103
  field.addEventListener("blur", commit);
82
104
  render();
105
+ return () => {
106
+ alive = false;
107
+ if (shakeTimer) clearTimeout(shakeTimer);
108
+ field.removeEventListener("compositionstart", onCompositionStart);
109
+ field.removeEventListener("compositionend", onCompositionEnd);
110
+ field.removeEventListener("keydown", onKeyDown);
111
+ field.removeEventListener("blur", commit);
112
+ el.__icenTagInit = false;
113
+ };
83
114
  }
84
115
  function initTagInput(root) {
85
- if (typeof document === "undefined") return;
86
- const scope = root ?? document;
87
- const containers = [];
88
- if (scope instanceof HTMLElement && scope.matches(".tag-input")) containers.push(scope);
89
- containers.push(...Array.from(scope.querySelectorAll(".tag-input")));
90
- for (const c of containers) setup(c);
116
+ const cleanups = [];
117
+ if (typeof document !== "undefined") {
118
+ const scope = root ?? document;
119
+ const containers = [];
120
+ if (scope instanceof HTMLElement && scope.matches(".tag-input")) containers.push(scope);
121
+ containers.push(...Array.from(scope.querySelectorAll(".tag-input")));
122
+ for (const c of containers) {
123
+ const cleanup = setup(c);
124
+ if (cleanup) cleanups.push(cleanup);
125
+ }
126
+ }
127
+ return () => {
128
+ for (const fn of cleanups) fn();
129
+ cleanups.length = 0;
130
+ };
91
131
  }
92
132
  export {
93
133
  initTagInput
@@ -6,20 +6,31 @@ interface ThemeState {
6
6
  declare const PRESETS: readonly ["clay", "piano", "art", "vangogh", "ink", "retro"];
7
7
  declare const STYLES: readonly ["modern", "retro", "terminal"];
8
8
  declare const STORAGE_KEY = "icen.ui.theme";
9
+ /**
10
+ * 注册宿主自建预设(宿主先在自家 CSS 写好 `.<name>` 与 `.<name>.dark` 完整 token 面,
11
+ * 仿照 colors.css 现有 6 套)。名字须命中 /^[a-z][a-z0-9-]{1,20}$/ 且不与
12
+ * dark / style-* 保留字冲突;注册只影响 listThemePresets() 枚举(UI 切换器用),
13
+ * 不注册的自建预设同样可以 setTheme(校验同样放行)。返回是否接受注册。
14
+ */
15
+ declare function registerThemePreset(name: string): boolean;
16
+ /** 全部可用预设名:内置 6 套 + 已注册的自建预设(UI 切换器枚举用,顺序稳定)。 */
17
+ declare function listThemePresets(): string[];
9
18
  /** 默认主题:clay + 跟随系统明暗 + modern。SSR 下返回离线兜底值。 */
10
19
  declare function getDefaultTheme(): ThemeState;
11
20
  /** 读 localStorage 中的持久化主题;缺失或坏数据回退默认。 */
12
21
  declare function getTheme(): ThemeState;
13
- /** 把主题应用到 <html>:先清掉所有 preset/style/dark 相关类,再按 t 挂回。 */
22
+ /** 把主题应用到 <html> 并在状态实际变化时派发 icen:theme-change(见私有 apply)。 */
14
23
  declare function applyTheme(t: ThemeState): void;
15
24
  /** 合并当前主题与 patch,应用并持久化,返回新态。patch 中的非法枚举值回退默认。 */
16
25
  declare function setTheme(patch: Partial<ThemeState>): ThemeState;
17
26
  /** 切换明暗,返回新态。 */
18
27
  declare function toggleDark(): ThemeState;
28
+ /** 只切风格 profile(modern / retro / terminal;非法值回退 modern),返回新态。 */
29
+ declare function setStyle(style: string): ThemeState;
19
30
  /** 页面加载早期调用:读持久化主题并应用,防闪烁。
20
31
  * 无持久化偏好时,自动跟随系统的 prefers-color-scheme 变化(实时);
21
32
  * 一旦用户显式 setTheme / toggleDark,跟随停止(持久化优先)。
22
33
  * 重复调用幂等(系统监听不会重复挂)。 */
23
34
  declare function initTheme(): ThemeState;
24
35
 
25
- export { PRESETS, STORAGE_KEY, STYLES, type ThemeState, applyTheme, getDefaultTheme, getTheme, initTheme, setTheme, toggleDark };
36
+ export { PRESETS, STORAGE_KEY, STYLES, type ThemeState, applyTheme, getDefaultTheme, getTheme, initTheme, listThemePresets, registerThemePreset, setStyle, setTheme, toggleDark };
@@ -1,11 +1,32 @@
1
+ // src/behaviors/events.ts
2
+ function emitIcen(source, type, detail) {
3
+ return source.dispatchEvent(
4
+ new CustomEvent(type, { detail, bubbles: true, composed: true, cancelable: true })
5
+ );
6
+ }
7
+
1
8
  // src/behaviors/theme.ts
2
9
  var PRESETS = ["clay", "piano", "art", "vangogh", "ink", "retro"];
3
10
  var STYLES = ["modern", "retro", "terminal"];
4
11
  var STORAGE_KEY = "icen.ui.theme";
12
+ var PRESET_NAME_RE = /^[a-z][a-z0-9-]{1,20}$/;
13
+ var RESERVED_PRESET_NAMES = /* @__PURE__ */ new Set(["dark", ...STYLES.map((s) => `style-${s}`)]);
14
+ var customPresets = /* @__PURE__ */ new Set();
5
15
  var FALLBACK = { preset: "clay", dark: false, style: "modern" };
6
16
  function isBrowser() {
7
17
  return typeof document !== "undefined";
8
18
  }
19
+ function isValidPreset(name) {
20
+ return PRESETS.includes(name) || PRESET_NAME_RE.test(name) && !RESERVED_PRESET_NAMES.has(name);
21
+ }
22
+ function registerThemePreset(name) {
23
+ if (!PRESET_NAME_RE.test(name) || RESERVED_PRESET_NAMES.has(name)) return false;
24
+ customPresets.add(name);
25
+ return true;
26
+ }
27
+ function listThemePresets() {
28
+ return [...PRESETS, ...customPresets];
29
+ }
9
30
  function getDefaultTheme() {
10
31
  if (!isBrowser() || typeof window.matchMedia !== "function") {
11
32
  return { ...FALLBACK };
@@ -19,7 +40,8 @@ function getDefaultTheme() {
19
40
  function isThemeState(v) {
20
41
  if (typeof v !== "object" || v === null) return false;
21
42
  const t = v;
22
- return typeof t.preset === "string" && PRESETS.includes(t.preset) && typeof t.dark === "boolean" && typeof t.style === "string" && STYLES.includes(t.style);
43
+ return typeof t.preset === "string" && /* 持久化路径与 setTheme 同一开放白名单:自建预设跨刷新存活 */
44
+ isValidPreset(t.preset) && typeof t.dark === "boolean" && typeof t.style === "string" && STYLES.includes(t.style);
23
45
  }
24
46
  function getTheme() {
25
47
  if (!isBrowser()) return getDefaultTheme();
@@ -33,24 +55,40 @@ function getTheme() {
33
55
  return getDefaultTheme();
34
56
  }
35
57
  }
36
- function applyTheme(t) {
58
+ var lastApplied = null;
59
+ function apply(t) {
37
60
  if (!isBrowser()) return;
38
61
  const cls = document.documentElement.classList;
62
+ if (lastApplied && !PRESETS.includes(lastApplied.preset)) {
63
+ cls.remove(lastApplied.preset);
64
+ }
39
65
  for (const p of PRESETS) cls.remove(p);
40
66
  for (const s of STYLES) cls.remove(`style-${s}`);
41
67
  cls.remove("dark");
42
68
  cls.add(t.preset);
43
69
  if (t.dark) cls.add("dark");
44
70
  cls.add(`style-${t.style}`);
71
+ const changed = lastApplied !== null && (lastApplied.preset !== t.preset || lastApplied.dark !== t.dark || lastApplied.style !== t.style);
72
+ lastApplied = { ...t };
73
+ if (changed) {
74
+ emitIcen(document.documentElement, "icen:theme-change", {
75
+ preset: t.preset,
76
+ dark: t.dark,
77
+ style: t.style
78
+ });
79
+ }
80
+ }
81
+ function applyTheme(t) {
82
+ apply(t);
45
83
  }
46
84
  function setTheme(patch) {
47
85
  const merged = { ...getTheme(), ...patch };
48
86
  const next = {
49
- preset: PRESETS.includes(merged.preset) ? merged.preset : FALLBACK.preset,
87
+ preset: isValidPreset(merged.preset) ? merged.preset : FALLBACK.preset,
50
88
  dark: merged.dark === true,
51
89
  style: STYLES.includes(merged.style) ? merged.style : FALLBACK.style
52
90
  };
53
- applyTheme(next);
91
+ apply(next);
54
92
  if (isBrowser()) {
55
93
  try {
56
94
  window.localStorage.setItem(STORAGE_KEY, JSON.stringify(next));
@@ -62,6 +100,9 @@ function setTheme(patch) {
62
100
  function toggleDark() {
63
101
  return setTheme({ dark: !getTheme().dark });
64
102
  }
103
+ function setStyle(style) {
104
+ return setTheme({ style });
105
+ }
65
106
  var systemListenerAttached = false;
66
107
  function initTheme() {
67
108
  const t = getTheme();
@@ -103,6 +144,9 @@ export {
103
144
  getDefaultTheme,
104
145
  getTheme,
105
146
  initTheme,
147
+ listThemePresets,
148
+ registerThemePreset,
149
+ setStyle,
106
150
  setTheme,
107
151
  toggleDark
108
152
  };
@@ -1,4 +1,4 @@
1
- type ToastKind = 'ok' | 'err' | 'warn' | 'info';
1
+ type ToastKind = 'ok' | 'err' | 'warn' | 'info' | 'success' | 'error';
2
2
  interface ToastOptions {
3
3
  /** 保留时长(毫秒),默认 2600;带 action 时默认 6000 */
4
4
  ms?: number;
@@ -6,21 +6,43 @@ interface ToastOptions {
6
6
  actionLabel?: string;
7
7
  /** action 触发回调;返回 false 阻止 toast 关闭 */
8
8
  onAction?: () => void | Promise<void> | boolean;
9
+ /** 内联 SVG 字符串图标(消毒后渲染为 .toast-icon);非 <svg> 开头或解析失败静默忽略 */
10
+ icon?: string;
11
+ /** 显示右上角关闭按钮(.toast-close,点击即关闭本条),默认 false */
12
+ closable?: boolean;
13
+ /** 显示底部超时进度条(.toast-progress,动画时长 = 本条保留时长),默认 false */
14
+ progress?: boolean;
15
+ }
16
+ /** 单条 toast 句柄:dismiss() 立即关闭本条(幂等,可重复调用)。 */
17
+ interface ToastHandle {
18
+ dismiss(): void;
9
19
  }
10
20
  interface ToastConfig {
11
21
  /** 同屏最大堆叠,默认 5;超出从最早的开始移除 */
12
22
  maxStack?: number;
13
23
  }
14
24
  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;
25
+ (message: string, kind?: ToastKind, ms?: number): ToastHandle;
26
+ /** success 语义(.toast--success),规范词表入口 */
27
+ success(message: string, ms?: number): ToastHandle;
28
+ /**
29
+ * success 语义(.toast--success)。
30
+ * @deprecated 词表统一:改用 toast.success(),本别名下个大版本删除。
31
+ */
32
+ ok(message: string, ms?: number): ToastHandle;
33
+ /** error 语义(.toast--error),规范词表入口 */
34
+ error(message: string, ms?: number): ToastHandle;
35
+ /**
36
+ * error 语义(.toast--error)。
37
+ * @deprecated 词表统一:改用 toast.error(),本别名下个大版本删除。
38
+ */
39
+ err(message: string, ms?: number): ToastHandle;
40
+ warn(message: string, ms?: number): ToastHandle;
41
+ info(message: string, ms?: number): ToastHandle;
42
+ action(message: string, actionLabel: string, onAction: () => void, ms?: number): ToastHandle;
21
43
  dismiss(): void;
22
44
  config(opts: ToastConfig): void;
23
45
  }
24
46
  declare const toast: ToastFn;
25
47
 
26
- export { type ToastConfig, type ToastKind, type ToastOptions, toast };
48
+ export { type ToastConfig, type ToastHandle, type ToastKind, type ToastOptions, toast };