@luxalgo/vela 0.6.3 → 0.6.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/dist/{DataProvider-DHX4x6-r.d.ts → DataProvider-BmHkcwvJ.d.ts} +1 -1
  2. package/dist/{DataProvider-CNmk84SH.d.cts → DataProvider-DrN7ZIou.d.cts} +1 -1
  3. package/dist/{chunk-TP56QRMK.js → chunk-2R7BANEM.js} +1 -1
  4. package/dist/{chunk-7Y7CJ7EN.js → chunk-BEJ57HP4.js} +8 -1
  5. package/dist/{chunk-7D6YIF34.js → chunk-FQBT4NFM.js} +431 -74
  6. package/dist/{chunk-U5KCQHAC.js → chunk-N6MBJHMV.js} +129 -41
  7. package/dist/{chunk-QHZ7IXFL.js → chunk-OICPLNU7.js} +59 -9
  8. package/dist/{contributions-DDpv7hEL.d.cts → contributions-2bEmCF9S.d.cts} +2 -2
  9. package/dist/{contributions-ciV6Neyd.d.ts → contributions-DatoAzMx.d.ts} +2 -2
  10. package/dist/{history-CFqZm5re.d.ts → history-BmyjaVir.d.cts} +6 -6
  11. package/dist/{history-ChoT34nz.d.cts → history-CvOtsjT5.d.ts} +6 -6
  12. package/dist/index.cjs +500 -85
  13. package/dist/index.d.cts +63 -9
  14. package/dist/index.d.ts +63 -9
  15. package/dist/index.js +4 -4
  16. package/dist/{options-BqGeFHtp.d.ts → options-YdiaaVjt.d.cts} +20 -1
  17. package/dist/{options-BqGeFHtp.d.cts → options-YdiaaVjt.d.ts} +20 -1
  18. package/dist/{plugin-C97gUCVf.d.ts → plugin-Bcone6Vh.d.ts} +9 -5
  19. package/dist/{plugin-CSb_sTiN.d.cts → plugin-DO_DX0gp.d.cts} +9 -5
  20. package/dist/plugin.cjs +8 -0
  21. package/dist/plugin.d.cts +4 -4
  22. package/dist/plugin.d.ts +4 -4
  23. package/dist/plugin.js +2 -2
  24. package/dist/providers/binance.d.cts +2 -2
  25. package/dist/providers/binance.d.ts +2 -2
  26. package/dist/providers/coinbase.d.cts +2 -2
  27. package/dist/providers/coinbase.d.ts +2 -2
  28. package/dist/providers/hyperliquid.d.cts +2 -2
  29. package/dist/providers/hyperliquid.d.ts +2 -2
  30. package/dist/ui.cjs +59 -9
  31. package/dist/ui.d.cts +10 -2
  32. package/dist/ui.d.ts +10 -2
  33. package/dist/ui.js +2 -2
  34. package/dist/vela.global.js +500 -85
  35. package/dist/vela.global.min.js +84 -47
  36. package/dist/widget.cjs +801 -227
  37. package/dist/widget.d.cts +34 -8
  38. package/dist/widget.d.ts +34 -8
  39. package/dist/widget.js +145 -73
  40. package/dist/workspace.cjs +756 -178
  41. package/dist/workspace.d.cts +29 -5
  42. package/dist/workspace.d.ts +29 -5
  43. package/dist/workspace.js +143 -67
  44. package/package.json +1 -1
  45. /package/dist/{chunk-Q3XQHLIH.js → chunk-AOGZBKUE.js} +0 -0
@@ -960,8 +960,13 @@ var STATIC_CSS = `
960
960
  ${STATIC_DECLS}
961
961
  font-family: var(--vela-font, -apple-system, system-ui, sans-serif);
962
962
  box-sizing: border-box;
963
+ /* Chrome text (titles, buttons, menus, readouts) is UI, not copy \u2014 never selectable. */
964
+ user-select: none;
965
+ -webkit-user-select: none;
963
966
  }
964
967
  .vela-ui *, .vela-ui-layer * { box-sizing: border-box; }
968
+ /* Text ENTRY is the one exception: selection is part of editing. */
969
+ .vela-ui :is(input, textarea), .vela-ui-layer :is(input, textarea) { user-select: text; -webkit-user-select: text; }
965
970
  .vela-icon { display: inline-flex; align-items: center; flex: none; }
966
971
  .vela-icon svg { display: block; }
967
972
  `;
@@ -1449,6 +1454,23 @@ var MENU_CSS = `
1449
1454
  background: var(--vela-hover-strong);
1450
1455
  color: var(--vela-fg-bright);
1451
1456
  }
1457
+ /* Checkmark mode (context/action menus): every row reserves the leading mark column so
1458
+ labels align; a checked row fills it with a \u2713 and brightens its ink \u2014 the row surface
1459
+ stays free for the hover wash. */
1460
+ .vela-menu-item .vela-menu-mark {
1461
+ width: 14px;
1462
+ flex: none;
1463
+ display: inline-flex;
1464
+ align-items: center;
1465
+ justify-content: center;
1466
+ }
1467
+ .vela-menu-item .vela-menu-mark .vela-icon {
1468
+ width: 14px;
1469
+ height: 14px;
1470
+ font-size: 14px;
1471
+ color: var(--vela-fg-bright);
1472
+ }
1473
+ .vela-menu-item[data-checkmark] { color: var(--vela-fg-bright); }
1452
1474
  /* Switch rows (boolean settings in a dropdown): a right-aligned toggle pill \u2014 the
1453
1475
  same control language as the settings dialog's toggles. */
1454
1476
  .vela-menu-switch {
@@ -1517,6 +1539,7 @@ var Surface = class _Surface {
1517
1539
  this.host = opts.host;
1518
1540
  this.onSelect = opts.onSelect;
1519
1541
  this.onFavorite = opts.onFavorite;
1542
+ this.checkmarks = opts.checkmarks === true;
1520
1543
  this.positioner = doc.createElement("div");
1521
1544
  this.positioner.className = "vela-ui-layer";
1522
1545
  this.list = doc.createElement("ul");
@@ -1595,6 +1618,7 @@ var Surface = class _Surface {
1595
1618
  build() {
1596
1619
  const doc = this.doc;
1597
1620
  this.list.replaceChildren();
1621
+ const markable = this.checkmarks && this.items.some((i) => !(i.submenu && i.submenu.length > 0) && !i.toggle && i.checked !== void 0);
1598
1622
  for (const item of this.items) {
1599
1623
  if (item.separatorBefore) {
1600
1624
  const sep = doc.createElement("li");
@@ -1608,7 +1632,16 @@ var Surface = class _Surface {
1608
1632
  li.dataset.veiId = item.id;
1609
1633
  if (item.toggle) {
1610
1634
  li.dataset.toggle = "1";
1611
- } else if (!branch && item.checked) {
1635
+ }
1636
+ if (markable) {
1637
+ const mark = doc.createElement("span");
1638
+ mark.className = "vela-menu-mark";
1639
+ if (!branch && !item.toggle && item.checked) {
1640
+ mark.appendChild(iconEl("check", doc));
1641
+ li.dataset.checkmark = "1";
1642
+ }
1643
+ li.appendChild(mark);
1644
+ } else if (!branch && !item.toggle && item.checked) {
1612
1645
  li.dataset.checked = "1";
1613
1646
  }
1614
1647
  if (item.icon) li.appendChild(iconEl(item.icon, doc));
@@ -1657,6 +1690,7 @@ var Surface = class _Surface {
1657
1690
  placement: "right-start",
1658
1691
  onSelect: this.onSelect,
1659
1692
  onFavorite: this.onFavorite,
1693
+ checkmarks: this.checkmarks,
1660
1694
  id: `${this.mid}--${item.id}`
1661
1695
  });
1662
1696
  sub.setItems(item.submenu ?? []);
@@ -1681,7 +1715,8 @@ var Menu = class {
1681
1715
  triggerId: opts.triggerId,
1682
1716
  id: opts.id,
1683
1717
  minWidth: opts.minWidth,
1684
- onFavorite: opts.onFavorite
1718
+ onFavorite: opts.onFavorite,
1719
+ checkmarks: opts.checkmarks
1685
1720
  });
1686
1721
  this.root.setItems(opts.items);
1687
1722
  }
@@ -2972,6 +3007,9 @@ var NUMBER_CSS = `
2972
3007
  transition: border-color 0.12s ease, box-shadow 0.12s ease;
2973
3008
  -moz-appearance: textfield;
2974
3009
  appearance: textfield;
3010
+ /* Selectable even under a user-select:none host (chart wrapper, .vela-ui). */
3011
+ user-select: text;
3012
+ -webkit-user-select: text;
2975
3013
  }
2976
3014
  .vela-num input::-webkit-inner-spin-button, .vela-num input::-webkit-outer-spin-button { -webkit-appearance: none; margin: 0; }
2977
3015
  .vela-num input:hover { border-color: var(--vela-fg-muted); }
@@ -3184,6 +3222,9 @@ var TEXT_CSS = `
3184
3222
  overflow: hidden;
3185
3223
  text-overflow: ellipsis;
3186
3224
  transition: border-color 0.12s ease, box-shadow 0.12s ease;
3225
+ /* Selectable even under a user-select:none host (chart wrapper, .vela-ui). */
3226
+ user-select: text;
3227
+ -webkit-user-select: text;
3187
3228
  }
3188
3229
  .vela-text-field:hover { border-color: var(--vela-fg-muted); }
3189
3230
  .vela-text-field:focus { border-color: var(--vela-focus); box-shadow: 0 0 0 3px var(--vela-focus-soft); }
@@ -3528,6 +3569,9 @@ var TEXTAREA_CSS = `
3528
3569
  resize: vertical;
3529
3570
  outline: none;
3530
3571
  transition: border-color 0.12s ease, box-shadow 0.12s ease;
3572
+ /* Selectable even under a user-select:none host (chart wrapper, .vela-ui). */
3573
+ user-select: text;
3574
+ -webkit-user-select: text;
3531
3575
  }
3532
3576
  .vela-textarea-field:hover { border-color: var(--vela-fg-muted); }
3533
3577
  .vela-textarea-field:focus { border-color: var(--vela-focus); box-shadow: 0 0 0 3px var(--vela-focus-soft); }
@@ -3805,6 +3849,8 @@ var FIELD_CSS = `
3805
3849
  font-size: 14px;
3806
3850
  }
3807
3851
  .vela-field-label[data-size='sm'] { font-size: inherit; opacity: 0.85; }
3852
+ /* Titles are fully inert: spans, not label[for] \u2014 native labels propagate :hover and
3853
+ clicks onto their control, and both belong to the input alone. */
3808
3854
  .vela-field-cell { display: flex; align-items: center; gap: 8px; justify-self: start; }
3809
3855
  .vela-field-bool { display: flex; align-items: center; gap: 8px; min-height: 22px; }
3810
3856
  .vela-field-stacked { display: flex; flex-direction: column; gap: 8px; }
@@ -3863,11 +3909,17 @@ function fieldSeparator() {
3863
3909
  return el;
3864
3910
  }
3865
3911
  function labelEl(text, opts) {
3866
- const el = opts.id ? document.createElement("label") : document.createElement("span");
3867
- if (opts.id && el instanceof HTMLLabelElement) el.htmlFor = opts.id;
3912
+ const el = document.createElement("span");
3868
3913
  el.className = "vela-field-label";
3869
3914
  if (opts.size) el.dataset.size = opts.size;
3870
3915
  el.textContent = text;
3916
+ const id = opts.id;
3917
+ if (id) {
3918
+ el.id = `${id}--title`;
3919
+ queueMicrotask(() => {
3920
+ el.ownerDocument.getElementById(id)?.setAttribute("aria-labelledby", el.id);
3921
+ });
3922
+ }
3871
3923
  return el;
3872
3924
  }
3873
3925
  function asList(control) {
@@ -3896,9 +3948,7 @@ function fieldRow(opts) {
3896
3948
  it.className = "vela-field-inline-item";
3897
3949
  if (item.toggleFirst) it.style.flexDirection = "row-reverse";
3898
3950
  if (item.label) {
3899
- const lbl = labelEl(item.label, { id: item.id, size });
3900
- if (item.toggleFirst) lbl.style.cursor = "pointer";
3901
- it.appendChild(lbl);
3951
+ it.appendChild(labelEl(item.label, { id: item.id, size }));
3902
3952
  }
3903
3953
  if (item.fit) it.appendChild(item.control);
3904
3954
  else {
@@ -3923,7 +3973,7 @@ function fieldRow(opts) {
3923
3973
  tone: "bright",
3924
3974
  onChange: (v) => opts.toggle?.onChange(v)
3925
3975
  });
3926
- wrap2.append(sw.el, labelEl(opts.label, { size }));
3976
+ wrap2.append(sw.el, labelEl(opts.label, { id: opts.toggle?.id ?? opts.id, size }));
3927
3977
  if (opts.info) wrap2.appendChild(opts.info);
3928
3978
  if (opts.toggle?.get) {
3929
3979
  const get = opts.toggle.get;
@@ -3962,7 +4012,7 @@ function fieldRow(opts) {
3962
4012
  if (controls.length > 0) dimControls(controls, v);
3963
4013
  }
3964
4014
  });
3965
- left.append(sw.el, labelEl(opts.label, { size }));
4015
+ left.append(sw.el, labelEl(opts.label, { id: opts.toggle.id ?? opts.id, size }));
3966
4016
  wrap.appendChild(left);
3967
4017
  if (controls.length > 0) dimControls(controls, opts.toggle.checked);
3968
4018
  if (opts.toggle.get) {
@@ -4533,6 +4583,8 @@ var BASELINE_BOTTOM_LINE = BEARISH;
4533
4583
  var BASELINE_FILL_ALPHA = 0.25;
4534
4584
  var BASELINE_FILL_ALPHA_FAR = 0.05;
4535
4585
  var BASELINE_LEVEL_DEFAULT = 50;
4586
+ var PREMARKET_SHADE = withAlpha(WARNING, 0.08);
4587
+ var POSTMARKET_SHADE = withAlpha(ACCENT, 0.08);
4536
4588
  function defaultChartStyle() {
4537
4589
  return {
4538
4590
  chartTypes: {},
@@ -4563,7 +4615,8 @@ function defaultChartStyle() {
4563
4615
  bottomFillColor2: null,
4564
4616
  width: 2,
4565
4617
  baselineLevel: BASELINE_LEVEL_DEFAULT
4566
- }
4618
+ },
4619
+ sessions: { premarketColor: PREMARKET_SHADE, postmarketColor: POSTMARKET_SHADE }
4567
4620
  };
4568
4621
  }
4569
4622
  var LINE_STYLES = ["solid", "dashed", "dotted"];
@@ -4697,6 +4750,7 @@ function mergeConfig(base, patch) {
4697
4750
  const area = asObject(p.area);
4698
4751
  const baseline = asObject(p.baseline);
4699
4752
  const series = asObject(p.series);
4753
+ const sessions = asObject(p.sessions);
4700
4754
  const stacking = asObject(p.stacking);
4701
4755
  const stackSeries = asObject(stacking.series);
4702
4756
  return {
@@ -4789,6 +4843,10 @@ function mergeConfig(base, patch) {
4789
4843
  baseline: series.baseline === null ? null : isNum(series.baseline) ? series.baseline : base.series.baseline,
4790
4844
  spacing: isNum(series.spacing) ? clampSpacing(series.spacing) : base.series.spacing
4791
4845
  },
4846
+ sessions: {
4847
+ premarketColor: isColor(sessions.premarketColor) ? sessions.premarketColor : base.sessions.premarketColor,
4848
+ postmarketColor: isColor(sessions.postmarketColor) ? sessions.postmarketColor : base.sessions.postmarketColor
4849
+ },
4792
4850
  stacking: {
4793
4851
  candles: isNum(stacking.candles) ? stacking.candles : base.stacking.candles,
4794
4852
  // Additive like `chartTypes`: a patch names only the ids it carries; the rest keep
@@ -4961,12 +5019,14 @@ var CSS3 = `
4961
5019
  color: var(--vela-fg-muted);
4962
5020
  }
4963
5021
  .vela-widget-tf-caret:hover { background: var(--vela-hover); color: var(--vela-fg-bright); }
5022
+ /* The merged trigger is a plain button (hover feedback only) \u2014 the highlight
5023
+ background marks the CURRENT chip among favorites, and a lone trigger with a
5024
+ permanent highlight would read as stuck-pressed. */
4964
5025
  .vela-widget-tf-caret[data-solo='1'] {
4965
5026
  width: auto;
4966
5027
  padding: 0 6px 0 9px;
4967
5028
  gap: 4px;
4968
5029
  color: var(--vela-fg-bright);
4969
- background: var(--vela-hover-strong);
4970
5030
  font-size: 13px;
4971
5031
  font-weight: 550;
4972
5032
  white-space: nowrap;
@@ -5565,7 +5625,8 @@ var Bottombar = class {
5565
5625
  const session = doc.createElement("span");
5566
5626
  session.className = "vela-bb-session";
5567
5627
  this.sessionEl = session;
5568
- session.title = "Session \u2014 stocks & ETFs only";
5628
+ session.style.display = "none";
5629
+ session.title = "Session \u2014 regular (RTH) vs extended (ETH) hours";
5569
5630
  for (const [key, label] of [["regular", "RTH"], ["extended", "ETH"]]) {
5570
5631
  const b = doc.createElement("button");
5571
5632
  b.className = "vela-bb-session-btn" + (key === "regular" ? " is-active" : "");
@@ -5616,15 +5677,15 @@ var Bottombar = class {
5616
5677
  }
5617
5678
  /**
5618
5679
  * Reflect the ACTIVE chart's session posture. `enabled: false` (a continuous
5619
- * market, or metadata not landed yet) shows the disabled RTH-active stub the
5620
- * pre-session-model look, byte-for-byte. Enabled, the chips become clickable and
5621
- * the active one tracks the chart's current session.
5680
+ * market, or metadata not landed yet) HIDES the toggle entirelyRTH/ETH is
5681
+ * meaningless there. Enabled, the chips appear and the active one tracks the
5682
+ * chart's current session.
5622
5683
  */
5623
5684
  setSession(state) {
5624
- if (this.sessionEl) this.sessionEl.title = state.enabled ? "Session \u2014 regular (RTH) vs extended (ETH) hours" : "Session \u2014 stocks & ETFs only";
5685
+ if (this.sessionEl) this.sessionEl.style.display = state.enabled ? "" : "none";
5625
5686
  for (const [key, b] of this.sessionButtons) {
5626
5687
  b.disabled = !state.enabled;
5627
- b.classList.toggle("is-active", state.enabled ? key === state.session : key === "regular");
5688
+ b.classList.toggle("is-active", key === (state.enabled ? state.session : "regular"));
5628
5689
  }
5629
5690
  }
5630
5691
  destroy() {
@@ -12963,6 +13024,9 @@ var CSS7 = `
12963
13024
  letter-spacing: 0.08em;
12964
13025
  }
12965
13026
  .vela-dw-group:first-child { border-top: none; margin-top: 0; padding-top: 8px; }
13027
+ /* The readout is DATA, not chrome: selectable (an exception to the UI-wide
13028
+ user-select:none) so values can be copied out. The panel header stays chrome. */
13029
+ .vela-dw-group, .vela-dw-row { user-select: text; -webkit-user-select: text; cursor: text; }
12966
13030
  .vela-dw-row {
12967
13031
  display: flex;
12968
13032
  align-items: center;
@@ -13343,6 +13407,7 @@ var CSS8 = `
13343
13407
  .vela-sp-badge[data-p='hyperliquid'] { color: #50d2c1; } /* palette-exempt: venue brand mark */
13344
13408
  .vela-sp-empty { padding: var(--vela-space-3); color: var(--vela-fg-muted); text-align: center; }
13345
13409
  `;
13410
+ var PAGE = 100;
13346
13411
  var SymbolPicker = class {
13347
13412
  constructor(opts) {
13348
13413
  this.source = () => [];
@@ -13350,6 +13415,7 @@ var SymbolPicker = class {
13350
13415
  this.highlighted = 0;
13351
13416
  this.seed = "";
13352
13417
  this.activeTab = "All";
13418
+ this.visible = PAGE;
13353
13419
  const doc = (opts.host ?? document.body).ownerDocument;
13354
13420
  injectStyles(STYLE_ID7, CSS8, doc);
13355
13421
  this.input = doc.createElement("input");
@@ -13376,6 +13442,12 @@ var SymbolPicker = class {
13376
13442
  }
13377
13443
  this.list = doc.createElement("div");
13378
13444
  this.list.className = "vela-sp-list";
13445
+ this.list.addEventListener("scroll", () => {
13446
+ if (this.rows.length < this.visible) return;
13447
+ if (this.list.scrollTop + this.list.clientHeight < this.list.scrollHeight - 200) return;
13448
+ this.visible += PAGE;
13449
+ this.grow();
13450
+ });
13379
13451
  this.dialog = new Dialog({
13380
13452
  title: "Symbol Search",
13381
13453
  host: opts.host,
@@ -13441,11 +13513,15 @@ var SymbolPicker = class {
13441
13513
  } else delete el.dataset.highlighted;
13442
13514
  });
13443
13515
  }
13444
- refresh() {
13445
- const doc = this.list.ownerDocument;
13516
+ computeRows() {
13446
13517
  const TAB_TYPES = { Crypto: ["crypto"], Stocks: ["stock"], ETFs: ["etf"], Forex: ["forex"], Commodities: ["commodity"] };
13447
13518
  const pool = this.activeTab === "All" ? this.source() : this.source().filter((s) => TAB_TYPES[this.activeTab]?.includes((s.type ?? "").toLowerCase()) || this.activeTab === "Crypto" && (s.type ?? "").toLowerCase() === "futures");
13448
- this.rows = filterSymbols(pool, this.input.value);
13519
+ return filterSymbols(pool, this.input.value, this.visible);
13520
+ }
13521
+ refresh() {
13522
+ const doc = this.list.ownerDocument;
13523
+ this.visible = PAGE;
13524
+ this.rows = this.computeRows();
13449
13525
  this.highlighted = 0;
13450
13526
  this.list.replaceChildren();
13451
13527
  if (!this.rows.length) {
@@ -13455,34 +13531,42 @@ var SymbolPicker = class {
13455
13531
  this.list.appendChild(empty);
13456
13532
  return;
13457
13533
  }
13458
- for (const s of this.rows) {
13459
- const row = doc.createElement("div");
13460
- row.className = "vela-sp-row";
13461
- row.dataset.ticker = s.ticker;
13462
- const venue = s.prefix ?? s.provider;
13463
- if (venue) row.dataset.venue = venue;
13464
- const av = tickerIconEl(doc, baseOf(s), s.ticker, "vela-sp-avatar");
13465
- const main = doc.createElement("span");
13466
- main.className = "vela-sp-main";
13467
- const t = doc.createElement("span");
13468
- t.className = "vela-sp-ticker";
13469
- t.textContent = s.ticker;
13470
- const d = doc.createElement("span");
13471
- d.className = "vela-sp-desc";
13472
- d.textContent = s.description ?? (s.type ?? "");
13473
- main.append(t, d);
13474
- row.append(av, main);
13475
- if (venue) {
13476
- const badge = doc.createElement("span");
13477
- badge.className = "vela-sp-badge";
13478
- badge.dataset.p = s.provider ?? venue;
13479
- badge.textContent = venue;
13480
- row.appendChild(badge);
13481
- }
13482
- this.list.appendChild(row);
13483
- }
13534
+ for (const s of this.rows) this.list.appendChild(this.rowEl(s));
13484
13535
  this.renderHighlight();
13485
13536
  }
13537
+ /** Append the page the grown `visible` just uncovered — rows already on screen stay put. */
13538
+ grow() {
13539
+ const already = this.rows.length;
13540
+ this.rows = this.computeRows();
13541
+ for (const s of this.rows.slice(already)) this.list.appendChild(this.rowEl(s));
13542
+ }
13543
+ rowEl(s) {
13544
+ const doc = this.list.ownerDocument;
13545
+ const row = doc.createElement("div");
13546
+ row.className = "vela-sp-row";
13547
+ row.dataset.ticker = s.ticker;
13548
+ const venue = s.prefix ?? s.provider;
13549
+ if (venue) row.dataset.venue = venue;
13550
+ const av = tickerIconEl(doc, baseOf(s), s.ticker, "vela-sp-avatar");
13551
+ const main = doc.createElement("span");
13552
+ main.className = "vela-sp-main";
13553
+ const t = doc.createElement("span");
13554
+ t.className = "vela-sp-ticker";
13555
+ t.textContent = s.ticker;
13556
+ const d = doc.createElement("span");
13557
+ d.className = "vela-sp-desc";
13558
+ d.textContent = s.description ?? (s.type ?? "");
13559
+ main.append(t, d);
13560
+ row.append(av, main);
13561
+ if (venue) {
13562
+ const badge = doc.createElement("span");
13563
+ badge.className = "vela-sp-badge";
13564
+ badge.dataset.p = s.provider ?? venue;
13565
+ badge.textContent = venue;
13566
+ row.appendChild(badge);
13567
+ }
13568
+ return row;
13569
+ }
13486
13570
  };
13487
13571
 
13488
13572
  // src/widget/indicator-picker.ts
@@ -16169,7 +16253,7 @@ function attachChromeTooltip(anchor, opts) {
16169
16253
  } else {
16170
16254
  const left = Math.min(a.left - h.left, Math.max(0, h.width - tip.offsetWidth - 4));
16171
16255
  tip.style.left = `${Math.max(0, left)}px`;
16172
- tip.style.top = `${a.bottom - h.top + 6}px`;
16256
+ tip.style.top = opts.placement === "above" ? `${Math.max(0, a.top - h.top - tip.offsetHeight - 6)}px` : `${a.bottom - h.top + 6}px`;
16173
16257
  }
16174
16258
  };
16175
16259
  const arm = (e) => {
@@ -19342,6 +19426,13 @@ var DrawingsControl = class {
19342
19426
  }
19343
19427
  };
19344
19428
 
19429
+ // src/core/model/inputs.ts
19430
+ function inputVisible(when, values) {
19431
+ if (!when) return true;
19432
+ const conds = Array.isArray(when) ? when : [when];
19433
+ return conds.every((c) => c.anyOf ? c.anyOf.some((x) => x === values[c.key]) : values[c.key] === c.equals);
19434
+ }
19435
+
19345
19436
  // src/renderers/shared/IndicatorInputsDialog.ts
19346
19437
  var SOURCES = ["close", "open", "high", "low", "hl2", "hlc3", "ohlc4", "volume"];
19347
19438
  var IndicatorInputsDialog = class {
@@ -19354,6 +19445,8 @@ var IndicatorInputsDialog = class {
19354
19445
  this.row = null;
19355
19446
  this.snapshot = null;
19356
19447
  this.dialogTips = [];
19448
+ /** Re-applies every `when` gate against current values; set while the dialog is open. */
19449
+ this.refreshVisibility = null;
19357
19450
  this.choicePop = null;
19358
19451
  this.calendarPop = null;
19359
19452
  this.calendarAnchor = null;
@@ -19416,14 +19509,17 @@ var IndicatorInputsDialog = class {
19416
19509
  tabs.style.cssText = `display:flex;gap:12px;padding:0 20px;border-bottom:1px solid ${border};flex:0 0 auto;`;
19417
19510
  const tabEls = [];
19418
19511
  const bodies = [];
19512
+ const gates = [];
19513
+ let active = 0;
19419
19514
  const activate = (idx) => {
19515
+ active = idx;
19420
19516
  for (let i = 0; i < tabEls.length; i += 1) {
19421
- const active = i === idx;
19517
+ const on = i === idx;
19422
19518
  const el = tabEls[i];
19423
- el.style.borderBottomColor = active ? fg : "transparent";
19424
- el.style.color = active ? "inherit" : "var(--vela-fg-muted)";
19425
- el.classList.toggle("vela-ind-tab-active", active);
19426
- bodies[i].style.display = active ? "grid" : "none";
19519
+ el.style.borderBottomColor = on ? fg : "transparent";
19520
+ el.style.color = on ? "inherit" : "var(--vela-fg-muted)";
19521
+ el.classList.toggle("vela-ind-tab-active", on);
19522
+ bodies[i].style.display = on ? "grid" : "none";
19427
19523
  }
19428
19524
  };
19429
19525
  for (const [idx, def] of tabDefs.entries()) {
@@ -19434,11 +19530,24 @@ var IndicatorInputsDialog = class {
19434
19530
  tab.addEventListener("click", () => activate(idx));
19435
19531
  tabs.appendChild(tab);
19436
19532
  tabEls.push(tab);
19437
- bodies.push(this.buildTabBody(def.inputs, row));
19533
+ bodies.push(this.buildTabBody(def.inputs, row, gates));
19534
+ if (def.inputs.every((d) => d.when)) {
19535
+ gates.push({ el: tab, visible: (bag) => def.inputs.some((d) => inputVisible(d.when, bag)) });
19536
+ }
19438
19537
  }
19439
19538
  ui.body.appendChild(tabs);
19440
19539
  for (const b of bodies) ui.body.appendChild(b);
19441
19540
  activate(0);
19541
+ const refresh = () => {
19542
+ const bag = valuesBag(row);
19543
+ for (const g of gates) g.el.style.display = g.visible(bag) ? "" : "none";
19544
+ if (tabEls[active]?.style.display === "none") {
19545
+ const next = tabEls.findIndex((el) => el.style.display !== "none");
19546
+ if (next >= 0) activate(next);
19547
+ }
19548
+ };
19549
+ this.refreshVisibility = refresh;
19550
+ refresh();
19442
19551
  const onBackdropDown = (e) => {
19443
19552
  if (e.target !== ui.backdrop && e.target !== ui.positioner) return;
19444
19553
  if (isPopoverOpen() || eventDismissedPopover(e)) {
@@ -19458,16 +19567,27 @@ var IndicatorInputsDialog = class {
19458
19567
  }
19459
19568
  /** One tab's scrollable body — one shared grid so every group's controls share a
19460
19569
  * left edge (a 100px number field lines up with a wider session/time pair). */
19461
- buildTabBody(inputs, row) {
19570
+ buildTabBody(inputs, row, gates) {
19462
19571
  const body = document.createElement("div");
19463
19572
  body.style.cssText = inputDialogBodyStyle(this.host.mobile());
19464
19573
  for (const group of groupInputs(inputs)) {
19465
19574
  const section = document.createElement("div");
19466
19575
  section.style.cssText = "display:contents;";
19576
+ let header = null;
19467
19577
  if (group.name) {
19468
- section.appendChild(fieldSection(group.name, { variant: "inputs", first: body.childElementCount === 0 }));
19578
+ header = fieldSection(group.name, { variant: "inputs", first: body.childElementCount === 0 });
19579
+ section.appendChild(header);
19580
+ }
19581
+ for (const inputRow of group.rows) {
19582
+ const el = this.buildInputRow(section, inputRow, row);
19583
+ if (inputRow.some((d) => d.when)) {
19584
+ gates.push({ el, visible: (bag) => rowVisible(inputRow, bag) });
19585
+ }
19586
+ }
19587
+ if (header && group.rows.every((r) => r.every((d) => d.when))) {
19588
+ const rows = group.rows;
19589
+ gates.push({ el: header, visible: (bag) => rows.some((r) => rowVisible(r, bag)) });
19469
19590
  }
19470
- for (const inputRow of group.rows) this.buildInputRow(section, inputRow, row);
19471
19591
  body.appendChild(section);
19472
19592
  }
19473
19593
  return body;
@@ -19475,6 +19595,7 @@ var IndicatorInputsDialog = class {
19475
19595
  close() {
19476
19596
  document.removeEventListener("keydown", this.onDialogKey);
19477
19597
  closeOpenPopovers();
19598
+ this.refreshVisibility = null;
19478
19599
  this.choicePop = null;
19479
19600
  this.calendarPop = null;
19480
19601
  this.calendarAnchor = null;
@@ -19504,11 +19625,21 @@ var IndicatorInputsDialog = class {
19504
19625
  }
19505
19626
  this.close();
19506
19627
  }
19628
+ /** Write one edit through: store it, notify the host, and re-apply the `when` gates. */
19629
+ commit(row, key, value) {
19630
+ row.values[key] = value;
19631
+ this.host.onChange?.({ indicatorId: row.id, key, value });
19632
+ this.refreshVisibility?.();
19633
+ }
19507
19634
  /** One settings row (or several `inline=` inputs) placed into a section's grid. */
19508
19635
  buildInputRow(grid, decls, row) {
19509
19636
  const idOf = (inp) => `vela-inp-${row.id}-${inp.key}`;
19637
+ const append = (el) => {
19638
+ grid.appendChild(el);
19639
+ return el;
19640
+ };
19510
19641
  if (decls.length > 1) {
19511
- grid.appendChild(fieldRow({
19642
+ return append(fieldRow({
19512
19643
  label: "",
19513
19644
  inline: decls.map((d) => ({
19514
19645
  label: nameOf(d) || void 0,
@@ -19519,14 +19650,13 @@ var IndicatorInputsDialog = class {
19519
19650
  })),
19520
19651
  info: [...decls].reverse().find((d) => d.tooltip)?.tooltip ? this.infoButton([...decls].reverse().find((d) => d.tooltip).tooltip) : void 0
19521
19652
  }));
19522
- return;
19523
19653
  }
19524
19654
  const lead = decls[0];
19525
19655
  const id = idOf(lead);
19526
19656
  const info = lead.tooltip ? this.infoButton(lead.tooltip) : void 0;
19527
19657
  if (lead.type === "bool") {
19528
19658
  const current = Boolean(row.values[lead.key] ?? lead.defval);
19529
- grid.appendChild(fieldRow({
19659
+ return append(fieldRow({
19530
19660
  label: nameOf(lead),
19531
19661
  id,
19532
19662
  bool: true,
@@ -19534,26 +19664,21 @@ var IndicatorInputsDialog = class {
19534
19664
  toggle: {
19535
19665
  id,
19536
19666
  checked: current,
19537
- onChange: (v) => {
19538
- row.values[lead.key] = v;
19539
- this.host.onChange?.({ indicatorId: row.id, key: lead.key, value: v });
19540
- }
19667
+ onChange: (v) => this.commit(row, lead.key, v)
19541
19668
  }
19542
19669
  }));
19543
- return;
19544
19670
  }
19545
19671
  if (lead.type === "text_area") {
19546
- grid.appendChild(fieldRow({
19672
+ return append(fieldRow({
19547
19673
  label: nameOf(lead),
19548
19674
  id,
19549
19675
  stacked: true,
19550
19676
  info,
19551
19677
  control: this.buildControl(row, lead, id)
19552
19678
  }));
19553
- return;
19554
19679
  }
19555
19680
  const fit = lead.type === "color" || lead.type === "session" || lead.type === "time";
19556
- grid.appendChild(fieldRow({
19681
+ return append(fieldRow({
19557
19682
  label: nameOf(lead),
19558
19683
  id,
19559
19684
  fit,
@@ -19565,10 +19690,7 @@ var IndicatorInputsDialog = class {
19565
19690
  /** Build the typed control for one input, committing edits live via `onChange`. */
19566
19691
  buildControl(row, inp, id) {
19567
19692
  const current = row.values[inp.key] ?? inp.defval;
19568
- const emit = (value) => {
19569
- row.values[inp.key] = value;
19570
- this.host.onChange?.({ indicatorId: row.id, key: inp.key, value });
19571
- };
19693
+ const emit = (value) => this.commit(row, inp.key, value);
19572
19694
  if (inp.type === "bool") {
19573
19695
  return buildFieldControl({ kind: "switch", id, checked: Boolean(current), onChange: (v) => emit(v) }).el;
19574
19696
  }
@@ -19989,6 +20111,14 @@ function tabInputs(inputs) {
19989
20111
  }
19990
20112
  return names.map((name) => ({ name, inputs: byTab.get(name) }));
19991
20113
  }
20114
+ function rowVisible(decls, bag) {
20115
+ return decls.some((d) => inputVisible(d.when, bag));
20116
+ }
20117
+ function valuesBag(row) {
20118
+ const bag = {};
20119
+ for (const inp of row.inputs) bag[inp.key] = row.values[inp.key] ?? inp.defval;
20120
+ return bag;
20121
+ }
19992
20122
  function groupInputs(inputs) {
19993
20123
  const order = [];
19994
20124
  const byGroup = /* @__PURE__ */ new Map();
@@ -20451,6 +20581,8 @@ var InputsUI = class {
20451
20581
  host: this.container,
20452
20582
  items,
20453
20583
  placement: "bottom-start",
20584
+ // Right-click action menu — checked state reads as a leading ✓ (see the kit Menu).
20585
+ checkmarks: true,
20454
20586
  onSelect: (itemId) => {
20455
20587
  if (itemId !== "values" || !this.rowMenuId) return;
20456
20588
  const target = this.rows.get(this.rowMenuId);
@@ -20786,6 +20918,8 @@ var InputsUI = class {
20786
20918
  syncRowActions(row) {
20787
20919
  const open2 = row.highlighted;
20788
20920
  row.controlsEl.style.display = open2 || row.hidden ? "inline-flex" : "none";
20921
+ if (open2) row.el.appendChild(row.statusEl);
20922
+ else row.el.insertBefore(row.statusEl, row.valuesEl);
20789
20923
  for (const child of Array.from(row.controlsEl.children)) {
20790
20924
  if (!(child instanceof HTMLElement) || child === row.eyeEl) continue;
20791
20925
  if (child === row.extrasEl) {
@@ -21029,6 +21163,148 @@ var PaneControls = class {
21029
21163
  }
21030
21164
  };
21031
21165
 
21166
+ // src/renderers/native/chrome/axisLayout.ts
21167
+ var AXIS_MASTER_W = 64;
21168
+ var AXIS_MERGED_W = 56;
21169
+ var PANE_SEPARATOR_PX = 3;
21170
+ function axisColumnX(dataW, column) {
21171
+ return column === 0 ? dataW : dataW + AXIS_MASTER_W + (column - 1) * AXIS_MERGED_W;
21172
+ }
21173
+
21174
+ // src/renderers/native/chrome/AxisScaleButtons.ts
21175
+ var STYLE_ID22 = "vela-axis-scale-buttons";
21176
+ var BTN_PX = 18;
21177
+ var BTN_GAP = 4;
21178
+ var AXIS_BORDER = 1;
21179
+ var CLUSTER_W = AXIS_MASTER_W - AXIS_BORDER;
21180
+ var PAD_TOP = 8;
21181
+ var PAD_BOTTOM = 6;
21182
+ var CLUSTER_H = BTN_PX + PAD_TOP + PAD_BOTTOM;
21183
+ var MIN_PANE_H = 48;
21184
+ function ensureStyles4() {
21185
+ if (typeof document === "undefined" || document.getElementById(STYLE_ID22)) return;
21186
+ const st = document.createElement("style");
21187
+ st.id = STYLE_ID22;
21188
+ st.textContent = `
21189
+ .vela-axis-btn{display:inline-flex;align-items:center;justify-content:center;padding:0;border:none;border-radius:var(--vela-radius-sm);background:transparent;cursor:pointer;font:600 10px/1 -apple-system,Segoe UI,sans-serif;color:var(--vela-fg-muted);}
21190
+ .vela-axis-btn:hover{background:var(--vela-active);color:var(--vela-fg-bright);}
21191
+ .vela-axis-on,.vela-axis-on:hover{background:var(--vela-selected-bg);color:var(--vela-selected-fg);}
21192
+ `;
21193
+ document.head.appendChild(st);
21194
+ }
21195
+ var AxisScaleButtons = class {
21196
+ constructor(plot, theme, deps) {
21197
+ this.plot = plot;
21198
+ this.theme = theme;
21199
+ this.deps = deps;
21200
+ this.tipDisposers = [];
21201
+ this.hoverPaneId = null;
21202
+ /** Reveal over the master scale column of the pane under the cursor (mouse only —
21203
+ * touch has no hover, and a drag-rescale on the axis must not sprout buttons). */
21204
+ this.onPlotMove = (e) => {
21205
+ if (e.pointerType !== "mouse") return;
21206
+ const rect = this.plot.getBoundingClientRect();
21207
+ const x = e.clientX - rect.left;
21208
+ const gutterLeft = rect.width - this.deps.rightAxis();
21209
+ let hit = null;
21210
+ if (x >= gutterLeft && x <= gutterLeft + AXIS_MASTER_W) {
21211
+ const y = e.clientY - rect.top;
21212
+ for (const p of this.deps.panes()) {
21213
+ if (p.collapsed || p.height < MIN_PANE_H) continue;
21214
+ if (y >= p.top && y <= p.top + p.height) {
21215
+ hit = p.id;
21216
+ break;
21217
+ }
21218
+ }
21219
+ }
21220
+ this.setHoverPane(hit);
21221
+ if (hit) this.sync();
21222
+ };
21223
+ this.onPlotLeave = () => this.setHoverPane(null);
21224
+ ensureStyles4();
21225
+ this.cluster = document.createElement("div");
21226
+ Object.assign(this.cluster.style, {
21227
+ position: "absolute",
21228
+ display: "none",
21229
+ justifyContent: "center",
21230
+ alignItems: "center",
21231
+ gap: `${BTN_GAP}px`,
21232
+ padding: `${PAD_TOP}px 0 ${PAD_BOTTOM}px`,
21233
+ width: `${CLUSTER_W}px`,
21234
+ boxSizing: "border-box",
21235
+ zIndex: "6",
21236
+ pointerEvents: "auto"
21237
+ });
21238
+ this.applyTheme();
21239
+ this.autoBtn = this.button("A", "Auto (fits data to screen)", () => this.deps.onToggleAuto(this.hoverPaneId ?? ""));
21240
+ this.logBtn = this.button("L", "Logarithmic scale", () => this.deps.onToggleLog(this.hoverPaneId ?? ""));
21241
+ this.cluster.append(this.autoBtn, this.logBtn);
21242
+ this.plot.appendChild(this.cluster);
21243
+ this.plot.addEventListener("pointermove", this.onPlotMove);
21244
+ this.plot.addEventListener("pointerleave", this.onPlotLeave);
21245
+ }
21246
+ button(label, title, onClick) {
21247
+ const b = document.createElement("button");
21248
+ b.type = "button";
21249
+ b.setAttribute("aria-label", title);
21250
+ b.textContent = label;
21251
+ b.className = "vela-axis-btn";
21252
+ Object.assign(b.style, { width: `${BTN_PX}px`, height: `${BTN_PX}px` });
21253
+ this.tipDisposers.push(attachChromeTooltip(b, {
21254
+ host: this.plot,
21255
+ theme: () => this.theme,
21256
+ text: () => title,
21257
+ placement: "above"
21258
+ }));
21259
+ b.addEventListener("click", (e) => {
21260
+ e.stopPropagation();
21261
+ onClick();
21262
+ this.sync();
21263
+ });
21264
+ return b;
21265
+ }
21266
+ /** Re-tint the chip when the plot background/border change (theme swap, config edit). */
21267
+ setTheme(theme) {
21268
+ this.theme = theme;
21269
+ this.applyTheme();
21270
+ }
21271
+ /** The strip is SOLID chart background: the buttons cover axis labels, never blend with them. */
21272
+ applyTheme() {
21273
+ this.cluster.style.background = this.theme.background;
21274
+ }
21275
+ setHoverPane(paneId) {
21276
+ if (this.hoverPaneId === paneId) return;
21277
+ this.hoverPaneId = paneId;
21278
+ this.reposition();
21279
+ }
21280
+ /** Re-place (or hide) the cluster after a hover or pane-layout change. */
21281
+ reposition() {
21282
+ const pane = this.hoverPaneId ? this.deps.panes().find((p) => p.id === this.hoverPaneId) : void 0;
21283
+ if (!pane || pane.collapsed || pane.height < MIN_PANE_H) {
21284
+ this.cluster.style.display = "none";
21285
+ return;
21286
+ }
21287
+ this.cluster.style.right = `${this.deps.rightAxis() - AXIS_MASTER_W}px`;
21288
+ this.cluster.style.top = `${pane.top + pane.height - CLUSTER_H}px`;
21289
+ this.cluster.style.display = "flex";
21290
+ this.sync();
21291
+ }
21292
+ /** Reflect the hovered pane's current auto/log state on the buttons. */
21293
+ sync() {
21294
+ const pane = this.deps.panes().find((p) => p.id === this.hoverPaneId);
21295
+ if (!pane) return;
21296
+ this.autoBtn.classList.toggle("vela-axis-on", pane.auto);
21297
+ this.logBtn.classList.toggle("vela-axis-on", pane.log);
21298
+ }
21299
+ destroy() {
21300
+ this.plot.removeEventListener("pointermove", this.onPlotMove);
21301
+ this.plot.removeEventListener("pointerleave", this.onPlotLeave);
21302
+ for (const dispose of this.tipDisposers) dispose();
21303
+ this.tipDisposers.length = 0;
21304
+ this.cluster.remove();
21305
+ }
21306
+ };
21307
+
21032
21308
  // src/renderers/shared/TableOverlay.ts
21033
21309
  var SIZE_PX3 = {
21034
21310
  auto: 13,
@@ -21613,6 +21889,8 @@ var SceneGraph = class {
21613
21889
  this.tradeMarkers = defaultTradeMarkersState();
21614
21890
  /** Renderer-owned shaded time bands (session highlighting), behind grid + data. */
21615
21891
  this.highlights = [];
21892
+ /** Pre/post-market bands pushed by the host (`sessionZones` feature); null ⇒ no sessions. */
21893
+ this.sessionZones = null;
21616
21894
  /** Draw-order key of the price candles, relative to indicator series z (see `seriesZ`).
21617
21895
  * Indicators with z below this draw BEHIND the candles; at/above draw in front.
21618
21896
  * Default 0 with indicators mounting at z < 0 ⇒ the price reads on top of every overlay,
@@ -21647,6 +21925,15 @@ var SceneGraph = class {
21647
21925
  if (!this.orderedCache) this.orderedCache = [...this.panes.values()].sort((a, b) => a.order - b.order);
21648
21926
  return this.orderedCache;
21649
21927
  }
21928
+ /** The session zones resolved into colored bands (pre/post-market washes from the
21929
+ * config's session colors) — consumed by the same painting path as {@link highlights}. */
21930
+ sessionHighlightBands() {
21931
+ if (!this.sessionZones) return [];
21932
+ const out = [];
21933
+ for (const [from, to] of this.sessionZones.pre) out.push({ from, to, color: this.style.sessions.premarketColor });
21934
+ for (const [from, to] of this.sessionZones.post) out.push({ from, to, color: this.style.sessions.postmarketColor });
21935
+ return out;
21936
+ }
21650
21937
  indicatorsForPane(paneId) {
21651
21938
  const out = [];
21652
21939
  for (const model of this.indicators.values()) if (model.paneId === paneId) out.push(model);
@@ -21730,6 +22017,12 @@ var SceneGraph = class {
21730
22017
  assignIndicatorZ(id) {
21731
22018
  if (!this.seriesZ.has(id)) this.seriesZ.set(id, this.bottomZ() - 1);
21732
22019
  }
22020
+ /** Mount-time default for a LAYER-BACKED native (it paints on a canvas stacked above the
22021
+ * data canvas by default): top of the stack, so the recorded order tells the truth from
22022
+ * the first frame. Keeps an existing key, so a restored stack survives the remount. */
22023
+ assignIndicatorZTop(id) {
22024
+ if (!this.seriesZ.has(id)) this.seriesZ.set(id, this.topZ() + 1);
22025
+ }
21733
22026
  forgetIndicatorZ(id) {
21734
22027
  this.seriesZ.delete(id);
21735
22028
  }
@@ -22577,10 +22870,12 @@ var WebGL2Backend = class {
22577
22870
  }
22578
22871
  }
22579
22872
  }
22580
- /** Renderer-owned session highlight bands, clipped per-pane (scissor reconstructs full height). */
22873
+ /** Renderer-owned session highlight bands, clipped per-pane (scissor reconstructs full height).
22874
+ * Session-zone washes (pre/post-market) paint first, host highlights on top. */
22581
22875
  emitHighlights(b, scene, pane, coords) {
22582
- if (scene.highlights.length === 0) return;
22583
- for (const band of scene.highlights) {
22876
+ const bands = [...scene.sessionHighlightBands(), ...scene.highlights];
22877
+ if (bands.length === 0) return;
22878
+ for (const band of bands) {
22584
22879
  const x1 = coords.timeToX(band.from);
22585
22880
  const x2 = coords.timeToX(band.to);
22586
22881
  if (x2 < 0 || x1 > coords.width || x2 <= x1) continue;
@@ -24554,10 +24849,12 @@ var Canvas2dBackend = class {
24554
24849
  ctx.fillStyle = bg.color;
24555
24850
  ctx.fillRect(x1, pane.bounds.top, x2 - x1, pane.bounds.height);
24556
24851
  }
24557
- /** Renderer-owned session highlight bands: full-height (all panes), behind grid + data. */
24852
+ /** Renderer-owned session highlight bands: full-height (all panes), behind grid + data.
24853
+ * Session-zone washes (pre/post-market) paint first, host highlights on top. */
24558
24854
  drawHighlights(ctx, scene, coords) {
24559
- if (scene.highlights.length === 0) return;
24560
- for (const band of scene.highlights) {
24855
+ const bands = [...scene.sessionHighlightBands(), ...scene.highlights];
24856
+ if (bands.length === 0) return;
24857
+ for (const band of bands) {
24561
24858
  const x1 = coords.timeToX(band.from);
24562
24859
  const x2 = coords.timeToX(band.to);
24563
24860
  if (x2 < 0 || x1 > coords.width || x2 <= x1) continue;
@@ -25331,14 +25628,6 @@ var DrawingSceneRenderer = class {
25331
25628
  }
25332
25629
  };
25333
25630
 
25334
- // src/renderers/native/chrome/axisLayout.ts
25335
- var AXIS_MASTER_W = 64;
25336
- var AXIS_MERGED_W = 56;
25337
- var PANE_SEPARATOR_PX = 3;
25338
- function axisColumnX(dataW, column) {
25339
- return column === 0 ? dataW : dataW + AXIS_MASTER_W + (column - 1) * AXIS_MERGED_W;
25340
- }
25341
-
25342
25631
  // src/renderers/native/chrome/ChromeRenderer.ts
25343
25632
  var ChromeRenderer = class {
25344
25633
  constructor() {
@@ -26144,6 +26433,7 @@ var SettingsDialog = class {
26144
26433
  for (const hr of hs.rows) {
26145
26434
  if (hr.kind === "heading") body.append(this.sectionTitle(hr.label));
26146
26435
  else if (hr.kind === "toggle") body.append(this.boolRow(hr.label, hr.get(), (v) => hr.set(v)));
26436
+ else if (hr.kind === "color") body.append(this.colorRow(hr.label, hr.get(), (v) => hr.set(v)));
26147
26437
  else body.append(this.selectRowLabeled(hr.label, hr.get(), hr.options.map((o) => [o, o]), (v) => hr.set(v)));
26148
26438
  }
26149
26439
  }
@@ -28855,14 +29145,14 @@ function btn(label, primary, onClick) {
28855
29145
  // src/renderers/native/drawings/DrawingSettingsPopup.ts
28856
29146
  var BTN = 30;
28857
29147
  var ICON = 21;
28858
- var STYLE_ID22 = "vela-drawing-popup-styles";
29148
+ var STYLE_ID23 = "vela-drawing-popup-styles";
28859
29149
  var STYLE_REV = "3";
28860
- function ensureStyles4() {
29150
+ function ensureStyles5() {
28861
29151
  if (typeof document === "undefined") return;
28862
- const existing = document.getElementById(STYLE_ID22);
29152
+ const existing = document.getElementById(STYLE_ID23);
28863
29153
  if (existing?.dataset.rev === STYLE_REV) return;
28864
29154
  const s = existing ?? document.createElement("style");
28865
- s.id = STYLE_ID22;
29155
+ s.id = STYLE_ID23;
28866
29156
  s.dataset.rev = STYLE_REV;
28867
29157
  s.textContent = `
28868
29158
  .vela-dpop-btn{background:transparent;color:var(--vela-fg-muted);transition:background var(--vela-dur-fast) ease,color var(--vela-dur-fast) ease;}
@@ -28919,7 +29209,7 @@ var DrawingSettingsPopup = class {
28919
29209
  * `onClose` fires on an outside-click dismissal (not a programmatic close). */
28920
29210
  open(drawing, anchor, actions, onClose) {
28921
29211
  this.close();
28922
- ensureStyles4();
29212
+ ensureStyles5();
28923
29213
  this.onClose = onClose ?? null;
28924
29214
  const t = this.theme;
28925
29215
  const schema = drawing.schema();
@@ -30771,6 +31061,19 @@ function rendererLayers() {
30771
31061
  return [...registry4.values()];
30772
31062
  }
30773
31063
 
31064
+ // src/renderers/native/core/layerStacking.ts
31065
+ function stackLayers(entries, candleZ) {
31066
+ const keyed = entries.map((e) => ({
31067
+ id: e.id,
31068
+ key: e.ownerZ ?? (e.placement === "below-data" ? -Infinity : candleZ)
31069
+ }));
31070
+ keyed.sort((a, b) => a.key - b.key);
31071
+ const below = [];
31072
+ const above = [];
31073
+ for (const k of keyed) (k.key < candleZ ? below : above).push(k.id);
31074
+ return { below, above };
31075
+ }
31076
+
30774
31077
  // src/renderers/shared/dom-raster.ts
30775
31078
  function hasInk(color) {
30776
31079
  if (!color || color === "transparent") return false;
@@ -31143,6 +31446,8 @@ var NativeRenderer = class {
31143
31446
  this.volumeRenderer = new VolumeRenderer();
31144
31447
  /** SDK renderer layers instantiated at mount ({@link registerRendererLayer}). */
31145
31448
  this.extLayers = [];
31449
+ /** Last applied layer-canvas order (ids below + above the data canvas) — re-slotted only on change. */
31450
+ this.layerOrderSig = "";
31146
31451
  // The attribution mark (see chrome/AttributionMark + the NOTICE file): default-on;
31147
31452
  // disabling requires an equivalent visible attribution elsewhere in the host UI.
31148
31453
  this.attributionEl = null;
@@ -31257,6 +31562,8 @@ var NativeRenderer = class {
31257
31562
  // total right-gutter width (master column + merged-scale columns)
31258
31563
  this.paneControls = null;
31259
31564
  // per-pane hover button cluster (top-right)
31565
+ this.axisScaleButtons = null;
31566
+ // A/L hover buttons at the bottom of each pane's price scale
31260
31567
  this.paneActionCbs = /* @__PURE__ */ new Set();
31261
31568
  // ── data window (crosshair OHLC + per-series readout, pulled by host panels) ──
31262
31569
  this.hoverLogical = null;
@@ -31300,7 +31607,7 @@ var NativeRenderer = class {
31300
31607
  this.priceStyleCbs = /* @__PURE__ */ new Set();
31301
31608
  this.tableOverlays = /* @__PURE__ */ new Map();
31302
31609
  this.name = "native";
31303
- this.features = ["logScale", "currentPriceLine", "priceLabel", "countdown", "upColor", "downColor", "glow", "animZoom", "animPan", "intro", "zoomAnchor", "axisDrag", "paneResize", "candleZOrder", "candleVisible", "seriesOrder", "highlights", "gridlines", "axisLabels", "scaleMode", "invertScale", "paneScales", "autoScale", "timezone", "keyboard", "historyChords", "priceStyle", "priceBaseline", "baselinePrice", "settings", "attribution", "dialogHost", "tradeMarkers", "indicatorTitles", "indicatorValues"];
31610
+ this.features = ["logScale", "currentPriceLine", "priceLabel", "countdown", "upColor", "downColor", "glow", "animZoom", "animPan", "intro", "zoomAnchor", "axisDrag", "paneResize", "candleZOrder", "candleVisible", "seriesOrder", "highlights", "sessionZones", "gridlines", "axisLabels", "scaleMode", "invertScale", "paneScales", "autoScale", "timezone", "keyboard", "historyChords", "priceStyle", "priceBaseline", "baselinePrice", "settings", "attribution", "dialogHost", "tradeMarkers", "indicatorTitles", "indicatorValues"];
31304
31611
  /** Track cursor proximity to the scroll button on the plot (bubbles from the button too,
31305
31612
  * so moving onto the button doesn't count as leaving). */
31306
31613
  this.onScrollProximityMove = (e) => {
@@ -31416,6 +31723,9 @@ var NativeRenderer = class {
31416
31723
  case "highlights":
31417
31724
  this.scene.highlights = sanitizeHighlights(value);
31418
31725
  break;
31726
+ case "sessionZones":
31727
+ this.scene.sessionZones = sanitizeSessionZones(value);
31728
+ break;
31419
31729
  case "gridlines":
31420
31730
  this.scene.showGrid = Boolean(value);
31421
31731
  break;
@@ -31532,6 +31842,8 @@ var NativeRenderer = class {
31532
31842
  return this.scene.indicatorZOrder();
31533
31843
  case "highlights":
31534
31844
  return this.scene.highlights;
31845
+ case "sessionZones":
31846
+ return this.scene.sessionZones;
31535
31847
  case "gridlines":
31536
31848
  return this.scene.showGrid;
31537
31849
  case "axisLabels":
@@ -31741,7 +32053,8 @@ var NativeRenderer = class {
31741
32053
  baselineLevel: s.baseline.baselineLevel
31742
32054
  };
31743
32055
  })(),
31744
- series: { style: this.scene.priceStyle, baseline: this.scene.baselineValue, spacing: this.coords.spacingScale }
32056
+ series: { style: this.scene.priceStyle, baseline: this.scene.baselineValue, spacing: this.coords.spacingScale },
32057
+ sessions: { premarketColor: s.sessions.premarketColor, postmarketColor: s.sessions.postmarketColor }
31745
32058
  };
31746
32059
  }
31747
32060
  /**
@@ -31823,6 +32136,7 @@ var NativeRenderer = class {
31823
32136
  width: next.baseline.width,
31824
32137
  baselineLevel: next.baseline.baselineLevel
31825
32138
  };
32139
+ s.sessions = { premarketColor: next.sessions.premarketColor, postmarketColor: next.sessions.postmarketColor };
31826
32140
  this.setPriceStyle(next.series.style);
31827
32141
  this.scene.candleOverride = candleOverrideFor(this.scene.priceStyle, s.chartTypes);
31828
32142
  this.scene.baselineValue = next.series.baseline;
@@ -31836,6 +32150,7 @@ var NativeRenderer = class {
31836
32150
  }
31837
32151
  this.inputsUI?.setTheme(this.theme);
31838
32152
  this.paneControls?.setTheme(this.theme);
32153
+ this.axisScaleButtons?.setTheme(this.theme);
31839
32154
  this.userDrawings?.setTheme(this.chromeTheme());
31840
32155
  this.refreshScrollButtonTheme();
31841
32156
  this.refreshAttributionColor();
@@ -32063,9 +32378,7 @@ var NativeRenderer = class {
32063
32378
  if (el.getAttribute("data-vela-screenshot") === "under") rasterizeOverlay(ctx, el, frame);
32064
32379
  }
32065
32380
  }
32066
- const below = this.extLayers.filter((l) => l.def.placement === "below-data").map((l) => l.canvas);
32067
- const above = this.extLayers.filter((l) => l.def.placement !== "below-data").map((l) => l.canvas);
32068
- for (const canvas of [...below, this.dataCanvas, this.volumeCanvas, this.vpvrCanvas, ...above, this.chromeCanvas, this.drawingsCanvas]) {
32381
+ for (const canvas of [...this.canvasPile(), this.chromeCanvas, this.drawingsCanvas]) {
32069
32382
  if (canvas && canvas.width > 0 && canvas.height > 0) ctx.drawImage(canvas, 0, 0);
32070
32383
  }
32071
32384
  if (frame) {
@@ -32174,7 +32487,7 @@ var NativeRenderer = class {
32174
32487
  this.surfaceBackground = theme.background;
32175
32488
  this.surfaceTextColor = theme.textColor;
32176
32489
  this.wrapper = document.createElement("div");
32177
- Object.assign(this.wrapper.style, { position: "relative", width: "100%", height: "100%", overflow: "hidden", cursor: "crosshair" });
32490
+ Object.assign(this.wrapper.style, { position: "relative", width: "100%", height: "100%", overflow: "hidden", cursor: "crosshair", userSelect: "none", webkitUserSelect: "none" });
32178
32491
  applyChromeTokens(this.wrapper, this.chromeTheme());
32179
32492
  this.volumeCanvas = document.createElement("canvas");
32180
32493
  Object.assign(this.volumeCanvas.style, { position: "absolute", inset: "0", width: "100%", height: "100%", pointerEvents: "none" });
@@ -32199,6 +32512,7 @@ var NativeRenderer = class {
32199
32512
  const below = this.extLayers.filter((l) => l.def.placement === "below-data").map((l) => l.canvas);
32200
32513
  const above = this.extLayers.filter((l) => l.def.placement !== "below-data").map((l) => l.canvas);
32201
32514
  this.plot.append(...below, this.dataCanvas, this.volumeCanvas, this.vpvrCanvas, ...above, this.chromeCanvas, this.drawingsCanvas, this.cursorCanvas, this.overlayRoot);
32515
+ this.layerOrderSig = "";
32202
32516
  this.wrapper.appendChild(this.plot);
32203
32517
  this.factoryConfig = this.getConfig();
32204
32518
  this.attributionEl = this.buildAttributionEl();
@@ -32343,6 +32657,15 @@ var NativeRenderer = class {
32343
32657
  this.emitPaneAction({ type: "maximize", paneId, maximized });
32344
32658
  }
32345
32659
  });
32660
+ this.axisScaleButtons = new AxisScaleButtons(this.plot, theme, {
32661
+ panes: () => this.axisScaleViews(),
32662
+ rightAxis: () => this.rightAxisW,
32663
+ onToggleAuto: (paneId) => this.togglePaneAuto(paneId),
32664
+ onToggleLog: (paneId) => {
32665
+ const pane = this.scene.panes.get(paneId);
32666
+ if (pane) this.setPaneLog(paneId, !paneLogScale(this.scene, pane));
32667
+ }
32668
+ });
32346
32669
  this.resizeObserver = new ResizeObserver(() => this.resize());
32347
32670
  this.resizeObserver.observe(this.wrapper);
32348
32671
  this.watchDpr();
@@ -32419,6 +32742,7 @@ var NativeRenderer = class {
32419
32742
  this.applyBackground();
32420
32743
  this.inputsUI.setTheme(theme);
32421
32744
  this.paneControls?.setTheme(this.theme);
32745
+ this.axisScaleButtons?.setTheme(this.theme);
32422
32746
  this.settingsDialog?.refreshConfig(this.getConfig());
32423
32747
  this.syncThemeControl();
32424
32748
  this.settingsDialog?.setTheme(this.theme);
@@ -32467,6 +32791,7 @@ var NativeRenderer = class {
32467
32791
  this.liveRegion = null;
32468
32792
  this.inputsUI?.destroy();
32469
32793
  this.paneControls?.destroy();
32794
+ this.axisScaleButtons?.destroy();
32470
32795
  for (const overlay of this.tableOverlays.values()) overlay.destroy();
32471
32796
  this.tableOverlays.clear();
32472
32797
  this.resizeObserver?.disconnect();
@@ -32716,7 +33041,8 @@ var NativeRenderer = class {
32716
33041
  mountIndicator(model) {
32717
33042
  this.scene.indicators.set(model.id, model);
32718
33043
  this.refreshAnchorOffset(model);
32719
- this.scene.assignIndicatorZ(model.id);
33044
+ if (model.native && this.extLayers.some((l) => l.def.id === model.native.type)) this.scene.assignIndicatorZTop(model.id);
33045
+ else this.scene.assignIndicatorZ(model.id);
32720
33046
  this.inputsUI.upsert(model.id, model.shorttitle ?? model.title, model.inputs, model.inputValues, model.paneId, {
32721
33047
  native: !!model.native,
32722
33048
  ...model.shorttitle ? { settingsTitle: model.title } : {}
@@ -33535,10 +33861,17 @@ var NativeRenderer = class {
33535
33861
  const nowMs = typeof performance !== "undefined" ? performance.now() : Date.now();
33536
33862
  for (const l of this.extLayers) {
33537
33863
  if (!l.def.repaintOnCursor) continue;
33538
- l.instance.render(this.extLayerArgs(l.def.id, pane.scale, pane.bounds, nowMs));
33864
+ const lp = this.layerPane(l.def.id) ?? pane;
33865
+ if (lp.collapsed) continue;
33866
+ l.instance.render(this.extLayerArgs(l.def.id, lp.scale, lp.bounds, nowMs));
33539
33867
  if (this.animZoom && l.instance.animating?.()) this.animator.start();
33540
33868
  }
33541
33869
  }
33870
+ /** Blank one SDK layer canvas (a collapsed host pane suppresses the layer's painting). */
33871
+ clearLayerCanvas(canvas) {
33872
+ if (canvas.width === 0 || canvas.height === 0) return;
33873
+ canvas.getContext("2d")?.clearRect(0, 0, canvas.width, canvas.height);
33874
+ }
33542
33875
  /** One frame's args for an SDK renderer layer (shared by the data + cursor paint paths). */
33543
33876
  extLayerArgs(id, scale, bounds, nowMs) {
33544
33877
  return {
@@ -33557,6 +33890,7 @@ var NativeRenderer = class {
33557
33890
  }
33558
33891
  /** Paint the below-data (L-1) + geometry (L0) + chrome (L1) layers from the current scene/coords. */
33559
33892
  paintData() {
33893
+ this.syncLayerCanvasOrder();
33560
33894
  this.stampScaleInvert();
33561
33895
  this.backend.modelAlpha = this.modelAlpha;
33562
33896
  this.backend.candleBodyAlpha = this.candleBodyAlpha;
@@ -33587,9 +33921,14 @@ var NativeRenderer = class {
33587
33921
  const nowMs = typeof performance !== "undefined" ? performance.now() : Date.now();
33588
33922
  let folded = null;
33589
33923
  for (const l of this.extLayers) {
33590
- const args = this.extLayerArgs(l.def.id, pane.scale, pane.bounds, nowMs);
33924
+ const lp = this.layerPane(l.def.id) ?? pane;
33925
+ if (lp.collapsed) {
33926
+ this.clearLayerCanvas(l.canvas);
33927
+ continue;
33928
+ }
33929
+ const args = this.extLayerArgs(l.def.id, lp.scale, lp.bounds, nowMs);
33591
33930
  l.instance.render(args);
33592
- folded = foldBaseModulation(folded, l.instance.modulateBase?.(args) ?? null);
33931
+ if (lp === pane) folded = foldBaseModulation(folded, l.instance.modulateBase?.(args) ?? null);
33593
33932
  if (this.animZoom && l.instance.animating?.()) this.animator.start();
33594
33933
  }
33595
33934
  if (folded) {
@@ -33751,6 +34090,9 @@ var NativeRenderer = class {
33751
34090
  pane.scaleTarget = { min: 0, max: maxVol / VOLUME_PANE_FILL_FRAC };
33752
34091
  pane.axisFormat = "volume";
33753
34092
  }
34093
+ } else if (this.layerNativesOwnPane(pane, masterModels)) {
34094
+ pane.scaleTarget = computePaneScale([], this.bars, true, i0, i1, dr, paneLogScale(this.scene, pane), (id) => this.scene.offsetOf(id));
34095
+ pane.percentBaseline = this.bars[i0]?.close ?? 0;
33754
34096
  }
33755
34097
  if (pane === pricePane && this.scene.tradeMarkers.visible && pane.bounds.height > 0) {
33756
34098
  const th = this.tradesScaleHints(vr);
@@ -33837,6 +34179,59 @@ var NativeRenderer = class {
33837
34179
  }
33838
34180
  return null;
33839
34181
  }
34182
+ /** The mounted native indicator that OWNS an SDK layer — the one whose type equals the
34183
+ * layer id (the id doubles as the data channel, so the pairing is the SDK's own
34184
+ * contract). Null for chart-type channels and while the owner is hidden (a hidden
34185
+ * indicator leaves the scene; its cleared data channel paints nothing anyway). */
34186
+ layerOwner(layerId) {
34187
+ for (const m of this.scene.indicators.values()) {
34188
+ if (m.native?.type === layerId) return m;
34189
+ }
34190
+ return null;
34191
+ }
34192
+ /** The pane an SDK layer paints on: its owner's pane, else the price pane. */
34193
+ layerPane(layerId) {
34194
+ const owner = this.layerOwner(layerId);
34195
+ const paneId = owner ? owner.paneId ?? PRICE_PANE_ID2 : PRICE_PANE_ID2;
34196
+ return this.scene.panes.get(paneId) ?? null;
34197
+ }
34198
+ /** The SDK layer canvases split around the data canvas, each side back-to-front:
34199
+ * owned layers by their owner's z key against the candles' (an indicator restacked
34200
+ * below the candles takes its layer canvas along), unowned by declared placement. */
34201
+ orderedLayerCanvases() {
34202
+ const byId = new Map(this.extLayers.map((l) => [l.def.id, l.canvas]));
34203
+ const { below, above } = stackLayers(
34204
+ this.extLayers.map((l) => {
34205
+ const owner = this.layerOwner(l.def.id);
34206
+ return {
34207
+ id: l.def.id,
34208
+ placement: l.def.placement === "below-data" ? "below-data" : "above-data",
34209
+ ownerZ: owner ? this.scene.zOf(owner.id) : null
34210
+ };
34211
+ }),
34212
+ this.scene.candleZ
34213
+ );
34214
+ return { below: below.map((id) => byId.get(id)), above: above.map((id) => byId.get(id)) };
34215
+ }
34216
+ /** The full canvas pile in paint order (layers + data/volume/vpvr) — what the DOM
34217
+ * stacking and the screenshot compositor must both follow. */
34218
+ canvasPile() {
34219
+ const { below, above } = this.orderedLayerCanvases();
34220
+ return [...below, this.dataCanvas, this.volumeCanvas, this.vpvrCanvas, ...above];
34221
+ }
34222
+ /** Re-slot the SDK layer canvases in the plot when the computed order changed (a z
34223
+ * write, a restored config, an indicator mount/remove/restack). Runs at the top of
34224
+ * every data frame; a no-op when the signature is unchanged. Re-inserting an
34225
+ * absolutely-positioned, pointer-transparent canvas repaints nothing by itself. */
34226
+ syncLayerCanvasOrder() {
34227
+ if (this.extLayers.length === 0 || !this.plot) return;
34228
+ const { below, above } = this.orderedLayerCanvases();
34229
+ const sig = [...below.map((c) => this.extLayers.find((l) => l.canvas === c).def.id), "|", ...above.map((c) => this.extLayers.find((l) => l.canvas === c).def.id)].join(",");
34230
+ if (sig === this.layerOrderSig) return;
34231
+ this.layerOrderSig = sig;
34232
+ for (const c of below) this.plot.insertBefore(c, this.dataCanvas);
34233
+ for (const c of above) this.plot.insertBefore(c, this.chromeCanvas);
34234
+ }
33840
34235
  /** True when an active volume layer is this study pane's ONLY content — so its scale should
33841
34236
  * come from volume, not the empty {0,1} placeholder. (In the price pane, or alongside a real
33842
34237
  * series, volume stays a bottom overlay and the master scale wins.) */
@@ -33846,6 +34241,15 @@ var NativeRenderer = class {
33846
34241
  if (this.nativeLayerPane("volume") !== pane) return false;
33847
34242
  return masterModels.every((m) => m.native?.type === "volume");
33848
34243
  }
34244
+ /** True when this study pane's master content is only SDK-layer natives (series-less
34245
+ * models whose type names a mounted layer) — its scale then follows the visible bars
34246
+ * (see the call site). Any real master series takes over the scale as usual. */
34247
+ layerNativesOwnPane(pane, masterModels) {
34248
+ if (pane.kind === "price" || masterModels.length === 0) return false;
34249
+ return masterModels.every(
34250
+ (m) => m.series.length === 0 && !!m.native && this.extLayers.some((l) => l.def.id === m.native.type)
34251
+ );
34252
+ }
33849
34253
  /** Per-pane scale state for a host UI (e.g. a price-axis context menu): the pane's pixel
33850
34254
  * band (`top`/`height`, so a click y maps to a pane) plus its current axis `mode`/`log`.
33851
34255
  * Top-to-bottom order. Every pane is independent — the price pane from the scene setting,
@@ -33861,6 +34265,25 @@ var NativeRenderer = class {
33861
34265
  invert: paneInvert(this.scene, p)
33862
34266
  }));
33863
34267
  }
34268
+ /** Thin projection of the panes for the axis A/L hover buttons (top-to-bottom order). */
34269
+ axisScaleViews() {
34270
+ return this.scene.orderedPanes().map((p) => ({
34271
+ id: p.id,
34272
+ top: p.bounds.top,
34273
+ height: p.bounds.height,
34274
+ collapsed: p.collapsed,
34275
+ auto: p.manualScale == null,
34276
+ log: paneLogScale(this.scene, p)
34277
+ }));
34278
+ }
34279
+ /** Toggle one pane's autoscale (the A axis button): off freezes the current window into
34280
+ * manual mode, on drops it — the per-pane twin of the chart-level `autoScale` feature. */
34281
+ togglePaneAuto(paneId) {
34282
+ const pane = this.scene.panes.get(paneId);
34283
+ if (!pane) return;
34284
+ pane.manualScale = pane.manualScale == null ? { ...pane.scale } : null;
34285
+ this.scheduler?.invalidate(4 /* Full */);
34286
+ }
33864
34287
  /** Set one pane's axis mode. The price pane routes to the scene-level setting (persisted +
33865
34288
  * keyboard shortcuts); a study pane keeps its own, so panes never affect each other. */
33866
34289
  setPaneScaleMode(paneId, mode) {
@@ -33872,19 +34295,18 @@ var NativeRenderer = class {
33872
34295
  }
33873
34296
  this.scheduler?.invalidate(4 /* Full */);
33874
34297
  }
33875
- /** Set one pane's logarithmic flag. Clears that pane's frozen (manual) window so the new
33876
- * space re-fits, and routes the price pane to the scene-level flag. */
34298
+ /** Set one pane's logarithmic flag; the price pane routes to the scene-level flag.
34299
+ * Log and auto are INDEPENDENT: a frozen (manual) window is kept — its price bounds
34300
+ * stay put and only its `log` tag flips, so the same range re-renders in the new
34301
+ * space instead of snapping back to autoscale. */
33877
34302
  setPaneLog(paneId, log) {
33878
- if (paneId === PRICE_PANE_ID2) {
33879
- this.scene.logScale = log;
33880
- const price = this.scene.orderedPanes().find((p) => p.kind === "price");
33881
- if (price) price.manualScale = null;
33882
- } else {
33883
- const p = this.scene.panes.get(paneId);
33884
- if (!p) return;
33885
- p.logScale = log;
33886
- p.manualScale = null;
34303
+ const pane = paneId === PRICE_PANE_ID2 ? this.scene.orderedPanes().find((p) => p.kind === "price") : this.scene.panes.get(paneId);
34304
+ if (paneId === PRICE_PANE_ID2) this.scene.logScale = log;
34305
+ else {
34306
+ if (!pane) return;
34307
+ pane.logScale = log;
33887
34308
  }
34309
+ if (pane?.manualScale) pane.manualScale.log = log;
33888
34310
  this.scheduler?.invalidate(4 /* Full */);
33889
34311
  }
33890
34312
  /** Flip one pane's axis (high at the bottom). The price pane routes to the scene-level flag
@@ -33983,6 +34405,7 @@ var NativeRenderer = class {
33983
34405
  }
33984
34406
  this.inputsUI?.reposition();
33985
34407
  this.paneControls?.reposition();
34408
+ this.axisScaleButtons?.reposition();
33986
34409
  this.repositionScrollButton();
33987
34410
  this.positionAttribution();
33988
34411
  this.publishPricePaneBounds();
@@ -34001,6 +34424,7 @@ var NativeRenderer = class {
34001
34424
  }
34002
34425
  this.inputsUI?.reposition();
34003
34426
  this.paneControls?.reposition();
34427
+ this.axisScaleButtons?.reposition();
34004
34428
  this.repositionScrollButton();
34005
34429
  this.positionAttribution();
34006
34430
  this.publishPricePaneBounds();
@@ -34208,6 +34632,22 @@ function sanitizeHighlights(value) {
34208
34632
  }
34209
34633
  return out.sort((a, b) => a.from - b.from);
34210
34634
  }
34635
+ function sanitizeSessionZones(value) {
34636
+ if (value == null || typeof value !== "object") return null;
34637
+ const v = value;
34638
+ const windows = (raw) => {
34639
+ if (!Array.isArray(raw)) return [];
34640
+ const out = [];
34641
+ for (const w of raw) {
34642
+ if (!Array.isArray(w)) continue;
34643
+ const from = Number(w[0]);
34644
+ const to = Number(w[1]);
34645
+ if (Number.isFinite(from) && Number.isFinite(to) && to > from) out.push([from, to]);
34646
+ }
34647
+ return out.sort((a, b) => a[0] - b[0]);
34648
+ };
34649
+ return { pre: windows(v.pre), post: windows(v.post) };
34650
+ }
34211
34651
  function clamp012(v) {
34212
34652
  return Math.max(0, Math.min(1, v));
34213
34653
  }
@@ -34761,7 +35201,7 @@ function fmtChange(open2, close) {
34761
35201
  }
34762
35202
 
34763
35203
  // src/widget/statusline.ts
34764
- var STYLE_ID23 = "vela-widget-statusline";
35204
+ var STYLE_ID24 = "vela-widget-statusline";
34765
35205
  var CSS21 = `
34766
35206
  .vela-statusline {
34767
35207
  position: absolute;
@@ -34953,7 +35393,7 @@ var Statusline = class {
34953
35393
  this.fitMode = false;
34954
35394
  this.fitRO = null;
34955
35395
  const doc = host.ownerDocument;
34956
- injectStyles(STYLE_ID23, CSS21, doc);
35396
+ injectStyles(STYLE_ID24, CSS21, doc);
34957
35397
  host.classList.add("vela-has-statusline");
34958
35398
  this.el = doc.createElement("div");
34959
35399
  this.el.className = "vela-statusline";
@@ -35226,11 +35666,70 @@ var MarketStatusTracker = class {
35226
35666
  }
35227
35667
  };
35228
35668
 
35669
+ // src/widget/session-shading.ts
35670
+ function deriveSessionZones(regular, extended) {
35671
+ const pre = [];
35672
+ const post = [];
35673
+ for (const [extStart, extEnd] of extended) {
35674
+ const inside = regular.filter(([s, e]) => e > extStart && s < extEnd).sort((a, b) => a[0] - b[0]);
35675
+ let cursor = extStart;
35676
+ for (const [regStart, regEnd] of inside) {
35677
+ if (regStart > cursor) pre.push([cursor, Math.min(regStart, extEnd)]);
35678
+ cursor = Math.max(cursor, regEnd);
35679
+ }
35680
+ if (cursor < extEnd) post.push([cursor, extEnd]);
35681
+ }
35682
+ return { pre, post };
35683
+ }
35684
+ var FETCH_AHEAD_MS2 = 10 * 864e5;
35685
+ var MIN_LOOKBACK_MS = 3 * 864e5;
35686
+ var MAX_LOOKBACK_MS = 120 * 864e5;
35687
+ var SessionShadingTracker = class {
35688
+ constructor(onZones) {
35689
+ this.onZones = onZones;
35690
+ /** Invalidates detached async work — bumped by every track()/stop(). */
35691
+ this.epoch = 0;
35692
+ }
35693
+ /** (Re)bind to a chart's data surface + market and evaluate once. */
35694
+ track(data, symbol, opts) {
35695
+ const my = ++this.epoch;
35696
+ void this.evaluate(my, data, symbol, opts);
35697
+ }
35698
+ stop() {
35699
+ this.epoch += 1;
35700
+ }
35701
+ async evaluate(my, data, symbol, opts) {
35702
+ const resolved = data.resolve(symbol);
35703
+ const provider = resolved ? data.providerInstance(resolved.provider) : void 0;
35704
+ const si = await data.symbolInfo(symbol).catch(() => void 0);
35705
+ if (my !== this.epoch) return;
35706
+ const hasSessions = typeof si?.session === "string" && si.session !== "" && si.session !== "24x7";
35707
+ if (!provider?.getCalendar || !hasSessions || !resolved) {
35708
+ this.onZones(null);
35709
+ return;
35710
+ }
35711
+ if (opts.session !== "extended") {
35712
+ this.onZones({ pre: [], post: [] });
35713
+ return;
35714
+ }
35715
+ const now = Date.now();
35716
+ const lookback = Math.max(MIN_LOOKBACK_MS, Math.min(MAX_LOOKBACK_MS, opts.lookbackMs));
35717
+ const range = { from: now - lookback, to: now + FETCH_AHEAD_MS2 };
35718
+ const [regular, extended] = await Promise.all([
35719
+ provider.getCalendar(resolved.ticker, { ...range, session: "regular" }).catch(() => null),
35720
+ provider.getCalendar(resolved.ticker, { ...range, session: "extended" }).catch(() => null)
35721
+ ]);
35722
+ if (my !== this.epoch) return;
35723
+ if (!regular || !extended) return;
35724
+ this.onZones(deriveSessionZones(regular, extended));
35725
+ }
35726
+ };
35727
+
35229
35728
  // src/widget/watermark.ts
35230
35729
  var MAX_FONT_PX = 36;
35231
35730
  var MIN_FONT_PX = 12;
35232
35731
  var FILL = 0.9;
35233
- var STYLE_ID24 = "vela-widget-watermark";
35732
+ var STYLE_ID25 = "vela-widget-watermark";
35234
35733
  var CSS22 = `
35235
35734
  .vela-watermark {
35236
35735
  position: absolute;
@@ -35269,7 +35768,7 @@ var Watermark = class {
35269
35768
  * canvas, so the mark stays out of its way. Starts true: the FIRST `load:start`
35270
35769
  * fires during chart construction, before any subscriber can see it. */
35271
35770
  this.loading = true;
35272
- injectStyles(STYLE_ID24, CSS22, host.ownerDocument);
35771
+ injectStyles(STYLE_ID25, CSS22, host.ownerDocument);
35273
35772
  this.el = host.ownerDocument.createElement("div");
35274
35773
  this.el.className = "vela-watermark";
35275
35774
  this.el.dataset.velaScreenshot = "under";
@@ -35335,6 +35834,9 @@ var ChartContextMenu = class {
35335
35834
  host,
35336
35835
  items: [],
35337
35836
  placement: "bottom-start",
35837
+ // Pointer-anchored action menu: checked state reads as a leading ✓, not a
35838
+ // washed row (which would read as hover in a menu with no trigger button).
35839
+ checkmarks: true,
35338
35840
  onSelect: (id) => this.run(id)
35339
35841
  });
35340
35842
  host.addEventListener("contextmenu", this.onContextMenu);
@@ -35553,6 +36055,8 @@ var ChartCell = class {
35553
36055
  this.activeRangeId = null;
35554
36056
  /** Latched verdict of {@link sessionAvailable} (async metadata, sticky per symbol). */
35555
36057
  this.sessionAvailableFlag = false;
36058
+ /** Keeps the pre/post-market shading on the symbol's real calendar (see {@link SessionShadingTracker}). */
36059
+ this.sessionShading = new SessionShadingTracker((zones) => this.inner?.renderer.set("sessionZones", zones));
35556
36060
  this.manifest = [];
35557
36061
  /** A restored ledger's manifest entry NAMES, waiting for the manifest to resolve
35558
36062
  * (a pool/persisted cell can be built before the shared manifest has loaded). */
@@ -35709,67 +36213,7 @@ var ChartCell = class {
35709
36213
  });
35710
36214
  this.syncPresentNatives();
35711
36215
  this.refreshNativeCatalog();
35712
- const advanced = {
35713
- title: "Advanced",
35714
- placement: "end",
35715
- rows: [
35716
- {
35717
- kind: "select",
35718
- label: "Bars to fetch",
35719
- options: ["500", "1000", "2000", "5000", "10000", "20000"],
35720
- get: () => String(this.state.bars ?? 1e3),
35721
- set: (v) => {
35722
- this.state.bars = Number(v);
35723
- this.deps.onStateDirty();
35724
- void this.inner?.setMarket({ bars: Math.max(this.state.bars, this.rangeBars) });
35725
- }
35726
- }
35727
- ]
35728
- };
35729
- const watermarkSection = {
35730
- title: "Watermark",
35731
- placement: "symbol",
35732
- rows: [
35733
- {
35734
- kind: "toggle",
35735
- label: "Symbol watermark",
35736
- get: () => this.watermarkOn,
35737
- set: (v) => this.setWatermarkVisible(v)
35738
- }
35739
- ]
35740
- };
35741
- if (this.statusline) {
35742
- const sl = this.statusline;
35743
- this.inner.renderer.setSettingsSections([
35744
- {
35745
- title: "Status line",
35746
- rows: [
35747
- { kind: "heading", label: "Status line" },
35748
- { kind: "toggle", label: "Symbol name", get: () => sl.partVisible("name"), set: (v) => sl.setPartVisible("name", v) },
35749
- { kind: "toggle", label: "Market status", get: () => sl.partVisible("market"), set: (v) => sl.setPartVisible("market", v) },
35750
- { kind: "toggle", label: "OHLC values", get: () => sl.partVisible("ohlc"), set: (v) => sl.setPartVisible("ohlc", v) },
35751
- { kind: "toggle", label: "Bar change values", get: () => sl.partVisible("change"), set: (v) => sl.setPartVisible("change", v) },
35752
- { kind: "heading", label: "Indicators" },
35753
- {
35754
- kind: "toggle",
35755
- label: "Titles",
35756
- get: () => this.indicatorTitlesOn,
35757
- set: (v) => this.setIndicatorTitlesVisible(v)
35758
- },
35759
- {
35760
- kind: "toggle",
35761
- label: "Values",
35762
- get: () => this.indicatorValuesOn,
35763
- set: (v) => this.setIndicatorValuesVisible(v)
35764
- }
35765
- ]
35766
- },
35767
- advanced,
35768
- watermarkSection
35769
- ]);
35770
- } else {
35771
- this.inner.renderer.setSettingsSections([advanced, watermarkSection]);
35772
- }
36216
+ this.pushSettingsSections();
35773
36217
  this.offMarket = this.inner.on("market:changed", ({ symbol: symbol2, timeframe }) => {
35774
36218
  this.state.symbol = symbol2;
35775
36219
  this.state.provider = parseSymbol(symbol2).provider ?? void 0;
@@ -35814,9 +36258,130 @@ var ChartCell = class {
35814
36258
  if (available !== this.sessionAvailableFlag) {
35815
36259
  this.sessionAvailableFlag = available;
35816
36260
  this.deps.onMarketChanged(this.id);
36261
+ this.pushSettingsSections();
35817
36262
  }
36263
+ this.refreshSessionShading();
35818
36264
  });
35819
36265
  }
36266
+ /** (Re)derive the pre/post-market shading bands for this cell's market. The loaded
36267
+ * depth (bars × timeframe) bounds the calendar fetch; the tracker clamps it. */
36268
+ refreshSessionShading() {
36269
+ const chart = this.inner;
36270
+ const symbol = this.state.symbol;
36271
+ if (!chart || !symbol) return;
36272
+ const lookbackMs = Math.max(this.state.bars ?? 1e3, this.rangeBars) * timeframeMs(this.state.timeframe ?? "60");
36273
+ this.sessionShading.track(chart.data, symbol, { session: this.session, lookbackMs });
36274
+ }
36275
+ /** The session-shade colors live in the renderer CONFIG (persisted with it, edited
36276
+ * live by the dialog swatch) — the cell only proxies them into its settings rows. */
36277
+ sessionShadeColor(key) {
36278
+ const cfg = this.inner?.renderer.getConfig();
36279
+ const v = cfg?.sessions?.[key];
36280
+ return typeof v === "string" ? v : "";
36281
+ }
36282
+ setSessionShadeColor(key, color) {
36283
+ this.inner?.renderer.applyConfig({ sessions: { [key]: color } });
36284
+ this.deps.onStateDirty();
36285
+ }
36286
+ /**
36287
+ * (Re)contribute this cell's settings-dialog sections: status line parts, the
36288
+ * per-cell fetch depth, the watermark toggle, and — only while the cell's symbol
36289
+ * HAS sessions — the Trading session group (RTH/ETH switch + the pre/post-market
36290
+ * shading colors) inside the Symbol tab. Bars/watermark/titles are persistable
36291
+ * cell state; a depth-only reload is silent, so mark dirty here. Re-run whenever
36292
+ * a gate changes (the dialog reads the sections on open).
36293
+ */
36294
+ pushSettingsSections() {
36295
+ const chart = this.inner;
36296
+ if (!chart) return;
36297
+ const rth = "Regular hours (RTH)";
36298
+ const eth = "Extended hours (ETH)";
36299
+ const sessionSection = {
36300
+ title: "Trading session",
36301
+ placement: "symbol",
36302
+ rows: [
36303
+ {
36304
+ kind: "select",
36305
+ label: "Session",
36306
+ options: [rth, eth],
36307
+ get: () => this.session === "extended" ? eth : rth,
36308
+ set: (v) => this.setSession(v === eth ? "extended" : "regular")
36309
+ },
36310
+ {
36311
+ kind: "color",
36312
+ label: "Pre-market",
36313
+ get: () => this.sessionShadeColor("premarketColor"),
36314
+ set: (v) => this.setSessionShadeColor("premarketColor", v)
36315
+ },
36316
+ {
36317
+ kind: "color",
36318
+ label: "Post-market",
36319
+ get: () => this.sessionShadeColor("postmarketColor"),
36320
+ set: (v) => this.setSessionShadeColor("postmarketColor", v)
36321
+ }
36322
+ ]
36323
+ };
36324
+ const advanced = {
36325
+ title: "Advanced",
36326
+ placement: "end",
36327
+ rows: [
36328
+ {
36329
+ kind: "select",
36330
+ label: "Bars to fetch",
36331
+ options: ["500", "1000", "2000", "5000", "10000", "20000"],
36332
+ get: () => String(this.state.bars ?? 1e3),
36333
+ set: (v) => {
36334
+ this.state.bars = Number(v);
36335
+ this.deps.onStateDirty();
36336
+ void this.inner?.setMarket({ bars: Math.max(this.state.bars, this.rangeBars) });
36337
+ }
36338
+ }
36339
+ ]
36340
+ };
36341
+ const watermarkSection = {
36342
+ title: "Watermark",
36343
+ placement: "symbol",
36344
+ rows: [
36345
+ {
36346
+ kind: "toggle",
36347
+ label: "Symbol watermark",
36348
+ get: () => this.watermarkOn,
36349
+ set: (v) => this.setWatermarkVisible(v)
36350
+ }
36351
+ ]
36352
+ };
36353
+ const sections = [];
36354
+ if (this.statusline) {
36355
+ const sl = this.statusline;
36356
+ sections.push({
36357
+ title: "Status line",
36358
+ rows: [
36359
+ { kind: "heading", label: "Status line" },
36360
+ { kind: "toggle", label: "Symbol name", get: () => sl.partVisible("name"), set: (v) => sl.setPartVisible("name", v) },
36361
+ { kind: "toggle", label: "Market status", get: () => sl.partVisible("market"), set: (v) => sl.setPartVisible("market", v) },
36362
+ { kind: "toggle", label: "OHLC values", get: () => sl.partVisible("ohlc"), set: (v) => sl.setPartVisible("ohlc", v) },
36363
+ { kind: "toggle", label: "Bar change values", get: () => sl.partVisible("change"), set: (v) => sl.setPartVisible("change", v) },
36364
+ { kind: "heading", label: "Indicators" },
36365
+ {
36366
+ kind: "toggle",
36367
+ label: "Titles",
36368
+ get: () => this.indicatorTitlesOn,
36369
+ set: (v) => this.setIndicatorTitlesVisible(v)
36370
+ },
36371
+ {
36372
+ kind: "toggle",
36373
+ label: "Values",
36374
+ get: () => this.indicatorValuesOn,
36375
+ set: (v) => this.setIndicatorValuesVisible(v)
36376
+ }
36377
+ ]
36378
+ });
36379
+ }
36380
+ sections.push(advanced);
36381
+ if (this.sessionAvailableFlag) sections.push(sessionSection);
36382
+ sections.push(watermarkSection);
36383
+ chart.renderer.setSettingsSections(sections);
36384
+ }
35820
36385
  /** Show/hide this cell's symbol watermark (persisted per cell). */
35821
36386
  setWatermarkVisible(visible) {
35822
36387
  this.watermarkOn = visible;
@@ -35871,6 +36436,7 @@ var ChartCell = class {
35871
36436
  this.state.priceStyle = style;
35872
36437
  this.inner?.renderer.set("priceStyle", style);
35873
36438
  this.syncStatuslineColors();
36439
+ this.deps.onPriceStyleChanged(this.id);
35874
36440
  }
35875
36441
  /** OHLC/change ink in the status line follows the ACTIVE price style's configured
35876
36442
  * colors and direction rule (candle bodies by close-vs-open, baseline by position
@@ -36094,6 +36660,7 @@ var ChartCell = class {
36094
36660
  this.contextMenu.destroy();
36095
36661
  this.history.destroy();
36096
36662
  this.marketStatus?.stop();
36663
+ this.sessionShading.stop();
36097
36664
  this.statusline?.destroy();
36098
36665
  this.watermark?.destroy();
36099
36666
  this.inner?.destroy();
@@ -36378,7 +36945,7 @@ var GAP_PX = 2;
36378
36945
  var POOL_CAP = 16;
36379
36946
  var TIME_AXIS_H3 = 22;
36380
36947
  var ALERT_CAP = 50;
36381
- var STYLE_ID25 = "vela-workspace";
36948
+ var STYLE_ID26 = "vela-workspace";
36382
36949
  var CSS23 = `
36383
36950
  .vela-workspace { position: relative; width: 100%; height: 100%; display: flex; flex-direction: column; background: var(--vela-bg); }
36384
36951
  .vela-ws-main { position: relative; display: flex; flex-direction: row; flex: 1 1 auto; min-height: 0; }
@@ -36525,7 +37092,7 @@ var VelaWorkspace = class {
36525
37092
  this.order = boot?.charts ? boot.charts.map((c) => c.id) : declaredOrder(opts.cells);
36526
37093
  const bootActive = boot?.activeCellId ?? null;
36527
37094
  const doc = hostEl.ownerDocument;
36528
- injectStyles(STYLE_ID25, CSS23, doc);
37095
+ injectStyles(STYLE_ID26, CSS23, doc);
36529
37096
  this.root = doc.createElement("div");
36530
37097
  this.root.className = "vela-workspace";
36531
37098
  ensureUIHost(this.root, resolveTheme(opts.theme));
@@ -37125,6 +37692,7 @@ var VelaWorkspace = class {
37125
37692
  context: () => this.context(),
37126
37693
  activate: (id2) => this.setActiveCell(id2),
37127
37694
  onMarketChanged: (id2) => this.onCellMarketChanged(id2),
37695
+ onPriceStyleChanged: (id2) => this.onCellPriceStyleChanged(id2),
37128
37696
  onIndicatorsChanged: (id2) => this.onCellIndicatorsChanged(id2),
37129
37697
  onStateDirty: () => this.markStateDirty(),
37130
37698
  manifestSettled: () => this.manifestSettled
@@ -37414,6 +37982,16 @@ var VelaWorkspace = class {
37414
37982
  this.objectTree.setSymbol(cell.symbol);
37415
37983
  this.bottombar?.setSession({ session: cell.session, enabled: cell.sessionAvailable });
37416
37984
  }
37985
+ /** Trigger ② — a cell's price style changed: the topbar button/menu only if active.
37986
+ * Reads the cell back (not the requested style) so the button reflects what the
37987
+ * renderer actually applied. */
37988
+ onCellPriceStyleChanged(id) {
37989
+ this.markStateDirty();
37990
+ if (id !== this.activeId) return;
37991
+ const cell = this.cellsById.get(id);
37992
+ if (!cell) return;
37993
+ this.topbar.setPriceStyle(cell.priceStyle);
37994
+ }
37417
37995
  /** Trigger ② — a cell's indicator ledger changed: count + picker only if active. */
37418
37996
  onCellIndicatorsChanged(id) {
37419
37997
  this.markStateDirty();