@luxalgo/vela 0.7.8 → 0.8.1

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 (47) hide show
  1. package/NOTICE +28 -28
  2. package/README.md +15 -7
  3. package/dist/{DataProvider-CGa4KHRa.d.cts → DataProvider-BrlxntPN.d.cts} +1 -1
  4. package/dist/{DataProvider-CmWUtDVL.d.ts → DataProvider-DwFIoV4r.d.ts} +1 -1
  5. package/dist/{chunk-N4ZZYLMP.js → chunk-ATGTCWHJ.js} +726 -273
  6. package/dist/{chunk-CAFCLMPF.js → chunk-BZQM2XO7.js} +1 -0
  7. package/dist/{chunk-M6GP7DAX.js → chunk-DBDXKZUF.js} +474 -47
  8. package/dist/{chunk-BFA32GOU.js → chunk-EZ5FWVLA.js} +5 -2
  9. package/dist/{chunk-PVGWNF7V.js → chunk-LMBYEGUR.js} +2 -2
  10. package/dist/{chunk-WTWGEB7K.js → chunk-NELQJCGK.js} +298 -5
  11. package/dist/{contributions-DF_Ea07m.d.cts → contributions-CtlptxR8.d.cts} +187 -5
  12. package/dist/{contributions-nKOSeLy8.d.ts → contributions-tjNExo1o.d.ts} +187 -5
  13. package/dist/index.cjs +1044 -272
  14. package/dist/index.d.cts +24 -8
  15. package/dist/index.d.ts +24 -8
  16. package/dist/index.js +28 -4
  17. package/dist/{options-D1AJKsn_.d.ts → options-BnEPUuVV.d.cts} +1 -1
  18. package/dist/{options-D1AJKsn_.d.cts → options-BnEPUuVV.d.ts} +1 -1
  19. package/dist/{plugin-6F04gjQe.d.cts → plugin-BWxJRKzS.d.cts} +10 -4
  20. package/dist/{plugin-BLsNipkc.d.ts → plugin-e2WUEtiI.d.ts} +10 -4
  21. package/dist/plugin.cjs +1 -0
  22. package/dist/plugin.d.cts +4 -4
  23. package/dist/plugin.d.ts +4 -4
  24. package/dist/plugin.js +2 -2
  25. package/dist/providers/binance.d.cts +2 -2
  26. package/dist/providers/binance.d.ts +2 -2
  27. package/dist/providers/coinbase.d.cts +2 -2
  28. package/dist/providers/coinbase.d.ts +2 -2
  29. package/dist/providers/hyperliquid.d.cts +2 -2
  30. package/dist/providers/hyperliquid.d.ts +2 -2
  31. package/dist/{statusline-model-DAFyqVmR.d.ts → statusline-model-DFY7tuHj.d.cts} +5 -3
  32. package/dist/{statusline-model-BoWJ3cWQ.d.cts → statusline-model-zKXtGIjj.d.ts} +5 -3
  33. package/dist/ui.cjs +301 -3
  34. package/dist/ui.d.cts +75 -2
  35. package/dist/ui.d.ts +75 -2
  36. package/dist/ui.js +3 -3
  37. package/dist/vela.global.js +1046 -274
  38. package/dist/vela.global.min.js +82 -87
  39. package/dist/widget.cjs +1504 -327
  40. package/dist/widget.d.cts +26 -7
  41. package/dist/widget.d.ts +26 -7
  42. package/dist/widget.js +7 -7
  43. package/dist/workspace.cjs +1507 -327
  44. package/dist/workspace.d.cts +125 -7
  45. package/dist/workspace.d.ts +125 -7
  46. package/dist/workspace.js +6 -6
  47. package/package.json +2 -2
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;
@@ -13240,14 +14047,49 @@ function buildColorPicker(color, theme, onChange, options = {}) {
13240
14047
  const knob = document.createElement("div");
13241
14048
  knob.style.cssText = "position:absolute;top:50%;width:15px;height:15px;border-radius:50%;background:var(--vela-selected-bg);box-shadow:0 1px 3px rgba(0,0,0,0.55);transform:translate(-50%,-50%);pointer-events:none;";
13242
14049
  track.appendChild(knob);
13243
- const pctBox = document.createElement("div");
13244
- pctBox.style.cssText = `min-width:42px;text-align:center;font:var(--vela-font-size-md) ${theme.fontFamily};color:var(--vela-fg);border:1px solid var(--vela-border);border-radius:5px;padding:3px 4px;`;
14050
+ const pctBox = document.createElement("input");
14051
+ pctBox.type = "text";
14052
+ pctBox.inputMode = "numeric";
14053
+ pctBox.title = "Opacity (%)";
14054
+ pctBox.style.cssText = `width:46px;box-sizing:border-box;text-align:center;font:var(--vela-font-size-md) ${theme.fontFamily};color:var(--vela-fg);background:transparent;border:1px solid var(--vela-border);border-radius:5px;padding:3px 4px;outline:none;`;
14055
+ pctBox.addEventListener("focus", () => {
14056
+ pctBox.style.borderColor = "var(--vela-border-strong)";
14057
+ pctBox.select();
14058
+ });
13245
14059
  opRow.append(track, pctBox);
13246
14060
  const paintOpacity = () => {
13247
14061
  track.style.background = `linear-gradient(to right, ${curHex}00, ${curHex}ff), ${CHECKER}`;
13248
14062
  knob.style.left = `${curAlpha * 100}%`;
13249
- pctBox.textContent = `${Math.round(curAlpha * 100)}%`;
14063
+ pctBox.value = `${Math.round(curAlpha * 100)}%`;
13250
14064
  };
14065
+ const commitTyped = () => {
14066
+ pctBox.style.borderColor = "var(--vela-border)";
14067
+ const n = parseFloat(pctBox.value.replace("%", ""));
14068
+ if (Number.isFinite(n)) {
14069
+ const next = Math.max(0, Math.min(100, n)) / 100;
14070
+ if (Math.round(next * 100) !== Math.round(curAlpha * 100)) {
14071
+ curAlpha = next;
14072
+ paintOpacity();
14073
+ emit();
14074
+ return;
14075
+ }
14076
+ }
14077
+ paintOpacity();
14078
+ };
14079
+ pctBox.addEventListener("change", commitTyped);
14080
+ pctBox.addEventListener("blur", () => {
14081
+ pctBox.style.borderColor = "var(--vela-border)";
14082
+ paintOpacity();
14083
+ });
14084
+ pctBox.addEventListener("keydown", (e) => {
14085
+ if (e.key === "Escape") return;
14086
+ e.stopPropagation();
14087
+ if (e.key === "Enter") {
14088
+ e.preventDefault();
14089
+ commitTyped();
14090
+ pctBox.blur();
14091
+ }
14092
+ });
13251
14093
  let dragging = false;
13252
14094
  const onDrag = (clientX) => {
13253
14095
  const r = track.getBoundingClientRect();
@@ -14471,10 +15313,16 @@ var IndicatorInputsDialog = class {
14471
15313
  this.calendarPop = null;
14472
15314
  this.calendarAnchor = null;
14473
15315
  },
14474
- content: (el) => fillCalendar(el, current, (iso) => {
14475
- pop.hide();
14476
- onPick(iso);
14477
- })
15316
+ content: (el) => {
15317
+ const picker = new DatePicker({
15318
+ value: current,
15319
+ onPick: (iso) => {
15320
+ pop.hide();
15321
+ onPick(iso);
15322
+ }
15323
+ });
15324
+ el.append(picker.el);
15325
+ }
14478
15326
  });
14479
15327
  this.calendarPop = pop;
14480
15328
  this.calendarAnchor = anchor;
@@ -14599,28 +15447,6 @@ function ensureDialogStyles() {
14599
15447
  .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
15448
  .vela-ind-combo-chevron:hover{opacity:0.9;}
14601
15449
  .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
15450
  ${overlayScrollbarCss(".vela-dialog.vela-ind-dialog *", 9)}
14625
15451
  .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
15452
  .vela-ind-tab:not(.vela-ind-tab-active):hover{color:var(--vela-fg-bright);}
@@ -14654,174 +15480,6 @@ var TIME_OPTIONS = Array.from({ length: 48 }, (_, i) => {
14654
15480
  const v = `${hh}:${mm}`;
14655
15481
  return { value: v, label: v };
14656
15482
  });
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
15483
  function normalizeTimeInput(raw) {
14826
15484
  const t = raw.trim();
14827
15485
  const m = /^(\d{1,2}):(\d{2})$/.exec(t) ?? /^(\d{2})(\d{2})$/.exec(t);
@@ -16199,6 +16857,22 @@ function priceStyleIds() {
16199
16857
  for (const t of chartTypes()) if (!out.includes(t.id)) out.push(t.id);
16200
16858
  return out;
16201
16859
  }
16860
+ function sanitizeCrosshairOverride(value) {
16861
+ if (!value || typeof value !== "object") return null;
16862
+ const v = value;
16863
+ const out = {};
16864
+ if (isBool(v.vertical)) out.vertical = v.vertical;
16865
+ if (isBool(v.horizontal)) out.horizontal = v.horizontal;
16866
+ if (isColor(v.color)) out.color = v.color;
16867
+ if (isNum(v.width)) out.width = Math.max(0.5, Math.min(8, v.width));
16868
+ if (isLineStyle(v.style)) out.style = v.style;
16869
+ if (isNum(v.opacity)) out.opacity = clampOpacity(v.opacity);
16870
+ const shade = v.shadeRight;
16871
+ if (shade && typeof shade === "object" && isColor(shade.color)) {
16872
+ out.shadeRight = isNum(shade.opacity) ? { color: shade.color, opacity: clampOpacity(shade.opacity) } : { color: shade.color };
16873
+ }
16874
+ return Object.keys(out).length > 0 ? out : null;
16875
+ }
16202
16876
  function isColor(v) {
16203
16877
  return typeof v === "string" && v.trim().length > 0;
16204
16878
  }
@@ -17860,7 +18534,7 @@ var Scheduler = class {
17860
18534
 
17861
18535
  // src/renderers/native/core/Animator.ts
17862
18536
  var Animator = class {
17863
- constructor(tick, raf, cancel, now) {
18537
+ constructor(tick, raf, cancel, now2) {
17864
18538
  this.tick = tick;
17865
18539
  this.handle = null;
17866
18540
  this.last = 0;
@@ -17874,7 +18548,7 @@ var Animator = class {
17874
18548
  };
17875
18549
  this.raf = raf ?? ((cb) => requestAnimationFrame(cb));
17876
18550
  this.cancel = cancel ?? ((h) => cancelAnimationFrame(h));
17877
- this.now = now ?? (() => performance.now());
18551
+ this.now = now2 ?? (() => performance.now());
17878
18552
  }
17879
18553
  get active() {
17880
18554
  return this.handle !== null;
@@ -18006,6 +18680,12 @@ var InputController = class {
18006
18680
  this.lastTapT = 0;
18007
18681
  this.lastTapX = 0;
18008
18682
  this.lastTapY = 0;
18683
+ // Whether the last two primary presses were claimed by the drawings layer. A double-click
18684
+ // is the last press plus the one before it; a tool-driven click pair must never fall through
18685
+ // to the pane maximize toggle, even when the tool disarmed itself between the two clicks
18686
+ // (a finished placement leaves plain cursor mode for the second click).
18687
+ this.lastPressDrawing = false;
18688
+ this.prevPressDrawing = false;
18009
18689
  this.onTouchContextMenu = (e) => {
18010
18690
  if (this.touches.size === 0) return;
18011
18691
  if (!(e.target instanceof Node) || !this.el?.contains(e.target)) return;
@@ -18060,19 +18740,24 @@ var InputController = class {
18060
18740
  this.moved = false;
18061
18741
  this.startX = x;
18062
18742
  this.startY = y;
18743
+ this.prevPressDrawing = this.lastPressDrawing;
18744
+ this.lastPressDrawing = false;
18063
18745
  if (this.deps.drawingsClaim?.(x, y)) {
18064
18746
  this.region = "drawing";
18747
+ this.lastPressDrawing = true;
18065
18748
  this.deps.drawingsPointerDown?.(x, y, this.snapMode(e), e.shiftKey, e.ctrlKey || e.metaKey);
18066
18749
  this.capture(e.pointerId);
18067
18750
  return;
18068
18751
  }
18069
18752
  if (e.shiftKey && this.regionAt(x, y) === "data" && this.deps.drawingsMeasureStart?.(x, y, this.snapMode(e))) {
18070
18753
  this.region = "drawing";
18754
+ this.lastPressDrawing = true;
18071
18755
  this.capture(e.pointerId);
18072
18756
  return;
18073
18757
  }
18074
18758
  if ((e.ctrlKey || e.metaKey) && this.regionAt(x, y) === "data" && this.deps.drawingsMarqueeStart?.(x, y)) {
18075
18759
  this.region = "drawing";
18760
+ this.lastPressDrawing = true;
18076
18761
  this.capture(e.pointerId);
18077
18762
  return;
18078
18763
  }
@@ -18394,6 +19079,7 @@ var InputController = class {
18394
19079
  if (region === "price") this.deps.resetPriceScale(x, y);
18395
19080
  else if (region === "separator") this.deps.resetPaneSize(y);
18396
19081
  else if (region === "time") this.deps.resetView();
19082
+ else if (this.lastPressDrawing || this.prevPressDrawing) return;
18397
19083
  else this.deps.dataDblClick(x, y);
18398
19084
  }
18399
19085
  };
@@ -18673,6 +19359,8 @@ var SceneGraph = class {
18673
19359
  /** VPVR-layer config pushed by the VPVR native indicator (null ⇒ layer off). Ephemeral. */
18674
19360
  this.vpvrLayer = null;
18675
19361
  this.crosshair = null;
19362
+ /** Runtime override of the crosshair's lines and style (the `crosshairOverride` feature). Ephemeral. */
19363
+ this.crosshairOverride = null;
18676
19364
  /** How the base price series is drawn on the price pane (candles by default). */
18677
19365
  this.priceStyle = "candles";
18678
19366
  /** Price-series base painting for the ACTIVE style (see ChartTypeDefinition.basePainting). */
@@ -20557,6 +21245,7 @@ function timeTicks(fromMs, toMs, target = 8, offsetMs = 0) {
20557
21245
  const step = pickStep(span / Math.max(1, target));
20558
21246
  const zFrom = fromMs + offsetMs;
20559
21247
  const zTo = toMs + offsetMs;
21248
+ if (step >= MONTH) return calendarTicks(zFrom, zTo, step >= YEAR ? 12 : step >= 3 * MONTH ? 3 : 1, offsetMs);
20560
21249
  const first = Math.ceil(zFrom / step) * step;
20561
21250
  const out = [];
20562
21251
  for (let zt = first; zt <= zTo; zt += step) {
@@ -20587,11 +21276,23 @@ function timeTicks(fromMs, toMs, target = 8, offsetMs = 0) {
20587
21276
  }
20588
21277
  return out;
20589
21278
  }
21279
+ function calendarTicks(zFrom, zTo, months, offsetMs) {
21280
+ const d0 = new Date(zFrom);
21281
+ let idx = d0.getUTCFullYear() * 12 + d0.getUTCMonth();
21282
+ if (Date.UTC(Math.floor(idx / 12), idx % 12, 1) < zFrom) idx += 1;
21283
+ idx = Math.ceil(idx / months) * months;
21284
+ const out = [];
21285
+ for (let zt = Date.UTC(Math.floor(idx / 12), idx % 12, 1); zt <= zTo; idx += months, zt = Date.UTC(Math.floor(idx / 12), idx % 12, 1)) {
21286
+ const label = months === 12 ? String(Math.floor(idx / 12)) : MONTHS[idx % 12];
21287
+ out.push({ time: zt - offsetMs, label, major: true });
21288
+ }
21289
+ return out;
21290
+ }
20590
21291
 
20591
21292
  // src/renderers/native/chrome/countdown.ts
20592
- function countdownText(barOpen, barMs, now) {
21293
+ function countdownText(barOpen, barMs, now2) {
20593
21294
  if (!(barMs > 0)) return null;
20594
- const remaining = barOpen + barMs - now;
21295
+ const remaining = barOpen + barMs - now2;
20595
21296
  if (remaining <= 0) return null;
20596
21297
  return formatCountdown(remaining);
20597
21298
  }
@@ -21403,19 +22104,35 @@ var CrosshairRenderer = class {
21403
22104
  const dataH = coords.height;
21404
22105
  if (!ch || ch.x < 0 || ch.x > dataW || ch.y < 0 || ch.y > dataH) return;
21405
22106
  const cs = scene.style.crosshair;
22107
+ const ov = scene.crosshairOverride;
22108
+ const vertical = ov?.vertical !== false;
22109
+ const horizontal = ov?.horizontal !== false;
21406
22110
  ctx.font = `${scene.style.fontSize}px ${theme.fontFamily}`;
21407
22111
  ctx.textBaseline = "middle";
21408
22112
  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);
22113
+ const x = crisp(coords.logicalToX(logical), ov?.width ?? cs.width);
22114
+ if (vertical && ov?.shadeRight) {
22115
+ const from = Math.max(0, Math.round(coords.logicalToX(logical + 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
+ ctx.strokeStyle = ov?.color ?? cs.color ?? theme.textColor;
22124
+ ctx.lineWidth = ov?.width ?? cs.width;
22125
+ ctx.globalAlpha = ov?.opacity ?? cs.opacity;
22126
+ setDash3(ctx, ov?.style ?? cs.style);
21414
22127
  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);
22128
+ if (vertical) {
22129
+ ctx.moveTo(x, 0);
22130
+ ctx.lineTo(x, dataH);
22131
+ }
22132
+ if (horizontal) {
22133
+ ctx.moveTo(0, Math.round(ch.y) + 0.5);
22134
+ ctx.lineTo(dataW, Math.round(ch.y) + 0.5);
22135
+ }
21419
22136
  ctx.stroke();
21420
22137
  ctx.setLineDash([]);
21421
22138
  ctx.lineWidth = 1;
@@ -21428,11 +22145,11 @@ var CrosshairRenderer = class {
21428
22145
  }
21429
22146
  }
21430
22147
  const chipBg = cs.labelBackground ?? theme.borderColor;
21431
- if (pane && pane.axisFormat !== "none") {
22148
+ if (horizontal && pane && pane.axisFormat !== "none") {
21432
22149
  const price = coords.yToPrice(ch.y, pane.scale, pane.bounds);
21433
22150
  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
22151
  }
21435
- this.chip(ctx, x, dataH + 1, formatTimeStamp(coords.logicalToTime(logical), scene.timezone, coords.barInterval), chipBg, "center", true, theme.background);
22152
+ if (vertical) this.chip(ctx, x, dataH + 1, formatTimeStamp(coords.logicalToTime(logical), scene.timezone, coords.barInterval), chipBg, "center", true, theme.background);
21436
22153
  }
21437
22154
  destroy() {
21438
22155
  this.canvas = null;
@@ -21443,23 +22160,37 @@ var CrosshairRenderer = class {
21443
22160
  * along), with that bar's time chip in this chart's own timezone and — when the
21444
22161
  * level resolved — the price chip on the right axis. The snap happened upstream
21445
22162
  * (`externalCrossPx`, floor-to-containing-bar) — this method only draws. Chips
21446
- * render slightly dimmed so the ghost still reads as foreign. */
22163
+ * render slightly dimmed so the ghost still reads as foreign. A `crosshairOverride`
22164
+ * applies to the ghost as well — its line style, a dropped horizontal line, and the
22165
+ * `shadeRight` veil from the ghost bar's right edge — so a pick that spans several
22166
+ * charts reads the same on every one of them. */
21447
22167
  drawExternal(ctx, ext, scene, coords, theme) {
21448
22168
  const cs = scene.style.crosshair;
22169
+ const ov = scene.crosshairOverride;
21449
22170
  const dataW = coords.width;
21450
22171
  const dataH = coords.height;
21451
- const x = Math.round(ext.x) + 0.5;
21452
- if (x < 0 || x > dataW) return;
22172
+ const x = crisp(ext.x, ov?.width ?? cs.width);
22173
+ if (ov?.vertical === false) return;
22174
+ if (ov?.shadeRight) {
22175
+ const from = Math.max(0, Math.round(coords.logicalToX(Math.round(coords.xToLogical(ext.x)) + 0.5)));
22176
+ if (from < dataW) {
22177
+ ctx.fillStyle = ov.shadeRight.color;
22178
+ ctx.globalAlpha = ov.shadeRight.opacity ?? 1;
22179
+ ctx.fillRect(from, 0, dataW - from, dataH);
22180
+ ctx.globalAlpha = 1;
22181
+ }
22182
+ }
22183
+ if (ext.line === false || x < 0 || x > dataW) return;
21453
22184
  ctx.font = `${scene.style.fontSize}px ${theme.fontFamily}`;
21454
22185
  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);
22186
+ ctx.strokeStyle = ov?.color ?? cs.color ?? theme.textColor;
22187
+ ctx.lineWidth = ov?.width ?? cs.width;
22188
+ ctx.globalAlpha = (ov?.opacity ?? cs.opacity) * 0.55;
22189
+ setDash3(ctx, ov?.style ?? cs.style);
21459
22190
  ctx.beginPath();
21460
22191
  ctx.moveTo(x, 0);
21461
22192
  ctx.lineTo(x, dataH);
21462
- if (ext.y != null) {
22193
+ if (ext.y != null && ov?.horizontal !== false) {
21463
22194
  ctx.moveTo(0, Math.round(ext.y) + 0.5);
21464
22195
  ctx.lineTo(dataW, Math.round(ext.y) + 0.5);
21465
22196
  }
@@ -21468,7 +22199,7 @@ var CrosshairRenderer = class {
21468
22199
  ctx.lineWidth = 1;
21469
22200
  ctx.globalAlpha = 0.8;
21470
22201
  const chipBg = cs.labelBackground ?? theme.borderColor;
21471
- if (ext.y != null && ext.price != null) {
22202
+ if (ext.y != null && ext.price != null && ov?.horizontal !== false) {
21472
22203
  let pane;
21473
22204
  for (const p of scene.panes.values()) {
21474
22205
  if (ext.y >= p.bounds.top && ext.y <= p.bounds.top + p.bounds.height) {
@@ -21508,6 +22239,9 @@ var CrosshairRenderer = class {
21508
22239
  ctx.textAlign = "start";
21509
22240
  }
21510
22241
  };
22242
+ function crisp(px, width) {
22243
+ return Math.round(px) + (Math.round(width) % 2 === 1 ? 0.5 : 0);
22244
+ }
21511
22245
  function setDash3(ctx, style) {
21512
22246
  if (style === "dashed") ctx.setLineDash([6, 4]);
21513
22247
  else if (style === "dotted") ctx.setLineDash([2, 3]);
@@ -29099,23 +29833,21 @@ function stackLayers(entries, candleZ) {
29099
29833
  return { below, above };
29100
29834
  }
29101
29835
 
29102
- // src/renderers/native/chrome/luxalgo-logos.ts
29103
- var LUXALGO_SYMBOL_SVG = '<svg viewBox="0 0 45 40" aria-hidden="true"><g fill="currentColor"><path d="m40.25 38 4.58-7.998L28.802 2l-16.03 28 9.16-.001 6.87-12z"/><path d="M34.525 32.002 9.33 31.997 27.655 0h-9.158L.18 31.993 4.759 40h34.347z"/></g></svg>';
29104
- var LUXALGO_WORDMARK_SVG = '<svg viewBox="0 0 964 252" aria-hidden="true"><path fill="currentColor" fill-rule="evenodd" d="M 614 94.500 L 614 189 633.500 189 L 653 189 653 94.500 L 653 0 633.500 0 L 614 0 614 94.500 M 0 97.500 L 0 189 63.023 189 L 126.045 189 125.773 170.750 L 125.500 152.500 84.250 152.239 L 43 151.978 43 78.989 L 43 6 21.500 6 L 0 6 0 97.500 M 450.387 96.451 C 421.871 146.253, 398.306 186.994, 398.020 186.987 C 397.423 186.972, 362 124.724, 362 123.691 C 362 123.316, 370.100 108.818, 380 91.472 C 389.900 74.127, 398 59.500, 398 58.968 C 398 58.380, 391.007 58, 380.210 58 L 362.420 58 348.060 83.138 C 334.400 107.052, 333.637 108.160, 332.399 105.888 C 331.682 104.575, 325.287 93.375, 318.187 81 L 305.277 58.500 287.139 58.227 C 277.162 58.078, 269 58.186, 269 58.469 C 269 58.751, 277.154 73.274, 287.120 90.741 C 297.086 108.209, 305.261 122.983, 305.287 123.574 C 305.312 124.164, 297.318 138.789, 287.522 156.074 C 277.725 173.358, 269.524 187.838, 269.296 188.250 C 269.068 188.662, 276.977 189, 286.870 189 L 304.859 189 319.163 163.959 C 331.321 142.674, 333.647 139.165, 334.670 140.563 C 335.331 141.468, 341.864 152.737, 349.186 165.606 L 362.500 189.003 402.442 188.752 L 442.384 188.500 472.080 136.695 C 488.413 108.203, 501.939 84.729, 502.138 84.531 C 502.337 84.333, 516 107.757, 532.500 136.583 L 562.500 188.994 584.750 188.997 C 596.987 188.999, 607 188.854, 607 188.675 C 607 188.319, 503.014 6.686, 502.532 6.201 C 502.369 6.037, 478.904 46.649, 450.387 96.451 M 717.500 55.017 C 697.175 57.389, 680.641 68.752, 672.036 86.264 C 665.866 98.818, 663.840 108.024, 663.840 123.500 C 663.840 133.409, 664.328 138.355, 665.892 144.300 C 672.076 167.810, 689.191 184.757, 711.529 189.489 C 730.549 193.518, 747.264 189.540, 761.282 177.648 C 764.944 174.542, 768.178 172, 768.470 172 C 769.345 172, 769.030 185.018, 767.979 192.285 C 765.618 208.613, 755.148 218.672, 738.473 220.632 C 732.586 221.324, 723.136 219.599, 717.709 216.843 C 712.693 214.295, 706.636 207.946, 703.972 202.443 C 702.789 199.999, 701.749 198, 701.661 198.001 C 700.201 198.007, 665.596 205.071, 665.230 205.437 C 664.326 206.341, 669.156 218.313, 672.607 223.721 C 682.146 238.672, 700.008 248.257, 723.500 251.033 C 758.132 255.124, 788.930 241.875, 800.287 218 C 806.923 204.047, 807 203.001, 807 126.542 L 807 58 788.500 58 L 770 58 770 66.500 C 770 71.175, 769.641 75, 769.203 75 C 768.765 75, 766.627 73.142, 764.453 70.872 C 752.805 58.708, 735.614 52.903, 717.500 55.017 M 878.500 55.031 C 850.107 59.412, 830.755 75.064, 822.285 100.500 C 819.905 107.644, 819.614 110.051, 819.565 123 C 819.501 139.581, 820.786 145.825, 826.763 158 C 832.104 168.880, 842.842 179.424, 854.077 184.822 C 867.651 191.343, 873.132 192.462, 891.500 192.462 C 909.868 192.462, 915.349 191.343, 928.923 184.822 C 944.653 177.265, 956.667 162.306, 961.710 144 C 963.074 139.048, 963.481 134.047, 963.418 123 C 963.346 110.140, 963.038 107.595, 960.694 100.500 C 953.325 78.193, 936.695 62.854, 913.500 56.969 C 904.262 54.625, 887.240 53.682, 878.500 55.031 M 134.015 102.750 C 134.027 139.250, 134.310 148.689, 135.549 153.947 C 141.139 177.663, 159.815 193, 183.105 193 C 196.453 193, 210.163 186.171, 217.936 175.650 C 219.826 173.093, 221.738 171, 222.186 171 C 222.634 171, 223 175.050, 223 180 L 223 189 242.507 189 L 262.014 189 261.757 123.750 L 261.500 58.500 242.500 58.500 L 223.500 58.500 223 99.500 C 222.513 139.406, 222.441 140.610, 220.289 144.634 C 209.729 164.377, 183.014 164.880, 174.299 145.500 C 172.677 141.895, 172.469 137.382, 172.190 99.750 L 171.880 58 152.940 58 L 134 58 134.015 102.750 M 729.932 87.998 C 719.311 89.821, 710.155 97.164, 705.141 107.880 C 702.851 112.772, 702.510 114.798, 702.505 123.521 C 702.500 132.834, 702.732 134.014, 705.787 140.218 C 709.761 148.290, 715.989 154.097, 723.853 157.063 C 731.409 159.912, 744.359 159.466, 750.795 156.136 C 767.289 147.601, 774.103 127.713, 767.019 108.780 C 761.433 93.851, 746.051 85.232, 729.932 87.998 M 883.972 88.348 C 865.749 92.310, 854.873 109.966, 858.119 130.316 C 860.933 147.963, 872.346 158.830, 889.084 159.799 C 900.714 160.472, 906.867 158.234, 914.650 150.497 C 919.856 145.323, 921.123 143.340, 922.900 137.587 C 925.412 129.456, 925.655 118.570, 923.489 111.275 C 918.479 94.405, 901.478 84.541, 883.972 88.348"/></svg>';
29836
+ // src/renderers/native/chrome/vela-logos.ts
29837
+ var VELA_MARK_SVG = '<svg viewBox="0 -142 161.66 148" aria-hidden="true"><g fill="currentColor"><path d="M0 -140L32.91 -140L77.52 -62.73L61.06 -34.24Z"/><path d="M161.66 -140L128.75 -140L84.14 -62.73L100.59 -34.24Z"/><path d="M80.83 -52.04L95.85 -26.02L80.83 0L65.81 -26.02Z"/></g></svg>';
29838
+ var VELA_LETTERS_SVG = '<svg viewBox="161.66 -142 310.57 148" aria-hidden="true"><g fill="currentColor"><path d="M226.3 1.17Q200.81 1.17 186.12 -12.99Q171.42 -27.15 171.42 -51.17Q171.42 -75.2 185.87 -89.6Q200.33 -104 224.25 -104Q247.4 -104 261.7 -90.58Q276.01 -77.15 276.3 -54.59Q276.3 -49.9 275.42 -45.02H200.81V-43.55Q201.2 -32.62 207.75 -26.42Q214.29 -20.21 225.03 -20.21Q233.72 -20.21 239.34 -23.93Q244.95 -27.64 246.81 -34.77H274.64Q272.1 -19.14 259.16 -8.98Q246.22 1.17 226.3 1.17ZM201.5 -63.38H247.88Q246.42 -72.75 240.27 -77.88Q234.12 -83.01 224.45 -83.01Q215.17 -83.01 208.92 -77.73Q202.67 -72.46 201.5 -63.38ZM287.54 0V-140.04H315.76V0ZM429.34 -23.83H432.56V0H418.11Q407.76 0 403.36 -4.44Q398.97 -8.89 399.16 -16.8Q387.83 1.17 365.57 1.17Q348.48 1.17 337.88 -6.74Q327.29 -14.65 327.29 -29Q327.29 -45.41 339.45 -54.3Q351.6 -63.18 374.55 -63.18H394.96V-68.16Q394.96 -75.2 390.08 -79.3Q385.2 -83.4 376.51 -83.4Q368.3 -83.4 363.03 -79.98Q357.76 -76.56 356.88 -70.8H329.73Q331.19 -85.84 344.08 -94.92Q356.97 -104 377.29 -104Q399.06 -104 410.88 -94.43Q422.7 -84.86 422.7 -66.6V-30.18Q422.7 -26.37 424.26 -25.1Q425.82 -23.83 429.34 -23.83ZM394.96 -42.38V-44.43H374.36Q365.47 -44.43 360.59 -40.92Q355.7 -37.4 355.7 -30.96Q355.7 -25.59 359.81 -22.41Q363.91 -19.24 370.94 -19.24Q381.97 -19.24 388.37 -25.54Q394.77 -31.84 394.96 -42.38Z"/><path fill-opacity="0.55" d="M437.44 -124.28H441.96V-113.41H445.34V-124.28H449.87V-127.4H437.44ZM451.43 -113.41H454.7V-123.29L458.97 -113.41H461.62L465.99 -123.29V-113.41H469.21V-127.4H464.53L460.42 -117.68L456.16 -127.4H451.43Z"/></g></svg>';
29105
29839
 
29106
29840
  // src/renderers/native/chrome/AttributionMark.ts
29107
- var ATTRIBUTION_URL = "https://luxalgo.com/vela";
29841
+ var ATTRIBUTION_URL = "https://velacharts.dev/?utm_source=vela&utm_medium=attribution";
29108
29842
  var STYLE_ID5 = "vela-attribution-styles";
29109
29843
  var CSS2 = `
29110
29844
  .vela-attribution {
29111
29845
  text-decoration: none;
29112
29846
  display: flex;
29113
29847
  align-items: center;
29114
- /* Gutter, wordmark size and offset below are the brand lockup's own ratios,
29115
- measured off the official horizontal logo: gutter 0.157, wordmark height
29116
- 0.966, and wordmark 0.109 LOWER than the symbol \u2014 box-centering the two
29117
- reads wrong because the symbol's ink hangs low and the wordmark descends. */
29118
- gap: 4px;
29848
+ /* No gutter: the V is the wordmark's first letter, and the letters' viewBox starts
29849
+ where the V's ends, so the kit's own letter spacing comes with them. */
29850
+ gap: 0;
29119
29851
  }
29120
29852
  .vela-attribution .vela-attr-symbol {
29121
29853
  flex: none;
@@ -29123,7 +29855,7 @@ var CSS2 = `
29123
29855
  line-height: 0;
29124
29856
  }
29125
29857
  .vela-attribution .vela-attr-symbol svg {
29126
- height: 28px;
29858
+ height: 24px;
29127
29859
  width: auto;
29128
29860
  display: block;
29129
29861
  filter: drop-shadow(0 1px 2px var(--vela-attr-shadow, rgba(0,0,0,0.45)));
@@ -29134,15 +29866,13 @@ var CSS2 = `
29134
29866
  opacity: 0;
29135
29867
  display: flex;
29136
29868
  align-items: center;
29137
- /* Offset here, not on the image: the clip box must not crop the descender. */
29138
- position: relative;
29139
- top: 3px;
29869
+ /* Same height and baseline as the V (one viewBox height), so no vertical offset. */
29140
29870
  transform: translateX(-8px);
29141
29871
  transition: max-width 0.3s ease, opacity 0.25s ease, transform 0.3s ease;
29142
29872
  flex: none;
29143
29873
  }
29144
29874
  .vela-attribution .vela-attr-wordmark svg {
29145
- height: 27px;
29875
+ height: 24px;
29146
29876
  width: auto;
29147
29877
  display: block;
29148
29878
  filter: drop-shadow(0 1px 2px var(--vela-attr-shadow, rgba(0,0,0,0.45)));
@@ -29157,14 +29887,11 @@ var CSS2 = `
29157
29887
  (data-layout): the workspace's single grid-wide mark lives OUTSIDE any renderer
29158
29888
  container, so only the shell attribute reaches it. */
29159
29889
  [data-vela-layout='mobile'] .vela-attribution .vela-attr-symbol svg,
29160
- [data-layout='mobile'] .vela-attribution .vela-attr-symbol svg { height: 22px; }
29890
+ [data-layout='mobile'] .vela-attribution .vela-attr-symbol svg { height: 20px; }
29161
29891
  [data-vela-layout='mobile'] .vela-attribution .vela-attr-wordmark,
29162
- [data-layout='mobile'] .vela-attribution .vela-attr-wordmark {
29163
- top: 2px;
29164
- transform: translateX(-6px);
29165
- }
29892
+ [data-layout='mobile'] .vela-attribution .vela-attr-wordmark { transform: translateX(-6px); }
29166
29893
  [data-vela-layout='mobile'] .vela-attribution .vela-attr-wordmark svg,
29167
- [data-layout='mobile'] .vela-attribution .vela-attr-wordmark svg { height: 21px; }
29894
+ [data-layout='mobile'] .vela-attribution .vela-attr-wordmark svg { height: 20px; }
29168
29895
  [data-vela-layout='mobile'] .vela-attribution:hover .vela-attr-wordmark,
29169
29896
  [data-layout='mobile'] .vela-attribution:hover .vela-attr-wordmark { transform: translateX(0); }
29170
29897
  `;
@@ -29221,10 +29948,10 @@ function createAttributionMark(doc, background) {
29221
29948
  const symbol = doc.createElement("span");
29222
29949
  symbol.className = "vela-attr-symbol";
29223
29950
  symbol.setAttribute("aria-hidden", "true");
29224
- symbol.innerHTML = LUXALGO_SYMBOL_SVG;
29951
+ symbol.innerHTML = VELA_MARK_SVG;
29225
29952
  const wordmark = doc.createElement("span");
29226
29953
  wordmark.className = "vela-attr-wordmark";
29227
- wordmark.innerHTML = LUXALGO_WORDMARK_SVG;
29954
+ wordmark.innerHTML = VELA_LETTERS_SVG;
29228
29955
  a.append(symbol, wordmark);
29229
29956
  return a;
29230
29957
  }
@@ -29804,7 +30531,7 @@ var NativeRenderer = class {
29804
30531
  this.moveIndicatorCbs = /* @__PURE__ */ new Set();
29805
30532
  this.priceStyleCbs = /* @__PURE__ */ new Set();
29806
30533
  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"];
30534
+ 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
30535
  /** Track cursor proximity to the scroll button on the plot (bubbles from the button too,
29809
30536
  * so moving onto the button doesn't count as leaving). */
29810
30537
  this.onScrollProximityMove = (e) => {
@@ -29983,6 +30710,9 @@ var NativeRenderer = class {
29983
30710
  this.scene.marks = mergeMarksState(this.scene.marks, value);
29984
30711
  this.markPopover?.close();
29985
30712
  break;
30713
+ case "crosshairOverride":
30714
+ this.scene.crosshairOverride = sanitizeCrosshairOverride(value);
30715
+ break;
29986
30716
  case "keyboard":
29987
30717
  this.setKeyboardEnabled(Boolean(value));
29988
30718
  return;
@@ -30096,6 +30826,8 @@ var NativeRenderer = class {
30096
30826
  return { ...this.scene.tradeMarkers, colors: { ...this.scene.tradeMarkers.colors } };
30097
30827
  case "marks":
30098
30828
  return { visible: this.scene.marks.visible, groups: { ...this.scene.marks.groups } };
30829
+ case "crosshairOverride":
30830
+ return this.scene.crosshairOverride ? { ...this.scene.crosshairOverride } : null;
30099
30831
  case "keyboard":
30100
30832
  return this.keyboardEnabled;
30101
30833
  case "historyChords":
@@ -30674,8 +31406,8 @@ var NativeRenderer = class {
30674
31406
  for (const pane of this.scene.panes.values()) pane.scale = { ...pane.scaleTarget };
30675
31407
  this.modelAlpha = 0;
30676
31408
  const start = performance.now();
30677
- const step = (now) => {
30678
- const p = Math.min(1, (now - start) / duration);
31409
+ const step = (now2) => {
31410
+ const p = Math.min(1, (now2 - start) / duration);
30679
31411
  this.scene.bars = p >= 1 ? real : real.map((b, i) => this.revealCandle(b, i, p, n, style));
30680
31412
  this.paintData();
30681
31413
  if (p < 1) {
@@ -30691,8 +31423,8 @@ var NativeRenderer = class {
30691
31423
  fadeInModels() {
30692
31424
  const fade = Math.min(INTRO_MODEL_FADE_MS, this.intro.duration || INTRO_MODEL_FADE_MS);
30693
31425
  const start = performance.now();
30694
- const step = (now) => {
30695
- this.modelAlpha = Math.min(1, (now - start) / fade);
31426
+ const step = (now2) => {
31427
+ this.modelAlpha = Math.min(1, (now2 - start) / fade);
30696
31428
  this.paintData();
30697
31429
  if (this.modelAlpha < 1) {
30698
31430
  this.introRaf = requestAnimationFrame(step);
@@ -31215,10 +31947,13 @@ var NativeRenderer = class {
31215
31947
  this.animator.start();
31216
31948
  }
31217
31949
  } else if (!last || bar.time > last.time) {
31950
+ const vp = this.coords.getViewport();
31951
+ const holdView = n > 0 && vp.rightOffset < 0 && this.scrollTargetRO === null;
31218
31952
  this.bars.push(bar);
31219
31953
  this.syncLiveEase(bar);
31220
31954
  if (this.coords.barInterval > 0) this.coords.appendBar(bar.time);
31221
31955
  else this.coords.setBars(this.bars.map((b) => b.time));
31956
+ if (holdView) this.coords.setViewport({ ...vp, rightOffset: vp.rightOffset - 1 });
31222
31957
  } else {
31223
31958
  return;
31224
31959
  }
@@ -31900,10 +32635,10 @@ var NativeRenderer = class {
31900
32635
  if (this.markPulseRaf !== null || typeof requestAnimationFrame !== "function") return;
31901
32636
  const tick = () => {
31902
32637
  this.markPulseRaf = null;
31903
- const now = frameNow();
31904
- if (this.scene.marksFlash && this.scene.marksFlash.until <= now) this.scene.marksFlash = null;
32638
+ const now2 = frameNow();
32639
+ if (this.scene.marksFlash && this.scene.marksFlash.until <= now2) this.scene.marksFlash = null;
31905
32640
  this.scheduler?.invalidate(2 /* Chrome */);
31906
- const pulsing = this.scene.marksHoverKey !== null && now - this.scene.marksHoverSince < MARK_PULSE_MS;
32641
+ const pulsing = this.scene.marksHoverKey !== null && now2 - this.scene.marksHoverSince < MARK_PULSE_MS;
31907
32642
  if (pulsing || this.scene.marksFlash !== null) this.markPulseRaf = requestAnimationFrame(tick);
31908
32643
  };
31909
32644
  this.markPulseRaf = requestAnimationFrame(tick);
@@ -32464,9 +33199,10 @@ var NativeRenderer = class {
32464
33199
  const ext = this.externalCross;
32465
33200
  if (!ext || this.coords.barCount === 0) return null;
32466
33201
  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;
33202
+ if (!(logical < this.coords.barCount)) return null;
33203
+ const x = this.coords.logicalToX(Math.max(logical, -1));
33204
+ if (!Number.isFinite(x)) return null;
33205
+ const line = logical >= 0 && x >= 0 && x <= this.coords.width;
32470
33206
  let y = null;
32471
33207
  if (ext.price != null) {
32472
33208
  const pricePane = this.scene.orderedPanes().find((p) => p.kind === "price");
@@ -32475,7 +33211,7 @@ var NativeRenderer = class {
32475
33211
  if (!Number.isFinite(y) || y < pricePane.bounds.top || y > pricePane.bounds.top + pricePane.bounds.height) y = null;
32476
33212
  }
32477
33213
  }
32478
- return { x, y, time: this.coords.logicalToTime(logical), price: y != null ? ext.price : null };
33214
+ return { x, y, time: this.coords.logicalToTime(Math.max(logical, 0)), price: y != null ? ext.price : null, line };
32479
33215
  }
32480
33216
  /** Set the sticky magnet mode (called by the drawings toolbar's 3-state button). */
32481
33217
  setSnapMode(mode) {
@@ -37180,6 +37916,7 @@ var Vela = class {
37180
37916
  this.panesControl = new PanesControl(this.orchestrator);
37181
37917
  this.drawingsControl = new DrawingsControl(this.orchestrator.drawings);
37182
37918
  this.marksControl = new MarksControl(this.orchestrator.marks);
37919
+ this.replayControl = new ReplayControl(this.orchestrator);
37183
37920
  }
37184
37921
  /**
37185
37922
  * Register a scripting engine so `addIndicator({ language })` can run that
@@ -37419,6 +38156,16 @@ var Vela = class {
37419
38156
  get marks() {
37420
38157
  return this.marksControl;
37421
38158
  }
38159
+ /**
38160
+ * The chart's bar-replay control surface: rewind to a past bar and reveal the
38161
+ * following ones by hand or on a timer, from the history already loaded —
38162
+ * `await chart.replay.start({ from })`, `chart.replay.play(2000)`, `chart.replay.step()`,
38163
+ * `chart.replay.stop()`. Revealed bars behave like live bars for indicators; live
38164
+ * updates pause meanwhile. Follow it with the `replay:*` events.
38165
+ */
38166
+ get replay() {
38167
+ return this.replayControl;
38168
+ }
37422
38169
  on(event, handler) {
37423
38170
  return this.orchestrator.events.on(event, handler);
37424
38171
  }
@@ -37477,6 +38224,28 @@ function resolveElement(container) {
37477
38224
  return element;
37478
38225
  }
37479
38226
 
38227
+ // src/core/replayTicks.ts
38228
+ function lowerTimeframeTicks(chart, timeframe) {
38229
+ return async (bar, { end, signal }) => {
38230
+ const { symbol, session } = chart.market;
38231
+ const resolved = symbol ? chart.data.resolve(symbol) : null;
38232
+ const provider = resolved ? chart.data.providerInstance(resolved.provider) : void 0;
38233
+ if (!resolved || !provider) return [];
38234
+ const bars = await provider.getBars(resolved.ticker, timeframe, { from: bar.time, to: end - 1, ...session ? { session } : {} });
38235
+ if (signal.aborted) return [];
38236
+ return barsToTicks(bars.filter((b) => b.time >= bar.time && b.time < end));
38237
+ };
38238
+ }
38239
+ function barsToTicks(bars) {
38240
+ return bars.map((b) => ({
38241
+ price: b.close,
38242
+ open: b.open,
38243
+ high: b.high,
38244
+ low: b.low,
38245
+ ...b.volume === void 0 ? {} : { volume: b.volume }
38246
+ }));
38247
+ }
38248
+
37480
38249
  // src/widget/topbar-composition.ts
37481
38250
  var TOPBAR_BUILTIN_IDS = ["symbol", "timeframes", "style", "layout", "indicators", "actions", "undo-redo", "alerts", "panels", "screenshot"];
37482
38251
 
@@ -37637,6 +38406,7 @@ exports.NEUTRAL = NEUTRAL;
37637
38406
  exports.NativeRenderer = NativeRenderer;
37638
38407
  exports.OVERRIDABLE_TOPBAR_IDS = OVERRIDABLE_TOPBAR_IDS;
37639
38408
  exports.RendererControl = RendererControl;
38409
+ exports.ReplayControl = ReplayControl;
37640
38410
  exports.SERIES_LINE = SERIES_LINE;
37641
38411
  exports.SESSION_OFF = SESSION_OFF;
37642
38412
  exports.SESSION_POST = SESSION_POST;
@@ -37651,6 +38421,7 @@ exports.TypedEventBus = TypedEventBus;
37651
38421
  exports.VALID = VALID;
37652
38422
  exports.Vela = Vela;
37653
38423
  exports.WARNING = WARNING;
38424
+ exports.barsToTicks = barsToTicks;
37654
38425
  exports.buildToolbar = buildToolbar;
37655
38426
  exports.categoricalColor = categoricalColor;
37656
38427
  exports.chartType = chartType;
@@ -37667,6 +38438,7 @@ exports.inputDeltas = inputDeltas;
37667
38438
  exports.inputVisible = inputVisible;
37668
38439
  exports.legendActions = legendActions;
37669
38440
  exports.legendCallouts = legendCallouts;
38441
+ exports.lowerTimeframeTicks = lowerTimeframeTicks;
37670
38442
  exports.nativeIndicatorDescriptors = nativeIndicatorDescriptors;
37671
38443
  exports.nativeIndicatorTypes = nativeIndicatorTypes;
37672
38444
  exports.normalizeSettingsRow = normalizeSettingsRow;