@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
@@ -0,0 +1,208 @@
1
+ // src/behaviors/events.ts
2
+ function onIcen(type, a, b, c) {
3
+ if (typeof document === "undefined") return () => {
4
+ };
5
+ const selector = typeof a === "string" ? a : null;
6
+ const handler = selector ? b : a;
7
+ const opts = selector ? c : b;
8
+ const within = opts?.within ?? document;
9
+ const once = opts?.once === true;
10
+ let off = () => {
11
+ };
12
+ const fn = (e) => {
13
+ const broadcast = !(e.target instanceof Element);
14
+ if (selector) {
15
+ if (broadcast) return;
16
+ const detail = e.detail;
17
+ const src = detail?.source instanceof Element ? detail.source : e.target;
18
+ if (!(src instanceof Element) || !src.closest(selector)) return;
19
+ }
20
+ handler(e);
21
+ if (once) off();
22
+ };
23
+ within.addEventListener(type, fn);
24
+ off = () => {
25
+ within.removeEventListener(type, fn);
26
+ opts?.signal?.removeEventListener("abort", off);
27
+ };
28
+ if (opts?.signal) {
29
+ if (opts.signal.aborted) {
30
+ off();
31
+ return () => {
32
+ };
33
+ }
34
+ opts.signal.addEventListener("abort", off, { once: true });
35
+ }
36
+ return off;
37
+ }
38
+ function emitIcen(source, type, detail) {
39
+ return source.dispatchEvent(
40
+ new CustomEvent(type, { detail, bubbles: true, composed: true, cancelable: true })
41
+ );
42
+ }
43
+ var policies = /* @__PURE__ */ new Map();
44
+ function setEventPolicy(type, policy) {
45
+ if (policy) policies.set(type, policy);
46
+ else policies.delete(type);
47
+ }
48
+ function getEventPolicy(type) {
49
+ return policies.get(type);
50
+ }
51
+ function resetEventPolicies() {
52
+ policies.clear();
53
+ }
54
+ var LONGPRESS_MS = 520;
55
+ var LONGPRESS_MOVE = 12;
56
+ var CLICK_DELAY = 220;
57
+ var CLICK_DRAG_TOLERANCE = 5;
58
+ function gestureHost(e, name) {
59
+ const t = e.target;
60
+ if (!(t instanceof Element)) return null;
61
+ return t.closest(`[data-gestures~="${name}"]`);
62
+ }
63
+ function pointOf(e) {
64
+ return e instanceof MouseEvent || typeof PointerEvent !== "undefined" && e instanceof PointerEvent ? { x: e.clientX, y: e.clientY } : { x: 0, y: 0 };
65
+ }
66
+ function emit(host, type, originalEvent, text) {
67
+ const { x, y } = pointOf(originalEvent);
68
+ const detail = { source: host, originalEvent, x, y };
69
+ if (text != null) detail.text = text;
70
+ const ev = new CustomEvent(type, {
71
+ detail,
72
+ bubbles: true,
73
+ composed: true,
74
+ cancelable: true
75
+ });
76
+ const delivered = host.dispatchEvent(ev);
77
+ if (delivered) policies.get(type)?.(ev);
78
+ return ev;
79
+ }
80
+ var gesturesInit = false;
81
+ var gesturesTeardown = null;
82
+ function initGestures() {
83
+ if (gesturesInit || typeof document === "undefined") return () => {
84
+ };
85
+ gesturesInit = true;
86
+ let clickTimer = null;
87
+ let pending = null;
88
+ let downPos = null;
89
+ let suppressTrailingClick = false;
90
+ const cancelPendingClick = () => {
91
+ if (clickTimer) clearTimeout(clickTimer);
92
+ clickTimer = null;
93
+ pending = null;
94
+ };
95
+ const onDownPos = (e) => {
96
+ downPos = { x: e.clientX, y: e.clientY };
97
+ suppressTrailingClick = false;
98
+ };
99
+ const isDragRelease = (e) => downPos != null && Math.hypot(e.clientX - downPos.x, e.clientY - downPos.y) > CLICK_DRAG_TOLERANCE;
100
+ const onClick = (e) => {
101
+ const host = gestureHost(e, "click");
102
+ if (!host) return;
103
+ if (suppressTrailingClick) {
104
+ suppressTrailingClick = false;
105
+ return;
106
+ }
107
+ if (isDragRelease(e)) return;
108
+ if (host.matches('[data-gestures~="dblclick"]')) {
109
+ if (clickTimer) clearTimeout(clickTimer);
110
+ pending = { host, e };
111
+ clickTimer = setTimeout(() => {
112
+ clickTimer = null;
113
+ const p = pending;
114
+ pending = null;
115
+ if (p) emit(p.host, "icen:click", p.e);
116
+ }, CLICK_DELAY);
117
+ } else {
118
+ emit(host, "icen:click", e);
119
+ }
120
+ };
121
+ const onDblClick = (e) => {
122
+ const host = gestureHost(e, "dblclick");
123
+ if (!host) return;
124
+ cancelPendingClick();
125
+ if (isDragRelease(e)) return;
126
+ emit(host, "icen:dblclick", e);
127
+ };
128
+ const onContextMenu = (e) => {
129
+ const host = gestureHost(e, "contextmenu");
130
+ if (!host) return;
131
+ const hasPolicy = policies.has("icen:contextmenu");
132
+ const ev = emit(host, "icen:contextmenu", e);
133
+ if (ev.defaultPrevented || hasPolicy) e.preventDefault();
134
+ };
135
+ let lp = null;
136
+ const clearLp = () => {
137
+ if (lp) clearTimeout(lp.timer);
138
+ lp = null;
139
+ };
140
+ const onLpDown = (e) => {
141
+ const host = gestureHost(e, "longpress");
142
+ if (!host) return;
143
+ clearLp();
144
+ lp = {
145
+ host,
146
+ e,
147
+ x: e.clientX,
148
+ y: e.clientY,
149
+ timer: setTimeout(() => {
150
+ const cur = lp;
151
+ lp = null;
152
+ if (cur) {
153
+ suppressTrailingClick = true;
154
+ emit(cur.host, "icen:longpress", cur.e);
155
+ }
156
+ }, LONGPRESS_MS)
157
+ };
158
+ };
159
+ const onLpMove = (e) => {
160
+ if (!lp) return;
161
+ if (Math.hypot(e.clientX - lp.x, e.clientY - lp.y) > LONGPRESS_MOVE) clearLp();
162
+ };
163
+ const onTextSelect = (e) => {
164
+ const host = gestureHost(e, "text-select");
165
+ if (!host) return;
166
+ const sel = window.getSelection();
167
+ const text = sel?.toString() ?? "";
168
+ if (!text.trim() || !sel || !host.contains(sel.anchorNode)) return;
169
+ emit(host, "icen:text-select", e, text);
170
+ };
171
+ document.addEventListener("pointerdown", onDownPos);
172
+ document.addEventListener("click", onClick);
173
+ document.addEventListener("dblclick", onDblClick);
174
+ document.addEventListener("contextmenu", onContextMenu);
175
+ document.addEventListener("pointerdown", onLpDown);
176
+ document.addEventListener("pointermove", onLpMove);
177
+ document.addEventListener("pointerup", clearLp);
178
+ document.addEventListener("pointercancel", clearLp);
179
+ document.addEventListener("mouseup", onTextSelect);
180
+ gesturesTeardown = () => {
181
+ document.removeEventListener("pointerdown", onDownPos);
182
+ document.removeEventListener("click", onClick);
183
+ document.removeEventListener("dblclick", onDblClick);
184
+ document.removeEventListener("contextmenu", onContextMenu);
185
+ document.removeEventListener("pointerdown", onLpDown);
186
+ document.removeEventListener("pointermove", onLpMove);
187
+ document.removeEventListener("pointerup", clearLp);
188
+ document.removeEventListener("pointercancel", clearLp);
189
+ document.removeEventListener("mouseup", onTextSelect);
190
+ cancelPendingClick();
191
+ clearLp();
192
+ gesturesInit = false;
193
+ gesturesTeardown = null;
194
+ };
195
+ return gesturesTeardown;
196
+ }
197
+ function destroyGestures() {
198
+ gesturesTeardown?.();
199
+ }
200
+ export {
201
+ destroyGestures,
202
+ emitIcen,
203
+ getEventPolicy,
204
+ initGestures,
205
+ onIcen,
206
+ resetEventPolicies,
207
+ setEventPolicy
208
+ };
@@ -0,0 +1,4 @@
1
+ /** 为 root 下所有输入类结构初始化(root 自身匹配也算)。幂等。 */
2
+ declare function initInput(root?: ParentNode): void;
3
+
4
+ export { initInput };
@@ -1,12 +1,20 @@
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/input.ts
2
- var isComposing = false;
3
- if (typeof document !== "undefined") {
4
- document.addEventListener("compositionstart", () => {
5
- isComposing = true;
9
+ function trackComposing(target) {
10
+ let composing = false;
11
+ target.addEventListener("compositionstart", () => {
12
+ composing = true;
6
13
  });
7
- document.addEventListener("compositionend", () => {
8
- isComposing = false;
14
+ target.addEventListener("compositionend", () => {
15
+ composing = false;
9
16
  });
17
+ return () => composing;
10
18
  }
11
19
  function setupClear(wrap) {
12
20
  const btn = wrap.querySelector(".input-clear");
@@ -53,6 +61,8 @@ function setupAutosize(ta) {
53
61
  resizeTextarea(ta);
54
62
  }
55
63
  function setupCounter(field) {
64
+ const el = field;
65
+ if (el.__icenCounterInit) return;
56
66
  const maxAttr = field.getAttribute("maxlength");
57
67
  const dataCount = field.getAttribute("data-count");
58
68
  if (!maxAttr && dataCount === null) return;
@@ -72,6 +82,8 @@ function setupCounter(field) {
72
82
  }
73
83
  const counterEl = counter;
74
84
  if (!counterEl) return;
85
+ el.__icenCounterInit = true;
86
+ const isComposing = trackComposing(field);
75
87
  function update() {
76
88
  const len = field.value.length;
77
89
  counterEl.textContent = max > 0 ? `${len} / ${max}` : `${len}`;
@@ -82,14 +94,19 @@ function setupCounter(field) {
82
94
  else if (ratio >= 0.8) counterEl.classList.add("is-warn");
83
95
  }
84
96
  }
97
+ ;
85
98
  field.addEventListener("input", () => {
86
- if (!isComposing) update();
99
+ if (!isComposing()) update();
87
100
  });
88
101
  update();
89
102
  }
90
103
  function setupMask(field) {
104
+ const el = field;
105
+ if (el.__icenMaskInit) return;
91
106
  const mask = field.getAttribute("data-mask");
92
107
  if (!mask) return;
108
+ el.__icenMaskInit = true;
109
+ const isComposing = trackComposing(field);
93
110
  const maskChars = [];
94
111
  for (const ch of mask) maskChars.push(ch);
95
112
  function isInputSlot(ch) {
@@ -131,7 +148,7 @@ function setupMask(field) {
131
148
  field.value = lastFormatted;
132
149
  field.setAttribute("data-raw-value", extractRaw(lastFormatted));
133
150
  field.addEventListener("input", () => {
134
- if (isComposing) return;
151
+ if (isComposing()) return;
135
152
  const cursorPos = field.selectionStart ?? 0;
136
153
  const oldRaw = extractRaw(lastFormatted);
137
154
  const newFull = field.value;
@@ -159,14 +176,10 @@ function setupMask(field) {
159
176
  field.setSelectionRange(targetCursor, targetCursor);
160
177
  });
161
178
  field.addEventListener("keydown", (ev) => {
162
- if (isComposing) return;
179
+ if (isComposing()) return;
163
180
  if (ev.ctrlKey || ev.metaKey || ev.altKey) return;
164
181
  if (["Backspace", "Delete", "Tab", "Escape", "ArrowLeft", "ArrowRight", "Home", "End", "Enter"].includes(ev.key)) return;
165
182
  const currentRaw = extractRaw(field.value);
166
- const nextSlotIdx = maskChars.findIndex((ch, i) => {
167
- if (!isInputSlot(ch)) return false;
168
- return i >= field.value.length || !isInputSlot(maskChars[Math.min(field.value.length, maskChars.length - 1)]);
169
- });
170
183
  const nextSlot = maskChars.find((ch, i) => {
171
184
  const filledSlots = maskChars.slice(0, i).filter(isInputSlot).length;
172
185
  return isInputSlot(ch) && filledSlots >= currentRaw.length;
@@ -178,6 +191,9 @@ function setupMask(field) {
178
191
  }
179
192
  function setupTrim(field) {
180
193
  if (!field.hasAttribute("data-trim")) return;
194
+ const el = field;
195
+ if (el.__icenTrimInit) return;
196
+ el.__icenTrimInit = true;
181
197
  field.addEventListener("blur", () => {
182
198
  const trimmed = field.value.trim();
183
199
  if (trimmed !== field.value) {
@@ -190,6 +206,7 @@ function setupOtp(group) {
190
206
  const el = group;
191
207
  if (el.__icenInputInit) return;
192
208
  el.__icenInputInit = true;
209
+ const isComposing = trackComposing(group);
193
210
  const cells = Array.from(group.querySelectorAll(".otp-cell"));
194
211
  const syncFilled = (cell) => {
195
212
  cell.classList.toggle("is-filled", cell.value.length > 0);
@@ -200,7 +217,7 @@ function setupOtp(group) {
200
217
  }
201
218
  syncFilled(cell);
202
219
  cell.addEventListener("input", () => {
203
- if (isComposing) return;
220
+ if (isComposing()) return;
204
221
  const ch = cell.value.slice(-1);
205
222
  cell.value = ch;
206
223
  syncFilled(cell);
@@ -234,16 +251,19 @@ function setupOtp(group) {
234
251
  const fireComplete = () => {
235
252
  const code = cells.map((c) => c.value).join("");
236
253
  if (code.length === cells.length && cells.every((c) => c.value.length > 0)) {
254
+ group.classList.add("is-complete");
255
+ emitIcen(group, "icen:otp-complete", { code });
237
256
  group.dispatchEvent(new CustomEvent("otp:complete", { detail: { code }, bubbles: true }));
238
257
  }
239
258
  };
240
259
  cells.forEach((cell) => cell.addEventListener("input", fireComplete));
241
260
  }
242
- function initInput(root = document) {
261
+ function initInput(root) {
243
262
  if (typeof document === "undefined") return;
263
+ const scope = root ?? document;
244
264
  const wraps = [];
245
- if (root instanceof Element && root.matches(".input-wrap")) wraps.push(root);
246
- wraps.push(...Array.from(root.querySelectorAll(".input-wrap")));
265
+ if (scope instanceof Element && scope.matches(".input-wrap")) wraps.push(scope);
266
+ wraps.push(...Array.from(scope.querySelectorAll(".input-wrap")));
247
267
  for (const w of wraps) {
248
268
  const el = w;
249
269
  if (el.__icenInputInit) continue;
@@ -252,37 +272,37 @@ function initInput(root = document) {
252
272
  setupPasswordToggle(w);
253
273
  }
254
274
  const textareas = [];
255
- if (root instanceof HTMLTextAreaElement && root.matches("[data-autosize]")) textareas.push(root);
256
- textareas.push(...Array.from(root.querySelectorAll("textarea[data-autosize]")));
275
+ if (scope instanceof HTMLTextAreaElement && scope.matches("[data-autosize]")) textareas.push(scope);
276
+ textareas.push(...Array.from(scope.querySelectorAll("textarea[data-autosize]")));
257
277
  for (const ta of textareas) setupAutosize(ta);
258
278
  const countedFields = [];
259
- if (root instanceof Element) {
260
- const fields = Array.from(root.querySelectorAll(
279
+ if (scope instanceof Element) {
280
+ const fields = Array.from(scope.querySelectorAll(
261
281
  "input[maxlength], input[data-count], textarea[maxlength], textarea[data-count]"
262
282
  ));
263
283
  countedFields.push(...fields);
264
- if (root instanceof HTMLInputElement || root instanceof HTMLTextAreaElement) {
265
- if (root.hasAttribute("maxlength") || root.hasAttribute("data-count")) countedFields.push(root);
284
+ if (scope instanceof HTMLInputElement || scope instanceof HTMLTextAreaElement) {
285
+ if (scope.hasAttribute("maxlength") || scope.hasAttribute("data-count")) countedFields.push(scope);
266
286
  }
267
287
  }
268
288
  for (const f of countedFields) setupCounter(f);
269
289
  const maskedInputs = [];
270
- if (root instanceof Element) {
271
- maskedInputs.push(...Array.from(root.querySelectorAll("input[data-mask]")));
272
- if (root instanceof HTMLInputElement && root.hasAttribute("data-mask")) maskedInputs.push(root);
290
+ if (scope instanceof Element) {
291
+ maskedInputs.push(...Array.from(scope.querySelectorAll("input[data-mask]")));
292
+ if (scope instanceof HTMLInputElement && scope.hasAttribute("data-mask")) maskedInputs.push(scope);
273
293
  }
274
294
  for (const inp of maskedInputs) setupMask(inp);
275
295
  const trimFields = [];
276
- if (root instanceof Element) {
277
- trimFields.push(...Array.from(root.querySelectorAll("[data-trim]")));
278
- if ((root instanceof HTMLInputElement || root instanceof HTMLTextAreaElement) && root.hasAttribute("data-trim")) {
279
- trimFields.push(root);
296
+ if (scope instanceof Element) {
297
+ trimFields.push(...Array.from(scope.querySelectorAll("[data-trim]")));
298
+ if ((scope instanceof HTMLInputElement || scope instanceof HTMLTextAreaElement) && scope.hasAttribute("data-trim")) {
299
+ trimFields.push(scope);
280
300
  }
281
301
  }
282
302
  for (const f of trimFields) setupTrim(f);
283
303
  const otps = [];
284
- if (root instanceof Element && root.matches(".otp")) otps.push(root);
285
- otps.push(...Array.from(root.querySelectorAll(".otp")));
304
+ if (scope instanceof Element && scope.matches(".otp")) otps.push(scope);
305
+ otps.push(...Array.from(scope.querySelectorAll(".otp")));
286
306
  for (const g of otps) setupOtp(g);
287
307
  }
288
308
  export {
@@ -0,0 +1,30 @@
1
+ /** 焦点陷阱的可聚焦元素选择器(modal / command-palette 焦点陷阱全库共用)。 */
2
+ declare const FOCUSABLE = "button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex=\"-1\"]), [contenteditable]:not([contenteditable=\"false\"]), audio, video, details>summary";
3
+ /** openModal 可选项。 */
4
+ interface ModalOpenOptions {
5
+ /**
6
+ * 初始焦点:缺省 = 聚焦面板内第一个可聚焦元素(无则面板自身);
7
+ * 传 HTMLElement = 聚焦该元素;'none' = 不动焦点(调用方自管,焦点陷阱仍生效)。
8
+ */
9
+ initialFocus?: HTMLElement | 'none';
10
+ }
11
+ /**
12
+ * 打开指定 id 的 modal;已有其他 modal 打开时先关闭它。
13
+ * 打开成功后从 document 派发 icen:modal-open { id }(广播)。
14
+ */
15
+ declare function openModal(id: string, opts?: ModalOpenOptions): void;
16
+ /**
17
+ * 关闭指定 id 的 modal(backdrop 已被外部移除时也照常复位状态/监听/滚动锁)。
18
+ * 关闭已打开的 modal 后从 document 派发 icen:modal-close { id }(广播);
19
+ * 关闭未被接管打开的 modal(幂等空操作路径)不派发。
20
+ */
21
+ declare function closeModal(id: string): void;
22
+ /**
23
+ * 绑定触发器/关闭钮/backdrop 点击(document 级事件委托,重复调用安全)。
24
+ * root 用于扫描 [data-modal-open] 补 aria-haspopup=dialog。
25
+ * 返回销毁函数:摘除 document 委托并复位幂等标记(销毁后可重新 init);
26
+ * 销毁时若有 modal 开着会顺带关闭(复位滚动锁/监听/焦点)。SSR 下返回 no-op。
27
+ */
28
+ declare function initModal(root?: ParentNode): () => void;
29
+
30
+ export { FOCUSABLE, type ModalOpenOptions, closeModal, initModal, openModal };
@@ -1,5 +1,53 @@
1
+ // src/behaviors/popover.ts
2
+ var PANEL_SIZING_ATTRS = {
3
+ width: "data-panel-width",
4
+ minWidth: "data-panel-min",
5
+ maxWidth: "data-panel-max",
6
+ minHeight: "data-panel-min-height",
7
+ maxHeight: "data-panel-max-height"
8
+ };
9
+ function readPanelSizing(el) {
10
+ const out = {};
11
+ if (!el) return out;
12
+ for (const key of Object.keys(PANEL_SIZING_ATTRS)) {
13
+ const raw = el.getAttribute(PANEL_SIZING_ATTRS[key]);
14
+ if (raw == null) continue;
15
+ const n = Number(raw);
16
+ if (Number.isFinite(n) && n > 0) out[key] = n;
17
+ }
18
+ return out;
19
+ }
20
+ function applyPanelSizing(panel, sizing) {
21
+ if (sizing == null) {
22
+ panel.style.width = "";
23
+ panel.style.minWidth = "";
24
+ panel.style.maxWidth = "";
25
+ panel.style.minHeight = "";
26
+ panel.style.maxHeight = "";
27
+ return;
28
+ }
29
+ const vw = typeof document === "undefined" ? Number.POSITIVE_INFINITY : document.documentElement.clientWidth;
30
+ const fitW = (n) => Math.min(n, Math.max(80, vw - 24));
31
+ if (sizing.width != null) {
32
+ const w = fitW(sizing.width);
33
+ panel.style.width = `${w}px`;
34
+ panel.style.maxWidth = `${w}px`;
35
+ }
36
+ if (sizing.minWidth != null) panel.style.minWidth = `${fitW(sizing.minWidth)}px`;
37
+ if (sizing.maxWidth != null && sizing.width == null) panel.style.maxWidth = `${fitW(sizing.maxWidth)}px`;
38
+ if (sizing.minHeight != null) panel.style.minHeight = `${sizing.minHeight}px`;
39
+ if (sizing.maxHeight != null) panel.style.maxHeight = `${sizing.maxHeight}px`;
40
+ }
41
+
42
+ // src/behaviors/events.ts
43
+ function emitIcen(source, type, detail) {
44
+ return source.dispatchEvent(
45
+ new CustomEvent(type, { detail, bubbles: true, composed: true, cancelable: true })
46
+ );
47
+ }
48
+
1
49
  // src/behaviors/modal.ts
2
- var FOCUSABLE = 'button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])';
50
+ var FOCUSABLE = 'button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"]), [contenteditable]:not([contenteditable="false"]), audio, video, details>summary';
3
51
  var openId = null;
4
52
  var restoreFocusTo = null;
5
53
  var prevBodyOverflow = "";
@@ -11,14 +59,18 @@ function findBackdrop(id) {
11
59
  function portal(backdrop) {
12
60
  if (backdrop.parentElement !== document.body) document.body.appendChild(backdrop);
13
61
  }
14
- function openModal(id) {
62
+ function openModal(id, opts) {
15
63
  if (typeof document === "undefined") return;
16
64
  if (openId === id) return;
17
65
  if (openId) closeModal(openId);
18
66
  const backdrop = findBackdrop(id);
19
- if (!backdrop) return;
67
+ if (!backdrop) {
68
+ console.warn(`[@icen.ai/ui] modal: \u672A\u627E\u5230 data-modal="${id}" \u7684 .modal-backdrop\uFF0C\u5DF2\u8DF3\u8FC7\u6253\u5F00`);
69
+ return;
70
+ }
20
71
  const panel = backdrop.querySelector(".modal");
21
72
  if (!panel) return;
73
+ applyPanelSizing(panel, readPanelSizing(backdrop));
22
74
  portal(backdrop);
23
75
  restoreFocusTo = document.activeElement instanceof HTMLElement ? document.activeElement : null;
24
76
  openId = id;
@@ -31,14 +83,14 @@ function openModal(id) {
31
83
  if (!panel.hasAttribute("aria-labelledby")) {
32
84
  const title = panel.querySelector(".modal-title");
33
85
  if (title) {
34
- if (!title.id) title.id = `modal-title-${CSS.escape(id)}`;
86
+ if (!title.id) title.id = `modal-title-${id.replace(/[^\w-]/g, "-")}`;
35
87
  panel.setAttribute("aria-labelledby", title.id);
36
88
  }
37
89
  }
38
90
  if (!panel.hasAttribute("aria-describedby")) {
39
91
  const desc = panel.querySelector(".modal-description");
40
92
  if (desc) {
41
- if (!desc.id) desc.id = `modal-desc-${CSS.escape(id)}`;
93
+ if (!desc.id) desc.id = `modal-desc-${id.replace(/[^\w-]/g, "-")}`;
42
94
  panel.setAttribute("aria-describedby", desc.id);
43
95
  }
44
96
  }
@@ -52,7 +104,9 @@ function openModal(id) {
52
104
  return;
53
105
  }
54
106
  if (e.key !== "Tab") return;
55
- const els = panel.querySelectorAll(FOCUSABLE);
107
+ const els = Array.from(panel.querySelectorAll(FOCUSABLE)).filter(
108
+ (el) => !el.hidden && el.offsetParent !== null
109
+ );
56
110
  if (els.length === 0) {
57
111
  e.preventDefault();
58
112
  panel.focus();
@@ -71,14 +125,19 @@ function openModal(id) {
71
125
  }
72
126
  };
73
127
  window.addEventListener("keydown", keyHandler);
74
- const focusable = panel.querySelector(FOCUSABLE);
75
- (focusable ?? panel).focus();
128
+ if (opts?.initialFocus === "none") {
129
+ } else if (opts?.initialFocus instanceof HTMLElement) {
130
+ opts.initialFocus.focus();
131
+ } else {
132
+ const focusable = panel.querySelector(FOCUSABLE);
133
+ (focusable ?? panel).focus();
134
+ }
135
+ emitIcen(document, "icen:modal-open", { id });
76
136
  }
77
137
  function closeModal(id) {
78
138
  if (typeof document === "undefined") return;
79
139
  const backdrop = findBackdrop(id);
80
- if (!backdrop) return;
81
- backdrop.hidden = true;
140
+ if (backdrop) backdrop.hidden = true;
82
141
  if (openId !== id) return;
83
142
  openId = null;
84
143
  if (keyHandler) {
@@ -88,13 +147,17 @@ function closeModal(id) {
88
147
  document.body.style.overflow = prevBodyOverflow;
89
148
  if (restoreFocusTo && restoreFocusTo.isConnected) restoreFocusTo.focus();
90
149
  restoreFocusTo = null;
150
+ emitIcen(document, "icen:modal-close", { id });
91
151
  }
92
- function initModal(root = document) {
93
- if (typeof document === "undefined") return;
94
- root.querySelectorAll("[data-modal-open]").forEach((el) => el.setAttribute("aria-haspopup", "dialog"));
95
- if (initialized) return;
152
+ function initModal(root) {
153
+ if (typeof document === "undefined") return () => {
154
+ };
155
+ const scope = root ?? document;
156
+ scope.querySelectorAll("[data-modal-open]").forEach((el) => el.setAttribute("aria-haspopup", "dialog"));
157
+ if (initialized) return () => {
158
+ };
96
159
  initialized = true;
97
- document.addEventListener("click", (e) => {
160
+ const onClick = (e) => {
98
161
  const target = e.target instanceof Element ? e.target : null;
99
162
  if (!target) return;
100
163
  const opener = target.closest("[data-modal-open]");
@@ -112,9 +175,17 @@ function initModal(root = document) {
112
175
  if (target instanceof HTMLElement && target.classList.contains("modal-backdrop") && target.dataset.modal && target.dataset.closeOnOverlay !== "false") {
113
176
  closeModal(target.dataset.modal);
114
177
  }
115
- });
178
+ };
179
+ document.addEventListener("click", onClick);
180
+ const destroy = () => {
181
+ document.removeEventListener("click", onClick);
182
+ if (openId) closeModal(openId);
183
+ initialized = false;
184
+ };
185
+ return destroy;
116
186
  }
117
187
  export {
188
+ FOCUSABLE,
118
189
  closeModal,
119
190
  initModal,
120
191
  openModal
@@ -0,0 +1,7 @@
1
+ /**
2
+ * 为 root 下每个 .nav[data-scroll-reactive] 初始化(root 自身匹配也算)。
3
+ * 返回销毁函数:移除本次初始化挂上的全部 window scroll/resize 与 nav click 监听。
4
+ */
5
+ declare function initNav(root?: ParentNode): () => void;
6
+
7
+ export { initNav };