@icen.ai/ui 0.1.1 → 0.2.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 (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
package/README.md CHANGED
@@ -30,6 +30,17 @@ initCopy(); // .copy-btn[data-copy] 委托
30
30
  toast.ok('已保存');
31
31
  ```
32
32
 
33
+ ## 文档站(已上线 <https://ui.icen.ai>)
34
+
35
+ `site/` 是公开文档站(首页 / 色彩预设色板 / 组件演示,导航栏实时换肤换明暗)。部署:
36
+
37
+ ```bash
38
+ cd site && bun install && bun run build
39
+ bunx wrangler pages deploy dist --project-name=icen-ui # 需 CLOUDFLARE_ACCOUNT_ID
40
+ ```
41
+
42
+ Pages 项目 `icen-ui`(aidoll 账号),自定义域 `ui.icen.ai` → CNAME `icen-ui.pages.dev`。**已接 Git 自动部署**:push 到 `main` 即由 Cloudflare 构建(根目录 `site`,命令 `bun install && bun run build`,输出 `dist`),无需 wrangler 手动部署、无需任何 token/secret。
43
+
33
44
  ## 构建
34
45
 
35
46
  ```bash
package/dist/base.css CHANGED
@@ -53,3 +53,75 @@ html { scrollbar-width: thin; scrollbar-color: color-mix(in srgb, var(--token-te
53
53
  .mono { font-family: var(--font-mono); }
54
54
  .dim { color: var(--token-text-muted); }
55
55
  .faint { color: var(--token-text-faint); }
56
+
57
+ /* ── z-index 标尺(全组件统一引用,浮层一律 portal 到 body) ── */
58
+ :root {
59
+ --z-base: 0;
60
+ --z-raised: 10;
61
+ --z-toast: 100;
62
+ --z-chrome: 200;
63
+ --z-dialog: 500;
64
+ --z-popover: 620;
65
+ --z-tooltip: 700;
66
+ }
67
+
68
+ /* ── 交互基元(全组件地基,规格源自 opengal interaction-classes) ── */
69
+
70
+ /* 可按压:hover 顶部高光 + active 下陷缩放 */
71
+ .pressable {
72
+ position: relative; overflow: hidden; will-change: transform;
73
+ transition: background-color var(--duration-normal) var(--ease-out),
74
+ border-color var(--duration-normal) var(--ease-out),
75
+ box-shadow var(--duration-normal) var(--ease-out),
76
+ color var(--duration-normal) var(--ease-out),
77
+ opacity var(--duration-normal) var(--ease-out),
78
+ transform var(--duration-normal) var(--ease-out);
79
+ }
80
+ .pressable::after {
81
+ content: ''; position: absolute; inset: 0; border-radius: inherit;
82
+ opacity: 0; pointer-events: none;
83
+ background: linear-gradient(180deg, color-mix(in srgb, var(--token-surface) 24%, transparent), transparent 46%);
84
+ transition: opacity var(--duration-normal) var(--ease-out);
85
+ }
86
+ .pressable:hover::after, .pressable:focus-visible::after { opacity: 1; }
87
+ .pressable:active { transform: translateY(1px) scale(0.985); }
88
+ .pressable:disabled, .pressable[aria-disabled='true'], .pressable[data-disabled] {
89
+ transform: none;
90
+ }
91
+ .pressable:disabled::after, .pressable[aria-disabled='true']::after, .pressable[data-disabled]::after { display: none; }
92
+
93
+ /* 控件聚焦光晕(wrapper 式表单控件用 :focus-within) */
94
+ .control {
95
+ transition: background-color var(--duration-normal) var(--ease-out),
96
+ border-color var(--duration-normal) var(--ease-out),
97
+ box-shadow var(--duration-normal) var(--ease-out);
98
+ }
99
+ .control:focus-within {
100
+ box-shadow: var(--token-shadow-inset), 0 0 0 3px color-mix(in srgb, var(--token-accent) 12%, transparent);
101
+ }
102
+
103
+ /* 悬停浮起(可交互卡片) */
104
+ .lift {
105
+ transition: border-color var(--duration-slow) var(--ease-out),
106
+ box-shadow var(--duration-slow) var(--ease-out),
107
+ transform var(--duration-slow) var(--ease-out);
108
+ }
109
+ .lift:hover {
110
+ transform: translateY(-1px);
111
+ border-color: color-mix(in srgb, var(--token-accent) 26%, var(--token-outline-soft));
112
+ box-shadow: var(--token-shadow-card);
113
+ }
114
+
115
+ /* 聚焦环(ring-2 + offset-1 等价) */
116
+ .focus-ring:focus-visible {
117
+ outline: none;
118
+ box-shadow: 0 0 0 1px var(--token-bg), 0 0 0 3px color-mix(in srgb, var(--token-accent) 60%, transparent);
119
+ }
120
+
121
+ /* 动效开关(reduced-motion 时全部关闭) */
122
+ @media (prefers-reduced-motion: no-preference) {
123
+ .motion { transition: all var(--duration-normal) var(--ease-out); }
124
+ }
125
+ @media (prefers-reduced-motion: reduce) {
126
+ *, *::before, *::after { animation-duration: 0.01ms !important; transition-duration: 0.01ms !important; }
127
+ }
@@ -0,0 +1,34 @@
1
+ // src/behaviors/accordion.ts
2
+ function setup(container) {
3
+ const el = container;
4
+ if (el.__icenAccordionInit) return;
5
+ el.__icenAccordionInit = true;
6
+ const single = container.hasAttribute("data-single");
7
+ container.addEventListener("click", (ev) => {
8
+ const target = ev.target;
9
+ if (!(target instanceof Element)) return;
10
+ const trigger = target.closest(".accordion-trigger");
11
+ if (!trigger) return;
12
+ const item = trigger.closest(".accordion-item");
13
+ if (!item || trigger.closest(".accordion") !== container) return;
14
+ const open = item.classList.toggle("is-open");
15
+ trigger.setAttribute("aria-expanded", String(open));
16
+ if (single && open) {
17
+ container.querySelectorAll(".accordion-item.is-open").forEach((other) => {
18
+ if (other === item) return;
19
+ other.classList.remove("is-open");
20
+ other.querySelector(".accordion-trigger")?.setAttribute("aria-expanded", "false");
21
+ });
22
+ }
23
+ });
24
+ }
25
+ function initAccordion(root = document) {
26
+ if (typeof document === "undefined") return;
27
+ const containers = [];
28
+ if (root instanceof Element && root.matches(".accordion")) containers.push(root);
29
+ containers.push(...Array.from(root.querySelectorAll(".accordion")));
30
+ for (const c of containers) setup(c);
31
+ }
32
+ export {
33
+ initAccordion
34
+ };
@@ -0,0 +1,50 @@
1
+ // src/behaviors/carousel.ts
2
+ function setup(carousel) {
3
+ const el = carousel;
4
+ if (el.__icenCarouselInit) return;
5
+ el.__icenCarouselInit = true;
6
+ const track = carousel.querySelector(":scope > .carousel-track");
7
+ if (!track) return;
8
+ const slides = Array.from(track.querySelectorAll(":scope > .carousel-slide"));
9
+ const count = slides.length;
10
+ if (count === 0) return;
11
+ const prev = carousel.querySelector(".carousel-arrow--prev");
12
+ const next = carousel.querySelector(".carousel-arrow--next");
13
+ const dotsBox = carousel.querySelector(".carousel-dots");
14
+ let index = 0;
15
+ const dots = [];
16
+ if (dotsBox && count > 1) {
17
+ for (let i = 0; i < count; i++) {
18
+ const dot = document.createElement("button");
19
+ dot.type = "button";
20
+ dot.className = "carousel-dot";
21
+ dot.setAttribute("aria-label", `\u7B2C ${i + 1} \u5F20`);
22
+ dot.addEventListener("click", () => go(i));
23
+ dotsBox.appendChild(dot);
24
+ dots.push(dot);
25
+ }
26
+ }
27
+ function render() {
28
+ if (!track) return;
29
+ track.style.transform = `translateX(${-index * 100}%)`;
30
+ slides.forEach((s, i) => s.setAttribute("aria-hidden", String(i !== index)));
31
+ dots.forEach((d, i) => d.classList.toggle("active", i === index));
32
+ }
33
+ function go(i) {
34
+ index = (i % count + count) % count;
35
+ render();
36
+ }
37
+ prev?.addEventListener("click", () => go(index - 1));
38
+ next?.addEventListener("click", () => go(index + 1));
39
+ render();
40
+ }
41
+ function initCarousel(root = document) {
42
+ if (typeof document === "undefined") return;
43
+ const containers = [];
44
+ if (root instanceof Element && root.matches(".carousel")) containers.push(root);
45
+ containers.push(...Array.from(root.querySelectorAll(".carousel")));
46
+ for (const c of containers) setup(c);
47
+ }
48
+ export {
49
+ initCarousel
50
+ };
@@ -0,0 +1,267 @@
1
+ // src/behaviors/charts.ts
2
+ var SVG_NS = "http://www.w3.org/2000/svg";
3
+ function toneVar(tone = "accent") {
4
+ switch (tone) {
5
+ case "success":
6
+ return "var(--token-success)";
7
+ case "warning":
8
+ return "var(--token-warning)";
9
+ case "error":
10
+ return "var(--token-error)";
11
+ case "muted":
12
+ return "var(--token-text-faint)";
13
+ case "accent":
14
+ default:
15
+ return "var(--token-accent)";
16
+ }
17
+ }
18
+ function fmt(opts, value) {
19
+ return opts.formatValue ? opts.formatValue(value) : String(value);
20
+ }
21
+ function svgEl(tag, attrs) {
22
+ const el = document.createElementNS(SVG_NS, tag);
23
+ for (const [k, v] of Object.entries(attrs)) el.setAttribute(k, v);
24
+ return el;
25
+ }
26
+ function renderEmpty(el, label) {
27
+ const box = document.createElement("div");
28
+ box.className = "chart-empty";
29
+ box.textContent = label ?? "\u6682\u65E0\u6570\u636E";
30
+ el.appendChild(box);
31
+ }
32
+ function legendItem(label, valueText, tone) {
33
+ const row = document.createElement("div");
34
+ row.className = "chart-legend-item";
35
+ const dot = document.createElement("span");
36
+ dot.className = "chart-legend-dot";
37
+ if (tone) dot.style.setProperty("--chart-tone", toneVar(tone));
38
+ const name = document.createElement("span");
39
+ name.className = "chart-legend-label";
40
+ name.textContent = label;
41
+ const val = document.createElement("span");
42
+ val.className = "chart-legend-value";
43
+ val.textContent = valueText;
44
+ row.append(dot, name, val);
45
+ return row;
46
+ }
47
+ function labelsRow(labels) {
48
+ const row = document.createElement("div");
49
+ row.className = "chart-labels";
50
+ for (const label of labels) {
51
+ const span = document.createElement("span");
52
+ span.textContent = label;
53
+ row.appendChild(span);
54
+ }
55
+ return row;
56
+ }
57
+ function renderVBar(el, opts) {
58
+ if (typeof document === "undefined") return;
59
+ el.textContent = "";
60
+ const n = Math.min(opts.labels.length, opts.values.length);
61
+ if (n === 0) {
62
+ renderEmpty(el, opts.emptyLabel);
63
+ return;
64
+ }
65
+ const labels = opts.labels.slice(0, n);
66
+ const values = opts.values.slice(0, n);
67
+ const max = Math.max(...values, 1);
68
+ const wrap = document.createElement("div");
69
+ wrap.className = "chart-vbar";
70
+ if (opts.tone) wrap.style.setProperty("--chart-tone", toneVar(opts.tone));
71
+ values.forEach((value, i) => {
72
+ const track = document.createElement("div");
73
+ track.className = "chart-vbar-track";
74
+ const bar = document.createElement("div");
75
+ bar.className = "chart-vbar-bar";
76
+ bar.style.height = `${Math.max(6, value / max * 100)}%`;
77
+ bar.setAttribute("title", `${labels[i]}: ${fmt(opts, value)}`);
78
+ track.appendChild(bar);
79
+ wrap.appendChild(track);
80
+ });
81
+ el.append(wrap, labelsRow(labels));
82
+ }
83
+ function renderHBar(el, opts) {
84
+ if (typeof document === "undefined") return;
85
+ el.textContent = "";
86
+ const n = Math.min(opts.labels.length, opts.values.length);
87
+ if (n === 0) {
88
+ renderEmpty(el, opts.emptyLabel);
89
+ return;
90
+ }
91
+ const labels = opts.labels.slice(0, n);
92
+ const values = opts.values.slice(0, n);
93
+ const max = Math.max(...values, 1);
94
+ const wrap = document.createElement("div");
95
+ wrap.className = "chart-hbar";
96
+ if (opts.tone) wrap.style.setProperty("--chart-tone", toneVar(opts.tone));
97
+ values.forEach((value, i) => {
98
+ const row = document.createElement("div");
99
+ row.className = "chart-hbar-row";
100
+ const label = document.createElement("span");
101
+ label.className = "chart-hbar-label";
102
+ label.textContent = labels[i] ?? "";
103
+ const track = document.createElement("div");
104
+ track.className = "chart-hbar-track";
105
+ const bar = document.createElement("div");
106
+ bar.className = "chart-hbar-bar";
107
+ bar.style.width = `${Math.max(2, value / max * 100)}%`;
108
+ const val = document.createElement("span");
109
+ val.className = "chart-hbar-value";
110
+ val.textContent = fmt(opts, value);
111
+ track.appendChild(bar);
112
+ row.append(label, track, val);
113
+ wrap.appendChild(row);
114
+ });
115
+ el.appendChild(wrap);
116
+ }
117
+ function renderStack(el, opts) {
118
+ if (typeof document === "undefined") return;
119
+ el.textContent = "";
120
+ const total = opts.segments.reduce((sum, s) => sum + s.value, 0);
121
+ if (opts.segments.length === 0 || total <= 0) {
122
+ renderEmpty(el, opts.emptyLabel);
123
+ return;
124
+ }
125
+ const bar = document.createElement("div");
126
+ bar.className = "chart-stack";
127
+ for (const seg of opts.segments) {
128
+ const s = document.createElement("div");
129
+ s.className = "chart-stack-seg";
130
+ s.style.width = `${seg.value / total * 100}%`;
131
+ if (seg.tone) s.style.setProperty("--chart-tone", toneVar(seg.tone));
132
+ s.setAttribute("title", `${seg.label}: ${fmt(opts, seg.value)}`);
133
+ bar.appendChild(s);
134
+ }
135
+ const legend = document.createElement("div");
136
+ legend.className = "chart-legend";
137
+ for (const seg of opts.segments) {
138
+ legend.appendChild(legendItem(seg.label, fmt(opts, seg.value), seg.tone));
139
+ }
140
+ el.append(bar, legend);
141
+ }
142
+ function renderDonut(el, opts) {
143
+ if (typeof document === "undefined") return;
144
+ el.textContent = "";
145
+ const total = opts.segments.reduce((sum, s) => sum + s.value, 0);
146
+ if (opts.segments.length === 0 || total <= 0) {
147
+ renderEmpty(el, opts.emptyLabel);
148
+ return;
149
+ }
150
+ const radius = 42;
151
+ const circumference = 2 * Math.PI * radius;
152
+ const wrap = document.createElement("div");
153
+ wrap.className = "chart-donut";
154
+ const svg = svgEl("svg", { viewBox: "0 0 120 120", role: "img" });
155
+ svg.style.transform = "rotate(-90deg)";
156
+ svg.appendChild(svgEl("circle", {
157
+ cx: "60",
158
+ cy: "60",
159
+ r: String(radius),
160
+ fill: "none",
161
+ stroke: "var(--token-line-soft)",
162
+ "stroke-width": "14"
163
+ }));
164
+ let offset = 0;
165
+ for (const seg of opts.segments) {
166
+ const dash = seg.value / total * circumference;
167
+ svg.appendChild(svgEl("circle", {
168
+ cx: "60",
169
+ cy: "60",
170
+ r: String(radius),
171
+ fill: "none",
172
+ stroke: toneVar(seg.tone),
173
+ "stroke-width": "14",
174
+ "stroke-dasharray": `${dash} ${circumference - dash}`,
175
+ "stroke-dashoffset": String(-offset),
176
+ "stroke-linecap": "butt"
177
+ }));
178
+ offset += dash;
179
+ }
180
+ const legend = document.createElement("div");
181
+ legend.className = "chart-legend";
182
+ for (const seg of opts.segments) {
183
+ legend.appendChild(legendItem(seg.label, `${Math.round(seg.value / total * 100)}%`, seg.tone));
184
+ }
185
+ wrap.append(svg, legend);
186
+ el.appendChild(wrap);
187
+ }
188
+ var lineGradientSeq = 0;
189
+ function renderLine(el, opts) {
190
+ if (typeof document === "undefined") return;
191
+ el.textContent = "";
192
+ const n = Math.min(opts.labels.length, opts.values.length);
193
+ if (n === 0) {
194
+ renderEmpty(el, opts.emptyLabel);
195
+ return;
196
+ }
197
+ const labels = opts.labels.slice(0, n);
198
+ const values = opts.values.slice(0, n);
199
+ const width = 360;
200
+ const height = 150;
201
+ const pad = 14;
202
+ const max = Math.max(...values, 1);
203
+ const tone = toneVar(opts.tone);
204
+ const points = values.map((value, i) => ({
205
+ x: values.length <= 1 ? width / 2 : pad + i / (values.length - 1) * (width - pad * 2),
206
+ y: height - pad - value / max * (height - pad * 2),
207
+ value,
208
+ label: labels[i] ?? ""
209
+ }));
210
+ const line = points.map((p, i) => `${i === 0 ? "M" : "L"}${p.x.toFixed(1)},${p.y.toFixed(1)}`).join(" ");
211
+ const last = points[points.length - 1];
212
+ const first = points[0];
213
+ const area = `${line} L${last.x.toFixed(1)},${height - pad} L${first.x.toFixed(1)},${height - pad} Z`;
214
+ const gradientId = `icen-chart-line-${++lineGradientSeq}`;
215
+ const wrap = document.createElement("div");
216
+ wrap.className = "chart-line";
217
+ const svg = svgEl("svg", { viewBox: `0 0 ${width} ${height}`, role: "img" });
218
+ const defs = svgEl("defs", {});
219
+ const gradient = svgEl("linearGradient", { id: gradientId, x1: "0", x2: "0", y1: "0", y2: "1" });
220
+ gradient.appendChild(svgEl("stop", { offset: "0%", "stop-color": tone, "stop-opacity": "0.32" }));
221
+ gradient.appendChild(svgEl("stop", { offset: "100%", "stop-color": tone, "stop-opacity": "0.02" }));
222
+ defs.appendChild(gradient);
223
+ svg.appendChild(defs);
224
+ for (let i = 0; i < 4; i++) {
225
+ const y = pad + i * (height - pad * 2) / 3;
226
+ svg.appendChild(svgEl("line", {
227
+ x1: String(pad),
228
+ x2: String(width - pad),
229
+ y1: String(y),
230
+ y2: String(y),
231
+ stroke: "var(--token-line-soft)",
232
+ "stroke-width": "1"
233
+ }));
234
+ }
235
+ svg.appendChild(svgEl("path", { d: area, fill: `url(#${gradientId})` }));
236
+ svg.appendChild(svgEl("path", {
237
+ d: line,
238
+ fill: "none",
239
+ stroke: tone,
240
+ "stroke-width": "2.5",
241
+ "stroke-linecap": "round",
242
+ "stroke-linejoin": "round"
243
+ }));
244
+ for (const p of points) {
245
+ const dot = svgEl("circle", {
246
+ cx: p.x.toFixed(1),
247
+ cy: p.y.toFixed(1),
248
+ r: "3",
249
+ fill: "var(--token-card)",
250
+ stroke: tone,
251
+ "stroke-width": "2"
252
+ });
253
+ const title = svgEl("title", {});
254
+ title.textContent = `${p.label}: ${fmt(opts, p.value)}`;
255
+ dot.appendChild(title);
256
+ svg.appendChild(dot);
257
+ }
258
+ wrap.append(svg, labelsRow(labels.slice(-7)));
259
+ el.appendChild(wrap);
260
+ }
261
+ export {
262
+ renderDonut,
263
+ renderHBar,
264
+ renderLine,
265
+ renderStack,
266
+ renderVBar
267
+ };
@@ -0,0 +1,223 @@
1
+ // src/behaviors/context-menu.ts
2
+ var LONG_PRESS_DELAY = 520;
3
+ var LONG_PRESS_MOVE_LIMIT = 12;
4
+ var FLIP_MARGIN = 10;
5
+ var CLAMP_MARGIN = 8;
6
+ var registry = /* @__PURE__ */ new Map();
7
+ var fallbackItems = null;
8
+ var initialized = false;
9
+ var panel = null;
10
+ var focusableItems = [];
11
+ var focusedIndex = -1;
12
+ var longPress = null;
13
+ function registerContextMenu(id, items) {
14
+ registry.set(id, items);
15
+ }
16
+ function defaultItems() {
17
+ return [
18
+ {
19
+ label: "\u540E\u9000",
20
+ shortcut: "Alt \u2190",
21
+ disabled: window.history.length <= 1,
22
+ onClick: () => window.history.back()
23
+ },
24
+ { label: "\u524D\u8FDB", shortcut: "Alt \u2192", onClick: () => window.history.forward() },
25
+ { type: "separator" },
26
+ { label: "\u5237\u65B0", shortcut: "Ctrl R", onClick: () => window.location.reload() },
27
+ {
28
+ label: "\u590D\u5236\u9875\u9762\u94FE\u63A5",
29
+ onClick: () => {
30
+ void navigator.clipboard?.writeText(window.location.href);
31
+ }
32
+ }
33
+ ];
34
+ }
35
+ function resolveItems(target) {
36
+ const host = target?.closest("[data-context-menu]");
37
+ const id = host?.dataset.contextMenu;
38
+ if (id) {
39
+ const items = registry.get(id);
40
+ if (items) return items;
41
+ }
42
+ return fallbackItems ?? defaultItems();
43
+ }
44
+ function isEditable(target) {
45
+ return Boolean(target?.closest('input, textarea, [contenteditable="true"]'));
46
+ }
47
+ function buildItem(item) {
48
+ if ("type" in item && item.type === "separator") {
49
+ const sep = document.createElement("div");
50
+ sep.className = "menu-separator";
51
+ sep.setAttribute("role", "separator");
52
+ return sep;
53
+ }
54
+ if ("type" in item && item.type === "header") {
55
+ const header = document.createElement("div");
56
+ header.className = "menu-header";
57
+ const label2 = document.createElement("span");
58
+ label2.className = "menu-header-label";
59
+ label2.textContent = item.label;
60
+ header.appendChild(label2);
61
+ if (item.description) {
62
+ const desc = document.createElement("span");
63
+ desc.className = "menu-header-desc";
64
+ desc.textContent = item.description;
65
+ header.appendChild(desc);
66
+ }
67
+ return header;
68
+ }
69
+ const entry = item;
70
+ const btn = document.createElement("button");
71
+ btn.type = "button";
72
+ btn.className = entry.danger ? "menu-item pressable menu-item--danger" : "menu-item pressable";
73
+ if (entry.disabled) btn.disabled = true;
74
+ btn.setAttribute("role", "menuitem");
75
+ btn.tabIndex = -1;
76
+ const icon = document.createElement("span");
77
+ icon.className = "menu-item-icon";
78
+ if (entry.icon) icon.innerHTML = entry.icon;
79
+ btn.appendChild(icon);
80
+ const label = document.createElement("span");
81
+ label.className = "menu-item-label";
82
+ label.textContent = entry.label;
83
+ if (entry.subLabel) {
84
+ const sub = document.createElement("span");
85
+ sub.className = "menu-item-sub";
86
+ sub.textContent = entry.subLabel;
87
+ label.appendChild(sub);
88
+ }
89
+ btn.appendChild(label);
90
+ if (entry.shortcut) {
91
+ const shortcut = document.createElement("span");
92
+ shortcut.className = "menu-item-shortcut";
93
+ shortcut.textContent = entry.shortcut;
94
+ btn.appendChild(shortcut);
95
+ }
96
+ btn.addEventListener("click", () => {
97
+ entry.onClick?.();
98
+ closeMenu();
99
+ });
100
+ return btn;
101
+ }
102
+ function openContextMenu(x, y, items) {
103
+ if (typeof document === "undefined") return;
104
+ closeMenu();
105
+ panel = document.createElement("div");
106
+ panel.className = "menu menu--context";
107
+ panel.setAttribute("role", "menu");
108
+ panel.style.visibility = "hidden";
109
+ for (const item of items) panel.appendChild(buildItem(item));
110
+ document.body.appendChild(panel);
111
+ const rect = panel.getBoundingClientRect();
112
+ const vw = window.innerWidth;
113
+ const vh = window.innerHeight;
114
+ let nx = x;
115
+ let ny = y;
116
+ if (x + rect.width > vw - FLIP_MARGIN) nx = x - rect.width;
117
+ if (y + rect.height > vh - FLIP_MARGIN) ny = y - rect.height;
118
+ if (nx < CLAMP_MARGIN) nx = CLAMP_MARGIN;
119
+ if (ny < CLAMP_MARGIN) ny = CLAMP_MARGIN;
120
+ panel.style.left = `${nx}px`;
121
+ panel.style.top = `${ny}px`;
122
+ panel.style.visibility = "";
123
+ focusableItems = Array.from(panel.querySelectorAll(".menu-item:not(:disabled)"));
124
+ focusedIndex = -1;
125
+ requestAnimationFrame(() => {
126
+ if (!panel || focusableItems.length === 0) return;
127
+ setFocused(0);
128
+ });
129
+ document.addEventListener("pointerdown", onOutsidePointerDown, true);
130
+ document.addEventListener("scroll", onCloseSignal, true);
131
+ window.addEventListener("resize", onCloseSignal);
132
+ window.addEventListener("keydown", onKeyDown);
133
+ }
134
+ function closeMenu() {
135
+ if (!panel) return;
136
+ panel.remove();
137
+ panel = null;
138
+ focusableItems = [];
139
+ focusedIndex = -1;
140
+ document.removeEventListener("pointerdown", onOutsidePointerDown, true);
141
+ document.removeEventListener("scroll", onCloseSignal, true);
142
+ window.removeEventListener("resize", onCloseSignal);
143
+ window.removeEventListener("keydown", onKeyDown);
144
+ }
145
+ function setFocused(next) {
146
+ if (!panel || focusableItems.length === 0) return;
147
+ focusedIndex = (next % focusableItems.length + focusableItems.length) % focusableItems.length;
148
+ focusableItems[focusedIndex].focus();
149
+ }
150
+ function onOutsidePointerDown(e) {
151
+ if (!panel) return;
152
+ const target = e.target;
153
+ if (!(target instanceof Node) || !panel.contains(target)) closeMenu();
154
+ }
155
+ function onCloseSignal() {
156
+ closeMenu();
157
+ }
158
+ function onKeyDown(e) {
159
+ if (!panel) return;
160
+ if (e.key === "Escape") {
161
+ e.preventDefault();
162
+ closeMenu();
163
+ return;
164
+ }
165
+ if (focusableItems.length === 0) return;
166
+ if (e.key === "ArrowDown") {
167
+ e.preventDefault();
168
+ setFocused(focusedIndex + 1);
169
+ } else if (e.key === "ArrowUp") {
170
+ e.preventDefault();
171
+ setFocused(focusedIndex - 1);
172
+ } else if (e.key === "Enter" && focusedIndex >= 0) {
173
+ e.preventDefault();
174
+ focusableItems[focusedIndex].click();
175
+ }
176
+ }
177
+ function clearLongPress() {
178
+ if (!longPress) return;
179
+ window.clearTimeout(longPress.timer);
180
+ longPress = null;
181
+ }
182
+ function openForTarget(target, x, y) {
183
+ openContextMenu(x, y, resolveItems(target));
184
+ }
185
+ function initContextMenu(opts = {}) {
186
+ if (typeof document === "undefined") return;
187
+ if (opts.items) fallbackItems = opts.items;
188
+ if (initialized) return;
189
+ initialized = true;
190
+ document.addEventListener("contextmenu", (e) => {
191
+ const target = e.target instanceof Element ? e.target : null;
192
+ if (isEditable(target)) return;
193
+ e.preventDefault();
194
+ openForTarget(target, e.clientX, e.clientY);
195
+ });
196
+ document.addEventListener("pointerdown", (e) => {
197
+ if (e.pointerType !== "touch") return;
198
+ const target = e.target instanceof Element ? e.target : null;
199
+ if (!target || isEditable(target)) return;
200
+ clearLongPress();
201
+ const startX = e.clientX;
202
+ const startY = e.clientY;
203
+ const timer = window.setTimeout(() => {
204
+ longPress = null;
205
+ openForTarget(target, startX, startY);
206
+ }, LONG_PRESS_DELAY);
207
+ longPress = { timer, x: startX, y: startY, target };
208
+ });
209
+ document.addEventListener("pointermove", (e) => {
210
+ if (!longPress) return;
211
+ if (Math.hypot(e.clientX - longPress.x, e.clientY - longPress.y) > LONG_PRESS_MOVE_LIMIT) {
212
+ clearLongPress();
213
+ }
214
+ });
215
+ document.addEventListener("pointerup", clearLongPress);
216
+ document.addEventListener("pointercancel", clearLongPress);
217
+ }
218
+ export {
219
+ closeMenu,
220
+ initContextMenu,
221
+ openContextMenu,
222
+ registerContextMenu
223
+ };