@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.
- package/AGENTS.md +262 -0
- package/README.md +52 -13
- package/dist/base.css +33 -5
- package/dist/behaviors/accordion.d.mts +7 -0
- package/dist/behaviors/accordion.mjs +37 -11
- package/dist/behaviors/ai-chat.d.mts +59 -0
- package/dist/behaviors/ai-chat.mjs +580 -0
- package/dist/behaviors/ai-composer.d.mts +137 -0
- package/dist/behaviors/ai-composer.mjs +2114 -0
- package/dist/behaviors/ai-core.d.mts +156 -0
- package/dist/behaviors/ai-core.mjs +504 -0
- package/dist/behaviors/ai-diff.d.mts +53 -0
- package/dist/behaviors/ai-diff.mjs +413 -0
- package/dist/behaviors/ai-panel.d.mts +101 -0
- package/dist/behaviors/ai-panel.mjs +1087 -0
- package/dist/behaviors/ai-provider.d.mts +198 -0
- package/dist/behaviors/ai-provider.mjs +1061 -0
- package/dist/behaviors/ai-tool.d.mts +26 -0
- package/dist/behaviors/ai-tool.mjs +2173 -0
- package/dist/behaviors/ai-tools.d.mts +84 -0
- package/dist/behaviors/ai-tools.mjs +1762 -0
- package/dist/behaviors/back-top.d.mts +20 -0
- package/dist/behaviors/back-top.mjs +64 -32
- package/dist/behaviors/carousel.d.mts +7 -0
- package/dist/behaviors/carousel.mjs +72 -27
- package/dist/behaviors/charts.d.mts +296 -0
- package/dist/behaviors/charts.mjs +1364 -440
- package/dist/behaviors/command-palette.d.mts +42 -0
- package/dist/behaviors/command-palette.mjs +167 -27
- package/dist/behaviors/context-menu.d.mts +53 -0
- package/dist/behaviors/context-menu.mjs +127 -39
- package/dist/behaviors/controls.d.mts +59 -0
- package/dist/behaviors/controls.mjs +601 -0
- package/dist/behaviors/copy.d.mts +7 -0
- package/dist/behaviors/copy.mjs +41 -15
- package/dist/behaviors/datatable.d.mts +90 -0
- package/dist/behaviors/datatable.mjs +279 -53
- package/dist/behaviors/date-picker.d.mts +4 -0
- package/dist/behaviors/date-picker.mjs +276 -69
- package/dist/behaviors/dropdown.d.mts +8 -0
- package/dist/behaviors/dropdown.mjs +171 -28
- package/dist/behaviors/events.d.mts +330 -0
- package/dist/behaviors/events.mjs +208 -0
- package/dist/behaviors/input.d.mts +4 -0
- package/dist/behaviors/input.mjs +52 -32
- package/dist/behaviors/modal.d.mts +30 -0
- package/dist/behaviors/modal.mjs +87 -16
- package/dist/behaviors/nav.d.mts +7 -0
- package/dist/behaviors/nav.mjs +57 -15
- package/dist/behaviors/notification.d.mts +188 -0
- package/dist/behaviors/notification.mjs +274 -140
- package/dist/behaviors/popover.d.mts +82 -0
- package/dist/behaviors/popover.mjs +92 -5
- package/dist/behaviors/select.d.mts +6 -0
- package/dist/behaviors/select.mjs +287 -45
- package/dist/behaviors/sidebar.d.mts +7 -0
- package/dist/behaviors/sidebar.mjs +44 -13
- package/dist/behaviors/slider.d.mts +7 -0
- package/dist/behaviors/slider.mjs +55 -43
- package/dist/behaviors/split-pane.d.mts +8 -0
- package/dist/behaviors/split-pane.mjs +70 -29
- package/dist/behaviors/tabs.d.mts +9 -0
- package/dist/behaviors/tabs.mjs +102 -26
- package/dist/behaviors/tag-input.d.mts +7 -0
- package/dist/behaviors/tag-input.mjs +66 -17
- package/dist/behaviors/theme.d.mts +36 -0
- package/dist/behaviors/theme.mjs +51 -5
- package/dist/behaviors/toast.d.mts +48 -0
- package/dist/behaviors/toast.mjs +98 -25
- package/dist/behaviors/tree.d.mts +7 -0
- package/dist/behaviors/tree.mjs +39 -15
- package/dist/behaviors/upload.d.mts +4 -0
- package/dist/behaviors/upload.mjs +79 -44
- package/dist/cli.mjs +1 -1
- package/dist/components/ai-chat.css +1037 -0
- package/dist/components/ai-chat.mjs +3 -0
- package/dist/components/ai-composer.mjs +3 -0
- package/dist/components/ai-context.mjs +2 -0
- package/dist/components/ai-diff.css +311 -0
- package/dist/components/ai-diff.mjs +3 -0
- package/dist/components/ai-files.mjs +3 -0
- package/dist/components/ai-message.mjs +3 -0
- package/dist/components/ai-panel.css +682 -0
- package/dist/components/ai-reasoning.mjs +3 -0
- package/dist/components/ai-subagent.mjs +2 -0
- package/dist/components/ai-todo.mjs +2 -0
- package/dist/components/ai-tool-call.mjs +2 -0
- package/dist/components/ai-tool.css +532 -0
- package/dist/components/ai-usage.mjs +3 -0
- package/dist/components/badge.css +5 -9
- package/dist/components/breadcrumb.css +0 -36
- package/dist/components/btn.css +4 -7
- package/dist/components/card.css +3 -6
- package/dist/components/chart-bar.css +10 -0
- package/dist/components/chart-calendar.mjs +4 -0
- package/dist/components/chart-gauge.css +1 -1
- package/dist/components/chart-heatmap.css +40 -0
- package/dist/components/chart-pie.css +0 -6
- package/dist/components/chart-scatter.css +24 -0
- package/dist/components/chart-scatter.mjs +4 -0
- package/dist/components/charts.css +87 -2
- package/dist/components/charts.mjs +1 -0
- package/dist/components/command-palette.css +2 -2
- package/dist/components/content.css +11 -25
- package/dist/components/copy.css +8 -8
- package/dist/components/datatable.css +5 -9
- package/dist/components/date-picker.css +4 -8
- package/dist/components/empty.css +0 -4
- package/dist/components/feedback.css +10 -17
- package/dist/components/form.css +13 -14
- package/dist/components/input.css +2 -2
- package/dist/components/layout.css +36 -36
- package/dist/components/media.css +12 -8
- package/dist/components/menu.css +2 -2
- package/dist/components/modal.css +1 -1
- package/dist/components/nav.css +11 -6
- package/dist/components/notification.css +27 -28
- package/dist/components/pagination.css +4 -2
- package/dist/components/pagination.mjs +1 -0
- package/dist/components/panel.css +16 -16
- package/dist/components/pill.css +15 -15
- package/dist/components/rating.mjs +1 -0
- package/dist/components/segmented.css +14 -6
- package/dist/components/segmented.mjs +1 -0
- package/dist/components/select.css +79 -24
- package/dist/components/sidebar.css +1 -1
- package/dist/components/split-pane.css +0 -1
- package/dist/components/stat.css +14 -18
- package/dist/components/steps.css +15 -5
- package/dist/components/steps.mjs +1 -0
- package/dist/components/switch.css +11 -13
- package/dist/components/switch.mjs +1 -0
- package/dist/components/table.css +46 -47
- package/dist/components/table.mjs +1 -0
- package/dist/components/tabs.css +57 -45
- package/dist/components/tag-input.css +5 -10
- package/dist/components/toast.css +16 -13
- package/dist/components/tooltip.css +2 -2
- package/dist/components/upload.css +0 -15
- package/dist/index.d.mts +39 -0
- package/dist/index.mjs +11243 -3312
- package/dist/registry.json +147 -9
- package/dist/retro-effects.css +23 -32
- package/dist/tokens/colors.css +4 -1
- package/dist/tokens/retro-effects.css +23 -32
- package/dist/tokens/style-profiles.css +24 -2
- package/dist/tokens/typography.css +2 -0
- package/dist/tokens.css +30 -3
- package/dist/ui.css +3420 -645
- package/docs/spec/ai-native.md +662 -0
- 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
|
+
};
|
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");
|
|
@@ -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
|
|
261
|
+
function initInput(root) {
|
|
243
262
|
if (typeof document === "undefined") return;
|
|
263
|
+
const scope = root ?? document;
|
|
244
264
|
const wraps = [];
|
|
245
|
-
if (
|
|
246
|
-
wraps.push(...Array.from(
|
|
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 (
|
|
256
|
-
textareas.push(...Array.from(
|
|
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 (
|
|
260
|
-
const fields = Array.from(
|
|
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 (
|
|
265
|
-
if (
|
|
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 (
|
|
271
|
-
maskedInputs.push(...Array.from(
|
|
272
|
-
if (
|
|
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 (
|
|
277
|
-
trimFields.push(...Array.from(
|
|
278
|
-
if ((
|
|
279
|
-
trimFields.push(
|
|
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 (
|
|
285
|
-
otps.push(...Array.from(
|
|
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 };
|
package/dist/behaviors/modal.mjs
CHANGED
|
@@ -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)
|
|
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-${
|
|
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-${
|
|
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
|
-
|
|
75
|
-
(
|
|
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 (
|
|
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
|
|
93
|
-
if (typeof document === "undefined") return
|
|
94
|
-
|
|
95
|
-
|
|
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
|
-
|
|
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
|