@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
@@ -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-ATGTCWHJ.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-LMBYEGUR.js';
4
+ import { Menu, Dialog, CalloutBubble, applyPlotOverlayTokens, ensureUIHost } from './chunk-NELQJCGK.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
@@ -3652,7 +3652,12 @@ var TimeframeQuick = class {
3652
3652
  content: (body) => body.append(this.input, this.hint),
3653
3653
  onOpenChange: (open) => opts.onOpenChange?.(open)
3654
3654
  });
3655
- this.input.addEventListener("input", () => this.renderHint());
3655
+ this.input.addEventListener("input", () => {
3656
+ const { selectionStart, selectionEnd } = this.input;
3657
+ this.input.value = this.input.value.toUpperCase();
3658
+ this.input.setSelectionRange(selectionStart, selectionEnd);
3659
+ this.renderHint();
3660
+ });
3656
3661
  this.input.addEventListener("keydown", (e) => {
3657
3662
  if (e.key !== "Enter") return;
3658
3663
  const parsed = parseTimeframe(this.input.value);
@@ -3664,7 +3669,7 @@ var TimeframeQuick = class {
3664
3669
  }
3665
3670
  open(seed = "") {
3666
3671
  this.dialog.show();
3667
- this.input.value = seed;
3672
+ this.input.value = seed.toUpperCase();
3668
3673
  this.renderHint();
3669
3674
  setTimeout(() => {
3670
3675
  this.input.focus();
@@ -3682,7 +3687,7 @@ var TimeframeQuick = class {
3682
3687
  this.open(text);
3683
3688
  return;
3684
3689
  }
3685
- this.input.value += text;
3690
+ this.input.value += text.toUpperCase();
3686
3691
  this.renderHint();
3687
3692
  }
3688
3693
  close() {
@@ -3695,7 +3700,7 @@ var TimeframeQuick = class {
3695
3700
  const raw = this.input.value.trim();
3696
3701
  const parsed = parseTimeframe(raw);
3697
3702
  if (!raw) {
3698
- this.hint.textContent = "e.g. 15, 4h, D, 3M";
3703
+ this.hint.textContent = "e.g. 15, 4H, D, 3M";
3699
3704
  delete this.hint.dataset.invalid;
3700
3705
  } else if (parsed.valid) {
3701
3706
  this.hint.textContent = parsed.label ?? "";
@@ -3798,7 +3803,7 @@ var ShortcutsHelp = class {
3798
3803
  addRow("Delete the drawing under the cursor", ["Middle-click"]);
3799
3804
  const s = doc.createElement("div");
3800
3805
  s.className = "vela-sh-static";
3801
- s.textContent = "Typing a letter opens the symbol search; typing a digit opens the timeframe entry.";
3806
+ s.textContent = "Typing a letter or 0 opens the symbol search; typing a digit from 1 to 9 opens the timeframe entry.";
3802
3807
  this.list.appendChild(s);
3803
3808
  }
3804
3809
  };
@@ -3925,6 +3930,7 @@ function sanitizeCell(raw) {
3925
3930
  if (typeof c.bars === "number" && Number.isFinite(c.bars) && c.bars > 0) out.bars = c.bars;
3926
3931
  if (c.session === "regular" || c.session === "extended") out.session = c.session;
3927
3932
  if (typeof c.watermark === "boolean") out.watermark = c.watermark;
3933
+ if (typeof c.replayWatermark === "boolean") out.replayWatermark = c.replayWatermark;
3928
3934
  if (typeof c.indicatorTitles === "boolean") out.indicatorTitles = c.indicatorTitles;
3929
3935
  if (typeof c.indicatorValues === "boolean") out.indicatorValues = c.indicatorValues;
3930
3936
  if (c.rendererConfig != null && typeof c.rendererConfig === "object") out.rendererConfig = c.rendererConfig;
@@ -4208,9 +4214,15 @@ var CSS10 = `
4208
4214
  }
4209
4215
  .vela-statusline .vela-sl-symbol { font-weight: 600; font-size: var(--vela-font-size-lg); }
4210
4216
  .vela-statusline .vela-sl-meta { color: var(--vela-fg-muted); font-size: var(--vela-font-size-md); font-weight: 600; }
4217
+ /* The meta opens with "\xB7 " \u2014 sit its dot one space-width after the ticker, as far as the
4218
+ * venue sits after it, not a full row gap away. */
4219
+ .vela-statusline .vela-sl-symbol + .vela-sl-meta { margin-left: calc(var(--vela-space-1) - var(--vela-space-2)); }
4211
4220
  /* 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; }
4221
+ * via the kit tooltip); the session tint is applied per status in setMarketStatus. While
4222
+ * the chart replays past bars it wears the replay badge instead (the inverse chip). */
4223
+ .vela-statusline .vela-sl-market { align-self: center; display: inline-flex; }
4224
+ .vela-statusline .vela-sl-market > [hidden] { display: none !important; }
4225
+ .vela-statusline .vela-sl-replay-badge, .vela-statusline .vela-sl-replay-badge svg { display: block; width: 16px; height: 16px; }
4214
4226
  .vela-statusline .vela-sl-ohlc { display: flex; gap: var(--vela-space-1); color: var(--vela-fg-muted); }
4215
4227
  .vela-statusline .vela-sl-ohlc b { color: var(--vela-fg); font-weight: 500; }
4216
4228
  /* The change value wears the SAME ink as the OHLC values (set inline per render) \u2014
@@ -4263,6 +4275,8 @@ var MARKET_LABELS = {
4263
4275
  closed: "Market Closed",
4264
4276
  holiday: "Market Holiday"
4265
4277
  };
4278
+ var REPLAY_LABEL = "Replay Mode";
4279
+ 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
4280
  var MARKET_INKS = {
4267
4281
  open: "var(--vela-up)",
4268
4282
  pre: SESSION_PRE,
@@ -4301,6 +4315,9 @@ var Statusline = class {
4301
4315
  this.host = host;
4302
4316
  this.iconFor = iconFor;
4303
4317
  this.parts = { logo: true, name: true, market: true, ohlc: true, change: true };
4318
+ this.marketStatus = "open";
4319
+ /** The chart replays past bars — the badge shows the replay mode (see {@link setReplaying}). */
4320
+ this.replaying = false;
4304
4321
  /** The right-click action menu — present once a host wires it via {@link attachMenu}. */
4305
4322
  this.menu = null;
4306
4323
  this.menuHooks = null;
@@ -4352,8 +4369,13 @@ var Statusline = class {
4352
4369
  label: MARKET_LABELS.open,
4353
4370
  host
4354
4371
  });
4355
- this.marketEl = this.marketBubble.el;
4356
- this.marketEl.classList.add("vela-sl-market");
4372
+ this.replayBadge = doc.createElement("span");
4373
+ this.replayBadge.className = "vela-sl-replay-badge";
4374
+ this.replayBadge.innerHTML = REPLAY_BADGE_SVG;
4375
+ this.replayBadge.hidden = true;
4376
+ this.marketEl = doc.createElement("span");
4377
+ this.marketEl.className = "vela-sl-market";
4378
+ this.marketEl.append(this.marketBubble.el, this.replayBadge);
4357
4379
  this.ohlcEl = doc.createElement("span");
4358
4380
  this.ohlcEl.className = "vela-sl-ohlc";
4359
4381
  this.changeEl = doc.createElement("span");
@@ -4475,6 +4497,25 @@ var Statusline = class {
4475
4497
  /** Dress the market badge for a session state: its icon, tinted circle, and the
4476
4498
  * hover label. Callers with no session model leave the constructor's 'open'. */
4477
4499
  setMarketStatus(status) {
4500
+ this.marketStatus = status;
4501
+ this.dressBadge();
4502
+ }
4503
+ /** The chart entered or left a bar replay: the badge shows the replay mode meanwhile
4504
+ * (the session state of a past bar says nothing about the market now). */
4505
+ setReplaying(replaying) {
4506
+ if (replaying === this.replaying) return;
4507
+ this.replaying = replaying;
4508
+ this.dressBadge();
4509
+ }
4510
+ dressBadge() {
4511
+ this.marketBubble.el.hidden = this.replaying;
4512
+ this.replayBadge.hidden = !this.replaying;
4513
+ if (this.replaying) {
4514
+ this.marketEl.dataset.status = "replay";
4515
+ this.marketTip.setContent(REPLAY_LABEL);
4516
+ return;
4517
+ }
4518
+ const status = this.marketStatus;
4478
4519
  this.marketEl.dataset.status = status;
4479
4520
  const ink = MARKET_INKS[status];
4480
4521
  this.marketBubble.set({
@@ -4547,11 +4588,23 @@ var Statusline = class {
4547
4588
  this.lastBar = b;
4548
4589
  if (!this.hoverBar) this.render();
4549
4590
  }),
4591
+ // A replay cut or restore replaces the newest bar without a `bar` event.
4592
+ chart.on("replay:start", () => {
4593
+ this.lastBar = null;
4594
+ this.setReplaying(true);
4595
+ this.render();
4596
+ }),
4597
+ chart.on("replay:end", () => {
4598
+ this.lastBar = null;
4599
+ this.setReplaying(false);
4600
+ this.render();
4601
+ }),
4550
4602
  chart.renderer.onCrosshairMove((e) => {
4551
4603
  this.hoverBar = e.ohlc;
4552
4604
  this.render();
4553
4605
  })
4554
4606
  );
4607
+ this.setReplaying(chart.replay.state.active);
4555
4608
  this.render();
4556
4609
  }
4557
4610
  destroy() {
@@ -4620,6 +4673,7 @@ var CSS11 = `
4620
4673
  left: var(--vela-toolbar-gutter, 0px);
4621
4674
  right: var(--vela-scale-gutter, 0px);
4622
4675
  display: flex;
4676
+ flex-direction: column;
4623
4677
  align-items: center;
4624
4678
  justify-content: center;
4625
4679
  overflow: hidden;
@@ -4633,6 +4687,14 @@ var CSS11 = `
4633
4687
  user-select: none;
4634
4688
  white-space: nowrap;
4635
4689
  }
4690
+ .vela-watermark-replay {
4691
+ display: inline-flex;
4692
+ align-items: center;
4693
+ gap: 0.25em;
4694
+ margin-top: 0.1em;
4695
+ font-size: 0.8em;
4696
+ }
4697
+ .vela-watermark [hidden] { display: none !important; }
4636
4698
  `;
4637
4699
  function watermarkFontPx(availPx, textPxAtMax) {
4638
4700
  if (textPxAtMax <= 0) return MAX_FONT_PX;
@@ -4643,6 +4705,10 @@ var Watermark = class {
4643
4705
  this.resizeObserver = null;
4644
4706
  /** The host's visibility preference (the persisted watermark toggle). */
4645
4707
  this.shown = true;
4708
+ /** The host's preference for the replay line (its own persisted toggle). */
4709
+ this.replayShown = true;
4710
+ /** The chart is replaying past bars. */
4711
+ this.replaying = false;
4646
4712
  /** A bar load is in flight with nothing painted — the loading affordance owns the
4647
4713
  * canvas, so the mark stays out of its way. Starts true: the FIRST `load:start`
4648
4714
  * fires during chart construction, before any subscriber can see it. */
@@ -4652,7 +4718,10 @@ var Watermark = class {
4652
4718
  this.el.className = "vela-watermark";
4653
4719
  this.el.dataset.velaScreenshot = "under";
4654
4720
  this.text = host.ownerDocument.createElement("span");
4655
- this.el.appendChild(this.text);
4721
+ this.replayLine = host.ownerDocument.createElement("span");
4722
+ this.replayLine.className = "vela-watermark-replay";
4723
+ this.replayLine.append(iconEl("replay", host.ownerDocument), host.ownerDocument.createTextNode("Replay"));
4724
+ this.el.append(this.text, this.replayLine);
4656
4725
  host.appendChild(this.el);
4657
4726
  if (typeof ResizeObserver !== "undefined") {
4658
4727
  this.resizeObserver = new ResizeObserver(() => this.fit());
@@ -4665,13 +4734,26 @@ var Watermark = class {
4665
4734
  this.shown = visible;
4666
4735
  this.sync();
4667
4736
  }
4737
+ /** Show/hide the replay line (the host's toggle); it only ever shows while replaying. */
4738
+ setReplayVisible(visible) {
4739
+ this.replayShown = visible;
4740
+ this.sync();
4741
+ }
4742
+ /** The chart entered or left a bar replay. */
4743
+ setReplaying(replaying) {
4744
+ this.replaying = replaying;
4745
+ this.sync();
4746
+ }
4668
4747
  /** Loading and the watermark never share the canvas — hidden while a load is up. */
4669
4748
  setLoading(loading) {
4670
4749
  this.loading = loading;
4671
4750
  this.sync();
4672
4751
  }
4673
4752
  sync() {
4674
- this.el.style.display = this.shown && !this.loading ? "" : "none";
4753
+ const replay = this.replayShown && this.replaying;
4754
+ this.text.hidden = !this.shown;
4755
+ this.replayLine.hidden = !replay;
4756
+ this.el.style.display = (this.shown || replay) && !this.loading ? "" : "none";
4675
4757
  }
4676
4758
  update(symbol, timeframe) {
4677
4759
  this.text.textContent = symbol ? `${parseSymbol(symbol).ticker} \xB7 ${timeframeLabel(timeframe)}` : "";
@@ -5707,8 +5789,12 @@ var ChartCell = class {
5707
5789
  this.indicatorValuesOn = seed.indicatorValues ?? true;
5708
5790
  if (!this.indicatorValuesOn) this.inner.renderer.set("indicatorValues", false);
5709
5791
  this.watermarkOn = seed.watermark ?? deps.watermark;
5792
+ this.replayWatermarkOn = seed.replayWatermark ?? true;
5710
5793
  this.watermark = deps.watermark ? new Watermark(this.host, symbol ?? "", seed.timeframe ?? "60") : null;
5711
5794
  if (!this.watermarkOn) this.watermark?.setVisible(false);
5795
+ if (!this.replayWatermarkOn) this.watermark?.setReplayVisible(false);
5796
+ this.inner.on("replay:start", () => this.watermark?.setReplaying(true));
5797
+ this.inner.on("replay:end", () => this.watermark?.setReplaying(false));
5712
5798
  this.statusline = deps.statusline ? new Statusline(this.host, symbol ?? "", (sym) => this.inner?.data.symbolIcon(sym)) : null;
5713
5799
  this.statusline?.setMeta(seed.timeframe ?? "60", this.state.provider ?? "");
5714
5800
  this.statusline?.onChart(this.inner);
@@ -6022,6 +6108,13 @@ var ChartCell = class {
6022
6108
  id: "visible",
6023
6109
  get: () => this.watermarkOn,
6024
6110
  set: (v) => this.setWatermarkVisible(v)
6111
+ },
6112
+ {
6113
+ kind: "toggle",
6114
+ label: "Replay watermark",
6115
+ id: "replay",
6116
+ get: () => this.replayWatermarkOn,
6117
+ set: (v) => this.setReplayWatermarkVisible(v)
6025
6118
  }
6026
6119
  ]
6027
6120
  };
@@ -6090,6 +6183,12 @@ var ChartCell = class {
6090
6183
  this.watermark?.setVisible(visible);
6091
6184
  this.deps.onStateDirty();
6092
6185
  }
6186
+ /** Show/hide the "Replay" line under this cell's watermark while it replays (persisted per cell). */
6187
+ setReplayWatermarkVisible(visible) {
6188
+ this.replayWatermarkOn = visible;
6189
+ this.watermark?.setReplayVisible(visible);
6190
+ this.deps.onStateDirty();
6191
+ }
6093
6192
  /** Show/hide this cell's indicator titles — the in-chart legend rows (persisted per cell). */
6094
6193
  setIndicatorTitlesVisible(visible) {
6095
6194
  this.indicatorTitlesOn = visible;
@@ -6330,7 +6429,7 @@ var ChartCell = class {
6330
6429
  * then the manifest in the host's own order. */
6331
6430
  libraryRows() {
6332
6431
  return [
6333
- ...this.supportedNatives().map((n) => ({ name: n.title, category: "Vela", native: true, nativeType: n.type, beta: n.beta })),
6432
+ ...this.supportedNatives().map((n) => ({ name: n.title, category: "Built-in", native: true, nativeType: n.type, beta: n.beta })),
6334
6433
  ...this.manifest.map((e) => ({ name: e.name, language: e.language, category: e.category }))
6335
6434
  ];
6336
6435
  }
@@ -6563,6 +6662,7 @@ var ChartCell = class {
6563
6662
  if (!this.inner || this.destroyed) return;
6564
6663
  if (cs.priceStyle && cs.priceStyle !== this.priceStyle) this.setPriceStyle(cs.priceStyle);
6565
6664
  if (cs.watermark !== void 0 && cs.watermark !== this.watermarkOn) this.setWatermarkVisible(cs.watermark);
6665
+ if (cs.replayWatermark !== void 0 && cs.replayWatermark !== this.replayWatermarkOn) this.setReplayWatermarkVisible(cs.replayWatermark);
6566
6666
  if (cs.indicatorTitles !== void 0 && cs.indicatorTitles !== this.indicatorTitlesOn) this.setIndicatorTitlesVisible(cs.indicatorTitles);
6567
6667
  if (cs.indicatorValues !== void 0 && cs.indicatorValues !== this.indicatorValuesOn) this.setIndicatorValuesVisible(cs.indicatorValues);
6568
6668
  if (cs.rendererConfig != null) this.inner.renderer.applyConfig(cs.rendererConfig);
@@ -6597,6 +6697,7 @@ var ChartCell = class {
6597
6697
  ...live ? { symbol: live.symbol, provider: live.provider, timeframe: live.timeframe } : {},
6598
6698
  priceStyle: this.priceStyle,
6599
6699
  watermark: this.watermarkOn,
6700
+ replayWatermark: this.replayWatermarkOn,
6600
6701
  indicatorTitles: this.indicatorTitlesOn,
6601
6702
  indicatorValues: this.indicatorValuesOn,
6602
6703
  rendererConfig: this.inner?.renderer.getConfig() ?? void 0,
@@ -6639,6 +6740,298 @@ var ChartCell = class {
6639
6740
  }
6640
6741
  };
6641
6742
 
6743
+ // src/workspace/WorkspaceReplay.ts
6744
+ var MAX_BARS_PER_STEP = 1e5;
6745
+ var WorkspaceReplay = class {
6746
+ constructor(host) {
6747
+ this.host = host;
6748
+ this.bus = new TypedEventBus();
6749
+ /** The running session: the shared replay time (every chart shows the bars closed by it). */
6750
+ this.session = null;
6751
+ this.playing = false;
6752
+ this.intervalMs = 1e3;
6753
+ this.timer = null;
6754
+ /** Cells whose replay a symbol switch ended — restarted at the shared time once their bars land. */
6755
+ this.rejoining = /* @__PURE__ */ new Set();
6756
+ this.cellSubs = /* @__PURE__ */ new Map();
6757
+ /** Inside a group step: chart events are the controller's own doing. */
6758
+ this.stepping = false;
6759
+ /** Inside `finish`: the charts' `replay:end` events are the controller's own doing. */
6760
+ this.stopping = false;
6761
+ /** Driving a chart's own timer: its play/pause events are not the user's. */
6762
+ this.muted = 0;
6763
+ /** A chart finished during a group step — the session ends right after it. */
6764
+ this.finishedDuringStep = false;
6765
+ for (const cell of host.cells()) this.watch(cell);
6766
+ this.offHost = host.onCells((e) => {
6767
+ if (e.kind === "created") this.onCellCreated(e.id);
6768
+ else if (e.kind === "destroyed") this.onCellDestroyed(e.id);
6769
+ });
6770
+ }
6771
+ /**
6772
+ * Enter replay on every cell (or seek, when already replaying) — paused. Resolves once
6773
+ * every chart shows its rewound history (a chart whose history does not reach that far
6774
+ * loads older bars first). A chart with nothing to replay after the shared time stays
6775
+ * as it is.
6776
+ */
6777
+ async start(opts) {
6778
+ const cells = this.host.cells();
6779
+ const ref = cells.find((c) => c.id === (opts.cell ?? this.host.activeId())) ?? cells[0];
6780
+ if (!ref) return;
6781
+ if (this.playing) this.pause();
6782
+ this.rejoining.clear();
6783
+ await ref.chart.replay.start({ from: opts.from });
6784
+ const cursor = ref.chart.replay.state.cursorTime;
6785
+ if (cursor == null) return;
6786
+ const session = { clock: barClose(cursor, ref.chart.market.timeframe) };
6787
+ this.session = session;
6788
+ await Promise.all(this.host.cells().filter((c) => c.id !== ref.id).map((c) => this.startCell(c, session.clock)));
6789
+ if (this.session !== session) return;
6790
+ this.syncPlayback();
6791
+ const s = this.activeChart()?.replay.state;
6792
+ this.bus.emit("replay:start", { cursorTime: s?.cursorTime ?? cursor, remaining: s?.remaining ?? 0 });
6793
+ }
6794
+ /** Reveal the next bar(s): the clock moves to the next bar close on any chart. Returns false when nothing is left. */
6795
+ step() {
6796
+ const solo = this.solo();
6797
+ return solo ? solo.replay.step() : this.stepGroup();
6798
+ }
6799
+ /** Reveal the next update — in a single-chart layout, the chart's `stepUpdate()` (tick replay); otherwise {@link step}. */
6800
+ stepUpdate() {
6801
+ const solo = this.solo();
6802
+ return solo ? solo.replay.stepUpdate() : this.stepGroup();
6803
+ }
6804
+ /** Advance one update every `intervalMs` (default: the last pace, initially 1000). Calling it again while playing changes the pace. */
6805
+ play(intervalMs) {
6806
+ if (!this.session) return this;
6807
+ if (intervalMs !== void 0) {
6808
+ if (!(Number.isFinite(intervalMs) && intervalMs > 0)) {
6809
+ console.warn(`[vela] workspace.replay.play(${intervalMs}) ignored \u2014 the interval must be a positive number of ms`);
6810
+ return this;
6811
+ }
6812
+ this.intervalMs = intervalMs;
6813
+ }
6814
+ this.playing = true;
6815
+ this.clearTimer();
6816
+ this.syncPlayback();
6817
+ this.bus.emit("replay:play", { intervalMs: this.intervalMs });
6818
+ return this;
6819
+ }
6820
+ pause() {
6821
+ if (!this.playing) return this;
6822
+ this.playing = false;
6823
+ this.syncPlayback();
6824
+ this.bus.emit("replay:pause", void 0);
6825
+ return this;
6826
+ }
6827
+ /** Leave replay on every chart: full history back, live updates resumed. */
6828
+ stop() {
6829
+ this.finish("stopped");
6830
+ return this;
6831
+ }
6832
+ /** `active`/`playing`/`intervalMs` for the whole workspace; the cursor fields read the ACTIVE cell's chart. */
6833
+ get state() {
6834
+ const s = this.session ? this.activeChart()?.replay.state : void 0;
6835
+ return {
6836
+ active: this.session !== null,
6837
+ playing: this.playing,
6838
+ cursorTime: s?.cursorTime ?? null,
6839
+ remaining: s?.remaining ?? 0,
6840
+ nextTime: s?.nextTime ?? null,
6841
+ intervalMs: this.intervalMs
6842
+ };
6843
+ }
6844
+ /** The active cell's replayable history (`chart.replay.bounds`). */
6845
+ get bounds() {
6846
+ return this.activeChart()?.replay.bounds ?? null;
6847
+ }
6848
+ on(event, handler) {
6849
+ return this.bus.on(event, handler);
6850
+ }
6851
+ destroy() {
6852
+ this.session = null;
6853
+ this.playing = false;
6854
+ this.clearTimer();
6855
+ this.offHost();
6856
+ for (const off of this.cellSubs.values()) off();
6857
+ this.cellSubs.clear();
6858
+ this.bus.clear();
6859
+ }
6860
+ // ── internals ──
6861
+ /** The one chart of a single-chart layout — it replays on its own, tick replay included. */
6862
+ solo() {
6863
+ const cells = this.host.cells();
6864
+ return cells.length === 1 ? cells[0].chart : null;
6865
+ }
6866
+ activeChart() {
6867
+ const cells = this.host.cells();
6868
+ return (cells.find((c) => c.id === this.host.activeId()) ?? cells[0])?.chart ?? null;
6869
+ }
6870
+ /** Rewind one chart to the shared time: every bar that closed by `clock`, none after. */
6871
+ async startCell(cell, clock) {
6872
+ const replay = cell.chart.replay;
6873
+ const from = lastOpenClosedBy(clock, cell.chart.market.timeframe);
6874
+ const bounds = replay.bounds;
6875
+ if (!bounds || bounds.last <= from) return;
6876
+ await replay.start({ from });
6877
+ }
6878
+ /** Move the clock to the next bar close on any chart; each chart reveals what closed by then. */
6879
+ stepGroup() {
6880
+ const session = this.session;
6881
+ if (!session) return false;
6882
+ const cells = this.host.cells();
6883
+ let next = Infinity;
6884
+ for (const c of cells) {
6885
+ const t = c.chart.replay.state.nextTime;
6886
+ if (t != null) next = Math.min(next, barClose(t, c.chart.market.timeframe));
6887
+ }
6888
+ if (!Number.isFinite(next)) return false;
6889
+ this.stepping = true;
6890
+ try {
6891
+ for (const c of cells) {
6892
+ const tf = c.chart.market.timeframe;
6893
+ for (let n = 0; n < MAX_BARS_PER_STEP; n += 1) {
6894
+ const t = c.chart.replay.state.nextTime;
6895
+ if (t == null || barClose(t, tf) > next) break;
6896
+ if (!c.chart.replay.step()) break;
6897
+ }
6898
+ }
6899
+ } finally {
6900
+ this.stepping = false;
6901
+ }
6902
+ session.clock = next;
6903
+ if (this.finishedDuringStep) {
6904
+ this.finishedDuringStep = false;
6905
+ this.finish("finished");
6906
+ return true;
6907
+ }
6908
+ const s = this.activeChart()?.replay.state;
6909
+ if (s?.cursorTime != null) this.bus.emit("replay:step", { cursorTime: s.cursorTime, remaining: s.remaining });
6910
+ return true;
6911
+ }
6912
+ /** Put the timers in the state `playing` asks for: a single chart runs its own (it paces
6913
+ * ticks), several run on the controller's clock. */
6914
+ syncPlayback() {
6915
+ const solo = this.solo();
6916
+ const run = this.playing && this.session !== null;
6917
+ this.muted += 1;
6918
+ try {
6919
+ for (const c of this.host.cells()) {
6920
+ if (c.chart === solo && run) {
6921
+ if (!c.chart.replay.state.playing || c.chart.replay.state.intervalMs !== this.intervalMs) c.chart.replay.play(this.intervalMs);
6922
+ } else if (c.chart.replay.state.playing) {
6923
+ c.chart.replay.pause();
6924
+ }
6925
+ }
6926
+ } finally {
6927
+ this.muted -= 1;
6928
+ }
6929
+ if (!run || solo) this.clearTimer();
6930
+ else if (this.timer == null) this.schedule();
6931
+ }
6932
+ schedule() {
6933
+ this.timer = setTimeout(() => {
6934
+ this.timer = null;
6935
+ if (!this.playing || !this.session || this.solo()) return;
6936
+ if (this.stepGroup() || this.rejoining.size > 0) {
6937
+ if (this.playing && this.session && this.timer == null) this.schedule();
6938
+ } else {
6939
+ this.pause();
6940
+ }
6941
+ }, this.intervalMs);
6942
+ }
6943
+ clearTimer() {
6944
+ if (this.timer != null) clearTimeout(this.timer);
6945
+ this.timer = null;
6946
+ }
6947
+ /** End the session on every chart (idempotent). */
6948
+ finish(reason) {
6949
+ const had = this.session !== null;
6950
+ this.session = null;
6951
+ this.playing = false;
6952
+ this.clearTimer();
6953
+ this.rejoining.clear();
6954
+ this.stopping = true;
6955
+ try {
6956
+ for (const c of this.host.cells()) c.chart.replay.stop();
6957
+ } finally {
6958
+ this.stopping = false;
6959
+ }
6960
+ if (had) this.bus.emit("replay:end", { reason });
6961
+ }
6962
+ watch(cell) {
6963
+ const { id, chart } = cell;
6964
+ const isActive = () => (this.host.activeId() ?? this.host.cells()[0]?.id) === id;
6965
+ const offs = [
6966
+ chart.on("replay:end", ({ reason }) => {
6967
+ if (!this.session || this.stopping) return;
6968
+ if (reason === "market") this.rejoining.add(id);
6969
+ else if (this.stepping) this.finishedDuringStep || (this.finishedDuringStep = reason === "finished");
6970
+ else this.finish(reason);
6971
+ }),
6972
+ chart.on("market:changed", () => {
6973
+ if (!this.rejoining.delete(id) || !this.session) return;
6974
+ void this.startCell(cell, this.session.clock).then(() => this.syncPlayback());
6975
+ }),
6976
+ chart.on("replay:step", (e) => {
6977
+ if (this.session && !this.stepping && isActive()) this.bus.emit("replay:step", e);
6978
+ }),
6979
+ chart.on("replay:tick", (e) => {
6980
+ if (this.session && isActive()) this.bus.emit("replay:tick", e);
6981
+ }),
6982
+ // A single chart runs its own timer: a pause it takes on its own (a seek) is the session's.
6983
+ chart.on("replay:pause", () => {
6984
+ if (!this.session || this.muted > 0 || !this.playing || chart !== this.solo()) return;
6985
+ this.playing = false;
6986
+ this.bus.emit("replay:pause", void 0);
6987
+ })
6988
+ ];
6989
+ this.cellSubs.set(id, () => {
6990
+ for (const off of offs) off();
6991
+ });
6992
+ }
6993
+ onCellCreated(id) {
6994
+ const cell = this.host.cells().find((c) => c.id === id);
6995
+ if (!cell) return;
6996
+ this.watch(cell);
6997
+ const session = this.session;
6998
+ if (!session) return;
6999
+ this.rejoining.add(id);
7000
+ void cell.chart.ready().then(async () => {
7001
+ if (this.session !== session || !this.rejoining.delete(id)) return;
7002
+ await this.startCell(cell, session.clock);
7003
+ this.syncPlayback();
7004
+ });
7005
+ this.syncPlayback();
7006
+ }
7007
+ onCellDestroyed(id) {
7008
+ this.cellSubs.get(id)?.();
7009
+ this.cellSubs.delete(id);
7010
+ this.rejoining.delete(id);
7011
+ this.syncPlayback();
7012
+ }
7013
+ };
7014
+ function barClose(open, timeframe) {
7015
+ const months = monthSpan(timeframe);
7016
+ if (months > 0) {
7017
+ const d = new Date(open);
7018
+ return Date.UTC(d.getUTCFullYear(), d.getUTCMonth() + months, d.getUTCDate(), d.getUTCHours(), d.getUTCMinutes());
7019
+ }
7020
+ return open + timeframeToMs(timeframe ?? "60");
7021
+ }
7022
+ function lastOpenClosedBy(time, timeframe) {
7023
+ const months = monthSpan(timeframe);
7024
+ if (months > 0) {
7025
+ const d = new Date(time);
7026
+ return Date.UTC(d.getUTCFullYear(), d.getUTCMonth() - months, d.getUTCDate(), d.getUTCHours(), d.getUTCMinutes());
7027
+ }
7028
+ return time - timeframeToMs(timeframe ?? "60");
7029
+ }
7030
+ function monthSpan(timeframe) {
7031
+ const m = /^(\d*)M$/.exec((timeframe ?? "").trim());
7032
+ return m ? Number(m[1] || 1) : 0;
7033
+ }
7034
+
6642
7035
  // src/workspace/layouts.ts
6643
7036
  var registry = /* @__PURE__ */ new Map();
6644
7037
  function registerLayout(def) {
@@ -7095,16 +7488,16 @@ var MobileBar = class {
7095
7488
  host.appendChild(this.el);
7096
7489
  this.renderActions();
7097
7490
  }
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. */
7491
+ /** Re-project the contributed actions placed on the bar (left-aligned by default —
7492
+ * see `mobilePlacement`) as icon-only stops in the indicators slot (call after
7493
+ * registrations change). The rest are three-dots drawer rows. */
7101
7494
  renderActions() {
7102
7495
  const ctx = this.opts.getContext?.();
7103
7496
  if (!ctx) return;
7104
7497
  const doc = this.el.ownerDocument;
7105
7498
  this.actionsHost.replaceChildren();
7106
7499
  const builtin = new Set(TOPBAR_BUILTIN_IDS);
7107
- for (const action of widgetActions("topbar", ctx).filter((a) => a.align === "left" && !builtin.has(a.id))) {
7500
+ for (const action of widgetActions("topbar", ctx).filter((a) => mobilePlacement(a) === "bar" && !builtin.has(a.id))) {
7108
7501
  const b = doc.createElement("button");
7109
7502
  b.className = "vela-mb-item";
7110
7503
  b.setAttribute("aria-label", action.label);
@@ -7660,6 +8053,7 @@ var MoreDrawer = class {
7660
8053
  const value = shape ? `${shape.cols} \xD7 ${shape.rows}` : this.opts.layout.presets().find((p) => p.checked)?.label;
7661
8054
  list.appendChild(this.row(doc, "Layout", { icon: "layout", value, chevron: true, onClick: () => this.show("layout") }));
7662
8055
  }
8056
+ for (const act of this.opts.primaryActions?.() ?? []) list.appendChild(this.actionRow(doc, act));
7663
8057
  for (const panel of this.opts.panels()) {
7664
8058
  list.appendChild(
7665
8059
  this.row(doc, panel.title, {
@@ -7675,19 +8069,19 @@ var MoreDrawer = class {
7675
8069
  const alertCount = this.opts.alerts().length;
7676
8070
  list.appendChild(this.row(doc, "Alerts", { icon: "bell", value: alertCount > 0 ? String(alertCount) : void 0, chevron: true, onClick: () => this.show("alerts") }));
7677
8071
  }
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
- }
8072
+ for (const act of this.opts.actions()) list.appendChild(this.actionRow(doc, act));
7689
8073
  this.drawer.body.appendChild(list);
7690
8074
  }
8075
+ /** A contributed action's row: runs it and closes the drawer. */
8076
+ actionRow(doc, act) {
8077
+ return this.row(doc, act.label, {
8078
+ icon: act.icon,
8079
+ onClick: () => {
8080
+ act.run();
8081
+ this.drawer.hide();
8082
+ }
8083
+ });
8084
+ }
7691
8085
  renderStyle(doc) {
7692
8086
  const list = doc.createElement("div");
7693
8087
  list.className = "vela-md-list";
@@ -8151,6 +8545,7 @@ function buildContext(host) {
8151
8545
  setPriceStyle: (style) => host.active()?.setPriceStyle(style),
8152
8546
  openSymbolSearch: (query) => host.openSymbolSearch(query),
8153
8547
  togglePanel: (id, open) => host.togglePanel(id, open),
8548
+ dockStrip: (el) => host.dockStrip(el),
8154
8549
  host: host.root,
8155
8550
  toast: (message, kind) => host.toast(message, kind),
8156
8551
  addIndicator: (entry) => host.active()?.addExternalIndicator(entry),
@@ -8162,7 +8557,8 @@ function buildContext(host) {
8162
8557
  get activeCellId() {
8163
8558
  return host.active()?.id ?? "";
8164
8559
  },
8165
- setActiveCell: (id) => host.setActiveCell(id)
8560
+ setActiveCell: (id) => host.setActiveCell(id),
8561
+ replay: host.replay
8166
8562
  };
8167
8563
  }
8168
8564
 
@@ -8225,6 +8621,8 @@ var CSS21 = `
8225
8621
  .vela-workspace { position: relative; width: 100%; height: 100%; display: flex; flex-direction: column; background: var(--vela-bg); }
8226
8622
  .vela-ws-main { position: relative; display: flex; flex-direction: row; flex: 1 1 auto; min-height: 0; }
8227
8623
  .vela-ws-toolbar { position: relative; flex: none; }
8624
+ .vela-ws-strips { position: relative; flex: none; display: flex; flex-direction: column; min-width: 0; }
8625
+ .vela-ws-strips:empty { display: none; }
8228
8626
  .vela-ws-grid { position: relative; flex: 1 1 auto; min-width: 0; display: grid; gap: ${GAP_PX}px; background: var(--vela-border-soft); }
8229
8627
  .vela-cell { background: var(--vela-bg); position: relative; }
8230
8628
  /* Active-cell highlight: an overlay ring ABOVE the chart's own canvas stack (a plain
@@ -8283,11 +8681,11 @@ function resolveTyping(key, open) {
8283
8681
  if (open.dialogs > 0) {
8284
8682
  if (key.length !== 1) return null;
8285
8683
  if (open.symbolSearch) return { target: "symbol", text: key.toUpperCase() };
8286
- if (open.timeframeEntry) return { target: "timeframe", text: key };
8684
+ if (open.timeframeEntry) return { target: "timeframe", text: key.toUpperCase() };
8287
8685
  return null;
8288
8686
  }
8289
- if (/^[a-zA-Z]$/.test(key)) return { target: "symbol", text: key.toUpperCase() };
8290
- if (/^[0-9]$/.test(key)) return { target: "timeframe", text: key };
8687
+ if (/^[a-zA-Z0]$/.test(key)) return { target: "symbol", text: key.toUpperCase() };
8688
+ if (/^[1-9]$/.test(key)) return { target: "timeframe", text: key };
8291
8689
  return null;
8292
8690
  }
8293
8691
  var VelaWorkspace = class {
@@ -8396,6 +8794,20 @@ var VelaWorkspace = class {
8396
8794
  const hostEl = typeof container === "string" ? document.querySelector(container) : container;
8397
8795
  if (!hostEl) throw new Error(`VelaWorkspace: container not found: ${String(container)}`);
8398
8796
  this.opts = opts;
8797
+ this.replay = new WorkspaceReplay({
8798
+ cells: () => this.cells(),
8799
+ activeId: () => this.activeId,
8800
+ onCells: (handler) => {
8801
+ const offs = [
8802
+ this.events.on("cell:created", ({ id }) => handler({ kind: "created", id })),
8803
+ this.events.on("cell:destroyed", ({ id }) => handler({ kind: "destroyed", id })),
8804
+ this.events.on("cell:active", ({ id }) => handler({ kind: "active", id }))
8805
+ ];
8806
+ return () => {
8807
+ for (const off of offs) off();
8808
+ };
8809
+ }
8810
+ });
8399
8811
  this.persistKey = opts.persist === void 0 || opts.persist === false ? null : opts.persist === true ? "vela-workspace" : opts.persist;
8400
8812
  this.storage = opts.storage ?? localStorageAdapter();
8401
8813
  let boot = null;
@@ -8533,6 +8945,9 @@ var VelaWorkspace = class {
8533
8945
  this.dock.refresh();
8534
8946
  if (boot?.panels) this.dock.applyState(boot.panels);
8535
8947
  this.root.appendChild(main);
8948
+ this.stripsEl = doc.createElement("div");
8949
+ this.stripsEl.className = "vela-ws-strips";
8950
+ this.root.appendChild(this.stripsEl);
8536
8951
  this.toastHost = new Toast(this.gridEl);
8537
8952
  const attribution = rendererDefaults().attribution;
8538
8953
  if (attribution !== false) {
@@ -8767,9 +9182,16 @@ var VelaWorkspace = class {
8767
9182
  setActiveCell: (id) => this.setActiveCell(id),
8768
9183
  openSymbolSearch: (query) => this.symbolPicker.open(query ?? ""),
8769
9184
  togglePanel: (id, open) => this.dock.toggle(id, open),
9185
+ dockStrip: (el) => {
9186
+ this.stripsEl.appendChild(el);
9187
+ return () => {
9188
+ if (el.parentElement === this.stripsEl) el.remove();
9189
+ };
9190
+ },
8770
9191
  root: this.root,
8771
9192
  toast: (message, kind) => this.toastHost.show(message, kind),
8772
- stateDirty: () => this.markStateDirty()
9193
+ stateDirty: () => this.markStateDirty(),
9194
+ replay: this.replay
8773
9195
  });
8774
9196
  }
8775
9197
  /** Re-project contributed topbar actions + side panels, and mount late-registered attachments. */
@@ -9077,6 +9499,7 @@ var VelaWorkspace = class {
9077
9499
  if (this.destroyed) return;
9078
9500
  this.flushPendingState();
9079
9501
  this.destroyed = true;
9502
+ this.replay.destroy();
9080
9503
  if (this.persistKey !== null && typeof window !== "undefined") window.removeEventListener("beforeunload", this.onUnload);
9081
9504
  this.resizeObserver?.disconnect();
9082
9505
  this.splitters.destroy();
@@ -9843,14 +10266,13 @@ var VelaWorkspace = class {
9843
10266
  panels: () => has("panels") ? [...this.dock.list()] : [],
9844
10267
  onTogglePanel: (id) => this.dock.toggle(id),
9845
10268
  ...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
- },
10269
+ // Actions placed on the bottom bar (left-aligned by default) have their stop
10270
+ // there — only the menu-placed ones land in the drawer, or they would appear
10271
+ // twice: left (primary) ones with the primary rows, right ones at the end.
10272
+ // Built-in-id actions are slot OVERRIDES: they reach the drawer through the
10273
+ // slot's own routed button (screenshot) or stop (indicators), never as a row.
10274
+ primaryActions: () => this.drawerActions("left"),
10275
+ actions: () => this.drawerActions("right"),
9854
10276
  // The desktop layout dropdown's whole surface — the grid canvas, the
9855
10277
  // non-canvas presets and the sync switches — relocated into the kebab
9856
10278
  // drawer (the topbar is hidden on mobile). Same reads as the topbar block;
@@ -9875,6 +10297,11 @@ var VelaWorkspace = class {
9875
10297
  }));
9876
10298
  this.moreDrawer.open();
9877
10299
  }
10300
+ /** The contributed topbar actions the mobile menu lists, from one cluster. */
10301
+ drawerActions(cluster) {
10302
+ const builtin = new Set(TOPBAR_BUILTIN_IDS);
10303
+ 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()) }));
10304
+ }
9878
10305
  openTimezoneDrawer() {
9879
10306
  this.timezoneDrawer ?? (this.timezoneDrawer = new TimezoneDrawer({
9880
10307
  host: this.root,
@@ -9996,7 +10423,7 @@ var VelaWorkspace = class {
9996
10423
  }
9997
10424
  });
9998
10425
  }
9999
- /** Bare-typing router: letters → symbol search (seeded), digits → timeframe entry. */
10426
+ /** Bare-typing router: letters and `0` → symbol search (seeded), digits 1–9 → timeframe entry. */
10000
10427
  routeTyping(ev) {
10001
10428
  if (this.destroyed) return;
10002
10429
  if (ev.ctrlKey || ev.metaKey || ev.altKey) return;
@@ -10038,4 +10465,4 @@ var VelaWorkspace = class {
10038
10465
  }
10039
10466
  };
10040
10467
 
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 };
10468
+ 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 };