@luxalgo/vela 0.7.7 → 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 (45) hide show
  1. package/dist/{DataProvider-CGa4KHRa.d.cts → DataProvider-BrlxntPN.d.cts} +1 -1
  2. package/dist/{DataProvider-CmWUtDVL.d.ts → DataProvider-DwFIoV4r.d.ts} +1 -1
  3. package/dist/{chunk-WTWGEB7K.js → chunk-2IAETM2I.js} +260 -2
  4. package/dist/{chunk-CAFCLMPF.js → chunk-BZQM2XO7.js} +1 -0
  5. package/dist/{chunk-ZA2XWTGH.js → chunk-E3GU6AUI.js} +499 -43
  6. package/dist/{chunk-BFA32GOU.js → chunk-EZ5FWVLA.js} +5 -2
  7. package/dist/{chunk-PVGWNF7V.js → chunk-KA77UTRF.js} +2 -2
  8. package/dist/{chunk-RVQWJOEE.js → chunk-QBPUT5SU.js} +2862 -1034
  9. package/dist/{contributions-DF_Ea07m.d.cts → contributions-CtlptxR8.d.cts} +187 -5
  10. package/dist/{contributions-nKOSeLy8.d.ts → contributions-tjNExo1o.d.ts} +187 -5
  11. package/dist/index.cjs +3142 -1030
  12. package/dist/index.d.cts +40 -8
  13. package/dist/index.d.ts +40 -8
  14. package/dist/index.js +28 -4
  15. package/dist/{options-D1AJKsn_.d.ts → options-BnEPUuVV.d.cts} +1 -1
  16. package/dist/{options-D1AJKsn_.d.cts → options-BnEPUuVV.d.ts} +1 -1
  17. package/dist/{plugin-DPyVJFoA.d.cts → plugin-BWxJRKzS.d.cts} +31 -25
  18. package/dist/{plugin-0oFDnexb.d.ts → plugin-e2WUEtiI.d.ts} +31 -25
  19. package/dist/plugin.cjs +1 -0
  20. package/dist/plugin.d.cts +4 -4
  21. package/dist/plugin.d.ts +4 -4
  22. package/dist/plugin.js +2 -2
  23. package/dist/providers/binance.d.cts +2 -2
  24. package/dist/providers/binance.d.ts +2 -2
  25. package/dist/providers/coinbase.d.cts +2 -2
  26. package/dist/providers/coinbase.d.ts +2 -2
  27. package/dist/providers/hyperliquid.d.cts +2 -2
  28. package/dist/providers/hyperliquid.d.ts +2 -2
  29. package/dist/{statusline-model-DAFyqVmR.d.ts → statusline-model-DFY7tuHj.d.cts} +5 -3
  30. package/dist/{statusline-model-BoWJ3cWQ.d.cts → statusline-model-zKXtGIjj.d.ts} +5 -3
  31. package/dist/ui.cjs +263 -0
  32. package/dist/ui.d.cts +75 -2
  33. package/dist/ui.d.ts +75 -2
  34. package/dist/ui.js +3 -3
  35. package/dist/vela.global.js +3144 -1032
  36. package/dist/vela.global.min.js +78 -76
  37. package/dist/widget.cjs +3627 -1081
  38. package/dist/widget.d.cts +26 -7
  39. package/dist/widget.d.ts +26 -7
  40. package/dist/widget.js +7 -7
  41. package/dist/workspace.cjs +3630 -1081
  42. package/dist/workspace.d.cts +126 -6
  43. package/dist/workspace.d.ts +126 -6
  44. package/dist/workspace.js +6 -6
  45. 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, TypedEventBus, MultiProviderFeed, registerBuiltinChartTypes, resolveTheme, buildToolbar, createCustomMark, createAttributionMark, DrawingToolbar, applyAttributionMarkTheme, sharedBarStore, timeframeToMs } from './chunk-RVQWJOEE.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)}` : "";
@@ -5569,6 +5646,8 @@ var ChartCell = class {
5569
5646
  this.activeRangeId = null;
5570
5647
  /** Latched verdict of {@link sessionAvailable} (async metadata, sticky per symbol). */
5571
5648
  this.sessionAvailableFlag = false;
5649
+ /** The symbol of the most recent metadata probe — an older one landing late is dropped. */
5650
+ this.metadataProbeSymbol = null;
5572
5651
  /** Latched: the symbol's extended tape wraps midnight (an overnight roll market) —
5573
5652
  * one extended-hours shading phase instead of the pre/post split. */
5574
5653
  this.sessionOvernightFlag = false;
@@ -5637,7 +5716,11 @@ var ChartCell = class {
5637
5716
  nativeBackend: deps.nativeBackend,
5638
5717
  // The user's drawings option minus its toolbar: one SHARED bar serves
5639
5718
  // the whole workspace (per-cell bars would cost a 44px gutter each).
5640
- drawings: cellDrawings(deps.chartDefaults.drawings)
5719
+ drawings: cellDrawings(deps.chartDefaults.drawings),
5720
+ // The renderer's own Time zone row edits a resolved IANA zone; the cell
5721
+ // contributes the workspace picker instead (`time-zone`, with the
5722
+ // exchange rule — see pushSettingsSections).
5723
+ settings: { ...deps.chartDefaults.settings, hidden: [...deps.chartDefaults.settings?.hidden ?? [], "symbol.timezone"] }
5641
5724
  },
5642
5725
  { dataFeed: deps.feed }
5643
5726
  );
@@ -5686,7 +5769,10 @@ var ChartCell = class {
5686
5769
  const list = providers.length > 0 ? providers.join(", ") : "none";
5687
5770
  this.deps.toast(`No registered provider serves "${symbol2}" (registered: ${list})`, "error", 6e3);
5688
5771
  });
5689
- this.inner.on("load:start", () => this.watermark?.setLoading(true));
5772
+ this.inner.on("load:start", ({ symbol: symbol2 }) => {
5773
+ this.watermark?.setLoading(true);
5774
+ this.refreshSymbolMetadata(symbol2);
5775
+ });
5690
5776
  this.inner.on("load:end", () => {
5691
5777
  this.watermark?.setLoading(false);
5692
5778
  this.refreshSessionShading();
@@ -5698,8 +5784,12 @@ var ChartCell = class {
5698
5784
  this.indicatorValuesOn = seed.indicatorValues ?? true;
5699
5785
  if (!this.indicatorValuesOn) this.inner.renderer.set("indicatorValues", false);
5700
5786
  this.watermarkOn = seed.watermark ?? deps.watermark;
5787
+ this.replayWatermarkOn = seed.replayWatermark ?? true;
5701
5788
  this.watermark = deps.watermark ? new Watermark(this.host, symbol ?? "", seed.timeframe ?? "60") : null;
5702
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));
5703
5793
  this.statusline = deps.statusline ? new Statusline(this.host, symbol ?? "", (sym) => this.inner?.data.symbolIcon(sym)) : null;
5704
5794
  this.statusline?.setMeta(seed.timeframe ?? "60", this.state.provider ?? "");
5705
5795
  this.statusline?.onChart(this.inner);
@@ -5857,12 +5947,19 @@ var ChartCell = class {
5857
5947
  }
5858
5948
  /** Re-read the symbol's metadata: session posture (RTH/ETH toggle, shading) and its
5859
5949
  * trading zone (the exchange rule). Async — the workspace re-projects when a verdict lands. */
5860
- refreshSymbolMetadata() {
5950
+ refreshSymbolMetadata(symbol = this.state.symbol, retry = true) {
5861
5951
  const chart = this.inner;
5862
- const symbol = this.state.symbol;
5863
5952
  if (!chart || !symbol) return;
5953
+ this.metadataProbeSymbol = symbol;
5864
5954
  void chart.data.symbolInfo(symbol).then((si) => {
5865
- if (this.inner !== chart) return;
5955
+ if (this.inner !== chart || this.metadataProbeSymbol !== symbol) return;
5956
+ if (si === void 0 && retry) {
5957
+ void chart.data.ready().then(() => {
5958
+ if (this.inner === chart && this.metadataProbeSymbol === symbol) this.refreshSymbolMetadata(symbol, false);
5959
+ });
5960
+ return;
5961
+ }
5962
+ if (this.state.symbol !== symbol) return;
5866
5963
  const available = typeof si?.session === "string" && si.session !== "" && si.session !== "24x7";
5867
5964
  const overnight = parseSessionSpec(si)?.overnight === true;
5868
5965
  const zone = typeof si?.timezone === "string" && si.timezone !== "" ? si.timezone : void 0;
@@ -5977,6 +6074,24 @@ var ChartCell = class {
5977
6074
  }
5978
6075
  ]
5979
6076
  };
6077
+ const timezoneSection = {
6078
+ title: "Time zone",
6079
+ id: "time-zone",
6080
+ placement: "symbol",
6081
+ rows: [
6082
+ {
6083
+ kind: "select",
6084
+ label: "Time zone",
6085
+ id: "zone",
6086
+ options: timezoneMenuRows(this.deps.timezone()).map((r) => [r.value, r.label]),
6087
+ get: () => {
6088
+ const choice = this.deps.timezone();
6089
+ return isExchangeTimezone(choice) ? choice : normalizeTimezone(choice);
6090
+ },
6091
+ set: (v) => this.deps.setTimezone(v)
6092
+ }
6093
+ ]
6094
+ };
5980
6095
  const watermarkSection = {
5981
6096
  title: "Watermark",
5982
6097
  id: "watermark",
@@ -5988,6 +6103,13 @@ var ChartCell = class {
5988
6103
  id: "visible",
5989
6104
  get: () => this.watermarkOn,
5990
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)
5991
6113
  }
5992
6114
  ]
5993
6115
  };
@@ -6045,7 +6167,7 @@ var ChartCell = class {
6045
6167
  ]
6046
6168
  });
6047
6169
  }
6048
- sections.push(advanced);
6170
+ sections.push(advanced, timezoneSection);
6049
6171
  if (this.sessionAvailableFlag) sections.push(sessionSection);
6050
6172
  sections.push(watermarkSection);
6051
6173
  chart.renderer.setSettingsSections(sections);
@@ -6056,6 +6178,12 @@ var ChartCell = class {
6056
6178
  this.watermark?.setVisible(visible);
6057
6179
  this.deps.onStateDirty();
6058
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
+ }
6059
6187
  /** Show/hide this cell's indicator titles — the in-chart legend rows (persisted per cell). */
6060
6188
  setIndicatorTitlesVisible(visible) {
6061
6189
  this.indicatorTitlesOn = visible;
@@ -6529,6 +6657,7 @@ var ChartCell = class {
6529
6657
  if (!this.inner || this.destroyed) return;
6530
6658
  if (cs.priceStyle && cs.priceStyle !== this.priceStyle) this.setPriceStyle(cs.priceStyle);
6531
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);
6532
6661
  if (cs.indicatorTitles !== void 0 && cs.indicatorTitles !== this.indicatorTitlesOn) this.setIndicatorTitlesVisible(cs.indicatorTitles);
6533
6662
  if (cs.indicatorValues !== void 0 && cs.indicatorValues !== this.indicatorValuesOn) this.setIndicatorValuesVisible(cs.indicatorValues);
6534
6663
  if (cs.rendererConfig != null) this.inner.renderer.applyConfig(cs.rendererConfig);
@@ -6563,6 +6692,7 @@ var ChartCell = class {
6563
6692
  ...live ? { symbol: live.symbol, provider: live.provider, timeframe: live.timeframe } : {},
6564
6693
  priceStyle: this.priceStyle,
6565
6694
  watermark: this.watermarkOn,
6695
+ replayWatermark: this.replayWatermarkOn,
6566
6696
  indicatorTitles: this.indicatorTitlesOn,
6567
6697
  indicatorValues: this.indicatorValuesOn,
6568
6698
  rendererConfig: this.inner?.renderer.getConfig() ?? void 0,
@@ -6605,6 +6735,298 @@ var ChartCell = class {
6605
6735
  }
6606
6736
  };
6607
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
+
6608
7030
  // src/workspace/layouts.ts
6609
7031
  var registry = /* @__PURE__ */ new Map();
6610
7032
  function registerLayout(def) {
@@ -7061,16 +7483,16 @@ var MobileBar = class {
7061
7483
  host.appendChild(this.el);
7062
7484
  this.renderActions();
7063
7485
  }
7064
- /** Re-project the left-aligned contributed actions as icon-only stops in the
7065
- * indicators slot (call after registrations change). Right-aligned actions stay
7066
- * 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. */
7067
7489
  renderActions() {
7068
7490
  const ctx = this.opts.getContext?.();
7069
7491
  if (!ctx) return;
7070
7492
  const doc = this.el.ownerDocument;
7071
7493
  this.actionsHost.replaceChildren();
7072
7494
  const builtin = new Set(TOPBAR_BUILTIN_IDS);
7073
- 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))) {
7074
7496
  const b = doc.createElement("button");
7075
7497
  b.className = "vela-mb-item";
7076
7498
  b.setAttribute("aria-label", action.label);
@@ -7626,6 +8048,7 @@ var MoreDrawer = class {
7626
8048
  const value = shape ? `${shape.cols} \xD7 ${shape.rows}` : this.opts.layout.presets().find((p) => p.checked)?.label;
7627
8049
  list.appendChild(this.row(doc, "Layout", { icon: "layout", value, chevron: true, onClick: () => this.show("layout") }));
7628
8050
  }
8051
+ for (const act of this.opts.primaryActions?.() ?? []) list.appendChild(this.actionRow(doc, act));
7629
8052
  for (const panel of this.opts.panels()) {
7630
8053
  list.appendChild(
7631
8054
  this.row(doc, panel.title, {
@@ -7641,19 +8064,19 @@ var MoreDrawer = class {
7641
8064
  const alertCount = this.opts.alerts().length;
7642
8065
  list.appendChild(this.row(doc, "Alerts", { icon: "bell", value: alertCount > 0 ? String(alertCount) : void 0, chevron: true, onClick: () => this.show("alerts") }));
7643
8066
  }
7644
- for (const act of this.opts.actions()) {
7645
- list.appendChild(
7646
- this.row(doc, act.label, {
7647
- icon: act.icon,
7648
- onClick: () => {
7649
- act.run();
7650
- this.drawer.hide();
7651
- }
7652
- })
7653
- );
7654
- }
8067
+ for (const act of this.opts.actions()) list.appendChild(this.actionRow(doc, act));
7655
8068
  this.drawer.body.appendChild(list);
7656
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
+ }
7657
8080
  renderStyle(doc) {
7658
8081
  const list = doc.createElement("div");
7659
8082
  list.className = "vela-md-list";
@@ -8117,6 +8540,7 @@ function buildContext(host) {
8117
8540
  setPriceStyle: (style) => host.active()?.setPriceStyle(style),
8118
8541
  openSymbolSearch: (query) => host.openSymbolSearch(query),
8119
8542
  togglePanel: (id, open) => host.togglePanel(id, open),
8543
+ dockStrip: (el) => host.dockStrip(el),
8120
8544
  host: host.root,
8121
8545
  toast: (message, kind) => host.toast(message, kind),
8122
8546
  addIndicator: (entry) => host.active()?.addExternalIndicator(entry),
@@ -8128,7 +8552,8 @@ function buildContext(host) {
8128
8552
  get activeCellId() {
8129
8553
  return host.active()?.id ?? "";
8130
8554
  },
8131
- setActiveCell: (id) => host.setActiveCell(id)
8555
+ setActiveCell: (id) => host.setActiveCell(id),
8556
+ replay: host.replay
8132
8557
  };
8133
8558
  }
8134
8559
 
@@ -8191,6 +8616,8 @@ var CSS21 = `
8191
8616
  .vela-workspace { position: relative; width: 100%; height: 100%; display: flex; flex-direction: column; background: var(--vela-bg); }
8192
8617
  .vela-ws-main { position: relative; display: flex; flex-direction: row; flex: 1 1 auto; min-height: 0; }
8193
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; }
8194
8621
  .vela-ws-grid { position: relative; flex: 1 1 auto; min-width: 0; display: grid; gap: ${GAP_PX}px; background: var(--vela-border-soft); }
8195
8622
  .vela-cell { background: var(--vela-bg); position: relative; }
8196
8623
  /* Active-cell highlight: an overlay ring ABOVE the chart's own canvas stack (a plain
@@ -8362,6 +8789,20 @@ var VelaWorkspace = class {
8362
8789
  const hostEl = typeof container === "string" ? document.querySelector(container) : container;
8363
8790
  if (!hostEl) throw new Error(`VelaWorkspace: container not found: ${String(container)}`);
8364
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
+ });
8365
8806
  this.persistKey = opts.persist === void 0 || opts.persist === false ? null : opts.persist === true ? "vela-workspace" : opts.persist;
8366
8807
  this.storage = opts.storage ?? localStorageAdapter();
8367
8808
  let boot = null;
@@ -8499,6 +8940,9 @@ var VelaWorkspace = class {
8499
8940
  this.dock.refresh();
8500
8941
  if (boot?.panels) this.dock.applyState(boot.panels);
8501
8942
  this.root.appendChild(main);
8943
+ this.stripsEl = doc.createElement("div");
8944
+ this.stripsEl.className = "vela-ws-strips";
8945
+ this.root.appendChild(this.stripsEl);
8502
8946
  this.toastHost = new Toast(this.gridEl);
8503
8947
  const attribution = rendererDefaults().attribution;
8504
8948
  if (attribution !== false) {
@@ -8733,9 +9177,16 @@ var VelaWorkspace = class {
8733
9177
  setActiveCell: (id) => this.setActiveCell(id),
8734
9178
  openSymbolSearch: (query) => this.symbolPicker.open(query ?? ""),
8735
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
+ },
8736
9186
  root: this.root,
8737
9187
  toast: (message, kind) => this.toastHost.show(message, kind),
8738
- stateDirty: () => this.markStateDirty()
9188
+ stateDirty: () => this.markStateDirty(),
9189
+ replay: this.replay
8739
9190
  });
8740
9191
  }
8741
9192
  /** Re-project contributed topbar actions + side panels, and mount late-registered attachments. */
@@ -9043,6 +9494,7 @@ var VelaWorkspace = class {
9043
9494
  if (this.destroyed) return;
9044
9495
  this.flushPendingState();
9045
9496
  this.destroyed = true;
9497
+ this.replay.destroy();
9046
9498
  if (this.persistKey !== null && typeof window !== "undefined") window.removeEventListener("beforeunload", this.onUnload);
9047
9499
  this.resizeObserver?.disconnect();
9048
9500
  this.splitters.destroy();
@@ -9809,14 +10261,13 @@ var VelaWorkspace = class {
9809
10261
  panels: () => has("panels") ? [...this.dock.list()] : [],
9810
10262
  onTogglePanel: (id) => this.dock.toggle(id),
9811
10263
  ...has("alerts") ? { alerts: () => this.alerts.map((a) => ({ title: `${a.source} \xB7 ${a.title}`, message: a.message, time: a.time })) } : {},
9812
- // Left-aligned actions have their own bottom-bar stop — only the rest
9813
- // lands in the drawer, or every left action would appear twice. Built-in-id
9814
- // actions are slot OVERRIDES: they reach the drawer through the slot's own
9815
- // routed button (screenshot) or stop (indicators), never as an extra row.
9816
- actions: () => {
9817
- const builtin = new Set(TOPBAR_BUILTIN_IDS);
9818
- 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()) }));
9819
- },
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"),
9820
10271
  // The desktop layout dropdown's whole surface — the grid canvas, the
9821
10272
  // non-canvas presets and the sync switches — relocated into the kebab
9822
10273
  // drawer (the topbar is hidden on mobile). Same reads as the topbar block;
@@ -9841,6 +10292,11 @@ var VelaWorkspace = class {
9841
10292
  }));
9842
10293
  this.moreDrawer.open();
9843
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
+ }
9844
10300
  openTimezoneDrawer() {
9845
10301
  this.timezoneDrawer ?? (this.timezoneDrawer = new TimezoneDrawer({
9846
10302
  host: this.root,
@@ -10004,4 +10460,4 @@ var VelaWorkspace = class {
10004
10460
  }
10005
10461
  };
10006
10462
 
10007
- 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 };