@luxalgo/vela 0.5.2 → 0.5.4

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 (47) hide show
  1. package/README.md +3 -2
  2. package/dist/{DataProvider-DTOEXKTI.d.ts → DataProvider-Cqc_BaJ9.d.ts} +1 -1
  3. package/dist/{DataProvider-gnzv-dgc.d.cts → DataProvider-DgS2UyMc.d.cts} +1 -1
  4. package/dist/{chunk-RRFGWZNJ.js → chunk-D6WM5IUE.js} +786 -864
  5. package/dist/{chunk-LJLZR44Y.js → chunk-EMS6PO2T.js} +5 -7
  6. package/dist/{chunk-Y7TK4ZWK.js → chunk-JBQUY2RI.js} +126 -19
  7. package/dist/chunk-OGRQW3M6.js +1328 -0
  8. package/dist/{chunk-P556H6EA.js → chunk-QWIEKZRE.js} +59 -34
  9. package/dist/{chunk-B5TIKQJ5.js → chunk-ZNL2X6U2.js} +16 -2
  10. package/dist/{contributions-Ci_i3IN2.d.cts → contributions-4Nh1jbvb.d.cts} +2 -2
  11. package/dist/{contributions-b6AVrqqG.d.ts → contributions-DuIxJWeH.d.ts} +2 -2
  12. package/dist/{history-Cf1lVsap.d.cts → history-FsmvCIo4.d.ts} +5 -3
  13. package/dist/{history-Ci3M0xLV.d.ts → history-HofqYEh2.d.cts} +5 -3
  14. package/dist/index.cjs +2783 -1611
  15. package/dist/index.d.cts +14 -8
  16. package/dist/index.d.ts +14 -8
  17. package/dist/index.js +4 -4
  18. package/dist/{options-XTBpbx0s.d.ts → options-BlQ00Fju.d.cts} +3 -1
  19. package/dist/{options-XTBpbx0s.d.cts → options-BlQ00Fju.d.ts} +3 -1
  20. package/dist/{plugin-DfH4Y-Om.d.cts → plugin-DJR6z3e0.d.cts} +8 -7
  21. package/dist/{plugin-CnNd7cuF.d.ts → plugin-N_EaXN4-.d.ts} +8 -7
  22. package/dist/plugin.cjs +4 -0
  23. package/dist/plugin.d.cts +4 -4
  24. package/dist/plugin.d.ts +4 -4
  25. package/dist/plugin.js +2 -2
  26. package/dist/providers/binance.d.cts +2 -2
  27. package/dist/providers/binance.d.ts +2 -2
  28. package/dist/providers/coinbase.d.cts +2 -2
  29. package/dist/providers/coinbase.d.ts +2 -2
  30. package/dist/providers/hyperliquid.d.cts +2 -2
  31. package/dist/providers/hyperliquid.d.ts +2 -2
  32. package/dist/ui.cjs +1318 -4
  33. package/dist/ui.d.cts +383 -2
  34. package/dist/ui.d.ts +383 -2
  35. package/dist/ui.js +3 -3
  36. package/dist/vela.global.js +2777 -1605
  37. package/dist/vela.global.min.js +315 -66
  38. package/dist/widget.cjs +2358 -1052
  39. package/dist/widget.d.cts +26 -8
  40. package/dist/widget.d.ts +26 -8
  41. package/dist/widget.js +27 -9
  42. package/dist/workspace.cjs +2264 -946
  43. package/dist/workspace.d.cts +10 -5
  44. package/dist/workspace.d.ts +10 -5
  45. package/dist/workspace.js +25 -7
  46. package/package.json +1 -1
  47. package/dist/chunk-ATPU5CI6.js +0 -81
@@ -0,0 +1,1328 @@
1
+ import { injectStyles, iconEl, isDarkColor, mix, HIGHLIGHT, ACCENT_BRIGHT, ACCENT, withAlpha, BULLISH, BEARISH, WARNING, NEUTRAL } from './chunk-EMS6PO2T.js';
2
+
3
+ // src/ui/components/switch/controller.ts
4
+ function switchController(opts = {}) {
5
+ let checked = opts.checked ?? false;
6
+ const disabled = opts.disabled ?? false;
7
+ return {
8
+ get checked() {
9
+ return checked;
10
+ },
11
+ disabled,
12
+ size: opts.size ?? "md",
13
+ tone: opts.tone ?? "bright",
14
+ setChecked(v) {
15
+ checked = v;
16
+ },
17
+ toggle() {
18
+ if (disabled) return checked;
19
+ checked = !checked;
20
+ opts.onChange?.(checked);
21
+ return checked;
22
+ }
23
+ };
24
+ }
25
+
26
+ // src/ui/components/switch/styles.ts
27
+ var SWITCH_STYLE_ID = "vela-ui-switch";
28
+ var SWITCH_CSS = `
29
+ .vela-switch {
30
+ width: 20px;
31
+ height: 20px;
32
+ flex: 0 0 auto;
33
+ display: inline-flex;
34
+ align-items: center;
35
+ justify-content: center;
36
+ padding: 0;
37
+ border: 1px solid var(--vela-border);
38
+ border-radius: 5px;
39
+ background: transparent;
40
+ color: transparent;
41
+ cursor: pointer;
42
+ line-height: 0;
43
+ transition: background 0.12s ease, border-color 0.12s ease, color 0.12s ease;
44
+ }
45
+ .vela-switch .vela-icon, .vela-switch svg { display: block; width: 12px; height: 12px; }
46
+ .vela-switch[data-size='sm'] .vela-icon, .vela-switch[data-size='sm'] svg { width: 11px; height: 11px; }
47
+ .vela-switch:hover { border-color: var(--vela-fg-muted); }
48
+ .vela-switch[data-checked] {
49
+ background: var(--vela-fg-bright);
50
+ border-color: var(--vela-fg-bright);
51
+ color: var(--vela-bg);
52
+ }
53
+ .vela-switch[data-size='sm'] { width: 18px; height: 18px; border-color: var(--vela-border-strong); }
54
+ .vela-switch[data-size='sm']:hover { border-color: var(--vela-fg-muted); }
55
+ .vela-switch[data-size='sm'][data-checked] {
56
+ background: var(--vela-selected-bg);
57
+ border-color: var(--vela-selected-bg);
58
+ color: var(--vela-selected-fg);
59
+ }
60
+ .vela-switch[data-tone='selected'][data-checked] {
61
+ background: var(--vela-selected-bg);
62
+ border-color: var(--vela-selected-bg);
63
+ color: var(--vela-selected-fg);
64
+ }
65
+ .vela-switch[disabled] { opacity: 0.4; cursor: default; }
66
+ `;
67
+
68
+ // src/ui/components/switch/view.ts
69
+ var Switch = class {
70
+ constructor(opts = {}) {
71
+ injectStyles(SWITCH_STYLE_ID, SWITCH_CSS, document);
72
+ this.ctrl = switchController(opts);
73
+ const b = document.createElement("button");
74
+ b.type = "button";
75
+ if (opts.id) b.id = opts.id;
76
+ b.className = "vela-switch";
77
+ b.dataset.size = this.ctrl.size;
78
+ b.dataset.tone = this.ctrl.tone;
79
+ b.setAttribute("role", "switch");
80
+ if (this.ctrl.disabled) b.disabled = true;
81
+ b.appendChild(iconEl("check", b.ownerDocument));
82
+ this.el = b;
83
+ this.paint();
84
+ b.addEventListener("pointerdown", (e) => {
85
+ e.preventDefault();
86
+ b.focus({ preventScroll: true });
87
+ });
88
+ b.addEventListener("click", () => {
89
+ this.ctrl.toggle();
90
+ this.paint();
91
+ });
92
+ }
93
+ get checked() {
94
+ return this.ctrl.checked;
95
+ }
96
+ setChecked(v) {
97
+ this.ctrl.setChecked(v);
98
+ this.paint();
99
+ }
100
+ paint() {
101
+ const on = this.ctrl.checked;
102
+ this.el.setAttribute("aria-checked", on ? "true" : "false");
103
+ if (on) this.el.dataset.checked = "";
104
+ else delete this.el.dataset.checked;
105
+ }
106
+ };
107
+
108
+ // src/core/tokens.ts
109
+ var STATIC_TOKENS = {
110
+ "--vela-space-1": "4px",
111
+ "--vela-space-2": "8px",
112
+ "--vela-space-3": "12px",
113
+ "--vela-space-4": "16px",
114
+ "--vela-radius-sm": "4px",
115
+ "--vela-radius-md": "6px",
116
+ "--vela-radius-lg": "10px",
117
+ "--vela-z-tooltip": "60",
118
+ "--vela-z-menu": "50",
119
+ "--vela-z-dialog": "40",
120
+ // Form popovers portal to <body> (or a chart host) and must sit above a dialog
121
+ // whose own stacking context may be nested inside the chart container.
122
+ "--vela-z-popover": "6000",
123
+ "--vela-ease": "cubic-bezier(0.22, 1, 0.36, 1)",
124
+ "--vela-dur-fast": "90ms",
125
+ "--vela-dur-med": "160ms",
126
+ "--vela-font-size-sm": "11px",
127
+ "--vela-font-size-md": "12px",
128
+ "--vela-font-size-lg": "14px"
129
+ };
130
+ function themeTokens(t) {
131
+ const dark = isDarkColor(t.background);
132
+ const wash = (a) => dark ? `rgba(255,255,255,${a})` : withAlpha(t.textColor, a + 0.02);
133
+ const elevated = mix(t.background, dark ? "#ffffff" : t.textColor, dark ? 0.03 : 0.05);
134
+ return {
135
+ "--vela-font": t.fontFamily,
136
+ "--vela-bg": t.background,
137
+ // Chrome text is slightly brighter than the chart's own axis text, which is
138
+ // deliberately recessive; the chart keeps using `t.textColor` directly.
139
+ "--vela-fg": dark ? "#d1d4dc" : t.textColor,
140
+ "--vela-fg-muted": dark ? "#868a96" : withAlpha(t.textColor, 0.62),
141
+ "--vela-fg-faint": withAlpha(t.textColor, 0.35),
142
+ "--vela-fg-bright": dark ? "#f0f3fa" : "#000000",
143
+ "--vela-surface": t.background,
144
+ // Panels and menus float ABOVE the chart, so their surface is the wash flattened onto
145
+ // the chart background — opaque, or candles read through the panel.
146
+ "--vela-surface-elev": elevated,
147
+ "--vela-surface-overlay": elevated,
148
+ // Recessed fields (inputs, selects) read as cut INTO their panel, so they fall back
149
+ // to the chart surface and are separated from the panel by their border alone.
150
+ "--vela-surface-sunken": t.background,
151
+ "--vela-border": t.borderColor,
152
+ "--vela-border-strong": dark ? "#34353b" : withAlpha(t.textColor, 0.28),
153
+ "--vela-border-soft": t.borderColor,
154
+ // Barely-there rules INSIDE a panel (row separators), where a full border would
155
+ // chop the list into boxes.
156
+ "--vela-border-faint": withAlpha(t.textColor, 0.08),
157
+ "--vela-hover": wash(0.06),
158
+ "--vela-active": wash(0.1),
159
+ // A deliberately stronger hover for rows inside an already-tinted surface (menu
160
+ // items in an active flyout), where the normal wash would not separate from it.
161
+ "--vela-hover-strong": wash(0.16),
162
+ "--vela-focus": withAlpha(t.textColor, 0.5),
163
+ "--vela-focus-soft": withAlpha(t.textColor, 0.12),
164
+ // Separator hover — the SAME wash the chart's pane separators paint on hover
165
+ // (soft full-thickness band + solid 2px center line), so DOM-drawn dividers
166
+ // (the workspace grid) and canvas-drawn ones read as one family.
167
+ "--vela-separator-hover-band": withAlpha(t.textColor, 0.1),
168
+ "--vela-separator-hover-line": withAlpha(t.textColor, 0.55),
169
+ "--vela-scroll": withAlpha(t.textColor, 0.3),
170
+ "--vela-accent": ACCENT,
171
+ "--vela-accent-bright": ACCENT_BRIGHT,
172
+ "--vela-highlight": HIGHLIGHT,
173
+ // The inverse chip: a filled selected state (active tab, ticked checkbox). Its ink
174
+ // must contrast the fill, so the pair flips together with the theme.
175
+ "--vela-selected-bg": dark ? "#f0f3fa" : t.textColor,
176
+ "--vela-selected-fg": dark ? t.background : "#ffffff",
177
+ // Fixed ink for saturated fills (accent buttons, categorical avatars) — those fills
178
+ // are theme-independent, so their ink is too.
179
+ "--vela-fg-on-fill": "#ffffff",
180
+ "--vela-up": t.upColor,
181
+ "--vela-down": t.downColor,
182
+ "--vela-danger": t.downColor,
183
+ "--vela-shadow": "0 8px 30px rgba(0,0,0,0.5)",
184
+ "--vela-shadow-dialog": "0 20px 60px rgba(0,0,0,0.5)",
185
+ "--vela-backdrop": "rgba(0,0,0,0.45)"
186
+ };
187
+ }
188
+
189
+ // src/ui/tokens.ts
190
+ var STATIC_ID = "vela-ui-tokens";
191
+ var STATIC_DECLS = Object.entries(STATIC_TOKENS).map(([k, v]) => ` ${k}: ${v};`).join("\n");
192
+ var STATIC_CSS = `
193
+ .vela-ui, .vela-ui-layer {
194
+ ${STATIC_DECLS}
195
+ font-family: var(--vela-font, -apple-system, system-ui, sans-serif);
196
+ box-sizing: border-box;
197
+ }
198
+ .vela-ui *, .vela-ui-layer * { box-sizing: border-box; }
199
+ .vela-icon { display: inline-flex; align-items: center; flex: none; }
200
+ .vela-icon svg { display: block; }
201
+ `;
202
+ function applyThemeTokens(el, t) {
203
+ const tokens = themeTokens(t);
204
+ for (const key in tokens) el.style.setProperty(key, tokens[key]);
205
+ }
206
+ function applyPlotOverlayTokens(host, base, config) {
207
+ const layout = config?.layout;
208
+ const t = layout?.background && layout.textColor ? { ...base, background: layout.background, textColor: layout.textColor } : base;
209
+ applyThemeTokens(host, t);
210
+ }
211
+ function ensureUIHost(el, theme) {
212
+ injectStyles(STATIC_ID, STATIC_CSS, el.getRootNode());
213
+ el.classList.add("vela-ui");
214
+ if (theme) applyThemeTokens(el, theme);
215
+ }
216
+
217
+ // src/ui/components/popover/controller.ts
218
+ function insetRect(r, inset) {
219
+ return {
220
+ left: r.left + inset,
221
+ top: r.top + inset,
222
+ right: r.right - inset,
223
+ bottom: r.bottom - inset,
224
+ width: r.width - inset * 2,
225
+ height: r.height - inset * 2
226
+ };
227
+ }
228
+ function viewportRect(width, height, inset) {
229
+ return {
230
+ left: inset,
231
+ top: inset,
232
+ right: width - inset,
233
+ bottom: height - inset,
234
+ width: width - inset * 2,
235
+ height: height - inset * 2
236
+ };
237
+ }
238
+ function intersectRects(a, b) {
239
+ const left = Math.max(a.left, b.left);
240
+ const top = Math.max(a.top, b.top);
241
+ const right = Math.min(a.right, b.right);
242
+ const bottom = Math.min(a.bottom, b.bottom);
243
+ return { left, top, right, bottom, width: right - left, height: bottom - top };
244
+ }
245
+ function placePopover(a) {
246
+ let left = a.align === "end" ? a.trigger.right - a.pop.width : a.trigger.left;
247
+ const below = a.trigger.bottom + a.gap;
248
+ const above = a.trigger.top - a.pop.height - a.gap;
249
+ const fitsBelow = below + a.pop.height <= a.clamp.bottom;
250
+ const fitsAbove = above >= a.clamp.top;
251
+ let top = below;
252
+ if (!fitsBelow && (fitsAbove || a.trigger.top - a.clamp.top > a.clamp.bottom - a.trigger.bottom)) {
253
+ top = Math.max(a.clamp.top, above);
254
+ }
255
+ if (left + a.pop.width > a.clamp.right) left = a.clamp.right - a.pop.width;
256
+ if (left < a.clamp.left) left = a.clamp.left;
257
+ if (top + a.pop.height > a.clamp.bottom) top = a.clamp.bottom - a.pop.height;
258
+ if (top < a.clamp.top) top = a.clamp.top;
259
+ return {
260
+ left: Math.round(left - a.originX),
261
+ top: Math.round(top - a.originY)
262
+ };
263
+ }
264
+ function popoverController(opts = {}) {
265
+ return {
266
+ gap: opts.gap ?? 4,
267
+ align: opts.align ?? "start",
268
+ matchWidth: opts.matchWidth ?? false,
269
+ position: opts.position ?? "fixed",
270
+ boundaryInset: opts.boundaryInset ?? 0,
271
+ viewportInset: opts.viewportInset ?? 6,
272
+ onClose: opts.onClose
273
+ };
274
+ }
275
+
276
+ // src/ui/components/popover/styles.ts
277
+ var POPOVER_STYLE_ID = "vela-ui-popover";
278
+ var POPOVER_CSS = `
279
+ .vela-popover {
280
+ position: fixed;
281
+ z-index: var(--vela-z-popover);
282
+ box-sizing: border-box;
283
+ /* Never serve as a scroll anchor: the shell is portaled and re-placed between
284
+ gestures, and anchoring against it jumps the scroller underneath. */
285
+ overflow-anchor: none;
286
+ }
287
+ .vela-popover[data-position='absolute'] { position: absolute; }
288
+ .vela-popover[hidden] { display: none !important; }
289
+ `;
290
+
291
+ // src/ui/components/popover/view.ts
292
+ var open = null;
293
+ function closeOpenPopovers() {
294
+ open?.hide();
295
+ }
296
+ function isPopoverOpen() {
297
+ return open !== null;
298
+ }
299
+ function openPopoverTrigger() {
300
+ return open?.trigger ?? null;
301
+ }
302
+ function toRect(r) {
303
+ return { left: r.left, top: r.top, right: r.right, bottom: r.bottom, width: r.width, height: r.height };
304
+ }
305
+ var Popover = class {
306
+ constructor(opts) {
307
+ this.onOutside = null;
308
+ this.onKey = null;
309
+ this.onReflow = null;
310
+ this.shown = false;
311
+ const doc = opts.trigger.ownerDocument;
312
+ injectStyles(POPOVER_STYLE_ID, POPOVER_CSS, doc);
313
+ this.trigger = opts.trigger;
314
+ this.host = opts.host ?? doc.body;
315
+ this.ctrl = popoverController(opts);
316
+ this.boundary = opts.boundary ?? "viewport";
317
+ this.theme = opts.theme;
318
+ this.el = doc.createElement("div");
319
+ this.el.className = "vela-popover vela-ui-layer" + (opts.className ? ` ${opts.className}` : "");
320
+ this.el.dataset.position = this.ctrl.position;
321
+ if (opts.zIndex !== void 0) this.el.style.zIndex = String(opts.zIndex);
322
+ this.el.addEventListener("pointerdown", (e) => e.stopPropagation());
323
+ if (opts.content instanceof Node) this.el.appendChild(opts.content);
324
+ else if (typeof opts.content === "function") opts.content(this.el);
325
+ }
326
+ get open() {
327
+ return this.shown;
328
+ }
329
+ get position() {
330
+ return this.ctrl.position;
331
+ }
332
+ get align() {
333
+ return this.ctrl.align;
334
+ }
335
+ show() {
336
+ if (this.shown) {
337
+ this.place();
338
+ return;
339
+ }
340
+ if (open && open !== this) open.hide();
341
+ ensureUIHost(this.el, this.theme);
342
+ this.host.appendChild(this.el);
343
+ this.shown = true;
344
+ open = this;
345
+ this.place();
346
+ const onOutside = (ev) => {
347
+ const t = ev.target;
348
+ if (this.el.contains(t) || this.trigger.contains(t)) return;
349
+ this.hide();
350
+ };
351
+ const onKey = (ev) => {
352
+ if (ev.key !== "Escape") return;
353
+ ev.preventDefault();
354
+ ev.stopPropagation();
355
+ this.hide();
356
+ };
357
+ const onReflow = () => this.place();
358
+ setTimeout(() => document.addEventListener("pointerdown", onOutside, true), 0);
359
+ document.addEventListener("keydown", onKey, true);
360
+ window.addEventListener("resize", onReflow, true);
361
+ document.addEventListener("scroll", onReflow, true);
362
+ this.onOutside = onOutside;
363
+ this.onKey = onKey;
364
+ this.onReflow = onReflow;
365
+ }
366
+ hide() {
367
+ if (!this.shown) return;
368
+ if (this.onOutside) document.removeEventListener("pointerdown", this.onOutside, true);
369
+ if (this.onKey) document.removeEventListener("keydown", this.onKey, true);
370
+ if (this.onReflow) {
371
+ window.removeEventListener("resize", this.onReflow, true);
372
+ document.removeEventListener("scroll", this.onReflow, true);
373
+ }
374
+ this.onOutside = null;
375
+ this.onKey = null;
376
+ this.onReflow = null;
377
+ this.el.remove();
378
+ this.shown = false;
379
+ if (open === this) open = null;
380
+ this.ctrl.onClose?.();
381
+ }
382
+ /** Show if closed, hide if this instance is the open popover. */
383
+ toggle() {
384
+ if (this.shown) this.hide();
385
+ else this.show();
386
+ }
387
+ destroy() {
388
+ this.hide();
389
+ }
390
+ reposition() {
391
+ if (this.shown) this.place();
392
+ }
393
+ clampRect() {
394
+ const view = viewportRect(window.innerWidth, window.innerHeight, this.ctrl.viewportInset);
395
+ const bound = this.readBoundary();
396
+ if (!bound) return view;
397
+ const inset = insetRect(bound, this.ctrl.boundaryInset);
398
+ return intersectRects(view, inset);
399
+ }
400
+ readBoundary() {
401
+ const b = this.boundary;
402
+ if (b === "viewport") return null;
403
+ const raw = typeof b === "function" ? b() : b.getBoundingClientRect();
404
+ return raw ? toRect(raw) : null;
405
+ }
406
+ place() {
407
+ const ar = this.trigger.getBoundingClientRect();
408
+ if (this.ctrl.matchWidth) {
409
+ this.el.style.minWidth = `${Math.round(Math.max(this.el.offsetWidth, ar.width))}px`;
410
+ }
411
+ const origin = this.ctrl.position === "absolute" ? this.host.getBoundingClientRect() : { left: 0, top: 0 };
412
+ const pos = placePopover({
413
+ trigger: toRect(ar),
414
+ pop: { width: this.el.offsetWidth, height: this.el.offsetHeight },
415
+ gap: this.ctrl.gap,
416
+ align: this.ctrl.align,
417
+ clamp: this.clampRect(),
418
+ originX: origin.left,
419
+ originY: origin.top
420
+ });
421
+ this.el.style.left = `${pos.left}px`;
422
+ this.el.style.top = `${pos.top}px`;
423
+ }
424
+ };
425
+
426
+ // src/ui/components/select/controller.ts
427
+ function selectController(opts) {
428
+ const options = opts.options;
429
+ let value = opts.value ?? options[0]?.value ?? "";
430
+ const size = opts.size ?? "md";
431
+ return {
432
+ options,
433
+ get value() {
434
+ return value;
435
+ },
436
+ size,
437
+ fill: opts.fill ?? size !== "sm",
438
+ disabled: opts.disabled ?? false,
439
+ setValue(v) {
440
+ value = v;
441
+ },
442
+ labelOf(v) {
443
+ return options.find((o) => o.value === v)?.label ?? v;
444
+ },
445
+ pick(v) {
446
+ value = v;
447
+ const label = options.find((o) => o.value === v)?.label ?? v;
448
+ opts.onChange?.(v, label);
449
+ return { value: v, label };
450
+ }
451
+ };
452
+ }
453
+
454
+ // src/ui/components/select/styles.ts
455
+ var SELECT_STYLE_ID = "vela-ui-select";
456
+ var SELECT_CSS = `
457
+ .vela-select { position: relative; display: inline-block; min-width: 0; }
458
+ .vela-select[data-fill] { width: 100%; }
459
+ .vela-select-trigger {
460
+ display: flex;
461
+ align-items: center;
462
+ width: 100%;
463
+ box-sizing: border-box;
464
+ height: 34px;
465
+ padding: 0 26px 0 8px;
466
+ background: transparent;
467
+ border: 1px solid var(--vela-border-strong);
468
+ border-radius: 6px;
469
+ color: var(--vela-fg-bright);
470
+ font-size: 14px;
471
+ font-family: inherit;
472
+ cursor: pointer;
473
+ text-align: left;
474
+ outline: none;
475
+ transition: border-color 0.12s ease, box-shadow 0.12s ease;
476
+ }
477
+ .vela-select-trigger:hover { border-color: var(--vela-fg-muted); }
478
+ .vela-select-trigger:focus { border-color: var(--vela-focus); box-shadow: 0 0 0 3px var(--vela-focus-soft); }
479
+ /* As a grid item (chart-settings rows dissolve via display:contents into a shared
480
+ max-content column) the wrap must NOT stretch to the column: it sizes to its own
481
+ widest option, like the native select it replaces. */
482
+ .vela-select[data-size='sm'] { max-width: 200px; flex: 0 0 auto; justify-self: start; }
483
+ .vela-select[data-size='sm'] .vela-select-trigger {
484
+ height: 28px;
485
+ background: var(--vela-surface-elev);
486
+ border-radius: var(--vela-radius-sm);
487
+ color: var(--vela-fg);
488
+ font-size: 13px;
489
+ box-shadow: none;
490
+ }
491
+ .vela-select[data-size='sm'] .vela-select-trigger:focus { box-shadow: none; border-color: var(--vela-fg-muted); }
492
+ .vela-select-label { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
493
+ /* Invisible option-label stack: gives the shrink-to-fit wrap the WIDEST option's
494
+ intrinsic width (like a native select), so picking a shorter/longer label never
495
+ resizes the trigger and shifts the row around it. */
496
+ .vela-select-sizer { visibility: hidden; height: 0; overflow: hidden; font-size: 14px; }
497
+ .vela-select[data-size='sm'] .vela-select-sizer { font-size: 13px; }
498
+ .vela-select-sizer span { display: block; height: 0; white-space: nowrap; padding: 0 26px 0 8px; border-inline: 1px solid transparent; }
499
+ .vela-select-chevron {
500
+ position: absolute;
501
+ right: 8px;
502
+ top: 50%;
503
+ transform: translateY(-50%);
504
+ pointer-events: none;
505
+ display: flex;
506
+ opacity: 0.55;
507
+ line-height: 0;
508
+ color: inherit;
509
+ }
510
+ .vela-select-chevron .vela-icon, .vela-select-chevron svg { width: 12px; height: 12px; display: block; }
511
+ .vela-select-list {
512
+ background: var(--vela-bg);
513
+ color: var(--vela-fg);
514
+ border: none;
515
+ border-radius: 6px;
516
+ box-shadow: var(--vela-shadow);
517
+ font: 14px var(--vela-font);
518
+ padding: 4px;
519
+ overflow: hidden;
520
+ }
521
+ .vela-select-list[data-size='sm'] { font-size: 13px; background: var(--vela-surface-overlay); }
522
+ .vela-select-items { max-height: none; overflow: hidden; }
523
+ .vela-select-list.is-scroll { display: flex; align-items: stretch; gap: 2px; }
524
+ .vela-select-list.is-scroll .vela-select-items {
525
+ flex: 1 1 auto;
526
+ min-width: 0;
527
+ max-height: 240px;
528
+ overflow-y: auto;
529
+ scrollbar-width: none;
530
+ }
531
+ .vela-select-list.is-scroll .vela-select-items::-webkit-scrollbar { display: none; width: 0; height: 0; }
532
+ .vela-select-rail { position: relative; flex: none; width: 3px; margin: 2px 1px 2px 0; pointer-events: none; }
533
+ .vela-select-thumb { width: 3px; border-radius: 2px; background: var(--vela-scroll); }
534
+ .vela-select-item {
535
+ display: block;
536
+ width: 100%;
537
+ text-align: left;
538
+ padding: 7px 10px;
539
+ border: none;
540
+ border-radius: 4px;
541
+ background: transparent;
542
+ color: inherit;
543
+ cursor: pointer;
544
+ font: inherit;
545
+ font-weight: 400;
546
+ }
547
+ .vela-select-item:hover { background: var(--vela-hover); }
548
+ .vela-select-item[data-checked] { background: var(--vela-hover-strong); color: var(--vela-fg-bright); }
549
+ .vela-select-item[data-checked]:hover { background: var(--vela-hover-strong); }
550
+ `;
551
+
552
+ // src/ui/components/select/view.ts
553
+ function ensureSelectStyles(doc) {
554
+ injectStyles(SELECT_STYLE_ID, SELECT_CSS, doc);
555
+ }
556
+ function syncThumb(list, thumb) {
557
+ const view = list.clientHeight;
558
+ const total = list.scrollHeight;
559
+ if (total <= view) {
560
+ thumb.style.height = "0";
561
+ return;
562
+ }
563
+ const thumbH = Math.max(20, view / total * view);
564
+ const top = list.scrollTop / (total - view) * (view - thumbH);
565
+ thumb.style.height = `${Math.round(thumbH)}px`;
566
+ thumb.style.transform = `translateY(${Math.round(top)}px`;
567
+ }
568
+ function fillSelectList(menu, options, current, onPick) {
569
+ menu.replaceChildren();
570
+ const list = menu.ownerDocument.createElement("div");
571
+ list.className = "vela-select-items";
572
+ for (const p of options) {
573
+ const item = menu.ownerDocument.createElement("button");
574
+ item.type = "button";
575
+ item.className = "vela-select-item";
576
+ item.textContent = p.label;
577
+ if (p.value === current) item.dataset.checked = "1";
578
+ item.addEventListener("click", (e) => {
579
+ e.stopPropagation();
580
+ onPick(p.value, p.label);
581
+ });
582
+ list.appendChild(item);
583
+ }
584
+ menu.appendChild(list);
585
+ }
586
+ function decorateSelectScroll(menu) {
587
+ const list = menu.querySelector(".vela-select-items");
588
+ if (!list) return;
589
+ if (list.scrollHeight > 240) {
590
+ menu.classList.add("is-scroll");
591
+ const rail = menu.ownerDocument.createElement("div");
592
+ rail.className = "vela-select-rail";
593
+ const thumb = menu.ownerDocument.createElement("div");
594
+ thumb.className = "vela-select-thumb";
595
+ rail.appendChild(thumb);
596
+ menu.appendChild(rail);
597
+ list.addEventListener("scroll", () => syncThumb(list, thumb));
598
+ syncThumb(list, thumb);
599
+ }
600
+ menu.querySelector(".vela-select-item[data-checked]")?.scrollIntoView({ block: "nearest" });
601
+ list.dispatchEvent(new Event("scroll"));
602
+ }
603
+ function toggleSelectList(trigger, options, current, onPick, opts = {}) {
604
+ if (openPopoverTrigger() === trigger) {
605
+ closeOpenPopovers();
606
+ return null;
607
+ }
608
+ return openSelectList(trigger, options, current, onPick, opts);
609
+ }
610
+ function openSelectList(trigger, options, current, onPick, opts = {}) {
611
+ ensureSelectStyles(trigger.ownerDocument);
612
+ closeOpenPopovers();
613
+ const pop = new Popover({
614
+ trigger,
615
+ theme: opts.theme,
616
+ matchWidth: opts.matchWidth ?? true,
617
+ boundary: opts.boundary,
618
+ boundaryInset: opts.boundaryInset,
619
+ gap: opts.gap ?? 4,
620
+ align: opts.align ?? "start",
621
+ host: opts.host,
622
+ position: opts.position,
623
+ zIndex: opts.zIndex,
624
+ className: "vela-select-list",
625
+ onClose: opts.onClose,
626
+ content: (el) => {
627
+ if (opts.size) el.dataset.size = opts.size;
628
+ fillSelectList(el, options, current, (value, label) => {
629
+ pop.hide();
630
+ onPick(value, label);
631
+ });
632
+ }
633
+ });
634
+ pop.show();
635
+ decorateSelectScroll(pop.el);
636
+ pop.reposition();
637
+ return pop;
638
+ }
639
+ var Select = class {
640
+ constructor(opts) {
641
+ this.list = null;
642
+ const doc = opts.list?.host?.ownerDocument ?? document;
643
+ ensureSelectStyles(doc);
644
+ this.ctrl = selectController(opts);
645
+ this.listOpts = { ...opts.list, theme: opts.theme ?? opts.list?.theme, size: this.ctrl.size };
646
+ const wrap = doc.createElement("div");
647
+ wrap.className = "vela-select";
648
+ wrap.dataset.size = this.ctrl.size;
649
+ if (this.ctrl.fill) wrap.dataset.fill = "";
650
+ const btn = doc.createElement("button");
651
+ btn.type = "button";
652
+ if (opts.id) btn.id = opts.id;
653
+ btn.className = "vela-select-trigger";
654
+ if (this.ctrl.disabled) btn.disabled = true;
655
+ const label = doc.createElement("span");
656
+ label.className = "vela-select-label";
657
+ label.textContent = this.ctrl.labelOf(this.ctrl.value);
658
+ const chevron = doc.createElement("span");
659
+ chevron.className = "vela-select-chevron";
660
+ chevron.appendChild(iconEl("chevron-down", doc));
661
+ btn.append(label, chevron);
662
+ wrap.appendChild(btn);
663
+ const sizer = doc.createElement("div");
664
+ sizer.className = "vela-select-sizer";
665
+ sizer.setAttribute("aria-hidden", "true");
666
+ for (const o of this.ctrl.options) {
667
+ const s = doc.createElement("span");
668
+ s.textContent = o.label;
669
+ sizer.appendChild(s);
670
+ }
671
+ wrap.appendChild(sizer);
672
+ btn.addEventListener("pointerdown", (e) => {
673
+ e.preventDefault();
674
+ btn.focus({ preventScroll: true });
675
+ });
676
+ btn.addEventListener("click", (e) => {
677
+ e.stopPropagation();
678
+ this.toggle();
679
+ });
680
+ this.el = wrap;
681
+ this.trigger = btn;
682
+ this.labelEl = label;
683
+ }
684
+ get value() {
685
+ return this.ctrl.value;
686
+ }
687
+ setValue(v) {
688
+ this.ctrl.setValue(v);
689
+ this.labelEl.textContent = this.ctrl.labelOf(v);
690
+ }
691
+ toggle() {
692
+ this.list = toggleSelectList(
693
+ this.trigger,
694
+ this.ctrl.options,
695
+ this.ctrl.value,
696
+ (value, label) => {
697
+ this.ctrl.pick(value);
698
+ this.labelEl.textContent = label;
699
+ this.list = null;
700
+ },
701
+ { ...this.listOpts, onClose: () => {
702
+ this.list = null;
703
+ this.listOpts.onClose?.();
704
+ } }
705
+ );
706
+ }
707
+ destroy() {
708
+ this.list?.hide();
709
+ this.list = null;
710
+ }
711
+ };
712
+
713
+ // src/ui/components/number-input/controller.ts
714
+ function clampNumber(n, opts) {
715
+ let v = n;
716
+ if (opts.min !== void 0) v = Math.max(opts.min, v);
717
+ if (opts.max !== void 0) v = Math.min(opts.max, v);
718
+ if (opts.integer) v = Math.round(v);
719
+ return v;
720
+ }
721
+ function decimalsOf(n) {
722
+ const s = String(n);
723
+ const e = s.search(/[eE]/);
724
+ if (e >= 0) {
725
+ const frac = s.slice(0, e).split(".")[1]?.length ?? 0;
726
+ return Math.max(0, Math.min(20, frac - Number(s.slice(e + 1))));
727
+ }
728
+ return s.split(".")[1]?.length ?? 0;
729
+ }
730
+ function snapToStep(n, base, step) {
731
+ const d = Math.min(20, Math.max(decimalsOf(base), decimalsOf(step)));
732
+ return Number(n.toFixed(d));
733
+ }
734
+ function numberInputController(opts) {
735
+ let value = opts.value;
736
+ const integer = opts.integer ?? false;
737
+ const commit = opts.commit ?? "blur";
738
+ const clamp = opts.clamp ?? commit === "blur";
739
+ const step = opts.step ?? (integer ? 1 : 0.1);
740
+ const onChange = opts.onChange;
741
+ const normalize = (raw) => clamp ? clampNumber(raw, { min: opts.min, max: opts.max, integer }) : raw;
742
+ const apply = (raw) => {
743
+ if (!Number.isFinite(raw)) return null;
744
+ const n = normalize(raw);
745
+ if (n !== value) {
746
+ value = n;
747
+ onChange?.(n);
748
+ }
749
+ return n;
750
+ };
751
+ return {
752
+ get value() {
753
+ return value;
754
+ },
755
+ min: opts.min,
756
+ max: opts.max,
757
+ step,
758
+ integer,
759
+ size: opts.size ?? "md",
760
+ commit,
761
+ steppers: opts.steppers ?? commit === "blur",
762
+ clamp,
763
+ disabled: opts.disabled ?? false,
764
+ apply,
765
+ sync(raw) {
766
+ const n = Number.isFinite(raw) ? normalize(raw) : value;
767
+ value = n;
768
+ return n;
769
+ },
770
+ nudge(dir) {
771
+ return apply(snapToStep(value + dir * step, value, step));
772
+ }
773
+ };
774
+ }
775
+
776
+ // src/ui/components/number-input/styles.ts
777
+ var NUMBER_STYLE_ID = "vela-ui-number";
778
+ var NUMBER_CSS = `
779
+ .vela-num { position: relative; display: inline-block; min-width: 0; }
780
+ .vela-num[data-fill] { width: 100%; }
781
+ .vela-num input {
782
+ width: 100%;
783
+ box-sizing: border-box;
784
+ height: 34px;
785
+ background: transparent;
786
+ border: 1px solid var(--vela-border-strong);
787
+ border-radius: 6px;
788
+ color: var(--vela-fg-bright);
789
+ padding: 0 8px;
790
+ font-size: 14px;
791
+ font-family: inherit;
792
+ outline: none;
793
+ transition: border-color 0.12s ease, box-shadow 0.12s ease;
794
+ -moz-appearance: textfield;
795
+ appearance: textfield;
796
+ }
797
+ .vela-num input::-webkit-inner-spin-button, .vela-num input::-webkit-outer-spin-button { -webkit-appearance: none; margin: 0; }
798
+ .vela-num input:hover { border-color: var(--vela-fg-muted); }
799
+ .vela-num input:focus { border-color: var(--vela-focus); box-shadow: 0 0 0 3px var(--vela-focus-soft); }
800
+ .vela-num[data-steppers] input { padding-right: 26px; }
801
+ .vela-num[data-size='sm'] { width: 64px; flex: none; }
802
+ .vela-num[data-size='sm'][data-compact] { width: 56px; }
803
+ .vela-num[data-size='sm'] input {
804
+ height: 28px;
805
+ background: var(--vela-surface-elev);
806
+ border-radius: var(--vela-radius-sm);
807
+ color: var(--vela-fg);
808
+ font-size: 13px;
809
+ box-shadow: none;
810
+ }
811
+ .vela-num[data-size='sm'] input:focus { box-shadow: none; }
812
+ .vela-num-step {
813
+ position: absolute;
814
+ right: 0;
815
+ top: 0;
816
+ bottom: 0;
817
+ width: 22px;
818
+ display: none;
819
+ flex-direction: column;
820
+ justify-content: center;
821
+ box-sizing: border-box;
822
+ padding: 2px 6px 2px 0;
823
+ line-height: 0;
824
+ }
825
+ .vela-num[data-steppers]:hover .vela-num-step { display: flex; }
826
+ .vela-num-step button {
827
+ flex: 1;
828
+ width: 100%;
829
+ min-height: 0;
830
+ border: none;
831
+ background: transparent;
832
+ color: var(--vela-fg-muted);
833
+ border-radius: 3px;
834
+ padding: 0;
835
+ cursor: pointer;
836
+ display: flex;
837
+ align-items: center;
838
+ justify-content: center;
839
+ }
840
+ .vela-num-step button:hover { background: var(--vela-hover); color: var(--vela-fg-bright); }
841
+ .vela-num-step .vela-icon, .vela-num-step svg { width: 12px; height: 12px; display: block; }
842
+ `;
843
+
844
+ // src/ui/components/number-input/view.ts
845
+ var NumberInput = class {
846
+ constructor(opts) {
847
+ const doc = document;
848
+ injectStyles(NUMBER_STYLE_ID, NUMBER_CSS, doc);
849
+ this.ctrl = numberInputController(opts);
850
+ this.placeholderMode = opts.placeholder !== void 0;
851
+ this.emptyValue = opts.emptyValue ?? opts.value;
852
+ this.last = String(opts.value);
853
+ const wrap = doc.createElement("div");
854
+ wrap.className = "vela-num";
855
+ wrap.dataset.size = this.ctrl.size;
856
+ if (this.ctrl.size !== "sm") wrap.dataset.fill = "";
857
+ if (opts.compact) wrap.dataset.compact = "";
858
+ if (this.ctrl.steppers) wrap.dataset.steppers = "";
859
+ const ni = doc.createElement("input");
860
+ ni.type = "number";
861
+ if (opts.id) ni.id = opts.id;
862
+ if (opts.title) ni.title = opts.title;
863
+ if (opts.min !== void 0) ni.min = String(opts.min);
864
+ if (opts.max !== void 0) ni.max = String(opts.max);
865
+ ni.step = String(this.ctrl.step);
866
+ if (this.ctrl.disabled) ni.disabled = true;
867
+ if (this.placeholderMode) {
868
+ ni.placeholder = opts.placeholder ?? "";
869
+ ni.value = opts.value !== this.emptyValue ? String(opts.value) : "";
870
+ } else {
871
+ ni.value = String(opts.value);
872
+ }
873
+ const commitRaw = (raw) => {
874
+ const n = this.ctrl.apply(raw);
875
+ if (n === null) {
876
+ ni.value = this.placeholderMode && this.ctrl.value === this.emptyValue ? "" : this.last;
877
+ return;
878
+ }
879
+ this.last = String(n);
880
+ if (this.placeholderMode && n === this.emptyValue) ni.value = "";
881
+ else ni.value = String(n);
882
+ };
883
+ if (this.placeholderMode) {
884
+ ni.addEventListener("change", () => {
885
+ const raw = ni.value.trim() === "" ? this.emptyValue : Number(ni.value);
886
+ commitRaw(Number.isFinite(raw) ? raw : this.emptyValue);
887
+ });
888
+ } else if (this.ctrl.commit === "live") {
889
+ ni.addEventListener("input", () => {
890
+ const v = Number(ni.value);
891
+ if (Number.isFinite(v)) this.ctrl.apply(v);
892
+ });
893
+ } else {
894
+ ni.addEventListener("blur", () => commitRaw(Number(ni.value)));
895
+ ni.addEventListener("keydown", (e) => {
896
+ if (e.key === "Enter") {
897
+ e.preventDefault();
898
+ ni.blur();
899
+ }
900
+ });
901
+ }
902
+ wrap.appendChild(ni);
903
+ if (this.ctrl.steppers) {
904
+ wrap.appendChild(this.buildSteppers(doc, (dir) => {
905
+ const cur = Number(ni.value);
906
+ const base = Number.isFinite(cur) ? cur : this.ctrl.value;
907
+ commitRaw(snapToStep(base + dir * this.ctrl.step, base, this.ctrl.step));
908
+ }));
909
+ }
910
+ this.el = wrap;
911
+ this.input = ni;
912
+ }
913
+ get value() {
914
+ return this.ctrl.value;
915
+ }
916
+ setValue(v) {
917
+ const n = this.ctrl.sync(v);
918
+ this.last = String(n);
919
+ if (this.placeholderMode && n === this.emptyValue) this.input.value = "";
920
+ else this.input.value = String(n);
921
+ }
922
+ buildSteppers(doc, applyDir) {
923
+ const steps = doc.createElement("div");
924
+ steps.className = "vela-num-step";
925
+ const mk = (dir, icon, label) => {
926
+ const b = doc.createElement("button");
927
+ b.type = "button";
928
+ b.tabIndex = -1;
929
+ b.setAttribute("aria-label", label);
930
+ b.appendChild(iconEl(icon, doc));
931
+ let timer = 0;
932
+ const apply = () => applyDir(dir);
933
+ const stop = () => {
934
+ if (timer) {
935
+ window.clearTimeout(timer);
936
+ timer = 0;
937
+ }
938
+ };
939
+ b.addEventListener("pointerdown", (e) => {
940
+ e.preventDefault();
941
+ e.stopPropagation();
942
+ apply();
943
+ timer = window.setTimeout(function tick() {
944
+ apply();
945
+ timer = window.setTimeout(tick, 60);
946
+ }, 400);
947
+ });
948
+ b.addEventListener("pointerup", stop);
949
+ b.addEventListener("pointerleave", stop);
950
+ b.addEventListener("pointercancel", stop);
951
+ return b;
952
+ };
953
+ steps.append(mk(1, "chevron-up", "Increase"), mk(-1, "chevron-down", "Decrease"));
954
+ return steps;
955
+ }
956
+ };
957
+
958
+ // src/ui/components/text-field/controller.ts
959
+ function textFieldController(opts = {}) {
960
+ let value = opts.value ?? "";
961
+ const size = opts.size ?? "md";
962
+ return {
963
+ get value() {
964
+ return value;
965
+ },
966
+ size,
967
+ fill: opts.fill ?? size !== "sm",
968
+ disabled: opts.disabled ?? false,
969
+ commit(next) {
970
+ if (next === value) return null;
971
+ value = next;
972
+ opts.onChange?.(next);
973
+ return next;
974
+ },
975
+ sync(next) {
976
+ value = next;
977
+ }
978
+ };
979
+ }
980
+
981
+ // src/ui/components/text-field/styles.ts
982
+ var TEXT_STYLE_ID = "vela-ui-text";
983
+ var TEXT_CSS = `
984
+ .vela-text {
985
+ display: inline-block;
986
+ min-width: 0;
987
+ }
988
+ .vela-text[data-fill] { width: 100%; }
989
+ .vela-text-field {
990
+ width: 100%;
991
+ box-sizing: border-box;
992
+ height: 34px;
993
+ background: transparent;
994
+ border: 1px solid var(--vela-border-strong);
995
+ border-radius: 6px;
996
+ color: var(--vela-fg-bright);
997
+ padding: 0 8px;
998
+ font-size: 14px;
999
+ font-family: inherit;
1000
+ outline: none;
1001
+ transition: border-color 0.12s ease, box-shadow 0.12s ease;
1002
+ }
1003
+ .vela-text-field:hover { border-color: var(--vela-fg-muted); }
1004
+ .vela-text-field:focus { border-color: var(--vela-focus); box-shadow: 0 0 0 3px var(--vela-focus-soft); }
1005
+ .vela-text[data-size='sm'] .vela-text-field {
1006
+ height: 28px;
1007
+ background: var(--vela-surface-elev);
1008
+ border-radius: var(--vela-radius-sm);
1009
+ color: var(--vela-fg);
1010
+ font-size: 13px;
1011
+ }
1012
+ `;
1013
+
1014
+ // src/ui/components/text-field/view.ts
1015
+ var TextField = class {
1016
+ constructor(opts = {}) {
1017
+ const doc = document;
1018
+ injectStyles(TEXT_STYLE_ID, TEXT_CSS, doc);
1019
+ this.ctrl = textFieldController(opts);
1020
+ const wrap = doc.createElement("div");
1021
+ wrap.className = "vela-text";
1022
+ wrap.dataset.size = this.ctrl.size;
1023
+ if (this.ctrl.fill) wrap.dataset.fill = "";
1024
+ const ti = doc.createElement("input");
1025
+ ti.type = "text";
1026
+ if (opts.id) ti.id = opts.id;
1027
+ ti.value = this.ctrl.value;
1028
+ ti.className = "vela-text-field";
1029
+ if (this.ctrl.disabled) ti.disabled = true;
1030
+ ti.addEventListener("blur", () => {
1031
+ this.ctrl.commit(ti.value);
1032
+ });
1033
+ ti.addEventListener("keydown", (e) => {
1034
+ if (e.key === "Enter") {
1035
+ e.preventDefault();
1036
+ ti.blur();
1037
+ }
1038
+ });
1039
+ wrap.appendChild(ti);
1040
+ this.el = wrap;
1041
+ this.input = ti;
1042
+ }
1043
+ get value() {
1044
+ return this.ctrl.value;
1045
+ }
1046
+ setValue(v) {
1047
+ this.ctrl.sync(v);
1048
+ this.input.value = v;
1049
+ }
1050
+ };
1051
+
1052
+ // src/ui/components/color-picker/controller.ts
1053
+ function splitColor(color) {
1054
+ const s = String(color ?? "").trim();
1055
+ let m = /^#([0-9a-f]{6})([0-9a-f]{2})?$/i.exec(s);
1056
+ if (m) return { hex6: `#${m[1].toLowerCase()}`, alpha: m[2] ? parseInt(m[2], 16) / 255 : 1 };
1057
+ m = /^#([0-9a-f]{3})$/i.exec(s);
1058
+ if (m) {
1059
+ const h = m[1].toLowerCase();
1060
+ return { hex6: `#${h[0]}${h[0]}${h[1]}${h[1]}${h[2]}${h[2]}`, alpha: 1 };
1061
+ }
1062
+ const rgba = /^rgba?\(([^)]+)\)/i.exec(s);
1063
+ if (rgba) {
1064
+ const p = rgba[1].split(",").map((v) => v.trim());
1065
+ const hx = (n) => Math.max(0, Math.min(255, Math.round(n))).toString(16).padStart(2, "0");
1066
+ const a = p[3] != null ? Math.max(0, Math.min(1, parseFloat(p[3]))) : 1;
1067
+ return { hex6: `#${hx(parseInt(p[0] ?? "0", 10))}${hx(parseInt(p[1] ?? "0", 10))}${hx(parseInt(p[2] ?? "0", 10))}`, alpha: a };
1068
+ }
1069
+ return { hex6: ACCENT_BRIGHT, alpha: 1 };
1070
+ }
1071
+ function combineColor(hex6, alpha) {
1072
+ const a = Math.max(0, Math.min(1, alpha));
1073
+ if (a >= 0.999) return hex6;
1074
+ return `${hex6}${Math.round(a * 255).toString(16).padStart(2, "0")}`;
1075
+ }
1076
+ function blendOver(fg, bg, alpha) {
1077
+ const rgb = (c) => {
1078
+ const n = parseInt(splitColor(c).hex6.slice(1), 16);
1079
+ return [n >> 16 & 255, n >> 8 & 255, n & 255];
1080
+ };
1081
+ const [fr, fgc, fb] = rgb(fg);
1082
+ const [br, bgc, bb] = rgb(bg);
1083
+ const a = Math.max(0, Math.min(1, alpha));
1084
+ const h = (n) => Math.round(n).toString(16).padStart(2, "0");
1085
+ return `#${h(fr * a + br * (1 - a))}${h(fgc * a + bgc * (1 - a))}${h(fb * a + bb * (1 - a))}`;
1086
+ }
1087
+ function hslHex(h, s, l) {
1088
+ const sn = s / 100;
1089
+ const ln = l / 100;
1090
+ const k = (n) => (n + h / 30) % 12;
1091
+ const a = sn * Math.min(ln, 1 - ln);
1092
+ const f = (n) => ln - a * Math.max(-1, Math.min(k(n) - 3, Math.min(9 - k(n), 1)));
1093
+ const to = (x) => Math.round(x * 255).toString(16).padStart(2, "0");
1094
+ return `#${to(f(0))}${to(f(8))}${to(f(4))}`;
1095
+ }
1096
+ function buildPalette() {
1097
+ const grays = [100, 90, 80, 68, 56, 45, 35, 25, 14, 0].map((l) => hslHex(0, 0, l));
1098
+ const hues = [4, 28, 50, 96, 140, 174, 200, 224, 270, 322];
1099
+ const levels = [
1100
+ [82, 56],
1101
+ [58, 84],
1102
+ [62, 74],
1103
+ [66, 62],
1104
+ [70, 50],
1105
+ [64, 39],
1106
+ [54, 29]
1107
+ ];
1108
+ return [grays, ...levels.map(([s, l]) => hues.map((h) => hslHex(h, s, l)))];
1109
+ }
1110
+ function transparencyChecker(size) {
1111
+ return `repeating-conic-gradient(#9aa0a6 0% 25%, #d3d6da 0% 50%) 0 0 / ${size}px ${size}px`;
1112
+ }
1113
+ function colorPickerController(opts = {}) {
1114
+ const parsed = splitColor(opts.color ?? ACCENT_BRIGHT);
1115
+ let hex6 = parsed.hex6;
1116
+ let alpha = parsed.alpha;
1117
+ return {
1118
+ get hex6() {
1119
+ return hex6;
1120
+ },
1121
+ get alpha() {
1122
+ return alpha;
1123
+ },
1124
+ combined() {
1125
+ return combineColor(hex6, alpha);
1126
+ },
1127
+ setHex(hex) {
1128
+ hex6 = splitColor(hex).hex6;
1129
+ },
1130
+ setAlpha(a) {
1131
+ alpha = Math.max(0, Math.min(1, a));
1132
+ }
1133
+ };
1134
+ }
1135
+
1136
+ // src/ui/components/color-picker/styles.ts
1137
+ var COLOR_STYLE_ID = "vela-ui-color";
1138
+ var COLOR_CSS = `
1139
+ .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;}
1140
+ .vela-color-field:hover{border-color:var(--vela-fg-muted);}
1141
+ .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);}
1142
+ .vela-color-field-circle{width:26px;height:26px;padding:3px;border:1px solid var(--vela-border-strong);border-radius:4px;background:transparent;overflow:hidden;}
1143
+ .vela-color-field-circle .vela-color-field-swatch{border-radius:2px;box-shadow:none;}
1144
+ .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;}
1145
+ `;
1146
+
1147
+ // src/ui/components/color-picker/view.ts
1148
+ var PALETTE = buildPalette();
1149
+ var CHECKER = transparencyChecker(10);
1150
+ var FIELD_CHECKER = transparencyChecker(8);
1151
+ var recents = [ACCENT, BULLISH, BEARISH, WARNING, NEUTRAL];
1152
+ function addRecent(hex6) {
1153
+ const i = recents.findIndex((c) => c.toLowerCase() === hex6.toLowerCase());
1154
+ if (i >= 0) recents.splice(i, 1);
1155
+ recents.unshift(hex6);
1156
+ if (recents.length > 10) recents.length = 10;
1157
+ }
1158
+ function buildColorPicker(color, theme, onChange) {
1159
+ const parsed = splitColor(color);
1160
+ let curHex = parsed.hex6;
1161
+ let curAlpha = parsed.alpha;
1162
+ const root = document.createElement("div");
1163
+ root.style.cssText = "display:flex;flex-direction:column;gap:9px;width:236px;";
1164
+ const grid = document.createElement("div");
1165
+ grid.style.cssText = "display:grid;grid-template-columns:repeat(10,1fr);gap:4px;";
1166
+ const swatches = [];
1167
+ for (const row of PALETTE) {
1168
+ for (const c of row) {
1169
+ const sw = document.createElement("button");
1170
+ sw.type = "button";
1171
+ sw.dataset.c = c;
1172
+ sw.style.cssText = `width:100%;aspect-ratio:1;border-radius:4px;border:none;cursor:pointer;background:${c};padding:0;`;
1173
+ sw.addEventListener("click", (e) => {
1174
+ e.stopPropagation();
1175
+ pickHex(c);
1176
+ });
1177
+ grid.appendChild(sw);
1178
+ swatches.push(sw);
1179
+ }
1180
+ }
1181
+ const paintSelection = () => {
1182
+ for (const sw of swatches) {
1183
+ const on = (sw.dataset.c ?? "").toLowerCase() === curHex.toLowerCase();
1184
+ sw.style.outline = on ? `2px solid ${theme.textColor}` : "none";
1185
+ sw.style.outlineOffset = "2px";
1186
+ sw.style.zIndex = on ? "1" : "";
1187
+ sw.style.position = on ? "relative" : "";
1188
+ }
1189
+ };
1190
+ const recentRow = document.createElement("div");
1191
+ recentRow.style.cssText = "display:flex;align-items:center;gap:5px;flex-wrap:wrap;border-top:1px solid var(--vela-border);padding-top:9px;";
1192
+ const renderRecents = () => {
1193
+ recentRow.replaceChildren();
1194
+ for (const c of recents) {
1195
+ const sw = document.createElement("button");
1196
+ sw.type = "button";
1197
+ 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;`;
1198
+ sw.addEventListener("click", (e) => {
1199
+ e.stopPropagation();
1200
+ pickHex(c);
1201
+ });
1202
+ recentRow.appendChild(sw);
1203
+ }
1204
+ const add = document.createElement("label");
1205
+ 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;`;
1206
+ add.textContent = "+";
1207
+ const input = document.createElement("input");
1208
+ input.type = "color";
1209
+ input.value = curHex;
1210
+ input.style.cssText = "position:absolute;inset:0;opacity:0;cursor:pointer;";
1211
+ input.addEventListener("input", () => pickHex(input.value, true));
1212
+ add.appendChild(input);
1213
+ recentRow.appendChild(add);
1214
+ };
1215
+ const opLabel = document.createElement("div");
1216
+ opLabel.textContent = "Opacity";
1217
+ opLabel.style.cssText = `font:11px ${theme.fontFamily};color:var(--vela-fg-muted);`;
1218
+ const opRow = document.createElement("div");
1219
+ opRow.style.cssText = "display:flex;align-items:center;gap:10px;";
1220
+ const track = document.createElement("div");
1221
+ track.style.cssText = "flex:1;position:relative;height:13px;border-radius:7px;cursor:pointer;";
1222
+ const knob = document.createElement("div");
1223
+ 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;";
1224
+ track.appendChild(knob);
1225
+ const pctBox = document.createElement("div");
1226
+ 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;`;
1227
+ opRow.append(track, pctBox);
1228
+ const paintOpacity = () => {
1229
+ track.style.background = `linear-gradient(to right, ${curHex}00, ${curHex}ff), ${CHECKER}`;
1230
+ knob.style.left = `${curAlpha * 100}%`;
1231
+ pctBox.textContent = `${Math.round(curAlpha * 100)}%`;
1232
+ };
1233
+ let dragging = false;
1234
+ const onDrag = (clientX) => {
1235
+ const r = track.getBoundingClientRect();
1236
+ curAlpha = Math.max(0, Math.min(1, (clientX - r.left) / r.width));
1237
+ paintOpacity();
1238
+ emit();
1239
+ };
1240
+ track.addEventListener("pointerdown", (e) => {
1241
+ e.stopPropagation();
1242
+ dragging = true;
1243
+ track.setPointerCapture(e.pointerId);
1244
+ onDrag(e.clientX);
1245
+ });
1246
+ track.addEventListener("pointermove", (e) => {
1247
+ if (dragging) onDrag(e.clientX);
1248
+ });
1249
+ track.addEventListener("pointerup", () => dragging = false);
1250
+ function emit() {
1251
+ onChange(combineColor(curHex, curAlpha));
1252
+ }
1253
+ function pickHex(hex, custom = false) {
1254
+ curHex = splitColor(hex).hex6;
1255
+ addRecent(curHex);
1256
+ paintSelection();
1257
+ paintOpacity();
1258
+ if (custom) renderRecents();
1259
+ emit();
1260
+ }
1261
+ renderRecents();
1262
+ paintSelection();
1263
+ paintOpacity();
1264
+ root.append(grid, recentRow, opLabel, opRow);
1265
+ return root;
1266
+ }
1267
+ function colorField(theme, getVal, onVal, opts) {
1268
+ return new ColorField({ theme, getVal, onVal, shape: opts?.shape, id: opts?.id, popover: opts?.popover }).el;
1269
+ }
1270
+ var ColorField = class {
1271
+ constructor(opts) {
1272
+ injectStyles(COLOR_STYLE_ID, COLOR_CSS, document);
1273
+ this.theme = opts.theme;
1274
+ this.getVal = opts.getVal;
1275
+ this.onVal = opts.onVal;
1276
+ this.popoverOpts = opts.popover;
1277
+ const trigger = document.createElement("button");
1278
+ trigger.type = "button";
1279
+ if (opts.id) trigger.id = opts.id;
1280
+ trigger.className = opts.shape === "circle" ? "vela-color-field vela-color-field-circle" : "vela-color-field";
1281
+ const swatch = document.createElement("span");
1282
+ swatch.className = "vela-color-field-swatch";
1283
+ trigger.appendChild(swatch);
1284
+ this.el = trigger;
1285
+ this.swatch = swatch;
1286
+ this.paint();
1287
+ trigger.addEventListener("vela-sync", () => this.paint());
1288
+ trigger.addEventListener("pointerdown", (e) => {
1289
+ e.preventDefault();
1290
+ trigger.focus({ preventScroll: true });
1291
+ });
1292
+ trigger.addEventListener("click", (e) => {
1293
+ e.stopPropagation();
1294
+ this.toggle();
1295
+ });
1296
+ }
1297
+ paint() {
1298
+ const v = this.getVal();
1299
+ this.swatch.style.background = `linear-gradient(${v}, ${v}), ${FIELD_CHECKER}`;
1300
+ }
1301
+ toggle() {
1302
+ if (openPopoverTrigger() === this.el) {
1303
+ closeOpenPopovers();
1304
+ return;
1305
+ }
1306
+ const pop = new Popover({
1307
+ trigger: this.el,
1308
+ theme: this.theme,
1309
+ align: this.popoverOpts?.align ?? "end",
1310
+ gap: this.popoverOpts?.gap ?? 6,
1311
+ host: this.popoverOpts?.host,
1312
+ position: this.popoverOpts?.position,
1313
+ boundary: this.popoverOpts?.boundary,
1314
+ zIndex: this.popoverOpts?.zIndex,
1315
+ className: "vela-color-field-pop",
1316
+ content: buildColorPicker(this.getVal(), this.theme, (val) => {
1317
+ this.onVal(val);
1318
+ this.paint();
1319
+ })
1320
+ });
1321
+ pop.show();
1322
+ }
1323
+ };
1324
+ function closeColorPopover() {
1325
+ closeOpenPopovers();
1326
+ }
1327
+
1328
+ export { ColorField, NumberInput, Popover, STATIC_TOKENS, Select, Switch, TextField, applyPlotOverlayTokens, applyThemeTokens, blendOver, buildColorPicker, buildPalette, clampNumber, closeColorPopover, closeOpenPopovers, colorField, colorPickerController, combineColor, decorateSelectScroll, ensureUIHost, fillSelectList, hslHex, insetRect, intersectRects, isPopoverOpen, numberInputController, openPopoverTrigger, openSelectList, placePopover, popoverController, selectController, snapToStep, splitColor, switchController, textFieldController, themeTokens, toggleSelectList, transparencyChecker, viewportRect };