@luxalgo/vela 0.8.3 → 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 (44) hide show
  1. package/dist/{DataProvider-wbtJ3-Sa.d.cts → DataProvider-Bw58CjYy.d.cts} +1 -1
  2. package/dist/{DataProvider-oH78tYXL.d.ts → DataProvider-Byo8pEv0.d.ts} +1 -1
  3. package/dist/{chunk-KWI3YDBW.js → chunk-ALTGHKIM.js} +584 -25
  4. package/dist/{chunk-TO3GIK7Z.js → chunk-AUOB7A5Y.js} +2 -2
  5. package/dist/{chunk-VHGACEHO.js → chunk-I2U4NZD2.js} +7 -1
  6. package/dist/{chunk-2IPXYM4B.js → chunk-MOJ4T26N.js} +88 -5
  7. package/dist/{chunk-HMHA733L.js → chunk-UNUVPQFW.js} +1924 -734
  8. package/dist/{chunk-G7B7ZCBF.js → chunk-WY5H4S4I.js} +1 -1
  9. package/dist/{contributions-BhOBRenr.d.cts → contributions-BY3zSYiZ.d.ts} +56 -5
  10. package/dist/{contributions-HGqnNb_P.d.ts → contributions-DOm40g5-.d.cts} +56 -5
  11. package/dist/index.cjs +2583 -828
  12. package/dist/index.d.cts +9 -15
  13. package/dist/index.d.ts +9 -15
  14. package/dist/index.js +4 -4
  15. package/dist/{options-lMUwmRwh.d.cts → options-CSXbjMM5.d.cts} +64 -4
  16. package/dist/{options-lMUwmRwh.d.ts → options-CSXbjMM5.d.ts} +64 -4
  17. package/dist/{plugin-Cr2Pj8q8.d.ts → plugin-BDxUhGO3.d.ts} +3 -3
  18. package/dist/{plugin-Do5J5xuF.d.cts → plugin-CiQVyETg.d.cts} +3 -3
  19. package/dist/plugin.cjs +555 -17
  20. package/dist/plugin.d.cts +4 -4
  21. package/dist/plugin.d.ts +4 -4
  22. package/dist/plugin.js +2 -2
  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/{statusline-model-B-2xGlB_.d.cts → statusline-model-C9tYXuux.d.ts} +19 -4
  30. package/dist/{statusline-model-CY1wzxHg.d.ts → statusline-model-DUBX6Fib.d.cts} +19 -4
  31. package/dist/ui.d.cts +1 -1
  32. package/dist/ui.d.ts +1 -1
  33. package/dist/ui.js +3 -3
  34. package/dist/vela.global.js +2590 -835
  35. package/dist/vela.global.min.js +124 -54
  36. package/dist/widget.cjs +2807 -969
  37. package/dist/widget.d.cts +6 -6
  38. package/dist/widget.d.ts +6 -6
  39. package/dist/widget.js +7 -7
  40. package/dist/workspace.cjs +2807 -969
  41. package/dist/workspace.d.cts +19 -5
  42. package/dist/workspace.d.ts +19 -5
  43. package/dist/workspace.js +6 -6
  44. package/package.json +1 -1
@@ -1,6 +1,6 @@
1
- import { deserializeDrawing, chartTypes, rendererLayers, isLineLikeSeries, seriesShownOn, foldBaseModulation, registerChartType, rendererDefaults, getDrawingType, normalizeTimezone, settingsRowVisible, normalizeSettingsRow, Magnifier, TextLabel, Callout, 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, 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-KWI3YDBW.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-G7B7ZCBF.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, SLATE_DEEP, VALID, INVALID, SLATE, FIELD_FOCUS_CSS, FIELD_FOCUS_RING, svg24, svg24Solid } from './chunk-VHGACEHO.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,6 +1860,74 @@ var SecondClock = class _SecondClock {
1885
1860
  }
1886
1861
  };
1887
1862
 
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;
1873
+ }
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));
1879
+ }
1880
+ function sameToolDefaults(a, b) {
1881
+ return JSON.stringify(a ?? null) === JSON.stringify(b ?? null);
1882
+ }
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
+ ];
1921
+ }
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 [];
1929
+ }
1930
+
1888
1931
  // src/core/drawings/effectiveColor.ts
1889
1932
  var VALID_FILL = VALID;
1890
1933
  var INVALID_FILL = INVALID;
@@ -1982,6 +2025,10 @@ var DrawingHistory = class {
1982
2025
  };
1983
2026
 
1984
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
+ }
1985
2032
  var DrawingController = class {
1986
2033
  constructor(renderer, events, option, seriesGateway) {
1987
2034
  this.events = events;
@@ -2000,8 +2047,10 @@ var DrawingController = class {
2000
2047
  // ordered; [0] is the primary (settings-popup) selection
2001
2048
  this.clipboard = [];
2002
2049
  // in-memory copy buffer (per chart)
2003
- this.lastStyle = /* @__PURE__ */ new Map();
2004
- // 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();
2005
2054
  this.subs = [];
2006
2055
  this.enabled = !!renderer.capabilities.userDrawings && !!renderer.userDrawingsPort;
2007
2056
  this.port = this.enabled ? renderer.userDrawingsPort : null;
@@ -2023,7 +2072,8 @@ var DrawingController = class {
2023
2072
  if (!this.port) return;
2024
2073
  const changed = type !== this.activeTool;
2025
2074
  this.activeTool = type;
2026
- 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);
2027
2077
  if (changed) this.events.emit("drawing:tool", { type });
2028
2078
  }
2029
2079
  getTool() {
@@ -2091,6 +2141,84 @@ var DrawingController = class {
2091
2141
  this.port?.setFavorites?.(list);
2092
2142
  this.events.emit("drawing:favorites", { favorites: list });
2093
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
+ }
2094
2222
  setToolbar(option) {
2095
2223
  this.port?.setToolbar(buildToolbar(option).definition);
2096
2224
  }
@@ -2106,21 +2234,22 @@ var DrawingController = class {
2106
2234
  // ── programmatic CRUD (facade-facing) ── each is one undo step
2107
2235
  add(type, init = {}) {
2108
2236
  if (!this.enabled) return null;
2109
- const last = this.lastStyle.get(type);
2110
- const style = { ...last ?? {}, ...init.style ?? {} };
2237
+ const defaults = this.defaults.get(type);
2111
2238
  const d = createDrawing(type, {
2112
2239
  id: this.store.nextId(),
2113
2240
  paneId: init.paneId ?? "price",
2114
2241
  anchors: init.anchors,
2115
- style,
2116
2242
  text: init.text,
2117
- props: init.props,
2118
2243
  zIndex: init.zIndex ?? this.startZ(type, init.paneId ?? "price")
2119
2244
  });
2120
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);
2121
2250
  this.history.record(this.store.serialize());
2122
2251
  this.store.add(d);
2123
- this.captureStyle(d.id);
2252
+ this.captureDefaults(d.id);
2124
2253
  this.events.emit("drawing:created", { id: d.id });
2125
2254
  return d;
2126
2255
  }
@@ -2132,9 +2261,8 @@ var DrawingController = class {
2132
2261
  const before = this.store.serialize();
2133
2262
  const cur = patch.style ? this.store.get(id) : void 0;
2134
2263
  const resolved = patch.style && cur ? { ...patch, style: { ...cur.style, ...patch.style } } : patch;
2135
- if (this.store.update(id, resolved)) {
2264
+ if (this.updateDrawing(id, resolved)) {
2136
2265
  this.history.record(before);
2137
- this.captureStyle(id);
2138
2266
  this.events.emit("drawing:edited", { id });
2139
2267
  }
2140
2268
  }
@@ -2142,7 +2270,7 @@ var DrawingController = class {
2142
2270
  updateMany(patches) {
2143
2271
  this.history.begin(this.store.serialize());
2144
2272
  for (const { id, patch } of patches) {
2145
- if (this.store.update(id, patch)) {
2273
+ if (this.updateDrawing(id, patch)) {
2146
2274
  this.history.markDirty();
2147
2275
  this.events.emit("drawing:edited", { id });
2148
2276
  }
@@ -2322,10 +2450,27 @@ var DrawingController = class {
2322
2450
  resolve(ids) {
2323
2451
  return ids.map((id) => this.store.get(id)).filter((d) => d != null);
2324
2452
  }
2325
- /** Remember a drawing's style as the "last used" for its type (seeds the next one). */
2326
- 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) {
2327
2466
  const d = this.store.get(id);
2328
- 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 });
2329
2474
  }
2330
2475
  sync() {
2331
2476
  this.port?.syncDrawings(this.store.serialize().drawings);
@@ -2341,22 +2486,20 @@ var DrawingController = class {
2341
2486
  break;
2342
2487
  case "create": {
2343
2488
  const before = this.store.serialize();
2344
- const last = this.lastStyle.get(i.doc.type);
2345
- const style = last ? { ...i.doc.style, ...last } : i.doc.style;
2346
- const d = deserializeDrawing({ ...i.doc, id: this.store.nextId(), style });
2489
+ const d = deserializeDrawing({ ...i.doc, id: this.store.nextId() });
2347
2490
  if (!d) return;
2491
+ applyToolDefaults(d, this.defaults.get(d.type));
2348
2492
  if (!d.zIndex) d.zIndex = this.startZ(d.type, d.paneId) ?? 0;
2349
2493
  this.history.record(before);
2350
2494
  this.store.add(d);
2351
- this.captureStyle(d.id);
2495
+ this.captureDefaults(d.id);
2352
2496
  this.events.emit("drawing:created", { id: d.id });
2353
2497
  break;
2354
2498
  }
2355
2499
  case "edit": {
2356
2500
  const before = this.store.serialize();
2357
- if (this.store.update(i.doc.id, i.doc)) {
2501
+ if (this.updateDrawing(i.doc.id, i.doc)) {
2358
2502
  this.history.record(before);
2359
- this.captureStyle(i.doc.id);
2360
2503
  this.events.emit("drawing:edited", { id: i.doc.id });
2361
2504
  }
2362
2505
  break;
@@ -2364,7 +2507,7 @@ var DrawingController = class {
2364
2507
  case "edit-many": {
2365
2508
  this.history.begin(this.store.serialize());
2366
2509
  for (const doc of i.docs) {
2367
- if (this.store.update(doc.id, doc)) {
2510
+ if (this.updateDrawing(doc.id, doc)) {
2368
2511
  this.history.markDirty();
2369
2512
  this.events.emit("drawing:edited", { id: doc.id });
2370
2513
  }
@@ -2388,6 +2531,12 @@ var DrawingController = class {
2388
2531
  case "favorite":
2389
2532
  this.setFavorite(i.type, i.on);
2390
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;
2391
2540
  case "snap-mode":
2392
2541
  if (i.mode !== this.snapMode) {
2393
2542
  this.snapMode = i.mode;
@@ -3069,17 +3218,17 @@ var DrawingToolbar = class {
3069
3218
  this.paintStay();
3070
3219
  }
3071
3220
  makeButton(icon2, title, onClick) {
3072
- const btn2 = document.createElement("button");
3073
- btn2.type = "button";
3074
- btn2.className = "vela-dtb-btn";
3075
- btn2.setAttribute("aria-label", title);
3076
- this.tipText.set(btn2, title);
3077
- 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);
3078
3227
  this.tipDisposers.push(
3079
- 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" })
3080
3229
  );
3081
- btn2.addEventListener("click", onClick);
3082
- return btn2;
3230
+ btn.addEventListener("click", onClick);
3231
+ return btn;
3083
3232
  }
3084
3233
  divider() {
3085
3234
  const d = document.createElement("div");
@@ -3216,8 +3365,8 @@ function resolveAnimations(animations) {
3216
3365
 
3217
3366
  // src/renderers/shared/IndicatorInputsDialog.ts
3218
3367
  var PROPS_TAB = "Properties";
3219
- function bagOf(row, decl) {
3220
- return decl.prop ? row.propValues : row.values;
3368
+ function bagOf(row2, decl) {
3369
+ return decl.prop ? row2.propValues : row2.values;
3221
3370
  }
3222
3371
  var SOURCES = ["close", "open", "high", "low", "hl2", "hlc3", "ohlc4", "volume"];
3223
3372
  var IndicatorInputsDialog = class {
@@ -3250,13 +3399,13 @@ var IndicatorInputsDialog = class {
3250
3399
  isOpen() {
3251
3400
  return this.dialog !== null;
3252
3401
  }
3253
- open(row) {
3402
+ open(row2) {
3254
3403
  this.close();
3255
- if (row.inputs.length === 0 && row.props.length === 0) return;
3256
- this.row = row;
3257
- this.openId = row.id;
3258
- this.snapshot = { ...row.values };
3259
- 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 };
3260
3409
  ensureDialogStyles();
3261
3410
  const t = this.host.theme();
3262
3411
  const border = "var(--vela-border)";
@@ -3264,7 +3413,7 @@ var IndicatorInputsDialog = class {
3264
3413
  const host = this.host.dialogHost() ?? this.host.container;
3265
3414
  const ui = new Dialog({
3266
3415
  host,
3267
- title: row.title,
3416
+ title: row2.title,
3268
3417
  // Non-modal: live-edit dialog — a modal machine locks pointer events on the
3269
3418
  // whole body, killing the chart and the body-portaled popovers.
3270
3419
  modal: false,
@@ -3295,9 +3444,9 @@ var IndicatorInputsDialog = class {
3295
3444
  theme: () => this.host.theme(),
3296
3445
  text: () => "Close"
3297
3446
  }));
3298
- const tabDefs = tabInputs(row.inputs);
3299
- if (row.props.length > 0) {
3300
- 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) => {
3301
3450
  const d = { ...p, prop: true };
3302
3451
  delete d.tab;
3303
3452
  return d;
@@ -3331,7 +3480,7 @@ var IndicatorInputsDialog = class {
3331
3480
  tab.addEventListener("click", () => activate(idx));
3332
3481
  tabs.appendChild(tab);
3333
3482
  tabEls.push(tab);
3334
- bodies.push(this.buildTabBody(def.inputs, row, gates));
3483
+ bodies.push(this.buildTabBody(def.inputs, row2, gates));
3335
3484
  if (def.inputs.every((d) => d.when)) {
3336
3485
  gates.push({ el: tab, visible: (bag) => def.inputs.some((d) => inputVisible(d.when, bag)) });
3337
3486
  }
@@ -3340,7 +3489,7 @@ var IndicatorInputsDialog = class {
3340
3489
  for (const b of bodies) ui.body.appendChild(b);
3341
3490
  activate(0);
3342
3491
  const refresh = () => {
3343
- const bag = valuesBag(row);
3492
+ const bag = valuesBag(row2);
3344
3493
  for (const g of gates) g.el.style.display = g.visible(bag) ? "" : "none";
3345
3494
  if (tabEls[active]?.style.display === "none") {
3346
3495
  const next = tabEls.findIndex((el) => el.style.display !== "none");
@@ -3368,7 +3517,7 @@ var IndicatorInputsDialog = class {
3368
3517
  }
3369
3518
  /** One tab's scrollable body — one shared grid so every group's controls share a
3370
3519
  * left edge (a 100px number field lines up with a wider session/time pair). */
3371
- buildTabBody(inputs, row, gates) {
3520
+ buildTabBody(inputs, row2, gates) {
3372
3521
  const body = document.createElement("div");
3373
3522
  body.style.cssText = inputDialogBodyStyle(this.host.mobile());
3374
3523
  for (const group of groupInputs(inputs)) {
@@ -3380,7 +3529,7 @@ var IndicatorInputsDialog = class {
3380
3529
  section.appendChild(header);
3381
3530
  }
3382
3531
  for (const inputRow of group.rows) {
3383
- const el = this.buildInputRow(section, inputRow, row);
3532
+ const el = this.buildInputRow(section, inputRow, row2);
3384
3533
  if (inputRow.some((d) => d.when)) {
3385
3534
  gates.push({ el, visible: (bag) => rowVisible(inputRow, bag) });
3386
3535
  }
@@ -3413,26 +3562,26 @@ var IndicatorInputsDialog = class {
3413
3562
  }
3414
3563
  /** Restore every input and prop to its open-time value (re-running the indicator), then close. */
3415
3564
  revertAndClose() {
3416
- const row = this.row;
3565
+ const row2 = this.row;
3417
3566
  const snap = this.snapshot;
3418
- if (row && snap) {
3419
- for (const inp of row.inputs) {
3567
+ if (row2 && snap) {
3568
+ for (const inp of row2.inputs) {
3420
3569
  const snapped = snap[inp.key];
3421
3570
  const before = snapped !== void 0 ? snapped : inp.defval;
3422
- if (row.values[inp.key] !== before) {
3423
- row.values[inp.key] = before;
3424
- 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 });
3425
3574
  }
3426
3575
  }
3427
3576
  }
3428
3577
  const propSnap = this.propSnapshot;
3429
- if (row && propSnap) {
3430
- for (const p of row.props) {
3578
+ if (row2 && propSnap) {
3579
+ for (const p of row2.props) {
3431
3580
  const snapped = propSnap[p.key];
3432
3581
  const before = snapped !== void 0 ? snapped : p.defval;
3433
- if (row.propValues[p.key] !== before) {
3434
- row.propValues[p.key] = before;
3435
- 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" });
3436
3585
  }
3437
3586
  }
3438
3587
  }
@@ -3450,35 +3599,35 @@ var IndicatorInputsDialog = class {
3450
3599
  * rebuild-after-reset move as the chart-settings dialog. The open-time snapshot
3451
3600
  * survives the rebuild, so Cancel after a reset still reverts the whole session. */
3452
3601
  resetToDefaults() {
3453
- const row = this.row;
3454
- if (!row) return;
3602
+ const row2 = this.row;
3603
+ if (!row2) return;
3455
3604
  const snap = this.snapshot;
3456
3605
  const propSnap = this.propSnapshot;
3457
- for (const inp of row.inputs) {
3458
- if (row.values[inp.key] !== inp.defval) {
3459
- row.values[inp.key] = inp.defval;
3460
- 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 });
3461
3610
  }
3462
3611
  }
3463
- for (const p of row.props) {
3464
- if (row.propValues[p.key] !== p.defval) {
3465
- row.propValues[p.key] = p.defval;
3466
- 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" });
3467
3616
  }
3468
3617
  }
3469
- this.open(row);
3618
+ this.open(row2);
3470
3619
  if (snap) this.snapshot = snap;
3471
3620
  if (propSnap) this.propSnapshot = propSnap;
3472
3621
  }
3473
3622
  /** Write one edit through: store it, notify the host, and re-apply the `when` gates. */
3474
- commit(row, decl, value) {
3475
- bagOf(row, decl)[decl.key] = value;
3476
- 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" } : {} });
3477
3626
  this.refreshVisibility?.();
3478
3627
  }
3479
3628
  /** One settings row (or several `inline=` inputs) placed into a section's grid. */
3480
- buildInputRow(grid, decls, row) {
3481
- const idOf = (inp) => `vela-inp-${row.id}-${inp.key}`;
3629
+ buildInputRow(grid, decls, row2) {
3630
+ const idOf = (inp) => `vela-inp-${row2.id}-${inp.key}`;
3482
3631
  const append = (el) => {
3483
3632
  grid.appendChild(el);
3484
3633
  return el;
@@ -3489,7 +3638,7 @@ var IndicatorInputsDialog = class {
3489
3638
  inline: decls.map((d) => ({
3490
3639
  label: nameOf(d) || void 0,
3491
3640
  id: idOf(d),
3492
- control: this.buildControl(row, d, idOf(d)),
3641
+ control: this.buildControl(row2, d, idOf(d)),
3493
3642
  toggleFirst: d.type === "bool",
3494
3643
  fit: d.type === "color" || d.type === "bool"
3495
3644
  })),
@@ -3500,7 +3649,7 @@ var IndicatorInputsDialog = class {
3500
3649
  const id = idOf(lead);
3501
3650
  const info = lead.tooltip ? this.infoButton(lead.tooltip) : void 0;
3502
3651
  if (lead.type === "bool") {
3503
- const current = Boolean(bagOf(row, lead)[lead.key] ?? lead.defval);
3652
+ const current = Boolean(bagOf(row2, lead)[lead.key] ?? lead.defval);
3504
3653
  return append(fieldRow({
3505
3654
  label: nameOf(lead),
3506
3655
  id,
@@ -3509,7 +3658,7 @@ var IndicatorInputsDialog = class {
3509
3658
  toggle: {
3510
3659
  id,
3511
3660
  checked: current,
3512
- onChange: (v) => this.commit(row, lead, v)
3661
+ onChange: (v) => this.commit(row2, lead, v)
3513
3662
  }
3514
3663
  }));
3515
3664
  }
@@ -3519,7 +3668,7 @@ var IndicatorInputsDialog = class {
3519
3668
  id,
3520
3669
  stacked: true,
3521
3670
  info,
3522
- control: this.buildControl(row, lead, id)
3671
+ control: this.buildControl(row2, lead, id)
3523
3672
  }));
3524
3673
  }
3525
3674
  const fit = lead.type === "color" || lead.type === "session" || lead.type === "time";
@@ -3529,13 +3678,13 @@ var IndicatorInputsDialog = class {
3529
3678
  fit,
3530
3679
  controlWidth: fit ? void 0 : 100,
3531
3680
  info,
3532
- control: this.buildControl(row, lead, id)
3681
+ control: this.buildControl(row2, lead, id)
3533
3682
  }));
3534
3683
  }
3535
3684
  /** Build the typed control for one input, committing edits live via `onChange`. */
3536
- buildControl(row, inp, id) {
3537
- const current = bagOf(row, inp)[inp.key] ?? inp.defval;
3538
- 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);
3539
3688
  if (inp.type === "bool") {
3540
3689
  return buildFieldControl({ kind: "switch", id, checked: Boolean(current), onChange: (v) => emit(v) }).el;
3541
3690
  }
@@ -3546,7 +3695,7 @@ var IndicatorInputsDialog = class {
3546
3695
  kind: "color",
3547
3696
  id,
3548
3697
  theme: this.host.theme(),
3549
- get: () => String(bagOf(row, inp)[inp.key] ?? inp.defval),
3698
+ get: () => String(bagOf(row2, inp)[inp.key] ?? inp.defval),
3550
3699
  onChange: (v) => emit(v),
3551
3700
  // An input change re-executes the script over its whole history: commit the
3552
3701
  // opacity drag once on release, not once per pointer move.
@@ -3831,21 +3980,21 @@ var IndicatorInputsDialog = class {
3831
3980
  const picker = this.host.symbolPicker();
3832
3981
  if (!picker) return this.buildTextField(id, current, emit);
3833
3982
  let value = current;
3834
- const btn2 = document.createElement("button");
3835
- btn2.type = "button";
3836
- btn2.id = id;
3837
- 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;`;
3838
3987
  const label = document.createElement("span");
3839
3988
  label.textContent = value || "Select symbol\u2026";
3840
3989
  label.style.cssText = "overflow:hidden;text-overflow:ellipsis;white-space:nowrap;";
3841
- btn2.append(label);
3842
- btn2.addEventListener("click", () => picker(value, (picked) => {
3990
+ btn.append(label);
3991
+ btn.addEventListener("click", () => picker(value, (picked) => {
3843
3992
  if (!picked) return;
3844
3993
  value = picked;
3845
3994
  label.textContent = picked;
3846
3995
  emit(picked);
3847
3996
  }));
3848
- return btn2;
3997
+ return btn;
3849
3998
  }
3850
3999
  /** Shared field chrome for text / number / select controls (fills its wrapper's width). */
3851
4000
  ctrlStyle() {
@@ -3891,9 +4040,9 @@ function tabInputs(inputs) {
3891
4040
  function rowVisible(decls, bag) {
3892
4041
  return decls.some((d) => inputVisible(d.when, bag));
3893
4042
  }
3894
- function valuesBag(row) {
4043
+ function valuesBag(row2) {
3895
4044
  const bag = {};
3896
- 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;
3897
4046
  return bag;
3898
4047
  }
3899
4048
  function groupInputs(inputs) {
@@ -4089,7 +4238,7 @@ var InputsUI = class {
4089
4238
  if (!this.selectedId) return;
4090
4239
  const target = e.target;
4091
4240
  if (target) {
4092
- 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;
4093
4242
  }
4094
4243
  this.clearSelection();
4095
4244
  };
@@ -4172,9 +4321,9 @@ var InputsUI = class {
4172
4321
  setTheme(theme) {
4173
4322
  this.theme = theme;
4174
4323
  for (const lg of this.legends.values()) applyChromeTokens(lg, theme);
4175
- for (const row of this.rows.values()) {
4176
- row.el.style.background = row.highlighted ? theme.background : this.idleRowFill();
4177
- 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;
4178
4327
  }
4179
4328
  if (this.foldToggle) this.syncFoldToggle();
4180
4329
  }
@@ -4193,26 +4342,26 @@ var InputsUI = class {
4193
4342
  */
4194
4343
  setLegendActions(provider) {
4195
4344
  this.legendActions = provider;
4196
- 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);
4197
4346
  }
4198
4347
  /** (Re)build one row's contributed buttons. Same look, reveal and tooltips as the built-ins. */
4199
4348
  renderExtras(id, extrasEl) {
4200
4349
  this.disposeTips(this.extrasTips, id);
4201
4350
  extrasEl.replaceChildren();
4202
4351
  for (const action of this.legendActions?.(id) ?? []) {
4203
- const btn2 = document.createElement("button");
4204
- btn2.type = "button";
4205
- btn2.setAttribute("aria-label", action.tooltip);
4206
- this.tip(this.extrasTips, id, btn2, action.tooltip);
4207
- btn2.innerHTML = iconAt(action.icon, LEGEND_ICON_PX2);
4208
- btn2.className = "vela-ind-ctl";
4209
- btn2.dataset.legendAction = action.id;
4210
- btn2.style.cssText = LEGEND_CTL_CSS;
4211
- 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) => {
4212
4361
  e.stopPropagation();
4213
4362
  action.run();
4214
4363
  });
4215
- extrasEl.appendChild(btn2);
4364
+ extrasEl.appendChild(btn);
4216
4365
  }
4217
4366
  }
4218
4367
  /**
@@ -4222,17 +4371,17 @@ var InputsUI = class {
4222
4371
  */
4223
4372
  setLegendCallouts(provider) {
4224
4373
  this.legendCallouts = provider;
4225
- for (const row of this.rows.values()) this.renderCallouts(row);
4374
+ for (const row2 of this.rows.values()) this.renderCallouts(row2);
4226
4375
  }
4227
4376
  /** (Re)build one row's callout bubbles — tinted icon circles beside the title whose
4228
4377
  * click (when the view carries content) deploys a panel of text and actions. */
4229
- renderCallouts(row) {
4230
- this.disposeTips(this.calloutTips, row.id);
4231
- for (const bubble of row.callouts) bubble.destroy();
4232
- row.callouts = [];
4233
- row.calloutsEl.replaceChildren();
4234
- const views = this.legendCallouts?.(row.id) ?? [];
4235
- 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);
4236
4385
  for (const view of views) {
4237
4386
  const bubble = new CalloutBubble({
4238
4387
  icon: view.icon,
@@ -4246,9 +4395,9 @@ var InputsUI = class {
4246
4395
  theme: () => this.theme
4247
4396
  });
4248
4397
  bubble.el.dataset.legendCallout = view.id;
4249
- this.tip(this.calloutTips, row.id, bubble.el, view.tooltip);
4250
- row.callouts.push(bubble);
4251
- 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);
4252
4401
  }
4253
4402
  }
4254
4403
  /** Reposition the per-pane legend containers after a layout change. */
@@ -4268,9 +4417,9 @@ var InputsUI = class {
4268
4417
  */
4269
4418
  setValuesVisible(visible) {
4270
4419
  this.valuesVisible = visible;
4271
- for (const row of this.rows.values()) {
4272
- row.showValues = null;
4273
- this.renderRowValues(row);
4420
+ for (const row2 of this.rows.values()) {
4421
+ row2.showValues = null;
4422
+ this.renderRowValues(row2);
4274
4423
  }
4275
4424
  }
4276
4425
  /**
@@ -4279,35 +4428,35 @@ var InputsUI = class {
4279
4428
  * unchanged frames: each row diffs against its last rendered values before touching DOM.
4280
4429
  */
4281
4430
  setPlotValues(values) {
4282
- for (const row of this.rows.values()) {
4283
- const next = values.get(row.id) ?? [];
4431
+ for (const row2 of this.rows.values()) {
4432
+ const next = values.get(row2.id) ?? [];
4284
4433
  const key = next.map((v) => `${v.value}|${v.color}`).join("\0");
4285
- if (key === row.plotValuesKey) continue;
4286
- row.plotValues = next;
4287
- row.plotValuesKey = key;
4288
- this.renderRowValues(row);
4434
+ if (key === row2.plotValuesKey) continue;
4435
+ row2.plotValues = next;
4436
+ row2.plotValuesKey = key;
4437
+ this.renderRowValues(row2);
4289
4438
  }
4290
4439
  }
4291
4440
  /** Whether a row currently shows its values: its own override, else the chart-wide flag. */
4292
- rowValuesShown(row) {
4293
- return row.showValues ?? this.valuesVisible;
4441
+ rowValuesShown(row2) {
4442
+ return row2.showValues ?? this.valuesVisible;
4294
4443
  }
4295
4444
  /** (Re)build one row's values readout from its stored plot values + visibility. A
4296
4445
  * highlighted (hovered/selected) row shows only the title + controls — no values. */
4297
- renderRowValues(row) {
4298
- if (!this.rowValuesShown(row) || row.plotValues.length === 0 || row.highlighted) {
4299
- row.valuesEl.style.display = "none";
4300
- 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();
4301
4450
  return;
4302
4451
  }
4303
- row.valuesEl.style.display = "block";
4304
- row.valuesEl.replaceChildren();
4305
- row.plotValues.forEach((v, i) => {
4452
+ row2.valuesEl.style.display = "block";
4453
+ row2.valuesEl.replaceChildren();
4454
+ row2.plotValues.forEach((v, i) => {
4306
4455
  const span = document.createElement("span");
4307
4456
  span.textContent = v.value;
4308
4457
  span.style.color = v.color;
4309
4458
  if (i > 0) span.style.marginLeft = `${LEGEND_VALUES_GAP_PX}px`;
4310
- row.valuesEl.appendChild(span);
4459
+ row2.valuesEl.appendChild(span);
4311
4460
  });
4312
4461
  }
4313
4462
  // ── legend move/merge (menu + drag) ─────────────────────────────────────
@@ -4316,8 +4465,8 @@ var InputsUI = class {
4316
4465
  this.closeMoveMenu();
4317
4466
  const api = this.moveApi;
4318
4467
  if (!api) return;
4319
- const row = this.rows.get(id);
4320
- const currentPane = row?.paneId ?? "price";
4468
+ const row2 = this.rows.get(id);
4469
+ const currentPane = row2?.paneId ?? "price";
4321
4470
  const panes = api.panes();
4322
4471
  const items = [];
4323
4472
  for (const p of panes) {
@@ -4357,13 +4506,13 @@ var InputsUI = class {
4357
4506
  openRowMenu(id, x, y) {
4358
4507
  this.closeRowMenu();
4359
4508
  this.closeMoveMenu();
4360
- const row = this.rows.get(id);
4361
- if (!row) return;
4509
+ const row2 = this.rows.get(id);
4510
+ if (!row2) return;
4362
4511
  this.rowMenuId = id;
4363
4512
  const items = [{
4364
4513
  id: "values",
4365
4514
  label: "Indicator values",
4366
- checked: this.rowValuesShown(row)
4515
+ checked: this.rowValuesShown(row2)
4367
4516
  }];
4368
4517
  if (!this.rowMenu) {
4369
4518
  this.rowMenu = new Menu({
@@ -4390,11 +4539,11 @@ var InputsUI = class {
4390
4539
  }
4391
4540
  /** Move a legend row to another pane (indicator merged/moved), tidying an emptied container. */
4392
4541
  setPane(id, paneId) {
4393
- const row = this.rows.get(id);
4394
- if (!row || row.paneId === paneId) return;
4395
- const prev2 = row.paneId;
4396
- row.paneId = paneId;
4397
- 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);
4398
4547
  this.syncFoldToggle();
4399
4548
  if (prev2 !== "price") {
4400
4549
  const lg = this.legends.get(prev2);
@@ -4424,9 +4573,9 @@ var InputsUI = class {
4424
4573
  lg.style.maxWidth = `calc(100% - ${(bounds.rightAxis ?? 0) + LEGEND_LEFT_PX + LEGEND_SCALE_GAP_PX}px)`;
4425
4574
  const collapsed = this.paneCollapse.has(paneId);
4426
4575
  const masterId = this.paneCollapse.get(paneId) ?? null;
4427
- for (const row of this.rows.values()) {
4428
- if (row.paneId !== paneId) continue;
4429
- 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";
4430
4579
  }
4431
4580
  let top = bounds.top + 8;
4432
4581
  if (collapsed && Number.isFinite(bounds.height)) {
@@ -4460,13 +4609,13 @@ var InputsUI = class {
4460
4609
  if (wasFolded && count > 0) this.reposition();
4461
4610
  return;
4462
4611
  }
4463
- let btn2 = this.foldToggle;
4464
- if (!btn2) {
4465
- btn2 = document.createElement("button");
4466
- btn2.type = "button";
4467
- btn2.className = "vela-ind-fold";
4468
- this.tip(this.rowTips, "fold", btn2, () => this.overviewAction !== null ? "Indicators" : this.legendFolded ? "Show indicator legend" : "Hide indicator legend");
4469
- 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", () => {
4470
4619
  if (this.overviewAction !== null) {
4471
4620
  this.overviewAction();
4472
4621
  return;
@@ -4474,24 +4623,24 @@ var InputsUI = class {
4474
4623
  this.legendFolded = !this.legendFolded;
4475
4624
  this.syncFoldToggle();
4476
4625
  });
4477
- this.foldToggle = btn2;
4626
+ this.foldToggle = btn;
4478
4627
  }
4479
4628
  const folded = this.legendFolded;
4480
- 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");
4481
4630
  const ink = this.theme.textColor;
4482
- 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;`;
4483
- 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();
4484
4633
  const icon2 = document.createElement("span");
4485
4634
  icon2.style.cssText = "display:inline-flex;align-items:center;line-height:0;opacity:0.7;";
4486
4635
  icon2.innerHTML = this.overviewAction !== null ? OVERVIEW_SVG : folded ? UNFOLD_SVG : FOLD_SVG;
4487
- btn2.appendChild(icon2);
4636
+ btn.appendChild(icon2);
4488
4637
  if (folded) {
4489
4638
  const label = document.createElement("span");
4490
4639
  label.textContent = String(count);
4491
4640
  label.style.cssText = `font-weight:600;line-height:normal;font-size:12px;color:${ink};`;
4492
- btn2.appendChild(label);
4641
+ btn.appendChild(label);
4493
4642
  }
4494
- this.legendFor("price").appendChild(btn2);
4643
+ this.legendFor("price").appendChild(btn);
4495
4644
  this.reposition();
4496
4645
  }
4497
4646
  /** Create or update an indicator's legend row (in the legend for its pane). */
@@ -4573,8 +4722,8 @@ var InputsUI = class {
4573
4722
  eye.className = "vela-ind-ctl";
4574
4723
  eye.style.cssText = LEGEND_CTL_CSS;
4575
4724
  eye.addEventListener("click", () => {
4576
- const row2 = this.rows.get(id);
4577
- this.onToggleVisible?.(id, Boolean(row2?.hidden));
4725
+ const row3 = this.rows.get(id);
4726
+ this.onToggleVisible?.(id, Boolean(row3?.hidden));
4578
4727
  });
4579
4728
  controlsEl.appendChild(eye);
4580
4729
  eyeEl = eye;
@@ -4619,9 +4768,9 @@ var InputsUI = class {
4619
4768
  controlsEl.appendChild(close);
4620
4769
  el.appendChild(controlsEl);
4621
4770
  this.attach(this.legendFor(paneId), el, !!opts.native);
4622
- 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 };
4623
- this.rows.set(id, row);
4624
- 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);
4625
4774
  this.syncFoldToggle();
4626
4775
  }
4627
4776
  /** Place a row in its pane's legend — native rows PREPEND (pinned to the top), Pine rows append. */
@@ -4631,10 +4780,10 @@ var InputsUI = class {
4631
4780
  }
4632
4781
  /** Reflect programmatic input changes (so a re-opened dialog shows current values). */
4633
4782
  setValues(id, values, props) {
4634
- const row = this.rows.get(id);
4635
- if (!row) return;
4636
- row.values = { ...row.values, ...values };
4637
- 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 };
4638
4787
  }
4639
4788
  /**
4640
4789
  * Reflect an indicator's live status in its legend row: `'loading'` shows three pulsing
@@ -4642,9 +4791,9 @@ var InputsUI = class {
4642
4791
  * at legend scale), `'live'` a pulsing dot, `'idle'` nothing. Rendered just right of the title.
4643
4792
  */
4644
4793
  setStatus(id, status) {
4645
- const row = this.rows.get(id);
4646
- if (!row) return;
4647
- const el = row.statusEl;
4794
+ const row2 = this.rows.get(id);
4795
+ if (!row2) return;
4796
+ const el = row2.statusEl;
4648
4797
  el.replaceChildren();
4649
4798
  if (status === "idle") {
4650
4799
  el.style.cssText = "display:none;box-sizing:border-box;flex:none;";
@@ -4673,15 +4822,15 @@ var InputsUI = class {
4673
4822
  }
4674
4823
  /** Mark a row hidden/shown — swaps the eye glyph and dims the row; does NOT remove it. */
4675
4824
  setVisible(id, visible) {
4676
- const row = this.rows.get(id);
4677
- if (!row) return;
4678
- row.hidden = !visible;
4679
- row.el.style.opacity = visible ? "1" : "0.5";
4680
- if (row.eyeEl) {
4681
- row.eyeEl.innerHTML = visible ? EYE_SVG : EYE_OFF_SVG;
4682
- 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");
4683
4832
  }
4684
- this.syncRowActions(row);
4833
+ this.syncRowActions(row2);
4685
4834
  }
4686
4835
  /**
4687
4836
  * Select a legend row (outlining it) and clear any previous selection, so at most one
@@ -4700,32 +4849,32 @@ var InputsUI = class {
4700
4849
  * visible even when idle, so it can be un-hidden without hovering.
4701
4850
  */
4702
4851
  setRowHighlighted(id, highlighted) {
4703
- const row = this.rows.get(id);
4704
- if (!row) return;
4705
- row.highlighted = highlighted;
4706
- this.renderRowValues(row);
4707
- row.el.style.boxShadow = highlighted ? `inset 0 0 0 1px ${this.neutralBorder()}` : "none";
4708
- row.el.style.background = highlighted ? this.theme.background : this.idleRowFill();
4709
- 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);
4710
4859
  }
4711
4860
  /**
4712
4861
  * Reveal the action cluster (or just the eye for a hidden indicator). Non-eye
4713
4862
  * buttons only appear while highlighted; the eye keeps its inline-flex from
4714
4863
  * construction and is shown/hidden with the cluster container.
4715
4864
  */
4716
- syncRowActions(row) {
4717
- const open = row.highlighted;
4718
- 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";
4719
4868
  if (open) {
4720
- row.el.appendChild(row.statusEl);
4721
- for (const bubble of row.callouts) bubble.hidePanel();
4869
+ row2.el.appendChild(row2.statusEl);
4870
+ for (const bubble of row2.callouts) bubble.hidePanel();
4722
4871
  } else {
4723
- row.el.insertBefore(row.statusEl, row.valuesEl);
4872
+ row2.el.insertBefore(row2.statusEl, row2.valuesEl);
4724
4873
  }
4725
- row.calloutsEl.style.display = legendCalloutsDisplay(open, row.callouts.length > 0);
4726
- for (const child of Array.from(row.controlsEl.children)) {
4727
- if (!(child instanceof HTMLElement) || child === row.eyeEl) continue;
4728
- 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) {
4729
4878
  child.style.display = open ? "contents" : "none";
4730
4879
  continue;
4731
4880
  }
@@ -4739,9 +4888,9 @@ var InputsUI = class {
4739
4888
  this.selectedId = null;
4740
4889
  }
4741
4890
  remove(id) {
4742
- const row = this.rows.get(id);
4743
- for (const bubble of row?.callouts ?? []) bubble.destroy();
4744
- row?.el.remove();
4891
+ const row2 = this.rows.get(id);
4892
+ for (const bubble of row2?.callouts ?? []) bubble.destroy();
4893
+ row2?.el.remove();
4745
4894
  this.rows.delete(id);
4746
4895
  this.disposeTips(this.rowTips, id);
4747
4896
  this.disposeTips(this.extrasTips, id);
@@ -4749,11 +4898,11 @@ var InputsUI = class {
4749
4898
  if (this.selectedId === id) this.selectedId = null;
4750
4899
  if (this.inputsDialog.openId === id) this.inputsDialog.close();
4751
4900
  this.syncFoldToggle();
4752
- if (row && row.paneId !== "price") {
4753
- const lg = this.legends.get(row.paneId);
4901
+ if (row2 && row2.paneId !== "price") {
4902
+ const lg = this.legends.get(row2.paneId);
4754
4903
  if (lg && lg.childElementCount === 0) {
4755
4904
  lg.remove();
4756
- this.legends.delete(row.paneId);
4905
+ this.legends.delete(row2.paneId);
4757
4906
  }
4758
4907
  }
4759
4908
  }
@@ -4766,7 +4915,7 @@ var InputsUI = class {
4766
4915
  for (const id of [...this.rowTips.keys()]) this.disposeTips(this.rowTips, id);
4767
4916
  for (const id of [...this.extrasTips.keys()]) this.disposeTips(this.extrasTips, id);
4768
4917
  for (const id of [...this.calloutTips.keys()]) this.disposeTips(this.calloutTips, id);
4769
- 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();
4770
4919
  for (const lg of this.legends.values()) lg.remove();
4771
4920
  this.legends.clear();
4772
4921
  this.rows.clear();
@@ -4787,8 +4936,8 @@ var InputsUI = class {
4787
4936
  this.inputsDialog.close();
4788
4937
  }
4789
4938
  openDialog(id) {
4790
- const row = this.rows.get(id);
4791
- if (row) this.inputsDialog.open(row);
4939
+ const row2 = this.rows.get(id);
4940
+ if (row2) this.inputsDialog.open(row2);
4792
4941
  }
4793
4942
  /** Idle legend-row fill — a translucent wash of the chart background, so the title keeps
4794
4943
  * contrast when candles reach it without laying a solid block over the plot. */
@@ -4960,9 +5109,9 @@ var PaneControls = class {
4960
5109
  const soloChip = stateChipRole != null && !hovered;
4961
5110
  cluster.style.background = soloChip ? "transparent" : CLUSTER_PILL;
4962
5111
  for (const child of cluster.children) {
4963
- const btn2 = child;
4964
- btn2.style.display = "inline-flex";
4965
- 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";
4966
5115
  }
4967
5116
  }
4968
5117
  }
@@ -9976,12 +10125,12 @@ var MARK_DECK_STEP = 3;
9976
10125
  var MARK_FAN_GAP = 4;
9977
10126
  var MARK_HIT_PAD = 3;
9978
10127
  var MARK_FAN_HOLD = 8;
9979
- function snapMarkBar(time, barTimes, intervalMs2) {
10128
+ function snapMarkBar(time, barTimes, intervalMs) {
9980
10129
  const n = barTimes.length;
9981
- if (n === 0 || !(intervalMs2 > 0) || !Number.isFinite(time)) return null;
10130
+ if (n === 0 || !(intervalMs > 0) || !Number.isFinite(time)) return null;
9982
10131
  if (time < barTimes[0]) return null;
9983
10132
  const last = barTimes[n - 1];
9984
- 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);
9985
10134
  let lo = 0;
9986
10135
  let hi = n - 1;
9987
10136
  while (lo < hi) {
@@ -9989,14 +10138,14 @@ function snapMarkBar(time, barTimes, intervalMs2) {
9989
10138
  if (barTimes[mid] <= time) lo = mid;
9990
10139
  else hi = mid - 1;
9991
10140
  }
9992
- if (time < barTimes[lo] + intervalMs2) return lo;
10141
+ if (time < barTimes[lo] + intervalMs) return lo;
9993
10142
  return lo + 1;
9994
10143
  }
9995
- function clusterMarks(marks, barTimes, intervalMs2, hidden) {
10144
+ function clusterMarks(marks, barTimes, intervalMs, hidden) {
9996
10145
  const byKey = /* @__PURE__ */ new Map();
9997
10146
  marks.forEach((m, seq) => {
9998
10147
  if (m.group !== void 0 && hidden(m.group)) return;
9999
- const bar = snapMarkBar(m.time, barTimes, intervalMs2);
10148
+ const bar = snapMarkBar(m.time, barTimes, intervalMs);
10000
10149
  if (bar === null) return;
10001
10150
  const key = `${bar}|${m.group ?? ""}`;
10002
10151
  let c = byKey.get(key);
@@ -11603,16 +11752,16 @@ var SettingsDialog = class {
11603
11752
  }
11604
11753
  };
11605
11754
  for (const { group: g, depth } of rows) {
11606
- const row = this.boolRow(g.label, this.markGroupOwnVisible(g.id), (v) => {
11755
+ const row2 = this.boolRow(g.label, this.markGroupOwnVisible(g.id), (v) => {
11607
11756
  this.emit({ marks: { groups: { [g.id]: v } } });
11608
11757
  refreshDimming();
11609
11758
  });
11610
11759
  if (depth > 0) {
11611
- row.classList.add("vela-sd-nested");
11612
- row.style.setProperty("--vela-sd-depth", String(depth));
11760
+ row2.classList.add("vela-sd-nested");
11761
+ row2.style.setProperty("--vela-sd-depth", String(depth));
11613
11762
  }
11614
- rowsById.set(g.id, row);
11615
- body.append(sid(row, markGroupSettingsId(g.id)));
11763
+ rowsById.set(g.id, row2);
11764
+ body.append(sid(row2, markGroupSettingsId(g.id)));
11616
11765
  }
11617
11766
  refreshDimming();
11618
11767
  }
@@ -12434,6 +12583,16 @@ var DrawingPainter = class {
12434
12583
  this.paintMagnifier(ctx, d, proj, theme);
12435
12584
  return;
12436
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
+ }
12437
12596
  if (d instanceof PatternDrawing) {
12438
12597
  this.paintPattern(ctx, d, proj, theme);
12439
12598
  return;
@@ -12476,9 +12635,11 @@ var DrawingPainter = class {
12476
12635
  case "arrow": {
12477
12636
  const pts = d.handlePoints(proj);
12478
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);
12479
12640
  this.stroke(ctx, d.style, () => {
12480
- ctx.moveTo(pts[0][0], pts[0][1]);
12481
- ctx.lineTo(pts[1][0], pts[1][1]);
12641
+ ctx.moveTo(ex1, ey1);
12642
+ ctx.lineTo(ex2, ey2);
12482
12643
  });
12483
12644
  if (d.style.arrowRight) this.paintArrowhead(ctx, pts[0], pts[1], d.style);
12484
12645
  if (d.style.arrowLeft) this.paintArrowhead(ctx, pts[1], pts[0], d.style);
@@ -12620,9 +12781,15 @@ var DrawingPainter = class {
12620
12781
  ctx.font = `${text.bold ? "bold " : ""}${text.italic ? "italic " : ""}${fs}px ${theme.fontFamily}`;
12621
12782
  ctx.textBaseline = "top";
12622
12783
  ctx.textAlign = layout.align;
12623
- ctx.fillStyle = text.color ?? theme.textColor;
12624
12784
  const lh = labelLineHeight(fs);
12625
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;
12626
12793
  lines.forEach((line, i) => ctx.fillText(line, layout.x, layout.top + i * lh));
12627
12794
  if (d.type === "text") this.paintTextFrame(ctx, d.id, layout.x, layout.top, lines, fs, theme);
12628
12795
  ctx.restore();
@@ -12677,6 +12844,78 @@ var DrawingPainter = class {
12677
12844
  ctx.textAlign = "left";
12678
12845
  ctx.textBaseline = "alphabetic";
12679
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
+ }
12680
12919
  /** Paint a concentric-ring fib tool (circles / arcs / wedge): each enabled level as an arc of
12681
12920
  * radius R0·ratio over the tool's angular sweep, plus any bounding lines + a ratio label per ring. */
12682
12921
  paintRadialFib(ctx, d, proj, theme) {
@@ -13146,15 +13385,15 @@ var DrawingPainter = class {
13146
13385
  const { profile, maxW, anchorX, grow, yEdges } = L;
13147
13386
  if (profile.maxTotal <= 0 || maxW <= 0) return;
13148
13387
  for (let k = 0; k < profile.rows.length; k += 1) {
13149
- const row = profile.rows[k];
13150
- const total = row.up + row.down;
13388
+ const row2 = profile.rows[k];
13389
+ const total = row2.up + row2.down;
13151
13390
  if (total <= 0) continue;
13152
13391
  const yA = yEdges[k];
13153
13392
  const yB = yEdges[k + 1];
13154
13393
  const yTop = Math.min(yA, yB);
13155
13394
  const h = Math.max(1, Math.abs(yB - yA) - 1);
13156
13395
  const w2 = total / profile.maxTotal * maxW;
13157
- const upW = row.up / total * w2;
13396
+ const upW = row2.up / total * w2;
13158
13397
  const inVa = k >= profile.vaFrom && k <= profile.vaTo;
13159
13398
  const upColor = inVa ? s.vaUpColor : s.upColor;
13160
13399
  const downColor = inVa ? s.vaDownColor : s.downColor;
@@ -13619,11 +13858,20 @@ var DrawingPainter = class {
13619
13858
  ctx.arc(x1, y1, arcR, Math.min(0, end), Math.max(0, end));
13620
13859
  ctx.stroke();
13621
13860
  }
13622
- 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;
13623
13864
  ctx.font = `11px ${theme.fontFamily}`;
13624
13865
  ctx.textAlign = "left";
13625
13866
  ctx.textBaseline = "alphabetic";
13626
- 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);
13627
13875
  }
13628
13876
  /** Paint a fixed-size arrow marker as a filled glyph at its anchor. */
13629
13877
  paintArrowMark(ctx, d, proj) {
@@ -13766,6 +14014,12 @@ function labelLayout(d, proj) {
13766
14014
  const pts = d.handlePoints(proj);
13767
14015
  const lines = text.value.split("\n").length;
13768
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
+ }
13769
14023
  switch (d.type) {
13770
14024
  case "text":
13771
14025
  return pts[0] ? { x: pts[0][0] + 2, top: pts[0][1] + 2, align: "left" } : null;
@@ -13780,11 +14034,13 @@ function labelLayout(d, proj) {
13780
14034
  if (pts.length < 2) return null;
13781
14035
  const [x1, y1] = pts[0];
13782
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";
13783
14039
  return {
13784
14040
  x: 0,
13785
- top: -6 - lh * lines,
14041
+ top: below ? 6 : -6 - lh * lines,
13786
14042
  align: "center",
13787
- 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 }
13788
14044
  };
13789
14045
  }
13790
14046
  case "box": {
@@ -13965,8 +14221,9 @@ var DrawingInteraction = class {
13965
14221
  const tool = this.deps.activeTool();
13966
14222
  if (tool) {
13967
14223
  const paneId = proj.paneIdAtY(y) ?? "price";
13968
- 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)] });
13969
14225
  if (!draft) return;
14226
+ applyToolDefaults(draft, this.deps.toolDefaults());
13970
14227
  this.state = { kind: "placing", draft, need: draft.anchorSchema().max, cursor: null };
13971
14228
  if (draft.placementMode() === "click" && draft.anchors.length >= this.state.need) this.finalize();
13972
14229
  else this.deps.changed();
@@ -14165,7 +14422,8 @@ var DrawingInteraction = class {
14165
14422
  if (this.state.kind !== "placing") return null;
14166
14423
  const { draft, cursor } = this.state;
14167
14424
  if (!cursor) return draft.anchors.length >= 1 && draft.type === "hline" ? draft : null;
14168
- 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 });
14169
14427
  }
14170
14428
  /** Map a raw placement cursor to the anchor the drawing actually stores.
14171
14429
  * Parallel channel: its 3rd anchor is the parallel-line price measured AT THE
@@ -14269,7 +14527,7 @@ function applyFree(anchor, pt, free) {
14269
14527
  if (free === "both" || free === "y") anchor.price = pt.price;
14270
14528
  }
14271
14529
 
14272
- // src/renderers/native/drawings/DrawingSettingsDialog.ts
14530
+ // src/renderers/native/drawings/DrawingSettingsSections.ts
14273
14531
  var LEVEL_UNITS = [
14274
14532
  { value: "price", label: "Price" },
14275
14533
  { value: "points", label: "Points" }
@@ -14278,15 +14536,319 @@ var FRVP_ANCHOR = [
14278
14536
  { value: "right", label: "Right" },
14279
14537
  { value: "left", label: "Left" }
14280
14538
  ];
14281
- var TITLES = {
14282
- position: "Position size",
14283
- frvp: "Volume profile",
14284
- 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>'
14285
14763
  };
14286
- var DrawingSettingsDialog = class {
14287
- 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) {
14288
14847
  this.host = host;
14848
+ this.env = env;
14289
14849
  this.ui = null;
14850
+ this.menu = null;
14851
+ this.refresh = null;
14290
14852
  this.theme = theme;
14291
14853
  }
14292
14854
  setTheme(theme) {
@@ -14296,69 +14858,97 @@ var DrawingSettingsDialog = class {
14296
14858
  return this.ui != null;
14297
14859
  }
14298
14860
  contains(node) {
14299
- 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?.();
14300
14866
  }
14301
- open(drawing, actions, kind) {
14867
+ open(drawing, actions, anchor) {
14302
14868
  this.close();
14303
- const live = actions.resolve() ?? drawing;
14304
- const snapshot = clonePlain(live.serialize());
14305
- const grid = fieldGrid({ variant: "inputs" });
14306
- grid.style.padding = "16px 20px";
14307
- grid.style.overflowY = "auto";
14308
- grid.style.overflowX = "hidden";
14309
- grid.style.flex = "1 1 auto";
14310
- if (kind === "position" && live instanceof PositionTool) this.buildPosition(grid, live, actions);
14311
- else if (kind === "frvp" && live instanceof FixedRangeVolumeProfile) this.buildFrvp(grid, live, actions);
14312
- else if (kind === "levels") this.buildLevels(grid, live, actions);
14313
- 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
+ };
14314
14904
  const ui = new Dialog({
14315
14905
  host: this.host,
14316
- title: TITLES[kind],
14317
- // Non-modal: live-edit dialog — a modal machine locks pointer events on the
14318
- // whole body, killing the chart and the body-portaled popovers.
14906
+ title: meta?.label ?? "Settings",
14319
14907
  modal: false,
14320
14908
  contained: true,
14321
14909
  align: "center",
14322
14910
  draggable: true,
14323
14911
  flush: true,
14324
- className: "vela-dialog--form",
14912
+ className: "vela-dsp",
14325
14913
  closeOnEscape: false,
14326
- footer: (foot) => {
14327
- foot.append(
14328
- btn("Cancel", false, () => {
14329
- actions.restore?.(snapshot);
14330
- this.close();
14331
- }),
14332
- btn("Ok", true, () => this.close())
14333
- );
14334
- },
14914
+ footer: (foot) => foot.append(button("Cancel", false, cancel), button("Ok", true, () => this.close())),
14335
14915
  onOpenChange: (open) => {
14336
14916
  if (!open) this.close();
14337
14917
  }
14338
14918
  });
14339
14919
  applyChromeTokens(ui.panel, this.theme);
14340
- ui.body.appendChild(grid);
14341
- const onBackdropDown = (e) => {
14342
- if (e.target !== ui.backdrop && e.target !== ui.positioner) return;
14343
- if (isPopoverOpen() || eventDismissedPopover(e)) {
14344
- closeOpenPopovers();
14345
- return;
14346
- }
14347
- this.close();
14348
- };
14349
- ui.backdrop.addEventListener("pointerdown", onBackdropDown);
14350
- 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);
14351
14929
  ui.panel.addEventListener("keydown", (e) => {
14352
14930
  if (e.key === "Escape") {
14353
14931
  e.preventDefault();
14354
- actions.restore?.(snapshot);
14355
- this.close();
14932
+ if (this.menu) this.menu.hide();
14933
+ else cancel();
14356
14934
  }
14357
14935
  });
14358
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();
14359
14946
  ui.show();
14360
14947
  }
14361
14948
  close() {
14949
+ this.menu?.hide();
14950
+ this.menu = null;
14951
+ this.refresh = null;
14362
14952
  const ui = this.ui;
14363
14953
  this.ui = null;
14364
14954
  ui?.destroy();
@@ -14366,292 +14956,771 @@ var DrawingSettingsDialog = class {
14366
14956
  destroy() {
14367
14957
  this.close();
14368
14958
  }
14369
- buildPosition(grid, drawing, actions) {
14370
- const live = () => {
14371
- const d = actions.resolve();
14372
- return d instanceof PositionTool ? d : drawing;
14373
- };
14374
- const refreshers = [];
14375
- const refreshAll = () => refreshers.forEach((f) => f());
14376
- const fmt = (n) => Math.round(n * 1e8) / 1e8;
14377
- const numberRow = (label, path, clamp) => {
14378
- const ctrl = buildFieldControl({
14379
- kind: "number",
14380
- value: fmt(live()[path]),
14381
- min: clamp?.min,
14382
- max: clamp?.max,
14383
- step: clamp?.step,
14384
- integer: clamp?.integer,
14385
- fill: false,
14386
- commit: "blur",
14387
- onChange: (n) => {
14388
- actions.patch({ [path]: n });
14389
- refreshAll();
14390
- }
14391
- });
14392
- refreshers.push(() => {
14393
- if (document.activeElement !== ctrl.el.querySelector("input")) ctrl.setValue?.(fmt(live()[path]));
14394
- });
14395
- grid.appendChild(fieldRow({ label, control: ctrl.el }));
14396
- };
14397
- const levelRow = (label, path, level) => {
14398
- let mode = "price";
14399
- const display = () => fmt(live().levelDisplayValue(level, mode));
14400
- const ni = buildFieldControl({
14401
- kind: "number",
14402
- value: display(),
14403
- fill: false,
14404
- commit: "blur",
14405
- onChange: (n) => {
14406
- actions.patch({ [path]: live().levelPriceFromDisplay(level, mode, n) });
14407
- refreshAll();
14408
- }
14409
- });
14410
- const sel = buildFieldControl({
14411
- kind: "select",
14412
- options: LEVEL_UNITS,
14413
- value: mode,
14414
- fill: false,
14415
- theme: this.theme,
14416
- onChange: (v) => {
14417
- mode = v;
14418
- ni.setValue?.(display());
14419
- }
14420
- });
14421
- refreshers.push(() => {
14422
- if (document.activeElement !== ni.el.querySelector("input")) ni.setValue?.(display());
14423
- });
14424
- grid.appendChild(fieldRow({ label, control: [ni.el, sel.el] }));
14425
- };
14426
- grid.appendChild(fieldSection("Account", { variant: "inputs", first: true }));
14427
- numberRow("Risk %", "riskPercent", { min: 0, max: 100, step: 0.1 });
14428
- numberRow("Account balance", "accountBalance", { min: 0, max: 1e12, step: 1, integer: true });
14429
- numberRow("Position size", "quantity");
14430
- grid.appendChild(fieldSection("Levels", { variant: "inputs" }));
14431
- const dir = buildFieldControl({
14432
- kind: "select",
14433
- options: DIRECTION_OPTIONS,
14434
- value: live().direction,
14435
- fill: false,
14436
- theme: this.theme,
14437
- onChange: (v) => {
14438
- actions.patch({ direction: v });
14439
- 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;
14440
15035
  }
14441
- });
14442
- refreshers.push(() => dir.setValue?.(live().direction));
14443
- grid.appendChild(fieldRow({ label: "Direction", control: dir.el }));
14444
- numberRow("Entry price", "entryPrice");
14445
- levelRow("Stop", "stopPrice", "stop");
14446
- levelRow("Target", "targetPrice", "target");
14447
- grid.appendChild(fieldSection("Display", { variant: "inputs" }));
14448
- const toggles = [
14449
- ["Show text", "showText"],
14450
- ["Show direction & ratio", "showHeader"],
14451
- ["Show loss & size", "showLossSize"],
14452
- ["Show target label", "showTargetLabel"],
14453
- ["Show stop label", "showStopLabel"],
14454
- ["Show level prices", "showPrices"]
14455
- ];
14456
- for (const [label, path] of toggles) {
14457
- grid.appendChild(fieldRow({
14458
- label,
14459
- bool: true,
14460
- toggle: {
14461
- checked: live()[path],
14462
- onChange: (v) => {
14463
- actions.patch({ [path]: v });
14464
- refreshAll();
14465
- },
14466
- get: () => live()[path]
14467
- }
14468
- }));
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;
14469
15074
  }
14470
- const summary = document.createElement("div");
14471
- summary.className = "vela-field-span";
14472
- 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;";
14473
- refreshers.push(() => {
14474
- const d = live();
14475
- summary.textContent = `${d.headerLabel()} \u2014 ${d.lossSizeLabel()}`;
14476
- });
14477
- grid.appendChild(summary);
14478
- refreshAll();
14479
15075
  }
14480
- buildFrvp(grid, drawing, actions) {
14481
- const styleOf = () => {
14482
- const d = actions.resolve();
14483
- 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();
14484
15095
  };
14485
- const s = styleOf();
14486
- const numberRow = (label, path, min, max, step, integer = true) => {
14487
- grid.appendChild(fieldRow({
14488
- label,
14489
- 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({
14490
15114
  kind: "number",
14491
- value: s[path],
14492
- min,
14493
- max,
14494
- step,
14495
- integer,
15115
+ value: lv.ratio,
15116
+ min: free ? void 0 : 0,
15117
+ step: 0.01,
15118
+ compact: true,
14496
15119
  fill: false,
14497
15120
  commit: "blur",
14498
- onChange: (n) => actions.patch({ [`frvp.${path}`]: n })
14499
- }).el
14500
- }));
14501
- };
14502
- numberRow("Rows", "rows", 1, 500, 1);
14503
- numberRow("Value Area", "valueAreaPct", 0, 100, 1);
14504
- numberRow("Width %", "widthPct", 0, 100, 1);
14505
- grid.appendChild(fieldRow({
14506
- label: "Anchor",
14507
- control: buildFieldControl({
14508
- kind: "select",
14509
- options: FRVP_ANCHOR,
14510
- value: s.anchor,
14511
- fill: false,
14512
- theme: this.theme,
14513
- onChange: (v) => actions.patch({ "frvp.anchor": v })
14514
- }).el
14515
- }));
14516
- const colorRow = (label, path) => {
14517
- grid.appendChild(fieldRow({
14518
- label,
14519
- fit: true,
14520
- 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({
14521
15133
  kind: "color",
14522
15134
  theme: this.theme,
14523
- get: () => styleOf()[path],
14524
- onChange: (v) => actions.patch({ [`frvp.${path}`]: v })
14525
- }).el
14526
- }));
14527
- };
14528
- colorRow("Up Volume", "upColor");
14529
- colorRow("Down Volume", "downColor");
14530
- colorRow("Value Area Up", "vaUpColor");
14531
- colorRow("Value Area Down", "vaDownColor");
14532
- const styles = LINE_STYLE_OPTIONS.map((o) => ({ value: o.value, label: o.label }));
14533
- const levelRow = (label, showPath, colorPath, stylePath) => {
14534
- const row = document.createElement("div");
14535
- row.className = "vela-field-span";
14536
- row.style.cssText = "display:flex;align-items:center;gap:8px;";
14537
- const sw = buildFieldControl({
14538
- kind: "switch",
14539
- checked: Boolean(s[showPath]),
14540
- onChange: (v) => actions.patch({ [`frvp.${showPath}`]: v })
14541
- });
14542
- const lbl = document.createElement("span");
14543
- lbl.className = "vela-field-label";
14544
- lbl.style.flex = "1";
14545
- lbl.textContent = label;
14546
- let cur = s[colorPath] ?? contrastColor(this.theme.background);
14547
- const col = buildFieldControl({
14548
- kind: "color",
14549
- theme: this.theme,
14550
- get: () => cur,
14551
- onChange: (v) => {
14552
- cur = v;
14553
- actions.patch({ [`frvp.${colorPath}`]: v });
14554
- }
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);
14555
15144
  });
14556
- 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({
14557
15261
  kind: "select",
14558
- options: styles,
14559
- value: s[stylePath],
15262
+ options: TEXT_SIZE_OPTIONS,
15263
+ value: b.live().text?.size ?? "normal",
14560
15264
  fill: false,
14561
15265
  theme: this.theme,
14562
- onChange: (v) => actions.patch({ [`frvp.${stylePath}`]: v })
15266
+ onChange: (v) => b.edit({ "text.size": v })
14563
15267
  });
14564
- row.append(sw.el, lbl, col.el, style.el);
14565
- 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)));
14566
15289
  };
14567
- levelRow("VAH", "showVah", "vahColor", "vahStyle");
14568
- levelRow("VAL", "showVal", "valColor", "valStyle");
14569
- levelRow("POC", "showPoc", "pocColor", "pocStyle");
14570
- levelRow("Developing POC", "showDevelopingPoc", "developingPocColor", "developingPocStyle");
14571
- levelRow("Developing VA", "showDevelopingVa", "developingVaColor", "developingVaStyle");
14572
- }
14573
- buildLevels(grid, drawing, actions) {
14574
- const levels = drawing.editableLevels();
14575
- if (!levels) return;
14576
- const isMach = drawing instanceof MachFigure;
14577
- if (isMach) {
14578
- const mach = drawing;
14579
- grid.appendChild(fieldRow({
14580
- label: "Show ratio labels",
14581
- bool: true,
14582
- toggle: {
14583
- checked: mach.showRatios !== false,
14584
- 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"));
14585
15326
  }
14586
- }));
14587
- }
14588
- if (drawing instanceof FibRetracement) {
14589
- const fib = drawing;
14590
- grid.appendChild(fieldRow({
14591
- label: "Reverse",
14592
- bool: true,
14593
- toggle: {
14594
- checked: fib.reverse,
14595
- 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";
14596
15383
  }
14597
- }));
14598
- }
14599
- levels.forEach((lv, i) => {
14600
- const row = document.createElement("div");
14601
- row.className = "vela-field-span";
14602
- row.style.cssText = "display:flex;align-items:center;gap:8px;";
14603
- const sw = buildFieldControl({
14604
- kind: "switch",
14605
- checked: lv.enabled,
14606
- 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.";
14607
15401
  });
14608
- let curC = lv.color;
14609
- const col = buildFieldControl({
14610
- kind: "color",
14611
- theme: this.theme,
14612
- get: () => curC,
14613
- onChange: (v) => {
14614
- curC = v;
14615
- 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);
14616
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();
14617
15523
  });
14618
- let curRatio = lv.ratio;
14619
- const ratio = buildFieldControl({
14620
- kind: "number",
14621
- value: curRatio,
14622
- min: 0,
14623
- step: 0.01,
14624
- fill: isMach,
14625
- compact: !isMach,
14626
- commit: "blur",
14627
- onChange: (n) => {
14628
- if (n <= 0) {
14629
- ratio.setValue?.(curRatio);
14630
- return;
14631
- }
14632
- curRatio = n;
14633
- actions.patch({ [`levels.${i}.ratio`]: n });
14634
- }
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();
14635
15561
  });
14636
- if (isMach) ratio.el.style.flex = "1";
14637
- row.append(sw.el, col.el, ratio.el);
14638
- if (!isMach) {
14639
- const label = buildFieldControl({
14640
- kind: "text",
14641
- value: lv.label ?? "",
14642
- fill: true,
14643
- placeholder: "label\u2026",
14644
- onChange: (v) => actions.patch({ [`levels.${i}.label`]: v })
14645
- });
14646
- label.el.style.flex = "1";
14647
- label.el.style.minWidth = "60px";
14648
- 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());
14649
15600
  }
14650
- 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;
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 })
14651
15676
  });
15677
+ const input = ni.el.querySelector("input");
15678
+ b.watch(() => {
15679
+ if (document.activeElement !== input) ni.setValue?.(read());
15680
+ });
15681
+ return ni.el;
14652
15682
  }
14653
15683
  };
14654
- 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) {
14655
15724
  const b = document.createElement("button");
14656
15725
  b.type = "button";
14657
15726
  b.textContent = label;
@@ -14659,6 +15728,50 @@ function btn(label, primary, onClick) {
14659
15728
  b.addEventListener("click", onClick);
14660
15729
  return b;
14661
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
+ }
14662
15775
 
14663
15776
  // src/renderers/native/drawings/DrawingSettingsPopup.ts
14664
15777
  var MIXED = /* @__PURE__ */ Symbol("mixed");
@@ -14680,15 +15793,15 @@ function distinctValues(drawings, read) {
14680
15793
  }
14681
15794
  var BTN = 30;
14682
15795
  var ICON = 21;
14683
- var STYLE_ID4 = "vela-drawing-popup-styles";
14684
- var STYLE_REV = "4";
14685
- function ensureStyles4() {
15796
+ var STYLE_ID5 = "vela-drawing-popup-styles";
15797
+ var STYLE_REV2 = "4";
15798
+ function ensureStyles5() {
14686
15799
  if (typeof document === "undefined") return;
14687
- const existing = document.getElementById(STYLE_ID4);
14688
- if (existing?.dataset.rev === STYLE_REV) return;
15800
+ const existing = document.getElementById(STYLE_ID5);
15801
+ if (existing?.dataset.rev === STYLE_REV2) return;
14689
15802
  const s = existing ?? document.createElement("style");
14690
- s.id = STYLE_ID4;
14691
- s.dataset.rev = STYLE_REV;
15803
+ s.id = STYLE_ID5;
15804
+ s.dataset.rev = STYLE_REV2;
14692
15805
  s.textContent = `
14693
15806
  .vela-dpop-btn{background:transparent;color:var(--vela-fg-muted);transition:background var(--vela-dur-fast) ease,color var(--vela-dur-fast) ease;}
14694
15807
  .vela-dpop-btn:hover{background:var(--vela-hover-strong);color:var(--vela-fg-bright);}
@@ -14704,13 +15817,14 @@ function ensureStyles4() {
14704
15817
  if (!existing) document.head.appendChild(s);
14705
15818
  }
14706
15819
  var DrawingSettingsPopup = class {
14707
- constructor(host, theme, chartBarMs = () => 0) {
15820
+ constructor(host, theme, env = {}) {
14708
15821
  this.host = host;
14709
- this.chartBarMs = chartBarMs;
14710
15822
  this.el = null;
14711
15823
  this.tipEl = null;
14712
15824
  // floating hover-label (above/below the toolbar)
14713
15825
  this.textPanel = null;
15826
+ /** The single drawing the bar is open on (a multi-selection has no settings panel). */
15827
+ this.current = null;
14714
15828
  this.colorPop = null;
14715
15829
  this.colorOwner = null;
14716
15830
  this.menuPop = null;
@@ -14718,9 +15832,10 @@ var DrawingSettingsPopup = class {
14718
15832
  this.onClose = null;
14719
15833
  this.onOutside = (e) => {
14720
15834
  const node = e.target;
14721
- if (this.settingsDialog.isOpen()) {
14722
- if (this.settingsDialog.contains(node) || this.isOwnChrome(node)) return;
14723
- 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();
14724
15839
  return;
14725
15840
  }
14726
15841
  if (this.isOwnChrome(node)) return;
@@ -14729,19 +15844,16 @@ var DrawingSettingsPopup = class {
14729
15844
  cb?.(e);
14730
15845
  };
14731
15846
  this.theme = theme;
14732
- this.settingsDialog = new DrawingSettingsDialog(host, theme);
14733
- }
14734
- /** The magnifier timeframe choices strictly below the chart's own bar duration
14735
- * (`auto` rides along while at least one concrete lower step exists). */
14736
- lowerTimeframeOptions() {
14737
- const chartMs = this.chartBarMs();
14738
- if (!(chartMs > 0)) return [...MAGNIFIER_TIMEFRAME_OPTIONS];
14739
- const lower = MAGNIFIER_TIMEFRAME_OPTIONS.filter((o) => o.ms > 0 && o.ms < chartMs);
14740
- 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
+ });
14741
15853
  }
14742
15854
  setTheme(theme) {
14743
15855
  this.theme = theme;
14744
- this.settingsDialog.setTheme(theme);
15856
+ this.panel.setTheme(theme);
14745
15857
  }
14746
15858
  isOpen() {
14747
15859
  return this.el != null;
@@ -14749,7 +15861,22 @@ var DrawingSettingsPopup = class {
14749
15861
  /** Whether `node` belongs to this popup — the bar, its host-floated color/menu shells,
14750
15862
  * or a kit popover (select list / color chip) portaled into the chart host. */
14751
15863
  contains(node) {
14752
- 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();
14753
15880
  }
14754
15881
  /** Open the quick toolbar for `drawings` (one, or a whole multi-selection), floating clear of
14755
15882
  * their `anchor` box. With several drawings the bar shows only the controls they ALL support,
@@ -14759,10 +15886,12 @@ var DrawingSettingsPopup = class {
14759
15886
  * programmatic close), with the dismissing press when there is one, so the caller can tell a
14760
15887
  * modifier press (multi-select) from a plain one. */
14761
15888
  open(drawings, anchor, actions, onClose) {
14762
- this.close();
14763
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);
14764
15892
  if (!drawing) return;
14765
- ensureStyles4();
15893
+ this.current = drawings.length === 1 ? { drawing, actions, anchor } : null;
15894
+ ensureStyles5();
14766
15895
  this.onClose = onClose ?? null;
14767
15896
  const t = this.theme;
14768
15897
  const multi = drawings.length > 1;
@@ -14790,11 +15919,11 @@ var DrawingSettingsPopup = class {
14790
15919
  }
14791
15920
  if (paths.has("size")) {
14792
15921
  const sz = common((d) => d.size ?? "normal");
14793
- 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 }));
14794
15923
  }
14795
15924
  if (paths.has("magnifier.timeframe") && every((d) => d instanceof Magnifier)) {
14796
15925
  const mag = (d) => d;
14797
- const options = this.lowerTimeframeOptions();
15926
+ const options = magnifierTimeframesBelow(this.chartBarMs());
14798
15927
  if (options.length > 0) {
14799
15928
  bar.appendChild(
14800
15929
  this.dropdown("Lower timeframe", options.map((o) => o.value), common((d) => mag(d).magnifier.timeframe), () => "", (v) => actions.patch({ "magnifier.timeframe": v }), {
@@ -14823,7 +15952,6 @@ var DrawingSettingsPopup = class {
14823
15952
  }
14824
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 }));
14825
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"));
14826
- const isFrvp = paths.has("frvp.rows") && drawing instanceof FixedRangeVolumeProfile;
14827
15955
  const isPosition = paths.has("riskPercent") && every((d) => d instanceof PositionTool);
14828
15956
  if (isPosition) {
14829
15957
  const pos = (d) => d;
@@ -14892,7 +16020,7 @@ var DrawingSettingsPopup = class {
14892
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 })));
14893
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 })));
14894
16022
  if (paths.has("text.color") && textOnBar) bar.appendChild(swatch("Text color", TYPE_ICON, (d) => d.text?.color || t.textColor, "text.color"));
14895
- 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 }));
14896
16024
  const textField = !multi && paths.has("text.value");
14897
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 })));
14898
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 })));
@@ -14901,13 +16029,11 @@ var DrawingSettingsPopup = class {
14901
16029
  if (editableLevels && every((d) => d.editableLevels() != null && !(d instanceof MachFigure))) {
14902
16030
  const fib = (d) => d;
14903
16031
  const sizes = TEXT_SIZE_OPTIONS.map((o) => o.value);
14904
- bar.appendChild(this.dropdown("Numbers size", sizes, common((d) => fib(d).numbersSize ?? "small"), (s) => numbersSizeIcon(s), (v) => actions.patch({ numbersSize: v }), { label: sizeLabel }));
14905
- 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 }));
14906
16034
  }
14907
16035
  bar.appendChild(this.divider());
14908
- if (!multi && isFrvp) bar.appendChild(this.iconBtn("Settings", GEAR_ICON, () => this.settingsDialog.open(drawing, actions, "frvp")));
14909
- if (!multi && isPosition) bar.appendChild(this.iconBtn("Position size", GEAR_ICON, () => this.settingsDialog.open(drawing, actions, "position")));
14910
- 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()));
14911
16037
  bar.appendChild(this.toggle("Lock", LOCK_ICON, common((d) => d.locked), (v) => actions.setLocked(v), UNLOCK_ICON));
14912
16038
  const del = this.iconBtn("Delete", TRASH_ICON, () => actions.remove());
14913
16039
  del.style.color = "var(--vela-danger)";
@@ -14925,16 +16051,17 @@ var DrawingSettingsPopup = class {
14925
16051
  this.position(anchor);
14926
16052
  setTimeout(() => document.addEventListener("pointerdown", this.onOutside), 0);
14927
16053
  }
14928
- close() {
16054
+ close(keepPanel = false) {
14929
16055
  document.removeEventListener("pointerdown", this.onOutside);
14930
16056
  this.closeColorPopover();
14931
16057
  this.closeMenu();
14932
16058
  this.hideTip();
14933
- this.settingsDialog.close();
16059
+ if (!keepPanel) this.panel.close();
14934
16060
  this.el?.remove();
14935
16061
  this.el = null;
14936
16062
  this.textPanel = null;
14937
16063
  this.onClose = null;
16064
+ this.current = null;
14938
16065
  }
14939
16066
  destroy() {
14940
16067
  this.close();
@@ -14990,7 +16117,7 @@ var DrawingSettingsPopup = class {
14990
16117
  if (withColorAndSize) {
14991
16118
  tools.append(
14992
16119
  this.colorButton("Text color", TYPE_ICON, text?.color || this.theme.textColor, (v) => actions.patch({ "text.color": v }), 14),
14993
- 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 })
14994
16121
  );
14995
16122
  }
14996
16123
  tools.append(
@@ -15164,9 +16291,9 @@ var DrawingSettingsPopup = class {
15164
16291
  * quick toolbar — the chip works without selecting the drawing first.
15165
16292
  */
15166
16293
  openMagnifierTimeframeMenu(rect, current, onPick) {
15167
- ensureStyles4();
16294
+ ensureStyles5();
15168
16295
  closeOpenPopovers();
15169
- const options = this.lowerTimeframeOptions();
16296
+ const options = magnifierTimeframesBelow(this.chartBarMs());
15170
16297
  const anchor = document.createElement("div");
15171
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;`;
15172
16299
  this.host.appendChild(anchor);
@@ -15207,22 +16334,22 @@ var DrawingSettingsPopup = class {
15207
16334
  zIndex: 26,
15208
16335
  padding: "4px",
15209
16336
  fill: (menu, pop) => {
15210
- for (const row of rows) {
16337
+ for (const row2 of rows) {
15211
16338
  const item = document.createElement("button");
15212
16339
  item.type = "button";
15213
16340
  item.className = "vela-dpop-item";
15214
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;";
15215
16342
  const ic = document.createElement("span");
15216
16343
  ic.style.cssText = "display:flex;flex:none;width:20px;justify-content:center;";
15217
- ic.innerHTML = sized(row.icon, 18);
16344
+ ic.innerHTML = sized(row2.icon, 18);
15218
16345
  const tx = document.createElement("span");
15219
- tx.textContent = row.label;
16346
+ tx.textContent = row2.label;
15220
16347
  tx.style.cssText = "flex:1;";
15221
16348
  item.append(ic, tx);
15222
16349
  item.addEventListener("click", (e) => {
15223
16350
  e.stopPropagation();
15224
16351
  pop.hide();
15225
- row.onClick();
16352
+ row2.onClick();
15226
16353
  });
15227
16354
  menu.appendChild(item);
15228
16355
  }
@@ -15447,12 +16574,12 @@ var DrawingSettingsPopup = class {
15447
16574
  padding: "10px",
15448
16575
  fill: (el, pop) => {
15449
16576
  if (used.length > 1) {
15450
- const row = document.createElement("div");
15451
- 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);";
15452
16579
  const lbl = document.createElement("span");
15453
16580
  lbl.textContent = "In use";
15454
16581
  lbl.style.cssText = "font:var(--vela-font-size-sm) inherit;opacity:0.7;margin-right:2px;";
15455
- row.appendChild(lbl);
16582
+ row2.appendChild(lbl);
15456
16583
  for (const c of used) {
15457
16584
  const sw = document.createElement("button");
15458
16585
  sw.type = "button";
@@ -15463,9 +16590,9 @@ var DrawingSettingsPopup = class {
15463
16590
  pop.hide();
15464
16591
  onChange(c);
15465
16592
  });
15466
- row.appendChild(sw);
16593
+ row2.appendChild(sw);
15467
16594
  }
15468
- el.appendChild(row);
16595
+ el.appendChild(row2);
15469
16596
  }
15470
16597
  el.appendChild(buildColorPicker(color, t, onChange));
15471
16598
  }
@@ -15539,7 +16666,7 @@ function labelSizeIcon(size) {
15539
16666
  function capitalize(s) {
15540
16667
  return s.charAt(0).toUpperCase() + s.slice(1);
15541
16668
  }
15542
- function sizeLabel(v) {
16669
+ function sizeLabel2(v) {
15543
16670
  return TEXT_SIZE_OPTIONS.find((o) => o.value === v)?.label ?? capitalize(String(v));
15544
16671
  }
15545
16672
  function styleLabel2(v) {
@@ -15762,7 +16889,9 @@ var UserDrawingController = class {
15762
16889
  this.hoveredId = null;
15763
16890
  // the drawing under the cursor (its handles show)
15764
16891
  this.activeTool = null;
15765
- // 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
15766
16895
  this.intentCb = null;
15767
16896
  /** Another chart's in-progress placement, mirrored here as a ghost (drawings sync). */
15768
16897
  this.externalGhost = null;
@@ -15810,7 +16939,11 @@ var UserDrawingController = class {
15810
16939
  this.clearSelection();
15811
16940
  };
15812
16941
  this.ctx = canvas.getContext("2d");
15813
- 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
+ });
15814
16947
  this.toolbar = new DrawingToolbar(
15815
16948
  toolbarHost,
15816
16949
  deps.theme(),
@@ -15833,7 +16966,7 @@ var UserDrawingController = class {
15833
16966
  this.interaction = new DrawingInteraction({
15834
16967
  projector: () => this.deps.projector(),
15835
16968
  activeTool: () => this.activeTool,
15836
- drawings: () => this.drawings,
16969
+ drawings: () => this.onScreen(),
15837
16970
  hoveredId: () => this.hoveredId,
15838
16971
  selectedIds: () => this.selectedIds,
15839
16972
  emit: (i) => this.emit(i),
@@ -15843,7 +16976,7 @@ var UserDrawingController = class {
15843
16976
  },
15844
16977
  openSettings: (id, x, y) => this.openSettingsById(id, x, y),
15845
16978
  snap: (pt, paneId, mode, cursorPx) => this.deps.snap(pt, paneId, mode, cursorPx),
15846
- lastStyle: () => this.activeToolStyle
16979
+ toolDefaults: () => this.activeToolDefaults
15847
16980
  });
15848
16981
  }
15849
16982
  // ── IDrawingsRendererPort (commands down) ──
@@ -15895,6 +17028,7 @@ var UserDrawingController = class {
15895
17028
  this.invalidateSlices();
15896
17029
  this.render();
15897
17030
  this.deps.requestScaleUpdate();
17031
+ this.popup.syncPanel();
15898
17032
  }
15899
17033
  /** The pane's series stack in z terms — how the core places a new drawing (just under
15900
17034
  * `price`) and computes "front of everything" / "behind everything" for the reorders. */
@@ -15922,7 +17056,7 @@ var UserDrawingController = class {
15922
17056
  }
15923
17057
  return lo <= hi ? { min: lo, max: hi } : null;
15924
17058
  }
15925
- setActiveTool(type, lastStyle) {
17059
+ setActiveTool(type, lastStyle, defaults) {
15926
17060
  if (type != null) {
15927
17061
  this.finishTextEditor(true);
15928
17062
  this.withModeIntent(() => {
@@ -15931,7 +17065,7 @@ var UserDrawingController = class {
15931
17065
  });
15932
17066
  }
15933
17067
  this.activeTool = type;
15934
- this.activeToolStyle = lastStyle;
17068
+ this.activeToolDefaults = defaults ?? (lastStyle ? { style: lastStyle } : void 0);
15935
17069
  this.toolbar.setActiveTool(type);
15936
17070
  if (type == null) this.interaction.onToolCleared();
15937
17071
  else this.clearSelection();
@@ -15941,6 +17075,10 @@ var UserDrawingController = class {
15941
17075
  setFavorites(types) {
15942
17076
  this.toolbar.setFavorites(types);
15943
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
+ }
15944
17082
  /** Core push: per-tool shortcut hints (display strings) shown in the toolbar flyouts. */
15945
17083
  setToolShortcuts(map2) {
15946
17084
  this.toolbar.setShortcuts(map2);
@@ -16013,8 +17151,9 @@ var UserDrawingController = class {
16013
17151
  /** The topmost visible (unlocked) magnifier whose timeframe chip contains (x, y) —
16014
17152
  * the chip's rect is what the painter measured last frame. */
16015
17153
  magnifierChipAt(x, y) {
16016
- for (let i = this.drawings.length - 1; i >= 0; i -= 1) {
16017
- 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];
16018
17157
  if (!(d instanceof Magnifier) || !d.visible || d.locked) continue;
16019
17158
  const r = d.chipRect;
16020
17159
  if (r && x >= r.x && x <= r.x + r.w && y >= r.y && y <= r.y + r.h) return d;
@@ -16040,7 +17179,7 @@ var UserDrawingController = class {
16040
17179
  * `withSelection` so a hit on a member of a multi-selection removes the selection's
16041
17180
  * unlocked members — whichever member was hit, locked or not. */
16042
17181
  deleteAt(x, y, withSelection = false) {
16043
- 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);
16044
17183
  if (!hit) return false;
16045
17184
  const ids = deleteTargets(hit, this.selectedIds, this.drawings, withSelection);
16046
17185
  if (ids.length === 0) return false;
@@ -16120,7 +17259,7 @@ var UserDrawingController = class {
16120
17259
  updateHover(x, y, mod = false) {
16121
17260
  let id = null;
16122
17261
  if (!mod && this.activeTool == null && !this.interaction.isPlacing() && !this.interaction.isDragging()) {
16123
- 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;
16124
17263
  }
16125
17264
  if (id !== this.hoveredId) {
16126
17265
  this.hoveredId = id;
@@ -16355,16 +17494,20 @@ var UserDrawingController = class {
16355
17494
  }
16356
17495
  return false;
16357
17496
  }
16358
- /** Double-click over a drawing → suppress the chart's view reset (single-click already
16359
- * 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. */
16360
17500
  dblClick(x, y) {
16361
17501
  if (this.interaction.finishPlacing(true)) return true;
16362
- 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);
16363
17503
  if (isInlineEditable(hit)) {
16364
17504
  this.editTextInline(hit.id);
16365
17505
  return true;
16366
17506
  }
16367
- 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;
16368
17511
  }
16369
17512
  /** Keyboard pre-empt: Escape (popup/placing/selection), undo/redo, copy/paste/duplicate,
16370
17513
  * delete (multi), and arrow-nudge. Stands down while a label text field is focused. */
@@ -16462,6 +17605,13 @@ var UserDrawingController = class {
16462
17605
  this.popup.close();
16463
17606
  this.render();
16464
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
+ }
16465
17615
  /** Whether the drawing's z puts it INSIDE the series stack (per the last-known boundaries)
16466
17616
  * rather than over it — i.e. its body belongs to an interleave layer, not the top canvas. */
16467
17617
  isInterleaved(d) {
@@ -16501,7 +17651,7 @@ var UserDrawingController = class {
16501
17651
  const proj = this.deps.projector();
16502
17652
  const theme = this.deps.theme();
16503
17653
  const buckets = /* @__PURE__ */ new Map();
16504
- for (const d of this.drawings) {
17654
+ for (const d of this.onScreen()) {
16505
17655
  if (!d.visible || this.lifted.has(d.id)) continue;
16506
17656
  const beforeZ = sliceKeyFor(d.zIndex, this.lastBounds.get(d.paneId) ?? []);
16507
17657
  if (beforeZ === null) continue;
@@ -16555,8 +17705,9 @@ var UserDrawingController = class {
16555
17705
  };
16556
17706
  this.painter.seriesLook = this.deps.seriesLook();
16557
17707
  const onTop = (d) => !this.isInterleaved(d) || this.lifted.has(d.id);
16558
- this.painter.paintAll(ctx, this.drawings.filter(onTop), proj, this.deps.theme(), targets);
16559
- 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));
16560
17711
  const clones = this.interaction.dragClones();
16561
17712
  if (clones) this.painter.paintAll(ctx, clones, proj, this.deps.theme(), { selected: new Set(clones.map((c) => c.id)) });
16562
17713
  this.layoutTextEditor();
@@ -16666,6 +17817,7 @@ var UserDrawingController = class {
16666
17817
  this.emit({ kind: "select", ids: [id] });
16667
17818
  this.emit({ kind: "settings", id });
16668
17819
  const anchor = drawing.bounds(this.deps.projector());
17820
+ let previewBase = null;
16669
17821
  this.popup.open([drawing], anchor, {
16670
17822
  // Sync rebuilds instances, so a panel that reads values back after a patch (e.g. the
16671
17823
  // position tool's price fields, where one edit can flip another level) resolves fresh.
@@ -16700,11 +17852,39 @@ var UserDrawingController = class {
16700
17852
  restore: (doc) => {
16701
17853
  const d = live();
16702
17854
  if (!d) return;
16703
- if (doc.style) d.style = { ...doc.style };
16704
- if (doc.text !== void 0) d.text = doc.text ? { ...doc.text } : void 0;
16705
- 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;
16706
17887
  this.render();
16707
- this.emit({ kind: "edit", doc });
16708
17888
  },
16709
17889
  remove: () => {
16710
17890
  this.closeTextEditor();
@@ -16753,6 +17933,11 @@ var UserDrawingController = class {
16753
17933
  this.intentCb?.(i);
16754
17934
  }
16755
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
+ }
16756
17941
 
16757
17942
  // src/renderers/shared/TableOverlay.ts
16758
17943
  var SIZE_PX2 = {
@@ -16768,7 +17953,7 @@ function fontPxOf(size) {
16768
17953
  return SIZE_PX2[size] ?? SIZE_PX2.auto;
16769
17954
  }
16770
17955
  function tableHasContent(t) {
16771
- 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));
16772
17957
  }
16773
17958
  function mergeRenderPlan(t) {
16774
17959
  const span = /* @__PURE__ */ new Map();
@@ -17529,15 +18714,15 @@ var MarkPopover = class {
17529
18714
  actions.className = "vela-callout-actions";
17530
18715
  section.appendChild(actions);
17531
18716
  }
17532
- const btn2 = doc.createElement("button");
17533
- btn2.type = "button";
17534
- btn2.className = "vela-callout-btn" + (item.primary ? " vela-callout-btn-primary" : "");
17535
- btn2.textContent = item.label;
17536
- 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", () => {
17537
18722
  item.run();
17538
18723
  if (item.close !== false) this.close();
17539
18724
  });
17540
- actions.appendChild(btn2);
18725
+ actions.appendChild(btn);
17541
18726
  continue;
17542
18727
  }
17543
18728
  actions = null;
@@ -17547,16 +18732,16 @@ var MarkPopover = class {
17547
18732
  text.textContent = item.text;
17548
18733
  section.appendChild(text);
17549
18734
  } else if (item.type === "field") {
17550
- const row = doc.createElement("div");
17551
- row.className = "vela-marks-field";
18735
+ const row2 = doc.createElement("div");
18736
+ row2.className = "vela-marks-field";
17552
18737
  const label = doc.createElement("span");
17553
18738
  label.className = "vela-marks-field-label";
17554
18739
  label.textContent = item.label;
17555
18740
  const value = doc.createElement("span");
17556
18741
  value.className = "vela-marks-field-value";
17557
18742
  value.textContent = item.value;
17558
- row.append(label, value);
17559
- section.appendChild(row);
18743
+ row2.append(label, value);
18744
+ section.appendChild(row2);
17560
18745
  }
17561
18746
  }
17562
18747
  }
@@ -17815,7 +19000,7 @@ var VELA_LETTERS_SVG = '<svg viewBox="161.66 -142 310.57 148" aria-hidden="true"
17815
19000
 
17816
19001
  // src/renderers/native/chrome/AttributionMark.ts
17817
19002
  var ATTRIBUTION_URL = "https://velacharts.dev/?utm_source=vela&utm_medium=attribution";
17818
- var STYLE_ID5 = "vela-attribution-styles";
19003
+ var STYLE_ID6 = "vela-attribution-styles";
17819
19004
  var CSS = `
17820
19005
  .vela-attribution {
17821
19006
  text-decoration: none;
@@ -17871,11 +19056,11 @@ var CSS = `
17871
19056
  [data-vela-layout='mobile'] .vela-attribution:hover .vela-attr-wordmark,
17872
19057
  [data-layout='mobile'] .vela-attribution:hover .vela-attr-wordmark { transform: translateX(0); }
17873
19058
  `;
17874
- function ensureStyles5(doc) {
17875
- let s = doc.getElementById(STYLE_ID5);
19059
+ function ensureStyles6(doc) {
19060
+ let s = doc.getElementById(STYLE_ID6);
17876
19061
  if (!s) {
17877
19062
  s = doc.createElement("style");
17878
- s.id = STYLE_ID5;
19063
+ s.id = STYLE_ID6;
17879
19064
  doc.head.appendChild(s);
17880
19065
  }
17881
19066
  s.textContent = CSS;
@@ -17905,7 +19090,7 @@ function createCustomMark(doc, html, background) {
17905
19090
  return el;
17906
19091
  }
17907
19092
  function createAttributionMark(doc, background) {
17908
- ensureStyles5(doc);
19093
+ ensureStyles6(doc);
17909
19094
  const a = doc.createElement("a");
17910
19095
  a.className = "vela-attribution";
17911
19096
  a.href = ATTRIBUTION_URL;
@@ -18178,13 +19363,13 @@ function paintVpvr(ctx, profile, geom, style) {
18178
19363
  const { rightX, maxW, yOf } = geom;
18179
19364
  if (maxTotal <= 0 || maxW <= 0) return;
18180
19365
  for (let k = 0; k < rows.length; k += 1) {
18181
- const row = rows[k];
18182
- const total = row.up + row.down;
19366
+ const row2 = rows[k];
19367
+ const total = row2.up + row2.down;
18183
19368
  if (total <= 0) continue;
18184
- const yTop = yOf(row.price + rowH);
18185
- 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);
18186
19371
  const w = total / maxTotal * maxW;
18187
- const upW = row.up / total * w;
19372
+ const upW = row2.up / total * w;
18188
19373
  ctx.globalAlpha = k >= vaFrom && k <= vaTo ? VPVR_VA_ALPHA : VPVR_OUTSIDE_ALPHA;
18189
19374
  if (upW > 0) {
18190
19375
  ctx.fillStyle = style.upColor;
@@ -18196,9 +19381,9 @@ function paintVpvr(ctx, profile, geom, style) {
18196
19381
  }
18197
19382
  }
18198
19383
  if (style.showPoc) {
18199
- const row = rows[poc];
18200
- const yTop = yOf(row.price + rowH);
18201
- 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);
18202
19387
  ctx.globalAlpha = VPVR_POC_ALPHA;
18203
19388
  ctx.strokeStyle = style.pocColor;
18204
19389
  ctx.lineWidth = 1;
@@ -19164,11 +20349,11 @@ var NativeRenderer = class {
19164
20349
  }
19165
20350
  }
19166
20351
  makeSettingsButton() {
19167
- const btn2 = document.createElement("button");
19168
- btn2.type = "button";
19169
- btn2.title = "Chart settings";
19170
- btn2.innerHTML = iconAt("gear", 14);
19171
- 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, {
19172
20357
  position: "absolute",
19173
20358
  bottom: `${TIME_AXIS_H + 10}px`,
19174
20359
  right: `${RIGHT_AXIS_W + 10}px`,
@@ -19189,10 +20374,10 @@ var NativeRenderer = class {
19189
20374
  pointerEvents: "auto",
19190
20375
  padding: "0"
19191
20376
  });
19192
- btn2.addEventListener("mouseenter", () => btn2.style.opacity = "1");
19193
- btn2.addEventListener("mouseleave", () => btn2.style.opacity = "0.75");
19194
- btn2.addEventListener("click", () => this.toggleSettingsDialog());
19195
- 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;
19196
20381
  }
19197
20382
  /** Port surface: hosts (bottom-bar / chrome buttons) open the same dialog as the
19198
20383
  * in-chart gear — created on demand, independent of the gear feature being enabled. */
@@ -19257,12 +20442,12 @@ var NativeRenderer = class {
19257
20442
  }
19258
20443
  /** Jump-back-to-latest button — same size/border chrome as the drawing-toolbar collapse toggle. */
19259
20444
  makeScrollToRealtimeButton() {
19260
- const btn2 = document.createElement("button");
19261
- btn2.type = "button";
19262
- btn2.title = "Scroll to most recent bar (Alt+Shift+\u2192)";
19263
- btn2.setAttribute("aria-label", "Scroll to most recent bar");
19264
- btn2.innerHTML = this.scrollButtonIcon();
19265
- 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, {
19266
20451
  position: "absolute",
19267
20452
  bottom: `${SCROLL_BTN_BOTTOM}px`,
19268
20453
  right: `${SCROLL_BTN_RIGHT}px`,
@@ -19285,10 +20470,10 @@ var NativeRenderer = class {
19285
20470
  pointerEvents: "auto",
19286
20471
  padding: "0"
19287
20472
  });
19288
- btn2.addEventListener("mouseenter", () => btn2.style.opacity = "1");
19289
- btn2.addEventListener("mouseleave", () => btn2.style.opacity = "0.85");
19290
- btn2.addEventListener("click", () => this.scrollToRealtime());
19291
- 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;
19292
20477
  }
19293
20478
  /** Double-chevron icon — the "jump back to the latest bar" affordance. */
19294
20479
  scrollButtonIcon() {
@@ -19311,14 +20496,14 @@ var NativeRenderer = class {
19311
20496
  * appears whenever the latest bars are off-screen and hides again once the view is back
19312
20497
  * at the right edge. */
19313
20498
  updateScrollToRealtimeButton() {
19314
- const btn2 = this.scrollButton;
19315
- if (!btn2) return;
20499
+ const btn = this.scrollButton;
20500
+ if (!btn) return;
19316
20501
  const latestOffScreen = this.coords.barCount > 0 && this.coords.getViewport().rightOffset < 0;
19317
20502
  if (this.layoutMode === "mobile") {
19318
- btn2.style.display = latestOffScreen ? "flex" : "none";
20503
+ btn.style.display = latestOffScreen ? "flex" : "none";
19319
20504
  return;
19320
20505
  }
19321
- btn2.style.display = latestOffScreen && this.pointerNearScrollBtn ? "flex" : "none";
20506
+ btn.style.display = latestOffScreen && this.pointerNearScrollBtn ? "flex" : "none";
19322
20507
  }
19323
20508
  /** Glide the view back to the most recent bars, keeping the current zoom (barSpacing). */
19324
20509
  scrollToRealtime() {
@@ -19627,6 +20812,8 @@ var NativeRenderer = class {
19627
20812
  };
19628
20813
  },
19629
20814
  chartBarMs: () => this.coords.barInterval,
20815
+ timeZone: () => this.scene.timezone,
20816
+ priceDecimals: () => this.scene.priceMintick ? tickDecimals(this.scene.priceMintick) : null,
19630
20817
  snap: (pt, paneId, mode, cursorPx) => this.snapToCandle(pt, paneId, mode, cursorPx),
19631
20818
  setSnapMode: (mode) => this.setSnapMode(mode),
19632
20819
  setToolbarGutter: (px) => this.setToolbarGutter(px)
@@ -20948,7 +22135,7 @@ var NativeRenderer = class {
20948
22135
  if (bar) parts.push(formatStamp(bar.time, this.scene.timezone));
20949
22136
  if (ohlc) parts.push(`Open ${ohlc.o}, High ${ohlc.h}, Low ${ohlc.l}, Close ${ohlc.c}`);
20950
22137
  for (const group of this.dataWindowGroups(idx, pricePane)) {
20951
- 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}`);
20952
22139
  }
20953
22140
  this.liveRegion.textContent = parts.join(". ");
20954
22141
  }
@@ -21904,7 +23091,10 @@ function applyPatch(model, patch) {
21904
23091
  const s = model.series.find((x) => x.id === delta.seriesId);
21905
23092
  if (!s) continue;
21906
23093
  if (delta.kind === "points" && isLineLikeSeries(s)) s.points = delta.points;
21907
- 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
+ }
21908
23098
  }
21909
23099
  if (patch.lines) model.lines = patch.lines;
21910
23100
  if (patch.boxes) model.boxes = patch.boxes;
@@ -23263,14 +24453,14 @@ var EngineOrchestrator = class _EngineOrchestrator {
23263
24453
  this.replayTickSource = source;
23264
24454
  this.prefetchTicks();
23265
24455
  }
23266
- replayPlay(intervalMs2) {
24456
+ replayPlay(intervalMs) {
23267
24457
  if (!this.replayQueue) return;
23268
- if (intervalMs2 !== void 0) {
23269
- if (!(Number.isFinite(intervalMs2) && intervalMs2 > 0)) {
23270
- 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`);
23271
24461
  return;
23272
24462
  }
23273
- this.replayIntervalMs = intervalMs2;
24463
+ this.replayIntervalMs = intervalMs;
23274
24464
  }
23275
24465
  this.clearReplayTimer();
23276
24466
  this.replayPlaying = true;
@@ -24810,7 +26000,7 @@ function modelToValuePatch(model) {
24810
26000
  let to = 0;
24811
26001
  for (const s of model.series) {
24812
26002
  if (s.kind === "candle" || s.kind === "bar") {
24813
- series.push({ seriesId: s.id, kind: "bars", bars: s.bars });
26003
+ series.push({ seriesId: s.id, kind: "bars", bars: s.bars, barColors: s.barColors ?? [] });
24814
26004
  for (const b of s.bars) {
24815
26005
  if (b.time < from) from = b.time;
24816
26006
  if (b.time > to) to = b.time;