@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
package/dist/widget.cjs CHANGED
@@ -9878,7 +9878,7 @@ function attachChromeTooltip(anchor, opts) {
9878
9878
  } else {
9879
9879
  const left = Math.min(a.left - h.left, Math.max(0, h.width - tip.offsetWidth - 4));
9880
9880
  tip.style.left = `${Math.max(0, left)}px`;
9881
- tip.style.top = `${a.bottom - h.top + 6}px`;
9881
+ tip.style.top = opts.placement === "above" ? `${Math.max(0, a.top - h.top - tip.offsetHeight - 6)}px` : `${a.bottom - h.top + 6}px`;
9882
9882
  }
9883
9883
  };
9884
9884
  const arm = (e) => {
@@ -10004,6 +10004,23 @@ var MENU_CSS = `
10004
10004
  background: var(--vela-hover-strong);
10005
10005
  color: var(--vela-fg-bright);
10006
10006
  }
10007
+ /* Checkmark mode (context/action menus): every row reserves the leading mark column so
10008
+ labels align; a checked row fills it with a \u2713 and brightens its ink \u2014 the row surface
10009
+ stays free for the hover wash. */
10010
+ .vela-menu-item .vela-menu-mark {
10011
+ width: 14px;
10012
+ flex: none;
10013
+ display: inline-flex;
10014
+ align-items: center;
10015
+ justify-content: center;
10016
+ }
10017
+ .vela-menu-item .vela-menu-mark .vela-icon {
10018
+ width: 14px;
10019
+ height: 14px;
10020
+ font-size: 14px;
10021
+ color: var(--vela-fg-bright);
10022
+ }
10023
+ .vela-menu-item[data-checkmark] { color: var(--vela-fg-bright); }
10007
10024
  /* Switch rows (boolean settings in a dropdown): a right-aligned toggle pill \u2014 the
10008
10025
  same control language as the settings dialog's toggles. */
10009
10026
  .vela-menu-switch {
@@ -10072,6 +10089,7 @@ var Surface = class _Surface {
10072
10089
  this.host = opts.host;
10073
10090
  this.onSelect = opts.onSelect;
10074
10091
  this.onFavorite = opts.onFavorite;
10092
+ this.checkmarks = opts.checkmarks === true;
10075
10093
  this.positioner = doc.createElement("div");
10076
10094
  this.positioner.className = "vela-ui-layer";
10077
10095
  this.list = doc.createElement("ul");
@@ -10150,6 +10168,7 @@ var Surface = class _Surface {
10150
10168
  build() {
10151
10169
  const doc = this.doc;
10152
10170
  this.list.replaceChildren();
10171
+ const markable = this.checkmarks && this.items.some((i) => !(i.submenu && i.submenu.length > 0) && !i.toggle && i.checked !== void 0);
10153
10172
  for (const item of this.items) {
10154
10173
  if (item.separatorBefore) {
10155
10174
  const sep = doc.createElement("li");
@@ -10163,7 +10182,16 @@ var Surface = class _Surface {
10163
10182
  li.dataset.veiId = item.id;
10164
10183
  if (item.toggle) {
10165
10184
  li.dataset.toggle = "1";
10166
- } else if (!branch && item.checked) {
10185
+ }
10186
+ if (markable) {
10187
+ const mark = doc.createElement("span");
10188
+ mark.className = "vela-menu-mark";
10189
+ if (!branch && !item.toggle && item.checked) {
10190
+ mark.appendChild(iconEl("check", doc));
10191
+ li.dataset.checkmark = "1";
10192
+ }
10193
+ li.appendChild(mark);
10194
+ } else if (!branch && !item.toggle && item.checked) {
10167
10195
  li.dataset.checked = "1";
10168
10196
  }
10169
10197
  if (item.icon) li.appendChild(iconEl(item.icon, doc));
@@ -10212,6 +10240,7 @@ var Surface = class _Surface {
10212
10240
  placement: "right-start",
10213
10241
  onSelect: this.onSelect,
10214
10242
  onFavorite: this.onFavorite,
10243
+ checkmarks: this.checkmarks,
10215
10244
  id: `${this.mid}--${item.id}`
10216
10245
  });
10217
10246
  sub.setItems(item.submenu ?? []);
@@ -10236,7 +10265,8 @@ var Menu = class {
10236
10265
  triggerId: opts.triggerId,
10237
10266
  id: opts.id,
10238
10267
  minWidth: opts.minWidth,
10239
- onFavorite: opts.onFavorite
10268
+ onFavorite: opts.onFavorite,
10269
+ checkmarks: opts.checkmarks
10240
10270
  });
10241
10271
  this.root.setItems(opts.items);
10242
10272
  }
@@ -10264,6 +10294,13 @@ var Menu = class {
10264
10294
  }
10265
10295
  };
10266
10296
 
10297
+ // src/core/model/inputs.ts
10298
+ function inputVisible(when, values) {
10299
+ if (!when) return true;
10300
+ const conds = Array.isArray(when) ? when : [when];
10301
+ return conds.every((c) => c.anyOf ? c.anyOf.some((x) => x === values[c.key]) : values[c.key] === c.equals);
10302
+ }
10303
+
10267
10304
  // src/ui/tokens.ts
10268
10305
  var STATIC_ID = "vela-ui-tokens";
10269
10306
  var STATIC_DECLS = Object.entries(STATIC_TOKENS).map(([k, v]) => ` ${k}: ${v};`).join("\n");
@@ -10272,8 +10309,13 @@ var STATIC_CSS = `
10272
10309
  ${STATIC_DECLS}
10273
10310
  font-family: var(--vela-font, -apple-system, system-ui, sans-serif);
10274
10311
  box-sizing: border-box;
10312
+ /* Chrome text (titles, buttons, menus, readouts) is UI, not copy \u2014 never selectable. */
10313
+ user-select: none;
10314
+ -webkit-user-select: none;
10275
10315
  }
10276
10316
  .vela-ui *, .vela-ui-layer * { box-sizing: border-box; }
10317
+ /* Text ENTRY is the one exception: selection is part of editing. */
10318
+ .vela-ui :is(input, textarea), .vela-ui-layer :is(input, textarea) { user-select: text; -webkit-user-select: text; }
10277
10319
  .vela-icon { display: inline-flex; align-items: center; flex: none; }
10278
10320
  .vela-icon svg { display: block; }
10279
10321
  `;
@@ -11275,6 +11317,9 @@ var NUMBER_CSS = `
11275
11317
  transition: border-color 0.12s ease, box-shadow 0.12s ease;
11276
11318
  -moz-appearance: textfield;
11277
11319
  appearance: textfield;
11320
+ /* Selectable even under a user-select:none host (chart wrapper, .vela-ui). */
11321
+ user-select: text;
11322
+ -webkit-user-select: text;
11278
11323
  }
11279
11324
  .vela-num input::-webkit-inner-spin-button, .vela-num input::-webkit-outer-spin-button { -webkit-appearance: none; margin: 0; }
11280
11325
  .vela-num input:hover { border-color: var(--vela-fg-muted); }
@@ -11487,6 +11532,9 @@ var TEXT_CSS = `
11487
11532
  overflow: hidden;
11488
11533
  text-overflow: ellipsis;
11489
11534
  transition: border-color 0.12s ease, box-shadow 0.12s ease;
11535
+ /* Selectable even under a user-select:none host (chart wrapper, .vela-ui). */
11536
+ user-select: text;
11537
+ -webkit-user-select: text;
11490
11538
  }
11491
11539
  .vela-text-field:hover { border-color: var(--vela-fg-muted); }
11492
11540
  .vela-text-field:focus { border-color: var(--vela-focus); box-shadow: 0 0 0 3px var(--vela-focus-soft); }
@@ -11580,6 +11628,9 @@ var TEXTAREA_CSS = `
11580
11628
  resize: vertical;
11581
11629
  outline: none;
11582
11630
  transition: border-color 0.12s ease, box-shadow 0.12s ease;
11631
+ /* Selectable even under a user-select:none host (chart wrapper, .vela-ui). */
11632
+ user-select: text;
11633
+ -webkit-user-select: text;
11583
11634
  }
11584
11635
  .vela-textarea-field:hover { border-color: var(--vela-fg-muted); }
11585
11636
  .vela-textarea-field:focus { border-color: var(--vela-focus); box-shadow: 0 0 0 3px var(--vela-focus-soft); }
@@ -12108,6 +12159,8 @@ var FIELD_CSS = `
12108
12159
  font-size: 14px;
12109
12160
  }
12110
12161
  .vela-field-label[data-size='sm'] { font-size: inherit; opacity: 0.85; }
12162
+ /* Titles are fully inert: spans, not label[for] \u2014 native labels propagate :hover and
12163
+ clicks onto their control, and both belong to the input alone. */
12111
12164
  .vela-field-cell { display: flex; align-items: center; gap: 8px; justify-self: start; }
12112
12165
  .vela-field-bool { display: flex; align-items: center; gap: 8px; min-height: 22px; }
12113
12166
  .vela-field-stacked { display: flex; flex-direction: column; gap: 8px; }
@@ -12166,11 +12219,17 @@ function fieldSeparator() {
12166
12219
  return el;
12167
12220
  }
12168
12221
  function labelEl(text, opts) {
12169
- const el = opts.id ? document.createElement("label") : document.createElement("span");
12170
- if (opts.id && el instanceof HTMLLabelElement) el.htmlFor = opts.id;
12222
+ const el = document.createElement("span");
12171
12223
  el.className = "vela-field-label";
12172
12224
  if (opts.size) el.dataset.size = opts.size;
12173
12225
  el.textContent = text;
12226
+ const id = opts.id;
12227
+ if (id) {
12228
+ el.id = `${id}--title`;
12229
+ queueMicrotask(() => {
12230
+ el.ownerDocument.getElementById(id)?.setAttribute("aria-labelledby", el.id);
12231
+ });
12232
+ }
12174
12233
  return el;
12175
12234
  }
12176
12235
  function asList(control) {
@@ -12199,9 +12258,7 @@ function fieldRow(opts) {
12199
12258
  it.className = "vela-field-inline-item";
12200
12259
  if (item.toggleFirst) it.style.flexDirection = "row-reverse";
12201
12260
  if (item.label) {
12202
- const lbl = labelEl(item.label, { id: item.id, size });
12203
- if (item.toggleFirst) lbl.style.cursor = "pointer";
12204
- it.appendChild(lbl);
12261
+ it.appendChild(labelEl(item.label, { id: item.id, size }));
12205
12262
  }
12206
12263
  if (item.fit) it.appendChild(item.control);
12207
12264
  else {
@@ -12226,7 +12283,7 @@ function fieldRow(opts) {
12226
12283
  tone: "bright",
12227
12284
  onChange: (v) => opts.toggle?.onChange(v)
12228
12285
  });
12229
- wrap2.append(sw.el, labelEl(opts.label, { size }));
12286
+ wrap2.append(sw.el, labelEl(opts.label, { id: opts.toggle?.id ?? opts.id, size }));
12230
12287
  if (opts.info) wrap2.appendChild(opts.info);
12231
12288
  if (opts.toggle?.get) {
12232
12289
  const get = opts.toggle.get;
@@ -12265,7 +12322,7 @@ function fieldRow(opts) {
12265
12322
  if (controls.length > 0) dimControls(controls, v);
12266
12323
  }
12267
12324
  });
12268
- left.append(sw.el, labelEl(opts.label, { size }));
12325
+ left.append(sw.el, labelEl(opts.label, { id: opts.toggle.id ?? opts.id, size }));
12269
12326
  wrap.appendChild(left);
12270
12327
  if (controls.length > 0) dimControls(controls, opts.toggle.checked);
12271
12328
  if (opts.toggle.get) {
@@ -12419,6 +12476,8 @@ var IndicatorInputsDialog = class {
12419
12476
  this.row = null;
12420
12477
  this.snapshot = null;
12421
12478
  this.dialogTips = [];
12479
+ /** Re-applies every `when` gate against current values; set while the dialog is open. */
12480
+ this.refreshVisibility = null;
12422
12481
  this.choicePop = null;
12423
12482
  this.calendarPop = null;
12424
12483
  this.calendarAnchor = null;
@@ -12481,14 +12540,17 @@ var IndicatorInputsDialog = class {
12481
12540
  tabs.style.cssText = `display:flex;gap:12px;padding:0 20px;border-bottom:1px solid ${border};flex:0 0 auto;`;
12482
12541
  const tabEls = [];
12483
12542
  const bodies = [];
12543
+ const gates = [];
12544
+ let active = 0;
12484
12545
  const activate = (idx) => {
12546
+ active = idx;
12485
12547
  for (let i = 0; i < tabEls.length; i += 1) {
12486
- const active = i === idx;
12548
+ const on = i === idx;
12487
12549
  const el = tabEls[i];
12488
- el.style.borderBottomColor = active ? fg : "transparent";
12489
- el.style.color = active ? "inherit" : "var(--vela-fg-muted)";
12490
- el.classList.toggle("vela-ind-tab-active", active);
12491
- bodies[i].style.display = active ? "grid" : "none";
12550
+ el.style.borderBottomColor = on ? fg : "transparent";
12551
+ el.style.color = on ? "inherit" : "var(--vela-fg-muted)";
12552
+ el.classList.toggle("vela-ind-tab-active", on);
12553
+ bodies[i].style.display = on ? "grid" : "none";
12492
12554
  }
12493
12555
  };
12494
12556
  for (const [idx, def] of tabDefs.entries()) {
@@ -12499,11 +12561,24 @@ var IndicatorInputsDialog = class {
12499
12561
  tab.addEventListener("click", () => activate(idx));
12500
12562
  tabs.appendChild(tab);
12501
12563
  tabEls.push(tab);
12502
- bodies.push(this.buildTabBody(def.inputs, row));
12564
+ bodies.push(this.buildTabBody(def.inputs, row, gates));
12565
+ if (def.inputs.every((d) => d.when)) {
12566
+ gates.push({ el: tab, visible: (bag) => def.inputs.some((d) => inputVisible(d.when, bag)) });
12567
+ }
12503
12568
  }
12504
12569
  ui.body.appendChild(tabs);
12505
12570
  for (const b of bodies) ui.body.appendChild(b);
12506
12571
  activate(0);
12572
+ const refresh = () => {
12573
+ const bag = valuesBag(row);
12574
+ for (const g of gates) g.el.style.display = g.visible(bag) ? "" : "none";
12575
+ if (tabEls[active]?.style.display === "none") {
12576
+ const next = tabEls.findIndex((el) => el.style.display !== "none");
12577
+ if (next >= 0) activate(next);
12578
+ }
12579
+ };
12580
+ this.refreshVisibility = refresh;
12581
+ refresh();
12507
12582
  const onBackdropDown = (e) => {
12508
12583
  if (e.target !== ui.backdrop && e.target !== ui.positioner) return;
12509
12584
  if (isPopoverOpen() || eventDismissedPopover(e)) {
@@ -12523,16 +12598,27 @@ var IndicatorInputsDialog = class {
12523
12598
  }
12524
12599
  /** One tab's scrollable body — one shared grid so every group's controls share a
12525
12600
  * left edge (a 100px number field lines up with a wider session/time pair). */
12526
- buildTabBody(inputs, row) {
12601
+ buildTabBody(inputs, row, gates) {
12527
12602
  const body = document.createElement("div");
12528
12603
  body.style.cssText = inputDialogBodyStyle(this.host.mobile());
12529
12604
  for (const group of groupInputs(inputs)) {
12530
12605
  const section = document.createElement("div");
12531
12606
  section.style.cssText = "display:contents;";
12607
+ let header = null;
12532
12608
  if (group.name) {
12533
- section.appendChild(fieldSection(group.name, { variant: "inputs", first: body.childElementCount === 0 }));
12609
+ header = fieldSection(group.name, { variant: "inputs", first: body.childElementCount === 0 });
12610
+ section.appendChild(header);
12611
+ }
12612
+ for (const inputRow of group.rows) {
12613
+ const el = this.buildInputRow(section, inputRow, row);
12614
+ if (inputRow.some((d) => d.when)) {
12615
+ gates.push({ el, visible: (bag) => rowVisible(inputRow, bag) });
12616
+ }
12617
+ }
12618
+ if (header && group.rows.every((r) => r.every((d) => d.when))) {
12619
+ const rows = group.rows;
12620
+ gates.push({ el: header, visible: (bag) => rows.some((r) => rowVisible(r, bag)) });
12534
12621
  }
12535
- for (const inputRow of group.rows) this.buildInputRow(section, inputRow, row);
12536
12622
  body.appendChild(section);
12537
12623
  }
12538
12624
  return body;
@@ -12540,6 +12626,7 @@ var IndicatorInputsDialog = class {
12540
12626
  close() {
12541
12627
  document.removeEventListener("keydown", this.onDialogKey);
12542
12628
  closeOpenPopovers();
12629
+ this.refreshVisibility = null;
12543
12630
  this.choicePop = null;
12544
12631
  this.calendarPop = null;
12545
12632
  this.calendarAnchor = null;
@@ -12569,11 +12656,21 @@ var IndicatorInputsDialog = class {
12569
12656
  }
12570
12657
  this.close();
12571
12658
  }
12659
+ /** Write one edit through: store it, notify the host, and re-apply the `when` gates. */
12660
+ commit(row, key, value) {
12661
+ row.values[key] = value;
12662
+ this.host.onChange?.({ indicatorId: row.id, key, value });
12663
+ this.refreshVisibility?.();
12664
+ }
12572
12665
  /** One settings row (or several `inline=` inputs) placed into a section's grid. */
12573
12666
  buildInputRow(grid, decls, row) {
12574
12667
  const idOf = (inp) => `vela-inp-${row.id}-${inp.key}`;
12668
+ const append = (el) => {
12669
+ grid.appendChild(el);
12670
+ return el;
12671
+ };
12575
12672
  if (decls.length > 1) {
12576
- grid.appendChild(fieldRow({
12673
+ return append(fieldRow({
12577
12674
  label: "",
12578
12675
  inline: decls.map((d) => ({
12579
12676
  label: nameOf(d) || void 0,
@@ -12584,14 +12681,13 @@ var IndicatorInputsDialog = class {
12584
12681
  })),
12585
12682
  info: [...decls].reverse().find((d) => d.tooltip)?.tooltip ? this.infoButton([...decls].reverse().find((d) => d.tooltip).tooltip) : void 0
12586
12683
  }));
12587
- return;
12588
12684
  }
12589
12685
  const lead = decls[0];
12590
12686
  const id = idOf(lead);
12591
12687
  const info = lead.tooltip ? this.infoButton(lead.tooltip) : void 0;
12592
12688
  if (lead.type === "bool") {
12593
12689
  const current = Boolean(row.values[lead.key] ?? lead.defval);
12594
- grid.appendChild(fieldRow({
12690
+ return append(fieldRow({
12595
12691
  label: nameOf(lead),
12596
12692
  id,
12597
12693
  bool: true,
@@ -12599,26 +12695,21 @@ var IndicatorInputsDialog = class {
12599
12695
  toggle: {
12600
12696
  id,
12601
12697
  checked: current,
12602
- onChange: (v) => {
12603
- row.values[lead.key] = v;
12604
- this.host.onChange?.({ indicatorId: row.id, key: lead.key, value: v });
12605
- }
12698
+ onChange: (v) => this.commit(row, lead.key, v)
12606
12699
  }
12607
12700
  }));
12608
- return;
12609
12701
  }
12610
12702
  if (lead.type === "text_area") {
12611
- grid.appendChild(fieldRow({
12703
+ return append(fieldRow({
12612
12704
  label: nameOf(lead),
12613
12705
  id,
12614
12706
  stacked: true,
12615
12707
  info,
12616
12708
  control: this.buildControl(row, lead, id)
12617
12709
  }));
12618
- return;
12619
12710
  }
12620
12711
  const fit = lead.type === "color" || lead.type === "session" || lead.type === "time";
12621
- grid.appendChild(fieldRow({
12712
+ return append(fieldRow({
12622
12713
  label: nameOf(lead),
12623
12714
  id,
12624
12715
  fit,
@@ -12630,10 +12721,7 @@ var IndicatorInputsDialog = class {
12630
12721
  /** Build the typed control for one input, committing edits live via `onChange`. */
12631
12722
  buildControl(row, inp, id) {
12632
12723
  const current = row.values[inp.key] ?? inp.defval;
12633
- const emit = (value) => {
12634
- row.values[inp.key] = value;
12635
- this.host.onChange?.({ indicatorId: row.id, key: inp.key, value });
12636
- };
12724
+ const emit = (value) => this.commit(row, inp.key, value);
12637
12725
  if (inp.type === "bool") {
12638
12726
  return buildFieldControl({ kind: "switch", id, checked: Boolean(current), onChange: (v) => emit(v) }).el;
12639
12727
  }
@@ -13054,6 +13142,14 @@ function tabInputs(inputs) {
13054
13142
  }
13055
13143
  return names.map((name) => ({ name, inputs: byTab.get(name) }));
13056
13144
  }
13145
+ function rowVisible(decls, bag) {
13146
+ return decls.some((d) => inputVisible(d.when, bag));
13147
+ }
13148
+ function valuesBag(row) {
13149
+ const bag = {};
13150
+ for (const inp of row.inputs) bag[inp.key] = row.values[inp.key] ?? inp.defval;
13151
+ return bag;
13152
+ }
13057
13153
  function groupInputs(inputs) {
13058
13154
  const order = [];
13059
13155
  const byGroup = /* @__PURE__ */ new Map();
@@ -13516,6 +13612,8 @@ var InputsUI = class {
13516
13612
  host: this.container,
13517
13613
  items,
13518
13614
  placement: "bottom-start",
13615
+ // Right-click action menu — checked state reads as a leading ✓ (see the kit Menu).
13616
+ checkmarks: true,
13519
13617
  onSelect: (itemId) => {
13520
13618
  if (itemId !== "values" || !this.rowMenuId) return;
13521
13619
  const target = this.rows.get(this.rowMenuId);
@@ -13851,6 +13949,8 @@ var InputsUI = class {
13851
13949
  syncRowActions(row) {
13852
13950
  const open2 = row.highlighted;
13853
13951
  row.controlsEl.style.display = open2 || row.hidden ? "inline-flex" : "none";
13952
+ if (open2) row.el.appendChild(row.statusEl);
13953
+ else row.el.insertBefore(row.statusEl, row.valuesEl);
13854
13954
  for (const child of Array.from(row.controlsEl.children)) {
13855
13955
  if (!(child instanceof HTMLElement) || child === row.eyeEl) continue;
13856
13956
  if (child === row.extrasEl) {
@@ -14094,6 +14194,148 @@ var PaneControls = class {
14094
14194
  }
14095
14195
  };
14096
14196
 
14197
+ // src/renderers/native/chrome/axisLayout.ts
14198
+ var AXIS_MASTER_W = 64;
14199
+ var AXIS_MERGED_W = 56;
14200
+ var PANE_SEPARATOR_PX = 3;
14201
+ function axisColumnX(dataW, column) {
14202
+ return column === 0 ? dataW : dataW + AXIS_MASTER_W + (column - 1) * AXIS_MERGED_W;
14203
+ }
14204
+
14205
+ // src/renderers/native/chrome/AxisScaleButtons.ts
14206
+ var STYLE_ID2 = "vela-axis-scale-buttons";
14207
+ var BTN_PX = 18;
14208
+ var BTN_GAP = 4;
14209
+ var AXIS_BORDER = 1;
14210
+ var CLUSTER_W = AXIS_MASTER_W - AXIS_BORDER;
14211
+ var PAD_TOP = 8;
14212
+ var PAD_BOTTOM = 6;
14213
+ var CLUSTER_H = BTN_PX + PAD_TOP + PAD_BOTTOM;
14214
+ var MIN_PANE_H = 48;
14215
+ function ensureStyles2() {
14216
+ if (typeof document === "undefined" || document.getElementById(STYLE_ID2)) return;
14217
+ const st = document.createElement("style");
14218
+ st.id = STYLE_ID2;
14219
+ st.textContent = `
14220
+ .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);}
14221
+ .vela-axis-btn:hover{background:var(--vela-active);color:var(--vela-fg-bright);}
14222
+ .vela-axis-on,.vela-axis-on:hover{background:var(--vela-selected-bg);color:var(--vela-selected-fg);}
14223
+ `;
14224
+ document.head.appendChild(st);
14225
+ }
14226
+ var AxisScaleButtons = class {
14227
+ constructor(plot, theme, deps) {
14228
+ this.plot = plot;
14229
+ this.theme = theme;
14230
+ this.deps = deps;
14231
+ this.tipDisposers = [];
14232
+ this.hoverPaneId = null;
14233
+ /** Reveal over the master scale column of the pane under the cursor (mouse only —
14234
+ * touch has no hover, and a drag-rescale on the axis must not sprout buttons). */
14235
+ this.onPlotMove = (e) => {
14236
+ if (e.pointerType !== "mouse") return;
14237
+ const rect = this.plot.getBoundingClientRect();
14238
+ const x = e.clientX - rect.left;
14239
+ const gutterLeft = rect.width - this.deps.rightAxis();
14240
+ let hit = null;
14241
+ if (x >= gutterLeft && x <= gutterLeft + AXIS_MASTER_W) {
14242
+ const y = e.clientY - rect.top;
14243
+ for (const p of this.deps.panes()) {
14244
+ if (p.collapsed || p.height < MIN_PANE_H) continue;
14245
+ if (y >= p.top && y <= p.top + p.height) {
14246
+ hit = p.id;
14247
+ break;
14248
+ }
14249
+ }
14250
+ }
14251
+ this.setHoverPane(hit);
14252
+ if (hit) this.sync();
14253
+ };
14254
+ this.onPlotLeave = () => this.setHoverPane(null);
14255
+ ensureStyles2();
14256
+ this.cluster = document.createElement("div");
14257
+ Object.assign(this.cluster.style, {
14258
+ position: "absolute",
14259
+ display: "none",
14260
+ justifyContent: "center",
14261
+ alignItems: "center",
14262
+ gap: `${BTN_GAP}px`,
14263
+ padding: `${PAD_TOP}px 0 ${PAD_BOTTOM}px`,
14264
+ width: `${CLUSTER_W}px`,
14265
+ boxSizing: "border-box",
14266
+ zIndex: "6",
14267
+ pointerEvents: "auto"
14268
+ });
14269
+ this.applyTheme();
14270
+ this.autoBtn = this.button("A", "Auto (fits data to screen)", () => this.deps.onToggleAuto(this.hoverPaneId ?? ""));
14271
+ this.logBtn = this.button("L", "Logarithmic scale", () => this.deps.onToggleLog(this.hoverPaneId ?? ""));
14272
+ this.cluster.append(this.autoBtn, this.logBtn);
14273
+ this.plot.appendChild(this.cluster);
14274
+ this.plot.addEventListener("pointermove", this.onPlotMove);
14275
+ this.plot.addEventListener("pointerleave", this.onPlotLeave);
14276
+ }
14277
+ button(label, title, onClick) {
14278
+ const b = document.createElement("button");
14279
+ b.type = "button";
14280
+ b.setAttribute("aria-label", title);
14281
+ b.textContent = label;
14282
+ b.className = "vela-axis-btn";
14283
+ Object.assign(b.style, { width: `${BTN_PX}px`, height: `${BTN_PX}px` });
14284
+ this.tipDisposers.push(attachChromeTooltip(b, {
14285
+ host: this.plot,
14286
+ theme: () => this.theme,
14287
+ text: () => title,
14288
+ placement: "above"
14289
+ }));
14290
+ b.addEventListener("click", (e) => {
14291
+ e.stopPropagation();
14292
+ onClick();
14293
+ this.sync();
14294
+ });
14295
+ return b;
14296
+ }
14297
+ /** Re-tint the chip when the plot background/border change (theme swap, config edit). */
14298
+ setTheme(theme) {
14299
+ this.theme = theme;
14300
+ this.applyTheme();
14301
+ }
14302
+ /** The strip is SOLID chart background: the buttons cover axis labels, never blend with them. */
14303
+ applyTheme() {
14304
+ this.cluster.style.background = this.theme.background;
14305
+ }
14306
+ setHoverPane(paneId) {
14307
+ if (this.hoverPaneId === paneId) return;
14308
+ this.hoverPaneId = paneId;
14309
+ this.reposition();
14310
+ }
14311
+ /** Re-place (or hide) the cluster after a hover or pane-layout change. */
14312
+ reposition() {
14313
+ const pane = this.hoverPaneId ? this.deps.panes().find((p) => p.id === this.hoverPaneId) : void 0;
14314
+ if (!pane || pane.collapsed || pane.height < MIN_PANE_H) {
14315
+ this.cluster.style.display = "none";
14316
+ return;
14317
+ }
14318
+ this.cluster.style.right = `${this.deps.rightAxis() - AXIS_MASTER_W}px`;
14319
+ this.cluster.style.top = `${pane.top + pane.height - CLUSTER_H}px`;
14320
+ this.cluster.style.display = "flex";
14321
+ this.sync();
14322
+ }
14323
+ /** Reflect the hovered pane's current auto/log state on the buttons. */
14324
+ sync() {
14325
+ const pane = this.deps.panes().find((p) => p.id === this.hoverPaneId);
14326
+ if (!pane) return;
14327
+ this.autoBtn.classList.toggle("vela-axis-on", pane.auto);
14328
+ this.logBtn.classList.toggle("vela-axis-on", pane.log);
14329
+ }
14330
+ destroy() {
14331
+ this.plot.removeEventListener("pointermove", this.onPlotMove);
14332
+ this.plot.removeEventListener("pointerleave", this.onPlotLeave);
14333
+ for (const dispose of this.tipDisposers) dispose();
14334
+ this.tipDisposers.length = 0;
14335
+ this.cluster.remove();
14336
+ }
14337
+ };
14338
+
14097
14339
  // src/renderers/shared/TableOverlay.ts
14098
14340
  var SIZE_PX3 = {
14099
14341
  auto: 13,
@@ -14451,6 +14693,8 @@ var BASELINE_BOTTOM_LINE = BEARISH;
14451
14693
  var BASELINE_FILL_ALPHA = 0.25;
14452
14694
  var BASELINE_FILL_ALPHA_FAR = 0.05;
14453
14695
  var BASELINE_LEVEL_DEFAULT = 50;
14696
+ var PREMARKET_SHADE = withAlpha(WARNING, 0.08);
14697
+ var POSTMARKET_SHADE = withAlpha(ACCENT, 0.08);
14454
14698
  function defaultChartStyle() {
14455
14699
  return {
14456
14700
  chartTypes: {},
@@ -14481,7 +14725,8 @@ function defaultChartStyle() {
14481
14725
  bottomFillColor2: null,
14482
14726
  width: 2,
14483
14727
  baselineLevel: BASELINE_LEVEL_DEFAULT
14484
- }
14728
+ },
14729
+ sessions: { premarketColor: PREMARKET_SHADE, postmarketColor: POSTMARKET_SHADE }
14485
14730
  };
14486
14731
  }
14487
14732
  var LINE_STYLES = ["solid", "dashed", "dotted"];
@@ -14615,6 +14860,7 @@ function mergeConfig(base, patch) {
14615
14860
  const area = asObject(p.area);
14616
14861
  const baseline = asObject(p.baseline);
14617
14862
  const series = asObject(p.series);
14863
+ const sessions = asObject(p.sessions);
14618
14864
  const stacking = asObject(p.stacking);
14619
14865
  const stackSeries = asObject(stacking.series);
14620
14866
  return {
@@ -14707,6 +14953,10 @@ function mergeConfig(base, patch) {
14707
14953
  baseline: series.baseline === null ? null : isNum(series.baseline) ? series.baseline : base.series.baseline,
14708
14954
  spacing: isNum(series.spacing) ? clampSpacing(series.spacing) : base.series.spacing
14709
14955
  },
14956
+ sessions: {
14957
+ premarketColor: isColor(sessions.premarketColor) ? sessions.premarketColor : base.sessions.premarketColor,
14958
+ postmarketColor: isColor(sessions.postmarketColor) ? sessions.postmarketColor : base.sessions.postmarketColor
14959
+ },
14710
14960
  stacking: {
14711
14961
  candles: isNum(stacking.candles) ? stacking.candles : base.stacking.candles,
14712
14962
  // Additive like `chartTypes`: a patch names only the ids it carries; the rest keep
@@ -14953,6 +15203,8 @@ var SceneGraph = class {
14953
15203
  this.tradeMarkers = defaultTradeMarkersState();
14954
15204
  /** Renderer-owned shaded time bands (session highlighting), behind grid + data. */
14955
15205
  this.highlights = [];
15206
+ /** Pre/post-market bands pushed by the host (`sessionZones` feature); null ⇒ no sessions. */
15207
+ this.sessionZones = null;
14956
15208
  /** Draw-order key of the price candles, relative to indicator series z (see `seriesZ`).
14957
15209
  * Indicators with z below this draw BEHIND the candles; at/above draw in front.
14958
15210
  * Default 0 with indicators mounting at z < 0 ⇒ the price reads on top of every overlay,
@@ -14987,6 +15239,15 @@ var SceneGraph = class {
14987
15239
  if (!this.orderedCache) this.orderedCache = [...this.panes.values()].sort((a, b) => a.order - b.order);
14988
15240
  return this.orderedCache;
14989
15241
  }
15242
+ /** The session zones resolved into colored bands (pre/post-market washes from the
15243
+ * config's session colors) — consumed by the same painting path as {@link highlights}. */
15244
+ sessionHighlightBands() {
15245
+ if (!this.sessionZones) return [];
15246
+ const out = [];
15247
+ for (const [from, to] of this.sessionZones.pre) out.push({ from, to, color: this.style.sessions.premarketColor });
15248
+ for (const [from, to] of this.sessionZones.post) out.push({ from, to, color: this.style.sessions.postmarketColor });
15249
+ return out;
15250
+ }
14990
15251
  indicatorsForPane(paneId) {
14991
15252
  const out = [];
14992
15253
  for (const model of this.indicators.values()) if (model.paneId === paneId) out.push(model);
@@ -15070,6 +15331,12 @@ var SceneGraph = class {
15070
15331
  assignIndicatorZ(id) {
15071
15332
  if (!this.seriesZ.has(id)) this.seriesZ.set(id, this.bottomZ() - 1);
15072
15333
  }
15334
+ /** Mount-time default for a LAYER-BACKED native (it paints on a canvas stacked above the
15335
+ * data canvas by default): top of the stack, so the recorded order tells the truth from
15336
+ * the first frame. Keeps an existing key, so a restored stack survives the remount. */
15337
+ assignIndicatorZTop(id) {
15338
+ if (!this.seriesZ.has(id)) this.seriesZ.set(id, this.topZ() + 1);
15339
+ }
15073
15340
  forgetIndicatorZ(id) {
15074
15341
  this.seriesZ.delete(id);
15075
15342
  }
@@ -15917,10 +16184,12 @@ var WebGL2Backend = class {
15917
16184
  }
15918
16185
  }
15919
16186
  }
15920
- /** Renderer-owned session highlight bands, clipped per-pane (scissor reconstructs full height). */
16187
+ /** Renderer-owned session highlight bands, clipped per-pane (scissor reconstructs full height).
16188
+ * Session-zone washes (pre/post-market) paint first, host highlights on top. */
15921
16189
  emitHighlights(b, scene, pane, coords) {
15922
- if (scene.highlights.length === 0) return;
15923
- for (const band of scene.highlights) {
16190
+ const bands = [...scene.sessionHighlightBands(), ...scene.highlights];
16191
+ if (bands.length === 0) return;
16192
+ for (const band of bands) {
15924
16193
  const x1 = coords.timeToX(band.from);
15925
16194
  const x2 = coords.timeToX(band.to);
15926
16195
  if (x2 < 0 || x1 > coords.width || x2 <= x1) continue;
@@ -17894,10 +18163,12 @@ var Canvas2dBackend = class {
17894
18163
  ctx.fillStyle = bg.color;
17895
18164
  ctx.fillRect(x1, pane.bounds.top, x2 - x1, pane.bounds.height);
17896
18165
  }
17897
- /** Renderer-owned session highlight bands: full-height (all panes), behind grid + data. */
18166
+ /** Renderer-owned session highlight bands: full-height (all panes), behind grid + data.
18167
+ * Session-zone washes (pre/post-market) paint first, host highlights on top. */
17898
18168
  drawHighlights(ctx, scene, coords) {
17899
- if (scene.highlights.length === 0) return;
17900
- for (const band of scene.highlights) {
18169
+ const bands = [...scene.sessionHighlightBands(), ...scene.highlights];
18170
+ if (bands.length === 0) return;
18171
+ for (const band of bands) {
17901
18172
  const x1 = coords.timeToX(band.from);
17902
18173
  const x2 = coords.timeToX(band.to);
17903
18174
  if (x2 < 0 || x1 > coords.width || x2 <= x1) continue;
@@ -18671,14 +18942,6 @@ var DrawingSceneRenderer = class {
18671
18942
  }
18672
18943
  };
18673
18944
 
18674
- // src/renderers/native/chrome/axisLayout.ts
18675
- var AXIS_MASTER_W = 64;
18676
- var AXIS_MERGED_W = 56;
18677
- var PANE_SEPARATOR_PX = 3;
18678
- function axisColumnX(dataW, column) {
18679
- return column === 0 ? dataW : dataW + AXIS_MASTER_W + (column - 1) * AXIS_MERGED_W;
18680
- }
18681
-
18682
18945
  // src/renderers/native/chrome/ChromeRenderer.ts
18683
18946
  var ChromeRenderer = class {
18684
18947
  constructor() {
@@ -19565,6 +19828,7 @@ var SettingsDialog = class {
19565
19828
  for (const hr of hs.rows) {
19566
19829
  if (hr.kind === "heading") body.append(this.sectionTitle(hr.label));
19567
19830
  else if (hr.kind === "toggle") body.append(this.boolRow(hr.label, hr.get(), (v) => hr.set(v)));
19831
+ else if (hr.kind === "color") body.append(this.colorRow(hr.label, hr.get(), (v) => hr.set(v)));
19568
19832
  else body.append(this.selectRowLabeled(hr.label, hr.get(), hr.options.map((o) => [o, o]), (v) => hr.set(v)));
19569
19833
  }
19570
19834
  }
@@ -22289,14 +22553,14 @@ function btn(label, primary, onClick) {
22289
22553
  // src/renderers/native/drawings/DrawingSettingsPopup.ts
22290
22554
  var BTN = 30;
22291
22555
  var ICON = 21;
22292
- var STYLE_ID2 = "vela-drawing-popup-styles";
22556
+ var STYLE_ID3 = "vela-drawing-popup-styles";
22293
22557
  var STYLE_REV = "3";
22294
- function ensureStyles2() {
22558
+ function ensureStyles3() {
22295
22559
  if (typeof document === "undefined") return;
22296
- const existing = document.getElementById(STYLE_ID2);
22560
+ const existing = document.getElementById(STYLE_ID3);
22297
22561
  if (existing?.dataset.rev === STYLE_REV) return;
22298
22562
  const s = existing ?? document.createElement("style");
22299
- s.id = STYLE_ID2;
22563
+ s.id = STYLE_ID3;
22300
22564
  s.dataset.rev = STYLE_REV;
22301
22565
  s.textContent = `
22302
22566
  .vela-dpop-btn{background:transparent;color:var(--vela-fg-muted);transition:background var(--vela-dur-fast) ease,color var(--vela-dur-fast) ease;}
@@ -22353,7 +22617,7 @@ var DrawingSettingsPopup = class {
22353
22617
  * `onClose` fires on an outside-click dismissal (not a programmatic close). */
22354
22618
  open(drawing, anchor, actions, onClose) {
22355
22619
  this.close();
22356
- ensureStyles2();
22620
+ ensureStyles3();
22357
22621
  this.onClose = onClose ?? null;
22358
22622
  const t = this.theme;
22359
22623
  const schema = drawing.schema();
@@ -23081,7 +23345,7 @@ var DrawingToolbar = class {
23081
23345
  this.onCollapse = options.onCollapse ?? (() => {
23082
23346
  });
23083
23347
  this.onDrawingsSync = options.onDrawingsSync ?? null;
23084
- ensureStyles3();
23348
+ ensureStyles4();
23085
23349
  this.root = document.createElement("div");
23086
23350
  this.root.className = "vela-dtb";
23087
23351
  this.styleRoot();
@@ -23564,13 +23828,13 @@ var DrawingToolbar = class {
23564
23828
  }
23565
23829
  };
23566
23830
  var MAGNET_ID = "__magnet__";
23567
- var STYLE_ID3 = "vela-drawing-toolbar-styles";
23568
- function ensureStyles3() {
23831
+ var STYLE_ID4 = "vela-drawing-toolbar-styles";
23832
+ function ensureStyles4() {
23569
23833
  if (typeof document === "undefined") return;
23570
- let s = document.getElementById(STYLE_ID3);
23834
+ let s = document.getElementById(STYLE_ID4);
23571
23835
  if (!s) {
23572
23836
  s = document.createElement("style");
23573
- s.id = STYLE_ID3;
23837
+ s.id = STYLE_ID4;
23574
23838
  document.head.appendChild(s);
23575
23839
  }
23576
23840
  s.textContent = `
@@ -24829,13 +25093,26 @@ function rendererLayers() {
24829
25093
  return [...registry3.values()];
24830
25094
  }
24831
25095
 
25096
+ // src/renderers/native/core/layerStacking.ts
25097
+ function stackLayers(entries, candleZ) {
25098
+ const keyed = entries.map((e) => ({
25099
+ id: e.id,
25100
+ key: e.ownerZ ?? (e.placement === "below-data" ? -Infinity : candleZ)
25101
+ }));
25102
+ keyed.sort((a, b) => a.key - b.key);
25103
+ const below = [];
25104
+ const above = [];
25105
+ for (const k of keyed) (k.key < candleZ ? below : above).push(k.id);
25106
+ return { below, above };
25107
+ }
25108
+
24832
25109
  // src/renderers/native/chrome/luxalgo-logos.ts
24833
25110
  var LUXALGO_SYMBOL_SVG = '<svg viewBox="0 0 45 40" aria-hidden="true"><g fill="currentColor"><path d="m40.25 38 4.58-7.998L28.802 2l-16.03 28 9.16-.001 6.87-12z"/><path d="M34.525 32.002 9.33 31.997 27.655 0h-9.158L.18 31.993 4.759 40h34.347z"/></g></svg>';
24834
25111
  var LUXALGO_WORDMARK_SVG = '<svg viewBox="0 0 964 252" aria-hidden="true"><path fill="currentColor" fill-rule="evenodd" d="M 614 94.500 L 614 189 633.500 189 L 653 189 653 94.500 L 653 0 633.500 0 L 614 0 614 94.500 M 0 97.500 L 0 189 63.023 189 L 126.045 189 125.773 170.750 L 125.500 152.500 84.250 152.239 L 43 151.978 43 78.989 L 43 6 21.500 6 L 0 6 0 97.500 M 450.387 96.451 C 421.871 146.253, 398.306 186.994, 398.020 186.987 C 397.423 186.972, 362 124.724, 362 123.691 C 362 123.316, 370.100 108.818, 380 91.472 C 389.900 74.127, 398 59.500, 398 58.968 C 398 58.380, 391.007 58, 380.210 58 L 362.420 58 348.060 83.138 C 334.400 107.052, 333.637 108.160, 332.399 105.888 C 331.682 104.575, 325.287 93.375, 318.187 81 L 305.277 58.500 287.139 58.227 C 277.162 58.078, 269 58.186, 269 58.469 C 269 58.751, 277.154 73.274, 287.120 90.741 C 297.086 108.209, 305.261 122.983, 305.287 123.574 C 305.312 124.164, 297.318 138.789, 287.522 156.074 C 277.725 173.358, 269.524 187.838, 269.296 188.250 C 269.068 188.662, 276.977 189, 286.870 189 L 304.859 189 319.163 163.959 C 331.321 142.674, 333.647 139.165, 334.670 140.563 C 335.331 141.468, 341.864 152.737, 349.186 165.606 L 362.500 189.003 402.442 188.752 L 442.384 188.500 472.080 136.695 C 488.413 108.203, 501.939 84.729, 502.138 84.531 C 502.337 84.333, 516 107.757, 532.500 136.583 L 562.500 188.994 584.750 188.997 C 596.987 188.999, 607 188.854, 607 188.675 C 607 188.319, 503.014 6.686, 502.532 6.201 C 502.369 6.037, 478.904 46.649, 450.387 96.451 M 717.500 55.017 C 697.175 57.389, 680.641 68.752, 672.036 86.264 C 665.866 98.818, 663.840 108.024, 663.840 123.500 C 663.840 133.409, 664.328 138.355, 665.892 144.300 C 672.076 167.810, 689.191 184.757, 711.529 189.489 C 730.549 193.518, 747.264 189.540, 761.282 177.648 C 764.944 174.542, 768.178 172, 768.470 172 C 769.345 172, 769.030 185.018, 767.979 192.285 C 765.618 208.613, 755.148 218.672, 738.473 220.632 C 732.586 221.324, 723.136 219.599, 717.709 216.843 C 712.693 214.295, 706.636 207.946, 703.972 202.443 C 702.789 199.999, 701.749 198, 701.661 198.001 C 700.201 198.007, 665.596 205.071, 665.230 205.437 C 664.326 206.341, 669.156 218.313, 672.607 223.721 C 682.146 238.672, 700.008 248.257, 723.500 251.033 C 758.132 255.124, 788.930 241.875, 800.287 218 C 806.923 204.047, 807 203.001, 807 126.542 L 807 58 788.500 58 L 770 58 770 66.500 C 770 71.175, 769.641 75, 769.203 75 C 768.765 75, 766.627 73.142, 764.453 70.872 C 752.805 58.708, 735.614 52.903, 717.500 55.017 M 878.500 55.031 C 850.107 59.412, 830.755 75.064, 822.285 100.500 C 819.905 107.644, 819.614 110.051, 819.565 123 C 819.501 139.581, 820.786 145.825, 826.763 158 C 832.104 168.880, 842.842 179.424, 854.077 184.822 C 867.651 191.343, 873.132 192.462, 891.500 192.462 C 909.868 192.462, 915.349 191.343, 928.923 184.822 C 944.653 177.265, 956.667 162.306, 961.710 144 C 963.074 139.048, 963.481 134.047, 963.418 123 C 963.346 110.140, 963.038 107.595, 960.694 100.500 C 953.325 78.193, 936.695 62.854, 913.500 56.969 C 904.262 54.625, 887.240 53.682, 878.500 55.031 M 134.015 102.750 C 134.027 139.250, 134.310 148.689, 135.549 153.947 C 141.139 177.663, 159.815 193, 183.105 193 C 196.453 193, 210.163 186.171, 217.936 175.650 C 219.826 173.093, 221.738 171, 222.186 171 C 222.634 171, 223 175.050, 223 180 L 223 189 242.507 189 L 262.014 189 261.757 123.750 L 261.500 58.500 242.500 58.500 L 223.500 58.500 223 99.500 C 222.513 139.406, 222.441 140.610, 220.289 144.634 C 209.729 164.377, 183.014 164.880, 174.299 145.500 C 172.677 141.895, 172.469 137.382, 172.190 99.750 L 171.880 58 152.940 58 L 134 58 134.015 102.750 M 729.932 87.998 C 719.311 89.821, 710.155 97.164, 705.141 107.880 C 702.851 112.772, 702.510 114.798, 702.505 123.521 C 702.500 132.834, 702.732 134.014, 705.787 140.218 C 709.761 148.290, 715.989 154.097, 723.853 157.063 C 731.409 159.912, 744.359 159.466, 750.795 156.136 C 767.289 147.601, 774.103 127.713, 767.019 108.780 C 761.433 93.851, 746.051 85.232, 729.932 87.998 M 883.972 88.348 C 865.749 92.310, 854.873 109.966, 858.119 130.316 C 860.933 147.963, 872.346 158.830, 889.084 159.799 C 900.714 160.472, 906.867 158.234, 914.650 150.497 C 919.856 145.323, 921.123 143.340, 922.900 137.587 C 925.412 129.456, 925.655 118.570, 923.489 111.275 C 918.479 94.405, 901.478 84.541, 883.972 88.348"/></svg>';
24835
25112
 
24836
25113
  // src/renderers/native/chrome/AttributionMark.ts
24837
25114
  var ATTRIBUTION_URL = "https://luxalgo.com/vela";
24838
- var STYLE_ID4 = "vela-attribution-styles";
25115
+ var STYLE_ID5 = "vela-attribution-styles";
24839
25116
  var CSS2 = `
24840
25117
  .vela-attribution {
24841
25118
  text-decoration: none;
@@ -24898,11 +25175,11 @@ var CSS2 = `
24898
25175
  [data-vela-layout='mobile'] .vela-attribution:hover .vela-attr-wordmark,
24899
25176
  [data-layout='mobile'] .vela-attribution:hover .vela-attr-wordmark { transform: translateX(0); }
24900
25177
  `;
24901
- function ensureStyles4(doc) {
24902
- let s = doc.getElementById(STYLE_ID4);
25178
+ function ensureStyles5(doc) {
25179
+ let s = doc.getElementById(STYLE_ID5);
24903
25180
  if (!s) {
24904
25181
  s = doc.createElement("style");
24905
- s.id = STYLE_ID4;
25182
+ s.id = STYLE_ID5;
24906
25183
  doc.head.appendChild(s);
24907
25184
  }
24908
25185
  s.textContent = CSS2;
@@ -24931,7 +25208,7 @@ function createCustomMark(doc, html, background) {
24931
25208
  return el;
24932
25209
  }
24933
25210
  function createAttributionMark(doc, background) {
24934
- ensureStyles4(doc);
25211
+ ensureStyles5(doc);
24935
25212
  const a = doc.createElement("a");
24936
25213
  a.className = "vela-attribution";
24937
25214
  a.href = ATTRIBUTION_URL;
@@ -25329,6 +25606,8 @@ var NativeRenderer = class {
25329
25606
  this.volumeRenderer = new VolumeRenderer();
25330
25607
  /** SDK renderer layers instantiated at mount ({@link registerRendererLayer}). */
25331
25608
  this.extLayers = [];
25609
+ /** Last applied layer-canvas order (ids below + above the data canvas) — re-slotted only on change. */
25610
+ this.layerOrderSig = "";
25332
25611
  // The attribution mark (see chrome/AttributionMark + the NOTICE file): default-on;
25333
25612
  // disabling requires an equivalent visible attribution elsewhere in the host UI.
25334
25613
  this.attributionEl = null;
@@ -25443,6 +25722,8 @@ var NativeRenderer = class {
25443
25722
  // total right-gutter width (master column + merged-scale columns)
25444
25723
  this.paneControls = null;
25445
25724
  // per-pane hover button cluster (top-right)
25725
+ this.axisScaleButtons = null;
25726
+ // A/L hover buttons at the bottom of each pane's price scale
25446
25727
  this.paneActionCbs = /* @__PURE__ */ new Set();
25447
25728
  // ── data window (crosshair OHLC + per-series readout, pulled by host panels) ──
25448
25729
  this.hoverLogical = null;
@@ -25486,7 +25767,7 @@ var NativeRenderer = class {
25486
25767
  this.priceStyleCbs = /* @__PURE__ */ new Set();
25487
25768
  this.tableOverlays = /* @__PURE__ */ new Map();
25488
25769
  this.name = "native";
25489
- 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"];
25770
+ 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"];
25490
25771
  /** Track cursor proximity to the scroll button on the plot (bubbles from the button too,
25491
25772
  * so moving onto the button doesn't count as leaving). */
25492
25773
  this.onScrollProximityMove = (e) => {
@@ -25602,6 +25883,9 @@ var NativeRenderer = class {
25602
25883
  case "highlights":
25603
25884
  this.scene.highlights = sanitizeHighlights(value);
25604
25885
  break;
25886
+ case "sessionZones":
25887
+ this.scene.sessionZones = sanitizeSessionZones(value);
25888
+ break;
25605
25889
  case "gridlines":
25606
25890
  this.scene.showGrid = Boolean(value);
25607
25891
  break;
@@ -25718,6 +26002,8 @@ var NativeRenderer = class {
25718
26002
  return this.scene.indicatorZOrder();
25719
26003
  case "highlights":
25720
26004
  return this.scene.highlights;
26005
+ case "sessionZones":
26006
+ return this.scene.sessionZones;
25721
26007
  case "gridlines":
25722
26008
  return this.scene.showGrid;
25723
26009
  case "axisLabels":
@@ -25927,7 +26213,8 @@ var NativeRenderer = class {
25927
26213
  baselineLevel: s.baseline.baselineLevel
25928
26214
  };
25929
26215
  })(),
25930
- series: { style: this.scene.priceStyle, baseline: this.scene.baselineValue, spacing: this.coords.spacingScale }
26216
+ series: { style: this.scene.priceStyle, baseline: this.scene.baselineValue, spacing: this.coords.spacingScale },
26217
+ sessions: { premarketColor: s.sessions.premarketColor, postmarketColor: s.sessions.postmarketColor }
25931
26218
  };
25932
26219
  }
25933
26220
  /**
@@ -26009,6 +26296,7 @@ var NativeRenderer = class {
26009
26296
  width: next.baseline.width,
26010
26297
  baselineLevel: next.baseline.baselineLevel
26011
26298
  };
26299
+ s.sessions = { premarketColor: next.sessions.premarketColor, postmarketColor: next.sessions.postmarketColor };
26012
26300
  this.setPriceStyle(next.series.style);
26013
26301
  this.scene.candleOverride = candleOverrideFor(this.scene.priceStyle, s.chartTypes);
26014
26302
  this.scene.baselineValue = next.series.baseline;
@@ -26022,6 +26310,7 @@ var NativeRenderer = class {
26022
26310
  }
26023
26311
  this.inputsUI?.setTheme(this.theme);
26024
26312
  this.paneControls?.setTheme(this.theme);
26313
+ this.axisScaleButtons?.setTheme(this.theme);
26025
26314
  this.userDrawings?.setTheme(this.chromeTheme());
26026
26315
  this.refreshScrollButtonTheme();
26027
26316
  this.refreshAttributionColor();
@@ -26249,9 +26538,7 @@ var NativeRenderer = class {
26249
26538
  if (el.getAttribute("data-vela-screenshot") === "under") rasterizeOverlay(ctx, el, frame);
26250
26539
  }
26251
26540
  }
26252
- const below = this.extLayers.filter((l) => l.def.placement === "below-data").map((l) => l.canvas);
26253
- const above = this.extLayers.filter((l) => l.def.placement !== "below-data").map((l) => l.canvas);
26254
- for (const canvas of [...below, this.dataCanvas, this.volumeCanvas, this.vpvrCanvas, ...above, this.chromeCanvas, this.drawingsCanvas]) {
26541
+ for (const canvas of [...this.canvasPile(), this.chromeCanvas, this.drawingsCanvas]) {
26255
26542
  if (canvas && canvas.width > 0 && canvas.height > 0) ctx.drawImage(canvas, 0, 0);
26256
26543
  }
26257
26544
  if (frame) {
@@ -26360,7 +26647,7 @@ var NativeRenderer = class {
26360
26647
  this.surfaceBackground = theme.background;
26361
26648
  this.surfaceTextColor = theme.textColor;
26362
26649
  this.wrapper = document.createElement("div");
26363
- Object.assign(this.wrapper.style, { position: "relative", width: "100%", height: "100%", overflow: "hidden", cursor: "crosshair" });
26650
+ Object.assign(this.wrapper.style, { position: "relative", width: "100%", height: "100%", overflow: "hidden", cursor: "crosshair", userSelect: "none", webkitUserSelect: "none" });
26364
26651
  applyChromeTokens(this.wrapper, this.chromeTheme());
26365
26652
  this.volumeCanvas = document.createElement("canvas");
26366
26653
  Object.assign(this.volumeCanvas.style, { position: "absolute", inset: "0", width: "100%", height: "100%", pointerEvents: "none" });
@@ -26385,6 +26672,7 @@ var NativeRenderer = class {
26385
26672
  const below = this.extLayers.filter((l) => l.def.placement === "below-data").map((l) => l.canvas);
26386
26673
  const above = this.extLayers.filter((l) => l.def.placement !== "below-data").map((l) => l.canvas);
26387
26674
  this.plot.append(...below, this.dataCanvas, this.volumeCanvas, this.vpvrCanvas, ...above, this.chromeCanvas, this.drawingsCanvas, this.cursorCanvas, this.overlayRoot);
26675
+ this.layerOrderSig = "";
26388
26676
  this.wrapper.appendChild(this.plot);
26389
26677
  this.factoryConfig = this.getConfig();
26390
26678
  this.attributionEl = this.buildAttributionEl();
@@ -26529,6 +26817,15 @@ var NativeRenderer = class {
26529
26817
  this.emitPaneAction({ type: "maximize", paneId, maximized });
26530
26818
  }
26531
26819
  });
26820
+ this.axisScaleButtons = new AxisScaleButtons(this.plot, theme, {
26821
+ panes: () => this.axisScaleViews(),
26822
+ rightAxis: () => this.rightAxisW,
26823
+ onToggleAuto: (paneId) => this.togglePaneAuto(paneId),
26824
+ onToggleLog: (paneId) => {
26825
+ const pane = this.scene.panes.get(paneId);
26826
+ if (pane) this.setPaneLog(paneId, !paneLogScale(this.scene, pane));
26827
+ }
26828
+ });
26532
26829
  this.resizeObserver = new ResizeObserver(() => this.resize());
26533
26830
  this.resizeObserver.observe(this.wrapper);
26534
26831
  this.watchDpr();
@@ -26605,6 +26902,7 @@ var NativeRenderer = class {
26605
26902
  this.applyBackground();
26606
26903
  this.inputsUI.setTheme(theme);
26607
26904
  this.paneControls?.setTheme(this.theme);
26905
+ this.axisScaleButtons?.setTheme(this.theme);
26608
26906
  this.settingsDialog?.refreshConfig(this.getConfig());
26609
26907
  this.syncThemeControl();
26610
26908
  this.settingsDialog?.setTheme(this.theme);
@@ -26653,6 +26951,7 @@ var NativeRenderer = class {
26653
26951
  this.liveRegion = null;
26654
26952
  this.inputsUI?.destroy();
26655
26953
  this.paneControls?.destroy();
26954
+ this.axisScaleButtons?.destroy();
26656
26955
  for (const overlay of this.tableOverlays.values()) overlay.destroy();
26657
26956
  this.tableOverlays.clear();
26658
26957
  this.resizeObserver?.disconnect();
@@ -26902,7 +27201,8 @@ var NativeRenderer = class {
26902
27201
  mountIndicator(model) {
26903
27202
  this.scene.indicators.set(model.id, model);
26904
27203
  this.refreshAnchorOffset(model);
26905
- this.scene.assignIndicatorZ(model.id);
27204
+ if (model.native && this.extLayers.some((l) => l.def.id === model.native.type)) this.scene.assignIndicatorZTop(model.id);
27205
+ else this.scene.assignIndicatorZ(model.id);
26906
27206
  this.inputsUI.upsert(model.id, model.shorttitle ?? model.title, model.inputs, model.inputValues, model.paneId, {
26907
27207
  native: !!model.native,
26908
27208
  ...model.shorttitle ? { settingsTitle: model.title } : {}
@@ -27721,10 +28021,17 @@ var NativeRenderer = class {
27721
28021
  const nowMs = typeof performance !== "undefined" ? performance.now() : Date.now();
27722
28022
  for (const l of this.extLayers) {
27723
28023
  if (!l.def.repaintOnCursor) continue;
27724
- l.instance.render(this.extLayerArgs(l.def.id, pane.scale, pane.bounds, nowMs));
28024
+ const lp = this.layerPane(l.def.id) ?? pane;
28025
+ if (lp.collapsed) continue;
28026
+ l.instance.render(this.extLayerArgs(l.def.id, lp.scale, lp.bounds, nowMs));
27725
28027
  if (this.animZoom && l.instance.animating?.()) this.animator.start();
27726
28028
  }
27727
28029
  }
28030
+ /** Blank one SDK layer canvas (a collapsed host pane suppresses the layer's painting). */
28031
+ clearLayerCanvas(canvas) {
28032
+ if (canvas.width === 0 || canvas.height === 0) return;
28033
+ canvas.getContext("2d")?.clearRect(0, 0, canvas.width, canvas.height);
28034
+ }
27728
28035
  /** One frame's args for an SDK renderer layer (shared by the data + cursor paint paths). */
27729
28036
  extLayerArgs(id, scale, bounds, nowMs) {
27730
28037
  return {
@@ -27743,6 +28050,7 @@ var NativeRenderer = class {
27743
28050
  }
27744
28051
  /** Paint the below-data (L-1) + geometry (L0) + chrome (L1) layers from the current scene/coords. */
27745
28052
  paintData() {
28053
+ this.syncLayerCanvasOrder();
27746
28054
  this.stampScaleInvert();
27747
28055
  this.backend.modelAlpha = this.modelAlpha;
27748
28056
  this.backend.candleBodyAlpha = this.candleBodyAlpha;
@@ -27773,9 +28081,14 @@ var NativeRenderer = class {
27773
28081
  const nowMs = typeof performance !== "undefined" ? performance.now() : Date.now();
27774
28082
  let folded = null;
27775
28083
  for (const l of this.extLayers) {
27776
- const args = this.extLayerArgs(l.def.id, pane.scale, pane.bounds, nowMs);
28084
+ const lp = this.layerPane(l.def.id) ?? pane;
28085
+ if (lp.collapsed) {
28086
+ this.clearLayerCanvas(l.canvas);
28087
+ continue;
28088
+ }
28089
+ const args = this.extLayerArgs(l.def.id, lp.scale, lp.bounds, nowMs);
27777
28090
  l.instance.render(args);
27778
- folded = foldBaseModulation(folded, l.instance.modulateBase?.(args) ?? null);
28091
+ if (lp === pane) folded = foldBaseModulation(folded, l.instance.modulateBase?.(args) ?? null);
27779
28092
  if (this.animZoom && l.instance.animating?.()) this.animator.start();
27780
28093
  }
27781
28094
  if (folded) {
@@ -27937,6 +28250,9 @@ var NativeRenderer = class {
27937
28250
  pane.scaleTarget = { min: 0, max: maxVol / VOLUME_PANE_FILL_FRAC };
27938
28251
  pane.axisFormat = "volume";
27939
28252
  }
28253
+ } else if (this.layerNativesOwnPane(pane, masterModels)) {
28254
+ pane.scaleTarget = computePaneScale([], this.bars, true, i0, i1, dr, paneLogScale(this.scene, pane), (id) => this.scene.offsetOf(id));
28255
+ pane.percentBaseline = this.bars[i0]?.close ?? 0;
27940
28256
  }
27941
28257
  if (pane === pricePane && this.scene.tradeMarkers.visible && pane.bounds.height > 0) {
27942
28258
  const th = this.tradesScaleHints(vr);
@@ -28023,6 +28339,59 @@ var NativeRenderer = class {
28023
28339
  }
28024
28340
  return null;
28025
28341
  }
28342
+ /** The mounted native indicator that OWNS an SDK layer — the one whose type equals the
28343
+ * layer id (the id doubles as the data channel, so the pairing is the SDK's own
28344
+ * contract). Null for chart-type channels and while the owner is hidden (a hidden
28345
+ * indicator leaves the scene; its cleared data channel paints nothing anyway). */
28346
+ layerOwner(layerId) {
28347
+ for (const m of this.scene.indicators.values()) {
28348
+ if (m.native?.type === layerId) return m;
28349
+ }
28350
+ return null;
28351
+ }
28352
+ /** The pane an SDK layer paints on: its owner's pane, else the price pane. */
28353
+ layerPane(layerId) {
28354
+ const owner = this.layerOwner(layerId);
28355
+ const paneId = owner ? owner.paneId ?? PRICE_PANE_ID : PRICE_PANE_ID;
28356
+ return this.scene.panes.get(paneId) ?? null;
28357
+ }
28358
+ /** The SDK layer canvases split around the data canvas, each side back-to-front:
28359
+ * owned layers by their owner's z key against the candles' (an indicator restacked
28360
+ * below the candles takes its layer canvas along), unowned by declared placement. */
28361
+ orderedLayerCanvases() {
28362
+ const byId = new Map(this.extLayers.map((l) => [l.def.id, l.canvas]));
28363
+ const { below, above } = stackLayers(
28364
+ this.extLayers.map((l) => {
28365
+ const owner = this.layerOwner(l.def.id);
28366
+ return {
28367
+ id: l.def.id,
28368
+ placement: l.def.placement === "below-data" ? "below-data" : "above-data",
28369
+ ownerZ: owner ? this.scene.zOf(owner.id) : null
28370
+ };
28371
+ }),
28372
+ this.scene.candleZ
28373
+ );
28374
+ return { below: below.map((id) => byId.get(id)), above: above.map((id) => byId.get(id)) };
28375
+ }
28376
+ /** The full canvas pile in paint order (layers + data/volume/vpvr) — what the DOM
28377
+ * stacking and the screenshot compositor must both follow. */
28378
+ canvasPile() {
28379
+ const { below, above } = this.orderedLayerCanvases();
28380
+ return [...below, this.dataCanvas, this.volumeCanvas, this.vpvrCanvas, ...above];
28381
+ }
28382
+ /** Re-slot the SDK layer canvases in the plot when the computed order changed (a z
28383
+ * write, a restored config, an indicator mount/remove/restack). Runs at the top of
28384
+ * every data frame; a no-op when the signature is unchanged. Re-inserting an
28385
+ * absolutely-positioned, pointer-transparent canvas repaints nothing by itself. */
28386
+ syncLayerCanvasOrder() {
28387
+ if (this.extLayers.length === 0 || !this.plot) return;
28388
+ const { below, above } = this.orderedLayerCanvases();
28389
+ 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(",");
28390
+ if (sig === this.layerOrderSig) return;
28391
+ this.layerOrderSig = sig;
28392
+ for (const c of below) this.plot.insertBefore(c, this.dataCanvas);
28393
+ for (const c of above) this.plot.insertBefore(c, this.chromeCanvas);
28394
+ }
28026
28395
  /** True when an active volume layer is this study pane's ONLY content — so its scale should
28027
28396
  * come from volume, not the empty {0,1} placeholder. (In the price pane, or alongside a real
28028
28397
  * series, volume stays a bottom overlay and the master scale wins.) */
@@ -28032,6 +28401,15 @@ var NativeRenderer = class {
28032
28401
  if (this.nativeLayerPane("volume") !== pane) return false;
28033
28402
  return masterModels.every((m) => m.native?.type === "volume");
28034
28403
  }
28404
+ /** True when this study pane's master content is only SDK-layer natives (series-less
28405
+ * models whose type names a mounted layer) — its scale then follows the visible bars
28406
+ * (see the call site). Any real master series takes over the scale as usual. */
28407
+ layerNativesOwnPane(pane, masterModels) {
28408
+ if (pane.kind === "price" || masterModels.length === 0) return false;
28409
+ return masterModels.every(
28410
+ (m) => m.series.length === 0 && !!m.native && this.extLayers.some((l) => l.def.id === m.native.type)
28411
+ );
28412
+ }
28035
28413
  /** Per-pane scale state for a host UI (e.g. a price-axis context menu): the pane's pixel
28036
28414
  * band (`top`/`height`, so a click y maps to a pane) plus its current axis `mode`/`log`.
28037
28415
  * Top-to-bottom order. Every pane is independent — the price pane from the scene setting,
@@ -28047,6 +28425,25 @@ var NativeRenderer = class {
28047
28425
  invert: paneInvert(this.scene, p)
28048
28426
  }));
28049
28427
  }
28428
+ /** Thin projection of the panes for the axis A/L hover buttons (top-to-bottom order). */
28429
+ axisScaleViews() {
28430
+ return this.scene.orderedPanes().map((p) => ({
28431
+ id: p.id,
28432
+ top: p.bounds.top,
28433
+ height: p.bounds.height,
28434
+ collapsed: p.collapsed,
28435
+ auto: p.manualScale == null,
28436
+ log: paneLogScale(this.scene, p)
28437
+ }));
28438
+ }
28439
+ /** Toggle one pane's autoscale (the A axis button): off freezes the current window into
28440
+ * manual mode, on drops it — the per-pane twin of the chart-level `autoScale` feature. */
28441
+ togglePaneAuto(paneId) {
28442
+ const pane = this.scene.panes.get(paneId);
28443
+ if (!pane) return;
28444
+ pane.manualScale = pane.manualScale == null ? { ...pane.scale } : null;
28445
+ this.scheduler?.invalidate(4 /* Full */);
28446
+ }
28050
28447
  /** Set one pane's axis mode. The price pane routes to the scene-level setting (persisted +
28051
28448
  * keyboard shortcuts); a study pane keeps its own, so panes never affect each other. */
28052
28449
  setPaneScaleMode(paneId, mode) {
@@ -28058,19 +28455,18 @@ var NativeRenderer = class {
28058
28455
  }
28059
28456
  this.scheduler?.invalidate(4 /* Full */);
28060
28457
  }
28061
- /** Set one pane's logarithmic flag. Clears that pane's frozen (manual) window so the new
28062
- * space re-fits, and routes the price pane to the scene-level flag. */
28458
+ /** Set one pane's logarithmic flag; the price pane routes to the scene-level flag.
28459
+ * Log and auto are INDEPENDENT: a frozen (manual) window is kept — its price bounds
28460
+ * stay put and only its `log` tag flips, so the same range re-renders in the new
28461
+ * space instead of snapping back to autoscale. */
28063
28462
  setPaneLog(paneId, log) {
28064
- if (paneId === PRICE_PANE_ID) {
28065
- this.scene.logScale = log;
28066
- const price = this.scene.orderedPanes().find((p) => p.kind === "price");
28067
- if (price) price.manualScale = null;
28068
- } else {
28069
- const p = this.scene.panes.get(paneId);
28070
- if (!p) return;
28071
- p.logScale = log;
28072
- p.manualScale = null;
28463
+ const pane = paneId === PRICE_PANE_ID ? this.scene.orderedPanes().find((p) => p.kind === "price") : this.scene.panes.get(paneId);
28464
+ if (paneId === PRICE_PANE_ID) this.scene.logScale = log;
28465
+ else {
28466
+ if (!pane) return;
28467
+ pane.logScale = log;
28073
28468
  }
28469
+ if (pane?.manualScale) pane.manualScale.log = log;
28074
28470
  this.scheduler?.invalidate(4 /* Full */);
28075
28471
  }
28076
28472
  /** Flip one pane's axis (high at the bottom). The price pane routes to the scene-level flag
@@ -28169,6 +28565,7 @@ var NativeRenderer = class {
28169
28565
  }
28170
28566
  this.inputsUI?.reposition();
28171
28567
  this.paneControls?.reposition();
28568
+ this.axisScaleButtons?.reposition();
28172
28569
  this.repositionScrollButton();
28173
28570
  this.positionAttribution();
28174
28571
  this.publishPricePaneBounds();
@@ -28187,6 +28584,7 @@ var NativeRenderer = class {
28187
28584
  }
28188
28585
  this.inputsUI?.reposition();
28189
28586
  this.paneControls?.reposition();
28587
+ this.axisScaleButtons?.reposition();
28190
28588
  this.repositionScrollButton();
28191
28589
  this.positionAttribution();
28192
28590
  this.publishPricePaneBounds();
@@ -28394,6 +28792,22 @@ function sanitizeHighlights(value) {
28394
28792
  }
28395
28793
  return out.sort((a, b) => a.from - b.from);
28396
28794
  }
28795
+ function sanitizeSessionZones(value) {
28796
+ if (value == null || typeof value !== "object") return null;
28797
+ const v = value;
28798
+ const windows = (raw) => {
28799
+ if (!Array.isArray(raw)) return [];
28800
+ const out = [];
28801
+ for (const w of raw) {
28802
+ if (!Array.isArray(w)) continue;
28803
+ const from = Number(w[0]);
28804
+ const to = Number(w[1]);
28805
+ if (Number.isFinite(from) && Number.isFinite(to) && to > from) out.push([from, to]);
28806
+ }
28807
+ return out.sort((a, b) => a[0] - b[0]);
28808
+ };
28809
+ return { pre: windows(v.pre), post: windows(v.post) };
28810
+ }
28397
28811
  function clamp012(v) {
28398
28812
  return Math.max(0, Math.min(1, v));
28399
28813
  }
@@ -29426,7 +29840,7 @@ var Drawer = class {
29426
29840
  };
29427
29841
 
29428
29842
  // src/widget/layout-picker.ts
29429
- var STYLE_ID5 = "vela-widget-layout-picker-v14";
29843
+ var STYLE_ID6 = "vela-widget-layout-picker-v14";
29430
29844
  var CSS3 = `
29431
29845
  .vela-lp-layer { position: absolute; z-index: var(--vela-z-menu); }
29432
29846
  .vela-lp {
@@ -29531,7 +29945,7 @@ var CSS3 = `
29531
29945
  `;
29532
29946
  var GRID = 4;
29533
29947
  function layoutGridCanvas(doc) {
29534
- injectStyles(STYLE_ID5, CSS3, doc);
29948
+ injectStyles(STYLE_ID6, CSS3, doc);
29535
29949
  const el = doc.createElement("div");
29536
29950
  el.className = "vela-lp-grid";
29537
29951
  const squares = [];
@@ -29571,7 +29985,7 @@ var LayoutPicker = class {
29571
29985
  };
29572
29986
  this.opts = opts;
29573
29987
  this.doc = opts.host.ownerDocument;
29574
- injectStyles(STYLE_ID5, CSS3, this.doc);
29988
+ injectStyles(STYLE_ID6, CSS3, this.doc);
29575
29989
  const doc = this.doc;
29576
29990
  this.layer = doc.createElement("div");
29577
29991
  this.layer.className = "vela-ui-layer vela-lp-layer";
@@ -29916,7 +30330,7 @@ function timeframeLabel(value) {
29916
30330
  }
29917
30331
 
29918
30332
  // src/widget/topbar.ts
29919
- var STYLE_ID6 = "vela-topbar";
30333
+ var STYLE_ID7 = "vela-topbar";
29920
30334
  var CSS4 = `
29921
30335
  .vela-widget-topbar {
29922
30336
  display: flex;
@@ -29973,12 +30387,14 @@ var CSS4 = `
29973
30387
  color: var(--vela-fg-muted);
29974
30388
  }
29975
30389
  .vela-widget-tf-caret:hover { background: var(--vela-hover); color: var(--vela-fg-bright); }
30390
+ /* The merged trigger is a plain button (hover feedback only) \u2014 the highlight
30391
+ background marks the CURRENT chip among favorites, and a lone trigger with a
30392
+ permanent highlight would read as stuck-pressed. */
29976
30393
  .vela-widget-tf-caret[data-solo='1'] {
29977
30394
  width: auto;
29978
30395
  padding: 0 6px 0 9px;
29979
30396
  gap: 4px;
29980
30397
  color: var(--vela-fg-bright);
29981
- background: var(--vela-hover-strong);
29982
30398
  font-size: 13px;
29983
30399
  font-weight: 550;
29984
30400
  white-space: nowrap;
@@ -30081,7 +30497,7 @@ var Topbar = class {
30081
30497
  this.timeframe = opts.timeframe;
30082
30498
  this.priceStyle = opts.priceStyle;
30083
30499
  const doc = host.ownerDocument;
30084
- injectStyles(STYLE_ID6, CSS4, doc);
30500
+ injectStyles(STYLE_ID7, CSS4, doc);
30085
30501
  this.el = doc.createElement("div");
30086
30502
  this.el.className = "vela-widget-topbar";
30087
30503
  this.symbolEl = doc.createElement("button");
@@ -30430,7 +30846,7 @@ function tickerIconEl(doc, base, name, className) {
30430
30846
  }
30431
30847
 
30432
30848
  // src/widget/statusline.ts
30433
- var STYLE_ID7 = "vela-widget-statusline";
30849
+ var STYLE_ID8 = "vela-widget-statusline";
30434
30850
  var CSS5 = `
30435
30851
  .vela-statusline {
30436
30852
  position: absolute;
@@ -30622,7 +31038,7 @@ var Statusline = class {
30622
31038
  this.fitMode = false;
30623
31039
  this.fitRO = null;
30624
31040
  const doc = host.ownerDocument;
30625
- injectStyles(STYLE_ID7, CSS5, doc);
31041
+ injectStyles(STYLE_ID8, CSS5, doc);
30626
31042
  host.classList.add("vela-has-statusline");
30627
31043
  this.el = doc.createElement("div");
30628
31044
  this.el.className = "vela-statusline";
@@ -30895,11 +31311,70 @@ var MarketStatusTracker = class {
30895
31311
  }
30896
31312
  };
30897
31313
 
31314
+ // src/widget/session-shading.ts
31315
+ function deriveSessionZones(regular, extended) {
31316
+ const pre = [];
31317
+ const post = [];
31318
+ for (const [extStart, extEnd] of extended) {
31319
+ const inside = regular.filter(([s, e]) => e > extStart && s < extEnd).sort((a, b) => a[0] - b[0]);
31320
+ let cursor = extStart;
31321
+ for (const [regStart, regEnd] of inside) {
31322
+ if (regStart > cursor) pre.push([cursor, Math.min(regStart, extEnd)]);
31323
+ cursor = Math.max(cursor, regEnd);
31324
+ }
31325
+ if (cursor < extEnd) post.push([cursor, extEnd]);
31326
+ }
31327
+ return { pre, post };
31328
+ }
31329
+ var FETCH_AHEAD_MS2 = 10 * 864e5;
31330
+ var MIN_LOOKBACK_MS = 3 * 864e5;
31331
+ var MAX_LOOKBACK_MS = 120 * 864e5;
31332
+ var SessionShadingTracker = class {
31333
+ constructor(onZones) {
31334
+ this.onZones = onZones;
31335
+ /** Invalidates detached async work — bumped by every track()/stop(). */
31336
+ this.epoch = 0;
31337
+ }
31338
+ /** (Re)bind to a chart's data surface + market and evaluate once. */
31339
+ track(data, symbol, opts) {
31340
+ const my = ++this.epoch;
31341
+ void this.evaluate(my, data, symbol, opts);
31342
+ }
31343
+ stop() {
31344
+ this.epoch += 1;
31345
+ }
31346
+ async evaluate(my, data, symbol, opts) {
31347
+ const resolved = data.resolve(symbol);
31348
+ const provider = resolved ? data.providerInstance(resolved.provider) : void 0;
31349
+ const si = await data.symbolInfo(symbol).catch(() => void 0);
31350
+ if (my !== this.epoch) return;
31351
+ const hasSessions = typeof si?.session === "string" && si.session !== "" && si.session !== "24x7";
31352
+ if (!provider?.getCalendar || !hasSessions || !resolved) {
31353
+ this.onZones(null);
31354
+ return;
31355
+ }
31356
+ if (opts.session !== "extended") {
31357
+ this.onZones({ pre: [], post: [] });
31358
+ return;
31359
+ }
31360
+ const now = Date.now();
31361
+ const lookback = Math.max(MIN_LOOKBACK_MS, Math.min(MAX_LOOKBACK_MS, opts.lookbackMs));
31362
+ const range = { from: now - lookback, to: now + FETCH_AHEAD_MS2 };
31363
+ const [regular, extended] = await Promise.all([
31364
+ provider.getCalendar(resolved.ticker, { ...range, session: "regular" }).catch(() => null),
31365
+ provider.getCalendar(resolved.ticker, { ...range, session: "extended" }).catch(() => null)
31366
+ ]);
31367
+ if (my !== this.epoch) return;
31368
+ if (!regular || !extended) return;
31369
+ this.onZones(deriveSessionZones(regular, extended));
31370
+ }
31371
+ };
31372
+
30898
31373
  // src/widget/watermark.ts
30899
31374
  var MAX_FONT_PX = 36;
30900
31375
  var MIN_FONT_PX = 12;
30901
31376
  var FILL = 0.9;
30902
- var STYLE_ID8 = "vela-widget-watermark";
31377
+ var STYLE_ID9 = "vela-widget-watermark";
30903
31378
  var CSS6 = `
30904
31379
  .vela-watermark {
30905
31380
  position: absolute;
@@ -30938,7 +31413,7 @@ var Watermark = class {
30938
31413
  * canvas, so the mark stays out of its way. Starts true: the FIRST `load:start`
30939
31414
  * fires during chart construction, before any subscriber can see it. */
30940
31415
  this.loading = true;
30941
- injectStyles(STYLE_ID8, CSS6, host.ownerDocument);
31416
+ injectStyles(STYLE_ID9, CSS6, host.ownerDocument);
30942
31417
  this.el = host.ownerDocument.createElement("div");
30943
31418
  this.el.className = "vela-watermark";
30944
31419
  this.el.dataset.velaScreenshot = "under";
@@ -31002,7 +31477,7 @@ var RANGE_PRESETS = [
31002
31477
  { id: "ALL", tf: "W", preset: "ALL", bars: 5e3 }
31003
31478
  // everything the provider serves
31004
31479
  ];
31005
- var STYLE_ID9 = "vela-widget-bottombar";
31480
+ var STYLE_ID10 = "vela-widget-bottombar";
31006
31481
  var CSS7 = `
31007
31482
  .vela-widget-bottombar {
31008
31483
  display: flex;
@@ -31082,7 +31557,7 @@ var Bottombar = class {
31082
31557
  this.timer = null;
31083
31558
  this.timezone = opts.timezone;
31084
31559
  const doc = host.ownerDocument;
31085
- injectStyles(STYLE_ID9, CSS7, doc);
31560
+ injectStyles(STYLE_ID10, CSS7, doc);
31086
31561
  this.el = doc.createElement("div");
31087
31562
  this.el.className = "vela-widget-bottombar";
31088
31563
  for (const preset of RANGE_PRESETS) {
@@ -31109,7 +31584,8 @@ var Bottombar = class {
31109
31584
  const session = doc.createElement("span");
31110
31585
  session.className = "vela-bb-session";
31111
31586
  this.sessionEl = session;
31112
- session.title = "Session \u2014 stocks & ETFs only";
31587
+ session.style.display = "none";
31588
+ session.title = "Session \u2014 regular (RTH) vs extended (ETH) hours";
31113
31589
  for (const [key, label] of [["regular", "RTH"], ["extended", "ETH"]]) {
31114
31590
  const b = doc.createElement("button");
31115
31591
  b.className = "vela-bb-session-btn" + (key === "regular" ? " is-active" : "");
@@ -31160,15 +31636,15 @@ var Bottombar = class {
31160
31636
  }
31161
31637
  /**
31162
31638
  * Reflect the ACTIVE chart's session posture. `enabled: false` (a continuous
31163
- * market, or metadata not landed yet) shows the disabled RTH-active stub the
31164
- * pre-session-model look, byte-for-byte. Enabled, the chips become clickable and
31165
- * the active one tracks the chart's current session.
31639
+ * market, or metadata not landed yet) HIDES the toggle entirelyRTH/ETH is
31640
+ * meaningless there. Enabled, the chips appear and the active one tracks the
31641
+ * chart's current session.
31166
31642
  */
31167
31643
  setSession(state) {
31168
- if (this.sessionEl) this.sessionEl.title = state.enabled ? "Session \u2014 regular (RTH) vs extended (ETH) hours" : "Session \u2014 stocks & ETFs only";
31644
+ if (this.sessionEl) this.sessionEl.style.display = state.enabled ? "" : "none";
31169
31645
  for (const [key, b] of this.sessionButtons) {
31170
31646
  b.disabled = !state.enabled;
31171
- b.classList.toggle("is-active", state.enabled ? key === state.session : key === "regular");
31647
+ b.classList.toggle("is-active", key === (state.enabled ? state.session : "regular"));
31172
31648
  }
31173
31649
  }
31174
31650
  destroy() {
@@ -31241,7 +31717,7 @@ function filterSymbols(list, query, limit = 100) {
31241
31717
  }
31242
31718
  return [...prefix, ...substr, ...desc, ...venue].slice(0, limit);
31243
31719
  }
31244
- var STYLE_ID10 = "vela-widget-symbolpicker";
31720
+ var STYLE_ID11 = "vela-widget-symbolpicker";
31245
31721
  var CSS8 = `
31246
31722
  .vela-sp-searchrow {
31247
31723
  display: flex;
@@ -31331,6 +31807,7 @@ var CSS8 = `
31331
31807
  .vela-sp-badge[data-p='hyperliquid'] { color: #50d2c1; } /* palette-exempt: venue brand mark */
31332
31808
  .vela-sp-empty { padding: var(--vela-space-3); color: var(--vela-fg-muted); text-align: center; }
31333
31809
  `;
31810
+ var PAGE = 100;
31334
31811
  var SymbolPicker = class {
31335
31812
  constructor(opts) {
31336
31813
  this.source = () => [];
@@ -31338,8 +31815,9 @@ var SymbolPicker = class {
31338
31815
  this.highlighted = 0;
31339
31816
  this.seed = "";
31340
31817
  this.activeTab = "All";
31818
+ this.visible = PAGE;
31341
31819
  const doc = (opts.host ?? document.body).ownerDocument;
31342
- injectStyles(STYLE_ID10, CSS8, doc);
31820
+ injectStyles(STYLE_ID11, CSS8, doc);
31343
31821
  this.input = doc.createElement("input");
31344
31822
  this.input.className = "vela-sp-input";
31345
31823
  this.input.placeholder = "Search symbol\u2026";
@@ -31364,6 +31842,12 @@ var SymbolPicker = class {
31364
31842
  }
31365
31843
  this.list = doc.createElement("div");
31366
31844
  this.list.className = "vela-sp-list";
31845
+ this.list.addEventListener("scroll", () => {
31846
+ if (this.rows.length < this.visible) return;
31847
+ if (this.list.scrollTop + this.list.clientHeight < this.list.scrollHeight - 200) return;
31848
+ this.visible += PAGE;
31849
+ this.grow();
31850
+ });
31367
31851
  this.dialog = new Dialog({
31368
31852
  title: "Symbol Search",
31369
31853
  host: opts.host,
@@ -31429,11 +31913,15 @@ var SymbolPicker = class {
31429
31913
  } else delete el.dataset.highlighted;
31430
31914
  });
31431
31915
  }
31432
- refresh() {
31433
- const doc = this.list.ownerDocument;
31916
+ computeRows() {
31434
31917
  const TAB_TYPES = { Crypto: ["crypto"], Stocks: ["stock"], ETFs: ["etf"], Forex: ["forex"], Commodities: ["commodity"] };
31435
31918
  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");
31436
- this.rows = filterSymbols(pool, this.input.value);
31919
+ return filterSymbols(pool, this.input.value, this.visible);
31920
+ }
31921
+ refresh() {
31922
+ const doc = this.list.ownerDocument;
31923
+ this.visible = PAGE;
31924
+ this.rows = this.computeRows();
31437
31925
  this.highlighted = 0;
31438
31926
  this.list.replaceChildren();
31439
31927
  if (!this.rows.length) {
@@ -31443,34 +31931,42 @@ var SymbolPicker = class {
31443
31931
  this.list.appendChild(empty);
31444
31932
  return;
31445
31933
  }
31446
- for (const s of this.rows) {
31447
- const row = doc.createElement("div");
31448
- row.className = "vela-sp-row";
31449
- row.dataset.ticker = s.ticker;
31450
- const venue = s.prefix ?? s.provider;
31451
- if (venue) row.dataset.venue = venue;
31452
- const av = tickerIconEl(doc, baseOf(s), s.ticker, "vela-sp-avatar");
31453
- const main = doc.createElement("span");
31454
- main.className = "vela-sp-main";
31455
- const t = doc.createElement("span");
31456
- t.className = "vela-sp-ticker";
31457
- t.textContent = s.ticker;
31458
- const d = doc.createElement("span");
31459
- d.className = "vela-sp-desc";
31460
- d.textContent = s.description ?? (s.type ?? "");
31461
- main.append(t, d);
31462
- row.append(av, main);
31463
- if (venue) {
31464
- const badge = doc.createElement("span");
31465
- badge.className = "vela-sp-badge";
31466
- badge.dataset.p = s.provider ?? venue;
31467
- badge.textContent = venue;
31468
- row.appendChild(badge);
31469
- }
31470
- this.list.appendChild(row);
31471
- }
31934
+ for (const s of this.rows) this.list.appendChild(this.rowEl(s));
31472
31935
  this.renderHighlight();
31473
31936
  }
31937
+ /** Append the page the grown `visible` just uncovered — rows already on screen stay put. */
31938
+ grow() {
31939
+ const already = this.rows.length;
31940
+ this.rows = this.computeRows();
31941
+ for (const s of this.rows.slice(already)) this.list.appendChild(this.rowEl(s));
31942
+ }
31943
+ rowEl(s) {
31944
+ const doc = this.list.ownerDocument;
31945
+ const row = doc.createElement("div");
31946
+ row.className = "vela-sp-row";
31947
+ row.dataset.ticker = s.ticker;
31948
+ const venue = s.prefix ?? s.provider;
31949
+ if (venue) row.dataset.venue = venue;
31950
+ const av = tickerIconEl(doc, baseOf(s), s.ticker, "vela-sp-avatar");
31951
+ const main = doc.createElement("span");
31952
+ main.className = "vela-sp-main";
31953
+ const t = doc.createElement("span");
31954
+ t.className = "vela-sp-ticker";
31955
+ t.textContent = s.ticker;
31956
+ const d = doc.createElement("span");
31957
+ d.className = "vela-sp-desc";
31958
+ d.textContent = s.description ?? (s.type ?? "");
31959
+ main.append(t, d);
31960
+ row.append(av, main);
31961
+ if (venue) {
31962
+ const badge = doc.createElement("span");
31963
+ badge.className = "vela-sp-badge";
31964
+ badge.dataset.p = s.provider ?? venue;
31965
+ badge.textContent = venue;
31966
+ row.appendChild(badge);
31967
+ }
31968
+ return row;
31969
+ }
31474
31970
  };
31475
31971
 
31476
31972
  // src/widget/object-tree-model.ts
@@ -31662,7 +32158,7 @@ function treeIsEmpty(panes) {
31662
32158
  }
31663
32159
 
31664
32160
  // src/widget/side-panel.ts
31665
- var STYLE_ID11 = "vela-widget-sidepanel";
32161
+ var STYLE_ID12 = "vela-widget-sidepanel";
31666
32162
  var DEFAULT_PANEL_WIDTH = 280;
31667
32163
  var DEFAULT_PANEL_MIN_WIDTH = 200;
31668
32164
  var DEFAULT_PANEL_MAX_WIDTH = 640;
@@ -31766,7 +32262,7 @@ var SidePanel = class {
31766
32262
  * a programmatic {@link setWidth}, so restoring a persisted width raises no change. */
31767
32263
  this.onWidthChange = null;
31768
32264
  const doc = host.ownerDocument;
31769
- injectStyles(STYLE_ID11, CSS9, doc);
32265
+ injectStyles(STYLE_ID12, CSS9, doc);
31770
32266
  this.minWidth = opts.resizable ? opts.minWidth ?? DEFAULT_PANEL_MIN_WIDTH : 1;
31771
32267
  this.maxWidth = opts.resizable ? opts.maxWidth ?? DEFAULT_PANEL_MAX_WIDTH : Number.POSITIVE_INFINITY;
31772
32268
  this.declaredWidth = clampPanelWidth(opts.width ?? DEFAULT_PANEL_WIDTH, this.minWidth, this.maxWidth);
@@ -31875,7 +32371,7 @@ var SidePanel = class {
31875
32371
  };
31876
32372
 
31877
32373
  // src/widget/object-tree.ts
31878
- var STYLE_ID12 = "vela-widget-objtree";
32374
+ var STYLE_ID13 = "vela-widget-objtree";
31879
32375
  var CSS10 = `
31880
32376
  /* One pane's block. The transparent border reserves the drop-target outline. */
31881
32377
  .vela-ot-pane {
@@ -32114,7 +32610,7 @@ var ObjectTree = class extends SidePanel {
32114
32610
  this.menu = null;
32115
32611
  this.menuActions = /* @__PURE__ */ new Map();
32116
32612
  this.drag = null;
32117
- injectStyles(STYLE_ID12, CSS10, host.ownerDocument);
32613
+ injectStyles(STYLE_ID13, CSS10, host.ownerDocument);
32118
32614
  this.body.addEventListener("contextmenu", (e) => this.onContextMenu(e));
32119
32615
  this.body.addEventListener("pointerdown", (e) => this.onPointerDown(e));
32120
32616
  }
@@ -33096,7 +33592,7 @@ var ObjectTree = class extends SidePanel {
33096
33592
  };
33097
33593
 
33098
33594
  // src/widget/data-window.ts
33099
- var STYLE_ID13 = "vela-widget-datawindow";
33595
+ var STYLE_ID14 = "vela-widget-datawindow";
33100
33596
  var CSS11 = `
33101
33597
  .vela-dw-group {
33102
33598
  padding: 10px 8px 4px;
@@ -33109,6 +33605,9 @@ var CSS11 = `
33109
33605
  letter-spacing: 0.08em;
33110
33606
  }
33111
33607
  .vela-dw-group:first-child { border-top: none; margin-top: 0; padding-top: 8px; }
33608
+ /* The readout is DATA, not chrome: selectable (an exception to the UI-wide
33609
+ user-select:none) so values can be copied out. The panel header stays chrome. */
33610
+ .vela-dw-group, .vela-dw-row { user-select: text; -webkit-user-select: text; cursor: text; }
33112
33611
  .vela-dw-row {
33113
33612
  display: flex;
33114
33613
  align-items: center;
@@ -33156,7 +33655,7 @@ var DataWindow = class extends SidePanel {
33156
33655
  super(host, "Data window", "vela-dw");
33157
33656
  this.chart = null;
33158
33657
  this.unsubs = [];
33159
- injectStyles(STYLE_ID13, CSS11, host.ownerDocument);
33658
+ injectStyles(STYLE_ID14, CSS11, host.ownerDocument);
33160
33659
  }
33161
33660
  toggle(open2 = this.el.hidden) {
33162
33661
  super.toggle(open2);
@@ -33358,7 +33857,7 @@ var PanelDock = class {
33358
33857
  };
33359
33858
 
33360
33859
  // src/widget/shortcuts-help.ts
33361
- var STYLE_ID14 = "vela-widget-shortcuts";
33860
+ var STYLE_ID15 = "vela-widget-shortcuts";
33362
33861
  var CSS12 = `
33363
33862
  .vela-sh-cat {
33364
33863
  color: var(--vela-fg-muted);
@@ -33384,7 +33883,7 @@ var ShortcutsHelp = class {
33384
33883
  constructor(keymap, host, onOpenChange) {
33385
33884
  this.keymap = keymap;
33386
33885
  const doc = (host ?? document.body).ownerDocument;
33387
- injectStyles(STYLE_ID14, CSS12, doc);
33886
+ injectStyles(STYLE_ID15, CSS12, doc);
33388
33887
  this.list = doc.createElement("div");
33389
33888
  this.dialog = new Dialog({
33390
33889
  title: "Keyboard shortcuts",
@@ -33571,6 +34070,9 @@ var ChartContextMenu = class {
33571
34070
  host,
33572
34071
  items: [],
33573
34072
  placement: "bottom-start",
34073
+ // Pointer-anchored action menu: checked state reads as a leading ✓, not a
34074
+ // washed row (which would read as hover in a menu with no trigger button).
34075
+ checkmarks: true,
33574
34076
  onSelect: (id) => this.run(id)
33575
34077
  });
33576
34078
  host.addEventListener("contextmenu", this.onContextMenu);
@@ -33672,7 +34174,7 @@ var ChartContextMenu = class {
33672
34174
  };
33673
34175
 
33674
34176
  // src/widget/indicator-picker.ts
33675
- var STYLE_ID15 = "vela-widget-indpicker";
34177
+ var STYLE_ID16 = "vela-widget-indpicker";
33676
34178
  var CSS13 = `
33677
34179
  .vela-ip-searchrow {
33678
34180
  display: flex;
@@ -33750,7 +34252,7 @@ var IndicatorPicker = class {
33750
34252
  this.isOpen = false;
33751
34253
  this.opts = opts;
33752
34254
  const doc = (opts.host ?? document.body).ownerDocument;
33753
- injectStyles(STYLE_ID15, CSS13, doc);
34255
+ injectStyles(STYLE_ID16, CSS13, doc);
33754
34256
  this.search = doc.createElement("input");
33755
34257
  this.search.className = "vela-ip-search";
33756
34258
  this.search.placeholder = "Search\u2026";
@@ -33878,7 +34380,7 @@ var IndicatorPicker = class {
33878
34380
  };
33879
34381
 
33880
34382
  // src/widget/timeframe-quick.ts
33881
- var STYLE_ID16 = "vela-widget-tfquick";
34383
+ var STYLE_ID17 = "vela-widget-tfquick";
33882
34384
  var CSS14 = `
33883
34385
  .vela-tq-input {
33884
34386
  display: block;
@@ -33902,7 +34404,7 @@ var CSS14 = `
33902
34404
  var TimeframeQuick = class {
33903
34405
  constructor(opts) {
33904
34406
  const doc = (opts.host ?? document.body).ownerDocument;
33905
- injectStyles(STYLE_ID16, CSS14, doc);
34407
+ injectStyles(STYLE_ID17, CSS14, doc);
33906
34408
  this.input = doc.createElement("input");
33907
34409
  this.input.className = "vela-tq-input";
33908
34410
  this.input.setAttribute("spellcheck", "false");
@@ -34084,7 +34586,7 @@ var LayoutModeController = class {
34084
34586
  };
34085
34587
 
34086
34588
  // src/widget/mobile-bar.ts
34087
- var STYLE_ID17 = "vela-widget-mobilebar";
34589
+ var STYLE_ID18 = "vela-widget-mobilebar";
34088
34590
  var CSS15 = `
34089
34591
  .vela-mobilebar {
34090
34592
  display: none;
@@ -34135,7 +34637,7 @@ var MobileBar = class {
34135
34637
  constructor(host, opts) {
34136
34638
  this.opts = opts;
34137
34639
  const doc = host.ownerDocument;
34138
- injectStyles(STYLE_ID17, CSS15, doc);
34640
+ injectStyles(STYLE_ID18, CSS15, doc);
34139
34641
  this.el = doc.createElement("div");
34140
34642
  this.el.className = "vela-mobilebar";
34141
34643
  const item = (cls, label, onClick, icon2) => {
@@ -34194,7 +34696,7 @@ var MobileBar = class {
34194
34696
  };
34195
34697
 
34196
34698
  // src/widget/timeframe-drawer.ts
34197
- var STYLE_ID18 = "vela-widget-tf-drawer";
34699
+ var STYLE_ID19 = "vela-widget-tf-drawer";
34198
34700
  var CSS16 = `
34199
34701
  .vela-tfd-heading {
34200
34702
  padding: 4px 2px 8px;
@@ -34251,7 +34753,7 @@ var TimeframeDrawer = class {
34251
34753
  this.rangeChips = /* @__PURE__ */ new Map();
34252
34754
  this.tfChips = /* @__PURE__ */ new Map();
34253
34755
  const doc = opts.host.ownerDocument;
34254
- injectStyles(STYLE_ID18, CSS16, doc);
34756
+ injectStyles(STYLE_ID19, CSS16, doc);
34255
34757
  this.drawer = new Drawer({ host: opts.host, onOpenChange: opts.onOpenChange });
34256
34758
  const rangeHeading = doc.createElement("div");
34257
34759
  rangeHeading.className = "vela-tfd-heading";
@@ -34309,7 +34811,7 @@ var TimeframeDrawer = class {
34309
34811
  };
34310
34812
 
34311
34813
  // src/widget/drawings-drawer.ts
34312
- var STYLE_ID19 = "vela-widget-drawings-drawer";
34814
+ var STYLE_ID20 = "vela-widget-drawings-drawer";
34313
34815
  var CSS17 = `
34314
34816
  /* Search + tabs stay pinned while the tool list scrolls underneath. */
34315
34817
  .vela-dd-sticky {
@@ -34412,7 +34914,7 @@ var DrawingsDrawer = class {
34412
34914
  this.definition = { groups: [] };
34413
34915
  this.activeGroup = "";
34414
34916
  const doc = opts.host.ownerDocument;
34415
- injectStyles(STYLE_ID19, CSS17, doc);
34917
+ injectStyles(STYLE_ID20, CSS17, doc);
34416
34918
  this.drawer = new Drawer({
34417
34919
  host: opts.host,
34418
34920
  title: "Drawings",
@@ -34554,7 +35056,7 @@ var DrawingsDrawer = class {
34554
35056
  };
34555
35057
 
34556
35058
  // src/widget/more-drawer.ts
34557
- var STYLE_ID20 = "vela-widget-more-drawer";
35059
+ var STYLE_ID21 = "vela-widget-more-drawer";
34558
35060
  var CSS18 = `
34559
35061
  .vela-md-actions {
34560
35062
  display: flex;
@@ -34630,7 +35132,7 @@ var MoreDrawer = class {
34630
35132
  constructor(opts) {
34631
35133
  this.opts = opts;
34632
35134
  this.view = "main";
34633
- injectStyles(STYLE_ID20, CSS18, opts.host.ownerDocument);
35135
+ injectStyles(STYLE_ID21, CSS18, opts.host.ownerDocument);
34634
35136
  this.drawer = new Drawer({ host: opts.host, onOpenChange: opts.onOpenChange });
34635
35137
  }
34636
35138
  open() {
@@ -34822,7 +35324,7 @@ var MoreDrawer = class {
34822
35324
  };
34823
35325
 
34824
35326
  // src/widget/timezone-drawer.ts
34825
- var STYLE_ID21 = "vela-widget-timezone-drawer";
35327
+ var STYLE_ID22 = "vela-widget-timezone-drawer";
34826
35328
  var CSS19 = `
34827
35329
  .vela-tzd-list { padding: 2px 0 4px; }
34828
35330
  .vela-tzd-row {
@@ -34842,7 +35344,7 @@ var CSS19 = `
34842
35344
  var TimezoneDrawer = class {
34843
35345
  constructor(opts) {
34844
35346
  this.opts = opts;
34845
- injectStyles(STYLE_ID21, CSS19, opts.host.ownerDocument);
35347
+ injectStyles(STYLE_ID22, CSS19, opts.host.ownerDocument);
34846
35348
  this.drawer = new Drawer({ host: opts.host, title: "Time zone", onOpenChange: opts.onOpenChange });
34847
35349
  }
34848
35350
  open() {
@@ -34880,7 +35382,7 @@ var TimezoneDrawer = class {
34880
35382
  };
34881
35383
 
34882
35384
  // src/widget/price-scale-drawer.ts
34883
- var STYLE_ID22 = "vela-widget-price-scale-drawer";
35385
+ var STYLE_ID23 = "vela-widget-price-scale-drawer";
34884
35386
  var CSS20 = `
34885
35387
  .vela-psd-list { padding: 2px 0 4px; }
34886
35388
  .vela-psd-row {
@@ -34910,7 +35412,7 @@ var PriceScaleDrawer = class {
34910
35412
  constructor(opts) {
34911
35413
  this.opts = opts;
34912
35414
  this.pane = null;
34913
- injectStyles(STYLE_ID22, CSS20, opts.host.ownerDocument);
35415
+ injectStyles(STYLE_ID23, CSS20, opts.host.ownerDocument);
34914
35416
  this.drawer = new Drawer({ host: opts.host, title: "Price scale", onOpenChange: opts.onOpenChange });
34915
35417
  }
34916
35418
  open() {
@@ -35046,7 +35548,7 @@ var PriceScaleDrawer = class {
35046
35548
  };
35047
35549
 
35048
35550
  // src/widget/drawing-pill.ts
35049
- var STYLE_ID23 = "vela-widget-drawing-pill";
35551
+ var STYLE_ID24 = "vela-widget-drawing-pill";
35050
35552
  var CSS21 = `
35051
35553
  .vela-drawpill { display: none; }
35052
35554
  [data-layout='mobile'] .vela-drawpill {
@@ -35107,7 +35609,7 @@ var DrawingPill = class {
35107
35609
  this.chart = null;
35108
35610
  this.chartSubs = [];
35109
35611
  const doc = host.ownerDocument;
35110
- injectStyles(STYLE_ID23, CSS21, doc);
35612
+ injectStyles(STYLE_ID24, CSS21, doc);
35111
35613
  this.el = doc.createElement("div");
35112
35614
  this.el.className = "vela-drawpill";
35113
35615
  this.el.hidden = true;
@@ -35495,7 +35997,7 @@ var WidgetHistory = class {
35495
35997
  };
35496
35998
 
35497
35999
  // src/widget/toast.ts
35498
- var STYLE_ID24 = "vela-widget-toast";
36000
+ var STYLE_ID25 = "vela-widget-toast";
35499
36001
  var CSS22 = `
35500
36002
  .vela-toast {
35501
36003
  position: absolute;
@@ -35526,7 +36028,7 @@ var CSS22 = `
35526
36028
  var Toast = class {
35527
36029
  constructor(host) {
35528
36030
  this.timer = null;
35529
- injectStyles(STYLE_ID24, CSS22, host.ownerDocument);
36031
+ injectStyles(STYLE_ID25, CSS22, host.ownerDocument);
35530
36032
  this.el = host.ownerDocument.createElement("div");
35531
36033
  this.el.className = "vela-toast";
35532
36034
  host.appendChild(this.el);
@@ -35609,6 +36111,11 @@ var WIDGET_CSS = `
35609
36111
  `;
35610
36112
  var VelaWidget = class {
35611
36113
  constructor(container, opts) {
36114
+ /** Keeps the pre/post-market shading on the symbol's real calendar (see {@link SessionShadingTracker}). */
36115
+ this.sessionShading = new SessionShadingTracker((zones) => this.inner?.renderer.set("sessionZones", zones));
36116
+ /** Whether the ACTIVE symbol declares real trading sessions (RTH/ETH meaningful) —
36117
+ * gates the bottombar toggle and the settings dialog's Trading session group. */
36118
+ this.sessionsAvailable = false;
35612
36119
  this.shortcutsHelp = null;
35613
36120
  this.inner = null;
35614
36121
  /** The manifest library (loaded once). */
@@ -36072,17 +36579,140 @@ var VelaWidget = class {
36072
36579
  this.markStateDirty();
36073
36580
  void this.inner?.setMarket({ session });
36074
36581
  }
36075
- /** Re-derive the RTH/ETH toggle's posture from the ACTIVE symbol's metadata: enabled
36076
- * iff the resolved symbol declares a real session vocabulary (not `24x7`). */
36582
+ /** Re-derive the session chrome from the ACTIVE symbol's metadata: the RTH/ETH
36583
+ * toggle shows (and the settings dialog's Trading session group appears) iff the
36584
+ * resolved symbol declares a real session vocabulary (not `24x7`); the pre/post
36585
+ * shading re-derives for the new market either way. */
36077
36586
  refreshSessionToggle() {
36078
36587
  const chart = this.inner;
36079
- if (!chart || !this.bottombar) return;
36588
+ if (!chart) return;
36080
36589
  void chart.data.symbolInfo(this.symbol).then((si) => {
36081
36590
  if (this.destroyed || this.inner !== chart) return;
36082
36591
  const enabled = typeof si?.session === "string" && si.session !== "" && si.session !== "24x7";
36083
36592
  this.bottombar?.setSession({ session: this.session ?? "regular", enabled });
36593
+ if (enabled !== this.sessionsAvailable) {
36594
+ this.sessionsAvailable = enabled;
36595
+ this.pushSettingsSections();
36596
+ }
36597
+ this.refreshSessionShading();
36084
36598
  });
36085
36599
  }
36600
+ /** (Re)derive the pre/post-market shading bands for the current market. The loaded
36601
+ * depth (bars × timeframe) bounds the calendar fetch; the tracker clamps it. */
36602
+ refreshSessionShading() {
36603
+ const chart = this.inner;
36604
+ if (!chart) return;
36605
+ const lookbackMs = Math.max(this.bars, this.rangeBars) * timeframeMs(this.timeframe);
36606
+ this.sessionShading.track(chart.data, this.symbol, { session: this.session ?? "regular", lookbackMs });
36607
+ }
36608
+ /** The session-shade colors live in the renderer CONFIG (persisted with it, edited
36609
+ * live by the dialog swatch) — the widget only proxies them into its settings rows. */
36610
+ sessionShadeColor(key) {
36611
+ const cfg = this.inner?.renderer.getConfig();
36612
+ const v = cfg?.sessions?.[key];
36613
+ return typeof v === "string" ? v : "";
36614
+ }
36615
+ setSessionShadeColor(key, color) {
36616
+ this.inner?.renderer.applyConfig({ sessions: { [key]: color } });
36617
+ this.markStateDirty();
36618
+ }
36619
+ /**
36620
+ * (Re)contribute the widget's settings-dialog sections: status line parts, the fetch
36621
+ * depth, the watermark toggle, and — only while the active symbol HAS sessions — the
36622
+ * Trading session group (RTH/ETH switch + the pre/post-market shading colors) inside
36623
+ * the Symbol tab. Re-run whenever a gate changes (the dialog reads them on open).
36624
+ */
36625
+ pushSettingsSections() {
36626
+ const chart = this.inner;
36627
+ if (!chart) return;
36628
+ const rth = "Regular hours (RTH)";
36629
+ const eth = "Extended hours (ETH)";
36630
+ const sessionSection = {
36631
+ title: "Trading session",
36632
+ placement: "symbol",
36633
+ rows: [
36634
+ {
36635
+ kind: "select",
36636
+ label: "Session",
36637
+ options: [rth, eth],
36638
+ get: () => (this.session ?? "regular") === "extended" ? eth : rth,
36639
+ set: (v) => this.setSession(v === eth ? "extended" : "regular")
36640
+ },
36641
+ {
36642
+ kind: "color",
36643
+ label: "Pre-market",
36644
+ get: () => this.sessionShadeColor("premarketColor"),
36645
+ set: (v) => this.setSessionShadeColor("premarketColor", v)
36646
+ },
36647
+ {
36648
+ kind: "color",
36649
+ label: "Post-market",
36650
+ get: () => this.sessionShadeColor("postmarketColor"),
36651
+ set: (v) => this.setSessionShadeColor("postmarketColor", v)
36652
+ }
36653
+ ]
36654
+ };
36655
+ const advanced = {
36656
+ title: "Advanced",
36657
+ placement: "end",
36658
+ rows: [
36659
+ {
36660
+ kind: "select",
36661
+ label: "Bars to fetch",
36662
+ options: ["500", "1000", "2000", "5000", "10000", "20000"],
36663
+ get: () => String(this.bars),
36664
+ set: (v) => {
36665
+ this.bars = Number(v);
36666
+ this.markStateDirty();
36667
+ void this.inner?.setMarket({ bars: Math.max(this.bars, this.rangeBars) });
36668
+ }
36669
+ }
36670
+ ]
36671
+ };
36672
+ const watermarkSection = {
36673
+ title: "Watermark",
36674
+ placement: "symbol",
36675
+ rows: [
36676
+ {
36677
+ kind: "toggle",
36678
+ label: "Symbol watermark",
36679
+ get: () => this.watermarkOn,
36680
+ set: (v) => this.setWatermarkVisible(v)
36681
+ }
36682
+ ]
36683
+ };
36684
+ const sections = [];
36685
+ if (this.statusline) {
36686
+ const sl = this.statusline;
36687
+ sections.push({
36688
+ title: "Status line",
36689
+ rows: [
36690
+ { kind: "heading", label: "Status line" },
36691
+ { kind: "toggle", label: "Symbol name", get: () => sl.partVisible("name"), set: (v) => sl.setPartVisible("name", v) },
36692
+ { kind: "toggle", label: "Market status", get: () => sl.partVisible("market"), set: (v) => sl.setPartVisible("market", v) },
36693
+ { kind: "toggle", label: "OHLC values", get: () => sl.partVisible("ohlc"), set: (v) => sl.setPartVisible("ohlc", v) },
36694
+ { kind: "toggle", label: "Bar change values", get: () => sl.partVisible("change"), set: (v) => sl.setPartVisible("change", v) },
36695
+ { kind: "heading", label: "Indicators" },
36696
+ {
36697
+ kind: "toggle",
36698
+ label: "Titles",
36699
+ get: () => this.indicatorTitlesOn,
36700
+ set: (v) => this.setIndicatorTitlesVisible(v)
36701
+ },
36702
+ {
36703
+ kind: "toggle",
36704
+ label: "Values",
36705
+ get: () => this.indicatorValuesOn,
36706
+ set: (v) => this.setIndicatorValuesVisible(v)
36707
+ }
36708
+ ]
36709
+ });
36710
+ }
36711
+ sections.push(advanced);
36712
+ if (this.sessionsAvailable) sections.push(sessionSection);
36713
+ sections.push(watermarkSection);
36714
+ chart.renderer.setSettingsSections(sections);
36715
+ }
36086
36716
  /** Star/unstar a timeframe — the topbar chips and dropdown stars follow, and the
36087
36717
  * set persists with the rest of the shell state. */
36088
36718
  setTimeframeFavorite(tf, on) {
@@ -36388,6 +37018,7 @@ var VelaWidget = class {
36388
37018
  this.indicatorPicker?.destroy();
36389
37019
  this.tfQuick.destroy();
36390
37020
  this.marketStatus?.stop();
37021
+ this.sessionShading.stop();
36391
37022
  this.statusline?.destroy();
36392
37023
  this.watermark?.destroy();
36393
37024
  this.bottombar?.destroy();
@@ -36526,7 +37157,10 @@ var VelaWidget = class {
36526
37157
  this.refreshSessionToggle();
36527
37158
  this.marketStatus?.track(chart.data, symbol);
36528
37159
  }
36529
- if (timeframe !== this.timeframe) this.syncTimeframeChrome(timeframe);
37160
+ if (timeframe !== this.timeframe) {
37161
+ this.syncTimeframeChrome(timeframe);
37162
+ this.refreshSessionShading();
37163
+ }
36530
37164
  });
36531
37165
  this.history.onChart(chart);
36532
37166
  chart.on("alert", (alert) => {
@@ -36575,67 +37209,7 @@ var VelaWidget = class {
36575
37209
  this.statusline?.onChart(chart);
36576
37210
  this.drawingPill.onChart(chart);
36577
37211
  this.syncStatuslineColors();
36578
- const advanced = {
36579
- title: "Advanced",
36580
- placement: "end",
36581
- rows: [
36582
- {
36583
- kind: "select",
36584
- label: "Bars to fetch",
36585
- options: ["500", "1000", "2000", "5000", "10000", "20000"],
36586
- get: () => String(this.bars),
36587
- set: (v) => {
36588
- this.bars = Number(v);
36589
- this.markStateDirty();
36590
- void this.inner?.setMarket({ bars: Math.max(this.bars, this.rangeBars) });
36591
- }
36592
- }
36593
- ]
36594
- };
36595
- const watermarkSection = {
36596
- title: "Watermark",
36597
- placement: "symbol",
36598
- rows: [
36599
- {
36600
- kind: "toggle",
36601
- label: "Symbol watermark",
36602
- get: () => this.watermarkOn,
36603
- set: (v) => this.setWatermarkVisible(v)
36604
- }
36605
- ]
36606
- };
36607
- if (this.statusline) {
36608
- const sl = this.statusline;
36609
- chart.renderer.setSettingsSections([
36610
- {
36611
- title: "Status line",
36612
- rows: [
36613
- { kind: "heading", label: "Status line" },
36614
- { kind: "toggle", label: "Symbol name", get: () => sl.partVisible("name"), set: (v) => sl.setPartVisible("name", v) },
36615
- { kind: "toggle", label: "Market status", get: () => sl.partVisible("market"), set: (v) => sl.setPartVisible("market", v) },
36616
- { kind: "toggle", label: "OHLC values", get: () => sl.partVisible("ohlc"), set: (v) => sl.setPartVisible("ohlc", v) },
36617
- { kind: "toggle", label: "Bar change values", get: () => sl.partVisible("change"), set: (v) => sl.setPartVisible("change", v) },
36618
- { kind: "heading", label: "Indicators" },
36619
- {
36620
- kind: "toggle",
36621
- label: "Titles",
36622
- get: () => this.indicatorTitlesOn,
36623
- set: (v) => this.setIndicatorTitlesVisible(v)
36624
- },
36625
- {
36626
- kind: "toggle",
36627
- label: "Values",
36628
- get: () => this.indicatorValuesOn,
36629
- set: (v) => this.setIndicatorValuesVisible(v)
36630
- }
36631
- ]
36632
- },
36633
- advanced,
36634
- watermarkSection
36635
- ]);
36636
- } else {
36637
- chart.renderer.setSettingsSections([advanced, watermarkSection]);
36638
- }
37212
+ this.pushSettingsSections();
36639
37213
  void chart.ready().then(() => {
36640
37214
  if (this.buildSeq !== seq || this.destroyed) return;
36641
37215
  if (this.pendingRange) {