@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
package/dist/index.cjs CHANGED
@@ -392,6 +392,39 @@ function defaultText(value = "") {
392
392
  return { value, size: "normal", hAlign: "left", vAlign: "top" };
393
393
  }
394
394
 
395
+ // src/core/drawings/visibility.ts
396
+ var MIN = 6e4;
397
+ var HOUR = 60 * MIN;
398
+ var DAY = 24 * HOUR;
399
+ var TIMEFRAME_BANDS = [
400
+ { key: "s", label: "Sec", fromMs: 0 },
401
+ { key: "1", label: "1m", fromMs: MIN },
402
+ { key: "5", label: "5m", fromMs: 5 * MIN },
403
+ { key: "15", label: "15m", fromMs: 15 * MIN },
404
+ { key: "60", label: "1H", fromMs: HOUR },
405
+ { key: "240", label: "4H", fromMs: 4 * HOUR },
406
+ { key: "D", label: "D", fromMs: DAY },
407
+ { key: "W", label: "W", fromMs: 7 * DAY },
408
+ { key: "M", label: "M", fromMs: 28 * DAY }
409
+ ];
410
+ var BAND_KEYS = new Set(TIMEFRAME_BANDS.map((b) => b.key));
411
+ function timeframeBandOf(barMs) {
412
+ if (!(barMs > 0)) return null;
413
+ let key = null;
414
+ for (const band of TIMEFRAME_BANDS) if (barMs >= band.fromMs) key = band.key;
415
+ return key;
416
+ }
417
+ function shownOnTimeframe(showOn, barMs) {
418
+ if (!showOn) return true;
419
+ const band = timeframeBandOf(barMs);
420
+ return band === null || showOn.includes(band);
421
+ }
422
+ function sanitizeShowOn(v) {
423
+ if (!Array.isArray(v)) return void 0;
424
+ const wanted = new Set(v.filter((k) => typeof k === "string" && BAND_KEYS.has(k)));
425
+ return TIMEFRAME_BANDS.map((b) => b.key).filter((k) => wanted.has(k));
426
+ }
427
+
395
428
  // src/core/drawings/Drawing.ts
396
429
  var _Drawing = class _Drawing {
397
430
  // Public so the registry can construct concrete subclasses; `abstract` still
@@ -408,6 +441,7 @@ var _Drawing = class _Drawing {
408
441
  this.locked = init.locked ?? false;
409
442
  this.visible = init.visible ?? true;
410
443
  this.zIndex = init.zIndex ?? 0;
444
+ this.showOn = sanitizeShowOn(init.showOn);
411
445
  this.createdAt = init.createdAt ?? Date.now();
412
446
  if (init.props) this.readProps(init.props);
413
447
  }
@@ -494,7 +528,10 @@ var _Drawing = class _Drawing {
494
528
  visible: this.visible,
495
529
  zIndex: this.zIndex,
496
530
  createdAt: this.createdAt,
497
- props: this.writeProps()
531
+ props: this.writeProps(),
532
+ // Always present (undefined = every timeframe) so an edit can lift a limit; JSON
533
+ // drops the undefined value, so unlimited drawings save as before.
534
+ showOn: this.showOn ? [...this.showOn] : void 0
498
535
  };
499
536
  }
500
537
  /** Per-type extras to serialize into `props` (override in subclasses). */
@@ -812,9 +849,16 @@ var TrendLine = class extends Drawing {
812
849
  if (y1 == null || y2 == null) return null;
813
850
  return [proj.xOf(a.time), y1, proj.xOf(b.time), y2];
814
851
  }
852
+ /** Which ends run on past the anchors. */
853
+ extension() {
854
+ const { extendLeft: l, extendRight: r } = this.style;
855
+ return l && r ? "both" : l ? "left" : r ? "right" : "none";
856
+ }
815
857
  hitTest(px, py, proj, tol) {
816
858
  const p = this.pixels(proj);
817
- return p != null && distToSegment(px, py, p[0], p[1], p[2], p[3]) <= tol;
859
+ if (!p) return false;
860
+ const [x1, y1, x2, y2] = extendRay(p[0], p[1], p[2], p[3], this.extension(), proj.width, proj.height);
861
+ return distToSegment(px, py, x1, y1, x2, y2) <= tol;
818
862
  }
819
863
  handlePoints(proj) {
820
864
  const p = this.pixels(proj);
@@ -841,6 +885,8 @@ var TrendLine = class extends Drawing {
841
885
  fields: [
842
886
  ...LINE_FIELDS,
843
887
  { path: "style.arrowRight", label: "Arrow", kind: "boolean", group: "line" },
888
+ { path: "style.extendLeft", label: "Extend left", kind: "boolean", group: "line" },
889
+ { path: "style.extendRight", label: "Extend right", kind: "boolean", group: "line" },
844
890
  ...TEXT_FIELDS
845
891
  ]
846
892
  };
@@ -3300,6 +3346,14 @@ var FibRatios = class extends Drawing {
3300
3346
 
3301
3347
  // src/core/drawings/types/FibLevels.ts
3302
3348
  var FibLevels = class extends FibRatios {
3349
+ // `declare`d: the base constructor reads saved props before these could be initialized.
3350
+ constructor(init) {
3351
+ super(init);
3352
+ if (this.showRatios === void 0) this.showRatios = true;
3353
+ if (this.showPrices === void 0) this.showPrices = true;
3354
+ if (this.labelSide === void 0) this.labelSide = "left";
3355
+ if (this.background === void 0) this.background = true;
3356
+ }
3303
3357
  /** Price of a level for the two anchor prices; subclasses override to change which anchor ratio 0 sits on. */
3304
3358
  levelPrice(ratio, p1, p2) {
3305
3359
  return p1 + ratio * (p2 - p1);
@@ -3311,8 +3365,8 @@ var FibLevels = class extends FibRatios {
3311
3365
  if (!a || !b) return null;
3312
3366
  const xa = proj.xOf(a.time);
3313
3367
  const xb = proj.xOf(b.time);
3314
- const x1 = Math.min(xa, xb);
3315
- const x2 = Math.max(xa, xb);
3368
+ const x1 = this.style.extendLeft ? 0 : Math.min(xa, xb);
3369
+ const x2 = this.style.extendRight ? proj.width : Math.max(xa, xb);
3316
3370
  const out = [];
3317
3371
  for (const lv of this.levels) {
3318
3372
  if (!lv.enabled) continue;
@@ -3326,6 +3380,7 @@ var FibLevels = class extends FibRatios {
3326
3380
  entryLines(proj) {
3327
3381
  const lines = this.levelLines(proj);
3328
3382
  if (!lines) return null;
3383
+ const right = this.labelSide === "right";
3329
3384
  return lines.map((l) => ({
3330
3385
  color: l.color,
3331
3386
  label: l.label,
@@ -3333,16 +3388,24 @@ var FibLevels = class extends FibRatios {
3333
3388
  y1: l.y,
3334
3389
  x2: l.x2,
3335
3390
  y2: l.y,
3336
- numberText: `${l.ratio} (${l.price.toFixed(2)})`,
3337
- numberX: l.x1 + 4,
3391
+ numberText: this.levelText(l.ratio, l.price),
3392
+ numberX: right ? l.x2 - 4 : l.x1 + 4,
3338
3393
  numberY: l.y - 7,
3339
- numberAlign: "left",
3394
+ numberAlign: right ? "right" : "left",
3340
3395
  labelX: (l.x1 + l.x2) / 2,
3341
3396
  labelY: l.y - 7
3342
3397
  }));
3343
3398
  }
3399
+ /** The text beside a level: its ratio, its price, both, or nothing. */
3400
+ levelText(ratio, price) {
3401
+ const p = price.toFixed(2);
3402
+ if (this.showRatios && this.showPrices) return `${ratio} (${p})`;
3403
+ if (this.showRatios) return String(ratio);
3404
+ return this.showPrices ? p : "";
3405
+ }
3344
3406
  fillBands(proj) {
3345
- const lines = this.levelLines(proj);
3407
+ if (!this.background) return [];
3408
+ const lines = this.levelLines(proj)?.sort((p, q) => p.ratio - q.ratio);
3346
3409
  if (!lines) return [];
3347
3410
  const bands = [];
3348
3411
  for (let i = 1; i < lines.length; i += 1) {
@@ -3360,6 +3423,26 @@ var FibLevels = class extends FibRatios {
3360
3423
  if (prices.length === 0) return null;
3361
3424
  return { min: Math.min(...prices), max: Math.max(...prices) };
3362
3425
  }
3426
+ schema() {
3427
+ const base = super.schema();
3428
+ return {
3429
+ fields: [
3430
+ ...base.fields,
3431
+ { path: "style.extendLeft", label: "Extend left", kind: "boolean", group: "line" },
3432
+ { path: "style.extendRight", label: "Extend right", kind: "boolean", group: "line" }
3433
+ ]
3434
+ };
3435
+ }
3436
+ writeProps() {
3437
+ return { ...super.writeProps(), showRatios: this.showRatios, showPrices: this.showPrices, labelSide: this.labelSide, background: this.background };
3438
+ }
3439
+ readProps(props) {
3440
+ super.readProps(props);
3441
+ if (typeof props.showRatios === "boolean") this.showRatios = props.showRatios;
3442
+ if (typeof props.showPrices === "boolean") this.showPrices = props.showPrices;
3443
+ if (props.labelSide === "left" || props.labelSide === "right") this.labelSide = props.labelSide;
3444
+ if (typeof props.background === "boolean") this.background = props.background;
3445
+ }
3363
3446
  };
3364
3447
 
3365
3448
  // src/core/drawings/levelPalette.ts
@@ -3405,14 +3488,31 @@ var RATIO_COLORS = [
3405
3488
  [0.75, LEVEL_BLUE],
3406
3489
  [0.786, LEVEL_BLUE],
3407
3490
  [1, LEVEL_ANCHOR],
3491
+ [0.705, LEVEL_AMBER],
3492
+ [0.886, LEVEL_PURPLE],
3408
3493
  [1.272, LEVEL_BLUE],
3409
3494
  [1.382, LEVEL_ORANGE],
3495
+ [1.414, LEVEL_AQUA],
3410
3496
  [1.618, LEVEL_RED],
3411
3497
  [2, LEVEL_ANCHOR],
3498
+ [2.272, LEVEL_VIOLET],
3412
3499
  [2.382, LEVEL_ORANGE],
3500
+ [2.414, LEVEL_PINK],
3413
3501
  [2.618, LEVEL_ORANGE],
3414
3502
  [3, LEVEL_ANCHOR],
3503
+ [3.272, LEVEL_CORAL],
3504
+ [3.618, LEVEL_PURPLE],
3415
3505
  [4.236, LEVEL_GREEN],
3506
+ [4.618, LEVEL_PINK],
3507
+ // Beyond the swing's end (targets): blues, deepening with distance.
3508
+ [-0.27, LEVEL_CYAN],
3509
+ [-0.272, LEVEL_CYAN],
3510
+ [-0.62, LEVEL_BLUE],
3511
+ [-0.618, LEVEL_BLUE],
3512
+ [-1, LEVEL_AQUA],
3513
+ [-1.5, LEVEL_VIOLET],
3514
+ [-2, LEVEL_PURPLE],
3515
+ [-2.5, LEVEL_PINK],
3416
3516
  [6.854, LEVEL_TEAL],
3417
3517
  [11.09, LEVEL_BLUE]
3418
3518
  ];
@@ -3439,8 +3539,41 @@ function cycleLevels(count, enabledCount = count) {
3439
3539
  }));
3440
3540
  }
3441
3541
 
3542
+ // src/core/drawings/fibPresets.ts
3543
+ var LADDER = [0, 0.236, 0.382, 0.5, 0.618, 0.705, 0.786, 0.886, 1, 1.272, 1.414, 1.618, 2, 2.272, 2.414, 2.618, 3, 3.272, 3.618, 4.236, 4.618, -0.272, -0.618, -1];
3544
+ var OTE_LADDER = [0, 0.236, 0.382, 0.5, 0.62, 0.705, 0.79, 0.886, 1, 1.272, 1.414, 1.618, 2, 2.618, 3, 3.618, 4.236, 4.618, -0.27, -0.62, -1, -1.5, -2, -2.5];
3545
+ function ladder(ratios, on, colors = {}) {
3546
+ return fibLevels(ratios.map((ratio) => ({ ratio, enabled: on.includes(ratio), color: colors[String(ratio)] })));
3547
+ }
3548
+ var FIB_PRESETS = [
3549
+ {
3550
+ key: "classic",
3551
+ label: "Classic",
3552
+ reverse: false,
3553
+ levels: ladder(LADDER, [0, 0.236, 0.382, 0.5, 0.618, 0.786, 1])
3554
+ },
3555
+ {
3556
+ key: "extension",
3557
+ label: "Extension",
3558
+ reverse: true,
3559
+ levels: ladder(LADDER, [0, 1, 1.272, 1.414, 1.618, 2, 2.618, 3.618, 4.236])
3560
+ },
3561
+ {
3562
+ key: "ote",
3563
+ label: "OTE",
3564
+ reverse: false,
3565
+ // The entry zone reads as one band.
3566
+ levels: ladder(OTE_LADDER, [0, 0.5, 0.62, 0.705, 0.79, 1, -0.27, -0.62], { "0.62": LEVEL_AMBER, "0.79": LEVEL_AMBER })
3567
+ }
3568
+ ];
3569
+ function matchFibPreset(levels, reverse) {
3570
+ const key = (ls) => JSON.stringify(ls.map((l) => [l.ratio, l.color, l.enabled, l.label ?? ""]));
3571
+ const mine = key(levels);
3572
+ return FIB_PRESETS.find((p) => p.reverse === reverse && key(p.levels) === mine)?.key ?? null;
3573
+ }
3574
+
3442
3575
  // src/core/drawings/types/FibRetracement.ts
3443
- var LEVELS = fibLevels([0, 0.236, 0.382, 0.5, 0.618, 0.786, 1]);
3576
+ var LEVELS = FIB_PRESETS[0].levels;
3444
3577
  var FibRetracement = class extends FibLevels {
3445
3578
  constructor(init) {
3446
3579
  super(init);
@@ -3463,7 +3596,11 @@ var FibRetracement = class extends FibLevels {
3463
3596
  };
3464
3597
 
3465
3598
  // src/core/drawings/types/FibExtension.ts
3466
- var LEVELS2 = fibLevels([0, 0.382, 0.618, 1, 1.272, 1.618, { ratio: 2.618, color: LEVEL_PURPLE }]);
3599
+ var LEVELS2 = ladder(
3600
+ [0, 0.236, 0.382, 0.5, 0.618, 0.786, 1, 1.272, 1.414, 1.618, 2, 2.272, 2.414, 2.618, 3, 3.272, 3.618, 4.236, 4.618],
3601
+ [0, 0.382, 0.618, 1, 1.272, 1.618, 2.618],
3602
+ { "2.618": LEVEL_PURPLE }
3603
+ );
3467
3604
  var FibExtension = class extends FibLevels {
3468
3605
  constructor() {
3469
3606
  super(...arguments);
@@ -5050,20 +5187,20 @@ function signed(n) {
5050
5187
  }
5051
5188
  function formatDuration(ms) {
5052
5189
  const m = Math.abs(ms);
5053
- const DAY2 = 864e5;
5190
+ const DAY3 = 864e5;
5054
5191
  const HR = 36e5;
5055
- const MIN2 = 6e4;
5056
- if (m >= DAY2) {
5057
- const d = Math.floor(m / DAY2);
5058
- const h = Math.round(m % DAY2 / HR);
5192
+ const MIN3 = 6e4;
5193
+ if (m >= DAY3) {
5194
+ const d = Math.floor(m / DAY3);
5195
+ const h = Math.round(m % DAY3 / HR);
5059
5196
  return h ? `${d}d ${h}h` : `${d}d`;
5060
5197
  }
5061
5198
  if (m >= HR) {
5062
5199
  const h = Math.floor(m / HR);
5063
- const mn = Math.round(m % HR / MIN2);
5200
+ const mn = Math.round(m % HR / MIN3);
5064
5201
  return mn ? `${h}h ${mn}m` : `${h}h`;
5065
5202
  }
5066
- if (m >= MIN2) return `${Math.round(m / MIN2)}m`;
5203
+ if (m >= MIN3) return `${Math.round(m / MIN3)}m`;
5067
5204
  return `${Math.round(m / 1e3)}s`;
5068
5205
  }
5069
5206
  var MeasureBox = class extends Drawing {
@@ -5543,6 +5680,11 @@ var MAGNIFIER_TIMEFRAME_OPTIONS = [
5543
5680
  { value: "240", label: "4h", ms: 144e5 },
5544
5681
  { value: "D", label: "1D", ms: 864e5 }
5545
5682
  ];
5683
+ function magnifierTimeframesBelow(chartMs) {
5684
+ if (!(chartMs > 0)) return [...MAGNIFIER_TIMEFRAME_OPTIONS];
5685
+ const lower = MAGNIFIER_TIMEFRAME_OPTIONS.filter((o) => o.ms > 0 && o.ms < chartMs);
5686
+ return lower.length > 0 ? [MAGNIFIER_TIMEFRAME_OPTIONS[0], ...lower] : [];
5687
+ }
5546
5688
  function magnifierTimeframeLabel(value) {
5547
5689
  const opt = MAGNIFIER_TIMEFRAME_OPTIONS.find((o) => o.value === value);
5548
5690
  if (opt) return opt.label;
@@ -5637,6 +5779,393 @@ var Magnifier = class extends Drawing {
5637
5779
  }
5638
5780
  };
5639
5781
 
5782
+ // src/core/drawings/priceAction.ts
5783
+ var GAP_REACH = 3;
5784
+ function nearestBarIndex(bars, time) {
5785
+ if (bars.length === 0) return -1;
5786
+ let lo = 0;
5787
+ let hi = bars.length - 1;
5788
+ while (lo < hi) {
5789
+ const mid = lo + hi >> 1;
5790
+ if (bars[mid].time < time) lo = mid + 1;
5791
+ else hi = mid;
5792
+ }
5793
+ if (lo > 0 && Math.abs(bars[lo - 1].time - time) <= Math.abs(bars[lo].time - time)) return lo - 1;
5794
+ return lo;
5795
+ }
5796
+ function gapAt(bars, i) {
5797
+ const prev2 = bars[i - 1];
5798
+ const next = bars[i + 1];
5799
+ if (!prev2 || !next || !bars[i]) return null;
5800
+ if (next.low > prev2.high) return { bull: true, top: next.low, bottom: prev2.high };
5801
+ if (next.high < prev2.low) return { bull: false, top: prev2.low, bottom: next.high };
5802
+ return null;
5803
+ }
5804
+ function nearestGap(bars, i) {
5805
+ for (let k = 0; k <= GAP_REACH; k += 1) {
5806
+ for (const j of k === 0 ? [i] : [i - k, i + k]) {
5807
+ const g = gapAt(bars, j);
5808
+ if (g) return { index: j, ...g };
5809
+ }
5810
+ }
5811
+ return null;
5812
+ }
5813
+ function gapState(bars, i) {
5814
+ const g = nearestGap(bars, i);
5815
+ if (!g) return null;
5816
+ const { index, bull, top, bottom } = g;
5817
+ const mid = (top + bottom) / 2;
5818
+ let tested = null;
5819
+ let half = null;
5820
+ let filled = null;
5821
+ let inverted = null;
5822
+ for (let j = index + 2; j < bars.length && inverted === null; j += 1) {
5823
+ const b = bars[j];
5824
+ const reach = bull ? b.low : b.high;
5825
+ const into = (v) => bull ? reach <= v : reach >= v;
5826
+ if (tested === null && into(bull ? top : bottom)) tested = j;
5827
+ if (half === null && into(mid)) half = j;
5828
+ if (filled === null && into(bull ? bottom : top)) filled = j;
5829
+ if (filled !== null && (bull ? b.close < bottom : b.close > top)) inverted = j;
5830
+ }
5831
+ return { index, bull, top, bottom, mid, tested, half, filled, inverted };
5832
+ }
5833
+ function blockState(bars, i, range) {
5834
+ const c = bars[i];
5835
+ if (!c) return null;
5836
+ const bull = c.close < c.open;
5837
+ const top = range === "body" ? Math.max(c.open, c.close) : c.high;
5838
+ const bottom = range === "body" ? Math.min(c.open, c.close) : c.low;
5839
+ const mid = (top + bottom) / 2;
5840
+ let left = false;
5841
+ let tested = null;
5842
+ let half = null;
5843
+ let broken = null;
5844
+ let leftAfterBreak = false;
5845
+ let retest = null;
5846
+ for (let j = i + 1; j < bars.length && retest === null; j += 1) {
5847
+ const b = bars[j];
5848
+ if (broken === null) {
5849
+ if (!left) left = bull ? b.low > top : b.high < bottom;
5850
+ else {
5851
+ if (tested === null && (bull ? b.low <= top : b.high >= bottom)) tested = j;
5852
+ if (half === null && (bull ? b.low <= mid : b.high >= mid)) half = j;
5853
+ }
5854
+ if (bull ? b.close < bottom : b.close > top) broken = j;
5855
+ } else if (!leftAfterBreak) leftAfterBreak = bull ? b.high < bottom : b.low > top;
5856
+ else if (bull ? b.high >= bottom : b.low <= top) retest = j;
5857
+ }
5858
+ return { index: i, bull, top, bottom, mid, tested, half, broken, retest };
5859
+ }
5860
+ function liquidityState(bars, i, side) {
5861
+ const c = bars[i];
5862
+ if (!c) return null;
5863
+ const high = side === "high";
5864
+ const price = high ? c.high : c.low;
5865
+ let swept = null;
5866
+ let broken = null;
5867
+ for (let j = i + 1; j < bars.length && broken === null; j += 1) {
5868
+ const b = bars[j];
5869
+ if (high ? b.close > price : b.close < price) broken = j;
5870
+ else if (swept === null && (high ? b.high > price : b.low < price)) swept = j;
5871
+ }
5872
+ return { index: i, high, price, swept, broken };
5873
+ }
5874
+
5875
+ // src/core/drawings/types/PriceActionZone.ts
5876
+ var candlesSeen = /* @__PURE__ */ new WeakMap();
5877
+ var PriceActionTool = class extends Drawing {
5878
+ anchorSchema() {
5879
+ return { min: 1, max: 1, slots: [{ role: "candle", free: "x" }] };
5880
+ }
5881
+ /** The chart's candles, and the one nearest the anchor. */
5882
+ candles(proj) {
5883
+ const a = this.anchors[0];
5884
+ if (!a) return null;
5885
+ let bars = candlesSeen.get(proj);
5886
+ if (!bars) {
5887
+ bars = proj.barsInRange?.(Number.NEGATIVE_INFINITY, Number.POSITIVE_INFINITY) ?? [];
5888
+ candlesSeen.set(proj, bars);
5889
+ }
5890
+ return bars.length > 0 ? { bars, at: nearestBarIndex(bars, a.time) } : null;
5891
+ }
5892
+ /** Half a candle's width at bar `i`, in pixels. */
5893
+ halfBar(proj, bars, i) {
5894
+ const a = bars[Math.max(0, i - 1)];
5895
+ const b = bars[Math.min(bars.length - 1, Math.max(1, i))];
5896
+ if (!a || !b || a === b) return 3;
5897
+ return Math.max(1, Math.abs(proj.xOf(b.time) - proj.xOf(a.time)) / 2);
5898
+ }
5899
+ /** Pixel x for the right edge of a zone that stops at bar `end`, or runs on to the chart's edge. */
5900
+ rightEdge(proj, bars, end) {
5901
+ if (end === null) return Math.max(proj.width, proj.xOf(bars[bars.length - 1].time));
5902
+ return proj.xOf(bars[end].time) + this.halfBar(proj, bars, end);
5903
+ }
5904
+ /** The anchor itself, for a tool with nothing on the candles to hang its handle on. */
5905
+ anchorHandle(proj) {
5906
+ const a = this.anchors[0];
5907
+ const y = a ? proj.yOf(a.price, this.paneId) : null;
5908
+ return a && y != null ? [[proj.xOf(a.time), y]] : [];
5909
+ }
5910
+ hitHandle(px, py, proj, tol) {
5911
+ return handleAt(px, py, this.handlePoints(proj), tol + 3);
5912
+ }
5913
+ priceRange() {
5914
+ return null;
5915
+ }
5916
+ };
5917
+ var PriceActionZone = class extends PriceActionTool {
5918
+ /** The zone's pixel layout, or null when the candles don't form one. */
5919
+ zone(proj) {
5920
+ const c = this.candles(proj);
5921
+ const f = c && this.formation(c.bars, c.at);
5922
+ if (!c || !f) return null;
5923
+ const yt = proj.yOf(f.top, this.paneId);
5924
+ const yb = proj.yOf(f.bottom, this.paneId);
5925
+ if (yt == null || yb == null) return null;
5926
+ const { bars } = c;
5927
+ const s = this.settings();
5928
+ const left = (i) => proj.xOf(bars[i].time) - this.halfBar(proj, bars, i);
5929
+ const yTop = Math.min(yt, yb);
5930
+ const yBottom = Math.max(yt, yb);
5931
+ const midY = proj.yOf(f.mid, this.paneId) ?? (yTop + yBottom) / 2;
5932
+ const flip = f.flip && { x1: left(f.flip.from), x2: this.rightEdge(proj, bars, f.flip.to), fill: f.bull ? s.downColor : s.upColor };
5933
+ const edge = s.extend === "tested" ? f.bull ? f.top : f.bottom : s.extend === "half" ? f.mid : f.bull ? f.bottom : f.top;
5934
+ const edgeY = proj.yOf(edge, this.paneId);
5935
+ return {
5936
+ x1: left(f.start),
5937
+ x2: this.rightEdge(proj, bars, f.end),
5938
+ yTop,
5939
+ yBottom,
5940
+ fill: f.bull ? s.upColor : s.downColor,
5941
+ border: s.border,
5942
+ midY: s.midline ? midY : null,
5943
+ faded: f.end !== null && s.fade && !flip,
5944
+ marker: f.end !== null && !flip && edgeY != null ? { x: proj.xOf(bars[f.end].time), y: edgeY } : null,
5945
+ flip,
5946
+ handle: [proj.xOf(bars[f.index].time), midY]
5947
+ };
5948
+ }
5949
+ hitTest(px, py, proj, tol) {
5950
+ const z = this.zone(proj);
5951
+ if (!z) {
5952
+ const a = this.anchors[0];
5953
+ return a != null && Math.abs(px - proj.xOf(a.time)) <= tol;
5954
+ }
5955
+ const inY = py >= z.yTop - tol && py <= z.yBottom + tol;
5956
+ if (inY && px >= z.x1 - tol && px <= z.x2 + tol) return true;
5957
+ return z.flip != null && inY && px >= z.flip.x1 - tol && px <= z.flip.x2 + tol;
5958
+ }
5959
+ handlePoints(proj) {
5960
+ const z = this.zone(proj);
5961
+ return z ? [z.handle] : this.anchorHandle(proj);
5962
+ }
5963
+ bounds(proj) {
5964
+ const z = this.zone(proj);
5965
+ if (!z) return null;
5966
+ const right = Math.min(proj.width, z.flip ? z.flip.x2 : z.x2);
5967
+ return { x: z.x1, y: z.yTop, w: Math.max(1, right - z.x1), h: Math.max(1, z.yBottom - z.yTop) };
5968
+ }
5969
+ };
5970
+
5971
+ // src/core/drawings/types/FairValueGap.ts
5972
+ var EXTEND_OPTIONS = [
5973
+ { value: "tested", label: "Until tested" },
5974
+ { value: "half", label: "Until 50%" },
5975
+ { value: "filled", label: "Until filled" },
5976
+ { value: "always", label: "Always" }
5977
+ ];
5978
+ function defaults() {
5979
+ return { extend: "filled", upColor: `${BULLISH}33`, downColor: `${BEARISH}33`, midline: true, border: false, fade: true, inverse: false };
5980
+ }
5981
+ var FairValueGap = class extends PriceActionZone {
5982
+ constructor(init) {
5983
+ super(init);
5984
+ this.type = "fairvaluegap";
5985
+ if (!this.gap) this.gap = defaults();
5986
+ }
5987
+ settings() {
5988
+ return this.gap;
5989
+ }
5990
+ formation(bars, at) {
5991
+ const s = gapState(bars, at);
5992
+ if (!s) return null;
5993
+ const { extend, inverse } = this.gap;
5994
+ const flip = inverse && extend === "filled" && s.inverted !== null ? { from: s.inverted, to: null } : null;
5995
+ return { ...s, start: s.index - 1, end: extend === "always" ? null : s[extend], flip };
5996
+ }
5997
+ schema() {
5998
+ return {
5999
+ fields: [
6000
+ { path: "gap.extend", label: "Extend right", kind: "select", options: EXTEND_OPTIONS, group: "behavior" },
6001
+ { path: "gap.upColor", label: "Bullish", kind: "color", group: "fill" },
6002
+ { path: "gap.downColor", label: "Bearish", kind: "color", group: "fill" },
6003
+ { path: "gap.midline", label: "Midline", kind: "boolean", group: "line" },
6004
+ { path: "gap.border", label: "Border", kind: "boolean", group: "line" },
6005
+ { path: "gap.fade", label: "Fade at end", kind: "boolean", group: "behavior", when: { path: "gap.extend", in: ["tested", "half", "filled"] } },
6006
+ { path: "gap.inverse", label: "Flip on close", kind: "boolean", group: "behavior", when: { path: "gap.extend", in: ["filled"] } }
6007
+ ]
6008
+ };
6009
+ }
6010
+ writeProps() {
6011
+ return { ...this.gap };
6012
+ }
6013
+ readProps(props) {
6014
+ const d = defaults();
6015
+ const p = props;
6016
+ this.gap = {
6017
+ extend: EXTEND_OPTIONS.some((o) => o.value === p.extend) ? p.extend : d.extend,
6018
+ upColor: typeof p.upColor === "string" ? p.upColor : d.upColor,
6019
+ downColor: typeof p.downColor === "string" ? p.downColor : d.downColor,
6020
+ midline: typeof p.midline === "boolean" ? p.midline : d.midline,
6021
+ border: typeof p.border === "boolean" ? p.border : d.border,
6022
+ fade: typeof p.fade === "boolean" ? p.fade : d.fade,
6023
+ inverse: typeof p.inverse === "boolean" ? p.inverse : d.inverse
6024
+ };
6025
+ }
6026
+ };
6027
+
6028
+ // src/core/drawings/types/OrderBlock.ts
6029
+ var RANGE_OPTIONS = [
6030
+ { value: "body", label: "Body" },
6031
+ { value: "wick", label: "Full range" }
6032
+ ];
6033
+ var EXTEND_OPTIONS2 = [
6034
+ { value: "tested", label: "Until tested" },
6035
+ { value: "half", label: "Until 50%" },
6036
+ { value: "broken", label: "Until broken" },
6037
+ { value: "always", label: "Always" }
6038
+ ];
6039
+ function defaults2() {
6040
+ return { range: "body", extend: "broken", upColor: `${BULLISH}29`, downColor: `${BEARISH}29`, midline: true, border: true, fade: true, breaker: true };
6041
+ }
6042
+ var OrderBlock = class extends PriceActionZone {
6043
+ constructor(init) {
6044
+ super(init);
6045
+ this.type = "orderblock";
6046
+ if (!this.block) this.block = defaults2();
6047
+ }
6048
+ settings() {
6049
+ return this.block;
6050
+ }
6051
+ formation(bars, at) {
6052
+ const s = blockState(bars, at, this.block.range);
6053
+ if (!s) return null;
6054
+ const { extend, breaker } = this.block;
6055
+ const flip = breaker && extend === "broken" && s.broken !== null ? { from: s.broken, to: s.retest } : null;
6056
+ return { ...s, start: s.index, end: extend === "always" ? null : s[extend], flip };
6057
+ }
6058
+ schema() {
6059
+ return {
6060
+ fields: [
6061
+ { path: "block.range", label: "Zone", kind: "select", options: RANGE_OPTIONS, group: "behavior" },
6062
+ { path: "block.extend", label: "Extend right", kind: "select", options: EXTEND_OPTIONS2, group: "behavior" },
6063
+ { path: "block.upColor", label: "Bullish", kind: "color", group: "fill" },
6064
+ { path: "block.downColor", label: "Bearish", kind: "color", group: "fill" },
6065
+ { path: "block.midline", label: "Midline", kind: "boolean", group: "line" },
6066
+ { path: "block.border", label: "Border", kind: "boolean", group: "line" },
6067
+ { path: "block.fade", label: "Fade at end", kind: "boolean", group: "behavior", when: { path: "block.extend", in: ["tested", "half", "broken"] } },
6068
+ { path: "block.breaker", label: "Breaker", kind: "boolean", group: "behavior", when: { path: "block.extend", in: ["broken"] } }
6069
+ ]
6070
+ };
6071
+ }
6072
+ writeProps() {
6073
+ return { ...this.block };
6074
+ }
6075
+ readProps(props) {
6076
+ const d = defaults2();
6077
+ const p = props;
6078
+ this.block = {
6079
+ range: p.range === "body" || p.range === "wick" ? p.range : d.range,
6080
+ extend: EXTEND_OPTIONS2.some((o) => o.value === p.extend) ? p.extend : d.extend,
6081
+ upColor: typeof p.upColor === "string" ? p.upColor : d.upColor,
6082
+ downColor: typeof p.downColor === "string" ? p.downColor : d.downColor,
6083
+ midline: typeof p.midline === "boolean" ? p.midline : d.midline,
6084
+ border: typeof p.border === "boolean" ? p.border : d.border,
6085
+ fade: typeof p.fade === "boolean" ? p.fade : d.fade,
6086
+ breaker: typeof p.breaker === "boolean" ? p.breaker : d.breaker
6087
+ };
6088
+ }
6089
+ };
6090
+
6091
+ // src/core/drawings/types/Liquidity.ts
6092
+ var EXTEND_OPTIONS3 = [
6093
+ { value: "swept", label: "Until swept" },
6094
+ { value: "broken", label: "Until broken" },
6095
+ { value: "always", label: "Always" }
6096
+ ];
6097
+ function defaults3() {
6098
+ return { extend: "swept", markSweep: true };
6099
+ }
6100
+ var Liquidity = class extends PriceActionTool {
6101
+ constructor(init) {
6102
+ super(init);
6103
+ this.type = "liquidity";
6104
+ if (!this.liq) this.liq = defaults3();
6105
+ }
6106
+ anchorSchema() {
6107
+ return { min: 1, max: 1, slots: [{ role: "candle", free: "both" }] };
6108
+ }
6109
+ state(proj) {
6110
+ const c = this.candles(proj);
6111
+ const a = this.anchors[0];
6112
+ if (!c || !a) return null;
6113
+ const bar = c.bars[c.at];
6114
+ return liquidityState(c.bars, c.at, a.price >= (bar.high + bar.low) / 2 ? "high" : "low");
6115
+ }
6116
+ line(proj) {
6117
+ const c = this.candles(proj);
6118
+ const s = this.state(proj);
6119
+ if (!c || !s) return null;
6120
+ const y = proj.yOf(s.price, this.paneId);
6121
+ if (y == null) return null;
6122
+ const { bars } = c;
6123
+ const { extend, markSweep } = this.liq;
6124
+ const end = extend === "always" ? null : extend === "broken" ? s.broken : s.swept ?? s.broken;
6125
+ const x1 = proj.xOf(bars[s.index].time);
6126
+ let sweep = null;
6127
+ if (markSweep && s.swept !== null && (end === null || s.swept <= end)) {
6128
+ const b = bars[s.swept];
6129
+ const wick = proj.yOf(s.high ? b.high : b.low, this.paneId);
6130
+ if (wick != null) sweep = { x: proj.xOf(b.time), y: wick + (s.high ? -9 : 9) };
6131
+ }
6132
+ return { x1, x2: this.rightEdge(proj, bars, end), y, sweep, handle: [x1, y] };
6133
+ }
6134
+ hitTest(px, py, proj, tol) {
6135
+ const l = this.line(proj);
6136
+ return l != null && px >= l.x1 - tol && px <= l.x2 + tol && Math.abs(py - l.y) <= tol;
6137
+ }
6138
+ handlePoints(proj) {
6139
+ const l = this.line(proj);
6140
+ return l ? [l.handle] : this.anchorHandle(proj);
6141
+ }
6142
+ bounds(proj) {
6143
+ const l = this.line(proj);
6144
+ if (!l) return null;
6145
+ return { x: l.x1, y: l.y - 4, w: Math.max(1, Math.min(proj.width, l.x2) - l.x1), h: 8 };
6146
+ }
6147
+ schema() {
6148
+ return {
6149
+ fields: [
6150
+ ...LINE_FIELDS,
6151
+ { path: "liq.extend", label: "Extend right", kind: "select", options: EXTEND_OPTIONS3, group: "behavior" },
6152
+ { path: "liq.markSweep", label: "Mark sweep", kind: "boolean", group: "behavior" }
6153
+ ]
6154
+ };
6155
+ }
6156
+ writeProps() {
6157
+ return { ...this.liq };
6158
+ }
6159
+ readProps(props) {
6160
+ const d = defaults3();
6161
+ const p = props;
6162
+ this.liq = {
6163
+ extend: EXTEND_OPTIONS3.some((o) => o.value === p.extend) ? p.extend : d.extend,
6164
+ markSweep: typeof p.markSweep === "boolean" ? p.markSweep : d.markSweep
6165
+ };
6166
+ }
6167
+ };
6168
+
5640
6169
  // src/core/drawings/registry.ts
5641
6170
  var REGISTRY2 = /* @__PURE__ */ new Map();
5642
6171
  function registerDrawingType(meta) {
@@ -6330,6 +6859,36 @@ registerDrawingType({
6330
6859
  defaultStyle: { lineColor: BULLISH, lineWidth: 1, lineStyle: "solid" },
6331
6860
  create: (init) => new FixedRangeVolumeProfile(init)
6332
6861
  });
6862
+ var FVG_ICON = svg24('<path d="M6 15v6M12 3v18M18 2v6"/><rect x="3.5" y="9.5" width="17" height="5" rx="1" fill="currentColor" fill-opacity="0.28" stroke-width="1.4"/>');
6863
+ var ORDER_BLOCK_ICON = svg24('<rect x="4" y="7.5" width="5" height="8" rx="1"/><path d="M6.5 4v3.5M6.5 15.5V19"/><rect x="9" y="7.5" width="11.5" height="8" rx="1" fill="currentColor" fill-opacity="0.22" stroke-width="1.4"/>');
6864
+ var LIQUIDITY_ICON = svg24('<path d="M3 8h12" stroke-dasharray="2.5 2.5"/><path d="m16.5 4.5 4 4m0-4-4 4"/><path d="M3.5 20 8 13.5l4 3.5 5-8"/>');
6865
+ registerDrawingType({
6866
+ type: "fairvaluegap",
6867
+ group: "priceaction",
6868
+ label: "Fair Value Gap",
6869
+ icon: FVG_ICON,
6870
+ defaultStyle: { lineColor: BULLISH, lineWidth: 1, lineStyle: "solid" },
6871
+ placementHint: "Click the middle candle of a gap",
6872
+ create: (init) => new FairValueGap(init)
6873
+ });
6874
+ registerDrawingType({
6875
+ type: "orderblock",
6876
+ group: "priceaction",
6877
+ label: "Order Block",
6878
+ icon: ORDER_BLOCK_ICON,
6879
+ defaultStyle: { lineColor: BULLISH, lineWidth: 1, lineStyle: "solid" },
6880
+ placementHint: "Click the candle that forms the block",
6881
+ create: (init) => new OrderBlock(init)
6882
+ });
6883
+ registerDrawingType({
6884
+ type: "liquidity",
6885
+ group: "priceaction",
6886
+ label: "Liquidity",
6887
+ icon: LIQUIDITY_ICON,
6888
+ defaultStyle: { lineColor: WARNING, lineWidth: 1, lineStyle: "solid" },
6889
+ placementHint: "Click near a candle's high or low",
6890
+ create: (init) => new Liquidity(init)
6891
+ });
6333
6892
 
6334
6893
  // src/core/drawings/toolbar.ts
6335
6894
  var FIBONACCI_TYPES = [
@@ -6346,6 +6905,7 @@ var FIBONACCI_TYPES = [
6346
6905
  "fibwedge",
6347
6906
  "fibspiral"
6348
6907
  ];
6908
+ var PRICE_ACTION_TYPES = ["fairvaluegap", "orderblock", "liquidity"];
6349
6909
  var GANN_TYPES = ["gannfan", "gannbox", "gannsquare"];
6350
6910
  var GEOMETRY_TYPES = ["dedekind", "sonic", "supersonic", "goldensonic", "goldensupersonic"];
6351
6911
  var PATTERN_TYPES = ["xabcd", "abcd", "headshoulders"];
@@ -6373,6 +6933,7 @@ var TOOLBAR_LAYOUT = [
6373
6933
  label: "Fibonacci",
6374
6934
  sections: [
6375
6935
  { label: "Fibonacci", types: FIBONACCI_TYPES },
6936
+ { label: "Price Action", types: PRICE_ACTION_TYPES },
6376
6937
  { label: "Gann", types: GANN_TYPES },
6377
6938
  { label: "Geometry", types: GEOMETRY_TYPES }
6378
6939
  ]
@@ -6550,6 +7111,7 @@ var DrawingStore = class {
6550
7111
  if (patch.visible !== void 0) d.visible = patch.visible;
6551
7112
  if (patch.zIndex !== void 0) d.zIndex = patch.zIndex;
6552
7113
  if (patch.props !== void 0) d.applyProps(patch.props);
7114
+ if ("showOn" in patch) d.showOn = sanitizeShowOn(patch.showOn);
6553
7115
  this.emit();
6554
7116
  return d;
6555
7117
  }
@@ -6713,7 +7275,79 @@ var DrawingHistory = class {
6713
7275
  }
6714
7276
  };
6715
7277
 
7278
+ // src/core/drawings/defaults.ts
7279
+ function captureToolDefaults(d) {
7280
+ const out = { style: { ...d.style } };
7281
+ if (d.text) {
7282
+ const { value: _value, ...styling } = d.text;
7283
+ out.text = styling;
7284
+ }
7285
+ const props = d.serialize().props;
7286
+ if (props !== void 0) out.props = clonePlain(props);
7287
+ return out;
7288
+ }
7289
+ function applyToolDefaults(d, defaults5) {
7290
+ if (!defaults5) return;
7291
+ if (defaults5.style) d.style = { ...d.style, ...defaults5.style };
7292
+ if (defaults5.text) d.text = { ...d.text ?? defaultText(), ...defaults5.text, value: d.text?.value ?? "" };
7293
+ if (defaults5.props) d.applyProps(clonePlain(defaults5.props));
7294
+ }
7295
+ function sameToolDefaults(a, b) {
7296
+ return JSON.stringify(a ?? null) === JSON.stringify(b ?? null);
7297
+ }
7298
+ function sanitizeToolDefaults(raw) {
7299
+ if (raw == null || typeof raw !== "object" || Array.isArray(raw)) return null;
7300
+ const r = raw;
7301
+ const obj = (v) => v != null && typeof v === "object" && !Array.isArray(v) ? v : void 0;
7302
+ const out = {};
7303
+ const style = obj(r.style);
7304
+ if (style) out.style = style;
7305
+ const text = obj(r.text);
7306
+ if (text) {
7307
+ const { value: _value, ...styling } = text;
7308
+ out.text = styling;
7309
+ }
7310
+ const props = obj(r.props);
7311
+ if (props) out.props = props;
7312
+ return out.style || out.text || out.props ? out : null;
7313
+ }
7314
+ var TEMPLATE_NAME_MAX = 60;
7315
+ function sanitizeToolTemplates(raw) {
7316
+ if (!Array.isArray(raw)) return [];
7317
+ const byName = /* @__PURE__ */ new Map();
7318
+ for (const entry of raw) {
7319
+ if (entry == null || typeof entry !== "object") continue;
7320
+ const e = entry;
7321
+ const name = typeof e.name === "string" ? e.name.trim().slice(0, TEMPLATE_NAME_MAX) : "";
7322
+ const settings = sanitizeToolDefaults(e.settings);
7323
+ if (name && settings) byName.set(name, { name, settings });
7324
+ }
7325
+ return [...byName.values()];
7326
+ }
7327
+ var LINE_TOOLS = /* @__PURE__ */ new Set(["trendline", "ray", "extendedline", "hline", "hray", "vline", "crossline", "infoline", "trendangle"]);
7328
+ var AREA_TOOLS = /* @__PURE__ */ new Set(["box", "rotatedrect", "ellipse", "circle", "triangle", "parallelchannel", "disjointchannel", "flattopbottom"]);
7329
+ function builtinToolTemplates(type) {
7330
+ if (LINE_TOOLS.has(type)) {
7331
+ return [
7332
+ { name: "Support", settings: { style: { lineColor: BULLISH, lineWidth: 2, lineStyle: "solid" } } },
7333
+ { name: "Resistance", settings: { style: { lineColor: BEARISH, lineWidth: 2, lineStyle: "solid" } } },
7334
+ { name: "Projection", settings: { style: { lineColor: NEUTRAL, lineWidth: 1, lineStyle: "dashed" } } }
7335
+ ];
7336
+ }
7337
+ if (AREA_TOOLS.has(type)) {
7338
+ return [
7339
+ { name: "Demand", settings: { style: { lineColor: BULLISH, fillColor: `${BULLISH}26` } } },
7340
+ { name: "Supply", settings: { style: { lineColor: BEARISH, fillColor: `${BEARISH}26` } } }
7341
+ ];
7342
+ }
7343
+ return [];
7344
+ }
7345
+
6716
7346
  // src/core/drawings/DrawingController.ts
7347
+ function factoryDefaults(d) {
7348
+ const fresh = createDrawing(d.type, { paneId: d.paneId });
7349
+ return fresh ? captureToolDefaults(fresh) : void 0;
7350
+ }
6717
7351
  var DrawingController = class {
6718
7352
  constructor(renderer, events, option, seriesGateway) {
6719
7353
  this.events = events;
@@ -6732,8 +7366,10 @@ var DrawingController = class {
6732
7366
  // ordered; [0] is the primary (settings-popup) selection
6733
7367
  this.clipboard = [];
6734
7368
  // in-memory copy buffer (per chart)
6735
- this.lastStyle = /* @__PURE__ */ new Map();
6736
- // per-tool "last used" style
7369
+ /** Per-tool remembered settings — the last drawing of each type the user styled seeds the next. */
7370
+ this.defaults = /* @__PURE__ */ new Map();
7371
+ /** Per-tool saved looks ("styles"), in the order they were saved. */
7372
+ this.templates = /* @__PURE__ */ new Map();
6737
7373
  this.subs = [];
6738
7374
  this.enabled = !!renderer.capabilities.userDrawings && !!renderer.userDrawingsPort;
6739
7375
  this.port = this.enabled ? renderer.userDrawingsPort : null;
@@ -6755,7 +7391,8 @@ var DrawingController = class {
6755
7391
  if (!this.port) return;
6756
7392
  const changed = type !== this.activeTool;
6757
7393
  this.activeTool = type;
6758
- this.port.setActiveTool(type, type ? this.lastStyle.get(type) : void 0);
7394
+ const defaults5 = type ? this.defaults.get(type) : void 0;
7395
+ this.port.setActiveTool(type, defaults5?.style, defaults5);
6759
7396
  if (changed) this.events.emit("drawing:tool", { type });
6760
7397
  }
6761
7398
  getTool() {
@@ -6823,6 +7460,84 @@ var DrawingController = class {
6823
7460
  this.port?.setFavorites?.(list);
6824
7461
  this.events.emit("drawing:favorites", { favorites: list });
6825
7462
  }
7463
+ /** Every tool's remembered settings, by type (plain JSON — persist it as-is). */
7464
+ toolDefaults() {
7465
+ const out = {};
7466
+ for (const [type, d] of this.defaults) out[type] = clonePlain(d);
7467
+ return out;
7468
+ }
7469
+ /** Replace every tool's remembered settings (restoring persisted prefs). Unknown types and
7470
+ * malformed entries are dropped; an unchanged set is a no-op and emits nothing. */
7471
+ setToolDefaults(map2) {
7472
+ const next = /* @__PURE__ */ new Map();
7473
+ for (const [type, raw] of Object.entries(map2 ?? {})) {
7474
+ if (!getDrawingType(type)) continue;
7475
+ const d = sanitizeToolDefaults(raw);
7476
+ if (d) next.set(type, clonePlain(d));
7477
+ }
7478
+ const changed = /* @__PURE__ */ new Set([...this.defaults.keys(), ...next.keys()]);
7479
+ for (const type of [...changed]) if (sameToolDefaults(this.defaults.get(type), next.get(type))) changed.delete(type);
7480
+ if (changed.size === 0) return;
7481
+ this.defaults.clear();
7482
+ for (const [type, d] of next) this.defaults.set(type, d);
7483
+ if (this.activeTool && changed.has(this.activeTool)) this.setTool(this.activeTool);
7484
+ for (const type of changed) this.events.emit("drawing:defaults", { type });
7485
+ }
7486
+ /** Forget a tool's remembered settings (every tool's when `type` is omitted) — new drawings
7487
+ * start from the factory defaults again. */
7488
+ resetToolDefaults(type) {
7489
+ const types = type ? this.defaults.has(type) ? [type] : [] : [...this.defaults.keys()];
7490
+ for (const t of types) {
7491
+ this.defaults.delete(t);
7492
+ this.events.emit("drawing:defaults", { type: t });
7493
+ }
7494
+ }
7495
+ /** Every tool's saved looks, by type (plain JSON — persist it as-is). */
7496
+ toolTemplates() {
7497
+ const out = {};
7498
+ for (const [type, list] of this.templates) out[type] = clonePlain(list);
7499
+ return out;
7500
+ }
7501
+ /** Replace every tool's saved looks (restoring persisted prefs). Unknown types and
7502
+ * malformed entries are dropped; an unchanged set is a no-op and emits nothing. */
7503
+ setToolTemplates(map2) {
7504
+ const next = /* @__PURE__ */ new Map();
7505
+ for (const [type, raw] of Object.entries(map2 ?? {})) {
7506
+ if (!getDrawingType(type)) continue;
7507
+ const list = sanitizeToolTemplates(raw);
7508
+ if (list.length > 0) next.set(type, list);
7509
+ }
7510
+ const changed = [.../* @__PURE__ */ new Set([...this.templates.keys(), ...next.keys()])].filter(
7511
+ (t) => JSON.stringify(this.templates.get(t) ?? []) !== JSON.stringify(next.get(t) ?? [])
7512
+ );
7513
+ if (changed.length === 0) return;
7514
+ this.templates = next;
7515
+ this.pushTemplates();
7516
+ for (const type of changed) this.events.emit("drawing:templates", { type });
7517
+ }
7518
+ /** Save the given settings as a named look for a tool (a same-named one is replaced). */
7519
+ saveToolTemplate(type, name, settings) {
7520
+ if (!getDrawingType(type)) return;
7521
+ const [entry] = sanitizeToolTemplates([{ name, settings }]);
7522
+ if (!entry) return;
7523
+ const list = (this.templates.get(type) ?? []).filter((t) => t.name !== entry.name);
7524
+ this.templates.set(type, [...list, clonePlain(entry)]);
7525
+ this.pushTemplates();
7526
+ this.events.emit("drawing:templates", { type });
7527
+ }
7528
+ /** Delete a tool's saved look by name. */
7529
+ removeToolTemplate(type, name) {
7530
+ const list = this.templates.get(type);
7531
+ if (!list?.some((t) => t.name === name)) return;
7532
+ const rest = list.filter((t) => t.name !== name);
7533
+ if (rest.length > 0) this.templates.set(type, rest);
7534
+ else this.templates.delete(type);
7535
+ this.pushTemplates();
7536
+ this.events.emit("drawing:templates", { type });
7537
+ }
7538
+ pushTemplates() {
7539
+ this.port?.setToolTemplates?.(this.toolTemplates());
7540
+ }
6826
7541
  setToolbar(option) {
6827
7542
  this.port?.setToolbar(buildToolbar(option).definition);
6828
7543
  }
@@ -6838,21 +7553,22 @@ var DrawingController = class {
6838
7553
  // ── programmatic CRUD (facade-facing) ── each is one undo step
6839
7554
  add(type, init = {}) {
6840
7555
  if (!this.enabled) return null;
6841
- const last = this.lastStyle.get(type);
6842
- const style = { ...last ?? {}, ...init.style ?? {} };
7556
+ const defaults5 = this.defaults.get(type);
6843
7557
  const d = createDrawing(type, {
6844
7558
  id: this.store.nextId(),
6845
7559
  paneId: init.paneId ?? "price",
6846
7560
  anchors: init.anchors,
6847
- style,
6848
7561
  text: init.text,
6849
- props: init.props,
6850
7562
  zIndex: init.zIndex ?? this.startZ(type, init.paneId ?? "price")
6851
7563
  });
6852
7564
  if (!d) return null;
7565
+ applyToolDefaults(d, defaults5);
7566
+ if (init.style) d.style = { ...d.style, ...init.style };
7567
+ if (init.text) d.text = { ...d.text ?? init.text, ...init.text };
7568
+ if (init.props) d.applyProps(init.props);
6853
7569
  this.history.record(this.store.serialize());
6854
7570
  this.store.add(d);
6855
- this.captureStyle(d.id);
7571
+ this.captureDefaults(d.id);
6856
7572
  this.events.emit("drawing:created", { id: d.id });
6857
7573
  return d;
6858
7574
  }
@@ -6864,9 +7580,8 @@ var DrawingController = class {
6864
7580
  const before = this.store.serialize();
6865
7581
  const cur = patch.style ? this.store.get(id) : void 0;
6866
7582
  const resolved = patch.style && cur ? { ...patch, style: { ...cur.style, ...patch.style } } : patch;
6867
- if (this.store.update(id, resolved)) {
7583
+ if (this.updateDrawing(id, resolved)) {
6868
7584
  this.history.record(before);
6869
- this.captureStyle(id);
6870
7585
  this.events.emit("drawing:edited", { id });
6871
7586
  }
6872
7587
  }
@@ -6874,7 +7589,7 @@ var DrawingController = class {
6874
7589
  updateMany(patches) {
6875
7590
  this.history.begin(this.store.serialize());
6876
7591
  for (const { id, patch } of patches) {
6877
- if (this.store.update(id, patch)) {
7592
+ if (this.updateDrawing(id, patch)) {
6878
7593
  this.history.markDirty();
6879
7594
  this.events.emit("drawing:edited", { id });
6880
7595
  }
@@ -7054,10 +7769,27 @@ var DrawingController = class {
7054
7769
  resolve(ids) {
7055
7770
  return ids.map((id) => this.store.get(id)).filter((d) => d != null);
7056
7771
  }
7057
- /** Remember a drawing's style as the "last used" for its type (seeds the next one). */
7058
- captureStyle(id) {
7772
+ /** `store.update` that remembers the drawing's settings only when the patch changed them:
7773
+ * moving an older drawing must not hand its look back to the tool. */
7774
+ updateDrawing(id, patch) {
7775
+ const cur = this.store.get(id);
7776
+ const before = cur ? captureToolDefaults(cur) : void 0;
7777
+ if (!this.store.update(id, patch)) return false;
7778
+ const after = this.store.get(id);
7779
+ if (after && !sameToolDefaults(before, captureToolDefaults(after))) this.captureDefaults(id);
7780
+ return true;
7781
+ }
7782
+ /** Remember a drawing's settings as its tool's defaults (they seed the next one), and
7783
+ * announce a real change so a host can save it. */
7784
+ captureDefaults(id) {
7059
7785
  const d = this.store.get(id);
7060
- if (d) this.lastStyle.set(d.type, { ...d.style });
7786
+ if (!d) return;
7787
+ const captured = captureToolDefaults(d);
7788
+ const next = sameToolDefaults(captured, factoryDefaults(d)) ? void 0 : captured;
7789
+ if (sameToolDefaults(this.defaults.get(d.type), next)) return;
7790
+ if (next) this.defaults.set(d.type, next);
7791
+ else this.defaults.delete(d.type);
7792
+ this.events.emit("drawing:defaults", { type: d.type });
7061
7793
  }
7062
7794
  sync() {
7063
7795
  this.port?.syncDrawings(this.store.serialize().drawings);
@@ -7073,22 +7805,20 @@ var DrawingController = class {
7073
7805
  break;
7074
7806
  case "create": {
7075
7807
  const before = this.store.serialize();
7076
- const last = this.lastStyle.get(i.doc.type);
7077
- const style = last ? { ...i.doc.style, ...last } : i.doc.style;
7078
- const d = deserializeDrawing({ ...i.doc, id: this.store.nextId(), style });
7808
+ const d = deserializeDrawing({ ...i.doc, id: this.store.nextId() });
7079
7809
  if (!d) return;
7810
+ applyToolDefaults(d, this.defaults.get(d.type));
7080
7811
  if (!d.zIndex) d.zIndex = this.startZ(d.type, d.paneId) ?? 0;
7081
7812
  this.history.record(before);
7082
7813
  this.store.add(d);
7083
- this.captureStyle(d.id);
7814
+ this.captureDefaults(d.id);
7084
7815
  this.events.emit("drawing:created", { id: d.id });
7085
7816
  break;
7086
7817
  }
7087
7818
  case "edit": {
7088
7819
  const before = this.store.serialize();
7089
- if (this.store.update(i.doc.id, i.doc)) {
7820
+ if (this.updateDrawing(i.doc.id, i.doc)) {
7090
7821
  this.history.record(before);
7091
- this.captureStyle(i.doc.id);
7092
7822
  this.events.emit("drawing:edited", { id: i.doc.id });
7093
7823
  }
7094
7824
  break;
@@ -7096,7 +7826,7 @@ var DrawingController = class {
7096
7826
  case "edit-many": {
7097
7827
  this.history.begin(this.store.serialize());
7098
7828
  for (const doc of i.docs) {
7099
- if (this.store.update(doc.id, doc)) {
7829
+ if (this.updateDrawing(doc.id, doc)) {
7100
7830
  this.history.markDirty();
7101
7831
  this.events.emit("drawing:edited", { id: doc.id });
7102
7832
  }
@@ -7120,6 +7850,12 @@ var DrawingController = class {
7120
7850
  case "favorite":
7121
7851
  this.setFavorite(i.type, i.on);
7122
7852
  break;
7853
+ case "template-save":
7854
+ this.saveToolTemplate(i.type, i.name, i.settings);
7855
+ break;
7856
+ case "template-remove":
7857
+ this.removeToolTemplate(i.type, i.name);
7858
+ break;
7123
7859
  case "snap-mode":
7124
7860
  if (i.mode !== this.snapMode) {
7125
7861
  this.snapMode = i.mode;
@@ -7493,8 +8229,8 @@ function groupMembers(pool, groupRow) {
7493
8229
  }
7494
8230
  function defaultMemberOf(pool, groupRow) {
7495
8231
  const members = groupMembers(pool, groupRow);
7496
- const defaults2 = members.filter((m) => m.default);
7497
- return (defaults2.length === 1 ? defaults2[0] : members[0]) ?? null;
8232
+ const defaults5 = members.filter((m) => m.default);
8233
+ return (defaults5.length === 1 ? defaults5[0] : members[0]) ?? null;
7498
8234
  }
7499
8235
 
7500
8236
  // src/data/ProviderRegistry.ts
@@ -8637,14 +9373,14 @@ var EngineOrchestrator = class _EngineOrchestrator {
8637
9373
  this.replayTickSource = source;
8638
9374
  this.prefetchTicks();
8639
9375
  }
8640
- replayPlay(intervalMs2) {
9376
+ replayPlay(intervalMs) {
8641
9377
  if (!this.replayQueue) return;
8642
- if (intervalMs2 !== void 0) {
8643
- if (!(Number.isFinite(intervalMs2) && intervalMs2 > 0)) {
8644
- console.warn(`[vela] chart.replay.play(${intervalMs2}) ignored \u2014 the interval must be a positive number of ms`);
9378
+ if (intervalMs !== void 0) {
9379
+ if (!(Number.isFinite(intervalMs) && intervalMs > 0)) {
9380
+ console.warn(`[vela] chart.replay.play(${intervalMs}) ignored \u2014 the interval must be a positive number of ms`);
8645
9381
  return;
8646
9382
  }
8647
- this.replayIntervalMs = intervalMs2;
9383
+ this.replayIntervalMs = intervalMs;
8648
9384
  }
8649
9385
  this.clearReplayTimer();
8650
9386
  this.replayPlaying = true;
@@ -9567,9 +10303,9 @@ var EngineOrchestrator = class _EngineOrchestrator {
9567
10303
  const prepared = await engine.prepare(source, id);
9568
10304
  if (record.source !== source) return;
9569
10305
  record.prepared = prepared;
9570
- const defaults2 = {};
9571
- for (const input of prepared.inputs) defaults2[input.key] = input.defval;
9572
- record.inputValues = { ...defaults2, ...record.inputValues };
10306
+ const defaults5 = {};
10307
+ for (const input of prepared.inputs) defaults5[input.key] = input.defval;
10308
+ record.inputValues = { ...defaults5, ...record.inputValues };
9573
10309
  handle.setSchema(prepared.inputs);
9574
10310
  const propDefaults = {};
9575
10311
  for (const prop of prepared.props ?? []) propDefaults[prop.key] = prop.defval;
@@ -10184,7 +10920,7 @@ function modelToValuePatch(model) {
10184
10920
  let to = 0;
10185
10921
  for (const s of model.series) {
10186
10922
  if (s.kind === "candle" || s.kind === "bar") {
10187
- series.push({ seriesId: s.id, kind: "bars", bars: s.bars });
10923
+ series.push({ seriesId: s.id, kind: "bars", bars: s.bars, barColors: s.barColors ?? [] });
10188
10924
  for (const b of s.bars) {
10189
10925
  if (b.time < from) from = b.time;
10190
10926
  if (b.time > to) to = b.time;
@@ -10457,20 +11193,20 @@ var RendererControl = class {
10457
11193
  };
10458
11194
 
10459
11195
  // src/core/renderer-defaults.ts
10460
- var defaults = /* @__PURE__ */ new Map();
11196
+ var defaults4 = /* @__PURE__ */ new Map();
10461
11197
  function registerRendererDefaults(values) {
10462
11198
  const applied = Object.entries(values);
10463
- for (const [key, value] of applied) defaults.set(key, value);
11199
+ for (const [key, value] of applied) defaults4.set(key, value);
10464
11200
  return () => {
10465
- for (const [key, value] of applied) if (defaults.get(key) === value) defaults.delete(key);
11201
+ for (const [key, value] of applied) if (defaults4.get(key) === value) defaults4.delete(key);
10466
11202
  };
10467
11203
  }
10468
11204
  function unregisterRendererDefaults(...keys) {
10469
- if (keys.length === 0) defaults.clear();
10470
- else for (const key of keys) defaults.delete(key);
11205
+ if (keys.length === 0) defaults4.clear();
11206
+ else for (const key of keys) defaults4.delete(key);
10471
11207
  }
10472
11208
  function rendererDefaults() {
10473
- return Object.fromEntries(defaults);
11209
+ return Object.fromEntries(defaults4);
10474
11210
  }
10475
11211
 
10476
11212
  // src/core/PanesControl.ts
@@ -10790,38 +11526,11 @@ function attachForming(closed, forming) {
10790
11526
  }
10791
11527
 
10792
11528
  // src/data/MultiProviderFeed.ts
10793
- function intervalMs(timeframe) {
10794
- const map2 = {
10795
- "1m": 6e4,
10796
- "3m": 18e4,
10797
- "5m": 3e5,
10798
- "15m": 9e5,
10799
- "30m": 18e5,
10800
- "1h": 36e5,
10801
- "2h": 72e5,
10802
- "4h": 144e5,
10803
- "1d": 864e5,
10804
- "1w": 6048e5,
10805
- "1M": 2592e6,
10806
- "1": 6e4,
10807
- "5": 3e5,
10808
- "15": 9e5,
10809
- "30": 18e5,
10810
- "60": 36e5,
10811
- "240": 144e5,
10812
- D: 864e5,
10813
- W: 6048e5,
10814
- M: 2592e6
10815
- };
10816
- return map2[timeframe] ?? 36e5;
10817
- }
10818
11529
  var MultiProviderFeed = class {
10819
11530
  constructor(store = sharedBarStore) {
10820
11531
  this.registry = new ProviderRegistry();
10821
11532
  /** The chart symbol's resolved provider — the default for bare secondary symbols. */
10822
11533
  this.primaryProvider = null;
10823
- /** A copy of offline history, used to synthesize ticks for the `data` path. */
10824
- this.liveBars = [];
10825
11534
  /** Sync-accessible symbol metadata, warmed by load()/symbolInfoFor (the engine reads it synchronously). */
10826
11535
  this.symInfoCache = /* @__PURE__ */ new Map();
10827
11536
  /** Requests in flight, keyed like the cache — every concurrent asker shares one round trip. */
@@ -10931,10 +11640,7 @@ var MultiProviderFeed = class {
10931
11640
  }
10932
11641
  // ── MarketDataFeed ─────────────────────────────────────────────────────
10933
11642
  async load(cfg) {
10934
- if (cfg.data && cfg.data.length > 0) {
10935
- this.liveBars = cfg.data.map((b) => ({ ...b }));
10936
- return cfg.data;
10937
- }
11643
+ if (cfg.data && cfg.data.length > 0) return cfg.data;
10938
11644
  const resolved = await this.registry.whenResolvable(rawSymbol(cfg), { default: this.primaryProvider });
10939
11645
  this.primaryProvider = resolved.provider;
10940
11646
  this.prefetchSymbolInfo(resolved);
@@ -10942,10 +11648,7 @@ var MultiProviderFeed = class {
10942
11648
  }
10943
11649
  /** Progressive twin of {@link load} — same resolution, the cache streams the batches. */
10944
11650
  async loadProgressive(cfg, onBatch, opts) {
10945
- if (cfg.data && cfg.data.length > 0) {
10946
- this.liveBars = cfg.data.map((b) => ({ ...b }));
10947
- return cfg.data;
10948
- }
11651
+ if (cfg.data && cfg.data.length > 0) return cfg.data;
10949
11652
  const resolved = await this.registry.whenResolvable(rawSymbol(cfg), { default: this.primaryProvider });
10950
11653
  this.primaryProvider = resolved.provider;
10951
11654
  this.prefetchSymbolInfo(resolved);
@@ -10971,39 +11674,16 @@ var MultiProviderFeed = class {
10971
11674
  if (!resolved) return [];
10972
11675
  return this.cache.loadRange(canonical(cfg, resolved), range);
10973
11676
  }
11677
+ /** Offline `data` has no source of ticks: the caller's bars are shown exactly as given,
11678
+ * and live updates need a registered provider that implements `subscribe`. */
10974
11679
  subscribe(cfg, onBar) {
10975
- if (cfg.data && cfg.data.length > 0) return this.synthesizeTicks(cfg, onBar);
11680
+ if (cfg.data && cfg.data.length > 0) return () => {
11681
+ };
10976
11682
  const resolved = this.registry.resolve(rawSymbol(cfg), { default: this.primaryProvider });
10977
11683
  if (!resolved) return () => {
10978
11684
  };
10979
11685
  return this.cache.subscribe(canonical(cfg, resolved), onBar);
10980
11686
  }
10981
- /**
10982
- * Synthesize price-relative ticks for offline `data` runs (no network), so the
10983
- * forming-candle path is exercised at any price scale. Mirrors the bundled
10984
- * provider feed's offline behavior.
10985
- */
10986
- synthesizeTicks(cfg, onBar) {
10987
- if (this.liveBars.length === 0) return () => {
10988
- };
10989
- const step = intervalMs(cfg.timeframe ?? "60");
10990
- const timer = setInterval(() => {
10991
- const bars = this.liveBars;
10992
- const forming = bars[bars.length - 1];
10993
- if (!forming) return;
10994
- const jitter = forming.close * 15e-4 * (Math.random() - 0.5) * 2;
10995
- const close = Math.max(0, forming.close + jitter);
10996
- const updated = { ...forming, close, high: Math.max(forming.high, close), low: Math.min(forming.low, close) };
10997
- bars[bars.length - 1] = updated;
10998
- onBar(updated);
10999
- if (Math.random() < 0.12) {
11000
- const next = { time: updated.time + step, open: updated.close, high: updated.close, low: updated.close, close: updated.close, volume: 0 };
11001
- bars.push(next);
11002
- onBar(next);
11003
- }
11004
- }, 1e3);
11005
- return () => clearInterval(timer);
11006
- }
11007
11687
  };
11008
11688
  function rawSymbol(cfg) {
11009
11689
  return cfg.symbol ?? "";
@@ -11355,6 +12035,34 @@ var DrawingsControl = class {
11355
12035
  if (this.ok("setFavorites")) this.ctrl.setFavorites([...types]);
11356
12036
  return this;
11357
12037
  }
12038
+ /** Every tool's remembered settings, by type — what the next drawing of each type starts
12039
+ * from (its style, text styling and per-type settings such as fib levels). Plain JSON:
12040
+ * persist it and hand it back to {@link setToolDefaults} on the next load. */
12041
+ toolDefaults() {
12042
+ return this.ctrl.toolDefaults();
12043
+ }
12044
+ /** Replace every tool's remembered settings (e.g. restoring persisted prefs). Malformed
12045
+ * entries and unknown types are dropped. Follow changes on `drawing:defaults`. */
12046
+ setToolDefaults(map2) {
12047
+ this.ctrl.setToolDefaults(map2);
12048
+ return this;
12049
+ }
12050
+ /** Forget one tool's remembered settings (every tool's without an argument). */
12051
+ resetToolDefaults(type) {
12052
+ this.ctrl.resetToolDefaults(type);
12053
+ return this;
12054
+ }
12055
+ /** Every tool's saved looks ("styles" in the settings panel), by type. Plain JSON —
12056
+ * persist it and hand it back to {@link setToolTemplates}. */
12057
+ toolTemplates() {
12058
+ return this.ctrl.toolTemplates();
12059
+ }
12060
+ /** Replace every tool's saved looks (e.g. restoring persisted prefs). Follow changes on
12061
+ * `drawing:templates`. */
12062
+ setToolTemplates(map2) {
12063
+ this.ctrl.setToolTemplates(map2);
12064
+ return this;
12065
+ }
11358
12066
  /** Aliases mirroring `renderer.getConfig()/applyConfig()` for symmetry. */
11359
12067
  getConfig() {
11360
12068
  return this.ctrl.toJSON();
@@ -11451,8 +12159,8 @@ var ReplayControl = class {
11451
12159
  }
11452
12160
  /** Reveal one bar (one tick, with {@link setTicks}) every `intervalMs` (default: the last
11453
12161
  * interval used, else 1000). Calling it again while playing changes the pace. */
11454
- play(intervalMs2) {
11455
- this.ctrl.replayPlay(intervalMs2);
12162
+ play(intervalMs) {
12163
+ this.ctrl.replayPlay(intervalMs);
11456
12164
  return this;
11457
12165
  }
11458
12166
  pause() {
@@ -11563,6 +12271,12 @@ function mix(base, top, alpha) {
11563
12271
  const c = (x, y) => Math.round(x + (y - x) * alpha);
11564
12272
  return `rgb(${c(b.r, t.r)},${c(b.g, t.g)},${c(b.b, t.b)})`;
11565
12273
  }
12274
+ function toHex6(color, fallback = "#888888") {
12275
+ const c = parseRgb(color);
12276
+ if (!c) return fallback;
12277
+ const h = (n) => Math.max(0, Math.min(255, Math.round(n))).toString(16).padStart(2, "0");
12278
+ return `#${h(c.r)}${h(c.g)}${h(c.b)}`;
12279
+ }
11566
12280
  function isDarkColor(color) {
11567
12281
  const c = parseRgb(color);
11568
12282
  if (!c) return true;
@@ -12713,26 +13427,26 @@ var CalloutBubble = class {
12713
13427
  title.textContent = panel.title;
12714
13428
  root.appendChild(title);
12715
13429
  }
12716
- for (const row of calloutPanelRows(panel.items)) {
12717
- if (row.type === "text") {
13430
+ for (const row2 of calloutPanelRows(panel.items)) {
13431
+ if (row2.type === "text") {
12718
13432
  const text = doc.createElement("div");
12719
13433
  text.className = "vela-callout-text";
12720
- text.textContent = row.text;
13434
+ text.textContent = row2.text;
12721
13435
  root.appendChild(text);
12722
13436
  continue;
12723
13437
  }
12724
13438
  const actions = doc.createElement("div");
12725
13439
  actions.className = "vela-callout-actions";
12726
- for (const item of row.buttons) {
12727
- const btn2 = doc.createElement("button");
12728
- btn2.type = "button";
12729
- btn2.className = "vela-callout-btn" + (item.primary ? " vela-callout-btn-primary" : "");
12730
- btn2.textContent = item.label;
12731
- btn2.addEventListener("click", () => {
13440
+ for (const item of row2.buttons) {
13441
+ const btn = doc.createElement("button");
13442
+ btn.type = "button";
13443
+ btn.className = "vela-callout-btn" + (item.primary ? " vela-callout-btn-primary" : "");
13444
+ btn.textContent = item.label;
13445
+ btn.addEventListener("click", () => {
12732
13446
  item.run();
12733
13447
  if (closesPanel(item)) this.pop?.hide();
12734
13448
  });
12735
- actions.appendChild(btn2);
13449
+ actions.appendChild(btn);
12736
13450
  }
12737
13451
  root.appendChild(actions);
12738
13452
  }
@@ -12980,19 +13694,19 @@ var Select = class {
12980
13694
  wrap.className = "vela-select";
12981
13695
  wrap.dataset.size = this.ctrl.size;
12982
13696
  if (this.ctrl.fill) wrap.dataset.fill = "";
12983
- const btn2 = doc.createElement("button");
12984
- btn2.type = "button";
12985
- if (opts.id) btn2.id = opts.id;
12986
- btn2.className = "vela-select-trigger";
12987
- if (this.ctrl.disabled) btn2.disabled = true;
13697
+ const btn = doc.createElement("button");
13698
+ btn.type = "button";
13699
+ if (opts.id) btn.id = opts.id;
13700
+ btn.className = "vela-select-trigger";
13701
+ if (this.ctrl.disabled) btn.disabled = true;
12988
13702
  const label = doc.createElement("span");
12989
13703
  label.className = "vela-select-label";
12990
13704
  label.textContent = this.ctrl.labelOf(this.ctrl.value);
12991
13705
  const chevron = doc.createElement("span");
12992
13706
  chevron.className = "vela-select-chevron";
12993
13707
  chevron.appendChild(iconEl("chevron-down", doc));
12994
- btn2.append(label, chevron);
12995
- wrap.appendChild(btn2);
13708
+ btn.append(label, chevron);
13709
+ wrap.appendChild(btn);
12996
13710
  if (this.ctrl.size === "sm") {
12997
13711
  const sizer = doc.createElement("div");
12998
13712
  sizer.className = "vela-select-sizer";
@@ -13004,16 +13718,16 @@ var Select = class {
13004
13718
  }
13005
13719
  wrap.appendChild(sizer);
13006
13720
  }
13007
- btn2.addEventListener("pointerdown", (e) => {
13721
+ btn.addEventListener("pointerdown", (e) => {
13008
13722
  e.preventDefault();
13009
- btn2.focus({ preventScroll: true });
13723
+ btn.focus({ preventScroll: true });
13010
13724
  });
13011
- btn2.addEventListener("click", (e) => {
13725
+ btn.addEventListener("click", (e) => {
13012
13726
  e.stopPropagation();
13013
13727
  this.toggle();
13014
13728
  });
13015
13729
  this.el = wrap;
13016
- this.trigger = btn2;
13730
+ this.trigger = btn;
13017
13731
  this.labelEl = label;
13018
13732
  }
13019
13733
  get value() {
@@ -13566,19 +14280,19 @@ var DatePicker = class {
13566
14280
  const doc = document;
13567
14281
  injectStyles(DATE_PICKER_STYLE_ID, DATE_PICKER_CSS, doc);
13568
14282
  this.ctrl = datePickerController(opts);
13569
- const button = (className) => {
14283
+ const button2 = (className) => {
13570
14284
  const b = doc.createElement("button");
13571
14285
  b.type = "button";
13572
14286
  b.className = className;
13573
14287
  return b;
13574
14288
  };
13575
- this.prev = button("vela-date-picker-nav");
14289
+ this.prev = button2("vela-date-picker-nav");
13576
14290
  this.prev.append(iconEl("chevron-left", doc));
13577
- this.next = button("vela-date-picker-nav");
14291
+ this.next = button2("vela-date-picker-nav");
13578
14292
  this.next.append(iconEl("chevron-right", doc));
13579
- this.monthBtn = button("vela-date-picker-switch");
14293
+ this.monthBtn = button2("vela-date-picker-switch");
13580
14294
  this.monthBtn.setAttribute("aria-label", "Choose month");
13581
- this.yearBtn = button("vela-date-picker-switch");
14295
+ this.yearBtn = button2("vela-date-picker-switch");
13582
14296
  this.yearBtn.setAttribute("aria-label", "Choose year");
13583
14297
  const title = doc.createElement("div");
13584
14298
  title.className = "vela-date-picker-title";
@@ -14314,8 +15028,8 @@ function buildColorPicker(color, theme, onChange, options = {}) {
14314
15028
  const grid = document.createElement("div");
14315
15029
  grid.style.cssText = "display:grid;grid-template-columns:repeat(10,1fr);gap:4px;";
14316
15030
  const swatches = [];
14317
- for (const row of PALETTE) {
14318
- for (const c of row) {
15031
+ for (const row2 of PALETTE) {
15032
+ for (const c of row2) {
14319
15033
  const sw = document.createElement("button");
14320
15034
  sw.type = "button";
14321
15035
  sw.dataset.c = c;
@@ -15049,8 +15763,8 @@ function buildFieldControl(desc) {
15049
15763
 
15050
15764
  // src/renderers/shared/IndicatorInputsDialog.ts
15051
15765
  var PROPS_TAB = "Properties";
15052
- function bagOf(row, decl) {
15053
- return decl.prop ? row.propValues : row.values;
15766
+ function bagOf(row2, decl) {
15767
+ return decl.prop ? row2.propValues : row2.values;
15054
15768
  }
15055
15769
  var SOURCES = ["close", "open", "high", "low", "hl2", "hlc3", "ohlc4", "volume"];
15056
15770
  var IndicatorInputsDialog = class {
@@ -15083,13 +15797,13 @@ var IndicatorInputsDialog = class {
15083
15797
  isOpen() {
15084
15798
  return this.dialog !== null;
15085
15799
  }
15086
- open(row) {
15800
+ open(row2) {
15087
15801
  this.close();
15088
- if (row.inputs.length === 0 && row.props.length === 0) return;
15089
- this.row = row;
15090
- this.openId = row.id;
15091
- this.snapshot = { ...row.values };
15092
- this.propSnapshot = { ...row.propValues };
15802
+ if (row2.inputs.length === 0 && row2.props.length === 0) return;
15803
+ this.row = row2;
15804
+ this.openId = row2.id;
15805
+ this.snapshot = { ...row2.values };
15806
+ this.propSnapshot = { ...row2.propValues };
15093
15807
  ensureDialogStyles();
15094
15808
  const t = this.host.theme();
15095
15809
  const border = "var(--vela-border)";
@@ -15097,7 +15811,7 @@ var IndicatorInputsDialog = class {
15097
15811
  const host = this.host.dialogHost() ?? this.host.container;
15098
15812
  const ui = new Dialog({
15099
15813
  host,
15100
- title: row.title,
15814
+ title: row2.title,
15101
15815
  // Non-modal: live-edit dialog — a modal machine locks pointer events on the
15102
15816
  // whole body, killing the chart and the body-portaled popovers.
15103
15817
  modal: false,
@@ -15128,9 +15842,9 @@ var IndicatorInputsDialog = class {
15128
15842
  theme: () => this.host.theme(),
15129
15843
  text: () => "Close"
15130
15844
  }));
15131
- const tabDefs = tabInputs(row.inputs);
15132
- if (row.props.length > 0) {
15133
- const propDecls = row.props.map((p) => {
15845
+ const tabDefs = tabInputs(row2.inputs);
15846
+ if (row2.props.length > 0) {
15847
+ const propDecls = row2.props.map((p) => {
15134
15848
  const d = { ...p, prop: true };
15135
15849
  delete d.tab;
15136
15850
  return d;
@@ -15164,7 +15878,7 @@ var IndicatorInputsDialog = class {
15164
15878
  tab.addEventListener("click", () => activate(idx));
15165
15879
  tabs.appendChild(tab);
15166
15880
  tabEls.push(tab);
15167
- bodies.push(this.buildTabBody(def.inputs, row, gates));
15881
+ bodies.push(this.buildTabBody(def.inputs, row2, gates));
15168
15882
  if (def.inputs.every((d) => d.when)) {
15169
15883
  gates.push({ el: tab, visible: (bag) => def.inputs.some((d) => inputVisible(d.when, bag)) });
15170
15884
  }
@@ -15173,7 +15887,7 @@ var IndicatorInputsDialog = class {
15173
15887
  for (const b of bodies) ui.body.appendChild(b);
15174
15888
  activate(0);
15175
15889
  const refresh = () => {
15176
- const bag = valuesBag(row);
15890
+ const bag = valuesBag(row2);
15177
15891
  for (const g of gates) g.el.style.display = g.visible(bag) ? "" : "none";
15178
15892
  if (tabEls[active]?.style.display === "none") {
15179
15893
  const next = tabEls.findIndex((el) => el.style.display !== "none");
@@ -15201,7 +15915,7 @@ var IndicatorInputsDialog = class {
15201
15915
  }
15202
15916
  /** One tab's scrollable body — one shared grid so every group's controls share a
15203
15917
  * left edge (a 100px number field lines up with a wider session/time pair). */
15204
- buildTabBody(inputs, row, gates) {
15918
+ buildTabBody(inputs, row2, gates) {
15205
15919
  const body = document.createElement("div");
15206
15920
  body.style.cssText = inputDialogBodyStyle(this.host.mobile());
15207
15921
  for (const group of groupInputs(inputs)) {
@@ -15213,7 +15927,7 @@ var IndicatorInputsDialog = class {
15213
15927
  section.appendChild(header);
15214
15928
  }
15215
15929
  for (const inputRow of group.rows) {
15216
- const el = this.buildInputRow(section, inputRow, row);
15930
+ const el = this.buildInputRow(section, inputRow, row2);
15217
15931
  if (inputRow.some((d) => d.when)) {
15218
15932
  gates.push({ el, visible: (bag) => rowVisible(inputRow, bag) });
15219
15933
  }
@@ -15246,26 +15960,26 @@ var IndicatorInputsDialog = class {
15246
15960
  }
15247
15961
  /** Restore every input and prop to its open-time value (re-running the indicator), then close. */
15248
15962
  revertAndClose() {
15249
- const row = this.row;
15963
+ const row2 = this.row;
15250
15964
  const snap = this.snapshot;
15251
- if (row && snap) {
15252
- for (const inp of row.inputs) {
15965
+ if (row2 && snap) {
15966
+ for (const inp of row2.inputs) {
15253
15967
  const snapped = snap[inp.key];
15254
15968
  const before = snapped !== void 0 ? snapped : inp.defval;
15255
- if (row.values[inp.key] !== before) {
15256
- row.values[inp.key] = before;
15257
- this.host.onChange?.({ indicatorId: row.id, key: inp.key, value: before });
15969
+ if (row2.values[inp.key] !== before) {
15970
+ row2.values[inp.key] = before;
15971
+ this.host.onChange?.({ indicatorId: row2.id, key: inp.key, value: before });
15258
15972
  }
15259
15973
  }
15260
15974
  }
15261
15975
  const propSnap = this.propSnapshot;
15262
- if (row && propSnap) {
15263
- for (const p of row.props) {
15976
+ if (row2 && propSnap) {
15977
+ for (const p of row2.props) {
15264
15978
  const snapped = propSnap[p.key];
15265
15979
  const before = snapped !== void 0 ? snapped : p.defval;
15266
- if (row.propValues[p.key] !== before) {
15267
- row.propValues[p.key] = before;
15268
- this.host.onChange?.({ indicatorId: row.id, key: p.key, value: before, kind: "prop" });
15980
+ if (row2.propValues[p.key] !== before) {
15981
+ row2.propValues[p.key] = before;
15982
+ this.host.onChange?.({ indicatorId: row2.id, key: p.key, value: before, kind: "prop" });
15269
15983
  }
15270
15984
  }
15271
15985
  }
@@ -15283,35 +15997,35 @@ var IndicatorInputsDialog = class {
15283
15997
  * rebuild-after-reset move as the chart-settings dialog. The open-time snapshot
15284
15998
  * survives the rebuild, so Cancel after a reset still reverts the whole session. */
15285
15999
  resetToDefaults() {
15286
- const row = this.row;
15287
- if (!row) return;
16000
+ const row2 = this.row;
16001
+ if (!row2) return;
15288
16002
  const snap = this.snapshot;
15289
16003
  const propSnap = this.propSnapshot;
15290
- for (const inp of row.inputs) {
15291
- if (row.values[inp.key] !== inp.defval) {
15292
- row.values[inp.key] = inp.defval;
15293
- this.host.onChange?.({ indicatorId: row.id, key: inp.key, value: inp.defval });
16004
+ for (const inp of row2.inputs) {
16005
+ if (row2.values[inp.key] !== inp.defval) {
16006
+ row2.values[inp.key] = inp.defval;
16007
+ this.host.onChange?.({ indicatorId: row2.id, key: inp.key, value: inp.defval });
15294
16008
  }
15295
16009
  }
15296
- for (const p of row.props) {
15297
- if (row.propValues[p.key] !== p.defval) {
15298
- row.propValues[p.key] = p.defval;
15299
- this.host.onChange?.({ indicatorId: row.id, key: p.key, value: p.defval, kind: "prop" });
16010
+ for (const p of row2.props) {
16011
+ if (row2.propValues[p.key] !== p.defval) {
16012
+ row2.propValues[p.key] = p.defval;
16013
+ this.host.onChange?.({ indicatorId: row2.id, key: p.key, value: p.defval, kind: "prop" });
15300
16014
  }
15301
16015
  }
15302
- this.open(row);
16016
+ this.open(row2);
15303
16017
  if (snap) this.snapshot = snap;
15304
16018
  if (propSnap) this.propSnapshot = propSnap;
15305
16019
  }
15306
16020
  /** Write one edit through: store it, notify the host, and re-apply the `when` gates. */
15307
- commit(row, decl, value) {
15308
- bagOf(row, decl)[decl.key] = value;
15309
- this.host.onChange?.({ indicatorId: row.id, key: decl.key, value, ...decl.prop ? { kind: "prop" } : {} });
16021
+ commit(row2, decl, value) {
16022
+ bagOf(row2, decl)[decl.key] = value;
16023
+ this.host.onChange?.({ indicatorId: row2.id, key: decl.key, value, ...decl.prop ? { kind: "prop" } : {} });
15310
16024
  this.refreshVisibility?.();
15311
16025
  }
15312
16026
  /** One settings row (or several `inline=` inputs) placed into a section's grid. */
15313
- buildInputRow(grid, decls, row) {
15314
- const idOf = (inp) => `vela-inp-${row.id}-${inp.key}`;
16027
+ buildInputRow(grid, decls, row2) {
16028
+ const idOf = (inp) => `vela-inp-${row2.id}-${inp.key}`;
15315
16029
  const append = (el) => {
15316
16030
  grid.appendChild(el);
15317
16031
  return el;
@@ -15322,7 +16036,7 @@ var IndicatorInputsDialog = class {
15322
16036
  inline: decls.map((d) => ({
15323
16037
  label: nameOf(d) || void 0,
15324
16038
  id: idOf(d),
15325
- control: this.buildControl(row, d, idOf(d)),
16039
+ control: this.buildControl(row2, d, idOf(d)),
15326
16040
  toggleFirst: d.type === "bool",
15327
16041
  fit: d.type === "color" || d.type === "bool"
15328
16042
  })),
@@ -15333,7 +16047,7 @@ var IndicatorInputsDialog = class {
15333
16047
  const id = idOf(lead);
15334
16048
  const info = lead.tooltip ? this.infoButton(lead.tooltip) : void 0;
15335
16049
  if (lead.type === "bool") {
15336
- const current = Boolean(bagOf(row, lead)[lead.key] ?? lead.defval);
16050
+ const current = Boolean(bagOf(row2, lead)[lead.key] ?? lead.defval);
15337
16051
  return append(fieldRow({
15338
16052
  label: nameOf(lead),
15339
16053
  id,
@@ -15342,7 +16056,7 @@ var IndicatorInputsDialog = class {
15342
16056
  toggle: {
15343
16057
  id,
15344
16058
  checked: current,
15345
- onChange: (v) => this.commit(row, lead, v)
16059
+ onChange: (v) => this.commit(row2, lead, v)
15346
16060
  }
15347
16061
  }));
15348
16062
  }
@@ -15352,7 +16066,7 @@ var IndicatorInputsDialog = class {
15352
16066
  id,
15353
16067
  stacked: true,
15354
16068
  info,
15355
- control: this.buildControl(row, lead, id)
16069
+ control: this.buildControl(row2, lead, id)
15356
16070
  }));
15357
16071
  }
15358
16072
  const fit = lead.type === "color" || lead.type === "session" || lead.type === "time";
@@ -15362,13 +16076,13 @@ var IndicatorInputsDialog = class {
15362
16076
  fit,
15363
16077
  controlWidth: fit ? void 0 : 100,
15364
16078
  info,
15365
- control: this.buildControl(row, lead, id)
16079
+ control: this.buildControl(row2, lead, id)
15366
16080
  }));
15367
16081
  }
15368
16082
  /** Build the typed control for one input, committing edits live via `onChange`. */
15369
- buildControl(row, inp, id) {
15370
- const current = bagOf(row, inp)[inp.key] ?? inp.defval;
15371
- const emit = (value) => this.commit(row, inp, value);
16083
+ buildControl(row2, inp, id) {
16084
+ const current = bagOf(row2, inp)[inp.key] ?? inp.defval;
16085
+ const emit = (value) => this.commit(row2, inp, value);
15372
16086
  if (inp.type === "bool") {
15373
16087
  return buildFieldControl({ kind: "switch", id, checked: Boolean(current), onChange: (v) => emit(v) }).el;
15374
16088
  }
@@ -15379,7 +16093,7 @@ var IndicatorInputsDialog = class {
15379
16093
  kind: "color",
15380
16094
  id,
15381
16095
  theme: this.host.theme(),
15382
- get: () => String(bagOf(row, inp)[inp.key] ?? inp.defval),
16096
+ get: () => String(bagOf(row2, inp)[inp.key] ?? inp.defval),
15383
16097
  onChange: (v) => emit(v),
15384
16098
  // An input change re-executes the script over its whole history: commit the
15385
16099
  // opacity drag once on release, not once per pointer move.
@@ -15664,21 +16378,21 @@ var IndicatorInputsDialog = class {
15664
16378
  const picker = this.host.symbolPicker();
15665
16379
  if (!picker) return this.buildTextField(id, current, emit);
15666
16380
  let value = current;
15667
- const btn2 = document.createElement("button");
15668
- btn2.type = "button";
15669
- btn2.id = id;
15670
- btn2.style.cssText = `${this.ctrlStyle()}display:flex;align-items:center;cursor:pointer;text-align:left;`;
16381
+ const btn = document.createElement("button");
16382
+ btn.type = "button";
16383
+ btn.id = id;
16384
+ btn.style.cssText = `${this.ctrlStyle()}display:flex;align-items:center;cursor:pointer;text-align:left;`;
15671
16385
  const label = document.createElement("span");
15672
16386
  label.textContent = value || "Select symbol\u2026";
15673
16387
  label.style.cssText = "overflow:hidden;text-overflow:ellipsis;white-space:nowrap;";
15674
- btn2.append(label);
15675
- btn2.addEventListener("click", () => picker(value, (picked) => {
16388
+ btn.append(label);
16389
+ btn.addEventListener("click", () => picker(value, (picked) => {
15676
16390
  if (!picked) return;
15677
16391
  value = picked;
15678
16392
  label.textContent = picked;
15679
16393
  emit(picked);
15680
16394
  }));
15681
- return btn2;
16395
+ return btn;
15682
16396
  }
15683
16397
  /** Shared field chrome for text / number / select controls (fills its wrapper's width). */
15684
16398
  ctrlStyle() {
@@ -15724,9 +16438,9 @@ function tabInputs(inputs) {
15724
16438
  function rowVisible(decls, bag) {
15725
16439
  return decls.some((d) => inputVisible(d.when, bag));
15726
16440
  }
15727
- function valuesBag(row) {
16441
+ function valuesBag(row2) {
15728
16442
  const bag = {};
15729
- for (const inp of row.inputs) bag[inp.key] = row.values[inp.key] ?? inp.defval;
16443
+ for (const inp of row2.inputs) bag[inp.key] = row2.values[inp.key] ?? inp.defval;
15730
16444
  return bag;
15731
16445
  }
15732
16446
  function groupInputs(inputs) {
@@ -15922,7 +16636,7 @@ var InputsUI = class {
15922
16636
  if (!this.selectedId) return;
15923
16637
  const target = e.target;
15924
16638
  if (target) {
15925
- for (const row of this.rows.values()) if (row.el.contains(target)) return;
16639
+ for (const row2 of this.rows.values()) if (row2.el.contains(target)) return;
15926
16640
  }
15927
16641
  this.clearSelection();
15928
16642
  };
@@ -16005,9 +16719,9 @@ var InputsUI = class {
16005
16719
  setTheme(theme) {
16006
16720
  this.theme = theme;
16007
16721
  for (const lg of this.legends.values()) applyChromeTokens(lg, theme);
16008
- for (const row of this.rows.values()) {
16009
- row.el.style.background = row.highlighted ? theme.background : this.idleRowFill();
16010
- row.el.style.color = theme.textColor;
16722
+ for (const row2 of this.rows.values()) {
16723
+ row2.el.style.background = row2.highlighted ? theme.background : this.idleRowFill();
16724
+ row2.el.style.color = theme.textColor;
16011
16725
  }
16012
16726
  if (this.foldToggle) this.syncFoldToggle();
16013
16727
  }
@@ -16026,26 +16740,26 @@ var InputsUI = class {
16026
16740
  */
16027
16741
  setLegendActions(provider) {
16028
16742
  this.legendActions = provider;
16029
- for (const row of this.rows.values()) this.renderExtras(row.id, row.extrasEl);
16743
+ for (const row2 of this.rows.values()) this.renderExtras(row2.id, row2.extrasEl);
16030
16744
  }
16031
16745
  /** (Re)build one row's contributed buttons. Same look, reveal and tooltips as the built-ins. */
16032
16746
  renderExtras(id, extrasEl) {
16033
16747
  this.disposeTips(this.extrasTips, id);
16034
16748
  extrasEl.replaceChildren();
16035
16749
  for (const action of this.legendActions?.(id) ?? []) {
16036
- const btn2 = document.createElement("button");
16037
- btn2.type = "button";
16038
- btn2.setAttribute("aria-label", action.tooltip);
16039
- this.tip(this.extrasTips, id, btn2, action.tooltip);
16040
- btn2.innerHTML = iconAt(action.icon, LEGEND_ICON_PX2);
16041
- btn2.className = "vela-ind-ctl";
16042
- btn2.dataset.legendAction = action.id;
16043
- btn2.style.cssText = LEGEND_CTL_CSS;
16044
- btn2.addEventListener("click", (e) => {
16750
+ const btn = document.createElement("button");
16751
+ btn.type = "button";
16752
+ btn.setAttribute("aria-label", action.tooltip);
16753
+ this.tip(this.extrasTips, id, btn, action.tooltip);
16754
+ btn.innerHTML = iconAt(action.icon, LEGEND_ICON_PX2);
16755
+ btn.className = "vela-ind-ctl";
16756
+ btn.dataset.legendAction = action.id;
16757
+ btn.style.cssText = LEGEND_CTL_CSS;
16758
+ btn.addEventListener("click", (e) => {
16045
16759
  e.stopPropagation();
16046
16760
  action.run();
16047
16761
  });
16048
- extrasEl.appendChild(btn2);
16762
+ extrasEl.appendChild(btn);
16049
16763
  }
16050
16764
  }
16051
16765
  /**
@@ -16055,17 +16769,17 @@ var InputsUI = class {
16055
16769
  */
16056
16770
  setLegendCallouts(provider) {
16057
16771
  this.legendCallouts = provider;
16058
- for (const row of this.rows.values()) this.renderCallouts(row);
16772
+ for (const row2 of this.rows.values()) this.renderCallouts(row2);
16059
16773
  }
16060
16774
  /** (Re)build one row's callout bubbles — tinted icon circles beside the title whose
16061
16775
  * click (when the view carries content) deploys a panel of text and actions. */
16062
- renderCallouts(row) {
16063
- this.disposeTips(this.calloutTips, row.id);
16064
- for (const bubble of row.callouts) bubble.destroy();
16065
- row.callouts = [];
16066
- row.calloutsEl.replaceChildren();
16067
- const views = this.legendCallouts?.(row.id) ?? [];
16068
- row.calloutsEl.style.display = legendCalloutsDisplay(row.highlighted, views.length > 0);
16776
+ renderCallouts(row2) {
16777
+ this.disposeTips(this.calloutTips, row2.id);
16778
+ for (const bubble of row2.callouts) bubble.destroy();
16779
+ row2.callouts = [];
16780
+ row2.calloutsEl.replaceChildren();
16781
+ const views = this.legendCallouts?.(row2.id) ?? [];
16782
+ row2.calloutsEl.style.display = legendCalloutsDisplay(row2.highlighted, views.length > 0);
16069
16783
  for (const view of views) {
16070
16784
  const bubble = new CalloutBubble({
16071
16785
  icon: view.icon,
@@ -16079,9 +16793,9 @@ var InputsUI = class {
16079
16793
  theme: () => this.theme
16080
16794
  });
16081
16795
  bubble.el.dataset.legendCallout = view.id;
16082
- this.tip(this.calloutTips, row.id, bubble.el, view.tooltip);
16083
- row.callouts.push(bubble);
16084
- row.calloutsEl.appendChild(bubble.el);
16796
+ this.tip(this.calloutTips, row2.id, bubble.el, view.tooltip);
16797
+ row2.callouts.push(bubble);
16798
+ row2.calloutsEl.appendChild(bubble.el);
16085
16799
  }
16086
16800
  }
16087
16801
  /** Reposition the per-pane legend containers after a layout change. */
@@ -16101,9 +16815,9 @@ var InputsUI = class {
16101
16815
  */
16102
16816
  setValuesVisible(visible) {
16103
16817
  this.valuesVisible = visible;
16104
- for (const row of this.rows.values()) {
16105
- row.showValues = null;
16106
- this.renderRowValues(row);
16818
+ for (const row2 of this.rows.values()) {
16819
+ row2.showValues = null;
16820
+ this.renderRowValues(row2);
16107
16821
  }
16108
16822
  }
16109
16823
  /**
@@ -16112,35 +16826,35 @@ var InputsUI = class {
16112
16826
  * unchanged frames: each row diffs against its last rendered values before touching DOM.
16113
16827
  */
16114
16828
  setPlotValues(values) {
16115
- for (const row of this.rows.values()) {
16116
- const next = values.get(row.id) ?? [];
16829
+ for (const row2 of this.rows.values()) {
16830
+ const next = values.get(row2.id) ?? [];
16117
16831
  const key = next.map((v) => `${v.value}|${v.color}`).join("\0");
16118
- if (key === row.plotValuesKey) continue;
16119
- row.plotValues = next;
16120
- row.plotValuesKey = key;
16121
- this.renderRowValues(row);
16832
+ if (key === row2.plotValuesKey) continue;
16833
+ row2.plotValues = next;
16834
+ row2.plotValuesKey = key;
16835
+ this.renderRowValues(row2);
16122
16836
  }
16123
16837
  }
16124
16838
  /** Whether a row currently shows its values: its own override, else the chart-wide flag. */
16125
- rowValuesShown(row) {
16126
- return row.showValues ?? this.valuesVisible;
16839
+ rowValuesShown(row2) {
16840
+ return row2.showValues ?? this.valuesVisible;
16127
16841
  }
16128
16842
  /** (Re)build one row's values readout from its stored plot values + visibility. A
16129
16843
  * highlighted (hovered/selected) row shows only the title + controls — no values. */
16130
- renderRowValues(row) {
16131
- if (!this.rowValuesShown(row) || row.plotValues.length === 0 || row.highlighted) {
16132
- row.valuesEl.style.display = "none";
16133
- row.valuesEl.replaceChildren();
16844
+ renderRowValues(row2) {
16845
+ if (!this.rowValuesShown(row2) || row2.plotValues.length === 0 || row2.highlighted) {
16846
+ row2.valuesEl.style.display = "none";
16847
+ row2.valuesEl.replaceChildren();
16134
16848
  return;
16135
16849
  }
16136
- row.valuesEl.style.display = "block";
16137
- row.valuesEl.replaceChildren();
16138
- row.plotValues.forEach((v, i) => {
16850
+ row2.valuesEl.style.display = "block";
16851
+ row2.valuesEl.replaceChildren();
16852
+ row2.plotValues.forEach((v, i) => {
16139
16853
  const span = document.createElement("span");
16140
16854
  span.textContent = v.value;
16141
16855
  span.style.color = v.color;
16142
16856
  if (i > 0) span.style.marginLeft = `${LEGEND_VALUES_GAP_PX}px`;
16143
- row.valuesEl.appendChild(span);
16857
+ row2.valuesEl.appendChild(span);
16144
16858
  });
16145
16859
  }
16146
16860
  // ── legend move/merge (menu + drag) ─────────────────────────────────────
@@ -16149,8 +16863,8 @@ var InputsUI = class {
16149
16863
  this.closeMoveMenu();
16150
16864
  const api = this.moveApi;
16151
16865
  if (!api) return;
16152
- const row = this.rows.get(id);
16153
- const currentPane = row?.paneId ?? "price";
16866
+ const row2 = this.rows.get(id);
16867
+ const currentPane = row2?.paneId ?? "price";
16154
16868
  const panes = api.panes();
16155
16869
  const items = [];
16156
16870
  for (const p of panes) {
@@ -16190,13 +16904,13 @@ var InputsUI = class {
16190
16904
  openRowMenu(id, x, y) {
16191
16905
  this.closeRowMenu();
16192
16906
  this.closeMoveMenu();
16193
- const row = this.rows.get(id);
16194
- if (!row) return;
16907
+ const row2 = this.rows.get(id);
16908
+ if (!row2) return;
16195
16909
  this.rowMenuId = id;
16196
16910
  const items = [{
16197
16911
  id: "values",
16198
16912
  label: "Indicator values",
16199
- checked: this.rowValuesShown(row)
16913
+ checked: this.rowValuesShown(row2)
16200
16914
  }];
16201
16915
  if (!this.rowMenu) {
16202
16916
  this.rowMenu = new Menu({
@@ -16223,11 +16937,11 @@ var InputsUI = class {
16223
16937
  }
16224
16938
  /** Move a legend row to another pane (indicator merged/moved), tidying an emptied container. */
16225
16939
  setPane(id, paneId) {
16226
- const row = this.rows.get(id);
16227
- if (!row || row.paneId === paneId) return;
16228
- const prev2 = row.paneId;
16229
- row.paneId = paneId;
16230
- this.attach(this.legendFor(paneId), row.el, row.native);
16940
+ const row2 = this.rows.get(id);
16941
+ if (!row2 || row2.paneId === paneId) return;
16942
+ const prev2 = row2.paneId;
16943
+ row2.paneId = paneId;
16944
+ this.attach(this.legendFor(paneId), row2.el, row2.native);
16231
16945
  this.syncFoldToggle();
16232
16946
  if (prev2 !== "price") {
16233
16947
  const lg = this.legends.get(prev2);
@@ -16257,9 +16971,9 @@ var InputsUI = class {
16257
16971
  lg.style.maxWidth = `calc(100% - ${(bounds.rightAxis ?? 0) + LEGEND_LEFT_PX + LEGEND_SCALE_GAP_PX}px)`;
16258
16972
  const collapsed = this.paneCollapse.has(paneId);
16259
16973
  const masterId = this.paneCollapse.get(paneId) ?? null;
16260
- for (const row of this.rows.values()) {
16261
- if (row.paneId !== paneId) continue;
16262
- row.el.style.display = (collapsed ? row.id !== masterId : this.legendFolded) ? "none" : "flex";
16974
+ for (const row2 of this.rows.values()) {
16975
+ if (row2.paneId !== paneId) continue;
16976
+ row2.el.style.display = (collapsed ? row2.id !== masterId : this.legendFolded) ? "none" : "flex";
16263
16977
  }
16264
16978
  let top = bounds.top + 8;
16265
16979
  if (collapsed && Number.isFinite(bounds.height)) {
@@ -16293,13 +17007,13 @@ var InputsUI = class {
16293
17007
  if (wasFolded && count > 0) this.reposition();
16294
17008
  return;
16295
17009
  }
16296
- let btn2 = this.foldToggle;
16297
- if (!btn2) {
16298
- btn2 = document.createElement("button");
16299
- btn2.type = "button";
16300
- btn2.className = "vela-ind-fold";
16301
- this.tip(this.rowTips, "fold", btn2, () => this.overviewAction !== null ? "Indicators" : this.legendFolded ? "Show indicator legend" : "Hide indicator legend");
16302
- btn2.addEventListener("click", () => {
17010
+ let btn = this.foldToggle;
17011
+ if (!btn) {
17012
+ btn = document.createElement("button");
17013
+ btn.type = "button";
17014
+ btn.className = "vela-ind-fold";
17015
+ this.tip(this.rowTips, "fold", btn, () => this.overviewAction !== null ? "Indicators" : this.legendFolded ? "Show indicator legend" : "Hide indicator legend");
17016
+ btn.addEventListener("click", () => {
16303
17017
  if (this.overviewAction !== null) {
16304
17018
  this.overviewAction();
16305
17019
  return;
@@ -16307,24 +17021,24 @@ var InputsUI = class {
16307
17021
  this.legendFolded = !this.legendFolded;
16308
17022
  this.syncFoldToggle();
16309
17023
  });
16310
- this.foldToggle = btn2;
17024
+ this.foldToggle = btn;
16311
17025
  }
16312
17026
  const folded = this.legendFolded;
16313
- btn2.setAttribute("aria-label", this.overviewAction !== null ? "Indicators" : folded ? "Show indicator legend" : "Hide indicator legend");
17027
+ btn.setAttribute("aria-label", this.overviewAction !== null ? "Indicators" : folded ? "Show indicator legend" : "Hide indicator legend");
16314
17028
  const ink = this.theme.textColor;
16315
- 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;`;
16316
- btn2.replaceChildren();
17029
+ 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;`;
17030
+ btn.replaceChildren();
16317
17031
  const icon2 = document.createElement("span");
16318
17032
  icon2.style.cssText = "display:inline-flex;align-items:center;line-height:0;opacity:0.7;";
16319
17033
  icon2.innerHTML = this.overviewAction !== null ? OVERVIEW_SVG : folded ? UNFOLD_SVG : FOLD_SVG;
16320
- btn2.appendChild(icon2);
17034
+ btn.appendChild(icon2);
16321
17035
  if (folded) {
16322
17036
  const label = document.createElement("span");
16323
17037
  label.textContent = String(count);
16324
17038
  label.style.cssText = `font-weight:600;line-height:normal;font-size:12px;color:${ink};`;
16325
- btn2.appendChild(label);
17039
+ btn.appendChild(label);
16326
17040
  }
16327
- this.legendFor("price").appendChild(btn2);
17041
+ this.legendFor("price").appendChild(btn);
16328
17042
  this.reposition();
16329
17043
  }
16330
17044
  /** Create or update an indicator's legend row (in the legend for its pane). */
@@ -16406,8 +17120,8 @@ var InputsUI = class {
16406
17120
  eye.className = "vela-ind-ctl";
16407
17121
  eye.style.cssText = LEGEND_CTL_CSS;
16408
17122
  eye.addEventListener("click", () => {
16409
- const row2 = this.rows.get(id);
16410
- this.onToggleVisible?.(id, Boolean(row2?.hidden));
17123
+ const row3 = this.rows.get(id);
17124
+ this.onToggleVisible?.(id, Boolean(row3?.hidden));
16411
17125
  });
16412
17126
  controlsEl.appendChild(eye);
16413
17127
  eyeEl = eye;
@@ -16452,9 +17166,9 @@ var InputsUI = class {
16452
17166
  controlsEl.appendChild(close);
16453
17167
  el.appendChild(controlsEl);
16454
17168
  this.attach(this.legendFor(paneId), el, !!opts.native);
16455
- 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 };
16456
- this.rows.set(id, row);
16457
- this.renderCallouts(row);
17169
+ 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 };
17170
+ this.rows.set(id, row2);
17171
+ this.renderCallouts(row2);
16458
17172
  this.syncFoldToggle();
16459
17173
  }
16460
17174
  /** Place a row in its pane's legend — native rows PREPEND (pinned to the top), Pine rows append. */
@@ -16464,10 +17178,10 @@ var InputsUI = class {
16464
17178
  }
16465
17179
  /** Reflect programmatic input changes (so a re-opened dialog shows current values). */
16466
17180
  setValues(id, values, props) {
16467
- const row = this.rows.get(id);
16468
- if (!row) return;
16469
- row.values = { ...row.values, ...values };
16470
- if (props) row.propValues = { ...row.propValues, ...props };
17181
+ const row2 = this.rows.get(id);
17182
+ if (!row2) return;
17183
+ row2.values = { ...row2.values, ...values };
17184
+ if (props) row2.propValues = { ...row2.propValues, ...props };
16471
17185
  }
16472
17186
  /**
16473
17187
  * Reflect an indicator's live status in its legend row: `'loading'` shows three pulsing
@@ -16475,9 +17189,9 @@ var InputsUI = class {
16475
17189
  * at legend scale), `'live'` a pulsing dot, `'idle'` nothing. Rendered just right of the title.
16476
17190
  */
16477
17191
  setStatus(id, status) {
16478
- const row = this.rows.get(id);
16479
- if (!row) return;
16480
- const el = row.statusEl;
17192
+ const row2 = this.rows.get(id);
17193
+ if (!row2) return;
17194
+ const el = row2.statusEl;
16481
17195
  el.replaceChildren();
16482
17196
  if (status === "idle") {
16483
17197
  el.style.cssText = "display:none;box-sizing:border-box;flex:none;";
@@ -16506,15 +17220,15 @@ var InputsUI = class {
16506
17220
  }
16507
17221
  /** Mark a row hidden/shown — swaps the eye glyph and dims the row; does NOT remove it. */
16508
17222
  setVisible(id, visible) {
16509
- const row = this.rows.get(id);
16510
- if (!row) return;
16511
- row.hidden = !visible;
16512
- row.el.style.opacity = visible ? "1" : "0.5";
16513
- if (row.eyeEl) {
16514
- row.eyeEl.innerHTML = visible ? EYE_SVG : EYE_OFF_SVG;
16515
- row.eyeEl.setAttribute("aria-label", visible ? "Hide" : "Show");
17223
+ const row2 = this.rows.get(id);
17224
+ if (!row2) return;
17225
+ row2.hidden = !visible;
17226
+ row2.el.style.opacity = visible ? "1" : "0.5";
17227
+ if (row2.eyeEl) {
17228
+ row2.eyeEl.innerHTML = visible ? EYE_SVG : EYE_OFF_SVG;
17229
+ row2.eyeEl.setAttribute("aria-label", visible ? "Hide" : "Show");
16516
17230
  }
16517
- this.syncRowActions(row);
17231
+ this.syncRowActions(row2);
16518
17232
  }
16519
17233
  /**
16520
17234
  * Select a legend row (outlining it) and clear any previous selection, so at most one
@@ -16533,32 +17247,32 @@ var InputsUI = class {
16533
17247
  * visible even when idle, so it can be un-hidden without hovering.
16534
17248
  */
16535
17249
  setRowHighlighted(id, highlighted) {
16536
- const row = this.rows.get(id);
16537
- if (!row) return;
16538
- row.highlighted = highlighted;
16539
- this.renderRowValues(row);
16540
- row.el.style.boxShadow = highlighted ? `inset 0 0 0 1px ${this.neutralBorder()}` : "none";
16541
- row.el.style.background = highlighted ? this.theme.background : this.idleRowFill();
16542
- this.syncRowActions(row);
17250
+ const row2 = this.rows.get(id);
17251
+ if (!row2) return;
17252
+ row2.highlighted = highlighted;
17253
+ this.renderRowValues(row2);
17254
+ row2.el.style.boxShadow = highlighted ? `inset 0 0 0 1px ${this.neutralBorder()}` : "none";
17255
+ row2.el.style.background = highlighted ? this.theme.background : this.idleRowFill();
17256
+ this.syncRowActions(row2);
16543
17257
  }
16544
17258
  /**
16545
17259
  * Reveal the action cluster (or just the eye for a hidden indicator). Non-eye
16546
17260
  * buttons only appear while highlighted; the eye keeps its inline-flex from
16547
17261
  * construction and is shown/hidden with the cluster container.
16548
17262
  */
16549
- syncRowActions(row) {
16550
- const open2 = row.highlighted;
16551
- row.controlsEl.style.display = open2 || row.hidden ? "inline-flex" : "none";
17263
+ syncRowActions(row2) {
17264
+ const open2 = row2.highlighted;
17265
+ row2.controlsEl.style.display = open2 || row2.hidden ? "inline-flex" : "none";
16552
17266
  if (open2) {
16553
- row.el.appendChild(row.statusEl);
16554
- for (const bubble of row.callouts) bubble.hidePanel();
17267
+ row2.el.appendChild(row2.statusEl);
17268
+ for (const bubble of row2.callouts) bubble.hidePanel();
16555
17269
  } else {
16556
- row.el.insertBefore(row.statusEl, row.valuesEl);
17270
+ row2.el.insertBefore(row2.statusEl, row2.valuesEl);
16557
17271
  }
16558
- row.calloutsEl.style.display = legendCalloutsDisplay(open2, row.callouts.length > 0);
16559
- for (const child of Array.from(row.controlsEl.children)) {
16560
- if (!(child instanceof HTMLElement) || child === row.eyeEl) continue;
16561
- if (child === row.extrasEl) {
17272
+ row2.calloutsEl.style.display = legendCalloutsDisplay(open2, row2.callouts.length > 0);
17273
+ for (const child of Array.from(row2.controlsEl.children)) {
17274
+ if (!(child instanceof HTMLElement) || child === row2.eyeEl) continue;
17275
+ if (child === row2.extrasEl) {
16562
17276
  child.style.display = open2 ? "contents" : "none";
16563
17277
  continue;
16564
17278
  }
@@ -16572,9 +17286,9 @@ var InputsUI = class {
16572
17286
  this.selectedId = null;
16573
17287
  }
16574
17288
  remove(id) {
16575
- const row = this.rows.get(id);
16576
- for (const bubble of row?.callouts ?? []) bubble.destroy();
16577
- row?.el.remove();
17289
+ const row2 = this.rows.get(id);
17290
+ for (const bubble of row2?.callouts ?? []) bubble.destroy();
17291
+ row2?.el.remove();
16578
17292
  this.rows.delete(id);
16579
17293
  this.disposeTips(this.rowTips, id);
16580
17294
  this.disposeTips(this.extrasTips, id);
@@ -16582,11 +17296,11 @@ var InputsUI = class {
16582
17296
  if (this.selectedId === id) this.selectedId = null;
16583
17297
  if (this.inputsDialog.openId === id) this.inputsDialog.close();
16584
17298
  this.syncFoldToggle();
16585
- if (row && row.paneId !== "price") {
16586
- const lg = this.legends.get(row.paneId);
17299
+ if (row2 && row2.paneId !== "price") {
17300
+ const lg = this.legends.get(row2.paneId);
16587
17301
  if (lg && lg.childElementCount === 0) {
16588
17302
  lg.remove();
16589
- this.legends.delete(row.paneId);
17303
+ this.legends.delete(row2.paneId);
16590
17304
  }
16591
17305
  }
16592
17306
  }
@@ -16599,7 +17313,7 @@ var InputsUI = class {
16599
17313
  for (const id of [...this.rowTips.keys()]) this.disposeTips(this.rowTips, id);
16600
17314
  for (const id of [...this.extrasTips.keys()]) this.disposeTips(this.extrasTips, id);
16601
17315
  for (const id of [...this.calloutTips.keys()]) this.disposeTips(this.calloutTips, id);
16602
- for (const row of this.rows.values()) for (const bubble of row.callouts) bubble.destroy();
17316
+ for (const row2 of this.rows.values()) for (const bubble of row2.callouts) bubble.destroy();
16603
17317
  for (const lg of this.legends.values()) lg.remove();
16604
17318
  this.legends.clear();
16605
17319
  this.rows.clear();
@@ -16620,8 +17334,8 @@ var InputsUI = class {
16620
17334
  this.inputsDialog.close();
16621
17335
  }
16622
17336
  openDialog(id) {
16623
- const row = this.rows.get(id);
16624
- if (row) this.inputsDialog.open(row);
17337
+ const row2 = this.rows.get(id);
17338
+ if (row2) this.inputsDialog.open(row2);
16625
17339
  }
16626
17340
  /** Idle legend-row fill — a translucent wash of the chart background, so the title keeps
16627
17341
  * contrast when candles reach it without laying a solid block over the plot. */
@@ -16793,9 +17507,9 @@ var PaneControls = class {
16793
17507
  const soloChip = stateChipRole != null && !hovered;
16794
17508
  cluster.style.background = soloChip ? "transparent" : CLUSTER_PILL;
16795
17509
  for (const child of cluster.children) {
16796
- const btn2 = child;
16797
- btn2.style.display = "inline-flex";
16798
- btn2.style.visibility = soloChip && btn2.dataset.role !== stateChipRole ? "hidden" : "visible";
17510
+ const btn = child;
17511
+ btn.style.display = "inline-flex";
17512
+ btn.style.visibility = soloChip && btn.dataset.role !== stateChipRole ? "hidden" : "visible";
16799
17513
  }
16800
17514
  }
16801
17515
  }
@@ -17250,28 +17964,28 @@ function factoryResetConfig(factory, priceStyle = factory.series.style) {
17250
17964
  for (const t of chartTypes()) {
17251
17965
  const section = t.settings;
17252
17966
  if (!section) continue;
17253
- const defaults2 = {};
17967
+ const defaults5 = {};
17254
17968
  const addRows = (rows) => {
17255
17969
  for (const r of rows ?? []) {
17256
- for (const k of settingsRowValueKeys(r)) defaults2[k.key] = k.defval;
17970
+ for (const k of settingsRowValueKeys(r)) defaults5[k.key] = k.defval;
17257
17971
  }
17258
17972
  };
17259
17973
  for (const inst of section.instances ?? []) {
17260
- if (inst.enableKey) defaults2[inst.enableKey] = false;
17974
+ if (inst.enableKey) defaults5[inst.enableKey] = false;
17261
17975
  addRows(inst.rows);
17262
17976
  }
17263
17977
  for (const sub2 of section.subsections ?? []) {
17264
- if (sub2.enableKey) defaults2[sub2.enableKey] = false;
17978
+ if (sub2.enableKey) defaults5[sub2.enableKey] = false;
17265
17979
  addRows(sub2.rows);
17266
17980
  }
17267
17981
  if (!section.instances) addRows(section.rows);
17268
- bag[t.id] = defaults2;
17982
+ bag[t.id] = defaults5;
17269
17983
  }
17270
17984
  for (const t of chartTypes()) {
17271
17985
  if (!hasOwnCandlePaint(t.id)) continue;
17272
- const defaults2 = bag[t.id] ?? {};
17273
- for (const key of CANDLE_OVERRIDE_KEYS) defaults2[key] = null;
17274
- bag[t.id] = defaults2;
17986
+ const defaults5 = bag[t.id] ?? {};
17987
+ for (const key of CANDLE_OVERRIDE_KEYS) defaults5[key] = null;
17988
+ bag[t.id] = defaults5;
17275
17989
  }
17276
17990
  for (const [typeId, vals] of Object.entries(factory.chartTypes)) {
17277
17991
  bag[typeId] = { ...bag[typeId] ?? {}, ...vals };
@@ -21548,28 +22262,28 @@ function formatPriceLabel(scale, heightPx, value, mintick) {
21548
22262
  return value.toFixed(axisDecimals(scale, heightPx, mintick));
21549
22263
  }
21550
22264
  var SEC = 1e3;
21551
- var MIN = 60 * SEC;
21552
- var HOUR = 60 * MIN;
21553
- var DAY = 24 * HOUR;
21554
- var WEEK = 7 * DAY;
21555
- var MONTH = 30 * DAY;
21556
- var YEAR = 365 * DAY;
22265
+ var MIN2 = 60 * SEC;
22266
+ var HOUR2 = 60 * MIN2;
22267
+ var DAY2 = 24 * HOUR2;
22268
+ var WEEK = 7 * DAY2;
22269
+ var MONTH = 30 * DAY2;
22270
+ var YEAR = 365 * DAY2;
21557
22271
  var STEP_LADDER = [
21558
22272
  SEC,
21559
22273
  5 * SEC,
21560
22274
  15 * SEC,
21561
22275
  30 * SEC,
21562
- MIN,
21563
- 5 * MIN,
21564
- 15 * MIN,
21565
- 30 * MIN,
21566
- HOUR,
21567
- 2 * HOUR,
21568
- 4 * HOUR,
21569
- 6 * HOUR,
21570
- 12 * HOUR,
21571
- DAY,
21572
- 2 * DAY,
22276
+ MIN2,
22277
+ 5 * MIN2,
22278
+ 15 * MIN2,
22279
+ 30 * MIN2,
22280
+ HOUR2,
22281
+ 2 * HOUR2,
22282
+ 4 * HOUR2,
22283
+ 6 * HOUR2,
22284
+ 12 * HOUR2,
22285
+ DAY2,
22286
+ 2 * DAY2,
21573
22287
  WEEK,
21574
22288
  MONTH,
21575
22289
  3 * MONTH,
@@ -21581,7 +22295,7 @@ var pad2 = (n) => n < 10 ? `0${n}` : String(n);
21581
22295
  function formatTimeStamp(ms, timeZone, barIntervalMs) {
21582
22296
  const d = zonedDate(ms, timeZone);
21583
22297
  const date = `${WEEKDAYS[d.getUTCDay()]} ${d.getUTCDate()} ${MONTHS[d.getUTCMonth()]} '${pad2(d.getUTCFullYear() % 100)}`;
21584
- if (barIntervalMs >= DAY) return date;
22298
+ if (barIntervalMs >= DAY2) return date;
21585
22299
  return `${date} ${pad2(d.getUTCHours())}:${pad2(d.getUTCMinutes())}`;
21586
22300
  }
21587
22301
  function pickStep(targetMs) {
@@ -21602,7 +22316,7 @@ function timeTicks(fromMs, toMs, target = 8, offsetMs = 0) {
21602
22316
  const d = new Date(zt);
21603
22317
  let label;
21604
22318
  let major = false;
21605
- if (step < DAY) {
22319
+ if (step < DAY2) {
21606
22320
  const h = d.getUTCHours();
21607
22321
  const m = d.getUTCMinutes();
21608
22322
  if (h === 0 && m === 0) {
@@ -21670,8 +22384,8 @@ var PriceAxisTickSource = class {
21670
22384
  if (m && m.kind === pane.kind && m.min === s.min && m.max === s.max && m.log === !!s.log && m.invert === !!s.invert && m.height === height && m.baseline === pct?.baseline && m.indexed === !!pct?.indexed && m.mintick === scene.priceMintick && m.format === format && m.fontSize === fontSize) {
21671
22385
  return m.ticks;
21672
22386
  }
21673
- const defaults2 = paneAxisTicks(s, height, pct, scene.priceMintick, format);
21674
- const ticks = this.callHook(fn, scene, pane, coords, defaults2, pct?.baseline, pct ? pct.indexed ? "indexed" : "percent" : "price");
22387
+ const defaults5 = paneAxisTicks(s, height, pct, scene.priceMintick, format);
22388
+ const ticks = this.callHook(fn, scene, pane, coords, defaults5, pct?.baseline, pct ? pct.indexed ? "indexed" : "percent" : "price");
21675
22389
  this.memo.set(pane, {
21676
22390
  kind: pane.kind,
21677
22391
  min: s.min,
@@ -21688,7 +22402,7 @@ var PriceAxisTickSource = class {
21688
22402
  });
21689
22403
  return ticks;
21690
22404
  }
21691
- callHook(fn, scene, pane, coords, defaults2, baseline, mode) {
22405
+ callHook(fn, scene, pane, coords, defaults5, baseline, mode) {
21692
22406
  const scale = { min: pane.scale.min, max: pane.scale.max, log: !!pane.scale.log, invert: !!pane.scale.invert };
21693
22407
  const bounds = { top: 0, height: pane.bounds.height };
21694
22408
  const ctx = {
@@ -21702,19 +22416,19 @@ var PriceAxisTickSource = class {
21702
22416
  mintick: scene.priceMintick,
21703
22417
  fontSize: scene.style.fontSize,
21704
22418
  priceToY: (price) => coords.priceToY(price, scale, bounds),
21705
- defaults: defaults2
22419
+ defaults: defaults5
21706
22420
  };
21707
22421
  let out;
21708
22422
  try {
21709
22423
  out = fn(ctx);
21710
22424
  } catch (err) {
21711
22425
  this.warnOnce("threw", err);
21712
- return defaults2;
22426
+ return defaults5;
21713
22427
  }
21714
- if (out == null) return defaults2;
22428
+ if (out == null) return defaults5;
21715
22429
  if (!Array.isArray(out)) {
21716
22430
  this.warnOnce("returned a non-array", out);
21717
- return defaults2;
22431
+ return defaults5;
21718
22432
  }
21719
22433
  const ticks = [];
21720
22434
  for (const t of out) {
@@ -21773,12 +22487,12 @@ var MARK_DECK_STEP = 3;
21773
22487
  var MARK_FAN_GAP = 4;
21774
22488
  var MARK_HIT_PAD = 3;
21775
22489
  var MARK_FAN_HOLD = 8;
21776
- function snapMarkBar(time, barTimes, intervalMs2) {
22490
+ function snapMarkBar(time, barTimes, intervalMs) {
21777
22491
  const n = barTimes.length;
21778
- if (n === 0 || !(intervalMs2 > 0) || !Number.isFinite(time)) return null;
22492
+ if (n === 0 || !(intervalMs > 0) || !Number.isFinite(time)) return null;
21779
22493
  if (time < barTimes[0]) return null;
21780
22494
  const last = barTimes[n - 1];
21781
- if (time >= last + intervalMs2) return n - 1 + Math.floor((time - last) / intervalMs2);
22495
+ if (time >= last + intervalMs) return n - 1 + Math.floor((time - last) / intervalMs);
21782
22496
  let lo = 0;
21783
22497
  let hi = n - 1;
21784
22498
  while (lo < hi) {
@@ -21786,14 +22500,14 @@ function snapMarkBar(time, barTimes, intervalMs2) {
21786
22500
  if (barTimes[mid] <= time) lo = mid;
21787
22501
  else hi = mid - 1;
21788
22502
  }
21789
- if (time < barTimes[lo] + intervalMs2) return lo;
22503
+ if (time < barTimes[lo] + intervalMs) return lo;
21790
22504
  return lo + 1;
21791
22505
  }
21792
- function clusterMarks(marks, barTimes, intervalMs2, hidden) {
22506
+ function clusterMarks(marks, barTimes, intervalMs, hidden) {
21793
22507
  const byKey = /* @__PURE__ */ new Map();
21794
22508
  marks.forEach((m, seq) => {
21795
22509
  if (m.group !== void 0 && hidden(m.group)) return;
21796
- const bar = snapMarkBar(m.time, barTimes, intervalMs2);
22510
+ const bar = snapMarkBar(m.time, barTimes, intervalMs);
21797
22511
  if (bar === null) return;
21798
22512
  const key = `${bar}|${m.group ?? ""}`;
21799
22513
  let c = byKey.get(key);
@@ -23477,16 +24191,16 @@ var SettingsDialog = class {
23477
24191
  }
23478
24192
  };
23479
24193
  for (const { group: g, depth } of rows) {
23480
- const row = this.boolRow(g.label, this.markGroupOwnVisible(g.id), (v) => {
24194
+ const row2 = this.boolRow(g.label, this.markGroupOwnVisible(g.id), (v) => {
23481
24195
  this.emit({ marks: { groups: { [g.id]: v } } });
23482
24196
  refreshDimming();
23483
24197
  });
23484
24198
  if (depth > 0) {
23485
- row.classList.add("vela-sd-nested");
23486
- row.style.setProperty("--vela-sd-depth", String(depth));
24199
+ row2.classList.add("vela-sd-nested");
24200
+ row2.style.setProperty("--vela-sd-depth", String(depth));
23487
24201
  }
23488
- rowsById.set(g.id, row);
23489
- body.append(sid(row, markGroupSettingsId(g.id)));
24202
+ rowsById.set(g.id, row2);
24203
+ body.append(sid(row2, markGroupSettingsId(g.id)));
23490
24204
  }
23491
24205
  refreshDimming();
23492
24206
  }
@@ -24309,6 +25023,16 @@ var DrawingPainter = class {
24309
25023
  this.paintMagnifier(ctx, d, proj, theme);
24310
25024
  return;
24311
25025
  }
25026
+ if (d instanceof PriceActionZone) {
25027
+ this.paintPriceZone(ctx, d, proj, theme);
25028
+ this.paintLabel(ctx, d, proj, theme);
25029
+ return;
25030
+ }
25031
+ if (d instanceof Liquidity) {
25032
+ this.paintLiquidity(ctx, d, proj);
25033
+ this.paintLabel(ctx, d, proj, theme);
25034
+ return;
25035
+ }
24312
25036
  if (d instanceof PatternDrawing) {
24313
25037
  this.paintPattern(ctx, d, proj, theme);
24314
25038
  return;
@@ -24351,9 +25075,11 @@ var DrawingPainter = class {
24351
25075
  case "arrow": {
24352
25076
  const pts = d.handlePoints(proj);
24353
25077
  if (pts.length < 2) return;
25078
+ const ext = d instanceof TrendLine ? d.extension() : "none";
25079
+ const [ex1, ey1, ex2, ey2] = extendEndpoints(pts[0][0], pts[0][1], pts[1][0], pts[1][1], ext, proj.width, proj.height);
24354
25080
  this.stroke(ctx, d.style, () => {
24355
- ctx.moveTo(pts[0][0], pts[0][1]);
24356
- ctx.lineTo(pts[1][0], pts[1][1]);
25081
+ ctx.moveTo(ex1, ey1);
25082
+ ctx.lineTo(ex2, ey2);
24357
25083
  });
24358
25084
  if (d.style.arrowRight) this.paintArrowhead(ctx, pts[0], pts[1], d.style);
24359
25085
  if (d.style.arrowLeft) this.paintArrowhead(ctx, pts[1], pts[0], d.style);
@@ -24495,9 +25221,15 @@ var DrawingPainter = class {
24495
25221
  ctx.font = `${text.bold ? "bold " : ""}${text.italic ? "italic " : ""}${fs}px ${theme.fontFamily}`;
24496
25222
  ctx.textBaseline = "top";
24497
25223
  ctx.textAlign = layout.align;
24498
- ctx.fillStyle = text.color ?? theme.textColor;
24499
25224
  const lh = labelLineHeight(fs);
24500
25225
  const lines = text.value.split("\n");
25226
+ if (d.type !== "text" && d.type !== "box") {
25227
+ ctx.lineJoin = "round";
25228
+ ctx.lineWidth = 3;
25229
+ ctx.strokeStyle = theme.background;
25230
+ lines.forEach((line, i) => ctx.strokeText(line, layout.x, layout.top + i * lh));
25231
+ }
25232
+ ctx.fillStyle = text.color ?? theme.textColor;
24501
25233
  lines.forEach((line, i) => ctx.fillText(line, layout.x, layout.top + i * lh));
24502
25234
  if (d.type === "text") this.paintTextFrame(ctx, d.id, layout.x, layout.top, lines, fs, theme);
24503
25235
  ctx.restore();
@@ -24552,6 +25284,78 @@ var DrawingPainter = class {
24552
25284
  ctx.textAlign = "left";
24553
25285
  ctx.textBaseline = "alphabetic";
24554
25286
  }
25287
+ /** Paint a price-action zone (fair value gap, order block): the box and its halfway line,
25288
+ * quieter once it has run its course, a dot where it stopped, and — once price flips it —
25289
+ * the dashed box it carries on as. With no zone under the anchor, a faint tick marks it. */
25290
+ paintPriceZone(ctx, d, proj, theme) {
25291
+ const z = d.zone(proj);
25292
+ if (!z) {
25293
+ const a = d.anchors[0];
25294
+ if (!a) return;
25295
+ const x = Math.round(proj.xOf(a.time)) + 0.5;
25296
+ this.stroke(ctx, { lineColor: withAlpha(theme.textColor, 0.35), lineWidth: 1, lineStyle: "dashed" }, () => {
25297
+ ctx.moveTo(x, 0);
25298
+ ctx.lineTo(x, proj.height);
25299
+ });
25300
+ return;
25301
+ }
25302
+ const h = z.yBottom - z.yTop;
25303
+ const stroke = toHex6(z.fill, z.fill);
25304
+ ctx.save();
25305
+ ctx.globalAlpha *= z.faded ? 0.6 : 1;
25306
+ ctx.fillStyle = z.fill;
25307
+ ctx.fillRect(z.x1, z.yTop, z.x2 - z.x1, h);
25308
+ if (z.border) {
25309
+ ctx.lineWidth = 1;
25310
+ ctx.setLineDash([]);
25311
+ ctx.strokeStyle = stroke;
25312
+ ctx.strokeRect(Math.round(z.x1) + 0.5, Math.round(z.yTop) + 0.5, Math.round(z.x2 - z.x1), Math.round(h));
25313
+ }
25314
+ if (z.midY != null) {
25315
+ const y = Math.round(z.midY) + 0.5;
25316
+ this.stroke(ctx, { lineColor: stroke, lineWidth: 1, lineStyle: "dashed" }, () => {
25317
+ ctx.moveTo(z.x1, y);
25318
+ ctx.lineTo(z.x2, y);
25319
+ });
25320
+ }
25321
+ ctx.restore();
25322
+ if (z.flip) {
25323
+ const f = z.flip;
25324
+ ctx.save();
25325
+ ctx.fillStyle = f.fill;
25326
+ ctx.fillRect(f.x1, z.yTop, f.x2 - f.x1, h);
25327
+ ctx.lineWidth = 1;
25328
+ ctx.strokeStyle = toHex6(f.fill, f.fill);
25329
+ ctx.setLineDash([5, 4]);
25330
+ ctx.strokeRect(Math.round(f.x1) + 0.5, Math.round(z.yTop) + 0.5, Math.round(f.x2 - f.x1), Math.round(h));
25331
+ ctx.restore();
25332
+ }
25333
+ if (z.marker) {
25334
+ ctx.beginPath();
25335
+ ctx.arc(z.marker.x, z.marker.y, 3.5, 0, Math.PI * 2);
25336
+ ctx.fillStyle = stroke;
25337
+ ctx.fill();
25338
+ }
25339
+ }
25340
+ /** Paint a liquidity level: the line to where it stopped, and a cross where it was swept. */
25341
+ paintLiquidity(ctx, d, proj) {
25342
+ const l = d.line(proj);
25343
+ if (!l) return;
25344
+ const y = Math.round(l.y) + 0.5;
25345
+ this.stroke(ctx, d.style, () => {
25346
+ ctx.moveTo(l.x1, y);
25347
+ ctx.lineTo(l.x2, y);
25348
+ });
25349
+ if (l.sweep) {
25350
+ const { x, y: sy } = l.sweep;
25351
+ this.stroke(ctx, { lineColor: d.style.lineColor, lineWidth: 1.75, lineStyle: "solid" }, () => {
25352
+ ctx.moveTo(x - 4, sy - 4);
25353
+ ctx.lineTo(x + 4, sy + 4);
25354
+ ctx.moveTo(x + 4, sy - 4);
25355
+ ctx.lineTo(x - 4, sy + 4);
25356
+ });
25357
+ }
25358
+ }
24555
25359
  /** Paint a concentric-ring fib tool (circles / arcs / wedge): each enabled level as an arc of
24556
25360
  * radius R0·ratio over the tool's angular sweep, plus any bounding lines + a ratio label per ring. */
24557
25361
  paintRadialFib(ctx, d, proj, theme) {
@@ -25021,15 +25825,15 @@ var DrawingPainter = class {
25021
25825
  const { profile, maxW, anchorX, grow, yEdges } = L;
25022
25826
  if (profile.maxTotal <= 0 || maxW <= 0) return;
25023
25827
  for (let k = 0; k < profile.rows.length; k += 1) {
25024
- const row = profile.rows[k];
25025
- const total = row.up + row.down;
25828
+ const row2 = profile.rows[k];
25829
+ const total = row2.up + row2.down;
25026
25830
  if (total <= 0) continue;
25027
25831
  const yA = yEdges[k];
25028
25832
  const yB = yEdges[k + 1];
25029
25833
  const yTop = Math.min(yA, yB);
25030
25834
  const h = Math.max(1, Math.abs(yB - yA) - 1);
25031
25835
  const w2 = total / profile.maxTotal * maxW;
25032
- const upW = row.up / total * w2;
25836
+ const upW = row2.up / total * w2;
25033
25837
  const inVa = k >= profile.vaFrom && k <= profile.vaTo;
25034
25838
  const upColor = inVa ? s.vaUpColor : s.upColor;
25035
25839
  const downColor = inVa ? s.vaDownColor : s.downColor;
@@ -25494,11 +26298,20 @@ var DrawingPainter = class {
25494
26298
  ctx.arc(x1, y1, arcR, Math.min(0, end), Math.max(0, end));
25495
26299
  ctx.stroke();
25496
26300
  }
25497
- ctx.fillStyle = color;
26301
+ const label = `${angleDeg.toFixed(1)}\xB0`;
26302
+ const lx = x1 + arcR + 6;
26303
+ const ly = dy < 0 ? y1 + 14 : y1 - 6;
25498
26304
  ctx.font = `11px ${theme.fontFamily}`;
25499
26305
  ctx.textAlign = "left";
25500
26306
  ctx.textBaseline = "alphabetic";
25501
- ctx.fillText(`${angleDeg.toFixed(1)}\xB0`, x1 + arcR + 6, y1 + (dy < 0 ? -14 : 2));
26307
+ ctx.save();
26308
+ ctx.lineJoin = "round";
26309
+ ctx.lineWidth = 3;
26310
+ ctx.strokeStyle = theme.background;
26311
+ ctx.strokeText(label, lx, ly);
26312
+ ctx.restore();
26313
+ ctx.fillStyle = color;
26314
+ ctx.fillText(label, lx, ly);
25502
26315
  }
25503
26316
  /** Paint a fixed-size arrow marker as a filled glyph at its anchor. */
25504
26317
  paintArrowMark(ctx, d, proj) {
@@ -25641,6 +26454,12 @@ function labelLayout(d, proj) {
25641
26454
  const pts = d.handlePoints(proj);
25642
26455
  const lines = text.value.split("\n").length;
25643
26456
  const lh = labelLineHeight(namedFontSize(text.size));
26457
+ if (d instanceof ArrowMark) {
26458
+ const b = d.bounds(proj);
26459
+ if (!b) return null;
26460
+ const x = b.x + b.w / 2;
26461
+ return d.type === "arrowmarkdown" ? { x, top: b.y - 4 - lh * lines, align: "center" } : { x, top: b.y + b.h + 4, align: "center" };
26462
+ }
25644
26463
  switch (d.type) {
25645
26464
  case "text":
25646
26465
  return pts[0] ? { x: pts[0][0] + 2, top: pts[0][1] + 2, align: "left" } : null;
@@ -25655,11 +26474,13 @@ function labelLayout(d, proj) {
25655
26474
  if (pts.length < 2) return null;
25656
26475
  const [x1, y1] = pts[0];
25657
26476
  const [x2, y2] = pts[1];
26477
+ const t = text.place === "start" ? 0.12 : text.place === "end" ? 0.88 : 0.5;
26478
+ const below = text.side === "below";
25658
26479
  return {
25659
26480
  x: 0,
25660
- top: -6 - lh * lines,
26481
+ top: below ? 6 : -6 - lh * lines,
25661
26482
  align: "center",
25662
- rotate: { angle: uprightLineAngle(x1, y1, x2, y2), cx: (x1 + x2) / 2, cy: (y1 + y2) / 2 }
26483
+ rotate: { angle: uprightLineAngle(x1, y1, x2, y2), cx: x1 + (x2 - x1) * t, cy: y1 + (y2 - y1) * t }
25663
26484
  };
25664
26485
  }
25665
26486
  case "box": {
@@ -25840,8 +26661,9 @@ var DrawingInteraction = class {
25840
26661
  const tool = this.deps.activeTool();
25841
26662
  if (tool) {
25842
26663
  const paneId = proj.paneIdAtY(y) ?? "price";
25843
- const draft = createDrawing(tool, { paneId, anchors: [this.resolve(x, y, paneId, mode)], style: this.deps.lastStyle() });
26664
+ const draft = createDrawing(tool, { paneId, anchors: [this.resolve(x, y, paneId, mode)] });
25844
26665
  if (!draft) return;
26666
+ applyToolDefaults(draft, this.deps.toolDefaults());
25845
26667
  this.state = { kind: "placing", draft, need: draft.anchorSchema().max, cursor: null };
25846
26668
  if (draft.placementMode() === "click" && draft.anchors.length >= this.state.need) this.finalize();
25847
26669
  else this.deps.changed();
@@ -26040,7 +26862,8 @@ var DrawingInteraction = class {
26040
26862
  if (this.state.kind !== "placing") return null;
26041
26863
  const { draft, cursor } = this.state;
26042
26864
  if (!cursor) return draft.anchors.length >= 1 && draft.type === "hline" ? draft : null;
26043
- return createDrawing(draft.type, { paneId: draft.paneId, anchors: [...draft.anchors, this.placementAnchor(draft, cursor)], style: draft.style });
26865
+ const { paneId, style, text, props } = draft.serialize();
26866
+ return createDrawing(draft.type, { paneId, anchors: [...draft.anchors, this.placementAnchor(draft, cursor)], style, text, props });
26044
26867
  }
26045
26868
  /** Map a raw placement cursor to the anchor the drawing actually stores.
26046
26869
  * Parallel channel: its 3rd anchor is the parallel-line price measured AT THE
@@ -26144,7 +26967,7 @@ function applyFree(anchor, pt, free) {
26144
26967
  if (free === "both" || free === "y") anchor.price = pt.price;
26145
26968
  }
26146
26969
 
26147
- // src/renderers/native/drawings/DrawingSettingsDialog.ts
26970
+ // src/renderers/native/drawings/DrawingSettingsSections.ts
26148
26971
  var LEVEL_UNITS = [
26149
26972
  { value: "price", label: "Price" },
26150
26973
  { value: "points", label: "Points" }
@@ -26153,15 +26976,319 @@ var FRVP_ANCHOR = [
26153
26976
  { value: "right", label: "Right" },
26154
26977
  { value: "left", label: "Left" }
26155
26978
  ];
26156
- var TITLES = {
26157
- position: "Position size",
26158
- frvp: "Volume profile",
26159
- levels: "Levels"
26979
+ function buildPositionSection(grid, drawing, actions, theme) {
26980
+ const live = () => {
26981
+ const d = actions.resolve();
26982
+ return d instanceof PositionTool ? d : drawing;
26983
+ };
26984
+ const refreshers = [];
26985
+ const refreshAll = () => refreshers.forEach((f) => f());
26986
+ const fmt = (n) => Math.round(n * 1e8) / 1e8;
26987
+ const numberRow = (label, path, clamp) => {
26988
+ const ctrl = buildFieldControl({
26989
+ kind: "number",
26990
+ value: fmt(live()[path]),
26991
+ min: clamp?.min,
26992
+ max: clamp?.max,
26993
+ step: clamp?.step,
26994
+ integer: clamp?.integer,
26995
+ fill: false,
26996
+ commit: "blur",
26997
+ onChange: (n) => {
26998
+ actions.patch({ [path]: n });
26999
+ refreshAll();
27000
+ }
27001
+ });
27002
+ refreshers.push(() => {
27003
+ if (document.activeElement !== ctrl.el.querySelector("input")) ctrl.setValue?.(fmt(live()[path]));
27004
+ });
27005
+ grid.appendChild(fieldRow({ label, control: ctrl.el }));
27006
+ };
27007
+ const levelRow = (label, path, level) => {
27008
+ let mode = "price";
27009
+ const display = () => fmt(live().levelDisplayValue(level, mode));
27010
+ const ni = buildFieldControl({
27011
+ kind: "number",
27012
+ value: display(),
27013
+ fill: false,
27014
+ commit: "blur",
27015
+ onChange: (n) => {
27016
+ actions.patch({ [path]: live().levelPriceFromDisplay(level, mode, n) });
27017
+ refreshAll();
27018
+ }
27019
+ });
27020
+ const sel = buildFieldControl({
27021
+ kind: "select",
27022
+ options: LEVEL_UNITS,
27023
+ value: mode,
27024
+ fill: false,
27025
+ theme,
27026
+ onChange: (v) => {
27027
+ mode = v;
27028
+ ni.setValue?.(display());
27029
+ }
27030
+ });
27031
+ refreshers.push(() => {
27032
+ if (document.activeElement !== ni.el.querySelector("input")) ni.setValue?.(display());
27033
+ });
27034
+ grid.appendChild(fieldRow({ label, control: [ni.el, sel.el] }));
27035
+ };
27036
+ grid.appendChild(fieldSection("Account", { variant: "inputs", first: true }));
27037
+ numberRow("Risk %", "riskPercent", { min: 0, max: 100, step: 0.1 });
27038
+ numberRow("Account balance", "accountBalance", { min: 0, max: 1e12, step: 1, integer: true });
27039
+ numberRow("Position size", "quantity");
27040
+ grid.appendChild(fieldSection("Levels", { variant: "inputs" }));
27041
+ const dir = buildFieldControl({
27042
+ kind: "select",
27043
+ options: DIRECTION_OPTIONS,
27044
+ value: live().direction,
27045
+ fill: false,
27046
+ theme,
27047
+ onChange: (v) => {
27048
+ actions.patch({ direction: v });
27049
+ refreshAll();
27050
+ }
27051
+ });
27052
+ refreshers.push(() => dir.setValue?.(live().direction));
27053
+ grid.appendChild(fieldRow({ label: "Direction", control: dir.el }));
27054
+ numberRow("Entry price", "entryPrice");
27055
+ levelRow("Stop", "stopPrice", "stop");
27056
+ levelRow("Target", "targetPrice", "target");
27057
+ grid.appendChild(fieldSection("Display", { variant: "inputs" }));
27058
+ const toggles = [
27059
+ ["Show text", "showText"],
27060
+ ["Show direction & ratio", "showHeader"],
27061
+ ["Show loss & size", "showLossSize"],
27062
+ ["Show target label", "showTargetLabel"],
27063
+ ["Show stop label", "showStopLabel"],
27064
+ ["Show level prices", "showPrices"]
27065
+ ];
27066
+ for (const [label, path] of toggles) {
27067
+ grid.appendChild(fieldRow({
27068
+ label,
27069
+ bool: true,
27070
+ toggle: {
27071
+ checked: live()[path],
27072
+ onChange: (v) => {
27073
+ actions.patch({ [path]: v });
27074
+ refreshAll();
27075
+ },
27076
+ get: () => live()[path]
27077
+ }
27078
+ }));
27079
+ }
27080
+ const summary = document.createElement("div");
27081
+ summary.className = "vela-field-span";
27082
+ 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;";
27083
+ refreshers.push(() => {
27084
+ const d = live();
27085
+ summary.textContent = `${d.headerLabel()} \u2014 ${d.lossSizeLabel()}`;
27086
+ });
27087
+ grid.appendChild(summary);
27088
+ refreshAll();
27089
+ }
27090
+ function buildProfileSection(grid, drawing, actions, theme) {
27091
+ const styleOf = () => {
27092
+ const d = actions.resolve();
27093
+ return d instanceof FixedRangeVolumeProfile ? d.frvp : drawing.frvp;
27094
+ };
27095
+ const s = styleOf();
27096
+ const numberRow = (label, path, min, max, step, integer = true) => {
27097
+ grid.appendChild(fieldRow({
27098
+ label,
27099
+ control: buildFieldControl({
27100
+ kind: "number",
27101
+ value: s[path],
27102
+ min,
27103
+ max,
27104
+ step,
27105
+ integer,
27106
+ fill: false,
27107
+ commit: "blur",
27108
+ onChange: (n) => actions.patch({ [`frvp.${path}`]: n })
27109
+ }).el
27110
+ }));
27111
+ };
27112
+ numberRow("Rows", "rows", 1, 500, 1);
27113
+ numberRow("Value Area", "valueAreaPct", 0, 100, 1);
27114
+ numberRow("Width %", "widthPct", 0, 100, 1);
27115
+ grid.appendChild(fieldRow({
27116
+ label: "Anchor",
27117
+ control: buildFieldControl({
27118
+ kind: "select",
27119
+ options: FRVP_ANCHOR,
27120
+ value: s.anchor,
27121
+ fill: false,
27122
+ theme,
27123
+ onChange: (v) => actions.patch({ "frvp.anchor": v })
27124
+ }).el
27125
+ }));
27126
+ const colorRow = (label, path) => {
27127
+ grid.appendChild(fieldRow({
27128
+ label,
27129
+ fit: true,
27130
+ control: buildFieldControl({
27131
+ kind: "color",
27132
+ theme,
27133
+ get: () => styleOf()[path],
27134
+ onChange: (v) => actions.patch({ [`frvp.${path}`]: v })
27135
+ }).el
27136
+ }));
27137
+ };
27138
+ colorRow("Up Volume", "upColor");
27139
+ colorRow("Down Volume", "downColor");
27140
+ colorRow("Value Area Up", "vaUpColor");
27141
+ colorRow("Value Area Down", "vaDownColor");
27142
+ const styles = LINE_STYLE_OPTIONS.map((o) => ({ value: o.value, label: o.label }));
27143
+ const levelRow = (label, showPath, colorPath, stylePath) => {
27144
+ const row2 = document.createElement("div");
27145
+ row2.className = "vela-field-span";
27146
+ row2.style.cssText = "display:flex;align-items:center;gap:8px;";
27147
+ const sw = buildFieldControl({
27148
+ kind: "switch",
27149
+ checked: Boolean(s[showPath]),
27150
+ onChange: (v) => actions.patch({ [`frvp.${showPath}`]: v })
27151
+ });
27152
+ const lbl = document.createElement("span");
27153
+ lbl.className = "vela-field-label";
27154
+ lbl.style.flex = "1";
27155
+ lbl.textContent = label;
27156
+ let cur = s[colorPath] ?? contrastColor(theme.background);
27157
+ const col = buildFieldControl({
27158
+ kind: "color",
27159
+ theme,
27160
+ get: () => cur,
27161
+ onChange: (v) => {
27162
+ cur = v;
27163
+ actions.patch({ [`frvp.${colorPath}`]: v });
27164
+ }
27165
+ });
27166
+ const style = buildFieldControl({
27167
+ kind: "select",
27168
+ options: styles,
27169
+ value: s[stylePath],
27170
+ fill: false,
27171
+ theme,
27172
+ onChange: (v) => actions.patch({ [`frvp.${stylePath}`]: v })
27173
+ });
27174
+ row2.append(sw.el, lbl, col.el, style.el);
27175
+ grid.appendChild(row2);
27176
+ };
27177
+ levelRow("VAH", "showVah", "vahColor", "vahStyle");
27178
+ levelRow("VAL", "showVal", "valColor", "valStyle");
27179
+ levelRow("POC", "showPoc", "pocColor", "pocStyle");
27180
+ levelRow("Developing POC", "showDevelopingPoc", "developingPocColor", "developingPocStyle");
27181
+ levelRow("Developing VA", "showDevelopingVa", "developingVaColor", "developingVaStyle");
27182
+ }
27183
+
27184
+ // src/renderers/native/drawings/DrawingSettingsPanel.ts
27185
+ var LINE_LABEL_TYPES = /* @__PURE__ */ new Set(["trendline", "ray", "extendedline", "infoline", "trendangle"]);
27186
+ var NO_POINTS = /* @__PURE__ */ new Set(["freehand", "highlighter", "vline", "position"]);
27187
+ var MAX_POINTS = 8;
27188
+ var POSITION_PATHS = /* @__PURE__ */ new Set(["riskPercent", "accountBalance", "quantity", "direction", "entryPrice", "stopPrice", "targetPrice", "showText", "showHeader", "showLossSize", "showTargetLabel", "showStopLabel", "showPrices"]);
27189
+ var LEVEL_PATHS = /* @__PURE__ */ new Set(["showRatios"]);
27190
+ var INTRADAY = ["s", "1", "5", "15", "60", "240"];
27191
+ var HIGHER = ["60", "240", "D", "W", "M"];
27192
+ var openFolds = /* @__PURE__ */ new Map();
27193
+ var STYLE_ID3 = "vela-dsp-styles";
27194
+ var STYLE_REV = "1";
27195
+ 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>';
27196
+ 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>';
27197
+ 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>';
27198
+ var EXTEND_ICONS = {
27199
+ 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>',
27200
+ 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>',
27201
+ 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>',
27202
+ 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>'
26160
27203
  };
26161
- var DrawingSettingsDialog = class {
26162
- constructor(host, theme) {
27204
+ 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>`;
27205
+ 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>`;
27206
+ function ensureStyles3() {
27207
+ if (typeof document === "undefined") return;
27208
+ const existing = document.getElementById(STYLE_ID3);
27209
+ if (existing?.dataset.rev === STYLE_REV) return;
27210
+ const s = existing ?? document.createElement("style");
27211
+ s.id = STYLE_ID3;
27212
+ s.dataset.rev = STYLE_REV;
27213
+ s.textContent = `
27214
+ .vela-dsp-pass { pointer-events: none !important; }
27215
+ .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; }
27216
+ .vela-dsp .vela-dialog-header { padding: 8px 8px 8px 16px; gap: 6px; }
27217
+ .vela-dsp .vela-dialog-title { font-size: 15px; line-height: 22px; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
27218
+ .vela-dsp .vela-dialog-footer { padding: 10px 12px; display: flex; justify-content: flex-end; gap: 8px; border-top: 1px solid var(--vela-border); }
27219
+ .vela-dsp .vela-dialog-btn { height: 30px; font-size: 13px; }
27220
+ .vela-dsp-scroll { overflow-y: auto; overflow-x: hidden; flex: 1 1 auto; min-height: 0; }
27221
+ .vela-dsp-block { padding: 12px 16px; display: flex; flex-direction: column; gap: 8px; }
27222
+ .vela-dsp-block + .vela-dsp-block, .vela-dsp-fold { border-top: 1px solid var(--vela-border); }
27223
+ .vela-dsp-h { font-size: 11px; font-weight: 600; letter-spacing: 0.05em; text-transform: uppercase; color: var(--vela-fg-muted); }
27224
+ .vela-dsp-row { display: flex; align-items: center; gap: 8px; min-height: 30px; }
27225
+ .vela-dsp-row[hidden] { display: none; }
27226
+ .vela-dsp-lab { flex: 0 0 96px; color: var(--vela-fg-muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
27227
+ .vela-dsp-row > .vela-dsp-lab + * { min-width: 0; }
27228
+ .vela-dsp-spacer { flex: 1; }
27229
+ .vela-dsp-fold-h { all: unset; box-sizing: border-box; width: 100%; display: flex; align-items: center; gap: 8px; padding: 11px 16px; cursor: pointer; }
27230
+ .vela-dsp-fold-h:hover { background: var(--vela-hover); }
27231
+ .vela-dsp-fold-h:focus-visible { outline: 2px solid var(--vela-selected-bg); outline-offset: -2px; }
27232
+ .vela-dsp-fold-t { font-weight: 600; color: var(--vela-fg-bright); white-space: nowrap; }
27233
+ .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; }
27234
+ .vela-dsp-chev { display: flex; color: var(--vela-fg-muted); transition: transform var(--vela-dur-fast) ease; }
27235
+ .vela-dsp-fold[data-open] .vela-dsp-chev { transform: rotate(90deg); }
27236
+ .vela-dsp-fold-b { padding: 0 16px 12px; display: flex; flex-direction: column; gap: 8px; }
27237
+ .vela-dsp-fold:not([data-open]) .vela-dsp-fold-b { display: none; }
27238
+ .vela-dsp-seg { display: inline-flex; flex: none; border: 1px solid var(--vela-border-strong); border-radius: 6px; overflow: hidden; }
27239
+ .vela-dsp-seg[data-full] { display: flex; flex: 1; }
27240
+ .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; }
27241
+ .vela-dsp-seg[data-full] > button { flex: 1 1 0; min-width: 0; }
27242
+ .vela-dsp-seg > button + button { border-left: 1px solid var(--vela-border); }
27243
+ .vela-dsp-seg > button:hover { background: var(--vela-hover); color: var(--vela-fg-bright); }
27244
+ .vela-dsp-seg > button[aria-pressed='true'] { background: var(--vela-active); color: var(--vela-fg-bright); }
27245
+ .vela-dsp-seg > button:focus-visible { outline: 2px solid var(--vela-selected-bg); outline-offset: -2px; }
27246
+ .vela-dsp-chips { display: flex; flex-wrap: wrap; gap: 6px; }
27247
+ .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; }
27248
+ .vela-dsp-chip:hover { color: var(--vela-fg-bright); border-color: var(--vela-fg-muted); }
27249
+ .vela-dsp-chip[aria-pressed='true'] { background: var(--vela-active); border-color: transparent; color: var(--vela-fg-bright); }
27250
+ .vela-dsp-chip:focus-visible { outline: 2px solid var(--vela-selected-bg); outline-offset: 1px; }
27251
+ .vela-dsp-note { font-size: 12px; color: var(--vela-fg-muted); line-height: 1.45; }
27252
+ .vela-dsp-pt { display: grid; grid-template-columns: 34px minmax(0, 120px) minmax(0, 1fr); align-items: center; gap: 8px; min-height: 30px; }
27253
+ .vela-dsp-pt-k { font-weight: 600; color: var(--vela-fg-muted); }
27254
+ .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; }
27255
+ .vela-dsp-tf[hidden], .vela-dsp-note[hidden] { display: none; }
27256
+ .vela-dsp-tf { display: grid; grid-template-columns: repeat(5, minmax(0, 1fr)); gap: 4px; }
27257
+ .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; }
27258
+ .vela-dsp-tf > button:hover { color: var(--vela-fg-bright); border-color: var(--vela-fg-muted); }
27259
+ .vela-dsp-tf > button[aria-pressed='true'] { background: var(--vela-active); border-color: transparent; color: var(--vela-fg-bright); }
27260
+ .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); }
27261
+ .vela-dsp-grid { padding: 0; }
27262
+ .vela-dsp-lv { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 6px 14px; }
27263
+ .vela-dsp-lv-c { display: flex; align-items: center; gap: 6px; min-width: 0; }
27264
+ .vela-dsp-lv-c .vela-num:not([data-fill])[data-compact] { width: 76px; }
27265
+ .vela-dsp-lv-c .vela-num input { height: 28px; }
27266
+ .vela-dsp-lv-c[data-off] .vela-num input { color: var(--vela-fg-muted); }
27267
+ .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; }
27268
+ .vela-dsp-style > span:first-child { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
27269
+ .vela-dsp-style:hover, .vela-dsp-style[aria-expanded='true'] { background: var(--vela-hover); color: var(--vela-fg-bright); }
27270
+ .vela-dsp-menu { min-width: 220px; max-height: 320px; overflow-y: auto; padding: 4px; display: flex; flex-direction: column; gap: 1px; }
27271
+ .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); }
27272
+ .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; }
27273
+ .vela-dsp-mi:hover, .vela-dsp-mi:focus-visible { background: var(--vela-hover-strong); }
27274
+ .vela-dsp-mi[data-current] { color: var(--vela-fg-bright); font-weight: 600; }
27275
+ .vela-dsp-mi > .vela-dsp-mi-t { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
27276
+ .vela-dsp-mi-dot { flex: none; width: 14px; height: 14px; border-radius: 4px; border: 1px solid var(--vela-border-strong); }
27277
+ .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); }
27278
+ .vela-dsp-mi-x:hover { background: var(--vela-hover); color: var(--vela-danger); }
27279
+ .vela-dsp-msep { height: 1px; background: var(--vela-border); margin: 4px 0; }
27280
+ .vela-dsp-mname { display: flex; gap: 6px; padding: 4px; }
27281
+ .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; }
27282
+ .vela-dsp-mname > input:focus { border-color: var(--vela-selected-bg); }`;
27283
+ if (!existing) document.head.appendChild(s);
27284
+ }
27285
+ var DrawingSettingsPanel = class {
27286
+ constructor(host, theme, env) {
26163
27287
  this.host = host;
27288
+ this.env = env;
26164
27289
  this.ui = null;
27290
+ this.menu = null;
27291
+ this.refresh = null;
26165
27292
  this.theme = theme;
26166
27293
  }
26167
27294
  setTheme(theme) {
@@ -26171,69 +27298,97 @@ var DrawingSettingsDialog = class {
26171
27298
  return this.ui != null;
26172
27299
  }
26173
27300
  contains(node) {
26174
- return this.ui?.contains(node) === true;
27301
+ return node != null && this.ui?.contains(node) === true;
26175
27302
  }
26176
- open(drawing, actions, kind) {
27303
+ /** Re-read the drawing after a change made outside the panel (a drag, an undo). */
27304
+ sync() {
27305
+ this.refresh?.();
27306
+ }
27307
+ open(drawing, actions, anchor) {
26177
27308
  this.close();
26178
- const live = actions.resolve() ?? drawing;
26179
- const snapshot = clonePlain(live.serialize());
26180
- const grid = fieldGrid({ variant: "inputs" });
26181
- grid.style.padding = "16px 20px";
26182
- grid.style.overflowY = "auto";
26183
- grid.style.overflowX = "hidden";
26184
- grid.style.flex = "1 1 auto";
26185
- if (kind === "position" && live instanceof PositionTool) this.buildPosition(grid, live, actions);
26186
- else if (kind === "frvp" && live instanceof FixedRangeVolumeProfile) this.buildFrvp(grid, live, actions);
26187
- else if (kind === "levels") this.buildLevels(grid, live, actions);
26188
- else return;
27309
+ ensureStyles3();
27310
+ const first = actions.resolve() ?? drawing;
27311
+ const snapshot = clonePlain(first.serialize());
27312
+ const live = () => actions.resolve() ?? first;
27313
+ const meta = getDrawingType(first.type);
27314
+ const scroll = document.createElement("div");
27315
+ scroll.className = "vela-dsp-scroll";
27316
+ let watchers = [];
27317
+ const runWatchers = () => {
27318
+ for (const fn of watchers) fn();
27319
+ };
27320
+ const build = {
27321
+ live,
27322
+ edit: (p) => {
27323
+ actions.patch(p);
27324
+ runWatchers();
27325
+ },
27326
+ watch: (fn) => {
27327
+ watchers.push(fn);
27328
+ fn();
27329
+ },
27330
+ rebuild: () => {
27331
+ const top = scroll.scrollTop;
27332
+ watchers = [];
27333
+ scroll.replaceChildren(...this.sections(build));
27334
+ scroll.scrollTop = top;
27335
+ styleBtn.refresh();
27336
+ },
27337
+ actions
27338
+ };
27339
+ const cancel = () => {
27340
+ actions.preview?.(null);
27341
+ actions.restore?.(snapshot);
27342
+ this.close();
27343
+ };
26189
27344
  const ui = new Dialog({
26190
27345
  host: this.host,
26191
- title: TITLES[kind],
26192
- // Non-modal: live-edit dialog — a modal machine locks pointer events on the
26193
- // whole body, killing the chart and the body-portaled popovers.
27346
+ title: meta?.label ?? "Settings",
26194
27347
  modal: false,
26195
27348
  contained: true,
26196
27349
  align: "center",
26197
27350
  draggable: true,
26198
27351
  flush: true,
26199
- className: "vela-dialog--form",
27352
+ className: "vela-dsp",
26200
27353
  closeOnEscape: false,
26201
- footer: (foot) => {
26202
- foot.append(
26203
- btn("Cancel", false, () => {
26204
- actions.restore?.(snapshot);
26205
- this.close();
26206
- }),
26207
- btn("Ok", true, () => this.close())
26208
- );
26209
- },
27354
+ footer: (foot) => foot.append(button("Cancel", false, cancel), button("Ok", true, () => this.close())),
26210
27355
  onOpenChange: (open2) => {
26211
27356
  if (!open2) this.close();
26212
27357
  }
26213
27358
  });
26214
27359
  applyChromeTokens(ui.panel, this.theme);
26215
- ui.body.appendChild(grid);
26216
- const onBackdropDown = (e) => {
26217
- if (e.target !== ui.backdrop && e.target !== ui.positioner) return;
26218
- if (isPopoverOpen() || eventDismissedPopover(e)) {
26219
- closeOpenPopovers();
26220
- return;
26221
- }
26222
- this.close();
26223
- };
26224
- ui.backdrop.addEventListener("pointerdown", onBackdropDown);
26225
- ui.positioner.addEventListener("pointerdown", onBackdropDown);
27360
+ ui.backdrop.classList.add("vela-dsp-pass");
27361
+ ui.positioner.classList.add("vela-dsp-pass");
27362
+ const hostW = this.host.clientWidth;
27363
+ const leftHalf = anchor != null && hostW > 0 && anchor.x + anchor.w / 2 < hostW / 2;
27364
+ ui.positioner.style.justifyContent = leftHalf ? "flex-end" : "flex-start";
27365
+ ui.positioner.style.padding = "0 12px";
27366
+ const styleBtn = this.styleMenuButton(build);
27367
+ ui.titleEl.after(styleBtn.el);
27368
+ ui.body.appendChild(scroll);
26226
27369
  ui.panel.addEventListener("keydown", (e) => {
26227
27370
  if (e.key === "Escape") {
26228
27371
  e.preventDefault();
26229
- actions.restore?.(snapshot);
26230
- this.close();
27372
+ if (this.menu) this.menu.hide();
27373
+ else cancel();
26231
27374
  }
26232
27375
  });
26233
27376
  this.ui = ui;
27377
+ this.refresh = () => {
27378
+ if (!actions.resolve()) {
27379
+ this.close();
27380
+ return;
27381
+ }
27382
+ runWatchers();
27383
+ styleBtn.refresh();
27384
+ };
27385
+ build.rebuild();
26234
27386
  ui.show();
26235
27387
  }
26236
27388
  close() {
27389
+ this.menu?.hide();
27390
+ this.menu = null;
27391
+ this.refresh = null;
26237
27392
  const ui = this.ui;
26238
27393
  this.ui = null;
26239
27394
  ui?.destroy();
@@ -26241,292 +27396,771 @@ var DrawingSettingsDialog = class {
26241
27396
  destroy() {
26242
27397
  this.close();
26243
27398
  }
26244
- buildPosition(grid, drawing, actions) {
26245
- const live = () => {
26246
- const d = actions.resolve();
26247
- return d instanceof PositionTool ? d : drawing;
26248
- };
26249
- const refreshers = [];
26250
- const refreshAll = () => refreshers.forEach((f) => f());
26251
- const fmt = (n) => Math.round(n * 1e8) / 1e8;
26252
- const numberRow = (label, path, clamp) => {
26253
- const ctrl = buildFieldControl({
26254
- kind: "number",
26255
- value: fmt(live()[path]),
26256
- min: clamp?.min,
26257
- max: clamp?.max,
26258
- step: clamp?.step,
26259
- integer: clamp?.integer,
26260
- fill: false,
26261
- commit: "blur",
26262
- onChange: (n) => {
26263
- actions.patch({ [path]: n });
26264
- refreshAll();
26265
- }
26266
- });
26267
- refreshers.push(() => {
26268
- if (document.activeElement !== ctrl.el.querySelector("input")) ctrl.setValue?.(fmt(live()[path]));
26269
- });
26270
- grid.appendChild(fieldRow({ label, control: ctrl.el }));
26271
- };
26272
- const levelRow = (label, path, level) => {
26273
- let mode = "price";
26274
- const display = () => fmt(live().levelDisplayValue(level, mode));
26275
- const ni = buildFieldControl({
26276
- kind: "number",
26277
- value: display(),
26278
- fill: false,
26279
- commit: "blur",
26280
- onChange: (n) => {
26281
- actions.patch({ [path]: live().levelPriceFromDisplay(level, mode, n) });
26282
- refreshAll();
26283
- }
26284
- });
26285
- const sel = buildFieldControl({
26286
- kind: "select",
26287
- options: LEVEL_UNITS,
26288
- value: mode,
26289
- fill: false,
26290
- theme: this.theme,
26291
- onChange: (v) => {
26292
- mode = v;
26293
- ni.setValue?.(display());
26294
- }
26295
- });
26296
- refreshers.push(() => {
26297
- if (document.activeElement !== ni.el.querySelector("input")) ni.setValue?.(display());
26298
- });
26299
- grid.appendChild(fieldRow({ label, control: [ni.el, sel.el] }));
26300
- };
26301
- grid.appendChild(fieldSection("Account", { variant: "inputs", first: true }));
26302
- numberRow("Risk %", "riskPercent", { min: 0, max: 100, step: 0.1 });
26303
- numberRow("Account balance", "accountBalance", { min: 0, max: 1e12, step: 1, integer: true });
26304
- numberRow("Position size", "quantity");
26305
- grid.appendChild(fieldSection("Levels", { variant: "inputs" }));
26306
- const dir = buildFieldControl({
26307
- kind: "select",
26308
- options: DIRECTION_OPTIONS,
26309
- value: live().direction,
26310
- fill: false,
26311
- theme: this.theme,
26312
- onChange: (v) => {
26313
- actions.patch({ direction: v });
26314
- refreshAll();
27399
+ // ── sections ──
27400
+ sections(b) {
27401
+ const d = b.live();
27402
+ const schema = d.schema();
27403
+ const fields = schema.fields;
27404
+ const has = (path) => fields.some((f) => f.path === path);
27405
+ const out = [];
27406
+ if (schema.textIsContent && has("text.value")) out.push(this.textContentBlock(b, has));
27407
+ const style = this.styleBlock(b, fields, has);
27408
+ if (style) out.push(style);
27409
+ if (d.editableLevels()) out.push(this.levelsBlock(b));
27410
+ if (d instanceof PositionTool) out.push(this.richBlock("Position", (grid) => buildPositionSection(grid, d, b.actions, this.theme)));
27411
+ if (d instanceof FixedRangeVolumeProfile) out.push(this.richBlock("Profile", (grid) => buildProfileSection(grid, d, b.actions, this.theme)));
27412
+ const own = this.optionsBlock(b, fields, d);
27413
+ if (own) out.push(own);
27414
+ if (!schema.textIsContent && fields.some((f) => f.path.startsWith("text."))) out.push(this.textFold(b, has));
27415
+ const points = this.pointsFold(b);
27416
+ if (points) out.push(points);
27417
+ out.push(this.showOnFold(b));
27418
+ return out;
27419
+ }
27420
+ /** Line, width, dash, ends and fill — the look every stroked tool shares. */
27421
+ styleBlock(b, fields, has) {
27422
+ const rows = [];
27423
+ if (has("style.lineColor") || has("style.lineWidth")) {
27424
+ const ctrls = [];
27425
+ if (has("style.lineColor")) ctrls.push(this.swatch(b, "Line color", () => b.live().style.lineColor || DEFAULT_DRAWING_COLOR, "style.lineColor"));
27426
+ const wf = fields.find((f) => f.path === "style.lineWidth");
27427
+ if (wf) {
27428
+ if ((wf.min ?? 1) > 1) ctrls.push(this.number(b, "Line width", () => b.live().style.lineWidth, "style.lineWidth", wf));
27429
+ 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 })));
27430
+ }
27431
+ rows.push(row("Line", ...ctrls));
27432
+ }
27433
+ if (has("style.lineStyle")) {
27434
+ 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 }))));
27435
+ }
27436
+ if (has("style.extendLeft") && has("style.extendRight")) {
27437
+ const opts = [
27438
+ { value: "none", html: EXTEND_ICONS.none, title: "Don't extend" },
27439
+ { value: "left", html: EXTEND_ICONS.left, title: "Extend left" },
27440
+ { value: "right", html: EXTEND_ICONS.right, title: "Extend right" },
27441
+ { value: "both", html: EXTEND_ICONS.both, title: "Extend both ways" }
27442
+ ];
27443
+ const read = () => {
27444
+ const { extendLeft: l, extendRight: r } = b.live().style;
27445
+ return l && r ? "both" : l ? "left" : r ? "right" : "none";
27446
+ };
27447
+ 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" }))));
27448
+ }
27449
+ const arrows = [];
27450
+ if (has("style.arrowLeft")) arrows.push(this.chip(b, "Start arrow", () => !!b.live().style.arrowLeft, (on) => b.edit({ "style.arrowLeft": on })));
27451
+ 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 })));
27452
+ if (arrows.length) rows.push(row("Ends", chips(...arrows)));
27453
+ if (has("style.fillColor")) {
27454
+ rows.push(row("Fill", this.swatch(b, "Fill", () => effectiveFillColor(b.live(), this.theme) ?? b.live().style.fillColor ?? DEFAULT_DRAWING_COLOR, "style.fillColor")));
27455
+ }
27456
+ if (!has("text.value") && has("text.color")) rows.push(row("Text", ...this.textStyling(b, has)));
27457
+ return rows.length ? block(null, ...rows) : null;
27458
+ }
27459
+ /** The tool's own options — every schema field the shared look and the rich sections
27460
+ * don't cover, each as the control its kind calls for. */
27461
+ optionsBlock(b, fields, d) {
27462
+ 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);
27463
+ const rows = [];
27464
+ const own = fields.filter((f) => !skip(f));
27465
+ for (let i = 0; i < own.length; i += 1) {
27466
+ const f = own[i];
27467
+ const next = own[i + 1];
27468
+ 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;
27469
+ if (base) {
27470
+ const color = this.fieldControl(b, f);
27471
+ const style = this.fieldControl(b, next);
27472
+ if (color && style) rows.push(row(base, color, style));
27473
+ i += 1;
27474
+ continue;
26315
27475
  }
26316
- });
26317
- refreshers.push(() => dir.setValue?.(live().direction));
26318
- grid.appendChild(fieldRow({ label: "Direction", control: dir.el }));
26319
- numberRow("Entry price", "entryPrice");
26320
- levelRow("Stop", "stopPrice", "stop");
26321
- levelRow("Target", "targetPrice", "target");
26322
- grid.appendChild(fieldSection("Display", { variant: "inputs" }));
26323
- const toggles = [
26324
- ["Show text", "showText"],
26325
- ["Show direction & ratio", "showHeader"],
26326
- ["Show loss & size", "showLossSize"],
26327
- ["Show target label", "showTargetLabel"],
26328
- ["Show stop label", "showStopLabel"],
26329
- ["Show level prices", "showPrices"]
26330
- ];
26331
- for (const [label, path] of toggles) {
26332
- grid.appendChild(fieldRow({
26333
- label,
26334
- bool: true,
26335
- toggle: {
26336
- checked: live()[path],
26337
- onChange: (v) => {
26338
- actions.patch({ [path]: v });
26339
- refreshAll();
26340
- },
26341
- get: () => live()[path]
26342
- }
26343
- }));
27476
+ const ctrl = this.fieldControl(b, f);
27477
+ if (!ctrl) continue;
27478
+ const r = row(f.label, ctrl);
27479
+ const cond = f.when;
27480
+ if (cond) b.watch(() => r.hidden = !cond.in.includes(readPath(b.live(), cond.path)));
27481
+ rows.push(r);
27482
+ }
27483
+ const props = d.serialize().props ?? {};
27484
+ const sizes = TEXT_SIZE_OPTIONS.map((o) => ({ value: o.value, html: o.label }));
27485
+ 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)));
27486
+ 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)));
27487
+ return rows.length ? block(null, ...rows) : null;
27488
+ }
27489
+ fieldControl(b, f) {
27490
+ const read = () => readPath(b.live(), f.path);
27491
+ switch (f.kind) {
27492
+ case "color":
27493
+ return this.swatch(b, f.label, () => read() || DEFAULT_DRAWING_COLOR, f.path);
27494
+ case "lineStyle":
27495
+ 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 }));
27496
+ case "boolean":
27497
+ return this.switch(b, () => read() !== false && read() != null, (v) => b.edit({ [f.path]: v }));
27498
+ case "number":
27499
+ case "opacity":
27500
+ return this.number(b, f.label, () => Number(read() ?? 0), f.path, f);
27501
+ case "select": {
27502
+ const options = f.path === "magnifier.timeframe" ? magnifierTimeframesBelow(this.env.chartBarMs()) : f.options ?? [];
27503
+ if (options.length === 0) return null;
27504
+ const sel = buildFieldControl({ kind: "select", options, value: String(read() ?? options[0].value), fill: false, theme: this.theme, onChange: (v) => b.edit({ [f.path]: v }) });
27505
+ b.watch(() => sel.setValue?.(String(read() ?? "")));
27506
+ return sel.el;
27507
+ }
27508
+ case "text": {
27509
+ const tf = buildFieldControl({ kind: "text", value: String(read() ?? ""), fill: true, onChange: (v) => b.edit({ [f.path]: v }) });
27510
+ return tf.el;
27511
+ }
27512
+ default:
27513
+ return null;
26344
27514
  }
26345
- const summary = document.createElement("div");
26346
- summary.className = "vela-field-span";
26347
- 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;";
26348
- refreshers.push(() => {
26349
- const d = live();
26350
- summary.textContent = `${d.headerLabel()} \u2014 ${d.lossSizeLabel()}`;
26351
- });
26352
- grid.appendChild(summary);
26353
- refreshAll();
26354
27515
  }
26355
- buildFrvp(grid, drawing, actions) {
26356
- const styleOf = () => {
26357
- const d = actions.resolve();
26358
- return d instanceof FixedRangeVolumeProfile ? d.frvp : drawing.frvp;
27516
+ /** A levelled tool's levels: every level in a two-column grid (on/off, ratio, color) — the
27517
+ * ones that are off stay listed, ready to switch on. A retracement leads with its ready-made
27518
+ * sets; the horizontal fibs follow with how their level text and fill show. */
27519
+ levelsBlock(b) {
27520
+ const d = b.live();
27521
+ const rows = [];
27522
+ const grid = document.createElement("div");
27523
+ grid.className = "vela-dsp-lv";
27524
+ const key = () => JSON.stringify(b.live().editableLevels() ?? []);
27525
+ let last = "";
27526
+ let editing = false;
27527
+ const mine = (p) => {
27528
+ editing = true;
27529
+ try {
27530
+ b.edit(p);
27531
+ } finally {
27532
+ editing = false;
27533
+ }
27534
+ last = key();
26359
27535
  };
26360
- const s = styleOf();
26361
- const numberRow = (label, path, min, max, step, integer = true) => {
26362
- grid.appendChild(fieldRow({
26363
- label,
26364
- control: buildFieldControl({
27536
+ const fill = () => {
27537
+ last = key();
27538
+ grid.replaceChildren();
27539
+ const free = b.live() instanceof FibLevels;
27540
+ (b.live().editableLevels() ?? []).forEach((lv, i) => {
27541
+ const cell = document.createElement("div");
27542
+ cell.className = "vela-dsp-lv-c";
27543
+ cell.toggleAttribute("data-off", !lv.enabled);
27544
+ const on = buildFieldControl({
27545
+ kind: "switch",
27546
+ checked: lv.enabled,
27547
+ onChange: (v) => {
27548
+ cell.toggleAttribute("data-off", !v);
27549
+ mine({ [`levels.${i}.enabled`]: v });
27550
+ }
27551
+ });
27552
+ let ratio = lv.ratio;
27553
+ const num4 = buildFieldControl({
26365
27554
  kind: "number",
26366
- value: s[path],
26367
- min,
26368
- max,
26369
- step,
26370
- integer,
27555
+ value: lv.ratio,
27556
+ min: free ? void 0 : 0,
27557
+ step: 0.01,
27558
+ compact: true,
26371
27559
  fill: false,
26372
27560
  commit: "blur",
26373
- onChange: (n) => actions.patch({ [`frvp.${path}`]: n })
26374
- }).el
26375
- }));
26376
- };
26377
- numberRow("Rows", "rows", 1, 500, 1);
26378
- numberRow("Value Area", "valueAreaPct", 0, 100, 1);
26379
- numberRow("Width %", "widthPct", 0, 100, 1);
26380
- grid.appendChild(fieldRow({
26381
- label: "Anchor",
26382
- control: buildFieldControl({
26383
- kind: "select",
26384
- options: FRVP_ANCHOR,
26385
- value: s.anchor,
26386
- fill: false,
26387
- theme: this.theme,
26388
- onChange: (v) => actions.patch({ "frvp.anchor": v })
26389
- }).el
26390
- }));
26391
- const colorRow = (label, path) => {
26392
- grid.appendChild(fieldRow({
26393
- label,
26394
- fit: true,
26395
- control: buildFieldControl({
27561
+ title: `Level ${i + 1}`,
27562
+ onChange: (n) => {
27563
+ if (!Number.isFinite(n) || !free && n <= 0) {
27564
+ num4.setValue?.(ratio);
27565
+ return;
27566
+ }
27567
+ ratio = n;
27568
+ mine({ [`levels.${i}.ratio`]: n });
27569
+ }
27570
+ });
27571
+ let color = lv.color;
27572
+ const swatch = buildFieldControl({
26396
27573
  kind: "color",
26397
27574
  theme: this.theme,
26398
- get: () => styleOf()[path],
26399
- onChange: (v) => actions.patch({ [`frvp.${path}`]: v })
26400
- }).el
26401
- }));
26402
- };
26403
- colorRow("Up Volume", "upColor");
26404
- colorRow("Down Volume", "downColor");
26405
- colorRow("Value Area Up", "vaUpColor");
26406
- colorRow("Value Area Down", "vaDownColor");
26407
- const styles = LINE_STYLE_OPTIONS.map((o) => ({ value: o.value, label: o.label }));
26408
- const levelRow = (label, showPath, colorPath, stylePath) => {
26409
- const row = document.createElement("div");
26410
- row.className = "vela-field-span";
26411
- row.style.cssText = "display:flex;align-items:center;gap:8px;";
26412
- const sw = buildFieldControl({
26413
- kind: "switch",
26414
- checked: Boolean(s[showPath]),
26415
- onChange: (v) => actions.patch({ [`frvp.${showPath}`]: v })
26416
- });
26417
- const lbl = document.createElement("span");
26418
- lbl.className = "vela-field-label";
26419
- lbl.style.flex = "1";
26420
- lbl.textContent = label;
26421
- let cur = s[colorPath] ?? contrastColor(this.theme.background);
26422
- const col = buildFieldControl({
26423
- kind: "color",
26424
- theme: this.theme,
26425
- get: () => cur,
26426
- onChange: (v) => {
26427
- cur = v;
26428
- actions.patch({ [`frvp.${colorPath}`]: v });
26429
- }
27575
+ title: `Level ${i + 1} color`,
27576
+ get: () => color,
27577
+ onChange: (v) => {
27578
+ color = v;
27579
+ mine({ [`levels.${i}.color`]: v });
27580
+ }
27581
+ });
27582
+ cell.append(on.el, num4.el, swatch.el);
27583
+ grid.appendChild(cell);
26430
27584
  });
26431
- const style = buildFieldControl({
27585
+ };
27586
+ b.watch(() => {
27587
+ if (!editing && key() !== last && !grid.contains(document.activeElement)) fill();
27588
+ });
27589
+ if (d instanceof FibRetracement) {
27590
+ rows.push(
27591
+ this.seg(
27592
+ b,
27593
+ "Level set",
27594
+ FIB_PRESETS.map((p) => ({ value: p.key, html: p.label })),
27595
+ () => {
27596
+ const f = b.live();
27597
+ return matchFibPreset(f.levels, f.reverse) ?? void 0;
27598
+ },
27599
+ (k) => {
27600
+ const preset = FIB_PRESETS.find((p) => p.key === k);
27601
+ b.edit({ levels: clonePlain(preset.levels), reverse: preset.reverse });
27602
+ },
27603
+ true
27604
+ )
27605
+ );
27606
+ }
27607
+ if (d instanceof MachFigure) rows.push(row("Ratios", this.switch(b, () => b.live().showRatios !== false, (v) => b.edit({ showRatios: v }))));
27608
+ rows.push(grid);
27609
+ if (d instanceof FibLevels) {
27610
+ const fib = () => b.live();
27611
+ const flags = [
27612
+ this.chip(b, "Ratios", () => fib().showRatios, (on) => b.edit({ showRatios: on })),
27613
+ this.chip(b, "Prices", () => fib().showPrices, (on) => b.edit({ showPrices: on })),
27614
+ this.chip(b, "Background", () => fib().background, (on) => b.edit({ background: on }))
27615
+ ];
27616
+ if (d instanceof FibRetracement) flags.push(this.chip(b, "Reverse", () => b.live().reverse, (on) => b.edit({ reverse: on })));
27617
+ rows.push(row("Show", chips(...flags)));
27618
+ rows.push(
27619
+ row(
27620
+ "Label side",
27621
+ this.seg(b, "Label side", [
27622
+ { value: "left", html: "Left" },
27623
+ { value: "right", html: "Right" }
27624
+ ], () => fib().labelSide, (v) => b.edit({ labelSide: v }))
27625
+ )
27626
+ );
27627
+ }
27628
+ return block("Levels", ...rows);
27629
+ }
27630
+ /** A rich per-type section on the shared field grid. */
27631
+ richBlock(title, fill) {
27632
+ const grid = fieldGrid({ variant: "inputs" });
27633
+ grid.classList.add("vela-dsp-grid");
27634
+ fill(grid);
27635
+ return block(title, grid);
27636
+ }
27637
+ /** A note, a callout, a text label: the words lead the panel. */
27638
+ textContentBlock(b, has) {
27639
+ return block(null, this.textArea(b, "Text", 3), row("Style", ...this.textStyling(b, has)));
27640
+ }
27641
+ /** A shape's optional label: folded to one line saying what it reads. */
27642
+ textFold(b, has) {
27643
+ const type = b.live().type;
27644
+ const words = () => b.live().text?.value?.trim() ?? "";
27645
+ const summary = () => {
27646
+ if (!has("text.value")) return sizeLabel(b.live().text?.size);
27647
+ const w = words();
27648
+ return w ? `\u201C${w.split("\n")[0]}\u201D` : "None";
27649
+ };
27650
+ return this.fold(b, "text", has("text.value") ? "Label" : "Text", summary, () => {
27651
+ const rows = [];
27652
+ if (has("text.value")) rows.push(this.textArea(b, "Add a label", 1));
27653
+ rows.push(row("Style", ...this.textStyling(b, has)));
27654
+ if (LINE_LABEL_TYPES.has(type) && has("text.value")) {
27655
+ const place = row(
27656
+ "Place",
27657
+ this.seg(b, "Position along the line", [
27658
+ { value: "start", html: "Start" },
27659
+ { value: "middle", html: "Middle" },
27660
+ { value: "end", html: "End" }
27661
+ ], () => b.live().text?.place ?? "middle", (v) => b.edit({ "text.place": v }), true)
27662
+ );
27663
+ const side = row(
27664
+ "Side",
27665
+ this.seg(b, "Side of the line", [
27666
+ { value: "above", html: "Above" },
27667
+ { value: "below", html: "Below" }
27668
+ ], () => b.live().text?.side ?? "above", (v) => b.edit({ "text.side": v }), true)
27669
+ );
27670
+ b.watch(() => place.hidden = side.hidden = words() === "");
27671
+ rows.push(place, side);
27672
+ }
27673
+ return rows;
27674
+ });
27675
+ }
27676
+ textArea(b, placeholder, rows) {
27677
+ const ta = new TextArea({
27678
+ value: b.live().text?.value ?? "",
27679
+ rows,
27680
+ size: "sm",
27681
+ autoGrow: true,
27682
+ maxLines: 6,
27683
+ placeholder,
27684
+ onChange: (v) => b.edit({ "text.value": v })
27685
+ });
27686
+ ta.input.addEventListener("keydown", (e) => {
27687
+ if (e.key !== "Escape") e.stopPropagation();
27688
+ });
27689
+ b.watch(() => {
27690
+ const v = b.live().text?.value ?? "";
27691
+ if (document.activeElement !== ta.input && ta.input.value !== v) ta.input.value = v;
27692
+ });
27693
+ ta.el.style.width = "100%";
27694
+ return ta.el;
27695
+ }
27696
+ textStyling(b, has) {
27697
+ const out = [];
27698
+ if (has("text.color")) out.push(this.swatch(b, "Text color", () => b.live().text?.color || this.theme.textColor, "text.color"));
27699
+ if (has("text.size")) {
27700
+ const sel = buildFieldControl({
26432
27701
  kind: "select",
26433
- options: styles,
26434
- value: s[stylePath],
27702
+ options: TEXT_SIZE_OPTIONS,
27703
+ value: b.live().text?.size ?? "normal",
26435
27704
  fill: false,
26436
27705
  theme: this.theme,
26437
- onChange: (v) => actions.patch({ [`frvp.${stylePath}`]: v })
27706
+ onChange: (v) => b.edit({ "text.size": v })
26438
27707
  });
26439
- row.append(sw.el, lbl, col.el, style.el);
26440
- grid.appendChild(row);
27708
+ b.watch(() => sel.setValue?.(b.live().text?.size ?? "normal"));
27709
+ out.push(sel.el);
27710
+ }
27711
+ const flags = [];
27712
+ if (has("text.bold")) flags.push(this.chip(b, "B", () => !!b.live().text?.bold, (on) => b.edit({ "text.bold": on }), "Bold"));
27713
+ if (has("text.italic")) flags.push(this.chip(b, "I", () => !!b.live().text?.italic, (on) => b.edit({ "text.italic": on }), "Italic"));
27714
+ if (flags.length) out.push(chips(...flags));
27715
+ return out;
27716
+ }
27717
+ /** Each anchor's price (editable) and time. */
27718
+ pointsFold(b) {
27719
+ const d = b.live();
27720
+ if (NO_POINTS.has(d.type) || d.anchors.length === 0 || d.anchors.length > MAX_POINTS) return null;
27721
+ const slots = d.anchorSchema().slots;
27722
+ const free = (i) => slots[i]?.free ?? slots[slots.length - 1]?.free ?? "both";
27723
+ const shown = d.anchors.map((_, i) => i).filter((i) => free(i) !== "none");
27724
+ if (shown.length === 0) return null;
27725
+ const priced = shown.filter((i) => free(i) !== "x");
27726
+ const shownPrice = (n) => {
27727
+ const dec = this.env.priceDecimals() ?? Math.max(2, valueDecimals(n));
27728
+ return Number(n.toFixed(Math.min(dec, 10)));
26441
27729
  };
26442
- levelRow("VAH", "showVah", "vahColor", "vahStyle");
26443
- levelRow("VAL", "showVal", "valColor", "valStyle");
26444
- levelRow("POC", "showPoc", "pocColor", "pocStyle");
26445
- levelRow("Developing POC", "showDevelopingPoc", "developingPocColor", "developingPocStyle");
26446
- levelRow("Developing VA", "showDevelopingVa", "developingVaColor", "developingVaStyle");
26447
- }
26448
- buildLevels(grid, drawing, actions) {
26449
- const levels = drawing.editableLevels();
26450
- if (!levels) return;
26451
- const isMach = drawing instanceof MachFigure;
26452
- if (isMach) {
26453
- const mach = drawing;
26454
- grid.appendChild(fieldRow({
26455
- label: "Show ratio labels",
26456
- bool: true,
26457
- toggle: {
26458
- checked: mach.showRatios !== false,
26459
- onChange: (v) => actions.patch({ showRatios: v })
27730
+ const fmt = (n) => String(shownPrice(n));
27731
+ const summary = () => {
27732
+ const a = b.live().anchors;
27733
+ if (priced.length) return priced.map((i) => fmt(a[i]?.price ?? 0)).join(" \u2192 ");
27734
+ const t = a[shown[0]]?.time;
27735
+ return shown.length === 1 && t != null ? formatTimeStamp(t, this.env.timeZone(), this.env.chartBarMs()) : `${shown.length} points`;
27736
+ };
27737
+ return this.fold(
27738
+ b,
27739
+ "points",
27740
+ "Points",
27741
+ summary,
27742
+ () => shown.map((i, n) => {
27743
+ const key = shown.length === 1 ? free(i) === "x" ? "Time" : "Price" : String.fromCharCode(65 + n);
27744
+ const line = document.createElement("div");
27745
+ line.className = "vela-dsp-pt";
27746
+ const k = document.createElement("span");
27747
+ k.className = "vela-dsp-pt-k";
27748
+ k.textContent = key;
27749
+ line.appendChild(k);
27750
+ if (free(i) !== "x") {
27751
+ const ni = buildFieldControl({
27752
+ kind: "number",
27753
+ value: shownPrice(d.anchors[i].price),
27754
+ fill: true,
27755
+ commit: "blur",
27756
+ title: `Point ${key} price`,
27757
+ onChange: (v) => b.edit({ [`anchors.${i}.price`]: v })
27758
+ });
27759
+ const input = ni.el.querySelector("input");
27760
+ b.watch(() => {
27761
+ if (document.activeElement !== input) ni.setValue?.(shownPrice(b.live().anchors[i]?.price ?? 0));
27762
+ });
27763
+ line.appendChild(ni.el);
27764
+ } else {
27765
+ line.appendChild(document.createElement("span"));
26460
27766
  }
26461
- }));
26462
- }
26463
- if (drawing instanceof FibRetracement) {
26464
- const fib = drawing;
26465
- grid.appendChild(fieldRow({
26466
- label: "Reverse",
26467
- bool: true,
26468
- toggle: {
26469
- checked: fib.reverse,
26470
- onChange: (v) => actions.patch({ reverse: v })
27767
+ const t = document.createElement("span");
27768
+ t.className = "vela-dsp-pt-t";
27769
+ b.watch(() => {
27770
+ const time = b.live().anchors[i]?.time;
27771
+ t.textContent = time != null ? formatTimeStamp(time, this.env.timeZone(), this.env.chartBarMs()) : "";
27772
+ });
27773
+ line.appendChild(t);
27774
+ return line;
27775
+ })
27776
+ );
27777
+ }
27778
+ /** The timeframes the drawing shows on: one line while it's every timeframe, the band
27779
+ * picker only once the user goes custom. */
27780
+ showOnFold(b) {
27781
+ const bands = () => b.live().showOn;
27782
+ let pickedCustom = false;
27783
+ const mode = () => pickedCustom && bands() ? "custom" : modeOf(bands());
27784
+ const here = timeframeBandOf(this.env.chartBarMs());
27785
+ const summary = () => {
27786
+ const s = bands();
27787
+ if (!s) return "All timeframes";
27788
+ if (mode() === "intraday") return "Intraday";
27789
+ if (mode() === "higher") return "1H and up";
27790
+ if (s.length === 0) return "Nowhere";
27791
+ return s.map((k) => TIMEFRAME_BANDS.find((t) => t.key === k)?.label ?? k).join(", ");
27792
+ };
27793
+ return this.fold(b, "show", "Show on", summary, () => {
27794
+ const set = (next) => b.edit({ showOn: next });
27795
+ const pick = this.seg(
27796
+ b,
27797
+ "Show on",
27798
+ [
27799
+ { value: "all", html: "All" },
27800
+ { value: "intraday", html: "Intraday" },
27801
+ { value: "higher", html: "1H and up" },
27802
+ { value: "custom", html: "Custom" }
27803
+ ],
27804
+ mode,
27805
+ (v) => {
27806
+ pickedCustom = v === "custom";
27807
+ if (v === "all") set(void 0);
27808
+ else if (v === "intraday") set([...INTRADAY]);
27809
+ else if (v === "higher") set([...HIGHER]);
27810
+ else set([...bands() ?? TIMEFRAME_BANDS.map((t) => t.key)]);
27811
+ },
27812
+ true
27813
+ );
27814
+ const grid = document.createElement("div");
27815
+ grid.className = "vela-dsp-tf";
27816
+ for (const band of TIMEFRAME_BANDS) {
27817
+ const btn = document.createElement("button");
27818
+ btn.type = "button";
27819
+ btn.textContent = band.label;
27820
+ if (band.key === here) {
27821
+ btn.dataset.here = "";
27822
+ btn.title = "The chart's timeframe";
26471
27823
  }
26472
- }));
26473
- }
26474
- levels.forEach((lv, i) => {
26475
- const row = document.createElement("div");
26476
- row.className = "vela-field-span";
26477
- row.style.cssText = "display:flex;align-items:center;gap:8px;";
26478
- const sw = buildFieldControl({
26479
- kind: "switch",
26480
- checked: lv.enabled,
26481
- onChange: (v) => actions.patch({ [`levels.${i}.enabled`]: v })
27824
+ btn.addEventListener("click", () => {
27825
+ const cur = new Set(bands() ?? []);
27826
+ if (cur.has(band.key)) cur.delete(band.key);
27827
+ else cur.add(band.key);
27828
+ set(TIMEFRAME_BANDS.map((t) => t.key).filter((k) => cur.has(k)));
27829
+ });
27830
+ b.watch(() => btn.setAttribute("aria-pressed", String(bands()?.includes(band.key) ?? true)));
27831
+ grid.appendChild(btn);
27832
+ }
27833
+ const note = document.createElement("div");
27834
+ note.className = "vela-dsp-note";
27835
+ b.watch(() => {
27836
+ grid.hidden = mode() !== "custom";
27837
+ const s = bands();
27838
+ const hidden = s != null && here != null && !s.includes(here);
27839
+ note.hidden = !hidden;
27840
+ note.textContent = s?.length === 0 ? "Hidden on every timeframe." : "Hidden on this chart's timeframe.";
26482
27841
  });
26483
- let curC = lv.color;
26484
- const col = buildFieldControl({
26485
- kind: "color",
26486
- theme: this.theme,
26487
- get: () => curC,
26488
- onChange: (v) => {
26489
- curC = v;
26490
- actions.patch({ [`levels.${i}.color`]: v });
27842
+ return [pick, grid, note];
27843
+ });
27844
+ }
27845
+ // ── the style menu ──
27846
+ /** The header's style menu: the tool's ready-made and saved looks (hover to preview, click
27847
+ * to apply), saving the current look, and going back to the tool's defaults. */
27848
+ styleMenuButton(b) {
27849
+ const btn = document.createElement("button");
27850
+ btn.type = "button";
27851
+ btn.className = "vela-dsp-style";
27852
+ btn.setAttribute("aria-haspopup", "menu");
27853
+ btn.setAttribute("aria-expanded", "false");
27854
+ const label = document.createElement("span");
27855
+ const chev = document.createElement("span");
27856
+ chev.style.display = "flex";
27857
+ chev.innerHTML = CHEVRON_DOWN;
27858
+ btn.append(label, chev);
27859
+ const all = () => [...builtinToolTemplates(b.live().type), ...b.actions.templates?.() ?? []];
27860
+ const refresh = () => {
27861
+ const match = all().filter((t) => matchesTemplate(b.live(), t.settings)).pop();
27862
+ label.textContent = match?.name ?? "Style";
27863
+ btn.title = match ? `Style: ${match.name}` : "Styles";
27864
+ };
27865
+ btn.addEventListener("pointerdown", (e) => e.stopPropagation());
27866
+ btn.addEventListener("click", (e) => {
27867
+ e.stopPropagation();
27868
+ if (this.menu) {
27869
+ this.menu.hide();
27870
+ return;
27871
+ }
27872
+ this.openStyleMenu(btn, b, all, refresh);
27873
+ });
27874
+ refresh();
27875
+ return { el: btn, refresh };
27876
+ }
27877
+ openStyleMenu(anchor, b, all, refresh) {
27878
+ closeOpenPopovers();
27879
+ const t = this.theme;
27880
+ const pop = new Popover({
27881
+ trigger: anchor,
27882
+ host: this.host,
27883
+ theme: t,
27884
+ position: "absolute",
27885
+ boundary: this.host,
27886
+ boundaryInset: 4,
27887
+ viewportInset: 0,
27888
+ gap: 6,
27889
+ align: "end",
27890
+ zIndex: 6e3,
27891
+ onClose: () => {
27892
+ b.actions.preview?.(null);
27893
+ anchor.setAttribute("aria-expanded", "false");
27894
+ if (this.menu === pop) this.menu = null;
27895
+ }
27896
+ });
27897
+ const el = pop.el;
27898
+ el.classList.add("vela-dsp-menu");
27899
+ el.setAttribute("role", "menu");
27900
+ 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;`;
27901
+ applyChromeTokens(el, t);
27902
+ const fill = () => {
27903
+ el.replaceChildren();
27904
+ const builtins = builtinToolTemplates(b.live().type);
27905
+ const saved = b.actions.templates?.() ?? [];
27906
+ const item = (tpl, removable) => {
27907
+ const row2 = document.createElement("button");
27908
+ row2.type = "button";
27909
+ row2.className = "vela-dsp-mi";
27910
+ row2.setAttribute("role", "menuitem");
27911
+ if (matchesTemplate(b.live(), tpl.settings)) row2.dataset.current = "";
27912
+ const dot = document.createElement("span");
27913
+ dot.className = "vela-dsp-mi-dot";
27914
+ dot.style.background = templateColor(tpl.settings) ?? "transparent";
27915
+ const name = document.createElement("span");
27916
+ name.className = "vela-dsp-mi-t";
27917
+ name.textContent = tpl.name;
27918
+ row2.append(dot, name);
27919
+ if (removable) {
27920
+ const x = document.createElement("span");
27921
+ x.className = "vela-dsp-mi-x";
27922
+ x.setAttribute("role", "button");
27923
+ x.setAttribute("aria-label", `Delete ${tpl.name}`);
27924
+ x.innerHTML = CROSS;
27925
+ x.addEventListener("click", (e) => {
27926
+ e.stopPropagation();
27927
+ b.actions.preview?.(null);
27928
+ b.actions.removeTemplate?.(tpl.name);
27929
+ fill();
27930
+ refresh();
27931
+ });
27932
+ row2.appendChild(x);
26491
27933
  }
27934
+ row2.addEventListener("pointerenter", () => b.actions.preview?.(tpl.settings));
27935
+ row2.addEventListener("pointerleave", () => b.actions.preview?.(null));
27936
+ row2.addEventListener("click", () => {
27937
+ b.actions.preview?.(null);
27938
+ b.actions.applyToolSettings?.(tpl.settings);
27939
+ pop.hide();
27940
+ b.rebuild();
27941
+ });
27942
+ return row2;
27943
+ };
27944
+ if (builtins.length) {
27945
+ el.appendChild(menuHeading("Presets"));
27946
+ for (const tpl of builtins) el.appendChild(item(tpl, false));
27947
+ }
27948
+ if (saved.length) {
27949
+ el.appendChild(menuHeading("Saved"));
27950
+ for (const tpl of saved) el.appendChild(item(tpl, true));
27951
+ }
27952
+ if (builtins.length || saved.length) el.appendChild(separator());
27953
+ if (b.actions.saveTemplate) el.appendChild(this.saveRow(b, fill, refresh));
27954
+ const reset = document.createElement("button");
27955
+ reset.type = "button";
27956
+ reset.className = "vela-dsp-mi";
27957
+ reset.setAttribute("role", "menuitem");
27958
+ reset.innerHTML = '<span class="vela-dsp-mi-t">Reset to default</span>';
27959
+ reset.addEventListener("click", () => {
27960
+ b.actions.resetSettings();
27961
+ pop.hide();
27962
+ b.rebuild();
26492
27963
  });
26493
- let curRatio = lv.ratio;
26494
- const ratio = buildFieldControl({
26495
- kind: "number",
26496
- value: curRatio,
26497
- min: 0,
26498
- step: 0.01,
26499
- fill: isMach,
26500
- compact: !isMach,
26501
- commit: "blur",
26502
- onChange: (n) => {
26503
- if (n <= 0) {
26504
- ratio.setValue?.(curRatio);
26505
- return;
26506
- }
26507
- curRatio = n;
26508
- actions.patch({ [`levels.${i}.ratio`]: n });
26509
- }
27964
+ el.appendChild(reset);
27965
+ };
27966
+ fill();
27967
+ anchor.setAttribute("aria-expanded", "true");
27968
+ this.menu = pop;
27969
+ pop.show();
27970
+ }
27971
+ /** "Save as style…": a menu row that turns into a name field. */
27972
+ saveRow(b, refill, refresh) {
27973
+ const wrap = document.createElement("div");
27974
+ const start = document.createElement("button");
27975
+ start.type = "button";
27976
+ start.className = "vela-dsp-mi";
27977
+ start.setAttribute("role", "menuitem");
27978
+ start.innerHTML = '<span class="vela-dsp-mi-t">Save as style\u2026</span>';
27979
+ start.addEventListener("click", (e) => {
27980
+ e.stopPropagation();
27981
+ const form = document.createElement("div");
27982
+ form.className = "vela-dsp-mname";
27983
+ const input = document.createElement("input");
27984
+ input.type = "text";
27985
+ input.placeholder = "Style name";
27986
+ input.maxLength = 60;
27987
+ input.setAttribute("aria-label", "Style name");
27988
+ const save = button("Save", true, () => commit());
27989
+ save.style.height = "28px";
27990
+ const commit = () => {
27991
+ const name = input.value.trim();
27992
+ if (!name) return;
27993
+ b.actions.saveTemplate?.(name);
27994
+ refill();
27995
+ refresh();
27996
+ };
27997
+ input.addEventListener("keydown", (ev) => {
27998
+ ev.stopPropagation();
27999
+ if (ev.key === "Enter") commit();
28000
+ if (ev.key === "Escape") refill();
26510
28001
  });
26511
- if (isMach) ratio.el.style.flex = "1";
26512
- row.append(sw.el, col.el, ratio.el);
26513
- if (!isMach) {
26514
- const label = buildFieldControl({
26515
- kind: "text",
26516
- value: lv.label ?? "",
26517
- fill: true,
26518
- placeholder: "label\u2026",
26519
- onChange: (v) => actions.patch({ [`levels.${i}.label`]: v })
26520
- });
26521
- label.el.style.flex = "1";
26522
- label.el.style.minWidth = "60px";
26523
- row.appendChild(label.el);
28002
+ form.append(input, save);
28003
+ wrap.replaceChildren(form);
28004
+ input.focus();
28005
+ });
28006
+ wrap.appendChild(start);
28007
+ return wrap;
28008
+ }
28009
+ // ── controls ──
28010
+ /** A fold: a one-line header saying what's set, opening to its rows. */
28011
+ fold(b, key, title, summary, body) {
28012
+ const type = b.live().type;
28013
+ const open2 = openFolds.get(type) ?? /* @__PURE__ */ new Set();
28014
+ openFolds.set(type, open2);
28015
+ const wrap = document.createElement("div");
28016
+ wrap.className = "vela-dsp-fold";
28017
+ const head = document.createElement("button");
28018
+ head.type = "button";
28019
+ head.className = "vela-dsp-fold-h";
28020
+ const t = document.createElement("span");
28021
+ t.className = "vela-dsp-fold-t";
28022
+ t.textContent = title;
28023
+ const s = document.createElement("span");
28024
+ s.className = "vela-dsp-fold-s";
28025
+ const chev = document.createElement("span");
28026
+ chev.className = "vela-dsp-chev";
28027
+ chev.innerHTML = CHEVRON;
28028
+ head.append(t, s, chev);
28029
+ const content = document.createElement("div");
28030
+ content.className = "vela-dsp-fold-b";
28031
+ let built = false;
28032
+ const setOpen = (on) => {
28033
+ if (on) open2.add(key);
28034
+ else open2.delete(key);
28035
+ wrap.toggleAttribute("data-open", on);
28036
+ head.setAttribute("aria-expanded", String(on));
28037
+ if (on && !built) {
28038
+ built = true;
28039
+ content.append(...body());
26524
28040
  }
26525
- grid.appendChild(row);
28041
+ };
28042
+ head.addEventListener("click", () => setOpen(!open2.has(key)));
28043
+ b.watch(() => s.textContent = summary());
28044
+ setOpen(open2.has(key));
28045
+ wrap.append(head, content);
28046
+ return wrap;
28047
+ }
28048
+ seg(b, label, options, read, pick, full = false) {
28049
+ const el = document.createElement("div");
28050
+ el.className = "vela-dsp-seg";
28051
+ el.setAttribute("role", "group");
28052
+ el.setAttribute("aria-label", label);
28053
+ if (full) el.dataset.full = "";
28054
+ const buttons = options.map((o) => {
28055
+ const btn = document.createElement("button");
28056
+ btn.type = "button";
28057
+ btn.innerHTML = o.html;
28058
+ if (o.title) {
28059
+ btn.title = o.title;
28060
+ btn.setAttribute("aria-label", o.title);
28061
+ }
28062
+ btn.addEventListener("click", () => {
28063
+ pick(o.value);
28064
+ sync();
28065
+ });
28066
+ el.appendChild(btn);
28067
+ return btn;
28068
+ });
28069
+ const sync = () => {
28070
+ const cur = read();
28071
+ options.forEach((o, i) => buttons[i].setAttribute("aria-pressed", String(o.value === cur)));
28072
+ };
28073
+ b.watch(sync);
28074
+ return el;
28075
+ }
28076
+ chip(b, text, read, set, title) {
28077
+ const btn = document.createElement("button");
28078
+ btn.type = "button";
28079
+ btn.className = "vela-dsp-chip";
28080
+ btn.textContent = text;
28081
+ if (title) {
28082
+ btn.title = title;
28083
+ btn.setAttribute("aria-label", title);
28084
+ }
28085
+ if (text === "B") btn.style.fontWeight = "700";
28086
+ if (text === "I") btn.style.fontStyle = "italic";
28087
+ btn.addEventListener("click", () => set(!read()));
28088
+ b.watch(() => btn.setAttribute("aria-pressed", String(read())));
28089
+ return btn;
28090
+ }
28091
+ swatch(b, title, read, path) {
28092
+ const ctrl = buildFieldControl({ kind: "color", theme: this.theme, title, get: read, onChange: (v) => b.edit({ [path]: v }) });
28093
+ return ctrl.el;
28094
+ }
28095
+ switch(b, read, set) {
28096
+ const sw = buildFieldControl({ kind: "switch", checked: read(), onChange: set });
28097
+ b.watch(() => sw.setValue?.(read()));
28098
+ const wrap = document.createElement("div");
28099
+ wrap.style.cssText = "display:flex;flex:1;justify-content:flex-end;";
28100
+ wrap.appendChild(sw.el);
28101
+ return wrap;
28102
+ }
28103
+ number(b, title, read, path, f) {
28104
+ const step = f.step ?? 1;
28105
+ const ni = buildFieldControl({
28106
+ kind: "number",
28107
+ value: read(),
28108
+ min: f.min,
28109
+ max: f.max,
28110
+ step,
28111
+ integer: step >= 1 && Number.isInteger(step),
28112
+ fill: false,
28113
+ commit: "blur",
28114
+ title,
28115
+ onChange: (v) => b.edit({ [path]: v })
28116
+ });
28117
+ const input = ni.el.querySelector("input");
28118
+ b.watch(() => {
28119
+ if (document.activeElement !== input) ni.setValue?.(read());
26526
28120
  });
28121
+ return ni.el;
26527
28122
  }
26528
28123
  };
26529
- function btn(label, primary, onClick) {
28124
+ function block(title, ...rows) {
28125
+ const el = document.createElement("div");
28126
+ el.className = "vela-dsp-block";
28127
+ if (title) {
28128
+ const h = document.createElement("div");
28129
+ h.className = "vela-dsp-h";
28130
+ h.textContent = title;
28131
+ el.appendChild(h);
28132
+ }
28133
+ el.append(...rows);
28134
+ return el;
28135
+ }
28136
+ function row(label, ...controls) {
28137
+ const el = document.createElement("div");
28138
+ el.className = "vela-dsp-row";
28139
+ const lab = document.createElement("span");
28140
+ lab.className = "vela-dsp-lab";
28141
+ lab.textContent = label;
28142
+ lab.title = label;
28143
+ el.append(lab, ...controls);
28144
+ return el;
28145
+ }
28146
+ function chips(...items) {
28147
+ const el = document.createElement("div");
28148
+ el.className = "vela-dsp-chips";
28149
+ el.append(...items);
28150
+ return el;
28151
+ }
28152
+ function menuHeading(text) {
28153
+ const el = document.createElement("div");
28154
+ el.className = "vela-dsp-mh";
28155
+ el.textContent = text;
28156
+ return el;
28157
+ }
28158
+ function separator() {
28159
+ const el = document.createElement("div");
28160
+ el.className = "vela-dsp-msep";
28161
+ return el;
28162
+ }
28163
+ function button(label, primary, onClick) {
26530
28164
  const b = document.createElement("button");
26531
28165
  b.type = "button";
26532
28166
  b.textContent = label;
@@ -26534,6 +28168,50 @@ function btn(label, primary, onClick) {
26534
28168
  b.addEventListener("click", onClick);
26535
28169
  return b;
26536
28170
  }
28171
+ function sizeLabel(size) {
28172
+ return TEXT_SIZE_OPTIONS.find((o) => o.value === (size ?? "normal"))?.label ?? "Normal";
28173
+ }
28174
+ function readPath(d, path) {
28175
+ let cur = d;
28176
+ for (const k of path.split(".")) {
28177
+ if (cur == null || typeof cur !== "object") return void 0;
28178
+ cur = cur[k];
28179
+ }
28180
+ return cur;
28181
+ }
28182
+ function pairBase(colorLabel, styleLabel3) {
28183
+ const c = /^(.*) colou?r$/i.exec(colorLabel);
28184
+ const st = /^(.*) style$/i.exec(styleLabel3);
28185
+ return c && st && c[1] === st[1] ? c[1] : null;
28186
+ }
28187
+ function upperLowerBase(upper, lower) {
28188
+ const u = /^Upper (.+)$/i.exec(upper);
28189
+ const l = /^Lower (.+)$/i.exec(lower);
28190
+ if (!u || !l || u[1] !== l[1]) return null;
28191
+ const name = u[1].replace(/ colou?r$/i, "");
28192
+ const word = name.charAt(0).toUpperCase() + name.slice(1);
28193
+ return /fill$/i.test(word) ? word : `${word}s`;
28194
+ }
28195
+ function modeOf(showOn) {
28196
+ if (!showOn) return "all";
28197
+ const same = (a) => a.length === showOn.length && a.every((k) => showOn.includes(k));
28198
+ if (same(INTRADAY)) return "intraday";
28199
+ if (same(HIGHER)) return "higher";
28200
+ return "custom";
28201
+ }
28202
+ function matchesTemplate(d, s) {
28203
+ const same = (a, b) => JSON.stringify(a ?? null) === JSON.stringify(b ?? null);
28204
+ const style = d.style;
28205
+ for (const [k, v] of Object.entries(s.style ?? {})) if (!same(style[k], v)) return false;
28206
+ const text = d.text ?? {};
28207
+ for (const [k, v] of Object.entries(s.text ?? {})) if (!same(text[k], v)) return false;
28208
+ const props = d.serialize().props ?? {};
28209
+ for (const [k, v] of Object.entries(s.props ?? {})) if (!same(props[k], v)) return false;
28210
+ return Object.keys(s.style ?? {}).length + Object.keys(s.text ?? {}).length + Object.keys(s.props ?? {}).length > 0;
28211
+ }
28212
+ function templateColor(s) {
28213
+ return s.style?.lineColor ?? s.style?.fillColor ?? s.text?.color;
28214
+ }
26537
28215
 
26538
28216
  // src/renderers/native/drawings/DrawingSettingsPopup.ts
26539
28217
  var MIXED = /* @__PURE__ */ Symbol("mixed");
@@ -26555,15 +28233,15 @@ function distinctValues(drawings, read) {
26555
28233
  }
26556
28234
  var BTN = 30;
26557
28235
  var ICON = 21;
26558
- var STYLE_ID3 = "vela-drawing-popup-styles";
26559
- var STYLE_REV = "4";
26560
- function ensureStyles3() {
28236
+ var STYLE_ID4 = "vela-drawing-popup-styles";
28237
+ var STYLE_REV2 = "4";
28238
+ function ensureStyles4() {
26561
28239
  if (typeof document === "undefined") return;
26562
- const existing = document.getElementById(STYLE_ID3);
26563
- if (existing?.dataset.rev === STYLE_REV) return;
28240
+ const existing = document.getElementById(STYLE_ID4);
28241
+ if (existing?.dataset.rev === STYLE_REV2) return;
26564
28242
  const s = existing ?? document.createElement("style");
26565
- s.id = STYLE_ID3;
26566
- s.dataset.rev = STYLE_REV;
28243
+ s.id = STYLE_ID4;
28244
+ s.dataset.rev = STYLE_REV2;
26567
28245
  s.textContent = `
26568
28246
  .vela-dpop-btn{background:transparent;color:var(--vela-fg-muted);transition:background var(--vela-dur-fast) ease,color var(--vela-dur-fast) ease;}
26569
28247
  .vela-dpop-btn:hover{background:var(--vela-hover-strong);color:var(--vela-fg-bright);}
@@ -26579,13 +28257,14 @@ function ensureStyles3() {
26579
28257
  if (!existing) document.head.appendChild(s);
26580
28258
  }
26581
28259
  var DrawingSettingsPopup = class {
26582
- constructor(host, theme, chartBarMs = () => 0) {
28260
+ constructor(host, theme, env = {}) {
26583
28261
  this.host = host;
26584
- this.chartBarMs = chartBarMs;
26585
28262
  this.el = null;
26586
28263
  this.tipEl = null;
26587
28264
  // floating hover-label (above/below the toolbar)
26588
28265
  this.textPanel = null;
28266
+ /** The single drawing the bar is open on (a multi-selection has no settings panel). */
28267
+ this.current = null;
26589
28268
  this.colorPop = null;
26590
28269
  this.colorOwner = null;
26591
28270
  this.menuPop = null;
@@ -26593,9 +28272,10 @@ var DrawingSettingsPopup = class {
26593
28272
  this.onClose = null;
26594
28273
  this.onOutside = (e) => {
26595
28274
  const node = e.target;
26596
- if (this.settingsDialog.isOpen()) {
26597
- if (this.settingsDialog.contains(node) || this.isOwnChrome(node)) return;
26598
- this.settingsDialog.close();
28275
+ if (this.panel.isOpen()) {
28276
+ if (this.panel.contains(node) || this.isOwnChrome(node)) return;
28277
+ if (this.host.contains(node)) return;
28278
+ this.panel.close();
26599
28279
  return;
26600
28280
  }
26601
28281
  if (this.isOwnChrome(node)) return;
@@ -26604,19 +28284,16 @@ var DrawingSettingsPopup = class {
26604
28284
  cb?.(e);
26605
28285
  };
26606
28286
  this.theme = theme;
26607
- this.settingsDialog = new DrawingSettingsDialog(host, theme);
26608
- }
26609
- /** The magnifier timeframe choices strictly below the chart's own bar duration
26610
- * (`auto` rides along while at least one concrete lower step exists). */
26611
- lowerTimeframeOptions() {
26612
- const chartMs = this.chartBarMs();
26613
- if (!(chartMs > 0)) return [...MAGNIFIER_TIMEFRAME_OPTIONS];
26614
- const lower = MAGNIFIER_TIMEFRAME_OPTIONS.filter((o) => o.ms > 0 && o.ms < chartMs);
26615
- return lower.length > 0 ? [MAGNIFIER_TIMEFRAME_OPTIONS[0], ...lower] : [];
28287
+ this.chartBarMs = env.chartBarMs ?? (() => 0);
28288
+ this.panel = new DrawingSettingsPanel(host, theme, {
28289
+ chartBarMs: this.chartBarMs,
28290
+ timeZone: env.timeZone ?? (() => "UTC"),
28291
+ priceDecimals: env.priceDecimals ?? (() => null)
28292
+ });
26616
28293
  }
26617
28294
  setTheme(theme) {
26618
28295
  this.theme = theme;
26619
- this.settingsDialog.setTheme(theme);
28296
+ this.panel.setTheme(theme);
26620
28297
  }
26621
28298
  isOpen() {
26622
28299
  return this.el != null;
@@ -26624,7 +28301,22 @@ var DrawingSettingsPopup = class {
26624
28301
  /** Whether `node` belongs to this popup — the bar, its host-floated color/menu shells,
26625
28302
  * or a kit popover (select list / color chip) portaled into the chart host. */
26626
28303
  contains(node) {
26627
- return node != null && (this.isOwnChrome(node) || this.settingsDialog.contains(node));
28304
+ return node != null && (this.isOwnChrome(node) || this.panel.contains(node));
28305
+ }
28306
+ /** Open the full settings panel for the drawing the bar is open on. False when there is
28307
+ * none (no bar, or a multi-selection). */
28308
+ openPanel() {
28309
+ const cur = this.current;
28310
+ if (!cur) return false;
28311
+ this.hideTip();
28312
+ this.closeColorPopover();
28313
+ this.closeMenu();
28314
+ this.panel.open(cur.drawing, cur.actions, cur.anchor);
28315
+ return true;
28316
+ }
28317
+ /** The drawings changed under the panel (a drag, an undo) — let it re-read them. */
28318
+ syncPanel() {
28319
+ this.panel.sync();
26628
28320
  }
26629
28321
  /** Open the quick toolbar for `drawings` (one, or a whole multi-selection), floating clear of
26630
28322
  * their `anchor` box. With several drawings the bar shows only the controls they ALL support,
@@ -26634,10 +28326,12 @@ var DrawingSettingsPopup = class {
26634
28326
  * programmatic close), with the dismissing press when there is one, so the caller can tell a
26635
28327
  * modifier press (multi-select) from a plain one. */
26636
28328
  open(drawings, anchor, actions, onClose) {
26637
- this.close();
26638
28329
  const drawing = drawings[0];
28330
+ const keepPanel = this.panel.isOpen() && drawings.length === 1 && drawing != null && this.current?.drawing.id === drawing.id;
28331
+ this.close(keepPanel);
26639
28332
  if (!drawing) return;
26640
- ensureStyles3();
28333
+ this.current = drawings.length === 1 ? { drawing, actions, anchor } : null;
28334
+ ensureStyles4();
26641
28335
  this.onClose = onClose ?? null;
26642
28336
  const t = this.theme;
26643
28337
  const multi = drawings.length > 1;
@@ -26665,11 +28359,11 @@ var DrawingSettingsPopup = class {
26665
28359
  }
26666
28360
  if (paths.has("size")) {
26667
28361
  const sz = common((d) => d.size ?? "normal");
26668
- bar.appendChild(this.dropdown("Icon size", STAMP_SIZE_OPTIONS, sz, (s) => stampSizeIcon(s), (v) => actions.patch({ size: v }), { label: sizeLabel }));
28362
+ bar.appendChild(this.dropdown("Icon size", STAMP_SIZE_OPTIONS, sz, (s) => stampSizeIcon(s), (v) => actions.patch({ size: v }), { label: sizeLabel2 }));
26669
28363
  }
26670
28364
  if (paths.has("magnifier.timeframe") && every((d) => d instanceof Magnifier)) {
26671
28365
  const mag = (d) => d;
26672
- const options = this.lowerTimeframeOptions();
28366
+ const options = magnifierTimeframesBelow(this.chartBarMs());
26673
28367
  if (options.length > 0) {
26674
28368
  bar.appendChild(
26675
28369
  this.dropdown("Lower timeframe", options.map((o) => o.value), common((d) => mag(d).magnifier.timeframe), () => "", (v) => actions.patch({ "magnifier.timeframe": v }), {
@@ -26698,7 +28392,6 @@ var DrawingSettingsPopup = class {
26698
28392
  }
26699
28393
  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 }));
26700
28394
  if (paths.has("style.fillColor")) bar.appendChild(swatch("Fill", BUCKET_ICON, (d) => effectiveFillColor(d, this.theme) ?? d.style.fillColor ?? DEFAULT_DRAWING_COLOR, "style.fillColor"));
26701
- const isFrvp = paths.has("frvp.rows") && drawing instanceof FixedRangeVolumeProfile;
26702
28395
  const isPosition = paths.has("riskPercent") && every((d) => d instanceof PositionTool);
26703
28396
  if (isPosition) {
26704
28397
  const pos = (d) => d;
@@ -26767,7 +28460,7 @@ var DrawingSettingsPopup = class {
26767
28460
  if (paths.has("showPrice")) bar.appendChild(this.toggle("Show price", PRICE_DELTA_ICON, common((d) => flags(d).showPrice !== false), (v) => actions.patch({ showPrice: v })));
26768
28461
  if (paths.has("showDate")) bar.appendChild(this.toggle("Show date", DATE_DELTA_ICON, common((d) => flags(d).showDate !== false), (v) => actions.patch({ showDate: v })));
26769
28462
  if (paths.has("text.color") && textOnBar) bar.appendChild(swatch("Text color", TYPE_ICON, (d) => d.text?.color || t.textColor, "text.color"));
26770
- 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 }));
28463
+ 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 }));
26771
28464
  const textField = !multi && paths.has("text.value");
26772
28465
  if (paths.has("text.bold") && !textField) bar.appendChild(this.toggle("Bold", BOLD_ICON, common((d) => !!d.text?.bold), (v) => actions.patch({ "text.bold": v })));
26773
28466
  if (paths.has("text.italic") && !textField) bar.appendChild(this.toggle("Italic", ITALIC_ICON, common((d) => !!d.text?.italic), (v) => actions.patch({ "text.italic": v })));
@@ -26776,13 +28469,11 @@ var DrawingSettingsPopup = class {
26776
28469
  if (editableLevels && every((d) => d.editableLevels() != null && !(d instanceof MachFigure))) {
26777
28470
  const fib = (d) => d;
26778
28471
  const sizes = TEXT_SIZE_OPTIONS.map((o) => o.value);
26779
- bar.appendChild(this.dropdown("Numbers size", sizes, common((d) => fib(d).numbersSize ?? "small"), (s) => numbersSizeIcon(s), (v) => actions.patch({ numbersSize: v }), { label: sizeLabel }));
26780
- bar.appendChild(this.dropdown("Label size", sizes, common((d) => fib(d).labelsSize ?? "normal"), (s) => labelSizeIcon(s), (v) => actions.patch({ labelsSize: v }), { label: sizeLabel }));
28472
+ bar.appendChild(this.dropdown("Numbers size", sizes, common((d) => fib(d).numbersSize ?? "small"), (s) => numbersSizeIcon(s), (v) => actions.patch({ numbersSize: v }), { label: sizeLabel2 }));
28473
+ bar.appendChild(this.dropdown("Label size", sizes, common((d) => fib(d).labelsSize ?? "normal"), (s) => labelSizeIcon(s), (v) => actions.patch({ labelsSize: v }), { label: sizeLabel2 }));
26781
28474
  }
26782
28475
  bar.appendChild(this.divider());
26783
- if (!multi && isFrvp) bar.appendChild(this.iconBtn("Settings", GEAR_ICON, () => this.settingsDialog.open(drawing, actions, "frvp")));
26784
- if (!multi && isPosition) bar.appendChild(this.iconBtn("Position size", GEAR_ICON, () => this.settingsDialog.open(drawing, actions, "position")));
26785
- if (!multi && editableLevels) bar.appendChild(this.iconBtn("Levels", GEAR_ICON, () => this.settingsDialog.open(drawing, actions, "levels")));
28476
+ if (!multi) bar.appendChild(this.iconBtn("Settings", GEAR_ICON, () => this.openPanel()));
26786
28477
  bar.appendChild(this.toggle("Lock", LOCK_ICON, common((d) => d.locked), (v) => actions.setLocked(v), UNLOCK_ICON));
26787
28478
  const del = this.iconBtn("Delete", TRASH_ICON, () => actions.remove());
26788
28479
  del.style.color = "var(--vela-danger)";
@@ -26800,16 +28491,17 @@ var DrawingSettingsPopup = class {
26800
28491
  this.position(anchor);
26801
28492
  setTimeout(() => document.addEventListener("pointerdown", this.onOutside), 0);
26802
28493
  }
26803
- close() {
28494
+ close(keepPanel = false) {
26804
28495
  document.removeEventListener("pointerdown", this.onOutside);
26805
28496
  this.closeColorPopover();
26806
28497
  this.closeMenu();
26807
28498
  this.hideTip();
26808
- this.settingsDialog.close();
28499
+ if (!keepPanel) this.panel.close();
26809
28500
  this.el?.remove();
26810
28501
  this.el = null;
26811
28502
  this.textPanel = null;
26812
28503
  this.onClose = null;
28504
+ this.current = null;
26813
28505
  }
26814
28506
  destroy() {
26815
28507
  this.close();
@@ -26865,7 +28557,7 @@ var DrawingSettingsPopup = class {
26865
28557
  if (withColorAndSize) {
26866
28558
  tools.append(
26867
28559
  this.colorButton("Text color", TYPE_ICON, text?.color || this.theme.textColor, (v) => actions.patch({ "text.color": v }), 14),
26868
- 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 })
28560
+ 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 })
26869
28561
  );
26870
28562
  }
26871
28563
  tools.append(
@@ -27039,9 +28731,9 @@ var DrawingSettingsPopup = class {
27039
28731
  * quick toolbar — the chip works without selecting the drawing first.
27040
28732
  */
27041
28733
  openMagnifierTimeframeMenu(rect, current, onPick) {
27042
- ensureStyles3();
28734
+ ensureStyles4();
27043
28735
  closeOpenPopovers();
27044
- const options = this.lowerTimeframeOptions();
28736
+ const options = magnifierTimeframesBelow(this.chartBarMs());
27045
28737
  const anchor = document.createElement("div");
27046
28738
  anchor.style.cssText = `position:absolute;left:${rect.x}px;top:${rect.y}px;width:${rect.w}px;height:${rect.h}px;pointer-events:none;`;
27047
28739
  this.host.appendChild(anchor);
@@ -27082,22 +28774,22 @@ var DrawingSettingsPopup = class {
27082
28774
  zIndex: 26,
27083
28775
  padding: "4px",
27084
28776
  fill: (menu2, pop) => {
27085
- for (const row of rows) {
28777
+ for (const row2 of rows) {
27086
28778
  const item = document.createElement("button");
27087
28779
  item.type = "button";
27088
28780
  item.className = "vela-dpop-item";
27089
28781
  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;";
27090
28782
  const ic = document.createElement("span");
27091
28783
  ic.style.cssText = "display:flex;flex:none;width:20px;justify-content:center;";
27092
- ic.innerHTML = sized(row.icon, 18);
28784
+ ic.innerHTML = sized(row2.icon, 18);
27093
28785
  const tx = document.createElement("span");
27094
- tx.textContent = row.label;
28786
+ tx.textContent = row2.label;
27095
28787
  tx.style.cssText = "flex:1;";
27096
28788
  item.append(ic, tx);
27097
28789
  item.addEventListener("click", (e) => {
27098
28790
  e.stopPropagation();
27099
28791
  pop.hide();
27100
- row.onClick();
28792
+ row2.onClick();
27101
28793
  });
27102
28794
  menu2.appendChild(item);
27103
28795
  }
@@ -27322,12 +29014,12 @@ var DrawingSettingsPopup = class {
27322
29014
  padding: "10px",
27323
29015
  fill: (el, pop) => {
27324
29016
  if (used.length > 1) {
27325
- const row = document.createElement("div");
27326
- row.style.cssText = "display:flex;align-items:center;gap:6px;padding-bottom:9px;border-bottom:1px solid var(--vela-border);";
29017
+ const row2 = document.createElement("div");
29018
+ row2.style.cssText = "display:flex;align-items:center;gap:6px;padding-bottom:9px;border-bottom:1px solid var(--vela-border);";
27327
29019
  const lbl = document.createElement("span");
27328
29020
  lbl.textContent = "In use";
27329
29021
  lbl.style.cssText = "font:var(--vela-font-size-sm) inherit;opacity:0.7;margin-right:2px;";
27330
- row.appendChild(lbl);
29022
+ row2.appendChild(lbl);
27331
29023
  for (const c of used) {
27332
29024
  const sw = document.createElement("button");
27333
29025
  sw.type = "button";
@@ -27338,9 +29030,9 @@ var DrawingSettingsPopup = class {
27338
29030
  pop.hide();
27339
29031
  onChange(c);
27340
29032
  });
27341
- row.appendChild(sw);
29033
+ row2.appendChild(sw);
27342
29034
  }
27343
- el.appendChild(row);
29035
+ el.appendChild(row2);
27344
29036
  }
27345
29037
  el.appendChild(buildColorPicker(color, t, onChange));
27346
29038
  }
@@ -27414,7 +29106,7 @@ function labelSizeIcon(size) {
27414
29106
  function capitalize(s) {
27415
29107
  return s.charAt(0).toUpperCase() + s.slice(1);
27416
29108
  }
27417
- function sizeLabel(v) {
29109
+ function sizeLabel2(v) {
27418
29110
  return TEXT_SIZE_OPTIONS.find((o) => o.value === v)?.label ?? capitalize(String(v));
27419
29111
  }
27420
29112
  function styleLabel2(v) {
@@ -27495,7 +29187,7 @@ var DrawingToolbar = class {
27495
29187
  this.onCollapse = options.onCollapse ?? (() => {
27496
29188
  });
27497
29189
  this.onDrawingsSync = options.onDrawingsSync ?? null;
27498
- ensureStyles4();
29190
+ ensureStyles5();
27499
29191
  this.root = document.createElement("div");
27500
29192
  this.root.className = "vela-dtb";
27501
29193
  this.styleRoot();
@@ -27988,17 +29680,17 @@ var DrawingToolbar = class {
27988
29680
  this.paintStay();
27989
29681
  }
27990
29682
  makeButton(icon2, title, onClick) {
27991
- const btn2 = document.createElement("button");
27992
- btn2.type = "button";
27993
- btn2.className = "vela-dtb-btn";
27994
- btn2.setAttribute("aria-label", title);
27995
- this.tipText.set(btn2, title);
27996
- if (icon2) btn2.innerHTML = hitHtml(icon2);
29683
+ const btn = document.createElement("button");
29684
+ btn.type = "button";
29685
+ btn.className = "vela-dtb-btn";
29686
+ btn.setAttribute("aria-label", title);
29687
+ this.tipText.set(btn, title);
29688
+ if (icon2) btn.innerHTML = hitHtml(icon2);
27997
29689
  this.tipDisposers.push(
27998
- attachChromeTooltip(btn2, { host: this.host, theme: () => this.theme, text: () => this.tipText.get(btn2) ?? title, placement: "right" })
29690
+ attachChromeTooltip(btn, { host: this.host, theme: () => this.theme, text: () => this.tipText.get(btn) ?? title, placement: "right" })
27999
29691
  );
28000
- btn2.addEventListener("click", onClick);
28001
- return btn2;
29692
+ btn.addEventListener("click", onClick);
29693
+ return btn;
28002
29694
  }
28003
29695
  divider() {
28004
29696
  const d = document.createElement("div");
@@ -28007,13 +29699,13 @@ var DrawingToolbar = class {
28007
29699
  }
28008
29700
  };
28009
29701
  var MAGNET_ID = "__magnet__";
28010
- var STYLE_ID4 = "vela-drawing-toolbar-styles";
28011
- function ensureStyles4() {
29702
+ var STYLE_ID5 = "vela-drawing-toolbar-styles";
29703
+ function ensureStyles5() {
28012
29704
  if (typeof document === "undefined") return;
28013
- let s = document.getElementById(STYLE_ID4);
29705
+ let s = document.getElementById(STYLE_ID5);
28014
29706
  if (!s) {
28015
29707
  s = document.createElement("style");
28016
- s.id = STYLE_ID4;
29708
+ s.id = STYLE_ID5;
28017
29709
  document.head.appendChild(s);
28018
29710
  }
28019
29711
  s.textContent = `
@@ -28299,7 +29991,9 @@ var UserDrawingController = class {
28299
29991
  this.hoveredId = null;
28300
29992
  // the drawing under the cursor (its handles show)
28301
29993
  this.activeTool = null;
28302
- // last-used style for the armed tool (seeds the placement ghost)
29994
+ // the armed tool's remembered settings (seed the placement ghost)
29995
+ this.toolTemplates = {};
29996
+ // saved styles, per tool
28303
29997
  this.intentCb = null;
28304
29998
  /** Another chart's in-progress placement, mirrored here as a ghost (drawings sync). */
28305
29999
  this.externalGhost = null;
@@ -28347,7 +30041,11 @@ var UserDrawingController = class {
28347
30041
  this.clearSelection();
28348
30042
  };
28349
30043
  this.ctx = canvas.getContext("2d");
28350
- this.popup = new DrawingSettingsPopup(overlayHost, deps.theme(), () => deps.chartBarMs());
30044
+ this.popup = new DrawingSettingsPopup(overlayHost, deps.theme(), {
30045
+ chartBarMs: () => deps.chartBarMs(),
30046
+ timeZone: () => deps.timeZone?.() ?? "UTC",
30047
+ priceDecimals: () => deps.priceDecimals?.() ?? null
30048
+ });
28351
30049
  this.toolbar = new DrawingToolbar(
28352
30050
  toolbarHost,
28353
30051
  deps.theme(),
@@ -28370,7 +30068,7 @@ var UserDrawingController = class {
28370
30068
  this.interaction = new DrawingInteraction({
28371
30069
  projector: () => this.deps.projector(),
28372
30070
  activeTool: () => this.activeTool,
28373
- drawings: () => this.drawings,
30071
+ drawings: () => this.onScreen(),
28374
30072
  hoveredId: () => this.hoveredId,
28375
30073
  selectedIds: () => this.selectedIds,
28376
30074
  emit: (i) => this.emit(i),
@@ -28380,7 +30078,7 @@ var UserDrawingController = class {
28380
30078
  },
28381
30079
  openSettings: (id, x, y) => this.openSettingsById(id, x, y),
28382
30080
  snap: (pt, paneId, mode, cursorPx) => this.deps.snap(pt, paneId, mode, cursorPx),
28383
- lastStyle: () => this.activeToolStyle
30081
+ toolDefaults: () => this.activeToolDefaults
28384
30082
  });
28385
30083
  }
28386
30084
  // ── IDrawingsRendererPort (commands down) ──
@@ -28432,6 +30130,7 @@ var UserDrawingController = class {
28432
30130
  this.invalidateSlices();
28433
30131
  this.render();
28434
30132
  this.deps.requestScaleUpdate();
30133
+ this.popup.syncPanel();
28435
30134
  }
28436
30135
  /** The pane's series stack in z terms — how the core places a new drawing (just under
28437
30136
  * `price`) and computes "front of everything" / "behind everything" for the reorders. */
@@ -28459,7 +30158,7 @@ var UserDrawingController = class {
28459
30158
  }
28460
30159
  return lo <= hi ? { min: lo, max: hi } : null;
28461
30160
  }
28462
- setActiveTool(type, lastStyle) {
30161
+ setActiveTool(type, lastStyle, defaults5) {
28463
30162
  if (type != null) {
28464
30163
  this.finishTextEditor(true);
28465
30164
  this.withModeIntent(() => {
@@ -28468,7 +30167,7 @@ var UserDrawingController = class {
28468
30167
  });
28469
30168
  }
28470
30169
  this.activeTool = type;
28471
- this.activeToolStyle = lastStyle;
30170
+ this.activeToolDefaults = defaults5 ?? (lastStyle ? { style: lastStyle } : void 0);
28472
30171
  this.toolbar.setActiveTool(type);
28473
30172
  if (type == null) this.interaction.onToolCleared();
28474
30173
  else this.clearSelection();
@@ -28478,6 +30177,10 @@ var UserDrawingController = class {
28478
30177
  setFavorites(types) {
28479
30178
  this.toolbar.setFavorites(types);
28480
30179
  }
30180
+ /** Core push: every tool's saved styles — the settings panel's style menu reads them. */
30181
+ setToolTemplates(map2) {
30182
+ this.toolTemplates = map2;
30183
+ }
28481
30184
  /** Core push: per-tool shortcut hints (display strings) shown in the toolbar flyouts. */
28482
30185
  setToolShortcuts(map2) {
28483
30186
  this.toolbar.setShortcuts(map2);
@@ -28550,8 +30253,9 @@ var UserDrawingController = class {
28550
30253
  /** The topmost visible (unlocked) magnifier whose timeframe chip contains (x, y) —
28551
30254
  * the chip's rect is what the painter measured last frame. */
28552
30255
  magnifierChipAt(x, y) {
28553
- for (let i = this.drawings.length - 1; i >= 0; i -= 1) {
28554
- const d = this.drawings[i];
30256
+ const shown = this.onScreen();
30257
+ for (let i = shown.length - 1; i >= 0; i -= 1) {
30258
+ const d = shown[i];
28555
30259
  if (!(d instanceof Magnifier) || !d.visible || d.locked) continue;
28556
30260
  const r = d.chipRect;
28557
30261
  if (r && x >= r.x && x <= r.x + r.w && y >= r.y && y <= r.y + r.h) return d;
@@ -28577,7 +30281,7 @@ var UserDrawingController = class {
28577
30281
  * `withSelection` so a hit on a member of a multi-selection removes the selection's
28578
30282
  * unlocked members — whichever member was hit, locked or not. */
28579
30283
  deleteAt(x, y, withSelection = false) {
28580
- const hit = topDrawingAt(this.drawings, x, y, this.deps.projector(), HIT_TOLERANCE);
30284
+ const hit = topDrawingAt(this.onScreen(), x, y, this.deps.projector(), HIT_TOLERANCE);
28581
30285
  if (!hit) return false;
28582
30286
  const ids = deleteTargets(hit, this.selectedIds, this.drawings, withSelection);
28583
30287
  if (ids.length === 0) return false;
@@ -28657,7 +30361,7 @@ var UserDrawingController = class {
28657
30361
  updateHover(x, y, mod = false) {
28658
30362
  let id = null;
28659
30363
  if (!mod && this.activeTool == null && !this.interaction.isPlacing() && !this.interaction.isDragging()) {
28660
- id = topDrawingAt(this.drawings, x, y, this.deps.projector(), HIT_TOLERANCE)?.id ?? null;
30364
+ id = topDrawingAt(this.onScreen(), x, y, this.deps.projector(), HIT_TOLERANCE)?.id ?? null;
28661
30365
  }
28662
30366
  if (id !== this.hoveredId) {
28663
30367
  this.hoveredId = id;
@@ -28892,16 +30596,20 @@ var UserDrawingController = class {
28892
30596
  }
28893
30597
  return false;
28894
30598
  }
28895
- /** Double-click over a drawing → suppress the chart's view reset (single-click already
28896
- * opens settings). Returns true only when a drawing is under the cursor. */
30599
+ /** Double-click over a drawing opens its full settings panel (a callout or text label edits
30600
+ * its words in place instead) and suppresses the chart's view reset. Returns true only
30601
+ * when a drawing is under the cursor. */
28897
30602
  dblClick(x, y) {
28898
30603
  if (this.interaction.finishPlacing(true)) return true;
28899
- const hit = topDrawingAt(this.drawings, x, y, this.deps.projector(), HIT_TOLERANCE);
30604
+ const hit = topDrawingAt(this.onScreen(), x, y, this.deps.projector(), HIT_TOLERANCE);
28900
30605
  if (isInlineEditable(hit)) {
28901
30606
  this.editTextInline(hit.id);
28902
30607
  return true;
28903
30608
  }
28904
- return hit != null;
30609
+ if (!hit) return false;
30610
+ if (this.popupMulti || !this.selectedIds.has(hit.id) || !this.popup.isOpen()) this.openSettingsById(hit.id, 0, 0);
30611
+ this.popup.openPanel();
30612
+ return true;
28905
30613
  }
28906
30614
  /** Keyboard pre-empt: Escape (popup/placing/selection), undo/redo, copy/paste/duplicate,
28907
30615
  * delete (multi), and arrow-nudge. Stands down while a label text field is focused. */
@@ -28999,6 +30707,13 @@ var UserDrawingController = class {
28999
30707
  this.popup.close();
29000
30708
  this.render();
29001
30709
  }
30710
+ /** The drawings shown on the chart's current timeframe. One limited to other timeframes
30711
+ * ("show on") is neither painted nor clickable here, but stays in the model: the object
30712
+ * tree still lists it, and its own visibility flag is untouched. */
30713
+ onScreen() {
30714
+ const barMs = this.deps.chartBarMs();
30715
+ return this.drawings.filter((d) => shownOnTimeframe(d.showOn, barMs));
30716
+ }
29002
30717
  /** Whether the drawing's z puts it INSIDE the series stack (per the last-known boundaries)
29003
30718
  * rather than over it — i.e. its body belongs to an interleave layer, not the top canvas. */
29004
30719
  isInterleaved(d) {
@@ -29038,7 +30753,7 @@ var UserDrawingController = class {
29038
30753
  const proj = this.deps.projector();
29039
30754
  const theme = this.deps.theme();
29040
30755
  const buckets = /* @__PURE__ */ new Map();
29041
- for (const d of this.drawings) {
30756
+ for (const d of this.onScreen()) {
29042
30757
  if (!d.visible || this.lifted.has(d.id)) continue;
29043
30758
  const beforeZ = sliceKeyFor(d.zIndex, this.lastBounds.get(d.paneId) ?? []);
29044
30759
  if (beforeZ === null) continue;
@@ -29092,8 +30807,9 @@ var UserDrawingController = class {
29092
30807
  };
29093
30808
  this.painter.seriesLook = this.deps.seriesLook();
29094
30809
  const onTop = (d) => !this.isInterleaved(d) || this.lifted.has(d.id);
29095
- this.painter.paintAll(ctx, this.drawings.filter(onTop), proj, this.deps.theme(), targets);
29096
- this.painter.paintHighlights(ctx, this.drawings.filter((d) => !onTop(d)), proj, handleIdsFor(targets));
30810
+ const shown = this.onScreen();
30811
+ this.painter.paintAll(ctx, shown.filter(onTop), proj, this.deps.theme(), targets);
30812
+ this.painter.paintHighlights(ctx, shown.filter((d) => !onTop(d)), proj, handleIdsFor(targets));
29097
30813
  const clones = this.interaction.dragClones();
29098
30814
  if (clones) this.painter.paintAll(ctx, clones, proj, this.deps.theme(), { selected: new Set(clones.map((c) => c.id)) });
29099
30815
  this.layoutTextEditor();
@@ -29203,6 +30919,7 @@ var UserDrawingController = class {
29203
30919
  this.emit({ kind: "select", ids: [id] });
29204
30920
  this.emit({ kind: "settings", id });
29205
30921
  const anchor = drawing.bounds(this.deps.projector());
30922
+ let previewBase = null;
29206
30923
  this.popup.open([drawing], anchor, {
29207
30924
  // Sync rebuilds instances, so a panel that reads values back after a patch (e.g. the
29208
30925
  // position tool's price fields, where one edit can flip another level) resolves fresh.
@@ -29237,11 +30954,39 @@ var UserDrawingController = class {
29237
30954
  restore: (doc) => {
29238
30955
  const d = live();
29239
30956
  if (!d) return;
29240
- if (doc.style) d.style = { ...doc.style };
29241
- if (doc.text !== void 0) d.text = doc.text ? { ...doc.text } : void 0;
29242
- if (doc.props !== void 0) d.applyProps(doc.props);
30957
+ const text = doc.text ?? (d.text ? { ...d.text, value: "" } : void 0);
30958
+ restoreLook(d, { ...doc, text });
30959
+ d.anchors = doc.anchors.map((a) => ({ ...a }));
30960
+ d.showOn = doc.showOn ? [...doc.showOn] : void 0;
30961
+ this.render();
30962
+ this.emit({ kind: "edit", doc: { ...doc, text } });
30963
+ },
30964
+ templates: () => this.toolTemplates[drawing.type] ?? [],
30965
+ saveTemplate: (name) => {
30966
+ const d = live();
30967
+ if (d) this.emit({ kind: "template-save", type: d.type, name, settings: captureToolDefaults(d) });
30968
+ },
30969
+ removeTemplate: (name) => this.emit({ kind: "template-remove", type: drawing.type, name }),
30970
+ applyToolSettings: (settings) => {
30971
+ const d = live();
30972
+ if (!d) return;
30973
+ applyToolDefaults(d, settings);
30974
+ this.render();
30975
+ this.emit({ kind: "edit", doc: d.serialize() });
30976
+ },
30977
+ // A hovered style paints on the drawing without an edit; leaving puts the look back.
30978
+ preview: (settings) => {
30979
+ const d = live();
30980
+ if (!d) return;
30981
+ if (settings) {
30982
+ if (previewBase) restoreLook(d, previewBase);
30983
+ else previewBase = d.serialize();
30984
+ applyToolDefaults(d, settings);
30985
+ } else if (previewBase) {
30986
+ restoreLook(d, previewBase);
30987
+ previewBase = null;
30988
+ } else return;
29243
30989
  this.render();
29244
- this.emit({ kind: "edit", doc });
29245
30990
  },
29246
30991
  remove: () => {
29247
30992
  this.closeTextEditor();
@@ -29290,6 +31035,11 @@ var UserDrawingController = class {
29290
31035
  this.intentCb?.(i);
29291
31036
  }
29292
31037
  };
31038
+ function restoreLook(d, doc) {
31039
+ d.style = { ...doc.style };
31040
+ d.text = doc.text ? { ...doc.text } : void 0;
31041
+ if (doc.props !== void 0) d.applyProps(doc.props);
31042
+ }
29293
31043
 
29294
31044
  // src/renderers/shared/TableOverlay.ts
29295
31045
  var SIZE_PX4 = {
@@ -29305,7 +31055,7 @@ function fontPxOf(size) {
29305
31055
  return SIZE_PX4[size] ?? SIZE_PX4.auto;
29306
31056
  }
29307
31057
  function tableHasContent(t) {
29308
- return t.cells.some((row) => row?.some((c) => c != null && !c.merged));
31058
+ return t.cells.some((row2) => row2?.some((c) => c != null && !c.merged));
29309
31059
  }
29310
31060
  function mergeRenderPlan(t) {
29311
31061
  const span = /* @__PURE__ */ new Map();
@@ -30066,15 +31816,15 @@ var MarkPopover = class {
30066
31816
  actions.className = "vela-callout-actions";
30067
31817
  section.appendChild(actions);
30068
31818
  }
30069
- const btn2 = doc.createElement("button");
30070
- btn2.type = "button";
30071
- btn2.className = "vela-callout-btn" + (item.primary ? " vela-callout-btn-primary" : "");
30072
- btn2.textContent = item.label;
30073
- btn2.addEventListener("click", () => {
31819
+ const btn = doc.createElement("button");
31820
+ btn.type = "button";
31821
+ btn.className = "vela-callout-btn" + (item.primary ? " vela-callout-btn-primary" : "");
31822
+ btn.textContent = item.label;
31823
+ btn.addEventListener("click", () => {
30074
31824
  item.run();
30075
31825
  if (item.close !== false) this.close();
30076
31826
  });
30077
- actions.appendChild(btn2);
31827
+ actions.appendChild(btn);
30078
31828
  continue;
30079
31829
  }
30080
31830
  actions = null;
@@ -30084,16 +31834,16 @@ var MarkPopover = class {
30084
31834
  text.textContent = item.text;
30085
31835
  section.appendChild(text);
30086
31836
  } else if (item.type === "field") {
30087
- const row = doc.createElement("div");
30088
- row.className = "vela-marks-field";
31837
+ const row2 = doc.createElement("div");
31838
+ row2.className = "vela-marks-field";
30089
31839
  const label = doc.createElement("span");
30090
31840
  label.className = "vela-marks-field-label";
30091
31841
  label.textContent = item.label;
30092
31842
  const value = doc.createElement("span");
30093
31843
  value.className = "vela-marks-field-value";
30094
31844
  value.textContent = item.value;
30095
- row.append(label, value);
30096
- section.appendChild(row);
31845
+ row2.append(label, value);
31846
+ section.appendChild(row2);
30097
31847
  }
30098
31848
  }
30099
31849
  }
@@ -30378,7 +32128,7 @@ var VELA_LETTERS_SVG = '<svg viewBox="161.66 -142 310.57 148" aria-hidden="true"
30378
32128
 
30379
32129
  // src/renderers/native/chrome/AttributionMark.ts
30380
32130
  var ATTRIBUTION_URL = "https://velacharts.dev/?utm_source=vela&utm_medium=attribution";
30381
- var STYLE_ID5 = "vela-attribution-styles";
32131
+ var STYLE_ID6 = "vela-attribution-styles";
30382
32132
  var CSS2 = `
30383
32133
  .vela-attribution {
30384
32134
  text-decoration: none;
@@ -30434,11 +32184,11 @@ var CSS2 = `
30434
32184
  [data-vela-layout='mobile'] .vela-attribution:hover .vela-attr-wordmark,
30435
32185
  [data-layout='mobile'] .vela-attribution:hover .vela-attr-wordmark { transform: translateX(0); }
30436
32186
  `;
30437
- function ensureStyles5(doc) {
30438
- let s = doc.getElementById(STYLE_ID5);
32187
+ function ensureStyles6(doc) {
32188
+ let s = doc.getElementById(STYLE_ID6);
30439
32189
  if (!s) {
30440
32190
  s = doc.createElement("style");
30441
- s.id = STYLE_ID5;
32191
+ s.id = STYLE_ID6;
30442
32192
  doc.head.appendChild(s);
30443
32193
  }
30444
32194
  s.textContent = CSS2;
@@ -30468,7 +32218,7 @@ function createCustomMark(doc, html, background) {
30468
32218
  return el;
30469
32219
  }
30470
32220
  function createAttributionMark(doc, background) {
30471
- ensureStyles5(doc);
32221
+ ensureStyles6(doc);
30472
32222
  const a = doc.createElement("a");
30473
32223
  a.className = "vela-attribution";
30474
32224
  a.href = ATTRIBUTION_URL;
@@ -30741,13 +32491,13 @@ function paintVpvr(ctx, profile, geom, style) {
30741
32491
  const { rightX, maxW, yOf } = geom;
30742
32492
  if (maxTotal <= 0 || maxW <= 0) return;
30743
32493
  for (let k = 0; k < rows.length; k += 1) {
30744
- const row = rows[k];
30745
- const total = row.up + row.down;
32494
+ const row2 = rows[k];
32495
+ const total = row2.up + row2.down;
30746
32496
  if (total <= 0) continue;
30747
- const yTop = yOf(row.price + rowH);
30748
- const h = Math.max(1, yOf(row.price) - yTop - 1);
32497
+ const yTop = yOf(row2.price + rowH);
32498
+ const h = Math.max(1, yOf(row2.price) - yTop - 1);
30749
32499
  const w = total / maxTotal * maxW;
30750
- const upW = row.up / total * w;
32500
+ const upW = row2.up / total * w;
30751
32501
  ctx.globalAlpha = k >= vaFrom && k <= vaTo ? VPVR_VA_ALPHA : VPVR_OUTSIDE_ALPHA;
30752
32502
  if (upW > 0) {
30753
32503
  ctx.fillStyle = style.upColor;
@@ -30759,9 +32509,9 @@ function paintVpvr(ctx, profile, geom, style) {
30759
32509
  }
30760
32510
  }
30761
32511
  if (style.showPoc) {
30762
- const row = rows[poc];
30763
- const yTop = yOf(row.price + rowH);
30764
- const h = Math.max(1, yOf(row.price) - yTop - 1);
32512
+ const row2 = rows[poc];
32513
+ const yTop = yOf(row2.price + rowH);
32514
+ const h = Math.max(1, yOf(row2.price) - yTop - 1);
30765
32515
  ctx.globalAlpha = VPVR_POC_ALPHA;
30766
32516
  ctx.strokeStyle = style.pocColor;
30767
32517
  ctx.lineWidth = 1;
@@ -31727,11 +33477,11 @@ var NativeRenderer = class {
31727
33477
  }
31728
33478
  }
31729
33479
  makeSettingsButton() {
31730
- const btn2 = document.createElement("button");
31731
- btn2.type = "button";
31732
- btn2.title = "Chart settings";
31733
- btn2.innerHTML = iconAt("gear", 14);
31734
- Object.assign(btn2.style, {
33480
+ const btn = document.createElement("button");
33481
+ btn.type = "button";
33482
+ btn.title = "Chart settings";
33483
+ btn.innerHTML = iconAt("gear", 14);
33484
+ Object.assign(btn.style, {
31735
33485
  position: "absolute",
31736
33486
  bottom: `${TIME_AXIS_H + 10}px`,
31737
33487
  right: `${RIGHT_AXIS_W + 10}px`,
@@ -31752,10 +33502,10 @@ var NativeRenderer = class {
31752
33502
  pointerEvents: "auto",
31753
33503
  padding: "0"
31754
33504
  });
31755
- btn2.addEventListener("mouseenter", () => btn2.style.opacity = "1");
31756
- btn2.addEventListener("mouseleave", () => btn2.style.opacity = "0.75");
31757
- btn2.addEventListener("click", () => this.toggleSettingsDialog());
31758
- return btn2;
33505
+ btn.addEventListener("mouseenter", () => btn.style.opacity = "1");
33506
+ btn.addEventListener("mouseleave", () => btn.style.opacity = "0.75");
33507
+ btn.addEventListener("click", () => this.toggleSettingsDialog());
33508
+ return btn;
31759
33509
  }
31760
33510
  /** Port surface: hosts (bottom-bar / chrome buttons) open the same dialog as the
31761
33511
  * in-chart gear — created on demand, independent of the gear feature being enabled. */
@@ -31820,12 +33570,12 @@ var NativeRenderer = class {
31820
33570
  }
31821
33571
  /** Jump-back-to-latest button — same size/border chrome as the drawing-toolbar collapse toggle. */
31822
33572
  makeScrollToRealtimeButton() {
31823
- const btn2 = document.createElement("button");
31824
- btn2.type = "button";
31825
- btn2.title = "Scroll to most recent bar (Alt+Shift+\u2192)";
31826
- btn2.setAttribute("aria-label", "Scroll to most recent bar");
31827
- btn2.innerHTML = this.scrollButtonIcon();
31828
- Object.assign(btn2.style, {
33573
+ const btn = document.createElement("button");
33574
+ btn.type = "button";
33575
+ btn.title = "Scroll to most recent bar (Alt+Shift+\u2192)";
33576
+ btn.setAttribute("aria-label", "Scroll to most recent bar");
33577
+ btn.innerHTML = this.scrollButtonIcon();
33578
+ Object.assign(btn.style, {
31829
33579
  position: "absolute",
31830
33580
  bottom: `${SCROLL_BTN_BOTTOM}px`,
31831
33581
  right: `${SCROLL_BTN_RIGHT}px`,
@@ -31848,10 +33598,10 @@ var NativeRenderer = class {
31848
33598
  pointerEvents: "auto",
31849
33599
  padding: "0"
31850
33600
  });
31851
- btn2.addEventListener("mouseenter", () => btn2.style.opacity = "1");
31852
- btn2.addEventListener("mouseleave", () => btn2.style.opacity = "0.85");
31853
- btn2.addEventListener("click", () => this.scrollToRealtime());
31854
- return btn2;
33601
+ btn.addEventListener("mouseenter", () => btn.style.opacity = "1");
33602
+ btn.addEventListener("mouseleave", () => btn.style.opacity = "0.85");
33603
+ btn.addEventListener("click", () => this.scrollToRealtime());
33604
+ return btn;
31855
33605
  }
31856
33606
  /** Double-chevron icon — the "jump back to the latest bar" affordance. */
31857
33607
  scrollButtonIcon() {
@@ -31874,14 +33624,14 @@ var NativeRenderer = class {
31874
33624
  * appears whenever the latest bars are off-screen and hides again once the view is back
31875
33625
  * at the right edge. */
31876
33626
  updateScrollToRealtimeButton() {
31877
- const btn2 = this.scrollButton;
31878
- if (!btn2) return;
33627
+ const btn = this.scrollButton;
33628
+ if (!btn) return;
31879
33629
  const latestOffScreen = this.coords.barCount > 0 && this.coords.getViewport().rightOffset < 0;
31880
33630
  if (this.layoutMode === "mobile") {
31881
- btn2.style.display = latestOffScreen ? "flex" : "none";
33631
+ btn.style.display = latestOffScreen ? "flex" : "none";
31882
33632
  return;
31883
33633
  }
31884
- btn2.style.display = latestOffScreen && this.pointerNearScrollBtn ? "flex" : "none";
33634
+ btn.style.display = latestOffScreen && this.pointerNearScrollBtn ? "flex" : "none";
31885
33635
  }
31886
33636
  /** Glide the view back to the most recent bars, keeping the current zoom (barSpacing). */
31887
33637
  scrollToRealtime() {
@@ -32190,6 +33940,8 @@ var NativeRenderer = class {
32190
33940
  };
32191
33941
  },
32192
33942
  chartBarMs: () => this.coords.barInterval,
33943
+ timeZone: () => this.scene.timezone,
33944
+ priceDecimals: () => this.scene.priceMintick ? tickDecimals(this.scene.priceMintick) : null,
32193
33945
  snap: (pt, paneId, mode, cursorPx) => this.snapToCandle(pt, paneId, mode, cursorPx),
32194
33946
  setSnapMode: (mode) => this.setSnapMode(mode),
32195
33947
  setToolbarGutter: (px) => this.setToolbarGutter(px)
@@ -33511,7 +35263,7 @@ var NativeRenderer = class {
33511
35263
  if (bar) parts.push(formatStamp(bar.time, this.scene.timezone));
33512
35264
  if (ohlc) parts.push(`Open ${ohlc.o}, High ${ohlc.h}, Low ${ohlc.l}, Close ${ohlc.c}`);
33513
35265
  for (const group of this.dataWindowGroups(idx, pricePane)) {
33514
- for (const row of group.rows) parts.push(`${row.label} ${row.value}`);
35266
+ for (const row2 of group.rows) parts.push(`${row2.label} ${row2.value}`);
33515
35267
  }
33516
35268
  this.liveRegion.textContent = parts.join(". ");
33517
35269
  }
@@ -34467,7 +36219,10 @@ function applyPatch(model, patch) {
34467
36219
  const s = model.series.find((x) => x.id === delta.seriesId);
34468
36220
  if (!s) continue;
34469
36221
  if (delta.kind === "points" && isLineLikeSeries(s)) s.points = delta.points;
34470
- else if (delta.kind === "bars" && (s.kind === "candle" || s.kind === "bar")) s.bars = delta.bars;
36222
+ else if (delta.kind === "bars" && (s.kind === "candle" || s.kind === "bar")) {
36223
+ s.bars = delta.bars;
36224
+ if (delta.barColors !== void 0) s.barColors = delta.barColors;
36225
+ }
34471
36226
  }
34472
36227
  if (patch.lines) model.lines = patch.lines;
34473
36228
  if (patch.boxes) model.boxes = patch.boxes;
@@ -38513,8 +40268,8 @@ var Vela = class {
38513
40268
  if (options.settings) this.rendererControl.setSettingsVisibility(options.settings);
38514
40269
  this.dataControl = new DataControl(feed);
38515
40270
  this.orchestrator = new EngineOrchestrator(element, renderer, feed, engines, config, this.dataControl);
38516
- const defaults2 = rendererDefaults();
38517
- if (Object.keys(defaults2).length > 0) this.rendererControl.set(defaults2);
40271
+ const defaults5 = rendererDefaults();
40272
+ if (Object.keys(defaults5).length > 0) this.rendererControl.set(defaults5);
38518
40273
  this.panesControl = new PanesControl(this.orchestrator);
38519
40274
  this.drawingsControl = new DrawingsControl(this.orchestrator.drawings);
38520
40275
  this.marksControl = new MarksControl(this.orchestrator.marks);