@luxalgo/vela 0.7.8 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/NOTICE +28 -28
  2. package/dist/{DataProvider-CGa4KHRa.d.cts → DataProvider-BrlxntPN.d.cts} +1 -1
  3. package/dist/{DataProvider-CmWUtDVL.d.ts → DataProvider-DwFIoV4r.d.ts} +1 -1
  4. package/dist/{chunk-WTWGEB7K.js → chunk-2IAETM2I.js} +260 -2
  5. package/dist/{chunk-CAFCLMPF.js → chunk-BZQM2XO7.js} +1 -0
  6. package/dist/{chunk-M6GP7DAX.js → chunk-E3GU6AUI.js} +459 -37
  7. package/dist/{chunk-BFA32GOU.js → chunk-EZ5FWVLA.js} +5 -2
  8. package/dist/{chunk-PVGWNF7V.js → chunk-KA77UTRF.js} +2 -2
  9. package/dist/{chunk-N4ZZYLMP.js → chunk-QBPUT5SU.js} +686 -251
  10. package/dist/{contributions-DF_Ea07m.d.cts → contributions-CtlptxR8.d.cts} +187 -5
  11. package/dist/{contributions-nKOSeLy8.d.ts → contributions-tjNExo1o.d.ts} +187 -5
  12. package/dist/index.cjs +966 -247
  13. package/dist/index.d.cts +24 -8
  14. package/dist/index.d.ts +24 -8
  15. package/dist/index.js +28 -4
  16. package/dist/{options-D1AJKsn_.d.ts → options-BnEPUuVV.d.cts} +1 -1
  17. package/dist/{options-D1AJKsn_.d.cts → options-BnEPUuVV.d.ts} +1 -1
  18. package/dist/{plugin-6F04gjQe.d.cts → plugin-BWxJRKzS.d.cts} +10 -4
  19. package/dist/{plugin-BLsNipkc.d.ts → plugin-e2WUEtiI.d.ts} +10 -4
  20. package/dist/plugin.cjs +1 -0
  21. package/dist/plugin.d.cts +4 -4
  22. package/dist/plugin.d.ts +4 -4
  23. package/dist/plugin.js +2 -2
  24. package/dist/providers/binance.d.cts +2 -2
  25. package/dist/providers/binance.d.ts +2 -2
  26. package/dist/providers/coinbase.d.cts +2 -2
  27. package/dist/providers/coinbase.d.ts +2 -2
  28. package/dist/providers/hyperliquid.d.cts +2 -2
  29. package/dist/providers/hyperliquid.d.ts +2 -2
  30. package/dist/{statusline-model-DAFyqVmR.d.ts → statusline-model-DFY7tuHj.d.cts} +5 -3
  31. package/dist/{statusline-model-BoWJ3cWQ.d.cts → statusline-model-zKXtGIjj.d.ts} +5 -3
  32. package/dist/ui.cjs +263 -0
  33. package/dist/ui.d.cts +75 -2
  34. package/dist/ui.d.ts +75 -2
  35. package/dist/ui.js +3 -3
  36. package/dist/vela.global.js +968 -249
  37. package/dist/vela.global.min.js +73 -71
  38. package/dist/widget.cjs +1411 -292
  39. package/dist/widget.d.cts +26 -7
  40. package/dist/widget.d.ts +26 -7
  41. package/dist/widget.js +7 -7
  42. package/dist/workspace.cjs +1414 -292
  43. package/dist/workspace.d.cts +124 -6
  44. package/dist/workspace.d.ts +124 -6
  45. package/dist/workspace.js +6 -6
  46. package/package.json +1 -1
@@ -1,8 +1,8 @@
1
- import { parseSymbol, priceStyleIds, BUILTIN_PRICE_STYLES, tzButtonLabel, SecondClock, resolveTimezone, timezoneMenuRows, isGroupRow, defaultMemberOf, groupKeyOf, groupMembers, LEGEND_AT_TOP_ATTR, normalizeSession, Vela, normalizeTimezone, isExchangeTimezone, TypedEventBus, MultiProviderFeed, registerBuiltinChartTypes, resolveTheme, buildToolbar, createCustomMark, createAttributionMark, DrawingToolbar, applyAttributionMarkTheme, sharedBarStore, timeframeToMs } from './chunk-N4ZZYLMP.js';
2
- import { chartType, resolveTopbarComposition, topbarActionOverride, TOPBAR_BUILTIN_IDS, widgetActions, SidePanel, sidePanels, DEFAULT_PANEL_ORDER, symbolRanking, resolveEngines, legendActionsProviderFor, legendCalloutsProviderFor, statePersistenceHandlers, topbarHas, rendererDefaults, widgetAttachments, getDrawingType, inputDeltas } from './chunk-BFA32GOU.js';
3
- import { Tooltip, KeymapManager, isEditableTarget, Drawer } from './chunk-PVGWNF7V.js';
4
- import { Menu, Dialog, CalloutBubble, applyPlotOverlayTokens, ensureUIHost } from './chunk-WTWGEB7K.js';
5
- import { registerIcon, svg16, injectStyles, iconEl, iconMarkup, iconAt, SESSION_OFF, SESSION_POST, SESSION_PRE, icon, categoricalColor } from './chunk-CAFCLMPF.js';
1
+ import { parseSymbol, priceStyleIds, BUILTIN_PRICE_STYLES, tzButtonLabel, SecondClock, resolveTimezone, timezoneMenuRows, isGroupRow, defaultMemberOf, groupKeyOf, groupMembers, LEGEND_AT_TOP_ATTR, normalizeSession, Vela, normalizeTimezone, isExchangeTimezone, TypedEventBus, timeframeToMs, MultiProviderFeed, registerBuiltinChartTypes, resolveTheme, buildToolbar, createCustomMark, createAttributionMark, DrawingToolbar, applyAttributionMarkTheme, sharedBarStore } from './chunk-QBPUT5SU.js';
2
+ import { chartType, resolveTopbarComposition, topbarActionOverride, TOPBAR_BUILTIN_IDS, widgetActions, SidePanel, sidePanels, DEFAULT_PANEL_ORDER, symbolRanking, resolveEngines, legendActionsProviderFor, legendCalloutsProviderFor, statePersistenceHandlers, topbarHas, rendererDefaults, mobilePlacement, widgetAttachments, getDrawingType, inputDeltas } from './chunk-EZ5FWVLA.js';
3
+ import { Tooltip, KeymapManager, isEditableTarget, Drawer } from './chunk-KA77UTRF.js';
4
+ import { Menu, Dialog, CalloutBubble, applyPlotOverlayTokens, ensureUIHost } from './chunk-2IAETM2I.js';
5
+ import { registerIcon, svg16, injectStyles, iconEl, iconMarkup, iconAt, SESSION_OFF, SESSION_POST, SESSION_PRE, icon, categoricalColor } from './chunk-BZQM2XO7.js';
6
6
  import { baseOf } from './chunk-W4EJWLEO.js';
7
7
 
8
8
  // src/widget/timeframe.ts
@@ -3925,6 +3925,7 @@ function sanitizeCell(raw) {
3925
3925
  if (typeof c.bars === "number" && Number.isFinite(c.bars) && c.bars > 0) out.bars = c.bars;
3926
3926
  if (c.session === "regular" || c.session === "extended") out.session = c.session;
3927
3927
  if (typeof c.watermark === "boolean") out.watermark = c.watermark;
3928
+ if (typeof c.replayWatermark === "boolean") out.replayWatermark = c.replayWatermark;
3928
3929
  if (typeof c.indicatorTitles === "boolean") out.indicatorTitles = c.indicatorTitles;
3929
3930
  if (typeof c.indicatorValues === "boolean") out.indicatorValues = c.indicatorValues;
3930
3931
  if (c.rendererConfig != null && typeof c.rendererConfig === "object") out.rendererConfig = c.rendererConfig;
@@ -4208,9 +4209,15 @@ var CSS10 = `
4208
4209
  }
4209
4210
  .vela-statusline .vela-sl-symbol { font-weight: 600; font-size: var(--vela-font-size-lg); }
4210
4211
  .vela-statusline .vela-sl-meta { color: var(--vela-fg-muted); font-size: var(--vela-font-size-md); font-weight: 600; }
4212
+ /* The meta opens with "\xB7 " \u2014 sit its dot one space-width after the ticker, as far as the
4213
+ * venue sits after it, not a full row gap away. */
4214
+ .vela-statusline .vela-sl-symbol + .vela-sl-meta { margin-left: calc(var(--vela-space-1) - var(--vela-space-2)); }
4211
4215
  /* Market status badge \u2014 a kit callout bubble (icon-only 16px circle, label on hover
4212
- * via the kit tooltip); the session tint is applied per status in setMarketStatus. */
4213
- .vela-statusline .vela-sl-market { align-self: center; }
4216
+ * via the kit tooltip); the session tint is applied per status in setMarketStatus. While
4217
+ * the chart replays past bars it wears the replay badge instead (the inverse chip). */
4218
+ .vela-statusline .vela-sl-market { align-self: center; display: inline-flex; }
4219
+ .vela-statusline .vela-sl-market > [hidden] { display: none !important; }
4220
+ .vela-statusline .vela-sl-replay-badge, .vela-statusline .vela-sl-replay-badge svg { display: block; width: 16px; height: 16px; }
4214
4221
  .vela-statusline .vela-sl-ohlc { display: flex; gap: var(--vela-space-1); color: var(--vela-fg-muted); }
4215
4222
  .vela-statusline .vela-sl-ohlc b { color: var(--vela-fg); font-weight: 500; }
4216
4223
  /* The change value wears the SAME ink as the OHLC values (set inline per render) \u2014
@@ -4263,6 +4270,8 @@ var MARKET_LABELS = {
4263
4270
  closed: "Market Closed",
4264
4271
  holiday: "Market Holiday"
4265
4272
  };
4273
+ var REPLAY_LABEL = "Replay Mode";
4274
+ var REPLAY_BADGE_SVG = '<svg viewBox="0 0 16 16" width="16" height="16" aria-hidden="true"><circle cx="8" cy="8" r="8" style="fill: var(--vela-selected-bg)"/><path d="M11.7 4.75v6.5L7.2 8zM7.2 4.75v6.5L2.7 8z" style="fill: var(--vela-selected-fg); stroke: var(--vela-selected-fg); stroke-width: 0.8; stroke-linejoin: round"/></svg>';
4266
4275
  var MARKET_INKS = {
4267
4276
  open: "var(--vela-up)",
4268
4277
  pre: SESSION_PRE,
@@ -4301,6 +4310,9 @@ var Statusline = class {
4301
4310
  this.host = host;
4302
4311
  this.iconFor = iconFor;
4303
4312
  this.parts = { logo: true, name: true, market: true, ohlc: true, change: true };
4313
+ this.marketStatus = "open";
4314
+ /** The chart replays past bars — the badge shows the replay mode (see {@link setReplaying}). */
4315
+ this.replaying = false;
4304
4316
  /** The right-click action menu — present once a host wires it via {@link attachMenu}. */
4305
4317
  this.menu = null;
4306
4318
  this.menuHooks = null;
@@ -4352,8 +4364,13 @@ var Statusline = class {
4352
4364
  label: MARKET_LABELS.open,
4353
4365
  host
4354
4366
  });
4355
- this.marketEl = this.marketBubble.el;
4356
- this.marketEl.classList.add("vela-sl-market");
4367
+ this.replayBadge = doc.createElement("span");
4368
+ this.replayBadge.className = "vela-sl-replay-badge";
4369
+ this.replayBadge.innerHTML = REPLAY_BADGE_SVG;
4370
+ this.replayBadge.hidden = true;
4371
+ this.marketEl = doc.createElement("span");
4372
+ this.marketEl.className = "vela-sl-market";
4373
+ this.marketEl.append(this.marketBubble.el, this.replayBadge);
4357
4374
  this.ohlcEl = doc.createElement("span");
4358
4375
  this.ohlcEl.className = "vela-sl-ohlc";
4359
4376
  this.changeEl = doc.createElement("span");
@@ -4475,6 +4492,25 @@ var Statusline = class {
4475
4492
  /** Dress the market badge for a session state: its icon, tinted circle, and the
4476
4493
  * hover label. Callers with no session model leave the constructor's 'open'. */
4477
4494
  setMarketStatus(status) {
4495
+ this.marketStatus = status;
4496
+ this.dressBadge();
4497
+ }
4498
+ /** The chart entered or left a bar replay: the badge shows the replay mode meanwhile
4499
+ * (the session state of a past bar says nothing about the market now). */
4500
+ setReplaying(replaying) {
4501
+ if (replaying === this.replaying) return;
4502
+ this.replaying = replaying;
4503
+ this.dressBadge();
4504
+ }
4505
+ dressBadge() {
4506
+ this.marketBubble.el.hidden = this.replaying;
4507
+ this.replayBadge.hidden = !this.replaying;
4508
+ if (this.replaying) {
4509
+ this.marketEl.dataset.status = "replay";
4510
+ this.marketTip.setContent(REPLAY_LABEL);
4511
+ return;
4512
+ }
4513
+ const status = this.marketStatus;
4478
4514
  this.marketEl.dataset.status = status;
4479
4515
  const ink = MARKET_INKS[status];
4480
4516
  this.marketBubble.set({
@@ -4547,11 +4583,23 @@ var Statusline = class {
4547
4583
  this.lastBar = b;
4548
4584
  if (!this.hoverBar) this.render();
4549
4585
  }),
4586
+ // A replay cut or restore replaces the newest bar without a `bar` event.
4587
+ chart.on("replay:start", () => {
4588
+ this.lastBar = null;
4589
+ this.setReplaying(true);
4590
+ this.render();
4591
+ }),
4592
+ chart.on("replay:end", () => {
4593
+ this.lastBar = null;
4594
+ this.setReplaying(false);
4595
+ this.render();
4596
+ }),
4550
4597
  chart.renderer.onCrosshairMove((e) => {
4551
4598
  this.hoverBar = e.ohlc;
4552
4599
  this.render();
4553
4600
  })
4554
4601
  );
4602
+ this.setReplaying(chart.replay.state.active);
4555
4603
  this.render();
4556
4604
  }
4557
4605
  destroy() {
@@ -4620,6 +4668,7 @@ var CSS11 = `
4620
4668
  left: var(--vela-toolbar-gutter, 0px);
4621
4669
  right: var(--vela-scale-gutter, 0px);
4622
4670
  display: flex;
4671
+ flex-direction: column;
4623
4672
  align-items: center;
4624
4673
  justify-content: center;
4625
4674
  overflow: hidden;
@@ -4633,6 +4682,14 @@ var CSS11 = `
4633
4682
  user-select: none;
4634
4683
  white-space: nowrap;
4635
4684
  }
4685
+ .vela-watermark-replay {
4686
+ display: inline-flex;
4687
+ align-items: center;
4688
+ gap: 0.25em;
4689
+ margin-top: 0.1em;
4690
+ font-size: 0.8em;
4691
+ }
4692
+ .vela-watermark [hidden] { display: none !important; }
4636
4693
  `;
4637
4694
  function watermarkFontPx(availPx, textPxAtMax) {
4638
4695
  if (textPxAtMax <= 0) return MAX_FONT_PX;
@@ -4643,6 +4700,10 @@ var Watermark = class {
4643
4700
  this.resizeObserver = null;
4644
4701
  /** The host's visibility preference (the persisted watermark toggle). */
4645
4702
  this.shown = true;
4703
+ /** The host's preference for the replay line (its own persisted toggle). */
4704
+ this.replayShown = true;
4705
+ /** The chart is replaying past bars. */
4706
+ this.replaying = false;
4646
4707
  /** A bar load is in flight with nothing painted — the loading affordance owns the
4647
4708
  * canvas, so the mark stays out of its way. Starts true: the FIRST `load:start`
4648
4709
  * fires during chart construction, before any subscriber can see it. */
@@ -4652,7 +4713,10 @@ var Watermark = class {
4652
4713
  this.el.className = "vela-watermark";
4653
4714
  this.el.dataset.velaScreenshot = "under";
4654
4715
  this.text = host.ownerDocument.createElement("span");
4655
- this.el.appendChild(this.text);
4716
+ this.replayLine = host.ownerDocument.createElement("span");
4717
+ this.replayLine.className = "vela-watermark-replay";
4718
+ this.replayLine.append(iconEl("replay", host.ownerDocument), host.ownerDocument.createTextNode("Replay"));
4719
+ this.el.append(this.text, this.replayLine);
4656
4720
  host.appendChild(this.el);
4657
4721
  if (typeof ResizeObserver !== "undefined") {
4658
4722
  this.resizeObserver = new ResizeObserver(() => this.fit());
@@ -4665,13 +4729,26 @@ var Watermark = class {
4665
4729
  this.shown = visible;
4666
4730
  this.sync();
4667
4731
  }
4732
+ /** Show/hide the replay line (the host's toggle); it only ever shows while replaying. */
4733
+ setReplayVisible(visible) {
4734
+ this.replayShown = visible;
4735
+ this.sync();
4736
+ }
4737
+ /** The chart entered or left a bar replay. */
4738
+ setReplaying(replaying) {
4739
+ this.replaying = replaying;
4740
+ this.sync();
4741
+ }
4668
4742
  /** Loading and the watermark never share the canvas — hidden while a load is up. */
4669
4743
  setLoading(loading) {
4670
4744
  this.loading = loading;
4671
4745
  this.sync();
4672
4746
  }
4673
4747
  sync() {
4674
- this.el.style.display = this.shown && !this.loading ? "" : "none";
4748
+ const replay = this.replayShown && this.replaying;
4749
+ this.text.hidden = !this.shown;
4750
+ this.replayLine.hidden = !replay;
4751
+ this.el.style.display = (this.shown || replay) && !this.loading ? "" : "none";
4675
4752
  }
4676
4753
  update(symbol, timeframe) {
4677
4754
  this.text.textContent = symbol ? `${parseSymbol(symbol).ticker} \xB7 ${timeframeLabel(timeframe)}` : "";
@@ -5707,8 +5784,12 @@ var ChartCell = class {
5707
5784
  this.indicatorValuesOn = seed.indicatorValues ?? true;
5708
5785
  if (!this.indicatorValuesOn) this.inner.renderer.set("indicatorValues", false);
5709
5786
  this.watermarkOn = seed.watermark ?? deps.watermark;
5787
+ this.replayWatermarkOn = seed.replayWatermark ?? true;
5710
5788
  this.watermark = deps.watermark ? new Watermark(this.host, symbol ?? "", seed.timeframe ?? "60") : null;
5711
5789
  if (!this.watermarkOn) this.watermark?.setVisible(false);
5790
+ if (!this.replayWatermarkOn) this.watermark?.setReplayVisible(false);
5791
+ this.inner.on("replay:start", () => this.watermark?.setReplaying(true));
5792
+ this.inner.on("replay:end", () => this.watermark?.setReplaying(false));
5712
5793
  this.statusline = deps.statusline ? new Statusline(this.host, symbol ?? "", (sym) => this.inner?.data.symbolIcon(sym)) : null;
5713
5794
  this.statusline?.setMeta(seed.timeframe ?? "60", this.state.provider ?? "");
5714
5795
  this.statusline?.onChart(this.inner);
@@ -6022,6 +6103,13 @@ var ChartCell = class {
6022
6103
  id: "visible",
6023
6104
  get: () => this.watermarkOn,
6024
6105
  set: (v) => this.setWatermarkVisible(v)
6106
+ },
6107
+ {
6108
+ kind: "toggle",
6109
+ label: "Replay watermark",
6110
+ id: "replay",
6111
+ get: () => this.replayWatermarkOn,
6112
+ set: (v) => this.setReplayWatermarkVisible(v)
6025
6113
  }
6026
6114
  ]
6027
6115
  };
@@ -6090,6 +6178,12 @@ var ChartCell = class {
6090
6178
  this.watermark?.setVisible(visible);
6091
6179
  this.deps.onStateDirty();
6092
6180
  }
6181
+ /** Show/hide the "Replay" line under this cell's watermark while it replays (persisted per cell). */
6182
+ setReplayWatermarkVisible(visible) {
6183
+ this.replayWatermarkOn = visible;
6184
+ this.watermark?.setReplayVisible(visible);
6185
+ this.deps.onStateDirty();
6186
+ }
6093
6187
  /** Show/hide this cell's indicator titles — the in-chart legend rows (persisted per cell). */
6094
6188
  setIndicatorTitlesVisible(visible) {
6095
6189
  this.indicatorTitlesOn = visible;
@@ -6563,6 +6657,7 @@ var ChartCell = class {
6563
6657
  if (!this.inner || this.destroyed) return;
6564
6658
  if (cs.priceStyle && cs.priceStyle !== this.priceStyle) this.setPriceStyle(cs.priceStyle);
6565
6659
  if (cs.watermark !== void 0 && cs.watermark !== this.watermarkOn) this.setWatermarkVisible(cs.watermark);
6660
+ if (cs.replayWatermark !== void 0 && cs.replayWatermark !== this.replayWatermarkOn) this.setReplayWatermarkVisible(cs.replayWatermark);
6566
6661
  if (cs.indicatorTitles !== void 0 && cs.indicatorTitles !== this.indicatorTitlesOn) this.setIndicatorTitlesVisible(cs.indicatorTitles);
6567
6662
  if (cs.indicatorValues !== void 0 && cs.indicatorValues !== this.indicatorValuesOn) this.setIndicatorValuesVisible(cs.indicatorValues);
6568
6663
  if (cs.rendererConfig != null) this.inner.renderer.applyConfig(cs.rendererConfig);
@@ -6597,6 +6692,7 @@ var ChartCell = class {
6597
6692
  ...live ? { symbol: live.symbol, provider: live.provider, timeframe: live.timeframe } : {},
6598
6693
  priceStyle: this.priceStyle,
6599
6694
  watermark: this.watermarkOn,
6695
+ replayWatermark: this.replayWatermarkOn,
6600
6696
  indicatorTitles: this.indicatorTitlesOn,
6601
6697
  indicatorValues: this.indicatorValuesOn,
6602
6698
  rendererConfig: this.inner?.renderer.getConfig() ?? void 0,
@@ -6639,6 +6735,298 @@ var ChartCell = class {
6639
6735
  }
6640
6736
  };
6641
6737
 
6738
+ // src/workspace/WorkspaceReplay.ts
6739
+ var MAX_BARS_PER_STEP = 1e5;
6740
+ var WorkspaceReplay = class {
6741
+ constructor(host) {
6742
+ this.host = host;
6743
+ this.bus = new TypedEventBus();
6744
+ /** The running session: the shared replay time (every chart shows the bars closed by it). */
6745
+ this.session = null;
6746
+ this.playing = false;
6747
+ this.intervalMs = 1e3;
6748
+ this.timer = null;
6749
+ /** Cells whose replay a symbol switch ended — restarted at the shared time once their bars land. */
6750
+ this.rejoining = /* @__PURE__ */ new Set();
6751
+ this.cellSubs = /* @__PURE__ */ new Map();
6752
+ /** Inside a group step: chart events are the controller's own doing. */
6753
+ this.stepping = false;
6754
+ /** Inside `finish`: the charts' `replay:end` events are the controller's own doing. */
6755
+ this.stopping = false;
6756
+ /** Driving a chart's own timer: its play/pause events are not the user's. */
6757
+ this.muted = 0;
6758
+ /** A chart finished during a group step — the session ends right after it. */
6759
+ this.finishedDuringStep = false;
6760
+ for (const cell of host.cells()) this.watch(cell);
6761
+ this.offHost = host.onCells((e) => {
6762
+ if (e.kind === "created") this.onCellCreated(e.id);
6763
+ else if (e.kind === "destroyed") this.onCellDestroyed(e.id);
6764
+ });
6765
+ }
6766
+ /**
6767
+ * Enter replay on every cell (or seek, when already replaying) — paused. Resolves once
6768
+ * every chart shows its rewound history (a chart whose history does not reach that far
6769
+ * loads older bars first). A chart with nothing to replay after the shared time stays
6770
+ * as it is.
6771
+ */
6772
+ async start(opts) {
6773
+ const cells = this.host.cells();
6774
+ const ref = cells.find((c) => c.id === (opts.cell ?? this.host.activeId())) ?? cells[0];
6775
+ if (!ref) return;
6776
+ if (this.playing) this.pause();
6777
+ this.rejoining.clear();
6778
+ await ref.chart.replay.start({ from: opts.from });
6779
+ const cursor = ref.chart.replay.state.cursorTime;
6780
+ if (cursor == null) return;
6781
+ const session = { clock: barClose(cursor, ref.chart.market.timeframe) };
6782
+ this.session = session;
6783
+ await Promise.all(this.host.cells().filter((c) => c.id !== ref.id).map((c) => this.startCell(c, session.clock)));
6784
+ if (this.session !== session) return;
6785
+ this.syncPlayback();
6786
+ const s = this.activeChart()?.replay.state;
6787
+ this.bus.emit("replay:start", { cursorTime: s?.cursorTime ?? cursor, remaining: s?.remaining ?? 0 });
6788
+ }
6789
+ /** Reveal the next bar(s): the clock moves to the next bar close on any chart. Returns false when nothing is left. */
6790
+ step() {
6791
+ const solo = this.solo();
6792
+ return solo ? solo.replay.step() : this.stepGroup();
6793
+ }
6794
+ /** Reveal the next update — in a single-chart layout, the chart's `stepUpdate()` (tick replay); otherwise {@link step}. */
6795
+ stepUpdate() {
6796
+ const solo = this.solo();
6797
+ return solo ? solo.replay.stepUpdate() : this.stepGroup();
6798
+ }
6799
+ /** Advance one update every `intervalMs` (default: the last pace, initially 1000). Calling it again while playing changes the pace. */
6800
+ play(intervalMs) {
6801
+ if (!this.session) return this;
6802
+ if (intervalMs !== void 0) {
6803
+ if (!(Number.isFinite(intervalMs) && intervalMs > 0)) {
6804
+ console.warn(`[vela] workspace.replay.play(${intervalMs}) ignored \u2014 the interval must be a positive number of ms`);
6805
+ return this;
6806
+ }
6807
+ this.intervalMs = intervalMs;
6808
+ }
6809
+ this.playing = true;
6810
+ this.clearTimer();
6811
+ this.syncPlayback();
6812
+ this.bus.emit("replay:play", { intervalMs: this.intervalMs });
6813
+ return this;
6814
+ }
6815
+ pause() {
6816
+ if (!this.playing) return this;
6817
+ this.playing = false;
6818
+ this.syncPlayback();
6819
+ this.bus.emit("replay:pause", void 0);
6820
+ return this;
6821
+ }
6822
+ /** Leave replay on every chart: full history back, live updates resumed. */
6823
+ stop() {
6824
+ this.finish("stopped");
6825
+ return this;
6826
+ }
6827
+ /** `active`/`playing`/`intervalMs` for the whole workspace; the cursor fields read the ACTIVE cell's chart. */
6828
+ get state() {
6829
+ const s = this.session ? this.activeChart()?.replay.state : void 0;
6830
+ return {
6831
+ active: this.session !== null,
6832
+ playing: this.playing,
6833
+ cursorTime: s?.cursorTime ?? null,
6834
+ remaining: s?.remaining ?? 0,
6835
+ nextTime: s?.nextTime ?? null,
6836
+ intervalMs: this.intervalMs
6837
+ };
6838
+ }
6839
+ /** The active cell's replayable history (`chart.replay.bounds`). */
6840
+ get bounds() {
6841
+ return this.activeChart()?.replay.bounds ?? null;
6842
+ }
6843
+ on(event, handler) {
6844
+ return this.bus.on(event, handler);
6845
+ }
6846
+ destroy() {
6847
+ this.session = null;
6848
+ this.playing = false;
6849
+ this.clearTimer();
6850
+ this.offHost();
6851
+ for (const off of this.cellSubs.values()) off();
6852
+ this.cellSubs.clear();
6853
+ this.bus.clear();
6854
+ }
6855
+ // ── internals ──
6856
+ /** The one chart of a single-chart layout — it replays on its own, tick replay included. */
6857
+ solo() {
6858
+ const cells = this.host.cells();
6859
+ return cells.length === 1 ? cells[0].chart : null;
6860
+ }
6861
+ activeChart() {
6862
+ const cells = this.host.cells();
6863
+ return (cells.find((c) => c.id === this.host.activeId()) ?? cells[0])?.chart ?? null;
6864
+ }
6865
+ /** Rewind one chart to the shared time: every bar that closed by `clock`, none after. */
6866
+ async startCell(cell, clock) {
6867
+ const replay = cell.chart.replay;
6868
+ const from = lastOpenClosedBy(clock, cell.chart.market.timeframe);
6869
+ const bounds = replay.bounds;
6870
+ if (!bounds || bounds.last <= from) return;
6871
+ await replay.start({ from });
6872
+ }
6873
+ /** Move the clock to the next bar close on any chart; each chart reveals what closed by then. */
6874
+ stepGroup() {
6875
+ const session = this.session;
6876
+ if (!session) return false;
6877
+ const cells = this.host.cells();
6878
+ let next = Infinity;
6879
+ for (const c of cells) {
6880
+ const t = c.chart.replay.state.nextTime;
6881
+ if (t != null) next = Math.min(next, barClose(t, c.chart.market.timeframe));
6882
+ }
6883
+ if (!Number.isFinite(next)) return false;
6884
+ this.stepping = true;
6885
+ try {
6886
+ for (const c of cells) {
6887
+ const tf = c.chart.market.timeframe;
6888
+ for (let n = 0; n < MAX_BARS_PER_STEP; n += 1) {
6889
+ const t = c.chart.replay.state.nextTime;
6890
+ if (t == null || barClose(t, tf) > next) break;
6891
+ if (!c.chart.replay.step()) break;
6892
+ }
6893
+ }
6894
+ } finally {
6895
+ this.stepping = false;
6896
+ }
6897
+ session.clock = next;
6898
+ if (this.finishedDuringStep) {
6899
+ this.finishedDuringStep = false;
6900
+ this.finish("finished");
6901
+ return true;
6902
+ }
6903
+ const s = this.activeChart()?.replay.state;
6904
+ if (s?.cursorTime != null) this.bus.emit("replay:step", { cursorTime: s.cursorTime, remaining: s.remaining });
6905
+ return true;
6906
+ }
6907
+ /** Put the timers in the state `playing` asks for: a single chart runs its own (it paces
6908
+ * ticks), several run on the controller's clock. */
6909
+ syncPlayback() {
6910
+ const solo = this.solo();
6911
+ const run = this.playing && this.session !== null;
6912
+ this.muted += 1;
6913
+ try {
6914
+ for (const c of this.host.cells()) {
6915
+ if (c.chart === solo && run) {
6916
+ if (!c.chart.replay.state.playing || c.chart.replay.state.intervalMs !== this.intervalMs) c.chart.replay.play(this.intervalMs);
6917
+ } else if (c.chart.replay.state.playing) {
6918
+ c.chart.replay.pause();
6919
+ }
6920
+ }
6921
+ } finally {
6922
+ this.muted -= 1;
6923
+ }
6924
+ if (!run || solo) this.clearTimer();
6925
+ else if (this.timer == null) this.schedule();
6926
+ }
6927
+ schedule() {
6928
+ this.timer = setTimeout(() => {
6929
+ this.timer = null;
6930
+ if (!this.playing || !this.session || this.solo()) return;
6931
+ if (this.stepGroup() || this.rejoining.size > 0) {
6932
+ if (this.playing && this.session && this.timer == null) this.schedule();
6933
+ } else {
6934
+ this.pause();
6935
+ }
6936
+ }, this.intervalMs);
6937
+ }
6938
+ clearTimer() {
6939
+ if (this.timer != null) clearTimeout(this.timer);
6940
+ this.timer = null;
6941
+ }
6942
+ /** End the session on every chart (idempotent). */
6943
+ finish(reason) {
6944
+ const had = this.session !== null;
6945
+ this.session = null;
6946
+ this.playing = false;
6947
+ this.clearTimer();
6948
+ this.rejoining.clear();
6949
+ this.stopping = true;
6950
+ try {
6951
+ for (const c of this.host.cells()) c.chart.replay.stop();
6952
+ } finally {
6953
+ this.stopping = false;
6954
+ }
6955
+ if (had) this.bus.emit("replay:end", { reason });
6956
+ }
6957
+ watch(cell) {
6958
+ const { id, chart } = cell;
6959
+ const isActive = () => (this.host.activeId() ?? this.host.cells()[0]?.id) === id;
6960
+ const offs = [
6961
+ chart.on("replay:end", ({ reason }) => {
6962
+ if (!this.session || this.stopping) return;
6963
+ if (reason === "market") this.rejoining.add(id);
6964
+ else if (this.stepping) this.finishedDuringStep || (this.finishedDuringStep = reason === "finished");
6965
+ else this.finish(reason);
6966
+ }),
6967
+ chart.on("market:changed", () => {
6968
+ if (!this.rejoining.delete(id) || !this.session) return;
6969
+ void this.startCell(cell, this.session.clock).then(() => this.syncPlayback());
6970
+ }),
6971
+ chart.on("replay:step", (e) => {
6972
+ if (this.session && !this.stepping && isActive()) this.bus.emit("replay:step", e);
6973
+ }),
6974
+ chart.on("replay:tick", (e) => {
6975
+ if (this.session && isActive()) this.bus.emit("replay:tick", e);
6976
+ }),
6977
+ // A single chart runs its own timer: a pause it takes on its own (a seek) is the session's.
6978
+ chart.on("replay:pause", () => {
6979
+ if (!this.session || this.muted > 0 || !this.playing || chart !== this.solo()) return;
6980
+ this.playing = false;
6981
+ this.bus.emit("replay:pause", void 0);
6982
+ })
6983
+ ];
6984
+ this.cellSubs.set(id, () => {
6985
+ for (const off of offs) off();
6986
+ });
6987
+ }
6988
+ onCellCreated(id) {
6989
+ const cell = this.host.cells().find((c) => c.id === id);
6990
+ if (!cell) return;
6991
+ this.watch(cell);
6992
+ const session = this.session;
6993
+ if (!session) return;
6994
+ this.rejoining.add(id);
6995
+ void cell.chart.ready().then(async () => {
6996
+ if (this.session !== session || !this.rejoining.delete(id)) return;
6997
+ await this.startCell(cell, session.clock);
6998
+ this.syncPlayback();
6999
+ });
7000
+ this.syncPlayback();
7001
+ }
7002
+ onCellDestroyed(id) {
7003
+ this.cellSubs.get(id)?.();
7004
+ this.cellSubs.delete(id);
7005
+ this.rejoining.delete(id);
7006
+ this.syncPlayback();
7007
+ }
7008
+ };
7009
+ function barClose(open, timeframe) {
7010
+ const months = monthSpan(timeframe);
7011
+ if (months > 0) {
7012
+ const d = new Date(open);
7013
+ return Date.UTC(d.getUTCFullYear(), d.getUTCMonth() + months, d.getUTCDate(), d.getUTCHours(), d.getUTCMinutes());
7014
+ }
7015
+ return open + timeframeToMs(timeframe ?? "60");
7016
+ }
7017
+ function lastOpenClosedBy(time, timeframe) {
7018
+ const months = monthSpan(timeframe);
7019
+ if (months > 0) {
7020
+ const d = new Date(time);
7021
+ return Date.UTC(d.getUTCFullYear(), d.getUTCMonth() - months, d.getUTCDate(), d.getUTCHours(), d.getUTCMinutes());
7022
+ }
7023
+ return time - timeframeToMs(timeframe ?? "60");
7024
+ }
7025
+ function monthSpan(timeframe) {
7026
+ const m = /^(\d*)M$/.exec((timeframe ?? "").trim());
7027
+ return m ? Number(m[1] || 1) : 0;
7028
+ }
7029
+
6642
7030
  // src/workspace/layouts.ts
6643
7031
  var registry = /* @__PURE__ */ new Map();
6644
7032
  function registerLayout(def) {
@@ -7095,16 +7483,16 @@ var MobileBar = class {
7095
7483
  host.appendChild(this.el);
7096
7484
  this.renderActions();
7097
7485
  }
7098
- /** Re-project the left-aligned contributed actions as icon-only stops in the
7099
- * indicators slot (call after registrations change). Right-aligned actions stay
7100
- * in the three-dots drawer — a primary stop is what `align: 'left'` opts into. */
7486
+ /** Re-project the contributed actions placed on the bar (left-aligned by default —
7487
+ * see `mobilePlacement`) as icon-only stops in the indicators slot (call after
7488
+ * registrations change). The rest are three-dots drawer rows. */
7101
7489
  renderActions() {
7102
7490
  const ctx = this.opts.getContext?.();
7103
7491
  if (!ctx) return;
7104
7492
  const doc = this.el.ownerDocument;
7105
7493
  this.actionsHost.replaceChildren();
7106
7494
  const builtin = new Set(TOPBAR_BUILTIN_IDS);
7107
- for (const action of widgetActions("topbar", ctx).filter((a) => a.align === "left" && !builtin.has(a.id))) {
7495
+ for (const action of widgetActions("topbar", ctx).filter((a) => mobilePlacement(a) === "bar" && !builtin.has(a.id))) {
7108
7496
  const b = doc.createElement("button");
7109
7497
  b.className = "vela-mb-item";
7110
7498
  b.setAttribute("aria-label", action.label);
@@ -7660,6 +8048,7 @@ var MoreDrawer = class {
7660
8048
  const value = shape ? `${shape.cols} \xD7 ${shape.rows}` : this.opts.layout.presets().find((p) => p.checked)?.label;
7661
8049
  list.appendChild(this.row(doc, "Layout", { icon: "layout", value, chevron: true, onClick: () => this.show("layout") }));
7662
8050
  }
8051
+ for (const act of this.opts.primaryActions?.() ?? []) list.appendChild(this.actionRow(doc, act));
7663
8052
  for (const panel of this.opts.panels()) {
7664
8053
  list.appendChild(
7665
8054
  this.row(doc, panel.title, {
@@ -7675,19 +8064,19 @@ var MoreDrawer = class {
7675
8064
  const alertCount = this.opts.alerts().length;
7676
8065
  list.appendChild(this.row(doc, "Alerts", { icon: "bell", value: alertCount > 0 ? String(alertCount) : void 0, chevron: true, onClick: () => this.show("alerts") }));
7677
8066
  }
7678
- for (const act of this.opts.actions()) {
7679
- list.appendChild(
7680
- this.row(doc, act.label, {
7681
- icon: act.icon,
7682
- onClick: () => {
7683
- act.run();
7684
- this.drawer.hide();
7685
- }
7686
- })
7687
- );
7688
- }
8067
+ for (const act of this.opts.actions()) list.appendChild(this.actionRow(doc, act));
7689
8068
  this.drawer.body.appendChild(list);
7690
8069
  }
8070
+ /** A contributed action's row: runs it and closes the drawer. */
8071
+ actionRow(doc, act) {
8072
+ return this.row(doc, act.label, {
8073
+ icon: act.icon,
8074
+ onClick: () => {
8075
+ act.run();
8076
+ this.drawer.hide();
8077
+ }
8078
+ });
8079
+ }
7691
8080
  renderStyle(doc) {
7692
8081
  const list = doc.createElement("div");
7693
8082
  list.className = "vela-md-list";
@@ -8151,6 +8540,7 @@ function buildContext(host) {
8151
8540
  setPriceStyle: (style) => host.active()?.setPriceStyle(style),
8152
8541
  openSymbolSearch: (query) => host.openSymbolSearch(query),
8153
8542
  togglePanel: (id, open) => host.togglePanel(id, open),
8543
+ dockStrip: (el) => host.dockStrip(el),
8154
8544
  host: host.root,
8155
8545
  toast: (message, kind) => host.toast(message, kind),
8156
8546
  addIndicator: (entry) => host.active()?.addExternalIndicator(entry),
@@ -8162,7 +8552,8 @@ function buildContext(host) {
8162
8552
  get activeCellId() {
8163
8553
  return host.active()?.id ?? "";
8164
8554
  },
8165
- setActiveCell: (id) => host.setActiveCell(id)
8555
+ setActiveCell: (id) => host.setActiveCell(id),
8556
+ replay: host.replay
8166
8557
  };
8167
8558
  }
8168
8559
 
@@ -8225,6 +8616,8 @@ var CSS21 = `
8225
8616
  .vela-workspace { position: relative; width: 100%; height: 100%; display: flex; flex-direction: column; background: var(--vela-bg); }
8226
8617
  .vela-ws-main { position: relative; display: flex; flex-direction: row; flex: 1 1 auto; min-height: 0; }
8227
8618
  .vela-ws-toolbar { position: relative; flex: none; }
8619
+ .vela-ws-strips { position: relative; flex: none; display: flex; flex-direction: column; min-width: 0; }
8620
+ .vela-ws-strips:empty { display: none; }
8228
8621
  .vela-ws-grid { position: relative; flex: 1 1 auto; min-width: 0; display: grid; gap: ${GAP_PX}px; background: var(--vela-border-soft); }
8229
8622
  .vela-cell { background: var(--vela-bg); position: relative; }
8230
8623
  /* Active-cell highlight: an overlay ring ABOVE the chart's own canvas stack (a plain
@@ -8396,6 +8789,20 @@ var VelaWorkspace = class {
8396
8789
  const hostEl = typeof container === "string" ? document.querySelector(container) : container;
8397
8790
  if (!hostEl) throw new Error(`VelaWorkspace: container not found: ${String(container)}`);
8398
8791
  this.opts = opts;
8792
+ this.replay = new WorkspaceReplay({
8793
+ cells: () => this.cells(),
8794
+ activeId: () => this.activeId,
8795
+ onCells: (handler) => {
8796
+ const offs = [
8797
+ this.events.on("cell:created", ({ id }) => handler({ kind: "created", id })),
8798
+ this.events.on("cell:destroyed", ({ id }) => handler({ kind: "destroyed", id })),
8799
+ this.events.on("cell:active", ({ id }) => handler({ kind: "active", id }))
8800
+ ];
8801
+ return () => {
8802
+ for (const off of offs) off();
8803
+ };
8804
+ }
8805
+ });
8399
8806
  this.persistKey = opts.persist === void 0 || opts.persist === false ? null : opts.persist === true ? "vela-workspace" : opts.persist;
8400
8807
  this.storage = opts.storage ?? localStorageAdapter();
8401
8808
  let boot = null;
@@ -8533,6 +8940,9 @@ var VelaWorkspace = class {
8533
8940
  this.dock.refresh();
8534
8941
  if (boot?.panels) this.dock.applyState(boot.panels);
8535
8942
  this.root.appendChild(main);
8943
+ this.stripsEl = doc.createElement("div");
8944
+ this.stripsEl.className = "vela-ws-strips";
8945
+ this.root.appendChild(this.stripsEl);
8536
8946
  this.toastHost = new Toast(this.gridEl);
8537
8947
  const attribution = rendererDefaults().attribution;
8538
8948
  if (attribution !== false) {
@@ -8767,9 +9177,16 @@ var VelaWorkspace = class {
8767
9177
  setActiveCell: (id) => this.setActiveCell(id),
8768
9178
  openSymbolSearch: (query) => this.symbolPicker.open(query ?? ""),
8769
9179
  togglePanel: (id, open) => this.dock.toggle(id, open),
9180
+ dockStrip: (el) => {
9181
+ this.stripsEl.appendChild(el);
9182
+ return () => {
9183
+ if (el.parentElement === this.stripsEl) el.remove();
9184
+ };
9185
+ },
8770
9186
  root: this.root,
8771
9187
  toast: (message, kind) => this.toastHost.show(message, kind),
8772
- stateDirty: () => this.markStateDirty()
9188
+ stateDirty: () => this.markStateDirty(),
9189
+ replay: this.replay
8773
9190
  });
8774
9191
  }
8775
9192
  /** Re-project contributed topbar actions + side panels, and mount late-registered attachments. */
@@ -9077,6 +9494,7 @@ var VelaWorkspace = class {
9077
9494
  if (this.destroyed) return;
9078
9495
  this.flushPendingState();
9079
9496
  this.destroyed = true;
9497
+ this.replay.destroy();
9080
9498
  if (this.persistKey !== null && typeof window !== "undefined") window.removeEventListener("beforeunload", this.onUnload);
9081
9499
  this.resizeObserver?.disconnect();
9082
9500
  this.splitters.destroy();
@@ -9843,14 +10261,13 @@ var VelaWorkspace = class {
9843
10261
  panels: () => has("panels") ? [...this.dock.list()] : [],
9844
10262
  onTogglePanel: (id) => this.dock.toggle(id),
9845
10263
  ...has("alerts") ? { alerts: () => this.alerts.map((a) => ({ title: `${a.source} \xB7 ${a.title}`, message: a.message, time: a.time })) } : {},
9846
- // Left-aligned actions have their own bottom-bar stop — only the rest
9847
- // lands in the drawer, or every left action would appear twice. Built-in-id
9848
- // actions are slot OVERRIDES: they reach the drawer through the slot's own
9849
- // routed button (screenshot) or stop (indicators), never as an extra row.
9850
- actions: () => {
9851
- const builtin = new Set(TOPBAR_BUILTIN_IDS);
9852
- return widgetActions("topbar", this.context()).filter((a) => a.align !== "left" && !builtin.has(a.id)).map((a) => ({ label: a.label, icon: a.icon, run: () => a.run(this.context()) }));
9853
- },
10264
+ // Actions placed on the bottom bar (left-aligned by default) have their stop
10265
+ // there — only the menu-placed ones land in the drawer, or they would appear
10266
+ // twice: left (primary) ones with the primary rows, right ones at the end.
10267
+ // Built-in-id actions are slot OVERRIDES: they reach the drawer through the
10268
+ // slot's own routed button (screenshot) or stop (indicators), never as a row.
10269
+ primaryActions: () => this.drawerActions("left"),
10270
+ actions: () => this.drawerActions("right"),
9854
10271
  // The desktop layout dropdown's whole surface — the grid canvas, the
9855
10272
  // non-canvas presets and the sync switches — relocated into the kebab
9856
10273
  // drawer (the topbar is hidden on mobile). Same reads as the topbar block;
@@ -9875,6 +10292,11 @@ var VelaWorkspace = class {
9875
10292
  }));
9876
10293
  this.moreDrawer.open();
9877
10294
  }
10295
+ /** The contributed topbar actions the mobile menu lists, from one cluster. */
10296
+ drawerActions(cluster) {
10297
+ const builtin = new Set(TOPBAR_BUILTIN_IDS);
10298
+ return widgetActions("topbar", this.context()).filter((a) => mobilePlacement(a) === "menu" && a.align === "left" === (cluster === "left") && !builtin.has(a.id)).map((a) => ({ label: a.label, icon: a.icon, run: () => a.run(this.context()) }));
10299
+ }
9878
10300
  openTimezoneDrawer() {
9879
10301
  this.timezoneDrawer ?? (this.timezoneDrawer = new TimezoneDrawer({
9880
10302
  host: this.root,
@@ -10038,4 +10460,4 @@ var VelaWorkspace = class {
10038
10460
  }
10039
10461
  };
10040
10462
 
10041
- export { Bottombar, ChartCell, ChartContextMenu, DataWindow, GRID_PICKER_MAX, IndicatorPicker, ObjectTree, PanelDock, RANGE_PRESETS, ShortcutsHelp, Statusline, SymbolPicker, TimeframeQuick, Topbar, VelaWorkspace, Watermark, activeAfterLayout, dataWindowSections, decimalsFor, decodeState, encodeState, ensureLayout, evenTracks, filterSymbols, fmtChange, fmtPrice, gridStyles, layoutDefinition, layoutForGrid, layoutShape, layouts, localStorageAdapter, memoryStorageAdapter, parseTimeframe, priceStyleLabel, rangesWithin, registerBuiltinLayouts, registerLayout, resizeTracks, resolveIndicators, sanitizeState, syncTargets, timeframeLabel, timeframeMs, trackOffsets, unregisterLayout };
10463
+ export { Bottombar, ChartCell, ChartContextMenu, DataWindow, GRID_PICKER_MAX, IndicatorPicker, ObjectTree, PanelDock, RANGE_PRESETS, ShortcutsHelp, Statusline, SymbolPicker, TimeframeQuick, Topbar, VelaWorkspace, Watermark, WorkspaceReplay, activeAfterLayout, barClose, dataWindowSections, decimalsFor, decodeState, encodeState, ensureLayout, evenTracks, filterSymbols, fmtChange, fmtPrice, gridStyles, lastOpenClosedBy, layoutDefinition, layoutForGrid, layoutShape, layouts, localStorageAdapter, memoryStorageAdapter, parseTimeframe, priceStyleLabel, rangesWithin, registerBuiltinLayouts, registerLayout, resizeTracks, resolveIndicators, sanitizeState, syncTargets, timeframeLabel, timeframeMs, trackOffsets, unregisterLayout };