@icen.ai/ui 0.4.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 (155) 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 +101 -0
  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 +189 -0
  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 +296 -0
  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 +184 -8
  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 +908 -0
  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 +268 -34
  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 +102 -0
  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 +205 -17
  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/back-top.css +88 -0
  86. package/dist/components/back-top.mjs +3 -0
  87. package/dist/components/badge.css +127 -0
  88. package/dist/components/badge.mjs +2 -0
  89. package/dist/components/breadcrumb.css +53 -0
  90. package/dist/components/btn.css +4 -7
  91. package/dist/components/card.css +66 -1
  92. package/dist/components/chart-bar.css +10 -0
  93. package/dist/components/chart-calendar.mjs +5 -0
  94. package/dist/components/chart-gauge.css +1 -1
  95. package/dist/components/chart-heatmap.css +40 -0
  96. package/dist/components/chart-scatter.css +24 -0
  97. package/dist/components/chart-scatter.mjs +4 -0
  98. package/dist/components/charts.css +86 -1
  99. package/dist/components/charts.mjs +1 -0
  100. package/dist/components/command-palette.css +246 -0
  101. package/dist/components/command-palette.mjs +3 -0
  102. package/dist/components/content.css +221 -8
  103. package/dist/components/copy.css +93 -0
  104. package/dist/components/copy.mjs +3 -0
  105. package/dist/components/datatable.css +4 -8
  106. package/dist/components/date-picker.css +240 -0
  107. package/dist/components/date-picker.mjs +3 -0
  108. package/dist/components/empty.css +69 -3
  109. package/dist/components/feedback.css +241 -10
  110. package/dist/components/form.css +129 -11
  111. package/dist/components/form.mjs +2 -0
  112. package/dist/components/input.css +25 -2
  113. package/dist/components/layout.css +36 -36
  114. package/dist/components/media.css +153 -6
  115. package/dist/components/menu.css +2 -2
  116. package/dist/components/modal.css +1 -1
  117. package/dist/components/nav.css +106 -2
  118. package/dist/components/notification.css +387 -0
  119. package/dist/components/notification.mjs +3 -0
  120. package/dist/components/pagination.css +1 -1
  121. package/dist/components/panel.css +104 -9
  122. package/dist/components/pill.css +82 -12
  123. package/dist/components/scroll-area.css +112 -0
  124. package/dist/components/scroll-area.mjs +2 -0
  125. package/dist/components/segmented.css +80 -5
  126. package/dist/components/segmented.mjs +1 -0
  127. package/dist/components/select.css +104 -17
  128. package/dist/components/sidebar.css +119 -1
  129. package/dist/components/slider.css +113 -0
  130. package/dist/components/split-pane.css +111 -0
  131. package/dist/components/split-pane.mjs +3 -0
  132. package/dist/components/stat.css +131 -12
  133. package/dist/components/steps.css +98 -1
  134. package/dist/components/switch.css +145 -7
  135. package/dist/components/table.css +111 -29
  136. package/dist/components/tabs.css +132 -22
  137. package/dist/components/tag-input.css +4 -4
  138. package/dist/components/tag.css +164 -0
  139. package/dist/components/tag.mjs +2 -0
  140. package/dist/components/toast.css +122 -5
  141. package/dist/components/toolbar.css +101 -0
  142. package/dist/components/toolbar.mjs +2 -0
  143. package/dist/components/tooltip.css +2 -2
  144. package/dist/components/upload.css +61 -4
  145. package/dist/index.d.mts +37 -0
  146. package/dist/index.mjs +9409 -929
  147. package/dist/registry.json +176 -4
  148. package/dist/retro-effects.css +21 -17
  149. package/dist/tokens/colors.css +4 -1
  150. package/dist/tokens/retro-effects.css +21 -17
  151. package/dist/tokens/style-profiles.css +21 -0
  152. package/dist/tokens.css +25 -1
  153. package/dist/ui.css +9616 -2839
  154. package/docs/spec/ai-native.md +659 -0
  155. package/package.json +15 -5
@@ -0,0 +1,296 @@
1
+ // src/behaviors/date-picker.ts
2
+ var openPicker = null;
3
+ var panelEl = null;
4
+ var outsideHandler = null;
5
+ var escHandler = null;
6
+ var repositionHandler = null;
7
+ function pad(n) {
8
+ return n < 10 ? "0" + n : String(n);
9
+ }
10
+ function toDateStr(d) {
11
+ return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
12
+ }
13
+ function parseDateStr(s) {
14
+ const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(s.trim());
15
+ if (!m) return null;
16
+ const d = new Date(Number(m[1]), Number(m[2]) - 1, Number(m[3]));
17
+ return Number.isNaN(d.getTime()) ? null : d;
18
+ }
19
+ function format(d, fmt) {
20
+ if (!d) return "";
21
+ return fmt.replace(/yyyy/g, String(d.getFullYear())).replace(/yy/g, String(d.getFullYear()).slice(-2)).replace(/mm/g, pad(d.getMonth() + 1)).replace(/m/g, String(d.getMonth() + 1)).replace(/dd/g, pad(d.getDate())).replace(/d/g, String(d.getDate()));
22
+ }
23
+ function ensurePanel() {
24
+ if (panelEl && panelEl.isConnected) return panelEl;
25
+ const el = document.createElement("div");
26
+ el.className = "date-picker-panel";
27
+ el.setAttribute("role", "dialog");
28
+ el.setAttribute("aria-label", "\u9009\u62E9\u65E5\u671F");
29
+ el.hidden = true;
30
+ document.body.appendChild(el);
31
+ panelEl = el;
32
+ return el;
33
+ }
34
+ function positionPanel(panel, trigger) {
35
+ const r = trigger.getBoundingClientRect();
36
+ const pw = 280;
37
+ const ph = panel.offsetHeight || 320;
38
+ let left = r.left;
39
+ let top = r.bottom + 4;
40
+ if (left + pw > window.innerWidth - 12) left = window.innerWidth - pw - 12;
41
+ if (left < 12) left = 12;
42
+ if (top + ph > window.innerHeight - 12) top = r.top - ph - 4;
43
+ if (top < 12) top = 12;
44
+ panel.style.left = left + "px";
45
+ panel.style.top = top + "px";
46
+ }
47
+ var SVG_NS = "http://www.w3.org/2000/svg";
48
+ function buildCalIcon() {
49
+ const svg = document.createElementNS(SVG_NS, "svg");
50
+ svg.setAttribute("viewBox", "0 0 24 24");
51
+ svg.setAttribute("fill", "none");
52
+ svg.setAttribute("stroke", "currentColor");
53
+ svg.setAttribute("stroke-width", "2");
54
+ svg.setAttribute("stroke-linecap", "round");
55
+ svg.setAttribute("stroke-linejoin", "round");
56
+ const rect = document.createElementNS(SVG_NS, "rect");
57
+ rect.setAttribute("x", "3");
58
+ rect.setAttribute("y", "4");
59
+ rect.setAttribute("width", "18");
60
+ rect.setAttribute("height", "18");
61
+ rect.setAttribute("rx", "2");
62
+ rect.setAttribute("ry", "2");
63
+ svg.appendChild(rect);
64
+ const lines = [
65
+ ["16", "16", "2", "6"],
66
+ ["8", "8", "2", "6"],
67
+ ["3", "21", "10", "10"]
68
+ ];
69
+ for (const [x1, x2, y1, y2] of lines) {
70
+ const line = document.createElementNS(SVG_NS, "line");
71
+ line.setAttribute("x1", x1);
72
+ line.setAttribute("x2", x2);
73
+ line.setAttribute("y1", y1);
74
+ line.setAttribute("y2", y2);
75
+ svg.appendChild(line);
76
+ }
77
+ return svg;
78
+ }
79
+ var WEEK_LABELS_ZH = ["\u4E00", "\u4E8C", "\u4E09", "\u56DB", "\u4E94", "\u516D", "\u65E5"];
80
+ var WEEK_LABELS_SUN = ["\u65E5", "\u4E00", "\u4E8C", "\u4E09", "\u56DB", "\u4E94", "\u516D"];
81
+ function renderCalendar(panel, ctx) {
82
+ const { view, selected, min, max, weekStart, onPick } = ctx;
83
+ const year = view.getFullYear();
84
+ const month = view.getMonth();
85
+ const labels = weekStart === 0 ? WEEK_LABELS_SUN : WEEK_LABELS_ZH;
86
+ const firstDay = new Date(year, month, 1);
87
+ const offsetRaw = firstDay.getDay();
88
+ const leadDays = weekStart === 0 ? offsetRaw : (offsetRaw + 6) % 7;
89
+ const gridStart = new Date(year, month, 1 - leadDays);
90
+ const today = /* @__PURE__ */ new Date();
91
+ today.setHours(0, 0, 0, 0);
92
+ const cells = [];
93
+ for (let i = 0; i < 42; i++) {
94
+ cells.push(new Date(gridStart.getFullYear(), gridStart.getMonth(), gridStart.getDate() + i));
95
+ }
96
+ panel.textContent = "";
97
+ const head = document.createElement("div");
98
+ head.className = "date-picker-head";
99
+ const prevBtn = document.createElement("button");
100
+ prevBtn.type = "button";
101
+ prevBtn.className = "date-picker-nav";
102
+ prevBtn.setAttribute("aria-label", "\u4E0A\u4E00\u6708");
103
+ prevBtn.textContent = "\u2039";
104
+ const title = document.createElement("span");
105
+ title.className = "date-picker-title";
106
+ title.textContent = `${year} \u5E74 ${month + 1} \u6708`;
107
+ const nextBtn = document.createElement("button");
108
+ nextBtn.type = "button";
109
+ nextBtn.className = "date-picker-nav";
110
+ nextBtn.setAttribute("aria-label", "\u4E0B\u4E00\u6708");
111
+ nextBtn.textContent = "\u203A";
112
+ head.append(prevBtn, title, nextBtn);
113
+ const weekdays = document.createElement("div");
114
+ weekdays.className = "date-picker-weekdays";
115
+ for (const label of labels) {
116
+ const s = document.createElement("span");
117
+ s.textContent = label;
118
+ weekdays.appendChild(s);
119
+ }
120
+ const grid = document.createElement("div");
121
+ grid.className = "date-picker-grid";
122
+ for (const d of cells) {
123
+ const isOutside = d.getMonth() !== month;
124
+ const isToday = d.getTime() === today.getTime();
125
+ const isSelected = selected !== null && toDateStr(d) === toDateStr(selected);
126
+ const disabled = min !== null && d < min || max !== null && d > max;
127
+ const classes = ["date-picker-cell"];
128
+ if (isOutside) classes.push("is-outside");
129
+ if (isToday) classes.push("is-today");
130
+ if (isSelected) classes.push("is-selected");
131
+ if (disabled) classes.push("is-disabled");
132
+ const btn = document.createElement("button");
133
+ btn.type = "button";
134
+ btn.className = classes.join(" ");
135
+ btn.textContent = String(d.getDate());
136
+ if (disabled) btn.disabled = true;
137
+ btn.addEventListener("click", () => onPick(d));
138
+ grid.appendChild(btn);
139
+ }
140
+ const foot = document.createElement("div");
141
+ foot.className = "date-picker-foot";
142
+ const todayBtn = document.createElement("button");
143
+ todayBtn.type = "button";
144
+ todayBtn.className = "date-picker-today";
145
+ todayBtn.textContent = "\u4ECA\u5929";
146
+ const clearBtn = document.createElement("button");
147
+ clearBtn.type = "button";
148
+ clearBtn.className = "date-picker-clear";
149
+ clearBtn.textContent = "\u6E05\u9664";
150
+ foot.append(todayBtn, clearBtn);
151
+ panel.append(head, weekdays, grid, foot);
152
+ prevBtn.addEventListener("click", () => {
153
+ ctx.view = new Date(year, month - 1, 15);
154
+ renderCalendar(panel, { ...ctx, view: ctx.view });
155
+ });
156
+ nextBtn.addEventListener("click", () => {
157
+ ctx.view = new Date(year, month + 1, 15);
158
+ renderCalendar(panel, { ...ctx, view: ctx.view });
159
+ });
160
+ todayBtn.addEventListener("click", () => {
161
+ onPick(new Date(today.getFullYear(), today.getMonth(), today.getDate()));
162
+ });
163
+ clearBtn.addEventListener("click", () => {
164
+ onPick(null);
165
+ });
166
+ }
167
+ function closePanel() {
168
+ if (!panelEl) return;
169
+ panelEl.hidden = true;
170
+ openPicker?.querySelector(".date-picker-trigger")?.classList.remove("is-open");
171
+ openPicker = null;
172
+ if (outsideHandler) {
173
+ document.removeEventListener("mousedown", outsideHandler);
174
+ outsideHandler = null;
175
+ }
176
+ if (escHandler) {
177
+ window.removeEventListener("keydown", escHandler);
178
+ escHandler = null;
179
+ }
180
+ if (repositionHandler) {
181
+ window.removeEventListener("scroll", repositionHandler);
182
+ window.removeEventListener("resize", repositionHandler);
183
+ repositionHandler = null;
184
+ }
185
+ }
186
+ function setupPicker(container) {
187
+ const el = container;
188
+ if (el.__icenDatePickerInit) return;
189
+ el.__icenDatePickerInit = true;
190
+ const fmt = el.dataset.datePickerFormat ?? "yyyy-mm-dd";
191
+ const minStr = el.dataset.datePickerMin;
192
+ const maxStr = el.dataset.datePickerMax;
193
+ const weekStart = Number(el.dataset.datePickerWeekStart ?? "1");
194
+ const placeholder = el.dataset.datePickerPlaceholder ?? "\u9009\u62E9\u65E5\u671F";
195
+ const min = minStr ? parseDateStr(minStr) : null;
196
+ const max = maxStr ? parseDateStr(maxStr) : null;
197
+ const hidden = el.querySelector('input[type="hidden"]');
198
+ const initialStr = hidden?.value ?? "";
199
+ const initialDate = initialStr ? parseDateStr(initialStr) : null;
200
+ let trigger = el.querySelector(".date-picker-trigger");
201
+ if (!trigger) {
202
+ trigger = document.createElement("button");
203
+ trigger.type = "button";
204
+ trigger.className = "date-picker-trigger";
205
+ const value = document.createElement("span");
206
+ value.className = initialDate ? "date-picker-value" : "date-picker-value is-empty";
207
+ const icon = document.createElement("span");
208
+ icon.className = "date-picker-icon";
209
+ icon.appendChild(buildCalIcon());
210
+ trigger.append(value, icon);
211
+ el.prepend(trigger);
212
+ }
213
+ trigger.setAttribute("aria-haspopup", "dialog");
214
+ const valueEl = trigger.querySelector(".date-picker-value");
215
+ function paint() {
216
+ const v = hidden?.value ?? "";
217
+ const d = v ? parseDateStr(v) : null;
218
+ if (valueEl) {
219
+ valueEl.textContent = d ? format(d, fmt) : placeholder;
220
+ valueEl.classList.toggle("is-empty", !d);
221
+ }
222
+ }
223
+ paint();
224
+ if (hidden) {
225
+ const obs = new MutationObserver(() => paint());
226
+ obs.observe(hidden, { attributes: true, attributeFilter: ["value"] });
227
+ }
228
+ trigger.addEventListener("click", () => {
229
+ if (openPicker === el) {
230
+ closePanel();
231
+ return;
232
+ }
233
+ closePanel();
234
+ openPicker = el;
235
+ trigger.classList.add("is-open");
236
+ const panel = ensurePanel();
237
+ panel.hidden = false;
238
+ const selected = hidden?.value ? parseDateStr(hidden.value) : null;
239
+ const view = selected ? new Date(selected.getTime()) : /* @__PURE__ */ new Date();
240
+ view.setDate(15);
241
+ positionPanel(panel, trigger);
242
+ renderCalendar(panel, {
243
+ view,
244
+ selected,
245
+ min,
246
+ max,
247
+ weekStart,
248
+ onPick: (d) => {
249
+ if (d === null) {
250
+ if (hidden) {
251
+ hidden.value = "";
252
+ hidden.dispatchEvent(new Event("change", { bubbles: true }));
253
+ }
254
+ } else {
255
+ if (hidden) {
256
+ hidden.value = toDateStr(d);
257
+ hidden.dispatchEvent(new Event("change", { bubbles: true }));
258
+ }
259
+ }
260
+ paint();
261
+ closePanel();
262
+ }
263
+ });
264
+ outsideHandler = (ev) => {
265
+ const t = ev.target instanceof Node ? ev.target : null;
266
+ if (!t) return;
267
+ if (panel.contains(t) || el.contains(t)) return;
268
+ closePanel();
269
+ };
270
+ document.addEventListener("mousedown", outsideHandler);
271
+ escHandler = (ev) => {
272
+ if (ev.key === "Escape") {
273
+ ev.preventDefault();
274
+ closePanel();
275
+ }
276
+ };
277
+ window.addEventListener("keydown", escHandler);
278
+ repositionHandler = () => {
279
+ if (openPicker === el && !panel.hidden) positionPanel(panel, trigger);
280
+ };
281
+ window.addEventListener("scroll", repositionHandler, { passive: true });
282
+ window.addEventListener("resize", repositionHandler, { passive: true });
283
+ });
284
+ }
285
+ function initDatePicker(root) {
286
+ if (typeof document === "undefined") return;
287
+ if (typeof window === "undefined") return;
288
+ const scope = root ?? document;
289
+ const pickers = [];
290
+ if (scope instanceof Element && scope.matches(".date-picker[data-date-picker]")) pickers.push(scope);
291
+ pickers.push(...Array.from(scope.querySelectorAll(".date-picker[data-date-picker]")));
292
+ for (const p of pickers) setupPicker(p);
293
+ }
294
+ export {
295
+ initDatePicker
296
+ };
@@ -0,0 +1,4 @@
1
+ /** 绑定全部 [data-dropdown-trigger](document 级委托,重复调用安全)。 */
2
+ declare function initDropdown(root?: ParentNode): void;
3
+
4
+ export { initDropdown };
@@ -75,7 +75,10 @@ function onOutsidePointerDown(e) {
75
75
  if (active.panel.contains(target) || active.trigger.contains(target)) return;
76
76
  closeDropdown();
77
77
  }
78
- function onScroll() {
78
+ function onScroll(e) {
79
+ if (!active) return;
80
+ const target = e.target;
81
+ if (target instanceof Node && active.panel.contains(target)) return;
79
82
  closeDropdown();
80
83
  }
81
84
  function onKeyDown(e) {
@@ -101,9 +104,10 @@ function onKeyDown(e) {
101
104
  item.click();
102
105
  }
103
106
  }
104
- function initDropdown(root = document) {
107
+ function initDropdown(root) {
105
108
  if (typeof document === "undefined") return;
106
- root.querySelectorAll("[data-dropdown-trigger]").forEach((el) => el.setAttribute("aria-expanded", "false"));
109
+ const scope = root ?? document;
110
+ scope.querySelectorAll("[data-dropdown-trigger]").forEach((el) => el.setAttribute("aria-expanded", "false"));
107
111
  if (initialized) return;
108
112
  initialized = true;
109
113
  document.addEventListener("click", (e) => {
@@ -0,0 +1,4 @@
1
+ /** 为 root 下所有输入类结构初始化(root 自身匹配也算)。幂等。 */
2
+ declare function initInput(root?: ParentNode): void;
3
+
4
+ export { initInput };
@@ -1,7 +1,16 @@
1
1
  // src/behaviors/input.ts
2
+ var isComposing = false;
3
+ if (typeof document !== "undefined") {
4
+ document.addEventListener("compositionstart", () => {
5
+ isComposing = true;
6
+ });
7
+ document.addEventListener("compositionend", () => {
8
+ isComposing = false;
9
+ });
10
+ }
2
11
  function setupClear(wrap) {
3
12
  const btn = wrap.querySelector(".input-clear");
4
- const field = wrap.querySelector("input.input");
13
+ const field = wrap.querySelector("input.input, textarea.textarea");
5
14
  if (!btn || !field) return;
6
15
  btn.addEventListener("click", () => {
7
16
  field.value = "";
@@ -43,6 +52,145 @@ function setupAutosize(ta) {
43
52
  ta.addEventListener("input", () => resizeTextarea(ta));
44
53
  resizeTextarea(ta);
45
54
  }
55
+ function setupCounter(field) {
56
+ const el = field;
57
+ if (el.__icenCounterInit) return;
58
+ const maxAttr = field.getAttribute("maxlength");
59
+ const dataCount = field.getAttribute("data-count");
60
+ if (!maxAttr && dataCount === null) return;
61
+ el.__icenCounterInit = true;
62
+ const max = maxAttr ? Number(maxAttr) : 0;
63
+ let counter = null;
64
+ const wrap = field.closest(".input-wrap, .textarea-wrap");
65
+ if (wrap) {
66
+ counter = wrap.querySelector(".input-counter");
67
+ if (!counter) {
68
+ counter = document.createElement("span");
69
+ counter.className = "input-counter";
70
+ wrap.appendChild(counter);
71
+ if (wrap.classList.contains("input-wrap")) {
72
+ wrap.classList.add("input-wrap--has-counter");
73
+ }
74
+ }
75
+ }
76
+ const counterEl = counter;
77
+ if (!counterEl) return;
78
+ function update() {
79
+ const len = field.value.length;
80
+ counterEl.textContent = max > 0 ? `${len} / ${max}` : `${len}`;
81
+ counterEl.classList.remove("is-warn", "is-error");
82
+ if (max > 0) {
83
+ const ratio = len / max;
84
+ if (ratio >= 1) counterEl.classList.add("is-error");
85
+ else if (ratio >= 0.8) counterEl.classList.add("is-warn");
86
+ }
87
+ }
88
+ field.addEventListener("input", () => {
89
+ if (!isComposing) update();
90
+ });
91
+ update();
92
+ }
93
+ function setupMask(field) {
94
+ const el = field;
95
+ if (el.__icenMaskInit) return;
96
+ const mask = field.getAttribute("data-mask");
97
+ if (!mask) return;
98
+ el.__icenMaskInit = true;
99
+ const maskChars = [];
100
+ for (const ch of mask) maskChars.push(ch);
101
+ function isInputSlot(ch) {
102
+ return ch === "#" || ch === "A" || ch === "*";
103
+ }
104
+ function validate(rawChar, slotType) {
105
+ if (slotType === "#") return /[0-9]/.test(rawChar);
106
+ if (slotType === "A") return /[a-zA-Z]/.test(rawChar);
107
+ return true;
108
+ }
109
+ function applyMask(input) {
110
+ let result = "";
111
+ let rawIdx = 0;
112
+ const rawChars = input.split("");
113
+ for (let i = 0; i < maskChars.length && rawIdx < rawChars.length; i++) {
114
+ const slot = maskChars[i];
115
+ if (isInputSlot(slot)) {
116
+ while (rawIdx < rawChars.length && !validate(rawChars[rawIdx], slot)) {
117
+ rawIdx++;
118
+ }
119
+ if (rawIdx < rawChars.length) {
120
+ result += rawChars[rawIdx];
121
+ rawIdx++;
122
+ }
123
+ } else {
124
+ result += slot;
125
+ }
126
+ }
127
+ return result;
128
+ }
129
+ function extractRaw(formatted) {
130
+ let raw = "";
131
+ for (let i = 0; i < formatted.length && i < maskChars.length; i++) {
132
+ if (isInputSlot(maskChars[i])) raw += formatted[i];
133
+ }
134
+ return raw;
135
+ }
136
+ let lastFormatted = applyMask(field.value);
137
+ field.value = lastFormatted;
138
+ field.setAttribute("data-raw-value", extractRaw(lastFormatted));
139
+ field.addEventListener("input", () => {
140
+ if (isComposing) return;
141
+ const cursorPos = field.selectionStart ?? 0;
142
+ const oldRaw = extractRaw(lastFormatted);
143
+ const newFull = field.value;
144
+ const newRaw = extractRaw(newFull);
145
+ const formatted = applyMask(newRaw);
146
+ field.value = formatted;
147
+ field.setAttribute("data-raw-value", extractRaw(formatted));
148
+ lastFormatted = formatted;
149
+ const rawDelta = newRaw.length - oldRaw.length;
150
+ let targetCursor = cursorPos;
151
+ if (rawDelta > 0) {
152
+ let count = 0;
153
+ for (let i = 0; i < formatted.length; i++) {
154
+ if (isInputSlot(maskChars[i])) {
155
+ count++;
156
+ if (count >= newRaw.length) {
157
+ targetCursor = i + 1;
158
+ break;
159
+ }
160
+ }
161
+ }
162
+ } else if (rawDelta < 0) {
163
+ targetCursor = Math.min(cursorPos, formatted.length);
164
+ }
165
+ field.setSelectionRange(targetCursor, targetCursor);
166
+ });
167
+ field.addEventListener("keydown", (ev) => {
168
+ if (isComposing) return;
169
+ if (ev.ctrlKey || ev.metaKey || ev.altKey) return;
170
+ if (["Backspace", "Delete", "Tab", "Escape", "ArrowLeft", "ArrowRight", "Home", "End", "Enter"].includes(ev.key)) return;
171
+ const currentRaw = extractRaw(field.value);
172
+ const nextSlot = maskChars.find((ch, i) => {
173
+ const filledSlots = maskChars.slice(0, i).filter(isInputSlot).length;
174
+ return isInputSlot(ch) && filledSlots >= currentRaw.length;
175
+ });
176
+ if (nextSlot && !validate(ev.key, nextSlot)) {
177
+ ev.preventDefault();
178
+ }
179
+ });
180
+ }
181
+ function setupTrim(field) {
182
+ if (!field.hasAttribute("data-trim")) return;
183
+ const el = field;
184
+ if (el.__icenTrimInit) return;
185
+ el.__icenTrimInit = true;
186
+ field.addEventListener("blur", () => {
187
+ const trimmed = field.value.trim();
188
+ if (trimmed !== field.value) {
189
+ field.value = trimmed;
190
+ field.dispatchEvent(new Event("input", { bubbles: true }));
191
+ }
192
+ });
193
+ }
46
194
  function setupOtp(group) {
47
195
  const el = group;
48
196
  if (el.__icenInputInit) return;
@@ -57,6 +205,7 @@ function setupOtp(group) {
57
205
  }
58
206
  syncFilled(cell);
59
207
  cell.addEventListener("input", () => {
208
+ if (isComposing) return;
60
209
  const ch = cell.value.slice(-1);
61
210
  cell.value = ch;
62
211
  syncFilled(cell);
@@ -90,16 +239,18 @@ function setupOtp(group) {
90
239
  const fireComplete = () => {
91
240
  const code = cells.map((c) => c.value).join("");
92
241
  if (code.length === cells.length && cells.every((c) => c.value.length > 0)) {
242
+ group.dispatchEvent(new CustomEvent("icen:otp-complete", { detail: { code }, bubbles: true }));
93
243
  group.dispatchEvent(new CustomEvent("otp:complete", { detail: { code }, bubbles: true }));
94
244
  }
95
245
  };
96
246
  cells.forEach((cell) => cell.addEventListener("input", fireComplete));
97
247
  }
98
- function initInput(root = document) {
248
+ function initInput(root) {
99
249
  if (typeof document === "undefined") return;
250
+ const scope = root ?? document;
100
251
  const wraps = [];
101
- if (root instanceof Element && root.matches(".input-wrap")) wraps.push(root);
102
- wraps.push(...Array.from(root.querySelectorAll(".input-wrap")));
252
+ if (scope instanceof Element && scope.matches(".input-wrap")) wraps.push(scope);
253
+ wraps.push(...Array.from(scope.querySelectorAll(".input-wrap")));
103
254
  for (const w of wraps) {
104
255
  const el = w;
105
256
  if (el.__icenInputInit) continue;
@@ -108,12 +259,37 @@ function initInput(root = document) {
108
259
  setupPasswordToggle(w);
109
260
  }
110
261
  const textareas = [];
111
- if (root instanceof HTMLTextAreaElement && root.matches("[data-autosize]")) textareas.push(root);
112
- textareas.push(...Array.from(root.querySelectorAll("textarea[data-autosize]")));
262
+ if (scope instanceof HTMLTextAreaElement && scope.matches("[data-autosize]")) textareas.push(scope);
263
+ textareas.push(...Array.from(scope.querySelectorAll("textarea[data-autosize]")));
113
264
  for (const ta of textareas) setupAutosize(ta);
265
+ const countedFields = [];
266
+ if (scope instanceof Element) {
267
+ const fields = Array.from(scope.querySelectorAll(
268
+ "input[maxlength], input[data-count], textarea[maxlength], textarea[data-count]"
269
+ ));
270
+ countedFields.push(...fields);
271
+ if (scope instanceof HTMLInputElement || scope instanceof HTMLTextAreaElement) {
272
+ if (scope.hasAttribute("maxlength") || scope.hasAttribute("data-count")) countedFields.push(scope);
273
+ }
274
+ }
275
+ for (const f of countedFields) setupCounter(f);
276
+ const maskedInputs = [];
277
+ if (scope instanceof Element) {
278
+ maskedInputs.push(...Array.from(scope.querySelectorAll("input[data-mask]")));
279
+ if (scope instanceof HTMLInputElement && scope.hasAttribute("data-mask")) maskedInputs.push(scope);
280
+ }
281
+ for (const inp of maskedInputs) setupMask(inp);
282
+ const trimFields = [];
283
+ if (scope instanceof Element) {
284
+ trimFields.push(...Array.from(scope.querySelectorAll("[data-trim]")));
285
+ if ((scope instanceof HTMLInputElement || scope instanceof HTMLTextAreaElement) && scope.hasAttribute("data-trim")) {
286
+ trimFields.push(scope);
287
+ }
288
+ }
289
+ for (const f of trimFields) setupTrim(f);
114
290
  const otps = [];
115
- if (root instanceof Element && root.matches(".otp")) otps.push(root);
116
- otps.push(...Array.from(root.querySelectorAll(".otp")));
291
+ if (scope instanceof Element && scope.matches(".otp")) otps.push(scope);
292
+ otps.push(...Array.from(scope.querySelectorAll(".otp")));
117
293
  for (const g of otps) setupOtp(g);
118
294
  }
119
295
  export {
@@ -0,0 +1,11 @@
1
+ /** 打开指定 id 的 modal;已有其他 modal 打开时先关闭它。 */
2
+ declare function openModal(id: string): void;
3
+ /** 关闭指定 id 的 modal(backdrop 已被外部移除时也照常复位状态/监听/滚动锁)。 */
4
+ declare function closeModal(id: string): void;
5
+ /**
6
+ * 绑定触发器/关闭钮/backdrop 点击(document 级事件委托,重复调用安全)。
7
+ * root 用于扫描 [data-modal-open] 补 aria-haspopup=dialog。
8
+ */
9
+ declare function initModal(root?: ParentNode): void;
10
+
11
+ export { closeModal, initModal, openModal };
@@ -16,7 +16,10 @@ function openModal(id) {
16
16
  if (openId === id) return;
17
17
  if (openId) closeModal(openId);
18
18
  const backdrop = findBackdrop(id);
19
- if (!backdrop) return;
19
+ if (!backdrop) {
20
+ console.warn(`[@icen.ai/ui] modal: \u672A\u627E\u5230 data-modal="${id}" \u7684 .modal-backdrop\uFF0C\u5DF2\u8DF3\u8FC7\u6253\u5F00`);
21
+ return;
22
+ }
20
23
  const panel = backdrop.querySelector(".modal");
21
24
  if (!panel) return;
22
25
  portal(backdrop);
@@ -52,7 +55,9 @@ function openModal(id) {
52
55
  return;
53
56
  }
54
57
  if (e.key !== "Tab") return;
55
- const els = panel.querySelectorAll(FOCUSABLE);
58
+ const els = Array.from(panel.querySelectorAll(FOCUSABLE)).filter(
59
+ (el) => !el.hidden && el.offsetParent !== null
60
+ );
56
61
  if (els.length === 0) {
57
62
  e.preventDefault();
58
63
  panel.focus();
@@ -77,8 +82,7 @@ function openModal(id) {
77
82
  function closeModal(id) {
78
83
  if (typeof document === "undefined") return;
79
84
  const backdrop = findBackdrop(id);
80
- if (!backdrop) return;
81
- backdrop.hidden = true;
85
+ if (backdrop) backdrop.hidden = true;
82
86
  if (openId !== id) return;
83
87
  openId = null;
84
88
  if (keyHandler) {
@@ -89,9 +93,10 @@ function closeModal(id) {
89
93
  if (restoreFocusTo && restoreFocusTo.isConnected) restoreFocusTo.focus();
90
94
  restoreFocusTo = null;
91
95
  }
92
- function initModal(root = document) {
96
+ function initModal(root) {
93
97
  if (typeof document === "undefined") return;
94
- root.querySelectorAll("[data-modal-open]").forEach((el) => el.setAttribute("aria-haspopup", "dialog"));
98
+ const scope = root ?? document;
99
+ scope.querySelectorAll("[data-modal-open]").forEach((el) => el.setAttribute("aria-haspopup", "dialog"));
95
100
  if (initialized) return;
96
101
  initialized = true;
97
102
  document.addEventListener("click", (e) => {
@@ -0,0 +1,7 @@
1
+ /**
2
+ * 为 root 下每个 .nav[data-scroll-reactive] 初始化(root 自身匹配也算)。
3
+ * 返回销毁函数:移除本次初始化挂上的全部 window scroll/resize 监听。
4
+ */
5
+ declare function initNav(root?: ParentNode): () => void;
6
+
7
+ export { initNav };
@@ -5,9 +5,11 @@ function prefersReducedMotion() {
5
5
  }
6
6
  function setup(nav) {
7
7
  const el = nav;
8
- if (el.__icenNavInit) return;
8
+ if (el.__icenNavInit) return void 0;
9
9
  el.__icenNavInit = true;
10
- const threshold = Number(nav.dataset.navThreshold ?? "12") || 12;
10
+ const rawThreshold = nav.dataset.navThreshold;
11
+ const parsedThreshold = rawThreshold === void 0 || rawThreshold === "" ? NaN : Number(rawThreshold);
12
+ const threshold = Number.isFinite(parsedThreshold) ? parsedThreshold : 12;
11
13
  const hideOnScroll = nav.hasAttribute("data-nav-hide-on-scroll");
12
14
  let ticking = false;
13
15
  let lastY = window.scrollY;
@@ -42,14 +44,28 @@ function setup(nav) {
42
44
  window.addEventListener("scroll", onScroll, { passive: true });
43
45
  window.addEventListener("resize", onResize, { passive: true });
44
46
  apply();
47
+ return () => {
48
+ window.removeEventListener("scroll", onScroll);
49
+ window.removeEventListener("resize", onResize);
50
+ el.__icenNavInit = false;
51
+ };
45
52
  }
46
- function initNav(root = document) {
47
- if (typeof document === "undefined") return;
48
- if (typeof window === "undefined") return;
49
- const navs = [];
50
- if (root instanceof Element && root.matches(".nav[data-scroll-reactive]")) navs.push(root);
51
- navs.push(...Array.from(root.querySelectorAll(".nav[data-scroll-reactive]")));
52
- for (const n of navs) setup(n);
53
+ function initNav(root) {
54
+ const cleanups = [];
55
+ if (typeof document !== "undefined" && typeof window !== "undefined") {
56
+ const scope = root ?? document;
57
+ const navs = [];
58
+ if (scope instanceof Element && scope.matches(".nav[data-scroll-reactive]")) navs.push(scope);
59
+ navs.push(...Array.from(scope.querySelectorAll(".nav[data-scroll-reactive]")));
60
+ for (const n of navs) {
61
+ const cleanup = setup(n);
62
+ if (cleanup) cleanups.push(cleanup);
63
+ }
64
+ }
65
+ return () => {
66
+ for (const fn of cleanups) fn();
67
+ cleanups.length = 0;
68
+ };
53
69
  }
54
70
  export {
55
71
  initNav