@luxalgo/vela 0.8.2 → 0.8.4

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 (48) hide show
  1. package/dist/{DataProvider-COGDs9Xg.d.cts → DataProvider-Bw58CjYy.d.cts} +1 -1
  2. package/dist/{DataProvider-DpOWZD8F.d.ts → DataProvider-Byo8pEv0.d.ts} +1 -1
  3. package/dist/{chunk-DPAA3SRT.js → chunk-ALTGHKIM.js} +836 -32
  4. package/dist/{chunk-27UTYE7T.js → chunk-AUOB7A5Y.js} +49 -7
  5. package/dist/{chunk-HKNO24GD.js → chunk-I2U4NZD2.js} +56 -1
  6. package/dist/{chunk-R2JMNIHY.js → chunk-MOJ4T26N.js} +341 -169
  7. package/dist/{chunk-T3DZIR5Y.js → chunk-UNUVPQFW.js} +2214 -886
  8. package/dist/{chunk-NLDTVFOI.js → chunk-WY5H4S4I.js} +129 -10
  9. package/dist/{contributions-Cgm9RJYP.d.cts → contributions-BY3zSYiZ.d.ts} +98 -7
  10. package/dist/{contributions-bWgwyiVi.d.ts → contributions-DOm40g5-.d.cts} +98 -7
  11. package/dist/index.cjs +3066 -893
  12. package/dist/index.d.cts +36 -22
  13. package/dist/index.d.ts +36 -22
  14. package/dist/index.js +5 -5
  15. package/dist/{options-NAUHiQEC.d.ts → options-CSXbjMM5.d.cts} +129 -4
  16. package/dist/{options-NAUHiQEC.d.cts → options-CSXbjMM5.d.ts} +129 -4
  17. package/dist/{plugin-CqxWoDKS.d.ts → plugin-BDxUhGO3.d.ts} +4 -4
  18. package/dist/{plugin-CjjzZX2F.d.cts → plugin-CiQVyETg.d.cts} +4 -4
  19. package/dist/plugin.cjs +573 -17
  20. package/dist/plugin.d.cts +5 -5
  21. package/dist/plugin.d.ts +5 -5
  22. package/dist/plugin.js +3 -3
  23. package/dist/providers/binance.d.cts +2 -2
  24. package/dist/providers/binance.d.ts +2 -2
  25. package/dist/providers/coinbase.d.cts +2 -2
  26. package/dist/providers/coinbase.d.ts +2 -2
  27. package/dist/providers/hyperliquid.d.cts +2 -2
  28. package/dist/providers/hyperliquid.d.ts +2 -2
  29. package/dist/{side-panel-wMNfgyZb.d.cts → side-panel-_2UK1gSm.d.cts} +45 -3
  30. package/dist/{side-panel-wMNfgyZb.d.ts → side-panel-_2UK1gSm.d.ts} +45 -3
  31. package/dist/{statusline-model-Dih6FGWP.d.ts → statusline-model-C9tYXuux.d.ts} +19 -4
  32. package/dist/{statusline-model-Dpfpq54A.d.cts → statusline-model-DUBX6Fib.d.cts} +19 -4
  33. package/dist/ui.cjs +224 -14
  34. package/dist/ui.d.cts +119 -87
  35. package/dist/ui.d.ts +119 -87
  36. package/dist/ui.js +3 -3
  37. package/dist/vela.global.js +3073 -900
  38. package/dist/vela.global.min.js +158 -54
  39. package/dist/widget.cjs +3640 -1119
  40. package/dist/widget.d.cts +24 -10
  41. package/dist/widget.d.ts +24 -10
  42. package/dist/widget.js +7 -7
  43. package/dist/workspace.cjs +3639 -1119
  44. package/dist/workspace.d.cts +61 -20
  45. package/dist/workspace.d.ts +61 -20
  46. package/dist/workspace.js +6 -6
  47. package/package.json +1 -1
  48. /package/dist/{chunk-JU4CWEBF.js → chunk-W5YPYZ2P.js} +0 -0
@@ -1,6 +1,6 @@
1
- import { deserializeDrawing, chartTypes, rendererLayers, isLineLikeSeries, seriesShownOn, foldBaseModulation, registerChartType, rendererDefaults, getDrawingType, settingsRowVisible, normalizeSettingsRow, Magnifier, TextLabel, Callout, resetDrawingSettings, registerNativeIndicator, chartType, getNativeIndicator, nativeIndicatorDescriptors, nativeInstanceChannel, drawingTypes, settingsRowValueKeys, formatDuration, RegressionChannel, FixedRangeVolumeProfile, DEFAULT_DRAWING_COLOR, SegmentDrawing, ArrowMark, GlyphStamp, RadialFib, FibSpiral, DedekindTessellation, MachFigure, GannSquare, FibRatios, MeasureBox, PositionTool, AnchoredVwap, PatternDrawing, Comment, PriceNote, Signpost, Note, PriceLabel, GANN_SQUARE_ARCS, magnifierTimeframeLabel, lineSegmentIntersection, MAGNIFIER_TIMEFRAME_OPTIONS, GLYPH_OPTIONS, STAMP_SIZE_OPTIONS, LINE_STYLE_OPTIONS, DEDEKIND_CURVATURE_OPTIONS, MACH_NUMBER_OPTIONS, MACH_WAVE_COUNT_OPTIONS, TEXT_SIZE_OPTIONS, createDrawing, inputVisible, seriesInScale, tickerModifierIds, CalloutBase, DIRECTION_OPTIONS, FibRetracement, stableSeriesId } from './chunk-DPAA3SRT.js';
2
- import { themeTokens, Dialog, closeOpenPopovers, closeWidthPopover, fieldSection, buildFieldControl, fieldSeparator, fieldGrid, fieldRow, CALLOUT_STYLE_ID, CALLOUT_CSS, Popover, blendOver, splitColor, CalloutBubble, Menu, TextArea, NumberInput, buildColorPicker, isPopoverOpen, DatePicker, eventDismissedPopover, toggleSelectList, normalizeDateInput, openPopoverTrigger, fieldGridColumns, FIELD_GAP_PX, STATIC_TOKENS } from './chunk-NLDTVFOI.js';
3
- import { icon, iconAt, withAlpha, WARNING, ACCENT, NEUTRAL, INFO, BULLISH, BEARISH, announceSurface, isDarkColor, injectStyles, SERIES_LINE, CHIP_PLATE, CROSSHAIR, TRADE_EXIT, TRADE_SHORT, TRADE_LONG, iconMarkup, overlayScrollbarCss, SLATE_DEEP, VALID, INVALID, SLATE, FIELD_FOCUS_CSS, FIELD_FOCUS_RING, svg24, svg24Solid } from './chunk-HKNO24GD.js';
1
+ import { sanitizeShowOn, deserializeDrawing, chartTypes, rendererLayers, isLineLikeSeries, seriesShownOn, foldBaseModulation, registerChartType, rendererDefaults, getDrawingType, normalizeTimezone, settingsRowVisible, normalizeSettingsRow, Magnifier, TextLabel, Callout, shownOnTimeframe, resetDrawingSettings, registerNativeIndicator, chartType, getNativeIndicator, nativeIndicatorDescriptors, nativeInstanceChannel, drawingTypes, settingsRowValueKeys, TIMEZONES, tzMenuLabel, formatDuration, RegressionChannel, FixedRangeVolumeProfile, DEFAULT_DRAWING_COLOR, SegmentDrawing, ArrowMark, GlyphStamp, RadialFib, FibSpiral, DedekindTessellation, MachFigure, GannSquare, FibRatios, MeasureBox, PositionTool, AnchoredVwap, PriceActionZone, Liquidity, PatternDrawing, Comment, PriceNote, Signpost, Note, PriceLabel, TrendLine, GANN_SQUARE_ARCS, magnifierTimeframeLabel, lineSegmentIntersection, GLYPH_OPTIONS, STAMP_SIZE_OPTIONS, magnifierTimeframesBelow, LINE_STYLE_OPTIONS, DEDEKIND_CURVATURE_OPTIONS, MACH_NUMBER_OPTIONS, MACH_WAVE_COUNT_OPTIONS, TEXT_SIZE_OPTIONS, createDrawing, defaultText, inputVisible, seriesInScale, tickerModifierIds, CalloutBase, FibRetracement, FIB_PRESETS, matchFibPreset, FibLevels, timeframeBandOf, TIMEFRAME_BANDS, stableSeriesId, DIRECTION_OPTIONS } from './chunk-ALTGHKIM.js';
2
+ import { themeTokens, Dialog, closeOpenPopovers, closeWidthPopover, fieldSection, buildFieldControl, fieldSeparator, fieldGrid, fieldRow, CALLOUT_STYLE_ID, CALLOUT_CSS, Popover, blendOver, splitColor, CalloutBubble, Menu, TextArea, NumberInput, buildColorPicker, isPopoverOpen, DatePicker, eventDismissedPopover, toggleSelectList, normalizeDateInput, openPopoverTrigger, fieldGridColumns, FIELD_GAP_PX, STATIC_TOKENS } from './chunk-WY5H4S4I.js';
3
+ import { icon, iconAt, withAlpha, WARNING, ACCENT, NEUTRAL, INFO, BULLISH, BEARISH, announceSurface, holdForExit, isDarkColor, injectStyles, SERIES_LINE, CHIP_PLATE, CROSSHAIR, TRADE_EXIT, TRADE_SHORT, TRADE_LONG, iconMarkup, overlayScrollbarCss, toHex6, SLATE_DEEP, VALID, INVALID, SLATE, FIELD_FOCUS_CSS, FIELD_FOCUS_RING, svg24, svg24Solid } from './chunk-I2U4NZD2.js';
4
4
 
5
5
  // src/core/events/EventBus.ts
6
6
  var TypedEventBus = class {
@@ -57,6 +57,7 @@ var FIBONACCI_TYPES = [
57
57
  "fibwedge",
58
58
  "fibspiral"
59
59
  ];
60
+ var PRICE_ACTION_TYPES = ["fairvaluegap", "orderblock", "liquidity"];
60
61
  var GANN_TYPES = ["gannfan", "gannbox", "gannsquare"];
61
62
  var GEOMETRY_TYPES = ["dedekind", "sonic", "supersonic", "goldensonic", "goldensupersonic"];
62
63
  var PATTERN_TYPES = ["xabcd", "abcd", "headshoulders"];
@@ -84,6 +85,7 @@ var TOOLBAR_LAYOUT = [
84
85
  label: "Fibonacci",
85
86
  sections: [
86
87
  { label: "Fibonacci", types: FIBONACCI_TYPES },
88
+ { label: "Price Action", types: PRICE_ACTION_TYPES },
87
89
  { label: "Gann", types: GANN_TYPES },
88
90
  { label: "Geometry", types: GEOMETRY_TYPES }
89
91
  ]
@@ -261,6 +263,7 @@ var DrawingStore = class {
261
263
  if (patch.visible !== void 0) d.visible = patch.visible;
262
264
  if (patch.zIndex !== void 0) d.zIndex = patch.zIndex;
263
265
  if (patch.props !== void 0) d.applyProps(patch.props);
266
+ if ("showOn" in patch) d.showOn = sanitizeShowOn(patch.showOn);
264
267
  this.emit();
265
268
  return d;
266
269
  }
@@ -1151,38 +1154,11 @@ function attachForming(closed, forming) {
1151
1154
  }
1152
1155
 
1153
1156
  // src/data/MultiProviderFeed.ts
1154
- function intervalMs(timeframe) {
1155
- const map2 = {
1156
- "1m": 6e4,
1157
- "3m": 18e4,
1158
- "5m": 3e5,
1159
- "15m": 9e5,
1160
- "30m": 18e5,
1161
- "1h": 36e5,
1162
- "2h": 72e5,
1163
- "4h": 144e5,
1164
- "1d": 864e5,
1165
- "1w": 6048e5,
1166
- "1M": 2592e6,
1167
- "1": 6e4,
1168
- "5": 3e5,
1169
- "15": 9e5,
1170
- "30": 18e5,
1171
- "60": 36e5,
1172
- "240": 144e5,
1173
- D: 864e5,
1174
- W: 6048e5,
1175
- M: 2592e6
1176
- };
1177
- return map2[timeframe] ?? 36e5;
1178
- }
1179
1157
  var MultiProviderFeed = class {
1180
1158
  constructor(store = sharedBarStore) {
1181
1159
  this.registry = new ProviderRegistry();
1182
1160
  /** The chart symbol's resolved provider — the default for bare secondary symbols. */
1183
1161
  this.primaryProvider = null;
1184
- /** A copy of offline history, used to synthesize ticks for the `data` path. */
1185
- this.liveBars = [];
1186
1162
  /** Sync-accessible symbol metadata, warmed by load()/symbolInfoFor (the engine reads it synchronously). */
1187
1163
  this.symInfoCache = /* @__PURE__ */ new Map();
1188
1164
  /** Requests in flight, keyed like the cache — every concurrent asker shares one round trip. */
@@ -1292,10 +1268,7 @@ var MultiProviderFeed = class {
1292
1268
  }
1293
1269
  // ── MarketDataFeed ─────────────────────────────────────────────────────
1294
1270
  async load(cfg) {
1295
- if (cfg.data && cfg.data.length > 0) {
1296
- this.liveBars = cfg.data.map((b) => ({ ...b }));
1297
- return cfg.data;
1298
- }
1271
+ if (cfg.data && cfg.data.length > 0) return cfg.data;
1299
1272
  const resolved = await this.registry.whenResolvable(rawSymbol(cfg), { default: this.primaryProvider });
1300
1273
  this.primaryProvider = resolved.provider;
1301
1274
  this.prefetchSymbolInfo(resolved);
@@ -1303,10 +1276,7 @@ var MultiProviderFeed = class {
1303
1276
  }
1304
1277
  /** Progressive twin of {@link load} — same resolution, the cache streams the batches. */
1305
1278
  async loadProgressive(cfg, onBatch, opts) {
1306
- if (cfg.data && cfg.data.length > 0) {
1307
- this.liveBars = cfg.data.map((b) => ({ ...b }));
1308
- return cfg.data;
1309
- }
1279
+ if (cfg.data && cfg.data.length > 0) return cfg.data;
1310
1280
  const resolved = await this.registry.whenResolvable(rawSymbol(cfg), { default: this.primaryProvider });
1311
1281
  this.primaryProvider = resolved.provider;
1312
1282
  this.prefetchSymbolInfo(resolved);
@@ -1332,39 +1302,16 @@ var MultiProviderFeed = class {
1332
1302
  if (!resolved) return [];
1333
1303
  return this.cache.loadRange(canonical(cfg, resolved), range);
1334
1304
  }
1305
+ /** Offline `data` has no source of ticks: the caller's bars are shown exactly as given,
1306
+ * and live updates need a registered provider that implements `subscribe`. */
1335
1307
  subscribe(cfg, onBar) {
1336
- if (cfg.data && cfg.data.length > 0) return this.synthesizeTicks(cfg, onBar);
1308
+ if (cfg.data && cfg.data.length > 0) return () => {
1309
+ };
1337
1310
  const resolved = this.registry.resolve(rawSymbol(cfg), { default: this.primaryProvider });
1338
1311
  if (!resolved) return () => {
1339
1312
  };
1340
1313
  return this.cache.subscribe(canonical(cfg, resolved), onBar);
1341
1314
  }
1342
- /**
1343
- * Synthesize price-relative ticks for offline `data` runs (no network), so the
1344
- * forming-candle path is exercised at any price scale. Mirrors the bundled
1345
- * provider feed's offline behavior.
1346
- */
1347
- synthesizeTicks(cfg, onBar) {
1348
- if (this.liveBars.length === 0) return () => {
1349
- };
1350
- const step = intervalMs(cfg.timeframe ?? "60");
1351
- const timer = setInterval(() => {
1352
- const bars = this.liveBars;
1353
- const forming = bars[bars.length - 1];
1354
- if (!forming) return;
1355
- const jitter = forming.close * 15e-4 * (Math.random() - 0.5) * 2;
1356
- const close = Math.max(0, forming.close + jitter);
1357
- const updated = { ...forming, close, high: Math.max(forming.high, close), low: Math.min(forming.low, close) };
1358
- bars[bars.length - 1] = updated;
1359
- onBar(updated);
1360
- if (Math.random() < 0.12) {
1361
- const next = { time: updated.time + step, open: updated.close, high: updated.close, low: updated.close, close: updated.close, volume: 0 };
1362
- bars.push(next);
1363
- onBar(next);
1364
- }
1365
- }, 1e3);
1366
- return () => clearInterval(timer);
1367
- }
1368
1315
  };
1369
1316
  function rawSymbol(cfg) {
1370
1317
  return cfg.symbol ?? "";
@@ -1716,6 +1663,34 @@ var DrawingsControl = class {
1716
1663
  if (this.ok("setFavorites")) this.ctrl.setFavorites([...types]);
1717
1664
  return this;
1718
1665
  }
1666
+ /** Every tool's remembered settings, by type — what the next drawing of each type starts
1667
+ * from (its style, text styling and per-type settings such as fib levels). Plain JSON:
1668
+ * persist it and hand it back to {@link setToolDefaults} on the next load. */
1669
+ toolDefaults() {
1670
+ return this.ctrl.toolDefaults();
1671
+ }
1672
+ /** Replace every tool's remembered settings (e.g. restoring persisted prefs). Malformed
1673
+ * entries and unknown types are dropped. Follow changes on `drawing:defaults`. */
1674
+ setToolDefaults(map2) {
1675
+ this.ctrl.setToolDefaults(map2);
1676
+ return this;
1677
+ }
1678
+ /** Forget one tool's remembered settings (every tool's without an argument). */
1679
+ resetToolDefaults(type) {
1680
+ this.ctrl.resetToolDefaults(type);
1681
+ return this;
1682
+ }
1683
+ /** Every tool's saved looks ("styles" in the settings panel), by type. Plain JSON —
1684
+ * persist it and hand it back to {@link setToolTemplates}. */
1685
+ toolTemplates() {
1686
+ return this.ctrl.toolTemplates();
1687
+ }
1688
+ /** Replace every tool's saved looks (e.g. restoring persisted prefs). Follow changes on
1689
+ * `drawing:templates`. */
1690
+ setToolTemplates(map2) {
1691
+ this.ctrl.setToolTemplates(map2);
1692
+ return this;
1693
+ }
1719
1694
  /** Aliases mirroring `renderer.getConfig()/applyConfig()` for symmetry. */
1720
1695
  getConfig() {
1721
1696
  return this.ctrl.toJSON();
@@ -1812,8 +1787,8 @@ var ReplayControl = class {
1812
1787
  }
1813
1788
  /** Reveal one bar (one tick, with {@link setTicks}) every `intervalMs` (default: the last
1814
1789
  * interval used, else 1000). Calling it again while playing changes the pace. */
1815
- play(intervalMs2) {
1816
- this.ctrl.replayPlay(intervalMs2);
1790
+ play(intervalMs) {
1791
+ this.ctrl.replayPlay(intervalMs);
1817
1792
  return this;
1818
1793
  }
1819
1794
  pause() {
@@ -1885,98 +1860,72 @@ var SecondClock = class _SecondClock {
1885
1860
  }
1886
1861
  };
1887
1862
 
1888
- // src/core/timezones.ts
1889
- var TIMEZONES = [
1890
- { value: "Etc/UTC", label: "UTC" },
1891
- { value: "Etc/GMT+12", label: "International Date Line West" },
1892
- { value: "Pacific/Pago_Pago", label: "Pago Pago" },
1893
- { value: "Pacific/Honolulu", label: "Honolulu" },
1894
- { value: "Pacific/Marquesas", label: "Marquesas Islands" },
1895
- { value: "America/Anchorage", label: "Anchorage" },
1896
- { value: "America/Los_Angeles", label: "Los Angeles" },
1897
- { value: "America/Phoenix", label: "Phoenix" },
1898
- { value: "America/Denver", label: "Denver" },
1899
- { value: "America/Chicago", label: "Chicago" },
1900
- { value: "America/Mexico_City", label: "Mexico City" },
1901
- { value: "America/New_York", label: "New York" },
1902
- { value: "America/Bogota", label: "Bogot\xE1" },
1903
- { value: "America/Caracas", label: "Caracas" },
1904
- { value: "America/Santiago", label: "Santiago" },
1905
- { value: "America/St_Johns", label: "St. John's" },
1906
- { value: "America/Sao_Paulo", label: "S\xE3o Paulo" },
1907
- { value: "America/Argentina/Buenos_Aires", label: "Buenos Aires" },
1908
- { value: "America/Noronha", label: "Fernando de Noronha" },
1909
- { value: "Atlantic/Azores", label: "Azores" },
1910
- { value: "Atlantic/Reykjavik", label: "Reykjavik" },
1911
- { value: "Europe/London", label: "London" },
1912
- { value: "Europe/Paris", label: "Paris" },
1913
- { value: "Europe/Berlin", label: "Berlin" },
1914
- { value: "Europe/Athens", label: "Athens" },
1915
- { value: "Africa/Cairo", label: "Cairo" },
1916
- { value: "Africa/Johannesburg", label: "Johannesburg" },
1917
- { value: "Europe/Moscow", label: "Moscow" },
1918
- { value: "Europe/Istanbul", label: "Istanbul" },
1919
- { value: "Asia/Tehran", label: "Tehran" },
1920
- { value: "Asia/Dubai", label: "Dubai" },
1921
- { value: "Asia/Kabul", label: "Kabul" },
1922
- { value: "Asia/Karachi", label: "Karachi" },
1923
- { value: "Asia/Kolkata", label: "Mumbai" },
1924
- { value: "Asia/Kathmandu", label: "Kathmandu" },
1925
- { value: "Asia/Dhaka", label: "Dhaka" },
1926
- { value: "Asia/Yangon", label: "Yangon" },
1927
- { value: "Asia/Bangkok", label: "Bangkok" },
1928
- { value: "Asia/Shanghai", label: "Shanghai" },
1929
- { value: "Asia/Hong_Kong", label: "Hong Kong" },
1930
- { value: "Asia/Singapore", label: "Singapore" },
1931
- { value: "Australia/Eucla", label: "Eucla" },
1932
- { value: "Asia/Tokyo", label: "Tokyo" },
1933
- { value: "Asia/Seoul", label: "Seoul" },
1934
- { value: "Australia/Adelaide", label: "Adelaide" },
1935
- { value: "Australia/Sydney", label: "Sydney" },
1936
- { value: "Australia/Lord_Howe", label: "Lord Howe Island" },
1937
- { value: "Pacific/Noumea", label: "Noum\xE9a" },
1938
- { value: "Pacific/Auckland", label: "Auckland" },
1939
- { value: "Pacific/Chatham", label: "Chatham Islands" },
1940
- { value: "Pacific/Apia", label: "Apia" },
1941
- { value: "Pacific/Kiritimati", label: "Kiritimati" }
1942
- ];
1943
- var EXCHANGE_TIMEZONE = "exchange";
1944
- function isExchangeTimezone(zone) {
1945
- return zone === EXCHANGE_TIMEZONE;
1946
- }
1947
- function resolveTimezone(zone, exchangeZone) {
1948
- if (!isExchangeTimezone(zone)) return zone;
1949
- return exchangeZone && exchangeZone !== "" ? exchangeZone : "Etc/UTC";
1863
+ // src/core/drawings/defaults.ts
1864
+ function captureToolDefaults(d) {
1865
+ const out = { style: { ...d.style } };
1866
+ if (d.text) {
1867
+ const { value: _value, ...styling } = d.text;
1868
+ out.text = styling;
1869
+ }
1870
+ const props = d.serialize().props;
1871
+ if (props !== void 0) out.props = clonePlain(props);
1872
+ return out;
1950
1873
  }
1951
- function normalizeTimezone(zone) {
1952
- return zone === "UTC" || zone === "Etc/UTC" || zone === "Etc/GMT" ? "Etc/UTC" : zone;
1874
+ function applyToolDefaults(d, defaults) {
1875
+ if (!defaults) return;
1876
+ if (defaults.style) d.style = { ...d.style, ...defaults.style };
1877
+ if (defaults.text) d.text = { ...d.text ?? defaultText(), ...defaults.text, value: d.text?.value ?? "" };
1878
+ if (defaults.props) d.applyProps(clonePlain(defaults.props));
1953
1879
  }
1954
- function timezoneMenuRows(current) {
1955
- const active = normalizeTimezone(current);
1956
- const [utc, ...zones] = TIMEZONES.map((t) => ({ value: t.value, label: tzMenuLabel(t.value, t.label), checked: t.value === active }));
1957
- return [utc, { value: EXCHANGE_TIMEZONE, label: "Exchange", checked: isExchangeTimezone(current) }, ...zones];
1880
+ function sameToolDefaults(a, b) {
1881
+ return JSON.stringify(a ?? null) === JSON.stringify(b ?? null);
1958
1882
  }
1959
- function tzOffset(zone, date = /* @__PURE__ */ new Date()) {
1960
- try {
1961
- const parts = new Intl.DateTimeFormat("en-US", { timeZone: zone, timeZoneName: "shortOffset" }).formatToParts(date);
1962
- const raw = parts.find((p) => p.type === "timeZoneName")?.value ?? "";
1963
- const m = raw.match(/GMT(?:(\+|-)(\d{1,2})(?::(\d{2}))?)?/);
1964
- if (!m || !m[1]) return "UTC";
1965
- const sign = m[1] === "-" ? "-" : "+";
1966
- const hrs = m[2] ?? "0";
1967
- const mins = m[3] ?? "";
1968
- return mins ? `UTC${sign}${hrs}:${mins}` : `UTC${sign}${hrs}`;
1969
- } catch {
1970
- return "UTC";
1883
+ function sanitizeToolDefaults(raw) {
1884
+ if (raw == null || typeof raw !== "object" || Array.isArray(raw)) return null;
1885
+ const r = raw;
1886
+ const obj = (v) => v != null && typeof v === "object" && !Array.isArray(v) ? v : void 0;
1887
+ const out = {};
1888
+ const style = obj(r.style);
1889
+ if (style) out.style = style;
1890
+ const text = obj(r.text);
1891
+ if (text) {
1892
+ const { value: _value, ...styling } = text;
1893
+ out.text = styling;
1894
+ }
1895
+ const props = obj(r.props);
1896
+ if (props) out.props = props;
1897
+ return out.style || out.text || out.props ? out : null;
1898
+ }
1899
+ var TEMPLATE_NAME_MAX = 60;
1900
+ function sanitizeToolTemplates(raw) {
1901
+ if (!Array.isArray(raw)) return [];
1902
+ const byName = /* @__PURE__ */ new Map();
1903
+ for (const entry of raw) {
1904
+ if (entry == null || typeof entry !== "object") continue;
1905
+ const e = entry;
1906
+ const name = typeof e.name === "string" ? e.name.trim().slice(0, TEMPLATE_NAME_MAX) : "";
1907
+ const settings = sanitizeToolDefaults(e.settings);
1908
+ if (name && settings) byName.set(name, { name, settings });
1909
+ }
1910
+ return [...byName.values()];
1911
+ }
1912
+ var LINE_TOOLS = /* @__PURE__ */ new Set(["trendline", "ray", "extendedline", "hline", "hray", "vline", "crossline", "infoline", "trendangle"]);
1913
+ var AREA_TOOLS = /* @__PURE__ */ new Set(["box", "rotatedrect", "ellipse", "circle", "triangle", "parallelchannel", "disjointchannel", "flattopbottom"]);
1914
+ function builtinToolTemplates(type) {
1915
+ if (LINE_TOOLS.has(type)) {
1916
+ return [
1917
+ { name: "Support", settings: { style: { lineColor: BULLISH, lineWidth: 2, lineStyle: "solid" } } },
1918
+ { name: "Resistance", settings: { style: { lineColor: BEARISH, lineWidth: 2, lineStyle: "solid" } } },
1919
+ { name: "Projection", settings: { style: { lineColor: NEUTRAL, lineWidth: 1, lineStyle: "dashed" } } }
1920
+ ];
1971
1921
  }
1972
- }
1973
- function tzMenuLabel(zone, location) {
1974
- if (normalizeTimezone(zone) === "Etc/UTC") return location;
1975
- return `(${tzOffset(zone)}) ${location}`;
1976
- }
1977
- function tzButtonLabel(zone) {
1978
- if (normalizeTimezone(zone) === "Etc/UTC") return "UTC";
1979
- return tzOffset(zone);
1922
+ if (AREA_TOOLS.has(type)) {
1923
+ return [
1924
+ { name: "Demand", settings: { style: { lineColor: BULLISH, fillColor: `${BULLISH}26` } } },
1925
+ { name: "Supply", settings: { style: { lineColor: BEARISH, fillColor: `${BEARISH}26` } } }
1926
+ ];
1927
+ }
1928
+ return [];
1980
1929
  }
1981
1930
 
1982
1931
  // src/core/drawings/effectiveColor.ts
@@ -2076,6 +2025,10 @@ var DrawingHistory = class {
2076
2025
  };
2077
2026
 
2078
2027
  // src/core/drawings/DrawingController.ts
2028
+ function factoryDefaults(d) {
2029
+ const fresh = createDrawing(d.type, { paneId: d.paneId });
2030
+ return fresh ? captureToolDefaults(fresh) : void 0;
2031
+ }
2079
2032
  var DrawingController = class {
2080
2033
  constructor(renderer, events, option, seriesGateway) {
2081
2034
  this.events = events;
@@ -2094,8 +2047,10 @@ var DrawingController = class {
2094
2047
  // ordered; [0] is the primary (settings-popup) selection
2095
2048
  this.clipboard = [];
2096
2049
  // in-memory copy buffer (per chart)
2097
- this.lastStyle = /* @__PURE__ */ new Map();
2098
- // per-tool "last used" style
2050
+ /** Per-tool remembered settings — the last drawing of each type the user styled seeds the next. */
2051
+ this.defaults = /* @__PURE__ */ new Map();
2052
+ /** Per-tool saved looks ("styles"), in the order they were saved. */
2053
+ this.templates = /* @__PURE__ */ new Map();
2099
2054
  this.subs = [];
2100
2055
  this.enabled = !!renderer.capabilities.userDrawings && !!renderer.userDrawingsPort;
2101
2056
  this.port = this.enabled ? renderer.userDrawingsPort : null;
@@ -2117,7 +2072,8 @@ var DrawingController = class {
2117
2072
  if (!this.port) return;
2118
2073
  const changed = type !== this.activeTool;
2119
2074
  this.activeTool = type;
2120
- this.port.setActiveTool(type, type ? this.lastStyle.get(type) : void 0);
2075
+ const defaults = type ? this.defaults.get(type) : void 0;
2076
+ this.port.setActiveTool(type, defaults?.style, defaults);
2121
2077
  if (changed) this.events.emit("drawing:tool", { type });
2122
2078
  }
2123
2079
  getTool() {
@@ -2185,6 +2141,84 @@ var DrawingController = class {
2185
2141
  this.port?.setFavorites?.(list);
2186
2142
  this.events.emit("drawing:favorites", { favorites: list });
2187
2143
  }
2144
+ /** Every tool's remembered settings, by type (plain JSON — persist it as-is). */
2145
+ toolDefaults() {
2146
+ const out = {};
2147
+ for (const [type, d] of this.defaults) out[type] = clonePlain(d);
2148
+ return out;
2149
+ }
2150
+ /** Replace every tool's remembered settings (restoring persisted prefs). Unknown types and
2151
+ * malformed entries are dropped; an unchanged set is a no-op and emits nothing. */
2152
+ setToolDefaults(map2) {
2153
+ const next = /* @__PURE__ */ new Map();
2154
+ for (const [type, raw] of Object.entries(map2 ?? {})) {
2155
+ if (!getDrawingType(type)) continue;
2156
+ const d = sanitizeToolDefaults(raw);
2157
+ if (d) next.set(type, clonePlain(d));
2158
+ }
2159
+ const changed = /* @__PURE__ */ new Set([...this.defaults.keys(), ...next.keys()]);
2160
+ for (const type of [...changed]) if (sameToolDefaults(this.defaults.get(type), next.get(type))) changed.delete(type);
2161
+ if (changed.size === 0) return;
2162
+ this.defaults.clear();
2163
+ for (const [type, d] of next) this.defaults.set(type, d);
2164
+ if (this.activeTool && changed.has(this.activeTool)) this.setTool(this.activeTool);
2165
+ for (const type of changed) this.events.emit("drawing:defaults", { type });
2166
+ }
2167
+ /** Forget a tool's remembered settings (every tool's when `type` is omitted) — new drawings
2168
+ * start from the factory defaults again. */
2169
+ resetToolDefaults(type) {
2170
+ const types = type ? this.defaults.has(type) ? [type] : [] : [...this.defaults.keys()];
2171
+ for (const t of types) {
2172
+ this.defaults.delete(t);
2173
+ this.events.emit("drawing:defaults", { type: t });
2174
+ }
2175
+ }
2176
+ /** Every tool's saved looks, by type (plain JSON — persist it as-is). */
2177
+ toolTemplates() {
2178
+ const out = {};
2179
+ for (const [type, list] of this.templates) out[type] = clonePlain(list);
2180
+ return out;
2181
+ }
2182
+ /** Replace every tool's saved looks (restoring persisted prefs). Unknown types and
2183
+ * malformed entries are dropped; an unchanged set is a no-op and emits nothing. */
2184
+ setToolTemplates(map2) {
2185
+ const next = /* @__PURE__ */ new Map();
2186
+ for (const [type, raw] of Object.entries(map2 ?? {})) {
2187
+ if (!getDrawingType(type)) continue;
2188
+ const list = sanitizeToolTemplates(raw);
2189
+ if (list.length > 0) next.set(type, list);
2190
+ }
2191
+ const changed = [.../* @__PURE__ */ new Set([...this.templates.keys(), ...next.keys()])].filter(
2192
+ (t) => JSON.stringify(this.templates.get(t) ?? []) !== JSON.stringify(next.get(t) ?? [])
2193
+ );
2194
+ if (changed.length === 0) return;
2195
+ this.templates = next;
2196
+ this.pushTemplates();
2197
+ for (const type of changed) this.events.emit("drawing:templates", { type });
2198
+ }
2199
+ /** Save the given settings as a named look for a tool (a same-named one is replaced). */
2200
+ saveToolTemplate(type, name, settings) {
2201
+ if (!getDrawingType(type)) return;
2202
+ const [entry] = sanitizeToolTemplates([{ name, settings }]);
2203
+ if (!entry) return;
2204
+ const list = (this.templates.get(type) ?? []).filter((t) => t.name !== entry.name);
2205
+ this.templates.set(type, [...list, clonePlain(entry)]);
2206
+ this.pushTemplates();
2207
+ this.events.emit("drawing:templates", { type });
2208
+ }
2209
+ /** Delete a tool's saved look by name. */
2210
+ removeToolTemplate(type, name) {
2211
+ const list = this.templates.get(type);
2212
+ if (!list?.some((t) => t.name === name)) return;
2213
+ const rest = list.filter((t) => t.name !== name);
2214
+ if (rest.length > 0) this.templates.set(type, rest);
2215
+ else this.templates.delete(type);
2216
+ this.pushTemplates();
2217
+ this.events.emit("drawing:templates", { type });
2218
+ }
2219
+ pushTemplates() {
2220
+ this.port?.setToolTemplates?.(this.toolTemplates());
2221
+ }
2188
2222
  setToolbar(option) {
2189
2223
  this.port?.setToolbar(buildToolbar(option).definition);
2190
2224
  }
@@ -2200,21 +2234,22 @@ var DrawingController = class {
2200
2234
  // ── programmatic CRUD (facade-facing) ── each is one undo step
2201
2235
  add(type, init = {}) {
2202
2236
  if (!this.enabled) return null;
2203
- const last = this.lastStyle.get(type);
2204
- const style = { ...last ?? {}, ...init.style ?? {} };
2237
+ const defaults = this.defaults.get(type);
2205
2238
  const d = createDrawing(type, {
2206
2239
  id: this.store.nextId(),
2207
2240
  paneId: init.paneId ?? "price",
2208
2241
  anchors: init.anchors,
2209
- style,
2210
2242
  text: init.text,
2211
- props: init.props,
2212
2243
  zIndex: init.zIndex ?? this.startZ(type, init.paneId ?? "price")
2213
2244
  });
2214
2245
  if (!d) return null;
2246
+ applyToolDefaults(d, defaults);
2247
+ if (init.style) d.style = { ...d.style, ...init.style };
2248
+ if (init.text) d.text = { ...d.text ?? init.text, ...init.text };
2249
+ if (init.props) d.applyProps(init.props);
2215
2250
  this.history.record(this.store.serialize());
2216
2251
  this.store.add(d);
2217
- this.captureStyle(d.id);
2252
+ this.captureDefaults(d.id);
2218
2253
  this.events.emit("drawing:created", { id: d.id });
2219
2254
  return d;
2220
2255
  }
@@ -2226,9 +2261,8 @@ var DrawingController = class {
2226
2261
  const before = this.store.serialize();
2227
2262
  const cur = patch.style ? this.store.get(id) : void 0;
2228
2263
  const resolved = patch.style && cur ? { ...patch, style: { ...cur.style, ...patch.style } } : patch;
2229
- if (this.store.update(id, resolved)) {
2264
+ if (this.updateDrawing(id, resolved)) {
2230
2265
  this.history.record(before);
2231
- this.captureStyle(id);
2232
2266
  this.events.emit("drawing:edited", { id });
2233
2267
  }
2234
2268
  }
@@ -2236,7 +2270,7 @@ var DrawingController = class {
2236
2270
  updateMany(patches) {
2237
2271
  this.history.begin(this.store.serialize());
2238
2272
  for (const { id, patch } of patches) {
2239
- if (this.store.update(id, patch)) {
2273
+ if (this.updateDrawing(id, patch)) {
2240
2274
  this.history.markDirty();
2241
2275
  this.events.emit("drawing:edited", { id });
2242
2276
  }
@@ -2416,10 +2450,27 @@ var DrawingController = class {
2416
2450
  resolve(ids) {
2417
2451
  return ids.map((id) => this.store.get(id)).filter((d) => d != null);
2418
2452
  }
2419
- /** Remember a drawing's style as the "last used" for its type (seeds the next one). */
2420
- captureStyle(id) {
2453
+ /** `store.update` that remembers the drawing's settings only when the patch changed them:
2454
+ * moving an older drawing must not hand its look back to the tool. */
2455
+ updateDrawing(id, patch) {
2456
+ const cur = this.store.get(id);
2457
+ const before = cur ? captureToolDefaults(cur) : void 0;
2458
+ if (!this.store.update(id, patch)) return false;
2459
+ const after = this.store.get(id);
2460
+ if (after && !sameToolDefaults(before, captureToolDefaults(after))) this.captureDefaults(id);
2461
+ return true;
2462
+ }
2463
+ /** Remember a drawing's settings as its tool's defaults (they seed the next one), and
2464
+ * announce a real change so a host can save it. */
2465
+ captureDefaults(id) {
2421
2466
  const d = this.store.get(id);
2422
- if (d) this.lastStyle.set(d.type, { ...d.style });
2467
+ if (!d) return;
2468
+ const captured = captureToolDefaults(d);
2469
+ const next = sameToolDefaults(captured, factoryDefaults(d)) ? void 0 : captured;
2470
+ if (sameToolDefaults(this.defaults.get(d.type), next)) return;
2471
+ if (next) this.defaults.set(d.type, next);
2472
+ else this.defaults.delete(d.type);
2473
+ this.events.emit("drawing:defaults", { type: d.type });
2423
2474
  }
2424
2475
  sync() {
2425
2476
  this.port?.syncDrawings(this.store.serialize().drawings);
@@ -2435,22 +2486,20 @@ var DrawingController = class {
2435
2486
  break;
2436
2487
  case "create": {
2437
2488
  const before = this.store.serialize();
2438
- const last = this.lastStyle.get(i.doc.type);
2439
- const style = last ? { ...i.doc.style, ...last } : i.doc.style;
2440
- const d = deserializeDrawing({ ...i.doc, id: this.store.nextId(), style });
2489
+ const d = deserializeDrawing({ ...i.doc, id: this.store.nextId() });
2441
2490
  if (!d) return;
2491
+ applyToolDefaults(d, this.defaults.get(d.type));
2442
2492
  if (!d.zIndex) d.zIndex = this.startZ(d.type, d.paneId) ?? 0;
2443
2493
  this.history.record(before);
2444
2494
  this.store.add(d);
2445
- this.captureStyle(d.id);
2495
+ this.captureDefaults(d.id);
2446
2496
  this.events.emit("drawing:created", { id: d.id });
2447
2497
  break;
2448
2498
  }
2449
2499
  case "edit": {
2450
2500
  const before = this.store.serialize();
2451
- if (this.store.update(i.doc.id, i.doc)) {
2501
+ if (this.updateDrawing(i.doc.id, i.doc)) {
2452
2502
  this.history.record(before);
2453
- this.captureStyle(i.doc.id);
2454
2503
  this.events.emit("drawing:edited", { id: i.doc.id });
2455
2504
  }
2456
2505
  break;
@@ -2458,7 +2507,7 @@ var DrawingController = class {
2458
2507
  case "edit-many": {
2459
2508
  this.history.begin(this.store.serialize());
2460
2509
  for (const doc of i.docs) {
2461
- if (this.store.update(doc.id, doc)) {
2510
+ if (this.updateDrawing(doc.id, doc)) {
2462
2511
  this.history.markDirty();
2463
2512
  this.events.emit("drawing:edited", { id: doc.id });
2464
2513
  }
@@ -2482,6 +2531,12 @@ var DrawingController = class {
2482
2531
  case "favorite":
2483
2532
  this.setFavorite(i.type, i.on);
2484
2533
  break;
2534
+ case "template-save":
2535
+ this.saveToolTemplate(i.type, i.name, i.settings);
2536
+ break;
2537
+ case "template-remove":
2538
+ this.removeToolTemplate(i.type, i.name);
2539
+ break;
2485
2540
  case "snap-mode":
2486
2541
  if (i.mode !== this.snapMode) {
2487
2542
  this.snapMode = i.mode;
@@ -2602,6 +2657,7 @@ function chromeHint(text, opts) {
2602
2657
  // src/renderers/native/drawings/DrawingToolbar.ts
2603
2658
  var TOOLBAR_WIDTH = 44;
2604
2659
  var TOOLBAR_COLLAPSED_WIDTH = 16;
2660
+ var FLYOUT_GAP = 3;
2605
2661
  var DrawingToolbar = class {
2606
2662
  constructor(host, theme, onArm, onMagnet = () => {
2607
2663
  }, onMeasure = () => {
@@ -2631,6 +2687,9 @@ var DrawingToolbar = class {
2631
2687
  // group id (or MAGNET_ID) whose flyout is open
2632
2688
  this.flyoutCell = null;
2633
2689
  // the cell the open flyout is anchored to
2690
+ /** Closed flyouts still playing their exit animation, by owner id — reopening the same
2691
+ * owner takes its element back instead of stacking a second one on it. */
2692
+ this.exitingFlyouts = /* @__PURE__ */ new Map();
2634
2693
  this.groupCells = /* @__PURE__ */ new Map();
2635
2694
  // the composite cell (hover/active bg + flyout anchor)
2636
2695
  this.groupIcons = /* @__PURE__ */ new Map();
@@ -2739,6 +2798,7 @@ var DrawingToolbar = class {
2739
2798
  }
2740
2799
  destroy() {
2741
2800
  this.closeFlyout();
2801
+ for (const { exit } of [...this.exitingFlyouts.values()]) exit.finish();
2742
2802
  for (const dispose of this.tipDisposers.splice(0)) dispose();
2743
2803
  this.root.remove();
2744
2804
  }
@@ -2966,16 +3026,22 @@ var DrawingToolbar = class {
2966
3026
  * outside-dismiss. Callers fill it with items. */
2967
3027
  beginFlyout(ownerId, cell) {
2968
3028
  const t = this.theme;
2969
- const fly = document.createElement("div");
3029
+ const back = this.exitingFlyouts.get(ownerId);
3030
+ if (back) {
3031
+ this.exitingFlyouts.delete(ownerId);
3032
+ back.exit.cancel();
3033
+ back.fly.replaceChildren();
3034
+ }
3035
+ const fly = back?.fly ?? document.createElement("div");
2970
3036
  fly.className = "vela-dtb-flyout";
2971
- fly.style.cssText = `position:absolute;z-index:23;display:flex;flex-direction:column;gap:2px;padding:4px;border-radius:0 8px 8px 0;background:var(--vela-surface-elev);border:1px solid ${this.borderColor};border-left:none;box-shadow:var(--vela-shadow);pointer-events:auto;overflow-y:auto;overscroll-behavior:contain;`;
3037
+ fly.style.cssText = `position:absolute;z-index:23;display:flex;flex-direction:column;gap:2px;padding:4px;border-radius:var(--vela-radius-md);background:var(--vela-surface);border:1px solid ${this.borderOverride ?? "var(--vela-border-strong)"};box-shadow:var(--vela-shadow);pointer-events:auto;overflow-y:auto;overscroll-behavior:contain;`;
2972
3038
  applyChromeTokens(fly, t);
2973
3039
  this.host.appendChild(fly);
2974
3040
  const r = cell.getBoundingClientRect();
2975
3041
  const rootR = this.root.getBoundingClientRect();
2976
3042
  const hostR = this.host.getBoundingClientRect();
2977
3043
  const top = r.top - hostR.top;
2978
- fly.style.left = `${rootR.right - hostR.left - 1}px`;
3044
+ fly.style.left = `${rootR.right - hostR.left + FLYOUT_GAP}px`;
2979
3045
  fly.style.top = `${top}px`;
2980
3046
  fly.style.maxHeight = `${Math.max(120, hostR.height - top - 8)}px`;
2981
3047
  this.flyout = fly;
@@ -2991,6 +3057,7 @@ var DrawingToolbar = class {
2991
3057
  if (!cell) return;
2992
3058
  const fly = this.beginFlyout(group.id, cell);
2993
3059
  const sections = group.sections ?? [{ label: "", tools: group.tools }];
3060
+ const badge = sections.some((s) => s.tools.some((tool) => !!tool.icon));
2994
3061
  for (let si = 0; si < sections.length; si++) {
2995
3062
  const section = sections[si];
2996
3063
  if (section.label) {
@@ -3001,6 +3068,7 @@ var DrawingToolbar = class {
3001
3068
  fly.appendChild(
3002
3069
  this.makeFlyoutItem({
3003
3070
  icon: tool.icon,
3071
+ badge,
3004
3072
  label: tool.label,
3005
3073
  selected: tool.type === this.active,
3006
3074
  shortcut: this.shortcuts.get(tool.type),
@@ -3051,21 +3119,24 @@ var DrawingToolbar = class {
3051
3119
  div.style.cssText = `height:1px;margin:4px 8px;background:${this.borderColor};`;
3052
3120
  return div;
3053
3121
  }
3054
- /** A flyout row, left to right: optional leading icon, label, a check when it's the selected
3055
- * entry, an optional shortcut hint, and — at the far right — the favorite star (tool rows).
3056
- * Hover tint is CSS (`.vela-dtb-item:hover`), so navigating the menu stays smooth. */
3122
+ /** A flyout row, left to right: optional leading icon badge, label, a check when it's the
3123
+ * selected entry, an optional shortcut hint, and — at the far right — the favorite star (tool
3124
+ * rows). Hover tint is CSS (`.vela-dtb-item:hover`), so navigating the menu stays smooth. */
3057
3125
  makeFlyoutItem(opts) {
3058
3126
  const t = this.theme;
3059
3127
  const item = document.createElement("button");
3060
3128
  item.type = "button";
3061
3129
  item.className = "vela-dtb-item";
3062
3130
  item.setAttribute("aria-label", opts.label);
3063
- item.style.cssText = `display:flex;align-items:center;gap:8px;padding:5px 10px 5px 8px;cursor:pointer;color:${t.textColor};border-radius:var(--vela-radius-sm);font:13px ${t.fontFamily};white-space:nowrap;min-width:148px;`;
3064
- if (opts.icon) {
3065
- const icon2 = document.createElement("span");
3066
- icon2.style.cssText = "width:18px;height:18px;display:flex;align-items:center;justify-content:center;flex:none;";
3067
- icon2.innerHTML = sizedIcon(opts.icon);
3068
- item.appendChild(icon2);
3131
+ const badged = opts.badge === true || !!opts.icon;
3132
+ const box = badged ? "gap:10px;padding:4px 10px 4px 6px;" : "gap:8px;padding:5px 10px 5px 8px;";
3133
+ item.style.cssText = `display:flex;align-items:center;${box}cursor:pointer;color:${t.textColor};border-radius:var(--vela-radius-sm);font:13px ${t.fontFamily};white-space:nowrap;min-width:148px;`;
3134
+ if (opts.selected) item.dataset.selected = "1";
3135
+ if (badged) {
3136
+ const badge = document.createElement("span");
3137
+ badge.className = "vela-dtb-badge";
3138
+ if (opts.icon) badge.innerHTML = opts.icon;
3139
+ item.appendChild(badge);
3069
3140
  }
3070
3141
  const label = document.createElement("span");
3071
3142
  label.textContent = opts.label;
@@ -3110,7 +3181,12 @@ var DrawingToolbar = class {
3110
3181
  if (fly) {
3111
3182
  this.flyout = null;
3112
3183
  announceSurface(fly, false, "menu", this.flyoutCell);
3113
- fly.remove();
3184
+ const owner = this.flyoutOwnerId ?? "";
3185
+ const exit = holdForExit(fly, () => {
3186
+ if (this.exitingFlyouts.get(owner)?.fly === fly) this.exitingFlyouts.delete(owner);
3187
+ fly.remove();
3188
+ });
3189
+ if (exit) this.exitingFlyouts.set(owner, { fly, exit });
3114
3190
  this.flyoutCell?.classList.remove("vela-open");
3115
3191
  this.flyoutCell = null;
3116
3192
  this.flyoutOwnerId = null;
@@ -3142,17 +3218,17 @@ var DrawingToolbar = class {
3142
3218
  this.paintStay();
3143
3219
  }
3144
3220
  makeButton(icon2, title, onClick) {
3145
- const btn2 = document.createElement("button");
3146
- btn2.type = "button";
3147
- btn2.className = "vela-dtb-btn";
3148
- btn2.setAttribute("aria-label", title);
3149
- this.tipText.set(btn2, title);
3150
- if (icon2) btn2.innerHTML = hitHtml(icon2);
3221
+ const btn = document.createElement("button");
3222
+ btn.type = "button";
3223
+ btn.className = "vela-dtb-btn";
3224
+ btn.setAttribute("aria-label", title);
3225
+ this.tipText.set(btn, title);
3226
+ if (icon2) btn.innerHTML = hitHtml(icon2);
3151
3227
  this.tipDisposers.push(
3152
- attachChromeTooltip(btn2, { host: this.host, theme: () => this.theme, text: () => this.tipText.get(btn2) ?? title, placement: "right" })
3228
+ attachChromeTooltip(btn, { host: this.host, theme: () => this.theme, text: () => this.tipText.get(btn) ?? title, placement: "right" })
3153
3229
  );
3154
- btn2.addEventListener("click", onClick);
3155
- return btn2;
3230
+ btn.addEventListener("click", onClick);
3231
+ return btn;
3156
3232
  }
3157
3233
  divider() {
3158
3234
  const d = document.createElement("div");
@@ -3194,6 +3270,11 @@ function ensureStyles() {
3194
3270
  .vela-dtb[data-collapsed='1'] .vela-dtb-collapse .vela-dtb-hit{width:14px;}
3195
3271
  .vela-dtb-item{background:transparent;border:none;transition:background var(--vela-dur-fast) ease;}
3196
3272
  .vela-dtb-item:hover{background:var(--vela-hover-strong);}
3273
+ .vela-dtb-badge{width:24px;height:24px;padding:3px;box-sizing:border-box;display:flex;align-items:center;justify-content:center;flex:none;border-radius:var(--vela-radius-md);background:var(--vela-hover);border:1px solid var(--vela-border);color:var(--vela-fg-muted);transition:background var(--vela-dur-fast) ease,border-color var(--vela-dur-fast) ease,color var(--vela-dur-fast) ease;}
3274
+ .vela-dtb-badge:empty{background:transparent;border-color:transparent;}
3275
+ .vela-dtb-badge svg{width:100%;height:100%;}
3276
+ .vela-dtb-item:hover .vela-dtb-badge:not(:empty){background:var(--vela-active);color:var(--vela-fg-bright);}
3277
+ .vela-dtb-item[data-selected='1'] .vela-dtb-badge:not(:empty){background:color-mix(in srgb,var(--vela-fg-bright) 83%,var(--vela-surface));border-color:color-mix(in srgb,var(--vela-fg-bright) 40%,transparent);color:var(--vela-surface);}
3197
3278
  .vela-dtb-star{width:26px;height:22px;margin:-3px -5px -3px 0;padding:3px 5px;box-sizing:border-box;display:flex;align-items:center;justify-content:center;flex:none;opacity:0;color:inherit;border-radius:var(--vela-radius-sm);transition:opacity .1s ease,color .1s ease,background .1s ease;}
3198
3279
  .vela-dtb-star svg{width:16px;height:16px;}
3199
3280
  .vela-dtb-item:hover .vela-dtb-star{opacity:.55;}
@@ -3284,8 +3365,8 @@ function resolveAnimations(animations) {
3284
3365
 
3285
3366
  // src/renderers/shared/IndicatorInputsDialog.ts
3286
3367
  var PROPS_TAB = "Properties";
3287
- function bagOf(row, decl) {
3288
- return decl.prop ? row.propValues : row.values;
3368
+ function bagOf(row2, decl) {
3369
+ return decl.prop ? row2.propValues : row2.values;
3289
3370
  }
3290
3371
  var SOURCES = ["close", "open", "high", "low", "hl2", "hlc3", "ohlc4", "volume"];
3291
3372
  var IndicatorInputsDialog = class {
@@ -3318,13 +3399,13 @@ var IndicatorInputsDialog = class {
3318
3399
  isOpen() {
3319
3400
  return this.dialog !== null;
3320
3401
  }
3321
- open(row) {
3402
+ open(row2) {
3322
3403
  this.close();
3323
- if (row.inputs.length === 0 && row.props.length === 0) return;
3324
- this.row = row;
3325
- this.openId = row.id;
3326
- this.snapshot = { ...row.values };
3327
- this.propSnapshot = { ...row.propValues };
3404
+ if (row2.inputs.length === 0 && row2.props.length === 0) return;
3405
+ this.row = row2;
3406
+ this.openId = row2.id;
3407
+ this.snapshot = { ...row2.values };
3408
+ this.propSnapshot = { ...row2.propValues };
3328
3409
  ensureDialogStyles();
3329
3410
  const t = this.host.theme();
3330
3411
  const border = "var(--vela-border)";
@@ -3332,7 +3413,7 @@ var IndicatorInputsDialog = class {
3332
3413
  const host = this.host.dialogHost() ?? this.host.container;
3333
3414
  const ui = new Dialog({
3334
3415
  host,
3335
- title: row.title,
3416
+ title: row2.title,
3336
3417
  // Non-modal: live-edit dialog — a modal machine locks pointer events on the
3337
3418
  // whole body, killing the chart and the body-portaled popovers.
3338
3419
  modal: false,
@@ -3363,9 +3444,9 @@ var IndicatorInputsDialog = class {
3363
3444
  theme: () => this.host.theme(),
3364
3445
  text: () => "Close"
3365
3446
  }));
3366
- const tabDefs = tabInputs(row.inputs);
3367
- if (row.props.length > 0) {
3368
- const propDecls = row.props.map((p) => {
3447
+ const tabDefs = tabInputs(row2.inputs);
3448
+ if (row2.props.length > 0) {
3449
+ const propDecls = row2.props.map((p) => {
3369
3450
  const d = { ...p, prop: true };
3370
3451
  delete d.tab;
3371
3452
  return d;
@@ -3399,7 +3480,7 @@ var IndicatorInputsDialog = class {
3399
3480
  tab.addEventListener("click", () => activate(idx));
3400
3481
  tabs.appendChild(tab);
3401
3482
  tabEls.push(tab);
3402
- bodies.push(this.buildTabBody(def.inputs, row, gates));
3483
+ bodies.push(this.buildTabBody(def.inputs, row2, gates));
3403
3484
  if (def.inputs.every((d) => d.when)) {
3404
3485
  gates.push({ el: tab, visible: (bag) => def.inputs.some((d) => inputVisible(d.when, bag)) });
3405
3486
  }
@@ -3408,7 +3489,7 @@ var IndicatorInputsDialog = class {
3408
3489
  for (const b of bodies) ui.body.appendChild(b);
3409
3490
  activate(0);
3410
3491
  const refresh = () => {
3411
- const bag = valuesBag(row);
3492
+ const bag = valuesBag(row2);
3412
3493
  for (const g of gates) g.el.style.display = g.visible(bag) ? "" : "none";
3413
3494
  if (tabEls[active]?.style.display === "none") {
3414
3495
  const next = tabEls.findIndex((el) => el.style.display !== "none");
@@ -3436,7 +3517,7 @@ var IndicatorInputsDialog = class {
3436
3517
  }
3437
3518
  /** One tab's scrollable body — one shared grid so every group's controls share a
3438
3519
  * left edge (a 100px number field lines up with a wider session/time pair). */
3439
- buildTabBody(inputs, row, gates) {
3520
+ buildTabBody(inputs, row2, gates) {
3440
3521
  const body = document.createElement("div");
3441
3522
  body.style.cssText = inputDialogBodyStyle(this.host.mobile());
3442
3523
  for (const group of groupInputs(inputs)) {
@@ -3448,7 +3529,7 @@ var IndicatorInputsDialog = class {
3448
3529
  section.appendChild(header);
3449
3530
  }
3450
3531
  for (const inputRow of group.rows) {
3451
- const el = this.buildInputRow(section, inputRow, row);
3532
+ const el = this.buildInputRow(section, inputRow, row2);
3452
3533
  if (inputRow.some((d) => d.when)) {
3453
3534
  gates.push({ el, visible: (bag) => rowVisible(inputRow, bag) });
3454
3535
  }
@@ -3481,26 +3562,26 @@ var IndicatorInputsDialog = class {
3481
3562
  }
3482
3563
  /** Restore every input and prop to its open-time value (re-running the indicator), then close. */
3483
3564
  revertAndClose() {
3484
- const row = this.row;
3565
+ const row2 = this.row;
3485
3566
  const snap = this.snapshot;
3486
- if (row && snap) {
3487
- for (const inp of row.inputs) {
3567
+ if (row2 && snap) {
3568
+ for (const inp of row2.inputs) {
3488
3569
  const snapped = snap[inp.key];
3489
3570
  const before = snapped !== void 0 ? snapped : inp.defval;
3490
- if (row.values[inp.key] !== before) {
3491
- row.values[inp.key] = before;
3492
- this.host.onChange?.({ indicatorId: row.id, key: inp.key, value: before });
3571
+ if (row2.values[inp.key] !== before) {
3572
+ row2.values[inp.key] = before;
3573
+ this.host.onChange?.({ indicatorId: row2.id, key: inp.key, value: before });
3493
3574
  }
3494
3575
  }
3495
3576
  }
3496
3577
  const propSnap = this.propSnapshot;
3497
- if (row && propSnap) {
3498
- for (const p of row.props) {
3578
+ if (row2 && propSnap) {
3579
+ for (const p of row2.props) {
3499
3580
  const snapped = propSnap[p.key];
3500
3581
  const before = snapped !== void 0 ? snapped : p.defval;
3501
- if (row.propValues[p.key] !== before) {
3502
- row.propValues[p.key] = before;
3503
- this.host.onChange?.({ indicatorId: row.id, key: p.key, value: before, kind: "prop" });
3582
+ if (row2.propValues[p.key] !== before) {
3583
+ row2.propValues[p.key] = before;
3584
+ this.host.onChange?.({ indicatorId: row2.id, key: p.key, value: before, kind: "prop" });
3504
3585
  }
3505
3586
  }
3506
3587
  }
@@ -3518,35 +3599,35 @@ var IndicatorInputsDialog = class {
3518
3599
  * rebuild-after-reset move as the chart-settings dialog. The open-time snapshot
3519
3600
  * survives the rebuild, so Cancel after a reset still reverts the whole session. */
3520
3601
  resetToDefaults() {
3521
- const row = this.row;
3522
- if (!row) return;
3602
+ const row2 = this.row;
3603
+ if (!row2) return;
3523
3604
  const snap = this.snapshot;
3524
3605
  const propSnap = this.propSnapshot;
3525
- for (const inp of row.inputs) {
3526
- if (row.values[inp.key] !== inp.defval) {
3527
- row.values[inp.key] = inp.defval;
3528
- this.host.onChange?.({ indicatorId: row.id, key: inp.key, value: inp.defval });
3606
+ for (const inp of row2.inputs) {
3607
+ if (row2.values[inp.key] !== inp.defval) {
3608
+ row2.values[inp.key] = inp.defval;
3609
+ this.host.onChange?.({ indicatorId: row2.id, key: inp.key, value: inp.defval });
3529
3610
  }
3530
3611
  }
3531
- for (const p of row.props) {
3532
- if (row.propValues[p.key] !== p.defval) {
3533
- row.propValues[p.key] = p.defval;
3534
- this.host.onChange?.({ indicatorId: row.id, key: p.key, value: p.defval, kind: "prop" });
3612
+ for (const p of row2.props) {
3613
+ if (row2.propValues[p.key] !== p.defval) {
3614
+ row2.propValues[p.key] = p.defval;
3615
+ this.host.onChange?.({ indicatorId: row2.id, key: p.key, value: p.defval, kind: "prop" });
3535
3616
  }
3536
3617
  }
3537
- this.open(row);
3618
+ this.open(row2);
3538
3619
  if (snap) this.snapshot = snap;
3539
3620
  if (propSnap) this.propSnapshot = propSnap;
3540
3621
  }
3541
3622
  /** Write one edit through: store it, notify the host, and re-apply the `when` gates. */
3542
- commit(row, decl, value) {
3543
- bagOf(row, decl)[decl.key] = value;
3544
- this.host.onChange?.({ indicatorId: row.id, key: decl.key, value, ...decl.prop ? { kind: "prop" } : {} });
3623
+ commit(row2, decl, value) {
3624
+ bagOf(row2, decl)[decl.key] = value;
3625
+ this.host.onChange?.({ indicatorId: row2.id, key: decl.key, value, ...decl.prop ? { kind: "prop" } : {} });
3545
3626
  this.refreshVisibility?.();
3546
3627
  }
3547
3628
  /** One settings row (or several `inline=` inputs) placed into a section's grid. */
3548
- buildInputRow(grid, decls, row) {
3549
- const idOf = (inp) => `vela-inp-${row.id}-${inp.key}`;
3629
+ buildInputRow(grid, decls, row2) {
3630
+ const idOf = (inp) => `vela-inp-${row2.id}-${inp.key}`;
3550
3631
  const append = (el) => {
3551
3632
  grid.appendChild(el);
3552
3633
  return el;
@@ -3557,7 +3638,7 @@ var IndicatorInputsDialog = class {
3557
3638
  inline: decls.map((d) => ({
3558
3639
  label: nameOf(d) || void 0,
3559
3640
  id: idOf(d),
3560
- control: this.buildControl(row, d, idOf(d)),
3641
+ control: this.buildControl(row2, d, idOf(d)),
3561
3642
  toggleFirst: d.type === "bool",
3562
3643
  fit: d.type === "color" || d.type === "bool"
3563
3644
  })),
@@ -3568,7 +3649,7 @@ var IndicatorInputsDialog = class {
3568
3649
  const id = idOf(lead);
3569
3650
  const info = lead.tooltip ? this.infoButton(lead.tooltip) : void 0;
3570
3651
  if (lead.type === "bool") {
3571
- const current = Boolean(bagOf(row, lead)[lead.key] ?? lead.defval);
3652
+ const current = Boolean(bagOf(row2, lead)[lead.key] ?? lead.defval);
3572
3653
  return append(fieldRow({
3573
3654
  label: nameOf(lead),
3574
3655
  id,
@@ -3577,7 +3658,7 @@ var IndicatorInputsDialog = class {
3577
3658
  toggle: {
3578
3659
  id,
3579
3660
  checked: current,
3580
- onChange: (v) => this.commit(row, lead, v)
3661
+ onChange: (v) => this.commit(row2, lead, v)
3581
3662
  }
3582
3663
  }));
3583
3664
  }
@@ -3587,7 +3668,7 @@ var IndicatorInputsDialog = class {
3587
3668
  id,
3588
3669
  stacked: true,
3589
3670
  info,
3590
- control: this.buildControl(row, lead, id)
3671
+ control: this.buildControl(row2, lead, id)
3591
3672
  }));
3592
3673
  }
3593
3674
  const fit = lead.type === "color" || lead.type === "session" || lead.type === "time";
@@ -3597,13 +3678,13 @@ var IndicatorInputsDialog = class {
3597
3678
  fit,
3598
3679
  controlWidth: fit ? void 0 : 100,
3599
3680
  info,
3600
- control: this.buildControl(row, lead, id)
3681
+ control: this.buildControl(row2, lead, id)
3601
3682
  }));
3602
3683
  }
3603
3684
  /** Build the typed control for one input, committing edits live via `onChange`. */
3604
- buildControl(row, inp, id) {
3605
- const current = bagOf(row, inp)[inp.key] ?? inp.defval;
3606
- const emit = (value) => this.commit(row, inp, value);
3685
+ buildControl(row2, inp, id) {
3686
+ const current = bagOf(row2, inp)[inp.key] ?? inp.defval;
3687
+ const emit = (value) => this.commit(row2, inp, value);
3607
3688
  if (inp.type === "bool") {
3608
3689
  return buildFieldControl({ kind: "switch", id, checked: Boolean(current), onChange: (v) => emit(v) }).el;
3609
3690
  }
@@ -3614,7 +3695,7 @@ var IndicatorInputsDialog = class {
3614
3695
  kind: "color",
3615
3696
  id,
3616
3697
  theme: this.host.theme(),
3617
- get: () => String(bagOf(row, inp)[inp.key] ?? inp.defval),
3698
+ get: () => String(bagOf(row2, inp)[inp.key] ?? inp.defval),
3618
3699
  onChange: (v) => emit(v),
3619
3700
  // An input change re-executes the script over its whole history: commit the
3620
3701
  // opacity drag once on release, not once per pointer move.
@@ -3899,21 +3980,21 @@ var IndicatorInputsDialog = class {
3899
3980
  const picker = this.host.symbolPicker();
3900
3981
  if (!picker) return this.buildTextField(id, current, emit);
3901
3982
  let value = current;
3902
- const btn2 = document.createElement("button");
3903
- btn2.type = "button";
3904
- btn2.id = id;
3905
- btn2.style.cssText = `${this.ctrlStyle()}display:flex;align-items:center;cursor:pointer;text-align:left;`;
3983
+ const btn = document.createElement("button");
3984
+ btn.type = "button";
3985
+ btn.id = id;
3986
+ btn.style.cssText = `${this.ctrlStyle()}display:flex;align-items:center;cursor:pointer;text-align:left;`;
3906
3987
  const label = document.createElement("span");
3907
3988
  label.textContent = value || "Select symbol\u2026";
3908
3989
  label.style.cssText = "overflow:hidden;text-overflow:ellipsis;white-space:nowrap;";
3909
- btn2.append(label);
3910
- btn2.addEventListener("click", () => picker(value, (picked) => {
3990
+ btn.append(label);
3991
+ btn.addEventListener("click", () => picker(value, (picked) => {
3911
3992
  if (!picked) return;
3912
3993
  value = picked;
3913
3994
  label.textContent = picked;
3914
3995
  emit(picked);
3915
3996
  }));
3916
- return btn2;
3997
+ return btn;
3917
3998
  }
3918
3999
  /** Shared field chrome for text / number / select controls (fills its wrapper's width). */
3919
4000
  ctrlStyle() {
@@ -3959,9 +4040,9 @@ function tabInputs(inputs) {
3959
4040
  function rowVisible(decls, bag) {
3960
4041
  return decls.some((d) => inputVisible(d.when, bag));
3961
4042
  }
3962
- function valuesBag(row) {
4043
+ function valuesBag(row2) {
3963
4044
  const bag = {};
3964
- for (const inp of row.inputs) bag[inp.key] = row.values[inp.key] ?? inp.defval;
4045
+ for (const inp of row2.inputs) bag[inp.key] = row2.values[inp.key] ?? inp.defval;
3965
4046
  return bag;
3966
4047
  }
3967
4048
  function groupInputs(inputs) {
@@ -4079,7 +4160,10 @@ var LEGEND_ICON_PX2 = 16;
4079
4160
  var LEGEND_CTL_PX = 18;
4080
4161
  var LEGEND_ROW_PAD_Y = 2;
4081
4162
  var LEGEND_ROW_PAD_X = 6;
4163
+ var LEGEND_LEFT_PX = 10;
4164
+ var LEGEND_SCALE_GAP_PX = 6;
4082
4165
  var LEGEND_TITLE_VALUES_GAP_PX = 6;
4166
+ var LEGEND_VALUES_GAP_PX = 5;
4083
4167
  var LEGEND_TITLE_STATUS_GAP_PX = 8;
4084
4168
  var LEGEND_ACTION_GAP_PX = 6;
4085
4169
  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;`;
@@ -4154,7 +4238,7 @@ var InputsUI = class {
4154
4238
  if (!this.selectedId) return;
4155
4239
  const target = e.target;
4156
4240
  if (target) {
4157
- for (const row of this.rows.values()) if (row.el.contains(target)) return;
4241
+ for (const row2 of this.rows.values()) if (row2.el.contains(target)) return;
4158
4242
  }
4159
4243
  this.clearSelection();
4160
4244
  };
@@ -4237,9 +4321,9 @@ var InputsUI = class {
4237
4321
  setTheme(theme) {
4238
4322
  this.theme = theme;
4239
4323
  for (const lg of this.legends.values()) applyChromeTokens(lg, theme);
4240
- for (const row of this.rows.values()) {
4241
- row.el.style.background = row.highlighted ? theme.background : this.idleRowFill();
4242
- row.el.style.color = theme.textColor;
4324
+ for (const row2 of this.rows.values()) {
4325
+ row2.el.style.background = row2.highlighted ? theme.background : this.idleRowFill();
4326
+ row2.el.style.color = theme.textColor;
4243
4327
  }
4244
4328
  if (this.foldToggle) this.syncFoldToggle();
4245
4329
  }
@@ -4258,26 +4342,26 @@ var InputsUI = class {
4258
4342
  */
4259
4343
  setLegendActions(provider) {
4260
4344
  this.legendActions = provider;
4261
- for (const row of this.rows.values()) this.renderExtras(row.id, row.extrasEl);
4345
+ for (const row2 of this.rows.values()) this.renderExtras(row2.id, row2.extrasEl);
4262
4346
  }
4263
4347
  /** (Re)build one row's contributed buttons. Same look, reveal and tooltips as the built-ins. */
4264
4348
  renderExtras(id, extrasEl) {
4265
4349
  this.disposeTips(this.extrasTips, id);
4266
4350
  extrasEl.replaceChildren();
4267
4351
  for (const action of this.legendActions?.(id) ?? []) {
4268
- const btn2 = document.createElement("button");
4269
- btn2.type = "button";
4270
- btn2.setAttribute("aria-label", action.tooltip);
4271
- this.tip(this.extrasTips, id, btn2, action.tooltip);
4272
- btn2.innerHTML = iconAt(action.icon, LEGEND_ICON_PX2);
4273
- btn2.className = "vela-ind-ctl";
4274
- btn2.dataset.legendAction = action.id;
4275
- btn2.style.cssText = LEGEND_CTL_CSS;
4276
- btn2.addEventListener("click", (e) => {
4352
+ const btn = document.createElement("button");
4353
+ btn.type = "button";
4354
+ btn.setAttribute("aria-label", action.tooltip);
4355
+ this.tip(this.extrasTips, id, btn, action.tooltip);
4356
+ btn.innerHTML = iconAt(action.icon, LEGEND_ICON_PX2);
4357
+ btn.className = "vela-ind-ctl";
4358
+ btn.dataset.legendAction = action.id;
4359
+ btn.style.cssText = LEGEND_CTL_CSS;
4360
+ btn.addEventListener("click", (e) => {
4277
4361
  e.stopPropagation();
4278
4362
  action.run();
4279
4363
  });
4280
- extrasEl.appendChild(btn2);
4364
+ extrasEl.appendChild(btn);
4281
4365
  }
4282
4366
  }
4283
4367
  /**
@@ -4287,17 +4371,17 @@ var InputsUI = class {
4287
4371
  */
4288
4372
  setLegendCallouts(provider) {
4289
4373
  this.legendCallouts = provider;
4290
- for (const row of this.rows.values()) this.renderCallouts(row);
4374
+ for (const row2 of this.rows.values()) this.renderCallouts(row2);
4291
4375
  }
4292
4376
  /** (Re)build one row's callout bubbles — tinted icon circles beside the title whose
4293
4377
  * click (when the view carries content) deploys a panel of text and actions. */
4294
- renderCallouts(row) {
4295
- this.disposeTips(this.calloutTips, row.id);
4296
- for (const bubble of row.callouts) bubble.destroy();
4297
- row.callouts = [];
4298
- row.calloutsEl.replaceChildren();
4299
- const views = this.legendCallouts?.(row.id) ?? [];
4300
- row.calloutsEl.style.display = legendCalloutsDisplay(row.highlighted, views.length > 0);
4378
+ renderCallouts(row2) {
4379
+ this.disposeTips(this.calloutTips, row2.id);
4380
+ for (const bubble of row2.callouts) bubble.destroy();
4381
+ row2.callouts = [];
4382
+ row2.calloutsEl.replaceChildren();
4383
+ const views = this.legendCallouts?.(row2.id) ?? [];
4384
+ row2.calloutsEl.style.display = legendCalloutsDisplay(row2.highlighted, views.length > 0);
4301
4385
  for (const view of views) {
4302
4386
  const bubble = new CalloutBubble({
4303
4387
  icon: view.icon,
@@ -4311,9 +4395,9 @@ var InputsUI = class {
4311
4395
  theme: () => this.theme
4312
4396
  });
4313
4397
  bubble.el.dataset.legendCallout = view.id;
4314
- this.tip(this.calloutTips, row.id, bubble.el, view.tooltip);
4315
- row.callouts.push(bubble);
4316
- row.calloutsEl.appendChild(bubble.el);
4398
+ this.tip(this.calloutTips, row2.id, bubble.el, view.tooltip);
4399
+ row2.callouts.push(bubble);
4400
+ row2.calloutsEl.appendChild(bubble.el);
4317
4401
  }
4318
4402
  }
4319
4403
  /** Reposition the per-pane legend containers after a layout change. */
@@ -4333,9 +4417,9 @@ var InputsUI = class {
4333
4417
  */
4334
4418
  setValuesVisible(visible) {
4335
4419
  this.valuesVisible = visible;
4336
- for (const row of this.rows.values()) {
4337
- row.showValues = null;
4338
- this.renderRowValues(row);
4420
+ for (const row2 of this.rows.values()) {
4421
+ row2.showValues = null;
4422
+ this.renderRowValues(row2);
4339
4423
  }
4340
4424
  }
4341
4425
  /**
@@ -4344,35 +4428,36 @@ var InputsUI = class {
4344
4428
  * unchanged frames: each row diffs against its last rendered values before touching DOM.
4345
4429
  */
4346
4430
  setPlotValues(values) {
4347
- for (const row of this.rows.values()) {
4348
- const next = values.get(row.id) ?? [];
4431
+ for (const row2 of this.rows.values()) {
4432
+ const next = values.get(row2.id) ?? [];
4349
4433
  const key = next.map((v) => `${v.value}|${v.color}`).join("\0");
4350
- if (key === row.plotValuesKey) continue;
4351
- row.plotValues = next;
4352
- row.plotValuesKey = key;
4353
- this.renderRowValues(row);
4434
+ if (key === row2.plotValuesKey) continue;
4435
+ row2.plotValues = next;
4436
+ row2.plotValuesKey = key;
4437
+ this.renderRowValues(row2);
4354
4438
  }
4355
4439
  }
4356
4440
  /** Whether a row currently shows its values: its own override, else the chart-wide flag. */
4357
- rowValuesShown(row) {
4358
- return row.showValues ?? this.valuesVisible;
4441
+ rowValuesShown(row2) {
4442
+ return row2.showValues ?? this.valuesVisible;
4359
4443
  }
4360
4444
  /** (Re)build one row's values readout from its stored plot values + visibility. A
4361
4445
  * highlighted (hovered/selected) row shows only the title + controls — no values. */
4362
- renderRowValues(row) {
4363
- if (!this.rowValuesShown(row) || row.plotValues.length === 0 || row.highlighted) {
4364
- row.valuesEl.style.display = "none";
4365
- row.valuesEl.replaceChildren();
4446
+ renderRowValues(row2) {
4447
+ if (!this.rowValuesShown(row2) || row2.plotValues.length === 0 || row2.highlighted) {
4448
+ row2.valuesEl.style.display = "none";
4449
+ row2.valuesEl.replaceChildren();
4366
4450
  return;
4367
4451
  }
4368
- row.valuesEl.style.display = "inline-flex";
4369
- row.valuesEl.replaceChildren();
4370
- for (const v of row.plotValues) {
4452
+ row2.valuesEl.style.display = "block";
4453
+ row2.valuesEl.replaceChildren();
4454
+ row2.plotValues.forEach((v, i) => {
4371
4455
  const span = document.createElement("span");
4372
4456
  span.textContent = v.value;
4373
4457
  span.style.color = v.color;
4374
- row.valuesEl.appendChild(span);
4375
- }
4458
+ if (i > 0) span.style.marginLeft = `${LEGEND_VALUES_GAP_PX}px`;
4459
+ row2.valuesEl.appendChild(span);
4460
+ });
4376
4461
  }
4377
4462
  // ── legend move/merge (menu + drag) ─────────────────────────────────────
4378
4463
  /** Open the "Move to" menu for a row, anchored under its move button. */
@@ -4380,8 +4465,8 @@ var InputsUI = class {
4380
4465
  this.closeMoveMenu();
4381
4466
  const api = this.moveApi;
4382
4467
  if (!api) return;
4383
- const row = this.rows.get(id);
4384
- const currentPane = row?.paneId ?? "price";
4468
+ const row2 = this.rows.get(id);
4469
+ const currentPane = row2?.paneId ?? "price";
4385
4470
  const panes = api.panes();
4386
4471
  const items = [];
4387
4472
  for (const p of panes) {
@@ -4421,13 +4506,13 @@ var InputsUI = class {
4421
4506
  openRowMenu(id, x, y) {
4422
4507
  this.closeRowMenu();
4423
4508
  this.closeMoveMenu();
4424
- const row = this.rows.get(id);
4425
- if (!row) return;
4509
+ const row2 = this.rows.get(id);
4510
+ if (!row2) return;
4426
4511
  this.rowMenuId = id;
4427
4512
  const items = [{
4428
4513
  id: "values",
4429
4514
  label: "Indicator values",
4430
- checked: this.rowValuesShown(row)
4515
+ checked: this.rowValuesShown(row2)
4431
4516
  }];
4432
4517
  if (!this.rowMenu) {
4433
4518
  this.rowMenu = new Menu({
@@ -4454,11 +4539,11 @@ var InputsUI = class {
4454
4539
  }
4455
4540
  /** Move a legend row to another pane (indicator merged/moved), tidying an emptied container. */
4456
4541
  setPane(id, paneId) {
4457
- const row = this.rows.get(id);
4458
- if (!row || row.paneId === paneId) return;
4459
- const prev2 = row.paneId;
4460
- row.paneId = paneId;
4461
- this.attach(this.legendFor(paneId), row.el, row.native);
4542
+ const row2 = this.rows.get(id);
4543
+ if (!row2 || row2.paneId === paneId) return;
4544
+ const prev2 = row2.paneId;
4545
+ row2.paneId = paneId;
4546
+ this.attach(this.legendFor(paneId), row2.el, row2.native);
4462
4547
  this.syncFoldToggle();
4463
4548
  if (prev2 !== "price") {
4464
4549
  const lg = this.legends.get(prev2);
@@ -4473,7 +4558,7 @@ var InputsUI = class {
4473
4558
  if (!lg) {
4474
4559
  lg = document.createElement("div");
4475
4560
  lg.dataset.velaPane = paneId;
4476
- 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;";
4561
+ lg.style.cssText = `position:absolute;left:${LEGEND_LEFT_PX}px;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;`;
4477
4562
  applyChromeTokens(lg, this.theme);
4478
4563
  this.positionLegend(lg, paneId);
4479
4564
  this.container.appendChild(lg);
@@ -4485,11 +4570,12 @@ var InputsUI = class {
4485
4570
  const bounds = this.paneBoundsOf ? this.paneBoundsOf(paneId) : { top: 0, height: Infinity };
4486
4571
  lg.toggleAttribute(LEGEND_AT_TOP_ATTR, bounds.top === 0);
4487
4572
  lg.style.display = bounds.height < 4 || !this.titlesVisible ? "none" : "flex";
4573
+ lg.style.maxWidth = `calc(100% - ${(bounds.rightAxis ?? 0) + LEGEND_LEFT_PX + LEGEND_SCALE_GAP_PX}px)`;
4488
4574
  const collapsed = this.paneCollapse.has(paneId);
4489
4575
  const masterId = this.paneCollapse.get(paneId) ?? null;
4490
- for (const row of this.rows.values()) {
4491
- if (row.paneId !== paneId) continue;
4492
- row.el.style.display = (collapsed ? row.id !== masterId : this.legendFolded) ? "none" : "flex";
4576
+ for (const row2 of this.rows.values()) {
4577
+ if (row2.paneId !== paneId) continue;
4578
+ row2.el.style.display = (collapsed ? row2.id !== masterId : this.legendFolded) ? "none" : "flex";
4493
4579
  }
4494
4580
  let top = bounds.top + 8;
4495
4581
  if (collapsed && Number.isFinite(bounds.height)) {
@@ -4523,13 +4609,13 @@ var InputsUI = class {
4523
4609
  if (wasFolded && count > 0) this.reposition();
4524
4610
  return;
4525
4611
  }
4526
- let btn2 = this.foldToggle;
4527
- if (!btn2) {
4528
- btn2 = document.createElement("button");
4529
- btn2.type = "button";
4530
- btn2.className = "vela-ind-fold";
4531
- this.tip(this.rowTips, "fold", btn2, () => this.overviewAction !== null ? "Indicators" : this.legendFolded ? "Show indicator legend" : "Hide indicator legend");
4532
- btn2.addEventListener("click", () => {
4612
+ let btn = this.foldToggle;
4613
+ if (!btn) {
4614
+ btn = document.createElement("button");
4615
+ btn.type = "button";
4616
+ btn.className = "vela-ind-fold";
4617
+ this.tip(this.rowTips, "fold", btn, () => this.overviewAction !== null ? "Indicators" : this.legendFolded ? "Show indicator legend" : "Hide indicator legend");
4618
+ btn.addEventListener("click", () => {
4533
4619
  if (this.overviewAction !== null) {
4534
4620
  this.overviewAction();
4535
4621
  return;
@@ -4537,24 +4623,24 @@ var InputsUI = class {
4537
4623
  this.legendFolded = !this.legendFolded;
4538
4624
  this.syncFoldToggle();
4539
4625
  });
4540
- this.foldToggle = btn2;
4626
+ this.foldToggle = btn;
4541
4627
  }
4542
4628
  const folded = this.legendFolded;
4543
- btn2.setAttribute("aria-label", this.overviewAction !== null ? "Indicators" : folded ? "Show indicator legend" : "Hide indicator legend");
4629
+ btn.setAttribute("aria-label", this.overviewAction !== null ? "Indicators" : folded ? "Show indicator legend" : "Hide indicator legend");
4544
4630
  const ink = this.theme.textColor;
4545
- btn2.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;`;
4546
- btn2.replaceChildren();
4631
+ 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;`;
4632
+ btn.replaceChildren();
4547
4633
  const icon2 = document.createElement("span");
4548
4634
  icon2.style.cssText = "display:inline-flex;align-items:center;line-height:0;opacity:0.7;";
4549
4635
  icon2.innerHTML = this.overviewAction !== null ? OVERVIEW_SVG : folded ? UNFOLD_SVG : FOLD_SVG;
4550
- btn2.appendChild(icon2);
4636
+ btn.appendChild(icon2);
4551
4637
  if (folded) {
4552
4638
  const label = document.createElement("span");
4553
4639
  label.textContent = String(count);
4554
4640
  label.style.cssText = `font-weight:600;line-height:normal;font-size:12px;color:${ink};`;
4555
- btn2.appendChild(label);
4641
+ btn.appendChild(label);
4556
4642
  }
4557
- this.legendFor("price").appendChild(btn2);
4643
+ this.legendFor("price").appendChild(btn);
4558
4644
  this.reposition();
4559
4645
  }
4560
4646
  /** Create or update an indicator's legend row (in the legend for its pane). */
@@ -4576,7 +4662,7 @@ var InputsUI = class {
4576
4662
  }
4577
4663
  ensureDialogStyles();
4578
4664
  const el = document.createElement("div");
4579
- 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;`;
4665
+ 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;max-width:calc(100% + ${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;`;
4580
4666
  el.addEventListener("mouseleave", () => {
4581
4667
  if (this.selectedId !== id) this.setRowHighlighted(id, false);
4582
4668
  });
@@ -4604,7 +4690,7 @@ var InputsUI = class {
4604
4690
  const statusEl = document.createElement("span");
4605
4691
  statusEl.style.cssText = "display:none;box-sizing:border-box;flex:none;";
4606
4692
  const titleWrap = document.createElement("span");
4607
- titleWrap.style.cssText = "white-space:nowrap;";
4693
+ titleWrap.style.cssText = "white-space:nowrap;min-width:0;overflow:hidden;text-overflow:ellipsis;";
4608
4694
  titleWrap.addEventListener("mouseenter", () => this.setRowHighlighted(id, true));
4609
4695
  const titleEl = document.createElement("span");
4610
4696
  titleEl.textContent = title;
@@ -4622,7 +4708,7 @@ var InputsUI = class {
4622
4708
  el.appendChild(calloutsEl);
4623
4709
  el.appendChild(statusEl);
4624
4710
  const valuesEl = document.createElement("span");
4625
- 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;`;
4711
+ valuesEl.style.cssText = `display:none;flex:1 1 0%;min-width:0;overflow:hidden;text-overflow:ellipsis;margin-left:${LEGEND_TITLE_VALUES_GAP_PX}px;white-space:nowrap;font-variant-numeric:tabular-nums;`;
4626
4712
  el.appendChild(valuesEl);
4627
4713
  const controlsEl = document.createElement("span");
4628
4714
  controlsEl.style.cssText = `display:none;align-items:center;gap:${LEGEND_ACTION_GAP_PX}px;flex:none;margin-left:${LEGEND_ACTION_GAP_PX}px;`;
@@ -4636,8 +4722,8 @@ var InputsUI = class {
4636
4722
  eye.className = "vela-ind-ctl";
4637
4723
  eye.style.cssText = LEGEND_CTL_CSS;
4638
4724
  eye.addEventListener("click", () => {
4639
- const row2 = this.rows.get(id);
4640
- this.onToggleVisible?.(id, Boolean(row2?.hidden));
4725
+ const row3 = this.rows.get(id);
4726
+ this.onToggleVisible?.(id, Boolean(row3?.hidden));
4641
4727
  });
4642
4728
  controlsEl.appendChild(eye);
4643
4729
  eyeEl = eye;
@@ -4682,9 +4768,9 @@ var InputsUI = class {
4682
4768
  controlsEl.appendChild(close);
4683
4769
  el.appendChild(controlsEl);
4684
4770
  this.attach(this.legendFor(paneId), el, !!opts.native);
4685
- const row = { id, title, inputs, values: { ...values }, props: opts.props ?? [], propValues: { ...opts.propValues ?? {} }, el, titleEl, statusEl, valuesEl, plotValues: [], plotValuesKey: "", showValues: null, highlighted: false, paneId, hidden: false, eyeEl, controlsEl, extrasEl, calloutsEl, callouts: [], native: !!opts.native };
4686
- this.rows.set(id, row);
4687
- this.renderCallouts(row);
4771
+ const row2 = { id, title, inputs, values: { ...values }, props: opts.props ?? [], propValues: { ...opts.propValues ?? {} }, el, titleEl, statusEl, valuesEl, plotValues: [], plotValuesKey: "", showValues: null, highlighted: false, paneId, hidden: false, eyeEl, controlsEl, extrasEl, calloutsEl, callouts: [], native: !!opts.native };
4772
+ this.rows.set(id, row2);
4773
+ this.renderCallouts(row2);
4688
4774
  this.syncFoldToggle();
4689
4775
  }
4690
4776
  /** Place a row in its pane's legend — native rows PREPEND (pinned to the top), Pine rows append. */
@@ -4694,10 +4780,10 @@ var InputsUI = class {
4694
4780
  }
4695
4781
  /** Reflect programmatic input changes (so a re-opened dialog shows current values). */
4696
4782
  setValues(id, values, props) {
4697
- const row = this.rows.get(id);
4698
- if (!row) return;
4699
- row.values = { ...row.values, ...values };
4700
- if (props) row.propValues = { ...row.propValues, ...props };
4783
+ const row2 = this.rows.get(id);
4784
+ if (!row2) return;
4785
+ row2.values = { ...row2.values, ...values };
4786
+ if (props) row2.propValues = { ...row2.propValues, ...props };
4701
4787
  }
4702
4788
  /**
4703
4789
  * Reflect an indicator's live status in its legend row: `'loading'` shows three pulsing
@@ -4705,9 +4791,9 @@ var InputsUI = class {
4705
4791
  * at legend scale), `'live'` a pulsing dot, `'idle'` nothing. Rendered just right of the title.
4706
4792
  */
4707
4793
  setStatus(id, status) {
4708
- const row = this.rows.get(id);
4709
- if (!row) return;
4710
- const el = row.statusEl;
4794
+ const row2 = this.rows.get(id);
4795
+ if (!row2) return;
4796
+ const el = row2.statusEl;
4711
4797
  el.replaceChildren();
4712
4798
  if (status === "idle") {
4713
4799
  el.style.cssText = "display:none;box-sizing:border-box;flex:none;";
@@ -4736,15 +4822,15 @@ var InputsUI = class {
4736
4822
  }
4737
4823
  /** Mark a row hidden/shown — swaps the eye glyph and dims the row; does NOT remove it. */
4738
4824
  setVisible(id, visible) {
4739
- const row = this.rows.get(id);
4740
- if (!row) return;
4741
- row.hidden = !visible;
4742
- row.el.style.opacity = visible ? "1" : "0.5";
4743
- if (row.eyeEl) {
4744
- row.eyeEl.innerHTML = visible ? EYE_SVG : EYE_OFF_SVG;
4745
- row.eyeEl.setAttribute("aria-label", visible ? "Hide" : "Show");
4825
+ const row2 = this.rows.get(id);
4826
+ if (!row2) return;
4827
+ row2.hidden = !visible;
4828
+ row2.el.style.opacity = visible ? "1" : "0.5";
4829
+ if (row2.eyeEl) {
4830
+ row2.eyeEl.innerHTML = visible ? EYE_SVG : EYE_OFF_SVG;
4831
+ row2.eyeEl.setAttribute("aria-label", visible ? "Hide" : "Show");
4746
4832
  }
4747
- this.syncRowActions(row);
4833
+ this.syncRowActions(row2);
4748
4834
  }
4749
4835
  /**
4750
4836
  * Select a legend row (outlining it) and clear any previous selection, so at most one
@@ -4763,32 +4849,32 @@ var InputsUI = class {
4763
4849
  * visible even when idle, so it can be un-hidden without hovering.
4764
4850
  */
4765
4851
  setRowHighlighted(id, highlighted) {
4766
- const row = this.rows.get(id);
4767
- if (!row) return;
4768
- row.highlighted = highlighted;
4769
- this.renderRowValues(row);
4770
- row.el.style.boxShadow = highlighted ? `inset 0 0 0 1px ${this.neutralBorder()}` : "none";
4771
- row.el.style.background = highlighted ? this.theme.background : this.idleRowFill();
4772
- this.syncRowActions(row);
4852
+ const row2 = this.rows.get(id);
4853
+ if (!row2) return;
4854
+ row2.highlighted = highlighted;
4855
+ this.renderRowValues(row2);
4856
+ row2.el.style.boxShadow = highlighted ? `inset 0 0 0 1px ${this.neutralBorder()}` : "none";
4857
+ row2.el.style.background = highlighted ? this.theme.background : this.idleRowFill();
4858
+ this.syncRowActions(row2);
4773
4859
  }
4774
4860
  /**
4775
4861
  * Reveal the action cluster (or just the eye for a hidden indicator). Non-eye
4776
4862
  * buttons only appear while highlighted; the eye keeps its inline-flex from
4777
4863
  * construction and is shown/hidden with the cluster container.
4778
4864
  */
4779
- syncRowActions(row) {
4780
- const open = row.highlighted;
4781
- row.controlsEl.style.display = open || row.hidden ? "inline-flex" : "none";
4865
+ syncRowActions(row2) {
4866
+ const open = row2.highlighted;
4867
+ row2.controlsEl.style.display = open || row2.hidden ? "inline-flex" : "none";
4782
4868
  if (open) {
4783
- row.el.appendChild(row.statusEl);
4784
- for (const bubble of row.callouts) bubble.hidePanel();
4869
+ row2.el.appendChild(row2.statusEl);
4870
+ for (const bubble of row2.callouts) bubble.hidePanel();
4785
4871
  } else {
4786
- row.el.insertBefore(row.statusEl, row.valuesEl);
4872
+ row2.el.insertBefore(row2.statusEl, row2.valuesEl);
4787
4873
  }
4788
- row.calloutsEl.style.display = legendCalloutsDisplay(open, row.callouts.length > 0);
4789
- for (const child of Array.from(row.controlsEl.children)) {
4790
- if (!(child instanceof HTMLElement) || child === row.eyeEl) continue;
4791
- if (child === row.extrasEl) {
4874
+ row2.calloutsEl.style.display = legendCalloutsDisplay(open, row2.callouts.length > 0);
4875
+ for (const child of Array.from(row2.controlsEl.children)) {
4876
+ if (!(child instanceof HTMLElement) || child === row2.eyeEl) continue;
4877
+ if (child === row2.extrasEl) {
4792
4878
  child.style.display = open ? "contents" : "none";
4793
4879
  continue;
4794
4880
  }
@@ -4802,9 +4888,9 @@ var InputsUI = class {
4802
4888
  this.selectedId = null;
4803
4889
  }
4804
4890
  remove(id) {
4805
- const row = this.rows.get(id);
4806
- for (const bubble of row?.callouts ?? []) bubble.destroy();
4807
- row?.el.remove();
4891
+ const row2 = this.rows.get(id);
4892
+ for (const bubble of row2?.callouts ?? []) bubble.destroy();
4893
+ row2?.el.remove();
4808
4894
  this.rows.delete(id);
4809
4895
  this.disposeTips(this.rowTips, id);
4810
4896
  this.disposeTips(this.extrasTips, id);
@@ -4812,11 +4898,11 @@ var InputsUI = class {
4812
4898
  if (this.selectedId === id) this.selectedId = null;
4813
4899
  if (this.inputsDialog.openId === id) this.inputsDialog.close();
4814
4900
  this.syncFoldToggle();
4815
- if (row && row.paneId !== "price") {
4816
- const lg = this.legends.get(row.paneId);
4901
+ if (row2 && row2.paneId !== "price") {
4902
+ const lg = this.legends.get(row2.paneId);
4817
4903
  if (lg && lg.childElementCount === 0) {
4818
4904
  lg.remove();
4819
- this.legends.delete(row.paneId);
4905
+ this.legends.delete(row2.paneId);
4820
4906
  }
4821
4907
  }
4822
4908
  }
@@ -4829,7 +4915,7 @@ var InputsUI = class {
4829
4915
  for (const id of [...this.rowTips.keys()]) this.disposeTips(this.rowTips, id);
4830
4916
  for (const id of [...this.extrasTips.keys()]) this.disposeTips(this.extrasTips, id);
4831
4917
  for (const id of [...this.calloutTips.keys()]) this.disposeTips(this.calloutTips, id);
4832
- for (const row of this.rows.values()) for (const bubble of row.callouts) bubble.destroy();
4918
+ for (const row2 of this.rows.values()) for (const bubble of row2.callouts) bubble.destroy();
4833
4919
  for (const lg of this.legends.values()) lg.remove();
4834
4920
  this.legends.clear();
4835
4921
  this.rows.clear();
@@ -4850,8 +4936,8 @@ var InputsUI = class {
4850
4936
  this.inputsDialog.close();
4851
4937
  }
4852
4938
  openDialog(id) {
4853
- const row = this.rows.get(id);
4854
- if (row) this.inputsDialog.open(row);
4939
+ const row2 = this.rows.get(id);
4940
+ if (row2) this.inputsDialog.open(row2);
4855
4941
  }
4856
4942
  /** Idle legend-row fill — a translucent wash of the chart background, so the title keeps
4857
4943
  * contrast when candles reach it without laying a solid block over the plot. */
@@ -5023,9 +5109,9 @@ var PaneControls = class {
5023
5109
  const soloChip = stateChipRole != null && !hovered;
5024
5110
  cluster.style.background = soloChip ? "transparent" : CLUSTER_PILL;
5025
5111
  for (const child of cluster.children) {
5026
- const btn2 = child;
5027
- btn2.style.display = "inline-flex";
5028
- btn2.style.visibility = soloChip && btn2.dataset.role !== stateChipRole ? "hidden" : "visible";
5112
+ const btn = child;
5113
+ btn.style.display = "inline-flex";
5114
+ btn.style.visibility = soloChip && btn.dataset.role !== stateChipRole ? "hidden" : "visible";
5029
5115
  }
5030
5116
  }
5031
5117
  }
@@ -7217,6 +7303,11 @@ var InputController = class {
7217
7303
  /** When true (the default), the separators between stacked panes are draggable to
7218
7304
  * resize the panes above/below. When false, a press there is a normal data-area pan. */
7219
7305
  this.paneResize = true;
7306
+ /** When true (the default), the user drawings take part in pointer input: they claim a
7307
+ * press over a drawing or with a tool armed, and see hover moves. When false, every press
7308
+ * is a plain pan/click and hover leaves them alone; a drawing gesture already under way
7309
+ * still finishes. */
7310
+ this.drawings = true;
7220
7311
  this.el = null;
7221
7312
  this.dragging = false;
7222
7313
  this.moved = false;
@@ -7280,18 +7371,18 @@ var InputController = class {
7280
7371
  this.onModifier = (e) => {
7281
7372
  if (e.repeat || e.key !== "Shift" && e.key !== "Control" && e.key !== "Meta") return;
7282
7373
  if (Number.isNaN(this.cursorX)) return;
7283
- if (this.dragging && this.region !== "drawing") return;
7374
+ if (this.dragging ? this.region !== "drawing" : !this.drawings) return;
7284
7375
  this.deps.drawingsPointerMove?.(this.cursorX, this.cursorY, this.snapMode(e), e.shiftKey, e.ctrlKey || e.metaKey);
7285
7376
  };
7286
7377
  this.onDown = (e) => {
7287
7378
  if (e.button === 1) {
7288
7379
  const { x: x2, y: y2 } = this.local(e);
7289
- this.middleDeleted = this.deps.drawingsDeleteAt?.(x2, y2) ?? false;
7380
+ this.middleDeleted = this.drawings && (this.deps.drawingsDeleteAt?.(x2, y2) ?? false);
7290
7381
  if (this.middleDeleted) e.preventDefault();
7291
7382
  return;
7292
7383
  }
7293
7384
  if (e.button === 2) {
7294
- this.rightCancelled = this.deps.drawingsCancelPlacement?.() ?? false;
7385
+ this.rightCancelled = this.drawings && (this.deps.drawingsCancelPlacement?.() ?? false);
7295
7386
  if (this.rightCancelled) e.preventDefault();
7296
7387
  return;
7297
7388
  }
@@ -7313,20 +7404,20 @@ var InputController = class {
7313
7404
  this.startY = y;
7314
7405
  this.prevPressDrawing = this.lastPressDrawing;
7315
7406
  this.lastPressDrawing = false;
7316
- if (this.deps.drawingsClaim?.(x, y)) {
7407
+ if (this.drawings && this.deps.drawingsClaim?.(x, y)) {
7317
7408
  this.region = "drawing";
7318
7409
  this.lastPressDrawing = true;
7319
7410
  this.deps.drawingsPointerDown?.(x, y, this.snapMode(e), e.shiftKey, e.ctrlKey || e.metaKey);
7320
7411
  this.capture(e.pointerId);
7321
7412
  return;
7322
7413
  }
7323
- if (e.shiftKey && this.regionAt(x, y) === "data" && this.deps.drawingsMeasureStart?.(x, y, this.snapMode(e))) {
7414
+ if (this.drawings && e.shiftKey && this.regionAt(x, y) === "data" && this.deps.drawingsMeasureStart?.(x, y, this.snapMode(e))) {
7324
7415
  this.region = "drawing";
7325
7416
  this.lastPressDrawing = true;
7326
7417
  this.capture(e.pointerId);
7327
7418
  return;
7328
7419
  }
7329
- if ((e.ctrlKey || e.metaKey) && this.regionAt(x, y) === "data" && this.deps.drawingsMarqueeStart?.(x, y)) {
7420
+ if (this.drawings && (e.ctrlKey || e.metaKey) && this.regionAt(x, y) === "data" && this.deps.drawingsMarqueeStart?.(x, y)) {
7330
7421
  this.region = "drawing";
7331
7422
  this.lastPressDrawing = true;
7332
7423
  this.capture(e.pointerId);
@@ -7403,7 +7494,7 @@ var InputController = class {
7403
7494
  const drawCursor = this.deps.drawingsCursor?.(x, y);
7404
7495
  const r = this.regionAt(x, y);
7405
7496
  this.el.style.cursor = drawCursor ?? (r === "price" ? "ns-resize" : r === "time" ? "ew-resize" : r === "separator" ? "row-resize" : "");
7406
- this.deps.drawingsPointerMove?.(x, y, this.snapMode(e), e.shiftKey, e.ctrlKey || e.metaKey);
7497
+ if (this.drawings) this.deps.drawingsPointerMove?.(x, y, this.snapMode(e), e.shiftKey, e.ctrlKey || e.metaKey);
7407
7498
  }
7408
7499
  if (e.pointerType !== "touch") this.deps.onPointerMove(x, y);
7409
7500
  };
@@ -7645,7 +7736,7 @@ var InputController = class {
7645
7736
  }
7646
7737
  /** Shared double-click / double-tap routing, by the region under the point. */
7647
7738
  doubleActivate(x, y) {
7648
- if (this.deps.drawingsDblClick?.(x, y)) return;
7739
+ if (this.drawings && this.deps.drawingsDblClick?.(x, y)) return;
7649
7740
  const region = this.regionAt(x, y);
7650
7741
  if (region === "price") this.deps.resetPriceScale(x, y);
7651
7742
  else if (region === "separator") this.deps.resetPaneSize(y);
@@ -9695,10 +9786,13 @@ function priceTicks(min, max, target = 6) {
9695
9786
  const norm = raw / mag;
9696
9787
  const step = (norm < 1.5 ? 1 : norm < 3 ? 2 : norm < 7 ? 5 : 10) * mag;
9697
9788
  const decimals = Math.max(0, -Math.floor(Math.log10(step)) + 1);
9698
- const out = [];
9699
9789
  const start = Math.ceil(min / step) * step;
9700
- for (let v = start; v <= max + step * 1e-6; v += step) {
9701
- out.push(Number(v.toFixed(decimals)));
9790
+ if (!(step > 0) || start + step === start) return [];
9791
+ const count = Math.floor((max - start) / step + 1e-6) + 1;
9792
+ if (!(count > 0) || count > 1e3) return [];
9793
+ const out = [];
9794
+ for (let i = 0; i < count; i += 1) {
9795
+ out.push(Number((start + i * step).toFixed(decimals)));
9702
9796
  }
9703
9797
  return out;
9704
9798
  }
@@ -9896,6 +9990,100 @@ function calendarTicks(zFrom, zTo, months, offsetMs) {
9896
9990
  return out;
9897
9991
  }
9898
9992
 
9993
+ // src/renderers/native/chrome/priceAxisTicks.ts
9994
+ var PriceAxisTickSource = class {
9995
+ constructor() {
9996
+ this.fn = null;
9997
+ this.memo = /* @__PURE__ */ new WeakMap();
9998
+ this.warned = false;
9999
+ }
10000
+ /** The host function, or null for the built-in ticks. */
10001
+ get hook() {
10002
+ return this.fn;
10003
+ }
10004
+ /** Set (or clear) the host function. Drops every memoized result, so setting the same
10005
+ * function again recomputes — how a host refreshes ticks that depend on its own state. */
10006
+ setHook(fn) {
10007
+ this.fn = fn;
10008
+ this.memo = /* @__PURE__ */ new WeakMap();
10009
+ }
10010
+ /** The pane's master-scale ticks for this frame. An unscaled pane (`axisFormat: 'none'`)
10011
+ * gets none, and the host is never asked for it. */
10012
+ ticksFor(scene, pane, coords) {
10013
+ const format = pane.axisFormat;
10014
+ if (format === "none") return [];
10015
+ const pct = percentScaleFor(scene, pane);
10016
+ const height = pane.bounds.height;
10017
+ const fn = this.fn;
10018
+ if (!fn || !(height > 0)) return paneAxisTicks(pane.scale, height, pct, scene.priceMintick, format);
10019
+ const s = pane.scale;
10020
+ const m = this.memo.get(pane);
10021
+ const fontSize = scene.style.fontSize;
10022
+ if (m && m.kind === pane.kind && m.min === s.min && m.max === s.max && m.log === !!s.log && m.invert === !!s.invert && m.height === height && m.baseline === pct?.baseline && m.indexed === !!pct?.indexed && m.mintick === scene.priceMintick && m.format === format && m.fontSize === fontSize) {
10023
+ return m.ticks;
10024
+ }
10025
+ const defaults = paneAxisTicks(s, height, pct, scene.priceMintick, format);
10026
+ const ticks = this.callHook(fn, scene, pane, coords, defaults, pct?.baseline, pct ? pct.indexed ? "indexed" : "percent" : "price");
10027
+ this.memo.set(pane, {
10028
+ kind: pane.kind,
10029
+ min: s.min,
10030
+ max: s.max,
10031
+ log: !!s.log,
10032
+ invert: !!s.invert,
10033
+ height,
10034
+ baseline: pct?.baseline,
10035
+ indexed: !!pct?.indexed,
10036
+ mintick: scene.priceMintick,
10037
+ format,
10038
+ fontSize,
10039
+ ticks
10040
+ });
10041
+ return ticks;
10042
+ }
10043
+ callHook(fn, scene, pane, coords, defaults, baseline, mode) {
10044
+ const scale = { min: pane.scale.min, max: pane.scale.max, log: !!pane.scale.log, invert: !!pane.scale.invert };
10045
+ const bounds = { top: 0, height: pane.bounds.height };
10046
+ const ctx = {
10047
+ pane: { id: pane.id, kind: pane.kind },
10048
+ min: Math.min(scale.min, scale.max),
10049
+ max: Math.max(scale.min, scale.max),
10050
+ log: !!scale.log,
10051
+ height: bounds.height,
10052
+ mode,
10053
+ baseline,
10054
+ mintick: scene.priceMintick,
10055
+ fontSize: scene.style.fontSize,
10056
+ priceToY: (price) => coords.priceToY(price, scale, bounds),
10057
+ defaults
10058
+ };
10059
+ let out;
10060
+ try {
10061
+ out = fn(ctx);
10062
+ } catch (err) {
10063
+ this.warnOnce("threw", err);
10064
+ return defaults;
10065
+ }
10066
+ if (out == null) return defaults;
10067
+ if (!Array.isArray(out)) {
10068
+ this.warnOnce("returned a non-array", out);
10069
+ return defaults;
10070
+ }
10071
+ const ticks = [];
10072
+ for (const t of out) {
10073
+ if (!t || typeof t !== "object") continue;
10074
+ const { price, label, major } = t;
10075
+ if (typeof price !== "number" || !Number.isFinite(price) || typeof label !== "string") continue;
10076
+ ticks.push(typeof major === "boolean" ? { price, label, major } : { price, label });
10077
+ }
10078
+ return ticks;
10079
+ }
10080
+ warnOnce(what, detail) {
10081
+ if (this.warned) return;
10082
+ this.warned = true;
10083
+ console.warn(`[vela] the priceAxisTicks function ${what} \u2014 the default ticks are used instead.`, detail);
10084
+ }
10085
+ };
10086
+
9899
10087
  // src/renderers/native/chrome/countdown.ts
9900
10088
  function countdownText(barOpen, barMs, now2) {
9901
10089
  if (!(barMs > 0)) return null;
@@ -9937,12 +10125,12 @@ var MARK_DECK_STEP = 3;
9937
10125
  var MARK_FAN_GAP = 4;
9938
10126
  var MARK_HIT_PAD = 3;
9939
10127
  var MARK_FAN_HOLD = 8;
9940
- function snapMarkBar(time, barTimes, intervalMs2) {
10128
+ function snapMarkBar(time, barTimes, intervalMs) {
9941
10129
  const n = barTimes.length;
9942
- if (n === 0 || !(intervalMs2 > 0) || !Number.isFinite(time)) return null;
10130
+ if (n === 0 || !(intervalMs > 0) || !Number.isFinite(time)) return null;
9943
10131
  if (time < barTimes[0]) return null;
9944
10132
  const last = barTimes[n - 1];
9945
- if (time >= last + intervalMs2) return n - 1 + Math.floor((time - last) / intervalMs2);
10133
+ if (time >= last + intervalMs) return n - 1 + Math.floor((time - last) / intervalMs);
9946
10134
  let lo = 0;
9947
10135
  let hi = n - 1;
9948
10136
  while (lo < hi) {
@@ -9950,14 +10138,14 @@ function snapMarkBar(time, barTimes, intervalMs2) {
9950
10138
  if (barTimes[mid] <= time) lo = mid;
9951
10139
  else hi = mid - 1;
9952
10140
  }
9953
- if (time < barTimes[lo] + intervalMs2) return lo;
10141
+ if (time < barTimes[lo] + intervalMs) return lo;
9954
10142
  return lo + 1;
9955
10143
  }
9956
- function clusterMarks(marks, barTimes, intervalMs2, hidden) {
10144
+ function clusterMarks(marks, barTimes, intervalMs, hidden) {
9957
10145
  const byKey = /* @__PURE__ */ new Map();
9958
10146
  marks.forEach((m, seq) => {
9959
10147
  if (m.group !== void 0 && hidden(m.group)) return;
9960
- const bar = snapMarkBar(m.time, barTimes, intervalMs2);
10148
+ const bar = snapMarkBar(m.time, barTimes, intervalMs);
9961
10149
  if (bar === null) return;
9962
10150
  const key = `${bar}|${m.group ?? ""}`;
9963
10151
  let c = byKey.get(key);
@@ -10248,8 +10436,11 @@ function standaloneSvg(markup, ink, px) {
10248
10436
  }
10249
10437
 
10250
10438
  // src/renderers/native/chrome/ChromeRenderer.ts
10439
+ var MINOR_LABEL_ALPHA = 0.5;
10251
10440
  var ChromeRenderer = class {
10252
- constructor() {
10441
+ /** `axisTicks` is shared with the backdrop layer, so price labels and gridlines agree. */
10442
+ constructor(axisTicks = new PriceAxisTickSource()) {
10443
+ this.axisTicks = axisTicks;
10253
10444
  this.canvas = null;
10254
10445
  this.ctx = null;
10255
10446
  // The color for axis tick labels — the host-passed surface text, set each frame in render().
@@ -10354,7 +10545,8 @@ var ChromeRenderer = class {
10354
10545
  this.drawTimeAxis(ctx, scene, coords, theme, dataW, dataH, fullH);
10355
10546
  this.drawMarkLane(ctx, scene, coords, theme, dataW, dataH);
10356
10547
  }
10357
- /** The timeline-mark lane — after the axis, so the tokens read over the plot's bottom edge. */
10548
+ /** The timeline-mark lane — after the axis, so the tokens read over the plot's bottom edge,
10549
+ * but clipped to the data area so a glyph on the edge bar never paints over the price scale. */
10358
10550
  drawMarkLane(ctx, scene, coords, theme, dataW, dataH) {
10359
10551
  if (!scene.marks.visible || scene.timelineMarks.length === 0) {
10360
10552
  this.markLayout = { glyphs: [], stacks: /* @__PURE__ */ new Map() };
@@ -10372,6 +10564,10 @@ var ChromeRenderer = class {
10372
10564
  expanded: scene.marksExpandedStack
10373
10565
  });
10374
10566
  const nowMs = typeof performance !== "undefined" ? performance.now() : Date.now();
10567
+ ctx.save();
10568
+ ctx.beginPath();
10569
+ ctx.rect(0, 0, dataW, dataH);
10570
+ ctx.clip();
10375
10571
  paintMarkLane(ctx, this.markLayout, {
10376
10572
  axisY: dataH,
10377
10573
  background: theme.background,
@@ -10385,6 +10581,7 @@ var ChromeRenderer = class {
10385
10581
  flashKey: scene.marksFlash && scene.marksFlash.until > nowMs ? scene.marksFlash.key : null,
10386
10582
  nowMs
10387
10583
  });
10584
+ ctx.restore();
10388
10585
  }
10389
10586
  barTimes(scene) {
10390
10587
  if (this.barTimesSrc !== scene.bars || this.barTimesCache.length !== scene.bars.length) {
@@ -10441,13 +10638,19 @@ var ChromeRenderer = class {
10441
10638
  if (!scene.showAxisLabels) return;
10442
10639
  ctx.fillStyle = this.axisTextColor;
10443
10640
  ctx.textAlign = "left";
10641
+ const font = `${scene.style.fontSize}px ${theme.fontFamily}`;
10642
+ const majorFont = `600 ${font}`;
10643
+ const alpha = ctx.globalAlpha;
10444
10644
  for (const pane of panes) {
10445
10645
  if (pane.collapsed) continue;
10446
- const pct = percentScaleFor(scene, pane);
10447
- for (const t of paneAxisTicks(pane.scale, pane.bounds.height, pct, scene.priceMintick, pane.axisFormat)) {
10646
+ for (const t of this.axisTicks.ticksFor(scene, pane, coords)) {
10448
10647
  const y = coords.priceToY(t.price, pane.scale, pane.bounds);
10449
10648
  if (y < pane.bounds.top + 6 || y > pane.bounds.top + pane.bounds.height - 4) continue;
10649
+ if (t.major === true) ctx.font = majorFont;
10650
+ else if (t.major === false) ctx.globalAlpha = alpha * MINOR_LABEL_ALPHA;
10450
10651
  ctx.fillText(t.label, dataW + 6, y);
10652
+ if (t.major === true) ctx.font = font;
10653
+ else if (t.major === false) ctx.globalAlpha = alpha;
10451
10654
  }
10452
10655
  if (pane.axisBands) {
10453
10656
  for (const b of pane.axisBands) {
@@ -10689,15 +10892,23 @@ var CrosshairRenderer = class {
10689
10892
  constructor() {
10690
10893
  this.canvas = null;
10691
10894
  this.ctx = null;
10895
+ this.shade = null;
10692
10896
  }
10693
10897
  mount(canvas) {
10694
10898
  this.canvas = canvas;
10695
10899
  this.ctx = canvas.getContext("2d");
10696
10900
  }
10901
+ /** The plot area the last frame veiled for `crosshairOverride.shadeRight` — from the
10902
+ * picked bar's right edge to the price scale, full plot height (the union when both the
10903
+ * local and a synced crosshair veil) — or null when no veil was painted. */
10904
+ get shadeRightArea() {
10905
+ return this.shade;
10906
+ }
10697
10907
  /** Clear the cursor canvas and (re)draw the crosshair lines + axis chips. The optional
10698
10908
  * `separatorHoverY` highlights the draggable pane separator under the cursor;
10699
10909
  * `external` is a SYNCED ghost crosshair (another chart's pointer, pixel-resolved). */
10700
10910
  render(scene, coords, theme, separatorHoverY = null, external = null) {
10911
+ this.shade = null;
10701
10912
  const ctx = this.ctx;
10702
10913
  const canvas = this.canvas;
10703
10914
  if (!ctx || !canvas) return;
@@ -10720,12 +10931,7 @@ var CrosshairRenderer = class {
10720
10931
  const x = crisp(coords.logicalToX(logical), ov?.width ?? cs.width);
10721
10932
  if (vertical && ov?.shadeRight) {
10722
10933
  const from = Math.max(0, Math.round(coords.logicalToX(logical + 0.5)));
10723
- if (from < dataW) {
10724
- ctx.fillStyle = ov.shadeRight.color;
10725
- ctx.globalAlpha = ov.shadeRight.opacity ?? 1;
10726
- ctx.fillRect(from, 0, dataW - from, dataH);
10727
- ctx.globalAlpha = 1;
10728
- }
10934
+ if (from < dataW) this.paintShade(ctx, ov.shadeRight, from, dataW, dataH);
10729
10935
  }
10730
10936
  ctx.strokeStyle = ov?.color ?? cs.color ?? theme.textColor;
10731
10937
  ctx.lineWidth = ov?.width ?? cs.width;
@@ -10761,6 +10967,7 @@ var CrosshairRenderer = class {
10761
10967
  destroy() {
10762
10968
  this.canvas = null;
10763
10969
  this.ctx = null;
10970
+ this.shade = null;
10764
10971
  }
10765
10972
  /** The synced ghost: a dimmed vertical line at the bar the renderer resolved as
10766
10973
  * CONTAINING the foreign time (+ horizontal line when a comparable price came
@@ -10780,12 +10987,7 @@ var CrosshairRenderer = class {
10780
10987
  if (ov?.vertical === false) return;
10781
10988
  if (ov?.shadeRight) {
10782
10989
  const from = Math.max(0, Math.round(coords.logicalToX(Math.round(coords.xToLogical(ext.x)) + 0.5)));
10783
- if (from < dataW) {
10784
- ctx.fillStyle = ov.shadeRight.color;
10785
- ctx.globalAlpha = ov.shadeRight.opacity ?? 1;
10786
- ctx.fillRect(from, 0, dataW - from, dataH);
10787
- ctx.globalAlpha = 1;
10788
- }
10990
+ if (from < dataW) this.paintShade(ctx, ov.shadeRight, from, dataW, dataH);
10789
10991
  }
10790
10992
  if (ext.line === false || x < 0 || x > dataW) return;
10791
10993
  ctx.font = `${scene.style.fontSize}px ${theme.fontFamily}`;
@@ -10821,6 +11023,15 @@ var CrosshairRenderer = class {
10821
11023
  this.chip(ctx, x, dataH + 1, formatTimeStamp(ext.time, scene.timezone, coords.barInterval), chipBg, "center", true, theme.background);
10822
11024
  ctx.globalAlpha = 1;
10823
11025
  }
11026
+ /** Paint the `shadeRight` veil from `from` to the price scale and record the veiled area. */
11027
+ paintShade(ctx, shade, from, dataW, dataH) {
11028
+ ctx.fillStyle = shade.color;
11029
+ ctx.globalAlpha = shade.opacity ?? 1;
11030
+ ctx.fillRect(from, 0, dataW - from, dataH);
11031
+ ctx.globalAlpha = 1;
11032
+ const x = this.shade ? Math.min(this.shade.x, from) : from;
11033
+ this.shade = { x, width: dataW - x, height: dataH };
11034
+ }
10824
11035
  /** A soft band + a brighter crisp center line over the hovered separator, so it reads as
10825
11036
  * a draggable handle (the cursor is already `row-resize`). Spans the full width (data +
10826
11037
  * scale gutter) to match the separator itself. Theme-derived (textColor). */
@@ -11541,16 +11752,16 @@ var SettingsDialog = class {
11541
11752
  }
11542
11753
  };
11543
11754
  for (const { group: g, depth } of rows) {
11544
- const row = this.boolRow(g.label, this.markGroupOwnVisible(g.id), (v) => {
11755
+ const row2 = this.boolRow(g.label, this.markGroupOwnVisible(g.id), (v) => {
11545
11756
  this.emit({ marks: { groups: { [g.id]: v } } });
11546
11757
  refreshDimming();
11547
11758
  });
11548
11759
  if (depth > 0) {
11549
- row.classList.add("vela-sd-nested");
11550
- row.style.setProperty("--vela-sd-depth", String(depth));
11760
+ row2.classList.add("vela-sd-nested");
11761
+ row2.style.setProperty("--vela-sd-depth", String(depth));
11551
11762
  }
11552
- rowsById.set(g.id, row);
11553
- body.append(sid(row, markGroupSettingsId(g.id)));
11763
+ rowsById.set(g.id, row2);
11764
+ body.append(sid(row2, markGroupSettingsId(g.id)));
11554
11765
  }
11555
11766
  refreshDimming();
11556
11767
  }
@@ -12372,6 +12583,16 @@ var DrawingPainter = class {
12372
12583
  this.paintMagnifier(ctx, d, proj, theme);
12373
12584
  return;
12374
12585
  }
12586
+ if (d instanceof PriceActionZone) {
12587
+ this.paintPriceZone(ctx, d, proj, theme);
12588
+ this.paintLabel(ctx, d, proj, theme);
12589
+ return;
12590
+ }
12591
+ if (d instanceof Liquidity) {
12592
+ this.paintLiquidity(ctx, d, proj);
12593
+ this.paintLabel(ctx, d, proj, theme);
12594
+ return;
12595
+ }
12375
12596
  if (d instanceof PatternDrawing) {
12376
12597
  this.paintPattern(ctx, d, proj, theme);
12377
12598
  return;
@@ -12414,9 +12635,11 @@ var DrawingPainter = class {
12414
12635
  case "arrow": {
12415
12636
  const pts = d.handlePoints(proj);
12416
12637
  if (pts.length < 2) return;
12638
+ const ext = d instanceof TrendLine ? d.extension() : "none";
12639
+ const [ex1, ey1, ex2, ey2] = extendEndpoints(pts[0][0], pts[0][1], pts[1][0], pts[1][1], ext, proj.width, proj.height);
12417
12640
  this.stroke(ctx, d.style, () => {
12418
- ctx.moveTo(pts[0][0], pts[0][1]);
12419
- ctx.lineTo(pts[1][0], pts[1][1]);
12641
+ ctx.moveTo(ex1, ey1);
12642
+ ctx.lineTo(ex2, ey2);
12420
12643
  });
12421
12644
  if (d.style.arrowRight) this.paintArrowhead(ctx, pts[0], pts[1], d.style);
12422
12645
  if (d.style.arrowLeft) this.paintArrowhead(ctx, pts[1], pts[0], d.style);
@@ -12558,9 +12781,15 @@ var DrawingPainter = class {
12558
12781
  ctx.font = `${text.bold ? "bold " : ""}${text.italic ? "italic " : ""}${fs}px ${theme.fontFamily}`;
12559
12782
  ctx.textBaseline = "top";
12560
12783
  ctx.textAlign = layout.align;
12561
- ctx.fillStyle = text.color ?? theme.textColor;
12562
12784
  const lh = labelLineHeight(fs);
12563
12785
  const lines = text.value.split("\n");
12786
+ if (d.type !== "text" && d.type !== "box") {
12787
+ ctx.lineJoin = "round";
12788
+ ctx.lineWidth = 3;
12789
+ ctx.strokeStyle = theme.background;
12790
+ lines.forEach((line, i) => ctx.strokeText(line, layout.x, layout.top + i * lh));
12791
+ }
12792
+ ctx.fillStyle = text.color ?? theme.textColor;
12564
12793
  lines.forEach((line, i) => ctx.fillText(line, layout.x, layout.top + i * lh));
12565
12794
  if (d.type === "text") this.paintTextFrame(ctx, d.id, layout.x, layout.top, lines, fs, theme);
12566
12795
  ctx.restore();
@@ -12615,6 +12844,78 @@ var DrawingPainter = class {
12615
12844
  ctx.textAlign = "left";
12616
12845
  ctx.textBaseline = "alphabetic";
12617
12846
  }
12847
+ /** Paint a price-action zone (fair value gap, order block): the box and its halfway line,
12848
+ * quieter once it has run its course, a dot where it stopped, and — once price flips it —
12849
+ * the dashed box it carries on as. With no zone under the anchor, a faint tick marks it. */
12850
+ paintPriceZone(ctx, d, proj, theme) {
12851
+ const z = d.zone(proj);
12852
+ if (!z) {
12853
+ const a = d.anchors[0];
12854
+ if (!a) return;
12855
+ const x = Math.round(proj.xOf(a.time)) + 0.5;
12856
+ this.stroke(ctx, { lineColor: withAlpha(theme.textColor, 0.35), lineWidth: 1, lineStyle: "dashed" }, () => {
12857
+ ctx.moveTo(x, 0);
12858
+ ctx.lineTo(x, proj.height);
12859
+ });
12860
+ return;
12861
+ }
12862
+ const h = z.yBottom - z.yTop;
12863
+ const stroke = toHex6(z.fill, z.fill);
12864
+ ctx.save();
12865
+ ctx.globalAlpha *= z.faded ? 0.6 : 1;
12866
+ ctx.fillStyle = z.fill;
12867
+ ctx.fillRect(z.x1, z.yTop, z.x2 - z.x1, h);
12868
+ if (z.border) {
12869
+ ctx.lineWidth = 1;
12870
+ ctx.setLineDash([]);
12871
+ ctx.strokeStyle = stroke;
12872
+ ctx.strokeRect(Math.round(z.x1) + 0.5, Math.round(z.yTop) + 0.5, Math.round(z.x2 - z.x1), Math.round(h));
12873
+ }
12874
+ if (z.midY != null) {
12875
+ const y = Math.round(z.midY) + 0.5;
12876
+ this.stroke(ctx, { lineColor: stroke, lineWidth: 1, lineStyle: "dashed" }, () => {
12877
+ ctx.moveTo(z.x1, y);
12878
+ ctx.lineTo(z.x2, y);
12879
+ });
12880
+ }
12881
+ ctx.restore();
12882
+ if (z.flip) {
12883
+ const f = z.flip;
12884
+ ctx.save();
12885
+ ctx.fillStyle = f.fill;
12886
+ ctx.fillRect(f.x1, z.yTop, f.x2 - f.x1, h);
12887
+ ctx.lineWidth = 1;
12888
+ ctx.strokeStyle = toHex6(f.fill, f.fill);
12889
+ ctx.setLineDash([5, 4]);
12890
+ ctx.strokeRect(Math.round(f.x1) + 0.5, Math.round(z.yTop) + 0.5, Math.round(f.x2 - f.x1), Math.round(h));
12891
+ ctx.restore();
12892
+ }
12893
+ if (z.marker) {
12894
+ ctx.beginPath();
12895
+ ctx.arc(z.marker.x, z.marker.y, 3.5, 0, Math.PI * 2);
12896
+ ctx.fillStyle = stroke;
12897
+ ctx.fill();
12898
+ }
12899
+ }
12900
+ /** Paint a liquidity level: the line to where it stopped, and a cross where it was swept. */
12901
+ paintLiquidity(ctx, d, proj) {
12902
+ const l = d.line(proj);
12903
+ if (!l) return;
12904
+ const y = Math.round(l.y) + 0.5;
12905
+ this.stroke(ctx, d.style, () => {
12906
+ ctx.moveTo(l.x1, y);
12907
+ ctx.lineTo(l.x2, y);
12908
+ });
12909
+ if (l.sweep) {
12910
+ const { x, y: sy } = l.sweep;
12911
+ this.stroke(ctx, { lineColor: d.style.lineColor, lineWidth: 1.75, lineStyle: "solid" }, () => {
12912
+ ctx.moveTo(x - 4, sy - 4);
12913
+ ctx.lineTo(x + 4, sy + 4);
12914
+ ctx.moveTo(x + 4, sy - 4);
12915
+ ctx.lineTo(x - 4, sy + 4);
12916
+ });
12917
+ }
12918
+ }
12618
12919
  /** Paint a concentric-ring fib tool (circles / arcs / wedge): each enabled level as an arc of
12619
12920
  * radius R0·ratio over the tool's angular sweep, plus any bounding lines + a ratio label per ring. */
12620
12921
  paintRadialFib(ctx, d, proj, theme) {
@@ -13084,15 +13385,15 @@ var DrawingPainter = class {
13084
13385
  const { profile, maxW, anchorX, grow, yEdges } = L;
13085
13386
  if (profile.maxTotal <= 0 || maxW <= 0) return;
13086
13387
  for (let k = 0; k < profile.rows.length; k += 1) {
13087
- const row = profile.rows[k];
13088
- const total = row.up + row.down;
13388
+ const row2 = profile.rows[k];
13389
+ const total = row2.up + row2.down;
13089
13390
  if (total <= 0) continue;
13090
13391
  const yA = yEdges[k];
13091
13392
  const yB = yEdges[k + 1];
13092
13393
  const yTop = Math.min(yA, yB);
13093
13394
  const h = Math.max(1, Math.abs(yB - yA) - 1);
13094
13395
  const w2 = total / profile.maxTotal * maxW;
13095
- const upW = row.up / total * w2;
13396
+ const upW = row2.up / total * w2;
13096
13397
  const inVa = k >= profile.vaFrom && k <= profile.vaTo;
13097
13398
  const upColor = inVa ? s.vaUpColor : s.upColor;
13098
13399
  const downColor = inVa ? s.vaDownColor : s.downColor;
@@ -13557,11 +13858,20 @@ var DrawingPainter = class {
13557
13858
  ctx.arc(x1, y1, arcR, Math.min(0, end), Math.max(0, end));
13558
13859
  ctx.stroke();
13559
13860
  }
13560
- ctx.fillStyle = color;
13861
+ const label = `${angleDeg.toFixed(1)}\xB0`;
13862
+ const lx = x1 + arcR + 6;
13863
+ const ly = dy < 0 ? y1 + 14 : y1 - 6;
13561
13864
  ctx.font = `11px ${theme.fontFamily}`;
13562
13865
  ctx.textAlign = "left";
13563
13866
  ctx.textBaseline = "alphabetic";
13564
- ctx.fillText(`${angleDeg.toFixed(1)}\xB0`, x1 + arcR + 6, y1 + (dy < 0 ? -14 : 2));
13867
+ ctx.save();
13868
+ ctx.lineJoin = "round";
13869
+ ctx.lineWidth = 3;
13870
+ ctx.strokeStyle = theme.background;
13871
+ ctx.strokeText(label, lx, ly);
13872
+ ctx.restore();
13873
+ ctx.fillStyle = color;
13874
+ ctx.fillText(label, lx, ly);
13565
13875
  }
13566
13876
  /** Paint a fixed-size arrow marker as a filled glyph at its anchor. */
13567
13877
  paintArrowMark(ctx, d, proj) {
@@ -13704,6 +14014,12 @@ function labelLayout(d, proj) {
13704
14014
  const pts = d.handlePoints(proj);
13705
14015
  const lines = text.value.split("\n").length;
13706
14016
  const lh = labelLineHeight(namedFontSize(text.size));
14017
+ if (d instanceof ArrowMark) {
14018
+ const b = d.bounds(proj);
14019
+ if (!b) return null;
14020
+ const x = b.x + b.w / 2;
14021
+ return d.type === "arrowmarkdown" ? { x, top: b.y - 4 - lh * lines, align: "center" } : { x, top: b.y + b.h + 4, align: "center" };
14022
+ }
13707
14023
  switch (d.type) {
13708
14024
  case "text":
13709
14025
  return pts[0] ? { x: pts[0][0] + 2, top: pts[0][1] + 2, align: "left" } : null;
@@ -13718,11 +14034,13 @@ function labelLayout(d, proj) {
13718
14034
  if (pts.length < 2) return null;
13719
14035
  const [x1, y1] = pts[0];
13720
14036
  const [x2, y2] = pts[1];
14037
+ const t = text.place === "start" ? 0.12 : text.place === "end" ? 0.88 : 0.5;
14038
+ const below = text.side === "below";
13721
14039
  return {
13722
14040
  x: 0,
13723
- top: -6 - lh * lines,
14041
+ top: below ? 6 : -6 - lh * lines,
13724
14042
  align: "center",
13725
- rotate: { angle: uprightLineAngle(x1, y1, x2, y2), cx: (x1 + x2) / 2, cy: (y1 + y2) / 2 }
14043
+ rotate: { angle: uprightLineAngle(x1, y1, x2, y2), cx: x1 + (x2 - x1) * t, cy: y1 + (y2 - y1) * t }
13726
14044
  };
13727
14045
  }
13728
14046
  case "box": {
@@ -13903,8 +14221,9 @@ var DrawingInteraction = class {
13903
14221
  const tool = this.deps.activeTool();
13904
14222
  if (tool) {
13905
14223
  const paneId = proj.paneIdAtY(y) ?? "price";
13906
- const draft = createDrawing(tool, { paneId, anchors: [this.resolve(x, y, paneId, mode)], style: this.deps.lastStyle() });
14224
+ const draft = createDrawing(tool, { paneId, anchors: [this.resolve(x, y, paneId, mode)] });
13907
14225
  if (!draft) return;
14226
+ applyToolDefaults(draft, this.deps.toolDefaults());
13908
14227
  this.state = { kind: "placing", draft, need: draft.anchorSchema().max, cursor: null };
13909
14228
  if (draft.placementMode() === "click" && draft.anchors.length >= this.state.need) this.finalize();
13910
14229
  else this.deps.changed();
@@ -14103,7 +14422,8 @@ var DrawingInteraction = class {
14103
14422
  if (this.state.kind !== "placing") return null;
14104
14423
  const { draft, cursor } = this.state;
14105
14424
  if (!cursor) return draft.anchors.length >= 1 && draft.type === "hline" ? draft : null;
14106
- return createDrawing(draft.type, { paneId: draft.paneId, anchors: [...draft.anchors, this.placementAnchor(draft, cursor)], style: draft.style });
14425
+ const { paneId, style, text, props } = draft.serialize();
14426
+ return createDrawing(draft.type, { paneId, anchors: [...draft.anchors, this.placementAnchor(draft, cursor)], style, text, props });
14107
14427
  }
14108
14428
  /** Map a raw placement cursor to the anchor the drawing actually stores.
14109
14429
  * Parallel channel: its 3rd anchor is the parallel-line price measured AT THE
@@ -14207,7 +14527,7 @@ function applyFree(anchor, pt, free) {
14207
14527
  if (free === "both" || free === "y") anchor.price = pt.price;
14208
14528
  }
14209
14529
 
14210
- // src/renderers/native/drawings/DrawingSettingsDialog.ts
14530
+ // src/renderers/native/drawings/DrawingSettingsSections.ts
14211
14531
  var LEVEL_UNITS = [
14212
14532
  { value: "price", label: "Price" },
14213
14533
  { value: "points", label: "Points" }
@@ -14216,15 +14536,319 @@ var FRVP_ANCHOR = [
14216
14536
  { value: "right", label: "Right" },
14217
14537
  { value: "left", label: "Left" }
14218
14538
  ];
14219
- var TITLES = {
14220
- position: "Position size",
14221
- frvp: "Volume profile",
14222
- levels: "Levels"
14539
+ function buildPositionSection(grid, drawing, actions, theme) {
14540
+ const live = () => {
14541
+ const d = actions.resolve();
14542
+ return d instanceof PositionTool ? d : drawing;
14543
+ };
14544
+ const refreshers = [];
14545
+ const refreshAll = () => refreshers.forEach((f) => f());
14546
+ const fmt = (n) => Math.round(n * 1e8) / 1e8;
14547
+ const numberRow = (label, path, clamp) => {
14548
+ const ctrl = buildFieldControl({
14549
+ kind: "number",
14550
+ value: fmt(live()[path]),
14551
+ min: clamp?.min,
14552
+ max: clamp?.max,
14553
+ step: clamp?.step,
14554
+ integer: clamp?.integer,
14555
+ fill: false,
14556
+ commit: "blur",
14557
+ onChange: (n) => {
14558
+ actions.patch({ [path]: n });
14559
+ refreshAll();
14560
+ }
14561
+ });
14562
+ refreshers.push(() => {
14563
+ if (document.activeElement !== ctrl.el.querySelector("input")) ctrl.setValue?.(fmt(live()[path]));
14564
+ });
14565
+ grid.appendChild(fieldRow({ label, control: ctrl.el }));
14566
+ };
14567
+ const levelRow = (label, path, level) => {
14568
+ let mode = "price";
14569
+ const display = () => fmt(live().levelDisplayValue(level, mode));
14570
+ const ni = buildFieldControl({
14571
+ kind: "number",
14572
+ value: display(),
14573
+ fill: false,
14574
+ commit: "blur",
14575
+ onChange: (n) => {
14576
+ actions.patch({ [path]: live().levelPriceFromDisplay(level, mode, n) });
14577
+ refreshAll();
14578
+ }
14579
+ });
14580
+ const sel = buildFieldControl({
14581
+ kind: "select",
14582
+ options: LEVEL_UNITS,
14583
+ value: mode,
14584
+ fill: false,
14585
+ theme,
14586
+ onChange: (v) => {
14587
+ mode = v;
14588
+ ni.setValue?.(display());
14589
+ }
14590
+ });
14591
+ refreshers.push(() => {
14592
+ if (document.activeElement !== ni.el.querySelector("input")) ni.setValue?.(display());
14593
+ });
14594
+ grid.appendChild(fieldRow({ label, control: [ni.el, sel.el] }));
14595
+ };
14596
+ grid.appendChild(fieldSection("Account", { variant: "inputs", first: true }));
14597
+ numberRow("Risk %", "riskPercent", { min: 0, max: 100, step: 0.1 });
14598
+ numberRow("Account balance", "accountBalance", { min: 0, max: 1e12, step: 1, integer: true });
14599
+ numberRow("Position size", "quantity");
14600
+ grid.appendChild(fieldSection("Levels", { variant: "inputs" }));
14601
+ const dir = buildFieldControl({
14602
+ kind: "select",
14603
+ options: DIRECTION_OPTIONS,
14604
+ value: live().direction,
14605
+ fill: false,
14606
+ theme,
14607
+ onChange: (v) => {
14608
+ actions.patch({ direction: v });
14609
+ refreshAll();
14610
+ }
14611
+ });
14612
+ refreshers.push(() => dir.setValue?.(live().direction));
14613
+ grid.appendChild(fieldRow({ label: "Direction", control: dir.el }));
14614
+ numberRow("Entry price", "entryPrice");
14615
+ levelRow("Stop", "stopPrice", "stop");
14616
+ levelRow("Target", "targetPrice", "target");
14617
+ grid.appendChild(fieldSection("Display", { variant: "inputs" }));
14618
+ const toggles = [
14619
+ ["Show text", "showText"],
14620
+ ["Show direction & ratio", "showHeader"],
14621
+ ["Show loss & size", "showLossSize"],
14622
+ ["Show target label", "showTargetLabel"],
14623
+ ["Show stop label", "showStopLabel"],
14624
+ ["Show level prices", "showPrices"]
14625
+ ];
14626
+ for (const [label, path] of toggles) {
14627
+ grid.appendChild(fieldRow({
14628
+ label,
14629
+ bool: true,
14630
+ toggle: {
14631
+ checked: live()[path],
14632
+ onChange: (v) => {
14633
+ actions.patch({ [path]: v });
14634
+ refreshAll();
14635
+ },
14636
+ get: () => live()[path]
14637
+ }
14638
+ }));
14639
+ }
14640
+ const summary = document.createElement("div");
14641
+ summary.className = "vela-field-span";
14642
+ summary.style.cssText = "opacity:0.7;font-size:11px;line-height:1.4;border-top:1px solid var(--vela-border);padding-top:6px;font-variant-numeric:tabular-nums;";
14643
+ refreshers.push(() => {
14644
+ const d = live();
14645
+ summary.textContent = `${d.headerLabel()} \u2014 ${d.lossSizeLabel()}`;
14646
+ });
14647
+ grid.appendChild(summary);
14648
+ refreshAll();
14649
+ }
14650
+ function buildProfileSection(grid, drawing, actions, theme) {
14651
+ const styleOf = () => {
14652
+ const d = actions.resolve();
14653
+ return d instanceof FixedRangeVolumeProfile ? d.frvp : drawing.frvp;
14654
+ };
14655
+ const s = styleOf();
14656
+ const numberRow = (label, path, min, max, step, integer = true) => {
14657
+ grid.appendChild(fieldRow({
14658
+ label,
14659
+ control: buildFieldControl({
14660
+ kind: "number",
14661
+ value: s[path],
14662
+ min,
14663
+ max,
14664
+ step,
14665
+ integer,
14666
+ fill: false,
14667
+ commit: "blur",
14668
+ onChange: (n) => actions.patch({ [`frvp.${path}`]: n })
14669
+ }).el
14670
+ }));
14671
+ };
14672
+ numberRow("Rows", "rows", 1, 500, 1);
14673
+ numberRow("Value Area", "valueAreaPct", 0, 100, 1);
14674
+ numberRow("Width %", "widthPct", 0, 100, 1);
14675
+ grid.appendChild(fieldRow({
14676
+ label: "Anchor",
14677
+ control: buildFieldControl({
14678
+ kind: "select",
14679
+ options: FRVP_ANCHOR,
14680
+ value: s.anchor,
14681
+ fill: false,
14682
+ theme,
14683
+ onChange: (v) => actions.patch({ "frvp.anchor": v })
14684
+ }).el
14685
+ }));
14686
+ const colorRow = (label, path) => {
14687
+ grid.appendChild(fieldRow({
14688
+ label,
14689
+ fit: true,
14690
+ control: buildFieldControl({
14691
+ kind: "color",
14692
+ theme,
14693
+ get: () => styleOf()[path],
14694
+ onChange: (v) => actions.patch({ [`frvp.${path}`]: v })
14695
+ }).el
14696
+ }));
14697
+ };
14698
+ colorRow("Up Volume", "upColor");
14699
+ colorRow("Down Volume", "downColor");
14700
+ colorRow("Value Area Up", "vaUpColor");
14701
+ colorRow("Value Area Down", "vaDownColor");
14702
+ const styles = LINE_STYLE_OPTIONS.map((o) => ({ value: o.value, label: o.label }));
14703
+ const levelRow = (label, showPath, colorPath, stylePath) => {
14704
+ const row2 = document.createElement("div");
14705
+ row2.className = "vela-field-span";
14706
+ row2.style.cssText = "display:flex;align-items:center;gap:8px;";
14707
+ const sw = buildFieldControl({
14708
+ kind: "switch",
14709
+ checked: Boolean(s[showPath]),
14710
+ onChange: (v) => actions.patch({ [`frvp.${showPath}`]: v })
14711
+ });
14712
+ const lbl = document.createElement("span");
14713
+ lbl.className = "vela-field-label";
14714
+ lbl.style.flex = "1";
14715
+ lbl.textContent = label;
14716
+ let cur = s[colorPath] ?? contrastColor(theme.background);
14717
+ const col = buildFieldControl({
14718
+ kind: "color",
14719
+ theme,
14720
+ get: () => cur,
14721
+ onChange: (v) => {
14722
+ cur = v;
14723
+ actions.patch({ [`frvp.${colorPath}`]: v });
14724
+ }
14725
+ });
14726
+ const style = buildFieldControl({
14727
+ kind: "select",
14728
+ options: styles,
14729
+ value: s[stylePath],
14730
+ fill: false,
14731
+ theme,
14732
+ onChange: (v) => actions.patch({ [`frvp.${stylePath}`]: v })
14733
+ });
14734
+ row2.append(sw.el, lbl, col.el, style.el);
14735
+ grid.appendChild(row2);
14736
+ };
14737
+ levelRow("VAH", "showVah", "vahColor", "vahStyle");
14738
+ levelRow("VAL", "showVal", "valColor", "valStyle");
14739
+ levelRow("POC", "showPoc", "pocColor", "pocStyle");
14740
+ levelRow("Developing POC", "showDevelopingPoc", "developingPocColor", "developingPocStyle");
14741
+ levelRow("Developing VA", "showDevelopingVa", "developingVaColor", "developingVaStyle");
14742
+ }
14743
+
14744
+ // src/renderers/native/drawings/DrawingSettingsPanel.ts
14745
+ var LINE_LABEL_TYPES = /* @__PURE__ */ new Set(["trendline", "ray", "extendedline", "infoline", "trendangle"]);
14746
+ var NO_POINTS = /* @__PURE__ */ new Set(["freehand", "highlighter", "vline", "position"]);
14747
+ var MAX_POINTS = 8;
14748
+ var POSITION_PATHS = /* @__PURE__ */ new Set(["riskPercent", "accountBalance", "quantity", "direction", "entryPrice", "stopPrice", "targetPrice", "showText", "showHeader", "showLossSize", "showTargetLabel", "showStopLabel", "showPrices"]);
14749
+ var LEVEL_PATHS = /* @__PURE__ */ new Set(["showRatios"]);
14750
+ var INTRADAY = ["s", "1", "5", "15", "60", "240"];
14751
+ var HIGHER = ["60", "240", "D", "W", "M"];
14752
+ var openFolds = /* @__PURE__ */ new Map();
14753
+ var STYLE_ID4 = "vela-dsp-styles";
14754
+ var STYLE_REV = "1";
14755
+ var CHEVRON = '<svg viewBox="0 0 12 12" width="12" height="12" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="m4.5 2.5 3.5 3.5-3.5 3.5"/></svg>';
14756
+ var CHEVRON_DOWN = '<svg viewBox="0 0 12 12" width="12" height="12" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="m2.5 4.5 3.5 3.5 3.5-3.5"/></svg>';
14757
+ var CROSS = '<svg viewBox="0 0 16 16" width="14" height="14" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"><path d="m4.5 4.5 7 7M11.5 4.5l-7 7"/></svg>';
14758
+ var EXTEND_ICONS = {
14759
+ none: '<svg viewBox="0 0 18 14" width="18" height="14" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"><path d="M5 7h8"/><circle cx="5" cy="7" r="1.5" fill="currentColor"/><circle cx="13" cy="7" r="1.5" fill="currentColor"/></svg>',
14760
+ left: '<svg viewBox="0 0 18 14" width="18" height="14" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"><path d="M1 7h12"/><path d="m3.5 4.5-2.5 2.5 2.5 2.5"/><circle cx="13" cy="7" r="1.5" fill="currentColor"/></svg>',
14761
+ right: '<svg viewBox="0 0 18 14" width="18" height="14" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"><path d="M5 7h12"/><path d="m14.5 4.5 2.5 2.5-2.5 2.5"/><circle cx="5" cy="7" r="1.5" fill="currentColor"/></svg>',
14762
+ both: '<svg viewBox="0 0 18 14" width="18" height="14" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"><path d="M1 7h16"/><path d="m3.5 4.5-2.5 2.5 2.5 2.5M14.5 4.5l2.5 2.5-2.5 2.5"/></svg>'
14223
14763
  };
14224
- var DrawingSettingsDialog = class {
14225
- constructor(host, theme) {
14764
+ var widthIcon = (w) => `<svg viewBox="0 0 18 14" width="18" height="14"><rect x="2" y="${7 - w / 2}" width="14" height="${w}" rx="${w / 2}" fill="currentColor"/></svg>`;
14765
+ var dashIcon = (s) => `<svg viewBox="0 0 18 14" width="18" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-dasharray="${s === "dashed" ? "4 3" : s === "dotted" ? "0.1 3.5" : "0"}"><path d="M2 7h14"/></svg>`;
14766
+ function ensureStyles4() {
14767
+ if (typeof document === "undefined") return;
14768
+ const existing = document.getElementById(STYLE_ID4);
14769
+ if (existing?.dataset.rev === STYLE_REV) return;
14770
+ const s = existing ?? document.createElement("style");
14771
+ s.id = STYLE_ID4;
14772
+ s.dataset.rev = STYLE_REV;
14773
+ s.textContent = `
14774
+ .vela-dsp-pass { pointer-events: none !important; }
14775
+ .vela-dialog.vela-dsp { pointer-events: auto; width: 360px; min-width: 0; max-width: calc(100% - 24px); max-height: calc(100% - 24px); font-size: 13px; }
14776
+ .vela-dsp .vela-dialog-header { padding: 8px 8px 8px 16px; gap: 6px; }
14777
+ .vela-dsp .vela-dialog-title { font-size: 15px; line-height: 22px; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
14778
+ .vela-dsp .vela-dialog-footer { padding: 10px 12px; display: flex; justify-content: flex-end; gap: 8px; border-top: 1px solid var(--vela-border); }
14779
+ .vela-dsp .vela-dialog-btn { height: 30px; font-size: 13px; }
14780
+ .vela-dsp-scroll { overflow-y: auto; overflow-x: hidden; flex: 1 1 auto; min-height: 0; }
14781
+ .vela-dsp-block { padding: 12px 16px; display: flex; flex-direction: column; gap: 8px; }
14782
+ .vela-dsp-block + .vela-dsp-block, .vela-dsp-fold { border-top: 1px solid var(--vela-border); }
14783
+ .vela-dsp-h { font-size: 11px; font-weight: 600; letter-spacing: 0.05em; text-transform: uppercase; color: var(--vela-fg-muted); }
14784
+ .vela-dsp-row { display: flex; align-items: center; gap: 8px; min-height: 30px; }
14785
+ .vela-dsp-row[hidden] { display: none; }
14786
+ .vela-dsp-lab { flex: 0 0 96px; color: var(--vela-fg-muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
14787
+ .vela-dsp-row > .vela-dsp-lab + * { min-width: 0; }
14788
+ .vela-dsp-spacer { flex: 1; }
14789
+ .vela-dsp-fold-h { all: unset; box-sizing: border-box; width: 100%; display: flex; align-items: center; gap: 8px; padding: 11px 16px; cursor: pointer; }
14790
+ .vela-dsp-fold-h:hover { background: var(--vela-hover); }
14791
+ .vela-dsp-fold-h:focus-visible { outline: 2px solid var(--vela-selected-bg); outline-offset: -2px; }
14792
+ .vela-dsp-fold-t { font-weight: 600; color: var(--vela-fg-bright); white-space: nowrap; }
14793
+ .vela-dsp-fold-s { flex: 1; min-width: 0; text-align: right; color: var(--vela-fg-muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; font-variant-numeric: tabular-nums; }
14794
+ .vela-dsp-chev { display: flex; color: var(--vela-fg-muted); transition: transform var(--vela-dur-fast) ease; }
14795
+ .vela-dsp-fold[data-open] .vela-dsp-chev { transform: rotate(90deg); }
14796
+ .vela-dsp-fold-b { padding: 0 16px 12px; display: flex; flex-direction: column; gap: 8px; }
14797
+ .vela-dsp-fold:not([data-open]) .vela-dsp-fold-b { display: none; }
14798
+ .vela-dsp-seg { display: inline-flex; flex: none; border: 1px solid var(--vela-border-strong); border-radius: 6px; overflow: hidden; }
14799
+ .vela-dsp-seg[data-full] { display: flex; flex: 1; }
14800
+ .vela-dsp-seg > button { all: unset; box-sizing: border-box; cursor: pointer; height: 28px; min-width: 32px; padding: 0 8px; display: flex; align-items: center; justify-content: center; color: var(--vela-fg-muted); white-space: nowrap; }
14801
+ .vela-dsp-seg[data-full] > button { flex: 1 1 0; min-width: 0; }
14802
+ .vela-dsp-seg > button + button { border-left: 1px solid var(--vela-border); }
14803
+ .vela-dsp-seg > button:hover { background: var(--vela-hover); color: var(--vela-fg-bright); }
14804
+ .vela-dsp-seg > button[aria-pressed='true'] { background: var(--vela-active); color: var(--vela-fg-bright); }
14805
+ .vela-dsp-seg > button:focus-visible { outline: 2px solid var(--vela-selected-bg); outline-offset: -2px; }
14806
+ .vela-dsp-chips { display: flex; flex-wrap: wrap; gap: 6px; }
14807
+ .vela-dsp-chip { all: unset; box-sizing: border-box; cursor: pointer; height: 26px; padding: 0 10px; border-radius: 13px; border: 1px solid var(--vela-border-strong); color: var(--vela-fg-muted); display: inline-flex; align-items: center; gap: 6px; white-space: nowrap; }
14808
+ .vela-dsp-chip:hover { color: var(--vela-fg-bright); border-color: var(--vela-fg-muted); }
14809
+ .vela-dsp-chip[aria-pressed='true'] { background: var(--vela-active); border-color: transparent; color: var(--vela-fg-bright); }
14810
+ .vela-dsp-chip:focus-visible { outline: 2px solid var(--vela-selected-bg); outline-offset: 1px; }
14811
+ .vela-dsp-note { font-size: 12px; color: var(--vela-fg-muted); line-height: 1.45; }
14812
+ .vela-dsp-pt { display: grid; grid-template-columns: 34px minmax(0, 120px) minmax(0, 1fr); align-items: center; gap: 8px; min-height: 30px; }
14813
+ .vela-dsp-pt-k { font-weight: 600; color: var(--vela-fg-muted); }
14814
+ .vela-dsp-pt-t { color: var(--vela-fg-muted); font-size: 12px; text-align: right; font-variant-numeric: tabular-nums; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
14815
+ .vela-dsp-tf[hidden], .vela-dsp-note[hidden] { display: none; }
14816
+ .vela-dsp-tf { display: grid; grid-template-columns: repeat(5, minmax(0, 1fr)); gap: 4px; }
14817
+ .vela-dsp-tf > button { all: unset; box-sizing: border-box; cursor: pointer; position: relative; height: 28px; border-radius: 6px; border: 1px solid var(--vela-border-strong); color: var(--vela-fg-muted); display: flex; align-items: center; justify-content: center; font-variant-numeric: tabular-nums; }
14818
+ .vela-dsp-tf > button:hover { color: var(--vela-fg-bright); border-color: var(--vela-fg-muted); }
14819
+ .vela-dsp-tf > button[aria-pressed='true'] { background: var(--vela-active); border-color: transparent; color: var(--vela-fg-bright); }
14820
+ .vela-dsp-tf > button[data-here]::after { content: ''; position: absolute; top: 3px; right: 3px; width: 5px; height: 5px; border-radius: 50%; background: var(--vela-selected-bg); }
14821
+ .vela-dsp-grid { padding: 0; }
14822
+ .vela-dsp-lv { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 6px 14px; }
14823
+ .vela-dsp-lv-c { display: flex; align-items: center; gap: 6px; min-width: 0; }
14824
+ .vela-dsp-lv-c .vela-num:not([data-fill])[data-compact] { width: 76px; }
14825
+ .vela-dsp-lv-c .vela-num input { height: 28px; }
14826
+ .vela-dsp-lv-c[data-off] .vela-num input { color: var(--vela-fg-muted); }
14827
+ .vela-dsp-style { all: unset; box-sizing: border-box; cursor: pointer; flex: none; height: 28px; max-width: 150px; padding: 0 6px 0 10px; border-radius: 6px; border: 1px solid var(--vela-border-strong); display: inline-flex; align-items: center; gap: 4px; color: var(--vela-fg); font-size: 12px; }
14828
+ .vela-dsp-style > span:first-child { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
14829
+ .vela-dsp-style:hover, .vela-dsp-style[aria-expanded='true'] { background: var(--vela-hover); color: var(--vela-fg-bright); }
14830
+ .vela-dsp-menu { min-width: 220px; max-height: 320px; overflow-y: auto; padding: 4px; display: flex; flex-direction: column; gap: 1px; }
14831
+ .vela-dsp-mh { padding: 6px 8px 4px; font-size: 11px; font-weight: 600; letter-spacing: 0.05em; text-transform: uppercase; color: var(--vela-fg-muted); }
14832
+ .vela-dsp-mi { all: unset; box-sizing: border-box; cursor: pointer; display: flex; align-items: center; gap: 8px; padding: 0 4px 0 8px; height: 30px; border-radius: 5px; color: inherit; }
14833
+ .vela-dsp-mi:hover, .vela-dsp-mi:focus-visible { background: var(--vela-hover-strong); }
14834
+ .vela-dsp-mi[data-current] { color: var(--vela-fg-bright); font-weight: 600; }
14835
+ .vela-dsp-mi > .vela-dsp-mi-t { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
14836
+ .vela-dsp-mi-dot { flex: none; width: 14px; height: 14px; border-radius: 4px; border: 1px solid var(--vela-border-strong); }
14837
+ .vela-dsp-mi-x { all: unset; cursor: pointer; flex: none; width: 24px; height: 24px; border-radius: 4px; display: flex; align-items: center; justify-content: center; color: var(--vela-fg-muted); }
14838
+ .vela-dsp-mi-x:hover { background: var(--vela-hover); color: var(--vela-danger); }
14839
+ .vela-dsp-msep { height: 1px; background: var(--vela-border); margin: 4px 0; }
14840
+ .vela-dsp-mname { display: flex; gap: 6px; padding: 4px; }
14841
+ .vela-dsp-mname > input { flex: 1; min-width: 0; height: 28px; box-sizing: border-box; padding: 0 8px; border-radius: 6px; border: 1px solid var(--vela-border-strong); background: transparent; color: inherit; font: inherit; outline: none; }
14842
+ .vela-dsp-mname > input:focus { border-color: var(--vela-selected-bg); }`;
14843
+ if (!existing) document.head.appendChild(s);
14844
+ }
14845
+ var DrawingSettingsPanel = class {
14846
+ constructor(host, theme, env) {
14226
14847
  this.host = host;
14848
+ this.env = env;
14227
14849
  this.ui = null;
14850
+ this.menu = null;
14851
+ this.refresh = null;
14228
14852
  this.theme = theme;
14229
14853
  }
14230
14854
  setTheme(theme) {
@@ -14234,68 +14858,97 @@ var DrawingSettingsDialog = class {
14234
14858
  return this.ui != null;
14235
14859
  }
14236
14860
  contains(node) {
14237
- return this.ui?.contains(node) === true;
14861
+ return node != null && this.ui?.contains(node) === true;
14862
+ }
14863
+ /** Re-read the drawing after a change made outside the panel (a drag, an undo). */
14864
+ sync() {
14865
+ this.refresh?.();
14238
14866
  }
14239
- open(drawing, actions, kind) {
14867
+ open(drawing, actions, anchor) {
14240
14868
  this.close();
14241
- const snapshot = clonePlain(drawing.serialize());
14242
- const grid = fieldGrid({ variant: "inputs" });
14243
- grid.style.padding = "16px 20px";
14244
- grid.style.overflowY = "auto";
14245
- grid.style.overflowX = "hidden";
14246
- grid.style.flex = "1 1 auto";
14247
- if (kind === "position" && drawing instanceof PositionTool) this.buildPosition(grid, drawing, actions);
14248
- else if (kind === "frvp" && drawing instanceof FixedRangeVolumeProfile) this.buildFrvp(grid, drawing, actions);
14249
- else if (kind === "levels") this.buildLevels(grid, drawing, actions);
14250
- else return;
14869
+ ensureStyles4();
14870
+ const first = actions.resolve() ?? drawing;
14871
+ const snapshot = clonePlain(first.serialize());
14872
+ const live = () => actions.resolve() ?? first;
14873
+ const meta = getDrawingType(first.type);
14874
+ const scroll = document.createElement("div");
14875
+ scroll.className = "vela-dsp-scroll";
14876
+ let watchers = [];
14877
+ const runWatchers = () => {
14878
+ for (const fn of watchers) fn();
14879
+ };
14880
+ const build = {
14881
+ live,
14882
+ edit: (p) => {
14883
+ actions.patch(p);
14884
+ runWatchers();
14885
+ },
14886
+ watch: (fn) => {
14887
+ watchers.push(fn);
14888
+ fn();
14889
+ },
14890
+ rebuild: () => {
14891
+ const top = scroll.scrollTop;
14892
+ watchers = [];
14893
+ scroll.replaceChildren(...this.sections(build));
14894
+ scroll.scrollTop = top;
14895
+ styleBtn.refresh();
14896
+ },
14897
+ actions
14898
+ };
14899
+ const cancel = () => {
14900
+ actions.preview?.(null);
14901
+ actions.restore?.(snapshot);
14902
+ this.close();
14903
+ };
14251
14904
  const ui = new Dialog({
14252
14905
  host: this.host,
14253
- title: TITLES[kind],
14254
- // Non-modal: live-edit dialog — a modal machine locks pointer events on the
14255
- // whole body, killing the chart and the body-portaled popovers.
14906
+ title: meta?.label ?? "Settings",
14256
14907
  modal: false,
14257
14908
  contained: true,
14258
14909
  align: "center",
14259
14910
  draggable: true,
14260
14911
  flush: true,
14261
- className: "vela-dialog--form",
14912
+ className: "vela-dsp",
14262
14913
  closeOnEscape: false,
14263
- footer: (foot) => {
14264
- foot.append(
14265
- btn("Cancel", false, () => {
14266
- actions.restore?.(snapshot);
14267
- this.close();
14268
- }),
14269
- btn("Ok", true, () => this.close())
14270
- );
14271
- },
14914
+ footer: (foot) => foot.append(button("Cancel", false, cancel), button("Ok", true, () => this.close())),
14272
14915
  onOpenChange: (open) => {
14273
14916
  if (!open) this.close();
14274
14917
  }
14275
14918
  });
14276
14919
  applyChromeTokens(ui.panel, this.theme);
14277
- ui.body.appendChild(grid);
14278
- const onBackdropDown = (e) => {
14279
- if (e.target !== ui.backdrop && e.target !== ui.positioner) return;
14280
- if (isPopoverOpen() || eventDismissedPopover(e)) {
14281
- closeOpenPopovers();
14282
- return;
14283
- }
14284
- this.close();
14285
- };
14286
- ui.backdrop.addEventListener("pointerdown", onBackdropDown);
14287
- ui.positioner.addEventListener("pointerdown", onBackdropDown);
14920
+ ui.backdrop.classList.add("vela-dsp-pass");
14921
+ ui.positioner.classList.add("vela-dsp-pass");
14922
+ const hostW = this.host.clientWidth;
14923
+ const leftHalf = anchor != null && hostW > 0 && anchor.x + anchor.w / 2 < hostW / 2;
14924
+ ui.positioner.style.justifyContent = leftHalf ? "flex-end" : "flex-start";
14925
+ ui.positioner.style.padding = "0 12px";
14926
+ const styleBtn = this.styleMenuButton(build);
14927
+ ui.titleEl.after(styleBtn.el);
14928
+ ui.body.appendChild(scroll);
14288
14929
  ui.panel.addEventListener("keydown", (e) => {
14289
14930
  if (e.key === "Escape") {
14290
14931
  e.preventDefault();
14291
- actions.restore?.(snapshot);
14292
- this.close();
14932
+ if (this.menu) this.menu.hide();
14933
+ else cancel();
14293
14934
  }
14294
14935
  });
14295
14936
  this.ui = ui;
14937
+ this.refresh = () => {
14938
+ if (!actions.resolve()) {
14939
+ this.close();
14940
+ return;
14941
+ }
14942
+ runWatchers();
14943
+ styleBtn.refresh();
14944
+ };
14945
+ build.rebuild();
14296
14946
  ui.show();
14297
14947
  }
14298
14948
  close() {
14949
+ this.menu?.hide();
14950
+ this.menu = null;
14951
+ this.refresh = null;
14299
14952
  const ui = this.ui;
14300
14953
  this.ui = null;
14301
14954
  ui?.destroy();
@@ -14303,292 +14956,771 @@ var DrawingSettingsDialog = class {
14303
14956
  destroy() {
14304
14957
  this.close();
14305
14958
  }
14306
- buildPosition(grid, drawing, actions) {
14307
- const live = () => {
14308
- const d = actions.resolve();
14309
- return d instanceof PositionTool ? d : drawing;
14310
- };
14311
- const refreshers = [];
14312
- const refreshAll = () => refreshers.forEach((f) => f());
14313
- const fmt = (n) => Math.round(n * 1e8) / 1e8;
14314
- const numberRow = (label, path, clamp) => {
14315
- const ctrl = buildFieldControl({
14316
- kind: "number",
14317
- value: fmt(live()[path]),
14318
- min: clamp?.min,
14319
- max: clamp?.max,
14320
- step: clamp?.step,
14321
- integer: clamp?.integer,
14322
- fill: false,
14323
- commit: "blur",
14324
- onChange: (n) => {
14325
- actions.patch({ [path]: n });
14326
- refreshAll();
14327
- }
14328
- });
14329
- refreshers.push(() => {
14330
- if (document.activeElement !== ctrl.el.querySelector("input")) ctrl.setValue?.(fmt(live()[path]));
14331
- });
14332
- grid.appendChild(fieldRow({ label, control: ctrl.el }));
14333
- };
14334
- const levelRow = (label, path, level) => {
14335
- let mode = "price";
14336
- const display = () => fmt(live().levelDisplayValue(level, mode));
14337
- const ni = buildFieldControl({
14338
- kind: "number",
14339
- value: display(),
14340
- fill: false,
14341
- commit: "blur",
14342
- onChange: (n) => {
14343
- actions.patch({ [path]: live().levelPriceFromDisplay(level, mode, n) });
14344
- refreshAll();
14345
- }
14346
- });
14347
- const sel = buildFieldControl({
14348
- kind: "select",
14349
- options: LEVEL_UNITS,
14350
- value: mode,
14351
- fill: false,
14352
- theme: this.theme,
14353
- onChange: (v) => {
14354
- mode = v;
14355
- ni.setValue?.(display());
14356
- }
14357
- });
14358
- refreshers.push(() => {
14359
- if (document.activeElement !== ni.el.querySelector("input")) ni.setValue?.(display());
14360
- });
14361
- grid.appendChild(fieldRow({ label, control: [ni.el, sel.el] }));
14362
- };
14363
- grid.appendChild(fieldSection("Account", { variant: "inputs", first: true }));
14364
- numberRow("Risk %", "riskPercent", { min: 0, max: 100, step: 0.1 });
14365
- numberRow("Account balance", "accountBalance", { min: 0, max: 1e12, step: 1, integer: true });
14366
- numberRow("Position size", "quantity");
14367
- grid.appendChild(fieldSection("Levels", { variant: "inputs" }));
14368
- const dir = buildFieldControl({
14369
- kind: "select",
14370
- options: DIRECTION_OPTIONS,
14371
- value: live().direction,
14372
- fill: false,
14373
- theme: this.theme,
14374
- onChange: (v) => {
14375
- actions.patch({ direction: v });
14376
- refreshAll();
14959
+ // ── sections ──
14960
+ sections(b) {
14961
+ const d = b.live();
14962
+ const schema = d.schema();
14963
+ const fields = schema.fields;
14964
+ const has = (path) => fields.some((f) => f.path === path);
14965
+ const out = [];
14966
+ if (schema.textIsContent && has("text.value")) out.push(this.textContentBlock(b, has));
14967
+ const style = this.styleBlock(b, fields, has);
14968
+ if (style) out.push(style);
14969
+ if (d.editableLevels()) out.push(this.levelsBlock(b));
14970
+ if (d instanceof PositionTool) out.push(this.richBlock("Position", (grid) => buildPositionSection(grid, d, b.actions, this.theme)));
14971
+ if (d instanceof FixedRangeVolumeProfile) out.push(this.richBlock("Profile", (grid) => buildProfileSection(grid, d, b.actions, this.theme)));
14972
+ const own = this.optionsBlock(b, fields, d);
14973
+ if (own) out.push(own);
14974
+ if (!schema.textIsContent && fields.some((f) => f.path.startsWith("text."))) out.push(this.textFold(b, has));
14975
+ const points = this.pointsFold(b);
14976
+ if (points) out.push(points);
14977
+ out.push(this.showOnFold(b));
14978
+ return out;
14979
+ }
14980
+ /** Line, width, dash, ends and fill — the look every stroked tool shares. */
14981
+ styleBlock(b, fields, has) {
14982
+ const rows = [];
14983
+ if (has("style.lineColor") || has("style.lineWidth")) {
14984
+ const ctrls = [];
14985
+ if (has("style.lineColor")) ctrls.push(this.swatch(b, "Line color", () => b.live().style.lineColor || DEFAULT_DRAWING_COLOR, "style.lineColor"));
14986
+ const wf = fields.find((f) => f.path === "style.lineWidth");
14987
+ if (wf) {
14988
+ if ((wf.min ?? 1) > 1) ctrls.push(this.number(b, "Line width", () => b.live().style.lineWidth, "style.lineWidth", wf));
14989
+ else ctrls.push(this.seg(b, "Line width", [1, 2, 3, 4].map((w) => ({ value: w, html: widthIcon(w), title: `${w}px` })), () => b.live().style.lineWidth, (v) => b.edit({ "style.lineWidth": v })));
14990
+ }
14991
+ rows.push(row("Line", ...ctrls));
14992
+ }
14993
+ if (has("style.lineStyle")) {
14994
+ rows.push(row("Style", this.seg(b, "Line style", LINE_STYLE_OPTIONS.map((o) => ({ value: o.value, html: dashIcon(o.value), title: o.label })), () => b.live().style.lineStyle, (v) => b.edit({ "style.lineStyle": v }))));
14995
+ }
14996
+ if (has("style.extendLeft") && has("style.extendRight")) {
14997
+ const opts = [
14998
+ { value: "none", html: EXTEND_ICONS.none, title: "Don't extend" },
14999
+ { value: "left", html: EXTEND_ICONS.left, title: "Extend left" },
15000
+ { value: "right", html: EXTEND_ICONS.right, title: "Extend right" },
15001
+ { value: "both", html: EXTEND_ICONS.both, title: "Extend both ways" }
15002
+ ];
15003
+ const read = () => {
15004
+ const { extendLeft: l, extendRight: r } = b.live().style;
15005
+ return l && r ? "both" : l ? "left" : r ? "right" : "none";
15006
+ };
15007
+ rows.push(row("Extend", this.seg(b, "Extend", opts, read, (v) => b.edit({ "style.extendLeft": v === "left" || v === "both", "style.extendRight": v === "right" || v === "both" }))));
15008
+ }
15009
+ const arrows = [];
15010
+ if (has("style.arrowLeft")) arrows.push(this.chip(b, "Start arrow", () => !!b.live().style.arrowLeft, (on) => b.edit({ "style.arrowLeft": on })));
15011
+ if (has("style.arrowRight")) arrows.push(this.chip(b, has("style.arrowLeft") ? "End arrow" : "Arrow", () => !!b.live().style.arrowRight, (on) => b.edit({ "style.arrowRight": on })));
15012
+ if (arrows.length) rows.push(row("Ends", chips(...arrows)));
15013
+ if (has("style.fillColor")) {
15014
+ rows.push(row("Fill", this.swatch(b, "Fill", () => effectiveFillColor(b.live(), this.theme) ?? b.live().style.fillColor ?? DEFAULT_DRAWING_COLOR, "style.fillColor")));
15015
+ }
15016
+ if (!has("text.value") && has("text.color")) rows.push(row("Text", ...this.textStyling(b, has)));
15017
+ return rows.length ? block(null, ...rows) : null;
15018
+ }
15019
+ /** The tool's own options — every schema field the shared look and the rich sections
15020
+ * don't cover, each as the control its kind calls for. */
15021
+ optionsBlock(b, fields, d) {
15022
+ const skip = (f) => f.path.startsWith("style.") || f.path.startsWith("text.") || f.path.startsWith("frvp.") || d instanceof PositionTool && POSITION_PATHS.has(f.path) || d.editableLevels() != null && LEVEL_PATHS.has(f.path);
15023
+ const rows = [];
15024
+ const own = fields.filter((f) => !skip(f));
15025
+ for (let i = 0; i < own.length; i += 1) {
15026
+ const f = own[i];
15027
+ const next = own[i + 1];
15028
+ const base = f.kind === "color" && next?.kind === "lineStyle" ? pairBase(f.label, next.label) : f.kind === "color" && next?.kind === "color" ? upperLowerBase(f.label, next.label) : null;
15029
+ if (base) {
15030
+ const color = this.fieldControl(b, f);
15031
+ const style = this.fieldControl(b, next);
15032
+ if (color && style) rows.push(row(base, color, style));
15033
+ i += 1;
15034
+ continue;
14377
15035
  }
14378
- });
14379
- refreshers.push(() => dir.setValue?.(live().direction));
14380
- grid.appendChild(fieldRow({ label: "Direction", control: dir.el }));
14381
- numberRow("Entry price", "entryPrice");
14382
- levelRow("Stop", "stopPrice", "stop");
14383
- levelRow("Target", "targetPrice", "target");
14384
- grid.appendChild(fieldSection("Display", { variant: "inputs" }));
14385
- const toggles = [
14386
- ["Show text", "showText"],
14387
- ["Show direction & ratio", "showHeader"],
14388
- ["Show loss & size", "showLossSize"],
14389
- ["Show target label", "showTargetLabel"],
14390
- ["Show stop label", "showStopLabel"],
14391
- ["Show level prices", "showPrices"]
14392
- ];
14393
- for (const [label, path] of toggles) {
14394
- grid.appendChild(fieldRow({
14395
- label,
14396
- bool: true,
14397
- toggle: {
14398
- checked: live()[path],
14399
- onChange: (v) => {
14400
- actions.patch({ [path]: v });
14401
- refreshAll();
14402
- },
14403
- get: () => live()[path]
14404
- }
14405
- }));
15036
+ const ctrl = this.fieldControl(b, f);
15037
+ if (!ctrl) continue;
15038
+ const r = row(f.label, ctrl);
15039
+ const cond = f.when;
15040
+ if (cond) b.watch(() => r.hidden = !cond.in.includes(readPath(b.live(), cond.path)));
15041
+ rows.push(r);
15042
+ }
15043
+ const props = d.serialize().props ?? {};
15044
+ const sizes = TEXT_SIZE_OPTIONS.map((o) => ({ value: o.value, html: o.label }));
15045
+ if ("numbersSize" in props) rows.push(row("Numbers", this.seg(b, "Numbers size", sizes, () => b.live().serialize().props?.numbersSize ?? "small", (v) => b.edit({ numbersSize: v }), true)));
15046
+ if ("labelsSize" in props) rows.push(row("Labels", this.seg(b, "Labels size", sizes, () => b.live().serialize().props?.labelsSize ?? "normal", (v) => b.edit({ labelsSize: v }), true)));
15047
+ return rows.length ? block(null, ...rows) : null;
15048
+ }
15049
+ fieldControl(b, f) {
15050
+ const read = () => readPath(b.live(), f.path);
15051
+ switch (f.kind) {
15052
+ case "color":
15053
+ return this.swatch(b, f.label, () => read() || DEFAULT_DRAWING_COLOR, f.path);
15054
+ case "lineStyle":
15055
+ return this.seg(b, f.label, LINE_STYLE_OPTIONS.map((o) => ({ value: o.value, html: dashIcon(o.value), title: o.label })), () => read(), (v) => b.edit({ [f.path]: v }));
15056
+ case "boolean":
15057
+ return this.switch(b, () => read() !== false && read() != null, (v) => b.edit({ [f.path]: v }));
15058
+ case "number":
15059
+ case "opacity":
15060
+ return this.number(b, f.label, () => Number(read() ?? 0), f.path, f);
15061
+ case "select": {
15062
+ const options = f.path === "magnifier.timeframe" ? magnifierTimeframesBelow(this.env.chartBarMs()) : f.options ?? [];
15063
+ if (options.length === 0) return null;
15064
+ const sel = buildFieldControl({ kind: "select", options, value: String(read() ?? options[0].value), fill: false, theme: this.theme, onChange: (v) => b.edit({ [f.path]: v }) });
15065
+ b.watch(() => sel.setValue?.(String(read() ?? "")));
15066
+ return sel.el;
15067
+ }
15068
+ case "text": {
15069
+ const tf = buildFieldControl({ kind: "text", value: String(read() ?? ""), fill: true, onChange: (v) => b.edit({ [f.path]: v }) });
15070
+ return tf.el;
15071
+ }
15072
+ default:
15073
+ return null;
14406
15074
  }
14407
- const summary = document.createElement("div");
14408
- summary.className = "vela-field-span";
14409
- summary.style.cssText = "opacity:0.7;font-size:11px;line-height:1.4;border-top:1px solid var(--vela-border);padding-top:6px;font-variant-numeric:tabular-nums;";
14410
- refreshers.push(() => {
14411
- const d = live();
14412
- summary.textContent = `${d.headerLabel()} \u2014 ${d.lossSizeLabel()}`;
14413
- });
14414
- grid.appendChild(summary);
14415
- refreshAll();
14416
15075
  }
14417
- buildFrvp(grid, drawing, actions) {
14418
- const styleOf = () => {
14419
- const d = actions.resolve();
14420
- return d instanceof FixedRangeVolumeProfile ? d.frvp : drawing.frvp;
15076
+ /** A levelled tool's levels: every level in a two-column grid (on/off, ratio, color) — the
15077
+ * ones that are off stay listed, ready to switch on. A retracement leads with its ready-made
15078
+ * sets; the horizontal fibs follow with how their level text and fill show. */
15079
+ levelsBlock(b) {
15080
+ const d = b.live();
15081
+ const rows = [];
15082
+ const grid = document.createElement("div");
15083
+ grid.className = "vela-dsp-lv";
15084
+ const key = () => JSON.stringify(b.live().editableLevels() ?? []);
15085
+ let last = "";
15086
+ let editing = false;
15087
+ const mine = (p) => {
15088
+ editing = true;
15089
+ try {
15090
+ b.edit(p);
15091
+ } finally {
15092
+ editing = false;
15093
+ }
15094
+ last = key();
14421
15095
  };
14422
- const s = styleOf();
14423
- const numberRow = (label, path, min, max, step, integer = true) => {
14424
- grid.appendChild(fieldRow({
14425
- label,
14426
- control: buildFieldControl({
15096
+ const fill = () => {
15097
+ last = key();
15098
+ grid.replaceChildren();
15099
+ const free = b.live() instanceof FibLevels;
15100
+ (b.live().editableLevels() ?? []).forEach((lv, i) => {
15101
+ const cell = document.createElement("div");
15102
+ cell.className = "vela-dsp-lv-c";
15103
+ cell.toggleAttribute("data-off", !lv.enabled);
15104
+ const on = buildFieldControl({
15105
+ kind: "switch",
15106
+ checked: lv.enabled,
15107
+ onChange: (v) => {
15108
+ cell.toggleAttribute("data-off", !v);
15109
+ mine({ [`levels.${i}.enabled`]: v });
15110
+ }
15111
+ });
15112
+ let ratio = lv.ratio;
15113
+ const num4 = buildFieldControl({
14427
15114
  kind: "number",
14428
- value: s[path],
14429
- min,
14430
- max,
14431
- step,
14432
- integer,
15115
+ value: lv.ratio,
15116
+ min: free ? void 0 : 0,
15117
+ step: 0.01,
15118
+ compact: true,
14433
15119
  fill: false,
14434
15120
  commit: "blur",
14435
- onChange: (n) => actions.patch({ [`frvp.${path}`]: n })
14436
- }).el
14437
- }));
14438
- };
14439
- numberRow("Rows", "rows", 1, 500, 1);
14440
- numberRow("Value Area", "valueAreaPct", 0, 100, 1);
14441
- numberRow("Width %", "widthPct", 0, 100, 1);
14442
- grid.appendChild(fieldRow({
14443
- label: "Anchor",
14444
- control: buildFieldControl({
14445
- kind: "select",
14446
- options: FRVP_ANCHOR,
14447
- value: s.anchor,
14448
- fill: false,
14449
- theme: this.theme,
14450
- onChange: (v) => actions.patch({ "frvp.anchor": v })
14451
- }).el
14452
- }));
14453
- const colorRow = (label, path) => {
14454
- grid.appendChild(fieldRow({
14455
- label,
14456
- fit: true,
14457
- control: buildFieldControl({
15121
+ title: `Level ${i + 1}`,
15122
+ onChange: (n) => {
15123
+ if (!Number.isFinite(n) || !free && n <= 0) {
15124
+ num4.setValue?.(ratio);
15125
+ return;
15126
+ }
15127
+ ratio = n;
15128
+ mine({ [`levels.${i}.ratio`]: n });
15129
+ }
15130
+ });
15131
+ let color = lv.color;
15132
+ const swatch = buildFieldControl({
14458
15133
  kind: "color",
14459
15134
  theme: this.theme,
14460
- get: () => styleOf()[path],
14461
- onChange: (v) => actions.patch({ [`frvp.${path}`]: v })
14462
- }).el
14463
- }));
14464
- };
14465
- colorRow("Up Volume", "upColor");
14466
- colorRow("Down Volume", "downColor");
14467
- colorRow("Value Area Up", "vaUpColor");
14468
- colorRow("Value Area Down", "vaDownColor");
14469
- const styles = LINE_STYLE_OPTIONS.map((o) => ({ value: o.value, label: o.label }));
14470
- const levelRow = (label, showPath, colorPath, stylePath) => {
14471
- const row = document.createElement("div");
14472
- row.className = "vela-field-span";
14473
- row.style.cssText = "display:flex;align-items:center;gap:8px;";
14474
- const sw = buildFieldControl({
14475
- kind: "switch",
14476
- checked: Boolean(s[showPath]),
14477
- onChange: (v) => actions.patch({ [`frvp.${showPath}`]: v })
14478
- });
14479
- const lbl = document.createElement("span");
14480
- lbl.className = "vela-field-label";
14481
- lbl.style.flex = "1";
14482
- lbl.textContent = label;
14483
- let cur = s[colorPath] ?? contrastColor(this.theme.background);
14484
- const col = buildFieldControl({
14485
- kind: "color",
14486
- theme: this.theme,
14487
- get: () => cur,
14488
- onChange: (v) => {
14489
- cur = v;
14490
- actions.patch({ [`frvp.${colorPath}`]: v });
14491
- }
15135
+ title: `Level ${i + 1} color`,
15136
+ get: () => color,
15137
+ onChange: (v) => {
15138
+ color = v;
15139
+ mine({ [`levels.${i}.color`]: v });
15140
+ }
15141
+ });
15142
+ cell.append(on.el, num4.el, swatch.el);
15143
+ grid.appendChild(cell);
14492
15144
  });
14493
- const style = buildFieldControl({
15145
+ };
15146
+ b.watch(() => {
15147
+ if (!editing && key() !== last && !grid.contains(document.activeElement)) fill();
15148
+ });
15149
+ if (d instanceof FibRetracement) {
15150
+ rows.push(
15151
+ this.seg(
15152
+ b,
15153
+ "Level set",
15154
+ FIB_PRESETS.map((p) => ({ value: p.key, html: p.label })),
15155
+ () => {
15156
+ const f = b.live();
15157
+ return matchFibPreset(f.levels, f.reverse) ?? void 0;
15158
+ },
15159
+ (k) => {
15160
+ const preset = FIB_PRESETS.find((p) => p.key === k);
15161
+ b.edit({ levels: clonePlain(preset.levels), reverse: preset.reverse });
15162
+ },
15163
+ true
15164
+ )
15165
+ );
15166
+ }
15167
+ if (d instanceof MachFigure) rows.push(row("Ratios", this.switch(b, () => b.live().showRatios !== false, (v) => b.edit({ showRatios: v }))));
15168
+ rows.push(grid);
15169
+ if (d instanceof FibLevels) {
15170
+ const fib = () => b.live();
15171
+ const flags = [
15172
+ this.chip(b, "Ratios", () => fib().showRatios, (on) => b.edit({ showRatios: on })),
15173
+ this.chip(b, "Prices", () => fib().showPrices, (on) => b.edit({ showPrices: on })),
15174
+ this.chip(b, "Background", () => fib().background, (on) => b.edit({ background: on }))
15175
+ ];
15176
+ if (d instanceof FibRetracement) flags.push(this.chip(b, "Reverse", () => b.live().reverse, (on) => b.edit({ reverse: on })));
15177
+ rows.push(row("Show", chips(...flags)));
15178
+ rows.push(
15179
+ row(
15180
+ "Label side",
15181
+ this.seg(b, "Label side", [
15182
+ { value: "left", html: "Left" },
15183
+ { value: "right", html: "Right" }
15184
+ ], () => fib().labelSide, (v) => b.edit({ labelSide: v }))
15185
+ )
15186
+ );
15187
+ }
15188
+ return block("Levels", ...rows);
15189
+ }
15190
+ /** A rich per-type section on the shared field grid. */
15191
+ richBlock(title, fill) {
15192
+ const grid = fieldGrid({ variant: "inputs" });
15193
+ grid.classList.add("vela-dsp-grid");
15194
+ fill(grid);
15195
+ return block(title, grid);
15196
+ }
15197
+ /** A note, a callout, a text label: the words lead the panel. */
15198
+ textContentBlock(b, has) {
15199
+ return block(null, this.textArea(b, "Text", 3), row("Style", ...this.textStyling(b, has)));
15200
+ }
15201
+ /** A shape's optional label: folded to one line saying what it reads. */
15202
+ textFold(b, has) {
15203
+ const type = b.live().type;
15204
+ const words = () => b.live().text?.value?.trim() ?? "";
15205
+ const summary = () => {
15206
+ if (!has("text.value")) return sizeLabel(b.live().text?.size);
15207
+ const w = words();
15208
+ return w ? `\u201C${w.split("\n")[0]}\u201D` : "None";
15209
+ };
15210
+ return this.fold(b, "text", has("text.value") ? "Label" : "Text", summary, () => {
15211
+ const rows = [];
15212
+ if (has("text.value")) rows.push(this.textArea(b, "Add a label", 1));
15213
+ rows.push(row("Style", ...this.textStyling(b, has)));
15214
+ if (LINE_LABEL_TYPES.has(type) && has("text.value")) {
15215
+ const place = row(
15216
+ "Place",
15217
+ this.seg(b, "Position along the line", [
15218
+ { value: "start", html: "Start" },
15219
+ { value: "middle", html: "Middle" },
15220
+ { value: "end", html: "End" }
15221
+ ], () => b.live().text?.place ?? "middle", (v) => b.edit({ "text.place": v }), true)
15222
+ );
15223
+ const side = row(
15224
+ "Side",
15225
+ this.seg(b, "Side of the line", [
15226
+ { value: "above", html: "Above" },
15227
+ { value: "below", html: "Below" }
15228
+ ], () => b.live().text?.side ?? "above", (v) => b.edit({ "text.side": v }), true)
15229
+ );
15230
+ b.watch(() => place.hidden = side.hidden = words() === "");
15231
+ rows.push(place, side);
15232
+ }
15233
+ return rows;
15234
+ });
15235
+ }
15236
+ textArea(b, placeholder, rows) {
15237
+ const ta = new TextArea({
15238
+ value: b.live().text?.value ?? "",
15239
+ rows,
15240
+ size: "sm",
15241
+ autoGrow: true,
15242
+ maxLines: 6,
15243
+ placeholder,
15244
+ onChange: (v) => b.edit({ "text.value": v })
15245
+ });
15246
+ ta.input.addEventListener("keydown", (e) => {
15247
+ if (e.key !== "Escape") e.stopPropagation();
15248
+ });
15249
+ b.watch(() => {
15250
+ const v = b.live().text?.value ?? "";
15251
+ if (document.activeElement !== ta.input && ta.input.value !== v) ta.input.value = v;
15252
+ });
15253
+ ta.el.style.width = "100%";
15254
+ return ta.el;
15255
+ }
15256
+ textStyling(b, has) {
15257
+ const out = [];
15258
+ if (has("text.color")) out.push(this.swatch(b, "Text color", () => b.live().text?.color || this.theme.textColor, "text.color"));
15259
+ if (has("text.size")) {
15260
+ const sel = buildFieldControl({
14494
15261
  kind: "select",
14495
- options: styles,
14496
- value: s[stylePath],
15262
+ options: TEXT_SIZE_OPTIONS,
15263
+ value: b.live().text?.size ?? "normal",
14497
15264
  fill: false,
14498
15265
  theme: this.theme,
14499
- onChange: (v) => actions.patch({ [`frvp.${stylePath}`]: v })
15266
+ onChange: (v) => b.edit({ "text.size": v })
14500
15267
  });
14501
- row.append(sw.el, lbl, col.el, style.el);
14502
- grid.appendChild(row);
15268
+ b.watch(() => sel.setValue?.(b.live().text?.size ?? "normal"));
15269
+ out.push(sel.el);
15270
+ }
15271
+ const flags = [];
15272
+ if (has("text.bold")) flags.push(this.chip(b, "B", () => !!b.live().text?.bold, (on) => b.edit({ "text.bold": on }), "Bold"));
15273
+ if (has("text.italic")) flags.push(this.chip(b, "I", () => !!b.live().text?.italic, (on) => b.edit({ "text.italic": on }), "Italic"));
15274
+ if (flags.length) out.push(chips(...flags));
15275
+ return out;
15276
+ }
15277
+ /** Each anchor's price (editable) and time. */
15278
+ pointsFold(b) {
15279
+ const d = b.live();
15280
+ if (NO_POINTS.has(d.type) || d.anchors.length === 0 || d.anchors.length > MAX_POINTS) return null;
15281
+ const slots = d.anchorSchema().slots;
15282
+ const free = (i) => slots[i]?.free ?? slots[slots.length - 1]?.free ?? "both";
15283
+ const shown = d.anchors.map((_, i) => i).filter((i) => free(i) !== "none");
15284
+ if (shown.length === 0) return null;
15285
+ const priced = shown.filter((i) => free(i) !== "x");
15286
+ const shownPrice = (n) => {
15287
+ const dec = this.env.priceDecimals() ?? Math.max(2, valueDecimals(n));
15288
+ return Number(n.toFixed(Math.min(dec, 10)));
14503
15289
  };
14504
- levelRow("VAH", "showVah", "vahColor", "vahStyle");
14505
- levelRow("VAL", "showVal", "valColor", "valStyle");
14506
- levelRow("POC", "showPoc", "pocColor", "pocStyle");
14507
- levelRow("Developing POC", "showDevelopingPoc", "developingPocColor", "developingPocStyle");
14508
- levelRow("Developing VA", "showDevelopingVa", "developingVaColor", "developingVaStyle");
14509
- }
14510
- buildLevels(grid, drawing, actions) {
14511
- const levels = drawing.editableLevels();
14512
- if (!levels) return;
14513
- const isMach = drawing instanceof MachFigure;
14514
- if (isMach) {
14515
- const mach = drawing;
14516
- grid.appendChild(fieldRow({
14517
- label: "Show ratio labels",
14518
- bool: true,
14519
- toggle: {
14520
- checked: mach.showRatios !== false,
14521
- onChange: (v) => actions.patch({ showRatios: v })
15290
+ const fmt = (n) => String(shownPrice(n));
15291
+ const summary = () => {
15292
+ const a = b.live().anchors;
15293
+ if (priced.length) return priced.map((i) => fmt(a[i]?.price ?? 0)).join(" \u2192 ");
15294
+ const t = a[shown[0]]?.time;
15295
+ return shown.length === 1 && t != null ? formatTimeStamp(t, this.env.timeZone(), this.env.chartBarMs()) : `${shown.length} points`;
15296
+ };
15297
+ return this.fold(
15298
+ b,
15299
+ "points",
15300
+ "Points",
15301
+ summary,
15302
+ () => shown.map((i, n) => {
15303
+ const key = shown.length === 1 ? free(i) === "x" ? "Time" : "Price" : String.fromCharCode(65 + n);
15304
+ const line = document.createElement("div");
15305
+ line.className = "vela-dsp-pt";
15306
+ const k = document.createElement("span");
15307
+ k.className = "vela-dsp-pt-k";
15308
+ k.textContent = key;
15309
+ line.appendChild(k);
15310
+ if (free(i) !== "x") {
15311
+ const ni = buildFieldControl({
15312
+ kind: "number",
15313
+ value: shownPrice(d.anchors[i].price),
15314
+ fill: true,
15315
+ commit: "blur",
15316
+ title: `Point ${key} price`,
15317
+ onChange: (v) => b.edit({ [`anchors.${i}.price`]: v })
15318
+ });
15319
+ const input = ni.el.querySelector("input");
15320
+ b.watch(() => {
15321
+ if (document.activeElement !== input) ni.setValue?.(shownPrice(b.live().anchors[i]?.price ?? 0));
15322
+ });
15323
+ line.appendChild(ni.el);
15324
+ } else {
15325
+ line.appendChild(document.createElement("span"));
14522
15326
  }
14523
- }));
14524
- }
14525
- if (drawing instanceof FibRetracement) {
14526
- const fib = drawing;
14527
- grid.appendChild(fieldRow({
14528
- label: "Reverse",
14529
- bool: true,
14530
- toggle: {
14531
- checked: fib.reverse,
14532
- onChange: (v) => actions.patch({ reverse: v })
15327
+ const t = document.createElement("span");
15328
+ t.className = "vela-dsp-pt-t";
15329
+ b.watch(() => {
15330
+ const time = b.live().anchors[i]?.time;
15331
+ t.textContent = time != null ? formatTimeStamp(time, this.env.timeZone(), this.env.chartBarMs()) : "";
15332
+ });
15333
+ line.appendChild(t);
15334
+ return line;
15335
+ })
15336
+ );
15337
+ }
15338
+ /** The timeframes the drawing shows on: one line while it's every timeframe, the band
15339
+ * picker only once the user goes custom. */
15340
+ showOnFold(b) {
15341
+ const bands = () => b.live().showOn;
15342
+ let pickedCustom = false;
15343
+ const mode = () => pickedCustom && bands() ? "custom" : modeOf(bands());
15344
+ const here = timeframeBandOf(this.env.chartBarMs());
15345
+ const summary = () => {
15346
+ const s = bands();
15347
+ if (!s) return "All timeframes";
15348
+ if (mode() === "intraday") return "Intraday";
15349
+ if (mode() === "higher") return "1H and up";
15350
+ if (s.length === 0) return "Nowhere";
15351
+ return s.map((k) => TIMEFRAME_BANDS.find((t) => t.key === k)?.label ?? k).join(", ");
15352
+ };
15353
+ return this.fold(b, "show", "Show on", summary, () => {
15354
+ const set = (next) => b.edit({ showOn: next });
15355
+ const pick = this.seg(
15356
+ b,
15357
+ "Show on",
15358
+ [
15359
+ { value: "all", html: "All" },
15360
+ { value: "intraday", html: "Intraday" },
15361
+ { value: "higher", html: "1H and up" },
15362
+ { value: "custom", html: "Custom" }
15363
+ ],
15364
+ mode,
15365
+ (v) => {
15366
+ pickedCustom = v === "custom";
15367
+ if (v === "all") set(void 0);
15368
+ else if (v === "intraday") set([...INTRADAY]);
15369
+ else if (v === "higher") set([...HIGHER]);
15370
+ else set([...bands() ?? TIMEFRAME_BANDS.map((t) => t.key)]);
15371
+ },
15372
+ true
15373
+ );
15374
+ const grid = document.createElement("div");
15375
+ grid.className = "vela-dsp-tf";
15376
+ for (const band of TIMEFRAME_BANDS) {
15377
+ const btn = document.createElement("button");
15378
+ btn.type = "button";
15379
+ btn.textContent = band.label;
15380
+ if (band.key === here) {
15381
+ btn.dataset.here = "";
15382
+ btn.title = "The chart's timeframe";
14533
15383
  }
14534
- }));
14535
- }
14536
- levels.forEach((lv, i) => {
14537
- const row = document.createElement("div");
14538
- row.className = "vela-field-span";
14539
- row.style.cssText = "display:flex;align-items:center;gap:8px;";
14540
- const sw = buildFieldControl({
14541
- kind: "switch",
14542
- checked: lv.enabled,
14543
- onChange: (v) => actions.patch({ [`levels.${i}.enabled`]: v })
15384
+ btn.addEventListener("click", () => {
15385
+ const cur = new Set(bands() ?? []);
15386
+ if (cur.has(band.key)) cur.delete(band.key);
15387
+ else cur.add(band.key);
15388
+ set(TIMEFRAME_BANDS.map((t) => t.key).filter((k) => cur.has(k)));
15389
+ });
15390
+ b.watch(() => btn.setAttribute("aria-pressed", String(bands()?.includes(band.key) ?? true)));
15391
+ grid.appendChild(btn);
15392
+ }
15393
+ const note = document.createElement("div");
15394
+ note.className = "vela-dsp-note";
15395
+ b.watch(() => {
15396
+ grid.hidden = mode() !== "custom";
15397
+ const s = bands();
15398
+ const hidden = s != null && here != null && !s.includes(here);
15399
+ note.hidden = !hidden;
15400
+ note.textContent = s?.length === 0 ? "Hidden on every timeframe." : "Hidden on this chart's timeframe.";
14544
15401
  });
14545
- let curC = lv.color;
14546
- const col = buildFieldControl({
14547
- kind: "color",
14548
- theme: this.theme,
14549
- get: () => curC,
14550
- onChange: (v) => {
14551
- curC = v;
14552
- actions.patch({ [`levels.${i}.color`]: v });
15402
+ return [pick, grid, note];
15403
+ });
15404
+ }
15405
+ // ── the style menu ──
15406
+ /** The header's style menu: the tool's ready-made and saved looks (hover to preview, click
15407
+ * to apply), saving the current look, and going back to the tool's defaults. */
15408
+ styleMenuButton(b) {
15409
+ const btn = document.createElement("button");
15410
+ btn.type = "button";
15411
+ btn.className = "vela-dsp-style";
15412
+ btn.setAttribute("aria-haspopup", "menu");
15413
+ btn.setAttribute("aria-expanded", "false");
15414
+ const label = document.createElement("span");
15415
+ const chev = document.createElement("span");
15416
+ chev.style.display = "flex";
15417
+ chev.innerHTML = CHEVRON_DOWN;
15418
+ btn.append(label, chev);
15419
+ const all = () => [...builtinToolTemplates(b.live().type), ...b.actions.templates?.() ?? []];
15420
+ const refresh = () => {
15421
+ const match = all().filter((t) => matchesTemplate(b.live(), t.settings)).pop();
15422
+ label.textContent = match?.name ?? "Style";
15423
+ btn.title = match ? `Style: ${match.name}` : "Styles";
15424
+ };
15425
+ btn.addEventListener("pointerdown", (e) => e.stopPropagation());
15426
+ btn.addEventListener("click", (e) => {
15427
+ e.stopPropagation();
15428
+ if (this.menu) {
15429
+ this.menu.hide();
15430
+ return;
15431
+ }
15432
+ this.openStyleMenu(btn, b, all, refresh);
15433
+ });
15434
+ refresh();
15435
+ return { el: btn, refresh };
15436
+ }
15437
+ openStyleMenu(anchor, b, all, refresh) {
15438
+ closeOpenPopovers();
15439
+ const t = this.theme;
15440
+ const pop = new Popover({
15441
+ trigger: anchor,
15442
+ host: this.host,
15443
+ theme: t,
15444
+ position: "absolute",
15445
+ boundary: this.host,
15446
+ boundaryInset: 4,
15447
+ viewportInset: 0,
15448
+ gap: 6,
15449
+ align: "end",
15450
+ zIndex: 6e3,
15451
+ onClose: () => {
15452
+ b.actions.preview?.(null);
15453
+ anchor.setAttribute("aria-expanded", "false");
15454
+ if (this.menu === pop) this.menu = null;
15455
+ }
15456
+ });
15457
+ const el = pop.el;
15458
+ el.classList.add("vela-dsp-menu");
15459
+ el.setAttribute("role", "menu");
15460
+ el.style.cssText += `background:${t.background};border:1px solid var(--vela-border);border-radius:var(--vela-radius-lg);box-shadow:var(--vela-shadow);color:${t.textColor};font:13px ${t.fontFamily};pointer-events:auto;`;
15461
+ applyChromeTokens(el, t);
15462
+ const fill = () => {
15463
+ el.replaceChildren();
15464
+ const builtins = builtinToolTemplates(b.live().type);
15465
+ const saved = b.actions.templates?.() ?? [];
15466
+ const item = (tpl, removable) => {
15467
+ const row2 = document.createElement("button");
15468
+ row2.type = "button";
15469
+ row2.className = "vela-dsp-mi";
15470
+ row2.setAttribute("role", "menuitem");
15471
+ if (matchesTemplate(b.live(), tpl.settings)) row2.dataset.current = "";
15472
+ const dot = document.createElement("span");
15473
+ dot.className = "vela-dsp-mi-dot";
15474
+ dot.style.background = templateColor(tpl.settings) ?? "transparent";
15475
+ const name = document.createElement("span");
15476
+ name.className = "vela-dsp-mi-t";
15477
+ name.textContent = tpl.name;
15478
+ row2.append(dot, name);
15479
+ if (removable) {
15480
+ const x = document.createElement("span");
15481
+ x.className = "vela-dsp-mi-x";
15482
+ x.setAttribute("role", "button");
15483
+ x.setAttribute("aria-label", `Delete ${tpl.name}`);
15484
+ x.innerHTML = CROSS;
15485
+ x.addEventListener("click", (e) => {
15486
+ e.stopPropagation();
15487
+ b.actions.preview?.(null);
15488
+ b.actions.removeTemplate?.(tpl.name);
15489
+ fill();
15490
+ refresh();
15491
+ });
15492
+ row2.appendChild(x);
14553
15493
  }
15494
+ row2.addEventListener("pointerenter", () => b.actions.preview?.(tpl.settings));
15495
+ row2.addEventListener("pointerleave", () => b.actions.preview?.(null));
15496
+ row2.addEventListener("click", () => {
15497
+ b.actions.preview?.(null);
15498
+ b.actions.applyToolSettings?.(tpl.settings);
15499
+ pop.hide();
15500
+ b.rebuild();
15501
+ });
15502
+ return row2;
15503
+ };
15504
+ if (builtins.length) {
15505
+ el.appendChild(menuHeading("Presets"));
15506
+ for (const tpl of builtins) el.appendChild(item(tpl, false));
15507
+ }
15508
+ if (saved.length) {
15509
+ el.appendChild(menuHeading("Saved"));
15510
+ for (const tpl of saved) el.appendChild(item(tpl, true));
15511
+ }
15512
+ if (builtins.length || saved.length) el.appendChild(separator());
15513
+ if (b.actions.saveTemplate) el.appendChild(this.saveRow(b, fill, refresh));
15514
+ const reset = document.createElement("button");
15515
+ reset.type = "button";
15516
+ reset.className = "vela-dsp-mi";
15517
+ reset.setAttribute("role", "menuitem");
15518
+ reset.innerHTML = '<span class="vela-dsp-mi-t">Reset to default</span>';
15519
+ reset.addEventListener("click", () => {
15520
+ b.actions.resetSettings();
15521
+ pop.hide();
15522
+ b.rebuild();
14554
15523
  });
14555
- let curRatio = lv.ratio;
14556
- const ratio = buildFieldControl({
14557
- kind: "number",
14558
- value: curRatio,
14559
- min: 0,
14560
- step: 0.01,
14561
- fill: isMach,
14562
- compact: !isMach,
14563
- commit: "blur",
14564
- onChange: (n) => {
14565
- if (n <= 0) {
14566
- ratio.setValue?.(curRatio);
14567
- return;
14568
- }
14569
- curRatio = n;
14570
- actions.patch({ [`levels.${i}.ratio`]: n });
14571
- }
15524
+ el.appendChild(reset);
15525
+ };
15526
+ fill();
15527
+ anchor.setAttribute("aria-expanded", "true");
15528
+ this.menu = pop;
15529
+ pop.show();
15530
+ }
15531
+ /** "Save as style…": a menu row that turns into a name field. */
15532
+ saveRow(b, refill, refresh) {
15533
+ const wrap = document.createElement("div");
15534
+ const start = document.createElement("button");
15535
+ start.type = "button";
15536
+ start.className = "vela-dsp-mi";
15537
+ start.setAttribute("role", "menuitem");
15538
+ start.innerHTML = '<span class="vela-dsp-mi-t">Save as style\u2026</span>';
15539
+ start.addEventListener("click", (e) => {
15540
+ e.stopPropagation();
15541
+ const form = document.createElement("div");
15542
+ form.className = "vela-dsp-mname";
15543
+ const input = document.createElement("input");
15544
+ input.type = "text";
15545
+ input.placeholder = "Style name";
15546
+ input.maxLength = 60;
15547
+ input.setAttribute("aria-label", "Style name");
15548
+ const save = button("Save", true, () => commit());
15549
+ save.style.height = "28px";
15550
+ const commit = () => {
15551
+ const name = input.value.trim();
15552
+ if (!name) return;
15553
+ b.actions.saveTemplate?.(name);
15554
+ refill();
15555
+ refresh();
15556
+ };
15557
+ input.addEventListener("keydown", (ev) => {
15558
+ ev.stopPropagation();
15559
+ if (ev.key === "Enter") commit();
15560
+ if (ev.key === "Escape") refill();
14572
15561
  });
14573
- if (isMach) ratio.el.style.flex = "1";
14574
- row.append(sw.el, col.el, ratio.el);
14575
- if (!isMach) {
14576
- const label = buildFieldControl({
14577
- kind: "text",
14578
- value: lv.label ?? "",
14579
- fill: true,
14580
- placeholder: "label\u2026",
14581
- onChange: (v) => actions.patch({ [`levels.${i}.label`]: v })
14582
- });
14583
- label.el.style.flex = "1";
14584
- label.el.style.minWidth = "60px";
14585
- row.appendChild(label.el);
15562
+ form.append(input, save);
15563
+ wrap.replaceChildren(form);
15564
+ input.focus();
15565
+ });
15566
+ wrap.appendChild(start);
15567
+ return wrap;
15568
+ }
15569
+ // ── controls ──
15570
+ /** A fold: a one-line header saying what's set, opening to its rows. */
15571
+ fold(b, key, title, summary, body) {
15572
+ const type = b.live().type;
15573
+ const open = openFolds.get(type) ?? /* @__PURE__ */ new Set();
15574
+ openFolds.set(type, open);
15575
+ const wrap = document.createElement("div");
15576
+ wrap.className = "vela-dsp-fold";
15577
+ const head = document.createElement("button");
15578
+ head.type = "button";
15579
+ head.className = "vela-dsp-fold-h";
15580
+ const t = document.createElement("span");
15581
+ t.className = "vela-dsp-fold-t";
15582
+ t.textContent = title;
15583
+ const s = document.createElement("span");
15584
+ s.className = "vela-dsp-fold-s";
15585
+ const chev = document.createElement("span");
15586
+ chev.className = "vela-dsp-chev";
15587
+ chev.innerHTML = CHEVRON;
15588
+ head.append(t, s, chev);
15589
+ const content = document.createElement("div");
15590
+ content.className = "vela-dsp-fold-b";
15591
+ let built = false;
15592
+ const setOpen = (on) => {
15593
+ if (on) open.add(key);
15594
+ else open.delete(key);
15595
+ wrap.toggleAttribute("data-open", on);
15596
+ head.setAttribute("aria-expanded", String(on));
15597
+ if (on && !built) {
15598
+ built = true;
15599
+ content.append(...body());
14586
15600
  }
14587
- grid.appendChild(row);
15601
+ };
15602
+ head.addEventListener("click", () => setOpen(!open.has(key)));
15603
+ b.watch(() => s.textContent = summary());
15604
+ setOpen(open.has(key));
15605
+ wrap.append(head, content);
15606
+ return wrap;
15607
+ }
15608
+ seg(b, label, options, read, pick, full = false) {
15609
+ const el = document.createElement("div");
15610
+ el.className = "vela-dsp-seg";
15611
+ el.setAttribute("role", "group");
15612
+ el.setAttribute("aria-label", label);
15613
+ if (full) el.dataset.full = "";
15614
+ const buttons = options.map((o) => {
15615
+ const btn = document.createElement("button");
15616
+ btn.type = "button";
15617
+ btn.innerHTML = o.html;
15618
+ if (o.title) {
15619
+ btn.title = o.title;
15620
+ btn.setAttribute("aria-label", o.title);
15621
+ }
15622
+ btn.addEventListener("click", () => {
15623
+ pick(o.value);
15624
+ sync();
15625
+ });
15626
+ el.appendChild(btn);
15627
+ return btn;
14588
15628
  });
15629
+ const sync = () => {
15630
+ const cur = read();
15631
+ options.forEach((o, i) => buttons[i].setAttribute("aria-pressed", String(o.value === cur)));
15632
+ };
15633
+ b.watch(sync);
15634
+ return el;
15635
+ }
15636
+ chip(b, text, read, set, title) {
15637
+ const btn = document.createElement("button");
15638
+ btn.type = "button";
15639
+ btn.className = "vela-dsp-chip";
15640
+ btn.textContent = text;
15641
+ if (title) {
15642
+ btn.title = title;
15643
+ btn.setAttribute("aria-label", title);
15644
+ }
15645
+ if (text === "B") btn.style.fontWeight = "700";
15646
+ if (text === "I") btn.style.fontStyle = "italic";
15647
+ btn.addEventListener("click", () => set(!read()));
15648
+ b.watch(() => btn.setAttribute("aria-pressed", String(read())));
15649
+ return btn;
15650
+ }
15651
+ swatch(b, title, read, path) {
15652
+ const ctrl = buildFieldControl({ kind: "color", theme: this.theme, title, get: read, onChange: (v) => b.edit({ [path]: v }) });
15653
+ return ctrl.el;
15654
+ }
15655
+ switch(b, read, set) {
15656
+ const sw = buildFieldControl({ kind: "switch", checked: read(), onChange: set });
15657
+ b.watch(() => sw.setValue?.(read()));
15658
+ const wrap = document.createElement("div");
15659
+ wrap.style.cssText = "display:flex;flex:1;justify-content:flex-end;";
15660
+ wrap.appendChild(sw.el);
15661
+ return wrap;
15662
+ }
15663
+ number(b, title, read, path, f) {
15664
+ const step = f.step ?? 1;
15665
+ const ni = buildFieldControl({
15666
+ kind: "number",
15667
+ value: read(),
15668
+ min: f.min,
15669
+ max: f.max,
15670
+ step,
15671
+ integer: step >= 1 && Number.isInteger(step),
15672
+ fill: false,
15673
+ commit: "blur",
15674
+ title,
15675
+ onChange: (v) => b.edit({ [path]: v })
15676
+ });
15677
+ const input = ni.el.querySelector("input");
15678
+ b.watch(() => {
15679
+ if (document.activeElement !== input) ni.setValue?.(read());
15680
+ });
15681
+ return ni.el;
14589
15682
  }
14590
15683
  };
14591
- function btn(label, primary, onClick) {
15684
+ function block(title, ...rows) {
15685
+ const el = document.createElement("div");
15686
+ el.className = "vela-dsp-block";
15687
+ if (title) {
15688
+ const h = document.createElement("div");
15689
+ h.className = "vela-dsp-h";
15690
+ h.textContent = title;
15691
+ el.appendChild(h);
15692
+ }
15693
+ el.append(...rows);
15694
+ return el;
15695
+ }
15696
+ function row(label, ...controls) {
15697
+ const el = document.createElement("div");
15698
+ el.className = "vela-dsp-row";
15699
+ const lab = document.createElement("span");
15700
+ lab.className = "vela-dsp-lab";
15701
+ lab.textContent = label;
15702
+ lab.title = label;
15703
+ el.append(lab, ...controls);
15704
+ return el;
15705
+ }
15706
+ function chips(...items) {
15707
+ const el = document.createElement("div");
15708
+ el.className = "vela-dsp-chips";
15709
+ el.append(...items);
15710
+ return el;
15711
+ }
15712
+ function menuHeading(text) {
15713
+ const el = document.createElement("div");
15714
+ el.className = "vela-dsp-mh";
15715
+ el.textContent = text;
15716
+ return el;
15717
+ }
15718
+ function separator() {
15719
+ const el = document.createElement("div");
15720
+ el.className = "vela-dsp-msep";
15721
+ return el;
15722
+ }
15723
+ function button(label, primary, onClick) {
14592
15724
  const b = document.createElement("button");
14593
15725
  b.type = "button";
14594
15726
  b.textContent = label;
@@ -14596,6 +15728,50 @@ function btn(label, primary, onClick) {
14596
15728
  b.addEventListener("click", onClick);
14597
15729
  return b;
14598
15730
  }
15731
+ function sizeLabel(size) {
15732
+ return TEXT_SIZE_OPTIONS.find((o) => o.value === (size ?? "normal"))?.label ?? "Normal";
15733
+ }
15734
+ function readPath(d, path) {
15735
+ let cur = d;
15736
+ for (const k of path.split(".")) {
15737
+ if (cur == null || typeof cur !== "object") return void 0;
15738
+ cur = cur[k];
15739
+ }
15740
+ return cur;
15741
+ }
15742
+ function pairBase(colorLabel, styleLabel3) {
15743
+ const c = /^(.*) colou?r$/i.exec(colorLabel);
15744
+ const st = /^(.*) style$/i.exec(styleLabel3);
15745
+ return c && st && c[1] === st[1] ? c[1] : null;
15746
+ }
15747
+ function upperLowerBase(upper, lower) {
15748
+ const u = /^Upper (.+)$/i.exec(upper);
15749
+ const l = /^Lower (.+)$/i.exec(lower);
15750
+ if (!u || !l || u[1] !== l[1]) return null;
15751
+ const name = u[1].replace(/ colou?r$/i, "");
15752
+ const word = name.charAt(0).toUpperCase() + name.slice(1);
15753
+ return /fill$/i.test(word) ? word : `${word}s`;
15754
+ }
15755
+ function modeOf(showOn) {
15756
+ if (!showOn) return "all";
15757
+ const same = (a) => a.length === showOn.length && a.every((k) => showOn.includes(k));
15758
+ if (same(INTRADAY)) return "intraday";
15759
+ if (same(HIGHER)) return "higher";
15760
+ return "custom";
15761
+ }
15762
+ function matchesTemplate(d, s) {
15763
+ const same = (a, b) => JSON.stringify(a ?? null) === JSON.stringify(b ?? null);
15764
+ const style = d.style;
15765
+ for (const [k, v] of Object.entries(s.style ?? {})) if (!same(style[k], v)) return false;
15766
+ const text = d.text ?? {};
15767
+ for (const [k, v] of Object.entries(s.text ?? {})) if (!same(text[k], v)) return false;
15768
+ const props = d.serialize().props ?? {};
15769
+ for (const [k, v] of Object.entries(s.props ?? {})) if (!same(props[k], v)) return false;
15770
+ return Object.keys(s.style ?? {}).length + Object.keys(s.text ?? {}).length + Object.keys(s.props ?? {}).length > 0;
15771
+ }
15772
+ function templateColor(s) {
15773
+ return s.style?.lineColor ?? s.style?.fillColor ?? s.text?.color;
15774
+ }
14599
15775
 
14600
15776
  // src/renderers/native/drawings/DrawingSettingsPopup.ts
14601
15777
  var MIXED = /* @__PURE__ */ Symbol("mixed");
@@ -14617,15 +15793,15 @@ function distinctValues(drawings, read) {
14617
15793
  }
14618
15794
  var BTN = 30;
14619
15795
  var ICON = 21;
14620
- var STYLE_ID4 = "vela-drawing-popup-styles";
14621
- var STYLE_REV = "4";
14622
- function ensureStyles4() {
15796
+ var STYLE_ID5 = "vela-drawing-popup-styles";
15797
+ var STYLE_REV2 = "4";
15798
+ function ensureStyles5() {
14623
15799
  if (typeof document === "undefined") return;
14624
- const existing = document.getElementById(STYLE_ID4);
14625
- if (existing?.dataset.rev === STYLE_REV) return;
15800
+ const existing = document.getElementById(STYLE_ID5);
15801
+ if (existing?.dataset.rev === STYLE_REV2) return;
14626
15802
  const s = existing ?? document.createElement("style");
14627
- s.id = STYLE_ID4;
14628
- s.dataset.rev = STYLE_REV;
15803
+ s.id = STYLE_ID5;
15804
+ s.dataset.rev = STYLE_REV2;
14629
15805
  s.textContent = `
14630
15806
  .vela-dpop-btn{background:transparent;color:var(--vela-fg-muted);transition:background var(--vela-dur-fast) ease,color var(--vela-dur-fast) ease;}
14631
15807
  .vela-dpop-btn:hover{background:var(--vela-hover-strong);color:var(--vela-fg-bright);}
@@ -14641,13 +15817,14 @@ function ensureStyles4() {
14641
15817
  if (!existing) document.head.appendChild(s);
14642
15818
  }
14643
15819
  var DrawingSettingsPopup = class {
14644
- constructor(host, theme, chartBarMs = () => 0) {
15820
+ constructor(host, theme, env = {}) {
14645
15821
  this.host = host;
14646
- this.chartBarMs = chartBarMs;
14647
15822
  this.el = null;
14648
15823
  this.tipEl = null;
14649
15824
  // floating hover-label (above/below the toolbar)
14650
15825
  this.textPanel = null;
15826
+ /** The single drawing the bar is open on (a multi-selection has no settings panel). */
15827
+ this.current = null;
14651
15828
  this.colorPop = null;
14652
15829
  this.colorOwner = null;
14653
15830
  this.menuPop = null;
@@ -14655,9 +15832,10 @@ var DrawingSettingsPopup = class {
14655
15832
  this.onClose = null;
14656
15833
  this.onOutside = (e) => {
14657
15834
  const node = e.target;
14658
- if (this.settingsDialog.isOpen()) {
14659
- if (this.settingsDialog.contains(node) || this.isOwnChrome(node)) return;
14660
- this.settingsDialog.close();
15835
+ if (this.panel.isOpen()) {
15836
+ if (this.panel.contains(node) || this.isOwnChrome(node)) return;
15837
+ if (this.host.contains(node)) return;
15838
+ this.panel.close();
14661
15839
  return;
14662
15840
  }
14663
15841
  if (this.isOwnChrome(node)) return;
@@ -14666,19 +15844,16 @@ var DrawingSettingsPopup = class {
14666
15844
  cb?.(e);
14667
15845
  };
14668
15846
  this.theme = theme;
14669
- this.settingsDialog = new DrawingSettingsDialog(host, theme);
14670
- }
14671
- /** The magnifier timeframe choices strictly below the chart's own bar duration
14672
- * (`auto` rides along while at least one concrete lower step exists). */
14673
- lowerTimeframeOptions() {
14674
- const chartMs = this.chartBarMs();
14675
- if (!(chartMs > 0)) return [...MAGNIFIER_TIMEFRAME_OPTIONS];
14676
- const lower = MAGNIFIER_TIMEFRAME_OPTIONS.filter((o) => o.ms > 0 && o.ms < chartMs);
14677
- return lower.length > 0 ? [MAGNIFIER_TIMEFRAME_OPTIONS[0], ...lower] : [];
15847
+ this.chartBarMs = env.chartBarMs ?? (() => 0);
15848
+ this.panel = new DrawingSettingsPanel(host, theme, {
15849
+ chartBarMs: this.chartBarMs,
15850
+ timeZone: env.timeZone ?? (() => "UTC"),
15851
+ priceDecimals: env.priceDecimals ?? (() => null)
15852
+ });
14678
15853
  }
14679
15854
  setTheme(theme) {
14680
15855
  this.theme = theme;
14681
- this.settingsDialog.setTheme(theme);
15856
+ this.panel.setTheme(theme);
14682
15857
  }
14683
15858
  isOpen() {
14684
15859
  return this.el != null;
@@ -14686,7 +15861,22 @@ var DrawingSettingsPopup = class {
14686
15861
  /** Whether `node` belongs to this popup — the bar, its host-floated color/menu shells,
14687
15862
  * or a kit popover (select list / color chip) portaled into the chart host. */
14688
15863
  contains(node) {
14689
- return node != null && (this.isOwnChrome(node) || this.settingsDialog.contains(node));
15864
+ return node != null && (this.isOwnChrome(node) || this.panel.contains(node));
15865
+ }
15866
+ /** Open the full settings panel for the drawing the bar is open on. False when there is
15867
+ * none (no bar, or a multi-selection). */
15868
+ openPanel() {
15869
+ const cur = this.current;
15870
+ if (!cur) return false;
15871
+ this.hideTip();
15872
+ this.closeColorPopover();
15873
+ this.closeMenu();
15874
+ this.panel.open(cur.drawing, cur.actions, cur.anchor);
15875
+ return true;
15876
+ }
15877
+ /** The drawings changed under the panel (a drag, an undo) — let it re-read them. */
15878
+ syncPanel() {
15879
+ this.panel.sync();
14690
15880
  }
14691
15881
  /** Open the quick toolbar for `drawings` (one, or a whole multi-selection), floating clear of
14692
15882
  * their `anchor` box. With several drawings the bar shows only the controls they ALL support,
@@ -14696,10 +15886,12 @@ var DrawingSettingsPopup = class {
14696
15886
  * programmatic close), with the dismissing press when there is one, so the caller can tell a
14697
15887
  * modifier press (multi-select) from a plain one. */
14698
15888
  open(drawings, anchor, actions, onClose) {
14699
- this.close();
14700
15889
  const drawing = drawings[0];
15890
+ const keepPanel = this.panel.isOpen() && drawings.length === 1 && drawing != null && this.current?.drawing.id === drawing.id;
15891
+ this.close(keepPanel);
14701
15892
  if (!drawing) return;
14702
- ensureStyles4();
15893
+ this.current = drawings.length === 1 ? { drawing, actions, anchor } : null;
15894
+ ensureStyles5();
14703
15895
  this.onClose = onClose ?? null;
14704
15896
  const t = this.theme;
14705
15897
  const multi = drawings.length > 1;
@@ -14727,11 +15919,11 @@ var DrawingSettingsPopup = class {
14727
15919
  }
14728
15920
  if (paths.has("size")) {
14729
15921
  const sz = common((d) => d.size ?? "normal");
14730
- bar.appendChild(this.dropdown("Icon size", STAMP_SIZE_OPTIONS, sz, (s) => stampSizeIcon(s), (v) => actions.patch({ size: v }), { label: sizeLabel }));
15922
+ bar.appendChild(this.dropdown("Icon size", STAMP_SIZE_OPTIONS, sz, (s) => stampSizeIcon(s), (v) => actions.patch({ size: v }), { label: sizeLabel2 }));
14731
15923
  }
14732
15924
  if (paths.has("magnifier.timeframe") && every((d) => d instanceof Magnifier)) {
14733
15925
  const mag = (d) => d;
14734
- const options = this.lowerTimeframeOptions();
15926
+ const options = magnifierTimeframesBelow(this.chartBarMs());
14735
15927
  if (options.length > 0) {
14736
15928
  bar.appendChild(
14737
15929
  this.dropdown("Lower timeframe", options.map((o) => o.value), common((d) => mag(d).magnifier.timeframe), () => "", (v) => actions.patch({ "magnifier.timeframe": v }), {
@@ -14760,7 +15952,6 @@ var DrawingSettingsPopup = class {
14760
15952
  }
14761
15953
  if (paths.has("style.lineStyle")) bar.appendChild(this.dropdown("Line style", LINE_STYLE_OPTIONS.map((o) => o.value), common((d) => d.style.lineStyle), (s) => lineIcon(2, s), (v) => actions.patch({ "style.lineStyle": v }), { label: styleLabel2 }));
14762
15954
  if (paths.has("style.fillColor")) bar.appendChild(swatch("Fill", BUCKET_ICON, (d) => effectiveFillColor(d, this.theme) ?? d.style.fillColor ?? DEFAULT_DRAWING_COLOR, "style.fillColor"));
14763
- const isFrvp = paths.has("frvp.rows") && drawing instanceof FixedRangeVolumeProfile;
14764
15955
  const isPosition = paths.has("riskPercent") && every((d) => d instanceof PositionTool);
14765
15956
  if (isPosition) {
14766
15957
  const pos = (d) => d;
@@ -14829,7 +16020,7 @@ var DrawingSettingsPopup = class {
14829
16020
  if (paths.has("showPrice")) bar.appendChild(this.toggle("Show price", PRICE_DELTA_ICON, common((d) => flags(d).showPrice !== false), (v) => actions.patch({ showPrice: v })));
14830
16021
  if (paths.has("showDate")) bar.appendChild(this.toggle("Show date", DATE_DELTA_ICON, common((d) => flags(d).showDate !== false), (v) => actions.patch({ showDate: v })));
14831
16022
  if (paths.has("text.color") && textOnBar) bar.appendChild(swatch("Text color", TYPE_ICON, (d) => d.text?.color || t.textColor, "text.color"));
14832
- if (paths.has("text.size") && textOnBar) bar.appendChild(this.dropdown("Text size", TEXT_SIZE_OPTIONS.map((o) => o.value), common((d) => d.text?.size ?? "normal"), (s) => labelSizeIcon(s), (v) => actions.patch({ "text.size": v }), { label: sizeLabel }));
16023
+ if (paths.has("text.size") && textOnBar) bar.appendChild(this.dropdown("Text size", TEXT_SIZE_OPTIONS.map((o) => o.value), common((d) => d.text?.size ?? "normal"), (s) => labelSizeIcon(s), (v) => actions.patch({ "text.size": v }), { label: sizeLabel2 }));
14833
16024
  const textField = !multi && paths.has("text.value");
14834
16025
  if (paths.has("text.bold") && !textField) bar.appendChild(this.toggle("Bold", BOLD_ICON, common((d) => !!d.text?.bold), (v) => actions.patch({ "text.bold": v })));
14835
16026
  if (paths.has("text.italic") && !textField) bar.appendChild(this.toggle("Italic", ITALIC_ICON, common((d) => !!d.text?.italic), (v) => actions.patch({ "text.italic": v })));
@@ -14838,13 +16029,11 @@ var DrawingSettingsPopup = class {
14838
16029
  if (editableLevels && every((d) => d.editableLevels() != null && !(d instanceof MachFigure))) {
14839
16030
  const fib = (d) => d;
14840
16031
  const sizes = TEXT_SIZE_OPTIONS.map((o) => o.value);
14841
- bar.appendChild(this.dropdown("Numbers size", sizes, common((d) => fib(d).numbersSize ?? "small"), (s) => numbersSizeIcon(s), (v) => actions.patch({ numbersSize: v }), { label: sizeLabel }));
14842
- bar.appendChild(this.dropdown("Label size", sizes, common((d) => fib(d).labelsSize ?? "normal"), (s) => labelSizeIcon(s), (v) => actions.patch({ labelsSize: v }), { label: sizeLabel }));
16032
+ bar.appendChild(this.dropdown("Numbers size", sizes, common((d) => fib(d).numbersSize ?? "small"), (s) => numbersSizeIcon(s), (v) => actions.patch({ numbersSize: v }), { label: sizeLabel2 }));
16033
+ bar.appendChild(this.dropdown("Label size", sizes, common((d) => fib(d).labelsSize ?? "normal"), (s) => labelSizeIcon(s), (v) => actions.patch({ labelsSize: v }), { label: sizeLabel2 }));
14843
16034
  }
14844
16035
  bar.appendChild(this.divider());
14845
- if (!multi && isFrvp) bar.appendChild(this.iconBtn("Settings", GEAR_ICON, () => this.settingsDialog.open(drawing, actions, "frvp")));
14846
- if (!multi && isPosition) bar.appendChild(this.iconBtn("Position size", GEAR_ICON, () => this.settingsDialog.open(drawing, actions, "position")));
14847
- if (!multi && editableLevels) bar.appendChild(this.iconBtn("Levels", GEAR_ICON, () => this.settingsDialog.open(drawing, actions, "levels")));
16036
+ if (!multi) bar.appendChild(this.iconBtn("Settings", GEAR_ICON, () => this.openPanel()));
14848
16037
  bar.appendChild(this.toggle("Lock", LOCK_ICON, common((d) => d.locked), (v) => actions.setLocked(v), UNLOCK_ICON));
14849
16038
  const del = this.iconBtn("Delete", TRASH_ICON, () => actions.remove());
14850
16039
  del.style.color = "var(--vela-danger)";
@@ -14862,16 +16051,17 @@ var DrawingSettingsPopup = class {
14862
16051
  this.position(anchor);
14863
16052
  setTimeout(() => document.addEventListener("pointerdown", this.onOutside), 0);
14864
16053
  }
14865
- close() {
16054
+ close(keepPanel = false) {
14866
16055
  document.removeEventListener("pointerdown", this.onOutside);
14867
16056
  this.closeColorPopover();
14868
16057
  this.closeMenu();
14869
16058
  this.hideTip();
14870
- this.settingsDialog.close();
16059
+ if (!keepPanel) this.panel.close();
14871
16060
  this.el?.remove();
14872
16061
  this.el = null;
14873
16062
  this.textPanel = null;
14874
16063
  this.onClose = null;
16064
+ this.current = null;
14875
16065
  }
14876
16066
  destroy() {
14877
16067
  this.close();
@@ -14893,7 +16083,7 @@ var DrawingSettingsPopup = class {
14893
16083
  this.reposition();
14894
16084
  return;
14895
16085
  }
14896
- const text = drawing.text;
16086
+ const text = (actions.resolve() ?? drawing).text;
14897
16087
  const panel = document.createElement("div");
14898
16088
  panel.style.cssText = `padding:6px;border-top:1px solid var(--vela-border);display:flex;flex-direction:column;gap:4px;`;
14899
16089
  const ta = new TextArea({
@@ -14927,7 +16117,7 @@ var DrawingSettingsPopup = class {
14927
16117
  if (withColorAndSize) {
14928
16118
  tools.append(
14929
16119
  this.colorButton("Text color", TYPE_ICON, text?.color || this.theme.textColor, (v) => actions.patch({ "text.color": v }), 14),
14930
- this.dropdown("Text size", TEXT_SIZE_OPTIONS.map((o) => o.value), text?.size ?? "normal", (s) => sizeIcon(s), (v) => actions.patch({ "text.size": v }), { label: sizeLabel })
16120
+ this.dropdown("Text size", TEXT_SIZE_OPTIONS.map((o) => o.value), text?.size ?? "normal", (s) => sizeIcon(s), (v) => actions.patch({ "text.size": v }), { label: sizeLabel2 })
14931
16121
  );
14932
16122
  }
14933
16123
  tools.append(
@@ -15101,9 +16291,9 @@ var DrawingSettingsPopup = class {
15101
16291
  * quick toolbar — the chip works without selecting the drawing first.
15102
16292
  */
15103
16293
  openMagnifierTimeframeMenu(rect, current, onPick) {
15104
- ensureStyles4();
16294
+ ensureStyles5();
15105
16295
  closeOpenPopovers();
15106
- const options = this.lowerTimeframeOptions();
16296
+ const options = magnifierTimeframesBelow(this.chartBarMs());
15107
16297
  const anchor = document.createElement("div");
15108
16298
  anchor.style.cssText = `position:absolute;left:${rect.x}px;top:${rect.y}px;width:${rect.w}px;height:${rect.h}px;pointer-events:none;`;
15109
16299
  this.host.appendChild(anchor);
@@ -15144,22 +16334,22 @@ var DrawingSettingsPopup = class {
15144
16334
  zIndex: 26,
15145
16335
  padding: "4px",
15146
16336
  fill: (menu, pop) => {
15147
- for (const row of rows) {
16337
+ for (const row2 of rows) {
15148
16338
  const item = document.createElement("button");
15149
16339
  item.type = "button";
15150
16340
  item.className = "vela-dpop-item";
15151
16341
  item.style.cssText = "display:flex;align-items:center;gap:9px;min-width:150px;padding:6px 9px;border:none;border-radius:5px;color:inherit;cursor:pointer;text-align:left;font:inherit;";
15152
16342
  const ic = document.createElement("span");
15153
16343
  ic.style.cssText = "display:flex;flex:none;width:20px;justify-content:center;";
15154
- ic.innerHTML = sized(row.icon, 18);
16344
+ ic.innerHTML = sized(row2.icon, 18);
15155
16345
  const tx = document.createElement("span");
15156
- tx.textContent = row.label;
16346
+ tx.textContent = row2.label;
15157
16347
  tx.style.cssText = "flex:1;";
15158
16348
  item.append(ic, tx);
15159
16349
  item.addEventListener("click", (e) => {
15160
16350
  e.stopPropagation();
15161
16351
  pop.hide();
15162
- row.onClick();
16352
+ row2.onClick();
15163
16353
  });
15164
16354
  menu.appendChild(item);
15165
16355
  }
@@ -15384,12 +16574,12 @@ var DrawingSettingsPopup = class {
15384
16574
  padding: "10px",
15385
16575
  fill: (el, pop) => {
15386
16576
  if (used.length > 1) {
15387
- const row = document.createElement("div");
15388
- row.style.cssText = "display:flex;align-items:center;gap:6px;padding-bottom:9px;border-bottom:1px solid var(--vela-border);";
16577
+ const row2 = document.createElement("div");
16578
+ row2.style.cssText = "display:flex;align-items:center;gap:6px;padding-bottom:9px;border-bottom:1px solid var(--vela-border);";
15389
16579
  const lbl = document.createElement("span");
15390
16580
  lbl.textContent = "In use";
15391
16581
  lbl.style.cssText = "font:var(--vela-font-size-sm) inherit;opacity:0.7;margin-right:2px;";
15392
- row.appendChild(lbl);
16582
+ row2.appendChild(lbl);
15393
16583
  for (const c of used) {
15394
16584
  const sw = document.createElement("button");
15395
16585
  sw.type = "button";
@@ -15400,9 +16590,9 @@ var DrawingSettingsPopup = class {
15400
16590
  pop.hide();
15401
16591
  onChange(c);
15402
16592
  });
15403
- row.appendChild(sw);
16593
+ row2.appendChild(sw);
15404
16594
  }
15405
- el.appendChild(row);
16595
+ el.appendChild(row2);
15406
16596
  }
15407
16597
  el.appendChild(buildColorPicker(color, t, onChange));
15408
16598
  }
@@ -15476,7 +16666,7 @@ function labelSizeIcon(size) {
15476
16666
  function capitalize(s) {
15477
16667
  return s.charAt(0).toUpperCase() + s.slice(1);
15478
16668
  }
15479
- function sizeLabel(v) {
16669
+ function sizeLabel2(v) {
15480
16670
  return TEXT_SIZE_OPTIONS.find((o) => o.value === v)?.label ?? capitalize(String(v));
15481
16671
  }
15482
16672
  function styleLabel2(v) {
@@ -15699,7 +16889,9 @@ var UserDrawingController = class {
15699
16889
  this.hoveredId = null;
15700
16890
  // the drawing under the cursor (its handles show)
15701
16891
  this.activeTool = null;
15702
- // last-used style for the armed tool (seeds the placement ghost)
16892
+ // the armed tool's remembered settings (seed the placement ghost)
16893
+ this.toolTemplates = {};
16894
+ // saved styles, per tool
15703
16895
  this.intentCb = null;
15704
16896
  /** Another chart's in-progress placement, mirrored here as a ghost (drawings sync). */
15705
16897
  this.externalGhost = null;
@@ -15747,7 +16939,11 @@ var UserDrawingController = class {
15747
16939
  this.clearSelection();
15748
16940
  };
15749
16941
  this.ctx = canvas.getContext("2d");
15750
- this.popup = new DrawingSettingsPopup(overlayHost, deps.theme(), () => deps.chartBarMs());
16942
+ this.popup = new DrawingSettingsPopup(overlayHost, deps.theme(), {
16943
+ chartBarMs: () => deps.chartBarMs(),
16944
+ timeZone: () => deps.timeZone?.() ?? "UTC",
16945
+ priceDecimals: () => deps.priceDecimals?.() ?? null
16946
+ });
15751
16947
  this.toolbar = new DrawingToolbar(
15752
16948
  toolbarHost,
15753
16949
  deps.theme(),
@@ -15770,7 +16966,7 @@ var UserDrawingController = class {
15770
16966
  this.interaction = new DrawingInteraction({
15771
16967
  projector: () => this.deps.projector(),
15772
16968
  activeTool: () => this.activeTool,
15773
- drawings: () => this.drawings,
16969
+ drawings: () => this.onScreen(),
15774
16970
  hoveredId: () => this.hoveredId,
15775
16971
  selectedIds: () => this.selectedIds,
15776
16972
  emit: (i) => this.emit(i),
@@ -15780,7 +16976,7 @@ var UserDrawingController = class {
15780
16976
  },
15781
16977
  openSettings: (id, x, y) => this.openSettingsById(id, x, y),
15782
16978
  snap: (pt, paneId, mode, cursorPx) => this.deps.snap(pt, paneId, mode, cursorPx),
15783
- lastStyle: () => this.activeToolStyle
16979
+ toolDefaults: () => this.activeToolDefaults
15784
16980
  });
15785
16981
  }
15786
16982
  // ── IDrawingsRendererPort (commands down) ──
@@ -15832,6 +17028,7 @@ var UserDrawingController = class {
15832
17028
  this.invalidateSlices();
15833
17029
  this.render();
15834
17030
  this.deps.requestScaleUpdate();
17031
+ this.popup.syncPanel();
15835
17032
  }
15836
17033
  /** The pane's series stack in z terms — how the core places a new drawing (just under
15837
17034
  * `price`) and computes "front of everything" / "behind everything" for the reorders. */
@@ -15859,7 +17056,7 @@ var UserDrawingController = class {
15859
17056
  }
15860
17057
  return lo <= hi ? { min: lo, max: hi } : null;
15861
17058
  }
15862
- setActiveTool(type, lastStyle) {
17059
+ setActiveTool(type, lastStyle, defaults) {
15863
17060
  if (type != null) {
15864
17061
  this.finishTextEditor(true);
15865
17062
  this.withModeIntent(() => {
@@ -15868,7 +17065,7 @@ var UserDrawingController = class {
15868
17065
  });
15869
17066
  }
15870
17067
  this.activeTool = type;
15871
- this.activeToolStyle = lastStyle;
17068
+ this.activeToolDefaults = defaults ?? (lastStyle ? { style: lastStyle } : void 0);
15872
17069
  this.toolbar.setActiveTool(type);
15873
17070
  if (type == null) this.interaction.onToolCleared();
15874
17071
  else this.clearSelection();
@@ -15878,6 +17075,10 @@ var UserDrawingController = class {
15878
17075
  setFavorites(types) {
15879
17076
  this.toolbar.setFavorites(types);
15880
17077
  }
17078
+ /** Core push: every tool's saved styles — the settings panel's style menu reads them. */
17079
+ setToolTemplates(map2) {
17080
+ this.toolTemplates = map2;
17081
+ }
15881
17082
  /** Core push: per-tool shortcut hints (display strings) shown in the toolbar flyouts. */
15882
17083
  setToolShortcuts(map2) {
15883
17084
  this.toolbar.setShortcuts(map2);
@@ -15950,8 +17151,9 @@ var UserDrawingController = class {
15950
17151
  /** The topmost visible (unlocked) magnifier whose timeframe chip contains (x, y) —
15951
17152
  * the chip's rect is what the painter measured last frame. */
15952
17153
  magnifierChipAt(x, y) {
15953
- for (let i = this.drawings.length - 1; i >= 0; i -= 1) {
15954
- const d = this.drawings[i];
17154
+ const shown = this.onScreen();
17155
+ for (let i = shown.length - 1; i >= 0; i -= 1) {
17156
+ const d = shown[i];
15955
17157
  if (!(d instanceof Magnifier) || !d.visible || d.locked) continue;
15956
17158
  const r = d.chipRect;
15957
17159
  if (r && x >= r.x && x <= r.x + r.w && y >= r.y && y <= r.y + r.h) return d;
@@ -15977,7 +17179,7 @@ var UserDrawingController = class {
15977
17179
  * `withSelection` so a hit on a member of a multi-selection removes the selection's
15978
17180
  * unlocked members — whichever member was hit, locked or not. */
15979
17181
  deleteAt(x, y, withSelection = false) {
15980
- const hit = topDrawingAt(this.drawings, x, y, this.deps.projector(), HIT_TOLERANCE);
17182
+ const hit = topDrawingAt(this.onScreen(), x, y, this.deps.projector(), HIT_TOLERANCE);
15981
17183
  if (!hit) return false;
15982
17184
  const ids = deleteTargets(hit, this.selectedIds, this.drawings, withSelection);
15983
17185
  if (ids.length === 0) return false;
@@ -16057,13 +17259,19 @@ var UserDrawingController = class {
16057
17259
  updateHover(x, y, mod = false) {
16058
17260
  let id = null;
16059
17261
  if (!mod && this.activeTool == null && !this.interaction.isPlacing() && !this.interaction.isDragging()) {
16060
- id = topDrawingAt(this.drawings, x, y, this.deps.projector(), HIT_TOLERANCE)?.id ?? null;
17262
+ id = topDrawingAt(this.onScreen(), x, y, this.deps.projector(), HIT_TOLERANCE)?.id ?? null;
16061
17263
  }
16062
17264
  if (id !== this.hoveredId) {
16063
17265
  this.hoveredId = id;
16064
17266
  this.render();
16065
17267
  }
16066
17268
  }
17269
+ /** Drop the hover (its handles) — the pointer no longer reaches the drawings. */
17270
+ clearHover() {
17271
+ if (this.hoveredId == null) return;
17272
+ this.hoveredId = null;
17273
+ this.render();
17274
+ }
16067
17275
  pointerUp(x, y, snap = "off") {
16068
17276
  if (this.eraserMode) {
16069
17277
  this.erasing = false;
@@ -16286,16 +17494,20 @@ var UserDrawingController = class {
16286
17494
  }
16287
17495
  return false;
16288
17496
  }
16289
- /** Double-click over a drawing → suppress the chart's view reset (single-click already
16290
- * opens settings). Returns true only when a drawing is under the cursor. */
17497
+ /** Double-click over a drawing opens its full settings panel (a callout or text label edits
17498
+ * its words in place instead) and suppresses the chart's view reset. Returns true only
17499
+ * when a drawing is under the cursor. */
16291
17500
  dblClick(x, y) {
16292
17501
  if (this.interaction.finishPlacing(true)) return true;
16293
- const hit = topDrawingAt(this.drawings, x, y, this.deps.projector(), HIT_TOLERANCE);
17502
+ const hit = topDrawingAt(this.onScreen(), x, y, this.deps.projector(), HIT_TOLERANCE);
16294
17503
  if (isInlineEditable(hit)) {
16295
17504
  this.editTextInline(hit.id);
16296
17505
  return true;
16297
17506
  }
16298
- return hit != null;
17507
+ if (!hit) return false;
17508
+ if (this.popupMulti || !this.selectedIds.has(hit.id) || !this.popup.isOpen()) this.openSettingsById(hit.id, 0, 0);
17509
+ this.popup.openPanel();
17510
+ return true;
16299
17511
  }
16300
17512
  /** Keyboard pre-empt: Escape (popup/placing/selection), undo/redo, copy/paste/duplicate,
16301
17513
  * delete (multi), and arrow-nudge. Stands down while a label text field is focused. */
@@ -16393,6 +17605,13 @@ var UserDrawingController = class {
16393
17605
  this.popup.close();
16394
17606
  this.render();
16395
17607
  }
17608
+ /** The drawings shown on the chart's current timeframe. One limited to other timeframes
17609
+ * ("show on") is neither painted nor clickable here, but stays in the model: the object
17610
+ * tree still lists it, and its own visibility flag is untouched. */
17611
+ onScreen() {
17612
+ const barMs = this.deps.chartBarMs();
17613
+ return this.drawings.filter((d) => shownOnTimeframe(d.showOn, barMs));
17614
+ }
16396
17615
  /** Whether the drawing's z puts it INSIDE the series stack (per the last-known boundaries)
16397
17616
  * rather than over it — i.e. its body belongs to an interleave layer, not the top canvas. */
16398
17617
  isInterleaved(d) {
@@ -16432,7 +17651,7 @@ var UserDrawingController = class {
16432
17651
  const proj = this.deps.projector();
16433
17652
  const theme = this.deps.theme();
16434
17653
  const buckets = /* @__PURE__ */ new Map();
16435
- for (const d of this.drawings) {
17654
+ for (const d of this.onScreen()) {
16436
17655
  if (!d.visible || this.lifted.has(d.id)) continue;
16437
17656
  const beforeZ = sliceKeyFor(d.zIndex, this.lastBounds.get(d.paneId) ?? []);
16438
17657
  if (beforeZ === null) continue;
@@ -16486,8 +17705,9 @@ var UserDrawingController = class {
16486
17705
  };
16487
17706
  this.painter.seriesLook = this.deps.seriesLook();
16488
17707
  const onTop = (d) => !this.isInterleaved(d) || this.lifted.has(d.id);
16489
- this.painter.paintAll(ctx, this.drawings.filter(onTop), proj, this.deps.theme(), targets);
16490
- this.painter.paintHighlights(ctx, this.drawings.filter((d) => !onTop(d)), proj, handleIdsFor(targets));
17708
+ const shown = this.onScreen();
17709
+ this.painter.paintAll(ctx, shown.filter(onTop), proj, this.deps.theme(), targets);
17710
+ this.painter.paintHighlights(ctx, shown.filter((d) => !onTop(d)), proj, handleIdsFor(targets));
16491
17711
  const clones = this.interaction.dragClones();
16492
17712
  if (clones) this.painter.paintAll(ctx, clones, proj, this.deps.theme(), { selected: new Set(clones.map((c) => c.id)) });
16493
17713
  this.layoutTextEditor();
@@ -16597,6 +17817,7 @@ var UserDrawingController = class {
16597
17817
  this.emit({ kind: "select", ids: [id] });
16598
17818
  this.emit({ kind: "settings", id });
16599
17819
  const anchor = drawing.bounds(this.deps.projector());
17820
+ let previewBase = null;
16600
17821
  this.popup.open([drawing], anchor, {
16601
17822
  // Sync rebuilds instances, so a panel that reads values back after a patch (e.g. the
16602
17823
  // position tool's price fields, where one edit can flip another level) resolves fresh.
@@ -16631,11 +17852,39 @@ var UserDrawingController = class {
16631
17852
  restore: (doc) => {
16632
17853
  const d = live();
16633
17854
  if (!d) return;
16634
- if (doc.style) d.style = { ...doc.style };
16635
- if (doc.text !== void 0) d.text = doc.text ? { ...doc.text } : void 0;
16636
- if (doc.props !== void 0) d.applyProps(doc.props);
17855
+ const text = doc.text ?? (d.text ? { ...d.text, value: "" } : void 0);
17856
+ restoreLook(d, { ...doc, text });
17857
+ d.anchors = doc.anchors.map((a) => ({ ...a }));
17858
+ d.showOn = doc.showOn ? [...doc.showOn] : void 0;
17859
+ this.render();
17860
+ this.emit({ kind: "edit", doc: { ...doc, text } });
17861
+ },
17862
+ templates: () => this.toolTemplates[drawing.type] ?? [],
17863
+ saveTemplate: (name) => {
17864
+ const d = live();
17865
+ if (d) this.emit({ kind: "template-save", type: d.type, name, settings: captureToolDefaults(d) });
17866
+ },
17867
+ removeTemplate: (name) => this.emit({ kind: "template-remove", type: drawing.type, name }),
17868
+ applyToolSettings: (settings) => {
17869
+ const d = live();
17870
+ if (!d) return;
17871
+ applyToolDefaults(d, settings);
17872
+ this.render();
17873
+ this.emit({ kind: "edit", doc: d.serialize() });
17874
+ },
17875
+ // A hovered style paints on the drawing without an edit; leaving puts the look back.
17876
+ preview: (settings) => {
17877
+ const d = live();
17878
+ if (!d) return;
17879
+ if (settings) {
17880
+ if (previewBase) restoreLook(d, previewBase);
17881
+ else previewBase = d.serialize();
17882
+ applyToolDefaults(d, settings);
17883
+ } else if (previewBase) {
17884
+ restoreLook(d, previewBase);
17885
+ previewBase = null;
17886
+ } else return;
16637
17887
  this.render();
16638
- this.emit({ kind: "edit", doc });
16639
17888
  },
16640
17889
  remove: () => {
16641
17890
  this.closeTextEditor();
@@ -16684,6 +17933,11 @@ var UserDrawingController = class {
16684
17933
  this.intentCb?.(i);
16685
17934
  }
16686
17935
  };
17936
+ function restoreLook(d, doc) {
17937
+ d.style = { ...doc.style };
17938
+ d.text = doc.text ? { ...doc.text } : void 0;
17939
+ if (doc.props !== void 0) d.applyProps(doc.props);
17940
+ }
16687
17941
 
16688
17942
  // src/renderers/shared/TableOverlay.ts
16689
17943
  var SIZE_PX2 = {
@@ -16699,7 +17953,7 @@ function fontPxOf(size) {
16699
17953
  return SIZE_PX2[size] ?? SIZE_PX2.auto;
16700
17954
  }
16701
17955
  function tableHasContent(t) {
16702
- return t.cells.some((row) => row?.some((c) => c != null && !c.merged));
17956
+ return t.cells.some((row2) => row2?.some((c) => c != null && !c.merged));
16703
17957
  }
16704
17958
  function mergeRenderPlan(t) {
16705
17959
  const span = /* @__PURE__ */ new Map();
@@ -17054,9 +18308,10 @@ function computePaneScale(models, bars, includeCandles, i0, i1, drawings, log =
17054
18308
  consider(drawings.max);
17055
18309
  }
17056
18310
  if (min === Infinity || max === -Infinity) return { min: 0, max: 1 };
17057
- if (min === max) {
17058
- const pad = Math.abs(min) * 0.1 || 1;
17059
- return { min: min - pad, max: max + pad, log: log && min - pad > 0 };
18311
+ if (max - min <= Math.max(Math.abs(min), Math.abs(max)) * 1e-12) {
18312
+ const mid = (min + max) / 2;
18313
+ const pad = Math.abs(mid) * 0.1 || 1;
18314
+ return { min: mid - pad, max: mid + pad, log: log && mid - pad > 0 };
17060
18315
  }
17061
18316
  const content = Math.max(0.1, 1 - (margins.top + margins.bottom) / 100);
17062
18317
  const above = margins.top / 100 / content;
@@ -17459,15 +18714,15 @@ var MarkPopover = class {
17459
18714
  actions.className = "vela-callout-actions";
17460
18715
  section.appendChild(actions);
17461
18716
  }
17462
- const btn2 = doc.createElement("button");
17463
- btn2.type = "button";
17464
- btn2.className = "vela-callout-btn" + (item.primary ? " vela-callout-btn-primary" : "");
17465
- btn2.textContent = item.label;
17466
- btn2.addEventListener("click", () => {
18717
+ const btn = doc.createElement("button");
18718
+ btn.type = "button";
18719
+ btn.className = "vela-callout-btn" + (item.primary ? " vela-callout-btn-primary" : "");
18720
+ btn.textContent = item.label;
18721
+ btn.addEventListener("click", () => {
17467
18722
  item.run();
17468
18723
  if (item.close !== false) this.close();
17469
18724
  });
17470
- actions.appendChild(btn2);
18725
+ actions.appendChild(btn);
17471
18726
  continue;
17472
18727
  }
17473
18728
  actions = null;
@@ -17477,16 +18732,16 @@ var MarkPopover = class {
17477
18732
  text.textContent = item.text;
17478
18733
  section.appendChild(text);
17479
18734
  } else if (item.type === "field") {
17480
- const row = doc.createElement("div");
17481
- row.className = "vela-marks-field";
18735
+ const row2 = doc.createElement("div");
18736
+ row2.className = "vela-marks-field";
17482
18737
  const label = doc.createElement("span");
17483
18738
  label.className = "vela-marks-field-label";
17484
18739
  label.textContent = item.label;
17485
18740
  const value = doc.createElement("span");
17486
18741
  value.className = "vela-marks-field-value";
17487
18742
  value.textContent = item.value;
17488
- row.append(label, value);
17489
- section.appendChild(row);
18743
+ row2.append(label, value);
18744
+ section.appendChild(row2);
17490
18745
  }
17491
18746
  }
17492
18747
  }
@@ -17560,7 +18815,9 @@ function bandEdgeSlot(logical) {
17560
18815
  return Math.ceil(logical) - 0.5;
17561
18816
  }
17562
18817
  var BackdropRenderer = class {
17563
- constructor() {
18818
+ /** `axisTicks` is shared with the chrome layer, so gridlines and price labels agree. */
18819
+ constructor(axisTicks = new PriceAxisTickSource()) {
18820
+ this.axisTicks = axisTicks;
17564
18821
  this.canvas = null;
17565
18822
  this.ctx = null;
17566
18823
  }
@@ -17637,9 +18894,8 @@ var BackdropRenderer = class {
17637
18894
  if (scene.showGrid && gridHorz.visible && !pane.collapsed) {
17638
18895
  ctx.globalAlpha = gridAlpha;
17639
18896
  ctx.strokeStyle = horzColor;
17640
- const pct = percentScaleFor(scene, pane);
17641
18897
  ctx.beginPath();
17642
- for (const t of paneAxisTicks(pane.scale, pane.bounds.height, pct, void 0, pane.axisFormat)) {
18898
+ for (const t of this.axisTicks.ticksFor(scene, pane, coords)) {
17643
18899
  const y = Math.round(coords.priceToY(t.price, pane.scale, pane.bounds)) + 0.5;
17644
18900
  if (y < pane.bounds.top || y > pane.bounds.top + pane.bounds.height) continue;
17645
18901
  ctx.moveTo(0, y);
@@ -17744,7 +19000,7 @@ var VELA_LETTERS_SVG = '<svg viewBox="161.66 -142 310.57 148" aria-hidden="true"
17744
19000
 
17745
19001
  // src/renderers/native/chrome/AttributionMark.ts
17746
19002
  var ATTRIBUTION_URL = "https://velacharts.dev/?utm_source=vela&utm_medium=attribution";
17747
- var STYLE_ID5 = "vela-attribution-styles";
19003
+ var STYLE_ID6 = "vela-attribution-styles";
17748
19004
  var CSS = `
17749
19005
  .vela-attribution {
17750
19006
  text-decoration: none;
@@ -17800,11 +19056,11 @@ var CSS = `
17800
19056
  [data-vela-layout='mobile'] .vela-attribution:hover .vela-attr-wordmark,
17801
19057
  [data-layout='mobile'] .vela-attribution:hover .vela-attr-wordmark { transform: translateX(0); }
17802
19058
  `;
17803
- function ensureStyles5(doc) {
17804
- let s = doc.getElementById(STYLE_ID5);
19059
+ function ensureStyles6(doc) {
19060
+ let s = doc.getElementById(STYLE_ID6);
17805
19061
  if (!s) {
17806
19062
  s = doc.createElement("style");
17807
- s.id = STYLE_ID5;
19063
+ s.id = STYLE_ID6;
17808
19064
  doc.head.appendChild(s);
17809
19065
  }
17810
19066
  s.textContent = CSS;
@@ -17834,7 +19090,7 @@ function createCustomMark(doc, html, background) {
17834
19090
  return el;
17835
19091
  }
17836
19092
  function createAttributionMark(doc, background) {
17837
- ensureStyles5(doc);
19093
+ ensureStyles6(doc);
17838
19094
  const a = doc.createElement("a");
17839
19095
  a.className = "vela-attribution";
17840
19096
  a.href = ATTRIBUTION_URL;
@@ -18107,13 +19363,13 @@ function paintVpvr(ctx, profile, geom, style) {
18107
19363
  const { rightX, maxW, yOf } = geom;
18108
19364
  if (maxTotal <= 0 || maxW <= 0) return;
18109
19365
  for (let k = 0; k < rows.length; k += 1) {
18110
- const row = rows[k];
18111
- const total = row.up + row.down;
19366
+ const row2 = rows[k];
19367
+ const total = row2.up + row2.down;
18112
19368
  if (total <= 0) continue;
18113
- const yTop = yOf(row.price + rowH);
18114
- const h = Math.max(1, yOf(row.price) - yTop - 1);
19369
+ const yTop = yOf(row2.price + rowH);
19370
+ const h = Math.max(1, yOf(row2.price) - yTop - 1);
18115
19371
  const w = total / maxTotal * maxW;
18116
- const upW = row.up / total * w;
19372
+ const upW = row2.up / total * w;
18117
19373
  ctx.globalAlpha = k >= vaFrom && k <= vaTo ? VPVR_VA_ALPHA : VPVR_OUTSIDE_ALPHA;
18118
19374
  if (upW > 0) {
18119
19375
  ctx.fillStyle = style.upColor;
@@ -18125,9 +19381,9 @@ function paintVpvr(ctx, profile, geom, style) {
18125
19381
  }
18126
19382
  }
18127
19383
  if (style.showPoc) {
18128
- const row = rows[poc];
18129
- const yTop = yOf(row.price + rowH);
18130
- const h = Math.max(1, yOf(row.price) - yTop - 1);
19384
+ const row2 = rows[poc];
19385
+ const yTop = yOf(row2.price + rowH);
19386
+ const h = Math.max(1, yOf(row2.price) - yTop - 1);
18131
19387
  ctx.globalAlpha = VPVR_POC_ALPHA;
18132
19388
  ctx.strokeStyle = style.pocColor;
18133
19389
  ctx.lineWidth = 1;
@@ -18229,8 +19485,16 @@ var NativeRenderer = class {
18229
19485
  this.toolbarGutter = 0;
18230
19486
  // px reserved on the left for the docked drawings toolbar (0 when hidden)
18231
19487
  this.mountContainer = null;
19488
+ /** The host-styleable twin of the `shadeRight` veil (`.vela-shade-right`), directly under
19489
+ * the cursor canvas; hidden whenever the crosshair layer paints no veil. */
19490
+ this.shadeRightEl = null;
19491
+ /** The veil geometry last written to {@link shadeRightEl} ('' = hidden) — the DOM is
19492
+ * touched only when it changes, never on a plain pointer move. */
19493
+ this.shadeRightSig = "";
18232
19494
  this.userDrawings = null;
18233
- this.backdropRenderer = new BackdropRenderer();
19495
+ /** One price-axis tick source for the gridlines (backdrop) and the labels (chrome). */
19496
+ this.priceAxisTicks = new PriceAxisTickSource();
19497
+ this.backdropRenderer = new BackdropRenderer(this.priceAxisTicks);
18234
19498
  this.volumeRenderer = new VolumeRenderer();
18235
19499
  /** SDK renderer layers instantiated at mount ({@link registerRendererLayer}). */
18236
19500
  /**
@@ -18263,7 +19527,7 @@ var NativeRenderer = class {
18263
19527
  this.backendMode = "auto";
18264
19528
  this.glowAmount = 0;
18265
19529
  // WebGL2 neon-glow intensity (canvas2d ignores it)
18266
- this.chrome = new ChromeRenderer();
19530
+ this.chrome = new ChromeRenderer(this.priceAxisTicks);
18267
19531
  /** Prepaints each indicator's Pine drawings into interleave slices at the model's z. */
18268
19532
  this.indicatorSlices = new IndicatorDrawingSlices();
18269
19533
  /** Hover tooltips for Pine labels (canvas hit-rects collected by the chrome layer). */
@@ -18380,6 +19644,8 @@ var NativeRenderer = class {
18380
19644
  // pane just below it
18381
19645
  this.resizeSplitStart = null;
18382
19646
  // the two panes' shared span when the drag began
19647
+ /** The user drawings take part in pointer input (the runtime `drawingsInteractive` feature). */
19648
+ this.drawingsInteractive = true;
18383
19649
  this.hoverSeparatorY = null;
18384
19650
  // pixel y of the separator under the cursor (drives its hover highlight)
18385
19651
  // ── pane management (merge / reorder / collapse / maximize) ──
@@ -18435,8 +19701,9 @@ var NativeRenderer = class {
18435
19701
  this.toggleVisibleCbs = /* @__PURE__ */ new Set();
18436
19702
  this.moveIndicatorCbs = /* @__PURE__ */ new Set();
18437
19703
  this.priceStyleCbs = /* @__PURE__ */ new Set();
19704
+ this.priceStyleWillChangeCbs = /* @__PURE__ */ new Set();
18438
19705
  this.name = "native";
18439
- this.features = ["logScale", "currentPriceLine", "priceLabel", "countdown", "upColor", "downColor", "glow", "animZoom", "animPan", "animScroll", "animAutoscale", "animLiveBar", "intro", "zoomAnchor", "axisDrag", "paneResize", "candleZOrder", "candleVisible", "seriesOrder", "highlights", "sessionZones", "gridlines", "axisLabels", "scaleMode", "invertScale", "paneScales", "autoScale", "timezone", "keyboard", "historyChords", "priceStyle", "priceBaseline", "baselinePrice", "settings", "attribution", "dialogHost", "tradeMarkers", "marks", "indicatorTitles", "indicatorValues", "crosshairOverride"];
19706
+ this.features = ["logScale", "currentPriceLine", "priceLabel", "countdown", "upColor", "downColor", "glow", "animZoom", "animPan", "animScroll", "animAutoscale", "animLiveBar", "intro", "zoomAnchor", "axisDrag", "paneResize", "candleZOrder", "candleVisible", "seriesOrder", "highlights", "sessionZones", "gridlines", "axisLabels", "scaleMode", "invertScale", "paneScales", "autoScale", "timezone", "keyboard", "historyChords", "priceStyle", "priceBaseline", "baselinePrice", "settings", "attribution", "dialogHost", "tradeMarkers", "marks", "indicatorTitles", "indicatorValues", "crosshairOverride", "drawingsInteractive", "priceAxisTicks"];
18440
19707
  /** Track cursor proximity to the scroll button on the plot (bubbles from the button too,
18441
19708
  * so moving onto the button doesn't count as leaving). */
18442
19709
  this.onScrollProximityMove = (e) => {
@@ -18480,6 +19747,7 @@ var NativeRenderer = class {
18480
19747
  this.scene.priceStyle = opts.priceStyle;
18481
19748
  this.scene.basePainting = basePaintingOf(opts.priceStyle);
18482
19749
  this.scene.candleOverride = candleOverrideFor(opts.priceStyle, this.scene.style.chartTypes);
19750
+ if (typeof opts.priceAxisTicks === "function") this.priceAxisTicks.setHook(opts.priceAxisTicks);
18483
19751
  }
18484
19752
  this.theme = this.deriveTheme(DARK_THEME);
18485
19753
  }
@@ -18618,6 +19886,15 @@ var NativeRenderer = class {
18618
19886
  case "crosshairOverride":
18619
19887
  this.scene.crosshairOverride = sanitizeCrosshairOverride(value);
18620
19888
  break;
19889
+ case "drawingsInteractive":
19890
+ this.drawingsInteractive = value !== false;
19891
+ if (this.input) this.input.drawings = this.drawingsInteractive;
19892
+ if (!this.drawingsInteractive) this.userDrawings?.clearHover();
19893
+ return;
19894
+ // affects the next gesture only — clearHover repaints what it drops
19895
+ case "priceAxisTicks":
19896
+ this.priceAxisTicks.setHook(typeof value === "function" ? value : null);
19897
+ break;
18621
19898
  case "keyboard":
18622
19899
  this.setKeyboardEnabled(Boolean(value));
18623
19900
  return;
@@ -18733,6 +20010,10 @@ var NativeRenderer = class {
18733
20010
  return { visible: this.scene.marks.visible, groups: { ...this.scene.marks.groups } };
18734
20011
  case "crosshairOverride":
18735
20012
  return this.scene.crosshairOverride ? { ...this.scene.crosshairOverride } : null;
20013
+ case "drawingsInteractive":
20014
+ return this.drawingsInteractive;
20015
+ case "priceAxisTicks":
20016
+ return this.priceAxisTicks.hook;
18736
20017
  case "keyboard":
18737
20018
  return this.keyboardEnabled;
18738
20019
  case "historyChords":
@@ -19068,11 +20349,11 @@ var NativeRenderer = class {
19068
20349
  }
19069
20350
  }
19070
20351
  makeSettingsButton() {
19071
- const btn2 = document.createElement("button");
19072
- btn2.type = "button";
19073
- btn2.title = "Chart settings";
19074
- btn2.innerHTML = iconAt("gear", 14);
19075
- Object.assign(btn2.style, {
20352
+ const btn = document.createElement("button");
20353
+ btn.type = "button";
20354
+ btn.title = "Chart settings";
20355
+ btn.innerHTML = iconAt("gear", 14);
20356
+ Object.assign(btn.style, {
19076
20357
  position: "absolute",
19077
20358
  bottom: `${TIME_AXIS_H + 10}px`,
19078
20359
  right: `${RIGHT_AXIS_W + 10}px`,
@@ -19093,10 +20374,10 @@ var NativeRenderer = class {
19093
20374
  pointerEvents: "auto",
19094
20375
  padding: "0"
19095
20376
  });
19096
- btn2.addEventListener("mouseenter", () => btn2.style.opacity = "1");
19097
- btn2.addEventListener("mouseleave", () => btn2.style.opacity = "0.75");
19098
- btn2.addEventListener("click", () => this.toggleSettingsDialog());
19099
- return btn2;
20377
+ btn.addEventListener("mouseenter", () => btn.style.opacity = "1");
20378
+ btn.addEventListener("mouseleave", () => btn.style.opacity = "0.75");
20379
+ btn.addEventListener("click", () => this.toggleSettingsDialog());
20380
+ return btn;
19100
20381
  }
19101
20382
  /** Port surface: hosts (bottom-bar / chrome buttons) open the same dialog as the
19102
20383
  * in-chart gear — created on demand, independent of the gear feature being enabled. */
@@ -19161,12 +20442,12 @@ var NativeRenderer = class {
19161
20442
  }
19162
20443
  /** Jump-back-to-latest button — same size/border chrome as the drawing-toolbar collapse toggle. */
19163
20444
  makeScrollToRealtimeButton() {
19164
- const btn2 = document.createElement("button");
19165
- btn2.type = "button";
19166
- btn2.title = "Scroll to most recent bar (Alt+Shift+\u2192)";
19167
- btn2.setAttribute("aria-label", "Scroll to most recent bar");
19168
- btn2.innerHTML = this.scrollButtonIcon();
19169
- Object.assign(btn2.style, {
20445
+ const btn = document.createElement("button");
20446
+ btn.type = "button";
20447
+ btn.title = "Scroll to most recent bar (Alt+Shift+\u2192)";
20448
+ btn.setAttribute("aria-label", "Scroll to most recent bar");
20449
+ btn.innerHTML = this.scrollButtonIcon();
20450
+ Object.assign(btn.style, {
19170
20451
  position: "absolute",
19171
20452
  bottom: `${SCROLL_BTN_BOTTOM}px`,
19172
20453
  right: `${SCROLL_BTN_RIGHT}px`,
@@ -19189,10 +20470,10 @@ var NativeRenderer = class {
19189
20470
  pointerEvents: "auto",
19190
20471
  padding: "0"
19191
20472
  });
19192
- btn2.addEventListener("mouseenter", () => btn2.style.opacity = "1");
19193
- btn2.addEventListener("mouseleave", () => btn2.style.opacity = "0.85");
19194
- btn2.addEventListener("click", () => this.scrollToRealtime());
19195
- return btn2;
20473
+ btn.addEventListener("mouseenter", () => btn.style.opacity = "1");
20474
+ btn.addEventListener("mouseleave", () => btn.style.opacity = "0.85");
20475
+ btn.addEventListener("click", () => this.scrollToRealtime());
20476
+ return btn;
19196
20477
  }
19197
20478
  /** Double-chevron icon — the "jump back to the latest bar" affordance. */
19198
20479
  scrollButtonIcon() {
@@ -19215,14 +20496,14 @@ var NativeRenderer = class {
19215
20496
  * appears whenever the latest bars are off-screen and hides again once the view is back
19216
20497
  * at the right edge. */
19217
20498
  updateScrollToRealtimeButton() {
19218
- const btn2 = this.scrollButton;
19219
- if (!btn2) return;
20499
+ const btn = this.scrollButton;
20500
+ if (!btn) return;
19220
20501
  const latestOffScreen = this.coords.barCount > 0 && this.coords.getViewport().rightOffset < 0;
19221
20502
  if (this.layoutMode === "mobile") {
19222
- btn2.style.display = latestOffScreen ? "flex" : "none";
20503
+ btn.style.display = latestOffScreen ? "flex" : "none";
19223
20504
  return;
19224
20505
  }
19225
- btn2.style.display = latestOffScreen && this.pointerNearScrollBtn ? "flex" : "none";
20506
+ btn.style.display = latestOffScreen && this.pointerNearScrollBtn ? "flex" : "none";
19226
20507
  }
19227
20508
  /** Glide the view back to the most recent bars, keeping the current zoom (barSpacing). */
19228
20509
  scrollToRealtime() {
@@ -19406,6 +20687,12 @@ var NativeRenderer = class {
19406
20687
  Object.assign(this.drawingsCanvas.style, { position: "absolute", inset: "0", width: "100%", height: "100%", pointerEvents: "none" });
19407
20688
  this.cursorCanvas = document.createElement("canvas");
19408
20689
  Object.assign(this.cursorCanvas.style, { position: "absolute", inset: "0", width: "100%", height: "100%", pointerEvents: "none" });
20690
+ this.shadeRightEl = document.createElement("div");
20691
+ this.shadeRightEl.className = "vela-shade-right";
20692
+ this.shadeRightEl.setAttribute("aria-hidden", "true");
20693
+ this.shadeRightEl.hidden = true;
20694
+ Object.assign(this.shadeRightEl.style, { position: "absolute", top: "0", left: "0", width: "0", height: "0", pointerEvents: "none", display: "none" });
20695
+ this.shadeRightSig = "";
19409
20696
  this.overlayRoot = document.createElement("div");
19410
20697
  Object.assign(this.overlayRoot.style, { position: "absolute", inset: "0", pointerEvents: "none" });
19411
20698
  this.plot = document.createElement("div");
@@ -19413,7 +20700,7 @@ var NativeRenderer = class {
19413
20700
  this.extLayers = rendererLayers().map((def) => ({ def, instance: def.create(), canvas: this.createLayerCanvas(), channel: def.id, owner: null }));
19414
20701
  const below = this.extLayers.filter((l) => l.def.placement === "below-data").map((l) => l.canvas);
19415
20702
  const above = this.extLayers.filter((l) => l.def.placement !== "below-data").map((l) => l.canvas);
19416
- this.plot.append(this.backdropCanvas, ...below, this.dataCanvas, this.volumeCanvas, this.vpvrCanvas, ...above, this.chromeCanvas, this.drawingsCanvas, this.cursorCanvas, this.overlayRoot);
20703
+ this.plot.append(this.backdropCanvas, ...below, this.dataCanvas, this.volumeCanvas, this.vpvrCanvas, ...above, this.chromeCanvas, this.drawingsCanvas, this.shadeRightEl, this.cursorCanvas, this.overlayRoot);
19417
20704
  this.layerOrderSig = "";
19418
20705
  this.wrapper.appendChild(this.plot);
19419
20706
  this.factoryConfig = this.getConfig();
@@ -19471,13 +20758,14 @@ var NativeRenderer = class {
19471
20758
  drawingsPointerDown: (x, y, snap, shift2, mod) => this.userDrawings?.pointerDown(x, y, snap, shift2, mod),
19472
20759
  drawingsPointerMove: (x, y, snap, shift2, mod) => this.userDrawings?.pointerMove(x, y, snap, shift2, mod),
19473
20760
  drawingsPointerUp: (x, y, snap) => this.userDrawings?.pointerUp(x, y, snap),
19474
- drawingsCursor: (x, y) => this.userDrawings?.cursorAt(x, y) ?? (this.chrome.markGlyphAt(x, y) ? "pointer" : null),
20761
+ drawingsCursor: (x, y) => (this.drawingsInteractive ? this.userDrawings?.cursorAt(x, y) : null) ?? (this.chrome.markGlyphAt(x, y) ? "pointer" : null),
19475
20762
  drawingsDblClick: (x, y) => this.userDrawings?.dblClick(x, y) ?? false,
19476
20763
  drawingsClearTransient: () => this.userDrawings?.clearTransient()
19477
20764
  });
19478
20765
  this.input.rightEdgeZoom = this.zoomAnchorMode === "right";
19479
20766
  this.input.axisDrag = this.axisDragEnabled;
19480
20767
  this.input.paneResize = this.paneResizeEnabled;
20768
+ this.input.drawings = this.drawingsInteractive;
19481
20769
  this.input.attach(this.dataCanvas);
19482
20770
  if (this.settingsEnabled) this.setSettingsEnabled(true);
19483
20771
  this.syncCountdownTimer();
@@ -19524,6 +20812,8 @@ var NativeRenderer = class {
19524
20812
  };
19525
20813
  },
19526
20814
  chartBarMs: () => this.coords.barInterval,
20815
+ timeZone: () => this.scene.timezone,
20816
+ priceDecimals: () => this.scene.priceMintick ? tickDecimals(this.scene.priceMintick) : null,
19527
20817
  snap: (pt, paneId, mode, cursorPx) => this.snapToCandle(pt, paneId, mode, cursorPx),
19528
20818
  setSnapMode: (mode) => this.setSnapMode(mode),
19529
20819
  setToolbarGutter: (px) => this.setToolbarGutter(px)
@@ -19786,6 +21076,8 @@ var NativeRenderer = class {
19786
21076
  this.backend.destroy();
19787
21077
  this.chrome.destroy();
19788
21078
  this.crosshairLayer.destroy();
21079
+ this.shadeRightEl?.remove();
21080
+ this.shadeRightEl = null;
19789
21081
  this.attributionEl?.remove();
19790
21082
  this.attributionEl = null;
19791
21083
  this.mountContainer?.style.removeProperty("--vela-toolbar-gutter");
@@ -20132,14 +21424,21 @@ var NativeRenderer = class {
20132
21424
  this.priceStyleCbs.add(cb);
20133
21425
  return () => this.priceStyleCbs.delete(cb);
20134
21426
  }
21427
+ onPriceStyleWillChange(cb) {
21428
+ this.priceStyleWillChangeCbs.add(cb);
21429
+ return () => this.priceStyleWillChangeCbs.delete(cb);
21430
+ }
20135
21431
  /**
20136
21432
  * THE single write path for the base price style at runtime (feature set / settings dialog /
20137
- * config template — the constructor seeds the field directly, pre-listeners). Updates the
20138
- * scene, eases any reveal layer toward the new style's target, and notifies the core —
20139
- * which owns the DATA side of styles that need one (a chart type's SeriesDataEngine).
21433
+ * config template — the constructor seeds the field directly, pre-listeners). Announces the
21434
+ * switch while the scene still holds the old style (a listener may paint/capture that frame),
21435
+ * then updates the scene, eases any reveal layer toward the new style's target, and notifies
21436
+ * the core — which owns the DATA side of styles that need one (a chart type's SeriesDataEngine).
20140
21437
  */
20141
21438
  setPriceStyle(style) {
20142
21439
  if (style === this.scene.priceStyle) return;
21440
+ const from = this.scene.priceStyle;
21441
+ for (const cb of this.priceStyleWillChangeCbs) cb(from, style);
20143
21442
  this.scene.priceStyle = style;
20144
21443
  this.scene.basePainting = basePaintingOf(style);
20145
21444
  this.scene.candleOverride = candleOverrideFor(style, this.scene.style.chartTypes);
@@ -20369,7 +21668,7 @@ var NativeRenderer = class {
20369
21668
  if (this.easeLiveBar(dtMs)) active = true;
20370
21669
  this.skeletonClockMs += dtMs;
20371
21670
  this.paintData();
20372
- this.crosshairLayer.render(this.scene, this.coords, this.theme, this.hoverSeparatorY, this.externalCrossPx());
21671
+ this.renderCrosshairLayer();
20373
21672
  this.updateLegendValues();
20374
21673
  this.emitViewportChange();
20375
21674
  return active;
@@ -20836,7 +22135,7 @@ var NativeRenderer = class {
20836
22135
  if (bar) parts.push(formatStamp(bar.time, this.scene.timezone));
20837
22136
  if (ohlc) parts.push(`Open ${ohlc.o}, High ${ohlc.h}, Low ${ohlc.l}, Close ${ohlc.c}`);
20838
22137
  for (const group of this.dataWindowGroups(idx, pricePane)) {
20839
- for (const row of group.rows) parts.push(`${row.label} ${row.value}`);
22138
+ for (const row2 of group.rows) parts.push(`${row2.label} ${row2.value}`);
20840
22139
  }
20841
22140
  this.liveRegion.textContent = parts.join(". ");
20842
22141
  }
@@ -20944,10 +22243,31 @@ var NativeRenderer = class {
20944
22243
  this.chrome.render(this.scene, this.coords, this.theme, this.axisSurface());
20945
22244
  this.trackMarkPopover();
20946
22245
  }
20947
- this.crosshairLayer.render(this.scene, this.coords, this.theme, this.hoverSeparatorY, this.externalCrossPx());
22246
+ this.renderCrosshairLayer();
20948
22247
  if (!repaintsData(level) && this.paintedData) this.repaintCursorLayers();
20949
22248
  this.updateLegendValues();
20950
22249
  }
22250
+ /** Repaint the L2 crosshair canvas and keep the `.vela-shade-right` element on the veil it painted. */
22251
+ renderCrosshairLayer() {
22252
+ this.crosshairLayer.render(this.scene, this.coords, this.theme, this.hoverSeparatorY, this.externalCrossPx());
22253
+ this.syncShadeRight();
22254
+ }
22255
+ /** Position (or hide) the `.vela-shade-right` element over the veil of the last crosshair frame. */
22256
+ syncShadeRight() {
22257
+ const el = this.shadeRightEl;
22258
+ if (!el) return;
22259
+ const area = this.crosshairLayer.shadeRightArea;
22260
+ const sig = area ? `${area.x},${area.width},${area.height}` : "";
22261
+ if (sig === this.shadeRightSig) return;
22262
+ this.shadeRightSig = sig;
22263
+ if (!area) {
22264
+ el.hidden = true;
22265
+ el.style.display = "none";
22266
+ return;
22267
+ }
22268
+ Object.assign(el.style, { left: `${area.x}px`, width: `${area.width}px`, height: `${area.height}px`, display: "" });
22269
+ el.hidden = false;
22270
+ }
20951
22271
  /** Repaint the SDK layers that opted into cursor tracking (their own canvas only). */
20952
22272
  repaintCursorLayers() {
20953
22273
  const pane = this.scene.panes.get(PRICE_PANE_ID);
@@ -21741,7 +23061,7 @@ var NativeRenderer = class {
21741
23061
  if (this.animator.active) {
21742
23062
  this.computeScales();
21743
23063
  this.paintData();
21744
- this.crosshairLayer.render(this.scene, this.coords, this.theme, this.hoverSeparatorY, this.externalCrossPx());
23064
+ this.renderCrosshairLayer();
21745
23065
  } else {
21746
23066
  this.scheduler.flushNow(4 /* Full */);
21747
23067
  }
@@ -21771,7 +23091,10 @@ function applyPatch(model, patch) {
21771
23091
  const s = model.series.find((x) => x.id === delta.seriesId);
21772
23092
  if (!s) continue;
21773
23093
  if (delta.kind === "points" && isLineLikeSeries(s)) s.points = delta.points;
21774
- else if (delta.kind === "bars" && (s.kind === "candle" || s.kind === "bar")) s.bars = delta.bars;
23094
+ else if (delta.kind === "bars" && (s.kind === "candle" || s.kind === "bar")) {
23095
+ s.bars = delta.bars;
23096
+ if (delta.barColors !== void 0) s.barColors = delta.barColors;
23097
+ }
21775
23098
  }
21776
23099
  if (patch.lines) model.lines = patch.lines;
21777
23100
  if (patch.boxes) model.boxes = patch.boxes;
@@ -22381,6 +23704,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
22381
23704
  this.activeEngineStyle = null;
22382
23705
  /** The chart's current price style (tracked from the renderer's change events + initial read). */
22383
23706
  this.priceStyle = "candles";
23707
+ this.priceStyleWillChangeUnsub = null;
22384
23708
  this.unresolvedUnsub = null;
22385
23709
  /** Latest chart visible range (left/right bar times), fed to viewport-dependent scripts. */
22386
23710
  this.visibleRange = null;
@@ -22479,6 +23803,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
22479
23803
  this.renderer.onThemeSelect?.((name) => this.setTheme(resolveTheme(name)));
22480
23804
  this.viewportUnsub = this.renderer.onViewportChange((range) => this.onViewportChange(range));
22481
23805
  this.renderer.onPriceStyleChange?.((style) => this.syncPriceStyle(style));
23806
+ this.priceStyleWillChangeUnsub = this.renderer.onPriceStyleWillChange?.((from, to) => this.events.emit("priceStyle:change", { from, to })) ?? null;
22482
23807
  const initialStyle = this.renderer.readFeature("priceStyle");
22483
23808
  if (typeof initialStyle === "string") this.priceStyle = initialStyle;
22484
23809
  this.barTransform = barTransformFor(initialStyle);
@@ -23128,14 +24453,14 @@ var EngineOrchestrator = class _EngineOrchestrator {
23128
24453
  this.replayTickSource = source;
23129
24454
  this.prefetchTicks();
23130
24455
  }
23131
- replayPlay(intervalMs2) {
24456
+ replayPlay(intervalMs) {
23132
24457
  if (!this.replayQueue) return;
23133
- if (intervalMs2 !== void 0) {
23134
- if (!(Number.isFinite(intervalMs2) && intervalMs2 > 0)) {
23135
- console.warn(`[vela] chart.replay.play(${intervalMs2}) ignored \u2014 the interval must be a positive number of ms`);
24458
+ if (intervalMs !== void 0) {
24459
+ if (!(Number.isFinite(intervalMs) && intervalMs > 0)) {
24460
+ console.warn(`[vela] chart.replay.play(${intervalMs}) ignored \u2014 the interval must be a positive number of ms`);
23136
24461
  return;
23137
24462
  }
23138
- this.replayIntervalMs = intervalMs2;
24463
+ this.replayIntervalMs = intervalMs;
23139
24464
  }
23140
24465
  this.clearReplayTimer();
23141
24466
  this.replayPlaying = true;
@@ -24009,6 +25334,8 @@ var EngineOrchestrator = class _EngineOrchestrator {
24009
25334
  if (this.viewportTimer != null) clearTimeout(this.viewportTimer);
24010
25335
  this.viewportUnsub?.();
24011
25336
  this.paneActionUnsub?.();
25337
+ this.priceStyleWillChangeUnsub?.();
25338
+ this.priceStyleWillChangeUnsub = null;
24012
25339
  for (const record of this.registry.all()) {
24013
25340
  this.settleCode(record, { ok: false, error: new Error("[vela] updateCode \u2014 the chart was destroyed.") });
24014
25341
  record.session?.stop();
@@ -24673,7 +26000,7 @@ function modelToValuePatch(model) {
24673
26000
  let to = 0;
24674
26001
  for (const s of model.series) {
24675
26002
  if (s.kind === "candle" || s.kind === "bar") {
24676
- series.push({ seriesId: s.id, kind: "bars", bars: s.bars });
26003
+ series.push({ seriesId: s.id, kind: "bars", bars: s.bars, barColors: s.barColors ?? [] });
24677
26004
  for (const b of s.bars) {
24678
26005
  if (b.time < from) from = b.time;
24679
26006
  if (b.time > to) to = b.time;
@@ -28672,7 +29999,8 @@ var Vela = class {
28672
29999
  glow: options.glow ?? 0,
28673
30000
  upColor: options.upColor ?? BULLISH,
28674
30001
  downColor: options.downColor ?? BEARISH,
28675
- priceStyle: options.priceStyle ?? "candles"
30002
+ priceStyle: options.priceStyle ?? "candles",
30003
+ priceAxisTicks: options.priceAxis?.ticks ?? null
28676
30004
  };
28677
30005
  const RendererClass = options.renderer ?? NativeRenderer;
28678
30006
  if (typeof RendererClass !== "function") {
@@ -29016,4 +30344,4 @@ function resolveElement(container) {
29016
30344
  return element;
29017
30345
  }
29018
30346
 
29019
- export { BUILTIN_PRICE_STYLES, BarStore, CachingDataFeed, DARK_THEME, DataControl, DrawingStore, DrawingToolbar, DrawingsControl, LEGEND_AT_TOP_ATTR, LIGHT_THEME, MarksControl, MultiProviderFeed, NativeRenderer, RendererControl, ReplayControl, SecondClock, TIMEZONES, TypedEventBus, Vela, applyAttributionMarkTheme, buildToolbar, createAttributionMark, createCustomMark, defaultMemberOf, defaultToolbar, groupKeyOf, groupMembers, isExchangeTimezone, isGroupRow, normalizeSession, normalizeTimezone, parseSymbol, priceStyleIds, registerBuiltinChartTypes, resolveTheme, resolveTimezone, sharedBarStore, timeframeToMs, timezoneMenuRows, tzButtonLabel, tzMenuLabel, tzOffset };
30347
+ export { BUILTIN_PRICE_STYLES, BarStore, CANDLE_OVERRIDE_KEYS, CachingDataFeed, DARK_THEME, DataControl, DrawingStore, DrawingToolbar, DrawingsControl, LEGEND_AT_TOP_ATTR, LIGHT_THEME, MarksControl, MultiProviderFeed, NativeRenderer, RendererControl, ReplayControl, SecondClock, TypedEventBus, Vela, applyAttributionMarkTheme, buildToolbar, createAttributionMark, createCustomMark, defaultMemberOf, defaultToolbar, groupKeyOf, groupMembers, isGroupRow, normalizeSession, parseSymbol, priceStyleIds, registerBuiltinChartTypes, resolveTheme, sharedBarStore, timeframeToMs };