@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.
- package/AGENTS.md +15 -9
- package/README.md +8 -3
- package/dist/base.css +4 -3
- package/dist/behaviors/accordion.d.mts +5 -2
- package/dist/behaviors/accordion.mjs +36 -11
- package/dist/behaviors/ai-chat.d.mts +3 -1
- package/dist/behaviors/ai-chat.mjs +66 -43
- package/dist/behaviors/ai-composer.d.mts +28 -3
- package/dist/behaviors/ai-composer.mjs +291 -209
- package/dist/behaviors/ai-core.d.mts +9 -4
- package/dist/behaviors/ai-core.mjs +21 -8
- package/dist/behaviors/ai-diff.d.mts +2 -2
- package/dist/behaviors/ai-diff.mjs +172 -41
- package/dist/behaviors/ai-panel.d.mts +11 -8
- package/dist/behaviors/ai-panel.mjs +185 -110
- package/dist/behaviors/ai-provider.mjs +54 -48
- package/dist/behaviors/ai-tool.d.mts +12 -4
- package/dist/behaviors/ai-tool.mjs +311 -263
- package/dist/behaviors/ai-tools.d.mts +9 -6
- package/dist/behaviors/ai-tools.mjs +259 -155
- package/dist/behaviors/back-top.mjs +16 -1
- package/dist/behaviors/carousel.d.mts +5 -2
- package/dist/behaviors/carousel.mjs +53 -26
- package/dist/behaviors/charts.d.mts +68 -22
- package/dist/behaviors/charts.mjs +221 -150
- package/dist/behaviors/command-palette.d.mts +40 -6
- package/dist/behaviors/command-palette.mjs +143 -15
- package/dist/behaviors/context-menu.d.mts +21 -4
- package/dist/behaviors/context-menu.mjs +122 -38
- package/dist/behaviors/controls.d.mts +59 -0
- package/dist/behaviors/controls.mjs +601 -0
- package/dist/behaviors/copy.d.mts +5 -2
- package/dist/behaviors/copy.mjs +37 -12
- package/dist/behaviors/datatable.d.mts +12 -0
- package/dist/behaviors/datatable.mjs +226 -43
- package/dist/behaviors/date-picker.d.mts +2 -2
- package/dist/behaviors/date-picker.mjs +171 -32
- package/dist/behaviors/dropdown.d.mts +6 -2
- package/dist/behaviors/dropdown.mjs +164 -25
- package/dist/behaviors/events.d.mts +330 -0
- package/dist/behaviors/events.mjs +208 -0
- package/dist/behaviors/input.mjs +25 -12
- package/dist/behaviors/modal.d.mts +24 -5
- package/dist/behaviors/modal.mjs +76 -10
- package/dist/behaviors/nav.d.mts +1 -1
- package/dist/behaviors/nav.mjs +33 -7
- package/dist/behaviors/notification.d.mts +20 -4
- package/dist/behaviors/notification.mjs +53 -73
- package/dist/behaviors/popover.d.mts +39 -3
- package/dist/behaviors/popover.mjs +90 -4
- package/dist/behaviors/select.d.mts +4 -2
- package/dist/behaviors/select.mjs +161 -24
- package/dist/behaviors/sidebar.d.mts +5 -2
- package/dist/behaviors/sidebar.mjs +43 -13
- package/dist/behaviors/slider.d.mts +5 -2
- package/dist/behaviors/slider.mjs +54 -43
- package/dist/behaviors/split-pane.d.mts +6 -2
- package/dist/behaviors/split-pane.mjs +62 -21
- package/dist/behaviors/tabs.d.mts +3 -2
- package/dist/behaviors/tabs.mjs +65 -14
- package/dist/behaviors/tag-input.d.mts +5 -2
- package/dist/behaviors/tag-input.mjs +61 -21
- package/dist/behaviors/theme.d.mts +13 -2
- package/dist/behaviors/theme.mjs +48 -4
- package/dist/behaviors/toast.d.mts +30 -8
- package/dist/behaviors/toast.mjs +74 -7
- package/dist/behaviors/tree.d.mts +5 -2
- package/dist/behaviors/tree.mjs +38 -15
- package/dist/behaviors/upload.mjs +21 -23
- package/dist/cli.mjs +1 -1
- package/dist/components/ai-panel.css +1 -1
- package/dist/components/ai-tool-call.mjs +1 -1
- package/dist/components/badge.css +1 -1
- package/dist/components/breadcrumb.css +0 -32
- package/dist/components/chart-calendar.mjs +1 -2
- package/dist/components/chart-pie.css +0 -6
- package/dist/components/charts.css +1 -1
- package/dist/components/content.css +3 -16
- package/dist/components/datatable.css +1 -1
- package/dist/components/date-picker.css +1 -1
- package/dist/components/notification.css +2 -3
- package/dist/components/pagination.css +3 -1
- package/dist/components/pagination.mjs +1 -0
- package/dist/components/rating.mjs +1 -0
- package/dist/components/segmented.css +9 -1
- package/dist/components/segmented.mjs +1 -1
- package/dist/components/select.css +67 -4
- package/dist/components/steps.css +13 -3
- package/dist/components/steps.mjs +1 -0
- package/dist/components/switch.css +3 -1
- package/dist/components/switch.mjs +1 -0
- package/dist/components/table.mjs +1 -0
- package/dist/components/tabs.css +23 -0
- package/dist/components/tag-input.css +1 -6
- package/dist/components/upload.css +0 -15
- package/dist/index.d.mts +12 -10
- package/dist/index.mjs +2700 -1460
- package/dist/registry.json +42 -13
- package/dist/retro-effects.css +3 -16
- package/dist/tokens/retro-effects.css +3 -16
- package/dist/tokens/style-profiles.css +3 -2
- package/dist/tokens/typography.css +2 -0
- package/dist/tokens.css +5 -2
- package/dist/ui.css +138 -98
- package/docs/spec/ai-native.md +7 -4
- 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
|
+
};
|
package/dist/behaviors/input.mjs
CHANGED
|
@@ -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
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
9
|
+
function trackComposing(target) {
|
|
10
|
+
let composing = false;
|
|
11
|
+
target.addEventListener("compositionstart", () => {
|
|
12
|
+
composing = true;
|
|
6
13
|
});
|
|
7
|
-
|
|
8
|
-
|
|
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.
|
|
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
|
-
/**
|
|
2
|
-
declare
|
|
3
|
-
/**
|
|
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 };
|