@luxalgo/vela 0.5.1 → 0.5.3

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-DGesjOJT.d.ts → DataProvider-DTOEXKTI.d.ts} +1 -1
  2. package/dist/{DataProvider-BBQ3Hr21.d.cts → DataProvider-gnzv-dgc.d.cts} +1 -1
  3. package/dist/{chunk-ULIX6AE2.js → chunk-ATPU5CI6.js} +1 -1
  4. package/dist/{chunk-5SE4PL3H.js → chunk-EY63XBFZ.js} +1370 -55
  5. package/dist/{chunk-FNOMKHEZ.js → chunk-L5NTMWPP.js} +1383 -376
  6. package/dist/{chunk-EP6NDJYK.js → chunk-LJLZR44Y.js} +4 -2
  7. package/dist/{chunk-AIW3EJUE.js → chunk-P556H6EA.js} +329 -10
  8. package/dist/{chunk-S6F6SQ2L.js → chunk-WJKLBYRM.js} +74 -7
  9. package/dist/{contributions-BlV3kb0O.d.cts → contributions-Ci_i3IN2.d.cts} +33 -2
  10. package/dist/{contributions-6N0rbrPC.d.ts → contributions-b6AVrqqG.d.ts} +33 -2
  11. package/dist/{history-DX3Zne3a.d.cts → history-Cf1lVsap.d.cts} +15 -3
  12. package/dist/{history-BTd52YMA.d.ts → history-Ci3M0xLV.d.ts} +15 -3
  13. package/dist/index.cjs +1450 -379
  14. package/dist/index.d.cts +60 -21
  15. package/dist/index.d.ts +60 -21
  16. package/dist/index.js +4 -4
  17. package/dist/{options-DbQholFV.d.cts → options-XTBpbx0s.d.cts} +47 -1
  18. package/dist/{options-DbQholFV.d.ts → options-XTBpbx0s.d.ts} +47 -1
  19. package/dist/{plugin-TN3n-9YN.d.ts → plugin-DP6A4t4o.d.ts} +177 -29
  20. package/dist/{plugin-O34MbXf-.d.cts → plugin-DlidwMj6.d.cts} +177 -29
  21. package/dist/plugin.cjs +53 -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 +332 -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 +1450 -379
  36. package/dist/vela.global.min.js +84 -34
  37. package/dist/widget.cjs +3430 -569
  38. package/dist/widget.d.cts +66 -8
  39. package/dist/widget.d.ts +66 -8
  40. package/dist/widget.js +169 -18
  41. package/dist/workspace.cjs +4332 -1418
  42. package/dist/workspace.d.cts +45 -9
  43. package/dist/workspace.d.ts +45 -9
  44. package/dist/workspace.js +226 -22
  45. package/package.json +1 -1
package/dist/index.cjs CHANGED
@@ -481,14 +481,15 @@ registerIcon(
481
481
  svg24('<path d="m10 20-1.25-2.5L6 16"/><path d="M10 4 8.75 6.5 6 8"/><path d="m14 20 1.25-2.5L18 16"/><path d="m14 4 1.25 2.5L18 8"/><path d="m17 10-5 5-5-5"/><path d="M4 12h16"/>')
482
482
  );
483
483
  registerIcon("chevron-right", S('<path d="m6 3.5 4.5 4.5L6 12.5"/>'));
484
+ registerIcon("chevron-left", S('<path d="M10 3.5 5.5 8l4.5 4.5"/>'));
484
485
  registerIcon("chevron-down", S('<path d="M3.5 6 8 10.5 12.5 6"/>'));
485
486
  registerIcon("chevron-up", S('<path d="M3.5 10 8 5.5 12.5 10"/>'));
486
487
  registerIcon("chevrons-right", S('<path d="m4 3.5 4.5 4.5L4 12.5"/><path d="m8.5 3.5 4.5 4.5-4.5 4.5"/>'));
487
488
  registerIcon("chevrons-left", S('<path d="M12 3.5 7.5 8l4.5 4.5"/><path d="M7.5 3.5 3 8l4.5 4.5"/>'));
488
489
  registerIcon("check", S('<path d="m2.8 8.4 3.4 3.4 7-7.6"/>'));
489
490
  registerIcon("close", S('<path d="m3.8 3.8 8.4 8.4M12.2 3.8l-8.4 8.4"/>'));
490
- registerIcon("eye", S('<path d="M1.5 8s2.5-4.2 6.5-4.2S14.5 8 14.5 8 12 12.2 8 12.2 1.5 8 1.5 8z"/><circle cx="8" cy="8" r="1.8"/>'));
491
- registerIcon("eye-off", S('<path d="M1.5 8s2.5-4.2 6.5-4.2S14.5 8 14.5 8 12 12.2 8 12.2 1.5 8 1.5 8z" opacity="0.45"/><path d="m3 13 10-10"/>'));
491
+ registerIcon("eye", S('<path d="M1.5 8s2.5-5.4 6.5-5.4S14.5 8 14.5 8 12 13.4 8 13.4 1.5 8 1.5 8z"/><circle cx="8" cy="8" r="1.8"/>'));
492
+ registerIcon("eye-off", S('<path d="M1.5 8s2.5-5.4 6.5-5.4S14.5 8 14.5 8 12 13.4 8 13.4 1.5 8 1.5 8z" opacity="0.45"/><path d="m3 13 10-10"/>'));
492
493
  registerIcon("lock", S('<rect x="3.2" y="7" width="9.6" height="6.6" rx="1.2"/><path d="M5.6 7V5.2a2.4 2.4 0 0 1 4.8 0V7"/>'));
493
494
  registerIcon("unlock", S('<rect x="3.2" y="7" width="9.6" height="6.6" rx="1.2"/><path d="M5.6 7V5.2a2.4 2.4 0 0 1 4.7-.7"/>'));
494
495
  registerIcon("trash", S('<path d="M2.5 4.3h11M6.2 4.3V3.1a1 1 0 0 1 1-1h1.6a1 1 0 0 1 1 1v1.2M4.2 4.3l.5 9.1a1.15 1.15 0 0 0 1.15 1.1h4.3a1.15 1.15 0 0 0 1.15-1.1l.5-9.1M6.5 7v4.5M9.5 7v4.5"/>'));
@@ -511,6 +512,7 @@ registerIcon("star", S('<path d="M8 2.2l1.75 3.55 3.9.55-2.8 2.75.65 3.9L8 11.1l
511
512
  registerIcon("star-filled", S('<path d="M8 2.2l1.75 3.55 3.9.55-2.8 2.75.65 3.9L8 11.1l-3.5 1.85.65-3.9-2.8-2.75 3.9-.55z"/>', 'fill="currentColor"'));
512
513
  registerIcon("grip", S('<circle cx="6" cy="3.5" r="1"/><circle cx="10" cy="3.5" r="1"/><circle cx="6" cy="8" r="1"/><circle cx="10" cy="8" r="1"/><circle cx="6" cy="12.5" r="1"/><circle cx="10" cy="12.5" r="1"/>', 'fill="currentColor" stroke="none"'));
513
514
  registerIcon("kebab", S('<circle cx="8" cy="3.2" r="1.2"/><circle cx="8" cy="8" r="1.2"/><circle cx="8" cy="12.8" r="1.2"/>', 'fill="currentColor" stroke="none"'));
515
+ registerIcon("burger", S('<path d="M2.5 4.5h11M2.5 8h11M2.5 11.5h11"/>'));
514
516
  registerIcon("reset", S('<rect x="6" y="6" width="4" height="4" rx="0.8"/><path d="M2.2 8a5.8 5.8 0 1 0 1.9-4.3L2.2 5.3"/><path d="M2.2 2.2v3.1h3.1"/>'));
515
517
  registerIcon("pane-collapse", S('<path d="M2.6 9.4h4v4M13.4 6.6h-4v-4"/><path d="m9.4 6.6 4.2-4.2M2.4 13.6l4.2-4.2"/>'));
516
518
  registerIcon("pane-expand", S('<path d="M9.8 2.4h3.8v3.8M6.2 13.6H2.4V9.8"/><path d="m13.6 2.4-4.4 4.4M2.4 13.6l4.4-4.4"/>'));
@@ -1889,10 +1891,10 @@ function polylineHit(px, py, pts, tol) {
1889
1891
  }
1890
1892
 
1891
1893
  // src/core/drawings/types/FixedRangeVolumeProfile.ts
1892
- var OUTSIDE_UP = `${BULLISH}BF`;
1893
- var OUTSIDE_DOWN = `${BEARISH}BF`;
1894
- var VA_UP = `${BULLISH}66`;
1895
- var VA_DOWN = `${BEARISH}66`;
1894
+ var OUTSIDE_UP = `${BULLISH}66`;
1895
+ var OUTSIDE_DOWN = `${BEARISH}66`;
1896
+ var VA_UP = `${BULLISH}BF`;
1897
+ var VA_DOWN = `${BEARISH}BF`;
1896
1898
  function defaultFrvpStyle() {
1897
1899
  return {
1898
1900
  rows: 24,
@@ -1910,7 +1912,8 @@ function defaultFrvpStyle() {
1910
1912
  valColor: NEUTRAL,
1911
1913
  valStyle: "solid",
1912
1914
  showPoc: true,
1913
- pocColor: ACCENT,
1915
+ pocColor: void 0,
1916
+ // theme contrast ink until the user picks a color
1914
1917
  pocStyle: "solid",
1915
1918
  showDevelopingPoc: false,
1916
1919
  developingPocColor: ACCENT,
@@ -7052,6 +7055,47 @@ var ProviderRegistry = class {
7052
7055
  };
7053
7056
 
7054
7057
  // src/chart-types/registry.ts
7058
+ function normalizeSettingsRow(r) {
7059
+ switch (r.kind) {
7060
+ case "row":
7061
+ return { label: r.label, toggle: r.toggle, controls: r.controls, when: r.when };
7062
+ case "toggle": {
7063
+ const controls = [];
7064
+ if (r.number) controls.push({ kind: "number", ...r.number });
7065
+ for (const c of r.colors ?? []) controls.push({ kind: "color", ...c });
7066
+ if (r.width) controls.push({ kind: "width", ...r.width });
7067
+ return { label: r.label, toggle: { key: r.key, defval: r.defval }, controls, when: r.when };
7068
+ }
7069
+ case "number":
7070
+ return { label: r.label, controls: [{ kind: "number", key: r.key, label: r.label, defval: r.defval, min: r.min, max: r.max, step: r.step }], when: r.when };
7071
+ case "color":
7072
+ return { label: r.label, controls: [{ kind: "color", key: r.key, label: r.label, defval: r.defval }], when: r.when };
7073
+ case "select":
7074
+ return { label: r.label, controls: [{ kind: "select", key: r.key, label: r.label, options: r.options, defval: r.defval }], when: r.when };
7075
+ case "range":
7076
+ return {
7077
+ label: r.label,
7078
+ controls: [
7079
+ { kind: "number", key: r.minKey, label: `${r.label} \u2014 min`, defval: r.defval, min: r.min, max: r.max, step: r.step, placeholder: r.placeholder },
7080
+ { kind: "hint", text: "\u2013" },
7081
+ { kind: "number", key: r.maxKey, label: `${r.label} \u2014 max`, defval: r.defval, min: r.min, max: r.max, step: r.step, placeholder: r.placeholder }
7082
+ ],
7083
+ when: r.when
7084
+ };
7085
+ }
7086
+ }
7087
+ function settingsRowValueKeys(r) {
7088
+ if (r.kind === "heading" || r.kind === "header") return [];
7089
+ const n = normalizeSettingsRow(r);
7090
+ const keys = [];
7091
+ if (n.toggle) keys.push({ key: n.toggle.key, type: "boolean", defval: n.toggle.defval });
7092
+ for (const c of n.controls) {
7093
+ if (c.kind === "hint") continue;
7094
+ if (c.kind === "color" || c.kind === "select") keys.push({ key: c.key, type: "string", defval: c.defval });
7095
+ else keys.push({ key: c.key, type: "number", defval: c.defval });
7096
+ }
7097
+ return keys;
7098
+ }
7055
7099
  function settingsRowVisible(when, bag) {
7056
7100
  if (!when) return true;
7057
7101
  const conds = Array.isArray(when) ? when : [when];
@@ -7156,6 +7200,9 @@ var EngineOrchestrator = class _EngineOrchestrator {
7156
7200
  this.barTransform = null;
7157
7201
  // ── chart-type data engines (SDK): one lazily-created engine per style id ──
7158
7202
  this.typeEngines = /* @__PURE__ */ new Map();
7203
+ /** Last-seen chart-type settings per type id (dialog edits + persisted restores) —
7204
+ * replayed into a data engine whenever one is (re)created. */
7205
+ this.typeSettings = /* @__PURE__ */ new Map();
7159
7206
  /** The style id whose data engine is currently ACTIVE (drives suspend/resume + pokes). */
7160
7207
  this.activeEngineStyle = null;
7161
7208
  /** The chart's current price style (tracked from the renderer's change events + initial read). */
@@ -7218,7 +7265,10 @@ var EngineOrchestrator = class _EngineOrchestrator {
7218
7265
  this.renderer.onRemoveIndicator((id) => this.removeIndicator(id));
7219
7266
  this.renderer.onToggleIndicatorVisible?.((id, visible) => this.setVisible(id, visible));
7220
7267
  this.paneActionUnsub = this.renderer.onPaneAction?.((a) => this.handlePaneAction(a)) ?? null;
7221
- this.renderer.onChartTypeSettingsChange?.((typeId, values) => this.typeEngines.get(typeId)?.onSettings?.(values));
7268
+ this.renderer.onChartTypeSettingsChange?.((typeId, values) => {
7269
+ this.typeSettings.set(typeId, values);
7270
+ this.typeEngines.get(typeId)?.onSettings?.(values);
7271
+ });
7222
7272
  this.renderer.onMoveIndicator?.((id, target) => this.moveIndicator(id, target));
7223
7273
  this.renderer.onThemeSelect?.((name) => this.setTheme(resolveTheme(name)));
7224
7274
  this.viewportUnsub = this.renderer.onViewportChange((range) => this.onViewportChange(range));
@@ -7706,6 +7756,8 @@ var EngineOrchestrator = class _EngineOrchestrator {
7706
7756
  if (!def?.dataEngine) return;
7707
7757
  const engine = def.dataEngine();
7708
7758
  this.typeEngines.set(id, engine);
7759
+ const stored = this.typeSettings.get(id);
7760
+ if (stored) engine.onSettings?.(stored);
7709
7761
  engine.start({
7710
7762
  symbol: this.config.market.symbol ?? "TEST",
7711
7763
  timeframe: this.config.market.timeframe ?? "60",
@@ -8177,6 +8229,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
8177
8229
  return {
8178
8230
  id: record.id,
8179
8231
  title: record.title,
8232
+ ...d.shortTitle ? { shorttitle: d.shortTitle } : {},
8180
8233
  overlay: d.overlay,
8181
8234
  paneHint: d.paneHint,
8182
8235
  native: { type: record.native.type },
@@ -8637,6 +8690,28 @@ var RendererControl = class {
8637
8690
  setLegendActions(provider) {
8638
8691
  this.renderer.setLegendActions?.(provider);
8639
8692
  }
8693
+ /**
8694
+ * Replace the indicator legend's fold toggle with a host action (or restore it with
8695
+ * `null`) — multi-chart shells point the chip at their indicator overview instead of
8696
+ * unfolding rows in place. Silent no-op on a renderer without a foldable legend.
8697
+ */
8698
+ setLegendOverviewAction(action) {
8699
+ this.renderer.setLegendOverviewAction?.(action);
8700
+ return this;
8701
+ }
8702
+ /** Whether the active renderer can open a per-indicator settings dialog. */
8703
+ get supportsIndicatorSettings() {
8704
+ return typeof this.renderer.openIndicatorSettings === "function";
8705
+ }
8706
+ /**
8707
+ * Open one indicator's settings dialog — the programmatic twin of the legend gear
8708
+ * (see {@link supportsIndicatorSettings}). Silent no-op without the seam or for an
8709
+ * unknown id.
8710
+ */
8711
+ openIndicatorSettings(indicatorId) {
8712
+ this.renderer.openIndicatorSettings?.(indicatorId);
8713
+ return this;
8714
+ }
8640
8715
  /**
8641
8716
  * Export the current chart as a PNG data URL, or null if the active renderer
8642
8717
  * doesn't support it (warns). DOM overlays (tables, legend) are not included.
@@ -8683,6 +8758,10 @@ var RendererControl = class {
8683
8758
  onCrosshairMove(cb) {
8684
8759
  return this.renderer.onCrosshairMove(cb);
8685
8760
  }
8761
+ /** Touch long-press on a price or time axis strip — silent no-op without the seam. */
8762
+ onAxisLongPress(cb) {
8763
+ return this.renderer.onAxisLongPress?.(cb) ?? (() => void 0);
8764
+ }
8686
8765
  /**
8687
8766
  * The current data-window readout — the hovered bar's date/time and OHLCV plus every
8688
8767
  * indicator's value there, each already formatted on its pane's scale (the latest bar when
@@ -8748,6 +8827,13 @@ var RendererControl = class {
8748
8827
  this.renderer.setSettingsSections?.(sections);
8749
8828
  return this;
8750
8829
  }
8830
+ /** Tell the renderer's own chrome which size class the host shell is in —
8831
+ * `'mobile'` switches its dialogs/toolbars to the touch-first presentation.
8832
+ * Silent no-op on a renderer without adaptive chrome. */
8833
+ setLayoutMode(mode) {
8834
+ this.renderer.setLayoutMode?.(mode);
8835
+ return this;
8836
+ }
8751
8837
  };
8752
8838
 
8753
8839
  // src/core/renderer-defaults.ts
@@ -9699,7 +9785,7 @@ function attachChromeTooltip(anchor, opts) {
9699
9785
  const doc = anchor.ownerDocument;
9700
9786
  tip = doc.createElement("div");
9701
9787
  tip.textContent = text;
9702
- tip.style.cssText = "position:absolute;z-index:25;pointer-events:none;background:var(--vela-bg);border:1px solid var(--vela-border);color:var(--vela-fg);border-radius:var(--vela-radius-md);padding:4px 9px;box-shadow:var(--vela-shadow);font:var(--vela-font-size-md) var(--vela-font);" + (opts.wrap ? "max-width:260px;" : "white-space:nowrap;");
9788
+ tip.style.cssText = "position:absolute;z-index:var(--vela-z-tooltip);pointer-events:none;background:var(--vela-bg);border:1px solid var(--vela-border);color:var(--vela-fg);border-radius:var(--vela-radius-md);padding:4px 9px;box-shadow:var(--vela-shadow);font:var(--vela-font-size-md) var(--vela-font);" + (opts.wrap ? "max-width:260px;" : "white-space:nowrap;");
9703
9789
  applyChromeTokens(tip, opts.theme());
9704
9790
  opts.host.appendChild(tip);
9705
9791
  const a = anchor.getBoundingClientRect();
@@ -9713,16 +9799,17 @@ function attachChromeTooltip(anchor, opts) {
9713
9799
  tip.style.top = `${a.bottom - h.top + 6}px`;
9714
9800
  }
9715
9801
  };
9716
- const arm = () => {
9802
+ const arm = (e) => {
9803
+ if (e.pointerType !== "mouse") return;
9717
9804
  clear();
9718
9805
  timer = window.setTimeout(show, opts.delayMs ?? 700);
9719
9806
  };
9720
- anchor.addEventListener("mouseenter", arm);
9721
- anchor.addEventListener("mouseleave", clear);
9807
+ anchor.addEventListener("pointerenter", arm);
9808
+ anchor.addEventListener("pointerleave", clear);
9722
9809
  anchor.addEventListener("pointerdown", clear);
9723
9810
  return () => {
9724
- anchor.removeEventListener("mouseenter", arm);
9725
- anchor.removeEventListener("mouseleave", clear);
9811
+ anchor.removeEventListener("pointerenter", arm);
9812
+ anchor.removeEventListener("pointerleave", clear);
9726
9813
  anchor.removeEventListener("pointerdown", clear);
9727
9814
  clear();
9728
9815
  };
@@ -9789,14 +9876,22 @@ function makeDialogDraggable(box, header, options) {
9789
9876
 
9790
9877
  // src/renderers/shared/InputsUI.ts
9791
9878
  var SOURCES = ["close", "open", "high", "low", "hl2", "hlc3", "ohlc4", "volume"];
9792
- 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)}}";
9793
- var LEGEND_ICON_PX = 13;
9879
+ var STATUS_KEYFRAMES = "@keyframes vela-ind-pulse{0%,100%{opacity:1;transform:scale(1)}50%{opacity:.3;transform:scale(.6)}}";
9880
+ var LEGEND_ICON_PX = 16;
9881
+ var LEGEND_CTL_PX = 18;
9882
+ var LEGEND_ROW_PAD_Y = 2;
9883
+ var LEGEND_ROW_PAD_X = 6;
9884
+ var LEGEND_TITLE_VALUES_GAP_PX = 6;
9885
+ var LEGEND_ACTION_GAP_PX = 6;
9886
+ var LEGEND_CTL_CSS = `cursor:pointer;display:inline-flex;align-items:center;justify-content:center;border:none;line-height:0;padding:0;width:${LEGEND_CTL_PX}px;height:${LEGEND_CTL_PX}px;border-radius:3px;box-sizing:border-box;flex:none;`;
9887
+ var LEGEND_ROW_MIN_H = LEGEND_CTL_PX + LEGEND_ROW_PAD_Y * 2;
9794
9888
  var EYE_SVG = iconAt("eye", LEGEND_ICON_PX);
9795
9889
  var EYE_OFF_SVG = iconAt("eye-off", LEGEND_ICON_PX);
9796
9890
  var GEAR_SVG = iconAt("gear", LEGEND_ICON_PX);
9797
- var CLOSE_SVG = iconAt("close", 11);
9891
+ var CLOSE_SVG = iconAt("close", LEGEND_ICON_PX);
9798
9892
  var FOLD_SVG = iconAt("chevron-up", LEGEND_ICON_PX);
9799
9893
  var UNFOLD_SVG = iconAt("chevron-down", LEGEND_ICON_PX);
9894
+ var OVERVIEW_SVG = iconAt("objects", LEGEND_ICON_PX);
9800
9895
  var InputsUI = class {
9801
9896
  constructor(container, theme, paneBoundsOf) {
9802
9897
  this.container = container;
@@ -9817,6 +9912,8 @@ var InputsUI = class {
9817
9912
  this.onToggleVisible = null;
9818
9913
  /** Host symbol picker for `input.symbol`; when set the control opens the host's ticker UI. */
9819
9914
  this.symbolPicker = null;
9915
+ /** Mobile chrome: the inputs dialog opens fullscreen and the legends render compact. */
9916
+ this.mobileLayout = false;
9820
9917
  /** Pane move/merge hook — when set, rows get a "Move to" menu + become drag-to-pane sources. */
9821
9918
  this.moveApi = null;
9822
9919
  /** Host-contributed legend actions, resolved PER ROW at render time (see setLegendActions). */
@@ -9838,9 +9935,18 @@ var InputsUI = class {
9838
9935
  /** Titles switched off entirely (a settings toggle): every pane's legend container
9839
9936
  * hides — rows, fold chevron and all — unlike the fold, which leaves its chip. */
9840
9937
  this.titlesVisible = true;
9938
+ /** Plot values shown beside the titles chart-wide (a settings toggle); a row's own
9939
+ * context-menu choice ({@link LegendRow.showValues}) overrides it per indicator. */
9940
+ this.valuesVisible = true;
9941
+ /** Open legend-row context menu (kept so it can be torn down). */
9942
+ this.rowMenu = null;
9841
9943
  /** The single fold toggle, on the price-pane legend: a bordered ^ chevron under the
9842
9944
  * rows, or the bordered "˅ N" chip (N counts every pane's indicators) when folded. */
9843
9945
  this.foldToggle = null;
9946
+ /** Host-provided replacement for the fold toggle (multi-chart shells route it to
9947
+ * their indicator overview, e.g. an object tree). While set, the legend stays
9948
+ * folded and the chip runs this instead of unfolding inline. */
9949
+ this.overviewAction = null;
9844
9950
  /** Esc-to-close for the settings modal (bound so focus can sit anywhere). */
9845
9951
  this.onDialogKey = (e) => {
9846
9952
  if (e.key === "Escape") {
@@ -9864,6 +9970,9 @@ var InputsUI = class {
9864
9970
  this.onMoveMenuOutside = (e) => {
9865
9971
  if (this.moveMenu && !this.moveMenu.contains(e.target)) this.closeMoveMenu();
9866
9972
  };
9973
+ this.onRowMenuOutside = (e) => {
9974
+ if (this.rowMenu && !this.rowMenu.contains(e.target)) this.closeRowMenu();
9975
+ };
9867
9976
  if (!container.style.position) container.style.position = "relative";
9868
9977
  if (typeof document !== "undefined") document.addEventListener("click", this.onDocClick);
9869
9978
  }
@@ -9871,6 +9980,36 @@ var InputsUI = class {
9871
9980
  this.dialogHost = host;
9872
9981
  if (host && getComputedStyle(host).position === "static") host.style.position = "relative";
9873
9982
  }
9983
+ /** The host shell's chrome size class — mobile makes the inputs dialog fullscreen
9984
+ * (an open dialog re-presents on the flip) and folds the legend behind its chip by
9985
+ * default (a phone-width plot has no room for the rows). */
9986
+ setLayoutMode(mode) {
9987
+ const mobile = mode === "mobile";
9988
+ if (mobile === this.mobileLayout) return;
9989
+ this.mobileLayout = mobile;
9990
+ if (this.overviewAction === null) this.legendFolded = mobile;
9991
+ this.syncFoldToggle();
9992
+ if (this.openId !== null) {
9993
+ const id = this.openId;
9994
+ this.closeOpenDialog();
9995
+ this.openDialog(id);
9996
+ }
9997
+ }
9998
+ /** Replace the fold toggle's behavior with a HOST action (or restore it with null):
9999
+ * the chip stays — objects icon + indicator count — but a press runs the action
10000
+ * (a multi-chart shell opens its indicator overview) instead of unfolding the rows,
10001
+ * which stay hidden while the override is in force. */
10002
+ setLegendOverviewAction(action) {
10003
+ if (action === this.overviewAction) return;
10004
+ this.overviewAction = action;
10005
+ this.legendFolded = action !== null ? true : this.mobileLayout;
10006
+ this.syncFoldToggle();
10007
+ }
10008
+ /** Open the settings dialog of one indicator (the legend gear's programmatic twin) —
10009
+ * no-op for an unknown id. */
10010
+ openSettingsFor(id) {
10011
+ if (this.rows.has(id)) this.openDialog(id);
10012
+ }
9874
10013
  /** Attach a themed chrome tooltip to a control, recording its disposer under `id`. */
9875
10014
  tip(store, id, anchor, text, wrap = false) {
9876
10015
  const list = store.get(id) ?? [];
@@ -9898,12 +10037,12 @@ var InputsUI = class {
9898
10037
  }
9899
10038
  setTheme(theme) {
9900
10039
  this.theme = theme;
10040
+ for (const lg of this.legends.values()) applyChromeTokens(lg, theme);
9901
10041
  for (const row of this.rows.values()) {
9902
- const open = row.controlsEl.style.display !== "none";
9903
- row.el.style.background = open ? theme.background : this.idleRowFill();
10042
+ row.el.style.background = row.highlighted ? theme.background : this.idleRowFill();
9904
10043
  row.el.style.color = theme.textColor;
9905
10044
  }
9906
- if (this.foldToggle) this.foldToggle.style.background = theme.background;
10045
+ if (this.foldToggle) this.syncFoldToggle();
9907
10046
  }
9908
10047
  /** Provide (or clear) the host symbol picker that `input.symbol` opens on activation. */
9909
10048
  setSymbolPicker(picker) {
@@ -9934,7 +10073,7 @@ var InputsUI = class {
9934
10073
  btn.innerHTML = iconAt(action.icon, LEGEND_ICON_PX);
9935
10074
  btn.className = "vela-ind-ctl";
9936
10075
  btn.dataset.legendAction = action.id;
9937
- btn.style.cssText = "cursor:pointer;display:inline-flex;align-items:center;background:transparent;border:none;line-height:0;padding:0 1px;";
10076
+ btn.style.cssText = LEGEND_CTL_CSS;
9938
10077
  btn.addEventListener("click", (e) => {
9939
10078
  e.stopPropagation();
9940
10079
  action.run();
@@ -9952,6 +10091,54 @@ var InputsUI = class {
9952
10091
  this.titlesVisible = visible;
9953
10092
  this.reposition();
9954
10093
  }
10094
+ /**
10095
+ * Show/hide the plot values chart-wide (the settings dialog's Indicators → Values
10096
+ * toggle). "All" is meant literally: per-row context-menu overrides are cleared, so
10097
+ * every legend follows the new state.
10098
+ */
10099
+ setValuesVisible(visible) {
10100
+ this.valuesVisible = visible;
10101
+ for (const row of this.rows.values()) {
10102
+ row.showValues = null;
10103
+ this.renderRowValues(row);
10104
+ }
10105
+ }
10106
+ /**
10107
+ * Push the current plot values of every indicator at once (the renderer calls this per
10108
+ * paint). Rows absent from the map (e.g. a hidden indicator) show no values. Cheap on
10109
+ * unchanged frames: each row diffs against its last rendered values before touching DOM.
10110
+ */
10111
+ setPlotValues(values) {
10112
+ for (const row of this.rows.values()) {
10113
+ const next = values.get(row.id) ?? [];
10114
+ const key = next.map((v) => `${v.value}|${v.color}`).join("\0");
10115
+ if (key === row.plotValuesKey) continue;
10116
+ row.plotValues = next;
10117
+ row.plotValuesKey = key;
10118
+ this.renderRowValues(row);
10119
+ }
10120
+ }
10121
+ /** Whether a row currently shows its values: its own override, else the chart-wide flag. */
10122
+ rowValuesShown(row) {
10123
+ return row.showValues ?? this.valuesVisible;
10124
+ }
10125
+ /** (Re)build one row's values readout from its stored plot values + visibility. A
10126
+ * highlighted (hovered/selected) row shows only the title + controls — no values. */
10127
+ renderRowValues(row) {
10128
+ if (!this.rowValuesShown(row) || row.plotValues.length === 0 || row.highlighted) {
10129
+ row.valuesEl.style.display = "none";
10130
+ row.valuesEl.replaceChildren();
10131
+ return;
10132
+ }
10133
+ row.valuesEl.style.display = "inline-flex";
10134
+ row.valuesEl.replaceChildren();
10135
+ for (const v of row.plotValues) {
10136
+ const span = document.createElement("span");
10137
+ span.textContent = v.value;
10138
+ span.style.color = v.color;
10139
+ row.valuesEl.appendChild(span);
10140
+ }
10141
+ }
9955
10142
  // ── legend move/merge (menu + drag) ─────────────────────────────────────
9956
10143
  /** Open the "Move to" menu for a row, anchored under its move button. */
9957
10144
  openMoveMenu(id, anchor) {
@@ -10003,6 +10190,48 @@ var InputsUI = class {
10003
10190
  this.moveMenu?.remove();
10004
10191
  this.moveMenu = null;
10005
10192
  }
10193
+ // ── legend row context menu (right-click) ───────────────────────────────
10194
+ /** Open the row's action menu at the pointer. One entry today: the values toggle. */
10195
+ openRowMenu(id, x, y) {
10196
+ this.closeRowMenu();
10197
+ this.closeMoveMenu();
10198
+ const row = this.rows.get(id);
10199
+ if (!row) return;
10200
+ const menu = document.createElement("div");
10201
+ menu.style.cssText = `position:fixed;z-index:var(--vela-z-tooltip);min-width:170px;padding:4px;border-radius:var(--vela-radius-md);background:var(--vela-surface-elev);color:${this.theme.textColor};border:1px solid var(--vela-border);box-shadow:var(--vela-shadow);font-size:var(--vela-font-size-md);`;
10202
+ applyChromeTokens(menu, this.theme);
10203
+ const item = document.createElement("button");
10204
+ item.type = "button";
10205
+ item.className = "vela-ind-menuitem";
10206
+ item.style.cssText = "display:flex;align-items:center;gap:8px;width:100%;text-align:left;padding:6px 10px;border:none;border-radius:var(--vela-radius-sm);color:inherit;cursor:pointer;white-space:nowrap;";
10207
+ const check = document.createElement("span");
10208
+ check.style.cssText = "display:inline-flex;align-items:center;justify-content:center;width:14px;flex:none;line-height:0;";
10209
+ check.innerHTML = this.rowValuesShown(row) ? CHECK_SVG : "";
10210
+ const label = document.createElement("span");
10211
+ label.textContent = "Indicator values";
10212
+ item.append(check, label);
10213
+ item.addEventListener("click", (e) => {
10214
+ e.stopPropagation();
10215
+ this.closeRowMenu();
10216
+ row.showValues = !this.rowValuesShown(row);
10217
+ this.renderRowValues(row);
10218
+ });
10219
+ menu.appendChild(item);
10220
+ document.body.appendChild(menu);
10221
+ const mw = menu.offsetWidth || 180;
10222
+ const mh = menu.offsetHeight || 40;
10223
+ menu.style.left = `${Math.min(x, window.innerWidth - mw - 6)}px`;
10224
+ menu.style.top = `${Math.min(y, window.innerHeight - mh - 6)}px`;
10225
+ this.rowMenu = menu;
10226
+ setTimeout(() => {
10227
+ if (typeof document !== "undefined") document.addEventListener("pointerdown", this.onRowMenuOutside, true);
10228
+ }, 0);
10229
+ }
10230
+ closeRowMenu() {
10231
+ if (typeof document !== "undefined") document.removeEventListener("pointerdown", this.onRowMenuOutside, true);
10232
+ this.rowMenu?.remove();
10233
+ this.rowMenu = null;
10234
+ }
10006
10235
  /** Move a legend row to another pane (indicator merged/moved), tidying an emptied container. */
10007
10236
  setPane(id, paneId) {
10008
10237
  const row = this.rows.get(id);
@@ -10024,7 +10253,8 @@ var InputsUI = class {
10024
10253
  if (!lg) {
10025
10254
  lg = document.createElement("div");
10026
10255
  lg.dataset.velaPane = paneId;
10027
- lg.style.cssText = "position:absolute;left:10px;z-index:5;display:flex;flex-direction:column;align-items:flex-start;gap:3px;pointer-events:none;font:12px -apple-system,Segoe UI,sans-serif;";
10256
+ lg.style.cssText = "position:absolute;left:10px;z-index:5;display:flex;flex-direction:column;align-items:flex-start;gap:0;pointer-events:none;font:12px -apple-system,Segoe UI,sans-serif;";
10257
+ applyChromeTokens(lg, this.theme);
10028
10258
  this.positionLegend(lg, paneId);
10029
10259
  this.container.appendChild(lg);
10030
10260
  this.legends.set(paneId, lg);
@@ -10060,15 +10290,16 @@ var InputsUI = class {
10060
10290
  */
10061
10291
  syncFoldToggle() {
10062
10292
  const count = this.rows.size;
10063
- if (count < 2) {
10293
+ const minRows = this.overviewAction !== null || this.mobileLayout ? 1 : 2;
10294
+ if (count < minRows) {
10064
10295
  const wasFolded = this.legendFolded;
10065
- this.legendFolded = false;
10296
+ if (count > 0) this.legendFolded = false;
10066
10297
  if (this.foldToggle) {
10067
10298
  this.foldToggle.remove();
10068
10299
  this.foldToggle = null;
10069
10300
  this.disposeTips(this.rowTips, "fold");
10070
10301
  }
10071
- if (wasFolded) this.reposition();
10302
+ if (wasFolded && count > 0) this.reposition();
10072
10303
  return;
10073
10304
  }
10074
10305
  let btn = this.foldToggle;
@@ -10076,25 +10307,30 @@ var InputsUI = class {
10076
10307
  btn = document.createElement("button");
10077
10308
  btn.type = "button";
10078
10309
  btn.className = "vela-ind-fold";
10079
- this.tip(this.rowTips, "fold", btn, () => this.legendFolded ? "Show indicator legend" : "Hide indicator legend");
10310
+ this.tip(this.rowTips, "fold", btn, () => this.overviewAction !== null ? "Indicators" : this.legendFolded ? "Show indicator legend" : "Hide indicator legend");
10080
10311
  btn.addEventListener("click", () => {
10312
+ if (this.overviewAction !== null) {
10313
+ this.overviewAction();
10314
+ return;
10315
+ }
10081
10316
  this.legendFolded = !this.legendFolded;
10082
10317
  this.syncFoldToggle();
10083
10318
  });
10084
10319
  this.foldToggle = btn;
10085
10320
  }
10086
10321
  const folded = this.legendFolded;
10087
- btn.setAttribute("aria-label", folded ? "Show indicator legend" : "Hide indicator legend");
10088
- btn.style.cssText = `pointer-events:auto;display:inline-flex;align-items:center;gap:4px;background:${this.theme.background};border:1px solid var(--vela-border);border-radius:4px;padding:2px 6px;margin-top:6px;cursor:pointer;font:inherit;line-height:0;`;
10322
+ btn.setAttribute("aria-label", this.overviewAction !== null ? "Indicators" : folded ? "Show indicator legend" : "Hide indicator legend");
10323
+ const ink = this.theme.textColor;
10324
+ btn.style.cssText = `pointer-events:auto;display:inline-flex;align-items:center;gap:4px;background:${this.theme.background};color:${ink};border:1px solid ${this.theme.borderColor};border-radius:4px;padding:2px 6px;margin-top:6px;cursor:pointer;font:inherit;line-height:0;`;
10089
10325
  btn.replaceChildren();
10090
10326
  const icon2 = document.createElement("span");
10091
- icon2.style.cssText = "display:inline-flex;align-items:center;line-height:0;";
10092
- icon2.innerHTML = folded ? UNFOLD_SVG : FOLD_SVG;
10327
+ icon2.style.cssText = "display:inline-flex;align-items:center;line-height:0;opacity:0.7;";
10328
+ icon2.innerHTML = this.overviewAction !== null ? OVERVIEW_SVG : folded ? UNFOLD_SVG : FOLD_SVG;
10093
10329
  btn.appendChild(icon2);
10094
10330
  if (folded) {
10095
10331
  const label = document.createElement("span");
10096
10332
  label.textContent = String(count);
10097
- label.style.cssText = "font-weight:600;line-height:normal;font-size:12px;color:var(--vela-fg-bright);";
10333
+ label.style.cssText = `font-weight:600;line-height:normal;font-size:12px;color:${ink};`;
10098
10334
  btn.appendChild(label);
10099
10335
  }
10100
10336
  this.legendFor("price").appendChild(btn);
@@ -10102,9 +10338,11 @@ var InputsUI = class {
10102
10338
  }
10103
10339
  /** Create or update an indicator's legend row (in the legend for its pane). */
10104
10340
  upsert(id, title, inputs, values, paneId = "price", opts = {}) {
10341
+ const settingsTitle = opts.settingsTitle ?? title;
10105
10342
  const existing = this.rows.get(id);
10106
10343
  if (existing) {
10107
10344
  existing.title = title;
10345
+ existing.settingsTitle = settingsTitle;
10108
10346
  existing.inputs = inputs;
10109
10347
  existing.values = { ...values };
10110
10348
  existing.titleEl.textContent = title;
@@ -10117,8 +10355,7 @@ var InputsUI = class {
10117
10355
  }
10118
10356
  ensureDialogStyles();
10119
10357
  const el = document.createElement("div");
10120
- 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;`;
10121
- el.addEventListener("mouseenter", () => this.setRowHighlighted(id, true));
10358
+ el.style.cssText = `pointer-events:auto;display:flex;align-items:center;background:${this.idleRowFill()};border-radius:4px;padding:${LEGEND_ROW_PAD_Y}px ${LEGEND_ROW_PAD_X}px;margin-left:-${LEGEND_ROW_PAD_X}px;min-height:${LEGEND_ROW_MIN_H}px;box-sizing:border-box;color:${this.theme.textColor};user-select:none;-webkit-user-select:none;`;
10122
10359
  el.addEventListener("mouseleave", () => {
10123
10360
  if (this.selectedId !== id) this.setRowHighlighted(id, false);
10124
10361
  });
@@ -10138,11 +10375,16 @@ var InputsUI = class {
10138
10375
  e.preventDefault();
10139
10376
  this.onRemove?.(id);
10140
10377
  });
10378
+ el.addEventListener("contextmenu", (e) => {
10379
+ e.preventDefault();
10380
+ e.stopPropagation();
10381
+ this.openRowMenu(id, e.clientX, e.clientY);
10382
+ });
10141
10383
  const statusEl = document.createElement("span");
10142
10384
  statusEl.style.cssText = "display:none;box-sizing:border-box;flex:none;";
10143
- el.appendChild(statusEl);
10144
10385
  const titleWrap = document.createElement("span");
10145
10386
  titleWrap.style.cssText = "white-space:nowrap;";
10387
+ titleWrap.addEventListener("mouseenter", () => this.setRowHighlighted(id, true));
10146
10388
  const titleEl = document.createElement("span");
10147
10389
  titleEl.textContent = title;
10148
10390
  titleEl.style.cssText = "font-weight:600;";
@@ -10154,6 +10396,11 @@ var InputsUI = class {
10154
10396
  titleWrap.appendChild(beta);
10155
10397
  }
10156
10398
  el.appendChild(titleWrap);
10399
+ const valuesEl = document.createElement("span");
10400
+ valuesEl.style.cssText = `display:none;align-items:center;gap:5px;margin-left:${LEGEND_TITLE_VALUES_GAP_PX}px;white-space:nowrap;font-variant-numeric:tabular-nums;`;
10401
+ el.appendChild(valuesEl);
10402
+ const controlsEl = document.createElement("span");
10403
+ controlsEl.style.cssText = `display:none;align-items:center;gap:${LEGEND_ACTION_GAP_PX}px;flex:none;margin-left:${LEGEND_ACTION_GAP_PX}px;`;
10157
10404
  let eyeEl = null;
10158
10405
  if (this.onToggleVisible) {
10159
10406
  const eye = document.createElement("button");
@@ -10162,16 +10409,14 @@ var InputsUI = class {
10162
10409
  this.tip(this.rowTips, id, eye, () => this.rows.get(id)?.hidden ? "Show" : "Hide");
10163
10410
  eye.innerHTML = EYE_SVG;
10164
10411
  eye.className = "vela-ind-ctl";
10165
- eye.style.cssText = "cursor:pointer;display:none;align-items:center;background:transparent;border:none;line-height:0;padding:0 1px;";
10412
+ eye.style.cssText = LEGEND_CTL_CSS;
10166
10413
  eye.addEventListener("click", () => {
10167
10414
  const row = this.rows.get(id);
10168
10415
  this.onToggleVisible?.(id, Boolean(row?.hidden));
10169
10416
  });
10170
- el.appendChild(eye);
10417
+ controlsEl.appendChild(eye);
10171
10418
  eyeEl = eye;
10172
10419
  }
10173
- const controlsEl = document.createElement("span");
10174
- controlsEl.style.cssText = "display:none;align-items:center;gap:6px;";
10175
10420
  if (inputs.length > 0) {
10176
10421
  const gear = document.createElement("button");
10177
10422
  gear.type = "button";
@@ -10179,7 +10424,7 @@ var InputsUI = class {
10179
10424
  this.tip(this.rowTips, id, gear, "Settings");
10180
10425
  gear.innerHTML = GEAR_SVG;
10181
10426
  gear.className = "vela-ind-ctl";
10182
- gear.style.cssText = "cursor:pointer;display:inline-flex;align-items:center;background:transparent;border:none;line-height:0;padding:0 1px;";
10427
+ gear.style.cssText = LEGEND_CTL_CSS;
10183
10428
  gear.addEventListener("click", () => this.openDialog(id));
10184
10429
  controlsEl.appendChild(gear);
10185
10430
  }
@@ -10190,7 +10435,7 @@ var InputsUI = class {
10190
10435
  this.tip(this.rowTips, id, mv, "Move to pane");
10191
10436
  mv.innerHTML = iconAt("move", LEGEND_ICON_PX);
10192
10437
  mv.className = "vela-ind-ctl";
10193
- mv.style.cssText = "cursor:pointer;display:inline-flex;align-items:center;background:transparent;border:none;line-height:0;padding:0 1px;";
10438
+ mv.style.cssText = LEGEND_CTL_CSS;
10194
10439
  mv.addEventListener("click", (e) => {
10195
10440
  e.stopPropagation();
10196
10441
  this.openMoveMenu(id, mv);
@@ -10207,12 +10452,13 @@ var InputsUI = class {
10207
10452
  this.tip(this.rowTips, id, close, "Remove indicator");
10208
10453
  close.innerHTML = CLOSE_SVG;
10209
10454
  close.className = "vela-ind-close";
10210
- close.style.cssText = "cursor:pointer;display:inline-flex;align-items:center;background:transparent;border:none;color:var(--vela-fg-muted);line-height:0;padding:0 1px;";
10455
+ close.style.cssText = LEGEND_CTL_CSS;
10211
10456
  close.addEventListener("click", () => this.onRemove?.(id));
10212
10457
  controlsEl.appendChild(close);
10213
10458
  el.appendChild(controlsEl);
10459
+ el.appendChild(statusEl);
10214
10460
  this.attach(this.legendFor(paneId), el, !!opts.native);
10215
- this.rows.set(id, { id, title, inputs, values: { ...values }, el, titleEl, statusEl, paneId, hidden: false, eyeEl, controlsEl, extrasEl, native: !!opts.native });
10461
+ this.rows.set(id, { id, title, settingsTitle, inputs, values: { ...values }, el, titleEl, statusEl, valuesEl, plotValues: [], plotValuesKey: "", showValues: null, highlighted: false, paneId, hidden: false, eyeEl, controlsEl, extrasEl, native: !!opts.native });
10216
10462
  this.syncFoldToggle();
10217
10463
  }
10218
10464
  /** Place a row in its pane's legend — native rows PREPEND (pinned to the top), Pine rows append. */
@@ -10226,24 +10472,31 @@ var InputsUI = class {
10226
10472
  if (row) row.values = { ...row.values, ...values };
10227
10473
  }
10228
10474
  /**
10229
- * Reflect an indicator's live status in its legend row: `'loading'` shows a spinner (a fetch is
10230
- * in flight), `'live'` a pulsing dot (live-updating), `'idle'` nothing. Rendered left of the title.
10475
+ * Reflect an indicator's live status in its legend row: `'loading'` shows three pulsing
10476
+ * dots (a fetch is in flight the same load affordance as the chart's own bar-load dots,
10477
+ * at legend scale), `'live'` a pulsing dot, `'idle'` nothing. Rendered at the row's right end.
10231
10478
  */
10232
10479
  setStatus(id, status) {
10233
10480
  const row = this.rows.get(id);
10234
10481
  if (!row) return;
10235
10482
  const el = row.statusEl;
10483
+ el.replaceChildren();
10236
10484
  if (status === "idle") {
10237
- el.style.display = "none";
10238
- el.style.animation = "none";
10485
+ el.style.cssText = "display:none;box-sizing:border-box;flex:none;";
10239
10486
  return;
10240
10487
  }
10241
- this.ensureStatusKeyframes();
10242
10488
  if (status === "loading") {
10243
- 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;`;
10244
- } else {
10245
- el.style.cssText = `display:inline-block;box-sizing:border-box;flex:none;width:8px;height:8px;border-radius:50%;background:${this.theme.upColor};animation:vela-ind-pulse 1.2s ease-in-out infinite;`;
10489
+ el.style.cssText = "display:inline-flex;align-items:center;gap:3px;box-sizing:border-box;flex:none;";
10490
+ for (let i = 0; i < 3; i += 1) {
10491
+ const dot = document.createElement("span");
10492
+ dot.style.cssText = "width:4px;height:4px;border-radius:50%;background:currentColor;opacity:0.15;flex:none;";
10493
+ dot.animate?.([{ opacity: 0.12 }, { opacity: 0.55 }], { duration: 800, iterations: Infinity, direction: "alternate", easing: "ease-in-out", delay: -i * 260 });
10494
+ el.appendChild(dot);
10495
+ }
10496
+ return;
10246
10497
  }
10498
+ this.ensureStatusKeyframes();
10499
+ el.style.cssText = `display:inline-block;box-sizing:border-box;flex:none;width:8px;height:8px;border-radius:50%;background:${this.theme.upColor};animation:vela-ind-pulse 1.2s ease-in-out infinite;`;
10247
10500
  }
10248
10501
  /** Inject the status keyframes once (idempotent). */
10249
10502
  ensureStatusKeyframes() {
@@ -10262,8 +10515,8 @@ var InputsUI = class {
10262
10515
  if (row.eyeEl) {
10263
10516
  row.eyeEl.innerHTML = visible ? EYE_SVG : EYE_OFF_SVG;
10264
10517
  row.eyeEl.setAttribute("aria-label", visible ? "Hide" : "Show");
10265
- row.eyeEl.style.display = !visible || row.controlsEl.style.display !== "none" ? "inline-flex" : "none";
10266
10518
  }
10519
+ this.syncRowActions(row);
10267
10520
  }
10268
10521
  /**
10269
10522
  * Select a legend row (outlining it) and clear any previous selection, so at most one
@@ -10276,17 +10529,36 @@ var InputsUI = class {
10276
10529
  this.setRowHighlighted(id, true);
10277
10530
  }
10278
10531
  /**
10279
- * Show or hide a row's outline and eye/gear/✕ controls together — driven by both hover and
10280
- * selection, so an idle (unhovered, unselected) row shows just its title. Exception: a hidden
10281
- * indicator keeps its eye visible even when idle, so it can be un-hidden without hovering.
10532
+ * Show or hide a row's outline and eye/gear/✕ controls together — driven by title
10533
+ * hover and selection (plot values do not open the chip). An idle unselected row
10534
+ * shows just its title (+ values). Exception: a hidden indicator keeps its eye
10535
+ * visible even when idle, so it can be un-hidden without hovering.
10282
10536
  */
10283
10537
  setRowHighlighted(id, highlighted) {
10284
10538
  const row = this.rows.get(id);
10285
10539
  if (!row) return;
10540
+ row.highlighted = highlighted;
10541
+ this.renderRowValues(row);
10286
10542
  row.el.style.boxShadow = highlighted ? `inset 0 0 0 1px ${this.neutralBorder()}` : "none";
10287
10543
  row.el.style.background = highlighted ? this.theme.background : this.idleRowFill();
10288
- row.controlsEl.style.display = highlighted ? "inline-flex" : "none";
10289
- if (row.eyeEl) row.eyeEl.style.display = highlighted || row.hidden ? "inline-flex" : "none";
10544
+ this.syncRowActions(row);
10545
+ }
10546
+ /**
10547
+ * Reveal the action cluster (or just the eye for a hidden indicator). Non-eye
10548
+ * buttons only appear while highlighted; the eye keeps its inline-flex from
10549
+ * construction and is shown/hidden with the cluster container.
10550
+ */
10551
+ syncRowActions(row) {
10552
+ const open = row.highlighted;
10553
+ row.controlsEl.style.display = open || row.hidden ? "inline-flex" : "none";
10554
+ for (const child of Array.from(row.controlsEl.children)) {
10555
+ if (!(child instanceof HTMLElement) || child === row.eyeEl) continue;
10556
+ if (child === row.extrasEl) {
10557
+ child.style.display = open ? "contents" : "none";
10558
+ continue;
10559
+ }
10560
+ child.style.display = open ? "inline-flex" : "none";
10561
+ }
10290
10562
  }
10291
10563
  /** Drop the current selection outline, if any. */
10292
10564
  clearSelection() {
@@ -10314,6 +10586,7 @@ var InputsUI = class {
10314
10586
  destroy() {
10315
10587
  this.closeDialog();
10316
10588
  this.closeMoveMenu();
10589
+ this.closeRowMenu();
10317
10590
  for (const id of [...this.rowTips.keys()]) this.disposeTips(this.rowTips, id);
10318
10591
  for (const id of [...this.extrasTips.keys()]) this.disposeTips(this.extrasTips, id);
10319
10592
  for (const lg of this.legends.values()) lg.remove();
@@ -10369,6 +10642,7 @@ var InputsUI = class {
10369
10642
  const card = document.createElement("div");
10370
10643
  card.className = "vela-ind-dialog";
10371
10644
  card.style.cssText = `display:flex;flex-direction:column;width:fit-content;min-width:min(360px,90%);max-width:min(640px,94%);max-height:82%;background:${t.background};border:1px solid ${border};border-radius:var(--vela-radius-lg);box-shadow:var(--vela-shadow-dialog);color:${fg};color-scheme:${scheme};font:13px ${font};overflow:hidden;`;
10645
+ if (this.mobileLayout) card.style.cssText += "width:100%;min-width:0;max-width:none;height:100%;max-height:none;border:none;border-radius:0;";
10372
10646
  applyChromeTokens(card, t);
10373
10647
  card.addEventListener("keydown", (e) => {
10374
10648
  if (e.key !== "Escape") e.stopPropagation();
@@ -10376,7 +10650,7 @@ var InputsUI = class {
10376
10650
  const header = document.createElement("div");
10377
10651
  header.style.cssText = "display:flex;justify-content:space-between;align-items:flex-start;gap:12px;padding:16px 20px 12px;flex:0 0 auto;";
10378
10652
  const hTitle = document.createElement("span");
10379
- hTitle.textContent = row.title;
10653
+ hTitle.textContent = row.settingsTitle;
10380
10654
  hTitle.style.cssText = "font-weight:600;font-size:16px;line-height:1.3;";
10381
10655
  const closeBtn = document.createElement("button");
10382
10656
  closeBtn.type = "button";
@@ -10388,7 +10662,7 @@ var InputsUI = class {
10388
10662
  closeBtn.addEventListener("click", () => this.closeDialog());
10389
10663
  header.append(hTitle, closeBtn);
10390
10664
  card.appendChild(header);
10391
- makeDialogDraggable(card, header, { closeSelector: "button" });
10665
+ if (!this.mobileLayout) makeDialogDraggable(card, header, { closeSelector: "button" });
10392
10666
  const tabs = document.createElement("div");
10393
10667
  tabs.style.cssText = `display:flex;gap:16px;padding:0 20px;border-bottom:1px solid ${border};flex:0 0 auto;`;
10394
10668
  const tab = document.createElement("div");
@@ -10408,7 +10682,7 @@ var InputsUI = class {
10408
10682
  section.appendChild(gh);
10409
10683
  }
10410
10684
  const grid = document.createElement("div");
10411
- grid.style.cssText = "display:grid;grid-template-columns:minmax(140px,auto) auto auto 1fr;align-items:center;column-gap:12px;row-gap:22px;";
10685
+ grid.style.cssText = this.mobileLayout ? "display:grid;grid-template-columns:minmax(0,1fr) auto auto max-content;align-items:center;column-gap:12px;row-gap:22px;" : "display:grid;grid-template-columns:minmax(140px,auto) auto auto 1fr;align-items:center;column-gap:12px;row-gap:22px;";
10412
10686
  for (const inputRow of group.rows) this.buildInputRow(grid, inputRow, row);
10413
10687
  section.appendChild(grid);
10414
10688
  body.appendChild(section);
@@ -10857,10 +11131,14 @@ function groupInputs(inputs) {
10857
11131
  var CHECK_SVG = iconAt("check", 12);
10858
11132
  var SELECT_CHEVRON_SVG = iconAt("chevron-down", 16);
10859
11133
  var DIALOG_STYLE_ID = "vela-ind-dialog-styles";
11134
+ var DIALOG_STYLE_REV = "10";
10860
11135
  function ensureDialogStyles() {
10861
- if (typeof document === "undefined" || document.getElementById(DIALOG_STYLE_ID)) return;
10862
- const s = document.createElement("style");
11136
+ if (typeof document === "undefined") return;
11137
+ const existing = document.getElementById(DIALOG_STYLE_ID);
11138
+ if (existing?.dataset.rev === DIALOG_STYLE_REV) return;
11139
+ const s = existing ?? document.createElement("style");
10863
11140
  s.id = DIALOG_STYLE_ID;
11141
+ s.dataset.rev = DIALOG_STYLE_REV;
10864
11142
  s.textContent = `
10865
11143
  .vela-ind-dialog input[type=color]{-webkit-appearance:none;appearance:none;}
10866
11144
  .vela-ind-dialog input[type=color]::-webkit-color-swatch-wrapper{padding:0;}
@@ -10874,19 +11152,19 @@ function ensureDialogStyles() {
10874
11152
  .vela-ind-dialog ::-webkit-scrollbar-track{background:transparent;}
10875
11153
  .vela-ind-hint{background:var(--vela-hover);color:var(--vela-fg-muted);transition:background var(--vela-dur-fast) ease,color var(--vela-dur-fast) ease;}
10876
11154
  .vela-ind-hint:hover{background:var(--vela-active);color:var(--vela-fg-bright);}
10877
- .vela-ind-ctl{color:var(--vela-fg-muted);transition:color var(--vela-dur-fast) ease;}
10878
- .vela-ind-ctl:hover{color:var(--vela-fg-bright);}
10879
- .vela-ind-close{transition:color var(--vela-dur-fast) ease;}
10880
- .vela-ind-close:hover{color:var(--vela-danger) !important;}
10881
- .vela-ind-fold{color:var(--vela-fg-muted);transition:color var(--vela-dur-fast) ease,border-color var(--vela-dur-fast) ease;}
10882
- .vela-ind-fold:hover{color:var(--vela-fg-bright);border-color:var(--vela-border-strong);}
11155
+ .vela-ind-ctl,.vela-ind-close{background-color:transparent;color:inherit;transition:color var(--vela-dur-fast) ease,background-color var(--vela-dur-fast) ease;}
11156
+ .vela-ind-ctl svg,.vela-ind-close svg{width:${LEGEND_ICON_PX}px;height:${LEGEND_ICON_PX}px;display:block;flex:none;stroke-width:1;}
11157
+ .vela-ind-ctl:hover{background-color:var(--vela-hover-strong);}
11158
+ .vela-ind-close:hover{color:var(--vela-danger) !important;background-color:var(--vela-hover-strong);}
11159
+ .vela-ind-fold{transition:border-color var(--vela-dur-fast) ease,opacity var(--vela-dur-fast) ease;}
11160
+ .vela-ind-fold:hover{border-color:var(--vela-border-strong);opacity:0.9;}
10883
11161
  .vela-ind-menuitem{background:transparent;transition:background var(--vela-dur-fast) ease;}
10884
11162
  .vela-ind-menuitem:hover{background:var(--vela-hover-strong);}
10885
11163
  .vela-ind-btn{cursor:pointer;padding:7px 14px;border-radius:var(--vela-radius-md);border:1px solid transparent;background:transparent;color:var(--vela-fg-muted);font-weight:600;font-size:13px;font-family:inherit;transition:background var(--vela-dur-fast) ease,color var(--vela-dur-fast) ease,opacity var(--vela-dur-fast) ease;}
10886
11164
  .vela-ind-btn:hover{background:var(--vela-hover);color:var(--vela-fg-bright);}
10887
11165
  .vela-ind-btn-primary{padding:7px 16px;border-color:var(--vela-selected-bg);background:var(--vela-selected-bg);color:var(--vela-selected-fg);}
10888
11166
  .vela-ind-btn-primary:hover{background:var(--vela-selected-bg);color:var(--vela-selected-fg);opacity:0.85;}`;
10889
- document.head.appendChild(s);
11167
+ if (!existing) document.head.appendChild(s);
10890
11168
  }
10891
11169
  var TIMEFRAME_OPTIONS = [
10892
11170
  { value: "", label: "Chart" },
@@ -11579,14 +11857,7 @@ function factoryResetConfig(factory) {
11579
11857
  const defaults2 = {};
11580
11858
  const addRows = (rows) => {
11581
11859
  for (const r of rows ?? []) {
11582
- if (r.kind === "heading" || r.kind === "header") continue;
11583
- if (r.kind === "range") {
11584
- defaults2[r.minKey] = r.defval;
11585
- defaults2[r.maxKey] = r.defval;
11586
- continue;
11587
- }
11588
- defaults2[r.key] = r.defval;
11589
- if (r.kind === "toggle") for (const c of r.colors ?? []) defaults2[c.key] = c.defval;
11860
+ for (const k of settingsRowValueKeys(r)) defaults2[k.key] = k.defval;
11590
11861
  }
11591
11862
  };
11592
11863
  for (const inst of section.instances ?? []) {
@@ -13730,8 +14001,13 @@ function easeToward(current, target, dtMs, tauMs) {
13730
14001
  var FLING_MIN_SPEED = 0.04;
13731
14002
  var FLING_STALE_MS = 60;
13732
14003
  var DRAG_SLOP = 2;
14004
+ var LONG_PRESS_MS = 350;
14005
+ var TOUCH_SLOP = 8;
14006
+ var TOUCH_TAP_SLOP = 14;
14007
+ var DOUBLE_TAP_MS = 350;
14008
+ var DOUBLE_TAP_SLOP = 30;
13733
14009
  var TIME_SCALE_K = 4e-3;
13734
- var WHEEL_ZOOM_K = 25e-4;
14010
+ var WHEEL_ZOOM_K = 4e-3;
13735
14011
  function wheelZoomAnchor(coords, cursorX, rightEdge) {
13736
14012
  if (rightEdge) return { logical: coords.rightEdgeLogical, x: coords.width };
13737
14013
  return { logical: coords.xToLogical(cursorX), x: cursorX };
@@ -13750,6 +14026,12 @@ function wheelPanDelta(deltaX, deltaY, shift) {
13750
14026
  function effectiveSnapMode(momentaryOverride, sticky) {
13751
14027
  return momentaryOverride ? "strong" : sticky;
13752
14028
  }
14029
+ function pinchBarSpacing(startBarSpacing, startDist, dist) {
14030
+ return clampBarSpacing(startBarSpacing * (dist / Math.max(1, startDist)));
14031
+ }
14032
+ function pinchPinnedRightOffset(anchorLogical, barCount, width, x, pxPerBar) {
14033
+ return anchorLogical - (barCount - 1) + (width - x) / pxPerBar;
14034
+ }
13753
14035
  var InputController = class {
13754
14036
  constructor(deps) {
13755
14037
  this.deps = deps;
@@ -13779,13 +14061,40 @@ var InputController = class {
13779
14061
  // smoothed pointer velocity, px/ms
13780
14062
  this.middleDeleted = false;
13781
14063
  // the last middle press deleted a drawing (suppress autoscroll/paste)
14064
+ this.rightCancelled = false;
14065
+ // the last right press cancelled a placement (suppress the context menu)
13782
14066
  // Last cursor position over the element (NaN once it leaves) — lets a modifier
13783
14067
  // press/release re-shape the drawings preview with the pointer stationary.
13784
14068
  this.cursorX = NaN;
13785
14069
  this.cursorY = NaN;
14070
+ // ── touch state ──
14071
+ /** Live touch contacts (local coords) — two entries drive a pinch; extras are ignored. */
14072
+ this.touches = /* @__PURE__ */ new Map();
14073
+ this.lpTimer = null;
14074
+ this.pinchStartDist = 1;
14075
+ this.pinchStartBarSpacing = 0;
14076
+ this.pinchAnchorLogical = 0;
14077
+ // Last clean touch tap (for double-tap pairing). Time 0 = no pending first tap.
14078
+ this.lastTapT = 0;
14079
+ this.lastTapX = 0;
14080
+ this.lastTapY = 0;
14081
+ this.onTouchContextMenu = (e) => {
14082
+ if (this.touches.size === 0) return;
14083
+ if (!(e.target instanceof Node) || !this.el?.contains(e.target)) return;
14084
+ e.preventDefault();
14085
+ e.stopImmediatePropagation();
14086
+ };
13786
14087
  this.onMiddleGuard = (e) => {
13787
14088
  if (e.button === 1 && this.middleDeleted) e.preventDefault();
13788
14089
  };
14090
+ /** A right press that cancelled a placement must not ALSO open the host's chart
14091
+ * context menu — swallow its companion event before it bubbles to the host. */
14092
+ this.onContextGuard = (e) => {
14093
+ if (!this.rightCancelled) return;
14094
+ this.rightCancelled = false;
14095
+ e.preventDefault();
14096
+ e.stopPropagation();
14097
+ };
13789
14098
  /** A modifier press/release with the pointer stationary: re-issue the last cursor
13790
14099
  * position so the placing ghost / drag preview reflects the new state immediately
13791
14100
  * (Shift's 45° angle lock, Ctrl/Cmd's momentary strong magnet). */
@@ -13802,8 +14111,22 @@ var InputController = class {
13802
14111
  if (this.middleDeleted) e.preventDefault();
13803
14112
  return;
13804
14113
  }
14114
+ if (e.button === 2) {
14115
+ this.rightCancelled = this.deps.drawingsCancelPlacement?.() ?? false;
14116
+ if (this.rightCancelled) e.preventDefault();
14117
+ return;
14118
+ }
13805
14119
  if (e.button !== 0) return;
13806
14120
  const { x, y } = this.local(e);
14121
+ if (e.pointerType === "touch") {
14122
+ if (this.touches.size === 1 && this.dragging && (this.region === "data" || this.region === "crosshair")) {
14123
+ this.touches.set(e.pointerId, { x, y });
14124
+ this.beginPinch(e);
14125
+ return;
14126
+ }
14127
+ if (this.touches.size >= 1) return;
14128
+ this.touches.set(e.pointerId, { x, y });
14129
+ }
13807
14130
  this.deps.drawingsClearTransient?.();
13808
14131
  this.dragging = true;
13809
14132
  this.moved = false;
@@ -13829,6 +14152,9 @@ var InputController = class {
13829
14152
  if (this.region === "price") this.deps.beginPriceScale(x, y);
13830
14153
  else if (this.region === "separator") this.deps.beginPaneResize(y);
13831
14154
  else if (this.region === "data") this.verticalPan = this.deps.beginPricePan(y);
14155
+ if (e.pointerType === "touch" && (this.region === "data" || this.region === "price" || this.region === "time")) {
14156
+ this.startLongPress(x, y, this.region);
14157
+ }
13832
14158
  this.lastX = x;
13833
14159
  this.lastT = e.timeStamp;
13834
14160
  this.vx = 0;
@@ -13838,28 +14164,42 @@ var InputController = class {
13838
14164
  const { x, y } = this.local(e);
13839
14165
  this.cursorX = x;
13840
14166
  this.cursorY = y;
14167
+ if (e.pointerType === "touch" && this.touches.has(e.pointerId)) this.touches.set(e.pointerId, { x, y });
14168
+ if (this.dragging && this.region === "pinch") {
14169
+ this.applyPinch();
14170
+ return;
14171
+ }
14172
+ if (this.dragging && this.region === "crosshair") {
14173
+ this.deps.onPointerMove(x, y);
14174
+ return;
14175
+ }
14176
+ if (this.dragging && this.region === "axis-sheet") {
14177
+ return;
14178
+ }
14179
+ if (this.lpTimer !== null && Math.hypot(x - this.startX, y - this.startY) > TOUCH_SLOP) this.cancelLongPress();
14180
+ const slop = e.pointerType === "touch" ? TOUCH_SLOP : DRAG_SLOP;
13841
14181
  if (this.dragging) {
13842
14182
  if (this.region === "price") {
13843
- if (Math.abs(y - this.startY) > DRAG_SLOP) this.moved = true;
14183
+ if (Math.abs(y - this.startY) > slop) this.moved = true;
13844
14184
  this.deps.priceScaleBy(y - this.startY);
13845
14185
  } else if (this.region === "separator") {
13846
- if (Math.abs(y - this.startY) > DRAG_SLOP) this.moved = true;
14186
+ if (Math.abs(y - this.startY) > slop) this.moved = true;
13847
14187
  this.deps.paneResizeBy(y - this.startY);
13848
14188
  } else if (this.region === "time") {
13849
- if (Math.abs(x - this.startX) > DRAG_SLOP) this.moved = true;
14189
+ if (Math.abs(x - this.startX) > slop) this.moved = true;
13850
14190
  const barSpacing = clampBarSpacing(this.startBarSpacing * Math.exp((this.startX - x) * TIME_SCALE_K));
13851
14191
  this.deps.apply({ barSpacing, rightOffset: this.startRightOffset });
13852
14192
  } else if (this.region === "drawing") {
13853
- if (Math.abs(x - this.startX) > DRAG_SLOP || Math.abs(y - this.startY) > DRAG_SLOP) this.moved = true;
14193
+ if (Math.abs(x - this.startX) > slop || Math.abs(y - this.startY) > slop) this.moved = true;
13854
14194
  this.deps.drawingsPointerMove?.(x, y, this.snapMode(e), e.shiftKey);
13855
14195
  } else {
13856
14196
  const coords = this.deps.getCoords();
13857
14197
  const vp = coords.getViewport();
13858
14198
  const dx = x - this.startX;
13859
- if (Math.abs(dx) > DRAG_SLOP) this.moved = true;
14199
+ if (Math.abs(dx) > slop) this.moved = true;
13860
14200
  this.deps.apply({ barSpacing: vp.barSpacing, rightOffset: this.startRightOffset - dx / coords.pxPerBar() });
13861
14201
  if (this.verticalPan) {
13862
- if (Math.abs(y - this.startY) > DRAG_SLOP) this.moved = true;
14202
+ if (Math.abs(y - this.startY) > slop) this.moved = true;
13863
14203
  this.deps.pricePanBy(y - this.startY);
13864
14204
  }
13865
14205
  const dt = e.timeStamp - this.lastT;
@@ -13876,14 +14216,44 @@ var InputController = class {
13876
14216
  this.el.style.cursor = drawCursor ?? (r === "price" ? "ns-resize" : r === "time" ? "ew-resize" : r === "separator" ? "row-resize" : "");
13877
14217
  this.deps.drawingsPointerMove?.(x, y, this.snapMode(e), e.shiftKey);
13878
14218
  }
13879
- this.deps.onPointerMove(x, y);
14219
+ if (e.pointerType !== "touch") this.deps.onPointerMove(x, y);
13880
14220
  };
13881
14221
  this.onUp = (e) => {
14222
+ if (e.pointerType === "touch") this.touches.delete(e.pointerId);
14223
+ this.cancelLongPress();
14224
+ if (this.dragging && this.region === "pinch") {
14225
+ if (this.touches.size === 1) {
14226
+ const rest = [...this.touches.values()][0];
14227
+ const vp = this.deps.getCoords().getViewport();
14228
+ this.region = "data";
14229
+ this.verticalPan = false;
14230
+ this.startX = rest.x;
14231
+ this.startY = rest.y;
14232
+ this.startRightOffset = vp.rightOffset;
14233
+ this.startBarSpacing = vp.barSpacing;
14234
+ this.lastX = rest.x;
14235
+ this.lastT = e.timeStamp;
14236
+ this.vx = 0;
14237
+ return;
14238
+ }
14239
+ this.endGesture(e);
14240
+ return;
14241
+ }
14242
+ if (this.dragging && this.region === "crosshair") {
14243
+ this.deps.onPointerMove(null, null);
14244
+ this.endGesture(e);
14245
+ return;
14246
+ }
14247
+ if (this.dragging && this.region === "axis-sheet") {
14248
+ this.endGesture(e);
14249
+ return;
14250
+ }
14251
+ const { x, y } = this.local(e);
14252
+ const wasTouch = e.pointerType === "touch";
14253
+ const tapRelease = this.dragging && !this.moved && (!wasTouch || Math.hypot(x - this.startX, y - this.startY) <= TOUCH_TAP_SLOP);
13882
14254
  if (this.dragging && this.region === "drawing") {
13883
- const { x, y } = this.local(e);
13884
14255
  this.deps.drawingsPointerUp?.(x, y);
13885
- } else if (this.dragging && !this.moved && this.region === "data") {
13886
- const { x, y } = this.local(e);
14256
+ } else if (tapRelease && this.region === "data") {
13887
14257
  this.deps.onClick(x, y);
13888
14258
  } else if (this.dragging && this.region === "data") {
13889
14259
  const stale = e.timeStamp - this.lastT > FLING_STALE_MS;
@@ -13892,11 +14262,20 @@ var InputController = class {
13892
14262
  this.deps.fling(-this.vx / pitch);
13893
14263
  }
13894
14264
  }
13895
- this.dragging = false;
13896
- try {
13897
- this.el?.releasePointerCapture(e.pointerId);
13898
- } catch {
13899
- }
14265
+ const wasTap = wasTouch && tapRelease;
14266
+ this.endGesture(e);
14267
+ if (wasTouch) this.deps.onPointerMove(null, null);
14268
+ if (wasTap) this.registerTap(e.timeStamp, x, y);
14269
+ };
14270
+ /** The browser took the pointer back (system gesture, palm rejection, tab switch):
14271
+ * drop the gesture where it is — no click, no fling, no stuck `dragging`. */
14272
+ this.onCancel = (e) => {
14273
+ if (e.pointerType === "touch") this.touches.delete(e.pointerId);
14274
+ this.cancelLongPress();
14275
+ if (!this.dragging) return;
14276
+ if (this.region === "drawing" && !Number.isNaN(this.cursorX)) this.deps.drawingsPointerUp?.(this.cursorX, this.cursorY);
14277
+ if (this.region === "crosshair" || e.pointerType === "touch") this.deps.onPointerMove(null, null);
14278
+ this.endGesture(e);
13900
14279
  };
13901
14280
  this.onLeave = () => {
13902
14281
  this.cursorX = NaN;
@@ -13905,12 +14284,7 @@ var InputController = class {
13905
14284
  };
13906
14285
  this.onDblClick = (e) => {
13907
14286
  const { x, y } = this.local(e);
13908
- if (this.deps.drawingsDblClick?.(x, y)) return;
13909
- const region = this.regionAt(x, y);
13910
- if (region === "price") this.deps.resetPriceScale(x, y);
13911
- else if (region === "separator") this.deps.resetPaneSize(y);
13912
- else if (region === "time") this.deps.resetView();
13913
- else this.deps.dataDblClick(x, y);
14287
+ this.doubleActivate(x, y);
13914
14288
  };
13915
14289
  this.onWheel = (e) => {
13916
14290
  e.preventDefault();
@@ -13931,17 +14305,26 @@ var InputController = class {
13931
14305
  }
13932
14306
  attach(el) {
13933
14307
  this.el = el;
14308
+ if (el.style !== void 0) {
14309
+ el.style.touchAction = "none";
14310
+ el.style.userSelect = "none";
14311
+ el.style.setProperty("-webkit-user-select", "none");
14312
+ el.style.setProperty("-webkit-touch-callout", "none");
14313
+ }
13934
14314
  el.addEventListener("pointerdown", this.onDown);
13935
14315
  el.addEventListener("pointermove", this.onMove);
13936
14316
  el.addEventListener("pointerup", this.onUp);
13937
14317
  el.addEventListener("pointerleave", this.onLeave);
14318
+ el.addEventListener("pointercancel", this.onCancel);
13938
14319
  el.addEventListener("dblclick", this.onDblClick);
13939
14320
  el.addEventListener("wheel", this.onWheel, { passive: false });
13940
14321
  el.addEventListener("mousedown", this.onMiddleGuard);
13941
14322
  el.addEventListener("auxclick", this.onMiddleGuard);
14323
+ el.addEventListener("contextmenu", this.onContextGuard);
13942
14324
  const win = el.ownerDocument?.defaultView;
13943
14325
  win?.addEventListener("keydown", this.onModifier);
13944
14326
  win?.addEventListener("keyup", this.onModifier);
14327
+ win?.addEventListener("contextmenu", this.onTouchContextMenu, true);
13945
14328
  }
13946
14329
  detach() {
13947
14330
  const el = this.el;
@@ -13949,14 +14332,19 @@ var InputController = class {
13949
14332
  const win = el.ownerDocument?.defaultView;
13950
14333
  win?.removeEventListener("keydown", this.onModifier);
13951
14334
  win?.removeEventListener("keyup", this.onModifier);
14335
+ win?.removeEventListener("contextmenu", this.onTouchContextMenu, true);
13952
14336
  el.removeEventListener("pointerdown", this.onDown);
13953
14337
  el.removeEventListener("pointermove", this.onMove);
13954
14338
  el.removeEventListener("pointerup", this.onUp);
13955
14339
  el.removeEventListener("pointerleave", this.onLeave);
14340
+ el.removeEventListener("pointercancel", this.onCancel);
13956
14341
  el.removeEventListener("dblclick", this.onDblClick);
13957
14342
  el.removeEventListener("wheel", this.onWheel);
13958
14343
  el.removeEventListener("mousedown", this.onMiddleGuard);
13959
14344
  el.removeEventListener("auxclick", this.onMiddleGuard);
14345
+ el.removeEventListener("contextmenu", this.onContextGuard);
14346
+ this.cancelLongPress();
14347
+ this.touches.clear();
13960
14348
  this.el = null;
13961
14349
  }
13962
14350
  local(e) {
@@ -13980,6 +14368,59 @@ var InputController = class {
13980
14368
  snapMode(e) {
13981
14369
  return effectiveSnapMode(e.ctrlKey || e.metaKey, this.deps.drawingsSnapMode?.() ?? "off");
13982
14370
  }
14371
+ /** Arm the long-press timer for a data-area → crosshair or axis → sheet hold;
14372
+ * movement past TOUCH_SLOP or a second finger cancels it. */
14373
+ startLongPress(x, y, region) {
14374
+ this.cancelLongPress();
14375
+ this.lpTimer = setTimeout(() => {
14376
+ this.lpTimer = null;
14377
+ if (!this.dragging || this.region !== region) return;
14378
+ if (region === "data") {
14379
+ this.region = "crosshair";
14380
+ this.deps.onPointerMove(x, y);
14381
+ return;
14382
+ }
14383
+ this.region = "axis-sheet";
14384
+ this.moved = true;
14385
+ this.deps.onAxisLongPress?.(region, x, y);
14386
+ }, LONG_PRESS_MS);
14387
+ }
14388
+ cancelLongPress() {
14389
+ if (this.lpTimer !== null) clearTimeout(this.lpTimer);
14390
+ this.lpTimer = null;
14391
+ }
14392
+ /** Two fingers down: freeze the view and re-base the gesture as an anchored pinch. */
14393
+ beginPinch(e) {
14394
+ this.cancelLongPress();
14395
+ if (this.region === "crosshair") this.deps.onPointerMove(null, null);
14396
+ const coords = this.deps.getCoords();
14397
+ const vp = coords.getViewport();
14398
+ this.deps.apply(vp);
14399
+ const [a, b] = [...this.touches.values()];
14400
+ this.pinchStartDist = Math.max(1, Math.hypot(a.x - b.x, a.y - b.y));
14401
+ this.pinchStartBarSpacing = vp.barSpacing;
14402
+ this.pinchAnchorLogical = coords.xToLogical((a.x + b.x) / 2);
14403
+ this.region = "pinch";
14404
+ this.dragging = true;
14405
+ this.moved = true;
14406
+ this.vx = 0;
14407
+ this.capture(e.pointerId);
14408
+ }
14409
+ /** Track both fingers: distance ratio zooms, the live midpoint pans (anchor pinned under it). */
14410
+ applyPinch() {
14411
+ const coords = this.deps.getCoords();
14412
+ const pts = [...this.touches.values()];
14413
+ if (pts.length < 2) return;
14414
+ const [a, b] = pts;
14415
+ const barSpacing = pinchBarSpacing(this.pinchStartBarSpacing, this.pinchStartDist, Math.hypot(a.x - b.x, a.y - b.y));
14416
+ const vp = coords.getViewport();
14417
+ const pitchScale = vp.barSpacing > 0 ? coords.pxPerBar() / vp.barSpacing : 1;
14418
+ const midX = (a.x + b.x) / 2;
14419
+ this.deps.apply({
14420
+ barSpacing,
14421
+ rightOffset: pinchPinnedRightOffset(this.pinchAnchorLogical, coords.barCount, coords.width, midX, barSpacing * pitchScale)
14422
+ });
14423
+ }
13983
14424
  /** Capture defensively: a synthetic/already-released pointer can't be captured, and
13984
14425
  * the move/up pair still routes through the element listeners without it. */
13985
14426
  capture(pointerId) {
@@ -13988,6 +14429,35 @@ var InputController = class {
13988
14429
  } catch {
13989
14430
  }
13990
14431
  }
14432
+ /** Pair clean taps into a double-tap — the touch equivalent of dblclick (same
14433
+ * routing: axis taps reset that axis' view, a plot tap toggles pane maximize). */
14434
+ registerTap(t, x, y) {
14435
+ const paired = this.lastTapT > 0 && t - this.lastTapT <= DOUBLE_TAP_MS && Math.hypot(x - this.lastTapX, y - this.lastTapY) <= DOUBLE_TAP_SLOP;
14436
+ if (paired) {
14437
+ this.lastTapT = 0;
14438
+ this.doubleActivate(this.lastTapX, this.lastTapY);
14439
+ return;
14440
+ }
14441
+ this.lastTapT = t;
14442
+ this.lastTapX = x;
14443
+ this.lastTapY = y;
14444
+ }
14445
+ endGesture(e) {
14446
+ this.dragging = false;
14447
+ try {
14448
+ this.el?.releasePointerCapture(e.pointerId);
14449
+ } catch {
14450
+ }
14451
+ }
14452
+ /** Shared double-click / double-tap routing, by the region under the point. */
14453
+ doubleActivate(x, y) {
14454
+ if (this.deps.drawingsDblClick?.(x, y)) return;
14455
+ const region = this.regionAt(x, y);
14456
+ if (region === "price") this.deps.resetPriceScale(x, y);
14457
+ else if (region === "separator") this.deps.resetPaneSize(y);
14458
+ else if (region === "time") this.deps.resetView();
14459
+ else this.deps.dataDblClick(x, y);
14460
+ }
13991
14461
  };
13992
14462
 
13993
14463
  // src/renderers/native/core/KeyboardController.ts
@@ -15545,8 +16015,11 @@ var ChromeRenderer = class {
15545
16015
  }
15546
16016
  ctx.font = `${scene.style.fontSize}px ${theme.fontFamily}`;
15547
16017
  ctx.textBaseline = "middle";
15548
- if (coords.barCount === 0) return;
15549
16018
  const panes = scene.orderedPanes();
16019
+ if (coords.barCount === 0) {
16020
+ this.drawPaneSeparators(ctx, scene, theme, fullW, panes);
16021
+ return;
16022
+ }
15550
16023
  const pricePane = panes.find((p) => p.kind === "price") ?? null;
15551
16024
  for (const pane of panes) {
15552
16025
  if (pane.collapsed) continue;
@@ -15801,11 +16274,19 @@ var ChromeRenderer = class {
15801
16274
  const y = dataH + (fullH - dataH) / 2;
15802
16275
  const tr = coords.visibleTimeRange();
15803
16276
  const offset = tzOffsetMs((tr.from + tr.to) / 2, scene.timezone);
15804
- for (const tick of timeTicks(tr.from, tr.to, 8, offset)) {
15805
- const x = coords.timeToX(tick.time);
15806
- if (x < 20 || x > dataW - 20) continue;
15807
- ctx.fillText(tick.label, x, y);
15808
- }
16277
+ const target = Math.max(3, Math.min(8, Math.floor(dataW / 64)));
16278
+ 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);
16279
+ const GAP = 12;
16280
+ const placed = [];
16281
+ const put = (tick) => {
16282
+ const l = tick.x - tick.half;
16283
+ const r = tick.x + tick.half;
16284
+ if (!placed.every((p) => r + GAP <= p.l || l - GAP >= p.r)) return;
16285
+ placed.push({ l, r });
16286
+ ctx.fillText(tick.label, tick.x, y);
16287
+ };
16288
+ for (const tick of ticks) if (tick.major) put(tick);
16289
+ for (const tick of ticks) if (!tick.major) put(tick);
15809
16290
  ctx.textAlign = "start";
15810
16291
  }
15811
16292
  };
@@ -15907,10 +16388,10 @@ var CrosshairRenderer = class {
15907
16388
  }
15908
16389
  /** The synced ghost: a dimmed vertical line at the bar the renderer resolved as
15909
16390
  * CONTAINING the foreign time (+ horizontal line when a comparable price came
15910
- * along), with that bar's time chip in this chart's own timezone. The snap
15911
- * happened upstream (`externalCrossPx`, floor-to-containing-bar) this method
15912
- * only draws. No price chip the ghost answers "when", the local crosshair
15913
- * answers "where". */
16391
+ * along), with that bar's time chip in this chart's own timezone and — when the
16392
+ * level resolved the price chip on the right axis. The snap happened upstream
16393
+ * (`externalCrossPx`, floor-to-containing-bar)this method only draws. Chips
16394
+ * render slightly dimmed so the ghost still reads as foreign. */
15914
16395
  drawExternal(ctx, ext, scene, coords, theme) {
15915
16396
  const cs = scene.style.crosshair;
15916
16397
  const dataW = coords.width;
@@ -15934,7 +16415,20 @@ var CrosshairRenderer = class {
15934
16415
  ctx.setLineDash([]);
15935
16416
  ctx.lineWidth = 1;
15936
16417
  ctx.globalAlpha = 0.8;
15937
- this.chip(ctx, x, dataH + 1, formatStamp(ext.time, scene.timezone), cs.labelBackground ?? theme.borderColor, "center", true, theme.background);
16418
+ const chipBg = cs.labelBackground ?? theme.borderColor;
16419
+ if (ext.y != null && ext.price != null) {
16420
+ let pane;
16421
+ for (const p of scene.panes.values()) {
16422
+ if (ext.y >= p.bounds.top && ext.y <= p.bounds.top + p.bounds.height) {
16423
+ pane = p;
16424
+ break;
16425
+ }
16426
+ }
16427
+ if (pane) {
16428
+ 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);
16429
+ }
16430
+ }
16431
+ this.chip(ctx, x, dataH + 1, formatStamp(ext.time, scene.timezone), chipBg, "center", true, theme.background);
15938
16432
  ctx.globalAlpha = 1;
15939
16433
  }
15940
16434
  /** A soft band + a brighter crisp center line over the hovered separator, so it reads as
@@ -16295,6 +16789,7 @@ function colorField(theme, getVal, onVal) {
16295
16789
  swatch.style.background = `linear-gradient(${v}, ${v}), ${CHECKER2}`;
16296
16790
  };
16297
16791
  paint();
16792
+ trigger.addEventListener("vela-sync", paint);
16298
16793
  trigger.addEventListener("click", (e) => {
16299
16794
  e.stopPropagation();
16300
16795
  if (openPopover && openPopover.trigger === trigger) {
@@ -16330,6 +16825,101 @@ function closeColorPopover() {
16330
16825
  closeOpen();
16331
16826
  }
16332
16827
 
16828
+ // src/renderers/native/chrome/WidthField.ts
16829
+ var WIDTH_FIELD_OPTIONS = [1, 2, 3, 4, 5];
16830
+ var STYLE_ID3 = "vela-width-field";
16831
+ function ensureStyles3() {
16832
+ if (typeof document === "undefined" || document.getElementById(STYLE_ID3)) return;
16833
+ const st = document.createElement("style");
16834
+ st.id = STYLE_ID3;
16835
+ st.textContent = `
16836
+ .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;}
16837
+ .vela-width-field:hover{border-color:var(--vela-fg-muted);}
16838
+ .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);}
16839
+ .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;}
16840
+ .vela-width-field-item:hover{background:var(--vela-hover-strong);}
16841
+ .vela-width-field-item[data-active='1']{background:var(--vela-active);}
16842
+ `;
16843
+ document.head.appendChild(st);
16844
+ }
16845
+ function lineGlyph(width) {
16846
+ 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>`;
16847
+ }
16848
+ 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>`;
16849
+ var openPopover2 = null;
16850
+ function closeOpen2() {
16851
+ if (!openPopover2) return;
16852
+ document.removeEventListener("pointerdown", openPopover2.onOutside, true);
16853
+ window.removeEventListener("resize", openPopover2.reflow, true);
16854
+ openPopover2.el.remove();
16855
+ openPopover2 = null;
16856
+ }
16857
+ function place2(pop, trigger) {
16858
+ const r = trigger.getBoundingClientRect();
16859
+ const w = pop.offsetWidth;
16860
+ const h = pop.offsetHeight;
16861
+ let left = r.right - w;
16862
+ let top = r.bottom + 6;
16863
+ if (left < 6) left = 6;
16864
+ if (left + w > window.innerWidth - 6) left = window.innerWidth - w - 6;
16865
+ if (top + h > window.innerHeight - 6) top = r.top - h - 6;
16866
+ if (top < 6) top = 6;
16867
+ pop.style.left = `${Math.round(left)}px`;
16868
+ pop.style.top = `${Math.round(top)}px`;
16869
+ }
16870
+ function widthField(theme, getVal, onVal) {
16871
+ ensureStyles3();
16872
+ const trigger = document.createElement("button");
16873
+ trigger.type = "button";
16874
+ trigger.className = "vela-width-field";
16875
+ const paint = () => {
16876
+ trigger.innerHTML = `<span style="display:flex;">${lineGlyph(getVal())}</span><span style="display:flex;">${CHEVRON}</span>`;
16877
+ };
16878
+ paint();
16879
+ trigger.addEventListener("click", (e) => {
16880
+ e.stopPropagation();
16881
+ if (openPopover2 && openPopover2.trigger === trigger) {
16882
+ closeOpen2();
16883
+ return;
16884
+ }
16885
+ closeOpen2();
16886
+ const pop = document.createElement("div");
16887
+ pop.className = "vela-width-field-pop";
16888
+ applyChromeTokens(pop, theme);
16889
+ pop.style.font = `var(--vela-font-size-md) ${theme.fontFamily}`;
16890
+ pop.addEventListener("pointerdown", (ev) => ev.stopPropagation());
16891
+ for (const w of WIDTH_FIELD_OPTIONS) {
16892
+ const item = document.createElement("button");
16893
+ item.type = "button";
16894
+ item.className = "vela-width-field-item";
16895
+ item.dataset.active = w === getVal() ? "1" : "0";
16896
+ item.innerHTML = `<span style="display:flex;flex:none;">${lineGlyph(w)}</span><span style="flex:1;font-variant-numeric:tabular-nums;">${w}px</span>`;
16897
+ item.addEventListener("click", (ev) => {
16898
+ ev.stopPropagation();
16899
+ closeOpen2();
16900
+ onVal(w);
16901
+ paint();
16902
+ });
16903
+ pop.appendChild(item);
16904
+ }
16905
+ document.body.appendChild(pop);
16906
+ place2(pop, trigger);
16907
+ const onOutside = (ev) => {
16908
+ const t = ev.target;
16909
+ if (pop.contains(t) || trigger.contains(t)) return;
16910
+ closeOpen2();
16911
+ };
16912
+ const reflow = () => place2(pop, trigger);
16913
+ setTimeout(() => document.addEventListener("pointerdown", onOutside, true), 0);
16914
+ window.addEventListener("resize", reflow, true);
16915
+ openPopover2 = { el: pop, trigger, onOutside, reflow };
16916
+ });
16917
+ return trigger;
16918
+ }
16919
+ function closeWidthPopover() {
16920
+ closeOpen2();
16921
+ }
16922
+
16333
16923
  // src/renderers/native/chrome/SettingsDialog.ts
16334
16924
  var BUILTIN_STYLE_LABELS = {
16335
16925
  candles: "Candles",
@@ -16412,6 +17002,30 @@ function ensureControlStyles() {
16412
17002
  muted and non-interactive. Applied to each row's children so it survives display:contents;
16413
17003
  !important beats the inline opacity on labels. */
16414
17004
  .vela-sd-soft>*{opacity:0.4 !important;pointer-events:none !important;}
17005
+ /* \u2500\u2500 mobile presentation (.vela-sd-mobile on the scrim; structural sizes are inline in open()) \u2500\u2500
17006
+ The tab rail becomes a burger-opened overlay sidebar; the group TOC becomes a sticky
17007
+ row of horizontally scrollable tabs; the instance strip scrolls instead of wrapping;
17008
+ controls grow to touch size and labels may wrap (the grid gives them a flexible track). */
17009
+ .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;}
17010
+ .vela-sd-mobile .vela-sd-burger:active{background:var(--vela-hover);}
17011
+ .vela-sd-mobile .vela-sd-railscrim{position:absolute;inset:0;z-index:2;background:var(--vela-backdrop);display:none;}
17012
+ .vela-sd-mobile .vela-sd-railscrim.open{display:block;}
17013
+ .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);}
17014
+ .vela-sd-mobile .vela-sd-rail.open{transform:translateX(0);}
17015
+ .vela-sd-mobile .vela-sd-tab{padding:12px;}
17016
+ .vela-sd-mobile .vela-sd-struct{flex-direction:column;padding-top:8px;}
17017
+ .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;}
17018
+ .vela-sd-mobile .vela-sd-toc::-webkit-scrollbar{display:none;}
17019
+ .vela-sd-mobile .vela-sd-toc-btn{flex:none;padding:8px 12px;font-size:13px;}
17020
+ .vela-sd-mobile .vela-sd-struct>[data-sd-rows-host]{border-left:none;padding-left:0;width:100%;}
17021
+ .vela-sd-mobile .vela-sd-itabs{flex-wrap:nowrap;overflow-x:auto;scrollbar-width:none;}
17022
+ .vela-sd-mobile .vela-sd-itabs::-webkit-scrollbar{display:none;}
17023
+ .vela-sd-mobile .vela-sd-itab{height:36px;flex:none;}
17024
+ .vela-sd-mobile .vela-sd-check{width:22px;height:22px;}
17025
+ .vela-sd-mobile .vela-sd-select,.vela-sd-mobile .vela-sd-number{height:34px;}
17026
+ .vela-sd-mobile .vela-sd-close{width:40px;height:40px;}
17027
+ .vela-sd-mobile .vela-sd-btn{height:38px;}
17028
+ .vela-sd-mobile .vela-sd-row span,.vela-sd-mobile .vela-sd-bool span{white-space:normal !important;}
16415
17029
  `;
16416
17030
  document.head.appendChild(st);
16417
17031
  }
@@ -16438,6 +17052,8 @@ var SettingsDialog = class {
16438
17052
  this.typeActiveGroup = /* @__PURE__ */ new Map();
16439
17053
  /** The tab currently shown, so a theme change (which rebuilds) lands back on it. */
16440
17054
  this.activeSection = null;
17055
+ /** Mobile chrome: fullscreen card, burger-opened section sidebar, TOC as top tabs. */
17056
+ this.mobileLayout = false;
16441
17057
  if (getComputedStyle(container).position === "static") container.style.position = "relative";
16442
17058
  }
16443
17059
  /** Host-app sections (e.g. the widget's Status line tab) — re-shown on next open. */
@@ -16456,15 +17072,27 @@ var SettingsDialog = class {
16456
17072
  }
16457
17073
  setTheme(theme) {
16458
17074
  this.theme = theme;
16459
- if (this.root) {
16460
- const cfg = this.config;
16461
- const oc = this.onChange;
16462
- const oi = this.onImport;
16463
- const orst = this.onReset;
16464
- const section = this.activeSection;
16465
- this.close();
16466
- if (cfg && oc) this.open(cfg, oc, oi ?? void 0, orst ?? void 0, section ?? void 0);
16467
- }
17075
+ this.reopenIfLive();
17076
+ }
17077
+ /** The host shell's chrome size class — mobile presents the dialog fullscreen with
17078
+ * the tab rail behind a burger sidebar and the TOC as top tabs. An open dialog
17079
+ * re-presents in place, on the same tab. */
17080
+ setLayoutMode(mode) {
17081
+ const mobile = mode === "mobile";
17082
+ if (mobile === this.mobileLayout) return;
17083
+ this.mobileLayout = mobile;
17084
+ this.reopenIfLive();
17085
+ }
17086
+ /** Rebuild an open dialog in place (theme swap, layout-mode flip) on the same tab. */
17087
+ reopenIfLive() {
17088
+ if (!this.root) return;
17089
+ const cfg = this.config;
17090
+ const oc = this.onChange;
17091
+ const oi = this.onImport;
17092
+ const orst = this.onReset;
17093
+ const section = this.activeSection;
17094
+ this.close();
17095
+ if (cfg && oc) this.open(cfg, oc, oi ?? void 0, orst ?? void 0, section ?? void 0);
16468
17096
  }
16469
17097
  isOpen() {
16470
17098
  return this.root !== null;
@@ -16486,18 +17114,37 @@ var SettingsDialog = class {
16486
17114
  this.onImport = onImport ?? null;
16487
17115
  this.onReset = onReset ?? null;
16488
17116
  ensureControlStyles();
17117
+ const mobile = this.mobileLayout;
16489
17118
  const scrim = document.createElement("div");
16490
- 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;";
17119
+ 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;";
17120
+ if (mobile) {
17121
+ scrim.classList.add("vela-sd-mobile");
17122
+ scrim.style.paddingTop = "0";
17123
+ scrim.style.alignItems = "stretch";
17124
+ }
16491
17125
  scrim.addEventListener("mousedown", (e) => {
16492
17126
  if (e.target === scrim) this.close();
16493
17127
  });
16494
17128
  const dlg = document.createElement("div");
16495
17129
  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;`;
17130
+ 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;";
16496
17131
  const header = document.createElement("div");
16497
17132
  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;`;
17133
+ if (mobile) header.style.paddingLeft = "8px";
16498
17134
  const hTitle = document.createElement("span");
16499
17135
  hTitle.textContent = "Chart settings";
16500
17136
  hTitle.style.cssText = "font-size:17px;font-weight:600;letter-spacing:0.2px;";
17137
+ if (mobile) hTitle.style.cssText += "flex:1 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;";
17138
+ let toggleRail = null;
17139
+ if (mobile) {
17140
+ const burger = document.createElement("button");
17141
+ burger.type = "button";
17142
+ burger.className = "vela-sd-burger";
17143
+ burger.innerHTML = iconAt("burger", 16);
17144
+ burger.title = "Sections";
17145
+ burger.addEventListener("click", () => toggleRail?.());
17146
+ header.append(burger);
17147
+ }
16501
17148
  const closeBtn = document.createElement("button");
16502
17149
  closeBtn.type = "button";
16503
17150
  closeBtn.innerHTML = iconAt("close", 15);
@@ -16505,8 +17152,8 @@ var SettingsDialog = class {
16505
17152
  closeBtn.className = "vela-sd-close";
16506
17153
  closeBtn.addEventListener("click", () => this.close());
16507
17154
  header.append(hTitle, closeBtn);
16508
- header.style.cursor = "move";
16509
- {
17155
+ if (!mobile) {
17156
+ header.style.cursor = "move";
16510
17157
  let sx = 0, sy = 0, ox = 0, oy = 0, dragging = false;
16511
17158
  header.addEventListener("pointerdown", (e) => {
16512
17159
  if (e.target?.closest(".vela-sd-close")) return;
@@ -16687,12 +17334,24 @@ var SettingsDialog = class {
16687
17334
  renderChartTypeSections("end");
16688
17335
  renderHostSections("end");
16689
17336
  const shell = document.createElement("div");
16690
- shell.style.cssText = "display:flex;min-height:360px;max-height:calc(70vh - 100px);flex:1 1 auto;";
17337
+ 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;";
16691
17338
  const rail = document.createElement("div");
16692
- 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;`;
17339
+ rail.className = "vela-sd-rail";
17340
+ 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;`;
17341
+ let railScrim = null;
17342
+ if (mobile) {
17343
+ railScrim = document.createElement("div");
17344
+ railScrim.className = "vela-sd-railscrim";
17345
+ railScrim.addEventListener("click", () => toggleRail?.());
17346
+ toggleRail = (open) => {
17347
+ const on = open ?? !rail.classList.contains("open");
17348
+ rail.classList.toggle("open", on);
17349
+ railScrim?.classList.toggle("open", on);
17350
+ };
17351
+ }
16693
17352
  const paneHost = document.createElement("div");
16694
17353
  paneHost.className = "vela-sd-pane";
16695
- paneHost.style.cssText = "flex:1;overflow-y:auto;padding:6px 18px 14px;";
17354
+ 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;";
16696
17355
  const panes = [];
16697
17356
  let current = null;
16698
17357
  for (const child of [...body.children]) {
@@ -16727,6 +17386,10 @@ var SettingsDialog = class {
16727
17386
  p.tab.classList.toggle("on", i === idx);
16728
17387
  });
16729
17388
  this.activeSection = panes[idx]?.title ?? null;
17389
+ if (mobile) {
17390
+ hTitle.textContent = panes[idx]?.title ?? "Chart settings";
17391
+ toggleRail?.(false);
17392
+ }
16730
17393
  };
16731
17394
  panes.forEach((p, i) => {
16732
17395
  p.tab.addEventListener("click", () => activate(i));
@@ -16734,10 +17397,11 @@ var SettingsDialog = class {
16734
17397
  paneHost.appendChild(p.el);
16735
17398
  });
16736
17399
  this.tabs = panes.map((p, i) => ({ title: p.title, show: () => activate(i) }));
16737
- const wanted = section === void 0 ? -1 : panes.findIndex((p) => p.title.toLowerCase() === section.toLowerCase());
17400
+ 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"));
16738
17401
  activate(wanted >= 0 ? wanted : 0);
16739
17402
  this.syncTypeTabs?.(config.series.style);
16740
17403
  shell.append(rail, paneHost);
17404
+ if (railScrim) shell.append(railScrim);
16741
17405
  dlg.appendChild(shell);
16742
17406
  dlg.appendChild(this.footer(config));
16743
17407
  scrim.appendChild(dlg);
@@ -16754,6 +17418,7 @@ var SettingsDialog = class {
16754
17418
  }
16755
17419
  close() {
16756
17420
  closeColorPopover();
17421
+ closeWidthPopover();
16757
17422
  this.root?.remove();
16758
17423
  this.root = null;
16759
17424
  this.tabs = [];
@@ -16794,20 +17459,12 @@ var SettingsDialog = class {
16794
17459
  body.append(marker);
16795
17460
  const values = config.chartTypes[typeId] ?? {};
16796
17461
  const bag = {};
16797
- const seedKey = (key, want, defval) => {
16798
- const v = values[key];
16799
- bag[key] = typeof v === want ? v : defval;
16800
- };
16801
17462
  const seed = (rows) => {
16802
17463
  for (const r of rows) {
16803
- if (r.kind === "heading" || r.kind === "header") continue;
16804
- if (r.kind === "range") {
16805
- seedKey(r.minKey, "number", r.defval);
16806
- seedKey(r.maxKey, "number", r.defval);
16807
- continue;
17464
+ for (const k of settingsRowValueKeys(r)) {
17465
+ const v = values[k.key];
17466
+ bag[k.key] = typeof v === k.type ? v : k.defval;
16808
17467
  }
16809
- seedKey(r.key, r.kind === "toggle" ? "boolean" : r.kind === "number" ? "number" : "string", r.defval);
16810
- if (r.kind === "toggle") for (const c of r.colors ?? []) seedKey(c.key, "string", c.defval);
16811
17468
  }
16812
17469
  };
16813
17470
  if (section.rows) seed(section.rows);
@@ -16830,7 +17487,8 @@ var SettingsDialog = class {
16830
17487
  if (section.instances && section.instances.length > 0) {
16831
17488
  body.append(this.instancesBlock(typeId, section.instances, bag, put, refreshers));
16832
17489
  } else if (section.rows) {
16833
- this.flatTypeRows(section.rows, bag, put, refreshers, body);
17490
+ if (section.layout === "grouped") body.append(this.groupedRows(`${typeId}/rows`, section.rows, bag, put, refreshers));
17491
+ else this.flatTypeRows(section.rows, bag, put, refreshers, body);
16834
17492
  }
16835
17493
  for (const sub of section.subsections ?? []) {
16836
17494
  const subMarker = this.section(sub.title);
@@ -16847,7 +17505,7 @@ var SettingsDialog = class {
16847
17505
  flatTypeRows(rows, bag, put, refreshers, body) {
16848
17506
  const entries = [];
16849
17507
  for (const r of rows) {
16850
- const el = r.kind === "heading" || r.kind === "header" ? this.sectionTitle(r.label) : this.typeRow(r, bag, put);
17508
+ const el = r.kind === "heading" || r.kind === "header" ? this.sectionTitle(r.label) : this.typeRow(r, bag, put, refreshers);
16851
17509
  entries.push({ el, when: r.when });
16852
17510
  body.append(el);
16853
17511
  }
@@ -16855,60 +17513,109 @@ var SettingsDialog = class {
16855
17513
  for (const e of entries) e.el.classList.toggle("vela-sd-hide", !settingsRowVisible(e.when, bag));
16856
17514
  });
16857
17515
  }
16858
- /** One value row for a chart-type descriptor, writing through `put`. */
16859
- typeRow(r, bag, put) {
16860
- if (r.kind === "toggle") {
16861
- const swatches = (r.colors ?? []).map((c) => {
16862
- const sw = this.swatch(bag[c.key], (v) => put(c.key, v));
16863
- sw.title = c.label;
16864
- return sw;
16865
- });
16866
- return this.toggleRow(r.label, bag[r.key], (v) => put(r.key, v), swatches);
17516
+ /**
17517
+ * One value row for a chart-type descriptor, writing through `put`. EVERY kind is
17518
+ * first reduced to the canonical composite shape ({@link normalizeSettingsRow}) and
17519
+ * rendered by the ONE path: optional leading toggle, then the ordered inline
17520
+ * controls in the control column. A control carrying its own `when` registers a
17521
+ * visibility refresher — it appears and disappears live as the bag changes,
17522
+ * independent of the row's gate — and is exempt from the toggle-off dim (it may
17523
+ * exist FOR the off state).
17524
+ */
17525
+ typeRow(r, bag, put, refreshers) {
17526
+ const n = normalizeSettingsRow(r);
17527
+ const controls = [];
17528
+ for (const c of n.controls) {
17529
+ const el = this.inlineControl(c, bag, put, n.toggle !== void 0);
17530
+ if (c.kind !== "hint") refreshers.push(() => el.dispatchEvent(new Event("vela-sync")));
17531
+ if (c.kind !== "hint" && c.when) {
17532
+ el.dataset.sdSelfGated = "1";
17533
+ const when = c.when;
17534
+ refreshers.push(() => {
17535
+ el.style.display = settingsRowVisible(when, bag) ? "" : "none";
17536
+ });
17537
+ }
17538
+ controls.push(el);
16867
17539
  }
16868
- if (r.kind === "number") return this.numberRow(r.label, bag[r.key], r.min ?? 0, r.max ?? 1e6, r.step ?? 1, (v) => put(r.key, v));
16869
- if (r.kind === "color") return this.colorRow(r.label, bag[r.key], (v) => put(r.key, v));
16870
- if (r.kind === "range") return this.rangeRow(r, bag, put);
16871
- return this.selectRowLabeled(r.label, bag[r.key], normalizeSelectOptions(r.options), (v) => put(r.key, v));
17540
+ if (n.toggle) {
17541
+ const t = n.toggle;
17542
+ const el = this.toggleRow(n.label, bag[t.key], (v) => put(t.key, v), controls, () => bag[t.key] === true);
17543
+ refreshers.push(() => el.dispatchEvent(new Event("vela-sync")));
17544
+ return el;
17545
+ }
17546
+ return this.rowWith(n.label, controls);
16872
17547
  }
16873
17548
  /**
16874
- * A MIN–MAX row: two number inputs on one row (stored under the descriptor's
16875
- * `minKey`/`maxKey`). With a `placeholder`, an input at the DEFAULT value renders
16876
- * empty showing it, and clearing an input stores the default back — the
16877
- * placeholder names the unset state ('Off' for 0-disables bounds).
17549
+ * Build ONE inline control from its descriptor the factory behind the composite
17550
+ * row path. `compact` narrows number inputs on toggle rows (the historical inline
17551
+ * width) while standalone rows keep the full-width input.
16878
17552
  */
16879
- rangeRow(r, bag, put) {
16880
- const { wrap } = this.row(r.label);
16881
- const input = (key, title) => {
16882
- const ni = document.createElement("input");
16883
- ni.type = "number";
16884
- ni.className = "vela-sd-number";
16885
- ni.min = String(r.min ?? 0);
16886
- ni.max = String(r.max ?? 1e6);
16887
- ni.step = String(r.step ?? 1);
16888
- ni.title = title;
16889
- const current = bag[key];
16890
- if (r.placeholder !== void 0) {
16891
- ni.placeholder = r.placeholder;
16892
- if (current !== r.defval) ni.value = String(current);
16893
- } else {
16894
- ni.value = String(current);
17553
+ inlineControl(c, bag, put, compact) {
17554
+ if (c.kind === "hint") {
17555
+ const s = document.createElement("span");
17556
+ s.textContent = c.text;
17557
+ s.style.cssText = "color:var(--vela-fg-muted);";
17558
+ return s;
17559
+ }
17560
+ if (c.kind === "color") {
17561
+ const sw = this.swatch(bag[c.key], (v) => put(c.key, v), () => bag[c.key]);
17562
+ sw.title = c.label;
17563
+ return sw;
17564
+ }
17565
+ if (c.kind === "width") {
17566
+ let cur = typeof bag[c.key] === "number" ? bag[c.key] : c.defval;
17567
+ const wf = widthField(this.theme, () => cur, (v) => {
17568
+ cur = v;
17569
+ put(c.key, v);
17570
+ });
17571
+ wf.title = c.label;
17572
+ return wf;
17573
+ }
17574
+ if (c.kind === "select") {
17575
+ const sel = document.createElement("select");
17576
+ sel.className = "vela-sd-select";
17577
+ sel.style.cssText = "max-width:200px;flex:0 0 auto;";
17578
+ sel.title = c.label;
17579
+ const current2 = typeof bag[c.key] === "string" ? bag[c.key] : c.defval;
17580
+ for (const [val, lbl] of normalizeSelectOptions(c.options)) {
17581
+ const o = document.createElement("option");
17582
+ o.value = val;
17583
+ o.textContent = lbl;
17584
+ if (val === current2) o.selected = true;
17585
+ sel.appendChild(o);
16895
17586
  }
17587
+ sel.addEventListener("change", () => put(c.key, sel.value));
17588
+ sel.addEventListener("vela-sync", () => {
17589
+ sel.value = typeof bag[c.key] === "string" ? bag[c.key] : c.defval;
17590
+ });
17591
+ return sel;
17592
+ }
17593
+ const ni = document.createElement("input");
17594
+ ni.type = "number";
17595
+ ni.className = "vela-sd-number";
17596
+ if (compact) ni.style.width = "56px";
17597
+ if (c.min !== void 0) ni.min = String(c.min);
17598
+ if (c.max !== void 0) ni.max = String(c.max);
17599
+ ni.step = String(c.step ?? 1);
17600
+ ni.title = c.label;
17601
+ const current = typeof bag[c.key] === "number" ? bag[c.key] : c.defval;
17602
+ if (c.placeholder !== void 0) {
17603
+ ni.placeholder = c.placeholder;
17604
+ if (current !== c.defval) ni.value = String(current);
16896
17605
  ni.addEventListener("change", () => {
16897
- const raw = ni.value.trim() === "" ? r.defval : Number(ni.value);
16898
- const v = Number.isFinite(raw) ? Math.min(r.max ?? Infinity, Math.max(r.min ?? -Infinity, raw)) : r.defval;
16899
- ni.value = r.placeholder !== void 0 && v === r.defval ? "" : String(v);
16900
- put(key, v);
17606
+ const raw = ni.value.trim() === "" ? c.defval : Number(ni.value);
17607
+ const v = Number.isFinite(raw) ? Math.min(c.max ?? Infinity, Math.max(c.min ?? -Infinity, raw)) : c.defval;
17608
+ ni.value = v === c.defval ? "" : String(v);
17609
+ put(c.key, v);
16901
17610
  });
16902
- return ni;
16903
- };
16904
- const box = document.createElement("div");
16905
- box.style.cssText = "display:flex;align-items:center;gap:6px;flex:0 0 auto;";
16906
- const dash = document.createElement("span");
16907
- dash.textContent = "\u2013";
16908
- dash.style.cssText = "color:var(--vela-fg-muted);";
16909
- box.append(input(r.minKey, `${r.label} \u2014 min`), dash, input(r.maxKey, `${r.label} \u2014 max`));
16910
- wrap.appendChild(box);
16911
- return wrap;
17611
+ } else {
17612
+ ni.value = String(current);
17613
+ ni.addEventListener("input", () => {
17614
+ const v = Number(ni.value);
17615
+ if (Number.isFinite(v)) put(c.key, v);
17616
+ });
17617
+ }
17618
+ return ni;
16912
17619
  }
16913
17620
  /**
16914
17621
  * The INSTANCE STRIP block: a tab per present instance (label, `×` on the active
@@ -16921,7 +17628,7 @@ var SettingsDialog = class {
16921
17628
  const wrap = document.createElement("div");
16922
17629
  const strip = document.createElement("div");
16923
17630
  strip.className = "vela-sd-itabs";
16924
- wrap.append(strip);
17631
+ if (instances.length > 1 || instances[0]?.enableKey !== void 0) wrap.append(strip);
16925
17632
  const contents = instances.map((inst, i) => {
16926
17633
  const content = this.groupedRows(`${typeId}/#${i}`, inst.rows, bag, put, refreshers);
16927
17634
  wrap.append(content);
@@ -17016,8 +17723,8 @@ var SettingsDialog = class {
17016
17723
  rowsHost.append(entries[entries.length - 1].el);
17017
17724
  continue;
17018
17725
  }
17019
- const el = this.typeRow(r, bag, put);
17020
- const key = r.kind === "range" ? void 0 : r.key;
17726
+ const el = this.typeRow(r, bag, put, refreshers);
17727
+ const key = r.kind === "row" ? r.toggle?.key : r.kind === "range" ? void 0 : r.key;
17021
17728
  entries.push({ el, when: r.when, group: g, key });
17022
17729
  rowsHost.append(el);
17023
17730
  }
@@ -17083,6 +17790,14 @@ var SettingsDialog = class {
17083
17790
  */
17084
17791
  layoutSettingsGrids(pane, measureHost) {
17085
17792
  if (pane.childElementCount === 0) return;
17793
+ if (this.mobileLayout) {
17794
+ const grid2 = document.createElement("div");
17795
+ grid2.style.cssText = "display:grid;grid-template-columns:minmax(0,1fr) max-content;align-items:center;column-gap:12px;row-gap:16px;";
17796
+ while (pane.firstChild) grid2.appendChild(pane.firstChild);
17797
+ pane.appendChild(grid2);
17798
+ this.prepareGridItems(grid2);
17799
+ return;
17800
+ }
17086
17801
  const rows = [...pane.querySelectorAll(".vela-sd-row")];
17087
17802
  const probe2 = document.createElement("div");
17088
17803
  probe2.style.cssText = "position:absolute;left:-9999px;top:0;visibility:hidden;display:flex;flex-direction:column;font:13px var(--vela-font);pointer-events:none;";
@@ -17119,9 +17834,11 @@ var SettingsDialog = class {
17119
17834
  }
17120
17835
  }
17121
17836
  /** A bare color swatch input (for toggle-row right groups / swatch pairs). */
17122
- swatch(value, onChange) {
17837
+ /** With `get`, the field reads its value live (and repaints on 'vela-sync')
17838
+ * duplicate-keyed rows stay honest when another row edits the shared key. */
17839
+ swatch(value, onChange, get) {
17123
17840
  let current = value;
17124
- return colorField(this.theme, () => current, (v) => {
17841
+ return colorField(this.theme, () => get ? get() : current, (v) => {
17125
17842
  current = v;
17126
17843
  onChange(v);
17127
17844
  });
@@ -17173,8 +17890,11 @@ var SettingsDialog = class {
17173
17890
  }
17174
17891
  /** An enable row: checkbox + label in the label column, dependent controls in the
17175
17892
  * shared control column; the control group dims and ignores input while the toggle
17176
- * is off. With no controls it reads like a plain toggle row (full-width in the grid). */
17177
- toggleRow(label, value, onToggle, controls) {
17893
+ * is off. With no controls it reads like a plain toggle row (full-width in the grid).
17894
+ * With `get`, the row re-reads its state on a 'vela-sync' event — the seam that
17895
+ * keeps DUPLICATE-KEYED rows honest (several `when`-gated rows over one bag key,
17896
+ * only one visible at a time; see `typeRow`). */
17897
+ toggleRow(label, value, onToggle, controls, get) {
17178
17898
  const wrap = document.createElement("div");
17179
17899
  const cb = document.createElement("button");
17180
17900
  cb.type = "button";
@@ -17194,6 +17914,12 @@ var SettingsDialog = class {
17194
17914
  wrap.style.cssText = "display:flex;align-items:center;gap:8px;min-height:22px;";
17195
17915
  wrap.append(cb, lbl);
17196
17916
  cb.addEventListener("click", () => onToggle(cbToggle()));
17917
+ if (get) {
17918
+ wrap.addEventListener("vela-sync", () => {
17919
+ checked = get();
17920
+ cb.classList.toggle("on", checked);
17921
+ });
17922
+ }
17197
17923
  return wrap;
17198
17924
  }
17199
17925
  wrap.className = "vela-sd-row";
@@ -17204,8 +17930,11 @@ var SettingsDialog = class {
17204
17930
  box.style.cssText = "display:flex;align-items:center;gap:6px;";
17205
17931
  for (const c of controls) box.appendChild(c);
17206
17932
  const syncDim = (on) => {
17207
- box.style.opacity = on ? "1" : "0.4";
17208
- box.style.pointerEvents = on ? "" : "none";
17933
+ for (const c of controls) {
17934
+ if (c.dataset.sdSelfGated === "1") continue;
17935
+ c.style.opacity = on ? "1" : "0.4";
17936
+ c.style.pointerEvents = on ? "" : "none";
17937
+ }
17209
17938
  };
17210
17939
  syncDim(value);
17211
17940
  cb.addEventListener("click", () => {
@@ -17213,6 +17942,13 @@ var SettingsDialog = class {
17213
17942
  onToggle(v);
17214
17943
  syncDim(v);
17215
17944
  });
17945
+ if (get) {
17946
+ wrap.addEventListener("vela-sync", () => {
17947
+ checked = get();
17948
+ cb.classList.toggle("on", checked);
17949
+ syncDim(checked);
17950
+ });
17951
+ }
17216
17952
  wrap.append(left, box);
17217
17953
  return wrap;
17218
17954
  }
@@ -17232,96 +17968,6 @@ var SettingsDialog = class {
17232
17968
  wrap.appendChild(ni);
17233
17969
  return wrap;
17234
17970
  }
17235
- /** Inline Auto/Manual row (Resolution, Text size): a mode dropdown + a value input greyed out in Auto. */
17236
- autoManualRow(label, isAuto, value, min, max, unit, onMode, onValue) {
17237
- const { wrap } = this.row(label);
17238
- const box = document.createElement("div");
17239
- box.style.cssText = "display:flex;align-items:center;gap:6px;flex:0 0 auto;";
17240
- const sel = document.createElement("select");
17241
- sel.className = "vela-sd-select";
17242
- for (const [val, lbl] of AUTO_MANUAL_OPTS) {
17243
- const o = document.createElement("option");
17244
- o.value = val;
17245
- o.textContent = lbl;
17246
- if (val === "auto" === isAuto) o.selected = true;
17247
- sel.appendChild(o);
17248
- }
17249
- const ni = document.createElement("input");
17250
- ni.type = "number";
17251
- ni.min = String(min);
17252
- ni.max = String(max);
17253
- ni.step = "1";
17254
- ni.value = String(value);
17255
- ni.className = "vela-sd-number";
17256
- const unitLbl = document.createElement("span");
17257
- unitLbl.textContent = unit;
17258
- unitLbl.style.cssText = "opacity:0.6;";
17259
- const syncDisabled = (auto) => {
17260
- ni.disabled = auto;
17261
- ni.style.opacity = auto ? "0.4" : "1";
17262
- };
17263
- syncDisabled(isAuto);
17264
- sel.addEventListener("change", () => {
17265
- const auto = sel.value === "auto";
17266
- onMode(auto);
17267
- syncDisabled(auto);
17268
- });
17269
- ni.addEventListener("input", () => {
17270
- const n = Number(ni.value);
17271
- if (Number.isFinite(n)) onValue(n);
17272
- });
17273
- box.append(sel, ni, unitLbl);
17274
- wrap.appendChild(box);
17275
- return wrap;
17276
- }
17277
- /** A row whose control area holds several inline controls (e.g. show + color + width). */
17278
- inlineRow(label, controls) {
17279
- const { wrap } = this.row(label);
17280
- const box = document.createElement("div");
17281
- box.style.cssText = "display:flex;align-items:center;gap:6px;flex:0 0 auto;";
17282
- for (const c of controls) box.appendChild(c);
17283
- wrap.appendChild(box);
17284
- return wrap;
17285
- }
17286
- /** A small dimmed hint span (e.g. the ≥ / ≤ between filter inputs). */
17287
- hint(text) {
17288
- const s = document.createElement("span");
17289
- s.textContent = text;
17290
- s.style.cssText = "opacity:0.5;font-size:12px;";
17291
- return s;
17292
- }
17293
- /** A bare color input (for inline groups). */
17294
- colorInput(value, onChange) {
17295
- const ci = document.createElement("input");
17296
- ci.type = "color";
17297
- ci.value = toHex6(value);
17298
- ci.style.cssText = "cursor:pointer;width:34px;height:22px;border:none;background:transparent;padding:0;flex:0 0 auto;";
17299
- ci.addEventListener("input", () => onChange(ci.value));
17300
- return ci;
17301
- }
17302
- /** A bare compact number input (for inline groups). */
17303
- numberInput(value, min, max, step, onChange) {
17304
- const ni = document.createElement("input");
17305
- ni.type = "number";
17306
- ni.value = String(value);
17307
- ni.min = String(min);
17308
- ni.max = String(max);
17309
- ni.step = String(step);
17310
- ni.className = "vela-sd-number";
17311
- ni.style.flex = "0 0 auto";
17312
- ni.addEventListener("input", () => {
17313
- const n = Number(ni.value);
17314
- if (Number.isFinite(n)) onChange(n);
17315
- });
17316
- return ni;
17317
- }
17318
- /** A small in-group heading (lighter than a top-level section divider). */
17319
- subheading(text) {
17320
- const el = document.createElement("div");
17321
- el.textContent = text;
17322
- el.style.cssText = "margin-top:4px;font-size:10.5px;font-weight:700;letter-spacing:0.03em;text-transform:uppercase;opacity:0.45;";
17323
- return el;
17324
- }
17325
17971
  /** A dropdown whose option values differ from their display labels. */
17326
17972
  selectRowLabeled(label, value, options, onChange) {
17327
17973
  const { wrap } = this.row(label);
@@ -17367,19 +18013,7 @@ var SettingsDialog = class {
17367
18013
  foot.appendChild(resetBtn);
17368
18014
  return foot;
17369
18015
  }
17370
- button(label, onClick) {
17371
- const b = document.createElement("button");
17372
- b.type = "button";
17373
- b.textContent = label;
17374
- b.style.cssText = `cursor:pointer;flex:1 1 auto;${this.ctrlStyle()}padding:5px 8px;font-weight:600;`;
17375
- b.addEventListener("click", onClick);
17376
- return b;
17377
- }
17378
- ctrlStyle() {
17379
- 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;`;
17380
- }
17381
18016
  };
17382
- var AUTO_MANUAL_OPTS = [["auto", "Auto"], ["manual", "Manual"]];
17383
18017
  function normalizeSelectOptions(options) {
17384
18018
  return options.map((o) => typeof o === "string" ? [o, o] : o);
17385
18019
  }
@@ -17582,7 +18216,7 @@ var DrawingPainter = class {
17582
18216
  return;
17583
18217
  }
17584
18218
  if (d instanceof FixedRangeVolumeProfile) {
17585
- this.paintFixedRangeVp(ctx, d, proj);
18219
+ this.paintFixedRangeVp(ctx, d, proj, theme);
17586
18220
  this.paintLabel(ctx, d, proj, theme);
17587
18221
  return;
17588
18222
  }
@@ -18075,7 +18709,7 @@ var DrawingPainter = class {
18075
18709
  /** Paint a fixed-range volume profile: horizontal histogram rows (up/down split) anchored to
18076
18710
  * the left or right of the time span, optional VAH / VAL / POC levels across the range, and
18077
18711
  * optional developing POC / VA polylines. Recomputes from the two anchors on every paint. */
18078
- paintFixedRangeVp(ctx, d, proj) {
18712
+ paintFixedRangeVp(ctx, d, proj, theme) {
18079
18713
  const L = d.layout(proj);
18080
18714
  if (!L) return;
18081
18715
  const s = d.frvp;
@@ -18114,7 +18748,7 @@ var DrawingPainter = class {
18114
18748
  };
18115
18749
  hLine(s.showVah, s.vahColor, s.vahStyle, L.vahY);
18116
18750
  hLine(s.showVal, s.valColor, s.valStyle, L.valY);
18117
- hLine(s.showPoc, s.pocColor, s.pocStyle, L.pocY);
18751
+ hLine(s.showPoc, s.pocColor ?? contrastColor(theme.background), s.pocStyle, L.pocY);
18118
18752
  const devW = Math.max(1, w);
18119
18753
  if (s.showDevelopingPoc) {
18120
18754
  this.strokePolyline(ctx, L.developingPoc, {
@@ -18986,7 +19620,11 @@ var DrawingInteraction = class {
18986
19620
  const last = draft.anchors[draft.anchors.length - 1];
18987
19621
  const lx = last ? proj.xOf(last.time) : NaN;
18988
19622
  const ly = last ? proj.yOf(last.price, draft.paneId) ?? NaN : NaN;
18989
- if (draft.anchors.length < this.state.need && Math.hypot(x - lx, y - ly) > FREEHAND_SAMPLE_PX) {
19623
+ if (Math.hypot(x - lx, y - ly) > FREEHAND_SAMPLE_PX) {
19624
+ if (draft.anchors.length >= this.state.need) {
19625
+ const lastIdx = draft.anchors.length - 1;
19626
+ draft.anchors = draft.anchors.filter((_, i) => i % 2 === 0 || i === lastIdx);
19627
+ }
18990
19628
  draft.anchors.push(proj.pxToPoint(x, y, draft.paneId));
18991
19629
  }
18992
19630
  this.state.cursor = proj.pxToPoint(x, y, draft.paneId);
@@ -19059,11 +19697,11 @@ function applyFree(anchor, pt, free) {
19059
19697
  // src/renderers/native/drawings/DrawingSettingsPopup.ts
19060
19698
  var BTN = 30;
19061
19699
  var ICON = 21;
19062
- var STYLE_ID3 = "vela-drawing-popup-styles";
19063
- function ensureStyles3() {
19064
- if (typeof document === "undefined" || document.getElementById(STYLE_ID3)) return;
19700
+ var STYLE_ID4 = "vela-drawing-popup-styles";
19701
+ function ensureStyles4() {
19702
+ if (typeof document === "undefined" || document.getElementById(STYLE_ID4)) return;
19065
19703
  const s = document.createElement("style");
19066
- s.id = STYLE_ID3;
19704
+ s.id = STYLE_ID4;
19067
19705
  s.textContent = `
19068
19706
  .vela-dpop input[type=color]{-webkit-appearance:none;appearance:none;border:none;padding:0;background:none;cursor:pointer;border-radius:0;}
19069
19707
  .vela-dpop input[type=color]::-webkit-color-swatch-wrapper{padding:0;}
@@ -19082,7 +19720,10 @@ function ensureStyles3() {
19082
19720
  .vela-dpop-btn[data-active='1']{background:var(--vela-active);color:var(--vela-fg-bright);}
19083
19721
  .vela-dpop-item{background:transparent;transition:background var(--vela-dur-fast) ease;}
19084
19722
  .vela-dpop-item:hover{background:var(--vela-hover-strong);}
19085
- .vela-dpop-item[data-active='1']{background:var(--vela-active);}`;
19723
+ .vela-dpop-item[data-active='1']{background:var(--vela-active);}
19724
+ /* A finger needs a wider grab zone than a cursor \u2014 grow the move handle on touch-first
19725
+ devices (the glyph stays centered; only the hit target widens). */
19726
+ @media (pointer: coarse){.vela-dpop-grip{width:${BTN + 14}px !important;}}`;
19086
19727
  document.head.appendChild(s);
19087
19728
  }
19088
19729
  var DrawingSettingsPopup = class {
@@ -19129,7 +19770,7 @@ var DrawingSettingsPopup = class {
19129
19770
  * `onClose` fires on an outside-click dismissal (not a programmatic close). */
19130
19771
  open(drawing, anchor, actions, onClose) {
19131
19772
  this.close();
19132
- ensureStyles3();
19773
+ ensureStyles4();
19133
19774
  this.onClose = onClose ?? null;
19134
19775
  const t = this.theme;
19135
19776
  const schema = drawing.schema();
@@ -19155,7 +19796,14 @@ var DrawingSettingsPopup = class {
19155
19796
  bar.appendChild(this.dropdown("Icon size", STAMP_SIZE_OPTIONS, sz, (s) => stampSizeIcon(s), (v) => actions.patch({ size: v }), { label: sizeLabel }));
19156
19797
  }
19157
19798
  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 })));
19158
- 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 }));
19799
+ if (paths.has("style.lineWidth")) {
19800
+ const wf = schema.fields.find((f) => f.path === "style.lineWidth");
19801
+ if (wf?.kind === "number" && (wf.min ?? 1) > 1) {
19802
+ bar.appendChild(this.widthInput("Line width", drawing.style.lineWidth, wf.min ?? 1, wf.max ?? 60, wf.step ?? 1, (v) => actions.patch({ "style.lineWidth": v })));
19803
+ } else {
19804
+ 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 }));
19805
+ }
19806
+ }
19159
19807
  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 }));
19160
19808
  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 })));
19161
19809
  const isFrvp = paths.has("frvp.rows") && drawing instanceof FixedRangeVolumeProfile;
@@ -19623,14 +20271,15 @@ var DrawingSettingsPopup = class {
19623
20271
  const chk = document.createElement("input");
19624
20272
  chk.type = "checkbox";
19625
20273
  chk.checked = Boolean(s[showPath]);
19626
- chk.style.cssText = `accent-color:${s[colorPath]};width:15px;height:15px;flex:none;cursor:pointer;`;
20274
+ const current = s[colorPath] ?? contrastColor(t.background);
20275
+ chk.style.cssText = `accent-color:${current};width:15px;height:15px;flex:none;cursor:pointer;`;
19627
20276
  chk.addEventListener("change", () => actions.patch({ [`frvp.${showPath}`]: chk.checked }));
19628
20277
  const lbl = document.createElement("span");
19629
20278
  lbl.textContent = label;
19630
20279
  lbl.style.cssText = "flex:1;min-width:0;opacity:0.9;";
19631
20280
  const col = document.createElement("button");
19632
20281
  col.type = "button";
19633
- let cur = s[colorPath];
20282
+ let cur = current;
19634
20283
  col.style.cssText = `width:18px;height:18px;flex:none;border:1px solid var(--vela-border);border-radius:0;cursor:pointer;background:${cur};padding:0;`;
19635
20284
  col.addEventListener("pointerdown", (e) => e.stopPropagation());
19636
20285
  col.addEventListener("click", (e) => {
@@ -19810,7 +20459,8 @@ var DrawingSettingsPopup = class {
19810
20459
  dragHandle() {
19811
20460
  const b = document.createElement("button");
19812
20461
  b.type = "button";
19813
- 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);`;
20462
+ b.className = "vela-dpop-grip";
20463
+ 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);`;
19814
20464
  b.innerHTML = sized(GRIP_ICON);
19815
20465
  b.addEventListener("pointerdown", (e) => {
19816
20466
  const el = this.el;
@@ -19826,6 +20476,10 @@ var DrawingSettingsPopup = class {
19826
20476
  const origLeft = parseFloat(el.style.left) || 0;
19827
20477
  const origTop = parseFloat(el.style.top) || 0;
19828
20478
  b.style.cursor = "grabbing";
20479
+ try {
20480
+ b.setPointerCapture(e.pointerId);
20481
+ } catch {
20482
+ }
19829
20483
  const move = (ev) => {
19830
20484
  const w = el.offsetWidth;
19831
20485
  const h = el.offsetHeight;
@@ -19838,9 +20492,11 @@ var DrawingSettingsPopup = class {
19838
20492
  b.style.cursor = "grab";
19839
20493
  window.removeEventListener("pointermove", move);
19840
20494
  window.removeEventListener("pointerup", up);
20495
+ window.removeEventListener("pointercancel", up);
19841
20496
  };
19842
20497
  window.addEventListener("pointermove", move);
19843
20498
  window.addEventListener("pointerup", up);
20499
+ window.addEventListener("pointercancel", up);
19844
20500
  });
19845
20501
  return b;
19846
20502
  }
@@ -19956,6 +20612,39 @@ var DrawingSettingsPopup = class {
19956
20612
  });
19957
20613
  return b;
19958
20614
  }
20615
+ /** An inline numeric width field for tools whose stroke range outgrows the 1–4px
20616
+ * ladder — the value is clamped to the schema's declared min/max on commit. */
20617
+ widthInput(tip, value, min, max, step, onChange) {
20618
+ const input = document.createElement("input");
20619
+ input.type = "number";
20620
+ input.dataset.tip = tip;
20621
+ input.min = String(min);
20622
+ input.max = String(max);
20623
+ input.step = String(step);
20624
+ input.value = String(value);
20625
+ 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;`;
20626
+ const commit = () => {
20627
+ const n = parseFloat(input.value);
20628
+ if (!Number.isFinite(n)) {
20629
+ input.value = String(value);
20630
+ return;
20631
+ }
20632
+ const v = Math.min(max, Math.max(min, n));
20633
+ input.value = String(v);
20634
+ value = v;
20635
+ onChange(v);
20636
+ };
20637
+ input.addEventListener("change", commit);
20638
+ input.addEventListener("keydown", (e) => {
20639
+ e.stopPropagation();
20640
+ if (e.key === "Enter") {
20641
+ e.preventDefault();
20642
+ commit();
20643
+ input.blur();
20644
+ }
20645
+ });
20646
+ return input;
20647
+ }
19959
20648
  /** A pick-one dropdown: the trigger shows the current value's glyph (plus an optional inline
19960
20649
  * text label — e.g. `2px` — for controls that would otherwise look alike), and clicking it
19961
20650
  * opens a floating list of every option so a value is one click away (no cycling through). */
@@ -20262,7 +20951,7 @@ var DrawingToolbar = class {
20262
20951
  this.onCollapse = options.onCollapse ?? (() => {
20263
20952
  });
20264
20953
  this.onDrawingsSync = options.onDrawingsSync ?? null;
20265
- ensureStyles4();
20954
+ ensureStyles5();
20266
20955
  this.root = document.createElement("div");
20267
20956
  this.root.className = "vela-dtb";
20268
20957
  this.styleRoot();
@@ -20745,13 +21434,13 @@ var DrawingToolbar = class {
20745
21434
  }
20746
21435
  };
20747
21436
  var MAGNET_ID = "__magnet__";
20748
- var STYLE_ID4 = "vela-drawing-toolbar-styles";
20749
- function ensureStyles4() {
21437
+ var STYLE_ID5 = "vela-drawing-toolbar-styles";
21438
+ function ensureStyles5() {
20750
21439
  if (typeof document === "undefined") return;
20751
- let s = document.getElementById(STYLE_ID4);
21440
+ let s = document.getElementById(STYLE_ID5);
20752
21441
  if (!s) {
20753
21442
  s = document.createElement("style");
20754
- s.id = STYLE_ID4;
21443
+ s.id = STYLE_ID5;
20755
21444
  document.head.appendChild(s);
20756
21445
  }
20757
21446
  s.textContent = `
@@ -21033,6 +21722,9 @@ var UserDrawingController = class {
21033
21722
  // mirrors showToolbar (drives the gutter reservation)
21034
21723
  this.toolbarCollapsed = false;
21035
21724
  // the bar is a slim expand-strip (narrower gutter)
21725
+ /** Mobile chrome: the docked bar is suppressed (the shell provides its own tool
21726
+ * picker) while `toolbarVisible` keeps the host's INTENT for a later desktop flip. */
21727
+ this.mobileLayout = false;
21036
21728
  /** Self-serve Ctrl+Z / Ctrl+Y as drawing undo/redo. A host that owns a UNIFIED
21037
21729
  * history (drawings + app ops in one timeline) turns this off so the chords
21038
21730
  * bubble to its keymap instead — see the renderer's `historyChords` feature. */
@@ -21091,12 +21783,21 @@ var UserDrawingController = class {
21091
21783
  }
21092
21784
  showToolbar(visible) {
21093
21785
  this.toolbarVisible = visible;
21094
- this.toolbar.setVisible(visible);
21786
+ this.toolbar.setVisible(visible && !this.mobileLayout);
21787
+ this.syncToolbarGutter();
21788
+ }
21789
+ /** Mobile suppresses the docked toolbar (hover flyouts don't work with touch; the
21790
+ * shell's own drawer picks tools instead) and releases its gutter; flipping back
21791
+ * to desktop restores whatever `showToolbar` last asked for. */
21792
+ setLayoutMode(mode) {
21793
+ this.mobileLayout = mode === "mobile";
21794
+ this.toolbar.setVisible(this.toolbarVisible && !this.mobileLayout);
21095
21795
  this.syncToolbarGutter();
21096
21796
  }
21097
21797
  /** The gutter follows the bar's current footprint: hidden 0, collapsed a slim strip, else full width. */
21098
21798
  syncToolbarGutter() {
21099
- this.deps.setToolbarGutter(this.toolbarVisible ? this.toolbarCollapsed ? TOOLBAR_COLLAPSED_WIDTH : TOOLBAR_WIDTH : 0);
21799
+ const shown = this.toolbarVisible && !this.mobileLayout;
21800
+ this.deps.setToolbarGutter(shown ? this.toolbarCollapsed ? TOOLBAR_COLLAPSED_WIDTH : TOOLBAR_WIDTH : 0);
21100
21801
  }
21101
21802
  /** Core push: mirror (or clear) another chart's in-progress placement as a ghost. */
21102
21803
  setExternalGhost(doc) {
@@ -21476,6 +22177,16 @@ var UserDrawingController = class {
21476
22177
  if (this.eraserMode) return "pointer";
21477
22178
  return this.interaction.cursorAt(x, y);
21478
22179
  }
22180
+ /** Right-click while placing: cancel the in-progress drawing and revert to the
22181
+ * pointer — the gesture is an explicit escape, so it disarms even in
22182
+ * stay-in-drawing-mode (where Escape would leave the tool armed). Returns whether
22183
+ * the press was consumed; false lets the host's context menu open normally. */
22184
+ cancelPlacement() {
22185
+ if (!this.interaction.isPlacing()) return false;
22186
+ this.interaction.cancel();
22187
+ if (this.activeTool != null) this.emit({ kind: "arm", type: null });
22188
+ return true;
22189
+ }
21479
22190
  /** Double-click over a drawing → suppress the chart's view reset (single-click already
21480
22191
  * opens settings). Returns true only when a drawing is under the cursor. */
21481
22192
  dblClick(x, y) {
@@ -21960,6 +22671,20 @@ var VolumeRenderer = class {
21960
22671
  };
21961
22672
 
21962
22673
  // src/renderers/native/layers.ts
22674
+ function foldBaseModulation(acc, next) {
22675
+ if (next == null) return acc;
22676
+ if (acc == null) return { ...next };
22677
+ return {
22678
+ candleBodyScale: minOpt(acc.candleBodyScale, next.candleBodyScale),
22679
+ candleBodyAlpha: minOpt(acc.candleBodyAlpha, next.candleBodyAlpha),
22680
+ gridAlpha: minOpt(acc.gridAlpha, next.gridAlpha)
22681
+ };
22682
+ }
22683
+ function minOpt(a, b) {
22684
+ if (a == null) return b;
22685
+ if (b == null) return a;
22686
+ return Math.min(a, b);
22687
+ }
21963
22688
  var registry3 = /* @__PURE__ */ new Map();
21964
22689
  function registerRendererLayer(def) {
21965
22690
  registry3.set(def.id, def);
@@ -21977,7 +22702,7 @@ var LUXALGO_WORDMARK_SVG = '<svg viewBox="0 0 964 252" aria-hidden="true"><path
21977
22702
 
21978
22703
  // src/renderers/native/chrome/AttributionMark.ts
21979
22704
  var ATTRIBUTION_URL = "https://luxalgo.com/vela";
21980
- var STYLE_ID5 = "vela-attribution-styles";
22705
+ var STYLE_ID6 = "vela-attribution-styles";
21981
22706
  var CSS = `
21982
22707
  .vela-attribution {
21983
22708
  text-decoration: none;
@@ -22023,12 +22748,28 @@ var CSS = `
22023
22748
  max-width: 240px;
22024
22749
  opacity: 1;
22025
22750
  transform: translateX(0);
22026
- }`;
22027
- function ensureStyles5(doc) {
22028
- let s = doc.getElementById(STYLE_ID5);
22751
+ }
22752
+ /* Mobile: a hair smaller so the mark doesn't dominate a phone-width plot. Keyed on the
22753
+ renderer's own container attribute (data-vela-layout) AND the shell root's
22754
+ (data-layout): the workspace's single grid-wide mark lives OUTSIDE any renderer
22755
+ container, so only the shell attribute reaches it. */
22756
+ [data-vela-layout='mobile'] .vela-attribution .vela-attr-symbol svg,
22757
+ [data-layout='mobile'] .vela-attribution .vela-attr-symbol svg { height: 22px; }
22758
+ [data-vela-layout='mobile'] .vela-attribution .vela-attr-wordmark,
22759
+ [data-layout='mobile'] .vela-attribution .vela-attr-wordmark {
22760
+ top: 2px;
22761
+ transform: translateX(-6px);
22762
+ }
22763
+ [data-vela-layout='mobile'] .vela-attribution .vela-attr-wordmark svg,
22764
+ [data-layout='mobile'] .vela-attribution .vela-attr-wordmark svg { height: 21px; }
22765
+ [data-vela-layout='mobile'] .vela-attribution:hover .vela-attr-wordmark,
22766
+ [data-layout='mobile'] .vela-attribution:hover .vela-attr-wordmark { transform: translateX(0); }
22767
+ `;
22768
+ function ensureStyles6(doc) {
22769
+ let s = doc.getElementById(STYLE_ID6);
22029
22770
  if (!s) {
22030
22771
  s = doc.createElement("style");
22031
- s.id = STYLE_ID5;
22772
+ s.id = STYLE_ID6;
22032
22773
  doc.head.appendChild(s);
22033
22774
  }
22034
22775
  s.textContent = CSS;
@@ -22057,7 +22798,7 @@ function createCustomMark(doc, html, background) {
22057
22798
  return el;
22058
22799
  }
22059
22800
  function createAttributionMark(doc, background) {
22060
- ensureStyles5(doc);
22801
+ ensureStyles6(doc);
22061
22802
  const a = doc.createElement("a");
22062
22803
  a.className = "vela-attribution";
22063
22804
  a.href = ATTRIBUTION_URL;
@@ -22083,6 +22824,174 @@ function createAttributionMark(doc, background) {
22083
22824
  return a;
22084
22825
  }
22085
22826
 
22827
+ // src/renderers/shared/dom-raster.ts
22828
+ function hasInk(color) {
22829
+ if (!color || color === "transparent") return false;
22830
+ const m = /rgba?\(\s*[\d.]+\s*,\s*[\d.]+\s*,\s*[\d.]+\s*,\s*([\d.]+)\s*\)/.exec(color);
22831
+ return m ? parseFloat(m[1]) > 1e-3 : true;
22832
+ }
22833
+ function cornerRadius(cs, w, h) {
22834
+ const raw = cs.borderTopLeftRadius;
22835
+ const v = parseFloat(raw) || 0;
22836
+ const px = raw.endsWith("%") ? v / 100 * Math.min(w, h) : v;
22837
+ return Math.max(0, Math.min(px, Math.min(w, h) / 2));
22838
+ }
22839
+ function tracePath(ctx, x, y, w, h, r) {
22840
+ ctx.beginPath();
22841
+ if (r > 0 && typeof ctx.roundRect === "function") ctx.roundRect(x, y, w, h, r);
22842
+ else ctx.rect(x, y, w, h);
22843
+ }
22844
+ function canDrawUntainted(img) {
22845
+ try {
22846
+ const probe2 = img.ownerDocument.createElement("canvas");
22847
+ probe2.width = 1;
22848
+ probe2.height = 1;
22849
+ const ctx = probe2.getContext("2d");
22850
+ if (!ctx) return false;
22851
+ ctx.drawImage(img, 0, 0, 1, 1);
22852
+ probe2.toDataURL();
22853
+ return true;
22854
+ } catch {
22855
+ return false;
22856
+ }
22857
+ }
22858
+ function rasterizeOverlay(ctx, root, frame) {
22859
+ const win = root.ownerDocument.defaultView;
22860
+ if (!win) return;
22861
+ ctx.save();
22862
+ ctx.scale(frame.dpr, frame.dpr);
22863
+ const drawText = (node, cs, alpha) => {
22864
+ const text = node.textContent?.replace(/\s+/g, " ").trim();
22865
+ if (!text) return;
22866
+ const range = node.ownerDocument.createRange();
22867
+ range.selectNodeContents(node);
22868
+ const r = range.getBoundingClientRect();
22869
+ if (r.width <= 0 || r.height <= 0) return;
22870
+ ctx.globalAlpha = alpha;
22871
+ ctx.fillStyle = cs.color;
22872
+ ctx.font = `${cs.fontStyle} ${cs.fontWeight} ${cs.fontSize} ${cs.fontFamily}`;
22873
+ ctx.textBaseline = "middle";
22874
+ ctx.textAlign = "left";
22875
+ ctx.fillText(text, r.left - frame.left, r.top - frame.top + r.height / 2);
22876
+ };
22877
+ const drawImg = (img, cs, alpha) => {
22878
+ if (!img.complete || img.naturalWidth <= 0 || !canDrawUntainted(img)) return;
22879
+ const r = img.getBoundingClientRect();
22880
+ if (r.width <= 0 || r.height <= 0) return;
22881
+ const x = r.left - frame.left;
22882
+ const y = r.top - frame.top;
22883
+ ctx.save();
22884
+ ctx.globalAlpha = alpha;
22885
+ tracePath(ctx, x, y, r.width, r.height, cornerRadius(cs, r.width, r.height));
22886
+ ctx.clip();
22887
+ ctx.drawImage(img, x, y, r.width, r.height);
22888
+ ctx.restore();
22889
+ };
22890
+ const drawSvgShapes = (parent, alpha) => {
22891
+ for (const el of parent.children) {
22892
+ if (el.hasAttribute("transform")) continue;
22893
+ const tag = el.tagName.toLowerCase();
22894
+ if (tag === "g") {
22895
+ drawSvgShapes(el, alpha);
22896
+ continue;
22897
+ }
22898
+ const num3 = (name) => parseFloat(el.getAttribute(name) ?? "0") || 0;
22899
+ let path = null;
22900
+ if (tag === "path") {
22901
+ const d = el.getAttribute("d");
22902
+ if (d) path = new Path2D(d);
22903
+ } else if (tag === "circle") {
22904
+ path = new Path2D();
22905
+ path.arc(num3("cx"), num3("cy"), num3("r"), 0, Math.PI * 2);
22906
+ } else if (tag === "ellipse") {
22907
+ path = new Path2D();
22908
+ path.ellipse(num3("cx"), num3("cy"), num3("rx"), num3("ry"), 0, 0, Math.PI * 2);
22909
+ } else if (tag === "rect") {
22910
+ path = new Path2D();
22911
+ path.rect(num3("x"), num3("y"), num3("width"), num3("height"));
22912
+ } else if (tag === "line") {
22913
+ path = new Path2D();
22914
+ path.moveTo(num3("x1"), num3("y1"));
22915
+ path.lineTo(num3("x2"), num3("y2"));
22916
+ } else if (tag === "polyline" || tag === "polygon") {
22917
+ const pts = (el.getAttribute("points") ?? "").trim().split(/[\s,]+/).map(Number);
22918
+ if (pts.length < 4 || pts.some(Number.isNaN)) continue;
22919
+ path = new Path2D();
22920
+ path.moveTo(pts[0], pts[1]);
22921
+ for (let i = 2; i + 1 < pts.length; i += 2) path.lineTo(pts[i], pts[i + 1]);
22922
+ if (tag === "polygon") path.closePath();
22923
+ }
22924
+ if (!path) continue;
22925
+ const cs = win.getComputedStyle(el);
22926
+ if (cs.fill && cs.fill !== "none" && hasInk(cs.fill)) {
22927
+ ctx.globalAlpha = alpha * (parseFloat(cs.fillOpacity) || 1);
22928
+ ctx.fillStyle = cs.fill;
22929
+ ctx.fill(path);
22930
+ }
22931
+ const sw = parseFloat(cs.strokeWidth) || 0;
22932
+ if (cs.stroke && cs.stroke !== "none" && hasInk(cs.stroke) && sw > 0) {
22933
+ ctx.globalAlpha = alpha * (parseFloat(cs.strokeOpacity) || 1);
22934
+ ctx.strokeStyle = cs.stroke;
22935
+ ctx.lineWidth = sw;
22936
+ if (cs.strokeLinecap === "round" || cs.strokeLinecap === "square" || cs.strokeLinecap === "butt") ctx.lineCap = cs.strokeLinecap;
22937
+ if (cs.strokeLinejoin === "round" || cs.strokeLinejoin === "bevel" || cs.strokeLinejoin === "miter") ctx.lineJoin = cs.strokeLinejoin;
22938
+ ctx.stroke(path);
22939
+ }
22940
+ }
22941
+ };
22942
+ const drawSvg = (svg, parentAlpha) => {
22943
+ const r = svg.getBoundingClientRect();
22944
+ if (r.width <= 0 || r.height <= 0) return;
22945
+ const cs = win.getComputedStyle(svg);
22946
+ if (cs.display === "none" || cs.visibility === "hidden") return;
22947
+ const alpha = parentAlpha * (parseFloat(cs.opacity) || 1);
22948
+ if (alpha <= 1e-3) return;
22949
+ const vb = svg.viewBox.baseVal;
22950
+ const vw = vb && vb.width > 0 ? vb.width : r.width;
22951
+ const vh = vb && vb.height > 0 ? vb.height : r.height;
22952
+ ctx.save();
22953
+ ctx.translate(r.left - frame.left, r.top - frame.top);
22954
+ ctx.scale(r.width / vw, r.height / vh);
22955
+ if (vb) ctx.translate(-vb.x, -vb.y);
22956
+ drawSvgShapes(svg, alpha);
22957
+ ctx.restore();
22958
+ };
22959
+ const drawElement = (el, parentAlpha) => {
22960
+ const cs = win.getComputedStyle(el);
22961
+ if (cs.display === "none" || cs.visibility === "hidden") return;
22962
+ const alpha = parentAlpha * (parseFloat(cs.opacity) || 1);
22963
+ if (alpha <= 1e-3) return;
22964
+ const r = el.getBoundingClientRect();
22965
+ if (r.width > 0 && r.height > 0) {
22966
+ const x = r.left - frame.left;
22967
+ const y = r.top - frame.top;
22968
+ const radius = cornerRadius(cs, r.width, r.height);
22969
+ if (hasInk(cs.backgroundColor)) {
22970
+ ctx.globalAlpha = alpha;
22971
+ ctx.fillStyle = cs.backgroundColor;
22972
+ tracePath(ctx, x, y, r.width, r.height, radius);
22973
+ ctx.fill();
22974
+ }
22975
+ const bw = parseFloat(cs.borderTopWidth) || 0;
22976
+ if (bw > 0 && cs.borderTopStyle !== "none" && hasInk(cs.borderTopColor)) {
22977
+ ctx.globalAlpha = alpha;
22978
+ ctx.strokeStyle = cs.borderTopColor;
22979
+ ctx.lineWidth = bw;
22980
+ tracePath(ctx, x + bw / 2, y + bw / 2, r.width - bw, r.height - bw, Math.max(0, radius - bw / 2));
22981
+ ctx.stroke();
22982
+ }
22983
+ }
22984
+ for (const child of el.childNodes) {
22985
+ if (child.nodeType === 3) drawText(child, cs, alpha);
22986
+ else if (child instanceof win.HTMLImageElement) drawImg(child, win.getComputedStyle(child), alpha);
22987
+ else if (child instanceof win.SVGSVGElement) drawSvg(child, alpha);
22988
+ else if (child instanceof win.HTMLElement) drawElement(child, alpha);
22989
+ }
22990
+ };
22991
+ if (root instanceof win.HTMLElement) drawElement(root, 1);
22992
+ ctx.restore();
22993
+ }
22994
+
22086
22995
  // src/renderers/native/vpvr/profile.ts
22087
22996
  function buildVpvrProfile(bars, i0, i1, rowCount, valueAreaFrac) {
22088
22997
  const from = Math.max(0, i0);
@@ -22251,12 +23160,13 @@ var AXIS_COL_W = AXIS_MERGED_W;
22251
23160
  var COLLAPSED_PANE_H = 26;
22252
23161
  var TIME_AXIS_H = 22;
22253
23162
  var SCROLL_BTN_SIZE = 26;
23163
+ var SCROLL_BTN_SIZE_TOUCH = 24;
22254
23164
  var SCROLL_BTN_RADIUS = 5;
22255
23165
  var SCROLL_BTN_SHADOW = "0 2px 8px rgba(0,0,0,0.4)";
22256
23166
  var SCROLL_BTN_ICON_PX = 12;
22257
- var SCROLL_BTN_RIGHT_INSET = 25;
23167
+ var SCROLL_BTN_RIGHT_INSET = 12;
22258
23168
  var SCROLL_BTN_RIGHT = RIGHT_AXIS_W + SCROLL_BTN_RIGHT_INSET;
22259
- var SCROLL_BTN_BOTTOM = TIME_AXIS_H + 30;
23169
+ var SCROLL_BTN_BOTTOM = TIME_AXIS_H + 14;
22260
23170
  var SCROLL_BTN_PROXIMITY_PX = 120;
22261
23171
  var MIN_VISIBLE_BARS = 2;
22262
23172
  var ZOOM_OUT_MARGIN_BARS = 6;
@@ -22308,8 +23218,12 @@ var NativeRenderer = class {
22308
23218
  this.symbolPicker = null;
22309
23219
  /** Indicator titles (the legend rows) shown — held here so a remount re-applies it. */
22310
23220
  this.indicatorTitlesOn = true;
23221
+ /** Plot values beside the legend titles shown — held here so a remount re-applies it. */
23222
+ this.indicatorValuesOn = true;
22311
23223
  /** Host-contributed legend actions — held here so a rebuild of the legend re-wires them. */
22312
23224
  this.legendActionsProvider = null;
23225
+ /** Host override of the legend's fold toggle — held here so a remount re-applies it. */
23226
+ this.legendOverviewAction = null;
22313
23227
  // ── keyboard navigation / accessibility (item 11) ──
22314
23228
  this.keyboard = null;
22315
23229
  this.keyboardEnabled = true;
@@ -22414,11 +23328,13 @@ var NativeRenderer = class {
22414
23328
  this.scrollTargetRO = null;
22415
23329
  // eased rightOffset target while gliding back to the latest bars
22416
23330
  // Distance (px) from the plot's bottom edge to the button — tracks the bottom-most EXPANDED
22417
- // pane like the watermark: when the lower sub-panes collapse it rides up into the open pane.
23331
+ // pane: when the lower sub-panes collapse it rides up into the open pane.
22418
23332
  this.scrollBtnBottomPx = SCROLL_BTN_BOTTOM;
22419
23333
  // Distance (px) from the plot's right edge — clears the FULL scale gutter, which widens when a
22420
23334
  // pane carries merged (own-scale) columns; the constant only clears a single-column scale.
22421
23335
  this.scrollBtnRightPx = SCROLL_BTN_RIGHT;
23336
+ // ── chrome size class (pushed by the host shell; see IChartRenderer.setLayoutMode) ──
23337
+ this.layoutMode = "desktop";
22422
23338
  this.viewportCbs = /* @__PURE__ */ new Set();
22423
23339
  this.crosshairCbs = /* @__PURE__ */ new Set();
22424
23340
  this.chartTypeSettingsCbs = /* @__PURE__ */ new Set();
@@ -22429,6 +23345,7 @@ var NativeRenderer = class {
22429
23345
  this.factoryConfig = null;
22430
23346
  this.hostSettingsSections = [];
22431
23347
  this.clickCbs = /* @__PURE__ */ new Set();
23348
+ this.axisLongPressCbs = /* @__PURE__ */ new Set();
22432
23349
  this.inputChangeCbs = /* @__PURE__ */ new Set();
22433
23350
  this.removeIndicatorCbs = /* @__PURE__ */ new Set();
22434
23351
  this.toggleVisibleCbs = /* @__PURE__ */ new Set();
@@ -22436,7 +23353,7 @@ var NativeRenderer = class {
22436
23353
  this.priceStyleCbs = /* @__PURE__ */ new Set();
22437
23354
  this.tableOverlays = /* @__PURE__ */ new Map();
22438
23355
  this.name = "native";
22439
- 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"];
23356
+ 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"];
22440
23357
  /** Track cursor proximity to the scroll button on the plot (bubbles from the button too,
22441
23358
  * so moving onto the button doesn't count as leaving). */
22442
23359
  this.onScrollProximityMove = (e) => {
@@ -22609,6 +23526,11 @@ var NativeRenderer = class {
22609
23526
  this.inputsUI?.setTitlesVisible(this.indicatorTitlesOn);
22610
23527
  return;
22611
23528
  // own DOM, no repaint needed
23529
+ case "indicatorValues":
23530
+ this.indicatorValuesOn = value !== false;
23531
+ this.inputsUI?.setValuesVisible(this.indicatorValuesOn);
23532
+ return;
23533
+ // own DOM, no repaint needed
22612
23534
  case "attribution":
22613
23535
  this.attributionHtml = typeof value === "string" && value.trim() ? value : null;
22614
23536
  this.attributionEnabled = this.attributionHtml !== null || Boolean(value);
@@ -22697,6 +23619,8 @@ var NativeRenderer = class {
22697
23619
  return this.settingsEnabled;
22698
23620
  case "indicatorTitles":
22699
23621
  return this.indicatorTitlesOn;
23622
+ case "indicatorValues":
23623
+ return this.indicatorValuesOn;
22700
23624
  case "attribution":
22701
23625
  return this.attributionHtml ?? this.attributionEnabled;
22702
23626
  case "dialogHost":
@@ -23033,7 +23957,10 @@ var NativeRenderer = class {
23033
23957
  /** Port surface: hosts (bottom-bar / chrome buttons) open the same dialog as the
23034
23958
  * in-chart gear — created on demand, independent of the gear feature being enabled. */
23035
23959
  openSettingsDialog(section) {
23036
- if (!this.settingsDialog && this.plot) this.settingsDialog = new SettingsDialog(this.dialogHost ?? this.plot, this.theme);
23960
+ if (!this.settingsDialog && this.plot) {
23961
+ this.settingsDialog = new SettingsDialog(this.dialogHost ?? this.plot, this.theme);
23962
+ this.settingsDialog.setLayoutMode(this.layoutMode);
23963
+ }
23037
23964
  this.toggleSettingsDialog(section);
23038
23965
  }
23039
23966
  /** (Re)aim the dialog's Canvas → Theme row: the current selection reflects the STABLE
@@ -23122,11 +24049,18 @@ var NativeRenderer = class {
23122
24049
  this.scrollButton.style.color = this.theme.textColor;
23123
24050
  this.scrollButton.style.borderColor = this.theme.borderColor;
23124
24051
  }
23125
- /** Show the button only when the latest bar is scrolled off the right edge AND the cursor is nearby. */
24052
+ /** Show the button only when the latest bar is scrolled off the right edge. On desktop the
24053
+ * cursor must also be nearby (hover reveal); on mobile — no hovering cursor — the button
24054
+ * appears whenever the latest bars are off-screen and hides again once the view is back
24055
+ * at the right edge. */
23126
24056
  updateScrollToRealtimeButton() {
23127
24057
  const btn = this.scrollButton;
23128
24058
  if (!btn) return;
23129
24059
  const latestOffScreen = this.coords.barCount > 0 && this.coords.getViewport().rightOffset < 0;
24060
+ if (this.layoutMode === "mobile") {
24061
+ btn.style.display = latestOffScreen ? "flex" : "none";
24062
+ return;
24063
+ }
23130
24064
  btn.style.display = latestOffScreen && this.pointerNearScrollBtn ? "flex" : "none";
23131
24065
  }
23132
24066
  /** Glide the view back to the most recent bars, keeping the current zoom (barSpacing). */
@@ -23148,17 +24082,20 @@ var NativeRenderer = class {
23148
24082
  this.animator.start();
23149
24083
  }
23150
24084
  /**
23151
- * Export the current chart as a PNG data URL by compositing the geometry (L0)
23152
- * + chrome (L1) + user-drawings (L1.5) canvases onto an offscreen canvas. The
23153
- * background is filled first (the canvas2d data layer is transparent — its bg
23154
- * lives on the wrapper), and a fresh synchronous paint runs first so the WebGL2
23155
- * backend's (non-preserved) drawing buffer is populated before it's read back
23156
- * this tick the same paint also repaints the drawings layers, so they're
23157
- * current (interleaved drawings are already inside the data composite). They're
23158
- * drawn bottom-up in DOM stacking order so the front user drawings (trend lines,
23159
- * boxes, etc.) sit above the Pine drawings on the chrome layer, matching what's
23160
- * on screen. The crosshair (L2) and DOM overlays (tables, legend) are
23161
- * intentionally excluded.
24085
+ * Export the current chart as a PNG data URL by compositing EVERY plot canvas onto
24086
+ * an offscreen canvas, in the same stacking order the DOM shows: plugin layers
24087
+ * below the data, geometry (L0), volume columns (L0.25), the visible-range volume
24088
+ * profile (L0.6), plugin layers above the data, chrome (L1), and user drawings
24089
+ * (L1.5). The background is filled first (the canvas2d data layer is transparent —
24090
+ * its bg lives on the wrapper), and a fresh synchronous paint runs first so the
24091
+ * WebGL2 backend's (non-preserved) drawing buffer is populated before it's read
24092
+ * back this tick the same paint also repaints the drawings layers, so they're
24093
+ * current. DOM chrome joins as a best-effort text/chip raster (see
24094
+ * `rasterizeOverlay`): the per-pane indicator legends, plus any host overlay that
24095
+ * opts in with a `data-vela-screenshot` attribute on the mount container's
24096
+ * subtree (the widget marks its status line, and its symbol watermark with
24097
+ * `"under"` — drawn beneath the canvases, where it sits on screen). Only the
24098
+ * crosshair (L2) is intentionally excluded.
23162
24099
  */
23163
24100
  screenshot() {
23164
24101
  if (!this.dataCanvas) return null;
@@ -23171,9 +24108,25 @@ var NativeRenderer = class {
23171
24108
  if (!ctx) return null;
23172
24109
  ctx.fillStyle = this.theme.background;
23173
24110
  ctx.fillRect(0, 0, out.width, out.height);
23174
- ctx.drawImage(this.dataCanvas, 0, 0);
23175
- ctx.drawImage(this.chromeCanvas, 0, 0);
23176
- ctx.drawImage(this.drawingsCanvas, 0, 0);
24111
+ const plotRect = this.plot?.getBoundingClientRect();
24112
+ const frame = plotRect && plotRect.width > 0 ? { left: plotRect.left, top: plotRect.top, dpr: out.width / plotRect.width } : null;
24113
+ const hostMarked = [...this.mountContainer?.querySelectorAll("[data-vela-screenshot]") ?? []];
24114
+ if (frame) {
24115
+ for (const el of hostMarked) {
24116
+ if (el.getAttribute("data-vela-screenshot") === "under") rasterizeOverlay(ctx, el, frame);
24117
+ }
24118
+ }
24119
+ const below = this.extLayers.filter((l) => l.def.placement === "below-data").map((l) => l.canvas);
24120
+ const above = this.extLayers.filter((l) => l.def.placement !== "below-data").map((l) => l.canvas);
24121
+ for (const canvas of [...below, this.dataCanvas, this.volumeCanvas, this.vpvrCanvas, ...above, this.chromeCanvas, this.drawingsCanvas]) {
24122
+ if (canvas && canvas.width > 0 && canvas.height > 0) ctx.drawImage(canvas, 0, 0);
24123
+ }
24124
+ if (frame) {
24125
+ for (const lg of this.plot?.querySelectorAll("[data-vela-pane]") ?? []) rasterizeOverlay(ctx, lg, frame);
24126
+ for (const el of hostMarked) {
24127
+ if (el.getAttribute("data-vela-screenshot") !== "under") rasterizeOverlay(ctx, el, frame);
24128
+ }
24129
+ }
23177
24130
  return out.toDataURL("image/png");
23178
24131
  }
23179
24132
  /**
@@ -23324,6 +24277,9 @@ var NativeRenderer = class {
23324
24277
  fling: (v) => this.fling(v),
23325
24278
  onPointerMove: (x, y) => this.handlePointerMove(x, y),
23326
24279
  onClick: (x) => this.handleClick(x),
24280
+ onAxisLongPress: (axis, x, y) => {
24281
+ for (const cb of this.axisLongPressCbs) cb({ axis, x, y });
24282
+ },
23327
24283
  beginPriceScale: (x, y) => this.beginPriceScale(x, y),
23328
24284
  priceScaleBy: (dy) => this.priceScaleBy(dy),
23329
24285
  beginPricePan: (y) => this.beginPricePan(y),
@@ -23339,6 +24295,7 @@ var NativeRenderer = class {
23339
24295
  drawingsClaim: (x, y) => this.userDrawings?.claim(x, y) ?? false,
23340
24296
  drawingsMeasureStart: (x, y) => this.userDrawings?.beginMeasureAt(x, y) ?? false,
23341
24297
  drawingsDeleteAt: (x, y) => this.userDrawings?.deleteAt(x, y) ?? false,
24298
+ drawingsCancelPlacement: () => this.userDrawings?.cancelPlacement() ?? false,
23342
24299
  drawingsSnapMode: () => this.snapMode,
23343
24300
  drawingsPointerDown: (x, y, snap, shift) => this.userDrawings?.pointerDown(x, y, snap, shift),
23344
24301
  drawingsPointerMove: (x, y, snap, shift) => this.userDrawings?.pointerMove(x, y, snap, shift),
@@ -23370,6 +24327,14 @@ var NativeRenderer = class {
23370
24327
  setToolbarGutter: (px) => this.setToolbarGutter(px)
23371
24328
  });
23372
24329
  this.userDrawings.historyChords = this.historyChordsEnabled;
24330
+ if (this.layoutMode === "mobile") {
24331
+ this.mountContainer?.setAttribute("data-vela-layout", this.layoutMode);
24332
+ this.userDrawings.setLayoutMode(this.layoutMode);
24333
+ if (this.scrollButton) {
24334
+ this.scrollButton.style.width = `${SCROLL_BTN_SIZE_TOUCH}px`;
24335
+ this.scrollButton.style.height = `${SCROLL_BTN_SIZE_TOUCH}px`;
24336
+ }
24337
+ }
23373
24338
  this.keyboard = new KeyboardController({
23374
24339
  panByBars: (bars) => this.panByBars(bars),
23375
24340
  zoomByStep: (dir) => this.zoomByStep(dir),
@@ -23382,10 +24347,13 @@ var NativeRenderer = class {
23382
24347
  });
23383
24348
  this.setKeyboardEnabled(this.keyboardEnabled);
23384
24349
  this.inputsUI = new InputsUI(this.plot, theme, (paneId) => this.paneBoundsFor(paneId));
24350
+ this.inputsUI.setLayoutMode(this.layoutMode);
23385
24351
  this.inputsUI.setTitlesVisible(this.indicatorTitlesOn);
24352
+ this.inputsUI.setValuesVisible(this.indicatorValuesOn);
23386
24353
  this.inputsUI.setDialogHost(this.dialogHost);
23387
24354
  this.inputsUI.setSymbolPicker(this.symbolPicker);
23388
24355
  this.inputsUI.setLegendActions(this.legendActionsProvider);
24356
+ this.inputsUI.setLegendOverviewAction(this.legendOverviewAction);
23389
24357
  this.inputsUI.setOnChange((c) => {
23390
24358
  for (const cb of this.inputChangeCbs) cb({ indicatorId: c.indicatorId, key: c.key, value: c.value });
23391
24359
  });
@@ -23579,6 +24547,8 @@ var NativeRenderer = class {
23579
24547
  this.attributionEl = null;
23580
24548
  this.mountContainer?.style.removeProperty("--vela-toolbar-gutter");
23581
24549
  this.mountContainer?.style.removeProperty("--vela-scale-gutter");
24550
+ this.mountContainer?.style.removeProperty("--vela-price-pane-top");
24551
+ this.mountContainer?.style.removeProperty("--vela-price-pane-bottom");
23582
24552
  this.mountContainer = null;
23583
24553
  this.wrapper?.remove();
23584
24554
  }
@@ -23800,7 +24770,10 @@ var NativeRenderer = class {
23800
24770
  this.scene.indicators.set(model.id, model);
23801
24771
  this.refreshAnchorOffset(model);
23802
24772
  this.scene.assignIndicatorZ(model.id);
23803
- this.inputsUI.upsert(model.id, model.title, model.inputs, model.inputValues, model.paneId, { native: !!model.native });
24773
+ this.inputsUI.upsert(model.id, model.shorttitle ?? model.title, model.inputs, model.inputValues, model.paneId, {
24774
+ native: !!model.native,
24775
+ ...model.shorttitle ? { settingsTitle: model.title } : {}
24776
+ });
23804
24777
  this.syncTables(model);
23805
24778
  if (model.native?.type === "volume") {
23806
24779
  this.volumeActive = true;
@@ -23861,6 +24834,13 @@ var NativeRenderer = class {
23861
24834
  this.legendActionsProvider = provider;
23862
24835
  this.inputsUI?.setLegendActions(provider);
23863
24836
  }
24837
+ setLegendOverviewAction(action) {
24838
+ this.legendOverviewAction = action;
24839
+ this.inputsUI?.setLegendOverviewAction(action);
24840
+ }
24841
+ openIndicatorSettings(indicatorId) {
24842
+ this.inputsUI?.openSettingsFor(indicatorId);
24843
+ }
23864
24844
  /**
23865
24845
  * Hide/show a mounted indicator. Hiding drops its model from the scene (so every paint path —
23866
24846
  * series, fills, drawings, tables, glow, data window — skips it) while keeping its z key and its
@@ -23914,6 +24894,24 @@ var NativeRenderer = class {
23914
24894
  this.removeIndicatorCbs.add(cb);
23915
24895
  return () => this.removeIndicatorCbs.delete(cb);
23916
24896
  }
24897
+ /** Port surface: the host shell's chrome size class. Mobile switches the renderer
24898
+ * chrome to its touch-first presentation — fullscreen dialogs, no docked drawing
24899
+ * toolbar (the shell provides its own picker), the scroll-to-latest button shown
24900
+ * whenever the latest bars are off-screen (touch has no cursor proximity). */
24901
+ setLayoutMode(mode) {
24902
+ if (mode === this.layoutMode) return;
24903
+ this.layoutMode = mode;
24904
+ this.mountContainer?.setAttribute("data-vela-layout", mode);
24905
+ this.userDrawings?.setLayoutMode(mode);
24906
+ this.settingsDialog?.setLayoutMode(mode);
24907
+ this.inputsUI?.setLayoutMode(mode);
24908
+ if (this.scrollButton) {
24909
+ const px = mode === "mobile" ? SCROLL_BTN_SIZE_TOUCH : SCROLL_BTN_SIZE;
24910
+ this.scrollButton.style.width = `${px}px`;
24911
+ this.scrollButton.style.height = `${px}px`;
24912
+ }
24913
+ this.updateScrollToRealtimeButton();
24914
+ }
23917
24915
  setSettingsSections(sections) {
23918
24916
  this.hostSettingsSections = sections;
23919
24917
  this.settingsDialog?.setHostSections(sections);
@@ -23938,6 +24936,10 @@ var NativeRenderer = class {
23938
24936
  this.clickCbs.add(cb);
23939
24937
  return () => this.clickCbs.delete(cb);
23940
24938
  }
24939
+ onAxisLongPress(cb) {
24940
+ this.axisLongPressCbs.add(cb);
24941
+ return () => this.axisLongPressCbs.delete(cb);
24942
+ }
23941
24943
  onViewportChange(cb) {
23942
24944
  this.viewportCbs.add(cb);
23943
24945
  return () => this.viewportCbs.delete(cb);
@@ -24079,6 +25081,7 @@ var NativeRenderer = class {
24079
25081
  this.skeletonClockMs += dtMs;
24080
25082
  this.paintData();
24081
25083
  this.crosshairLayer.render(this.scene, this.coords, this.theme, this.hoverSeparatorY, this.externalCrossPx());
25084
+ this.updateLegendValues();
24082
25085
  this.emitViewportChange();
24083
25086
  return active;
24084
25087
  }
@@ -24143,7 +25146,7 @@ var NativeRenderer = class {
24143
25146
  this.lastPointer = null;
24144
25147
  this.scheduler.invalidate(1 /* Cursor */);
24145
25148
  this.hoverLogical = null;
24146
- const empty = { time: null, price: null, values: /* @__PURE__ */ new Map(), ohlc: null };
25149
+ const empty = { time: null, price: null, paneKind: null, values: /* @__PURE__ */ new Map(), ohlc: null };
24147
25150
  for (const cb of this.crosshairCbs) cb(empty);
24148
25151
  return;
24149
25152
  }
@@ -24155,7 +25158,7 @@ var NativeRenderer = class {
24155
25158
  const logical = Math.round(this.coords.xToLogical(x));
24156
25159
  const onBar = logical >= 0 && logical < this.coords.barCount;
24157
25160
  const time = onBar ? this.coords.logicalToTime(logical) : null;
24158
- const pane = this.paneAtY(y);
25161
+ const pane = this.paneNodeAtY(y);
24159
25162
  const price = inData && pane ? this.coords.yToPrice(y, pane.scale, pane.bounds) : null;
24160
25163
  const values = /* @__PURE__ */ new Map();
24161
25164
  if (onBar) {
@@ -24172,7 +25175,7 @@ var NativeRenderer = class {
24172
25175
  const bar = onBar ? this.bars[logical] : void 0;
24173
25176
  const ohlc = bar ? { time: bar.time, open: bar.open, high: bar.high, low: bar.low, close: bar.close, volume: bar.volume } : null;
24174
25177
  this.hoverLogical = onBar ? logical : null;
24175
- const event = { time, price, values, ohlc };
25178
+ const event = { time, price, paneKind: inData && pane ? pane.kind : null, values, ohlc };
24176
25179
  for (const cb of this.crosshairCbs) cb(event);
24177
25180
  }
24178
25181
  handleClick(x) {
@@ -24191,6 +25194,20 @@ var NativeRenderer = class {
24191
25194
  }
24192
25195
  return null;
24193
25196
  }
25197
+ /** The pane whose bounds sit closest to `y` — the forgiving fallback for points that
25198
+ * fall BETWEEN bounds (separator gaps, the strip under the last pane). */
25199
+ nearestPaneToY(y) {
25200
+ let best = null;
25201
+ let bestDist = Infinity;
25202
+ for (const pane of this.scene.panes.values()) {
25203
+ const d = y < pane.bounds.top ? pane.bounds.top - y : y - (pane.bounds.top + pane.bounds.height);
25204
+ if (d < bestDist) {
25205
+ bestDist = d;
25206
+ best = pane;
25207
+ }
25208
+ }
25209
+ return best;
25210
+ }
24194
25211
  // ── manual vertical scaling (price-axis drag + vertical price pan) ──
24195
25212
  /** The scale window a gesture targets: a pane's master scale, or a merged indicator's
24196
25213
  * own scale column (resolved from the grabbed x). Both share the four scale fields. */
@@ -24253,7 +25270,7 @@ var NativeRenderer = class {
24253
25270
  * still fits the view.)
24254
25271
  */
24255
25272
  dataDblClick(_x, y) {
24256
- const pane = this.paneNodeAtY(y);
25273
+ const pane = this.paneNodeAtY(y) ?? this.nearestPaneToY(y);
24257
25274
  if (!pane) return;
24258
25275
  const maximized = this.maximizedPaneId !== pane.id;
24259
25276
  this.maximizedPaneId = maximized ? pane.id : null;
@@ -24494,29 +25511,63 @@ var NativeRenderer = class {
24494
25511
  dataWindowGroups(idx, pricePane) {
24495
25512
  const groups = [];
24496
25513
  for (const model of this.scene.indicators.values()) {
24497
- const pane = (model.paneId ? this.scene.panes.get(model.paneId) : null) ?? pricePane;
24498
- const off = this.scene.offsetOf(model.id);
24499
- const rows = [];
24500
- for (const s of model.series) {
24501
- let value;
24502
- let color;
24503
- if (s.kind === "candle" || s.kind === "bar") {
24504
- value = s.bars[idx - off]?.close;
24505
- color = s.style?.up ?? this.theme.upColor;
24506
- } else if (isLineLikeSeries(s)) {
24507
- if (s.visible === false) continue;
24508
- value = s.points[idx - off]?.value;
24509
- color = s.points[idx - off]?.color ?? s.style.color;
24510
- } else {
24511
- continue;
24512
- }
24513
- if (value == null || !Number.isFinite(value)) continue;
24514
- rows.push({ label: s.title || model.title, value: this.dataWindowFmt(value, pane), color });
24515
- }
25514
+ const rows = this.dataWindowRowsFor(model, idx, pricePane);
24516
25515
  if (rows.length) groups.push({ name: model.title, rows });
24517
25516
  }
24518
25517
  return groups;
24519
25518
  }
25519
+ /** One indicator's readout at bar `idx`: a row per drawable plot, formatted on its pane's scale. */
25520
+ dataWindowRowsFor(model, idx, pricePane) {
25521
+ if (model.native?.type === "volume") return this.volumeReadoutRows(model, idx);
25522
+ const pane = (model.paneId ? this.scene.panes.get(model.paneId) : null) ?? pricePane;
25523
+ const off = this.scene.offsetOf(model.id);
25524
+ const rows = [];
25525
+ for (const s of model.series) {
25526
+ let value;
25527
+ let color;
25528
+ if (s.kind === "candle" || s.kind === "bar") {
25529
+ value = s.bars[idx - off]?.close;
25530
+ color = s.style?.up ?? this.theme.upColor;
25531
+ } else if (isLineLikeSeries(s)) {
25532
+ if (s.visible === false) continue;
25533
+ value = s.points[idx - off]?.value;
25534
+ color = s.points[idx - off]?.color ?? s.style.color;
25535
+ } else {
25536
+ continue;
25537
+ }
25538
+ if (value == null || !Number.isFinite(value)) continue;
25539
+ rows.push({ label: s.title || model.title, value: this.dataWindowFmt(value, pane), color });
25540
+ }
25541
+ return rows;
25542
+ }
25543
+ /** The volume indicator's readout: the bar's volume, tinted with the layer's own direction
25544
+ * colors. Hiding volume keeps its model in the scene (only the layer is suppressed — see
25545
+ * `setIndicatorVisible`), so the hidden flag is checked here rather than by model absence. */
25546
+ volumeReadoutRows(model, idx) {
25547
+ if (this.volumeHidden) return [];
25548
+ const bar = this.bars[idx];
25549
+ const vol = bar?.volume;
25550
+ if (bar == null || vol == null || !Number.isFinite(vol)) return [];
25551
+ const cfg = this.scene.volumeLayer;
25552
+ const color = bar.close >= bar.open ? cfg?.upColor ?? this.theme.upColor : cfg?.downColor ?? this.theme.downColor;
25553
+ return [{ label: model.title, value: formatVolume(vol), color }];
25554
+ }
25555
+ /** Refresh the plot values beside every legend title — the same readout the data window
25556
+ * shows (crosshair bar, else the latest bar), pushed per paint. A hidden indicator has
25557
+ * no scene model, so its row is absent from the map and its readout clears. */
25558
+ updateLegendValues() {
25559
+ if (!this.inputsUI) return;
25560
+ const n = this.bars.length;
25561
+ const values = /* @__PURE__ */ new Map();
25562
+ if (n > 0) {
25563
+ const idx = this.hoverLogical != null ? this.hoverLogical : n - 1;
25564
+ const pricePane = this.dataWindowPricePane();
25565
+ for (const model of this.scene.indicators.values()) {
25566
+ values.set(model.id, this.dataWindowRowsFor(model, idx, pricePane).map((r) => ({ value: r.value, color: r.color })));
25567
+ }
25568
+ }
25569
+ this.inputsUI.setPlotValues(values);
25570
+ }
24520
25571
  renderFrame(level) {
24521
25572
  if (this.animator.active) return;
24522
25573
  if (repaintsData(level)) {
@@ -24528,6 +25579,7 @@ var NativeRenderer = class {
24528
25579
  }
24529
25580
  this.crosshairLayer.render(this.scene, this.coords, this.theme, this.hoverSeparatorY, this.externalCrossPx());
24530
25581
  if (!repaintsData(level) && this.paintedData) this.repaintCursorLayers();
25582
+ this.updateLegendValues();
24531
25583
  }
24532
25584
  /** Repaint the SDK layers that opted into cursor tracking (their own canvas only). */
24533
25585
  repaintCursorLayers() {
@@ -24586,19 +25638,18 @@ var NativeRenderer = class {
24586
25638
  theme: this.theme
24587
25639
  });
24588
25640
  const nowMs = typeof performance !== "undefined" ? performance.now() : Date.now();
25641
+ let folded = null;
24589
25642
  for (const l of this.extLayers) {
24590
25643
  const args = this.extLayerArgs(l.def.id, pane.scale, pane.bounds, nowMs);
24591
25644
  l.instance.render(args);
24592
- if (l.def.id === this.scene.priceStyle && l.instance.modulateBase) {
24593
- const mod = l.instance.modulateBase(args);
24594
- if (mod) {
24595
- if (mod.candleBodyScale != null) this.backend.candleBodyScale = clamp012(mod.candleBodyScale) || 0.01;
24596
- if (mod.candleBodyAlpha != null) this.backend.candleBodyAlpha = clamp012(mod.candleBodyAlpha) * this.candleBodyAlpha;
24597
- if (mod.gridAlpha != null) this.backend.gridAlpha = clamp012(mod.gridAlpha);
24598
- }
24599
- }
25645
+ folded = foldBaseModulation(folded, l.instance.modulateBase?.(args) ?? null);
24600
25646
  if (this.animZoom && l.instance.animating?.()) this.animator.start();
24601
25647
  }
25648
+ if (folded) {
25649
+ if (folded.candleBodyScale != null) this.backend.candleBodyScale = clamp012(folded.candleBodyScale) || 0.01;
25650
+ if (folded.candleBodyAlpha != null) this.backend.candleBodyAlpha = clamp012(folded.candleBodyAlpha) * this.candleBodyAlpha;
25651
+ if (folded.gridAlpha != null) this.backend.gridAlpha = clamp012(folded.gridAlpha);
25652
+ }
24602
25653
  }
24603
25654
  const li = this.bars.length - 1;
24604
25655
  const liveActual = li >= 0 ? this.bars[li] : void 0;
@@ -24676,7 +25727,7 @@ var NativeRenderer = class {
24676
25727
  if (!Number.isFinite(y) || y < pricePane.bounds.top || y > pricePane.bounds.top + pricePane.bounds.height) y = null;
24677
25728
  }
24678
25729
  }
24679
- return { x, y, time: this.coords.logicalToTime(logical) };
25730
+ return { x, y, time: this.coords.logicalToTime(logical), price: y != null ? ext.price : null };
24680
25731
  }
24681
25732
  /** Set the sticky magnet mode (called by the drawings toolbar's 3-state button). */
24682
25733
  setSnapMode(mode) {
@@ -24990,6 +26041,7 @@ var NativeRenderer = class {
24990
26041
  this.paneControls?.reposition();
24991
26042
  this.repositionScrollButton();
24992
26043
  this.positionAttribution();
26044
+ this.publishPricePaneBounds();
24993
26045
  return;
24994
26046
  }
24995
26047
  const collapsed = panes.filter((p) => p.collapsed);
@@ -25007,9 +26059,10 @@ var NativeRenderer = class {
25007
26059
  this.paneControls?.reposition();
25008
26060
  this.repositionScrollButton();
25009
26061
  this.positionAttribution();
26062
+ this.publishPricePaneBounds();
25010
26063
  }
25011
- /** Pin the scroll-to-realtime button above the bottom-most EXPANDED pane's data area (like the
25012
- * watermark): with all lower sub-panes collapsed it settles into the lowest open pane. */
26064
+ /** Pin the scroll-to-realtime button above the bottom-most EXPANDED pane's data area:
26065
+ * with all lower sub-panes collapsed it settles into the lowest open pane. */
25013
26066
  repositionScrollButton() {
25014
26067
  const dataHeight = this.coords.height;
25015
26068
  if (dataHeight <= 0) return;
@@ -25053,6 +26106,21 @@ var NativeRenderer = class {
25053
26106
  this.mountContainer?.style.setProperty("--vela-toolbar-gutter", `${this.toolbarGutter}px`);
25054
26107
  this.mountContainer?.style.setProperty("--vela-scale-gutter", `${this.rightAxisW}px`);
25055
26108
  }
26109
+ /** Publish the price pane's vertical insets as `--vela-price-pane-top` /
26110
+ * `--vela-price-pane-bottom` on the mount container, so the symbol watermark
26111
+ * (and any other host overlay) can clip to the price pane instead of spanning
26112
+ * study panes and the time axis. A maximized study pane collapses the box to
26113
+ * zero height — the mark does not appear on a study. */
26114
+ publishPricePaneBounds() {
26115
+ if (!this.mountContainer) return;
26116
+ const plotH = this.coords.height + TIME_AXIS_H;
26117
+ const price = this.scene.panes.get(PRICE_PANE_ID);
26118
+ const top = price ? price.bounds.top : 0;
26119
+ const height = price ? price.bounds.height : this.coords.height;
26120
+ const bottom = Math.max(0, plotH - (top + height));
26121
+ this.mountContainer.style.setProperty("--vela-price-pane-top", `${top}px`);
26122
+ this.mountContainer.style.setProperty("--vela-price-pane-bottom", `${bottom}px`);
26123
+ }
25056
26124
  /** The built-in mark, or the host's own when one is set. */
25057
26125
  buildAttributionEl() {
25058
26126
  return this.attributionHtml !== null ? createCustomMark(document, this.attributionHtml, this.theme.background) : createAttributionMark(document, this.theme.background);
@@ -25368,7 +26436,8 @@ var VpvrIndicator = class {
25368
26436
  };
25369
26437
  var vpvrDescriptor = {
25370
26438
  type: "vpvr",
25371
- title: "VPVR",
26439
+ title: "Visible Range Volume Profile",
26440
+ shortTitle: "VRVP",
25372
26441
  paneHint: "price",
25373
26442
  overlay: true,
25374
26443
  inputsSchema: () => [
@@ -25866,6 +26935,7 @@ exports.iconMarkup = iconMarkup;
25866
26935
  exports.legendActions = legendActions;
25867
26936
  exports.nativeIndicatorDescriptors = nativeIndicatorDescriptors;
25868
26937
  exports.nativeIndicatorTypes = nativeIndicatorTypes;
26938
+ exports.normalizeSettingsRow = normalizeSettingsRow;
25869
26939
  exports.registerChartType = registerChartType;
25870
26940
  exports.registerDefaultEngine = registerDefaultEngine;
25871
26941
  exports.registerDrawingType = registerDrawingType;
@@ -25881,6 +26951,7 @@ exports.rendererDefaults = rendererDefaults;
25881
26951
  exports.rendererLayers = rendererLayers;
25882
26952
  exports.resolveEngines = resolveEngines;
25883
26953
  exports.resolveTheme = resolveTheme;
26954
+ exports.settingsRowValueKeys = settingsRowValueKeys;
25884
26955
  exports.sharedBarStore = sharedBarStore;
25885
26956
  exports.sidePanels = sidePanels;
25886
26957
  exports.stableSeriesId = stableSeriesId;