@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.
- package/dist/{DataProvider-CGa4KHRa.d.cts → DataProvider-BrlxntPN.d.cts} +1 -1
- package/dist/{DataProvider-CmWUtDVL.d.ts → DataProvider-DwFIoV4r.d.ts} +1 -1
- package/dist/{chunk-WTWGEB7K.js → chunk-2IAETM2I.js} +260 -2
- package/dist/{chunk-CAFCLMPF.js → chunk-BZQM2XO7.js} +1 -0
- package/dist/{chunk-ZA2XWTGH.js → chunk-E3GU6AUI.js} +499 -43
- package/dist/{chunk-BFA32GOU.js → chunk-EZ5FWVLA.js} +5 -2
- package/dist/{chunk-PVGWNF7V.js → chunk-KA77UTRF.js} +2 -2
- package/dist/{chunk-RVQWJOEE.js → chunk-QBPUT5SU.js} +2862 -1034
- package/dist/{contributions-DF_Ea07m.d.cts → contributions-CtlptxR8.d.cts} +187 -5
- package/dist/{contributions-nKOSeLy8.d.ts → contributions-tjNExo1o.d.ts} +187 -5
- package/dist/index.cjs +3142 -1030
- package/dist/index.d.cts +40 -8
- package/dist/index.d.ts +40 -8
- package/dist/index.js +28 -4
- package/dist/{options-D1AJKsn_.d.ts → options-BnEPUuVV.d.cts} +1 -1
- package/dist/{options-D1AJKsn_.d.cts → options-BnEPUuVV.d.ts} +1 -1
- package/dist/{plugin-DPyVJFoA.d.cts → plugin-BWxJRKzS.d.cts} +31 -25
- package/dist/{plugin-0oFDnexb.d.ts → plugin-e2WUEtiI.d.ts} +31 -25
- package/dist/plugin.cjs +1 -0
- package/dist/plugin.d.cts +4 -4
- package/dist/plugin.d.ts +4 -4
- package/dist/plugin.js +2 -2
- package/dist/providers/binance.d.cts +2 -2
- package/dist/providers/binance.d.ts +2 -2
- package/dist/providers/coinbase.d.cts +2 -2
- package/dist/providers/coinbase.d.ts +2 -2
- package/dist/providers/hyperliquid.d.cts +2 -2
- package/dist/providers/hyperliquid.d.ts +2 -2
- package/dist/{statusline-model-DAFyqVmR.d.ts → statusline-model-DFY7tuHj.d.cts} +5 -3
- package/dist/{statusline-model-BoWJ3cWQ.d.cts → statusline-model-zKXtGIjj.d.ts} +5 -3
- package/dist/ui.cjs +263 -0
- package/dist/ui.d.cts +75 -2
- package/dist/ui.d.ts +75 -2
- package/dist/ui.js +3 -3
- package/dist/vela.global.js +3144 -1032
- package/dist/vela.global.min.js +78 -76
- package/dist/widget.cjs +3627 -1081
- package/dist/widget.d.cts +26 -7
- package/dist/widget.d.ts +26 -7
- package/dist/widget.js +7 -7
- package/dist/workspace.cjs +3630 -1081
- package/dist/workspace.d.cts +126 -6
- package/dist/workspace.d.ts +126 -6
- package/dist/workspace.js +6 -6
- 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
|
|
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-
|
|
3
|
-
import { Tooltip, KeymapManager, isEditableTarget, Drawer } from './chunk-
|
|
4
|
-
import { Menu, Dialog, CalloutBubble, applyPlotOverlayTokens, ensureUIHost } from './chunk-
|
|
5
|
-
import { registerIcon, svg16, injectStyles, iconEl, iconMarkup, iconAt, SESSION_OFF, SESSION_POST, SESSION_PRE, icon, categoricalColor } from './chunk-
|
|
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
|
-
|
|
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.
|
|
4356
|
-
this.
|
|
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.
|
|
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
|
-
|
|
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", () =>
|
|
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
|
|
7065
|
-
* indicators slot (call after
|
|
7066
|
-
*
|
|
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
|
|
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
|
-
//
|
|
9813
|
-
//
|
|
9814
|
-
//
|
|
9815
|
-
//
|
|
9816
|
-
|
|
9817
|
-
|
|
9818
|
-
|
|
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 };
|