@luxalgo/vela 0.5.0 → 0.5.2

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-Dzd-Erlk.d.ts → DataProvider-DTOEXKTI.d.ts} +1 -1
  2. package/dist/{DataProvider-DKNDHpNv.d.cts → DataProvider-gnzv-dgc.d.cts} +1 -1
  3. package/dist/{chunk-KM6LHB3Y.js → chunk-ATPU5CI6.js} +6 -1
  4. package/dist/{chunk-7UFX5ZIG.js → chunk-B5TIKQJ5.js} +65 -7
  5. package/dist/{chunk-GYD2THPV.js → chunk-LJLZR44Y.js} +9 -2
  6. package/dist/{chunk-RHIDOUFL.js → chunk-P556H6EA.js} +329 -10
  7. package/dist/{chunk-KCCZNKH7.js → chunk-RRFGWZNJ.js} +1915 -367
  8. package/dist/{chunk-OVQKKXLZ.js → chunk-Y7TK4ZWK.js} +1810 -153
  9. package/dist/{contributions-o1GRKPI_.d.cts → contributions-Ci_i3IN2.d.cts} +77 -31
  10. package/dist/{contributions-hX3EUyjG.d.ts → contributions-b6AVrqqG.d.ts} +77 -31
  11. package/dist/{history-LJkz4-sS.d.cts → history-Cf1lVsap.d.cts} +22 -5
  12. package/dist/{history-Dzxz-MQj.d.ts → history-Ci3M0xLV.d.ts} +22 -5
  13. package/dist/index.cjs +2010 -397
  14. package/dist/index.d.cts +83 -25
  15. package/dist/index.d.ts +83 -25
  16. package/dist/index.js +4 -4
  17. package/dist/{options-Q-576hIi.d.cts → options-XTBpbx0s.d.cts} +61 -1
  18. package/dist/{options-Q-576hIi.d.ts → options-XTBpbx0s.d.ts} +61 -1
  19. package/dist/{plugin-aGUD1epn.d.ts → plugin-CnNd7cuF.d.ts} +305 -8
  20. package/dist/{plugin-D94muTV-.d.cts → plugin-DfH4Y-Om.d.cts} +305 -8
  21. package/dist/plugin.cjs +58 -7
  22. package/dist/plugin.d.cts +4 -4
  23. package/dist/plugin.d.ts +4 -4
  24. package/dist/plugin.js +2 -2
  25. package/dist/providers/binance.d.cts +2 -2
  26. package/dist/providers/binance.d.ts +2 -2
  27. package/dist/providers/coinbase.d.cts +2 -2
  28. package/dist/providers/coinbase.d.ts +2 -2
  29. package/dist/providers/hyperliquid.d.cts +2 -2
  30. package/dist/providers/hyperliquid.d.ts +2 -2
  31. package/dist/ui.cjs +342 -9
  32. package/dist/ui.d.cts +64 -2
  33. package/dist/ui.d.ts +64 -2
  34. package/dist/ui.js +3 -3
  35. package/dist/vela.global.js +2010 -397
  36. package/dist/vela.global.min.js +115 -34
  37. package/dist/widget.cjs +5987 -2242
  38. package/dist/widget.d.cts +89 -17
  39. package/dist/widget.d.ts +89 -17
  40. package/dist/widget.js +169 -18
  41. package/dist/workspace.cjs +5627 -1587
  42. package/dist/workspace.d.cts +106 -12
  43. package/dist/workspace.d.ts +106 -12
  44. package/dist/workspace.js +494 -52
  45. package/package.json +1 -1
package/dist/index.cjs CHANGED
@@ -481,14 +481,15 @@ registerIcon(
481
481
  svg24('<path d="m10 20-1.25-2.5L6 16"/><path d="M10 4 8.75 6.5 6 8"/><path d="m14 20 1.25-2.5L18 16"/><path d="m14 4 1.25 2.5L18 8"/><path d="m17 10-5 5-5-5"/><path d="M4 12h16"/>')
482
482
  );
483
483
  registerIcon("chevron-right", S('<path d="m6 3.5 4.5 4.5L6 12.5"/>'));
484
+ registerIcon("chevron-left", S('<path d="M10 3.5 5.5 8l4.5 4.5"/>'));
484
485
  registerIcon("chevron-down", S('<path d="M3.5 6 8 10.5 12.5 6"/>'));
485
486
  registerIcon("chevron-up", S('<path d="M3.5 10 8 5.5 12.5 10"/>'));
486
487
  registerIcon("chevrons-right", S('<path d="m4 3.5 4.5 4.5L4 12.5"/><path d="m8.5 3.5 4.5 4.5-4.5 4.5"/>'));
487
488
  registerIcon("chevrons-left", S('<path d="M12 3.5 7.5 8l4.5 4.5"/><path d="M7.5 3.5 3 8l4.5 4.5"/>'));
488
489
  registerIcon("check", S('<path d="m2.8 8.4 3.4 3.4 7-7.6"/>'));
489
490
  registerIcon("close", S('<path d="m3.8 3.8 8.4 8.4M12.2 3.8l-8.4 8.4"/>'));
490
- registerIcon("eye", S('<path d="M1.5 8s2.5-4.2 6.5-4.2S14.5 8 14.5 8 12 12.2 8 12.2 1.5 8 1.5 8z"/><circle cx="8" cy="8" r="1.8"/>'));
491
- registerIcon("eye-off", S('<path d="M1.5 8s2.5-4.2 6.5-4.2S14.5 8 14.5 8 12 12.2 8 12.2 1.5 8 1.5 8z" opacity="0.45"/><path d="m3 13 10-10"/>'));
491
+ registerIcon("eye", S('<path d="M1.5 8s2.5-5.4 6.5-5.4S14.5 8 14.5 8 12 13.4 8 13.4 1.5 8 1.5 8z"/><circle cx="8" cy="8" r="1.8"/>'));
492
+ registerIcon("eye-off", S('<path d="M1.5 8s2.5-5.4 6.5-5.4S14.5 8 14.5 8 12 13.4 8 13.4 1.5 8 1.5 8z" opacity="0.45"/><path d="m3 13 10-10"/>'));
492
493
  registerIcon("lock", S('<rect x="3.2" y="7" width="9.6" height="6.6" rx="1.2"/><path d="M5.6 7V5.2a2.4 2.4 0 0 1 4.8 0V7"/>'));
493
494
  registerIcon("unlock", S('<rect x="3.2" y="7" width="9.6" height="6.6" rx="1.2"/><path d="M5.6 7V5.2a2.4 2.4 0 0 1 4.7-.7"/>'));
494
495
  registerIcon("trash", S('<path d="M2.5 4.3h11M6.2 4.3V3.1a1 1 0 0 1 1-1h1.6a1 1 0 0 1 1 1v1.2M4.2 4.3l.5 9.1a1.15 1.15 0 0 0 1.15 1.1h4.3a1.15 1.15 0 0 0 1.15-1.1l.5-9.1M6.5 7v4.5M9.5 7v4.5"/>'));
@@ -511,6 +512,7 @@ registerIcon("star", S('<path d="M8 2.2l1.75 3.55 3.9.55-2.8 2.75.65 3.9L8 11.1l
511
512
  registerIcon("star-filled", S('<path d="M8 2.2l1.75 3.55 3.9.55-2.8 2.75.65 3.9L8 11.1l-3.5 1.85.65-3.9-2.8-2.75 3.9-.55z"/>', 'fill="currentColor"'));
512
513
  registerIcon("grip", S('<circle cx="6" cy="3.5" r="1"/><circle cx="10" cy="3.5" r="1"/><circle cx="6" cy="8" r="1"/><circle cx="10" cy="8" r="1"/><circle cx="6" cy="12.5" r="1"/><circle cx="10" cy="12.5" r="1"/>', 'fill="currentColor" stroke="none"'));
513
514
  registerIcon("kebab", S('<circle cx="8" cy="3.2" r="1.2"/><circle cx="8" cy="8" r="1.2"/><circle cx="8" cy="12.8" r="1.2"/>', 'fill="currentColor" stroke="none"'));
515
+ registerIcon("burger", S('<path d="M2.5 4.5h11M2.5 8h11M2.5 11.5h11"/>'));
514
516
  registerIcon("reset", S('<rect x="6" y="6" width="4" height="4" rx="0.8"/><path d="M2.2 8a5.8 5.8 0 1 0 1.9-4.3L2.2 5.3"/><path d="M2.2 2.2v3.1h3.1"/>'));
515
517
  registerIcon("pane-collapse", S('<path d="M2.6 9.4h4v4M13.4 6.6h-4v-4"/><path d="m9.4 6.6 4.2-4.2M2.4 13.6l4.2-4.2"/>'));
516
518
  registerIcon("pane-expand", S('<path d="M9.8 2.4h3.8v3.8M6.2 13.6H2.4V9.8"/><path d="m13.6 2.4-4.4 4.4M2.4 13.6l4.4-4.4"/>'));
@@ -531,6 +533,11 @@ registerIcon(
531
533
  "pen-lock",
532
534
  svg24('<path d="M13.8 4.2a2.1 2.1 0 0 1 3 3L8.5 15.5l-3.5 1 1-3.5Z"/><rect x="13" y="14.5" width="8" height="6" rx="1.2"/><path d="M15 14.5v-1.6a2 2 0 0 1 4 0v1.6"/>')
533
535
  );
536
+ registerIcon(
537
+ "pen-sync",
538
+ // Pen + two stacked panes — drawing onto every linked chart at once.
539
+ svg24('<path d="M13.8 4.2a2.1 2.1 0 0 1 3 3L8.5 15.5l-3.5 1 1-3.5Z"/><rect x="12.5" y="13.5" width="6" height="4.8" rx="1"/><path d="M15.5 18.3v1a1 1 0 0 0 1 1h4a1 1 0 0 0 1-1v-2.8a1 1 0 0 0-1-1h-1"/>')
540
+ );
534
541
  registerIcon("brush", svg24('<path d="M9.5 12 17 4.5a2.12 2.12 0 0 1 3 3L12.5 15"/><path d="M7 14a3 3 0 0 0-3 3c0 1.3-1.2 1.5-1.5 2 .8.9 2 1.5 3.5 1.5a3.5 3.5 0 0 0 3.5-3.5 3 3 0 0 0-2.5-3Z"/>'));
535
542
  registerIcon(
536
543
  "bucket",
@@ -1884,10 +1891,10 @@ function polylineHit(px, py, pts, tol) {
1884
1891
  }
1885
1892
 
1886
1893
  // src/core/drawings/types/FixedRangeVolumeProfile.ts
1887
- var OUTSIDE_UP = `${BULLISH}BF`;
1888
- var OUTSIDE_DOWN = `${BEARISH}BF`;
1889
- var VA_UP = `${BULLISH}66`;
1890
- var VA_DOWN = `${BEARISH}66`;
1894
+ var OUTSIDE_UP = `${BULLISH}66`;
1895
+ var OUTSIDE_DOWN = `${BEARISH}66`;
1896
+ var VA_UP = `${BULLISH}BF`;
1897
+ var VA_DOWN = `${BEARISH}BF`;
1891
1898
  function defaultFrvpStyle() {
1892
1899
  return {
1893
1900
  rows: 24,
@@ -1905,7 +1912,8 @@ function defaultFrvpStyle() {
1905
1912
  valColor: NEUTRAL,
1906
1913
  valStyle: "solid",
1907
1914
  showPoc: true,
1908
- pocColor: ACCENT,
1915
+ pocColor: void 0,
1916
+ // theme contrast ink until the user picks a color
1909
1917
  pocStyle: "solid",
1910
1918
  showDevelopingPoc: false,
1911
1919
  developingPocColor: ACCENT,
@@ -6534,6 +6542,11 @@ var DrawingController = class {
6534
6542
  setToolbar(option) {
6535
6543
  this.port?.setToolbar(buildToolbar(option).definition);
6536
6544
  }
6545
+ /** Display (or clear) another chart's in-progress placement as a ghost — silently
6546
+ * inert on a renderer without the optional `setExternalGhost` seam. */
6547
+ setExternalGhost(doc) {
6548
+ this.port?.setExternalGhost?.(doc);
6549
+ }
6537
6550
  /** Push per-tool shortcut hints (pre-formatted display strings) to the toolbar flyouts. */
6538
6551
  setToolShortcuts(map) {
6539
6552
  this.port?.setToolShortcuts?.(map);
@@ -6764,6 +6777,9 @@ var DrawingController = class {
6764
6777
  case "arm":
6765
6778
  this.setTool(i.type);
6766
6779
  break;
6780
+ case "draft":
6781
+ this.events.emit("drawing:draft", { doc: i.doc });
6782
+ break;
6767
6783
  case "create": {
6768
6784
  const before = this.store.serialize();
6769
6785
  const last = this.lastStyle.get(i.doc.type);
@@ -7039,6 +7055,52 @@ var ProviderRegistry = class {
7039
7055
  };
7040
7056
 
7041
7057
  // src/chart-types/registry.ts
7058
+ function normalizeSettingsRow(r) {
7059
+ switch (r.kind) {
7060
+ case "row":
7061
+ return { label: r.label, toggle: r.toggle, controls: r.controls, when: r.when };
7062
+ case "toggle": {
7063
+ const controls = [];
7064
+ if (r.number) controls.push({ kind: "number", ...r.number });
7065
+ for (const c of r.colors ?? []) controls.push({ kind: "color", ...c });
7066
+ if (r.width) controls.push({ kind: "width", ...r.width });
7067
+ return { label: r.label, toggle: { key: r.key, defval: r.defval }, controls, when: r.when };
7068
+ }
7069
+ case "number":
7070
+ return { label: r.label, controls: [{ kind: "number", key: r.key, label: r.label, defval: r.defval, min: r.min, max: r.max, step: r.step }], when: r.when };
7071
+ case "color":
7072
+ return { label: r.label, controls: [{ kind: "color", key: r.key, label: r.label, defval: r.defval }], when: r.when };
7073
+ case "select":
7074
+ return { label: r.label, controls: [{ kind: "select", key: r.key, label: r.label, options: r.options, defval: r.defval }], when: r.when };
7075
+ case "range":
7076
+ return {
7077
+ label: r.label,
7078
+ controls: [
7079
+ { kind: "number", key: r.minKey, label: `${r.label} \u2014 min`, defval: r.defval, min: r.min, max: r.max, step: r.step, placeholder: r.placeholder },
7080
+ { kind: "hint", text: "\u2013" },
7081
+ { kind: "number", key: r.maxKey, label: `${r.label} \u2014 max`, defval: r.defval, min: r.min, max: r.max, step: r.step, placeholder: r.placeholder }
7082
+ ],
7083
+ when: r.when
7084
+ };
7085
+ }
7086
+ }
7087
+ function settingsRowValueKeys(r) {
7088
+ if (r.kind === "heading" || r.kind === "header") return [];
7089
+ const n = normalizeSettingsRow(r);
7090
+ const keys = [];
7091
+ if (n.toggle) keys.push({ key: n.toggle.key, type: "boolean", defval: n.toggle.defval });
7092
+ for (const c of n.controls) {
7093
+ if (c.kind === "hint") continue;
7094
+ if (c.kind === "color" || c.kind === "select") keys.push({ key: c.key, type: "string", defval: c.defval });
7095
+ else keys.push({ key: c.key, type: "number", defval: c.defval });
7096
+ }
7097
+ return keys;
7098
+ }
7099
+ function settingsRowVisible(when, bag) {
7100
+ if (!when) return true;
7101
+ const conds = Array.isArray(when) ? when : [when];
7102
+ return conds.every((c) => c.anyOf ? c.anyOf.some((x) => x === bag[c.key]) : bag[c.key] === c.equals);
7103
+ }
7042
7104
  var registry2 = /* @__PURE__ */ new Map();
7043
7105
  function registerChartType(def) {
7044
7106
  registry2.set(def.id, def);
@@ -7138,6 +7200,9 @@ var EngineOrchestrator = class _EngineOrchestrator {
7138
7200
  this.barTransform = null;
7139
7201
  // ── chart-type data engines (SDK): one lazily-created engine per style id ──
7140
7202
  this.typeEngines = /* @__PURE__ */ new Map();
7203
+ /** Last-seen chart-type settings per type id (dialog edits + persisted restores) —
7204
+ * replayed into a data engine whenever one is (re)created. */
7205
+ this.typeSettings = /* @__PURE__ */ new Map();
7141
7206
  /** The style id whose data engine is currently ACTIVE (drives suspend/resume + pokes). */
7142
7207
  this.activeEngineStyle = null;
7143
7208
  /** The chart's current price style (tracked from the renderer's change events + initial read). */
@@ -7200,7 +7265,10 @@ var EngineOrchestrator = class _EngineOrchestrator {
7200
7265
  this.renderer.onRemoveIndicator((id) => this.removeIndicator(id));
7201
7266
  this.renderer.onToggleIndicatorVisible?.((id, visible) => this.setVisible(id, visible));
7202
7267
  this.paneActionUnsub = this.renderer.onPaneAction?.((a) => this.handlePaneAction(a)) ?? null;
7203
- this.renderer.onChartTypeSettingsChange?.((typeId, values) => this.typeEngines.get(typeId)?.onSettings?.(values));
7268
+ this.renderer.onChartTypeSettingsChange?.((typeId, values) => {
7269
+ this.typeSettings.set(typeId, values);
7270
+ this.typeEngines.get(typeId)?.onSettings?.(values);
7271
+ });
7204
7272
  this.renderer.onMoveIndicator?.((id, target) => this.moveIndicator(id, target));
7205
7273
  this.renderer.onThemeSelect?.((name) => this.setTheme(resolveTheme(name)));
7206
7274
  this.viewportUnsub = this.renderer.onViewportChange((range) => this.onViewportChange(range));
@@ -7688,6 +7756,8 @@ var EngineOrchestrator = class _EngineOrchestrator {
7688
7756
  if (!def?.dataEngine) return;
7689
7757
  const engine = def.dataEngine();
7690
7758
  this.typeEngines.set(id, engine);
7759
+ const stored = this.typeSettings.get(id);
7760
+ if (stored) engine.onSettings?.(stored);
7691
7761
  engine.start({
7692
7762
  symbol: this.config.market.symbol ?? "TEST",
7693
7763
  timeframe: this.config.market.timeframe ?? "60",
@@ -8159,6 +8229,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
8159
8229
  return {
8160
8230
  id: record.id,
8161
8231
  title: record.title,
8232
+ ...d.shortTitle ? { shorttitle: d.shortTitle } : {},
8162
8233
  overlay: d.overlay,
8163
8234
  paneHint: d.paneHint,
8164
8235
  native: { type: record.native.type },
@@ -8619,6 +8690,28 @@ var RendererControl = class {
8619
8690
  setLegendActions(provider) {
8620
8691
  this.renderer.setLegendActions?.(provider);
8621
8692
  }
8693
+ /**
8694
+ * Replace the indicator legend's fold toggle with a host action (or restore it with
8695
+ * `null`) — multi-chart shells point the chip at their indicator overview instead of
8696
+ * unfolding rows in place. Silent no-op on a renderer without a foldable legend.
8697
+ */
8698
+ setLegendOverviewAction(action) {
8699
+ this.renderer.setLegendOverviewAction?.(action);
8700
+ return this;
8701
+ }
8702
+ /** Whether the active renderer can open a per-indicator settings dialog. */
8703
+ get supportsIndicatorSettings() {
8704
+ return typeof this.renderer.openIndicatorSettings === "function";
8705
+ }
8706
+ /**
8707
+ * Open one indicator's settings dialog — the programmatic twin of the legend gear
8708
+ * (see {@link supportsIndicatorSettings}). Silent no-op without the seam or for an
8709
+ * unknown id.
8710
+ */
8711
+ openIndicatorSettings(indicatorId) {
8712
+ this.renderer.openIndicatorSettings?.(indicatorId);
8713
+ return this;
8714
+ }
8622
8715
  /**
8623
8716
  * Export the current chart as a PNG data URL, or null if the active renderer
8624
8717
  * doesn't support it (warns). DOM overlays (tables, legend) are not included.
@@ -8665,6 +8758,10 @@ var RendererControl = class {
8665
8758
  onCrosshairMove(cb) {
8666
8759
  return this.renderer.onCrosshairMove(cb);
8667
8760
  }
8761
+ /** Touch long-press on a price or time axis strip — silent no-op without the seam. */
8762
+ onAxisLongPress(cb) {
8763
+ return this.renderer.onAxisLongPress?.(cb) ?? (() => void 0);
8764
+ }
8668
8765
  /**
8669
8766
  * The current data-window readout — the hovered bar's date/time and OHLCV plus every
8670
8767
  * indicator's value there, each already formatted on its pane's scale (the latest bar when
@@ -8730,6 +8827,13 @@ var RendererControl = class {
8730
8827
  this.renderer.setSettingsSections?.(sections);
8731
8828
  return this;
8732
8829
  }
8830
+ /** Tell the renderer's own chrome which size class the host shell is in —
8831
+ * `'mobile'` switches its dialogs/toolbars to the touch-first presentation.
8832
+ * Silent no-op on a renderer without adaptive chrome. */
8833
+ setLayoutMode(mode) {
8834
+ this.renderer.setLayoutMode?.(mode);
8835
+ return this;
8836
+ }
8733
8837
  };
8734
8838
 
8735
8839
  // src/core/renderer-defaults.ts
@@ -9383,6 +9487,14 @@ var DrawingsControl = class {
9383
9487
  if (this.ok("setToolShortcuts")) this.ctrl.setToolShortcuts(map);
9384
9488
  return this;
9385
9489
  }
9490
+ /** Display another chart's in-progress placement as a GHOST at reduced opacity
9491
+ * (`null` clears it) — how a multi-chart host mirrors `drawing:draft` onto linked
9492
+ * charts. Never a real drawing: no store entry, no selection, no persistence.
9493
+ * Silently inert on a renderer without the optional seam. */
9494
+ setExternalGhost(doc) {
9495
+ this.ctrl.setExternalGhost(doc);
9496
+ return this;
9497
+ }
9386
9498
  /** Create a drawing programmatically (no clicking). Returns it, or null if unsupported. */
9387
9499
  add(type, init = {}) {
9388
9500
  if (!this.ok("add")) return null;
@@ -9621,6 +9733,11 @@ function themeTokens(t) {
9621
9733
  "--vela-hover-strong": wash(0.16),
9622
9734
  "--vela-focus": withAlpha(t.textColor, 0.5),
9623
9735
  "--vela-focus-soft": withAlpha(t.textColor, 0.12),
9736
+ // Separator hover — the SAME wash the chart's pane separators paint on hover
9737
+ // (soft full-thickness band + solid 2px center line), so DOM-drawn dividers
9738
+ // (the workspace grid) and canvas-drawn ones read as one family.
9739
+ "--vela-separator-hover-band": withAlpha(t.textColor, 0.1),
9740
+ "--vela-separator-hover-line": withAlpha(t.textColor, 0.55),
9624
9741
  "--vela-scroll": withAlpha(t.textColor, 0.3),
9625
9742
  "--vela-accent": ACCENT,
9626
9743
  "--vela-accent-bright": ACCENT_BRIGHT,
@@ -9668,7 +9785,7 @@ function attachChromeTooltip(anchor, opts) {
9668
9785
  const doc = anchor.ownerDocument;
9669
9786
  tip = doc.createElement("div");
9670
9787
  tip.textContent = text;
9671
- tip.style.cssText = "position:absolute;z-index:25;pointer-events:none;background:var(--vela-bg);border:1px solid var(--vela-border);color:var(--vela-fg);border-radius:var(--vela-radius-md);padding:4px 9px;box-shadow:var(--vela-shadow);font:var(--vela-font-size-md) var(--vela-font);" + (opts.wrap ? "max-width:260px;" : "white-space:nowrap;");
9788
+ tip.style.cssText = "position:absolute;z-index:var(--vela-z-tooltip);pointer-events:none;background:var(--vela-bg);border:1px solid var(--vela-border);color:var(--vela-fg);border-radius:var(--vela-radius-md);padding:4px 9px;box-shadow:var(--vela-shadow);font:var(--vela-font-size-md) var(--vela-font);" + (opts.wrap ? "max-width:260px;" : "white-space:nowrap;");
9672
9789
  applyChromeTokens(tip, opts.theme());
9673
9790
  opts.host.appendChild(tip);
9674
9791
  const a = anchor.getBoundingClientRect();
@@ -9682,16 +9799,17 @@ function attachChromeTooltip(anchor, opts) {
9682
9799
  tip.style.top = `${a.bottom - h.top + 6}px`;
9683
9800
  }
9684
9801
  };
9685
- const arm = () => {
9802
+ const arm = (e) => {
9803
+ if (e.pointerType !== "mouse") return;
9686
9804
  clear();
9687
9805
  timer = window.setTimeout(show, opts.delayMs ?? 700);
9688
9806
  };
9689
- anchor.addEventListener("mouseenter", arm);
9690
- anchor.addEventListener("mouseleave", clear);
9807
+ anchor.addEventListener("pointerenter", arm);
9808
+ anchor.addEventListener("pointerleave", clear);
9691
9809
  anchor.addEventListener("pointerdown", clear);
9692
9810
  return () => {
9693
- anchor.removeEventListener("mouseenter", arm);
9694
- anchor.removeEventListener("mouseleave", clear);
9811
+ anchor.removeEventListener("pointerenter", arm);
9812
+ anchor.removeEventListener("pointerleave", clear);
9695
9813
  anchor.removeEventListener("pointerdown", clear);
9696
9814
  clear();
9697
9815
  };
@@ -9758,14 +9876,22 @@ function makeDialogDraggable(box, header, options) {
9758
9876
 
9759
9877
  // src/renderers/shared/InputsUI.ts
9760
9878
  var SOURCES = ["close", "open", "high", "low", "hl2", "hlc3", "ohlc4", "volume"];
9761
- var STATUS_KEYFRAMES = "@keyframes vela-ind-spin{to{transform:rotate(360deg)}}@keyframes vela-ind-pulse{0%,100%{opacity:1;transform:scale(1)}50%{opacity:.3;transform:scale(.6)}}";
9762
- var LEGEND_ICON_PX = 13;
9879
+ var STATUS_KEYFRAMES = "@keyframes vela-ind-pulse{0%,100%{opacity:1;transform:scale(1)}50%{opacity:.3;transform:scale(.6)}}";
9880
+ var LEGEND_ICON_PX = 16;
9881
+ var LEGEND_CTL_PX = 18;
9882
+ var LEGEND_ROW_PAD_Y = 2;
9883
+ var LEGEND_ROW_PAD_X = 6;
9884
+ var LEGEND_TITLE_VALUES_GAP_PX = 6;
9885
+ var LEGEND_ACTION_GAP_PX = 6;
9886
+ var LEGEND_CTL_CSS = `cursor:pointer;display:inline-flex;align-items:center;justify-content:center;border:none;line-height:0;padding:0;width:${LEGEND_CTL_PX}px;height:${LEGEND_CTL_PX}px;border-radius:3px;box-sizing:border-box;flex:none;`;
9887
+ var LEGEND_ROW_MIN_H = LEGEND_CTL_PX + LEGEND_ROW_PAD_Y * 2;
9763
9888
  var EYE_SVG = iconAt("eye", LEGEND_ICON_PX);
9764
9889
  var EYE_OFF_SVG = iconAt("eye-off", LEGEND_ICON_PX);
9765
9890
  var GEAR_SVG = iconAt("gear", LEGEND_ICON_PX);
9766
- var CLOSE_SVG = iconAt("close", 11);
9891
+ var CLOSE_SVG = iconAt("close", LEGEND_ICON_PX);
9767
9892
  var FOLD_SVG = iconAt("chevron-up", LEGEND_ICON_PX);
9768
9893
  var UNFOLD_SVG = iconAt("chevron-down", LEGEND_ICON_PX);
9894
+ var OVERVIEW_SVG = iconAt("objects", LEGEND_ICON_PX);
9769
9895
  var InputsUI = class {
9770
9896
  constructor(container, theme, paneBoundsOf) {
9771
9897
  this.container = container;
@@ -9786,6 +9912,8 @@ var InputsUI = class {
9786
9912
  this.onToggleVisible = null;
9787
9913
  /** Host symbol picker for `input.symbol`; when set the control opens the host's ticker UI. */
9788
9914
  this.symbolPicker = null;
9915
+ /** Mobile chrome: the inputs dialog opens fullscreen and the legends render compact. */
9916
+ this.mobileLayout = false;
9789
9917
  /** Pane move/merge hook — when set, rows get a "Move to" menu + become drag-to-pane sources. */
9790
9918
  this.moveApi = null;
9791
9919
  /** Host-contributed legend actions, resolved PER ROW at render time (see setLegendActions). */
@@ -9807,9 +9935,18 @@ var InputsUI = class {
9807
9935
  /** Titles switched off entirely (a settings toggle): every pane's legend container
9808
9936
  * hides — rows, fold chevron and all — unlike the fold, which leaves its chip. */
9809
9937
  this.titlesVisible = true;
9938
+ /** Plot values shown beside the titles chart-wide (a settings toggle); a row's own
9939
+ * context-menu choice ({@link LegendRow.showValues}) overrides it per indicator. */
9940
+ this.valuesVisible = true;
9941
+ /** Open legend-row context menu (kept so it can be torn down). */
9942
+ this.rowMenu = null;
9810
9943
  /** The single fold toggle, on the price-pane legend: a bordered ^ chevron under the
9811
9944
  * rows, or the bordered "˅ N" chip (N counts every pane's indicators) when folded. */
9812
9945
  this.foldToggle = null;
9946
+ /** Host-provided replacement for the fold toggle (multi-chart shells route it to
9947
+ * their indicator overview, e.g. an object tree). While set, the legend stays
9948
+ * folded and the chip runs this instead of unfolding inline. */
9949
+ this.overviewAction = null;
9813
9950
  /** Esc-to-close for the settings modal (bound so focus can sit anywhere). */
9814
9951
  this.onDialogKey = (e) => {
9815
9952
  if (e.key === "Escape") {
@@ -9833,6 +9970,9 @@ var InputsUI = class {
9833
9970
  this.onMoveMenuOutside = (e) => {
9834
9971
  if (this.moveMenu && !this.moveMenu.contains(e.target)) this.closeMoveMenu();
9835
9972
  };
9973
+ this.onRowMenuOutside = (e) => {
9974
+ if (this.rowMenu && !this.rowMenu.contains(e.target)) this.closeRowMenu();
9975
+ };
9836
9976
  if (!container.style.position) container.style.position = "relative";
9837
9977
  if (typeof document !== "undefined") document.addEventListener("click", this.onDocClick);
9838
9978
  }
@@ -9840,6 +9980,36 @@ var InputsUI = class {
9840
9980
  this.dialogHost = host;
9841
9981
  if (host && getComputedStyle(host).position === "static") host.style.position = "relative";
9842
9982
  }
9983
+ /** The host shell's chrome size class — mobile makes the inputs dialog fullscreen
9984
+ * (an open dialog re-presents on the flip) and folds the legend behind its chip by
9985
+ * default (a phone-width plot has no room for the rows). */
9986
+ setLayoutMode(mode) {
9987
+ const mobile = mode === "mobile";
9988
+ if (mobile === this.mobileLayout) return;
9989
+ this.mobileLayout = mobile;
9990
+ if (this.overviewAction === null) this.legendFolded = mobile;
9991
+ this.syncFoldToggle();
9992
+ if (this.openId !== null) {
9993
+ const id = this.openId;
9994
+ this.closeOpenDialog();
9995
+ this.openDialog(id);
9996
+ }
9997
+ }
9998
+ /** Replace the fold toggle's behavior with a HOST action (or restore it with null):
9999
+ * the chip stays — objects icon + indicator count — but a press runs the action
10000
+ * (a multi-chart shell opens its indicator overview) instead of unfolding the rows,
10001
+ * which stay hidden while the override is in force. */
10002
+ setLegendOverviewAction(action) {
10003
+ if (action === this.overviewAction) return;
10004
+ this.overviewAction = action;
10005
+ this.legendFolded = action !== null ? true : this.mobileLayout;
10006
+ this.syncFoldToggle();
10007
+ }
10008
+ /** Open the settings dialog of one indicator (the legend gear's programmatic twin) —
10009
+ * no-op for an unknown id. */
10010
+ openSettingsFor(id) {
10011
+ if (this.rows.has(id)) this.openDialog(id);
10012
+ }
9843
10013
  /** Attach a themed chrome tooltip to a control, recording its disposer under `id`. */
9844
10014
  tip(store, id, anchor, text, wrap = false) {
9845
10015
  const list = store.get(id) ?? [];
@@ -9867,12 +10037,12 @@ var InputsUI = class {
9867
10037
  }
9868
10038
  setTheme(theme) {
9869
10039
  this.theme = theme;
10040
+ for (const lg of this.legends.values()) applyChromeTokens(lg, theme);
9870
10041
  for (const row of this.rows.values()) {
9871
- const open = row.controlsEl.style.display !== "none";
9872
- row.el.style.background = open ? theme.background : this.idleRowFill();
10042
+ row.el.style.background = row.highlighted ? theme.background : this.idleRowFill();
9873
10043
  row.el.style.color = theme.textColor;
9874
10044
  }
9875
- if (this.foldToggle) this.foldToggle.style.background = theme.background;
10045
+ if (this.foldToggle) this.syncFoldToggle();
9876
10046
  }
9877
10047
  /** Provide (or clear) the host symbol picker that `input.symbol` opens on activation. */
9878
10048
  setSymbolPicker(picker) {
@@ -9903,7 +10073,7 @@ var InputsUI = class {
9903
10073
  btn.innerHTML = iconAt(action.icon, LEGEND_ICON_PX);
9904
10074
  btn.className = "vela-ind-ctl";
9905
10075
  btn.dataset.legendAction = action.id;
9906
- btn.style.cssText = "cursor:pointer;display:inline-flex;align-items:center;background:transparent;border:none;line-height:0;padding:0 1px;";
10076
+ btn.style.cssText = LEGEND_CTL_CSS;
9907
10077
  btn.addEventListener("click", (e) => {
9908
10078
  e.stopPropagation();
9909
10079
  action.run();
@@ -9921,6 +10091,54 @@ var InputsUI = class {
9921
10091
  this.titlesVisible = visible;
9922
10092
  this.reposition();
9923
10093
  }
10094
+ /**
10095
+ * Show/hide the plot values chart-wide (the settings dialog's Indicators → Values
10096
+ * toggle). "All" is meant literally: per-row context-menu overrides are cleared, so
10097
+ * every legend follows the new state.
10098
+ */
10099
+ setValuesVisible(visible) {
10100
+ this.valuesVisible = visible;
10101
+ for (const row of this.rows.values()) {
10102
+ row.showValues = null;
10103
+ this.renderRowValues(row);
10104
+ }
10105
+ }
10106
+ /**
10107
+ * Push the current plot values of every indicator at once (the renderer calls this per
10108
+ * paint). Rows absent from the map (e.g. a hidden indicator) show no values. Cheap on
10109
+ * unchanged frames: each row diffs against its last rendered values before touching DOM.
10110
+ */
10111
+ setPlotValues(values) {
10112
+ for (const row of this.rows.values()) {
10113
+ const next = values.get(row.id) ?? [];
10114
+ const key = next.map((v) => `${v.value}|${v.color}`).join("\0");
10115
+ if (key === row.plotValuesKey) continue;
10116
+ row.plotValues = next;
10117
+ row.plotValuesKey = key;
10118
+ this.renderRowValues(row);
10119
+ }
10120
+ }
10121
+ /** Whether a row currently shows its values: its own override, else the chart-wide flag. */
10122
+ rowValuesShown(row) {
10123
+ return row.showValues ?? this.valuesVisible;
10124
+ }
10125
+ /** (Re)build one row's values readout from its stored plot values + visibility. A
10126
+ * highlighted (hovered/selected) row shows only the title + controls — no values. */
10127
+ renderRowValues(row) {
10128
+ if (!this.rowValuesShown(row) || row.plotValues.length === 0 || row.highlighted) {
10129
+ row.valuesEl.style.display = "none";
10130
+ row.valuesEl.replaceChildren();
10131
+ return;
10132
+ }
10133
+ row.valuesEl.style.display = "inline-flex";
10134
+ row.valuesEl.replaceChildren();
10135
+ for (const v of row.plotValues) {
10136
+ const span = document.createElement("span");
10137
+ span.textContent = v.value;
10138
+ span.style.color = v.color;
10139
+ row.valuesEl.appendChild(span);
10140
+ }
10141
+ }
9924
10142
  // ── legend move/merge (menu + drag) ─────────────────────────────────────
9925
10143
  /** Open the "Move to" menu for a row, anchored under its move button. */
9926
10144
  openMoveMenu(id, anchor) {
@@ -9972,6 +10190,48 @@ var InputsUI = class {
9972
10190
  this.moveMenu?.remove();
9973
10191
  this.moveMenu = null;
9974
10192
  }
10193
+ // ── legend row context menu (right-click) ───────────────────────────────
10194
+ /** Open the row's action menu at the pointer. One entry today: the values toggle. */
10195
+ openRowMenu(id, x, y) {
10196
+ this.closeRowMenu();
10197
+ this.closeMoveMenu();
10198
+ const row = this.rows.get(id);
10199
+ if (!row) return;
10200
+ const menu = document.createElement("div");
10201
+ menu.style.cssText = `position:fixed;z-index:var(--vela-z-tooltip);min-width:170px;padding:4px;border-radius:var(--vela-radius-md);background:var(--vela-surface-elev);color:${this.theme.textColor};border:1px solid var(--vela-border);box-shadow:var(--vela-shadow);font-size:var(--vela-font-size-md);`;
10202
+ applyChromeTokens(menu, this.theme);
10203
+ const item = document.createElement("button");
10204
+ item.type = "button";
10205
+ item.className = "vela-ind-menuitem";
10206
+ item.style.cssText = "display:flex;align-items:center;gap:8px;width:100%;text-align:left;padding:6px 10px;border:none;border-radius:var(--vela-radius-sm);color:inherit;cursor:pointer;white-space:nowrap;";
10207
+ const check = document.createElement("span");
10208
+ check.style.cssText = "display:inline-flex;align-items:center;justify-content:center;width:14px;flex:none;line-height:0;";
10209
+ check.innerHTML = this.rowValuesShown(row) ? CHECK_SVG : "";
10210
+ const label = document.createElement("span");
10211
+ label.textContent = "Indicator values";
10212
+ item.append(check, label);
10213
+ item.addEventListener("click", (e) => {
10214
+ e.stopPropagation();
10215
+ this.closeRowMenu();
10216
+ row.showValues = !this.rowValuesShown(row);
10217
+ this.renderRowValues(row);
10218
+ });
10219
+ menu.appendChild(item);
10220
+ document.body.appendChild(menu);
10221
+ const mw = menu.offsetWidth || 180;
10222
+ const mh = menu.offsetHeight || 40;
10223
+ menu.style.left = `${Math.min(x, window.innerWidth - mw - 6)}px`;
10224
+ menu.style.top = `${Math.min(y, window.innerHeight - mh - 6)}px`;
10225
+ this.rowMenu = menu;
10226
+ setTimeout(() => {
10227
+ if (typeof document !== "undefined") document.addEventListener("pointerdown", this.onRowMenuOutside, true);
10228
+ }, 0);
10229
+ }
10230
+ closeRowMenu() {
10231
+ if (typeof document !== "undefined") document.removeEventListener("pointerdown", this.onRowMenuOutside, true);
10232
+ this.rowMenu?.remove();
10233
+ this.rowMenu = null;
10234
+ }
9975
10235
  /** Move a legend row to another pane (indicator merged/moved), tidying an emptied container. */
9976
10236
  setPane(id, paneId) {
9977
10237
  const row = this.rows.get(id);
@@ -9993,7 +10253,8 @@ var InputsUI = class {
9993
10253
  if (!lg) {
9994
10254
  lg = document.createElement("div");
9995
10255
  lg.dataset.velaPane = paneId;
9996
- lg.style.cssText = "position:absolute;left:10px;z-index:5;display:flex;flex-direction:column;align-items:flex-start;gap:3px;pointer-events:none;font:12px -apple-system,Segoe UI,sans-serif;";
10256
+ lg.style.cssText = "position:absolute;left:10px;z-index:5;display:flex;flex-direction:column;align-items:flex-start;gap:0;pointer-events:none;font:12px -apple-system,Segoe UI,sans-serif;";
10257
+ applyChromeTokens(lg, this.theme);
9997
10258
  this.positionLegend(lg, paneId);
9998
10259
  this.container.appendChild(lg);
9999
10260
  this.legends.set(paneId, lg);
@@ -10029,15 +10290,16 @@ var InputsUI = class {
10029
10290
  */
10030
10291
  syncFoldToggle() {
10031
10292
  const count = this.rows.size;
10032
- if (count < 2) {
10293
+ const minRows = this.overviewAction !== null || this.mobileLayout ? 1 : 2;
10294
+ if (count < minRows) {
10033
10295
  const wasFolded = this.legendFolded;
10034
- this.legendFolded = false;
10296
+ if (count > 0) this.legendFolded = false;
10035
10297
  if (this.foldToggle) {
10036
10298
  this.foldToggle.remove();
10037
10299
  this.foldToggle = null;
10038
10300
  this.disposeTips(this.rowTips, "fold");
10039
10301
  }
10040
- if (wasFolded) this.reposition();
10302
+ if (wasFolded && count > 0) this.reposition();
10041
10303
  return;
10042
10304
  }
10043
10305
  let btn = this.foldToggle;
@@ -10045,25 +10307,30 @@ var InputsUI = class {
10045
10307
  btn = document.createElement("button");
10046
10308
  btn.type = "button";
10047
10309
  btn.className = "vela-ind-fold";
10048
- this.tip(this.rowTips, "fold", btn, () => this.legendFolded ? "Show indicator legend" : "Hide indicator legend");
10310
+ this.tip(this.rowTips, "fold", btn, () => this.overviewAction !== null ? "Indicators" : this.legendFolded ? "Show indicator legend" : "Hide indicator legend");
10049
10311
  btn.addEventListener("click", () => {
10312
+ if (this.overviewAction !== null) {
10313
+ this.overviewAction();
10314
+ return;
10315
+ }
10050
10316
  this.legendFolded = !this.legendFolded;
10051
10317
  this.syncFoldToggle();
10052
10318
  });
10053
10319
  this.foldToggle = btn;
10054
10320
  }
10055
10321
  const folded = this.legendFolded;
10056
- btn.setAttribute("aria-label", folded ? "Show indicator legend" : "Hide indicator legend");
10057
- btn.style.cssText = `pointer-events:auto;display:inline-flex;align-items:center;gap:4px;background:${this.theme.background};border:1px solid var(--vela-border);border-radius:4px;padding:2px 6px;margin-top:6px;cursor:pointer;font:inherit;line-height:0;`;
10322
+ btn.setAttribute("aria-label", this.overviewAction !== null ? "Indicators" : folded ? "Show indicator legend" : "Hide indicator legend");
10323
+ const ink = this.theme.textColor;
10324
+ btn.style.cssText = `pointer-events:auto;display:inline-flex;align-items:center;gap:4px;background:${this.theme.background};color:${ink};border:1px solid ${this.theme.borderColor};border-radius:4px;padding:2px 6px;margin-top:6px;cursor:pointer;font:inherit;line-height:0;`;
10058
10325
  btn.replaceChildren();
10059
10326
  const icon2 = document.createElement("span");
10060
- icon2.style.cssText = "display:inline-flex;align-items:center;line-height:0;";
10061
- icon2.innerHTML = folded ? UNFOLD_SVG : FOLD_SVG;
10327
+ icon2.style.cssText = "display:inline-flex;align-items:center;line-height:0;opacity:0.7;";
10328
+ icon2.innerHTML = this.overviewAction !== null ? OVERVIEW_SVG : folded ? UNFOLD_SVG : FOLD_SVG;
10062
10329
  btn.appendChild(icon2);
10063
10330
  if (folded) {
10064
10331
  const label = document.createElement("span");
10065
10332
  label.textContent = String(count);
10066
- label.style.cssText = "font-weight:600;line-height:normal;font-size:12px;color:var(--vela-fg-bright);";
10333
+ label.style.cssText = `font-weight:600;line-height:normal;font-size:12px;color:${ink};`;
10067
10334
  btn.appendChild(label);
10068
10335
  }
10069
10336
  this.legendFor("price").appendChild(btn);
@@ -10071,9 +10338,11 @@ var InputsUI = class {
10071
10338
  }
10072
10339
  /** Create or update an indicator's legend row (in the legend for its pane). */
10073
10340
  upsert(id, title, inputs, values, paneId = "price", opts = {}) {
10341
+ const settingsTitle = opts.settingsTitle ?? title;
10074
10342
  const existing = this.rows.get(id);
10075
10343
  if (existing) {
10076
10344
  existing.title = title;
10345
+ existing.settingsTitle = settingsTitle;
10077
10346
  existing.inputs = inputs;
10078
10347
  existing.values = { ...values };
10079
10348
  existing.titleEl.textContent = title;
@@ -10086,8 +10355,7 @@ var InputsUI = class {
10086
10355
  }
10087
10356
  ensureDialogStyles();
10088
10357
  const el = document.createElement("div");
10089
- el.style.cssText = `pointer-events:auto;display:flex;align-items:center;gap:6px;background:${this.idleRowFill()};border-radius:4px;padding:2px 7px;margin-left:-7px;color:${this.theme.textColor};user-select:none;-webkit-user-select:none;`;
10090
- el.addEventListener("mouseenter", () => this.setRowHighlighted(id, true));
10358
+ el.style.cssText = `pointer-events:auto;display:flex;align-items:center;background:${this.idleRowFill()};border-radius:4px;padding:${LEGEND_ROW_PAD_Y}px ${LEGEND_ROW_PAD_X}px;margin-left:-${LEGEND_ROW_PAD_X}px;min-height:${LEGEND_ROW_MIN_H}px;box-sizing:border-box;color:${this.theme.textColor};user-select:none;-webkit-user-select:none;`;
10091
10359
  el.addEventListener("mouseleave", () => {
10092
10360
  if (this.selectedId !== id) this.setRowHighlighted(id, false);
10093
10361
  });
@@ -10107,11 +10375,16 @@ var InputsUI = class {
10107
10375
  e.preventDefault();
10108
10376
  this.onRemove?.(id);
10109
10377
  });
10378
+ el.addEventListener("contextmenu", (e) => {
10379
+ e.preventDefault();
10380
+ e.stopPropagation();
10381
+ this.openRowMenu(id, e.clientX, e.clientY);
10382
+ });
10110
10383
  const statusEl = document.createElement("span");
10111
10384
  statusEl.style.cssText = "display:none;box-sizing:border-box;flex:none;";
10112
- el.appendChild(statusEl);
10113
10385
  const titleWrap = document.createElement("span");
10114
10386
  titleWrap.style.cssText = "white-space:nowrap;";
10387
+ titleWrap.addEventListener("mouseenter", () => this.setRowHighlighted(id, true));
10115
10388
  const titleEl = document.createElement("span");
10116
10389
  titleEl.textContent = title;
10117
10390
  titleEl.style.cssText = "font-weight:600;";
@@ -10123,6 +10396,11 @@ var InputsUI = class {
10123
10396
  titleWrap.appendChild(beta);
10124
10397
  }
10125
10398
  el.appendChild(titleWrap);
10399
+ const valuesEl = document.createElement("span");
10400
+ valuesEl.style.cssText = `display:none;align-items:center;gap:5px;margin-left:${LEGEND_TITLE_VALUES_GAP_PX}px;white-space:nowrap;font-variant-numeric:tabular-nums;`;
10401
+ el.appendChild(valuesEl);
10402
+ const controlsEl = document.createElement("span");
10403
+ controlsEl.style.cssText = `display:none;align-items:center;gap:${LEGEND_ACTION_GAP_PX}px;flex:none;margin-left:${LEGEND_ACTION_GAP_PX}px;`;
10126
10404
  let eyeEl = null;
10127
10405
  if (this.onToggleVisible) {
10128
10406
  const eye = document.createElement("button");
@@ -10131,16 +10409,14 @@ var InputsUI = class {
10131
10409
  this.tip(this.rowTips, id, eye, () => this.rows.get(id)?.hidden ? "Show" : "Hide");
10132
10410
  eye.innerHTML = EYE_SVG;
10133
10411
  eye.className = "vela-ind-ctl";
10134
- eye.style.cssText = "cursor:pointer;display:none;align-items:center;background:transparent;border:none;line-height:0;padding:0 1px;";
10412
+ eye.style.cssText = LEGEND_CTL_CSS;
10135
10413
  eye.addEventListener("click", () => {
10136
10414
  const row = this.rows.get(id);
10137
10415
  this.onToggleVisible?.(id, Boolean(row?.hidden));
10138
10416
  });
10139
- el.appendChild(eye);
10417
+ controlsEl.appendChild(eye);
10140
10418
  eyeEl = eye;
10141
10419
  }
10142
- const controlsEl = document.createElement("span");
10143
- controlsEl.style.cssText = "display:none;align-items:center;gap:6px;";
10144
10420
  if (inputs.length > 0) {
10145
10421
  const gear = document.createElement("button");
10146
10422
  gear.type = "button";
@@ -10148,7 +10424,7 @@ var InputsUI = class {
10148
10424
  this.tip(this.rowTips, id, gear, "Settings");
10149
10425
  gear.innerHTML = GEAR_SVG;
10150
10426
  gear.className = "vela-ind-ctl";
10151
- gear.style.cssText = "cursor:pointer;display:inline-flex;align-items:center;background:transparent;border:none;line-height:0;padding:0 1px;";
10427
+ gear.style.cssText = LEGEND_CTL_CSS;
10152
10428
  gear.addEventListener("click", () => this.openDialog(id));
10153
10429
  controlsEl.appendChild(gear);
10154
10430
  }
@@ -10159,7 +10435,7 @@ var InputsUI = class {
10159
10435
  this.tip(this.rowTips, id, mv, "Move to pane");
10160
10436
  mv.innerHTML = iconAt("move", LEGEND_ICON_PX);
10161
10437
  mv.className = "vela-ind-ctl";
10162
- mv.style.cssText = "cursor:pointer;display:inline-flex;align-items:center;background:transparent;border:none;line-height:0;padding:0 1px;";
10438
+ mv.style.cssText = LEGEND_CTL_CSS;
10163
10439
  mv.addEventListener("click", (e) => {
10164
10440
  e.stopPropagation();
10165
10441
  this.openMoveMenu(id, mv);
@@ -10176,12 +10452,13 @@ var InputsUI = class {
10176
10452
  this.tip(this.rowTips, id, close, "Remove indicator");
10177
10453
  close.innerHTML = CLOSE_SVG;
10178
10454
  close.className = "vela-ind-close";
10179
- close.style.cssText = "cursor:pointer;display:inline-flex;align-items:center;background:transparent;border:none;color:var(--vela-fg-muted);line-height:0;padding:0 1px;";
10455
+ close.style.cssText = LEGEND_CTL_CSS;
10180
10456
  close.addEventListener("click", () => this.onRemove?.(id));
10181
10457
  controlsEl.appendChild(close);
10182
10458
  el.appendChild(controlsEl);
10459
+ el.appendChild(statusEl);
10183
10460
  this.attach(this.legendFor(paneId), el, !!opts.native);
10184
- this.rows.set(id, { id, title, inputs, values: { ...values }, el, titleEl, statusEl, paneId, hidden: false, eyeEl, controlsEl, extrasEl, native: !!opts.native });
10461
+ this.rows.set(id, { id, title, settingsTitle, inputs, values: { ...values }, el, titleEl, statusEl, valuesEl, plotValues: [], plotValuesKey: "", showValues: null, highlighted: false, paneId, hidden: false, eyeEl, controlsEl, extrasEl, native: !!opts.native });
10185
10462
  this.syncFoldToggle();
10186
10463
  }
10187
10464
  /** Place a row in its pane's legend — native rows PREPEND (pinned to the top), Pine rows append. */
@@ -10195,24 +10472,31 @@ var InputsUI = class {
10195
10472
  if (row) row.values = { ...row.values, ...values };
10196
10473
  }
10197
10474
  /**
10198
- * Reflect an indicator's live status in its legend row: `'loading'` shows a spinner (a fetch is
10199
- * in flight), `'live'` a pulsing dot (live-updating), `'idle'` nothing. Rendered left of the title.
10475
+ * Reflect an indicator's live status in its legend row: `'loading'` shows three pulsing
10476
+ * dots (a fetch is in flight the same load affordance as the chart's own bar-load dots,
10477
+ * at legend scale), `'live'` a pulsing dot, `'idle'` nothing. Rendered at the row's right end.
10200
10478
  */
10201
10479
  setStatus(id, status) {
10202
10480
  const row = this.rows.get(id);
10203
10481
  if (!row) return;
10204
10482
  const el = row.statusEl;
10483
+ el.replaceChildren();
10205
10484
  if (status === "idle") {
10206
- el.style.display = "none";
10207
- el.style.animation = "none";
10485
+ el.style.cssText = "display:none;box-sizing:border-box;flex:none;";
10208
10486
  return;
10209
10487
  }
10210
- this.ensureStatusKeyframes();
10211
10488
  if (status === "loading") {
10212
- el.style.cssText = `display:inline-block;box-sizing:border-box;flex:none;width:11px;height:11px;border-radius:50%;border:2px solid var(--vela-border-strong);border-top-color:var(--vela-fg-bright);animation:vela-ind-spin .7s linear infinite;`;
10213
- } else {
10214
- el.style.cssText = `display:inline-block;box-sizing:border-box;flex:none;width:8px;height:8px;border-radius:50%;background:${this.theme.upColor};animation:vela-ind-pulse 1.2s ease-in-out infinite;`;
10489
+ el.style.cssText = "display:inline-flex;align-items:center;gap:3px;box-sizing:border-box;flex:none;";
10490
+ for (let i = 0; i < 3; i += 1) {
10491
+ const dot = document.createElement("span");
10492
+ dot.style.cssText = "width:4px;height:4px;border-radius:50%;background:currentColor;opacity:0.15;flex:none;";
10493
+ dot.animate?.([{ opacity: 0.12 }, { opacity: 0.55 }], { duration: 800, iterations: Infinity, direction: "alternate", easing: "ease-in-out", delay: -i * 260 });
10494
+ el.appendChild(dot);
10495
+ }
10496
+ return;
10215
10497
  }
10498
+ this.ensureStatusKeyframes();
10499
+ el.style.cssText = `display:inline-block;box-sizing:border-box;flex:none;width:8px;height:8px;border-radius:50%;background:${this.theme.upColor};animation:vela-ind-pulse 1.2s ease-in-out infinite;`;
10216
10500
  }
10217
10501
  /** Inject the status keyframes once (idempotent). */
10218
10502
  ensureStatusKeyframes() {
@@ -10231,8 +10515,8 @@ var InputsUI = class {
10231
10515
  if (row.eyeEl) {
10232
10516
  row.eyeEl.innerHTML = visible ? EYE_SVG : EYE_OFF_SVG;
10233
10517
  row.eyeEl.setAttribute("aria-label", visible ? "Hide" : "Show");
10234
- row.eyeEl.style.display = !visible || row.controlsEl.style.display !== "none" ? "inline-flex" : "none";
10235
10518
  }
10519
+ this.syncRowActions(row);
10236
10520
  }
10237
10521
  /**
10238
10522
  * Select a legend row (outlining it) and clear any previous selection, so at most one
@@ -10245,17 +10529,36 @@ var InputsUI = class {
10245
10529
  this.setRowHighlighted(id, true);
10246
10530
  }
10247
10531
  /**
10248
- * Show or hide a row's outline and eye/gear/✕ controls together — driven by both hover and
10249
- * selection, so an idle (unhovered, unselected) row shows just its title. Exception: a hidden
10250
- * indicator keeps its eye visible even when idle, so it can be un-hidden without hovering.
10532
+ * Show or hide a row's outline and eye/gear/✕ controls together — driven by title
10533
+ * hover and selection (plot values do not open the chip). An idle unselected row
10534
+ * shows just its title (+ values). Exception: a hidden indicator keeps its eye
10535
+ * visible even when idle, so it can be un-hidden without hovering.
10251
10536
  */
10252
10537
  setRowHighlighted(id, highlighted) {
10253
10538
  const row = this.rows.get(id);
10254
10539
  if (!row) return;
10540
+ row.highlighted = highlighted;
10541
+ this.renderRowValues(row);
10255
10542
  row.el.style.boxShadow = highlighted ? `inset 0 0 0 1px ${this.neutralBorder()}` : "none";
10256
10543
  row.el.style.background = highlighted ? this.theme.background : this.idleRowFill();
10257
- row.controlsEl.style.display = highlighted ? "inline-flex" : "none";
10258
- if (row.eyeEl) row.eyeEl.style.display = highlighted || row.hidden ? "inline-flex" : "none";
10544
+ this.syncRowActions(row);
10545
+ }
10546
+ /**
10547
+ * Reveal the action cluster (or just the eye for a hidden indicator). Non-eye
10548
+ * buttons only appear while highlighted; the eye keeps its inline-flex from
10549
+ * construction and is shown/hidden with the cluster container.
10550
+ */
10551
+ syncRowActions(row) {
10552
+ const open = row.highlighted;
10553
+ row.controlsEl.style.display = open || row.hidden ? "inline-flex" : "none";
10554
+ for (const child of Array.from(row.controlsEl.children)) {
10555
+ if (!(child instanceof HTMLElement) || child === row.eyeEl) continue;
10556
+ if (child === row.extrasEl) {
10557
+ child.style.display = open ? "contents" : "none";
10558
+ continue;
10559
+ }
10560
+ child.style.display = open ? "inline-flex" : "none";
10561
+ }
10259
10562
  }
10260
10563
  /** Drop the current selection outline, if any. */
10261
10564
  clearSelection() {
@@ -10283,6 +10586,7 @@ var InputsUI = class {
10283
10586
  destroy() {
10284
10587
  this.closeDialog();
10285
10588
  this.closeMoveMenu();
10589
+ this.closeRowMenu();
10286
10590
  for (const id of [...this.rowTips.keys()]) this.disposeTips(this.rowTips, id);
10287
10591
  for (const id of [...this.extrasTips.keys()]) this.disposeTips(this.extrasTips, id);
10288
10592
  for (const lg of this.legends.values()) lg.remove();
@@ -10338,6 +10642,7 @@ var InputsUI = class {
10338
10642
  const card = document.createElement("div");
10339
10643
  card.className = "vela-ind-dialog";
10340
10644
  card.style.cssText = `display:flex;flex-direction:column;width:fit-content;min-width:min(360px,90%);max-width:min(640px,94%);max-height:82%;background:${t.background};border:1px solid ${border};border-radius:var(--vela-radius-lg);box-shadow:var(--vela-shadow-dialog);color:${fg};color-scheme:${scheme};font:13px ${font};overflow:hidden;`;
10645
+ if (this.mobileLayout) card.style.cssText += "width:100%;min-width:0;max-width:none;height:100%;max-height:none;border:none;border-radius:0;";
10341
10646
  applyChromeTokens(card, t);
10342
10647
  card.addEventListener("keydown", (e) => {
10343
10648
  if (e.key !== "Escape") e.stopPropagation();
@@ -10345,7 +10650,7 @@ var InputsUI = class {
10345
10650
  const header = document.createElement("div");
10346
10651
  header.style.cssText = "display:flex;justify-content:space-between;align-items:flex-start;gap:12px;padding:16px 20px 12px;flex:0 0 auto;";
10347
10652
  const hTitle = document.createElement("span");
10348
- hTitle.textContent = row.title;
10653
+ hTitle.textContent = row.settingsTitle;
10349
10654
  hTitle.style.cssText = "font-weight:600;font-size:16px;line-height:1.3;";
10350
10655
  const closeBtn = document.createElement("button");
10351
10656
  closeBtn.type = "button";
@@ -10357,7 +10662,7 @@ var InputsUI = class {
10357
10662
  closeBtn.addEventListener("click", () => this.closeDialog());
10358
10663
  header.append(hTitle, closeBtn);
10359
10664
  card.appendChild(header);
10360
- makeDialogDraggable(card, header, { closeSelector: "button" });
10665
+ if (!this.mobileLayout) makeDialogDraggable(card, header, { closeSelector: "button" });
10361
10666
  const tabs = document.createElement("div");
10362
10667
  tabs.style.cssText = `display:flex;gap:16px;padding:0 20px;border-bottom:1px solid ${border};flex:0 0 auto;`;
10363
10668
  const tab = document.createElement("div");
@@ -10377,7 +10682,7 @@ var InputsUI = class {
10377
10682
  section.appendChild(gh);
10378
10683
  }
10379
10684
  const grid = document.createElement("div");
10380
- grid.style.cssText = "display:grid;grid-template-columns:minmax(140px,auto) auto auto 1fr;align-items:center;column-gap:12px;row-gap:22px;";
10685
+ grid.style.cssText = this.mobileLayout ? "display:grid;grid-template-columns:minmax(0,1fr) auto auto max-content;align-items:center;column-gap:12px;row-gap:22px;" : "display:grid;grid-template-columns:minmax(140px,auto) auto auto 1fr;align-items:center;column-gap:12px;row-gap:22px;";
10381
10686
  for (const inputRow of group.rows) this.buildInputRow(grid, inputRow, row);
10382
10687
  section.appendChild(grid);
10383
10688
  body.appendChild(section);
@@ -10826,10 +11131,14 @@ function groupInputs(inputs) {
10826
11131
  var CHECK_SVG = iconAt("check", 12);
10827
11132
  var SELECT_CHEVRON_SVG = iconAt("chevron-down", 16);
10828
11133
  var DIALOG_STYLE_ID = "vela-ind-dialog-styles";
11134
+ var DIALOG_STYLE_REV = "10";
10829
11135
  function ensureDialogStyles() {
10830
- if (typeof document === "undefined" || document.getElementById(DIALOG_STYLE_ID)) return;
10831
- const s = document.createElement("style");
11136
+ if (typeof document === "undefined") return;
11137
+ const existing = document.getElementById(DIALOG_STYLE_ID);
11138
+ if (existing?.dataset.rev === DIALOG_STYLE_REV) return;
11139
+ const s = existing ?? document.createElement("style");
10832
11140
  s.id = DIALOG_STYLE_ID;
11141
+ s.dataset.rev = DIALOG_STYLE_REV;
10833
11142
  s.textContent = `
10834
11143
  .vela-ind-dialog input[type=color]{-webkit-appearance:none;appearance:none;}
10835
11144
  .vela-ind-dialog input[type=color]::-webkit-color-swatch-wrapper{padding:0;}
@@ -10843,19 +11152,19 @@ function ensureDialogStyles() {
10843
11152
  .vela-ind-dialog ::-webkit-scrollbar-track{background:transparent;}
10844
11153
  .vela-ind-hint{background:var(--vela-hover);color:var(--vela-fg-muted);transition:background var(--vela-dur-fast) ease,color var(--vela-dur-fast) ease;}
10845
11154
  .vela-ind-hint:hover{background:var(--vela-active);color:var(--vela-fg-bright);}
10846
- .vela-ind-ctl{color:var(--vela-fg-muted);transition:color var(--vela-dur-fast) ease;}
10847
- .vela-ind-ctl:hover{color:var(--vela-fg-bright);}
10848
- .vela-ind-close{transition:color var(--vela-dur-fast) ease;}
10849
- .vela-ind-close:hover{color:var(--vela-danger) !important;}
10850
- .vela-ind-fold{color:var(--vela-fg-muted);transition:color var(--vela-dur-fast) ease,border-color var(--vela-dur-fast) ease;}
10851
- .vela-ind-fold:hover{color:var(--vela-fg-bright);border-color:var(--vela-border-strong);}
11155
+ .vela-ind-ctl,.vela-ind-close{background-color:transparent;color:inherit;transition:color var(--vela-dur-fast) ease,background-color var(--vela-dur-fast) ease;}
11156
+ .vela-ind-ctl svg,.vela-ind-close svg{width:${LEGEND_ICON_PX}px;height:${LEGEND_ICON_PX}px;display:block;flex:none;stroke-width:1;}
11157
+ .vela-ind-ctl:hover{background-color:var(--vela-hover-strong);}
11158
+ .vela-ind-close:hover{color:var(--vela-danger) !important;background-color:var(--vela-hover-strong);}
11159
+ .vela-ind-fold{transition:border-color var(--vela-dur-fast) ease,opacity var(--vela-dur-fast) ease;}
11160
+ .vela-ind-fold:hover{border-color:var(--vela-border-strong);opacity:0.9;}
10852
11161
  .vela-ind-menuitem{background:transparent;transition:background var(--vela-dur-fast) ease;}
10853
11162
  .vela-ind-menuitem:hover{background:var(--vela-hover-strong);}
10854
11163
  .vela-ind-btn{cursor:pointer;padding:7px 14px;border-radius:var(--vela-radius-md);border:1px solid transparent;background:transparent;color:var(--vela-fg-muted);font-weight:600;font-size:13px;font-family:inherit;transition:background var(--vela-dur-fast) ease,color var(--vela-dur-fast) ease,opacity var(--vela-dur-fast) ease;}
10855
11164
  .vela-ind-btn:hover{background:var(--vela-hover);color:var(--vela-fg-bright);}
10856
11165
  .vela-ind-btn-primary{padding:7px 16px;border-color:var(--vela-selected-bg);background:var(--vela-selected-bg);color:var(--vela-selected-fg);}
10857
11166
  .vela-ind-btn-primary:hover{background:var(--vela-selected-bg);color:var(--vela-selected-fg);opacity:0.85;}`;
10858
- document.head.appendChild(s);
11167
+ if (!existing) document.head.appendChild(s);
10859
11168
  }
10860
11169
  var TIMEFRAME_OPTIONS = [
10861
11170
  { value: "", label: "Chart" },
@@ -11464,6 +11773,44 @@ function basePaintingOf(style) {
11464
11773
  for (const t of chartTypes()) if (t.id === style) return t.basePainting ?? "candles";
11465
11774
  return "candles";
11466
11775
  }
11776
+ function hasOwnCandlePaint(style) {
11777
+ if (BUILTIN_PRICE_STYLES.includes(style)) return false;
11778
+ for (const t of chartTypes()) if (t.id === style) return (t.basePainting ?? "candles") === "candles";
11779
+ return false;
11780
+ }
11781
+ function candleOverrideFor(style, bags) {
11782
+ if (!hasOwnCandlePaint(style)) return null;
11783
+ const bag = bags[style] ?? {};
11784
+ const color = (v) => isColor(v) ? v : null;
11785
+ const bool = (v) => isBool(v) ? v : null;
11786
+ return {
11787
+ upColor: color(bag.candleUpColor),
11788
+ downColor: color(bag.candleDownColor),
11789
+ bodyVisible: bool(bag.candleBodyVisible),
11790
+ borderVisible: bool(bag.candleBorderVisible),
11791
+ borderUpColor: color(bag.candleBorderUpColor),
11792
+ borderDownColor: color(bag.candleBorderDownColor),
11793
+ wickVisible: bool(bag.candleWickVisible),
11794
+ wickUpColor: color(bag.candleWickUpColor),
11795
+ wickDownColor: color(bag.candleWickDownColor)
11796
+ };
11797
+ }
11798
+ function effectiveCandlePaint(base, override, themeUp, themeDown) {
11799
+ if (!override) return { up: themeUp, down: themeDown, candle: base };
11800
+ return {
11801
+ up: override.upColor ?? themeUp,
11802
+ down: override.downColor ?? themeDown,
11803
+ candle: {
11804
+ bodyVisible: override.bodyVisible ?? base.bodyVisible,
11805
+ borderVisible: override.borderVisible ?? base.borderVisible,
11806
+ borderUpColor: override.borderUpColor ?? base.borderUpColor,
11807
+ borderDownColor: override.borderDownColor ?? base.borderDownColor,
11808
+ wickVisible: override.wickVisible ?? base.wickVisible,
11809
+ wickUpColor: override.wickUpColor ?? base.wickUpColor,
11810
+ wickDownColor: override.wickDownColor ?? base.wickDownColor
11811
+ }
11812
+ };
11813
+ }
11467
11814
  function priceStyleIds() {
11468
11815
  const out = [...BUILTIN_PRICE_STYLES];
11469
11816
  for (const t of chartTypes()) if (!out.includes(t.id)) out.push(t.id);
@@ -11502,6 +11849,33 @@ function clampLevel(v) {
11502
11849
  function clampSpacing(v) {
11503
11850
  return v < 0.1 ? 0.1 : v > 10 ? 10 : v;
11504
11851
  }
11852
+ function factoryResetConfig(factory) {
11853
+ const bag = {};
11854
+ for (const t of chartTypes()) {
11855
+ const section = t.settings;
11856
+ if (!section) continue;
11857
+ const defaults2 = {};
11858
+ const addRows = (rows) => {
11859
+ for (const r of rows ?? []) {
11860
+ for (const k of settingsRowValueKeys(r)) defaults2[k.key] = k.defval;
11861
+ }
11862
+ };
11863
+ for (const inst of section.instances ?? []) {
11864
+ if (inst.enableKey) defaults2[inst.enableKey] = false;
11865
+ addRows(inst.rows);
11866
+ }
11867
+ for (const sub of section.subsections ?? []) {
11868
+ if (sub.enableKey) defaults2[sub.enableKey] = false;
11869
+ addRows(sub.rows);
11870
+ }
11871
+ if (!section.instances) addRows(section.rows);
11872
+ bag[t.id] = defaults2;
11873
+ }
11874
+ for (const [typeId, vals] of Object.entries(factory.chartTypes)) {
11875
+ bag[typeId] = { ...bag[typeId] ?? {}, ...vals };
11876
+ }
11877
+ return { ...factory, chartTypes: bag };
11878
+ }
11505
11879
  function mergeConfig(base, patch) {
11506
11880
  const p = asObject(patch);
11507
11881
  const ctPatch = asObject(p.chartTypes);
@@ -11816,6 +12190,9 @@ var SceneGraph = class {
11816
12190
  this.priceStyle = "candles";
11817
12191
  /** Price-series base painting for the ACTIVE style (see ChartTypeDefinition.basePainting). */
11818
12192
  this.basePainting = "candles";
12193
+ /** The ACTIVE style's own candle cosmetics (`chartTypes.<id>.candle*`) when it is a
12194
+ * candle-based plugin type; null ⇒ paint with the shared `style.candle` block. */
12195
+ this.candleOverride = null;
11819
12196
  /** Explicit baseline reference price for `priceStyle:'baseline'`; when null the
11820
12197
  * baseline follows `style.baseline.baselineLevel` as a percent of the visible pane
11821
12198
  * range (resolved per frame via `baselinePriceFor`). */
@@ -13045,14 +13422,15 @@ var WebGL2Backend = class {
13045
13422
  emitCandles(b, scene, bars, i0, i1, coords, pane, theme, barColors) {
13046
13423
  const spacing = coords.bodySpacing();
13047
13424
  const tier = candleTier(spacing);
13425
+ const paint = effectiveCandlePaint(scene.style.candle, scene.candleOverride, theme.upColor, theme.downColor);
13048
13426
  if (tier === "aggregate") {
13049
- this.emitCandlesAggregated(b, bars, i0, i1, coords, pane, theme.upColor, theme.downColor, barColors);
13427
+ this.emitCandlesAggregated(b, bars, i0, i1, coords, pane, paint.up, paint.down, barColors);
13050
13428
  return;
13051
13429
  }
13052
13430
  const drawBody = tier === "full";
13053
- const up = theme.upColor;
13054
- const down = theme.downColor;
13055
- const cs = scene.style.candle;
13431
+ const up = paint.up;
13432
+ const down = paint.down;
13433
+ const cs = paint.candle;
13056
13434
  const fading = this.candleStructureAlpha > this.candleBodyAlpha + 1e-3;
13057
13435
  for (let i = i0; i <= i1; i += 1) {
13058
13436
  const bar = bars[i];
@@ -13623,6 +14001,11 @@ function easeToward(current, target, dtMs, tauMs) {
13623
14001
  var FLING_MIN_SPEED = 0.04;
13624
14002
  var FLING_STALE_MS = 60;
13625
14003
  var DRAG_SLOP = 2;
14004
+ var LONG_PRESS_MS = 350;
14005
+ var TOUCH_SLOP = 8;
14006
+ var TOUCH_TAP_SLOP = 14;
14007
+ var DOUBLE_TAP_MS = 350;
14008
+ var DOUBLE_TAP_SLOP = 30;
13626
14009
  var TIME_SCALE_K = 4e-3;
13627
14010
  var WHEEL_ZOOM_K = 25e-4;
13628
14011
  function wheelZoomAnchor(coords, cursorX, rightEdge) {
@@ -13643,8 +14026,13 @@ function wheelPanDelta(deltaX, deltaY, shift) {
13643
14026
  function effectiveSnapMode(momentaryOverride, sticky) {
13644
14027
  return momentaryOverride ? "strong" : sticky;
13645
14028
  }
14029
+ function pinchBarSpacing(startBarSpacing, startDist, dist) {
14030
+ return clampBarSpacing(startBarSpacing * (dist / Math.max(1, startDist)));
14031
+ }
14032
+ function pinchPinnedRightOffset(anchorLogical, barCount, width, x, pxPerBar) {
14033
+ return anchorLogical - (barCount - 1) + (width - x) / pxPerBar;
14034
+ }
13646
14035
  var InputController = class {
13647
- // the last middle press deleted a drawing (suppress autoscroll/paste)
13648
14036
  constructor(deps) {
13649
14037
  this.deps = deps;
13650
14038
  /** When true (the default), wheel-zoom pins the right edge instead of the bar
@@ -13672,9 +14060,40 @@ var InputController = class {
13672
14060
  this.vx = 0;
13673
14061
  // smoothed pointer velocity, px/ms
13674
14062
  this.middleDeleted = false;
14063
+ // the last middle press deleted a drawing (suppress autoscroll/paste)
14064
+ // Last cursor position over the element (NaN once it leaves) — lets a modifier
14065
+ // press/release re-shape the drawings preview with the pointer stationary.
14066
+ this.cursorX = NaN;
14067
+ this.cursorY = NaN;
14068
+ // ── touch state ──
14069
+ /** Live touch contacts (local coords) — two entries drive a pinch; extras are ignored. */
14070
+ this.touches = /* @__PURE__ */ new Map();
14071
+ this.lpTimer = null;
14072
+ this.pinchStartDist = 1;
14073
+ this.pinchStartBarSpacing = 0;
14074
+ this.pinchAnchorLogical = 0;
14075
+ // Last clean touch tap (for double-tap pairing). Time 0 = no pending first tap.
14076
+ this.lastTapT = 0;
14077
+ this.lastTapX = 0;
14078
+ this.lastTapY = 0;
14079
+ this.onTouchContextMenu = (e) => {
14080
+ if (this.touches.size === 0) return;
14081
+ if (!(e.target instanceof Node) || !this.el?.contains(e.target)) return;
14082
+ e.preventDefault();
14083
+ e.stopImmediatePropagation();
14084
+ };
13675
14085
  this.onMiddleGuard = (e) => {
13676
14086
  if (e.button === 1 && this.middleDeleted) e.preventDefault();
13677
14087
  };
14088
+ /** A modifier press/release with the pointer stationary: re-issue the last cursor
14089
+ * position so the placing ghost / drag preview reflects the new state immediately
14090
+ * (Shift's 45° angle lock, Ctrl/Cmd's momentary strong magnet). */
14091
+ this.onModifier = (e) => {
14092
+ if (e.repeat || e.key !== "Shift" && e.key !== "Control" && e.key !== "Meta") return;
14093
+ if (Number.isNaN(this.cursorX)) return;
14094
+ if (this.dragging && this.region !== "drawing") return;
14095
+ this.deps.drawingsPointerMove?.(this.cursorX, this.cursorY, this.snapMode(e), e.shiftKey);
14096
+ };
13678
14097
  this.onDown = (e) => {
13679
14098
  if (e.button === 1) {
13680
14099
  const { x: x2, y: y2 } = this.local(e);
@@ -13684,6 +14103,15 @@ var InputController = class {
13684
14103
  }
13685
14104
  if (e.button !== 0) return;
13686
14105
  const { x, y } = this.local(e);
14106
+ if (e.pointerType === "touch") {
14107
+ if (this.touches.size === 1 && this.dragging && (this.region === "data" || this.region === "crosshair")) {
14108
+ this.touches.set(e.pointerId, { x, y });
14109
+ this.beginPinch(e);
14110
+ return;
14111
+ }
14112
+ if (this.touches.size >= 1) return;
14113
+ this.touches.set(e.pointerId, { x, y });
14114
+ }
13687
14115
  this.deps.drawingsClearTransient?.();
13688
14116
  this.dragging = true;
13689
14117
  this.moved = false;
@@ -13709,6 +14137,9 @@ var InputController = class {
13709
14137
  if (this.region === "price") this.deps.beginPriceScale(x, y);
13710
14138
  else if (this.region === "separator") this.deps.beginPaneResize(y);
13711
14139
  else if (this.region === "data") this.verticalPan = this.deps.beginPricePan(y);
14140
+ if (e.pointerType === "touch" && (this.region === "data" || this.region === "price" || this.region === "time")) {
14141
+ this.startLongPress(x, y, this.region);
14142
+ }
13712
14143
  this.lastX = x;
13713
14144
  this.lastT = e.timeStamp;
13714
14145
  this.vx = 0;
@@ -13716,28 +14147,44 @@ var InputController = class {
13716
14147
  };
13717
14148
  this.onMove = (e) => {
13718
14149
  const { x, y } = this.local(e);
14150
+ this.cursorX = x;
14151
+ this.cursorY = y;
14152
+ if (e.pointerType === "touch" && this.touches.has(e.pointerId)) this.touches.set(e.pointerId, { x, y });
14153
+ if (this.dragging && this.region === "pinch") {
14154
+ this.applyPinch();
14155
+ return;
14156
+ }
14157
+ if (this.dragging && this.region === "crosshair") {
14158
+ this.deps.onPointerMove(x, y);
14159
+ return;
14160
+ }
14161
+ if (this.dragging && this.region === "axis-sheet") {
14162
+ return;
14163
+ }
14164
+ if (this.lpTimer !== null && Math.hypot(x - this.startX, y - this.startY) > TOUCH_SLOP) this.cancelLongPress();
14165
+ const slop = e.pointerType === "touch" ? TOUCH_SLOP : DRAG_SLOP;
13719
14166
  if (this.dragging) {
13720
14167
  if (this.region === "price") {
13721
- if (Math.abs(y - this.startY) > DRAG_SLOP) this.moved = true;
14168
+ if (Math.abs(y - this.startY) > slop) this.moved = true;
13722
14169
  this.deps.priceScaleBy(y - this.startY);
13723
14170
  } else if (this.region === "separator") {
13724
- if (Math.abs(y - this.startY) > DRAG_SLOP) this.moved = true;
14171
+ if (Math.abs(y - this.startY) > slop) this.moved = true;
13725
14172
  this.deps.paneResizeBy(y - this.startY);
13726
14173
  } else if (this.region === "time") {
13727
- if (Math.abs(x - this.startX) > DRAG_SLOP) this.moved = true;
14174
+ if (Math.abs(x - this.startX) > slop) this.moved = true;
13728
14175
  const barSpacing = clampBarSpacing(this.startBarSpacing * Math.exp((this.startX - x) * TIME_SCALE_K));
13729
14176
  this.deps.apply({ barSpacing, rightOffset: this.startRightOffset });
13730
14177
  } else if (this.region === "drawing") {
13731
- if (Math.abs(x - this.startX) > DRAG_SLOP || Math.abs(y - this.startY) > DRAG_SLOP) this.moved = true;
13732
- this.deps.drawingsPointerMove?.(x, y, this.snapMode(e));
14178
+ if (Math.abs(x - this.startX) > slop || Math.abs(y - this.startY) > slop) this.moved = true;
14179
+ this.deps.drawingsPointerMove?.(x, y, this.snapMode(e), e.shiftKey);
13733
14180
  } else {
13734
14181
  const coords = this.deps.getCoords();
13735
14182
  const vp = coords.getViewport();
13736
14183
  const dx = x - this.startX;
13737
- if (Math.abs(dx) > DRAG_SLOP) this.moved = true;
14184
+ if (Math.abs(dx) > slop) this.moved = true;
13738
14185
  this.deps.apply({ barSpacing: vp.barSpacing, rightOffset: this.startRightOffset - dx / coords.pxPerBar() });
13739
14186
  if (this.verticalPan) {
13740
- if (Math.abs(y - this.startY) > DRAG_SLOP) this.moved = true;
14187
+ if (Math.abs(y - this.startY) > slop) this.moved = true;
13741
14188
  this.deps.pricePanBy(y - this.startY);
13742
14189
  }
13743
14190
  const dt = e.timeStamp - this.lastT;
@@ -13752,16 +14199,46 @@ var InputController = class {
13752
14199
  const drawCursor = this.deps.drawingsCursor?.(x, y);
13753
14200
  const r = this.regionAt(x, y);
13754
14201
  this.el.style.cursor = drawCursor ?? (r === "price" ? "ns-resize" : r === "time" ? "ew-resize" : r === "separator" ? "row-resize" : "");
13755
- this.deps.drawingsPointerMove?.(x, y, this.snapMode(e));
14202
+ this.deps.drawingsPointerMove?.(x, y, this.snapMode(e), e.shiftKey);
13756
14203
  }
13757
- this.deps.onPointerMove(x, y);
14204
+ if (e.pointerType !== "touch") this.deps.onPointerMove(x, y);
13758
14205
  };
13759
14206
  this.onUp = (e) => {
14207
+ if (e.pointerType === "touch") this.touches.delete(e.pointerId);
14208
+ this.cancelLongPress();
14209
+ if (this.dragging && this.region === "pinch") {
14210
+ if (this.touches.size === 1) {
14211
+ const rest = [...this.touches.values()][0];
14212
+ const vp = this.deps.getCoords().getViewport();
14213
+ this.region = "data";
14214
+ this.verticalPan = false;
14215
+ this.startX = rest.x;
14216
+ this.startY = rest.y;
14217
+ this.startRightOffset = vp.rightOffset;
14218
+ this.startBarSpacing = vp.barSpacing;
14219
+ this.lastX = rest.x;
14220
+ this.lastT = e.timeStamp;
14221
+ this.vx = 0;
14222
+ return;
14223
+ }
14224
+ this.endGesture(e);
14225
+ return;
14226
+ }
14227
+ if (this.dragging && this.region === "crosshair") {
14228
+ this.deps.onPointerMove(null, null);
14229
+ this.endGesture(e);
14230
+ return;
14231
+ }
14232
+ if (this.dragging && this.region === "axis-sheet") {
14233
+ this.endGesture(e);
14234
+ return;
14235
+ }
14236
+ const { x, y } = this.local(e);
14237
+ const wasTouch = e.pointerType === "touch";
14238
+ const tapRelease = this.dragging && !this.moved && (!wasTouch || Math.hypot(x - this.startX, y - this.startY) <= TOUCH_TAP_SLOP);
13760
14239
  if (this.dragging && this.region === "drawing") {
13761
- const { x, y } = this.local(e);
13762
14240
  this.deps.drawingsPointerUp?.(x, y);
13763
- } else if (this.dragging && !this.moved && this.region === "data") {
13764
- const { x, y } = this.local(e);
14241
+ } else if (tapRelease && this.region === "data") {
13765
14242
  this.deps.onClick(x, y);
13766
14243
  } else if (this.dragging && this.region === "data") {
13767
14244
  const stale = e.timeStamp - this.lastT > FLING_STALE_MS;
@@ -13770,23 +14247,29 @@ var InputController = class {
13770
14247
  this.deps.fling(-this.vx / pitch);
13771
14248
  }
13772
14249
  }
13773
- this.dragging = false;
13774
- try {
13775
- this.el?.releasePointerCapture(e.pointerId);
13776
- } catch {
13777
- }
14250
+ const wasTap = wasTouch && tapRelease;
14251
+ this.endGesture(e);
14252
+ if (wasTouch) this.deps.onPointerMove(null, null);
14253
+ if (wasTap) this.registerTap(e.timeStamp, x, y);
14254
+ };
14255
+ /** The browser took the pointer back (system gesture, palm rejection, tab switch):
14256
+ * drop the gesture where it is — no click, no fling, no stuck `dragging`. */
14257
+ this.onCancel = (e) => {
14258
+ if (e.pointerType === "touch") this.touches.delete(e.pointerId);
14259
+ this.cancelLongPress();
14260
+ if (!this.dragging) return;
14261
+ if (this.region === "drawing" && !Number.isNaN(this.cursorX)) this.deps.drawingsPointerUp?.(this.cursorX, this.cursorY);
14262
+ if (this.region === "crosshair" || e.pointerType === "touch") this.deps.onPointerMove(null, null);
14263
+ this.endGesture(e);
13778
14264
  };
13779
14265
  this.onLeave = () => {
14266
+ this.cursorX = NaN;
14267
+ this.cursorY = NaN;
13780
14268
  this.deps.onPointerMove(null, null);
13781
14269
  };
13782
14270
  this.onDblClick = (e) => {
13783
14271
  const { x, y } = this.local(e);
13784
- if (this.deps.drawingsDblClick?.(x, y)) return;
13785
- const region = this.regionAt(x, y);
13786
- if (region === "price") this.deps.resetPriceScale(x, y);
13787
- else if (region === "separator") this.deps.resetPaneSize(y);
13788
- else if (region === "time") this.deps.resetView();
13789
- else this.deps.dataDblClick(x, y);
14272
+ this.doubleActivate(x, y);
13790
14273
  };
13791
14274
  this.onWheel = (e) => {
13792
14275
  e.preventDefault();
@@ -13807,26 +14290,44 @@ var InputController = class {
13807
14290
  }
13808
14291
  attach(el) {
13809
14292
  this.el = el;
14293
+ if (el.style !== void 0) {
14294
+ el.style.touchAction = "none";
14295
+ el.style.userSelect = "none";
14296
+ el.style.setProperty("-webkit-user-select", "none");
14297
+ el.style.setProperty("-webkit-touch-callout", "none");
14298
+ }
13810
14299
  el.addEventListener("pointerdown", this.onDown);
13811
14300
  el.addEventListener("pointermove", this.onMove);
13812
14301
  el.addEventListener("pointerup", this.onUp);
13813
14302
  el.addEventListener("pointerleave", this.onLeave);
14303
+ el.addEventListener("pointercancel", this.onCancel);
13814
14304
  el.addEventListener("dblclick", this.onDblClick);
13815
14305
  el.addEventListener("wheel", this.onWheel, { passive: false });
13816
14306
  el.addEventListener("mousedown", this.onMiddleGuard);
13817
14307
  el.addEventListener("auxclick", this.onMiddleGuard);
14308
+ const win = el.ownerDocument?.defaultView;
14309
+ win?.addEventListener("keydown", this.onModifier);
14310
+ win?.addEventListener("keyup", this.onModifier);
14311
+ win?.addEventListener("contextmenu", this.onTouchContextMenu, true);
13818
14312
  }
13819
14313
  detach() {
13820
14314
  const el = this.el;
13821
14315
  if (!el) return;
14316
+ const win = el.ownerDocument?.defaultView;
14317
+ win?.removeEventListener("keydown", this.onModifier);
14318
+ win?.removeEventListener("keyup", this.onModifier);
14319
+ win?.removeEventListener("contextmenu", this.onTouchContextMenu, true);
13822
14320
  el.removeEventListener("pointerdown", this.onDown);
13823
14321
  el.removeEventListener("pointermove", this.onMove);
13824
14322
  el.removeEventListener("pointerup", this.onUp);
13825
14323
  el.removeEventListener("pointerleave", this.onLeave);
14324
+ el.removeEventListener("pointercancel", this.onCancel);
13826
14325
  el.removeEventListener("dblclick", this.onDblClick);
13827
14326
  el.removeEventListener("wheel", this.onWheel);
13828
14327
  el.removeEventListener("mousedown", this.onMiddleGuard);
13829
14328
  el.removeEventListener("auxclick", this.onMiddleGuard);
14329
+ this.cancelLongPress();
14330
+ this.touches.clear();
13830
14331
  this.el = null;
13831
14332
  }
13832
14333
  local(e) {
@@ -13850,6 +14351,59 @@ var InputController = class {
13850
14351
  snapMode(e) {
13851
14352
  return effectiveSnapMode(e.ctrlKey || e.metaKey, this.deps.drawingsSnapMode?.() ?? "off");
13852
14353
  }
14354
+ /** Arm the long-press timer for a data-area → crosshair or axis → sheet hold;
14355
+ * movement past TOUCH_SLOP or a second finger cancels it. */
14356
+ startLongPress(x, y, region) {
14357
+ this.cancelLongPress();
14358
+ this.lpTimer = setTimeout(() => {
14359
+ this.lpTimer = null;
14360
+ if (!this.dragging || this.region !== region) return;
14361
+ if (region === "data") {
14362
+ this.region = "crosshair";
14363
+ this.deps.onPointerMove(x, y);
14364
+ return;
14365
+ }
14366
+ this.region = "axis-sheet";
14367
+ this.moved = true;
14368
+ this.deps.onAxisLongPress?.(region, x, y);
14369
+ }, LONG_PRESS_MS);
14370
+ }
14371
+ cancelLongPress() {
14372
+ if (this.lpTimer !== null) clearTimeout(this.lpTimer);
14373
+ this.lpTimer = null;
14374
+ }
14375
+ /** Two fingers down: freeze the view and re-base the gesture as an anchored pinch. */
14376
+ beginPinch(e) {
14377
+ this.cancelLongPress();
14378
+ if (this.region === "crosshair") this.deps.onPointerMove(null, null);
14379
+ const coords = this.deps.getCoords();
14380
+ const vp = coords.getViewport();
14381
+ this.deps.apply(vp);
14382
+ const [a, b] = [...this.touches.values()];
14383
+ this.pinchStartDist = Math.max(1, Math.hypot(a.x - b.x, a.y - b.y));
14384
+ this.pinchStartBarSpacing = vp.barSpacing;
14385
+ this.pinchAnchorLogical = coords.xToLogical((a.x + b.x) / 2);
14386
+ this.region = "pinch";
14387
+ this.dragging = true;
14388
+ this.moved = true;
14389
+ this.vx = 0;
14390
+ this.capture(e.pointerId);
14391
+ }
14392
+ /** Track both fingers: distance ratio zooms, the live midpoint pans (anchor pinned under it). */
14393
+ applyPinch() {
14394
+ const coords = this.deps.getCoords();
14395
+ const pts = [...this.touches.values()];
14396
+ if (pts.length < 2) return;
14397
+ const [a, b] = pts;
14398
+ const barSpacing = pinchBarSpacing(this.pinchStartBarSpacing, this.pinchStartDist, Math.hypot(a.x - b.x, a.y - b.y));
14399
+ const vp = coords.getViewport();
14400
+ const pitchScale = vp.barSpacing > 0 ? coords.pxPerBar() / vp.barSpacing : 1;
14401
+ const midX = (a.x + b.x) / 2;
14402
+ this.deps.apply({
14403
+ barSpacing,
14404
+ rightOffset: pinchPinnedRightOffset(this.pinchAnchorLogical, coords.barCount, coords.width, midX, barSpacing * pitchScale)
14405
+ });
14406
+ }
13853
14407
  /** Capture defensively: a synthetic/already-released pointer can't be captured, and
13854
14408
  * the move/up pair still routes through the element listeners without it. */
13855
14409
  capture(pointerId) {
@@ -13858,6 +14412,35 @@ var InputController = class {
13858
14412
  } catch {
13859
14413
  }
13860
14414
  }
14415
+ /** Pair clean taps into a double-tap — the touch equivalent of dblclick (same
14416
+ * routing: axis taps reset that axis' view, a plot tap toggles pane maximize). */
14417
+ registerTap(t, x, y) {
14418
+ const paired = this.lastTapT > 0 && t - this.lastTapT <= DOUBLE_TAP_MS && Math.hypot(x - this.lastTapX, y - this.lastTapY) <= DOUBLE_TAP_SLOP;
14419
+ if (paired) {
14420
+ this.lastTapT = 0;
14421
+ this.doubleActivate(this.lastTapX, this.lastTapY);
14422
+ return;
14423
+ }
14424
+ this.lastTapT = t;
14425
+ this.lastTapX = x;
14426
+ this.lastTapY = y;
14427
+ }
14428
+ endGesture(e) {
14429
+ this.dragging = false;
14430
+ try {
14431
+ this.el?.releasePointerCapture(e.pointerId);
14432
+ } catch {
14433
+ }
14434
+ }
14435
+ /** Shared double-click / double-tap routing, by the region under the point. */
14436
+ doubleActivate(x, y) {
14437
+ if (this.deps.drawingsDblClick?.(x, y)) return;
14438
+ const region = this.regionAt(x, y);
14439
+ if (region === "price") this.deps.resetPriceScale(x, y);
14440
+ else if (region === "separator") this.deps.resetPaneSize(y);
14441
+ else if (region === "time") this.deps.resetView();
14442
+ else this.deps.dataDblClick(x, y);
14443
+ }
13861
14444
  };
13862
14445
 
13863
14446
  // src/renderers/native/core/KeyboardController.ts
@@ -14236,12 +14819,13 @@ var Canvas2dBackend = class {
14236
14819
  drawCandles(ctx, scene, bars, i0, i1, coords, pane, theme, barColors) {
14237
14820
  const spacing = coords.bodySpacing();
14238
14821
  const tier = candleTier(spacing);
14822
+ const paint = effectiveCandlePaint(scene.style.candle, scene.candleOverride, theme.upColor, theme.downColor);
14239
14823
  if (tier === "aggregate") {
14240
- this.drawCandlesAggregated(ctx, bars, i0, i1, coords, pane, theme.upColor, theme.downColor, barColors);
14824
+ this.drawCandlesAggregated(ctx, bars, i0, i1, coords, pane, paint.up, paint.down, barColors);
14241
14825
  return;
14242
14826
  }
14243
14827
  const drawBody = tier === "full";
14244
- const cs = scene.style.candle;
14828
+ const cs = paint.candle;
14245
14829
  const fading = this.candleStructureAlpha > this.candleBodyAlpha + 1e-3;
14246
14830
  for (let i = i0; i <= i1; i += 1) {
14247
14831
  const b = bars[i];
@@ -14249,7 +14833,7 @@ var Canvas2dBackend = class {
14249
14833
  const g = candleGeometry(coords.logicalToX(i), spacing, coords.dpr, this.candleBodyScale);
14250
14834
  const x = g.center;
14251
14835
  const up = b.close >= b.open;
14252
- const dir = up ? theme.upColor : theme.downColor;
14836
+ const dir = up ? paint.up : paint.down;
14253
14837
  const bc = barColors.get(b.time);
14254
14838
  const color = bc ?? dir;
14255
14839
  let top = 0;
@@ -15360,7 +15944,7 @@ var ChromeRenderer = class {
15360
15944
  constructor() {
15361
15945
  this.canvas = null;
15362
15946
  this.ctx = null;
15363
- // The color for axis tick labels — the stable chrome surface text, set each frame in render().
15947
+ // The color for axis tick labels — the host-passed surface text, set each frame in render().
15364
15948
  this.axisTextColor = DARK_THEME.textColor;
15365
15949
  // Shared Pine-drawing renderer (line/box/label/polyline/linefill); widthCache persists.
15366
15950
  this.drawScene = new DrawingSceneRenderer({ timeToLogical: () => 0, barAt: () => null, theme: {} });
@@ -15392,8 +15976,8 @@ var ChromeRenderer = class {
15392
15976
  return dr;
15393
15977
  }
15394
15978
  /** Clear the chrome canvas and draw drawings + axes + current-price line.
15395
- * `surface` (background + text) paints the axis-scale gutters so they keep the app
15396
- * chrome color and stay readable, rather than tracking the user-editable plot cosmetics.
15979
+ * `surface` (background + text) paints the axis-scale gutters the host passes the live
15980
+ * chart background so the scales read as part of the plot, with contrast-corrected text.
15397
15981
  * Falls back to the theme's own colors when no surface is supplied. */
15398
15982
  render(scene, coords, theme, surface) {
15399
15983
  const ctx = this.ctx;
@@ -15670,11 +16254,19 @@ var ChromeRenderer = class {
15670
16254
  const y = dataH + (fullH - dataH) / 2;
15671
16255
  const tr = coords.visibleTimeRange();
15672
16256
  const offset = tzOffsetMs((tr.from + tr.to) / 2, scene.timezone);
15673
- for (const tick of timeTicks(tr.from, tr.to, 8, offset)) {
15674
- const x = coords.timeToX(tick.time);
15675
- if (x < 20 || x > dataW - 20) continue;
15676
- ctx.fillText(tick.label, x, y);
15677
- }
16257
+ const target = Math.max(3, Math.min(8, Math.floor(dataW / 64)));
16258
+ const ticks = timeTicks(tr.from, tr.to, target, offset).map((tick) => ({ ...tick, x: coords.timeToX(tick.time), half: ctx.measureText(tick.label).width / 2 })).filter((tick) => tick.x >= 20 && tick.x <= dataW - 20);
16259
+ const GAP = 12;
16260
+ const placed = [];
16261
+ const put = (tick) => {
16262
+ const l = tick.x - tick.half;
16263
+ const r = tick.x + tick.half;
16264
+ if (!placed.every((p) => r + GAP <= p.l || l - GAP >= p.r)) return;
16265
+ placed.push({ l, r });
16266
+ ctx.fillText(tick.label, tick.x, y);
16267
+ };
16268
+ for (const tick of ticks) if (tick.major) put(tick);
16269
+ for (const tick of ticks) if (!tick.major) put(tick);
15678
16270
  ctx.textAlign = "start";
15679
16271
  }
15680
16272
  };
@@ -15776,10 +16368,10 @@ var CrosshairRenderer = class {
15776
16368
  }
15777
16369
  /** The synced ghost: a dimmed vertical line at the bar the renderer resolved as
15778
16370
  * CONTAINING the foreign time (+ horizontal line when a comparable price came
15779
- * along), with that bar's time chip in this chart's own timezone. The snap
15780
- * happened upstream (`externalCrossPx`, floor-to-containing-bar) this method
15781
- * only draws. No price chip the ghost answers "when", the local crosshair
15782
- * answers "where". */
16371
+ * along), with that bar's time chip in this chart's own timezone and — when the
16372
+ * level resolved the price chip on the right axis. The snap happened upstream
16373
+ * (`externalCrossPx`, floor-to-containing-bar)this method only draws. Chips
16374
+ * render slightly dimmed so the ghost still reads as foreign. */
15783
16375
  drawExternal(ctx, ext, scene, coords, theme) {
15784
16376
  const cs = scene.style.crosshair;
15785
16377
  const dataW = coords.width;
@@ -15803,7 +16395,20 @@ var CrosshairRenderer = class {
15803
16395
  ctx.setLineDash([]);
15804
16396
  ctx.lineWidth = 1;
15805
16397
  ctx.globalAlpha = 0.8;
15806
- this.chip(ctx, x, dataH + 1, formatStamp(ext.time, scene.timezone), cs.labelBackground ?? theme.borderColor, "center", true, theme.background);
16398
+ const chipBg = cs.labelBackground ?? theme.borderColor;
16399
+ if (ext.y != null && ext.price != null) {
16400
+ let pane;
16401
+ for (const p of scene.panes.values()) {
16402
+ if (ext.y >= p.bounds.top && ext.y <= p.bounds.top + p.bounds.height) {
16403
+ pane = p;
16404
+ break;
16405
+ }
16406
+ }
16407
+ if (pane) {
16408
+ this.chip(ctx, dataW + 1, ext.y, formatAxisValue(pane.scale, pane.bounds.height, ext.price, percentScaleFor(scene, pane), scene.priceMintick, pane.axisFormat), chipBg, "left", false, theme.background);
16409
+ }
16410
+ }
16411
+ this.chip(ctx, x, dataH + 1, formatStamp(ext.time, scene.timezone), chipBg, "center", true, theme.background);
15807
16412
  ctx.globalAlpha = 1;
15808
16413
  }
15809
16414
  /** A soft band + a brighter crisp center line over the hovered separator, so it reads as
@@ -16164,6 +16769,7 @@ function colorField(theme, getVal, onVal) {
16164
16769
  swatch.style.background = `linear-gradient(${v}, ${v}), ${CHECKER2}`;
16165
16770
  };
16166
16771
  paint();
16772
+ trigger.addEventListener("vela-sync", paint);
16167
16773
  trigger.addEventListener("click", (e) => {
16168
16774
  e.stopPropagation();
16169
16775
  if (openPopover && openPopover.trigger === trigger) {
@@ -16199,6 +16805,101 @@ function closeColorPopover() {
16199
16805
  closeOpen();
16200
16806
  }
16201
16807
 
16808
+ // src/renderers/native/chrome/WidthField.ts
16809
+ var WIDTH_FIELD_OPTIONS = [1, 2, 3, 4, 5];
16810
+ var STYLE_ID3 = "vela-width-field";
16811
+ function ensureStyles3() {
16812
+ if (typeof document === "undefined" || document.getElementById(STYLE_ID3)) return;
16813
+ const st = document.createElement("style");
16814
+ st.id = STYLE_ID3;
16815
+ st.textContent = `
16816
+ .vela-width-field{height:28px;padding:0 8px;border:1px solid var(--vela-border-strong);border-radius:var(--vela-radius-sm);background:var(--vela-surface-elev);cursor:pointer;display:inline-flex;align-items:center;gap:8px;flex:none;color:var(--vela-fg);font-family:inherit;outline:none;}
16817
+ .vela-width-field:hover{border-color:var(--vela-fg-muted);}
16818
+ .vela-width-field-pop{position:fixed;z-index:6000;background:var(--vela-surface-overlay);border:1px solid var(--vela-border);border-radius:var(--vela-radius-lg);box-shadow:var(--vela-shadow);padding:4px;display:flex;flex-direction:column;gap:1px;color:var(--vela-fg);}
16819
+ .vela-width-field-item{display:flex;align-items:center;gap:8px;min-width:96px;padding:5px 8px;border:none;border-radius:5px;background:transparent;color:inherit;cursor:pointer;text-align:left;font:inherit;}
16820
+ .vela-width-field-item:hover{background:var(--vela-hover-strong);}
16821
+ .vela-width-field-item[data-active='1']{background:var(--vela-active);}
16822
+ `;
16823
+ document.head.appendChild(st);
16824
+ }
16825
+ function lineGlyph(width) {
16826
+ return `<svg width="22" height="14" viewBox="0 0 22 14" fill="none"><line x1="2" y1="7" x2="20" y2="7" stroke="currentColor" stroke-width="${width}" stroke-linecap="round"/></svg>`;
16827
+ }
16828
+ var CHEVRON = `<svg width="8" height="5" viewBox="0 0 8 5" fill="none"><path d="M1 1l3 3 3-3" stroke="#868a96" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>`;
16829
+ var openPopover2 = null;
16830
+ function closeOpen2() {
16831
+ if (!openPopover2) return;
16832
+ document.removeEventListener("pointerdown", openPopover2.onOutside, true);
16833
+ window.removeEventListener("resize", openPopover2.reflow, true);
16834
+ openPopover2.el.remove();
16835
+ openPopover2 = null;
16836
+ }
16837
+ function place2(pop, trigger) {
16838
+ const r = trigger.getBoundingClientRect();
16839
+ const w = pop.offsetWidth;
16840
+ const h = pop.offsetHeight;
16841
+ let left = r.right - w;
16842
+ let top = r.bottom + 6;
16843
+ if (left < 6) left = 6;
16844
+ if (left + w > window.innerWidth - 6) left = window.innerWidth - w - 6;
16845
+ if (top + h > window.innerHeight - 6) top = r.top - h - 6;
16846
+ if (top < 6) top = 6;
16847
+ pop.style.left = `${Math.round(left)}px`;
16848
+ pop.style.top = `${Math.round(top)}px`;
16849
+ }
16850
+ function widthField(theme, getVal, onVal) {
16851
+ ensureStyles3();
16852
+ const trigger = document.createElement("button");
16853
+ trigger.type = "button";
16854
+ trigger.className = "vela-width-field";
16855
+ const paint = () => {
16856
+ trigger.innerHTML = `<span style="display:flex;">${lineGlyph(getVal())}</span><span style="display:flex;">${CHEVRON}</span>`;
16857
+ };
16858
+ paint();
16859
+ trigger.addEventListener("click", (e) => {
16860
+ e.stopPropagation();
16861
+ if (openPopover2 && openPopover2.trigger === trigger) {
16862
+ closeOpen2();
16863
+ return;
16864
+ }
16865
+ closeOpen2();
16866
+ const pop = document.createElement("div");
16867
+ pop.className = "vela-width-field-pop";
16868
+ applyChromeTokens(pop, theme);
16869
+ pop.style.font = `var(--vela-font-size-md) ${theme.fontFamily}`;
16870
+ pop.addEventListener("pointerdown", (ev) => ev.stopPropagation());
16871
+ for (const w of WIDTH_FIELD_OPTIONS) {
16872
+ const item = document.createElement("button");
16873
+ item.type = "button";
16874
+ item.className = "vela-width-field-item";
16875
+ item.dataset.active = w === getVal() ? "1" : "0";
16876
+ item.innerHTML = `<span style="display:flex;flex:none;">${lineGlyph(w)}</span><span style="flex:1;font-variant-numeric:tabular-nums;">${w}px</span>`;
16877
+ item.addEventListener("click", (ev) => {
16878
+ ev.stopPropagation();
16879
+ closeOpen2();
16880
+ onVal(w);
16881
+ paint();
16882
+ });
16883
+ pop.appendChild(item);
16884
+ }
16885
+ document.body.appendChild(pop);
16886
+ place2(pop, trigger);
16887
+ const onOutside = (ev) => {
16888
+ const t = ev.target;
16889
+ if (pop.contains(t) || trigger.contains(t)) return;
16890
+ closeOpen2();
16891
+ };
16892
+ const reflow = () => place2(pop, trigger);
16893
+ setTimeout(() => document.addEventListener("pointerdown", onOutside, true), 0);
16894
+ window.addEventListener("resize", reflow, true);
16895
+ openPopover2 = { el: pop, trigger, onOutside, reflow };
16896
+ });
16897
+ return trigger;
16898
+ }
16899
+ function closeWidthPopover() {
16900
+ closeOpen2();
16901
+ }
16902
+
16202
16903
  // src/renderers/native/chrome/SettingsDialog.ts
16203
16904
  var BUILTIN_STYLE_LABELS = {
16204
16905
  candles: "Candles",
@@ -16250,6 +16951,61 @@ function ensureControlStyles() {
16250
16951
  .vela-sd-btn:hover{background:var(--vela-hover);border-color:var(--vela-border-strong);color:var(--vela-fg-bright);}
16251
16952
  .vela-sd-close{cursor:pointer;display:inline-flex;align-items:center;justify-content:center;background:transparent;border:none;color:var(--vela-fg-muted);line-height:0;width:30px;height:30px;border-radius:var(--vela-radius-sm);transition:background var(--vela-dur-fast) ease,color var(--vela-dur-fast) ease;}
16252
16953
  .vela-sd-close:hover{background:var(--vela-hover);color:var(--vela-fg-bright);}
16954
+ /* Rows/blocks gated away by chart-type conditions, TOC filters, or the instance strip.
16955
+ !important: the pane grid rewrites inline display ('contents') AFTER the initial
16956
+ visibility pass, so a class must win. */
16957
+ .vela-sd-hide{display:none !important;}
16958
+ /* Indented rail sub-entry (a chart-type section's subsection tab). */
16959
+ .vela-sd-tab-sub{padding-left:28px;font-weight:600;font-size:12px;}
16960
+ /* Instance strip: a tab per present instance (label, \u2715 on the active removable one)
16961
+ and a dashed + that turns on the next absent instance. The rule under it separates
16962
+ the strip from the instance's TOC + rows area. */
16963
+ .vela-sd-itabs{display:flex;align-items:center;gap:6px;flex-wrap:wrap;margin:14px 0 0;padding-bottom:12px;border-bottom:1px solid var(--vela-border);}
16964
+ .vela-sd-itab{display:inline-flex;align-items:center;gap:7px;height:30px;padding:0 11px;background:transparent;border:1px solid var(--vela-border);border-radius:var(--vela-radius-md);color:var(--vela-fg-muted);font-family:inherit;font-size:var(--vela-font-size-md);font-weight:600;cursor:pointer;transition:background var(--vela-dur-fast) ease,color var(--vela-dur-fast) ease,border-color var(--vela-dur-fast) ease;}
16965
+ .vela-sd-itab:hover{background:var(--vela-hover);color:var(--vela-fg-bright);}
16966
+ .vela-sd-itab.on{background:var(--vela-active);color:var(--vela-fg-bright);border-color:var(--vela-border-strong);}
16967
+ .vela-sd-ix{display:inline-flex;align-items:center;justify-content:center;width:16px;height:16px;margin-right:-4px;border-radius:var(--vela-radius-sm);color:var(--vela-fg-muted);font-size:10px;line-height:1;}
16968
+ .vela-sd-ix:hover{background:var(--vela-hover);color:var(--vela-fg-bright);}
16969
+ .vela-sd-itab-add{border-style:dashed;min-width:30px;justify-content:center;padding:0;}
16970
+ /* Structured pane: group TOC column + rows column, TOP-ALIGNED (the shared padding-top
16971
+ lives on the wrap, never on one column). The TOC sticks while the pane scrolls; the
16972
+ vertical rule sits on the rows column so it spans the full content height. When every
16973
+ group gates out the TOC hides and .no-toc drops the rule with it. */
16974
+ .vela-sd-struct{display:flex;align-items:flex-start;padding-top:14px;}
16975
+ .vela-sd-toc{position:sticky;top:0;flex:0 0 auto;min-width:104px;display:flex;flex-direction:column;gap:2px;padding:2px 14px 0 0;}
16976
+ .vela-sd-struct>[data-sd-rows-host]{border-left:1px solid var(--vela-border);padding-left:18px;}
16977
+ .vela-sd-struct.no-toc>[data-sd-rows-host]{border-left:none;padding-left:0;}
16978
+ .vela-sd-toc-btn{text-align:left;padding:6px 10px;background:transparent;border:none;border-radius:var(--vela-radius-sm);color:var(--vela-fg-muted);font-family:inherit;font-size:12px;font-weight:600;cursor:pointer;transition:background var(--vela-dur-fast) ease,color var(--vela-dur-fast) ease;}
16979
+ .vela-sd-toc-btn:hover{background:var(--vela-hover);color:var(--vela-fg-bright);}
16980
+ .vela-sd-toc-btn.on{background:var(--vela-active);color:var(--vela-fg-bright);}
16981
+ /* Soft-disable: a subsection's enableKey is off \u2014 rows stay visible (browseable) but
16982
+ muted and non-interactive. Applied to each row's children so it survives display:contents;
16983
+ !important beats the inline opacity on labels. */
16984
+ .vela-sd-soft>*{opacity:0.4 !important;pointer-events:none !important;}
16985
+ /* \u2500\u2500 mobile presentation (.vela-sd-mobile on the scrim; structural sizes are inline in open()) \u2500\u2500
16986
+ The tab rail becomes a burger-opened overlay sidebar; the group TOC becomes a sticky
16987
+ row of horizontally scrollable tabs; the instance strip scrolls instead of wrapping;
16988
+ controls grow to touch size and labels may wrap (the grid gives them a flexible track). */
16989
+ .vela-sd-mobile .vela-sd-burger{cursor:pointer;display:inline-flex;align-items:center;justify-content:center;background:transparent;border:none;color:var(--vela-fg);line-height:0;width:36px;height:36px;border-radius:var(--vela-radius-sm);margin-right:2px;}
16990
+ .vela-sd-mobile .vela-sd-burger:active{background:var(--vela-hover);}
16991
+ .vela-sd-mobile .vela-sd-railscrim{position:absolute;inset:0;z-index:2;background:var(--vela-backdrop);display:none;}
16992
+ .vela-sd-mobile .vela-sd-railscrim.open{display:block;}
16993
+ .vela-sd-mobile .vela-sd-rail{position:absolute;left:0;top:0;bottom:0;z-index:3;width:min(240px,80%);box-sizing:border-box;background:var(--vela-surface);border-right:1px solid var(--vela-border);box-shadow:var(--vela-shadow-dialog);transform:translateX(-105%);transition:transform var(--vela-dur-med) var(--vela-ease);}
16994
+ .vela-sd-mobile .vela-sd-rail.open{transform:translateX(0);}
16995
+ .vela-sd-mobile .vela-sd-tab{padding:12px;}
16996
+ .vela-sd-mobile .vela-sd-struct{flex-direction:column;padding-top:8px;}
16997
+ .vela-sd-mobile .vela-sd-toc{position:sticky;top:0;z-index:1;background:var(--vela-surface);flex-direction:row;overflow-x:auto;scrollbar-width:none;min-width:0;width:100%;box-sizing:border-box;gap:4px;padding:2px 0 8px;}
16998
+ .vela-sd-mobile .vela-sd-toc::-webkit-scrollbar{display:none;}
16999
+ .vela-sd-mobile .vela-sd-toc-btn{flex:none;padding:8px 12px;font-size:13px;}
17000
+ .vela-sd-mobile .vela-sd-struct>[data-sd-rows-host]{border-left:none;padding-left:0;width:100%;}
17001
+ .vela-sd-mobile .vela-sd-itabs{flex-wrap:nowrap;overflow-x:auto;scrollbar-width:none;}
17002
+ .vela-sd-mobile .vela-sd-itabs::-webkit-scrollbar{display:none;}
17003
+ .vela-sd-mobile .vela-sd-itab{height:36px;flex:none;}
17004
+ .vela-sd-mobile .vela-sd-check{width:22px;height:22px;}
17005
+ .vela-sd-mobile .vela-sd-select,.vela-sd-mobile .vela-sd-number{height:34px;}
17006
+ .vela-sd-mobile .vela-sd-close{width:40px;height:40px;}
17007
+ .vela-sd-mobile .vela-sd-btn{height:38px;}
17008
+ .vela-sd-mobile .vela-sd-row span,.vela-sd-mobile .vela-sd-bool span{white-space:normal !important;}
16253
17009
  `;
16254
17010
  document.head.appendChild(st);
16255
17011
  }
@@ -16270,8 +17026,14 @@ var SettingsDialog = class {
16270
17026
  this.themeControl = null;
16271
17027
  /** The built tabs, by title — how `showSection` reaches a pane while the dialog is open. */
16272
17028
  this.tabs = [];
17029
+ /** Active instance-strip tab per chart type — remembered across dialog rebuilds. */
17030
+ this.typeActiveInstance = /* @__PURE__ */ new Map();
17031
+ /** Active TOC group per structured pane (`<typeId>/<pane>` → group label). */
17032
+ this.typeActiveGroup = /* @__PURE__ */ new Map();
16273
17033
  /** The tab currently shown, so a theme change (which rebuilds) lands back on it. */
16274
17034
  this.activeSection = null;
17035
+ /** Mobile chrome: fullscreen card, burger-opened section sidebar, TOC as top tabs. */
17036
+ this.mobileLayout = false;
16275
17037
  if (getComputedStyle(container).position === "static") container.style.position = "relative";
16276
17038
  }
16277
17039
  /** Host-app sections (e.g. the widget's Status line tab) — re-shown on next open. */
@@ -16290,15 +17052,27 @@ var SettingsDialog = class {
16290
17052
  }
16291
17053
  setTheme(theme) {
16292
17054
  this.theme = theme;
16293
- if (this.root) {
16294
- const cfg = this.config;
16295
- const oc = this.onChange;
16296
- const oi = this.onImport;
16297
- const orst = this.onReset;
16298
- const section = this.activeSection;
16299
- this.close();
16300
- if (cfg && oc) this.open(cfg, oc, oi ?? void 0, orst ?? void 0, section ?? void 0);
16301
- }
17055
+ this.reopenIfLive();
17056
+ }
17057
+ /** The host shell's chrome size class — mobile presents the dialog fullscreen with
17058
+ * the tab rail behind a burger sidebar and the TOC as top tabs. An open dialog
17059
+ * re-presents in place, on the same tab. */
17060
+ setLayoutMode(mode) {
17061
+ const mobile = mode === "mobile";
17062
+ if (mobile === this.mobileLayout) return;
17063
+ this.mobileLayout = mobile;
17064
+ this.reopenIfLive();
17065
+ }
17066
+ /** Rebuild an open dialog in place (theme swap, layout-mode flip) on the same tab. */
17067
+ reopenIfLive() {
17068
+ if (!this.root) return;
17069
+ const cfg = this.config;
17070
+ const oc = this.onChange;
17071
+ const oi = this.onImport;
17072
+ const orst = this.onReset;
17073
+ const section = this.activeSection;
17074
+ this.close();
17075
+ if (cfg && oc) this.open(cfg, oc, oi ?? void 0, orst ?? void 0, section ?? void 0);
16302
17076
  }
16303
17077
  isOpen() {
16304
17078
  return this.root !== null;
@@ -16320,18 +17094,37 @@ var SettingsDialog = class {
16320
17094
  this.onImport = onImport ?? null;
16321
17095
  this.onReset = onReset ?? null;
16322
17096
  ensureControlStyles();
17097
+ const mobile = this.mobileLayout;
16323
17098
  const scrim = document.createElement("div");
16324
- scrim.style.cssText = "position:absolute;inset:0;z-index:21;display:flex;align-items:flex-start;justify-content:center;background:transparent;padding-top:8vh;pointer-events:auto;";
17099
+ scrim.style.cssText = "position:absolute;inset:0;z-index:var(--vela-z-dialog);display:flex;align-items:flex-start;justify-content:center;background:transparent;padding-top:8vh;pointer-events:auto;";
17100
+ if (mobile) {
17101
+ scrim.classList.add("vela-sd-mobile");
17102
+ scrim.style.paddingTop = "0";
17103
+ scrim.style.alignItems = "stretch";
17104
+ }
16325
17105
  scrim.addEventListener("mousedown", (e) => {
16326
17106
  if (e.target === scrim) this.close();
16327
17107
  });
16328
17108
  const dlg = document.createElement("div");
16329
17109
  dlg.style.cssText = `width:fit-content;min-width:min(560px,94vw);max-width:min(720px,94vw);max-height:70vh;display:flex;flex-direction:column;background:${SETTINGS_SURFACE};border:1px solid ${SETTINGS_BORDER};border-radius:var(--vela-radius-lg);box-shadow:var(--vela-shadow-dialog);color:${SETTINGS_TEXT};font:13px var(--vela-font);overflow:hidden;cursor:default;`;
17110
+ if (mobile) dlg.style.cssText += "width:100%;min-width:0;max-width:none;max-height:none;flex:1 1 auto;border:none;border-radius:0;";
16330
17111
  const header = document.createElement("div");
16331
17112
  header.style.cssText = `display:flex;justify-content:space-between;align-items:center;padding:9px 9px 9px 16px;border-bottom:1px solid ${SETTINGS_BORDER};flex:0 0 auto;user-select:none;`;
17113
+ if (mobile) header.style.paddingLeft = "8px";
16332
17114
  const hTitle = document.createElement("span");
16333
17115
  hTitle.textContent = "Chart settings";
16334
17116
  hTitle.style.cssText = "font-size:17px;font-weight:600;letter-spacing:0.2px;";
17117
+ if (mobile) hTitle.style.cssText += "flex:1 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;";
17118
+ let toggleRail = null;
17119
+ if (mobile) {
17120
+ const burger = document.createElement("button");
17121
+ burger.type = "button";
17122
+ burger.className = "vela-sd-burger";
17123
+ burger.innerHTML = iconAt("burger", 16);
17124
+ burger.title = "Sections";
17125
+ burger.addEventListener("click", () => toggleRail?.());
17126
+ header.append(burger);
17127
+ }
16335
17128
  const closeBtn = document.createElement("button");
16336
17129
  closeBtn.type = "button";
16337
17130
  closeBtn.innerHTML = iconAt("close", 15);
@@ -16339,8 +17132,8 @@ var SettingsDialog = class {
16339
17132
  closeBtn.className = "vela-sd-close";
16340
17133
  closeBtn.addEventListener("click", () => this.close());
16341
17134
  header.append(hTitle, closeBtn);
16342
- header.style.cursor = "move";
16343
- {
17135
+ if (!mobile) {
17136
+ header.style.cursor = "move";
16344
17137
  let sx = 0, sy = 0, ox = 0, oy = 0, dragging = false;
16345
17138
  header.addEventListener("pointerdown", (e) => {
16346
17139
  if (e.target?.closest(".vela-sd-close")) return;
@@ -16431,6 +17224,29 @@ var SettingsDialog = class {
16431
17224
  baseline.append(this.numberRow("Width", config.baseline.width, 1, 10, 1, (v) => this.emit({ baseline: { width: v } })));
16432
17225
  groups.baseline = baseline;
16433
17226
  body.append(baseline);
17227
+ for (const def of chartTypes()) {
17228
+ if (!hasOwnCandlePaint(def.id)) continue;
17229
+ const bag = config.chartTypes[def.id] ?? {};
17230
+ const colorOf2 = (key, shared) => typeof bag[key] === "string" && bag[key] !== "" ? bag[key] : shared;
17231
+ const boolOf = (key, shared) => typeof bag[key] === "boolean" ? bag[key] : shared;
17232
+ const g = this.group();
17233
+ g.append(this.sectionTitle("Candles"));
17234
+ g.append(this.toggleRow("Body", boolOf("candleBodyVisible", config.candles.bodyVisible), (v) => this.emitType(def.id, "candleBodyVisible", v), [
17235
+ this.swatch(colorOf2("candleUpColor", config.candles.upColor), (v) => this.emitType(def.id, "candleUpColor", v)),
17236
+ this.swatch(colorOf2("candleDownColor", config.candles.downColor), (v) => this.emitType(def.id, "candleDownColor", v))
17237
+ ]));
17238
+ g.append(this.toggleRow("Borders", boolOf("candleBorderVisible", config.candles.borderVisible), (v) => this.emitType(def.id, "candleBorderVisible", v), [
17239
+ this.swatch(colorOf2("candleBorderUpColor", config.candles.borderUpColor), (v) => this.emitType(def.id, "candleBorderUpColor", v)),
17240
+ this.swatch(colorOf2("candleBorderDownColor", config.candles.borderDownColor), (v) => this.emitType(def.id, "candleBorderDownColor", v))
17241
+ ]));
17242
+ g.append(this.toggleRow("Wick", boolOf("candleWickVisible", config.candles.wickVisible), (v) => this.emitType(def.id, "candleWickVisible", v), [
17243
+ this.swatch(colorOf2("candleWickUpColor", config.candles.wickUpColor), (v) => this.emitType(def.id, "candleWickUpColor", v)),
17244
+ this.swatch(colorOf2("candleWickDownColor", config.candles.wickDownColor), (v) => this.emitType(def.id, "candleWickDownColor", v))
17245
+ ]));
17246
+ g.append(this.numberRow("Spacing", config.series.spacing, 0.1, 10, 0.1, (v) => this.emit({ series: { spacing: v } })));
17247
+ groups[def.id] = g;
17248
+ body.append(g);
17249
+ }
16434
17250
  showActive(config.series.style);
16435
17251
  body.append(this.sectionTitle("Time zone"));
16436
17252
  body.append(this.selectRowLabeled("Time zone", normalizeTimezone(config.timeScale.timezone), timezoneOptions(config.timeScale.timezone), (v) => this.emit({ timeScale: { timezone: v } })));
@@ -16445,7 +17261,16 @@ var SettingsDialog = class {
16445
17261
  }
16446
17262
  }
16447
17263
  };
17264
+ const renderChartTypeSections = (placement) => {
17265
+ for (const def of chartTypes()) {
17266
+ const typeSettings = def.settings;
17267
+ if (!typeSettings) continue;
17268
+ if ((typeSettings.placement ?? "end") !== placement) continue;
17269
+ this.chartTypeSection(def.id, typeSettings, config, body);
17270
+ }
17271
+ };
16448
17272
  renderHostSections("symbol");
17273
+ renderChartTypeSections("after-symbol");
16449
17274
  renderHostSections("after-symbol");
16450
17275
  body.append(this.section("Scales and lines"));
16451
17276
  body.append(this.sectionTitle("Price scale"));
@@ -16486,35 +17311,27 @@ var SettingsDialog = class {
16486
17311
  body.append(this.sectionTitle("Theme"));
16487
17312
  body.append(this.selectRow("Color theme", tc.current === "dark" ? "Dark" : "Light", ["Dark", "Light"], (v) => tc.onSelect(v === "Dark" ? "dark" : "light")));
16488
17313
  }
16489
- for (const def of chartTypes()) {
16490
- const typeSettings = def.settings;
16491
- if (!typeSettings) continue;
16492
- const marker = this.section(typeSettings.title);
16493
- marker.dataset.sdStyle = def.id;
16494
- marker.dataset.sdVisibility = typeSettings.visibility ?? "active";
16495
- body.append(marker);
16496
- const values = config.chartTypes[def.id] ?? {};
16497
- for (const r of typeSettings.rows) {
16498
- const current2 = values[r.key];
16499
- if (r.kind === "toggle") {
16500
- body.append(this.boolRow(r.label, typeof current2 === "boolean" ? current2 : r.defval, (v) => this.emitType(def.id, r.key, v)));
16501
- } else if (r.kind === "number") {
16502
- body.append(this.numberRow(r.label, typeof current2 === "number" ? current2 : r.defval, r.min ?? 0, r.max ?? 1e6, r.step ?? 1, (v) => this.emitType(def.id, r.key, v)));
16503
- } else if (r.kind === "color") {
16504
- body.append(this.colorRow(r.label, typeof current2 === "string" ? current2 : r.defval, (v) => this.emitType(def.id, r.key, v)));
16505
- } else {
16506
- body.append(this.selectRow(r.label, typeof current2 === "string" ? current2 : r.defval, [...r.options], (v) => this.emitType(def.id, r.key, v)));
16507
- }
16508
- }
16509
- }
17314
+ renderChartTypeSections("end");
16510
17315
  renderHostSections("end");
16511
17316
  const shell = document.createElement("div");
16512
- shell.style.cssText = "display:flex;min-height:360px;max-height:calc(70vh - 100px);flex:1 1 auto;";
17317
+ shell.style.cssText = mobile ? "display:flex;min-height:0;flex:1 1 auto;position:relative;overflow:hidden;" : "display:flex;min-height:360px;max-height:calc(70vh - 100px);flex:1 1 auto;";
16513
17318
  const rail = document.createElement("div");
16514
- rail.style.cssText = `flex:0 0 170px;display:flex;flex-direction:column;gap:2px;padding:10px 8px;border-right:1px solid ${SETTINGS_BORDER};overflow-y:auto;`;
17319
+ rail.className = "vela-sd-rail";
17320
+ rail.style.cssText = mobile ? "display:flex;flex-direction:column;gap:2px;padding:10px 8px;overflow-y:auto;" : `flex:0 0 170px;display:flex;flex-direction:column;gap:2px;padding:10px 8px;border-right:1px solid ${SETTINGS_BORDER};overflow-y:auto;`;
17321
+ let railScrim = null;
17322
+ if (mobile) {
17323
+ railScrim = document.createElement("div");
17324
+ railScrim.className = "vela-sd-railscrim";
17325
+ railScrim.addEventListener("click", () => toggleRail?.());
17326
+ toggleRail = (open) => {
17327
+ const on = open ?? !rail.classList.contains("open");
17328
+ rail.classList.toggle("open", on);
17329
+ railScrim?.classList.toggle("open", on);
17330
+ };
17331
+ }
16515
17332
  const paneHost = document.createElement("div");
16516
17333
  paneHost.className = "vela-sd-pane";
16517
- paneHost.style.cssText = "flex:1;overflow-y:auto;padding:6px 18px 14px;";
17334
+ paneHost.style.cssText = mobile ? "flex:1;min-width:0;overflow-y:auto;padding:6px 14px calc(14px + env(safe-area-inset-bottom, 0px));" : "flex:1;overflow-y:auto;padding:6px 18px 14px;";
16518
17335
  const panes = [];
16519
17336
  let current = null;
16520
17337
  for (const child of [...body.children]) {
@@ -16525,7 +17342,7 @@ var SettingsDialog = class {
16525
17342
  const tab = document.createElement("button");
16526
17343
  tab.type = "button";
16527
17344
  tab.textContent = title;
16528
- tab.className = "vela-sd-tab";
17345
+ tab.className = "vela-sd-tab" + (child.dataset.sdSub !== void 0 ? " vela-sd-tab-sub" : "");
16529
17346
  panes.push({ title, el, tab, style: child.dataset.sdStyle, visibility: child.dataset.sdVisibility });
16530
17347
  current = el;
16531
17348
  child.remove();
@@ -16549,6 +17366,10 @@ var SettingsDialog = class {
16549
17366
  p.tab.classList.toggle("on", i === idx);
16550
17367
  });
16551
17368
  this.activeSection = panes[idx]?.title ?? null;
17369
+ if (mobile) {
17370
+ hTitle.textContent = panes[idx]?.title ?? "Chart settings";
17371
+ toggleRail?.(false);
17372
+ }
16552
17373
  };
16553
17374
  panes.forEach((p, i) => {
16554
17375
  p.tab.addEventListener("click", () => activate(i));
@@ -16556,19 +17377,28 @@ var SettingsDialog = class {
16556
17377
  paneHost.appendChild(p.el);
16557
17378
  });
16558
17379
  this.tabs = panes.map((p, i) => ({ title: p.title, show: () => activate(i) }));
16559
- const wanted = section === void 0 ? -1 : panes.findIndex((p) => p.title.toLowerCase() === section.toLowerCase());
17380
+ const wanted = section !== void 0 ? panes.findIndex((p) => p.title.toLowerCase() === section.toLowerCase()) : panes.findIndex((p) => p.style === config.series.style && !p.tab.classList.contains("vela-sd-tab-sub"));
16560
17381
  activate(wanted >= 0 ? wanted : 0);
16561
17382
  this.syncTypeTabs?.(config.series.style);
16562
17383
  shell.append(rail, paneHost);
17384
+ if (railScrim) shell.append(railScrim);
16563
17385
  dlg.appendChild(shell);
16564
17386
  dlg.appendChild(this.footer(config));
16565
17387
  scrim.appendChild(dlg);
16566
17388
  this.container.appendChild(scrim);
16567
17389
  this.root = scrim;
16568
- for (const p of panes) this.layoutSettingsGrids(p.el, dlg);
17390
+ for (const p of panes) {
17391
+ const hosts = [...p.el.querySelectorAll("[data-sd-rows-host]")];
17392
+ if (hosts.length === 0) {
17393
+ this.layoutSettingsGrids(p.el, dlg);
17394
+ continue;
17395
+ }
17396
+ for (const h of hosts) this.layoutSettingsGrids(h, dlg);
17397
+ }
16569
17398
  }
16570
17399
  close() {
16571
17400
  closeColorPopover();
17401
+ closeWidthPopover();
16572
17402
  this.root?.remove();
16573
17403
  this.root = null;
16574
17404
  this.tabs = [];
@@ -16595,35 +17425,370 @@ var SettingsDialog = class {
16595
17425
  el.textContent = text;
16596
17426
  return el;
16597
17427
  }
16598
- /** Vertical breathing space between row clusters — grouping reads from whitespace,
16599
- * so no drawn rule. */
16600
- separator() {
16601
- const el = document.createElement("div");
16602
- el.className = "vela-sd-sep";
16603
- el.style.cssText = "height:14px;";
16604
- return el;
16605
- }
16606
17428
  /**
16607
- * One pane-wide control column (section-agnostic), sized to the longest setting
16608
- * title same idea as the indicator settings dialog's `minmax(..., auto)` label
16609
- * track, but shared across every section in the tab.
16610
- *
16611
- * `measureHost` must be a visible, in-document ancestor (the dialog): panes are
16612
- * `display:none` until selected, which would zero layout measurements.
17429
+ * One chart type's settings tab (and its subsection tabs), appended to the linear
17430
+ * `body` for the pane splitter to file. One live values BAG serves the whole section
17431
+ * instances and subsections share the per-type store, so a `when` gate anywhere
17432
+ * can reference a key edited anywhere else; every edit runs every registered
17433
+ * refresher.
16613
17434
  */
16614
- layoutSettingsGrids(pane, measureHost) {
16615
- if (pane.childElementCount === 0) return;
16616
- const rows = [...pane.querySelectorAll(".vela-sd-row")];
16617
- const probe2 = document.createElement("div");
16618
- probe2.style.cssText = "position:absolute;left:-9999px;top:0;visibility:hidden;display:flex;flex-direction:column;font:13px var(--vela-font);pointer-events:none;";
16619
- measureHost.appendChild(probe2);
16620
- let widest = 0;
16621
- for (const row of rows) {
16622
- const cell = row.children[0];
16623
- if (!cell) continue;
16624
- const clone = cell.cloneNode(true);
16625
- probe2.appendChild(clone);
16626
- widest = Math.max(widest, clone.getBoundingClientRect().width);
17435
+ chartTypeSection(typeId, section, config, body) {
17436
+ const marker = this.section(section.title);
17437
+ marker.dataset.sdStyle = typeId;
17438
+ marker.dataset.sdVisibility = section.visibility ?? "active";
17439
+ body.append(marker);
17440
+ const values = config.chartTypes[typeId] ?? {};
17441
+ const bag = {};
17442
+ const seed = (rows) => {
17443
+ for (const r of rows) {
17444
+ for (const k of settingsRowValueKeys(r)) {
17445
+ const v = values[k.key];
17446
+ bag[k.key] = typeof v === k.type ? v : k.defval;
17447
+ }
17448
+ }
17449
+ };
17450
+ if (section.rows) seed(section.rows);
17451
+ for (const inst of section.instances ?? []) seed(inst.rows);
17452
+ for (const sub of section.subsections ?? []) seed(sub.rows);
17453
+ for (const inst of section.instances ?? []) {
17454
+ if (!inst.enableKey || inst.enableKey in bag) continue;
17455
+ bag[inst.enableKey] = values[inst.enableKey] === true;
17456
+ }
17457
+ for (const sub of section.subsections ?? []) {
17458
+ if (!sub.enableKey || sub.enableKey in bag) continue;
17459
+ bag[sub.enableKey] = values[sub.enableKey] === true;
17460
+ }
17461
+ const refreshers = [];
17462
+ const put = (key, v) => {
17463
+ bag[key] = v;
17464
+ this.emitType(typeId, key, v);
17465
+ for (const r of refreshers) r();
17466
+ };
17467
+ if (section.instances && section.instances.length > 0) {
17468
+ body.append(this.instancesBlock(typeId, section.instances, bag, put, refreshers));
17469
+ } else if (section.rows) {
17470
+ if (section.layout === "grouped") body.append(this.groupedRows(`${typeId}/rows`, section.rows, bag, put, refreshers));
17471
+ else this.flatTypeRows(section.rows, bag, put, refreshers, body);
17472
+ }
17473
+ for (const sub of section.subsections ?? []) {
17474
+ const subMarker = this.section(sub.title);
17475
+ subMarker.dataset.sdStyle = typeId;
17476
+ subMarker.dataset.sdVisibility = section.visibility ?? "active";
17477
+ subMarker.dataset.sdSub = "1";
17478
+ body.append(subMarker);
17479
+ body.append(this.groupedRows(`${typeId}/${sub.title}`, sub.rows, bag, put, refreshers, sub.enableKey));
17480
+ }
17481
+ for (const r of refreshers) r();
17482
+ }
17483
+ /** The FLAT chart-type form: rows appended straight to the body (the pane grid wraps
17484
+ * them), headings/headers as inline group titles, `when` gates refreshed live. */
17485
+ flatTypeRows(rows, bag, put, refreshers, body) {
17486
+ const entries = [];
17487
+ for (const r of rows) {
17488
+ const el = r.kind === "heading" || r.kind === "header" ? this.sectionTitle(r.label) : this.typeRow(r, bag, put, refreshers);
17489
+ entries.push({ el, when: r.when });
17490
+ body.append(el);
17491
+ }
17492
+ refreshers.push(() => {
17493
+ for (const e of entries) e.el.classList.toggle("vela-sd-hide", !settingsRowVisible(e.when, bag));
17494
+ });
17495
+ }
17496
+ /**
17497
+ * One value row for a chart-type descriptor, writing through `put`. EVERY kind is
17498
+ * first reduced to the canonical composite shape ({@link normalizeSettingsRow}) and
17499
+ * rendered by the ONE path: optional leading toggle, then the ordered inline
17500
+ * controls in the control column. A control carrying its own `when` registers a
17501
+ * visibility refresher — it appears and disappears live as the bag changes,
17502
+ * independent of the row's gate — and is exempt from the toggle-off dim (it may
17503
+ * exist FOR the off state).
17504
+ */
17505
+ typeRow(r, bag, put, refreshers) {
17506
+ const n = normalizeSettingsRow(r);
17507
+ const controls = [];
17508
+ for (const c of n.controls) {
17509
+ const el = this.inlineControl(c, bag, put, n.toggle !== void 0);
17510
+ if (c.kind !== "hint") refreshers.push(() => el.dispatchEvent(new Event("vela-sync")));
17511
+ if (c.kind !== "hint" && c.when) {
17512
+ el.dataset.sdSelfGated = "1";
17513
+ const when = c.when;
17514
+ refreshers.push(() => {
17515
+ el.style.display = settingsRowVisible(when, bag) ? "" : "none";
17516
+ });
17517
+ }
17518
+ controls.push(el);
17519
+ }
17520
+ if (n.toggle) {
17521
+ const t = n.toggle;
17522
+ const el = this.toggleRow(n.label, bag[t.key], (v) => put(t.key, v), controls, () => bag[t.key] === true);
17523
+ refreshers.push(() => el.dispatchEvent(new Event("vela-sync")));
17524
+ return el;
17525
+ }
17526
+ return this.rowWith(n.label, controls);
17527
+ }
17528
+ /**
17529
+ * Build ONE inline control from its descriptor — the factory behind the composite
17530
+ * row path. `compact` narrows number inputs on toggle rows (the historical inline
17531
+ * width) while standalone rows keep the full-width input.
17532
+ */
17533
+ inlineControl(c, bag, put, compact) {
17534
+ if (c.kind === "hint") {
17535
+ const s = document.createElement("span");
17536
+ s.textContent = c.text;
17537
+ s.style.cssText = "color:var(--vela-fg-muted);";
17538
+ return s;
17539
+ }
17540
+ if (c.kind === "color") {
17541
+ const sw = this.swatch(bag[c.key], (v) => put(c.key, v), () => bag[c.key]);
17542
+ sw.title = c.label;
17543
+ return sw;
17544
+ }
17545
+ if (c.kind === "width") {
17546
+ let cur = typeof bag[c.key] === "number" ? bag[c.key] : c.defval;
17547
+ const wf = widthField(this.theme, () => cur, (v) => {
17548
+ cur = v;
17549
+ put(c.key, v);
17550
+ });
17551
+ wf.title = c.label;
17552
+ return wf;
17553
+ }
17554
+ if (c.kind === "select") {
17555
+ const sel = document.createElement("select");
17556
+ sel.className = "vela-sd-select";
17557
+ sel.style.cssText = "max-width:200px;flex:0 0 auto;";
17558
+ sel.title = c.label;
17559
+ const current2 = typeof bag[c.key] === "string" ? bag[c.key] : c.defval;
17560
+ for (const [val, lbl] of normalizeSelectOptions(c.options)) {
17561
+ const o = document.createElement("option");
17562
+ o.value = val;
17563
+ o.textContent = lbl;
17564
+ if (val === current2) o.selected = true;
17565
+ sel.appendChild(o);
17566
+ }
17567
+ sel.addEventListener("change", () => put(c.key, sel.value));
17568
+ sel.addEventListener("vela-sync", () => {
17569
+ sel.value = typeof bag[c.key] === "string" ? bag[c.key] : c.defval;
17570
+ });
17571
+ return sel;
17572
+ }
17573
+ const ni = document.createElement("input");
17574
+ ni.type = "number";
17575
+ ni.className = "vela-sd-number";
17576
+ if (compact) ni.style.width = "56px";
17577
+ if (c.min !== void 0) ni.min = String(c.min);
17578
+ if (c.max !== void 0) ni.max = String(c.max);
17579
+ ni.step = String(c.step ?? 1);
17580
+ ni.title = c.label;
17581
+ const current = typeof bag[c.key] === "number" ? bag[c.key] : c.defval;
17582
+ if (c.placeholder !== void 0) {
17583
+ ni.placeholder = c.placeholder;
17584
+ if (current !== c.defval) ni.value = String(current);
17585
+ ni.addEventListener("change", () => {
17586
+ const raw = ni.value.trim() === "" ? c.defval : Number(ni.value);
17587
+ const v = Number.isFinite(raw) ? Math.min(c.max ?? Infinity, Math.max(c.min ?? -Infinity, raw)) : c.defval;
17588
+ ni.value = v === c.defval ? "" : String(v);
17589
+ put(c.key, v);
17590
+ });
17591
+ } else {
17592
+ ni.value = String(current);
17593
+ ni.addEventListener("input", () => {
17594
+ const v = Number(ni.value);
17595
+ if (Number.isFinite(v)) put(c.key, v);
17596
+ });
17597
+ }
17598
+ return ni;
17599
+ }
17600
+ /**
17601
+ * The INSTANCE STRIP block: a tab per present instance (label, `×` on the active
17602
+ * removable one), a dashed `+` while an instance is still absent, and one
17603
+ * grouped-rows content per instance below — only the active tab's content shows.
17604
+ * Presence is the boolean at each instance's `enableKey`; the strip rebuilds on
17605
+ * every section edit, so gates elsewhere stay coherent.
17606
+ */
17607
+ instancesBlock(typeId, instances, bag, put, refreshers) {
17608
+ const wrap = document.createElement("div");
17609
+ const strip = document.createElement("div");
17610
+ strip.className = "vela-sd-itabs";
17611
+ if (instances.length > 1 || instances[0]?.enableKey !== void 0) wrap.append(strip);
17612
+ const contents = instances.map((inst, i) => {
17613
+ const content = this.groupedRows(`${typeId}/#${i}`, inst.rows, bag, put, refreshers);
17614
+ wrap.append(content);
17615
+ return content;
17616
+ });
17617
+ const present = (inst) => !inst.enableKey || bag[inst.enableKey] === true;
17618
+ const refresh = () => {
17619
+ let active = this.typeActiveInstance.get(typeId) ?? 0;
17620
+ if (!present(instances[active] ?? instances[0])) active = 0;
17621
+ this.typeActiveInstance.set(typeId, active);
17622
+ strip.replaceChildren();
17623
+ instances.forEach((inst, i) => {
17624
+ if (!present(inst)) return;
17625
+ const tab = document.createElement("button");
17626
+ tab.type = "button";
17627
+ tab.className = "vela-sd-itab" + (i === active ? " on" : "");
17628
+ const lbl = document.createElement("span");
17629
+ lbl.textContent = inst.label;
17630
+ tab.append(lbl);
17631
+ if (i === active && inst.enableKey) {
17632
+ const x = document.createElement("span");
17633
+ x.className = "vela-sd-ix";
17634
+ x.textContent = "\u2715";
17635
+ x.title = `Remove ${inst.label}`;
17636
+ x.addEventListener("click", (e) => {
17637
+ e.stopPropagation();
17638
+ this.typeActiveInstance.set(typeId, 0);
17639
+ put(inst.enableKey, false);
17640
+ });
17641
+ tab.append(x);
17642
+ }
17643
+ tab.addEventListener("click", () => {
17644
+ this.typeActiveInstance.set(typeId, i);
17645
+ refresh();
17646
+ });
17647
+ strip.append(tab);
17648
+ });
17649
+ const absent = instances.findIndex((inst) => !present(inst));
17650
+ if (absent >= 0) {
17651
+ const add = document.createElement("button");
17652
+ add.type = "button";
17653
+ add.className = "vela-sd-itab vela-sd-itab-add";
17654
+ add.textContent = "+";
17655
+ add.title = `Add ${instances[absent].label}`;
17656
+ add.addEventListener("click", () => {
17657
+ this.typeActiveInstance.set(typeId, absent);
17658
+ put(instances[absent].enableKey, true);
17659
+ });
17660
+ strip.append(add);
17661
+ }
17662
+ contents.forEach((c, i) => c.classList.toggle("vela-sd-hide", i !== active));
17663
+ };
17664
+ refreshers.push(refresh);
17665
+ return wrap;
17666
+ }
17667
+ /**
17668
+ * A GROUPED rows pane: a TOC column of the group labels (from `heading` rows) and
17669
+ * ONE rows host holding every row — the active group's rows show, the rest hide.
17670
+ * Rows BEFORE the first heading are the always block, visible above every group.
17671
+ * `header` rows stay in the rows column as in-group subgroup titles (not TOC
17672
+ * entries). A group whose value rows are all gated out (or whose heading's own
17673
+ * `when` fails) leaves the TOC; the whole TOC hides when no group is live. The
17674
+ * rows host is tagged for `layoutSettingsGrids`, keeping one shared label column
17675
+ * across all groups.
17676
+ *
17677
+ * `enableKey` (optional): while that bag boolean is false, every row except the one
17678
+ * whose key matches is soft-disabled (visible but grayed / non-interactive) so the
17679
+ * pane stays browseable with the feature off.
17680
+ */
17681
+ groupedRows(paneKey, rows, bag, put, refreshers, enableKey) {
17682
+ const wrap = document.createElement("div");
17683
+ wrap.className = "vela-sd-struct";
17684
+ const toc = document.createElement("div");
17685
+ toc.className = "vela-sd-toc";
17686
+ const rowsHost = document.createElement("div");
17687
+ rowsHost.dataset.sdRowsHost = "1";
17688
+ rowsHost.style.cssText = "flex:1 1 auto;min-width:0;";
17689
+ wrap.append(toc, rowsHost);
17690
+ const entries = [];
17691
+ const groups = [];
17692
+ const groupWhens = [];
17693
+ let g = -1;
17694
+ for (const r of rows) {
17695
+ if (r.kind === "heading") {
17696
+ g = groups.length;
17697
+ groups.push(r.label);
17698
+ groupWhens.push(r.when);
17699
+ continue;
17700
+ }
17701
+ if (r.kind === "header") {
17702
+ entries.push({ el: this.sectionTitle(r.label), when: r.when, group: g, header: true });
17703
+ rowsHost.append(entries[entries.length - 1].el);
17704
+ continue;
17705
+ }
17706
+ const el = this.typeRow(r, bag, put, refreshers);
17707
+ const key = r.kind === "row" ? r.toggle?.key : r.kind === "range" ? void 0 : r.key;
17708
+ entries.push({ el, when: r.when, group: g, key });
17709
+ rowsHost.append(el);
17710
+ }
17711
+ const refresh = () => {
17712
+ const live = groups.map((_, gi) => settingsRowVisible(groupWhens[gi], bag) && entries.some((e) => e.group === gi && !e.header && settingsRowVisible(e.when, bag)));
17713
+ let activeIdx = groups.indexOf(this.typeActiveGroup.get(paneKey) ?? "");
17714
+ if (activeIdx < 0 || !live[activeIdx]) activeIdx = live.findIndex(Boolean);
17715
+ if (activeIdx >= 0) this.typeActiveGroup.set(paneKey, groups[activeIdx]);
17716
+ toc.replaceChildren();
17717
+ groups.forEach((label, gi) => {
17718
+ if (!live[gi]) return;
17719
+ const b = document.createElement("button");
17720
+ b.type = "button";
17721
+ b.className = "vela-sd-toc-btn" + (gi === activeIdx ? " on" : "");
17722
+ b.textContent = label;
17723
+ b.addEventListener("click", () => {
17724
+ this.typeActiveGroup.set(paneKey, label);
17725
+ refresh();
17726
+ });
17727
+ toc.append(b);
17728
+ });
17729
+ const anyGroup = live.some(Boolean);
17730
+ toc.classList.toggle("vela-sd-hide", !anyGroup);
17731
+ wrap.classList.toggle("no-toc", !anyGroup);
17732
+ const enabled = !enableKey || bag[enableKey] === true;
17733
+ for (let i = 0; i < entries.length; i++) {
17734
+ const e = entries[i];
17735
+ let visible = (e.group === -1 || e.group === activeIdx) && settingsRowVisible(e.when, bag);
17736
+ if (visible && e.header) {
17737
+ let hasContent = false;
17738
+ for (let j = i + 1; j < entries.length; j++) {
17739
+ const n = entries[j];
17740
+ if (n.group !== e.group || n.header) break;
17741
+ if (settingsRowVisible(n.when, bag)) {
17742
+ hasContent = true;
17743
+ break;
17744
+ }
17745
+ }
17746
+ visible = hasContent;
17747
+ }
17748
+ e.el.classList.toggle("vela-sd-hide", !visible);
17749
+ e.el.classList.toggle("vela-sd-soft", visible && !enabled && e.key !== enableKey);
17750
+ }
17751
+ };
17752
+ refreshers.push(refresh);
17753
+ return wrap;
17754
+ }
17755
+ /** Vertical breathing space between row clusters — grouping reads from whitespace,
17756
+ * so no drawn rule. */
17757
+ separator() {
17758
+ const el = document.createElement("div");
17759
+ el.className = "vela-sd-sep";
17760
+ el.style.cssText = "height:14px;";
17761
+ return el;
17762
+ }
17763
+ /**
17764
+ * One pane-wide control column (section-agnostic), sized to the longest setting
17765
+ * title — same idea as the indicator settings dialog's `minmax(..., auto)` label
17766
+ * track, but shared across every section in the tab.
17767
+ *
17768
+ * `measureHost` must be a visible, in-document ancestor (the dialog): panes are
17769
+ * `display:none` until selected, which would zero layout measurements.
17770
+ */
17771
+ layoutSettingsGrids(pane, measureHost) {
17772
+ if (pane.childElementCount === 0) return;
17773
+ if (this.mobileLayout) {
17774
+ const grid2 = document.createElement("div");
17775
+ grid2.style.cssText = "display:grid;grid-template-columns:minmax(0,1fr) max-content;align-items:center;column-gap:12px;row-gap:16px;";
17776
+ while (pane.firstChild) grid2.appendChild(pane.firstChild);
17777
+ pane.appendChild(grid2);
17778
+ this.prepareGridItems(grid2);
17779
+ return;
17780
+ }
17781
+ const rows = [...pane.querySelectorAll(".vela-sd-row")];
17782
+ const probe2 = document.createElement("div");
17783
+ probe2.style.cssText = "position:absolute;left:-9999px;top:0;visibility:hidden;display:flex;flex-direction:column;font:13px var(--vela-font);pointer-events:none;";
17784
+ measureHost.appendChild(probe2);
17785
+ let widest = 0;
17786
+ for (const row of rows) {
17787
+ const cell = row.children[0];
17788
+ if (!cell) continue;
17789
+ const clone = cell.cloneNode(true);
17790
+ probe2.appendChild(clone);
17791
+ widest = Math.max(widest, clone.getBoundingClientRect().width);
16627
17792
  }
16628
17793
  probe2.remove();
16629
17794
  const labelTrack = widest > 0 ? `${Math.ceil(widest)}px` : "max-content";
@@ -16649,9 +17814,11 @@ var SettingsDialog = class {
16649
17814
  }
16650
17815
  }
16651
17816
  /** A bare color swatch input (for toggle-row right groups / swatch pairs). */
16652
- swatch(value, onChange) {
17817
+ /** With `get`, the field reads its value live (and repaints on 'vela-sync')
17818
+ * duplicate-keyed rows stay honest when another row edits the shared key. */
17819
+ swatch(value, onChange, get) {
16653
17820
  let current = value;
16654
- return colorField(this.theme, () => current, (v) => {
17821
+ return colorField(this.theme, () => get ? get() : current, (v) => {
16655
17822
  current = v;
16656
17823
  onChange(v);
16657
17824
  });
@@ -16703,8 +17870,11 @@ var SettingsDialog = class {
16703
17870
  }
16704
17871
  /** An enable row: checkbox + label in the label column, dependent controls in the
16705
17872
  * shared control column; the control group dims and ignores input while the toggle
16706
- * is off. With no controls it reads like a plain toggle row (full-width in the grid). */
16707
- toggleRow(label, value, onToggle, controls) {
17873
+ * is off. With no controls it reads like a plain toggle row (full-width in the grid).
17874
+ * With `get`, the row re-reads its state on a 'vela-sync' event — the seam that
17875
+ * keeps DUPLICATE-KEYED rows honest (several `when`-gated rows over one bag key,
17876
+ * only one visible at a time; see `typeRow`). */
17877
+ toggleRow(label, value, onToggle, controls, get) {
16708
17878
  const wrap = document.createElement("div");
16709
17879
  const cb = document.createElement("button");
16710
17880
  cb.type = "button";
@@ -16724,6 +17894,12 @@ var SettingsDialog = class {
16724
17894
  wrap.style.cssText = "display:flex;align-items:center;gap:8px;min-height:22px;";
16725
17895
  wrap.append(cb, lbl);
16726
17896
  cb.addEventListener("click", () => onToggle(cbToggle()));
17897
+ if (get) {
17898
+ wrap.addEventListener("vela-sync", () => {
17899
+ checked = get();
17900
+ cb.classList.toggle("on", checked);
17901
+ });
17902
+ }
16727
17903
  return wrap;
16728
17904
  }
16729
17905
  wrap.className = "vela-sd-row";
@@ -16734,8 +17910,11 @@ var SettingsDialog = class {
16734
17910
  box.style.cssText = "display:flex;align-items:center;gap:6px;";
16735
17911
  for (const c of controls) box.appendChild(c);
16736
17912
  const syncDim = (on) => {
16737
- box.style.opacity = on ? "1" : "0.4";
16738
- box.style.pointerEvents = on ? "" : "none";
17913
+ for (const c of controls) {
17914
+ if (c.dataset.sdSelfGated === "1") continue;
17915
+ c.style.opacity = on ? "1" : "0.4";
17916
+ c.style.pointerEvents = on ? "" : "none";
17917
+ }
16739
17918
  };
16740
17919
  syncDim(value);
16741
17920
  cb.addEventListener("click", () => {
@@ -16743,6 +17922,13 @@ var SettingsDialog = class {
16743
17922
  onToggle(v);
16744
17923
  syncDim(v);
16745
17924
  });
17925
+ if (get) {
17926
+ wrap.addEventListener("vela-sync", () => {
17927
+ checked = get();
17928
+ cb.classList.toggle("on", checked);
17929
+ syncDim(checked);
17930
+ });
17931
+ }
16746
17932
  wrap.append(left, box);
16747
17933
  return wrap;
16748
17934
  }
@@ -16762,102 +17948,12 @@ var SettingsDialog = class {
16762
17948
  wrap.appendChild(ni);
16763
17949
  return wrap;
16764
17950
  }
16765
- /** Inline Auto/Manual row (Resolution, Text size): a mode dropdown + a value input greyed out in Auto. */
16766
- autoManualRow(label, isAuto, value, min, max, unit, onMode, onValue) {
16767
- const { wrap } = this.row(label);
16768
- const box = document.createElement("div");
16769
- box.style.cssText = "display:flex;align-items:center;gap:6px;flex:0 0 auto;";
16770
- const sel = document.createElement("select");
16771
- sel.className = "vela-sd-select";
16772
- for (const [val, lbl] of AUTO_MANUAL_OPTS) {
16773
- const o = document.createElement("option");
16774
- o.value = val;
16775
- o.textContent = lbl;
16776
- if (val === "auto" === isAuto) o.selected = true;
16777
- sel.appendChild(o);
16778
- }
16779
- const ni = document.createElement("input");
16780
- ni.type = "number";
16781
- ni.min = String(min);
16782
- ni.max = String(max);
16783
- ni.step = "1";
16784
- ni.value = String(value);
16785
- ni.className = "vela-sd-number";
16786
- const unitLbl = document.createElement("span");
16787
- unitLbl.textContent = unit;
16788
- unitLbl.style.cssText = "opacity:0.6;";
16789
- const syncDisabled = (auto) => {
16790
- ni.disabled = auto;
16791
- ni.style.opacity = auto ? "0.4" : "1";
16792
- };
16793
- syncDisabled(isAuto);
16794
- sel.addEventListener("change", () => {
16795
- const auto = sel.value === "auto";
16796
- onMode(auto);
16797
- syncDisabled(auto);
16798
- });
16799
- ni.addEventListener("input", () => {
16800
- const n = Number(ni.value);
16801
- if (Number.isFinite(n)) onValue(n);
16802
- });
16803
- box.append(sel, ni, unitLbl);
16804
- wrap.appendChild(box);
16805
- return wrap;
16806
- }
16807
- /** A row whose control area holds several inline controls (e.g. show + color + width). */
16808
- inlineRow(label, controls) {
16809
- const { wrap } = this.row(label);
16810
- const box = document.createElement("div");
16811
- box.style.cssText = "display:flex;align-items:center;gap:6px;flex:0 0 auto;";
16812
- for (const c of controls) box.appendChild(c);
16813
- wrap.appendChild(box);
16814
- return wrap;
16815
- }
16816
- /** A small dimmed hint span (e.g. the ≥ / ≤ between filter inputs). */
16817
- hint(text) {
16818
- const s = document.createElement("span");
16819
- s.textContent = text;
16820
- s.style.cssText = "opacity:0.5;font-size:12px;";
16821
- return s;
16822
- }
16823
- /** A bare color input (for inline groups). */
16824
- colorInput(value, onChange) {
16825
- const ci = document.createElement("input");
16826
- ci.type = "color";
16827
- ci.value = toHex6(value);
16828
- ci.style.cssText = "cursor:pointer;width:34px;height:22px;border:none;background:transparent;padding:0;flex:0 0 auto;";
16829
- ci.addEventListener("input", () => onChange(ci.value));
16830
- return ci;
16831
- }
16832
- /** A bare compact number input (for inline groups). */
16833
- numberInput(value, min, max, step, onChange) {
16834
- const ni = document.createElement("input");
16835
- ni.type = "number";
16836
- ni.value = String(value);
16837
- ni.min = String(min);
16838
- ni.max = String(max);
16839
- ni.step = String(step);
16840
- ni.className = "vela-sd-number";
16841
- ni.style.flex = "0 0 auto";
16842
- ni.addEventListener("input", () => {
16843
- const n = Number(ni.value);
16844
- if (Number.isFinite(n)) onChange(n);
16845
- });
16846
- return ni;
16847
- }
16848
- /** A small in-group heading (lighter than a top-level section divider). */
16849
- subheading(text) {
16850
- const el = document.createElement("div");
16851
- el.textContent = text;
16852
- el.style.cssText = "margin-top:4px;font-size:10.5px;font-weight:700;letter-spacing:0.03em;text-transform:uppercase;opacity:0.45;";
16853
- return el;
16854
- }
16855
17951
  /** A dropdown whose option values differ from their display labels. */
16856
17952
  selectRowLabeled(label, value, options, onChange) {
16857
17953
  const { wrap } = this.row(label);
16858
17954
  const sel = document.createElement("select");
16859
17955
  sel.className = "vela-sd-select";
16860
- sel.style.cssText = "max-width:140px;flex:0 0 auto;";
17956
+ sel.style.cssText = "max-width:200px;flex:0 0 auto;";
16861
17957
  for (const [val, lbl] of options) {
16862
17958
  const o = document.createElement("option");
16863
17959
  o.value = val;
@@ -16873,7 +17969,7 @@ var SettingsDialog = class {
16873
17969
  const { wrap } = this.row(label);
16874
17970
  const sel = document.createElement("select");
16875
17971
  sel.className = "vela-sd-select";
16876
- sel.style.cssText = "max-width:140px;flex:0 0 auto;";
17972
+ sel.style.cssText = "max-width:200px;flex:0 0 auto;";
16877
17973
  for (const opt of options) {
16878
17974
  const o = document.createElement("option");
16879
17975
  o.value = opt;
@@ -16897,19 +17993,10 @@ var SettingsDialog = class {
16897
17993
  foot.appendChild(resetBtn);
16898
17994
  return foot;
16899
17995
  }
16900
- button(label, onClick) {
16901
- const b = document.createElement("button");
16902
- b.type = "button";
16903
- b.textContent = label;
16904
- b.style.cssText = `cursor:pointer;flex:1 1 auto;${this.ctrlStyle()}padding:5px 8px;font-weight:600;`;
16905
- b.addEventListener("click", onClick);
16906
- return b;
16907
- }
16908
- ctrlStyle() {
16909
- return `background:var(--vela-surface-sunken);border:1px solid ${SETTINGS_BORDER};color:${SETTINGS_TEXT};border-radius:var(--vela-radius-sm);padding:3px 6px;font-size:var(--vela-font-size-md);font-family:inherit;outline:none;`;
16910
- }
16911
17996
  };
16912
- var AUTO_MANUAL_OPTS = [["auto", "Auto"], ["manual", "Manual"]];
17997
+ function normalizeSelectOptions(options) {
17998
+ return options.map((o) => typeof o === "string" ? [o, o] : o);
17999
+ }
16913
18000
  function timezoneOptions(current) {
16914
18001
  const options = TIMEZONES.map((t) => [t.value, tzMenuLabel(t.value, t.label)]);
16915
18002
  const normalized = normalizeTimezone(current);
@@ -17109,7 +18196,7 @@ var DrawingPainter = class {
17109
18196
  return;
17110
18197
  }
17111
18198
  if (d instanceof FixedRangeVolumeProfile) {
17112
- this.paintFixedRangeVp(ctx, d, proj);
18199
+ this.paintFixedRangeVp(ctx, d, proj, theme);
17113
18200
  this.paintLabel(ctx, d, proj, theme);
17114
18201
  return;
17115
18202
  }
@@ -17602,7 +18689,7 @@ var DrawingPainter = class {
17602
18689
  /** Paint a fixed-range volume profile: horizontal histogram rows (up/down split) anchored to
17603
18690
  * the left or right of the time span, optional VAH / VAL / POC levels across the range, and
17604
18691
  * optional developing POC / VA polylines. Recomputes from the two anchors on every paint. */
17605
- paintFixedRangeVp(ctx, d, proj) {
18692
+ paintFixedRangeVp(ctx, d, proj, theme) {
17606
18693
  const L = d.layout(proj);
17607
18694
  if (!L) return;
17608
18695
  const s = d.frvp;
@@ -17641,7 +18728,7 @@ var DrawingPainter = class {
17641
18728
  };
17642
18729
  hLine(s.showVah, s.vahColor, s.vahStyle, L.vahY);
17643
18730
  hLine(s.showVal, s.valColor, s.valStyle, L.valY);
17644
- hLine(s.showPoc, s.pocColor, s.pocStyle, L.pocY);
18731
+ hLine(s.showPoc, s.pocColor ?? contrastColor(theme.background), s.pocStyle, L.pocY);
17645
18732
  const devW = Math.max(1, w);
17646
18733
  if (s.showDevelopingPoc) {
17647
18734
  this.strokePolyline(ctx, L.developingPoc, {
@@ -18268,6 +19355,8 @@ function topDrawingAt(drawings, x, y, proj, tol = HIT_TOLERANCE) {
18268
19355
  // src/renderers/native/drawings/DrawingInteraction.ts
18269
19356
  var DRAG_SLOP2 = 3;
18270
19357
  var FREEHAND_SAMPLE_PX = 4;
19358
+ var ANGLE_SNAP_STEP = Math.PI / 4;
19359
+ var ANGLE_SNAP_TYPES = /* @__PURE__ */ new Set(["trendline", "ray", "extendedline", "infoline", "trendangle", "arrow"]);
18271
19360
  var DrawingInteraction = class {
18272
19361
  // magnet target for the ring
18273
19362
  constructor(deps) {
@@ -18308,6 +19397,27 @@ var DrawingInteraction = class {
18308
19397
  this.snapAt = changed ? { point: snapped, paneId } : null;
18309
19398
  return snapped;
18310
19399
  }
19400
+ /** Resolve a pixel to a data point with the segment angle locked to 45° steps around
19401
+ * `pivot` (Shift held on a line tool). Works in PIXEL space — the user reasons about
19402
+ * the angle they see, not about time/price units. The magnet is bypassed: snapping
19403
+ * the result to a candle afterwards would break the exact angle just enforced. */
19404
+ resolveAngleSnapped(pivot, paneId, x, y) {
19405
+ const proj = this.deps.projector();
19406
+ const px = proj.xOf(pivot.time);
19407
+ const py = proj.yOf(pivot.price, paneId);
19408
+ this.snapAt = null;
19409
+ if (py == null) return proj.pxToPoint(x, y, paneId);
19410
+ const r = Math.hypot(x - px, y - py);
19411
+ if (r < 1) return proj.pxToPoint(x, y, paneId);
19412
+ const angle = Math.round(Math.atan2(y - py, x - px) / ANGLE_SNAP_STEP) * ANGLE_SNAP_STEP;
19413
+ return proj.pxToPoint(px + r * Math.cos(angle), py + r * Math.sin(angle), paneId);
19414
+ }
19415
+ /** The pivot the free endpoint angle-snaps around while placing `draft`, or null when
19416
+ * the tool is not a two-point segment (or no anchor is committed yet). */
19417
+ placingPivot(draft) {
19418
+ if (!ANGLE_SNAP_TYPES.has(draft.type)) return null;
19419
+ return draft.anchors[draft.anchors.length - 1] ?? null;
19420
+ }
18311
19421
  /** Should the drawing layer win this press (vs pan)? Armed/placing/pressed, or over a drawing/handle. */
18312
19422
  claim(x, y) {
18313
19423
  if (this.deps.activeTool() != null || this.state.kind !== "idle") return true;
@@ -18329,12 +19439,15 @@ var DrawingInteraction = class {
18329
19439
  return this.state.kind === "pressed" && this.state.moved ? this.state.id : null;
18330
19440
  }
18331
19441
  /** A press: place the next anchor, or grab a drawing/handle (resolved as click or drag on release).
18332
- * `shift` over a drawing toggles it in/out of the selection (multi-select) instead of dragging. */
19442
+ * `shift` over a drawing toggles it in/out of the selection (multi-select) instead of dragging;
19443
+ * while placing a line tool it locks the segment angle to 45° steps. */
18333
19444
  down(x, y, mode = "off", shift = false) {
18334
19445
  const proj = this.deps.projector();
18335
19446
  if (this.state.kind === "placing") {
18336
19447
  if (this.state.draft.placementMode() !== "click") return;
18337
- this.state.draft.anchors.push(this.placementAnchor(this.state.draft, this.resolve(x, y, this.state.draft.paneId, mode)));
19448
+ const pivot = shift ? this.placingPivot(this.state.draft) : null;
19449
+ const point = pivot ? this.resolveAngleSnapped(pivot, this.state.draft.paneId, x, y) : this.resolve(x, y, this.state.draft.paneId, mode);
19450
+ this.state.draft.anchors.push(this.placementAnchor(this.state.draft, point));
18338
19451
  if (this.state.draft.anchors.length >= this.state.need) this.finalize();
18339
19452
  else this.deps.changed();
18340
19453
  return;
@@ -18359,8 +19472,9 @@ var DrawingInteraction = class {
18359
19472
  this.state = { kind: "pressed", id: hit.id, handle, grab: proj.pxToPoint(x, y, hit.paneId), orig: cloneAnchors(hit), px: x, py: y, moved: false };
18360
19473
  }
18361
19474
  }
18362
- /** Pointer move: advance the placing ghost, or live-preview a drag once past the slop. */
18363
- move(x, y, mode = "off") {
19475
+ /** Pointer move: advance the placing ghost, or live-preview a drag once past the slop.
19476
+ * `shift` on a line tool locks the segment angle to 45° steps (placing + handle drags). */
19477
+ move(x, y, mode = "off", shift = false) {
18364
19478
  if (this.state.kind === "idle") {
18365
19479
  const tool = this.deps.activeTool();
18366
19480
  const had = this.snapAt != null;
@@ -18374,7 +19488,8 @@ var DrawingInteraction = class {
18374
19488
  this.captureFreehand(x, y);
18375
19489
  return;
18376
19490
  }
18377
- this.state.cursor = this.resolve(x, y, this.state.draft.paneId, mode);
19491
+ const pivot = shift ? this.placingPivot(this.state.draft) : null;
19492
+ this.state.cursor = pivot ? this.resolveAngleSnapped(pivot, this.state.draft.paneId, x, y) : this.resolve(x, y, this.state.draft.paneId, mode);
18378
19493
  this.deps.changed();
18379
19494
  return;
18380
19495
  }
@@ -18385,7 +19500,11 @@ var DrawingInteraction = class {
18385
19500
  this.state.moved = true;
18386
19501
  if (this.state.handle >= 0) {
18387
19502
  const a = d.anchors[this.state.handle];
18388
- if (a) applyFree(a, this.resolve(x, y, d.paneId, mode), d.anchorSchema().slots[this.state.handle]?.free ?? "both");
19503
+ if (a) {
19504
+ const pivot = shift && ANGLE_SNAP_TYPES.has(d.type) && d.anchors.length === 2 ? d.anchors[1 - this.state.handle] : void 0;
19505
+ const point = pivot ? this.resolveAngleSnapped(pivot, d.paneId, x, y) : this.resolve(x, y, d.paneId, mode);
19506
+ applyFree(a, point, d.anchorSchema().slots[this.state.handle]?.free ?? "both");
19507
+ }
18389
19508
  d.constrainHandleDrag(this.state.handle);
18390
19509
  } else {
18391
19510
  const pt = this.deps.projector().pxToPoint(x, y, d.paneId);
@@ -18481,7 +19600,11 @@ var DrawingInteraction = class {
18481
19600
  const last = draft.anchors[draft.anchors.length - 1];
18482
19601
  const lx = last ? proj.xOf(last.time) : NaN;
18483
19602
  const ly = last ? proj.yOf(last.price, draft.paneId) ?? NaN : NaN;
18484
- if (draft.anchors.length < this.state.need && Math.hypot(x - lx, y - ly) > FREEHAND_SAMPLE_PX) {
19603
+ if (Math.hypot(x - lx, y - ly) > FREEHAND_SAMPLE_PX) {
19604
+ if (draft.anchors.length >= this.state.need) {
19605
+ const lastIdx = draft.anchors.length - 1;
19606
+ draft.anchors = draft.anchors.filter((_, i) => i % 2 === 0 || i === lastIdx);
19607
+ }
18485
19608
  draft.anchors.push(proj.pxToPoint(x, y, draft.paneId));
18486
19609
  }
18487
19610
  this.state.cursor = proj.pxToPoint(x, y, draft.paneId);
@@ -18554,11 +19677,11 @@ function applyFree(anchor, pt, free) {
18554
19677
  // src/renderers/native/drawings/DrawingSettingsPopup.ts
18555
19678
  var BTN = 30;
18556
19679
  var ICON = 21;
18557
- var STYLE_ID3 = "vela-drawing-popup-styles";
18558
- function ensureStyles3() {
18559
- if (typeof document === "undefined" || document.getElementById(STYLE_ID3)) return;
19680
+ var STYLE_ID4 = "vela-drawing-popup-styles";
19681
+ function ensureStyles4() {
19682
+ if (typeof document === "undefined" || document.getElementById(STYLE_ID4)) return;
18560
19683
  const s = document.createElement("style");
18561
- s.id = STYLE_ID3;
19684
+ s.id = STYLE_ID4;
18562
19685
  s.textContent = `
18563
19686
  .vela-dpop input[type=color]{-webkit-appearance:none;appearance:none;border:none;padding:0;background:none;cursor:pointer;border-radius:0;}
18564
19687
  .vela-dpop input[type=color]::-webkit-color-swatch-wrapper{padding:0;}
@@ -18577,7 +19700,10 @@ function ensureStyles3() {
18577
19700
  .vela-dpop-btn[data-active='1']{background:var(--vela-active);color:var(--vela-fg-bright);}
18578
19701
  .vela-dpop-item{background:transparent;transition:background var(--vela-dur-fast) ease;}
18579
19702
  .vela-dpop-item:hover{background:var(--vela-hover-strong);}
18580
- .vela-dpop-item[data-active='1']{background:var(--vela-active);}`;
19703
+ .vela-dpop-item[data-active='1']{background:var(--vela-active);}
19704
+ /* A finger needs a wider grab zone than a cursor \u2014 grow the move handle on touch-first
19705
+ devices (the glyph stays centered; only the hit target widens). */
19706
+ @media (pointer: coarse){.vela-dpop-grip{width:${BTN + 14}px !important;}}`;
18581
19707
  document.head.appendChild(s);
18582
19708
  }
18583
19709
  var DrawingSettingsPopup = class {
@@ -18624,7 +19750,7 @@ var DrawingSettingsPopup = class {
18624
19750
  * `onClose` fires on an outside-click dismissal (not a programmatic close). */
18625
19751
  open(drawing, anchor, actions, onClose) {
18626
19752
  this.close();
18627
- ensureStyles3();
19753
+ ensureStyles4();
18628
19754
  this.onClose = onClose ?? null;
18629
19755
  const t = this.theme;
18630
19756
  const schema = drawing.schema();
@@ -18650,7 +19776,14 @@ var DrawingSettingsPopup = class {
18650
19776
  bar.appendChild(this.dropdown("Icon size", STAMP_SIZE_OPTIONS, sz, (s) => stampSizeIcon(s), (v) => actions.patch({ size: v }), { label: sizeLabel }));
18651
19777
  }
18652
19778
  if (paths.has("style.lineColor")) bar.appendChild(this.colorButton("Line color", BRUSH_ICON, drawing.style.lineColor || DEFAULT_DRAWING_COLOR, (v) => actions.patch({ "style.lineColor": v })));
18653
- if (paths.has("style.lineWidth")) bar.appendChild(this.dropdown("Line width", [1, 2, 3, 4], drawing.style.lineWidth, (w) => lineIcon(w, "solid"), (v) => actions.patch({ "style.lineWidth": v }), { label: (v) => `${v}px`, labelInTrigger: true }));
19779
+ if (paths.has("style.lineWidth")) {
19780
+ const wf = schema.fields.find((f) => f.path === "style.lineWidth");
19781
+ if (wf?.kind === "number" && (wf.min ?? 1) > 1) {
19782
+ bar.appendChild(this.widthInput("Line width", drawing.style.lineWidth, wf.min ?? 1, wf.max ?? 60, wf.step ?? 1, (v) => actions.patch({ "style.lineWidth": v })));
19783
+ } else {
19784
+ bar.appendChild(this.dropdown("Line width", [1, 2, 3, 4], drawing.style.lineWidth, (w) => lineIcon(w, "solid"), (v) => actions.patch({ "style.lineWidth": v }), { label: (v) => `${v}px`, labelInTrigger: true }));
19785
+ }
19786
+ }
18654
19787
  if (paths.has("style.lineStyle")) bar.appendChild(this.dropdown("Line style", LINE_STYLE_OPTIONS.map((o) => o.value), drawing.style.lineStyle, (s) => lineIcon(2, s), (v) => actions.patch({ "style.lineStyle": v }), { label: styleLabel2 }));
18655
19788
  if (paths.has("style.fillColor")) bar.appendChild(this.colorButton("Fill", BUCKET_ICON, effectiveFillColor(drawing, this.theme) ?? drawing.style.fillColor ?? DEFAULT_DRAWING_COLOR, (v) => actions.patch({ "style.fillColor": v })));
18656
19789
  const isFrvp = paths.has("frvp.rows") && drawing instanceof FixedRangeVolumeProfile;
@@ -19118,14 +20251,15 @@ var DrawingSettingsPopup = class {
19118
20251
  const chk = document.createElement("input");
19119
20252
  chk.type = "checkbox";
19120
20253
  chk.checked = Boolean(s[showPath]);
19121
- chk.style.cssText = `accent-color:${s[colorPath]};width:15px;height:15px;flex:none;cursor:pointer;`;
20254
+ const current = s[colorPath] ?? contrastColor(t.background);
20255
+ chk.style.cssText = `accent-color:${current};width:15px;height:15px;flex:none;cursor:pointer;`;
19122
20256
  chk.addEventListener("change", () => actions.patch({ [`frvp.${showPath}`]: chk.checked }));
19123
20257
  const lbl = document.createElement("span");
19124
20258
  lbl.textContent = label;
19125
20259
  lbl.style.cssText = "flex:1;min-width:0;opacity:0.9;";
19126
20260
  const col = document.createElement("button");
19127
20261
  col.type = "button";
19128
- let cur = s[colorPath];
20262
+ let cur = current;
19129
20263
  col.style.cssText = `width:18px;height:18px;flex:none;border:1px solid var(--vela-border);border-radius:0;cursor:pointer;background:${cur};padding:0;`;
19130
20264
  col.addEventListener("pointerdown", (e) => e.stopPropagation());
19131
20265
  col.addEventListener("click", (e) => {
@@ -19305,7 +20439,8 @@ var DrawingSettingsPopup = class {
19305
20439
  dragHandle() {
19306
20440
  const b = document.createElement("button");
19307
20441
  b.type = "button";
19308
- b.style.cssText = `width:${BTN}px;height:${BTN}px;display:flex;align-items:center;justify-content:center;background:transparent;border:none;padding:0;cursor:grab;color:var(--vela-fg-faint);`;
20442
+ b.className = "vela-dpop-grip";
20443
+ b.style.cssText = `width:${BTN}px;height:${BTN}px;display:flex;align-items:center;justify-content:center;background:transparent;border:none;padding:0;cursor:grab;touch-action:none;color:var(--vela-fg-faint);`;
19309
20444
  b.innerHTML = sized(GRIP_ICON);
19310
20445
  b.addEventListener("pointerdown", (e) => {
19311
20446
  const el = this.el;
@@ -19321,6 +20456,10 @@ var DrawingSettingsPopup = class {
19321
20456
  const origLeft = parseFloat(el.style.left) || 0;
19322
20457
  const origTop = parseFloat(el.style.top) || 0;
19323
20458
  b.style.cursor = "grabbing";
20459
+ try {
20460
+ b.setPointerCapture(e.pointerId);
20461
+ } catch {
20462
+ }
19324
20463
  const move = (ev) => {
19325
20464
  const w = el.offsetWidth;
19326
20465
  const h = el.offsetHeight;
@@ -19333,9 +20472,11 @@ var DrawingSettingsPopup = class {
19333
20472
  b.style.cursor = "grab";
19334
20473
  window.removeEventListener("pointermove", move);
19335
20474
  window.removeEventListener("pointerup", up);
20475
+ window.removeEventListener("pointercancel", up);
19336
20476
  };
19337
20477
  window.addEventListener("pointermove", move);
19338
20478
  window.addEventListener("pointerup", up);
20479
+ window.addEventListener("pointercancel", up);
19339
20480
  });
19340
20481
  return b;
19341
20482
  }
@@ -19451,6 +20592,39 @@ var DrawingSettingsPopup = class {
19451
20592
  });
19452
20593
  return b;
19453
20594
  }
20595
+ /** An inline numeric width field for tools whose stroke range outgrows the 1–4px
20596
+ * ladder — the value is clamped to the schema's declared min/max on commit. */
20597
+ widthInput(tip, value, min, max, step, onChange) {
20598
+ const input = document.createElement("input");
20599
+ input.type = "number";
20600
+ input.dataset.tip = tip;
20601
+ input.min = String(min);
20602
+ input.max = String(max);
20603
+ input.step = String(step);
20604
+ input.value = String(value);
20605
+ input.style.cssText = `width:52px;height:${BTN}px;box-sizing:border-box;flex:none;background:transparent;color:inherit;border:1px solid var(--vela-border);border-radius:5px;padding:0 4px;font:12px ${this.theme.fontFamily};font-variant-numeric:tabular-nums;outline:none;`;
20606
+ const commit = () => {
20607
+ const n = parseFloat(input.value);
20608
+ if (!Number.isFinite(n)) {
20609
+ input.value = String(value);
20610
+ return;
20611
+ }
20612
+ const v = Math.min(max, Math.max(min, n));
20613
+ input.value = String(v);
20614
+ value = v;
20615
+ onChange(v);
20616
+ };
20617
+ input.addEventListener("change", commit);
20618
+ input.addEventListener("keydown", (e) => {
20619
+ e.stopPropagation();
20620
+ if (e.key === "Enter") {
20621
+ e.preventDefault();
20622
+ commit();
20623
+ input.blur();
20624
+ }
20625
+ });
20626
+ return input;
20627
+ }
19454
20628
  /** A pick-one dropdown: the trigger shows the current value's glyph (plus an optional inline
19455
20629
  * text label — e.g. `2px` — for controls that would otherwise look alike), and clicking it
19456
20630
  * opens a floating list of every option so a value is one click away (no cycling through). */
@@ -19738,6 +20912,8 @@ var DrawingToolbar = class {
19738
20912
  this.eraserActive = false;
19739
20913
  this.stayBtn = null;
19740
20914
  this.stayActive = false;
20915
+ this.syncBtn = null;
20916
+ this.syncActive = false;
19741
20917
  this.collapseBtn = null;
19742
20918
  this.collapsed = false;
19743
20919
  this.visible = false;
@@ -19754,7 +20930,8 @@ var DrawingToolbar = class {
19754
20930
  this.dock = options.dock ?? "absolute";
19755
20931
  this.onCollapse = options.onCollapse ?? (() => {
19756
20932
  });
19757
- ensureStyles4();
20933
+ this.onDrawingsSync = options.onDrawingsSync ?? null;
20934
+ ensureStyles5();
19758
20935
  this.root = document.createElement("div");
19759
20936
  this.root.className = "vela-dtb";
19760
20937
  this.styleRoot();
@@ -19852,6 +21029,10 @@ var DrawingToolbar = class {
19852
21029
  this.root.appendChild(this.makeMagnetCell());
19853
21030
  this.stayBtn = this.makeButton(STAY_ICON, "Stay in drawing mode", () => this.toggleStay());
19854
21031
  this.root.appendChild(this.stayBtn);
21032
+ if (this.onDrawingsSync) {
21033
+ this.syncBtn = this.makeButton(SYNC_ICON, "Sync drawings on all charts", () => this.toggleDrawingsSync());
21034
+ this.root.appendChild(this.syncBtn);
21035
+ }
19855
21036
  this.collapseBtn = this.makeButton(COLLAPSE_ICON, "Collapse toolbar", () => this.toggleCollapsed());
19856
21037
  this.collapseBtn.classList.add("vela-dtb-collapse");
19857
21038
  this.root.appendChild(this.collapseBtn);
@@ -19859,6 +21040,7 @@ var DrawingToolbar = class {
19859
21040
  this.paintEraser();
19860
21041
  this.paintMagnet();
19861
21042
  this.paintStay();
21043
+ this.paintDrawingsSync();
19862
21044
  this.paintCollapse();
19863
21045
  this.highlight();
19864
21046
  }
@@ -19909,7 +21091,7 @@ var DrawingToolbar = class {
19909
21091
  cell.appendChild(arrow);
19910
21092
  }
19911
21093
  this.tipDisposers.push(
19912
- attachChromeTooltip(cell, { host: this.host, theme: () => this.theme, text: () => this.tipText.get(cell) ?? label, placement: "right", delayMs: 2e3 })
21094
+ attachChromeTooltip(cell, { host: this.host, theme: () => this.theme, text: () => this.tipText.get(cell) ?? label, placement: "right" })
19913
21095
  );
19914
21096
  return { cell, icon: icon2, arrow };
19915
21097
  }
@@ -20019,6 +21201,23 @@ var DrawingToolbar = class {
20019
21201
  b.dataset.active = this.stayActive ? "1" : "";
20020
21202
  b.style.opacity = this.stayActive ? "1" : "0.5";
20021
21203
  }
21204
+ /** Toggle drawings sync on/off and notify the host. */
21205
+ toggleDrawingsSync() {
21206
+ this.syncActive = !this.syncActive;
21207
+ this.onDrawingsSync?.(this.syncActive);
21208
+ this.paintDrawingsSync();
21209
+ }
21210
+ /** Reflect drawings sync externally (e.g. set through the API) without notifying back. */
21211
+ setDrawingsSyncMode(on) {
21212
+ this.syncActive = on;
21213
+ this.paintDrawingsSync();
21214
+ }
21215
+ paintDrawingsSync() {
21216
+ const b = this.syncBtn;
21217
+ if (!b) return;
21218
+ b.dataset.active = this.syncActive ? "1" : "";
21219
+ b.style.opacity = this.syncActive ? "1" : "0.5";
21220
+ }
20022
21221
  // ── flyout ──
20023
21222
  /** Create the positioned flyout panel anchored to a cell, register it as open, and start the
20024
21223
  * outside-dismiss. Callers fill it with items. */
@@ -20178,6 +21377,8 @@ var DrawingToolbar = class {
20178
21377
  const type = this.lastUsed.get(group.id) ?? group.tools[0]?.type;
20179
21378
  const tool = group.tools.find((t) => t.type === type) ?? group.tools[0];
20180
21379
  iconBtn.innerHTML = hitHtml(tool?.icon ?? "");
21380
+ const cell = this.groupCells.get(group.id);
21381
+ if (cell) this.tipText.set(cell, tool?.label ?? group.label);
20181
21382
  }
20182
21383
  /** Reconcile active tints from state (armed group / open flyout / cursor / modes). Active is a
20183
21384
  * `data-active` attribute the stylesheet paints — no inline background, so hover never sticks. */
@@ -20201,7 +21402,7 @@ var DrawingToolbar = class {
20201
21402
  this.tipText.set(btn, title);
20202
21403
  if (icon2) btn.innerHTML = hitHtml(icon2);
20203
21404
  this.tipDisposers.push(
20204
- attachChromeTooltip(btn, { host: this.host, theme: () => this.theme, text: () => this.tipText.get(btn) ?? title, placement: "right", delayMs: 2e3 })
21405
+ attachChromeTooltip(btn, { host: this.host, theme: () => this.theme, text: () => this.tipText.get(btn) ?? title, placement: "right" })
20205
21406
  );
20206
21407
  btn.addEventListener("click", onClick);
20207
21408
  return btn;
@@ -20213,13 +21414,13 @@ var DrawingToolbar = class {
20213
21414
  }
20214
21415
  };
20215
21416
  var MAGNET_ID = "__magnet__";
20216
- var STYLE_ID4 = "vela-drawing-toolbar-styles";
20217
- function ensureStyles4() {
21417
+ var STYLE_ID5 = "vela-drawing-toolbar-styles";
21418
+ function ensureStyles5() {
20218
21419
  if (typeof document === "undefined") return;
20219
- let s = document.getElementById(STYLE_ID4);
21420
+ let s = document.getElementById(STYLE_ID5);
20220
21421
  if (!s) {
20221
21422
  s = document.createElement("style");
20222
- s.id = STYLE_ID4;
21423
+ s.id = STYLE_ID5;
20223
21424
  document.head.appendChild(s);
20224
21425
  }
20225
21426
  s.textContent = `
@@ -20267,6 +21468,7 @@ var COLLAPSE_ICON = icon("chevrons-left");
20267
21468
  var EXPAND_ICON = icon("chevrons-right");
20268
21469
  var MAGNET_ICON = icon("magnet");
20269
21470
  var STAY_ICON = icon("pen-lock");
21471
+ var SYNC_ICON = icon("pen-sync");
20270
21472
  var ERASER_ICON = icon("eraser");
20271
21473
  var ARROW_ICON2 = icon("chevron-right");
20272
21474
  var CHECK_ICON = icon("check");
@@ -20490,12 +21692,19 @@ var UserDrawingController = class {
20490
21692
  this.activeTool = null;
20491
21693
  // last-used style for the armed tool (seeds the placement ghost)
20492
21694
  this.intentCb = null;
21695
+ /** Another chart's in-progress placement, mirrored here as a ghost (drawings sync). */
21696
+ this.externalGhost = null;
21697
+ /** Last draft fingerprint reported upstream — gates the per-render emission to actual changes. */
21698
+ this.lastDraftKey = null;
20493
21699
  this.measure = new MeasureOverlay();
20494
21700
  // transient ruler — not a persistent drawing
20495
21701
  this.toolbarVisible = false;
20496
21702
  // mirrors showToolbar (drives the gutter reservation)
20497
21703
  this.toolbarCollapsed = false;
20498
21704
  // the bar is a slim expand-strip (narrower gutter)
21705
+ /** Mobile chrome: the docked bar is suppressed (the shell provides its own tool
21706
+ * picker) while `toolbarVisible` keeps the host's INTENT for a later desktop flip. */
21707
+ this.mobileLayout = false;
20499
21708
  /** Self-serve Ctrl+Z / Ctrl+Y as drawing undo/redo. A host that owns a UNIFIED
20500
21709
  * history (drawings + app ops in one timeline) turns this off so the chords
20501
21710
  * bubble to its keymap instead — see the renderer's `historyChords` feature. */
@@ -20554,12 +21763,26 @@ var UserDrawingController = class {
20554
21763
  }
20555
21764
  showToolbar(visible) {
20556
21765
  this.toolbarVisible = visible;
20557
- this.toolbar.setVisible(visible);
21766
+ this.toolbar.setVisible(visible && !this.mobileLayout);
21767
+ this.syncToolbarGutter();
21768
+ }
21769
+ /** Mobile suppresses the docked toolbar (hover flyouts don't work with touch; the
21770
+ * shell's own drawer picks tools instead) and releases its gutter; flipping back
21771
+ * to desktop restores whatever `showToolbar` last asked for. */
21772
+ setLayoutMode(mode) {
21773
+ this.mobileLayout = mode === "mobile";
21774
+ this.toolbar.setVisible(this.toolbarVisible && !this.mobileLayout);
20558
21775
  this.syncToolbarGutter();
20559
21776
  }
20560
21777
  /** The gutter follows the bar's current footprint: hidden 0, collapsed a slim strip, else full width. */
20561
21778
  syncToolbarGutter() {
20562
- this.deps.setToolbarGutter(this.toolbarVisible ? this.toolbarCollapsed ? TOOLBAR_COLLAPSED_WIDTH : TOOLBAR_WIDTH : 0);
21779
+ const shown = this.toolbarVisible && !this.mobileLayout;
21780
+ this.deps.setToolbarGutter(shown ? this.toolbarCollapsed ? TOOLBAR_COLLAPSED_WIDTH : TOOLBAR_WIDTH : 0);
21781
+ }
21782
+ /** Core push: mirror (or clear) another chart's in-progress placement as a ghost. */
21783
+ setExternalGhost(doc) {
21784
+ this.externalGhost = doc ? deserializeDrawing(doc) : null;
21785
+ this.render();
20563
21786
  }
20564
21787
  syncDrawings(docs) {
20565
21788
  this.drawings = docs.map((d) => deserializeDrawing(d)).filter((d) => d != null);
@@ -20719,7 +21942,7 @@ var UserDrawingController = class {
20719
21942
  }
20720
21943
  this.interaction.down(x, y, snap, shift);
20721
21944
  }
20722
- pointerMove(x, y, snap = "off") {
21945
+ pointerMove(x, y, snap = "off", shift = false) {
20723
21946
  if (this.eraserMode) {
20724
21947
  if (this.erasing) this.deleteAt(x, y);
20725
21948
  return;
@@ -20729,7 +21952,7 @@ var UserDrawingController = class {
20729
21952
  this.render();
20730
21953
  return;
20731
21954
  }
20732
- this.interaction.move(x, y, snap);
21955
+ this.interaction.move(x, y, snap, shift);
20733
21956
  this.updateHover(x, y);
20734
21957
  }
20735
21958
  /** Track which drawing is hovered so its handles appear on hover (and only then). */
@@ -21115,6 +22338,8 @@ var UserDrawingController = class {
21115
22338
  this.layoutTextEditor();
21116
22339
  const ghost = this.interaction.ghost();
21117
22340
  if (ghost) this.painter.paintGhost(ctx, ghost, proj, this.deps.theme());
22341
+ if (this.externalGhost) this.painter.paintGhost(ctx, this.externalGhost, proj, this.deps.theme());
22342
+ this.emitDraft(ghost);
21118
22343
  const markers = this.interaction.placingMarkers(proj);
21119
22344
  if (markers) this.painter.paintHandles(ctx, markers);
21120
22345
  const m = this.interaction.snapMarker();
@@ -21176,6 +22401,15 @@ var UserDrawingController = class {
21176
22401
  }
21177
22402
  }, () => this.clearSelection());
21178
22403
  }
22404
+ /** Report placement progress upstream (`draft` intent) so the drawings sync can
22405
+ * mirror the ghost live on linked charts. Called from every render; only actual
22406
+ * shape changes emit, and the end of a placement emits one `null`. */
22407
+ emitDraft(ghost) {
22408
+ const key = ghost ? JSON.stringify({ t: ghost.type, p: ghost.paneId, a: ghost.anchors, s: ghost.style, x: ghost.text }) : null;
22409
+ if (key === this.lastDraftKey) return;
22410
+ this.lastDraftKey = key;
22411
+ this.intentCb?.({ kind: "draft", doc: ghost ? ghost.serialize() : null });
22412
+ }
21179
22413
  emit(i) {
21180
22414
  if (i.kind === "create") {
21181
22415
  if (i.doc.text && i.doc.text.color === void 0) {
@@ -21424,7 +22658,7 @@ var LUXALGO_WORDMARK_SVG = '<svg viewBox="0 0 964 252" aria-hidden="true"><path
21424
22658
 
21425
22659
  // src/renderers/native/chrome/AttributionMark.ts
21426
22660
  var ATTRIBUTION_URL = "https://luxalgo.com/vela";
21427
- var STYLE_ID5 = "vela-attribution-styles";
22661
+ var STYLE_ID6 = "vela-attribution-styles";
21428
22662
  var CSS = `
21429
22663
  .vela-attribution {
21430
22664
  text-decoration: none;
@@ -21470,12 +22704,28 @@ var CSS = `
21470
22704
  max-width: 240px;
21471
22705
  opacity: 1;
21472
22706
  transform: translateX(0);
21473
- }`;
21474
- function ensureStyles5(doc) {
21475
- let s = doc.getElementById(STYLE_ID5);
22707
+ }
22708
+ /* Mobile: a hair smaller so the mark doesn't dominate a phone-width plot. Keyed on the
22709
+ renderer's own container attribute (data-vela-layout) AND the shell root's
22710
+ (data-layout): the workspace's single grid-wide mark lives OUTSIDE any renderer
22711
+ container, so only the shell attribute reaches it. */
22712
+ [data-vela-layout='mobile'] .vela-attribution .vela-attr-symbol svg,
22713
+ [data-layout='mobile'] .vela-attribution .vela-attr-symbol svg { height: 22px; }
22714
+ [data-vela-layout='mobile'] .vela-attribution .vela-attr-wordmark,
22715
+ [data-layout='mobile'] .vela-attribution .vela-attr-wordmark {
22716
+ top: 2px;
22717
+ transform: translateX(-6px);
22718
+ }
22719
+ [data-vela-layout='mobile'] .vela-attribution .vela-attr-wordmark svg,
22720
+ [data-layout='mobile'] .vela-attribution .vela-attr-wordmark svg { height: 21px; }
22721
+ [data-vela-layout='mobile'] .vela-attribution:hover .vela-attr-wordmark,
22722
+ [data-layout='mobile'] .vela-attribution:hover .vela-attr-wordmark { transform: translateX(0); }
22723
+ `;
22724
+ function ensureStyles6(doc) {
22725
+ let s = doc.getElementById(STYLE_ID6);
21476
22726
  if (!s) {
21477
22727
  s = doc.createElement("style");
21478
- s.id = STYLE_ID5;
22728
+ s.id = STYLE_ID6;
21479
22729
  doc.head.appendChild(s);
21480
22730
  }
21481
22731
  s.textContent = CSS;
@@ -21504,7 +22754,7 @@ function createCustomMark(doc, html, background) {
21504
22754
  return el;
21505
22755
  }
21506
22756
  function createAttributionMark(doc, background) {
21507
- ensureStyles5(doc);
22757
+ ensureStyles6(doc);
21508
22758
  const a = doc.createElement("a");
21509
22759
  a.className = "vela-attribution";
21510
22760
  a.href = ATTRIBUTION_URL;
@@ -21530,6 +22780,174 @@ function createAttributionMark(doc, background) {
21530
22780
  return a;
21531
22781
  }
21532
22782
 
22783
+ // src/renderers/shared/dom-raster.ts
22784
+ function hasInk(color) {
22785
+ if (!color || color === "transparent") return false;
22786
+ const m = /rgba?\(\s*[\d.]+\s*,\s*[\d.]+\s*,\s*[\d.]+\s*,\s*([\d.]+)\s*\)/.exec(color);
22787
+ return m ? parseFloat(m[1]) > 1e-3 : true;
22788
+ }
22789
+ function cornerRadius(cs, w, h) {
22790
+ const raw = cs.borderTopLeftRadius;
22791
+ const v = parseFloat(raw) || 0;
22792
+ const px = raw.endsWith("%") ? v / 100 * Math.min(w, h) : v;
22793
+ return Math.max(0, Math.min(px, Math.min(w, h) / 2));
22794
+ }
22795
+ function tracePath(ctx, x, y, w, h, r) {
22796
+ ctx.beginPath();
22797
+ if (r > 0 && typeof ctx.roundRect === "function") ctx.roundRect(x, y, w, h, r);
22798
+ else ctx.rect(x, y, w, h);
22799
+ }
22800
+ function canDrawUntainted(img) {
22801
+ try {
22802
+ const probe2 = img.ownerDocument.createElement("canvas");
22803
+ probe2.width = 1;
22804
+ probe2.height = 1;
22805
+ const ctx = probe2.getContext("2d");
22806
+ if (!ctx) return false;
22807
+ ctx.drawImage(img, 0, 0, 1, 1);
22808
+ probe2.toDataURL();
22809
+ return true;
22810
+ } catch {
22811
+ return false;
22812
+ }
22813
+ }
22814
+ function rasterizeOverlay(ctx, root, frame) {
22815
+ const win = root.ownerDocument.defaultView;
22816
+ if (!win) return;
22817
+ ctx.save();
22818
+ ctx.scale(frame.dpr, frame.dpr);
22819
+ const drawText = (node, cs, alpha) => {
22820
+ const text = node.textContent?.replace(/\s+/g, " ").trim();
22821
+ if (!text) return;
22822
+ const range = node.ownerDocument.createRange();
22823
+ range.selectNodeContents(node);
22824
+ const r = range.getBoundingClientRect();
22825
+ if (r.width <= 0 || r.height <= 0) return;
22826
+ ctx.globalAlpha = alpha;
22827
+ ctx.fillStyle = cs.color;
22828
+ ctx.font = `${cs.fontStyle} ${cs.fontWeight} ${cs.fontSize} ${cs.fontFamily}`;
22829
+ ctx.textBaseline = "middle";
22830
+ ctx.textAlign = "left";
22831
+ ctx.fillText(text, r.left - frame.left, r.top - frame.top + r.height / 2);
22832
+ };
22833
+ const drawImg = (img, cs, alpha) => {
22834
+ if (!img.complete || img.naturalWidth <= 0 || !canDrawUntainted(img)) return;
22835
+ const r = img.getBoundingClientRect();
22836
+ if (r.width <= 0 || r.height <= 0) return;
22837
+ const x = r.left - frame.left;
22838
+ const y = r.top - frame.top;
22839
+ ctx.save();
22840
+ ctx.globalAlpha = alpha;
22841
+ tracePath(ctx, x, y, r.width, r.height, cornerRadius(cs, r.width, r.height));
22842
+ ctx.clip();
22843
+ ctx.drawImage(img, x, y, r.width, r.height);
22844
+ ctx.restore();
22845
+ };
22846
+ const drawSvgShapes = (parent, alpha) => {
22847
+ for (const el of parent.children) {
22848
+ if (el.hasAttribute("transform")) continue;
22849
+ const tag = el.tagName.toLowerCase();
22850
+ if (tag === "g") {
22851
+ drawSvgShapes(el, alpha);
22852
+ continue;
22853
+ }
22854
+ const num3 = (name) => parseFloat(el.getAttribute(name) ?? "0") || 0;
22855
+ let path = null;
22856
+ if (tag === "path") {
22857
+ const d = el.getAttribute("d");
22858
+ if (d) path = new Path2D(d);
22859
+ } else if (tag === "circle") {
22860
+ path = new Path2D();
22861
+ path.arc(num3("cx"), num3("cy"), num3("r"), 0, Math.PI * 2);
22862
+ } else if (tag === "ellipse") {
22863
+ path = new Path2D();
22864
+ path.ellipse(num3("cx"), num3("cy"), num3("rx"), num3("ry"), 0, 0, Math.PI * 2);
22865
+ } else if (tag === "rect") {
22866
+ path = new Path2D();
22867
+ path.rect(num3("x"), num3("y"), num3("width"), num3("height"));
22868
+ } else if (tag === "line") {
22869
+ path = new Path2D();
22870
+ path.moveTo(num3("x1"), num3("y1"));
22871
+ path.lineTo(num3("x2"), num3("y2"));
22872
+ } else if (tag === "polyline" || tag === "polygon") {
22873
+ const pts = (el.getAttribute("points") ?? "").trim().split(/[\s,]+/).map(Number);
22874
+ if (pts.length < 4 || pts.some(Number.isNaN)) continue;
22875
+ path = new Path2D();
22876
+ path.moveTo(pts[0], pts[1]);
22877
+ for (let i = 2; i + 1 < pts.length; i += 2) path.lineTo(pts[i], pts[i + 1]);
22878
+ if (tag === "polygon") path.closePath();
22879
+ }
22880
+ if (!path) continue;
22881
+ const cs = win.getComputedStyle(el);
22882
+ if (cs.fill && cs.fill !== "none" && hasInk(cs.fill)) {
22883
+ ctx.globalAlpha = alpha * (parseFloat(cs.fillOpacity) || 1);
22884
+ ctx.fillStyle = cs.fill;
22885
+ ctx.fill(path);
22886
+ }
22887
+ const sw = parseFloat(cs.strokeWidth) || 0;
22888
+ if (cs.stroke && cs.stroke !== "none" && hasInk(cs.stroke) && sw > 0) {
22889
+ ctx.globalAlpha = alpha * (parseFloat(cs.strokeOpacity) || 1);
22890
+ ctx.strokeStyle = cs.stroke;
22891
+ ctx.lineWidth = sw;
22892
+ if (cs.strokeLinecap === "round" || cs.strokeLinecap === "square" || cs.strokeLinecap === "butt") ctx.lineCap = cs.strokeLinecap;
22893
+ if (cs.strokeLinejoin === "round" || cs.strokeLinejoin === "bevel" || cs.strokeLinejoin === "miter") ctx.lineJoin = cs.strokeLinejoin;
22894
+ ctx.stroke(path);
22895
+ }
22896
+ }
22897
+ };
22898
+ const drawSvg = (svg, parentAlpha) => {
22899
+ const r = svg.getBoundingClientRect();
22900
+ if (r.width <= 0 || r.height <= 0) return;
22901
+ const cs = win.getComputedStyle(svg);
22902
+ if (cs.display === "none" || cs.visibility === "hidden") return;
22903
+ const alpha = parentAlpha * (parseFloat(cs.opacity) || 1);
22904
+ if (alpha <= 1e-3) return;
22905
+ const vb = svg.viewBox.baseVal;
22906
+ const vw = vb && vb.width > 0 ? vb.width : r.width;
22907
+ const vh = vb && vb.height > 0 ? vb.height : r.height;
22908
+ ctx.save();
22909
+ ctx.translate(r.left - frame.left, r.top - frame.top);
22910
+ ctx.scale(r.width / vw, r.height / vh);
22911
+ if (vb) ctx.translate(-vb.x, -vb.y);
22912
+ drawSvgShapes(svg, alpha);
22913
+ ctx.restore();
22914
+ };
22915
+ const drawElement = (el, parentAlpha) => {
22916
+ const cs = win.getComputedStyle(el);
22917
+ if (cs.display === "none" || cs.visibility === "hidden") return;
22918
+ const alpha = parentAlpha * (parseFloat(cs.opacity) || 1);
22919
+ if (alpha <= 1e-3) return;
22920
+ const r = el.getBoundingClientRect();
22921
+ if (r.width > 0 && r.height > 0) {
22922
+ const x = r.left - frame.left;
22923
+ const y = r.top - frame.top;
22924
+ const radius = cornerRadius(cs, r.width, r.height);
22925
+ if (hasInk(cs.backgroundColor)) {
22926
+ ctx.globalAlpha = alpha;
22927
+ ctx.fillStyle = cs.backgroundColor;
22928
+ tracePath(ctx, x, y, r.width, r.height, radius);
22929
+ ctx.fill();
22930
+ }
22931
+ const bw = parseFloat(cs.borderTopWidth) || 0;
22932
+ if (bw > 0 && cs.borderTopStyle !== "none" && hasInk(cs.borderTopColor)) {
22933
+ ctx.globalAlpha = alpha;
22934
+ ctx.strokeStyle = cs.borderTopColor;
22935
+ ctx.lineWidth = bw;
22936
+ tracePath(ctx, x + bw / 2, y + bw / 2, r.width - bw, r.height - bw, Math.max(0, radius - bw / 2));
22937
+ ctx.stroke();
22938
+ }
22939
+ }
22940
+ for (const child of el.childNodes) {
22941
+ if (child.nodeType === 3) drawText(child, cs, alpha);
22942
+ else if (child instanceof win.HTMLImageElement) drawImg(child, win.getComputedStyle(child), alpha);
22943
+ else if (child instanceof win.SVGSVGElement) drawSvg(child, alpha);
22944
+ else if (child instanceof win.HTMLElement) drawElement(child, alpha);
22945
+ }
22946
+ };
22947
+ if (root instanceof win.HTMLElement) drawElement(root, 1);
22948
+ ctx.restore();
22949
+ }
22950
+
21533
22951
  // src/renderers/native/vpvr/profile.ts
21534
22952
  function buildVpvrProfile(bars, i0, i1, rowCount, valueAreaFrac) {
21535
22953
  const from = Math.max(0, i0);
@@ -21698,12 +23116,13 @@ var AXIS_COL_W = AXIS_MERGED_W;
21698
23116
  var COLLAPSED_PANE_H = 26;
21699
23117
  var TIME_AXIS_H = 22;
21700
23118
  var SCROLL_BTN_SIZE = 26;
23119
+ var SCROLL_BTN_SIZE_TOUCH = 24;
21701
23120
  var SCROLL_BTN_RADIUS = 5;
21702
23121
  var SCROLL_BTN_SHADOW = "0 2px 8px rgba(0,0,0,0.4)";
21703
23122
  var SCROLL_BTN_ICON_PX = 12;
21704
- var SCROLL_BTN_RIGHT_INSET = 25;
23123
+ var SCROLL_BTN_RIGHT_INSET = 12;
21705
23124
  var SCROLL_BTN_RIGHT = RIGHT_AXIS_W + SCROLL_BTN_RIGHT_INSET;
21706
- var SCROLL_BTN_BOTTOM = TIME_AXIS_H + 30;
23125
+ var SCROLL_BTN_BOTTOM = TIME_AXIS_H + 14;
21707
23126
  var SCROLL_BTN_PROXIMITY_PX = 120;
21708
23127
  var MIN_VISIBLE_BARS = 2;
21709
23128
  var ZOOM_OUT_MARGIN_BARS = 6;
@@ -21719,10 +23138,10 @@ var SEPARATOR_HIT_PX = 4;
21719
23138
  var NativeRenderer = class {
21720
23139
  constructor(opts) {
21721
23140
  this.capabilities = NATIVE_CAPABILITIES;
21722
- // The app "chrome" surface (drawing toolbar + axis-scale gutters): background + text.
23141
+ // The app "chrome" surface (drawing toolbar + in-chart dialogs): background + text.
21723
23142
  // Owned by the app theme alone (mount/setTheme) — config-level background/text edits
21724
- // (settings dialog, persisted configs) recolor only the plot, keeping the toolbar and
21725
- // scales on a consistent, always-readable surface.
23143
+ // (settings dialog, persisted configs) recolor the plot AND the axis scales (see
23144
+ // axisSurface), but never the toolbar/dialog chrome.
21726
23145
  this.surfaceBackground = DARK_THEME.background;
21727
23146
  this.surfaceTextColor = DARK_THEME.textColor;
21728
23147
  // the plot area (canvases + DOM overlays), inset to the right of the toolbar gutter
@@ -21755,8 +23174,12 @@ var NativeRenderer = class {
21755
23174
  this.symbolPicker = null;
21756
23175
  /** Indicator titles (the legend rows) shown — held here so a remount re-applies it. */
21757
23176
  this.indicatorTitlesOn = true;
23177
+ /** Plot values beside the legend titles shown — held here so a remount re-applies it. */
23178
+ this.indicatorValuesOn = true;
21758
23179
  /** Host-contributed legend actions — held here so a rebuild of the legend re-wires them. */
21759
23180
  this.legendActionsProvider = null;
23181
+ /** Host override of the legend's fold toggle — held here so a remount re-applies it. */
23182
+ this.legendOverviewAction = null;
21760
23183
  // ── keyboard navigation / accessibility (item 11) ──
21761
23184
  this.keyboard = null;
21762
23185
  this.keyboardEnabled = true;
@@ -21866,6 +23289,8 @@ var NativeRenderer = class {
21866
23289
  // Distance (px) from the plot's right edge — clears the FULL scale gutter, which widens when a
21867
23290
  // pane carries merged (own-scale) columns; the constant only clears a single-column scale.
21868
23291
  this.scrollBtnRightPx = SCROLL_BTN_RIGHT;
23292
+ // ── chrome size class (pushed by the host shell; see IChartRenderer.setLayoutMode) ──
23293
+ this.layoutMode = "desktop";
21869
23294
  this.viewportCbs = /* @__PURE__ */ new Set();
21870
23295
  this.crosshairCbs = /* @__PURE__ */ new Set();
21871
23296
  this.chartTypeSettingsCbs = /* @__PURE__ */ new Set();
@@ -21876,6 +23301,7 @@ var NativeRenderer = class {
21876
23301
  this.factoryConfig = null;
21877
23302
  this.hostSettingsSections = [];
21878
23303
  this.clickCbs = /* @__PURE__ */ new Set();
23304
+ this.axisLongPressCbs = /* @__PURE__ */ new Set();
21879
23305
  this.inputChangeCbs = /* @__PURE__ */ new Set();
21880
23306
  this.removeIndicatorCbs = /* @__PURE__ */ new Set();
21881
23307
  this.toggleVisibleCbs = /* @__PURE__ */ new Set();
@@ -21883,7 +23309,7 @@ var NativeRenderer = class {
21883
23309
  this.priceStyleCbs = /* @__PURE__ */ new Set();
21884
23310
  this.tableOverlays = /* @__PURE__ */ new Map();
21885
23311
  this.name = "native";
21886
- 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"];
23312
+ 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"];
21887
23313
  /** Track cursor proximity to the scroll button on the plot (bubbles from the button too,
21888
23314
  * so moving onto the button doesn't count as leaving). */
21889
23315
  this.onScrollProximityMove = (e) => {
@@ -21922,6 +23348,7 @@ var NativeRenderer = class {
21922
23348
  this.candleDown = opts.downColor;
21923
23349
  this.scene.priceStyle = opts.priceStyle;
21924
23350
  this.scene.basePainting = basePaintingOf(opts.priceStyle);
23351
+ this.scene.candleOverride = candleOverrideFor(opts.priceStyle, this.scene.style.chartTypes);
21925
23352
  }
21926
23353
  this.theme = this.deriveTheme(DARK_THEME);
21927
23354
  }
@@ -22055,6 +23482,11 @@ var NativeRenderer = class {
22055
23482
  this.inputsUI?.setTitlesVisible(this.indicatorTitlesOn);
22056
23483
  return;
22057
23484
  // own DOM, no repaint needed
23485
+ case "indicatorValues":
23486
+ this.indicatorValuesOn = value !== false;
23487
+ this.inputsUI?.setValuesVisible(this.indicatorValuesOn);
23488
+ return;
23489
+ // own DOM, no repaint needed
22058
23490
  case "attribution":
22059
23491
  this.attributionHtml = typeof value === "string" && value.trim() ? value : null;
22060
23492
  this.attributionEnabled = this.attributionHtml !== null || Boolean(value);
@@ -22143,6 +23575,8 @@ var NativeRenderer = class {
22143
23575
  return this.settingsEnabled;
22144
23576
  case "indicatorTitles":
22145
23577
  return this.indicatorTitlesOn;
23578
+ case "indicatorValues":
23579
+ return this.indicatorValuesOn;
22146
23580
  case "attribution":
22147
23581
  return this.attributionHtml ?? this.attributionEnabled;
22148
23582
  case "dialogHost":
@@ -22399,6 +23833,7 @@ var NativeRenderer = class {
22399
23833
  baselineLevel: next.baseline.baselineLevel
22400
23834
  };
22401
23835
  this.setPriceStyle(next.series.style);
23836
+ this.scene.candleOverride = candleOverrideFor(this.scene.priceStyle, s.chartTypes);
22402
23837
  this.scene.baselineValue = next.series.baseline;
22403
23838
  this.coords.setPitchScale(next.series.spacing);
22404
23839
  this.coords.setViewport(this.clampViewport(this.coords.getViewport().barSpacing, this.coords.getViewport().rightOffset));
@@ -22478,7 +23913,10 @@ var NativeRenderer = class {
22478
23913
  /** Port surface: hosts (bottom-bar / chrome buttons) open the same dialog as the
22479
23914
  * in-chart gear — created on demand, independent of the gear feature being enabled. */
22480
23915
  openSettingsDialog(section) {
22481
- if (!this.settingsDialog && this.plot) this.settingsDialog = new SettingsDialog(this.dialogHost ?? this.plot, this.theme);
23916
+ if (!this.settingsDialog && this.plot) {
23917
+ this.settingsDialog = new SettingsDialog(this.dialogHost ?? this.plot, this.theme);
23918
+ this.settingsDialog.setLayoutMode(this.layoutMode);
23919
+ }
22482
23920
  this.toggleSettingsDialog(section);
22483
23921
  }
22484
23922
  /** (Re)aim the dialog's Canvas → Theme row: the current selection reflects the STABLE
@@ -22504,7 +23942,7 @@ var NativeRenderer = class {
22504
23942
  (patch) => this.applyConfig(patch),
22505
23943
  (json) => this.applyConfig(json),
22506
23944
  () => {
22507
- if (this.factoryConfig) this.applyConfig(this.factoryConfig);
23945
+ if (this.factoryConfig) this.applyConfig(factoryResetConfig(this.factoryConfig));
22508
23946
  this.settingsDialog?.close();
22509
23947
  this.openSettingsDialog();
22510
23948
  },
@@ -22567,11 +24005,18 @@ var NativeRenderer = class {
22567
24005
  this.scrollButton.style.color = this.theme.textColor;
22568
24006
  this.scrollButton.style.borderColor = this.theme.borderColor;
22569
24007
  }
22570
- /** Show the button only when the latest bar is scrolled off the right edge AND the cursor is nearby. */
24008
+ /** Show the button only when the latest bar is scrolled off the right edge. On desktop the
24009
+ * cursor must also be nearby (hover reveal); on mobile — no hovering cursor — the button
24010
+ * appears whenever the latest bars are off-screen and hides again once the view is back
24011
+ * at the right edge. */
22571
24012
  updateScrollToRealtimeButton() {
22572
24013
  const btn = this.scrollButton;
22573
24014
  if (!btn) return;
22574
24015
  const latestOffScreen = this.coords.barCount > 0 && this.coords.getViewport().rightOffset < 0;
24016
+ if (this.layoutMode === "mobile") {
24017
+ btn.style.display = latestOffScreen ? "flex" : "none";
24018
+ return;
24019
+ }
22575
24020
  btn.style.display = latestOffScreen && this.pointerNearScrollBtn ? "flex" : "none";
22576
24021
  }
22577
24022
  /** Glide the view back to the most recent bars, keeping the current zoom (barSpacing). */
@@ -22593,17 +24038,20 @@ var NativeRenderer = class {
22593
24038
  this.animator.start();
22594
24039
  }
22595
24040
  /**
22596
- * Export the current chart as a PNG data URL by compositing the geometry (L0)
22597
- * + chrome (L1) + user-drawings (L1.5) canvases onto an offscreen canvas. The
22598
- * background is filled first (the canvas2d data layer is transparent — its bg
22599
- * lives on the wrapper), and a fresh synchronous paint runs first so the WebGL2
22600
- * backend's (non-preserved) drawing buffer is populated before it's read back
22601
- * this tick the same paint also repaints the drawings layers, so they're
22602
- * current (interleaved drawings are already inside the data composite). They're
22603
- * drawn bottom-up in DOM stacking order so the front user drawings (trend lines,
22604
- * boxes, etc.) sit above the Pine drawings on the chrome layer, matching what's
22605
- * on screen. The crosshair (L2) and DOM overlays (tables, legend) are
22606
- * intentionally excluded.
24041
+ * Export the current chart as a PNG data URL by compositing EVERY plot canvas onto
24042
+ * an offscreen canvas, in the same stacking order the DOM shows: plugin layers
24043
+ * below the data, geometry (L0), volume columns (L0.25), the visible-range volume
24044
+ * profile (L0.6), plugin layers above the data, chrome (L1), and user drawings
24045
+ * (L1.5). The background is filled first (the canvas2d data layer is transparent —
24046
+ * its bg lives on the wrapper), and a fresh synchronous paint runs first so the
24047
+ * WebGL2 backend's (non-preserved) drawing buffer is populated before it's read
24048
+ * back this tick the same paint also repaints the drawings layers, so they're
24049
+ * current. DOM chrome joins as a best-effort text/chip raster (see
24050
+ * `rasterizeOverlay`): the per-pane indicator legends, plus any host overlay that
24051
+ * opts in with a `data-vela-screenshot` attribute on the mount container's
24052
+ * subtree (the widget marks its status line, and its symbol watermark with
24053
+ * `"under"` — drawn beneath the canvases, where it sits on screen). Only the
24054
+ * crosshair (L2) is intentionally excluded.
22607
24055
  */
22608
24056
  screenshot() {
22609
24057
  if (!this.dataCanvas) return null;
@@ -22616,9 +24064,25 @@ var NativeRenderer = class {
22616
24064
  if (!ctx) return null;
22617
24065
  ctx.fillStyle = this.theme.background;
22618
24066
  ctx.fillRect(0, 0, out.width, out.height);
22619
- ctx.drawImage(this.dataCanvas, 0, 0);
22620
- ctx.drawImage(this.chromeCanvas, 0, 0);
22621
- ctx.drawImage(this.drawingsCanvas, 0, 0);
24067
+ const plotRect = this.plot?.getBoundingClientRect();
24068
+ const frame = plotRect && plotRect.width > 0 ? { left: plotRect.left, top: plotRect.top, dpr: out.width / plotRect.width } : null;
24069
+ const hostMarked = [...this.mountContainer?.querySelectorAll("[data-vela-screenshot]") ?? []];
24070
+ if (frame) {
24071
+ for (const el of hostMarked) {
24072
+ if (el.getAttribute("data-vela-screenshot") === "under") rasterizeOverlay(ctx, el, frame);
24073
+ }
24074
+ }
24075
+ const below = this.extLayers.filter((l) => l.def.placement === "below-data").map((l) => l.canvas);
24076
+ const above = this.extLayers.filter((l) => l.def.placement !== "below-data").map((l) => l.canvas);
24077
+ for (const canvas of [...below, this.dataCanvas, this.volumeCanvas, this.vpvrCanvas, ...above, this.chromeCanvas, this.drawingsCanvas]) {
24078
+ if (canvas && canvas.width > 0 && canvas.height > 0) ctx.drawImage(canvas, 0, 0);
24079
+ }
24080
+ if (frame) {
24081
+ for (const lg of this.plot?.querySelectorAll("[data-vela-pane]") ?? []) rasterizeOverlay(ctx, lg, frame);
24082
+ for (const el of hostMarked) {
24083
+ if (el.getAttribute("data-vela-screenshot") !== "under") rasterizeOverlay(ctx, el, frame);
24084
+ }
24085
+ }
22622
24086
  return out.toDataURL("image/png");
22623
24087
  }
22624
24088
  /**
@@ -22705,10 +24169,16 @@ var NativeRenderer = class {
22705
24169
  borderColor: isDarkColor(this.surfaceBackground) ? DARK_THEME.borderColor : LIGHT_THEME.borderColor
22706
24170
  };
22707
24171
  }
24172
+ /** The colors the axis-scale gutters (price + time) paint with: the LIVE chart background
24173
+ * (`layout.background`) and its contrast-corrected text, so the scales read as part of the
24174
+ * plot. Only the toolbar/dialog chrome stays on the stable app-theme surface. */
24175
+ axisSurface() {
24176
+ return { background: this.theme.background, textColor: this.theme.textColor };
24177
+ }
22708
24178
  // ── lifecycle ──
22709
24179
  mount(container, theme) {
22710
24180
  this.mountContainer = container;
22711
- this.publishToolbarGutter();
24181
+ this.publishGutters();
22712
24182
  this.theme = this.deriveTheme(theme);
22713
24183
  this.surfaceBackground = theme.background;
22714
24184
  this.surfaceTextColor = theme.textColor;
@@ -22763,6 +24233,9 @@ var NativeRenderer = class {
22763
24233
  fling: (v) => this.fling(v),
22764
24234
  onPointerMove: (x, y) => this.handlePointerMove(x, y),
22765
24235
  onClick: (x) => this.handleClick(x),
24236
+ onAxisLongPress: (axis, x, y) => {
24237
+ for (const cb of this.axisLongPressCbs) cb({ axis, x, y });
24238
+ },
22766
24239
  beginPriceScale: (x, y) => this.beginPriceScale(x, y),
22767
24240
  priceScaleBy: (dy) => this.priceScaleBy(dy),
22768
24241
  beginPricePan: (y) => this.beginPricePan(y),
@@ -22780,7 +24253,7 @@ var NativeRenderer = class {
22780
24253
  drawingsDeleteAt: (x, y) => this.userDrawings?.deleteAt(x, y) ?? false,
22781
24254
  drawingsSnapMode: () => this.snapMode,
22782
24255
  drawingsPointerDown: (x, y, snap, shift) => this.userDrawings?.pointerDown(x, y, snap, shift),
22783
- drawingsPointerMove: (x, y, snap) => this.userDrawings?.pointerMove(x, y, snap),
24256
+ drawingsPointerMove: (x, y, snap, shift) => this.userDrawings?.pointerMove(x, y, snap, shift),
22784
24257
  drawingsPointerUp: (x, y) => this.userDrawings?.pointerUp(x, y),
22785
24258
  drawingsCursor: (x, y) => this.userDrawings?.cursorAt(x, y) ?? null,
22786
24259
  drawingsDblClick: (x, y) => this.userDrawings?.dblClick(x, y) ?? false,
@@ -22809,6 +24282,14 @@ var NativeRenderer = class {
22809
24282
  setToolbarGutter: (px) => this.setToolbarGutter(px)
22810
24283
  });
22811
24284
  this.userDrawings.historyChords = this.historyChordsEnabled;
24285
+ if (this.layoutMode === "mobile") {
24286
+ this.mountContainer?.setAttribute("data-vela-layout", this.layoutMode);
24287
+ this.userDrawings.setLayoutMode(this.layoutMode);
24288
+ if (this.scrollButton) {
24289
+ this.scrollButton.style.width = `${SCROLL_BTN_SIZE_TOUCH}px`;
24290
+ this.scrollButton.style.height = `${SCROLL_BTN_SIZE_TOUCH}px`;
24291
+ }
24292
+ }
22812
24293
  this.keyboard = new KeyboardController({
22813
24294
  panByBars: (bars) => this.panByBars(bars),
22814
24295
  zoomByStep: (dir) => this.zoomByStep(dir),
@@ -22821,10 +24302,13 @@ var NativeRenderer = class {
22821
24302
  });
22822
24303
  this.setKeyboardEnabled(this.keyboardEnabled);
22823
24304
  this.inputsUI = new InputsUI(this.plot, theme, (paneId) => this.paneBoundsFor(paneId));
24305
+ this.inputsUI.setLayoutMode(this.layoutMode);
22824
24306
  this.inputsUI.setTitlesVisible(this.indicatorTitlesOn);
24307
+ this.inputsUI.setValuesVisible(this.indicatorValuesOn);
22825
24308
  this.inputsUI.setDialogHost(this.dialogHost);
22826
24309
  this.inputsUI.setSymbolPicker(this.symbolPicker);
22827
24310
  this.inputsUI.setLegendActions(this.legendActionsProvider);
24311
+ this.inputsUI.setLegendOverviewAction(this.legendOverviewAction);
22828
24312
  this.inputsUI.setOnChange((c) => {
22829
24313
  for (const cb of this.inputChangeCbs) cb({ indicatorId: c.indicatorId, key: c.key, value: c.value });
22830
24314
  });
@@ -23017,6 +24501,7 @@ var NativeRenderer = class {
23017
24501
  this.attributionEl?.remove();
23018
24502
  this.attributionEl = null;
23019
24503
  this.mountContainer?.style.removeProperty("--vela-toolbar-gutter");
24504
+ this.mountContainer?.style.removeProperty("--vela-scale-gutter");
23020
24505
  this.mountContainer = null;
23021
24506
  this.wrapper?.remove();
23022
24507
  }
@@ -23185,6 +24670,7 @@ var NativeRenderer = class {
23185
24670
  const w = RIGHT_AXIS_W + AXIS_COL_W * this.maxOwnScaleColumns();
23186
24671
  if (w === this.rightAxisW) return false;
23187
24672
  this.rightAxisW = w;
24673
+ this.publishGutters();
23188
24674
  if (this.coords.width > 0) this.syncSize();
23189
24675
  return true;
23190
24676
  }
@@ -23237,7 +24723,10 @@ var NativeRenderer = class {
23237
24723
  this.scene.indicators.set(model.id, model);
23238
24724
  this.refreshAnchorOffset(model);
23239
24725
  this.scene.assignIndicatorZ(model.id);
23240
- this.inputsUI.upsert(model.id, model.title, model.inputs, model.inputValues, model.paneId, { native: !!model.native });
24726
+ this.inputsUI.upsert(model.id, model.shorttitle ?? model.title, model.inputs, model.inputValues, model.paneId, {
24727
+ native: !!model.native,
24728
+ ...model.shorttitle ? { settingsTitle: model.title } : {}
24729
+ });
23241
24730
  this.syncTables(model);
23242
24731
  if (model.native?.type === "volume") {
23243
24732
  this.volumeActive = true;
@@ -23298,6 +24787,13 @@ var NativeRenderer = class {
23298
24787
  this.legendActionsProvider = provider;
23299
24788
  this.inputsUI?.setLegendActions(provider);
23300
24789
  }
24790
+ setLegendOverviewAction(action) {
24791
+ this.legendOverviewAction = action;
24792
+ this.inputsUI?.setLegendOverviewAction(action);
24793
+ }
24794
+ openIndicatorSettings(indicatorId) {
24795
+ this.inputsUI?.openSettingsFor(indicatorId);
24796
+ }
23301
24797
  /**
23302
24798
  * Hide/show a mounted indicator. Hiding drops its model from the scene (so every paint path —
23303
24799
  * series, fills, drawings, tables, glow, data window — skips it) while keeping its z key and its
@@ -23344,12 +24840,31 @@ var NativeRenderer = class {
23344
24840
  if (style === this.scene.priceStyle) return;
23345
24841
  this.scene.priceStyle = style;
23346
24842
  this.scene.basePainting = basePaintingOf(style);
24843
+ this.scene.candleOverride = candleOverrideFor(style, this.scene.style.chartTypes);
23347
24844
  for (const cb of this.priceStyleCbs) cb(style);
23348
24845
  }
23349
24846
  onRemoveIndicator(cb) {
23350
24847
  this.removeIndicatorCbs.add(cb);
23351
24848
  return () => this.removeIndicatorCbs.delete(cb);
23352
24849
  }
24850
+ /** Port surface: the host shell's chrome size class. Mobile switches the renderer
24851
+ * chrome to its touch-first presentation — fullscreen dialogs, no docked drawing
24852
+ * toolbar (the shell provides its own picker), the scroll-to-latest button shown
24853
+ * whenever the latest bars are off-screen (touch has no cursor proximity). */
24854
+ setLayoutMode(mode) {
24855
+ if (mode === this.layoutMode) return;
24856
+ this.layoutMode = mode;
24857
+ this.mountContainer?.setAttribute("data-vela-layout", mode);
24858
+ this.userDrawings?.setLayoutMode(mode);
24859
+ this.settingsDialog?.setLayoutMode(mode);
24860
+ this.inputsUI?.setLayoutMode(mode);
24861
+ if (this.scrollButton) {
24862
+ const px = mode === "mobile" ? SCROLL_BTN_SIZE_TOUCH : SCROLL_BTN_SIZE;
24863
+ this.scrollButton.style.width = `${px}px`;
24864
+ this.scrollButton.style.height = `${px}px`;
24865
+ }
24866
+ this.updateScrollToRealtimeButton();
24867
+ }
23353
24868
  setSettingsSections(sections) {
23354
24869
  this.hostSettingsSections = sections;
23355
24870
  this.settingsDialog?.setHostSections(sections);
@@ -23374,6 +24889,10 @@ var NativeRenderer = class {
23374
24889
  this.clickCbs.add(cb);
23375
24890
  return () => this.clickCbs.delete(cb);
23376
24891
  }
24892
+ onAxisLongPress(cb) {
24893
+ this.axisLongPressCbs.add(cb);
24894
+ return () => this.axisLongPressCbs.delete(cb);
24895
+ }
23377
24896
  onViewportChange(cb) {
23378
24897
  this.viewportCbs.add(cb);
23379
24898
  return () => this.viewportCbs.delete(cb);
@@ -23505,13 +25024,17 @@ var NativeRenderer = class {
23505
25024
  active = true;
23506
25025
  }
23507
25026
  }
23508
- this.coords.setViewport(this.clampViewport(barSpacing, rightOffset));
25027
+ const clamped = this.clampViewport(barSpacing, rightOffset);
25028
+ if (clamped.barSpacing !== barSpacing) this.targetBarSpacing = clamped.barSpacing;
25029
+ if (this.scrollTargetRO != null && clamped.rightOffset !== rightOffset) this.scrollTargetRO = clamped.rightOffset;
25030
+ this.coords.setViewport(clamped);
23509
25031
  this.computeScales();
23510
25032
  if (this.easeScales(dtMs)) active = true;
23511
25033
  if (this.easeLiveBar(dtMs)) active = true;
23512
25034
  this.skeletonClockMs += dtMs;
23513
25035
  this.paintData();
23514
25036
  this.crosshairLayer.render(this.scene, this.coords, this.theme, this.hoverSeparatorY, this.externalCrossPx());
25037
+ this.updateLegendValues();
23515
25038
  this.emitViewportChange();
23516
25039
  return active;
23517
25040
  }
@@ -23576,7 +25099,7 @@ var NativeRenderer = class {
23576
25099
  this.lastPointer = null;
23577
25100
  this.scheduler.invalidate(1 /* Cursor */);
23578
25101
  this.hoverLogical = null;
23579
- const empty = { time: null, price: null, values: /* @__PURE__ */ new Map(), ohlc: null };
25102
+ const empty = { time: null, price: null, paneKind: null, values: /* @__PURE__ */ new Map(), ohlc: null };
23580
25103
  for (const cb of this.crosshairCbs) cb(empty);
23581
25104
  return;
23582
25105
  }
@@ -23588,7 +25111,7 @@ var NativeRenderer = class {
23588
25111
  const logical = Math.round(this.coords.xToLogical(x));
23589
25112
  const onBar = logical >= 0 && logical < this.coords.barCount;
23590
25113
  const time = onBar ? this.coords.logicalToTime(logical) : null;
23591
- const pane = this.paneAtY(y);
25114
+ const pane = this.paneNodeAtY(y);
23592
25115
  const price = inData && pane ? this.coords.yToPrice(y, pane.scale, pane.bounds) : null;
23593
25116
  const values = /* @__PURE__ */ new Map();
23594
25117
  if (onBar) {
@@ -23605,7 +25128,7 @@ var NativeRenderer = class {
23605
25128
  const bar = onBar ? this.bars[logical] : void 0;
23606
25129
  const ohlc = bar ? { time: bar.time, open: bar.open, high: bar.high, low: bar.low, close: bar.close, volume: bar.volume } : null;
23607
25130
  this.hoverLogical = onBar ? logical : null;
23608
- const event = { time, price, values, ohlc };
25131
+ const event = { time, price, paneKind: inData && pane ? pane.kind : null, values, ohlc };
23609
25132
  for (const cb of this.crosshairCbs) cb(event);
23610
25133
  }
23611
25134
  handleClick(x) {
@@ -23624,6 +25147,20 @@ var NativeRenderer = class {
23624
25147
  }
23625
25148
  return null;
23626
25149
  }
25150
+ /** The pane whose bounds sit closest to `y` — the forgiving fallback for points that
25151
+ * fall BETWEEN bounds (separator gaps, the strip under the last pane). */
25152
+ nearestPaneToY(y) {
25153
+ let best = null;
25154
+ let bestDist = Infinity;
25155
+ for (const pane of this.scene.panes.values()) {
25156
+ const d = y < pane.bounds.top ? pane.bounds.top - y : y - (pane.bounds.top + pane.bounds.height);
25157
+ if (d < bestDist) {
25158
+ bestDist = d;
25159
+ best = pane;
25160
+ }
25161
+ }
25162
+ return best;
25163
+ }
23627
25164
  // ── manual vertical scaling (price-axis drag + vertical price pan) ──
23628
25165
  /** The scale window a gesture targets: a pane's master scale, or a merged indicator's
23629
25166
  * own scale column (resolved from the grabbed x). Both share the four scale fields. */
@@ -23686,7 +25223,7 @@ var NativeRenderer = class {
23686
25223
  * still fits the view.)
23687
25224
  */
23688
25225
  dataDblClick(_x, y) {
23689
- const pane = this.paneNodeAtY(y);
25226
+ const pane = this.paneNodeAtY(y) ?? this.nearestPaneToY(y);
23690
25227
  if (!pane) return;
23691
25228
  const maximized = this.maximizedPaneId !== pane.id;
23692
25229
  this.maximizedPaneId = maximized ? pane.id : null;
@@ -23927,29 +25464,63 @@ var NativeRenderer = class {
23927
25464
  dataWindowGroups(idx, pricePane) {
23928
25465
  const groups = [];
23929
25466
  for (const model of this.scene.indicators.values()) {
23930
- const pane = (model.paneId ? this.scene.panes.get(model.paneId) : null) ?? pricePane;
23931
- const off = this.scene.offsetOf(model.id);
23932
- const rows = [];
23933
- for (const s of model.series) {
23934
- let value;
23935
- let color;
23936
- if (s.kind === "candle" || s.kind === "bar") {
23937
- value = s.bars[idx - off]?.close;
23938
- color = s.style?.up ?? this.theme.upColor;
23939
- } else if (isLineLikeSeries(s)) {
23940
- if (s.visible === false) continue;
23941
- value = s.points[idx - off]?.value;
23942
- color = s.points[idx - off]?.color ?? s.style.color;
23943
- } else {
23944
- continue;
23945
- }
23946
- if (value == null || !Number.isFinite(value)) continue;
23947
- rows.push({ label: s.title || model.title, value: this.dataWindowFmt(value, pane), color });
23948
- }
25467
+ const rows = this.dataWindowRowsFor(model, idx, pricePane);
23949
25468
  if (rows.length) groups.push({ name: model.title, rows });
23950
25469
  }
23951
25470
  return groups;
23952
25471
  }
25472
+ /** One indicator's readout at bar `idx`: a row per drawable plot, formatted on its pane's scale. */
25473
+ dataWindowRowsFor(model, idx, pricePane) {
25474
+ if (model.native?.type === "volume") return this.volumeReadoutRows(model, idx);
25475
+ const pane = (model.paneId ? this.scene.panes.get(model.paneId) : null) ?? pricePane;
25476
+ const off = this.scene.offsetOf(model.id);
25477
+ const rows = [];
25478
+ for (const s of model.series) {
25479
+ let value;
25480
+ let color;
25481
+ if (s.kind === "candle" || s.kind === "bar") {
25482
+ value = s.bars[idx - off]?.close;
25483
+ color = s.style?.up ?? this.theme.upColor;
25484
+ } else if (isLineLikeSeries(s)) {
25485
+ if (s.visible === false) continue;
25486
+ value = s.points[idx - off]?.value;
25487
+ color = s.points[idx - off]?.color ?? s.style.color;
25488
+ } else {
25489
+ continue;
25490
+ }
25491
+ if (value == null || !Number.isFinite(value)) continue;
25492
+ rows.push({ label: s.title || model.title, value: this.dataWindowFmt(value, pane), color });
25493
+ }
25494
+ return rows;
25495
+ }
25496
+ /** The volume indicator's readout: the bar's volume, tinted with the layer's own direction
25497
+ * colors. Hiding volume keeps its model in the scene (only the layer is suppressed — see
25498
+ * `setIndicatorVisible`), so the hidden flag is checked here rather than by model absence. */
25499
+ volumeReadoutRows(model, idx) {
25500
+ if (this.volumeHidden) return [];
25501
+ const bar = this.bars[idx];
25502
+ const vol = bar?.volume;
25503
+ if (bar == null || vol == null || !Number.isFinite(vol)) return [];
25504
+ const cfg = this.scene.volumeLayer;
25505
+ const color = bar.close >= bar.open ? cfg?.upColor ?? this.theme.upColor : cfg?.downColor ?? this.theme.downColor;
25506
+ return [{ label: model.title, value: formatVolume(vol), color }];
25507
+ }
25508
+ /** Refresh the plot values beside every legend title — the same readout the data window
25509
+ * shows (crosshair bar, else the latest bar), pushed per paint. A hidden indicator has
25510
+ * no scene model, so its row is absent from the map and its readout clears. */
25511
+ updateLegendValues() {
25512
+ if (!this.inputsUI) return;
25513
+ const n = this.bars.length;
25514
+ const values = /* @__PURE__ */ new Map();
25515
+ if (n > 0) {
25516
+ const idx = this.hoverLogical != null ? this.hoverLogical : n - 1;
25517
+ const pricePane = this.dataWindowPricePane();
25518
+ for (const model of this.scene.indicators.values()) {
25519
+ values.set(model.id, this.dataWindowRowsFor(model, idx, pricePane).map((r) => ({ value: r.value, color: r.color })));
25520
+ }
25521
+ }
25522
+ this.inputsUI.setPlotValues(values);
25523
+ }
23953
25524
  renderFrame(level) {
23954
25525
  if (this.animator.active) return;
23955
25526
  if (repaintsData(level)) {
@@ -23957,9 +25528,38 @@ var NativeRenderer = class {
23957
25528
  this.paintData();
23958
25529
  } else if (repaintsChrome(level) && this.paintedData) {
23959
25530
  this.chrome.prepare(this.scene, this.coords, this.theme);
23960
- this.chrome.render(this.scene, this.coords, this.theme, { background: this.surfaceBackground, textColor: this.surfaceTextColor });
25531
+ this.chrome.render(this.scene, this.coords, this.theme, this.axisSurface());
23961
25532
  }
23962
25533
  this.crosshairLayer.render(this.scene, this.coords, this.theme, this.hoverSeparatorY, this.externalCrossPx());
25534
+ if (!repaintsData(level) && this.paintedData) this.repaintCursorLayers();
25535
+ this.updateLegendValues();
25536
+ }
25537
+ /** Repaint the SDK layers that opted into cursor tracking (their own canvas only). */
25538
+ repaintCursorLayers() {
25539
+ const pane = this.scene.panes.get(PRICE_PANE_ID);
25540
+ if (!pane) return;
25541
+ const nowMs = typeof performance !== "undefined" ? performance.now() : Date.now();
25542
+ for (const l of this.extLayers) {
25543
+ if (!l.def.repaintOnCursor) continue;
25544
+ l.instance.render(this.extLayerArgs(l.def.id, pane.scale, pane.bounds, nowMs));
25545
+ if (this.animZoom && l.instance.animating?.()) this.animator.start();
25546
+ }
25547
+ }
25548
+ /** One frame's args for an SDK renderer layer (shared by the data + cursor paint paths). */
25549
+ extLayerArgs(id, scale, bounds, nowMs) {
25550
+ return {
25551
+ bars: this.scene.bars,
25552
+ data: this.scene.nativeData.get(id),
25553
+ settings: this.scene.nativeData.get(`${id}-settings`) ?? {},
25554
+ pending: this.scene.nativePending.get(id) ?? [],
25555
+ coords: this.coords,
25556
+ scale,
25557
+ bounds,
25558
+ theme: this.theme,
25559
+ priceStyle: this.scene.priceStyle,
25560
+ nowMs,
25561
+ cursor: this.lastPointer
25562
+ };
23963
25563
  }
23964
25564
  /** Paint the below-data (L-1) + geometry (L0) + chrome (L1) layers from the current scene/coords. */
23965
25565
  paintData() {
@@ -23992,18 +25592,16 @@ var NativeRenderer = class {
23992
25592
  });
23993
25593
  const nowMs = typeof performance !== "undefined" ? performance.now() : Date.now();
23994
25594
  for (const l of this.extLayers) {
23995
- l.instance.render({
23996
- bars: this.scene.bars,
23997
- data: this.scene.nativeData.get(l.def.id),
23998
- settings: this.scene.nativeData.get(`${l.def.id}-settings`) ?? {},
23999
- pending: this.scene.nativePending.get(l.def.id) ?? [],
24000
- coords: this.coords,
24001
- scale: pane.scale,
24002
- bounds: pane.bounds,
24003
- theme: this.theme,
24004
- priceStyle: this.scene.priceStyle,
24005
- nowMs
24006
- });
25595
+ const args = this.extLayerArgs(l.def.id, pane.scale, pane.bounds, nowMs);
25596
+ l.instance.render(args);
25597
+ if (l.def.id === this.scene.priceStyle && l.instance.modulateBase) {
25598
+ const mod = l.instance.modulateBase(args);
25599
+ if (mod) {
25600
+ if (mod.candleBodyScale != null) this.backend.candleBodyScale = clamp012(mod.candleBodyScale) || 0.01;
25601
+ if (mod.candleBodyAlpha != null) this.backend.candleBodyAlpha = clamp012(mod.candleBodyAlpha) * this.candleBodyAlpha;
25602
+ if (mod.gridAlpha != null) this.backend.gridAlpha = clamp012(mod.gridAlpha);
25603
+ }
25604
+ }
24007
25605
  if (this.animZoom && l.instance.animating?.()) this.animator.start();
24008
25606
  }
24009
25607
  }
@@ -24013,7 +25611,7 @@ var NativeRenderer = class {
24013
25611
  if (easeLive && liveActual) this.bars[li] = { ...liveActual, high: this.liveEaseHigh, low: this.liveEaseLow, close: this.liveEaseClose };
24014
25612
  this.scene.drawingSlices = this.userDrawings?.prepareSlices(this.scene.orderedPanes().map((p) => p.id)) ?? /* @__PURE__ */ new Map();
24015
25613
  this.backend.render(this.scene, this.coords, this.theme);
24016
- this.chrome.render(this.scene, this.coords, this.theme, { background: this.surfaceBackground, textColor: this.surfaceTextColor });
25614
+ this.chrome.render(this.scene, this.coords, this.theme, this.axisSurface());
24017
25615
  this.userDrawings?.render();
24018
25616
  if (easeLive && liveActual) this.bars[li] = liveActual;
24019
25617
  this.paintedData = true;
@@ -24083,7 +25681,7 @@ var NativeRenderer = class {
24083
25681
  if (!Number.isFinite(y) || y < pricePane.bounds.top || y > pricePane.bounds.top + pricePane.bounds.height) y = null;
24084
25682
  }
24085
25683
  }
24086
- return { x, y, time: this.coords.logicalToTime(logical) };
25684
+ return { x, y, time: this.coords.logicalToTime(logical), price: y != null ? ext.price : null };
24087
25685
  }
24088
25686
  /** Set the sticky magnet mode (called by the drawings toolbar's 3-state button). */
24089
25687
  setSnapMode(mode) {
@@ -24447,15 +26045,18 @@ var NativeRenderer = class {
24447
26045
  setToolbarGutter(px) {
24448
26046
  if (px === this.toolbarGutter) return;
24449
26047
  this.toolbarGutter = px;
24450
- this.publishToolbarGutter();
26048
+ this.publishGutters();
24451
26049
  this.positionAttribution();
24452
26050
  this.syncSize();
24453
26051
  }
24454
- /** Publish the gutter on the mount container as `--vela-toolbar-gutter`, so host
24455
- * overlays sharing that container (a status line, a custom legend) can anchor to
24456
- * the plot's left edge without reaching into the renderer's DOM. */
24457
- publishToolbarGutter() {
26052
+ /** Publish the gutters on the mount container as `--vela-toolbar-gutter` (left,
26053
+ * drawings toolbar) and `--vela-scale-gutter` (right, the full price-scale width
26054
+ * incl. merged own-scale columns), so host overlays sharing that container (a
26055
+ * status line, a watermark, a custom legend) can anchor to the plot's edges
26056
+ * without reaching into the renderer's DOM. */
26057
+ publishGutters() {
24458
26058
  this.mountContainer?.style.setProperty("--vela-toolbar-gutter", `${this.toolbarGutter}px`);
26059
+ this.mountContainer?.style.setProperty("--vela-scale-gutter", `${this.rightAxisW}px`);
24459
26060
  }
24460
26061
  /** The built-in mark, or the host's own when one is set. */
24461
26062
  buildAttributionEl() {
@@ -24525,7 +26126,13 @@ var NativeRenderer = class {
24525
26126
  this.fitContent();
24526
26127
  this.didInitialFit = true;
24527
26128
  }
24528
- this.scheduler.flushNow(4 /* Full */);
26129
+ if (this.animator.active) {
26130
+ this.computeScales();
26131
+ this.paintData();
26132
+ this.crosshairLayer.render(this.scene, this.coords, this.theme, this.hoverSeparatorY, this.externalCrossPx());
26133
+ } else {
26134
+ this.scheduler.flushNow(4 /* Full */);
26135
+ }
24529
26136
  }
24530
26137
  applyBackground() {
24531
26138
  this.wrapper.style.background = this.theme.background;
@@ -24594,6 +26201,9 @@ function sanitizeHighlights(value) {
24594
26201
  }
24595
26202
  return out.sort((a, b) => a.from - b.from);
24596
26203
  }
26204
+ function clamp012(v) {
26205
+ return Math.max(0, Math.min(1, v));
26206
+ }
24597
26207
  function isPriceStyle2(v) {
24598
26208
  return typeof v === "string" && priceStyleIds().includes(v);
24599
26209
  }
@@ -24763,7 +26373,8 @@ var VpvrIndicator = class {
24763
26373
  };
24764
26374
  var vpvrDescriptor = {
24765
26375
  type: "vpvr",
24766
- title: "VPVR",
26376
+ title: "Visible Range Volume Profile",
26377
+ shortTitle: "VRVP",
24767
26378
  paneHint: "price",
24768
26379
  overlay: true,
24769
26380
  inputsSchema: () => [
@@ -25261,6 +26872,7 @@ exports.iconMarkup = iconMarkup;
25261
26872
  exports.legendActions = legendActions;
25262
26873
  exports.nativeIndicatorDescriptors = nativeIndicatorDescriptors;
25263
26874
  exports.nativeIndicatorTypes = nativeIndicatorTypes;
26875
+ exports.normalizeSettingsRow = normalizeSettingsRow;
25264
26876
  exports.registerChartType = registerChartType;
25265
26877
  exports.registerDefaultEngine = registerDefaultEngine;
25266
26878
  exports.registerDrawingType = registerDrawingType;
@@ -25276,6 +26888,7 @@ exports.rendererDefaults = rendererDefaults;
25276
26888
  exports.rendererLayers = rendererLayers;
25277
26889
  exports.resolveEngines = resolveEngines;
25278
26890
  exports.resolveTheme = resolveTheme;
26891
+ exports.settingsRowValueKeys = settingsRowValueKeys;
25279
26892
  exports.sharedBarStore = sharedBarStore;
25280
26893
  exports.sidePanels = sidePanels;
25281
26894
  exports.stableSeriesId = stableSeriesId;