@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
@@ -1,6 +1,6 @@
1
- import { deserializeDrawing, chartTypes, rendererLayers, registerChartType, rendererDefaults, getDrawingType, settingsRowVisible, TextLabel, Callout, resetDrawingSettings, registerNativeIndicator, chartType, getNativeIndicator, nativeIndicatorDescriptors, drawingTypes, formatDuration, RegressionChannel, FixedRangeVolumeProfile, SegmentDrawing, ArrowMark, GlyphStamp, RadialFib, FibSpiral, DedekindTessellation, MachFigure, GannSquare, FibRatios, MeasureBox, PositionTool, AnchoredVwap, PatternDrawing, Comment, PriceNote, Signpost, Note, PriceLabel, GANN_SQUARE_ARCS, lineSegmentIntersection, DEFAULT_DRAWING_COLOR, GLYPH_OPTIONS, STAMP_SIZE_OPTIONS, LINE_STYLE_OPTIONS, DEDEKIND_CURVATURE_OPTIONS, MACH_NUMBER_OPTIONS, MACH_WAVE_COUNT_OPTIONS, TEXT_SIZE_OPTIONS, createDrawing, tickerModifierIds, CalloutBase, DIRECTION_OPTIONS } from './chunk-S6F6SQ2L.js';
2
- import { themeTokens, STATIC_TOKENS } from './chunk-ULIX6AE2.js';
3
- import { icon, iconAt, withAlpha, NEUTRAL, BEARISH, BULLISH, isDarkColor, toHex6, ACCENT, SERIES_LINE, SLATE, CROSSHAIR, TRADE_EXIT, TRADE_SHORT, TRADE_LONG, SLATE_DEEP, VALID, INVALID, svg24, WARNING, svg24Solid } from './chunk-EP6NDJYK.js';
1
+ import { deserializeDrawing, chartTypes, rendererLayers, foldBaseModulation, registerChartType, rendererDefaults, getDrawingType, settingsRowVisible, normalizeSettingsRow, TextLabel, Callout, resetDrawingSettings, registerNativeIndicator, chartType, getNativeIndicator, nativeIndicatorDescriptors, drawingTypes, settingsRowValueKeys, formatDuration, RegressionChannel, FixedRangeVolumeProfile, SegmentDrawing, ArrowMark, GlyphStamp, RadialFib, FibSpiral, DedekindTessellation, MachFigure, GannSquare, FibRatios, MeasureBox, PositionTool, AnchoredVwap, PatternDrawing, Comment, PriceNote, Signpost, Note, PriceLabel, GANN_SQUARE_ARCS, lineSegmentIntersection, DEFAULT_DRAWING_COLOR, GLYPH_OPTIONS, STAMP_SIZE_OPTIONS, LINE_STYLE_OPTIONS, DEDEKIND_CURVATURE_OPTIONS, MACH_NUMBER_OPTIONS, MACH_WAVE_COUNT_OPTIONS, TEXT_SIZE_OPTIONS, createDrawing, tickerModifierIds, CalloutBase, DIRECTION_OPTIONS } from './chunk-WJKLBYRM.js';
2
+ import { themeTokens, STATIC_TOKENS } from './chunk-ATPU5CI6.js';
3
+ import { icon, iconAt, withAlpha, NEUTRAL, BEARISH, BULLISH, isDarkColor, toHex6, ACCENT, SERIES_LINE, SLATE, CROSSHAIR, TRADE_EXIT, TRADE_SHORT, TRADE_LONG, SLATE_DEEP, VALID, INVALID, svg24, WARNING, svg24Solid } from './chunk-LJLZR44Y.js';
4
4
 
5
5
  // src/core/events/EventBus.ts
6
6
  var TypedEventBus = class {
@@ -428,6 +428,28 @@ var RendererControl = class {
428
428
  setLegendActions(provider) {
429
429
  this.renderer.setLegendActions?.(provider);
430
430
  }
431
+ /**
432
+ * Replace the indicator legend's fold toggle with a host action (or restore it with
433
+ * `null`) — multi-chart shells point the chip at their indicator overview instead of
434
+ * unfolding rows in place. Silent no-op on a renderer without a foldable legend.
435
+ */
436
+ setLegendOverviewAction(action) {
437
+ this.renderer.setLegendOverviewAction?.(action);
438
+ return this;
439
+ }
440
+ /** Whether the active renderer can open a per-indicator settings dialog. */
441
+ get supportsIndicatorSettings() {
442
+ return typeof this.renderer.openIndicatorSettings === "function";
443
+ }
444
+ /**
445
+ * Open one indicator's settings dialog — the programmatic twin of the legend gear
446
+ * (see {@link supportsIndicatorSettings}). Silent no-op without the seam or for an
447
+ * unknown id.
448
+ */
449
+ openIndicatorSettings(indicatorId) {
450
+ this.renderer.openIndicatorSettings?.(indicatorId);
451
+ return this;
452
+ }
431
453
  /**
432
454
  * Export the current chart as a PNG data URL, or null if the active renderer
433
455
  * doesn't support it (warns). DOM overlays (tables, legend) are not included.
@@ -474,6 +496,10 @@ var RendererControl = class {
474
496
  onCrosshairMove(cb) {
475
497
  return this.renderer.onCrosshairMove(cb);
476
498
  }
499
+ /** Touch long-press on a price or time axis strip — silent no-op without the seam. */
500
+ onAxisLongPress(cb) {
501
+ return this.renderer.onAxisLongPress?.(cb) ?? (() => void 0);
502
+ }
477
503
  /**
478
504
  * The current data-window readout — the hovered bar's date/time and OHLCV plus every
479
505
  * indicator's value there, each already formatted on its pane's scale (the latest bar when
@@ -539,6 +565,13 @@ var RendererControl = class {
539
565
  this.renderer.setSettingsSections?.(sections);
540
566
  return this;
541
567
  }
568
+ /** Tell the renderer's own chrome which size class the host shell is in —
569
+ * `'mobile'` switches its dialogs/toolbars to the touch-first presentation.
570
+ * Silent no-op on a renderer without adaptive chrome. */
571
+ setLayoutMode(mode) {
572
+ this.renderer.setLayoutMode?.(mode);
573
+ return this;
574
+ }
542
575
  };
543
576
 
544
577
  // src/data/BarStore.ts
@@ -1518,13 +1551,9 @@ function tzMenuLabel(zone, location) {
1518
1551
  if (normalizeTimezone(zone) === "Etc/UTC") return location;
1519
1552
  return `(${tzOffset(zone)}) ${location}`;
1520
1553
  }
1521
- function tzLocation(zone) {
1522
- const entry = TIMEZONES.find((t) => t.value === normalizeTimezone(zone));
1523
- return entry?.label ?? (zone.split("/").pop() ?? zone).replace(/_/g, " ");
1524
- }
1525
1554
  function tzButtonLabel(zone) {
1526
1555
  if (normalizeTimezone(zone) === "Etc/UTC") return "UTC";
1527
- return `${tzOffset(zone)} ${tzLocation(zone)}`;
1556
+ return tzOffset(zone);
1528
1557
  }
1529
1558
 
1530
1559
  // src/core/drawings/effectiveColor.ts
@@ -2089,7 +2118,7 @@ function attachChromeTooltip(anchor, opts) {
2089
2118
  const doc = anchor.ownerDocument;
2090
2119
  tip = doc.createElement("div");
2091
2120
  tip.textContent = text;
2092
- 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;");
2121
+ 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;");
2093
2122
  applyChromeTokens(tip, opts.theme());
2094
2123
  opts.host.appendChild(tip);
2095
2124
  const a = anchor.getBoundingClientRect();
@@ -2103,16 +2132,17 @@ function attachChromeTooltip(anchor, opts) {
2103
2132
  tip.style.top = `${a.bottom - h.top + 6}px`;
2104
2133
  }
2105
2134
  };
2106
- const arm = () => {
2135
+ const arm = (e) => {
2136
+ if (e.pointerType !== "mouse") return;
2107
2137
  clear();
2108
2138
  timer = window.setTimeout(show, opts.delayMs ?? 700);
2109
2139
  };
2110
- anchor.addEventListener("mouseenter", arm);
2111
- anchor.addEventListener("mouseleave", clear);
2140
+ anchor.addEventListener("pointerenter", arm);
2141
+ anchor.addEventListener("pointerleave", clear);
2112
2142
  anchor.addEventListener("pointerdown", clear);
2113
2143
  return () => {
2114
- anchor.removeEventListener("mouseenter", arm);
2115
- anchor.removeEventListener("mouseleave", clear);
2144
+ anchor.removeEventListener("pointerenter", arm);
2145
+ anchor.removeEventListener("pointerleave", clear);
2116
2146
  anchor.removeEventListener("pointerdown", clear);
2117
2147
  clear();
2118
2148
  };
@@ -2808,14 +2838,22 @@ function makeDialogDraggable(box, header, options) {
2808
2838
 
2809
2839
  // src/renderers/shared/InputsUI.ts
2810
2840
  var SOURCES = ["close", "open", "high", "low", "hl2", "hlc3", "ohlc4", "volume"];
2811
- 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)}}";
2812
- var LEGEND_ICON_PX = 13;
2841
+ var STATUS_KEYFRAMES = "@keyframes vela-ind-pulse{0%,100%{opacity:1;transform:scale(1)}50%{opacity:.3;transform:scale(.6)}}";
2842
+ var LEGEND_ICON_PX = 16;
2843
+ var LEGEND_CTL_PX = 18;
2844
+ var LEGEND_ROW_PAD_Y = 2;
2845
+ var LEGEND_ROW_PAD_X = 6;
2846
+ var LEGEND_TITLE_VALUES_GAP_PX = 6;
2847
+ var LEGEND_ACTION_GAP_PX = 6;
2848
+ 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;`;
2849
+ var LEGEND_ROW_MIN_H = LEGEND_CTL_PX + LEGEND_ROW_PAD_Y * 2;
2813
2850
  var EYE_SVG = iconAt("eye", LEGEND_ICON_PX);
2814
2851
  var EYE_OFF_SVG = iconAt("eye-off", LEGEND_ICON_PX);
2815
2852
  var GEAR_SVG = iconAt("gear", LEGEND_ICON_PX);
2816
- var CLOSE_SVG = iconAt("close", 11);
2853
+ var CLOSE_SVG = iconAt("close", LEGEND_ICON_PX);
2817
2854
  var FOLD_SVG = iconAt("chevron-up", LEGEND_ICON_PX);
2818
2855
  var UNFOLD_SVG = iconAt("chevron-down", LEGEND_ICON_PX);
2856
+ var OVERVIEW_SVG = iconAt("objects", LEGEND_ICON_PX);
2819
2857
  var InputsUI = class {
2820
2858
  constructor(container, theme, paneBoundsOf) {
2821
2859
  this.container = container;
@@ -2836,6 +2874,8 @@ var InputsUI = class {
2836
2874
  this.onToggleVisible = null;
2837
2875
  /** Host symbol picker for `input.symbol`; when set the control opens the host's ticker UI. */
2838
2876
  this.symbolPicker = null;
2877
+ /** Mobile chrome: the inputs dialog opens fullscreen and the legends render compact. */
2878
+ this.mobileLayout = false;
2839
2879
  /** Pane move/merge hook — when set, rows get a "Move to" menu + become drag-to-pane sources. */
2840
2880
  this.moveApi = null;
2841
2881
  /** Host-contributed legend actions, resolved PER ROW at render time (see setLegendActions). */
@@ -2857,9 +2897,18 @@ var InputsUI = class {
2857
2897
  /** Titles switched off entirely (a settings toggle): every pane's legend container
2858
2898
  * hides — rows, fold chevron and all — unlike the fold, which leaves its chip. */
2859
2899
  this.titlesVisible = true;
2900
+ /** Plot values shown beside the titles chart-wide (a settings toggle); a row's own
2901
+ * context-menu choice ({@link LegendRow.showValues}) overrides it per indicator. */
2902
+ this.valuesVisible = true;
2903
+ /** Open legend-row context menu (kept so it can be torn down). */
2904
+ this.rowMenu = null;
2860
2905
  /** The single fold toggle, on the price-pane legend: a bordered ^ chevron under the
2861
2906
  * rows, or the bordered "˅ N" chip (N counts every pane's indicators) when folded. */
2862
2907
  this.foldToggle = null;
2908
+ /** Host-provided replacement for the fold toggle (multi-chart shells route it to
2909
+ * their indicator overview, e.g. an object tree). While set, the legend stays
2910
+ * folded and the chip runs this instead of unfolding inline. */
2911
+ this.overviewAction = null;
2863
2912
  /** Esc-to-close for the settings modal (bound so focus can sit anywhere). */
2864
2913
  this.onDialogKey = (e) => {
2865
2914
  if (e.key === "Escape") {
@@ -2883,6 +2932,9 @@ var InputsUI = class {
2883
2932
  this.onMoveMenuOutside = (e) => {
2884
2933
  if (this.moveMenu && !this.moveMenu.contains(e.target)) this.closeMoveMenu();
2885
2934
  };
2935
+ this.onRowMenuOutside = (e) => {
2936
+ if (this.rowMenu && !this.rowMenu.contains(e.target)) this.closeRowMenu();
2937
+ };
2886
2938
  if (!container.style.position) container.style.position = "relative";
2887
2939
  if (typeof document !== "undefined") document.addEventListener("click", this.onDocClick);
2888
2940
  }
@@ -2890,6 +2942,36 @@ var InputsUI = class {
2890
2942
  this.dialogHost = host;
2891
2943
  if (host && getComputedStyle(host).position === "static") host.style.position = "relative";
2892
2944
  }
2945
+ /** The host shell's chrome size class — mobile makes the inputs dialog fullscreen
2946
+ * (an open dialog re-presents on the flip) and folds the legend behind its chip by
2947
+ * default (a phone-width plot has no room for the rows). */
2948
+ setLayoutMode(mode) {
2949
+ const mobile = mode === "mobile";
2950
+ if (mobile === this.mobileLayout) return;
2951
+ this.mobileLayout = mobile;
2952
+ if (this.overviewAction === null) this.legendFolded = mobile;
2953
+ this.syncFoldToggle();
2954
+ if (this.openId !== null) {
2955
+ const id = this.openId;
2956
+ this.closeOpenDialog();
2957
+ this.openDialog(id);
2958
+ }
2959
+ }
2960
+ /** Replace the fold toggle's behavior with a HOST action (or restore it with null):
2961
+ * the chip stays — objects icon + indicator count — but a press runs the action
2962
+ * (a multi-chart shell opens its indicator overview) instead of unfolding the rows,
2963
+ * which stay hidden while the override is in force. */
2964
+ setLegendOverviewAction(action) {
2965
+ if (action === this.overviewAction) return;
2966
+ this.overviewAction = action;
2967
+ this.legendFolded = action !== null ? true : this.mobileLayout;
2968
+ this.syncFoldToggle();
2969
+ }
2970
+ /** Open the settings dialog of one indicator (the legend gear's programmatic twin) —
2971
+ * no-op for an unknown id. */
2972
+ openSettingsFor(id) {
2973
+ if (this.rows.has(id)) this.openDialog(id);
2974
+ }
2893
2975
  /** Attach a themed chrome tooltip to a control, recording its disposer under `id`. */
2894
2976
  tip(store, id, anchor, text, wrap = false) {
2895
2977
  const list = store.get(id) ?? [];
@@ -2917,12 +2999,12 @@ var InputsUI = class {
2917
2999
  }
2918
3000
  setTheme(theme) {
2919
3001
  this.theme = theme;
3002
+ for (const lg of this.legends.values()) applyChromeTokens(lg, theme);
2920
3003
  for (const row of this.rows.values()) {
2921
- const open = row.controlsEl.style.display !== "none";
2922
- row.el.style.background = open ? theme.background : this.idleRowFill();
3004
+ row.el.style.background = row.highlighted ? theme.background : this.idleRowFill();
2923
3005
  row.el.style.color = theme.textColor;
2924
3006
  }
2925
- if (this.foldToggle) this.foldToggle.style.background = theme.background;
3007
+ if (this.foldToggle) this.syncFoldToggle();
2926
3008
  }
2927
3009
  /** Provide (or clear) the host symbol picker that `input.symbol` opens on activation. */
2928
3010
  setSymbolPicker(picker) {
@@ -2953,7 +3035,7 @@ var InputsUI = class {
2953
3035
  btn.innerHTML = iconAt(action.icon, LEGEND_ICON_PX);
2954
3036
  btn.className = "vela-ind-ctl";
2955
3037
  btn.dataset.legendAction = action.id;
2956
- btn.style.cssText = "cursor:pointer;display:inline-flex;align-items:center;background:transparent;border:none;line-height:0;padding:0 1px;";
3038
+ btn.style.cssText = LEGEND_CTL_CSS;
2957
3039
  btn.addEventListener("click", (e) => {
2958
3040
  e.stopPropagation();
2959
3041
  action.run();
@@ -2971,6 +3053,54 @@ var InputsUI = class {
2971
3053
  this.titlesVisible = visible;
2972
3054
  this.reposition();
2973
3055
  }
3056
+ /**
3057
+ * Show/hide the plot values chart-wide (the settings dialog's Indicators → Values
3058
+ * toggle). "All" is meant literally: per-row context-menu overrides are cleared, so
3059
+ * every legend follows the new state.
3060
+ */
3061
+ setValuesVisible(visible) {
3062
+ this.valuesVisible = visible;
3063
+ for (const row of this.rows.values()) {
3064
+ row.showValues = null;
3065
+ this.renderRowValues(row);
3066
+ }
3067
+ }
3068
+ /**
3069
+ * Push the current plot values of every indicator at once (the renderer calls this per
3070
+ * paint). Rows absent from the map (e.g. a hidden indicator) show no values. Cheap on
3071
+ * unchanged frames: each row diffs against its last rendered values before touching DOM.
3072
+ */
3073
+ setPlotValues(values) {
3074
+ for (const row of this.rows.values()) {
3075
+ const next = values.get(row.id) ?? [];
3076
+ const key = next.map((v) => `${v.value}|${v.color}`).join("\0");
3077
+ if (key === row.plotValuesKey) continue;
3078
+ row.plotValues = next;
3079
+ row.plotValuesKey = key;
3080
+ this.renderRowValues(row);
3081
+ }
3082
+ }
3083
+ /** Whether a row currently shows its values: its own override, else the chart-wide flag. */
3084
+ rowValuesShown(row) {
3085
+ return row.showValues ?? this.valuesVisible;
3086
+ }
3087
+ /** (Re)build one row's values readout from its stored plot values + visibility. A
3088
+ * highlighted (hovered/selected) row shows only the title + controls — no values. */
3089
+ renderRowValues(row) {
3090
+ if (!this.rowValuesShown(row) || row.plotValues.length === 0 || row.highlighted) {
3091
+ row.valuesEl.style.display = "none";
3092
+ row.valuesEl.replaceChildren();
3093
+ return;
3094
+ }
3095
+ row.valuesEl.style.display = "inline-flex";
3096
+ row.valuesEl.replaceChildren();
3097
+ for (const v of row.plotValues) {
3098
+ const span = document.createElement("span");
3099
+ span.textContent = v.value;
3100
+ span.style.color = v.color;
3101
+ row.valuesEl.appendChild(span);
3102
+ }
3103
+ }
2974
3104
  // ── legend move/merge (menu + drag) ─────────────────────────────────────
2975
3105
  /** Open the "Move to" menu for a row, anchored under its move button. */
2976
3106
  openMoveMenu(id, anchor) {
@@ -3022,6 +3152,48 @@ var InputsUI = class {
3022
3152
  this.moveMenu?.remove();
3023
3153
  this.moveMenu = null;
3024
3154
  }
3155
+ // ── legend row context menu (right-click) ───────────────────────────────
3156
+ /** Open the row's action menu at the pointer. One entry today: the values toggle. */
3157
+ openRowMenu(id, x, y) {
3158
+ this.closeRowMenu();
3159
+ this.closeMoveMenu();
3160
+ const row = this.rows.get(id);
3161
+ if (!row) return;
3162
+ const menu = document.createElement("div");
3163
+ 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);`;
3164
+ applyChromeTokens(menu, this.theme);
3165
+ const item = document.createElement("button");
3166
+ item.type = "button";
3167
+ item.className = "vela-ind-menuitem";
3168
+ 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;";
3169
+ const check = document.createElement("span");
3170
+ check.style.cssText = "display:inline-flex;align-items:center;justify-content:center;width:14px;flex:none;line-height:0;";
3171
+ check.innerHTML = this.rowValuesShown(row) ? CHECK_SVG : "";
3172
+ const label = document.createElement("span");
3173
+ label.textContent = "Indicator values";
3174
+ item.append(check, label);
3175
+ item.addEventListener("click", (e) => {
3176
+ e.stopPropagation();
3177
+ this.closeRowMenu();
3178
+ row.showValues = !this.rowValuesShown(row);
3179
+ this.renderRowValues(row);
3180
+ });
3181
+ menu.appendChild(item);
3182
+ document.body.appendChild(menu);
3183
+ const mw = menu.offsetWidth || 180;
3184
+ const mh = menu.offsetHeight || 40;
3185
+ menu.style.left = `${Math.min(x, window.innerWidth - mw - 6)}px`;
3186
+ menu.style.top = `${Math.min(y, window.innerHeight - mh - 6)}px`;
3187
+ this.rowMenu = menu;
3188
+ setTimeout(() => {
3189
+ if (typeof document !== "undefined") document.addEventListener("pointerdown", this.onRowMenuOutside, true);
3190
+ }, 0);
3191
+ }
3192
+ closeRowMenu() {
3193
+ if (typeof document !== "undefined") document.removeEventListener("pointerdown", this.onRowMenuOutside, true);
3194
+ this.rowMenu?.remove();
3195
+ this.rowMenu = null;
3196
+ }
3025
3197
  /** Move a legend row to another pane (indicator merged/moved), tidying an emptied container. */
3026
3198
  setPane(id, paneId) {
3027
3199
  const row = this.rows.get(id);
@@ -3043,7 +3215,8 @@ var InputsUI = class {
3043
3215
  if (!lg) {
3044
3216
  lg = document.createElement("div");
3045
3217
  lg.dataset.velaPane = paneId;
3046
- 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;";
3218
+ 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;";
3219
+ applyChromeTokens(lg, this.theme);
3047
3220
  this.positionLegend(lg, paneId);
3048
3221
  this.container.appendChild(lg);
3049
3222
  this.legends.set(paneId, lg);
@@ -3079,15 +3252,16 @@ var InputsUI = class {
3079
3252
  */
3080
3253
  syncFoldToggle() {
3081
3254
  const count = this.rows.size;
3082
- if (count < 2) {
3255
+ const minRows = this.overviewAction !== null || this.mobileLayout ? 1 : 2;
3256
+ if (count < minRows) {
3083
3257
  const wasFolded = this.legendFolded;
3084
- this.legendFolded = false;
3258
+ if (count > 0) this.legendFolded = false;
3085
3259
  if (this.foldToggle) {
3086
3260
  this.foldToggle.remove();
3087
3261
  this.foldToggle = null;
3088
3262
  this.disposeTips(this.rowTips, "fold");
3089
3263
  }
3090
- if (wasFolded) this.reposition();
3264
+ if (wasFolded && count > 0) this.reposition();
3091
3265
  return;
3092
3266
  }
3093
3267
  let btn = this.foldToggle;
@@ -3095,25 +3269,30 @@ var InputsUI = class {
3095
3269
  btn = document.createElement("button");
3096
3270
  btn.type = "button";
3097
3271
  btn.className = "vela-ind-fold";
3098
- this.tip(this.rowTips, "fold", btn, () => this.legendFolded ? "Show indicator legend" : "Hide indicator legend");
3272
+ this.tip(this.rowTips, "fold", btn, () => this.overviewAction !== null ? "Indicators" : this.legendFolded ? "Show indicator legend" : "Hide indicator legend");
3099
3273
  btn.addEventListener("click", () => {
3274
+ if (this.overviewAction !== null) {
3275
+ this.overviewAction();
3276
+ return;
3277
+ }
3100
3278
  this.legendFolded = !this.legendFolded;
3101
3279
  this.syncFoldToggle();
3102
3280
  });
3103
3281
  this.foldToggle = btn;
3104
3282
  }
3105
3283
  const folded = this.legendFolded;
3106
- btn.setAttribute("aria-label", folded ? "Show indicator legend" : "Hide indicator legend");
3107
- 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;`;
3284
+ btn.setAttribute("aria-label", this.overviewAction !== null ? "Indicators" : folded ? "Show indicator legend" : "Hide indicator legend");
3285
+ const ink = this.theme.textColor;
3286
+ 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;`;
3108
3287
  btn.replaceChildren();
3109
3288
  const icon2 = document.createElement("span");
3110
- icon2.style.cssText = "display:inline-flex;align-items:center;line-height:0;";
3111
- icon2.innerHTML = folded ? UNFOLD_SVG : FOLD_SVG;
3289
+ icon2.style.cssText = "display:inline-flex;align-items:center;line-height:0;opacity:0.7;";
3290
+ icon2.innerHTML = this.overviewAction !== null ? OVERVIEW_SVG : folded ? UNFOLD_SVG : FOLD_SVG;
3112
3291
  btn.appendChild(icon2);
3113
3292
  if (folded) {
3114
3293
  const label = document.createElement("span");
3115
3294
  label.textContent = String(count);
3116
- label.style.cssText = "font-weight:600;line-height:normal;font-size:12px;color:var(--vela-fg-bright);";
3295
+ label.style.cssText = `font-weight:600;line-height:normal;font-size:12px;color:${ink};`;
3117
3296
  btn.appendChild(label);
3118
3297
  }
3119
3298
  this.legendFor("price").appendChild(btn);
@@ -3121,9 +3300,11 @@ var InputsUI = class {
3121
3300
  }
3122
3301
  /** Create or update an indicator's legend row (in the legend for its pane). */
3123
3302
  upsert(id, title, inputs, values, paneId = "price", opts = {}) {
3303
+ const settingsTitle = opts.settingsTitle ?? title;
3124
3304
  const existing = this.rows.get(id);
3125
3305
  if (existing) {
3126
3306
  existing.title = title;
3307
+ existing.settingsTitle = settingsTitle;
3127
3308
  existing.inputs = inputs;
3128
3309
  existing.values = { ...values };
3129
3310
  existing.titleEl.textContent = title;
@@ -3136,8 +3317,7 @@ var InputsUI = class {
3136
3317
  }
3137
3318
  ensureDialogStyles();
3138
3319
  const el = document.createElement("div");
3139
- 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;`;
3140
- el.addEventListener("mouseenter", () => this.setRowHighlighted(id, true));
3320
+ 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;`;
3141
3321
  el.addEventListener("mouseleave", () => {
3142
3322
  if (this.selectedId !== id) this.setRowHighlighted(id, false);
3143
3323
  });
@@ -3157,11 +3337,16 @@ var InputsUI = class {
3157
3337
  e.preventDefault();
3158
3338
  this.onRemove?.(id);
3159
3339
  });
3340
+ el.addEventListener("contextmenu", (e) => {
3341
+ e.preventDefault();
3342
+ e.stopPropagation();
3343
+ this.openRowMenu(id, e.clientX, e.clientY);
3344
+ });
3160
3345
  const statusEl = document.createElement("span");
3161
3346
  statusEl.style.cssText = "display:none;box-sizing:border-box;flex:none;";
3162
- el.appendChild(statusEl);
3163
3347
  const titleWrap = document.createElement("span");
3164
3348
  titleWrap.style.cssText = "white-space:nowrap;";
3349
+ titleWrap.addEventListener("mouseenter", () => this.setRowHighlighted(id, true));
3165
3350
  const titleEl = document.createElement("span");
3166
3351
  titleEl.textContent = title;
3167
3352
  titleEl.style.cssText = "font-weight:600;";
@@ -3173,6 +3358,11 @@ var InputsUI = class {
3173
3358
  titleWrap.appendChild(beta);
3174
3359
  }
3175
3360
  el.appendChild(titleWrap);
3361
+ const valuesEl = document.createElement("span");
3362
+ 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;`;
3363
+ el.appendChild(valuesEl);
3364
+ const controlsEl = document.createElement("span");
3365
+ controlsEl.style.cssText = `display:none;align-items:center;gap:${LEGEND_ACTION_GAP_PX}px;flex:none;margin-left:${LEGEND_ACTION_GAP_PX}px;`;
3176
3366
  let eyeEl = null;
3177
3367
  if (this.onToggleVisible) {
3178
3368
  const eye = document.createElement("button");
@@ -3181,16 +3371,14 @@ var InputsUI = class {
3181
3371
  this.tip(this.rowTips, id, eye, () => this.rows.get(id)?.hidden ? "Show" : "Hide");
3182
3372
  eye.innerHTML = EYE_SVG;
3183
3373
  eye.className = "vela-ind-ctl";
3184
- eye.style.cssText = "cursor:pointer;display:none;align-items:center;background:transparent;border:none;line-height:0;padding:0 1px;";
3374
+ eye.style.cssText = LEGEND_CTL_CSS;
3185
3375
  eye.addEventListener("click", () => {
3186
3376
  const row = this.rows.get(id);
3187
3377
  this.onToggleVisible?.(id, Boolean(row?.hidden));
3188
3378
  });
3189
- el.appendChild(eye);
3379
+ controlsEl.appendChild(eye);
3190
3380
  eyeEl = eye;
3191
3381
  }
3192
- const controlsEl = document.createElement("span");
3193
- controlsEl.style.cssText = "display:none;align-items:center;gap:6px;";
3194
3382
  if (inputs.length > 0) {
3195
3383
  const gear = document.createElement("button");
3196
3384
  gear.type = "button";
@@ -3198,7 +3386,7 @@ var InputsUI = class {
3198
3386
  this.tip(this.rowTips, id, gear, "Settings");
3199
3387
  gear.innerHTML = GEAR_SVG;
3200
3388
  gear.className = "vela-ind-ctl";
3201
- gear.style.cssText = "cursor:pointer;display:inline-flex;align-items:center;background:transparent;border:none;line-height:0;padding:0 1px;";
3389
+ gear.style.cssText = LEGEND_CTL_CSS;
3202
3390
  gear.addEventListener("click", () => this.openDialog(id));
3203
3391
  controlsEl.appendChild(gear);
3204
3392
  }
@@ -3209,7 +3397,7 @@ var InputsUI = class {
3209
3397
  this.tip(this.rowTips, id, mv, "Move to pane");
3210
3398
  mv.innerHTML = iconAt("move", LEGEND_ICON_PX);
3211
3399
  mv.className = "vela-ind-ctl";
3212
- mv.style.cssText = "cursor:pointer;display:inline-flex;align-items:center;background:transparent;border:none;line-height:0;padding:0 1px;";
3400
+ mv.style.cssText = LEGEND_CTL_CSS;
3213
3401
  mv.addEventListener("click", (e) => {
3214
3402
  e.stopPropagation();
3215
3403
  this.openMoveMenu(id, mv);
@@ -3226,12 +3414,13 @@ var InputsUI = class {
3226
3414
  this.tip(this.rowTips, id, close, "Remove indicator");
3227
3415
  close.innerHTML = CLOSE_SVG;
3228
3416
  close.className = "vela-ind-close";
3229
- 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;";
3417
+ close.style.cssText = LEGEND_CTL_CSS;
3230
3418
  close.addEventListener("click", () => this.onRemove?.(id));
3231
3419
  controlsEl.appendChild(close);
3232
3420
  el.appendChild(controlsEl);
3421
+ el.appendChild(statusEl);
3233
3422
  this.attach(this.legendFor(paneId), el, !!opts.native);
3234
- this.rows.set(id, { id, title, inputs, values: { ...values }, el, titleEl, statusEl, paneId, hidden: false, eyeEl, controlsEl, extrasEl, native: !!opts.native });
3423
+ 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 });
3235
3424
  this.syncFoldToggle();
3236
3425
  }
3237
3426
  /** Place a row in its pane's legend — native rows PREPEND (pinned to the top), Pine rows append. */
@@ -3245,24 +3434,31 @@ var InputsUI = class {
3245
3434
  if (row) row.values = { ...row.values, ...values };
3246
3435
  }
3247
3436
  /**
3248
- * Reflect an indicator's live status in its legend row: `'loading'` shows a spinner (a fetch is
3249
- * in flight), `'live'` a pulsing dot (live-updating), `'idle'` nothing. Rendered left of the title.
3437
+ * Reflect an indicator's live status in its legend row: `'loading'` shows three pulsing
3438
+ * dots (a fetch is in flight the same load affordance as the chart's own bar-load dots,
3439
+ * at legend scale), `'live'` a pulsing dot, `'idle'` nothing. Rendered at the row's right end.
3250
3440
  */
3251
3441
  setStatus(id, status) {
3252
3442
  const row = this.rows.get(id);
3253
3443
  if (!row) return;
3254
3444
  const el = row.statusEl;
3445
+ el.replaceChildren();
3255
3446
  if (status === "idle") {
3256
- el.style.display = "none";
3257
- el.style.animation = "none";
3447
+ el.style.cssText = "display:none;box-sizing:border-box;flex:none;";
3258
3448
  return;
3259
3449
  }
3260
- this.ensureStatusKeyframes();
3261
3450
  if (status === "loading") {
3262
- 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;`;
3263
- } else {
3264
- 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;`;
3451
+ el.style.cssText = "display:inline-flex;align-items:center;gap:3px;box-sizing:border-box;flex:none;";
3452
+ for (let i = 0; i < 3; i += 1) {
3453
+ const dot = document.createElement("span");
3454
+ dot.style.cssText = "width:4px;height:4px;border-radius:50%;background:currentColor;opacity:0.15;flex:none;";
3455
+ dot.animate?.([{ opacity: 0.12 }, { opacity: 0.55 }], { duration: 800, iterations: Infinity, direction: "alternate", easing: "ease-in-out", delay: -i * 260 });
3456
+ el.appendChild(dot);
3457
+ }
3458
+ return;
3265
3459
  }
3460
+ this.ensureStatusKeyframes();
3461
+ 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;`;
3266
3462
  }
3267
3463
  /** Inject the status keyframes once (idempotent). */
3268
3464
  ensureStatusKeyframes() {
@@ -3281,8 +3477,8 @@ var InputsUI = class {
3281
3477
  if (row.eyeEl) {
3282
3478
  row.eyeEl.innerHTML = visible ? EYE_SVG : EYE_OFF_SVG;
3283
3479
  row.eyeEl.setAttribute("aria-label", visible ? "Hide" : "Show");
3284
- row.eyeEl.style.display = !visible || row.controlsEl.style.display !== "none" ? "inline-flex" : "none";
3285
3480
  }
3481
+ this.syncRowActions(row);
3286
3482
  }
3287
3483
  /**
3288
3484
  * Select a legend row (outlining it) and clear any previous selection, so at most one
@@ -3295,17 +3491,36 @@ var InputsUI = class {
3295
3491
  this.setRowHighlighted(id, true);
3296
3492
  }
3297
3493
  /**
3298
- * Show or hide a row's outline and eye/gear/✕ controls together — driven by both hover and
3299
- * selection, so an idle (unhovered, unselected) row shows just its title. Exception: a hidden
3300
- * indicator keeps its eye visible even when idle, so it can be un-hidden without hovering.
3494
+ * Show or hide a row's outline and eye/gear/✕ controls together — driven by title
3495
+ * hover and selection (plot values do not open the chip). An idle unselected row
3496
+ * shows just its title (+ values). Exception: a hidden indicator keeps its eye
3497
+ * visible even when idle, so it can be un-hidden without hovering.
3301
3498
  */
3302
3499
  setRowHighlighted(id, highlighted) {
3303
3500
  const row = this.rows.get(id);
3304
3501
  if (!row) return;
3502
+ row.highlighted = highlighted;
3503
+ this.renderRowValues(row);
3305
3504
  row.el.style.boxShadow = highlighted ? `inset 0 0 0 1px ${this.neutralBorder()}` : "none";
3306
3505
  row.el.style.background = highlighted ? this.theme.background : this.idleRowFill();
3307
- row.controlsEl.style.display = highlighted ? "inline-flex" : "none";
3308
- if (row.eyeEl) row.eyeEl.style.display = highlighted || row.hidden ? "inline-flex" : "none";
3506
+ this.syncRowActions(row);
3507
+ }
3508
+ /**
3509
+ * Reveal the action cluster (or just the eye for a hidden indicator). Non-eye
3510
+ * buttons only appear while highlighted; the eye keeps its inline-flex from
3511
+ * construction and is shown/hidden with the cluster container.
3512
+ */
3513
+ syncRowActions(row) {
3514
+ const open = row.highlighted;
3515
+ row.controlsEl.style.display = open || row.hidden ? "inline-flex" : "none";
3516
+ for (const child of Array.from(row.controlsEl.children)) {
3517
+ if (!(child instanceof HTMLElement) || child === row.eyeEl) continue;
3518
+ if (child === row.extrasEl) {
3519
+ child.style.display = open ? "contents" : "none";
3520
+ continue;
3521
+ }
3522
+ child.style.display = open ? "inline-flex" : "none";
3523
+ }
3309
3524
  }
3310
3525
  /** Drop the current selection outline, if any. */
3311
3526
  clearSelection() {
@@ -3333,6 +3548,7 @@ var InputsUI = class {
3333
3548
  destroy() {
3334
3549
  this.closeDialog();
3335
3550
  this.closeMoveMenu();
3551
+ this.closeRowMenu();
3336
3552
  for (const id of [...this.rowTips.keys()]) this.disposeTips(this.rowTips, id);
3337
3553
  for (const id of [...this.extrasTips.keys()]) this.disposeTips(this.extrasTips, id);
3338
3554
  for (const lg of this.legends.values()) lg.remove();
@@ -3388,6 +3604,7 @@ var InputsUI = class {
3388
3604
  const card = document.createElement("div");
3389
3605
  card.className = "vela-ind-dialog";
3390
3606
  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;`;
3607
+ if (this.mobileLayout) card.style.cssText += "width:100%;min-width:0;max-width:none;height:100%;max-height:none;border:none;border-radius:0;";
3391
3608
  applyChromeTokens(card, t);
3392
3609
  card.addEventListener("keydown", (e) => {
3393
3610
  if (e.key !== "Escape") e.stopPropagation();
@@ -3395,7 +3612,7 @@ var InputsUI = class {
3395
3612
  const header = document.createElement("div");
3396
3613
  header.style.cssText = "display:flex;justify-content:space-between;align-items:flex-start;gap:12px;padding:16px 20px 12px;flex:0 0 auto;";
3397
3614
  const hTitle = document.createElement("span");
3398
- hTitle.textContent = row.title;
3615
+ hTitle.textContent = row.settingsTitle;
3399
3616
  hTitle.style.cssText = "font-weight:600;font-size:16px;line-height:1.3;";
3400
3617
  const closeBtn = document.createElement("button");
3401
3618
  closeBtn.type = "button";
@@ -3407,7 +3624,7 @@ var InputsUI = class {
3407
3624
  closeBtn.addEventListener("click", () => this.closeDialog());
3408
3625
  header.append(hTitle, closeBtn);
3409
3626
  card.appendChild(header);
3410
- makeDialogDraggable(card, header, { closeSelector: "button" });
3627
+ if (!this.mobileLayout) makeDialogDraggable(card, header, { closeSelector: "button" });
3411
3628
  const tabs = document.createElement("div");
3412
3629
  tabs.style.cssText = `display:flex;gap:16px;padding:0 20px;border-bottom:1px solid ${border};flex:0 0 auto;`;
3413
3630
  const tab = document.createElement("div");
@@ -3427,7 +3644,7 @@ var InputsUI = class {
3427
3644
  section.appendChild(gh);
3428
3645
  }
3429
3646
  const grid = document.createElement("div");
3430
- grid.style.cssText = "display:grid;grid-template-columns:minmax(140px,auto) auto auto 1fr;align-items:center;column-gap:12px;row-gap:22px;";
3647
+ 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;";
3431
3648
  for (const inputRow of group.rows) this.buildInputRow(grid, inputRow, row);
3432
3649
  section.appendChild(grid);
3433
3650
  body.appendChild(section);
@@ -3876,10 +4093,14 @@ function groupInputs(inputs) {
3876
4093
  var CHECK_SVG = iconAt("check", 12);
3877
4094
  var SELECT_CHEVRON_SVG = iconAt("chevron-down", 16);
3878
4095
  var DIALOG_STYLE_ID = "vela-ind-dialog-styles";
4096
+ var DIALOG_STYLE_REV = "10";
3879
4097
  function ensureDialogStyles() {
3880
- if (typeof document === "undefined" || document.getElementById(DIALOG_STYLE_ID)) return;
3881
- const s = document.createElement("style");
4098
+ if (typeof document === "undefined") return;
4099
+ const existing = document.getElementById(DIALOG_STYLE_ID);
4100
+ if (existing?.dataset.rev === DIALOG_STYLE_REV) return;
4101
+ const s = existing ?? document.createElement("style");
3882
4102
  s.id = DIALOG_STYLE_ID;
4103
+ s.dataset.rev = DIALOG_STYLE_REV;
3883
4104
  s.textContent = `
3884
4105
  .vela-ind-dialog input[type=color]{-webkit-appearance:none;appearance:none;}
3885
4106
  .vela-ind-dialog input[type=color]::-webkit-color-swatch-wrapper{padding:0;}
@@ -3893,19 +4114,19 @@ function ensureDialogStyles() {
3893
4114
  .vela-ind-dialog ::-webkit-scrollbar-track{background:transparent;}
3894
4115
  .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;}
3895
4116
  .vela-ind-hint:hover{background:var(--vela-active);color:var(--vela-fg-bright);}
3896
- .vela-ind-ctl{color:var(--vela-fg-muted);transition:color var(--vela-dur-fast) ease;}
3897
- .vela-ind-ctl:hover{color:var(--vela-fg-bright);}
3898
- .vela-ind-close{transition:color var(--vela-dur-fast) ease;}
3899
- .vela-ind-close:hover{color:var(--vela-danger) !important;}
3900
- .vela-ind-fold{color:var(--vela-fg-muted);transition:color var(--vela-dur-fast) ease,border-color var(--vela-dur-fast) ease;}
3901
- .vela-ind-fold:hover{color:var(--vela-fg-bright);border-color:var(--vela-border-strong);}
4117
+ .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;}
4118
+ .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;}
4119
+ .vela-ind-ctl:hover{background-color:var(--vela-hover-strong);}
4120
+ .vela-ind-close:hover{color:var(--vela-danger) !important;background-color:var(--vela-hover-strong);}
4121
+ .vela-ind-fold{transition:border-color var(--vela-dur-fast) ease,opacity var(--vela-dur-fast) ease;}
4122
+ .vela-ind-fold:hover{border-color:var(--vela-border-strong);opacity:0.9;}
3902
4123
  .vela-ind-menuitem{background:transparent;transition:background var(--vela-dur-fast) ease;}
3903
4124
  .vela-ind-menuitem:hover{background:var(--vela-hover-strong);}
3904
4125
  .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;}
3905
4126
  .vela-ind-btn:hover{background:var(--vela-hover);color:var(--vela-fg-bright);}
3906
4127
  .vela-ind-btn-primary{padding:7px 16px;border-color:var(--vela-selected-bg);background:var(--vela-selected-bg);color:var(--vela-selected-fg);}
3907
4128
  .vela-ind-btn-primary:hover{background:var(--vela-selected-bg);color:var(--vela-selected-fg);opacity:0.85;}`;
3908
- document.head.appendChild(s);
4129
+ if (!existing) document.head.appendChild(s);
3909
4130
  }
3910
4131
  var TIMEFRAME_OPTIONS = [
3911
4132
  { value: "", label: "Chart" },
@@ -4598,14 +4819,7 @@ function factoryResetConfig(factory) {
4598
4819
  const defaults = {};
4599
4820
  const addRows = (rows) => {
4600
4821
  for (const r of rows ?? []) {
4601
- if (r.kind === "heading" || r.kind === "header") continue;
4602
- if (r.kind === "range") {
4603
- defaults[r.minKey] = r.defval;
4604
- defaults[r.maxKey] = r.defval;
4605
- continue;
4606
- }
4607
- defaults[r.key] = r.defval;
4608
- if (r.kind === "toggle") for (const c of r.colors ?? []) defaults[c.key] = c.defval;
4822
+ for (const k of settingsRowValueKeys(r)) defaults[k.key] = k.defval;
4609
4823
  }
4610
4824
  };
4611
4825
  for (const inst of section.instances ?? []) {
@@ -6749,8 +6963,13 @@ function easeToward(current, target, dtMs, tauMs) {
6749
6963
  var FLING_MIN_SPEED = 0.04;
6750
6964
  var FLING_STALE_MS = 60;
6751
6965
  var DRAG_SLOP = 2;
6966
+ var LONG_PRESS_MS = 350;
6967
+ var TOUCH_SLOP = 8;
6968
+ var TOUCH_TAP_SLOP = 14;
6969
+ var DOUBLE_TAP_MS = 350;
6970
+ var DOUBLE_TAP_SLOP = 30;
6752
6971
  var TIME_SCALE_K = 4e-3;
6753
- var WHEEL_ZOOM_K = 25e-4;
6972
+ var WHEEL_ZOOM_K = 4e-3;
6754
6973
  function wheelZoomAnchor(coords, cursorX, rightEdge) {
6755
6974
  if (rightEdge) return { logical: coords.rightEdgeLogical, x: coords.width };
6756
6975
  return { logical: coords.xToLogical(cursorX), x: cursorX };
@@ -6769,6 +6988,12 @@ function wheelPanDelta(deltaX, deltaY, shift) {
6769
6988
  function effectiveSnapMode(momentaryOverride, sticky) {
6770
6989
  return momentaryOverride ? "strong" : sticky;
6771
6990
  }
6991
+ function pinchBarSpacing(startBarSpacing, startDist, dist) {
6992
+ return clampBarSpacing(startBarSpacing * (dist / Math.max(1, startDist)));
6993
+ }
6994
+ function pinchPinnedRightOffset(anchorLogical, barCount, width, x, pxPerBar) {
6995
+ return anchorLogical - (barCount - 1) + (width - x) / pxPerBar;
6996
+ }
6772
6997
  var InputController = class {
6773
6998
  constructor(deps) {
6774
6999
  this.deps = deps;
@@ -6798,13 +7023,40 @@ var InputController = class {
6798
7023
  // smoothed pointer velocity, px/ms
6799
7024
  this.middleDeleted = false;
6800
7025
  // the last middle press deleted a drawing (suppress autoscroll/paste)
7026
+ this.rightCancelled = false;
7027
+ // the last right press cancelled a placement (suppress the context menu)
6801
7028
  // Last cursor position over the element (NaN once it leaves) — lets a modifier
6802
7029
  // press/release re-shape the drawings preview with the pointer stationary.
6803
7030
  this.cursorX = NaN;
6804
7031
  this.cursorY = NaN;
7032
+ // ── touch state ──
7033
+ /** Live touch contacts (local coords) — two entries drive a pinch; extras are ignored. */
7034
+ this.touches = /* @__PURE__ */ new Map();
7035
+ this.lpTimer = null;
7036
+ this.pinchStartDist = 1;
7037
+ this.pinchStartBarSpacing = 0;
7038
+ this.pinchAnchorLogical = 0;
7039
+ // Last clean touch tap (for double-tap pairing). Time 0 = no pending first tap.
7040
+ this.lastTapT = 0;
7041
+ this.lastTapX = 0;
7042
+ this.lastTapY = 0;
7043
+ this.onTouchContextMenu = (e) => {
7044
+ if (this.touches.size === 0) return;
7045
+ if (!(e.target instanceof Node) || !this.el?.contains(e.target)) return;
7046
+ e.preventDefault();
7047
+ e.stopImmediatePropagation();
7048
+ };
6805
7049
  this.onMiddleGuard = (e) => {
6806
7050
  if (e.button === 1 && this.middleDeleted) e.preventDefault();
6807
7051
  };
7052
+ /** A right press that cancelled a placement must not ALSO open the host's chart
7053
+ * context menu — swallow its companion event before it bubbles to the host. */
7054
+ this.onContextGuard = (e) => {
7055
+ if (!this.rightCancelled) return;
7056
+ this.rightCancelled = false;
7057
+ e.preventDefault();
7058
+ e.stopPropagation();
7059
+ };
6808
7060
  /** A modifier press/release with the pointer stationary: re-issue the last cursor
6809
7061
  * position so the placing ghost / drag preview reflects the new state immediately
6810
7062
  * (Shift's 45° angle lock, Ctrl/Cmd's momentary strong magnet). */
@@ -6821,8 +7073,22 @@ var InputController = class {
6821
7073
  if (this.middleDeleted) e.preventDefault();
6822
7074
  return;
6823
7075
  }
7076
+ if (e.button === 2) {
7077
+ this.rightCancelled = this.deps.drawingsCancelPlacement?.() ?? false;
7078
+ if (this.rightCancelled) e.preventDefault();
7079
+ return;
7080
+ }
6824
7081
  if (e.button !== 0) return;
6825
7082
  const { x, y } = this.local(e);
7083
+ if (e.pointerType === "touch") {
7084
+ if (this.touches.size === 1 && this.dragging && (this.region === "data" || this.region === "crosshair")) {
7085
+ this.touches.set(e.pointerId, { x, y });
7086
+ this.beginPinch(e);
7087
+ return;
7088
+ }
7089
+ if (this.touches.size >= 1) return;
7090
+ this.touches.set(e.pointerId, { x, y });
7091
+ }
6826
7092
  this.deps.drawingsClearTransient?.();
6827
7093
  this.dragging = true;
6828
7094
  this.moved = false;
@@ -6848,6 +7114,9 @@ var InputController = class {
6848
7114
  if (this.region === "price") this.deps.beginPriceScale(x, y);
6849
7115
  else if (this.region === "separator") this.deps.beginPaneResize(y);
6850
7116
  else if (this.region === "data") this.verticalPan = this.deps.beginPricePan(y);
7117
+ if (e.pointerType === "touch" && (this.region === "data" || this.region === "price" || this.region === "time")) {
7118
+ this.startLongPress(x, y, this.region);
7119
+ }
6851
7120
  this.lastX = x;
6852
7121
  this.lastT = e.timeStamp;
6853
7122
  this.vx = 0;
@@ -6857,28 +7126,42 @@ var InputController = class {
6857
7126
  const { x, y } = this.local(e);
6858
7127
  this.cursorX = x;
6859
7128
  this.cursorY = y;
7129
+ if (e.pointerType === "touch" && this.touches.has(e.pointerId)) this.touches.set(e.pointerId, { x, y });
7130
+ if (this.dragging && this.region === "pinch") {
7131
+ this.applyPinch();
7132
+ return;
7133
+ }
7134
+ if (this.dragging && this.region === "crosshair") {
7135
+ this.deps.onPointerMove(x, y);
7136
+ return;
7137
+ }
7138
+ if (this.dragging && this.region === "axis-sheet") {
7139
+ return;
7140
+ }
7141
+ if (this.lpTimer !== null && Math.hypot(x - this.startX, y - this.startY) > TOUCH_SLOP) this.cancelLongPress();
7142
+ const slop = e.pointerType === "touch" ? TOUCH_SLOP : DRAG_SLOP;
6860
7143
  if (this.dragging) {
6861
7144
  if (this.region === "price") {
6862
- if (Math.abs(y - this.startY) > DRAG_SLOP) this.moved = true;
7145
+ if (Math.abs(y - this.startY) > slop) this.moved = true;
6863
7146
  this.deps.priceScaleBy(y - this.startY);
6864
7147
  } else if (this.region === "separator") {
6865
- if (Math.abs(y - this.startY) > DRAG_SLOP) this.moved = true;
7148
+ if (Math.abs(y - this.startY) > slop) this.moved = true;
6866
7149
  this.deps.paneResizeBy(y - this.startY);
6867
7150
  } else if (this.region === "time") {
6868
- if (Math.abs(x - this.startX) > DRAG_SLOP) this.moved = true;
7151
+ if (Math.abs(x - this.startX) > slop) this.moved = true;
6869
7152
  const barSpacing = clampBarSpacing(this.startBarSpacing * Math.exp((this.startX - x) * TIME_SCALE_K));
6870
7153
  this.deps.apply({ barSpacing, rightOffset: this.startRightOffset });
6871
7154
  } else if (this.region === "drawing") {
6872
- if (Math.abs(x - this.startX) > DRAG_SLOP || Math.abs(y - this.startY) > DRAG_SLOP) this.moved = true;
7155
+ if (Math.abs(x - this.startX) > slop || Math.abs(y - this.startY) > slop) this.moved = true;
6873
7156
  this.deps.drawingsPointerMove?.(x, y, this.snapMode(e), e.shiftKey);
6874
7157
  } else {
6875
7158
  const coords = this.deps.getCoords();
6876
7159
  const vp = coords.getViewport();
6877
7160
  const dx = x - this.startX;
6878
- if (Math.abs(dx) > DRAG_SLOP) this.moved = true;
7161
+ if (Math.abs(dx) > slop) this.moved = true;
6879
7162
  this.deps.apply({ barSpacing: vp.barSpacing, rightOffset: this.startRightOffset - dx / coords.pxPerBar() });
6880
7163
  if (this.verticalPan) {
6881
- if (Math.abs(y - this.startY) > DRAG_SLOP) this.moved = true;
7164
+ if (Math.abs(y - this.startY) > slop) this.moved = true;
6882
7165
  this.deps.pricePanBy(y - this.startY);
6883
7166
  }
6884
7167
  const dt = e.timeStamp - this.lastT;
@@ -6895,14 +7178,44 @@ var InputController = class {
6895
7178
  this.el.style.cursor = drawCursor ?? (r === "price" ? "ns-resize" : r === "time" ? "ew-resize" : r === "separator" ? "row-resize" : "");
6896
7179
  this.deps.drawingsPointerMove?.(x, y, this.snapMode(e), e.shiftKey);
6897
7180
  }
6898
- this.deps.onPointerMove(x, y);
7181
+ if (e.pointerType !== "touch") this.deps.onPointerMove(x, y);
6899
7182
  };
6900
7183
  this.onUp = (e) => {
7184
+ if (e.pointerType === "touch") this.touches.delete(e.pointerId);
7185
+ this.cancelLongPress();
7186
+ if (this.dragging && this.region === "pinch") {
7187
+ if (this.touches.size === 1) {
7188
+ const rest = [...this.touches.values()][0];
7189
+ const vp = this.deps.getCoords().getViewport();
7190
+ this.region = "data";
7191
+ this.verticalPan = false;
7192
+ this.startX = rest.x;
7193
+ this.startY = rest.y;
7194
+ this.startRightOffset = vp.rightOffset;
7195
+ this.startBarSpacing = vp.barSpacing;
7196
+ this.lastX = rest.x;
7197
+ this.lastT = e.timeStamp;
7198
+ this.vx = 0;
7199
+ return;
7200
+ }
7201
+ this.endGesture(e);
7202
+ return;
7203
+ }
7204
+ if (this.dragging && this.region === "crosshair") {
7205
+ this.deps.onPointerMove(null, null);
7206
+ this.endGesture(e);
7207
+ return;
7208
+ }
7209
+ if (this.dragging && this.region === "axis-sheet") {
7210
+ this.endGesture(e);
7211
+ return;
7212
+ }
7213
+ const { x, y } = this.local(e);
7214
+ const wasTouch = e.pointerType === "touch";
7215
+ const tapRelease = this.dragging && !this.moved && (!wasTouch || Math.hypot(x - this.startX, y - this.startY) <= TOUCH_TAP_SLOP);
6901
7216
  if (this.dragging && this.region === "drawing") {
6902
- const { x, y } = this.local(e);
6903
7217
  this.deps.drawingsPointerUp?.(x, y);
6904
- } else if (this.dragging && !this.moved && this.region === "data") {
6905
- const { x, y } = this.local(e);
7218
+ } else if (tapRelease && this.region === "data") {
6906
7219
  this.deps.onClick(x, y);
6907
7220
  } else if (this.dragging && this.region === "data") {
6908
7221
  const stale = e.timeStamp - this.lastT > FLING_STALE_MS;
@@ -6911,11 +7224,20 @@ var InputController = class {
6911
7224
  this.deps.fling(-this.vx / pitch);
6912
7225
  }
6913
7226
  }
6914
- this.dragging = false;
6915
- try {
6916
- this.el?.releasePointerCapture(e.pointerId);
6917
- } catch {
6918
- }
7227
+ const wasTap = wasTouch && tapRelease;
7228
+ this.endGesture(e);
7229
+ if (wasTouch) this.deps.onPointerMove(null, null);
7230
+ if (wasTap) this.registerTap(e.timeStamp, x, y);
7231
+ };
7232
+ /** The browser took the pointer back (system gesture, palm rejection, tab switch):
7233
+ * drop the gesture where it is — no click, no fling, no stuck `dragging`. */
7234
+ this.onCancel = (e) => {
7235
+ if (e.pointerType === "touch") this.touches.delete(e.pointerId);
7236
+ this.cancelLongPress();
7237
+ if (!this.dragging) return;
7238
+ if (this.region === "drawing" && !Number.isNaN(this.cursorX)) this.deps.drawingsPointerUp?.(this.cursorX, this.cursorY);
7239
+ if (this.region === "crosshair" || e.pointerType === "touch") this.deps.onPointerMove(null, null);
7240
+ this.endGesture(e);
6919
7241
  };
6920
7242
  this.onLeave = () => {
6921
7243
  this.cursorX = NaN;
@@ -6924,12 +7246,7 @@ var InputController = class {
6924
7246
  };
6925
7247
  this.onDblClick = (e) => {
6926
7248
  const { x, y } = this.local(e);
6927
- if (this.deps.drawingsDblClick?.(x, y)) return;
6928
- const region = this.regionAt(x, y);
6929
- if (region === "price") this.deps.resetPriceScale(x, y);
6930
- else if (region === "separator") this.deps.resetPaneSize(y);
6931
- else if (region === "time") this.deps.resetView();
6932
- else this.deps.dataDblClick(x, y);
7249
+ this.doubleActivate(x, y);
6933
7250
  };
6934
7251
  this.onWheel = (e) => {
6935
7252
  e.preventDefault();
@@ -6950,17 +7267,26 @@ var InputController = class {
6950
7267
  }
6951
7268
  attach(el) {
6952
7269
  this.el = el;
7270
+ if (el.style !== void 0) {
7271
+ el.style.touchAction = "none";
7272
+ el.style.userSelect = "none";
7273
+ el.style.setProperty("-webkit-user-select", "none");
7274
+ el.style.setProperty("-webkit-touch-callout", "none");
7275
+ }
6953
7276
  el.addEventListener("pointerdown", this.onDown);
6954
7277
  el.addEventListener("pointermove", this.onMove);
6955
7278
  el.addEventListener("pointerup", this.onUp);
6956
7279
  el.addEventListener("pointerleave", this.onLeave);
7280
+ el.addEventListener("pointercancel", this.onCancel);
6957
7281
  el.addEventListener("dblclick", this.onDblClick);
6958
7282
  el.addEventListener("wheel", this.onWheel, { passive: false });
6959
7283
  el.addEventListener("mousedown", this.onMiddleGuard);
6960
7284
  el.addEventListener("auxclick", this.onMiddleGuard);
7285
+ el.addEventListener("contextmenu", this.onContextGuard);
6961
7286
  const win = el.ownerDocument?.defaultView;
6962
7287
  win?.addEventListener("keydown", this.onModifier);
6963
7288
  win?.addEventListener("keyup", this.onModifier);
7289
+ win?.addEventListener("contextmenu", this.onTouchContextMenu, true);
6964
7290
  }
6965
7291
  detach() {
6966
7292
  const el = this.el;
@@ -6968,14 +7294,19 @@ var InputController = class {
6968
7294
  const win = el.ownerDocument?.defaultView;
6969
7295
  win?.removeEventListener("keydown", this.onModifier);
6970
7296
  win?.removeEventListener("keyup", this.onModifier);
7297
+ win?.removeEventListener("contextmenu", this.onTouchContextMenu, true);
6971
7298
  el.removeEventListener("pointerdown", this.onDown);
6972
7299
  el.removeEventListener("pointermove", this.onMove);
6973
7300
  el.removeEventListener("pointerup", this.onUp);
6974
7301
  el.removeEventListener("pointerleave", this.onLeave);
7302
+ el.removeEventListener("pointercancel", this.onCancel);
6975
7303
  el.removeEventListener("dblclick", this.onDblClick);
6976
7304
  el.removeEventListener("wheel", this.onWheel);
6977
7305
  el.removeEventListener("mousedown", this.onMiddleGuard);
6978
7306
  el.removeEventListener("auxclick", this.onMiddleGuard);
7307
+ el.removeEventListener("contextmenu", this.onContextGuard);
7308
+ this.cancelLongPress();
7309
+ this.touches.clear();
6979
7310
  this.el = null;
6980
7311
  }
6981
7312
  local(e) {
@@ -6999,6 +7330,59 @@ var InputController = class {
6999
7330
  snapMode(e) {
7000
7331
  return effectiveSnapMode(e.ctrlKey || e.metaKey, this.deps.drawingsSnapMode?.() ?? "off");
7001
7332
  }
7333
+ /** Arm the long-press timer for a data-area → crosshair or axis → sheet hold;
7334
+ * movement past TOUCH_SLOP or a second finger cancels it. */
7335
+ startLongPress(x, y, region) {
7336
+ this.cancelLongPress();
7337
+ this.lpTimer = setTimeout(() => {
7338
+ this.lpTimer = null;
7339
+ if (!this.dragging || this.region !== region) return;
7340
+ if (region === "data") {
7341
+ this.region = "crosshair";
7342
+ this.deps.onPointerMove(x, y);
7343
+ return;
7344
+ }
7345
+ this.region = "axis-sheet";
7346
+ this.moved = true;
7347
+ this.deps.onAxisLongPress?.(region, x, y);
7348
+ }, LONG_PRESS_MS);
7349
+ }
7350
+ cancelLongPress() {
7351
+ if (this.lpTimer !== null) clearTimeout(this.lpTimer);
7352
+ this.lpTimer = null;
7353
+ }
7354
+ /** Two fingers down: freeze the view and re-base the gesture as an anchored pinch. */
7355
+ beginPinch(e) {
7356
+ this.cancelLongPress();
7357
+ if (this.region === "crosshair") this.deps.onPointerMove(null, null);
7358
+ const coords = this.deps.getCoords();
7359
+ const vp = coords.getViewport();
7360
+ this.deps.apply(vp);
7361
+ const [a, b] = [...this.touches.values()];
7362
+ this.pinchStartDist = Math.max(1, Math.hypot(a.x - b.x, a.y - b.y));
7363
+ this.pinchStartBarSpacing = vp.barSpacing;
7364
+ this.pinchAnchorLogical = coords.xToLogical((a.x + b.x) / 2);
7365
+ this.region = "pinch";
7366
+ this.dragging = true;
7367
+ this.moved = true;
7368
+ this.vx = 0;
7369
+ this.capture(e.pointerId);
7370
+ }
7371
+ /** Track both fingers: distance ratio zooms, the live midpoint pans (anchor pinned under it). */
7372
+ applyPinch() {
7373
+ const coords = this.deps.getCoords();
7374
+ const pts = [...this.touches.values()];
7375
+ if (pts.length < 2) return;
7376
+ const [a, b] = pts;
7377
+ const barSpacing = pinchBarSpacing(this.pinchStartBarSpacing, this.pinchStartDist, Math.hypot(a.x - b.x, a.y - b.y));
7378
+ const vp = coords.getViewport();
7379
+ const pitchScale = vp.barSpacing > 0 ? coords.pxPerBar() / vp.barSpacing : 1;
7380
+ const midX = (a.x + b.x) / 2;
7381
+ this.deps.apply({
7382
+ barSpacing,
7383
+ rightOffset: pinchPinnedRightOffset(this.pinchAnchorLogical, coords.barCount, coords.width, midX, barSpacing * pitchScale)
7384
+ });
7385
+ }
7002
7386
  /** Capture defensively: a synthetic/already-released pointer can't be captured, and
7003
7387
  * the move/up pair still routes through the element listeners without it. */
7004
7388
  capture(pointerId) {
@@ -7007,6 +7391,35 @@ var InputController = class {
7007
7391
  } catch {
7008
7392
  }
7009
7393
  }
7394
+ /** Pair clean taps into a double-tap — the touch equivalent of dblclick (same
7395
+ * routing: axis taps reset that axis' view, a plot tap toggles pane maximize). */
7396
+ registerTap(t, x, y) {
7397
+ const paired = this.lastTapT > 0 && t - this.lastTapT <= DOUBLE_TAP_MS && Math.hypot(x - this.lastTapX, y - this.lastTapY) <= DOUBLE_TAP_SLOP;
7398
+ if (paired) {
7399
+ this.lastTapT = 0;
7400
+ this.doubleActivate(this.lastTapX, this.lastTapY);
7401
+ return;
7402
+ }
7403
+ this.lastTapT = t;
7404
+ this.lastTapX = x;
7405
+ this.lastTapY = y;
7406
+ }
7407
+ endGesture(e) {
7408
+ this.dragging = false;
7409
+ try {
7410
+ this.el?.releasePointerCapture(e.pointerId);
7411
+ } catch {
7412
+ }
7413
+ }
7414
+ /** Shared double-click / double-tap routing, by the region under the point. */
7415
+ doubleActivate(x, y) {
7416
+ if (this.deps.drawingsDblClick?.(x, y)) return;
7417
+ const region = this.regionAt(x, y);
7418
+ if (region === "price") this.deps.resetPriceScale(x, y);
7419
+ else if (region === "separator") this.deps.resetPaneSize(y);
7420
+ else if (region === "time") this.deps.resetView();
7421
+ else this.deps.dataDblClick(x, y);
7422
+ }
7010
7423
  };
7011
7424
 
7012
7425
  // src/renderers/native/core/KeyboardController.ts
@@ -8564,8 +8977,11 @@ var ChromeRenderer = class {
8564
8977
  }
8565
8978
  ctx.font = `${scene.style.fontSize}px ${theme.fontFamily}`;
8566
8979
  ctx.textBaseline = "middle";
8567
- if (coords.barCount === 0) return;
8568
8980
  const panes = scene.orderedPanes();
8981
+ if (coords.barCount === 0) {
8982
+ this.drawPaneSeparators(ctx, scene, theme, fullW, panes);
8983
+ return;
8984
+ }
8569
8985
  const pricePane = panes.find((p) => p.kind === "price") ?? null;
8570
8986
  for (const pane of panes) {
8571
8987
  if (pane.collapsed) continue;
@@ -8820,11 +9236,19 @@ var ChromeRenderer = class {
8820
9236
  const y = dataH + (fullH - dataH) / 2;
8821
9237
  const tr = coords.visibleTimeRange();
8822
9238
  const offset = tzOffsetMs((tr.from + tr.to) / 2, scene.timezone);
8823
- for (const tick of timeTicks(tr.from, tr.to, 8, offset)) {
8824
- const x = coords.timeToX(tick.time);
8825
- if (x < 20 || x > dataW - 20) continue;
8826
- ctx.fillText(tick.label, x, y);
8827
- }
9239
+ const target = Math.max(3, Math.min(8, Math.floor(dataW / 64)));
9240
+ 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);
9241
+ const GAP = 12;
9242
+ const placed = [];
9243
+ const put = (tick) => {
9244
+ const l = tick.x - tick.half;
9245
+ const r = tick.x + tick.half;
9246
+ if (!placed.every((p) => r + GAP <= p.l || l - GAP >= p.r)) return;
9247
+ placed.push({ l, r });
9248
+ ctx.fillText(tick.label, tick.x, y);
9249
+ };
9250
+ for (const tick of ticks) if (tick.major) put(tick);
9251
+ for (const tick of ticks) if (!tick.major) put(tick);
8828
9252
  ctx.textAlign = "start";
8829
9253
  }
8830
9254
  };
@@ -8926,10 +9350,10 @@ var CrosshairRenderer = class {
8926
9350
  }
8927
9351
  /** The synced ghost: a dimmed vertical line at the bar the renderer resolved as
8928
9352
  * CONTAINING the foreign time (+ horizontal line when a comparable price came
8929
- * along), with that bar's time chip in this chart's own timezone. The snap
8930
- * happened upstream (`externalCrossPx`, floor-to-containing-bar) this method
8931
- * only draws. No price chip the ghost answers "when", the local crosshair
8932
- * answers "where". */
9353
+ * along), with that bar's time chip in this chart's own timezone and — when the
9354
+ * level resolved the price chip on the right axis. The snap happened upstream
9355
+ * (`externalCrossPx`, floor-to-containing-bar)this method only draws. Chips
9356
+ * render slightly dimmed so the ghost still reads as foreign. */
8933
9357
  drawExternal(ctx, ext, scene, coords, theme) {
8934
9358
  const cs = scene.style.crosshair;
8935
9359
  const dataW = coords.width;
@@ -8953,7 +9377,20 @@ var CrosshairRenderer = class {
8953
9377
  ctx.setLineDash([]);
8954
9378
  ctx.lineWidth = 1;
8955
9379
  ctx.globalAlpha = 0.8;
8956
- this.chip(ctx, x, dataH + 1, formatStamp(ext.time, scene.timezone), cs.labelBackground ?? theme.borderColor, "center", true, theme.background);
9380
+ const chipBg = cs.labelBackground ?? theme.borderColor;
9381
+ if (ext.y != null && ext.price != null) {
9382
+ let pane;
9383
+ for (const p of scene.panes.values()) {
9384
+ if (ext.y >= p.bounds.top && ext.y <= p.bounds.top + p.bounds.height) {
9385
+ pane = p;
9386
+ break;
9387
+ }
9388
+ }
9389
+ if (pane) {
9390
+ 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);
9391
+ }
9392
+ }
9393
+ this.chip(ctx, x, dataH + 1, formatStamp(ext.time, scene.timezone), chipBg, "center", true, theme.background);
8957
9394
  ctx.globalAlpha = 1;
8958
9395
  }
8959
9396
  /** A soft band + a brighter crisp center line over the hovered separator, so it reads as
@@ -9224,6 +9661,7 @@ function colorField(theme, getVal, onVal) {
9224
9661
  swatch.style.background = `linear-gradient(${v}, ${v}), ${CHECKER2}`;
9225
9662
  };
9226
9663
  paint();
9664
+ trigger.addEventListener("vela-sync", paint);
9227
9665
  trigger.addEventListener("click", (e) => {
9228
9666
  e.stopPropagation();
9229
9667
  if (openPopover && openPopover.trigger === trigger) {
@@ -9259,6 +9697,101 @@ function closeColorPopover() {
9259
9697
  closeOpen();
9260
9698
  }
9261
9699
 
9700
+ // src/renderers/native/chrome/WidthField.ts
9701
+ var WIDTH_FIELD_OPTIONS = [1, 2, 3, 4, 5];
9702
+ var STYLE_ID4 = "vela-width-field";
9703
+ function ensureStyles4() {
9704
+ if (typeof document === "undefined" || document.getElementById(STYLE_ID4)) return;
9705
+ const st = document.createElement("style");
9706
+ st.id = STYLE_ID4;
9707
+ st.textContent = `
9708
+ .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;}
9709
+ .vela-width-field:hover{border-color:var(--vela-fg-muted);}
9710
+ .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);}
9711
+ .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;}
9712
+ .vela-width-field-item:hover{background:var(--vela-hover-strong);}
9713
+ .vela-width-field-item[data-active='1']{background:var(--vela-active);}
9714
+ `;
9715
+ document.head.appendChild(st);
9716
+ }
9717
+ function lineGlyph(width) {
9718
+ 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>`;
9719
+ }
9720
+ 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>`;
9721
+ var openPopover2 = null;
9722
+ function closeOpen2() {
9723
+ if (!openPopover2) return;
9724
+ document.removeEventListener("pointerdown", openPopover2.onOutside, true);
9725
+ window.removeEventListener("resize", openPopover2.reflow, true);
9726
+ openPopover2.el.remove();
9727
+ openPopover2 = null;
9728
+ }
9729
+ function place2(pop, trigger) {
9730
+ const r = trigger.getBoundingClientRect();
9731
+ const w = pop.offsetWidth;
9732
+ const h = pop.offsetHeight;
9733
+ let left = r.right - w;
9734
+ let top = r.bottom + 6;
9735
+ if (left < 6) left = 6;
9736
+ if (left + w > window.innerWidth - 6) left = window.innerWidth - w - 6;
9737
+ if (top + h > window.innerHeight - 6) top = r.top - h - 6;
9738
+ if (top < 6) top = 6;
9739
+ pop.style.left = `${Math.round(left)}px`;
9740
+ pop.style.top = `${Math.round(top)}px`;
9741
+ }
9742
+ function widthField(theme, getVal, onVal) {
9743
+ ensureStyles4();
9744
+ const trigger = document.createElement("button");
9745
+ trigger.type = "button";
9746
+ trigger.className = "vela-width-field";
9747
+ const paint = () => {
9748
+ trigger.innerHTML = `<span style="display:flex;">${lineGlyph(getVal())}</span><span style="display:flex;">${CHEVRON}</span>`;
9749
+ };
9750
+ paint();
9751
+ trigger.addEventListener("click", (e) => {
9752
+ e.stopPropagation();
9753
+ if (openPopover2 && openPopover2.trigger === trigger) {
9754
+ closeOpen2();
9755
+ return;
9756
+ }
9757
+ closeOpen2();
9758
+ const pop = document.createElement("div");
9759
+ pop.className = "vela-width-field-pop";
9760
+ applyChromeTokens(pop, theme);
9761
+ pop.style.font = `var(--vela-font-size-md) ${theme.fontFamily}`;
9762
+ pop.addEventListener("pointerdown", (ev) => ev.stopPropagation());
9763
+ for (const w of WIDTH_FIELD_OPTIONS) {
9764
+ const item = document.createElement("button");
9765
+ item.type = "button";
9766
+ item.className = "vela-width-field-item";
9767
+ item.dataset.active = w === getVal() ? "1" : "0";
9768
+ item.innerHTML = `<span style="display:flex;flex:none;">${lineGlyph(w)}</span><span style="flex:1;font-variant-numeric:tabular-nums;">${w}px</span>`;
9769
+ item.addEventListener("click", (ev) => {
9770
+ ev.stopPropagation();
9771
+ closeOpen2();
9772
+ onVal(w);
9773
+ paint();
9774
+ });
9775
+ pop.appendChild(item);
9776
+ }
9777
+ document.body.appendChild(pop);
9778
+ place2(pop, trigger);
9779
+ const onOutside = (ev) => {
9780
+ const t = ev.target;
9781
+ if (pop.contains(t) || trigger.contains(t)) return;
9782
+ closeOpen2();
9783
+ };
9784
+ const reflow = () => place2(pop, trigger);
9785
+ setTimeout(() => document.addEventListener("pointerdown", onOutside, true), 0);
9786
+ window.addEventListener("resize", reflow, true);
9787
+ openPopover2 = { el: pop, trigger, onOutside, reflow };
9788
+ });
9789
+ return trigger;
9790
+ }
9791
+ function closeWidthPopover() {
9792
+ closeOpen2();
9793
+ }
9794
+
9262
9795
  // src/renderers/native/chrome/SettingsDialog.ts
9263
9796
  var BUILTIN_STYLE_LABELS = {
9264
9797
  candles: "Candles",
@@ -9341,6 +9874,30 @@ function ensureControlStyles() {
9341
9874
  muted and non-interactive. Applied to each row's children so it survives display:contents;
9342
9875
  !important beats the inline opacity on labels. */
9343
9876
  .vela-sd-soft>*{opacity:0.4 !important;pointer-events:none !important;}
9877
+ /* \u2500\u2500 mobile presentation (.vela-sd-mobile on the scrim; structural sizes are inline in open()) \u2500\u2500
9878
+ The tab rail becomes a burger-opened overlay sidebar; the group TOC becomes a sticky
9879
+ row of horizontally scrollable tabs; the instance strip scrolls instead of wrapping;
9880
+ controls grow to touch size and labels may wrap (the grid gives them a flexible track). */
9881
+ .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;}
9882
+ .vela-sd-mobile .vela-sd-burger:active{background:var(--vela-hover);}
9883
+ .vela-sd-mobile .vela-sd-railscrim{position:absolute;inset:0;z-index:2;background:var(--vela-backdrop);display:none;}
9884
+ .vela-sd-mobile .vela-sd-railscrim.open{display:block;}
9885
+ .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);}
9886
+ .vela-sd-mobile .vela-sd-rail.open{transform:translateX(0);}
9887
+ .vela-sd-mobile .vela-sd-tab{padding:12px;}
9888
+ .vela-sd-mobile .vela-sd-struct{flex-direction:column;padding-top:8px;}
9889
+ .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;}
9890
+ .vela-sd-mobile .vela-sd-toc::-webkit-scrollbar{display:none;}
9891
+ .vela-sd-mobile .vela-sd-toc-btn{flex:none;padding:8px 12px;font-size:13px;}
9892
+ .vela-sd-mobile .vela-sd-struct>[data-sd-rows-host]{border-left:none;padding-left:0;width:100%;}
9893
+ .vela-sd-mobile .vela-sd-itabs{flex-wrap:nowrap;overflow-x:auto;scrollbar-width:none;}
9894
+ .vela-sd-mobile .vela-sd-itabs::-webkit-scrollbar{display:none;}
9895
+ .vela-sd-mobile .vela-sd-itab{height:36px;flex:none;}
9896
+ .vela-sd-mobile .vela-sd-check{width:22px;height:22px;}
9897
+ .vela-sd-mobile .vela-sd-select,.vela-sd-mobile .vela-sd-number{height:34px;}
9898
+ .vela-sd-mobile .vela-sd-close{width:40px;height:40px;}
9899
+ .vela-sd-mobile .vela-sd-btn{height:38px;}
9900
+ .vela-sd-mobile .vela-sd-row span,.vela-sd-mobile .vela-sd-bool span{white-space:normal !important;}
9344
9901
  `;
9345
9902
  document.head.appendChild(st);
9346
9903
  }
@@ -9367,6 +9924,8 @@ var SettingsDialog = class {
9367
9924
  this.typeActiveGroup = /* @__PURE__ */ new Map();
9368
9925
  /** The tab currently shown, so a theme change (which rebuilds) lands back on it. */
9369
9926
  this.activeSection = null;
9927
+ /** Mobile chrome: fullscreen card, burger-opened section sidebar, TOC as top tabs. */
9928
+ this.mobileLayout = false;
9370
9929
  if (getComputedStyle(container).position === "static") container.style.position = "relative";
9371
9930
  }
9372
9931
  /** Host-app sections (e.g. the widget's Status line tab) — re-shown on next open. */
@@ -9385,15 +9944,27 @@ var SettingsDialog = class {
9385
9944
  }
9386
9945
  setTheme(theme) {
9387
9946
  this.theme = theme;
9388
- if (this.root) {
9389
- const cfg = this.config;
9390
- const oc = this.onChange;
9391
- const oi = this.onImport;
9392
- const orst = this.onReset;
9393
- const section = this.activeSection;
9394
- this.close();
9395
- if (cfg && oc) this.open(cfg, oc, oi ?? void 0, orst ?? void 0, section ?? void 0);
9396
- }
9947
+ this.reopenIfLive();
9948
+ }
9949
+ /** The host shell's chrome size class — mobile presents the dialog fullscreen with
9950
+ * the tab rail behind a burger sidebar and the TOC as top tabs. An open dialog
9951
+ * re-presents in place, on the same tab. */
9952
+ setLayoutMode(mode) {
9953
+ const mobile = mode === "mobile";
9954
+ if (mobile === this.mobileLayout) return;
9955
+ this.mobileLayout = mobile;
9956
+ this.reopenIfLive();
9957
+ }
9958
+ /** Rebuild an open dialog in place (theme swap, layout-mode flip) on the same tab. */
9959
+ reopenIfLive() {
9960
+ if (!this.root) return;
9961
+ const cfg = this.config;
9962
+ const oc = this.onChange;
9963
+ const oi = this.onImport;
9964
+ const orst = this.onReset;
9965
+ const section = this.activeSection;
9966
+ this.close();
9967
+ if (cfg && oc) this.open(cfg, oc, oi ?? void 0, orst ?? void 0, section ?? void 0);
9397
9968
  }
9398
9969
  isOpen() {
9399
9970
  return this.root !== null;
@@ -9415,18 +9986,37 @@ var SettingsDialog = class {
9415
9986
  this.onImport = onImport ?? null;
9416
9987
  this.onReset = onReset ?? null;
9417
9988
  ensureControlStyles();
9989
+ const mobile = this.mobileLayout;
9418
9990
  const scrim = document.createElement("div");
9419
- 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;";
9991
+ 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;";
9992
+ if (mobile) {
9993
+ scrim.classList.add("vela-sd-mobile");
9994
+ scrim.style.paddingTop = "0";
9995
+ scrim.style.alignItems = "stretch";
9996
+ }
9420
9997
  scrim.addEventListener("mousedown", (e) => {
9421
9998
  if (e.target === scrim) this.close();
9422
9999
  });
9423
10000
  const dlg = document.createElement("div");
9424
10001
  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;`;
10002
+ 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;";
9425
10003
  const header = document.createElement("div");
9426
10004
  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;`;
10005
+ if (mobile) header.style.paddingLeft = "8px";
9427
10006
  const hTitle = document.createElement("span");
9428
10007
  hTitle.textContent = "Chart settings";
9429
10008
  hTitle.style.cssText = "font-size:17px;font-weight:600;letter-spacing:0.2px;";
10009
+ if (mobile) hTitle.style.cssText += "flex:1 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;";
10010
+ let toggleRail = null;
10011
+ if (mobile) {
10012
+ const burger = document.createElement("button");
10013
+ burger.type = "button";
10014
+ burger.className = "vela-sd-burger";
10015
+ burger.innerHTML = iconAt("burger", 16);
10016
+ burger.title = "Sections";
10017
+ burger.addEventListener("click", () => toggleRail?.());
10018
+ header.append(burger);
10019
+ }
9430
10020
  const closeBtn = document.createElement("button");
9431
10021
  closeBtn.type = "button";
9432
10022
  closeBtn.innerHTML = iconAt("close", 15);
@@ -9434,8 +10024,8 @@ var SettingsDialog = class {
9434
10024
  closeBtn.className = "vela-sd-close";
9435
10025
  closeBtn.addEventListener("click", () => this.close());
9436
10026
  header.append(hTitle, closeBtn);
9437
- header.style.cursor = "move";
9438
- {
10027
+ if (!mobile) {
10028
+ header.style.cursor = "move";
9439
10029
  let sx = 0, sy = 0, ox = 0, oy = 0, dragging = false;
9440
10030
  header.addEventListener("pointerdown", (e) => {
9441
10031
  if (e.target?.closest(".vela-sd-close")) return;
@@ -9616,12 +10206,24 @@ var SettingsDialog = class {
9616
10206
  renderChartTypeSections("end");
9617
10207
  renderHostSections("end");
9618
10208
  const shell = document.createElement("div");
9619
- shell.style.cssText = "display:flex;min-height:360px;max-height:calc(70vh - 100px);flex:1 1 auto;";
10209
+ 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;";
9620
10210
  const rail = document.createElement("div");
9621
- 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;`;
10211
+ rail.className = "vela-sd-rail";
10212
+ 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;`;
10213
+ let railScrim = null;
10214
+ if (mobile) {
10215
+ railScrim = document.createElement("div");
10216
+ railScrim.className = "vela-sd-railscrim";
10217
+ railScrim.addEventListener("click", () => toggleRail?.());
10218
+ toggleRail = (open) => {
10219
+ const on = open ?? !rail.classList.contains("open");
10220
+ rail.classList.toggle("open", on);
10221
+ railScrim?.classList.toggle("open", on);
10222
+ };
10223
+ }
9622
10224
  const paneHost = document.createElement("div");
9623
10225
  paneHost.className = "vela-sd-pane";
9624
- paneHost.style.cssText = "flex:1;overflow-y:auto;padding:6px 18px 14px;";
10226
+ 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;";
9625
10227
  const panes = [];
9626
10228
  let current = null;
9627
10229
  for (const child of [...body.children]) {
@@ -9656,6 +10258,10 @@ var SettingsDialog = class {
9656
10258
  p.tab.classList.toggle("on", i === idx);
9657
10259
  });
9658
10260
  this.activeSection = panes[idx]?.title ?? null;
10261
+ if (mobile) {
10262
+ hTitle.textContent = panes[idx]?.title ?? "Chart settings";
10263
+ toggleRail?.(false);
10264
+ }
9659
10265
  };
9660
10266
  panes.forEach((p, i) => {
9661
10267
  p.tab.addEventListener("click", () => activate(i));
@@ -9663,10 +10269,11 @@ var SettingsDialog = class {
9663
10269
  paneHost.appendChild(p.el);
9664
10270
  });
9665
10271
  this.tabs = panes.map((p, i) => ({ title: p.title, show: () => activate(i) }));
9666
- const wanted = section === void 0 ? -1 : panes.findIndex((p) => p.title.toLowerCase() === section.toLowerCase());
10272
+ 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"));
9667
10273
  activate(wanted >= 0 ? wanted : 0);
9668
10274
  this.syncTypeTabs?.(config.series.style);
9669
10275
  shell.append(rail, paneHost);
10276
+ if (railScrim) shell.append(railScrim);
9670
10277
  dlg.appendChild(shell);
9671
10278
  dlg.appendChild(this.footer(config));
9672
10279
  scrim.appendChild(dlg);
@@ -9683,6 +10290,7 @@ var SettingsDialog = class {
9683
10290
  }
9684
10291
  close() {
9685
10292
  closeColorPopover();
10293
+ closeWidthPopover();
9686
10294
  this.root?.remove();
9687
10295
  this.root = null;
9688
10296
  this.tabs = [];
@@ -9723,20 +10331,12 @@ var SettingsDialog = class {
9723
10331
  body.append(marker);
9724
10332
  const values = config.chartTypes[typeId] ?? {};
9725
10333
  const bag = {};
9726
- const seedKey = (key, want, defval) => {
9727
- const v = values[key];
9728
- bag[key] = typeof v === want ? v : defval;
9729
- };
9730
10334
  const seed = (rows) => {
9731
10335
  for (const r of rows) {
9732
- if (r.kind === "heading" || r.kind === "header") continue;
9733
- if (r.kind === "range") {
9734
- seedKey(r.minKey, "number", r.defval);
9735
- seedKey(r.maxKey, "number", r.defval);
9736
- continue;
10336
+ for (const k of settingsRowValueKeys(r)) {
10337
+ const v = values[k.key];
10338
+ bag[k.key] = typeof v === k.type ? v : k.defval;
9737
10339
  }
9738
- seedKey(r.key, r.kind === "toggle" ? "boolean" : r.kind === "number" ? "number" : "string", r.defval);
9739
- if (r.kind === "toggle") for (const c of r.colors ?? []) seedKey(c.key, "string", c.defval);
9740
10340
  }
9741
10341
  };
9742
10342
  if (section.rows) seed(section.rows);
@@ -9759,7 +10359,8 @@ var SettingsDialog = class {
9759
10359
  if (section.instances && section.instances.length > 0) {
9760
10360
  body.append(this.instancesBlock(typeId, section.instances, bag, put, refreshers));
9761
10361
  } else if (section.rows) {
9762
- this.flatTypeRows(section.rows, bag, put, refreshers, body);
10362
+ if (section.layout === "grouped") body.append(this.groupedRows(`${typeId}/rows`, section.rows, bag, put, refreshers));
10363
+ else this.flatTypeRows(section.rows, bag, put, refreshers, body);
9763
10364
  }
9764
10365
  for (const sub of section.subsections ?? []) {
9765
10366
  const subMarker = this.section(sub.title);
@@ -9776,7 +10377,7 @@ var SettingsDialog = class {
9776
10377
  flatTypeRows(rows, bag, put, refreshers, body) {
9777
10378
  const entries = [];
9778
10379
  for (const r of rows) {
9779
- const el = r.kind === "heading" || r.kind === "header" ? this.sectionTitle(r.label) : this.typeRow(r, bag, put);
10380
+ const el = r.kind === "heading" || r.kind === "header" ? this.sectionTitle(r.label) : this.typeRow(r, bag, put, refreshers);
9780
10381
  entries.push({ el, when: r.when });
9781
10382
  body.append(el);
9782
10383
  }
@@ -9784,60 +10385,109 @@ var SettingsDialog = class {
9784
10385
  for (const e of entries) e.el.classList.toggle("vela-sd-hide", !settingsRowVisible(e.when, bag));
9785
10386
  });
9786
10387
  }
9787
- /** One value row for a chart-type descriptor, writing through `put`. */
9788
- typeRow(r, bag, put) {
9789
- if (r.kind === "toggle") {
9790
- const swatches = (r.colors ?? []).map((c) => {
9791
- const sw = this.swatch(bag[c.key], (v) => put(c.key, v));
9792
- sw.title = c.label;
9793
- return sw;
9794
- });
9795
- return this.toggleRow(r.label, bag[r.key], (v) => put(r.key, v), swatches);
10388
+ /**
10389
+ * One value row for a chart-type descriptor, writing through `put`. EVERY kind is
10390
+ * first reduced to the canonical composite shape ({@link normalizeSettingsRow}) and
10391
+ * rendered by the ONE path: optional leading toggle, then the ordered inline
10392
+ * controls in the control column. A control carrying its own `when` registers a
10393
+ * visibility refresher — it appears and disappears live as the bag changes,
10394
+ * independent of the row's gate — and is exempt from the toggle-off dim (it may
10395
+ * exist FOR the off state).
10396
+ */
10397
+ typeRow(r, bag, put, refreshers) {
10398
+ const n = normalizeSettingsRow(r);
10399
+ const controls = [];
10400
+ for (const c of n.controls) {
10401
+ const el = this.inlineControl(c, bag, put, n.toggle !== void 0);
10402
+ if (c.kind !== "hint") refreshers.push(() => el.dispatchEvent(new Event("vela-sync")));
10403
+ if (c.kind !== "hint" && c.when) {
10404
+ el.dataset.sdSelfGated = "1";
10405
+ const when = c.when;
10406
+ refreshers.push(() => {
10407
+ el.style.display = settingsRowVisible(when, bag) ? "" : "none";
10408
+ });
10409
+ }
10410
+ controls.push(el);
10411
+ }
10412
+ if (n.toggle) {
10413
+ const t = n.toggle;
10414
+ const el = this.toggleRow(n.label, bag[t.key], (v) => put(t.key, v), controls, () => bag[t.key] === true);
10415
+ refreshers.push(() => el.dispatchEvent(new Event("vela-sync")));
10416
+ return el;
9796
10417
  }
9797
- 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));
9798
- if (r.kind === "color") return this.colorRow(r.label, bag[r.key], (v) => put(r.key, v));
9799
- if (r.kind === "range") return this.rangeRow(r, bag, put);
9800
- return this.selectRowLabeled(r.label, bag[r.key], normalizeSelectOptions(r.options), (v) => put(r.key, v));
10418
+ return this.rowWith(n.label, controls);
9801
10419
  }
9802
10420
  /**
9803
- * A MIN–MAX row: two number inputs on one row (stored under the descriptor's
9804
- * `minKey`/`maxKey`). With a `placeholder`, an input at the DEFAULT value renders
9805
- * empty showing it, and clearing an input stores the default back — the
9806
- * placeholder names the unset state ('Off' for 0-disables bounds).
10421
+ * Build ONE inline control from its descriptor the factory behind the composite
10422
+ * row path. `compact` narrows number inputs on toggle rows (the historical inline
10423
+ * width) while standalone rows keep the full-width input.
9807
10424
  */
9808
- rangeRow(r, bag, put) {
9809
- const { wrap } = this.row(r.label);
9810
- const input = (key, title) => {
9811
- const ni = document.createElement("input");
9812
- ni.type = "number";
9813
- ni.className = "vela-sd-number";
9814
- ni.min = String(r.min ?? 0);
9815
- ni.max = String(r.max ?? 1e6);
9816
- ni.step = String(r.step ?? 1);
9817
- ni.title = title;
9818
- const current = bag[key];
9819
- if (r.placeholder !== void 0) {
9820
- ni.placeholder = r.placeholder;
9821
- if (current !== r.defval) ni.value = String(current);
9822
- } else {
9823
- ni.value = String(current);
10425
+ inlineControl(c, bag, put, compact) {
10426
+ if (c.kind === "hint") {
10427
+ const s = document.createElement("span");
10428
+ s.textContent = c.text;
10429
+ s.style.cssText = "color:var(--vela-fg-muted);";
10430
+ return s;
10431
+ }
10432
+ if (c.kind === "color") {
10433
+ const sw = this.swatch(bag[c.key], (v) => put(c.key, v), () => bag[c.key]);
10434
+ sw.title = c.label;
10435
+ return sw;
10436
+ }
10437
+ if (c.kind === "width") {
10438
+ let cur = typeof bag[c.key] === "number" ? bag[c.key] : c.defval;
10439
+ const wf = widthField(this.theme, () => cur, (v) => {
10440
+ cur = v;
10441
+ put(c.key, v);
10442
+ });
10443
+ wf.title = c.label;
10444
+ return wf;
10445
+ }
10446
+ if (c.kind === "select") {
10447
+ const sel = document.createElement("select");
10448
+ sel.className = "vela-sd-select";
10449
+ sel.style.cssText = "max-width:200px;flex:0 0 auto;";
10450
+ sel.title = c.label;
10451
+ const current2 = typeof bag[c.key] === "string" ? bag[c.key] : c.defval;
10452
+ for (const [val, lbl] of normalizeSelectOptions(c.options)) {
10453
+ const o = document.createElement("option");
10454
+ o.value = val;
10455
+ o.textContent = lbl;
10456
+ if (val === current2) o.selected = true;
10457
+ sel.appendChild(o);
9824
10458
  }
10459
+ sel.addEventListener("change", () => put(c.key, sel.value));
10460
+ sel.addEventListener("vela-sync", () => {
10461
+ sel.value = typeof bag[c.key] === "string" ? bag[c.key] : c.defval;
10462
+ });
10463
+ return sel;
10464
+ }
10465
+ const ni = document.createElement("input");
10466
+ ni.type = "number";
10467
+ ni.className = "vela-sd-number";
10468
+ if (compact) ni.style.width = "56px";
10469
+ if (c.min !== void 0) ni.min = String(c.min);
10470
+ if (c.max !== void 0) ni.max = String(c.max);
10471
+ ni.step = String(c.step ?? 1);
10472
+ ni.title = c.label;
10473
+ const current = typeof bag[c.key] === "number" ? bag[c.key] : c.defval;
10474
+ if (c.placeholder !== void 0) {
10475
+ ni.placeholder = c.placeholder;
10476
+ if (current !== c.defval) ni.value = String(current);
9825
10477
  ni.addEventListener("change", () => {
9826
- const raw = ni.value.trim() === "" ? r.defval : Number(ni.value);
9827
- const v = Number.isFinite(raw) ? Math.min(r.max ?? Infinity, Math.max(r.min ?? -Infinity, raw)) : r.defval;
9828
- ni.value = r.placeholder !== void 0 && v === r.defval ? "" : String(v);
9829
- put(key, v);
10478
+ const raw = ni.value.trim() === "" ? c.defval : Number(ni.value);
10479
+ const v = Number.isFinite(raw) ? Math.min(c.max ?? Infinity, Math.max(c.min ?? -Infinity, raw)) : c.defval;
10480
+ ni.value = v === c.defval ? "" : String(v);
10481
+ put(c.key, v);
9830
10482
  });
9831
- return ni;
9832
- };
9833
- const box = document.createElement("div");
9834
- box.style.cssText = "display:flex;align-items:center;gap:6px;flex:0 0 auto;";
9835
- const dash = document.createElement("span");
9836
- dash.textContent = "\u2013";
9837
- dash.style.cssText = "color:var(--vela-fg-muted);";
9838
- box.append(input(r.minKey, `${r.label} \u2014 min`), dash, input(r.maxKey, `${r.label} \u2014 max`));
9839
- wrap.appendChild(box);
9840
- return wrap;
10483
+ } else {
10484
+ ni.value = String(current);
10485
+ ni.addEventListener("input", () => {
10486
+ const v = Number(ni.value);
10487
+ if (Number.isFinite(v)) put(c.key, v);
10488
+ });
10489
+ }
10490
+ return ni;
9841
10491
  }
9842
10492
  /**
9843
10493
  * The INSTANCE STRIP block: a tab per present instance (label, `×` on the active
@@ -9850,7 +10500,7 @@ var SettingsDialog = class {
9850
10500
  const wrap = document.createElement("div");
9851
10501
  const strip = document.createElement("div");
9852
10502
  strip.className = "vela-sd-itabs";
9853
- wrap.append(strip);
10503
+ if (instances.length > 1 || instances[0]?.enableKey !== void 0) wrap.append(strip);
9854
10504
  const contents = instances.map((inst, i) => {
9855
10505
  const content = this.groupedRows(`${typeId}/#${i}`, inst.rows, bag, put, refreshers);
9856
10506
  wrap.append(content);
@@ -9945,8 +10595,8 @@ var SettingsDialog = class {
9945
10595
  rowsHost.append(entries[entries.length - 1].el);
9946
10596
  continue;
9947
10597
  }
9948
- const el = this.typeRow(r, bag, put);
9949
- const key = r.kind === "range" ? void 0 : r.key;
10598
+ const el = this.typeRow(r, bag, put, refreshers);
10599
+ const key = r.kind === "row" ? r.toggle?.key : r.kind === "range" ? void 0 : r.key;
9950
10600
  entries.push({ el, when: r.when, group: g, key });
9951
10601
  rowsHost.append(el);
9952
10602
  }
@@ -10012,6 +10662,14 @@ var SettingsDialog = class {
10012
10662
  */
10013
10663
  layoutSettingsGrids(pane, measureHost) {
10014
10664
  if (pane.childElementCount === 0) return;
10665
+ if (this.mobileLayout) {
10666
+ const grid2 = document.createElement("div");
10667
+ grid2.style.cssText = "display:grid;grid-template-columns:minmax(0,1fr) max-content;align-items:center;column-gap:12px;row-gap:16px;";
10668
+ while (pane.firstChild) grid2.appendChild(pane.firstChild);
10669
+ pane.appendChild(grid2);
10670
+ this.prepareGridItems(grid2);
10671
+ return;
10672
+ }
10015
10673
  const rows = [...pane.querySelectorAll(".vela-sd-row")];
10016
10674
  const probe2 = document.createElement("div");
10017
10675
  probe2.style.cssText = "position:absolute;left:-9999px;top:0;visibility:hidden;display:flex;flex-direction:column;font:13px var(--vela-font);pointer-events:none;";
@@ -10048,9 +10706,11 @@ var SettingsDialog = class {
10048
10706
  }
10049
10707
  }
10050
10708
  /** A bare color swatch input (for toggle-row right groups / swatch pairs). */
10051
- swatch(value, onChange) {
10709
+ /** With `get`, the field reads its value live (and repaints on 'vela-sync')
10710
+ * duplicate-keyed rows stay honest when another row edits the shared key. */
10711
+ swatch(value, onChange, get) {
10052
10712
  let current = value;
10053
- return colorField(this.theme, () => current, (v) => {
10713
+ return colorField(this.theme, () => get ? get() : current, (v) => {
10054
10714
  current = v;
10055
10715
  onChange(v);
10056
10716
  });
@@ -10102,8 +10762,11 @@ var SettingsDialog = class {
10102
10762
  }
10103
10763
  /** An enable row: checkbox + label in the label column, dependent controls in the
10104
10764
  * shared control column; the control group dims and ignores input while the toggle
10105
- * is off. With no controls it reads like a plain toggle row (full-width in the grid). */
10106
- toggleRow(label, value, onToggle, controls) {
10765
+ * is off. With no controls it reads like a plain toggle row (full-width in the grid).
10766
+ * With `get`, the row re-reads its state on a 'vela-sync' event — the seam that
10767
+ * keeps DUPLICATE-KEYED rows honest (several `when`-gated rows over one bag key,
10768
+ * only one visible at a time; see `typeRow`). */
10769
+ toggleRow(label, value, onToggle, controls, get) {
10107
10770
  const wrap = document.createElement("div");
10108
10771
  const cb = document.createElement("button");
10109
10772
  cb.type = "button";
@@ -10123,6 +10786,12 @@ var SettingsDialog = class {
10123
10786
  wrap.style.cssText = "display:flex;align-items:center;gap:8px;min-height:22px;";
10124
10787
  wrap.append(cb, lbl);
10125
10788
  cb.addEventListener("click", () => onToggle(cbToggle()));
10789
+ if (get) {
10790
+ wrap.addEventListener("vela-sync", () => {
10791
+ checked = get();
10792
+ cb.classList.toggle("on", checked);
10793
+ });
10794
+ }
10126
10795
  return wrap;
10127
10796
  }
10128
10797
  wrap.className = "vela-sd-row";
@@ -10133,8 +10802,11 @@ var SettingsDialog = class {
10133
10802
  box.style.cssText = "display:flex;align-items:center;gap:6px;";
10134
10803
  for (const c of controls) box.appendChild(c);
10135
10804
  const syncDim = (on) => {
10136
- box.style.opacity = on ? "1" : "0.4";
10137
- box.style.pointerEvents = on ? "" : "none";
10805
+ for (const c of controls) {
10806
+ if (c.dataset.sdSelfGated === "1") continue;
10807
+ c.style.opacity = on ? "1" : "0.4";
10808
+ c.style.pointerEvents = on ? "" : "none";
10809
+ }
10138
10810
  };
10139
10811
  syncDim(value);
10140
10812
  cb.addEventListener("click", () => {
@@ -10142,6 +10814,13 @@ var SettingsDialog = class {
10142
10814
  onToggle(v);
10143
10815
  syncDim(v);
10144
10816
  });
10817
+ if (get) {
10818
+ wrap.addEventListener("vela-sync", () => {
10819
+ checked = get();
10820
+ cb.classList.toggle("on", checked);
10821
+ syncDim(checked);
10822
+ });
10823
+ }
10145
10824
  wrap.append(left, box);
10146
10825
  return wrap;
10147
10826
  }
@@ -10161,96 +10840,6 @@ var SettingsDialog = class {
10161
10840
  wrap.appendChild(ni);
10162
10841
  return wrap;
10163
10842
  }
10164
- /** Inline Auto/Manual row (Resolution, Text size): a mode dropdown + a value input greyed out in Auto. */
10165
- autoManualRow(label, isAuto, value, min, max, unit, onMode, onValue) {
10166
- const { wrap } = this.row(label);
10167
- const box = document.createElement("div");
10168
- box.style.cssText = "display:flex;align-items:center;gap:6px;flex:0 0 auto;";
10169
- const sel = document.createElement("select");
10170
- sel.className = "vela-sd-select";
10171
- for (const [val, lbl] of AUTO_MANUAL_OPTS) {
10172
- const o = document.createElement("option");
10173
- o.value = val;
10174
- o.textContent = lbl;
10175
- if (val === "auto" === isAuto) o.selected = true;
10176
- sel.appendChild(o);
10177
- }
10178
- const ni = document.createElement("input");
10179
- ni.type = "number";
10180
- ni.min = String(min);
10181
- ni.max = String(max);
10182
- ni.step = "1";
10183
- ni.value = String(value);
10184
- ni.className = "vela-sd-number";
10185
- const unitLbl = document.createElement("span");
10186
- unitLbl.textContent = unit;
10187
- unitLbl.style.cssText = "opacity:0.6;";
10188
- const syncDisabled = (auto) => {
10189
- ni.disabled = auto;
10190
- ni.style.opacity = auto ? "0.4" : "1";
10191
- };
10192
- syncDisabled(isAuto);
10193
- sel.addEventListener("change", () => {
10194
- const auto = sel.value === "auto";
10195
- onMode(auto);
10196
- syncDisabled(auto);
10197
- });
10198
- ni.addEventListener("input", () => {
10199
- const n = Number(ni.value);
10200
- if (Number.isFinite(n)) onValue(n);
10201
- });
10202
- box.append(sel, ni, unitLbl);
10203
- wrap.appendChild(box);
10204
- return wrap;
10205
- }
10206
- /** A row whose control area holds several inline controls (e.g. show + color + width). */
10207
- inlineRow(label, controls) {
10208
- const { wrap } = this.row(label);
10209
- const box = document.createElement("div");
10210
- box.style.cssText = "display:flex;align-items:center;gap:6px;flex:0 0 auto;";
10211
- for (const c of controls) box.appendChild(c);
10212
- wrap.appendChild(box);
10213
- return wrap;
10214
- }
10215
- /** A small dimmed hint span (e.g. the ≥ / ≤ between filter inputs). */
10216
- hint(text) {
10217
- const s = document.createElement("span");
10218
- s.textContent = text;
10219
- s.style.cssText = "opacity:0.5;font-size:12px;";
10220
- return s;
10221
- }
10222
- /** A bare color input (for inline groups). */
10223
- colorInput(value, onChange) {
10224
- const ci = document.createElement("input");
10225
- ci.type = "color";
10226
- ci.value = toHex6(value);
10227
- ci.style.cssText = "cursor:pointer;width:34px;height:22px;border:none;background:transparent;padding:0;flex:0 0 auto;";
10228
- ci.addEventListener("input", () => onChange(ci.value));
10229
- return ci;
10230
- }
10231
- /** A bare compact number input (for inline groups). */
10232
- numberInput(value, min, max, step, onChange) {
10233
- const ni = document.createElement("input");
10234
- ni.type = "number";
10235
- ni.value = String(value);
10236
- ni.min = String(min);
10237
- ni.max = String(max);
10238
- ni.step = String(step);
10239
- ni.className = "vela-sd-number";
10240
- ni.style.flex = "0 0 auto";
10241
- ni.addEventListener("input", () => {
10242
- const n = Number(ni.value);
10243
- if (Number.isFinite(n)) onChange(n);
10244
- });
10245
- return ni;
10246
- }
10247
- /** A small in-group heading (lighter than a top-level section divider). */
10248
- subheading(text) {
10249
- const el = document.createElement("div");
10250
- el.textContent = text;
10251
- el.style.cssText = "margin-top:4px;font-size:10.5px;font-weight:700;letter-spacing:0.03em;text-transform:uppercase;opacity:0.45;";
10252
- return el;
10253
- }
10254
10843
  /** A dropdown whose option values differ from their display labels. */
10255
10844
  selectRowLabeled(label, value, options, onChange) {
10256
10845
  const { wrap } = this.row(label);
@@ -10296,19 +10885,7 @@ var SettingsDialog = class {
10296
10885
  foot.appendChild(resetBtn);
10297
10886
  return foot;
10298
10887
  }
10299
- button(label, onClick) {
10300
- const b = document.createElement("button");
10301
- b.type = "button";
10302
- b.textContent = label;
10303
- b.style.cssText = `cursor:pointer;flex:1 1 auto;${this.ctrlStyle()}padding:5px 8px;font-weight:600;`;
10304
- b.addEventListener("click", onClick);
10305
- return b;
10306
- }
10307
- ctrlStyle() {
10308
- 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;`;
10309
- }
10310
10888
  };
10311
- var AUTO_MANUAL_OPTS = [["auto", "Auto"], ["manual", "Manual"]];
10312
10889
  function normalizeSelectOptions(options) {
10313
10890
  return options.map((o) => typeof o === "string" ? [o, o] : o);
10314
10891
  }
@@ -10511,7 +11088,7 @@ var DrawingPainter = class {
10511
11088
  return;
10512
11089
  }
10513
11090
  if (d instanceof FixedRangeVolumeProfile) {
10514
- this.paintFixedRangeVp(ctx, d, proj);
11091
+ this.paintFixedRangeVp(ctx, d, proj, theme);
10515
11092
  this.paintLabel(ctx, d, proj, theme);
10516
11093
  return;
10517
11094
  }
@@ -11004,7 +11581,7 @@ var DrawingPainter = class {
11004
11581
  /** Paint a fixed-range volume profile: horizontal histogram rows (up/down split) anchored to
11005
11582
  * the left or right of the time span, optional VAH / VAL / POC levels across the range, and
11006
11583
  * optional developing POC / VA polylines. Recomputes from the two anchors on every paint. */
11007
- paintFixedRangeVp(ctx, d, proj) {
11584
+ paintFixedRangeVp(ctx, d, proj, theme) {
11008
11585
  const L = d.layout(proj);
11009
11586
  if (!L) return;
11010
11587
  const s = d.frvp;
@@ -11043,7 +11620,7 @@ var DrawingPainter = class {
11043
11620
  };
11044
11621
  hLine(s.showVah, s.vahColor, s.vahStyle, L.vahY);
11045
11622
  hLine(s.showVal, s.valColor, s.valStyle, L.valY);
11046
- hLine(s.showPoc, s.pocColor, s.pocStyle, L.pocY);
11623
+ hLine(s.showPoc, s.pocColor ?? contrastColor(theme.background), s.pocStyle, L.pocY);
11047
11624
  const devW = Math.max(1, w);
11048
11625
  if (s.showDevelopingPoc) {
11049
11626
  this.strokePolyline(ctx, L.developingPoc, {
@@ -11915,7 +12492,11 @@ var DrawingInteraction = class {
11915
12492
  const last = draft.anchors[draft.anchors.length - 1];
11916
12493
  const lx = last ? proj.xOf(last.time) : NaN;
11917
12494
  const ly = last ? proj.yOf(last.price, draft.paneId) ?? NaN : NaN;
11918
- if (draft.anchors.length < this.state.need && Math.hypot(x - lx, y - ly) > FREEHAND_SAMPLE_PX) {
12495
+ if (Math.hypot(x - lx, y - ly) > FREEHAND_SAMPLE_PX) {
12496
+ if (draft.anchors.length >= this.state.need) {
12497
+ const lastIdx = draft.anchors.length - 1;
12498
+ draft.anchors = draft.anchors.filter((_, i) => i % 2 === 0 || i === lastIdx);
12499
+ }
11919
12500
  draft.anchors.push(proj.pxToPoint(x, y, draft.paneId));
11920
12501
  }
11921
12502
  this.state.cursor = proj.pxToPoint(x, y, draft.paneId);
@@ -11988,11 +12569,11 @@ function applyFree(anchor, pt, free) {
11988
12569
  // src/renderers/native/drawings/DrawingSettingsPopup.ts
11989
12570
  var BTN = 30;
11990
12571
  var ICON = 21;
11991
- var STYLE_ID4 = "vela-drawing-popup-styles";
11992
- function ensureStyles4() {
11993
- if (typeof document === "undefined" || document.getElementById(STYLE_ID4)) return;
12572
+ var STYLE_ID5 = "vela-drawing-popup-styles";
12573
+ function ensureStyles5() {
12574
+ if (typeof document === "undefined" || document.getElementById(STYLE_ID5)) return;
11994
12575
  const s = document.createElement("style");
11995
- s.id = STYLE_ID4;
12576
+ s.id = STYLE_ID5;
11996
12577
  s.textContent = `
11997
12578
  .vela-dpop input[type=color]{-webkit-appearance:none;appearance:none;border:none;padding:0;background:none;cursor:pointer;border-radius:0;}
11998
12579
  .vela-dpop input[type=color]::-webkit-color-swatch-wrapper{padding:0;}
@@ -12011,7 +12592,10 @@ function ensureStyles4() {
12011
12592
  .vela-dpop-btn[data-active='1']{background:var(--vela-active);color:var(--vela-fg-bright);}
12012
12593
  .vela-dpop-item{background:transparent;transition:background var(--vela-dur-fast) ease;}
12013
12594
  .vela-dpop-item:hover{background:var(--vela-hover-strong);}
12014
- .vela-dpop-item[data-active='1']{background:var(--vela-active);}`;
12595
+ .vela-dpop-item[data-active='1']{background:var(--vela-active);}
12596
+ /* A finger needs a wider grab zone than a cursor \u2014 grow the move handle on touch-first
12597
+ devices (the glyph stays centered; only the hit target widens). */
12598
+ @media (pointer: coarse){.vela-dpop-grip{width:${BTN + 14}px !important;}}`;
12015
12599
  document.head.appendChild(s);
12016
12600
  }
12017
12601
  var DrawingSettingsPopup = class {
@@ -12058,7 +12642,7 @@ var DrawingSettingsPopup = class {
12058
12642
  * `onClose` fires on an outside-click dismissal (not a programmatic close). */
12059
12643
  open(drawing, anchor, actions, onClose) {
12060
12644
  this.close();
12061
- ensureStyles4();
12645
+ ensureStyles5();
12062
12646
  this.onClose = onClose ?? null;
12063
12647
  const t = this.theme;
12064
12648
  const schema = drawing.schema();
@@ -12084,7 +12668,14 @@ var DrawingSettingsPopup = class {
12084
12668
  bar.appendChild(this.dropdown("Icon size", STAMP_SIZE_OPTIONS, sz, (s) => stampSizeIcon(s), (v) => actions.patch({ size: v }), { label: sizeLabel }));
12085
12669
  }
12086
12670
  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 })));
12087
- 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 }));
12671
+ if (paths.has("style.lineWidth")) {
12672
+ const wf = schema.fields.find((f) => f.path === "style.lineWidth");
12673
+ if (wf?.kind === "number" && (wf.min ?? 1) > 1) {
12674
+ bar.appendChild(this.widthInput("Line width", drawing.style.lineWidth, wf.min ?? 1, wf.max ?? 60, wf.step ?? 1, (v) => actions.patch({ "style.lineWidth": v })));
12675
+ } else {
12676
+ 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 }));
12677
+ }
12678
+ }
12088
12679
  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 }));
12089
12680
  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 })));
12090
12681
  const isFrvp = paths.has("frvp.rows") && drawing instanceof FixedRangeVolumeProfile;
@@ -12552,14 +13143,15 @@ var DrawingSettingsPopup = class {
12552
13143
  const chk = document.createElement("input");
12553
13144
  chk.type = "checkbox";
12554
13145
  chk.checked = Boolean(s[showPath]);
12555
- chk.style.cssText = `accent-color:${s[colorPath]};width:15px;height:15px;flex:none;cursor:pointer;`;
13146
+ const current = s[colorPath] ?? contrastColor(t.background);
13147
+ chk.style.cssText = `accent-color:${current};width:15px;height:15px;flex:none;cursor:pointer;`;
12556
13148
  chk.addEventListener("change", () => actions.patch({ [`frvp.${showPath}`]: chk.checked }));
12557
13149
  const lbl = document.createElement("span");
12558
13150
  lbl.textContent = label;
12559
13151
  lbl.style.cssText = "flex:1;min-width:0;opacity:0.9;";
12560
13152
  const col = document.createElement("button");
12561
13153
  col.type = "button";
12562
- let cur = s[colorPath];
13154
+ let cur = current;
12563
13155
  col.style.cssText = `width:18px;height:18px;flex:none;border:1px solid var(--vela-border);border-radius:0;cursor:pointer;background:${cur};padding:0;`;
12564
13156
  col.addEventListener("pointerdown", (e) => e.stopPropagation());
12565
13157
  col.addEventListener("click", (e) => {
@@ -12739,7 +13331,8 @@ var DrawingSettingsPopup = class {
12739
13331
  dragHandle() {
12740
13332
  const b = document.createElement("button");
12741
13333
  b.type = "button";
12742
- 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);`;
13334
+ b.className = "vela-dpop-grip";
13335
+ 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);`;
12743
13336
  b.innerHTML = sized(GRIP_ICON);
12744
13337
  b.addEventListener("pointerdown", (e) => {
12745
13338
  const el = this.el;
@@ -12755,6 +13348,10 @@ var DrawingSettingsPopup = class {
12755
13348
  const origLeft = parseFloat(el.style.left) || 0;
12756
13349
  const origTop = parseFloat(el.style.top) || 0;
12757
13350
  b.style.cursor = "grabbing";
13351
+ try {
13352
+ b.setPointerCapture(e.pointerId);
13353
+ } catch {
13354
+ }
12758
13355
  const move = (ev) => {
12759
13356
  const w = el.offsetWidth;
12760
13357
  const h = el.offsetHeight;
@@ -12767,9 +13364,11 @@ var DrawingSettingsPopup = class {
12767
13364
  b.style.cursor = "grab";
12768
13365
  window.removeEventListener("pointermove", move);
12769
13366
  window.removeEventListener("pointerup", up);
13367
+ window.removeEventListener("pointercancel", up);
12770
13368
  };
12771
13369
  window.addEventListener("pointermove", move);
12772
13370
  window.addEventListener("pointerup", up);
13371
+ window.addEventListener("pointercancel", up);
12773
13372
  });
12774
13373
  return b;
12775
13374
  }
@@ -12885,6 +13484,39 @@ var DrawingSettingsPopup = class {
12885
13484
  });
12886
13485
  return b;
12887
13486
  }
13487
+ /** An inline numeric width field for tools whose stroke range outgrows the 1–4px
13488
+ * ladder — the value is clamped to the schema's declared min/max on commit. */
13489
+ widthInput(tip, value, min, max, step, onChange) {
13490
+ const input = document.createElement("input");
13491
+ input.type = "number";
13492
+ input.dataset.tip = tip;
13493
+ input.min = String(min);
13494
+ input.max = String(max);
13495
+ input.step = String(step);
13496
+ input.value = String(value);
13497
+ 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;`;
13498
+ const commit = () => {
13499
+ const n = parseFloat(input.value);
13500
+ if (!Number.isFinite(n)) {
13501
+ input.value = String(value);
13502
+ return;
13503
+ }
13504
+ const v = Math.min(max, Math.max(min, n));
13505
+ input.value = String(v);
13506
+ value = v;
13507
+ onChange(v);
13508
+ };
13509
+ input.addEventListener("change", commit);
13510
+ input.addEventListener("keydown", (e) => {
13511
+ e.stopPropagation();
13512
+ if (e.key === "Enter") {
13513
+ e.preventDefault();
13514
+ commit();
13515
+ input.blur();
13516
+ }
13517
+ });
13518
+ return input;
13519
+ }
12888
13520
  /** A pick-one dropdown: the trigger shows the current value's glyph (plus an optional inline
12889
13521
  * text label — e.g. `2px` — for controls that would otherwise look alike), and clicking it
12890
13522
  * opens a floating list of every option so a value is one click away (no cycling through). */
@@ -13338,6 +13970,9 @@ var UserDrawingController = class {
13338
13970
  // mirrors showToolbar (drives the gutter reservation)
13339
13971
  this.toolbarCollapsed = false;
13340
13972
  // the bar is a slim expand-strip (narrower gutter)
13973
+ /** Mobile chrome: the docked bar is suppressed (the shell provides its own tool
13974
+ * picker) while `toolbarVisible` keeps the host's INTENT for a later desktop flip. */
13975
+ this.mobileLayout = false;
13341
13976
  /** Self-serve Ctrl+Z / Ctrl+Y as drawing undo/redo. A host that owns a UNIFIED
13342
13977
  * history (drawings + app ops in one timeline) turns this off so the chords
13343
13978
  * bubble to its keymap instead — see the renderer's `historyChords` feature. */
@@ -13396,12 +14031,21 @@ var UserDrawingController = class {
13396
14031
  }
13397
14032
  showToolbar(visible) {
13398
14033
  this.toolbarVisible = visible;
13399
- this.toolbar.setVisible(visible);
14034
+ this.toolbar.setVisible(visible && !this.mobileLayout);
14035
+ this.syncToolbarGutter();
14036
+ }
14037
+ /** Mobile suppresses the docked toolbar (hover flyouts don't work with touch; the
14038
+ * shell's own drawer picks tools instead) and releases its gutter; flipping back
14039
+ * to desktop restores whatever `showToolbar` last asked for. */
14040
+ setLayoutMode(mode) {
14041
+ this.mobileLayout = mode === "mobile";
14042
+ this.toolbar.setVisible(this.toolbarVisible && !this.mobileLayout);
13400
14043
  this.syncToolbarGutter();
13401
14044
  }
13402
14045
  /** The gutter follows the bar's current footprint: hidden 0, collapsed a slim strip, else full width. */
13403
14046
  syncToolbarGutter() {
13404
- this.deps.setToolbarGutter(this.toolbarVisible ? this.toolbarCollapsed ? TOOLBAR_COLLAPSED_WIDTH : TOOLBAR_WIDTH : 0);
14047
+ const shown = this.toolbarVisible && !this.mobileLayout;
14048
+ this.deps.setToolbarGutter(shown ? this.toolbarCollapsed ? TOOLBAR_COLLAPSED_WIDTH : TOOLBAR_WIDTH : 0);
13405
14049
  }
13406
14050
  /** Core push: mirror (or clear) another chart's in-progress placement as a ghost. */
13407
14051
  setExternalGhost(doc) {
@@ -13781,6 +14425,16 @@ var UserDrawingController = class {
13781
14425
  if (this.eraserMode) return "pointer";
13782
14426
  return this.interaction.cursorAt(x, y);
13783
14427
  }
14428
+ /** Right-click while placing: cancel the in-progress drawing and revert to the
14429
+ * pointer — the gesture is an explicit escape, so it disarms even in
14430
+ * stay-in-drawing-mode (where Escape would leave the tool armed). Returns whether
14431
+ * the press was consumed; false lets the host's context menu open normally. */
14432
+ cancelPlacement() {
14433
+ if (!this.interaction.isPlacing()) return false;
14434
+ this.interaction.cancel();
14435
+ if (this.activeTool != null) this.emit({ kind: "arm", type: null });
14436
+ return true;
14437
+ }
13784
14438
  /** Double-click over a drawing → suppress the chart's view reset (single-click already
13785
14439
  * opens settings). Returns true only when a drawing is under the cursor. */
13786
14440
  dblClick(x, y) {
@@ -14270,7 +14924,7 @@ var LUXALGO_WORDMARK_SVG = '<svg viewBox="0 0 964 252" aria-hidden="true"><path
14270
14924
 
14271
14925
  // src/renderers/native/chrome/AttributionMark.ts
14272
14926
  var ATTRIBUTION_URL = "https://luxalgo.com/vela";
14273
- var STYLE_ID5 = "vela-attribution-styles";
14927
+ var STYLE_ID6 = "vela-attribution-styles";
14274
14928
  var CSS = `
14275
14929
  .vela-attribution {
14276
14930
  text-decoration: none;
@@ -14316,12 +14970,28 @@ var CSS = `
14316
14970
  max-width: 240px;
14317
14971
  opacity: 1;
14318
14972
  transform: translateX(0);
14319
- }`;
14320
- function ensureStyles5(doc) {
14321
- let s = doc.getElementById(STYLE_ID5);
14973
+ }
14974
+ /* Mobile: a hair smaller so the mark doesn't dominate a phone-width plot. Keyed on the
14975
+ renderer's own container attribute (data-vela-layout) AND the shell root's
14976
+ (data-layout): the workspace's single grid-wide mark lives OUTSIDE any renderer
14977
+ container, so only the shell attribute reaches it. */
14978
+ [data-vela-layout='mobile'] .vela-attribution .vela-attr-symbol svg,
14979
+ [data-layout='mobile'] .vela-attribution .vela-attr-symbol svg { height: 22px; }
14980
+ [data-vela-layout='mobile'] .vela-attribution .vela-attr-wordmark,
14981
+ [data-layout='mobile'] .vela-attribution .vela-attr-wordmark {
14982
+ top: 2px;
14983
+ transform: translateX(-6px);
14984
+ }
14985
+ [data-vela-layout='mobile'] .vela-attribution .vela-attr-wordmark svg,
14986
+ [data-layout='mobile'] .vela-attribution .vela-attr-wordmark svg { height: 21px; }
14987
+ [data-vela-layout='mobile'] .vela-attribution:hover .vela-attr-wordmark,
14988
+ [data-layout='mobile'] .vela-attribution:hover .vela-attr-wordmark { transform: translateX(0); }
14989
+ `;
14990
+ function ensureStyles6(doc) {
14991
+ let s = doc.getElementById(STYLE_ID6);
14322
14992
  if (!s) {
14323
14993
  s = doc.createElement("style");
14324
- s.id = STYLE_ID5;
14994
+ s.id = STYLE_ID6;
14325
14995
  doc.head.appendChild(s);
14326
14996
  }
14327
14997
  s.textContent = CSS;
@@ -14350,7 +15020,7 @@ function createCustomMark(doc, html, background) {
14350
15020
  return el;
14351
15021
  }
14352
15022
  function createAttributionMark(doc, background) {
14353
- ensureStyles5(doc);
15023
+ ensureStyles6(doc);
14354
15024
  const a = doc.createElement("a");
14355
15025
  a.className = "vela-attribution";
14356
15026
  a.href = ATTRIBUTION_URL;
@@ -14376,6 +15046,174 @@ function createAttributionMark(doc, background) {
14376
15046
  return a;
14377
15047
  }
14378
15048
 
15049
+ // src/renderers/shared/dom-raster.ts
15050
+ function hasInk(color) {
15051
+ if (!color || color === "transparent") return false;
15052
+ const m = /rgba?\(\s*[\d.]+\s*,\s*[\d.]+\s*,\s*[\d.]+\s*,\s*([\d.]+)\s*\)/.exec(color);
15053
+ return m ? parseFloat(m[1]) > 1e-3 : true;
15054
+ }
15055
+ function cornerRadius(cs, w, h) {
15056
+ const raw = cs.borderTopLeftRadius;
15057
+ const v = parseFloat(raw) || 0;
15058
+ const px = raw.endsWith("%") ? v / 100 * Math.min(w, h) : v;
15059
+ return Math.max(0, Math.min(px, Math.min(w, h) / 2));
15060
+ }
15061
+ function tracePath(ctx, x, y, w, h, r) {
15062
+ ctx.beginPath();
15063
+ if (r > 0 && typeof ctx.roundRect === "function") ctx.roundRect(x, y, w, h, r);
15064
+ else ctx.rect(x, y, w, h);
15065
+ }
15066
+ function canDrawUntainted(img) {
15067
+ try {
15068
+ const probe2 = img.ownerDocument.createElement("canvas");
15069
+ probe2.width = 1;
15070
+ probe2.height = 1;
15071
+ const ctx = probe2.getContext("2d");
15072
+ if (!ctx) return false;
15073
+ ctx.drawImage(img, 0, 0, 1, 1);
15074
+ probe2.toDataURL();
15075
+ return true;
15076
+ } catch {
15077
+ return false;
15078
+ }
15079
+ }
15080
+ function rasterizeOverlay(ctx, root, frame) {
15081
+ const win = root.ownerDocument.defaultView;
15082
+ if (!win) return;
15083
+ ctx.save();
15084
+ ctx.scale(frame.dpr, frame.dpr);
15085
+ const drawText = (node, cs, alpha) => {
15086
+ const text = node.textContent?.replace(/\s+/g, " ").trim();
15087
+ if (!text) return;
15088
+ const range = node.ownerDocument.createRange();
15089
+ range.selectNodeContents(node);
15090
+ const r = range.getBoundingClientRect();
15091
+ if (r.width <= 0 || r.height <= 0) return;
15092
+ ctx.globalAlpha = alpha;
15093
+ ctx.fillStyle = cs.color;
15094
+ ctx.font = `${cs.fontStyle} ${cs.fontWeight} ${cs.fontSize} ${cs.fontFamily}`;
15095
+ ctx.textBaseline = "middle";
15096
+ ctx.textAlign = "left";
15097
+ ctx.fillText(text, r.left - frame.left, r.top - frame.top + r.height / 2);
15098
+ };
15099
+ const drawImg = (img, cs, alpha) => {
15100
+ if (!img.complete || img.naturalWidth <= 0 || !canDrawUntainted(img)) return;
15101
+ const r = img.getBoundingClientRect();
15102
+ if (r.width <= 0 || r.height <= 0) return;
15103
+ const x = r.left - frame.left;
15104
+ const y = r.top - frame.top;
15105
+ ctx.save();
15106
+ ctx.globalAlpha = alpha;
15107
+ tracePath(ctx, x, y, r.width, r.height, cornerRadius(cs, r.width, r.height));
15108
+ ctx.clip();
15109
+ ctx.drawImage(img, x, y, r.width, r.height);
15110
+ ctx.restore();
15111
+ };
15112
+ const drawSvgShapes = (parent, alpha) => {
15113
+ for (const el of parent.children) {
15114
+ if (el.hasAttribute("transform")) continue;
15115
+ const tag = el.tagName.toLowerCase();
15116
+ if (tag === "g") {
15117
+ drawSvgShapes(el, alpha);
15118
+ continue;
15119
+ }
15120
+ const num3 = (name) => parseFloat(el.getAttribute(name) ?? "0") || 0;
15121
+ let path = null;
15122
+ if (tag === "path") {
15123
+ const d = el.getAttribute("d");
15124
+ if (d) path = new Path2D(d);
15125
+ } else if (tag === "circle") {
15126
+ path = new Path2D();
15127
+ path.arc(num3("cx"), num3("cy"), num3("r"), 0, Math.PI * 2);
15128
+ } else if (tag === "ellipse") {
15129
+ path = new Path2D();
15130
+ path.ellipse(num3("cx"), num3("cy"), num3("rx"), num3("ry"), 0, 0, Math.PI * 2);
15131
+ } else if (tag === "rect") {
15132
+ path = new Path2D();
15133
+ path.rect(num3("x"), num3("y"), num3("width"), num3("height"));
15134
+ } else if (tag === "line") {
15135
+ path = new Path2D();
15136
+ path.moveTo(num3("x1"), num3("y1"));
15137
+ path.lineTo(num3("x2"), num3("y2"));
15138
+ } else if (tag === "polyline" || tag === "polygon") {
15139
+ const pts = (el.getAttribute("points") ?? "").trim().split(/[\s,]+/).map(Number);
15140
+ if (pts.length < 4 || pts.some(Number.isNaN)) continue;
15141
+ path = new Path2D();
15142
+ path.moveTo(pts[0], pts[1]);
15143
+ for (let i = 2; i + 1 < pts.length; i += 2) path.lineTo(pts[i], pts[i + 1]);
15144
+ if (tag === "polygon") path.closePath();
15145
+ }
15146
+ if (!path) continue;
15147
+ const cs = win.getComputedStyle(el);
15148
+ if (cs.fill && cs.fill !== "none" && hasInk(cs.fill)) {
15149
+ ctx.globalAlpha = alpha * (parseFloat(cs.fillOpacity) || 1);
15150
+ ctx.fillStyle = cs.fill;
15151
+ ctx.fill(path);
15152
+ }
15153
+ const sw = parseFloat(cs.strokeWidth) || 0;
15154
+ if (cs.stroke && cs.stroke !== "none" && hasInk(cs.stroke) && sw > 0) {
15155
+ ctx.globalAlpha = alpha * (parseFloat(cs.strokeOpacity) || 1);
15156
+ ctx.strokeStyle = cs.stroke;
15157
+ ctx.lineWidth = sw;
15158
+ if (cs.strokeLinecap === "round" || cs.strokeLinecap === "square" || cs.strokeLinecap === "butt") ctx.lineCap = cs.strokeLinecap;
15159
+ if (cs.strokeLinejoin === "round" || cs.strokeLinejoin === "bevel" || cs.strokeLinejoin === "miter") ctx.lineJoin = cs.strokeLinejoin;
15160
+ ctx.stroke(path);
15161
+ }
15162
+ }
15163
+ };
15164
+ const drawSvg = (svg, parentAlpha) => {
15165
+ const r = svg.getBoundingClientRect();
15166
+ if (r.width <= 0 || r.height <= 0) return;
15167
+ const cs = win.getComputedStyle(svg);
15168
+ if (cs.display === "none" || cs.visibility === "hidden") return;
15169
+ const alpha = parentAlpha * (parseFloat(cs.opacity) || 1);
15170
+ if (alpha <= 1e-3) return;
15171
+ const vb = svg.viewBox.baseVal;
15172
+ const vw = vb && vb.width > 0 ? vb.width : r.width;
15173
+ const vh = vb && vb.height > 0 ? vb.height : r.height;
15174
+ ctx.save();
15175
+ ctx.translate(r.left - frame.left, r.top - frame.top);
15176
+ ctx.scale(r.width / vw, r.height / vh);
15177
+ if (vb) ctx.translate(-vb.x, -vb.y);
15178
+ drawSvgShapes(svg, alpha);
15179
+ ctx.restore();
15180
+ };
15181
+ const drawElement = (el, parentAlpha) => {
15182
+ const cs = win.getComputedStyle(el);
15183
+ if (cs.display === "none" || cs.visibility === "hidden") return;
15184
+ const alpha = parentAlpha * (parseFloat(cs.opacity) || 1);
15185
+ if (alpha <= 1e-3) return;
15186
+ const r = el.getBoundingClientRect();
15187
+ if (r.width > 0 && r.height > 0) {
15188
+ const x = r.left - frame.left;
15189
+ const y = r.top - frame.top;
15190
+ const radius = cornerRadius(cs, r.width, r.height);
15191
+ if (hasInk(cs.backgroundColor)) {
15192
+ ctx.globalAlpha = alpha;
15193
+ ctx.fillStyle = cs.backgroundColor;
15194
+ tracePath(ctx, x, y, r.width, r.height, radius);
15195
+ ctx.fill();
15196
+ }
15197
+ const bw = parseFloat(cs.borderTopWidth) || 0;
15198
+ if (bw > 0 && cs.borderTopStyle !== "none" && hasInk(cs.borderTopColor)) {
15199
+ ctx.globalAlpha = alpha;
15200
+ ctx.strokeStyle = cs.borderTopColor;
15201
+ ctx.lineWidth = bw;
15202
+ tracePath(ctx, x + bw / 2, y + bw / 2, r.width - bw, r.height - bw, Math.max(0, radius - bw / 2));
15203
+ ctx.stroke();
15204
+ }
15205
+ }
15206
+ for (const child of el.childNodes) {
15207
+ if (child.nodeType === 3) drawText(child, cs, alpha);
15208
+ else if (child instanceof win.HTMLImageElement) drawImg(child, win.getComputedStyle(child), alpha);
15209
+ else if (child instanceof win.SVGSVGElement) drawSvg(child, alpha);
15210
+ else if (child instanceof win.HTMLElement) drawElement(child, alpha);
15211
+ }
15212
+ };
15213
+ if (root instanceof win.HTMLElement) drawElement(root, 1);
15214
+ ctx.restore();
15215
+ }
15216
+
14379
15217
  // src/renderers/native/vpvr/profile.ts
14380
15218
  function buildVpvrProfile(bars, i0, i1, rowCount, valueAreaFrac) {
14381
15219
  const from = Math.max(0, i0);
@@ -14544,12 +15382,13 @@ var AXIS_COL_W = AXIS_MERGED_W;
14544
15382
  var COLLAPSED_PANE_H = 26;
14545
15383
  var TIME_AXIS_H = 22;
14546
15384
  var SCROLL_BTN_SIZE = 26;
15385
+ var SCROLL_BTN_SIZE_TOUCH = 24;
14547
15386
  var SCROLL_BTN_RADIUS = 5;
14548
15387
  var SCROLL_BTN_SHADOW = "0 2px 8px rgba(0,0,0,0.4)";
14549
15388
  var SCROLL_BTN_ICON_PX = 12;
14550
- var SCROLL_BTN_RIGHT_INSET = 25;
15389
+ var SCROLL_BTN_RIGHT_INSET = 12;
14551
15390
  var SCROLL_BTN_RIGHT = RIGHT_AXIS_W + SCROLL_BTN_RIGHT_INSET;
14552
- var SCROLL_BTN_BOTTOM = TIME_AXIS_H + 30;
15391
+ var SCROLL_BTN_BOTTOM = TIME_AXIS_H + 14;
14553
15392
  var SCROLL_BTN_PROXIMITY_PX = 120;
14554
15393
  var MIN_VISIBLE_BARS = 2;
14555
15394
  var ZOOM_OUT_MARGIN_BARS = 6;
@@ -14601,8 +15440,12 @@ var NativeRenderer = class {
14601
15440
  this.symbolPicker = null;
14602
15441
  /** Indicator titles (the legend rows) shown — held here so a remount re-applies it. */
14603
15442
  this.indicatorTitlesOn = true;
15443
+ /** Plot values beside the legend titles shown — held here so a remount re-applies it. */
15444
+ this.indicatorValuesOn = true;
14604
15445
  /** Host-contributed legend actions — held here so a rebuild of the legend re-wires them. */
14605
15446
  this.legendActionsProvider = null;
15447
+ /** Host override of the legend's fold toggle — held here so a remount re-applies it. */
15448
+ this.legendOverviewAction = null;
14606
15449
  // ── keyboard navigation / accessibility (item 11) ──
14607
15450
  this.keyboard = null;
14608
15451
  this.keyboardEnabled = true;
@@ -14707,11 +15550,13 @@ var NativeRenderer = class {
14707
15550
  this.scrollTargetRO = null;
14708
15551
  // eased rightOffset target while gliding back to the latest bars
14709
15552
  // Distance (px) from the plot's bottom edge to the button — tracks the bottom-most EXPANDED
14710
- // pane like the watermark: when the lower sub-panes collapse it rides up into the open pane.
15553
+ // pane: when the lower sub-panes collapse it rides up into the open pane.
14711
15554
  this.scrollBtnBottomPx = SCROLL_BTN_BOTTOM;
14712
15555
  // Distance (px) from the plot's right edge — clears the FULL scale gutter, which widens when a
14713
15556
  // pane carries merged (own-scale) columns; the constant only clears a single-column scale.
14714
15557
  this.scrollBtnRightPx = SCROLL_BTN_RIGHT;
15558
+ // ── chrome size class (pushed by the host shell; see IChartRenderer.setLayoutMode) ──
15559
+ this.layoutMode = "desktop";
14715
15560
  this.viewportCbs = /* @__PURE__ */ new Set();
14716
15561
  this.crosshairCbs = /* @__PURE__ */ new Set();
14717
15562
  this.chartTypeSettingsCbs = /* @__PURE__ */ new Set();
@@ -14722,6 +15567,7 @@ var NativeRenderer = class {
14722
15567
  this.factoryConfig = null;
14723
15568
  this.hostSettingsSections = [];
14724
15569
  this.clickCbs = /* @__PURE__ */ new Set();
15570
+ this.axisLongPressCbs = /* @__PURE__ */ new Set();
14725
15571
  this.inputChangeCbs = /* @__PURE__ */ new Set();
14726
15572
  this.removeIndicatorCbs = /* @__PURE__ */ new Set();
14727
15573
  this.toggleVisibleCbs = /* @__PURE__ */ new Set();
@@ -14729,7 +15575,7 @@ var NativeRenderer = class {
14729
15575
  this.priceStyleCbs = /* @__PURE__ */ new Set();
14730
15576
  this.tableOverlays = /* @__PURE__ */ new Map();
14731
15577
  this.name = "native";
14732
- 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"];
15578
+ 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"];
14733
15579
  /** Track cursor proximity to the scroll button on the plot (bubbles from the button too,
14734
15580
  * so moving onto the button doesn't count as leaving). */
14735
15581
  this.onScrollProximityMove = (e) => {
@@ -14902,6 +15748,11 @@ var NativeRenderer = class {
14902
15748
  this.inputsUI?.setTitlesVisible(this.indicatorTitlesOn);
14903
15749
  return;
14904
15750
  // own DOM, no repaint needed
15751
+ case "indicatorValues":
15752
+ this.indicatorValuesOn = value !== false;
15753
+ this.inputsUI?.setValuesVisible(this.indicatorValuesOn);
15754
+ return;
15755
+ // own DOM, no repaint needed
14905
15756
  case "attribution":
14906
15757
  this.attributionHtml = typeof value === "string" && value.trim() ? value : null;
14907
15758
  this.attributionEnabled = this.attributionHtml !== null || Boolean(value);
@@ -14990,6 +15841,8 @@ var NativeRenderer = class {
14990
15841
  return this.settingsEnabled;
14991
15842
  case "indicatorTitles":
14992
15843
  return this.indicatorTitlesOn;
15844
+ case "indicatorValues":
15845
+ return this.indicatorValuesOn;
14993
15846
  case "attribution":
14994
15847
  return this.attributionHtml ?? this.attributionEnabled;
14995
15848
  case "dialogHost":
@@ -15326,7 +16179,10 @@ var NativeRenderer = class {
15326
16179
  /** Port surface: hosts (bottom-bar / chrome buttons) open the same dialog as the
15327
16180
  * in-chart gear — created on demand, independent of the gear feature being enabled. */
15328
16181
  openSettingsDialog(section) {
15329
- if (!this.settingsDialog && this.plot) this.settingsDialog = new SettingsDialog(this.dialogHost ?? this.plot, this.theme);
16182
+ if (!this.settingsDialog && this.plot) {
16183
+ this.settingsDialog = new SettingsDialog(this.dialogHost ?? this.plot, this.theme);
16184
+ this.settingsDialog.setLayoutMode(this.layoutMode);
16185
+ }
15330
16186
  this.toggleSettingsDialog(section);
15331
16187
  }
15332
16188
  /** (Re)aim the dialog's Canvas → Theme row: the current selection reflects the STABLE
@@ -15415,11 +16271,18 @@ var NativeRenderer = class {
15415
16271
  this.scrollButton.style.color = this.theme.textColor;
15416
16272
  this.scrollButton.style.borderColor = this.theme.borderColor;
15417
16273
  }
15418
- /** Show the button only when the latest bar is scrolled off the right edge AND the cursor is nearby. */
16274
+ /** Show the button only when the latest bar is scrolled off the right edge. On desktop the
16275
+ * cursor must also be nearby (hover reveal); on mobile — no hovering cursor — the button
16276
+ * appears whenever the latest bars are off-screen and hides again once the view is back
16277
+ * at the right edge. */
15419
16278
  updateScrollToRealtimeButton() {
15420
16279
  const btn = this.scrollButton;
15421
16280
  if (!btn) return;
15422
16281
  const latestOffScreen = this.coords.barCount > 0 && this.coords.getViewport().rightOffset < 0;
16282
+ if (this.layoutMode === "mobile") {
16283
+ btn.style.display = latestOffScreen ? "flex" : "none";
16284
+ return;
16285
+ }
15423
16286
  btn.style.display = latestOffScreen && this.pointerNearScrollBtn ? "flex" : "none";
15424
16287
  }
15425
16288
  /** Glide the view back to the most recent bars, keeping the current zoom (barSpacing). */
@@ -15441,17 +16304,20 @@ var NativeRenderer = class {
15441
16304
  this.animator.start();
15442
16305
  }
15443
16306
  /**
15444
- * Export the current chart as a PNG data URL by compositing the geometry (L0)
15445
- * + chrome (L1) + user-drawings (L1.5) canvases onto an offscreen canvas. The
15446
- * background is filled first (the canvas2d data layer is transparent — its bg
15447
- * lives on the wrapper), and a fresh synchronous paint runs first so the WebGL2
15448
- * backend's (non-preserved) drawing buffer is populated before it's read back
15449
- * this tick the same paint also repaints the drawings layers, so they're
15450
- * current (interleaved drawings are already inside the data composite). They're
15451
- * drawn bottom-up in DOM stacking order so the front user drawings (trend lines,
15452
- * boxes, etc.) sit above the Pine drawings on the chrome layer, matching what's
15453
- * on screen. The crosshair (L2) and DOM overlays (tables, legend) are
15454
- * intentionally excluded.
16307
+ * Export the current chart as a PNG data URL by compositing EVERY plot canvas onto
16308
+ * an offscreen canvas, in the same stacking order the DOM shows: plugin layers
16309
+ * below the data, geometry (L0), volume columns (L0.25), the visible-range volume
16310
+ * profile (L0.6), plugin layers above the data, chrome (L1), and user drawings
16311
+ * (L1.5). The background is filled first (the canvas2d data layer is transparent —
16312
+ * its bg lives on the wrapper), and a fresh synchronous paint runs first so the
16313
+ * WebGL2 backend's (non-preserved) drawing buffer is populated before it's read
16314
+ * back this tick the same paint also repaints the drawings layers, so they're
16315
+ * current. DOM chrome joins as a best-effort text/chip raster (see
16316
+ * `rasterizeOverlay`): the per-pane indicator legends, plus any host overlay that
16317
+ * opts in with a `data-vela-screenshot` attribute on the mount container's
16318
+ * subtree (the widget marks its status line, and its symbol watermark with
16319
+ * `"under"` — drawn beneath the canvases, where it sits on screen). Only the
16320
+ * crosshair (L2) is intentionally excluded.
15455
16321
  */
15456
16322
  screenshot() {
15457
16323
  if (!this.dataCanvas) return null;
@@ -15464,9 +16330,25 @@ var NativeRenderer = class {
15464
16330
  if (!ctx) return null;
15465
16331
  ctx.fillStyle = this.theme.background;
15466
16332
  ctx.fillRect(0, 0, out.width, out.height);
15467
- ctx.drawImage(this.dataCanvas, 0, 0);
15468
- ctx.drawImage(this.chromeCanvas, 0, 0);
15469
- ctx.drawImage(this.drawingsCanvas, 0, 0);
16333
+ const plotRect = this.plot?.getBoundingClientRect();
16334
+ const frame = plotRect && plotRect.width > 0 ? { left: plotRect.left, top: plotRect.top, dpr: out.width / plotRect.width } : null;
16335
+ const hostMarked = [...this.mountContainer?.querySelectorAll("[data-vela-screenshot]") ?? []];
16336
+ if (frame) {
16337
+ for (const el of hostMarked) {
16338
+ if (el.getAttribute("data-vela-screenshot") === "under") rasterizeOverlay(ctx, el, frame);
16339
+ }
16340
+ }
16341
+ const below = this.extLayers.filter((l) => l.def.placement === "below-data").map((l) => l.canvas);
16342
+ const above = this.extLayers.filter((l) => l.def.placement !== "below-data").map((l) => l.canvas);
16343
+ for (const canvas of [...below, this.dataCanvas, this.volumeCanvas, this.vpvrCanvas, ...above, this.chromeCanvas, this.drawingsCanvas]) {
16344
+ if (canvas && canvas.width > 0 && canvas.height > 0) ctx.drawImage(canvas, 0, 0);
16345
+ }
16346
+ if (frame) {
16347
+ for (const lg of this.plot?.querySelectorAll("[data-vela-pane]") ?? []) rasterizeOverlay(ctx, lg, frame);
16348
+ for (const el of hostMarked) {
16349
+ if (el.getAttribute("data-vela-screenshot") !== "under") rasterizeOverlay(ctx, el, frame);
16350
+ }
16351
+ }
15470
16352
  return out.toDataURL("image/png");
15471
16353
  }
15472
16354
  /**
@@ -15617,6 +16499,9 @@ var NativeRenderer = class {
15617
16499
  fling: (v) => this.fling(v),
15618
16500
  onPointerMove: (x, y) => this.handlePointerMove(x, y),
15619
16501
  onClick: (x) => this.handleClick(x),
16502
+ onAxisLongPress: (axis, x, y) => {
16503
+ for (const cb of this.axisLongPressCbs) cb({ axis, x, y });
16504
+ },
15620
16505
  beginPriceScale: (x, y) => this.beginPriceScale(x, y),
15621
16506
  priceScaleBy: (dy) => this.priceScaleBy(dy),
15622
16507
  beginPricePan: (y) => this.beginPricePan(y),
@@ -15632,6 +16517,7 @@ var NativeRenderer = class {
15632
16517
  drawingsClaim: (x, y) => this.userDrawings?.claim(x, y) ?? false,
15633
16518
  drawingsMeasureStart: (x, y) => this.userDrawings?.beginMeasureAt(x, y) ?? false,
15634
16519
  drawingsDeleteAt: (x, y) => this.userDrawings?.deleteAt(x, y) ?? false,
16520
+ drawingsCancelPlacement: () => this.userDrawings?.cancelPlacement() ?? false,
15635
16521
  drawingsSnapMode: () => this.snapMode,
15636
16522
  drawingsPointerDown: (x, y, snap, shift) => this.userDrawings?.pointerDown(x, y, snap, shift),
15637
16523
  drawingsPointerMove: (x, y, snap, shift) => this.userDrawings?.pointerMove(x, y, snap, shift),
@@ -15663,6 +16549,14 @@ var NativeRenderer = class {
15663
16549
  setToolbarGutter: (px) => this.setToolbarGutter(px)
15664
16550
  });
15665
16551
  this.userDrawings.historyChords = this.historyChordsEnabled;
16552
+ if (this.layoutMode === "mobile") {
16553
+ this.mountContainer?.setAttribute("data-vela-layout", this.layoutMode);
16554
+ this.userDrawings.setLayoutMode(this.layoutMode);
16555
+ if (this.scrollButton) {
16556
+ this.scrollButton.style.width = `${SCROLL_BTN_SIZE_TOUCH}px`;
16557
+ this.scrollButton.style.height = `${SCROLL_BTN_SIZE_TOUCH}px`;
16558
+ }
16559
+ }
15666
16560
  this.keyboard = new KeyboardController({
15667
16561
  panByBars: (bars) => this.panByBars(bars),
15668
16562
  zoomByStep: (dir) => this.zoomByStep(dir),
@@ -15675,10 +16569,13 @@ var NativeRenderer = class {
15675
16569
  });
15676
16570
  this.setKeyboardEnabled(this.keyboardEnabled);
15677
16571
  this.inputsUI = new InputsUI(this.plot, theme, (paneId) => this.paneBoundsFor(paneId));
16572
+ this.inputsUI.setLayoutMode(this.layoutMode);
15678
16573
  this.inputsUI.setTitlesVisible(this.indicatorTitlesOn);
16574
+ this.inputsUI.setValuesVisible(this.indicatorValuesOn);
15679
16575
  this.inputsUI.setDialogHost(this.dialogHost);
15680
16576
  this.inputsUI.setSymbolPicker(this.symbolPicker);
15681
16577
  this.inputsUI.setLegendActions(this.legendActionsProvider);
16578
+ this.inputsUI.setLegendOverviewAction(this.legendOverviewAction);
15682
16579
  this.inputsUI.setOnChange((c) => {
15683
16580
  for (const cb of this.inputChangeCbs) cb({ indicatorId: c.indicatorId, key: c.key, value: c.value });
15684
16581
  });
@@ -15872,6 +16769,8 @@ var NativeRenderer = class {
15872
16769
  this.attributionEl = null;
15873
16770
  this.mountContainer?.style.removeProperty("--vela-toolbar-gutter");
15874
16771
  this.mountContainer?.style.removeProperty("--vela-scale-gutter");
16772
+ this.mountContainer?.style.removeProperty("--vela-price-pane-top");
16773
+ this.mountContainer?.style.removeProperty("--vela-price-pane-bottom");
15875
16774
  this.mountContainer = null;
15876
16775
  this.wrapper?.remove();
15877
16776
  }
@@ -16093,7 +16992,10 @@ var NativeRenderer = class {
16093
16992
  this.scene.indicators.set(model.id, model);
16094
16993
  this.refreshAnchorOffset(model);
16095
16994
  this.scene.assignIndicatorZ(model.id);
16096
- this.inputsUI.upsert(model.id, model.title, model.inputs, model.inputValues, model.paneId, { native: !!model.native });
16995
+ this.inputsUI.upsert(model.id, model.shorttitle ?? model.title, model.inputs, model.inputValues, model.paneId, {
16996
+ native: !!model.native,
16997
+ ...model.shorttitle ? { settingsTitle: model.title } : {}
16998
+ });
16097
16999
  this.syncTables(model);
16098
17000
  if (model.native?.type === "volume") {
16099
17001
  this.volumeActive = true;
@@ -16154,6 +17056,13 @@ var NativeRenderer = class {
16154
17056
  this.legendActionsProvider = provider;
16155
17057
  this.inputsUI?.setLegendActions(provider);
16156
17058
  }
17059
+ setLegendOverviewAction(action) {
17060
+ this.legendOverviewAction = action;
17061
+ this.inputsUI?.setLegendOverviewAction(action);
17062
+ }
17063
+ openIndicatorSettings(indicatorId) {
17064
+ this.inputsUI?.openSettingsFor(indicatorId);
17065
+ }
16157
17066
  /**
16158
17067
  * Hide/show a mounted indicator. Hiding drops its model from the scene (so every paint path —
16159
17068
  * series, fills, drawings, tables, glow, data window — skips it) while keeping its z key and its
@@ -16207,6 +17116,24 @@ var NativeRenderer = class {
16207
17116
  this.removeIndicatorCbs.add(cb);
16208
17117
  return () => this.removeIndicatorCbs.delete(cb);
16209
17118
  }
17119
+ /** Port surface: the host shell's chrome size class. Mobile switches the renderer
17120
+ * chrome to its touch-first presentation — fullscreen dialogs, no docked drawing
17121
+ * toolbar (the shell provides its own picker), the scroll-to-latest button shown
17122
+ * whenever the latest bars are off-screen (touch has no cursor proximity). */
17123
+ setLayoutMode(mode) {
17124
+ if (mode === this.layoutMode) return;
17125
+ this.layoutMode = mode;
17126
+ this.mountContainer?.setAttribute("data-vela-layout", mode);
17127
+ this.userDrawings?.setLayoutMode(mode);
17128
+ this.settingsDialog?.setLayoutMode(mode);
17129
+ this.inputsUI?.setLayoutMode(mode);
17130
+ if (this.scrollButton) {
17131
+ const px = mode === "mobile" ? SCROLL_BTN_SIZE_TOUCH : SCROLL_BTN_SIZE;
17132
+ this.scrollButton.style.width = `${px}px`;
17133
+ this.scrollButton.style.height = `${px}px`;
17134
+ }
17135
+ this.updateScrollToRealtimeButton();
17136
+ }
16210
17137
  setSettingsSections(sections) {
16211
17138
  this.hostSettingsSections = sections;
16212
17139
  this.settingsDialog?.setHostSections(sections);
@@ -16231,6 +17158,10 @@ var NativeRenderer = class {
16231
17158
  this.clickCbs.add(cb);
16232
17159
  return () => this.clickCbs.delete(cb);
16233
17160
  }
17161
+ onAxisLongPress(cb) {
17162
+ this.axisLongPressCbs.add(cb);
17163
+ return () => this.axisLongPressCbs.delete(cb);
17164
+ }
16234
17165
  onViewportChange(cb) {
16235
17166
  this.viewportCbs.add(cb);
16236
17167
  return () => this.viewportCbs.delete(cb);
@@ -16372,6 +17303,7 @@ var NativeRenderer = class {
16372
17303
  this.skeletonClockMs += dtMs;
16373
17304
  this.paintData();
16374
17305
  this.crosshairLayer.render(this.scene, this.coords, this.theme, this.hoverSeparatorY, this.externalCrossPx());
17306
+ this.updateLegendValues();
16375
17307
  this.emitViewportChange();
16376
17308
  return active;
16377
17309
  }
@@ -16436,7 +17368,7 @@ var NativeRenderer = class {
16436
17368
  this.lastPointer = null;
16437
17369
  this.scheduler.invalidate(1 /* Cursor */);
16438
17370
  this.hoverLogical = null;
16439
- const empty = { time: null, price: null, values: /* @__PURE__ */ new Map(), ohlc: null };
17371
+ const empty = { time: null, price: null, paneKind: null, values: /* @__PURE__ */ new Map(), ohlc: null };
16440
17372
  for (const cb of this.crosshairCbs) cb(empty);
16441
17373
  return;
16442
17374
  }
@@ -16448,7 +17380,7 @@ var NativeRenderer = class {
16448
17380
  const logical = Math.round(this.coords.xToLogical(x));
16449
17381
  const onBar = logical >= 0 && logical < this.coords.barCount;
16450
17382
  const time = onBar ? this.coords.logicalToTime(logical) : null;
16451
- const pane = this.paneAtY(y);
17383
+ const pane = this.paneNodeAtY(y);
16452
17384
  const price = inData && pane ? this.coords.yToPrice(y, pane.scale, pane.bounds) : null;
16453
17385
  const values = /* @__PURE__ */ new Map();
16454
17386
  if (onBar) {
@@ -16465,7 +17397,7 @@ var NativeRenderer = class {
16465
17397
  const bar = onBar ? this.bars[logical] : void 0;
16466
17398
  const ohlc = bar ? { time: bar.time, open: bar.open, high: bar.high, low: bar.low, close: bar.close, volume: bar.volume } : null;
16467
17399
  this.hoverLogical = onBar ? logical : null;
16468
- const event = { time, price, values, ohlc };
17400
+ const event = { time, price, paneKind: inData && pane ? pane.kind : null, values, ohlc };
16469
17401
  for (const cb of this.crosshairCbs) cb(event);
16470
17402
  }
16471
17403
  handleClick(x) {
@@ -16484,6 +17416,20 @@ var NativeRenderer = class {
16484
17416
  }
16485
17417
  return null;
16486
17418
  }
17419
+ /** The pane whose bounds sit closest to `y` — the forgiving fallback for points that
17420
+ * fall BETWEEN bounds (separator gaps, the strip under the last pane). */
17421
+ nearestPaneToY(y) {
17422
+ let best = null;
17423
+ let bestDist = Infinity;
17424
+ for (const pane of this.scene.panes.values()) {
17425
+ const d = y < pane.bounds.top ? pane.bounds.top - y : y - (pane.bounds.top + pane.bounds.height);
17426
+ if (d < bestDist) {
17427
+ bestDist = d;
17428
+ best = pane;
17429
+ }
17430
+ }
17431
+ return best;
17432
+ }
16487
17433
  // ── manual vertical scaling (price-axis drag + vertical price pan) ──
16488
17434
  /** The scale window a gesture targets: a pane's master scale, or a merged indicator's
16489
17435
  * own scale column (resolved from the grabbed x). Both share the four scale fields. */
@@ -16546,7 +17492,7 @@ var NativeRenderer = class {
16546
17492
  * still fits the view.)
16547
17493
  */
16548
17494
  dataDblClick(_x, y) {
16549
- const pane = this.paneNodeAtY(y);
17495
+ const pane = this.paneNodeAtY(y) ?? this.nearestPaneToY(y);
16550
17496
  if (!pane) return;
16551
17497
  const maximized = this.maximizedPaneId !== pane.id;
16552
17498
  this.maximizedPaneId = maximized ? pane.id : null;
@@ -16787,29 +17733,63 @@ var NativeRenderer = class {
16787
17733
  dataWindowGroups(idx, pricePane) {
16788
17734
  const groups = [];
16789
17735
  for (const model of this.scene.indicators.values()) {
16790
- const pane = (model.paneId ? this.scene.panes.get(model.paneId) : null) ?? pricePane;
16791
- const off = this.scene.offsetOf(model.id);
16792
- const rows = [];
16793
- for (const s of model.series) {
16794
- let value;
16795
- let color;
16796
- if (s.kind === "candle" || s.kind === "bar") {
16797
- value = s.bars[idx - off]?.close;
16798
- color = s.style?.up ?? this.theme.upColor;
16799
- } else if (isLineLikeSeries(s)) {
16800
- if (s.visible === false) continue;
16801
- value = s.points[idx - off]?.value;
16802
- color = s.points[idx - off]?.color ?? s.style.color;
16803
- } else {
16804
- continue;
16805
- }
16806
- if (value == null || !Number.isFinite(value)) continue;
16807
- rows.push({ label: s.title || model.title, value: this.dataWindowFmt(value, pane), color });
16808
- }
17736
+ const rows = this.dataWindowRowsFor(model, idx, pricePane);
16809
17737
  if (rows.length) groups.push({ name: model.title, rows });
16810
17738
  }
16811
17739
  return groups;
16812
17740
  }
17741
+ /** One indicator's readout at bar `idx`: a row per drawable plot, formatted on its pane's scale. */
17742
+ dataWindowRowsFor(model, idx, pricePane) {
17743
+ if (model.native?.type === "volume") return this.volumeReadoutRows(model, idx);
17744
+ const pane = (model.paneId ? this.scene.panes.get(model.paneId) : null) ?? pricePane;
17745
+ const off = this.scene.offsetOf(model.id);
17746
+ const rows = [];
17747
+ for (const s of model.series) {
17748
+ let value;
17749
+ let color;
17750
+ if (s.kind === "candle" || s.kind === "bar") {
17751
+ value = s.bars[idx - off]?.close;
17752
+ color = s.style?.up ?? this.theme.upColor;
17753
+ } else if (isLineLikeSeries(s)) {
17754
+ if (s.visible === false) continue;
17755
+ value = s.points[idx - off]?.value;
17756
+ color = s.points[idx - off]?.color ?? s.style.color;
17757
+ } else {
17758
+ continue;
17759
+ }
17760
+ if (value == null || !Number.isFinite(value)) continue;
17761
+ rows.push({ label: s.title || model.title, value: this.dataWindowFmt(value, pane), color });
17762
+ }
17763
+ return rows;
17764
+ }
17765
+ /** The volume indicator's readout: the bar's volume, tinted with the layer's own direction
17766
+ * colors. Hiding volume keeps its model in the scene (only the layer is suppressed — see
17767
+ * `setIndicatorVisible`), so the hidden flag is checked here rather than by model absence. */
17768
+ volumeReadoutRows(model, idx) {
17769
+ if (this.volumeHidden) return [];
17770
+ const bar = this.bars[idx];
17771
+ const vol = bar?.volume;
17772
+ if (bar == null || vol == null || !Number.isFinite(vol)) return [];
17773
+ const cfg = this.scene.volumeLayer;
17774
+ const color = bar.close >= bar.open ? cfg?.upColor ?? this.theme.upColor : cfg?.downColor ?? this.theme.downColor;
17775
+ return [{ label: model.title, value: formatVolume(vol), color }];
17776
+ }
17777
+ /** Refresh the plot values beside every legend title — the same readout the data window
17778
+ * shows (crosshair bar, else the latest bar), pushed per paint. A hidden indicator has
17779
+ * no scene model, so its row is absent from the map and its readout clears. */
17780
+ updateLegendValues() {
17781
+ if (!this.inputsUI) return;
17782
+ const n = this.bars.length;
17783
+ const values = /* @__PURE__ */ new Map();
17784
+ if (n > 0) {
17785
+ const idx = this.hoverLogical != null ? this.hoverLogical : n - 1;
17786
+ const pricePane = this.dataWindowPricePane();
17787
+ for (const model of this.scene.indicators.values()) {
17788
+ values.set(model.id, this.dataWindowRowsFor(model, idx, pricePane).map((r) => ({ value: r.value, color: r.color })));
17789
+ }
17790
+ }
17791
+ this.inputsUI.setPlotValues(values);
17792
+ }
16813
17793
  renderFrame(level) {
16814
17794
  if (this.animator.active) return;
16815
17795
  if (repaintsData(level)) {
@@ -16821,6 +17801,7 @@ var NativeRenderer = class {
16821
17801
  }
16822
17802
  this.crosshairLayer.render(this.scene, this.coords, this.theme, this.hoverSeparatorY, this.externalCrossPx());
16823
17803
  if (!repaintsData(level) && this.paintedData) this.repaintCursorLayers();
17804
+ this.updateLegendValues();
16824
17805
  }
16825
17806
  /** Repaint the SDK layers that opted into cursor tracking (their own canvas only). */
16826
17807
  repaintCursorLayers() {
@@ -16879,19 +17860,18 @@ var NativeRenderer = class {
16879
17860
  theme: this.theme
16880
17861
  });
16881
17862
  const nowMs = typeof performance !== "undefined" ? performance.now() : Date.now();
17863
+ let folded = null;
16882
17864
  for (const l of this.extLayers) {
16883
17865
  const args = this.extLayerArgs(l.def.id, pane.scale, pane.bounds, nowMs);
16884
17866
  l.instance.render(args);
16885
- if (l.def.id === this.scene.priceStyle && l.instance.modulateBase) {
16886
- const mod = l.instance.modulateBase(args);
16887
- if (mod) {
16888
- if (mod.candleBodyScale != null) this.backend.candleBodyScale = clamp012(mod.candleBodyScale) || 0.01;
16889
- if (mod.candleBodyAlpha != null) this.backend.candleBodyAlpha = clamp012(mod.candleBodyAlpha) * this.candleBodyAlpha;
16890
- if (mod.gridAlpha != null) this.backend.gridAlpha = clamp012(mod.gridAlpha);
16891
- }
16892
- }
17867
+ folded = foldBaseModulation(folded, l.instance.modulateBase?.(args) ?? null);
16893
17868
  if (this.animZoom && l.instance.animating?.()) this.animator.start();
16894
17869
  }
17870
+ if (folded) {
17871
+ if (folded.candleBodyScale != null) this.backend.candleBodyScale = clamp012(folded.candleBodyScale) || 0.01;
17872
+ if (folded.candleBodyAlpha != null) this.backend.candleBodyAlpha = clamp012(folded.candleBodyAlpha) * this.candleBodyAlpha;
17873
+ if (folded.gridAlpha != null) this.backend.gridAlpha = clamp012(folded.gridAlpha);
17874
+ }
16895
17875
  }
16896
17876
  const li = this.bars.length - 1;
16897
17877
  const liveActual = li >= 0 ? this.bars[li] : void 0;
@@ -16969,7 +17949,7 @@ var NativeRenderer = class {
16969
17949
  if (!Number.isFinite(y) || y < pricePane.bounds.top || y > pricePane.bounds.top + pricePane.bounds.height) y = null;
16970
17950
  }
16971
17951
  }
16972
- return { x, y, time: this.coords.logicalToTime(logical) };
17952
+ return { x, y, time: this.coords.logicalToTime(logical), price: y != null ? ext.price : null };
16973
17953
  }
16974
17954
  /** Set the sticky magnet mode (called by the drawings toolbar's 3-state button). */
16975
17955
  setSnapMode(mode) {
@@ -17283,6 +18263,7 @@ var NativeRenderer = class {
17283
18263
  this.paneControls?.reposition();
17284
18264
  this.repositionScrollButton();
17285
18265
  this.positionAttribution();
18266
+ this.publishPricePaneBounds();
17286
18267
  return;
17287
18268
  }
17288
18269
  const collapsed = panes.filter((p) => p.collapsed);
@@ -17300,9 +18281,10 @@ var NativeRenderer = class {
17300
18281
  this.paneControls?.reposition();
17301
18282
  this.repositionScrollButton();
17302
18283
  this.positionAttribution();
18284
+ this.publishPricePaneBounds();
17303
18285
  }
17304
- /** Pin the scroll-to-realtime button above the bottom-most EXPANDED pane's data area (like the
17305
- * watermark): with all lower sub-panes collapsed it settles into the lowest open pane. */
18286
+ /** Pin the scroll-to-realtime button above the bottom-most EXPANDED pane's data area:
18287
+ * with all lower sub-panes collapsed it settles into the lowest open pane. */
17306
18288
  repositionScrollButton() {
17307
18289
  const dataHeight = this.coords.height;
17308
18290
  if (dataHeight <= 0) return;
@@ -17346,6 +18328,21 @@ var NativeRenderer = class {
17346
18328
  this.mountContainer?.style.setProperty("--vela-toolbar-gutter", `${this.toolbarGutter}px`);
17347
18329
  this.mountContainer?.style.setProperty("--vela-scale-gutter", `${this.rightAxisW}px`);
17348
18330
  }
18331
+ /** Publish the price pane's vertical insets as `--vela-price-pane-top` /
18332
+ * `--vela-price-pane-bottom` on the mount container, so the symbol watermark
18333
+ * (and any other host overlay) can clip to the price pane instead of spanning
18334
+ * study panes and the time axis. A maximized study pane collapses the box to
18335
+ * zero height — the mark does not appear on a study. */
18336
+ publishPricePaneBounds() {
18337
+ if (!this.mountContainer) return;
18338
+ const plotH = this.coords.height + TIME_AXIS_H;
18339
+ const price = this.scene.panes.get(PRICE_PANE_ID);
18340
+ const top = price ? price.bounds.top : 0;
18341
+ const height = price ? price.bounds.height : this.coords.height;
18342
+ const bottom = Math.max(0, plotH - (top + height));
18343
+ this.mountContainer.style.setProperty("--vela-price-pane-top", `${top}px`);
18344
+ this.mountContainer.style.setProperty("--vela-price-pane-bottom", `${bottom}px`);
18345
+ }
17349
18346
  /** The built-in mark, or the host's own when one is set. */
17350
18347
  buildAttributionEl() {
17351
18348
  return this.attributionHtml !== null ? createCustomMark(document, this.attributionHtml, this.theme.background) : createAttributionMark(document, this.theme.background);
@@ -17739,6 +18736,9 @@ var EngineOrchestrator = class _EngineOrchestrator {
17739
18736
  this.barTransform = null;
17740
18737
  // ── chart-type data engines (SDK): one lazily-created engine per style id ──
17741
18738
  this.typeEngines = /* @__PURE__ */ new Map();
18739
+ /** Last-seen chart-type settings per type id (dialog edits + persisted restores) —
18740
+ * replayed into a data engine whenever one is (re)created. */
18741
+ this.typeSettings = /* @__PURE__ */ new Map();
17742
18742
  /** The style id whose data engine is currently ACTIVE (drives suspend/resume + pokes). */
17743
18743
  this.activeEngineStyle = null;
17744
18744
  /** The chart's current price style (tracked from the renderer's change events + initial read). */
@@ -17801,7 +18801,10 @@ var EngineOrchestrator = class _EngineOrchestrator {
17801
18801
  this.renderer.onRemoveIndicator((id) => this.removeIndicator(id));
17802
18802
  this.renderer.onToggleIndicatorVisible?.((id, visible) => this.setVisible(id, visible));
17803
18803
  this.paneActionUnsub = this.renderer.onPaneAction?.((a) => this.handlePaneAction(a)) ?? null;
17804
- this.renderer.onChartTypeSettingsChange?.((typeId, values) => this.typeEngines.get(typeId)?.onSettings?.(values));
18804
+ this.renderer.onChartTypeSettingsChange?.((typeId, values) => {
18805
+ this.typeSettings.set(typeId, values);
18806
+ this.typeEngines.get(typeId)?.onSettings?.(values);
18807
+ });
17805
18808
  this.renderer.onMoveIndicator?.((id, target) => this.moveIndicator(id, target));
17806
18809
  this.renderer.onThemeSelect?.((name) => this.setTheme(resolveTheme(name)));
17807
18810
  this.viewportUnsub = this.renderer.onViewportChange((range) => this.onViewportChange(range));
@@ -18289,6 +19292,8 @@ var EngineOrchestrator = class _EngineOrchestrator {
18289
19292
  if (!def?.dataEngine) return;
18290
19293
  const engine = def.dataEngine();
18291
19294
  this.typeEngines.set(id, engine);
19295
+ const stored = this.typeSettings.get(id);
19296
+ if (stored) engine.onSettings?.(stored);
18292
19297
  engine.start({
18293
19298
  symbol: this.config.market.symbol ?? "TEST",
18294
19299
  timeframe: this.config.market.timeframe ?? "60",
@@ -18760,6 +19765,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
18760
19765
  return {
18761
19766
  id: record.id,
18762
19767
  title: record.title,
19768
+ ...d.shortTitle ? { shorttitle: d.shortTitle } : {},
18763
19769
  overlay: d.overlay,
18764
19770
  paneHint: d.paneHint,
18765
19771
  native: { type: record.native.type },
@@ -19367,7 +20373,8 @@ var VpvrIndicator = class {
19367
20373
  };
19368
20374
  var vpvrDescriptor = {
19369
20375
  type: "vpvr",
19370
- title: "VPVR",
20376
+ title: "Visible Range Volume Profile",
20377
+ shortTitle: "VRVP",
19371
20378
  paneHint: "price",
19372
20379
  overlay: true,
19373
20380
  inputsSchema: () => [
@@ -19724,4 +20731,4 @@ function resolveElement(container) {
19724
20731
  return element;
19725
20732
  }
19726
20733
 
19727
- export { BarStore, CachingDataFeed, DARK_THEME, DataControl, DrawingStore, DrawingToolbar, DrawingsControl, LIGHT_THEME, MultiProviderFeed, NativeRenderer, RendererControl, TIMEZONES, TypedEventBus, Vela, applyAttributionMarkTheme, buildToolbar, createAttributionMark, createCustomMark, defaultToolbar, normalizeTimezone, parseSymbol, priceStyleIds, registerBuiltinChartTypes, resolveTheme, sharedBarStore, timeframeToMs, tzButtonLabel, tzMenuLabel, tzOffset };
20734
+ export { BUILTIN_PRICE_STYLES, BarStore, CachingDataFeed, DARK_THEME, DataControl, DrawingStore, DrawingToolbar, DrawingsControl, LIGHT_THEME, MultiProviderFeed, NativeRenderer, RendererControl, TIMEZONES, TypedEventBus, Vela, applyAttributionMarkTheme, buildToolbar, createAttributionMark, createCustomMark, defaultToolbar, normalizeTimezone, parseSymbol, priceStyleIds, registerBuiltinChartTypes, resolveTheme, sharedBarStore, timeframeToMs, tzButtonLabel, tzMenuLabel, tzOffset };