@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
@@ -591,6 +591,7 @@ var Vela = (function (exports) {
591
591
  registerIcon("chevron-up", S('<path d="M3.5 10 8 5.5 12.5 10"/>'));
592
592
  registerIcon("chevrons-right", S('<path d="m4 3.5 4.5 4.5L4 12.5"/><path d="m8.5 3.5 4.5 4.5-4.5 4.5"/>'));
593
593
  registerIcon("chevrons-left", S('<path d="M12 3.5 7.5 8l4.5 4.5"/><path d="M7.5 3.5 3 8l4.5 4.5"/>'));
594
+ registerIcon("replay", S('<path d="M14 4.2v7.6L8.6 8z"/><path d="M7.4 4.2v7.6L2 8z"/>'));
594
595
  registerIcon("check", S('<path d="m2.8 8.4 3.4 3.4 7-7.6"/>'));
595
596
  registerIcon("close", S('<path d="m3.8 3.8 8.4 8.4M12.2 3.8l-8.4 8.4"/>'));
596
597
  registerIcon("pin", S('<path d="M5.6 2.5h4.8M6.4 2.5v3.6L4.4 8.4v.9h7.2v-.9L9.6 6.1V2.5M8 9.3v4.2"/>'));
@@ -7808,6 +7809,7 @@ var Vela = (function (exports) {
7808
7809
  var FIRST_PAINT_BARS = 20;
7809
7810
  var GAP_FACTOR = 1.5;
7810
7811
  var HEAL_COOLDOWN_MS = 5e3;
7812
+ var MIN_TICK_UPDATE_MS = 16;
7811
7813
  var EngineOrchestrator = class _EngineOrchestrator {
7812
7814
  constructor(container, renderer, feed, engines, config, dataControl) {
7813
7815
  this.renderer = renderer;
@@ -7860,6 +7862,32 @@ var Vela = (function (exports) {
7860
7862
  this.lastHealAt = 0;
7861
7863
  /** The `visibilitychange` catch-up listener (removed on destroy). */
7862
7864
  this.onVisible = null;
7865
+ // ── bar replay: `rawBars` holds the history up to the cursor, the rest waits here ──
7866
+ /** Bars still hidden right of the replay cursor, oldest first; null ≡ replay off. */
7867
+ this.replayQueue = null;
7868
+ this.replayTimer = null;
7869
+ this.replayPlaying = false;
7870
+ this.replayIntervalMs = 1e3;
7871
+ /** The depth (`market.bars`) before a replay deepened the history — restored when it ends. */
7872
+ this.replayDepth = null;
7873
+ /** A start is loading older history before it can cut; `seek` = a replay was running
7874
+ * (and left silently to deepen), so abandoning the load must announce its end. */
7875
+ this.replayLoading = null;
7876
+ /** A replay carried over a timeframe/session switch, between the old series' silent end
7877
+ * and the cut of the new one: still ACTIVE for the outside (`replayState`), resumed
7878
+ * at `revealedEnd` once the new bars land, unless stopped (`cancelled`) meanwhile. */
7879
+ this.replayCarrying = null;
7880
+ /** Intrabar prices for the bars replay reveals — null ≡ whole bars. */
7881
+ this.replayTickSource = null;
7882
+ /** The bar being revealed tick by tick: its stored form (what it settles on), its ticks,
7883
+ * the next tick to apply, the candle built so far, the volume each tick adds when the
7884
+ * ticks carry none, and its pacing (ticks per update, ms between updates — one play
7885
+ * interval per tick — and the clock time tick 0 is due at, so late timers catch up). */
7886
+ this.replayForming = null;
7887
+ /** Tick requests by bar time, shared by prefetch and playback; `ticks` once they landed. */
7888
+ this.replayTickFetches = /* @__PURE__ */ new Map();
7889
+ /** Aborts the tick requests in flight — renewed whenever the replay moves on. */
7890
+ this.replayTickAbort = null;
7863
7891
  /** What the last applied bar did: refine the open candle, or open a new one (which
7864
7892
  * settles the previous). Read by the `script:run` cause attribution. */
7865
7893
  this.barCause = "tick";
@@ -8083,6 +8111,10 @@ var Vela = (function (exports) {
8083
8111
  const paint = (bars, final) => {
8084
8112
  if (this.generation !== gen || !final && bars.length === 0) return;
8085
8113
  if (!painted && !final && bars.length < Math.min(requested, FIRST_PAINT_BARS)) return;
8114
+ if (painted && this.replayQueue) {
8115
+ this.mergeHistoryIntoReplay(bars);
8116
+ return;
8117
+ }
8086
8118
  this.setBarSeries(bars, painted ? { preserveView: true } : void 0);
8087
8119
  if (!painted && bars.length > 0) {
8088
8120
  painted = true;
@@ -8225,6 +8257,36 @@ var Vela = (function (exports) {
8225
8257
  // exactly like a timeframe change; the cache keys the sessions apart.
8226
8258
  next2.session !== void 0 && next2.session !== m.session || next2.data !== void 0;
8227
8259
  const depthChanged = next2.bars !== void 0 && next2.bars !== m.bars;
8260
+ const sameClock = identityChanged && next2.data === void 0 && (next2.symbol === void 0 || next2.symbol === m.symbol) && (next2.timeframe !== void 0 || next2.session !== void 0);
8261
+ const pending = identityChanged ? this.replayCarrying : null;
8262
+ if (pending) {
8263
+ this.replayCarrying = null;
8264
+ if (!sameClock) {
8265
+ pending.cancelled = true;
8266
+ this.events.emit("replay:end", { reason: "market" });
8267
+ }
8268
+ }
8269
+ const replayCarry = this.replayQueue && sameClock && this.rawBars.length > 0 ? { revealedEnd: this.rawBars[this.rawBars.length - 1].time + this.barIntervalMs(), playing: this.replayPlaying, cancelled: false } : pending && sameClock ? { revealedEnd: pending.revealedEnd, playing: pending.playing, cancelled: false } : null;
8270
+ if (pending) pending.cancelled = true;
8271
+ if (replayCarry) this.replayCarrying = replayCarry;
8272
+ if (replayCarry && this.replayQueue) {
8273
+ this.endReplay(null, { restore: false, resume: false });
8274
+ const depth = this.replayDepth;
8275
+ this.replayDepth = null;
8276
+ if (depth && next2.bars === void 0) {
8277
+ if (depth.bars === void 0) delete m.bars;
8278
+ else m.bars = depth.bars;
8279
+ }
8280
+ }
8281
+ if (this.replayQueue && (identityChanged || depthChanged)) this.endReplay("market", { restore: !identityChanged, resume: false });
8282
+ if (this.replayLoading && (identityChanged || depthChanged)) {
8283
+ const depth = this.replayDepth;
8284
+ this.abandonReplayLoad("market");
8285
+ if (depth && next2.bars === void 0) {
8286
+ if (depth.bars === void 0) delete m.bars;
8287
+ else m.bars = depth.bars;
8288
+ }
8289
+ }
8228
8290
  const depthOnly = depthChanged && !identityChanged && this.rawBars.length > 0 && !m.data?.length && ((next2.bars ?? 0) <= this.rawBars.length || typeof this.feed.loadRange === "function");
8229
8291
  if (!identityChanged && !depthChanged) {
8230
8292
  if (typeof next2.visibleRange === "string") this.setVisibleRangePreset(next2.visibleRange);
@@ -8282,15 +8344,20 @@ var Vela = (function (exports) {
8282
8344
  const from = bars[Math.max(0, idx - (carriedCount - 1))].time;
8283
8345
  this.renderer.setVisibleRange({ from, to: Math.max(carried.right, bars[idx].time + 1) });
8284
8346
  }
8285
- if (!depthOnly) {
8347
+ const carry = replayCarry && !replayCarry.cancelled ? replayCarry : null;
8348
+ const carryFrom = carry ? carry.revealedEnd - this.barIntervalMs() : 0;
8349
+ const carryNow = carry != null && this.rawBars.length > 0 && carryFrom >= this.rawBars[0].time;
8350
+ const replayBack = carryNow && this.resumeCarriedReplay(this.replayStart({ from: carryFrom }), carry);
8351
+ if (!depthOnly && !replayBack) {
8286
8352
  this.restartNativeIndicators();
8287
8353
  this.restartChartTypeEngine();
8288
8354
  this.reexecuteIndicators();
8289
8355
  }
8290
- this.startLive();
8356
+ if (!replayBack) this.startLive();
8291
8357
  if (identityChanged) {
8292
8358
  this.events.emit("market:changed", { symbol: m.symbol ?? "TEST", timeframe: m.timeframe ?? "60", prev: prev3 });
8293
8359
  }
8360
+ if (carry && !carryNow) this.resumeCarriedReplay(this.replayStart({ from: carryFrom }), carry);
8294
8361
  }
8295
8362
  /**
8296
8363
  * Restart every native indicator over the new market: a `NativeIndicatorContext`
@@ -8414,6 +8481,397 @@ var Vela = (function (exports) {
8414
8481
  historyComplete() {
8415
8482
  return this.historyCompletePromise;
8416
8483
  }
8484
+ // ── bar replay ──────────────────────────────────────────────
8485
+ /**
8486
+ * Enter replay — or seek, when already replaying — keeping every bar opened at or before
8487
+ * `opts.from` and queueing the rest. A `from` inside the loaded bars cuts at once, even
8488
+ * while older history is still streaming in: what lands later joins the tape's head
8489
+ * (see {@link mergeHistoryIntoReplay}) and leaves the cut alone. An older `from` waits
8490
+ * for that load, then deepens if it still has to. Consumers restart over the cut exactly
8491
+ * as on a market switch — their models describe bars that are now hidden — and the
8492
+ * active chart-type engine is rebuilt without live data.
8493
+ */
8494
+ async replayStart(opts) {
8495
+ if (this.rawBars.length === 0) {
8496
+ console.warn("[vela] chart.replay.start() ignored \u2014 the chart has no bars yet");
8497
+ return;
8498
+ }
8499
+ let gen = this.generation;
8500
+ if (opts.from < this.rawBars[0].time) {
8501
+ await this.historyCompletePromise;
8502
+ if (this.generation !== gen || this.rawBars.length === 0) return;
8503
+ }
8504
+ const oldest = this.rawBars[0].time;
8505
+ if (opts.from < oldest && this.canHeal() && typeof this.feed.loadRange === "function") {
8506
+ const held = this.rawBars.length + (this.replayQueue?.length ?? 0);
8507
+ const bars = held + Math.ceil((oldest - opts.from) / this.barIntervalMs()) + 1;
8508
+ const seek = this.replayQueue !== null;
8509
+ if (seek) this.endReplay(null, { restore: true, resume: false });
8510
+ this.replayDepth ?? (this.replayDepth = { bars: this.config.market.bars });
8511
+ const deepened = this.setMarket({ bars });
8512
+ this.replayLoading = { seek };
8513
+ await deepened;
8514
+ gen = this.generation;
8515
+ await this.historyCompletePromise;
8516
+ if (!this.replayLoading || this.generation !== gen || this.rawBars.length === 0) return;
8517
+ this.replayLoading = null;
8518
+ }
8519
+ this.settleFormingBar();
8520
+ this.dropTickFetches();
8521
+ const tape = this.replayQueue ? [...this.rawBars, ...this.replayQueue] : this.rawBars;
8522
+ let cut = 0;
8523
+ while (cut < tape.length && tape[cut].time <= opts.from) cut += 1;
8524
+ cut = Math.max(1, cut);
8525
+ if (cut >= tape.length) {
8526
+ console.warn("[vela] chart.replay.start() ignored \u2014 no bar opens after the requested time");
8527
+ return;
8528
+ }
8529
+ const wasPlaying = this.replayPlaying;
8530
+ this.clearReplayTimer();
8531
+ if (!this.replayQueue) this.stopLive();
8532
+ this.replayQueue = tape.slice(cut);
8533
+ this.setBarSeries(tape.slice(0, cut));
8534
+ this.restartBarConsumers({ blank: true });
8535
+ if (wasPlaying) this.events.emit("replay:pause", void 0);
8536
+ this.events.emit("replay:start", { cursorTime: tape[cut - 1].time, remaining: this.replayQueue.length });
8537
+ this.prefetchTicks();
8538
+ }
8539
+ /** Reveal the next queued bar through the live-bar path — or complete the bar forming
8540
+ * tick by tick; the last one ends the replay. */
8541
+ replayStep() {
8542
+ if (this.replayForming) {
8543
+ this.finishFormingBar();
8544
+ return true;
8545
+ }
8546
+ const queue = this.replayQueue;
8547
+ const bar = queue?.shift();
8548
+ if (!queue || !bar) return false;
8549
+ this.replayTickFetches.delete(bar.time);
8550
+ this.applyBar(bar);
8551
+ this.completeReveal(bar);
8552
+ this.prefetchTicks();
8553
+ return true;
8554
+ }
8555
+ /**
8556
+ * Follow up a replay carried onto a new series of the same symbol (another timeframe or
8557
+ * session). The cut asked for keeps the bars that CLOSED by the end of what was revealed —
8558
+ * every finer bar of the last revealed one, never a coarser bar still open at that point
8559
+ * (no look-ahead). Once it settles: playback resumes if it was on; if the replay did not
8560
+ * come back (nothing left after that point, or a later switch took over), it ends as a
8561
+ * market switch would. Returns whether the replay is back already — a start with no
8562
+ * history to wait for cuts synchronously (an async function runs up to its first await).
8563
+ */
8564
+ resumeCarriedReplay(start, carry) {
8565
+ if (this.replayQueue && this.replayCarrying === carry) this.replayCarrying = null;
8566
+ void start.then(() => {
8567
+ if (this.replayCarrying === carry) this.replayCarrying = null;
8568
+ if (carry.cancelled) return;
8569
+ if (!this.replayQueue) this.events.emit("replay:end", { reason: "market" });
8570
+ else if (carry.playing) this.replayPlay();
8571
+ });
8572
+ return this.replayQueue !== null;
8573
+ }
8574
+ replayStepUpdate() {
8575
+ if (!this.replayQueue) return false;
8576
+ if (this.replayForming) {
8577
+ this.applyReplayTicks(1);
8578
+ return true;
8579
+ }
8580
+ const bar = this.replayQueue[0];
8581
+ if (!bar) return false;
8582
+ const entry = this.replayTickSource ? this.requestTicks(bar) : null;
8583
+ if (entry && !entry.ticks) {
8584
+ const epoch = this.replayTickAbort;
8585
+ void entry.done.then(() => {
8586
+ if (!this.replayPlaying && !this.replayForming && this.replayTickAbort === epoch && this.replayQueue?.[0] === bar) this.replayStepUpdate();
8587
+ });
8588
+ return true;
8589
+ }
8590
+ if (!entry?.ticks?.length) return this.replayStep();
8591
+ this.openFormingBar(entry.ticks, 1);
8592
+ return true;
8593
+ }
8594
+ replaySetTicks(source) {
8595
+ this.finishFormingBar();
8596
+ this.dropTickFetches();
8597
+ this.replayTickSource = source;
8598
+ this.prefetchTicks();
8599
+ }
8600
+ replayPlay(intervalMs2) {
8601
+ if (!this.replayQueue) return;
8602
+ if (intervalMs2 !== void 0) {
8603
+ if (!(Number.isFinite(intervalMs2) && intervalMs2 > 0)) {
8604
+ console.warn(`[vela] chart.replay.play(${intervalMs2}) ignored \u2014 the interval must be a positive number of ms`);
8605
+ return;
8606
+ }
8607
+ this.replayIntervalMs = intervalMs2;
8608
+ }
8609
+ this.clearReplayTimer();
8610
+ this.replayPlaying = true;
8611
+ if (this.replayForming) this.paceFormingBar(this.replayForming);
8612
+ this.scheduleReplayStep(this.replayForming?.delayMs);
8613
+ this.events.emit("replay:play", { intervalMs: this.replayIntervalMs });
8614
+ }
8615
+ replayPause() {
8616
+ if (!this.replayPlaying) return;
8617
+ this.clearReplayTimer();
8618
+ this.events.emit("replay:pause", void 0);
8619
+ }
8620
+ replayStop() {
8621
+ const carrying = this.replayCarrying;
8622
+ if (carrying) {
8623
+ carrying.cancelled = true;
8624
+ this.replayCarrying = null;
8625
+ this.events.emit("replay:end", { reason: "stopped" });
8626
+ }
8627
+ if (this.replayLoading) {
8628
+ const depth = this.replayDepth;
8629
+ this.abandonReplayLoad("stopped");
8630
+ void this.setMarket({ bars: depth?.bars ?? 500 });
8631
+ return;
8632
+ }
8633
+ this.endReplay("stopped", { restore: true, resume: true });
8634
+ }
8635
+ abandonReplayLoad(reason) {
8636
+ const loading = this.replayLoading;
8637
+ if (!loading) return;
8638
+ this.replayLoading = null;
8639
+ this.replayDepth = null;
8640
+ if (loading.seek) this.events.emit("replay:end", { reason });
8641
+ }
8642
+ replayState() {
8643
+ const active = this.replayQueue !== null;
8644
+ return {
8645
+ active: active || this.replayCarrying !== null,
8646
+ // a replay carried over a switch stays on while the new bars load
8647
+ playing: this.replayCarrying?.playing ?? this.replayPlaying,
8648
+ cursorTime: active ? this.rawBars[this.rawBars.length - 1]?.time ?? null : null,
8649
+ remaining: this.replayQueue?.length ?? 0,
8650
+ nextTime: this.replayForming?.bar.time ?? this.replayQueue?.[0]?.time ?? null,
8651
+ intervalMs: this.replayIntervalMs
8652
+ };
8653
+ }
8654
+ replayBounds() {
8655
+ const first2 = this.rawBars[0];
8656
+ const last2 = this.replayQueue?.[this.replayQueue.length - 1] ?? this.rawBars[this.rawBars.length - 1];
8657
+ return first2 && last2 ? { first: first2.time, last: last2.time } : null;
8658
+ }
8659
+ scheduleReplayStep(delayMs = this.replayIntervalMs) {
8660
+ this.replayTimer = setTimeout(() => {
8661
+ this.replayTimer = null;
8662
+ this.replayAdvance();
8663
+ }, delayMs);
8664
+ }
8665
+ /** One beat of timed playback: the forming bar's next ticks, else the next bar — whole,
8666
+ * or opened from its ticks (waiting for them when they have not landed yet). */
8667
+ replayAdvance() {
8668
+ if (!this.replayPlaying || !this.replayQueue) return;
8669
+ const forming = this.replayForming;
8670
+ if (forming) {
8671
+ const due = Math.floor((now() - forming.origin) / this.replayIntervalMs) + 1;
8672
+ this.applyReplayTicks(Math.max(forming.batch, due - forming.next));
8673
+ if (this.replayPlaying) this.scheduleReplayStep(forming.delayMs);
8674
+ return;
8675
+ }
8676
+ const bar = this.replayQueue[0];
8677
+ if (!bar) return;
8678
+ const entry = this.replayTickSource ? this.requestTicks(bar) : null;
8679
+ if (entry && !entry.ticks) {
8680
+ const epoch = this.replayTickAbort;
8681
+ void entry.done.then(() => {
8682
+ if (this.replayPlaying && this.replayTimer == null && this.replayTickAbort === epoch && this.replayQueue?.[0] === bar) this.replayAdvance();
8683
+ });
8684
+ return;
8685
+ }
8686
+ if (!entry?.ticks?.length) {
8687
+ if (this.replayStep() && this.replayPlaying) this.scheduleReplayStep();
8688
+ return;
8689
+ }
8690
+ this.openFormingBar(entry.ticks);
8691
+ if (this.replayPlaying) this.scheduleReplayStep(this.replayForming?.delayMs);
8692
+ }
8693
+ /** Take the next queued bar off the tape and open it at its first tick (the first
8694
+ * `count` ticks — default: one timed update's worth). */
8695
+ openFormingBar(ticks, count) {
8696
+ const bar = this.replayQueue.shift();
8697
+ const volumeEach = ticks.every((t) => t.volume === void 0) ? (bar.volume ?? 0) / ticks.length : null;
8698
+ const forming = { bar, ticks, next: 0, candle: null, volumeEach, batch: 1, delayMs: this.replayIntervalMs, origin: 0 };
8699
+ this.paceFormingBar(forming);
8700
+ this.replayForming = forming;
8701
+ this.applyReplayTicks(count ?? forming.batch);
8702
+ this.prefetchTicks();
8703
+ }
8704
+ /** One tick per interval, batched when the interval is shorter than a frame; the clock
8705
+ * is anchored on the ticks already applied (a resume or a pace change carries on from
8706
+ * there, never jumps). */
8707
+ paceFormingBar(f) {
8708
+ f.batch = Math.max(1, Math.ceil(MIN_TICK_UPDATE_MS / this.replayIntervalMs));
8709
+ f.delayMs = this.replayIntervalMs * f.batch;
8710
+ f.origin = now() - Math.max(0, f.next - 1) * this.replayIntervalMs;
8711
+ }
8712
+ /** Fold the next `count` ticks into the forming candle; the last one settles the bar. */
8713
+ applyReplayTicks(count) {
8714
+ const f = this.replayForming;
8715
+ if (!f) return;
8716
+ let c = f.candle;
8717
+ const stop = Math.min(f.ticks.length, f.next + count);
8718
+ for (; f.next < stop; f.next += 1) {
8719
+ const t = f.ticks[f.next];
8720
+ const v = f.volumeEach ?? t.volume ?? 0;
8721
+ const hi = Math.max(t.high ?? t.price, t.price);
8722
+ const lo = Math.min(t.low ?? t.price, t.price);
8723
+ if (c) {
8724
+ c = { ...c, high: Math.max(c.high, hi), low: Math.min(c.low, lo), close: t.price, volume: (c.volume ?? 0) + v };
8725
+ } else {
8726
+ const open2 = t.open ?? t.price;
8727
+ c = { time: f.bar.time, open: open2, high: Math.max(open2, hi), low: Math.min(open2, lo), close: t.price, volume: v };
8728
+ }
8729
+ }
8730
+ f.candle = c;
8731
+ if (f.next >= f.ticks.length) {
8732
+ this.finishFormingBar();
8733
+ return;
8734
+ }
8735
+ this.applyBar(c);
8736
+ this.events.emit("replay:tick", { cursorTime: f.bar.time, index: f.next - 1, count: f.ticks.length });
8737
+ }
8738
+ /** Settle the forming bar on its stored values and announce it revealed. */
8739
+ finishFormingBar() {
8740
+ const f = this.replayForming;
8741
+ if (!f) return;
8742
+ this.replayForming = null;
8743
+ this.replayTickFetches.delete(f.bar.time);
8744
+ this.applyBar(f.bar);
8745
+ this.events.emit("replay:tick", { cursorTime: f.bar.time, index: f.ticks.length - 1, count: f.ticks.length });
8746
+ this.completeReveal(f.bar);
8747
+ }
8748
+ /** Put the forming bar back in its stored form, silently — a seek or an end rebuilds
8749
+ * from the tape, which must hold the stored bar. */
8750
+ settleFormingBar() {
8751
+ const f = this.replayForming;
8752
+ if (!f) return;
8753
+ this.replayForming = null;
8754
+ this.applyBar(f.bar);
8755
+ }
8756
+ /**
8757
+ * A history answer landing mid-replay (a progressive snapshot — the whole answer so far):
8758
+ * the tape keeps its cut and its hidden queue; bars older than the tape join its head,
8759
+ * bars newer than it join the queue's end.
8760
+ */
8761
+ mergeHistoryIntoReplay(bars) {
8762
+ const queue = this.replayQueue;
8763
+ const head = this.rawBars[0];
8764
+ if (!queue || !head) return;
8765
+ const tail = queue[queue.length - 1] ?? this.rawBars[this.rawBars.length - 1];
8766
+ let older = 0;
8767
+ while (older < bars.length && bars[older].time < head.time) older += 1;
8768
+ let newer = bars.length;
8769
+ while (newer > older && bars[newer - 1].time > tail.time) newer -= 1;
8770
+ if (newer < bars.length) queue.push(...bars.slice(newer));
8771
+ if (older > 0) {
8772
+ this.setBarSeries([...bars.slice(0, older), ...this.rawBars], { preserveView: true });
8773
+ this.notifySessionsBars("backfill");
8774
+ }
8775
+ }
8776
+ /** A bar is fully on screen: announce it, and end the replay if it was the last. */
8777
+ completeReveal(bar) {
8778
+ const queue = this.replayQueue;
8779
+ if (!queue) return;
8780
+ this.events.emit("replay:step", { cursorTime: bar.time, remaining: queue.length });
8781
+ if (queue.length === 0) this.endReplay("finished", { restore: true, resume: true });
8782
+ }
8783
+ /** Ask for the ticks of the next hidden bar ahead of time. */
8784
+ prefetchTicks() {
8785
+ const next2 = this.replayQueue?.[0];
8786
+ if (next2 && this.replayTickSource) this.requestTicks(next2);
8787
+ }
8788
+ /** The (shared) tick request of a queued bar — the source runs once per bar. */
8789
+ requestTicks(bar) {
8790
+ const known = this.replayTickFetches.get(bar.time);
8791
+ if (known) return known;
8792
+ const source = this.replayTickSource;
8793
+ this.replayTickAbort ?? (this.replayTickAbort = new AbortController());
8794
+ const signal = this.replayTickAbort.signal;
8795
+ const queue = this.replayQueue ?? [];
8796
+ const end = queue[queue.indexOf(bar) + 1]?.time ?? bar.time + this.barIntervalMs();
8797
+ const entry = { ticks: null, done: Promise.resolve() };
8798
+ const land = (ticks) => {
8799
+ entry.ticks = usableTicks(ticks);
8800
+ };
8801
+ const fail = (e) => {
8802
+ if (!signal.aborted) console.warn(`[vela] replay tick source failed \u2014 revealing the bar whole (${e instanceof Error ? e.message : String(e)})`);
8803
+ land([]);
8804
+ };
8805
+ try {
8806
+ const out = source(bar, { end, signal });
8807
+ if (Array.isArray(out)) land(out);
8808
+ else entry.done = Promise.resolve(out).then(land, fail);
8809
+ } catch (e) {
8810
+ fail(e);
8811
+ }
8812
+ this.replayTickFetches.set(bar.time, entry);
8813
+ return entry;
8814
+ }
8815
+ /** Abort the tick requests in flight and forget the landed ones. */
8816
+ dropTickFetches() {
8817
+ this.replayTickAbort?.abort();
8818
+ this.replayTickAbort = null;
8819
+ this.replayTickFetches.clear();
8820
+ }
8821
+ clearReplayTimer() {
8822
+ if (this.replayTimer != null) clearTimeout(this.replayTimer);
8823
+ this.replayTimer = null;
8824
+ this.replayPlaying = false;
8825
+ }
8826
+ /**
8827
+ * Leave replay. `restore` puts the queued bars back and restarts the consumers over the
8828
+ * full history (a market switch skips it: its load replaces everything anyway); `resume`
8829
+ * restarts live updates and heals the bars that closed while replaying. A null reason
8830
+ * leaves silently — a seek that re-enters right after.
8831
+ */
8832
+ endReplay(reason, opts) {
8833
+ const queue = this.replayQueue;
8834
+ if (!queue) return;
8835
+ this.clearReplayTimer();
8836
+ if (opts.restore) this.settleFormingBar();
8837
+ else this.replayForming = null;
8838
+ this.dropTickFetches();
8839
+ this.replayQueue = null;
8840
+ const depth = reason ? this.replayDepth : null;
8841
+ if (reason) this.replayDepth = null;
8842
+ if (depth) {
8843
+ if (depth.bars === void 0) delete this.config.market.bars;
8844
+ else this.config.market.bars = depth.bars;
8845
+ }
8846
+ if (opts.restore) {
8847
+ const keep = depth ? depth.bars ?? 500 : Infinity;
8848
+ if (queue.length > 0 || this.rawBars.length > keep) {
8849
+ const full = [...this.rawBars, ...queue];
8850
+ this.setBarSeries(full.length > keep ? full.slice(full.length - keep) : full);
8851
+ this.restartBarConsumers({ blank: false });
8852
+ } else if (this.config.live) {
8853
+ this.restartNativeIndicators();
8854
+ this.restartChartTypeEngine();
8855
+ }
8856
+ }
8857
+ if (opts.resume && this.config.live) {
8858
+ this.startLive();
8859
+ const last2 = this.rawBars[this.rawBars.length - 1];
8860
+ if (last2 && this.canHeal()) void this.healGap(last2.time);
8861
+ }
8862
+ if (reason) this.events.emit("replay:end", { reason });
8863
+ }
8864
+ /**
8865
+ * The bars were replaced by a cut or a restore of the SAME market: every consumer re-runs.
8866
+ * A cut also blanks the mounted models first — they still paint the hidden bars' values,
8867
+ * which must not show until the re-runs land.
8868
+ */
8869
+ restartBarConsumers(opts) {
8870
+ if (opts.blank) this.blankIndicatorVisuals();
8871
+ this.restartNativeIndicators();
8872
+ this.restartChartTypeEngine();
8873
+ this.reexecuteIndicators();
8874
+ }
8417
8875
  /** The primary symbol as the data layer resolves it, or null if no symbol is set. */
8418
8876
  qualifiedSymbol() {
8419
8877
  const market = this.config.market;
@@ -8485,7 +8943,7 @@ var Vela = (function (exports) {
8485
8943
  engine.start({
8486
8944
  symbol: this.config.market.symbol ?? "TEST",
8487
8945
  timeframe: this.config.market.timeframe ?? "60",
8488
- live: this.config.live ?? false,
8946
+ live: (this.config.live ?? false) && this.replayQueue === null,
8489
8947
  session: this.config.market.session,
8490
8948
  bars: () => this.bars,
8491
8949
  data: this.dataControl,
@@ -8611,6 +9069,7 @@ var Vela = (function (exports) {
8611
9069
  record.session.notifyBars(reason);
8612
9070
  } else if (record.native) record.native.instance.onBars();
8613
9071
  }
9072
+ if (this.activeEngineStyle) this.typeEngines.get(this.activeEngineStyle)?.onBars?.();
8614
9073
  }
8615
9074
  /** Gap healing needs a ranged feed and a provider-backed series (offline `data` has no source). */
8616
9075
  canHeal() {
@@ -8633,10 +9092,11 @@ var Vela = (function (exports) {
8633
9092
  try {
8634
9093
  const bars = await this.feed.loadRange(this.config.market, { from: fromMs });
8635
9094
  if (this.generation !== gen) return;
9095
+ if (this.replayQueue) return;
8636
9096
  for (const b of bars) this.applyBar(b, false);
8637
9097
  } catch {
8638
9098
  } finally {
8639
- if (this.generation === gen) {
9099
+ if (this.generation === gen && !this.replayQueue) {
8640
9100
  this.healing = false;
8641
9101
  const pending = this.healBuffer;
8642
9102
  this.healBuffer = [];
@@ -8941,6 +9401,13 @@ var Vela = (function (exports) {
8941
9401
  destroy() {
8942
9402
  this.bumpGeneration();
8943
9403
  this.resolveHistoryComplete();
9404
+ this.clearReplayTimer();
9405
+ this.replayQueue = null;
9406
+ this.replayForming = null;
9407
+ this.replayCarrying = null;
9408
+ this.dropTickFetches();
9409
+ this.replayDepth = null;
9410
+ this.replayLoading = null;
8944
9411
  this.stopLive();
8945
9412
  if (this.viewportTimer != null) clearTimeout(this.viewportTimer);
8946
9413
  this.viewportUnsub?.();
@@ -9039,6 +9506,7 @@ var Vela = (function (exports) {
9039
9506
  this.emitScriptRun(id, cause, first2);
9040
9507
  },
9041
9508
  onAlert: (a) => {
9509
+ if (this.replayQueue || this.replayCarrying) return;
9042
9510
  const indicator = record.options?.title ?? record.prepared?.meta.title ?? record.title;
9043
9511
  this.events.emit("alert", { ...a, indicator });
9044
9512
  handle.emit("alert", { id: a.id, message: a.message, title: a.title, time: a.time });
@@ -9065,7 +9533,7 @@ var Vela = (function (exports) {
9065
9533
  id,
9066
9534
  symbol: this.config.market.symbol ?? "TEST",
9067
9535
  timeframe: this.config.market.timeframe ?? "60",
9068
- live: this.config.live,
9536
+ live: this.config.live && this.replayQueue === null,
9069
9537
  session: this.config.market.session,
9070
9538
  bars: () => this.bars,
9071
9539
  data: this.dataControl,
@@ -9468,17 +9936,17 @@ var Vela = (function (exports) {
9468
9936
  }
9469
9937
  emitContextChanged(id) {
9470
9938
  if (!this.registry.get(id)?.session?.getContext) return;
9471
- const now = Date.now();
9472
- if (now - (this.contextEmitAt.get(id) ?? 0) < 1e3) return;
9473
- this.contextEmitAt.set(id, now);
9939
+ const now2 = Date.now();
9940
+ if (now2 - (this.contextEmitAt.get(id) ?? 0) < 1e3) return;
9941
+ this.contextEmitAt.set(id, now2);
9474
9942
  this.events.emit("context:changed", { id });
9475
9943
  }
9476
9944
  emitScriptRun(id, cause, first2) {
9477
9945
  const record = this.registry.get(id);
9478
9946
  if (!record || record.native || !this.events.hasListeners("script:run")) return;
9479
- const now = Date.now();
9480
- if (cause === "tick" && now - (this.runEmitAt.get(id) ?? 0) < RUN_EMIT_THROTTLE_MS) return;
9481
- this.runEmitAt.set(id, now);
9947
+ const now2 = Date.now();
9948
+ if (cause === "tick" && now2 - (this.runEmitAt.get(id) ?? 0) < RUN_EMIT_THROTTLE_MS) return;
9949
+ this.runEmitAt.set(id, now2);
9482
9950
  void Promise.resolve().then(() => this.buildScriptRun(record, cause, first2)).then((run) => {
9483
9951
  if (run && this.registry.get(id)) this.events.emit("script:run", run);
9484
9952
  });
@@ -9531,6 +9999,16 @@ var Vela = (function (exports) {
9531
9999
  handle?.emit("error", { error });
9532
10000
  }
9533
10001
  };
10002
+ function now() {
10003
+ return typeof performance !== "undefined" ? performance.now() : Date.now();
10004
+ }
10005
+ function usableTicks(value) {
10006
+ if (!Array.isArray(value)) return [];
10007
+ const optional = (n) => n === void 0 || Number.isFinite(n);
10008
+ return value.filter(
10009
+ (t) => t != null && Number.isFinite(t.price) && optional(t.volume) && optional(t.high) && optional(t.low) && optional(t.open)
10010
+ );
10011
+ }
9534
10012
  function yieldToPaint() {
9535
10013
  if (typeof requestAnimationFrame === "function") {
9536
10014
  return new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(() => resolve())));
@@ -9731,6 +10209,12 @@ var Vela = (function (exports) {
9731
10209
  onCrosshairMove(cb) {
9732
10210
  return this.renderer.onCrosshairMove(cb);
9733
10211
  }
10212
+ /** A click — or a touch tap — on the plot, never the end of a pan: the open time of
10213
+ * the bar under it (`null` off the bars). On touch, where a tap moves no crosshair,
10214
+ * this is how an interaction learns which bar was chosen. */
10215
+ onClick(cb) {
10216
+ return this.renderer.onClick(cb);
10217
+ }
9734
10218
  /** Touch long-press on a price or time axis strip — silent no-op without the seam. */
9735
10219
  onAxisLongPress(cb) {
9736
10220
  return this.renderer.onAxisLongPress?.(cb) ?? (() => void 0);
@@ -10806,11 +11290,76 @@ var Vela = (function (exports) {
10806
11290
  }
10807
11291
  };
10808
11292
 
11293
+ // src/core/ReplayControl.ts
11294
+ var ReplayControl = class {
11295
+ constructor(ctrl) {
11296
+ this.ctrl = ctrl;
11297
+ }
11298
+ /**
11299
+ * Enter replay (or seek, when already replaying) at `opts.from`. Resolves once the
11300
+ * chart shows the rewound history — after any in-flight history load has finished.
11301
+ * Starts paused; call {@link play} or {@link step}.
11302
+ */
11303
+ start(opts) {
11304
+ return this.ctrl.replayStart(opts);
11305
+ }
11306
+ /** Reveal the next bar — or, mid-way through a bar played tick by tick, complete it.
11307
+ * Returns false when replay is off or nothing is left. */
11308
+ step() {
11309
+ return this.ctrl.replayStep();
11310
+ }
11311
+ /** Reveal the next UPDATE: with {@link setTicks}, the next tick of the forming bar — or
11312
+ * the next bar opened at its first tick (once its ticks are in, if still loading);
11313
+ * otherwise the next whole bar, like {@link step}. Returns false when nothing is left. */
11314
+ stepUpdate() {
11315
+ return this.ctrl.replayStepUpdate();
11316
+ }
11317
+ /** Reveal one bar (one tick, with {@link setTicks}) every `intervalMs` (default: the last
11318
+ * interval used, else 1000). Calling it again while playing changes the pace. */
11319
+ play(intervalMs2) {
11320
+ this.ctrl.replayPlay(intervalMs2);
11321
+ return this;
11322
+ }
11323
+ pause() {
11324
+ this.ctrl.replayPause();
11325
+ return this;
11326
+ }
11327
+ /** Leave replay: the full history comes back and live updates resume. Also cancels a
11328
+ * `start()` still loading older history (the chart returns to its previous depth). */
11329
+ stop() {
11330
+ this.ctrl.replayStop();
11331
+ return this;
11332
+ }
11333
+ get state() {
11334
+ return this.ctrl.replayState();
11335
+ }
11336
+ /** The loaded history a replay can walk — hidden bars included while replaying. Null
11337
+ * before any bar loaded. A start older than `first` deepens the history first. */
11338
+ get bounds() {
11339
+ return this.ctrl.replayBounds();
11340
+ }
11341
+ /**
11342
+ * Play each revealed bar as a series of intrabar updates instead of whole: while
11343
+ * playing, the forming candle opens at the first tick, then every tick moves its close
11344
+ * and stretches its high and low, exactly like a live tick — indicators, chart types
11345
+ * and the `bar` event follow — and the last one settles it on the stored bar. The play
11346
+ * interval is then the time between two TICKS: a bar lasts as many intervals as it
11347
+ * has ticks (below ~16 ms, ticks are batched per frame). The next bar's ticks are
11348
+ * requested while the current one plays; playback waits for them. `null` goes back to
11349
+ * whole bars. Takes effect from the next bar; a bar forming when the source changes
11350
+ * completes at once.
11351
+ */
11352
+ setTicks(source) {
11353
+ this.ctrl.replaySetTicks(source);
11354
+ return this;
11355
+ }
11356
+ };
11357
+
10809
11358
  // src/core/util/wall-clock.ts
10810
11359
  var BOUNDARY_SLACK_MS = 5;
10811
11360
  var SecondClock = class _SecondClock {
10812
- constructor(now = () => Date.now()) {
10813
- this.now = now;
11361
+ constructor(now2 = () => Date.now()) {
11362
+ this.now = now2;
10814
11363
  this.subs = /* @__PURE__ */ new Set();
10815
11364
  this.timer = null;
10816
11365
  }
@@ -10826,15 +11375,15 @@ var Vela = (function (exports) {
10826
11375
  };
10827
11376
  }
10828
11377
  /** Milliseconds from `now` to just past the next second boundary. */
10829
- static delayToNextSecond(now) {
10830
- const intoSecond = (now % 1e3 + 1e3) % 1e3;
11378
+ static delayToNextSecond(now2) {
11379
+ const intoSecond = (now2 % 1e3 + 1e3) % 1e3;
10831
11380
  return 1e3 - intoSecond + BOUNDARY_SLACK_MS;
10832
11381
  }
10833
11382
  arm() {
10834
11383
  this.timer = setTimeout(() => {
10835
11384
  this.timer = null;
10836
- const now = this.now();
10837
- for (const cb of this.subs) cb(now);
11385
+ const now2 = this.now();
11386
+ for (const cb of this.subs) cb(now2);
10838
11387
  if (this.subs.size > 0) this.arm();
10839
11388
  }, _SecondClock.delayToNextSecond(this.now()));
10840
11389
  }
@@ -19935,6 +20484,264 @@ ${overlayScrollbarCss(".vela-dialog *")}
19935
20484
  }
19936
20485
  };
19937
20486
 
20487
+ // src/ui/components/date-picker/controller.ts
20488
+ var MONTH_LABELS = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"];
20489
+ var MONTH_SHORT = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
20490
+ var WEEKDAY_LABELS = ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"];
20491
+ function isoDate(d) {
20492
+ return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
20493
+ }
20494
+ function normalizeDateInput(raw) {
20495
+ const m = /^(\d{4})-(\d{1,2})-(\d{1,2})$/.exec(raw.trim());
20496
+ if (!m) return null;
20497
+ const y = Number(m[1]);
20498
+ const mo = Number(m[2]);
20499
+ const d = Number(m[3]);
20500
+ const dt = new Date(y, mo - 1, d);
20501
+ if (dt.getFullYear() !== y || dt.getMonth() !== mo - 1 || dt.getDate() !== d) return null;
20502
+ return isoDate(dt);
20503
+ }
20504
+ function parseIsoDate(raw) {
20505
+ const iso = raw ? normalizeDateInput(raw) : null;
20506
+ if (!iso) return null;
20507
+ const [y, mo, d] = iso.split("-").map(Number);
20508
+ return new Date(y, mo - 1, d);
20509
+ }
20510
+ function datePickerController(opts = {}) {
20511
+ const today = opts.today ?? /* @__PURE__ */ new Date();
20512
+ let selected = parseIsoDate(opts.value);
20513
+ let year = selected?.getFullYear() ?? today.getFullYear();
20514
+ let month = selected?.getMonth() ?? today.getMonth();
20515
+ let panel = "date";
20516
+ const decade = () => Math.floor(year / 10) * 10;
20517
+ return {
20518
+ get panel() {
20519
+ return panel;
20520
+ },
20521
+ get year() {
20522
+ return year;
20523
+ },
20524
+ get month() {
20525
+ return month;
20526
+ },
20527
+ get value() {
20528
+ return selected ? isoDate(selected) : null;
20529
+ },
20530
+ header() {
20531
+ return {
20532
+ month: MONTH_LABELS[month] ?? "",
20533
+ monthVisible: panel === "date",
20534
+ year: panel === "year" ? `${decade()}-${decade() + 9}` : String(year),
20535
+ yearDisabled: panel === "year"
20536
+ // in the decade panel the year reads as its title
20537
+ };
20538
+ },
20539
+ navLabels() {
20540
+ if (panel === "date") return { prev: "Previous month", next: "Next month" };
20541
+ if (panel === "month") return { prev: "Previous year", next: "Next year" };
20542
+ return { prev: "Previous decade", next: "Next decade" };
20543
+ },
20544
+ cells() {
20545
+ const out = [];
20546
+ if (panel === "date") {
20547
+ const startPad = new Date(year, month, 1).getDay();
20548
+ const days = new Date(year, month + 1, 0).getDate();
20549
+ const todayIso = isoDate(today);
20550
+ const selectedIso = selected ? isoDate(selected) : "";
20551
+ for (let i = 0; i < startPad; i++) out.push({ label: "", value: "", checked: false, today: false, blank: true });
20552
+ for (let day = 1; day <= days; day++) {
20553
+ const iso = `${year}-${String(month + 1).padStart(2, "0")}-${String(day).padStart(2, "0")}`;
20554
+ out.push({ label: String(day), value: iso, checked: iso === selectedIso, today: iso === todayIso });
20555
+ }
20556
+ return out;
20557
+ }
20558
+ if (panel === "month") {
20559
+ for (let m = 0; m < 12; m++) {
20560
+ out.push({
20561
+ label: MONTH_SHORT[m] ?? "",
20562
+ value: m,
20563
+ checked: selected?.getFullYear() === year && selected.getMonth() === m,
20564
+ today: today.getFullYear() === year && today.getMonth() === m
20565
+ });
20566
+ }
20567
+ return out;
20568
+ }
20569
+ const d = decade();
20570
+ for (let y = d - 1; y <= d + 10; y++) {
20571
+ out.push({ label: String(y), value: y, checked: selected?.getFullYear() === y, today: today.getFullYear() === y, outside: y < d || y > d + 9 });
20572
+ }
20573
+ return out;
20574
+ },
20575
+ step(dir) {
20576
+ if (panel === "date") {
20577
+ month += dir;
20578
+ if (month < 0) {
20579
+ month = 11;
20580
+ year -= 1;
20581
+ }
20582
+ if (month > 11) {
20583
+ month = 0;
20584
+ year += 1;
20585
+ }
20586
+ } else {
20587
+ year += panel === "month" ? dir : dir * 10;
20588
+ }
20589
+ },
20590
+ showMonths() {
20591
+ panel = "month";
20592
+ },
20593
+ showYears() {
20594
+ panel = "year";
20595
+ },
20596
+ choose(cell) {
20597
+ if (cell.blank) return null;
20598
+ if (panel === "date") {
20599
+ selected = parseIsoDate(String(cell.value));
20600
+ return selected ? isoDate(selected) : null;
20601
+ }
20602
+ if (panel === "month") {
20603
+ month = Number(cell.value);
20604
+ panel = "date";
20605
+ return null;
20606
+ }
20607
+ year = Number(cell.value);
20608
+ panel = "month";
20609
+ return null;
20610
+ },
20611
+ setValue(value) {
20612
+ selected = parseIsoDate(value);
20613
+ if (selected) {
20614
+ year = selected.getFullYear();
20615
+ month = selected.getMonth();
20616
+ }
20617
+ panel = "date";
20618
+ }
20619
+ };
20620
+ }
20621
+
20622
+ // src/ui/components/date-picker/styles.ts
20623
+ var DATE_PICKER_STYLE_ID = "vela-date-picker-styles";
20624
+ var DATE_PICKER_CSS = `
20625
+ .vela-date-picker{color:var(--vela-fg);font:14px var(--vela-font);user-select:none;}
20626
+ .vela-date-picker [hidden]{display:none !important;}
20627
+ .vela-date-picker-head{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:8px;}
20628
+ .vela-date-picker-title{flex:1;display:flex;align-items:center;justify-content:center;gap:2px;min-width:0;}
20629
+ .vela-date-picker-switch{border:none;background:transparent;color:var(--vela-fg-bright);font:inherit;font-weight:600;font-size:14px;padding:2px 6px;border-radius:4px;cursor:pointer;}
20630
+ .vela-date-picker-switch:not(:disabled):hover{background:var(--vela-hover);}
20631
+ .vela-date-picker-switch:disabled{cursor:default;}
20632
+ .vela-date-picker-nav{width:24px;height:24px;border:none;background:transparent;color:var(--vela-fg-muted);border-radius:4px;padding:0;cursor:pointer;display:inline-flex;align-items:center;justify-content:center;font-size:14px;}
20633
+ .vela-date-picker-nav:hover{background:var(--vela-hover);color:var(--vela-fg-bright);}
20634
+ .vela-date-picker-week,.vela-date-picker-grid{display:grid;grid-template-columns:repeat(7,28px);gap:2px;}
20635
+ .vela-date-picker-week{margin-bottom:4px;color:var(--vela-fg-muted);font-size:11px;text-align:center;}
20636
+ .vela-date-picker-week span{line-height:20px;}
20637
+ .vela-date-picker-blank{width:28px;height:28px;}
20638
+ .vela-date-picker-day{width:28px;height:28px;border:none;background:transparent;color:inherit;border-radius:4px;padding:0;cursor:pointer;font:inherit;font-size:14px;}
20639
+ .vela-date-picker-day:hover{background:var(--vela-hover);}
20640
+ .vela-date-picker-day[data-checked]{background:var(--vela-hover-strong);color:var(--vela-fg-bright);}
20641
+ .vela-date-picker-day[data-today]:not([data-checked]){box-shadow:inset 0 0 0 1px var(--vela-border-strong);}
20642
+ .vela-date-picker-cells{display:grid;grid-template-columns:repeat(3,1fr);gap:4px;width:calc(7 * 28px + 6 * 2px);}
20643
+ .vela-date-picker-cell{height:36px;border:none;background:transparent;color:inherit;border-radius:4px;padding:0 4px;cursor:pointer;font:inherit;font-size:14px;}
20644
+ .vela-date-picker-cell:hover{background:var(--vela-hover);}
20645
+ .vela-date-picker-cell[data-checked]{background:var(--vela-hover-strong);color:var(--vela-fg-bright);}
20646
+ .vela-date-picker-cell[data-today]:not([data-checked]){box-shadow:inset 0 0 0 1px var(--vela-border-strong);}
20647
+ .vela-date-picker-cell[data-outside]{opacity:0.45;}
20648
+ `;
20649
+
20650
+ // src/ui/components/date-picker/view.ts
20651
+ var DatePicker = class {
20652
+ constructor(opts = {}) {
20653
+ this.opts = opts;
20654
+ const doc = document;
20655
+ injectStyles(DATE_PICKER_STYLE_ID, DATE_PICKER_CSS, doc);
20656
+ this.ctrl = datePickerController(opts);
20657
+ const button = (className) => {
20658
+ const b = doc.createElement("button");
20659
+ b.type = "button";
20660
+ b.className = className;
20661
+ return b;
20662
+ };
20663
+ this.prev = button("vela-date-picker-nav");
20664
+ this.prev.append(iconEl("chevron-left", doc));
20665
+ this.next = button("vela-date-picker-nav");
20666
+ this.next.append(iconEl("chevron-right", doc));
20667
+ this.monthBtn = button("vela-date-picker-switch");
20668
+ this.monthBtn.setAttribute("aria-label", "Choose month");
20669
+ this.yearBtn = button("vela-date-picker-switch");
20670
+ this.yearBtn.setAttribute("aria-label", "Choose year");
20671
+ const title = doc.createElement("div");
20672
+ title.className = "vela-date-picker-title";
20673
+ title.append(this.monthBtn, this.yearBtn);
20674
+ const head = doc.createElement("div");
20675
+ head.className = "vela-date-picker-head";
20676
+ head.append(this.prev, title, this.next);
20677
+ this.week = doc.createElement("div");
20678
+ this.week.className = "vela-date-picker-week";
20679
+ for (const d of WEEKDAY_LABELS) {
20680
+ const cell = doc.createElement("span");
20681
+ cell.textContent = d;
20682
+ this.week.appendChild(cell);
20683
+ }
20684
+ this.grid = doc.createElement("div");
20685
+ const on = (el, run) => el.addEventListener("click", (e) => {
20686
+ e.stopPropagation();
20687
+ run();
20688
+ this.paint();
20689
+ });
20690
+ on(this.prev, () => this.ctrl.step(-1));
20691
+ on(this.next, () => this.ctrl.step(1));
20692
+ on(this.monthBtn, () => this.ctrl.showMonths());
20693
+ on(this.yearBtn, () => this.ctrl.showYears());
20694
+ this.el = doc.createElement("div");
20695
+ this.el.className = "vela-date-picker";
20696
+ this.el.append(head, this.week, this.grid);
20697
+ this.paint();
20698
+ }
20699
+ get value() {
20700
+ return this.ctrl.value;
20701
+ }
20702
+ /** Rewrite the selection without emitting (the calendar opens on its month). */
20703
+ setValue(value) {
20704
+ this.ctrl.setValue(value);
20705
+ this.paint();
20706
+ }
20707
+ paint() {
20708
+ const doc = this.el.ownerDocument;
20709
+ const h = this.ctrl.header();
20710
+ this.monthBtn.hidden = !h.monthVisible;
20711
+ this.monthBtn.textContent = h.month;
20712
+ this.yearBtn.textContent = h.year;
20713
+ this.yearBtn.disabled = h.yearDisabled;
20714
+ const nav = this.ctrl.navLabels();
20715
+ this.prev.setAttribute("aria-label", nav.prev);
20716
+ this.next.setAttribute("aria-label", nav.next);
20717
+ const dates = this.ctrl.panel === "date";
20718
+ this.week.hidden = !dates;
20719
+ this.grid.className = dates ? "vela-date-picker-grid" : "vela-date-picker-cells";
20720
+ this.grid.replaceChildren(...this.ctrl.cells().map((cell) => this.cellEl(doc, cell, dates)));
20721
+ }
20722
+ cellEl(doc, cell, dates) {
20723
+ if (cell.blank) {
20724
+ const blank = doc.createElement("span");
20725
+ blank.className = "vela-date-picker-blank";
20726
+ return blank;
20727
+ }
20728
+ const b = doc.createElement("button");
20729
+ b.type = "button";
20730
+ b.className = dates ? "vela-date-picker-day" : "vela-date-picker-cell";
20731
+ b.textContent = cell.label;
20732
+ if (cell.checked) b.dataset.checked = "1";
20733
+ if (cell.today) b.dataset.today = "1";
20734
+ if (cell.outside) b.dataset.outside = "1";
20735
+ b.addEventListener("click", (e) => {
20736
+ e.stopPropagation();
20737
+ const picked = this.ctrl.choose(cell);
20738
+ if (picked) this.opts.onPick?.(picked);
20739
+ else this.paint();
20740
+ });
20741
+ return b;
20742
+ }
20743
+ };
20744
+
19938
20745
  // src/ui/components/switch/controller.ts
19939
20746
  function switchController(opts = {}) {
19940
20747
  let checked = opts.checked ?? false;
@@ -20656,14 +21463,49 @@ ${overlayScrollbarCss(".vela-dialog *")}
20656
21463
  const knob = document.createElement("div");
20657
21464
  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;";
20658
21465
  track.appendChild(knob);
20659
- const pctBox = document.createElement("div");
20660
- 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;`;
21466
+ const pctBox = document.createElement("input");
21467
+ pctBox.type = "text";
21468
+ pctBox.inputMode = "numeric";
21469
+ pctBox.title = "Opacity (%)";
21470
+ 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;`;
21471
+ pctBox.addEventListener("focus", () => {
21472
+ pctBox.style.borderColor = "var(--vela-border-strong)";
21473
+ pctBox.select();
21474
+ });
20661
21475
  opRow.append(track, pctBox);
20662
21476
  const paintOpacity = () => {
20663
21477
  track.style.background = `linear-gradient(to right, ${curHex}00, ${curHex}ff), ${CHECKER}`;
20664
21478
  knob.style.left = `${curAlpha * 100}%`;
20665
- pctBox.textContent = `${Math.round(curAlpha * 100)}%`;
21479
+ pctBox.value = `${Math.round(curAlpha * 100)}%`;
20666
21480
  };
21481
+ const commitTyped = () => {
21482
+ pctBox.style.borderColor = "var(--vela-border)";
21483
+ const n = parseFloat(pctBox.value.replace("%", ""));
21484
+ if (Number.isFinite(n)) {
21485
+ const next2 = Math.max(0, Math.min(100, n)) / 100;
21486
+ if (Math.round(next2 * 100) !== Math.round(curAlpha * 100)) {
21487
+ curAlpha = next2;
21488
+ paintOpacity();
21489
+ emit();
21490
+ return;
21491
+ }
21492
+ }
21493
+ paintOpacity();
21494
+ };
21495
+ pctBox.addEventListener("change", commitTyped);
21496
+ pctBox.addEventListener("blur", () => {
21497
+ pctBox.style.borderColor = "var(--vela-border)";
21498
+ paintOpacity();
21499
+ });
21500
+ pctBox.addEventListener("keydown", (e) => {
21501
+ if (e.key === "Escape") return;
21502
+ e.stopPropagation();
21503
+ if (e.key === "Enter") {
21504
+ e.preventDefault();
21505
+ commitTyped();
21506
+ pctBox.blur();
21507
+ }
21508
+ });
20667
21509
  let dragging = false;
20668
21510
  const onDrag = (clientX) => {
20669
21511
  const r = track.getBoundingClientRect();
@@ -21887,10 +22729,16 @@ ${overlayScrollbarCss(".vela-dialog *")}
21887
22729
  this.calendarPop = null;
21888
22730
  this.calendarAnchor = null;
21889
22731
  },
21890
- content: (el) => fillCalendar(el, current, (iso) => {
21891
- pop.hide();
21892
- onPick(iso);
21893
- })
22732
+ content: (el) => {
22733
+ const picker = new DatePicker({
22734
+ value: current,
22735
+ onPick: (iso) => {
22736
+ pop.hide();
22737
+ onPick(iso);
22738
+ }
22739
+ });
22740
+ el.append(picker.el);
22741
+ }
21894
22742
  });
21895
22743
  this.calendarPop = pop;
21896
22744
  this.calendarAnchor = anchor;
@@ -22015,28 +22863,6 @@ ${overlayScrollbarCss(".vela-dialog *")}
22015
22863
  .vela-ind-combo-chevron{position:absolute;right:0;top:0;bottom:0;width:26px;border:none;background:transparent;color:inherit;opacity:0.55;cursor:pointer;display:flex;align-items:center;justify-content:center;padding:0;}
22016
22864
  .vela-ind-combo-chevron:hover{opacity:0.9;}
22017
22865
  .vela-ind-cal{background:var(--vela-bg);color:var(--vela-fg);border:none;border-radius:6px;box-shadow:var(--vela-shadow);font:14px var(--vela-font);padding:10px 12px;user-select:none;}
22018
- .vela-ind-cal [hidden]{display:none !important;}
22019
- .vela-ind-cal-head{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:8px;}
22020
- .vela-ind-cal-title{flex:1;display:flex;align-items:center;justify-content:center;gap:2px;min-width:0;}
22021
- .vela-ind-cal-switch{border:none;background:transparent;color:var(--vela-fg-bright);font:inherit;font-weight:600;font-size:14px;padding:2px 6px;border-radius:4px;cursor:pointer;}
22022
- .vela-ind-cal-switch:not(:disabled):hover{background:var(--vela-hover);}
22023
- .vela-ind-cal-switch:disabled{cursor:default;}
22024
- .vela-ind-cal-nav{width:24px;height:24px;border:none;background:transparent;color:var(--vela-fg-muted);border-radius:4px;padding:0;cursor:pointer;display:inline-flex;align-items:center;justify-content:center;}
22025
- .vela-ind-cal-nav:hover{background:var(--vela-hover);color:var(--vela-fg-bright);}
22026
- .vela-ind-cal-week,.vela-ind-cal-grid{display:grid;grid-template-columns:repeat(7,28px);gap:2px;}
22027
- .vela-ind-cal-week{margin-bottom:4px;color:var(--vela-fg-muted);font-size:11px;text-align:center;}
22028
- .vela-ind-cal-week span{line-height:20px;}
22029
- .vela-ind-cal-blank{width:28px;height:28px;}
22030
- .vela-ind-cal-day{width:28px;height:28px;border:none;background:transparent;color:inherit;border-radius:4px;padding:0;cursor:pointer;font:inherit;font-size:14px;}
22031
- .vela-ind-cal-day:hover{background:var(--vela-hover);}
22032
- .vela-ind-cal-day[data-checked]{background:var(--vela-hover-strong);color:var(--vela-fg-bright);}
22033
- .vela-ind-cal-day[data-today]:not([data-checked]){box-shadow:inset 0 0 0 1px var(--vela-border-strong);}
22034
- .vela-ind-cal-cells{display:grid;grid-template-columns:repeat(3,1fr);gap:4px;width:calc(7 * 28px + 6 * 2px);}
22035
- .vela-ind-cal-cell{height:36px;border:none;background:transparent;color:inherit;border-radius:4px;padding:0 4px;cursor:pointer;font:inherit;font-size:14px;}
22036
- .vela-ind-cal-cell:hover{background:var(--vela-hover);}
22037
- .vela-ind-cal-cell[data-checked]{background:var(--vela-hover-strong);color:var(--vela-fg-bright);}
22038
- .vela-ind-cal-cell[data-today]:not([data-checked]){box-shadow:inset 0 0 0 1px var(--vela-border-strong);}
22039
- .vela-ind-cal-cell[data-outside]{opacity:0.45;}
22040
22866
  ${overlayScrollbarCss(".vela-dialog.vela-ind-dialog *", 9)}
22041
22867
  .vela-ind-tab{font-weight:600;font-size:13px;line-height:20px;transition:color var(--vela-dur-fast) ease,border-color var(--vela-dur-fast) ease;}
22042
22868
  .vela-ind-tab:not(.vela-ind-tab-active):hover{color:var(--vela-fg-bright);}
@@ -22070,174 +22896,6 @@ ${overlayScrollbarCss(".vela-dialog.vela-ind-dialog *", 9)}
22070
22896
  const v = `${hh}:${mm}`;
22071
22897
  return { value: v, label: v };
22072
22898
  });
22073
- var MONTH_LABELS = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"];
22074
- var MONTH_SHORT = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
22075
- var WEEKDAY_LABELS = ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"];
22076
- function normalizeDateInput(raw) {
22077
- const t = raw.trim();
22078
- const m = /^(\d{4})-(\d{1,2})-(\d{1,2})$/.exec(t);
22079
- if (!m) return null;
22080
- const y = Number(m[1]);
22081
- const mo = Number(m[2]);
22082
- const d = Number(m[3]);
22083
- const dt = new Date(y, mo - 1, d);
22084
- if (dt.getFullYear() !== y || dt.getMonth() !== mo - 1 || dt.getDate() !== d) return null;
22085
- return isoDate(dt);
22086
- }
22087
- function parseIsoDate(raw) {
22088
- const iso = normalizeDateInput(raw);
22089
- if (!iso) return null;
22090
- const [y, mo, d] = iso.split("-").map(Number);
22091
- return new Date(y, mo - 1, d);
22092
- }
22093
- function isoDate(d) {
22094
- return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
22095
- }
22096
- function fillCalendar(el, current, onPick) {
22097
- const today = /* @__PURE__ */ new Date();
22098
- const selected = parseIsoDate(current);
22099
- let year = selected?.getFullYear() ?? today.getFullYear();
22100
- let month = selected?.getMonth() ?? today.getMonth();
22101
- let mode = "date";
22102
- const prev3 = document.createElement("button");
22103
- prev3.type = "button";
22104
- prev3.className = "vela-ind-cal-nav";
22105
- prev3.innerHTML = iconAt("chevron-left", 14);
22106
- const next2 = document.createElement("button");
22107
- next2.type = "button";
22108
- next2.className = "vela-ind-cal-nav";
22109
- next2.innerHTML = iconAt("chevron-right", 14);
22110
- const monthBtn = document.createElement("button");
22111
- monthBtn.type = "button";
22112
- monthBtn.className = "vela-ind-cal-switch";
22113
- monthBtn.setAttribute("aria-label", "Choose month");
22114
- const yearBtn = document.createElement("button");
22115
- yearBtn.type = "button";
22116
- yearBtn.className = "vela-ind-cal-switch";
22117
- yearBtn.setAttribute("aria-label", "Choose year");
22118
- const title = document.createElement("div");
22119
- title.className = "vela-ind-cal-title";
22120
- title.append(monthBtn, yearBtn);
22121
- const head = document.createElement("div");
22122
- head.className = "vela-ind-cal-head";
22123
- head.append(prev3, title, next2);
22124
- const week = document.createElement("div");
22125
- week.className = "vela-ind-cal-week";
22126
- for (const d of WEEKDAY_LABELS) {
22127
- const cell = document.createElement("span");
22128
- cell.textContent = d;
22129
- week.appendChild(cell);
22130
- }
22131
- const grid = document.createElement("div");
22132
- const paint = () => {
22133
- const decade = Math.floor(year / 10) * 10;
22134
- monthBtn.hidden = mode !== "date";
22135
- monthBtn.textContent = MONTH_LABELS[month] ?? "";
22136
- yearBtn.textContent = mode === "year" ? `${decade}-${decade + 9}` : String(year);
22137
- yearBtn.disabled = mode === "year";
22138
- week.hidden = mode !== "date";
22139
- prev3.setAttribute("aria-label", mode === "date" ? "Previous month" : mode === "month" ? "Previous year" : "Previous decade");
22140
- next2.setAttribute("aria-label", mode === "date" ? "Next month" : mode === "month" ? "Next year" : "Next decade");
22141
- grid.className = mode === "date" ? "vela-ind-cal-grid" : "vela-ind-cal-cells";
22142
- grid.replaceChildren();
22143
- if (mode === "date") {
22144
- const startPad = new Date(year, month, 1).getDay();
22145
- const days = new Date(year, month + 1, 0).getDate();
22146
- const todayIso = isoDate(today);
22147
- const selectedIso = selected ? isoDate(selected) : "";
22148
- for (let i = 0; i < startPad; i++) {
22149
- const blank = document.createElement("span");
22150
- blank.className = "vela-ind-cal-blank";
22151
- grid.appendChild(blank);
22152
- }
22153
- for (let day = 1; day <= days; day++) {
22154
- const iso = `${year}-${String(month + 1).padStart(2, "0")}-${String(day).padStart(2, "0")}`;
22155
- const b = document.createElement("button");
22156
- b.type = "button";
22157
- b.className = "vela-ind-cal-day";
22158
- b.textContent = String(day);
22159
- if (iso === selectedIso) b.dataset.checked = "1";
22160
- if (iso === todayIso) b.dataset.today = "1";
22161
- b.addEventListener("click", (e) => {
22162
- e.stopPropagation();
22163
- onPick(iso);
22164
- });
22165
- grid.appendChild(b);
22166
- }
22167
- return;
22168
- }
22169
- if (mode === "month") {
22170
- for (let m = 0; m < 12; m++) {
22171
- const b = document.createElement("button");
22172
- b.type = "button";
22173
- b.className = "vela-ind-cal-cell";
22174
- b.textContent = MONTH_SHORT[m] ?? "";
22175
- if (selected?.getFullYear() === year && selected.getMonth() === m) b.dataset.checked = "1";
22176
- if (today.getFullYear() === year && today.getMonth() === m) b.dataset.today = "1";
22177
- b.addEventListener("click", (e) => {
22178
- e.stopPropagation();
22179
- month = m;
22180
- mode = "date";
22181
- paint();
22182
- });
22183
- grid.appendChild(b);
22184
- }
22185
- return;
22186
- }
22187
- for (let y = decade - 1; y <= decade + 10; y++) {
22188
- const b = document.createElement("button");
22189
- b.type = "button";
22190
- b.className = "vela-ind-cal-cell";
22191
- b.textContent = String(y);
22192
- if (y < decade || y > decade + 9) b.dataset.outside = "1";
22193
- if (selected?.getFullYear() === y) b.dataset.checked = "1";
22194
- if (today.getFullYear() === y) b.dataset.today = "1";
22195
- b.addEventListener("click", (e) => {
22196
- e.stopPropagation();
22197
- year = y;
22198
- mode = "month";
22199
- paint();
22200
- });
22201
- grid.appendChild(b);
22202
- }
22203
- };
22204
- const step = (dir) => {
22205
- if (mode === "date") {
22206
- month += dir;
22207
- if (month < 0) {
22208
- month = 11;
22209
- year -= 1;
22210
- }
22211
- if (month > 11) {
22212
- month = 0;
22213
- year += 1;
22214
- }
22215
- } else {
22216
- year += mode === "month" ? dir : dir * 10;
22217
- }
22218
- paint();
22219
- };
22220
- prev3.addEventListener("click", (e) => {
22221
- e.stopPropagation();
22222
- step(-1);
22223
- });
22224
- next2.addEventListener("click", (e) => {
22225
- e.stopPropagation();
22226
- step(1);
22227
- });
22228
- monthBtn.addEventListener("click", (e) => {
22229
- e.stopPropagation();
22230
- mode = "month";
22231
- paint();
22232
- });
22233
- yearBtn.addEventListener("click", (e) => {
22234
- e.stopPropagation();
22235
- mode = "year";
22236
- paint();
22237
- });
22238
- paint();
22239
- el.append(head, week, grid);
22240
- }
22241
22899
  function normalizeTimeInput(raw) {
22242
22900
  const t = raw.trim();
22243
22901
  const m = /^(\d{1,2}):(\d{2})$/.exec(t) ?? /^(\d{2})(\d{2})$/.exec(t);
@@ -23615,6 +24273,22 @@ ${overlayScrollbarCss(".vela-dialog.vela-ind-dialog *", 9)}
23615
24273
  for (const t of chartTypes()) if (!out.includes(t.id)) out.push(t.id);
23616
24274
  return out;
23617
24275
  }
24276
+ function sanitizeCrosshairOverride(value) {
24277
+ if (!value || typeof value !== "object") return null;
24278
+ const v = value;
24279
+ const out = {};
24280
+ if (isBool(v.vertical)) out.vertical = v.vertical;
24281
+ if (isBool(v.horizontal)) out.horizontal = v.horizontal;
24282
+ if (isColor(v.color)) out.color = v.color;
24283
+ if (isNum(v.width)) out.width = Math.max(0.5, Math.min(8, v.width));
24284
+ if (isLineStyle(v.style)) out.style = v.style;
24285
+ if (isNum(v.opacity)) out.opacity = clampOpacity(v.opacity);
24286
+ const shade = v.shadeRight;
24287
+ if (shade && typeof shade === "object" && isColor(shade.color)) {
24288
+ out.shadeRight = isNum(shade.opacity) ? { color: shade.color, opacity: clampOpacity(shade.opacity) } : { color: shade.color };
24289
+ }
24290
+ return Object.keys(out).length > 0 ? out : null;
24291
+ }
23618
24292
  function isColor(v) {
23619
24293
  return typeof v === "string" && v.trim().length > 0;
23620
24294
  }
@@ -25276,7 +25950,7 @@ void main() {
25276
25950
 
25277
25951
  // src/renderers/native/core/Animator.ts
25278
25952
  var Animator = class {
25279
- constructor(tick, raf3, cancel, now) {
25953
+ constructor(tick, raf3, cancel, now2) {
25280
25954
  this.tick = tick;
25281
25955
  this.handle = null;
25282
25956
  this.last = 0;
@@ -25290,7 +25964,7 @@ void main() {
25290
25964
  };
25291
25965
  this.raf = raf3 ?? ((cb) => requestAnimationFrame(cb));
25292
25966
  this.cancel = cancel ?? ((h) => cancelAnimationFrame(h));
25293
- this.now = now ?? (() => performance.now());
25967
+ this.now = now2 ?? (() => performance.now());
25294
25968
  }
25295
25969
  get active() {
25296
25970
  return this.handle !== null;
@@ -25422,6 +26096,12 @@ void main() {
25422
26096
  this.lastTapT = 0;
25423
26097
  this.lastTapX = 0;
25424
26098
  this.lastTapY = 0;
26099
+ // Whether the last two primary presses were claimed by the drawings layer. A double-click
26100
+ // is the last press plus the one before it; a tool-driven click pair must never fall through
26101
+ // to the pane maximize toggle, even when the tool disarmed itself between the two clicks
26102
+ // (a finished placement leaves plain cursor mode for the second click).
26103
+ this.lastPressDrawing = false;
26104
+ this.prevPressDrawing = false;
25425
26105
  this.onTouchContextMenu = (e) => {
25426
26106
  if (this.touches.size === 0) return;
25427
26107
  if (!(e.target instanceof Node) || !this.el?.contains(e.target)) return;
@@ -25476,19 +26156,24 @@ void main() {
25476
26156
  this.moved = false;
25477
26157
  this.startX = x;
25478
26158
  this.startY = y;
26159
+ this.prevPressDrawing = this.lastPressDrawing;
26160
+ this.lastPressDrawing = false;
25479
26161
  if (this.deps.drawingsClaim?.(x, y)) {
25480
26162
  this.region = "drawing";
26163
+ this.lastPressDrawing = true;
25481
26164
  this.deps.drawingsPointerDown?.(x, y, this.snapMode(e), e.shiftKey, e.ctrlKey || e.metaKey);
25482
26165
  this.capture(e.pointerId);
25483
26166
  return;
25484
26167
  }
25485
26168
  if (e.shiftKey && this.regionAt(x, y) === "data" && this.deps.drawingsMeasureStart?.(x, y, this.snapMode(e))) {
25486
26169
  this.region = "drawing";
26170
+ this.lastPressDrawing = true;
25487
26171
  this.capture(e.pointerId);
25488
26172
  return;
25489
26173
  }
25490
26174
  if ((e.ctrlKey || e.metaKey) && this.regionAt(x, y) === "data" && this.deps.drawingsMarqueeStart?.(x, y)) {
25491
26175
  this.region = "drawing";
26176
+ this.lastPressDrawing = true;
25492
26177
  this.capture(e.pointerId);
25493
26178
  return;
25494
26179
  }
@@ -25810,6 +26495,7 @@ void main() {
25810
26495
  if (region === "price") this.deps.resetPriceScale(x, y);
25811
26496
  else if (region === "separator") this.deps.resetPaneSize(y);
25812
26497
  else if (region === "time") this.deps.resetView();
26498
+ else if (this.lastPressDrawing || this.prevPressDrawing) return;
25813
26499
  else this.deps.dataDblClick(x, y);
25814
26500
  }
25815
26501
  };
@@ -26089,6 +26775,8 @@ void main() {
26089
26775
  /** VPVR-layer config pushed by the VPVR native indicator (null ⇒ layer off). Ephemeral. */
26090
26776
  this.vpvrLayer = null;
26091
26777
  this.crosshair = null;
26778
+ /** Runtime override of the crosshair's lines and style (the `crosshairOverride` feature). Ephemeral. */
26779
+ this.crosshairOverride = null;
26092
26780
  /** How the base price series is drawn on the price pane (candles by default). */
26093
26781
  this.priceStyle = "candles";
26094
26782
  /** Price-series base painting for the ACTIVE style (see ChartTypeDefinition.basePainting). */
@@ -27973,6 +28661,7 @@ void main() {
27973
28661
  const step = pickStep(span / Math.max(1, target));
27974
28662
  const zFrom = fromMs + offsetMs;
27975
28663
  const zTo = toMs + offsetMs;
28664
+ if (step >= MONTH) return calendarTicks(zFrom, zTo, step >= YEAR ? 12 : step >= 3 * MONTH ? 3 : 1, offsetMs);
27976
28665
  const first2 = Math.ceil(zFrom / step) * step;
27977
28666
  const out = [];
27978
28667
  for (let zt = first2; zt <= zTo; zt += step) {
@@ -28003,11 +28692,23 @@ void main() {
28003
28692
  }
28004
28693
  return out;
28005
28694
  }
28695
+ function calendarTicks(zFrom, zTo, months, offsetMs) {
28696
+ const d0 = new Date(zFrom);
28697
+ let idx = d0.getUTCFullYear() * 12 + d0.getUTCMonth();
28698
+ if (Date.UTC(Math.floor(idx / 12), idx % 12, 1) < zFrom) idx += 1;
28699
+ idx = Math.ceil(idx / months) * months;
28700
+ const out = [];
28701
+ 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)) {
28702
+ const label = months === 12 ? String(Math.floor(idx / 12)) : MONTHS[idx % 12];
28703
+ out.push({ time: zt - offsetMs, label, major: true });
28704
+ }
28705
+ return out;
28706
+ }
28006
28707
 
28007
28708
  // src/renderers/native/chrome/countdown.ts
28008
- function countdownText(barOpen, barMs, now) {
28709
+ function countdownText(barOpen, barMs, now2) {
28009
28710
  if (!(barMs > 0)) return null;
28010
- const remaining = barOpen + barMs - now;
28711
+ const remaining = barOpen + barMs - now2;
28011
28712
  if (remaining <= 0) return null;
28012
28713
  return formatCountdown(remaining);
28013
28714
  }
@@ -28819,19 +29520,35 @@ void main() {
28819
29520
  const dataH = coords.height;
28820
29521
  if (!ch || ch.x < 0 || ch.x > dataW || ch.y < 0 || ch.y > dataH) return;
28821
29522
  const cs = scene.style.crosshair;
29523
+ const ov = scene.crosshairOverride;
29524
+ const vertical = ov?.vertical !== false;
29525
+ const horizontal = ov?.horizontal !== false;
28822
29526
  ctx.font = `${scene.style.fontSize}px ${theme.fontFamily}`;
28823
29527
  ctx.textBaseline = "middle";
28824
29528
  const logical = Math.round(coords.xToLogical(ch.x));
28825
- const x = Math.round(coords.logicalToX(logical)) + 0.5;
28826
- ctx.strokeStyle = cs.color ?? theme.textColor;
28827
- ctx.lineWidth = cs.width;
28828
- ctx.globalAlpha = cs.opacity;
28829
- setDash3(ctx, cs.style);
29529
+ const x = crisp(coords.logicalToX(logical), ov?.width ?? cs.width);
29530
+ if (vertical && ov?.shadeRight) {
29531
+ const from = Math.max(0, Math.round(coords.logicalToX(logical + 0.5)));
29532
+ if (from < dataW) {
29533
+ ctx.fillStyle = ov.shadeRight.color;
29534
+ ctx.globalAlpha = ov.shadeRight.opacity ?? 1;
29535
+ ctx.fillRect(from, 0, dataW - from, dataH);
29536
+ ctx.globalAlpha = 1;
29537
+ }
29538
+ }
29539
+ ctx.strokeStyle = ov?.color ?? cs.color ?? theme.textColor;
29540
+ ctx.lineWidth = ov?.width ?? cs.width;
29541
+ ctx.globalAlpha = ov?.opacity ?? cs.opacity;
29542
+ setDash3(ctx, ov?.style ?? cs.style);
28830
29543
  ctx.beginPath();
28831
- ctx.moveTo(x, 0);
28832
- ctx.lineTo(x, dataH);
28833
- ctx.moveTo(0, Math.round(ch.y) + 0.5);
28834
- ctx.lineTo(dataW, Math.round(ch.y) + 0.5);
29544
+ if (vertical) {
29545
+ ctx.moveTo(x, 0);
29546
+ ctx.lineTo(x, dataH);
29547
+ }
29548
+ if (horizontal) {
29549
+ ctx.moveTo(0, Math.round(ch.y) + 0.5);
29550
+ ctx.lineTo(dataW, Math.round(ch.y) + 0.5);
29551
+ }
28835
29552
  ctx.stroke();
28836
29553
  ctx.setLineDash([]);
28837
29554
  ctx.lineWidth = 1;
@@ -28844,11 +29561,11 @@ void main() {
28844
29561
  }
28845
29562
  }
28846
29563
  const chipBg = cs.labelBackground ?? theme.borderColor;
28847
- if (pane && pane.axisFormat !== "none") {
29564
+ if (horizontal && pane && pane.axisFormat !== "none") {
28848
29565
  const price = coords.yToPrice(ch.y, pane.scale, pane.bounds);
28849
29566
  this.chip(ctx, dataW + 1, ch.y, formatAxisValue(pane.scale, pane.bounds.height, price, percentScaleFor(scene, pane), scene.priceMintick, pane.axisFormat), chipBg, "left", false, theme.background);
28850
29567
  }
28851
- this.chip(ctx, x, dataH + 1, formatTimeStamp(coords.logicalToTime(logical), scene.timezone, coords.barInterval), chipBg, "center", true, theme.background);
29568
+ if (vertical) this.chip(ctx, x, dataH + 1, formatTimeStamp(coords.logicalToTime(logical), scene.timezone, coords.barInterval), chipBg, "center", true, theme.background);
28852
29569
  }
28853
29570
  destroy() {
28854
29571
  this.canvas = null;
@@ -28859,23 +29576,37 @@ void main() {
28859
29576
  * along), with that bar's time chip in this chart's own timezone and — when the
28860
29577
  * level resolved — the price chip on the right axis. The snap happened upstream
28861
29578
  * (`externalCrossPx`, floor-to-containing-bar) — this method only draws. Chips
28862
- * render slightly dimmed so the ghost still reads as foreign. */
29579
+ * render slightly dimmed so the ghost still reads as foreign. A `crosshairOverride`
29580
+ * applies to the ghost as well — its line style, a dropped horizontal line, and the
29581
+ * `shadeRight` veil from the ghost bar's right edge — so a pick that spans several
29582
+ * charts reads the same on every one of them. */
28863
29583
  drawExternal(ctx, ext, scene, coords, theme) {
28864
29584
  const cs = scene.style.crosshair;
29585
+ const ov = scene.crosshairOverride;
28865
29586
  const dataW = coords.width;
28866
29587
  const dataH = coords.height;
28867
- const x = Math.round(ext.x) + 0.5;
28868
- if (x < 0 || x > dataW) return;
29588
+ const x = crisp(ext.x, ov?.width ?? cs.width);
29589
+ if (ov?.vertical === false) return;
29590
+ if (ov?.shadeRight) {
29591
+ const from = Math.max(0, Math.round(coords.logicalToX(Math.round(coords.xToLogical(ext.x)) + 0.5)));
29592
+ if (from < dataW) {
29593
+ ctx.fillStyle = ov.shadeRight.color;
29594
+ ctx.globalAlpha = ov.shadeRight.opacity ?? 1;
29595
+ ctx.fillRect(from, 0, dataW - from, dataH);
29596
+ ctx.globalAlpha = 1;
29597
+ }
29598
+ }
29599
+ if (ext.line === false || x < 0 || x > dataW) return;
28869
29600
  ctx.font = `${scene.style.fontSize}px ${theme.fontFamily}`;
28870
29601
  ctx.textBaseline = "middle";
28871
- ctx.strokeStyle = cs.color ?? theme.textColor;
28872
- ctx.lineWidth = cs.width;
28873
- ctx.globalAlpha = cs.opacity * 0.55;
28874
- setDash3(ctx, cs.style);
29602
+ ctx.strokeStyle = ov?.color ?? cs.color ?? theme.textColor;
29603
+ ctx.lineWidth = ov?.width ?? cs.width;
29604
+ ctx.globalAlpha = (ov?.opacity ?? cs.opacity) * 0.55;
29605
+ setDash3(ctx, ov?.style ?? cs.style);
28875
29606
  ctx.beginPath();
28876
29607
  ctx.moveTo(x, 0);
28877
29608
  ctx.lineTo(x, dataH);
28878
- if (ext.y != null) {
29609
+ if (ext.y != null && ov?.horizontal !== false) {
28879
29610
  ctx.moveTo(0, Math.round(ext.y) + 0.5);
28880
29611
  ctx.lineTo(dataW, Math.round(ext.y) + 0.5);
28881
29612
  }
@@ -28884,7 +29615,7 @@ void main() {
28884
29615
  ctx.lineWidth = 1;
28885
29616
  ctx.globalAlpha = 0.8;
28886
29617
  const chipBg = cs.labelBackground ?? theme.borderColor;
28887
- if (ext.y != null && ext.price != null) {
29618
+ if (ext.y != null && ext.price != null && ov?.horizontal !== false) {
28888
29619
  let pane;
28889
29620
  for (const p of scene.panes.values()) {
28890
29621
  if (ext.y >= p.bounds.top && ext.y <= p.bounds.top + p.bounds.height) {
@@ -28924,6 +29655,9 @@ void main() {
28924
29655
  ctx.textAlign = "start";
28925
29656
  }
28926
29657
  };
29658
+ function crisp(px, width) {
29659
+ return Math.round(px) + (Math.round(width) % 2 === 1 ? 0.5 : 0);
29660
+ }
28927
29661
  function setDash3(ctx, style) {
28928
29662
  if (style === "dashed") ctx.setLineDash([6, 4]);
28929
29663
  else if (style === "dotted") ctx.setLineDash([2, 3]);
@@ -36515,23 +37249,21 @@ ${overlayScrollbarCss(".vela-sd-pane")}
36515
37249
  return { below, above };
36516
37250
  }
36517
37251
 
36518
- // src/renderers/native/chrome/luxalgo-logos.ts
36519
- 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>';
36520
- 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>';
37252
+ // src/renderers/native/chrome/vela-logos.ts
37253
+ 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>';
37254
+ 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>';
36521
37255
 
36522
37256
  // src/renderers/native/chrome/AttributionMark.ts
36523
- var ATTRIBUTION_URL = "https://luxalgo.com/vela";
37257
+ var ATTRIBUTION_URL = "https://velacharts.dev/?utm_source=vela&utm_medium=attribution";
36524
37258
  var STYLE_ID5 = "vela-attribution-styles";
36525
37259
  var CSS2 = `
36526
37260
  .vela-attribution {
36527
37261
  text-decoration: none;
36528
37262
  display: flex;
36529
37263
  align-items: center;
36530
- /* Gutter, wordmark size and offset below are the brand lockup's own ratios,
36531
- measured off the official horizontal logo: gutter 0.157, wordmark height
36532
- 0.966, and wordmark 0.109 LOWER than the symbol \u2014 box-centering the two
36533
- reads wrong because the symbol's ink hangs low and the wordmark descends. */
36534
- gap: 4px;
37264
+ /* No gutter: the V is the wordmark's first letter, and the letters' viewBox starts
37265
+ where the V's ends, so the kit's own letter spacing comes with them. */
37266
+ gap: 0;
36535
37267
  }
36536
37268
  .vela-attribution .vela-attr-symbol {
36537
37269
  flex: none;
@@ -36539,7 +37271,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
36539
37271
  line-height: 0;
36540
37272
  }
36541
37273
  .vela-attribution .vela-attr-symbol svg {
36542
- height: 28px;
37274
+ height: 24px;
36543
37275
  width: auto;
36544
37276
  display: block;
36545
37277
  filter: drop-shadow(0 1px 2px var(--vela-attr-shadow, rgba(0,0,0,0.45)));
@@ -36550,15 +37282,13 @@ ${overlayScrollbarCss(".vela-sd-pane")}
36550
37282
  opacity: 0;
36551
37283
  display: flex;
36552
37284
  align-items: center;
36553
- /* Offset here, not on the image: the clip box must not crop the descender. */
36554
- position: relative;
36555
- top: 3px;
37285
+ /* Same height and baseline as the V (one viewBox height), so no vertical offset. */
36556
37286
  transform: translateX(-8px);
36557
37287
  transition: max-width 0.3s ease, opacity 0.25s ease, transform 0.3s ease;
36558
37288
  flex: none;
36559
37289
  }
36560
37290
  .vela-attribution .vela-attr-wordmark svg {
36561
- height: 27px;
37291
+ height: 24px;
36562
37292
  width: auto;
36563
37293
  display: block;
36564
37294
  filter: drop-shadow(0 1px 2px var(--vela-attr-shadow, rgba(0,0,0,0.45)));
@@ -36573,14 +37303,11 @@ ${overlayScrollbarCss(".vela-sd-pane")}
36573
37303
  (data-layout): the workspace's single grid-wide mark lives OUTSIDE any renderer
36574
37304
  container, so only the shell attribute reaches it. */
36575
37305
  [data-vela-layout='mobile'] .vela-attribution .vela-attr-symbol svg,
36576
- [data-layout='mobile'] .vela-attribution .vela-attr-symbol svg { height: 22px; }
37306
+ [data-layout='mobile'] .vela-attribution .vela-attr-symbol svg { height: 20px; }
36577
37307
  [data-vela-layout='mobile'] .vela-attribution .vela-attr-wordmark,
36578
- [data-layout='mobile'] .vela-attribution .vela-attr-wordmark {
36579
- top: 2px;
36580
- transform: translateX(-6px);
36581
- }
37308
+ [data-layout='mobile'] .vela-attribution .vela-attr-wordmark { transform: translateX(-6px); }
36582
37309
  [data-vela-layout='mobile'] .vela-attribution .vela-attr-wordmark svg,
36583
- [data-layout='mobile'] .vela-attribution .vela-attr-wordmark svg { height: 21px; }
37310
+ [data-layout='mobile'] .vela-attribution .vela-attr-wordmark svg { height: 20px; }
36584
37311
  [data-vela-layout='mobile'] .vela-attribution:hover .vela-attr-wordmark,
36585
37312
  [data-layout='mobile'] .vela-attribution:hover .vela-attr-wordmark { transform: translateX(0); }
36586
37313
  `;
@@ -36637,10 +37364,10 @@ ${overlayScrollbarCss(".vela-sd-pane")}
36637
37364
  const symbol = doc.createElement("span");
36638
37365
  symbol.className = "vela-attr-symbol";
36639
37366
  symbol.setAttribute("aria-hidden", "true");
36640
- symbol.innerHTML = LUXALGO_SYMBOL_SVG;
37367
+ symbol.innerHTML = VELA_MARK_SVG;
36641
37368
  const wordmark = doc.createElement("span");
36642
37369
  wordmark.className = "vela-attr-wordmark";
36643
- wordmark.innerHTML = LUXALGO_WORDMARK_SVG;
37370
+ wordmark.innerHTML = VELA_LETTERS_SVG;
36644
37371
  a.append(symbol, wordmark);
36645
37372
  return a;
36646
37373
  }
@@ -37220,7 +37947,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
37220
37947
  this.moveIndicatorCbs = /* @__PURE__ */ new Set();
37221
37948
  this.priceStyleCbs = /* @__PURE__ */ new Set();
37222
37949
  this.name = "native";
37223
- this.features = ["logScale", "currentPriceLine", "priceLabel", "countdown", "upColor", "downColor", "glow", "animZoom", "animPan", "animScroll", "animAutoscale", "animLiveBar", "intro", "zoomAnchor", "axisDrag", "paneResize", "candleZOrder", "candleVisible", "seriesOrder", "highlights", "sessionZones", "gridlines", "axisLabels", "scaleMode", "invertScale", "paneScales", "autoScale", "timezone", "keyboard", "historyChords", "priceStyle", "priceBaseline", "baselinePrice", "settings", "attribution", "dialogHost", "tradeMarkers", "marks", "indicatorTitles", "indicatorValues"];
37950
+ this.features = ["logScale", "currentPriceLine", "priceLabel", "countdown", "upColor", "downColor", "glow", "animZoom", "animPan", "animScroll", "animAutoscale", "animLiveBar", "intro", "zoomAnchor", "axisDrag", "paneResize", "candleZOrder", "candleVisible", "seriesOrder", "highlights", "sessionZones", "gridlines", "axisLabels", "scaleMode", "invertScale", "paneScales", "autoScale", "timezone", "keyboard", "historyChords", "priceStyle", "priceBaseline", "baselinePrice", "settings", "attribution", "dialogHost", "tradeMarkers", "marks", "indicatorTitles", "indicatorValues", "crosshairOverride"];
37224
37951
  /** Track cursor proximity to the scroll button on the plot (bubbles from the button too,
37225
37952
  * so moving onto the button doesn't count as leaving). */
37226
37953
  this.onScrollProximityMove = (e) => {
@@ -37399,6 +38126,9 @@ ${overlayScrollbarCss(".vela-sd-pane")}
37399
38126
  this.scene.marks = mergeMarksState(this.scene.marks, value);
37400
38127
  this.markPopover?.close();
37401
38128
  break;
38129
+ case "crosshairOverride":
38130
+ this.scene.crosshairOverride = sanitizeCrosshairOverride(value);
38131
+ break;
37402
38132
  case "keyboard":
37403
38133
  this.setKeyboardEnabled(Boolean(value));
37404
38134
  return;
@@ -37512,6 +38242,8 @@ ${overlayScrollbarCss(".vela-sd-pane")}
37512
38242
  return { ...this.scene.tradeMarkers, colors: { ...this.scene.tradeMarkers.colors } };
37513
38243
  case "marks":
37514
38244
  return { visible: this.scene.marks.visible, groups: { ...this.scene.marks.groups } };
38245
+ case "crosshairOverride":
38246
+ return this.scene.crosshairOverride ? { ...this.scene.crosshairOverride } : null;
37515
38247
  case "keyboard":
37516
38248
  return this.keyboardEnabled;
37517
38249
  case "historyChords":
@@ -38090,8 +38822,8 @@ ${overlayScrollbarCss(".vela-sd-pane")}
38090
38822
  for (const pane of this.scene.panes.values()) pane.scale = { ...pane.scaleTarget };
38091
38823
  this.modelAlpha = 0;
38092
38824
  const start = performance.now();
38093
- const step = (now) => {
38094
- const p = Math.min(1, (now - start) / duration);
38825
+ const step = (now2) => {
38826
+ const p = Math.min(1, (now2 - start) / duration);
38095
38827
  this.scene.bars = p >= 1 ? real : real.map((b, i) => this.revealCandle(b, i, p, n, style));
38096
38828
  this.paintData();
38097
38829
  if (p < 1) {
@@ -38107,8 +38839,8 @@ ${overlayScrollbarCss(".vela-sd-pane")}
38107
38839
  fadeInModels() {
38108
38840
  const fade = Math.min(INTRO_MODEL_FADE_MS, this.intro.duration || INTRO_MODEL_FADE_MS);
38109
38841
  const start = performance.now();
38110
- const step = (now) => {
38111
- this.modelAlpha = Math.min(1, (now - start) / fade);
38842
+ const step = (now2) => {
38843
+ this.modelAlpha = Math.min(1, (now2 - start) / fade);
38112
38844
  this.paintData();
38113
38845
  if (this.modelAlpha < 1) {
38114
38846
  this.introRaf = requestAnimationFrame(step);
@@ -38631,10 +39363,13 @@ ${overlayScrollbarCss(".vela-sd-pane")}
38631
39363
  this.animator.start();
38632
39364
  }
38633
39365
  } else if (!last2 || bar.time > last2.time) {
39366
+ const vp = this.coords.getViewport();
39367
+ const holdView = n > 0 && vp.rightOffset < 0 && this.scrollTargetRO === null;
38634
39368
  this.bars.push(bar);
38635
39369
  this.syncLiveEase(bar);
38636
39370
  if (this.coords.barInterval > 0) this.coords.appendBar(bar.time);
38637
39371
  else this.coords.setBars(this.bars.map((b) => b.time));
39372
+ if (holdView) this.coords.setViewport({ ...vp, rightOffset: vp.rightOffset - 1 });
38638
39373
  } else {
38639
39374
  return;
38640
39375
  }
@@ -39316,10 +40051,10 @@ ${overlayScrollbarCss(".vela-sd-pane")}
39316
40051
  if (this.markPulseRaf !== null || typeof requestAnimationFrame !== "function") return;
39317
40052
  const tick = () => {
39318
40053
  this.markPulseRaf = null;
39319
- const now = frameNow();
39320
- if (this.scene.marksFlash && this.scene.marksFlash.until <= now) this.scene.marksFlash = null;
40054
+ const now2 = frameNow();
40055
+ if (this.scene.marksFlash && this.scene.marksFlash.until <= now2) this.scene.marksFlash = null;
39321
40056
  this.scheduler?.invalidate(2 /* Chrome */);
39322
- const pulsing = this.scene.marksHoverKey !== null && now - this.scene.marksHoverSince < MARK_PULSE_MS;
40057
+ const pulsing = this.scene.marksHoverKey !== null && now2 - this.scene.marksHoverSince < MARK_PULSE_MS;
39323
40058
  if (pulsing || this.scene.marksFlash !== null) this.markPulseRaf = requestAnimationFrame(tick);
39324
40059
  };
39325
40060
  this.markPulseRaf = requestAnimationFrame(tick);
@@ -39880,9 +40615,10 @@ ${overlayScrollbarCss(".vela-sd-pane")}
39880
40615
  const ext = this.externalCross;
39881
40616
  if (!ext || this.coords.barCount === 0) return null;
39882
40617
  const logical = Math.floor(this.coords.timeToLogical(ext.time));
39883
- if (logical < 0 || logical >= this.coords.barCount) return null;
39884
- const x = this.coords.logicalToX(logical);
39885
- if (!Number.isFinite(x) || x < 0 || x > this.coords.width) return null;
40618
+ if (!(logical < this.coords.barCount)) return null;
40619
+ const x = this.coords.logicalToX(Math.max(logical, -1));
40620
+ if (!Number.isFinite(x)) return null;
40621
+ const line = logical >= 0 && x >= 0 && x <= this.coords.width;
39886
40622
  let y = null;
39887
40623
  if (ext.price != null) {
39888
40624
  const pricePane = this.scene.orderedPanes().find((p) => p.kind === "price");
@@ -39891,7 +40627,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
39891
40627
  if (!Number.isFinite(y) || y < pricePane.bounds.top || y > pricePane.bounds.top + pricePane.bounds.height) y = null;
39892
40628
  }
39893
40629
  }
39894
- return { x, y, time: this.coords.logicalToTime(logical), price: y != null ? ext.price : null };
40630
+ return { x, y, time: this.coords.logicalToTime(Math.max(logical, 0)), price: y != null ? ext.price : null, line };
39895
40631
  }
39896
40632
  /** Set the sticky magnet mode (called by the drawings toolbar's 3-state button). */
39897
40633
  setSnapMode(mode) {
@@ -44596,6 +45332,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
44596
45332
  this.panesControl = new PanesControl(this.orchestrator);
44597
45333
  this.drawingsControl = new DrawingsControl(this.orchestrator.drawings);
44598
45334
  this.marksControl = new MarksControl(this.orchestrator.marks);
45335
+ this.replayControl = new ReplayControl(this.orchestrator);
44599
45336
  }
44600
45337
  /**
44601
45338
  * Register a scripting engine so `addIndicator({ language })` can run that
@@ -44835,6 +45572,16 @@ ${overlayScrollbarCss(".vela-sd-pane")}
44835
45572
  get marks() {
44836
45573
  return this.marksControl;
44837
45574
  }
45575
+ /**
45576
+ * The chart's bar-replay control surface: rewind to a past bar and reveal the
45577
+ * following ones by hand or on a timer, from the history already loaded —
45578
+ * `await chart.replay.start({ from })`, `chart.replay.play(2000)`, `chart.replay.step()`,
45579
+ * `chart.replay.stop()`. Revealed bars behave like live bars for indicators; live
45580
+ * updates pause meanwhile. Follow it with the `replay:*` events.
45581
+ */
45582
+ get replay() {
45583
+ return this.replayControl;
45584
+ }
44838
45585
  on(event, handler) {
44839
45586
  return this.orchestrator.events.on(event, handler);
44840
45587
  }
@@ -44893,6 +45640,28 @@ ${overlayScrollbarCss(".vela-sd-pane")}
44893
45640
  return element;
44894
45641
  }
44895
45642
 
45643
+ // src/core/replayTicks.ts
45644
+ function lowerTimeframeTicks(chart, timeframe) {
45645
+ return async (bar, { end, signal }) => {
45646
+ const { symbol, session } = chart.market;
45647
+ const resolved = symbol ? chart.data.resolve(symbol) : null;
45648
+ const provider = resolved ? chart.data.providerInstance(resolved.provider) : void 0;
45649
+ if (!resolved || !provider) return [];
45650
+ const bars = await provider.getBars(resolved.ticker, timeframe, { from: bar.time, to: end - 1, ...session ? { session } : {} });
45651
+ if (signal.aborted) return [];
45652
+ return barsToTicks(bars.filter((b) => b.time >= bar.time && b.time < end));
45653
+ };
45654
+ }
45655
+ function barsToTicks(bars) {
45656
+ return bars.map((b) => ({
45657
+ price: b.close,
45658
+ open: b.open,
45659
+ high: b.high,
45660
+ low: b.low,
45661
+ ...b.volume === void 0 ? {} : { volume: b.volume }
45662
+ }));
45663
+ }
45664
+
44896
45665
  // src/widget/topbar-composition.ts
44897
45666
  var TOPBAR_BUILTIN_IDS = ["symbol", "timeframes", "style", "layout", "indicators", "actions", "undo-redo", "alerts", "panels", "screenshot"];
44898
45667
 
@@ -45824,8 +46593,8 @@ ${overlayScrollbarCss(".vela-sd-pane")}
45824
46593
  /** Block until the spacing window opens and any pause has elapsed, then reserve the next slot. */
45825
46594
  async waitForTurn() {
45826
46595
  for (; ; ) {
45827
- const now = this.clock.now();
45828
- const wait = Math.max(this.pausedUntil - now, this.nextStartAt - now);
46596
+ const now2 = this.clock.now();
46597
+ const wait = Math.max(this.pausedUntil - now2, this.nextStartAt - now2);
45829
46598
  if (wait <= 0) break;
45830
46599
  await this.clock.delay(wait);
45831
46600
  }
@@ -46286,6 +47055,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
46286
47055
  exports.NativeRenderer = NativeRenderer;
46287
47056
  exports.OVERRIDABLE_TOPBAR_IDS = OVERRIDABLE_TOPBAR_IDS;
46288
47057
  exports.RendererControl = RendererControl;
47058
+ exports.ReplayControl = ReplayControl;
46289
47059
  exports.SERIES_LINE = SERIES_LINE;
46290
47060
  exports.SESSION_OFF = SESSION_OFF;
46291
47061
  exports.SESSION_POST = SESSION_POST;
@@ -46300,6 +47070,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
46300
47070
  exports.VALID = VALID;
46301
47071
  exports.Vela = Vela;
46302
47072
  exports.WARNING = WARNING;
47073
+ exports.barsToTicks = barsToTicks;
46303
47074
  exports.buildColorPicker = buildColorPicker;
46304
47075
  exports.buildToolbar = buildToolbar;
46305
47076
  exports.categoricalColor = categoricalColor;
@@ -46318,6 +47089,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
46318
47089
  exports.inputVisible = inputVisible;
46319
47090
  exports.legendActions = legendActions;
46320
47091
  exports.legendCallouts = legendCallouts;
47092
+ exports.lowerTimeframeTicks = lowerTimeframeTicks;
46321
47093
  exports.nativeIndicatorDescriptors = nativeIndicatorDescriptors;
46322
47094
  exports.nativeIndicatorTypes = nativeIndicatorTypes;
46323
47095
  exports.normalizeSettingsRow = normalizeSettingsRow;