@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
package/dist/index.cjs CHANGED
@@ -615,6 +615,7 @@ registerIcon("chevron-down", S('<path d="M3.5 6 8 10.5 12.5 6"/>'));
615
615
  registerIcon("chevron-up", S('<path d="M3.5 10 8 5.5 12.5 10"/>'));
616
616
  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"/>'));
617
617
  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"/>'));
618
+ registerIcon("replay", S('<path d="M14 4.2v7.6L8.6 8z"/><path d="M7.4 4.2v7.6L2 8z"/>'));
618
619
  registerIcon("check", S('<path d="m2.8 8.4 3.4 3.4 7-7.6"/>'));
619
620
  registerIcon("close", S('<path d="m3.8 3.8 8.4 8.4M12.2 3.8l-8.4 8.4"/>'));
620
621
  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"/>'));
@@ -7832,6 +7833,7 @@ var CHUNK_BARS = 1e4;
7832
7833
  var FIRST_PAINT_BARS = 20;
7833
7834
  var GAP_FACTOR = 1.5;
7834
7835
  var HEAL_COOLDOWN_MS = 5e3;
7836
+ var MIN_TICK_UPDATE_MS = 16;
7835
7837
  var EngineOrchestrator = class _EngineOrchestrator {
7836
7838
  constructor(container, renderer, feed, engines, config, dataControl) {
7837
7839
  this.renderer = renderer;
@@ -7884,6 +7886,32 @@ var EngineOrchestrator = class _EngineOrchestrator {
7884
7886
  this.lastHealAt = 0;
7885
7887
  /** The `visibilitychange` catch-up listener (removed on destroy). */
7886
7888
  this.onVisible = null;
7889
+ // ── bar replay: `rawBars` holds the history up to the cursor, the rest waits here ──
7890
+ /** Bars still hidden right of the replay cursor, oldest first; null ≡ replay off. */
7891
+ this.replayQueue = null;
7892
+ this.replayTimer = null;
7893
+ this.replayPlaying = false;
7894
+ this.replayIntervalMs = 1e3;
7895
+ /** The depth (`market.bars`) before a replay deepened the history — restored when it ends. */
7896
+ this.replayDepth = null;
7897
+ /** A start is loading older history before it can cut; `seek` = a replay was running
7898
+ * (and left silently to deepen), so abandoning the load must announce its end. */
7899
+ this.replayLoading = null;
7900
+ /** A replay carried over a timeframe/session switch, between the old series' silent end
7901
+ * and the cut of the new one: still ACTIVE for the outside (`replayState`), resumed
7902
+ * at `revealedEnd` once the new bars land, unless stopped (`cancelled`) meanwhile. */
7903
+ this.replayCarrying = null;
7904
+ /** Intrabar prices for the bars replay reveals — null ≡ whole bars. */
7905
+ this.replayTickSource = null;
7906
+ /** The bar being revealed tick by tick: its stored form (what it settles on), its ticks,
7907
+ * the next tick to apply, the candle built so far, the volume each tick adds when the
7908
+ * ticks carry none, and its pacing (ticks per update, ms between updates — one play
7909
+ * interval per tick — and the clock time tick 0 is due at, so late timers catch up). */
7910
+ this.replayForming = null;
7911
+ /** Tick requests by bar time, shared by prefetch and playback; `ticks` once they landed. */
7912
+ this.replayTickFetches = /* @__PURE__ */ new Map();
7913
+ /** Aborts the tick requests in flight — renewed whenever the replay moves on. */
7914
+ this.replayTickAbort = null;
7887
7915
  /** What the last applied bar did: refine the open candle, or open a new one (which
7888
7916
  * settles the previous). Read by the `script:run` cause attribution. */
7889
7917
  this.barCause = "tick";
@@ -8107,6 +8135,10 @@ var EngineOrchestrator = class _EngineOrchestrator {
8107
8135
  const paint = (bars, final) => {
8108
8136
  if (this.generation !== gen || !final && bars.length === 0) return;
8109
8137
  if (!painted && !final && bars.length < Math.min(requested, FIRST_PAINT_BARS)) return;
8138
+ if (painted && this.replayQueue) {
8139
+ this.mergeHistoryIntoReplay(bars);
8140
+ return;
8141
+ }
8110
8142
  this.setBarSeries(bars, painted ? { preserveView: true } : void 0);
8111
8143
  if (!painted && bars.length > 0) {
8112
8144
  painted = true;
@@ -8249,6 +8281,36 @@ var EngineOrchestrator = class _EngineOrchestrator {
8249
8281
  // exactly like a timeframe change; the cache keys the sessions apart.
8250
8282
  next.session !== void 0 && next.session !== m.session || next.data !== void 0;
8251
8283
  const depthChanged = next.bars !== void 0 && next.bars !== m.bars;
8284
+ const sameClock = identityChanged && next.data === void 0 && (next.symbol === void 0 || next.symbol === m.symbol) && (next.timeframe !== void 0 || next.session !== void 0);
8285
+ const pending = identityChanged ? this.replayCarrying : null;
8286
+ if (pending) {
8287
+ this.replayCarrying = null;
8288
+ if (!sameClock) {
8289
+ pending.cancelled = true;
8290
+ this.events.emit("replay:end", { reason: "market" });
8291
+ }
8292
+ }
8293
+ 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;
8294
+ if (pending) pending.cancelled = true;
8295
+ if (replayCarry) this.replayCarrying = replayCarry;
8296
+ if (replayCarry && this.replayQueue) {
8297
+ this.endReplay(null, { restore: false, resume: false });
8298
+ const depth = this.replayDepth;
8299
+ this.replayDepth = null;
8300
+ if (depth && next.bars === void 0) {
8301
+ if (depth.bars === void 0) delete m.bars;
8302
+ else m.bars = depth.bars;
8303
+ }
8304
+ }
8305
+ if (this.replayQueue && (identityChanged || depthChanged)) this.endReplay("market", { restore: !identityChanged, resume: false });
8306
+ if (this.replayLoading && (identityChanged || depthChanged)) {
8307
+ const depth = this.replayDepth;
8308
+ this.abandonReplayLoad("market");
8309
+ if (depth && next.bars === void 0) {
8310
+ if (depth.bars === void 0) delete m.bars;
8311
+ else m.bars = depth.bars;
8312
+ }
8313
+ }
8252
8314
  const depthOnly = depthChanged && !identityChanged && this.rawBars.length > 0 && !m.data?.length && ((next.bars ?? 0) <= this.rawBars.length || typeof this.feed.loadRange === "function");
8253
8315
  if (!identityChanged && !depthChanged) {
8254
8316
  if (typeof next.visibleRange === "string") this.setVisibleRangePreset(next.visibleRange);
@@ -8306,15 +8368,20 @@ var EngineOrchestrator = class _EngineOrchestrator {
8306
8368
  const from = bars[Math.max(0, idx - (carriedCount - 1))].time;
8307
8369
  this.renderer.setVisibleRange({ from, to: Math.max(carried.right, bars[idx].time + 1) });
8308
8370
  }
8309
- if (!depthOnly) {
8371
+ const carry = replayCarry && !replayCarry.cancelled ? replayCarry : null;
8372
+ const carryFrom = carry ? carry.revealedEnd - this.barIntervalMs() : 0;
8373
+ const carryNow = carry != null && this.rawBars.length > 0 && carryFrom >= this.rawBars[0].time;
8374
+ const replayBack = carryNow && this.resumeCarriedReplay(this.replayStart({ from: carryFrom }), carry);
8375
+ if (!depthOnly && !replayBack) {
8310
8376
  this.restartNativeIndicators();
8311
8377
  this.restartChartTypeEngine();
8312
8378
  this.reexecuteIndicators();
8313
8379
  }
8314
- this.startLive();
8380
+ if (!replayBack) this.startLive();
8315
8381
  if (identityChanged) {
8316
8382
  this.events.emit("market:changed", { symbol: m.symbol ?? "TEST", timeframe: m.timeframe ?? "60", prev: prev2 });
8317
8383
  }
8384
+ if (carry && !carryNow) this.resumeCarriedReplay(this.replayStart({ from: carryFrom }), carry);
8318
8385
  }
8319
8386
  /**
8320
8387
  * Restart every native indicator over the new market: a `NativeIndicatorContext`
@@ -8438,6 +8505,397 @@ var EngineOrchestrator = class _EngineOrchestrator {
8438
8505
  historyComplete() {
8439
8506
  return this.historyCompletePromise;
8440
8507
  }
8508
+ // ── bar replay ──────────────────────────────────────────────
8509
+ /**
8510
+ * Enter replay — or seek, when already replaying — keeping every bar opened at or before
8511
+ * `opts.from` and queueing the rest. A `from` inside the loaded bars cuts at once, even
8512
+ * while older history is still streaming in: what lands later joins the tape's head
8513
+ * (see {@link mergeHistoryIntoReplay}) and leaves the cut alone. An older `from` waits
8514
+ * for that load, then deepens if it still has to. Consumers restart over the cut exactly
8515
+ * as on a market switch — their models describe bars that are now hidden — and the
8516
+ * active chart-type engine is rebuilt without live data.
8517
+ */
8518
+ async replayStart(opts) {
8519
+ if (this.rawBars.length === 0) {
8520
+ console.warn("[vela] chart.replay.start() ignored \u2014 the chart has no bars yet");
8521
+ return;
8522
+ }
8523
+ let gen = this.generation;
8524
+ if (opts.from < this.rawBars[0].time) {
8525
+ await this.historyCompletePromise;
8526
+ if (this.generation !== gen || this.rawBars.length === 0) return;
8527
+ }
8528
+ const oldest = this.rawBars[0].time;
8529
+ if (opts.from < oldest && this.canHeal() && typeof this.feed.loadRange === "function") {
8530
+ const held = this.rawBars.length + (this.replayQueue?.length ?? 0);
8531
+ const bars = held + Math.ceil((oldest - opts.from) / this.barIntervalMs()) + 1;
8532
+ const seek = this.replayQueue !== null;
8533
+ if (seek) this.endReplay(null, { restore: true, resume: false });
8534
+ this.replayDepth ?? (this.replayDepth = { bars: this.config.market.bars });
8535
+ const deepened = this.setMarket({ bars });
8536
+ this.replayLoading = { seek };
8537
+ await deepened;
8538
+ gen = this.generation;
8539
+ await this.historyCompletePromise;
8540
+ if (!this.replayLoading || this.generation !== gen || this.rawBars.length === 0) return;
8541
+ this.replayLoading = null;
8542
+ }
8543
+ this.settleFormingBar();
8544
+ this.dropTickFetches();
8545
+ const tape = this.replayQueue ? [...this.rawBars, ...this.replayQueue] : this.rawBars;
8546
+ let cut = 0;
8547
+ while (cut < tape.length && tape[cut].time <= opts.from) cut += 1;
8548
+ cut = Math.max(1, cut);
8549
+ if (cut >= tape.length) {
8550
+ console.warn("[vela] chart.replay.start() ignored \u2014 no bar opens after the requested time");
8551
+ return;
8552
+ }
8553
+ const wasPlaying = this.replayPlaying;
8554
+ this.clearReplayTimer();
8555
+ if (!this.replayQueue) this.stopLive();
8556
+ this.replayQueue = tape.slice(cut);
8557
+ this.setBarSeries(tape.slice(0, cut));
8558
+ this.restartBarConsumers({ blank: true });
8559
+ if (wasPlaying) this.events.emit("replay:pause", void 0);
8560
+ this.events.emit("replay:start", { cursorTime: tape[cut - 1].time, remaining: this.replayQueue.length });
8561
+ this.prefetchTicks();
8562
+ }
8563
+ /** Reveal the next queued bar through the live-bar path — or complete the bar forming
8564
+ * tick by tick; the last one ends the replay. */
8565
+ replayStep() {
8566
+ if (this.replayForming) {
8567
+ this.finishFormingBar();
8568
+ return true;
8569
+ }
8570
+ const queue = this.replayQueue;
8571
+ const bar = queue?.shift();
8572
+ if (!queue || !bar) return false;
8573
+ this.replayTickFetches.delete(bar.time);
8574
+ this.applyBar(bar);
8575
+ this.completeReveal(bar);
8576
+ this.prefetchTicks();
8577
+ return true;
8578
+ }
8579
+ /**
8580
+ * Follow up a replay carried onto a new series of the same symbol (another timeframe or
8581
+ * session). The cut asked for keeps the bars that CLOSED by the end of what was revealed —
8582
+ * every finer bar of the last revealed one, never a coarser bar still open at that point
8583
+ * (no look-ahead). Once it settles: playback resumes if it was on; if the replay did not
8584
+ * come back (nothing left after that point, or a later switch took over), it ends as a
8585
+ * market switch would. Returns whether the replay is back already — a start with no
8586
+ * history to wait for cuts synchronously (an async function runs up to its first await).
8587
+ */
8588
+ resumeCarriedReplay(start, carry) {
8589
+ if (this.replayQueue && this.replayCarrying === carry) this.replayCarrying = null;
8590
+ void start.then(() => {
8591
+ if (this.replayCarrying === carry) this.replayCarrying = null;
8592
+ if (carry.cancelled) return;
8593
+ if (!this.replayQueue) this.events.emit("replay:end", { reason: "market" });
8594
+ else if (carry.playing) this.replayPlay();
8595
+ });
8596
+ return this.replayQueue !== null;
8597
+ }
8598
+ replayStepUpdate() {
8599
+ if (!this.replayQueue) return false;
8600
+ if (this.replayForming) {
8601
+ this.applyReplayTicks(1);
8602
+ return true;
8603
+ }
8604
+ const bar = this.replayQueue[0];
8605
+ if (!bar) return false;
8606
+ const entry = this.replayTickSource ? this.requestTicks(bar) : null;
8607
+ if (entry && !entry.ticks) {
8608
+ const epoch = this.replayTickAbort;
8609
+ void entry.done.then(() => {
8610
+ if (!this.replayPlaying && !this.replayForming && this.replayTickAbort === epoch && this.replayQueue?.[0] === bar) this.replayStepUpdate();
8611
+ });
8612
+ return true;
8613
+ }
8614
+ if (!entry?.ticks?.length) return this.replayStep();
8615
+ this.openFormingBar(entry.ticks, 1);
8616
+ return true;
8617
+ }
8618
+ replaySetTicks(source) {
8619
+ this.finishFormingBar();
8620
+ this.dropTickFetches();
8621
+ this.replayTickSource = source;
8622
+ this.prefetchTicks();
8623
+ }
8624
+ replayPlay(intervalMs2) {
8625
+ if (!this.replayQueue) return;
8626
+ if (intervalMs2 !== void 0) {
8627
+ if (!(Number.isFinite(intervalMs2) && intervalMs2 > 0)) {
8628
+ console.warn(`[vela] chart.replay.play(${intervalMs2}) ignored \u2014 the interval must be a positive number of ms`);
8629
+ return;
8630
+ }
8631
+ this.replayIntervalMs = intervalMs2;
8632
+ }
8633
+ this.clearReplayTimer();
8634
+ this.replayPlaying = true;
8635
+ if (this.replayForming) this.paceFormingBar(this.replayForming);
8636
+ this.scheduleReplayStep(this.replayForming?.delayMs);
8637
+ this.events.emit("replay:play", { intervalMs: this.replayIntervalMs });
8638
+ }
8639
+ replayPause() {
8640
+ if (!this.replayPlaying) return;
8641
+ this.clearReplayTimer();
8642
+ this.events.emit("replay:pause", void 0);
8643
+ }
8644
+ replayStop() {
8645
+ const carrying = this.replayCarrying;
8646
+ if (carrying) {
8647
+ carrying.cancelled = true;
8648
+ this.replayCarrying = null;
8649
+ this.events.emit("replay:end", { reason: "stopped" });
8650
+ }
8651
+ if (this.replayLoading) {
8652
+ const depth = this.replayDepth;
8653
+ this.abandonReplayLoad("stopped");
8654
+ void this.setMarket({ bars: depth?.bars ?? 500 });
8655
+ return;
8656
+ }
8657
+ this.endReplay("stopped", { restore: true, resume: true });
8658
+ }
8659
+ abandonReplayLoad(reason) {
8660
+ const loading = this.replayLoading;
8661
+ if (!loading) return;
8662
+ this.replayLoading = null;
8663
+ this.replayDepth = null;
8664
+ if (loading.seek) this.events.emit("replay:end", { reason });
8665
+ }
8666
+ replayState() {
8667
+ const active = this.replayQueue !== null;
8668
+ return {
8669
+ active: active || this.replayCarrying !== null,
8670
+ // a replay carried over a switch stays on while the new bars load
8671
+ playing: this.replayCarrying?.playing ?? this.replayPlaying,
8672
+ cursorTime: active ? this.rawBars[this.rawBars.length - 1]?.time ?? null : null,
8673
+ remaining: this.replayQueue?.length ?? 0,
8674
+ nextTime: this.replayForming?.bar.time ?? this.replayQueue?.[0]?.time ?? null,
8675
+ intervalMs: this.replayIntervalMs
8676
+ };
8677
+ }
8678
+ replayBounds() {
8679
+ const first = this.rawBars[0];
8680
+ const last = this.replayQueue?.[this.replayQueue.length - 1] ?? this.rawBars[this.rawBars.length - 1];
8681
+ return first && last ? { first: first.time, last: last.time } : null;
8682
+ }
8683
+ scheduleReplayStep(delayMs = this.replayIntervalMs) {
8684
+ this.replayTimer = setTimeout(() => {
8685
+ this.replayTimer = null;
8686
+ this.replayAdvance();
8687
+ }, delayMs);
8688
+ }
8689
+ /** One beat of timed playback: the forming bar's next ticks, else the next bar — whole,
8690
+ * or opened from its ticks (waiting for them when they have not landed yet). */
8691
+ replayAdvance() {
8692
+ if (!this.replayPlaying || !this.replayQueue) return;
8693
+ const forming = this.replayForming;
8694
+ if (forming) {
8695
+ const due = Math.floor((now() - forming.origin) / this.replayIntervalMs) + 1;
8696
+ this.applyReplayTicks(Math.max(forming.batch, due - forming.next));
8697
+ if (this.replayPlaying) this.scheduleReplayStep(forming.delayMs);
8698
+ return;
8699
+ }
8700
+ const bar = this.replayQueue[0];
8701
+ if (!bar) return;
8702
+ const entry = this.replayTickSource ? this.requestTicks(bar) : null;
8703
+ if (entry && !entry.ticks) {
8704
+ const epoch = this.replayTickAbort;
8705
+ void entry.done.then(() => {
8706
+ if (this.replayPlaying && this.replayTimer == null && this.replayTickAbort === epoch && this.replayQueue?.[0] === bar) this.replayAdvance();
8707
+ });
8708
+ return;
8709
+ }
8710
+ if (!entry?.ticks?.length) {
8711
+ if (this.replayStep() && this.replayPlaying) this.scheduleReplayStep();
8712
+ return;
8713
+ }
8714
+ this.openFormingBar(entry.ticks);
8715
+ if (this.replayPlaying) this.scheduleReplayStep(this.replayForming?.delayMs);
8716
+ }
8717
+ /** Take the next queued bar off the tape and open it at its first tick (the first
8718
+ * `count` ticks — default: one timed update's worth). */
8719
+ openFormingBar(ticks, count) {
8720
+ const bar = this.replayQueue.shift();
8721
+ const volumeEach = ticks.every((t) => t.volume === void 0) ? (bar.volume ?? 0) / ticks.length : null;
8722
+ const forming = { bar, ticks, next: 0, candle: null, volumeEach, batch: 1, delayMs: this.replayIntervalMs, origin: 0 };
8723
+ this.paceFormingBar(forming);
8724
+ this.replayForming = forming;
8725
+ this.applyReplayTicks(count ?? forming.batch);
8726
+ this.prefetchTicks();
8727
+ }
8728
+ /** One tick per interval, batched when the interval is shorter than a frame; the clock
8729
+ * is anchored on the ticks already applied (a resume or a pace change carries on from
8730
+ * there, never jumps). */
8731
+ paceFormingBar(f) {
8732
+ f.batch = Math.max(1, Math.ceil(MIN_TICK_UPDATE_MS / this.replayIntervalMs));
8733
+ f.delayMs = this.replayIntervalMs * f.batch;
8734
+ f.origin = now() - Math.max(0, f.next - 1) * this.replayIntervalMs;
8735
+ }
8736
+ /** Fold the next `count` ticks into the forming candle; the last one settles the bar. */
8737
+ applyReplayTicks(count) {
8738
+ const f = this.replayForming;
8739
+ if (!f) return;
8740
+ let c = f.candle;
8741
+ const stop = Math.min(f.ticks.length, f.next + count);
8742
+ for (; f.next < stop; f.next += 1) {
8743
+ const t = f.ticks[f.next];
8744
+ const v = f.volumeEach ?? t.volume ?? 0;
8745
+ const hi = Math.max(t.high ?? t.price, t.price);
8746
+ const lo = Math.min(t.low ?? t.price, t.price);
8747
+ if (c) {
8748
+ c = { ...c, high: Math.max(c.high, hi), low: Math.min(c.low, lo), close: t.price, volume: (c.volume ?? 0) + v };
8749
+ } else {
8750
+ const open2 = t.open ?? t.price;
8751
+ c = { time: f.bar.time, open: open2, high: Math.max(open2, hi), low: Math.min(open2, lo), close: t.price, volume: v };
8752
+ }
8753
+ }
8754
+ f.candle = c;
8755
+ if (f.next >= f.ticks.length) {
8756
+ this.finishFormingBar();
8757
+ return;
8758
+ }
8759
+ this.applyBar(c);
8760
+ this.events.emit("replay:tick", { cursorTime: f.bar.time, index: f.next - 1, count: f.ticks.length });
8761
+ }
8762
+ /** Settle the forming bar on its stored values and announce it revealed. */
8763
+ finishFormingBar() {
8764
+ const f = this.replayForming;
8765
+ if (!f) return;
8766
+ this.replayForming = null;
8767
+ this.replayTickFetches.delete(f.bar.time);
8768
+ this.applyBar(f.bar);
8769
+ this.events.emit("replay:tick", { cursorTime: f.bar.time, index: f.ticks.length - 1, count: f.ticks.length });
8770
+ this.completeReveal(f.bar);
8771
+ }
8772
+ /** Put the forming bar back in its stored form, silently — a seek or an end rebuilds
8773
+ * from the tape, which must hold the stored bar. */
8774
+ settleFormingBar() {
8775
+ const f = this.replayForming;
8776
+ if (!f) return;
8777
+ this.replayForming = null;
8778
+ this.applyBar(f.bar);
8779
+ }
8780
+ /**
8781
+ * A history answer landing mid-replay (a progressive snapshot — the whole answer so far):
8782
+ * the tape keeps its cut and its hidden queue; bars older than the tape join its head,
8783
+ * bars newer than it join the queue's end.
8784
+ */
8785
+ mergeHistoryIntoReplay(bars) {
8786
+ const queue = this.replayQueue;
8787
+ const head = this.rawBars[0];
8788
+ if (!queue || !head) return;
8789
+ const tail = queue[queue.length - 1] ?? this.rawBars[this.rawBars.length - 1];
8790
+ let older = 0;
8791
+ while (older < bars.length && bars[older].time < head.time) older += 1;
8792
+ let newer = bars.length;
8793
+ while (newer > older && bars[newer - 1].time > tail.time) newer -= 1;
8794
+ if (newer < bars.length) queue.push(...bars.slice(newer));
8795
+ if (older > 0) {
8796
+ this.setBarSeries([...bars.slice(0, older), ...this.rawBars], { preserveView: true });
8797
+ this.notifySessionsBars("backfill");
8798
+ }
8799
+ }
8800
+ /** A bar is fully on screen: announce it, and end the replay if it was the last. */
8801
+ completeReveal(bar) {
8802
+ const queue = this.replayQueue;
8803
+ if (!queue) return;
8804
+ this.events.emit("replay:step", { cursorTime: bar.time, remaining: queue.length });
8805
+ if (queue.length === 0) this.endReplay("finished", { restore: true, resume: true });
8806
+ }
8807
+ /** Ask for the ticks of the next hidden bar ahead of time. */
8808
+ prefetchTicks() {
8809
+ const next = this.replayQueue?.[0];
8810
+ if (next && this.replayTickSource) this.requestTicks(next);
8811
+ }
8812
+ /** The (shared) tick request of a queued bar — the source runs once per bar. */
8813
+ requestTicks(bar) {
8814
+ const known = this.replayTickFetches.get(bar.time);
8815
+ if (known) return known;
8816
+ const source = this.replayTickSource;
8817
+ this.replayTickAbort ?? (this.replayTickAbort = new AbortController());
8818
+ const signal = this.replayTickAbort.signal;
8819
+ const queue = this.replayQueue ?? [];
8820
+ const end = queue[queue.indexOf(bar) + 1]?.time ?? bar.time + this.barIntervalMs();
8821
+ const entry = { ticks: null, done: Promise.resolve() };
8822
+ const land = (ticks) => {
8823
+ entry.ticks = usableTicks(ticks);
8824
+ };
8825
+ const fail = (e) => {
8826
+ if (!signal.aborted) console.warn(`[vela] replay tick source failed \u2014 revealing the bar whole (${e instanceof Error ? e.message : String(e)})`);
8827
+ land([]);
8828
+ };
8829
+ try {
8830
+ const out = source(bar, { end, signal });
8831
+ if (Array.isArray(out)) land(out);
8832
+ else entry.done = Promise.resolve(out).then(land, fail);
8833
+ } catch (e) {
8834
+ fail(e);
8835
+ }
8836
+ this.replayTickFetches.set(bar.time, entry);
8837
+ return entry;
8838
+ }
8839
+ /** Abort the tick requests in flight and forget the landed ones. */
8840
+ dropTickFetches() {
8841
+ this.replayTickAbort?.abort();
8842
+ this.replayTickAbort = null;
8843
+ this.replayTickFetches.clear();
8844
+ }
8845
+ clearReplayTimer() {
8846
+ if (this.replayTimer != null) clearTimeout(this.replayTimer);
8847
+ this.replayTimer = null;
8848
+ this.replayPlaying = false;
8849
+ }
8850
+ /**
8851
+ * Leave replay. `restore` puts the queued bars back and restarts the consumers over the
8852
+ * full history (a market switch skips it: its load replaces everything anyway); `resume`
8853
+ * restarts live updates and heals the bars that closed while replaying. A null reason
8854
+ * leaves silently — a seek that re-enters right after.
8855
+ */
8856
+ endReplay(reason, opts) {
8857
+ const queue = this.replayQueue;
8858
+ if (!queue) return;
8859
+ this.clearReplayTimer();
8860
+ if (opts.restore) this.settleFormingBar();
8861
+ else this.replayForming = null;
8862
+ this.dropTickFetches();
8863
+ this.replayQueue = null;
8864
+ const depth = reason ? this.replayDepth : null;
8865
+ if (reason) this.replayDepth = null;
8866
+ if (depth) {
8867
+ if (depth.bars === void 0) delete this.config.market.bars;
8868
+ else this.config.market.bars = depth.bars;
8869
+ }
8870
+ if (opts.restore) {
8871
+ const keep = depth ? depth.bars ?? 500 : Infinity;
8872
+ if (queue.length > 0 || this.rawBars.length > keep) {
8873
+ const full = [...this.rawBars, ...queue];
8874
+ this.setBarSeries(full.length > keep ? full.slice(full.length - keep) : full);
8875
+ this.restartBarConsumers({ blank: false });
8876
+ } else if (this.config.live) {
8877
+ this.restartNativeIndicators();
8878
+ this.restartChartTypeEngine();
8879
+ }
8880
+ }
8881
+ if (opts.resume && this.config.live) {
8882
+ this.startLive();
8883
+ const last = this.rawBars[this.rawBars.length - 1];
8884
+ if (last && this.canHeal()) void this.healGap(last.time);
8885
+ }
8886
+ if (reason) this.events.emit("replay:end", { reason });
8887
+ }
8888
+ /**
8889
+ * The bars were replaced by a cut or a restore of the SAME market: every consumer re-runs.
8890
+ * A cut also blanks the mounted models first — they still paint the hidden bars' values,
8891
+ * which must not show until the re-runs land.
8892
+ */
8893
+ restartBarConsumers(opts) {
8894
+ if (opts.blank) this.blankIndicatorVisuals();
8895
+ this.restartNativeIndicators();
8896
+ this.restartChartTypeEngine();
8897
+ this.reexecuteIndicators();
8898
+ }
8441
8899
  /** The primary symbol as the data layer resolves it, or null if no symbol is set. */
8442
8900
  qualifiedSymbol() {
8443
8901
  const market = this.config.market;
@@ -8509,7 +8967,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
8509
8967
  engine.start({
8510
8968
  symbol: this.config.market.symbol ?? "TEST",
8511
8969
  timeframe: this.config.market.timeframe ?? "60",
8512
- live: this.config.live ?? false,
8970
+ live: (this.config.live ?? false) && this.replayQueue === null,
8513
8971
  session: this.config.market.session,
8514
8972
  bars: () => this.bars,
8515
8973
  data: this.dataControl,
@@ -8635,6 +9093,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
8635
9093
  record.session.notifyBars(reason);
8636
9094
  } else if (record.native) record.native.instance.onBars();
8637
9095
  }
9096
+ if (this.activeEngineStyle) this.typeEngines.get(this.activeEngineStyle)?.onBars?.();
8638
9097
  }
8639
9098
  /** Gap healing needs a ranged feed and a provider-backed series (offline `data` has no source). */
8640
9099
  canHeal() {
@@ -8657,10 +9116,11 @@ var EngineOrchestrator = class _EngineOrchestrator {
8657
9116
  try {
8658
9117
  const bars = await this.feed.loadRange(this.config.market, { from: fromMs });
8659
9118
  if (this.generation !== gen) return;
9119
+ if (this.replayQueue) return;
8660
9120
  for (const b of bars) this.applyBar(b, false);
8661
9121
  } catch {
8662
9122
  } finally {
8663
- if (this.generation === gen) {
9123
+ if (this.generation === gen && !this.replayQueue) {
8664
9124
  this.healing = false;
8665
9125
  const pending = this.healBuffer;
8666
9126
  this.healBuffer = [];
@@ -8965,6 +9425,13 @@ var EngineOrchestrator = class _EngineOrchestrator {
8965
9425
  destroy() {
8966
9426
  this.bumpGeneration();
8967
9427
  this.resolveHistoryComplete();
9428
+ this.clearReplayTimer();
9429
+ this.replayQueue = null;
9430
+ this.replayForming = null;
9431
+ this.replayCarrying = null;
9432
+ this.dropTickFetches();
9433
+ this.replayDepth = null;
9434
+ this.replayLoading = null;
8968
9435
  this.stopLive();
8969
9436
  if (this.viewportTimer != null) clearTimeout(this.viewportTimer);
8970
9437
  this.viewportUnsub?.();
@@ -9063,6 +9530,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
9063
9530
  this.emitScriptRun(id, cause, first);
9064
9531
  },
9065
9532
  onAlert: (a) => {
9533
+ if (this.replayQueue || this.replayCarrying) return;
9066
9534
  const indicator = record.options?.title ?? record.prepared?.meta.title ?? record.title;
9067
9535
  this.events.emit("alert", { ...a, indicator });
9068
9536
  handle.emit("alert", { id: a.id, message: a.message, title: a.title, time: a.time });
@@ -9089,7 +9557,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
9089
9557
  id,
9090
9558
  symbol: this.config.market.symbol ?? "TEST",
9091
9559
  timeframe: this.config.market.timeframe ?? "60",
9092
- live: this.config.live,
9560
+ live: this.config.live && this.replayQueue === null,
9093
9561
  session: this.config.market.session,
9094
9562
  bars: () => this.bars,
9095
9563
  data: this.dataControl,
@@ -9492,17 +9960,17 @@ var EngineOrchestrator = class _EngineOrchestrator {
9492
9960
  }
9493
9961
  emitContextChanged(id) {
9494
9962
  if (!this.registry.get(id)?.session?.getContext) return;
9495
- const now = Date.now();
9496
- if (now - (this.contextEmitAt.get(id) ?? 0) < 1e3) return;
9497
- this.contextEmitAt.set(id, now);
9963
+ const now2 = Date.now();
9964
+ if (now2 - (this.contextEmitAt.get(id) ?? 0) < 1e3) return;
9965
+ this.contextEmitAt.set(id, now2);
9498
9966
  this.events.emit("context:changed", { id });
9499
9967
  }
9500
9968
  emitScriptRun(id, cause, first) {
9501
9969
  const record = this.registry.get(id);
9502
9970
  if (!record || record.native || !this.events.hasListeners("script:run")) return;
9503
- const now = Date.now();
9504
- if (cause === "tick" && now - (this.runEmitAt.get(id) ?? 0) < RUN_EMIT_THROTTLE_MS) return;
9505
- this.runEmitAt.set(id, now);
9971
+ const now2 = Date.now();
9972
+ if (cause === "tick" && now2 - (this.runEmitAt.get(id) ?? 0) < RUN_EMIT_THROTTLE_MS) return;
9973
+ this.runEmitAt.set(id, now2);
9506
9974
  void Promise.resolve().then(() => this.buildScriptRun(record, cause, first)).then((run) => {
9507
9975
  if (run && this.registry.get(id)) this.events.emit("script:run", run);
9508
9976
  });
@@ -9555,6 +10023,16 @@ var EngineOrchestrator = class _EngineOrchestrator {
9555
10023
  handle?.emit("error", { error });
9556
10024
  }
9557
10025
  };
10026
+ function now() {
10027
+ return typeof performance !== "undefined" ? performance.now() : Date.now();
10028
+ }
10029
+ function usableTicks(value) {
10030
+ if (!Array.isArray(value)) return [];
10031
+ const optional = (n) => n === void 0 || Number.isFinite(n);
10032
+ return value.filter(
10033
+ (t) => t != null && Number.isFinite(t.price) && optional(t.volume) && optional(t.high) && optional(t.low) && optional(t.open)
10034
+ );
10035
+ }
9558
10036
  function yieldToPaint() {
9559
10037
  if (typeof requestAnimationFrame === "function") {
9560
10038
  return new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(() => resolve())));
@@ -9755,6 +10233,12 @@ var RendererControl = class {
9755
10233
  onCrosshairMove(cb) {
9756
10234
  return this.renderer.onCrosshairMove(cb);
9757
10235
  }
10236
+ /** A click — or a touch tap — on the plot, never the end of a pan: the open time of
10237
+ * the bar under it (`null` off the bars). On touch, where a tap moves no crosshair,
10238
+ * this is how an interaction learns which bar was chosen. */
10239
+ onClick(cb) {
10240
+ return this.renderer.onClick(cb);
10241
+ }
9758
10242
  /** Touch long-press on a price or time axis strip — silent no-op without the seam. */
9759
10243
  onAxisLongPress(cb) {
9760
10244
  return this.renderer.onAxisLongPress?.(cb) ?? (() => void 0);
@@ -10830,11 +11314,76 @@ var MarksControl = class {
10830
11314
  }
10831
11315
  };
10832
11316
 
11317
+ // src/core/ReplayControl.ts
11318
+ var ReplayControl = class {
11319
+ constructor(ctrl) {
11320
+ this.ctrl = ctrl;
11321
+ }
11322
+ /**
11323
+ * Enter replay (or seek, when already replaying) at `opts.from`. Resolves once the
11324
+ * chart shows the rewound history — after any in-flight history load has finished.
11325
+ * Starts paused; call {@link play} or {@link step}.
11326
+ */
11327
+ start(opts) {
11328
+ return this.ctrl.replayStart(opts);
11329
+ }
11330
+ /** Reveal the next bar — or, mid-way through a bar played tick by tick, complete it.
11331
+ * Returns false when replay is off or nothing is left. */
11332
+ step() {
11333
+ return this.ctrl.replayStep();
11334
+ }
11335
+ /** Reveal the next UPDATE: with {@link setTicks}, the next tick of the forming bar — or
11336
+ * the next bar opened at its first tick (once its ticks are in, if still loading);
11337
+ * otherwise the next whole bar, like {@link step}. Returns false when nothing is left. */
11338
+ stepUpdate() {
11339
+ return this.ctrl.replayStepUpdate();
11340
+ }
11341
+ /** Reveal one bar (one tick, with {@link setTicks}) every `intervalMs` (default: the last
11342
+ * interval used, else 1000). Calling it again while playing changes the pace. */
11343
+ play(intervalMs2) {
11344
+ this.ctrl.replayPlay(intervalMs2);
11345
+ return this;
11346
+ }
11347
+ pause() {
11348
+ this.ctrl.replayPause();
11349
+ return this;
11350
+ }
11351
+ /** Leave replay: the full history comes back and live updates resume. Also cancels a
11352
+ * `start()` still loading older history (the chart returns to its previous depth). */
11353
+ stop() {
11354
+ this.ctrl.replayStop();
11355
+ return this;
11356
+ }
11357
+ get state() {
11358
+ return this.ctrl.replayState();
11359
+ }
11360
+ /** The loaded history a replay can walk — hidden bars included while replaying. Null
11361
+ * before any bar loaded. A start older than `first` deepens the history first. */
11362
+ get bounds() {
11363
+ return this.ctrl.replayBounds();
11364
+ }
11365
+ /**
11366
+ * Play each revealed bar as a series of intrabar updates instead of whole: while
11367
+ * playing, the forming candle opens at the first tick, then every tick moves its close
11368
+ * and stretches its high and low, exactly like a live tick — indicators, chart types
11369
+ * and the `bar` event follow — and the last one settles it on the stored bar. The play
11370
+ * interval is then the time between two TICKS: a bar lasts as many intervals as it
11371
+ * has ticks (below ~16 ms, ticks are batched per frame). The next bar's ticks are
11372
+ * requested while the current one plays; playback waits for them. `null` goes back to
11373
+ * whole bars. Takes effect from the next bar; a bar forming when the source changes
11374
+ * completes at once.
11375
+ */
11376
+ setTicks(source) {
11377
+ this.ctrl.replaySetTicks(source);
11378
+ return this;
11379
+ }
11380
+ };
11381
+
10833
11382
  // src/core/util/wall-clock.ts
10834
11383
  var BOUNDARY_SLACK_MS = 5;
10835
11384
  var SecondClock = class _SecondClock {
10836
- constructor(now = () => Date.now()) {
10837
- this.now = now;
11385
+ constructor(now2 = () => Date.now()) {
11386
+ this.now = now2;
10838
11387
  this.subs = /* @__PURE__ */ new Set();
10839
11388
  this.timer = null;
10840
11389
  }
@@ -10850,15 +11399,15 @@ var SecondClock = class _SecondClock {
10850
11399
  };
10851
11400
  }
10852
11401
  /** Milliseconds from `now` to just past the next second boundary. */
10853
- static delayToNextSecond(now) {
10854
- const intoSecond = (now % 1e3 + 1e3) % 1e3;
11402
+ static delayToNextSecond(now2) {
11403
+ const intoSecond = (now2 % 1e3 + 1e3) % 1e3;
10855
11404
  return 1e3 - intoSecond + BOUNDARY_SLACK_MS;
10856
11405
  }
10857
11406
  arm() {
10858
11407
  this.timer = setTimeout(() => {
10859
11408
  this.timer = null;
10860
- const now = this.now();
10861
- for (const cb of this.subs) cb(now);
11409
+ const now2 = this.now();
11410
+ for (const cb of this.subs) cb(now2);
10862
11411
  if (this.subs.size > 0) this.arm();
10863
11412
  }, _SecondClock.delayToNextSecond(this.now()));
10864
11413
  }
@@ -12519,6 +13068,264 @@ var Dialog = class {
12519
13068
  }
12520
13069
  };
12521
13070
 
13071
+ // src/ui/components/date-picker/controller.ts
13072
+ var MONTH_LABELS = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"];
13073
+ var MONTH_SHORT = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
13074
+ var WEEKDAY_LABELS = ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"];
13075
+ function isoDate(d) {
13076
+ return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
13077
+ }
13078
+ function normalizeDateInput(raw) {
13079
+ const m = /^(\d{4})-(\d{1,2})-(\d{1,2})$/.exec(raw.trim());
13080
+ if (!m) return null;
13081
+ const y = Number(m[1]);
13082
+ const mo = Number(m[2]);
13083
+ const d = Number(m[3]);
13084
+ const dt = new Date(y, mo - 1, d);
13085
+ if (dt.getFullYear() !== y || dt.getMonth() !== mo - 1 || dt.getDate() !== d) return null;
13086
+ return isoDate(dt);
13087
+ }
13088
+ function parseIsoDate(raw) {
13089
+ const iso = raw ? normalizeDateInput(raw) : null;
13090
+ if (!iso) return null;
13091
+ const [y, mo, d] = iso.split("-").map(Number);
13092
+ return new Date(y, mo - 1, d);
13093
+ }
13094
+ function datePickerController(opts = {}) {
13095
+ const today = opts.today ?? /* @__PURE__ */ new Date();
13096
+ let selected = parseIsoDate(opts.value);
13097
+ let year = selected?.getFullYear() ?? today.getFullYear();
13098
+ let month = selected?.getMonth() ?? today.getMonth();
13099
+ let panel = "date";
13100
+ const decade = () => Math.floor(year / 10) * 10;
13101
+ return {
13102
+ get panel() {
13103
+ return panel;
13104
+ },
13105
+ get year() {
13106
+ return year;
13107
+ },
13108
+ get month() {
13109
+ return month;
13110
+ },
13111
+ get value() {
13112
+ return selected ? isoDate(selected) : null;
13113
+ },
13114
+ header() {
13115
+ return {
13116
+ month: MONTH_LABELS[month] ?? "",
13117
+ monthVisible: panel === "date",
13118
+ year: panel === "year" ? `${decade()}-${decade() + 9}` : String(year),
13119
+ yearDisabled: panel === "year"
13120
+ // in the decade panel the year reads as its title
13121
+ };
13122
+ },
13123
+ navLabels() {
13124
+ if (panel === "date") return { prev: "Previous month", next: "Next month" };
13125
+ if (panel === "month") return { prev: "Previous year", next: "Next year" };
13126
+ return { prev: "Previous decade", next: "Next decade" };
13127
+ },
13128
+ cells() {
13129
+ const out = [];
13130
+ if (panel === "date") {
13131
+ const startPad = new Date(year, month, 1).getDay();
13132
+ const days = new Date(year, month + 1, 0).getDate();
13133
+ const todayIso = isoDate(today);
13134
+ const selectedIso = selected ? isoDate(selected) : "";
13135
+ for (let i = 0; i < startPad; i++) out.push({ label: "", value: "", checked: false, today: false, blank: true });
13136
+ for (let day = 1; day <= days; day++) {
13137
+ const iso = `${year}-${String(month + 1).padStart(2, "0")}-${String(day).padStart(2, "0")}`;
13138
+ out.push({ label: String(day), value: iso, checked: iso === selectedIso, today: iso === todayIso });
13139
+ }
13140
+ return out;
13141
+ }
13142
+ if (panel === "month") {
13143
+ for (let m = 0; m < 12; m++) {
13144
+ out.push({
13145
+ label: MONTH_SHORT[m] ?? "",
13146
+ value: m,
13147
+ checked: selected?.getFullYear() === year && selected.getMonth() === m,
13148
+ today: today.getFullYear() === year && today.getMonth() === m
13149
+ });
13150
+ }
13151
+ return out;
13152
+ }
13153
+ const d = decade();
13154
+ for (let y = d - 1; y <= d + 10; y++) {
13155
+ out.push({ label: String(y), value: y, checked: selected?.getFullYear() === y, today: today.getFullYear() === y, outside: y < d || y > d + 9 });
13156
+ }
13157
+ return out;
13158
+ },
13159
+ step(dir) {
13160
+ if (panel === "date") {
13161
+ month += dir;
13162
+ if (month < 0) {
13163
+ month = 11;
13164
+ year -= 1;
13165
+ }
13166
+ if (month > 11) {
13167
+ month = 0;
13168
+ year += 1;
13169
+ }
13170
+ } else {
13171
+ year += panel === "month" ? dir : dir * 10;
13172
+ }
13173
+ },
13174
+ showMonths() {
13175
+ panel = "month";
13176
+ },
13177
+ showYears() {
13178
+ panel = "year";
13179
+ },
13180
+ choose(cell) {
13181
+ if (cell.blank) return null;
13182
+ if (panel === "date") {
13183
+ selected = parseIsoDate(String(cell.value));
13184
+ return selected ? isoDate(selected) : null;
13185
+ }
13186
+ if (panel === "month") {
13187
+ month = Number(cell.value);
13188
+ panel = "date";
13189
+ return null;
13190
+ }
13191
+ year = Number(cell.value);
13192
+ panel = "month";
13193
+ return null;
13194
+ },
13195
+ setValue(value) {
13196
+ selected = parseIsoDate(value);
13197
+ if (selected) {
13198
+ year = selected.getFullYear();
13199
+ month = selected.getMonth();
13200
+ }
13201
+ panel = "date";
13202
+ }
13203
+ };
13204
+ }
13205
+
13206
+ // src/ui/components/date-picker/styles.ts
13207
+ var DATE_PICKER_STYLE_ID = "vela-date-picker-styles";
13208
+ var DATE_PICKER_CSS = `
13209
+ .vela-date-picker{color:var(--vela-fg);font:14px var(--vela-font);user-select:none;}
13210
+ .vela-date-picker [hidden]{display:none !important;}
13211
+ .vela-date-picker-head{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:8px;}
13212
+ .vela-date-picker-title{flex:1;display:flex;align-items:center;justify-content:center;gap:2px;min-width:0;}
13213
+ .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;}
13214
+ .vela-date-picker-switch:not(:disabled):hover{background:var(--vela-hover);}
13215
+ .vela-date-picker-switch:disabled{cursor:default;}
13216
+ .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;}
13217
+ .vela-date-picker-nav:hover{background:var(--vela-hover);color:var(--vela-fg-bright);}
13218
+ .vela-date-picker-week,.vela-date-picker-grid{display:grid;grid-template-columns:repeat(7,28px);gap:2px;}
13219
+ .vela-date-picker-week{margin-bottom:4px;color:var(--vela-fg-muted);font-size:11px;text-align:center;}
13220
+ .vela-date-picker-week span{line-height:20px;}
13221
+ .vela-date-picker-blank{width:28px;height:28px;}
13222
+ .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;}
13223
+ .vela-date-picker-day:hover{background:var(--vela-hover);}
13224
+ .vela-date-picker-day[data-checked]{background:var(--vela-hover-strong);color:var(--vela-fg-bright);}
13225
+ .vela-date-picker-day[data-today]:not([data-checked]){box-shadow:inset 0 0 0 1px var(--vela-border-strong);}
13226
+ .vela-date-picker-cells{display:grid;grid-template-columns:repeat(3,1fr);gap:4px;width:calc(7 * 28px + 6 * 2px);}
13227
+ .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;}
13228
+ .vela-date-picker-cell:hover{background:var(--vela-hover);}
13229
+ .vela-date-picker-cell[data-checked]{background:var(--vela-hover-strong);color:var(--vela-fg-bright);}
13230
+ .vela-date-picker-cell[data-today]:not([data-checked]){box-shadow:inset 0 0 0 1px var(--vela-border-strong);}
13231
+ .vela-date-picker-cell[data-outside]{opacity:0.45;}
13232
+ `;
13233
+
13234
+ // src/ui/components/date-picker/view.ts
13235
+ var DatePicker = class {
13236
+ constructor(opts = {}) {
13237
+ this.opts = opts;
13238
+ const doc = document;
13239
+ injectStyles(DATE_PICKER_STYLE_ID, DATE_PICKER_CSS, doc);
13240
+ this.ctrl = datePickerController(opts);
13241
+ const button = (className) => {
13242
+ const b = doc.createElement("button");
13243
+ b.type = "button";
13244
+ b.className = className;
13245
+ return b;
13246
+ };
13247
+ this.prev = button("vela-date-picker-nav");
13248
+ this.prev.append(iconEl("chevron-left", doc));
13249
+ this.next = button("vela-date-picker-nav");
13250
+ this.next.append(iconEl("chevron-right", doc));
13251
+ this.monthBtn = button("vela-date-picker-switch");
13252
+ this.monthBtn.setAttribute("aria-label", "Choose month");
13253
+ this.yearBtn = button("vela-date-picker-switch");
13254
+ this.yearBtn.setAttribute("aria-label", "Choose year");
13255
+ const title = doc.createElement("div");
13256
+ title.className = "vela-date-picker-title";
13257
+ title.append(this.monthBtn, this.yearBtn);
13258
+ const head = doc.createElement("div");
13259
+ head.className = "vela-date-picker-head";
13260
+ head.append(this.prev, title, this.next);
13261
+ this.week = doc.createElement("div");
13262
+ this.week.className = "vela-date-picker-week";
13263
+ for (const d of WEEKDAY_LABELS) {
13264
+ const cell = doc.createElement("span");
13265
+ cell.textContent = d;
13266
+ this.week.appendChild(cell);
13267
+ }
13268
+ this.grid = doc.createElement("div");
13269
+ const on = (el, run) => el.addEventListener("click", (e) => {
13270
+ e.stopPropagation();
13271
+ run();
13272
+ this.paint();
13273
+ });
13274
+ on(this.prev, () => this.ctrl.step(-1));
13275
+ on(this.next, () => this.ctrl.step(1));
13276
+ on(this.monthBtn, () => this.ctrl.showMonths());
13277
+ on(this.yearBtn, () => this.ctrl.showYears());
13278
+ this.el = doc.createElement("div");
13279
+ this.el.className = "vela-date-picker";
13280
+ this.el.append(head, this.week, this.grid);
13281
+ this.paint();
13282
+ }
13283
+ get value() {
13284
+ return this.ctrl.value;
13285
+ }
13286
+ /** Rewrite the selection without emitting (the calendar opens on its month). */
13287
+ setValue(value) {
13288
+ this.ctrl.setValue(value);
13289
+ this.paint();
13290
+ }
13291
+ paint() {
13292
+ const doc = this.el.ownerDocument;
13293
+ const h = this.ctrl.header();
13294
+ this.monthBtn.hidden = !h.monthVisible;
13295
+ this.monthBtn.textContent = h.month;
13296
+ this.yearBtn.textContent = h.year;
13297
+ this.yearBtn.disabled = h.yearDisabled;
13298
+ const nav = this.ctrl.navLabels();
13299
+ this.prev.setAttribute("aria-label", nav.prev);
13300
+ this.next.setAttribute("aria-label", nav.next);
13301
+ const dates = this.ctrl.panel === "date";
13302
+ this.week.hidden = !dates;
13303
+ this.grid.className = dates ? "vela-date-picker-grid" : "vela-date-picker-cells";
13304
+ this.grid.replaceChildren(...this.ctrl.cells().map((cell) => this.cellEl(doc, cell, dates)));
13305
+ }
13306
+ cellEl(doc, cell, dates) {
13307
+ if (cell.blank) {
13308
+ const blank = doc.createElement("span");
13309
+ blank.className = "vela-date-picker-blank";
13310
+ return blank;
13311
+ }
13312
+ const b = doc.createElement("button");
13313
+ b.type = "button";
13314
+ b.className = dates ? "vela-date-picker-day" : "vela-date-picker-cell";
13315
+ b.textContent = cell.label;
13316
+ if (cell.checked) b.dataset.checked = "1";
13317
+ if (cell.today) b.dataset.today = "1";
13318
+ if (cell.outside) b.dataset.outside = "1";
13319
+ b.addEventListener("click", (e) => {
13320
+ e.stopPropagation();
13321
+ const picked = this.ctrl.choose(cell);
13322
+ if (picked) this.opts.onPick?.(picked);
13323
+ else this.paint();
13324
+ });
13325
+ return b;
13326
+ }
13327
+ };
13328
+
12522
13329
  // src/ui/components/switch/controller.ts
12523
13330
  function switchController(opts = {}) {
12524
13331
  let checked = opts.checked ?? false;
@@ -14471,10 +15278,16 @@ var IndicatorInputsDialog = class {
14471
15278
  this.calendarPop = null;
14472
15279
  this.calendarAnchor = null;
14473
15280
  },
14474
- content: (el) => fillCalendar(el, current, (iso) => {
14475
- pop.hide();
14476
- onPick(iso);
14477
- })
15281
+ content: (el) => {
15282
+ const picker = new DatePicker({
15283
+ value: current,
15284
+ onPick: (iso) => {
15285
+ pop.hide();
15286
+ onPick(iso);
15287
+ }
15288
+ });
15289
+ el.append(picker.el);
15290
+ }
14478
15291
  });
14479
15292
  this.calendarPop = pop;
14480
15293
  this.calendarAnchor = anchor;
@@ -14599,28 +15412,6 @@ function ensureDialogStyles() {
14599
15412
  .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;}
14600
15413
  .vela-ind-combo-chevron:hover{opacity:0.9;}
14601
15414
  .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;}
14602
- .vela-ind-cal [hidden]{display:none !important;}
14603
- .vela-ind-cal-head{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:8px;}
14604
- .vela-ind-cal-title{flex:1;display:flex;align-items:center;justify-content:center;gap:2px;min-width:0;}
14605
- .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;}
14606
- .vela-ind-cal-switch:not(:disabled):hover{background:var(--vela-hover);}
14607
- .vela-ind-cal-switch:disabled{cursor:default;}
14608
- .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;}
14609
- .vela-ind-cal-nav:hover{background:var(--vela-hover);color:var(--vela-fg-bright);}
14610
- .vela-ind-cal-week,.vela-ind-cal-grid{display:grid;grid-template-columns:repeat(7,28px);gap:2px;}
14611
- .vela-ind-cal-week{margin-bottom:4px;color:var(--vela-fg-muted);font-size:11px;text-align:center;}
14612
- .vela-ind-cal-week span{line-height:20px;}
14613
- .vela-ind-cal-blank{width:28px;height:28px;}
14614
- .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;}
14615
- .vela-ind-cal-day:hover{background:var(--vela-hover);}
14616
- .vela-ind-cal-day[data-checked]{background:var(--vela-hover-strong);color:var(--vela-fg-bright);}
14617
- .vela-ind-cal-day[data-today]:not([data-checked]){box-shadow:inset 0 0 0 1px var(--vela-border-strong);}
14618
- .vela-ind-cal-cells{display:grid;grid-template-columns:repeat(3,1fr);gap:4px;width:calc(7 * 28px + 6 * 2px);}
14619
- .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;}
14620
- .vela-ind-cal-cell:hover{background:var(--vela-hover);}
14621
- .vela-ind-cal-cell[data-checked]{background:var(--vela-hover-strong);color:var(--vela-fg-bright);}
14622
- .vela-ind-cal-cell[data-today]:not([data-checked]){box-shadow:inset 0 0 0 1px var(--vela-border-strong);}
14623
- .vela-ind-cal-cell[data-outside]{opacity:0.45;}
14624
15415
  ${overlayScrollbarCss(".vela-dialog.vela-ind-dialog *", 9)}
14625
15416
  .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;}
14626
15417
  .vela-ind-tab:not(.vela-ind-tab-active):hover{color:var(--vela-fg-bright);}
@@ -14654,174 +15445,6 @@ var TIME_OPTIONS = Array.from({ length: 48 }, (_, i) => {
14654
15445
  const v = `${hh}:${mm}`;
14655
15446
  return { value: v, label: v };
14656
15447
  });
14657
- var MONTH_LABELS = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"];
14658
- var MONTH_SHORT = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
14659
- var WEEKDAY_LABELS = ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"];
14660
- function normalizeDateInput(raw) {
14661
- const t = raw.trim();
14662
- const m = /^(\d{4})-(\d{1,2})-(\d{1,2})$/.exec(t);
14663
- if (!m) return null;
14664
- const y = Number(m[1]);
14665
- const mo = Number(m[2]);
14666
- const d = Number(m[3]);
14667
- const dt = new Date(y, mo - 1, d);
14668
- if (dt.getFullYear() !== y || dt.getMonth() !== mo - 1 || dt.getDate() !== d) return null;
14669
- return isoDate(dt);
14670
- }
14671
- function parseIsoDate(raw) {
14672
- const iso = normalizeDateInput(raw);
14673
- if (!iso) return null;
14674
- const [y, mo, d] = iso.split("-").map(Number);
14675
- return new Date(y, mo - 1, d);
14676
- }
14677
- function isoDate(d) {
14678
- return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
14679
- }
14680
- function fillCalendar(el, current, onPick) {
14681
- const today = /* @__PURE__ */ new Date();
14682
- const selected = parseIsoDate(current);
14683
- let year = selected?.getFullYear() ?? today.getFullYear();
14684
- let month = selected?.getMonth() ?? today.getMonth();
14685
- let mode = "date";
14686
- const prev2 = document.createElement("button");
14687
- prev2.type = "button";
14688
- prev2.className = "vela-ind-cal-nav";
14689
- prev2.innerHTML = iconAt("chevron-left", 14);
14690
- const next = document.createElement("button");
14691
- next.type = "button";
14692
- next.className = "vela-ind-cal-nav";
14693
- next.innerHTML = iconAt("chevron-right", 14);
14694
- const monthBtn = document.createElement("button");
14695
- monthBtn.type = "button";
14696
- monthBtn.className = "vela-ind-cal-switch";
14697
- monthBtn.setAttribute("aria-label", "Choose month");
14698
- const yearBtn = document.createElement("button");
14699
- yearBtn.type = "button";
14700
- yearBtn.className = "vela-ind-cal-switch";
14701
- yearBtn.setAttribute("aria-label", "Choose year");
14702
- const title = document.createElement("div");
14703
- title.className = "vela-ind-cal-title";
14704
- title.append(monthBtn, yearBtn);
14705
- const head = document.createElement("div");
14706
- head.className = "vela-ind-cal-head";
14707
- head.append(prev2, title, next);
14708
- const week = document.createElement("div");
14709
- week.className = "vela-ind-cal-week";
14710
- for (const d of WEEKDAY_LABELS) {
14711
- const cell = document.createElement("span");
14712
- cell.textContent = d;
14713
- week.appendChild(cell);
14714
- }
14715
- const grid = document.createElement("div");
14716
- const paint = () => {
14717
- const decade = Math.floor(year / 10) * 10;
14718
- monthBtn.hidden = mode !== "date";
14719
- monthBtn.textContent = MONTH_LABELS[month] ?? "";
14720
- yearBtn.textContent = mode === "year" ? `${decade}-${decade + 9}` : String(year);
14721
- yearBtn.disabled = mode === "year";
14722
- week.hidden = mode !== "date";
14723
- prev2.setAttribute("aria-label", mode === "date" ? "Previous month" : mode === "month" ? "Previous year" : "Previous decade");
14724
- next.setAttribute("aria-label", mode === "date" ? "Next month" : mode === "month" ? "Next year" : "Next decade");
14725
- grid.className = mode === "date" ? "vela-ind-cal-grid" : "vela-ind-cal-cells";
14726
- grid.replaceChildren();
14727
- if (mode === "date") {
14728
- const startPad = new Date(year, month, 1).getDay();
14729
- const days = new Date(year, month + 1, 0).getDate();
14730
- const todayIso = isoDate(today);
14731
- const selectedIso = selected ? isoDate(selected) : "";
14732
- for (let i = 0; i < startPad; i++) {
14733
- const blank = document.createElement("span");
14734
- blank.className = "vela-ind-cal-blank";
14735
- grid.appendChild(blank);
14736
- }
14737
- for (let day = 1; day <= days; day++) {
14738
- const iso = `${year}-${String(month + 1).padStart(2, "0")}-${String(day).padStart(2, "0")}`;
14739
- const b = document.createElement("button");
14740
- b.type = "button";
14741
- b.className = "vela-ind-cal-day";
14742
- b.textContent = String(day);
14743
- if (iso === selectedIso) b.dataset.checked = "1";
14744
- if (iso === todayIso) b.dataset.today = "1";
14745
- b.addEventListener("click", (e) => {
14746
- e.stopPropagation();
14747
- onPick(iso);
14748
- });
14749
- grid.appendChild(b);
14750
- }
14751
- return;
14752
- }
14753
- if (mode === "month") {
14754
- for (let m = 0; m < 12; m++) {
14755
- const b = document.createElement("button");
14756
- b.type = "button";
14757
- b.className = "vela-ind-cal-cell";
14758
- b.textContent = MONTH_SHORT[m] ?? "";
14759
- if (selected?.getFullYear() === year && selected.getMonth() === m) b.dataset.checked = "1";
14760
- if (today.getFullYear() === year && today.getMonth() === m) b.dataset.today = "1";
14761
- b.addEventListener("click", (e) => {
14762
- e.stopPropagation();
14763
- month = m;
14764
- mode = "date";
14765
- paint();
14766
- });
14767
- grid.appendChild(b);
14768
- }
14769
- return;
14770
- }
14771
- for (let y = decade - 1; y <= decade + 10; y++) {
14772
- const b = document.createElement("button");
14773
- b.type = "button";
14774
- b.className = "vela-ind-cal-cell";
14775
- b.textContent = String(y);
14776
- if (y < decade || y > decade + 9) b.dataset.outside = "1";
14777
- if (selected?.getFullYear() === y) b.dataset.checked = "1";
14778
- if (today.getFullYear() === y) b.dataset.today = "1";
14779
- b.addEventListener("click", (e) => {
14780
- e.stopPropagation();
14781
- year = y;
14782
- mode = "month";
14783
- paint();
14784
- });
14785
- grid.appendChild(b);
14786
- }
14787
- };
14788
- const step = (dir) => {
14789
- if (mode === "date") {
14790
- month += dir;
14791
- if (month < 0) {
14792
- month = 11;
14793
- year -= 1;
14794
- }
14795
- if (month > 11) {
14796
- month = 0;
14797
- year += 1;
14798
- }
14799
- } else {
14800
- year += mode === "month" ? dir : dir * 10;
14801
- }
14802
- paint();
14803
- };
14804
- prev2.addEventListener("click", (e) => {
14805
- e.stopPropagation();
14806
- step(-1);
14807
- });
14808
- next.addEventListener("click", (e) => {
14809
- e.stopPropagation();
14810
- step(1);
14811
- });
14812
- monthBtn.addEventListener("click", (e) => {
14813
- e.stopPropagation();
14814
- mode = "month";
14815
- paint();
14816
- });
14817
- yearBtn.addEventListener("click", (e) => {
14818
- e.stopPropagation();
14819
- mode = "year";
14820
- paint();
14821
- });
14822
- paint();
14823
- el.append(head, week, grid);
14824
- }
14825
15448
  function normalizeTimeInput(raw) {
14826
15449
  const t = raw.trim();
14827
15450
  const m = /^(\d{1,2}):(\d{2})$/.exec(t) ?? /^(\d{2})(\d{2})$/.exec(t);
@@ -16199,6 +16822,22 @@ function priceStyleIds() {
16199
16822
  for (const t of chartTypes()) if (!out.includes(t.id)) out.push(t.id);
16200
16823
  return out;
16201
16824
  }
16825
+ function sanitizeCrosshairOverride(value) {
16826
+ if (!value || typeof value !== "object") return null;
16827
+ const v = value;
16828
+ const out = {};
16829
+ if (isBool(v.vertical)) out.vertical = v.vertical;
16830
+ if (isBool(v.horizontal)) out.horizontal = v.horizontal;
16831
+ if (isColor(v.color)) out.color = v.color;
16832
+ if (isNum(v.width)) out.width = Math.max(0.5, Math.min(8, v.width));
16833
+ if (isLineStyle(v.style)) out.style = v.style;
16834
+ if (isNum(v.opacity)) out.opacity = clampOpacity(v.opacity);
16835
+ const shade = v.shadeRight;
16836
+ if (shade && typeof shade === "object" && isColor(shade.color)) {
16837
+ out.shadeRight = isNum(shade.opacity) ? { color: shade.color, opacity: clampOpacity(shade.opacity) } : { color: shade.color };
16838
+ }
16839
+ return Object.keys(out).length > 0 ? out : null;
16840
+ }
16202
16841
  function isColor(v) {
16203
16842
  return typeof v === "string" && v.trim().length > 0;
16204
16843
  }
@@ -17860,7 +18499,7 @@ var Scheduler = class {
17860
18499
 
17861
18500
  // src/renderers/native/core/Animator.ts
17862
18501
  var Animator = class {
17863
- constructor(tick, raf, cancel, now) {
18502
+ constructor(tick, raf, cancel, now2) {
17864
18503
  this.tick = tick;
17865
18504
  this.handle = null;
17866
18505
  this.last = 0;
@@ -17874,7 +18513,7 @@ var Animator = class {
17874
18513
  };
17875
18514
  this.raf = raf ?? ((cb) => requestAnimationFrame(cb));
17876
18515
  this.cancel = cancel ?? ((h) => cancelAnimationFrame(h));
17877
- this.now = now ?? (() => performance.now());
18516
+ this.now = now2 ?? (() => performance.now());
17878
18517
  }
17879
18518
  get active() {
17880
18519
  return this.handle !== null;
@@ -18673,6 +19312,8 @@ var SceneGraph = class {
18673
19312
  /** VPVR-layer config pushed by the VPVR native indicator (null ⇒ layer off). Ephemeral. */
18674
19313
  this.vpvrLayer = null;
18675
19314
  this.crosshair = null;
19315
+ /** Runtime override of the crosshair's lines and style (the `crosshairOverride` feature). Ephemeral. */
19316
+ this.crosshairOverride = null;
18676
19317
  /** How the base price series is drawn on the price pane (candles by default). */
18677
19318
  this.priceStyle = "candles";
18678
19319
  /** Price-series base painting for the ACTIVE style (see ChartTypeDefinition.basePainting). */
@@ -20589,9 +21230,9 @@ function timeTicks(fromMs, toMs, target = 8, offsetMs = 0) {
20589
21230
  }
20590
21231
 
20591
21232
  // src/renderers/native/chrome/countdown.ts
20592
- function countdownText(barOpen, barMs, now) {
21233
+ function countdownText(barOpen, barMs, now2) {
20593
21234
  if (!(barMs > 0)) return null;
20594
- const remaining = barOpen + barMs - now;
21235
+ const remaining = barOpen + barMs - now2;
20595
21236
  if (remaining <= 0) return null;
20596
21237
  return formatCountdown(remaining);
20597
21238
  }
@@ -21403,19 +22044,35 @@ var CrosshairRenderer = class {
21403
22044
  const dataH = coords.height;
21404
22045
  if (!ch || ch.x < 0 || ch.x > dataW || ch.y < 0 || ch.y > dataH) return;
21405
22046
  const cs = scene.style.crosshair;
22047
+ const ov = scene.crosshairOverride;
22048
+ const vertical = ov?.vertical !== false;
22049
+ const horizontal = ov?.horizontal !== false;
21406
22050
  ctx.font = `${scene.style.fontSize}px ${theme.fontFamily}`;
21407
22051
  ctx.textBaseline = "middle";
21408
22052
  const logical = Math.round(coords.xToLogical(ch.x));
21409
- const x = Math.round(coords.logicalToX(logical)) + 0.5;
21410
- ctx.strokeStyle = cs.color ?? theme.textColor;
21411
- ctx.lineWidth = cs.width;
21412
- ctx.globalAlpha = cs.opacity;
21413
- setDash3(ctx, cs.style);
22053
+ const x = crisp(coords.logicalToX(logical), ov?.width ?? cs.width);
22054
+ if (vertical && ov?.shadeRight) {
22055
+ const from = Math.max(0, Math.round(coords.logicalToX(logical + 0.5)));
22056
+ if (from < dataW) {
22057
+ ctx.fillStyle = ov.shadeRight.color;
22058
+ ctx.globalAlpha = ov.shadeRight.opacity ?? 1;
22059
+ ctx.fillRect(from, 0, dataW - from, dataH);
22060
+ ctx.globalAlpha = 1;
22061
+ }
22062
+ }
22063
+ ctx.strokeStyle = ov?.color ?? cs.color ?? theme.textColor;
22064
+ ctx.lineWidth = ov?.width ?? cs.width;
22065
+ ctx.globalAlpha = ov?.opacity ?? cs.opacity;
22066
+ setDash3(ctx, ov?.style ?? cs.style);
21414
22067
  ctx.beginPath();
21415
- ctx.moveTo(x, 0);
21416
- ctx.lineTo(x, dataH);
21417
- ctx.moveTo(0, Math.round(ch.y) + 0.5);
21418
- ctx.lineTo(dataW, Math.round(ch.y) + 0.5);
22068
+ if (vertical) {
22069
+ ctx.moveTo(x, 0);
22070
+ ctx.lineTo(x, dataH);
22071
+ }
22072
+ if (horizontal) {
22073
+ ctx.moveTo(0, Math.round(ch.y) + 0.5);
22074
+ ctx.lineTo(dataW, Math.round(ch.y) + 0.5);
22075
+ }
21419
22076
  ctx.stroke();
21420
22077
  ctx.setLineDash([]);
21421
22078
  ctx.lineWidth = 1;
@@ -21428,11 +22085,11 @@ var CrosshairRenderer = class {
21428
22085
  }
21429
22086
  }
21430
22087
  const chipBg = cs.labelBackground ?? theme.borderColor;
21431
- if (pane && pane.axisFormat !== "none") {
22088
+ if (horizontal && pane && pane.axisFormat !== "none") {
21432
22089
  const price = coords.yToPrice(ch.y, pane.scale, pane.bounds);
21433
22090
  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);
21434
22091
  }
21435
- this.chip(ctx, x, dataH + 1, formatTimeStamp(coords.logicalToTime(logical), scene.timezone, coords.barInterval), chipBg, "center", true, theme.background);
22092
+ if (vertical) this.chip(ctx, x, dataH + 1, formatTimeStamp(coords.logicalToTime(logical), scene.timezone, coords.barInterval), chipBg, "center", true, theme.background);
21436
22093
  }
21437
22094
  destroy() {
21438
22095
  this.canvas = null;
@@ -21443,23 +22100,37 @@ var CrosshairRenderer = class {
21443
22100
  * along), with that bar's time chip in this chart's own timezone and — when the
21444
22101
  * level resolved — the price chip on the right axis. The snap happened upstream
21445
22102
  * (`externalCrossPx`, floor-to-containing-bar) — this method only draws. Chips
21446
- * render slightly dimmed so the ghost still reads as foreign. */
22103
+ * render slightly dimmed so the ghost still reads as foreign. A `crosshairOverride`
22104
+ * applies to the ghost as well — its line style, a dropped horizontal line, and the
22105
+ * `shadeRight` veil from the ghost bar's right edge — so a pick that spans several
22106
+ * charts reads the same on every one of them. */
21447
22107
  drawExternal(ctx, ext, scene, coords, theme) {
21448
22108
  const cs = scene.style.crosshair;
22109
+ const ov = scene.crosshairOverride;
21449
22110
  const dataW = coords.width;
21450
22111
  const dataH = coords.height;
21451
- const x = Math.round(ext.x) + 0.5;
21452
- if (x < 0 || x > dataW) return;
22112
+ const x = crisp(ext.x, ov?.width ?? cs.width);
22113
+ if (ov?.vertical === false) return;
22114
+ if (ov?.shadeRight) {
22115
+ const from = Math.max(0, Math.round(coords.logicalToX(Math.round(coords.xToLogical(ext.x)) + 0.5)));
22116
+ if (from < dataW) {
22117
+ ctx.fillStyle = ov.shadeRight.color;
22118
+ ctx.globalAlpha = ov.shadeRight.opacity ?? 1;
22119
+ ctx.fillRect(from, 0, dataW - from, dataH);
22120
+ ctx.globalAlpha = 1;
22121
+ }
22122
+ }
22123
+ if (ext.line === false || x < 0 || x > dataW) return;
21453
22124
  ctx.font = `${scene.style.fontSize}px ${theme.fontFamily}`;
21454
22125
  ctx.textBaseline = "middle";
21455
- ctx.strokeStyle = cs.color ?? theme.textColor;
21456
- ctx.lineWidth = cs.width;
21457
- ctx.globalAlpha = cs.opacity * 0.55;
21458
- setDash3(ctx, cs.style);
22126
+ ctx.strokeStyle = ov?.color ?? cs.color ?? theme.textColor;
22127
+ ctx.lineWidth = ov?.width ?? cs.width;
22128
+ ctx.globalAlpha = (ov?.opacity ?? cs.opacity) * 0.55;
22129
+ setDash3(ctx, ov?.style ?? cs.style);
21459
22130
  ctx.beginPath();
21460
22131
  ctx.moveTo(x, 0);
21461
22132
  ctx.lineTo(x, dataH);
21462
- if (ext.y != null) {
22133
+ if (ext.y != null && ov?.horizontal !== false) {
21463
22134
  ctx.moveTo(0, Math.round(ext.y) + 0.5);
21464
22135
  ctx.lineTo(dataW, Math.round(ext.y) + 0.5);
21465
22136
  }
@@ -21468,7 +22139,7 @@ var CrosshairRenderer = class {
21468
22139
  ctx.lineWidth = 1;
21469
22140
  ctx.globalAlpha = 0.8;
21470
22141
  const chipBg = cs.labelBackground ?? theme.borderColor;
21471
- if (ext.y != null && ext.price != null) {
22142
+ if (ext.y != null && ext.price != null && ov?.horizontal !== false) {
21472
22143
  let pane;
21473
22144
  for (const p of scene.panes.values()) {
21474
22145
  if (ext.y >= p.bounds.top && ext.y <= p.bounds.top + p.bounds.height) {
@@ -21508,6 +22179,9 @@ var CrosshairRenderer = class {
21508
22179
  ctx.textAlign = "start";
21509
22180
  }
21510
22181
  };
22182
+ function crisp(px, width) {
22183
+ return Math.round(px) + (Math.round(width) % 2 === 1 ? 0.5 : 0);
22184
+ }
21511
22185
  function setDash3(ctx, style) {
21512
22186
  if (style === "dashed") ctx.setLineDash([6, 4]);
21513
22187
  else if (style === "dotted") ctx.setLineDash([2, 3]);
@@ -29804,7 +30478,7 @@ var NativeRenderer = class {
29804
30478
  this.moveIndicatorCbs = /* @__PURE__ */ new Set();
29805
30479
  this.priceStyleCbs = /* @__PURE__ */ new Set();
29806
30480
  this.name = "native";
29807
- 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"];
30481
+ 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"];
29808
30482
  /** Track cursor proximity to the scroll button on the plot (bubbles from the button too,
29809
30483
  * so moving onto the button doesn't count as leaving). */
29810
30484
  this.onScrollProximityMove = (e) => {
@@ -29983,6 +30657,9 @@ var NativeRenderer = class {
29983
30657
  this.scene.marks = mergeMarksState(this.scene.marks, value);
29984
30658
  this.markPopover?.close();
29985
30659
  break;
30660
+ case "crosshairOverride":
30661
+ this.scene.crosshairOverride = sanitizeCrosshairOverride(value);
30662
+ break;
29986
30663
  case "keyboard":
29987
30664
  this.setKeyboardEnabled(Boolean(value));
29988
30665
  return;
@@ -30096,6 +30773,8 @@ var NativeRenderer = class {
30096
30773
  return { ...this.scene.tradeMarkers, colors: { ...this.scene.tradeMarkers.colors } };
30097
30774
  case "marks":
30098
30775
  return { visible: this.scene.marks.visible, groups: { ...this.scene.marks.groups } };
30776
+ case "crosshairOverride":
30777
+ return this.scene.crosshairOverride ? { ...this.scene.crosshairOverride } : null;
30099
30778
  case "keyboard":
30100
30779
  return this.keyboardEnabled;
30101
30780
  case "historyChords":
@@ -30674,8 +31353,8 @@ var NativeRenderer = class {
30674
31353
  for (const pane of this.scene.panes.values()) pane.scale = { ...pane.scaleTarget };
30675
31354
  this.modelAlpha = 0;
30676
31355
  const start = performance.now();
30677
- const step = (now) => {
30678
- const p = Math.min(1, (now - start) / duration);
31356
+ const step = (now2) => {
31357
+ const p = Math.min(1, (now2 - start) / duration);
30679
31358
  this.scene.bars = p >= 1 ? real : real.map((b, i) => this.revealCandle(b, i, p, n, style));
30680
31359
  this.paintData();
30681
31360
  if (p < 1) {
@@ -30691,8 +31370,8 @@ var NativeRenderer = class {
30691
31370
  fadeInModels() {
30692
31371
  const fade = Math.min(INTRO_MODEL_FADE_MS, this.intro.duration || INTRO_MODEL_FADE_MS);
30693
31372
  const start = performance.now();
30694
- const step = (now) => {
30695
- this.modelAlpha = Math.min(1, (now - start) / fade);
31373
+ const step = (now2) => {
31374
+ this.modelAlpha = Math.min(1, (now2 - start) / fade);
30696
31375
  this.paintData();
30697
31376
  if (this.modelAlpha < 1) {
30698
31377
  this.introRaf = requestAnimationFrame(step);
@@ -31215,10 +31894,13 @@ var NativeRenderer = class {
31215
31894
  this.animator.start();
31216
31895
  }
31217
31896
  } else if (!last || bar.time > last.time) {
31897
+ const vp = this.coords.getViewport();
31898
+ const holdView = n > 0 && vp.rightOffset < 0 && this.scrollTargetRO === null;
31218
31899
  this.bars.push(bar);
31219
31900
  this.syncLiveEase(bar);
31220
31901
  if (this.coords.barInterval > 0) this.coords.appendBar(bar.time);
31221
31902
  else this.coords.setBars(this.bars.map((b) => b.time));
31903
+ if (holdView) this.coords.setViewport({ ...vp, rightOffset: vp.rightOffset - 1 });
31222
31904
  } else {
31223
31905
  return;
31224
31906
  }
@@ -31900,10 +32582,10 @@ var NativeRenderer = class {
31900
32582
  if (this.markPulseRaf !== null || typeof requestAnimationFrame !== "function") return;
31901
32583
  const tick = () => {
31902
32584
  this.markPulseRaf = null;
31903
- const now = frameNow();
31904
- if (this.scene.marksFlash && this.scene.marksFlash.until <= now) this.scene.marksFlash = null;
32585
+ const now2 = frameNow();
32586
+ if (this.scene.marksFlash && this.scene.marksFlash.until <= now2) this.scene.marksFlash = null;
31905
32587
  this.scheduler?.invalidate(2 /* Chrome */);
31906
- const pulsing = this.scene.marksHoverKey !== null && now - this.scene.marksHoverSince < MARK_PULSE_MS;
32588
+ const pulsing = this.scene.marksHoverKey !== null && now2 - this.scene.marksHoverSince < MARK_PULSE_MS;
31907
32589
  if (pulsing || this.scene.marksFlash !== null) this.markPulseRaf = requestAnimationFrame(tick);
31908
32590
  };
31909
32591
  this.markPulseRaf = requestAnimationFrame(tick);
@@ -32464,9 +33146,10 @@ var NativeRenderer = class {
32464
33146
  const ext = this.externalCross;
32465
33147
  if (!ext || this.coords.barCount === 0) return null;
32466
33148
  const logical = Math.floor(this.coords.timeToLogical(ext.time));
32467
- if (logical < 0 || logical >= this.coords.barCount) return null;
32468
- const x = this.coords.logicalToX(logical);
32469
- if (!Number.isFinite(x) || x < 0 || x > this.coords.width) return null;
33149
+ if (!(logical < this.coords.barCount)) return null;
33150
+ const x = this.coords.logicalToX(Math.max(logical, -1));
33151
+ if (!Number.isFinite(x)) return null;
33152
+ const line = logical >= 0 && x >= 0 && x <= this.coords.width;
32470
33153
  let y = null;
32471
33154
  if (ext.price != null) {
32472
33155
  const pricePane = this.scene.orderedPanes().find((p) => p.kind === "price");
@@ -32475,7 +33158,7 @@ var NativeRenderer = class {
32475
33158
  if (!Number.isFinite(y) || y < pricePane.bounds.top || y > pricePane.bounds.top + pricePane.bounds.height) y = null;
32476
33159
  }
32477
33160
  }
32478
- return { x, y, time: this.coords.logicalToTime(logical), price: y != null ? ext.price : null };
33161
+ return { x, y, time: this.coords.logicalToTime(Math.max(logical, 0)), price: y != null ? ext.price : null, line };
32479
33162
  }
32480
33163
  /** Set the sticky magnet mode (called by the drawings toolbar's 3-state button). */
32481
33164
  setSnapMode(mode) {
@@ -37180,6 +37863,7 @@ var Vela = class {
37180
37863
  this.panesControl = new PanesControl(this.orchestrator);
37181
37864
  this.drawingsControl = new DrawingsControl(this.orchestrator.drawings);
37182
37865
  this.marksControl = new MarksControl(this.orchestrator.marks);
37866
+ this.replayControl = new ReplayControl(this.orchestrator);
37183
37867
  }
37184
37868
  /**
37185
37869
  * Register a scripting engine so `addIndicator({ language })` can run that
@@ -37419,6 +38103,16 @@ var Vela = class {
37419
38103
  get marks() {
37420
38104
  return this.marksControl;
37421
38105
  }
38106
+ /**
38107
+ * The chart's bar-replay control surface: rewind to a past bar and reveal the
38108
+ * following ones by hand or on a timer, from the history already loaded —
38109
+ * `await chart.replay.start({ from })`, `chart.replay.play(2000)`, `chart.replay.step()`,
38110
+ * `chart.replay.stop()`. Revealed bars behave like live bars for indicators; live
38111
+ * updates pause meanwhile. Follow it with the `replay:*` events.
38112
+ */
38113
+ get replay() {
38114
+ return this.replayControl;
38115
+ }
37422
38116
  on(event, handler) {
37423
38117
  return this.orchestrator.events.on(event, handler);
37424
38118
  }
@@ -37477,6 +38171,28 @@ function resolveElement(container) {
37477
38171
  return element;
37478
38172
  }
37479
38173
 
38174
+ // src/core/replayTicks.ts
38175
+ function lowerTimeframeTicks(chart, timeframe) {
38176
+ return async (bar, { end, signal }) => {
38177
+ const { symbol, session } = chart.market;
38178
+ const resolved = symbol ? chart.data.resolve(symbol) : null;
38179
+ const provider = resolved ? chart.data.providerInstance(resolved.provider) : void 0;
38180
+ if (!resolved || !provider) return [];
38181
+ const bars = await provider.getBars(resolved.ticker, timeframe, { from: bar.time, to: end - 1, ...session ? { session } : {} });
38182
+ if (signal.aborted) return [];
38183
+ return barsToTicks(bars.filter((b) => b.time >= bar.time && b.time < end));
38184
+ };
38185
+ }
38186
+ function barsToTicks(bars) {
38187
+ return bars.map((b) => ({
38188
+ price: b.close,
38189
+ open: b.open,
38190
+ high: b.high,
38191
+ low: b.low,
38192
+ ...b.volume === void 0 ? {} : { volume: b.volume }
38193
+ }));
38194
+ }
38195
+
37480
38196
  // src/widget/topbar-composition.ts
37481
38197
  var TOPBAR_BUILTIN_IDS = ["symbol", "timeframes", "style", "layout", "indicators", "actions", "undo-redo", "alerts", "panels", "screenshot"];
37482
38198
 
@@ -37637,6 +38353,7 @@ exports.NEUTRAL = NEUTRAL;
37637
38353
  exports.NativeRenderer = NativeRenderer;
37638
38354
  exports.OVERRIDABLE_TOPBAR_IDS = OVERRIDABLE_TOPBAR_IDS;
37639
38355
  exports.RendererControl = RendererControl;
38356
+ exports.ReplayControl = ReplayControl;
37640
38357
  exports.SERIES_LINE = SERIES_LINE;
37641
38358
  exports.SESSION_OFF = SESSION_OFF;
37642
38359
  exports.SESSION_POST = SESSION_POST;
@@ -37651,6 +38368,7 @@ exports.TypedEventBus = TypedEventBus;
37651
38368
  exports.VALID = VALID;
37652
38369
  exports.Vela = Vela;
37653
38370
  exports.WARNING = WARNING;
38371
+ exports.barsToTicks = barsToTicks;
37654
38372
  exports.buildToolbar = buildToolbar;
37655
38373
  exports.categoricalColor = categoricalColor;
37656
38374
  exports.chartType = chartType;
@@ -37667,6 +38385,7 @@ exports.inputDeltas = inputDeltas;
37667
38385
  exports.inputVisible = inputVisible;
37668
38386
  exports.legendActions = legendActions;
37669
38387
  exports.legendCallouts = legendCallouts;
38388
+ exports.lowerTimeframeTicks = lowerTimeframeTicks;
37670
38389
  exports.nativeIndicatorDescriptors = nativeIndicatorDescriptors;
37671
38390
  exports.nativeIndicatorTypes = nativeIndicatorTypes;
37672
38391
  exports.normalizeSettingsRow = normalizeSettingsRow;