@luxalgo/vela 0.5.3 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/README.md +3 -2
  2. package/dist/{DataProvider-gnzv-dgc.d.cts → DataProvider-Ce6PuCzQ.d.cts} +10 -1
  3. package/dist/{DataProvider-DTOEXKTI.d.ts → DataProvider-DNx9ntBD.d.ts} +10 -1
  4. package/dist/{chunk-EY63XBFZ.js → chunk-7ROONW6H.js} +132 -23
  5. package/dist/{chunk-L5NTMWPP.js → chunk-CA3N3PYT.js} +785 -857
  6. package/dist/{chunk-LJLZR44Y.js → chunk-EMS6PO2T.js} +5 -7
  7. package/dist/chunk-OGRQW3M6.js +1328 -0
  8. package/dist/{chunk-P556H6EA.js → chunk-QWIEKZRE.js} +59 -34
  9. package/dist/{chunk-WJKLBYRM.js → chunk-ZNL2X6U2.js} +1 -1
  10. package/dist/{contributions-Ci_i3IN2.d.cts → contributions-DToMhiw3.d.ts} +9 -2
  11. package/dist/{contributions-b6AVrqqG.d.ts → contributions-Dlc7ZWZz.d.cts} +9 -2
  12. package/dist/{history-Ci3M0xLV.d.ts → history-BZADxpKx.d.cts} +5 -3
  13. package/dist/{history-Cf1lVsap.d.cts → history-BuHXJZJ2.d.ts} +5 -3
  14. package/dist/index.cjs +2761 -1597
  15. package/dist/index.d.cts +14 -6
  16. package/dist/index.d.ts +14 -6
  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-DlidwMj6.d.cts → plugin-BNPLOQO1.d.cts} +3 -3
  21. package/dist/{plugin-DP6A4t4o.d.ts → plugin-BqjEa5r3.d.ts} +3 -3
  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 +3729 -2565
  37. package/dist/vela.global.min.js +315 -66
  38. package/dist/widget.cjs +2359 -1054
  39. package/dist/widget.d.cts +30 -16
  40. package/dist/widget.d.ts +30 -16
  41. package/dist/widget.js +37 -14
  42. package/dist/workspace.cjs +2259 -944
  43. package/dist/workspace.d.cts +10 -5
  44. package/dist/workspace.d.ts +10 -5
  45. package/dist/workspace.js +29 -8
  46. package/package.json +1 -1
  47. package/dist/chunk-ATPU5CI6.js +0 -81
package/dist/ui.cjs CHANGED
@@ -86,7 +86,11 @@ function injectStyles(id, css, root = document) {
86
86
  // src/core/palette.ts
87
87
  var ACCENT = "#2962ff";
88
88
  var ACCENT_BRIGHT = "#38c0fd";
89
+ var BULLISH = "#089981";
90
+ var BEARISH = "#f23645";
91
+ var NEUTRAL = "#787b86";
89
92
  var HIGHLIGHT = "#e0b400";
93
+ var WARNING = "#ff9800";
90
94
 
91
95
  // src/core/tokens.ts
92
96
  var STATIC_TOKENS = {
@@ -100,6 +104,9 @@ var STATIC_TOKENS = {
100
104
  "--vela-z-tooltip": "60",
101
105
  "--vela-z-menu": "50",
102
106
  "--vela-z-dialog": "40",
107
+ // Form popovers portal to <body> (or a chart host) and must sit above a dialog
108
+ // whose own stacking context may be nested inside the chart container.
109
+ "--vela-z-popover": "6000",
103
110
  "--vela-ease": "cubic-bezier(0.22, 1, 0.36, 1)",
104
111
  "--vela-dur-fast": "90ms",
105
112
  "--vela-dur-med": "160ms",
@@ -238,6 +245,10 @@ registerIcon(
238
245
  S('<path d="M8 1.8 14 5 8 8.2 2 5z"/><path d="M2 8l6 3.2L14 8" opacity="0.7"/><path d="M2 11l6 3.2 6-3.2" opacity="0.4"/>')
239
246
  );
240
247
  registerIcon("clock", S('<circle cx="8" cy="8" r="6.2"/><path d="M8 4.8V8l2.4 1.6"/>'));
248
+ registerIcon(
249
+ "calendar",
250
+ S('<rect x="2.2" y="3.2" width="11.6" height="10.6" rx="1.4"/><path d="M5.2 1.8v2.8M10.8 1.8v2.8M2.2 6.8h11.6"/>')
251
+ );
241
252
  registerIcon("datawindow", S('<rect x="1.8" y="2.5" width="12.4" height="11" rx="1.5"/><path d="M4.5 5.5h4M4.5 8h7M4.5 10.5h5.5"/>'));
242
253
  registerIcon("camera", S('<path d="M5.5 4 6.5 2.5h3L10.5 4h3A1 1 0 0 1 14.5 5v7.5a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1V5a1 1 0 0 1 1-1z"/><circle cx="8" cy="8.5" r="2.6"/>'));
243
254
  registerIcon(
@@ -592,7 +603,10 @@ function menuController(opts) {
592
603
  props: {
593
604
  id: opts.id ?? nextUid("vela-menu"),
594
605
  ids: opts.triggerId ? { trigger: opts.triggerId } : void 0,
595
- positioning: { placement: opts.placement ?? "bottom-start" },
606
+ positioning: {
607
+ placement: opts.placement ?? "bottom-start",
608
+ ...opts.getAnchorRect ? { getAnchorRect: () => opts.getAnchorRect() } : {}
609
+ },
596
610
  onSelect: (d) => opts.onSelect?.(d.value),
597
611
  onOpenChange: (d) => opts.onOpenChange?.(d.open)
598
612
  },
@@ -674,6 +688,22 @@ var MENU_CSS = `
674
688
  }
675
689
  .vela-menu-switch.on { background: var(--vela-accent-bright, var(--vela-accent)); border-color: var(--vela-accent-bright, var(--vela-accent)); }
676
690
  .vela-menu-switch.on::after { transform: translateX(16px); background: var(--vela-bg); }
691
+ /* Favorite star (rows carrying \`favorite\`): reserved space always \u2014 it fades in on row
692
+ hover instead of shifting the layout \u2014 and a starred row keeps its filled star visible.
693
+ Same control language as the drawing toolbar's flyout stars. */
694
+ .vela-menu-item .vela-menu-star {
695
+ order: 98;
696
+ /* Pad the 16px icon slot into a bigger hit target without moving the row's layout. */
697
+ margin: -3px -5px -3px 0;
698
+ padding: 3px 5px;
699
+ box-sizing: content-box;
700
+ border-radius: var(--vela-radius-sm);
701
+ opacity: 0;
702
+ transition: opacity 0.1s ease, background 0.1s ease;
703
+ }
704
+ .vela-menu-item[data-highlighted] .vela-menu-star { opacity: 0.55; }
705
+ .vela-menu-item .vela-menu-star:hover { opacity: 1; background: var(--vela-hover-strong); }
706
+ .vela-menu-item .vela-menu-star.vela-fav { opacity: 0.95; color: var(--vela-highlight); }
677
707
  .vela-menu-sep { height: 1px; margin: 4px 6px; background: var(--vela-border); }
678
708
  /* Submenu trigger row: a right-aligned chevron, and it stays highlighted while its own
679
709
  list is open so the path you came down remains readable. */
@@ -692,9 +722,13 @@ var Surface = class _Surface {
692
722
  this.items = [];
693
723
  /** Branch item id → the surface it opens. */
694
724
  this.subs = /* @__PURE__ */ new Map();
725
+ /** Trigger rect captured when this level opened — the list stays put if the
726
+ * trigger then moves (favorite chips shifting the caret). */
727
+ this.pinnedAnchor = null;
695
728
  this.doc = doc;
696
729
  this.host = opts.host;
697
730
  this.onSelect = opts.onSelect;
731
+ this.onFavorite = opts.onFavorite;
698
732
  this.positioner = doc.createElement("div");
699
733
  this.positioner.className = "vela-ui-layer";
700
734
  this.list = doc.createElement("ul");
@@ -702,16 +736,25 @@ var Surface = class _Surface {
702
736
  if (opts.minWidth) this.list.style.minWidth = opts.minWidth;
703
737
  this.positioner.appendChild(this.list);
704
738
  this.host.appendChild(this.positioner);
739
+ const trigger = opts.trigger;
705
740
  this.ctrl = menuController({
706
741
  items: [],
707
742
  id: opts.id,
708
743
  placement: opts.placement,
709
744
  onSelect: (id) => this.onSelect(id),
710
- onOpenChange: opts.onOpenChange,
745
+ getAnchorRect: () => this.pinnedAnchor,
746
+ onOpenChange: (open2) => {
747
+ if (open2 && trigger) {
748
+ const r = trigger.getBoundingClientRect();
749
+ this.pinnedAnchor = { x: r.x, y: r.y, width: r.width, height: r.height };
750
+ } else {
751
+ this.pinnedAnchor = null;
752
+ }
753
+ opts.onOpenChange?.(open2);
754
+ },
711
755
  triggerId: opts.triggerId
712
756
  });
713
757
  this.mid = String(this.ctrl.props.id);
714
- const trigger = opts.trigger;
715
758
  if (opts.triggerId && trigger) trigger.id = opts.triggerId;
716
759
  this.handle = runMachine(this.ctrl.machine, this.ctrl.props, (service) => {
717
760
  const api = this.ctrl.connect(service);
@@ -791,6 +834,25 @@ var Surface = class _Surface {
791
834
  hint.textContent = item.hint;
792
835
  li.appendChild(hint);
793
836
  }
837
+ if (item.favorite !== void 0 && this.onFavorite) {
838
+ const on = item.favorite;
839
+ const star = iconEl(on ? "star-filled" : "star", doc);
840
+ star.classList.add("vela-menu-star");
841
+ if (on) star.classList.add("vela-fav");
842
+ star.removeAttribute("aria-hidden");
843
+ star.setAttribute("role", "button");
844
+ star.setAttribute("aria-label", on ? "Remove from favorites" : "Add to favorites");
845
+ star.setAttribute("aria-pressed", on ? "true" : "false");
846
+ for (const ev of ["pointerdown", "pointerup", "mousedown", "mouseup"]) {
847
+ star.addEventListener(ev, (e) => e.stopPropagation());
848
+ }
849
+ star.addEventListener("click", (e) => {
850
+ e.stopPropagation();
851
+ e.preventDefault();
852
+ this.onFavorite?.(item.id, !on);
853
+ });
854
+ li.appendChild(star);
855
+ }
794
856
  if (item.toggle) {
795
857
  const sw = doc.createElement("span");
796
858
  sw.className = "vela-menu-switch" + (item.checked ? " on" : "");
@@ -806,6 +868,7 @@ var Surface = class _Surface {
806
868
  host: this.host,
807
869
  placement: "right-start",
808
870
  onSelect: this.onSelect,
871
+ onFavorite: this.onFavorite,
809
872
  id: `${this.mid}--${item.id}`
810
873
  });
811
874
  sub.setItems(item.submenu ?? []);
@@ -829,7 +892,8 @@ var Menu = class {
829
892
  trigger: opts.trigger,
830
893
  triggerId: opts.triggerId,
831
894
  id: opts.id,
832
- minWidth: opts.minWidth
895
+ minWidth: opts.minWidth,
896
+ onFavorite: opts.onFavorite
833
897
  });
834
898
  this.root.setItems(opts.items);
835
899
  }
@@ -1332,6 +1396,1222 @@ var Drawer = class {
1332
1396
  }
1333
1397
  };
1334
1398
 
1399
+ // src/ui/components/popover/controller.ts
1400
+ function insetRect(r, inset) {
1401
+ return {
1402
+ left: r.left + inset,
1403
+ top: r.top + inset,
1404
+ right: r.right - inset,
1405
+ bottom: r.bottom - inset,
1406
+ width: r.width - inset * 2,
1407
+ height: r.height - inset * 2
1408
+ };
1409
+ }
1410
+ function viewportRect(width, height, inset) {
1411
+ return {
1412
+ left: inset,
1413
+ top: inset,
1414
+ right: width - inset,
1415
+ bottom: height - inset,
1416
+ width: width - inset * 2,
1417
+ height: height - inset * 2
1418
+ };
1419
+ }
1420
+ function intersectRects(a, b) {
1421
+ const left = Math.max(a.left, b.left);
1422
+ const top = Math.max(a.top, b.top);
1423
+ const right = Math.min(a.right, b.right);
1424
+ const bottom = Math.min(a.bottom, b.bottom);
1425
+ return { left, top, right, bottom, width: right - left, height: bottom - top };
1426
+ }
1427
+ function placePopover(a) {
1428
+ let left = a.align === "end" ? a.trigger.right - a.pop.width : a.trigger.left;
1429
+ const below = a.trigger.bottom + a.gap;
1430
+ const above = a.trigger.top - a.pop.height - a.gap;
1431
+ const fitsBelow = below + a.pop.height <= a.clamp.bottom;
1432
+ const fitsAbove = above >= a.clamp.top;
1433
+ let top = below;
1434
+ if (!fitsBelow && (fitsAbove || a.trigger.top - a.clamp.top > a.clamp.bottom - a.trigger.bottom)) {
1435
+ top = Math.max(a.clamp.top, above);
1436
+ }
1437
+ if (left + a.pop.width > a.clamp.right) left = a.clamp.right - a.pop.width;
1438
+ if (left < a.clamp.left) left = a.clamp.left;
1439
+ if (top + a.pop.height > a.clamp.bottom) top = a.clamp.bottom - a.pop.height;
1440
+ if (top < a.clamp.top) top = a.clamp.top;
1441
+ return {
1442
+ left: Math.round(left - a.originX),
1443
+ top: Math.round(top - a.originY)
1444
+ };
1445
+ }
1446
+ function popoverController(opts = {}) {
1447
+ return {
1448
+ gap: opts.gap ?? 4,
1449
+ align: opts.align ?? "start",
1450
+ matchWidth: opts.matchWidth ?? false,
1451
+ position: opts.position ?? "fixed",
1452
+ boundaryInset: opts.boundaryInset ?? 0,
1453
+ viewportInset: opts.viewportInset ?? 6,
1454
+ onClose: opts.onClose
1455
+ };
1456
+ }
1457
+
1458
+ // src/ui/components/popover/styles.ts
1459
+ var POPOVER_STYLE_ID = "vela-ui-popover";
1460
+ var POPOVER_CSS = `
1461
+ .vela-popover {
1462
+ position: fixed;
1463
+ z-index: var(--vela-z-popover);
1464
+ box-sizing: border-box;
1465
+ /* Never serve as a scroll anchor: the shell is portaled and re-placed between
1466
+ gestures, and anchoring against it jumps the scroller underneath. */
1467
+ overflow-anchor: none;
1468
+ }
1469
+ .vela-popover[data-position='absolute'] { position: absolute; }
1470
+ .vela-popover[hidden] { display: none !important; }
1471
+ `;
1472
+
1473
+ // src/ui/components/popover/view.ts
1474
+ var open = null;
1475
+ function closeOpenPopovers() {
1476
+ open?.hide();
1477
+ }
1478
+ function isPopoverOpen() {
1479
+ return open !== null;
1480
+ }
1481
+ function openPopoverTrigger() {
1482
+ return open?.trigger ?? null;
1483
+ }
1484
+ function toRect(r) {
1485
+ return { left: r.left, top: r.top, right: r.right, bottom: r.bottom, width: r.width, height: r.height };
1486
+ }
1487
+ var Popover = class {
1488
+ constructor(opts) {
1489
+ this.onOutside = null;
1490
+ this.onKey = null;
1491
+ this.onReflow = null;
1492
+ this.shown = false;
1493
+ const doc = opts.trigger.ownerDocument;
1494
+ injectStyles(POPOVER_STYLE_ID, POPOVER_CSS, doc);
1495
+ this.trigger = opts.trigger;
1496
+ this.host = opts.host ?? doc.body;
1497
+ this.ctrl = popoverController(opts);
1498
+ this.boundary = opts.boundary ?? "viewport";
1499
+ this.theme = opts.theme;
1500
+ this.el = doc.createElement("div");
1501
+ this.el.className = "vela-popover vela-ui-layer" + (opts.className ? ` ${opts.className}` : "");
1502
+ this.el.dataset.position = this.ctrl.position;
1503
+ if (opts.zIndex !== void 0) this.el.style.zIndex = String(opts.zIndex);
1504
+ this.el.addEventListener("pointerdown", (e) => e.stopPropagation());
1505
+ if (opts.content instanceof Node) this.el.appendChild(opts.content);
1506
+ else if (typeof opts.content === "function") opts.content(this.el);
1507
+ }
1508
+ get open() {
1509
+ return this.shown;
1510
+ }
1511
+ get position() {
1512
+ return this.ctrl.position;
1513
+ }
1514
+ get align() {
1515
+ return this.ctrl.align;
1516
+ }
1517
+ show() {
1518
+ if (this.shown) {
1519
+ this.place();
1520
+ return;
1521
+ }
1522
+ if (open && open !== this) open.hide();
1523
+ ensureUIHost(this.el, this.theme);
1524
+ this.host.appendChild(this.el);
1525
+ this.shown = true;
1526
+ open = this;
1527
+ this.place();
1528
+ const onOutside = (ev) => {
1529
+ const t = ev.target;
1530
+ if (this.el.contains(t) || this.trigger.contains(t)) return;
1531
+ this.hide();
1532
+ };
1533
+ const onKey = (ev) => {
1534
+ if (ev.key !== "Escape") return;
1535
+ ev.preventDefault();
1536
+ ev.stopPropagation();
1537
+ this.hide();
1538
+ };
1539
+ const onReflow = () => this.place();
1540
+ setTimeout(() => document.addEventListener("pointerdown", onOutside, true), 0);
1541
+ document.addEventListener("keydown", onKey, true);
1542
+ window.addEventListener("resize", onReflow, true);
1543
+ document.addEventListener("scroll", onReflow, true);
1544
+ this.onOutside = onOutside;
1545
+ this.onKey = onKey;
1546
+ this.onReflow = onReflow;
1547
+ }
1548
+ hide() {
1549
+ if (!this.shown) return;
1550
+ if (this.onOutside) document.removeEventListener("pointerdown", this.onOutside, true);
1551
+ if (this.onKey) document.removeEventListener("keydown", this.onKey, true);
1552
+ if (this.onReflow) {
1553
+ window.removeEventListener("resize", this.onReflow, true);
1554
+ document.removeEventListener("scroll", this.onReflow, true);
1555
+ }
1556
+ this.onOutside = null;
1557
+ this.onKey = null;
1558
+ this.onReflow = null;
1559
+ this.el.remove();
1560
+ this.shown = false;
1561
+ if (open === this) open = null;
1562
+ this.ctrl.onClose?.();
1563
+ }
1564
+ /** Show if closed, hide if this instance is the open popover. */
1565
+ toggle() {
1566
+ if (this.shown) this.hide();
1567
+ else this.show();
1568
+ }
1569
+ destroy() {
1570
+ this.hide();
1571
+ }
1572
+ reposition() {
1573
+ if (this.shown) this.place();
1574
+ }
1575
+ clampRect() {
1576
+ const view = viewportRect(window.innerWidth, window.innerHeight, this.ctrl.viewportInset);
1577
+ const bound = this.readBoundary();
1578
+ if (!bound) return view;
1579
+ const inset = insetRect(bound, this.ctrl.boundaryInset);
1580
+ return intersectRects(view, inset);
1581
+ }
1582
+ readBoundary() {
1583
+ const b = this.boundary;
1584
+ if (b === "viewport") return null;
1585
+ const raw = typeof b === "function" ? b() : b.getBoundingClientRect();
1586
+ return raw ? toRect(raw) : null;
1587
+ }
1588
+ place() {
1589
+ const ar = this.trigger.getBoundingClientRect();
1590
+ if (this.ctrl.matchWidth) {
1591
+ this.el.style.minWidth = `${Math.round(Math.max(this.el.offsetWidth, ar.width))}px`;
1592
+ }
1593
+ const origin = this.ctrl.position === "absolute" ? this.host.getBoundingClientRect() : { left: 0, top: 0 };
1594
+ const pos = placePopover({
1595
+ trigger: toRect(ar),
1596
+ pop: { width: this.el.offsetWidth, height: this.el.offsetHeight },
1597
+ gap: this.ctrl.gap,
1598
+ align: this.ctrl.align,
1599
+ clamp: this.clampRect(),
1600
+ originX: origin.left,
1601
+ originY: origin.top
1602
+ });
1603
+ this.el.style.left = `${pos.left}px`;
1604
+ this.el.style.top = `${pos.top}px`;
1605
+ }
1606
+ };
1607
+
1608
+ // src/ui/components/switch/controller.ts
1609
+ function switchController(opts = {}) {
1610
+ let checked = opts.checked ?? false;
1611
+ const disabled = opts.disabled ?? false;
1612
+ return {
1613
+ get checked() {
1614
+ return checked;
1615
+ },
1616
+ disabled,
1617
+ size: opts.size ?? "md",
1618
+ tone: opts.tone ?? "bright",
1619
+ setChecked(v) {
1620
+ checked = v;
1621
+ },
1622
+ toggle() {
1623
+ if (disabled) return checked;
1624
+ checked = !checked;
1625
+ opts.onChange?.(checked);
1626
+ return checked;
1627
+ }
1628
+ };
1629
+ }
1630
+
1631
+ // src/ui/components/switch/styles.ts
1632
+ var SWITCH_STYLE_ID = "vela-ui-switch";
1633
+ var SWITCH_CSS = `
1634
+ .vela-switch {
1635
+ width: 20px;
1636
+ height: 20px;
1637
+ flex: 0 0 auto;
1638
+ display: inline-flex;
1639
+ align-items: center;
1640
+ justify-content: center;
1641
+ padding: 0;
1642
+ border: 1px solid var(--vela-border);
1643
+ border-radius: 5px;
1644
+ background: transparent;
1645
+ color: transparent;
1646
+ cursor: pointer;
1647
+ line-height: 0;
1648
+ transition: background 0.12s ease, border-color 0.12s ease, color 0.12s ease;
1649
+ }
1650
+ .vela-switch .vela-icon, .vela-switch svg { display: block; width: 12px; height: 12px; }
1651
+ .vela-switch[data-size='sm'] .vela-icon, .vela-switch[data-size='sm'] svg { width: 11px; height: 11px; }
1652
+ .vela-switch:hover { border-color: var(--vela-fg-muted); }
1653
+ .vela-switch[data-checked] {
1654
+ background: var(--vela-fg-bright);
1655
+ border-color: var(--vela-fg-bright);
1656
+ color: var(--vela-bg);
1657
+ }
1658
+ .vela-switch[data-size='sm'] { width: 18px; height: 18px; border-color: var(--vela-border-strong); }
1659
+ .vela-switch[data-size='sm']:hover { border-color: var(--vela-fg-muted); }
1660
+ .vela-switch[data-size='sm'][data-checked] {
1661
+ background: var(--vela-selected-bg);
1662
+ border-color: var(--vela-selected-bg);
1663
+ color: var(--vela-selected-fg);
1664
+ }
1665
+ .vela-switch[data-tone='selected'][data-checked] {
1666
+ background: var(--vela-selected-bg);
1667
+ border-color: var(--vela-selected-bg);
1668
+ color: var(--vela-selected-fg);
1669
+ }
1670
+ .vela-switch[disabled] { opacity: 0.4; cursor: default; }
1671
+ `;
1672
+
1673
+ // src/ui/components/switch/view.ts
1674
+ var Switch = class {
1675
+ constructor(opts = {}) {
1676
+ injectStyles(SWITCH_STYLE_ID, SWITCH_CSS, document);
1677
+ this.ctrl = switchController(opts);
1678
+ const b = document.createElement("button");
1679
+ b.type = "button";
1680
+ if (opts.id) b.id = opts.id;
1681
+ b.className = "vela-switch";
1682
+ b.dataset.size = this.ctrl.size;
1683
+ b.dataset.tone = this.ctrl.tone;
1684
+ b.setAttribute("role", "switch");
1685
+ if (this.ctrl.disabled) b.disabled = true;
1686
+ b.appendChild(iconEl("check", b.ownerDocument));
1687
+ this.el = b;
1688
+ this.paint();
1689
+ b.addEventListener("pointerdown", (e) => {
1690
+ e.preventDefault();
1691
+ b.focus({ preventScroll: true });
1692
+ });
1693
+ b.addEventListener("click", () => {
1694
+ this.ctrl.toggle();
1695
+ this.paint();
1696
+ });
1697
+ }
1698
+ get checked() {
1699
+ return this.ctrl.checked;
1700
+ }
1701
+ setChecked(v) {
1702
+ this.ctrl.setChecked(v);
1703
+ this.paint();
1704
+ }
1705
+ paint() {
1706
+ const on = this.ctrl.checked;
1707
+ this.el.setAttribute("aria-checked", on ? "true" : "false");
1708
+ if (on) this.el.dataset.checked = "";
1709
+ else delete this.el.dataset.checked;
1710
+ }
1711
+ };
1712
+
1713
+ // src/ui/components/select/controller.ts
1714
+ function selectController(opts) {
1715
+ const options = opts.options;
1716
+ let value = opts.value ?? options[0]?.value ?? "";
1717
+ const size = opts.size ?? "md";
1718
+ return {
1719
+ options,
1720
+ get value() {
1721
+ return value;
1722
+ },
1723
+ size,
1724
+ fill: opts.fill ?? size !== "sm",
1725
+ disabled: opts.disabled ?? false,
1726
+ setValue(v) {
1727
+ value = v;
1728
+ },
1729
+ labelOf(v) {
1730
+ return options.find((o) => o.value === v)?.label ?? v;
1731
+ },
1732
+ pick(v) {
1733
+ value = v;
1734
+ const label = options.find((o) => o.value === v)?.label ?? v;
1735
+ opts.onChange?.(v, label);
1736
+ return { value: v, label };
1737
+ }
1738
+ };
1739
+ }
1740
+
1741
+ // src/ui/components/select/styles.ts
1742
+ var SELECT_STYLE_ID = "vela-ui-select";
1743
+ var SELECT_CSS = `
1744
+ .vela-select { position: relative; display: inline-block; min-width: 0; }
1745
+ .vela-select[data-fill] { width: 100%; }
1746
+ .vela-select-trigger {
1747
+ display: flex;
1748
+ align-items: center;
1749
+ width: 100%;
1750
+ box-sizing: border-box;
1751
+ height: 34px;
1752
+ padding: 0 26px 0 8px;
1753
+ background: transparent;
1754
+ border: 1px solid var(--vela-border-strong);
1755
+ border-radius: 6px;
1756
+ color: var(--vela-fg-bright);
1757
+ font-size: 14px;
1758
+ font-family: inherit;
1759
+ cursor: pointer;
1760
+ text-align: left;
1761
+ outline: none;
1762
+ transition: border-color 0.12s ease, box-shadow 0.12s ease;
1763
+ }
1764
+ .vela-select-trigger:hover { border-color: var(--vela-fg-muted); }
1765
+ .vela-select-trigger:focus { border-color: var(--vela-focus); box-shadow: 0 0 0 3px var(--vela-focus-soft); }
1766
+ /* As a grid item (chart-settings rows dissolve via display:contents into a shared
1767
+ max-content column) the wrap must NOT stretch to the column: it sizes to its own
1768
+ widest option, like the native select it replaces. */
1769
+ .vela-select[data-size='sm'] { max-width: 200px; flex: 0 0 auto; justify-self: start; }
1770
+ .vela-select[data-size='sm'] .vela-select-trigger {
1771
+ height: 28px;
1772
+ background: var(--vela-surface-elev);
1773
+ border-radius: var(--vela-radius-sm);
1774
+ color: var(--vela-fg);
1775
+ font-size: 13px;
1776
+ box-shadow: none;
1777
+ }
1778
+ .vela-select[data-size='sm'] .vela-select-trigger:focus { box-shadow: none; border-color: var(--vela-fg-muted); }
1779
+ .vela-select-label { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
1780
+ /* Invisible option-label stack: gives the shrink-to-fit wrap the WIDEST option's
1781
+ intrinsic width (like a native select), so picking a shorter/longer label never
1782
+ resizes the trigger and shifts the row around it. */
1783
+ .vela-select-sizer { visibility: hidden; height: 0; overflow: hidden; font-size: 14px; }
1784
+ .vela-select[data-size='sm'] .vela-select-sizer { font-size: 13px; }
1785
+ .vela-select-sizer span { display: block; height: 0; white-space: nowrap; padding: 0 26px 0 8px; border-inline: 1px solid transparent; }
1786
+ .vela-select-chevron {
1787
+ position: absolute;
1788
+ right: 8px;
1789
+ top: 50%;
1790
+ transform: translateY(-50%);
1791
+ pointer-events: none;
1792
+ display: flex;
1793
+ opacity: 0.55;
1794
+ line-height: 0;
1795
+ color: inherit;
1796
+ }
1797
+ .vela-select-chevron .vela-icon, .vela-select-chevron svg { width: 12px; height: 12px; display: block; }
1798
+ .vela-select-list {
1799
+ background: var(--vela-bg);
1800
+ color: var(--vela-fg);
1801
+ border: none;
1802
+ border-radius: 6px;
1803
+ box-shadow: var(--vela-shadow);
1804
+ font: 14px var(--vela-font);
1805
+ padding: 4px;
1806
+ overflow: hidden;
1807
+ }
1808
+ .vela-select-list[data-size='sm'] { font-size: 13px; background: var(--vela-surface-overlay); }
1809
+ .vela-select-items { max-height: none; overflow: hidden; }
1810
+ .vela-select-list.is-scroll { display: flex; align-items: stretch; gap: 2px; }
1811
+ .vela-select-list.is-scroll .vela-select-items {
1812
+ flex: 1 1 auto;
1813
+ min-width: 0;
1814
+ max-height: 240px;
1815
+ overflow-y: auto;
1816
+ scrollbar-width: none;
1817
+ }
1818
+ .vela-select-list.is-scroll .vela-select-items::-webkit-scrollbar { display: none; width: 0; height: 0; }
1819
+ .vela-select-rail { position: relative; flex: none; width: 3px; margin: 2px 1px 2px 0; pointer-events: none; }
1820
+ .vela-select-thumb { width: 3px; border-radius: 2px; background: var(--vela-scroll); }
1821
+ .vela-select-item {
1822
+ display: block;
1823
+ width: 100%;
1824
+ text-align: left;
1825
+ padding: 7px 10px;
1826
+ border: none;
1827
+ border-radius: 4px;
1828
+ background: transparent;
1829
+ color: inherit;
1830
+ cursor: pointer;
1831
+ font: inherit;
1832
+ font-weight: 400;
1833
+ }
1834
+ .vela-select-item:hover { background: var(--vela-hover); }
1835
+ .vela-select-item[data-checked] { background: var(--vela-hover-strong); color: var(--vela-fg-bright); }
1836
+ .vela-select-item[data-checked]:hover { background: var(--vela-hover-strong); }
1837
+ `;
1838
+
1839
+ // src/ui/components/select/view.ts
1840
+ function ensureSelectStyles(doc) {
1841
+ injectStyles(SELECT_STYLE_ID, SELECT_CSS, doc);
1842
+ }
1843
+ function syncThumb(list, thumb) {
1844
+ const view = list.clientHeight;
1845
+ const total = list.scrollHeight;
1846
+ if (total <= view) {
1847
+ thumb.style.height = "0";
1848
+ return;
1849
+ }
1850
+ const thumbH = Math.max(20, view / total * view);
1851
+ const top = list.scrollTop / (total - view) * (view - thumbH);
1852
+ thumb.style.height = `${Math.round(thumbH)}px`;
1853
+ thumb.style.transform = `translateY(${Math.round(top)}px`;
1854
+ }
1855
+ function fillSelectList(menu2, options, current, onPick) {
1856
+ menu2.replaceChildren();
1857
+ const list = menu2.ownerDocument.createElement("div");
1858
+ list.className = "vela-select-items";
1859
+ for (const p of options) {
1860
+ const item = menu2.ownerDocument.createElement("button");
1861
+ item.type = "button";
1862
+ item.className = "vela-select-item";
1863
+ item.textContent = p.label;
1864
+ if (p.value === current) item.dataset.checked = "1";
1865
+ item.addEventListener("click", (e) => {
1866
+ e.stopPropagation();
1867
+ onPick(p.value, p.label);
1868
+ });
1869
+ list.appendChild(item);
1870
+ }
1871
+ menu2.appendChild(list);
1872
+ }
1873
+ function decorateSelectScroll(menu2) {
1874
+ const list = menu2.querySelector(".vela-select-items");
1875
+ if (!list) return;
1876
+ if (list.scrollHeight > 240) {
1877
+ menu2.classList.add("is-scroll");
1878
+ const rail = menu2.ownerDocument.createElement("div");
1879
+ rail.className = "vela-select-rail";
1880
+ const thumb = menu2.ownerDocument.createElement("div");
1881
+ thumb.className = "vela-select-thumb";
1882
+ rail.appendChild(thumb);
1883
+ menu2.appendChild(rail);
1884
+ list.addEventListener("scroll", () => syncThumb(list, thumb));
1885
+ syncThumb(list, thumb);
1886
+ }
1887
+ menu2.querySelector(".vela-select-item[data-checked]")?.scrollIntoView({ block: "nearest" });
1888
+ list.dispatchEvent(new Event("scroll"));
1889
+ }
1890
+ function toggleSelectList(trigger, options, current, onPick, opts = {}) {
1891
+ if (openPopoverTrigger() === trigger) {
1892
+ closeOpenPopovers();
1893
+ return null;
1894
+ }
1895
+ return openSelectList(trigger, options, current, onPick, opts);
1896
+ }
1897
+ function openSelectList(trigger, options, current, onPick, opts = {}) {
1898
+ ensureSelectStyles(trigger.ownerDocument);
1899
+ closeOpenPopovers();
1900
+ const pop = new Popover({
1901
+ trigger,
1902
+ theme: opts.theme,
1903
+ matchWidth: opts.matchWidth ?? true,
1904
+ boundary: opts.boundary,
1905
+ boundaryInset: opts.boundaryInset,
1906
+ gap: opts.gap ?? 4,
1907
+ align: opts.align ?? "start",
1908
+ host: opts.host,
1909
+ position: opts.position,
1910
+ zIndex: opts.zIndex,
1911
+ className: "vela-select-list",
1912
+ onClose: opts.onClose,
1913
+ content: (el) => {
1914
+ if (opts.size) el.dataset.size = opts.size;
1915
+ fillSelectList(el, options, current, (value, label) => {
1916
+ pop.hide();
1917
+ onPick(value, label);
1918
+ });
1919
+ }
1920
+ });
1921
+ pop.show();
1922
+ decorateSelectScroll(pop.el);
1923
+ pop.reposition();
1924
+ return pop;
1925
+ }
1926
+ var Select = class {
1927
+ constructor(opts) {
1928
+ this.list = null;
1929
+ const doc = opts.list?.host?.ownerDocument ?? document;
1930
+ ensureSelectStyles(doc);
1931
+ this.ctrl = selectController(opts);
1932
+ this.listOpts = { ...opts.list, theme: opts.theme ?? opts.list?.theme, size: this.ctrl.size };
1933
+ const wrap = doc.createElement("div");
1934
+ wrap.className = "vela-select";
1935
+ wrap.dataset.size = this.ctrl.size;
1936
+ if (this.ctrl.fill) wrap.dataset.fill = "";
1937
+ const btn = doc.createElement("button");
1938
+ btn.type = "button";
1939
+ if (opts.id) btn.id = opts.id;
1940
+ btn.className = "vela-select-trigger";
1941
+ if (this.ctrl.disabled) btn.disabled = true;
1942
+ const label = doc.createElement("span");
1943
+ label.className = "vela-select-label";
1944
+ label.textContent = this.ctrl.labelOf(this.ctrl.value);
1945
+ const chevron = doc.createElement("span");
1946
+ chevron.className = "vela-select-chevron";
1947
+ chevron.appendChild(iconEl("chevron-down", doc));
1948
+ btn.append(label, chevron);
1949
+ wrap.appendChild(btn);
1950
+ const sizer = doc.createElement("div");
1951
+ sizer.className = "vela-select-sizer";
1952
+ sizer.setAttribute("aria-hidden", "true");
1953
+ for (const o of this.ctrl.options) {
1954
+ const s = doc.createElement("span");
1955
+ s.textContent = o.label;
1956
+ sizer.appendChild(s);
1957
+ }
1958
+ wrap.appendChild(sizer);
1959
+ btn.addEventListener("pointerdown", (e) => {
1960
+ e.preventDefault();
1961
+ btn.focus({ preventScroll: true });
1962
+ });
1963
+ btn.addEventListener("click", (e) => {
1964
+ e.stopPropagation();
1965
+ this.toggle();
1966
+ });
1967
+ this.el = wrap;
1968
+ this.trigger = btn;
1969
+ this.labelEl = label;
1970
+ }
1971
+ get value() {
1972
+ return this.ctrl.value;
1973
+ }
1974
+ setValue(v) {
1975
+ this.ctrl.setValue(v);
1976
+ this.labelEl.textContent = this.ctrl.labelOf(v);
1977
+ }
1978
+ toggle() {
1979
+ this.list = toggleSelectList(
1980
+ this.trigger,
1981
+ this.ctrl.options,
1982
+ this.ctrl.value,
1983
+ (value, label) => {
1984
+ this.ctrl.pick(value);
1985
+ this.labelEl.textContent = label;
1986
+ this.list = null;
1987
+ },
1988
+ { ...this.listOpts, onClose: () => {
1989
+ this.list = null;
1990
+ this.listOpts.onClose?.();
1991
+ } }
1992
+ );
1993
+ }
1994
+ destroy() {
1995
+ this.list?.hide();
1996
+ this.list = null;
1997
+ }
1998
+ };
1999
+
2000
+ // src/ui/components/number-input/controller.ts
2001
+ function clampNumber(n, opts) {
2002
+ let v = n;
2003
+ if (opts.min !== void 0) v = Math.max(opts.min, v);
2004
+ if (opts.max !== void 0) v = Math.min(opts.max, v);
2005
+ if (opts.integer) v = Math.round(v);
2006
+ return v;
2007
+ }
2008
+ function decimalsOf(n) {
2009
+ const s = String(n);
2010
+ const e = s.search(/[eE]/);
2011
+ if (e >= 0) {
2012
+ const frac = s.slice(0, e).split(".")[1]?.length ?? 0;
2013
+ return Math.max(0, Math.min(20, frac - Number(s.slice(e + 1))));
2014
+ }
2015
+ return s.split(".")[1]?.length ?? 0;
2016
+ }
2017
+ function snapToStep(n, base, step) {
2018
+ const d = Math.min(20, Math.max(decimalsOf(base), decimalsOf(step)));
2019
+ return Number(n.toFixed(d));
2020
+ }
2021
+ function numberInputController(opts) {
2022
+ let value = opts.value;
2023
+ const integer = opts.integer ?? false;
2024
+ const commit = opts.commit ?? "blur";
2025
+ const clamp = opts.clamp ?? commit === "blur";
2026
+ const step = opts.step ?? (integer ? 1 : 0.1);
2027
+ const onChange = opts.onChange;
2028
+ const normalize = (raw) => clamp ? clampNumber(raw, { min: opts.min, max: opts.max, integer }) : raw;
2029
+ const apply = (raw) => {
2030
+ if (!Number.isFinite(raw)) return null;
2031
+ const n = normalize(raw);
2032
+ if (n !== value) {
2033
+ value = n;
2034
+ onChange?.(n);
2035
+ }
2036
+ return n;
2037
+ };
2038
+ return {
2039
+ get value() {
2040
+ return value;
2041
+ },
2042
+ min: opts.min,
2043
+ max: opts.max,
2044
+ step,
2045
+ integer,
2046
+ size: opts.size ?? "md",
2047
+ commit,
2048
+ steppers: opts.steppers ?? commit === "blur",
2049
+ clamp,
2050
+ disabled: opts.disabled ?? false,
2051
+ apply,
2052
+ sync(raw) {
2053
+ const n = Number.isFinite(raw) ? normalize(raw) : value;
2054
+ value = n;
2055
+ return n;
2056
+ },
2057
+ nudge(dir) {
2058
+ return apply(snapToStep(value + dir * step, value, step));
2059
+ }
2060
+ };
2061
+ }
2062
+
2063
+ // src/ui/components/number-input/styles.ts
2064
+ var NUMBER_STYLE_ID = "vela-ui-number";
2065
+ var NUMBER_CSS = `
2066
+ .vela-num { position: relative; display: inline-block; min-width: 0; }
2067
+ .vela-num[data-fill] { width: 100%; }
2068
+ .vela-num input {
2069
+ width: 100%;
2070
+ box-sizing: border-box;
2071
+ height: 34px;
2072
+ background: transparent;
2073
+ border: 1px solid var(--vela-border-strong);
2074
+ border-radius: 6px;
2075
+ color: var(--vela-fg-bright);
2076
+ padding: 0 8px;
2077
+ font-size: 14px;
2078
+ font-family: inherit;
2079
+ outline: none;
2080
+ transition: border-color 0.12s ease, box-shadow 0.12s ease;
2081
+ -moz-appearance: textfield;
2082
+ appearance: textfield;
2083
+ }
2084
+ .vela-num input::-webkit-inner-spin-button, .vela-num input::-webkit-outer-spin-button { -webkit-appearance: none; margin: 0; }
2085
+ .vela-num input:hover { border-color: var(--vela-fg-muted); }
2086
+ .vela-num input:focus { border-color: var(--vela-focus); box-shadow: 0 0 0 3px var(--vela-focus-soft); }
2087
+ .vela-num[data-steppers] input { padding-right: 26px; }
2088
+ .vela-num[data-size='sm'] { width: 64px; flex: none; }
2089
+ .vela-num[data-size='sm'][data-compact] { width: 56px; }
2090
+ .vela-num[data-size='sm'] input {
2091
+ height: 28px;
2092
+ background: var(--vela-surface-elev);
2093
+ border-radius: var(--vela-radius-sm);
2094
+ color: var(--vela-fg);
2095
+ font-size: 13px;
2096
+ box-shadow: none;
2097
+ }
2098
+ .vela-num[data-size='sm'] input:focus { box-shadow: none; }
2099
+ .vela-num-step {
2100
+ position: absolute;
2101
+ right: 0;
2102
+ top: 0;
2103
+ bottom: 0;
2104
+ width: 22px;
2105
+ display: none;
2106
+ flex-direction: column;
2107
+ justify-content: center;
2108
+ box-sizing: border-box;
2109
+ padding: 2px 6px 2px 0;
2110
+ line-height: 0;
2111
+ }
2112
+ .vela-num[data-steppers]:hover .vela-num-step { display: flex; }
2113
+ .vela-num-step button {
2114
+ flex: 1;
2115
+ width: 100%;
2116
+ min-height: 0;
2117
+ border: none;
2118
+ background: transparent;
2119
+ color: var(--vela-fg-muted);
2120
+ border-radius: 3px;
2121
+ padding: 0;
2122
+ cursor: pointer;
2123
+ display: flex;
2124
+ align-items: center;
2125
+ justify-content: center;
2126
+ }
2127
+ .vela-num-step button:hover { background: var(--vela-hover); color: var(--vela-fg-bright); }
2128
+ .vela-num-step .vela-icon, .vela-num-step svg { width: 12px; height: 12px; display: block; }
2129
+ `;
2130
+
2131
+ // src/ui/components/number-input/view.ts
2132
+ var NumberInput = class {
2133
+ constructor(opts) {
2134
+ const doc = document;
2135
+ injectStyles(NUMBER_STYLE_ID, NUMBER_CSS, doc);
2136
+ this.ctrl = numberInputController(opts);
2137
+ this.placeholderMode = opts.placeholder !== void 0;
2138
+ this.emptyValue = opts.emptyValue ?? opts.value;
2139
+ this.last = String(opts.value);
2140
+ const wrap = doc.createElement("div");
2141
+ wrap.className = "vela-num";
2142
+ wrap.dataset.size = this.ctrl.size;
2143
+ if (this.ctrl.size !== "sm") wrap.dataset.fill = "";
2144
+ if (opts.compact) wrap.dataset.compact = "";
2145
+ if (this.ctrl.steppers) wrap.dataset.steppers = "";
2146
+ const ni = doc.createElement("input");
2147
+ ni.type = "number";
2148
+ if (opts.id) ni.id = opts.id;
2149
+ if (opts.title) ni.title = opts.title;
2150
+ if (opts.min !== void 0) ni.min = String(opts.min);
2151
+ if (opts.max !== void 0) ni.max = String(opts.max);
2152
+ ni.step = String(this.ctrl.step);
2153
+ if (this.ctrl.disabled) ni.disabled = true;
2154
+ if (this.placeholderMode) {
2155
+ ni.placeholder = opts.placeholder ?? "";
2156
+ ni.value = opts.value !== this.emptyValue ? String(opts.value) : "";
2157
+ } else {
2158
+ ni.value = String(opts.value);
2159
+ }
2160
+ const commitRaw = (raw) => {
2161
+ const n = this.ctrl.apply(raw);
2162
+ if (n === null) {
2163
+ ni.value = this.placeholderMode && this.ctrl.value === this.emptyValue ? "" : this.last;
2164
+ return;
2165
+ }
2166
+ this.last = String(n);
2167
+ if (this.placeholderMode && n === this.emptyValue) ni.value = "";
2168
+ else ni.value = String(n);
2169
+ };
2170
+ if (this.placeholderMode) {
2171
+ ni.addEventListener("change", () => {
2172
+ const raw = ni.value.trim() === "" ? this.emptyValue : Number(ni.value);
2173
+ commitRaw(Number.isFinite(raw) ? raw : this.emptyValue);
2174
+ });
2175
+ } else if (this.ctrl.commit === "live") {
2176
+ ni.addEventListener("input", () => {
2177
+ const v = Number(ni.value);
2178
+ if (Number.isFinite(v)) this.ctrl.apply(v);
2179
+ });
2180
+ } else {
2181
+ ni.addEventListener("blur", () => commitRaw(Number(ni.value)));
2182
+ ni.addEventListener("keydown", (e) => {
2183
+ if (e.key === "Enter") {
2184
+ e.preventDefault();
2185
+ ni.blur();
2186
+ }
2187
+ });
2188
+ }
2189
+ wrap.appendChild(ni);
2190
+ if (this.ctrl.steppers) {
2191
+ wrap.appendChild(this.buildSteppers(doc, (dir) => {
2192
+ const cur = Number(ni.value);
2193
+ const base = Number.isFinite(cur) ? cur : this.ctrl.value;
2194
+ commitRaw(snapToStep(base + dir * this.ctrl.step, base, this.ctrl.step));
2195
+ }));
2196
+ }
2197
+ this.el = wrap;
2198
+ this.input = ni;
2199
+ }
2200
+ get value() {
2201
+ return this.ctrl.value;
2202
+ }
2203
+ setValue(v) {
2204
+ const n = this.ctrl.sync(v);
2205
+ this.last = String(n);
2206
+ if (this.placeholderMode && n === this.emptyValue) this.input.value = "";
2207
+ else this.input.value = String(n);
2208
+ }
2209
+ buildSteppers(doc, applyDir) {
2210
+ const steps = doc.createElement("div");
2211
+ steps.className = "vela-num-step";
2212
+ const mk = (dir, icon, label) => {
2213
+ const b = doc.createElement("button");
2214
+ b.type = "button";
2215
+ b.tabIndex = -1;
2216
+ b.setAttribute("aria-label", label);
2217
+ b.appendChild(iconEl(icon, doc));
2218
+ let timer = 0;
2219
+ const apply = () => applyDir(dir);
2220
+ const stop = () => {
2221
+ if (timer) {
2222
+ window.clearTimeout(timer);
2223
+ timer = 0;
2224
+ }
2225
+ };
2226
+ b.addEventListener("pointerdown", (e) => {
2227
+ e.preventDefault();
2228
+ e.stopPropagation();
2229
+ apply();
2230
+ timer = window.setTimeout(function tick() {
2231
+ apply();
2232
+ timer = window.setTimeout(tick, 60);
2233
+ }, 400);
2234
+ });
2235
+ b.addEventListener("pointerup", stop);
2236
+ b.addEventListener("pointerleave", stop);
2237
+ b.addEventListener("pointercancel", stop);
2238
+ return b;
2239
+ };
2240
+ steps.append(mk(1, "chevron-up", "Increase"), mk(-1, "chevron-down", "Decrease"));
2241
+ return steps;
2242
+ }
2243
+ };
2244
+
2245
+ // src/ui/components/text-field/controller.ts
2246
+ function textFieldController(opts = {}) {
2247
+ let value = opts.value ?? "";
2248
+ const size = opts.size ?? "md";
2249
+ return {
2250
+ get value() {
2251
+ return value;
2252
+ },
2253
+ size,
2254
+ fill: opts.fill ?? size !== "sm",
2255
+ disabled: opts.disabled ?? false,
2256
+ commit(next) {
2257
+ if (next === value) return null;
2258
+ value = next;
2259
+ opts.onChange?.(next);
2260
+ return next;
2261
+ },
2262
+ sync(next) {
2263
+ value = next;
2264
+ }
2265
+ };
2266
+ }
2267
+
2268
+ // src/ui/components/text-field/styles.ts
2269
+ var TEXT_STYLE_ID = "vela-ui-text";
2270
+ var TEXT_CSS = `
2271
+ .vela-text {
2272
+ display: inline-block;
2273
+ min-width: 0;
2274
+ }
2275
+ .vela-text[data-fill] { width: 100%; }
2276
+ .vela-text-field {
2277
+ width: 100%;
2278
+ box-sizing: border-box;
2279
+ height: 34px;
2280
+ background: transparent;
2281
+ border: 1px solid var(--vela-border-strong);
2282
+ border-radius: 6px;
2283
+ color: var(--vela-fg-bright);
2284
+ padding: 0 8px;
2285
+ font-size: 14px;
2286
+ font-family: inherit;
2287
+ outline: none;
2288
+ transition: border-color 0.12s ease, box-shadow 0.12s ease;
2289
+ }
2290
+ .vela-text-field:hover { border-color: var(--vela-fg-muted); }
2291
+ .vela-text-field:focus { border-color: var(--vela-focus); box-shadow: 0 0 0 3px var(--vela-focus-soft); }
2292
+ .vela-text[data-size='sm'] .vela-text-field {
2293
+ height: 28px;
2294
+ background: var(--vela-surface-elev);
2295
+ border-radius: var(--vela-radius-sm);
2296
+ color: var(--vela-fg);
2297
+ font-size: 13px;
2298
+ }
2299
+ `;
2300
+
2301
+ // src/ui/components/text-field/view.ts
2302
+ var TextField = class {
2303
+ constructor(opts = {}) {
2304
+ const doc = document;
2305
+ injectStyles(TEXT_STYLE_ID, TEXT_CSS, doc);
2306
+ this.ctrl = textFieldController(opts);
2307
+ const wrap = doc.createElement("div");
2308
+ wrap.className = "vela-text";
2309
+ wrap.dataset.size = this.ctrl.size;
2310
+ if (this.ctrl.fill) wrap.dataset.fill = "";
2311
+ const ti = doc.createElement("input");
2312
+ ti.type = "text";
2313
+ if (opts.id) ti.id = opts.id;
2314
+ ti.value = this.ctrl.value;
2315
+ ti.className = "vela-text-field";
2316
+ if (this.ctrl.disabled) ti.disabled = true;
2317
+ ti.addEventListener("blur", () => {
2318
+ this.ctrl.commit(ti.value);
2319
+ });
2320
+ ti.addEventListener("keydown", (e) => {
2321
+ if (e.key === "Enter") {
2322
+ e.preventDefault();
2323
+ ti.blur();
2324
+ }
2325
+ });
2326
+ wrap.appendChild(ti);
2327
+ this.el = wrap;
2328
+ this.input = ti;
2329
+ }
2330
+ get value() {
2331
+ return this.ctrl.value;
2332
+ }
2333
+ setValue(v) {
2334
+ this.ctrl.sync(v);
2335
+ this.input.value = v;
2336
+ }
2337
+ };
2338
+
2339
+ // src/ui/components/color-picker/controller.ts
2340
+ function splitColor(color) {
2341
+ const s = String(color ?? "").trim();
2342
+ let m = /^#([0-9a-f]{6})([0-9a-f]{2})?$/i.exec(s);
2343
+ if (m) return { hex6: `#${m[1].toLowerCase()}`, alpha: m[2] ? parseInt(m[2], 16) / 255 : 1 };
2344
+ m = /^#([0-9a-f]{3})$/i.exec(s);
2345
+ if (m) {
2346
+ const h = m[1].toLowerCase();
2347
+ return { hex6: `#${h[0]}${h[0]}${h[1]}${h[1]}${h[2]}${h[2]}`, alpha: 1 };
2348
+ }
2349
+ const rgba = /^rgba?\(([^)]+)\)/i.exec(s);
2350
+ if (rgba) {
2351
+ const p = rgba[1].split(",").map((v) => v.trim());
2352
+ const hx = (n) => Math.max(0, Math.min(255, Math.round(n))).toString(16).padStart(2, "0");
2353
+ const a = p[3] != null ? Math.max(0, Math.min(1, parseFloat(p[3]))) : 1;
2354
+ return { hex6: `#${hx(parseInt(p[0] ?? "0", 10))}${hx(parseInt(p[1] ?? "0", 10))}${hx(parseInt(p[2] ?? "0", 10))}`, alpha: a };
2355
+ }
2356
+ return { hex6: ACCENT_BRIGHT, alpha: 1 };
2357
+ }
2358
+ function combineColor(hex6, alpha) {
2359
+ const a = Math.max(0, Math.min(1, alpha));
2360
+ if (a >= 0.999) return hex6;
2361
+ return `${hex6}${Math.round(a * 255).toString(16).padStart(2, "0")}`;
2362
+ }
2363
+ function blendOver(fg, bg, alpha) {
2364
+ const rgb = (c) => {
2365
+ const n = parseInt(splitColor(c).hex6.slice(1), 16);
2366
+ return [n >> 16 & 255, n >> 8 & 255, n & 255];
2367
+ };
2368
+ const [fr, fgc, fb] = rgb(fg);
2369
+ const [br, bgc, bb] = rgb(bg);
2370
+ const a = Math.max(0, Math.min(1, alpha));
2371
+ const h = (n) => Math.round(n).toString(16).padStart(2, "0");
2372
+ return `#${h(fr * a + br * (1 - a))}${h(fgc * a + bgc * (1 - a))}${h(fb * a + bb * (1 - a))}`;
2373
+ }
2374
+ function hslHex(h, s, l) {
2375
+ const sn = s / 100;
2376
+ const ln = l / 100;
2377
+ const k = (n) => (n + h / 30) % 12;
2378
+ const a = sn * Math.min(ln, 1 - ln);
2379
+ const f = (n) => ln - a * Math.max(-1, Math.min(k(n) - 3, Math.min(9 - k(n), 1)));
2380
+ const to = (x) => Math.round(x * 255).toString(16).padStart(2, "0");
2381
+ return `#${to(f(0))}${to(f(8))}${to(f(4))}`;
2382
+ }
2383
+ function buildPalette() {
2384
+ const grays = [100, 90, 80, 68, 56, 45, 35, 25, 14, 0].map((l) => hslHex(0, 0, l));
2385
+ const hues = [4, 28, 50, 96, 140, 174, 200, 224, 270, 322];
2386
+ const levels = [
2387
+ [82, 56],
2388
+ [58, 84],
2389
+ [62, 74],
2390
+ [66, 62],
2391
+ [70, 50],
2392
+ [64, 39],
2393
+ [54, 29]
2394
+ ];
2395
+ return [grays, ...levels.map(([s, l]) => hues.map((h) => hslHex(h, s, l)))];
2396
+ }
2397
+ function transparencyChecker(size) {
2398
+ return `repeating-conic-gradient(#9aa0a6 0% 25%, #d3d6da 0% 50%) 0 0 / ${size}px ${size}px`;
2399
+ }
2400
+ function colorPickerController(opts = {}) {
2401
+ const parsed = splitColor(opts.color ?? ACCENT_BRIGHT);
2402
+ let hex6 = parsed.hex6;
2403
+ let alpha = parsed.alpha;
2404
+ return {
2405
+ get hex6() {
2406
+ return hex6;
2407
+ },
2408
+ get alpha() {
2409
+ return alpha;
2410
+ },
2411
+ combined() {
2412
+ return combineColor(hex6, alpha);
2413
+ },
2414
+ setHex(hex) {
2415
+ hex6 = splitColor(hex).hex6;
2416
+ },
2417
+ setAlpha(a) {
2418
+ alpha = Math.max(0, Math.min(1, a));
2419
+ }
2420
+ };
2421
+ }
2422
+
2423
+ // src/ui/components/color-picker/styles.ts
2424
+ var COLOR_STYLE_ID = "vela-ui-color";
2425
+ var COLOR_CSS = `
2426
+ .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;}
2427
+ .vela-color-field:hover{border-color:var(--vela-fg-muted);}
2428
+ .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);}
2429
+ .vela-color-field-circle{width:26px;height:26px;padding:3px;border:1px solid var(--vela-border-strong);border-radius:4px;background:transparent;overflow:hidden;}
2430
+ .vela-color-field-circle .vela-color-field-swatch{border-radius:2px;box-shadow:none;}
2431
+ .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;}
2432
+ `;
2433
+
2434
+ // src/ui/components/color-picker/view.ts
2435
+ var PALETTE = buildPalette();
2436
+ var CHECKER = transparencyChecker(10);
2437
+ var FIELD_CHECKER = transparencyChecker(8);
2438
+ var recents = [ACCENT, BULLISH, BEARISH, WARNING, NEUTRAL];
2439
+ function addRecent(hex6) {
2440
+ const i = recents.findIndex((c) => c.toLowerCase() === hex6.toLowerCase());
2441
+ if (i >= 0) recents.splice(i, 1);
2442
+ recents.unshift(hex6);
2443
+ if (recents.length > 10) recents.length = 10;
2444
+ }
2445
+ function buildColorPicker(color, theme, onChange) {
2446
+ const parsed = splitColor(color);
2447
+ let curHex = parsed.hex6;
2448
+ let curAlpha = parsed.alpha;
2449
+ const root = document.createElement("div");
2450
+ root.style.cssText = "display:flex;flex-direction:column;gap:9px;width:236px;";
2451
+ const grid = document.createElement("div");
2452
+ grid.style.cssText = "display:grid;grid-template-columns:repeat(10,1fr);gap:4px;";
2453
+ const swatches = [];
2454
+ for (const row of PALETTE) {
2455
+ for (const c of row) {
2456
+ const sw = document.createElement("button");
2457
+ sw.type = "button";
2458
+ sw.dataset.c = c;
2459
+ sw.style.cssText = `width:100%;aspect-ratio:1;border-radius:4px;border:none;cursor:pointer;background:${c};padding:0;`;
2460
+ sw.addEventListener("click", (e) => {
2461
+ e.stopPropagation();
2462
+ pickHex(c);
2463
+ });
2464
+ grid.appendChild(sw);
2465
+ swatches.push(sw);
2466
+ }
2467
+ }
2468
+ const paintSelection = () => {
2469
+ for (const sw of swatches) {
2470
+ const on = (sw.dataset.c ?? "").toLowerCase() === curHex.toLowerCase();
2471
+ sw.style.outline = on ? `2px solid ${theme.textColor}` : "none";
2472
+ sw.style.outlineOffset = "2px";
2473
+ sw.style.zIndex = on ? "1" : "";
2474
+ sw.style.position = on ? "relative" : "";
2475
+ }
2476
+ };
2477
+ const recentRow = document.createElement("div");
2478
+ recentRow.style.cssText = "display:flex;align-items:center;gap:5px;flex-wrap:wrap;border-top:1px solid var(--vela-border);padding-top:9px;";
2479
+ const renderRecents = () => {
2480
+ recentRow.replaceChildren();
2481
+ for (const c of recents) {
2482
+ const sw = document.createElement("button");
2483
+ sw.type = "button";
2484
+ 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;`;
2485
+ sw.addEventListener("click", (e) => {
2486
+ e.stopPropagation();
2487
+ pickHex(c);
2488
+ });
2489
+ recentRow.appendChild(sw);
2490
+ }
2491
+ const add = document.createElement("label");
2492
+ 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;`;
2493
+ add.textContent = "+";
2494
+ const input = document.createElement("input");
2495
+ input.type = "color";
2496
+ input.value = curHex;
2497
+ input.style.cssText = "position:absolute;inset:0;opacity:0;cursor:pointer;";
2498
+ input.addEventListener("input", () => pickHex(input.value, true));
2499
+ add.appendChild(input);
2500
+ recentRow.appendChild(add);
2501
+ };
2502
+ const opLabel = document.createElement("div");
2503
+ opLabel.textContent = "Opacity";
2504
+ opLabel.style.cssText = `font:11px ${theme.fontFamily};color:var(--vela-fg-muted);`;
2505
+ const opRow = document.createElement("div");
2506
+ opRow.style.cssText = "display:flex;align-items:center;gap:10px;";
2507
+ const track = document.createElement("div");
2508
+ track.style.cssText = "flex:1;position:relative;height:13px;border-radius:7px;cursor:pointer;";
2509
+ const knob = document.createElement("div");
2510
+ 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;";
2511
+ track.appendChild(knob);
2512
+ const pctBox = document.createElement("div");
2513
+ 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;`;
2514
+ opRow.append(track, pctBox);
2515
+ const paintOpacity = () => {
2516
+ track.style.background = `linear-gradient(to right, ${curHex}00, ${curHex}ff), ${CHECKER}`;
2517
+ knob.style.left = `${curAlpha * 100}%`;
2518
+ pctBox.textContent = `${Math.round(curAlpha * 100)}%`;
2519
+ };
2520
+ let dragging = false;
2521
+ const onDrag = (clientX) => {
2522
+ const r = track.getBoundingClientRect();
2523
+ curAlpha = Math.max(0, Math.min(1, (clientX - r.left) / r.width));
2524
+ paintOpacity();
2525
+ emit();
2526
+ };
2527
+ track.addEventListener("pointerdown", (e) => {
2528
+ e.stopPropagation();
2529
+ dragging = true;
2530
+ track.setPointerCapture(e.pointerId);
2531
+ onDrag(e.clientX);
2532
+ });
2533
+ track.addEventListener("pointermove", (e) => {
2534
+ if (dragging) onDrag(e.clientX);
2535
+ });
2536
+ track.addEventListener("pointerup", () => dragging = false);
2537
+ function emit() {
2538
+ onChange(combineColor(curHex, curAlpha));
2539
+ }
2540
+ function pickHex(hex, custom = false) {
2541
+ curHex = splitColor(hex).hex6;
2542
+ addRecent(curHex);
2543
+ paintSelection();
2544
+ paintOpacity();
2545
+ if (custom) renderRecents();
2546
+ emit();
2547
+ }
2548
+ renderRecents();
2549
+ paintSelection();
2550
+ paintOpacity();
2551
+ root.append(grid, recentRow, opLabel, opRow);
2552
+ return root;
2553
+ }
2554
+ function colorField(theme, getVal, onVal, opts) {
2555
+ return new ColorField({ theme, getVal, onVal, shape: opts?.shape, id: opts?.id, popover: opts?.popover }).el;
2556
+ }
2557
+ var ColorField = class {
2558
+ constructor(opts) {
2559
+ injectStyles(COLOR_STYLE_ID, COLOR_CSS, document);
2560
+ this.theme = opts.theme;
2561
+ this.getVal = opts.getVal;
2562
+ this.onVal = opts.onVal;
2563
+ this.popoverOpts = opts.popover;
2564
+ const trigger = document.createElement("button");
2565
+ trigger.type = "button";
2566
+ if (opts.id) trigger.id = opts.id;
2567
+ trigger.className = opts.shape === "circle" ? "vela-color-field vela-color-field-circle" : "vela-color-field";
2568
+ const swatch = document.createElement("span");
2569
+ swatch.className = "vela-color-field-swatch";
2570
+ trigger.appendChild(swatch);
2571
+ this.el = trigger;
2572
+ this.swatch = swatch;
2573
+ this.paint();
2574
+ trigger.addEventListener("vela-sync", () => this.paint());
2575
+ trigger.addEventListener("pointerdown", (e) => {
2576
+ e.preventDefault();
2577
+ trigger.focus({ preventScroll: true });
2578
+ });
2579
+ trigger.addEventListener("click", (e) => {
2580
+ e.stopPropagation();
2581
+ this.toggle();
2582
+ });
2583
+ }
2584
+ paint() {
2585
+ const v = this.getVal();
2586
+ this.swatch.style.background = `linear-gradient(${v}, ${v}), ${FIELD_CHECKER}`;
2587
+ }
2588
+ toggle() {
2589
+ if (openPopoverTrigger() === this.el) {
2590
+ closeOpenPopovers();
2591
+ return;
2592
+ }
2593
+ const pop = new Popover({
2594
+ trigger: this.el,
2595
+ theme: this.theme,
2596
+ align: this.popoverOpts?.align ?? "end",
2597
+ gap: this.popoverOpts?.gap ?? 6,
2598
+ host: this.popoverOpts?.host,
2599
+ position: this.popoverOpts?.position,
2600
+ boundary: this.popoverOpts?.boundary,
2601
+ zIndex: this.popoverOpts?.zIndex,
2602
+ className: "vela-color-field-pop",
2603
+ content: buildColorPicker(this.getVal(), this.theme, (val) => {
2604
+ this.onVal(val);
2605
+ this.paint();
2606
+ })
2607
+ });
2608
+ pop.show();
2609
+ }
2610
+ };
2611
+ function closeColorPopover() {
2612
+ closeOpenPopovers();
2613
+ }
2614
+
1335
2615
  Object.defineProperty(exports, "normalizeProps", {
1336
2616
  enumerable: true,
1337
2617
  get: function () { return vanilla.normalizeProps; }
@@ -1340,24 +2620,58 @@ Object.defineProperty(exports, "spreadProps", {
1340
2620
  enumerable: true,
1341
2621
  get: function () { return vanilla.spreadProps; }
1342
2622
  });
2623
+ exports.ColorField = ColorField;
1343
2624
  exports.Dialog = Dialog;
1344
2625
  exports.Drawer = Drawer;
1345
2626
  exports.KeymapManager = KeymapManager;
1346
2627
  exports.Menu = Menu;
2628
+ exports.NumberInput = NumberInput;
2629
+ exports.Popover = Popover;
2630
+ exports.Select = Select;
2631
+ exports.Switch = Switch;
2632
+ exports.TextField = TextField;
1347
2633
  exports.Tooltip = Tooltip;
1348
2634
  exports.applyPlotOverlayTokens = applyPlotOverlayTokens;
1349
2635
  exports.applyThemeTokens = applyThemeTokens;
2636
+ exports.blendOver = blendOver;
2637
+ exports.buildColorPicker = buildColorPicker;
2638
+ exports.buildPalette = buildPalette;
2639
+ exports.clampNumber = clampNumber;
2640
+ exports.closeColorPopover = closeColorPopover;
2641
+ exports.closeOpenPopovers = closeOpenPopovers;
2642
+ exports.colorField = colorField;
2643
+ exports.colorPickerController = colorPickerController;
2644
+ exports.combineColor = combineColor;
2645
+ exports.decorateSelectScroll = decorateSelectScroll;
1350
2646
  exports.dialogController = dialogController;
1351
2647
  exports.drawerController = drawerController;
1352
2648
  exports.ensureUIHost = ensureUIHost;
2649
+ exports.fillSelectList = fillSelectList;
2650
+ exports.hslHex = hslHex;
1353
2651
  exports.iconEl = iconEl;
1354
2652
  exports.iconMarkup = iconMarkup;
1355
2653
  exports.injectStyles = injectStyles;
2654
+ exports.insetRect = insetRect;
2655
+ exports.intersectRects = intersectRects;
2656
+ exports.isPopoverOpen = isPopoverOpen;
1356
2657
  exports.menuController = menuController;
1357
2658
  exports.nextUid = nextUid;
2659
+ exports.numberInputController = numberInputController;
2660
+ exports.openPopoverTrigger = openPopoverTrigger;
2661
+ exports.openSelectList = openSelectList;
2662
+ exports.placePopover = placePopover;
2663
+ exports.popoverController = popoverController;
1358
2664
  exports.registerIcon = registerIcon;
1359
2665
  exports.runMachine = runMachine;
2666
+ exports.selectController = selectController;
2667
+ exports.snapToStep = snapToStep;
2668
+ exports.splitColor = splitColor;
1360
2669
  exports.svg16 = svg16;
1361
2670
  exports.svg24 = svg24;
2671
+ exports.switchController = switchController;
2672
+ exports.textFieldController = textFieldController;
2673
+ exports.toggleSelectList = toggleSelectList;
1362
2674
  exports.tooltipController = tooltipController;
2675
+ exports.transparencyChecker = transparencyChecker;
2676
+ exports.viewportRect = viewportRect;
1363
2677
  exports.withAlpha = withAlpha;