@luxalgo/vela 0.5.1 → 0.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/dist/{DataProvider-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-S6F6SQ2L.js → chunk-B5TIKQJ5.js} +60 -7
  5. package/dist/{chunk-EP6NDJYK.js → chunk-LJLZR44Y.js} +4 -2
  6. package/dist/{chunk-AIW3EJUE.js → chunk-P556H6EA.js} +329 -10
  7. package/dist/{chunk-FNOMKHEZ.js → chunk-RRFGWZNJ.js} +1321 -363
  8. package/dist/{chunk-5SE4PL3H.js → chunk-Y7TK4ZWK.js} +1363 -48
  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 +1374 -366
  14. package/dist/index.d.cts +52 -19
  15. package/dist/index.d.ts +52 -19
  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-CnNd7cuF.d.ts} +172 -25
  20. package/dist/{plugin-O34MbXf-.d.cts → plugin-DfH4Y-Om.d.cts} +172 -25
  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 +1374 -366
  36. package/dist/vela.global.min.js +84 -34
  37. package/dist/widget.cjs +3348 -550
  38. package/dist/widget.d.cts +65 -7
  39. package/dist/widget.d.ts +65 -7
  40. package/dist/widget.js +169 -18
  41. package/dist/workspace.cjs +4251 -1400
  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, 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-B5TIKQJ5.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,6 +6963,11 @@ 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
6972
  var WHEEL_ZOOM_K = 25e-4;
6754
6973
  function wheelZoomAnchor(coords, cursorX, rightEdge) {
@@ -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;
@@ -6802,6 +7027,23 @@ var InputController = class {
6802
7027
  // press/release re-shape the drawings preview with the pointer stationary.
6803
7028
  this.cursorX = NaN;
6804
7029
  this.cursorY = NaN;
7030
+ // ── touch state ──
7031
+ /** Live touch contacts (local coords) — two entries drive a pinch; extras are ignored. */
7032
+ this.touches = /* @__PURE__ */ new Map();
7033
+ this.lpTimer = null;
7034
+ this.pinchStartDist = 1;
7035
+ this.pinchStartBarSpacing = 0;
7036
+ this.pinchAnchorLogical = 0;
7037
+ // Last clean touch tap (for double-tap pairing). Time 0 = no pending first tap.
7038
+ this.lastTapT = 0;
7039
+ this.lastTapX = 0;
7040
+ this.lastTapY = 0;
7041
+ this.onTouchContextMenu = (e) => {
7042
+ if (this.touches.size === 0) return;
7043
+ if (!(e.target instanceof Node) || !this.el?.contains(e.target)) return;
7044
+ e.preventDefault();
7045
+ e.stopImmediatePropagation();
7046
+ };
6805
7047
  this.onMiddleGuard = (e) => {
6806
7048
  if (e.button === 1 && this.middleDeleted) e.preventDefault();
6807
7049
  };
@@ -6823,6 +7065,15 @@ var InputController = class {
6823
7065
  }
6824
7066
  if (e.button !== 0) return;
6825
7067
  const { x, y } = this.local(e);
7068
+ if (e.pointerType === "touch") {
7069
+ if (this.touches.size === 1 && this.dragging && (this.region === "data" || this.region === "crosshair")) {
7070
+ this.touches.set(e.pointerId, { x, y });
7071
+ this.beginPinch(e);
7072
+ return;
7073
+ }
7074
+ if (this.touches.size >= 1) return;
7075
+ this.touches.set(e.pointerId, { x, y });
7076
+ }
6826
7077
  this.deps.drawingsClearTransient?.();
6827
7078
  this.dragging = true;
6828
7079
  this.moved = false;
@@ -6848,6 +7099,9 @@ var InputController = class {
6848
7099
  if (this.region === "price") this.deps.beginPriceScale(x, y);
6849
7100
  else if (this.region === "separator") this.deps.beginPaneResize(y);
6850
7101
  else if (this.region === "data") this.verticalPan = this.deps.beginPricePan(y);
7102
+ if (e.pointerType === "touch" && (this.region === "data" || this.region === "price" || this.region === "time")) {
7103
+ this.startLongPress(x, y, this.region);
7104
+ }
6851
7105
  this.lastX = x;
6852
7106
  this.lastT = e.timeStamp;
6853
7107
  this.vx = 0;
@@ -6857,28 +7111,42 @@ var InputController = class {
6857
7111
  const { x, y } = this.local(e);
6858
7112
  this.cursorX = x;
6859
7113
  this.cursorY = y;
7114
+ if (e.pointerType === "touch" && this.touches.has(e.pointerId)) this.touches.set(e.pointerId, { x, y });
7115
+ if (this.dragging && this.region === "pinch") {
7116
+ this.applyPinch();
7117
+ return;
7118
+ }
7119
+ if (this.dragging && this.region === "crosshair") {
7120
+ this.deps.onPointerMove(x, y);
7121
+ return;
7122
+ }
7123
+ if (this.dragging && this.region === "axis-sheet") {
7124
+ return;
7125
+ }
7126
+ if (this.lpTimer !== null && Math.hypot(x - this.startX, y - this.startY) > TOUCH_SLOP) this.cancelLongPress();
7127
+ const slop = e.pointerType === "touch" ? TOUCH_SLOP : DRAG_SLOP;
6860
7128
  if (this.dragging) {
6861
7129
  if (this.region === "price") {
6862
- if (Math.abs(y - this.startY) > DRAG_SLOP) this.moved = true;
7130
+ if (Math.abs(y - this.startY) > slop) this.moved = true;
6863
7131
  this.deps.priceScaleBy(y - this.startY);
6864
7132
  } else if (this.region === "separator") {
6865
- if (Math.abs(y - this.startY) > DRAG_SLOP) this.moved = true;
7133
+ if (Math.abs(y - this.startY) > slop) this.moved = true;
6866
7134
  this.deps.paneResizeBy(y - this.startY);
6867
7135
  } else if (this.region === "time") {
6868
- if (Math.abs(x - this.startX) > DRAG_SLOP) this.moved = true;
7136
+ if (Math.abs(x - this.startX) > slop) this.moved = true;
6869
7137
  const barSpacing = clampBarSpacing(this.startBarSpacing * Math.exp((this.startX - x) * TIME_SCALE_K));
6870
7138
  this.deps.apply({ barSpacing, rightOffset: this.startRightOffset });
6871
7139
  } else if (this.region === "drawing") {
6872
- if (Math.abs(x - this.startX) > DRAG_SLOP || Math.abs(y - this.startY) > DRAG_SLOP) this.moved = true;
7140
+ if (Math.abs(x - this.startX) > slop || Math.abs(y - this.startY) > slop) this.moved = true;
6873
7141
  this.deps.drawingsPointerMove?.(x, y, this.snapMode(e), e.shiftKey);
6874
7142
  } else {
6875
7143
  const coords = this.deps.getCoords();
6876
7144
  const vp = coords.getViewport();
6877
7145
  const dx = x - this.startX;
6878
- if (Math.abs(dx) > DRAG_SLOP) this.moved = true;
7146
+ if (Math.abs(dx) > slop) this.moved = true;
6879
7147
  this.deps.apply({ barSpacing: vp.barSpacing, rightOffset: this.startRightOffset - dx / coords.pxPerBar() });
6880
7148
  if (this.verticalPan) {
6881
- if (Math.abs(y - this.startY) > DRAG_SLOP) this.moved = true;
7149
+ if (Math.abs(y - this.startY) > slop) this.moved = true;
6882
7150
  this.deps.pricePanBy(y - this.startY);
6883
7151
  }
6884
7152
  const dt = e.timeStamp - this.lastT;
@@ -6895,14 +7163,44 @@ var InputController = class {
6895
7163
  this.el.style.cursor = drawCursor ?? (r === "price" ? "ns-resize" : r === "time" ? "ew-resize" : r === "separator" ? "row-resize" : "");
6896
7164
  this.deps.drawingsPointerMove?.(x, y, this.snapMode(e), e.shiftKey);
6897
7165
  }
6898
- this.deps.onPointerMove(x, y);
7166
+ if (e.pointerType !== "touch") this.deps.onPointerMove(x, y);
6899
7167
  };
6900
7168
  this.onUp = (e) => {
7169
+ if (e.pointerType === "touch") this.touches.delete(e.pointerId);
7170
+ this.cancelLongPress();
7171
+ if (this.dragging && this.region === "pinch") {
7172
+ if (this.touches.size === 1) {
7173
+ const rest = [...this.touches.values()][0];
7174
+ const vp = this.deps.getCoords().getViewport();
7175
+ this.region = "data";
7176
+ this.verticalPan = false;
7177
+ this.startX = rest.x;
7178
+ this.startY = rest.y;
7179
+ this.startRightOffset = vp.rightOffset;
7180
+ this.startBarSpacing = vp.barSpacing;
7181
+ this.lastX = rest.x;
7182
+ this.lastT = e.timeStamp;
7183
+ this.vx = 0;
7184
+ return;
7185
+ }
7186
+ this.endGesture(e);
7187
+ return;
7188
+ }
7189
+ if (this.dragging && this.region === "crosshair") {
7190
+ this.deps.onPointerMove(null, null);
7191
+ this.endGesture(e);
7192
+ return;
7193
+ }
7194
+ if (this.dragging && this.region === "axis-sheet") {
7195
+ this.endGesture(e);
7196
+ return;
7197
+ }
7198
+ const { x, y } = this.local(e);
7199
+ const wasTouch = e.pointerType === "touch";
7200
+ const tapRelease = this.dragging && !this.moved && (!wasTouch || Math.hypot(x - this.startX, y - this.startY) <= TOUCH_TAP_SLOP);
6901
7201
  if (this.dragging && this.region === "drawing") {
6902
- const { x, y } = this.local(e);
6903
7202
  this.deps.drawingsPointerUp?.(x, y);
6904
- } else if (this.dragging && !this.moved && this.region === "data") {
6905
- const { x, y } = this.local(e);
7203
+ } else if (tapRelease && this.region === "data") {
6906
7204
  this.deps.onClick(x, y);
6907
7205
  } else if (this.dragging && this.region === "data") {
6908
7206
  const stale = e.timeStamp - this.lastT > FLING_STALE_MS;
@@ -6911,11 +7209,20 @@ var InputController = class {
6911
7209
  this.deps.fling(-this.vx / pitch);
6912
7210
  }
6913
7211
  }
6914
- this.dragging = false;
6915
- try {
6916
- this.el?.releasePointerCapture(e.pointerId);
6917
- } catch {
6918
- }
7212
+ const wasTap = wasTouch && tapRelease;
7213
+ this.endGesture(e);
7214
+ if (wasTouch) this.deps.onPointerMove(null, null);
7215
+ if (wasTap) this.registerTap(e.timeStamp, x, y);
7216
+ };
7217
+ /** The browser took the pointer back (system gesture, palm rejection, tab switch):
7218
+ * drop the gesture where it is — no click, no fling, no stuck `dragging`. */
7219
+ this.onCancel = (e) => {
7220
+ if (e.pointerType === "touch") this.touches.delete(e.pointerId);
7221
+ this.cancelLongPress();
7222
+ if (!this.dragging) return;
7223
+ if (this.region === "drawing" && !Number.isNaN(this.cursorX)) this.deps.drawingsPointerUp?.(this.cursorX, this.cursorY);
7224
+ if (this.region === "crosshair" || e.pointerType === "touch") this.deps.onPointerMove(null, null);
7225
+ this.endGesture(e);
6919
7226
  };
6920
7227
  this.onLeave = () => {
6921
7228
  this.cursorX = NaN;
@@ -6924,12 +7231,7 @@ var InputController = class {
6924
7231
  };
6925
7232
  this.onDblClick = (e) => {
6926
7233
  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);
7234
+ this.doubleActivate(x, y);
6933
7235
  };
6934
7236
  this.onWheel = (e) => {
6935
7237
  e.preventDefault();
@@ -6950,10 +7252,17 @@ var InputController = class {
6950
7252
  }
6951
7253
  attach(el) {
6952
7254
  this.el = el;
7255
+ if (el.style !== void 0) {
7256
+ el.style.touchAction = "none";
7257
+ el.style.userSelect = "none";
7258
+ el.style.setProperty("-webkit-user-select", "none");
7259
+ el.style.setProperty("-webkit-touch-callout", "none");
7260
+ }
6953
7261
  el.addEventListener("pointerdown", this.onDown);
6954
7262
  el.addEventListener("pointermove", this.onMove);
6955
7263
  el.addEventListener("pointerup", this.onUp);
6956
7264
  el.addEventListener("pointerleave", this.onLeave);
7265
+ el.addEventListener("pointercancel", this.onCancel);
6957
7266
  el.addEventListener("dblclick", this.onDblClick);
6958
7267
  el.addEventListener("wheel", this.onWheel, { passive: false });
6959
7268
  el.addEventListener("mousedown", this.onMiddleGuard);
@@ -6961,6 +7270,7 @@ var InputController = class {
6961
7270
  const win = el.ownerDocument?.defaultView;
6962
7271
  win?.addEventListener("keydown", this.onModifier);
6963
7272
  win?.addEventListener("keyup", this.onModifier);
7273
+ win?.addEventListener("contextmenu", this.onTouchContextMenu, true);
6964
7274
  }
6965
7275
  detach() {
6966
7276
  const el = this.el;
@@ -6968,14 +7278,18 @@ var InputController = class {
6968
7278
  const win = el.ownerDocument?.defaultView;
6969
7279
  win?.removeEventListener("keydown", this.onModifier);
6970
7280
  win?.removeEventListener("keyup", this.onModifier);
7281
+ win?.removeEventListener("contextmenu", this.onTouchContextMenu, true);
6971
7282
  el.removeEventListener("pointerdown", this.onDown);
6972
7283
  el.removeEventListener("pointermove", this.onMove);
6973
7284
  el.removeEventListener("pointerup", this.onUp);
6974
7285
  el.removeEventListener("pointerleave", this.onLeave);
7286
+ el.removeEventListener("pointercancel", this.onCancel);
6975
7287
  el.removeEventListener("dblclick", this.onDblClick);
6976
7288
  el.removeEventListener("wheel", this.onWheel);
6977
7289
  el.removeEventListener("mousedown", this.onMiddleGuard);
6978
7290
  el.removeEventListener("auxclick", this.onMiddleGuard);
7291
+ this.cancelLongPress();
7292
+ this.touches.clear();
6979
7293
  this.el = null;
6980
7294
  }
6981
7295
  local(e) {
@@ -6999,6 +7313,59 @@ var InputController = class {
6999
7313
  snapMode(e) {
7000
7314
  return effectiveSnapMode(e.ctrlKey || e.metaKey, this.deps.drawingsSnapMode?.() ?? "off");
7001
7315
  }
7316
+ /** Arm the long-press timer for a data-area → crosshair or axis → sheet hold;
7317
+ * movement past TOUCH_SLOP or a second finger cancels it. */
7318
+ startLongPress(x, y, region) {
7319
+ this.cancelLongPress();
7320
+ this.lpTimer = setTimeout(() => {
7321
+ this.lpTimer = null;
7322
+ if (!this.dragging || this.region !== region) return;
7323
+ if (region === "data") {
7324
+ this.region = "crosshair";
7325
+ this.deps.onPointerMove(x, y);
7326
+ return;
7327
+ }
7328
+ this.region = "axis-sheet";
7329
+ this.moved = true;
7330
+ this.deps.onAxisLongPress?.(region, x, y);
7331
+ }, LONG_PRESS_MS);
7332
+ }
7333
+ cancelLongPress() {
7334
+ if (this.lpTimer !== null) clearTimeout(this.lpTimer);
7335
+ this.lpTimer = null;
7336
+ }
7337
+ /** Two fingers down: freeze the view and re-base the gesture as an anchored pinch. */
7338
+ beginPinch(e) {
7339
+ this.cancelLongPress();
7340
+ if (this.region === "crosshair") this.deps.onPointerMove(null, null);
7341
+ const coords = this.deps.getCoords();
7342
+ const vp = coords.getViewport();
7343
+ this.deps.apply(vp);
7344
+ const [a, b] = [...this.touches.values()];
7345
+ this.pinchStartDist = Math.max(1, Math.hypot(a.x - b.x, a.y - b.y));
7346
+ this.pinchStartBarSpacing = vp.barSpacing;
7347
+ this.pinchAnchorLogical = coords.xToLogical((a.x + b.x) / 2);
7348
+ this.region = "pinch";
7349
+ this.dragging = true;
7350
+ this.moved = true;
7351
+ this.vx = 0;
7352
+ this.capture(e.pointerId);
7353
+ }
7354
+ /** Track both fingers: distance ratio zooms, the live midpoint pans (anchor pinned under it). */
7355
+ applyPinch() {
7356
+ const coords = this.deps.getCoords();
7357
+ const pts = [...this.touches.values()];
7358
+ if (pts.length < 2) return;
7359
+ const [a, b] = pts;
7360
+ const barSpacing = pinchBarSpacing(this.pinchStartBarSpacing, this.pinchStartDist, Math.hypot(a.x - b.x, a.y - b.y));
7361
+ const vp = coords.getViewport();
7362
+ const pitchScale = vp.barSpacing > 0 ? coords.pxPerBar() / vp.barSpacing : 1;
7363
+ const midX = (a.x + b.x) / 2;
7364
+ this.deps.apply({
7365
+ barSpacing,
7366
+ rightOffset: pinchPinnedRightOffset(this.pinchAnchorLogical, coords.barCount, coords.width, midX, barSpacing * pitchScale)
7367
+ });
7368
+ }
7002
7369
  /** Capture defensively: a synthetic/already-released pointer can't be captured, and
7003
7370
  * the move/up pair still routes through the element listeners without it. */
7004
7371
  capture(pointerId) {
@@ -7007,6 +7374,35 @@ var InputController = class {
7007
7374
  } catch {
7008
7375
  }
7009
7376
  }
7377
+ /** Pair clean taps into a double-tap — the touch equivalent of dblclick (same
7378
+ * routing: axis taps reset that axis' view, a plot tap toggles pane maximize). */
7379
+ registerTap(t, x, y) {
7380
+ const paired = this.lastTapT > 0 && t - this.lastTapT <= DOUBLE_TAP_MS && Math.hypot(x - this.lastTapX, y - this.lastTapY) <= DOUBLE_TAP_SLOP;
7381
+ if (paired) {
7382
+ this.lastTapT = 0;
7383
+ this.doubleActivate(this.lastTapX, this.lastTapY);
7384
+ return;
7385
+ }
7386
+ this.lastTapT = t;
7387
+ this.lastTapX = x;
7388
+ this.lastTapY = y;
7389
+ }
7390
+ endGesture(e) {
7391
+ this.dragging = false;
7392
+ try {
7393
+ this.el?.releasePointerCapture(e.pointerId);
7394
+ } catch {
7395
+ }
7396
+ }
7397
+ /** Shared double-click / double-tap routing, by the region under the point. */
7398
+ doubleActivate(x, y) {
7399
+ if (this.deps.drawingsDblClick?.(x, y)) return;
7400
+ const region = this.regionAt(x, y);
7401
+ if (region === "price") this.deps.resetPriceScale(x, y);
7402
+ else if (region === "separator") this.deps.resetPaneSize(y);
7403
+ else if (region === "time") this.deps.resetView();
7404
+ else this.deps.dataDblClick(x, y);
7405
+ }
7010
7406
  };
7011
7407
 
7012
7408
  // src/renderers/native/core/KeyboardController.ts
@@ -8820,11 +9216,19 @@ var ChromeRenderer = class {
8820
9216
  const y = dataH + (fullH - dataH) / 2;
8821
9217
  const tr = coords.visibleTimeRange();
8822
9218
  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
- }
9219
+ const target = Math.max(3, Math.min(8, Math.floor(dataW / 64)));
9220
+ 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);
9221
+ const GAP = 12;
9222
+ const placed = [];
9223
+ const put = (tick) => {
9224
+ const l = tick.x - tick.half;
9225
+ const r = tick.x + tick.half;
9226
+ if (!placed.every((p) => r + GAP <= p.l || l - GAP >= p.r)) return;
9227
+ placed.push({ l, r });
9228
+ ctx.fillText(tick.label, tick.x, y);
9229
+ };
9230
+ for (const tick of ticks) if (tick.major) put(tick);
9231
+ for (const tick of ticks) if (!tick.major) put(tick);
8828
9232
  ctx.textAlign = "start";
8829
9233
  }
8830
9234
  };
@@ -8926,10 +9330,10 @@ var CrosshairRenderer = class {
8926
9330
  }
8927
9331
  /** The synced ghost: a dimmed vertical line at the bar the renderer resolved as
8928
9332
  * 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". */
9333
+ * along), with that bar's time chip in this chart's own timezone and — when the
9334
+ * level resolved the price chip on the right axis. The snap happened upstream
9335
+ * (`externalCrossPx`, floor-to-containing-bar)this method only draws. Chips
9336
+ * render slightly dimmed so the ghost still reads as foreign. */
8933
9337
  drawExternal(ctx, ext, scene, coords, theme) {
8934
9338
  const cs = scene.style.crosshair;
8935
9339
  const dataW = coords.width;
@@ -8953,7 +9357,20 @@ var CrosshairRenderer = class {
8953
9357
  ctx.setLineDash([]);
8954
9358
  ctx.lineWidth = 1;
8955
9359
  ctx.globalAlpha = 0.8;
8956
- this.chip(ctx, x, dataH + 1, formatStamp(ext.time, scene.timezone), cs.labelBackground ?? theme.borderColor, "center", true, theme.background);
9360
+ const chipBg = cs.labelBackground ?? theme.borderColor;
9361
+ if (ext.y != null && ext.price != null) {
9362
+ let pane;
9363
+ for (const p of scene.panes.values()) {
9364
+ if (ext.y >= p.bounds.top && ext.y <= p.bounds.top + p.bounds.height) {
9365
+ pane = p;
9366
+ break;
9367
+ }
9368
+ }
9369
+ if (pane) {
9370
+ 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);
9371
+ }
9372
+ }
9373
+ this.chip(ctx, x, dataH + 1, formatStamp(ext.time, scene.timezone), chipBg, "center", true, theme.background);
8957
9374
  ctx.globalAlpha = 1;
8958
9375
  }
8959
9376
  /** A soft band + a brighter crisp center line over the hovered separator, so it reads as
@@ -9224,6 +9641,7 @@ function colorField(theme, getVal, onVal) {
9224
9641
  swatch.style.background = `linear-gradient(${v}, ${v}), ${CHECKER2}`;
9225
9642
  };
9226
9643
  paint();
9644
+ trigger.addEventListener("vela-sync", paint);
9227
9645
  trigger.addEventListener("click", (e) => {
9228
9646
  e.stopPropagation();
9229
9647
  if (openPopover && openPopover.trigger === trigger) {
@@ -9259,6 +9677,101 @@ function closeColorPopover() {
9259
9677
  closeOpen();
9260
9678
  }
9261
9679
 
9680
+ // src/renderers/native/chrome/WidthField.ts
9681
+ var WIDTH_FIELD_OPTIONS = [1, 2, 3, 4, 5];
9682
+ var STYLE_ID4 = "vela-width-field";
9683
+ function ensureStyles4() {
9684
+ if (typeof document === "undefined" || document.getElementById(STYLE_ID4)) return;
9685
+ const st = document.createElement("style");
9686
+ st.id = STYLE_ID4;
9687
+ st.textContent = `
9688
+ .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;}
9689
+ .vela-width-field:hover{border-color:var(--vela-fg-muted);}
9690
+ .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);}
9691
+ .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;}
9692
+ .vela-width-field-item:hover{background:var(--vela-hover-strong);}
9693
+ .vela-width-field-item[data-active='1']{background:var(--vela-active);}
9694
+ `;
9695
+ document.head.appendChild(st);
9696
+ }
9697
+ function lineGlyph(width) {
9698
+ 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>`;
9699
+ }
9700
+ 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>`;
9701
+ var openPopover2 = null;
9702
+ function closeOpen2() {
9703
+ if (!openPopover2) return;
9704
+ document.removeEventListener("pointerdown", openPopover2.onOutside, true);
9705
+ window.removeEventListener("resize", openPopover2.reflow, true);
9706
+ openPopover2.el.remove();
9707
+ openPopover2 = null;
9708
+ }
9709
+ function place2(pop, trigger) {
9710
+ const r = trigger.getBoundingClientRect();
9711
+ const w = pop.offsetWidth;
9712
+ const h = pop.offsetHeight;
9713
+ let left = r.right - w;
9714
+ let top = r.bottom + 6;
9715
+ if (left < 6) left = 6;
9716
+ if (left + w > window.innerWidth - 6) left = window.innerWidth - w - 6;
9717
+ if (top + h > window.innerHeight - 6) top = r.top - h - 6;
9718
+ if (top < 6) top = 6;
9719
+ pop.style.left = `${Math.round(left)}px`;
9720
+ pop.style.top = `${Math.round(top)}px`;
9721
+ }
9722
+ function widthField(theme, getVal, onVal) {
9723
+ ensureStyles4();
9724
+ const trigger = document.createElement("button");
9725
+ trigger.type = "button";
9726
+ trigger.className = "vela-width-field";
9727
+ const paint = () => {
9728
+ trigger.innerHTML = `<span style="display:flex;">${lineGlyph(getVal())}</span><span style="display:flex;">${CHEVRON}</span>`;
9729
+ };
9730
+ paint();
9731
+ trigger.addEventListener("click", (e) => {
9732
+ e.stopPropagation();
9733
+ if (openPopover2 && openPopover2.trigger === trigger) {
9734
+ closeOpen2();
9735
+ return;
9736
+ }
9737
+ closeOpen2();
9738
+ const pop = document.createElement("div");
9739
+ pop.className = "vela-width-field-pop";
9740
+ applyChromeTokens(pop, theme);
9741
+ pop.style.font = `var(--vela-font-size-md) ${theme.fontFamily}`;
9742
+ pop.addEventListener("pointerdown", (ev) => ev.stopPropagation());
9743
+ for (const w of WIDTH_FIELD_OPTIONS) {
9744
+ const item = document.createElement("button");
9745
+ item.type = "button";
9746
+ item.className = "vela-width-field-item";
9747
+ item.dataset.active = w === getVal() ? "1" : "0";
9748
+ item.innerHTML = `<span style="display:flex;flex:none;">${lineGlyph(w)}</span><span style="flex:1;font-variant-numeric:tabular-nums;">${w}px</span>`;
9749
+ item.addEventListener("click", (ev) => {
9750
+ ev.stopPropagation();
9751
+ closeOpen2();
9752
+ onVal(w);
9753
+ paint();
9754
+ });
9755
+ pop.appendChild(item);
9756
+ }
9757
+ document.body.appendChild(pop);
9758
+ place2(pop, trigger);
9759
+ const onOutside = (ev) => {
9760
+ const t = ev.target;
9761
+ if (pop.contains(t) || trigger.contains(t)) return;
9762
+ closeOpen2();
9763
+ };
9764
+ const reflow = () => place2(pop, trigger);
9765
+ setTimeout(() => document.addEventListener("pointerdown", onOutside, true), 0);
9766
+ window.addEventListener("resize", reflow, true);
9767
+ openPopover2 = { el: pop, trigger, onOutside, reflow };
9768
+ });
9769
+ return trigger;
9770
+ }
9771
+ function closeWidthPopover() {
9772
+ closeOpen2();
9773
+ }
9774
+
9262
9775
  // src/renderers/native/chrome/SettingsDialog.ts
9263
9776
  var BUILTIN_STYLE_LABELS = {
9264
9777
  candles: "Candles",
@@ -9341,6 +9854,30 @@ function ensureControlStyles() {
9341
9854
  muted and non-interactive. Applied to each row's children so it survives display:contents;
9342
9855
  !important beats the inline opacity on labels. */
9343
9856
  .vela-sd-soft>*{opacity:0.4 !important;pointer-events:none !important;}
9857
+ /* \u2500\u2500 mobile presentation (.vela-sd-mobile on the scrim; structural sizes are inline in open()) \u2500\u2500
9858
+ The tab rail becomes a burger-opened overlay sidebar; the group TOC becomes a sticky
9859
+ row of horizontally scrollable tabs; the instance strip scrolls instead of wrapping;
9860
+ controls grow to touch size and labels may wrap (the grid gives them a flexible track). */
9861
+ .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;}
9862
+ .vela-sd-mobile .vela-sd-burger:active{background:var(--vela-hover);}
9863
+ .vela-sd-mobile .vela-sd-railscrim{position:absolute;inset:0;z-index:2;background:var(--vela-backdrop);display:none;}
9864
+ .vela-sd-mobile .vela-sd-railscrim.open{display:block;}
9865
+ .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);}
9866
+ .vela-sd-mobile .vela-sd-rail.open{transform:translateX(0);}
9867
+ .vela-sd-mobile .vela-sd-tab{padding:12px;}
9868
+ .vela-sd-mobile .vela-sd-struct{flex-direction:column;padding-top:8px;}
9869
+ .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;}
9870
+ .vela-sd-mobile .vela-sd-toc::-webkit-scrollbar{display:none;}
9871
+ .vela-sd-mobile .vela-sd-toc-btn{flex:none;padding:8px 12px;font-size:13px;}
9872
+ .vela-sd-mobile .vela-sd-struct>[data-sd-rows-host]{border-left:none;padding-left:0;width:100%;}
9873
+ .vela-sd-mobile .vela-sd-itabs{flex-wrap:nowrap;overflow-x:auto;scrollbar-width:none;}
9874
+ .vela-sd-mobile .vela-sd-itabs::-webkit-scrollbar{display:none;}
9875
+ .vela-sd-mobile .vela-sd-itab{height:36px;flex:none;}
9876
+ .vela-sd-mobile .vela-sd-check{width:22px;height:22px;}
9877
+ .vela-sd-mobile .vela-sd-select,.vela-sd-mobile .vela-sd-number{height:34px;}
9878
+ .vela-sd-mobile .vela-sd-close{width:40px;height:40px;}
9879
+ .vela-sd-mobile .vela-sd-btn{height:38px;}
9880
+ .vela-sd-mobile .vela-sd-row span,.vela-sd-mobile .vela-sd-bool span{white-space:normal !important;}
9344
9881
  `;
9345
9882
  document.head.appendChild(st);
9346
9883
  }
@@ -9367,6 +9904,8 @@ var SettingsDialog = class {
9367
9904
  this.typeActiveGroup = /* @__PURE__ */ new Map();
9368
9905
  /** The tab currently shown, so a theme change (which rebuilds) lands back on it. */
9369
9906
  this.activeSection = null;
9907
+ /** Mobile chrome: fullscreen card, burger-opened section sidebar, TOC as top tabs. */
9908
+ this.mobileLayout = false;
9370
9909
  if (getComputedStyle(container).position === "static") container.style.position = "relative";
9371
9910
  }
9372
9911
  /** Host-app sections (e.g. the widget's Status line tab) — re-shown on next open. */
@@ -9385,15 +9924,27 @@ var SettingsDialog = class {
9385
9924
  }
9386
9925
  setTheme(theme) {
9387
9926
  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
- }
9927
+ this.reopenIfLive();
9928
+ }
9929
+ /** The host shell's chrome size class — mobile presents the dialog fullscreen with
9930
+ * the tab rail behind a burger sidebar and the TOC as top tabs. An open dialog
9931
+ * re-presents in place, on the same tab. */
9932
+ setLayoutMode(mode) {
9933
+ const mobile = mode === "mobile";
9934
+ if (mobile === this.mobileLayout) return;
9935
+ this.mobileLayout = mobile;
9936
+ this.reopenIfLive();
9937
+ }
9938
+ /** Rebuild an open dialog in place (theme swap, layout-mode flip) on the same tab. */
9939
+ reopenIfLive() {
9940
+ if (!this.root) return;
9941
+ const cfg = this.config;
9942
+ const oc = this.onChange;
9943
+ const oi = this.onImport;
9944
+ const orst = this.onReset;
9945
+ const section = this.activeSection;
9946
+ this.close();
9947
+ if (cfg && oc) this.open(cfg, oc, oi ?? void 0, orst ?? void 0, section ?? void 0);
9397
9948
  }
9398
9949
  isOpen() {
9399
9950
  return this.root !== null;
@@ -9415,18 +9966,37 @@ var SettingsDialog = class {
9415
9966
  this.onImport = onImport ?? null;
9416
9967
  this.onReset = onReset ?? null;
9417
9968
  ensureControlStyles();
9969
+ const mobile = this.mobileLayout;
9418
9970
  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;";
9971
+ 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;";
9972
+ if (mobile) {
9973
+ scrim.classList.add("vela-sd-mobile");
9974
+ scrim.style.paddingTop = "0";
9975
+ scrim.style.alignItems = "stretch";
9976
+ }
9420
9977
  scrim.addEventListener("mousedown", (e) => {
9421
9978
  if (e.target === scrim) this.close();
9422
9979
  });
9423
9980
  const dlg = document.createElement("div");
9424
9981
  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;`;
9982
+ 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
9983
  const header = document.createElement("div");
9426
9984
  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;`;
9985
+ if (mobile) header.style.paddingLeft = "8px";
9427
9986
  const hTitle = document.createElement("span");
9428
9987
  hTitle.textContent = "Chart settings";
9429
9988
  hTitle.style.cssText = "font-size:17px;font-weight:600;letter-spacing:0.2px;";
9989
+ if (mobile) hTitle.style.cssText += "flex:1 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;";
9990
+ let toggleRail = null;
9991
+ if (mobile) {
9992
+ const burger = document.createElement("button");
9993
+ burger.type = "button";
9994
+ burger.className = "vela-sd-burger";
9995
+ burger.innerHTML = iconAt("burger", 16);
9996
+ burger.title = "Sections";
9997
+ burger.addEventListener("click", () => toggleRail?.());
9998
+ header.append(burger);
9999
+ }
9430
10000
  const closeBtn = document.createElement("button");
9431
10001
  closeBtn.type = "button";
9432
10002
  closeBtn.innerHTML = iconAt("close", 15);
@@ -9434,8 +10004,8 @@ var SettingsDialog = class {
9434
10004
  closeBtn.className = "vela-sd-close";
9435
10005
  closeBtn.addEventListener("click", () => this.close());
9436
10006
  header.append(hTitle, closeBtn);
9437
- header.style.cursor = "move";
9438
- {
10007
+ if (!mobile) {
10008
+ header.style.cursor = "move";
9439
10009
  let sx = 0, sy = 0, ox = 0, oy = 0, dragging = false;
9440
10010
  header.addEventListener("pointerdown", (e) => {
9441
10011
  if (e.target?.closest(".vela-sd-close")) return;
@@ -9616,12 +10186,24 @@ var SettingsDialog = class {
9616
10186
  renderChartTypeSections("end");
9617
10187
  renderHostSections("end");
9618
10188
  const shell = document.createElement("div");
9619
- shell.style.cssText = "display:flex;min-height:360px;max-height:calc(70vh - 100px);flex:1 1 auto;";
10189
+ 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
10190
  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;`;
10191
+ rail.className = "vela-sd-rail";
10192
+ 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;`;
10193
+ let railScrim = null;
10194
+ if (mobile) {
10195
+ railScrim = document.createElement("div");
10196
+ railScrim.className = "vela-sd-railscrim";
10197
+ railScrim.addEventListener("click", () => toggleRail?.());
10198
+ toggleRail = (open) => {
10199
+ const on = open ?? !rail.classList.contains("open");
10200
+ rail.classList.toggle("open", on);
10201
+ railScrim?.classList.toggle("open", on);
10202
+ };
10203
+ }
9622
10204
  const paneHost = document.createElement("div");
9623
10205
  paneHost.className = "vela-sd-pane";
9624
- paneHost.style.cssText = "flex:1;overflow-y:auto;padding:6px 18px 14px;";
10206
+ 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
10207
  const panes = [];
9626
10208
  let current = null;
9627
10209
  for (const child of [...body.children]) {
@@ -9656,6 +10238,10 @@ var SettingsDialog = class {
9656
10238
  p.tab.classList.toggle("on", i === idx);
9657
10239
  });
9658
10240
  this.activeSection = panes[idx]?.title ?? null;
10241
+ if (mobile) {
10242
+ hTitle.textContent = panes[idx]?.title ?? "Chart settings";
10243
+ toggleRail?.(false);
10244
+ }
9659
10245
  };
9660
10246
  panes.forEach((p, i) => {
9661
10247
  p.tab.addEventListener("click", () => activate(i));
@@ -9663,10 +10249,11 @@ var SettingsDialog = class {
9663
10249
  paneHost.appendChild(p.el);
9664
10250
  });
9665
10251
  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());
10252
+ 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
10253
  activate(wanted >= 0 ? wanted : 0);
9668
10254
  this.syncTypeTabs?.(config.series.style);
9669
10255
  shell.append(rail, paneHost);
10256
+ if (railScrim) shell.append(railScrim);
9670
10257
  dlg.appendChild(shell);
9671
10258
  dlg.appendChild(this.footer(config));
9672
10259
  scrim.appendChild(dlg);
@@ -9683,6 +10270,7 @@ var SettingsDialog = class {
9683
10270
  }
9684
10271
  close() {
9685
10272
  closeColorPopover();
10273
+ closeWidthPopover();
9686
10274
  this.root?.remove();
9687
10275
  this.root = null;
9688
10276
  this.tabs = [];
@@ -9723,20 +10311,12 @@ var SettingsDialog = class {
9723
10311
  body.append(marker);
9724
10312
  const values = config.chartTypes[typeId] ?? {};
9725
10313
  const bag = {};
9726
- const seedKey = (key, want, defval) => {
9727
- const v = values[key];
9728
- bag[key] = typeof v === want ? v : defval;
9729
- };
9730
10314
  const seed = (rows) => {
9731
10315
  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;
10316
+ for (const k of settingsRowValueKeys(r)) {
10317
+ const v = values[k.key];
10318
+ bag[k.key] = typeof v === k.type ? v : k.defval;
9737
10319
  }
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
10320
  }
9741
10321
  };
9742
10322
  if (section.rows) seed(section.rows);
@@ -9759,7 +10339,8 @@ var SettingsDialog = class {
9759
10339
  if (section.instances && section.instances.length > 0) {
9760
10340
  body.append(this.instancesBlock(typeId, section.instances, bag, put, refreshers));
9761
10341
  } else if (section.rows) {
9762
- this.flatTypeRows(section.rows, bag, put, refreshers, body);
10342
+ if (section.layout === "grouped") body.append(this.groupedRows(`${typeId}/rows`, section.rows, bag, put, refreshers));
10343
+ else this.flatTypeRows(section.rows, bag, put, refreshers, body);
9763
10344
  }
9764
10345
  for (const sub of section.subsections ?? []) {
9765
10346
  const subMarker = this.section(sub.title);
@@ -9776,7 +10357,7 @@ var SettingsDialog = class {
9776
10357
  flatTypeRows(rows, bag, put, refreshers, body) {
9777
10358
  const entries = [];
9778
10359
  for (const r of rows) {
9779
- const el = r.kind === "heading" || r.kind === "header" ? this.sectionTitle(r.label) : this.typeRow(r, bag, put);
10360
+ const el = r.kind === "heading" || r.kind === "header" ? this.sectionTitle(r.label) : this.typeRow(r, bag, put, refreshers);
9780
10361
  entries.push({ el, when: r.when });
9781
10362
  body.append(el);
9782
10363
  }
@@ -9784,60 +10365,109 @@ var SettingsDialog = class {
9784
10365
  for (const e of entries) e.el.classList.toggle("vela-sd-hide", !settingsRowVisible(e.when, bag));
9785
10366
  });
9786
10367
  }
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);
10368
+ /**
10369
+ * One value row for a chart-type descriptor, writing through `put`. EVERY kind is
10370
+ * first reduced to the canonical composite shape ({@link normalizeSettingsRow}) and
10371
+ * rendered by the ONE path: optional leading toggle, then the ordered inline
10372
+ * controls in the control column. A control carrying its own `when` registers a
10373
+ * visibility refresher — it appears and disappears live as the bag changes,
10374
+ * independent of the row's gate — and is exempt from the toggle-off dim (it may
10375
+ * exist FOR the off state).
10376
+ */
10377
+ typeRow(r, bag, put, refreshers) {
10378
+ const n = normalizeSettingsRow(r);
10379
+ const controls = [];
10380
+ for (const c of n.controls) {
10381
+ const el = this.inlineControl(c, bag, put, n.toggle !== void 0);
10382
+ if (c.kind !== "hint") refreshers.push(() => el.dispatchEvent(new Event("vela-sync")));
10383
+ if (c.kind !== "hint" && c.when) {
10384
+ el.dataset.sdSelfGated = "1";
10385
+ const when = c.when;
10386
+ refreshers.push(() => {
10387
+ el.style.display = settingsRowVisible(when, bag) ? "" : "none";
10388
+ });
10389
+ }
10390
+ controls.push(el);
9796
10391
  }
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));
10392
+ if (n.toggle) {
10393
+ const t = n.toggle;
10394
+ const el = this.toggleRow(n.label, bag[t.key], (v) => put(t.key, v), controls, () => bag[t.key] === true);
10395
+ refreshers.push(() => el.dispatchEvent(new Event("vela-sync")));
10396
+ return el;
10397
+ }
10398
+ return this.rowWith(n.label, controls);
9801
10399
  }
9802
10400
  /**
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).
10401
+ * Build ONE inline control from its descriptor the factory behind the composite
10402
+ * row path. `compact` narrows number inputs on toggle rows (the historical inline
10403
+ * width) while standalone rows keep the full-width input.
9807
10404
  */
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);
10405
+ inlineControl(c, bag, put, compact) {
10406
+ if (c.kind === "hint") {
10407
+ const s = document.createElement("span");
10408
+ s.textContent = c.text;
10409
+ s.style.cssText = "color:var(--vela-fg-muted);";
10410
+ return s;
10411
+ }
10412
+ if (c.kind === "color") {
10413
+ const sw = this.swatch(bag[c.key], (v) => put(c.key, v), () => bag[c.key]);
10414
+ sw.title = c.label;
10415
+ return sw;
10416
+ }
10417
+ if (c.kind === "width") {
10418
+ let cur = typeof bag[c.key] === "number" ? bag[c.key] : c.defval;
10419
+ const wf = widthField(this.theme, () => cur, (v) => {
10420
+ cur = v;
10421
+ put(c.key, v);
10422
+ });
10423
+ wf.title = c.label;
10424
+ return wf;
10425
+ }
10426
+ if (c.kind === "select") {
10427
+ const sel = document.createElement("select");
10428
+ sel.className = "vela-sd-select";
10429
+ sel.style.cssText = "max-width:200px;flex:0 0 auto;";
10430
+ sel.title = c.label;
10431
+ const current2 = typeof bag[c.key] === "string" ? bag[c.key] : c.defval;
10432
+ for (const [val, lbl] of normalizeSelectOptions(c.options)) {
10433
+ const o = document.createElement("option");
10434
+ o.value = val;
10435
+ o.textContent = lbl;
10436
+ if (val === current2) o.selected = true;
10437
+ sel.appendChild(o);
9824
10438
  }
10439
+ sel.addEventListener("change", () => put(c.key, sel.value));
10440
+ sel.addEventListener("vela-sync", () => {
10441
+ sel.value = typeof bag[c.key] === "string" ? bag[c.key] : c.defval;
10442
+ });
10443
+ return sel;
10444
+ }
10445
+ const ni = document.createElement("input");
10446
+ ni.type = "number";
10447
+ ni.className = "vela-sd-number";
10448
+ if (compact) ni.style.width = "56px";
10449
+ if (c.min !== void 0) ni.min = String(c.min);
10450
+ if (c.max !== void 0) ni.max = String(c.max);
10451
+ ni.step = String(c.step ?? 1);
10452
+ ni.title = c.label;
10453
+ const current = typeof bag[c.key] === "number" ? bag[c.key] : c.defval;
10454
+ if (c.placeholder !== void 0) {
10455
+ ni.placeholder = c.placeholder;
10456
+ if (current !== c.defval) ni.value = String(current);
9825
10457
  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);
10458
+ const raw = ni.value.trim() === "" ? c.defval : Number(ni.value);
10459
+ const v = Number.isFinite(raw) ? Math.min(c.max ?? Infinity, Math.max(c.min ?? -Infinity, raw)) : c.defval;
10460
+ ni.value = v === c.defval ? "" : String(v);
10461
+ put(c.key, v);
9830
10462
  });
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;
10463
+ } else {
10464
+ ni.value = String(current);
10465
+ ni.addEventListener("input", () => {
10466
+ const v = Number(ni.value);
10467
+ if (Number.isFinite(v)) put(c.key, v);
10468
+ });
10469
+ }
10470
+ return ni;
9841
10471
  }
9842
10472
  /**
9843
10473
  * The INSTANCE STRIP block: a tab per present instance (label, `×` on the active
@@ -9850,7 +10480,7 @@ var SettingsDialog = class {
9850
10480
  const wrap = document.createElement("div");
9851
10481
  const strip = document.createElement("div");
9852
10482
  strip.className = "vela-sd-itabs";
9853
- wrap.append(strip);
10483
+ if (instances.length > 1 || instances[0]?.enableKey !== void 0) wrap.append(strip);
9854
10484
  const contents = instances.map((inst, i) => {
9855
10485
  const content = this.groupedRows(`${typeId}/#${i}`, inst.rows, bag, put, refreshers);
9856
10486
  wrap.append(content);
@@ -9945,8 +10575,8 @@ var SettingsDialog = class {
9945
10575
  rowsHost.append(entries[entries.length - 1].el);
9946
10576
  continue;
9947
10577
  }
9948
- const el = this.typeRow(r, bag, put);
9949
- const key = r.kind === "range" ? void 0 : r.key;
10578
+ const el = this.typeRow(r, bag, put, refreshers);
10579
+ const key = r.kind === "row" ? r.toggle?.key : r.kind === "range" ? void 0 : r.key;
9950
10580
  entries.push({ el, when: r.when, group: g, key });
9951
10581
  rowsHost.append(el);
9952
10582
  }
@@ -10012,6 +10642,14 @@ var SettingsDialog = class {
10012
10642
  */
10013
10643
  layoutSettingsGrids(pane, measureHost) {
10014
10644
  if (pane.childElementCount === 0) return;
10645
+ if (this.mobileLayout) {
10646
+ const grid2 = document.createElement("div");
10647
+ grid2.style.cssText = "display:grid;grid-template-columns:minmax(0,1fr) max-content;align-items:center;column-gap:12px;row-gap:16px;";
10648
+ while (pane.firstChild) grid2.appendChild(pane.firstChild);
10649
+ pane.appendChild(grid2);
10650
+ this.prepareGridItems(grid2);
10651
+ return;
10652
+ }
10015
10653
  const rows = [...pane.querySelectorAll(".vela-sd-row")];
10016
10654
  const probe2 = document.createElement("div");
10017
10655
  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 +10686,11 @@ var SettingsDialog = class {
10048
10686
  }
10049
10687
  }
10050
10688
  /** A bare color swatch input (for toggle-row right groups / swatch pairs). */
10051
- swatch(value, onChange) {
10689
+ /** With `get`, the field reads its value live (and repaints on 'vela-sync')
10690
+ * duplicate-keyed rows stay honest when another row edits the shared key. */
10691
+ swatch(value, onChange, get) {
10052
10692
  let current = value;
10053
- return colorField(this.theme, () => current, (v) => {
10693
+ return colorField(this.theme, () => get ? get() : current, (v) => {
10054
10694
  current = v;
10055
10695
  onChange(v);
10056
10696
  });
@@ -10102,8 +10742,11 @@ var SettingsDialog = class {
10102
10742
  }
10103
10743
  /** An enable row: checkbox + label in the label column, dependent controls in the
10104
10744
  * 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) {
10745
+ * is off. With no controls it reads like a plain toggle row (full-width in the grid).
10746
+ * With `get`, the row re-reads its state on a 'vela-sync' event — the seam that
10747
+ * keeps DUPLICATE-KEYED rows honest (several `when`-gated rows over one bag key,
10748
+ * only one visible at a time; see `typeRow`). */
10749
+ toggleRow(label, value, onToggle, controls, get) {
10107
10750
  const wrap = document.createElement("div");
10108
10751
  const cb = document.createElement("button");
10109
10752
  cb.type = "button";
@@ -10123,6 +10766,12 @@ var SettingsDialog = class {
10123
10766
  wrap.style.cssText = "display:flex;align-items:center;gap:8px;min-height:22px;";
10124
10767
  wrap.append(cb, lbl);
10125
10768
  cb.addEventListener("click", () => onToggle(cbToggle()));
10769
+ if (get) {
10770
+ wrap.addEventListener("vela-sync", () => {
10771
+ checked = get();
10772
+ cb.classList.toggle("on", checked);
10773
+ });
10774
+ }
10126
10775
  return wrap;
10127
10776
  }
10128
10777
  wrap.className = "vela-sd-row";
@@ -10133,8 +10782,11 @@ var SettingsDialog = class {
10133
10782
  box.style.cssText = "display:flex;align-items:center;gap:6px;";
10134
10783
  for (const c of controls) box.appendChild(c);
10135
10784
  const syncDim = (on) => {
10136
- box.style.opacity = on ? "1" : "0.4";
10137
- box.style.pointerEvents = on ? "" : "none";
10785
+ for (const c of controls) {
10786
+ if (c.dataset.sdSelfGated === "1") continue;
10787
+ c.style.opacity = on ? "1" : "0.4";
10788
+ c.style.pointerEvents = on ? "" : "none";
10789
+ }
10138
10790
  };
10139
10791
  syncDim(value);
10140
10792
  cb.addEventListener("click", () => {
@@ -10142,6 +10794,13 @@ var SettingsDialog = class {
10142
10794
  onToggle(v);
10143
10795
  syncDim(v);
10144
10796
  });
10797
+ if (get) {
10798
+ wrap.addEventListener("vela-sync", () => {
10799
+ checked = get();
10800
+ cb.classList.toggle("on", checked);
10801
+ syncDim(checked);
10802
+ });
10803
+ }
10145
10804
  wrap.append(left, box);
10146
10805
  return wrap;
10147
10806
  }
@@ -10161,96 +10820,6 @@ var SettingsDialog = class {
10161
10820
  wrap.appendChild(ni);
10162
10821
  return wrap;
10163
10822
  }
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
10823
  /** A dropdown whose option values differ from their display labels. */
10255
10824
  selectRowLabeled(label, value, options, onChange) {
10256
10825
  const { wrap } = this.row(label);
@@ -10296,19 +10865,7 @@ var SettingsDialog = class {
10296
10865
  foot.appendChild(resetBtn);
10297
10866
  return foot;
10298
10867
  }
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
10868
  };
10311
- var AUTO_MANUAL_OPTS = [["auto", "Auto"], ["manual", "Manual"]];
10312
10869
  function normalizeSelectOptions(options) {
10313
10870
  return options.map((o) => typeof o === "string" ? [o, o] : o);
10314
10871
  }
@@ -10511,7 +11068,7 @@ var DrawingPainter = class {
10511
11068
  return;
10512
11069
  }
10513
11070
  if (d instanceof FixedRangeVolumeProfile) {
10514
- this.paintFixedRangeVp(ctx, d, proj);
11071
+ this.paintFixedRangeVp(ctx, d, proj, theme);
10515
11072
  this.paintLabel(ctx, d, proj, theme);
10516
11073
  return;
10517
11074
  }
@@ -11004,7 +11561,7 @@ var DrawingPainter = class {
11004
11561
  /** Paint a fixed-range volume profile: horizontal histogram rows (up/down split) anchored to
11005
11562
  * the left or right of the time span, optional VAH / VAL / POC levels across the range, and
11006
11563
  * optional developing POC / VA polylines. Recomputes from the two anchors on every paint. */
11007
- paintFixedRangeVp(ctx, d, proj) {
11564
+ paintFixedRangeVp(ctx, d, proj, theme) {
11008
11565
  const L = d.layout(proj);
11009
11566
  if (!L) return;
11010
11567
  const s = d.frvp;
@@ -11043,7 +11600,7 @@ var DrawingPainter = class {
11043
11600
  };
11044
11601
  hLine(s.showVah, s.vahColor, s.vahStyle, L.vahY);
11045
11602
  hLine(s.showVal, s.valColor, s.valStyle, L.valY);
11046
- hLine(s.showPoc, s.pocColor, s.pocStyle, L.pocY);
11603
+ hLine(s.showPoc, s.pocColor ?? contrastColor(theme.background), s.pocStyle, L.pocY);
11047
11604
  const devW = Math.max(1, w);
11048
11605
  if (s.showDevelopingPoc) {
11049
11606
  this.strokePolyline(ctx, L.developingPoc, {
@@ -11915,7 +12472,11 @@ var DrawingInteraction = class {
11915
12472
  const last = draft.anchors[draft.anchors.length - 1];
11916
12473
  const lx = last ? proj.xOf(last.time) : NaN;
11917
12474
  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) {
12475
+ if (Math.hypot(x - lx, y - ly) > FREEHAND_SAMPLE_PX) {
12476
+ if (draft.anchors.length >= this.state.need) {
12477
+ const lastIdx = draft.anchors.length - 1;
12478
+ draft.anchors = draft.anchors.filter((_, i) => i % 2 === 0 || i === lastIdx);
12479
+ }
11919
12480
  draft.anchors.push(proj.pxToPoint(x, y, draft.paneId));
11920
12481
  }
11921
12482
  this.state.cursor = proj.pxToPoint(x, y, draft.paneId);
@@ -11988,11 +12549,11 @@ function applyFree(anchor, pt, free) {
11988
12549
  // src/renderers/native/drawings/DrawingSettingsPopup.ts
11989
12550
  var BTN = 30;
11990
12551
  var ICON = 21;
11991
- var STYLE_ID4 = "vela-drawing-popup-styles";
11992
- function ensureStyles4() {
11993
- if (typeof document === "undefined" || document.getElementById(STYLE_ID4)) return;
12552
+ var STYLE_ID5 = "vela-drawing-popup-styles";
12553
+ function ensureStyles5() {
12554
+ if (typeof document === "undefined" || document.getElementById(STYLE_ID5)) return;
11994
12555
  const s = document.createElement("style");
11995
- s.id = STYLE_ID4;
12556
+ s.id = STYLE_ID5;
11996
12557
  s.textContent = `
11997
12558
  .vela-dpop input[type=color]{-webkit-appearance:none;appearance:none;border:none;padding:0;background:none;cursor:pointer;border-radius:0;}
11998
12559
  .vela-dpop input[type=color]::-webkit-color-swatch-wrapper{padding:0;}
@@ -12011,7 +12572,10 @@ function ensureStyles4() {
12011
12572
  .vela-dpop-btn[data-active='1']{background:var(--vela-active);color:var(--vela-fg-bright);}
12012
12573
  .vela-dpop-item{background:transparent;transition:background var(--vela-dur-fast) ease;}
12013
12574
  .vela-dpop-item:hover{background:var(--vela-hover-strong);}
12014
- .vela-dpop-item[data-active='1']{background:var(--vela-active);}`;
12575
+ .vela-dpop-item[data-active='1']{background:var(--vela-active);}
12576
+ /* A finger needs a wider grab zone than a cursor \u2014 grow the move handle on touch-first
12577
+ devices (the glyph stays centered; only the hit target widens). */
12578
+ @media (pointer: coarse){.vela-dpop-grip{width:${BTN + 14}px !important;}}`;
12015
12579
  document.head.appendChild(s);
12016
12580
  }
12017
12581
  var DrawingSettingsPopup = class {
@@ -12058,7 +12622,7 @@ var DrawingSettingsPopup = class {
12058
12622
  * `onClose` fires on an outside-click dismissal (not a programmatic close). */
12059
12623
  open(drawing, anchor, actions, onClose) {
12060
12624
  this.close();
12061
- ensureStyles4();
12625
+ ensureStyles5();
12062
12626
  this.onClose = onClose ?? null;
12063
12627
  const t = this.theme;
12064
12628
  const schema = drawing.schema();
@@ -12084,7 +12648,14 @@ var DrawingSettingsPopup = class {
12084
12648
  bar.appendChild(this.dropdown("Icon size", STAMP_SIZE_OPTIONS, sz, (s) => stampSizeIcon(s), (v) => actions.patch({ size: v }), { label: sizeLabel }));
12085
12649
  }
12086
12650
  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 }));
12651
+ if (paths.has("style.lineWidth")) {
12652
+ const wf = schema.fields.find((f) => f.path === "style.lineWidth");
12653
+ if (wf?.kind === "number" && (wf.min ?? 1) > 1) {
12654
+ bar.appendChild(this.widthInput("Line width", drawing.style.lineWidth, wf.min ?? 1, wf.max ?? 60, wf.step ?? 1, (v) => actions.patch({ "style.lineWidth": v })));
12655
+ } else {
12656
+ 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 }));
12657
+ }
12658
+ }
12088
12659
  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
12660
  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
12661
  const isFrvp = paths.has("frvp.rows") && drawing instanceof FixedRangeVolumeProfile;
@@ -12552,14 +13123,15 @@ var DrawingSettingsPopup = class {
12552
13123
  const chk = document.createElement("input");
12553
13124
  chk.type = "checkbox";
12554
13125
  chk.checked = Boolean(s[showPath]);
12555
- chk.style.cssText = `accent-color:${s[colorPath]};width:15px;height:15px;flex:none;cursor:pointer;`;
13126
+ const current = s[colorPath] ?? contrastColor(t.background);
13127
+ chk.style.cssText = `accent-color:${current};width:15px;height:15px;flex:none;cursor:pointer;`;
12556
13128
  chk.addEventListener("change", () => actions.patch({ [`frvp.${showPath}`]: chk.checked }));
12557
13129
  const lbl = document.createElement("span");
12558
13130
  lbl.textContent = label;
12559
13131
  lbl.style.cssText = "flex:1;min-width:0;opacity:0.9;";
12560
13132
  const col = document.createElement("button");
12561
13133
  col.type = "button";
12562
- let cur = s[colorPath];
13134
+ let cur = current;
12563
13135
  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
13136
  col.addEventListener("pointerdown", (e) => e.stopPropagation());
12565
13137
  col.addEventListener("click", (e) => {
@@ -12739,7 +13311,8 @@ var DrawingSettingsPopup = class {
12739
13311
  dragHandle() {
12740
13312
  const b = document.createElement("button");
12741
13313
  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);`;
13314
+ b.className = "vela-dpop-grip";
13315
+ 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
13316
  b.innerHTML = sized(GRIP_ICON);
12744
13317
  b.addEventListener("pointerdown", (e) => {
12745
13318
  const el = this.el;
@@ -12755,6 +13328,10 @@ var DrawingSettingsPopup = class {
12755
13328
  const origLeft = parseFloat(el.style.left) || 0;
12756
13329
  const origTop = parseFloat(el.style.top) || 0;
12757
13330
  b.style.cursor = "grabbing";
13331
+ try {
13332
+ b.setPointerCapture(e.pointerId);
13333
+ } catch {
13334
+ }
12758
13335
  const move = (ev) => {
12759
13336
  const w = el.offsetWidth;
12760
13337
  const h = el.offsetHeight;
@@ -12767,9 +13344,11 @@ var DrawingSettingsPopup = class {
12767
13344
  b.style.cursor = "grab";
12768
13345
  window.removeEventListener("pointermove", move);
12769
13346
  window.removeEventListener("pointerup", up);
13347
+ window.removeEventListener("pointercancel", up);
12770
13348
  };
12771
13349
  window.addEventListener("pointermove", move);
12772
13350
  window.addEventListener("pointerup", up);
13351
+ window.addEventListener("pointercancel", up);
12773
13352
  });
12774
13353
  return b;
12775
13354
  }
@@ -12885,6 +13464,39 @@ var DrawingSettingsPopup = class {
12885
13464
  });
12886
13465
  return b;
12887
13466
  }
13467
+ /** An inline numeric width field for tools whose stroke range outgrows the 1–4px
13468
+ * ladder — the value is clamped to the schema's declared min/max on commit. */
13469
+ widthInput(tip, value, min, max, step, onChange) {
13470
+ const input = document.createElement("input");
13471
+ input.type = "number";
13472
+ input.dataset.tip = tip;
13473
+ input.min = String(min);
13474
+ input.max = String(max);
13475
+ input.step = String(step);
13476
+ input.value = String(value);
13477
+ 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;`;
13478
+ const commit = () => {
13479
+ const n = parseFloat(input.value);
13480
+ if (!Number.isFinite(n)) {
13481
+ input.value = String(value);
13482
+ return;
13483
+ }
13484
+ const v = Math.min(max, Math.max(min, n));
13485
+ input.value = String(v);
13486
+ value = v;
13487
+ onChange(v);
13488
+ };
13489
+ input.addEventListener("change", commit);
13490
+ input.addEventListener("keydown", (e) => {
13491
+ e.stopPropagation();
13492
+ if (e.key === "Enter") {
13493
+ e.preventDefault();
13494
+ commit();
13495
+ input.blur();
13496
+ }
13497
+ });
13498
+ return input;
13499
+ }
12888
13500
  /** A pick-one dropdown: the trigger shows the current value's glyph (plus an optional inline
12889
13501
  * text label — e.g. `2px` — for controls that would otherwise look alike), and clicking it
12890
13502
  * opens a floating list of every option so a value is one click away (no cycling through). */
@@ -13338,6 +13950,9 @@ var UserDrawingController = class {
13338
13950
  // mirrors showToolbar (drives the gutter reservation)
13339
13951
  this.toolbarCollapsed = false;
13340
13952
  // the bar is a slim expand-strip (narrower gutter)
13953
+ /** Mobile chrome: the docked bar is suppressed (the shell provides its own tool
13954
+ * picker) while `toolbarVisible` keeps the host's INTENT for a later desktop flip. */
13955
+ this.mobileLayout = false;
13341
13956
  /** Self-serve Ctrl+Z / Ctrl+Y as drawing undo/redo. A host that owns a UNIFIED
13342
13957
  * history (drawings + app ops in one timeline) turns this off so the chords
13343
13958
  * bubble to its keymap instead — see the renderer's `historyChords` feature. */
@@ -13396,12 +14011,21 @@ var UserDrawingController = class {
13396
14011
  }
13397
14012
  showToolbar(visible) {
13398
14013
  this.toolbarVisible = visible;
13399
- this.toolbar.setVisible(visible);
14014
+ this.toolbar.setVisible(visible && !this.mobileLayout);
14015
+ this.syncToolbarGutter();
14016
+ }
14017
+ /** Mobile suppresses the docked toolbar (hover flyouts don't work with touch; the
14018
+ * shell's own drawer picks tools instead) and releases its gutter; flipping back
14019
+ * to desktop restores whatever `showToolbar` last asked for. */
14020
+ setLayoutMode(mode) {
14021
+ this.mobileLayout = mode === "mobile";
14022
+ this.toolbar.setVisible(this.toolbarVisible && !this.mobileLayout);
13400
14023
  this.syncToolbarGutter();
13401
14024
  }
13402
14025
  /** The gutter follows the bar's current footprint: hidden 0, collapsed a slim strip, else full width. */
13403
14026
  syncToolbarGutter() {
13404
- this.deps.setToolbarGutter(this.toolbarVisible ? this.toolbarCollapsed ? TOOLBAR_COLLAPSED_WIDTH : TOOLBAR_WIDTH : 0);
14027
+ const shown = this.toolbarVisible && !this.mobileLayout;
14028
+ this.deps.setToolbarGutter(shown ? this.toolbarCollapsed ? TOOLBAR_COLLAPSED_WIDTH : TOOLBAR_WIDTH : 0);
13405
14029
  }
13406
14030
  /** Core push: mirror (or clear) another chart's in-progress placement as a ghost. */
13407
14031
  setExternalGhost(doc) {
@@ -14270,7 +14894,7 @@ var LUXALGO_WORDMARK_SVG = '<svg viewBox="0 0 964 252" aria-hidden="true"><path
14270
14894
 
14271
14895
  // src/renderers/native/chrome/AttributionMark.ts
14272
14896
  var ATTRIBUTION_URL = "https://luxalgo.com/vela";
14273
- var STYLE_ID5 = "vela-attribution-styles";
14897
+ var STYLE_ID6 = "vela-attribution-styles";
14274
14898
  var CSS = `
14275
14899
  .vela-attribution {
14276
14900
  text-decoration: none;
@@ -14316,12 +14940,28 @@ var CSS = `
14316
14940
  max-width: 240px;
14317
14941
  opacity: 1;
14318
14942
  transform: translateX(0);
14319
- }`;
14320
- function ensureStyles5(doc) {
14321
- let s = doc.getElementById(STYLE_ID5);
14943
+ }
14944
+ /* Mobile: a hair smaller so the mark doesn't dominate a phone-width plot. Keyed on the
14945
+ renderer's own container attribute (data-vela-layout) AND the shell root's
14946
+ (data-layout): the workspace's single grid-wide mark lives OUTSIDE any renderer
14947
+ container, so only the shell attribute reaches it. */
14948
+ [data-vela-layout='mobile'] .vela-attribution .vela-attr-symbol svg,
14949
+ [data-layout='mobile'] .vela-attribution .vela-attr-symbol svg { height: 22px; }
14950
+ [data-vela-layout='mobile'] .vela-attribution .vela-attr-wordmark,
14951
+ [data-layout='mobile'] .vela-attribution .vela-attr-wordmark {
14952
+ top: 2px;
14953
+ transform: translateX(-6px);
14954
+ }
14955
+ [data-vela-layout='mobile'] .vela-attribution .vela-attr-wordmark svg,
14956
+ [data-layout='mobile'] .vela-attribution .vela-attr-wordmark svg { height: 21px; }
14957
+ [data-vela-layout='mobile'] .vela-attribution:hover .vela-attr-wordmark,
14958
+ [data-layout='mobile'] .vela-attribution:hover .vela-attr-wordmark { transform: translateX(0); }
14959
+ `;
14960
+ function ensureStyles6(doc) {
14961
+ let s = doc.getElementById(STYLE_ID6);
14322
14962
  if (!s) {
14323
14963
  s = doc.createElement("style");
14324
- s.id = STYLE_ID5;
14964
+ s.id = STYLE_ID6;
14325
14965
  doc.head.appendChild(s);
14326
14966
  }
14327
14967
  s.textContent = CSS;
@@ -14350,7 +14990,7 @@ function createCustomMark(doc, html, background) {
14350
14990
  return el;
14351
14991
  }
14352
14992
  function createAttributionMark(doc, background) {
14353
- ensureStyles5(doc);
14993
+ ensureStyles6(doc);
14354
14994
  const a = doc.createElement("a");
14355
14995
  a.className = "vela-attribution";
14356
14996
  a.href = ATTRIBUTION_URL;
@@ -14376,6 +15016,174 @@ function createAttributionMark(doc, background) {
14376
15016
  return a;
14377
15017
  }
14378
15018
 
15019
+ // src/renderers/shared/dom-raster.ts
15020
+ function hasInk(color) {
15021
+ if (!color || color === "transparent") return false;
15022
+ const m = /rgba?\(\s*[\d.]+\s*,\s*[\d.]+\s*,\s*[\d.]+\s*,\s*([\d.]+)\s*\)/.exec(color);
15023
+ return m ? parseFloat(m[1]) > 1e-3 : true;
15024
+ }
15025
+ function cornerRadius(cs, w, h) {
15026
+ const raw = cs.borderTopLeftRadius;
15027
+ const v = parseFloat(raw) || 0;
15028
+ const px = raw.endsWith("%") ? v / 100 * Math.min(w, h) : v;
15029
+ return Math.max(0, Math.min(px, Math.min(w, h) / 2));
15030
+ }
15031
+ function tracePath(ctx, x, y, w, h, r) {
15032
+ ctx.beginPath();
15033
+ if (r > 0 && typeof ctx.roundRect === "function") ctx.roundRect(x, y, w, h, r);
15034
+ else ctx.rect(x, y, w, h);
15035
+ }
15036
+ function canDrawUntainted(img) {
15037
+ try {
15038
+ const probe2 = img.ownerDocument.createElement("canvas");
15039
+ probe2.width = 1;
15040
+ probe2.height = 1;
15041
+ const ctx = probe2.getContext("2d");
15042
+ if (!ctx) return false;
15043
+ ctx.drawImage(img, 0, 0, 1, 1);
15044
+ probe2.toDataURL();
15045
+ return true;
15046
+ } catch {
15047
+ return false;
15048
+ }
15049
+ }
15050
+ function rasterizeOverlay(ctx, root, frame) {
15051
+ const win = root.ownerDocument.defaultView;
15052
+ if (!win) return;
15053
+ ctx.save();
15054
+ ctx.scale(frame.dpr, frame.dpr);
15055
+ const drawText = (node, cs, alpha) => {
15056
+ const text = node.textContent?.replace(/\s+/g, " ").trim();
15057
+ if (!text) return;
15058
+ const range = node.ownerDocument.createRange();
15059
+ range.selectNodeContents(node);
15060
+ const r = range.getBoundingClientRect();
15061
+ if (r.width <= 0 || r.height <= 0) return;
15062
+ ctx.globalAlpha = alpha;
15063
+ ctx.fillStyle = cs.color;
15064
+ ctx.font = `${cs.fontStyle} ${cs.fontWeight} ${cs.fontSize} ${cs.fontFamily}`;
15065
+ ctx.textBaseline = "middle";
15066
+ ctx.textAlign = "left";
15067
+ ctx.fillText(text, r.left - frame.left, r.top - frame.top + r.height / 2);
15068
+ };
15069
+ const drawImg = (img, cs, alpha) => {
15070
+ if (!img.complete || img.naturalWidth <= 0 || !canDrawUntainted(img)) return;
15071
+ const r = img.getBoundingClientRect();
15072
+ if (r.width <= 0 || r.height <= 0) return;
15073
+ const x = r.left - frame.left;
15074
+ const y = r.top - frame.top;
15075
+ ctx.save();
15076
+ ctx.globalAlpha = alpha;
15077
+ tracePath(ctx, x, y, r.width, r.height, cornerRadius(cs, r.width, r.height));
15078
+ ctx.clip();
15079
+ ctx.drawImage(img, x, y, r.width, r.height);
15080
+ ctx.restore();
15081
+ };
15082
+ const drawSvgShapes = (parent, alpha) => {
15083
+ for (const el of parent.children) {
15084
+ if (el.hasAttribute("transform")) continue;
15085
+ const tag = el.tagName.toLowerCase();
15086
+ if (tag === "g") {
15087
+ drawSvgShapes(el, alpha);
15088
+ continue;
15089
+ }
15090
+ const num3 = (name) => parseFloat(el.getAttribute(name) ?? "0") || 0;
15091
+ let path = null;
15092
+ if (tag === "path") {
15093
+ const d = el.getAttribute("d");
15094
+ if (d) path = new Path2D(d);
15095
+ } else if (tag === "circle") {
15096
+ path = new Path2D();
15097
+ path.arc(num3("cx"), num3("cy"), num3("r"), 0, Math.PI * 2);
15098
+ } else if (tag === "ellipse") {
15099
+ path = new Path2D();
15100
+ path.ellipse(num3("cx"), num3("cy"), num3("rx"), num3("ry"), 0, 0, Math.PI * 2);
15101
+ } else if (tag === "rect") {
15102
+ path = new Path2D();
15103
+ path.rect(num3("x"), num3("y"), num3("width"), num3("height"));
15104
+ } else if (tag === "line") {
15105
+ path = new Path2D();
15106
+ path.moveTo(num3("x1"), num3("y1"));
15107
+ path.lineTo(num3("x2"), num3("y2"));
15108
+ } else if (tag === "polyline" || tag === "polygon") {
15109
+ const pts = (el.getAttribute("points") ?? "").trim().split(/[\s,]+/).map(Number);
15110
+ if (pts.length < 4 || pts.some(Number.isNaN)) continue;
15111
+ path = new Path2D();
15112
+ path.moveTo(pts[0], pts[1]);
15113
+ for (let i = 2; i + 1 < pts.length; i += 2) path.lineTo(pts[i], pts[i + 1]);
15114
+ if (tag === "polygon") path.closePath();
15115
+ }
15116
+ if (!path) continue;
15117
+ const cs = win.getComputedStyle(el);
15118
+ if (cs.fill && cs.fill !== "none" && hasInk(cs.fill)) {
15119
+ ctx.globalAlpha = alpha * (parseFloat(cs.fillOpacity) || 1);
15120
+ ctx.fillStyle = cs.fill;
15121
+ ctx.fill(path);
15122
+ }
15123
+ const sw = parseFloat(cs.strokeWidth) || 0;
15124
+ if (cs.stroke && cs.stroke !== "none" && hasInk(cs.stroke) && sw > 0) {
15125
+ ctx.globalAlpha = alpha * (parseFloat(cs.strokeOpacity) || 1);
15126
+ ctx.strokeStyle = cs.stroke;
15127
+ ctx.lineWidth = sw;
15128
+ if (cs.strokeLinecap === "round" || cs.strokeLinecap === "square" || cs.strokeLinecap === "butt") ctx.lineCap = cs.strokeLinecap;
15129
+ if (cs.strokeLinejoin === "round" || cs.strokeLinejoin === "bevel" || cs.strokeLinejoin === "miter") ctx.lineJoin = cs.strokeLinejoin;
15130
+ ctx.stroke(path);
15131
+ }
15132
+ }
15133
+ };
15134
+ const drawSvg = (svg, parentAlpha) => {
15135
+ const r = svg.getBoundingClientRect();
15136
+ if (r.width <= 0 || r.height <= 0) return;
15137
+ const cs = win.getComputedStyle(svg);
15138
+ if (cs.display === "none" || cs.visibility === "hidden") return;
15139
+ const alpha = parentAlpha * (parseFloat(cs.opacity) || 1);
15140
+ if (alpha <= 1e-3) return;
15141
+ const vb = svg.viewBox.baseVal;
15142
+ const vw = vb && vb.width > 0 ? vb.width : r.width;
15143
+ const vh = vb && vb.height > 0 ? vb.height : r.height;
15144
+ ctx.save();
15145
+ ctx.translate(r.left - frame.left, r.top - frame.top);
15146
+ ctx.scale(r.width / vw, r.height / vh);
15147
+ if (vb) ctx.translate(-vb.x, -vb.y);
15148
+ drawSvgShapes(svg, alpha);
15149
+ ctx.restore();
15150
+ };
15151
+ const drawElement = (el, parentAlpha) => {
15152
+ const cs = win.getComputedStyle(el);
15153
+ if (cs.display === "none" || cs.visibility === "hidden") return;
15154
+ const alpha = parentAlpha * (parseFloat(cs.opacity) || 1);
15155
+ if (alpha <= 1e-3) return;
15156
+ const r = el.getBoundingClientRect();
15157
+ if (r.width > 0 && r.height > 0) {
15158
+ const x = r.left - frame.left;
15159
+ const y = r.top - frame.top;
15160
+ const radius = cornerRadius(cs, r.width, r.height);
15161
+ if (hasInk(cs.backgroundColor)) {
15162
+ ctx.globalAlpha = alpha;
15163
+ ctx.fillStyle = cs.backgroundColor;
15164
+ tracePath(ctx, x, y, r.width, r.height, radius);
15165
+ ctx.fill();
15166
+ }
15167
+ const bw = parseFloat(cs.borderTopWidth) || 0;
15168
+ if (bw > 0 && cs.borderTopStyle !== "none" && hasInk(cs.borderTopColor)) {
15169
+ ctx.globalAlpha = alpha;
15170
+ ctx.strokeStyle = cs.borderTopColor;
15171
+ ctx.lineWidth = bw;
15172
+ tracePath(ctx, x + bw / 2, y + bw / 2, r.width - bw, r.height - bw, Math.max(0, radius - bw / 2));
15173
+ ctx.stroke();
15174
+ }
15175
+ }
15176
+ for (const child of el.childNodes) {
15177
+ if (child.nodeType === 3) drawText(child, cs, alpha);
15178
+ else if (child instanceof win.HTMLImageElement) drawImg(child, win.getComputedStyle(child), alpha);
15179
+ else if (child instanceof win.SVGSVGElement) drawSvg(child, alpha);
15180
+ else if (child instanceof win.HTMLElement) drawElement(child, alpha);
15181
+ }
15182
+ };
15183
+ if (root instanceof win.HTMLElement) drawElement(root, 1);
15184
+ ctx.restore();
15185
+ }
15186
+
14379
15187
  // src/renderers/native/vpvr/profile.ts
14380
15188
  function buildVpvrProfile(bars, i0, i1, rowCount, valueAreaFrac) {
14381
15189
  const from = Math.max(0, i0);
@@ -14544,12 +15352,13 @@ var AXIS_COL_W = AXIS_MERGED_W;
14544
15352
  var COLLAPSED_PANE_H = 26;
14545
15353
  var TIME_AXIS_H = 22;
14546
15354
  var SCROLL_BTN_SIZE = 26;
15355
+ var SCROLL_BTN_SIZE_TOUCH = 24;
14547
15356
  var SCROLL_BTN_RADIUS = 5;
14548
15357
  var SCROLL_BTN_SHADOW = "0 2px 8px rgba(0,0,0,0.4)";
14549
15358
  var SCROLL_BTN_ICON_PX = 12;
14550
- var SCROLL_BTN_RIGHT_INSET = 25;
15359
+ var SCROLL_BTN_RIGHT_INSET = 12;
14551
15360
  var SCROLL_BTN_RIGHT = RIGHT_AXIS_W + SCROLL_BTN_RIGHT_INSET;
14552
- var SCROLL_BTN_BOTTOM = TIME_AXIS_H + 30;
15361
+ var SCROLL_BTN_BOTTOM = TIME_AXIS_H + 14;
14553
15362
  var SCROLL_BTN_PROXIMITY_PX = 120;
14554
15363
  var MIN_VISIBLE_BARS = 2;
14555
15364
  var ZOOM_OUT_MARGIN_BARS = 6;
@@ -14601,8 +15410,12 @@ var NativeRenderer = class {
14601
15410
  this.symbolPicker = null;
14602
15411
  /** Indicator titles (the legend rows) shown — held here so a remount re-applies it. */
14603
15412
  this.indicatorTitlesOn = true;
15413
+ /** Plot values beside the legend titles shown — held here so a remount re-applies it. */
15414
+ this.indicatorValuesOn = true;
14604
15415
  /** Host-contributed legend actions — held here so a rebuild of the legend re-wires them. */
14605
15416
  this.legendActionsProvider = null;
15417
+ /** Host override of the legend's fold toggle — held here so a remount re-applies it. */
15418
+ this.legendOverviewAction = null;
14606
15419
  // ── keyboard navigation / accessibility (item 11) ──
14607
15420
  this.keyboard = null;
14608
15421
  this.keyboardEnabled = true;
@@ -14712,6 +15525,8 @@ var NativeRenderer = class {
14712
15525
  // Distance (px) from the plot's right edge — clears the FULL scale gutter, which widens when a
14713
15526
  // pane carries merged (own-scale) columns; the constant only clears a single-column scale.
14714
15527
  this.scrollBtnRightPx = SCROLL_BTN_RIGHT;
15528
+ // ── chrome size class (pushed by the host shell; see IChartRenderer.setLayoutMode) ──
15529
+ this.layoutMode = "desktop";
14715
15530
  this.viewportCbs = /* @__PURE__ */ new Set();
14716
15531
  this.crosshairCbs = /* @__PURE__ */ new Set();
14717
15532
  this.chartTypeSettingsCbs = /* @__PURE__ */ new Set();
@@ -14722,6 +15537,7 @@ var NativeRenderer = class {
14722
15537
  this.factoryConfig = null;
14723
15538
  this.hostSettingsSections = [];
14724
15539
  this.clickCbs = /* @__PURE__ */ new Set();
15540
+ this.axisLongPressCbs = /* @__PURE__ */ new Set();
14725
15541
  this.inputChangeCbs = /* @__PURE__ */ new Set();
14726
15542
  this.removeIndicatorCbs = /* @__PURE__ */ new Set();
14727
15543
  this.toggleVisibleCbs = /* @__PURE__ */ new Set();
@@ -14729,7 +15545,7 @@ var NativeRenderer = class {
14729
15545
  this.priceStyleCbs = /* @__PURE__ */ new Set();
14730
15546
  this.tableOverlays = /* @__PURE__ */ new Map();
14731
15547
  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"];
15548
+ 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
15549
  /** Track cursor proximity to the scroll button on the plot (bubbles from the button too,
14734
15550
  * so moving onto the button doesn't count as leaving). */
14735
15551
  this.onScrollProximityMove = (e) => {
@@ -14902,6 +15718,11 @@ var NativeRenderer = class {
14902
15718
  this.inputsUI?.setTitlesVisible(this.indicatorTitlesOn);
14903
15719
  return;
14904
15720
  // own DOM, no repaint needed
15721
+ case "indicatorValues":
15722
+ this.indicatorValuesOn = value !== false;
15723
+ this.inputsUI?.setValuesVisible(this.indicatorValuesOn);
15724
+ return;
15725
+ // own DOM, no repaint needed
14905
15726
  case "attribution":
14906
15727
  this.attributionHtml = typeof value === "string" && value.trim() ? value : null;
14907
15728
  this.attributionEnabled = this.attributionHtml !== null || Boolean(value);
@@ -14990,6 +15811,8 @@ var NativeRenderer = class {
14990
15811
  return this.settingsEnabled;
14991
15812
  case "indicatorTitles":
14992
15813
  return this.indicatorTitlesOn;
15814
+ case "indicatorValues":
15815
+ return this.indicatorValuesOn;
14993
15816
  case "attribution":
14994
15817
  return this.attributionHtml ?? this.attributionEnabled;
14995
15818
  case "dialogHost":
@@ -15326,7 +16149,10 @@ var NativeRenderer = class {
15326
16149
  /** Port surface: hosts (bottom-bar / chrome buttons) open the same dialog as the
15327
16150
  * in-chart gear — created on demand, independent of the gear feature being enabled. */
15328
16151
  openSettingsDialog(section) {
15329
- if (!this.settingsDialog && this.plot) this.settingsDialog = new SettingsDialog(this.dialogHost ?? this.plot, this.theme);
16152
+ if (!this.settingsDialog && this.plot) {
16153
+ this.settingsDialog = new SettingsDialog(this.dialogHost ?? this.plot, this.theme);
16154
+ this.settingsDialog.setLayoutMode(this.layoutMode);
16155
+ }
15330
16156
  this.toggleSettingsDialog(section);
15331
16157
  }
15332
16158
  /** (Re)aim the dialog's Canvas → Theme row: the current selection reflects the STABLE
@@ -15415,11 +16241,18 @@ var NativeRenderer = class {
15415
16241
  this.scrollButton.style.color = this.theme.textColor;
15416
16242
  this.scrollButton.style.borderColor = this.theme.borderColor;
15417
16243
  }
15418
- /** Show the button only when the latest bar is scrolled off the right edge AND the cursor is nearby. */
16244
+ /** Show the button only when the latest bar is scrolled off the right edge. On desktop the
16245
+ * cursor must also be nearby (hover reveal); on mobile — no hovering cursor — the button
16246
+ * appears whenever the latest bars are off-screen and hides again once the view is back
16247
+ * at the right edge. */
15419
16248
  updateScrollToRealtimeButton() {
15420
16249
  const btn = this.scrollButton;
15421
16250
  if (!btn) return;
15422
16251
  const latestOffScreen = this.coords.barCount > 0 && this.coords.getViewport().rightOffset < 0;
16252
+ if (this.layoutMode === "mobile") {
16253
+ btn.style.display = latestOffScreen ? "flex" : "none";
16254
+ return;
16255
+ }
15423
16256
  btn.style.display = latestOffScreen && this.pointerNearScrollBtn ? "flex" : "none";
15424
16257
  }
15425
16258
  /** Glide the view back to the most recent bars, keeping the current zoom (barSpacing). */
@@ -15441,17 +16274,20 @@ var NativeRenderer = class {
15441
16274
  this.animator.start();
15442
16275
  }
15443
16276
  /**
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.
16277
+ * Export the current chart as a PNG data URL by compositing EVERY plot canvas onto
16278
+ * an offscreen canvas, in the same stacking order the DOM shows: plugin layers
16279
+ * below the data, geometry (L0), volume columns (L0.25), the visible-range volume
16280
+ * profile (L0.6), plugin layers above the data, chrome (L1), and user drawings
16281
+ * (L1.5). The background is filled first (the canvas2d data layer is transparent —
16282
+ * its bg lives on the wrapper), and a fresh synchronous paint runs first so the
16283
+ * WebGL2 backend's (non-preserved) drawing buffer is populated before it's read
16284
+ * back this tick the same paint also repaints the drawings layers, so they're
16285
+ * current. DOM chrome joins as a best-effort text/chip raster (see
16286
+ * `rasterizeOverlay`): the per-pane indicator legends, plus any host overlay that
16287
+ * opts in with a `data-vela-screenshot` attribute on the mount container's
16288
+ * subtree (the widget marks its status line, and its symbol watermark with
16289
+ * `"under"` — drawn beneath the canvases, where it sits on screen). Only the
16290
+ * crosshair (L2) is intentionally excluded.
15455
16291
  */
15456
16292
  screenshot() {
15457
16293
  if (!this.dataCanvas) return null;
@@ -15464,9 +16300,25 @@ var NativeRenderer = class {
15464
16300
  if (!ctx) return null;
15465
16301
  ctx.fillStyle = this.theme.background;
15466
16302
  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);
16303
+ const plotRect = this.plot?.getBoundingClientRect();
16304
+ const frame = plotRect && plotRect.width > 0 ? { left: plotRect.left, top: plotRect.top, dpr: out.width / plotRect.width } : null;
16305
+ const hostMarked = [...this.mountContainer?.querySelectorAll("[data-vela-screenshot]") ?? []];
16306
+ if (frame) {
16307
+ for (const el of hostMarked) {
16308
+ if (el.getAttribute("data-vela-screenshot") === "under") rasterizeOverlay(ctx, el, frame);
16309
+ }
16310
+ }
16311
+ const below = this.extLayers.filter((l) => l.def.placement === "below-data").map((l) => l.canvas);
16312
+ const above = this.extLayers.filter((l) => l.def.placement !== "below-data").map((l) => l.canvas);
16313
+ for (const canvas of [...below, this.dataCanvas, this.volumeCanvas, this.vpvrCanvas, ...above, this.chromeCanvas, this.drawingsCanvas]) {
16314
+ if (canvas && canvas.width > 0 && canvas.height > 0) ctx.drawImage(canvas, 0, 0);
16315
+ }
16316
+ if (frame) {
16317
+ for (const lg of this.plot?.querySelectorAll("[data-vela-pane]") ?? []) rasterizeOverlay(ctx, lg, frame);
16318
+ for (const el of hostMarked) {
16319
+ if (el.getAttribute("data-vela-screenshot") !== "under") rasterizeOverlay(ctx, el, frame);
16320
+ }
16321
+ }
15470
16322
  return out.toDataURL("image/png");
15471
16323
  }
15472
16324
  /**
@@ -15617,6 +16469,9 @@ var NativeRenderer = class {
15617
16469
  fling: (v) => this.fling(v),
15618
16470
  onPointerMove: (x, y) => this.handlePointerMove(x, y),
15619
16471
  onClick: (x) => this.handleClick(x),
16472
+ onAxisLongPress: (axis, x, y) => {
16473
+ for (const cb of this.axisLongPressCbs) cb({ axis, x, y });
16474
+ },
15620
16475
  beginPriceScale: (x, y) => this.beginPriceScale(x, y),
15621
16476
  priceScaleBy: (dy) => this.priceScaleBy(dy),
15622
16477
  beginPricePan: (y) => this.beginPricePan(y),
@@ -15663,6 +16518,14 @@ var NativeRenderer = class {
15663
16518
  setToolbarGutter: (px) => this.setToolbarGutter(px)
15664
16519
  });
15665
16520
  this.userDrawings.historyChords = this.historyChordsEnabled;
16521
+ if (this.layoutMode === "mobile") {
16522
+ this.mountContainer?.setAttribute("data-vela-layout", this.layoutMode);
16523
+ this.userDrawings.setLayoutMode(this.layoutMode);
16524
+ if (this.scrollButton) {
16525
+ this.scrollButton.style.width = `${SCROLL_BTN_SIZE_TOUCH}px`;
16526
+ this.scrollButton.style.height = `${SCROLL_BTN_SIZE_TOUCH}px`;
16527
+ }
16528
+ }
15666
16529
  this.keyboard = new KeyboardController({
15667
16530
  panByBars: (bars) => this.panByBars(bars),
15668
16531
  zoomByStep: (dir) => this.zoomByStep(dir),
@@ -15675,10 +16538,13 @@ var NativeRenderer = class {
15675
16538
  });
15676
16539
  this.setKeyboardEnabled(this.keyboardEnabled);
15677
16540
  this.inputsUI = new InputsUI(this.plot, theme, (paneId) => this.paneBoundsFor(paneId));
16541
+ this.inputsUI.setLayoutMode(this.layoutMode);
15678
16542
  this.inputsUI.setTitlesVisible(this.indicatorTitlesOn);
16543
+ this.inputsUI.setValuesVisible(this.indicatorValuesOn);
15679
16544
  this.inputsUI.setDialogHost(this.dialogHost);
15680
16545
  this.inputsUI.setSymbolPicker(this.symbolPicker);
15681
16546
  this.inputsUI.setLegendActions(this.legendActionsProvider);
16547
+ this.inputsUI.setLegendOverviewAction(this.legendOverviewAction);
15682
16548
  this.inputsUI.setOnChange((c) => {
15683
16549
  for (const cb of this.inputChangeCbs) cb({ indicatorId: c.indicatorId, key: c.key, value: c.value });
15684
16550
  });
@@ -16093,7 +16959,10 @@ var NativeRenderer = class {
16093
16959
  this.scene.indicators.set(model.id, model);
16094
16960
  this.refreshAnchorOffset(model);
16095
16961
  this.scene.assignIndicatorZ(model.id);
16096
- this.inputsUI.upsert(model.id, model.title, model.inputs, model.inputValues, model.paneId, { native: !!model.native });
16962
+ this.inputsUI.upsert(model.id, model.shorttitle ?? model.title, model.inputs, model.inputValues, model.paneId, {
16963
+ native: !!model.native,
16964
+ ...model.shorttitle ? { settingsTitle: model.title } : {}
16965
+ });
16097
16966
  this.syncTables(model);
16098
16967
  if (model.native?.type === "volume") {
16099
16968
  this.volumeActive = true;
@@ -16154,6 +17023,13 @@ var NativeRenderer = class {
16154
17023
  this.legendActionsProvider = provider;
16155
17024
  this.inputsUI?.setLegendActions(provider);
16156
17025
  }
17026
+ setLegendOverviewAction(action) {
17027
+ this.legendOverviewAction = action;
17028
+ this.inputsUI?.setLegendOverviewAction(action);
17029
+ }
17030
+ openIndicatorSettings(indicatorId) {
17031
+ this.inputsUI?.openSettingsFor(indicatorId);
17032
+ }
16157
17033
  /**
16158
17034
  * Hide/show a mounted indicator. Hiding drops its model from the scene (so every paint path —
16159
17035
  * series, fills, drawings, tables, glow, data window — skips it) while keeping its z key and its
@@ -16207,6 +17083,24 @@ var NativeRenderer = class {
16207
17083
  this.removeIndicatorCbs.add(cb);
16208
17084
  return () => this.removeIndicatorCbs.delete(cb);
16209
17085
  }
17086
+ /** Port surface: the host shell's chrome size class. Mobile switches the renderer
17087
+ * chrome to its touch-first presentation — fullscreen dialogs, no docked drawing
17088
+ * toolbar (the shell provides its own picker), the scroll-to-latest button shown
17089
+ * whenever the latest bars are off-screen (touch has no cursor proximity). */
17090
+ setLayoutMode(mode) {
17091
+ if (mode === this.layoutMode) return;
17092
+ this.layoutMode = mode;
17093
+ this.mountContainer?.setAttribute("data-vela-layout", mode);
17094
+ this.userDrawings?.setLayoutMode(mode);
17095
+ this.settingsDialog?.setLayoutMode(mode);
17096
+ this.inputsUI?.setLayoutMode(mode);
17097
+ if (this.scrollButton) {
17098
+ const px = mode === "mobile" ? SCROLL_BTN_SIZE_TOUCH : SCROLL_BTN_SIZE;
17099
+ this.scrollButton.style.width = `${px}px`;
17100
+ this.scrollButton.style.height = `${px}px`;
17101
+ }
17102
+ this.updateScrollToRealtimeButton();
17103
+ }
16210
17104
  setSettingsSections(sections) {
16211
17105
  this.hostSettingsSections = sections;
16212
17106
  this.settingsDialog?.setHostSections(sections);
@@ -16231,6 +17125,10 @@ var NativeRenderer = class {
16231
17125
  this.clickCbs.add(cb);
16232
17126
  return () => this.clickCbs.delete(cb);
16233
17127
  }
17128
+ onAxisLongPress(cb) {
17129
+ this.axisLongPressCbs.add(cb);
17130
+ return () => this.axisLongPressCbs.delete(cb);
17131
+ }
16234
17132
  onViewportChange(cb) {
16235
17133
  this.viewportCbs.add(cb);
16236
17134
  return () => this.viewportCbs.delete(cb);
@@ -16372,6 +17270,7 @@ var NativeRenderer = class {
16372
17270
  this.skeletonClockMs += dtMs;
16373
17271
  this.paintData();
16374
17272
  this.crosshairLayer.render(this.scene, this.coords, this.theme, this.hoverSeparatorY, this.externalCrossPx());
17273
+ this.updateLegendValues();
16375
17274
  this.emitViewportChange();
16376
17275
  return active;
16377
17276
  }
@@ -16436,7 +17335,7 @@ var NativeRenderer = class {
16436
17335
  this.lastPointer = null;
16437
17336
  this.scheduler.invalidate(1 /* Cursor */);
16438
17337
  this.hoverLogical = null;
16439
- const empty = { time: null, price: null, values: /* @__PURE__ */ new Map(), ohlc: null };
17338
+ const empty = { time: null, price: null, paneKind: null, values: /* @__PURE__ */ new Map(), ohlc: null };
16440
17339
  for (const cb of this.crosshairCbs) cb(empty);
16441
17340
  return;
16442
17341
  }
@@ -16448,7 +17347,7 @@ var NativeRenderer = class {
16448
17347
  const logical = Math.round(this.coords.xToLogical(x));
16449
17348
  const onBar = logical >= 0 && logical < this.coords.barCount;
16450
17349
  const time = onBar ? this.coords.logicalToTime(logical) : null;
16451
- const pane = this.paneAtY(y);
17350
+ const pane = this.paneNodeAtY(y);
16452
17351
  const price = inData && pane ? this.coords.yToPrice(y, pane.scale, pane.bounds) : null;
16453
17352
  const values = /* @__PURE__ */ new Map();
16454
17353
  if (onBar) {
@@ -16465,7 +17364,7 @@ var NativeRenderer = class {
16465
17364
  const bar = onBar ? this.bars[logical] : void 0;
16466
17365
  const ohlc = bar ? { time: bar.time, open: bar.open, high: bar.high, low: bar.low, close: bar.close, volume: bar.volume } : null;
16467
17366
  this.hoverLogical = onBar ? logical : null;
16468
- const event = { time, price, values, ohlc };
17367
+ const event = { time, price, paneKind: inData && pane ? pane.kind : null, values, ohlc };
16469
17368
  for (const cb of this.crosshairCbs) cb(event);
16470
17369
  }
16471
17370
  handleClick(x) {
@@ -16484,6 +17383,20 @@ var NativeRenderer = class {
16484
17383
  }
16485
17384
  return null;
16486
17385
  }
17386
+ /** The pane whose bounds sit closest to `y` — the forgiving fallback for points that
17387
+ * fall BETWEEN bounds (separator gaps, the strip under the last pane). */
17388
+ nearestPaneToY(y) {
17389
+ let best = null;
17390
+ let bestDist = Infinity;
17391
+ for (const pane of this.scene.panes.values()) {
17392
+ const d = y < pane.bounds.top ? pane.bounds.top - y : y - (pane.bounds.top + pane.bounds.height);
17393
+ if (d < bestDist) {
17394
+ bestDist = d;
17395
+ best = pane;
17396
+ }
17397
+ }
17398
+ return best;
17399
+ }
16487
17400
  // ── manual vertical scaling (price-axis drag + vertical price pan) ──
16488
17401
  /** The scale window a gesture targets: a pane's master scale, or a merged indicator's
16489
17402
  * own scale column (resolved from the grabbed x). Both share the four scale fields. */
@@ -16546,7 +17459,7 @@ var NativeRenderer = class {
16546
17459
  * still fits the view.)
16547
17460
  */
16548
17461
  dataDblClick(_x, y) {
16549
- const pane = this.paneNodeAtY(y);
17462
+ const pane = this.paneNodeAtY(y) ?? this.nearestPaneToY(y);
16550
17463
  if (!pane) return;
16551
17464
  const maximized = this.maximizedPaneId !== pane.id;
16552
17465
  this.maximizedPaneId = maximized ? pane.id : null;
@@ -16787,29 +17700,63 @@ var NativeRenderer = class {
16787
17700
  dataWindowGroups(idx, pricePane) {
16788
17701
  const groups = [];
16789
17702
  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
- }
17703
+ const rows = this.dataWindowRowsFor(model, idx, pricePane);
16809
17704
  if (rows.length) groups.push({ name: model.title, rows });
16810
17705
  }
16811
17706
  return groups;
16812
17707
  }
17708
+ /** One indicator's readout at bar `idx`: a row per drawable plot, formatted on its pane's scale. */
17709
+ dataWindowRowsFor(model, idx, pricePane) {
17710
+ if (model.native?.type === "volume") return this.volumeReadoutRows(model, idx);
17711
+ const pane = (model.paneId ? this.scene.panes.get(model.paneId) : null) ?? pricePane;
17712
+ const off = this.scene.offsetOf(model.id);
17713
+ const rows = [];
17714
+ for (const s of model.series) {
17715
+ let value;
17716
+ let color;
17717
+ if (s.kind === "candle" || s.kind === "bar") {
17718
+ value = s.bars[idx - off]?.close;
17719
+ color = s.style?.up ?? this.theme.upColor;
17720
+ } else if (isLineLikeSeries(s)) {
17721
+ if (s.visible === false) continue;
17722
+ value = s.points[idx - off]?.value;
17723
+ color = s.points[idx - off]?.color ?? s.style.color;
17724
+ } else {
17725
+ continue;
17726
+ }
17727
+ if (value == null || !Number.isFinite(value)) continue;
17728
+ rows.push({ label: s.title || model.title, value: this.dataWindowFmt(value, pane), color });
17729
+ }
17730
+ return rows;
17731
+ }
17732
+ /** The volume indicator's readout: the bar's volume, tinted with the layer's own direction
17733
+ * colors. Hiding volume keeps its model in the scene (only the layer is suppressed — see
17734
+ * `setIndicatorVisible`), so the hidden flag is checked here rather than by model absence. */
17735
+ volumeReadoutRows(model, idx) {
17736
+ if (this.volumeHidden) return [];
17737
+ const bar = this.bars[idx];
17738
+ const vol = bar?.volume;
17739
+ if (bar == null || vol == null || !Number.isFinite(vol)) return [];
17740
+ const cfg = this.scene.volumeLayer;
17741
+ const color = bar.close >= bar.open ? cfg?.upColor ?? this.theme.upColor : cfg?.downColor ?? this.theme.downColor;
17742
+ return [{ label: model.title, value: formatVolume(vol), color }];
17743
+ }
17744
+ /** Refresh the plot values beside every legend title — the same readout the data window
17745
+ * shows (crosshair bar, else the latest bar), pushed per paint. A hidden indicator has
17746
+ * no scene model, so its row is absent from the map and its readout clears. */
17747
+ updateLegendValues() {
17748
+ if (!this.inputsUI) return;
17749
+ const n = this.bars.length;
17750
+ const values = /* @__PURE__ */ new Map();
17751
+ if (n > 0) {
17752
+ const idx = this.hoverLogical != null ? this.hoverLogical : n - 1;
17753
+ const pricePane = this.dataWindowPricePane();
17754
+ for (const model of this.scene.indicators.values()) {
17755
+ values.set(model.id, this.dataWindowRowsFor(model, idx, pricePane).map((r) => ({ value: r.value, color: r.color })));
17756
+ }
17757
+ }
17758
+ this.inputsUI.setPlotValues(values);
17759
+ }
16813
17760
  renderFrame(level) {
16814
17761
  if (this.animator.active) return;
16815
17762
  if (repaintsData(level)) {
@@ -16821,6 +17768,7 @@ var NativeRenderer = class {
16821
17768
  }
16822
17769
  this.crosshairLayer.render(this.scene, this.coords, this.theme, this.hoverSeparatorY, this.externalCrossPx());
16823
17770
  if (!repaintsData(level) && this.paintedData) this.repaintCursorLayers();
17771
+ this.updateLegendValues();
16824
17772
  }
16825
17773
  /** Repaint the SDK layers that opted into cursor tracking (their own canvas only). */
16826
17774
  repaintCursorLayers() {
@@ -16969,7 +17917,7 @@ var NativeRenderer = class {
16969
17917
  if (!Number.isFinite(y) || y < pricePane.bounds.top || y > pricePane.bounds.top + pricePane.bounds.height) y = null;
16970
17918
  }
16971
17919
  }
16972
- return { x, y, time: this.coords.logicalToTime(logical) };
17920
+ return { x, y, time: this.coords.logicalToTime(logical), price: y != null ? ext.price : null };
16973
17921
  }
16974
17922
  /** Set the sticky magnet mode (called by the drawings toolbar's 3-state button). */
16975
17923
  setSnapMode(mode) {
@@ -17739,6 +18687,9 @@ var EngineOrchestrator = class _EngineOrchestrator {
17739
18687
  this.barTransform = null;
17740
18688
  // ── chart-type data engines (SDK): one lazily-created engine per style id ──
17741
18689
  this.typeEngines = /* @__PURE__ */ new Map();
18690
+ /** Last-seen chart-type settings per type id (dialog edits + persisted restores) —
18691
+ * replayed into a data engine whenever one is (re)created. */
18692
+ this.typeSettings = /* @__PURE__ */ new Map();
17742
18693
  /** The style id whose data engine is currently ACTIVE (drives suspend/resume + pokes). */
17743
18694
  this.activeEngineStyle = null;
17744
18695
  /** The chart's current price style (tracked from the renderer's change events + initial read). */
@@ -17801,7 +18752,10 @@ var EngineOrchestrator = class _EngineOrchestrator {
17801
18752
  this.renderer.onRemoveIndicator((id) => this.removeIndicator(id));
17802
18753
  this.renderer.onToggleIndicatorVisible?.((id, visible) => this.setVisible(id, visible));
17803
18754
  this.paneActionUnsub = this.renderer.onPaneAction?.((a) => this.handlePaneAction(a)) ?? null;
17804
- this.renderer.onChartTypeSettingsChange?.((typeId, values) => this.typeEngines.get(typeId)?.onSettings?.(values));
18755
+ this.renderer.onChartTypeSettingsChange?.((typeId, values) => {
18756
+ this.typeSettings.set(typeId, values);
18757
+ this.typeEngines.get(typeId)?.onSettings?.(values);
18758
+ });
17805
18759
  this.renderer.onMoveIndicator?.((id, target) => this.moveIndicator(id, target));
17806
18760
  this.renderer.onThemeSelect?.((name) => this.setTheme(resolveTheme(name)));
17807
18761
  this.viewportUnsub = this.renderer.onViewportChange((range) => this.onViewportChange(range));
@@ -18289,6 +19243,8 @@ var EngineOrchestrator = class _EngineOrchestrator {
18289
19243
  if (!def?.dataEngine) return;
18290
19244
  const engine = def.dataEngine();
18291
19245
  this.typeEngines.set(id, engine);
19246
+ const stored = this.typeSettings.get(id);
19247
+ if (stored) engine.onSettings?.(stored);
18292
19248
  engine.start({
18293
19249
  symbol: this.config.market.symbol ?? "TEST",
18294
19250
  timeframe: this.config.market.timeframe ?? "60",
@@ -18760,6 +19716,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
18760
19716
  return {
18761
19717
  id: record.id,
18762
19718
  title: record.title,
19719
+ ...d.shortTitle ? { shorttitle: d.shortTitle } : {},
18763
19720
  overlay: d.overlay,
18764
19721
  paneHint: d.paneHint,
18765
19722
  native: { type: record.native.type },
@@ -19367,7 +20324,8 @@ var VpvrIndicator = class {
19367
20324
  };
19368
20325
  var vpvrDescriptor = {
19369
20326
  type: "vpvr",
19370
- title: "VPVR",
20327
+ title: "Visible Range Volume Profile",
20328
+ shortTitle: "VRVP",
19371
20329
  paneHint: "price",
19372
20330
  overlay: true,
19373
20331
  inputsSchema: () => [
@@ -19724,4 +20682,4 @@ function resolveElement(container) {
19724
20682
  return element;
19725
20683
  }
19726
20684
 
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 };
20685
+ 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 };