@luxalgo/vela 0.6.4 → 0.6.6
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/README.md +16 -16
- package/dist/{DataProvider-DrN7ZIou.d.cts → DataProvider-DDUYw2qP.d.cts} +1 -1
- package/dist/{DataProvider-BmHkcwvJ.d.ts → DataProvider-JJq7M2it.d.ts} +1 -1
- package/dist/{history-CvOtsjT5.d.ts → bottombar-COnL2Sk4.d.ts} +110 -127
- package/dist/{history-BmyjaVir.d.cts → bottombar-DeDXbF_K.d.cts} +110 -127
- package/dist/{chunk-6FA4TDUF.js → chunk-JQFO2WMU.js} +2666 -2333
- package/dist/{chunk-BEJ57HP4.js → chunk-OEUGUXL7.js} +14 -1
- package/dist/{chunk-3QX6QOCX.js → chunk-TMXK2SJR.js} +5852 -3340
- package/dist/{contributions-2bEmCF9S.d.cts → contributions-BqCIsbaP.d.ts} +108 -4
- package/dist/{contributions-DatoAzMx.d.ts → contributions-DNMqrAuw.d.cts} +108 -4
- package/dist/index.cjs +2681 -2332
- package/dist/index.d.cts +65 -14
- package/dist/index.d.ts +65 -14
- package/dist/index.js +2 -2
- package/dist/{options-YdiaaVjt.d.cts → options-BaVTMXaO.d.cts} +48 -2
- package/dist/{options-YdiaaVjt.d.ts → options-BaVTMXaO.d.ts} +48 -2
- package/dist/{plugin-CkiStRaI.d.cts → plugin-CN8U2__Q.d.cts} +11 -5
- package/dist/{plugin-BeCMF1VQ.d.ts → plugin-WnvNNJOp.d.ts} +11 -5
- package/dist/plugin.cjs +16 -0
- package/dist/plugin.d.cts +4 -4
- package/dist/plugin.d.ts +4 -4
- package/dist/plugin.js +1 -1
- 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/ui.d.cts +1 -1
- package/dist/ui.d.ts +1 -1
- package/dist/vela.global.js +2661 -2312
- package/dist/vela.global.min.js +48 -48
- package/dist/widget.cjs +35658 -34074
- package/dist/widget.d.cts +39 -271
- package/dist/widget.d.ts +39 -271
- package/dist/widget.js +60 -1313
- package/dist/workspace.cjs +1584 -960
- package/dist/workspace.d.cts +111 -15
- package/dist/workspace.d.ts +111 -15
- package/dist/workspace.js +6 -2281
- package/package.json +1 -1
package/dist/workspace.js
CHANGED
|
@@ -1,2281 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
import
|
|
4
|
-
import
|
|
5
|
-
import
|
|
6
|
-
import
|
|
7
|
-
import { registerIcon, svg16, injectStyles } from './chunk-PN5KWFZ4.js';
|
|
8
|
-
|
|
9
|
-
// src/workspace/sync.ts
|
|
10
|
-
function syncTargets(originId, setting, cellIds) {
|
|
11
|
-
if (setting == null || setting === false) return [];
|
|
12
|
-
if (setting === true) return cellIds.filter((id) => id !== originId);
|
|
13
|
-
const group = setting[originId];
|
|
14
|
-
if (group == null) return [];
|
|
15
|
-
return cellIds.filter((id) => id !== originId && setting[id] === group);
|
|
16
|
-
}
|
|
17
|
-
function rangesWithin(a, b, epsMs) {
|
|
18
|
-
return Math.abs(a.from - b.from) <= epsMs && Math.abs(a.to - b.to) <= epsMs;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
// src/workspace/persist.ts
|
|
22
|
-
var memoryStore = /* @__PURE__ */ new Map();
|
|
23
|
-
function memoryStorageAdapter() {
|
|
24
|
-
return {
|
|
25
|
-
get: (key) => memoryStore.get(key) ?? null,
|
|
26
|
-
set: (key, value) => {
|
|
27
|
-
memoryStore.set(key, value);
|
|
28
|
-
},
|
|
29
|
-
remove: (key) => {
|
|
30
|
-
memoryStore.delete(key);
|
|
31
|
-
}
|
|
32
|
-
};
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
// src/workspace/ChartCell.ts
|
|
36
|
-
function seedDefaults(opts) {
|
|
37
|
-
return {
|
|
38
|
-
symbol: opts.symbol,
|
|
39
|
-
timeframe: opts.timeframe,
|
|
40
|
-
bars: opts.bars,
|
|
41
|
-
priceStyle: opts.priceStyle,
|
|
42
|
-
session: opts.session,
|
|
43
|
-
data: opts.data,
|
|
44
|
-
visibleRange: opts.visibleRange
|
|
45
|
-
};
|
|
46
|
-
}
|
|
47
|
-
function cellChartDefaults(opts) {
|
|
48
|
-
const { renderer, defaultLanguage, currentPriceLine, logScale, animations, glow, upColor, downColor, drawings } = opts;
|
|
49
|
-
return { renderer, defaultLanguage, currentPriceLine, logScale, animations, glow, upColor, downColor, drawings };
|
|
50
|
-
}
|
|
51
|
-
function cellDrawings(opt) {
|
|
52
|
-
if (opt === false) return false;
|
|
53
|
-
if (opt === true || opt == null) return { toolbar: false };
|
|
54
|
-
return { ...opt, toolbar: false };
|
|
55
|
-
}
|
|
56
|
-
var ChartCell = class {
|
|
57
|
-
constructor(id, gridHost, seed, deps) {
|
|
58
|
-
this.id = id;
|
|
59
|
-
this.deps = deps;
|
|
60
|
-
/** This cell's unified app+drawings undo timeline (the shared Ctrl+Z routes here). */
|
|
61
|
-
this.history = new WidgetHistory(() => this.inner);
|
|
62
|
-
/** Live manifest-indicator instances on this cell (the SAME entry may repeat). */
|
|
63
|
-
this.instances = [];
|
|
64
|
-
/** The native-indicator catalog with this cell's live supported/present flags. */
|
|
65
|
-
this.nativeCatalog = [];
|
|
66
|
-
/** Last crosshair position in this cell (the alt+H/alt+V shortcuts anchor here). */
|
|
67
|
-
this.lastCrossTime = null;
|
|
68
|
-
this.lastCrossPrice = null;
|
|
69
|
-
/** The bottombar range chip this cell is framed on (null = none). */
|
|
70
|
-
this.activeRangeId = null;
|
|
71
|
-
/** Latched verdict of {@link sessionAvailable} (async metadata, sticky per symbol). */
|
|
72
|
-
this.sessionAvailableFlag = false;
|
|
73
|
-
/** Keeps the pre/post-market shading on the symbol's real calendar (see {@link SessionShadingTracker}). */
|
|
74
|
-
this.sessionShading = new SessionShadingTracker((zones) => this.inner?.renderer.set("sessionZones", zones));
|
|
75
|
-
this.manifest = [];
|
|
76
|
-
/** A restored ledger's manifest entry NAMES, waiting for the manifest to resolve
|
|
77
|
-
* (a pool/persisted cell can be built before the shared manifest has loaded). */
|
|
78
|
-
this.pendingManifestNames = null;
|
|
79
|
-
/** The volume auto-add rides the cell's first candles (`load:end`); until then the
|
|
80
|
-
* registry can't show it and the dehydrated ledger reports the INTENT instead. */
|
|
81
|
-
this.volumeMayBePending = true;
|
|
82
|
-
/** Sync mirror of the chart's present native types — the removal handler diffs
|
|
83
|
-
* against it to identify (and record) whichever type was just removed. */
|
|
84
|
-
this.presentNatives = [];
|
|
85
|
-
this.rangeBars = 0;
|
|
86
|
-
this.pendingRange = null;
|
|
87
|
-
/** Indicator titles (this cell's in-chart legend rows) shown. */
|
|
88
|
-
this.indicatorTitlesOn = true;
|
|
89
|
-
/** Plot values beside this cell's legend titles shown. */
|
|
90
|
-
this.indicatorValuesOn = true;
|
|
91
|
-
this.destroyed = false;
|
|
92
|
-
this.appTheme = deps.theme;
|
|
93
|
-
const symbol = prefixedSymbol(seed);
|
|
94
|
-
this.state = { symbol, provider: parseSymbol(symbol ?? "").provider ?? void 0, timeframe: seed.timeframe, priceStyle: seed.priceStyle, bars: seed.bars, session: normalizeSession(seed.session) };
|
|
95
|
-
const doc = gridHost.ownerDocument;
|
|
96
|
-
this.host = doc.createElement("div");
|
|
97
|
-
this.host.className = "vela-cell";
|
|
98
|
-
this.host.dataset.cellId = id;
|
|
99
|
-
this.host.style.cssText = "position:relative;overflow:hidden;";
|
|
100
|
-
this.host.addEventListener("pointerdown", () => this.deps.activate(id), true);
|
|
101
|
-
this.host.addEventListener("focusin", () => this.deps.activate(id));
|
|
102
|
-
gridHost.appendChild(this.host);
|
|
103
|
-
this.inner = new Vela(
|
|
104
|
-
this.host,
|
|
105
|
-
{
|
|
106
|
-
...deps.chartDefaults,
|
|
107
|
-
symbol,
|
|
108
|
-
timeframe: seed.timeframe,
|
|
109
|
-
bars: seed.bars,
|
|
110
|
-
priceStyle: seed.priceStyle,
|
|
111
|
-
session: normalizeSession(seed.session),
|
|
112
|
-
data: seed.data,
|
|
113
|
-
visibleRange: seed.visibleRange,
|
|
114
|
-
theme: deps.theme,
|
|
115
|
-
live: deps.live,
|
|
116
|
-
// A RESTORED ledger is authoritative for the auto-added volume too: a
|
|
117
|
-
// slot persisted without it must come back without it (fresh slots
|
|
118
|
-
// keep the workspace default).
|
|
119
|
-
volume: seed.indicators ? seed.indicators.natives.includes("volume") : deps.volume,
|
|
120
|
-
nativeBackend: deps.nativeBackend,
|
|
121
|
-
// The user's drawings option minus its toolbar: one SHARED bar serves
|
|
122
|
-
// the whole workspace (per-cell bars would cost a 44px gutter each).
|
|
123
|
-
drawings: cellDrawings(deps.chartDefaults.drawings)
|
|
124
|
-
},
|
|
125
|
-
{ dataFeed: deps.feed }
|
|
126
|
-
);
|
|
127
|
-
for (const [language, make] of Object.entries(resolveEngines(deps.engines))) this.inner.registerEngine(language, make());
|
|
128
|
-
this.inner.renderer.set("attribution", false);
|
|
129
|
-
this.inner.renderer.set("dialogHost", deps.dialogHost);
|
|
130
|
-
this.inner.renderer.setLegendActions(legendActionsProviderFor(this.inner, () => deps.context()));
|
|
131
|
-
if (this.inner.renderer.supports("historyChords")) this.inner.renderer.set("historyChords", false);
|
|
132
|
-
this.history.onChart(this.inner);
|
|
133
|
-
this.inner.renderer.onConfigChanged(() => {
|
|
134
|
-
const zone = this.inner?.renderer.get("timezone");
|
|
135
|
-
if (typeof zone === "string" && normalizeTimezone(zone) !== normalizeTimezone(this.deps.timezone())) {
|
|
136
|
-
this.deps.setTimezone(normalizeTimezone(zone));
|
|
137
|
-
}
|
|
138
|
-
this.syncStatuslineColors();
|
|
139
|
-
this.syncPlotOverlayTokens();
|
|
140
|
-
});
|
|
141
|
-
this.inner.on("theme:changed", (t) => {
|
|
142
|
-
this.appTheme = t;
|
|
143
|
-
this.syncPlotOverlayTokens();
|
|
144
|
-
});
|
|
145
|
-
this.syncPlotOverlayTokens();
|
|
146
|
-
if (seed.rendererConfig != null) this.inner.renderer.applyConfig(seed.rendererConfig);
|
|
147
|
-
if (seed.drawings != null) this.inner.drawings.fromJSON(seed.drawings);
|
|
148
|
-
if (seed.indicators) {
|
|
149
|
-
for (const type of seed.indicators.natives) this.inner.addNativeIndicator(type);
|
|
150
|
-
this.pendingManifestNames = [...seed.indicators.manifest];
|
|
151
|
-
}
|
|
152
|
-
this.volumeIntent = seed.indicators ? seed.indicators.natives.includes("volume") : deps.volume;
|
|
153
|
-
this.inner.on("load:end", () => {
|
|
154
|
-
this.volumeMayBePending = false;
|
|
155
|
-
});
|
|
156
|
-
this.inner.on("load:start", () => this.watermark?.setLoading(true));
|
|
157
|
-
this.inner.on("load:end", () => this.watermark?.setLoading(false));
|
|
158
|
-
const tz = deps.timezone();
|
|
159
|
-
if (tz !== "Etc/UTC") this.inner.renderer.set("timezone", tz);
|
|
160
|
-
this.indicatorTitlesOn = seed.indicatorTitles ?? true;
|
|
161
|
-
if (!this.indicatorTitlesOn) this.inner.renderer.set("indicatorTitles", false);
|
|
162
|
-
this.indicatorValuesOn = seed.indicatorValues ?? true;
|
|
163
|
-
if (!this.indicatorValuesOn) this.inner.renderer.set("indicatorValues", false);
|
|
164
|
-
this.watermarkOn = seed.watermark ?? deps.watermark;
|
|
165
|
-
this.watermark = deps.watermark ? new Watermark(this.host, symbol ?? "", seed.timeframe ?? "60") : null;
|
|
166
|
-
if (!this.watermarkOn) this.watermark?.setVisible(false);
|
|
167
|
-
this.statusline = deps.statusline ? new Statusline(this.host, symbol ?? "") : null;
|
|
168
|
-
this.statusline?.setMeta(seed.timeframe ?? "60", this.state.provider ?? "");
|
|
169
|
-
this.statusline?.onChart(this.inner);
|
|
170
|
-
this.marketStatus = this.statusline ? new MarketStatusTracker((s) => this.statusline?.setMarketStatus(s)) : null;
|
|
171
|
-
void this.inner.data.ready().then(() => {
|
|
172
|
-
if (this.inner && this.state.symbol) this.statusline?.setMeta(this.state.timeframe ?? "60", this.inner.data.displayPrefix(this.state.symbol) ?? this.state.provider ?? "");
|
|
173
|
-
this.refreshSessionAvailable();
|
|
174
|
-
if (this.inner && this.state.symbol) this.marketStatus?.track(this.inner.data, this.state.symbol);
|
|
175
|
-
});
|
|
176
|
-
this.syncStatuslineColors();
|
|
177
|
-
this.contextMenu = new ChartContextMenu(this.host, {
|
|
178
|
-
resetView: () => {
|
|
179
|
-
this.inner?.renderer.set("autoScale", true);
|
|
180
|
-
this.inner?.setVisibleRangePreset("ALL");
|
|
181
|
-
},
|
|
182
|
-
timezone: () => this.deps.timezone(),
|
|
183
|
-
setTimezone: (zone) => this.deps.setTimezone(zone),
|
|
184
|
-
// Right-clicking activates the cell first (capture-phase pointerdown), so the
|
|
185
|
-
// context the actions receive is this cell's — the active one.
|
|
186
|
-
getContext: () => this.deps.context()
|
|
187
|
-
});
|
|
188
|
-
this.contextMenu.onChart(this.inner);
|
|
189
|
-
this.inner.renderer.onCrosshairMove((e) => {
|
|
190
|
-
this.lastCrossTime = e.time;
|
|
191
|
-
this.lastCrossPrice = e.price;
|
|
192
|
-
});
|
|
193
|
-
this.inner.on("indicator:added", () => {
|
|
194
|
-
this.syncPresentNatives();
|
|
195
|
-
this.refreshNativeCatalog();
|
|
196
|
-
});
|
|
197
|
-
this.inner.on("indicator:removed", ({ id: id2 }) => {
|
|
198
|
-
if (this.destroyed) return;
|
|
199
|
-
const idx = this.instances.findIndex((it) => it.handle?.id === id2);
|
|
200
|
-
if (idx >= 0) {
|
|
201
|
-
const snapshot = this.instances[idx];
|
|
202
|
-
this.instances.splice(idx, 1);
|
|
203
|
-
this.history.push({
|
|
204
|
-
undo: () => {
|
|
205
|
-
snapshot.handle = this.addToChart(snapshot.entry);
|
|
206
|
-
this.instances.push(snapshot);
|
|
207
|
-
this.deps.onIndicatorsChanged(this.id);
|
|
208
|
-
},
|
|
209
|
-
redo: () => this.dropInstance(snapshot)
|
|
210
|
-
});
|
|
211
|
-
} else {
|
|
212
|
-
const now = this.inner?.presentNativeIndicators() ?? [];
|
|
213
|
-
for (const type of this.presentNatives.filter((t) => !now.includes(t))) {
|
|
214
|
-
this.history.push({
|
|
215
|
-
undo: () => {
|
|
216
|
-
this.inner?.addNativeIndicator(type);
|
|
217
|
-
this.refreshNativeCatalog();
|
|
218
|
-
},
|
|
219
|
-
redo: () => {
|
|
220
|
-
this.inner?.addNativeIndicator(type).remove();
|
|
221
|
-
this.refreshNativeCatalog();
|
|
222
|
-
}
|
|
223
|
-
});
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
this.syncPresentNatives();
|
|
227
|
-
this.refreshNativeCatalog();
|
|
228
|
-
});
|
|
229
|
-
this.syncPresentNatives();
|
|
230
|
-
this.refreshNativeCatalog();
|
|
231
|
-
this.pushSettingsSections();
|
|
232
|
-
this.offMarket = this.inner.on("market:changed", ({ symbol: symbol2, timeframe }) => {
|
|
233
|
-
this.state.symbol = symbol2;
|
|
234
|
-
this.state.provider = parseSymbol(symbol2).provider ?? void 0;
|
|
235
|
-
this.state.timeframe = timeframe;
|
|
236
|
-
this.state.session = normalizeSession(this.inner?.market.session);
|
|
237
|
-
this.watermark?.update(symbol2, timeframe);
|
|
238
|
-
this.statusline?.setSymbol(symbol2);
|
|
239
|
-
this.statusline?.setMeta(timeframe, this.inner?.data.displayPrefix(symbol2) ?? this.state.provider ?? "");
|
|
240
|
-
if (this.inner) this.statusline?.onChart(this.inner);
|
|
241
|
-
this.refreshNativeCatalog();
|
|
242
|
-
this.refreshSessionAvailable();
|
|
243
|
-
if (this.inner) this.marketStatus?.track(this.inner.data, symbol2);
|
|
244
|
-
this.deps.onMarketChanged(this.id);
|
|
245
|
-
});
|
|
246
|
-
}
|
|
247
|
-
/**
|
|
248
|
-
* Does this cell's market HAVE sessions (RTH/ETH meaningful)? Derived from the
|
|
249
|
-
* symbol's own metadata (`syminfo.session !== '24x7'`), asynchronously — the
|
|
250
|
-
* workspace re-projects the shared bottombar when the verdict lands or changes.
|
|
251
|
-
*/
|
|
252
|
-
get sessionAvailable() {
|
|
253
|
-
return this.sessionAvailableFlag;
|
|
254
|
-
}
|
|
255
|
-
/** This cell's shown session (`regular` when unset — the provider default). */
|
|
256
|
-
get session() {
|
|
257
|
-
return normalizeSession(this.state.session) ?? "regular";
|
|
258
|
-
}
|
|
259
|
-
/** Switch this cell's shown session in place (a reload — RTH and ETH are different bars). */
|
|
260
|
-
setSession(session) {
|
|
261
|
-
if (session === this.session) return;
|
|
262
|
-
this.state.session = session;
|
|
263
|
-
this.deps.onStateDirty();
|
|
264
|
-
void this.inner?.setMarket({ session });
|
|
265
|
-
}
|
|
266
|
-
refreshSessionAvailable() {
|
|
267
|
-
const chart = this.inner;
|
|
268
|
-
const symbol = this.state.symbol;
|
|
269
|
-
if (!chart || !symbol) return;
|
|
270
|
-
void chart.data.symbolInfo(symbol).then((si) => {
|
|
271
|
-
if (this.inner !== chart) return;
|
|
272
|
-
const available = typeof si?.session === "string" && si.session !== "" && si.session !== "24x7";
|
|
273
|
-
if (available !== this.sessionAvailableFlag) {
|
|
274
|
-
this.sessionAvailableFlag = available;
|
|
275
|
-
this.deps.onMarketChanged(this.id);
|
|
276
|
-
this.pushSettingsSections();
|
|
277
|
-
}
|
|
278
|
-
this.refreshSessionShading();
|
|
279
|
-
});
|
|
280
|
-
}
|
|
281
|
-
/** (Re)derive the pre/post-market shading bands for this cell's market. The loaded
|
|
282
|
-
* depth (bars × timeframe) bounds the calendar fetch; the tracker clamps it. */
|
|
283
|
-
refreshSessionShading() {
|
|
284
|
-
const chart = this.inner;
|
|
285
|
-
const symbol = this.state.symbol;
|
|
286
|
-
if (!chart || !symbol) return;
|
|
287
|
-
const lookbackMs = Math.max(this.state.bars ?? 1e3, this.rangeBars) * timeframeMs(this.state.timeframe ?? "60");
|
|
288
|
-
this.sessionShading.track(chart.data, symbol, { session: this.session, lookbackMs });
|
|
289
|
-
}
|
|
290
|
-
/** The session-shade colors live in the renderer CONFIG (persisted with it, edited
|
|
291
|
-
* live by the dialog swatch) — the cell only proxies them into its settings rows. */
|
|
292
|
-
sessionShadeColor(key) {
|
|
293
|
-
const cfg = this.inner?.renderer.getConfig();
|
|
294
|
-
const v = cfg?.sessions?.[key];
|
|
295
|
-
return typeof v === "string" ? v : "";
|
|
296
|
-
}
|
|
297
|
-
setSessionShadeColor(key, color) {
|
|
298
|
-
this.inner?.renderer.applyConfig({ sessions: { [key]: color } });
|
|
299
|
-
this.deps.onStateDirty();
|
|
300
|
-
}
|
|
301
|
-
/**
|
|
302
|
-
* (Re)contribute this cell's settings-dialog sections: status line parts, the
|
|
303
|
-
* per-cell fetch depth, the watermark toggle, and — only while the cell's symbol
|
|
304
|
-
* HAS sessions — the Trading session group (RTH/ETH switch + the pre/post-market
|
|
305
|
-
* shading colors) inside the Symbol tab. Bars/watermark/titles are persistable
|
|
306
|
-
* cell state; a depth-only reload is silent, so mark dirty here. Re-run whenever
|
|
307
|
-
* a gate changes (the dialog reads the sections on open).
|
|
308
|
-
*/
|
|
309
|
-
pushSettingsSections() {
|
|
310
|
-
const chart = this.inner;
|
|
311
|
-
if (!chart) return;
|
|
312
|
-
const rth = "Regular hours (RTH)";
|
|
313
|
-
const eth = "Extended hours (ETH)";
|
|
314
|
-
const sessionSection = {
|
|
315
|
-
title: "Trading session",
|
|
316
|
-
placement: "symbol",
|
|
317
|
-
rows: [
|
|
318
|
-
{
|
|
319
|
-
kind: "select",
|
|
320
|
-
label: "Session",
|
|
321
|
-
options: [rth, eth],
|
|
322
|
-
get: () => this.session === "extended" ? eth : rth,
|
|
323
|
-
set: (v) => this.setSession(v === eth ? "extended" : "regular")
|
|
324
|
-
},
|
|
325
|
-
{
|
|
326
|
-
kind: "color",
|
|
327
|
-
label: "Pre-market",
|
|
328
|
-
get: () => this.sessionShadeColor("premarketColor"),
|
|
329
|
-
set: (v) => this.setSessionShadeColor("premarketColor", v)
|
|
330
|
-
},
|
|
331
|
-
{
|
|
332
|
-
kind: "color",
|
|
333
|
-
label: "Post-market",
|
|
334
|
-
get: () => this.sessionShadeColor("postmarketColor"),
|
|
335
|
-
set: (v) => this.setSessionShadeColor("postmarketColor", v)
|
|
336
|
-
}
|
|
337
|
-
]
|
|
338
|
-
};
|
|
339
|
-
const advanced = {
|
|
340
|
-
title: "Advanced",
|
|
341
|
-
placement: "end",
|
|
342
|
-
rows: [
|
|
343
|
-
{
|
|
344
|
-
kind: "select",
|
|
345
|
-
label: "Bars to fetch",
|
|
346
|
-
options: ["500", "1000", "2000", "5000", "10000", "20000"],
|
|
347
|
-
get: () => String(this.state.bars ?? 1e3),
|
|
348
|
-
set: (v) => {
|
|
349
|
-
this.state.bars = Number(v);
|
|
350
|
-
this.deps.onStateDirty();
|
|
351
|
-
void this.inner?.setMarket({ bars: Math.max(this.state.bars, this.rangeBars) });
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
]
|
|
355
|
-
};
|
|
356
|
-
const watermarkSection = {
|
|
357
|
-
title: "Watermark",
|
|
358
|
-
placement: "symbol",
|
|
359
|
-
rows: [
|
|
360
|
-
{
|
|
361
|
-
kind: "toggle",
|
|
362
|
-
label: "Symbol watermark",
|
|
363
|
-
get: () => this.watermarkOn,
|
|
364
|
-
set: (v) => this.setWatermarkVisible(v)
|
|
365
|
-
}
|
|
366
|
-
]
|
|
367
|
-
};
|
|
368
|
-
const sections = [];
|
|
369
|
-
if (this.statusline) {
|
|
370
|
-
const sl = this.statusline;
|
|
371
|
-
sections.push({
|
|
372
|
-
title: "Status line",
|
|
373
|
-
rows: [
|
|
374
|
-
{ kind: "heading", label: "Status line" },
|
|
375
|
-
{ kind: "toggle", label: "Symbol name", get: () => sl.partVisible("name"), set: (v) => sl.setPartVisible("name", v) },
|
|
376
|
-
{ kind: "toggle", label: "Market status", get: () => sl.partVisible("market"), set: (v) => sl.setPartVisible("market", v) },
|
|
377
|
-
{ kind: "toggle", label: "OHLC values", get: () => sl.partVisible("ohlc"), set: (v) => sl.setPartVisible("ohlc", v) },
|
|
378
|
-
{ kind: "toggle", label: "Bar change values", get: () => sl.partVisible("change"), set: (v) => sl.setPartVisible("change", v) },
|
|
379
|
-
{ kind: "heading", label: "Indicators" },
|
|
380
|
-
{
|
|
381
|
-
kind: "toggle",
|
|
382
|
-
label: "Titles",
|
|
383
|
-
get: () => this.indicatorTitlesOn,
|
|
384
|
-
set: (v) => this.setIndicatorTitlesVisible(v)
|
|
385
|
-
},
|
|
386
|
-
{
|
|
387
|
-
kind: "toggle",
|
|
388
|
-
label: "Values",
|
|
389
|
-
get: () => this.indicatorValuesOn,
|
|
390
|
-
set: (v) => this.setIndicatorValuesVisible(v)
|
|
391
|
-
}
|
|
392
|
-
]
|
|
393
|
-
});
|
|
394
|
-
}
|
|
395
|
-
sections.push(advanced);
|
|
396
|
-
if (this.sessionAvailableFlag) sections.push(sessionSection);
|
|
397
|
-
sections.push(watermarkSection);
|
|
398
|
-
chart.renderer.setSettingsSections(sections);
|
|
399
|
-
}
|
|
400
|
-
/** Show/hide this cell's symbol watermark (persisted per cell). */
|
|
401
|
-
setWatermarkVisible(visible) {
|
|
402
|
-
this.watermarkOn = visible;
|
|
403
|
-
this.watermark?.setVisible(visible);
|
|
404
|
-
this.deps.onStateDirty();
|
|
405
|
-
}
|
|
406
|
-
/** Show/hide this cell's indicator titles — the in-chart legend rows (persisted per cell). */
|
|
407
|
-
setIndicatorTitlesVisible(visible) {
|
|
408
|
-
this.indicatorTitlesOn = visible;
|
|
409
|
-
this.inner?.renderer.set("indicatorTitles", visible);
|
|
410
|
-
this.deps.onStateDirty();
|
|
411
|
-
}
|
|
412
|
-
/** Show/hide the plot values beside this cell's legend titles (persisted per cell). */
|
|
413
|
-
setIndicatorValuesVisible(visible) {
|
|
414
|
-
this.indicatorValuesOn = visible;
|
|
415
|
-
this.inner?.renderer.set("indicatorValues", visible);
|
|
416
|
-
this.deps.onStateDirty();
|
|
417
|
-
}
|
|
418
|
-
/** The LIVE chart of this cell — never cache it across a layout change (the cell's
|
|
419
|
-
* identity is what endures; the chart dies with the cell). */
|
|
420
|
-
get chart() {
|
|
421
|
-
if (!this.inner) throw new Error(`[vela] cell "${this.id}" is destroyed`);
|
|
422
|
-
return this.inner;
|
|
423
|
-
}
|
|
424
|
-
get symbol() {
|
|
425
|
-
return this.state.symbol ?? "";
|
|
426
|
-
}
|
|
427
|
-
get timeframe() {
|
|
428
|
-
return this.state.timeframe ?? "60";
|
|
429
|
-
}
|
|
430
|
-
get priceStyle() {
|
|
431
|
-
const live = this.inner?.renderer.get("priceStyle");
|
|
432
|
-
return typeof live === "string" ? live : this.state.priceStyle ?? "candles";
|
|
433
|
-
}
|
|
434
|
-
/** Manifest instances + present natives — the topbar indicator count. */
|
|
435
|
-
get indicatorCount() {
|
|
436
|
-
return this.instances.length + this.nativeCatalog.filter((n) => n.present).length;
|
|
437
|
-
}
|
|
438
|
-
/** Switch this cell's market in place (the chart instance survives). */
|
|
439
|
-
setSymbol(symbol) {
|
|
440
|
-
if (!this.inner || symbol === this.symbol) return;
|
|
441
|
-
void this.inner.setMarket({ symbol });
|
|
442
|
-
}
|
|
443
|
-
setTimeframe(timeframe) {
|
|
444
|
-
if (!this.inner || timeframe === this.timeframe) return;
|
|
445
|
-
this.activeRangeId = null;
|
|
446
|
-
this.rangeBars = 0;
|
|
447
|
-
void this.inner.setMarket({ timeframe, bars: this.state.bars });
|
|
448
|
-
}
|
|
449
|
-
/** Applied live (renderer feature) — no reload. */
|
|
450
|
-
setPriceStyle(style) {
|
|
451
|
-
this.state.priceStyle = style;
|
|
452
|
-
this.inner?.renderer.set("priceStyle", style);
|
|
453
|
-
this.syncStatuslineColors();
|
|
454
|
-
this.deps.onPriceStyleChanged(this.id);
|
|
455
|
-
}
|
|
456
|
-
/** OHLC/change ink in the status line follows the ACTIVE price style's configured
|
|
457
|
-
* colors and direction rule (candle bodies by close-vs-open, baseline by position
|
|
458
|
-
* against the live baseline price, …) instead of the fixed theme tokens. */
|
|
459
|
-
syncStatuslineColors() {
|
|
460
|
-
if (!this.statusline || !this.inner) return;
|
|
461
|
-
this.statusline.setDirectionColors(...statuslineInkOf(this.inner.renderer, this.priceStyle));
|
|
462
|
-
}
|
|
463
|
-
/** Multi-cell grids keep the status line on one row and hide what doesn't fit —
|
|
464
|
-
* the workspace flips this with the layout (see Statusline.setFitMode). */
|
|
465
|
-
setStatuslineFit(on) {
|
|
466
|
-
this.statusline?.setFitMode(on);
|
|
467
|
-
}
|
|
468
|
-
/** The workspace shell keeps the app theme; the cell host's tokens re-derive from
|
|
469
|
-
* the LIVE plot surface (see {@link applyPlotOverlayTokens}). */
|
|
470
|
-
syncPlotOverlayTokens() {
|
|
471
|
-
applyPlotOverlayTokens(this.host, this.appTheme, this.inner?.renderer.getConfig() ?? null);
|
|
472
|
-
}
|
|
473
|
-
/**
|
|
474
|
-
* Frame a bottombar range chip: switch to its timeframe, fetch the depth its window
|
|
475
|
-
* needs, and keep it framed (re-asserted once the deeper history is painted).
|
|
476
|
-
*/
|
|
477
|
-
applyRange(preset) {
|
|
478
|
-
if (!this.inner || this.destroyed) return;
|
|
479
|
-
this.activeRangeId = preset.id;
|
|
480
|
-
const tfChanged = preset.tf !== this.timeframe;
|
|
481
|
-
const deeper = preset.bars > Math.max(this.state.bars ?? 500, this.rangeBars);
|
|
482
|
-
this.rangeBars = preset.bars;
|
|
483
|
-
if (tfChanged || deeper) {
|
|
484
|
-
this.pendingRange = preset;
|
|
485
|
-
void this.inner.setMarket({ timeframe: preset.tf, bars: Math.max(this.state.bars ?? 500, this.rangeBars), visibleRange: preset.preset }).then(() => {
|
|
486
|
-
if (!this.destroyed && this.pendingRange === preset) {
|
|
487
|
-
this.inner?.setVisibleRangePreset(preset.preset);
|
|
488
|
-
this.pendingRange = null;
|
|
489
|
-
}
|
|
490
|
-
});
|
|
491
|
-
} else {
|
|
492
|
-
this.inner.setVisibleRangePreset(preset.preset);
|
|
493
|
-
}
|
|
494
|
-
}
|
|
495
|
-
/** Make this cell the active one and put keyboard focus on its chart surface. */
|
|
496
|
-
focus() {
|
|
497
|
-
this.deps.activate(this.id);
|
|
498
|
-
this.inner?.renderer.focus();
|
|
499
|
-
}
|
|
500
|
-
/** Download this cell's chart as a PNG (named after its market). */
|
|
501
|
-
downloadScreenshot() {
|
|
502
|
-
const url = this.inner?.renderer.screenshot();
|
|
503
|
-
if (!url) return;
|
|
504
|
-
const a = this.host.ownerDocument.createElement("a");
|
|
505
|
-
a.href = url;
|
|
506
|
-
a.download = `${this.symbol || "chart"}-${this.timeframe}.png`;
|
|
507
|
-
a.click();
|
|
508
|
-
}
|
|
509
|
-
// ── indicator ledger (shared manifest, per-cell instances) ──
|
|
510
|
-
/**
|
|
511
|
-
* Hand the cell the workspace's resolved manifest. A RESTORED ledger (pool or
|
|
512
|
-
* persisted state) re-adds its recorded entries by name — held until the manifest
|
|
513
|
-
* actually carries them. Otherwise `seedEnabled` auto-adds the manifest's `enabled`
|
|
514
|
-
* entries (fresh cells only).
|
|
515
|
-
*/
|
|
516
|
-
setManifest(list, seedEnabled) {
|
|
517
|
-
this.manifest = list;
|
|
518
|
-
if (this.pendingManifestNames) {
|
|
519
|
-
if (list.length === 0) return;
|
|
520
|
-
for (const name of this.pendingManifestNames) {
|
|
521
|
-
const entry = list.find((e) => e.name === name);
|
|
522
|
-
if (entry) this.addManifestInstance(entry, { record: false });
|
|
523
|
-
}
|
|
524
|
-
this.pendingManifestNames = null;
|
|
525
|
-
return;
|
|
526
|
-
}
|
|
527
|
-
if (seedEnabled) {
|
|
528
|
-
for (const entry of list) if (entry.enabled) this.addManifestInstance(entry, { record: false });
|
|
529
|
-
}
|
|
530
|
-
}
|
|
531
|
-
/** The picker's library rows: supported natives first, then the manifest. */
|
|
532
|
-
libraryRows() {
|
|
533
|
-
return [
|
|
534
|
-
...this.nativeCatalog.filter((n) => n.supported).map((n) => ({ name: n.title, category: "Vela", native: true, nativeType: n.type, beta: n.beta })),
|
|
535
|
-
...this.manifest.map((e) => ({ name: e.name, language: e.language, category: e.category }))
|
|
536
|
-
];
|
|
537
|
-
}
|
|
538
|
-
/** The picker's on-chart rows: present natives first, then live instances. */
|
|
539
|
-
onChartRows() {
|
|
540
|
-
return [
|
|
541
|
-
...this.nativeCatalog.filter((n) => n.present).map((n) => ({ name: n.title, native: true, nativeType: n.type })),
|
|
542
|
-
...this.instances.map((it) => ({ name: it.entry.name, language: it.entry.language }))
|
|
543
|
-
];
|
|
544
|
-
}
|
|
545
|
-
/** Add by picker LIBRARY index (natives precede the manifest — mirrors libraryRows). */
|
|
546
|
-
addFromLibrary(index) {
|
|
547
|
-
const natives = this.nativeCatalog.filter((n) => n.supported);
|
|
548
|
-
if (index < natives.length) this.addNative(natives[index].type);
|
|
549
|
-
else {
|
|
550
|
-
const entry = this.manifest[index - natives.length];
|
|
551
|
-
if (entry) this.addManifestInstance(entry);
|
|
552
|
-
}
|
|
553
|
-
}
|
|
554
|
-
/** Remove by picker ON-CHART index (present natives precede instances). */
|
|
555
|
-
removeFromChart(index) {
|
|
556
|
-
const present = this.nativeCatalog.filter((n) => n.present);
|
|
557
|
-
if (index < present.length) this.removeNative(present[index].type);
|
|
558
|
-
else this.removeInstance(index - present.length);
|
|
559
|
-
}
|
|
560
|
-
/** Add ONE instance of a manifest entry (repeatable — duplicates are legitimate). */
|
|
561
|
-
addManifestInstance(entry, opts = {}) {
|
|
562
|
-
if (this.destroyed) return;
|
|
563
|
-
const it = { entry, handle: this.addToChart(entry) };
|
|
564
|
-
this.instances.push(it);
|
|
565
|
-
this.deps.onIndicatorsChanged(this.id);
|
|
566
|
-
if (opts.record === false) return;
|
|
567
|
-
const snapshot = it;
|
|
568
|
-
this.history.push({
|
|
569
|
-
undo: () => this.dropInstance(snapshot),
|
|
570
|
-
redo: () => {
|
|
571
|
-
snapshot.handle = this.addToChart(snapshot.entry);
|
|
572
|
-
this.instances.push(snapshot);
|
|
573
|
-
this.deps.onIndicatorsChanged(this.id);
|
|
574
|
-
}
|
|
575
|
-
});
|
|
576
|
-
}
|
|
577
|
-
removeInstance(index) {
|
|
578
|
-
const it = this.instances[index];
|
|
579
|
-
if (!it || this.destroyed) return;
|
|
580
|
-
this.dropInstance(it);
|
|
581
|
-
const snapshot = it;
|
|
582
|
-
this.history.push({
|
|
583
|
-
undo: () => {
|
|
584
|
-
snapshot.handle = this.addToChart(snapshot.entry);
|
|
585
|
-
this.instances.push(snapshot);
|
|
586
|
-
this.deps.onIndicatorsChanged(this.id);
|
|
587
|
-
},
|
|
588
|
-
redo: () => this.dropInstance(snapshot)
|
|
589
|
-
});
|
|
590
|
-
}
|
|
591
|
-
dropInstance(it) {
|
|
592
|
-
const idx = this.instances.indexOf(it);
|
|
593
|
-
if (idx >= 0) this.instances.splice(idx, 1);
|
|
594
|
-
try {
|
|
595
|
-
it.handle?.remove();
|
|
596
|
-
} catch {
|
|
597
|
-
}
|
|
598
|
-
it.handle = null;
|
|
599
|
-
this.deps.onIndicatorsChanged(this.id);
|
|
600
|
-
}
|
|
601
|
-
/** Add a native indicator (single-instance per type — the core dedupes). */
|
|
602
|
-
addNative(type) {
|
|
603
|
-
this.inner?.addNativeIndicator(type);
|
|
604
|
-
this.syncPresentNatives();
|
|
605
|
-
this.refreshNativeCatalog();
|
|
606
|
-
this.history.push({
|
|
607
|
-
undo: () => {
|
|
608
|
-
this.inner?.addNativeIndicator(type).remove();
|
|
609
|
-
this.refreshNativeCatalog();
|
|
610
|
-
},
|
|
611
|
-
redo: () => {
|
|
612
|
-
this.inner?.addNativeIndicator(type);
|
|
613
|
-
this.refreshNativeCatalog();
|
|
614
|
-
}
|
|
615
|
-
});
|
|
616
|
-
}
|
|
617
|
-
removeNative(type) {
|
|
618
|
-
this.inner?.addNativeIndicator(type).remove();
|
|
619
|
-
this.refreshNativeCatalog();
|
|
620
|
-
}
|
|
621
|
-
/** Sync mirror of the chart's present native types — the removal handler diffs
|
|
622
|
-
* against it to identify (and record) whichever type was just removed. */
|
|
623
|
-
syncPresentNatives() {
|
|
624
|
-
this.presentNatives = this.inner?.presentNativeIndicators() ?? [];
|
|
625
|
-
}
|
|
626
|
-
/** Refresh the native catalog (supported/present flags) for this cell's market. */
|
|
627
|
-
refreshNativeCatalog() {
|
|
628
|
-
const chart = this.inner;
|
|
629
|
-
if (!chart) return;
|
|
630
|
-
void chart.availableNativeIndicators().then((list) => {
|
|
631
|
-
if (this.destroyed || this.inner !== chart) return;
|
|
632
|
-
this.nativeCatalog = list.map((n) => ({ type: n.type, title: n.title, supported: n.supported, present: n.present, beta: n.beta }));
|
|
633
|
-
this.deps.onIndicatorsChanged(this.id);
|
|
634
|
-
});
|
|
635
|
-
}
|
|
636
|
-
addToChart(entry) {
|
|
637
|
-
try {
|
|
638
|
-
return this.inner?.addIndicator(entry.script, entry.language !== void 0 ? { language: entry.language } : void 0) ?? null;
|
|
639
|
-
} catch (err) {
|
|
640
|
-
console.warn(`[vela] indicator "${entry.name}" failed to add:`, err);
|
|
641
|
-
return null;
|
|
642
|
-
}
|
|
643
|
-
}
|
|
644
|
-
// ── lifecycle ──
|
|
645
|
-
/** Snapshot everything the pool needs to restore this slot later. The market fields
|
|
646
|
-
* come from the LIVE config (`chart.market`) — the requested identity — so a switch
|
|
647
|
-
* still loading when the snapshot is taken (persist-on-close) is not lost. */
|
|
648
|
-
dehydrate() {
|
|
649
|
-
const live = this.inner?.market;
|
|
650
|
-
return {
|
|
651
|
-
...this.state,
|
|
652
|
-
...live ? { symbol: live.symbol, provider: live.provider, timeframe: live.timeframe } : {},
|
|
653
|
-
priceStyle: this.priceStyle,
|
|
654
|
-
watermark: this.watermarkOn,
|
|
655
|
-
indicatorTitles: this.indicatorTitlesOn,
|
|
656
|
-
indicatorValues: this.indicatorValuesOn,
|
|
657
|
-
rendererConfig: this.inner?.renderer.getConfig() ?? void 0,
|
|
658
|
-
drawings: this.inner ? this.inner.drawings.toJSON() : void 0,
|
|
659
|
-
// Natives from the chart's SYNC registry read — an async catalog mirror here
|
|
660
|
-
// lost unload-time saves, and the old empty-set fallbacks resurrected removed
|
|
661
|
-
// indicators. Manifest names fall back to the restored ledger only until the
|
|
662
|
-
// shared manifest settles. See {@link indicatorLedger}.
|
|
663
|
-
indicators: indicatorLedger({
|
|
664
|
-
present: this.inner ? this.inner.presentNativeIndicators() : [],
|
|
665
|
-
instanceNames: this.instances.map((it) => it.entry.name),
|
|
666
|
-
pendingManifest: this.pendingManifestNames,
|
|
667
|
-
manifestSettled: this.deps.manifestSettled(),
|
|
668
|
-
volumePending: this.volumeMayBePending && this.volumeIntent
|
|
669
|
-
})
|
|
670
|
-
};
|
|
671
|
-
}
|
|
672
|
-
destroy() {
|
|
673
|
-
this.destroyed = true;
|
|
674
|
-
this.offMarket();
|
|
675
|
-
this.contextMenu.destroy();
|
|
676
|
-
this.history.destroy();
|
|
677
|
-
this.marketStatus?.stop();
|
|
678
|
-
this.sessionShading.stop();
|
|
679
|
-
this.statusline?.destroy();
|
|
680
|
-
this.watermark?.destroy();
|
|
681
|
-
this.inner?.destroy();
|
|
682
|
-
this.inner = null;
|
|
683
|
-
this.host.remove();
|
|
684
|
-
}
|
|
685
|
-
};
|
|
686
|
-
|
|
687
|
-
// src/workspace/context.ts
|
|
688
|
-
function buildContext(host) {
|
|
689
|
-
const active = host.active();
|
|
690
|
-
return {
|
|
691
|
-
get chart() {
|
|
692
|
-
const cell = host.active();
|
|
693
|
-
if (!cell) throw new Error("VelaWorkspace has no active cell yet");
|
|
694
|
-
return cell.chart;
|
|
695
|
-
},
|
|
696
|
-
symbol: active?.symbol ?? "",
|
|
697
|
-
timeframe: active?.timeframe ?? "60",
|
|
698
|
-
priceStyle: active?.priceStyle ?? "candles",
|
|
699
|
-
setSymbol: (symbol) => host.active()?.setSymbol(symbol),
|
|
700
|
-
setTimeframe: (tf) => host.active()?.setTimeframe(tf),
|
|
701
|
-
setPriceStyle: (style) => host.active()?.setPriceStyle(style),
|
|
702
|
-
openSymbolSearch: (query) => host.openSymbolSearch(query),
|
|
703
|
-
togglePanel: (id, open) => host.togglePanel(id, open),
|
|
704
|
-
host: host.root,
|
|
705
|
-
toast: (message, kind) => host.toast(message, kind),
|
|
706
|
-
cells: host.cells().map((c) => ({ id: c.id, chart: c.chart, symbol: c.symbol, timeframe: c.timeframe })),
|
|
707
|
-
activeCellId: active?.id ?? "",
|
|
708
|
-
setActiveCell: (id) => host.setActiveCell(id)
|
|
709
|
-
};
|
|
710
|
-
}
|
|
711
|
-
|
|
712
|
-
// src/workspace/layouts.ts
|
|
713
|
-
var registry = /* @__PURE__ */ new Map();
|
|
714
|
-
function registerLayout(def) {
|
|
715
|
-
registry.set(def.id, def);
|
|
716
|
-
}
|
|
717
|
-
function unregisterLayout(id) {
|
|
718
|
-
registry.delete(id);
|
|
719
|
-
}
|
|
720
|
-
function layoutDefinition(id) {
|
|
721
|
-
return registry.get(id);
|
|
722
|
-
}
|
|
723
|
-
function layouts() {
|
|
724
|
-
return [...registry.values()];
|
|
725
|
-
}
|
|
726
|
-
function slots(n) {
|
|
727
|
-
return Array.from({ length: n }, (_, i) => ({ id: `c${i + 1}` }));
|
|
728
|
-
}
|
|
729
|
-
function registerBuiltinLayouts() {
|
|
730
|
-
registerLayout({ id: "1", label: "Single", cols: [1], rows: [1], cells: slots(1) });
|
|
731
|
-
registerLayout({ id: "2h", label: "2 side by side", cols: [1, 1], rows: [1], cells: slots(2) });
|
|
732
|
-
registerLayout({ id: "2v", label: "2 stacked", cols: [1], rows: [1, 1], cells: slots(2) });
|
|
733
|
-
registerLayout({ id: "4", label: "4 grid", cols: [1, 1], rows: [1, 1], cells: slots(4) });
|
|
734
|
-
registerLayout({ id: "8", label: "8 grid", cols: [1, 1, 1, 1], rows: [1, 1], cells: slots(8) });
|
|
735
|
-
}
|
|
736
|
-
var GRID_PICKER_MAX = 4;
|
|
737
|
-
var GRID_BUILTIN_IDS = {
|
|
738
|
-
"1x1": "1",
|
|
739
|
-
"1x2": "2h",
|
|
740
|
-
"2x1": "2v",
|
|
741
|
-
"2x2": "4",
|
|
742
|
-
"2x4": "8"
|
|
743
|
-
};
|
|
744
|
-
var clampTrack = (n) => Math.max(1, Math.min(GRID_PICKER_MAX, Math.round(n)));
|
|
745
|
-
function layoutForGrid(rows, cols) {
|
|
746
|
-
const r = clampTrack(rows);
|
|
747
|
-
const c = clampTrack(cols);
|
|
748
|
-
const builtin = GRID_BUILTIN_IDS[`${r}x${c}`];
|
|
749
|
-
const registered = builtin ? registry.get(builtin) : void 0;
|
|
750
|
-
if (registered) return registered;
|
|
751
|
-
return {
|
|
752
|
-
id: `g${r}x${c}`,
|
|
753
|
-
// Width-first label — matches the picker's caption ("3 × 2" = 3 wide, 2 tall).
|
|
754
|
-
label: `${c} \xD7 ${r} grid`,
|
|
755
|
-
cols: Array.from({ length: c }, () => 1),
|
|
756
|
-
rows: Array.from({ length: r }, () => 1),
|
|
757
|
-
cells: slots(r * c)
|
|
758
|
-
};
|
|
759
|
-
}
|
|
760
|
-
var GRID_ID_RE = /^g([1-4])x([1-4])$/;
|
|
761
|
-
function ensureLayout(id) {
|
|
762
|
-
const registered = registry.get(id);
|
|
763
|
-
if (registered) return registered;
|
|
764
|
-
const g = GRID_ID_RE.exec(id);
|
|
765
|
-
if (g) return layoutForGrid(Number(g[1]), Number(g[2]));
|
|
766
|
-
return void 0;
|
|
767
|
-
}
|
|
768
|
-
function layoutShape(def) {
|
|
769
|
-
if (!def.areas && def.rows.length <= GRID_PICKER_MAX && def.cols.length <= GRID_PICKER_MAX && def.cells.length === def.rows.length * def.cols.length) {
|
|
770
|
-
return { rows: def.rows.length, cols: def.cols.length };
|
|
771
|
-
}
|
|
772
|
-
return null;
|
|
773
|
-
}
|
|
774
|
-
function occupancyGrid(def) {
|
|
775
|
-
if (def.areas) return def.areas.map((row) => row.trim().split(/\s+/));
|
|
776
|
-
const cols = def.cols.length;
|
|
777
|
-
return def.rows.map((_, r) => def.cols.map((_2, c) => def.cells[r * cols + c]?.id ?? `\xB7${r}x${c}`));
|
|
778
|
-
}
|
|
779
|
-
function gridStyles(def, trackSizes) {
|
|
780
|
-
const cols = trackSizes?.cols?.length === def.cols.length ? trackSizes.cols : def.cols;
|
|
781
|
-
const rows = trackSizes?.rows?.length === def.rows.length ? trackSizes.rows : def.rows;
|
|
782
|
-
const container = {
|
|
783
|
-
display: "grid",
|
|
784
|
-
gridTemplateColumns: cols.map((w) => `${w}fr`).join(" "),
|
|
785
|
-
gridTemplateRows: rows.map((w) => `${w}fr`).join(" ")
|
|
786
|
-
};
|
|
787
|
-
if (def.areas) container.gridTemplateAreas = def.areas.map((r) => `"${r}"`).join(" ");
|
|
788
|
-
const perCell = {};
|
|
789
|
-
for (const cell of def.cells) {
|
|
790
|
-
perCell[cell.id] = cell.area ? { gridArea: cell.area } : {};
|
|
791
|
-
}
|
|
792
|
-
return { container, perCell };
|
|
793
|
-
}
|
|
794
|
-
function activeAfterLayout(current, cellIds) {
|
|
795
|
-
if (current != null && cellIds.includes(current)) return current;
|
|
796
|
-
return cellIds[0] ?? null;
|
|
797
|
-
}
|
|
798
|
-
function orderAfterLayout(order, slots2, active) {
|
|
799
|
-
const next = [...order];
|
|
800
|
-
if (active == null || slots2 <= 0) return next;
|
|
801
|
-
const idx = next.indexOf(active);
|
|
802
|
-
if (idx < 0 || idx < slots2) return next;
|
|
803
|
-
next.splice(idx, 1);
|
|
804
|
-
next.splice(slots2 - 1, 0, active);
|
|
805
|
-
return next;
|
|
806
|
-
}
|
|
807
|
-
|
|
808
|
-
// src/workspace/splitters.ts
|
|
809
|
-
var MIN_TRACK_FRAC = 0.1;
|
|
810
|
-
function evenTracks(n) {
|
|
811
|
-
return Array.from({ length: n }, () => 1);
|
|
812
|
-
}
|
|
813
|
-
function resizeTracks(weights, index, deltaPx, sizePx) {
|
|
814
|
-
const out = [...weights];
|
|
815
|
-
const a = out[index];
|
|
816
|
-
const b = out[index + 1];
|
|
817
|
-
if (a === void 0 || b === void 0 || sizePx <= 0) return out;
|
|
818
|
-
const total = out.reduce((s, w) => s + w, 0);
|
|
819
|
-
const min = total * MIN_TRACK_FRAC;
|
|
820
|
-
let deltaFr = deltaPx / sizePx * total;
|
|
821
|
-
deltaFr = Math.max(deltaFr, min - a);
|
|
822
|
-
deltaFr = Math.min(deltaFr, b - min);
|
|
823
|
-
out[index] = a + deltaFr;
|
|
824
|
-
out[index + 1] = b - deltaFr;
|
|
825
|
-
return out;
|
|
826
|
-
}
|
|
827
|
-
function trackOffsets(weights, sizePx, gapPx) {
|
|
828
|
-
const total = weights.reduce((s, w) => s + w, 0);
|
|
829
|
-
if (total <= 0) return [];
|
|
830
|
-
const content = sizePx - gapPx * (weights.length - 1);
|
|
831
|
-
const out = [];
|
|
832
|
-
let acc = 0;
|
|
833
|
-
for (let i = 0; i < weights.length - 1; i += 1) {
|
|
834
|
-
acc += weights[i] / total * content;
|
|
835
|
-
out.push(acc + gapPx * i + gapPx / 2);
|
|
836
|
-
}
|
|
837
|
-
return out;
|
|
838
|
-
}
|
|
839
|
-
function seamSegments(grid, axis, index) {
|
|
840
|
-
const count = axis === "cols" ? grid.length : grid[0]?.length ?? 0;
|
|
841
|
-
const isSeam = (k) => axis === "cols" ? grid[k]?.[index] !== grid[k]?.[index + 1] : grid[index]?.[k] !== grid[index + 1]?.[k];
|
|
842
|
-
const out = [];
|
|
843
|
-
for (let k = 0; k < count; k += 1) {
|
|
844
|
-
if (!isSeam(k)) continue;
|
|
845
|
-
const last = out[out.length - 1];
|
|
846
|
-
if (last && last[1] === k - 1) last[1] = k;
|
|
847
|
-
else out.push([k, k]);
|
|
848
|
-
}
|
|
849
|
-
return out;
|
|
850
|
-
}
|
|
851
|
-
function segmentSpanPx(weights, sizePx, gapPx, from, to) {
|
|
852
|
-
const total = weights.reduce((s, w) => s + w, 0);
|
|
853
|
-
if (total <= 0) return { start: 0, end: 0 };
|
|
854
|
-
const content = sizePx - gapPx * (weights.length - 1);
|
|
855
|
-
const startOf = (k) => weights.slice(0, k).reduce((s, w) => s + w / total * content, 0) + gapPx * k;
|
|
856
|
-
const start = from === 0 ? 0 : startOf(from) - gapPx / 2;
|
|
857
|
-
const end = to === weights.length - 1 ? sizePx : startOf(to + 1) - gapPx / 2;
|
|
858
|
-
return { start, end };
|
|
859
|
-
}
|
|
860
|
-
var HIT_PX = 8;
|
|
861
|
-
var SplitterLayer = class {
|
|
862
|
-
constructor(container, deps) {
|
|
863
|
-
this.container = container;
|
|
864
|
-
this.deps = deps;
|
|
865
|
-
this.strips = [];
|
|
866
|
-
this.drag = null;
|
|
867
|
-
}
|
|
868
|
-
/** Rebuild + reposition the strips for the current tracks/size/occupancy. */
|
|
869
|
-
layout() {
|
|
870
|
-
const { cols, rows } = this.deps.tracks();
|
|
871
|
-
const grid = this.deps.grid();
|
|
872
|
-
const rect = this.container.getBoundingClientRect();
|
|
873
|
-
const gap = this.deps.gapPx();
|
|
874
|
-
const placements = [];
|
|
875
|
-
for (const [i, x] of trackOffsets(cols, rect.width, gap).entries()) {
|
|
876
|
-
for (const [from, to] of seamSegments(grid, "cols", i)) {
|
|
877
|
-
const { start, end } = segmentSpanPx(rows, rect.height, gap, from, to);
|
|
878
|
-
placements.push({
|
|
879
|
-
axis: "cols",
|
|
880
|
-
index: i,
|
|
881
|
-
css: `left:${Math.round(x - HIT_PX / 2)}px;top:${Math.round(start)}px;width:${HIT_PX}px;height:${Math.round(end - start)}px;cursor:col-resize;`
|
|
882
|
-
});
|
|
883
|
-
}
|
|
884
|
-
}
|
|
885
|
-
for (const [i, y] of trackOffsets(rows, rect.height, gap).entries()) {
|
|
886
|
-
for (const [from, to] of seamSegments(grid, "rows", i)) {
|
|
887
|
-
const { start, end } = segmentSpanPx(cols, rect.width, gap, from, to);
|
|
888
|
-
placements.push({
|
|
889
|
-
axis: "rows",
|
|
890
|
-
index: i,
|
|
891
|
-
css: `left:${Math.round(start)}px;top:${Math.round(y - HIT_PX / 2)}px;width:${Math.round(end - start)}px;height:${HIT_PX}px;cursor:row-resize;`
|
|
892
|
-
});
|
|
893
|
-
}
|
|
894
|
-
}
|
|
895
|
-
while (this.strips.length > placements.length) this.strips.pop().remove();
|
|
896
|
-
while (this.strips.length < placements.length) this.strips.push(this.makeStrip());
|
|
897
|
-
for (const [k, p] of placements.entries()) this.place(this.strips[k], p.axis, p.index, p.css);
|
|
898
|
-
}
|
|
899
|
-
destroy() {
|
|
900
|
-
for (const s of this.strips.splice(0)) s.remove();
|
|
901
|
-
this.drag = null;
|
|
902
|
-
}
|
|
903
|
-
makeStrip() {
|
|
904
|
-
const el = document.createElement("div");
|
|
905
|
-
el.className = "vela-ws-splitter";
|
|
906
|
-
el.style.cssText = "position:absolute;z-index:30;";
|
|
907
|
-
el.addEventListener("pointerdown", (e) => this.onDown(el, e));
|
|
908
|
-
el.addEventListener("dblclick", () => {
|
|
909
|
-
const axis = el.dataset.axis;
|
|
910
|
-
if (axis) this.deps.reset(axis);
|
|
911
|
-
});
|
|
912
|
-
this.container.appendChild(el);
|
|
913
|
-
return el;
|
|
914
|
-
}
|
|
915
|
-
place(el, axis, index, css) {
|
|
916
|
-
el.dataset.axis = axis;
|
|
917
|
-
el.dataset.index = String(index);
|
|
918
|
-
el.style.cssText = `position:absolute;z-index:30;${css}`;
|
|
919
|
-
}
|
|
920
|
-
onDown(el, e) {
|
|
921
|
-
const axis = el.dataset.axis;
|
|
922
|
-
const index = Number(el.dataset.index);
|
|
923
|
-
if (!axis || Number.isNaN(index)) return;
|
|
924
|
-
e.preventDefault();
|
|
925
|
-
const rect = this.container.getBoundingClientRect();
|
|
926
|
-
const gap = this.deps.gapPx();
|
|
927
|
-
const tracks = this.deps.tracks()[axis];
|
|
928
|
-
this.drag = {
|
|
929
|
-
axis,
|
|
930
|
-
index,
|
|
931
|
-
startPx: axis === "cols" ? e.clientX : e.clientY,
|
|
932
|
-
startWeights: [...tracks],
|
|
933
|
-
sizePx: (axis === "cols" ? rect.width : rect.height) - gap * (tracks.length - 1)
|
|
934
|
-
};
|
|
935
|
-
try {
|
|
936
|
-
el.setPointerCapture(e.pointerId);
|
|
937
|
-
} catch {
|
|
938
|
-
}
|
|
939
|
-
const move = (ev) => {
|
|
940
|
-
const d = this.drag;
|
|
941
|
-
if (!d) return;
|
|
942
|
-
const delta = (d.axis === "cols" ? ev.clientX : ev.clientY) - d.startPx;
|
|
943
|
-
this.deps.apply(d.axis, resizeTracks(d.startWeights, d.index, delta, d.sizePx));
|
|
944
|
-
};
|
|
945
|
-
const up = () => {
|
|
946
|
-
this.drag = null;
|
|
947
|
-
el.removeEventListener("pointermove", move);
|
|
948
|
-
el.removeEventListener("pointerup", up);
|
|
949
|
-
el.removeEventListener("pointercancel", up);
|
|
950
|
-
};
|
|
951
|
-
el.addEventListener("pointermove", move);
|
|
952
|
-
el.addEventListener("pointerup", up);
|
|
953
|
-
el.addEventListener("pointercancel", up);
|
|
954
|
-
}
|
|
955
|
-
};
|
|
956
|
-
|
|
957
|
-
// src/workspace/VelaWorkspace.ts
|
|
958
|
-
var DEFAULT_TIMEFRAMES = ["1", "5", "15", "60", "240", "D", "W"];
|
|
959
|
-
var GAP_PX = 2;
|
|
960
|
-
var POOL_CAP = 16;
|
|
961
|
-
var TIME_AXIS_H = 22;
|
|
962
|
-
var ALERT_CAP = 50;
|
|
963
|
-
var STYLE_ID = "vela-workspace";
|
|
964
|
-
var CSS = `
|
|
965
|
-
.vela-workspace { position: relative; width: 100%; height: 100%; display: flex; flex-direction: column; background: var(--vela-bg); }
|
|
966
|
-
.vela-ws-main { position: relative; display: flex; flex-direction: row; flex: 1 1 auto; min-height: 0; }
|
|
967
|
-
.vela-ws-toolbar { position: relative; flex: none; }
|
|
968
|
-
.vela-ws-grid { position: relative; flex: 1 1 auto; min-width: 0; display: grid; gap: ${GAP_PX}px; background: var(--vela-border-soft); }
|
|
969
|
-
.vela-cell { background: var(--vela-bg); position: relative; }
|
|
970
|
-
/* Active-cell highlight: an overlay ring ABOVE the chart's own canvas stack (a plain
|
|
971
|
-
outline on the cell is painted under them) \u2014 inert to the pointer. Scoped to
|
|
972
|
-
multi-cell grids ([data-multi]): a single-cell layout always has an active cell,
|
|
973
|
-
and ringing the only chart would just be noise. */
|
|
974
|
-
.vela-ws-grid[data-multi='1'] .vela-cell[data-active='1']::after {
|
|
975
|
-
content: '';
|
|
976
|
-
position: absolute;
|
|
977
|
-
inset: 0;
|
|
978
|
-
border: 2px solid var(--vela-fg-bright);
|
|
979
|
-
pointer-events: none;
|
|
980
|
-
z-index: 10;
|
|
981
|
-
}
|
|
982
|
-
/* Splitter hover mirrors the in-chart pane separator hover (CrosshairRenderer):
|
|
983
|
-
a soft band over the whole grab target + a solid 2px line on the seam center. */
|
|
984
|
-
.vela-ws-splitter:hover { background: var(--vela-separator-hover-band); }
|
|
985
|
-
.vela-ws-splitter:hover::after { content: ''; position: absolute; background: var(--vela-separator-hover-line); }
|
|
986
|
-
.vela-ws-splitter[data-axis='cols']:hover::after { left: calc(50% - 1px); top: 0; width: 2px; height: 100%; }
|
|
987
|
-
.vela-ws-splitter[data-axis='rows']:hover::after { top: calc(50% - 1px); left: 0; height: 2px; width: 100%; }
|
|
988
|
-
/* Mobile: the docked drawing-toolbar column would eat a phone-width grid \u2014 the shell's
|
|
989
|
-
drawings drawer + on-chart pill replace it (same policy as the widget's in-chart bar). */
|
|
990
|
-
[data-layout='mobile'] .vela-ws-toolbar { display: none; }
|
|
991
|
-
`;
|
|
992
|
-
registerIcon("layout", svg16('<rect x="1.5" y="1.5" width="13" height="13" rx="1.5"/><path d="M8 1.5v13M1.5 8h13"/>'));
|
|
993
|
-
function declaredOrder(cells) {
|
|
994
|
-
const names = Object.keys(cells ?? {});
|
|
995
|
-
for (const n of names) {
|
|
996
|
-
if (/^\d+$/.test(n)) {
|
|
997
|
-
console.warn(`[vela] workspace cell "${n}" ignored \u2014 a purely-numeric name cannot keep its declaration order (JS object key semantics); use e.g. "cell${n}"`);
|
|
998
|
-
}
|
|
999
|
-
}
|
|
1000
|
-
return names.filter((n) => !/^\d+$/.test(n));
|
|
1001
|
-
}
|
|
1002
|
-
function nextAutoCellId(taken) {
|
|
1003
|
-
for (let i = 1; ; i += 1) {
|
|
1004
|
-
if (!taken.has(`c${i}`)) return `c${i}`;
|
|
1005
|
-
}
|
|
1006
|
-
}
|
|
1007
|
-
var VelaWorkspace = class {
|
|
1008
|
-
constructor(container, opts = {}) {
|
|
1009
|
-
this.events = new TypedEventBus();
|
|
1010
|
-
this.feed = new MultiProviderFeed();
|
|
1011
|
-
this.cellsById = /* @__PURE__ */ new Map();
|
|
1012
|
-
this.pool = /* @__PURE__ */ new Map();
|
|
1013
|
-
this.trackSizes = /* @__PURE__ */ new Map();
|
|
1014
|
-
this.resizeObserver = null;
|
|
1015
|
-
/** Cell identities by SLOT POSITION — `order[i]` lives in the layout's i-th slot.
|
|
1016
|
-
* Names come from the `cells` declaration order (then the persisted document);
|
|
1017
|
-
* slots beyond the list get auto identities. Grows, never reorders. */
|
|
1018
|
-
this.order = [];
|
|
1019
|
-
this.activeId = null;
|
|
1020
|
-
this.cellBackend = "auto";
|
|
1021
|
-
this.destroyed = false;
|
|
1022
|
-
this.shortcutsHelp = null;
|
|
1023
|
-
this.glider = new Glider(() => this.activeId ? this.cellsById.get(this.activeId)?.chart ?? null : null);
|
|
1024
|
-
/** The GLOBAL armed tool/magnet/stay (workspace policy) — re-applied to whichever cell
|
|
1025
|
-
* takes the focus; only the ACTIVE cell ever holds a non-null tool. Measure/eraser
|
|
1026
|
-
* stay transient and per-cell: they exit when the focus leaves. */
|
|
1027
|
-
this.globalTool = null;
|
|
1028
|
-
this.globalSnap = "off";
|
|
1029
|
-
this.globalStay = false;
|
|
1030
|
-
/** Live subscription to the ACTIVE cell's unified history (rebound on every projection). */
|
|
1031
|
-
this.historyUnsub = null;
|
|
1032
|
-
/** Favorite drawing tools — a WORKSPACE preference (one star set, every cell). */
|
|
1033
|
-
this.favs = [];
|
|
1034
|
-
/** Favorite timeframes — the shared topbar's quick-switch chips, one set for the grid. */
|
|
1035
|
-
this.tfFavs = [];
|
|
1036
|
-
/** Live sync configuration (mutable copy of the option). */
|
|
1037
|
-
this.syncOpts = {};
|
|
1038
|
-
this.stateTimer = null;
|
|
1039
|
-
this.onUnload = () => this.persistNow();
|
|
1040
|
-
/** Re-entrance guard around one propagation tick: followers' synchronous echoes
|
|
1041
|
-
* (their setVisibleRange re-emits viewport:changed) must not re-propagate. */
|
|
1042
|
-
this.syncBusy = false;
|
|
1043
|
-
/** Same guard for the drawings link: the propagated mutations' own `drawing:*`
|
|
1044
|
-
* events fire synchronously inside the propagation loop and must not fan out again. */
|
|
1045
|
-
this.drawingSyncBusy = false;
|
|
1046
|
-
/** LINKED drawings (the drawings sync): one map per synced set (cellId → that
|
|
1047
|
-
* cell's drawing id), reachable from every member under its `cellId\0drawingId`
|
|
1048
|
-
* key — any member finds its peers to push edits/removals onto. Survives a
|
|
1049
|
-
* toggle-off (propagation freezes while the setting is off; re-enabling resumes
|
|
1050
|
-
* edit/delete for these pairs). Cleared on reload / `applyState`. */
|
|
1051
|
-
this.drawingLinks = /* @__PURE__ */ new Map();
|
|
1052
|
-
this.manifest = [];
|
|
1053
|
-
/** The shared manifest can no longer change instance sets — resolved, or no
|
|
1054
|
-
* `indicators` option so nothing ever will. Gates the cells' ledger fallback. */
|
|
1055
|
-
this.manifestSettled = false;
|
|
1056
|
-
this.openDialogs = 0;
|
|
1057
|
-
this.alerts = [];
|
|
1058
|
-
this.alertsMenu = null;
|
|
1059
|
-
this.tfDrawer = null;
|
|
1060
|
-
this.drawingsDrawer = null;
|
|
1061
|
-
this.moreDrawer = null;
|
|
1062
|
-
this.timezoneDrawer = null;
|
|
1063
|
-
this.priceScaleDrawer = null;
|
|
1064
|
-
/** Plot-local y of the last price-axis long-press (targets the pane under the finger). */
|
|
1065
|
-
this.priceScalePressY = 0;
|
|
1066
|
-
this.attachmentDisposers = /* @__PURE__ */ new Map();
|
|
1067
|
-
/** The single grid-wide attribution mark — re-inked on a live theme swap. */
|
|
1068
|
-
this.attributionMark = null;
|
|
1069
|
-
this.onRootKeydown = (ev) => this.routeTyping(ev);
|
|
1070
|
-
/** The sync-link control surface: `set(kind, true | {cellId: group} | false)`,
|
|
1071
|
-
* `get(kind)`, `state()`. Enabling a market/viewport link aligns the followers to
|
|
1072
|
-
* the ACTIVE cell once, so the grid starts coherent; `crosshair` mirrors the
|
|
1073
|
-
* pointer time as ghost crosshairs on the followers (also a toggle in the layout
|
|
1074
|
-
* dropdown). */
|
|
1075
|
-
this.sync = {
|
|
1076
|
-
set: (kind, setting) => this.applySyncSetting(kind, setting === false ? void 0 : setting, true),
|
|
1077
|
-
get: (kind) => this.syncOpts[kind],
|
|
1078
|
-
state: () => ({ ...this.syncOpts })
|
|
1079
|
-
};
|
|
1080
|
-
registerBuiltinLayouts();
|
|
1081
|
-
const hostEl = typeof container === "string" ? document.querySelector(container) : container;
|
|
1082
|
-
if (!hostEl) throw new Error(`VelaWorkspace: container not found: ${String(container)}`);
|
|
1083
|
-
this.opts = opts;
|
|
1084
|
-
this.persistKey = opts.persist === void 0 || opts.persist === false ? null : opts.persist === true ? "vela-workspace" : opts.persist;
|
|
1085
|
-
this.storage = opts.storage ?? localStorageAdapter();
|
|
1086
|
-
let boot = null;
|
|
1087
|
-
if (this.persistKey !== null) {
|
|
1088
|
-
const raw = this.storage.get(this.persistKey);
|
|
1089
|
-
if (typeof raw === "string") boot = decodeState(raw);
|
|
1090
|
-
else if (raw != null && typeof raw === "object") {
|
|
1091
|
-
void raw.then((r) => {
|
|
1092
|
-
if (!this.destroyed && r) this.applyState(decodeState(r));
|
|
1093
|
-
});
|
|
1094
|
-
}
|
|
1095
|
-
if (typeof window !== "undefined") window.addEventListener("beforeunload", this.onUnload);
|
|
1096
|
-
}
|
|
1097
|
-
this.timezone = boot?.timezone ?? opts.timezone ?? "Etc/UTC";
|
|
1098
|
-
if (boot?.favorites) this.favs = [...boot.favorites];
|
|
1099
|
-
if (boot?.timeframeFavorites) this.tfFavs = [...boot.timeframeFavorites];
|
|
1100
|
-
const sync = boot?.sync ?? opts.sync;
|
|
1101
|
-
for (const kind of ["viewport", "symbol", "timeframe", "crosshair", "drawings"]) {
|
|
1102
|
-
this.applySyncSetting(kind, sync?.[kind]);
|
|
1103
|
-
}
|
|
1104
|
-
this.def = this.resolveLayout(boot?.layout && ensureLayout(boot.layout) ? boot.layout : opts.layout ?? "4");
|
|
1105
|
-
if (boot?.trackSizes) for (const [id, ts] of Object.entries(boot.trackSizes)) this.trackSizes.set(id, ts);
|
|
1106
|
-
if (boot?.charts) for (const { id, ...cs } of boot.charts) this.pool.set(id, cs);
|
|
1107
|
-
this.order = boot?.charts ? boot.charts.map((c) => c.id) : declaredOrder(opts.cells);
|
|
1108
|
-
const bootActive = boot?.activeCellId ?? null;
|
|
1109
|
-
const doc = hostEl.ownerDocument;
|
|
1110
|
-
injectStyles(STYLE_ID, CSS, doc);
|
|
1111
|
-
this.root = doc.createElement("div");
|
|
1112
|
-
this.root.className = "vela-workspace";
|
|
1113
|
-
ensureUIHost(this.root, resolveTheme(opts.theme));
|
|
1114
|
-
for (const [name, make] of Object.entries(opts.providers ?? {})) void this.feed.registerProvider(name, make());
|
|
1115
|
-
void this.feed.ready().then(() => {
|
|
1116
|
-
if (!this.destroyed) this.refreshRetention();
|
|
1117
|
-
});
|
|
1118
|
-
this.symbolPicker = new SymbolPicker({
|
|
1119
|
-
host: this.root,
|
|
1120
|
-
onSelect: (ticker) => this.active.setSymbol(ticker),
|
|
1121
|
-
onOpenChange: (open) => {
|
|
1122
|
-
if (open) for (const cell of this.cells()) cell.chart.renderer.closeDialogs();
|
|
1123
|
-
this.trackDialog(open);
|
|
1124
|
-
}
|
|
1125
|
-
});
|
|
1126
|
-
this.symbolPicker.setSource(() => this.feed.symbols());
|
|
1127
|
-
this.indicatorPicker = opts.indicatorPicker !== false ? new IndicatorPicker({
|
|
1128
|
-
host: this.root,
|
|
1129
|
-
library: () => this.active.libraryRows(),
|
|
1130
|
-
onChart: () => this.active.onChartRows(),
|
|
1131
|
-
onAdd: (i) => this.active.addFromLibrary(i),
|
|
1132
|
-
onRemove: (i) => this.active.removeFromChart(i),
|
|
1133
|
-
onOpenChange: (open) => {
|
|
1134
|
-
if (open) for (const cell of this.cells()) cell.chart.renderer.closeDialogs();
|
|
1135
|
-
this.trackDialog(open);
|
|
1136
|
-
}
|
|
1137
|
-
}) : null;
|
|
1138
|
-
const picker = this.indicatorPicker;
|
|
1139
|
-
this.tfQuick = new TimeframeQuick({
|
|
1140
|
-
host: this.root,
|
|
1141
|
-
onApply: (tf) => this.setActiveTimeframe(tf),
|
|
1142
|
-
onOpenChange: (open) => this.trackDialog(open)
|
|
1143
|
-
});
|
|
1144
|
-
this.topbar = new Topbar(this.root, {
|
|
1145
|
-
symbol: "",
|
|
1146
|
-
onSymbolClick: () => this.symbolPicker.open(),
|
|
1147
|
-
...picker ? { onIndicatorsClick: () => picker.open() } : {},
|
|
1148
|
-
onUndoClick: () => this.active.history.undo(),
|
|
1149
|
-
onRedoClick: () => this.active.history.redo(),
|
|
1150
|
-
onScreenshotClick: () => this.active.downloadScreenshot(),
|
|
1151
|
-
onAlertsClick: (anchor) => this.openAlertsMenu(anchor),
|
|
1152
|
-
timeframe: "60",
|
|
1153
|
-
timeframes: opts.timeframes ?? DEFAULT_TIMEFRAMES,
|
|
1154
|
-
timeframeFavorites: this.tfFavs,
|
|
1155
|
-
priceStyle: "candles",
|
|
1156
|
-
onTimeframe: (tf) => this.setActiveTimeframe(tf),
|
|
1157
|
-
onTimeframeFavorite: (tf, on) => this.setTimeframeFavorite(tf, on),
|
|
1158
|
-
onPriceStyle: (style) => this.active.setPriceStyle(style),
|
|
1159
|
-
layout: {
|
|
1160
|
-
current: this.def.id,
|
|
1161
|
-
// The picker composes dynamic layouts on its grid canvas; registered
|
|
1162
|
-
// presets the canvas cannot express (bespoke plugin areas) list as rows.
|
|
1163
|
-
shape: () => layoutShape(this.def),
|
|
1164
|
-
presets: () => layouts().filter((l) => layoutShape(l) === null).map((l) => ({ id: l.id, label: l.label })),
|
|
1165
|
-
onSelectGrid: (rows, cols) => this.setLayout(layoutForGrid(rows, cols)),
|
|
1166
|
-
onSelectPreset: (id) => this.setLayout(id),
|
|
1167
|
-
// The SYNC switches reflect the simple all-cells form; flipping one
|
|
1168
|
-
// OVERRIDES a host-set group record with plain on/off (groups stay an
|
|
1169
|
-
// API-only shape).
|
|
1170
|
-
syncs: () => [
|
|
1171
|
-
{ id: "symbol", label: "Symbol", checked: this.syncOpts.symbol === true },
|
|
1172
|
-
{ id: "timeframe", label: "Interval", checked: this.syncOpts.timeframe === true },
|
|
1173
|
-
{ id: "crosshair", label: "Crosshair", checked: this.syncOpts.crosshair === true }
|
|
1174
|
-
],
|
|
1175
|
-
onToggleSync: (id) => {
|
|
1176
|
-
const kind = id;
|
|
1177
|
-
this.sync.set(kind, this.syncOpts[kind] ? false : true);
|
|
1178
|
-
}
|
|
1179
|
-
},
|
|
1180
|
-
getContext: () => this.context()
|
|
1181
|
-
});
|
|
1182
|
-
const main = doc.createElement("div");
|
|
1183
|
-
main.className = "vela-ws-main";
|
|
1184
|
-
let toolbarHost = null;
|
|
1185
|
-
if (opts.drawingToolbar !== false) {
|
|
1186
|
-
toolbarHost = doc.createElement("div");
|
|
1187
|
-
toolbarHost.className = "vela-ws-toolbar";
|
|
1188
|
-
main.appendChild(toolbarHost);
|
|
1189
|
-
}
|
|
1190
|
-
this.gridEl = doc.createElement("div");
|
|
1191
|
-
this.gridEl.className = "vela-ws-grid";
|
|
1192
|
-
main.appendChild(this.gridEl);
|
|
1193
|
-
this.dock = new PanelDock(main, {
|
|
1194
|
-
chrome: this.topbar,
|
|
1195
|
-
context: () => this.context(),
|
|
1196
|
-
changed: () => this.markStateDirty()
|
|
1197
|
-
});
|
|
1198
|
-
this.objectTree = new ObjectTree(main);
|
|
1199
|
-
this.dataWindow = new DataWindow(main);
|
|
1200
|
-
this.dock.addBuiltIn({ id: "dataWindow", title: "Data window", icon: "datawindow", order: 10, panel: this.dataWindow, onChart: (c) => this.dataWindow.onChart(c) });
|
|
1201
|
-
this.dock.addBuiltIn({ id: "objects", title: "Object tree", icon: "objects", order: 20, panel: this.objectTree, onChart: (c) => this.objectTree.onChart(c) });
|
|
1202
|
-
this.dock.refresh();
|
|
1203
|
-
this.root.appendChild(main);
|
|
1204
|
-
this.toast = new Toast(this.gridEl);
|
|
1205
|
-
const attribution = rendererDefaults().attribution;
|
|
1206
|
-
if (attribution !== false) {
|
|
1207
|
-
const background = resolveTheme(opts.theme).background;
|
|
1208
|
-
const mark = typeof attribution === "string" && attribution.trim() ? createCustomMark(doc, attribution, background) : createAttributionMark(doc, background);
|
|
1209
|
-
Object.assign(mark.style, { left: "12px", bottom: `${TIME_AXIS_H + 10}px`, zIndex: "11" });
|
|
1210
|
-
this.gridEl.appendChild(mark);
|
|
1211
|
-
this.attributionMark = mark;
|
|
1212
|
-
}
|
|
1213
|
-
this.drawToolbar = toolbarHost ? new DrawingToolbar(
|
|
1214
|
-
toolbarHost,
|
|
1215
|
-
resolveTheme(opts.theme),
|
|
1216
|
-
(type) => {
|
|
1217
|
-
this.active.chart.drawings.setTool(type);
|
|
1218
|
-
this.refocusActive();
|
|
1219
|
-
},
|
|
1220
|
-
(mode) => {
|
|
1221
|
-
this.active.chart.drawings.setSnapMode(mode);
|
|
1222
|
-
this.refocusActive();
|
|
1223
|
-
},
|
|
1224
|
-
() => {
|
|
1225
|
-
const d = this.active.chart.drawings;
|
|
1226
|
-
d.setMode(d.getMode() === "measure" ? null : "measure");
|
|
1227
|
-
this.refocusActive();
|
|
1228
|
-
},
|
|
1229
|
-
() => {
|
|
1230
|
-
const d = this.active.chart.drawings;
|
|
1231
|
-
d.setMode(d.getMode() === "eraser" ? null : "eraser");
|
|
1232
|
-
this.refocusActive();
|
|
1233
|
-
},
|
|
1234
|
-
// No refocus on a star: the flyout stays open for more browsing.
|
|
1235
|
-
(type, on) => this.active.chart.drawings.setFavorite(type, on),
|
|
1236
|
-
(on) => {
|
|
1237
|
-
this.active.chart.drawings.setStayMode(on);
|
|
1238
|
-
this.refocusActive();
|
|
1239
|
-
},
|
|
1240
|
-
{
|
|
1241
|
-
dock: "static",
|
|
1242
|
-
// Drawings sync is a WORKSPACE link (same model as the layout
|
|
1243
|
-
// dropdown's switches) — the bar only hosts its toggle.
|
|
1244
|
-
onDrawingsSync: (on) => {
|
|
1245
|
-
this.sync.set("drawings", on);
|
|
1246
|
-
this.refocusActive();
|
|
1247
|
-
}
|
|
1248
|
-
}
|
|
1249
|
-
) : null;
|
|
1250
|
-
this.drawToolbar?.setDefinition(defaultToolbar());
|
|
1251
|
-
this.drawToolbar?.setVisible(true);
|
|
1252
|
-
this.drawToolbar?.setDrawingsSyncMode(!!this.syncOpts.drawings);
|
|
1253
|
-
this.bottombar = opts.bottombar !== false ? new Bottombar(this.root, {
|
|
1254
|
-
timezone: this.timezone,
|
|
1255
|
-
onRange: (preset) => {
|
|
1256
|
-
this.active.applyRange(preset);
|
|
1257
|
-
this.bottombar?.setActiveRange(preset.id);
|
|
1258
|
-
},
|
|
1259
|
-
onTimezone: (zone) => this.setTimezone(zone),
|
|
1260
|
-
// RTH/ETH acts on the ACTIVE cell (like the range chips): sessions
|
|
1261
|
-
// are a per-chart market dimension, not a shell preference.
|
|
1262
|
-
onSession: (session) => this.active.setSession(session),
|
|
1263
|
-
onSettingsClick: () => this.active.chart.renderer.openSettings()
|
|
1264
|
-
}) : null;
|
|
1265
|
-
this.mobileBar = opts.bottombar !== false ? new MobileBar(this.root, {
|
|
1266
|
-
symbol: "",
|
|
1267
|
-
timeframe: "60",
|
|
1268
|
-
onSymbolClick: () => this.symbolPicker.open(),
|
|
1269
|
-
onTimeframeClick: () => this.openTimeframeDrawer(),
|
|
1270
|
-
...picker ? { onIndicatorsClick: () => picker.open() } : {},
|
|
1271
|
-
getContext: () => this.context(),
|
|
1272
|
-
onDrawingsClick: () => this.openDrawingsDrawer(),
|
|
1273
|
-
onMoreClick: () => this.openMoreDrawer(),
|
|
1274
|
-
onSettingsClick: () => this.active.chart.renderer.openSettings()
|
|
1275
|
-
}) : null;
|
|
1276
|
-
this.drawingPill = new DrawingPill(this.gridEl);
|
|
1277
|
-
hostEl.appendChild(this.root);
|
|
1278
|
-
this.layoutCtl = new LayoutModeController(this.root, opts.layoutMode ?? "auto");
|
|
1279
|
-
this.layoutCtl.onChange((mode) => this.onLayoutModeChange(mode));
|
|
1280
|
-
this.splitters = new SplitterLayer(this.gridEl, {
|
|
1281
|
-
tracks: () => this.currentTracks(),
|
|
1282
|
-
grid: () => occupancyGrid(this.def),
|
|
1283
|
-
apply: (axis, weights) => this.applyTracks(axis, weights),
|
|
1284
|
-
reset: (axis) => this.applyTracks(axis, evenTracks(this.currentTracks()[axis].length)),
|
|
1285
|
-
gapPx: () => GAP_PX
|
|
1286
|
-
});
|
|
1287
|
-
if (typeof ResizeObserver !== "undefined") {
|
|
1288
|
-
this.resizeObserver = new ResizeObserver(() => this.splitters.layout());
|
|
1289
|
-
this.resizeObserver.observe(this.gridEl);
|
|
1290
|
-
}
|
|
1291
|
-
this.keymap = new KeymapManager();
|
|
1292
|
-
this.keymap.attach(this.root);
|
|
1293
|
-
this.registerDefaultKeys();
|
|
1294
|
-
this.drawToolbar?.setShortcuts(toolShortcutHints(this.keymap));
|
|
1295
|
-
this.root.addEventListener("keydown", this.onRootKeydown);
|
|
1296
|
-
this.root.tabIndex = -1;
|
|
1297
|
-
this.cellBackend = this.backendFor(this.def);
|
|
1298
|
-
this.applyGrid();
|
|
1299
|
-
this.buildCells();
|
|
1300
|
-
this.syncCellPresentation();
|
|
1301
|
-
this.setActiveCell(bootActive != null && this.cellsById.has(bootActive) ? bootActive : this.order[0] ?? null);
|
|
1302
|
-
if (opts.autofocus) this.refocusActive();
|
|
1303
|
-
if (opts.indicators !== void 0) {
|
|
1304
|
-
void resolveIndicators(opts.indicators).then((list) => {
|
|
1305
|
-
if (this.destroyed) return;
|
|
1306
|
-
this.manifest = list;
|
|
1307
|
-
this.manifestSettled = true;
|
|
1308
|
-
for (const cell of this.cellsById.values()) cell.setManifest(list, true);
|
|
1309
|
-
this.projectActiveCell();
|
|
1310
|
-
});
|
|
1311
|
-
} else {
|
|
1312
|
-
this.manifestSettled = true;
|
|
1313
|
-
}
|
|
1314
|
-
this.mountAttachments();
|
|
1315
|
-
}
|
|
1316
|
-
// ── access ──────────────────────────────────────────────────
|
|
1317
|
-
/** The cell with identity `id` (its declared name, or `c<N>` when undeclared), or
|
|
1318
|
-
* undefined when no live cell holds it. */
|
|
1319
|
-
cell(id) {
|
|
1320
|
-
return this.cellsById.get(id);
|
|
1321
|
-
}
|
|
1322
|
-
/** Every live cell, in slot order. Enumerated over `order` — the IDENTITY space —
|
|
1323
|
-
* never the layout's positional slot ids, which only coincide with it for a
|
|
1324
|
-
* workspace whose cells are undeclared. Identities past the current layout size are
|
|
1325
|
-
* pooled, not live, so they drop out here. */
|
|
1326
|
-
cells() {
|
|
1327
|
-
return this.order.map((id) => this.cellsById.get(id)).filter((c) => c != null);
|
|
1328
|
-
}
|
|
1329
|
-
/** The ACTIVE cell — the one the shared chrome reflects and acts on. */
|
|
1330
|
-
get active() {
|
|
1331
|
-
const cell = this.activeId ? this.cellsById.get(this.activeId) : void 0;
|
|
1332
|
-
if (!cell) throw new Error("VelaWorkspace has no active cell (destroyed?)");
|
|
1333
|
-
return cell;
|
|
1334
|
-
}
|
|
1335
|
-
/** Shortcut for `active.chart` — the same habit as `widget.chart`. LIVE: read it at
|
|
1336
|
-
* the point of use; the durable identity to hold is the cell (or its id). */
|
|
1337
|
-
get chart() {
|
|
1338
|
-
return this.active.chart;
|
|
1339
|
-
}
|
|
1340
|
-
setActiveCell(id) {
|
|
1341
|
-
if (id === this.activeId || this.destroyed) return;
|
|
1342
|
-
const prev = this.activeId;
|
|
1343
|
-
if (prev) {
|
|
1344
|
-
const el = this.cellsById.get(prev)?.host;
|
|
1345
|
-
if (el) delete el.dataset.active;
|
|
1346
|
-
}
|
|
1347
|
-
this.activeId = id;
|
|
1348
|
-
const prevCell = prev ? this.cellsById.get(prev) : void 0;
|
|
1349
|
-
if (prevCell) {
|
|
1350
|
-
prevCell.chart.drawings.setTool(null);
|
|
1351
|
-
prevCell.chart.drawings.setMode(null);
|
|
1352
|
-
}
|
|
1353
|
-
if (id) {
|
|
1354
|
-
const el = this.cellsById.get(id)?.host;
|
|
1355
|
-
if (el) el.dataset.active = "1";
|
|
1356
|
-
}
|
|
1357
|
-
if (id) {
|
|
1358
|
-
this.projectActiveCell();
|
|
1359
|
-
this.events.emit("cell:active", { id, prev });
|
|
1360
|
-
this.markStateDirty();
|
|
1361
|
-
}
|
|
1362
|
-
}
|
|
1363
|
-
on(event, handler) {
|
|
1364
|
-
return this.events.on(event, handler);
|
|
1365
|
-
}
|
|
1366
|
-
/** The context handed to contributed actions/attachments (rebuilt per invocation). */
|
|
1367
|
-
context() {
|
|
1368
|
-
return buildContext({
|
|
1369
|
-
// Null during early construction (the topbar projects actions before cells exist).
|
|
1370
|
-
active: () => this.activeId ? this.cellsById.get(this.activeId) ?? null : null,
|
|
1371
|
-
cells: () => this.cells(),
|
|
1372
|
-
setActiveCell: (id) => this.setActiveCell(id),
|
|
1373
|
-
openSymbolSearch: (query) => this.symbolPicker.open(query ?? ""),
|
|
1374
|
-
togglePanel: (id, open) => this.dock.toggle(id, open),
|
|
1375
|
-
root: this.root,
|
|
1376
|
-
toast: (message, kind) => this.toast.show(message, kind)
|
|
1377
|
-
});
|
|
1378
|
-
}
|
|
1379
|
-
/** Re-project contributed topbar actions + side panels, and mount late-registered attachments. */
|
|
1380
|
-
refreshActions() {
|
|
1381
|
-
this.mountAttachments();
|
|
1382
|
-
this.topbar.renderActions();
|
|
1383
|
-
this.mobileBar?.renderActions();
|
|
1384
|
-
this.dock.refresh();
|
|
1385
|
-
for (const cell of this.cells()) cell.chart.renderer.setLegendActions(legendActionsProviderFor(cell.chart, () => this.context()));
|
|
1386
|
-
}
|
|
1387
|
-
// ── state surface (the SDK's read/restore of the whole grid's config + content) ──
|
|
1388
|
-
/**
|
|
1389
|
-
* Snapshot the COMPLETE workspace state as a versioned, serializable document:
|
|
1390
|
-
* layout + splitter sizes, active cell, sync links, timezone, and — per slot, live
|
|
1391
|
-
* AND dormant — the market, the renderer's cosmetic config, the user-drawings
|
|
1392
|
-
* document, and the indicator ledger. This is what `persist` writes; hosts build
|
|
1393
|
-
* custom flows on it (server snapshots, share links, templates).
|
|
1394
|
-
*/
|
|
1395
|
-
getState() {
|
|
1396
|
-
const byId = /* @__PURE__ */ new Map();
|
|
1397
|
-
for (const [id, cs] of this.pool) byId.set(id, cs);
|
|
1398
|
-
for (const [id, cell] of this.cellsById) byId.set(id, cell.dehydrate());
|
|
1399
|
-
const charts = [];
|
|
1400
|
-
for (const id of this.order) {
|
|
1401
|
-
const cs = byId.get(id);
|
|
1402
|
-
if (cs) {
|
|
1403
|
-
charts.push({ id, ...cs });
|
|
1404
|
-
byId.delete(id);
|
|
1405
|
-
}
|
|
1406
|
-
}
|
|
1407
|
-
for (const [id, cs] of byId) charts.push({ id, ...cs });
|
|
1408
|
-
const state = { version: 1, layout: this.def.id, timezone: this.timezone, sync: { ...this.syncOpts }, charts };
|
|
1409
|
-
if (this.activeId) state.activeCellId = this.activeId;
|
|
1410
|
-
if (this.favs.length > 0) state.favorites = [...this.favs];
|
|
1411
|
-
if (this.tfFavs.length > 0) state.timeframeFavorites = [...this.tfFavs];
|
|
1412
|
-
if (this.trackSizes.size > 0) state.trackSizes = Object.fromEntries([...this.trackSizes].map(([k, v]) => [k, { ...v }]));
|
|
1413
|
-
const panels = this.dock.getState();
|
|
1414
|
-
if (panels) state.panels = panels;
|
|
1415
|
-
return state;
|
|
1416
|
-
}
|
|
1417
|
-
/**
|
|
1418
|
-
* Restore a state document produced by {@link getState} (untrusted-safe: malformed
|
|
1419
|
-
* fields are dropped). Replaces the WHOLE workspace state: prefs, sync links,
|
|
1420
|
-
* layout, and every slot — current cells are rebuilt from the document. A layout id
|
|
1421
|
-
* that is not registered keeps the current grid (register custom layouts first).
|
|
1422
|
-
*/
|
|
1423
|
-
applyState(state) {
|
|
1424
|
-
if (this.destroyed) return;
|
|
1425
|
-
const st = sanitizeState(state);
|
|
1426
|
-
if (!st) return;
|
|
1427
|
-
if (st.timezone) {
|
|
1428
|
-
this.timezone = st.timezone;
|
|
1429
|
-
this.bottombar?.setTimezone(st.timezone);
|
|
1430
|
-
}
|
|
1431
|
-
if (st.favorites) this.favs = [...st.favorites];
|
|
1432
|
-
if (st.timeframeFavorites) {
|
|
1433
|
-
this.tfFavs = [...st.timeframeFavorites];
|
|
1434
|
-
this.topbar.setTimeframeFavorites(this.tfFavs);
|
|
1435
|
-
}
|
|
1436
|
-
this.dock.applyState(st.panels);
|
|
1437
|
-
for (const kind of ["viewport", "symbol", "timeframe", "crosshair", "drawings"]) this.applySyncSetting(kind, st.sync?.[kind]);
|
|
1438
|
-
this.trackSizes.clear();
|
|
1439
|
-
if (st.trackSizes) for (const [id, ts] of Object.entries(st.trackSizes)) this.trackSizes.set(id, ts);
|
|
1440
|
-
for (const [id, cell] of [...this.cellsById]) {
|
|
1441
|
-
cell.destroy();
|
|
1442
|
-
this.cellsById.delete(id);
|
|
1443
|
-
this.events.emit("cell:destroyed", { id });
|
|
1444
|
-
}
|
|
1445
|
-
this.pool.clear();
|
|
1446
|
-
this.drawingLinks.clear();
|
|
1447
|
-
for (const { id, ...cs } of st.charts) this.pool.set(id, cs);
|
|
1448
|
-
this.order = st.charts.map((c) => c.id);
|
|
1449
|
-
const def = ensureLayout(st.layout);
|
|
1450
|
-
if (def) this.def = def;
|
|
1451
|
-
this.cellBackend = this.backendFor(this.def);
|
|
1452
|
-
this.applyGrid();
|
|
1453
|
-
this.buildCells();
|
|
1454
|
-
this.syncCellPresentation();
|
|
1455
|
-
this.topbar.setLayout(this.def.id);
|
|
1456
|
-
const nextActive = st.activeCellId && this.cellsById.has(st.activeCellId) ? st.activeCellId : this.order[0] ?? null;
|
|
1457
|
-
if (nextActive === this.activeId) this.projectActiveCell();
|
|
1458
|
-
else this.setActiveCell(nextActive);
|
|
1459
|
-
this.refreshRetention();
|
|
1460
|
-
this.events.emit("layout:changed", { layout: this.def.id });
|
|
1461
|
-
this.markStateDirty();
|
|
1462
|
-
}
|
|
1463
|
-
/** Set the workspace-global display timezone — applied to EVERY cell. */
|
|
1464
|
-
setTimezone(zone) {
|
|
1465
|
-
this.timezone = zone;
|
|
1466
|
-
this.bottombar?.setTimezone(zone);
|
|
1467
|
-
for (const cell of this.cellsById.values()) cell.chart.renderer.set("timezone", zone);
|
|
1468
|
-
this.markStateDirty();
|
|
1469
|
-
}
|
|
1470
|
-
/**
|
|
1471
|
-
* Swap the workspace theme at runtime — `'dark'`, `'light'`, or a full custom theme,
|
|
1472
|
-
* applied to the shared chrome (topbar, panels, drawing toolbar) and EVERY cell.
|
|
1473
|
-
* Also reached from any cell's chart settings → Canvas → Theme. The choice sticks:
|
|
1474
|
-
* cells rebuilt by later layout switches reconstruct with it.
|
|
1475
|
-
*/
|
|
1476
|
-
setTheme(theme) {
|
|
1477
|
-
if (this.destroyed) return;
|
|
1478
|
-
const t = resolveTheme(theme);
|
|
1479
|
-
this.opts.theme = theme;
|
|
1480
|
-
ensureUIHost(this.root, t);
|
|
1481
|
-
this.drawToolbar?.setTheme(t);
|
|
1482
|
-
if (this.attributionMark) applyAttributionMarkTheme(this.attributionMark, t.background);
|
|
1483
|
-
for (const cell of this.cellsById.values()) cell.chart.setTheme(t);
|
|
1484
|
-
}
|
|
1485
|
-
// ── layout ──────────────────────────────────────────────────
|
|
1486
|
-
get layout() {
|
|
1487
|
-
return this.def;
|
|
1488
|
-
}
|
|
1489
|
-
/**
|
|
1490
|
-
* Switch the grid. Cells are diffed BY IDENTITY (`order` head of the next size):
|
|
1491
|
-
* surviving cells keep their live charts untouched; cells past the new size
|
|
1492
|
-
* dehydrate into the pool; (re)appearing positions hydrate their identity from
|
|
1493
|
-
* the pool (or its seed). Crossing the WebGL budget rebuilds every cell through
|
|
1494
|
-
* the pool so the backend stays uniform.
|
|
1495
|
-
*/
|
|
1496
|
-
setLayout(layout) {
|
|
1497
|
-
if (this.destroyed) return;
|
|
1498
|
-
const next = this.resolveLayout(layout);
|
|
1499
|
-
const nextBackend = this.backendFor(next);
|
|
1500
|
-
const rebuildAll = nextBackend !== this.cellBackend;
|
|
1501
|
-
this.order = orderAfterLayout(this.order, next.cells.length, this.activeId);
|
|
1502
|
-
const keep = new Set(this.order.slice(0, next.cells.length));
|
|
1503
|
-
for (const [id, cell] of [...this.cellsById]) {
|
|
1504
|
-
if (!keep.has(id) || rebuildAll) {
|
|
1505
|
-
this.poolSet(id, cell.dehydrate());
|
|
1506
|
-
cell.destroy();
|
|
1507
|
-
this.cellsById.delete(id);
|
|
1508
|
-
this.events.emit("cell:destroyed", { id });
|
|
1509
|
-
}
|
|
1510
|
-
}
|
|
1511
|
-
this.def = next;
|
|
1512
|
-
this.cellBackend = nextBackend;
|
|
1513
|
-
this.applyGrid();
|
|
1514
|
-
this.buildCells();
|
|
1515
|
-
this.syncCellPresentation();
|
|
1516
|
-
this.topbar.setLayout(next.id);
|
|
1517
|
-
const nextActive = activeAfterLayout(this.activeId, this.order.slice(0, next.cells.length));
|
|
1518
|
-
if (nextActive === this.activeId) this.projectActiveCell();
|
|
1519
|
-
else this.setActiveCell(nextActive);
|
|
1520
|
-
this.refreshRetention();
|
|
1521
|
-
this.events.emit("layout:changed", { layout: next.id });
|
|
1522
|
-
this.markStateDirty();
|
|
1523
|
-
}
|
|
1524
|
-
resize() {
|
|
1525
|
-
this.splitters.layout();
|
|
1526
|
-
}
|
|
1527
|
-
destroy() {
|
|
1528
|
-
if (this.destroyed) return;
|
|
1529
|
-
this.persistNow();
|
|
1530
|
-
this.destroyed = true;
|
|
1531
|
-
if (this.stateTimer != null) clearTimeout(this.stateTimer);
|
|
1532
|
-
if (this.persistKey !== null && typeof window !== "undefined") window.removeEventListener("beforeunload", this.onUnload);
|
|
1533
|
-
this.resizeObserver?.disconnect();
|
|
1534
|
-
this.splitters.destroy();
|
|
1535
|
-
for (const [id, cell] of [...this.cellsById]) {
|
|
1536
|
-
cell.destroy();
|
|
1537
|
-
this.cellsById.delete(id);
|
|
1538
|
-
}
|
|
1539
|
-
for (const dispose of this.attachmentDisposers.values()) {
|
|
1540
|
-
try {
|
|
1541
|
-
dispose();
|
|
1542
|
-
} catch {
|
|
1543
|
-
}
|
|
1544
|
-
}
|
|
1545
|
-
this.attachmentDisposers.clear();
|
|
1546
|
-
this.root.removeEventListener("keydown", this.onRootKeydown);
|
|
1547
|
-
this.keymap.destroy();
|
|
1548
|
-
this.drawToolbar?.destroy();
|
|
1549
|
-
this.topbar.destroy();
|
|
1550
|
-
this.bottombar?.destroy();
|
|
1551
|
-
this.mobileBar?.destroy();
|
|
1552
|
-
this.drawingPill.destroy();
|
|
1553
|
-
this.tfDrawer?.destroy();
|
|
1554
|
-
this.drawingsDrawer?.destroy();
|
|
1555
|
-
this.moreDrawer?.destroy();
|
|
1556
|
-
this.timezoneDrawer?.destroy();
|
|
1557
|
-
this.priceScaleDrawer?.destroy();
|
|
1558
|
-
this.layoutCtl.destroy();
|
|
1559
|
-
this.dock.destroy();
|
|
1560
|
-
this.objectTree.destroy();
|
|
1561
|
-
this.dataWindow.destroy();
|
|
1562
|
-
this.symbolPicker.destroy();
|
|
1563
|
-
this.indicatorPicker?.destroy();
|
|
1564
|
-
this.tfQuick.destroy();
|
|
1565
|
-
this.shortcutsHelp?.destroy();
|
|
1566
|
-
this.toast.destroy();
|
|
1567
|
-
this.alertsMenu?.destroy();
|
|
1568
|
-
this.glider.stop();
|
|
1569
|
-
sharedBarStore.retain(/* @__PURE__ */ new Set());
|
|
1570
|
-
this.root.remove();
|
|
1571
|
-
this.events.clear();
|
|
1572
|
-
}
|
|
1573
|
-
// ── the projection rule (trigger ① — full rebind on activation) ──
|
|
1574
|
-
/** Re-project the shared chrome from the ACTIVE cell. The chrome holds no state of
|
|
1575
|
-
* its own — this is a pure read of the cell, safe to call redundantly. */
|
|
1576
|
-
projectActiveCell() {
|
|
1577
|
-
const cell = this.activeId ? this.cellsById.get(this.activeId) : void 0;
|
|
1578
|
-
if (!cell) return;
|
|
1579
|
-
this.topbar.setSymbol(cell.symbol);
|
|
1580
|
-
this.topbar.setTimeframe(cell.timeframe);
|
|
1581
|
-
this.topbar.setPriceStyle(cell.priceStyle);
|
|
1582
|
-
this.topbar.setIndicatorCount(cell.indicatorCount);
|
|
1583
|
-
this.topbar.renderActions();
|
|
1584
|
-
this.mobileBar?.renderActions();
|
|
1585
|
-
this.mobileBar?.setSymbol(cell.symbol);
|
|
1586
|
-
this.mobileBar?.setTimeframe(cell.timeframe);
|
|
1587
|
-
this.drawingPill.onChart(cell.chart);
|
|
1588
|
-
const pushHistory = () => this.topbar.setHistoryState(cell.history.canUndo, cell.history.canRedo);
|
|
1589
|
-
this.historyUnsub?.();
|
|
1590
|
-
this.historyUnsub = cell.history.onChange(pushHistory);
|
|
1591
|
-
pushHistory();
|
|
1592
|
-
this.objectTree.setSymbol(cell.symbol);
|
|
1593
|
-
this.dock.onChart(cell.chart);
|
|
1594
|
-
this.bottombar?.setActiveRange(cell.activeRangeId);
|
|
1595
|
-
this.bottombar?.setSession({ session: cell.session, enabled: cell.sessionAvailable });
|
|
1596
|
-
this.indicatorPicker?.sync();
|
|
1597
|
-
this.glider.stop();
|
|
1598
|
-
const d = cell.chart.drawings;
|
|
1599
|
-
if (d.getTool() !== this.globalTool) d.setTool(this.globalTool);
|
|
1600
|
-
if (d.getSnapMode() !== this.globalSnap) d.setSnapMode(this.globalSnap);
|
|
1601
|
-
if (d.getStayMode() !== this.globalStay) d.setStayMode(this.globalStay);
|
|
1602
|
-
if (this.drawToolbar) {
|
|
1603
|
-
this.drawToolbar.setActiveTool(this.globalTool);
|
|
1604
|
-
this.drawToolbar.setMagnetMode(this.globalSnap);
|
|
1605
|
-
this.drawToolbar.setStayMode(this.globalStay);
|
|
1606
|
-
const mode = d.getMode();
|
|
1607
|
-
this.drawToolbar.setMeasureActive(mode === "measure");
|
|
1608
|
-
this.drawToolbar.setEraserActive(mode === "eraser");
|
|
1609
|
-
this.drawToolbar.setFavorites(d.favorites());
|
|
1610
|
-
}
|
|
1611
|
-
}
|
|
1612
|
-
/** Put keyboard focus back on the active cell's chart surface (after a toolbar press
|
|
1613
|
-
* stole it) so chart/drawing shortcuts keep working. */
|
|
1614
|
-
refocusActive() {
|
|
1615
|
-
if (this.activeId) this.cellsById.get(this.activeId)?.chart.renderer.focus();
|
|
1616
|
-
}
|
|
1617
|
-
/** Debounced dirty mark: one `state:changed` (+ one storage write in persist mode)
|
|
1618
|
-
* per burst of edits, flushed hard on unload/destroy. */
|
|
1619
|
-
markStateDirty() {
|
|
1620
|
-
if (this.destroyed) return;
|
|
1621
|
-
if (this.stateTimer != null) clearTimeout(this.stateTimer);
|
|
1622
|
-
this.stateTimer = setTimeout(() => {
|
|
1623
|
-
this.stateTimer = null;
|
|
1624
|
-
this.events.emit("state:changed", void 0);
|
|
1625
|
-
this.persistNow();
|
|
1626
|
-
}, 500);
|
|
1627
|
-
}
|
|
1628
|
-
/** Write the current state through the storage adapter now (fire-and-forget). */
|
|
1629
|
-
persistNow() {
|
|
1630
|
-
if (this.persistKey === null || this.destroyed) return;
|
|
1631
|
-
try {
|
|
1632
|
-
void this.storage.set(this.persistKey, encodeState(this.getState()));
|
|
1633
|
-
} catch {
|
|
1634
|
-
}
|
|
1635
|
-
}
|
|
1636
|
-
// ── internals ───────────────────────────────────────────────
|
|
1637
|
-
resolveLayout(layout) {
|
|
1638
|
-
if (typeof layout !== "string") return layout;
|
|
1639
|
-
const def = ensureLayout(layout);
|
|
1640
|
-
if (!def) throw new Error(`[vela] unknown workspace layout "${layout}" \u2014 register it with registerLayout().`);
|
|
1641
|
-
return def;
|
|
1642
|
-
}
|
|
1643
|
-
backendFor(def) {
|
|
1644
|
-
const explicit = this.opts.nativeBackend;
|
|
1645
|
-
if (explicit && explicit !== "auto") return explicit;
|
|
1646
|
-
return def.cells.length > (this.opts.maxWebglCells ?? 8) ? "canvas2d" : "auto";
|
|
1647
|
-
}
|
|
1648
|
-
currentTracks() {
|
|
1649
|
-
const sizes = this.trackSizes.get(this.def.id);
|
|
1650
|
-
return {
|
|
1651
|
-
cols: sizes?.cols?.length === this.def.cols.length ? [...sizes.cols] : [...this.def.cols],
|
|
1652
|
-
rows: sizes?.rows?.length === this.def.rows.length ? [...sizes.rows] : [...this.def.rows]
|
|
1653
|
-
};
|
|
1654
|
-
}
|
|
1655
|
-
applyTracks(axis, weights) {
|
|
1656
|
-
const sizes = this.trackSizes.get(this.def.id) ?? {};
|
|
1657
|
-
sizes[axis] = weights;
|
|
1658
|
-
this.trackSizes.set(this.def.id, sizes);
|
|
1659
|
-
this.applyGrid();
|
|
1660
|
-
this.markStateDirty();
|
|
1661
|
-
}
|
|
1662
|
-
/** Apply the grid template (+ per-cell areas) and reposition the splitter strips.
|
|
1663
|
-
* Geometry is keyed by SLOT (`perCell[slot.id]`); the cell living there is
|
|
1664
|
-
* `order[i]` — the identity/position decoupling in one line. */
|
|
1665
|
-
applyGrid() {
|
|
1666
|
-
const { container, perCell } = gridStyles(this.def, this.trackSizes.get(this.def.id));
|
|
1667
|
-
if (this.def.cells.length > 1) this.gridEl.dataset.multi = "1";
|
|
1668
|
-
else delete this.gridEl.dataset.multi;
|
|
1669
|
-
this.gridEl.style.gridTemplateColumns = container.gridTemplateColumns ?? "";
|
|
1670
|
-
this.gridEl.style.gridTemplateRows = container.gridTemplateRows ?? "";
|
|
1671
|
-
this.gridEl.style.gridTemplateAreas = container.gridTemplateAreas ?? "";
|
|
1672
|
-
for (const [i, slot] of this.def.cells.entries()) {
|
|
1673
|
-
const host = this.cellsById.get(this.order[i] ?? "")?.host;
|
|
1674
|
-
if (host) host.style.gridArea = perCell[slot.id]?.gridArea ?? "";
|
|
1675
|
-
}
|
|
1676
|
-
this.splitters.layout();
|
|
1677
|
-
}
|
|
1678
|
-
/** Create the cells the current layout wants but don't exist yet (pool-first).
|
|
1679
|
-
* A slot's CELL IDENTITY is `order[i]` (declaration order — never the slot's own
|
|
1680
|
-
* positional id); slots past the declared list mint an auto identity once. */
|
|
1681
|
-
buildCells() {
|
|
1682
|
-
const theme = resolveTheme(this.opts.theme);
|
|
1683
|
-
const { perCell } = gridStyles(this.def, this.trackSizes.get(this.def.id));
|
|
1684
|
-
for (const [i, slot] of this.def.cells.entries()) {
|
|
1685
|
-
let id = this.order[i];
|
|
1686
|
-
if (!id) {
|
|
1687
|
-
id = nextAutoCellId(/* @__PURE__ */ new Set([...this.order, ...this.pool.keys(), ...this.cellsById.keys()]));
|
|
1688
|
-
this.order[i] = id;
|
|
1689
|
-
}
|
|
1690
|
-
if (this.cellsById.has(id)) continue;
|
|
1691
|
-
const pooled = this.pool.get(id);
|
|
1692
|
-
const seed = pooled ?? { ...seedDefaults(this.opts), ...this.opts.cells?.[id] ?? {} };
|
|
1693
|
-
this.pool.delete(id);
|
|
1694
|
-
const cell = new ChartCell(id, this.gridEl, seed, {
|
|
1695
|
-
feed: this.feed,
|
|
1696
|
-
engines: this.opts.engines ?? {},
|
|
1697
|
-
chartDefaults: cellChartDefaults(this.opts),
|
|
1698
|
-
theme,
|
|
1699
|
-
live: this.opts.live ?? false,
|
|
1700
|
-
volume: this.opts.volume ?? true,
|
|
1701
|
-
statusline: this.opts.statusline !== false,
|
|
1702
|
-
watermark: this.opts.watermark !== false,
|
|
1703
|
-
nativeBackend: this.cellBackend,
|
|
1704
|
-
dialogHost: this.root,
|
|
1705
|
-
timezone: () => this.timezone,
|
|
1706
|
-
setTimezone: (zone) => this.setTimezone(zone),
|
|
1707
|
-
context: () => this.context(),
|
|
1708
|
-
activate: (id2) => this.setActiveCell(id2),
|
|
1709
|
-
onMarketChanged: (id2) => this.onCellMarketChanged(id2),
|
|
1710
|
-
onPriceStyleChanged: (id2) => this.onCellPriceStyleChanged(id2),
|
|
1711
|
-
onIndicatorsChanged: (id2) => this.onCellIndicatorsChanged(id2),
|
|
1712
|
-
onStateDirty: () => this.markStateDirty(),
|
|
1713
|
-
manifestSettled: () => this.manifestSettled
|
|
1714
|
-
});
|
|
1715
|
-
cell.host.style.gridArea = perCell[slot.id]?.gridArea ?? "";
|
|
1716
|
-
this.cellsById.set(id, cell);
|
|
1717
|
-
if (id === this.activeId) cell.host.dataset.active = "1";
|
|
1718
|
-
this.wireCell(cell);
|
|
1719
|
-
cell.chart.renderer.setLayoutMode(this.layoutCtl.current);
|
|
1720
|
-
if (this.favs.length > 0) cell.chart.drawings.setFavorites(this.favs);
|
|
1721
|
-
cell.setManifest(this.manifest, pooled?.indicators == null);
|
|
1722
|
-
this.events.emit("cell:created", { id });
|
|
1723
|
-
}
|
|
1724
|
-
for (const [i] of this.def.cells.entries()) {
|
|
1725
|
-
const host = this.cellsById.get(this.order[i] ?? "")?.host;
|
|
1726
|
-
if (host) this.gridEl.appendChild(host);
|
|
1727
|
-
}
|
|
1728
|
-
}
|
|
1729
|
-
/** Per-cell chart subscriptions (trigger ② — the chart instance is stable for the
|
|
1730
|
-
* cell's whole life, so these live and die with the cell). */
|
|
1731
|
-
wireCell(cell) {
|
|
1732
|
-
const chart = cell.chart;
|
|
1733
|
-
chart.on("indicator:error", ({ error }) => this.toast.show(`[${cell.id}] ${error.message}`, "error", 5e3));
|
|
1734
|
-
chart.on("script:run", (run) => this.events.emit("script:run", { ...run, cell: cell.id }));
|
|
1735
|
-
chart.on("alert", (alert) => {
|
|
1736
|
-
this.alerts.unshift({ cellId: cell.id, symbol: cell.symbol, title: alert.title ?? "Alert", message: alert.message, time: alert.time });
|
|
1737
|
-
if (this.alerts.length > ALERT_CAP) this.alerts.pop();
|
|
1738
|
-
this.toast.show(`[${cell.id} \xB7 ${cell.symbol}] ${alert.title ? alert.title + " \u2014 " : ""}${alert.message}`, "info", 4e3);
|
|
1739
|
-
this.topbar.setAlertCount(this.alerts.length);
|
|
1740
|
-
});
|
|
1741
|
-
chart.on("drawing:favorites", ({ favorites }) => {
|
|
1742
|
-
this.favs = favorites;
|
|
1743
|
-
for (const other of this.cellsById.values()) {
|
|
1744
|
-
if (other !== cell) other.chart.drawings.setFavorites(favorites);
|
|
1745
|
-
}
|
|
1746
|
-
this.drawToolbar?.setFavorites(favorites);
|
|
1747
|
-
this.markStateDirty();
|
|
1748
|
-
});
|
|
1749
|
-
chart.renderer.onCrosshairMove((e) => this.propagateCrosshair(cell.id, e.time, e.paneKind === "price" ? e.price : null));
|
|
1750
|
-
chart.on("drawing:created", ({ id }) => {
|
|
1751
|
-
this.propagateDrawing(cell.id, id);
|
|
1752
|
-
this.markStateDirty();
|
|
1753
|
-
});
|
|
1754
|
-
chart.on("drawing:draft", ({ doc }) => this.propagateDraft(cell.id, doc));
|
|
1755
|
-
chart.on("drawing:edited", ({ id }) => {
|
|
1756
|
-
this.propagateDrawingEdit(cell.id, id);
|
|
1757
|
-
this.markStateDirty();
|
|
1758
|
-
});
|
|
1759
|
-
chart.on("drawing:removed", ({ id }) => {
|
|
1760
|
-
this.propagateDrawingRemoval(cell.id, id);
|
|
1761
|
-
this.markStateDirty();
|
|
1762
|
-
});
|
|
1763
|
-
chart.on("drawing:tool", ({ type }) => {
|
|
1764
|
-
if (cell.id !== this.activeId) return;
|
|
1765
|
-
this.globalTool = type;
|
|
1766
|
-
this.drawToolbar?.setActiveTool(type);
|
|
1767
|
-
});
|
|
1768
|
-
chart.on("drawing:snap", ({ mode }) => {
|
|
1769
|
-
if (cell.id !== this.activeId) return;
|
|
1770
|
-
this.globalSnap = mode;
|
|
1771
|
-
this.drawToolbar?.setMagnetMode(mode);
|
|
1772
|
-
});
|
|
1773
|
-
chart.on("drawing:stay", ({ on }) => {
|
|
1774
|
-
if (cell.id !== this.activeId) return;
|
|
1775
|
-
this.globalStay = on;
|
|
1776
|
-
this.drawToolbar?.setStayMode(on);
|
|
1777
|
-
});
|
|
1778
|
-
chart.on("drawing:mode", ({ mode }) => {
|
|
1779
|
-
if (cell.id !== this.activeId) return;
|
|
1780
|
-
this.drawToolbar?.setMeasureActive(mode === "measure");
|
|
1781
|
-
this.drawToolbar?.setEraserActive(mode === "eraser");
|
|
1782
|
-
});
|
|
1783
|
-
chart.on("viewport:changed", (range) => this.propagateViewport(cell.id, range));
|
|
1784
|
-
chart.on("theme:changed", (t) => this.setTheme(t));
|
|
1785
|
-
chart.renderer.onAxisLongPress((e) => {
|
|
1786
|
-
if (this.layoutCtl.current !== "mobile") return;
|
|
1787
|
-
if (e.axis === "time") this.openTimezoneDrawer();
|
|
1788
|
-
else this.openPriceScaleDrawer(e.y);
|
|
1789
|
-
});
|
|
1790
|
-
}
|
|
1791
|
-
// ── sync links ──────────────────────────────────────────────
|
|
1792
|
-
applySyncSetting(kind, setting, align = false) {
|
|
1793
|
-
if (setting == null || setting === false) delete this.syncOpts[kind];
|
|
1794
|
-
else this.syncOpts[kind] = setting;
|
|
1795
|
-
if (kind === "drawings") {
|
|
1796
|
-
for (const cell of this.cellsById.values()) cell.chart.drawings.setExternalGhost(null);
|
|
1797
|
-
this.drawToolbar?.setDrawingsSyncMode(!!setting);
|
|
1798
|
-
this.markStateDirty();
|
|
1799
|
-
return;
|
|
1800
|
-
}
|
|
1801
|
-
if (kind === "crosshair") {
|
|
1802
|
-
for (const cell of this.cellsById.values()) cell.chart.renderer.setExternalCrosshair(null);
|
|
1803
|
-
if (setting && ![...this.cellsById.values()].some((c) => c.chart.renderer.supportsExternalCrosshair)) {
|
|
1804
|
-
console.warn("[vela] crosshair sync: no cell renderer supports an external crosshair \u2014 nothing will show.");
|
|
1805
|
-
}
|
|
1806
|
-
if (this.topbar && this.def) this.topbar.setLayout(this.def.id);
|
|
1807
|
-
this.markStateDirty();
|
|
1808
|
-
return;
|
|
1809
|
-
}
|
|
1810
|
-
if (this.topbar && this.def) this.topbar.setLayout(this.def.id);
|
|
1811
|
-
this.markStateDirty();
|
|
1812
|
-
if (align && setting && this.activeId) {
|
|
1813
|
-
if (kind === "viewport") {
|
|
1814
|
-
const range = this.cellsById.get(this.activeId)?.chart.getVisibleRange();
|
|
1815
|
-
if (range) this.propagateViewport(this.activeId, range);
|
|
1816
|
-
} else {
|
|
1817
|
-
this.propagateMarket(this.activeId);
|
|
1818
|
-
}
|
|
1819
|
-
}
|
|
1820
|
-
}
|
|
1821
|
-
/**
|
|
1822
|
-
* Mirror an origin cell's pointer time onto its same-group followers as GHOST
|
|
1823
|
-
* crosshairs (`renderer.setExternalCrosshair`). The horizontal price level rides
|
|
1824
|
-
* along ONLY to followers showing the same ticker — price scales are comparable
|
|
1825
|
-
* there, while an origin's price painted on another market's pane would be noise.
|
|
1826
|
-
* Leaving the origin propagates `null` (the event already carries it) and clears
|
|
1827
|
-
* every ghost. No busy guard needed: an external crosshair never re-emits
|
|
1828
|
-
* `onCrosshairMove` — the flow is one-way by port contract, so no echo loop can
|
|
1829
|
-
* exist.
|
|
1830
|
-
*/
|
|
1831
|
-
propagateCrosshair(originId, time, price = null) {
|
|
1832
|
-
if (this.destroyed) return;
|
|
1833
|
-
const setting = this.syncOpts.crosshair;
|
|
1834
|
-
if (!setting) return;
|
|
1835
|
-
const originTicker = parseSymbol(this.cellsById.get(originId)?.symbol ?? "").ticker;
|
|
1836
|
-
for (const id of syncTargets(originId, setting, [...this.cellsById.keys()])) {
|
|
1837
|
-
const follower = this.cellsById.get(id);
|
|
1838
|
-
if (!follower) continue;
|
|
1839
|
-
const comparable = originTicker !== "" && parseSymbol(follower.symbol).ticker === originTicker;
|
|
1840
|
-
follower.chart.renderer.setExternalCrosshair(time, comparable ? price : null);
|
|
1841
|
-
}
|
|
1842
|
-
}
|
|
1843
|
-
/**
|
|
1844
|
-
* Push an origin cell's visible range onto its same-group followers. Loop-safe two
|
|
1845
|
-
* ways: the busy guard eats the followers' SYNCHRONOUS echoes (their setVisibleRange
|
|
1846
|
-
* re-emits `viewport:changed` in the same tick), and the half-bar epsilon
|
|
1847
|
-
* short-circuits any async residue — a follower already within half of ITS OWN bar
|
|
1848
|
-
* interval is left alone, so cross-timeframe groups settle instead of oscillating.
|
|
1849
|
-
*/
|
|
1850
|
-
propagateViewport(originId, range) {
|
|
1851
|
-
if (this.syncBusy || this.destroyed) return;
|
|
1852
|
-
const targets = syncTargets(originId, this.syncOpts.viewport, [...this.cellsById.keys()]);
|
|
1853
|
-
if (targets.length === 0) return;
|
|
1854
|
-
this.syncBusy = true;
|
|
1855
|
-
try {
|
|
1856
|
-
for (const id of targets) {
|
|
1857
|
-
const cell = this.cellsById.get(id);
|
|
1858
|
-
if (!cell) continue;
|
|
1859
|
-
const current = cell.chart.getVisibleRange();
|
|
1860
|
-
const tfMs = timeframeToMs(cell.timeframe);
|
|
1861
|
-
const eps = Number.isFinite(tfMs) ? tfMs / 2 : 0;
|
|
1862
|
-
if (current && rangesWithin(current, range, eps)) continue;
|
|
1863
|
-
cell.chart.setVisibleRange(range);
|
|
1864
|
-
}
|
|
1865
|
-
} finally {
|
|
1866
|
-
this.syncBusy = false;
|
|
1867
|
-
}
|
|
1868
|
-
}
|
|
1869
|
-
/**
|
|
1870
|
-
* Copy a freshly created drawing onto the origin's same-group followers (fresh ids
|
|
1871
|
-
* through `drawings.add` — anchors are time+price, so the copy lands at the same
|
|
1872
|
-
* spot whatever the follower shows) and register the whole set as LINKED: edits
|
|
1873
|
-
* and removals of any member follow while the link is on ({@link drawingLinks}).
|
|
1874
|
-
* The busy guard stops the copies' own `drawing:created` events from fanning out.
|
|
1875
|
-
*/
|
|
1876
|
-
propagateDrawing(originId, drawingId) {
|
|
1877
|
-
if (this.drawingSyncBusy || this.destroyed) return;
|
|
1878
|
-
const setting = this.syncOpts.drawings;
|
|
1879
|
-
if (!setting) return;
|
|
1880
|
-
const doc = this.cellsById.get(originId)?.chart.drawings.all().find((d) => d.id === drawingId);
|
|
1881
|
-
if (!doc) return;
|
|
1882
|
-
this.drawingSyncBusy = true;
|
|
1883
|
-
try {
|
|
1884
|
-
const group = /* @__PURE__ */ new Map([[originId, drawingId]]);
|
|
1885
|
-
for (const id of syncTargets(originId, setting, [...this.cellsById.keys()])) {
|
|
1886
|
-
const copy = this.cellsById.get(id)?.chart.drawings.add(doc.type, {
|
|
1887
|
-
paneId: doc.paneId,
|
|
1888
|
-
anchors: doc.anchors,
|
|
1889
|
-
style: doc.style,
|
|
1890
|
-
text: doc.text,
|
|
1891
|
-
props: doc.props
|
|
1892
|
-
});
|
|
1893
|
-
if (copy) group.set(id, copy.id);
|
|
1894
|
-
}
|
|
1895
|
-
if (group.size > 1) {
|
|
1896
|
-
for (const [cellId, dId] of group) this.drawingLinks.set(`${cellId}\0${dId}`, group);
|
|
1897
|
-
}
|
|
1898
|
-
} finally {
|
|
1899
|
-
this.drawingSyncBusy = false;
|
|
1900
|
-
}
|
|
1901
|
-
}
|
|
1902
|
-
/** Mirror an in-progress placement (its current ghost, `null` = placement ended)
|
|
1903
|
-
* onto the origin's same-group followers — the live half of the drawings link;
|
|
1904
|
-
* the created copy replaces the ghosts when the placement completes. One-way by
|
|
1905
|
-
* contract (an external ghost never re-emits drafts), so no busy guard needed. */
|
|
1906
|
-
propagateDraft(originId, doc) {
|
|
1907
|
-
if (this.destroyed) return;
|
|
1908
|
-
const setting = this.syncOpts.drawings;
|
|
1909
|
-
if (!setting) return;
|
|
1910
|
-
for (const id of syncTargets(originId, setting, [...this.cellsById.keys()])) {
|
|
1911
|
-
this.cellsById.get(id)?.chart.drawings.setExternalGhost(doc);
|
|
1912
|
-
}
|
|
1913
|
-
}
|
|
1914
|
-
/** Push a linked drawing's edited CONTENT (anchors, style, text, per-type props)
|
|
1915
|
-
* onto its same-group peers — any member propagates, not just the original. */
|
|
1916
|
-
propagateDrawingEdit(originId, drawingId) {
|
|
1917
|
-
if (this.drawingSyncBusy || this.destroyed) return;
|
|
1918
|
-
const setting = this.syncOpts.drawings;
|
|
1919
|
-
if (!setting) return;
|
|
1920
|
-
const group = this.drawingLinks.get(`${originId}\0${drawingId}`);
|
|
1921
|
-
if (!group) return;
|
|
1922
|
-
const doc = this.cellsById.get(originId)?.chart.drawings.all().find((d) => d.id === drawingId);
|
|
1923
|
-
if (!doc) return;
|
|
1924
|
-
this.drawingSyncBusy = true;
|
|
1925
|
-
try {
|
|
1926
|
-
for (const id of syncTargets(originId, setting, [...this.cellsById.keys()])) {
|
|
1927
|
-
const peerId = group.get(id);
|
|
1928
|
-
if (peerId == null) continue;
|
|
1929
|
-
this.cellsById.get(id)?.chart.drawings.update(peerId, {
|
|
1930
|
-
anchors: doc.anchors,
|
|
1931
|
-
style: doc.style,
|
|
1932
|
-
text: doc.text,
|
|
1933
|
-
props: doc.props
|
|
1934
|
-
});
|
|
1935
|
-
}
|
|
1936
|
-
} finally {
|
|
1937
|
-
this.drawingSyncBusy = false;
|
|
1938
|
-
}
|
|
1939
|
-
}
|
|
1940
|
-
/** Remove a linked drawing's peers with it. The removed member always leaves its
|
|
1941
|
-
* link group (whatever the setting); peers are only deleted while the link is on.
|
|
1942
|
-
* A propagated peer removal re-enters here under the busy guard and just cleans
|
|
1943
|
-
* its own link key. */
|
|
1944
|
-
propagateDrawingRemoval(originId, drawingId) {
|
|
1945
|
-
if (this.destroyed) return;
|
|
1946
|
-
const key = `${originId}\0${drawingId}`;
|
|
1947
|
-
const group = this.drawingLinks.get(key);
|
|
1948
|
-
if (!group) return;
|
|
1949
|
-
this.drawingLinks.delete(key);
|
|
1950
|
-
group.delete(originId);
|
|
1951
|
-
if (this.drawingSyncBusy) return;
|
|
1952
|
-
const setting = this.syncOpts.drawings;
|
|
1953
|
-
if (!setting) return;
|
|
1954
|
-
this.drawingSyncBusy = true;
|
|
1955
|
-
try {
|
|
1956
|
-
for (const id of syncTargets(originId, setting, [...this.cellsById.keys()])) {
|
|
1957
|
-
const peerId = group.get(id);
|
|
1958
|
-
if (peerId != null) this.cellsById.get(id)?.chart.drawings.remove(peerId);
|
|
1959
|
-
}
|
|
1960
|
-
} finally {
|
|
1961
|
-
this.drawingSyncBusy = false;
|
|
1962
|
-
}
|
|
1963
|
-
}
|
|
1964
|
-
/**
|
|
1965
|
-
* Push an origin cell's symbol/timeframe onto its same-group followers (fired from
|
|
1966
|
-
* `market:changed`). Convergence comes from IDEMPOTENCE, not the guard: a follower's
|
|
1967
|
-
* own (async) `market:changed` propagates back, but every peer already carries the
|
|
1968
|
-
* value, so the cell setters no-op and the wave dies.
|
|
1969
|
-
*/
|
|
1970
|
-
propagateMarket(originId) {
|
|
1971
|
-
if (this.syncBusy || this.destroyed) return;
|
|
1972
|
-
const origin = this.cellsById.get(originId);
|
|
1973
|
-
if (!origin) return;
|
|
1974
|
-
const ids = [...this.cellsById.keys()];
|
|
1975
|
-
this.syncBusy = true;
|
|
1976
|
-
try {
|
|
1977
|
-
if (origin.symbol) {
|
|
1978
|
-
for (const id of syncTargets(originId, this.syncOpts.symbol, ids)) this.cellsById.get(id)?.setSymbol(origin.symbol);
|
|
1979
|
-
}
|
|
1980
|
-
for (const id of syncTargets(originId, this.syncOpts.timeframe, ids)) this.cellsById.get(id)?.setTimeframe(origin.timeframe);
|
|
1981
|
-
} finally {
|
|
1982
|
-
this.syncBusy = false;
|
|
1983
|
-
}
|
|
1984
|
-
}
|
|
1985
|
-
/** Trigger ② — a cell's market changed: retention + sync always; chrome only if active. */
|
|
1986
|
-
onCellMarketChanged(id) {
|
|
1987
|
-
this.refreshRetention();
|
|
1988
|
-
this.propagateMarket(id);
|
|
1989
|
-
this.markStateDirty();
|
|
1990
|
-
if (id !== this.activeId) return;
|
|
1991
|
-
const cell = this.cellsById.get(id);
|
|
1992
|
-
if (!cell) return;
|
|
1993
|
-
this.topbar.setSymbol(cell.symbol);
|
|
1994
|
-
this.topbar.setTimeframe(cell.timeframe);
|
|
1995
|
-
this.mobileBar?.setSymbol(cell.symbol);
|
|
1996
|
-
this.mobileBar?.setTimeframe(cell.timeframe);
|
|
1997
|
-
this.objectTree.setSymbol(cell.symbol);
|
|
1998
|
-
this.bottombar?.setSession({ session: cell.session, enabled: cell.sessionAvailable });
|
|
1999
|
-
}
|
|
2000
|
-
/** Trigger ② — a cell's price style changed: the topbar button/menu only if active.
|
|
2001
|
-
* Reads the cell back (not the requested style) so the button reflects what the
|
|
2002
|
-
* renderer actually applied. */
|
|
2003
|
-
onCellPriceStyleChanged(id) {
|
|
2004
|
-
this.markStateDirty();
|
|
2005
|
-
if (id !== this.activeId) return;
|
|
2006
|
-
const cell = this.cellsById.get(id);
|
|
2007
|
-
if (!cell) return;
|
|
2008
|
-
this.topbar.setPriceStyle(cell.priceStyle);
|
|
2009
|
-
}
|
|
2010
|
-
/** Trigger ② — a cell's indicator ledger changed: count + picker only if active. */
|
|
2011
|
-
onCellIndicatorsChanged(id) {
|
|
2012
|
-
this.markStateDirty();
|
|
2013
|
-
if (id !== this.activeId) return;
|
|
2014
|
-
const cell = this.cellsById.get(id);
|
|
2015
|
-
if (!cell) return;
|
|
2016
|
-
this.topbar.setIndicatorCount(cell.indicatorCount);
|
|
2017
|
-
this.indicatorPicker?.sync();
|
|
2018
|
-
}
|
|
2019
|
-
/** Timeframe changes routed from the topbar menu / quick entry (chip state follows). */
|
|
2020
|
-
setActiveTimeframe(tf) {
|
|
2021
|
-
this.bottombar?.setActiveRange(null);
|
|
2022
|
-
this.active.setTimeframe(tf);
|
|
2023
|
-
}
|
|
2024
|
-
/** Star/unstar a timeframe — a WORKSPACE preference (one chip row, whatever the
|
|
2025
|
-
* active cell); the topbar follows and the set persists with the document. */
|
|
2026
|
-
setTimeframeFavorite(tf, on) {
|
|
2027
|
-
if (on === this.tfFavs.includes(tf)) return;
|
|
2028
|
-
this.tfFavs = on ? [tf, ...this.tfFavs] : this.tfFavs.filter((f) => f !== tf);
|
|
2029
|
-
this.topbar.setTimeframeFavorites(this.tfFavs);
|
|
2030
|
-
this.markStateDirty();
|
|
2031
|
-
}
|
|
2032
|
-
/** The mode flipped (container resized across the breakpoint, or a coarse-pointer
|
|
2033
|
-
* change). Close the open surfaces — a desktop card must not linger over the
|
|
2034
|
-
* mobile chrome (and vice versa) — and re-present every cell's own chrome. */
|
|
2035
|
-
onLayoutModeChange(mode) {
|
|
2036
|
-
this.symbolPicker.close();
|
|
2037
|
-
this.indicatorPicker?.close();
|
|
2038
|
-
this.tfDrawer?.close();
|
|
2039
|
-
this.drawingsDrawer?.close();
|
|
2040
|
-
this.moreDrawer?.close();
|
|
2041
|
-
this.timezoneDrawer?.close();
|
|
2042
|
-
this.priceScaleDrawer?.close();
|
|
2043
|
-
this.alertsMenu?.destroy();
|
|
2044
|
-
this.alertsMenu = null;
|
|
2045
|
-
for (const cell of this.cellsById.values()) {
|
|
2046
|
-
cell.chart.renderer.closeDialogs();
|
|
2047
|
-
cell.chart.renderer.setLayoutMode(mode);
|
|
2048
|
-
}
|
|
2049
|
-
this.syncCellPresentation();
|
|
2050
|
-
}
|
|
2051
|
-
/** Push the layout-shape-dependent chrome onto every cell: multi-cell grids keep
|
|
2052
|
-
* the status line on one row (segments that don't fit hide), and on MOBILE their
|
|
2053
|
-
* legends' fold chip routes to the object tree instead of unfolding in place —
|
|
2054
|
-
* per-indicator controls live there (the legend rows have no room in a grid cell). */
|
|
2055
|
-
syncCellPresentation() {
|
|
2056
|
-
const multi = this.def.cells.length > 1;
|
|
2057
|
-
const overview = multi && this.layoutCtl.current === "mobile" ? () => this.dock.toggle("objects", true) : null;
|
|
2058
|
-
for (const cell of this.cellsById.values()) {
|
|
2059
|
-
cell.setStatuslineFit(multi);
|
|
2060
|
-
cell.chart.renderer.setLegendOverviewAction(overview);
|
|
2061
|
-
}
|
|
2062
|
-
}
|
|
2063
|
-
// ── mobile drawers (built on first open; every read is live and hits the ACTIVE cell) ──
|
|
2064
|
-
openTimeframeDrawer() {
|
|
2065
|
-
this.tfDrawer ?? (this.tfDrawer = new TimeframeDrawer({
|
|
2066
|
-
host: this.root,
|
|
2067
|
-
timeframes: this.opts.timeframes ?? DEFAULT_TIMEFRAMES,
|
|
2068
|
-
ranges: RANGE_PRESETS,
|
|
2069
|
-
currentTimeframe: () => this.active.timeframe,
|
|
2070
|
-
activeRange: () => this.active.activeRangeId,
|
|
2071
|
-
onTimeframe: (tf) => this.setActiveTimeframe(tf),
|
|
2072
|
-
onRange: (preset) => {
|
|
2073
|
-
this.active.applyRange(preset);
|
|
2074
|
-
this.bottombar?.setActiveRange(preset.id);
|
|
2075
|
-
},
|
|
2076
|
-
onOpenChange: (open) => this.trackDialog(open)
|
|
2077
|
-
}));
|
|
2078
|
-
this.tfDrawer.open();
|
|
2079
|
-
}
|
|
2080
|
-
openDrawingsDrawer() {
|
|
2081
|
-
this.drawingsDrawer ?? (this.drawingsDrawer = new DrawingsDrawer({
|
|
2082
|
-
host: this.root,
|
|
2083
|
-
toolbar: () => defaultToolbar(),
|
|
2084
|
-
// the shared static toolbar's definition (see constructor)
|
|
2085
|
-
currentTool: () => this.active.chart.drawings.getTool(),
|
|
2086
|
-
isFavorite: (type) => this.active.chart.drawings.isFavorite(type),
|
|
2087
|
-
onFavorite: (type, on) => this.active.chart.drawings.setFavorite(type, on),
|
|
2088
|
-
onSelect: (type) => this.active.chart.drawings.setTool(type),
|
|
2089
|
-
onOpenChange: (open) => this.trackDialog(open)
|
|
2090
|
-
}));
|
|
2091
|
-
this.drawingsDrawer.open();
|
|
2092
|
-
}
|
|
2093
|
-
openMoreDrawer() {
|
|
2094
|
-
this.moreDrawer ?? (this.moreDrawer = new MoreDrawer({
|
|
2095
|
-
host: this.root,
|
|
2096
|
-
onUndo: () => this.active.history.undo(),
|
|
2097
|
-
onRedo: () => this.active.history.redo(),
|
|
2098
|
-
onScreenshot: () => this.active.downloadScreenshot(),
|
|
2099
|
-
canUndo: () => this.active.history.canUndo,
|
|
2100
|
-
canRedo: () => this.active.history.canRedo,
|
|
2101
|
-
priceStyles: () => priceStyleIds().map((id) => ({ id, label: priceStyleLabel(id), icon: priceStyleIcon(id) })),
|
|
2102
|
-
priceStyle: () => this.active.priceStyle,
|
|
2103
|
-
onPriceStyle: (id) => this.active.setPriceStyle(id),
|
|
2104
|
-
panels: () => [...this.dock.list()],
|
|
2105
|
-
onTogglePanel: (id) => this.dock.toggle(id),
|
|
2106
|
-
alerts: () => this.alerts.map((a) => ({ title: `[${a.cellId} \xB7 ${a.symbol}] ${a.title}`, message: a.message, time: a.time })),
|
|
2107
|
-
// Left-aligned actions have their own bottom-bar stop — only the rest
|
|
2108
|
-
// lands in the drawer, or every left action would appear twice.
|
|
2109
|
-
actions: () => widgetActions("topbar", this.context()).filter((a) => a.align !== "left").map((a) => ({ label: a.label, icon: a.icon, run: () => a.run(this.context()) })),
|
|
2110
|
-
// The desktop layout dropdown's whole surface — the grid canvas, the
|
|
2111
|
-
// non-canvas presets and the sync switches — relocated into the kebab
|
|
2112
|
-
// drawer (the topbar is hidden on mobile). Same reads as the topbar block.
|
|
2113
|
-
layout: {
|
|
2114
|
-
shape: () => layoutShape(this.def),
|
|
2115
|
-
presets: () => layouts().filter((l) => layoutShape(l) === null).map((l) => ({ id: l.id, label: l.label, checked: l.id === this.def.id })),
|
|
2116
|
-
onSelectGrid: (rows, cols) => this.setLayout(layoutForGrid(rows, cols)),
|
|
2117
|
-
onSelectPreset: (id) => this.setLayout(id),
|
|
2118
|
-
syncs: () => [
|
|
2119
|
-
{ id: "symbol", label: "Symbol", checked: this.syncOpts.symbol === true },
|
|
2120
|
-
{ id: "timeframe", label: "Interval", checked: this.syncOpts.timeframe === true },
|
|
2121
|
-
{ id: "crosshair", label: "Crosshair", checked: this.syncOpts.crosshair === true }
|
|
2122
|
-
],
|
|
2123
|
-
onToggleSync: (id) => {
|
|
2124
|
-
const kind = id;
|
|
2125
|
-
this.sync.set(kind, this.syncOpts[kind] ? false : true);
|
|
2126
|
-
}
|
|
2127
|
-
},
|
|
2128
|
-
onOpenChange: (open) => this.trackDialog(open)
|
|
2129
|
-
}));
|
|
2130
|
-
this.moreDrawer.open();
|
|
2131
|
-
}
|
|
2132
|
-
openTimezoneDrawer() {
|
|
2133
|
-
this.timezoneDrawer ?? (this.timezoneDrawer = new TimezoneDrawer({
|
|
2134
|
-
host: this.root,
|
|
2135
|
-
timezone: () => this.timezone,
|
|
2136
|
-
onTimezone: (zone) => this.setTimezone(zone),
|
|
2137
|
-
onOpenChange: (open) => this.trackDialog(open)
|
|
2138
|
-
}));
|
|
2139
|
-
this.timezoneDrawer.open();
|
|
2140
|
-
}
|
|
2141
|
-
openPriceScaleDrawer(y) {
|
|
2142
|
-
this.priceScalePressY = y;
|
|
2143
|
-
this.priceScaleDrawer ?? (this.priceScaleDrawer = new PriceScaleDrawer({
|
|
2144
|
-
host: this.root,
|
|
2145
|
-
chart: () => this.activeId ? this.cellsById.get(this.activeId)?.chart ?? null : null,
|
|
2146
|
-
pressY: () => this.priceScalePressY,
|
|
2147
|
-
onOpenChange: (open) => this.trackDialog(open)
|
|
2148
|
-
}));
|
|
2149
|
-
this.priceScaleDrawer.open();
|
|
2150
|
-
}
|
|
2151
|
-
/** The aggregated alerts bell — entries carry their cell; selecting one activates it. */
|
|
2152
|
-
openAlertsMenu(anchor) {
|
|
2153
|
-
this.alertsMenu?.destroy();
|
|
2154
|
-
const items = this.alerts.length ? this.alerts.map((a, i) => ({
|
|
2155
|
-
id: String(i),
|
|
2156
|
-
label: `[${a.cellId} \xB7 ${a.symbol}] ${new Date(a.time).toLocaleTimeString()} \xB7 ${a.title}: ${a.message}`.slice(0, 80)
|
|
2157
|
-
})) : [{ id: "none", label: "No alerts yet", disabled: true }];
|
|
2158
|
-
this.alertsMenu = new Menu({
|
|
2159
|
-
host: this.root,
|
|
2160
|
-
items,
|
|
2161
|
-
onSelect: (id) => {
|
|
2162
|
-
const alert = this.alerts[Number(id)];
|
|
2163
|
-
if (alert && this.cellsById.has(alert.cellId)) this.setActiveCell(alert.cellId);
|
|
2164
|
-
}
|
|
2165
|
-
});
|
|
2166
|
-
const r = anchor.getBoundingClientRect();
|
|
2167
|
-
this.alertsMenu.openAt(r.left, r.bottom + 4);
|
|
2168
|
-
}
|
|
2169
|
-
/** Mount registered attachments not yet mounted on this workspace (idempotent per id). */
|
|
2170
|
-
mountAttachments() {
|
|
2171
|
-
for (const att of widgetAttachments()) {
|
|
2172
|
-
if (this.attachmentDisposers.has(att.id)) continue;
|
|
2173
|
-
try {
|
|
2174
|
-
this.attachmentDisposers.set(att.id, att.mount(this.context()));
|
|
2175
|
-
} catch (err) {
|
|
2176
|
-
console.warn(`[vela] workspace attachment "${att.id}" failed to mount:`, err);
|
|
2177
|
-
}
|
|
2178
|
-
}
|
|
2179
|
-
}
|
|
2180
|
-
/** The default shortcut set — every binding acts on the ACTIVE cell. */
|
|
2181
|
-
registerDefaultKeys() {
|
|
2182
|
-
this.keymap.register({ id: "chart.screenshot", keys: "mod+alt+s", label: "Download a chart screenshot", category: "Chart", run: () => this.active.downloadScreenshot() });
|
|
2183
|
-
this.keymap.register({ id: "chart.reset-view", keys: "alt+r", label: "Reset view (all history)", category: "Chart", run: () => this.active.chart.setVisibleRangePreset("ALL") });
|
|
2184
|
-
this.keymap.register({ id: "chart.toggle-log", keys: "alt+l", label: "Toggle logarithmic scale", category: "Chart", run: () => this.active.chart.renderer.set("logScale", !this.active.chart.renderer.get("logScale")) });
|
|
2185
|
-
this.keymap.register({
|
|
2186
|
-
id: "chart.toggle-percent",
|
|
2187
|
-
keys: "alt+p",
|
|
2188
|
-
label: "Toggle percent scale",
|
|
2189
|
-
category: "Chart",
|
|
2190
|
-
run: () => {
|
|
2191
|
-
const mode = this.active.chart.renderer.get("scaleMode");
|
|
2192
|
-
this.active.chart.renderer.set("scaleMode", mode === "percent" ? "price" : "percent");
|
|
2193
|
-
}
|
|
2194
|
-
});
|
|
2195
|
-
this.keymap.register({ id: "drawings.trendline", keys: "alt+t", label: "Arm the trend line tool", category: "Drawings", run: () => this.active.chart.drawings.setTool("trendline") });
|
|
2196
|
-
this.keymap.register({
|
|
2197
|
-
id: "drawings.hline-cursor",
|
|
2198
|
-
keys: "alt+h",
|
|
2199
|
-
label: "Horizontal line at the cursor price",
|
|
2200
|
-
category: "Drawings",
|
|
2201
|
-
run: () => {
|
|
2202
|
-
const c = this.active;
|
|
2203
|
-
if (c.lastCrossTime != null && c.lastCrossPrice != null) c.chart.drawings.add("hline", { anchors: [{ time: c.lastCrossTime, price: c.lastCrossPrice }] });
|
|
2204
|
-
}
|
|
2205
|
-
});
|
|
2206
|
-
this.keymap.register({
|
|
2207
|
-
id: "drawings.vline-cursor",
|
|
2208
|
-
keys: "alt+v",
|
|
2209
|
-
label: "Vertical line at the cursor time",
|
|
2210
|
-
category: "Drawings",
|
|
2211
|
-
run: () => {
|
|
2212
|
-
const c = this.active;
|
|
2213
|
-
if (c.lastCrossTime != null && c.lastCrossPrice != null) c.chart.drawings.add("vline", { anchors: [{ time: c.lastCrossTime, price: c.lastCrossPrice }] });
|
|
2214
|
-
}
|
|
2215
|
-
});
|
|
2216
|
-
this.keymap.register({ id: "history.undo", keys: ["mod+z"], label: "Undo (active chart)", category: "Edit", run: () => this.active.history.undo() });
|
|
2217
|
-
this.keymap.register({ id: "history.redo", keys: ["mod+y", "mod+shift+z"], label: "Redo (active chart)", category: "Edit", run: () => this.active.history.redo() });
|
|
2218
|
-
this.keymap.register({ id: "view.zoom-in", keys: "mod+arrowup", label: "Zoom in", category: "Chart", run: () => this.glider.zoom(ZOOM_IN) });
|
|
2219
|
-
this.keymap.register({ id: "view.zoom-out", keys: "mod+arrowdown", label: "Zoom out", category: "Chart", run: () => this.glider.zoom(ZOOM_OUT) });
|
|
2220
|
-
this.keymap.register({ id: "view.pan-left", keys: "mod+arrowleft", label: "Pan toward history", category: "Chart", run: () => this.active.chart.panBy(-PAN_FAST) });
|
|
2221
|
-
this.keymap.register({ id: "view.pan-right", keys: "mod+arrowright", label: "Pan toward now", category: "Chart", run: () => this.active.chart.panBy(PAN_FAST) });
|
|
2222
|
-
if (this.indicatorPicker) {
|
|
2223
|
-
this.keymap.register({ id: "indicators.open", keys: "/", label: "Open the indicator picker", category: "Indicators", run: () => this.indicatorPicker?.open() });
|
|
2224
|
-
}
|
|
2225
|
-
this.keymap.register({
|
|
2226
|
-
id: "help.shortcuts",
|
|
2227
|
-
keys: "?",
|
|
2228
|
-
label: "Show this shortcuts panel",
|
|
2229
|
-
category: "Help",
|
|
2230
|
-
run: () => {
|
|
2231
|
-
this.shortcutsHelp ?? (this.shortcutsHelp = new ShortcutsHelp(this.keymap, this.root, (open) => this.trackDialog(open)));
|
|
2232
|
-
this.shortcutsHelp.open();
|
|
2233
|
-
}
|
|
2234
|
-
});
|
|
2235
|
-
}
|
|
2236
|
-
/** Bare-typing router: letters → symbol search (seeded), digits → timeframe entry. */
|
|
2237
|
-
routeTyping(ev) {
|
|
2238
|
-
if (this.destroyed || this.openDialogs > 0) return;
|
|
2239
|
-
if (ev.ctrlKey || ev.metaKey || ev.altKey) return;
|
|
2240
|
-
if (isEditableTarget(ev)) return;
|
|
2241
|
-
const key = ev.key;
|
|
2242
|
-
if (/^[a-zA-Z]$/.test(key)) {
|
|
2243
|
-
ev.preventDefault();
|
|
2244
|
-
this.symbolPicker.open(key.toUpperCase());
|
|
2245
|
-
} else if (/^[0-9]$/.test(key)) {
|
|
2246
|
-
ev.preventDefault();
|
|
2247
|
-
this.tfQuick.open(key);
|
|
2248
|
-
}
|
|
2249
|
-
}
|
|
2250
|
-
trackDialog(open) {
|
|
2251
|
-
this.openDialogs = Math.max(0, this.openDialogs + (open ? 1 : -1));
|
|
2252
|
-
if (open) this.keymap.pushScope("dialog");
|
|
2253
|
-
else this.keymap.popScope("dialog");
|
|
2254
|
-
}
|
|
2255
|
-
/** Pool a dehydrated slot state (bounded — oldest entries drop past the cap). */
|
|
2256
|
-
poolSet(id, state) {
|
|
2257
|
-
this.pool.delete(id);
|
|
2258
|
-
this.pool.set(id, state);
|
|
2259
|
-
if (this.pool.size > POOL_CAP) {
|
|
2260
|
-
const oldest = this.pool.keys().next().value;
|
|
2261
|
-
if (oldest != null) this.pool.delete(oldest);
|
|
2262
|
-
}
|
|
2263
|
-
}
|
|
2264
|
-
/**
|
|
2265
|
-
* Declare every live cell's symbol to the shared bar cache so one cell's load never
|
|
2266
|
-
* evicts the others' history (multi-symbol retention). Keys are CANONICAL tickers —
|
|
2267
|
-
* resolved through the registry when its indexes are ready, raw until then (refreshed
|
|
2268
|
-
* again on `feed.ready()`).
|
|
2269
|
-
*/
|
|
2270
|
-
refreshRetention() {
|
|
2271
|
-
const symbols = /* @__PURE__ */ new Set();
|
|
2272
|
-
for (const cell of this.cellsById.values()) {
|
|
2273
|
-
const raw = cell.symbol;
|
|
2274
|
-
if (!raw) continue;
|
|
2275
|
-
symbols.add(this.feed.resolveSymbol(raw)?.ticker ?? raw);
|
|
2276
|
-
}
|
|
2277
|
-
sharedBarStore.retain(symbols);
|
|
2278
|
-
}
|
|
2279
|
-
};
|
|
2280
|
-
|
|
2281
|
-
export { ChartCell, GRID_PICKER_MAX, VelaWorkspace, activeAfterLayout, ensureLayout, evenTracks, gridStyles, layoutDefinition, layoutForGrid, layoutShape, layouts, memoryStorageAdapter, rangesWithin, registerBuiltinLayouts, registerLayout, resizeTracks, syncTargets, trackOffsets, unregisterLayout };
|
|
1
|
+
export { ChartCell, GRID_PICKER_MAX, VelaWorkspace, activeAfterLayout, decodeState, encodeState, ensureLayout, evenTracks, gridStyles, layoutDefinition, layoutForGrid, layoutShape, layouts, memoryStorageAdapter, rangesWithin, registerBuiltinLayouts, registerLayout, resizeTracks, sanitizeState, syncTargets, trackOffsets, unregisterLayout } from './chunk-TMXK2SJR.js';
|
|
2
|
+
import './chunk-JQFO2WMU.js';
|
|
3
|
+
import './chunk-OEUGUXL7.js';
|
|
4
|
+
import './chunk-2R7BANEM.js';
|
|
5
|
+
import './chunk-OICPLNU7.js';
|
|
6
|
+
import './chunk-PN5KWFZ4.js';
|