@luxalgo/vela 0.6.1 → 0.6.3

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 (44) hide show
  1. package/dist/{DataProvider-BNKtYU5V.d.cts → DataProvider-CNmk84SH.d.cts} +14 -0
  2. package/dist/{DataProvider-gJjN_0eh.d.ts → DataProvider-DHX4x6-r.d.ts} +14 -0
  3. package/dist/{chunk-4MQEG67Z.js → chunk-7D6YIF34.js} +2135 -2452
  4. package/dist/{chunk-ZNL2X6U2.js → chunk-7Y7CJ7EN.js} +1 -1
  5. package/dist/{chunk-EMS6PO2T.js → chunk-PN5KWFZ4.js} +6 -1
  6. package/dist/chunk-QHZ7IXFL.js +2598 -0
  7. package/dist/chunk-TP56QRMK.js +503 -0
  8. package/dist/{chunk-6LZJRO2S.js → chunk-U5KCQHAC.js} +108 -5
  9. package/dist/{contributions-D64UA74H.d.cts → contributions-DDpv7hEL.d.cts} +5 -1
  10. package/dist/{contributions-lPAgo9Cu.d.ts → contributions-ciV6Neyd.d.ts} +5 -1
  11. package/dist/{history-CVoFDJ2D.d.ts → history-CFqZm5re.d.ts} +2 -2
  12. package/dist/{history-fSMXVLt1.d.cts → history-ChoT34nz.d.cts} +2 -2
  13. package/dist/index.cjs +3708 -2730
  14. package/dist/index.d.cts +4 -4
  15. package/dist/index.d.ts +4 -4
  16. package/dist/index.js +4 -4
  17. package/dist/{plugin-o5HJH46Z.d.ts → plugin-C97gUCVf.d.ts} +6 -2
  18. package/dist/{plugin-D5ni4WEJ.d.cts → plugin-CSb_sTiN.d.cts} +6 -2
  19. package/dist/plugin.d.cts +3 -3
  20. package/dist/plugin.d.ts +3 -3
  21. package/dist/plugin.js +2 -2
  22. package/dist/providers/binance.d.cts +1 -1
  23. package/dist/providers/binance.d.ts +1 -1
  24. package/dist/providers/coinbase.d.cts +1 -1
  25. package/dist/providers/coinbase.d.ts +1 -1
  26. package/dist/providers/hyperliquid.d.cts +1 -1
  27. package/dist/providers/hyperliquid.d.ts +1 -1
  28. package/dist/ui.cjs +1001 -247
  29. package/dist/ui.d.cts +257 -10
  30. package/dist/ui.d.ts +257 -10
  31. package/dist/ui.js +3 -3
  32. package/dist/vela.global.js +12345 -3952
  33. package/dist/vela.global.min.js +479 -124
  34. package/dist/widget.cjs +3388 -2865
  35. package/dist/widget.d.cts +7 -5
  36. package/dist/widget.d.ts +7 -5
  37. package/dist/widget.js +13 -9
  38. package/dist/workspace.cjs +3051 -2528
  39. package/dist/workspace.d.cts +6 -4
  40. package/dist/workspace.d.ts +6 -4
  41. package/dist/workspace.js +11 -7
  42. package/package.json +1 -1
  43. package/dist/chunk-OGRQW3M6.js +0 -1328
  44. package/dist/chunk-QWIEKZRE.js +0 -1042
@@ -0,0 +1,2598 @@
1
+ import { overlayScrollbarCss, injectStyles, isDarkColor, mix, HIGHLIGHT, ACCENT_BRIGHT, ACCENT, withAlpha, iconEl, BULLISH, BEARISH, WARNING, NEUTRAL } from './chunk-PN5KWFZ4.js';
2
+ import { VanillaMachine, normalizeProps, spreadProps } from '@zag-js/vanilla';
3
+ export { normalizeProps, spreadProps } from '@zag-js/vanilla';
4
+ import * as menu from '@zag-js/menu';
5
+ import * as dialog from '@zag-js/dialog';
6
+
7
+ function runMachine(machine3, props, render) {
8
+ const m = new VanillaMachine(machine3, props);
9
+ const unsub = m.subscribe(render);
10
+ m.start();
11
+ render(m.service);
12
+ return {
13
+ service: m.service,
14
+ flush: () => render(m.service),
15
+ stop: () => {
16
+ unsub();
17
+ m.stop();
18
+ }
19
+ };
20
+ }
21
+ var uid = 0;
22
+ function nextUid(prefix) {
23
+ return `${prefix}-${++uid}`;
24
+ }
25
+ function menuController(opts) {
26
+ return {
27
+ machine: menu.machine,
28
+ props: {
29
+ id: opts.id ?? nextUid("vela-menu"),
30
+ ids: opts.triggerId ? { trigger: opts.triggerId } : void 0,
31
+ positioning: {
32
+ placement: opts.placement ?? "bottom-start",
33
+ ...opts.getAnchorRect ? { getAnchorRect: () => opts.getAnchorRect() } : {}
34
+ },
35
+ onSelect: (d) => opts.onSelect?.(d.value),
36
+ onOpenChange: (d) => opts.onOpenChange?.(d.open)
37
+ },
38
+ connect: (service) => menu.connect(service, normalizeProps)
39
+ };
40
+ }
41
+
42
+ // src/ui/components/menu/styles.ts
43
+ var MENU_STYLE_ID = "vela-ui-menu";
44
+ var MENU_CSS = `
45
+ .vela-menu {
46
+ /* The list is a <ul>: without this, block rows (the separator) paint a ::marker dot. */
47
+ list-style: none;
48
+ margin: 0;
49
+ background: var(--vela-surface-elev);
50
+ color: var(--vela-fg);
51
+ border: 1px solid var(--vela-border-strong);
52
+ border-radius: 6px;
53
+ box-shadow: var(--vela-shadow);
54
+ padding: 6px;
55
+ min-width: 180px;
56
+ max-height: 60vh;
57
+ overflow-y: auto;
58
+ font-size: 13px;
59
+ z-index: var(--vela-z-menu);
60
+ outline: none;
61
+ }
62
+ .vela-menu[data-state='open'] { animation: vela-menu-in var(--vela-dur-fast) var(--vela-ease); }
63
+ @keyframes vela-menu-in {
64
+ from { opacity: 0; transform: translateY(-3px); }
65
+ to { opacity: 1; transform: translateY(0); }
66
+ }
67
+ .vela-menu-item {
68
+ display: flex;
69
+ align-items: center;
70
+ gap: 10px;
71
+ padding: 7px 10px;
72
+ border-radius: 4px;
73
+ cursor: pointer;
74
+ user-select: none;
75
+ white-space: nowrap;
76
+ }
77
+ .vela-menu-item[data-highlighted] { background: var(--vela-hover); }
78
+ .vela-menu-item[data-disabled] { opacity: 0.4; cursor: default; }
79
+ .vela-menu-item .vela-icon { width: 16px; height: 16px; font-size: 16px; justify-content: center; color: var(--vela-fg-muted); }
80
+ .vela-menu-item .vela-menu-label { flex: 1 1 auto; }
81
+ .vela-menu-item .vela-menu-hint { color: var(--vela-fg-faint); font-size: var(--vela-font-size-sm); }
82
+ /* Active entry: the row surface carries the selection \u2014 a stronger background wash
83
+ than the hover one, plus bright ink. Declared after the hover rule so a selected
84
+ row stays visibly selected while highlighted. */
85
+ .vela-menu-item[data-checked] {
86
+ background: var(--vela-hover-strong);
87
+ color: var(--vela-fg-bright);
88
+ }
89
+ /* Switch rows (boolean settings in a dropdown): a right-aligned toggle pill \u2014 the
90
+ same control language as the settings dialog's toggles. */
91
+ .vela-menu-switch {
92
+ order: 99;
93
+ margin-left: auto;
94
+ position: relative;
95
+ flex: none;
96
+ width: 34px;
97
+ height: 18px;
98
+ border-radius: 9px;
99
+ background: var(--vela-surface-overlay);
100
+ border: 1px solid var(--vela-border-soft);
101
+ transition: background 0.16s ease, border-color 0.16s ease;
102
+ }
103
+ .vela-menu-switch::after {
104
+ content: '';
105
+ position: absolute;
106
+ top: 2px;
107
+ left: 2px;
108
+ width: 12px;
109
+ height: 12px;
110
+ border-radius: 50%;
111
+ background: var(--vela-fg-muted);
112
+ transition: transform 0.16s ease, background 0.16s ease;
113
+ }
114
+ .vela-menu-switch.on { background: var(--vela-accent-bright, var(--vela-accent)); border-color: var(--vela-accent-bright, var(--vela-accent)); }
115
+ .vela-menu-switch.on::after { transform: translateX(16px); background: var(--vela-bg); }
116
+ /* Favorite star (rows carrying \`favorite\`): reserved space always \u2014 it fades in on row
117
+ hover instead of shifting the layout \u2014 and a starred row keeps its filled star visible.
118
+ Same control language as the drawing toolbar's flyout stars. */
119
+ .vela-menu-item .vela-menu-star {
120
+ order: 98;
121
+ /* Pad the 16px icon slot into a bigger hit target without moving the row's layout. */
122
+ margin: -3px -5px -3px 0;
123
+ padding: 3px 5px;
124
+ box-sizing: content-box;
125
+ border-radius: var(--vela-radius-sm);
126
+ opacity: 0;
127
+ transition: opacity 0.1s ease, background 0.1s ease;
128
+ }
129
+ .vela-menu-item[data-highlighted] .vela-menu-star { opacity: 0.55; }
130
+ .vela-menu-item .vela-menu-star:hover { opacity: 1; background: var(--vela-hover-strong); }
131
+ .vela-menu-item .vela-menu-star.vela-fav { opacity: 0.95; color: var(--vela-highlight); }
132
+ .vela-menu-sep { height: 1px; margin: 4px 6px; background: var(--vela-border); }
133
+ /* Submenu trigger row: a right-aligned chevron, and it stays highlighted while its own
134
+ list is open so the path you came down remains readable. */
135
+ .vela-menu-item .vela-menu-arrow {
136
+ order: 99;
137
+ margin-left: auto;
138
+ width: 12px;
139
+ flex: none;
140
+ font-size: 11px;
141
+ color: var(--vela-fg-faint);
142
+ }
143
+ .vela-menu-item[data-state='open'] { background: var(--vela-hover); }
144
+ `;
145
+ var Surface = class _Surface {
146
+ constructor(doc, opts) {
147
+ this.items = [];
148
+ /** Branch item id → the surface it opens. */
149
+ this.subs = /* @__PURE__ */ new Map();
150
+ /** Trigger rect captured when this level opened — the list stays put if the
151
+ * trigger then moves (favorite chips shifting the caret). */
152
+ this.pinnedAnchor = null;
153
+ this.doc = doc;
154
+ this.host = opts.host;
155
+ this.onSelect = opts.onSelect;
156
+ this.onFavorite = opts.onFavorite;
157
+ this.positioner = doc.createElement("div");
158
+ this.positioner.className = "vela-ui-layer";
159
+ this.list = doc.createElement("ul");
160
+ this.list.className = "vela-menu";
161
+ if (opts.minWidth) this.list.style.minWidth = opts.minWidth;
162
+ this.positioner.appendChild(this.list);
163
+ this.host.appendChild(this.positioner);
164
+ const trigger = opts.trigger;
165
+ this.ctrl = menuController({
166
+ items: [],
167
+ id: opts.id,
168
+ placement: opts.placement,
169
+ onSelect: (id) => this.onSelect(id),
170
+ getAnchorRect: () => this.pinnedAnchor,
171
+ onOpenChange: (open2) => {
172
+ if (open2 && trigger) {
173
+ const r = trigger.getBoundingClientRect();
174
+ this.pinnedAnchor = { x: r.x, y: r.y, width: r.width, height: r.height };
175
+ } else {
176
+ this.pinnedAnchor = null;
177
+ }
178
+ opts.onOpenChange?.(open2);
179
+ },
180
+ triggerId: opts.triggerId
181
+ });
182
+ this.mid = String(this.ctrl.props.id);
183
+ if (opts.triggerId && trigger) trigger.id = opts.triggerId;
184
+ this.handle = runMachine(this.ctrl.machine, this.ctrl.props, (service) => {
185
+ const api = this.ctrl.connect(service);
186
+ if (trigger) spreadProps(trigger, api.getTriggerProps(), this.mid);
187
+ spreadProps(this.positioner, api.getPositionerProps(), this.mid);
188
+ spreadProps(this.list, api.getContentProps(), this.mid);
189
+ this.project(api);
190
+ });
191
+ }
192
+ get api() {
193
+ return this.ctrl.connect(this.handle.service);
194
+ }
195
+ setItems(items) {
196
+ this.items = items;
197
+ for (const sub of this.subs.values()) sub.destroy();
198
+ this.subs.clear();
199
+ this.build();
200
+ const api = this.api;
201
+ for (const sub of this.subs.values()) {
202
+ api.setChild(sub.handle.service);
203
+ sub.api.setParent(this.handle.service);
204
+ }
205
+ this.handle.flush();
206
+ if (this.subs.size > 0) queueMicrotask(() => this.handle.flush());
207
+ }
208
+ destroy() {
209
+ for (const sub of this.subs.values()) sub.destroy();
210
+ this.subs.clear();
211
+ this.handle.stop();
212
+ this.positioner.remove();
213
+ }
214
+ byId(id) {
215
+ return this.items.find((i) => i.id === id);
216
+ }
217
+ /** Push the machine's item props onto the rendered rows. A branch takes the submenu
218
+ * trigger props, which carry the child's ids and hover handlers; a leaf takes plain ones. */
219
+ project(api) {
220
+ for (const li of this.list.children) {
221
+ const id = li.dataset.veiId;
222
+ if (!id) continue;
223
+ const sub = this.subs.get(id);
224
+ if (sub) {
225
+ spreadProps(li, api.getTriggerItemProps(sub.api), this.mid);
226
+ continue;
227
+ }
228
+ const item = this.byId(id);
229
+ spreadProps(li, api.getItemProps({ value: id, disabled: item?.disabled, closeOnSelect: item?.toggle ? false : void 0 }), this.mid);
230
+ }
231
+ }
232
+ build() {
233
+ const doc = this.doc;
234
+ this.list.replaceChildren();
235
+ for (const item of this.items) {
236
+ if (item.separatorBefore) {
237
+ const sep = doc.createElement("li");
238
+ sep.className = "vela-menu-sep";
239
+ sep.setAttribute("role", "separator");
240
+ this.list.appendChild(sep);
241
+ }
242
+ const branch = item.submenu !== void 0 && item.submenu.length > 0;
243
+ const li = doc.createElement("li");
244
+ li.className = "vela-menu-item";
245
+ li.dataset.veiId = item.id;
246
+ if (item.toggle) {
247
+ li.dataset.toggle = "1";
248
+ } else if (!branch && item.checked) {
249
+ li.dataset.checked = "1";
250
+ }
251
+ if (item.icon) li.appendChild(iconEl(item.icon, doc));
252
+ const label = doc.createElement("span");
253
+ label.className = "vela-menu-label";
254
+ label.textContent = item.label;
255
+ li.appendChild(label);
256
+ if (item.hint) {
257
+ const hint = doc.createElement("span");
258
+ hint.className = "vela-menu-hint";
259
+ hint.textContent = item.hint;
260
+ li.appendChild(hint);
261
+ }
262
+ if (item.favorite !== void 0 && this.onFavorite) {
263
+ const on = item.favorite;
264
+ const star = iconEl(on ? "star-filled" : "star", doc);
265
+ star.classList.add("vela-menu-star");
266
+ if (on) star.classList.add("vela-fav");
267
+ star.removeAttribute("aria-hidden");
268
+ star.setAttribute("role", "button");
269
+ star.setAttribute("aria-label", on ? "Remove from favorites" : "Add to favorites");
270
+ star.setAttribute("aria-pressed", on ? "true" : "false");
271
+ for (const ev of ["pointerdown", "pointerup", "mousedown", "mouseup"]) {
272
+ star.addEventListener(ev, (e) => e.stopPropagation());
273
+ }
274
+ star.addEventListener("click", (e) => {
275
+ e.stopPropagation();
276
+ e.preventDefault();
277
+ this.onFavorite?.(item.id, !on);
278
+ });
279
+ li.appendChild(star);
280
+ }
281
+ if (item.toggle) {
282
+ const sw = doc.createElement("span");
283
+ sw.className = "vela-menu-switch" + (item.checked ? " on" : "");
284
+ sw.setAttribute("aria-hidden", "true");
285
+ li.appendChild(sw);
286
+ }
287
+ if (branch) {
288
+ li.dataset.branch = "1";
289
+ const arrow = iconEl("chevron-right", doc);
290
+ arrow.classList.add("vela-menu-arrow");
291
+ li.appendChild(arrow);
292
+ const sub = new _Surface(doc, {
293
+ host: this.host,
294
+ placement: "right-start",
295
+ onSelect: this.onSelect,
296
+ onFavorite: this.onFavorite,
297
+ id: `${this.mid}--${item.id}`
298
+ });
299
+ sub.setItems(item.submenu ?? []);
300
+ this.subs.set(item.id, sub);
301
+ }
302
+ this.list.appendChild(li);
303
+ }
304
+ }
305
+ };
306
+ var Menu = class {
307
+ constructor(opts) {
308
+ const anchor = opts.trigger ?? opts.host ?? document.body;
309
+ const doc = anchor.ownerDocument;
310
+ injectStyles(MENU_STYLE_ID, MENU_CSS, doc);
311
+ const host = opts.host ?? anchor.closest?.(".vela-ui") ?? doc.body;
312
+ this.root = new Surface(doc, {
313
+ host,
314
+ placement: opts.placement,
315
+ onSelect: (id) => opts.onSelect?.(id),
316
+ onOpenChange: opts.onOpenChange,
317
+ trigger: opts.trigger,
318
+ triggerId: opts.triggerId,
319
+ id: opts.id,
320
+ minWidth: opts.minWidth,
321
+ onFavorite: opts.onFavorite
322
+ });
323
+ this.root.setItems(opts.items);
324
+ }
325
+ get api() {
326
+ return this.root.api;
327
+ }
328
+ open() {
329
+ this.api.setOpen(true);
330
+ }
331
+ /** Open anchored to a viewport point (context menus). */
332
+ openAt(clientX, clientY) {
333
+ const api = this.api;
334
+ api.setOpen(true);
335
+ api.reposition({ getAnchorRect: () => ({ x: clientX, y: clientY, width: 0, height: 0 }) });
336
+ }
337
+ close() {
338
+ this.api.setOpen(false);
339
+ }
340
+ /** Swap the item descriptors (e.g. checked states) and re-project. */
341
+ setItems(items) {
342
+ this.root.setItems(items);
343
+ }
344
+ destroy() {
345
+ this.root.destroy();
346
+ }
347
+ };
348
+
349
+ // src/core/tokens.ts
350
+ var STATIC_TOKENS = {
351
+ "--vela-space-1": "4px",
352
+ "--vela-space-2": "8px",
353
+ "--vela-space-3": "12px",
354
+ "--vela-space-4": "16px",
355
+ "--vela-radius-sm": "4px",
356
+ "--vela-radius-md": "6px",
357
+ "--vela-radius-lg": "10px",
358
+ "--vela-z-tooltip": "60",
359
+ "--vela-z-menu": "50",
360
+ "--vela-z-dialog": "40",
361
+ // Form popovers portal to <body> (or a chart host) and must sit above a dialog
362
+ // whose own stacking context may be nested inside the chart container.
363
+ "--vela-z-popover": "6000",
364
+ "--vela-ease": "cubic-bezier(0.22, 1, 0.36, 1)",
365
+ "--vela-dur-fast": "90ms",
366
+ "--vela-dur-med": "160ms",
367
+ "--vela-font-size-sm": "11px",
368
+ "--vela-font-size-md": "12px",
369
+ "--vela-font-size-lg": "14px"
370
+ };
371
+ function themeTokens(t) {
372
+ const dark = isDarkColor(t.background);
373
+ const wash = (a) => dark ? `rgba(255,255,255,${a})` : withAlpha(t.textColor, a + 0.02);
374
+ const elevated = mix(t.background, dark ? "#ffffff" : t.textColor, dark ? 0.03 : 0.05);
375
+ return {
376
+ "--vela-font": t.fontFamily,
377
+ "--vela-bg": t.background,
378
+ // Chrome text is slightly brighter than the chart's own axis text, which is
379
+ // deliberately recessive; the chart keeps using `t.textColor` directly.
380
+ "--vela-fg": dark ? "#d1d4dc" : t.textColor,
381
+ "--vela-fg-muted": dark ? "#868a96" : withAlpha(t.textColor, 0.62),
382
+ "--vela-fg-faint": withAlpha(t.textColor, 0.35),
383
+ "--vela-fg-bright": dark ? "#f0f3fa" : "#000000",
384
+ "--vela-surface": t.background,
385
+ // Panels and menus float ABOVE the chart, so their surface is the wash flattened onto
386
+ // the chart background — opaque, or candles read through the panel.
387
+ "--vela-surface-elev": elevated,
388
+ "--vela-surface-overlay": elevated,
389
+ // Recessed fields (inputs, selects) read as cut INTO their panel, so they fall back
390
+ // to the chart surface and are separated from the panel by their border alone.
391
+ "--vela-surface-sunken": t.background,
392
+ "--vela-border": t.borderColor,
393
+ "--vela-border-strong": dark ? "#34353b" : withAlpha(t.textColor, 0.28),
394
+ "--vela-border-soft": t.borderColor,
395
+ // Barely-there rules INSIDE a panel (row separators), where a full border would
396
+ // chop the list into boxes.
397
+ "--vela-border-faint": withAlpha(t.textColor, 0.08),
398
+ "--vela-hover": wash(0.06),
399
+ "--vela-active": wash(0.1),
400
+ // A deliberately stronger hover for rows inside an already-tinted surface (menu
401
+ // items in an active flyout), where the normal wash would not separate from it.
402
+ "--vela-hover-strong": wash(0.16),
403
+ "--vela-focus": withAlpha(t.textColor, 0.5),
404
+ "--vela-focus-soft": withAlpha(t.textColor, 0.12),
405
+ // Separator hover — the SAME wash the chart's pane separators paint on hover
406
+ // (soft full-thickness band + solid 2px center line), so DOM-drawn dividers
407
+ // (the workspace grid) and canvas-drawn ones read as one family.
408
+ "--vela-separator-hover-band": withAlpha(t.textColor, 0.1),
409
+ "--vela-separator-hover-line": withAlpha(t.textColor, 0.55),
410
+ "--vela-scroll": withAlpha(t.textColor, 0.3),
411
+ "--vela-accent": ACCENT,
412
+ "--vela-accent-bright": ACCENT_BRIGHT,
413
+ "--vela-highlight": HIGHLIGHT,
414
+ // The inverse chip: a filled selected state (active tab, ticked checkbox). Its ink
415
+ // must contrast the fill, so the pair flips together with the theme.
416
+ "--vela-selected-bg": dark ? "#f0f3fa" : t.textColor,
417
+ "--vela-selected-fg": dark ? t.background : "#ffffff",
418
+ // Fixed ink for saturated fills (accent buttons, categorical avatars) — those fills
419
+ // are theme-independent, so their ink is too.
420
+ "--vela-fg-on-fill": "#ffffff",
421
+ "--vela-up": t.upColor,
422
+ "--vela-down": t.downColor,
423
+ "--vela-danger": t.downColor,
424
+ "--vela-shadow": "0 8px 30px rgba(0,0,0,0.5)",
425
+ "--vela-shadow-dialog": "0 20px 60px rgba(0,0,0,0.5)",
426
+ "--vela-backdrop": "rgba(0,0,0,0.45)"
427
+ };
428
+ }
429
+
430
+ // src/ui/tokens.ts
431
+ var STATIC_ID = "vela-ui-tokens";
432
+ var STATIC_DECLS = Object.entries(STATIC_TOKENS).map(([k, v]) => ` ${k}: ${v};`).join("\n");
433
+ var STATIC_CSS = `
434
+ .vela-ui, .vela-ui-layer {
435
+ ${STATIC_DECLS}
436
+ font-family: var(--vela-font, -apple-system, system-ui, sans-serif);
437
+ box-sizing: border-box;
438
+ }
439
+ .vela-ui *, .vela-ui-layer * { box-sizing: border-box; }
440
+ .vela-icon { display: inline-flex; align-items: center; flex: none; }
441
+ .vela-icon svg { display: block; }
442
+ `;
443
+ function applyThemeTokens(el, t) {
444
+ const tokens = themeTokens(t);
445
+ for (const key in tokens) el.style.setProperty(key, tokens[key]);
446
+ }
447
+ function applyPlotOverlayTokens(host, base, config) {
448
+ const layout = config?.layout;
449
+ const t = layout?.background && layout.textColor ? { ...base, background: layout.background, textColor: layout.textColor } : base;
450
+ applyThemeTokens(host, t);
451
+ }
452
+ function ensureUIHost(el, theme) {
453
+ injectStyles(STATIC_ID, STATIC_CSS, el.getRootNode());
454
+ el.classList.add("vela-ui");
455
+ if (theme) applyThemeTokens(el, theme);
456
+ }
457
+
458
+ // src/ui/components/popover/controller.ts
459
+ function insetRect(r, inset) {
460
+ return {
461
+ left: r.left + inset,
462
+ top: r.top + inset,
463
+ right: r.right - inset,
464
+ bottom: r.bottom - inset,
465
+ width: r.width - inset * 2,
466
+ height: r.height - inset * 2
467
+ };
468
+ }
469
+ function viewportRect(width, height, inset) {
470
+ return {
471
+ left: inset,
472
+ top: inset,
473
+ right: width - inset,
474
+ bottom: height - inset,
475
+ width: width - inset * 2,
476
+ height: height - inset * 2
477
+ };
478
+ }
479
+ function intersectRects(a, b) {
480
+ const left = Math.max(a.left, b.left);
481
+ const top = Math.max(a.top, b.top);
482
+ const right = Math.min(a.right, b.right);
483
+ const bottom = Math.min(a.bottom, b.bottom);
484
+ return { left, top, right, bottom, width: right - left, height: bottom - top };
485
+ }
486
+ function placePopover(a) {
487
+ let left = a.align === "end" ? a.trigger.right - a.pop.width : a.trigger.left;
488
+ const below = a.trigger.bottom + a.gap;
489
+ const above = a.trigger.top - a.pop.height - a.gap;
490
+ const fitsBelow = below + a.pop.height <= a.clamp.bottom;
491
+ const fitsAbove = above >= a.clamp.top;
492
+ let top = below;
493
+ if (!fitsBelow && (fitsAbove || a.trigger.top - a.clamp.top > a.clamp.bottom - a.trigger.bottom)) {
494
+ top = Math.max(a.clamp.top, above);
495
+ }
496
+ if (left + a.pop.width > a.clamp.right) left = a.clamp.right - a.pop.width;
497
+ if (left < a.clamp.left) left = a.clamp.left;
498
+ if (top + a.pop.height > a.clamp.bottom) top = a.clamp.bottom - a.pop.height;
499
+ if (top < a.clamp.top) top = a.clamp.top;
500
+ return {
501
+ left: Math.round(left - a.originX),
502
+ top: Math.round(top - a.originY)
503
+ };
504
+ }
505
+ function popoverController(opts = {}) {
506
+ return {
507
+ gap: opts.gap ?? 4,
508
+ align: opts.align ?? "start",
509
+ matchWidth: opts.matchWidth ?? false,
510
+ position: opts.position ?? "fixed",
511
+ boundaryInset: opts.boundaryInset ?? 0,
512
+ viewportInset: opts.viewportInset ?? 6,
513
+ onClose: opts.onClose
514
+ };
515
+ }
516
+
517
+ // src/ui/components/popover/styles.ts
518
+ var POPOVER_STYLE_ID = "vela-ui-popover";
519
+ var POPOVER_CSS = `
520
+ .vela-popover {
521
+ position: fixed;
522
+ z-index: var(--vela-z-popover);
523
+ box-sizing: border-box;
524
+ /* Never serve as a scroll anchor: the shell is portaled and re-placed between
525
+ gestures, and anchoring against it jumps the scroller underneath. */
526
+ overflow-anchor: none;
527
+ }
528
+ .vela-popover[data-position='absolute'] { position: absolute; }
529
+ .vela-popover[hidden] { display: none !important; }
530
+ `;
531
+
532
+ // src/ui/components/popover/view.ts
533
+ var open = null;
534
+ function closeOpenPopovers() {
535
+ open?.hide();
536
+ }
537
+ function isPopoverOpen() {
538
+ return open !== null;
539
+ }
540
+ function openPopoverTrigger() {
541
+ return open?.trigger ?? null;
542
+ }
543
+ var POPOVER_DISMISS_FLAG = "__velaPopoverDismiss";
544
+ function markPopoverDismiss(e) {
545
+ e[POPOVER_DISMISS_FLAG] = true;
546
+ }
547
+ function eventDismissedPopover(e) {
548
+ return e[POPOVER_DISMISS_FLAG] === true;
549
+ }
550
+ function toRect(r) {
551
+ return { left: r.left, top: r.top, right: r.right, bottom: r.bottom, width: r.width, height: r.height };
552
+ }
553
+ var Popover = class {
554
+ constructor(opts) {
555
+ this.onOutside = null;
556
+ this.onKey = null;
557
+ this.onReflow = null;
558
+ this.shown = false;
559
+ const doc = opts.trigger.ownerDocument;
560
+ injectStyles(POPOVER_STYLE_ID, POPOVER_CSS, doc);
561
+ this.trigger = opts.trigger;
562
+ this.host = opts.host ?? doc.body;
563
+ this.ctrl = popoverController(opts);
564
+ this.boundary = opts.boundary ?? "viewport";
565
+ this.theme = opts.theme;
566
+ this.el = doc.createElement("div");
567
+ this.el.className = "vela-popover vela-ui-layer" + (opts.className ? ` ${opts.className}` : "");
568
+ this.el.dataset.position = this.ctrl.position;
569
+ if (opts.zIndex !== void 0) this.el.style.zIndex = String(opts.zIndex);
570
+ this.el.addEventListener("pointerdown", (e) => e.stopPropagation());
571
+ if (opts.content instanceof Node) this.el.appendChild(opts.content);
572
+ else if (typeof opts.content === "function") opts.content(this.el);
573
+ }
574
+ get open() {
575
+ return this.shown;
576
+ }
577
+ get position() {
578
+ return this.ctrl.position;
579
+ }
580
+ get align() {
581
+ return this.ctrl.align;
582
+ }
583
+ show() {
584
+ if (this.shown) {
585
+ this.place();
586
+ return;
587
+ }
588
+ if (open && open !== this) open.hide();
589
+ ensureUIHost(this.el, this.theme);
590
+ this.host.appendChild(this.el);
591
+ this.shown = true;
592
+ open = this;
593
+ this.place();
594
+ const onOutside = (ev) => {
595
+ const t = ev.target;
596
+ if (this.el.contains(t) || this.trigger.contains(t)) return;
597
+ markPopoverDismiss(ev);
598
+ this.hide();
599
+ };
600
+ const onKey = (ev) => {
601
+ if (ev.key !== "Escape") return;
602
+ ev.preventDefault();
603
+ ev.stopPropagation();
604
+ this.hide();
605
+ };
606
+ const onReflow = () => this.place();
607
+ setTimeout(() => document.addEventListener("pointerdown", onOutside, true), 0);
608
+ document.addEventListener("keydown", onKey, true);
609
+ window.addEventListener("resize", onReflow, true);
610
+ document.addEventListener("scroll", onReflow, true);
611
+ this.onOutside = onOutside;
612
+ this.onKey = onKey;
613
+ this.onReflow = onReflow;
614
+ }
615
+ hide() {
616
+ if (!this.shown) return;
617
+ if (this.onOutside) document.removeEventListener("pointerdown", this.onOutside, true);
618
+ if (this.onKey) document.removeEventListener("keydown", this.onKey, true);
619
+ if (this.onReflow) {
620
+ window.removeEventListener("resize", this.onReflow, true);
621
+ document.removeEventListener("scroll", this.onReflow, true);
622
+ }
623
+ this.onOutside = null;
624
+ this.onKey = null;
625
+ this.onReflow = null;
626
+ this.el.remove();
627
+ this.shown = false;
628
+ if (open === this) open = null;
629
+ this.ctrl.onClose?.();
630
+ }
631
+ /** Show if closed, hide if this instance is the open popover. */
632
+ toggle() {
633
+ if (this.shown) this.hide();
634
+ else this.show();
635
+ }
636
+ destroy() {
637
+ this.hide();
638
+ }
639
+ reposition() {
640
+ if (this.shown) this.place();
641
+ }
642
+ clampRect() {
643
+ const view = viewportRect(window.innerWidth, window.innerHeight, this.ctrl.viewportInset);
644
+ const bound = this.readBoundary();
645
+ if (!bound) return view;
646
+ const inset = insetRect(bound, this.ctrl.boundaryInset);
647
+ return intersectRects(view, inset);
648
+ }
649
+ readBoundary() {
650
+ const b = this.boundary;
651
+ if (b === "viewport") return null;
652
+ const raw = typeof b === "function" ? b() : b.getBoundingClientRect();
653
+ return raw ? toRect(raw) : null;
654
+ }
655
+ place() {
656
+ const ar = this.trigger.getBoundingClientRect();
657
+ if (this.ctrl.matchWidth) {
658
+ this.el.style.minWidth = `${Math.round(Math.max(this.el.offsetWidth, ar.width))}px`;
659
+ }
660
+ const origin = this.ctrl.position === "absolute" ? this.host.getBoundingClientRect() : { left: 0, top: 0 };
661
+ const pos = placePopover({
662
+ trigger: toRect(ar),
663
+ pop: { width: this.el.offsetWidth, height: this.el.offsetHeight },
664
+ gap: this.ctrl.gap,
665
+ align: this.ctrl.align,
666
+ clamp: this.clampRect(),
667
+ originX: origin.left,
668
+ originY: origin.top
669
+ });
670
+ this.el.style.left = `${pos.left}px`;
671
+ this.el.style.top = `${pos.top}px`;
672
+ }
673
+ };
674
+
675
+ // src/ui/components/select/controller.ts
676
+ function selectController(opts) {
677
+ const options = opts.options;
678
+ let value = opts.value ?? options[0]?.value ?? "";
679
+ const size = opts.size ?? "md";
680
+ return {
681
+ options,
682
+ get value() {
683
+ return value;
684
+ },
685
+ size,
686
+ fill: opts.fill ?? size !== "sm",
687
+ disabled: opts.disabled ?? false,
688
+ setValue(v) {
689
+ value = v;
690
+ },
691
+ labelOf(v) {
692
+ return options.find((o) => o.value === v)?.label ?? v;
693
+ },
694
+ pick(v) {
695
+ value = v;
696
+ const label = options.find((o) => o.value === v)?.label ?? v;
697
+ opts.onChange?.(v, label);
698
+ return { value: v, label };
699
+ }
700
+ };
701
+ }
702
+
703
+ // src/ui/components/select/styles.ts
704
+ var SELECT_STYLE_ID = "vela-ui-select-3";
705
+ var SELECT_CSS = `
706
+ .vela-select { position: relative; display: inline-block; min-width: 0; }
707
+ .vela-select[data-fill] { width: 100%; }
708
+ /* Closed trigger matches NumberInput / TextField (100px). Long labels ellipsis;
709
+ the open list still sizes to the longest item. */
710
+ .vela-select:not([data-fill]) { width: 100px; flex: none; justify-self: start; }
711
+ .vela-select-trigger {
712
+ display: flex;
713
+ align-items: center;
714
+ width: 100%;
715
+ min-width: 0;
716
+ box-sizing: border-box;
717
+ height: 34px;
718
+ padding: 0 26px 0 8px;
719
+ background: transparent;
720
+ border: 1px solid var(--vela-border-strong);
721
+ border-radius: 6px;
722
+ color: var(--vela-fg-bright);
723
+ font-size: 14px;
724
+ font-family: inherit;
725
+ cursor: pointer;
726
+ text-align: left;
727
+ outline: none;
728
+ transition: border-color 0.12s ease, box-shadow 0.12s ease;
729
+ }
730
+ .vela-select-trigger:hover { border-color: var(--vela-fg-muted); }
731
+ .vela-select-trigger:focus { border-color: var(--vela-focus); box-shadow: 0 0 0 3px var(--vela-focus-soft); }
732
+ .vela-select[data-size='sm'] .vela-select-trigger {
733
+ height: 28px;
734
+ background: var(--vela-surface-elev);
735
+ border-radius: var(--vela-radius-sm);
736
+ color: var(--vela-fg);
737
+ font-size: 13px;
738
+ box-shadow: none;
739
+ }
740
+ .vela-select[data-size='sm'] .vela-select-trigger:focus { box-shadow: none; border-color: var(--vela-fg-muted); }
741
+ .vela-select-label { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
742
+ /* Compact toolbar selects still hug the widest option (no 100px kit column). */
743
+ .vela-select[data-size='sm']:not([data-fill]) { width: auto; max-width: 200px; }
744
+ .vela-select-sizer { visibility: hidden; height: 0; overflow: hidden; font-size: 13px; }
745
+ .vela-select-sizer span { display: block; height: 0; white-space: nowrap; padding: 0 26px 0 8px; border-inline: 1px solid transparent; }
746
+ .vela-select-chevron {
747
+ position: absolute;
748
+ right: 8px;
749
+ top: 50%;
750
+ transform: translateY(-50%);
751
+ pointer-events: none;
752
+ display: flex;
753
+ opacity: 0.55;
754
+ line-height: 0;
755
+ color: inherit;
756
+ }
757
+ .vela-select-chevron .vela-icon, .vela-select-chevron svg { width: 12px; height: 12px; display: block; }
758
+ .vela-select-list {
759
+ width: max-content;
760
+ background: var(--vela-bg);
761
+ color: var(--vela-fg);
762
+ border: none;
763
+ border-radius: 6px;
764
+ box-shadow: var(--vela-shadow);
765
+ font: 14px var(--vela-font);
766
+ padding: 4px;
767
+ overflow: hidden;
768
+ }
769
+ .vela-select-list[data-size='sm'] { font-size: 13px; background: var(--vela-surface-overlay); }
770
+ .vela-select-items { width: max-content; min-width: 100%; max-height: none; overflow: hidden; }
771
+ .vela-select-list.is-scroll { display: flex; align-items: stretch; gap: 2px; }
772
+ .vela-select-list.is-scroll .vela-select-items {
773
+ flex: 1 1 auto;
774
+ min-width: min-content;
775
+ max-height: 240px;
776
+ overflow-y: auto;
777
+ scrollbar-width: none;
778
+ }
779
+ .vela-select-list.is-scroll .vela-select-items::-webkit-scrollbar { display: none; width: 0; height: 0; }
780
+ .vela-select-rail { position: relative; flex: none; width: 3px; margin: 2px 1px 2px 0; pointer-events: none; }
781
+ .vela-select-thumb { width: 3px; border-radius: 2px; background: var(--vela-scroll); }
782
+ .vela-select-item {
783
+ display: block;
784
+ width: auto;
785
+ min-width: 100%;
786
+ white-space: nowrap;
787
+ text-align: left;
788
+ padding: 7px 10px;
789
+ border: none;
790
+ border-radius: 4px;
791
+ background: transparent;
792
+ color: inherit;
793
+ cursor: pointer;
794
+ font: inherit;
795
+ font-weight: 400;
796
+ }
797
+ .vela-select-item:hover { background: var(--vela-hover); }
798
+ .vela-select-item[data-checked] { background: var(--vela-hover-strong); color: var(--vela-fg-bright); }
799
+ .vela-select-item[data-checked]:hover { background: var(--vela-hover-strong); }
800
+ `;
801
+
802
+ // src/ui/components/select/view.ts
803
+ function ensureSelectStyles(doc) {
804
+ injectStyles(SELECT_STYLE_ID, SELECT_CSS, doc);
805
+ }
806
+ function syncThumb(list, thumb) {
807
+ const view = list.clientHeight;
808
+ const total = list.scrollHeight;
809
+ if (total <= view) {
810
+ thumb.style.height = "0";
811
+ return;
812
+ }
813
+ const thumbH = Math.max(20, view / total * view);
814
+ const top = list.scrollTop / (total - view) * (view - thumbH);
815
+ thumb.style.height = `${Math.round(thumbH)}px`;
816
+ thumb.style.transform = `translateY(${Math.round(top)}px`;
817
+ }
818
+ function fillSelectList(menu2, options, current, onPick) {
819
+ menu2.replaceChildren();
820
+ const list = menu2.ownerDocument.createElement("div");
821
+ list.className = "vela-select-items";
822
+ for (const p of options) {
823
+ const item = menu2.ownerDocument.createElement("button");
824
+ item.type = "button";
825
+ item.className = "vela-select-item";
826
+ item.textContent = p.label;
827
+ if (p.value === current) item.dataset.checked = "1";
828
+ item.addEventListener("click", (e) => {
829
+ e.stopPropagation();
830
+ onPick(p.value, p.label);
831
+ });
832
+ list.appendChild(item);
833
+ }
834
+ menu2.appendChild(list);
835
+ }
836
+ function decorateSelectScroll(menu2) {
837
+ const list = menu2.querySelector(".vela-select-items");
838
+ if (!list) return;
839
+ if (list.scrollHeight > 240) {
840
+ menu2.classList.add("is-scroll");
841
+ const rail = menu2.ownerDocument.createElement("div");
842
+ rail.className = "vela-select-rail";
843
+ const thumb = menu2.ownerDocument.createElement("div");
844
+ thumb.className = "vela-select-thumb";
845
+ rail.appendChild(thumb);
846
+ menu2.appendChild(rail);
847
+ list.addEventListener("scroll", () => syncThumb(list, thumb));
848
+ syncThumb(list, thumb);
849
+ }
850
+ menu2.querySelector(".vela-select-item[data-checked]")?.scrollIntoView({ block: "nearest" });
851
+ list.dispatchEvent(new Event("scroll"));
852
+ }
853
+ function toggleSelectList(trigger, options, current, onPick, opts = {}) {
854
+ if (openPopoverTrigger() === trigger) {
855
+ closeOpenPopovers();
856
+ return null;
857
+ }
858
+ return openSelectList(trigger, options, current, onPick, opts);
859
+ }
860
+ function openSelectList(trigger, options, current, onPick, opts = {}) {
861
+ ensureSelectStyles(trigger.ownerDocument);
862
+ closeOpenPopovers();
863
+ const pop = new Popover({
864
+ trigger,
865
+ theme: opts.theme,
866
+ matchWidth: opts.matchWidth ?? true,
867
+ boundary: opts.boundary,
868
+ boundaryInset: opts.boundaryInset,
869
+ gap: opts.gap ?? 4,
870
+ align: opts.align ?? "start",
871
+ host: opts.host,
872
+ position: opts.position,
873
+ zIndex: opts.zIndex,
874
+ className: "vela-select-list",
875
+ onClose: opts.onClose,
876
+ content: (el) => {
877
+ if (opts.size) el.dataset.size = opts.size;
878
+ fillSelectList(el, options, current, (value, label) => {
879
+ pop.hide();
880
+ onPick(value, label);
881
+ });
882
+ }
883
+ });
884
+ pop.show();
885
+ decorateSelectScroll(pop.el);
886
+ pop.reposition();
887
+ return pop;
888
+ }
889
+ var Select = class {
890
+ constructor(opts) {
891
+ this.list = null;
892
+ const doc = opts.list?.host?.ownerDocument ?? document;
893
+ ensureSelectStyles(doc);
894
+ this.ctrl = selectController(opts);
895
+ this.listOpts = { ...opts.list, theme: opts.theme ?? opts.list?.theme, size: this.ctrl.size };
896
+ const wrap = doc.createElement("div");
897
+ wrap.className = "vela-select";
898
+ wrap.dataset.size = this.ctrl.size;
899
+ if (this.ctrl.fill) wrap.dataset.fill = "";
900
+ const btn = doc.createElement("button");
901
+ btn.type = "button";
902
+ if (opts.id) btn.id = opts.id;
903
+ btn.className = "vela-select-trigger";
904
+ if (this.ctrl.disabled) btn.disabled = true;
905
+ const label = doc.createElement("span");
906
+ label.className = "vela-select-label";
907
+ label.textContent = this.ctrl.labelOf(this.ctrl.value);
908
+ const chevron = doc.createElement("span");
909
+ chevron.className = "vela-select-chevron";
910
+ chevron.appendChild(iconEl("chevron-down", doc));
911
+ btn.append(label, chevron);
912
+ wrap.appendChild(btn);
913
+ if (this.ctrl.size === "sm") {
914
+ const sizer = doc.createElement("div");
915
+ sizer.className = "vela-select-sizer";
916
+ sizer.setAttribute("aria-hidden", "true");
917
+ for (const o of this.ctrl.options) {
918
+ const s = doc.createElement("span");
919
+ s.textContent = o.label;
920
+ sizer.appendChild(s);
921
+ }
922
+ wrap.appendChild(sizer);
923
+ }
924
+ btn.addEventListener("pointerdown", (e) => {
925
+ e.preventDefault();
926
+ btn.focus({ preventScroll: true });
927
+ });
928
+ btn.addEventListener("click", (e) => {
929
+ e.stopPropagation();
930
+ this.toggle();
931
+ });
932
+ this.el = wrap;
933
+ this.trigger = btn;
934
+ this.labelEl = label;
935
+ }
936
+ get value() {
937
+ return this.ctrl.value;
938
+ }
939
+ setValue(v) {
940
+ this.ctrl.setValue(v);
941
+ this.labelEl.textContent = this.ctrl.labelOf(v);
942
+ }
943
+ toggle() {
944
+ this.list = toggleSelectList(
945
+ this.trigger,
946
+ this.ctrl.options,
947
+ this.ctrl.value,
948
+ (value, label) => {
949
+ this.ctrl.pick(value);
950
+ this.labelEl.textContent = label;
951
+ this.list = null;
952
+ },
953
+ { ...this.listOpts, onClose: () => {
954
+ this.list = null;
955
+ this.listOpts.onClose?.();
956
+ } }
957
+ );
958
+ }
959
+ destroy() {
960
+ this.list?.hide();
961
+ this.list = null;
962
+ }
963
+ };
964
+ function dialogController(opts = {}) {
965
+ return {
966
+ machine: dialog.machine,
967
+ props: {
968
+ id: nextUid("vela-dialog"),
969
+ modal: opts.modal ?? true,
970
+ closeOnEscape: opts.closeOnEscape ?? true,
971
+ closeOnInteractOutside: opts.closeOnInteractOutside ?? false,
972
+ initialFocusEl: opts.initialFocusEl,
973
+ onOpenChange: (d) => opts.onOpenChange?.(d.open)
974
+ },
975
+ connect: (service) => dialog.connect(service, normalizeProps)
976
+ };
977
+ }
978
+
979
+ // src/ui/components/dialog/styles.ts
980
+ var DIALOG_STYLE_ID = "vela-ui-dialog-5";
981
+ var DIALOG_CSS = `
982
+ .vela-dialog-backdrop {
983
+ position: fixed;
984
+ inset: 0;
985
+ background: var(--vela-backdrop);
986
+ z-index: var(--vela-z-dialog);
987
+ }
988
+ /* Non-dimming variant: still catches outside clicks, but the page stays readable. */
989
+ .vela-dialog-backdrop--clear { background: transparent; }
990
+ .vela-dialog-positioner {
991
+ position: fixed;
992
+ inset: 0;
993
+ display: flex;
994
+ align-items: flex-start;
995
+ justify-content: center;
996
+ padding-top: 10vh;
997
+ z-index: var(--vela-z-dialog);
998
+ }
999
+ .vela-dialog-backdrop[data-contained],
1000
+ .vela-dialog-positioner[data-contained] { position: absolute; }
1001
+ .vela-dialog-positioner[data-align='center'] {
1002
+ align-items: center;
1003
+ padding-top: 0;
1004
+ }
1005
+ .vela-dialog {
1006
+ background: var(--vela-surface);
1007
+ color: var(--vela-fg);
1008
+ border: 1px solid var(--vela-border-strong);
1009
+ border-radius: 10px;
1010
+ box-shadow: var(--vela-shadow-dialog);
1011
+ font-size: 13px;
1012
+ min-width: 300px;
1013
+ max-width: min(92vw, 560px);
1014
+ max-height: 70vh;
1015
+ display: flex;
1016
+ flex-direction: column;
1017
+ overflow: hidden;
1018
+ outline: none;
1019
+ }
1020
+ .vela-dialog[data-state='open'] { animation: vela-dialog-in var(--vela-dur-med) var(--vela-ease); }
1021
+ @keyframes vela-dialog-in {
1022
+ from { opacity: 0; transform: translateY(6px) scale(0.98); }
1023
+ to { opacity: 1; transform: translateY(0) scale(1); }
1024
+ }
1025
+ .vela-dialog-header {
1026
+ display: flex;
1027
+ align-items: center;
1028
+ padding: 9px 9px 9px 16px;
1029
+ border-bottom: 1px solid var(--vela-border);
1030
+ user-select: none;
1031
+ }
1032
+ .vela-dialog-title { flex: 1; font-size: 17px; font-weight: 600; letter-spacing: 0.2px; color: var(--vela-fg-bright); }
1033
+ .vela-dialog-close {
1034
+ all: unset;
1035
+ cursor: pointer;
1036
+ width: 30px;
1037
+ height: 30px;
1038
+ display: flex;
1039
+ align-items: center;
1040
+ justify-content: center;
1041
+ border-radius: 4px;
1042
+ color: var(--vela-fg-muted);
1043
+ line-height: 1;
1044
+ font-size: 15px;
1045
+ }
1046
+ .vela-dialog-close:hover { background: var(--vela-hover); color: var(--vela-fg-bright); }
1047
+ .vela-dialog-body { padding: var(--vela-space-4); overflow: auto; min-height: 0; flex: 1 1 auto; }
1048
+ .vela-dialog-body[data-flush] { padding: 0; overflow: hidden; display: flex; flex-direction: column; }
1049
+ .vela-dialog-footer { flex: 0 0 auto; }
1050
+ .vela-dialog--settings {
1051
+ width: fit-content;
1052
+ min-width: min(560px, 94vw);
1053
+ max-width: min(720px, 94vw);
1054
+ max-height: 70vh;
1055
+ font-size: 13px;
1056
+ cursor: default;
1057
+ }
1058
+ .vela-dialog--form {
1059
+ width: fit-content;
1060
+ min-width: min(380px, 90%);
1061
+ max-width: min(640px, 94%);
1062
+ max-height: 82%;
1063
+ font-size: 14px;
1064
+ }
1065
+ /* Form dialogs (indicator inputs, drawing settings): the tab strip / body owns the
1066
+ line under the header, and the footer carries its own top delimiter. */
1067
+ .vela-dialog--form .vela-dialog-header { align-items: flex-start; padding: 16px 20px 20px; border-bottom: none; }
1068
+ .vela-dialog--form .vela-dialog-title { font-size: 20px; line-height: 28px; }
1069
+ .vela-dialog--form .vela-dialog-footer {
1070
+ padding: 16px 20px;
1071
+ display: flex;
1072
+ justify-content: flex-end;
1073
+ gap: 12px;
1074
+ border-top: 1px solid var(--vela-border);
1075
+ }
1076
+ .vela-dialog-btn {
1077
+ cursor: pointer;
1078
+ height: 34px;
1079
+ padding: 0 11px;
1080
+ border-radius: 6px;
1081
+ border: 1px solid var(--vela-border-strong);
1082
+ background: transparent;
1083
+ color: var(--vela-fg);
1084
+ font-weight: 400;
1085
+ font-size: 14px;
1086
+ font-family: inherit;
1087
+ transition: background var(--vela-dur-fast) ease, color var(--vela-dur-fast) ease, opacity var(--vela-dur-fast) ease, border-color var(--vela-dur-fast) ease;
1088
+ }
1089
+ .vela-dialog-btn:hover { background: var(--vela-hover); color: var(--vela-fg-bright); border-color: var(--vela-fg-muted); }
1090
+ .vela-dialog-btn-primary { border-color: var(--vela-selected-bg); background: var(--vela-selected-bg); color: var(--vela-selected-fg); }
1091
+ .vela-dialog-btn-primary:hover { background: var(--vela-selected-bg); color: var(--vela-selected-fg); opacity: 0.85; border-color: var(--vela-selected-bg); }
1092
+ ${overlayScrollbarCss(".vela-dialog-body")}
1093
+ /* Scrollers NESTED in a dialog (a flush body's grid, a tab pane) get the same thin
1094
+ overlay bars \u2014 the two-class selectors below stay overridable by more specific
1095
+ per-dialog sheets. */
1096
+ ${overlayScrollbarCss(".vela-dialog *")}
1097
+ /* \u2500\u2500 mobile chrome: dialogs fill the shell \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
1098
+ Inside a shell in the mobile size class ([data-layout='mobile'] on the widget root,
1099
+ which is position:relative) every kit dialog presents fullscreen: desktop cards are
1100
+ unusable at phone widths, and the shell's bounds \u2014 not the viewport \u2014 are the honest
1101
+ "screen" for an embedded chart. */
1102
+ [data-layout='mobile'] .vela-dialog-backdrop { position: absolute; }
1103
+ [data-layout='mobile'] .vela-dialog-positioner {
1104
+ position: absolute;
1105
+ padding: 0;
1106
+ align-items: stretch;
1107
+ }
1108
+ [data-layout='mobile'] .vela-dialog {
1109
+ width: 100%;
1110
+ min-width: 0;
1111
+ max-width: none;
1112
+ max-height: none;
1113
+ flex: 1 1 auto;
1114
+ border: none;
1115
+ border-radius: 0;
1116
+ transform: none !important; /* a desktop drag offset must not survive the flip */
1117
+ }
1118
+ [data-layout='mobile'] .vela-dialog-close { width: 40px; height: 40px; }
1119
+ [data-layout='mobile'] .vela-dialog-body { padding-bottom: calc(var(--vela-space-4) + env(safe-area-inset-bottom, 0px)); }
1120
+ `;
1121
+ var Dialog = class {
1122
+ constructor(opts = {}) {
1123
+ const doc = (opts.host ?? document.body).ownerDocument;
1124
+ injectStyles(DIALOG_STYLE_ID, DIALOG_CSS, doc);
1125
+ const host = opts.host ?? doc.body;
1126
+ this.backdrop = doc.createElement("div");
1127
+ this.backdrop.className = "vela-dialog-backdrop vela-ui-layer";
1128
+ if (opts.dimBackdrop !== true) this.backdrop.classList.add("vela-dialog-backdrop--clear");
1129
+ this.positioner = doc.createElement("div");
1130
+ this.positioner.className = "vela-dialog-positioner vela-ui-layer";
1131
+ if (opts.contained) {
1132
+ this.backdrop.dataset.contained = "";
1133
+ this.positioner.dataset.contained = "";
1134
+ }
1135
+ this.positioner.dataset.align = opts.align ?? "top";
1136
+ this.panel = doc.createElement("div");
1137
+ this.panel.className = "vela-dialog";
1138
+ if (opts.className) {
1139
+ for (const cls of opts.className.split(/\s+/)) if (cls) this.panel.classList.add(cls);
1140
+ }
1141
+ this.panel.tabIndex = -1;
1142
+ this.panel.addEventListener("keydown", (e) => {
1143
+ if (e.key !== "Escape") e.stopPropagation();
1144
+ });
1145
+ const header = doc.createElement("div");
1146
+ header.className = "vela-dialog-header";
1147
+ if (opts.draggable) {
1148
+ header.style.cursor = "move";
1149
+ let sx = 0, sy = 0, ox = 0, oy = 0, dragging = false;
1150
+ header.addEventListener("pointerdown", (e) => {
1151
+ if (e.target.closest(".vela-dialog-close")) return;
1152
+ dragging = true;
1153
+ sx = e.clientX - ox;
1154
+ sy = e.clientY - oy;
1155
+ header.setPointerCapture(e.pointerId);
1156
+ });
1157
+ header.addEventListener("pointermove", (e) => {
1158
+ if (!dragging) return;
1159
+ ox = e.clientX - sx;
1160
+ oy = e.clientY - sy;
1161
+ this.panel.style.transform = `translate(${ox}px, ${oy}px)`;
1162
+ });
1163
+ header.addEventListener("pointerup", () => dragging = false);
1164
+ }
1165
+ if (opts.headerStart) header.appendChild(opts.headerStart);
1166
+ const title = doc.createElement("div");
1167
+ title.className = "vela-dialog-title";
1168
+ title.textContent = opts.title ?? "";
1169
+ this.titleEl = title;
1170
+ const close = doc.createElement("button");
1171
+ close.className = "vela-dialog-close";
1172
+ close.appendChild(iconEl("close", doc));
1173
+ header.append(title, close);
1174
+ this.body = doc.createElement("div");
1175
+ this.body.className = "vela-dialog-body";
1176
+ if (opts.flush) this.body.dataset.flush = "";
1177
+ if (opts.content instanceof Node) this.body.appendChild(opts.content);
1178
+ else if (typeof opts.content === "function") opts.content(this.body);
1179
+ this.panel.append(header, this.body);
1180
+ if (opts.footer) {
1181
+ const foot = doc.createElement("div");
1182
+ foot.className = "vela-dialog-footer";
1183
+ if (opts.footer instanceof Node) foot.appendChild(opts.footer);
1184
+ else opts.footer(foot);
1185
+ this.panel.appendChild(foot);
1186
+ this.footer = foot;
1187
+ } else {
1188
+ this.footer = null;
1189
+ }
1190
+ this.positioner.appendChild(this.panel);
1191
+ host.append(this.backdrop, this.positioner);
1192
+ if (opts.closeOnBackdrop) {
1193
+ const closeOn = (e) => {
1194
+ if (e.target !== this.backdrop && e.target !== this.positioner) return;
1195
+ if (isPopoverOpen() || eventDismissedPopover(e)) {
1196
+ closeOpenPopovers();
1197
+ return;
1198
+ }
1199
+ this.hide();
1200
+ };
1201
+ this.backdrop.addEventListener("pointerdown", closeOn);
1202
+ this.positioner.addEventListener("pointerdown", closeOn);
1203
+ }
1204
+ this.ctrl = dialogController({
1205
+ ...opts,
1206
+ // Mobile chrome (fullscreen presentation): opening must never land focus on
1207
+ // an input — that pops the on-screen keyboard over the just-opened dialog.
1208
+ // Focus goes to the panel; a caller that WANTS the keyboard focuses its
1209
+ // input explicitly. Desktop keeps the caller's pick, else the machine's
1210
+ // first-tabbable default.
1211
+ initialFocusEl: () => {
1212
+ if (this.positioner.closest('[data-layout="mobile"]')) return this.panel;
1213
+ return opts.initialFocusEl?.() ?? null;
1214
+ }
1215
+ });
1216
+ const mid = String(this.ctrl.props.id);
1217
+ this.handle = runMachine(this.ctrl.machine, this.ctrl.props, (service) => {
1218
+ const api = this.ctrl.connect(service);
1219
+ spreadProps(this.backdrop, api.getBackdropProps(), mid);
1220
+ spreadProps(this.positioner, api.getPositionerProps(), mid);
1221
+ spreadProps(this.panel, api.getContentProps(), mid);
1222
+ spreadProps(title, api.getTitleProps(), mid);
1223
+ spreadProps(close, api.getCloseTriggerProps(), mid);
1224
+ this.backdrop.style.display = api.open ? "" : "none";
1225
+ this.positioner.style.display = api.open ? "" : "none";
1226
+ });
1227
+ }
1228
+ get open() {
1229
+ return this.ctrl.connect(this.handle.service).open;
1230
+ }
1231
+ show() {
1232
+ this.ctrl.connect(this.handle.service).setOpen(true);
1233
+ }
1234
+ hide() {
1235
+ this.ctrl.connect(this.handle.service).setOpen(false);
1236
+ }
1237
+ contains(node) {
1238
+ if (node == null) return false;
1239
+ if (this.panel.contains(node) || this.backdrop.contains(node) || this.positioner.contains(node)) return true;
1240
+ const el = node instanceof Element ? node : node.parentElement;
1241
+ return el?.closest(".vela-popover, .vela-menu") != null;
1242
+ }
1243
+ destroy() {
1244
+ if (this.open) this.hide();
1245
+ this.handle.stop();
1246
+ this.backdrop.remove();
1247
+ this.positioner.remove();
1248
+ }
1249
+ };
1250
+
1251
+ // src/ui/components/switch/controller.ts
1252
+ function switchController(opts = {}) {
1253
+ let checked = opts.checked ?? false;
1254
+ const disabled = opts.disabled ?? false;
1255
+ return {
1256
+ get checked() {
1257
+ return checked;
1258
+ },
1259
+ disabled,
1260
+ size: opts.size ?? "md",
1261
+ tone: opts.tone ?? "bright",
1262
+ setChecked(v) {
1263
+ checked = v;
1264
+ },
1265
+ toggle() {
1266
+ if (disabled) return checked;
1267
+ checked = !checked;
1268
+ opts.onChange?.(checked);
1269
+ return checked;
1270
+ }
1271
+ };
1272
+ }
1273
+
1274
+ // src/ui/components/switch/styles.ts
1275
+ var SWITCH_STYLE_ID = "vela-ui-switch";
1276
+ var SWITCH_CSS = `
1277
+ .vela-switch {
1278
+ width: 20px;
1279
+ height: 20px;
1280
+ flex: 0 0 auto;
1281
+ display: inline-flex;
1282
+ align-items: center;
1283
+ justify-content: center;
1284
+ padding: 0;
1285
+ border: 1px solid var(--vela-border);
1286
+ border-radius: 5px;
1287
+ background: transparent;
1288
+ color: transparent;
1289
+ cursor: pointer;
1290
+ line-height: 0;
1291
+ transition: background 0.12s ease, border-color 0.12s ease, color 0.12s ease;
1292
+ }
1293
+ .vela-switch .vela-icon, .vela-switch svg { display: block; width: 12px; height: 12px; }
1294
+ .vela-switch[data-size='sm'] .vela-icon, .vela-switch[data-size='sm'] svg { width: 11px; height: 11px; }
1295
+ .vela-switch:hover { border-color: var(--vela-fg-muted); }
1296
+ .vela-switch[data-checked] {
1297
+ background: var(--vela-fg-bright);
1298
+ border-color: var(--vela-fg-bright);
1299
+ color: var(--vela-bg);
1300
+ }
1301
+ .vela-switch[data-size='sm'] { width: 18px; height: 18px; border-color: var(--vela-border-strong); }
1302
+ .vela-switch[data-size='sm']:hover { border-color: var(--vela-fg-muted); }
1303
+ .vela-switch[data-size='sm'][data-checked] {
1304
+ background: var(--vela-selected-bg);
1305
+ border-color: var(--vela-selected-bg);
1306
+ color: var(--vela-selected-fg);
1307
+ }
1308
+ .vela-switch[data-tone='selected'][data-checked] {
1309
+ background: var(--vela-selected-bg);
1310
+ border-color: var(--vela-selected-bg);
1311
+ color: var(--vela-selected-fg);
1312
+ }
1313
+ .vela-switch[disabled] { opacity: 0.4; cursor: default; }
1314
+ `;
1315
+
1316
+ // src/ui/components/switch/view.ts
1317
+ var Switch = class {
1318
+ constructor(opts = {}) {
1319
+ injectStyles(SWITCH_STYLE_ID, SWITCH_CSS, document);
1320
+ this.ctrl = switchController(opts);
1321
+ const b = document.createElement("button");
1322
+ b.type = "button";
1323
+ if (opts.id) b.id = opts.id;
1324
+ b.className = "vela-switch";
1325
+ b.dataset.size = this.ctrl.size;
1326
+ b.dataset.tone = this.ctrl.tone;
1327
+ b.setAttribute("role", "switch");
1328
+ if (this.ctrl.disabled) b.disabled = true;
1329
+ b.appendChild(iconEl("check", b.ownerDocument));
1330
+ this.el = b;
1331
+ this.paint();
1332
+ b.addEventListener("pointerdown", (e) => {
1333
+ e.preventDefault();
1334
+ b.focus({ preventScroll: true });
1335
+ });
1336
+ b.addEventListener("click", () => {
1337
+ this.ctrl.toggle();
1338
+ this.paint();
1339
+ });
1340
+ }
1341
+ get checked() {
1342
+ return this.ctrl.checked;
1343
+ }
1344
+ setChecked(v) {
1345
+ this.ctrl.setChecked(v);
1346
+ this.paint();
1347
+ }
1348
+ paint() {
1349
+ const on = this.ctrl.checked;
1350
+ this.el.setAttribute("aria-checked", on ? "true" : "false");
1351
+ if (on) this.el.dataset.checked = "";
1352
+ else delete this.el.dataset.checked;
1353
+ }
1354
+ };
1355
+
1356
+ // src/ui/components/number-input/controller.ts
1357
+ function clampNumber(n, opts) {
1358
+ let v = n;
1359
+ if (opts.min !== void 0) v = Math.max(opts.min, v);
1360
+ if (opts.max !== void 0) v = Math.min(opts.max, v);
1361
+ if (opts.integer) v = Math.round(v);
1362
+ return v;
1363
+ }
1364
+ function decimalsOf(n) {
1365
+ const s = String(n);
1366
+ const e = s.search(/[eE]/);
1367
+ if (e >= 0) {
1368
+ const frac = s.slice(0, e).split(".")[1]?.length ?? 0;
1369
+ return Math.max(0, Math.min(20, frac - Number(s.slice(e + 1))));
1370
+ }
1371
+ return s.split(".")[1]?.length ?? 0;
1372
+ }
1373
+ function snapToStep(n, base, step) {
1374
+ const d = Math.min(20, Math.max(decimalsOf(base), decimalsOf(step)));
1375
+ return Number(n.toFixed(d));
1376
+ }
1377
+ function numberInputController(opts) {
1378
+ let value = opts.value;
1379
+ const integer = opts.integer ?? false;
1380
+ const commit = opts.commit ?? "blur";
1381
+ const clamp = opts.clamp ?? commit === "blur";
1382
+ const step = opts.step ?? (integer ? 1 : 0.1);
1383
+ const onChange = opts.onChange;
1384
+ const normalize = (raw) => clamp ? clampNumber(raw, { min: opts.min, max: opts.max, integer }) : raw;
1385
+ const apply = (raw) => {
1386
+ if (!Number.isFinite(raw)) return null;
1387
+ const n = normalize(raw);
1388
+ if (n !== value) {
1389
+ value = n;
1390
+ onChange?.(n);
1391
+ }
1392
+ return n;
1393
+ };
1394
+ return {
1395
+ get value() {
1396
+ return value;
1397
+ },
1398
+ min: opts.min,
1399
+ max: opts.max,
1400
+ step,
1401
+ integer,
1402
+ size: opts.size ?? "md",
1403
+ commit,
1404
+ steppers: opts.steppers ?? commit === "blur",
1405
+ clamp,
1406
+ disabled: opts.disabled ?? false,
1407
+ apply,
1408
+ sync(raw) {
1409
+ const n = Number.isFinite(raw) ? normalize(raw) : value;
1410
+ value = n;
1411
+ return n;
1412
+ },
1413
+ nudge(dir) {
1414
+ return apply(snapToStep(value + dir * step, value, step));
1415
+ }
1416
+ };
1417
+ }
1418
+
1419
+ // src/ui/components/number-input/styles.ts
1420
+ var NUMBER_STYLE_ID = "vela-ui-number-3";
1421
+ var NUMBER_CSS = `
1422
+ .vela-num { position: relative; display: inline-block; min-width: 0; }
1423
+ .vela-num[data-fill] { width: 100%; }
1424
+ .vela-num:not([data-fill]) { width: 100px; flex: none; justify-self: start; }
1425
+ .vela-num:not([data-fill])[data-compact] { width: 80px; }
1426
+ .vela-num input {
1427
+ width: 100%;
1428
+ box-sizing: border-box;
1429
+ height: 34px;
1430
+ background: transparent;
1431
+ border: 1px solid var(--vela-border-strong);
1432
+ border-radius: 6px;
1433
+ color: var(--vela-fg-bright);
1434
+ padding: 0 8px;
1435
+ font-size: 14px;
1436
+ font-family: inherit;
1437
+ outline: none;
1438
+ transition: border-color 0.12s ease, box-shadow 0.12s ease;
1439
+ -moz-appearance: textfield;
1440
+ appearance: textfield;
1441
+ }
1442
+ .vela-num input::-webkit-inner-spin-button, .vela-num input::-webkit-outer-spin-button { -webkit-appearance: none; margin: 0; }
1443
+ .vela-num input:hover { border-color: var(--vela-fg-muted); }
1444
+ .vela-num input:focus { border-color: var(--vela-focus); box-shadow: 0 0 0 3px var(--vela-focus-soft); }
1445
+ /* The stepper gutter exists only while the steppers do (hover) \u2014 an idle field keeps
1446
+ its full width so long values aren't cut under an invisible arrow column. */
1447
+ .vela-num[data-steppers]:hover input { padding-right: 26px; }
1448
+ .vela-num[data-size='sm'] { width: 64px; flex: none; }
1449
+ .vela-num[data-size='sm'][data-compact] { width: 56px; }
1450
+ .vela-num[data-size='sm'] input {
1451
+ height: 28px;
1452
+ background: var(--vela-surface-elev);
1453
+ border-radius: var(--vela-radius-sm);
1454
+ color: var(--vela-fg);
1455
+ font-size: 13px;
1456
+ box-shadow: none;
1457
+ }
1458
+ .vela-num[data-size='sm'] input:focus { box-shadow: none; }
1459
+ .vela-num-step {
1460
+ position: absolute;
1461
+ right: 0;
1462
+ top: 0;
1463
+ bottom: 0;
1464
+ width: 22px;
1465
+ display: none;
1466
+ flex-direction: column;
1467
+ justify-content: center;
1468
+ box-sizing: border-box;
1469
+ padding: 2px 6px 2px 0;
1470
+ line-height: 0;
1471
+ }
1472
+ .vela-num[data-steppers]:hover .vela-num-step { display: flex; }
1473
+ .vela-num-step button {
1474
+ flex: 1;
1475
+ width: 100%;
1476
+ min-height: 0;
1477
+ border: none;
1478
+ background: transparent;
1479
+ color: var(--vela-fg-muted);
1480
+ border-radius: 3px;
1481
+ padding: 0;
1482
+ cursor: pointer;
1483
+ display: flex;
1484
+ align-items: center;
1485
+ justify-content: center;
1486
+ }
1487
+ .vela-num-step button:hover { background: var(--vela-hover); color: var(--vela-fg-bright); }
1488
+ .vela-num-step .vela-icon, .vela-num-step svg { width: 12px; height: 12px; display: block; }
1489
+ `;
1490
+
1491
+ // src/ui/components/number-input/view.ts
1492
+ var NumberInput = class {
1493
+ constructor(opts) {
1494
+ const doc = document;
1495
+ injectStyles(NUMBER_STYLE_ID, NUMBER_CSS, doc);
1496
+ this.ctrl = numberInputController(opts);
1497
+ this.placeholderMode = opts.placeholder !== void 0;
1498
+ this.emptyValue = opts.emptyValue ?? opts.value;
1499
+ this.last = String(opts.value);
1500
+ const wrap = doc.createElement("div");
1501
+ wrap.className = "vela-num";
1502
+ wrap.dataset.size = this.ctrl.size;
1503
+ if (opts.fill ?? this.ctrl.size !== "sm") wrap.dataset.fill = "";
1504
+ if (opts.compact) wrap.dataset.compact = "";
1505
+ if (this.ctrl.steppers) wrap.dataset.steppers = "";
1506
+ const ni = doc.createElement("input");
1507
+ ni.type = "number";
1508
+ if (opts.id) ni.id = opts.id;
1509
+ if (opts.title) ni.title = opts.title;
1510
+ if (opts.min !== void 0) ni.min = String(opts.min);
1511
+ if (opts.max !== void 0) ni.max = String(opts.max);
1512
+ ni.step = String(this.ctrl.step);
1513
+ if (this.ctrl.disabled) ni.disabled = true;
1514
+ if (this.placeholderMode) {
1515
+ ni.placeholder = opts.placeholder ?? "";
1516
+ ni.value = opts.value !== this.emptyValue ? String(opts.value) : "";
1517
+ } else {
1518
+ ni.value = String(opts.value);
1519
+ }
1520
+ const commitRaw = (raw) => {
1521
+ const n = this.ctrl.apply(raw);
1522
+ if (n === null) {
1523
+ ni.value = this.placeholderMode && this.ctrl.value === this.emptyValue ? "" : this.last;
1524
+ return;
1525
+ }
1526
+ this.last = String(n);
1527
+ if (this.placeholderMode && n === this.emptyValue) ni.value = "";
1528
+ else ni.value = String(n);
1529
+ };
1530
+ if (this.placeholderMode) {
1531
+ ni.addEventListener("change", () => {
1532
+ const raw = ni.value.trim() === "" ? this.emptyValue : Number(ni.value);
1533
+ commitRaw(Number.isFinite(raw) ? raw : this.emptyValue);
1534
+ });
1535
+ } else if (this.ctrl.commit === "live") {
1536
+ ni.addEventListener("input", () => {
1537
+ const v = Number(ni.value);
1538
+ if (Number.isFinite(v)) this.ctrl.apply(v);
1539
+ });
1540
+ } else {
1541
+ ni.addEventListener("blur", () => commitRaw(Number(ni.value)));
1542
+ ni.addEventListener("keydown", (e) => {
1543
+ if (e.key === "Enter") {
1544
+ e.preventDefault();
1545
+ ni.blur();
1546
+ }
1547
+ });
1548
+ }
1549
+ wrap.appendChild(ni);
1550
+ if (this.ctrl.steppers) {
1551
+ wrap.appendChild(this.buildSteppers(doc, (dir) => {
1552
+ const cur = Number(ni.value);
1553
+ const base = Number.isFinite(cur) ? cur : this.ctrl.value;
1554
+ commitRaw(snapToStep(base + dir * this.ctrl.step, base, this.ctrl.step));
1555
+ }));
1556
+ }
1557
+ this.el = wrap;
1558
+ this.input = ni;
1559
+ }
1560
+ get value() {
1561
+ return this.ctrl.value;
1562
+ }
1563
+ setValue(v) {
1564
+ const n = this.ctrl.sync(v);
1565
+ this.last = String(n);
1566
+ if (this.placeholderMode && n === this.emptyValue) this.input.value = "";
1567
+ else this.input.value = String(n);
1568
+ }
1569
+ buildSteppers(doc, applyDir) {
1570
+ const steps = doc.createElement("div");
1571
+ steps.className = "vela-num-step";
1572
+ const mk = (dir, icon, label) => {
1573
+ const b = doc.createElement("button");
1574
+ b.type = "button";
1575
+ b.tabIndex = -1;
1576
+ b.setAttribute("aria-label", label);
1577
+ b.appendChild(iconEl(icon, doc));
1578
+ let timer = 0;
1579
+ const apply = () => applyDir(dir);
1580
+ const stop = () => {
1581
+ if (timer) {
1582
+ window.clearTimeout(timer);
1583
+ timer = 0;
1584
+ }
1585
+ };
1586
+ b.addEventListener("pointerdown", (e) => {
1587
+ e.preventDefault();
1588
+ e.stopPropagation();
1589
+ apply();
1590
+ timer = window.setTimeout(function tick() {
1591
+ apply();
1592
+ timer = window.setTimeout(tick, 60);
1593
+ }, 400);
1594
+ });
1595
+ b.addEventListener("pointerup", stop);
1596
+ b.addEventListener("pointerleave", stop);
1597
+ b.addEventListener("pointercancel", stop);
1598
+ return b;
1599
+ };
1600
+ steps.append(mk(1, "chevron-up", "Increase"), mk(-1, "chevron-down", "Decrease"));
1601
+ return steps;
1602
+ }
1603
+ };
1604
+
1605
+ // src/ui/components/text-field/controller.ts
1606
+ function textFieldController(opts = {}) {
1607
+ let value = opts.value ?? "";
1608
+ const size = opts.size ?? "md";
1609
+ return {
1610
+ get value() {
1611
+ return value;
1612
+ },
1613
+ size,
1614
+ fill: opts.fill ?? size !== "sm",
1615
+ disabled: opts.disabled ?? false,
1616
+ commit(next) {
1617
+ if (next === value) return null;
1618
+ value = next;
1619
+ opts.onChange?.(next);
1620
+ return next;
1621
+ },
1622
+ sync(next) {
1623
+ value = next;
1624
+ }
1625
+ };
1626
+ }
1627
+
1628
+ // src/ui/components/text-field/styles.ts
1629
+ var TEXT_STYLE_ID = "vela-ui-text-2";
1630
+ var TEXT_CSS = `
1631
+ .vela-text {
1632
+ display: inline-block;
1633
+ min-width: 0;
1634
+ }
1635
+ .vela-text[data-fill] { width: 100%; }
1636
+ .vela-text:not([data-fill]) { width: 100px; flex: none; justify-self: start; }
1637
+ .vela-text-field {
1638
+ width: 100%;
1639
+ min-width: 0;
1640
+ box-sizing: border-box;
1641
+ height: 34px;
1642
+ background: transparent;
1643
+ border: 1px solid var(--vela-border-strong);
1644
+ border-radius: 6px;
1645
+ color: var(--vela-fg-bright);
1646
+ padding: 0 8px;
1647
+ font-size: 14px;
1648
+ font-family: inherit;
1649
+ outline: none;
1650
+ overflow: hidden;
1651
+ text-overflow: ellipsis;
1652
+ transition: border-color 0.12s ease, box-shadow 0.12s ease;
1653
+ }
1654
+ .vela-text-field:hover { border-color: var(--vela-fg-muted); }
1655
+ .vela-text-field:focus { border-color: var(--vela-focus); box-shadow: 0 0 0 3px var(--vela-focus-soft); }
1656
+ .vela-text[data-size='sm'] .vela-text-field {
1657
+ height: 28px;
1658
+ background: var(--vela-surface-elev);
1659
+ border-radius: var(--vela-radius-sm);
1660
+ color: var(--vela-fg);
1661
+ font-size: 13px;
1662
+ }
1663
+ `;
1664
+
1665
+ // src/ui/components/text-field/view.ts
1666
+ var TextField = class {
1667
+ constructor(opts = {}) {
1668
+ const doc = document;
1669
+ injectStyles(TEXT_STYLE_ID, TEXT_CSS, doc);
1670
+ this.ctrl = textFieldController(opts);
1671
+ const wrap = doc.createElement("div");
1672
+ wrap.className = "vela-text";
1673
+ wrap.dataset.size = this.ctrl.size;
1674
+ if (this.ctrl.fill) wrap.dataset.fill = "";
1675
+ const ti = doc.createElement("input");
1676
+ ti.type = "text";
1677
+ ti.size = 1;
1678
+ if (opts.id) ti.id = opts.id;
1679
+ ti.value = this.ctrl.value;
1680
+ ti.className = "vela-text-field";
1681
+ if (this.ctrl.disabled) ti.disabled = true;
1682
+ ti.addEventListener("blur", () => {
1683
+ this.ctrl.commit(ti.value);
1684
+ });
1685
+ ti.addEventListener("keydown", (e) => {
1686
+ if (e.key === "Enter") {
1687
+ e.preventDefault();
1688
+ ti.blur();
1689
+ }
1690
+ });
1691
+ wrap.appendChild(ti);
1692
+ this.el = wrap;
1693
+ this.input = ti;
1694
+ }
1695
+ get value() {
1696
+ return this.ctrl.value;
1697
+ }
1698
+ setValue(v) {
1699
+ this.ctrl.sync(v);
1700
+ this.input.value = v;
1701
+ }
1702
+ };
1703
+
1704
+ // src/ui/components/text-area/controller.ts
1705
+ function textAreaController(opts = {}) {
1706
+ let value = opts.value ?? "";
1707
+ return {
1708
+ get value() {
1709
+ return value;
1710
+ },
1711
+ size: opts.size ?? "md",
1712
+ rows: opts.rows ?? 3,
1713
+ disabled: opts.disabled ?? false,
1714
+ commit(next) {
1715
+ if (next === value) return null;
1716
+ value = next;
1717
+ opts.onChange?.(next);
1718
+ return next;
1719
+ },
1720
+ sync(next) {
1721
+ value = next;
1722
+ }
1723
+ };
1724
+ }
1725
+
1726
+ // src/ui/components/text-area/styles.ts
1727
+ var TEXTAREA_STYLE_ID = "vela-ui-textarea";
1728
+ var TEXTAREA_CSS = `
1729
+ .vela-textarea { display: block; width: 100%; min-width: 0; }
1730
+ .vela-textarea-field {
1731
+ display: block;
1732
+ width: 100%;
1733
+ box-sizing: border-box;
1734
+ min-height: 64px;
1735
+ background: transparent;
1736
+ border: 1px solid var(--vela-border-strong);
1737
+ border-radius: 6px;
1738
+ color: var(--vela-fg-bright);
1739
+ padding: 8px;
1740
+ font-size: 14px;
1741
+ font-family: inherit;
1742
+ line-height: 1.4;
1743
+ resize: vertical;
1744
+ outline: none;
1745
+ transition: border-color 0.12s ease, box-shadow 0.12s ease;
1746
+ }
1747
+ .vela-textarea-field:hover { border-color: var(--vela-fg-muted); }
1748
+ .vela-textarea-field:focus { border-color: var(--vela-focus); box-shadow: 0 0 0 3px var(--vela-focus-soft); }
1749
+ .vela-textarea-field::placeholder { color: currentColor; opacity: 0.4; }
1750
+ .vela-textarea[data-autogrow] .vela-textarea-field { resize: none; overflow-y: hidden; min-height: 46px; }
1751
+ .vela-textarea[data-size='sm'] .vela-textarea-field {
1752
+ font-size: 13px;
1753
+ min-height: 46px;
1754
+ padding: 6px 9px;
1755
+ line-height: 18px;
1756
+ }
1757
+ `;
1758
+
1759
+ // src/ui/components/text-area/view.ts
1760
+ var TextArea = class {
1761
+ constructor(opts = {}) {
1762
+ const doc = document;
1763
+ injectStyles(TEXTAREA_STYLE_ID, TEXTAREA_CSS, doc);
1764
+ this.ctrl = textAreaController(opts);
1765
+ this.autoGrow = opts.autoGrow === true;
1766
+ this.maxLines = opts.maxLines ?? 4;
1767
+ const wrap = doc.createElement("div");
1768
+ wrap.className = "vela-textarea";
1769
+ wrap.dataset.size = this.ctrl.size;
1770
+ if (this.autoGrow) wrap.dataset.autogrow = "";
1771
+ const ta = doc.createElement("textarea");
1772
+ ta.className = "vela-textarea-field";
1773
+ if (opts.id) ta.id = opts.id;
1774
+ if (opts.placeholder) ta.placeholder = opts.placeholder;
1775
+ ta.rows = this.ctrl.rows;
1776
+ ta.value = this.ctrl.value;
1777
+ if (this.ctrl.disabled) ta.disabled = true;
1778
+ ta.addEventListener("blur", () => {
1779
+ this.ctrl.commit(ta.value);
1780
+ });
1781
+ if (this.autoGrow) {
1782
+ ta.addEventListener("input", () => this.grow());
1783
+ }
1784
+ wrap.appendChild(ta);
1785
+ this.el = wrap;
1786
+ this.input = ta;
1787
+ if (this.autoGrow) this.grow();
1788
+ }
1789
+ get value() {
1790
+ return this.ctrl.value;
1791
+ }
1792
+ setValue(v) {
1793
+ this.ctrl.sync(v);
1794
+ this.input.value = v;
1795
+ if (this.autoGrow) this.grow();
1796
+ }
1797
+ grow() {
1798
+ const ta = this.input;
1799
+ const lh = parseFloat(getComputedStyle(ta).lineHeight) || 18;
1800
+ const pad = ta.clientHeight - (ta.offsetHeight ? lh * (ta.rows || 1) : 0);
1801
+ const max = lh * this.maxLines + (Number.isFinite(pad) && pad > 0 ? pad : 12);
1802
+ ta.style.height = "auto";
1803
+ ta.style.height = `${Math.min(ta.scrollHeight, max)}px`;
1804
+ ta.style.overflowY = ta.scrollHeight > max ? "auto" : "hidden";
1805
+ }
1806
+ };
1807
+
1808
+ // src/ui/components/color-picker/controller.ts
1809
+ function splitColor(color) {
1810
+ const s = String(color ?? "").trim();
1811
+ let m = /^#([0-9a-f]{6})([0-9a-f]{2})?$/i.exec(s);
1812
+ if (m) return { hex6: `#${m[1].toLowerCase()}`, alpha: m[2] ? parseInt(m[2], 16) / 255 : 1 };
1813
+ m = /^#([0-9a-f]{3})$/i.exec(s);
1814
+ if (m) {
1815
+ const h = m[1].toLowerCase();
1816
+ return { hex6: `#${h[0]}${h[0]}${h[1]}${h[1]}${h[2]}${h[2]}`, alpha: 1 };
1817
+ }
1818
+ const rgba = /^rgba?\(([^)]+)\)/i.exec(s);
1819
+ if (rgba) {
1820
+ const p = rgba[1].split(",").map((v) => v.trim());
1821
+ const hx = (n) => Math.max(0, Math.min(255, Math.round(n))).toString(16).padStart(2, "0");
1822
+ const a = p[3] != null ? Math.max(0, Math.min(1, parseFloat(p[3]))) : 1;
1823
+ return { hex6: `#${hx(parseInt(p[0] ?? "0", 10))}${hx(parseInt(p[1] ?? "0", 10))}${hx(parseInt(p[2] ?? "0", 10))}`, alpha: a };
1824
+ }
1825
+ return { hex6: ACCENT_BRIGHT, alpha: 1 };
1826
+ }
1827
+ function combineColor(hex6, alpha) {
1828
+ const a = Math.max(0, Math.min(1, alpha));
1829
+ if (a >= 0.999) return hex6;
1830
+ return `${hex6}${Math.round(a * 255).toString(16).padStart(2, "0")}`;
1831
+ }
1832
+ function blendOver(fg, bg, alpha) {
1833
+ const rgb = (c) => {
1834
+ const n = parseInt(splitColor(c).hex6.slice(1), 16);
1835
+ return [n >> 16 & 255, n >> 8 & 255, n & 255];
1836
+ };
1837
+ const [fr, fgc, fb] = rgb(fg);
1838
+ const [br, bgc, bb] = rgb(bg);
1839
+ const a = Math.max(0, Math.min(1, alpha));
1840
+ const h = (n) => Math.round(n).toString(16).padStart(2, "0");
1841
+ return `#${h(fr * a + br * (1 - a))}${h(fgc * a + bgc * (1 - a))}${h(fb * a + bb * (1 - a))}`;
1842
+ }
1843
+ function hslHex(h, s, l) {
1844
+ const sn = s / 100;
1845
+ const ln = l / 100;
1846
+ const k = (n) => (n + h / 30) % 12;
1847
+ const a = sn * Math.min(ln, 1 - ln);
1848
+ const f = (n) => ln - a * Math.max(-1, Math.min(k(n) - 3, Math.min(9 - k(n), 1)));
1849
+ const to = (x) => Math.round(x * 255).toString(16).padStart(2, "0");
1850
+ return `#${to(f(0))}${to(f(8))}${to(f(4))}`;
1851
+ }
1852
+ function buildPalette() {
1853
+ const grays = [100, 90, 80, 68, 56, 45, 35, 25, 14, 0].map((l) => hslHex(0, 0, l));
1854
+ const hues = [4, 28, 50, 96, 140, 174, 200, 224, 270, 322];
1855
+ const levels = [
1856
+ [82, 56],
1857
+ [58, 84],
1858
+ [62, 74],
1859
+ [66, 62],
1860
+ [70, 50],
1861
+ [64, 39],
1862
+ [54, 29]
1863
+ ];
1864
+ return [grays, ...levels.map(([s, l]) => hues.map((h) => hslHex(h, s, l)))];
1865
+ }
1866
+ function transparencyChecker(size) {
1867
+ return `repeating-conic-gradient(#9aa0a6 0% 25%, #d3d6da 0% 50%) 0 0 / ${size}px ${size}px`;
1868
+ }
1869
+ function colorPickerController(opts = {}) {
1870
+ const parsed = splitColor(opts.color ?? ACCENT_BRIGHT);
1871
+ let hex6 = parsed.hex6;
1872
+ let alpha = parsed.alpha;
1873
+ return {
1874
+ get hex6() {
1875
+ return hex6;
1876
+ },
1877
+ get alpha() {
1878
+ return alpha;
1879
+ },
1880
+ combined() {
1881
+ return combineColor(hex6, alpha);
1882
+ },
1883
+ setHex(hex) {
1884
+ hex6 = splitColor(hex).hex6;
1885
+ },
1886
+ setAlpha(a) {
1887
+ alpha = Math.max(0, Math.min(1, a));
1888
+ }
1889
+ };
1890
+ }
1891
+
1892
+ // src/ui/components/color-picker/styles.ts
1893
+ var COLOR_STYLE_ID = "vela-ui-color";
1894
+ var COLOR_CSS = `
1895
+ .vela-color-field{width:24px;height:24px;padding:2px;border:1px solid var(--vela-border);border-radius:0;background:var(--vela-surface-sunken);cursor:pointer;display:inline-flex;flex:none;}
1896
+ .vela-color-field:hover{border-color:var(--vela-fg-muted);}
1897
+ .vela-color-field-swatch{display:block;width:100%;height:100%;border-radius:0;box-shadow:inset 0 0 0 1px rgba(0,0,0,0.25);}
1898
+ .vela-color-field-circle{width:26px;height:26px;padding:3px;border:1px solid var(--vela-border-strong);border-radius:4px;background:transparent;overflow:hidden;}
1899
+ .vela-color-field-circle .vela-color-field-swatch{border-radius:2px;box-shadow:none;}
1900
+ .vela-color-field-pop{background:var(--vela-surface-overlay);border:1px solid var(--vela-border);border-radius:var(--vela-radius-lg);box-shadow:var(--vela-shadow);padding:10px;}
1901
+ `;
1902
+
1903
+ // src/ui/components/color-picker/view.ts
1904
+ var PALETTE = buildPalette();
1905
+ var CHECKER = transparencyChecker(10);
1906
+ var FIELD_CHECKER = transparencyChecker(8);
1907
+ var recents = [ACCENT, BULLISH, BEARISH, WARNING, NEUTRAL];
1908
+ function addRecent(hex6) {
1909
+ const i = recents.findIndex((c) => c.toLowerCase() === hex6.toLowerCase());
1910
+ if (i >= 0) recents.splice(i, 1);
1911
+ recents.unshift(hex6);
1912
+ if (recents.length > 10) recents.length = 10;
1913
+ }
1914
+ function buildColorPicker(color, theme, onChange) {
1915
+ const parsed = splitColor(color);
1916
+ let curHex = parsed.hex6;
1917
+ let curAlpha = parsed.alpha;
1918
+ const root = document.createElement("div");
1919
+ root.style.cssText = "display:flex;flex-direction:column;gap:9px;width:236px;";
1920
+ const grid = document.createElement("div");
1921
+ grid.style.cssText = "display:grid;grid-template-columns:repeat(10,1fr);gap:4px;";
1922
+ const swatches = [];
1923
+ for (const row of PALETTE) {
1924
+ for (const c of row) {
1925
+ const sw = document.createElement("button");
1926
+ sw.type = "button";
1927
+ sw.dataset.c = c;
1928
+ sw.style.cssText = `width:100%;aspect-ratio:1;border-radius:4px;border:none;cursor:pointer;background:${c};padding:0;`;
1929
+ sw.addEventListener("click", (e) => {
1930
+ e.stopPropagation();
1931
+ pickHex(c);
1932
+ });
1933
+ grid.appendChild(sw);
1934
+ swatches.push(sw);
1935
+ }
1936
+ }
1937
+ const paintSelection = () => {
1938
+ for (const sw of swatches) {
1939
+ const on = (sw.dataset.c ?? "").toLowerCase() === curHex.toLowerCase();
1940
+ sw.style.outline = on ? `2px solid ${theme.textColor}` : "none";
1941
+ sw.style.outlineOffset = "2px";
1942
+ sw.style.zIndex = on ? "1" : "";
1943
+ sw.style.position = on ? "relative" : "";
1944
+ }
1945
+ };
1946
+ const recentRow = document.createElement("div");
1947
+ recentRow.style.cssText = "display:flex;align-items:center;gap:5px;flex-wrap:wrap;border-top:1px solid var(--vela-border);padding-top:9px;";
1948
+ const renderRecents = () => {
1949
+ recentRow.replaceChildren();
1950
+ for (const c of recents) {
1951
+ const sw = document.createElement("button");
1952
+ sw.type = "button";
1953
+ sw.style.cssText = `width:17px;height:17px;border-radius:var(--vela-radius-sm);border:1px solid var(--vela-border);cursor:pointer;background:${c};padding:0;`;
1954
+ sw.addEventListener("click", (e) => {
1955
+ e.stopPropagation();
1956
+ pickHex(c);
1957
+ });
1958
+ recentRow.appendChild(sw);
1959
+ }
1960
+ const add = document.createElement("label");
1961
+ add.style.cssText = `width:17px;height:17px;border-radius:var(--vela-radius-sm);border:1px dashed var(--vela-border-strong);cursor:pointer;display:flex;align-items:center;justify-content:center;color:${theme.textColor};font:14px ${theme.fontFamily};position:relative;`;
1962
+ add.textContent = "+";
1963
+ const input = document.createElement("input");
1964
+ input.type = "color";
1965
+ input.value = curHex;
1966
+ input.style.cssText = "position:absolute;inset:0;opacity:0;cursor:pointer;";
1967
+ input.addEventListener("input", () => pickHex(input.value, true));
1968
+ add.appendChild(input);
1969
+ recentRow.appendChild(add);
1970
+ };
1971
+ const opLabel = document.createElement("div");
1972
+ opLabel.textContent = "Opacity";
1973
+ opLabel.style.cssText = `font:11px ${theme.fontFamily};color:var(--vela-fg-muted);`;
1974
+ const opRow = document.createElement("div");
1975
+ opRow.style.cssText = "display:flex;align-items:center;gap:10px;";
1976
+ const track = document.createElement("div");
1977
+ track.style.cssText = "flex:1;position:relative;height:13px;border-radius:7px;cursor:pointer;";
1978
+ const knob = document.createElement("div");
1979
+ knob.style.cssText = "position:absolute;top:50%;width:15px;height:15px;border-radius:50%;background:var(--vela-selected-bg);box-shadow:0 1px 3px rgba(0,0,0,0.55);transform:translate(-50%,-50%);pointer-events:none;";
1980
+ track.appendChild(knob);
1981
+ const pctBox = document.createElement("div");
1982
+ pctBox.style.cssText = `min-width:42px;text-align:center;font:var(--vela-font-size-md) ${theme.fontFamily};color:var(--vela-fg);border:1px solid var(--vela-border);border-radius:5px;padding:3px 4px;`;
1983
+ opRow.append(track, pctBox);
1984
+ const paintOpacity = () => {
1985
+ track.style.background = `linear-gradient(to right, ${curHex}00, ${curHex}ff), ${CHECKER}`;
1986
+ knob.style.left = `${curAlpha * 100}%`;
1987
+ pctBox.textContent = `${Math.round(curAlpha * 100)}%`;
1988
+ };
1989
+ let dragging = false;
1990
+ const onDrag = (clientX) => {
1991
+ const r = track.getBoundingClientRect();
1992
+ curAlpha = Math.max(0, Math.min(1, (clientX - r.left) / r.width));
1993
+ paintOpacity();
1994
+ emit();
1995
+ };
1996
+ track.addEventListener("pointerdown", (e) => {
1997
+ e.stopPropagation();
1998
+ dragging = true;
1999
+ track.setPointerCapture(e.pointerId);
2000
+ onDrag(e.clientX);
2001
+ });
2002
+ track.addEventListener("pointermove", (e) => {
2003
+ if (dragging) onDrag(e.clientX);
2004
+ });
2005
+ track.addEventListener("pointerup", () => dragging = false);
2006
+ function emit() {
2007
+ onChange(combineColor(curHex, curAlpha));
2008
+ }
2009
+ function pickHex(hex, custom = false) {
2010
+ curHex = splitColor(hex).hex6;
2011
+ addRecent(curHex);
2012
+ paintSelection();
2013
+ paintOpacity();
2014
+ if (custom) renderRecents();
2015
+ emit();
2016
+ }
2017
+ renderRecents();
2018
+ paintSelection();
2019
+ paintOpacity();
2020
+ root.append(grid, recentRow, opLabel, opRow);
2021
+ return root;
2022
+ }
2023
+ function colorField(theme, getVal, onVal, opts) {
2024
+ return new ColorField({ theme, getVal, onVal, shape: opts?.shape, id: opts?.id, popover: opts?.popover }).el;
2025
+ }
2026
+ var ColorField = class {
2027
+ constructor(opts) {
2028
+ injectStyles(COLOR_STYLE_ID, COLOR_CSS, document);
2029
+ this.theme = opts.theme;
2030
+ this.getVal = opts.getVal;
2031
+ this.onVal = opts.onVal;
2032
+ this.popoverOpts = opts.popover;
2033
+ const trigger = document.createElement("button");
2034
+ trigger.type = "button";
2035
+ if (opts.id) trigger.id = opts.id;
2036
+ trigger.className = opts.shape === "circle" ? "vela-color-field vela-color-field-circle" : "vela-color-field";
2037
+ const swatch = document.createElement("span");
2038
+ swatch.className = "vela-color-field-swatch";
2039
+ trigger.appendChild(swatch);
2040
+ this.el = trigger;
2041
+ this.swatch = swatch;
2042
+ this.paint();
2043
+ trigger.addEventListener("vela-sync", () => this.paint());
2044
+ trigger.addEventListener("pointerdown", (e) => {
2045
+ e.preventDefault();
2046
+ trigger.focus({ preventScroll: true });
2047
+ });
2048
+ trigger.addEventListener("click", (e) => {
2049
+ e.stopPropagation();
2050
+ this.toggle();
2051
+ });
2052
+ }
2053
+ paint() {
2054
+ const v = this.getVal();
2055
+ this.swatch.style.background = `linear-gradient(${v}, ${v}), ${FIELD_CHECKER}`;
2056
+ }
2057
+ toggle() {
2058
+ if (openPopoverTrigger() === this.el) {
2059
+ closeOpenPopovers();
2060
+ return;
2061
+ }
2062
+ const pop = new Popover({
2063
+ trigger: this.el,
2064
+ theme: this.theme,
2065
+ align: this.popoverOpts?.align ?? "end",
2066
+ gap: this.popoverOpts?.gap ?? 6,
2067
+ host: this.popoverOpts?.host,
2068
+ position: this.popoverOpts?.position,
2069
+ boundary: this.popoverOpts?.boundary,
2070
+ zIndex: this.popoverOpts?.zIndex,
2071
+ className: "vela-color-field-pop",
2072
+ content: buildColorPicker(this.getVal(), this.theme, (val) => {
2073
+ this.onVal(val);
2074
+ this.paint();
2075
+ })
2076
+ });
2077
+ pop.show();
2078
+ }
2079
+ };
2080
+ function closeColorPopover() {
2081
+ closeOpenPopovers();
2082
+ }
2083
+
2084
+ // src/ui/components/glyph-select/controller.ts
2085
+ function glyphSelectController(opts) {
2086
+ let value = opts.value;
2087
+ return {
2088
+ options: opts.options,
2089
+ get value() {
2090
+ return value;
2091
+ },
2092
+ setValue(v) {
2093
+ value = v;
2094
+ },
2095
+ optionOf(v) {
2096
+ return opts.options.find((o) => o.value === v);
2097
+ },
2098
+ pick(v) {
2099
+ value = v;
2100
+ opts.onChange?.(v);
2101
+ return v;
2102
+ }
2103
+ };
2104
+ }
2105
+ var WIDTH_FIELD_OPTIONS = [1, 2, 3, 4, 5];
2106
+ function lineWidthGlyph(width) {
2107
+ return `<svg width="22" height="14" viewBox="0 0 22 14" fill="none"><line x1="2" y1="7" x2="20" y2="7" stroke="currentColor" stroke-width="${width}" stroke-linecap="round"/></svg>`;
2108
+ }
2109
+ function widthFieldOptions() {
2110
+ return WIDTH_FIELD_OPTIONS.map((w) => ({ value: w, label: `${w}px`, glyph: lineWidthGlyph(w) }));
2111
+ }
2112
+
2113
+ // src/ui/components/glyph-select/styles.ts
2114
+ var GLYPH_SELECT_STYLE_ID = "vela-ui-glyph-select";
2115
+ var GLYPH_SELECT_CSS = `
2116
+ .vela-glyph-select {
2117
+ height: 34px;
2118
+ padding: 0 26px 0 8px;
2119
+ border: 1px solid var(--vela-border-strong);
2120
+ border-radius: 6px;
2121
+ background: transparent;
2122
+ cursor: pointer;
2123
+ display: inline-flex;
2124
+ align-items: center;
2125
+ gap: 8px;
2126
+ flex: none;
2127
+ justify-self: start;
2128
+ color: var(--vela-fg-bright);
2129
+ font-size: 14px;
2130
+ font-family: inherit;
2131
+ outline: none;
2132
+ position: relative;
2133
+ transition: border-color 0.12s ease, box-shadow 0.12s ease;
2134
+ }
2135
+ .vela-glyph-select:hover { border-color: var(--vela-fg-muted); }
2136
+ .vela-glyph-select:focus { border-color: var(--vela-focus); box-shadow: 0 0 0 3px var(--vela-focus-soft); }
2137
+ .vela-glyph-select-caret {
2138
+ position: absolute;
2139
+ right: 8px;
2140
+ top: 50%;
2141
+ transform: translateY(-50%);
2142
+ display: flex;
2143
+ opacity: 0.55;
2144
+ color: inherit;
2145
+ line-height: 0;
2146
+ pointer-events: none;
2147
+ }
2148
+ .vela-glyph-select-pop {
2149
+ background: var(--vela-bg);
2150
+ border: none;
2151
+ border-radius: 6px;
2152
+ box-shadow: var(--vela-shadow);
2153
+ padding: 4px;
2154
+ display: flex;
2155
+ flex-direction: column;
2156
+ gap: 1px;
2157
+ color: var(--vela-fg);
2158
+ font: 14px var(--vela-font);
2159
+ }
2160
+ .vela-glyph-select-item {
2161
+ display: flex;
2162
+ align-items: center;
2163
+ gap: 8px;
2164
+ min-width: 96px;
2165
+ padding: 7px 10px;
2166
+ border: none;
2167
+ border-radius: 4px;
2168
+ background: transparent;
2169
+ color: inherit;
2170
+ cursor: pointer;
2171
+ text-align: left;
2172
+ font: inherit;
2173
+ }
2174
+ .vela-glyph-select-item:hover { background: var(--vela-hover); }
2175
+ .vela-glyph-select-item[data-active='1'] { background: var(--vela-hover-strong); color: var(--vela-fg-bright); }
2176
+ `;
2177
+
2178
+ // src/ui/components/glyph-select/view.ts
2179
+ var GlyphSelect = class {
2180
+ constructor(opts) {
2181
+ injectStyles(GLYPH_SELECT_STYLE_ID, GLYPH_SELECT_CSS, document);
2182
+ this.ctrl = glyphSelectController(opts);
2183
+ this.theme = opts.theme;
2184
+ this.get = opts.get ?? (() => this.ctrl.value);
2185
+ const trigger = document.createElement("button");
2186
+ trigger.type = "button";
2187
+ trigger.className = "vela-glyph-select";
2188
+ if (opts.title) trigger.title = opts.title;
2189
+ this.el = trigger;
2190
+ this.paint();
2191
+ trigger.addEventListener("pointerdown", (e) => {
2192
+ e.preventDefault();
2193
+ trigger.focus({ preventScroll: true });
2194
+ });
2195
+ trigger.addEventListener("click", (e) => {
2196
+ e.stopPropagation();
2197
+ this.toggle();
2198
+ });
2199
+ trigger.addEventListener("vela-sync", () => {
2200
+ this.ctrl.setValue(this.get());
2201
+ this.paint();
2202
+ });
2203
+ }
2204
+ setValue(v) {
2205
+ this.ctrl.setValue(v);
2206
+ this.paint();
2207
+ }
2208
+ paint() {
2209
+ const cur = this.get();
2210
+ const opt = this.ctrl.optionOf(cur);
2211
+ const glyph = document.createElement("span");
2212
+ glyph.style.display = "flex";
2213
+ glyph.innerHTML = opt?.glyph ?? "";
2214
+ const caret = document.createElement("span");
2215
+ caret.className = "vela-glyph-select-caret";
2216
+ caret.appendChild(iconEl("chevron-down", this.el.ownerDocument));
2217
+ this.el.replaceChildren(glyph, caret);
2218
+ }
2219
+ toggle() {
2220
+ if (openPopoverTrigger() === this.el) {
2221
+ closeOpenPopovers();
2222
+ return;
2223
+ }
2224
+ const current = this.get();
2225
+ const pop = new Popover({
2226
+ trigger: this.el,
2227
+ theme: this.theme,
2228
+ align: "end",
2229
+ gap: 6,
2230
+ className: "vela-glyph-select-pop",
2231
+ content: (el) => {
2232
+ el.style.font = `var(--vela-font-size-md) ${this.theme.fontFamily}`;
2233
+ for (const o of this.ctrl.options) {
2234
+ const item = document.createElement("button");
2235
+ item.type = "button";
2236
+ item.className = "vela-glyph-select-item";
2237
+ item.dataset.active = o.value === current ? "1" : "0";
2238
+ item.innerHTML = `<span style="display:flex;flex:none;">${o.glyph}</span><span style="flex:1;font-variant-numeric:tabular-nums;">${o.label}</span>`;
2239
+ item.addEventListener("click", (ev) => {
2240
+ ev.stopPropagation();
2241
+ pop.hide();
2242
+ this.ctrl.pick(o.value);
2243
+ this.paint();
2244
+ });
2245
+ el.appendChild(item);
2246
+ }
2247
+ }
2248
+ });
2249
+ pop.show();
2250
+ }
2251
+ };
2252
+ function widthField(theme, getVal, onVal) {
2253
+ return new GlyphSelect({
2254
+ theme,
2255
+ options: widthFieldOptions(),
2256
+ value: getVal(),
2257
+ get: getVal,
2258
+ onChange: onVal
2259
+ }).el;
2260
+ }
2261
+ function closeWidthPopover() {
2262
+ closeOpenPopovers();
2263
+ }
2264
+
2265
+ // src/ui/components/field/controller.ts
2266
+ var FIELD_GAP_PX = 16;
2267
+ function fieldGridColumns(variant, mobile) {
2268
+ if (variant === "inputs") return mobile ? "minmax(0,1fr) auto" : "max-content 1fr";
2269
+ return mobile ? "minmax(0,1fr) max-content" : "max-content max-content";
2270
+ }
2271
+
2272
+ // src/ui/components/field/styles.ts
2273
+ var FIELD_STYLE_ID = "vela-ui-field";
2274
+ var FIELD_CSS = `
2275
+ .vela-field-grid {
2276
+ display: grid;
2277
+ align-items: center;
2278
+ align-content: start;
2279
+ column-gap: ${FIELD_GAP_PX}px;
2280
+ row-gap: ${FIELD_GAP_PX}px;
2281
+ }
2282
+ .vela-field-grid[data-variant='inputs'] { grid-template-columns: max-content 1fr; }
2283
+ .vela-field-grid[data-variant='inputs'][data-mobile] { grid-template-columns: minmax(0,1fr) auto; }
2284
+ .vela-field-grid[data-variant='settings'] {
2285
+ grid-template-columns: max-content max-content;
2286
+ column-gap: 12px;
2287
+ }
2288
+ .vela-field-grid[data-variant='settings'][data-mobile] { grid-template-columns: minmax(0,1fr) max-content; }
2289
+ .vela-field-row { display: contents; }
2290
+ .vela-field-span { grid-column: 1 / -1; }
2291
+ .vela-field-label {
2292
+ opacity: 0.9;
2293
+ white-space: nowrap;
2294
+ overflow: hidden;
2295
+ text-overflow: ellipsis;
2296
+ font-size: 14px;
2297
+ }
2298
+ .vela-field-label[data-size='sm'] { font-size: inherit; opacity: 0.85; }
2299
+ .vela-field-cell { display: flex; align-items: center; gap: 8px; justify-self: start; }
2300
+ .vela-field-bool { display: flex; align-items: center; gap: 8px; min-height: 22px; }
2301
+ .vela-field-stacked { display: flex; flex-direction: column; gap: 8px; }
2302
+ .vela-field-stacked-head { display: flex; align-items: center; justify-content: center; gap: 8px; }
2303
+ .vela-field-inline { display: flex; flex-wrap: wrap; align-items: center; gap: 12px; }
2304
+ .vela-field-inline-item { display: flex; align-items: center; gap: 8px; }
2305
+ .vela-field-toggle-label { display: flex; align-items: center; gap: 8px; }
2306
+ .vela-field-section {
2307
+ grid-column: 1 / -1;
2308
+ margin: 24px 0 0;
2309
+ padding-bottom: 8px;
2310
+ font-size: var(--vela-font-size-sm);
2311
+ font-weight: 700;
2312
+ letter-spacing: 0.05em;
2313
+ text-transform: uppercase;
2314
+ color: var(--vela-fg-muted);
2315
+ }
2316
+ .vela-field-section[data-variant='inputs'] {
2317
+ margin: 0;
2318
+ padding: 20px 0 8px;
2319
+ font-size: 11px;
2320
+ font-weight: 600;
2321
+ }
2322
+ .vela-field-section[data-variant='inputs'][data-first] { padding-top: 4px; }
2323
+ .vela-field-sep { grid-column: 1 / -1; height: 14px; }
2324
+ .vela-field-dim { opacity: 0.4 !important; pointer-events: none !important; }
2325
+ `;
2326
+
2327
+ // src/ui/components/field/view.ts
2328
+ function ensureFieldStyles(doc = document) {
2329
+ injectStyles(FIELD_STYLE_ID, FIELD_CSS, doc);
2330
+ }
2331
+ function fieldGrid(opts = {}) {
2332
+ ensureFieldStyles();
2333
+ const el = document.createElement("div");
2334
+ el.className = "vela-field-grid";
2335
+ const variant = opts.variant ?? "inputs";
2336
+ el.dataset.variant = variant;
2337
+ if (opts.mobile) el.dataset.mobile = "";
2338
+ el.style.gridTemplateColumns = fieldGridColumns(variant, opts.mobile === true);
2339
+ return el;
2340
+ }
2341
+ function fieldSection(title, opts = {}) {
2342
+ ensureFieldStyles();
2343
+ const el = document.createElement("div");
2344
+ el.className = "vela-field-section";
2345
+ if (opts.variant) el.dataset.variant = opts.variant;
2346
+ if (opts.first) el.dataset.first = "";
2347
+ el.textContent = title;
2348
+ return el;
2349
+ }
2350
+ function fieldSeparator() {
2351
+ ensureFieldStyles();
2352
+ const el = document.createElement("div");
2353
+ el.className = "vela-field-sep";
2354
+ return el;
2355
+ }
2356
+ function labelEl(text, opts) {
2357
+ const el = opts.id ? document.createElement("label") : document.createElement("span");
2358
+ if (opts.id && el instanceof HTMLLabelElement) el.htmlFor = opts.id;
2359
+ el.className = "vela-field-label";
2360
+ if (opts.size) el.dataset.size = opts.size;
2361
+ el.textContent = text;
2362
+ return el;
2363
+ }
2364
+ function asList(control) {
2365
+ if (!control) return [];
2366
+ return Array.isArray(control) ? control : [control];
2367
+ }
2368
+ function applyClass(el, extra) {
2369
+ if (extra) {
2370
+ for (const c of extra.split(/\s+/)) if (c) el.classList.add(c);
2371
+ }
2372
+ }
2373
+ function dimControls(controls, on) {
2374
+ for (const c of controls) {
2375
+ if (c.dataset.sdSelfGated === "1") continue;
2376
+ c.classList.toggle("vela-field-dim", !on);
2377
+ }
2378
+ }
2379
+ function fieldRow(opts) {
2380
+ ensureFieldStyles();
2381
+ const size = opts.labelSize;
2382
+ if (opts.inline) {
2383
+ const wrap2 = document.createElement("div");
2384
+ wrap2.className = "vela-field-span vela-field-inline";
2385
+ for (const item of opts.inline) {
2386
+ const it = document.createElement("div");
2387
+ it.className = "vela-field-inline-item";
2388
+ if (item.toggleFirst) it.style.flexDirection = "row-reverse";
2389
+ if (item.label) {
2390
+ const lbl = labelEl(item.label, { id: item.id, size });
2391
+ if (item.toggleFirst) lbl.style.cursor = "pointer";
2392
+ it.appendChild(lbl);
2393
+ }
2394
+ if (item.fit) it.appendChild(item.control);
2395
+ else {
2396
+ const cw = document.createElement("div");
2397
+ cw.style.width = "100px";
2398
+ cw.appendChild(item.control);
2399
+ it.appendChild(cw);
2400
+ }
2401
+ wrap2.appendChild(it);
2402
+ }
2403
+ if (opts.info) wrap2.appendChild(opts.info);
2404
+ applyClass(wrap2, opts.className);
2405
+ return wrap2;
2406
+ }
2407
+ if (opts.bool) {
2408
+ const wrap2 = document.createElement("div");
2409
+ wrap2.className = "vela-field-span vela-field-bool";
2410
+ const sw = new Switch({
2411
+ id: opts.toggle?.id ?? opts.id,
2412
+ checked: opts.toggle?.checked ?? false,
2413
+ size: "md",
2414
+ tone: "bright",
2415
+ onChange: (v) => opts.toggle?.onChange(v)
2416
+ });
2417
+ wrap2.append(sw.el, labelEl(opts.label, { size }));
2418
+ if (opts.info) wrap2.appendChild(opts.info);
2419
+ if (opts.toggle?.get) {
2420
+ const get = opts.toggle.get;
2421
+ wrap2.addEventListener("vela-sync", () => {
2422
+ sw.setChecked(get());
2423
+ });
2424
+ }
2425
+ applyClass(wrap2, opts.className);
2426
+ return wrap2;
2427
+ }
2428
+ if (opts.stacked) {
2429
+ const wrap2 = document.createElement("div");
2430
+ wrap2.className = "vela-field-span vela-field-stacked";
2431
+ const head = document.createElement("div");
2432
+ head.className = "vela-field-stacked-head";
2433
+ head.appendChild(labelEl(opts.label, { id: opts.id, size }));
2434
+ if (opts.info) head.appendChild(opts.info);
2435
+ wrap2.appendChild(head);
2436
+ for (const c of asList(opts.control)) wrap2.appendChild(c);
2437
+ applyClass(wrap2, opts.className);
2438
+ return wrap2;
2439
+ }
2440
+ const wrap = document.createElement("div");
2441
+ wrap.className = "vela-field-row";
2442
+ const controls = asList(opts.control);
2443
+ if (opts.toggle) {
2444
+ const left = document.createElement("div");
2445
+ left.className = "vela-field-toggle-label";
2446
+ const sw = new Switch({
2447
+ id: opts.toggle.id ?? opts.id,
2448
+ checked: opts.toggle.checked,
2449
+ size: "md",
2450
+ tone: "bright",
2451
+ onChange: (v) => {
2452
+ opts.toggle.onChange(v);
2453
+ if (controls.length > 0) dimControls(controls, v);
2454
+ }
2455
+ });
2456
+ left.append(sw.el, labelEl(opts.label, { size }));
2457
+ wrap.appendChild(left);
2458
+ if (controls.length > 0) dimControls(controls, opts.toggle.checked);
2459
+ if (opts.toggle.get) {
2460
+ const get = opts.toggle.get;
2461
+ wrap.addEventListener("vela-sync", () => {
2462
+ const v = get();
2463
+ sw.setChecked(v);
2464
+ if (controls.length > 0) dimControls(controls, v);
2465
+ });
2466
+ }
2467
+ } else {
2468
+ wrap.appendChild(labelEl(opts.label, { id: opts.id, size }));
2469
+ }
2470
+ const cell = document.createElement("div");
2471
+ cell.className = "vela-field-cell";
2472
+ if (opts.controlWidth != null && controls.length === 1 && !opts.fit) {
2473
+ const cw = document.createElement("div");
2474
+ cw.style.width = `${opts.controlWidth}px`;
2475
+ cw.appendChild(controls[0]);
2476
+ cell.appendChild(cw);
2477
+ } else {
2478
+ for (const c of controls) cell.appendChild(c);
2479
+ }
2480
+ if (opts.info) cell.appendChild(opts.info);
2481
+ wrap.appendChild(cell);
2482
+ applyClass(wrap, opts.className);
2483
+ return wrap;
2484
+ }
2485
+ function buildFieldControl(desc) {
2486
+ if (desc.kind === "number") {
2487
+ const ni = new NumberInput({
2488
+ id: desc.id,
2489
+ value: desc.value,
2490
+ min: desc.min,
2491
+ max: desc.max,
2492
+ step: desc.step,
2493
+ integer: desc.integer,
2494
+ size: "md",
2495
+ fill: desc.fill,
2496
+ compact: desc.compact,
2497
+ commit: desc.commit,
2498
+ steppers: desc.steppers,
2499
+ clamp: desc.clamp,
2500
+ placeholder: desc.placeholder,
2501
+ emptyValue: desc.emptyValue,
2502
+ title: desc.title,
2503
+ onChange: desc.onChange
2504
+ });
2505
+ if (desc.sync) {
2506
+ const sync = desc.sync;
2507
+ ni.el.addEventListener("vela-sync", () => {
2508
+ ni.setValue(sync());
2509
+ });
2510
+ }
2511
+ return { el: ni.el, setValue: (v) => ni.setValue(Number(v)) };
2512
+ }
2513
+ if (desc.kind === "select") {
2514
+ const sel = new Select({
2515
+ id: desc.id,
2516
+ options: desc.options,
2517
+ value: desc.value,
2518
+ size: "md",
2519
+ fill: desc.fill,
2520
+ theme: desc.theme,
2521
+ list: desc.list,
2522
+ onChange: desc.onChange
2523
+ });
2524
+ if (desc.title) sel.el.title = desc.title;
2525
+ if (desc.sync) {
2526
+ const sync = desc.sync;
2527
+ sel.el.addEventListener("vela-sync", () => {
2528
+ sel.setValue(sync());
2529
+ });
2530
+ }
2531
+ return { el: sel.el, setValue: (v) => sel.setValue(String(v)) };
2532
+ }
2533
+ if (desc.kind === "switch") {
2534
+ const sw = new Switch({
2535
+ id: desc.id,
2536
+ checked: desc.checked,
2537
+ size: "md",
2538
+ tone: "bright",
2539
+ onChange: desc.onChange
2540
+ });
2541
+ if (desc.sync) {
2542
+ const sync = desc.sync;
2543
+ sw.el.addEventListener("vela-sync", () => {
2544
+ sw.setChecked(sync());
2545
+ });
2546
+ }
2547
+ return { el: sw.el, setValue: (v) => sw.setChecked(Boolean(v)) };
2548
+ }
2549
+ if (desc.kind === "color") {
2550
+ const el2 = colorField(desc.theme, desc.get, desc.onChange, {
2551
+ shape: "circle",
2552
+ id: desc.id,
2553
+ popover: desc.popover
2554
+ });
2555
+ if (desc.title) el2.title = desc.title;
2556
+ return { el: el2 };
2557
+ }
2558
+ if (desc.kind === "text") {
2559
+ const tf = new TextField({
2560
+ id: desc.id,
2561
+ value: desc.value,
2562
+ size: "md",
2563
+ fill: desc.fill,
2564
+ onChange: desc.onChange
2565
+ });
2566
+ if (desc.placeholder) tf.input.placeholder = desc.placeholder;
2567
+ if (desc.sync) {
2568
+ const sync = desc.sync;
2569
+ tf.el.addEventListener("vela-sync", () => {
2570
+ tf.setValue(sync());
2571
+ });
2572
+ }
2573
+ return { el: tf.el, setValue: (v) => tf.setValue(String(v)) };
2574
+ }
2575
+ if (desc.kind === "textarea") {
2576
+ const ta = new TextArea({
2577
+ id: desc.id,
2578
+ value: desc.value,
2579
+ rows: desc.rows,
2580
+ autoGrow: desc.autoGrow,
2581
+ maxLines: desc.maxLines,
2582
+ placeholder: desc.placeholder,
2583
+ onChange: desc.onChange
2584
+ });
2585
+ if (desc.sync) {
2586
+ const sync = desc.sync;
2587
+ ta.el.addEventListener("vela-sync", () => {
2588
+ ta.setValue(sync());
2589
+ });
2590
+ }
2591
+ return { el: ta.el, setValue: (v) => ta.setValue(String(v)) };
2592
+ }
2593
+ const el = widthField(desc.theme, desc.get, desc.onChange);
2594
+ if (desc.title) el.title = desc.title;
2595
+ return { el };
2596
+ }
2597
+
2598
+ export { ColorField, Dialog, FIELD_GAP_PX, GlyphSelect, Menu, NumberInput, Popover, STATIC_TOKENS, Select, Switch, TextArea, TextField, WIDTH_FIELD_OPTIONS, applyPlotOverlayTokens, applyThemeTokens, blendOver, buildColorPicker, buildFieldControl, buildPalette, clampNumber, closeColorPopover, closeOpenPopovers, closeWidthPopover, colorField, colorPickerController, combineColor, decorateSelectScroll, dialogController, ensureUIHost, eventDismissedPopover, fieldGrid, fieldGridColumns, fieldRow, fieldSection, fieldSeparator, fillSelectList, glyphSelectController, hslHex, insetRect, intersectRects, isPopoverOpen, lineWidthGlyph, menuController, nextUid, numberInputController, openPopoverTrigger, openSelectList, placePopover, popoverController, runMachine, selectController, snapToStep, splitColor, switchController, textAreaController, textFieldController, themeTokens, toggleSelectList, transparencyChecker, viewportRect, widthField, widthFieldOptions };