@icen.ai/ui 0.4.0 → 0.5.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.
Files changed (55) hide show
  1. package/dist/behaviors/back-top.mjs +84 -0
  2. package/dist/behaviors/command-palette.mjs +177 -0
  3. package/dist/behaviors/date-picker.mjs +228 -0
  4. package/dist/behaviors/input.mjs +170 -1
  5. package/dist/behaviors/notification.mjs +754 -0
  6. package/dist/behaviors/select.mjs +151 -22
  7. package/dist/behaviors/split-pane.mjs +102 -0
  8. package/dist/behaviors/upload.mjs +165 -14
  9. package/dist/components/back-top.css +88 -0
  10. package/dist/components/back-top.mjs +3 -0
  11. package/dist/components/badge.css +131 -0
  12. package/dist/components/badge.mjs +2 -0
  13. package/dist/components/breadcrumb.css +57 -0
  14. package/dist/components/card.css +68 -0
  15. package/dist/components/command-palette.css +246 -0
  16. package/dist/components/command-palette.mjs +3 -0
  17. package/dist/components/content.css +214 -0
  18. package/dist/components/copy.css +93 -0
  19. package/dist/components/copy.mjs +3 -0
  20. package/dist/components/date-picker.css +244 -0
  21. package/dist/components/date-picker.mjs +3 -0
  22. package/dist/components/empty.css +73 -3
  23. package/dist/components/feedback.css +239 -1
  24. package/dist/components/form.css +119 -0
  25. package/dist/components/form.mjs +2 -0
  26. package/dist/components/input.css +24 -1
  27. package/dist/components/media.css +144 -1
  28. package/dist/components/nav.css +99 -0
  29. package/dist/components/notification.css +387 -0
  30. package/dist/components/notification.mjs +3 -0
  31. package/dist/components/panel.css +100 -5
  32. package/dist/components/pill.css +70 -0
  33. package/dist/components/scroll-area.css +112 -0
  34. package/dist/components/scroll-area.mjs +2 -0
  35. package/dist/components/segmented.css +75 -0
  36. package/dist/components/select.css +97 -2
  37. package/dist/components/sidebar.css +118 -0
  38. package/dist/components/slider.css +113 -0
  39. package/dist/components/split-pane.css +112 -0
  40. package/dist/components/split-pane.mjs +3 -0
  41. package/dist/components/stat.css +129 -6
  42. package/dist/components/steps.css +97 -0
  43. package/dist/components/switch.css +142 -0
  44. package/dist/components/table.css +86 -3
  45. package/dist/components/tabs.css +122 -1
  46. package/dist/components/tag.css +164 -0
  47. package/dist/components/tag.mjs +2 -0
  48. package/dist/components/toast.css +114 -0
  49. package/dist/components/toolbar.css +101 -0
  50. package/dist/components/toolbar.mjs +2 -0
  51. package/dist/components/upload.css +61 -4
  52. package/dist/index.mjs +1858 -69
  53. package/dist/registry.json +64 -1
  54. package/dist/ui.css +4320 -278
  55. package/package.json +1 -1
@@ -0,0 +1,84 @@
1
+ // src/behaviors/back-top.ts
2
+ var AUTO_BTN_SVG = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m18 15-6-6-6 6"/></svg>';
3
+ function prefersReducedMotion() {
4
+ if (typeof window === "undefined" || typeof window.matchMedia !== "function") return false;
5
+ return window.matchMedia("(prefers-reduced-motion: reduce)").matches;
6
+ }
7
+ function getScrollTarget(target) {
8
+ if (!target) return { el: null, isWindow: true };
9
+ const el = document.querySelector(target);
10
+ return { el, isWindow: false };
11
+ }
12
+ function getScrollY(target) {
13
+ if (!target) return window.scrollY;
14
+ const el = document.querySelector(target);
15
+ return el ? el.scrollTop : window.scrollY;
16
+ }
17
+ function scrollToTop(target, offset = 0) {
18
+ const reduce = prefersReducedMotion();
19
+ const behavior = reduce ? "auto" : "smooth";
20
+ if (!target) {
21
+ window.scrollTo({ top: offset, behavior });
22
+ return;
23
+ }
24
+ const el = document.querySelector(target);
25
+ if (el) el.scrollTo({ top: offset, behavior });
26
+ }
27
+ function setup(btn, opts = {}) {
28
+ const el = btn;
29
+ if (el.__icenBackTopInit) return;
30
+ el.__icenBackTopInit = true;
31
+ const threshold = Number(btn.dataset.backTopThreshold ?? opts.threshold ?? 320) || 320;
32
+ const target = btn.dataset.backTopTarget ?? opts.target;
33
+ const offset = Number(btn.dataset.backTopOffset ?? opts.offset ?? 0) || 0;
34
+ let ticking = false;
35
+ const apply = () => {
36
+ ticking = false;
37
+ const y = getScrollY(target);
38
+ btn.classList.toggle("is-shown", y > threshold);
39
+ };
40
+ const onScroll = () => {
41
+ if (ticking) return;
42
+ ticking = true;
43
+ window.requestAnimationFrame(apply);
44
+ };
45
+ const { el: targetEl, isWindow } = getScrollTarget(target);
46
+ if (isWindow) {
47
+ window.addEventListener("scroll", onScroll, { passive: true });
48
+ } else if (targetEl) {
49
+ targetEl.addEventListener("scroll", onScroll, { passive: true });
50
+ }
51
+ window.addEventListener("resize", onScroll, { passive: true });
52
+ btn.addEventListener("click", () => scrollToTop(target, offset));
53
+ apply();
54
+ }
55
+ function initBackTop(opts = {}) {
56
+ if (typeof document === "undefined") return;
57
+ if (typeof window === "undefined") return;
58
+ const root = opts.root ?? document;
59
+ if (opts.autoCreate) {
60
+ let btn = document.body.querySelector(".back-top[data-back-top-auto]");
61
+ if (!btn) {
62
+ btn = document.createElement("button");
63
+ btn.type = "button";
64
+ btn.className = "back-top";
65
+ btn.setAttribute("data-back-top", "");
66
+ btn.setAttribute("data-back-top-auto", "");
67
+ btn.setAttribute("aria-label", "\u56DE\u5230\u9876\u90E8");
68
+ btn.innerHTML = AUTO_BTN_SVG;
69
+ document.body.appendChild(btn);
70
+ }
71
+ setup(btn, opts);
72
+ }
73
+ const candidates = [];
74
+ if (root instanceof Element && root.matches(".back-top[data-back-top]")) candidates.push(root);
75
+ candidates.push(...Array.from(root.querySelectorAll(".back-top[data-back-top]")));
76
+ for (const c of candidates) {
77
+ const el = c;
78
+ if (el.hasAttribute("data-back-top-auto") && opts.autoCreate) continue;
79
+ setup(el, opts);
80
+ }
81
+ }
82
+ export {
83
+ initBackTop
84
+ };
@@ -0,0 +1,177 @@
1
+ // src/behaviors/command-palette.ts
2
+ var FOCUSABLE = 'button:not([disabled]), [href], input:not([disabled]), [tabindex]:not([tabindex="-1"])';
3
+ var openId = null;
4
+ var restoreFocusTo = null;
5
+ var prevBodyOverflow = "";
6
+ var keyHandler = null;
7
+ var globalKeyHandler = null;
8
+ var initialized = false;
9
+ function findBackdrop(id) {
10
+ return document.querySelector(
11
+ `.command-palette-backdrop[data-command-palette="${CSS.escape(id)}"]`
12
+ );
13
+ }
14
+ function getVisibleItems(panel) {
15
+ return Array.from(
16
+ panel.querySelectorAll(".command-palette-item:not([hidden]):not([disabled])")
17
+ ).filter((el) => el.offsetParent !== null || el.getClientRects().length > 0);
18
+ }
19
+ function isDisabledItem(el) {
20
+ return el.hasAttribute("disabled") || el.getAttribute("aria-disabled") === "true";
21
+ }
22
+ function setActive(panel, idx) {
23
+ const items = getVisibleItems(panel);
24
+ items.forEach((el, i) => el.classList.toggle("is-active", i === idx));
25
+ const active = items[idx];
26
+ if (active && typeof active.scrollIntoView === "function") {
27
+ active.scrollIntoView({ block: "nearest" });
28
+ }
29
+ }
30
+ function filterPanel(panel, query) {
31
+ const q = query.trim().toLowerCase();
32
+ let visibleCount = 0;
33
+ panel.querySelectorAll(".command-palette-item").forEach((item) => {
34
+ if (isDisabledItem(item)) return;
35
+ const label = (item.querySelector(".command-palette-item-label")?.textContent ?? "").toLowerCase();
36
+ const hint = (item.querySelector(".command-palette-item-hint")?.textContent ?? "").toLowerCase();
37
+ const keyword = (item.dataset.commandPaletteKeyword ?? "").toLowerCase();
38
+ const match = !q || label.includes(q) || hint.includes(q) || keyword.includes(q);
39
+ item.hidden = !match;
40
+ item.classList.remove("is-active");
41
+ if (match) visibleCount++;
42
+ });
43
+ panel.querySelectorAll(".command-palette-group").forEach((group) => {
44
+ const hasVisible = Array.from(group.querySelectorAll(".command-palette-item")).some(
45
+ (it) => !it.hidden
46
+ );
47
+ group.hidden = !hasVisible;
48
+ });
49
+ const empty = panel.querySelector(".command-palette-empty");
50
+ if (empty) empty.hidden = visibleCount !== 0;
51
+ if (visibleCount > 0) setActive(panel, 0);
52
+ }
53
+ function openCommandPalette(id) {
54
+ if (typeof document === "undefined") return;
55
+ if (openId === id) return;
56
+ if (openId) closeCommandPalette(openId);
57
+ const backdrop = findBackdrop(id);
58
+ if (!backdrop) return;
59
+ const panel = backdrop.querySelector(".command-palette");
60
+ if (!panel) return;
61
+ if (backdrop.parentElement !== document.body) document.body.appendChild(backdrop);
62
+ restoreFocusTo = document.activeElement instanceof HTMLElement ? document.activeElement : null;
63
+ openId = id;
64
+ backdrop.hidden = false;
65
+ prevBodyOverflow = document.body.style.overflow;
66
+ document.body.style.overflow = "hidden";
67
+ const input = panel.querySelector(".command-palette-input");
68
+ if (input) {
69
+ input.value = "";
70
+ input.focus();
71
+ } else {
72
+ panel.focus();
73
+ }
74
+ filterPanel(panel, "");
75
+ keyHandler = (e) => {
76
+ if (e.key === "Escape") {
77
+ e.preventDefault();
78
+ closeCommandPalette(id);
79
+ return;
80
+ }
81
+ if (e.key === "ArrowDown" || e.key === "ArrowUp") {
82
+ e.preventDefault();
83
+ const items = getVisibleItems(panel);
84
+ if (items.length === 0) return;
85
+ const cur = items.findIndex((el) => el.classList.contains("is-active"));
86
+ const next = e.key === "ArrowDown" ? (cur + 1) % items.length : (cur - 1 + items.length) % items.length;
87
+ setActive(panel, next);
88
+ return;
89
+ }
90
+ if (e.key === "Enter") {
91
+ const active = panel.querySelector(".command-palette-item.is-active");
92
+ if (active && !isDisabledItem(active)) {
93
+ e.preventDefault();
94
+ active.click();
95
+ }
96
+ return;
97
+ }
98
+ if (e.key === "Tab") {
99
+ e.preventDefault();
100
+ const els = Array.from(panel.querySelectorAll(FOCUSABLE)).filter(
101
+ (el) => !el.hidden && el.offsetParent !== null
102
+ );
103
+ if (els.length === 0) return;
104
+ const cur = els.findIndex((el) => el === document.activeElement);
105
+ const next = e.shiftKey ? (cur - 1 + els.length) % els.length : (cur + 1) % els.length;
106
+ els[next]?.focus();
107
+ }
108
+ };
109
+ window.addEventListener("keydown", keyHandler);
110
+ }
111
+ function closeCommandPalette(id) {
112
+ if (typeof document === "undefined") return;
113
+ const backdrop = findBackdrop(id);
114
+ if (!backdrop) return;
115
+ backdrop.hidden = true;
116
+ if (openId !== id) return;
117
+ openId = null;
118
+ if (keyHandler) {
119
+ window.removeEventListener("keydown", keyHandler);
120
+ keyHandler = null;
121
+ }
122
+ document.body.style.overflow = prevBodyOverflow;
123
+ if (restoreFocusTo && restoreFocusTo.isConnected) restoreFocusTo.focus();
124
+ restoreFocusTo = null;
125
+ }
126
+ function initCommandPalette(root = document) {
127
+ if (typeof document === "undefined") return;
128
+ if (typeof window === "undefined") return;
129
+ root.querySelectorAll("[data-command-palette-open]").forEach((el) => el.setAttribute("aria-haspopup", "dialog"));
130
+ if (initialized) return;
131
+ initialized = true;
132
+ document.addEventListener("click", (e) => {
133
+ const target = e.target instanceof Element ? e.target : null;
134
+ if (!target) return;
135
+ const opener = target.closest("[data-command-palette-open]");
136
+ if (opener?.dataset.commandPaletteOpen) {
137
+ openCommandPalette(opener.dataset.commandPaletteOpen);
138
+ return;
139
+ }
140
+ const item = target.closest(".command-palette-item");
141
+ if (item) {
142
+ const backdrop = item.closest("[data-command-palette]");
143
+ if (backdrop?.dataset.commandPalette) {
144
+ if (!item.hasAttribute("data-command-palette-keep-open")) {
145
+ closeCommandPalette(backdrop.dataset.commandPalette);
146
+ }
147
+ }
148
+ return;
149
+ }
150
+ if (target instanceof HTMLElement && target.classList.contains("command-palette-backdrop") && target.dataset.commandPalette) {
151
+ closeCommandPalette(target.dataset.commandPalette);
152
+ }
153
+ });
154
+ document.addEventListener("input", (e) => {
155
+ if (!(e.target instanceof HTMLElement)) return;
156
+ if (!e.target.matches(".command-palette-input")) return;
157
+ const backdrop = e.target.closest("[data-command-palette]");
158
+ const panel = backdrop?.querySelector(".command-palette");
159
+ if (panel) filterPanel(panel, e.target.value);
160
+ });
161
+ globalKeyHandler = (e) => {
162
+ if ((e.metaKey || e.ctrlKey) && (e.key === "k" || e.key === "K")) {
163
+ e.preventDefault();
164
+ const first = document.querySelector(".command-palette-backdrop[data-command-palette]");
165
+ if (first?.dataset.commandPalette) {
166
+ if (openId === first.dataset.commandPalette) closeCommandPalette(openId);
167
+ else openCommandPalette(first.dataset.commandPalette);
168
+ }
169
+ }
170
+ };
171
+ window.addEventListener("keydown", globalKeyHandler);
172
+ }
173
+ export {
174
+ closeCommandPalette,
175
+ initCommandPalette,
176
+ openCommandPalette
177
+ };
@@ -0,0 +1,228 @@
1
+ // src/behaviors/date-picker.ts
2
+ var openPicker = null;
3
+ var panelEl = null;
4
+ var outsideHandler = null;
5
+ var escHandler = null;
6
+ function pad(n) {
7
+ return n < 10 ? "0" + n : String(n);
8
+ }
9
+ function toDateStr(d) {
10
+ return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
11
+ }
12
+ function parseDateStr(s) {
13
+ const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(s.trim());
14
+ if (!m) return null;
15
+ const d = new Date(Number(m[1]), Number(m[2]) - 1, Number(m[3]));
16
+ return Number.isNaN(d.getTime()) ? null : d;
17
+ }
18
+ function format(d, fmt) {
19
+ if (!d) return "";
20
+ 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()));
21
+ }
22
+ function ensurePanel() {
23
+ if (panelEl && panelEl.isConnected) return panelEl;
24
+ const el = document.createElement("div");
25
+ el.className = "date-picker-panel";
26
+ el.setAttribute("role", "dialog");
27
+ el.setAttribute("aria-label", "\u9009\u62E9\u65E5\u671F");
28
+ el.hidden = true;
29
+ document.body.appendChild(el);
30
+ panelEl = el;
31
+ return el;
32
+ }
33
+ function positionPanel(panel, trigger) {
34
+ const r = trigger.getBoundingClientRect();
35
+ const pw = 280;
36
+ const ph = panel.offsetHeight || 320;
37
+ let left = r.left;
38
+ let top = r.bottom + 4;
39
+ if (left + pw > window.innerWidth - 12) left = window.innerWidth - pw - 12;
40
+ if (left < 12) left = 12;
41
+ if (top + ph > window.innerHeight - 12) top = r.top - ph - 4;
42
+ if (top < 12) top = 12;
43
+ panel.style.left = left + "px";
44
+ panel.style.top = top + "px";
45
+ }
46
+ var CAL_ICON = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="4" width="18" height="18" rx="2" ry="2"/><line x1="16" x2="16" y1="2" y2="6"/><line x1="8" x2="8" y1="2" y2="6"/><line x1="3" x2="21" y1="10" y2="10"/></svg>';
47
+ var WEEK_LABELS_ZH = ["\u4E00", "\u4E8C", "\u4E09", "\u56DB", "\u4E94", "\u516D", "\u65E5"];
48
+ var WEEK_LABELS_SUN = ["\u65E5", "\u4E00", "\u4E8C", "\u4E09", "\u56DB", "\u4E94", "\u516D"];
49
+ function renderCalendar(panel, ctx) {
50
+ const { view, selected, min, max, weekStart, onPick } = ctx;
51
+ const year = view.getFullYear();
52
+ const month = view.getMonth();
53
+ const labels = weekStart === 0 ? WEEK_LABELS_SUN : WEEK_LABELS_ZH;
54
+ const firstDay = new Date(year, month, 1);
55
+ const offsetRaw = firstDay.getDay();
56
+ const leadDays = weekStart === 0 ? offsetRaw : (offsetRaw + 6) % 7;
57
+ const gridStart = new Date(year, month, 1 - leadDays);
58
+ const today = /* @__PURE__ */ new Date();
59
+ today.setHours(0, 0, 0, 0);
60
+ const cells = [];
61
+ for (let i = 0; i < 42; i++) {
62
+ cells.push(new Date(gridStart.getFullYear(), gridStart.getMonth(), gridStart.getDate() + i));
63
+ }
64
+ let html = `<div class="date-picker-head">
65
+ <button class="date-picker-nav" data-dp-nav="-1" type="button" aria-label="\u4E0A\u4E00\u6708">\u2039</button>
66
+ <span class="date-picker-title">${year} \u5E74 ${month + 1} \u6708</span>
67
+ <button class="date-picker-nav" data-dp-nav="1" type="button" aria-label="\u4E0B\u4E00\u6708">\u203A</button>
68
+ </div><div class="date-picker-weekdays">${labels.map((l) => `<span>${l}</span>`).join("")}</div><div class="date-picker-grid">`;
69
+ for (const d of cells) {
70
+ const isOutside = d.getMonth() !== month;
71
+ const ds = toDateStr(d);
72
+ const isToday = d.getTime() === today.getTime();
73
+ const isSelected = selected && ds === toDateStr(selected);
74
+ const disabled = min && d < min || max && d > max;
75
+ const classes = ["date-picker-cell"];
76
+ if (isOutside) classes.push("is-outside");
77
+ if (isToday) classes.push("is-today");
78
+ if (isSelected) classes.push("is-selected");
79
+ if (disabled) classes.push("is-disabled");
80
+ html += `<button type="button" class="${classes.join(" ")}" data-dp-date="${ds}"${disabled ? " disabled" : ""}>${d.getDate()}</button>`;
81
+ }
82
+ html += `</div><div class="date-picker-foot">
83
+ <button type="button" class="date-picker-today" data-dp-today>\u4ECA\u5929</button>
84
+ <button type="button" class="date-picker-clear" data-dp-clear>\u6E05\u9664</button>
85
+ </div>`;
86
+ panel.innerHTML = html;
87
+ panel.querySelector('[data-dp-nav="-1"]')?.addEventListener("click", () => {
88
+ ctx.view = new Date(year, month - 1, 15);
89
+ renderCalendar(panel, { ...ctx, view: ctx.view });
90
+ });
91
+ panel.querySelector('[data-dp-nav="1"]')?.addEventListener("click", () => {
92
+ ctx.view = new Date(year, month + 1, 15);
93
+ renderCalendar(panel, { ...ctx, view: ctx.view });
94
+ });
95
+ panel.querySelectorAll("[data-dp-date]").forEach((btn) => {
96
+ btn.addEventListener("click", () => {
97
+ const ds = btn.dataset.dpDate;
98
+ if (!ds) return;
99
+ const d = parseDateStr(ds);
100
+ if (d) onPick(d);
101
+ });
102
+ });
103
+ panel.querySelector("[data-dp-today]")?.addEventListener("click", () => {
104
+ onPick(new Date(today.getFullYear(), today.getMonth(), today.getDate()));
105
+ });
106
+ panel.querySelector("[data-dp-clear]")?.addEventListener("click", () => {
107
+ onPick(null);
108
+ });
109
+ }
110
+ function closePanel() {
111
+ if (!panelEl) return;
112
+ panelEl.hidden = true;
113
+ openPicker?.querySelector(".date-picker-trigger")?.classList.remove("is-open");
114
+ openPicker = null;
115
+ if (outsideHandler) {
116
+ document.removeEventListener("mousedown", outsideHandler);
117
+ outsideHandler = null;
118
+ }
119
+ if (escHandler) {
120
+ window.removeEventListener("keydown", escHandler);
121
+ escHandler = null;
122
+ }
123
+ }
124
+ function setupPicker(container) {
125
+ const el = container;
126
+ if (el.__icenDatePickerInit) return;
127
+ el.__icenDatePickerInit = true;
128
+ const fmt = el.dataset.datePickerFormat ?? "yyyy-mm-dd";
129
+ const minStr = el.dataset.datePickerMin;
130
+ const maxStr = el.dataset.datePickerMax;
131
+ const weekStart = Number(el.dataset.datePickerWeekStart ?? "1");
132
+ const placeholder = el.dataset.datePickerPlaceholder ?? "\u9009\u62E9\u65E5\u671F";
133
+ const min = minStr ? parseDateStr(minStr) : null;
134
+ const max = maxStr ? parseDateStr(maxStr) : null;
135
+ const hidden = el.querySelector('input[type="hidden"]');
136
+ const initialStr = hidden?.value ?? "";
137
+ const initialDate = initialStr ? parseDateStr(initialStr) : null;
138
+ let trigger = el.querySelector(".date-picker-trigger");
139
+ if (!trigger) {
140
+ trigger = document.createElement("button");
141
+ trigger.type = "button";
142
+ trigger.className = "date-picker-trigger";
143
+ trigger.innerHTML = `<span class="date-picker-value${initialDate ? "" : " is-empty"}"></span><span class="date-picker-icon">${CAL_ICON}</span>`;
144
+ el.prepend(trigger);
145
+ }
146
+ trigger.setAttribute("aria-haspopup", "dialog");
147
+ const valueEl = trigger.querySelector(".date-picker-value");
148
+ function paint() {
149
+ const v = hidden?.value ?? "";
150
+ const d = v ? parseDateStr(v) : null;
151
+ if (valueEl) {
152
+ valueEl.textContent = d ? format(d, fmt) : placeholder;
153
+ valueEl.classList.toggle("is-empty", !d);
154
+ }
155
+ }
156
+ paint();
157
+ if (hidden) {
158
+ const obs = new MutationObserver(() => paint());
159
+ obs.observe(hidden, { attributes: true, attributeFilter: ["value"] });
160
+ }
161
+ trigger.addEventListener("click", () => {
162
+ if (openPicker === el) {
163
+ closePanel();
164
+ return;
165
+ }
166
+ closePanel();
167
+ openPicker = el;
168
+ trigger.classList.add("is-open");
169
+ const panel = ensurePanel();
170
+ panel.hidden = false;
171
+ const selected = hidden?.value ? parseDateStr(hidden.value) : null;
172
+ const view = selected ?? /* @__PURE__ */ new Date();
173
+ view.setDate(15);
174
+ positionPanel(panel, trigger);
175
+ renderCalendar(panel, {
176
+ view,
177
+ selected,
178
+ min,
179
+ max,
180
+ weekStart,
181
+ onPick: (d) => {
182
+ if (d === null) {
183
+ if (hidden) {
184
+ hidden.value = "";
185
+ hidden.dispatchEvent(new Event("change", { bubbles: true }));
186
+ }
187
+ } else {
188
+ if (hidden) {
189
+ hidden.value = toDateStr(d);
190
+ hidden.dispatchEvent(new Event("change", { bubbles: true }));
191
+ }
192
+ }
193
+ paint();
194
+ closePanel();
195
+ }
196
+ });
197
+ outsideHandler = (ev) => {
198
+ const t = ev.target instanceof Node ? ev.target : null;
199
+ if (!t) return;
200
+ if (panel.contains(t) || el.contains(t)) return;
201
+ closePanel();
202
+ };
203
+ document.addEventListener("mousedown", outsideHandler);
204
+ escHandler = (ev) => {
205
+ if (ev.key === "Escape") {
206
+ ev.preventDefault();
207
+ closePanel();
208
+ }
209
+ };
210
+ window.addEventListener("keydown", escHandler);
211
+ const reposition = () => {
212
+ if (openPicker === el && panel && !panel.hidden) positionPanel(panel, trigger);
213
+ };
214
+ window.addEventListener("scroll", reposition, { passive: true, once: false });
215
+ window.addEventListener("resize", reposition, { passive: true });
216
+ });
217
+ }
218
+ function initDatePicker(root = document) {
219
+ if (typeof document === "undefined") return;
220
+ if (typeof window === "undefined") return;
221
+ const pickers = [];
222
+ if (root instanceof Element && root.matches(".date-picker[data-date-picker]")) pickers.push(root);
223
+ pickers.push(...Array.from(root.querySelectorAll(".date-picker[data-date-picker]")));
224
+ for (const p of pickers) setupPicker(p);
225
+ }
226
+ export {
227
+ initDatePicker
228
+ };
@@ -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,140 @@ function setupAutosize(ta) {
43
52
  ta.addEventListener("input", () => resizeTextarea(ta));
44
53
  resizeTextarea(ta);
45
54
  }
55
+ function setupCounter(field) {
56
+ const maxAttr = field.getAttribute("maxlength");
57
+ const dataCount = field.getAttribute("data-count");
58
+ if (!maxAttr && dataCount === null) return;
59
+ const max = maxAttr ? Number(maxAttr) : 0;
60
+ let counter = null;
61
+ const wrap = field.closest(".input-wrap, .textarea-wrap");
62
+ if (wrap) {
63
+ counter = wrap.querySelector(".input-counter");
64
+ if (!counter) {
65
+ counter = document.createElement("span");
66
+ counter.className = "input-counter";
67
+ wrap.appendChild(counter);
68
+ if (wrap.classList.contains("input-wrap")) {
69
+ wrap.classList.add("input-wrap--has-counter");
70
+ }
71
+ }
72
+ }
73
+ const counterEl = counter;
74
+ if (!counterEl) return;
75
+ function update() {
76
+ const len = field.value.length;
77
+ counterEl.textContent = max > 0 ? `${len} / ${max}` : `${len}`;
78
+ counterEl.classList.remove("is-warn", "is-error");
79
+ if (max > 0) {
80
+ const ratio = len / max;
81
+ if (ratio >= 1) counterEl.classList.add("is-error");
82
+ else if (ratio >= 0.8) counterEl.classList.add("is-warn");
83
+ }
84
+ }
85
+ field.addEventListener("input", () => {
86
+ if (!isComposing) update();
87
+ });
88
+ update();
89
+ }
90
+ function setupMask(field) {
91
+ const mask = field.getAttribute("data-mask");
92
+ if (!mask) return;
93
+ const maskChars = [];
94
+ for (const ch of mask) maskChars.push(ch);
95
+ function isInputSlot(ch) {
96
+ return ch === "#" || ch === "A" || ch === "*";
97
+ }
98
+ function validate(rawChar, slotType) {
99
+ if (slotType === "#") return /[0-9]/.test(rawChar);
100
+ if (slotType === "A") return /[a-zA-Z]/.test(rawChar);
101
+ return true;
102
+ }
103
+ function applyMask(input) {
104
+ let result = "";
105
+ let rawIdx = 0;
106
+ const rawChars = input.split("");
107
+ for (let i = 0; i < maskChars.length && rawIdx < rawChars.length; i++) {
108
+ const slot = maskChars[i];
109
+ if (isInputSlot(slot)) {
110
+ while (rawIdx < rawChars.length && !validate(rawChars[rawIdx], slot)) {
111
+ rawIdx++;
112
+ }
113
+ if (rawIdx < rawChars.length) {
114
+ result += rawChars[rawIdx];
115
+ rawIdx++;
116
+ }
117
+ } else {
118
+ result += slot;
119
+ }
120
+ }
121
+ return result;
122
+ }
123
+ function extractRaw(formatted) {
124
+ let raw = "";
125
+ for (let i = 0; i < formatted.length && i < maskChars.length; i++) {
126
+ if (isInputSlot(maskChars[i])) raw += formatted[i];
127
+ }
128
+ return raw;
129
+ }
130
+ let lastFormatted = applyMask(field.value);
131
+ field.value = lastFormatted;
132
+ field.setAttribute("data-raw-value", extractRaw(lastFormatted));
133
+ field.addEventListener("input", () => {
134
+ if (isComposing) return;
135
+ const cursorPos = field.selectionStart ?? 0;
136
+ const oldRaw = extractRaw(lastFormatted);
137
+ const newFull = field.value;
138
+ const newRaw = extractRaw(newFull);
139
+ const formatted = applyMask(newRaw);
140
+ field.value = formatted;
141
+ field.setAttribute("data-raw-value", extractRaw(formatted));
142
+ lastFormatted = formatted;
143
+ const rawDelta = newRaw.length - oldRaw.length;
144
+ let targetCursor = cursorPos;
145
+ if (rawDelta > 0) {
146
+ let count = 0;
147
+ for (let i = 0; i < formatted.length; i++) {
148
+ if (isInputSlot(maskChars[i])) {
149
+ count++;
150
+ if (count >= newRaw.length) {
151
+ targetCursor = i + 1;
152
+ break;
153
+ }
154
+ }
155
+ }
156
+ } else if (rawDelta < 0) {
157
+ targetCursor = Math.min(cursorPos, formatted.length);
158
+ }
159
+ field.setSelectionRange(targetCursor, targetCursor);
160
+ });
161
+ field.addEventListener("keydown", (ev) => {
162
+ if (isComposing) return;
163
+ if (ev.ctrlKey || ev.metaKey || ev.altKey) return;
164
+ if (["Backspace", "Delete", "Tab", "Escape", "ArrowLeft", "ArrowRight", "Home", "End", "Enter"].includes(ev.key)) return;
165
+ 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
+ const nextSlot = maskChars.find((ch, i) => {
171
+ const filledSlots = maskChars.slice(0, i).filter(isInputSlot).length;
172
+ return isInputSlot(ch) && filledSlots >= currentRaw.length;
173
+ });
174
+ if (nextSlot && !validate(ev.key, nextSlot)) {
175
+ ev.preventDefault();
176
+ }
177
+ });
178
+ }
179
+ function setupTrim(field) {
180
+ if (!field.hasAttribute("data-trim")) return;
181
+ field.addEventListener("blur", () => {
182
+ const trimmed = field.value.trim();
183
+ if (trimmed !== field.value) {
184
+ field.value = trimmed;
185
+ field.dispatchEvent(new Event("input", { bubbles: true }));
186
+ }
187
+ });
188
+ }
46
189
  function setupOtp(group) {
47
190
  const el = group;
48
191
  if (el.__icenInputInit) return;
@@ -57,6 +200,7 @@ function setupOtp(group) {
57
200
  }
58
201
  syncFilled(cell);
59
202
  cell.addEventListener("input", () => {
203
+ if (isComposing) return;
60
204
  const ch = cell.value.slice(-1);
61
205
  cell.value = ch;
62
206
  syncFilled(cell);
@@ -111,6 +255,31 @@ function initInput(root = document) {
111
255
  if (root instanceof HTMLTextAreaElement && root.matches("[data-autosize]")) textareas.push(root);
112
256
  textareas.push(...Array.from(root.querySelectorAll("textarea[data-autosize]")));
113
257
  for (const ta of textareas) setupAutosize(ta);
258
+ const countedFields = [];
259
+ if (root instanceof Element) {
260
+ const fields = Array.from(root.querySelectorAll(
261
+ "input[maxlength], input[data-count], textarea[maxlength], textarea[data-count]"
262
+ ));
263
+ countedFields.push(...fields);
264
+ if (root instanceof HTMLInputElement || root instanceof HTMLTextAreaElement) {
265
+ if (root.hasAttribute("maxlength") || root.hasAttribute("data-count")) countedFields.push(root);
266
+ }
267
+ }
268
+ for (const f of countedFields) setupCounter(f);
269
+ const maskedInputs = [];
270
+ if (root instanceof Element) {
271
+ maskedInputs.push(...Array.from(root.querySelectorAll("input[data-mask]")));
272
+ if (root instanceof HTMLInputElement && root.hasAttribute("data-mask")) maskedInputs.push(root);
273
+ }
274
+ for (const inp of maskedInputs) setupMask(inp);
275
+ const trimFields = [];
276
+ if (root instanceof Element) {
277
+ trimFields.push(...Array.from(root.querySelectorAll("[data-trim]")));
278
+ if ((root instanceof HTMLInputElement || root instanceof HTMLTextAreaElement) && root.hasAttribute("data-trim")) {
279
+ trimFields.push(root);
280
+ }
281
+ }
282
+ for (const f of trimFields) setupTrim(f);
114
283
  const otps = [];
115
284
  if (root instanceof Element && root.matches(".otp")) otps.push(root);
116
285
  otps.push(...Array.from(root.querySelectorAll(".otp")));