@icen.ai/ui 0.1.1 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/README.md +11 -0
  2. package/dist/base.css +72 -0
  3. package/dist/behaviors/accordion.mjs +34 -0
  4. package/dist/behaviors/carousel.mjs +50 -0
  5. package/dist/behaviors/charts.mjs +267 -0
  6. package/dist/behaviors/context-menu.mjs +223 -0
  7. package/dist/behaviors/dropdown.mjs +136 -0
  8. package/dist/behaviors/input.mjs +102 -0
  9. package/dist/behaviors/modal.mjs +107 -0
  10. package/dist/behaviors/nav.mjs +29 -0
  11. package/dist/behaviors/popover.mjs +164 -0
  12. package/dist/behaviors/select.mjs +132 -0
  13. package/dist/behaviors/sidebar.mjs +27 -0
  14. package/dist/behaviors/slider.mjs +72 -0
  15. package/dist/behaviors/tag-input.mjs +74 -0
  16. package/dist/behaviors/tree.mjs +44 -0
  17. package/dist/behaviors/upload.mjs +55 -0
  18. package/dist/components/breadcrumb.css +31 -0
  19. package/dist/components/card.css +56 -0
  20. package/dist/components/charts.css +95 -0
  21. package/dist/components/content.css +195 -0
  22. package/dist/components/empty.css +25 -0
  23. package/dist/components/feedback.css +106 -0
  24. package/dist/components/input.css +82 -0
  25. package/dist/components/layout.css +109 -0
  26. package/dist/components/media.css +133 -0
  27. package/dist/components/menu.css +85 -0
  28. package/dist/components/modal.css +107 -0
  29. package/dist/components/nav.css +59 -0
  30. package/dist/components/pagination.css +32 -0
  31. package/dist/components/popover.css +25 -0
  32. package/dist/components/segmented.css +112 -0
  33. package/dist/components/select.css +79 -0
  34. package/dist/components/sidebar.css +54 -0
  35. package/dist/components/slider.css +69 -0
  36. package/dist/components/steps.css +61 -0
  37. package/dist/components/switch.css +95 -0
  38. package/dist/components/tag-input.css +48 -0
  39. package/dist/components/toast.css +1 -1
  40. package/dist/components/tooltip.css +56 -0
  41. package/dist/components/upload.css +43 -0
  42. package/dist/index.mjs +1502 -1
  43. package/dist/registry.json +41 -3
  44. package/dist/ui.css +1899 -1
  45. package/package.json +1 -1
@@ -0,0 +1,136 @@
1
+ // src/behaviors/dropdown.ts
2
+ var VIEWPORT_MARGIN = 12;
3
+ var SIDE_OFFSET = 4;
4
+ var active = null;
5
+ var initialized = false;
6
+ function clamp(value, min, max) {
7
+ if (max < min) return min;
8
+ return Math.min(max, Math.max(min, value));
9
+ }
10
+ function menuItems(panel) {
11
+ return Array.from(panel.querySelectorAll(".menu-item:not(.is-disabled):not(:disabled)"));
12
+ }
13
+ function setHighlight(next) {
14
+ if (!active) return;
15
+ active.items.forEach((el, i) => el.classList.toggle("is-highlighted", i === next));
16
+ active.highlight = next;
17
+ }
18
+ function place() {
19
+ if (!active) return;
20
+ const { trigger, panel } = active;
21
+ const rect = trigger.getBoundingClientRect();
22
+ const vw = window.innerWidth;
23
+ const vh = window.innerHeight;
24
+ const pw = panel.offsetWidth;
25
+ const ph = panel.offsetHeight;
26
+ const left = clamp(rect.left, VIEWPORT_MARGIN, vw - VIEWPORT_MARGIN - pw);
27
+ let top = rect.bottom + SIDE_OFFSET;
28
+ let side = "bottom";
29
+ if (top + ph > vh - VIEWPORT_MARGIN && rect.top - SIDE_OFFSET - ph >= VIEWPORT_MARGIN) {
30
+ top = rect.top - SIDE_OFFSET - ph;
31
+ side = "top";
32
+ }
33
+ top = clamp(top, VIEWPORT_MARGIN, vh - VIEWPORT_MARGIN - ph);
34
+ panel.style.left = `${left}px`;
35
+ panel.style.top = `${top}px`;
36
+ panel.dataset.side = side;
37
+ }
38
+ function openDropdown(trigger, highlightFirst = false) {
39
+ closeDropdown();
40
+ const root = trigger.closest("[data-dropdown]");
41
+ const template = root?.querySelector("[data-dropdown-menu]");
42
+ if (!root || !template) return;
43
+ const panel = document.createElement("div");
44
+ panel.className = "menu";
45
+ panel.setAttribute("role", "menu");
46
+ panel.style.visibility = "hidden";
47
+ while (template.firstChild) panel.appendChild(template.firstChild);
48
+ panel.querySelectorAll(".menu-item").forEach((el) => el.setAttribute("role", "menuitem"));
49
+ document.body.appendChild(panel);
50
+ active = { trigger, template, panel, items: menuItems(panel), highlight: -1 };
51
+ trigger.setAttribute("aria-expanded", "true");
52
+ place();
53
+ panel.style.visibility = "";
54
+ if (highlightFirst && active.items.length > 0) setHighlight(0);
55
+ document.addEventListener("pointerdown", onOutsidePointerDown, true);
56
+ document.addEventListener("scroll", onScroll, true);
57
+ window.addEventListener("keydown", onKeyDown);
58
+ }
59
+ function closeDropdown(restoreFocus = false) {
60
+ if (!active) return;
61
+ const { trigger, template, panel } = active;
62
+ active = null;
63
+ trigger.setAttribute("aria-expanded", "false");
64
+ while (panel.firstChild) template.appendChild(panel.firstChild);
65
+ panel.remove();
66
+ document.removeEventListener("pointerdown", onOutsidePointerDown, true);
67
+ document.removeEventListener("scroll", onScroll, true);
68
+ window.removeEventListener("keydown", onKeyDown);
69
+ if (restoreFocus) trigger.focus();
70
+ }
71
+ function onOutsidePointerDown(e) {
72
+ if (!active) return;
73
+ const target = e.target;
74
+ if (!(target instanceof Node)) return;
75
+ if (active.panel.contains(target) || active.trigger.contains(target)) return;
76
+ closeDropdown();
77
+ }
78
+ function onScroll() {
79
+ closeDropdown();
80
+ }
81
+ function onKeyDown(e) {
82
+ if (!active) return;
83
+ const { items } = active;
84
+ if (e.key === "Escape") {
85
+ e.preventDefault();
86
+ closeDropdown(true);
87
+ return;
88
+ }
89
+ if (items.length === 0) return;
90
+ if (e.key === "ArrowDown" || e.key === "ArrowUp") {
91
+ e.preventDefault();
92
+ const offset = e.key === "ArrowDown" ? 1 : -1;
93
+ const next = active.highlight < 0 ? offset > 0 ? 0 : items.length - 1 : (active.highlight + offset + items.length) % items.length;
94
+ setHighlight(next);
95
+ return;
96
+ }
97
+ if (e.key === "Enter" && active.highlight >= 0) {
98
+ e.preventDefault();
99
+ const item = items[active.highlight];
100
+ closeDropdown(true);
101
+ item.click();
102
+ }
103
+ }
104
+ function initDropdown(root = document) {
105
+ if (typeof document === "undefined") return;
106
+ root.querySelectorAll("[data-dropdown-trigger]").forEach((el) => el.setAttribute("aria-expanded", "false"));
107
+ if (initialized) return;
108
+ initialized = true;
109
+ document.addEventListener("click", (e) => {
110
+ const target = e.target instanceof Element ? e.target : null;
111
+ if (!target) return;
112
+ if (active && target.closest(".menu-item") && active.panel.contains(target)) {
113
+ const item = target.closest(".menu-item");
114
+ if (item) {
115
+ closeDropdown();
116
+ return;
117
+ }
118
+ }
119
+ const trigger = target.closest("[data-dropdown-trigger]");
120
+ if (!trigger) return;
121
+ if (active && active.trigger === trigger) closeDropdown();
122
+ else openDropdown(trigger);
123
+ });
124
+ document.addEventListener("keydown", (e) => {
125
+ if (e.key !== "ArrowDown") return;
126
+ if (active) return;
127
+ const target = e.target instanceof Element ? e.target : null;
128
+ const trigger = target?.closest("[data-dropdown-trigger]");
129
+ if (!trigger) return;
130
+ e.preventDefault();
131
+ openDropdown(trigger, true);
132
+ });
133
+ }
134
+ export {
135
+ initDropdown
136
+ };
@@ -0,0 +1,102 @@
1
+ // src/behaviors/input.ts
2
+ function setupClear(wrap) {
3
+ const btn = wrap.querySelector(".input-clear");
4
+ const field = wrap.querySelector("input.input");
5
+ if (!btn || !field) return;
6
+ btn.addEventListener("click", () => {
7
+ field.value = "";
8
+ field.dispatchEvent(new Event("input", { bubbles: true }));
9
+ field.focus();
10
+ });
11
+ }
12
+ function setupPasswordToggle(wrap) {
13
+ const btn = wrap.querySelector(".input-pw-toggle");
14
+ const field = wrap.querySelector("input.input");
15
+ if (!btn || !field) return;
16
+ const iconShow = btn.querySelector('[data-icon="show"]');
17
+ const iconHide = btn.querySelector('[data-icon="hide"]');
18
+ btn.setAttribute("aria-label", "\u663E\u793A\u5BC6\u7801");
19
+ btn.addEventListener("click", () => {
20
+ const show = field.type === "password";
21
+ field.type = show ? "text" : "password";
22
+ if (iconShow) iconShow.hidden = show;
23
+ if (iconHide) iconHide.hidden = !show;
24
+ btn.setAttribute("aria-label", show ? "\u9690\u85CF\u5BC6\u7801" : "\u663E\u793A\u5BC6\u7801");
25
+ });
26
+ }
27
+ function resizeTextarea(ta) {
28
+ ta.style.height = "auto";
29
+ const cs = getComputedStyle(ta);
30
+ const lineHeight = parseFloat(cs.lineHeight) || 20;
31
+ const padY = (parseFloat(cs.paddingTop) || 0) + (parseFloat(cs.paddingBottom) || 0);
32
+ const minRows = Number(ta.getAttribute("data-min-rows") ?? "3") || 3;
33
+ const maxAttr = ta.getAttribute("data-max-rows");
34
+ const maxRows = maxAttr ? Number(maxAttr) || 0 : 0;
35
+ const minH = minRows * lineHeight + padY;
36
+ const maxH = maxRows > 0 ? maxRows * lineHeight + padY : Infinity;
37
+ ta.style.height = `${Math.max(minH, Math.min(ta.scrollHeight, maxH))}px`;
38
+ }
39
+ function setupAutosize(ta) {
40
+ const el = ta;
41
+ if (el.__icenInputInit) return;
42
+ el.__icenInputInit = true;
43
+ ta.addEventListener("input", () => resizeTextarea(ta));
44
+ resizeTextarea(ta);
45
+ }
46
+ function setupOtp(group) {
47
+ const el = group;
48
+ if (el.__icenInputInit) return;
49
+ el.__icenInputInit = true;
50
+ const cells = Array.from(group.querySelectorAll(".otp-cell"));
51
+ cells.forEach((cell, i) => {
52
+ cell.addEventListener("input", () => {
53
+ const ch = cell.value.slice(-1);
54
+ cell.value = ch;
55
+ if (ch && i < cells.length - 1) cells[i + 1].focus();
56
+ });
57
+ cell.addEventListener("keydown", (ev) => {
58
+ if (ev.key === "Backspace" && !cell.value && i > 0) cells[i - 1].focus();
59
+ if (ev.key === "ArrowLeft" && i > 0) {
60
+ ev.preventDefault();
61
+ cells[i - 1].focus();
62
+ }
63
+ if (ev.key === "ArrowRight" && i < cells.length - 1) {
64
+ ev.preventDefault();
65
+ cells[i + 1].focus();
66
+ }
67
+ });
68
+ cell.addEventListener("paste", (ev) => {
69
+ ev.preventDefault();
70
+ const text = (ev.clipboardData?.getData("text") ?? "").replace(/\s/g, "").slice(0, cells.length - i);
71
+ for (let k = 0; k < text.length; k++) {
72
+ const target = cells[i + k];
73
+ target.value = text[k] ?? "";
74
+ }
75
+ cells[Math.min(i + text.length, cells.length - 1)].focus();
76
+ });
77
+ });
78
+ }
79
+ function initInput(root = document) {
80
+ if (typeof document === "undefined") return;
81
+ const wraps = [];
82
+ if (root instanceof Element && root.matches(".input-wrap")) wraps.push(root);
83
+ wraps.push(...Array.from(root.querySelectorAll(".input-wrap")));
84
+ for (const w of wraps) {
85
+ const el = w;
86
+ if (el.__icenInputInit) continue;
87
+ el.__icenInputInit = true;
88
+ setupClear(w);
89
+ setupPasswordToggle(w);
90
+ }
91
+ const textareas = [];
92
+ if (root instanceof HTMLTextAreaElement && root.matches("[data-autosize]")) textareas.push(root);
93
+ textareas.push(...Array.from(root.querySelectorAll("textarea[data-autosize]")));
94
+ for (const ta of textareas) setupAutosize(ta);
95
+ const otps = [];
96
+ if (root instanceof Element && root.matches(".otp")) otps.push(root);
97
+ otps.push(...Array.from(root.querySelectorAll(".otp")));
98
+ for (const g of otps) setupOtp(g);
99
+ }
100
+ export {
101
+ initInput
102
+ };
@@ -0,0 +1,107 @@
1
+ // src/behaviors/modal.ts
2
+ var FOCUSABLE = 'button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])';
3
+ var openId = null;
4
+ var restoreFocusTo = null;
5
+ var prevBodyOverflow = "";
6
+ var keyHandler = null;
7
+ var initialized = false;
8
+ function findBackdrop(id) {
9
+ return document.querySelector(`.modal-backdrop[data-modal="${CSS.escape(id)}"]`);
10
+ }
11
+ function portal(backdrop) {
12
+ if (backdrop.parentElement !== document.body) document.body.appendChild(backdrop);
13
+ }
14
+ function openModal(id) {
15
+ if (typeof document === "undefined") return;
16
+ if (openId === id) return;
17
+ if (openId) closeModal(openId);
18
+ const backdrop = findBackdrop(id);
19
+ if (!backdrop) return;
20
+ const panel = backdrop.querySelector(".modal");
21
+ if (!panel) return;
22
+ portal(backdrop);
23
+ restoreFocusTo = document.activeElement instanceof HTMLElement ? document.activeElement : null;
24
+ openId = id;
25
+ backdrop.hidden = false;
26
+ prevBodyOverflow = document.body.style.overflow;
27
+ document.body.style.overflow = "hidden";
28
+ panel.setAttribute("role", "dialog");
29
+ panel.setAttribute("aria-modal", "true");
30
+ if (!panel.hasAttribute("tabindex")) panel.tabIndex = -1;
31
+ const closeOnEscape = backdrop.dataset.closeOnEscape !== "false";
32
+ keyHandler = (e) => {
33
+ if (e.key === "Escape") {
34
+ if (closeOnEscape) {
35
+ e.preventDefault();
36
+ closeModal(id);
37
+ }
38
+ return;
39
+ }
40
+ if (e.key !== "Tab") return;
41
+ const els = panel.querySelectorAll(FOCUSABLE);
42
+ if (els.length === 0) {
43
+ e.preventDefault();
44
+ panel.focus();
45
+ return;
46
+ }
47
+ const first = els[0];
48
+ const last = els[els.length - 1];
49
+ if (e.shiftKey) {
50
+ if (document.activeElement === first || !panel.contains(document.activeElement)) {
51
+ e.preventDefault();
52
+ last.focus();
53
+ }
54
+ } else if (document.activeElement === last || !panel.contains(document.activeElement)) {
55
+ e.preventDefault();
56
+ first.focus();
57
+ }
58
+ };
59
+ window.addEventListener("keydown", keyHandler);
60
+ const focusable = panel.querySelector(FOCUSABLE);
61
+ (focusable ?? panel).focus();
62
+ }
63
+ function closeModal(id) {
64
+ if (typeof document === "undefined") return;
65
+ const backdrop = findBackdrop(id);
66
+ if (!backdrop) return;
67
+ backdrop.hidden = true;
68
+ if (openId !== id) return;
69
+ openId = null;
70
+ if (keyHandler) {
71
+ window.removeEventListener("keydown", keyHandler);
72
+ keyHandler = null;
73
+ }
74
+ document.body.style.overflow = prevBodyOverflow;
75
+ if (restoreFocusTo && restoreFocusTo.isConnected) restoreFocusTo.focus();
76
+ restoreFocusTo = null;
77
+ }
78
+ function initModal(root = document) {
79
+ if (typeof document === "undefined") return;
80
+ root.querySelectorAll("[data-modal-open]").forEach((el) => el.setAttribute("aria-haspopup", "dialog"));
81
+ if (initialized) return;
82
+ initialized = true;
83
+ document.addEventListener("click", (e) => {
84
+ const target = e.target instanceof Element ? e.target : null;
85
+ if (!target) return;
86
+ const opener = target.closest("[data-modal-open]");
87
+ if (opener?.dataset.modalOpen) {
88
+ opener.setAttribute("aria-haspopup", "dialog");
89
+ openModal(opener.dataset.modalOpen);
90
+ return;
91
+ }
92
+ const closer = target.closest("[data-modal-close]");
93
+ if (closer) {
94
+ const backdrop = closer.closest("[data-modal]");
95
+ if (backdrop?.dataset.modal) closeModal(backdrop.dataset.modal);
96
+ return;
97
+ }
98
+ if (target instanceof HTMLElement && target.classList.contains("modal-backdrop") && target.dataset.modal && target.dataset.closeOnOverlay !== "false") {
99
+ closeModal(target.dataset.modal);
100
+ }
101
+ });
102
+ }
103
+ export {
104
+ closeModal,
105
+ initModal,
106
+ openModal
107
+ };
@@ -0,0 +1,29 @@
1
+ // src/behaviors/nav.ts
2
+ function setup(nav) {
3
+ const el = nav;
4
+ if (el.__icenNavInit) return;
5
+ el.__icenNavInit = true;
6
+ let ticking = false;
7
+ const update = () => {
8
+ ticking = false;
9
+ el.classList.toggle("is-scrolled", window.scrollY > 12);
10
+ };
11
+ const onScroll = () => {
12
+ if (ticking) return;
13
+ ticking = true;
14
+ window.requestAnimationFrame(update);
15
+ };
16
+ window.addEventListener("scroll", onScroll, { passive: true });
17
+ update();
18
+ }
19
+ function initNav(root = document) {
20
+ if (typeof document === "undefined") return;
21
+ if (typeof window === "undefined") return;
22
+ const navs = [];
23
+ if (root instanceof Element && root.matches(".nav[data-scroll-reactive]")) navs.push(root);
24
+ navs.push(...Array.from(root.querySelectorAll(".nav[data-scroll-reactive]")));
25
+ for (const n of navs) setup(n);
26
+ }
27
+ export {
28
+ initNav
29
+ };
@@ -0,0 +1,164 @@
1
+ // src/behaviors/popover.ts
2
+ var DEFAULT_MIN_WIDTH = 220;
3
+ var DEFAULT_MAX_WIDTH = 420;
4
+ var DEFAULT_MIN_HEIGHT = 120;
5
+ var DEFAULT_MAX_HEIGHT = 420;
6
+ function clamp(value, min, max) {
7
+ if (max < min) return min;
8
+ return Math.min(max, Math.max(min, value));
9
+ }
10
+ function chooseSide(preferredSide, availableTop, availableBottom, desiredHeight) {
11
+ const preferredSpace = preferredSide === "top" ? availableTop : availableBottom;
12
+ const oppositeSpace = preferredSide === "top" ? availableBottom : availableTop;
13
+ if (preferredSpace >= desiredHeight) return preferredSide;
14
+ if (oppositeSpace >= desiredHeight) return preferredSide === "top" ? "bottom" : "top";
15
+ if (preferredSpace >= oppositeSpace) return preferredSide;
16
+ return preferredSide === "top" ? "bottom" : "top";
17
+ }
18
+ function horizontalAnchor(anchor, width, align) {
19
+ if (align === "center") return anchor.left + anchor.width / 2 - width / 2;
20
+ if (align === "end") return anchor.right - width;
21
+ return anchor.left;
22
+ }
23
+ function computePopoverLayout(anchor, viewport, opts = {}) {
24
+ const margin = opts.margin ?? 12;
25
+ const offset = opts.offset ?? 8;
26
+ const availableWidth = Math.max(80, viewport.width - margin * 2);
27
+ const requestedMinWidth = Math.max(1, opts.minWidth ?? DEFAULT_MIN_WIDTH);
28
+ const requestedMaxWidth = Math.max(requestedMinWidth, opts.maxWidth ?? DEFAULT_MAX_WIDTH);
29
+ const maxWidth = Math.min(requestedMaxWidth, availableWidth);
30
+ const minWidth = Math.min(requestedMinWidth, maxWidth);
31
+ const preferredSide = opts.side ?? "top";
32
+ const align = opts.align ?? "start";
33
+ const availableTop = Math.max(0, anchor.top - margin - offset);
34
+ const availableBottom = Math.max(0, viewport.height - anchor.bottom - margin - offset);
35
+ const availableHeight = Math.max(80, viewport.height - margin * 2);
36
+ const minHeight = Math.min(Math.max(80, opts.minHeight ?? DEFAULT_MIN_HEIGHT), availableHeight);
37
+ const requestedMaxHeight = Math.max(minHeight, opts.maxHeight ?? DEFAULT_MAX_HEIGHT);
38
+ const fallbackHeight = Math.min(260, requestedMaxHeight);
39
+ const contentHeight = Math.max(opts.contentHeightHint ?? 0, fallbackHeight);
40
+ const desiredHeight = clamp(contentHeight, minHeight, Math.min(requestedMaxHeight, availableHeight));
41
+ const side = chooseSide(preferredSide, availableTop, availableBottom, desiredHeight);
42
+ const sideSpace = side === "top" ? availableTop : availableBottom;
43
+ const maxHeight = Math.min(
44
+ requestedMaxHeight,
45
+ Math.max(80, sideSpace || Math.max(availableTop, availableBottom)),
46
+ availableHeight
47
+ );
48
+ const positionedHeight = clamp(desiredHeight, 40, maxHeight);
49
+ const width = align === "stretch" ? clamp(anchor.width, minWidth, maxWidth) : maxWidth;
50
+ const left = clamp(horizontalAnchor(anchor, width, align), margin, viewport.width - margin - width);
51
+ const desiredTop = side === "top" ? anchor.top - offset - positionedHeight : anchor.bottom + offset;
52
+ const top = clamp(desiredTop, margin, viewport.height - margin - positionedHeight);
53
+ return { side, left, top, width, maxHeight };
54
+ }
55
+ var openPanels = /* @__PURE__ */ new Map();
56
+ function rectFromElement(el) {
57
+ const r = el.getBoundingClientRect();
58
+ return { left: r.left, top: r.top, right: r.right, bottom: r.bottom, width: r.width, height: r.height };
59
+ }
60
+ function resolveAnchor(state) {
61
+ if (state.anchor instanceof Element) {
62
+ if (!state.anchor.isConnected) return null;
63
+ return rectFromElement(state.anchor);
64
+ }
65
+ return state.anchor;
66
+ }
67
+ function relayout(state) {
68
+ const anchor = resolveAnchor(state);
69
+ if (!anchor) {
70
+ closePopover(state.panel);
71
+ return;
72
+ }
73
+ const layout = computePopoverLayout(
74
+ anchor,
75
+ { width: window.innerWidth, height: window.innerHeight },
76
+ state.opts
77
+ );
78
+ const { panel } = state;
79
+ panel.style.left = `${layout.left}px`;
80
+ panel.style.top = `${layout.top}px`;
81
+ panel.style.width = `${layout.width}px`;
82
+ panel.style.maxHeight = `${layout.maxHeight}px`;
83
+ panel.dataset.side = layout.side;
84
+ panel.style.visibility = state.prevVisibility;
85
+ }
86
+ function onWindowChange() {
87
+ openPanels.forEach(relayout);
88
+ }
89
+ function onOutsidePointerDown(e) {
90
+ const target = e.target;
91
+ if (!(target instanceof Node)) return;
92
+ openPanels.forEach((state) => {
93
+ if (state.panel.contains(target)) return;
94
+ if (state.anchor instanceof Element && state.anchor.contains(target)) return;
95
+ closePopover(state.panel);
96
+ });
97
+ }
98
+ function onKeyDown(e) {
99
+ if (e.key !== "Escape") return;
100
+ const states = Array.from(openPanels.values());
101
+ const top = states[states.length - 1];
102
+ if (top) {
103
+ e.preventDefault();
104
+ closePopover(top.panel);
105
+ }
106
+ }
107
+ function openPopover(panel, options) {
108
+ if (typeof document === "undefined") return;
109
+ if (openPanels.has(panel)) closePopover(panel);
110
+ const { anchor, onClose, ...layoutOpts } = options;
111
+ const placeholder = document.createComment("popover-anchor");
112
+ panel.parentNode?.insertBefore(placeholder, panel);
113
+ const wasHidden = panel.hidden;
114
+ const prevVisibility = panel.style.visibility;
115
+ const state = {
116
+ panel,
117
+ anchor,
118
+ opts: layoutOpts,
119
+ onClose,
120
+ placeholder,
121
+ wasHidden,
122
+ prevVisibility
123
+ };
124
+ openPanels.set(panel, state);
125
+ document.body.appendChild(panel);
126
+ panel.hidden = false;
127
+ panel.style.visibility = "hidden";
128
+ relayout(state);
129
+ if (openPanels.size === 1) {
130
+ window.addEventListener("resize", onWindowChange);
131
+ window.addEventListener("scroll", onWindowChange, true);
132
+ document.addEventListener("pointerdown", onOutsidePointerDown, true);
133
+ window.addEventListener("keydown", onKeyDown);
134
+ }
135
+ }
136
+ function closePopover(panel) {
137
+ if (typeof document === "undefined") return;
138
+ const state = openPanels.get(panel);
139
+ if (!state) return;
140
+ openPanels.delete(panel);
141
+ panel.style.visibility = state.prevVisibility;
142
+ panel.hidden = state.wasHidden;
143
+ panel.style.left = "";
144
+ panel.style.top = "";
145
+ panel.style.width = "";
146
+ panel.style.maxHeight = "";
147
+ delete panel.dataset.side;
148
+ if (state.placeholder.parentNode) {
149
+ state.placeholder.parentNode.insertBefore(panel, state.placeholder);
150
+ state.placeholder.remove();
151
+ }
152
+ if (openPanels.size === 0) {
153
+ window.removeEventListener("resize", onWindowChange);
154
+ window.removeEventListener("scroll", onWindowChange, true);
155
+ document.removeEventListener("pointerdown", onOutsidePointerDown, true);
156
+ window.removeEventListener("keydown", onKeyDown);
157
+ }
158
+ state.onClose?.();
159
+ }
160
+ export {
161
+ closePopover,
162
+ computePopoverLayout,
163
+ openPopover
164
+ };
@@ -0,0 +1,132 @@
1
+ // src/behaviors/select.ts
2
+ function setup(container) {
3
+ const el = container;
4
+ if (el.__icenSelectInit) return;
5
+ el.__icenSelectInit = true;
6
+ const triggerEl = container.querySelector(".select-trigger");
7
+ const panelEl = container.querySelector(".select-panel");
8
+ const valueEl = container.querySelector(".select-value");
9
+ const hiddenInput = container.querySelector("input[data-select-value]");
10
+ if (!triggerEl || !panelEl) return;
11
+ const trigger = triggerEl;
12
+ const panel = panelEl;
13
+ const options = () => Array.from(panel.querySelectorAll(".select-option"));
14
+ const isOpen = () => !panel.hidden;
15
+ let focusIdx = -1;
16
+ function setFocus(idx) {
17
+ const opts = options();
18
+ focusIdx = Math.max(-1, Math.min(idx, opts.length - 1));
19
+ opts.forEach((o, i) => o.classList.toggle("is-focused", i === focusIdx));
20
+ const cur = focusIdx >= 0 ? opts[focusIdx] : void 0;
21
+ cur?.scrollIntoView({ block: "nearest" });
22
+ }
23
+ function firstEnabled() {
24
+ return options().findIndex((o) => !o.classList.contains("is-disabled"));
25
+ }
26
+ function moveFocus(delta) {
27
+ const opts = options();
28
+ if (opts.length === 0) return;
29
+ let n = focusIdx;
30
+ for (let guard = 0; guard < opts.length; guard++) {
31
+ n = (n + delta + opts.length) % opts.length;
32
+ const opt = opts[n];
33
+ if (opt && !opt.classList.contains("is-disabled")) break;
34
+ }
35
+ setFocus(n);
36
+ }
37
+ function openPanel() {
38
+ if (isOpen()) return;
39
+ panel.hidden = false;
40
+ container.classList.add("is-open");
41
+ trigger.setAttribute("aria-expanded", "true");
42
+ const sel = options().findIndex((o) => o.classList.contains("is-selected"));
43
+ setFocus(sel >= 0 ? sel : firstEnabled());
44
+ }
45
+ function closePanel() {
46
+ if (!isOpen()) return;
47
+ panel.hidden = true;
48
+ container.classList.remove("is-open");
49
+ trigger.setAttribute("aria-expanded", "false");
50
+ setFocus(-1);
51
+ }
52
+ function choose(opt) {
53
+ if (opt.classList.contains("is-disabled")) return;
54
+ options().forEach((o) => o.classList.toggle("is-selected", o === opt));
55
+ if (valueEl) {
56
+ valueEl.textContent = opt.textContent ?? "";
57
+ valueEl.classList.remove("is-empty");
58
+ }
59
+ const v = opt.getAttribute("data-value") ?? "";
60
+ container.setAttribute("data-value", v);
61
+ if (hiddenInput) {
62
+ hiddenInput.value = v;
63
+ hiddenInput.dispatchEvent(new Event("change", { bubbles: true }));
64
+ }
65
+ closePanel();
66
+ trigger.focus();
67
+ }
68
+ trigger.addEventListener("click", () => isOpen() ? closePanel() : openPanel());
69
+ trigger.addEventListener("keydown", (ev) => {
70
+ if (!isOpen()) {
71
+ if (ev.key === "ArrowDown" || ev.key === "ArrowUp" || ev.key === "Enter" || ev.key === " ") {
72
+ ev.preventDefault();
73
+ openPanel();
74
+ }
75
+ return;
76
+ }
77
+ switch (ev.key) {
78
+ case "ArrowDown":
79
+ ev.preventDefault();
80
+ moveFocus(1);
81
+ break;
82
+ case "ArrowUp":
83
+ ev.preventDefault();
84
+ moveFocus(-1);
85
+ break;
86
+ case "Enter":
87
+ case " ": {
88
+ ev.preventDefault();
89
+ const opt = options()[focusIdx];
90
+ if (opt) choose(opt);
91
+ break;
92
+ }
93
+ case "Escape":
94
+ ev.preventDefault();
95
+ closePanel();
96
+ break;
97
+ }
98
+ });
99
+ panel.addEventListener("click", (ev) => {
100
+ const t = ev.target;
101
+ if (!(t instanceof Element)) return;
102
+ const opt = t.closest(".select-option");
103
+ if (opt instanceof HTMLButtonElement && panel.contains(opt)) choose(opt);
104
+ });
105
+ panel.addEventListener("mouseover", (ev) => {
106
+ const t = ev.target;
107
+ if (!(t instanceof Element)) return;
108
+ const opt = t.closest(".select-option");
109
+ if (!opt) return;
110
+ const idx = options().indexOf(opt);
111
+ if (idx >= 0 && !opt.classList.contains("is-disabled")) setFocus(idx);
112
+ });
113
+ document.addEventListener("pointerdown", (ev) => {
114
+ if (isOpen() && ev.target instanceof Node && !container.contains(ev.target)) closePanel();
115
+ });
116
+ document.addEventListener("keydown", (ev) => {
117
+ if (ev.key === "Escape" && isOpen()) {
118
+ closePanel();
119
+ trigger.focus();
120
+ }
121
+ });
122
+ }
123
+ function initSelect(root = document) {
124
+ if (typeof document === "undefined") return;
125
+ const containers = [];
126
+ if (root instanceof Element && root.matches("[data-select]")) containers.push(root);
127
+ containers.push(...Array.from(root.querySelectorAll("[data-select]")));
128
+ for (const c of containers) setup(c);
129
+ }
130
+ export {
131
+ initSelect
132
+ };