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