@luxalgo/vela 0.5.3 → 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-L5NTMWPP.js → chunk-D6WM5IUE.js} +724 -851
  5. package/dist/{chunk-LJLZR44Y.js → chunk-EMS6PO2T.js} +5 -7
  6. package/dist/{chunk-EY63XBFZ.js → chunk-JBQUY2RI.js} +119 -12
  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-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 +2715 -1606
  15. package/dist/index.d.cts +6 -6
  16. package/dist/index.d.ts +6 -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-DJR6z3e0.d.cts} +3 -3
  21. package/dist/{plugin-DP6A4t4o.d.ts → plugin-N_EaXN4-.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 +2709 -1600
  37. package/dist/vela.global.min.js +315 -66
  38. package/dist/widget.cjs +2275 -1032
  39. package/dist/widget.d.cts +25 -7
  40. package/dist/widget.d.ts +25 -7
  41. package/dist/widget.js +27 -9
  42. package/dist/workspace.cjs +2181 -926
  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
@@ -805,12 +805,6 @@ function mix(base, top, alpha) {
805
805
  const c = (x, y) => Math.round(x + (y - x) * alpha);
806
806
  return `rgb(${c(b.r, t.r)},${c(b.g, t.g)},${c(b.b, t.b)})`;
807
807
  }
808
- function toHex6(color, fallback = "#888888") {
809
- const c = parseRgb(color);
810
- if (!c) return fallback;
811
- const h = (n) => Math.max(0, Math.min(255, Math.round(n))).toString(16).padStart(2, "0");
812
- return `#${h(c.r)}${h(c.g)}${h(c.b)}`;
813
- }
814
808
  function isDarkColor(color) {
815
809
  const c = parseRgb(color);
816
810
  if (!c) return true;
@@ -840,6 +834,9 @@ var STATIC_TOKENS = {
840
834
  "--vela-z-tooltip": "60",
841
835
  "--vela-z-menu": "50",
842
836
  "--vela-z-dialog": "40",
837
+ // Form popovers portal to <body> (or a chart host) and must sit above a dialog
838
+ // whose own stacking context may be nested inside the chart container.
839
+ "--vela-z-popover": "6000",
843
840
  "--vela-ease": "cubic-bezier(0.22, 1, 0.36, 1)",
844
841
  "--vela-dur-fast": "90ms",
845
842
  "--vela-dur-med": "160ms",
@@ -987,6 +984,10 @@ registerIcon(
987
984
  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"/>')
988
985
  );
989
986
  registerIcon("clock", S('<circle cx="8" cy="8" r="6.2"/><path d="M8 4.8V8l2.4 1.6"/>'));
987
+ registerIcon(
988
+ "calendar",
989
+ 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"/>')
990
+ );
990
991
  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"/>'));
991
992
  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"/>'));
992
993
  registerIcon(
@@ -1341,7 +1342,10 @@ function menuController(opts) {
1341
1342
  props: {
1342
1343
  id: opts.id ?? nextUid("vela-menu"),
1343
1344
  ids: opts.triggerId ? { trigger: opts.triggerId } : void 0,
1344
- positioning: { placement: opts.placement ?? "bottom-start" },
1345
+ positioning: {
1346
+ placement: opts.placement ?? "bottom-start",
1347
+ ...opts.getAnchorRect ? { getAnchorRect: () => opts.getAnchorRect() } : {}
1348
+ },
1345
1349
  onSelect: (d) => opts.onSelect?.(d.value),
1346
1350
  onOpenChange: (d) => opts.onOpenChange?.(d.open)
1347
1351
  },
@@ -1423,6 +1427,22 @@ var MENU_CSS = `
1423
1427
  }
1424
1428
  .vela-menu-switch.on { background: var(--vela-accent-bright, var(--vela-accent)); border-color: var(--vela-accent-bright, var(--vela-accent)); }
1425
1429
  .vela-menu-switch.on::after { transform: translateX(16px); background: var(--vela-bg); }
1430
+ /* Favorite star (rows carrying \`favorite\`): reserved space always \u2014 it fades in on row
1431
+ hover instead of shifting the layout \u2014 and a starred row keeps its filled star visible.
1432
+ Same control language as the drawing toolbar's flyout stars. */
1433
+ .vela-menu-item .vela-menu-star {
1434
+ order: 98;
1435
+ /* Pad the 16px icon slot into a bigger hit target without moving the row's layout. */
1436
+ margin: -3px -5px -3px 0;
1437
+ padding: 3px 5px;
1438
+ box-sizing: content-box;
1439
+ border-radius: var(--vela-radius-sm);
1440
+ opacity: 0;
1441
+ transition: opacity 0.1s ease, background 0.1s ease;
1442
+ }
1443
+ .vela-menu-item[data-highlighted] .vela-menu-star { opacity: 0.55; }
1444
+ .vela-menu-item .vela-menu-star:hover { opacity: 1; background: var(--vela-hover-strong); }
1445
+ .vela-menu-item .vela-menu-star.vela-fav { opacity: 0.95; color: var(--vela-highlight); }
1426
1446
  .vela-menu-sep { height: 1px; margin: 4px 6px; background: var(--vela-border); }
1427
1447
  /* Submenu trigger row: a right-aligned chevron, and it stays highlighted while its own
1428
1448
  list is open so the path you came down remains readable. */
@@ -1441,9 +1461,13 @@ var Surface = class _Surface {
1441
1461
  this.items = [];
1442
1462
  /** Branch item id → the surface it opens. */
1443
1463
  this.subs = /* @__PURE__ */ new Map();
1464
+ /** Trigger rect captured when this level opened — the list stays put if the
1465
+ * trigger then moves (favorite chips shifting the caret). */
1466
+ this.pinnedAnchor = null;
1444
1467
  this.doc = doc;
1445
1468
  this.host = opts.host;
1446
1469
  this.onSelect = opts.onSelect;
1470
+ this.onFavorite = opts.onFavorite;
1447
1471
  this.positioner = doc.createElement("div");
1448
1472
  this.positioner.className = "vela-ui-layer";
1449
1473
  this.list = doc.createElement("ul");
@@ -1451,16 +1475,25 @@ var Surface = class _Surface {
1451
1475
  if (opts.minWidth) this.list.style.minWidth = opts.minWidth;
1452
1476
  this.positioner.appendChild(this.list);
1453
1477
  this.host.appendChild(this.positioner);
1478
+ const trigger = opts.trigger;
1454
1479
  this.ctrl = menuController({
1455
1480
  items: [],
1456
1481
  id: opts.id,
1457
1482
  placement: opts.placement,
1458
1483
  onSelect: (id) => this.onSelect(id),
1459
- onOpenChange: opts.onOpenChange,
1484
+ getAnchorRect: () => this.pinnedAnchor,
1485
+ onOpenChange: (open2) => {
1486
+ if (open2 && trigger) {
1487
+ const r = trigger.getBoundingClientRect();
1488
+ this.pinnedAnchor = { x: r.x, y: r.y, width: r.width, height: r.height };
1489
+ } else {
1490
+ this.pinnedAnchor = null;
1491
+ }
1492
+ opts.onOpenChange?.(open2);
1493
+ },
1460
1494
  triggerId: opts.triggerId
1461
1495
  });
1462
1496
  this.mid = String(this.ctrl.props.id);
1463
- const trigger = opts.trigger;
1464
1497
  if (opts.triggerId && trigger) trigger.id = opts.triggerId;
1465
1498
  this.handle = runMachine(this.ctrl.machine, this.ctrl.props, (service) => {
1466
1499
  const api = this.ctrl.connect(service);
@@ -1540,6 +1573,25 @@ var Surface = class _Surface {
1540
1573
  hint.textContent = item.hint;
1541
1574
  li.appendChild(hint);
1542
1575
  }
1576
+ if (item.favorite !== void 0 && this.onFavorite) {
1577
+ const on = item.favorite;
1578
+ const star = iconEl(on ? "star-filled" : "star", doc);
1579
+ star.classList.add("vela-menu-star");
1580
+ if (on) star.classList.add("vela-fav");
1581
+ star.removeAttribute("aria-hidden");
1582
+ star.setAttribute("role", "button");
1583
+ star.setAttribute("aria-label", on ? "Remove from favorites" : "Add to favorites");
1584
+ star.setAttribute("aria-pressed", on ? "true" : "false");
1585
+ for (const ev of ["pointerdown", "pointerup", "mousedown", "mouseup"]) {
1586
+ star.addEventListener(ev, (e) => e.stopPropagation());
1587
+ }
1588
+ star.addEventListener("click", (e) => {
1589
+ e.stopPropagation();
1590
+ e.preventDefault();
1591
+ this.onFavorite?.(item.id, !on);
1592
+ });
1593
+ li.appendChild(star);
1594
+ }
1543
1595
  if (item.toggle) {
1544
1596
  const sw = doc.createElement("span");
1545
1597
  sw.className = "vela-menu-switch" + (item.checked ? " on" : "");
@@ -1555,6 +1607,7 @@ var Surface = class _Surface {
1555
1607
  host: this.host,
1556
1608
  placement: "right-start",
1557
1609
  onSelect: this.onSelect,
1610
+ onFavorite: this.onFavorite,
1558
1611
  id: `${this.mid}--${item.id}`
1559
1612
  });
1560
1613
  sub.setItems(item.submenu ?? []);
@@ -1578,7 +1631,8 @@ var Menu = class {
1578
1631
  trigger: opts.trigger,
1579
1632
  triggerId: opts.triggerId,
1580
1633
  id: opts.id,
1581
- minWidth: opts.minWidth
1634
+ minWidth: opts.minWidth,
1635
+ onFavorite: opts.onFavorite
1582
1636
  });
1583
1637
  this.root.setItems(opts.items);
1584
1638
  }
@@ -2081,6 +2135,1197 @@ var Drawer = class {
2081
2135
  }
2082
2136
  };
2083
2137
 
2138
+ // src/ui/components/popover/controller.ts
2139
+ function insetRect(r, inset) {
2140
+ return {
2141
+ left: r.left + inset,
2142
+ top: r.top + inset,
2143
+ right: r.right - inset,
2144
+ bottom: r.bottom - inset,
2145
+ width: r.width - inset * 2,
2146
+ height: r.height - inset * 2
2147
+ };
2148
+ }
2149
+ function viewportRect(width, height, inset) {
2150
+ return {
2151
+ left: inset,
2152
+ top: inset,
2153
+ right: width - inset,
2154
+ bottom: height - inset,
2155
+ width: width - inset * 2,
2156
+ height: height - inset * 2
2157
+ };
2158
+ }
2159
+ function intersectRects(a, b) {
2160
+ const left = Math.max(a.left, b.left);
2161
+ const top = Math.max(a.top, b.top);
2162
+ const right = Math.min(a.right, b.right);
2163
+ const bottom = Math.min(a.bottom, b.bottom);
2164
+ return { left, top, right, bottom, width: right - left, height: bottom - top };
2165
+ }
2166
+ function placePopover(a) {
2167
+ let left = a.align === "end" ? a.trigger.right - a.pop.width : a.trigger.left;
2168
+ const below = a.trigger.bottom + a.gap;
2169
+ const above = a.trigger.top - a.pop.height - a.gap;
2170
+ const fitsBelow = below + a.pop.height <= a.clamp.bottom;
2171
+ const fitsAbove = above >= a.clamp.top;
2172
+ let top = below;
2173
+ if (!fitsBelow && (fitsAbove || a.trigger.top - a.clamp.top > a.clamp.bottom - a.trigger.bottom)) {
2174
+ top = Math.max(a.clamp.top, above);
2175
+ }
2176
+ if (left + a.pop.width > a.clamp.right) left = a.clamp.right - a.pop.width;
2177
+ if (left < a.clamp.left) left = a.clamp.left;
2178
+ if (top + a.pop.height > a.clamp.bottom) top = a.clamp.bottom - a.pop.height;
2179
+ if (top < a.clamp.top) top = a.clamp.top;
2180
+ return {
2181
+ left: Math.round(left - a.originX),
2182
+ top: Math.round(top - a.originY)
2183
+ };
2184
+ }
2185
+ function popoverController(opts = {}) {
2186
+ return {
2187
+ gap: opts.gap ?? 4,
2188
+ align: opts.align ?? "start",
2189
+ matchWidth: opts.matchWidth ?? false,
2190
+ position: opts.position ?? "fixed",
2191
+ boundaryInset: opts.boundaryInset ?? 0,
2192
+ viewportInset: opts.viewportInset ?? 6,
2193
+ onClose: opts.onClose
2194
+ };
2195
+ }
2196
+
2197
+ // src/ui/components/popover/styles.ts
2198
+ var POPOVER_STYLE_ID = "vela-ui-popover";
2199
+ var POPOVER_CSS = `
2200
+ .vela-popover {
2201
+ position: fixed;
2202
+ z-index: var(--vela-z-popover);
2203
+ box-sizing: border-box;
2204
+ /* Never serve as a scroll anchor: the shell is portaled and re-placed between
2205
+ gestures, and anchoring against it jumps the scroller underneath. */
2206
+ overflow-anchor: none;
2207
+ }
2208
+ .vela-popover[data-position='absolute'] { position: absolute; }
2209
+ .vela-popover[hidden] { display: none !important; }
2210
+ `;
2211
+
2212
+ // src/ui/components/popover/view.ts
2213
+ var open = null;
2214
+ function closeOpenPopovers() {
2215
+ open?.hide();
2216
+ }
2217
+ function isPopoverOpen() {
2218
+ return open !== null;
2219
+ }
2220
+ function openPopoverTrigger() {
2221
+ return open?.trigger ?? null;
2222
+ }
2223
+ function toRect(r) {
2224
+ return { left: r.left, top: r.top, right: r.right, bottom: r.bottom, width: r.width, height: r.height };
2225
+ }
2226
+ var Popover = class {
2227
+ constructor(opts) {
2228
+ this.onOutside = null;
2229
+ this.onKey = null;
2230
+ this.onReflow = null;
2231
+ this.shown = false;
2232
+ const doc = opts.trigger.ownerDocument;
2233
+ injectStyles(POPOVER_STYLE_ID, POPOVER_CSS, doc);
2234
+ this.trigger = opts.trigger;
2235
+ this.host = opts.host ?? doc.body;
2236
+ this.ctrl = popoverController(opts);
2237
+ this.boundary = opts.boundary ?? "viewport";
2238
+ this.theme = opts.theme;
2239
+ this.el = doc.createElement("div");
2240
+ this.el.className = "vela-popover vela-ui-layer" + (opts.className ? ` ${opts.className}` : "");
2241
+ this.el.dataset.position = this.ctrl.position;
2242
+ if (opts.zIndex !== void 0) this.el.style.zIndex = String(opts.zIndex);
2243
+ this.el.addEventListener("pointerdown", (e) => e.stopPropagation());
2244
+ if (opts.content instanceof Node) this.el.appendChild(opts.content);
2245
+ else if (typeof opts.content === "function") opts.content(this.el);
2246
+ }
2247
+ get open() {
2248
+ return this.shown;
2249
+ }
2250
+ get position() {
2251
+ return this.ctrl.position;
2252
+ }
2253
+ get align() {
2254
+ return this.ctrl.align;
2255
+ }
2256
+ show() {
2257
+ if (this.shown) {
2258
+ this.place();
2259
+ return;
2260
+ }
2261
+ if (open && open !== this) open.hide();
2262
+ ensureUIHost(this.el, this.theme);
2263
+ this.host.appendChild(this.el);
2264
+ this.shown = true;
2265
+ open = this;
2266
+ this.place();
2267
+ const onOutside = (ev) => {
2268
+ const t = ev.target;
2269
+ if (this.el.contains(t) || this.trigger.contains(t)) return;
2270
+ this.hide();
2271
+ };
2272
+ const onKey = (ev) => {
2273
+ if (ev.key !== "Escape") return;
2274
+ ev.preventDefault();
2275
+ ev.stopPropagation();
2276
+ this.hide();
2277
+ };
2278
+ const onReflow = () => this.place();
2279
+ setTimeout(() => document.addEventListener("pointerdown", onOutside, true), 0);
2280
+ document.addEventListener("keydown", onKey, true);
2281
+ window.addEventListener("resize", onReflow, true);
2282
+ document.addEventListener("scroll", onReflow, true);
2283
+ this.onOutside = onOutside;
2284
+ this.onKey = onKey;
2285
+ this.onReflow = onReflow;
2286
+ }
2287
+ hide() {
2288
+ if (!this.shown) return;
2289
+ if (this.onOutside) document.removeEventListener("pointerdown", this.onOutside, true);
2290
+ if (this.onKey) document.removeEventListener("keydown", this.onKey, true);
2291
+ if (this.onReflow) {
2292
+ window.removeEventListener("resize", this.onReflow, true);
2293
+ document.removeEventListener("scroll", this.onReflow, true);
2294
+ }
2295
+ this.onOutside = null;
2296
+ this.onKey = null;
2297
+ this.onReflow = null;
2298
+ this.el.remove();
2299
+ this.shown = false;
2300
+ if (open === this) open = null;
2301
+ this.ctrl.onClose?.();
2302
+ }
2303
+ /** Show if closed, hide if this instance is the open popover. */
2304
+ toggle() {
2305
+ if (this.shown) this.hide();
2306
+ else this.show();
2307
+ }
2308
+ destroy() {
2309
+ this.hide();
2310
+ }
2311
+ reposition() {
2312
+ if (this.shown) this.place();
2313
+ }
2314
+ clampRect() {
2315
+ const view = viewportRect(window.innerWidth, window.innerHeight, this.ctrl.viewportInset);
2316
+ const bound = this.readBoundary();
2317
+ if (!bound) return view;
2318
+ const inset = insetRect(bound, this.ctrl.boundaryInset);
2319
+ return intersectRects(view, inset);
2320
+ }
2321
+ readBoundary() {
2322
+ const b = this.boundary;
2323
+ if (b === "viewport") return null;
2324
+ const raw = typeof b === "function" ? b() : b.getBoundingClientRect();
2325
+ return raw ? toRect(raw) : null;
2326
+ }
2327
+ place() {
2328
+ const ar = this.trigger.getBoundingClientRect();
2329
+ if (this.ctrl.matchWidth) {
2330
+ this.el.style.minWidth = `${Math.round(Math.max(this.el.offsetWidth, ar.width))}px`;
2331
+ }
2332
+ const origin = this.ctrl.position === "absolute" ? this.host.getBoundingClientRect() : { left: 0, top: 0 };
2333
+ const pos = placePopover({
2334
+ trigger: toRect(ar),
2335
+ pop: { width: this.el.offsetWidth, height: this.el.offsetHeight },
2336
+ gap: this.ctrl.gap,
2337
+ align: this.ctrl.align,
2338
+ clamp: this.clampRect(),
2339
+ originX: origin.left,
2340
+ originY: origin.top
2341
+ });
2342
+ this.el.style.left = `${pos.left}px`;
2343
+ this.el.style.top = `${pos.top}px`;
2344
+ }
2345
+ };
2346
+
2347
+ // src/ui/components/switch/controller.ts
2348
+ function switchController(opts = {}) {
2349
+ let checked = opts.checked ?? false;
2350
+ const disabled = opts.disabled ?? false;
2351
+ return {
2352
+ get checked() {
2353
+ return checked;
2354
+ },
2355
+ disabled,
2356
+ size: opts.size ?? "md",
2357
+ tone: opts.tone ?? "bright",
2358
+ setChecked(v) {
2359
+ checked = v;
2360
+ },
2361
+ toggle() {
2362
+ if (disabled) return checked;
2363
+ checked = !checked;
2364
+ opts.onChange?.(checked);
2365
+ return checked;
2366
+ }
2367
+ };
2368
+ }
2369
+
2370
+ // src/ui/components/switch/styles.ts
2371
+ var SWITCH_STYLE_ID = "vela-ui-switch";
2372
+ var SWITCH_CSS = `
2373
+ .vela-switch {
2374
+ width: 20px;
2375
+ height: 20px;
2376
+ flex: 0 0 auto;
2377
+ display: inline-flex;
2378
+ align-items: center;
2379
+ justify-content: center;
2380
+ padding: 0;
2381
+ border: 1px solid var(--vela-border);
2382
+ border-radius: 5px;
2383
+ background: transparent;
2384
+ color: transparent;
2385
+ cursor: pointer;
2386
+ line-height: 0;
2387
+ transition: background 0.12s ease, border-color 0.12s ease, color 0.12s ease;
2388
+ }
2389
+ .vela-switch .vela-icon, .vela-switch svg { display: block; width: 12px; height: 12px; }
2390
+ .vela-switch[data-size='sm'] .vela-icon, .vela-switch[data-size='sm'] svg { width: 11px; height: 11px; }
2391
+ .vela-switch:hover { border-color: var(--vela-fg-muted); }
2392
+ .vela-switch[data-checked] {
2393
+ background: var(--vela-fg-bright);
2394
+ border-color: var(--vela-fg-bright);
2395
+ color: var(--vela-bg);
2396
+ }
2397
+ .vela-switch[data-size='sm'] { width: 18px; height: 18px; border-color: var(--vela-border-strong); }
2398
+ .vela-switch[data-size='sm']:hover { border-color: var(--vela-fg-muted); }
2399
+ .vela-switch[data-size='sm'][data-checked] {
2400
+ background: var(--vela-selected-bg);
2401
+ border-color: var(--vela-selected-bg);
2402
+ color: var(--vela-selected-fg);
2403
+ }
2404
+ .vela-switch[data-tone='selected'][data-checked] {
2405
+ background: var(--vela-selected-bg);
2406
+ border-color: var(--vela-selected-bg);
2407
+ color: var(--vela-selected-fg);
2408
+ }
2409
+ .vela-switch[disabled] { opacity: 0.4; cursor: default; }
2410
+ `;
2411
+
2412
+ // src/ui/components/switch/view.ts
2413
+ var Switch = class {
2414
+ constructor(opts = {}) {
2415
+ injectStyles(SWITCH_STYLE_ID, SWITCH_CSS, document);
2416
+ this.ctrl = switchController(opts);
2417
+ const b = document.createElement("button");
2418
+ b.type = "button";
2419
+ if (opts.id) b.id = opts.id;
2420
+ b.className = "vela-switch";
2421
+ b.dataset.size = this.ctrl.size;
2422
+ b.dataset.tone = this.ctrl.tone;
2423
+ b.setAttribute("role", "switch");
2424
+ if (this.ctrl.disabled) b.disabled = true;
2425
+ b.appendChild(iconEl("check", b.ownerDocument));
2426
+ this.el = b;
2427
+ this.paint();
2428
+ b.addEventListener("pointerdown", (e) => {
2429
+ e.preventDefault();
2430
+ b.focus({ preventScroll: true });
2431
+ });
2432
+ b.addEventListener("click", () => {
2433
+ this.ctrl.toggle();
2434
+ this.paint();
2435
+ });
2436
+ }
2437
+ get checked() {
2438
+ return this.ctrl.checked;
2439
+ }
2440
+ setChecked(v) {
2441
+ this.ctrl.setChecked(v);
2442
+ this.paint();
2443
+ }
2444
+ paint() {
2445
+ const on = this.ctrl.checked;
2446
+ this.el.setAttribute("aria-checked", on ? "true" : "false");
2447
+ if (on) this.el.dataset.checked = "";
2448
+ else delete this.el.dataset.checked;
2449
+ }
2450
+ };
2451
+
2452
+ // src/ui/components/select/controller.ts
2453
+ function selectController(opts) {
2454
+ const options = opts.options;
2455
+ let value = opts.value ?? options[0]?.value ?? "";
2456
+ const size = opts.size ?? "md";
2457
+ return {
2458
+ options,
2459
+ get value() {
2460
+ return value;
2461
+ },
2462
+ size,
2463
+ fill: opts.fill ?? size !== "sm",
2464
+ disabled: opts.disabled ?? false,
2465
+ setValue(v) {
2466
+ value = v;
2467
+ },
2468
+ labelOf(v) {
2469
+ return options.find((o) => o.value === v)?.label ?? v;
2470
+ },
2471
+ pick(v) {
2472
+ value = v;
2473
+ const label = options.find((o) => o.value === v)?.label ?? v;
2474
+ opts.onChange?.(v, label);
2475
+ return { value: v, label };
2476
+ }
2477
+ };
2478
+ }
2479
+
2480
+ // src/ui/components/select/styles.ts
2481
+ var SELECT_STYLE_ID = "vela-ui-select";
2482
+ var SELECT_CSS = `
2483
+ .vela-select { position: relative; display: inline-block; min-width: 0; }
2484
+ .vela-select[data-fill] { width: 100%; }
2485
+ .vela-select-trigger {
2486
+ display: flex;
2487
+ align-items: center;
2488
+ width: 100%;
2489
+ box-sizing: border-box;
2490
+ height: 34px;
2491
+ padding: 0 26px 0 8px;
2492
+ background: transparent;
2493
+ border: 1px solid var(--vela-border-strong);
2494
+ border-radius: 6px;
2495
+ color: var(--vela-fg-bright);
2496
+ font-size: 14px;
2497
+ font-family: inherit;
2498
+ cursor: pointer;
2499
+ text-align: left;
2500
+ outline: none;
2501
+ transition: border-color 0.12s ease, box-shadow 0.12s ease;
2502
+ }
2503
+ .vela-select-trigger:hover { border-color: var(--vela-fg-muted); }
2504
+ .vela-select-trigger:focus { border-color: var(--vela-focus); box-shadow: 0 0 0 3px var(--vela-focus-soft); }
2505
+ /* As a grid item (chart-settings rows dissolve via display:contents into a shared
2506
+ max-content column) the wrap must NOT stretch to the column: it sizes to its own
2507
+ widest option, like the native select it replaces. */
2508
+ .vela-select[data-size='sm'] { max-width: 200px; flex: 0 0 auto; justify-self: start; }
2509
+ .vela-select[data-size='sm'] .vela-select-trigger {
2510
+ height: 28px;
2511
+ background: var(--vela-surface-elev);
2512
+ border-radius: var(--vela-radius-sm);
2513
+ color: var(--vela-fg);
2514
+ font-size: 13px;
2515
+ box-shadow: none;
2516
+ }
2517
+ .vela-select[data-size='sm'] .vela-select-trigger:focus { box-shadow: none; border-color: var(--vela-fg-muted); }
2518
+ .vela-select-label { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
2519
+ /* Invisible option-label stack: gives the shrink-to-fit wrap the WIDEST option's
2520
+ intrinsic width (like a native select), so picking a shorter/longer label never
2521
+ resizes the trigger and shifts the row around it. */
2522
+ .vela-select-sizer { visibility: hidden; height: 0; overflow: hidden; font-size: 14px; }
2523
+ .vela-select[data-size='sm'] .vela-select-sizer { font-size: 13px; }
2524
+ .vela-select-sizer span { display: block; height: 0; white-space: nowrap; padding: 0 26px 0 8px; border-inline: 1px solid transparent; }
2525
+ .vela-select-chevron {
2526
+ position: absolute;
2527
+ right: 8px;
2528
+ top: 50%;
2529
+ transform: translateY(-50%);
2530
+ pointer-events: none;
2531
+ display: flex;
2532
+ opacity: 0.55;
2533
+ line-height: 0;
2534
+ color: inherit;
2535
+ }
2536
+ .vela-select-chevron .vela-icon, .vela-select-chevron svg { width: 12px; height: 12px; display: block; }
2537
+ .vela-select-list {
2538
+ background: var(--vela-bg);
2539
+ color: var(--vela-fg);
2540
+ border: none;
2541
+ border-radius: 6px;
2542
+ box-shadow: var(--vela-shadow);
2543
+ font: 14px var(--vela-font);
2544
+ padding: 4px;
2545
+ overflow: hidden;
2546
+ }
2547
+ .vela-select-list[data-size='sm'] { font-size: 13px; background: var(--vela-surface-overlay); }
2548
+ .vela-select-items { max-height: none; overflow: hidden; }
2549
+ .vela-select-list.is-scroll { display: flex; align-items: stretch; gap: 2px; }
2550
+ .vela-select-list.is-scroll .vela-select-items {
2551
+ flex: 1 1 auto;
2552
+ min-width: 0;
2553
+ max-height: 240px;
2554
+ overflow-y: auto;
2555
+ scrollbar-width: none;
2556
+ }
2557
+ .vela-select-list.is-scroll .vela-select-items::-webkit-scrollbar { display: none; width: 0; height: 0; }
2558
+ .vela-select-rail { position: relative; flex: none; width: 3px; margin: 2px 1px 2px 0; pointer-events: none; }
2559
+ .vela-select-thumb { width: 3px; border-radius: 2px; background: var(--vela-scroll); }
2560
+ .vela-select-item {
2561
+ display: block;
2562
+ width: 100%;
2563
+ text-align: left;
2564
+ padding: 7px 10px;
2565
+ border: none;
2566
+ border-radius: 4px;
2567
+ background: transparent;
2568
+ color: inherit;
2569
+ cursor: pointer;
2570
+ font: inherit;
2571
+ font-weight: 400;
2572
+ }
2573
+ .vela-select-item:hover { background: var(--vela-hover); }
2574
+ .vela-select-item[data-checked] { background: var(--vela-hover-strong); color: var(--vela-fg-bright); }
2575
+ .vela-select-item[data-checked]:hover { background: var(--vela-hover-strong); }
2576
+ `;
2577
+
2578
+ // src/ui/components/select/view.ts
2579
+ function ensureSelectStyles(doc) {
2580
+ injectStyles(SELECT_STYLE_ID, SELECT_CSS, doc);
2581
+ }
2582
+ function syncThumb(list, thumb) {
2583
+ const view = list.clientHeight;
2584
+ const total = list.scrollHeight;
2585
+ if (total <= view) {
2586
+ thumb.style.height = "0";
2587
+ return;
2588
+ }
2589
+ const thumbH = Math.max(20, view / total * view);
2590
+ const top = list.scrollTop / (total - view) * (view - thumbH);
2591
+ thumb.style.height = `${Math.round(thumbH)}px`;
2592
+ thumb.style.transform = `translateY(${Math.round(top)}px`;
2593
+ }
2594
+ function fillSelectList(menu2, options, current, onPick) {
2595
+ menu2.replaceChildren();
2596
+ const list = menu2.ownerDocument.createElement("div");
2597
+ list.className = "vela-select-items";
2598
+ for (const p of options) {
2599
+ const item = menu2.ownerDocument.createElement("button");
2600
+ item.type = "button";
2601
+ item.className = "vela-select-item";
2602
+ item.textContent = p.label;
2603
+ if (p.value === current) item.dataset.checked = "1";
2604
+ item.addEventListener("click", (e) => {
2605
+ e.stopPropagation();
2606
+ onPick(p.value, p.label);
2607
+ });
2608
+ list.appendChild(item);
2609
+ }
2610
+ menu2.appendChild(list);
2611
+ }
2612
+ function decorateSelectScroll(menu2) {
2613
+ const list = menu2.querySelector(".vela-select-items");
2614
+ if (!list) return;
2615
+ if (list.scrollHeight > 240) {
2616
+ menu2.classList.add("is-scroll");
2617
+ const rail = menu2.ownerDocument.createElement("div");
2618
+ rail.className = "vela-select-rail";
2619
+ const thumb = menu2.ownerDocument.createElement("div");
2620
+ thumb.className = "vela-select-thumb";
2621
+ rail.appendChild(thumb);
2622
+ menu2.appendChild(rail);
2623
+ list.addEventListener("scroll", () => syncThumb(list, thumb));
2624
+ syncThumb(list, thumb);
2625
+ }
2626
+ menu2.querySelector(".vela-select-item[data-checked]")?.scrollIntoView({ block: "nearest" });
2627
+ list.dispatchEvent(new Event("scroll"));
2628
+ }
2629
+ function toggleSelectList(trigger, options, current, onPick, opts = {}) {
2630
+ if (openPopoverTrigger() === trigger) {
2631
+ closeOpenPopovers();
2632
+ return null;
2633
+ }
2634
+ return openSelectList(trigger, options, current, onPick, opts);
2635
+ }
2636
+ function openSelectList(trigger, options, current, onPick, opts = {}) {
2637
+ ensureSelectStyles(trigger.ownerDocument);
2638
+ closeOpenPopovers();
2639
+ const pop = new Popover({
2640
+ trigger,
2641
+ theme: opts.theme,
2642
+ matchWidth: opts.matchWidth ?? true,
2643
+ boundary: opts.boundary,
2644
+ boundaryInset: opts.boundaryInset,
2645
+ gap: opts.gap ?? 4,
2646
+ align: opts.align ?? "start",
2647
+ host: opts.host,
2648
+ position: opts.position,
2649
+ zIndex: opts.zIndex,
2650
+ className: "vela-select-list",
2651
+ onClose: opts.onClose,
2652
+ content: (el) => {
2653
+ if (opts.size) el.dataset.size = opts.size;
2654
+ fillSelectList(el, options, current, (value, label) => {
2655
+ pop.hide();
2656
+ onPick(value, label);
2657
+ });
2658
+ }
2659
+ });
2660
+ pop.show();
2661
+ decorateSelectScroll(pop.el);
2662
+ pop.reposition();
2663
+ return pop;
2664
+ }
2665
+ var Select = class {
2666
+ constructor(opts) {
2667
+ this.list = null;
2668
+ const doc = opts.list?.host?.ownerDocument ?? document;
2669
+ ensureSelectStyles(doc);
2670
+ this.ctrl = selectController(opts);
2671
+ this.listOpts = { ...opts.list, theme: opts.theme ?? opts.list?.theme, size: this.ctrl.size };
2672
+ const wrap = doc.createElement("div");
2673
+ wrap.className = "vela-select";
2674
+ wrap.dataset.size = this.ctrl.size;
2675
+ if (this.ctrl.fill) wrap.dataset.fill = "";
2676
+ const btn = doc.createElement("button");
2677
+ btn.type = "button";
2678
+ if (opts.id) btn.id = opts.id;
2679
+ btn.className = "vela-select-trigger";
2680
+ if (this.ctrl.disabled) btn.disabled = true;
2681
+ const label = doc.createElement("span");
2682
+ label.className = "vela-select-label";
2683
+ label.textContent = this.ctrl.labelOf(this.ctrl.value);
2684
+ const chevron = doc.createElement("span");
2685
+ chevron.className = "vela-select-chevron";
2686
+ chevron.appendChild(iconEl("chevron-down", doc));
2687
+ btn.append(label, chevron);
2688
+ wrap.appendChild(btn);
2689
+ const sizer = doc.createElement("div");
2690
+ sizer.className = "vela-select-sizer";
2691
+ sizer.setAttribute("aria-hidden", "true");
2692
+ for (const o of this.ctrl.options) {
2693
+ const s = doc.createElement("span");
2694
+ s.textContent = o.label;
2695
+ sizer.appendChild(s);
2696
+ }
2697
+ wrap.appendChild(sizer);
2698
+ btn.addEventListener("pointerdown", (e) => {
2699
+ e.preventDefault();
2700
+ btn.focus({ preventScroll: true });
2701
+ });
2702
+ btn.addEventListener("click", (e) => {
2703
+ e.stopPropagation();
2704
+ this.toggle();
2705
+ });
2706
+ this.el = wrap;
2707
+ this.trigger = btn;
2708
+ this.labelEl = label;
2709
+ }
2710
+ get value() {
2711
+ return this.ctrl.value;
2712
+ }
2713
+ setValue(v) {
2714
+ this.ctrl.setValue(v);
2715
+ this.labelEl.textContent = this.ctrl.labelOf(v);
2716
+ }
2717
+ toggle() {
2718
+ this.list = toggleSelectList(
2719
+ this.trigger,
2720
+ this.ctrl.options,
2721
+ this.ctrl.value,
2722
+ (value, label) => {
2723
+ this.ctrl.pick(value);
2724
+ this.labelEl.textContent = label;
2725
+ this.list = null;
2726
+ },
2727
+ { ...this.listOpts, onClose: () => {
2728
+ this.list = null;
2729
+ this.listOpts.onClose?.();
2730
+ } }
2731
+ );
2732
+ }
2733
+ destroy() {
2734
+ this.list?.hide();
2735
+ this.list = null;
2736
+ }
2737
+ };
2738
+
2739
+ // src/ui/components/number-input/controller.ts
2740
+ function clampNumber(n, opts) {
2741
+ let v = n;
2742
+ if (opts.min !== void 0) v = Math.max(opts.min, v);
2743
+ if (opts.max !== void 0) v = Math.min(opts.max, v);
2744
+ if (opts.integer) v = Math.round(v);
2745
+ return v;
2746
+ }
2747
+ function decimalsOf(n) {
2748
+ const s = String(n);
2749
+ const e = s.search(/[eE]/);
2750
+ if (e >= 0) {
2751
+ const frac = s.slice(0, e).split(".")[1]?.length ?? 0;
2752
+ return Math.max(0, Math.min(20, frac - Number(s.slice(e + 1))));
2753
+ }
2754
+ return s.split(".")[1]?.length ?? 0;
2755
+ }
2756
+ function snapToStep(n, base, step) {
2757
+ const d = Math.min(20, Math.max(decimalsOf(base), decimalsOf(step)));
2758
+ return Number(n.toFixed(d));
2759
+ }
2760
+ function numberInputController(opts) {
2761
+ let value = opts.value;
2762
+ const integer = opts.integer ?? false;
2763
+ const commit = opts.commit ?? "blur";
2764
+ const clamp = opts.clamp ?? commit === "blur";
2765
+ const step = opts.step ?? (integer ? 1 : 0.1);
2766
+ const onChange = opts.onChange;
2767
+ const normalize = (raw) => clamp ? clampNumber(raw, { min: opts.min, max: opts.max, integer }) : raw;
2768
+ const apply = (raw) => {
2769
+ if (!Number.isFinite(raw)) return null;
2770
+ const n = normalize(raw);
2771
+ if (n !== value) {
2772
+ value = n;
2773
+ onChange?.(n);
2774
+ }
2775
+ return n;
2776
+ };
2777
+ return {
2778
+ get value() {
2779
+ return value;
2780
+ },
2781
+ min: opts.min,
2782
+ max: opts.max,
2783
+ step,
2784
+ integer,
2785
+ size: opts.size ?? "md",
2786
+ commit,
2787
+ steppers: opts.steppers ?? commit === "blur",
2788
+ clamp,
2789
+ disabled: opts.disabled ?? false,
2790
+ apply,
2791
+ sync(raw) {
2792
+ const n = Number.isFinite(raw) ? normalize(raw) : value;
2793
+ value = n;
2794
+ return n;
2795
+ },
2796
+ nudge(dir) {
2797
+ return apply(snapToStep(value + dir * step, value, step));
2798
+ }
2799
+ };
2800
+ }
2801
+
2802
+ // src/ui/components/number-input/styles.ts
2803
+ var NUMBER_STYLE_ID = "vela-ui-number";
2804
+ var NUMBER_CSS = `
2805
+ .vela-num { position: relative; display: inline-block; min-width: 0; }
2806
+ .vela-num[data-fill] { width: 100%; }
2807
+ .vela-num input {
2808
+ width: 100%;
2809
+ box-sizing: border-box;
2810
+ height: 34px;
2811
+ background: transparent;
2812
+ border: 1px solid var(--vela-border-strong);
2813
+ border-radius: 6px;
2814
+ color: var(--vela-fg-bright);
2815
+ padding: 0 8px;
2816
+ font-size: 14px;
2817
+ font-family: inherit;
2818
+ outline: none;
2819
+ transition: border-color 0.12s ease, box-shadow 0.12s ease;
2820
+ -moz-appearance: textfield;
2821
+ appearance: textfield;
2822
+ }
2823
+ .vela-num input::-webkit-inner-spin-button, .vela-num input::-webkit-outer-spin-button { -webkit-appearance: none; margin: 0; }
2824
+ .vela-num input:hover { border-color: var(--vela-fg-muted); }
2825
+ .vela-num input:focus { border-color: var(--vela-focus); box-shadow: 0 0 0 3px var(--vela-focus-soft); }
2826
+ .vela-num[data-steppers] input { padding-right: 26px; }
2827
+ .vela-num[data-size='sm'] { width: 64px; flex: none; }
2828
+ .vela-num[data-size='sm'][data-compact] { width: 56px; }
2829
+ .vela-num[data-size='sm'] input {
2830
+ height: 28px;
2831
+ background: var(--vela-surface-elev);
2832
+ border-radius: var(--vela-radius-sm);
2833
+ color: var(--vela-fg);
2834
+ font-size: 13px;
2835
+ box-shadow: none;
2836
+ }
2837
+ .vela-num[data-size='sm'] input:focus { box-shadow: none; }
2838
+ .vela-num-step {
2839
+ position: absolute;
2840
+ right: 0;
2841
+ top: 0;
2842
+ bottom: 0;
2843
+ width: 22px;
2844
+ display: none;
2845
+ flex-direction: column;
2846
+ justify-content: center;
2847
+ box-sizing: border-box;
2848
+ padding: 2px 6px 2px 0;
2849
+ line-height: 0;
2850
+ }
2851
+ .vela-num[data-steppers]:hover .vela-num-step { display: flex; }
2852
+ .vela-num-step button {
2853
+ flex: 1;
2854
+ width: 100%;
2855
+ min-height: 0;
2856
+ border: none;
2857
+ background: transparent;
2858
+ color: var(--vela-fg-muted);
2859
+ border-radius: 3px;
2860
+ padding: 0;
2861
+ cursor: pointer;
2862
+ display: flex;
2863
+ align-items: center;
2864
+ justify-content: center;
2865
+ }
2866
+ .vela-num-step button:hover { background: var(--vela-hover); color: var(--vela-fg-bright); }
2867
+ .vela-num-step .vela-icon, .vela-num-step svg { width: 12px; height: 12px; display: block; }
2868
+ `;
2869
+
2870
+ // src/ui/components/number-input/view.ts
2871
+ var NumberInput = class {
2872
+ constructor(opts) {
2873
+ const doc = document;
2874
+ injectStyles(NUMBER_STYLE_ID, NUMBER_CSS, doc);
2875
+ this.ctrl = numberInputController(opts);
2876
+ this.placeholderMode = opts.placeholder !== void 0;
2877
+ this.emptyValue = opts.emptyValue ?? opts.value;
2878
+ this.last = String(opts.value);
2879
+ const wrap = doc.createElement("div");
2880
+ wrap.className = "vela-num";
2881
+ wrap.dataset.size = this.ctrl.size;
2882
+ if (this.ctrl.size !== "sm") wrap.dataset.fill = "";
2883
+ if (opts.compact) wrap.dataset.compact = "";
2884
+ if (this.ctrl.steppers) wrap.dataset.steppers = "";
2885
+ const ni = doc.createElement("input");
2886
+ ni.type = "number";
2887
+ if (opts.id) ni.id = opts.id;
2888
+ if (opts.title) ni.title = opts.title;
2889
+ if (opts.min !== void 0) ni.min = String(opts.min);
2890
+ if (opts.max !== void 0) ni.max = String(opts.max);
2891
+ ni.step = String(this.ctrl.step);
2892
+ if (this.ctrl.disabled) ni.disabled = true;
2893
+ if (this.placeholderMode) {
2894
+ ni.placeholder = opts.placeholder ?? "";
2895
+ ni.value = opts.value !== this.emptyValue ? String(opts.value) : "";
2896
+ } else {
2897
+ ni.value = String(opts.value);
2898
+ }
2899
+ const commitRaw = (raw) => {
2900
+ const n = this.ctrl.apply(raw);
2901
+ if (n === null) {
2902
+ ni.value = this.placeholderMode && this.ctrl.value === this.emptyValue ? "" : this.last;
2903
+ return;
2904
+ }
2905
+ this.last = String(n);
2906
+ if (this.placeholderMode && n === this.emptyValue) ni.value = "";
2907
+ else ni.value = String(n);
2908
+ };
2909
+ if (this.placeholderMode) {
2910
+ ni.addEventListener("change", () => {
2911
+ const raw = ni.value.trim() === "" ? this.emptyValue : Number(ni.value);
2912
+ commitRaw(Number.isFinite(raw) ? raw : this.emptyValue);
2913
+ });
2914
+ } else if (this.ctrl.commit === "live") {
2915
+ ni.addEventListener("input", () => {
2916
+ const v = Number(ni.value);
2917
+ if (Number.isFinite(v)) this.ctrl.apply(v);
2918
+ });
2919
+ } else {
2920
+ ni.addEventListener("blur", () => commitRaw(Number(ni.value)));
2921
+ ni.addEventListener("keydown", (e) => {
2922
+ if (e.key === "Enter") {
2923
+ e.preventDefault();
2924
+ ni.blur();
2925
+ }
2926
+ });
2927
+ }
2928
+ wrap.appendChild(ni);
2929
+ if (this.ctrl.steppers) {
2930
+ wrap.appendChild(this.buildSteppers(doc, (dir) => {
2931
+ const cur = Number(ni.value);
2932
+ const base = Number.isFinite(cur) ? cur : this.ctrl.value;
2933
+ commitRaw(snapToStep(base + dir * this.ctrl.step, base, this.ctrl.step));
2934
+ }));
2935
+ }
2936
+ this.el = wrap;
2937
+ this.input = ni;
2938
+ }
2939
+ get value() {
2940
+ return this.ctrl.value;
2941
+ }
2942
+ setValue(v) {
2943
+ const n = this.ctrl.sync(v);
2944
+ this.last = String(n);
2945
+ if (this.placeholderMode && n === this.emptyValue) this.input.value = "";
2946
+ else this.input.value = String(n);
2947
+ }
2948
+ buildSteppers(doc, applyDir) {
2949
+ const steps = doc.createElement("div");
2950
+ steps.className = "vela-num-step";
2951
+ const mk = (dir, icon2, label) => {
2952
+ const b = doc.createElement("button");
2953
+ b.type = "button";
2954
+ b.tabIndex = -1;
2955
+ b.setAttribute("aria-label", label);
2956
+ b.appendChild(iconEl(icon2, doc));
2957
+ let timer = 0;
2958
+ const apply = () => applyDir(dir);
2959
+ const stop = () => {
2960
+ if (timer) {
2961
+ window.clearTimeout(timer);
2962
+ timer = 0;
2963
+ }
2964
+ };
2965
+ b.addEventListener("pointerdown", (e) => {
2966
+ e.preventDefault();
2967
+ e.stopPropagation();
2968
+ apply();
2969
+ timer = window.setTimeout(function tick() {
2970
+ apply();
2971
+ timer = window.setTimeout(tick, 60);
2972
+ }, 400);
2973
+ });
2974
+ b.addEventListener("pointerup", stop);
2975
+ b.addEventListener("pointerleave", stop);
2976
+ b.addEventListener("pointercancel", stop);
2977
+ return b;
2978
+ };
2979
+ steps.append(mk(1, "chevron-up", "Increase"), mk(-1, "chevron-down", "Decrease"));
2980
+ return steps;
2981
+ }
2982
+ };
2983
+
2984
+ // src/ui/components/text-field/controller.ts
2985
+ function textFieldController(opts = {}) {
2986
+ let value = opts.value ?? "";
2987
+ const size = opts.size ?? "md";
2988
+ return {
2989
+ get value() {
2990
+ return value;
2991
+ },
2992
+ size,
2993
+ fill: opts.fill ?? size !== "sm",
2994
+ disabled: opts.disabled ?? false,
2995
+ commit(next) {
2996
+ if (next === value) return null;
2997
+ value = next;
2998
+ opts.onChange?.(next);
2999
+ return next;
3000
+ },
3001
+ sync(next) {
3002
+ value = next;
3003
+ }
3004
+ };
3005
+ }
3006
+
3007
+ // src/ui/components/text-field/styles.ts
3008
+ var TEXT_STYLE_ID = "vela-ui-text";
3009
+ var TEXT_CSS = `
3010
+ .vela-text {
3011
+ display: inline-block;
3012
+ min-width: 0;
3013
+ }
3014
+ .vela-text[data-fill] { width: 100%; }
3015
+ .vela-text-field {
3016
+ width: 100%;
3017
+ box-sizing: border-box;
3018
+ height: 34px;
3019
+ background: transparent;
3020
+ border: 1px solid var(--vela-border-strong);
3021
+ border-radius: 6px;
3022
+ color: var(--vela-fg-bright);
3023
+ padding: 0 8px;
3024
+ font-size: 14px;
3025
+ font-family: inherit;
3026
+ outline: none;
3027
+ transition: border-color 0.12s ease, box-shadow 0.12s ease;
3028
+ }
3029
+ .vela-text-field:hover { border-color: var(--vela-fg-muted); }
3030
+ .vela-text-field:focus { border-color: var(--vela-focus); box-shadow: 0 0 0 3px var(--vela-focus-soft); }
3031
+ .vela-text[data-size='sm'] .vela-text-field {
3032
+ height: 28px;
3033
+ background: var(--vela-surface-elev);
3034
+ border-radius: var(--vela-radius-sm);
3035
+ color: var(--vela-fg);
3036
+ font-size: 13px;
3037
+ }
3038
+ `;
3039
+
3040
+ // src/ui/components/text-field/view.ts
3041
+ var TextField = class {
3042
+ constructor(opts = {}) {
3043
+ const doc = document;
3044
+ injectStyles(TEXT_STYLE_ID, TEXT_CSS, doc);
3045
+ this.ctrl = textFieldController(opts);
3046
+ const wrap = doc.createElement("div");
3047
+ wrap.className = "vela-text";
3048
+ wrap.dataset.size = this.ctrl.size;
3049
+ if (this.ctrl.fill) wrap.dataset.fill = "";
3050
+ const ti = doc.createElement("input");
3051
+ ti.type = "text";
3052
+ if (opts.id) ti.id = opts.id;
3053
+ ti.value = this.ctrl.value;
3054
+ ti.className = "vela-text-field";
3055
+ if (this.ctrl.disabled) ti.disabled = true;
3056
+ ti.addEventListener("blur", () => {
3057
+ this.ctrl.commit(ti.value);
3058
+ });
3059
+ ti.addEventListener("keydown", (e) => {
3060
+ if (e.key === "Enter") {
3061
+ e.preventDefault();
3062
+ ti.blur();
3063
+ }
3064
+ });
3065
+ wrap.appendChild(ti);
3066
+ this.el = wrap;
3067
+ this.input = ti;
3068
+ }
3069
+ get value() {
3070
+ return this.ctrl.value;
3071
+ }
3072
+ setValue(v) {
3073
+ this.ctrl.sync(v);
3074
+ this.input.value = v;
3075
+ }
3076
+ };
3077
+
3078
+ // src/ui/components/color-picker/controller.ts
3079
+ function splitColor(color) {
3080
+ const s = String(color ?? "").trim();
3081
+ let m = /^#([0-9a-f]{6})([0-9a-f]{2})?$/i.exec(s);
3082
+ if (m) return { hex6: `#${m[1].toLowerCase()}`, alpha: m[2] ? parseInt(m[2], 16) / 255 : 1 };
3083
+ m = /^#([0-9a-f]{3})$/i.exec(s);
3084
+ if (m) {
3085
+ const h = m[1].toLowerCase();
3086
+ return { hex6: `#${h[0]}${h[0]}${h[1]}${h[1]}${h[2]}${h[2]}`, alpha: 1 };
3087
+ }
3088
+ const rgba = /^rgba?\(([^)]+)\)/i.exec(s);
3089
+ if (rgba) {
3090
+ const p = rgba[1].split(",").map((v) => v.trim());
3091
+ const hx = (n) => Math.max(0, Math.min(255, Math.round(n))).toString(16).padStart(2, "0");
3092
+ const a = p[3] != null ? Math.max(0, Math.min(1, parseFloat(p[3]))) : 1;
3093
+ return { hex6: `#${hx(parseInt(p[0] ?? "0", 10))}${hx(parseInt(p[1] ?? "0", 10))}${hx(parseInt(p[2] ?? "0", 10))}`, alpha: a };
3094
+ }
3095
+ return { hex6: ACCENT_BRIGHT, alpha: 1 };
3096
+ }
3097
+ function combineColor(hex6, alpha) {
3098
+ const a = Math.max(0, Math.min(1, alpha));
3099
+ if (a >= 0.999) return hex6;
3100
+ return `${hex6}${Math.round(a * 255).toString(16).padStart(2, "0")}`;
3101
+ }
3102
+ function blendOver(fg, bg, alpha) {
3103
+ const rgb = (c) => {
3104
+ const n = parseInt(splitColor(c).hex6.slice(1), 16);
3105
+ return [n >> 16 & 255, n >> 8 & 255, n & 255];
3106
+ };
3107
+ const [fr, fgc, fb] = rgb(fg);
3108
+ const [br, bgc, bb] = rgb(bg);
3109
+ const a = Math.max(0, Math.min(1, alpha));
3110
+ const h = (n) => Math.round(n).toString(16).padStart(2, "0");
3111
+ return `#${h(fr * a + br * (1 - a))}${h(fgc * a + bgc * (1 - a))}${h(fb * a + bb * (1 - a))}`;
3112
+ }
3113
+ function hslHex(h, s, l) {
3114
+ const sn = s / 100;
3115
+ const ln = l / 100;
3116
+ const k = (n) => (n + h / 30) % 12;
3117
+ const a = sn * Math.min(ln, 1 - ln);
3118
+ const f = (n) => ln - a * Math.max(-1, Math.min(k(n) - 3, Math.min(9 - k(n), 1)));
3119
+ const to = (x) => Math.round(x * 255).toString(16).padStart(2, "0");
3120
+ return `#${to(f(0))}${to(f(8))}${to(f(4))}`;
3121
+ }
3122
+ function buildPalette() {
3123
+ const grays = [100, 90, 80, 68, 56, 45, 35, 25, 14, 0].map((l) => hslHex(0, 0, l));
3124
+ const hues = [4, 28, 50, 96, 140, 174, 200, 224, 270, 322];
3125
+ const levels = [
3126
+ [82, 56],
3127
+ [58, 84],
3128
+ [62, 74],
3129
+ [66, 62],
3130
+ [70, 50],
3131
+ [64, 39],
3132
+ [54, 29]
3133
+ ];
3134
+ return [grays, ...levels.map(([s, l]) => hues.map((h) => hslHex(h, s, l)))];
3135
+ }
3136
+ function transparencyChecker(size) {
3137
+ return `repeating-conic-gradient(#9aa0a6 0% 25%, #d3d6da 0% 50%) 0 0 / ${size}px ${size}px`;
3138
+ }
3139
+
3140
+ // src/ui/components/color-picker/styles.ts
3141
+ var COLOR_STYLE_ID = "vela-ui-color";
3142
+ var COLOR_CSS = `
3143
+ .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;}
3144
+ .vela-color-field:hover{border-color:var(--vela-fg-muted);}
3145
+ .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);}
3146
+ .vela-color-field-circle{width:26px;height:26px;padding:3px;border:1px solid var(--vela-border-strong);border-radius:4px;background:transparent;overflow:hidden;}
3147
+ .vela-color-field-circle .vela-color-field-swatch{border-radius:2px;box-shadow:none;}
3148
+ .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;}
3149
+ `;
3150
+
3151
+ // src/ui/components/color-picker/view.ts
3152
+ var PALETTE = buildPalette();
3153
+ var CHECKER = transparencyChecker(10);
3154
+ var FIELD_CHECKER = transparencyChecker(8);
3155
+ var recents = [ACCENT, BULLISH, BEARISH, WARNING, NEUTRAL];
3156
+ function addRecent(hex6) {
3157
+ const i = recents.findIndex((c) => c.toLowerCase() === hex6.toLowerCase());
3158
+ if (i >= 0) recents.splice(i, 1);
3159
+ recents.unshift(hex6);
3160
+ if (recents.length > 10) recents.length = 10;
3161
+ }
3162
+ function buildColorPicker(color, theme, onChange) {
3163
+ const parsed = splitColor(color);
3164
+ let curHex = parsed.hex6;
3165
+ let curAlpha = parsed.alpha;
3166
+ const root = document.createElement("div");
3167
+ root.style.cssText = "display:flex;flex-direction:column;gap:9px;width:236px;";
3168
+ const grid = document.createElement("div");
3169
+ grid.style.cssText = "display:grid;grid-template-columns:repeat(10,1fr);gap:4px;";
3170
+ const swatches = [];
3171
+ for (const row of PALETTE) {
3172
+ for (const c of row) {
3173
+ const sw = document.createElement("button");
3174
+ sw.type = "button";
3175
+ sw.dataset.c = c;
3176
+ sw.style.cssText = `width:100%;aspect-ratio:1;border-radius:4px;border:none;cursor:pointer;background:${c};padding:0;`;
3177
+ sw.addEventListener("click", (e) => {
3178
+ e.stopPropagation();
3179
+ pickHex(c);
3180
+ });
3181
+ grid.appendChild(sw);
3182
+ swatches.push(sw);
3183
+ }
3184
+ }
3185
+ const paintSelection = () => {
3186
+ for (const sw of swatches) {
3187
+ const on = (sw.dataset.c ?? "").toLowerCase() === curHex.toLowerCase();
3188
+ sw.style.outline = on ? `2px solid ${theme.textColor}` : "none";
3189
+ sw.style.outlineOffset = "2px";
3190
+ sw.style.zIndex = on ? "1" : "";
3191
+ sw.style.position = on ? "relative" : "";
3192
+ }
3193
+ };
3194
+ const recentRow = document.createElement("div");
3195
+ recentRow.style.cssText = "display:flex;align-items:center;gap:5px;flex-wrap:wrap;border-top:1px solid var(--vela-border);padding-top:9px;";
3196
+ const renderRecents = () => {
3197
+ recentRow.replaceChildren();
3198
+ for (const c of recents) {
3199
+ const sw = document.createElement("button");
3200
+ sw.type = "button";
3201
+ 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;`;
3202
+ sw.addEventListener("click", (e) => {
3203
+ e.stopPropagation();
3204
+ pickHex(c);
3205
+ });
3206
+ recentRow.appendChild(sw);
3207
+ }
3208
+ const add = document.createElement("label");
3209
+ 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;`;
3210
+ add.textContent = "+";
3211
+ const input = document.createElement("input");
3212
+ input.type = "color";
3213
+ input.value = curHex;
3214
+ input.style.cssText = "position:absolute;inset:0;opacity:0;cursor:pointer;";
3215
+ input.addEventListener("input", () => pickHex(input.value, true));
3216
+ add.appendChild(input);
3217
+ recentRow.appendChild(add);
3218
+ };
3219
+ const opLabel = document.createElement("div");
3220
+ opLabel.textContent = "Opacity";
3221
+ opLabel.style.cssText = `font:11px ${theme.fontFamily};color:var(--vela-fg-muted);`;
3222
+ const opRow = document.createElement("div");
3223
+ opRow.style.cssText = "display:flex;align-items:center;gap:10px;";
3224
+ const track = document.createElement("div");
3225
+ track.style.cssText = "flex:1;position:relative;height:13px;border-radius:7px;cursor:pointer;";
3226
+ const knob = document.createElement("div");
3227
+ 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;";
3228
+ track.appendChild(knob);
3229
+ const pctBox = document.createElement("div");
3230
+ 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;`;
3231
+ opRow.append(track, pctBox);
3232
+ const paintOpacity = () => {
3233
+ track.style.background = `linear-gradient(to right, ${curHex}00, ${curHex}ff), ${CHECKER}`;
3234
+ knob.style.left = `${curAlpha * 100}%`;
3235
+ pctBox.textContent = `${Math.round(curAlpha * 100)}%`;
3236
+ };
3237
+ let dragging = false;
3238
+ const onDrag = (clientX) => {
3239
+ const r = track.getBoundingClientRect();
3240
+ curAlpha = Math.max(0, Math.min(1, (clientX - r.left) / r.width));
3241
+ paintOpacity();
3242
+ emit();
3243
+ };
3244
+ track.addEventListener("pointerdown", (e) => {
3245
+ e.stopPropagation();
3246
+ dragging = true;
3247
+ track.setPointerCapture(e.pointerId);
3248
+ onDrag(e.clientX);
3249
+ });
3250
+ track.addEventListener("pointermove", (e) => {
3251
+ if (dragging) onDrag(e.clientX);
3252
+ });
3253
+ track.addEventListener("pointerup", () => dragging = false);
3254
+ function emit() {
3255
+ onChange(combineColor(curHex, curAlpha));
3256
+ }
3257
+ function pickHex(hex, custom = false) {
3258
+ curHex = splitColor(hex).hex6;
3259
+ addRecent(curHex);
3260
+ paintSelection();
3261
+ paintOpacity();
3262
+ if (custom) renderRecents();
3263
+ emit();
3264
+ }
3265
+ renderRecents();
3266
+ paintSelection();
3267
+ paintOpacity();
3268
+ root.append(grid, recentRow, opLabel, opRow);
3269
+ return root;
3270
+ }
3271
+ function colorField(theme, getVal, onVal, opts) {
3272
+ return new ColorField({ theme, getVal, onVal, shape: opts?.shape, id: opts?.id, popover: opts?.popover }).el;
3273
+ }
3274
+ var ColorField = class {
3275
+ constructor(opts) {
3276
+ injectStyles(COLOR_STYLE_ID, COLOR_CSS, document);
3277
+ this.theme = opts.theme;
3278
+ this.getVal = opts.getVal;
3279
+ this.onVal = opts.onVal;
3280
+ this.popoverOpts = opts.popover;
3281
+ const trigger = document.createElement("button");
3282
+ trigger.type = "button";
3283
+ if (opts.id) trigger.id = opts.id;
3284
+ trigger.className = opts.shape === "circle" ? "vela-color-field vela-color-field-circle" : "vela-color-field";
3285
+ const swatch = document.createElement("span");
3286
+ swatch.className = "vela-color-field-swatch";
3287
+ trigger.appendChild(swatch);
3288
+ this.el = trigger;
3289
+ this.swatch = swatch;
3290
+ this.paint();
3291
+ trigger.addEventListener("vela-sync", () => this.paint());
3292
+ trigger.addEventListener("pointerdown", (e) => {
3293
+ e.preventDefault();
3294
+ trigger.focus({ preventScroll: true });
3295
+ });
3296
+ trigger.addEventListener("click", (e) => {
3297
+ e.stopPropagation();
3298
+ this.toggle();
3299
+ });
3300
+ }
3301
+ paint() {
3302
+ const v = this.getVal();
3303
+ this.swatch.style.background = `linear-gradient(${v}, ${v}), ${FIELD_CHECKER}`;
3304
+ }
3305
+ toggle() {
3306
+ if (openPopoverTrigger() === this.el) {
3307
+ closeOpenPopovers();
3308
+ return;
3309
+ }
3310
+ const pop = new Popover({
3311
+ trigger: this.el,
3312
+ theme: this.theme,
3313
+ align: this.popoverOpts?.align ?? "end",
3314
+ gap: this.popoverOpts?.gap ?? 6,
3315
+ host: this.popoverOpts?.host,
3316
+ position: this.popoverOpts?.position,
3317
+ boundary: this.popoverOpts?.boundary,
3318
+ zIndex: this.popoverOpts?.zIndex,
3319
+ className: "vela-color-field-pop",
3320
+ content: buildColorPicker(this.getVal(), this.theme, (val) => {
3321
+ this.onVal(val);
3322
+ this.paint();
3323
+ })
3324
+ });
3325
+ pop.show();
3326
+ }
3327
+ };
3328
+
2084
3329
  // src/widget/layout-picker.ts
2085
3330
  var STYLE_ID = "vela-widget-layout-picker-v14";
2086
3331
  var CSS2 = `
@@ -2837,6 +4082,36 @@ function parseTimeframe(text) {
2837
4082
  short: `${count}${UNIT_SHORT[key]}`
2838
4083
  };
2839
4084
  }
4085
+ function timeframeMs(value) {
4086
+ const v = String(value ?? "").trim();
4087
+ if (!v) return NaN;
4088
+ const m = /^(\d*)\s*([a-zA-Z]?)$/.exec(v);
4089
+ if (!m) return NaN;
4090
+ const key = unitKey((m[2] ?? "").toUpperCase());
4091
+ if (key === null) return NaN;
4092
+ if ((m[1] ?? "") === "" && key === "MIN") return NaN;
4093
+ const count = (m[1] ?? "") === "" ? 1 : parseInt(m[1], 10);
4094
+ if (!Number.isFinite(count) || count < 1) return NaN;
4095
+ return count * UNIT_MS[key];
4096
+ }
4097
+ function favoriteTimeframeChips(favorites) {
4098
+ const seen = /* @__PURE__ */ new Set();
4099
+ const out = [];
4100
+ for (const tf of favorites) {
4101
+ if (seen.has(tf)) continue;
4102
+ seen.add(tf);
4103
+ out.push(tf);
4104
+ }
4105
+ return out.sort((a, b) => {
4106
+ const ma = timeframeMs(a);
4107
+ const mb = timeframeMs(b);
4108
+ const aOk = Number.isFinite(ma);
4109
+ const bOk = Number.isFinite(mb);
4110
+ if (aOk && bOk && ma !== mb) return ma - mb;
4111
+ if (aOk !== bOk) return aOk ? -1 : 1;
4112
+ return 0;
4113
+ });
4114
+ }
2840
4115
  function timeframeLabel(value) {
2841
4116
  const parsed = parseTimeframe(value);
2842
4117
  if (!parsed.valid) return value;
@@ -2889,6 +4164,36 @@ var CSS3 = `
2889
4164
  color: var(--vela-fg-bright);
2890
4165
  }
2891
4166
  .vela-widget-symbol:hover, .vela-widget-tf:hover, .vela-widget-style:hover, .vela-widget-indicators:hover, .vela-widget-action-left:hover { background: var(--vela-hover); color: var(--vela-fg-bright); }
4167
+ /* Timeframe cluster: duration-sorted favorite chips, highlight in place, caret
4168
+ opening the full dropdown. With no favorites the caret is the merged trigger
4169
+ (label + chevron). An unstarred current value sits as an extra chip by the caret. */
4170
+ .vela-widget-tf-group { display: inline-flex; align-items: center; gap: 2px; }
4171
+ .vela-widget-tf-chips { display: inline-flex; align-items: center; gap: 2px; }
4172
+ .vela-widget-tf-chips:empty { display: none; }
4173
+ .vela-widget-tf[data-current='1'] { background: var(--vela-hover-strong); color: var(--vela-fg-bright); }
4174
+ .vela-widget-tf-caret {
4175
+ all: unset;
4176
+ display: inline-flex;
4177
+ align-items: center;
4178
+ justify-content: center;
4179
+ width: 18px;
4180
+ height: 30px;
4181
+ border-radius: 4px;
4182
+ cursor: pointer;
4183
+ color: var(--vela-fg-muted);
4184
+ }
4185
+ .vela-widget-tf-caret:hover { background: var(--vela-hover); color: var(--vela-fg-bright); }
4186
+ .vela-widget-tf-caret[data-solo='1'] {
4187
+ width: auto;
4188
+ padding: 0 6px 0 9px;
4189
+ gap: 4px;
4190
+ color: var(--vela-fg-bright);
4191
+ background: var(--vela-hover-strong);
4192
+ font-size: 13px;
4193
+ font-weight: 550;
4194
+ white-space: nowrap;
4195
+ }
4196
+ .vela-widget-topbar .vela-widget-tf-caret .vela-icon { font-size: 14px; width: 14px; height: 14px; }
2892
4197
  .vela-widget-topbar .vela-icon { color: inherit; font-size: 16px; width: 16px; height: 16px; }
2893
4198
  /* Width is set in syncHairlines() to exactly one device pixel \u2014 a CSS 1px at
2894
4199
  fractional DPR (1.25, 1.5\u2026) straddles two physical pixels and siblings end
@@ -2993,9 +4298,16 @@ var Topbar = class {
2993
4298
  this.symbolEl.className = "vela-widget-symbol";
2994
4299
  this.symbolEl.textContent = parseSymbol(opts.symbol).ticker;
2995
4300
  if (opts.onSymbolClick) this.symbolEl.addEventListener("click", opts.onSymbolClick);
2996
- this.tfButton = doc.createElement("button");
2997
- this.tfButton.className = "vela-widget-tf";
2998
- this.tfButton.textContent = timeframeLabel(this.timeframe);
4301
+ this.tfFavs = [...opts.timeframeFavorites ?? []];
4302
+ this.tfChipsHost = doc.createElement("span");
4303
+ this.tfChipsHost.className = "vela-widget-tf-chips";
4304
+ this.tfCaret = doc.createElement("button");
4305
+ this.tfCaret.className = "vela-widget-tf-caret";
4306
+ this.tfCaret.appendChild(iconEl("chevron-down", doc));
4307
+ this.tfCaret.setAttribute("aria-label", "Timeframes");
4308
+ const tfGroup = doc.createElement("span");
4309
+ tfGroup.className = "vela-widget-tf-group";
4310
+ tfGroup.append(this.tfChipsHost, this.tfCaret);
2999
4311
  this.styleButton = doc.createElement("button");
3000
4312
  this.styleButton.className = "vela-widget-style";
3001
4313
  this.renderStyleButton(doc);
@@ -3035,19 +4347,20 @@ var Topbar = class {
3035
4347
  d.className = "vela-sep";
3036
4348
  return d;
3037
4349
  };
3038
- const leading = [this.symbolEl, sep(), this.tfButton, sep(), this.styleButton, sep()];
4350
+ const leading = [this.symbolEl, sep(), tfGroup, sep(), this.styleButton, sep()];
3039
4351
  if (this.layoutButton) leading.push(this.layoutButton, sep());
3040
4352
  if (indicatorsBtn) leading.push(indicatorsBtn, sep());
3041
4353
  this.leftActionsSep = sep();
3042
4354
  this.leftActionsSep.hidden = true;
3043
4355
  this.el.append(...leading, this.leftActionsHost, this.leftActionsSep, this.undoBtn, this.redoBtn, this.actionsHost, this.alertsBtn, this.panelsHost, screenshotBtn);
3044
4356
  host.appendChild(this.el);
4357
+ this.renderTfChips();
3045
4358
  this.onHairlineSync();
3046
4359
  this.hairlineRo = new ResizeObserver(this.onHairlineSync);
3047
4360
  this.hairlineRo.observe(this.el);
3048
4361
  doc.defaultView?.addEventListener("resize", this.onHairlineSync);
3049
4362
  this.renderActions();
3050
- this.tooltips.push(new Tooltip(this.tfButton, { content: "Timeframe", triggerId: "vela-topbar-tf", host }));
4363
+ this.tooltips.push(new Tooltip(this.tfCaret, { content: "Timeframe", triggerId: "vela-topbar-tf", host }));
3051
4364
  this.tooltips.push(new Tooltip(this.styleButton, { content: "Chart style", triggerId: "vela-topbar-style", host }));
3052
4365
  if (this.layoutButton && opts.layout) {
3053
4366
  this.tooltips.push(new Tooltip(this.layoutButton, { content: "Layout", triggerId: "vela-topbar-layout", host }));
@@ -3064,11 +4377,12 @@ var Topbar = class {
3064
4377
  });
3065
4378
  }
3066
4379
  this.tfMenu = new Menu({
3067
- trigger: this.tfButton,
4380
+ trigger: this.tfCaret,
3068
4381
  triggerId: "vela-topbar-tf",
3069
4382
  host,
3070
4383
  items: this.tfItems(),
3071
4384
  onSelect: (id) => opts.onTimeframe(id),
4385
+ onFavorite: (id, on) => opts.onTimeframeFavorite?.(id, on),
3072
4386
  // Timeframe labels are two-or-three characters ("1m", "4h", "1D") — the
3073
4387
  // stylesheet's default min-width would leave the list mostly empty.
3074
4388
  minWidth: "84px"
@@ -3086,8 +4400,48 @@ var Topbar = class {
3086
4400
  }
3087
4401
  setTimeframe(tf) {
3088
4402
  this.timeframe = tf;
3089
- this.tfButton.textContent = timeframeLabel(tf);
3090
4403
  this.tfMenu.setItems(this.tfItems());
4404
+ this.renderTfChips();
4405
+ }
4406
+ /** Reflect the favorite-timeframe set — the quick-switch chips and the dropdown stars. */
4407
+ setTimeframeFavorites(favs) {
4408
+ this.tfFavs = [...favs];
4409
+ this.renderTfChips();
4410
+ this.tfMenu.setItems(this.tfItems());
4411
+ }
4412
+ /** Rebuild the quick-switch chips (current value changed, or the favorite set did). */
4413
+ renderTfChips() {
4414
+ const doc = this.el.ownerDocument;
4415
+ this.tfChipsHost.replaceChildren();
4416
+ const stars = this.opts.onTimeframeFavorite !== void 0;
4417
+ const chips = stars ? favoriteTimeframeChips(this.tfFavs) : [];
4418
+ const currentIsFav = chips.includes(this.timeframe);
4419
+ const shown = currentIsFav || chips.length === 0 ? chips : [...chips, this.timeframe];
4420
+ for (const tf of shown) {
4421
+ const b = doc.createElement("button");
4422
+ b.className = "vela-widget-tf";
4423
+ const label = timeframeLabel(tf);
4424
+ b.textContent = label;
4425
+ if (tf === this.timeframe) {
4426
+ b.dataset.current = "1";
4427
+ b.setAttribute("aria-current", "true");
4428
+ } else {
4429
+ b.setAttribute("aria-label", `Switch timeframe to ${label}`);
4430
+ b.addEventListener("click", () => this.opts.onTimeframe(tf));
4431
+ }
4432
+ this.tfChipsHost.appendChild(b);
4433
+ }
4434
+ this.tfCaret.replaceChildren();
4435
+ if (chips.length === 0) {
4436
+ this.tfCaret.dataset.solo = "1";
4437
+ this.tfCaret.append(doc.createTextNode(timeframeLabel(this.timeframe)), iconEl("chevron-down", doc));
4438
+ this.tfCaret.setAttribute("aria-label", `Timeframe \u2014 ${timeframeLabel(this.timeframe)}`);
4439
+ } else {
4440
+ delete this.tfCaret.dataset.solo;
4441
+ this.tfCaret.appendChild(iconEl("chevron-down", doc));
4442
+ this.tfCaret.setAttribute("aria-label", "Timeframes");
4443
+ }
4444
+ this.onHairlineSync();
3091
4445
  }
3092
4446
  setPriceStyle(style) {
3093
4447
  this.priceStyle = style;
@@ -3165,9 +4519,9 @@ var Topbar = class {
3165
4519
  }
3166
4520
  /** Reflect a docked side panel's open state on its button — the panels toggle each other,
3167
4521
  * so the dock pushes the state rather than the button assuming it. */
3168
- setPanelActive(id, open) {
4522
+ setPanelActive(id, open2) {
3169
4523
  const btn = this.panelBtns.get(id);
3170
- if (btn) btn.dataset.active = open ? "1" : "";
4524
+ if (btn) btn.dataset.active = open2 ? "1" : "";
3171
4525
  }
3172
4526
  destroy() {
3173
4527
  this.hairlineRo?.disconnect();
@@ -3205,7 +4559,13 @@ var Topbar = class {
3205
4559
  }
3206
4560
  }
3207
4561
  tfItems() {
3208
- return this.opts.timeframes.map((tf) => ({ id: tf, label: timeframeLabel(tf), checked: tf === this.timeframe }));
4562
+ const stars = this.opts.onTimeframeFavorite !== void 0;
4563
+ return this.opts.timeframes.map((tf) => ({
4564
+ id: tf,
4565
+ label: timeframeLabel(tf),
4566
+ checked: tf === this.timeframe,
4567
+ ...stars ? { favorite: this.tfFavs.includes(tf) } : {}
4568
+ }));
3209
4569
  }
3210
4570
  styleItems() {
3211
4571
  return priceStyleIds().map((id, i) => ({
@@ -9490,10 +10850,10 @@ var SidePanel = class {
9490
10850
  return !this.el.hidden;
9491
10851
  }
9492
10852
  /** Open/close the panel — a bare call flips it. */
9493
- toggle(open = this.el.hidden) {
9494
- if (open === !this.el.hidden) return;
9495
- this.el.hidden = !open;
9496
- this.onOpenChange?.(open);
10853
+ toggle(open2 = this.el.hidden) {
10854
+ if (open2 === !this.el.hidden) return;
10855
+ this.el.hidden = !open2;
10856
+ this.onOpenChange?.(open2);
9497
10857
  }
9498
10858
  /** The scrolling body, for a panel filled from OUTSIDE the class — a contributed panel's
9499
10859
  * `mount` receives exactly this element. Subclasses use the protected `body`. */
@@ -9816,9 +11176,9 @@ var ObjectTree = class extends SidePanel {
9816
11176
  set groups(next) {
9817
11177
  if (this.chart) this.groupsPerChart.set(this.chart, next);
9818
11178
  }
9819
- toggle(open = this.el.hidden) {
9820
- super.toggle(open);
9821
- if (open) this.refresh();
11179
+ toggle(open2 = this.el.hidden) {
11180
+ super.toggle(open2);
11181
+ if (open2) this.refresh();
9822
11182
  }
9823
11183
  setSymbol(symbol) {
9824
11184
  this.symbolName = parseSymbol(symbol).ticker;
@@ -10850,9 +12210,9 @@ var DataWindow = class extends SidePanel {
10850
12210
  this.unsubs = [];
10851
12211
  injectStyles(STYLE_ID6, CSS7, host.ownerDocument);
10852
12212
  }
10853
- toggle(open = this.el.hidden) {
10854
- super.toggle(open);
10855
- if (open) this.refresh();
12213
+ toggle(open2 = this.el.hidden) {
12214
+ super.toggle(open2);
12215
+ if (open2) this.refresh();
10856
12216
  }
10857
12217
  /** (Re)bind to a chart instance — called after every widget rebuild. */
10858
12218
  onChart(chart) {
@@ -10966,8 +12326,8 @@ var PanelDock = class {
10966
12326
  }
10967
12327
  }
10968
12328
  /** Open/close one panel by id — a bare call flips it. Unknown ids are ignored. */
10969
- toggle(id, open) {
10970
- this.entries.find((e) => e.id === id)?.panel.toggle(open);
12329
+ toggle(id, open2) {
12330
+ this.entries.find((e) => e.id === id)?.panel.toggle(open2);
10971
12331
  }
10972
12332
  /** The open panel's id, or null when the column is closed. */
10973
12333
  get openId() {
@@ -10981,8 +12341,8 @@ var PanelDock = class {
10981
12341
  /** The dock's persistable state, or null when there is nothing worth saving. */
10982
12342
  getState() {
10983
12343
  const out = {};
10984
- const open = this.openId;
10985
- if (open) out.open = open;
12344
+ const open2 = this.openId;
12345
+ if (open2) out.open = open2;
10986
12346
  if (this.widths.size > 0) out.widths = Object.fromEntries(this.widths);
10987
12347
  return out.open || out.widths ? out : null;
10988
12348
  }
@@ -11015,13 +12375,13 @@ var PanelDock = class {
11015
12375
  this.entries.sort((a, b) => a.order - b.order);
11016
12376
  const stored = this.widths.get(entry.id);
11017
12377
  if (stored !== void 0) entry.panel.setWidth(stored);
11018
- entry.panel.onOpenChange = (open) => {
11019
- if (open) {
12378
+ entry.panel.onOpenChange = (open2) => {
12379
+ if (open2) {
11020
12380
  for (const other of this.entries) if (other !== entry) other.panel.toggle(false);
11021
12381
  this.pendingOpen = null;
11022
12382
  }
11023
- this.deps.chrome.setPanelActive(entry.id, open);
11024
- if (open) entry.handle?.onOpen?.();
12383
+ this.deps.chrome.setPanelActive(entry.id, open2);
12384
+ if (open2) entry.handle?.onOpen?.();
11025
12385
  this.deps.changed?.();
11026
12386
  };
11027
12387
  entry.panel.onWidthChange = (px) => {
@@ -11218,8 +12578,8 @@ var SymbolPicker = class {
11218
12578
  host: opts.host,
11219
12579
  closeOnInteractOutside: true,
11220
12580
  content: (body) => body.append(searchRow, this.tabs, this.list),
11221
- onOpenChange: (open) => {
11222
- if (open) {
12581
+ onOpenChange: (open2) => {
12582
+ if (open2) {
11223
12583
  this.input.value = this.seed;
11224
12584
  this.seed = "";
11225
12585
  this.refresh();
@@ -11228,7 +12588,7 @@ var SymbolPicker = class {
11228
12588
  this.input.setSelectionRange(this.input.value.length, this.input.value.length);
11229
12589
  }, 0);
11230
12590
  }
11231
- opts.onOpenChange?.(open);
12591
+ opts.onOpenChange?.(open2);
11232
12592
  }
11233
12593
  });
11234
12594
  this.input.addEventListener("input", () => this.refresh());
@@ -11418,14 +12778,14 @@ var IndicatorPicker = class {
11418
12778
  closeOnInteractOutside: true,
11419
12779
  // The picker adds/removes indicators live — keep the chart undimmed behind it.
11420
12780
  content: (body) => body.append(searchRow, this.list),
11421
- onOpenChange: (open) => {
11422
- this.isOpen = open;
11423
- if (open) {
12781
+ onOpenChange: (open2) => {
12782
+ this.isOpen = open2;
12783
+ if (open2) {
11424
12784
  this.search.value = "";
11425
12785
  this.refresh();
11426
12786
  if (!this.search.closest('[data-layout="mobile"]')) setTimeout(() => this.search.focus(), 0);
11427
12787
  }
11428
- opts.onOpenChange?.(open);
12788
+ opts.onOpenChange?.(open2);
11429
12789
  }
11430
12790
  });
11431
12791
  this.list.addEventListener("click", (e) => {
@@ -11564,7 +12924,7 @@ var TimeframeQuick = class {
11564
12924
  draggable: true,
11565
12925
  closeOnInteractOutside: true,
11566
12926
  content: (body) => body.append(this.input, this.hint),
11567
- onOpenChange: (open) => opts.onOpenChange?.(open)
12927
+ onOpenChange: (open2) => opts.onOpenChange?.(open2)
11568
12928
  });
11569
12929
  this.input.addEventListener("input", () => this.renderHint());
11570
12930
  this.input.addEventListener("keydown", (e) => {
@@ -11641,9 +13001,9 @@ var ShortcutsHelp = class {
11641
13001
  host,
11642
13002
  closeOnInteractOutside: true,
11643
13003
  content: (body) => body.appendChild(this.list),
11644
- onOpenChange: (open) => {
11645
- if (open) this.refresh();
11646
- onOpenChange?.(open);
13004
+ onOpenChange: (open2) => {
13005
+ if (open2) this.refresh();
13006
+ onOpenChange?.(open2);
11647
13007
  }
11648
13008
  });
11649
13009
  }
@@ -14831,6 +16191,10 @@ function sanitizeState(doc) {
14831
16191
  const favs = d.favorites.filter((f) => typeof f === "string");
14832
16192
  if (favs.length > 0) out.favorites = favs;
14833
16193
  }
16194
+ if (Array.isArray(d.timeframeFavorites)) {
16195
+ const favs = d.timeframeFavorites.filter((f) => typeof f === "string");
16196
+ if (favs.length > 0) out.timeframeFavorites = favs;
16197
+ }
14834
16198
  const sync = sanitizeSync(d.sync);
14835
16199
  if (sync) out.sync = sync;
14836
16200
  const tracks = sanitizeTrackSizes(d.trackSizes);
@@ -17210,6 +18574,7 @@ var LEGEND_CTL_PX = 18;
17210
18574
  var LEGEND_ROW_PAD_Y = 2;
17211
18575
  var LEGEND_ROW_PAD_X = 6;
17212
18576
  var LEGEND_TITLE_VALUES_GAP_PX = 6;
18577
+ var LEGEND_TITLE_STATUS_GAP_PX = 8;
17213
18578
  var LEGEND_ACTION_GAP_PX = 6;
17214
18579
  var LEGEND_CTL_CSS = `cursor:pointer;display:inline-flex;align-items:center;justify-content:center;border:none;line-height:0;padding:0;width:${LEGEND_CTL_PX}px;height:${LEGEND_CTL_PX}px;border-radius:3px;box-sizing:border-box;flex:none;`;
17215
18580
  var LEGEND_ROW_MIN_H = LEGEND_CTL_PX + LEGEND_ROW_PAD_Y * 2;
@@ -17254,6 +18619,11 @@ var InputsUI = class {
17254
18619
  this.dialogTips = [];
17255
18620
  /** Open "Move to" menu (kept so it can be torn down). */
17256
18621
  this.moveMenu = null;
18622
+ /** Open settings-dialog choice menu (custom dropdown — torn down with the dialog). */
18623
+ this.choicePop = null;
18624
+ /** Open settings-dialog calendar popover (torn down with the dialog). */
18625
+ this.calendarPop = null;
18626
+ this.calendarAnchor = null;
17257
18627
  /** Collapsed panes → the master indicator id to keep visible (others hidden in the strip). */
17258
18628
  this.paneCollapse = /* @__PURE__ */ new Map();
17259
18629
  /** The user folded the indicator legend away behind the chevron — CHART-WIDE: every
@@ -17279,6 +18649,10 @@ var InputsUI = class {
17279
18649
  this.onDialogKey = (e) => {
17280
18650
  if (e.key === "Escape") {
17281
18651
  e.preventDefault();
18652
+ if (isPopoverOpen()) {
18653
+ closeOpenPopovers();
18654
+ return;
18655
+ }
17282
18656
  this.closeDialog();
17283
18657
  }
17284
18658
  };
@@ -17724,6 +19098,7 @@ var InputsUI = class {
17724
19098
  titleWrap.appendChild(beta);
17725
19099
  }
17726
19100
  el.appendChild(titleWrap);
19101
+ el.appendChild(statusEl);
17727
19102
  const valuesEl = document.createElement("span");
17728
19103
  valuesEl.style.cssText = `display:none;align-items:center;gap:5px;margin-left:${LEGEND_TITLE_VALUES_GAP_PX}px;white-space:nowrap;font-variant-numeric:tabular-nums;`;
17729
19104
  el.appendChild(valuesEl);
@@ -17784,7 +19159,6 @@ var InputsUI = class {
17784
19159
  close.addEventListener("click", () => this.onRemove?.(id));
17785
19160
  controlsEl.appendChild(close);
17786
19161
  el.appendChild(controlsEl);
17787
- el.appendChild(statusEl);
17788
19162
  this.attach(this.legendFor(paneId), el, !!opts.native);
17789
19163
  this.rows.set(id, { id, title, settingsTitle, inputs, values: { ...values }, el, titleEl, statusEl, valuesEl, plotValues: [], plotValuesKey: "", showValues: null, highlighted: false, paneId, hidden: false, eyeEl, controlsEl, extrasEl, native: !!opts.native });
17790
19164
  this.syncFoldToggle();
@@ -17802,7 +19176,7 @@ var InputsUI = class {
17802
19176
  /**
17803
19177
  * Reflect an indicator's live status in its legend row: `'loading'` shows three pulsing
17804
19178
  * dots (a fetch is in flight — the same load affordance as the chart's own bar-load dots,
17805
- * at legend scale), `'live'` a pulsing dot, `'idle'` nothing. Rendered at the row's right end.
19179
+ * at legend scale), `'live'` a pulsing dot, `'idle'` nothing. Rendered just right of the title.
17806
19180
  */
17807
19181
  setStatus(id, status) {
17808
19182
  const row = this.rows.get(id);
@@ -17814,7 +19188,7 @@ var InputsUI = class {
17814
19188
  return;
17815
19189
  }
17816
19190
  if (status === "loading") {
17817
- el.style.cssText = "display:inline-flex;align-items:center;gap:3px;box-sizing:border-box;flex:none;";
19191
+ el.style.cssText = `display:inline-flex;align-items:center;gap:3px;box-sizing:border-box;flex:none;margin-left:${LEGEND_TITLE_STATUS_GAP_PX}px;line-height:0;transform:translateY(1px);`;
17818
19192
  for (let i = 0; i < 3; i += 1) {
17819
19193
  const dot = document.createElement("span");
17820
19194
  dot.style.cssText = "width:4px;height:4px;border-radius:50%;background:currentColor;opacity:0.15;flex:none;";
@@ -17824,7 +19198,7 @@ var InputsUI = class {
17824
19198
  return;
17825
19199
  }
17826
19200
  this.ensureStatusKeyframes();
17827
- el.style.cssText = `display:inline-block;box-sizing:border-box;flex:none;width:8px;height:8px;border-radius:50%;background:${this.theme.upColor};animation:vela-ind-pulse 1.2s ease-in-out infinite;`;
19201
+ el.style.cssText = `display:inline-block;box-sizing:border-box;flex:none;width:8px;height:8px;border-radius:50%;margin-left:${LEGEND_TITLE_STATUS_GAP_PX}px;transform:translateY(1px);background:${this.theme.upColor};animation:vela-ind-pulse 1.2s ease-in-out infinite;`;
17828
19202
  }
17829
19203
  /** Inject the status keyframes once (idempotent). */
17830
19204
  ensureStatusKeyframes() {
@@ -17877,15 +19251,15 @@ var InputsUI = class {
17877
19251
  * construction and is shown/hidden with the cluster container.
17878
19252
  */
17879
19253
  syncRowActions(row) {
17880
- const open = row.highlighted;
17881
- row.controlsEl.style.display = open || row.hidden ? "inline-flex" : "none";
19254
+ const open2 = row.highlighted;
19255
+ row.controlsEl.style.display = open2 || row.hidden ? "inline-flex" : "none";
17882
19256
  for (const child of Array.from(row.controlsEl.children)) {
17883
19257
  if (!(child instanceof HTMLElement) || child === row.eyeEl) continue;
17884
19258
  if (child === row.extrasEl) {
17885
- child.style.display = open ? "contents" : "none";
19259
+ child.style.display = open2 ? "contents" : "none";
17886
19260
  continue;
17887
19261
  }
17888
- child.style.display = open ? "inline-flex" : "none";
19262
+ child.style.display = open2 ? "inline-flex" : "none";
17889
19263
  }
17890
19264
  }
17891
19265
  /** Drop the current selection outline, if any. */
@@ -17939,7 +19313,8 @@ var InputsUI = class {
17939
19313
  /**
17940
19314
  * Open the indicator's settings as a centered modal over the chart — a themed,
17941
19315
  * dependency-free port of the app's tabbed script-settings dialog: a header with the
17942
- * indicator title, an "Inputs" tab, a scrollable body that groups inputs by `group=`
19316
+ * indicator title, a tab strip (the default "Inputs" tab plus any tab the inputs
19317
+ * declare via `tab=`), a scrollable body per tab that groups inputs by `group=`
17943
19318
  * (section headers) and packs `inline=` inputs onto one row, and a sticky footer.
17944
19319
  *
17945
19320
  * Edits commit LIVE (each change re-runs the indicator), matching the current
@@ -17969,55 +19344,57 @@ var InputsUI = class {
17969
19344
  });
17970
19345
  const card = document.createElement("div");
17971
19346
  card.className = "vela-ind-dialog";
17972
- card.style.cssText = `display:flex;flex-direction:column;width:fit-content;min-width:min(360px,90%);max-width:min(640px,94%);max-height:82%;background:${t.background};border:1px solid ${border};border-radius:var(--vela-radius-lg);box-shadow:var(--vela-shadow-dialog);color:${fg};color-scheme:${scheme};font:13px ${font};overflow:hidden;`;
19347
+ card.style.cssText = `display:flex;flex-direction:column;width:fit-content;min-width:min(380px,90%);max-width:min(640px,94%);max-height:82%;background:${t.background};border:1px solid ${border};border-radius:var(--vela-radius-lg);box-shadow:var(--vela-shadow-dialog);color:${fg};color-scheme:${scheme};font:14px ${font};overflow:hidden;`;
17973
19348
  if (this.mobileLayout) card.style.cssText += "width:100%;min-width:0;max-width:none;height:100%;max-height:none;border:none;border-radius:0;";
17974
19349
  applyChromeTokens(card, t);
17975
19350
  card.addEventListener("keydown", (e) => {
17976
19351
  if (e.key !== "Escape") e.stopPropagation();
17977
19352
  });
17978
19353
  const header = document.createElement("div");
17979
- header.style.cssText = "display:flex;justify-content:space-between;align-items:flex-start;gap:12px;padding:16px 20px 12px;flex:0 0 auto;";
19354
+ header.style.cssText = "display:flex;justify-content:space-between;align-items:flex-start;gap:12px;padding:16px 20px 20px;flex:0 0 auto;";
17980
19355
  const hTitle = document.createElement("span");
17981
19356
  hTitle.textContent = row.settingsTitle;
17982
- hTitle.style.cssText = "font-weight:600;font-size:16px;line-height:1.3;";
19357
+ hTitle.style.cssText = "font-weight:600;font-size:20px;line-height:28px;";
17983
19358
  const closeBtn = document.createElement("button");
17984
19359
  closeBtn.type = "button";
17985
19360
  closeBtn.setAttribute("aria-label", "Close");
17986
19361
  this.dialogTips.push(attachChromeTooltip(closeBtn, { host: this.container, theme: () => this.theme, text: () => "Close" }));
17987
19362
  closeBtn.innerHTML = iconAt("close", 15);
17988
- closeBtn.className = "vela-ind-ctl";
17989
- closeBtn.style.cssText = "cursor:pointer;display:inline-flex;align-items:center;background:transparent;border:none;line-height:0;padding:2px;flex:0 0 auto;";
19363
+ closeBtn.className = "vela-ind-dlg-close";
17990
19364
  closeBtn.addEventListener("click", () => this.closeDialog());
17991
19365
  header.append(hTitle, closeBtn);
17992
19366
  card.appendChild(header);
17993
19367
  if (!this.mobileLayout) makeDialogDraggable(card, header, { closeSelector: "button" });
19368
+ const tabDefs = tabInputs(row.inputs);
17994
19369
  const tabs = document.createElement("div");
17995
- tabs.style.cssText = `display:flex;gap:16px;padding:0 20px;border-bottom:1px solid ${border};flex:0 0 auto;`;
17996
- const tab = document.createElement("div");
17997
- tab.textContent = "Inputs";
17998
- tab.style.cssText = `padding:8px 2px;margin-bottom:-1px;border-bottom:2px solid ${fg};font-weight:600;font-size:13px;`;
17999
- tabs.appendChild(tab);
18000
- card.appendChild(tabs);
18001
- const body = document.createElement("div");
18002
- body.style.cssText = "padding:14px 20px 6px;overflow-y:auto;flex:1 1 auto;display:flex;flex-direction:column;gap:24px;";
18003
- for (const group of groupInputs(row.inputs)) {
18004
- const section = document.createElement("div");
18005
- section.style.cssText = "display:flex;flex-direction:column;gap:14px;";
18006
- if (group.name) {
18007
- const gh = document.createElement("div");
18008
- gh.textContent = group.name;
18009
- gh.style.cssText = "font-size:var(--vela-font-size-sm);font-weight:600;letter-spacing:0.05em;text-transform:uppercase;color:var(--vela-fg-muted);";
18010
- section.appendChild(gh);
19370
+ tabs.style.cssText = `display:flex;gap:12px;padding:0 20px;border-bottom:1px solid ${border};flex:0 0 auto;`;
19371
+ const tabEls = [];
19372
+ const bodies = [];
19373
+ const activate = (idx) => {
19374
+ for (let i = 0; i < tabEls.length; i += 1) {
19375
+ const active = i === idx;
19376
+ const el = tabEls[i];
19377
+ el.style.borderBottomColor = active ? fg : "transparent";
19378
+ el.style.color = active ? "inherit" : "var(--vela-fg-muted)";
19379
+ el.classList.toggle("vela-ind-tab-active", active);
19380
+ bodies[i].style.display = active ? "grid" : "none";
18011
19381
  }
18012
- const grid = document.createElement("div");
18013
- grid.style.cssText = this.mobileLayout ? "display:grid;grid-template-columns:minmax(0,1fr) auto auto max-content;align-items:center;column-gap:12px;row-gap:22px;" : "display:grid;grid-template-columns:minmax(140px,auto) auto auto 1fr;align-items:center;column-gap:12px;row-gap:22px;";
18014
- for (const inputRow of group.rows) this.buildInputRow(grid, inputRow, row);
18015
- section.appendChild(grid);
18016
- body.appendChild(section);
19382
+ };
19383
+ for (const [idx, def] of tabDefs.entries()) {
19384
+ const tab = document.createElement("div");
19385
+ tab.textContent = def.name;
19386
+ tab.className = "vela-ind-tab";
19387
+ tab.style.cssText = `padding:0 0 8px;margin-bottom:-1px;border-bottom:2px solid transparent;font-weight:600;font-size:16px;line-height:24px;${tabDefs.length > 1 ? "cursor:pointer;" : ""}`;
19388
+ tab.addEventListener("click", () => activate(idx));
19389
+ tabs.appendChild(tab);
19390
+ tabEls.push(tab);
19391
+ bodies.push(this.buildTabBody(def.inputs, row));
18017
19392
  }
18018
- card.appendChild(body);
19393
+ card.appendChild(tabs);
19394
+ for (const body of bodies) card.appendChild(body);
19395
+ activate(0);
18019
19396
  const footer = document.createElement("div");
18020
- footer.style.cssText = `display:flex;justify-content:flex-end;gap:8px;padding:14px 20px;border-top:1px solid ${border};flex:0 0 auto;`;
19397
+ footer.style.cssText = `display:flex;justify-content:flex-end;gap:12px;padding:16px 20px;border-top:1px solid ${border};flex:0 0 auto;`;
18021
19398
  footer.append(
18022
19399
  this.dialogButton("Cancel", false, () => this.revertAndClose()),
18023
19400
  this.dialogButton("Ok", true, () => this.closeDialog())
@@ -18029,8 +19406,32 @@ var InputsUI = class {
18029
19406
  this.backdrop = backdrop;
18030
19407
  document.addEventListener("keydown", this.onDialogKey);
18031
19408
  }
19409
+ /** One tab's scrollable body — one shared grid so every group's controls share a
19410
+ * left edge (a 100px number field lines up with a wider session/time pair). */
19411
+ buildTabBody(inputs, row) {
19412
+ const body = document.createElement("div");
19413
+ body.style.cssText = this.mobileLayout ? "padding:16px 20px;overflow-y:auto;overflow-x:hidden;overflow-anchor:none;flex:1 1 auto;display:grid;grid-template-columns:minmax(0,1fr) auto;align-items:center;column-gap:16px;row-gap:16px;" : "padding:16px 20px;overflow-y:auto;overflow-x:hidden;overflow-anchor:none;flex:1 1 auto;display:grid;grid-template-columns:max-content 1fr;align-items:center;column-gap:16px;row-gap:16px;";
19414
+ for (const group of groupInputs(inputs)) {
19415
+ const section = document.createElement("div");
19416
+ section.style.cssText = "display:contents;";
19417
+ if (group.name) {
19418
+ const gh = document.createElement("div");
19419
+ gh.textContent = group.name;
19420
+ const first = body.childElementCount === 0;
19421
+ gh.style.cssText = `grid-column:1 / -1;font-size:11px;font-weight:600;letter-spacing:0.05em;text-transform:uppercase;color:var(--vela-fg-muted);padding:${first ? "4px" : "20px"} 0 8px;`;
19422
+ section.appendChild(gh);
19423
+ }
19424
+ for (const inputRow of group.rows) this.buildInputRow(section, inputRow, row);
19425
+ body.appendChild(section);
19426
+ }
19427
+ return body;
19428
+ }
18032
19429
  closeDialog() {
18033
19430
  document.removeEventListener("keydown", this.onDialogKey);
19431
+ closeOpenPopovers();
19432
+ this.choicePop = null;
19433
+ this.calendarPop = null;
19434
+ this.calendarAnchor = null;
18034
19435
  for (const dispose of this.dialogTips.splice(0)) dispose();
18035
19436
  this.backdrop?.remove();
18036
19437
  this.backdrop = null;
@@ -18058,79 +19459,77 @@ var InputsUI = class {
18058
19459
  buildInputRow(grid, decls, row) {
18059
19460
  const idOf = (inp) => `vela-inp-${row.id}-${inp.key}`;
18060
19461
  if (decls.length > 1) {
18061
- const cell = document.createElement("div");
18062
- cell.style.cssText = "grid-column:1 / -1;display:flex;flex-wrap:wrap;align-items:center;gap:12px;";
19462
+ const cell2 = document.createElement("div");
19463
+ cell2.style.cssText = "grid-column:1 / -1;display:flex;flex-wrap:wrap;align-items:center;gap:12px;";
18063
19464
  for (const d of decls) {
18064
19465
  const fit2 = d.type === "color" || d.type === "bool";
18065
19466
  const toggleFirst = d.type === "bool";
18066
19467
  const id2 = idOf(d);
18067
19468
  const item = document.createElement("div");
18068
- item.style.cssText = `display:flex;align-items:center;gap:6px;${toggleFirst ? "flex-direction:row-reverse;" : ""}`;
19469
+ item.style.cssText = `display:flex;align-items:center;gap:8px;${toggleFirst ? "flex-direction:row-reverse;" : ""}`;
18069
19470
  const name = nameOf(d);
18070
19471
  if (name) {
18071
19472
  const lbl = document.createElement("label");
18072
19473
  lbl.htmlFor = id2;
18073
19474
  lbl.textContent = name;
18074
- lbl.style.cssText = `font-size:13px;opacity:0.9;${toggleFirst ? "cursor:pointer;" : ""}`;
19475
+ lbl.style.cssText = `font-size:14px;opacity:0.9;${toggleFirst ? "cursor:pointer;" : ""}`;
18075
19476
  item.appendChild(lbl);
18076
19477
  }
18077
19478
  const cw2 = document.createElement("div");
18078
- cw2.style.cssText = fit2 ? "" : "width:110px;";
19479
+ cw2.style.cssText = fit2 ? "" : "width:100px;";
18079
19480
  cw2.appendChild(this.buildControl(row, d, id2));
18080
19481
  item.appendChild(cw2);
18081
- cell.appendChild(item);
19482
+ cell2.appendChild(item);
18082
19483
  }
18083
19484
  const lastTip = [...decls].reverse().find((d) => d.tooltip)?.tooltip;
18084
- if (lastTip) cell.appendChild(this.infoButton(lastTip));
18085
- grid.appendChild(cell);
19485
+ if (lastTip) cell2.appendChild(this.infoButton(lastTip));
19486
+ grid.appendChild(cell2);
18086
19487
  return;
18087
19488
  }
18088
19489
  const lead = decls[0];
18089
19490
  const id = idOf(lead);
18090
19491
  if (lead.type === "bool") {
18091
- const cell = document.createElement("div");
18092
- cell.style.cssText = "grid-column:1 / 4;display:flex;align-items:center;gap:8px;";
19492
+ const cell2 = document.createElement("div");
19493
+ cell2.style.cssText = "grid-column:1 / -1;display:flex;align-items:center;gap:8px;";
18093
19494
  const lbl = document.createElement("label");
18094
- lbl.style.cssText = "display:flex;align-items:center;gap:8px;cursor:pointer;font-size:13px;";
19495
+ lbl.style.cssText = "display:flex;align-items:center;gap:8px;cursor:pointer;font-size:14px;";
18095
19496
  lbl.appendChild(this.buildControl(row, lead, id));
18096
19497
  lbl.appendChild(document.createTextNode(nameOf(lead)));
18097
- cell.appendChild(lbl);
18098
- if (lead.tooltip) cell.appendChild(this.infoButton(lead.tooltip));
18099
- grid.appendChild(cell);
18100
- grid.appendChild(document.createElement("div"));
19498
+ cell2.appendChild(lbl);
19499
+ if (lead.tooltip) cell2.appendChild(this.infoButton(lead.tooltip));
19500
+ grid.appendChild(cell2);
18101
19501
  return;
18102
19502
  }
18103
19503
  if (lead.type === "text_area") {
18104
- const cell = document.createElement("div");
18105
- cell.style.cssText = "grid-column:1 / -1;display:flex;flex-direction:column;gap:8px;";
19504
+ const cell2 = document.createElement("div");
19505
+ cell2.style.cssText = "grid-column:1 / -1;display:flex;flex-direction:column;gap:8px;";
18106
19506
  const head = document.createElement("div");
18107
19507
  head.style.cssText = "display:flex;align-items:center;justify-content:center;gap:8px;";
18108
19508
  const lbl = document.createElement("label");
18109
19509
  lbl.htmlFor = id;
18110
19510
  lbl.textContent = nameOf(lead);
18111
- lbl.style.cssText = "font-size:13px;";
19511
+ lbl.style.cssText = "font-size:14px;";
18112
19512
  head.appendChild(lbl);
18113
19513
  if (lead.tooltip) head.appendChild(this.infoButton(lead.tooltip));
18114
- cell.appendChild(head);
18115
- cell.appendChild(this.buildControl(row, lead, id));
18116
- grid.appendChild(cell);
19514
+ cell2.appendChild(head);
19515
+ cell2.appendChild(this.buildControl(row, lead, id));
19516
+ grid.appendChild(cell2);
18117
19517
  return;
18118
19518
  }
18119
19519
  const label = document.createElement("label");
18120
19520
  label.htmlFor = id;
18121
19521
  label.textContent = nameOf(lead);
18122
- label.style.cssText = "font-size:13px;opacity:0.9;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;";
18123
- const controlCell = document.createElement("div");
18124
- controlCell.style.cssText = "grid-column:span 2;display:flex;align-items:center;gap:8px;";
19522
+ label.style.cssText = "font-size:14px;opacity:0.9;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;";
18125
19523
  const fit = lead.type === "color" || lead.type === "session" || lead.type === "time";
19524
+ const cell = document.createElement("div");
19525
+ cell.style.cssText = "display:flex;align-items:center;gap:8px;justify-self:start;";
18126
19526
  const cw = document.createElement("div");
18127
- cw.style.cssText = fit ? "" : "width:140px;";
19527
+ cw.style.cssText = fit ? "" : "width:100px;";
18128
19528
  cw.appendChild(this.buildControl(row, lead, id));
18129
- controlCell.appendChild(cw);
18130
- if (lead.tooltip) controlCell.appendChild(this.infoButton(lead.tooltip));
19529
+ cell.appendChild(cw);
19530
+ if (lead.tooltip) cell.appendChild(this.infoButton(lead.tooltip));
18131
19531
  grid.appendChild(label);
18132
- grid.appendChild(controlCell);
18133
- grid.appendChild(document.createElement("div"));
19532
+ grid.appendChild(cell);
18134
19533
  }
18135
19534
  /** Build the typed control for one input, committing edits live via `onChange`. */
18136
19535
  buildControl(row, inp, id) {
@@ -18139,107 +19538,30 @@ var InputsUI = class {
18139
19538
  row.values[inp.key] = value;
18140
19539
  this.onChange?.({ indicatorId: row.id, key: inp.key, value });
18141
19540
  };
18142
- const ctrl = this.ctrlStyle();
18143
19541
  if (inp.type === "bool") return this.buildToggle(id, Boolean(current), emit);
18144
- if (inp.options && inp.options.length > 0) return this.select(id, inp.options.map(String), String(current), `${ctrl}cursor:pointer;`, emit);
18145
- if (inp.type === "source") return this.select(id, SOURCES, String(current), `${ctrl}cursor:pointer;`, emit);
19542
+ if (inp.options && inp.options.length > 0) return this.select(id, inp.options.map(String), String(current), emit);
19543
+ if (inp.type === "source") return this.select(id, SOURCES, String(current), emit);
18146
19544
  if (inp.type === "color") {
18147
- const ci = document.createElement("input");
18148
- ci.type = "color";
18149
- ci.id = id;
18150
- ci.value = toHex6(String(current));
18151
- ci.style.cssText = `flex:0 0 auto;box-sizing:border-box;width:32px;height:32px;padding:2px;border:1px solid ${this.neutralBorder()};border-radius:0;background:transparent;cursor:pointer;`;
18152
- ci.addEventListener("input", () => emit(ci.value));
18153
- return ci;
19545
+ const field = colorField(this.theme, () => String(row.values[inp.key] ?? inp.defval), (v) => emit(v), { shape: "circle", id });
19546
+ return field;
18154
19547
  }
18155
19548
  if (inp.type === "symbol") return this.buildSymbol(id, String(current), emit);
18156
- if (inp.type === "timeframe") return this.selectPairs(id, TIMEFRAME_OPTIONS, String(current), `${ctrl}cursor:pointer;`, emit);
19549
+ if (inp.type === "timeframe") return this.selectPairs(id, TIMEFRAME_OPTIONS, String(current), emit);
18157
19550
  if (inp.type === "session") return this.buildSession(id, String(current), emit);
18158
19551
  if (inp.type === "time") return this.buildTime(id, Number(current) || 0, emit);
18159
19552
  if (inp.type === "text_area") return this.buildTextArea(id, String(current), emit);
18160
19553
  if (inp.type === "int" || inp.type === "float" || inp.type === "price") {
18161
- const ni = document.createElement("input");
18162
- ni.type = "number";
18163
- ni.id = id;
18164
- ni.value = String(current);
18165
- if (inp.min !== void 0) ni.min = String(inp.min);
18166
- if (inp.max !== void 0) ni.max = String(inp.max);
18167
- ni.step = String(inp.step ?? (inp.type === "int" ? 1 : 0.1));
18168
- ni.style.cssText = ctrl;
18169
- let last = String(current);
18170
- const commit = () => {
18171
- let n = Number(ni.value);
18172
- if (!Number.isFinite(n)) {
18173
- ni.value = last;
18174
- return;
18175
- }
18176
- if (inp.min !== void 0) n = Math.max(inp.min, n);
18177
- if (inp.max !== void 0) n = Math.min(inp.max, n);
18178
- if (inp.type === "int") n = Math.round(n);
18179
- ni.value = String(n);
18180
- if (String(n) !== last) {
18181
- last = String(n);
18182
- emit(n);
18183
- }
18184
- };
18185
- ni.addEventListener("blur", commit);
18186
- ni.addEventListener("keydown", (e) => {
18187
- if (e.key === "Enter") {
18188
- e.preventDefault();
18189
- ni.blur();
18190
- }
18191
- });
18192
- return ni;
19554
+ return this.buildNumber(id, Number(current), inp, emit);
18193
19555
  }
18194
19556
  return this.buildTextField(id, String(current), emit);
18195
19557
  }
18196
19558
  /** A plain text field that commits on blur / Enter (shared by string inputs and the pickerless symbol field). */
18197
19559
  buildTextField(id, current, emit) {
18198
- const ti = document.createElement("input");
18199
- ti.type = "text";
18200
- ti.id = id;
18201
- ti.value = current;
18202
- ti.style.cssText = this.ctrlStyle();
18203
- let last = current;
18204
- ti.addEventListener("blur", () => {
18205
- if (ti.value !== last) {
18206
- last = ti.value;
18207
- emit(ti.value);
18208
- }
18209
- });
18210
- ti.addEventListener("keydown", (e) => {
18211
- if (e.key === "Enter") {
18212
- e.preventDefault();
18213
- ti.blur();
18214
- }
18215
- });
18216
- return ti;
19560
+ return new TextField({ id, value: current, size: "md", onChange: emit }).el;
18217
19561
  }
18218
19562
  /** A square check-toggle (filled + check when on) — replaces the raw checkbox. */
18219
19563
  buildToggle(id, checked, onChange) {
18220
- const t = this.theme;
18221
- const border = this.neutralBorder();
18222
- const fill = this.strongText();
18223
- const b = document.createElement("button");
18224
- b.type = "button";
18225
- b.id = id;
18226
- b.setAttribute("role", "switch");
18227
- b.innerHTML = CHECK_SVG;
18228
- b.style.cssText = `width:20px;height:20px;flex:0 0 auto;display:inline-flex;align-items:center;justify-content:center;border:1px solid ${border};border-radius:5px;cursor:pointer;padding:0;transition:background .12s ease,border-color .12s ease;`;
18229
- let on = checked;
18230
- const paint = (v) => {
18231
- b.setAttribute("aria-checked", v ? "true" : "false");
18232
- b.style.background = v ? fill : "transparent";
18233
- b.style.borderColor = v ? fill : border;
18234
- b.style.color = v ? t.background : "transparent";
18235
- };
18236
- paint(checked);
18237
- b.addEventListener("click", () => {
18238
- on = !on;
18239
- paint(on);
18240
- onChange(on);
18241
- });
18242
- return b;
19564
+ return new Switch({ id, checked, size: "md", tone: "bright", onChange }).el;
18243
19565
  }
18244
19566
  /** A hoverable ⓘ affordance carrying an input's `tooltip` (themed chrome tip; wraps). */
18245
19567
  infoButton(tooltip2) {
@@ -18251,45 +19573,22 @@ var InputsUI = class {
18251
19573
  return b;
18252
19574
  }
18253
19575
  /** A dropdown over plain string options (value === label) — a thin case of {@link selectPairs}. */
18254
- select(id, options, current, style, emit) {
18255
- return this.selectPairs(id, options.map((o) => ({ value: o, label: o })), current, style, emit);
19576
+ select(id, options, current, emit) {
19577
+ return this.selectPairs(id, options.map((o) => ({ value: o, label: o })), current, emit);
18256
19578
  }
18257
19579
  /** A dropdown whose visible labels differ from the committed values (label ≠ value pairs). */
18258
- selectPairs(id, pairs, current, style, onChange) {
18259
- const sel = document.createElement("select");
18260
- sel.id = id;
18261
- sel.style.cssText = style;
18262
- if (current !== "" && !pairs.some((p) => p.value === current)) {
18263
- const o = document.createElement("option");
18264
- o.value = current;
18265
- o.textContent = current;
18266
- o.selected = true;
18267
- o.disabled = true;
18268
- sel.appendChild(o);
18269
- }
18270
- for (const p of pairs) {
18271
- const o = document.createElement("option");
18272
- o.value = p.value;
18273
- o.textContent = p.label;
18274
- if (p.value === current) o.selected = true;
18275
- sel.appendChild(o);
18276
- }
18277
- this.styleSelect(sel);
18278
- sel.addEventListener("change", () => onChange(sel.value));
18279
- return this.wrapSelectChevron(sel);
18280
- }
18281
- /** Wrap a `<select>` with an inset chevron — native arrows can't be repositioned reliably. */
18282
- wrapSelectChevron(sel) {
18283
- const wrap = document.createElement("div");
18284
- wrap.style.cssText = "position:relative;width:100%;";
18285
- sel.style.paddingRight = "28px";
18286
- const chevron = document.createElement("span");
18287
- chevron.innerHTML = SELECT_CHEVRON_SVG;
18288
- chevron.style.cssText = `position:absolute;right:10px;top:50%;transform:translateY(-50%);pointer-events:none;display:flex;align-items:center;color:${this.strongText()};opacity:0.85;line-height:0;`;
18289
- wrap.append(sel, chevron);
18290
- return wrap;
19580
+ selectPairs(id, pairs, current, onChange) {
19581
+ return new Select({
19582
+ id,
19583
+ options: pairs,
19584
+ value: current,
19585
+ size: "md",
19586
+ theme: this.theme,
19587
+ list: this.listPopover(),
19588
+ onChange
19589
+ }).el;
18291
19590
  }
18292
- /** `input.session` → two HH:MM dropdowns committing a `HHMM-HHMM` session string. */
19591
+ /** `input.session` → two typeable HH:MM comboboxes committing a `HHMM-HHMM` session string. */
18293
19592
  buildSession(id, current, emit) {
18294
19593
  const [start, end] = sessionToTimes(current);
18295
19594
  const wrap = document.createElement("div");
@@ -18297,15 +19596,16 @@ var InputsUI = class {
18297
19596
  let from = start;
18298
19597
  let to = end;
18299
19598
  const commit = () => emit(timesToSession(from, to));
18300
- const startSel = this.timeSelect(id, from, 86, (v) => {
18301
- from = v;
18302
- commit();
18303
- });
18304
- const endSel = this.timeSelect(`${id}-end`, to, 86, (v) => {
18305
- to = v;
18306
- commit();
18307
- });
18308
- wrap.append(startSel, endSel);
19599
+ wrap.append(
19600
+ this.timeCombobox(id, from, 86, (v) => {
19601
+ from = v;
19602
+ commit();
19603
+ }),
19604
+ this.timeCombobox(`${id}-end`, to, 86, (v) => {
19605
+ to = v;
19606
+ commit();
19607
+ })
19608
+ );
18309
19609
  return wrap;
18310
19610
  }
18311
19611
  /** `input.time` → a date picker + an HH:MM dropdown, committing an epoch-ms timestamp. */
@@ -18320,20 +19620,15 @@ var InputsUI = class {
18320
19620
  const ms = Date.parse(`${date}T${time}:00`);
18321
19621
  if (Number.isFinite(ms)) emit(ms);
18322
19622
  };
18323
- const dateInput = document.createElement("input");
18324
- dateInput.type = "date";
18325
- dateInput.id = id;
18326
- dateInput.value = date;
18327
- dateInput.style.cssText = `${this.ctrlStyle()}width:auto;cursor:pointer;`;
18328
- dateInput.addEventListener("change", () => {
18329
- date = dateInput.value;
19623
+ const dateField = this.dateField(id, date, (v) => {
19624
+ date = v;
18330
19625
  commit();
18331
19626
  });
18332
- const timeSel = this.timeSelect(`${id}-time`, time, 86, (v) => {
19627
+ const timeSel = this.timeCombobox(`${id}-time`, time, 86, (v) => {
18333
19628
  time = v;
18334
19629
  commit();
18335
19630
  });
18336
- wrap.append(dateInput, timeSel);
19631
+ wrap.append(dateField, timeSel);
18337
19632
  return wrap;
18338
19633
  }
18339
19634
  /** `input.text_area` → a multi-line textarea, committed on blur. */
@@ -18352,9 +19647,261 @@ var InputsUI = class {
18352
19647
  });
18353
19648
  return ta;
18354
19649
  }
18355
- /** A fixed-width HH:MM dropdown (30-minute steps) used by the session + time controls. */
18356
- timeSelect(id, current, width, onChange) {
18357
- return this.selectPairs(id, TIME_OPTIONS, current, `${this.ctrlStyle()}width:${width}px;cursor:pointer;`, onChange);
19650
+ /** A fixed-width HH:MM combobox (typeable, plus a 30-minute-step dropdown) used by session + time. */
19651
+ timeCombobox(id, current, width, onChange) {
19652
+ const wrap = document.createElement("div");
19653
+ wrap.className = "vela-ind-combo";
19654
+ wrap.style.cssText = `position:relative;width:${width}px;`;
19655
+ const input = document.createElement("input");
19656
+ input.type = "text";
19657
+ input.id = id;
19658
+ input.value = current;
19659
+ input.autocomplete = "off";
19660
+ input.spellcheck = false;
19661
+ input.style.cssText = `${this.ctrlStyle()}padding-right:26px;`;
19662
+ const chevron = document.createElement("button");
19663
+ chevron.type = "button";
19664
+ chevron.tabIndex = -1;
19665
+ chevron.className = "vela-ind-combo-chevron";
19666
+ chevron.setAttribute("aria-label", "Open time list");
19667
+ chevron.innerHTML = CLOCK_SVG;
19668
+ let value = current;
19669
+ const commitTyped = () => {
19670
+ const next = normalizeTimeInput(input.value);
19671
+ if (!next) {
19672
+ input.value = value;
19673
+ return;
19674
+ }
19675
+ input.value = next;
19676
+ if (next !== value) {
19677
+ value = next;
19678
+ onChange(next);
19679
+ }
19680
+ };
19681
+ input.addEventListener("blur", () => {
19682
+ if (isPopoverOpen()) return;
19683
+ commitTyped();
19684
+ });
19685
+ const pick = (v) => {
19686
+ value = v;
19687
+ input.value = v;
19688
+ onChange(v);
19689
+ };
19690
+ const openList = () => {
19691
+ this.choicePop = toggleSelectList(wrap, TIME_OPTIONS, value, pick, {
19692
+ ...this.listPopover(),
19693
+ onClose: () => {
19694
+ this.choicePop = null;
19695
+ }
19696
+ });
19697
+ };
19698
+ input.addEventListener("keydown", (e) => {
19699
+ if (e.key === "Enter") {
19700
+ e.preventDefault();
19701
+ commitTyped();
19702
+ input.blur();
19703
+ }
19704
+ if (e.key === "ArrowDown") {
19705
+ e.preventDefault();
19706
+ openList();
19707
+ }
19708
+ });
19709
+ const open2 = (e) => {
19710
+ e.preventDefault();
19711
+ e.stopPropagation();
19712
+ openList();
19713
+ };
19714
+ input.addEventListener("click", open2);
19715
+ chevron.addEventListener("mousedown", (e) => e.preventDefault());
19716
+ chevron.addEventListener("click", open2);
19717
+ wrap.append(input, chevron);
19718
+ return wrap;
19719
+ }
19720
+ /** A typeable YYYY-MM-DD field that also opens a themed month calendar. */
19721
+ dateField(id, current, onChange) {
19722
+ const wrap = document.createElement("div");
19723
+ wrap.className = "vela-ind-combo";
19724
+ wrap.style.cssText = "position:relative;width:128px;";
19725
+ const input = document.createElement("input");
19726
+ input.type = "text";
19727
+ input.id = id;
19728
+ input.value = current;
19729
+ input.autocomplete = "off";
19730
+ input.spellcheck = false;
19731
+ input.style.cssText = `${this.ctrlStyle()}padding-right:26px;`;
19732
+ const chevron = document.createElement("button");
19733
+ chevron.type = "button";
19734
+ chevron.tabIndex = -1;
19735
+ chevron.className = "vela-ind-combo-chevron";
19736
+ chevron.setAttribute("aria-label", "Open calendar");
19737
+ chevron.innerHTML = CALENDAR_SVG;
19738
+ let value = current;
19739
+ const commitTyped = () => {
19740
+ const next = normalizeDateInput(input.value);
19741
+ if (!next) {
19742
+ input.value = value;
19743
+ return;
19744
+ }
19745
+ input.value = next;
19746
+ if (next !== value) {
19747
+ value = next;
19748
+ onChange(next);
19749
+ }
19750
+ };
19751
+ input.addEventListener("blur", () => {
19752
+ if (isPopoverOpen()) return;
19753
+ commitTyped();
19754
+ });
19755
+ const pick = (v) => {
19756
+ value = v;
19757
+ input.value = v;
19758
+ onChange(v);
19759
+ };
19760
+ input.addEventListener("keydown", (e) => {
19761
+ if (e.key === "Enter") {
19762
+ e.preventDefault();
19763
+ commitTyped();
19764
+ input.blur();
19765
+ }
19766
+ if (e.key === "ArrowDown") {
19767
+ e.preventDefault();
19768
+ this.openCalendar(wrap, value, pick);
19769
+ }
19770
+ });
19771
+ const open2 = (e) => {
19772
+ e.preventDefault();
19773
+ e.stopPropagation();
19774
+ if (openPopoverTrigger() === wrap) {
19775
+ closeOpenPopovers();
19776
+ return;
19777
+ }
19778
+ this.openCalendar(wrap, value, pick);
19779
+ };
19780
+ input.addEventListener("click", open2);
19781
+ chevron.addEventListener("mousedown", (e) => e.preventDefault());
19782
+ chevron.addEventListener("click", open2);
19783
+ wrap.append(input, chevron);
19784
+ return wrap;
19785
+ }
19786
+ /** Numeric field with hover steppers on the right (no fill until a triangle is hovered). */
19787
+ buildNumber(id, current, inp, emit) {
19788
+ return new NumberInput({
19789
+ id,
19790
+ value: current,
19791
+ min: inp.min,
19792
+ max: inp.max,
19793
+ step: inp.step ?? (inp.type === "int" ? 1 : 0.1),
19794
+ integer: inp.type === "int",
19795
+ size: "md",
19796
+ commit: "blur",
19797
+ onChange: emit
19798
+ }).el;
19799
+ }
19800
+ /** Placement shared by the indicator dialog's dropdowns and the time combobox. */
19801
+ listPopover() {
19802
+ return {
19803
+ theme: this.theme,
19804
+ matchWidth: true,
19805
+ boundary: this.dialog ?? "viewport",
19806
+ boundaryInset: 8,
19807
+ gap: 4
19808
+ };
19809
+ }
19810
+ /** Open a themed month calendar under `anchor` — same surface + shadow as the choice list. */
19811
+ openCalendar(anchor, current, onPick) {
19812
+ const parsed = parseIsoDate(current);
19813
+ let year = parsed?.getFullYear() ?? (/* @__PURE__ */ new Date()).getFullYear();
19814
+ let month = parsed?.getMonth() ?? (/* @__PURE__ */ new Date()).getMonth();
19815
+ const selected = parsed ? isoDate(parsed) : current;
19816
+ const pop = new Popover({
19817
+ trigger: anchor,
19818
+ theme: this.theme,
19819
+ className: "vela-ind-cal",
19820
+ boundary: this.dialog ?? "viewport",
19821
+ boundaryInset: 8,
19822
+ gap: 4,
19823
+ onClose: () => {
19824
+ this.calendarPop = null;
19825
+ this.calendarAnchor = null;
19826
+ },
19827
+ content: (el) => {
19828
+ const title = document.createElement("div");
19829
+ title.className = "vela-ind-cal-title";
19830
+ const prev2 = document.createElement("button");
19831
+ prev2.type = "button";
19832
+ prev2.className = "vela-ind-cal-nav";
19833
+ prev2.setAttribute("aria-label", "Previous month");
19834
+ prev2.innerHTML = iconAt("chevron-left", 14);
19835
+ const next = document.createElement("button");
19836
+ next.type = "button";
19837
+ next.className = "vela-ind-cal-nav";
19838
+ next.setAttribute("aria-label", "Next month");
19839
+ next.innerHTML = iconAt("chevron-right", 14);
19840
+ const head = document.createElement("div");
19841
+ head.className = "vela-ind-cal-head";
19842
+ head.append(prev2, title, next);
19843
+ const week = document.createElement("div");
19844
+ week.className = "vela-ind-cal-week";
19845
+ for (const d of WEEKDAY_LABELS) {
19846
+ const cell = document.createElement("span");
19847
+ cell.textContent = d;
19848
+ week.appendChild(cell);
19849
+ }
19850
+ const grid = document.createElement("div");
19851
+ grid.className = "vela-ind-cal-grid";
19852
+ const paint = () => {
19853
+ title.textContent = `${MONTH_LABELS[month]} ${year}`;
19854
+ grid.replaceChildren();
19855
+ const first = new Date(year, month, 1);
19856
+ const startPad = first.getDay();
19857
+ const days = new Date(year, month + 1, 0).getDate();
19858
+ const today = isoDate(/* @__PURE__ */ new Date());
19859
+ for (let i = 0; i < startPad; i++) {
19860
+ const blank = document.createElement("span");
19861
+ blank.className = "vela-ind-cal-blank";
19862
+ grid.appendChild(blank);
19863
+ }
19864
+ for (let day = 1; day <= days; day++) {
19865
+ const iso = `${year}-${String(month + 1).padStart(2, "0")}-${String(day).padStart(2, "0")}`;
19866
+ const b = document.createElement("button");
19867
+ b.type = "button";
19868
+ b.className = "vela-ind-cal-day";
19869
+ b.textContent = String(day);
19870
+ if (iso === selected) b.dataset.checked = "1";
19871
+ if (iso === today) b.dataset.today = "1";
19872
+ b.addEventListener("click", (e) => {
19873
+ e.stopPropagation();
19874
+ pop.hide();
19875
+ onPick(iso);
19876
+ });
19877
+ grid.appendChild(b);
19878
+ }
19879
+ };
19880
+ prev2.addEventListener("click", (e) => {
19881
+ e.stopPropagation();
19882
+ month -= 1;
19883
+ if (month < 0) {
19884
+ month = 11;
19885
+ year -= 1;
19886
+ }
19887
+ paint();
19888
+ });
19889
+ next.addEventListener("click", (e) => {
19890
+ e.stopPropagation();
19891
+ month += 1;
19892
+ if (month > 11) {
19893
+ month = 0;
19894
+ year += 1;
19895
+ }
19896
+ paint();
19897
+ });
19898
+ paint();
19899
+ el.append(head, week, grid);
19900
+ }
19901
+ });
19902
+ this.calendarPop = pop;
19903
+ this.calendarAnchor = anchor;
19904
+ pop.show();
18358
19905
  }
18359
19906
  /**
18360
19907
  * `input.symbol` → a field that opens the host's ticker-selection UI when a picker is wired
@@ -18382,7 +19929,7 @@ var InputsUI = class {
18382
19929
  }
18383
19930
  /** Shared field chrome for text / number / select controls (fills its wrapper's width). */
18384
19931
  ctrlStyle() {
18385
- return `width:100%;box-sizing:border-box;height:32px;background:transparent;border:1px solid ${this.neutralBorder()};color:${this.strongText()};border-radius:6px;padding:0 8px;font-size:13px;font-family:inherit;outline:none;`;
19932
+ return `width:100%;box-sizing:border-box;height:34px;background:transparent;border:1px solid var(--vela-border-strong);color:${this.strongText()};border-radius:6px;padding:0 8px;font-size:14px;font-family:inherit;outline:none;`;
18386
19933
  }
18387
19934
  /** Whether the active theme is dark (drives the dialog's `color-scheme`). */
18388
19935
  isDarkTheme() {
@@ -18401,21 +19948,6 @@ var InputsUI = class {
18401
19948
  strongText() {
18402
19949
  return "var(--vela-fg-bright)";
18403
19950
  }
18404
- /**
18405
- * Paint a `<select>` + its `<option>`s so the native dropdown popup matches the dialog: the
18406
- * option list's background is the dialog color and its text is the strong foreground. `color-scheme`
18407
- * alone doesn't theme the popup in every embedded browser (some render it white with white text —
18408
- * invisible), so the colors are set explicitly here.
18409
- */
18410
- styleSelect(sel) {
18411
- const bg = this.theme.background;
18412
- const fg = this.strongText();
18413
- sel.style.backgroundColor = bg;
18414
- for (const o of Array.from(sel.options)) {
18415
- o.style.backgroundColor = bg;
18416
- o.style.color = fg;
18417
- }
18418
- }
18419
19951
  dialogButton(label, primary, onClick) {
18420
19952
  const b = document.createElement("button");
18421
19953
  b.type = "button";
@@ -18429,6 +19961,21 @@ function nameOf(inp) {
18429
19961
  const t = inp.title;
18430
19962
  return t.length > 0 ? t.charAt(0).toUpperCase() + t.slice(1) : "";
18431
19963
  }
19964
+ var DEFAULT_INPUT_TAB = "Inputs";
19965
+ function tabInputs(inputs) {
19966
+ const byTab = /* @__PURE__ */ new Map();
19967
+ for (const inp of inputs) {
19968
+ const name = inp.tab && inp.tab.length > 0 ? inp.tab : DEFAULT_INPUT_TAB;
19969
+ if (!byTab.has(name)) byTab.set(name, []);
19970
+ byTab.get(name).push(inp);
19971
+ }
19972
+ const names = [...byTab.keys()];
19973
+ if (names.includes(DEFAULT_INPUT_TAB)) {
19974
+ names.splice(names.indexOf(DEFAULT_INPUT_TAB), 1);
19975
+ names.unshift(DEFAULT_INPUT_TAB);
19976
+ }
19977
+ return names.map((name) => ({ name, inputs: byTab.get(name) }));
19978
+ }
18432
19979
  function groupInputs(inputs) {
18433
19980
  const order = [];
18434
19981
  const byGroup = /* @__PURE__ */ new Map();
@@ -18457,9 +20004,10 @@ function groupInputs(inputs) {
18457
20004
  });
18458
20005
  }
18459
20006
  var CHECK_SVG = iconAt("check", 12);
18460
- var SELECT_CHEVRON_SVG = iconAt("chevron-down", 16);
20007
+ var CALENDAR_SVG = iconAt("calendar", 14);
20008
+ var CLOCK_SVG = iconAt("clock", 14);
18461
20009
  var DIALOG_STYLE_ID2 = "vela-ind-dialog-styles";
18462
- var DIALOG_STYLE_REV = "10";
20010
+ var DIALOG_STYLE_REV = "25";
18463
20011
  function ensureDialogStyles() {
18464
20012
  if (typeof document === "undefined") return;
18465
20013
  const existing = document.getElementById(DIALOG_STYLE_ID2);
@@ -18468,30 +20016,47 @@ function ensureDialogStyles() {
18468
20016
  s.id = DIALOG_STYLE_ID2;
18469
20017
  s.dataset.rev = DIALOG_STYLE_REV;
18470
20018
  s.textContent = `
18471
- .vela-ind-dialog input[type=color]{-webkit-appearance:none;appearance:none;}
18472
- .vela-ind-dialog input[type=color]::-webkit-color-swatch-wrapper{padding:0;}
18473
- .vela-ind-dialog input[type=color]::-webkit-color-swatch{border:none;border-radius:4px;}
18474
- .vela-ind-dialog input[type=color]::-moz-color-swatch{border:none;border-radius:4px;}
18475
- .vela-ind-dialog input[type=text],.vela-ind-dialog input[type=number],.vela-ind-dialog select{transition:border-color .12s ease,box-shadow .12s ease;}
18476
- .vela-ind-dialog select{-webkit-appearance:none;appearance:none;}
18477
- .vela-ind-dialog input[type=text]:focus,.vela-ind-dialog input[type=number]:focus,.vela-ind-dialog select:focus{border-color:var(--vela-focus);box-shadow:0 0 0 3px var(--vela-focus-soft);}
18478
- .vela-ind-dialog ::-webkit-scrollbar{width:9px;}
20019
+ .vela-ind-dialog input[type=text],.vela-ind-dialog input[type=number],.vela-ind-dialog input[type=date]{transition:border-color .12s ease,box-shadow .12s ease;}
20020
+ .vela-ind-dialog input[type=text]:hover,.vela-ind-dialog input[type=number]:hover,.vela-ind-dialog input[type=date]:hover{border-color:var(--vela-fg-muted);}
20021
+ .vela-ind-dialog input[type=text]:focus,.vela-ind-dialog input[type=number]:focus,.vela-ind-dialog input[type=date]:focus{border-color:var(--vela-focus);box-shadow:0 0 0 3px var(--vela-focus-soft);}
20022
+ .vela-ind-combo-chevron{position:absolute;right:0;top:0;bottom:0;width:26px;border:none;background:transparent;color:inherit;opacity:0.55;cursor:pointer;display:flex;align-items:center;justify-content:center;padding:0;}
20023
+ .vela-ind-combo-chevron:hover{opacity:0.9;}
20024
+ .vela-ind-cal{background:var(--vela-bg);color:var(--vela-fg);border:none;border-radius:6px;box-shadow:var(--vela-shadow);font:14px var(--vela-font);padding:10px 12px;user-select:none;}
20025
+ .vela-ind-cal-head{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:8px;}
20026
+ .vela-ind-cal-title{flex:1;text-align:center;font-weight:600;font-size:14px;color:var(--vela-fg-bright);}
20027
+ .vela-ind-cal-nav{width:24px;height:24px;border:none;background:transparent;color:var(--vela-fg-muted);border-radius:4px;padding:0;cursor:pointer;display:inline-flex;align-items:center;justify-content:center;}
20028
+ .vela-ind-cal-nav:hover{background:var(--vela-hover);color:var(--vela-fg-bright);}
20029
+ .vela-ind-cal-week,.vela-ind-cal-grid{display:grid;grid-template-columns:repeat(7,28px);gap:2px;}
20030
+ .vela-ind-cal-week{margin-bottom:4px;color:var(--vela-fg-muted);font-size:11px;text-align:center;}
20031
+ .vela-ind-cal-week span{line-height:20px;}
20032
+ .vela-ind-cal-blank{width:28px;height:28px;}
20033
+ .vela-ind-cal-day{width:28px;height:28px;border:none;background:transparent;color:inherit;border-radius:4px;padding:0;cursor:pointer;font:inherit;font-size:14px;}
20034
+ .vela-ind-cal-day:hover{background:var(--vela-hover);}
20035
+ .vela-ind-cal-day[data-checked]{background:var(--vela-hover-strong);color:var(--vela-fg-bright);}
20036
+ .vela-ind-cal-day[data-today]:not([data-checked]){box-shadow:inset 0 0 0 1px var(--vela-border-strong);}
20037
+ .vela-ind-dialog ::-webkit-scrollbar{width:9px;height:9px;}
18479
20038
  .vela-ind-dialog ::-webkit-scrollbar-thumb{background:var(--vela-scroll);border-radius:4px;border:2px solid transparent;background-clip:padding-box;}
18480
20039
  .vela-ind-dialog ::-webkit-scrollbar-track{background:transparent;}
20040
+ .vela-ind-dialog ::-webkit-scrollbar-button{display:none;width:0;height:0;}
20041
+ .vela-ind-tab{transition:color var(--vela-dur-fast) ease,border-color var(--vela-dur-fast) ease;}
20042
+ .vela-ind-tab:not(.vela-ind-tab-active):hover{color:var(--vela-fg-bright);}
18481
20043
  .vela-ind-hint{background:var(--vela-hover);color:var(--vela-fg-muted);transition:background var(--vela-dur-fast) ease,color var(--vela-dur-fast) ease;}
18482
20044
  .vela-ind-hint:hover{background:var(--vela-active);color:var(--vela-fg-bright);}
18483
20045
  .vela-ind-ctl,.vela-ind-close{background-color:transparent;color:inherit;transition:color var(--vela-dur-fast) ease,background-color var(--vela-dur-fast) ease;}
18484
20046
  .vela-ind-ctl svg,.vela-ind-close svg{width:${LEGEND_ICON_PX}px;height:${LEGEND_ICON_PX}px;display:block;flex:none;stroke-width:1;}
18485
20047
  .vela-ind-ctl:hover{background-color:var(--vela-hover-strong);}
18486
20048
  .vela-ind-close:hover{color:var(--vela-danger) !important;background-color:var(--vela-hover-strong);}
20049
+ .vela-ind-dlg-close{cursor:pointer;display:inline-flex;align-items:center;justify-content:center;background:transparent;border:none;color:var(--vela-fg-muted);line-height:0;width:30px;height:30px;flex:0 0 auto;border-radius:var(--vela-radius-sm);transition:background var(--vela-dur-fast) ease,color var(--vela-dur-fast) ease;}
20050
+ .vela-ind-dlg-close:hover{background:var(--vela-hover);color:var(--vela-fg-bright);}
20051
+ .vela-ind-dlg-close svg{width:15px;height:15px;display:block;flex:none;}
18487
20052
  .vela-ind-fold{transition:border-color var(--vela-dur-fast) ease,opacity var(--vela-dur-fast) ease;}
18488
20053
  .vela-ind-fold:hover{border-color:var(--vela-border-strong);opacity:0.9;}
18489
20054
  .vela-ind-menuitem{background:transparent;transition:background var(--vela-dur-fast) ease;}
18490
20055
  .vela-ind-menuitem:hover{background:var(--vela-hover-strong);}
18491
- .vela-ind-btn{cursor:pointer;padding:7px 14px;border-radius:var(--vela-radius-md);border:1px solid transparent;background:transparent;color:var(--vela-fg-muted);font-weight:600;font-size:13px;font-family:inherit;transition:background var(--vela-dur-fast) ease,color var(--vela-dur-fast) ease,opacity var(--vela-dur-fast) ease;}
18492
- .vela-ind-btn:hover{background:var(--vela-hover);color:var(--vela-fg-bright);}
18493
- .vela-ind-btn-primary{padding:7px 16px;border-color:var(--vela-selected-bg);background:var(--vela-selected-bg);color:var(--vela-selected-fg);}
18494
- .vela-ind-btn-primary:hover{background:var(--vela-selected-bg);color:var(--vela-selected-fg);opacity:0.85;}`;
20056
+ .vela-ind-btn{cursor:pointer;height:34px;padding:0 11px;border-radius:6px;border:1px solid var(--vela-border-strong);background:transparent;color:var(--vela-fg);font-weight:400;font-size:14px;font-family:inherit;transition:background var(--vela-dur-fast) ease,color var(--vela-dur-fast) ease,opacity var(--vela-dur-fast) ease,border-color var(--vela-dur-fast) ease;}
20057
+ .vela-ind-btn:hover{background:var(--vela-hover);color:var(--vela-fg-bright);border-color:var(--vela-fg-muted);}
20058
+ .vela-ind-btn-primary{border-color:var(--vela-selected-bg);background:var(--vela-selected-bg);color:var(--vela-selected-fg);}
20059
+ .vela-ind-btn-primary:hover{background:var(--vela-selected-bg);color:var(--vela-selected-fg);opacity:0.85;border-color:var(--vela-selected-bg);}`;
18495
20060
  if (!existing) document.head.appendChild(s);
18496
20061
  }
18497
20062
  var TIMEFRAME_OPTIONS = [
@@ -18516,6 +20081,37 @@ var TIME_OPTIONS = Array.from({ length: 48 }, (_, i) => {
18516
20081
  const v = `${hh}:${mm}`;
18517
20082
  return { value: v, label: v };
18518
20083
  });
20084
+ var MONTH_LABELS = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"];
20085
+ var WEEKDAY_LABELS = ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"];
20086
+ function normalizeDateInput(raw) {
20087
+ const t = raw.trim();
20088
+ const m = /^(\d{4})-(\d{1,2})-(\d{1,2})$/.exec(t);
20089
+ if (!m) return null;
20090
+ const y = Number(m[1]);
20091
+ const mo = Number(m[2]);
20092
+ const d = Number(m[3]);
20093
+ const dt = new Date(y, mo - 1, d);
20094
+ if (dt.getFullYear() !== y || dt.getMonth() !== mo - 1 || dt.getDate() !== d) return null;
20095
+ return isoDate(dt);
20096
+ }
20097
+ function parseIsoDate(raw) {
20098
+ const iso = normalizeDateInput(raw);
20099
+ if (!iso) return null;
20100
+ const [y, mo, d] = iso.split("-").map(Number);
20101
+ return new Date(y, mo - 1, d);
20102
+ }
20103
+ function isoDate(d) {
20104
+ return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
20105
+ }
20106
+ function normalizeTimeInput(raw) {
20107
+ const t = raw.trim();
20108
+ const m = /^(\d{1,2}):(\d{2})$/.exec(t) ?? /^(\d{2})(\d{2})$/.exec(t);
20109
+ if (!m) return null;
20110
+ const hh = Number(m[1]);
20111
+ const mm = Number(m[2]);
20112
+ if (hh > 23 || mm > 59) return null;
20113
+ return `${String(hh).padStart(2, "0")}:${String(mm).padStart(2, "0")}`;
20114
+ }
18519
20115
  function sessionToTimes(session) {
18520
20116
  const m = /^(\d{2})(\d{2})-(\d{2})(\d{2})$/.exec(session.trim());
18521
20117
  if (!m) return ["09:00", "16:00"];
@@ -22347,6 +23943,12 @@ function extendEndpoints(x1, y1, x2, y2, extend, width, height) {
22347
23943
  const rx = right ? width + 2 : Math.max(x1, x2);
22348
23944
  return [lx, yAt(lx), rx, yAt(rx)];
22349
23945
  }
23946
+ function uprightLineAngle(x1, y1, x2, y2) {
23947
+ let a = Math.atan2(y2 - y1, x2 - x1);
23948
+ if (a > Math.PI / 2) a -= Math.PI;
23949
+ if (a < -Math.PI / 2) a += Math.PI;
23950
+ return a;
23951
+ }
22350
23952
  function parseColor2(c) {
22351
23953
  const s = c.trim();
22352
23954
  let m = /^#([0-9a-f]{3})$/i.exec(s);
@@ -23520,367 +25122,71 @@ function formatStamp(ms, timeZone) {
23520
25122
  return `${pad(d.getUTCMonth() + 1)}-${pad(d.getUTCDate())} ${pad(d.getUTCHours())}:${pad(d.getUTCMinutes())}`;
23521
25123
  }
23522
25124
 
23523
- // src/renderers/native/drawings/colorPicker.ts
23524
- function splitColor(color) {
23525
- const s = String(color ?? "").trim();
23526
- let m = /^#([0-9a-f]{6})([0-9a-f]{2})?$/i.exec(s);
23527
- if (m) return { hex6: `#${m[1].toLowerCase()}`, alpha: m[2] ? parseInt(m[2], 16) / 255 : 1 };
23528
- m = /^#([0-9a-f]{3})$/i.exec(s);
23529
- if (m) {
23530
- const h = m[1].toLowerCase();
23531
- return { hex6: `#${h[0]}${h[0]}${h[1]}${h[1]}${h[2]}${h[2]}`, alpha: 1 };
23532
- }
23533
- const rgba = /^rgba?\(([^)]+)\)/i.exec(s);
23534
- if (rgba) {
23535
- const p = rgba[1].split(",").map((v) => v.trim());
23536
- const hx = (n) => Math.max(0, Math.min(255, Math.round(n))).toString(16).padStart(2, "0");
23537
- const a = p[3] != null ? Math.max(0, Math.min(1, parseFloat(p[3]))) : 1;
23538
- return { hex6: `#${hx(parseInt(p[0] ?? "0", 10))}${hx(parseInt(p[1] ?? "0", 10))}${hx(parseInt(p[2] ?? "0", 10))}`, alpha: a };
23539
- }
23540
- return { hex6: DEFAULT_DRAWING_COLOR, alpha: 1 };
23541
- }
23542
- function combineColor(hex6, alpha) {
23543
- const a = Math.max(0, Math.min(1, alpha));
23544
- if (a >= 0.999) return hex6;
23545
- return `${hex6}${Math.round(a * 255).toString(16).padStart(2, "0")}`;
23546
- }
23547
- function blendOver(fg, bg, alpha) {
23548
- const rgb = (c) => {
23549
- const n = parseInt(splitColor(c).hex6.slice(1), 16);
23550
- return [n >> 16 & 255, n >> 8 & 255, n & 255];
23551
- };
23552
- const [fr, fgc, fb] = rgb(fg);
23553
- const [br, bgc, bb] = rgb(bg);
23554
- const a = Math.max(0, Math.min(1, alpha));
23555
- const h = (n) => Math.round(n).toString(16).padStart(2, "0");
23556
- return `#${h(fr * a + br * (1 - a))}${h(fgc * a + bgc * (1 - a))}${h(fb * a + bb * (1 - a))}`;
23557
- }
23558
- function hslHex(h, s, l) {
23559
- const sn = s / 100;
23560
- const ln = l / 100;
23561
- const k = (n) => (n + h / 30) % 12;
23562
- const a = sn * Math.min(ln, 1 - ln);
23563
- const f = (n) => ln - a * Math.max(-1, Math.min(k(n) - 3, Math.min(9 - k(n), 1)));
23564
- const to = (x) => Math.round(x * 255).toString(16).padStart(2, "0");
23565
- return `#${to(f(0))}${to(f(8))}${to(f(4))}`;
23566
- }
23567
- function buildPalette() {
23568
- const grays = [100, 90, 80, 68, 56, 45, 35, 25, 14, 0].map((l) => hslHex(0, 0, l));
23569
- const hues = [4, 28, 50, 96, 140, 174, 200, 224, 270, 322];
23570
- const levels = [
23571
- [82, 56],
23572
- // vivid
23573
- [58, 84],
23574
- // light pastel
23575
- [62, 74],
23576
- [66, 62],
23577
- [70, 50],
23578
- [64, 39],
23579
- [54, 29]
23580
- // dark
23581
- ];
23582
- return [grays, ...levels.map(([s, l]) => hues.map((h) => hslHex(h, s, l)))];
23583
- }
23584
- var PALETTE = buildPalette();
23585
- function transparencyChecker(size) {
23586
- return `repeating-conic-gradient(#9aa0a6 0% 25%, #d3d6da 0% 50%) 0 0 / ${size}px ${size}px`;
23587
- }
23588
- var CHECKER = transparencyChecker(10);
23589
- var recents = [ACCENT, BULLISH, BEARISH, WARNING, NEUTRAL];
23590
- function addRecent(hex6) {
23591
- const i = recents.findIndex((c) => c.toLowerCase() === hex6.toLowerCase());
23592
- if (i >= 0) recents.splice(i, 1);
23593
- recents.unshift(hex6);
23594
- if (recents.length > 10) recents.length = 10;
23595
- }
23596
- function buildColorPicker(color, theme, onChange) {
23597
- const parsed = splitColor(color);
23598
- let curHex = parsed.hex6;
23599
- let curAlpha = parsed.alpha;
23600
- const root = document.createElement("div");
23601
- root.style.cssText = "display:flex;flex-direction:column;gap:9px;width:236px;";
23602
- const grid = document.createElement("div");
23603
- grid.style.cssText = "display:grid;grid-template-columns:repeat(10,1fr);gap:4px;";
23604
- const swatches = [];
23605
- for (const row of PALETTE) {
23606
- for (const c of row) {
23607
- const sw = document.createElement("button");
23608
- sw.type = "button";
23609
- sw.dataset.c = c;
23610
- sw.style.cssText = `width:100%;aspect-ratio:1;border-radius:4px;border:none;cursor:pointer;background:${c};padding:0;`;
23611
- sw.addEventListener("click", (e) => {
23612
- e.stopPropagation();
23613
- pickHex(c);
23614
- });
23615
- grid.appendChild(sw);
23616
- swatches.push(sw);
23617
- }
23618
- }
23619
- const paintSelection = () => {
23620
- for (const sw of swatches) {
23621
- const on = (sw.dataset.c ?? "").toLowerCase() === curHex.toLowerCase();
23622
- sw.style.outline = on ? `2px solid ${theme.textColor}` : "none";
23623
- sw.style.outlineOffset = "2px";
23624
- sw.style.zIndex = on ? "1" : "";
23625
- sw.style.position = on ? "relative" : "";
23626
- }
23627
- };
23628
- const recentRow = document.createElement("div");
23629
- recentRow.style.cssText = "display:flex;align-items:center;gap:5px;flex-wrap:wrap;border-top:1px solid var(--vela-border);padding-top:9px;";
23630
- const renderRecents = () => {
23631
- recentRow.replaceChildren();
23632
- for (const c of recents) {
23633
- const sw = document.createElement("button");
23634
- sw.type = "button";
23635
- 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;`;
23636
- sw.addEventListener("click", (e) => {
23637
- e.stopPropagation();
23638
- pickHex(c);
23639
- });
23640
- recentRow.appendChild(sw);
23641
- }
23642
- const add = document.createElement("label");
23643
- 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;`;
23644
- add.textContent = "+";
23645
- const input = document.createElement("input");
23646
- input.type = "color";
23647
- input.value = curHex;
23648
- input.style.cssText = "position:absolute;inset:0;opacity:0;cursor:pointer;";
23649
- input.addEventListener("input", () => pickHex(input.value, true));
23650
- add.appendChild(input);
23651
- recentRow.appendChild(add);
23652
- };
23653
- const opLabel = document.createElement("div");
23654
- opLabel.textContent = "Opacity";
23655
- opLabel.style.cssText = `font:11px ${theme.fontFamily};color:var(--vela-fg-muted);`;
23656
- const opRow = document.createElement("div");
23657
- opRow.style.cssText = "display:flex;align-items:center;gap:10px;";
23658
- const track = document.createElement("div");
23659
- track.style.cssText = "flex:1;position:relative;height:13px;border-radius:7px;cursor:pointer;";
23660
- const knob = document.createElement("div");
23661
- 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;";
23662
- track.appendChild(knob);
23663
- const pctBox = document.createElement("div");
23664
- 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;`;
23665
- opRow.append(track, pctBox);
23666
- const paintOpacity = () => {
23667
- track.style.background = `linear-gradient(to right, ${curHex}00, ${curHex}ff), ${CHECKER}`;
23668
- knob.style.left = `${curAlpha * 100}%`;
23669
- pctBox.textContent = `${Math.round(curAlpha * 100)}%`;
23670
- };
23671
- let dragging = false;
23672
- const onDrag = (clientX) => {
23673
- const r = track.getBoundingClientRect();
23674
- curAlpha = Math.max(0, Math.min(1, (clientX - r.left) / r.width));
23675
- paintOpacity();
23676
- emit();
23677
- };
23678
- track.addEventListener("pointerdown", (e) => {
23679
- e.stopPropagation();
23680
- dragging = true;
23681
- track.setPointerCapture(e.pointerId);
23682
- onDrag(e.clientX);
23683
- });
23684
- track.addEventListener("pointermove", (e) => {
23685
- if (dragging) onDrag(e.clientX);
23686
- });
23687
- track.addEventListener("pointerup", () => dragging = false);
23688
- function emit() {
23689
- onChange(combineColor(curHex, curAlpha));
23690
- }
23691
- function pickHex(hex, custom = false) {
23692
- curHex = splitColor(hex).hex6;
23693
- addRecent(curHex);
23694
- paintSelection();
23695
- paintOpacity();
23696
- if (custom) renderRecents();
23697
- emit();
23698
- }
23699
- renderRecents();
23700
- paintSelection();
23701
- paintOpacity();
23702
- root.append(grid, recentRow, opLabel, opRow);
23703
- return root;
23704
- }
23705
-
23706
- // src/renderers/native/chrome/ColorField.ts
23707
- var CHECKER2 = transparencyChecker(8);
23708
- var STYLE_ID22 = "vela-color-field";
23709
- function ensureStyles4() {
23710
- if (typeof document === "undefined" || document.getElementById(STYLE_ID22)) return;
23711
- const st = document.createElement("style");
23712
- st.id = STYLE_ID22;
23713
- st.textContent = `
23714
- .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;}
23715
- .vela-color-field:hover{border-color:var(--vela-fg-muted);}
23716
- .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);}
23717
- .vela-color-field-pop{position:fixed;z-index:6000;background:var(--vela-surface-overlay);border:1px solid var(--vela-border);border-radius:var(--vela-radius-lg);box-shadow:var(--vela-shadow);padding:10px;}
23718
- `;
23719
- document.head.appendChild(st);
23720
- }
23721
- var openPopover = null;
23722
- function closeOpen() {
23723
- if (!openPopover) return;
23724
- document.removeEventListener("pointerdown", openPopover.onOutside, true);
23725
- window.removeEventListener("resize", openPopover.reflow, true);
23726
- openPopover.el.remove();
23727
- openPopover = null;
23728
- }
23729
- function place(pop, trigger) {
23730
- const r = trigger.getBoundingClientRect();
23731
- const w = pop.offsetWidth;
23732
- const h = pop.offsetHeight;
23733
- let left = r.right - w;
23734
- let top = r.bottom + 6;
23735
- if (left < 6) left = 6;
23736
- if (left + w > window.innerWidth - 6) left = window.innerWidth - w - 6;
23737
- if (top + h > window.innerHeight - 6) top = r.top - h - 6;
23738
- if (top < 6) top = 6;
23739
- pop.style.left = `${Math.round(left)}px`;
23740
- pop.style.top = `${Math.round(top)}px`;
23741
- }
23742
- function colorField(theme, getVal, onVal) {
23743
- ensureStyles4();
23744
- const trigger = document.createElement("button");
23745
- trigger.type = "button";
23746
- trigger.className = "vela-color-field";
23747
- const swatch = document.createElement("span");
23748
- swatch.className = "vela-color-field-swatch";
23749
- trigger.appendChild(swatch);
23750
- const paint = () => {
23751
- const v = getVal();
23752
- swatch.style.background = `linear-gradient(${v}, ${v}), ${CHECKER2}`;
23753
- };
23754
- paint();
23755
- trigger.addEventListener("vela-sync", paint);
23756
- trigger.addEventListener("click", (e) => {
23757
- e.stopPropagation();
23758
- if (openPopover && openPopover.trigger === trigger) {
23759
- closeOpen();
23760
- return;
23761
- }
23762
- closeOpen();
23763
- const pop = document.createElement("div");
23764
- pop.className = "vela-color-field-pop";
23765
- applyChromeTokens(pop, theme);
23766
- pop.addEventListener("pointerdown", (ev) => ev.stopPropagation());
23767
- pop.appendChild(
23768
- buildColorPicker(getVal(), theme, (val) => {
23769
- onVal(val);
23770
- paint();
23771
- })
23772
- );
23773
- document.body.appendChild(pop);
23774
- place(pop, trigger);
23775
- const onOutside = (ev) => {
23776
- const t = ev.target;
23777
- if (pop.contains(t) || trigger.contains(t)) return;
23778
- closeOpen();
23779
- };
23780
- const reflow = () => place(pop, trigger);
23781
- setTimeout(() => document.addEventListener("pointerdown", onOutside, true), 0);
23782
- window.addEventListener("resize", reflow, true);
23783
- openPopover = { el: pop, trigger, onOutside, reflow };
23784
- });
23785
- return trigger;
23786
- }
23787
- function closeColorPopover() {
23788
- closeOpen();
23789
- }
23790
-
23791
25125
  // src/renderers/native/chrome/WidthField.ts
23792
25126
  var WIDTH_FIELD_OPTIONS = [1, 2, 3, 4, 5];
23793
- var STYLE_ID23 = "vela-width-field";
23794
- function ensureStyles5() {
23795
- if (typeof document === "undefined" || document.getElementById(STYLE_ID23)) return;
23796
- const st = document.createElement("style");
23797
- st.id = STYLE_ID23;
23798
- st.textContent = `
25127
+ var STYLE_ID22 = "vela-width-field";
25128
+ var WIDTH_CSS = `
23799
25129
  .vela-width-field{height:28px;padding:0 8px;border:1px solid var(--vela-border-strong);border-radius:var(--vela-radius-sm);background:var(--vela-surface-elev);cursor:pointer;display:inline-flex;align-items:center;gap:8px;flex:none;color:var(--vela-fg);font-family:inherit;outline:none;}
23800
25130
  .vela-width-field:hover{border-color:var(--vela-fg-muted);}
23801
- .vela-width-field-pop{position:fixed;z-index:6000;background:var(--vela-surface-overlay);border:1px solid var(--vela-border);border-radius:var(--vela-radius-lg);box-shadow:var(--vela-shadow);padding:4px;display:flex;flex-direction:column;gap:1px;color:var(--vela-fg);}
25131
+ .vela-width-field-caret{display:flex;color:var(--vela-fg-muted);}
25132
+ .vela-width-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:4px;display:flex;flex-direction:column;gap:1px;color:var(--vela-fg);}
23802
25133
  .vela-width-field-item{display:flex;align-items:center;gap:8px;min-width:96px;padding:5px 8px;border:none;border-radius:5px;background:transparent;color:inherit;cursor:pointer;text-align:left;font:inherit;}
23803
25134
  .vela-width-field-item:hover{background:var(--vela-hover-strong);}
23804
25135
  .vela-width-field-item[data-active='1']{background:var(--vela-active);}
23805
25136
  `;
23806
- document.head.appendChild(st);
23807
- }
23808
25137
  function lineGlyph(width) {
23809
25138
  return `<svg width="22" height="14" viewBox="0 0 22 14" fill="none"><line x1="2" y1="7" x2="20" y2="7" stroke="currentColor" stroke-width="${width}" stroke-linecap="round"/></svg>`;
23810
25139
  }
23811
- var CHEVRON = `<svg width="8" height="5" viewBox="0 0 8 5" fill="none"><path d="M1 1l3 3 3-3" stroke="#868a96" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>`;
23812
- var openPopover2 = null;
23813
- function closeOpen2() {
23814
- if (!openPopover2) return;
23815
- document.removeEventListener("pointerdown", openPopover2.onOutside, true);
23816
- window.removeEventListener("resize", openPopover2.reflow, true);
23817
- openPopover2.el.remove();
23818
- openPopover2 = null;
23819
- }
23820
- function place2(pop, trigger) {
23821
- const r = trigger.getBoundingClientRect();
23822
- const w = pop.offsetWidth;
23823
- const h = pop.offsetHeight;
23824
- let left = r.right - w;
23825
- let top = r.bottom + 6;
23826
- if (left < 6) left = 6;
23827
- if (left + w > window.innerWidth - 6) left = window.innerWidth - w - 6;
23828
- if (top + h > window.innerHeight - 6) top = r.top - h - 6;
23829
- if (top < 6) top = 6;
23830
- pop.style.left = `${Math.round(left)}px`;
23831
- pop.style.top = `${Math.round(top)}px`;
23832
- }
25140
+ var CHEVRON = `<svg width="8" height="5" viewBox="0 0 8 5" fill="none"><path d="M1 1l3 3 3-3" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>`;
23833
25141
  function widthField(theme, getVal, onVal) {
23834
- ensureStyles5();
25142
+ injectStyles(STYLE_ID22, WIDTH_CSS, document);
23835
25143
  const trigger = document.createElement("button");
23836
25144
  trigger.type = "button";
23837
25145
  trigger.className = "vela-width-field";
23838
25146
  const paint = () => {
23839
- trigger.innerHTML = `<span style="display:flex;">${lineGlyph(getVal())}</span><span style="display:flex;">${CHEVRON}</span>`;
25147
+ trigger.innerHTML = `<span style="display:flex;">${lineGlyph(getVal())}</span><span class="vela-width-field-caret">${CHEVRON}</span>`;
23840
25148
  };
23841
25149
  paint();
25150
+ trigger.addEventListener("pointerdown", (e) => {
25151
+ e.preventDefault();
25152
+ trigger.focus({ preventScroll: true });
25153
+ });
23842
25154
  trigger.addEventListener("click", (e) => {
23843
25155
  e.stopPropagation();
23844
- if (openPopover2 && openPopover2.trigger === trigger) {
23845
- closeOpen2();
25156
+ if (openPopoverTrigger() === trigger) {
25157
+ closeOpenPopovers();
23846
25158
  return;
23847
25159
  }
23848
- closeOpen2();
23849
- const pop = document.createElement("div");
23850
- pop.className = "vela-width-field-pop";
23851
- applyChromeTokens(pop, theme);
23852
- pop.style.font = `var(--vela-font-size-md) ${theme.fontFamily}`;
23853
- pop.addEventListener("pointerdown", (ev) => ev.stopPropagation());
23854
- for (const w of WIDTH_FIELD_OPTIONS) {
23855
- const item = document.createElement("button");
23856
- item.type = "button";
23857
- item.className = "vela-width-field-item";
23858
- item.dataset.active = w === getVal() ? "1" : "0";
23859
- item.innerHTML = `<span style="display:flex;flex:none;">${lineGlyph(w)}</span><span style="flex:1;font-variant-numeric:tabular-nums;">${w}px</span>`;
23860
- item.addEventListener("click", (ev) => {
23861
- ev.stopPropagation();
23862
- closeOpen2();
23863
- onVal(w);
23864
- paint();
23865
- });
23866
- pop.appendChild(item);
23867
- }
23868
- document.body.appendChild(pop);
23869
- place2(pop, trigger);
23870
- const onOutside = (ev) => {
23871
- const t = ev.target;
23872
- if (pop.contains(t) || trigger.contains(t)) return;
23873
- closeOpen2();
23874
- };
23875
- const reflow = () => place2(pop, trigger);
23876
- setTimeout(() => document.addEventListener("pointerdown", onOutside, true), 0);
23877
- window.addEventListener("resize", reflow, true);
23878
- openPopover2 = { el: pop, trigger, onOutside, reflow };
25160
+ const pop = new Popover({
25161
+ trigger,
25162
+ theme,
25163
+ align: "end",
25164
+ gap: 6,
25165
+ className: "vela-width-field-pop",
25166
+ content: (el) => {
25167
+ el.style.font = `var(--vela-font-size-md) ${theme.fontFamily}`;
25168
+ for (const w of WIDTH_FIELD_OPTIONS) {
25169
+ const item = document.createElement("button");
25170
+ item.type = "button";
25171
+ item.className = "vela-width-field-item";
25172
+ item.dataset.active = w === getVal() ? "1" : "0";
25173
+ item.innerHTML = `<span style="display:flex;flex:none;">${lineGlyph(w)}</span><span style="flex:1;font-variant-numeric:tabular-nums;">${w}px</span>`;
25174
+ item.addEventListener("click", (ev) => {
25175
+ ev.stopPropagation();
25176
+ pop.hide();
25177
+ onVal(w);
25178
+ paint();
25179
+ });
25180
+ el.appendChild(item);
25181
+ }
25182
+ }
25183
+ });
25184
+ pop.show();
23879
25185
  });
23880
25186
  return trigger;
23881
25187
  }
23882
25188
  function closeWidthPopover() {
23883
- closeOpen2();
25189
+ closeOpenPopovers();
23884
25190
  }
23885
25191
 
23886
25192
  // src/renderers/native/chrome/SettingsDialog.ts
@@ -23895,37 +25201,24 @@ function styleLabel(id) {
23895
25201
  return chartType(id)?.label ?? BUILTIN_STYLE_LABELS2[id] ?? id;
23896
25202
  }
23897
25203
  var SD_STYLE_ID = "vela-settings-controls";
25204
+ var SD_STYLE_REV = "2";
23898
25205
  var SETTINGS_SURFACE = "var(--vela-surface)";
23899
25206
  var SETTINGS_BORDER = "var(--vela-border)";
23900
25207
  var SETTINGS_TEXT = "var(--vela-fg)";
23901
25208
  function ensureControlStyles() {
23902
- if (typeof document === "undefined" || document.getElementById(SD_STYLE_ID)) return;
23903
- const st = document.createElement("style");
25209
+ if (typeof document === "undefined") return;
25210
+ const existing = document.getElementById(SD_STYLE_ID);
25211
+ if (existing?.dataset.rev === SD_STYLE_REV) return;
25212
+ const st = existing ?? document.createElement("style");
23904
25213
  st.id = SD_STYLE_ID;
25214
+ st.dataset.rev = SD_STYLE_REV;
23905
25215
  st.textContent = `
23906
- .vela-sd-check{width:18px;height:18px;flex:none;display:inline-flex;align-items:center;justify-content:center;padding:0;border:1px solid var(--vela-border-strong);border-radius:5px;background:transparent;color:transparent;cursor:pointer;}
23907
- .vela-sd-check:hover{border-color:var(--vela-fg-muted);}
23908
- .vela-sd-check.on{background:var(--vela-selected-bg);border-color:var(--vela-selected-bg);color:var(--vela-selected-fg);}
23909
- .vela-sd-check svg{display:block;}
23910
- .vela-sd-select,.vela-sd-number{height:28px;background:var(--vela-surface-elev);border:1px solid var(--vela-border-strong);border-radius:var(--vela-radius-sm);color:var(--vela-fg);padding:0 8px;font-size:13px;outline:none;font-family:inherit;}
23911
- .vela-sd-select:hover,.vela-sd-number:hover{border-color:var(--vela-fg-muted);}
23912
- /* Custom caret: the native one hugs the right border; ours gets 8px of air. (A data URI
23913
- can't read currentColor, so it uses the shared muted-gray ink, legible on both themes.) */
23914
- .vela-sd-select{-webkit-appearance:none;appearance:none;padding-right:26px;background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='8' height='5' viewBox='0 0 8 5'%3E%3Cpath d='M1 1l3 3 3-3' fill='none' stroke='%23868a96' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");background-repeat:no-repeat;background-position:right 8px center;}
23915
- .vela-sd-number{width:64px;}
23916
- .vela-sd-color{width:32px;height:26px;padding:0;border:1px solid var(--vela-border);border-radius:var(--vela-radius-sm);background:transparent;cursor:pointer;-webkit-appearance:none;appearance:none;}
23917
- .vela-sd-color::-webkit-color-swatch-wrapper{padding:2px;}
23918
- .vela-sd-color::-webkit-color-swatch{border:none;border-radius:2px;}
23919
25216
  .vela-sd-pane::-webkit-scrollbar{width:8px;}
23920
25217
  .vela-sd-pane::-webkit-scrollbar-thumb{background:var(--vela-scroll);border-radius:var(--vela-radius-sm);border:2px solid transparent;background-clip:padding-box;}
23921
25218
  .vela-sd-pane::-webkit-scrollbar-track{background:transparent;}
23922
- .vela-sd-toggle{position:relative;width:38px;height:22px;border-radius:11px;background:var(--vela-surface-sunken);border:1px solid var(--vela-border);cursor:pointer;flex:none;padding:0;}
23923
- .vela-sd-toggle::after{content:'';position:absolute;top:2px;left:2px;width:16px;height:16px;border-radius:50%;background:var(--vela-fg-muted);transition:transform var(--vela-dur-med) ease,background var(--vela-dur-med) ease;}
23924
- .vela-sd-toggle.on{background:var(--vela-active);border-color:var(--vela-selected-bg);}
23925
- .vela-sd-toggle.on::after{transform:translateX(16px);background:var(--vela-selected-bg);}
23926
25219
  /* Tab rail / footer button / header close: base styles live HERE, not inline on the
23927
25220
  elements \u2014 inline declarations always beat stylesheet :hover rules, which is exactly
23928
- what killed these hovers before. Active tab state is the .on class (like .vela-sd-check),
25221
+ what killed these hovers before. Active tab state is the .on class,
23929
25222
  hover fills follow the app convention (--vela-hover + --vela-fg-bright, fast transition). */
23930
25223
  .vela-sd-tab{text-align:left;padding:9px 12px;background:transparent;border:none;border-radius:var(--vela-radius-md);color:var(--vela-fg-muted);font-weight:600;font-size:13px;font-family:inherit;cursor:pointer;transition:background var(--vela-dur-fast) ease,color var(--vela-dur-fast) ease;}
23931
25224
  .vela-sd-tab:hover{background:var(--vela-hover);color:var(--vela-fg-bright);}
@@ -23984,13 +25277,13 @@ function ensureControlStyles() {
23984
25277
  .vela-sd-mobile .vela-sd-itabs{flex-wrap:nowrap;overflow-x:auto;scrollbar-width:none;}
23985
25278
  .vela-sd-mobile .vela-sd-itabs::-webkit-scrollbar{display:none;}
23986
25279
  .vela-sd-mobile .vela-sd-itab{height:36px;flex:none;}
23987
- .vela-sd-mobile .vela-sd-check{width:22px;height:22px;}
23988
- .vela-sd-mobile .vela-sd-select,.vela-sd-mobile .vela-sd-number{height:34px;}
25280
+ .vela-sd-mobile .vela-switch{width:22px;height:22px;}
25281
+ .vela-sd-mobile .vela-select[data-size='sm'] .vela-select-trigger,.vela-sd-mobile .vela-num[data-size='sm'] input{height:34px;}
23989
25282
  .vela-sd-mobile .vela-sd-close{width:40px;height:40px;}
23990
25283
  .vela-sd-mobile .vela-sd-btn{height:38px;}
23991
25284
  .vela-sd-mobile .vela-sd-row span,.vela-sd-mobile .vela-sd-bool span{white-space:normal !important;}
23992
25285
  `;
23993
- document.head.appendChild(st);
25286
+ if (!existing) document.head.appendChild(st);
23994
25287
  }
23995
25288
  var SettingsDialog = class {
23996
25289
  constructor(container, theme) {
@@ -24306,15 +25599,15 @@ var SettingsDialog = class {
24306
25599
  railScrim = document.createElement("div");
24307
25600
  railScrim.className = "vela-sd-railscrim";
24308
25601
  railScrim.addEventListener("click", () => toggleRail?.());
24309
- toggleRail = (open) => {
24310
- const on = open ?? !rail.classList.contains("open");
25602
+ toggleRail = (open2) => {
25603
+ const on = open2 ?? !rail.classList.contains("open");
24311
25604
  rail.classList.toggle("open", on);
24312
25605
  railScrim?.classList.toggle("open", on);
24313
25606
  };
24314
25607
  }
24315
25608
  const paneHost = document.createElement("div");
24316
25609
  paneHost.className = "vela-sd-pane";
24317
- paneHost.style.cssText = mobile ? "flex:1;min-width:0;overflow-y:auto;padding:6px 14px calc(14px + env(safe-area-inset-bottom, 0px));" : "flex:1;overflow-y:auto;padding:6px 18px 14px;";
25610
+ paneHost.style.cssText = mobile ? "flex:1;min-width:0;overflow-y:auto;overflow-anchor:none;padding:6px 14px calc(14px + env(safe-area-inset-bottom, 0px));" : "flex:1;overflow-y:auto;overflow-anchor:none;padding:6px 18px 14px;";
24318
25611
  const panes = [];
24319
25612
  let current = null;
24320
25613
  for (const child of [...body.children]) {
@@ -24380,7 +25673,7 @@ var SettingsDialog = class {
24380
25673
  }
24381
25674
  }
24382
25675
  close() {
24383
- closeColorPopover();
25676
+ closeOpenPopovers();
24384
25677
  closeWidthPopover();
24385
25678
  this.root?.remove();
24386
25679
  this.root = null;
@@ -24535,50 +25828,41 @@ var SettingsDialog = class {
24535
25828
  return wf;
24536
25829
  }
24537
25830
  if (c.kind === "select") {
24538
- const sel = document.createElement("select");
24539
- sel.className = "vela-sd-select";
24540
- sel.style.cssText = "max-width:200px;flex:0 0 auto;";
24541
- sel.title = c.label;
24542
25831
  const current2 = typeof bag[c.key] === "string" ? bag[c.key] : c.defval;
24543
- for (const [val, lbl] of normalizeSelectOptions(c.options)) {
24544
- const o = document.createElement("option");
24545
- o.value = val;
24546
- o.textContent = lbl;
24547
- if (val === current2) o.selected = true;
24548
- sel.appendChild(o);
24549
- }
24550
- sel.addEventListener("change", () => put(c.key, sel.value));
24551
- sel.addEventListener("vela-sync", () => {
24552
- sel.value = typeof bag[c.key] === "string" ? bag[c.key] : c.defval;
25832
+ const sel = new Select({
25833
+ options: normalizeSelectOptions(c.options).map(([value, label]) => ({ value, label })),
25834
+ value: current2,
25835
+ size: "sm",
25836
+ theme: this.theme,
25837
+ onChange: (v) => put(c.key, v)
24553
25838
  });
24554
- return sel;
24555
- }
24556
- const ni = document.createElement("input");
24557
- ni.type = "number";
24558
- ni.className = "vela-sd-number";
24559
- if (compact) ni.style.width = "56px";
24560
- if (c.min !== void 0) ni.min = String(c.min);
24561
- if (c.max !== void 0) ni.max = String(c.max);
24562
- ni.step = String(c.step ?? 1);
24563
- ni.title = c.label;
24564
- const current = typeof bag[c.key] === "number" ? bag[c.key] : c.defval;
24565
- if (c.placeholder !== void 0) {
24566
- ni.placeholder = c.placeholder;
24567
- if (current !== c.defval) ni.value = String(current);
24568
- ni.addEventListener("change", () => {
24569
- const raw = ni.value.trim() === "" ? c.defval : Number(ni.value);
24570
- const v = Number.isFinite(raw) ? Math.min(c.max ?? Infinity, Math.max(c.min ?? -Infinity, raw)) : c.defval;
24571
- ni.value = v === c.defval ? "" : String(v);
24572
- put(c.key, v);
24573
- });
24574
- } else {
24575
- ni.value = String(current);
24576
- ni.addEventListener("input", () => {
24577
- const v = Number(ni.value);
24578
- if (Number.isFinite(v)) put(c.key, v);
25839
+ sel.el.title = c.label;
25840
+ sel.el.addEventListener("vela-sync", () => {
25841
+ sel.setValue(typeof bag[c.key] === "string" ? bag[c.key] : c.defval);
24579
25842
  });
25843
+ return sel.el;
24580
25844
  }
24581
- return ni;
25845
+ const current = typeof bag[c.key] === "number" ? bag[c.key] : c.defval;
25846
+ const ni = new NumberInput({
25847
+ value: current,
25848
+ min: c.min,
25849
+ max: c.max,
25850
+ step: c.step ?? 1,
25851
+ size: "sm",
25852
+ commit: "live",
25853
+ clamp: c.placeholder !== void 0,
25854
+ steppers: false,
25855
+ compact,
25856
+ title: c.label,
25857
+ placeholder: c.placeholder,
25858
+ emptyValue: c.placeholder !== void 0 ? c.defval : void 0,
25859
+ onChange: (v) => put(c.key, v)
25860
+ });
25861
+ ni.el.addEventListener("vela-sync", () => {
25862
+ const next = typeof bag[c.key] === "number" ? bag[c.key] : c.defval;
25863
+ ni.setValue(next);
25864
+ });
25865
+ return ni.el;
24582
25866
  }
24583
25867
  /**
24584
25868
  * The INSTANCE STRIP block: a tab per present instance (label, `×` on the active
@@ -24859,28 +26143,27 @@ var SettingsDialog = class {
24859
26143
  * only one visible at a time; see `typeRow`). */
24860
26144
  toggleRow(label, value, onToggle, controls, get) {
24861
26145
  const wrap = document.createElement("div");
24862
- const cb = document.createElement("button");
24863
- cb.type = "button";
24864
- cb.className = "vela-sd-check" + (value ? " on" : "");
24865
- cb.innerHTML = iconAt("check", 11);
24866
- let checked = value;
24867
- const cbToggle = () => {
24868
- checked = !checked;
24869
- cb.classList.toggle("on", checked);
24870
- return checked;
24871
- };
26146
+ const sw = new Switch({ checked: value, size: "sm", onChange: (v) => {
26147
+ onToggle(v);
26148
+ if (controls.length > 0) syncDim(v);
26149
+ } });
24872
26150
  const lbl = document.createElement("span");
24873
26151
  lbl.textContent = label;
24874
26152
  lbl.style.cssText = "opacity:0.85;white-space:nowrap;";
26153
+ const syncDim = (on) => {
26154
+ for (const c of controls) {
26155
+ if (c.dataset.sdSelfGated === "1") continue;
26156
+ c.style.opacity = on ? "1" : "0.4";
26157
+ c.style.pointerEvents = on ? "" : "none";
26158
+ }
26159
+ };
24875
26160
  if (controls.length === 0) {
24876
26161
  wrap.className = "vela-sd-bool";
24877
26162
  wrap.style.cssText = "display:flex;align-items:center;gap:8px;min-height:22px;";
24878
- wrap.append(cb, lbl);
24879
- cb.addEventListener("click", () => onToggle(cbToggle()));
26163
+ wrap.append(sw.el, lbl);
24880
26164
  if (get) {
24881
26165
  wrap.addEventListener("vela-sync", () => {
24882
- checked = get();
24883
- cb.classList.toggle("on", checked);
26166
+ sw.setChecked(get());
24884
26167
  });
24885
26168
  }
24886
26169
  return wrap;
@@ -24888,28 +26171,16 @@ var SettingsDialog = class {
24888
26171
  wrap.className = "vela-sd-row";
24889
26172
  const left = document.createElement("div");
24890
26173
  left.style.cssText = "display:flex;align-items:center;gap:8px;";
24891
- left.append(cb, lbl);
26174
+ left.append(sw.el, lbl);
24892
26175
  const box = document.createElement("div");
24893
26176
  box.style.cssText = "display:flex;align-items:center;gap:6px;";
24894
26177
  for (const c of controls) box.appendChild(c);
24895
- const syncDim = (on) => {
24896
- for (const c of controls) {
24897
- if (c.dataset.sdSelfGated === "1") continue;
24898
- c.style.opacity = on ? "1" : "0.4";
24899
- c.style.pointerEvents = on ? "" : "none";
24900
- }
24901
- };
24902
26178
  syncDim(value);
24903
- cb.addEventListener("click", () => {
24904
- const v = cbToggle();
24905
- onToggle(v);
24906
- syncDim(v);
24907
- });
24908
26179
  if (get) {
24909
26180
  wrap.addEventListener("vela-sync", () => {
24910
- checked = get();
24911
- cb.classList.toggle("on", checked);
24912
- syncDim(checked);
26181
+ const v = get();
26182
+ sw.setChecked(v);
26183
+ syncDim(v);
24913
26184
  });
24914
26185
  }
24915
26186
  wrap.append(left, box);
@@ -24917,52 +26188,23 @@ var SettingsDialog = class {
24917
26188
  }
24918
26189
  numberRow(label, value, min, max, step, onChange) {
24919
26190
  const { wrap } = this.row(label);
24920
- const ni = document.createElement("input");
24921
- ni.type = "number";
24922
- ni.value = String(value);
24923
- ni.min = String(min);
24924
- ni.max = String(max);
24925
- ni.step = String(step);
24926
- ni.className = "vela-sd-number";
24927
- ni.addEventListener("input", () => {
24928
- const n = Number(ni.value);
24929
- if (Number.isFinite(n)) onChange(n);
24930
- });
24931
- wrap.appendChild(ni);
26191
+ wrap.appendChild(new NumberInput({ value, min, max, step, size: "sm", commit: "live", onChange }).el);
24932
26192
  return wrap;
24933
26193
  }
24934
26194
  /** A dropdown whose option values differ from their display labels. */
24935
26195
  selectRowLabeled(label, value, options, onChange) {
24936
26196
  const { wrap } = this.row(label);
24937
- const sel = document.createElement("select");
24938
- sel.className = "vela-sd-select";
24939
- sel.style.cssText = "max-width:200px;flex:0 0 auto;";
24940
- for (const [val, lbl] of options) {
24941
- const o = document.createElement("option");
24942
- o.value = val;
24943
- o.textContent = lbl;
24944
- if (val === value) o.selected = true;
24945
- sel.appendChild(o);
24946
- }
24947
- sel.addEventListener("change", () => onChange(sel.value));
24948
- wrap.appendChild(sel);
26197
+ wrap.appendChild(new Select({
26198
+ options: options.map(([v, l]) => ({ value: v, label: l })),
26199
+ value,
26200
+ size: "sm",
26201
+ theme: this.theme,
26202
+ onChange
26203
+ }).el);
24949
26204
  return wrap;
24950
26205
  }
24951
26206
  selectRow(label, value, options, onChange) {
24952
- const { wrap } = this.row(label);
24953
- const sel = document.createElement("select");
24954
- sel.className = "vela-sd-select";
24955
- sel.style.cssText = "max-width:200px;flex:0 0 auto;";
24956
- for (const opt of options) {
24957
- const o = document.createElement("option");
24958
- o.value = opt;
24959
- o.textContent = opt;
24960
- if (opt === value) o.selected = true;
24961
- sel.appendChild(o);
24962
- }
24963
- sel.addEventListener("change", () => onChange(sel.value));
24964
- wrap.appendChild(sel);
24965
- return wrap;
26207
+ return this.selectRowLabeled(label, value, options.map((o) => [o, o]), onChange);
24966
26208
  }
24967
26209
  /** Footer with the full config as JSON — the export/import (templating) surface. */
24968
26210
  footer(_config) {
@@ -25353,13 +26595,19 @@ var DrawingPainter = class {
25353
26595
  }
25354
26596
  this.paintLabel(ctx, d, proj, theme);
25355
26597
  }
25356
- /** Draw a drawing's optional text label at a type-appropriate anchor (multi-line aware). */
26598
+ /** Draw a drawing's optional text label at a type-appropriate anchor (multi-line aware).
26599
+ * Two-point line tools rotate the glyphs to follow the segment, kept upright. */
25357
26600
  paintLabel(ctx, d, proj, theme) {
25358
26601
  const text = d.text;
25359
26602
  if (!text || !text.value || d.id === this.targets.mutedLabel) return;
25360
26603
  const layout = labelLayout(d, proj);
25361
26604
  if (!layout) return;
25362
26605
  const fs = namedFontSize(text.size);
26606
+ ctx.save();
26607
+ if (layout.rotate) {
26608
+ ctx.translate(layout.rotate.cx, layout.rotate.cy);
26609
+ ctx.rotate(layout.rotate.angle);
26610
+ }
25363
26611
  ctx.font = `${text.bold ? "bold " : ""}${text.italic ? "italic " : ""}${fs}px ${theme.fontFamily}`;
25364
26612
  ctx.textBaseline = "top";
25365
26613
  ctx.textAlign = layout.align;
@@ -25367,8 +26615,8 @@ var DrawingPainter = class {
25367
26615
  const lh = labelLineHeight(fs);
25368
26616
  const lines = text.value.split("\n");
25369
26617
  lines.forEach((line, i) => ctx.fillText(line, layout.x, layout.top + i * lh));
25370
- ctx.textAlign = "left";
25371
26618
  if (d.type === "text") this.paintTextFrame(ctx, d.id, layout.x, layout.top, lines, fs, theme);
26619
+ ctx.restore();
25372
26620
  }
25373
26621
  /** Frame a plain text label while it is the target, so the text reads as a clickable object even
25374
26622
  * though it has no shape of its own: a firm frame once selected, a fainter one under the cursor.
@@ -26311,7 +27559,14 @@ function labelLayout(d, proj) {
26311
27559
  case "infoline":
26312
27560
  case "trendangle": {
26313
27561
  if (pts.length < 2) return null;
26314
- return { x: (pts[0][0] + pts[1][0]) / 2, top: (pts[0][1] + pts[1][1]) / 2 - 6 - lh * lines, align: "center" };
27562
+ const [x1, y1] = pts[0];
27563
+ const [x2, y2] = pts[1];
27564
+ return {
27565
+ x: 0,
27566
+ top: -6 - lh * lines,
27567
+ align: "center",
27568
+ rotate: { angle: uprightLineAngle(x1, y1, x2, y2), cx: (x1 + x2) / 2, cy: (y1 + y2) / 2 }
27569
+ };
26315
27570
  }
26316
27571
  case "box": {
26317
27572
  if (pts.length < 2) return null;
@@ -26660,11 +27915,11 @@ function applyFree(anchor, pt, free) {
26660
27915
  // src/renderers/native/drawings/DrawingSettingsPopup.ts
26661
27916
  var BTN = 30;
26662
27917
  var ICON = 21;
26663
- var STYLE_ID24 = "vela-drawing-popup-styles";
26664
- function ensureStyles6() {
26665
- if (typeof document === "undefined" || document.getElementById(STYLE_ID24)) return;
27918
+ var STYLE_ID23 = "vela-drawing-popup-styles";
27919
+ function ensureStyles4() {
27920
+ if (typeof document === "undefined" || document.getElementById(STYLE_ID23)) return;
26666
27921
  const s = document.createElement("style");
26667
- s.id = STYLE_ID24;
27922
+ s.id = STYLE_ID23;
26668
27923
  s.textContent = `
26669
27924
  .vela-dpop input[type=color]{-webkit-appearance:none;appearance:none;border:none;padding:0;background:none;cursor:pointer;border-radius:0;}
26670
27925
  .vela-dpop input[type=color]::-webkit-color-swatch-wrapper{padding:0;}
@@ -26698,17 +27953,14 @@ var DrawingSettingsPopup = class {
26698
27953
  this.textPanel = null;
26699
27954
  this.levelsPanel = null;
26700
27955
  this.colorPop = null;
26701
- // floating RGB + opacity picker for a color swatch
26702
27956
  this.colorOwner = null;
26703
- // the swatch the open color popover belongs to (for toggle)
26704
- this.menuEl = null;
26705
- // floating option list for a dropdown control
27957
+ this.menuPop = null;
26706
27958
  this.menuOwner = null;
26707
27959
  this.onClose = null;
26708
27960
  this.onOutside = (e) => {
26709
27961
  const target = e.target;
26710
- if (this.colorPop?.contains(target)) return;
26711
- if (this.menuEl?.contains(target)) return;
27962
+ if (this.colorPop?.el.contains(target)) return;
27963
+ if (this.menuPop?.el.contains(target)) return;
26712
27964
  if (this.el && !this.el.contains(target)) {
26713
27965
  const cb = this.onClose;
26714
27966
  this.close();
@@ -26727,13 +27979,13 @@ var DrawingSettingsPopup = class {
26727
27979
  * picker / dropdown menu), which live outside `el`. */
26728
27980
  contains(node) {
26729
27981
  if (!node) return false;
26730
- return this.el?.contains(node) === true || this.colorPop?.contains(node) === true || this.menuEl?.contains(node) === true;
27982
+ return this.el?.contains(node) === true || this.colorPop?.el.contains(node) === true || this.menuPop?.el.contains(node) === true;
26731
27983
  }
26732
27984
  /** Open the quick toolbar for `drawing`, floating clear of its `anchor` box.
26733
27985
  * `onClose` fires on an outside-click dismissal (not a programmatic close). */
26734
27986
  open(drawing, anchor, actions, onClose) {
26735
27987
  this.close();
26736
- ensureStyles6();
27988
+ ensureStyles4();
26737
27989
  this.onClose = onClose ?? null;
26738
27990
  const t = this.theme;
26739
27991
  const schema = drawing.schema();
@@ -26744,8 +27996,7 @@ var DrawingSettingsPopup = class {
26744
27996
  el.style.cssText = `position:absolute;z-index:22;background:${t.background};border:1px solid var(--vela-border);border-radius:var(--vela-radius-lg);box-shadow:var(--vela-shadow);color:${t.textColor};font:var(--vela-font-size-md) ${t.fontFamily};display:flex;flex-direction:column;pointer-events:auto;overflow:hidden;`;
26745
27997
  applyChromeTokens(el, t);
26746
27998
  el.addEventListener("pointerdown", () => {
26747
- this.closeColorPopover();
26748
- this.closeMenu();
27999
+ closeOpenPopovers();
26749
28000
  });
26750
28001
  const bar = document.createElement("div");
26751
28002
  bar.style.cssText = "display:flex;align-items:center;gap:2px;padding:4px;";
@@ -27482,49 +28733,76 @@ var DrawingSettingsPopup = class {
27482
28733
  });
27483
28734
  return b;
27484
28735
  }
28736
+ /** Host-anchored floating shell (stays inside the chart). Content is filled after
28737
+ * construction so `fill` can close over the live `Popover` without hitting TDZ. */
28738
+ hostFloat(anchor, opts) {
28739
+ const t = this.theme;
28740
+ const pop = new Popover({
28741
+ trigger: anchor,
28742
+ host: this.host,
28743
+ theme: t,
28744
+ position: "absolute",
28745
+ boundary: this.host,
28746
+ boundaryInset: 4,
28747
+ viewportInset: 0,
28748
+ gap: 6,
28749
+ align: opts.align ?? "start",
28750
+ zIndex: opts.zIndex,
28751
+ onClose: () => {
28752
+ if (this.menuPop === pop) {
28753
+ this.menuPop = null;
28754
+ this.menuOwner = null;
28755
+ }
28756
+ if (this.colorPop === pop) {
28757
+ this.colorPop = null;
28758
+ this.colorOwner = null;
28759
+ }
28760
+ }
28761
+ });
28762
+ const el = pop.el;
28763
+ el.style.background = t.background;
28764
+ el.style.border = "1px solid var(--vela-border)";
28765
+ el.style.borderRadius = "var(--vela-radius-lg)";
28766
+ el.style.boxShadow = "var(--vela-shadow)";
28767
+ el.style.padding = opts.padding;
28768
+ el.style.display = "flex";
28769
+ el.style.flexDirection = "column";
28770
+ el.style.gap = "1px";
28771
+ el.style.color = t.textColor;
28772
+ el.style.font = `var(--vela-font-size-md) ${t.fontFamily}`;
28773
+ el.style.pointerEvents = "auto";
28774
+ opts.fill(el, pop);
28775
+ pop.show();
28776
+ return pop;
28777
+ }
27485
28778
  /** A floating list of one-shot actions (icon + label rows) opened by the kebab. */
27486
28779
  openActionMenu(anchor, rows) {
27487
- this.closeMenu();
27488
- this.closeColorPopover();
27489
- const t = this.theme;
27490
- const menu2 = document.createElement("div");
27491
- menu2.className = "vela-dpop";
27492
- menu2.style.cssText = `position:absolute;z-index:26;background:${t.background};border:1px solid var(--vela-border);border-radius:var(--vela-radius-lg);box-shadow:var(--vela-shadow);padding:4px;pointer-events:auto;display:flex;flex-direction:column;gap:1px;color:${t.textColor};font:var(--vela-font-size-md) ${t.fontFamily};`;
27493
- applyChromeTokens(menu2, t);
27494
- menu2.addEventListener("pointerdown", (e) => e.stopPropagation());
27495
- for (const row of rows) {
27496
- const item = document.createElement("button");
27497
- item.type = "button";
27498
- item.className = "vela-dpop-item";
27499
- item.style.cssText = "display:flex;align-items:center;gap:9px;min-width:150px;padding:6px 9px;border:none;border-radius:5px;color:inherit;cursor:pointer;text-align:left;font:inherit;";
27500
- const ic = document.createElement("span");
27501
- ic.style.cssText = "display:flex;flex:none;width:20px;justify-content:center;";
27502
- ic.innerHTML = sized(row.icon, 18);
27503
- const tx = document.createElement("span");
27504
- tx.textContent = row.label;
27505
- tx.style.cssText = "flex:1;";
27506
- item.append(ic, tx);
27507
- item.addEventListener("click", (e) => {
27508
- e.stopPropagation();
27509
- this.closeMenu();
27510
- row.onClick();
27511
- });
27512
- menu2.appendChild(item);
27513
- }
27514
- this.host.appendChild(menu2);
27515
- const ar = anchor.getBoundingClientRect();
27516
- const hr = this.host.getBoundingClientRect();
27517
- const w = menu2.offsetWidth;
27518
- const h = menu2.offsetHeight;
27519
- let left = ar.right - hr.left - w;
27520
- let top = ar.bottom - hr.top + 6;
27521
- if (left + w > hr.width - 4) left = hr.width - w - 4;
27522
- if (left < 4) left = 4;
27523
- if (top + h > hr.height - 4) top = ar.top - hr.top - h - 6;
27524
- if (top < 4) top = 4;
27525
- menu2.style.left = `${Math.round(left)}px`;
27526
- menu2.style.top = `${Math.round(top)}px`;
27527
- this.menuEl = menu2;
28780
+ this.menuPop = this.hostFloat(anchor, {
28781
+ align: "end",
28782
+ zIndex: 26,
28783
+ padding: "4px",
28784
+ fill: (menu2, pop) => {
28785
+ for (const row of rows) {
28786
+ const item = document.createElement("button");
28787
+ item.type = "button";
28788
+ item.className = "vela-dpop-item";
28789
+ item.style.cssText = "display:flex;align-items:center;gap:9px;min-width:150px;padding:6px 9px;border:none;border-radius:5px;color:inherit;cursor:pointer;text-align:left;font:inherit;";
28790
+ const ic = document.createElement("span");
28791
+ ic.style.cssText = "display:flex;flex:none;width:20px;justify-content:center;";
28792
+ ic.innerHTML = sized(row.icon, 18);
28793
+ const tx = document.createElement("span");
28794
+ tx.textContent = row.label;
28795
+ tx.style.cssText = "flex:1;";
28796
+ item.append(ic, tx);
28797
+ item.addEventListener("click", (e) => {
28798
+ e.stopPropagation();
28799
+ pop.hide();
28800
+ row.onClick();
28801
+ });
28802
+ menu2.appendChild(item);
28803
+ }
28804
+ }
28805
+ });
27528
28806
  this.menuOwner = anchor;
27529
28807
  }
27530
28808
  /** The floating hover-label. Sits just above or below the toolbar depending on where it floats,
@@ -27654,60 +28932,40 @@ var DrawingSettingsPopup = class {
27654
28932
  /** The floating option list opened by a {@link dropdown} trigger. Each row shows the option's
27655
28933
  * glyph (and text label when one is supplied); the current value is highlighted. */
27656
28934
  openMenu(anchor, values, current, render, label, onPick) {
27657
- this.closeMenu();
27658
- this.closeColorPopover();
27659
- const t = this.theme;
27660
- const menu2 = document.createElement("div");
27661
- menu2.className = "vela-dpop";
27662
- menu2.style.cssText = `position:absolute;z-index:26;background:${t.background};border:1px solid var(--vela-border);border-radius:var(--vela-radius-lg);box-shadow:var(--vela-shadow);padding:4px;pointer-events:auto;display:flex;flex-direction:column;gap:1px;color:${t.textColor};font:var(--vela-font-size-md) ${t.fontFamily};`;
27663
- applyChromeTokens(menu2, t);
27664
- menu2.addEventListener("pointerdown", (e) => e.stopPropagation());
27665
- for (const v of values) {
27666
- const active = v === current;
27667
- const item = document.createElement("button");
27668
- item.type = "button";
27669
- item.className = "vela-dpop-item";
27670
- item.dataset.active = active ? "1" : "0";
27671
- item.style.cssText = `display:flex;align-items:center;gap:8px;${label ? "min-width:118px;" : ""}padding:5px 8px;border:none;border-radius:5px;color:inherit;cursor:pointer;text-align:left;font:inherit;`;
27672
- const ic = document.createElement("span");
27673
- ic.style.cssText = "display:flex;flex:none;width:22px;justify-content:center;";
27674
- ic.innerHTML = sized(render(v), 18);
27675
- item.appendChild(ic);
27676
- if (label) {
27677
- const tx = document.createElement("span");
27678
- tx.textContent = label(v);
27679
- tx.style.cssText = "flex:1;font-variant-numeric:tabular-nums;";
27680
- item.appendChild(tx);
28935
+ this.menuPop = this.hostFloat(anchor, {
28936
+ zIndex: 26,
28937
+ padding: "4px",
28938
+ fill: (menu2, pop) => {
28939
+ for (const v of values) {
28940
+ const active = v === current;
28941
+ const item = document.createElement("button");
28942
+ item.type = "button";
28943
+ item.className = "vela-dpop-item";
28944
+ item.dataset.active = active ? "1" : "0";
28945
+ item.style.cssText = `display:flex;align-items:center;gap:8px;${label ? "min-width:118px;" : ""}padding:5px 8px;border:none;border-radius:5px;color:inherit;cursor:pointer;text-align:left;font:inherit;`;
28946
+ const ic = document.createElement("span");
28947
+ ic.style.cssText = "display:flex;flex:none;width:22px;justify-content:center;";
28948
+ ic.innerHTML = sized(render(v), 18);
28949
+ item.appendChild(ic);
28950
+ if (label) {
28951
+ const tx = document.createElement("span");
28952
+ tx.textContent = label(v);
28953
+ tx.style.cssText = "flex:1;font-variant-numeric:tabular-nums;";
28954
+ item.appendChild(tx);
28955
+ }
28956
+ item.addEventListener("click", (e) => {
28957
+ e.stopPropagation();
28958
+ pop.hide();
28959
+ onPick(v);
28960
+ });
28961
+ menu2.appendChild(item);
28962
+ }
27681
28963
  }
27682
- item.addEventListener("click", (e) => {
27683
- e.stopPropagation();
27684
- this.closeMenu();
27685
- onPick(v);
27686
- });
27687
- menu2.appendChild(item);
27688
- }
27689
- this.host.appendChild(menu2);
27690
- const ar = anchor.getBoundingClientRect();
27691
- const hr = this.host.getBoundingClientRect();
27692
- const w = menu2.offsetWidth;
27693
- const h = menu2.offsetHeight;
27694
- let left = ar.left - hr.left;
27695
- let top = ar.bottom - hr.top + 6;
27696
- if (left + w > hr.width - 4) left = hr.width - w - 4;
27697
- if (left < 4) left = 4;
27698
- if (top + h > hr.height - 4) top = ar.top - hr.top - h - 6;
27699
- if (top < 4) top = 4;
27700
- menu2.style.left = `${Math.round(left)}px`;
27701
- menu2.style.top = `${Math.round(top)}px`;
27702
- this.menuEl = menu2;
28964
+ });
27703
28965
  this.menuOwner = anchor;
27704
28966
  }
27705
28967
  closeMenu() {
27706
- if (this.menuEl) {
27707
- this.menuEl.remove();
27708
- this.menuEl = null;
27709
- }
27710
- this.menuOwner = null;
28968
+ this.menuPop?.hide();
27711
28969
  }
27712
28970
  /** A color control: an `icon` over a thin **colored underline** showing the current
27713
28971
  * value (the common idiom), with an invisible native picker overlaid to edit it.
@@ -27746,36 +29004,18 @@ var DrawingSettingsPopup = class {
27746
29004
  }
27747
29005
  /** A floating RGB picker + opacity slider anchored to a color swatch — emits `#RRGGBB(AA)`. */
27748
29006
  openColorPopover(anchor, color, onChange) {
27749
- this.closeColorPopover();
27750
- this.closeMenu();
27751
29007
  const t = this.theme;
27752
- const pop = document.createElement("div");
27753
- pop.style.cssText = `position:absolute;z-index:25;background:${t.background};border:1px solid var(--vela-border);border-radius:var(--vela-radius-lg);box-shadow:var(--vela-shadow);padding:10px;pointer-events:auto;`;
27754
- applyChromeTokens(pop, t);
27755
- pop.addEventListener("pointerdown", (e) => e.stopPropagation());
27756
- pop.appendChild(buildColorPicker(color, t, onChange));
27757
- this.host.appendChild(pop);
27758
- const ar = anchor.getBoundingClientRect();
27759
- const hr = this.host.getBoundingClientRect();
27760
- const w = pop.offsetWidth;
27761
- const h = pop.offsetHeight;
27762
- let left = ar.left - hr.left;
27763
- let top = ar.bottom - hr.top + 6;
27764
- if (left + w > hr.width - 4) left = hr.width - w - 4;
27765
- if (left < 4) left = 4;
27766
- if (top + h > hr.height - 4) top = ar.top - hr.top - h - 6;
27767
- if (top < 4) top = 4;
27768
- pop.style.left = `${Math.round(left)}px`;
27769
- pop.style.top = `${Math.round(top)}px`;
27770
- this.colorPop = pop;
29008
+ this.colorPop = this.hostFloat(anchor, {
29009
+ zIndex: 25,
29010
+ padding: "10px",
29011
+ fill: (el) => {
29012
+ el.appendChild(buildColorPicker(color, t, onChange));
29013
+ }
29014
+ });
27771
29015
  this.colorOwner = anchor;
27772
29016
  }
27773
29017
  closeColorPopover() {
27774
- if (this.colorPop) {
27775
- this.colorPop.remove();
27776
- this.colorPop = null;
27777
- }
27778
- this.colorOwner = null;
29018
+ this.colorPop?.hide();
27779
29019
  }
27780
29020
  divider() {
27781
29021
  const d = document.createElement("div");
@@ -28158,9 +29398,9 @@ var UserDrawingController = class {
28158
29398
  }
28159
29399
  /**
28160
29400
  * Union of the visible drawings' price ranges on `paneId` whose time extent
28161
- * intersects [fromTime, toTime] folded into the pane's autoscale so an
28162
- * off-series drawing still expands the scale (mirrors Pine drawings). Hidden
28163
- * drawings and full-width references (hline null extent) are handled too.
29401
+ * intersects [fromTime, toTime]. Hidden drawings and full-width references
29402
+ * (hline null extent) are handled too. Kept as the seam for a future
29403
+ * per-drawing autoscale opt-in the renderer does not fold this in today.
28164
29404
  */
28165
29405
  priceRangeForPane(paneId, fromTime, toTime) {
28166
29406
  let lo = Infinity;
@@ -31981,7 +33221,6 @@ var NativeRenderer = class {
31981
33221
  const n = this.coords.barCount;
31982
33222
  if (n === 0) return;
31983
33223
  const vr = this.coords.visibleLogicalRange();
31984
- const vtr = this.coords.visibleTimeRange();
31985
33224
  const i0 = Math.max(0, Math.floor(vr.from));
31986
33225
  const i1 = Math.min(n - 1, Math.ceil(vr.to));
31987
33226
  if (i1 < i0) return;
@@ -31999,9 +33238,7 @@ var NativeRenderer = class {
31999
33238
  }
32000
33239
  const models = this.scene.indicatorsForPane(pane.id);
32001
33240
  const masterModels = models.filter((m) => m.ownScale !== true);
32002
- let dr = this.chrome.paneDrawingsRange(masterModels, this.scene, pane === pricePane, vr);
32003
- const udr = this.userDrawings?.priceRangeForPane(pane.id, vtr.from, vtr.to) ?? null;
32004
- if (udr) dr = dr ? { min: Math.min(dr.min, udr.min), max: Math.max(dr.max, udr.max) } : udr;
33241
+ const dr = this.chrome.paneDrawingsRange(masterModels, this.scene, pane === pricePane, vr);
32005
33242
  const includeCandles = pane.kind === "price" && !this.scene.candlesHidden;
32006
33243
  pane.scaleTarget = computePaneScale(masterModels, this.bars, includeCandles, i0, i1, dr, paneLogScale(this.scene, pane), (id) => this.scene.offsetOf(id));
32007
33244
  pane.percentBaseline = pane.kind === "price" ? this.bars[i0]?.close ?? 0 : this.firstVisibleValue(masterModels, i0);
@@ -33009,16 +34246,16 @@ function fmtPrice(n, dp) {
33009
34246
  const d = dp ?? decimalsFor(n);
33010
34247
  return n.toLocaleString("en-US", { minimumFractionDigits: d, maximumFractionDigits: d });
33011
34248
  }
33012
- function fmtChange(open, close) {
33013
- if (open == null || close == null || !Number.isFinite(open) || !Number.isFinite(close) || open === 0) return "";
33014
- const diff = close - open;
33015
- const pct = diff / open * 100;
34249
+ function fmtChange(open2, close) {
34250
+ if (open2 == null || close == null || !Number.isFinite(open2) || !Number.isFinite(close) || open2 === 0) return "";
34251
+ const diff = close - open2;
34252
+ const pct = diff / open2 * 100;
33016
34253
  const sign = diff >= 0 ? "+" : "";
33017
34254
  return `${sign}${fmtPrice(diff, decimalsFor(close))} (${sign}${pct.toFixed(2)}%)`;
33018
34255
  }
33019
34256
 
33020
34257
  // src/widget/statusline.ts
33021
- var STYLE_ID25 = "vela-widget-statusline";
34258
+ var STYLE_ID24 = "vela-widget-statusline";
33022
34259
  var CSS21 = `
33023
34260
  .vela-statusline {
33024
34261
  position: absolute;
@@ -33210,7 +34447,7 @@ var Statusline = class {
33210
34447
  this.fitMode = false;
33211
34448
  this.fitRO = null;
33212
34449
  const doc = host.ownerDocument;
33213
- injectStyles(STYLE_ID25, CSS21, doc);
34450
+ injectStyles(STYLE_ID24, CSS21, doc);
33214
34451
  host.classList.add("vela-has-statusline");
33215
34452
  this.el = doc.createElement("div");
33216
34453
  this.el.className = "vela-statusline";
@@ -33385,7 +34622,7 @@ var Statusline = class {
33385
34622
  var MAX_FONT_PX = 36;
33386
34623
  var MIN_FONT_PX = 12;
33387
34624
  var FILL = 0.9;
33388
- var STYLE_ID26 = "vela-widget-watermark";
34625
+ var STYLE_ID25 = "vela-widget-watermark";
33389
34626
  var CSS22 = `
33390
34627
  .vela-watermark {
33391
34628
  position: absolute;
@@ -33424,7 +34661,7 @@ var Watermark = class {
33424
34661
  * canvas, so the mark stays out of its way. Starts true: the FIRST `load:start`
33425
34662
  * fires during chart construction, before any subscriber can see it. */
33426
34663
  this.loading = true;
33427
- injectStyles(STYLE_ID26, CSS22, host.ownerDocument);
34664
+ injectStyles(STYLE_ID25, CSS22, host.ownerDocument);
33428
34665
  this.el = host.ownerDocument.createElement("div");
33429
34666
  this.el.className = "vela-watermark";
33430
34667
  this.el.dataset.velaScreenshot = "under";
@@ -34227,7 +35464,7 @@ function buildContext(host) {
34227
35464
  setTimeframe: (tf) => host.active()?.setTimeframe(tf),
34228
35465
  setPriceStyle: (style) => host.active()?.setPriceStyle(style),
34229
35466
  openSymbolSearch: (query) => host.openSymbolSearch(query),
34230
- togglePanel: (id, open) => host.togglePanel(id, open),
35467
+ togglePanel: (id, open2) => host.togglePanel(id, open2),
34231
35468
  host: host.root,
34232
35469
  toast: (message, kind) => host.toast(message, kind),
34233
35470
  cells: host.cells().map((c) => ({ id: c.id, chart: c.chart, symbol: c.symbol, timeframe: c.timeframe })),
@@ -34487,7 +35724,7 @@ var GAP_PX = 2;
34487
35724
  var POOL_CAP = 16;
34488
35725
  var TIME_AXIS_H3 = 22;
34489
35726
  var ALERT_CAP = 50;
34490
- var STYLE_ID27 = "vela-workspace";
35727
+ var STYLE_ID26 = "vela-workspace";
34491
35728
  var CSS23 = `
34492
35729
  .vela-workspace { position: relative; width: 100%; height: 100%; display: flex; flex-direction: column; background: var(--vela-bg); }
34493
35730
  .vela-ws-main { position: relative; display: flex; flex-direction: row; flex: 1 1 auto; min-height: 0; }
@@ -34558,6 +35795,8 @@ var VelaWorkspace = class {
34558
35795
  this.historyUnsub = null;
34559
35796
  /** Favorite drawing tools — a WORKSPACE preference (one star set, every cell). */
34560
35797
  this.favs = [];
35798
+ /** Favorite timeframes — the shared topbar's quick-switch chips, one set for the grid. */
35799
+ this.tfFavs = [];
34561
35800
  /** Live sync configuration (mutable copy of the option). */
34562
35801
  this.syncOpts = {};
34563
35802
  this.stateTimer = null;
@@ -34621,6 +35860,7 @@ var VelaWorkspace = class {
34621
35860
  }
34622
35861
  this.timezone = boot?.timezone ?? opts.timezone ?? "Etc/UTC";
34623
35862
  if (boot?.favorites) this.favs = [...boot.favorites];
35863
+ if (boot?.timeframeFavorites) this.tfFavs = [...boot.timeframeFavorites];
34624
35864
  const sync = boot?.sync ?? opts.sync;
34625
35865
  for (const kind of ["viewport", "symbol", "timeframe", "crosshair", "drawings"]) {
34626
35866
  this.applySyncSetting(kind, sync?.[kind]);
@@ -34631,7 +35871,7 @@ var VelaWorkspace = class {
34631
35871
  this.order = boot?.charts ? boot.charts.map((c) => c.id) : declaredOrder(opts.cells);
34632
35872
  const bootActive = boot?.activeCellId ?? null;
34633
35873
  const doc = hostEl.ownerDocument;
34634
- injectStyles(STYLE_ID27, CSS23, doc);
35874
+ injectStyles(STYLE_ID26, CSS23, doc);
34635
35875
  this.root = doc.createElement("div");
34636
35876
  this.root.className = "vela-workspace";
34637
35877
  ensureUIHost(this.root, resolveTheme(opts.theme));
@@ -34642,9 +35882,9 @@ var VelaWorkspace = class {
34642
35882
  this.symbolPicker = new SymbolPicker({
34643
35883
  host: this.root,
34644
35884
  onSelect: (ticker) => this.active.setSymbol(ticker),
34645
- onOpenChange: (open) => {
34646
- if (open) for (const cell of this.cells()) cell.chart.renderer.closeDialogs();
34647
- this.trackDialog(open);
35885
+ onOpenChange: (open2) => {
35886
+ if (open2) for (const cell of this.cells()) cell.chart.renderer.closeDialogs();
35887
+ this.trackDialog(open2);
34648
35888
  }
34649
35889
  });
34650
35890
  this.symbolPicker.setSource(() => this.feed.symbols());
@@ -34654,16 +35894,16 @@ var VelaWorkspace = class {
34654
35894
  onChart: () => this.active.onChartRows(),
34655
35895
  onAdd: (i) => this.active.addFromLibrary(i),
34656
35896
  onRemove: (i) => this.active.removeFromChart(i),
34657
- onOpenChange: (open) => {
34658
- if (open) for (const cell of this.cells()) cell.chart.renderer.closeDialogs();
34659
- this.trackDialog(open);
35897
+ onOpenChange: (open2) => {
35898
+ if (open2) for (const cell of this.cells()) cell.chart.renderer.closeDialogs();
35899
+ this.trackDialog(open2);
34660
35900
  }
34661
35901
  }) : null;
34662
35902
  const picker = this.indicatorPicker;
34663
35903
  this.tfQuick = new TimeframeQuick({
34664
35904
  host: this.root,
34665
35905
  onApply: (tf) => this.setActiveTimeframe(tf),
34666
- onOpenChange: (open) => this.trackDialog(open)
35906
+ onOpenChange: (open2) => this.trackDialog(open2)
34667
35907
  });
34668
35908
  this.topbar = new Topbar(this.root, {
34669
35909
  symbol: "",
@@ -34675,8 +35915,10 @@ var VelaWorkspace = class {
34675
35915
  onAlertsClick: (anchor) => this.openAlertsMenu(anchor),
34676
35916
  timeframe: "60",
34677
35917
  timeframes: opts.timeframes ?? DEFAULT_TIMEFRAMES,
35918
+ timeframeFavorites: this.tfFavs,
34678
35919
  priceStyle: "candles",
34679
35920
  onTimeframe: (tf) => this.setActiveTimeframe(tf),
35921
+ onTimeframeFavorite: (tf, on) => this.setTimeframeFavorite(tf, on),
34680
35922
  onPriceStyle: (style) => this.active.setPriceStyle(style),
34681
35923
  layout: {
34682
35924
  current: this.def.id,
@@ -34890,7 +36132,7 @@ var VelaWorkspace = class {
34890
36132
  cells: () => this.cells(),
34891
36133
  setActiveCell: (id) => this.setActiveCell(id),
34892
36134
  openSymbolSearch: (query) => this.symbolPicker.open(query ?? ""),
34893
- togglePanel: (id, open) => this.dock.toggle(id, open),
36135
+ togglePanel: (id, open2) => this.dock.toggle(id, open2),
34894
36136
  root: this.root,
34895
36137
  toast: (message, kind) => this.toast.show(message, kind)
34896
36138
  });
@@ -34927,6 +36169,7 @@ var VelaWorkspace = class {
34927
36169
  const state = { version: 1, layout: this.def.id, timezone: this.timezone, sync: { ...this.syncOpts }, charts };
34928
36170
  if (this.activeId) state.activeCellId = this.activeId;
34929
36171
  if (this.favs.length > 0) state.favorites = [...this.favs];
36172
+ if (this.tfFavs.length > 0) state.timeframeFavorites = [...this.tfFavs];
34930
36173
  if (this.trackSizes.size > 0) state.trackSizes = Object.fromEntries([...this.trackSizes].map(([k, v]) => [k, { ...v }]));
34931
36174
  const panels2 = this.dock.getState();
34932
36175
  if (panels2) state.panels = panels2;
@@ -34947,6 +36190,10 @@ var VelaWorkspace = class {
34947
36190
  this.bottombar?.setTimezone(st.timezone);
34948
36191
  }
34949
36192
  if (st.favorites) this.favs = [...st.favorites];
36193
+ if (st.timeframeFavorites) {
36194
+ this.tfFavs = [...st.timeframeFavorites];
36195
+ this.topbar.setTimeframeFavorites(this.tfFavs);
36196
+ }
34950
36197
  this.dock.applyState(st.panels);
34951
36198
  for (const kind of ["viewport", "symbol", "timeframe", "crosshair", "drawings"]) this.applySyncSetting(kind, st.sync?.[kind]);
34952
36199
  this.trackSizes.clear();
@@ -35522,6 +36769,14 @@ var VelaWorkspace = class {
35522
36769
  this.bottombar?.setActiveRange(null);
35523
36770
  this.active.setTimeframe(tf);
35524
36771
  }
36772
+ /** Star/unstar a timeframe — a WORKSPACE preference (one chip row, whatever the
36773
+ * active cell); the topbar follows and the set persists with the document. */
36774
+ setTimeframeFavorite(tf, on) {
36775
+ if (on === this.tfFavs.includes(tf)) return;
36776
+ this.tfFavs = on ? [tf, ...this.tfFavs] : this.tfFavs.filter((f) => f !== tf);
36777
+ this.topbar.setTimeframeFavorites(this.tfFavs);
36778
+ this.markStateDirty();
36779
+ }
35525
36780
  /** The mode flipped (container resized across the breakpoint, or a coarse-pointer
35526
36781
  * change). Close the open surfaces — a desktop card must not linger over the
35527
36782
  * mobile chrome (and vice versa) — and re-present every cell's own chrome. */
@@ -35566,7 +36821,7 @@ var VelaWorkspace = class {
35566
36821
  this.active.applyRange(preset);
35567
36822
  this.bottombar?.setActiveRange(preset.id);
35568
36823
  },
35569
- onOpenChange: (open) => this.trackDialog(open)
36824
+ onOpenChange: (open2) => this.trackDialog(open2)
35570
36825
  }));
35571
36826
  this.tfDrawer.open();
35572
36827
  }
@@ -35579,7 +36834,7 @@ var VelaWorkspace = class {
35579
36834
  isFavorite: (type) => this.active.chart.drawings.isFavorite(type),
35580
36835
  onFavorite: (type, on) => this.active.chart.drawings.setFavorite(type, on),
35581
36836
  onSelect: (type) => this.active.chart.drawings.setTool(type),
35582
- onOpenChange: (open) => this.trackDialog(open)
36837
+ onOpenChange: (open2) => this.trackDialog(open2)
35583
36838
  }));
35584
36839
  this.drawingsDrawer.open();
35585
36840
  }
@@ -35618,7 +36873,7 @@ var VelaWorkspace = class {
35618
36873
  this.sync.set(kind, this.syncOpts[kind] ? false : true);
35619
36874
  }
35620
36875
  },
35621
- onOpenChange: (open) => this.trackDialog(open)
36876
+ onOpenChange: (open2) => this.trackDialog(open2)
35622
36877
  }));
35623
36878
  this.moreDrawer.open();
35624
36879
  }
@@ -35627,7 +36882,7 @@ var VelaWorkspace = class {
35627
36882
  host: this.root,
35628
36883
  timezone: () => this.timezone,
35629
36884
  onTimezone: (zone) => this.setTimezone(zone),
35630
- onOpenChange: (open) => this.trackDialog(open)
36885
+ onOpenChange: (open2) => this.trackDialog(open2)
35631
36886
  }));
35632
36887
  this.timezoneDrawer.open();
35633
36888
  }
@@ -35637,7 +36892,7 @@ var VelaWorkspace = class {
35637
36892
  host: this.root,
35638
36893
  chart: () => this.activeId ? this.cellsById.get(this.activeId)?.chart ?? null : null,
35639
36894
  pressY: () => this.priceScalePressY,
35640
- onOpenChange: (open) => this.trackDialog(open)
36895
+ onOpenChange: (open2) => this.trackDialog(open2)
35641
36896
  }));
35642
36897
  this.priceScaleDrawer.open();
35643
36898
  }
@@ -35721,7 +36976,7 @@ var VelaWorkspace = class {
35721
36976
  label: "Show this shortcuts panel",
35722
36977
  category: "Help",
35723
36978
  run: () => {
35724
- this.shortcutsHelp ?? (this.shortcutsHelp = new ShortcutsHelp(this.keymap, this.root, (open) => this.trackDialog(open)));
36979
+ this.shortcutsHelp ?? (this.shortcutsHelp = new ShortcutsHelp(this.keymap, this.root, (open2) => this.trackDialog(open2)));
35725
36980
  this.shortcutsHelp.open();
35726
36981
  }
35727
36982
  });
@@ -35740,9 +36995,9 @@ var VelaWorkspace = class {
35740
36995
  this.tfQuick.open(key);
35741
36996
  }
35742
36997
  }
35743
- trackDialog(open) {
35744
- this.openDialogs = Math.max(0, this.openDialogs + (open ? 1 : -1));
35745
- if (open) this.keymap.pushScope("dialog");
36998
+ trackDialog(open2) {
36999
+ this.openDialogs = Math.max(0, this.openDialogs + (open2 ? 1 : -1));
37000
+ if (open2) this.keymap.pushScope("dialog");
35746
37001
  else this.keymap.popScope("dialog");
35747
37002
  }
35748
37003
  /** Pool a dehydrated slot state (bounded — oldest entries drop past the cap). */