@luxalgo/vela 0.7.8 → 0.8.0

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 (46) hide show
  1. package/NOTICE +28 -28
  2. package/dist/{DataProvider-CGa4KHRa.d.cts → DataProvider-BrlxntPN.d.cts} +1 -1
  3. package/dist/{DataProvider-CmWUtDVL.d.ts → DataProvider-DwFIoV4r.d.ts} +1 -1
  4. package/dist/{chunk-WTWGEB7K.js → chunk-2IAETM2I.js} +260 -2
  5. package/dist/{chunk-CAFCLMPF.js → chunk-BZQM2XO7.js} +1 -0
  6. package/dist/{chunk-M6GP7DAX.js → chunk-E3GU6AUI.js} +459 -37
  7. package/dist/{chunk-BFA32GOU.js → chunk-EZ5FWVLA.js} +5 -2
  8. package/dist/{chunk-PVGWNF7V.js → chunk-KA77UTRF.js} +2 -2
  9. package/dist/{chunk-N4ZZYLMP.js → chunk-QBPUT5SU.js} +686 -251
  10. package/dist/{contributions-DF_Ea07m.d.cts → contributions-CtlptxR8.d.cts} +187 -5
  11. package/dist/{contributions-nKOSeLy8.d.ts → contributions-tjNExo1o.d.ts} +187 -5
  12. package/dist/index.cjs +966 -247
  13. package/dist/index.d.cts +24 -8
  14. package/dist/index.d.ts +24 -8
  15. package/dist/index.js +28 -4
  16. package/dist/{options-D1AJKsn_.d.ts → options-BnEPUuVV.d.cts} +1 -1
  17. package/dist/{options-D1AJKsn_.d.cts → options-BnEPUuVV.d.ts} +1 -1
  18. package/dist/{plugin-6F04gjQe.d.cts → plugin-BWxJRKzS.d.cts} +10 -4
  19. package/dist/{plugin-BLsNipkc.d.ts → plugin-e2WUEtiI.d.ts} +10 -4
  20. package/dist/plugin.cjs +1 -0
  21. package/dist/plugin.d.cts +4 -4
  22. package/dist/plugin.d.ts +4 -4
  23. package/dist/plugin.js +2 -2
  24. package/dist/providers/binance.d.cts +2 -2
  25. package/dist/providers/binance.d.ts +2 -2
  26. package/dist/providers/coinbase.d.cts +2 -2
  27. package/dist/providers/coinbase.d.ts +2 -2
  28. package/dist/providers/hyperliquid.d.cts +2 -2
  29. package/dist/providers/hyperliquid.d.ts +2 -2
  30. package/dist/{statusline-model-DAFyqVmR.d.ts → statusline-model-DFY7tuHj.d.cts} +5 -3
  31. package/dist/{statusline-model-BoWJ3cWQ.d.cts → statusline-model-zKXtGIjj.d.ts} +5 -3
  32. package/dist/ui.cjs +263 -0
  33. package/dist/ui.d.cts +75 -2
  34. package/dist/ui.d.ts +75 -2
  35. package/dist/ui.js +3 -3
  36. package/dist/vela.global.js +968 -249
  37. package/dist/vela.global.min.js +73 -71
  38. package/dist/widget.cjs +1411 -292
  39. package/dist/widget.d.cts +26 -7
  40. package/dist/widget.d.ts +26 -7
  41. package/dist/widget.js +7 -7
  42. package/dist/workspace.cjs +1414 -292
  43. package/dist/workspace.d.cts +124 -6
  44. package/dist/workspace.d.ts +124 -6
  45. package/dist/workspace.js +6 -6
  46. package/package.json +1 -1
@@ -591,6 +591,7 @@ var Vela = (function (exports) {
591
591
  registerIcon("chevron-up", S('<path d="M3.5 10 8 5.5 12.5 10"/>'));
592
592
  registerIcon("chevrons-right", S('<path d="m4 3.5 4.5 4.5L4 12.5"/><path d="m8.5 3.5 4.5 4.5-4.5 4.5"/>'));
593
593
  registerIcon("chevrons-left", S('<path d="M12 3.5 7.5 8l4.5 4.5"/><path d="M7.5 3.5 3 8l4.5 4.5"/>'));
594
+ registerIcon("replay", S('<path d="M14 4.2v7.6L8.6 8z"/><path d="M7.4 4.2v7.6L2 8z"/>'));
594
595
  registerIcon("check", S('<path d="m2.8 8.4 3.4 3.4 7-7.6"/>'));
595
596
  registerIcon("close", S('<path d="m3.8 3.8 8.4 8.4M12.2 3.8l-8.4 8.4"/>'));
596
597
  registerIcon("pin", S('<path d="M5.6 2.5h4.8M6.4 2.5v3.6L4.4 8.4v.9h7.2v-.9L9.6 6.1V2.5M8 9.3v4.2"/>'));
@@ -7808,6 +7809,7 @@ var Vela = (function (exports) {
7808
7809
  var FIRST_PAINT_BARS = 20;
7809
7810
  var GAP_FACTOR = 1.5;
7810
7811
  var HEAL_COOLDOWN_MS = 5e3;
7812
+ var MIN_TICK_UPDATE_MS = 16;
7811
7813
  var EngineOrchestrator = class _EngineOrchestrator {
7812
7814
  constructor(container, renderer, feed, engines, config, dataControl) {
7813
7815
  this.renderer = renderer;
@@ -7860,6 +7862,32 @@ var Vela = (function (exports) {
7860
7862
  this.lastHealAt = 0;
7861
7863
  /** The `visibilitychange` catch-up listener (removed on destroy). */
7862
7864
  this.onVisible = null;
7865
+ // ── bar replay: `rawBars` holds the history up to the cursor, the rest waits here ──
7866
+ /** Bars still hidden right of the replay cursor, oldest first; null ≡ replay off. */
7867
+ this.replayQueue = null;
7868
+ this.replayTimer = null;
7869
+ this.replayPlaying = false;
7870
+ this.replayIntervalMs = 1e3;
7871
+ /** The depth (`market.bars`) before a replay deepened the history — restored when it ends. */
7872
+ this.replayDepth = null;
7873
+ /** A start is loading older history before it can cut; `seek` = a replay was running
7874
+ * (and left silently to deepen), so abandoning the load must announce its end. */
7875
+ this.replayLoading = null;
7876
+ /** A replay carried over a timeframe/session switch, between the old series' silent end
7877
+ * and the cut of the new one: still ACTIVE for the outside (`replayState`), resumed
7878
+ * at `revealedEnd` once the new bars land, unless stopped (`cancelled`) meanwhile. */
7879
+ this.replayCarrying = null;
7880
+ /** Intrabar prices for the bars replay reveals — null ≡ whole bars. */
7881
+ this.replayTickSource = null;
7882
+ /** The bar being revealed tick by tick: its stored form (what it settles on), its ticks,
7883
+ * the next tick to apply, the candle built so far, the volume each tick adds when the
7884
+ * ticks carry none, and its pacing (ticks per update, ms between updates — one play
7885
+ * interval per tick — and the clock time tick 0 is due at, so late timers catch up). */
7886
+ this.replayForming = null;
7887
+ /** Tick requests by bar time, shared by prefetch and playback; `ticks` once they landed. */
7888
+ this.replayTickFetches = /* @__PURE__ */ new Map();
7889
+ /** Aborts the tick requests in flight — renewed whenever the replay moves on. */
7890
+ this.replayTickAbort = null;
7863
7891
  /** What the last applied bar did: refine the open candle, or open a new one (which
7864
7892
  * settles the previous). Read by the `script:run` cause attribution. */
7865
7893
  this.barCause = "tick";
@@ -8083,6 +8111,10 @@ var Vela = (function (exports) {
8083
8111
  const paint = (bars, final) => {
8084
8112
  if (this.generation !== gen || !final && bars.length === 0) return;
8085
8113
  if (!painted && !final && bars.length < Math.min(requested, FIRST_PAINT_BARS)) return;
8114
+ if (painted && this.replayQueue) {
8115
+ this.mergeHistoryIntoReplay(bars);
8116
+ return;
8117
+ }
8086
8118
  this.setBarSeries(bars, painted ? { preserveView: true } : void 0);
8087
8119
  if (!painted && bars.length > 0) {
8088
8120
  painted = true;
@@ -8225,6 +8257,36 @@ var Vela = (function (exports) {
8225
8257
  // exactly like a timeframe change; the cache keys the sessions apart.
8226
8258
  next2.session !== void 0 && next2.session !== m.session || next2.data !== void 0;
8227
8259
  const depthChanged = next2.bars !== void 0 && next2.bars !== m.bars;
8260
+ const sameClock = identityChanged && next2.data === void 0 && (next2.symbol === void 0 || next2.symbol === m.symbol) && (next2.timeframe !== void 0 || next2.session !== void 0);
8261
+ const pending = identityChanged ? this.replayCarrying : null;
8262
+ if (pending) {
8263
+ this.replayCarrying = null;
8264
+ if (!sameClock) {
8265
+ pending.cancelled = true;
8266
+ this.events.emit("replay:end", { reason: "market" });
8267
+ }
8268
+ }
8269
+ const replayCarry = this.replayQueue && sameClock && this.rawBars.length > 0 ? { revealedEnd: this.rawBars[this.rawBars.length - 1].time + this.barIntervalMs(), playing: this.replayPlaying, cancelled: false } : pending && sameClock ? { revealedEnd: pending.revealedEnd, playing: pending.playing, cancelled: false } : null;
8270
+ if (pending) pending.cancelled = true;
8271
+ if (replayCarry) this.replayCarrying = replayCarry;
8272
+ if (replayCarry && this.replayQueue) {
8273
+ this.endReplay(null, { restore: false, resume: false });
8274
+ const depth = this.replayDepth;
8275
+ this.replayDepth = null;
8276
+ if (depth && next2.bars === void 0) {
8277
+ if (depth.bars === void 0) delete m.bars;
8278
+ else m.bars = depth.bars;
8279
+ }
8280
+ }
8281
+ if (this.replayQueue && (identityChanged || depthChanged)) this.endReplay("market", { restore: !identityChanged, resume: false });
8282
+ if (this.replayLoading && (identityChanged || depthChanged)) {
8283
+ const depth = this.replayDepth;
8284
+ this.abandonReplayLoad("market");
8285
+ if (depth && next2.bars === void 0) {
8286
+ if (depth.bars === void 0) delete m.bars;
8287
+ else m.bars = depth.bars;
8288
+ }
8289
+ }
8228
8290
  const depthOnly = depthChanged && !identityChanged && this.rawBars.length > 0 && !m.data?.length && ((next2.bars ?? 0) <= this.rawBars.length || typeof this.feed.loadRange === "function");
8229
8291
  if (!identityChanged && !depthChanged) {
8230
8292
  if (typeof next2.visibleRange === "string") this.setVisibleRangePreset(next2.visibleRange);
@@ -8282,15 +8344,20 @@ var Vela = (function (exports) {
8282
8344
  const from = bars[Math.max(0, idx - (carriedCount - 1))].time;
8283
8345
  this.renderer.setVisibleRange({ from, to: Math.max(carried.right, bars[idx].time + 1) });
8284
8346
  }
8285
- if (!depthOnly) {
8347
+ const carry = replayCarry && !replayCarry.cancelled ? replayCarry : null;
8348
+ const carryFrom = carry ? carry.revealedEnd - this.barIntervalMs() : 0;
8349
+ const carryNow = carry != null && this.rawBars.length > 0 && carryFrom >= this.rawBars[0].time;
8350
+ const replayBack = carryNow && this.resumeCarriedReplay(this.replayStart({ from: carryFrom }), carry);
8351
+ if (!depthOnly && !replayBack) {
8286
8352
  this.restartNativeIndicators();
8287
8353
  this.restartChartTypeEngine();
8288
8354
  this.reexecuteIndicators();
8289
8355
  }
8290
- this.startLive();
8356
+ if (!replayBack) this.startLive();
8291
8357
  if (identityChanged) {
8292
8358
  this.events.emit("market:changed", { symbol: m.symbol ?? "TEST", timeframe: m.timeframe ?? "60", prev: prev3 });
8293
8359
  }
8360
+ if (carry && !carryNow) this.resumeCarriedReplay(this.replayStart({ from: carryFrom }), carry);
8294
8361
  }
8295
8362
  /**
8296
8363
  * Restart every native indicator over the new market: a `NativeIndicatorContext`
@@ -8414,6 +8481,397 @@ var Vela = (function (exports) {
8414
8481
  historyComplete() {
8415
8482
  return this.historyCompletePromise;
8416
8483
  }
8484
+ // ── bar replay ──────────────────────────────────────────────
8485
+ /**
8486
+ * Enter replay — or seek, when already replaying — keeping every bar opened at or before
8487
+ * `opts.from` and queueing the rest. A `from` inside the loaded bars cuts at once, even
8488
+ * while older history is still streaming in: what lands later joins the tape's head
8489
+ * (see {@link mergeHistoryIntoReplay}) and leaves the cut alone. An older `from` waits
8490
+ * for that load, then deepens if it still has to. Consumers restart over the cut exactly
8491
+ * as on a market switch — their models describe bars that are now hidden — and the
8492
+ * active chart-type engine is rebuilt without live data.
8493
+ */
8494
+ async replayStart(opts) {
8495
+ if (this.rawBars.length === 0) {
8496
+ console.warn("[vela] chart.replay.start() ignored \u2014 the chart has no bars yet");
8497
+ return;
8498
+ }
8499
+ let gen = this.generation;
8500
+ if (opts.from < this.rawBars[0].time) {
8501
+ await this.historyCompletePromise;
8502
+ if (this.generation !== gen || this.rawBars.length === 0) return;
8503
+ }
8504
+ const oldest = this.rawBars[0].time;
8505
+ if (opts.from < oldest && this.canHeal() && typeof this.feed.loadRange === "function") {
8506
+ const held = this.rawBars.length + (this.replayQueue?.length ?? 0);
8507
+ const bars = held + Math.ceil((oldest - opts.from) / this.barIntervalMs()) + 1;
8508
+ const seek = this.replayQueue !== null;
8509
+ if (seek) this.endReplay(null, { restore: true, resume: false });
8510
+ this.replayDepth ?? (this.replayDepth = { bars: this.config.market.bars });
8511
+ const deepened = this.setMarket({ bars });
8512
+ this.replayLoading = { seek };
8513
+ await deepened;
8514
+ gen = this.generation;
8515
+ await this.historyCompletePromise;
8516
+ if (!this.replayLoading || this.generation !== gen || this.rawBars.length === 0) return;
8517
+ this.replayLoading = null;
8518
+ }
8519
+ this.settleFormingBar();
8520
+ this.dropTickFetches();
8521
+ const tape = this.replayQueue ? [...this.rawBars, ...this.replayQueue] : this.rawBars;
8522
+ let cut = 0;
8523
+ while (cut < tape.length && tape[cut].time <= opts.from) cut += 1;
8524
+ cut = Math.max(1, cut);
8525
+ if (cut >= tape.length) {
8526
+ console.warn("[vela] chart.replay.start() ignored \u2014 no bar opens after the requested time");
8527
+ return;
8528
+ }
8529
+ const wasPlaying = this.replayPlaying;
8530
+ this.clearReplayTimer();
8531
+ if (!this.replayQueue) this.stopLive();
8532
+ this.replayQueue = tape.slice(cut);
8533
+ this.setBarSeries(tape.slice(0, cut));
8534
+ this.restartBarConsumers({ blank: true });
8535
+ if (wasPlaying) this.events.emit("replay:pause", void 0);
8536
+ this.events.emit("replay:start", { cursorTime: tape[cut - 1].time, remaining: this.replayQueue.length });
8537
+ this.prefetchTicks();
8538
+ }
8539
+ /** Reveal the next queued bar through the live-bar path — or complete the bar forming
8540
+ * tick by tick; the last one ends the replay. */
8541
+ replayStep() {
8542
+ if (this.replayForming) {
8543
+ this.finishFormingBar();
8544
+ return true;
8545
+ }
8546
+ const queue = this.replayQueue;
8547
+ const bar = queue?.shift();
8548
+ if (!queue || !bar) return false;
8549
+ this.replayTickFetches.delete(bar.time);
8550
+ this.applyBar(bar);
8551
+ this.completeReveal(bar);
8552
+ this.prefetchTicks();
8553
+ return true;
8554
+ }
8555
+ /**
8556
+ * Follow up a replay carried onto a new series of the same symbol (another timeframe or
8557
+ * session). The cut asked for keeps the bars that CLOSED by the end of what was revealed —
8558
+ * every finer bar of the last revealed one, never a coarser bar still open at that point
8559
+ * (no look-ahead). Once it settles: playback resumes if it was on; if the replay did not
8560
+ * come back (nothing left after that point, or a later switch took over), it ends as a
8561
+ * market switch would. Returns whether the replay is back already — a start with no
8562
+ * history to wait for cuts synchronously (an async function runs up to its first await).
8563
+ */
8564
+ resumeCarriedReplay(start, carry) {
8565
+ if (this.replayQueue && this.replayCarrying === carry) this.replayCarrying = null;
8566
+ void start.then(() => {
8567
+ if (this.replayCarrying === carry) this.replayCarrying = null;
8568
+ if (carry.cancelled) return;
8569
+ if (!this.replayQueue) this.events.emit("replay:end", { reason: "market" });
8570
+ else if (carry.playing) this.replayPlay();
8571
+ });
8572
+ return this.replayQueue !== null;
8573
+ }
8574
+ replayStepUpdate() {
8575
+ if (!this.replayQueue) return false;
8576
+ if (this.replayForming) {
8577
+ this.applyReplayTicks(1);
8578
+ return true;
8579
+ }
8580
+ const bar = this.replayQueue[0];
8581
+ if (!bar) return false;
8582
+ const entry = this.replayTickSource ? this.requestTicks(bar) : null;
8583
+ if (entry && !entry.ticks) {
8584
+ const epoch = this.replayTickAbort;
8585
+ void entry.done.then(() => {
8586
+ if (!this.replayPlaying && !this.replayForming && this.replayTickAbort === epoch && this.replayQueue?.[0] === bar) this.replayStepUpdate();
8587
+ });
8588
+ return true;
8589
+ }
8590
+ if (!entry?.ticks?.length) return this.replayStep();
8591
+ this.openFormingBar(entry.ticks, 1);
8592
+ return true;
8593
+ }
8594
+ replaySetTicks(source) {
8595
+ this.finishFormingBar();
8596
+ this.dropTickFetches();
8597
+ this.replayTickSource = source;
8598
+ this.prefetchTicks();
8599
+ }
8600
+ replayPlay(intervalMs2) {
8601
+ if (!this.replayQueue) return;
8602
+ if (intervalMs2 !== void 0) {
8603
+ if (!(Number.isFinite(intervalMs2) && intervalMs2 > 0)) {
8604
+ console.warn(`[vela] chart.replay.play(${intervalMs2}) ignored \u2014 the interval must be a positive number of ms`);
8605
+ return;
8606
+ }
8607
+ this.replayIntervalMs = intervalMs2;
8608
+ }
8609
+ this.clearReplayTimer();
8610
+ this.replayPlaying = true;
8611
+ if (this.replayForming) this.paceFormingBar(this.replayForming);
8612
+ this.scheduleReplayStep(this.replayForming?.delayMs);
8613
+ this.events.emit("replay:play", { intervalMs: this.replayIntervalMs });
8614
+ }
8615
+ replayPause() {
8616
+ if (!this.replayPlaying) return;
8617
+ this.clearReplayTimer();
8618
+ this.events.emit("replay:pause", void 0);
8619
+ }
8620
+ replayStop() {
8621
+ const carrying = this.replayCarrying;
8622
+ if (carrying) {
8623
+ carrying.cancelled = true;
8624
+ this.replayCarrying = null;
8625
+ this.events.emit("replay:end", { reason: "stopped" });
8626
+ }
8627
+ if (this.replayLoading) {
8628
+ const depth = this.replayDepth;
8629
+ this.abandonReplayLoad("stopped");
8630
+ void this.setMarket({ bars: depth?.bars ?? 500 });
8631
+ return;
8632
+ }
8633
+ this.endReplay("stopped", { restore: true, resume: true });
8634
+ }
8635
+ abandonReplayLoad(reason) {
8636
+ const loading = this.replayLoading;
8637
+ if (!loading) return;
8638
+ this.replayLoading = null;
8639
+ this.replayDepth = null;
8640
+ if (loading.seek) this.events.emit("replay:end", { reason });
8641
+ }
8642
+ replayState() {
8643
+ const active = this.replayQueue !== null;
8644
+ return {
8645
+ active: active || this.replayCarrying !== null,
8646
+ // a replay carried over a switch stays on while the new bars load
8647
+ playing: this.replayCarrying?.playing ?? this.replayPlaying,
8648
+ cursorTime: active ? this.rawBars[this.rawBars.length - 1]?.time ?? null : null,
8649
+ remaining: this.replayQueue?.length ?? 0,
8650
+ nextTime: this.replayForming?.bar.time ?? this.replayQueue?.[0]?.time ?? null,
8651
+ intervalMs: this.replayIntervalMs
8652
+ };
8653
+ }
8654
+ replayBounds() {
8655
+ const first2 = this.rawBars[0];
8656
+ const last2 = this.replayQueue?.[this.replayQueue.length - 1] ?? this.rawBars[this.rawBars.length - 1];
8657
+ return first2 && last2 ? { first: first2.time, last: last2.time } : null;
8658
+ }
8659
+ scheduleReplayStep(delayMs = this.replayIntervalMs) {
8660
+ this.replayTimer = setTimeout(() => {
8661
+ this.replayTimer = null;
8662
+ this.replayAdvance();
8663
+ }, delayMs);
8664
+ }
8665
+ /** One beat of timed playback: the forming bar's next ticks, else the next bar — whole,
8666
+ * or opened from its ticks (waiting for them when they have not landed yet). */
8667
+ replayAdvance() {
8668
+ if (!this.replayPlaying || !this.replayQueue) return;
8669
+ const forming = this.replayForming;
8670
+ if (forming) {
8671
+ const due = Math.floor((now() - forming.origin) / this.replayIntervalMs) + 1;
8672
+ this.applyReplayTicks(Math.max(forming.batch, due - forming.next));
8673
+ if (this.replayPlaying) this.scheduleReplayStep(forming.delayMs);
8674
+ return;
8675
+ }
8676
+ const bar = this.replayQueue[0];
8677
+ if (!bar) return;
8678
+ const entry = this.replayTickSource ? this.requestTicks(bar) : null;
8679
+ if (entry && !entry.ticks) {
8680
+ const epoch = this.replayTickAbort;
8681
+ void entry.done.then(() => {
8682
+ if (this.replayPlaying && this.replayTimer == null && this.replayTickAbort === epoch && this.replayQueue?.[0] === bar) this.replayAdvance();
8683
+ });
8684
+ return;
8685
+ }
8686
+ if (!entry?.ticks?.length) {
8687
+ if (this.replayStep() && this.replayPlaying) this.scheduleReplayStep();
8688
+ return;
8689
+ }
8690
+ this.openFormingBar(entry.ticks);
8691
+ if (this.replayPlaying) this.scheduleReplayStep(this.replayForming?.delayMs);
8692
+ }
8693
+ /** Take the next queued bar off the tape and open it at its first tick (the first
8694
+ * `count` ticks — default: one timed update's worth). */
8695
+ openFormingBar(ticks, count) {
8696
+ const bar = this.replayQueue.shift();
8697
+ const volumeEach = ticks.every((t) => t.volume === void 0) ? (bar.volume ?? 0) / ticks.length : null;
8698
+ const forming = { bar, ticks, next: 0, candle: null, volumeEach, batch: 1, delayMs: this.replayIntervalMs, origin: 0 };
8699
+ this.paceFormingBar(forming);
8700
+ this.replayForming = forming;
8701
+ this.applyReplayTicks(count ?? forming.batch);
8702
+ this.prefetchTicks();
8703
+ }
8704
+ /** One tick per interval, batched when the interval is shorter than a frame; the clock
8705
+ * is anchored on the ticks already applied (a resume or a pace change carries on from
8706
+ * there, never jumps). */
8707
+ paceFormingBar(f) {
8708
+ f.batch = Math.max(1, Math.ceil(MIN_TICK_UPDATE_MS / this.replayIntervalMs));
8709
+ f.delayMs = this.replayIntervalMs * f.batch;
8710
+ f.origin = now() - Math.max(0, f.next - 1) * this.replayIntervalMs;
8711
+ }
8712
+ /** Fold the next `count` ticks into the forming candle; the last one settles the bar. */
8713
+ applyReplayTicks(count) {
8714
+ const f = this.replayForming;
8715
+ if (!f) return;
8716
+ let c = f.candle;
8717
+ const stop = Math.min(f.ticks.length, f.next + count);
8718
+ for (; f.next < stop; f.next += 1) {
8719
+ const t = f.ticks[f.next];
8720
+ const v = f.volumeEach ?? t.volume ?? 0;
8721
+ const hi = Math.max(t.high ?? t.price, t.price);
8722
+ const lo = Math.min(t.low ?? t.price, t.price);
8723
+ if (c) {
8724
+ c = { ...c, high: Math.max(c.high, hi), low: Math.min(c.low, lo), close: t.price, volume: (c.volume ?? 0) + v };
8725
+ } else {
8726
+ const open2 = t.open ?? t.price;
8727
+ c = { time: f.bar.time, open: open2, high: Math.max(open2, hi), low: Math.min(open2, lo), close: t.price, volume: v };
8728
+ }
8729
+ }
8730
+ f.candle = c;
8731
+ if (f.next >= f.ticks.length) {
8732
+ this.finishFormingBar();
8733
+ return;
8734
+ }
8735
+ this.applyBar(c);
8736
+ this.events.emit("replay:tick", { cursorTime: f.bar.time, index: f.next - 1, count: f.ticks.length });
8737
+ }
8738
+ /** Settle the forming bar on its stored values and announce it revealed. */
8739
+ finishFormingBar() {
8740
+ const f = this.replayForming;
8741
+ if (!f) return;
8742
+ this.replayForming = null;
8743
+ this.replayTickFetches.delete(f.bar.time);
8744
+ this.applyBar(f.bar);
8745
+ this.events.emit("replay:tick", { cursorTime: f.bar.time, index: f.ticks.length - 1, count: f.ticks.length });
8746
+ this.completeReveal(f.bar);
8747
+ }
8748
+ /** Put the forming bar back in its stored form, silently — a seek or an end rebuilds
8749
+ * from the tape, which must hold the stored bar. */
8750
+ settleFormingBar() {
8751
+ const f = this.replayForming;
8752
+ if (!f) return;
8753
+ this.replayForming = null;
8754
+ this.applyBar(f.bar);
8755
+ }
8756
+ /**
8757
+ * A history answer landing mid-replay (a progressive snapshot — the whole answer so far):
8758
+ * the tape keeps its cut and its hidden queue; bars older than the tape join its head,
8759
+ * bars newer than it join the queue's end.
8760
+ */
8761
+ mergeHistoryIntoReplay(bars) {
8762
+ const queue = this.replayQueue;
8763
+ const head = this.rawBars[0];
8764
+ if (!queue || !head) return;
8765
+ const tail = queue[queue.length - 1] ?? this.rawBars[this.rawBars.length - 1];
8766
+ let older = 0;
8767
+ while (older < bars.length && bars[older].time < head.time) older += 1;
8768
+ let newer = bars.length;
8769
+ while (newer > older && bars[newer - 1].time > tail.time) newer -= 1;
8770
+ if (newer < bars.length) queue.push(...bars.slice(newer));
8771
+ if (older > 0) {
8772
+ this.setBarSeries([...bars.slice(0, older), ...this.rawBars], { preserveView: true });
8773
+ this.notifySessionsBars("backfill");
8774
+ }
8775
+ }
8776
+ /** A bar is fully on screen: announce it, and end the replay if it was the last. */
8777
+ completeReveal(bar) {
8778
+ const queue = this.replayQueue;
8779
+ if (!queue) return;
8780
+ this.events.emit("replay:step", { cursorTime: bar.time, remaining: queue.length });
8781
+ if (queue.length === 0) this.endReplay("finished", { restore: true, resume: true });
8782
+ }
8783
+ /** Ask for the ticks of the next hidden bar ahead of time. */
8784
+ prefetchTicks() {
8785
+ const next2 = this.replayQueue?.[0];
8786
+ if (next2 && this.replayTickSource) this.requestTicks(next2);
8787
+ }
8788
+ /** The (shared) tick request of a queued bar — the source runs once per bar. */
8789
+ requestTicks(bar) {
8790
+ const known = this.replayTickFetches.get(bar.time);
8791
+ if (known) return known;
8792
+ const source = this.replayTickSource;
8793
+ this.replayTickAbort ?? (this.replayTickAbort = new AbortController());
8794
+ const signal = this.replayTickAbort.signal;
8795
+ const queue = this.replayQueue ?? [];
8796
+ const end = queue[queue.indexOf(bar) + 1]?.time ?? bar.time + this.barIntervalMs();
8797
+ const entry = { ticks: null, done: Promise.resolve() };
8798
+ const land = (ticks) => {
8799
+ entry.ticks = usableTicks(ticks);
8800
+ };
8801
+ const fail = (e) => {
8802
+ if (!signal.aborted) console.warn(`[vela] replay tick source failed \u2014 revealing the bar whole (${e instanceof Error ? e.message : String(e)})`);
8803
+ land([]);
8804
+ };
8805
+ try {
8806
+ const out = source(bar, { end, signal });
8807
+ if (Array.isArray(out)) land(out);
8808
+ else entry.done = Promise.resolve(out).then(land, fail);
8809
+ } catch (e) {
8810
+ fail(e);
8811
+ }
8812
+ this.replayTickFetches.set(bar.time, entry);
8813
+ return entry;
8814
+ }
8815
+ /** Abort the tick requests in flight and forget the landed ones. */
8816
+ dropTickFetches() {
8817
+ this.replayTickAbort?.abort();
8818
+ this.replayTickAbort = null;
8819
+ this.replayTickFetches.clear();
8820
+ }
8821
+ clearReplayTimer() {
8822
+ if (this.replayTimer != null) clearTimeout(this.replayTimer);
8823
+ this.replayTimer = null;
8824
+ this.replayPlaying = false;
8825
+ }
8826
+ /**
8827
+ * Leave replay. `restore` puts the queued bars back and restarts the consumers over the
8828
+ * full history (a market switch skips it: its load replaces everything anyway); `resume`
8829
+ * restarts live updates and heals the bars that closed while replaying. A null reason
8830
+ * leaves silently — a seek that re-enters right after.
8831
+ */
8832
+ endReplay(reason, opts) {
8833
+ const queue = this.replayQueue;
8834
+ if (!queue) return;
8835
+ this.clearReplayTimer();
8836
+ if (opts.restore) this.settleFormingBar();
8837
+ else this.replayForming = null;
8838
+ this.dropTickFetches();
8839
+ this.replayQueue = null;
8840
+ const depth = reason ? this.replayDepth : null;
8841
+ if (reason) this.replayDepth = null;
8842
+ if (depth) {
8843
+ if (depth.bars === void 0) delete this.config.market.bars;
8844
+ else this.config.market.bars = depth.bars;
8845
+ }
8846
+ if (opts.restore) {
8847
+ const keep = depth ? depth.bars ?? 500 : Infinity;
8848
+ if (queue.length > 0 || this.rawBars.length > keep) {
8849
+ const full = [...this.rawBars, ...queue];
8850
+ this.setBarSeries(full.length > keep ? full.slice(full.length - keep) : full);
8851
+ this.restartBarConsumers({ blank: false });
8852
+ } else if (this.config.live) {
8853
+ this.restartNativeIndicators();
8854
+ this.restartChartTypeEngine();
8855
+ }
8856
+ }
8857
+ if (opts.resume && this.config.live) {
8858
+ this.startLive();
8859
+ const last2 = this.rawBars[this.rawBars.length - 1];
8860
+ if (last2 && this.canHeal()) void this.healGap(last2.time);
8861
+ }
8862
+ if (reason) this.events.emit("replay:end", { reason });
8863
+ }
8864
+ /**
8865
+ * The bars were replaced by a cut or a restore of the SAME market: every consumer re-runs.
8866
+ * A cut also blanks the mounted models first — they still paint the hidden bars' values,
8867
+ * which must not show until the re-runs land.
8868
+ */
8869
+ restartBarConsumers(opts) {
8870
+ if (opts.blank) this.blankIndicatorVisuals();
8871
+ this.restartNativeIndicators();
8872
+ this.restartChartTypeEngine();
8873
+ this.reexecuteIndicators();
8874
+ }
8417
8875
  /** The primary symbol as the data layer resolves it, or null if no symbol is set. */
8418
8876
  qualifiedSymbol() {
8419
8877
  const market = this.config.market;
@@ -8485,7 +8943,7 @@ var Vela = (function (exports) {
8485
8943
  engine.start({
8486
8944
  symbol: this.config.market.symbol ?? "TEST",
8487
8945
  timeframe: this.config.market.timeframe ?? "60",
8488
- live: this.config.live ?? false,
8946
+ live: (this.config.live ?? false) && this.replayQueue === null,
8489
8947
  session: this.config.market.session,
8490
8948
  bars: () => this.bars,
8491
8949
  data: this.dataControl,
@@ -8611,6 +9069,7 @@ var Vela = (function (exports) {
8611
9069
  record.session.notifyBars(reason);
8612
9070
  } else if (record.native) record.native.instance.onBars();
8613
9071
  }
9072
+ if (this.activeEngineStyle) this.typeEngines.get(this.activeEngineStyle)?.onBars?.();
8614
9073
  }
8615
9074
  /** Gap healing needs a ranged feed and a provider-backed series (offline `data` has no source). */
8616
9075
  canHeal() {
@@ -8633,10 +9092,11 @@ var Vela = (function (exports) {
8633
9092
  try {
8634
9093
  const bars = await this.feed.loadRange(this.config.market, { from: fromMs });
8635
9094
  if (this.generation !== gen) return;
9095
+ if (this.replayQueue) return;
8636
9096
  for (const b of bars) this.applyBar(b, false);
8637
9097
  } catch {
8638
9098
  } finally {
8639
- if (this.generation === gen) {
9099
+ if (this.generation === gen && !this.replayQueue) {
8640
9100
  this.healing = false;
8641
9101
  const pending = this.healBuffer;
8642
9102
  this.healBuffer = [];
@@ -8941,6 +9401,13 @@ var Vela = (function (exports) {
8941
9401
  destroy() {
8942
9402
  this.bumpGeneration();
8943
9403
  this.resolveHistoryComplete();
9404
+ this.clearReplayTimer();
9405
+ this.replayQueue = null;
9406
+ this.replayForming = null;
9407
+ this.replayCarrying = null;
9408
+ this.dropTickFetches();
9409
+ this.replayDepth = null;
9410
+ this.replayLoading = null;
8944
9411
  this.stopLive();
8945
9412
  if (this.viewportTimer != null) clearTimeout(this.viewportTimer);
8946
9413
  this.viewportUnsub?.();
@@ -9039,6 +9506,7 @@ var Vela = (function (exports) {
9039
9506
  this.emitScriptRun(id, cause, first2);
9040
9507
  },
9041
9508
  onAlert: (a) => {
9509
+ if (this.replayQueue || this.replayCarrying) return;
9042
9510
  const indicator = record.options?.title ?? record.prepared?.meta.title ?? record.title;
9043
9511
  this.events.emit("alert", { ...a, indicator });
9044
9512
  handle.emit("alert", { id: a.id, message: a.message, title: a.title, time: a.time });
@@ -9065,7 +9533,7 @@ var Vela = (function (exports) {
9065
9533
  id,
9066
9534
  symbol: this.config.market.symbol ?? "TEST",
9067
9535
  timeframe: this.config.market.timeframe ?? "60",
9068
- live: this.config.live,
9536
+ live: this.config.live && this.replayQueue === null,
9069
9537
  session: this.config.market.session,
9070
9538
  bars: () => this.bars,
9071
9539
  data: this.dataControl,
@@ -9468,17 +9936,17 @@ var Vela = (function (exports) {
9468
9936
  }
9469
9937
  emitContextChanged(id) {
9470
9938
  if (!this.registry.get(id)?.session?.getContext) return;
9471
- const now = Date.now();
9472
- if (now - (this.contextEmitAt.get(id) ?? 0) < 1e3) return;
9473
- this.contextEmitAt.set(id, now);
9939
+ const now2 = Date.now();
9940
+ if (now2 - (this.contextEmitAt.get(id) ?? 0) < 1e3) return;
9941
+ this.contextEmitAt.set(id, now2);
9474
9942
  this.events.emit("context:changed", { id });
9475
9943
  }
9476
9944
  emitScriptRun(id, cause, first2) {
9477
9945
  const record = this.registry.get(id);
9478
9946
  if (!record || record.native || !this.events.hasListeners("script:run")) return;
9479
- const now = Date.now();
9480
- if (cause === "tick" && now - (this.runEmitAt.get(id) ?? 0) < RUN_EMIT_THROTTLE_MS) return;
9481
- this.runEmitAt.set(id, now);
9947
+ const now2 = Date.now();
9948
+ if (cause === "tick" && now2 - (this.runEmitAt.get(id) ?? 0) < RUN_EMIT_THROTTLE_MS) return;
9949
+ this.runEmitAt.set(id, now2);
9482
9950
  void Promise.resolve().then(() => this.buildScriptRun(record, cause, first2)).then((run) => {
9483
9951
  if (run && this.registry.get(id)) this.events.emit("script:run", run);
9484
9952
  });
@@ -9531,6 +9999,16 @@ var Vela = (function (exports) {
9531
9999
  handle?.emit("error", { error });
9532
10000
  }
9533
10001
  };
10002
+ function now() {
10003
+ return typeof performance !== "undefined" ? performance.now() : Date.now();
10004
+ }
10005
+ function usableTicks(value) {
10006
+ if (!Array.isArray(value)) return [];
10007
+ const optional = (n) => n === void 0 || Number.isFinite(n);
10008
+ return value.filter(
10009
+ (t) => t != null && Number.isFinite(t.price) && optional(t.volume) && optional(t.high) && optional(t.low) && optional(t.open)
10010
+ );
10011
+ }
9534
10012
  function yieldToPaint() {
9535
10013
  if (typeof requestAnimationFrame === "function") {
9536
10014
  return new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(() => resolve())));
@@ -9731,6 +10209,12 @@ var Vela = (function (exports) {
9731
10209
  onCrosshairMove(cb) {
9732
10210
  return this.renderer.onCrosshairMove(cb);
9733
10211
  }
10212
+ /** A click — or a touch tap — on the plot, never the end of a pan: the open time of
10213
+ * the bar under it (`null` off the bars). On touch, where a tap moves no crosshair,
10214
+ * this is how an interaction learns which bar was chosen. */
10215
+ onClick(cb) {
10216
+ return this.renderer.onClick(cb);
10217
+ }
9734
10218
  /** Touch long-press on a price or time axis strip — silent no-op without the seam. */
9735
10219
  onAxisLongPress(cb) {
9736
10220
  return this.renderer.onAxisLongPress?.(cb) ?? (() => void 0);
@@ -10806,11 +11290,76 @@ var Vela = (function (exports) {
10806
11290
  }
10807
11291
  };
10808
11292
 
11293
+ // src/core/ReplayControl.ts
11294
+ var ReplayControl = class {
11295
+ constructor(ctrl) {
11296
+ this.ctrl = ctrl;
11297
+ }
11298
+ /**
11299
+ * Enter replay (or seek, when already replaying) at `opts.from`. Resolves once the
11300
+ * chart shows the rewound history — after any in-flight history load has finished.
11301
+ * Starts paused; call {@link play} or {@link step}.
11302
+ */
11303
+ start(opts) {
11304
+ return this.ctrl.replayStart(opts);
11305
+ }
11306
+ /** Reveal the next bar — or, mid-way through a bar played tick by tick, complete it.
11307
+ * Returns false when replay is off or nothing is left. */
11308
+ step() {
11309
+ return this.ctrl.replayStep();
11310
+ }
11311
+ /** Reveal the next UPDATE: with {@link setTicks}, the next tick of the forming bar — or
11312
+ * the next bar opened at its first tick (once its ticks are in, if still loading);
11313
+ * otherwise the next whole bar, like {@link step}. Returns false when nothing is left. */
11314
+ stepUpdate() {
11315
+ return this.ctrl.replayStepUpdate();
11316
+ }
11317
+ /** Reveal one bar (one tick, with {@link setTicks}) every `intervalMs` (default: the last
11318
+ * interval used, else 1000). Calling it again while playing changes the pace. */
11319
+ play(intervalMs2) {
11320
+ this.ctrl.replayPlay(intervalMs2);
11321
+ return this;
11322
+ }
11323
+ pause() {
11324
+ this.ctrl.replayPause();
11325
+ return this;
11326
+ }
11327
+ /** Leave replay: the full history comes back and live updates resume. Also cancels a
11328
+ * `start()` still loading older history (the chart returns to its previous depth). */
11329
+ stop() {
11330
+ this.ctrl.replayStop();
11331
+ return this;
11332
+ }
11333
+ get state() {
11334
+ return this.ctrl.replayState();
11335
+ }
11336
+ /** The loaded history a replay can walk — hidden bars included while replaying. Null
11337
+ * before any bar loaded. A start older than `first` deepens the history first. */
11338
+ get bounds() {
11339
+ return this.ctrl.replayBounds();
11340
+ }
11341
+ /**
11342
+ * Play each revealed bar as a series of intrabar updates instead of whole: while
11343
+ * playing, the forming candle opens at the first tick, then every tick moves its close
11344
+ * and stretches its high and low, exactly like a live tick — indicators, chart types
11345
+ * and the `bar` event follow — and the last one settles it on the stored bar. The play
11346
+ * interval is then the time between two TICKS: a bar lasts as many intervals as it
11347
+ * has ticks (below ~16 ms, ticks are batched per frame). The next bar's ticks are
11348
+ * requested while the current one plays; playback waits for them. `null` goes back to
11349
+ * whole bars. Takes effect from the next bar; a bar forming when the source changes
11350
+ * completes at once.
11351
+ */
11352
+ setTicks(source) {
11353
+ this.ctrl.replaySetTicks(source);
11354
+ return this;
11355
+ }
11356
+ };
11357
+
10809
11358
  // src/core/util/wall-clock.ts
10810
11359
  var BOUNDARY_SLACK_MS = 5;
10811
11360
  var SecondClock = class _SecondClock {
10812
- constructor(now = () => Date.now()) {
10813
- this.now = now;
11361
+ constructor(now2 = () => Date.now()) {
11362
+ this.now = now2;
10814
11363
  this.subs = /* @__PURE__ */ new Set();
10815
11364
  this.timer = null;
10816
11365
  }
@@ -10826,15 +11375,15 @@ var Vela = (function (exports) {
10826
11375
  };
10827
11376
  }
10828
11377
  /** Milliseconds from `now` to just past the next second boundary. */
10829
- static delayToNextSecond(now) {
10830
- const intoSecond = (now % 1e3 + 1e3) % 1e3;
11378
+ static delayToNextSecond(now2) {
11379
+ const intoSecond = (now2 % 1e3 + 1e3) % 1e3;
10831
11380
  return 1e3 - intoSecond + BOUNDARY_SLACK_MS;
10832
11381
  }
10833
11382
  arm() {
10834
11383
  this.timer = setTimeout(() => {
10835
11384
  this.timer = null;
10836
- const now = this.now();
10837
- for (const cb of this.subs) cb(now);
11385
+ const now2 = this.now();
11386
+ for (const cb of this.subs) cb(now2);
10838
11387
  if (this.subs.size > 0) this.arm();
10839
11388
  }, _SecondClock.delayToNextSecond(this.now()));
10840
11389
  }
@@ -19935,6 +20484,264 @@ ${overlayScrollbarCss(".vela-dialog *")}
19935
20484
  }
19936
20485
  };
19937
20486
 
20487
+ // src/ui/components/date-picker/controller.ts
20488
+ var MONTH_LABELS = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"];
20489
+ var MONTH_SHORT = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
20490
+ var WEEKDAY_LABELS = ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"];
20491
+ function isoDate(d) {
20492
+ return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
20493
+ }
20494
+ function normalizeDateInput(raw) {
20495
+ const m = /^(\d{4})-(\d{1,2})-(\d{1,2})$/.exec(raw.trim());
20496
+ if (!m) return null;
20497
+ const y = Number(m[1]);
20498
+ const mo = Number(m[2]);
20499
+ const d = Number(m[3]);
20500
+ const dt = new Date(y, mo - 1, d);
20501
+ if (dt.getFullYear() !== y || dt.getMonth() !== mo - 1 || dt.getDate() !== d) return null;
20502
+ return isoDate(dt);
20503
+ }
20504
+ function parseIsoDate(raw) {
20505
+ const iso = raw ? normalizeDateInput(raw) : null;
20506
+ if (!iso) return null;
20507
+ const [y, mo, d] = iso.split("-").map(Number);
20508
+ return new Date(y, mo - 1, d);
20509
+ }
20510
+ function datePickerController(opts = {}) {
20511
+ const today = opts.today ?? /* @__PURE__ */ new Date();
20512
+ let selected = parseIsoDate(opts.value);
20513
+ let year = selected?.getFullYear() ?? today.getFullYear();
20514
+ let month = selected?.getMonth() ?? today.getMonth();
20515
+ let panel = "date";
20516
+ const decade = () => Math.floor(year / 10) * 10;
20517
+ return {
20518
+ get panel() {
20519
+ return panel;
20520
+ },
20521
+ get year() {
20522
+ return year;
20523
+ },
20524
+ get month() {
20525
+ return month;
20526
+ },
20527
+ get value() {
20528
+ return selected ? isoDate(selected) : null;
20529
+ },
20530
+ header() {
20531
+ return {
20532
+ month: MONTH_LABELS[month] ?? "",
20533
+ monthVisible: panel === "date",
20534
+ year: panel === "year" ? `${decade()}-${decade() + 9}` : String(year),
20535
+ yearDisabled: panel === "year"
20536
+ // in the decade panel the year reads as its title
20537
+ };
20538
+ },
20539
+ navLabels() {
20540
+ if (panel === "date") return { prev: "Previous month", next: "Next month" };
20541
+ if (panel === "month") return { prev: "Previous year", next: "Next year" };
20542
+ return { prev: "Previous decade", next: "Next decade" };
20543
+ },
20544
+ cells() {
20545
+ const out = [];
20546
+ if (panel === "date") {
20547
+ const startPad = new Date(year, month, 1).getDay();
20548
+ const days = new Date(year, month + 1, 0).getDate();
20549
+ const todayIso = isoDate(today);
20550
+ const selectedIso = selected ? isoDate(selected) : "";
20551
+ for (let i = 0; i < startPad; i++) out.push({ label: "", value: "", checked: false, today: false, blank: true });
20552
+ for (let day = 1; day <= days; day++) {
20553
+ const iso = `${year}-${String(month + 1).padStart(2, "0")}-${String(day).padStart(2, "0")}`;
20554
+ out.push({ label: String(day), value: iso, checked: iso === selectedIso, today: iso === todayIso });
20555
+ }
20556
+ return out;
20557
+ }
20558
+ if (panel === "month") {
20559
+ for (let m = 0; m < 12; m++) {
20560
+ out.push({
20561
+ label: MONTH_SHORT[m] ?? "",
20562
+ value: m,
20563
+ checked: selected?.getFullYear() === year && selected.getMonth() === m,
20564
+ today: today.getFullYear() === year && today.getMonth() === m
20565
+ });
20566
+ }
20567
+ return out;
20568
+ }
20569
+ const d = decade();
20570
+ for (let y = d - 1; y <= d + 10; y++) {
20571
+ out.push({ label: String(y), value: y, checked: selected?.getFullYear() === y, today: today.getFullYear() === y, outside: y < d || y > d + 9 });
20572
+ }
20573
+ return out;
20574
+ },
20575
+ step(dir) {
20576
+ if (panel === "date") {
20577
+ month += dir;
20578
+ if (month < 0) {
20579
+ month = 11;
20580
+ year -= 1;
20581
+ }
20582
+ if (month > 11) {
20583
+ month = 0;
20584
+ year += 1;
20585
+ }
20586
+ } else {
20587
+ year += panel === "month" ? dir : dir * 10;
20588
+ }
20589
+ },
20590
+ showMonths() {
20591
+ panel = "month";
20592
+ },
20593
+ showYears() {
20594
+ panel = "year";
20595
+ },
20596
+ choose(cell) {
20597
+ if (cell.blank) return null;
20598
+ if (panel === "date") {
20599
+ selected = parseIsoDate(String(cell.value));
20600
+ return selected ? isoDate(selected) : null;
20601
+ }
20602
+ if (panel === "month") {
20603
+ month = Number(cell.value);
20604
+ panel = "date";
20605
+ return null;
20606
+ }
20607
+ year = Number(cell.value);
20608
+ panel = "month";
20609
+ return null;
20610
+ },
20611
+ setValue(value) {
20612
+ selected = parseIsoDate(value);
20613
+ if (selected) {
20614
+ year = selected.getFullYear();
20615
+ month = selected.getMonth();
20616
+ }
20617
+ panel = "date";
20618
+ }
20619
+ };
20620
+ }
20621
+
20622
+ // src/ui/components/date-picker/styles.ts
20623
+ var DATE_PICKER_STYLE_ID = "vela-date-picker-styles";
20624
+ var DATE_PICKER_CSS = `
20625
+ .vela-date-picker{color:var(--vela-fg);font:14px var(--vela-font);user-select:none;}
20626
+ .vela-date-picker [hidden]{display:none !important;}
20627
+ .vela-date-picker-head{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:8px;}
20628
+ .vela-date-picker-title{flex:1;display:flex;align-items:center;justify-content:center;gap:2px;min-width:0;}
20629
+ .vela-date-picker-switch{border:none;background:transparent;color:var(--vela-fg-bright);font:inherit;font-weight:600;font-size:14px;padding:2px 6px;border-radius:4px;cursor:pointer;}
20630
+ .vela-date-picker-switch:not(:disabled):hover{background:var(--vela-hover);}
20631
+ .vela-date-picker-switch:disabled{cursor:default;}
20632
+ .vela-date-picker-nav{width:24px;height:24px;border:none;background:transparent;color:var(--vela-fg-muted);border-radius:4px;padding:0;cursor:pointer;display:inline-flex;align-items:center;justify-content:center;font-size:14px;}
20633
+ .vela-date-picker-nav:hover{background:var(--vela-hover);color:var(--vela-fg-bright);}
20634
+ .vela-date-picker-week,.vela-date-picker-grid{display:grid;grid-template-columns:repeat(7,28px);gap:2px;}
20635
+ .vela-date-picker-week{margin-bottom:4px;color:var(--vela-fg-muted);font-size:11px;text-align:center;}
20636
+ .vela-date-picker-week span{line-height:20px;}
20637
+ .vela-date-picker-blank{width:28px;height:28px;}
20638
+ .vela-date-picker-day{width:28px;height:28px;border:none;background:transparent;color:inherit;border-radius:4px;padding:0;cursor:pointer;font:inherit;font-size:14px;}
20639
+ .vela-date-picker-day:hover{background:var(--vela-hover);}
20640
+ .vela-date-picker-day[data-checked]{background:var(--vela-hover-strong);color:var(--vela-fg-bright);}
20641
+ .vela-date-picker-day[data-today]:not([data-checked]){box-shadow:inset 0 0 0 1px var(--vela-border-strong);}
20642
+ .vela-date-picker-cells{display:grid;grid-template-columns:repeat(3,1fr);gap:4px;width:calc(7 * 28px + 6 * 2px);}
20643
+ .vela-date-picker-cell{height:36px;border:none;background:transparent;color:inherit;border-radius:4px;padding:0 4px;cursor:pointer;font:inherit;font-size:14px;}
20644
+ .vela-date-picker-cell:hover{background:var(--vela-hover);}
20645
+ .vela-date-picker-cell[data-checked]{background:var(--vela-hover-strong);color:var(--vela-fg-bright);}
20646
+ .vela-date-picker-cell[data-today]:not([data-checked]){box-shadow:inset 0 0 0 1px var(--vela-border-strong);}
20647
+ .vela-date-picker-cell[data-outside]{opacity:0.45;}
20648
+ `;
20649
+
20650
+ // src/ui/components/date-picker/view.ts
20651
+ var DatePicker = class {
20652
+ constructor(opts = {}) {
20653
+ this.opts = opts;
20654
+ const doc = document;
20655
+ injectStyles(DATE_PICKER_STYLE_ID, DATE_PICKER_CSS, doc);
20656
+ this.ctrl = datePickerController(opts);
20657
+ const button = (className) => {
20658
+ const b = doc.createElement("button");
20659
+ b.type = "button";
20660
+ b.className = className;
20661
+ return b;
20662
+ };
20663
+ this.prev = button("vela-date-picker-nav");
20664
+ this.prev.append(iconEl("chevron-left", doc));
20665
+ this.next = button("vela-date-picker-nav");
20666
+ this.next.append(iconEl("chevron-right", doc));
20667
+ this.monthBtn = button("vela-date-picker-switch");
20668
+ this.monthBtn.setAttribute("aria-label", "Choose month");
20669
+ this.yearBtn = button("vela-date-picker-switch");
20670
+ this.yearBtn.setAttribute("aria-label", "Choose year");
20671
+ const title = doc.createElement("div");
20672
+ title.className = "vela-date-picker-title";
20673
+ title.append(this.monthBtn, this.yearBtn);
20674
+ const head = doc.createElement("div");
20675
+ head.className = "vela-date-picker-head";
20676
+ head.append(this.prev, title, this.next);
20677
+ this.week = doc.createElement("div");
20678
+ this.week.className = "vela-date-picker-week";
20679
+ for (const d of WEEKDAY_LABELS) {
20680
+ const cell = doc.createElement("span");
20681
+ cell.textContent = d;
20682
+ this.week.appendChild(cell);
20683
+ }
20684
+ this.grid = doc.createElement("div");
20685
+ const on = (el, run) => el.addEventListener("click", (e) => {
20686
+ e.stopPropagation();
20687
+ run();
20688
+ this.paint();
20689
+ });
20690
+ on(this.prev, () => this.ctrl.step(-1));
20691
+ on(this.next, () => this.ctrl.step(1));
20692
+ on(this.monthBtn, () => this.ctrl.showMonths());
20693
+ on(this.yearBtn, () => this.ctrl.showYears());
20694
+ this.el = doc.createElement("div");
20695
+ this.el.className = "vela-date-picker";
20696
+ this.el.append(head, this.week, this.grid);
20697
+ this.paint();
20698
+ }
20699
+ get value() {
20700
+ return this.ctrl.value;
20701
+ }
20702
+ /** Rewrite the selection without emitting (the calendar opens on its month). */
20703
+ setValue(value) {
20704
+ this.ctrl.setValue(value);
20705
+ this.paint();
20706
+ }
20707
+ paint() {
20708
+ const doc = this.el.ownerDocument;
20709
+ const h = this.ctrl.header();
20710
+ this.monthBtn.hidden = !h.monthVisible;
20711
+ this.monthBtn.textContent = h.month;
20712
+ this.yearBtn.textContent = h.year;
20713
+ this.yearBtn.disabled = h.yearDisabled;
20714
+ const nav = this.ctrl.navLabels();
20715
+ this.prev.setAttribute("aria-label", nav.prev);
20716
+ this.next.setAttribute("aria-label", nav.next);
20717
+ const dates = this.ctrl.panel === "date";
20718
+ this.week.hidden = !dates;
20719
+ this.grid.className = dates ? "vela-date-picker-grid" : "vela-date-picker-cells";
20720
+ this.grid.replaceChildren(...this.ctrl.cells().map((cell) => this.cellEl(doc, cell, dates)));
20721
+ }
20722
+ cellEl(doc, cell, dates) {
20723
+ if (cell.blank) {
20724
+ const blank = doc.createElement("span");
20725
+ blank.className = "vela-date-picker-blank";
20726
+ return blank;
20727
+ }
20728
+ const b = doc.createElement("button");
20729
+ b.type = "button";
20730
+ b.className = dates ? "vela-date-picker-day" : "vela-date-picker-cell";
20731
+ b.textContent = cell.label;
20732
+ if (cell.checked) b.dataset.checked = "1";
20733
+ if (cell.today) b.dataset.today = "1";
20734
+ if (cell.outside) b.dataset.outside = "1";
20735
+ b.addEventListener("click", (e) => {
20736
+ e.stopPropagation();
20737
+ const picked = this.ctrl.choose(cell);
20738
+ if (picked) this.opts.onPick?.(picked);
20739
+ else this.paint();
20740
+ });
20741
+ return b;
20742
+ }
20743
+ };
20744
+
19938
20745
  // src/ui/components/switch/controller.ts
19939
20746
  function switchController(opts = {}) {
19940
20747
  let checked = opts.checked ?? false;
@@ -21887,10 +22694,16 @@ ${overlayScrollbarCss(".vela-dialog *")}
21887
22694
  this.calendarPop = null;
21888
22695
  this.calendarAnchor = null;
21889
22696
  },
21890
- content: (el) => fillCalendar(el, current, (iso) => {
21891
- pop.hide();
21892
- onPick(iso);
21893
- })
22697
+ content: (el) => {
22698
+ const picker = new DatePicker({
22699
+ value: current,
22700
+ onPick: (iso) => {
22701
+ pop.hide();
22702
+ onPick(iso);
22703
+ }
22704
+ });
22705
+ el.append(picker.el);
22706
+ }
21894
22707
  });
21895
22708
  this.calendarPop = pop;
21896
22709
  this.calendarAnchor = anchor;
@@ -22015,28 +22828,6 @@ ${overlayScrollbarCss(".vela-dialog *")}
22015
22828
  .vela-ind-combo-chevron{position:absolute;right:0;top:0;bottom:0;width:26px;border:none;background:transparent;color:inherit;opacity:0.55;cursor:pointer;display:flex;align-items:center;justify-content:center;padding:0;}
22016
22829
  .vela-ind-combo-chevron:hover{opacity:0.9;}
22017
22830
  .vela-ind-cal{background:var(--vela-bg);color:var(--vela-fg);border:none;border-radius:6px;box-shadow:var(--vela-shadow);font:14px var(--vela-font);padding:10px 12px;user-select:none;}
22018
- .vela-ind-cal [hidden]{display:none !important;}
22019
- .vela-ind-cal-head{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:8px;}
22020
- .vela-ind-cal-title{flex:1;display:flex;align-items:center;justify-content:center;gap:2px;min-width:0;}
22021
- .vela-ind-cal-switch{border:none;background:transparent;color:var(--vela-fg-bright);font:inherit;font-weight:600;font-size:14px;padding:2px 6px;border-radius:4px;cursor:pointer;}
22022
- .vela-ind-cal-switch:not(:disabled):hover{background:var(--vela-hover);}
22023
- .vela-ind-cal-switch:disabled{cursor:default;}
22024
- .vela-ind-cal-nav{width:24px;height:24px;border:none;background:transparent;color:var(--vela-fg-muted);border-radius:4px;padding:0;cursor:pointer;display:inline-flex;align-items:center;justify-content:center;}
22025
- .vela-ind-cal-nav:hover{background:var(--vela-hover);color:var(--vela-fg-bright);}
22026
- .vela-ind-cal-week,.vela-ind-cal-grid{display:grid;grid-template-columns:repeat(7,28px);gap:2px;}
22027
- .vela-ind-cal-week{margin-bottom:4px;color:var(--vela-fg-muted);font-size:11px;text-align:center;}
22028
- .vela-ind-cal-week span{line-height:20px;}
22029
- .vela-ind-cal-blank{width:28px;height:28px;}
22030
- .vela-ind-cal-day{width:28px;height:28px;border:none;background:transparent;color:inherit;border-radius:4px;padding:0;cursor:pointer;font:inherit;font-size:14px;}
22031
- .vela-ind-cal-day:hover{background:var(--vela-hover);}
22032
- .vela-ind-cal-day[data-checked]{background:var(--vela-hover-strong);color:var(--vela-fg-bright);}
22033
- .vela-ind-cal-day[data-today]:not([data-checked]){box-shadow:inset 0 0 0 1px var(--vela-border-strong);}
22034
- .vela-ind-cal-cells{display:grid;grid-template-columns:repeat(3,1fr);gap:4px;width:calc(7 * 28px + 6 * 2px);}
22035
- .vela-ind-cal-cell{height:36px;border:none;background:transparent;color:inherit;border-radius:4px;padding:0 4px;cursor:pointer;font:inherit;font-size:14px;}
22036
- .vela-ind-cal-cell:hover{background:var(--vela-hover);}
22037
- .vela-ind-cal-cell[data-checked]{background:var(--vela-hover-strong);color:var(--vela-fg-bright);}
22038
- .vela-ind-cal-cell[data-today]:not([data-checked]){box-shadow:inset 0 0 0 1px var(--vela-border-strong);}
22039
- .vela-ind-cal-cell[data-outside]{opacity:0.45;}
22040
22831
  ${overlayScrollbarCss(".vela-dialog.vela-ind-dialog *", 9)}
22041
22832
  .vela-ind-tab{font-weight:600;font-size:13px;line-height:20px;transition:color var(--vela-dur-fast) ease,border-color var(--vela-dur-fast) ease;}
22042
22833
  .vela-ind-tab:not(.vela-ind-tab-active):hover{color:var(--vela-fg-bright);}
@@ -22070,174 +22861,6 @@ ${overlayScrollbarCss(".vela-dialog.vela-ind-dialog *", 9)}
22070
22861
  const v = `${hh}:${mm}`;
22071
22862
  return { value: v, label: v };
22072
22863
  });
22073
- var MONTH_LABELS = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"];
22074
- var MONTH_SHORT = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
22075
- var WEEKDAY_LABELS = ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"];
22076
- function normalizeDateInput(raw) {
22077
- const t = raw.trim();
22078
- const m = /^(\d{4})-(\d{1,2})-(\d{1,2})$/.exec(t);
22079
- if (!m) return null;
22080
- const y = Number(m[1]);
22081
- const mo = Number(m[2]);
22082
- const d = Number(m[3]);
22083
- const dt = new Date(y, mo - 1, d);
22084
- if (dt.getFullYear() !== y || dt.getMonth() !== mo - 1 || dt.getDate() !== d) return null;
22085
- return isoDate(dt);
22086
- }
22087
- function parseIsoDate(raw) {
22088
- const iso = normalizeDateInput(raw);
22089
- if (!iso) return null;
22090
- const [y, mo, d] = iso.split("-").map(Number);
22091
- return new Date(y, mo - 1, d);
22092
- }
22093
- function isoDate(d) {
22094
- return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
22095
- }
22096
- function fillCalendar(el, current, onPick) {
22097
- const today = /* @__PURE__ */ new Date();
22098
- const selected = parseIsoDate(current);
22099
- let year = selected?.getFullYear() ?? today.getFullYear();
22100
- let month = selected?.getMonth() ?? today.getMonth();
22101
- let mode = "date";
22102
- const prev3 = document.createElement("button");
22103
- prev3.type = "button";
22104
- prev3.className = "vela-ind-cal-nav";
22105
- prev3.innerHTML = iconAt("chevron-left", 14);
22106
- const next2 = document.createElement("button");
22107
- next2.type = "button";
22108
- next2.className = "vela-ind-cal-nav";
22109
- next2.innerHTML = iconAt("chevron-right", 14);
22110
- const monthBtn = document.createElement("button");
22111
- monthBtn.type = "button";
22112
- monthBtn.className = "vela-ind-cal-switch";
22113
- monthBtn.setAttribute("aria-label", "Choose month");
22114
- const yearBtn = document.createElement("button");
22115
- yearBtn.type = "button";
22116
- yearBtn.className = "vela-ind-cal-switch";
22117
- yearBtn.setAttribute("aria-label", "Choose year");
22118
- const title = document.createElement("div");
22119
- title.className = "vela-ind-cal-title";
22120
- title.append(monthBtn, yearBtn);
22121
- const head = document.createElement("div");
22122
- head.className = "vela-ind-cal-head";
22123
- head.append(prev3, title, next2);
22124
- const week = document.createElement("div");
22125
- week.className = "vela-ind-cal-week";
22126
- for (const d of WEEKDAY_LABELS) {
22127
- const cell = document.createElement("span");
22128
- cell.textContent = d;
22129
- week.appendChild(cell);
22130
- }
22131
- const grid = document.createElement("div");
22132
- const paint = () => {
22133
- const decade = Math.floor(year / 10) * 10;
22134
- monthBtn.hidden = mode !== "date";
22135
- monthBtn.textContent = MONTH_LABELS[month] ?? "";
22136
- yearBtn.textContent = mode === "year" ? `${decade}-${decade + 9}` : String(year);
22137
- yearBtn.disabled = mode === "year";
22138
- week.hidden = mode !== "date";
22139
- prev3.setAttribute("aria-label", mode === "date" ? "Previous month" : mode === "month" ? "Previous year" : "Previous decade");
22140
- next2.setAttribute("aria-label", mode === "date" ? "Next month" : mode === "month" ? "Next year" : "Next decade");
22141
- grid.className = mode === "date" ? "vela-ind-cal-grid" : "vela-ind-cal-cells";
22142
- grid.replaceChildren();
22143
- if (mode === "date") {
22144
- const startPad = new Date(year, month, 1).getDay();
22145
- const days = new Date(year, month + 1, 0).getDate();
22146
- const todayIso = isoDate(today);
22147
- const selectedIso = selected ? isoDate(selected) : "";
22148
- for (let i = 0; i < startPad; i++) {
22149
- const blank = document.createElement("span");
22150
- blank.className = "vela-ind-cal-blank";
22151
- grid.appendChild(blank);
22152
- }
22153
- for (let day = 1; day <= days; day++) {
22154
- const iso = `${year}-${String(month + 1).padStart(2, "0")}-${String(day).padStart(2, "0")}`;
22155
- const b = document.createElement("button");
22156
- b.type = "button";
22157
- b.className = "vela-ind-cal-day";
22158
- b.textContent = String(day);
22159
- if (iso === selectedIso) b.dataset.checked = "1";
22160
- if (iso === todayIso) b.dataset.today = "1";
22161
- b.addEventListener("click", (e) => {
22162
- e.stopPropagation();
22163
- onPick(iso);
22164
- });
22165
- grid.appendChild(b);
22166
- }
22167
- return;
22168
- }
22169
- if (mode === "month") {
22170
- for (let m = 0; m < 12; m++) {
22171
- const b = document.createElement("button");
22172
- b.type = "button";
22173
- b.className = "vela-ind-cal-cell";
22174
- b.textContent = MONTH_SHORT[m] ?? "";
22175
- if (selected?.getFullYear() === year && selected.getMonth() === m) b.dataset.checked = "1";
22176
- if (today.getFullYear() === year && today.getMonth() === m) b.dataset.today = "1";
22177
- b.addEventListener("click", (e) => {
22178
- e.stopPropagation();
22179
- month = m;
22180
- mode = "date";
22181
- paint();
22182
- });
22183
- grid.appendChild(b);
22184
- }
22185
- return;
22186
- }
22187
- for (let y = decade - 1; y <= decade + 10; y++) {
22188
- const b = document.createElement("button");
22189
- b.type = "button";
22190
- b.className = "vela-ind-cal-cell";
22191
- b.textContent = String(y);
22192
- if (y < decade || y > decade + 9) b.dataset.outside = "1";
22193
- if (selected?.getFullYear() === y) b.dataset.checked = "1";
22194
- if (today.getFullYear() === y) b.dataset.today = "1";
22195
- b.addEventListener("click", (e) => {
22196
- e.stopPropagation();
22197
- year = y;
22198
- mode = "month";
22199
- paint();
22200
- });
22201
- grid.appendChild(b);
22202
- }
22203
- };
22204
- const step = (dir) => {
22205
- if (mode === "date") {
22206
- month += dir;
22207
- if (month < 0) {
22208
- month = 11;
22209
- year -= 1;
22210
- }
22211
- if (month > 11) {
22212
- month = 0;
22213
- year += 1;
22214
- }
22215
- } else {
22216
- year += mode === "month" ? dir : dir * 10;
22217
- }
22218
- paint();
22219
- };
22220
- prev3.addEventListener("click", (e) => {
22221
- e.stopPropagation();
22222
- step(-1);
22223
- });
22224
- next2.addEventListener("click", (e) => {
22225
- e.stopPropagation();
22226
- step(1);
22227
- });
22228
- monthBtn.addEventListener("click", (e) => {
22229
- e.stopPropagation();
22230
- mode = "month";
22231
- paint();
22232
- });
22233
- yearBtn.addEventListener("click", (e) => {
22234
- e.stopPropagation();
22235
- mode = "year";
22236
- paint();
22237
- });
22238
- paint();
22239
- el.append(head, week, grid);
22240
- }
22241
22864
  function normalizeTimeInput(raw) {
22242
22865
  const t = raw.trim();
22243
22866
  const m = /^(\d{1,2}):(\d{2})$/.exec(t) ?? /^(\d{2})(\d{2})$/.exec(t);
@@ -23615,6 +24238,22 @@ ${overlayScrollbarCss(".vela-dialog.vela-ind-dialog *", 9)}
23615
24238
  for (const t of chartTypes()) if (!out.includes(t.id)) out.push(t.id);
23616
24239
  return out;
23617
24240
  }
24241
+ function sanitizeCrosshairOverride(value) {
24242
+ if (!value || typeof value !== "object") return null;
24243
+ const v = value;
24244
+ const out = {};
24245
+ if (isBool(v.vertical)) out.vertical = v.vertical;
24246
+ if (isBool(v.horizontal)) out.horizontal = v.horizontal;
24247
+ if (isColor(v.color)) out.color = v.color;
24248
+ if (isNum(v.width)) out.width = Math.max(0.5, Math.min(8, v.width));
24249
+ if (isLineStyle(v.style)) out.style = v.style;
24250
+ if (isNum(v.opacity)) out.opacity = clampOpacity(v.opacity);
24251
+ const shade = v.shadeRight;
24252
+ if (shade && typeof shade === "object" && isColor(shade.color)) {
24253
+ out.shadeRight = isNum(shade.opacity) ? { color: shade.color, opacity: clampOpacity(shade.opacity) } : { color: shade.color };
24254
+ }
24255
+ return Object.keys(out).length > 0 ? out : null;
24256
+ }
23618
24257
  function isColor(v) {
23619
24258
  return typeof v === "string" && v.trim().length > 0;
23620
24259
  }
@@ -25276,7 +25915,7 @@ void main() {
25276
25915
 
25277
25916
  // src/renderers/native/core/Animator.ts
25278
25917
  var Animator = class {
25279
- constructor(tick, raf3, cancel, now) {
25918
+ constructor(tick, raf3, cancel, now2) {
25280
25919
  this.tick = tick;
25281
25920
  this.handle = null;
25282
25921
  this.last = 0;
@@ -25290,7 +25929,7 @@ void main() {
25290
25929
  };
25291
25930
  this.raf = raf3 ?? ((cb) => requestAnimationFrame(cb));
25292
25931
  this.cancel = cancel ?? ((h) => cancelAnimationFrame(h));
25293
- this.now = now ?? (() => performance.now());
25932
+ this.now = now2 ?? (() => performance.now());
25294
25933
  }
25295
25934
  get active() {
25296
25935
  return this.handle !== null;
@@ -26089,6 +26728,8 @@ void main() {
26089
26728
  /** VPVR-layer config pushed by the VPVR native indicator (null ⇒ layer off). Ephemeral. */
26090
26729
  this.vpvrLayer = null;
26091
26730
  this.crosshair = null;
26731
+ /** Runtime override of the crosshair's lines and style (the `crosshairOverride` feature). Ephemeral. */
26732
+ this.crosshairOverride = null;
26092
26733
  /** How the base price series is drawn on the price pane (candles by default). */
26093
26734
  this.priceStyle = "candles";
26094
26735
  /** Price-series base painting for the ACTIVE style (see ChartTypeDefinition.basePainting). */
@@ -28005,9 +28646,9 @@ void main() {
28005
28646
  }
28006
28647
 
28007
28648
  // src/renderers/native/chrome/countdown.ts
28008
- function countdownText(barOpen, barMs, now) {
28649
+ function countdownText(barOpen, barMs, now2) {
28009
28650
  if (!(barMs > 0)) return null;
28010
- const remaining = barOpen + barMs - now;
28651
+ const remaining = barOpen + barMs - now2;
28011
28652
  if (remaining <= 0) return null;
28012
28653
  return formatCountdown(remaining);
28013
28654
  }
@@ -28819,19 +29460,35 @@ void main() {
28819
29460
  const dataH = coords.height;
28820
29461
  if (!ch || ch.x < 0 || ch.x > dataW || ch.y < 0 || ch.y > dataH) return;
28821
29462
  const cs = scene.style.crosshair;
29463
+ const ov = scene.crosshairOverride;
29464
+ const vertical = ov?.vertical !== false;
29465
+ const horizontal = ov?.horizontal !== false;
28822
29466
  ctx.font = `${scene.style.fontSize}px ${theme.fontFamily}`;
28823
29467
  ctx.textBaseline = "middle";
28824
29468
  const logical = Math.round(coords.xToLogical(ch.x));
28825
- const x = Math.round(coords.logicalToX(logical)) + 0.5;
28826
- ctx.strokeStyle = cs.color ?? theme.textColor;
28827
- ctx.lineWidth = cs.width;
28828
- ctx.globalAlpha = cs.opacity;
28829
- setDash3(ctx, cs.style);
29469
+ const x = crisp(coords.logicalToX(logical), ov?.width ?? cs.width);
29470
+ if (vertical && ov?.shadeRight) {
29471
+ const from = Math.max(0, Math.round(coords.logicalToX(logical + 0.5)));
29472
+ if (from < dataW) {
29473
+ ctx.fillStyle = ov.shadeRight.color;
29474
+ ctx.globalAlpha = ov.shadeRight.opacity ?? 1;
29475
+ ctx.fillRect(from, 0, dataW - from, dataH);
29476
+ ctx.globalAlpha = 1;
29477
+ }
29478
+ }
29479
+ ctx.strokeStyle = ov?.color ?? cs.color ?? theme.textColor;
29480
+ ctx.lineWidth = ov?.width ?? cs.width;
29481
+ ctx.globalAlpha = ov?.opacity ?? cs.opacity;
29482
+ setDash3(ctx, ov?.style ?? cs.style);
28830
29483
  ctx.beginPath();
28831
- ctx.moveTo(x, 0);
28832
- ctx.lineTo(x, dataH);
28833
- ctx.moveTo(0, Math.round(ch.y) + 0.5);
28834
- ctx.lineTo(dataW, Math.round(ch.y) + 0.5);
29484
+ if (vertical) {
29485
+ ctx.moveTo(x, 0);
29486
+ ctx.lineTo(x, dataH);
29487
+ }
29488
+ if (horizontal) {
29489
+ ctx.moveTo(0, Math.round(ch.y) + 0.5);
29490
+ ctx.lineTo(dataW, Math.round(ch.y) + 0.5);
29491
+ }
28835
29492
  ctx.stroke();
28836
29493
  ctx.setLineDash([]);
28837
29494
  ctx.lineWidth = 1;
@@ -28844,11 +29501,11 @@ void main() {
28844
29501
  }
28845
29502
  }
28846
29503
  const chipBg = cs.labelBackground ?? theme.borderColor;
28847
- if (pane && pane.axisFormat !== "none") {
29504
+ if (horizontal && pane && pane.axisFormat !== "none") {
28848
29505
  const price = coords.yToPrice(ch.y, pane.scale, pane.bounds);
28849
29506
  this.chip(ctx, dataW + 1, ch.y, formatAxisValue(pane.scale, pane.bounds.height, price, percentScaleFor(scene, pane), scene.priceMintick, pane.axisFormat), chipBg, "left", false, theme.background);
28850
29507
  }
28851
- this.chip(ctx, x, dataH + 1, formatTimeStamp(coords.logicalToTime(logical), scene.timezone, coords.barInterval), chipBg, "center", true, theme.background);
29508
+ if (vertical) this.chip(ctx, x, dataH + 1, formatTimeStamp(coords.logicalToTime(logical), scene.timezone, coords.barInterval), chipBg, "center", true, theme.background);
28852
29509
  }
28853
29510
  destroy() {
28854
29511
  this.canvas = null;
@@ -28859,23 +29516,37 @@ void main() {
28859
29516
  * along), with that bar's time chip in this chart's own timezone and — when the
28860
29517
  * level resolved — the price chip on the right axis. The snap happened upstream
28861
29518
  * (`externalCrossPx`, floor-to-containing-bar) — this method only draws. Chips
28862
- * render slightly dimmed so the ghost still reads as foreign. */
29519
+ * render slightly dimmed so the ghost still reads as foreign. A `crosshairOverride`
29520
+ * applies to the ghost as well — its line style, a dropped horizontal line, and the
29521
+ * `shadeRight` veil from the ghost bar's right edge — so a pick that spans several
29522
+ * charts reads the same on every one of them. */
28863
29523
  drawExternal(ctx, ext, scene, coords, theme) {
28864
29524
  const cs = scene.style.crosshair;
29525
+ const ov = scene.crosshairOverride;
28865
29526
  const dataW = coords.width;
28866
29527
  const dataH = coords.height;
28867
- const x = Math.round(ext.x) + 0.5;
28868
- if (x < 0 || x > dataW) return;
29528
+ const x = crisp(ext.x, ov?.width ?? cs.width);
29529
+ if (ov?.vertical === false) return;
29530
+ if (ov?.shadeRight) {
29531
+ const from = Math.max(0, Math.round(coords.logicalToX(Math.round(coords.xToLogical(ext.x)) + 0.5)));
29532
+ if (from < dataW) {
29533
+ ctx.fillStyle = ov.shadeRight.color;
29534
+ ctx.globalAlpha = ov.shadeRight.opacity ?? 1;
29535
+ ctx.fillRect(from, 0, dataW - from, dataH);
29536
+ ctx.globalAlpha = 1;
29537
+ }
29538
+ }
29539
+ if (ext.line === false || x < 0 || x > dataW) return;
28869
29540
  ctx.font = `${scene.style.fontSize}px ${theme.fontFamily}`;
28870
29541
  ctx.textBaseline = "middle";
28871
- ctx.strokeStyle = cs.color ?? theme.textColor;
28872
- ctx.lineWidth = cs.width;
28873
- ctx.globalAlpha = cs.opacity * 0.55;
28874
- setDash3(ctx, cs.style);
29542
+ ctx.strokeStyle = ov?.color ?? cs.color ?? theme.textColor;
29543
+ ctx.lineWidth = ov?.width ?? cs.width;
29544
+ ctx.globalAlpha = (ov?.opacity ?? cs.opacity) * 0.55;
29545
+ setDash3(ctx, ov?.style ?? cs.style);
28875
29546
  ctx.beginPath();
28876
29547
  ctx.moveTo(x, 0);
28877
29548
  ctx.lineTo(x, dataH);
28878
- if (ext.y != null) {
29549
+ if (ext.y != null && ov?.horizontal !== false) {
28879
29550
  ctx.moveTo(0, Math.round(ext.y) + 0.5);
28880
29551
  ctx.lineTo(dataW, Math.round(ext.y) + 0.5);
28881
29552
  }
@@ -28884,7 +29555,7 @@ void main() {
28884
29555
  ctx.lineWidth = 1;
28885
29556
  ctx.globalAlpha = 0.8;
28886
29557
  const chipBg = cs.labelBackground ?? theme.borderColor;
28887
- if (ext.y != null && ext.price != null) {
29558
+ if (ext.y != null && ext.price != null && ov?.horizontal !== false) {
28888
29559
  let pane;
28889
29560
  for (const p of scene.panes.values()) {
28890
29561
  if (ext.y >= p.bounds.top && ext.y <= p.bounds.top + p.bounds.height) {
@@ -28924,6 +29595,9 @@ void main() {
28924
29595
  ctx.textAlign = "start";
28925
29596
  }
28926
29597
  };
29598
+ function crisp(px, width) {
29599
+ return Math.round(px) + (Math.round(width) % 2 === 1 ? 0.5 : 0);
29600
+ }
28927
29601
  function setDash3(ctx, style) {
28928
29602
  if (style === "dashed") ctx.setLineDash([6, 4]);
28929
29603
  else if (style === "dotted") ctx.setLineDash([2, 3]);
@@ -37220,7 +37894,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
37220
37894
  this.moveIndicatorCbs = /* @__PURE__ */ new Set();
37221
37895
  this.priceStyleCbs = /* @__PURE__ */ new Set();
37222
37896
  this.name = "native";
37223
- this.features = ["logScale", "currentPriceLine", "priceLabel", "countdown", "upColor", "downColor", "glow", "animZoom", "animPan", "animScroll", "animAutoscale", "animLiveBar", "intro", "zoomAnchor", "axisDrag", "paneResize", "candleZOrder", "candleVisible", "seriesOrder", "highlights", "sessionZones", "gridlines", "axisLabels", "scaleMode", "invertScale", "paneScales", "autoScale", "timezone", "keyboard", "historyChords", "priceStyle", "priceBaseline", "baselinePrice", "settings", "attribution", "dialogHost", "tradeMarkers", "marks", "indicatorTitles", "indicatorValues"];
37897
+ this.features = ["logScale", "currentPriceLine", "priceLabel", "countdown", "upColor", "downColor", "glow", "animZoom", "animPan", "animScroll", "animAutoscale", "animLiveBar", "intro", "zoomAnchor", "axisDrag", "paneResize", "candleZOrder", "candleVisible", "seriesOrder", "highlights", "sessionZones", "gridlines", "axisLabels", "scaleMode", "invertScale", "paneScales", "autoScale", "timezone", "keyboard", "historyChords", "priceStyle", "priceBaseline", "baselinePrice", "settings", "attribution", "dialogHost", "tradeMarkers", "marks", "indicatorTitles", "indicatorValues", "crosshairOverride"];
37224
37898
  /** Track cursor proximity to the scroll button on the plot (bubbles from the button too,
37225
37899
  * so moving onto the button doesn't count as leaving). */
37226
37900
  this.onScrollProximityMove = (e) => {
@@ -37399,6 +38073,9 @@ ${overlayScrollbarCss(".vela-sd-pane")}
37399
38073
  this.scene.marks = mergeMarksState(this.scene.marks, value);
37400
38074
  this.markPopover?.close();
37401
38075
  break;
38076
+ case "crosshairOverride":
38077
+ this.scene.crosshairOverride = sanitizeCrosshairOverride(value);
38078
+ break;
37402
38079
  case "keyboard":
37403
38080
  this.setKeyboardEnabled(Boolean(value));
37404
38081
  return;
@@ -37512,6 +38189,8 @@ ${overlayScrollbarCss(".vela-sd-pane")}
37512
38189
  return { ...this.scene.tradeMarkers, colors: { ...this.scene.tradeMarkers.colors } };
37513
38190
  case "marks":
37514
38191
  return { visible: this.scene.marks.visible, groups: { ...this.scene.marks.groups } };
38192
+ case "crosshairOverride":
38193
+ return this.scene.crosshairOverride ? { ...this.scene.crosshairOverride } : null;
37515
38194
  case "keyboard":
37516
38195
  return this.keyboardEnabled;
37517
38196
  case "historyChords":
@@ -38090,8 +38769,8 @@ ${overlayScrollbarCss(".vela-sd-pane")}
38090
38769
  for (const pane of this.scene.panes.values()) pane.scale = { ...pane.scaleTarget };
38091
38770
  this.modelAlpha = 0;
38092
38771
  const start = performance.now();
38093
- const step = (now) => {
38094
- const p = Math.min(1, (now - start) / duration);
38772
+ const step = (now2) => {
38773
+ const p = Math.min(1, (now2 - start) / duration);
38095
38774
  this.scene.bars = p >= 1 ? real : real.map((b, i) => this.revealCandle(b, i, p, n, style));
38096
38775
  this.paintData();
38097
38776
  if (p < 1) {
@@ -38107,8 +38786,8 @@ ${overlayScrollbarCss(".vela-sd-pane")}
38107
38786
  fadeInModels() {
38108
38787
  const fade = Math.min(INTRO_MODEL_FADE_MS, this.intro.duration || INTRO_MODEL_FADE_MS);
38109
38788
  const start = performance.now();
38110
- const step = (now) => {
38111
- this.modelAlpha = Math.min(1, (now - start) / fade);
38789
+ const step = (now2) => {
38790
+ this.modelAlpha = Math.min(1, (now2 - start) / fade);
38112
38791
  this.paintData();
38113
38792
  if (this.modelAlpha < 1) {
38114
38793
  this.introRaf = requestAnimationFrame(step);
@@ -38631,10 +39310,13 @@ ${overlayScrollbarCss(".vela-sd-pane")}
38631
39310
  this.animator.start();
38632
39311
  }
38633
39312
  } else if (!last2 || bar.time > last2.time) {
39313
+ const vp = this.coords.getViewport();
39314
+ const holdView = n > 0 && vp.rightOffset < 0 && this.scrollTargetRO === null;
38634
39315
  this.bars.push(bar);
38635
39316
  this.syncLiveEase(bar);
38636
39317
  if (this.coords.barInterval > 0) this.coords.appendBar(bar.time);
38637
39318
  else this.coords.setBars(this.bars.map((b) => b.time));
39319
+ if (holdView) this.coords.setViewport({ ...vp, rightOffset: vp.rightOffset - 1 });
38638
39320
  } else {
38639
39321
  return;
38640
39322
  }
@@ -39316,10 +39998,10 @@ ${overlayScrollbarCss(".vela-sd-pane")}
39316
39998
  if (this.markPulseRaf !== null || typeof requestAnimationFrame !== "function") return;
39317
39999
  const tick = () => {
39318
40000
  this.markPulseRaf = null;
39319
- const now = frameNow();
39320
- if (this.scene.marksFlash && this.scene.marksFlash.until <= now) this.scene.marksFlash = null;
40001
+ const now2 = frameNow();
40002
+ if (this.scene.marksFlash && this.scene.marksFlash.until <= now2) this.scene.marksFlash = null;
39321
40003
  this.scheduler?.invalidate(2 /* Chrome */);
39322
- const pulsing = this.scene.marksHoverKey !== null && now - this.scene.marksHoverSince < MARK_PULSE_MS;
40004
+ const pulsing = this.scene.marksHoverKey !== null && now2 - this.scene.marksHoverSince < MARK_PULSE_MS;
39323
40005
  if (pulsing || this.scene.marksFlash !== null) this.markPulseRaf = requestAnimationFrame(tick);
39324
40006
  };
39325
40007
  this.markPulseRaf = requestAnimationFrame(tick);
@@ -39880,9 +40562,10 @@ ${overlayScrollbarCss(".vela-sd-pane")}
39880
40562
  const ext = this.externalCross;
39881
40563
  if (!ext || this.coords.barCount === 0) return null;
39882
40564
  const logical = Math.floor(this.coords.timeToLogical(ext.time));
39883
- if (logical < 0 || logical >= this.coords.barCount) return null;
39884
- const x = this.coords.logicalToX(logical);
39885
- if (!Number.isFinite(x) || x < 0 || x > this.coords.width) return null;
40565
+ if (!(logical < this.coords.barCount)) return null;
40566
+ const x = this.coords.logicalToX(Math.max(logical, -1));
40567
+ if (!Number.isFinite(x)) return null;
40568
+ const line = logical >= 0 && x >= 0 && x <= this.coords.width;
39886
40569
  let y = null;
39887
40570
  if (ext.price != null) {
39888
40571
  const pricePane = this.scene.orderedPanes().find((p) => p.kind === "price");
@@ -39891,7 +40574,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
39891
40574
  if (!Number.isFinite(y) || y < pricePane.bounds.top || y > pricePane.bounds.top + pricePane.bounds.height) y = null;
39892
40575
  }
39893
40576
  }
39894
- return { x, y, time: this.coords.logicalToTime(logical), price: y != null ? ext.price : null };
40577
+ return { x, y, time: this.coords.logicalToTime(Math.max(logical, 0)), price: y != null ? ext.price : null, line };
39895
40578
  }
39896
40579
  /** Set the sticky magnet mode (called by the drawings toolbar's 3-state button). */
39897
40580
  setSnapMode(mode) {
@@ -44596,6 +45279,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
44596
45279
  this.panesControl = new PanesControl(this.orchestrator);
44597
45280
  this.drawingsControl = new DrawingsControl(this.orchestrator.drawings);
44598
45281
  this.marksControl = new MarksControl(this.orchestrator.marks);
45282
+ this.replayControl = new ReplayControl(this.orchestrator);
44599
45283
  }
44600
45284
  /**
44601
45285
  * Register a scripting engine so `addIndicator({ language })` can run that
@@ -44835,6 +45519,16 @@ ${overlayScrollbarCss(".vela-sd-pane")}
44835
45519
  get marks() {
44836
45520
  return this.marksControl;
44837
45521
  }
45522
+ /**
45523
+ * The chart's bar-replay control surface: rewind to a past bar and reveal the
45524
+ * following ones by hand or on a timer, from the history already loaded —
45525
+ * `await chart.replay.start({ from })`, `chart.replay.play(2000)`, `chart.replay.step()`,
45526
+ * `chart.replay.stop()`. Revealed bars behave like live bars for indicators; live
45527
+ * updates pause meanwhile. Follow it with the `replay:*` events.
45528
+ */
45529
+ get replay() {
45530
+ return this.replayControl;
45531
+ }
44838
45532
  on(event, handler) {
44839
45533
  return this.orchestrator.events.on(event, handler);
44840
45534
  }
@@ -44893,6 +45587,28 @@ ${overlayScrollbarCss(".vela-sd-pane")}
44893
45587
  return element;
44894
45588
  }
44895
45589
 
45590
+ // src/core/replayTicks.ts
45591
+ function lowerTimeframeTicks(chart, timeframe) {
45592
+ return async (bar, { end, signal }) => {
45593
+ const { symbol, session } = chart.market;
45594
+ const resolved = symbol ? chart.data.resolve(symbol) : null;
45595
+ const provider = resolved ? chart.data.providerInstance(resolved.provider) : void 0;
45596
+ if (!resolved || !provider) return [];
45597
+ const bars = await provider.getBars(resolved.ticker, timeframe, { from: bar.time, to: end - 1, ...session ? { session } : {} });
45598
+ if (signal.aborted) return [];
45599
+ return barsToTicks(bars.filter((b) => b.time >= bar.time && b.time < end));
45600
+ };
45601
+ }
45602
+ function barsToTicks(bars) {
45603
+ return bars.map((b) => ({
45604
+ price: b.close,
45605
+ open: b.open,
45606
+ high: b.high,
45607
+ low: b.low,
45608
+ ...b.volume === void 0 ? {} : { volume: b.volume }
45609
+ }));
45610
+ }
45611
+
44896
45612
  // src/widget/topbar-composition.ts
44897
45613
  var TOPBAR_BUILTIN_IDS = ["symbol", "timeframes", "style", "layout", "indicators", "actions", "undo-redo", "alerts", "panels", "screenshot"];
44898
45614
 
@@ -45824,8 +46540,8 @@ ${overlayScrollbarCss(".vela-sd-pane")}
45824
46540
  /** Block until the spacing window opens and any pause has elapsed, then reserve the next slot. */
45825
46541
  async waitForTurn() {
45826
46542
  for (; ; ) {
45827
- const now = this.clock.now();
45828
- const wait = Math.max(this.pausedUntil - now, this.nextStartAt - now);
46543
+ const now2 = this.clock.now();
46544
+ const wait = Math.max(this.pausedUntil - now2, this.nextStartAt - now2);
45829
46545
  if (wait <= 0) break;
45830
46546
  await this.clock.delay(wait);
45831
46547
  }
@@ -46286,6 +47002,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
46286
47002
  exports.NativeRenderer = NativeRenderer;
46287
47003
  exports.OVERRIDABLE_TOPBAR_IDS = OVERRIDABLE_TOPBAR_IDS;
46288
47004
  exports.RendererControl = RendererControl;
47005
+ exports.ReplayControl = ReplayControl;
46289
47006
  exports.SERIES_LINE = SERIES_LINE;
46290
47007
  exports.SESSION_OFF = SESSION_OFF;
46291
47008
  exports.SESSION_POST = SESSION_POST;
@@ -46300,6 +47017,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
46300
47017
  exports.VALID = VALID;
46301
47018
  exports.Vela = Vela;
46302
47019
  exports.WARNING = WARNING;
47020
+ exports.barsToTicks = barsToTicks;
46303
47021
  exports.buildColorPicker = buildColorPicker;
46304
47022
  exports.buildToolbar = buildToolbar;
46305
47023
  exports.categoricalColor = categoricalColor;
@@ -46318,6 +47036,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
46318
47036
  exports.inputVisible = inputVisible;
46319
47037
  exports.legendActions = legendActions;
46320
47038
  exports.legendCallouts = legendCallouts;
47039
+ exports.lowerTimeframeTicks = lowerTimeframeTicks;
46321
47040
  exports.nativeIndicatorDescriptors = nativeIndicatorDescriptors;
46322
47041
  exports.nativeIndicatorTypes = nativeIndicatorTypes;
46323
47042
  exports.normalizeSettingsRow = normalizeSettingsRow;