@icen.ai/ui 0.5.0 → 0.8.1

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 (151) hide show
  1. package/AGENTS.md +262 -0
  2. package/README.md +52 -13
  3. package/dist/base.css +33 -5
  4. package/dist/behaviors/accordion.d.mts +7 -0
  5. package/dist/behaviors/accordion.mjs +37 -11
  6. package/dist/behaviors/ai-chat.d.mts +59 -0
  7. package/dist/behaviors/ai-chat.mjs +580 -0
  8. package/dist/behaviors/ai-composer.d.mts +137 -0
  9. package/dist/behaviors/ai-composer.mjs +2114 -0
  10. package/dist/behaviors/ai-core.d.mts +156 -0
  11. package/dist/behaviors/ai-core.mjs +504 -0
  12. package/dist/behaviors/ai-diff.d.mts +53 -0
  13. package/dist/behaviors/ai-diff.mjs +413 -0
  14. package/dist/behaviors/ai-panel.d.mts +101 -0
  15. package/dist/behaviors/ai-panel.mjs +1087 -0
  16. package/dist/behaviors/ai-provider.d.mts +198 -0
  17. package/dist/behaviors/ai-provider.mjs +1061 -0
  18. package/dist/behaviors/ai-tool.d.mts +26 -0
  19. package/dist/behaviors/ai-tool.mjs +2173 -0
  20. package/dist/behaviors/ai-tools.d.mts +84 -0
  21. package/dist/behaviors/ai-tools.mjs +1762 -0
  22. package/dist/behaviors/back-top.d.mts +20 -0
  23. package/dist/behaviors/back-top.mjs +64 -32
  24. package/dist/behaviors/carousel.d.mts +7 -0
  25. package/dist/behaviors/carousel.mjs +72 -27
  26. package/dist/behaviors/charts.d.mts +296 -0
  27. package/dist/behaviors/charts.mjs +1364 -440
  28. package/dist/behaviors/command-palette.d.mts +42 -0
  29. package/dist/behaviors/command-palette.mjs +167 -27
  30. package/dist/behaviors/context-menu.d.mts +53 -0
  31. package/dist/behaviors/context-menu.mjs +127 -39
  32. package/dist/behaviors/controls.d.mts +59 -0
  33. package/dist/behaviors/controls.mjs +601 -0
  34. package/dist/behaviors/copy.d.mts +7 -0
  35. package/dist/behaviors/copy.mjs +41 -15
  36. package/dist/behaviors/datatable.d.mts +90 -0
  37. package/dist/behaviors/datatable.mjs +279 -53
  38. package/dist/behaviors/date-picker.d.mts +4 -0
  39. package/dist/behaviors/date-picker.mjs +276 -69
  40. package/dist/behaviors/dropdown.d.mts +8 -0
  41. package/dist/behaviors/dropdown.mjs +171 -28
  42. package/dist/behaviors/events.d.mts +330 -0
  43. package/dist/behaviors/events.mjs +208 -0
  44. package/dist/behaviors/input.d.mts +4 -0
  45. package/dist/behaviors/input.mjs +52 -32
  46. package/dist/behaviors/modal.d.mts +30 -0
  47. package/dist/behaviors/modal.mjs +87 -16
  48. package/dist/behaviors/nav.d.mts +7 -0
  49. package/dist/behaviors/nav.mjs +57 -15
  50. package/dist/behaviors/notification.d.mts +188 -0
  51. package/dist/behaviors/notification.mjs +274 -140
  52. package/dist/behaviors/popover.d.mts +82 -0
  53. package/dist/behaviors/popover.mjs +92 -5
  54. package/dist/behaviors/select.d.mts +6 -0
  55. package/dist/behaviors/select.mjs +287 -45
  56. package/dist/behaviors/sidebar.d.mts +7 -0
  57. package/dist/behaviors/sidebar.mjs +44 -13
  58. package/dist/behaviors/slider.d.mts +7 -0
  59. package/dist/behaviors/slider.mjs +55 -43
  60. package/dist/behaviors/split-pane.d.mts +8 -0
  61. package/dist/behaviors/split-pane.mjs +70 -29
  62. package/dist/behaviors/tabs.d.mts +9 -0
  63. package/dist/behaviors/tabs.mjs +102 -26
  64. package/dist/behaviors/tag-input.d.mts +7 -0
  65. package/dist/behaviors/tag-input.mjs +66 -17
  66. package/dist/behaviors/theme.d.mts +36 -0
  67. package/dist/behaviors/theme.mjs +51 -5
  68. package/dist/behaviors/toast.d.mts +48 -0
  69. package/dist/behaviors/toast.mjs +98 -25
  70. package/dist/behaviors/tree.d.mts +7 -0
  71. package/dist/behaviors/tree.mjs +39 -15
  72. package/dist/behaviors/upload.d.mts +4 -0
  73. package/dist/behaviors/upload.mjs +79 -44
  74. package/dist/cli.mjs +1 -1
  75. package/dist/components/ai-chat.css +1037 -0
  76. package/dist/components/ai-chat.mjs +3 -0
  77. package/dist/components/ai-composer.mjs +3 -0
  78. package/dist/components/ai-context.mjs +2 -0
  79. package/dist/components/ai-diff.css +311 -0
  80. package/dist/components/ai-diff.mjs +3 -0
  81. package/dist/components/ai-files.mjs +3 -0
  82. package/dist/components/ai-message.mjs +3 -0
  83. package/dist/components/ai-panel.css +682 -0
  84. package/dist/components/ai-reasoning.mjs +3 -0
  85. package/dist/components/ai-subagent.mjs +2 -0
  86. package/dist/components/ai-todo.mjs +2 -0
  87. package/dist/components/ai-tool-call.mjs +2 -0
  88. package/dist/components/ai-tool.css +532 -0
  89. package/dist/components/ai-usage.mjs +3 -0
  90. package/dist/components/badge.css +5 -9
  91. package/dist/components/breadcrumb.css +0 -36
  92. package/dist/components/btn.css +4 -7
  93. package/dist/components/card.css +3 -6
  94. package/dist/components/chart-bar.css +10 -0
  95. package/dist/components/chart-calendar.mjs +4 -0
  96. package/dist/components/chart-gauge.css +1 -1
  97. package/dist/components/chart-heatmap.css +40 -0
  98. package/dist/components/chart-pie.css +0 -6
  99. package/dist/components/chart-scatter.css +24 -0
  100. package/dist/components/chart-scatter.mjs +4 -0
  101. package/dist/components/charts.css +87 -2
  102. package/dist/components/charts.mjs +1 -0
  103. package/dist/components/command-palette.css +2 -2
  104. package/dist/components/content.css +11 -25
  105. package/dist/components/copy.css +8 -8
  106. package/dist/components/datatable.css +5 -9
  107. package/dist/components/date-picker.css +4 -8
  108. package/dist/components/empty.css +0 -4
  109. package/dist/components/feedback.css +10 -17
  110. package/dist/components/form.css +13 -14
  111. package/dist/components/input.css +2 -2
  112. package/dist/components/layout.css +36 -36
  113. package/dist/components/media.css +12 -8
  114. package/dist/components/menu.css +2 -2
  115. package/dist/components/modal.css +1 -1
  116. package/dist/components/nav.css +11 -6
  117. package/dist/components/notification.css +27 -28
  118. package/dist/components/pagination.css +4 -2
  119. package/dist/components/pagination.mjs +1 -0
  120. package/dist/components/panel.css +16 -16
  121. package/dist/components/pill.css +15 -15
  122. package/dist/components/rating.mjs +1 -0
  123. package/dist/components/segmented.css +14 -6
  124. package/dist/components/segmented.mjs +1 -0
  125. package/dist/components/select.css +79 -24
  126. package/dist/components/sidebar.css +1 -1
  127. package/dist/components/split-pane.css +0 -1
  128. package/dist/components/stat.css +14 -18
  129. package/dist/components/steps.css +15 -5
  130. package/dist/components/steps.mjs +1 -0
  131. package/dist/components/switch.css +11 -13
  132. package/dist/components/switch.mjs +1 -0
  133. package/dist/components/table.css +46 -47
  134. package/dist/components/table.mjs +1 -0
  135. package/dist/components/tabs.css +57 -45
  136. package/dist/components/tag-input.css +5 -10
  137. package/dist/components/toast.css +16 -13
  138. package/dist/components/tooltip.css +2 -2
  139. package/dist/components/upload.css +0 -15
  140. package/dist/index.d.mts +39 -0
  141. package/dist/index.mjs +11243 -3312
  142. package/dist/registry.json +147 -9
  143. package/dist/retro-effects.css +23 -32
  144. package/dist/tokens/colors.css +4 -1
  145. package/dist/tokens/retro-effects.css +23 -32
  146. package/dist/tokens/style-profiles.css +24 -2
  147. package/dist/tokens/typography.css +2 -0
  148. package/dist/tokens.css +30 -3
  149. package/dist/ui.css +3420 -645
  150. package/docs/spec/ai-native.md +662 -0
  151. package/package.json +15 -5
@@ -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`;
@@ -18,18 +25,32 @@ function isAccepted(file, accept) {
18
25
  }
19
26
  var fileIconSvg = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/></svg>';
20
27
  var xIconSvg = '<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M18 6 6 18M6 6l12 12"/></svg>';
28
+ function parseSvg(source) {
29
+ try {
30
+ const doc = new DOMParser().parseFromString(source, "image/svg+xml");
31
+ const svg = doc.documentElement;
32
+ if (!svg || svg.tagName.toLowerCase() !== "svg") return null;
33
+ return document.importNode(svg, true);
34
+ } catch {
35
+ return null;
36
+ }
37
+ }
38
+ function sameFile(a, b) {
39
+ return a.name === b.name && a.size === b.size && a.lastModified === b.lastModified;
40
+ }
21
41
  function setup(zone) {
22
42
  const el = zone;
23
43
  if (el.__icenUploadInit) return;
24
- el.__icenUploadInit = true;
25
44
  const input = zone.querySelector('input[type="file"]');
26
45
  if (!input) return;
46
+ el.__icenUploadInit = true;
27
47
  const hasList = zone.hasAttribute("data-upload-list");
28
48
  const maxSize = Number(zone.getAttribute("data-max-size")) || 0;
29
49
  const maxFiles = Number(zone.getAttribute("data-max-files")) || 0;
30
50
  const accept = input.getAttribute("accept") ?? "";
31
51
  let currentFiles = [];
32
52
  const urlMap = /* @__PURE__ */ new Map();
53
+ let syncing = false;
33
54
  function getListEl() {
34
55
  if (!hasList) return null;
35
56
  let list = zone.querySelector(".upload-list");
@@ -43,8 +64,12 @@ function setup(zone) {
43
64
  function renderList() {
44
65
  const list = getListEl();
45
66
  if (!list) return;
46
- urlMap.forEach((url) => URL.revokeObjectURL(url));
47
- urlMap.clear();
67
+ urlMap.forEach((url, f) => {
68
+ if (!currentFiles.includes(f)) {
69
+ URL.revokeObjectURL(url);
70
+ urlMap.delete(f);
71
+ }
72
+ });
48
73
  list.textContent = "";
49
74
  if (currentFiles.length === 0) {
50
75
  list.hidden = true;
@@ -55,25 +80,29 @@ function setup(zone) {
55
80
  const li = document.createElement("li");
56
81
  li.className = "upload-list-item";
57
82
  const isImage = file.type.startsWith("image/");
83
+ let thumbUrl;
58
84
  if (isImage) {
59
- try {
60
- const url = URL.createObjectURL(file);
61
- urlMap.set(file, url);
62
- const img = document.createElement("img");
63
- img.className = "upload-list-item-thumb";
64
- img.src = url;
65
- img.alt = file.name;
66
- li.appendChild(img);
67
- } catch {
68
- const icon = document.createElement("span");
69
- icon.className = "upload-list-item-icon";
70
- icon.innerHTML = fileIconSvg;
71
- li.appendChild(icon);
85
+ thumbUrl = urlMap.get(file);
86
+ if (!thumbUrl) {
87
+ try {
88
+ thumbUrl = URL.createObjectURL(file);
89
+ urlMap.set(file, thumbUrl);
90
+ } catch {
91
+ thumbUrl = void 0;
92
+ }
72
93
  }
94
+ }
95
+ if (thumbUrl) {
96
+ const img = document.createElement("img");
97
+ img.className = "upload-list-item-thumb";
98
+ img.src = thumbUrl;
99
+ img.alt = file.name;
100
+ li.appendChild(img);
73
101
  } else {
74
102
  const icon = document.createElement("span");
75
103
  icon.className = "upload-list-item-icon";
76
- icon.innerHTML = fileIconSvg;
104
+ const svg = parseSvg(fileIconSvg);
105
+ if (svg) icon.appendChild(svg);
77
106
  li.appendChild(icon);
78
107
  }
79
108
  const info = document.createElement("div");
@@ -91,11 +120,12 @@ function setup(zone) {
91
120
  removeBtn.className = "upload-list-item-x";
92
121
  removeBtn.type = "button";
93
122
  removeBtn.setAttribute("aria-label", `\u79FB\u9664 ${file.name}`);
94
- removeBtn.innerHTML = xIconSvg;
123
+ const xSvg = parseSvg(xIconSvg);
124
+ if (xSvg) removeBtn.appendChild(xSvg);
95
125
  removeBtn.addEventListener("click", (ev) => {
96
126
  ev.stopPropagation();
97
127
  currentFiles = currentFiles.filter((f) => f !== file);
98
- zone.dispatchEvent(new CustomEvent("icen:upload-remove", { bubbles: true, detail: { file } }));
128
+ emitIcen(zone, "icen:upload-remove", { file });
99
129
  renderList();
100
130
  syncInputFiles();
101
131
  });
@@ -103,15 +133,21 @@ function setup(zone) {
103
133
  list.appendChild(li);
104
134
  }
105
135
  }
106
- function syncInputFiles() {
136
+ function setInputFiles(files) {
107
137
  try {
108
138
  const dt = new DataTransfer();
109
- for (const f of currentFiles) dt.items.add(f);
139
+ for (const f of files) dt.items.add(f);
140
+ syncing = true;
110
141
  input.files = dt.files;
111
142
  input.dispatchEvent(new Event("change", { bubbles: true }));
112
143
  } catch {
144
+ } finally {
145
+ syncing = false;
113
146
  }
114
147
  }
148
+ function syncInputFiles() {
149
+ setInputFiles(currentFiles);
150
+ }
115
151
  function validate(file) {
116
152
  if (accept && !isAccepted(file, accept)) return "\u6587\u4EF6\u7C7B\u578B\u4E0D\u652F\u6301";
117
153
  if (maxSize > 0 && file.size > maxSize) return `\u6587\u4EF6\u8D85\u8FC7 ${formatSize(maxSize)}`;
@@ -122,29 +158,34 @@ function setup(zone) {
122
158
  for (const file of files) {
123
159
  const reason = validate(file);
124
160
  if (reason) {
125
- zone.dispatchEvent(new CustomEvent("icen:upload-error", { bubbles: true, detail: { file, reason } }));
161
+ emitIcen(zone, "icen:upload-error", { file, reason });
126
162
  continue;
127
163
  }
128
164
  valid.push(file);
129
165
  }
130
166
  if (valid.length === 0) return;
131
167
  if (hasList) {
168
+ const fresh = [];
169
+ for (const f of valid) {
170
+ if (currentFiles.some((c) => sameFile(c, f)) || fresh.some((c) => sameFile(c, f))) continue;
171
+ fresh.push(f);
172
+ }
173
+ if (fresh.length === 0) return;
132
174
  if (maxFiles > 0) {
133
175
  const remaining = maxFiles - currentFiles.length;
134
176
  if (remaining <= 0) {
135
- zone.dispatchEvent(new CustomEvent("icen:upload-error", {
136
- bubbles: true,
137
- detail: { reason: `\u6700\u591A ${maxFiles} \u4E2A\u6587\u4EF6` }
138
- }));
177
+ emitIcen(zone, "icen:upload-error", { file: null, reason: `\u6700\u591A ${maxFiles} \u4E2A\u6587\u4EF6` });
139
178
  return;
140
179
  }
141
- valid.splice(remaining);
180
+ if (fresh.length > remaining) fresh.length = remaining;
142
181
  }
143
- currentFiles.push(...valid);
182
+ currentFiles.push(...fresh);
144
183
  renderList();
145
184
  syncInputFiles();
185
+ emitIcen(zone, "icen:upload", { files: fresh });
186
+ return;
146
187
  }
147
- zone.dispatchEvent(new CustomEvent("icen:upload", { bubbles: true, detail: { files: valid } }));
188
+ emitIcen(zone, "icen:upload", { files: valid });
148
189
  }
149
190
  const openPicker = () => input.click();
150
191
  zone.addEventListener("click", (ev) => {
@@ -155,19 +196,20 @@ function setup(zone) {
155
196
  });
156
197
  zone.addEventListener("keydown", (ev) => {
157
198
  if (ev.key === "Enter" || ev.key === " ") {
199
+ const tgt = ev.target;
200
+ if (tgt instanceof Element && tgt.closest(".upload-list")) return;
158
201
  ev.preventDefault();
159
202
  openPicker();
160
203
  }
161
204
  });
162
205
  input.addEventListener("change", () => {
206
+ if (syncing) return;
163
207
  if (!input.files) return;
164
208
  const files = Array.from(input.files);
165
209
  if (hasList) {
166
- handleFiles(files);
167
210
  input.value = "";
168
- } else {
169
- handleFiles(files);
170
211
  }
212
+ handleFiles(files);
171
213
  });
172
214
  zone.addEventListener("dragover", (ev) => {
173
215
  ev.preventDefault();
@@ -182,22 +224,15 @@ function setup(zone) {
182
224
  const files = ev.dataTransfer?.files;
183
225
  if (!files || files.length === 0) return;
184
226
  handleFiles(Array.from(files));
185
- if (!hasList) {
186
- try {
187
- const dt = new DataTransfer();
188
- for (const f of Array.from(files)) dt.items.add(f);
189
- input.files = dt.files;
190
- input.dispatchEvent(new Event("change", { bubbles: true }));
191
- } catch {
192
- }
193
- }
227
+ if (!hasList) setInputFiles(Array.from(files));
194
228
  });
195
229
  }
196
- function initUpload(root = document) {
230
+ function initUpload(root) {
197
231
  if (typeof document === "undefined") return;
232
+ const scope = root ?? document;
198
233
  const zones = [];
199
- if (root instanceof HTMLElement && root.matches(".upload")) zones.push(root);
200
- zones.push(...Array.from(root.querySelectorAll(".upload")));
234
+ if (scope instanceof HTMLElement && scope.matches(".upload")) zones.push(scope);
235
+ zones.push(...Array.from(scope.querySelectorAll(".upload")));
201
236
  for (const z of zones) setup(z);
202
237
  }
203
238
  export {
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