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