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