@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
@@ -18,10 +18,40 @@ function ensureContainer() {
18
18
  }
19
19
  var KIND_CLASS = {
20
20
  ok: "toast--success",
21
+ success: "toast--success",
21
22
  err: "toast--error",
23
+ error: "toast--error",
22
24
  warn: "toast--warning",
23
25
  info: "toast--info"
24
26
  };
27
+ var CLOSE_SVG = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>';
28
+ function stripUnsafeSvg(el) {
29
+ for (const attr of Array.from(el.attributes)) {
30
+ if (attr.name.toLowerCase().startsWith("on")) el.removeAttribute(attr.name);
31
+ }
32
+ for (const child of Array.from(el.children)) {
33
+ const tag = child.tagName.toLowerCase();
34
+ if (tag === "script" || tag === "foreignobject") child.remove();
35
+ else stripUnsafeSvg(child);
36
+ }
37
+ }
38
+ function sanitizeSvgIcon(source) {
39
+ if (!source.trimStart().startsWith("<svg")) return null;
40
+ try {
41
+ const doc = new DOMParser().parseFromString(source, "image/svg+xml");
42
+ const svg = doc.documentElement;
43
+ if (!svg || svg.tagName.toLowerCase() !== "svg") return null;
44
+ stripUnsafeSvg(svg);
45
+ return document.importNode(svg, true);
46
+ } catch {
47
+ return null;
48
+ }
49
+ }
50
+ var closeSvgCache = null;
51
+ function closeSvg() {
52
+ closeSvgCache ??= sanitizeSvgIcon(CLOSE_SVG);
53
+ return closeSvgCache ? closeSvgCache.cloneNode(true) : null;
54
+ }
25
55
  function scheduleRemove(el) {
26
56
  el.classList.add("is-leaving");
27
57
  let done = false;
@@ -48,12 +78,23 @@ function trimStack(container) {
48
78
  }
49
79
  }
50
80
  function show(message, kind = "info", opts = {}) {
51
- if (!isBrowser()) return;
52
- const { ms, actionLabel, onAction } = opts;
81
+ if (!isBrowser()) return { dismiss() {
82
+ } };
83
+ const { ms, actionLabel, onAction, icon, closable, progress } = opts;
53
84
  const container = ensureContainer();
54
85
  const el = document.createElement("div");
55
86
  el.className = `toast ${KIND_CLASS[kind]}`;
56
- el.setAttribute("role", kind === "err" ? "alert" : "status");
87
+ el.setAttribute("role", kind === "err" || kind === "error" ? "alert" : "status");
88
+ if (icon) {
89
+ const svg = sanitizeSvgIcon(icon);
90
+ if (svg) {
91
+ const iconWrap = document.createElement("span");
92
+ iconWrap.className = "toast-icon";
93
+ iconWrap.setAttribute("aria-hidden", "true");
94
+ iconWrap.appendChild(svg);
95
+ el.appendChild(iconWrap);
96
+ }
97
+ }
57
98
  const msgNode = document.createElement("span");
58
99
  msgNode.className = "toast-msg";
59
100
  msgNode.textContent = message;
@@ -66,16 +107,39 @@ function show(message, kind = "info", opts = {}) {
66
107
  actionBtn.textContent = actionLabel;
67
108
  el.appendChild(actionBtn);
68
109
  }
69
- container.appendChild(el);
70
- trimStack(container);
110
+ const duration = ms ?? (actionLabel ? ACTION_MS : DEFAULT_MS);
111
+ let timer = 0;
71
112
  let removed = false;
72
113
  const remove = () => {
73
114
  if (removed) return;
74
115
  removed = true;
116
+ if (timer) window.clearTimeout(timer);
75
117
  scheduleRemove(el);
76
118
  };
77
- const duration = ms ?? (actionLabel ? ACTION_MS : DEFAULT_MS);
78
- const timer = window.setTimeout(remove, duration);
119
+ if (closable) {
120
+ const closeBtn = document.createElement("button");
121
+ closeBtn.type = "button";
122
+ closeBtn.className = "toast-close";
123
+ closeBtn.setAttribute("aria-label", "\u5173\u95ED");
124
+ const svg = closeSvg();
125
+ if (svg) closeBtn.appendChild(svg);
126
+ else closeBtn.textContent = "\xD7";
127
+ closeBtn.addEventListener("click", remove);
128
+ el.appendChild(closeBtn);
129
+ }
130
+ if (progress) {
131
+ const bar = document.createElement("div");
132
+ bar.className = "toast-progress";
133
+ bar.setAttribute("aria-hidden", "true");
134
+ const fill = document.createElement("div");
135
+ fill.className = "toast-progress-fill";
136
+ fill.style.animationDuration = `${duration}ms`;
137
+ bar.appendChild(fill);
138
+ el.appendChild(bar);
139
+ }
140
+ container.appendChild(el);
141
+ trimStack(container);
142
+ timer = window.setTimeout(remove, duration);
79
143
  if (actionBtn && onAction) {
80
144
  actionBtn.addEventListener("click", () => {
81
145
  window.clearTimeout(timer);
@@ -84,11 +148,14 @@ function show(message, kind = "info", opts = {}) {
84
148
  remove();
85
149
  });
86
150
  }
151
+ return { dismiss: remove };
87
152
  }
88
153
  var toast = Object.assign(
89
154
  (message, kind = "info", ms) => show(message, kind, { ms }),
90
155
  {
156
+ success: (message, ms) => show(message, "success", { ms }),
91
157
  ok: (message, ms) => show(message, "ok", { ms }),
158
+ error: (message, ms) => show(message, "error", { ms }),
92
159
  err: (message, ms) => show(message, "err", { ms }),
93
160
  warn: (message, ms) => show(message, "warn", { ms }),
94
161
  info: (message, ms) => show(message, "info", { ms }),
@@ -1,4 +1,7 @@
1
- /** 为 root 下每棵 .tree 初始化(root 自身是 .tree 也算)。 */
2
- declare function initTree(root?: ParentNode): void;
1
+ /**
2
+ * 为 root 下每棵 .tree 初始化(root 自身是 .tree 也算)。
3
+ * 返回销毁函数:移除本次挂上的容器监听并复位幂等标记(销毁后可重新 init)。SSR 下返回 no-op。
4
+ */
5
+ declare function initTree(root?: ParentNode): () => void;
3
6
 
4
7
  export { initTree };
@@ -1,12 +1,19 @@
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/tree.ts
2
9
  function setup(tree) {
3
10
  const el = tree;
4
- if (el.__icenTreeInit) return;
11
+ if (el.__icenTreeInit) return void 0;
5
12
  el.__icenTreeInit = true;
6
13
  tree.querySelectorAll(".tree-node").forEach((node) => {
7
14
  if (!node.hasAttribute("tabindex")) node.tabIndex = 0;
8
15
  });
9
- tree.addEventListener("click", (ev) => {
16
+ const onClick = (ev) => {
10
17
  const target = ev.target;
11
18
  if (!(target instanceof Element)) return;
12
19
  const node = target.closest(".tree-node");
@@ -28,6 +35,7 @@ function setup(tree) {
28
35
  toggle?.classList.add("is-collapsed");
29
36
  toggle?.setAttribute("aria-expanded", "false");
30
37
  }
38
+ emitIcen(tree, "icen:tree-toggle", { node: li, open: !collapsed });
31
39
  return;
32
40
  }
33
41
  if (childUl) return;
@@ -37,12 +45,9 @@ function setup(tree) {
37
45
  });
38
46
  node.classList.add("is-selected");
39
47
  node.setAttribute("aria-selected", "true");
40
- tree.dispatchEvent(new CustomEvent("icen:tree-select", {
41
- bubbles: true,
42
- detail: { node }
43
- }));
44
- });
45
- tree.addEventListener("keydown", (ev) => {
48
+ emitIcen(tree, "icen:tree-select", { node });
49
+ };
50
+ const onKeyDown = (ev) => {
46
51
  const kev = ev;
47
52
  const target = kev.target;
48
53
  if (!(target instanceof Element)) return;
@@ -71,6 +76,7 @@ function setup(tree) {
71
76
  childUl.removeAttribute("hidden");
72
77
  node.querySelector(".tree-toggle")?.setAttribute("aria-expanded", "true");
73
78
  node.querySelector(".tree-toggle")?.classList.remove("is-collapsed");
79
+ if (li) emitIcen(tree, "icen:tree-toggle", { node: li, open: true });
74
80
  }
75
81
  break;
76
82
  }
@@ -82,19 +88,36 @@ function setup(tree) {
82
88
  childUl.setAttribute("hidden", "");
83
89
  node.querySelector(".tree-toggle")?.setAttribute("aria-expanded", "false");
84
90
  node.querySelector(".tree-toggle")?.classList.add("is-collapsed");
91
+ if (li) emitIcen(tree, "icen:tree-toggle", { node: li, open: false });
85
92
  }
86
93
  break;
87
94
  }
88
95
  }
89
- });
96
+ };
97
+ tree.addEventListener("click", onClick);
98
+ tree.addEventListener("keydown", onKeyDown);
99
+ return () => {
100
+ tree.removeEventListener("click", onClick);
101
+ tree.removeEventListener("keydown", onKeyDown);
102
+ el.__icenTreeInit = false;
103
+ };
90
104
  }
91
105
  function initTree(root) {
92
- if (typeof document === "undefined") return;
93
- const scope = root ?? document;
94
- const trees = [];
95
- if (scope instanceof Element && scope.matches(".tree")) trees.push(scope);
96
- trees.push(...Array.from(scope.querySelectorAll(".tree")));
97
- for (const t of trees) setup(t);
106
+ const cleanups = [];
107
+ if (typeof document !== "undefined") {
108
+ const scope = root ?? document;
109
+ const trees = [];
110
+ if (scope instanceof Element && scope.matches(".tree")) trees.push(scope);
111
+ trees.push(...Array.from(scope.querySelectorAll(".tree")));
112
+ for (const t of trees) {
113
+ const cleanup = setup(t);
114
+ if (cleanup) cleanups.push(cleanup);
115
+ }
116
+ }
117
+ return () => {
118
+ for (const fn of cleanups) fn();
119
+ cleanups.length = 0;
120
+ };
98
121
  }
99
122
  export {
100
123
  initTree
@@ -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/upload.ts
2
9
  function formatSize(bytes) {
3
10
  if (bytes < 1024) return `${bytes} B`;
@@ -34,9 +41,9 @@ function sameFile(a, b) {
34
41
  function setup(zone) {
35
42
  const el = zone;
36
43
  if (el.__icenUploadInit) return;
37
- el.__icenUploadInit = true;
38
44
  const input = zone.querySelector('input[type="file"]');
39
45
  if (!input) return;
46
+ el.__icenUploadInit = true;
40
47
  const hasList = zone.hasAttribute("data-upload-list");
41
48
  const maxSize = Number(zone.getAttribute("data-max-size")) || 0;
42
49
  const maxFiles = Number(zone.getAttribute("data-max-files")) || 0;
@@ -118,7 +125,7 @@ function setup(zone) {
118
125
  removeBtn.addEventListener("click", (ev) => {
119
126
  ev.stopPropagation();
120
127
  currentFiles = currentFiles.filter((f) => f !== file);
121
- zone.dispatchEvent(new CustomEvent("icen:upload-remove", { bubbles: true, detail: { file } }));
128
+ emitIcen(zone, "icen:upload-remove", { file });
122
129
  renderList();
123
130
  syncInputFiles();
124
131
  });
@@ -126,10 +133,10 @@ function setup(zone) {
126
133
  list.appendChild(li);
127
134
  }
128
135
  }
129
- function syncInputFiles() {
136
+ function setInputFiles(files) {
130
137
  try {
131
138
  const dt = new DataTransfer();
132
- for (const f of currentFiles) dt.items.add(f);
139
+ for (const f of files) dt.items.add(f);
133
140
  syncing = true;
134
141
  input.files = dt.files;
135
142
  input.dispatchEvent(new Event("change", { bubbles: true }));
@@ -138,6 +145,9 @@ function setup(zone) {
138
145
  syncing = false;
139
146
  }
140
147
  }
148
+ function syncInputFiles() {
149
+ setInputFiles(currentFiles);
150
+ }
141
151
  function validate(file) {
142
152
  if (accept && !isAccepted(file, accept)) return "\u6587\u4EF6\u7C7B\u578B\u4E0D\u652F\u6301";
143
153
  if (maxSize > 0 && file.size > maxSize) return `\u6587\u4EF6\u8D85\u8FC7 ${formatSize(maxSize)}`;
@@ -148,7 +158,7 @@ function setup(zone) {
148
158
  for (const file of files) {
149
159
  const reason = validate(file);
150
160
  if (reason) {
151
- zone.dispatchEvent(new CustomEvent("icen:upload-error", { bubbles: true, detail: { file, reason } }));
161
+ emitIcen(zone, "icen:upload-error", { file, reason });
152
162
  continue;
153
163
  }
154
164
  valid.push(file);
@@ -164,10 +174,7 @@ function setup(zone) {
164
174
  if (maxFiles > 0) {
165
175
  const remaining = maxFiles - currentFiles.length;
166
176
  if (remaining <= 0) {
167
- zone.dispatchEvent(new CustomEvent("icen:upload-error", {
168
- bubbles: true,
169
- detail: { file: null, reason: `\u6700\u591A ${maxFiles} \u4E2A\u6587\u4EF6` }
170
- }));
177
+ emitIcen(zone, "icen:upload-error", { file: null, reason: `\u6700\u591A ${maxFiles} \u4E2A\u6587\u4EF6` });
171
178
  return;
172
179
  }
173
180
  if (fresh.length > remaining) fresh.length = remaining;
@@ -175,10 +182,10 @@ function setup(zone) {
175
182
  currentFiles.push(...fresh);
176
183
  renderList();
177
184
  syncInputFiles();
178
- zone.dispatchEvent(new CustomEvent("icen:upload", { bubbles: true, detail: { files: fresh } }));
185
+ emitIcen(zone, "icen:upload", { files: fresh });
179
186
  return;
180
187
  }
181
- zone.dispatchEvent(new CustomEvent("icen:upload", { bubbles: true, detail: { files: valid } }));
188
+ emitIcen(zone, "icen:upload", { files: valid });
182
189
  }
183
190
  const openPicker = () => input.click();
184
191
  zone.addEventListener("click", (ev) => {
@@ -189,6 +196,8 @@ function setup(zone) {
189
196
  });
190
197
  zone.addEventListener("keydown", (ev) => {
191
198
  if (ev.key === "Enter" || ev.key === " ") {
199
+ const tgt = ev.target;
200
+ if (tgt instanceof Element && tgt.closest(".upload-list")) return;
192
201
  ev.preventDefault();
193
202
  openPicker();
194
203
  }
@@ -215,18 +224,7 @@ function setup(zone) {
215
224
  const files = ev.dataTransfer?.files;
216
225
  if (!files || files.length === 0) return;
217
226
  handleFiles(Array.from(files));
218
- if (!hasList) {
219
- try {
220
- const dt = new DataTransfer();
221
- for (const f of Array.from(files)) dt.items.add(f);
222
- syncing = true;
223
- input.files = dt.files;
224
- input.dispatchEvent(new Event("change", { bubbles: true }));
225
- } catch {
226
- } finally {
227
- syncing = false;
228
- }
229
- }
227
+ if (!hasList) setInputFiles(Array.from(files));
230
228
  });
231
229
  }
232
230
  function initUpload(root) {
package/dist/cli.mjs CHANGED
@@ -7,7 +7,7 @@
7
7
  * 用法:
8
8
  * bunx --bun @icen.ai/ui add <slug...> 打印引入行(多个 slug 空格分隔)
9
9
  * bunx --bun @icen.ai/ui list 列出全部组件 slug
10
- * 数据源:同目录 registry.json(构建期由 scripts/slugs.mjs 生成)。
10
+ * 数据源:同目录 registry.json(构建期由 scripts/build-css.ts 写入,映射数据源为 scripts/slugs.mjs)。
11
11
  */
12
12
  import { readFileSync } from 'node:fs';
13
13
 
@@ -0,0 +1,3 @@
1
+ // @icen.ai/ui — kit: ai-branch(CSS + behavior 一行入口;构建产物,勿手改)
2
+ import './ai-chat.css';
3
+ export * from '../behaviors/ai-branch.mjs';
@@ -1035,3 +1035,298 @@
1035
1035
  .ai-composer-todo.is-error svg { color: var(--token-warning); }
1036
1036
  .ai-composer-todo-pop { min-width: 240px; max-width: 320px; }
1037
1037
  .ai-composer-todo-pop .ai-todo { padding: 0; }
1038
+
1039
+ /* ══════════════ ai-threads · 会话列表侧栏(§5.27,behaviors/ai-threads.ts) ══════════════ */
1040
+
1041
+ /*
1042
+ * <div class="ai-threads"> ← 240px 侧栏形态(--ai-threads-width 可调)
1043
+ * <div class="ai-threads-group" data-group="今天" [.is-collapsed]>
1044
+ * <button class="ai-threads-group-head" aria-expanded="true">今天</button>
1045
+ * <div class="ai-threads-group-list">
1046
+ * <div class="ai-thread [.is-active|.is-archived]" role="button" tabindex="0" data-key="…">
1047
+ * <span class="ai-thread-icon">…svg…</span>
1048
+ * <span class="ai-thread-label">…</span>
1049
+ * <button class="ai-thread-action">···</button> ← 动作菜单挂载点(宿主实现)
1050
+ * </div>…
1051
+ * </div>
1052
+ * </div>… ← 「已归档」组由 behavior 沉底
1053
+ * </div>
1054
+ * .is-active:左 2px accent;.is-archived:降透明;组头 11px faint。
1055
+ */
1056
+
1057
+ .ai-threads {
1058
+ display: flex;
1059
+ flex-direction: column;
1060
+ gap: var(--density-space-xs);
1061
+ width: var(--ai-threads-width, 240px);
1062
+ min-width: 0;
1063
+ align-self: stretch; /* 宽度自稳定:宿主 flex/grid 居中时不 shrink-wrap */
1064
+ font-size: var(--density-font-size-sm);
1065
+ }
1066
+
1067
+ .ai-threads-group {
1068
+ display: flex;
1069
+ flex-direction: column;
1070
+ gap: 2px;
1071
+ min-width: 0;
1072
+ }
1073
+
1074
+ /* 组头:11px faint 小标,可折叠(aria-expanded 驱动 CSS 三角,与 .ai-reasoning-head 同款) */
1075
+ .ai-threads-group-head {
1076
+ display: flex;
1077
+ align-items: center;
1078
+ gap: 4px;
1079
+ padding: var(--density-space-xs) 2px;
1080
+ border: 0;
1081
+ background: none;
1082
+ font-size: 11px;
1083
+ font-weight: 500;
1084
+ color: var(--token-text-faint);
1085
+ letter-spacing: 0.02em;
1086
+ text-align: left;
1087
+ cursor: pointer;
1088
+ transition: color var(--icen-tx-fast);
1089
+ }
1090
+ .ai-threads-group-head:hover {
1091
+ color: var(--token-text-muted);
1092
+ }
1093
+ .ai-threads-group-head:focus-visible {
1094
+ outline: none;
1095
+ box-shadow: var(--icen-ring);
1096
+ border-radius: var(--radius-xs);
1097
+ }
1098
+ .ai-threads-group-head::before {
1099
+ content: '';
1100
+ flex-shrink: 0;
1101
+ width: 6px;
1102
+ height: 6px;
1103
+ border-right: 1.5px solid currentColor;
1104
+ border-bottom: 1.5px solid currentColor;
1105
+ transform: rotate(-45deg);
1106
+ transition: transform var(--icen-tx-fast);
1107
+ }
1108
+ .ai-threads-group-head[aria-expanded='true']::before {
1109
+ transform: rotate(45deg);
1110
+ }
1111
+ .ai-threads-group.is-collapsed .ai-threads-group-list {
1112
+ display: none;
1113
+ }
1114
+
1115
+ .ai-threads-group-list {
1116
+ display: flex;
1117
+ flex-direction: column;
1118
+ gap: 1px;
1119
+ min-width: 0;
1120
+ }
1121
+
1122
+ /* 会话行:激活左 2px accent 线;归档降透明 */
1123
+ .ai-thread {
1124
+ position: relative;
1125
+ display: flex;
1126
+ align-items: center;
1127
+ gap: var(--density-space-xs);
1128
+ min-width: 0;
1129
+ padding: var(--density-space-xs) var(--density-space-sm);
1130
+ border-left: 2px solid transparent;
1131
+ border-radius: var(--radius-xs);
1132
+ background: transparent;
1133
+ color: var(--token-text-muted);
1134
+ font-size: var(--density-font-size-sm);
1135
+ text-align: left;
1136
+ cursor: pointer;
1137
+ transition:
1138
+ background-color var(--icen-tx-fast),
1139
+ color var(--icen-tx-fast),
1140
+ border-color var(--icen-tx-fast),
1141
+ opacity var(--icen-tx-fast);
1142
+ }
1143
+ .ai-thread:hover {
1144
+ background: var(--token-fill-1);
1145
+ color: var(--token-text);
1146
+ }
1147
+ .ai-thread:focus-visible {
1148
+ outline: none;
1149
+ box-shadow: var(--icen-ring);
1150
+ }
1151
+ .ai-thread.is-active {
1152
+ border-left-color: var(--token-accent);
1153
+ background: var(--token-fill-1);
1154
+ color: var(--token-text);
1155
+ }
1156
+ .ai-thread.is-archived {
1157
+ opacity: 0.55;
1158
+ }
1159
+ .ai-thread-icon {
1160
+ display: inline-flex;
1161
+ align-items: center;
1162
+ justify-content: center;
1163
+ flex-shrink: 0;
1164
+ color: var(--token-text-faint);
1165
+ }
1166
+ .ai-thread-icon svg {
1167
+ width: 14px;
1168
+ height: 14px;
1169
+ }
1170
+ .ai-thread.is-active .ai-thread-icon {
1171
+ color: var(--token-accent);
1172
+ }
1173
+ .ai-thread-label {
1174
+ flex: 1;
1175
+ min-width: 0;
1176
+ overflow: hidden;
1177
+ text-overflow: ellipsis;
1178
+ white-space: nowrap;
1179
+ }
1180
+
1181
+ /* 动作钮「···」:hover/聚焦/激活才出现(菜单本体由宿主经 icen:ai-thread-action 实现) */
1182
+ .ai-thread-action {
1183
+ display: none;
1184
+ flex-shrink: 0;
1185
+ align-items: center;
1186
+ justify-content: center;
1187
+ width: 20px;
1188
+ height: 20px;
1189
+ padding: 0;
1190
+ border: 0;
1191
+ border-radius: var(--radius-xs);
1192
+ background: transparent;
1193
+ color: var(--token-text-faint);
1194
+ font-size: 13px;
1195
+ font-weight: 700;
1196
+ letter-spacing: 0.04em;
1197
+ line-height: 1;
1198
+ cursor: pointer;
1199
+ transition:
1200
+ color var(--icen-tx-fast),
1201
+ background-color var(--icen-tx-fast);
1202
+ }
1203
+ .ai-thread:hover .ai-thread-action,
1204
+ .ai-thread:focus-within .ai-thread-action,
1205
+ .ai-thread.is-active .ai-thread-action {
1206
+ display: inline-flex;
1207
+ }
1208
+ .ai-thread-action:hover {
1209
+ color: var(--token-accent);
1210
+ background: var(--token-fill-2);
1211
+ }
1212
+ .ai-thread-action:focus-visible {
1213
+ outline: none;
1214
+ box-shadow: var(--icen-ring);
1215
+ }
1216
+
1217
+ /* ══════════════ ai-branch · 分支版本切换器(§5.27,behaviors/ai-branch.ts) ══════════════ */
1218
+
1219
+ /*
1220
+ * <div class="ai-branch" data-index data-count role="group">
1221
+ * <button class="ai-branch-prev" [disabled]>‹</button>
1222
+ * <span class="ai-branch-page">2/3</span> ← mono 数字(当前从 1 计)
1223
+ * <button class="ai-branch-next" [disabled]>›</button>
1224
+ * </div>
1225
+ * count ≤ 1 时挂载容器由 behavior 置空 + hidden/aria-hidden(此处无额外规则)。
1226
+ */
1227
+
1228
+ .ai-branch {
1229
+ display: inline-flex;
1230
+ align-items: center;
1231
+ gap: 3px;
1232
+ color: var(--token-text-muted);
1233
+ }
1234
+ .ai-branch-prev,
1235
+ .ai-branch-next {
1236
+ display: inline-flex;
1237
+ align-items: center;
1238
+ justify-content: center;
1239
+ width: 22px;
1240
+ height: 22px;
1241
+ padding: 0;
1242
+ border: 0;
1243
+ border-radius: var(--radius-xs);
1244
+ background: transparent;
1245
+ color: var(--token-text-muted);
1246
+ font-family: var(--font-mono);
1247
+ font-size: 13px;
1248
+ line-height: 1;
1249
+ cursor: pointer;
1250
+ transition:
1251
+ color var(--icen-tx-fast),
1252
+ background-color var(--icen-tx-fast);
1253
+ }
1254
+ .ai-branch-prev:hover,
1255
+ .ai-branch-next:hover {
1256
+ color: var(--token-text);
1257
+ background: var(--token-fill-1);
1258
+ }
1259
+ .ai-branch-prev:focus-visible,
1260
+ .ai-branch-next:focus-visible {
1261
+ outline: none;
1262
+ box-shadow: var(--icen-ring);
1263
+ }
1264
+ .ai-branch-prev:disabled,
1265
+ .ai-branch-next:disabled {
1266
+ opacity: 0.4;
1267
+ cursor: default;
1268
+ background: transparent;
1269
+ }
1270
+ .ai-branch-page {
1271
+ min-width: 2.5em;
1272
+ text-align: center;
1273
+ font-family: var(--font-mono);
1274
+ font-size: var(--density-font-size-xs);
1275
+ color: var(--token-text-faint);
1276
+ font-variant-numeric: tabular-nums;
1277
+ user-select: none;
1278
+ }
1279
+
1280
+ /* ══════════════ ai-msg-continue · aborted「继续生成」槽(§5.27,createAiStream fail/cancel) ══════════════ */
1281
+
1282
+ /* ghost 小钮右浮:挂在 .ai-msg-body 尾部(消息尾),点按 → icen:ai-retry {el} */
1283
+ .ai-msg-continue {
1284
+ display: inline-flex;
1285
+ align-items: center;
1286
+ float: right;
1287
+ margin: var(--density-space-2xs, 2px) 0 0 var(--density-space-xs);
1288
+ padding: 2px 10px;
1289
+ border: 1px solid var(--token-line);
1290
+ border-radius: var(--radius-full);
1291
+ background: transparent;
1292
+ color: var(--token-text-muted);
1293
+ font-size: var(--density-font-size-xs);
1294
+ white-space: nowrap;
1295
+ cursor: pointer;
1296
+ transition:
1297
+ color var(--icen-tx-fast),
1298
+ border-color var(--icen-tx-fast),
1299
+ background-color var(--icen-tx-fast);
1300
+ }
1301
+ .ai-msg-continue:hover {
1302
+ color: var(--token-accent);
1303
+ border-color: var(--icen-hover-border);
1304
+ background: var(--token-fill-1);
1305
+ }
1306
+ .ai-msg-continue:focus-visible {
1307
+ outline: none;
1308
+ box-shadow: var(--icen-ring);
1309
+ }
1310
+
1311
+ /* §5.27 reasoning 自动收起无新样式:复用 .ai-reasoning 既有折叠/耗时规则 */
1312
+
1313
+ /* ── 动效守卫(§5.27 新增段) ── */
1314
+
1315
+ @media (prefers-reduced-motion: reduce) {
1316
+ .ai-thread,
1317
+ .ai-thread-action,
1318
+ .ai-threads-group-head,
1319
+ .ai-threads-group-head::before,
1320
+ .ai-branch-prev,
1321
+ .ai-branch-next,
1322
+ .ai-msg-continue {
1323
+ transition: none;
1324
+ }
1325
+ }
1326
+
1327
+ /* 触屏无 hover:动作钮常显(否则不可达) */
1328
+ @media (pointer: coarse) {
1329
+ .ai-thread .ai-thread-action {
1330
+ display: inline-flex;
1331
+ }
1332
+ }
@@ -0,0 +1,3 @@
1
+ // @icen.ai/ui — kit: ai-feedback(CSS + behavior 一行入口;构建产物,勿手改)
2
+ import './ai-panel.css';
3
+ export * from '../behaviors/ai-feedback.mjs';