@luxalgo/vela 0.5.0 → 0.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/dist/{DataProvider-Dzd-Erlk.d.ts → DataProvider-DTOEXKTI.d.ts} +1 -1
  2. package/dist/{DataProvider-DKNDHpNv.d.cts → DataProvider-gnzv-dgc.d.cts} +1 -1
  3. package/dist/{chunk-KM6LHB3Y.js → chunk-ATPU5CI6.js} +6 -1
  4. package/dist/{chunk-7UFX5ZIG.js → chunk-B5TIKQJ5.js} +65 -7
  5. package/dist/{chunk-GYD2THPV.js → chunk-LJLZR44Y.js} +9 -2
  6. package/dist/{chunk-RHIDOUFL.js → chunk-P556H6EA.js} +329 -10
  7. package/dist/{chunk-KCCZNKH7.js → chunk-RRFGWZNJ.js} +1915 -367
  8. package/dist/{chunk-OVQKKXLZ.js → chunk-Y7TK4ZWK.js} +1810 -153
  9. package/dist/{contributions-o1GRKPI_.d.cts → contributions-Ci_i3IN2.d.cts} +77 -31
  10. package/dist/{contributions-hX3EUyjG.d.ts → contributions-b6AVrqqG.d.ts} +77 -31
  11. package/dist/{history-LJkz4-sS.d.cts → history-Cf1lVsap.d.cts} +22 -5
  12. package/dist/{history-Dzxz-MQj.d.ts → history-Ci3M0xLV.d.ts} +22 -5
  13. package/dist/index.cjs +2010 -397
  14. package/dist/index.d.cts +83 -25
  15. package/dist/index.d.ts +83 -25
  16. package/dist/index.js +4 -4
  17. package/dist/{options-Q-576hIi.d.cts → options-XTBpbx0s.d.cts} +61 -1
  18. package/dist/{options-Q-576hIi.d.ts → options-XTBpbx0s.d.ts} +61 -1
  19. package/dist/{plugin-aGUD1epn.d.ts → plugin-CnNd7cuF.d.ts} +305 -8
  20. package/dist/{plugin-D94muTV-.d.cts → plugin-DfH4Y-Om.d.cts} +305 -8
  21. package/dist/plugin.cjs +58 -7
  22. package/dist/plugin.d.cts +4 -4
  23. package/dist/plugin.d.ts +4 -4
  24. package/dist/plugin.js +2 -2
  25. package/dist/providers/binance.d.cts +2 -2
  26. package/dist/providers/binance.d.ts +2 -2
  27. package/dist/providers/coinbase.d.cts +2 -2
  28. package/dist/providers/coinbase.d.ts +2 -2
  29. package/dist/providers/hyperliquid.d.cts +2 -2
  30. package/dist/providers/hyperliquid.d.ts +2 -2
  31. package/dist/ui.cjs +342 -9
  32. package/dist/ui.d.cts +64 -2
  33. package/dist/ui.d.ts +64 -2
  34. package/dist/ui.js +3 -3
  35. package/dist/vela.global.js +2010 -397
  36. package/dist/vela.global.min.js +115 -34
  37. package/dist/widget.cjs +5987 -2242
  38. package/dist/widget.d.cts +89 -17
  39. package/dist/widget.d.ts +89 -17
  40. package/dist/widget.js +169 -18
  41. package/dist/workspace.cjs +5627 -1587
  42. package/dist/workspace.d.cts +106 -12
  43. package/dist/workspace.d.ts +106 -12
  44. package/dist/workspace.js +494 -52
  45. package/package.json +1 -1
@@ -1,6 +1,6 @@
1
- import { deserializeDrawing, chartTypes, rendererLayers, registerChartType, rendererDefaults, getDrawingType, 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-7UFX5ZIG.js';
2
- import { themeTokens, STATIC_TOKENS } from './chunk-KM6LHB3Y.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-GYD2THPV.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
@@ -1300,6 +1333,14 @@ var DrawingsControl = class {
1300
1333
  if (this.ok("setToolShortcuts")) this.ctrl.setToolShortcuts(map);
1301
1334
  return this;
1302
1335
  }
1336
+ /** Display another chart's in-progress placement as a GHOST at reduced opacity
1337
+ * (`null` clears it) — how a multi-chart host mirrors `drawing:draft` onto linked
1338
+ * charts. Never a real drawing: no store entry, no selection, no persistence.
1339
+ * Silently inert on a renderer without the optional seam. */
1340
+ setExternalGhost(doc) {
1341
+ this.ctrl.setExternalGhost(doc);
1342
+ return this;
1343
+ }
1303
1344
  /** Create a drawing programmatically (no clicking). Returns it, or null if unsupported. */
1304
1345
  add(type, init = {}) {
1305
1346
  if (!this.ok("add")) return null;
@@ -1510,13 +1551,9 @@ function tzMenuLabel(zone, location) {
1510
1551
  if (normalizeTimezone(zone) === "Etc/UTC") return location;
1511
1552
  return `(${tzOffset(zone)}) ${location}`;
1512
1553
  }
1513
- function tzLocation(zone) {
1514
- const entry = TIMEZONES.find((t) => t.value === normalizeTimezone(zone));
1515
- return entry?.label ?? (zone.split("/").pop() ?? zone).replace(/_/g, " ");
1516
- }
1517
1554
  function tzButtonLabel(zone) {
1518
1555
  if (normalizeTimezone(zone) === "Etc/UTC") return "UTC";
1519
- return `${tzOffset(zone)} ${tzLocation(zone)}`;
1556
+ return tzOffset(zone);
1520
1557
  }
1521
1558
 
1522
1559
  // src/core/drawings/effectiveColor.ts
@@ -1727,6 +1764,11 @@ var DrawingController = class {
1727
1764
  setToolbar(option) {
1728
1765
  this.port?.setToolbar(buildToolbar(option).definition);
1729
1766
  }
1767
+ /** Display (or clear) another chart's in-progress placement as a ghost — silently
1768
+ * inert on a renderer without the optional `setExternalGhost` seam. */
1769
+ setExternalGhost(doc) {
1770
+ this.port?.setExternalGhost?.(doc);
1771
+ }
1730
1772
  /** Push per-tool shortcut hints (pre-formatted display strings) to the toolbar flyouts. */
1731
1773
  setToolShortcuts(map) {
1732
1774
  this.port?.setToolShortcuts?.(map);
@@ -1957,6 +1999,9 @@ var DrawingController = class {
1957
1999
  case "arm":
1958
2000
  this.setTool(i.type);
1959
2001
  break;
2002
+ case "draft":
2003
+ this.events.emit("drawing:draft", { doc: i.doc });
2004
+ break;
1960
2005
  case "create": {
1961
2006
  const before = this.store.serialize();
1962
2007
  const last = this.lastStyle.get(i.doc.type);
@@ -2073,7 +2118,7 @@ function attachChromeTooltip(anchor, opts) {
2073
2118
  const doc = anchor.ownerDocument;
2074
2119
  tip = doc.createElement("div");
2075
2120
  tip.textContent = text;
2076
- 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;");
2077
2122
  applyChromeTokens(tip, opts.theme());
2078
2123
  opts.host.appendChild(tip);
2079
2124
  const a = anchor.getBoundingClientRect();
@@ -2087,16 +2132,17 @@ function attachChromeTooltip(anchor, opts) {
2087
2132
  tip.style.top = `${a.bottom - h.top + 6}px`;
2088
2133
  }
2089
2134
  };
2090
- const arm = () => {
2135
+ const arm = (e) => {
2136
+ if (e.pointerType !== "mouse") return;
2091
2137
  clear();
2092
2138
  timer = window.setTimeout(show, opts.delayMs ?? 700);
2093
2139
  };
2094
- anchor.addEventListener("mouseenter", arm);
2095
- anchor.addEventListener("mouseleave", clear);
2140
+ anchor.addEventListener("pointerenter", arm);
2141
+ anchor.addEventListener("pointerleave", clear);
2096
2142
  anchor.addEventListener("pointerdown", clear);
2097
2143
  return () => {
2098
- anchor.removeEventListener("mouseenter", arm);
2099
- anchor.removeEventListener("mouseleave", clear);
2144
+ anchor.removeEventListener("pointerenter", arm);
2145
+ anchor.removeEventListener("pointerleave", clear);
2100
2146
  anchor.removeEventListener("pointerdown", clear);
2101
2147
  clear();
2102
2148
  };
@@ -2150,6 +2196,8 @@ var DrawingToolbar = class {
2150
2196
  this.eraserActive = false;
2151
2197
  this.stayBtn = null;
2152
2198
  this.stayActive = false;
2199
+ this.syncBtn = null;
2200
+ this.syncActive = false;
2153
2201
  this.collapseBtn = null;
2154
2202
  this.collapsed = false;
2155
2203
  this.visible = false;
@@ -2166,6 +2214,7 @@ var DrawingToolbar = class {
2166
2214
  this.dock = options.dock ?? "absolute";
2167
2215
  this.onCollapse = options.onCollapse ?? (() => {
2168
2216
  });
2217
+ this.onDrawingsSync = options.onDrawingsSync ?? null;
2169
2218
  ensureStyles();
2170
2219
  this.root = document.createElement("div");
2171
2220
  this.root.className = "vela-dtb";
@@ -2264,6 +2313,10 @@ var DrawingToolbar = class {
2264
2313
  this.root.appendChild(this.makeMagnetCell());
2265
2314
  this.stayBtn = this.makeButton(STAY_ICON, "Stay in drawing mode", () => this.toggleStay());
2266
2315
  this.root.appendChild(this.stayBtn);
2316
+ if (this.onDrawingsSync) {
2317
+ this.syncBtn = this.makeButton(SYNC_ICON, "Sync drawings on all charts", () => this.toggleDrawingsSync());
2318
+ this.root.appendChild(this.syncBtn);
2319
+ }
2267
2320
  this.collapseBtn = this.makeButton(COLLAPSE_ICON, "Collapse toolbar", () => this.toggleCollapsed());
2268
2321
  this.collapseBtn.classList.add("vela-dtb-collapse");
2269
2322
  this.root.appendChild(this.collapseBtn);
@@ -2271,6 +2324,7 @@ var DrawingToolbar = class {
2271
2324
  this.paintEraser();
2272
2325
  this.paintMagnet();
2273
2326
  this.paintStay();
2327
+ this.paintDrawingsSync();
2274
2328
  this.paintCollapse();
2275
2329
  this.highlight();
2276
2330
  }
@@ -2321,7 +2375,7 @@ var DrawingToolbar = class {
2321
2375
  cell.appendChild(arrow);
2322
2376
  }
2323
2377
  this.tipDisposers.push(
2324
- attachChromeTooltip(cell, { host: this.host, theme: () => this.theme, text: () => this.tipText.get(cell) ?? label, placement: "right", delayMs: 2e3 })
2378
+ attachChromeTooltip(cell, { host: this.host, theme: () => this.theme, text: () => this.tipText.get(cell) ?? label, placement: "right" })
2325
2379
  );
2326
2380
  return { cell, icon: icon2, arrow };
2327
2381
  }
@@ -2431,6 +2485,23 @@ var DrawingToolbar = class {
2431
2485
  b.dataset.active = this.stayActive ? "1" : "";
2432
2486
  b.style.opacity = this.stayActive ? "1" : "0.5";
2433
2487
  }
2488
+ /** Toggle drawings sync on/off and notify the host. */
2489
+ toggleDrawingsSync() {
2490
+ this.syncActive = !this.syncActive;
2491
+ this.onDrawingsSync?.(this.syncActive);
2492
+ this.paintDrawingsSync();
2493
+ }
2494
+ /** Reflect drawings sync externally (e.g. set through the API) without notifying back. */
2495
+ setDrawingsSyncMode(on) {
2496
+ this.syncActive = on;
2497
+ this.paintDrawingsSync();
2498
+ }
2499
+ paintDrawingsSync() {
2500
+ const b = this.syncBtn;
2501
+ if (!b) return;
2502
+ b.dataset.active = this.syncActive ? "1" : "";
2503
+ b.style.opacity = this.syncActive ? "1" : "0.5";
2504
+ }
2434
2505
  // ── flyout ──
2435
2506
  /** Create the positioned flyout panel anchored to a cell, register it as open, and start the
2436
2507
  * outside-dismiss. Callers fill it with items. */
@@ -2590,6 +2661,8 @@ var DrawingToolbar = class {
2590
2661
  const type = this.lastUsed.get(group.id) ?? group.tools[0]?.type;
2591
2662
  const tool = group.tools.find((t) => t.type === type) ?? group.tools[0];
2592
2663
  iconBtn.innerHTML = hitHtml(tool?.icon ?? "");
2664
+ const cell = this.groupCells.get(group.id);
2665
+ if (cell) this.tipText.set(cell, tool?.label ?? group.label);
2593
2666
  }
2594
2667
  /** Reconcile active tints from state (armed group / open flyout / cursor / modes). Active is a
2595
2668
  * `data-active` attribute the stylesheet paints — no inline background, so hover never sticks. */
@@ -2613,7 +2686,7 @@ var DrawingToolbar = class {
2613
2686
  this.tipText.set(btn, title);
2614
2687
  if (icon2) btn.innerHTML = hitHtml(icon2);
2615
2688
  this.tipDisposers.push(
2616
- attachChromeTooltip(btn, { host: this.host, theme: () => this.theme, text: () => this.tipText.get(btn) ?? title, placement: "right", delayMs: 2e3 })
2689
+ attachChromeTooltip(btn, { host: this.host, theme: () => this.theme, text: () => this.tipText.get(btn) ?? title, placement: "right" })
2617
2690
  );
2618
2691
  btn.addEventListener("click", onClick);
2619
2692
  return btn;
@@ -2679,6 +2752,7 @@ var COLLAPSE_ICON = icon("chevrons-left");
2679
2752
  var EXPAND_ICON = icon("chevrons-right");
2680
2753
  var MAGNET_ICON = icon("magnet");
2681
2754
  var STAY_ICON = icon("pen-lock");
2755
+ var SYNC_ICON = icon("pen-sync");
2682
2756
  var ERASER_ICON = icon("eraser");
2683
2757
  var ARROW_ICON = icon("chevron-right");
2684
2758
  var CHECK_ICON = icon("check");
@@ -2764,14 +2838,22 @@ function makeDialogDraggable(box, header, options) {
2764
2838
 
2765
2839
  // src/renderers/shared/InputsUI.ts
2766
2840
  var SOURCES = ["close", "open", "high", "low", "hl2", "hlc3", "ohlc4", "volume"];
2767
- 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)}}";
2768
- 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;
2769
2850
  var EYE_SVG = iconAt("eye", LEGEND_ICON_PX);
2770
2851
  var EYE_OFF_SVG = iconAt("eye-off", LEGEND_ICON_PX);
2771
2852
  var GEAR_SVG = iconAt("gear", LEGEND_ICON_PX);
2772
- var CLOSE_SVG = iconAt("close", 11);
2853
+ var CLOSE_SVG = iconAt("close", LEGEND_ICON_PX);
2773
2854
  var FOLD_SVG = iconAt("chevron-up", LEGEND_ICON_PX);
2774
2855
  var UNFOLD_SVG = iconAt("chevron-down", LEGEND_ICON_PX);
2856
+ var OVERVIEW_SVG = iconAt("objects", LEGEND_ICON_PX);
2775
2857
  var InputsUI = class {
2776
2858
  constructor(container, theme, paneBoundsOf) {
2777
2859
  this.container = container;
@@ -2792,6 +2874,8 @@ var InputsUI = class {
2792
2874
  this.onToggleVisible = null;
2793
2875
  /** Host symbol picker for `input.symbol`; when set the control opens the host's ticker UI. */
2794
2876
  this.symbolPicker = null;
2877
+ /** Mobile chrome: the inputs dialog opens fullscreen and the legends render compact. */
2878
+ this.mobileLayout = false;
2795
2879
  /** Pane move/merge hook — when set, rows get a "Move to" menu + become drag-to-pane sources. */
2796
2880
  this.moveApi = null;
2797
2881
  /** Host-contributed legend actions, resolved PER ROW at render time (see setLegendActions). */
@@ -2813,9 +2897,18 @@ var InputsUI = class {
2813
2897
  /** Titles switched off entirely (a settings toggle): every pane's legend container
2814
2898
  * hides — rows, fold chevron and all — unlike the fold, which leaves its chip. */
2815
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;
2816
2905
  /** The single fold toggle, on the price-pane legend: a bordered ^ chevron under the
2817
2906
  * rows, or the bordered "˅ N" chip (N counts every pane's indicators) when folded. */
2818
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;
2819
2912
  /** Esc-to-close for the settings modal (bound so focus can sit anywhere). */
2820
2913
  this.onDialogKey = (e) => {
2821
2914
  if (e.key === "Escape") {
@@ -2839,6 +2932,9 @@ var InputsUI = class {
2839
2932
  this.onMoveMenuOutside = (e) => {
2840
2933
  if (this.moveMenu && !this.moveMenu.contains(e.target)) this.closeMoveMenu();
2841
2934
  };
2935
+ this.onRowMenuOutside = (e) => {
2936
+ if (this.rowMenu && !this.rowMenu.contains(e.target)) this.closeRowMenu();
2937
+ };
2842
2938
  if (!container.style.position) container.style.position = "relative";
2843
2939
  if (typeof document !== "undefined") document.addEventListener("click", this.onDocClick);
2844
2940
  }
@@ -2846,6 +2942,36 @@ var InputsUI = class {
2846
2942
  this.dialogHost = host;
2847
2943
  if (host && getComputedStyle(host).position === "static") host.style.position = "relative";
2848
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
+ }
2849
2975
  /** Attach a themed chrome tooltip to a control, recording its disposer under `id`. */
2850
2976
  tip(store, id, anchor, text, wrap = false) {
2851
2977
  const list = store.get(id) ?? [];
@@ -2873,12 +2999,12 @@ var InputsUI = class {
2873
2999
  }
2874
3000
  setTheme(theme) {
2875
3001
  this.theme = theme;
3002
+ for (const lg of this.legends.values()) applyChromeTokens(lg, theme);
2876
3003
  for (const row of this.rows.values()) {
2877
- const open = row.controlsEl.style.display !== "none";
2878
- row.el.style.background = open ? theme.background : this.idleRowFill();
3004
+ row.el.style.background = row.highlighted ? theme.background : this.idleRowFill();
2879
3005
  row.el.style.color = theme.textColor;
2880
3006
  }
2881
- if (this.foldToggle) this.foldToggle.style.background = theme.background;
3007
+ if (this.foldToggle) this.syncFoldToggle();
2882
3008
  }
2883
3009
  /** Provide (or clear) the host symbol picker that `input.symbol` opens on activation. */
2884
3010
  setSymbolPicker(picker) {
@@ -2909,7 +3035,7 @@ var InputsUI = class {
2909
3035
  btn.innerHTML = iconAt(action.icon, LEGEND_ICON_PX);
2910
3036
  btn.className = "vela-ind-ctl";
2911
3037
  btn.dataset.legendAction = action.id;
2912
- 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;
2913
3039
  btn.addEventListener("click", (e) => {
2914
3040
  e.stopPropagation();
2915
3041
  action.run();
@@ -2927,6 +3053,54 @@ var InputsUI = class {
2927
3053
  this.titlesVisible = visible;
2928
3054
  this.reposition();
2929
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
+ }
2930
3104
  // ── legend move/merge (menu + drag) ─────────────────────────────────────
2931
3105
  /** Open the "Move to" menu for a row, anchored under its move button. */
2932
3106
  openMoveMenu(id, anchor) {
@@ -2978,6 +3152,48 @@ var InputsUI = class {
2978
3152
  this.moveMenu?.remove();
2979
3153
  this.moveMenu = null;
2980
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
+ }
2981
3197
  /** Move a legend row to another pane (indicator merged/moved), tidying an emptied container. */
2982
3198
  setPane(id, paneId) {
2983
3199
  const row = this.rows.get(id);
@@ -2999,7 +3215,8 @@ var InputsUI = class {
2999
3215
  if (!lg) {
3000
3216
  lg = document.createElement("div");
3001
3217
  lg.dataset.velaPane = paneId;
3002
- 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);
3003
3220
  this.positionLegend(lg, paneId);
3004
3221
  this.container.appendChild(lg);
3005
3222
  this.legends.set(paneId, lg);
@@ -3035,15 +3252,16 @@ var InputsUI = class {
3035
3252
  */
3036
3253
  syncFoldToggle() {
3037
3254
  const count = this.rows.size;
3038
- if (count < 2) {
3255
+ const minRows = this.overviewAction !== null || this.mobileLayout ? 1 : 2;
3256
+ if (count < minRows) {
3039
3257
  const wasFolded = this.legendFolded;
3040
- this.legendFolded = false;
3258
+ if (count > 0) this.legendFolded = false;
3041
3259
  if (this.foldToggle) {
3042
3260
  this.foldToggle.remove();
3043
3261
  this.foldToggle = null;
3044
3262
  this.disposeTips(this.rowTips, "fold");
3045
3263
  }
3046
- if (wasFolded) this.reposition();
3264
+ if (wasFolded && count > 0) this.reposition();
3047
3265
  return;
3048
3266
  }
3049
3267
  let btn = this.foldToggle;
@@ -3051,25 +3269,30 @@ var InputsUI = class {
3051
3269
  btn = document.createElement("button");
3052
3270
  btn.type = "button";
3053
3271
  btn.className = "vela-ind-fold";
3054
- 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");
3055
3273
  btn.addEventListener("click", () => {
3274
+ if (this.overviewAction !== null) {
3275
+ this.overviewAction();
3276
+ return;
3277
+ }
3056
3278
  this.legendFolded = !this.legendFolded;
3057
3279
  this.syncFoldToggle();
3058
3280
  });
3059
3281
  this.foldToggle = btn;
3060
3282
  }
3061
3283
  const folded = this.legendFolded;
3062
- btn.setAttribute("aria-label", folded ? "Show indicator legend" : "Hide indicator legend");
3063
- 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;`;
3064
3287
  btn.replaceChildren();
3065
3288
  const icon2 = document.createElement("span");
3066
- icon2.style.cssText = "display:inline-flex;align-items:center;line-height:0;";
3067
- 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;
3068
3291
  btn.appendChild(icon2);
3069
3292
  if (folded) {
3070
3293
  const label = document.createElement("span");
3071
3294
  label.textContent = String(count);
3072
- 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};`;
3073
3296
  btn.appendChild(label);
3074
3297
  }
3075
3298
  this.legendFor("price").appendChild(btn);
@@ -3077,9 +3300,11 @@ var InputsUI = class {
3077
3300
  }
3078
3301
  /** Create or update an indicator's legend row (in the legend for its pane). */
3079
3302
  upsert(id, title, inputs, values, paneId = "price", opts = {}) {
3303
+ const settingsTitle = opts.settingsTitle ?? title;
3080
3304
  const existing = this.rows.get(id);
3081
3305
  if (existing) {
3082
3306
  existing.title = title;
3307
+ existing.settingsTitle = settingsTitle;
3083
3308
  existing.inputs = inputs;
3084
3309
  existing.values = { ...values };
3085
3310
  existing.titleEl.textContent = title;
@@ -3092,8 +3317,7 @@ var InputsUI = class {
3092
3317
  }
3093
3318
  ensureDialogStyles();
3094
3319
  const el = document.createElement("div");
3095
- 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;`;
3096
- 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;`;
3097
3321
  el.addEventListener("mouseleave", () => {
3098
3322
  if (this.selectedId !== id) this.setRowHighlighted(id, false);
3099
3323
  });
@@ -3113,11 +3337,16 @@ var InputsUI = class {
3113
3337
  e.preventDefault();
3114
3338
  this.onRemove?.(id);
3115
3339
  });
3340
+ el.addEventListener("contextmenu", (e) => {
3341
+ e.preventDefault();
3342
+ e.stopPropagation();
3343
+ this.openRowMenu(id, e.clientX, e.clientY);
3344
+ });
3116
3345
  const statusEl = document.createElement("span");
3117
3346
  statusEl.style.cssText = "display:none;box-sizing:border-box;flex:none;";
3118
- el.appendChild(statusEl);
3119
3347
  const titleWrap = document.createElement("span");
3120
3348
  titleWrap.style.cssText = "white-space:nowrap;";
3349
+ titleWrap.addEventListener("mouseenter", () => this.setRowHighlighted(id, true));
3121
3350
  const titleEl = document.createElement("span");
3122
3351
  titleEl.textContent = title;
3123
3352
  titleEl.style.cssText = "font-weight:600;";
@@ -3129,6 +3358,11 @@ var InputsUI = class {
3129
3358
  titleWrap.appendChild(beta);
3130
3359
  }
3131
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;`;
3132
3366
  let eyeEl = null;
3133
3367
  if (this.onToggleVisible) {
3134
3368
  const eye = document.createElement("button");
@@ -3137,16 +3371,14 @@ var InputsUI = class {
3137
3371
  this.tip(this.rowTips, id, eye, () => this.rows.get(id)?.hidden ? "Show" : "Hide");
3138
3372
  eye.innerHTML = EYE_SVG;
3139
3373
  eye.className = "vela-ind-ctl";
3140
- 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;
3141
3375
  eye.addEventListener("click", () => {
3142
3376
  const row = this.rows.get(id);
3143
3377
  this.onToggleVisible?.(id, Boolean(row?.hidden));
3144
3378
  });
3145
- el.appendChild(eye);
3379
+ controlsEl.appendChild(eye);
3146
3380
  eyeEl = eye;
3147
3381
  }
3148
- const controlsEl = document.createElement("span");
3149
- controlsEl.style.cssText = "display:none;align-items:center;gap:6px;";
3150
3382
  if (inputs.length > 0) {
3151
3383
  const gear = document.createElement("button");
3152
3384
  gear.type = "button";
@@ -3154,7 +3386,7 @@ var InputsUI = class {
3154
3386
  this.tip(this.rowTips, id, gear, "Settings");
3155
3387
  gear.innerHTML = GEAR_SVG;
3156
3388
  gear.className = "vela-ind-ctl";
3157
- 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;
3158
3390
  gear.addEventListener("click", () => this.openDialog(id));
3159
3391
  controlsEl.appendChild(gear);
3160
3392
  }
@@ -3165,7 +3397,7 @@ var InputsUI = class {
3165
3397
  this.tip(this.rowTips, id, mv, "Move to pane");
3166
3398
  mv.innerHTML = iconAt("move", LEGEND_ICON_PX);
3167
3399
  mv.className = "vela-ind-ctl";
3168
- 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;
3169
3401
  mv.addEventListener("click", (e) => {
3170
3402
  e.stopPropagation();
3171
3403
  this.openMoveMenu(id, mv);
@@ -3182,12 +3414,13 @@ var InputsUI = class {
3182
3414
  this.tip(this.rowTips, id, close, "Remove indicator");
3183
3415
  close.innerHTML = CLOSE_SVG;
3184
3416
  close.className = "vela-ind-close";
3185
- 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;
3186
3418
  close.addEventListener("click", () => this.onRemove?.(id));
3187
3419
  controlsEl.appendChild(close);
3188
3420
  el.appendChild(controlsEl);
3421
+ el.appendChild(statusEl);
3189
3422
  this.attach(this.legendFor(paneId), el, !!opts.native);
3190
- 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 });
3191
3424
  this.syncFoldToggle();
3192
3425
  }
3193
3426
  /** Place a row in its pane's legend — native rows PREPEND (pinned to the top), Pine rows append. */
@@ -3201,24 +3434,31 @@ var InputsUI = class {
3201
3434
  if (row) row.values = { ...row.values, ...values };
3202
3435
  }
3203
3436
  /**
3204
- * Reflect an indicator's live status in its legend row: `'loading'` shows a spinner (a fetch is
3205
- * 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.
3206
3440
  */
3207
3441
  setStatus(id, status) {
3208
3442
  const row = this.rows.get(id);
3209
3443
  if (!row) return;
3210
3444
  const el = row.statusEl;
3445
+ el.replaceChildren();
3211
3446
  if (status === "idle") {
3212
- el.style.display = "none";
3213
- el.style.animation = "none";
3447
+ el.style.cssText = "display:none;box-sizing:border-box;flex:none;";
3214
3448
  return;
3215
3449
  }
3216
- this.ensureStatusKeyframes();
3217
3450
  if (status === "loading") {
3218
- 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;`;
3219
- } else {
3220
- 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;
3221
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;`;
3222
3462
  }
3223
3463
  /** Inject the status keyframes once (idempotent). */
3224
3464
  ensureStatusKeyframes() {
@@ -3237,8 +3477,8 @@ var InputsUI = class {
3237
3477
  if (row.eyeEl) {
3238
3478
  row.eyeEl.innerHTML = visible ? EYE_SVG : EYE_OFF_SVG;
3239
3479
  row.eyeEl.setAttribute("aria-label", visible ? "Hide" : "Show");
3240
- row.eyeEl.style.display = !visible || row.controlsEl.style.display !== "none" ? "inline-flex" : "none";
3241
3480
  }
3481
+ this.syncRowActions(row);
3242
3482
  }
3243
3483
  /**
3244
3484
  * Select a legend row (outlining it) and clear any previous selection, so at most one
@@ -3251,17 +3491,36 @@ var InputsUI = class {
3251
3491
  this.setRowHighlighted(id, true);
3252
3492
  }
3253
3493
  /**
3254
- * Show or hide a row's outline and eye/gear/✕ controls together — driven by both hover and
3255
- * selection, so an idle (unhovered, unselected) row shows just its title. Exception: a hidden
3256
- * 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.
3257
3498
  */
3258
3499
  setRowHighlighted(id, highlighted) {
3259
3500
  const row = this.rows.get(id);
3260
3501
  if (!row) return;
3502
+ row.highlighted = highlighted;
3503
+ this.renderRowValues(row);
3261
3504
  row.el.style.boxShadow = highlighted ? `inset 0 0 0 1px ${this.neutralBorder()}` : "none";
3262
3505
  row.el.style.background = highlighted ? this.theme.background : this.idleRowFill();
3263
- row.controlsEl.style.display = highlighted ? "inline-flex" : "none";
3264
- 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
+ }
3265
3524
  }
3266
3525
  /** Drop the current selection outline, if any. */
3267
3526
  clearSelection() {
@@ -3289,6 +3548,7 @@ var InputsUI = class {
3289
3548
  destroy() {
3290
3549
  this.closeDialog();
3291
3550
  this.closeMoveMenu();
3551
+ this.closeRowMenu();
3292
3552
  for (const id of [...this.rowTips.keys()]) this.disposeTips(this.rowTips, id);
3293
3553
  for (const id of [...this.extrasTips.keys()]) this.disposeTips(this.extrasTips, id);
3294
3554
  for (const lg of this.legends.values()) lg.remove();
@@ -3344,6 +3604,7 @@ var InputsUI = class {
3344
3604
  const card = document.createElement("div");
3345
3605
  card.className = "vela-ind-dialog";
3346
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;";
3347
3608
  applyChromeTokens(card, t);
3348
3609
  card.addEventListener("keydown", (e) => {
3349
3610
  if (e.key !== "Escape") e.stopPropagation();
@@ -3351,7 +3612,7 @@ var InputsUI = class {
3351
3612
  const header = document.createElement("div");
3352
3613
  header.style.cssText = "display:flex;justify-content:space-between;align-items:flex-start;gap:12px;padding:16px 20px 12px;flex:0 0 auto;";
3353
3614
  const hTitle = document.createElement("span");
3354
- hTitle.textContent = row.title;
3615
+ hTitle.textContent = row.settingsTitle;
3355
3616
  hTitle.style.cssText = "font-weight:600;font-size:16px;line-height:1.3;";
3356
3617
  const closeBtn = document.createElement("button");
3357
3618
  closeBtn.type = "button";
@@ -3363,7 +3624,7 @@ var InputsUI = class {
3363
3624
  closeBtn.addEventListener("click", () => this.closeDialog());
3364
3625
  header.append(hTitle, closeBtn);
3365
3626
  card.appendChild(header);
3366
- makeDialogDraggable(card, header, { closeSelector: "button" });
3627
+ if (!this.mobileLayout) makeDialogDraggable(card, header, { closeSelector: "button" });
3367
3628
  const tabs = document.createElement("div");
3368
3629
  tabs.style.cssText = `display:flex;gap:16px;padding:0 20px;border-bottom:1px solid ${border};flex:0 0 auto;`;
3369
3630
  const tab = document.createElement("div");
@@ -3383,7 +3644,7 @@ var InputsUI = class {
3383
3644
  section.appendChild(gh);
3384
3645
  }
3385
3646
  const grid = document.createElement("div");
3386
- 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;";
3387
3648
  for (const inputRow of group.rows) this.buildInputRow(grid, inputRow, row);
3388
3649
  section.appendChild(grid);
3389
3650
  body.appendChild(section);
@@ -3832,10 +4093,14 @@ function groupInputs(inputs) {
3832
4093
  var CHECK_SVG = iconAt("check", 12);
3833
4094
  var SELECT_CHEVRON_SVG = iconAt("chevron-down", 16);
3834
4095
  var DIALOG_STYLE_ID = "vela-ind-dialog-styles";
4096
+ var DIALOG_STYLE_REV = "10";
3835
4097
  function ensureDialogStyles() {
3836
- if (typeof document === "undefined" || document.getElementById(DIALOG_STYLE_ID)) return;
3837
- 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");
3838
4102
  s.id = DIALOG_STYLE_ID;
4103
+ s.dataset.rev = DIALOG_STYLE_REV;
3839
4104
  s.textContent = `
3840
4105
  .vela-ind-dialog input[type=color]{-webkit-appearance:none;appearance:none;}
3841
4106
  .vela-ind-dialog input[type=color]::-webkit-color-swatch-wrapper{padding:0;}
@@ -3849,19 +4114,19 @@ function ensureDialogStyles() {
3849
4114
  .vela-ind-dialog ::-webkit-scrollbar-track{background:transparent;}
3850
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;}
3851
4116
  .vela-ind-hint:hover{background:var(--vela-active);color:var(--vela-fg-bright);}
3852
- .vela-ind-ctl{color:var(--vela-fg-muted);transition:color var(--vela-dur-fast) ease;}
3853
- .vela-ind-ctl:hover{color:var(--vela-fg-bright);}
3854
- .vela-ind-close{transition:color var(--vela-dur-fast) ease;}
3855
- .vela-ind-close:hover{color:var(--vela-danger) !important;}
3856
- .vela-ind-fold{color:var(--vela-fg-muted);transition:color var(--vela-dur-fast) ease,border-color var(--vela-dur-fast) ease;}
3857
- .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;}
3858
4123
  .vela-ind-menuitem{background:transparent;transition:background var(--vela-dur-fast) ease;}
3859
4124
  .vela-ind-menuitem:hover{background:var(--vela-hover-strong);}
3860
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;}
3861
4126
  .vela-ind-btn:hover{background:var(--vela-hover);color:var(--vela-fg-bright);}
3862
4127
  .vela-ind-btn-primary{padding:7px 16px;border-color:var(--vela-selected-bg);background:var(--vela-selected-bg);color:var(--vela-selected-fg);}
3863
4128
  .vela-ind-btn-primary:hover{background:var(--vela-selected-bg);color:var(--vela-selected-fg);opacity:0.85;}`;
3864
- document.head.appendChild(s);
4129
+ if (!existing) document.head.appendChild(s);
3865
4130
  }
3866
4131
  var TIMEFRAME_OPTIONS = [
3867
4132
  { value: "", label: "Chart" },
@@ -4470,6 +4735,44 @@ function basePaintingOf(style) {
4470
4735
  for (const t of chartTypes()) if (t.id === style) return t.basePainting ?? "candles";
4471
4736
  return "candles";
4472
4737
  }
4738
+ function hasOwnCandlePaint(style) {
4739
+ if (BUILTIN_PRICE_STYLES.includes(style)) return false;
4740
+ for (const t of chartTypes()) if (t.id === style) return (t.basePainting ?? "candles") === "candles";
4741
+ return false;
4742
+ }
4743
+ function candleOverrideFor(style, bags) {
4744
+ if (!hasOwnCandlePaint(style)) return null;
4745
+ const bag = bags[style] ?? {};
4746
+ const color = (v) => isColor(v) ? v : null;
4747
+ const bool = (v) => isBool(v) ? v : null;
4748
+ return {
4749
+ upColor: color(bag.candleUpColor),
4750
+ downColor: color(bag.candleDownColor),
4751
+ bodyVisible: bool(bag.candleBodyVisible),
4752
+ borderVisible: bool(bag.candleBorderVisible),
4753
+ borderUpColor: color(bag.candleBorderUpColor),
4754
+ borderDownColor: color(bag.candleBorderDownColor),
4755
+ wickVisible: bool(bag.candleWickVisible),
4756
+ wickUpColor: color(bag.candleWickUpColor),
4757
+ wickDownColor: color(bag.candleWickDownColor)
4758
+ };
4759
+ }
4760
+ function effectiveCandlePaint(base, override, themeUp, themeDown) {
4761
+ if (!override) return { up: themeUp, down: themeDown, candle: base };
4762
+ return {
4763
+ up: override.upColor ?? themeUp,
4764
+ down: override.downColor ?? themeDown,
4765
+ candle: {
4766
+ bodyVisible: override.bodyVisible ?? base.bodyVisible,
4767
+ borderVisible: override.borderVisible ?? base.borderVisible,
4768
+ borderUpColor: override.borderUpColor ?? base.borderUpColor,
4769
+ borderDownColor: override.borderDownColor ?? base.borderDownColor,
4770
+ wickVisible: override.wickVisible ?? base.wickVisible,
4771
+ wickUpColor: override.wickUpColor ?? base.wickUpColor,
4772
+ wickDownColor: override.wickDownColor ?? base.wickDownColor
4773
+ }
4774
+ };
4775
+ }
4473
4776
  function priceStyleIds() {
4474
4777
  const out = [...BUILTIN_PRICE_STYLES];
4475
4778
  for (const t of chartTypes()) if (!out.includes(t.id)) out.push(t.id);
@@ -4508,6 +4811,33 @@ function clampLevel(v) {
4508
4811
  function clampSpacing(v) {
4509
4812
  return v < 0.1 ? 0.1 : v > 10 ? 10 : v;
4510
4813
  }
4814
+ function factoryResetConfig(factory) {
4815
+ const bag = {};
4816
+ for (const t of chartTypes()) {
4817
+ const section = t.settings;
4818
+ if (!section) continue;
4819
+ const defaults = {};
4820
+ const addRows = (rows) => {
4821
+ for (const r of rows ?? []) {
4822
+ for (const k of settingsRowValueKeys(r)) defaults[k.key] = k.defval;
4823
+ }
4824
+ };
4825
+ for (const inst of section.instances ?? []) {
4826
+ if (inst.enableKey) defaults[inst.enableKey] = false;
4827
+ addRows(inst.rows);
4828
+ }
4829
+ for (const sub of section.subsections ?? []) {
4830
+ if (sub.enableKey) defaults[sub.enableKey] = false;
4831
+ addRows(sub.rows);
4832
+ }
4833
+ if (!section.instances) addRows(section.rows);
4834
+ bag[t.id] = defaults;
4835
+ }
4836
+ for (const [typeId, vals] of Object.entries(factory.chartTypes)) {
4837
+ bag[typeId] = { ...bag[typeId] ?? {}, ...vals };
4838
+ }
4839
+ return { ...factory, chartTypes: bag };
4840
+ }
4511
4841
  function mergeConfig(base, patch) {
4512
4842
  const p = asObject(patch);
4513
4843
  const ctPatch = asObject(p.chartTypes);
@@ -4822,6 +5152,9 @@ var SceneGraph = class {
4822
5152
  this.priceStyle = "candles";
4823
5153
  /** Price-series base painting for the ACTIVE style (see ChartTypeDefinition.basePainting). */
4824
5154
  this.basePainting = "candles";
5155
+ /** The ACTIVE style's own candle cosmetics (`chartTypes.<id>.candle*`) when it is a
5156
+ * candle-based plugin type; null ⇒ paint with the shared `style.candle` block. */
5157
+ this.candleOverride = null;
4825
5158
  /** Explicit baseline reference price for `priceStyle:'baseline'`; when null the
4826
5159
  * baseline follows `style.baseline.baselineLevel` as a percent of the visible pane
4827
5160
  * range (resolved per frame via `baselinePriceFor`). */
@@ -6051,14 +6384,15 @@ var WebGL2Backend = class {
6051
6384
  emitCandles(b, scene, bars, i0, i1, coords, pane, theme, barColors) {
6052
6385
  const spacing = coords.bodySpacing();
6053
6386
  const tier = candleTier(spacing);
6387
+ const paint = effectiveCandlePaint(scene.style.candle, scene.candleOverride, theme.upColor, theme.downColor);
6054
6388
  if (tier === "aggregate") {
6055
- this.emitCandlesAggregated(b, bars, i0, i1, coords, pane, theme.upColor, theme.downColor, barColors);
6389
+ this.emitCandlesAggregated(b, bars, i0, i1, coords, pane, paint.up, paint.down, barColors);
6056
6390
  return;
6057
6391
  }
6058
6392
  const drawBody = tier === "full";
6059
- const up = theme.upColor;
6060
- const down = theme.downColor;
6061
- const cs = scene.style.candle;
6393
+ const up = paint.up;
6394
+ const down = paint.down;
6395
+ const cs = paint.candle;
6062
6396
  const fading = this.candleStructureAlpha > this.candleBodyAlpha + 1e-3;
6063
6397
  for (let i = i0; i <= i1; i += 1) {
6064
6398
  const bar = bars[i];
@@ -6629,6 +6963,11 @@ function easeToward(current, target, dtMs, tauMs) {
6629
6963
  var FLING_MIN_SPEED = 0.04;
6630
6964
  var FLING_STALE_MS = 60;
6631
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;
6632
6971
  var TIME_SCALE_K = 4e-3;
6633
6972
  var WHEEL_ZOOM_K = 25e-4;
6634
6973
  function wheelZoomAnchor(coords, cursorX, rightEdge) {
@@ -6649,8 +6988,13 @@ function wheelPanDelta(deltaX, deltaY, shift) {
6649
6988
  function effectiveSnapMode(momentaryOverride, sticky) {
6650
6989
  return momentaryOverride ? "strong" : sticky;
6651
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
+ }
6652
6997
  var InputController = class {
6653
- // the last middle press deleted a drawing (suppress autoscroll/paste)
6654
6998
  constructor(deps) {
6655
6999
  this.deps = deps;
6656
7000
  /** When true (the default), wheel-zoom pins the right edge instead of the bar
@@ -6678,9 +7022,40 @@ var InputController = class {
6678
7022
  this.vx = 0;
6679
7023
  // smoothed pointer velocity, px/ms
6680
7024
  this.middleDeleted = false;
7025
+ // the last middle press deleted a drawing (suppress autoscroll/paste)
7026
+ // Last cursor position over the element (NaN once it leaves) — lets a modifier
7027
+ // press/release re-shape the drawings preview with the pointer stationary.
7028
+ this.cursorX = NaN;
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
+ };
6681
7047
  this.onMiddleGuard = (e) => {
6682
7048
  if (e.button === 1 && this.middleDeleted) e.preventDefault();
6683
7049
  };
7050
+ /** A modifier press/release with the pointer stationary: re-issue the last cursor
7051
+ * position so the placing ghost / drag preview reflects the new state immediately
7052
+ * (Shift's 45° angle lock, Ctrl/Cmd's momentary strong magnet). */
7053
+ this.onModifier = (e) => {
7054
+ if (e.repeat || e.key !== "Shift" && e.key !== "Control" && e.key !== "Meta") return;
7055
+ if (Number.isNaN(this.cursorX)) return;
7056
+ if (this.dragging && this.region !== "drawing") return;
7057
+ this.deps.drawingsPointerMove?.(this.cursorX, this.cursorY, this.snapMode(e), e.shiftKey);
7058
+ };
6684
7059
  this.onDown = (e) => {
6685
7060
  if (e.button === 1) {
6686
7061
  const { x: x2, y: y2 } = this.local(e);
@@ -6690,6 +7065,15 @@ var InputController = class {
6690
7065
  }
6691
7066
  if (e.button !== 0) return;
6692
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
+ }
6693
7077
  this.deps.drawingsClearTransient?.();
6694
7078
  this.dragging = true;
6695
7079
  this.moved = false;
@@ -6715,6 +7099,9 @@ var InputController = class {
6715
7099
  if (this.region === "price") this.deps.beginPriceScale(x, y);
6716
7100
  else if (this.region === "separator") this.deps.beginPaneResize(y);
6717
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
+ }
6718
7105
  this.lastX = x;
6719
7106
  this.lastT = e.timeStamp;
6720
7107
  this.vx = 0;
@@ -6722,28 +7109,44 @@ var InputController = class {
6722
7109
  };
6723
7110
  this.onMove = (e) => {
6724
7111
  const { x, y } = this.local(e);
7112
+ this.cursorX = x;
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;
6725
7128
  if (this.dragging) {
6726
7129
  if (this.region === "price") {
6727
- if (Math.abs(y - this.startY) > DRAG_SLOP) this.moved = true;
7130
+ if (Math.abs(y - this.startY) > slop) this.moved = true;
6728
7131
  this.deps.priceScaleBy(y - this.startY);
6729
7132
  } else if (this.region === "separator") {
6730
- if (Math.abs(y - this.startY) > DRAG_SLOP) this.moved = true;
7133
+ if (Math.abs(y - this.startY) > slop) this.moved = true;
6731
7134
  this.deps.paneResizeBy(y - this.startY);
6732
7135
  } else if (this.region === "time") {
6733
- if (Math.abs(x - this.startX) > DRAG_SLOP) this.moved = true;
7136
+ if (Math.abs(x - this.startX) > slop) this.moved = true;
6734
7137
  const barSpacing = clampBarSpacing(this.startBarSpacing * Math.exp((this.startX - x) * TIME_SCALE_K));
6735
7138
  this.deps.apply({ barSpacing, rightOffset: this.startRightOffset });
6736
7139
  } else if (this.region === "drawing") {
6737
- if (Math.abs(x - this.startX) > DRAG_SLOP || Math.abs(y - this.startY) > DRAG_SLOP) this.moved = true;
6738
- this.deps.drawingsPointerMove?.(x, y, this.snapMode(e));
7140
+ if (Math.abs(x - this.startX) > slop || Math.abs(y - this.startY) > slop) this.moved = true;
7141
+ this.deps.drawingsPointerMove?.(x, y, this.snapMode(e), e.shiftKey);
6739
7142
  } else {
6740
7143
  const coords = this.deps.getCoords();
6741
7144
  const vp = coords.getViewport();
6742
7145
  const dx = x - this.startX;
6743
- if (Math.abs(dx) > DRAG_SLOP) this.moved = true;
7146
+ if (Math.abs(dx) > slop) this.moved = true;
6744
7147
  this.deps.apply({ barSpacing: vp.barSpacing, rightOffset: this.startRightOffset - dx / coords.pxPerBar() });
6745
7148
  if (this.verticalPan) {
6746
- if (Math.abs(y - this.startY) > DRAG_SLOP) this.moved = true;
7149
+ if (Math.abs(y - this.startY) > slop) this.moved = true;
6747
7150
  this.deps.pricePanBy(y - this.startY);
6748
7151
  }
6749
7152
  const dt = e.timeStamp - this.lastT;
@@ -6758,16 +7161,46 @@ var InputController = class {
6758
7161
  const drawCursor = this.deps.drawingsCursor?.(x, y);
6759
7162
  const r = this.regionAt(x, y);
6760
7163
  this.el.style.cursor = drawCursor ?? (r === "price" ? "ns-resize" : r === "time" ? "ew-resize" : r === "separator" ? "row-resize" : "");
6761
- this.deps.drawingsPointerMove?.(x, y, this.snapMode(e));
7164
+ this.deps.drawingsPointerMove?.(x, y, this.snapMode(e), e.shiftKey);
6762
7165
  }
6763
- this.deps.onPointerMove(x, y);
7166
+ if (e.pointerType !== "touch") this.deps.onPointerMove(x, y);
6764
7167
  };
6765
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);
6766
7201
  if (this.dragging && this.region === "drawing") {
6767
- const { x, y } = this.local(e);
6768
7202
  this.deps.drawingsPointerUp?.(x, y);
6769
- } else if (this.dragging && !this.moved && this.region === "data") {
6770
- const { x, y } = this.local(e);
7203
+ } else if (tapRelease && this.region === "data") {
6771
7204
  this.deps.onClick(x, y);
6772
7205
  } else if (this.dragging && this.region === "data") {
6773
7206
  const stale = e.timeStamp - this.lastT > FLING_STALE_MS;
@@ -6776,23 +7209,29 @@ var InputController = class {
6776
7209
  this.deps.fling(-this.vx / pitch);
6777
7210
  }
6778
7211
  }
6779
- this.dragging = false;
6780
- try {
6781
- this.el?.releasePointerCapture(e.pointerId);
6782
- } catch {
6783
- }
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);
6784
7226
  };
6785
7227
  this.onLeave = () => {
7228
+ this.cursorX = NaN;
7229
+ this.cursorY = NaN;
6786
7230
  this.deps.onPointerMove(null, null);
6787
7231
  };
6788
7232
  this.onDblClick = (e) => {
6789
7233
  const { x, y } = this.local(e);
6790
- if (this.deps.drawingsDblClick?.(x, y)) return;
6791
- const region = this.regionAt(x, y);
6792
- if (region === "price") this.deps.resetPriceScale(x, y);
6793
- else if (region === "separator") this.deps.resetPaneSize(y);
6794
- else if (region === "time") this.deps.resetView();
6795
- else this.deps.dataDblClick(x, y);
7234
+ this.doubleActivate(x, y);
6796
7235
  };
6797
7236
  this.onWheel = (e) => {
6798
7237
  e.preventDefault();
@@ -6813,26 +7252,44 @@ var InputController = class {
6813
7252
  }
6814
7253
  attach(el) {
6815
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
+ }
6816
7261
  el.addEventListener("pointerdown", this.onDown);
6817
7262
  el.addEventListener("pointermove", this.onMove);
6818
7263
  el.addEventListener("pointerup", this.onUp);
6819
7264
  el.addEventListener("pointerleave", this.onLeave);
7265
+ el.addEventListener("pointercancel", this.onCancel);
6820
7266
  el.addEventListener("dblclick", this.onDblClick);
6821
7267
  el.addEventListener("wheel", this.onWheel, { passive: false });
6822
7268
  el.addEventListener("mousedown", this.onMiddleGuard);
6823
7269
  el.addEventListener("auxclick", this.onMiddleGuard);
7270
+ const win = el.ownerDocument?.defaultView;
7271
+ win?.addEventListener("keydown", this.onModifier);
7272
+ win?.addEventListener("keyup", this.onModifier);
7273
+ win?.addEventListener("contextmenu", this.onTouchContextMenu, true);
6824
7274
  }
6825
7275
  detach() {
6826
7276
  const el = this.el;
6827
7277
  if (!el) return;
7278
+ const win = el.ownerDocument?.defaultView;
7279
+ win?.removeEventListener("keydown", this.onModifier);
7280
+ win?.removeEventListener("keyup", this.onModifier);
7281
+ win?.removeEventListener("contextmenu", this.onTouchContextMenu, true);
6828
7282
  el.removeEventListener("pointerdown", this.onDown);
6829
7283
  el.removeEventListener("pointermove", this.onMove);
6830
7284
  el.removeEventListener("pointerup", this.onUp);
6831
7285
  el.removeEventListener("pointerleave", this.onLeave);
7286
+ el.removeEventListener("pointercancel", this.onCancel);
6832
7287
  el.removeEventListener("dblclick", this.onDblClick);
6833
7288
  el.removeEventListener("wheel", this.onWheel);
6834
7289
  el.removeEventListener("mousedown", this.onMiddleGuard);
6835
7290
  el.removeEventListener("auxclick", this.onMiddleGuard);
7291
+ this.cancelLongPress();
7292
+ this.touches.clear();
6836
7293
  this.el = null;
6837
7294
  }
6838
7295
  local(e) {
@@ -6856,6 +7313,59 @@ var InputController = class {
6856
7313
  snapMode(e) {
6857
7314
  return effectiveSnapMode(e.ctrlKey || e.metaKey, this.deps.drawingsSnapMode?.() ?? "off");
6858
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
+ }
6859
7369
  /** Capture defensively: a synthetic/already-released pointer can't be captured, and
6860
7370
  * the move/up pair still routes through the element listeners without it. */
6861
7371
  capture(pointerId) {
@@ -6864,6 +7374,35 @@ var InputController = class {
6864
7374
  } catch {
6865
7375
  }
6866
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
+ }
6867
7406
  };
6868
7407
 
6869
7408
  // src/renderers/native/core/KeyboardController.ts
@@ -7242,12 +7781,13 @@ var Canvas2dBackend = class {
7242
7781
  drawCandles(ctx, scene, bars, i0, i1, coords, pane, theme, barColors) {
7243
7782
  const spacing = coords.bodySpacing();
7244
7783
  const tier = candleTier(spacing);
7784
+ const paint = effectiveCandlePaint(scene.style.candle, scene.candleOverride, theme.upColor, theme.downColor);
7245
7785
  if (tier === "aggregate") {
7246
- this.drawCandlesAggregated(ctx, bars, i0, i1, coords, pane, theme.upColor, theme.downColor, barColors);
7786
+ this.drawCandlesAggregated(ctx, bars, i0, i1, coords, pane, paint.up, paint.down, barColors);
7247
7787
  return;
7248
7788
  }
7249
7789
  const drawBody = tier === "full";
7250
- const cs = scene.style.candle;
7790
+ const cs = paint.candle;
7251
7791
  const fading = this.candleStructureAlpha > this.candleBodyAlpha + 1e-3;
7252
7792
  for (let i = i0; i <= i1; i += 1) {
7253
7793
  const b = bars[i];
@@ -7255,7 +7795,7 @@ var Canvas2dBackend = class {
7255
7795
  const g = candleGeometry(coords.logicalToX(i), spacing, coords.dpr, this.candleBodyScale);
7256
7796
  const x = g.center;
7257
7797
  const up = b.close >= b.open;
7258
- const dir = up ? theme.upColor : theme.downColor;
7798
+ const dir = up ? paint.up : paint.down;
7259
7799
  const bc = barColors.get(b.time);
7260
7800
  const color = bc ?? dir;
7261
7801
  let top = 0;
@@ -8366,7 +8906,7 @@ var ChromeRenderer = class {
8366
8906
  constructor() {
8367
8907
  this.canvas = null;
8368
8908
  this.ctx = null;
8369
- // The color for axis tick labels — the stable chrome surface text, set each frame in render().
8909
+ // The color for axis tick labels — the host-passed surface text, set each frame in render().
8370
8910
  this.axisTextColor = DARK_THEME.textColor;
8371
8911
  // Shared Pine-drawing renderer (line/box/label/polyline/linefill); widthCache persists.
8372
8912
  this.drawScene = new DrawingSceneRenderer({ timeToLogical: () => 0, barAt: () => null, theme: {} });
@@ -8398,8 +8938,8 @@ var ChromeRenderer = class {
8398
8938
  return dr;
8399
8939
  }
8400
8940
  /** Clear the chrome canvas and draw drawings + axes + current-price line.
8401
- * `surface` (background + text) paints the axis-scale gutters so they keep the app
8402
- * chrome color and stay readable, rather than tracking the user-editable plot cosmetics.
8941
+ * `surface` (background + text) paints the axis-scale gutters the host passes the live
8942
+ * chart background so the scales read as part of the plot, with contrast-corrected text.
8403
8943
  * Falls back to the theme's own colors when no surface is supplied. */
8404
8944
  render(scene, coords, theme, surface) {
8405
8945
  const ctx = this.ctx;
@@ -8676,11 +9216,19 @@ var ChromeRenderer = class {
8676
9216
  const y = dataH + (fullH - dataH) / 2;
8677
9217
  const tr = coords.visibleTimeRange();
8678
9218
  const offset = tzOffsetMs((tr.from + tr.to) / 2, scene.timezone);
8679
- for (const tick of timeTicks(tr.from, tr.to, 8, offset)) {
8680
- const x = coords.timeToX(tick.time);
8681
- if (x < 20 || x > dataW - 20) continue;
8682
- ctx.fillText(tick.label, x, y);
8683
- }
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);
8684
9232
  ctx.textAlign = "start";
8685
9233
  }
8686
9234
  };
@@ -8782,10 +9330,10 @@ var CrosshairRenderer = class {
8782
9330
  }
8783
9331
  /** The synced ghost: a dimmed vertical line at the bar the renderer resolved as
8784
9332
  * CONTAINING the foreign time (+ horizontal line when a comparable price came
8785
- * along), with that bar's time chip in this chart's own timezone. The snap
8786
- * happened upstream (`externalCrossPx`, floor-to-containing-bar) this method
8787
- * only draws. No price chip the ghost answers "when", the local crosshair
8788
- * 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. */
8789
9337
  drawExternal(ctx, ext, scene, coords, theme) {
8790
9338
  const cs = scene.style.crosshair;
8791
9339
  const dataW = coords.width;
@@ -8809,7 +9357,20 @@ var CrosshairRenderer = class {
8809
9357
  ctx.setLineDash([]);
8810
9358
  ctx.lineWidth = 1;
8811
9359
  ctx.globalAlpha = 0.8;
8812
- 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);
8813
9374
  ctx.globalAlpha = 1;
8814
9375
  }
8815
9376
  /** A soft band + a brighter crisp center line over the hovered separator, so it reads as
@@ -9080,6 +9641,7 @@ function colorField(theme, getVal, onVal) {
9080
9641
  swatch.style.background = `linear-gradient(${v}, ${v}), ${CHECKER2}`;
9081
9642
  };
9082
9643
  paint();
9644
+ trigger.addEventListener("vela-sync", paint);
9083
9645
  trigger.addEventListener("click", (e) => {
9084
9646
  e.stopPropagation();
9085
9647
  if (openPopover && openPopover.trigger === trigger) {
@@ -9115,6 +9677,101 @@ function closeColorPopover() {
9115
9677
  closeOpen();
9116
9678
  }
9117
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
+
9118
9775
  // src/renderers/native/chrome/SettingsDialog.ts
9119
9776
  var BUILTIN_STYLE_LABELS = {
9120
9777
  candles: "Candles",
@@ -9166,6 +9823,61 @@ function ensureControlStyles() {
9166
9823
  .vela-sd-btn:hover{background:var(--vela-hover);border-color:var(--vela-border-strong);color:var(--vela-fg-bright);}
9167
9824
  .vela-sd-close{cursor:pointer;display:inline-flex;align-items:center;justify-content:center;background:transparent;border:none;color:var(--vela-fg-muted);line-height:0;width:30px;height:30px;border-radius:var(--vela-radius-sm);transition:background var(--vela-dur-fast) ease,color var(--vela-dur-fast) ease;}
9168
9825
  .vela-sd-close:hover{background:var(--vela-hover);color:var(--vela-fg-bright);}
9826
+ /* Rows/blocks gated away by chart-type conditions, TOC filters, or the instance strip.
9827
+ !important: the pane grid rewrites inline display ('contents') AFTER the initial
9828
+ visibility pass, so a class must win. */
9829
+ .vela-sd-hide{display:none !important;}
9830
+ /* Indented rail sub-entry (a chart-type section's subsection tab). */
9831
+ .vela-sd-tab-sub{padding-left:28px;font-weight:600;font-size:12px;}
9832
+ /* Instance strip: a tab per present instance (label, \u2715 on the active removable one)
9833
+ and a dashed + that turns on the next absent instance. The rule under it separates
9834
+ the strip from the instance's TOC + rows area. */
9835
+ .vela-sd-itabs{display:flex;align-items:center;gap:6px;flex-wrap:wrap;margin:14px 0 0;padding-bottom:12px;border-bottom:1px solid var(--vela-border);}
9836
+ .vela-sd-itab{display:inline-flex;align-items:center;gap:7px;height:30px;padding:0 11px;background:transparent;border:1px solid var(--vela-border);border-radius:var(--vela-radius-md);color:var(--vela-fg-muted);font-family:inherit;font-size:var(--vela-font-size-md);font-weight:600;cursor:pointer;transition:background var(--vela-dur-fast) ease,color var(--vela-dur-fast) ease,border-color var(--vela-dur-fast) ease;}
9837
+ .vela-sd-itab:hover{background:var(--vela-hover);color:var(--vela-fg-bright);}
9838
+ .vela-sd-itab.on{background:var(--vela-active);color:var(--vela-fg-bright);border-color:var(--vela-border-strong);}
9839
+ .vela-sd-ix{display:inline-flex;align-items:center;justify-content:center;width:16px;height:16px;margin-right:-4px;border-radius:var(--vela-radius-sm);color:var(--vela-fg-muted);font-size:10px;line-height:1;}
9840
+ .vela-sd-ix:hover{background:var(--vela-hover);color:var(--vela-fg-bright);}
9841
+ .vela-sd-itab-add{border-style:dashed;min-width:30px;justify-content:center;padding:0;}
9842
+ /* Structured pane: group TOC column + rows column, TOP-ALIGNED (the shared padding-top
9843
+ lives on the wrap, never on one column). The TOC sticks while the pane scrolls; the
9844
+ vertical rule sits on the rows column so it spans the full content height. When every
9845
+ group gates out the TOC hides and .no-toc drops the rule with it. */
9846
+ .vela-sd-struct{display:flex;align-items:flex-start;padding-top:14px;}
9847
+ .vela-sd-toc{position:sticky;top:0;flex:0 0 auto;min-width:104px;display:flex;flex-direction:column;gap:2px;padding:2px 14px 0 0;}
9848
+ .vela-sd-struct>[data-sd-rows-host]{border-left:1px solid var(--vela-border);padding-left:18px;}
9849
+ .vela-sd-struct.no-toc>[data-sd-rows-host]{border-left:none;padding-left:0;}
9850
+ .vela-sd-toc-btn{text-align:left;padding:6px 10px;background:transparent;border:none;border-radius:var(--vela-radius-sm);color:var(--vela-fg-muted);font-family:inherit;font-size:12px;font-weight:600;cursor:pointer;transition:background var(--vela-dur-fast) ease,color var(--vela-dur-fast) ease;}
9851
+ .vela-sd-toc-btn:hover{background:var(--vela-hover);color:var(--vela-fg-bright);}
9852
+ .vela-sd-toc-btn.on{background:var(--vela-active);color:var(--vela-fg-bright);}
9853
+ /* Soft-disable: a subsection's enableKey is off \u2014 rows stay visible (browseable) but
9854
+ muted and non-interactive. Applied to each row's children so it survives display:contents;
9855
+ !important beats the inline opacity on labels. */
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;}
9169
9881
  `;
9170
9882
  document.head.appendChild(st);
9171
9883
  }
@@ -9186,8 +9898,14 @@ var SettingsDialog = class {
9186
9898
  this.themeControl = null;
9187
9899
  /** The built tabs, by title — how `showSection` reaches a pane while the dialog is open. */
9188
9900
  this.tabs = [];
9901
+ /** Active instance-strip tab per chart type — remembered across dialog rebuilds. */
9902
+ this.typeActiveInstance = /* @__PURE__ */ new Map();
9903
+ /** Active TOC group per structured pane (`<typeId>/<pane>` → group label). */
9904
+ this.typeActiveGroup = /* @__PURE__ */ new Map();
9189
9905
  /** The tab currently shown, so a theme change (which rebuilds) lands back on it. */
9190
9906
  this.activeSection = null;
9907
+ /** Mobile chrome: fullscreen card, burger-opened section sidebar, TOC as top tabs. */
9908
+ this.mobileLayout = false;
9191
9909
  if (getComputedStyle(container).position === "static") container.style.position = "relative";
9192
9910
  }
9193
9911
  /** Host-app sections (e.g. the widget's Status line tab) — re-shown on next open. */
@@ -9206,15 +9924,27 @@ var SettingsDialog = class {
9206
9924
  }
9207
9925
  setTheme(theme) {
9208
9926
  this.theme = theme;
9209
- if (this.root) {
9210
- const cfg = this.config;
9211
- const oc = this.onChange;
9212
- const oi = this.onImport;
9213
- const orst = this.onReset;
9214
- const section = this.activeSection;
9215
- this.close();
9216
- if (cfg && oc) this.open(cfg, oc, oi ?? void 0, orst ?? void 0, section ?? void 0);
9217
- }
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);
9218
9948
  }
9219
9949
  isOpen() {
9220
9950
  return this.root !== null;
@@ -9236,18 +9966,37 @@ var SettingsDialog = class {
9236
9966
  this.onImport = onImport ?? null;
9237
9967
  this.onReset = onReset ?? null;
9238
9968
  ensureControlStyles();
9969
+ const mobile = this.mobileLayout;
9239
9970
  const scrim = document.createElement("div");
9240
- 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
+ }
9241
9977
  scrim.addEventListener("mousedown", (e) => {
9242
9978
  if (e.target === scrim) this.close();
9243
9979
  });
9244
9980
  const dlg = document.createElement("div");
9245
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;";
9246
9983
  const header = document.createElement("div");
9247
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";
9248
9986
  const hTitle = document.createElement("span");
9249
9987
  hTitle.textContent = "Chart settings";
9250
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
+ }
9251
10000
  const closeBtn = document.createElement("button");
9252
10001
  closeBtn.type = "button";
9253
10002
  closeBtn.innerHTML = iconAt("close", 15);
@@ -9255,8 +10004,8 @@ var SettingsDialog = class {
9255
10004
  closeBtn.className = "vela-sd-close";
9256
10005
  closeBtn.addEventListener("click", () => this.close());
9257
10006
  header.append(hTitle, closeBtn);
9258
- header.style.cursor = "move";
9259
- {
10007
+ if (!mobile) {
10008
+ header.style.cursor = "move";
9260
10009
  let sx = 0, sy = 0, ox = 0, oy = 0, dragging = false;
9261
10010
  header.addEventListener("pointerdown", (e) => {
9262
10011
  if (e.target?.closest(".vela-sd-close")) return;
@@ -9347,6 +10096,29 @@ var SettingsDialog = class {
9347
10096
  baseline.append(this.numberRow("Width", config.baseline.width, 1, 10, 1, (v) => this.emit({ baseline: { width: v } })));
9348
10097
  groups.baseline = baseline;
9349
10098
  body.append(baseline);
10099
+ for (const def of chartTypes()) {
10100
+ if (!hasOwnCandlePaint(def.id)) continue;
10101
+ const bag = config.chartTypes[def.id] ?? {};
10102
+ const colorOf2 = (key, shared) => typeof bag[key] === "string" && bag[key] !== "" ? bag[key] : shared;
10103
+ const boolOf = (key, shared) => typeof bag[key] === "boolean" ? bag[key] : shared;
10104
+ const g = this.group();
10105
+ g.append(this.sectionTitle("Candles"));
10106
+ g.append(this.toggleRow("Body", boolOf("candleBodyVisible", config.candles.bodyVisible), (v) => this.emitType(def.id, "candleBodyVisible", v), [
10107
+ this.swatch(colorOf2("candleUpColor", config.candles.upColor), (v) => this.emitType(def.id, "candleUpColor", v)),
10108
+ this.swatch(colorOf2("candleDownColor", config.candles.downColor), (v) => this.emitType(def.id, "candleDownColor", v))
10109
+ ]));
10110
+ g.append(this.toggleRow("Borders", boolOf("candleBorderVisible", config.candles.borderVisible), (v) => this.emitType(def.id, "candleBorderVisible", v), [
10111
+ this.swatch(colorOf2("candleBorderUpColor", config.candles.borderUpColor), (v) => this.emitType(def.id, "candleBorderUpColor", v)),
10112
+ this.swatch(colorOf2("candleBorderDownColor", config.candles.borderDownColor), (v) => this.emitType(def.id, "candleBorderDownColor", v))
10113
+ ]));
10114
+ g.append(this.toggleRow("Wick", boolOf("candleWickVisible", config.candles.wickVisible), (v) => this.emitType(def.id, "candleWickVisible", v), [
10115
+ this.swatch(colorOf2("candleWickUpColor", config.candles.wickUpColor), (v) => this.emitType(def.id, "candleWickUpColor", v)),
10116
+ this.swatch(colorOf2("candleWickDownColor", config.candles.wickDownColor), (v) => this.emitType(def.id, "candleWickDownColor", v))
10117
+ ]));
10118
+ g.append(this.numberRow("Spacing", config.series.spacing, 0.1, 10, 0.1, (v) => this.emit({ series: { spacing: v } })));
10119
+ groups[def.id] = g;
10120
+ body.append(g);
10121
+ }
9350
10122
  showActive(config.series.style);
9351
10123
  body.append(this.sectionTitle("Time zone"));
9352
10124
  body.append(this.selectRowLabeled("Time zone", normalizeTimezone(config.timeScale.timezone), timezoneOptions(config.timeScale.timezone), (v) => this.emit({ timeScale: { timezone: v } })));
@@ -9361,7 +10133,16 @@ var SettingsDialog = class {
9361
10133
  }
9362
10134
  }
9363
10135
  };
10136
+ const renderChartTypeSections = (placement) => {
10137
+ for (const def of chartTypes()) {
10138
+ const typeSettings = def.settings;
10139
+ if (!typeSettings) continue;
10140
+ if ((typeSettings.placement ?? "end") !== placement) continue;
10141
+ this.chartTypeSection(def.id, typeSettings, config, body);
10142
+ }
10143
+ };
9364
10144
  renderHostSections("symbol");
10145
+ renderChartTypeSections("after-symbol");
9365
10146
  renderHostSections("after-symbol");
9366
10147
  body.append(this.section("Scales and lines"));
9367
10148
  body.append(this.sectionTitle("Price scale"));
@@ -9402,35 +10183,27 @@ var SettingsDialog = class {
9402
10183
  body.append(this.sectionTitle("Theme"));
9403
10184
  body.append(this.selectRow("Color theme", tc.current === "dark" ? "Dark" : "Light", ["Dark", "Light"], (v) => tc.onSelect(v === "Dark" ? "dark" : "light")));
9404
10185
  }
9405
- for (const def of chartTypes()) {
9406
- const typeSettings = def.settings;
9407
- if (!typeSettings) continue;
9408
- const marker = this.section(typeSettings.title);
9409
- marker.dataset.sdStyle = def.id;
9410
- marker.dataset.sdVisibility = typeSettings.visibility ?? "active";
9411
- body.append(marker);
9412
- const values = config.chartTypes[def.id] ?? {};
9413
- for (const r of typeSettings.rows) {
9414
- const current2 = values[r.key];
9415
- if (r.kind === "toggle") {
9416
- body.append(this.boolRow(r.label, typeof current2 === "boolean" ? current2 : r.defval, (v) => this.emitType(def.id, r.key, v)));
9417
- } else if (r.kind === "number") {
9418
- body.append(this.numberRow(r.label, typeof current2 === "number" ? current2 : r.defval, r.min ?? 0, r.max ?? 1e6, r.step ?? 1, (v) => this.emitType(def.id, r.key, v)));
9419
- } else if (r.kind === "color") {
9420
- body.append(this.colorRow(r.label, typeof current2 === "string" ? current2 : r.defval, (v) => this.emitType(def.id, r.key, v)));
9421
- } else {
9422
- body.append(this.selectRow(r.label, typeof current2 === "string" ? current2 : r.defval, [...r.options], (v) => this.emitType(def.id, r.key, v)));
9423
- }
9424
- }
9425
- }
10186
+ renderChartTypeSections("end");
9426
10187
  renderHostSections("end");
9427
10188
  const shell = document.createElement("div");
9428
- 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;";
9429
10190
  const rail = document.createElement("div");
9430
- 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
+ }
9431
10204
  const paneHost = document.createElement("div");
9432
10205
  paneHost.className = "vela-sd-pane";
9433
- 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;";
9434
10207
  const panes = [];
9435
10208
  let current = null;
9436
10209
  for (const child of [...body.children]) {
@@ -9441,7 +10214,7 @@ var SettingsDialog = class {
9441
10214
  const tab = document.createElement("button");
9442
10215
  tab.type = "button";
9443
10216
  tab.textContent = title;
9444
- tab.className = "vela-sd-tab";
10217
+ tab.className = "vela-sd-tab" + (child.dataset.sdSub !== void 0 ? " vela-sd-tab-sub" : "");
9445
10218
  panes.push({ title, el, tab, style: child.dataset.sdStyle, visibility: child.dataset.sdVisibility });
9446
10219
  current = el;
9447
10220
  child.remove();
@@ -9465,6 +10238,10 @@ var SettingsDialog = class {
9465
10238
  p.tab.classList.toggle("on", i === idx);
9466
10239
  });
9467
10240
  this.activeSection = panes[idx]?.title ?? null;
10241
+ if (mobile) {
10242
+ hTitle.textContent = panes[idx]?.title ?? "Chart settings";
10243
+ toggleRail?.(false);
10244
+ }
9468
10245
  };
9469
10246
  panes.forEach((p, i) => {
9470
10247
  p.tab.addEventListener("click", () => activate(i));
@@ -9472,19 +10249,28 @@ var SettingsDialog = class {
9472
10249
  paneHost.appendChild(p.el);
9473
10250
  });
9474
10251
  this.tabs = panes.map((p, i) => ({ title: p.title, show: () => activate(i) }));
9475
- 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"));
9476
10253
  activate(wanted >= 0 ? wanted : 0);
9477
10254
  this.syncTypeTabs?.(config.series.style);
9478
10255
  shell.append(rail, paneHost);
10256
+ if (railScrim) shell.append(railScrim);
9479
10257
  dlg.appendChild(shell);
9480
10258
  dlg.appendChild(this.footer(config));
9481
10259
  scrim.appendChild(dlg);
9482
10260
  this.container.appendChild(scrim);
9483
10261
  this.root = scrim;
9484
- for (const p of panes) this.layoutSettingsGrids(p.el, dlg);
10262
+ for (const p of panes) {
10263
+ const hosts = [...p.el.querySelectorAll("[data-sd-rows-host]")];
10264
+ if (hosts.length === 0) {
10265
+ this.layoutSettingsGrids(p.el, dlg);
10266
+ continue;
10267
+ }
10268
+ for (const h of hosts) this.layoutSettingsGrids(h, dlg);
10269
+ }
9485
10270
  }
9486
10271
  close() {
9487
10272
  closeColorPopover();
10273
+ closeWidthPopover();
9488
10274
  this.root?.remove();
9489
10275
  this.root = null;
9490
10276
  this.tabs = [];
@@ -9511,6 +10297,333 @@ var SettingsDialog = class {
9511
10297
  el.textContent = text;
9512
10298
  return el;
9513
10299
  }
10300
+ /**
10301
+ * One chart type's settings tab (and its subsection tabs), appended to the linear
10302
+ * `body` for the pane splitter to file. One live values BAG serves the whole section
10303
+ * — instances and subsections share the per-type store, so a `when` gate anywhere
10304
+ * can reference a key edited anywhere else; every edit runs every registered
10305
+ * refresher.
10306
+ */
10307
+ chartTypeSection(typeId, section, config, body) {
10308
+ const marker = this.section(section.title);
10309
+ marker.dataset.sdStyle = typeId;
10310
+ marker.dataset.sdVisibility = section.visibility ?? "active";
10311
+ body.append(marker);
10312
+ const values = config.chartTypes[typeId] ?? {};
10313
+ const bag = {};
10314
+ const seed = (rows) => {
10315
+ for (const r of rows) {
10316
+ for (const k of settingsRowValueKeys(r)) {
10317
+ const v = values[k.key];
10318
+ bag[k.key] = typeof v === k.type ? v : k.defval;
10319
+ }
10320
+ }
10321
+ };
10322
+ if (section.rows) seed(section.rows);
10323
+ for (const inst of section.instances ?? []) seed(inst.rows);
10324
+ for (const sub of section.subsections ?? []) seed(sub.rows);
10325
+ for (const inst of section.instances ?? []) {
10326
+ if (!inst.enableKey || inst.enableKey in bag) continue;
10327
+ bag[inst.enableKey] = values[inst.enableKey] === true;
10328
+ }
10329
+ for (const sub of section.subsections ?? []) {
10330
+ if (!sub.enableKey || sub.enableKey in bag) continue;
10331
+ bag[sub.enableKey] = values[sub.enableKey] === true;
10332
+ }
10333
+ const refreshers = [];
10334
+ const put = (key, v) => {
10335
+ bag[key] = v;
10336
+ this.emitType(typeId, key, v);
10337
+ for (const r of refreshers) r();
10338
+ };
10339
+ if (section.instances && section.instances.length > 0) {
10340
+ body.append(this.instancesBlock(typeId, section.instances, bag, put, refreshers));
10341
+ } else if (section.rows) {
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);
10344
+ }
10345
+ for (const sub of section.subsections ?? []) {
10346
+ const subMarker = this.section(sub.title);
10347
+ subMarker.dataset.sdStyle = typeId;
10348
+ subMarker.dataset.sdVisibility = section.visibility ?? "active";
10349
+ subMarker.dataset.sdSub = "1";
10350
+ body.append(subMarker);
10351
+ body.append(this.groupedRows(`${typeId}/${sub.title}`, sub.rows, bag, put, refreshers, sub.enableKey));
10352
+ }
10353
+ for (const r of refreshers) r();
10354
+ }
10355
+ /** The FLAT chart-type form: rows appended straight to the body (the pane grid wraps
10356
+ * them), headings/headers as inline group titles, `when` gates refreshed live. */
10357
+ flatTypeRows(rows, bag, put, refreshers, body) {
10358
+ const entries = [];
10359
+ for (const r of rows) {
10360
+ const el = r.kind === "heading" || r.kind === "header" ? this.sectionTitle(r.label) : this.typeRow(r, bag, put, refreshers);
10361
+ entries.push({ el, when: r.when });
10362
+ body.append(el);
10363
+ }
10364
+ refreshers.push(() => {
10365
+ for (const e of entries) e.el.classList.toggle("vela-sd-hide", !settingsRowVisible(e.when, bag));
10366
+ });
10367
+ }
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);
10391
+ }
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);
10399
+ }
10400
+ /**
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.
10404
+ */
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);
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);
10457
+ ni.addEventListener("change", () => {
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);
10462
+ });
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;
10471
+ }
10472
+ /**
10473
+ * The INSTANCE STRIP block: a tab per present instance (label, `×` on the active
10474
+ * removable one), a dashed `+` while an instance is still absent, and one
10475
+ * grouped-rows content per instance below — only the active tab's content shows.
10476
+ * Presence is the boolean at each instance's `enableKey`; the strip rebuilds on
10477
+ * every section edit, so gates elsewhere stay coherent.
10478
+ */
10479
+ instancesBlock(typeId, instances, bag, put, refreshers) {
10480
+ const wrap = document.createElement("div");
10481
+ const strip = document.createElement("div");
10482
+ strip.className = "vela-sd-itabs";
10483
+ if (instances.length > 1 || instances[0]?.enableKey !== void 0) wrap.append(strip);
10484
+ const contents = instances.map((inst, i) => {
10485
+ const content = this.groupedRows(`${typeId}/#${i}`, inst.rows, bag, put, refreshers);
10486
+ wrap.append(content);
10487
+ return content;
10488
+ });
10489
+ const present = (inst) => !inst.enableKey || bag[inst.enableKey] === true;
10490
+ const refresh = () => {
10491
+ let active = this.typeActiveInstance.get(typeId) ?? 0;
10492
+ if (!present(instances[active] ?? instances[0])) active = 0;
10493
+ this.typeActiveInstance.set(typeId, active);
10494
+ strip.replaceChildren();
10495
+ instances.forEach((inst, i) => {
10496
+ if (!present(inst)) return;
10497
+ const tab = document.createElement("button");
10498
+ tab.type = "button";
10499
+ tab.className = "vela-sd-itab" + (i === active ? " on" : "");
10500
+ const lbl = document.createElement("span");
10501
+ lbl.textContent = inst.label;
10502
+ tab.append(lbl);
10503
+ if (i === active && inst.enableKey) {
10504
+ const x = document.createElement("span");
10505
+ x.className = "vela-sd-ix";
10506
+ x.textContent = "\u2715";
10507
+ x.title = `Remove ${inst.label}`;
10508
+ x.addEventListener("click", (e) => {
10509
+ e.stopPropagation();
10510
+ this.typeActiveInstance.set(typeId, 0);
10511
+ put(inst.enableKey, false);
10512
+ });
10513
+ tab.append(x);
10514
+ }
10515
+ tab.addEventListener("click", () => {
10516
+ this.typeActiveInstance.set(typeId, i);
10517
+ refresh();
10518
+ });
10519
+ strip.append(tab);
10520
+ });
10521
+ const absent = instances.findIndex((inst) => !present(inst));
10522
+ if (absent >= 0) {
10523
+ const add = document.createElement("button");
10524
+ add.type = "button";
10525
+ add.className = "vela-sd-itab vela-sd-itab-add";
10526
+ add.textContent = "+";
10527
+ add.title = `Add ${instances[absent].label}`;
10528
+ add.addEventListener("click", () => {
10529
+ this.typeActiveInstance.set(typeId, absent);
10530
+ put(instances[absent].enableKey, true);
10531
+ });
10532
+ strip.append(add);
10533
+ }
10534
+ contents.forEach((c, i) => c.classList.toggle("vela-sd-hide", i !== active));
10535
+ };
10536
+ refreshers.push(refresh);
10537
+ return wrap;
10538
+ }
10539
+ /**
10540
+ * A GROUPED rows pane: a TOC column of the group labels (from `heading` rows) and
10541
+ * ONE rows host holding every row — the active group's rows show, the rest hide.
10542
+ * Rows BEFORE the first heading are the always block, visible above every group.
10543
+ * `header` rows stay in the rows column as in-group subgroup titles (not TOC
10544
+ * entries). A group whose value rows are all gated out (or whose heading's own
10545
+ * `when` fails) leaves the TOC; the whole TOC hides when no group is live. The
10546
+ * rows host is tagged for `layoutSettingsGrids`, keeping one shared label column
10547
+ * across all groups.
10548
+ *
10549
+ * `enableKey` (optional): while that bag boolean is false, every row except the one
10550
+ * whose key matches is soft-disabled (visible but grayed / non-interactive) so the
10551
+ * pane stays browseable with the feature off.
10552
+ */
10553
+ groupedRows(paneKey, rows, bag, put, refreshers, enableKey) {
10554
+ const wrap = document.createElement("div");
10555
+ wrap.className = "vela-sd-struct";
10556
+ const toc = document.createElement("div");
10557
+ toc.className = "vela-sd-toc";
10558
+ const rowsHost = document.createElement("div");
10559
+ rowsHost.dataset.sdRowsHost = "1";
10560
+ rowsHost.style.cssText = "flex:1 1 auto;min-width:0;";
10561
+ wrap.append(toc, rowsHost);
10562
+ const entries = [];
10563
+ const groups = [];
10564
+ const groupWhens = [];
10565
+ let g = -1;
10566
+ for (const r of rows) {
10567
+ if (r.kind === "heading") {
10568
+ g = groups.length;
10569
+ groups.push(r.label);
10570
+ groupWhens.push(r.when);
10571
+ continue;
10572
+ }
10573
+ if (r.kind === "header") {
10574
+ entries.push({ el: this.sectionTitle(r.label), when: r.when, group: g, header: true });
10575
+ rowsHost.append(entries[entries.length - 1].el);
10576
+ continue;
10577
+ }
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;
10580
+ entries.push({ el, when: r.when, group: g, key });
10581
+ rowsHost.append(el);
10582
+ }
10583
+ const refresh = () => {
10584
+ const live = groups.map((_, gi) => settingsRowVisible(groupWhens[gi], bag) && entries.some((e) => e.group === gi && !e.header && settingsRowVisible(e.when, bag)));
10585
+ let activeIdx = groups.indexOf(this.typeActiveGroup.get(paneKey) ?? "");
10586
+ if (activeIdx < 0 || !live[activeIdx]) activeIdx = live.findIndex(Boolean);
10587
+ if (activeIdx >= 0) this.typeActiveGroup.set(paneKey, groups[activeIdx]);
10588
+ toc.replaceChildren();
10589
+ groups.forEach((label, gi) => {
10590
+ if (!live[gi]) return;
10591
+ const b = document.createElement("button");
10592
+ b.type = "button";
10593
+ b.className = "vela-sd-toc-btn" + (gi === activeIdx ? " on" : "");
10594
+ b.textContent = label;
10595
+ b.addEventListener("click", () => {
10596
+ this.typeActiveGroup.set(paneKey, label);
10597
+ refresh();
10598
+ });
10599
+ toc.append(b);
10600
+ });
10601
+ const anyGroup = live.some(Boolean);
10602
+ toc.classList.toggle("vela-sd-hide", !anyGroup);
10603
+ wrap.classList.toggle("no-toc", !anyGroup);
10604
+ const enabled = !enableKey || bag[enableKey] === true;
10605
+ for (let i = 0; i < entries.length; i++) {
10606
+ const e = entries[i];
10607
+ let visible = (e.group === -1 || e.group === activeIdx) && settingsRowVisible(e.when, bag);
10608
+ if (visible && e.header) {
10609
+ let hasContent = false;
10610
+ for (let j = i + 1; j < entries.length; j++) {
10611
+ const n = entries[j];
10612
+ if (n.group !== e.group || n.header) break;
10613
+ if (settingsRowVisible(n.when, bag)) {
10614
+ hasContent = true;
10615
+ break;
10616
+ }
10617
+ }
10618
+ visible = hasContent;
10619
+ }
10620
+ e.el.classList.toggle("vela-sd-hide", !visible);
10621
+ e.el.classList.toggle("vela-sd-soft", visible && !enabled && e.key !== enableKey);
10622
+ }
10623
+ };
10624
+ refreshers.push(refresh);
10625
+ return wrap;
10626
+ }
9514
10627
  /** Vertical breathing space between row clusters — grouping reads from whitespace,
9515
10628
  * so no drawn rule. */
9516
10629
  separator() {
@@ -9529,6 +10642,14 @@ var SettingsDialog = class {
9529
10642
  */
9530
10643
  layoutSettingsGrids(pane, measureHost) {
9531
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
+ }
9532
10653
  const rows = [...pane.querySelectorAll(".vela-sd-row")];
9533
10654
  const probe2 = document.createElement("div");
9534
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;";
@@ -9565,9 +10686,11 @@ var SettingsDialog = class {
9565
10686
  }
9566
10687
  }
9567
10688
  /** A bare color swatch input (for toggle-row right groups / swatch pairs). */
9568
- 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) {
9569
10692
  let current = value;
9570
- return colorField(this.theme, () => current, (v) => {
10693
+ return colorField(this.theme, () => get ? get() : current, (v) => {
9571
10694
  current = v;
9572
10695
  onChange(v);
9573
10696
  });
@@ -9619,8 +10742,11 @@ var SettingsDialog = class {
9619
10742
  }
9620
10743
  /** An enable row: checkbox + label in the label column, dependent controls in the
9621
10744
  * shared control column; the control group dims and ignores input while the toggle
9622
- * is off. With no controls it reads like a plain toggle row (full-width in the grid). */
9623
- 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) {
9624
10750
  const wrap = document.createElement("div");
9625
10751
  const cb = document.createElement("button");
9626
10752
  cb.type = "button";
@@ -9640,6 +10766,12 @@ var SettingsDialog = class {
9640
10766
  wrap.style.cssText = "display:flex;align-items:center;gap:8px;min-height:22px;";
9641
10767
  wrap.append(cb, lbl);
9642
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
+ }
9643
10775
  return wrap;
9644
10776
  }
9645
10777
  wrap.className = "vela-sd-row";
@@ -9650,8 +10782,11 @@ var SettingsDialog = class {
9650
10782
  box.style.cssText = "display:flex;align-items:center;gap:6px;";
9651
10783
  for (const c of controls) box.appendChild(c);
9652
10784
  const syncDim = (on) => {
9653
- box.style.opacity = on ? "1" : "0.4";
9654
- 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
+ }
9655
10790
  };
9656
10791
  syncDim(value);
9657
10792
  cb.addEventListener("click", () => {
@@ -9659,6 +10794,13 @@ var SettingsDialog = class {
9659
10794
  onToggle(v);
9660
10795
  syncDim(v);
9661
10796
  });
10797
+ if (get) {
10798
+ wrap.addEventListener("vela-sync", () => {
10799
+ checked = get();
10800
+ cb.classList.toggle("on", checked);
10801
+ syncDim(checked);
10802
+ });
10803
+ }
9662
10804
  wrap.append(left, box);
9663
10805
  return wrap;
9664
10806
  }
@@ -9678,102 +10820,12 @@ var SettingsDialog = class {
9678
10820
  wrap.appendChild(ni);
9679
10821
  return wrap;
9680
10822
  }
9681
- /** Inline Auto/Manual row (Resolution, Text size): a mode dropdown + a value input greyed out in Auto. */
9682
- autoManualRow(label, isAuto, value, min, max, unit, onMode, onValue) {
9683
- const { wrap } = this.row(label);
9684
- const box = document.createElement("div");
9685
- box.style.cssText = "display:flex;align-items:center;gap:6px;flex:0 0 auto;";
9686
- const sel = document.createElement("select");
9687
- sel.className = "vela-sd-select";
9688
- for (const [val, lbl] of AUTO_MANUAL_OPTS) {
9689
- const o = document.createElement("option");
9690
- o.value = val;
9691
- o.textContent = lbl;
9692
- if (val === "auto" === isAuto) o.selected = true;
9693
- sel.appendChild(o);
9694
- }
9695
- const ni = document.createElement("input");
9696
- ni.type = "number";
9697
- ni.min = String(min);
9698
- ni.max = String(max);
9699
- ni.step = "1";
9700
- ni.value = String(value);
9701
- ni.className = "vela-sd-number";
9702
- const unitLbl = document.createElement("span");
9703
- unitLbl.textContent = unit;
9704
- unitLbl.style.cssText = "opacity:0.6;";
9705
- const syncDisabled = (auto) => {
9706
- ni.disabled = auto;
9707
- ni.style.opacity = auto ? "0.4" : "1";
9708
- };
9709
- syncDisabled(isAuto);
9710
- sel.addEventListener("change", () => {
9711
- const auto = sel.value === "auto";
9712
- onMode(auto);
9713
- syncDisabled(auto);
9714
- });
9715
- ni.addEventListener("input", () => {
9716
- const n = Number(ni.value);
9717
- if (Number.isFinite(n)) onValue(n);
9718
- });
9719
- box.append(sel, ni, unitLbl);
9720
- wrap.appendChild(box);
9721
- return wrap;
9722
- }
9723
- /** A row whose control area holds several inline controls (e.g. show + color + width). */
9724
- inlineRow(label, controls) {
9725
- const { wrap } = this.row(label);
9726
- const box = document.createElement("div");
9727
- box.style.cssText = "display:flex;align-items:center;gap:6px;flex:0 0 auto;";
9728
- for (const c of controls) box.appendChild(c);
9729
- wrap.appendChild(box);
9730
- return wrap;
9731
- }
9732
- /** A small dimmed hint span (e.g. the ≥ / ≤ between filter inputs). */
9733
- hint(text) {
9734
- const s = document.createElement("span");
9735
- s.textContent = text;
9736
- s.style.cssText = "opacity:0.5;font-size:12px;";
9737
- return s;
9738
- }
9739
- /** A bare color input (for inline groups). */
9740
- colorInput(value, onChange) {
9741
- const ci = document.createElement("input");
9742
- ci.type = "color";
9743
- ci.value = toHex6(value);
9744
- ci.style.cssText = "cursor:pointer;width:34px;height:22px;border:none;background:transparent;padding:0;flex:0 0 auto;";
9745
- ci.addEventListener("input", () => onChange(ci.value));
9746
- return ci;
9747
- }
9748
- /** A bare compact number input (for inline groups). */
9749
- numberInput(value, min, max, step, onChange) {
9750
- const ni = document.createElement("input");
9751
- ni.type = "number";
9752
- ni.value = String(value);
9753
- ni.min = String(min);
9754
- ni.max = String(max);
9755
- ni.step = String(step);
9756
- ni.className = "vela-sd-number";
9757
- ni.style.flex = "0 0 auto";
9758
- ni.addEventListener("input", () => {
9759
- const n = Number(ni.value);
9760
- if (Number.isFinite(n)) onChange(n);
9761
- });
9762
- return ni;
9763
- }
9764
- /** A small in-group heading (lighter than a top-level section divider). */
9765
- subheading(text) {
9766
- const el = document.createElement("div");
9767
- el.textContent = text;
9768
- el.style.cssText = "margin-top:4px;font-size:10.5px;font-weight:700;letter-spacing:0.03em;text-transform:uppercase;opacity:0.45;";
9769
- return el;
9770
- }
9771
10823
  /** A dropdown whose option values differ from their display labels. */
9772
10824
  selectRowLabeled(label, value, options, onChange) {
9773
10825
  const { wrap } = this.row(label);
9774
10826
  const sel = document.createElement("select");
9775
10827
  sel.className = "vela-sd-select";
9776
- sel.style.cssText = "max-width:140px;flex:0 0 auto;";
10828
+ sel.style.cssText = "max-width:200px;flex:0 0 auto;";
9777
10829
  for (const [val, lbl] of options) {
9778
10830
  const o = document.createElement("option");
9779
10831
  o.value = val;
@@ -9789,7 +10841,7 @@ var SettingsDialog = class {
9789
10841
  const { wrap } = this.row(label);
9790
10842
  const sel = document.createElement("select");
9791
10843
  sel.className = "vela-sd-select";
9792
- sel.style.cssText = "max-width:140px;flex:0 0 auto;";
10844
+ sel.style.cssText = "max-width:200px;flex:0 0 auto;";
9793
10845
  for (const opt of options) {
9794
10846
  const o = document.createElement("option");
9795
10847
  o.value = opt;
@@ -9813,19 +10865,10 @@ var SettingsDialog = class {
9813
10865
  foot.appendChild(resetBtn);
9814
10866
  return foot;
9815
10867
  }
9816
- button(label, onClick) {
9817
- const b = document.createElement("button");
9818
- b.type = "button";
9819
- b.textContent = label;
9820
- b.style.cssText = `cursor:pointer;flex:1 1 auto;${this.ctrlStyle()}padding:5px 8px;font-weight:600;`;
9821
- b.addEventListener("click", onClick);
9822
- return b;
9823
- }
9824
- ctrlStyle() {
9825
- 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;`;
9826
- }
9827
10868
  };
9828
- var AUTO_MANUAL_OPTS = [["auto", "Auto"], ["manual", "Manual"]];
10869
+ function normalizeSelectOptions(options) {
10870
+ return options.map((o) => typeof o === "string" ? [o, o] : o);
10871
+ }
9829
10872
  function timezoneOptions(current) {
9830
10873
  const options = TIMEZONES.map((t) => [t.value, tzMenuLabel(t.value, t.label)]);
9831
10874
  const normalized = normalizeTimezone(current);
@@ -10025,7 +11068,7 @@ var DrawingPainter = class {
10025
11068
  return;
10026
11069
  }
10027
11070
  if (d instanceof FixedRangeVolumeProfile) {
10028
- this.paintFixedRangeVp(ctx, d, proj);
11071
+ this.paintFixedRangeVp(ctx, d, proj, theme);
10029
11072
  this.paintLabel(ctx, d, proj, theme);
10030
11073
  return;
10031
11074
  }
@@ -10518,7 +11561,7 @@ var DrawingPainter = class {
10518
11561
  /** Paint a fixed-range volume profile: horizontal histogram rows (up/down split) anchored to
10519
11562
  * the left or right of the time span, optional VAH / VAL / POC levels across the range, and
10520
11563
  * optional developing POC / VA polylines. Recomputes from the two anchors on every paint. */
10521
- paintFixedRangeVp(ctx, d, proj) {
11564
+ paintFixedRangeVp(ctx, d, proj, theme) {
10522
11565
  const L = d.layout(proj);
10523
11566
  if (!L) return;
10524
11567
  const s = d.frvp;
@@ -10557,7 +11600,7 @@ var DrawingPainter = class {
10557
11600
  };
10558
11601
  hLine(s.showVah, s.vahColor, s.vahStyle, L.vahY);
10559
11602
  hLine(s.showVal, s.valColor, s.valStyle, L.valY);
10560
- hLine(s.showPoc, s.pocColor, s.pocStyle, L.pocY);
11603
+ hLine(s.showPoc, s.pocColor ?? contrastColor(theme.background), s.pocStyle, L.pocY);
10561
11604
  const devW = Math.max(1, w);
10562
11605
  if (s.showDevelopingPoc) {
10563
11606
  this.strokePolyline(ctx, L.developingPoc, {
@@ -11184,6 +12227,8 @@ function topDrawingAt(drawings, x, y, proj, tol = HIT_TOLERANCE) {
11184
12227
  // src/renderers/native/drawings/DrawingInteraction.ts
11185
12228
  var DRAG_SLOP2 = 3;
11186
12229
  var FREEHAND_SAMPLE_PX = 4;
12230
+ var ANGLE_SNAP_STEP = Math.PI / 4;
12231
+ var ANGLE_SNAP_TYPES = /* @__PURE__ */ new Set(["trendline", "ray", "extendedline", "infoline", "trendangle", "arrow"]);
11187
12232
  var DrawingInteraction = class {
11188
12233
  // magnet target for the ring
11189
12234
  constructor(deps) {
@@ -11224,6 +12269,27 @@ var DrawingInteraction = class {
11224
12269
  this.snapAt = changed ? { point: snapped, paneId } : null;
11225
12270
  return snapped;
11226
12271
  }
12272
+ /** Resolve a pixel to a data point with the segment angle locked to 45° steps around
12273
+ * `pivot` (Shift held on a line tool). Works in PIXEL space — the user reasons about
12274
+ * the angle they see, not about time/price units. The magnet is bypassed: snapping
12275
+ * the result to a candle afterwards would break the exact angle just enforced. */
12276
+ resolveAngleSnapped(pivot, paneId, x, y) {
12277
+ const proj = this.deps.projector();
12278
+ const px = proj.xOf(pivot.time);
12279
+ const py = proj.yOf(pivot.price, paneId);
12280
+ this.snapAt = null;
12281
+ if (py == null) return proj.pxToPoint(x, y, paneId);
12282
+ const r = Math.hypot(x - px, y - py);
12283
+ if (r < 1) return proj.pxToPoint(x, y, paneId);
12284
+ const angle = Math.round(Math.atan2(y - py, x - px) / ANGLE_SNAP_STEP) * ANGLE_SNAP_STEP;
12285
+ return proj.pxToPoint(px + r * Math.cos(angle), py + r * Math.sin(angle), paneId);
12286
+ }
12287
+ /** The pivot the free endpoint angle-snaps around while placing `draft`, or null when
12288
+ * the tool is not a two-point segment (or no anchor is committed yet). */
12289
+ placingPivot(draft) {
12290
+ if (!ANGLE_SNAP_TYPES.has(draft.type)) return null;
12291
+ return draft.anchors[draft.anchors.length - 1] ?? null;
12292
+ }
11227
12293
  /** Should the drawing layer win this press (vs pan)? Armed/placing/pressed, or over a drawing/handle. */
11228
12294
  claim(x, y) {
11229
12295
  if (this.deps.activeTool() != null || this.state.kind !== "idle") return true;
@@ -11245,12 +12311,15 @@ var DrawingInteraction = class {
11245
12311
  return this.state.kind === "pressed" && this.state.moved ? this.state.id : null;
11246
12312
  }
11247
12313
  /** A press: place the next anchor, or grab a drawing/handle (resolved as click or drag on release).
11248
- * `shift` over a drawing toggles it in/out of the selection (multi-select) instead of dragging. */
12314
+ * `shift` over a drawing toggles it in/out of the selection (multi-select) instead of dragging;
12315
+ * while placing a line tool it locks the segment angle to 45° steps. */
11249
12316
  down(x, y, mode = "off", shift = false) {
11250
12317
  const proj = this.deps.projector();
11251
12318
  if (this.state.kind === "placing") {
11252
12319
  if (this.state.draft.placementMode() !== "click") return;
11253
- this.state.draft.anchors.push(this.placementAnchor(this.state.draft, this.resolve(x, y, this.state.draft.paneId, mode)));
12320
+ const pivot = shift ? this.placingPivot(this.state.draft) : null;
12321
+ const point = pivot ? this.resolveAngleSnapped(pivot, this.state.draft.paneId, x, y) : this.resolve(x, y, this.state.draft.paneId, mode);
12322
+ this.state.draft.anchors.push(this.placementAnchor(this.state.draft, point));
11254
12323
  if (this.state.draft.anchors.length >= this.state.need) this.finalize();
11255
12324
  else this.deps.changed();
11256
12325
  return;
@@ -11275,8 +12344,9 @@ var DrawingInteraction = class {
11275
12344
  this.state = { kind: "pressed", id: hit.id, handle, grab: proj.pxToPoint(x, y, hit.paneId), orig: cloneAnchors(hit), px: x, py: y, moved: false };
11276
12345
  }
11277
12346
  }
11278
- /** Pointer move: advance the placing ghost, or live-preview a drag once past the slop. */
11279
- move(x, y, mode = "off") {
12347
+ /** Pointer move: advance the placing ghost, or live-preview a drag once past the slop.
12348
+ * `shift` on a line tool locks the segment angle to 45° steps (placing + handle drags). */
12349
+ move(x, y, mode = "off", shift = false) {
11280
12350
  if (this.state.kind === "idle") {
11281
12351
  const tool = this.deps.activeTool();
11282
12352
  const had = this.snapAt != null;
@@ -11290,7 +12360,8 @@ var DrawingInteraction = class {
11290
12360
  this.captureFreehand(x, y);
11291
12361
  return;
11292
12362
  }
11293
- this.state.cursor = this.resolve(x, y, this.state.draft.paneId, mode);
12363
+ const pivot = shift ? this.placingPivot(this.state.draft) : null;
12364
+ this.state.cursor = pivot ? this.resolveAngleSnapped(pivot, this.state.draft.paneId, x, y) : this.resolve(x, y, this.state.draft.paneId, mode);
11294
12365
  this.deps.changed();
11295
12366
  return;
11296
12367
  }
@@ -11301,7 +12372,11 @@ var DrawingInteraction = class {
11301
12372
  this.state.moved = true;
11302
12373
  if (this.state.handle >= 0) {
11303
12374
  const a = d.anchors[this.state.handle];
11304
- if (a) applyFree(a, this.resolve(x, y, d.paneId, mode), d.anchorSchema().slots[this.state.handle]?.free ?? "both");
12375
+ if (a) {
12376
+ const pivot = shift && ANGLE_SNAP_TYPES.has(d.type) && d.anchors.length === 2 ? d.anchors[1 - this.state.handle] : void 0;
12377
+ const point = pivot ? this.resolveAngleSnapped(pivot, d.paneId, x, y) : this.resolve(x, y, d.paneId, mode);
12378
+ applyFree(a, point, d.anchorSchema().slots[this.state.handle]?.free ?? "both");
12379
+ }
11305
12380
  d.constrainHandleDrag(this.state.handle);
11306
12381
  } else {
11307
12382
  const pt = this.deps.projector().pxToPoint(x, y, d.paneId);
@@ -11397,7 +12472,11 @@ var DrawingInteraction = class {
11397
12472
  const last = draft.anchors[draft.anchors.length - 1];
11398
12473
  const lx = last ? proj.xOf(last.time) : NaN;
11399
12474
  const ly = last ? proj.yOf(last.price, draft.paneId) ?? NaN : NaN;
11400
- 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
+ }
11401
12480
  draft.anchors.push(proj.pxToPoint(x, y, draft.paneId));
11402
12481
  }
11403
12482
  this.state.cursor = proj.pxToPoint(x, y, draft.paneId);
@@ -11470,11 +12549,11 @@ function applyFree(anchor, pt, free) {
11470
12549
  // src/renderers/native/drawings/DrawingSettingsPopup.ts
11471
12550
  var BTN = 30;
11472
12551
  var ICON = 21;
11473
- var STYLE_ID4 = "vela-drawing-popup-styles";
11474
- function ensureStyles4() {
11475
- 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;
11476
12555
  const s = document.createElement("style");
11477
- s.id = STYLE_ID4;
12556
+ s.id = STYLE_ID5;
11478
12557
  s.textContent = `
11479
12558
  .vela-dpop input[type=color]{-webkit-appearance:none;appearance:none;border:none;padding:0;background:none;cursor:pointer;border-radius:0;}
11480
12559
  .vela-dpop input[type=color]::-webkit-color-swatch-wrapper{padding:0;}
@@ -11493,7 +12572,10 @@ function ensureStyles4() {
11493
12572
  .vela-dpop-btn[data-active='1']{background:var(--vela-active);color:var(--vela-fg-bright);}
11494
12573
  .vela-dpop-item{background:transparent;transition:background var(--vela-dur-fast) ease;}
11495
12574
  .vela-dpop-item:hover{background:var(--vela-hover-strong);}
11496
- .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;}}`;
11497
12579
  document.head.appendChild(s);
11498
12580
  }
11499
12581
  var DrawingSettingsPopup = class {
@@ -11540,7 +12622,7 @@ var DrawingSettingsPopup = class {
11540
12622
  * `onClose` fires on an outside-click dismissal (not a programmatic close). */
11541
12623
  open(drawing, anchor, actions, onClose) {
11542
12624
  this.close();
11543
- ensureStyles4();
12625
+ ensureStyles5();
11544
12626
  this.onClose = onClose ?? null;
11545
12627
  const t = this.theme;
11546
12628
  const schema = drawing.schema();
@@ -11566,7 +12648,14 @@ var DrawingSettingsPopup = class {
11566
12648
  bar.appendChild(this.dropdown("Icon size", STAMP_SIZE_OPTIONS, sz, (s) => stampSizeIcon(s), (v) => actions.patch({ size: v }), { label: sizeLabel }));
11567
12649
  }
11568
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 })));
11569
- 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
+ }
11570
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 }));
11571
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 })));
11572
12661
  const isFrvp = paths.has("frvp.rows") && drawing instanceof FixedRangeVolumeProfile;
@@ -12034,14 +13123,15 @@ var DrawingSettingsPopup = class {
12034
13123
  const chk = document.createElement("input");
12035
13124
  chk.type = "checkbox";
12036
13125
  chk.checked = Boolean(s[showPath]);
12037
- 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;`;
12038
13128
  chk.addEventListener("change", () => actions.patch({ [`frvp.${showPath}`]: chk.checked }));
12039
13129
  const lbl = document.createElement("span");
12040
13130
  lbl.textContent = label;
12041
13131
  lbl.style.cssText = "flex:1;min-width:0;opacity:0.9;";
12042
13132
  const col = document.createElement("button");
12043
13133
  col.type = "button";
12044
- let cur = s[colorPath];
13134
+ let cur = current;
12045
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;`;
12046
13136
  col.addEventListener("pointerdown", (e) => e.stopPropagation());
12047
13137
  col.addEventListener("click", (e) => {
@@ -12221,7 +13311,8 @@ var DrawingSettingsPopup = class {
12221
13311
  dragHandle() {
12222
13312
  const b = document.createElement("button");
12223
13313
  b.type = "button";
12224
- 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);`;
12225
13316
  b.innerHTML = sized(GRIP_ICON);
12226
13317
  b.addEventListener("pointerdown", (e) => {
12227
13318
  const el = this.el;
@@ -12237,6 +13328,10 @@ var DrawingSettingsPopup = class {
12237
13328
  const origLeft = parseFloat(el.style.left) || 0;
12238
13329
  const origTop = parseFloat(el.style.top) || 0;
12239
13330
  b.style.cursor = "grabbing";
13331
+ try {
13332
+ b.setPointerCapture(e.pointerId);
13333
+ } catch {
13334
+ }
12240
13335
  const move = (ev) => {
12241
13336
  const w = el.offsetWidth;
12242
13337
  const h = el.offsetHeight;
@@ -12249,9 +13344,11 @@ var DrawingSettingsPopup = class {
12249
13344
  b.style.cursor = "grab";
12250
13345
  window.removeEventListener("pointermove", move);
12251
13346
  window.removeEventListener("pointerup", up);
13347
+ window.removeEventListener("pointercancel", up);
12252
13348
  };
12253
13349
  window.addEventListener("pointermove", move);
12254
13350
  window.addEventListener("pointerup", up);
13351
+ window.addEventListener("pointercancel", up);
12255
13352
  });
12256
13353
  return b;
12257
13354
  }
@@ -12367,6 +13464,39 @@ var DrawingSettingsPopup = class {
12367
13464
  });
12368
13465
  return b;
12369
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
+ }
12370
13500
  /** A pick-one dropdown: the trigger shows the current value's glyph (plus an optional inline
12371
13501
  * text label — e.g. `2px` — for controls that would otherwise look alike), and clicking it
12372
13502
  * opens a floating list of every option so a value is one click away (no cycling through). */
@@ -12810,12 +13940,19 @@ var UserDrawingController = class {
12810
13940
  this.activeTool = null;
12811
13941
  // last-used style for the armed tool (seeds the placement ghost)
12812
13942
  this.intentCb = null;
13943
+ /** Another chart's in-progress placement, mirrored here as a ghost (drawings sync). */
13944
+ this.externalGhost = null;
13945
+ /** Last draft fingerprint reported upstream — gates the per-render emission to actual changes. */
13946
+ this.lastDraftKey = null;
12813
13947
  this.measure = new MeasureOverlay();
12814
13948
  // transient ruler — not a persistent drawing
12815
13949
  this.toolbarVisible = false;
12816
13950
  // mirrors showToolbar (drives the gutter reservation)
12817
13951
  this.toolbarCollapsed = false;
12818
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;
12819
13956
  /** Self-serve Ctrl+Z / Ctrl+Y as drawing undo/redo. A host that owns a UNIFIED
12820
13957
  * history (drawings + app ops in one timeline) turns this off so the chords
12821
13958
  * bubble to its keymap instead — see the renderer's `historyChords` feature. */
@@ -12874,12 +14011,26 @@ var UserDrawingController = class {
12874
14011
  }
12875
14012
  showToolbar(visible) {
12876
14013
  this.toolbarVisible = visible;
12877
- 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);
12878
14023
  this.syncToolbarGutter();
12879
14024
  }
12880
14025
  /** The gutter follows the bar's current footprint: hidden 0, collapsed a slim strip, else full width. */
12881
14026
  syncToolbarGutter() {
12882
- 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);
14029
+ }
14030
+ /** Core push: mirror (or clear) another chart's in-progress placement as a ghost. */
14031
+ setExternalGhost(doc) {
14032
+ this.externalGhost = doc ? deserializeDrawing(doc) : null;
14033
+ this.render();
12883
14034
  }
12884
14035
  syncDrawings(docs) {
12885
14036
  this.drawings = docs.map((d) => deserializeDrawing(d)).filter((d) => d != null);
@@ -13039,7 +14190,7 @@ var UserDrawingController = class {
13039
14190
  }
13040
14191
  this.interaction.down(x, y, snap, shift);
13041
14192
  }
13042
- pointerMove(x, y, snap = "off") {
14193
+ pointerMove(x, y, snap = "off", shift = false) {
13043
14194
  if (this.eraserMode) {
13044
14195
  if (this.erasing) this.deleteAt(x, y);
13045
14196
  return;
@@ -13049,7 +14200,7 @@ var UserDrawingController = class {
13049
14200
  this.render();
13050
14201
  return;
13051
14202
  }
13052
- this.interaction.move(x, y, snap);
14203
+ this.interaction.move(x, y, snap, shift);
13053
14204
  this.updateHover(x, y);
13054
14205
  }
13055
14206
  /** Track which drawing is hovered so its handles appear on hover (and only then). */
@@ -13435,6 +14586,8 @@ var UserDrawingController = class {
13435
14586
  this.layoutTextEditor();
13436
14587
  const ghost = this.interaction.ghost();
13437
14588
  if (ghost) this.painter.paintGhost(ctx, ghost, proj, this.deps.theme());
14589
+ if (this.externalGhost) this.painter.paintGhost(ctx, this.externalGhost, proj, this.deps.theme());
14590
+ this.emitDraft(ghost);
13438
14591
  const markers = this.interaction.placingMarkers(proj);
13439
14592
  if (markers) this.painter.paintHandles(ctx, markers);
13440
14593
  const m = this.interaction.snapMarker();
@@ -13496,6 +14649,15 @@ var UserDrawingController = class {
13496
14649
  }
13497
14650
  }, () => this.clearSelection());
13498
14651
  }
14652
+ /** Report placement progress upstream (`draft` intent) so the drawings sync can
14653
+ * mirror the ghost live on linked charts. Called from every render; only actual
14654
+ * shape changes emit, and the end of a placement emits one `null`. */
14655
+ emitDraft(ghost) {
14656
+ const key = ghost ? JSON.stringify({ t: ghost.type, p: ghost.paneId, a: ghost.anchors, s: ghost.style, x: ghost.text }) : null;
14657
+ if (key === this.lastDraftKey) return;
14658
+ this.lastDraftKey = key;
14659
+ this.intentCb?.({ kind: "draft", doc: ghost ? ghost.serialize() : null });
14660
+ }
13499
14661
  emit(i) {
13500
14662
  if (i.kind === "create") {
13501
14663
  if (i.doc.text && i.doc.text.color === void 0) {
@@ -13732,7 +14894,7 @@ var LUXALGO_WORDMARK_SVG = '<svg viewBox="0 0 964 252" aria-hidden="true"><path
13732
14894
 
13733
14895
  // src/renderers/native/chrome/AttributionMark.ts
13734
14896
  var ATTRIBUTION_URL = "https://luxalgo.com/vela";
13735
- var STYLE_ID5 = "vela-attribution-styles";
14897
+ var STYLE_ID6 = "vela-attribution-styles";
13736
14898
  var CSS = `
13737
14899
  .vela-attribution {
13738
14900
  text-decoration: none;
@@ -13778,12 +14940,28 @@ var CSS = `
13778
14940
  max-width: 240px;
13779
14941
  opacity: 1;
13780
14942
  transform: translateX(0);
13781
- }`;
13782
- function ensureStyles5(doc) {
13783
- 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);
13784
14962
  if (!s) {
13785
14963
  s = doc.createElement("style");
13786
- s.id = STYLE_ID5;
14964
+ s.id = STYLE_ID6;
13787
14965
  doc.head.appendChild(s);
13788
14966
  }
13789
14967
  s.textContent = CSS;
@@ -13812,7 +14990,7 @@ function createCustomMark(doc, html, background) {
13812
14990
  return el;
13813
14991
  }
13814
14992
  function createAttributionMark(doc, background) {
13815
- ensureStyles5(doc);
14993
+ ensureStyles6(doc);
13816
14994
  const a = doc.createElement("a");
13817
14995
  a.className = "vela-attribution";
13818
14996
  a.href = ATTRIBUTION_URL;
@@ -13838,6 +15016,174 @@ function createAttributionMark(doc, background) {
13838
15016
  return a;
13839
15017
  }
13840
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
+
13841
15187
  // src/renderers/native/vpvr/profile.ts
13842
15188
  function buildVpvrProfile(bars, i0, i1, rowCount, valueAreaFrac) {
13843
15189
  const from = Math.max(0, i0);
@@ -14006,12 +15352,13 @@ var AXIS_COL_W = AXIS_MERGED_W;
14006
15352
  var COLLAPSED_PANE_H = 26;
14007
15353
  var TIME_AXIS_H = 22;
14008
15354
  var SCROLL_BTN_SIZE = 26;
15355
+ var SCROLL_BTN_SIZE_TOUCH = 24;
14009
15356
  var SCROLL_BTN_RADIUS = 5;
14010
15357
  var SCROLL_BTN_SHADOW = "0 2px 8px rgba(0,0,0,0.4)";
14011
15358
  var SCROLL_BTN_ICON_PX = 12;
14012
- var SCROLL_BTN_RIGHT_INSET = 25;
15359
+ var SCROLL_BTN_RIGHT_INSET = 12;
14013
15360
  var SCROLL_BTN_RIGHT = RIGHT_AXIS_W + SCROLL_BTN_RIGHT_INSET;
14014
- var SCROLL_BTN_BOTTOM = TIME_AXIS_H + 30;
15361
+ var SCROLL_BTN_BOTTOM = TIME_AXIS_H + 14;
14015
15362
  var SCROLL_BTN_PROXIMITY_PX = 120;
14016
15363
  var MIN_VISIBLE_BARS = 2;
14017
15364
  var ZOOM_OUT_MARGIN_BARS = 6;
@@ -14027,10 +15374,10 @@ var SEPARATOR_HIT_PX = 4;
14027
15374
  var NativeRenderer = class {
14028
15375
  constructor(opts) {
14029
15376
  this.capabilities = NATIVE_CAPABILITIES;
14030
- // The app "chrome" surface (drawing toolbar + axis-scale gutters): background + text.
15377
+ // The app "chrome" surface (drawing toolbar + in-chart dialogs): background + text.
14031
15378
  // Owned by the app theme alone (mount/setTheme) — config-level background/text edits
14032
- // (settings dialog, persisted configs) recolor only the plot, keeping the toolbar and
14033
- // scales on a consistent, always-readable surface.
15379
+ // (settings dialog, persisted configs) recolor the plot AND the axis scales (see
15380
+ // axisSurface), but never the toolbar/dialog chrome.
14034
15381
  this.surfaceBackground = DARK_THEME.background;
14035
15382
  this.surfaceTextColor = DARK_THEME.textColor;
14036
15383
  // the plot area (canvases + DOM overlays), inset to the right of the toolbar gutter
@@ -14063,8 +15410,12 @@ var NativeRenderer = class {
14063
15410
  this.symbolPicker = null;
14064
15411
  /** Indicator titles (the legend rows) shown — held here so a remount re-applies it. */
14065
15412
  this.indicatorTitlesOn = true;
15413
+ /** Plot values beside the legend titles shown — held here so a remount re-applies it. */
15414
+ this.indicatorValuesOn = true;
14066
15415
  /** Host-contributed legend actions — held here so a rebuild of the legend re-wires them. */
14067
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;
14068
15419
  // ── keyboard navigation / accessibility (item 11) ──
14069
15420
  this.keyboard = null;
14070
15421
  this.keyboardEnabled = true;
@@ -14174,6 +15525,8 @@ var NativeRenderer = class {
14174
15525
  // Distance (px) from the plot's right edge — clears the FULL scale gutter, which widens when a
14175
15526
  // pane carries merged (own-scale) columns; the constant only clears a single-column scale.
14176
15527
  this.scrollBtnRightPx = SCROLL_BTN_RIGHT;
15528
+ // ── chrome size class (pushed by the host shell; see IChartRenderer.setLayoutMode) ──
15529
+ this.layoutMode = "desktop";
14177
15530
  this.viewportCbs = /* @__PURE__ */ new Set();
14178
15531
  this.crosshairCbs = /* @__PURE__ */ new Set();
14179
15532
  this.chartTypeSettingsCbs = /* @__PURE__ */ new Set();
@@ -14184,6 +15537,7 @@ var NativeRenderer = class {
14184
15537
  this.factoryConfig = null;
14185
15538
  this.hostSettingsSections = [];
14186
15539
  this.clickCbs = /* @__PURE__ */ new Set();
15540
+ this.axisLongPressCbs = /* @__PURE__ */ new Set();
14187
15541
  this.inputChangeCbs = /* @__PURE__ */ new Set();
14188
15542
  this.removeIndicatorCbs = /* @__PURE__ */ new Set();
14189
15543
  this.toggleVisibleCbs = /* @__PURE__ */ new Set();
@@ -14191,7 +15545,7 @@ var NativeRenderer = class {
14191
15545
  this.priceStyleCbs = /* @__PURE__ */ new Set();
14192
15546
  this.tableOverlays = /* @__PURE__ */ new Map();
14193
15547
  this.name = "native";
14194
- 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"];
14195
15549
  /** Track cursor proximity to the scroll button on the plot (bubbles from the button too,
14196
15550
  * so moving onto the button doesn't count as leaving). */
14197
15551
  this.onScrollProximityMove = (e) => {
@@ -14230,6 +15584,7 @@ var NativeRenderer = class {
14230
15584
  this.candleDown = opts.downColor;
14231
15585
  this.scene.priceStyle = opts.priceStyle;
14232
15586
  this.scene.basePainting = basePaintingOf(opts.priceStyle);
15587
+ this.scene.candleOverride = candleOverrideFor(opts.priceStyle, this.scene.style.chartTypes);
14233
15588
  }
14234
15589
  this.theme = this.deriveTheme(DARK_THEME);
14235
15590
  }
@@ -14363,6 +15718,11 @@ var NativeRenderer = class {
14363
15718
  this.inputsUI?.setTitlesVisible(this.indicatorTitlesOn);
14364
15719
  return;
14365
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
14366
15726
  case "attribution":
14367
15727
  this.attributionHtml = typeof value === "string" && value.trim() ? value : null;
14368
15728
  this.attributionEnabled = this.attributionHtml !== null || Boolean(value);
@@ -14451,6 +15811,8 @@ var NativeRenderer = class {
14451
15811
  return this.settingsEnabled;
14452
15812
  case "indicatorTitles":
14453
15813
  return this.indicatorTitlesOn;
15814
+ case "indicatorValues":
15815
+ return this.indicatorValuesOn;
14454
15816
  case "attribution":
14455
15817
  return this.attributionHtml ?? this.attributionEnabled;
14456
15818
  case "dialogHost":
@@ -14707,6 +16069,7 @@ var NativeRenderer = class {
14707
16069
  baselineLevel: next.baseline.baselineLevel
14708
16070
  };
14709
16071
  this.setPriceStyle(next.series.style);
16072
+ this.scene.candleOverride = candleOverrideFor(this.scene.priceStyle, s.chartTypes);
14710
16073
  this.scene.baselineValue = next.series.baseline;
14711
16074
  this.coords.setPitchScale(next.series.spacing);
14712
16075
  this.coords.setViewport(this.clampViewport(this.coords.getViewport().barSpacing, this.coords.getViewport().rightOffset));
@@ -14786,7 +16149,10 @@ var NativeRenderer = class {
14786
16149
  /** Port surface: hosts (bottom-bar / chrome buttons) open the same dialog as the
14787
16150
  * in-chart gear — created on demand, independent of the gear feature being enabled. */
14788
16151
  openSettingsDialog(section) {
14789
- 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
+ }
14790
16156
  this.toggleSettingsDialog(section);
14791
16157
  }
14792
16158
  /** (Re)aim the dialog's Canvas → Theme row: the current selection reflects the STABLE
@@ -14812,7 +16178,7 @@ var NativeRenderer = class {
14812
16178
  (patch) => this.applyConfig(patch),
14813
16179
  (json) => this.applyConfig(json),
14814
16180
  () => {
14815
- if (this.factoryConfig) this.applyConfig(this.factoryConfig);
16181
+ if (this.factoryConfig) this.applyConfig(factoryResetConfig(this.factoryConfig));
14816
16182
  this.settingsDialog?.close();
14817
16183
  this.openSettingsDialog();
14818
16184
  },
@@ -14875,11 +16241,18 @@ var NativeRenderer = class {
14875
16241
  this.scrollButton.style.color = this.theme.textColor;
14876
16242
  this.scrollButton.style.borderColor = this.theme.borderColor;
14877
16243
  }
14878
- /** 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. */
14879
16248
  updateScrollToRealtimeButton() {
14880
16249
  const btn = this.scrollButton;
14881
16250
  if (!btn) return;
14882
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
+ }
14883
16256
  btn.style.display = latestOffScreen && this.pointerNearScrollBtn ? "flex" : "none";
14884
16257
  }
14885
16258
  /** Glide the view back to the most recent bars, keeping the current zoom (barSpacing). */
@@ -14901,17 +16274,20 @@ var NativeRenderer = class {
14901
16274
  this.animator.start();
14902
16275
  }
14903
16276
  /**
14904
- * Export the current chart as a PNG data URL by compositing the geometry (L0)
14905
- * + chrome (L1) + user-drawings (L1.5) canvases onto an offscreen canvas. The
14906
- * background is filled first (the canvas2d data layer is transparent — its bg
14907
- * lives on the wrapper), and a fresh synchronous paint runs first so the WebGL2
14908
- * backend's (non-preserved) drawing buffer is populated before it's read back
14909
- * this tick the same paint also repaints the drawings layers, so they're
14910
- * current (interleaved drawings are already inside the data composite). They're
14911
- * drawn bottom-up in DOM stacking order so the front user drawings (trend lines,
14912
- * boxes, etc.) sit above the Pine drawings on the chrome layer, matching what's
14913
- * on screen. The crosshair (L2) and DOM overlays (tables, legend) are
14914
- * 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.
14915
16291
  */
14916
16292
  screenshot() {
14917
16293
  if (!this.dataCanvas) return null;
@@ -14924,9 +16300,25 @@ var NativeRenderer = class {
14924
16300
  if (!ctx) return null;
14925
16301
  ctx.fillStyle = this.theme.background;
14926
16302
  ctx.fillRect(0, 0, out.width, out.height);
14927
- ctx.drawImage(this.dataCanvas, 0, 0);
14928
- ctx.drawImage(this.chromeCanvas, 0, 0);
14929
- 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
+ }
14930
16322
  return out.toDataURL("image/png");
14931
16323
  }
14932
16324
  /**
@@ -15013,10 +16405,16 @@ var NativeRenderer = class {
15013
16405
  borderColor: isDarkColor(this.surfaceBackground) ? DARK_THEME.borderColor : LIGHT_THEME.borderColor
15014
16406
  };
15015
16407
  }
16408
+ /** The colors the axis-scale gutters (price + time) paint with: the LIVE chart background
16409
+ * (`layout.background`) and its contrast-corrected text, so the scales read as part of the
16410
+ * plot. Only the toolbar/dialog chrome stays on the stable app-theme surface. */
16411
+ axisSurface() {
16412
+ return { background: this.theme.background, textColor: this.theme.textColor };
16413
+ }
15016
16414
  // ── lifecycle ──
15017
16415
  mount(container, theme) {
15018
16416
  this.mountContainer = container;
15019
- this.publishToolbarGutter();
16417
+ this.publishGutters();
15020
16418
  this.theme = this.deriveTheme(theme);
15021
16419
  this.surfaceBackground = theme.background;
15022
16420
  this.surfaceTextColor = theme.textColor;
@@ -15071,6 +16469,9 @@ var NativeRenderer = class {
15071
16469
  fling: (v) => this.fling(v),
15072
16470
  onPointerMove: (x, y) => this.handlePointerMove(x, y),
15073
16471
  onClick: (x) => this.handleClick(x),
16472
+ onAxisLongPress: (axis, x, y) => {
16473
+ for (const cb of this.axisLongPressCbs) cb({ axis, x, y });
16474
+ },
15074
16475
  beginPriceScale: (x, y) => this.beginPriceScale(x, y),
15075
16476
  priceScaleBy: (dy) => this.priceScaleBy(dy),
15076
16477
  beginPricePan: (y) => this.beginPricePan(y),
@@ -15088,7 +16489,7 @@ var NativeRenderer = class {
15088
16489
  drawingsDeleteAt: (x, y) => this.userDrawings?.deleteAt(x, y) ?? false,
15089
16490
  drawingsSnapMode: () => this.snapMode,
15090
16491
  drawingsPointerDown: (x, y, snap, shift) => this.userDrawings?.pointerDown(x, y, snap, shift),
15091
- drawingsPointerMove: (x, y, snap) => this.userDrawings?.pointerMove(x, y, snap),
16492
+ drawingsPointerMove: (x, y, snap, shift) => this.userDrawings?.pointerMove(x, y, snap, shift),
15092
16493
  drawingsPointerUp: (x, y) => this.userDrawings?.pointerUp(x, y),
15093
16494
  drawingsCursor: (x, y) => this.userDrawings?.cursorAt(x, y) ?? null,
15094
16495
  drawingsDblClick: (x, y) => this.userDrawings?.dblClick(x, y) ?? false,
@@ -15117,6 +16518,14 @@ var NativeRenderer = class {
15117
16518
  setToolbarGutter: (px) => this.setToolbarGutter(px)
15118
16519
  });
15119
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
+ }
15120
16529
  this.keyboard = new KeyboardController({
15121
16530
  panByBars: (bars) => this.panByBars(bars),
15122
16531
  zoomByStep: (dir) => this.zoomByStep(dir),
@@ -15129,10 +16538,13 @@ var NativeRenderer = class {
15129
16538
  });
15130
16539
  this.setKeyboardEnabled(this.keyboardEnabled);
15131
16540
  this.inputsUI = new InputsUI(this.plot, theme, (paneId) => this.paneBoundsFor(paneId));
16541
+ this.inputsUI.setLayoutMode(this.layoutMode);
15132
16542
  this.inputsUI.setTitlesVisible(this.indicatorTitlesOn);
16543
+ this.inputsUI.setValuesVisible(this.indicatorValuesOn);
15133
16544
  this.inputsUI.setDialogHost(this.dialogHost);
15134
16545
  this.inputsUI.setSymbolPicker(this.symbolPicker);
15135
16546
  this.inputsUI.setLegendActions(this.legendActionsProvider);
16547
+ this.inputsUI.setLegendOverviewAction(this.legendOverviewAction);
15136
16548
  this.inputsUI.setOnChange((c) => {
15137
16549
  for (const cb of this.inputChangeCbs) cb({ indicatorId: c.indicatorId, key: c.key, value: c.value });
15138
16550
  });
@@ -15325,6 +16737,7 @@ var NativeRenderer = class {
15325
16737
  this.attributionEl?.remove();
15326
16738
  this.attributionEl = null;
15327
16739
  this.mountContainer?.style.removeProperty("--vela-toolbar-gutter");
16740
+ this.mountContainer?.style.removeProperty("--vela-scale-gutter");
15328
16741
  this.mountContainer = null;
15329
16742
  this.wrapper?.remove();
15330
16743
  }
@@ -15493,6 +16906,7 @@ var NativeRenderer = class {
15493
16906
  const w = RIGHT_AXIS_W + AXIS_COL_W * this.maxOwnScaleColumns();
15494
16907
  if (w === this.rightAxisW) return false;
15495
16908
  this.rightAxisW = w;
16909
+ this.publishGutters();
15496
16910
  if (this.coords.width > 0) this.syncSize();
15497
16911
  return true;
15498
16912
  }
@@ -15545,7 +16959,10 @@ var NativeRenderer = class {
15545
16959
  this.scene.indicators.set(model.id, model);
15546
16960
  this.refreshAnchorOffset(model);
15547
16961
  this.scene.assignIndicatorZ(model.id);
15548
- 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
+ });
15549
16966
  this.syncTables(model);
15550
16967
  if (model.native?.type === "volume") {
15551
16968
  this.volumeActive = true;
@@ -15606,6 +17023,13 @@ var NativeRenderer = class {
15606
17023
  this.legendActionsProvider = provider;
15607
17024
  this.inputsUI?.setLegendActions(provider);
15608
17025
  }
17026
+ setLegendOverviewAction(action) {
17027
+ this.legendOverviewAction = action;
17028
+ this.inputsUI?.setLegendOverviewAction(action);
17029
+ }
17030
+ openIndicatorSettings(indicatorId) {
17031
+ this.inputsUI?.openSettingsFor(indicatorId);
17032
+ }
15609
17033
  /**
15610
17034
  * Hide/show a mounted indicator. Hiding drops its model from the scene (so every paint path —
15611
17035
  * series, fills, drawings, tables, glow, data window — skips it) while keeping its z key and its
@@ -15652,12 +17076,31 @@ var NativeRenderer = class {
15652
17076
  if (style === this.scene.priceStyle) return;
15653
17077
  this.scene.priceStyle = style;
15654
17078
  this.scene.basePainting = basePaintingOf(style);
17079
+ this.scene.candleOverride = candleOverrideFor(style, this.scene.style.chartTypes);
15655
17080
  for (const cb of this.priceStyleCbs) cb(style);
15656
17081
  }
15657
17082
  onRemoveIndicator(cb) {
15658
17083
  this.removeIndicatorCbs.add(cb);
15659
17084
  return () => this.removeIndicatorCbs.delete(cb);
15660
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
+ }
15661
17104
  setSettingsSections(sections) {
15662
17105
  this.hostSettingsSections = sections;
15663
17106
  this.settingsDialog?.setHostSections(sections);
@@ -15682,6 +17125,10 @@ var NativeRenderer = class {
15682
17125
  this.clickCbs.add(cb);
15683
17126
  return () => this.clickCbs.delete(cb);
15684
17127
  }
17128
+ onAxisLongPress(cb) {
17129
+ this.axisLongPressCbs.add(cb);
17130
+ return () => this.axisLongPressCbs.delete(cb);
17131
+ }
15685
17132
  onViewportChange(cb) {
15686
17133
  this.viewportCbs.add(cb);
15687
17134
  return () => this.viewportCbs.delete(cb);
@@ -15813,13 +17260,17 @@ var NativeRenderer = class {
15813
17260
  active = true;
15814
17261
  }
15815
17262
  }
15816
- this.coords.setViewport(this.clampViewport(barSpacing, rightOffset));
17263
+ const clamped = this.clampViewport(barSpacing, rightOffset);
17264
+ if (clamped.barSpacing !== barSpacing) this.targetBarSpacing = clamped.barSpacing;
17265
+ if (this.scrollTargetRO != null && clamped.rightOffset !== rightOffset) this.scrollTargetRO = clamped.rightOffset;
17266
+ this.coords.setViewport(clamped);
15817
17267
  this.computeScales();
15818
17268
  if (this.easeScales(dtMs)) active = true;
15819
17269
  if (this.easeLiveBar(dtMs)) active = true;
15820
17270
  this.skeletonClockMs += dtMs;
15821
17271
  this.paintData();
15822
17272
  this.crosshairLayer.render(this.scene, this.coords, this.theme, this.hoverSeparatorY, this.externalCrossPx());
17273
+ this.updateLegendValues();
15823
17274
  this.emitViewportChange();
15824
17275
  return active;
15825
17276
  }
@@ -15884,7 +17335,7 @@ var NativeRenderer = class {
15884
17335
  this.lastPointer = null;
15885
17336
  this.scheduler.invalidate(1 /* Cursor */);
15886
17337
  this.hoverLogical = null;
15887
- 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 };
15888
17339
  for (const cb of this.crosshairCbs) cb(empty);
15889
17340
  return;
15890
17341
  }
@@ -15896,7 +17347,7 @@ var NativeRenderer = class {
15896
17347
  const logical = Math.round(this.coords.xToLogical(x));
15897
17348
  const onBar = logical >= 0 && logical < this.coords.barCount;
15898
17349
  const time = onBar ? this.coords.logicalToTime(logical) : null;
15899
- const pane = this.paneAtY(y);
17350
+ const pane = this.paneNodeAtY(y);
15900
17351
  const price = inData && pane ? this.coords.yToPrice(y, pane.scale, pane.bounds) : null;
15901
17352
  const values = /* @__PURE__ */ new Map();
15902
17353
  if (onBar) {
@@ -15913,7 +17364,7 @@ var NativeRenderer = class {
15913
17364
  const bar = onBar ? this.bars[logical] : void 0;
15914
17365
  const ohlc = bar ? { time: bar.time, open: bar.open, high: bar.high, low: bar.low, close: bar.close, volume: bar.volume } : null;
15915
17366
  this.hoverLogical = onBar ? logical : null;
15916
- const event = { time, price, values, ohlc };
17367
+ const event = { time, price, paneKind: inData && pane ? pane.kind : null, values, ohlc };
15917
17368
  for (const cb of this.crosshairCbs) cb(event);
15918
17369
  }
15919
17370
  handleClick(x) {
@@ -15932,6 +17383,20 @@ var NativeRenderer = class {
15932
17383
  }
15933
17384
  return null;
15934
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
+ }
15935
17400
  // ── manual vertical scaling (price-axis drag + vertical price pan) ──
15936
17401
  /** The scale window a gesture targets: a pane's master scale, or a merged indicator's
15937
17402
  * own scale column (resolved from the grabbed x). Both share the four scale fields. */
@@ -15994,7 +17459,7 @@ var NativeRenderer = class {
15994
17459
  * still fits the view.)
15995
17460
  */
15996
17461
  dataDblClick(_x, y) {
15997
- const pane = this.paneNodeAtY(y);
17462
+ const pane = this.paneNodeAtY(y) ?? this.nearestPaneToY(y);
15998
17463
  if (!pane) return;
15999
17464
  const maximized = this.maximizedPaneId !== pane.id;
16000
17465
  this.maximizedPaneId = maximized ? pane.id : null;
@@ -16235,29 +17700,63 @@ var NativeRenderer = class {
16235
17700
  dataWindowGroups(idx, pricePane) {
16236
17701
  const groups = [];
16237
17702
  for (const model of this.scene.indicators.values()) {
16238
- const pane = (model.paneId ? this.scene.panes.get(model.paneId) : null) ?? pricePane;
16239
- const off = this.scene.offsetOf(model.id);
16240
- const rows = [];
16241
- for (const s of model.series) {
16242
- let value;
16243
- let color;
16244
- if (s.kind === "candle" || s.kind === "bar") {
16245
- value = s.bars[idx - off]?.close;
16246
- color = s.style?.up ?? this.theme.upColor;
16247
- } else if (isLineLikeSeries(s)) {
16248
- if (s.visible === false) continue;
16249
- value = s.points[idx - off]?.value;
16250
- color = s.points[idx - off]?.color ?? s.style.color;
16251
- } else {
16252
- continue;
16253
- }
16254
- if (value == null || !Number.isFinite(value)) continue;
16255
- rows.push({ label: s.title || model.title, value: this.dataWindowFmt(value, pane), color });
16256
- }
17703
+ const rows = this.dataWindowRowsFor(model, idx, pricePane);
16257
17704
  if (rows.length) groups.push({ name: model.title, rows });
16258
17705
  }
16259
17706
  return groups;
16260
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
+ }
16261
17760
  renderFrame(level) {
16262
17761
  if (this.animator.active) return;
16263
17762
  if (repaintsData(level)) {
@@ -16265,9 +17764,38 @@ var NativeRenderer = class {
16265
17764
  this.paintData();
16266
17765
  } else if (repaintsChrome(level) && this.paintedData) {
16267
17766
  this.chrome.prepare(this.scene, this.coords, this.theme);
16268
- this.chrome.render(this.scene, this.coords, this.theme, { background: this.surfaceBackground, textColor: this.surfaceTextColor });
17767
+ this.chrome.render(this.scene, this.coords, this.theme, this.axisSurface());
16269
17768
  }
16270
17769
  this.crosshairLayer.render(this.scene, this.coords, this.theme, this.hoverSeparatorY, this.externalCrossPx());
17770
+ if (!repaintsData(level) && this.paintedData) this.repaintCursorLayers();
17771
+ this.updateLegendValues();
17772
+ }
17773
+ /** Repaint the SDK layers that opted into cursor tracking (their own canvas only). */
17774
+ repaintCursorLayers() {
17775
+ const pane = this.scene.panes.get(PRICE_PANE_ID);
17776
+ if (!pane) return;
17777
+ const nowMs = typeof performance !== "undefined" ? performance.now() : Date.now();
17778
+ for (const l of this.extLayers) {
17779
+ if (!l.def.repaintOnCursor) continue;
17780
+ l.instance.render(this.extLayerArgs(l.def.id, pane.scale, pane.bounds, nowMs));
17781
+ if (this.animZoom && l.instance.animating?.()) this.animator.start();
17782
+ }
17783
+ }
17784
+ /** One frame's args for an SDK renderer layer (shared by the data + cursor paint paths). */
17785
+ extLayerArgs(id, scale, bounds, nowMs) {
17786
+ return {
17787
+ bars: this.scene.bars,
17788
+ data: this.scene.nativeData.get(id),
17789
+ settings: this.scene.nativeData.get(`${id}-settings`) ?? {},
17790
+ pending: this.scene.nativePending.get(id) ?? [],
17791
+ coords: this.coords,
17792
+ scale,
17793
+ bounds,
17794
+ theme: this.theme,
17795
+ priceStyle: this.scene.priceStyle,
17796
+ nowMs,
17797
+ cursor: this.lastPointer
17798
+ };
16271
17799
  }
16272
17800
  /** Paint the below-data (L-1) + geometry (L0) + chrome (L1) layers from the current scene/coords. */
16273
17801
  paintData() {
@@ -16300,18 +17828,16 @@ var NativeRenderer = class {
16300
17828
  });
16301
17829
  const nowMs = typeof performance !== "undefined" ? performance.now() : Date.now();
16302
17830
  for (const l of this.extLayers) {
16303
- l.instance.render({
16304
- bars: this.scene.bars,
16305
- data: this.scene.nativeData.get(l.def.id),
16306
- settings: this.scene.nativeData.get(`${l.def.id}-settings`) ?? {},
16307
- pending: this.scene.nativePending.get(l.def.id) ?? [],
16308
- coords: this.coords,
16309
- scale: pane.scale,
16310
- bounds: pane.bounds,
16311
- theme: this.theme,
16312
- priceStyle: this.scene.priceStyle,
16313
- nowMs
16314
- });
17831
+ const args = this.extLayerArgs(l.def.id, pane.scale, pane.bounds, nowMs);
17832
+ l.instance.render(args);
17833
+ if (l.def.id === this.scene.priceStyle && l.instance.modulateBase) {
17834
+ const mod = l.instance.modulateBase(args);
17835
+ if (mod) {
17836
+ if (mod.candleBodyScale != null) this.backend.candleBodyScale = clamp012(mod.candleBodyScale) || 0.01;
17837
+ if (mod.candleBodyAlpha != null) this.backend.candleBodyAlpha = clamp012(mod.candleBodyAlpha) * this.candleBodyAlpha;
17838
+ if (mod.gridAlpha != null) this.backend.gridAlpha = clamp012(mod.gridAlpha);
17839
+ }
17840
+ }
16315
17841
  if (this.animZoom && l.instance.animating?.()) this.animator.start();
16316
17842
  }
16317
17843
  }
@@ -16321,7 +17847,7 @@ var NativeRenderer = class {
16321
17847
  if (easeLive && liveActual) this.bars[li] = { ...liveActual, high: this.liveEaseHigh, low: this.liveEaseLow, close: this.liveEaseClose };
16322
17848
  this.scene.drawingSlices = this.userDrawings?.prepareSlices(this.scene.orderedPanes().map((p) => p.id)) ?? /* @__PURE__ */ new Map();
16323
17849
  this.backend.render(this.scene, this.coords, this.theme);
16324
- this.chrome.render(this.scene, this.coords, this.theme, { background: this.surfaceBackground, textColor: this.surfaceTextColor });
17850
+ this.chrome.render(this.scene, this.coords, this.theme, this.axisSurface());
16325
17851
  this.userDrawings?.render();
16326
17852
  if (easeLive && liveActual) this.bars[li] = liveActual;
16327
17853
  this.paintedData = true;
@@ -16391,7 +17917,7 @@ var NativeRenderer = class {
16391
17917
  if (!Number.isFinite(y) || y < pricePane.bounds.top || y > pricePane.bounds.top + pricePane.bounds.height) y = null;
16392
17918
  }
16393
17919
  }
16394
- return { x, y, time: this.coords.logicalToTime(logical) };
17920
+ return { x, y, time: this.coords.logicalToTime(logical), price: y != null ? ext.price : null };
16395
17921
  }
16396
17922
  /** Set the sticky magnet mode (called by the drawings toolbar's 3-state button). */
16397
17923
  setSnapMode(mode) {
@@ -16755,15 +18281,18 @@ var NativeRenderer = class {
16755
18281
  setToolbarGutter(px) {
16756
18282
  if (px === this.toolbarGutter) return;
16757
18283
  this.toolbarGutter = px;
16758
- this.publishToolbarGutter();
18284
+ this.publishGutters();
16759
18285
  this.positionAttribution();
16760
18286
  this.syncSize();
16761
18287
  }
16762
- /** Publish the gutter on the mount container as `--vela-toolbar-gutter`, so host
16763
- * overlays sharing that container (a status line, a custom legend) can anchor to
16764
- * the plot's left edge without reaching into the renderer's DOM. */
16765
- publishToolbarGutter() {
18288
+ /** Publish the gutters on the mount container as `--vela-toolbar-gutter` (left,
18289
+ * drawings toolbar) and `--vela-scale-gutter` (right, the full price-scale width
18290
+ * incl. merged own-scale columns), so host overlays sharing that container (a
18291
+ * status line, a watermark, a custom legend) can anchor to the plot's edges
18292
+ * without reaching into the renderer's DOM. */
18293
+ publishGutters() {
16766
18294
  this.mountContainer?.style.setProperty("--vela-toolbar-gutter", `${this.toolbarGutter}px`);
18295
+ this.mountContainer?.style.setProperty("--vela-scale-gutter", `${this.rightAxisW}px`);
16767
18296
  }
16768
18297
  /** The built-in mark, or the host's own when one is set. */
16769
18298
  buildAttributionEl() {
@@ -16833,7 +18362,13 @@ var NativeRenderer = class {
16833
18362
  this.fitContent();
16834
18363
  this.didInitialFit = true;
16835
18364
  }
16836
- this.scheduler.flushNow(4 /* Full */);
18365
+ if (this.animator.active) {
18366
+ this.computeScales();
18367
+ this.paintData();
18368
+ this.crosshairLayer.render(this.scene, this.coords, this.theme, this.hoverSeparatorY, this.externalCrossPx());
18369
+ } else {
18370
+ this.scheduler.flushNow(4 /* Full */);
18371
+ }
16837
18372
  }
16838
18373
  applyBackground() {
16839
18374
  this.wrapper.style.background = this.theme.background;
@@ -16902,6 +18437,9 @@ function sanitizeHighlights(value) {
16902
18437
  }
16903
18438
  return out.sort((a, b) => a.from - b.from);
16904
18439
  }
18440
+ function clamp012(v) {
18441
+ return Math.max(0, Math.min(1, v));
18442
+ }
16905
18443
  function isPriceStyle2(v) {
16906
18444
  return typeof v === "string" && priceStyleIds().includes(v);
16907
18445
  }
@@ -17149,6 +18687,9 @@ var EngineOrchestrator = class _EngineOrchestrator {
17149
18687
  this.barTransform = null;
17150
18688
  // ── chart-type data engines (SDK): one lazily-created engine per style id ──
17151
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();
17152
18693
  /** The style id whose data engine is currently ACTIVE (drives suspend/resume + pokes). */
17153
18694
  this.activeEngineStyle = null;
17154
18695
  /** The chart's current price style (tracked from the renderer's change events + initial read). */
@@ -17211,7 +18752,10 @@ var EngineOrchestrator = class _EngineOrchestrator {
17211
18752
  this.renderer.onRemoveIndicator((id) => this.removeIndicator(id));
17212
18753
  this.renderer.onToggleIndicatorVisible?.((id, visible) => this.setVisible(id, visible));
17213
18754
  this.paneActionUnsub = this.renderer.onPaneAction?.((a) => this.handlePaneAction(a)) ?? null;
17214
- 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
+ });
17215
18759
  this.renderer.onMoveIndicator?.((id, target) => this.moveIndicator(id, target));
17216
18760
  this.renderer.onThemeSelect?.((name) => this.setTheme(resolveTheme(name)));
17217
18761
  this.viewportUnsub = this.renderer.onViewportChange((range) => this.onViewportChange(range));
@@ -17699,6 +19243,8 @@ var EngineOrchestrator = class _EngineOrchestrator {
17699
19243
  if (!def?.dataEngine) return;
17700
19244
  const engine = def.dataEngine();
17701
19245
  this.typeEngines.set(id, engine);
19246
+ const stored = this.typeSettings.get(id);
19247
+ if (stored) engine.onSettings?.(stored);
17702
19248
  engine.start({
17703
19249
  symbol: this.config.market.symbol ?? "TEST",
17704
19250
  timeframe: this.config.market.timeframe ?? "60",
@@ -18170,6 +19716,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
18170
19716
  return {
18171
19717
  id: record.id,
18172
19718
  title: record.title,
19719
+ ...d.shortTitle ? { shorttitle: d.shortTitle } : {},
18173
19720
  overlay: d.overlay,
18174
19721
  paneHint: d.paneHint,
18175
19722
  native: { type: record.native.type },
@@ -18777,7 +20324,8 @@ var VpvrIndicator = class {
18777
20324
  };
18778
20325
  var vpvrDescriptor = {
18779
20326
  type: "vpvr",
18780
- title: "VPVR",
20327
+ title: "Visible Range Volume Profile",
20328
+ shortTitle: "VRVP",
18781
20329
  paneHint: "price",
18782
20330
  overlay: true,
18783
20331
  inputsSchema: () => [
@@ -19134,4 +20682,4 @@ function resolveElement(container) {
19134
20682
  return element;
19135
20683
  }
19136
20684
 
19137
- 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 };