@icen.ai/ui 0.8.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 (106) hide show
  1. package/AGENTS.md +15 -9
  2. package/README.md +8 -3
  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-chat.d.mts +3 -1
  7. package/dist/behaviors/ai-chat.mjs +66 -43
  8. package/dist/behaviors/ai-composer.d.mts +28 -3
  9. package/dist/behaviors/ai-composer.mjs +291 -209
  10. package/dist/behaviors/ai-core.d.mts +9 -4
  11. package/dist/behaviors/ai-core.mjs +21 -8
  12. package/dist/behaviors/ai-diff.d.mts +2 -2
  13. package/dist/behaviors/ai-diff.mjs +172 -41
  14. package/dist/behaviors/ai-panel.d.mts +11 -8
  15. package/dist/behaviors/ai-panel.mjs +185 -110
  16. package/dist/behaviors/ai-provider.mjs +54 -48
  17. package/dist/behaviors/ai-tool.d.mts +12 -4
  18. package/dist/behaviors/ai-tool.mjs +311 -263
  19. package/dist/behaviors/ai-tools.d.mts +9 -6
  20. package/dist/behaviors/ai-tools.mjs +259 -155
  21. package/dist/behaviors/back-top.mjs +16 -1
  22. package/dist/behaviors/carousel.d.mts +5 -2
  23. package/dist/behaviors/carousel.mjs +53 -26
  24. package/dist/behaviors/charts.d.mts +68 -22
  25. package/dist/behaviors/charts.mjs +221 -150
  26. package/dist/behaviors/command-palette.d.mts +40 -6
  27. package/dist/behaviors/command-palette.mjs +143 -15
  28. package/dist/behaviors/context-menu.d.mts +21 -4
  29. package/dist/behaviors/context-menu.mjs +122 -38
  30. package/dist/behaviors/controls.d.mts +59 -0
  31. package/dist/behaviors/controls.mjs +601 -0
  32. package/dist/behaviors/copy.d.mts +5 -2
  33. package/dist/behaviors/copy.mjs +37 -12
  34. package/dist/behaviors/datatable.d.mts +12 -0
  35. package/dist/behaviors/datatable.mjs +226 -43
  36. package/dist/behaviors/date-picker.d.mts +2 -2
  37. package/dist/behaviors/date-picker.mjs +171 -32
  38. package/dist/behaviors/dropdown.d.mts +6 -2
  39. package/dist/behaviors/dropdown.mjs +164 -25
  40. package/dist/behaviors/events.d.mts +330 -0
  41. package/dist/behaviors/events.mjs +208 -0
  42. package/dist/behaviors/input.mjs +25 -12
  43. package/dist/behaviors/modal.d.mts +24 -5
  44. package/dist/behaviors/modal.mjs +76 -10
  45. package/dist/behaviors/nav.d.mts +1 -1
  46. package/dist/behaviors/nav.mjs +33 -7
  47. package/dist/behaviors/notification.d.mts +20 -4
  48. package/dist/behaviors/notification.mjs +53 -73
  49. package/dist/behaviors/popover.d.mts +39 -3
  50. package/dist/behaviors/popover.mjs +90 -4
  51. package/dist/behaviors/select.d.mts +4 -2
  52. package/dist/behaviors/select.mjs +161 -24
  53. package/dist/behaviors/sidebar.d.mts +5 -2
  54. package/dist/behaviors/sidebar.mjs +43 -13
  55. package/dist/behaviors/slider.d.mts +5 -2
  56. package/dist/behaviors/slider.mjs +54 -43
  57. package/dist/behaviors/split-pane.d.mts +6 -2
  58. package/dist/behaviors/split-pane.mjs +62 -21
  59. package/dist/behaviors/tabs.d.mts +3 -2
  60. package/dist/behaviors/tabs.mjs +65 -14
  61. package/dist/behaviors/tag-input.d.mts +5 -2
  62. package/dist/behaviors/tag-input.mjs +61 -21
  63. package/dist/behaviors/theme.d.mts +13 -2
  64. package/dist/behaviors/theme.mjs +48 -4
  65. package/dist/behaviors/toast.d.mts +30 -8
  66. package/dist/behaviors/toast.mjs +74 -7
  67. package/dist/behaviors/tree.d.mts +5 -2
  68. package/dist/behaviors/tree.mjs +38 -15
  69. package/dist/behaviors/upload.mjs +21 -23
  70. package/dist/cli.mjs +1 -1
  71. package/dist/components/ai-panel.css +1 -1
  72. package/dist/components/ai-tool-call.mjs +1 -1
  73. package/dist/components/badge.css +1 -1
  74. package/dist/components/breadcrumb.css +0 -32
  75. package/dist/components/chart-calendar.mjs +1 -2
  76. package/dist/components/chart-pie.css +0 -6
  77. package/dist/components/charts.css +1 -1
  78. package/dist/components/content.css +3 -16
  79. package/dist/components/datatable.css +1 -1
  80. package/dist/components/date-picker.css +1 -1
  81. package/dist/components/notification.css +2 -3
  82. package/dist/components/pagination.css +3 -1
  83. package/dist/components/pagination.mjs +1 -0
  84. package/dist/components/rating.mjs +1 -0
  85. package/dist/components/segmented.css +9 -1
  86. package/dist/components/segmented.mjs +1 -1
  87. package/dist/components/select.css +67 -4
  88. package/dist/components/steps.css +13 -3
  89. package/dist/components/steps.mjs +1 -0
  90. package/dist/components/switch.css +3 -1
  91. package/dist/components/switch.mjs +1 -0
  92. package/dist/components/table.mjs +1 -0
  93. package/dist/components/tabs.css +23 -0
  94. package/dist/components/tag-input.css +1 -6
  95. package/dist/components/upload.css +0 -15
  96. package/dist/index.d.mts +12 -10
  97. package/dist/index.mjs +2700 -1460
  98. package/dist/registry.json +42 -13
  99. package/dist/retro-effects.css +3 -16
  100. package/dist/tokens/retro-effects.css +3 -16
  101. package/dist/tokens/style-profiles.css +3 -2
  102. package/dist/tokens/typography.css +2 -0
  103. package/dist/tokens.css +5 -2
  104. package/dist/ui.css +138 -98
  105. package/docs/spec/ai-native.md +7 -4
  106. package/package.json +1 -1
@@ -0,0 +1,330 @@
1
+ import { ChartEventDetail } from './charts.js';
2
+ import { AiUsage } from './ai-core.js';
3
+ import { AiDoneEventDetail } from './ai-provider.js';
4
+ import { AiComposerRefSource } from './ai-composer.js';
5
+ import './popover.js';
6
+ import './ai-panel.js';
7
+
8
+ interface IcenGestureDetail {
9
+ /** 手势命中源:最近的 [data-gestures] 宿主 */
10
+ source: HTMLElement;
11
+ /** 原始 DOM 事件(click / contextmenu / pointerdown / mouseup…) */
12
+ originalEvent: Event;
13
+ /** 视口坐标(指针手势) */
14
+ x: number;
15
+ y: number;
16
+ /** 划词文本(仅 icen:text-select) */
17
+ text?: string;
18
+ }
19
+ type IcenPolicy = (e: CustomEvent<IcenGestureDetail>) => void;
20
+ /** 手势层事件名(策略层只认这五种,编译期收死)。 */
21
+ type IcenGestureEvent = 'icen:click' | 'icen:dblclick' | 'icen:contextmenu' | 'icen:longpress' | 'icen:text-select';
22
+ /**
23
+ * 全库事件注册表:事件名 → detail 契约。onIcen 推断、emitIcen 校验,双侧同表。
24
+ * 新增事件必须在此登记(模板签名兜底只留给宿主自定义事件)。
25
+ */
26
+ interface IcenEventMap {
27
+ 'icen:click': IcenGestureDetail;
28
+ 'icen:dblclick': IcenGestureDetail;
29
+ 'icen:contextmenu': IcenGestureDetail;
30
+ 'icen:longpress': IcenGestureDetail;
31
+ 'icen:text-select': IcenGestureDetail;
32
+ 'icen:chart-hover': ChartEventDetail;
33
+ 'icen:chart-click': ChartEventDetail;
34
+ 'icen:chart-dblclick': ChartEventDetail;
35
+ 'icen:chart-contextmenu': ChartEventDetail;
36
+ 'icen:chart-legend-toggle': {
37
+ key: string;
38
+ seriesIndex: number;
39
+ hidden: boolean;
40
+ };
41
+ 'icen:chart-legend-visibility': {
42
+ visible: boolean;
43
+ };
44
+ 'icen:tab-change': {
45
+ tab: HTMLElement | null;
46
+ panel: HTMLElement | null;
47
+ index: number;
48
+ };
49
+ 'icen:tags-change': {
50
+ tags: string[];
51
+ };
52
+ 'icen:otp-complete': {
53
+ code: string;
54
+ };
55
+ 'icen:slider-input': {
56
+ value?: number;
57
+ lo?: number;
58
+ hi?: number;
59
+ };
60
+ 'icen:slider-change': {
61
+ value?: number;
62
+ lo?: number;
63
+ hi?: number;
64
+ };
65
+ 'icen:switch-change': {
66
+ el: HTMLElement;
67
+ checked: boolean;
68
+ };
69
+ 'icen:check-change': {
70
+ el: HTMLElement;
71
+ checked: boolean | 'mixed';
72
+ };
73
+ 'icen:stepper-change': {
74
+ el: HTMLElement;
75
+ value: number;
76
+ delta: number;
77
+ };
78
+ 'icen:segmented-change': {
79
+ el: HTMLElement;
80
+ value: string;
81
+ index: number;
82
+ };
83
+ 'icen:step-change': {
84
+ step: HTMLElement;
85
+ index: number;
86
+ };
87
+ 'icen:rating-change': {
88
+ el: HTMLElement;
89
+ value: number;
90
+ };
91
+ 'icen:page-change': {
92
+ page: number;
93
+ };
94
+ 'icen:sort-change': {
95
+ th: HTMLElement;
96
+ key: string;
97
+ dir: 'asc' | 'desc' | null;
98
+ };
99
+ 'icen:nav-select': {
100
+ item: HTMLElement;
101
+ index: number;
102
+ };
103
+ 'icen:sidebar-toggle': {
104
+ group: HTMLElement;
105
+ open: boolean;
106
+ };
107
+ 'icen:sidebar-collapse': {
108
+ collapsed: boolean;
109
+ };
110
+ 'icen:split-change': {
111
+ value: number;
112
+ };
113
+ 'icen:copy-success': {
114
+ text: string;
115
+ };
116
+ 'icen:copy-error': {
117
+ text: string;
118
+ reason: string;
119
+ };
120
+ 'icen:back-top': Record<string, never>;
121
+ 'icen:modal-open': {
122
+ id: string;
123
+ };
124
+ 'icen:modal-close': {
125
+ id: string;
126
+ };
127
+ 'icen:command-palette-open': Record<string, never>;
128
+ 'icen:command-palette-close': Record<string, never>;
129
+ 'icen:menu-select': {
130
+ source: HTMLElement;
131
+ item: HTMLElement;
132
+ value: string;
133
+ label: string;
134
+ };
135
+ 'icen:select-change': {
136
+ value: string;
137
+ values: string[];
138
+ label: string;
139
+ };
140
+ 'icen:date-change': {
141
+ value: string;
142
+ date: Date | null;
143
+ };
144
+ 'icen:accordion-toggle': {
145
+ item: HTMLElement;
146
+ index: number;
147
+ open: boolean;
148
+ };
149
+ 'icen:tree-select': {
150
+ node: HTMLElement;
151
+ };
152
+ 'icen:tree-toggle': {
153
+ node: HTMLElement;
154
+ open: boolean;
155
+ };
156
+ 'icen:carousel-change': {
157
+ index: number;
158
+ count: number;
159
+ };
160
+ 'icen:theme-change': {
161
+ preset: string;
162
+ dark: boolean;
163
+ style: string;
164
+ };
165
+ 'icen:table-sort-change': {
166
+ key: string;
167
+ dir: 'asc' | 'desc';
168
+ };
169
+ 'icen:table-page-change': {
170
+ page: number;
171
+ pageSize: number;
172
+ };
173
+ 'icen:table-page-size-change': {
174
+ pageSize: number;
175
+ };
176
+ 'icen:table-filter-change': {
177
+ key: string;
178
+ values: string[];
179
+ };
180
+ 'icen:table-search-change': {
181
+ query: string;
182
+ };
183
+ 'icen:table-selection-change': {
184
+ selected: unknown[];
185
+ };
186
+ 'icen:table-row-click': {
187
+ row: unknown;
188
+ index: number;
189
+ };
190
+ 'icen:table-row-toggle': {
191
+ row: unknown;
192
+ index: number;
193
+ expanded: boolean;
194
+ };
195
+ 'icen:table-columns-change': {
196
+ hidden: string[];
197
+ };
198
+ 'icen:table-export': {
199
+ count: number;
200
+ };
201
+ 'icen:upload': {
202
+ files: File[];
203
+ };
204
+ 'icen:upload-error': {
205
+ file: File | null;
206
+ reason: string;
207
+ };
208
+ 'icen:upload-remove': {
209
+ file: File;
210
+ };
211
+ 'icen:ai-toggle': {
212
+ el: HTMLElement;
213
+ open: boolean;
214
+ };
215
+ 'icen:ai-copy': {
216
+ el: HTMLElement;
217
+ };
218
+ 'icen:ai-retry': {
219
+ el: HTMLElement;
220
+ };
221
+ 'icen:ai-approve': {
222
+ id: string;
223
+ kind: string;
224
+ };
225
+ 'icen:ai-reject': {
226
+ id: string;
227
+ kind: string;
228
+ };
229
+ 'icen:ai-diff-accept': {
230
+ path: string;
231
+ };
232
+ 'icen:ai-diff-reject': {
233
+ path: string;
234
+ };
235
+ 'icen:ai-send': {
236
+ text: string;
237
+ };
238
+ 'icen:ai-stop': Record<string, never>;
239
+ 'icen:ai-queue': {
240
+ text: string;
241
+ };
242
+ 'icen:ai-dequeue': {
243
+ index: number;
244
+ };
245
+ 'icen:ai-attach': {
246
+ files: File[];
247
+ };
248
+ 'icen:ai-model-change': {
249
+ provider: string;
250
+ model: string;
251
+ label: string;
252
+ context?: number;
253
+ };
254
+ 'icen:ai-command': {
255
+ name: string;
256
+ args: string;
257
+ };
258
+ 'icen:ai-ref': {
259
+ action: 'add' | 'remove';
260
+ ref: AiComposerRefSource;
261
+ };
262
+ 'icen:ai-todo-toggle': {
263
+ index: number;
264
+ status: string;
265
+ };
266
+ 'icen:ai-audit-clear': Record<string, never>;
267
+ 'icen:ai-context-open': Record<string, never>;
268
+ 'icen:ai-context-close': Record<string, never>;
269
+ 'icen:ai-tool-call': {
270
+ name: string;
271
+ input: unknown;
272
+ el: HTMLElement;
273
+ };
274
+ 'icen:ai-tool-result': {
275
+ name: string;
276
+ ok: boolean;
277
+ el: HTMLElement;
278
+ error?: string;
279
+ count: number;
280
+ };
281
+ 'icen:ai-tool-evict': {
282
+ name: string;
283
+ el: HTMLElement;
284
+ };
285
+ 'icen:ai-usage': AiUsage;
286
+ 'icen:ai-done': AiDoneEventDetail;
287
+ [key: `icen:${string}`]: unknown;
288
+ }
289
+ interface IcenListenOptions {
290
+ /** 只监听该子树内冒泡上来的事件(组件实例级绑定)。
291
+ * 广播事件(自 document 派发)不经过元素子树:within 为 Element 时收不到,要收广播请用全局形态 */
292
+ within?: Element | Document;
293
+ /** AbortSignal:abort 即解绑(React useEffect 清理 / 批量卸载场景) */
294
+ signal?: AbortSignal;
295
+ /** 只触发一次(触发后自动解绑) */
296
+ once?: boolean;
297
+ }
298
+ /**
299
+ * 统一绑定器(全库事件一个入口):
300
+ * onIcen('icen:tab-change', (e) => e.detail.index) 全局
301
+ * onIcen('icen:chart-click', '.chart', (e) => …) 选择器委托
302
+ * onIcen('icen:upload', (e) => …, { within: formEl }) 子树限定
303
+ * onIcen('icen:ai-done', (e) => …, { signal: ctrl.signal }) 广播 + 信号解绑
304
+ * 返回解绑函数 —— React useEffect / Vue onUnmounted 直接 return。
305
+ */
306
+ declare function onIcen<K extends keyof IcenEventMap>(type: K, handler: (e: CustomEvent<IcenEventMap[K]>) => void, opts?: IcenListenOptions): () => void;
307
+ declare function onIcen<K extends keyof IcenEventMap>(type: K, selector: string, handler: (e: CustomEvent<IcenEventMap[K]>) => void, opts?: IcenListenOptions): () => void;
308
+ /**
309
+ * 全库唯一派生口:bubbles + composed + cancelable。返回 false = 被消费方 preventDefault。
310
+ * 组件作者派发领域事件一律走这里(不要裸写 new CustomEvent);detail 契约见 IcenEventMap。
311
+ * 从 document 派发 = 全局广播(传播只经 [document, window],仅 onIcen 全局形态可收;
312
+ * within 为 Element 收不到,要收广播请用全局形态)。
313
+ */
314
+ declare function emitIcen<K extends keyof IcenEventMap>(source: Element | Document, type: K, detail: IcenEventMap[K]): boolean;
315
+ /** 注册/覆盖某手势事件的全站默认行为;传 null 移除(= 禁用该默认)。
316
+ * 只接受手势层五种事件——领域事件的默认行为归组件交互契约,不会被静默忽略。 */
317
+ declare function setEventPolicy(type: IcenGestureEvent, policy: IcenPolicy | null): void;
318
+ /** 读取某手势事件当前注册的策略(与 setEventPolicy 对称的调试/测试读取器;未注册返回 undefined)。 */
319
+ declare function getEventPolicy(type: IcenGestureEvent): IcenPolicy | undefined;
320
+ /** 清空全部策略(测试/热重载用)。 */
321
+ declare function resetEventPolicies(): void;
322
+ /**
323
+ * 启动手势层(全局委托一次,幂等;重复调用安全)。
324
+ * 返回销毁函数:摘除全部 document 级委托并复位幂等标记,销毁后可重新 init。
325
+ */
326
+ declare function initGestures(): () => void;
327
+ /** 摘除手势层委托(等同 initGestures 返回的销毁函数;未启动时为 no-op)。 */
328
+ declare function destroyGestures(): void;
329
+
330
+ export { type IcenEventMap, type IcenGestureDetail, type IcenGestureEvent, type IcenListenOptions, type IcenPolicy, destroyGestures, emitIcen, getEventPolicy, initGestures, onIcen, resetEventPolicies, setEventPolicy };
@@ -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
+ };
@@ -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");
@@ -58,7 +66,6 @@ function setupCounter(field) {
58
66
  const maxAttr = field.getAttribute("maxlength");
59
67
  const dataCount = field.getAttribute("data-count");
60
68
  if (!maxAttr && dataCount === null) return;
61
- el.__icenCounterInit = true;
62
69
  const max = maxAttr ? Number(maxAttr) : 0;
63
70
  let counter = null;
64
71
  const wrap = field.closest(".input-wrap, .textarea-wrap");
@@ -75,6 +82,8 @@ function setupCounter(field) {
75
82
  }
76
83
  const counterEl = counter;
77
84
  if (!counterEl) return;
85
+ el.__icenCounterInit = true;
86
+ const isComposing = trackComposing(field);
78
87
  function update() {
79
88
  const len = field.value.length;
80
89
  counterEl.textContent = max > 0 ? `${len} / ${max}` : `${len}`;
@@ -85,8 +94,9 @@ function setupCounter(field) {
85
94
  else if (ratio >= 0.8) counterEl.classList.add("is-warn");
86
95
  }
87
96
  }
97
+ ;
88
98
  field.addEventListener("input", () => {
89
- if (!isComposing) update();
99
+ if (!isComposing()) update();
90
100
  });
91
101
  update();
92
102
  }
@@ -96,6 +106,7 @@ function setupMask(field) {
96
106
  const mask = field.getAttribute("data-mask");
97
107
  if (!mask) return;
98
108
  el.__icenMaskInit = true;
109
+ const isComposing = trackComposing(field);
99
110
  const maskChars = [];
100
111
  for (const ch of mask) maskChars.push(ch);
101
112
  function isInputSlot(ch) {
@@ -137,7 +148,7 @@ function setupMask(field) {
137
148
  field.value = lastFormatted;
138
149
  field.setAttribute("data-raw-value", extractRaw(lastFormatted));
139
150
  field.addEventListener("input", () => {
140
- if (isComposing) return;
151
+ if (isComposing()) return;
141
152
  const cursorPos = field.selectionStart ?? 0;
142
153
  const oldRaw = extractRaw(lastFormatted);
143
154
  const newFull = field.value;
@@ -165,7 +176,7 @@ function setupMask(field) {
165
176
  field.setSelectionRange(targetCursor, targetCursor);
166
177
  });
167
178
  field.addEventListener("keydown", (ev) => {
168
- if (isComposing) return;
179
+ if (isComposing()) return;
169
180
  if (ev.ctrlKey || ev.metaKey || ev.altKey) return;
170
181
  if (["Backspace", "Delete", "Tab", "Escape", "ArrowLeft", "ArrowRight", "Home", "End", "Enter"].includes(ev.key)) return;
171
182
  const currentRaw = extractRaw(field.value);
@@ -195,6 +206,7 @@ function setupOtp(group) {
195
206
  const el = group;
196
207
  if (el.__icenInputInit) return;
197
208
  el.__icenInputInit = true;
209
+ const isComposing = trackComposing(group);
198
210
  const cells = Array.from(group.querySelectorAll(".otp-cell"));
199
211
  const syncFilled = (cell) => {
200
212
  cell.classList.toggle("is-filled", cell.value.length > 0);
@@ -205,7 +217,7 @@ function setupOtp(group) {
205
217
  }
206
218
  syncFilled(cell);
207
219
  cell.addEventListener("input", () => {
208
- if (isComposing) return;
220
+ if (isComposing()) return;
209
221
  const ch = cell.value.slice(-1);
210
222
  cell.value = ch;
211
223
  syncFilled(cell);
@@ -239,7 +251,8 @@ function setupOtp(group) {
239
251
  const fireComplete = () => {
240
252
  const code = cells.map((c) => c.value).join("");
241
253
  if (code.length === cells.length && cells.every((c) => c.value.length > 0)) {
242
- group.dispatchEvent(new CustomEvent("icen:otp-complete", { detail: { code }, bubbles: true }));
254
+ group.classList.add("is-complete");
255
+ emitIcen(group, "icen:otp-complete", { code });
243
256
  group.dispatchEvent(new CustomEvent("otp:complete", { detail: { code }, bubbles: true }));
244
257
  }
245
258
  };
@@ -1,11 +1,30 @@
1
- /** 打开指定 id 的 modal;已有其他 modal 打开时先关闭它。 */
2
- declare function openModal(id: string): void;
3
- /** 关闭指定 id 的 modal(backdrop 已被外部移除时也照常复位状态/监听/滚动锁)。 */
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
+ */
4
21
  declare function closeModal(id: string): void;
5
22
  /**
6
23
  * 绑定触发器/关闭钮/backdrop 点击(document 级事件委托,重复调用安全)。
7
24
  * root 用于扫描 [data-modal-open] 补 aria-haspopup=dialog。
25
+ * 返回销毁函数:摘除 document 委托并复位幂等标记(销毁后可重新 init);
26
+ * 销毁时若有 modal 开着会顺带关闭(复位滚动锁/监听/焦点)。SSR 下返回 no-op。
8
27
  */
9
- declare function initModal(root?: ParentNode): void;
28
+ declare function initModal(root?: ParentNode): () => void;
10
29
 
11
- export { closeModal, initModal, openModal };
30
+ export { FOCUSABLE, type ModalOpenOptions, closeModal, initModal, openModal };