@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.
Files changed (41) hide show
  1. package/README.md +16 -16
  2. package/dist/{DataProvider-DrN7ZIou.d.cts → DataProvider-DDUYw2qP.d.cts} +1 -1
  3. package/dist/{DataProvider-BmHkcwvJ.d.ts → DataProvider-JJq7M2it.d.ts} +1 -1
  4. package/dist/{history-CvOtsjT5.d.ts → bottombar-COnL2Sk4.d.ts} +110 -127
  5. package/dist/{history-BmyjaVir.d.cts → bottombar-DeDXbF_K.d.cts} +110 -127
  6. package/dist/{chunk-6FA4TDUF.js → chunk-JQFO2WMU.js} +2666 -2333
  7. package/dist/{chunk-BEJ57HP4.js → chunk-OEUGUXL7.js} +14 -1
  8. package/dist/{chunk-3QX6QOCX.js → chunk-TMXK2SJR.js} +5852 -3340
  9. package/dist/{contributions-2bEmCF9S.d.cts → contributions-BqCIsbaP.d.ts} +108 -4
  10. package/dist/{contributions-DatoAzMx.d.ts → contributions-DNMqrAuw.d.cts} +108 -4
  11. package/dist/index.cjs +2681 -2332
  12. package/dist/index.d.cts +65 -14
  13. package/dist/index.d.ts +65 -14
  14. package/dist/index.js +2 -2
  15. package/dist/{options-YdiaaVjt.d.cts → options-BaVTMXaO.d.cts} +48 -2
  16. package/dist/{options-YdiaaVjt.d.ts → options-BaVTMXaO.d.ts} +48 -2
  17. package/dist/{plugin-CkiStRaI.d.cts → plugin-CN8U2__Q.d.cts} +11 -5
  18. package/dist/{plugin-BeCMF1VQ.d.ts → plugin-WnvNNJOp.d.ts} +11 -5
  19. package/dist/plugin.cjs +16 -0
  20. package/dist/plugin.d.cts +4 -4
  21. package/dist/plugin.d.ts +4 -4
  22. package/dist/plugin.js +1 -1
  23. package/dist/providers/binance.d.cts +2 -2
  24. package/dist/providers/binance.d.ts +2 -2
  25. package/dist/providers/coinbase.d.cts +2 -2
  26. package/dist/providers/coinbase.d.ts +2 -2
  27. package/dist/providers/hyperliquid.d.cts +2 -2
  28. package/dist/providers/hyperliquid.d.ts +2 -2
  29. package/dist/ui.d.cts +1 -1
  30. package/dist/ui.d.ts +1 -1
  31. package/dist/vela.global.js +2661 -2312
  32. package/dist/vela.global.min.js +48 -48
  33. package/dist/widget.cjs +35658 -34074
  34. package/dist/widget.d.cts +39 -271
  35. package/dist/widget.d.ts +39 -271
  36. package/dist/widget.js +60 -1313
  37. package/dist/workspace.cjs +1584 -960
  38. package/dist/workspace.d.cts +111 -15
  39. package/dist/workspace.d.ts +111 -15
  40. package/dist/workspace.js +6 -2281
  41. package/package.json +1 -1
package/dist/widget.js CHANGED
@@ -1,1351 +1,98 @@
1
- import { SessionShadingTracker, Glider, WidgetHistory, localStorageAdapter, decodeState, prefixedSymbol, normalizeSession, SymbolPicker, IndicatorPicker, TimeframeQuick, Topbar, PanelDock, ObjectTree, DataWindow, ChartContextMenu, Toast, Watermark, Statusline, MarketStatusTracker, Bottombar, MobileBar, DrawingPill, LayoutModeController, ZOOM_IN, ZOOM_OUT, PAN_FAST, ShortcutsHelp, TimeframeDrawer, RANGE_PRESETS, DrawingsDrawer, MoreDrawer, priceStyleIcon, priceStyleLabel, TimezoneDrawer, PriceScaleDrawer, timeframeMs, indicatorLedger, sanitizeState, legacyWidgetState, parsePersisted, statuslineInkOf, toolShortcutHints, resolveIndicators, encodeState } from './chunk-3QX6QOCX.js';
2
- export { Bottombar, ChartContextMenu, DataWindow, IndicatorPicker, ObjectTree, PanelDock, RANGE_PRESETS, ShortcutsHelp, Statusline, SymbolPicker, TimeframeQuick, Topbar, Watermark, dataWindowSections, decimalsFor, decodeState, encodeState, filterSymbols, fmtChange, fmtPrice, legacyWidgetState, loadPersisted, localStorageAdapter, parsePersisted, parseTimeframe, priceStyleLabel, resolveIndicators, sanitizeState, savePersisted, timeframeLabel, timeframeMs } from './chunk-3QX6QOCX.js';
3
- import { registerBuiltinChartTypes, resolveTheme, parseSymbol, buildToolbar, priceStyleIds, Vela, normalizeTimezone } from './chunk-6FA4TDUF.js';
4
- export { TIMEZONES, normalizeTimezone, tzButtonLabel, tzMenuLabel, tzOffset } from './chunk-6FA4TDUF.js';
5
- import { widgetActions, widgetAttachments, legendActionsProviderFor, resolveEngines } from './chunk-BEJ57HP4.js';
6
- export { DEFAULT_PANEL_MAX_WIDTH, DEFAULT_PANEL_MIN_WIDTH, DEFAULT_PANEL_ORDER, DEFAULT_PANEL_WIDTH, SidePanel, clampPanelWidth, registerSidePanel, registerWidgetAction, registerWidgetAttachment, sidePanels, unregisterSidePanel, unregisterWidgetAction, unregisterWidgetAttachment, widgetActions, widgetAttachments } from './chunk-BEJ57HP4.js';
7
- import { KeymapManager, isEditableTarget } from './chunk-2R7BANEM.js';
8
- import { ensureUIHost, Menu, applyPlotOverlayTokens } from './chunk-OICPLNU7.js';
9
- import { injectStyles } from './chunk-PN5KWFZ4.js';
10
-
11
- // src/widget/url-state.ts
12
- var PARAMS = [
13
- ["symbol", "symbol"],
14
- ["timeframe", "interval"],
15
- ["priceStyle", "style"],
16
- ["timezone", "tz"],
17
- ["bars", "bars"],
18
- ["session", "session"]
19
- ];
20
- function readUrlState(search) {
21
- const out = {};
22
- try {
23
- const q = new URLSearchParams(search);
24
- for (const [key, param] of PARAMS) {
25
- const v = q.get(param);
26
- if (v) out[key] = v;
27
- }
28
- } catch {
29
- }
30
- return out;
31
- }
32
- function writeUrlState(state) {
33
- if (typeof history === "undefined" || typeof location === "undefined") return;
34
- try {
35
- const q = new URLSearchParams(location.search);
36
- for (const [key, param] of PARAMS) {
37
- const v = state[key];
38
- if (v) q.set(param, v);
39
- else q.delete(param);
40
- }
41
- const qs = q.toString();
42
- history.replaceState(null, "", `${location.pathname}${qs ? `?${qs}` : ""}${location.hash}`);
43
- } catch {
44
- }
45
- }
1
+ import { VelaWorkspace } from './chunk-TMXK2SJR.js';
2
+ export { Bottombar, ChartContextMenu, DataWindow, IndicatorPicker, ObjectTree, PanelDock, RANGE_PRESETS, ShortcutsHelp, Statusline, SymbolPicker, TimeframeQuick, Topbar, Watermark, dataWindowSections, decimalsFor, decodeState, encodeState, filterSymbols, fmtChange, fmtPrice, localStorageAdapter, parseTimeframe, priceStyleLabel, resolveIndicators, sanitizeState, timeframeLabel, timeframeMs } from './chunk-TMXK2SJR.js';
3
+ export { TIMEZONES, normalizeTimezone, tzButtonLabel, tzMenuLabel, tzOffset } from './chunk-JQFO2WMU.js';
4
+ export { DEFAULT_PANEL_MAX_WIDTH, DEFAULT_PANEL_MIN_WIDTH, DEFAULT_PANEL_ORDER, DEFAULT_PANEL_WIDTH, SidePanel, clampPanelWidth, registerSidePanel, registerStatePersistence, registerWidgetAction, registerWidgetAttachment, sidePanels, statePersistenceHandlers, unregisterSidePanel, unregisterStatePersistence, unregisterWidgetAction, unregisterWidgetAttachment, widgetActions, widgetAttachments } from './chunk-OEUGUXL7.js';
5
+ import './chunk-2R7BANEM.js';
6
+ import './chunk-OICPLNU7.js';
7
+ import './chunk-PN5KWFZ4.js';
46
8
 
47
9
  // src/widget/VelaWidget.ts
48
- var DEFAULT_TIMEFRAMES = ["1", "5", "15", "60", "240", "D", "W"];
49
- var WIDGET_STYLE_ID = "vela-widget";
50
- var WIDGET_CSS = `
51
- .vela-widget { position: relative; display: flex; flex-direction: column; width: 100%; height: 100%; background: var(--vela-bg); }
52
- .vela-widget-main { position: relative; display: flex; flex-direction: row; flex: 1 1 auto; min-height: 0; }
53
- .vela-widget-chart { position: relative; flex: 1 1 auto; min-width: 0; }
54
- `;
10
+ var deprecationWarned = false;
55
11
  var VelaWidget = class {
56
12
  constructor(container, opts) {
57
- /** Keeps the pre/post-market shading on the symbol's real calendar (see {@link SessionShadingTracker}). */
58
- this.sessionShading = new SessionShadingTracker((zones) => this.inner?.renderer.set("sessionZones", zones));
59
- /** Whether the ACTIVE symbol declares real trading sessions (RTH/ETH meaningful) —
60
- * gates the bottombar toggle and the settings dialog's Trading session group. */
61
- this.sessionsAvailable = false;
62
- this.shortcutsHelp = null;
63
- this.inner = null;
64
- /** The manifest library (loaded once). */
65
- this.manifest = [];
66
- /** Live instances — the SAME entry may be added several times. */
67
- this.instances = [];
68
- /** Native-indicator catalog of the CURRENT chart (refreshed per rebuild/change). */
69
- this.nativeCatalog = [];
70
- /** Present native types, mirrored SYNC from the chart (see syncPresentNatives). */
71
- this.presentNatives = [];
72
- this.openDialogs = 0;
73
- this.onRootKeydown = (ev) => this.routeTyping(ev);
74
- /** Indicator titles (the in-chart legend rows) shown — reapplied across rebuilds. */
75
- this.indicatorTitlesOn = true;
76
- /** Plot values beside the legend titles shown — reapplied across rebuilds. */
77
- this.indicatorValuesOn = true;
78
- this.pendingRange = null;
79
- /** Symbol already reported as unservable (the core re-reports on every index settle). */
80
- this.unresolvedToasted = null;
81
- /** Extra fetch depth the ACTIVE range chip needs — a view concern, kept apart from the
82
- * user's own `bars` setting so a preset never overwrites their preference. */
83
- this.rangeBars = 0;
84
- this.toast = null;
85
- this.alerts = [];
86
- this.alertsMenu = null;
87
- this.glider = new Glider(() => this.inner);
88
- /** Unified app+drawings undo timeline (Ctrl+Z / Ctrl+Y). Late-resolves the CURRENT
89
- * inner chart so steps recorded before a rebuild never act on a destroyed instance. */
90
- this.history = new WidgetHistory(() => this.inner);
91
- this.lastCrossPrice = null;
92
- this.lastCrossTime = null;
93
- /** Renderer cosmetic template carried across rebuilds (and persisted when enabled). */
94
- this.savedConfig = null;
95
- /** User-drawings document carried across rebuilds (and persisted when enabled). */
96
- this.savedDrawings = null;
97
- /** A restored indicator ledger waiting for the manifest to resolve. */
98
- this.pendingIndicators = null;
99
- /** Volume presence decided by a RESTORED ledger (null = follow the option). */
100
- this.ledgerVolume = null;
101
- /** The manifest can no longer change the instance set: it resolved, or no `indicators`
102
- * option exists so nothing will ever resolve. Gates the ledger's pending fallback —
103
- * once settled, a live empty set means "the user removed everything" and persists so. */
104
- this.manifestSettled = false;
105
- /** The volume auto-add rides the first candles (`load:end`); until then the registry
106
- * can't show it and the persisted ledger reports the INTENT instead. */
107
- this.volumeMayBePending = true;
108
- /** True when the boot state came from the LEGACY three-key layout — the first
109
- * unified save then drops the old `:config`/`:drawings` sub-keys. */
110
- this.legacyKeys = false;
111
- this.stateTimer = null;
112
- this.stateListeners = /* @__PURE__ */ new Set();
113
- /** Favorite drawing-tool types — mirrored from chart events, reapplied on rebuilds. */
114
- this.favs = [];
115
- /** Favorite timeframes — the topbar's quick-switch chips, persisted with the shell. */
116
- this.tfFavs = [];
117
- /** Mounted attachment disposers (by id), torn down at destroy. */
118
- this.attachmentDisposers = /* @__PURE__ */ new Map();
119
- this.onUnload = () => this.persistNow();
120
- this.indicatorsPromise = null;
121
- this.destroyed = false;
122
- this.buildSeq = 0;
123
- this.tfDrawer = null;
124
- this.drawingsDrawer = null;
125
- this.moreDrawer = null;
126
- this.timezoneDrawer = null;
127
- this.priceScaleDrawer = null;
128
- /** Plot-local y of the last price-axis long-press (targets the pane under the finger). */
129
- this.priceScalePressY = 0;
130
- /** The active range chip id (both bottom bars + the timeframe drawer mirror it). */
131
- this.activeRangeId = null;
132
- const hostEl = typeof container === "string" ? document.querySelector(container) : container;
133
- if (!hostEl) throw new Error(`VelaWidget: container not found: ${String(container)}`);
134
- registerBuiltinChartTypes();
135
- this.opts = opts;
136
- this.storageKey = opts.persist === void 0 || opts.persist === false ? null : opts.persist === true ? "vela-widget" : opts.persist;
137
- this.storage = opts.storage ?? localStorageAdapter();
138
- let boot = null;
139
- if (this.storageKey !== null) {
140
- const raw = this.storage.get(this.storageKey);
141
- if (raw instanceof Promise) void this.bootAsync(raw);
142
- else {
143
- boot = raw != null ? decodeState(raw) : null;
144
- if (!boot) boot = this.bootLegacySync(raw);
145
- }
146
- window.addEventListener("beforeunload", this.onUnload);
13
+ if (!deprecationWarned) {
14
+ deprecationWarned = true;
15
+ console.warn(
16
+ "[vela] VelaWidget is deprecated and will be removed in a future release. Use `new VelaWorkspace(container, { ...options, layout: false })` \u2014 pass `persist: 'vela-widget'` to keep the same stored state. " + (opts.urlState ? "The `urlState` option is no longer supported and was ignored." : "")
17
+ );
147
18
  }
148
- const bootCell = boot ? boot.charts.find((c) => c.id === "c1") ?? boot.charts[0] : void 0;
149
- const fromUrl = opts.urlState ? readUrlState(typeof location !== "undefined" ? location.search : "") : {};
150
- this.symbol = fromUrl.symbol ?? prefixedSymbol(bootCell) ?? opts.symbol ?? "";
151
- this.timeframe = fromUrl.timeframe ?? bootCell?.timeframe ?? opts.timeframe ?? "60";
152
- this.priceStyle = fromUrl.priceStyle ?? bootCell?.priceStyle ?? opts.priceStyle ?? "candles";
153
- this.timezone = fromUrl.timezone ?? boot?.timezone ?? opts.timezone ?? "Etc/UTC";
154
- this.bars = Number(fromUrl.bars ?? bootCell?.bars ?? opts.bars ?? 1e3);
155
- this.session = normalizeSession(fromUrl.session ?? bootCell?.session ?? opts.session);
156
- this.watermarkOn = bootCell?.watermark !== void 0 ? bootCell.watermark : opts.watermark !== false;
157
- this.indicatorTitlesOn = bootCell?.indicatorTitles ?? true;
158
- this.indicatorValuesOn = bootCell?.indicatorValues ?? true;
159
- this.favs = boot?.favorites ? [...boot.favorites] : [];
160
- this.tfFavs = boot?.timeframeFavorites ? [...boot.timeframeFavorites] : [];
161
- this.savedConfig = bootCell?.rendererConfig ?? null;
162
- this.savedDrawings = bootCell?.drawings ?? null;
163
- this.pendingIndicators = bootCell?.indicators ?? null;
164
- this.ledgerVolume = bootCell?.indicators ? bootCell.indicators.natives.includes("volume") : null;
165
- this.manifestSettled = opts.indicators === void 0;
166
- const doc = hostEl.ownerDocument;
167
- injectStyles(WIDGET_STYLE_ID, WIDGET_CSS, doc);
168
- this.root = doc.createElement("div");
169
- this.root.className = "vela-widget";
170
- ensureUIHost(this.root, resolveTheme(opts.theme));
171
- this.symbolPicker = new SymbolPicker({
172
- host: this.root,
173
- onSelect: (symbol) => this.setSymbol(symbol),
174
- onOpenChange: (open) => {
175
- if (open) this.inner?.renderer.closeDialogs();
176
- this.trackDialog(open);
177
- }
178
- });
179
- this.indicatorPicker = opts.indicatorPicker !== false ? new IndicatorPicker({
180
- host: this.root,
181
- library: () => [
182
- ...this.nativeCatalog.filter((n) => n.supported).map((n) => ({ name: n.title, category: "Vela", native: true, nativeType: n.type, beta: n.beta })),
183
- ...this.manifest.map((e) => ({ name: e.name, language: e.language, category: e.category }))
184
- ],
185
- onChart: () => [
186
- ...this.nativeCatalog.filter((n) => n.present).map((n) => ({ name: n.title, native: true, nativeType: n.type })),
187
- ...this.instances.map((it) => ({ name: it.entry.name, language: it.entry.language }))
188
- ],
189
- onAdd: (i) => {
190
- const natives = this.nativeCatalog.filter((n) => n.supported);
191
- if (i < natives.length) this.addNative(natives[i].type);
192
- else this.addInstance(i - natives.length);
193
- },
194
- onRemove: (i) => {
195
- const present = this.nativeCatalog.filter((n) => n.present);
196
- if (i < present.length) this.removeNative(present[i].type);
197
- else this.removeInstance(i - present.length);
198
- },
199
- onOpenChange: (open) => {
200
- if (open) this.inner?.renderer.closeDialogs();
201
- this.trackDialog(open);
202
- }
203
- }) : null;
204
- this.tfQuick = new TimeframeQuick({
205
- host: this.root,
206
- onApply: (tf) => this.setTimeframe(tf),
207
- onOpenChange: (open) => this.trackDialog(open)
208
- });
209
- const picker = this.indicatorPicker;
210
- this.topbar = new Topbar(this.root, {
211
- symbol: this.symbol,
212
- onSymbolClick: () => this.symbolPicker.open(),
213
- ...picker ? { onIndicatorsClick: () => picker.open() } : {},
214
- onUndoClick: () => this.history.undo(),
215
- onRedoClick: () => this.history.redo(),
216
- onScreenshotClick: () => this.downloadScreenshot(),
217
- onAlertsClick: (anchor) => this.openAlertsMenu(anchor),
218
- timeframe: this.timeframe,
219
- timeframes: opts.timeframes ?? DEFAULT_TIMEFRAMES,
220
- timeframeFavorites: this.tfFavs,
221
- priceStyle: this.priceStyle,
222
- onTimeframe: (tf) => this.setTimeframe(tf),
223
- onTimeframeFavorite: (tf, on) => this.setTimeframeFavorite(tf, on),
224
- onPriceStyle: (style) => this.setPriceStyle(style),
225
- getContext: () => this.context()
226
- });
227
- this.history.onChange(() => this.topbar.setHistoryState(this.history.canUndo, this.history.canRedo));
228
- const main = doc.createElement("div");
229
- main.className = "vela-widget-main";
230
- this.chartHost = doc.createElement("div");
231
- this.chartHost.className = "vela-widget-chart";
232
- main.appendChild(this.chartHost);
233
- this.dock = new PanelDock(main, {
234
- chrome: this.topbar,
235
- context: () => this.context(),
236
- changed: () => this.markStateDirty()
237
- });
238
- this.objectTree = new ObjectTree(main);
239
- this.dataWindow = new DataWindow(main);
240
- this.dock.addBuiltIn({ id: "dataWindow", title: "Data window", icon: "datawindow", order: 10, panel: this.dataWindow, onChart: (c) => this.dataWindow.onChart(c) });
241
- this.dock.addBuiltIn({ id: "objects", title: "Object tree", icon: "objects", order: 20, panel: this.objectTree, onChart: (c) => this.objectTree.onChart(c) });
242
- this.dock.refresh();
243
- this.root.appendChild(main);
244
- this.contextMenu = new ChartContextMenu(this.chartHost, {
245
- resetView: () => {
246
- this.inner?.renderer.set("autoScale", true);
247
- this.inner?.setVisibleRangePreset("ALL");
248
- },
249
- timezone: () => this.timezone,
250
- setTimezone: (zone) => this.setTimezone(zone),
251
- getContext: () => this.context()
252
- });
253
- this.toast = new Toast(this.chartHost);
254
- this.watermark = opts.watermark !== false ? new Watermark(this.chartHost, this.symbol, this.timeframe) : null;
255
- this.watermark?.setVisible(this.watermarkOn);
256
- this.statusline = opts.statusline !== false ? new Statusline(this.chartHost, this.symbol) : null;
257
- this.statusline?.setMeta(this.timeframe, parseSymbol(this.symbol).provider ?? "");
258
- this.marketStatus = this.statusline ? new MarketStatusTracker((s) => this.statusline?.setMarketStatus(s)) : null;
259
- this.bottombar = opts.bottombar !== false ? new Bottombar(this.root, {
260
- timezone: this.timezone,
261
- onRange: (preset) => this.applyRange(preset),
262
- onTimezone: (zone) => this.setTimezone(zone),
263
- onSession: (session) => this.setSession(session),
264
- onSettingsClick: () => this.inner?.renderer.openSettings()
265
- }) : null;
266
- this.mobileBar = opts.bottombar !== false ? new MobileBar(this.root, {
267
- symbol: this.symbol,
268
- timeframe: this.timeframe,
269
- onSymbolClick: () => this.symbolPicker.open(),
270
- onTimeframeClick: () => this.openTimeframeDrawer(),
271
- ...picker ? { onIndicatorsClick: () => picker.open() } : {},
272
- getContext: () => this.context(),
273
- onDrawingsClick: () => this.openDrawingsDrawer(),
274
- onMoreClick: () => this.openMoreDrawer(),
275
- onSettingsClick: () => this.inner?.renderer.openSettings()
276
- }) : null;
277
- this.drawingPill = new DrawingPill(this.chartHost);
278
- hostEl.appendChild(this.root);
279
- this.layoutCtl = new LayoutModeController(this.root, opts.layoutMode ?? "auto");
280
- this.layoutCtl.onChange((mode) => this.onLayoutModeChange(mode));
281
- this.keymap = new KeymapManager();
282
- this.keymap.attach(this.root);
283
- this.keymap.register({
284
- id: "chart.screenshot",
285
- keys: "mod+alt+s",
286
- label: "Download a chart screenshot",
287
- category: "Chart",
288
- run: () => this.downloadScreenshot()
289
- });
290
- this.keymap.register({ id: "chart.reset-view", keys: "alt+r", label: "Reset view (all history)", category: "Chart", run: () => this.inner?.setVisibleRangePreset("ALL") });
291
- this.keymap.register({ id: "chart.toggle-log", keys: "alt+l", label: "Toggle logarithmic scale", category: "Chart", run: () => this.inner?.renderer.set("logScale", !this.inner.renderer.get("logScale")) });
292
- this.keymap.register({
293
- id: "chart.toggle-percent",
294
- keys: "alt+p",
295
- label: "Toggle percent scale",
296
- category: "Chart",
297
- run: () => {
298
- const mode = this.inner?.renderer.get("scaleMode");
299
- this.inner?.renderer.set("scaleMode", mode === "percent" ? "price" : "percent");
300
- }
301
- });
302
- this.keymap.register({ id: "drawings.trendline", keys: "alt+t", label: "Arm the trend line tool", category: "Drawings", run: () => this.inner?.drawings.setTool("trendline") });
303
- this.keymap.register({
304
- id: "drawings.hline-cursor",
305
- keys: "alt+h",
306
- label: "Horizontal line at the cursor price",
307
- category: "Drawings",
308
- run: () => {
309
- if (this.lastCrossPrice != null && this.lastCrossTime != null)
310
- this.inner?.drawings.add("hline", { anchors: [{ time: this.lastCrossTime, price: this.lastCrossPrice }] });
311
- }
312
- });
313
- this.keymap.register({
314
- id: "drawings.vline-cursor",
315
- keys: "alt+v",
316
- label: "Vertical line at the cursor time",
317
- category: "Drawings",
318
- run: () => {
319
- if (this.lastCrossTime != null && this.lastCrossPrice != null)
320
- this.inner?.drawings.add("vline", { anchors: [{ time: this.lastCrossTime, price: this.lastCrossPrice }] });
321
- }
19
+ const { urlState: _urlState, height: _height, persist, ...rest } = opts;
20
+ this.ws = new VelaWorkspace(container, {
21
+ ...rest,
22
+ layout: false,
23
+ // The widget's historical storage key existing persisted state restores.
24
+ persist: persist === true ? "vela-widget" : persist
322
25
  });
323
- this.keymap.register({ id: "history.undo", keys: ["mod+z"], label: "Undo", category: "Edit", run: () => this.history.undo() });
324
- this.keymap.register({ id: "history.redo", keys: ["mod+y", "mod+shift+z"], label: "Redo", category: "Edit", run: () => this.history.redo() });
325
- this.keymap.register({ id: "view.zoom-in", keys: "mod+arrowup", label: "Zoom in", category: "Chart", run: () => this.glider.zoom(ZOOM_IN) });
326
- this.keymap.register({ id: "view.zoom-out", keys: "mod+arrowdown", label: "Zoom out", category: "Chart", run: () => this.glider.zoom(ZOOM_OUT) });
327
- this.keymap.register({ id: "view.pan-left", keys: "mod+arrowleft", label: "Pan toward history", category: "Chart", run: () => this.inner?.panBy(-PAN_FAST) });
328
- this.keymap.register({ id: "view.pan-right", keys: "mod+arrowright", label: "Pan toward now", category: "Chart", run: () => this.inner?.panBy(PAN_FAST) });
329
- if (picker) this.keymap.register({ id: "indicators.open", keys: "/", label: "Open the indicator picker", category: "Indicators", run: () => picker.open() });
330
- this.keymap.register({
331
- id: "help.shortcuts",
332
- keys: "?",
333
- label: "Show this shortcuts panel",
334
- category: "Help",
335
- run: () => {
336
- this.shortcutsHelp ?? (this.shortcutsHelp = new ShortcutsHelp(this.keymap, this.root, (open) => this.trackDialog(open)));
337
- this.shortcutsHelp.open();
338
- }
339
- });
340
- this.root.addEventListener("keydown", this.onRootKeydown);
341
- this.root.tabIndex = -1;
342
- this.rebuild();
343
- this.mountAttachments();
344
- if (opts.autofocus) this.inner?.renderer.focus();
345
- }
346
- /** The mode flipped (container resized across the breakpoint, or a coarse-pointer
347
- * change). Close the open surfaces — a desktop card must not linger over the
348
- * mobile chrome (and vice versa) — and re-present the renderer's own chrome. */
349
- onLayoutModeChange(mode) {
350
- this.symbolPicker.close();
351
- this.indicatorPicker?.close();
352
- this.tfDrawer?.close();
353
- this.drawingsDrawer?.close();
354
- this.moreDrawer?.close();
355
- this.timezoneDrawer?.close();
356
- this.priceScaleDrawer?.close();
357
- this.inner?.renderer.closeDialogs();
358
- this.inner?.renderer.setLayoutMode(mode);
359
- }
360
- // ── mobile drawers (built on first open; every list is read live at open time) ──
361
- openTimeframeDrawer() {
362
- this.tfDrawer ?? (this.tfDrawer = new TimeframeDrawer({
363
- host: this.root,
364
- timeframes: this.opts.timeframes ?? DEFAULT_TIMEFRAMES,
365
- ranges: RANGE_PRESETS,
366
- currentTimeframe: () => this.timeframe,
367
- activeRange: () => this.activeRangeId,
368
- onTimeframe: (tf) => this.setTimeframe(tf),
369
- onRange: (preset) => this.applyRange(preset),
370
- onOpenChange: (open) => this.trackDialog(open)
371
- }));
372
- this.tfDrawer.open();
373
- }
374
- openDrawingsDrawer() {
375
- this.drawingsDrawer ?? (this.drawingsDrawer = new DrawingsDrawer({
376
- host: this.root,
377
- toolbar: () => buildToolbar(this.opts.drawings).definition,
378
- currentTool: () => this.inner?.drawings.getTool() ?? null,
379
- isFavorite: (type) => this.inner?.drawings.isFavorite(type) ?? false,
380
- onFavorite: (type, on) => this.inner?.drawings.setFavorite(type, on),
381
- onSelect: (type) => this.inner?.drawings.setTool(type),
382
- onOpenChange: (open) => this.trackDialog(open)
383
- }));
384
- this.drawingsDrawer.open();
385
26
  }
386
- openMoreDrawer() {
387
- this.moreDrawer ?? (this.moreDrawer = new MoreDrawer({
388
- host: this.root,
389
- onUndo: () => this.history.undo(),
390
- onRedo: () => this.history.redo(),
391
- onScreenshot: () => this.downloadScreenshot(),
392
- canUndo: () => this.history.canUndo,
393
- canRedo: () => this.history.canRedo,
394
- priceStyles: () => priceStyleIds().map((id) => ({ id, label: priceStyleLabel(id), icon: priceStyleIcon(id) })),
395
- priceStyle: () => this.priceStyle,
396
- onPriceStyle: (id) => this.setPriceStyle(id),
397
- panels: () => [...this.dock.list()],
398
- onTogglePanel: (id) => this.dock.toggle(id),
399
- alerts: () => this.alerts,
400
- // Left-aligned actions have their own bottom-bar stop — only the rest
401
- // lands in the drawer, or every left action would appear twice.
402
- actions: () => widgetActions("topbar", this.context()).filter((a) => a.align !== "left").map((a) => ({ label: a.label, icon: a.icon, run: () => a.run(this.context()) })),
403
- onOpenChange: (open) => this.trackDialog(open)
404
- }));
405
- this.moreDrawer.open();
27
+ /** The shell root element (the workspace root; carries `vela-widget` as an alias). */
28
+ get root() {
29
+ this.ws.root.classList.add("vela-widget");
30
+ return this.ws.root;
406
31
  }
407
- openTimezoneDrawer() {
408
- this.timezoneDrawer ?? (this.timezoneDrawer = new TimezoneDrawer({
409
- host: this.root,
410
- timezone: () => this.timezone,
411
- onTimezone: (zone) => this.setTimezone(zone),
412
- onOpenChange: (open) => this.trackDialog(open)
413
- }));
414
- this.timezoneDrawer.open();
32
+ get keymap() {
33
+ return this.ws.keymap;
415
34
  }
416
- openPriceScaleDrawer(y) {
417
- this.priceScalePressY = y;
418
- this.priceScaleDrawer ?? (this.priceScaleDrawer = new PriceScaleDrawer({
419
- host: this.root,
420
- chart: () => this.inner,
421
- pressY: () => this.priceScalePressY,
422
- onOpenChange: (open) => this.trackDialog(open)
423
- }));
424
- this.priceScaleDrawer.open();
35
+ /** The chart's unified undo timeline — resolve it fresh, never cache it. */
36
+ get history() {
37
+ return this.ws.active.history;
425
38
  }
426
- /** Mount registered attachments not yet mounted on this widget (idempotent per id). */
427
- mountAttachments() {
428
- for (const att of widgetAttachments()) {
429
- if (this.attachmentDisposers.has(att.id)) continue;
430
- try {
431
- this.attachmentDisposers.set(att.id, att.mount(this.context()));
432
- } catch (err) {
433
- console.warn(`[vela] widget attachment "${att.id}" failed to mount:`, err);
434
- }
435
- }
39
+ /** The inner headless chart. */
40
+ get chart() {
41
+ return this.ws.chart;
436
42
  }
437
- /** The context handed to contributed actions (see `registerWidgetAction`). */
43
+ /** A fresh contribution context (the workspace's is a superset of the widget's). */
438
44
  context() {
439
- const self = this;
440
- return {
441
- get chart() {
442
- return self.chart;
443
- },
444
- symbol: this.symbol,
445
- timeframe: this.timeframe,
446
- priceStyle: this.priceStyle,
447
- setSymbol: (symbol) => this.setSymbol(symbol),
448
- setTimeframe: (tf) => this.setTimeframe(tf),
449
- setPriceStyle: (style) => this.setPriceStyle(style),
450
- openSymbolSearch: (query) => this.symbolPicker.open(query ?? ""),
451
- togglePanel: (id, open) => this.dock.toggle(id, open),
452
- host: this.root,
453
- toast: (message, kind) => this.toast?.show(message, kind)
454
- };
455
- }
456
- /** The recent engine alerts (topbar bell). */
457
- openAlertsMenu(anchor) {
458
- this.alertsMenu?.destroy();
459
- const items = this.alerts.length ? this.alerts.map((a, i) => ({
460
- id: String(i),
461
- label: `${new Date(a.time).toLocaleTimeString()} \xB7 ${a.title}: ${a.message}`.slice(0, 70)
462
- })) : [{ id: "none", label: "No alerts yet", disabled: true }];
463
- this.alertsMenu = new Menu({ host: this.root, items, onSelect: () => {
464
- } });
465
- const r = anchor.getBoundingClientRect();
466
- this.alertsMenu.openAt(r.left, r.bottom + 4);
45
+ return this.ws.context();
467
46
  }
468
- /** Re-project contributed topbar actions and side panels (after late registrations). */
469
47
  refreshActions() {
470
- this.mountAttachments();
471
- this.topbar.renderActions();
472
- this.mobileBar?.renderActions();
473
- this.dock.refresh();
474
- if (this.inner) this.inner.renderer.setLegendActions(legendActionsProviderFor(this.inner, () => this.context()));
475
- }
476
- /** The inner headless chart of the CURRENT build — becomes a new instance after a
477
- * symbol/timeframe change; don't cache it across awaits. */
478
- get chart() {
479
- if (!this.inner) throw new Error("VelaWidget is destroyed");
480
- return this.inner;
481
- }
482
- /** Chrome that must follow the timeframe wherever it changes from (menu, quick entry,
483
- * range chip, API). Keeping it in one place is what stopped the status line from
484
- * drifting out of sync with the topbar. */
485
- syncTimeframeChrome(tf) {
486
- this.timeframe = tf;
487
- this.topbar.setTimeframe(tf);
488
- this.mobileBar?.setTimeframe(tf);
489
- this.watermark?.update(this.symbol, tf);
490
- this.statusline?.setMeta(tf, this.providerLabel());
491
- }
492
- /**
493
- * The venue label to show: the LISTING prefix the resolved symbol's data declares
494
- * (`NASDAQ` for AAPL), else the provider that RESOLVED the symbol — never the one
495
- * configured or typed. They differ whenever the `provider` option is just a default,
496
- * the prefix was a listing venue, or the spelling isn't registered — the status line
497
- * must not claim a venue that served nothing.
498
- */
499
- providerLabel() {
500
- const display = this.inner?.data.displayPrefix(this.symbol);
501
- return display ?? parseSymbol(this.symbol).provider ?? "";
502
- }
503
- setTimeframe(tf) {
504
- if (tf === this.timeframe || this.destroyed) return;
505
- this.activeRangeId = null;
506
- this.bottombar?.setActiveRange(null);
507
- this.rangeBars = 0;
508
- this.syncTimeframeChrome(tf);
509
- this.markStateDirty();
510
- void this.inner?.setMarket({ timeframe: tf, bars: this.bars });
511
- }
512
- /**
513
- * Switch the shown trading session (RTH/ETH) in place — a full reload, like a
514
- * timeframe change (the two sessions are different bar series). No-op on a chart
515
- * already showing that session; meaningless on continuous markets (the toggle is
516
- * disabled there, but the API tolerates the call — the provider ignores the flag).
517
- */
518
- setSession(session) {
519
- if (session === this.session || session === "regular" && this.session === void 0 || this.destroyed) return;
520
- this.session = session;
521
- this.refreshSessionToggle();
522
- this.markStateDirty();
523
- void this.inner?.setMarket({ session });
524
- }
525
- /** Re-derive the session chrome from the ACTIVE symbol's metadata: the RTH/ETH
526
- * toggle shows (and the settings dialog's Trading session group appears) iff the
527
- * resolved symbol declares a real session vocabulary (not `24x7`); the pre/post
528
- * shading re-derives for the new market either way. */
529
- refreshSessionToggle() {
530
- const chart = this.inner;
531
- if (!chart) return;
532
- void chart.data.symbolInfo(this.symbol).then((si) => {
533
- if (this.destroyed || this.inner !== chart) return;
534
- const enabled = typeof si?.session === "string" && si.session !== "" && si.session !== "24x7";
535
- this.bottombar?.setSession({ session: this.session ?? "regular", enabled });
536
- if (enabled !== this.sessionsAvailable) {
537
- this.sessionsAvailable = enabled;
538
- this.pushSettingsSections();
539
- }
540
- this.refreshSessionShading();
541
- });
542
- }
543
- /** (Re)derive the pre/post-market shading bands for the current market. The loaded
544
- * depth (bars × timeframe) bounds the calendar fetch; the tracker clamps it. */
545
- refreshSessionShading() {
546
- const chart = this.inner;
547
- if (!chart) return;
548
- const lookbackMs = Math.max(this.bars, this.rangeBars) * timeframeMs(this.timeframe);
549
- this.sessionShading.track(chart.data, this.symbol, { session: this.session ?? "regular", lookbackMs });
48
+ this.ws.refreshActions();
550
49
  }
551
- /** The session-shade colors live in the renderer CONFIG (persisted with it, edited
552
- * live by the dialog swatch) — the widget only proxies them into its settings rows. */
553
- sessionShadeColor(key) {
554
- const cfg = this.inner?.renderer.getConfig();
555
- const v = cfg?.sessions?.[key];
556
- return typeof v === "string" ? v : "";
557
- }
558
- setSessionShadeColor(key, color) {
559
- this.inner?.renderer.applyConfig({ sessions: { [key]: color } });
560
- this.markStateDirty();
561
- }
562
- /**
563
- * (Re)contribute the widget's settings-dialog sections: status line parts, the fetch
564
- * depth, the watermark toggle, and — only while the active symbol HAS sessions — the
565
- * Trading session group (RTH/ETH switch + the pre/post-market shading colors) inside
566
- * the Symbol tab. Re-run whenever a gate changes (the dialog reads them on open).
567
- */
568
- pushSettingsSections() {
569
- const chart = this.inner;
570
- if (!chart) return;
571
- const rth = "Regular hours (RTH)";
572
- const eth = "Extended hours (ETH)";
573
- const sessionSection = {
574
- title: "Trading session",
575
- placement: "symbol",
576
- rows: [
577
- {
578
- kind: "select",
579
- label: "Session",
580
- options: [rth, eth],
581
- get: () => (this.session ?? "regular") === "extended" ? eth : rth,
582
- set: (v) => this.setSession(v === eth ? "extended" : "regular")
583
- },
584
- {
585
- kind: "color",
586
- label: "Pre-market",
587
- get: () => this.sessionShadeColor("premarketColor"),
588
- set: (v) => this.setSessionShadeColor("premarketColor", v)
589
- },
590
- {
591
- kind: "color",
592
- label: "Post-market",
593
- get: () => this.sessionShadeColor("postmarketColor"),
594
- set: (v) => this.setSessionShadeColor("postmarketColor", v)
595
- }
596
- ]
597
- };
598
- const advanced = {
599
- title: "Advanced",
600
- placement: "end",
601
- rows: [
602
- {
603
- kind: "select",
604
- label: "Bars to fetch",
605
- options: ["500", "1000", "2000", "5000", "10000", "20000"],
606
- get: () => String(this.bars),
607
- set: (v) => {
608
- this.bars = Number(v);
609
- this.markStateDirty();
610
- void this.inner?.setMarket({ bars: Math.max(this.bars, this.rangeBars) });
611
- }
612
- }
613
- ]
614
- };
615
- const watermarkSection = {
616
- title: "Watermark",
617
- placement: "symbol",
618
- rows: [
619
- {
620
- kind: "toggle",
621
- label: "Symbol watermark",
622
- get: () => this.watermarkOn,
623
- set: (v) => this.setWatermarkVisible(v)
624
- }
625
- ]
626
- };
627
- const sections = [];
628
- if (this.statusline) {
629
- const sl = this.statusline;
630
- sections.push({
631
- title: "Status line",
632
- rows: [
633
- { kind: "heading", label: "Status line" },
634
- { kind: "toggle", label: "Symbol name", get: () => sl.partVisible("name"), set: (v) => sl.setPartVisible("name", v) },
635
- { kind: "toggle", label: "Market status", get: () => sl.partVisible("market"), set: (v) => sl.setPartVisible("market", v) },
636
- { kind: "toggle", label: "OHLC values", get: () => sl.partVisible("ohlc"), set: (v) => sl.setPartVisible("ohlc", v) },
637
- { kind: "toggle", label: "Bar change values", get: () => sl.partVisible("change"), set: (v) => sl.setPartVisible("change", v) },
638
- { kind: "heading", label: "Indicators" },
639
- {
640
- kind: "toggle",
641
- label: "Titles",
642
- get: () => this.indicatorTitlesOn,
643
- set: (v) => this.setIndicatorTitlesVisible(v)
644
- },
645
- {
646
- kind: "toggle",
647
- label: "Values",
648
- get: () => this.indicatorValuesOn,
649
- set: (v) => this.setIndicatorValuesVisible(v)
650
- }
651
- ]
652
- });
653
- }
654
- sections.push(advanced);
655
- if (this.sessionsAvailable) sections.push(sessionSection);
656
- sections.push(watermarkSection);
657
- chart.renderer.setSettingsSections(sections);
658
- }
659
- /** Star/unstar a timeframe — the topbar chips and dropdown stars follow, and the
660
- * set persists with the rest of the shell state. */
661
- setTimeframeFavorite(tf, on) {
662
- if (on === this.tfFavs.includes(tf)) return;
663
- this.tfFavs = on ? [tf, ...this.tfFavs] : this.tfFavs.filter((f) => f !== tf);
664
- this.topbar.setTimeframeFavorites(this.tfFavs);
665
- this.markStateDirty();
666
- }
667
- /**
668
- * Switch the chart symbol in place. An `EXCHANGE:` prefix pins the venue (the
669
- * symbol picker composes one from the row the user pointed at) — a bare ticker
670
- * resolves against the registered providers in declaration order.
671
- */
672
50
  setSymbol(symbol) {
673
- if (this.destroyed) return;
674
- if (symbol === this.symbol) return;
675
- this.unresolvedToasted = null;
676
- this.symbol = symbol;
677
- this.topbar.setSymbol(symbol);
678
- this.mobileBar?.setSymbol(symbol);
679
- this.statusline?.setSymbol(symbol);
680
- this.objectTree.setSymbol(symbol);
681
- this.watermark?.update(symbol, this.timeframe);
682
- this.markStateDirty();
683
- void this.inner?.setMarket({ symbol });
684
- this.statusline?.setMeta(this.timeframe, this.providerLabel());
685
- }
686
- // ── state surface (same triplet as the workspace: getState / applyState / state:changed) ──
687
- /**
688
- * Snapshot the COMPLETE widget state as the unified shell document — the SAME
689
- * format `VelaWorkspace.getState()` returns, with a single `c1` cell: market,
690
- * display prefs, the renderer config document, the user-drawings document, and
691
- * the indicator ledger. This is what `persist` writes; hosts build custom flows
692
- * on it (server snapshots, share links, templates) — and a saved widget state
693
- * drops into a workspace slot as-is.
694
- */
695
- getState() {
696
- const cell = {};
697
- const live = this.inner?.market;
698
- const symbol = live?.symbol ?? this.symbol;
699
- if (symbol) cell.symbol = symbol;
700
- const provider = parseSymbol(symbol ?? "").provider ?? void 0;
701
- if (provider) cell.provider = provider;
702
- cell.timeframe = live?.timeframe ?? this.timeframe;
703
- cell.priceStyle = this.priceStyle;
704
- if (this.bars > 0) cell.bars = this.bars;
705
- const session = normalizeSession(live?.session ?? this.session);
706
- if (session === "extended") cell.session = session;
707
- cell.watermark = this.watermarkOn;
708
- cell.indicatorTitles = this.indicatorTitlesOn;
709
- cell.indicatorValues = this.indicatorValuesOn;
710
- if (this.inner) {
711
- cell.rendererConfig = this.inner.renderer.getConfig();
712
- cell.drawings = this.inner.drawings.toJSON();
713
- } else {
714
- if (this.savedConfig != null) cell.rendererConfig = this.savedConfig;
715
- if (this.savedDrawings != null) cell.drawings = this.savedDrawings;
716
- }
717
- cell.indicators = indicatorLedger({
718
- present: this.inner ? this.inner.presentNativeIndicators() : this.pendingIndicators?.natives ?? [],
719
- instanceNames: this.instances.map((it) => it.entry.name),
720
- pendingManifest: this.pendingIndicators?.manifest ?? null,
721
- manifestSettled: this.manifestSettled,
722
- volumePending: this.volumeMayBePending && (this.ledgerVolume ?? this.opts.volume !== false)
723
- });
724
- const state = { version: 1, layout: "1", activeCellId: "c1", timezone: this.timezone, charts: [{ id: "c1", ...cell }] };
725
- if (this.favs.length > 0) state.favorites = [...this.favs];
726
- if (this.tfFavs.length > 0) state.timeframeFavorites = [...this.tfFavs];
727
- const panels = this.dock.getState();
728
- if (panels) state.panels = panels;
729
- return state;
51
+ this.ws.active.setSymbol(symbol);
730
52
  }
731
- /**
732
- * Restore a state document produced by {@link getState} — or by a WORKSPACE (the
733
- * first cell of a multi-cell document applies). Untrusted-safe: malformed fields
734
- * are dropped. Applied IN PLACE: the chart instance survives (market switches via
735
- * `setMarket`), config/drawings/indicators are replaced. With `urlState` enabled,
736
- * URL params still win over the document's market/prefs fields.
737
- */
738
- applyState(state) {
739
- if (this.destroyed) return;
740
- const st = sanitizeState(state);
741
- if (!st) return;
742
- const cell = st.charts.find((c) => c.id === "c1") ?? st.charts[0];
743
- const fromUrl = this.opts.urlState ? readUrlState(typeof location !== "undefined" ? location.search : "") : {};
744
- const tz = fromUrl.timezone ?? st.timezone;
745
- if (tz && tz !== this.timezone) this.setTimezone(tz);
746
- if (st.favorites) {
747
- this.favs = [...st.favorites];
748
- this.inner?.drawings.setFavorites(this.favs);
749
- }
750
- if (st.timeframeFavorites) {
751
- this.tfFavs = [...st.timeframeFavorites];
752
- this.topbar.setTimeframeFavorites(this.tfFavs);
753
- }
754
- this.dock.applyState(st.panels);
755
- if (cell) {
756
- const style = fromUrl.priceStyle ?? cell.priceStyle;
757
- if (style && style !== this.priceStyle) this.setPriceStyle(style);
758
- if (cell.watermark !== void 0 && cell.watermark !== this.watermarkOn) this.setWatermarkVisible(cell.watermark);
759
- if (cell.indicatorTitles !== void 0 && cell.indicatorTitles !== this.indicatorTitlesOn) this.setIndicatorTitlesVisible(cell.indicatorTitles);
760
- if (cell.indicatorValues !== void 0 && cell.indicatorValues !== this.indicatorValuesOn) this.setIndicatorValuesVisible(cell.indicatorValues);
761
- if (cell.rendererConfig != null) {
762
- this.savedConfig = cell.rendererConfig;
763
- this.inner?.renderer.applyConfig(cell.rendererConfig);
764
- }
765
- if (cell.drawings != null) {
766
- this.savedDrawings = cell.drawings;
767
- this.inner?.drawings.fromJSON(cell.drawings);
768
- }
769
- if (cell.indicators) this.applyIndicatorLedger(cell.indicators);
770
- const symbol = fromUrl.symbol ?? prefixedSymbol(cell);
771
- const timeframe = fromUrl.timeframe ?? cell.timeframe;
772
- const bars = Number(fromUrl.bars ?? cell.bars ?? 0);
773
- const session = normalizeSession(fromUrl.session ?? cell.session);
774
- const next = {};
775
- if (symbol && symbol !== this.symbol) next.symbol = symbol;
776
- if (timeframe && timeframe !== this.timeframe) next.timeframe = timeframe;
777
- if (session && session !== (this.session ?? "regular")) {
778
- this.session = session;
779
- next.session = session;
780
- this.refreshSessionToggle();
781
- }
782
- if (bars > 0 && bars !== this.bars) {
783
- this.bars = bars;
784
- next.bars = Math.max(bars, this.rangeBars);
785
- }
786
- if (Object.keys(next).length > 0) void this.inner?.setMarket(next);
787
- }
788
- this.markStateDirty();
789
- }
790
- /** Subscribe to `state:changed` — the persistable state changed (debounced ~500ms);
791
- * re-pull {@link getState}. Returns an unsubscribe function. */
792
- on(event, handler) {
793
- if (event !== "state:changed") return () => void 0;
794
- this.stateListeners.add(handler);
795
- return () => this.stateListeners.delete(handler);
53
+ setTimeframe(timeframe) {
54
+ this.ws.active.setTimeframe(timeframe);
796
55
  }
797
- /** Replace the indicator ledger: natives converge to the listed set (volume
798
- * included — removing it sticks, the core's auto-add respects the opt-out),
799
- * manifest instances are re-created by name — held until the manifest resolves. */
800
- applyIndicatorLedger(led) {
801
- const chart = this.inner;
802
- if (!chart) return;
803
- this.ledgerVolume = led.natives.includes("volume");
804
- this.history.silently(() => {
805
- for (const type of led.natives) {
806
- if (!this.nativeCatalog.some((n) => n.type === type && n.present)) chart.addNativeIndicator(type);
807
- }
808
- for (const n of this.nativeCatalog) {
809
- if (n.present && !led.natives.includes(n.type)) chart.addNativeIndicator(n.type).remove();
810
- }
811
- for (const it of [...this.instances]) this.dropInstance(it);
812
- });
813
- if (this.manifest.length > 0) {
814
- for (const name of led.manifest) {
815
- const entry = this.manifest.find((e) => e.name === name);
816
- if (entry) this.instances.push({ entry, handle: this.addToChart(chart, entry) });
817
- }
818
- this.pendingIndicators = null;
819
- } else if (!this.manifestSettled) {
820
- this.pendingIndicators = led;
821
- } else {
822
- this.pendingIndicators = null;
823
- }
824
- this.refreshNativeCatalog();
825
- this.syncIndicatorCount();
826
- }
827
- /** SYNC boot fallback: a LEGACY three-key payload read at construction. */
828
- bootLegacySync(rawMain) {
829
- if (this.storageKey === null) return null;
830
- const cfg = this.storage.get(`${this.storageKey}:config`);
831
- const drw = this.storage.get(`${this.storageKey}:drawings`);
832
- if (cfg instanceof Promise || drw instanceof Promise) return null;
833
- const doc = sanitizeState(legacyWidgetState(parsePersisted(rawMain), cfg, drw));
834
- if (doc) this.legacyKeys = true;
835
- return doc;
56
+ setSession(session) {
57
+ this.ws.active.setSession(session);
836
58
  }
837
- /** ASYNC boot: resolve the unified document (or migrate a legacy payload), then
838
- * late-apply it — the widget built with option defaults in the meantime. */
839
- async bootAsync(rawMain) {
840
- let doc = null;
841
- try {
842
- const raw = await rawMain;
843
- doc = raw != null ? decodeState(raw) : null;
844
- if (!doc && this.storageKey !== null) {
845
- const [cfg, drw] = await Promise.all([
846
- Promise.resolve(this.storage.get(`${this.storageKey}:config`)),
847
- Promise.resolve(this.storage.get(`${this.storageKey}:drawings`))
848
- ]);
849
- doc = sanitizeState(legacyWidgetState(parsePersisted(raw), cfg, drw));
850
- if (doc) this.legacyKeys = true;
851
- }
852
- } catch {
853
- doc = null;
854
- }
855
- if (doc && !this.destroyed) this.applyState(doc);
59
+ setPriceStyle(style) {
60
+ this.ws.active.setPriceStyle(style);
856
61
  }
857
- /** Show/hide the symbol watermark behind the chart (persisted). */
858
62
  setWatermarkVisible(visible) {
859
- this.watermarkOn = visible;
860
- this.watermark?.setVisible(visible);
861
- this.markStateDirty();
63
+ this.ws.active.setWatermarkVisible(visible);
862
64
  }
863
- /** Show/hide the indicator titles — the in-chart legend rows (persisted). */
864
65
  setIndicatorTitlesVisible(visible) {
865
- this.indicatorTitlesOn = visible;
866
- this.inner?.renderer.set("indicatorTitles", visible);
867
- this.markStateDirty();
66
+ this.ws.active.setIndicatorTitlesVisible(visible);
868
67
  }
869
- /** Show/hide the plot values beside every indicator's legend title (persisted). */
870
68
  setIndicatorValuesVisible(visible) {
871
- this.indicatorValuesOn = visible;
872
- this.inner?.renderer.set("indicatorValues", visible);
873
- this.markStateDirty();
69
+ this.ws.active.setIndicatorValuesVisible(visible);
874
70
  }
875
- /** Applied LIVE (renderer feature) — no rebuild; persists across rebuilds. */
876
- setPriceStyle(style) {
877
- this.priceStyle = style;
878
- this.topbar.setPriceStyle(style);
879
- this.inner?.renderer.set("priceStyle", style);
880
- this.syncStatuslineColors();
881
- this.markStateDirty();
882
- }
883
- /** OHLC/change ink in the status line follows the ACTIVE price style's configured
884
- * colors and direction rule (candle bodies by close-vs-open, baseline by position
885
- * against the live baseline price, …) instead of the fixed theme tokens. */
886
- syncStatuslineColors() {
887
- if (!this.statusline || !this.inner) return;
888
- this.statusline.setDirectionColors(...statuslineInkOf(this.inner.renderer, this.priceStyle));
889
- }
890
- /** The widget shell (topbar, panels) keeps the app theme; the chart host's tokens
891
- * re-derive from the LIVE plot surface (see {@link applyPlotOverlayTokens}). */
892
- syncPlotOverlayTokens() {
893
- applyPlotOverlayTokens(this.chartHost, resolveTheme(this.opts.theme), this.inner?.renderer.getConfig() ?? null);
894
- }
895
- /**
896
- * Swap the widget theme at runtime — `'dark'`, `'light'`, or a full custom theme.
897
- * Applied LIVE: the inner chart re-skins through `Vela.setTheme` and its
898
- * `theme:changed` event re-tokens the widget chrome (topbar, menus, panels) in
899
- * place; the choice also persists into rebuilds. Same switch the user reaches from
900
- * chart settings → Canvas → Theme.
901
- */
902
71
  setTheme(theme) {
903
- if (this.destroyed) return;
904
- this.opts.theme = theme;
905
- this.inner?.setTheme(theme);
72
+ this.ws.setTheme(theme);
906
73
  }
907
- /** Applied LIVE (renderer feature) — no rebuild; persists across rebuilds. */
908
74
  setTimezone(zone) {
909
- this.timezone = zone;
910
- this.bottombar?.setTimezone(zone);
911
- this.inner?.renderer.set("timezone", zone);
912
- this.markStateDirty();
75
+ this.ws.setTimezone(zone);
913
76
  }
914
- /**
915
- * Range chip: switch to the preset's timeframe, fetch enough history for its window,
916
- * and frame it once ready. The in-place `setMarket` loads the depth, so it also runs
917
- * when only the DEPTH grows (same timeframe, deeper window).
918
- */
919
77
  applyRange(preset) {
920
- if (this.destroyed) return;
921
- this.activeRangeId = preset.id;
922
- this.bottombar?.setActiveRange(preset.id);
923
- this.pendingRange = preset;
924
- const tfChanged = preset.tf !== this.timeframe;
925
- const deeper = preset.bars > Math.max(this.bars, this.rangeBars);
926
- this.rangeBars = preset.bars;
927
- if (tfChanged) this.syncTimeframeChrome(preset.tf);
928
- if (tfChanged || deeper) {
929
- void this.inner?.setMarket({ timeframe: preset.tf, bars: Math.max(this.bars, this.rangeBars), visibleRange: preset.preset }).then(() => {
930
- if (!this.destroyed && this.pendingRange === preset) {
931
- this.inner?.setVisibleRangePreset(preset.preset);
932
- this.pendingRange = null;
933
- }
934
- });
935
- } else {
936
- this.inner?.setVisibleRangePreset(preset.preset);
937
- this.pendingRange = null;
938
- }
939
- }
940
- destroy() {
941
- this.destroyed = true;
942
- this.persistNow();
943
- this.inner?.destroy();
944
- this.inner = null;
945
- for (const dispose of this.attachmentDisposers.values()) {
946
- try {
947
- dispose();
948
- } catch {
949
- }
950
- }
951
- this.attachmentDisposers.clear();
952
- window.removeEventListener("beforeunload", this.onUnload);
953
- this.root.removeEventListener("keydown", this.onRootKeydown);
954
- this.topbar.destroy();
955
- this.dock.destroy();
956
- this.objectTree.destroy();
957
- this.dataWindow.destroy();
958
- this.shortcutsHelp?.destroy();
959
- this.contextMenu.destroy();
960
- this.symbolPicker.destroy();
961
- this.indicatorPicker?.destroy();
962
- this.tfQuick.destroy();
963
- this.marketStatus?.stop();
964
- this.sessionShading.stop();
965
- this.statusline?.destroy();
966
- this.watermark?.destroy();
967
- this.bottombar?.destroy();
968
- this.mobileBar?.destroy();
969
- this.drawingPill.destroy();
970
- this.tfDrawer?.destroy();
971
- this.drawingsDrawer?.destroy();
972
- this.moreDrawer?.destroy();
973
- this.timezoneDrawer?.destroy();
974
- this.priceScaleDrawer?.destroy();
975
- this.toast?.destroy();
976
- this.alertsMenu?.destroy();
977
- this.glider.stop();
978
- this.history.destroy();
979
- this.keymap.destroy();
980
- this.layoutCtl.destroy();
981
- this.root.remove();
982
- }
983
- /** Destroy + recreate the inner chart with the current symbol/timeframe, then
984
- * re-register providers/engines, rebind chrome, and re-add manifest indicators. */
985
- rebuild() {
986
- const seq = ++this.buildSeq;
987
- if (this.inner) {
988
- this.savedConfig = this.inner.renderer.getConfig();
989
- this.savedDrawings = this.inner.drawings.toJSON();
990
- }
991
- this.inner?.destroy();
992
- const {
993
- providers,
994
- engines,
995
- indicators,
996
- timeframes: _timeframes,
997
- priceStyle: _priceStyle,
998
- timezone: _timezone,
999
- statusline: _statusline,
1000
- watermark: _watermark,
1001
- bottombar: _bottombar,
1002
- layoutMode: _layoutMode,
1003
- ...chartOpts
1004
- } = this.opts;
1005
- const chart = new Vela(this.chartHost, {
1006
- ...chartOpts,
1007
- symbol: this.symbol,
1008
- timeframe: this.timeframe,
1009
- bars: Math.max(this.bars, this.rangeBars),
1010
- session: this.session,
1011
- volume: this.ledgerVolume ?? chartOpts.volume,
1012
- // A pending range chip frames the FIRST paint (no preview flash, no re-frame).
1013
- ...this.pendingRange ? { visibleRange: this.pendingRange.preset } : {}
1014
- });
1015
- for (const [name, make] of Object.entries(providers ?? {})) chart.data.registerProvider(name, make());
1016
- for (const [language, make] of Object.entries(resolveEngines(engines))) chart.registerEngine(language, make());
1017
- this.inner = chart;
1018
- this.symbolPicker.setSource(() => chart.data.symbols());
1019
- void chart.data.ready().then(() => {
1020
- if (this.inner !== chart) return;
1021
- this.statusline?.setMeta(this.timeframe, this.providerLabel());
1022
- this.refreshSessionToggle();
1023
- this.marketStatus?.track(chart.data, this.symbol);
1024
- });
1025
- if (this.layoutCtl !== void 0) chart.renderer.setLayoutMode(this.layoutCtl.current);
1026
- this.objectTree.setSymbol(this.symbol);
1027
- chart.renderer.setLegendActions(legendActionsProviderFor(chart, () => this.context()));
1028
- if (chart.renderer.supports("historyChords")) chart.renderer.set("historyChords", false);
1029
- this.dock.onChart(chart);
1030
- this.contextMenu.onChart(chart);
1031
- chart.renderer.onAxisLongPress((e) => {
1032
- if (this.layoutCtl.current !== "mobile") return;
1033
- if (e.axis === "time") this.openTimezoneDrawer();
1034
- else this.openPriceScaleDrawer(e.y);
1035
- });
1036
- this.refreshNativeCatalog();
1037
- chart.on("indicator:added", () => {
1038
- this.syncPresentNatives();
1039
- this.refreshNativeCatalog();
1040
- this.markStateDirty();
1041
- });
1042
- chart.on("indicator:removed", ({ id }) => {
1043
- const idx = this.instances.findIndex((it) => it.handle?.id === id);
1044
- if (idx >= 0) {
1045
- const snapshot = this.instances[idx];
1046
- this.instances.splice(idx, 1);
1047
- this.syncIndicatorCount();
1048
- this.history.push({
1049
- undo: () => {
1050
- snapshot.handle = this.inner ? this.addToChart(this.inner, snapshot.entry) : null;
1051
- this.instances.push(snapshot);
1052
- this.syncIndicatorCount();
1053
- },
1054
- redo: () => this.dropInstance(snapshot)
1055
- });
1056
- } else {
1057
- const now = this.inner?.presentNativeIndicators() ?? [];
1058
- for (const type of this.presentNatives.filter((t) => !now.includes(t))) {
1059
- this.history.push({
1060
- undo: () => {
1061
- this.inner?.addNativeIndicator(type);
1062
- this.refreshNativeCatalog();
1063
- },
1064
- redo: () => {
1065
- this.inner?.addNativeIndicator(type).remove();
1066
- this.refreshNativeCatalog();
1067
- }
1068
- });
1069
- }
1070
- }
1071
- this.syncPresentNatives();
1072
- this.refreshNativeCatalog();
1073
- this.markStateDirty();
1074
- });
1075
- if (this.pendingIndicators) {
1076
- for (const type of this.pendingIndicators.natives) chart.addNativeIndicator(type);
1077
- if (this.manifestSettled) this.pendingIndicators = null;
1078
- }
1079
- this.syncPresentNatives();
1080
- chart.on("load:end", () => {
1081
- this.volumeMayBePending = false;
1082
- });
1083
- chart.on("load:start", () => this.watermark?.setLoading(true));
1084
- chart.on("load:end", () => this.watermark?.setLoading(false));
1085
- chart.on("theme:changed", (t) => {
1086
- this.opts.theme = t;
1087
- ensureUIHost(this.root, t);
1088
- this.syncPlotOverlayTokens();
1089
- });
1090
- chart.on("market:changed", ({ symbol, timeframe }) => {
1091
- this.refreshNativeCatalog();
1092
- if (this.statusline) this.statusline.onChart(chart);
1093
- if (symbol !== this.symbol) {
1094
- this.symbol = symbol;
1095
- this.topbar.setSymbol(symbol);
1096
- this.mobileBar?.setSymbol(symbol);
1097
- this.statusline?.setSymbol(symbol);
1098
- this.objectTree.setSymbol(symbol);
1099
- this.watermark?.update(symbol, this.timeframe);
1100
- this.refreshSessionToggle();
1101
- this.marketStatus?.track(chart.data, symbol);
1102
- }
1103
- if (timeframe !== this.timeframe) {
1104
- this.syncTimeframeChrome(timeframe);
1105
- this.refreshSessionShading();
1106
- }
1107
- });
1108
- this.history.onChart(chart);
1109
- chart.on("alert", (alert) => {
1110
- this.alerts.unshift({ title: alert.title ?? "Alert", message: alert.message, time: alert.time });
1111
- if (this.alerts.length > 20) this.alerts.pop();
1112
- this.toast?.show(`${alert.title ? alert.title + " \u2014 " : ""}${alert.message}`, "info", 4e3);
1113
- this.topbar.setAlertCount(this.alerts.length);
1114
- });
1115
- chart.on("indicator:error", ({ error }) => this.toast?.show(error.message, "error", 5e3));
1116
- chart.on("data:unresolved", ({ symbol, providers: providers2 }) => {
1117
- if (this.unresolvedToasted === symbol) return;
1118
- this.unresolvedToasted = symbol;
1119
- const list = providers2.length > 0 ? providers2.join(", ") : "none";
1120
- this.toast?.show(`No registered provider serves "${symbol}" (registered: ${list})`, "error", 6e3);
1121
- });
1122
- if (this.favs.length > 0) chart.drawings.setFavorites(this.favs);
1123
- chart.on("drawing:favorites", ({ favorites }) => {
1124
- this.favs = favorites;
1125
- this.markStateDirty();
1126
- });
1127
- if (chart.drawings.supported) chart.drawings.setToolShortcuts(toolShortcutHints(this.keymap));
1128
- chart.on("drawing:created", () => this.markStateDirty());
1129
- chart.on("drawing:edited", () => this.markStateDirty());
1130
- chart.on("drawing:removed", () => this.markStateDirty());
1131
- chart.renderer.onCrosshairMove((e) => {
1132
- this.lastCrossPrice = e.price;
1133
- this.lastCrossTime = e.time;
1134
- });
1135
- this.topbar.renderActions();
1136
- this.mobileBar?.renderActions();
1137
- if (this.savedConfig != null) chart.renderer.applyConfig(this.savedConfig);
1138
- if (this.savedDrawings != null) chart.drawings.fromJSON(this.savedDrawings);
1139
- if (this.priceStyle !== "candles") chart.renderer.set("priceStyle", this.priceStyle);
1140
- if (this.timezone !== "Etc/UTC") chart.renderer.set("timezone", this.timezone);
1141
- if (!this.indicatorTitlesOn) chart.renderer.set("indicatorTitles", false);
1142
- if (!this.indicatorValuesOn) chart.renderer.set("indicatorValues", false);
1143
- chart.renderer.onConfigChanged(() => {
1144
- const zone = chart.renderer.get("timezone");
1145
- if (typeof zone === "string" && normalizeTimezone(zone) !== normalizeTimezone(this.timezone)) {
1146
- this.setTimezone(normalizeTimezone(zone));
1147
- }
1148
- this.syncStatuslineColors();
1149
- this.syncPlotOverlayTokens();
1150
- });
1151
- this.syncPlotOverlayTokens();
1152
- this.statusline?.onChart(chart);
1153
- this.drawingPill.onChart(chart);
1154
- this.syncStatuslineColors();
1155
- this.pushSettingsSections();
1156
- void chart.ready().then(() => {
1157
- if (this.buildSeq !== seq || this.destroyed) return;
1158
- if (this.pendingRange) {
1159
- chart.setVisibleRangePreset(this.pendingRange.preset);
1160
- this.pendingRange = null;
1161
- }
1162
- this.statusline?.setMeta(this.timeframe, this.providerLabel());
1163
- });
1164
- if (this.manifest.length || this.instances.length) {
1165
- for (const it of this.instances) it.handle = this.addToChart(chart, it.entry);
1166
- this.syncIndicatorCount();
1167
- } else if (indicators !== void 0) {
1168
- this.indicatorsPromise ?? (this.indicatorsPromise = resolveIndicators(indicators).then((list) => {
1169
- this.manifest = list;
1170
- const pending = this.pendingIndicators;
1171
- this.instances = pending ? pending.manifest.map((name) => list.find((e) => e.name === name)).filter((e) => e != null).map((entry) => ({ entry, handle: null })) : list.filter((e) => e.enabled).map((entry) => ({ entry, handle: null }));
1172
- if (pending) this.pendingIndicators = null;
1173
- this.manifestSettled = true;
1174
- return list;
1175
- }));
1176
- void this.indicatorsPromise.then(() => {
1177
- if (this.buildSeq !== seq || this.destroyed) return;
1178
- for (const it of this.instances) {
1179
- it.handle = this.addToChart(chart, it.entry);
1180
- }
1181
- this.syncIndicatorCount();
1182
- });
1183
- }
78
+ this.ws.active.applyRange(preset);
1184
79
  }
1185
- /** Refresh the native catalog (supported/present flags) for the current chart. */
1186
- refreshNativeCatalog() {
1187
- const chart = this.inner;
1188
- if (!chart) return;
1189
- void chart.availableNativeIndicators().then((list) => {
1190
- if (this.inner !== chart || this.destroyed) return;
1191
- this.nativeCatalog = list.map((n) => ({ type: n.type, title: n.title, supported: n.supported, present: n.present, beta: n.beta }));
1192
- this.syncIndicatorCount();
1193
- this.indicatorPicker?.sync();
1194
- });
1195
- }
1196
- /** Add a native indicator (single-instance per type — the core dedupes). */
1197
- addNative(type) {
1198
- this.inner?.addNativeIndicator(type);
1199
- this.syncPresentNatives();
1200
- this.refreshNativeCatalog();
1201
- this.history.push({
1202
- undo: () => {
1203
- this.inner?.addNativeIndicator(type).remove();
1204
- this.refreshNativeCatalog();
1205
- },
1206
- redo: () => {
1207
- this.inner?.addNativeIndicator(type);
1208
- this.refreshNativeCatalog();
1209
- }
1210
- });
1211
- }
1212
- removeNative(type) {
1213
- this.inner?.addNativeIndicator(type).remove();
1214
- this.refreshNativeCatalog();
1215
- }
1216
- /** Sync mirror of the chart's present native types — the removal handler diffs
1217
- * against it to identify (and record) whichever type was just removed. */
1218
- syncPresentNatives() {
1219
- this.presentNatives = this.inner?.presentNativeIndicators() ?? [];
1220
- }
1221
- /** Add ONE instance of a manifest entry (repeatable — duplicates are legitimate). */
1222
- addInstance(libraryIndex) {
1223
- const entry = this.manifest[libraryIndex];
1224
- if (!entry || this.destroyed) return;
1225
- const it = { entry, handle: this.inner ? this.addToChart(this.inner, entry) : null };
1226
- this.instances.push(it);
1227
- this.syncIndicatorCount();
1228
- const snapshot = it;
1229
- this.history.push({
1230
- undo: () => this.dropInstance(snapshot),
1231
- redo: () => {
1232
- snapshot.handle = this.inner ? this.addToChart(this.inner, snapshot.entry) : null;
1233
- this.instances.push(snapshot);
1234
- this.syncIndicatorCount();
1235
- }
1236
- });
1237
- }
1238
- /** Remove one live instance (picker trash). */
1239
- removeInstance(instanceIndex) {
1240
- const it = this.instances[instanceIndex];
1241
- if (!it || this.destroyed) return;
1242
- this.dropInstance(it);
1243
- const snapshot = it;
1244
- this.history.push({
1245
- undo: () => {
1246
- snapshot.handle = this.inner ? this.addToChart(this.inner, snapshot.entry) : null;
1247
- this.instances.push(snapshot);
1248
- this.syncIndicatorCount();
1249
- },
1250
- redo: () => this.dropInstance(snapshot)
1251
- });
1252
- }
1253
- dropInstance(it) {
1254
- const idx = this.instances.indexOf(it);
1255
- if (idx >= 0) this.instances.splice(idx, 1);
1256
- try {
1257
- it.handle?.remove();
1258
- } catch {
1259
- }
1260
- it.handle = null;
1261
- this.syncIndicatorCount();
1262
- }
1263
- syncIndicatorCount() {
1264
- this.topbar.setIndicatorCount(this.instances.length + this.nativeCatalog.filter((n) => n.present).length);
1265
- }
1266
- addToChart(chart, ind) {
1267
- try {
1268
- return chart.addIndicator(ind.script, ind.language !== void 0 ? { language: ind.language } : void 0);
1269
- } catch (err) {
1270
- console.warn(`[vela] indicator "${ind.name}" failed to add:`, err);
1271
- return null;
1272
- }
1273
- }
1274
- /** Bare-typing router: letters → symbol search (seeded), digits → timeframe entry. */
1275
- routeTyping(ev) {
1276
- if (this.destroyed || this.openDialogs > 0) return;
1277
- if (ev.ctrlKey || ev.metaKey || ev.altKey) return;
1278
- if (isEditableTarget(ev)) return;
1279
- const key = ev.key;
1280
- if (/^[a-zA-Z]$/.test(key)) {
1281
- ev.preventDefault();
1282
- this.symbolPicker.open(key.toUpperCase());
1283
- } else if (/^[0-9]$/.test(key)) {
1284
- ev.preventDefault();
1285
- this.tfQuick.open(key);
1286
- }
1287
- }
1288
- trackDialog(open) {
1289
- this.openDialogs = Math.max(0, this.openDialogs + (open ? 1 : -1));
1290
- if (open) this.keymap.pushScope("dialog");
1291
- else this.keymap.popScope("dialog");
80
+ /** The unified state document (`layout: '1'`, one cell). */
81
+ getState() {
82
+ return this.ws.getState();
1292
83
  }
1293
- downloadScreenshot() {
1294
- const url = this.inner?.renderer.screenshot();
1295
- if (!url) return;
1296
- const a = this.root.ownerDocument.createElement("a");
1297
- a.href = url;
1298
- a.download = `${this.symbol || "chart"}-${this.timeframe}.png`;
1299
- a.click();
84
+ /** Restore a state document IN PLACE — the chart instance survives. */
85
+ applyState(state) {
86
+ this.ws.applyState(state);
1300
87
  }
1301
- /** The persistable state changed: debounce (~500ms), then notify `state:changed`
1302
- * listeners and flush — the same cadence as the workspace. */
1303
- markStateDirty() {
1304
- if (this.destroyed) return;
1305
- if (this.stateTimer != null) clearTimeout(this.stateTimer);
1306
- this.stateTimer = setTimeout(() => {
1307
- this.stateTimer = null;
1308
- for (const listener of [...this.stateListeners]) listener();
1309
- this.persistNow();
1310
- }, 500);
88
+ /** Subscribe to `state:changed` (the only widget event — unknown names no-op). */
89
+ on(event, handler) {
90
+ if (event !== "state:changed") return () => void 0;
91
+ return this.ws.on("state:changed", handler);
1311
92
  }
1312
- /** Write the unified state document through the adapter (persist mode only) — also
1313
- * the unload/destroy flush. The first save after a LEGACY boot rewrites the main
1314
- * key in the unified format and drops the old `:config`/`:drawings` sub-keys. */
1315
- persistNow() {
1316
- if (this.stateTimer != null) {
1317
- clearTimeout(this.stateTimer);
1318
- this.stateTimer = null;
1319
- }
1320
- const state = this.getState();
1321
- if (this.storageKey !== null) {
1322
- try {
1323
- void this.storage.set(this.storageKey, encodeState(state));
1324
- } catch {
1325
- }
1326
- if (this.legacyKeys) {
1327
- this.legacyKeys = false;
1328
- try {
1329
- void this.storage.remove?.(`${this.storageKey}:config`);
1330
- void this.storage.remove?.(`${this.storageKey}:drawings`);
1331
- } catch {
1332
- }
1333
- }
1334
- }
1335
- if (this.opts.urlState) {
1336
- writeUrlState({
1337
- symbol: this.symbol,
1338
- timeframe: this.timeframe,
1339
- priceStyle: this.priceStyle,
1340
- timezone: this.timezone,
1341
- bars: String(this.bars),
1342
- // Empty clears the param — the default (regular) keeps links lean.
1343
- session: this.session === "extended" ? "extended" : "",
1344
- watermark: this.watermarkOn ? "1" : "0",
1345
- favorites: this.favs.join(",")
1346
- });
1347
- }
93
+ destroy() {
94
+ this.ws.destroy();
1348
95
  }
1349
96
  };
1350
97
 
1351
- export { VelaWidget, readUrlState, writeUrlState };
98
+ export { VelaWidget };