@luxalgo/vela 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/LICENSE +201 -0
  2. package/NOTICE +28 -0
  3. package/README.md +135 -0
  4. package/dist/DataProvider-DKNDHpNv.d.cts +134 -0
  5. package/dist/DataProvider-Dzd-Erlk.d.ts +134 -0
  6. package/dist/chunk-7UFX5ZIG.js +5976 -0
  7. package/dist/chunk-GYD2THPV.js +252 -0
  8. package/dist/chunk-KCCZNKH7.js +19137 -0
  9. package/dist/chunk-KM6LHB3Y.js +76 -0
  10. package/dist/chunk-OVQKKXLZ.js +3978 -0
  11. package/dist/chunk-Q3XQHLIH.js +7 -0
  12. package/dist/chunk-RHIDOUFL.js +698 -0
  13. package/dist/contributions-hX3EUyjG.d.ts +1528 -0
  14. package/dist/contributions-o1GRKPI_.d.cts +1528 -0
  15. package/dist/history-Dzxz-MQj.d.ts +298 -0
  16. package/dist/history-LJkz4-sS.d.cts +298 -0
  17. package/dist/icons-BZYbJXSV.d.cts +10 -0
  18. package/dist/icons-BZYbJXSV.d.ts +10 -0
  19. package/dist/index.cjs +25294 -0
  20. package/dist/index.d.cts +1189 -0
  21. package/dist/index.d.ts +1189 -0
  22. package/dist/index.js +5 -0
  23. package/dist/keymap-CGOz5F5f.d.cts +64 -0
  24. package/dist/keymap-CGOz5F5f.d.ts +64 -0
  25. package/dist/options-Q-576hIi.d.cts +1545 -0
  26. package/dist/options-Q-576hIi.d.ts +1545 -0
  27. package/dist/plugin-D94muTV-.d.cts +405 -0
  28. package/dist/plugin-aGUD1epn.d.ts +405 -0
  29. package/dist/plugin.cjs +5967 -0
  30. package/dist/plugin.d.cts +7 -0
  31. package/dist/plugin.d.ts +7 -0
  32. package/dist/plugin.js +3 -0
  33. package/dist/providers/binance.cjs +390 -0
  34. package/dist/providers/binance.d.cts +62 -0
  35. package/dist/providers/binance.d.ts +62 -0
  36. package/dist/providers/binance.js +388 -0
  37. package/dist/providers/coinbase.cjs +462 -0
  38. package/dist/providers/coinbase.d.cts +59 -0
  39. package/dist/providers/coinbase.d.ts +59 -0
  40. package/dist/providers/coinbase.js +460 -0
  41. package/dist/providers/hyperliquid.cjs +361 -0
  42. package/dist/providers/hyperliquid.d.cts +54 -0
  43. package/dist/providers/hyperliquid.d.ts +54 -0
  44. package/dist/providers/hyperliquid.js +359 -0
  45. package/dist/side-panel-CT9ZwIGz.d.cts +63 -0
  46. package/dist/side-panel-CT9ZwIGz.d.ts +63 -0
  47. package/dist/ui.cjs +1030 -0
  48. package/dist/ui.d.cts +188 -0
  49. package/dist/ui.d.ts +188 -0
  50. package/dist/ui.js +3 -0
  51. package/dist/vela.global.js +26509 -0
  52. package/dist/vela.global.min.js +230 -0
  53. package/dist/widget.cjs +31178 -0
  54. package/dist/widget.d.cts +799 -0
  55. package/dist/widget.d.ts +799 -0
  56. package/dist/widget.js +1060 -0
  57. package/dist/workspace.cjs +31692 -0
  58. package/dist/workspace.d.cts +579 -0
  59. package/dist/workspace.d.ts +579 -0
  60. package/dist/workspace.js +1694 -0
  61. package/package.json +95 -0
@@ -0,0 +1,3978 @@
1
+ import { parseSymbol, priceStyleIds, tzButtonLabel, TIMEZONES, normalizeTimezone, tzMenuLabel } from './chunk-KCCZNKH7.js';
2
+ import { chartType, widgetActions, SidePanel, sidePanels, DEFAULT_PANEL_ORDER, getDrawingType } from './chunk-7UFX5ZIG.js';
3
+ import { Tooltip, Menu, Dialog } from './chunk-RHIDOUFL.js';
4
+ import { injectStyles, iconEl, iconMarkup, SESSION_PRE, SESSION_POST, SESSION_OFF, icon, registerIcon, categoricalColor } from './chunk-GYD2THPV.js';
5
+
6
+ // src/widget/timeframe.ts
7
+ var UNIT_MS = {
8
+ S: 1e3,
9
+ MIN: 6e4,
10
+ H: 36e5,
11
+ D: 864e5,
12
+ W: 6048e5,
13
+ M: 2592e6,
14
+ Y: 31536e6
15
+ };
16
+ var UNIT_NAME = {
17
+ S: "second",
18
+ MIN: "minute",
19
+ H: "hour",
20
+ D: "day",
21
+ W: "week",
22
+ M: "month",
23
+ Y: "year"
24
+ };
25
+ var UNIT_SHORT = { S: "s", MIN: "m", H: "h", D: "D", W: "W", M: "M", Y: "Y" };
26
+ function unitKey(letter) {
27
+ if (letter === "") return "MIN";
28
+ return letter === "S" || letter === "H" || letter === "D" || letter === "W" || letter === "M" || letter === "Y" ? letter : null;
29
+ }
30
+ var UNIT_MINUTES = {
31
+ MIN: 1,
32
+ H: 60,
33
+ D: 1440,
34
+ W: 10080,
35
+ M: 43200,
36
+ Y: 525600
37
+ };
38
+ function canonicalFor(key, count) {
39
+ if (key === "S") return `${count}S`;
40
+ return `${count * UNIT_MINUTES[key]}`;
41
+ }
42
+ function parseTimeframe(text) {
43
+ const raw = String(text ?? "").trim();
44
+ if (!raw) return { valid: false };
45
+ const m = /^(\d*)\s*([a-zA-Z]?)$/.exec(raw);
46
+ if (!m) return { valid: false };
47
+ const numStr = m[1] ?? "";
48
+ const key = unitKey((m[2] ?? "").toUpperCase());
49
+ if (key === null) return { valid: false };
50
+ if (numStr === "" && key === "MIN") return { valid: false };
51
+ const count = numStr === "" ? 1 : parseInt(numStr, 10);
52
+ if (!Number.isFinite(count) || count < 1) return { valid: false };
53
+ const ms = count * UNIT_MS[key];
54
+ const name = UNIT_NAME[key];
55
+ return {
56
+ valid: true,
57
+ count,
58
+ unit: key,
59
+ ms,
60
+ canonical: canonicalFor(key, count),
61
+ label: `${count} ${name}${count === 1 ? "" : "s"}`,
62
+ short: `${count}${UNIT_SHORT[key]}`
63
+ };
64
+ }
65
+ function timeframeMs(value) {
66
+ const v = String(value ?? "").trim();
67
+ if (!v) return NaN;
68
+ const m = /^(\d*)\s*([a-zA-Z]?)$/.exec(v);
69
+ if (!m) return NaN;
70
+ const key = unitKey((m[2] ?? "").toUpperCase());
71
+ if (key === null) return NaN;
72
+ if ((m[1] ?? "") === "" && key === "MIN") return NaN;
73
+ const count = (m[1] ?? "") === "" ? 1 : parseInt(m[1], 10);
74
+ if (!Number.isFinite(count) || count < 1) return NaN;
75
+ return count * UNIT_MS[key];
76
+ }
77
+ function timeframeLabel(value) {
78
+ const parsed = parseTimeframe(value);
79
+ if (!parsed.valid) return value;
80
+ const ms = parsed.ms;
81
+ const order = ["Y", "M", "W", "D", "H", "MIN", "S"];
82
+ for (const key of order) {
83
+ if (ms % UNIT_MS[key] === 0 && ms / UNIT_MS[key] >= 1) {
84
+ return `${ms / UNIT_MS[key]}${UNIT_SHORT[key]}`;
85
+ }
86
+ }
87
+ return parsed.short;
88
+ }
89
+
90
+ // src/widget/topbar.ts
91
+ var STYLE_ID = "vela-topbar";
92
+ var CSS = `
93
+ .vela-widget-topbar {
94
+ display: flex;
95
+ align-items: center;
96
+ gap: var(--vela-space-1);
97
+ padding: var(--vela-space-1) var(--vela-space-2);
98
+ border-bottom: 1px solid var(--vela-border-soft);
99
+ color: var(--vela-fg);
100
+ font-size: var(--vela-font-size-md);
101
+ flex: none;
102
+ }
103
+ .vela-widget-symbol, .vela-widget-tf, .vela-widget-style, .vela-widget-indicators {
104
+ all: unset;
105
+ display: inline-flex;
106
+ align-items: center;
107
+ gap: 6px;
108
+ height: 30px;
109
+ padding: 0 9px;
110
+ border-radius: 4px;
111
+ cursor: pointer;
112
+ color: var(--vela-fg-muted);
113
+ font-size: 13px;
114
+ font-weight: 550;
115
+ white-space: nowrap;
116
+ }
117
+ .vela-widget-symbol {
118
+ color: var(--vela-fg-bright);
119
+ font-size: 15px;
120
+ font-weight: 600;
121
+ letter-spacing: 0.3px;
122
+ padding: 0 10px;
123
+ gap: 7px;
124
+ }
125
+ .vela-widget-tf, .vela-widget-style, .vela-widget-indicators {
126
+ color: var(--vela-fg-bright);
127
+ }
128
+ .vela-widget-symbol:hover, .vela-widget-tf:hover, .vela-widget-style:hover, .vela-widget-indicators:hover { background: var(--vela-hover); color: var(--vela-fg-bright); }
129
+ .vela-widget-topbar .vela-icon { color: inherit; font-size: 16px; width: 16px; height: 16px; }
130
+ /* Width is set in syncHairlines() to exactly one device pixel \u2014 a CSS 1px at
131
+ fractional DPR (1.25, 1.5\u2026) straddles two physical pixels and siblings end
132
+ up looking like different thicknesses depending on subpixel placement. */
133
+ .vela-sep { height: 22px; margin: 0 2px; flex: none; background: var(--vela-border-strong); }
134
+ .vela-alerts-badge {
135
+ position: absolute;
136
+ top: 2px;
137
+ right: 2px;
138
+ min-width: 13px;
139
+ height: 13px;
140
+ padding: 0 3px;
141
+ border-radius: 7px;
142
+ background: var(--vela-accent);
143
+ color: var(--vela-fg-on-fill);
144
+ font-size: 9px;
145
+ font-weight: 700;
146
+ display: inline-flex;
147
+ align-items: center;
148
+ justify-content: center;
149
+ }
150
+ .vela-widget-actions { margin-left: auto; display: inline-flex; gap: var(--vela-space-1); }
151
+ /* The side-panel toggles, one per docked panel \u2014 a group so the dock can rebuild them
152
+ without disturbing the tools around it. */
153
+ .vela-widget-panels { display: inline-flex; align-items: center; gap: var(--vela-space-1); }
154
+ .vela-widget-tool {
155
+ all: unset;
156
+ display: inline-flex;
157
+ align-items: center;
158
+ justify-content: center;
159
+ width: 32px;
160
+ height: 30px;
161
+ border-radius: 4px;
162
+ cursor: pointer;
163
+ color: var(--vela-fg-muted);
164
+ font-size: 14px;
165
+ }
166
+ .vela-widget-tool:hover:not(:disabled) { background: var(--vela-hover); color: var(--vela-fg-bright); }
167
+ .vela-widget-tool:disabled { opacity: 0.35; cursor: default; }
168
+ .vela-widget-tool[data-active='1'] { background: var(--vela-hover); color: var(--vela-fg-bright); }
169
+ .vela-widget-action {
170
+ all: unset;
171
+ display: inline-flex;
172
+ align-items: center;
173
+ gap: 6px;
174
+ padding: 4px 8px;
175
+ border-radius: var(--vela-radius-sm);
176
+ cursor: pointer;
177
+ color: var(--vela-fg);
178
+ }
179
+ .vela-widget-action:hover { background: var(--vela-hover); }
180
+ `;
181
+ var BUILTIN_STYLE_LABELS = {
182
+ candles: "Candles",
183
+ bars: "Bars",
184
+ line: "Line",
185
+ area: "Area",
186
+ baseline: "Baseline"
187
+ };
188
+ function priceStyleLabel(id) {
189
+ return chartType(id)?.label ?? BUILTIN_STYLE_LABELS[id] ?? id;
190
+ }
191
+ function priceStyleIcon(id) {
192
+ const iconId = `style-${id}`;
193
+ if (iconMarkup(iconId)) return iconId;
194
+ const svg = chartType(id)?.icon;
195
+ if (svg) {
196
+ registerIcon(iconId, svg);
197
+ return iconId;
198
+ }
199
+ return void 0;
200
+ }
201
+ var Topbar = class {
202
+ constructor(host, opts) {
203
+ this.layoutButton = null;
204
+ this.layoutMenu = null;
205
+ this.layoutId = null;
206
+ this.tooltips = [];
207
+ this.panelBtns = /* @__PURE__ */ new Map();
208
+ this.panelTooltips = [];
209
+ this.onHairlineSync = () => {
210
+ if (this.hairlineRaf) return;
211
+ const win = this.el.ownerDocument.defaultView;
212
+ this.hairlineRaf = win?.requestAnimationFrame(() => {
213
+ this.hairlineRaf = 0;
214
+ this.syncHairlines();
215
+ }) ?? 0;
216
+ };
217
+ this.hairlineRo = null;
218
+ this.hairlineRaf = 0;
219
+ this.opts = opts;
220
+ this.host = host;
221
+ this.timeframe = opts.timeframe;
222
+ this.priceStyle = opts.priceStyle;
223
+ const doc = host.ownerDocument;
224
+ injectStyles(STYLE_ID, CSS, doc);
225
+ this.el = doc.createElement("div");
226
+ this.el.className = "vela-widget-topbar";
227
+ this.symbolEl = doc.createElement("button");
228
+ this.symbolEl.className = "vela-widget-symbol";
229
+ this.symbolEl.textContent = parseSymbol(opts.symbol).ticker;
230
+ if (opts.onSymbolClick) this.symbolEl.addEventListener("click", opts.onSymbolClick);
231
+ this.tfButton = doc.createElement("button");
232
+ this.tfButton.className = "vela-widget-tf";
233
+ this.tfButton.textContent = timeframeLabel(this.timeframe);
234
+ this.styleButton = doc.createElement("button");
235
+ this.styleButton.className = "vela-widget-style";
236
+ this.renderStyleButton(doc);
237
+ const indicatorsBtn = doc.createElement("button");
238
+ indicatorsBtn.className = "vela-widget-indicators";
239
+ indicatorsBtn.append(iconEl("indicators", doc), doc.createTextNode("Indicators"));
240
+ if (opts.onIndicatorsClick) indicatorsBtn.addEventListener("click", opts.onIndicatorsClick);
241
+ this.actionsHost = doc.createElement("span");
242
+ this.actionsHost.className = "vela-widget-actions";
243
+ this.panelsHost = doc.createElement("span");
244
+ this.panelsHost.className = "vela-widget-panels";
245
+ const tool = (cls, icon2, tip, onClick) => this.toolButton(cls, icon2, tip, onClick, this.tooltips);
246
+ this.undoBtn = tool("vela-widget-undo", "undo", "Undo", opts.onUndoClick);
247
+ this.redoBtn = tool("vela-widget-redo", "redo", "Redo", opts.onRedoClick);
248
+ this.setHistoryState(false, false);
249
+ const screenshotBtn = tool("vela-widget-screenshot", "camera", "Download screenshot", opts.onScreenshotClick);
250
+ this.alertsBtn = tool("vela-widget-alerts", "bell", "Alerts");
251
+ this.alertsBtn.style.position = "relative";
252
+ if (opts.onAlertsClick) this.alertsBtn.addEventListener("click", () => opts.onAlertsClick(this.alertsBtn));
253
+ this.alertsBadge = doc.createElement("span");
254
+ this.alertsBadge.className = "vela-alerts-badge";
255
+ this.alertsBadge.style.display = "none";
256
+ this.alertsBtn.appendChild(this.alertsBadge);
257
+ if (opts.layout) {
258
+ this.layoutId = opts.layout.current;
259
+ this.layoutButton = doc.createElement("button");
260
+ this.layoutButton.className = "vela-widget-style";
261
+ this.renderLayoutButton(doc);
262
+ }
263
+ const sep = () => {
264
+ const d = doc.createElement("span");
265
+ d.className = "vela-sep";
266
+ return d;
267
+ };
268
+ const leading = [this.symbolEl, sep(), this.tfButton, sep(), this.styleButton, sep()];
269
+ if (this.layoutButton) leading.push(this.layoutButton, sep());
270
+ this.el.append(...leading, indicatorsBtn, sep(), this.undoBtn, this.redoBtn, this.actionsHost, this.alertsBtn, this.panelsHost, screenshotBtn);
271
+ host.appendChild(this.el);
272
+ this.onHairlineSync();
273
+ this.hairlineRo = new ResizeObserver(this.onHairlineSync);
274
+ this.hairlineRo.observe(this.el);
275
+ doc.defaultView?.addEventListener("resize", this.onHairlineSync);
276
+ this.renderActions();
277
+ this.tooltips.push(new Tooltip(this.tfButton, { content: "Timeframe", triggerId: "vela-topbar-tf", host }));
278
+ this.tooltips.push(new Tooltip(this.styleButton, { content: "Chart style", triggerId: "vela-topbar-style", host }));
279
+ if (this.layoutButton && opts.layout) {
280
+ this.tooltips.push(new Tooltip(this.layoutButton, { content: "Layout", triggerId: "vela-topbar-layout", host }));
281
+ this.layoutMenu = new Menu({
282
+ trigger: this.layoutButton,
283
+ triggerId: "vela-topbar-layout",
284
+ host,
285
+ items: this.layoutItems(),
286
+ onSelect: (id) => {
287
+ if (id.startsWith("toggle:")) {
288
+ opts.layout.onToggle?.(id.slice("toggle:".length));
289
+ this.layoutMenu?.setItems(this.layoutItems());
290
+ return;
291
+ }
292
+ opts.layout.onSelect(id);
293
+ }
294
+ });
295
+ }
296
+ this.tfMenu = new Menu({
297
+ trigger: this.tfButton,
298
+ triggerId: "vela-topbar-tf",
299
+ host,
300
+ items: this.tfItems(),
301
+ onSelect: (id) => opts.onTimeframe(id),
302
+ // Timeframe labels are two-or-three characters ("1m", "4h", "1D") — the
303
+ // stylesheet's default min-width would leave the list mostly empty.
304
+ minWidth: "84px"
305
+ });
306
+ this.styleMenu = new Menu({
307
+ trigger: this.styleButton,
308
+ triggerId: "vela-topbar-style",
309
+ host,
310
+ items: this.styleItems(),
311
+ onSelect: (id) => opts.onPriceStyle(id)
312
+ });
313
+ }
314
+ setSymbol(symbol) {
315
+ this.symbolEl.textContent = parseSymbol(symbol).ticker;
316
+ }
317
+ setTimeframe(tf) {
318
+ this.timeframe = tf;
319
+ this.tfButton.textContent = timeframeLabel(tf);
320
+ this.tfMenu.setItems(this.tfItems());
321
+ }
322
+ setPriceStyle(style) {
323
+ this.priceStyle = style;
324
+ this.renderStyleButton(this.styleButton.ownerDocument);
325
+ this.styleMenu.setItems(this.styleItems());
326
+ }
327
+ /** Reflect the current workspace layout (no-op without the layout dropdown). */
328
+ setLayout(id) {
329
+ if (!this.layoutButton) return;
330
+ this.layoutId = id;
331
+ this.renderLayoutButton(this.layoutButton.ownerDocument);
332
+ this.layoutMenu?.setItems(this.layoutItems());
333
+ }
334
+ renderLayoutButton(doc) {
335
+ if (!this.layoutButton) return;
336
+ this.layoutButton.replaceChildren();
337
+ if (iconMarkup("layout")) this.layoutButton.appendChild(iconEl("layout", doc));
338
+ else this.layoutButton.appendChild(doc.createTextNode(this.layoutId ?? ""));
339
+ this.layoutButton.setAttribute("aria-label", `Layout \u2014 ${this.layoutId ?? ""}`);
340
+ }
341
+ layoutItems() {
342
+ const presets = (this.opts.layout?.options() ?? []).map((l) => ({ id: l.id, label: l.label, checked: l.id === this.layoutId }));
343
+ const toggles = (this.opts.layout?.toggles?.() ?? []).map((t, i) => ({ id: `toggle:${t.id}`, label: t.label, checked: t.checked, toggle: true, separatorBefore: i === 0 }));
344
+ return [...presets, ...toggles];
345
+ }
346
+ renderStyleButton(doc) {
347
+ this.styleButton.replaceChildren();
348
+ const icon2 = priceStyleIcon(this.priceStyle);
349
+ if (icon2) this.styleButton.appendChild(iconEl(icon2, doc));
350
+ else this.styleButton.appendChild(doc.createTextNode(priceStyleLabel(this.priceStyle)));
351
+ this.styleButton.setAttribute("aria-label", `Chart style \u2014 ${priceStyleLabel(this.priceStyle)}`);
352
+ }
353
+ /** Re-project the contributed topbar actions (call after registrations change). */
354
+ renderActions() {
355
+ const ctx = this.opts.getContext?.();
356
+ this.actionsHost.replaceChildren();
357
+ const doc = this.actionsHost.ownerDocument;
358
+ for (const action of widgetActions("topbar", ctx)) {
359
+ const b = doc.createElement("button");
360
+ b.className = "vela-widget-action";
361
+ if (action.icon) b.appendChild(iconEl(action.icon, doc));
362
+ b.appendChild(doc.createTextNode(action.label));
363
+ b.addEventListener("click", () => {
364
+ const c = this.opts.getContext?.();
365
+ if (c) action.run(c);
366
+ });
367
+ this.actionsHost.appendChild(b);
368
+ }
369
+ }
370
+ setIndicatorCount(_n) {
371
+ }
372
+ /** Enable/disable the undo and redo tools from the host's unified history. */
373
+ setHistoryState(canUndo, canRedo) {
374
+ this.undoBtn.disabled = !canUndo;
375
+ this.redoBtn.disabled = !canRedo;
376
+ }
377
+ setAlertCount(n) {
378
+ this.alertsBadge.textContent = n > 9 ? "9+" : String(n);
379
+ this.alertsBadge.style.display = n > 0 ? "" : "none";
380
+ }
381
+ /**
382
+ * Replace the side-panel toggle group — one icon button per docked panel, in the dock's own
383
+ * order. The dock calls this whenever its panel set changes (built-ins at construction,
384
+ * contributed panels on every `refreshActions()`), then pushes each pressed state.
385
+ */
386
+ setPanelButtons(buttons, onClick) {
387
+ for (const t of this.panelTooltips) t.destroy();
388
+ this.panelTooltips = [];
389
+ this.panelBtns.clear();
390
+ this.panelsHost.replaceChildren();
391
+ for (const b of buttons) {
392
+ const el = this.toolButton(`vela-widget-panel-${b.id}`, b.icon, b.title, () => onClick(b.id), this.panelTooltips);
393
+ this.panelBtns.set(b.id, el);
394
+ this.panelsHost.appendChild(el);
395
+ }
396
+ }
397
+ /** Reflect a docked side panel's open state on its button — the panels toggle each other,
398
+ * so the dock pushes the state rather than the button assuming it. */
399
+ setPanelActive(id, open) {
400
+ const btn = this.panelBtns.get(id);
401
+ if (btn) btn.dataset.active = open ? "1" : "";
402
+ }
403
+ destroy() {
404
+ this.hairlineRo?.disconnect();
405
+ const win = this.el.ownerDocument.defaultView;
406
+ win?.removeEventListener("resize", this.onHairlineSync);
407
+ if (this.hairlineRaf) win?.cancelAnimationFrame(this.hairlineRaf);
408
+ this.tfMenu.destroy();
409
+ this.styleMenu.destroy();
410
+ this.layoutMenu?.destroy();
411
+ for (const t of [...this.tooltips, ...this.panelTooltips]) t.destroy();
412
+ this.el.remove();
413
+ }
414
+ /** One icon-only tool button with its kit tooltip, parked in `sink` for disposal. */
415
+ toolButton(cls, icon2, tip, onClick, sink) {
416
+ const doc = this.el.ownerDocument;
417
+ const b = doc.createElement("button");
418
+ b.className = `vela-widget-tool ${cls}`;
419
+ b.appendChild(iconEl(icon2, doc));
420
+ b.setAttribute("aria-label", tip);
421
+ sink.push(new Tooltip(b, { content: tip, triggerId: `vela-tool-${cls}`, host: this.host }));
422
+ if (onClick) b.addEventListener("click", onClick);
423
+ return b;
424
+ }
425
+ /** Paint each `.vela-sep` as exactly one device pixel, snapped to the pixel grid. */
426
+ syncHairlines() {
427
+ const win = this.el.ownerDocument.defaultView;
428
+ if (!win) return;
429
+ const dpr = win.devicePixelRatio || 1;
430
+ for (const el of this.el.querySelectorAll(".vela-sep")) {
431
+ el.style.width = `${1 / dpr}px`;
432
+ el.style.transform = "";
433
+ const left = el.getBoundingClientRect().left;
434
+ const dx = Math.round(left * dpr) / dpr - left;
435
+ if (dx) el.style.transform = `translateX(${dx}px)`;
436
+ }
437
+ }
438
+ tfItems() {
439
+ return this.opts.timeframes.map((tf) => ({ id: tf, label: timeframeLabel(tf), checked: tf === this.timeframe }));
440
+ }
441
+ styleItems() {
442
+ return priceStyleIds().map((id) => ({
443
+ id,
444
+ label: priceStyleLabel(id),
445
+ icon: priceStyleIcon(id),
446
+ checked: id === this.priceStyle
447
+ }));
448
+ }
449
+ };
450
+
451
+ // src/widget/format.ts
452
+ function decimalsFor(ref) {
453
+ const a = Math.abs(ref);
454
+ return a >= 1 ? 2 : a >= 0.01 ? 4 : 6;
455
+ }
456
+ function fmtPrice(n, dp) {
457
+ if (n == null || !Number.isFinite(n)) return "\u2014";
458
+ const d = dp ?? decimalsFor(n);
459
+ return n.toLocaleString("en-US", { minimumFractionDigits: d, maximumFractionDigits: d });
460
+ }
461
+ function fmtChange(open, close) {
462
+ if (open == null || close == null || !Number.isFinite(open) || !Number.isFinite(close) || open === 0) return "";
463
+ const diff = close - open;
464
+ const pct = diff / open * 100;
465
+ const sign = diff >= 0 ? "+" : "";
466
+ return `${sign}${fmtPrice(diff, decimalsFor(close))} (${sign}${pct.toFixed(2)}%)`;
467
+ }
468
+
469
+ // src/widget/symbol-icon.ts
470
+ var iconFailed = /* @__PURE__ */ new Set();
471
+ function initialsOf(name) {
472
+ return (name || "?").replace(/[^A-Za-z0-9]/g, "").slice(0, 2).toUpperCase() || "?";
473
+ }
474
+ function cryptoIconUrl(base) {
475
+ return `https://crypto-icons.ledger.com/${encodeURIComponent(base.toUpperCase())}.png`;
476
+ }
477
+ function baseOf(d) {
478
+ const fromDesc = d.description?.split("/")[0]?.trim();
479
+ if (fromDesc) return fromDesc.replace(/\s+Perpetual$/i, "");
480
+ return d.ticker.replace(/[-_/]?(USDT|USDC|USD1|USDS|BUSD|USD|EUR|PERP)$/i, "") || d.ticker;
481
+ }
482
+ function tickerIconEl(doc, base, name, className) {
483
+ const wrap = doc.createElement("span");
484
+ wrap.className = className;
485
+ const key = base.toUpperCase();
486
+ const fallback = () => {
487
+ wrap.replaceChildren();
488
+ wrap.style.background = categoricalColor(name);
489
+ wrap.textContent = initialsOf(base || name);
490
+ };
491
+ if (!key || iconFailed.has(key)) {
492
+ fallback();
493
+ return wrap;
494
+ }
495
+ const img = doc.createElement("img");
496
+ img.alt = "";
497
+ img.src = cryptoIconUrl(key);
498
+ img.style.cssText = "width:100%;height:100%;border-radius:50%;display:block;object-fit:cover;";
499
+ img.addEventListener(
500
+ "error",
501
+ () => {
502
+ iconFailed.add(key);
503
+ fallback();
504
+ },
505
+ { once: true }
506
+ );
507
+ wrap.appendChild(img);
508
+ return wrap;
509
+ }
510
+
511
+ // src/widget/statusline.ts
512
+ var STYLE_ID2 = "vela-widget-statusline";
513
+ var CSS2 = `
514
+ .vela-statusline {
515
+ position: absolute;
516
+ top: var(--vela-space-2);
517
+ /* Track the indicator legend's left edge: the renderer publishes its toolbar gutter
518
+ * on the mount container, and the legend sits 10px into the plot to its right \u2014
519
+ * so the two columns stay aligned whether the toolbar is docked (44px), collapsed
520
+ * (16px), or absent entirely (a workspace cell: 0). */
521
+ left: calc(var(--vela-toolbar-gutter, 0px) + 10px);
522
+ z-index: 10;
523
+ display: flex;
524
+ align-items: baseline;
525
+ gap: var(--vela-space-2);
526
+ color: var(--vela-fg);
527
+ font-size: var(--vela-font-size-md);
528
+ /* Same chip treatment as the indicator legend rows (InputsUI): a translucent wash of
529
+ * the chart background when idle \u2014 enough to keep the readout legible when candles
530
+ * reach it \u2014 and the solid chart background on hover. Symmetric 7px padding with a
531
+ * compensating negative margin (mirroring the legend rows) keeps the avatar's left
532
+ * edge on the legend column's left edge (both at left:10px) while the chip itself
533
+ * extends 7px further left, so both columns' chips share the same left edge. */
534
+ pointer-events: auto;
535
+ background: color-mix(in srgb, var(--vela-bg) 60%, transparent);
536
+ border-radius: 4px;
537
+ padding: 2px 7px;
538
+ margin-left: -7px;
539
+ }
540
+ .vela-statusline:hover { background: var(--vela-bg); }
541
+ .vela-statusline .vela-sl-avatar {
542
+ width: 18px;
543
+ height: 18px;
544
+ border-radius: 50%;
545
+ flex: none;
546
+ align-self: center;
547
+ display: inline-flex;
548
+ align-items: center;
549
+ justify-content: center;
550
+ color: var(--vela-fg-on-fill);
551
+ font-size: 10px;
552
+ font-weight: 700;
553
+ }
554
+ .vela-statusline .vela-sl-symbol { font-weight: 600; font-size: var(--vela-font-size-lg); }
555
+ .vela-statusline .vela-sl-meta { color: var(--vela-fg-muted); font-size: var(--vela-font-size-md); font-weight: 600; }
556
+ /* Market status badge \u2014 icon-only 16px circle, label on hover (kit tooltip). */
557
+ .vela-statusline .vela-sl-market {
558
+ display: inline-grid;
559
+ place-items: center;
560
+ width: 16px;
561
+ height: 16px;
562
+ border-radius: 50%;
563
+ flex: none;
564
+ align-self: center;
565
+ line-height: 0;
566
+ cursor: default;
567
+ }
568
+ .vela-statusline .vela-sl-market svg {
569
+ width: 12px;
570
+ height: 12px;
571
+ display: block;
572
+ }
573
+ /* Open wears the theme's up color; the other sessions are meaning constants from the
574
+ * palette (amber pre, sky post, gray closed/holiday). Same ink at 20% for the circle. */
575
+ .vela-statusline .vela-sl-market[data-status='open'] {
576
+ background: color-mix(in srgb, var(--vela-up) 20%, transparent);
577
+ color: var(--vela-up);
578
+ }
579
+ .vela-statusline .vela-sl-market[data-status='pre'] { background: color-mix(in srgb, ${SESSION_PRE} 20%, transparent); color: ${SESSION_PRE}; }
580
+ .vela-statusline .vela-sl-market[data-status='post'] { background: color-mix(in srgb, ${SESSION_POST} 20%, transparent); color: ${SESSION_POST}; }
581
+ .vela-statusline .vela-sl-market[data-status='closed'],
582
+ .vela-statusline .vela-sl-market[data-status='holiday'] { background: color-mix(in srgb, ${SESSION_OFF} 20%, transparent); color: ${SESSION_OFF}; }
583
+ .vela-statusline .vela-sl-ohlc { display: flex; gap: var(--vela-space-1); color: var(--vela-fg-muted); }
584
+ .vela-statusline .vela-sl-ohlc b { color: var(--vela-fg); font-weight: 500; }
585
+ /* The change value wears the SAME ink as the OHLC values (set inline per render) \u2014
586
+ * these are the pre-ink fallbacks only. */
587
+ .vela-statusline .vela-sl-change[data-dir='up'] { color: var(--vela-up); }
588
+ .vela-statusline .vela-sl-change[data-dir='down'] { color: var(--vela-down); }
589
+ /* Stack the renderer's PRICE-pane legend below the status line (study panes stay put).
590
+ * The renderer sets the legend's inline top \u2014 shift with a transform, don't fight it.
591
+ * Scoped to hosts that actually CARRY a status line (the marker class set by the
592
+ * Statusline constructor) \u2014 the stylesheet is document-global, so a bare container
593
+ * class here would shift every chart on the page, including statusline-less ones. */
594
+ .vela-has-statusline [data-vela-pane='price'] { transform: translateY(26px); }
595
+ `;
596
+ function baseOfTicker(ticker) {
597
+ return ticker.replace(/[-_/]?(USDT|USDC|USD1|USDS|BUSD|USD|EUR|PERP)$/i, "") || ticker;
598
+ }
599
+ var MARKET_LABELS = {
600
+ open: "Market Open",
601
+ pre: "Pre-Market",
602
+ post: "Post-Market",
603
+ closed: "Market Closed",
604
+ holiday: "Market Holiday"
605
+ };
606
+ function statuslineInkOf(renderer, priceStyle) {
607
+ const cfg = renderer.getConfig();
608
+ const c = (v) => typeof v === "string" ? v : null;
609
+ switch (priceStyle) {
610
+ case "bars":
611
+ return [c(cfg?.bars?.upColor), c(cfg?.bars?.downColor), null, "ohlc"];
612
+ case "line": {
613
+ const v = c(cfg?.line?.color);
614
+ return [v, v, null, "value"];
615
+ }
616
+ case "area": {
617
+ const v = c(cfg?.area?.lineColor);
618
+ return [v, v, null, "value"];
619
+ }
620
+ case "baseline": {
621
+ const isUp = (bar) => {
622
+ const level = Number(renderer.get("baselinePrice"));
623
+ return Number.isFinite(level) ? bar.close >= level : bar.close >= bar.open;
624
+ };
625
+ return [c(cfg?.baseline?.topLineColor), c(cfg?.baseline?.bottomLineColor), isUp, "value"];
626
+ }
627
+ default:
628
+ return [c(cfg?.candles?.upColor), c(cfg?.candles?.downColor), null, "ohlc"];
629
+ }
630
+ }
631
+ var Statusline = class {
632
+ constructor(host, symbol) {
633
+ this.host = host;
634
+ this.parts = { name: true, market: true, ohlc: true, change: true };
635
+ this.lastBar = null;
636
+ this.hoverBar = null;
637
+ this.unsubs = [];
638
+ /** Up/down ink for the OHLC + change values — the ACTIVE price style's configured
639
+ * colors (candle bodies, bar ticks, the line color, …); null falls back to the theme
640
+ * tokens. `isUp` overrides the close-vs-open direction rule where the style paints by
641
+ * something else (baseline: position against the baseline price). */
642
+ this.upColor = null;
643
+ this.downColor = null;
644
+ this.isUp = null;
645
+ /** 'ohlc' for bar-shaped styles; 'value' (the single plotted close) for line styles. */
646
+ this.readout = "ohlc";
647
+ const doc = host.ownerDocument;
648
+ injectStyles(STYLE_ID2, CSS2, doc);
649
+ host.classList.add("vela-has-statusline");
650
+ this.el = doc.createElement("div");
651
+ this.el.className = "vela-statusline";
652
+ const ticker = parseSymbol(symbol).ticker;
653
+ this.avatarEl = tickerIconEl(doc, baseOfTicker(ticker), ticker, "vela-sl-avatar");
654
+ this.symbolEl = doc.createElement("span");
655
+ this.symbolEl.className = "vela-sl-symbol";
656
+ this.symbolEl.textContent = ticker;
657
+ this.metaEl = doc.createElement("span");
658
+ this.metaEl.className = "vela-sl-meta";
659
+ this.marketEl = doc.createElement("span");
660
+ this.marketEl.className = "vela-sl-market";
661
+ this.ohlcEl = doc.createElement("span");
662
+ this.ohlcEl.className = "vela-sl-ohlc";
663
+ this.changeEl = doc.createElement("span");
664
+ this.changeEl.className = "vela-sl-change";
665
+ this.el.append(this.avatarEl, this.symbolEl, this.metaEl, this.marketEl, this.ohlcEl, this.changeEl);
666
+ host.appendChild(this.el);
667
+ this.marketTip = new Tooltip(this.marketEl, { content: MARKET_LABELS.open, placement: "bottom" });
668
+ this.setMarketStatus("open");
669
+ this.render();
670
+ }
671
+ setSymbol(symbol) {
672
+ const ticker = parseSymbol(symbol).ticker;
673
+ this.symbolEl.textContent = ticker;
674
+ const fresh = tickerIconEl(this.el.ownerDocument, baseOfTicker(ticker), ticker, "vela-sl-avatar");
675
+ this.avatarEl.replaceWith(fresh);
676
+ this.avatarEl = fresh;
677
+ }
678
+ /** Shape + color the value readout after the active price style: its own up/down
679
+ * colors (candle bodies, bar ticks, the line color, …), the direction rule that
680
+ * picks between them (`isUp` replaces close-vs-open where the style paints by
681
+ * something else — baseline by position), and whether the readout is the four
682
+ * O/H/L/C values or the single plotted value (one-line styles). Null colors fall
683
+ * back to the theme's up/down tokens. See {@link statuslineInkOf}, which derives
684
+ * all of it from the live renderer. */
685
+ setDirectionColors(up, down, isUp = null, readout = "ohlc") {
686
+ this.upColor = up;
687
+ this.downColor = down;
688
+ this.isUp = isUp;
689
+ this.readout = readout;
690
+ this.render();
691
+ }
692
+ /** Show/hide one part (the settings dialog's Status line tab drives these). */
693
+ /** The "· BINANCE · 1h" segment after the symbol — venue first, then resolution. */
694
+ setMeta(timeframe, provider) {
695
+ this.metaEl.textContent = `${provider ? `\xB7 ${provider.toUpperCase()} ` : ""}\xB7 ${timeframeLabel(timeframe)}`;
696
+ }
697
+ /** Dress the market badge for a session state: its icon, tinted circle, and the
698
+ * hover label. Callers with no session model leave the constructor's 'open'. */
699
+ setMarketStatus(status) {
700
+ this.marketEl.dataset.status = status;
701
+ this.marketEl.innerHTML = icon(`market-${status}`);
702
+ this.marketEl.setAttribute("aria-label", MARKET_LABELS[status]);
703
+ this.marketTip.setContent(MARKET_LABELS[status]);
704
+ }
705
+ setPartVisible(part, visible) {
706
+ this.parts[part] = visible;
707
+ this.symbolEl.style.display = this.parts.name ? "" : "none";
708
+ this.marketEl.style.display = this.parts.market ? "" : "none";
709
+ this.ohlcEl.style.display = this.parts.ohlc ? "" : "none";
710
+ this.changeEl.style.display = this.parts.change ? "" : "none";
711
+ }
712
+ partVisible(part) {
713
+ return this.parts[part];
714
+ }
715
+ /** (Re)bind to a chart instance — called after every widget rebuild. */
716
+ onChart(chart) {
717
+ this.detach();
718
+ this.lastBar = null;
719
+ this.hoverBar = null;
720
+ this.unsubs.push(
721
+ chart.on("bar", (b) => {
722
+ this.lastBar = b;
723
+ if (!this.hoverBar) this.render();
724
+ }),
725
+ chart.renderer.onCrosshairMove((e) => {
726
+ this.hoverBar = e.ohlc;
727
+ this.render();
728
+ })
729
+ );
730
+ this.render();
731
+ }
732
+ destroy() {
733
+ this.detach();
734
+ this.marketTip.destroy();
735
+ this.host.classList.remove("vela-has-statusline");
736
+ this.el.remove();
737
+ }
738
+ detach() {
739
+ for (const u of this.unsubs) u();
740
+ this.unsubs = [];
741
+ }
742
+ render() {
743
+ const bar = this.hoverBar ?? this.lastBar;
744
+ if (!bar) {
745
+ this.ohlcEl.replaceChildren();
746
+ this.changeEl.textContent = "";
747
+ return;
748
+ }
749
+ const dp = decimalsFor(bar.close);
750
+ const doc = this.el.ownerDocument;
751
+ const up = this.isUp ? this.isUp(bar) : bar.close >= bar.open;
752
+ const ink = up ? this.upColor ?? "var(--vela-up)" : this.downColor ?? "var(--vela-down)";
753
+ const cell = (k, v) => {
754
+ const s = doc.createElement("span");
755
+ if (k) s.append(`${k} `);
756
+ const b = doc.createElement("b");
757
+ b.textContent = fmtPrice(v, dp);
758
+ b.style.color = ink;
759
+ s.appendChild(b);
760
+ return s;
761
+ };
762
+ if (this.readout === "value") this.ohlcEl.replaceChildren(cell("", bar.close));
763
+ else this.ohlcEl.replaceChildren(cell("O", bar.open), cell("H", bar.high), cell("L", bar.low), cell("C", bar.close));
764
+ this.changeEl.textContent = fmtChange(bar.open, bar.close);
765
+ this.changeEl.dataset.dir = up ? "up" : "down";
766
+ this.changeEl.style.color = ink;
767
+ }
768
+ };
769
+
770
+ // src/widget/watermark.ts
771
+ var STYLE_ID3 = "vela-widget-watermark";
772
+ var CSS3 = `
773
+ .vela-watermark {
774
+ position: absolute;
775
+ inset: 0;
776
+ display: flex;
777
+ align-items: center;
778
+ justify-content: center;
779
+ pointer-events: none;
780
+ z-index: 1;
781
+ color: var(--vela-fg);
782
+ opacity: 0.05;
783
+ font-size: clamp(28px, 7vw, 72px);
784
+ font-weight: 700;
785
+ letter-spacing: 0.04em;
786
+ user-select: none;
787
+ white-space: nowrap;
788
+ }
789
+ `;
790
+ var Watermark = class {
791
+ constructor(host, symbol, timeframe) {
792
+ injectStyles(STYLE_ID3, CSS3, host.ownerDocument);
793
+ this.el = host.ownerDocument.createElement("div");
794
+ this.el.className = "vela-watermark";
795
+ host.appendChild(this.el);
796
+ this.update(symbol, timeframe);
797
+ }
798
+ setVisible(visible) {
799
+ this.el.style.display = visible ? "" : "none";
800
+ }
801
+ update(symbol, timeframe) {
802
+ this.el.textContent = symbol ? `${parseSymbol(symbol).ticker} \xB7 ${timeframeLabel(timeframe)}` : "";
803
+ }
804
+ destroy() {
805
+ this.el.remove();
806
+ }
807
+ };
808
+
809
+ // src/widget/bottombar.ts
810
+ var RANGE_PRESETS = [
811
+ { id: "1D", tf: "1", preset: "1D", bars: 1500 },
812
+ // 1 day @ 1m = 1440 bars
813
+ { id: "7D", tf: "5", preset: "1W", bars: 2100 },
814
+ // 7 days @ 5m = 2016
815
+ { id: "1M", tf: "30", preset: "1M", bars: 1500 },
816
+ // 30 days @ 30m = 1440
817
+ { id: "3M", tf: "60", preset: "3M", bars: 2200 },
818
+ // 90 days @ 1h = 2160
819
+ { id: "6M", tf: "240", preset: "6M", bars: 1150 },
820
+ // 180 days @ 4h = 1080
821
+ { id: "YTD", tf: "D", preset: "YTD", bars: 400 },
822
+ // ≤366 days @ 1D
823
+ { id: "1Y", tf: "D", preset: "1Y", bars: 400 },
824
+ // 365 days @ 1D
825
+ { id: "5Y", tf: "W", preset: "5Y", bars: 300 },
826
+ // 5 years @ 1W = 261
827
+ { id: "ALL", tf: "W", preset: "ALL", bars: 5e3 }
828
+ // everything the provider serves
829
+ ];
830
+ var STYLE_ID4 = "vela-widget-bottombar";
831
+ var CSS4 = `
832
+ .vela-widget-bottombar {
833
+ display: flex;
834
+ align-items: center;
835
+ gap: 2px;
836
+ height: 38px;
837
+ padding: 0 8px;
838
+ border-top: 1px solid var(--vela-border);
839
+ color: var(--vela-fg-muted);
840
+ font-size: 12px;
841
+ flex: none;
842
+ }
843
+ .vela-bb-range {
844
+ all: unset;
845
+ height: 24px;
846
+ display: inline-flex;
847
+ align-items: center;
848
+ padding: 0 9px;
849
+ border-radius: 4px;
850
+ font-weight: 600;
851
+ cursor: pointer;
852
+ }
853
+ .vela-bb-range:hover { background: var(--vela-hover); color: var(--vela-fg-bright); }
854
+ .vela-bb-range[data-active='1'] { color: var(--vela-fg-bright); background: var(--vela-hover); }
855
+ .vela-bb-spacer { flex: 1 1 auto; }
856
+ .vela-bb-clock { font-variant-numeric: tabular-nums; color: var(--vela-fg-bright); font-weight: 600; }
857
+ .vela-bb-tz {
858
+ all: unset;
859
+ height: 26px;
860
+ display: inline-flex;
861
+ align-items: center;
862
+ gap: 8px;
863
+ padding: 0 8px;
864
+ border-radius: 4px;
865
+ font-weight: 600;
866
+ color: var(--vela-fg-bright);
867
+ cursor: pointer;
868
+ }
869
+ .vela-bb-tz:hover { background: var(--vela-hover); }
870
+ .vela-bb-session { display: inline-flex; border: 1px solid var(--vela-border-strong); border-radius: 4px; overflow: hidden; margin-left: 6px; }
871
+ .vela-bb-session-btn {
872
+ all: unset;
873
+ height: 24px;
874
+ display: inline-flex;
875
+ align-items: center;
876
+ padding: 0 8px;
877
+ color: var(--vela-fg-muted);
878
+ font-size: 11px;
879
+ font-weight: 600;
880
+ cursor: not-allowed;
881
+ opacity: 0.55;
882
+ }
883
+ .vela-bb-session-btn.is-active { color: var(--vela-fg); background: var(--vela-surface-elev); opacity: 0.8; }
884
+ .vela-bb-settings {
885
+ all: unset;
886
+ display: inline-flex;
887
+ align-items: center;
888
+ justify-content: center;
889
+ width: 28px;
890
+ height: 26px;
891
+ margin-left: 6px;
892
+ border-radius: 4px;
893
+ cursor: pointer;
894
+ color: var(--vela-fg-muted);
895
+ font-size: 14px;
896
+ }
897
+ .vela-bb-settings:hover { background: var(--vela-hover); color: var(--vela-fg-bright); }
898
+ `;
899
+ var Bottombar = class {
900
+ constructor(host, opts) {
901
+ this.settingsTip = null;
902
+ this.rangeButtons = /* @__PURE__ */ new Map();
903
+ this.timer = null;
904
+ this.timezone = opts.timezone;
905
+ const doc = host.ownerDocument;
906
+ injectStyles(STYLE_ID4, CSS4, doc);
907
+ this.el = doc.createElement("div");
908
+ this.el.className = "vela-widget-bottombar";
909
+ for (const preset of RANGE_PRESETS) {
910
+ const b = doc.createElement("button");
911
+ b.className = "vela-bb-range";
912
+ b.textContent = preset.id;
913
+ b.addEventListener("click", () => {
914
+ this.setActiveRange(preset.id);
915
+ opts.onRange(preset);
916
+ });
917
+ this.rangeButtons.set(preset.id, b);
918
+ this.el.appendChild(b);
919
+ }
920
+ const spacer = doc.createElement("span");
921
+ spacer.className = "vela-bb-spacer";
922
+ this.tzButton = doc.createElement("button");
923
+ this.tzButton.className = "vela-bb-tz";
924
+ this.tzButton.setAttribute("aria-label", "Time zone");
925
+ this.clockEl = doc.createElement("span");
926
+ this.clockEl.className = "vela-bb-clock";
927
+ this.tzLabelEl = doc.createElement("span");
928
+ this.tzLabelEl.textContent = tzButtonLabel(this.timezone);
929
+ this.tzButton.append(this.clockEl, this.tzLabelEl);
930
+ const session = doc.createElement("span");
931
+ session.className = "vela-bb-session";
932
+ session.title = "Session \u2014 stocks & ETFs only";
933
+ for (const [label, active] of [["RTH", true], ["ETH", false]]) {
934
+ const b = doc.createElement("button");
935
+ b.className = "vela-bb-session-btn" + (active ? " is-active" : "");
936
+ b.textContent = label;
937
+ b.disabled = true;
938
+ session.appendChild(b);
939
+ }
940
+ const settingsBtn = doc.createElement("button");
941
+ settingsBtn.className = "vela-bb-settings";
942
+ settingsBtn.appendChild(iconEl("gear", doc));
943
+ settingsBtn.setAttribute("aria-label", "Chart settings");
944
+ if (opts.onSettingsClick) settingsBtn.addEventListener("click", opts.onSettingsClick);
945
+ this.settingsTip = new Tooltip(settingsBtn, { content: "Chart settings", triggerId: "vela-bb-settings", host });
946
+ this.el.append(spacer, this.tzButton, session, settingsBtn);
947
+ host.appendChild(this.el);
948
+ this.tzMenu = new Menu({
949
+ trigger: this.tzButton,
950
+ triggerId: "vela-bb-tz",
951
+ host,
952
+ placement: "top-end",
953
+ items: this.tzItems(),
954
+ onSelect: (zone) => {
955
+ this.setTimezone(zone);
956
+ opts.onTimezone(zone);
957
+ }
958
+ });
959
+ this.tick();
960
+ this.timer = setInterval(() => this.tick(), 1e3);
961
+ }
962
+ setTimezone(zone) {
963
+ this.timezone = zone;
964
+ this.tzLabelEl.textContent = tzButtonLabel(zone);
965
+ this.tzMenu.setItems(this.tzItems());
966
+ this.tick();
967
+ }
968
+ /** Highlight (or clear with null) the active range chip — cleared on manual tf changes. */
969
+ setActiveRange(id) {
970
+ for (const [key, b] of this.rangeButtons) {
971
+ if (id !== null && key === id) b.dataset.active = "1";
972
+ else delete b.dataset.active;
973
+ }
974
+ }
975
+ destroy() {
976
+ if (this.timer !== null) clearInterval(this.timer);
977
+ this.tzMenu.destroy();
978
+ this.settingsTip?.destroy();
979
+ this.el.remove();
980
+ }
981
+ tzItems() {
982
+ return TIMEZONES.map((t) => ({
983
+ id: t.value,
984
+ label: tzMenuLabel(t.value, t.label),
985
+ checked: t.value === normalizeTimezone(this.timezone)
986
+ }));
987
+ }
988
+ tick() {
989
+ try {
990
+ this.clockEl.textContent = new Intl.DateTimeFormat("en-GB", {
991
+ hour: "2-digit",
992
+ minute: "2-digit",
993
+ second: "2-digit",
994
+ hour12: false,
995
+ timeZone: this.timezone
996
+ }).format(/* @__PURE__ */ new Date());
997
+ } catch {
998
+ this.clockEl.textContent = "";
999
+ }
1000
+ }
1001
+ };
1002
+
1003
+ // src/widget/symbol-picker.ts
1004
+ var TOP_TICKERS = ["BTCUSDT", "ETHUSDT", "SOLUSDT", "BNBUSDT", "XRPUSDT", "DOGEUSDT", "ADAUSDT", "LINKUSDT"];
1005
+ function parseQuery(raw, venues) {
1006
+ const m = raw.match(/^\s*([^\s:]+)\s*[:\s]\s*(.*)$/);
1007
+ if (m) {
1008
+ const t = m[1].toLowerCase();
1009
+ const scope = venues.includes(t) ? t : onlyOne(venues.filter((v) => v.startsWith(t)));
1010
+ if (scope) return { scope, term: m[2].trim() };
1011
+ }
1012
+ return { scope: null, term: raw.trim() };
1013
+ }
1014
+ function onlyOne(matches) {
1015
+ return matches.length === 1 ? matches[0] : null;
1016
+ }
1017
+ function filterSymbols(list, query, limit = 100) {
1018
+ const venues = [...new Set(list.map((s) => s.provider?.toLowerCase()).filter((p) => !!p))];
1019
+ const { scope, term } = parseQuery(query, venues);
1020
+ const pool = scope ? list.filter((s) => s.provider?.toLowerCase() === scope) : list;
1021
+ const q = term.toUpperCase();
1022
+ if (!q) {
1023
+ if (scope) return [...pool].sort((a, b) => a.ticker.localeCompare(b.ticker)).slice(0, limit);
1024
+ const byTicker = new Map(pool.map((s) => [s.ticker.toUpperCase(), s]));
1025
+ const top = TOP_TICKERS.map((t) => byTicker.get(t)).filter((s) => s !== void 0);
1026
+ const rest = pool.filter((s) => !TOP_TICKERS.includes(s.ticker.toUpperCase()));
1027
+ return [...top, ...rest].slice(0, limit);
1028
+ }
1029
+ const qLower = term.toLowerCase();
1030
+ const prefix = [];
1031
+ const substr = [];
1032
+ const desc = [];
1033
+ const venue = [];
1034
+ for (const s of pool) {
1035
+ const t = s.ticker.toUpperCase();
1036
+ if (t.startsWith(q)) prefix.push(s);
1037
+ else if (t.includes(q)) substr.push(s);
1038
+ else if ((s.description ?? "").toUpperCase().includes(q)) desc.push(s);
1039
+ else if (!scope && s.provider?.toLowerCase().includes(qLower)) venue.push(s);
1040
+ if (prefix.length >= limit) break;
1041
+ }
1042
+ return [...prefix, ...substr, ...desc, ...venue].slice(0, limit);
1043
+ }
1044
+ var STYLE_ID5 = "vela-widget-symbolpicker";
1045
+ var CSS5 = `
1046
+ .vela-sp-searchrow {
1047
+ display: flex;
1048
+ align-items: center;
1049
+ gap: 8px;
1050
+ height: 40px;
1051
+ padding: 0 12px;
1052
+ background: var(--vela-surface-elev);
1053
+ border: 1px solid var(--vela-border);
1054
+ border-radius: 8px;
1055
+ }
1056
+ .vela-sp-searchrow:focus-within { border-color: var(--vela-border-strong); }
1057
+ .vela-sp-searchrow .vela-icon { color: var(--vela-fg-muted); }
1058
+ .vela-sp-input {
1059
+ flex: 1;
1060
+ background: transparent;
1061
+ color: var(--vela-fg);
1062
+ border: none;
1063
+ font-size: 14px;
1064
+ outline: none;
1065
+ }
1066
+ .vela-sp-tabs { display: flex; gap: 14px; margin: 12px 2px 6px; border-bottom: 1px solid var(--vela-border); padding-bottom: 8px; }
1067
+ .vela-sp-tab {
1068
+ all: unset;
1069
+ padding: 3px 10px;
1070
+ border-radius: 5px;
1071
+ cursor: pointer;
1072
+ color: var(--vela-fg-muted);
1073
+ font-size: 13px;
1074
+ font-weight: 600;
1075
+ }
1076
+ .vela-sp-tab:hover { color: var(--vela-fg); }
1077
+ .vela-sp-tab[data-active] { background: var(--vela-selected-bg); color: var(--vela-selected-fg); }
1078
+ .vela-sp-list { margin-top: var(--vela-space-2); max-height: 46vh; overflow: auto; }
1079
+ .vela-sp-list::-webkit-scrollbar { width: 8px; }
1080
+ .vela-sp-list::-webkit-scrollbar-thumb {
1081
+ background: var(--vela-scroll);
1082
+ border-radius: 4px;
1083
+ border: 2px solid transparent;
1084
+ background-clip: padding-box;
1085
+ }
1086
+ .vela-sp-row {
1087
+ display: flex;
1088
+ align-items: center;
1089
+ gap: 12px;
1090
+ padding: 7px 10px;
1091
+ border-radius: 8px;
1092
+ cursor: pointer;
1093
+ }
1094
+ .vela-sp-row:hover, .vela-sp-row[data-highlighted] { background: var(--vela-hover); }
1095
+ .vela-sp-avatar {
1096
+ width: 28px;
1097
+ height: 28px;
1098
+ border-radius: 50%;
1099
+ flex: none;
1100
+ display: inline-flex;
1101
+ align-items: center;
1102
+ justify-content: center;
1103
+ color: var(--vela-fg-on-fill);
1104
+ font-size: var(--vela-font-size-md);
1105
+ font-weight: 700;
1106
+ }
1107
+ .vela-sp-main { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 1px; }
1108
+ .vela-sp-ticker { font-weight: 700; color: var(--vela-fg-bright); font-size: 14px; }
1109
+ .vela-sp-desc { color: var(--vela-fg-muted); font-size: 11px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
1110
+ .vela-sp-badge {
1111
+ flex: none;
1112
+ padding: 2px 8px;
1113
+ border-radius: 4px;
1114
+ background: var(--vela-surface-elev);
1115
+ border: 1px solid var(--vela-border);
1116
+ color: var(--vela-fg-muted);
1117
+ font-size: 10px;
1118
+ font-weight: 700;
1119
+ letter-spacing: 0.06em;
1120
+ text-transform: uppercase;
1121
+ }
1122
+ /* Provider brand marks \u2014 fixed by the venue, deliberately outside the theme palette. */
1123
+ .vela-sp-badge[data-p='binance'] { color: #f0b90b; } /* palette-exempt: venue brand mark */
1124
+ .vela-sp-badge[data-p='hyperliquid'] { color: #50d2c1; } /* palette-exempt: venue brand mark */
1125
+ .vela-sp-empty { padding: var(--vela-space-3); color: var(--vela-fg-muted); text-align: center; }
1126
+ `;
1127
+ var SymbolPicker = class {
1128
+ constructor(opts) {
1129
+ this.source = () => [];
1130
+ this.rows = [];
1131
+ this.highlighted = 0;
1132
+ this.seed = "";
1133
+ this.activeTab = "All";
1134
+ const doc = (opts.host ?? document.body).ownerDocument;
1135
+ injectStyles(STYLE_ID5, CSS5, doc);
1136
+ this.input = doc.createElement("input");
1137
+ this.input.className = "vela-sp-input";
1138
+ this.input.placeholder = "Search symbol\u2026";
1139
+ this.input.setAttribute("spellcheck", "false");
1140
+ const searchRow = doc.createElement("div");
1141
+ searchRow.className = "vela-sp-searchrow";
1142
+ searchRow.append(iconEl("search", doc), this.input);
1143
+ this.tabs = doc.createElement("div");
1144
+ this.tabs.className = "vela-sp-tabs";
1145
+ for (const t of ["All", "Stocks", "ETFs", "Crypto", "Forex", "Commodities"]) {
1146
+ const b = doc.createElement("button");
1147
+ b.className = "vela-sp-tab";
1148
+ b.textContent = t;
1149
+ if (t === this.activeTab) b.dataset.active = "1";
1150
+ b.addEventListener("click", () => {
1151
+ this.activeTab = t;
1152
+ for (const c of this.tabs.children) delete c.dataset.active;
1153
+ b.dataset.active = "1";
1154
+ this.refresh();
1155
+ });
1156
+ this.tabs.appendChild(b);
1157
+ }
1158
+ this.list = doc.createElement("div");
1159
+ this.list.className = "vela-sp-list";
1160
+ this.dialog = new Dialog({
1161
+ title: "Symbol Search",
1162
+ host: opts.host,
1163
+ closeOnInteractOutside: true,
1164
+ content: (body) => body.append(searchRow, this.tabs, this.list),
1165
+ onOpenChange: (open) => {
1166
+ if (open) {
1167
+ this.input.value = this.seed;
1168
+ this.seed = "";
1169
+ this.refresh();
1170
+ setTimeout(() => {
1171
+ this.input.focus();
1172
+ this.input.setSelectionRange(this.input.value.length, this.input.value.length);
1173
+ }, 0);
1174
+ }
1175
+ opts.onOpenChange?.(open);
1176
+ }
1177
+ });
1178
+ this.input.addEventListener("input", () => this.refresh());
1179
+ this.input.addEventListener("keydown", (e) => {
1180
+ if (e.key === "ArrowDown") this.moveHighlight(1);
1181
+ else if (e.key === "ArrowUp") this.moveHighlight(-1);
1182
+ else if (e.key === "Enter") {
1183
+ const pick = this.rows[this.highlighted];
1184
+ if (pick) this.select(pick.ticker, pick.provider, opts.onSelect);
1185
+ return;
1186
+ } else return;
1187
+ e.preventDefault();
1188
+ });
1189
+ this.list.addEventListener("click", (e) => {
1190
+ const row = e.target.closest(".vela-sp-row");
1191
+ if (row?.dataset.ticker) this.select(row.dataset.ticker, row.dataset.provider, opts.onSelect);
1192
+ });
1193
+ }
1194
+ /** Wire where symbols come from (re-called on every widget rebuild). */
1195
+ setSource(source) {
1196
+ this.source = source;
1197
+ }
1198
+ open(initialQuery = "") {
1199
+ this.seed = initialQuery;
1200
+ this.dialog.show();
1201
+ }
1202
+ close() {
1203
+ this.dialog.hide();
1204
+ }
1205
+ destroy() {
1206
+ this.dialog.destroy();
1207
+ }
1208
+ select(ticker, provider, onSelect) {
1209
+ this.close();
1210
+ onSelect(provider ? `${provider.toLowerCase()}:${ticker}` : ticker);
1211
+ }
1212
+ moveHighlight(delta) {
1213
+ if (!this.rows.length) return;
1214
+ this.highlighted = Math.min(this.rows.length - 1, Math.max(0, this.highlighted + delta));
1215
+ this.renderHighlight();
1216
+ }
1217
+ renderHighlight() {
1218
+ [...this.list.children].forEach((el, i) => {
1219
+ if (i === this.highlighted) {
1220
+ el.dataset.highlighted = "1";
1221
+ el.scrollIntoView({ block: "nearest" });
1222
+ } else delete el.dataset.highlighted;
1223
+ });
1224
+ }
1225
+ refresh() {
1226
+ const doc = this.list.ownerDocument;
1227
+ const TAB_TYPES = { Crypto: ["crypto"], Stocks: ["stock"], ETFs: ["etf"], Forex: ["forex"], Commodities: ["commodity"] };
1228
+ const pool = this.activeTab === "All" ? this.source() : this.source().filter((s) => TAB_TYPES[this.activeTab]?.includes((s.type ?? "").toLowerCase()) || this.activeTab === "Crypto" && (s.type ?? "").toLowerCase() === "futures");
1229
+ this.rows = filterSymbols(pool, this.input.value);
1230
+ this.highlighted = 0;
1231
+ this.list.replaceChildren();
1232
+ if (!this.rows.length) {
1233
+ const empty = doc.createElement("div");
1234
+ empty.className = "vela-sp-empty";
1235
+ empty.textContent = this.input.value ? "No symbols match." : "No symbols indexed (provider still loading?).";
1236
+ this.list.appendChild(empty);
1237
+ return;
1238
+ }
1239
+ for (const s of this.rows) {
1240
+ const row = doc.createElement("div");
1241
+ row.className = "vela-sp-row";
1242
+ row.dataset.ticker = s.ticker;
1243
+ if (s.provider) row.dataset.provider = s.provider;
1244
+ const av = tickerIconEl(doc, baseOf(s), s.ticker, "vela-sp-avatar");
1245
+ const main = doc.createElement("span");
1246
+ main.className = "vela-sp-main";
1247
+ const t = doc.createElement("span");
1248
+ t.className = "vela-sp-ticker";
1249
+ t.textContent = s.ticker;
1250
+ const d = doc.createElement("span");
1251
+ d.className = "vela-sp-desc";
1252
+ d.textContent = s.description ?? (s.type ?? "");
1253
+ main.append(t, d);
1254
+ row.append(av, main);
1255
+ if (s.provider) {
1256
+ const badge = doc.createElement("span");
1257
+ badge.className = "vela-sp-badge";
1258
+ badge.dataset.p = s.provider;
1259
+ badge.textContent = s.provider;
1260
+ row.appendChild(badge);
1261
+ }
1262
+ this.list.appendChild(row);
1263
+ }
1264
+ this.renderHighlight();
1265
+ }
1266
+ };
1267
+
1268
+ // src/widget/object-tree-model.ts
1269
+ var PRICE_PANE_ID = "price";
1270
+ var PRICE_PANE_LABEL = "Main chart";
1271
+ function drawingMeta(type) {
1272
+ const meta = getDrawingType(type);
1273
+ if (meta) return { label: meta.label, icon: meta.icon };
1274
+ return { label: String(type || "drawing").replace(/(^|\s)\S/g, (c) => c.toUpperCase()), icon: null };
1275
+ }
1276
+ function drawingLabel(d) {
1277
+ const { label } = drawingMeta(d.type);
1278
+ const text = d.text?.value?.trim();
1279
+ return text ? `${label} \u2014 ${text}` : label;
1280
+ }
1281
+ function paneLabel(pane, fallbackTitle) {
1282
+ if (pane.kind === "price") return PRICE_PANE_LABEL;
1283
+ const master = pane.indicators.find((i) => !i.ownScale) ?? pane.indicators[0];
1284
+ if (master) return indicatorLabel(master, fallbackTitle);
1285
+ return `Pane ${pane.order + 1}`;
1286
+ }
1287
+ function indicatorLabel(i, fallbackTitle) {
1288
+ return i.title || fallbackTitle(i.id) || i.id;
1289
+ }
1290
+ function paneRows(pane) {
1291
+ return pane.items.flatMap((it) => it.kind === "row" ? [it.row] : []);
1292
+ }
1293
+ function sameToken(a, b) {
1294
+ if (a.kind === "price" || b.kind === "price") return a.kind === b.kind;
1295
+ return a.kind === b.kind && a.id === b.id;
1296
+ }
1297
+ function unitTokens(u) {
1298
+ if (u.kind === "draw") return [{ kind: "drawing", id: u.drawing.id }];
1299
+ return u.members.map((m) => ({ kind: "drawing", id: m.id }));
1300
+ }
1301
+ function itemTokens(item) {
1302
+ if (item.kind === "unit") return unitTokens(item.unit);
1303
+ return [item.row.kind === "price" ? { kind: "price" } : { kind: "indicator", id: item.row.id }];
1304
+ }
1305
+ function paneTokens(pane) {
1306
+ return pane.items.flatMap(itemTokens);
1307
+ }
1308
+ function tokenIndexOfSlot(pane, slot) {
1309
+ let at = 0;
1310
+ for (let i = 0; i < Math.min(slot, pane.items.length); i += 1) at += itemTokens(pane.items[i]).length;
1311
+ return at;
1312
+ }
1313
+ function groupTokenIndex(pane, groupId, memberSlot) {
1314
+ let at = 0;
1315
+ for (const item of pane.items) {
1316
+ if (item.kind === "unit" && item.unit.kind === "group" && item.unit.group.id === groupId) {
1317
+ return at + Math.min(memberSlot, item.unit.members.length);
1318
+ }
1319
+ at += itemTokens(item).length;
1320
+ }
1321
+ return at;
1322
+ }
1323
+ function placeTokens(tokens, dragged, at) {
1324
+ const isDragged = (t) => dragged.some((d) => sameToken(d, t));
1325
+ let adjusted = at;
1326
+ tokens.forEach((t, i) => {
1327
+ if (i < at && isDragged(t)) adjusted -= 1;
1328
+ });
1329
+ const rest = tokens.filter((t) => !isDragged(t));
1330
+ rest.splice(Math.max(0, Math.min(adjusted, rest.length)), 0, ...dragged);
1331
+ return rest;
1332
+ }
1333
+ function tokensEqual(a, b) {
1334
+ return a.length === b.length && a.every((t, i) => sameToken(t, b[i]));
1335
+ }
1336
+ function stackWrites(tokens) {
1337
+ const n = tokens.length;
1338
+ let candleZ = null;
1339
+ const series = [];
1340
+ const drawings = [];
1341
+ tokens.forEach((t, i) => {
1342
+ const z = n - i;
1343
+ if (t.kind === "price") candleZ = z;
1344
+ else if (t.kind === "indicator") series.push({ id: t.id, z });
1345
+ else drawings.push({ id: t.id, z });
1346
+ });
1347
+ return { candleZ, series, drawings };
1348
+ }
1349
+ function zStackBounds(zOrder, candleZ, extra = []) {
1350
+ const zs = [...zOrder.map((e) => e.z), ...extra];
1351
+ return { top: Math.max(candleZ, 0, ...zs), bottom: Math.min(candleZ, 0, ...zs) };
1352
+ }
1353
+ function paneDrawings(drawings, paneId, paneIds) {
1354
+ const mine = drawings.filter((d) => paneId === PRICE_PANE_ID ? d.paneId === PRICE_PANE_ID || !paneIds.has(d.paneId) : d.paneId === paneId);
1355
+ return mine.slice().reverse();
1356
+ }
1357
+ function groupOf(groups, drawingId) {
1358
+ return groups.find((g) => g.ids.includes(drawingId)) ?? null;
1359
+ }
1360
+ function groupState(members) {
1361
+ return {
1362
+ allHidden: members.length > 0 && members.every((d) => d.visible === false),
1363
+ allLocked: members.length > 0 && members.every((d) => d.locked === true)
1364
+ };
1365
+ }
1366
+ function canGroup(ids, groups) {
1367
+ return ids.length > 0 && ids.every((id) => groupOf(groups, id) === null);
1368
+ }
1369
+ function nextGroupName(groups) {
1370
+ const taken = new Set(groups.map((g) => g.name));
1371
+ for (let n = 1; n <= groups.length; n += 1) {
1372
+ const name = `Group ${n}`;
1373
+ if (!taken.has(name)) return name;
1374
+ }
1375
+ return `Group ${groups.length + 1}`;
1376
+ }
1377
+ function withMembers(groups, keep) {
1378
+ return groups.map((g) => ({ ...g, ids: g.ids.filter(keep) })).filter((g) => g.ids.length > 0);
1379
+ }
1380
+ function pruneGroups(groups, live) {
1381
+ return withMembers(groups, (id) => live.has(id));
1382
+ }
1383
+ function removeFromGroups(groups, ids) {
1384
+ const drop = new Set(ids);
1385
+ return withMembers(groups, (id) => !drop.has(id));
1386
+ }
1387
+ function assignToGroup(groups, groupId, ids) {
1388
+ const moving = new Set(ids);
1389
+ const next = groups.map((g) => {
1390
+ const kept = g.ids.filter((id) => !moving.has(id));
1391
+ return { ...g, ids: g.id === groupId ? [...kept, ...ids] : kept };
1392
+ });
1393
+ return next.filter((g) => g.ids.length > 0);
1394
+ }
1395
+ function drawingUnits(paneDraws, groups) {
1396
+ const units = [];
1397
+ const seen = /* @__PURE__ */ new Set();
1398
+ for (const d of paneDraws) {
1399
+ if (seen.has(d.id)) continue;
1400
+ const g = groupOf(groups, d.id);
1401
+ if (g) {
1402
+ const members = paneDraws.filter((x) => g.ids.includes(x.id));
1403
+ for (const m of members) seen.add(m.id);
1404
+ units.push({ kind: "group", group: g, members });
1405
+ } else {
1406
+ seen.add(d.id);
1407
+ units.push({ kind: "draw", drawing: d });
1408
+ }
1409
+ }
1410
+ return units;
1411
+ }
1412
+ function unitZ(u) {
1413
+ if (u.kind === "draw") return u.drawing.zIndex;
1414
+ return u.members[0]?.zIndex ?? 0;
1415
+ }
1416
+ function paneItems(snap, pane, units) {
1417
+ const priceRow = () => ({ kind: "price", label: snap.priceLabel, visible: snap.priceVisible });
1418
+ const indicatorRow = (i) => ({
1419
+ kind: "indicator",
1420
+ id: i.id,
1421
+ label: indicatorLabel(i, snap.handleTitle),
1422
+ visible: snap.indicatorVisible(i.id),
1423
+ ownScale: i.ownScale
1424
+ });
1425
+ const unitItems = units.map((unit) => ({ kind: "unit", unit }));
1426
+ if (!snap.stackable) {
1427
+ const rows2 = pane.indicators.map((i) => ({ kind: "row", row: indicatorRow(i) }));
1428
+ if (pane.kind === "price") rows2.unshift({ kind: "row", row: priceRow() });
1429
+ return [...unitItems, ...rows2];
1430
+ }
1431
+ const zOf = new Map(snap.zOrder.map((e) => [e.id, e.z]));
1432
+ const rows = pane.indicators.map((i) => ({ item: { kind: "row", row: indicatorRow(i) }, z: zOf.get(i.id) ?? 0 }));
1433
+ if (pane.kind === "price") rows.push({ item: { kind: "row", row: priceRow() }, z: snap.candleZ });
1434
+ rows.sort((a, b) => b.z - a.z);
1435
+ if (!snap.interleave) return [...unitItems, ...rows.map((r) => r.item)];
1436
+ const entries = [...rows, ...units.map((unit) => ({ item: { kind: "unit", unit }, z: unitZ(unit) }))];
1437
+ entries.sort((a, b) => a.z === b.z ? Number(a.item.kind === "unit") - Number(b.item.kind === "unit") : b.z - a.z);
1438
+ return entries.map((e) => e.item);
1439
+ }
1440
+ function buildTree(snap) {
1441
+ const paneIds = new Set(snap.panes.map((p) => p.id));
1442
+ return snap.panes.map((pane) => ({
1443
+ id: pane.id,
1444
+ kind: pane.kind,
1445
+ label: paneLabel(pane, snap.handleTitle),
1446
+ order: pane.order,
1447
+ collapsed: pane.collapsed,
1448
+ maximized: pane.maximized,
1449
+ items: paneItems(snap, pane, drawingUnits(paneDrawings(snap.drawings, pane.id, paneIds), snap.groups))
1450
+ }));
1451
+ }
1452
+ function treeIsEmpty(panes) {
1453
+ return panes.every((p) => p.items.every((it) => it.kind === "row" && it.row.kind === "price"));
1454
+ }
1455
+
1456
+ // src/widget/object-tree.ts
1457
+ var STYLE_ID6 = "vela-widget-objtree";
1458
+ var CSS6 = `
1459
+ /* One pane's block. The transparent border reserves the drop-target outline. */
1460
+ .vela-ot-pane {
1461
+ border: 1px solid transparent;
1462
+ border-radius: 6px;
1463
+ margin: 2px 0;
1464
+ }
1465
+ .vela-ot-panehead { display: flex; align-items: center; gap: 2px; padding: 5px 6px; }
1466
+ .vela-ot-panename {
1467
+ flex: 1;
1468
+ min-width: 0;
1469
+ overflow: hidden;
1470
+ text-overflow: ellipsis;
1471
+ white-space: nowrap;
1472
+ color: var(--vela-fg-muted);
1473
+ font-size: 10px;
1474
+ font-weight: 700;
1475
+ text-transform: uppercase;
1476
+ letter-spacing: 0.08em;
1477
+ }
1478
+ .vela-ot-panesep { height: 1px; background: var(--vela-border); margin: 6px 8px; }
1479
+
1480
+ .vela-ot-row {
1481
+ display: flex;
1482
+ align-items: center;
1483
+ gap: 9px;
1484
+ padding: 6px 6px;
1485
+ border-radius: 6px;
1486
+ cursor: default;
1487
+ }
1488
+ .vela-ot-row:hover { background: var(--vela-hover); }
1489
+ .vela-ot-row > .vela-icon { color: var(--vela-fg-muted); width: 14px; height: 14px; font-size: 14px; justify-content: center; flex: none; }
1490
+ /* Drawing glyphs come from the type registry at toolbar scale \u2014 bring them down to row size. */
1491
+ .vela-ot-row > .vela-icon svg { width: 14px; height: 14px; }
1492
+ .vela-ot-name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
1493
+ .vela-ot-row[data-hidden] .vela-ot-name,
1494
+ .vela-ot-row[data-hidden] > .vela-icon { opacity: 0.45; }
1495
+ /* Two different states, and both can be true at once: "picked" is what the panel has selected
1496
+ (the group/duplicate candidates), "selected" mirrors what the CHART has selected. */
1497
+ .vela-ot-row[data-picked] { background: var(--vela-active); }
1498
+ .vela-ot-row[data-picked] .vela-ot-name { color: var(--vela-fg-bright); }
1499
+ .vela-ot-row[data-selected] { box-shadow: inset 2px 0 0 var(--vela-accent); }
1500
+ .vela-ot-avatar {
1501
+ width: 14px;
1502
+ height: 14px;
1503
+ border-radius: 50%;
1504
+ flex: none;
1505
+ display: inline-flex;
1506
+ align-items: center;
1507
+ justify-content: center;
1508
+ color: var(--vela-fg-on-fill);
1509
+ font-size: 10px;
1510
+ font-weight: 700;
1511
+ }
1512
+ /* "scale": this indicator draws against its own price scale, not the pane's. */
1513
+ .vela-ot-tag {
1514
+ flex: none;
1515
+ font-size: 9px;
1516
+ font-weight: 600;
1517
+ text-transform: uppercase;
1518
+ letter-spacing: 0.04em;
1519
+ color: var(--vela-fg-muted);
1520
+ background: var(--vela-hover);
1521
+ border-radius: 3px;
1522
+ padding: 1px 4px;
1523
+ }
1524
+ /* One row's actions, kept in a tight cluster: they read as one control group, and the row's
1525
+ own wider gap stays between the label and them. */
1526
+ .vela-ot-acts { display: flex; align-items: center; gap: 0; flex: none; }
1527
+ .vela-ot-btn {
1528
+ all: unset;
1529
+ cursor: pointer;
1530
+ flex: none;
1531
+ width: 20px;
1532
+ height: 20px;
1533
+ display: inline-flex;
1534
+ align-items: center;
1535
+ justify-content: center;
1536
+ border-radius: 3px;
1537
+ color: var(--vela-fg-muted);
1538
+ font-size: 12px;
1539
+ visibility: hidden;
1540
+ }
1541
+ .vela-ot-row:hover .vela-ot-btn,
1542
+ .vela-ot-panehead:hover .vela-ot-btn { visibility: visible; }
1543
+ /* An engaged action stays out: hidden and locked are states, and a state the user can only
1544
+ see by hovering is a state they will not find. */
1545
+ .vela-ot-row .vela-ot-btn[data-engaged] { visibility: visible; color: var(--vela-fg); }
1546
+ .vela-ot-btn:hover:not(:disabled) { background: var(--vela-active); color: var(--vela-fg-bright); }
1547
+ .vela-ot-btn:disabled { opacity: 0.35; cursor: default; }
1548
+ .vela-ot-empty { padding: 20px 10px; text-align: center; color: var(--vela-fg-muted); font-size: 12px; }
1549
+
1550
+ /* \u2500\u2500 drawing groups \u2500\u2500 */
1551
+ /* One top-level entry in a pane's drawing list: a lone drawing, or a whole group block. The
1552
+ transparent border reserves the outline used when a drawing is dropped into the group. */
1553
+ .vela-ot-unit { border: 1px solid transparent; border-radius: 6px; }
1554
+ .vela-ot-row[data-row-kind='group'] > .vela-icon { width: 12px; font-size: 11px; }
1555
+ /* A member sits indented under its group's header. */
1556
+ .vela-ot-subrow { padding-left: 26px; }
1557
+ .vela-ot-rename {
1558
+ flex: 1;
1559
+ min-width: 0;
1560
+ padding: 1px 4px;
1561
+ border: 1px solid var(--vela-accent);
1562
+ border-radius: var(--vela-radius-sm);
1563
+ background: var(--vela-surface-elev);
1564
+ color: var(--vela-fg-bright);
1565
+ font: inherit;
1566
+ }
1567
+ /* The selection bar. Always there \u2014 the actions it holds are the panel's, not a row's, so they
1568
+ stay in place and simply go dim until a drawing is selected. It stays put at the top of the
1569
+ list while the list scrolls under it, so they never scroll out of reach. */
1570
+ .vela-ot-selbar {
1571
+ position: sticky;
1572
+ top: -8px;
1573
+ z-index: 2;
1574
+ display: flex;
1575
+ align-items: center;
1576
+ gap: 2px;
1577
+ margin: -8px -8px 6px;
1578
+ padding: 6px 8px;
1579
+ border-bottom: 1px solid var(--vela-border);
1580
+ background: var(--vela-bg);
1581
+ }
1582
+ .vela-ot-selcount { flex: 1; min-width: 0; color: var(--vela-fg-muted); font-size: 11px; }
1583
+ .vela-ot-selbar .vela-ot-btn { visibility: visible; width: 24px; height: 22px; }
1584
+ .vela-ot-selbar .vela-ot-btn[data-icon='group'] .vela-icon { width: 16px; height: 16px; font-size: 16px; }
1585
+
1586
+ /* \u2500\u2500 drag-and-drop \u2500\u2500 */
1587
+ .vela-ot-row[data-drag] { cursor: grab; }
1588
+ .vela-ot .vela-panel-body[data-dragging] .vela-ot-row[data-drag] { cursor: grabbing; }
1589
+ /* The row in flight fades: the ghost under the pointer is the thing being moved. */
1590
+ .vela-ot-row[data-source] { opacity: 0.4; }
1591
+ /* Buttons would only invite a click that a drag is about to swallow. */
1592
+ .vela-ot .vela-panel-body[data-dragging] .vela-ot-btn { visibility: hidden; }
1593
+
1594
+ /* The band between two pane blocks: a hairline at rest, a bright bar when dropping there
1595
+ would open a new pane. Doubles as the plain separator when dragging isn't available. */
1596
+ .vela-ot-gap { position: relative; height: 11px; border-radius: 3px; margin: 0 8px; }
1597
+ .vela-ot-gap::before {
1598
+ content: '';
1599
+ position: absolute;
1600
+ left: 0;
1601
+ right: 0;
1602
+ top: 50%;
1603
+ height: 1px;
1604
+ background: var(--vela-border);
1605
+ transform: translateY(-50%);
1606
+ }
1607
+ .vela-ot-gap[data-drop] { height: 4px; background: var(--vela-fg-bright); }
1608
+ .vela-ot-gap[data-drop]::before { display: none; }
1609
+ /* A whole container accepts the drop: merging into a pane, or a pane with no drawings yet.
1610
+ The pane block's transparent border reserves the room for this outline. */
1611
+ .vela-ot [data-drop='target'] { border-color: var(--vela-fg-bright); background: var(--vela-hover); }
1612
+ /* Where a reorder would insert. */
1613
+ .vela-ot [data-drop='before'] { box-shadow: inset 0 2px 0 var(--vela-fg-bright); }
1614
+ .vela-ot [data-drop='after'] { box-shadow: inset 0 -2px 0 var(--vela-fg-bright); }
1615
+
1616
+ .vela-ot-ghost {
1617
+ position: fixed;
1618
+ z-index: 9999;
1619
+ pointer-events: none;
1620
+ max-width: 220px;
1621
+ padding: 3px 10px;
1622
+ border: 1px solid var(--vela-fg-bright);
1623
+ border-radius: var(--vela-radius-sm);
1624
+ background: var(--vela-surface-overlay);
1625
+ color: var(--vela-fg);
1626
+ box-shadow: var(--vela-shadow);
1627
+ font-size: 12px;
1628
+ overflow: hidden;
1629
+ text-overflow: ellipsis;
1630
+ white-space: nowrap;
1631
+ }
1632
+ `;
1633
+ var REFRESH_EVENTS = [
1634
+ "indicator:added",
1635
+ "indicator:removed",
1636
+ "indicator:moved",
1637
+ "indicator:visibility",
1638
+ "pane:changed",
1639
+ "drawing:created",
1640
+ "drawing:edited",
1641
+ "drawing:removed"
1642
+ ];
1643
+ var DRAG_SLOP = 4;
1644
+ function insertionSlot(els, y) {
1645
+ for (let i = 0; i < els.length; i += 1) {
1646
+ const r = els[i].getBoundingClientRect();
1647
+ if (y < r.top + r.height / 2) return i;
1648
+ }
1649
+ return els.length;
1650
+ }
1651
+ var MenuBuilder = class {
1652
+ constructor() {
1653
+ this.actions = /* @__PURE__ */ new Map();
1654
+ this.seq = 0;
1655
+ }
1656
+ entry(label, icon2, run, separatorBefore = false) {
1657
+ const id = this.next();
1658
+ this.actions.set(id, run);
1659
+ return { id, label, icon: icon2, separatorBefore };
1660
+ }
1661
+ submenu(label, icon2, submenu, separatorBefore = false) {
1662
+ return { id: this.next(), label, icon: icon2, submenu, separatorBefore };
1663
+ }
1664
+ next() {
1665
+ this.seq += 1;
1666
+ return `ot${this.seq}`;
1667
+ }
1668
+ };
1669
+ var ObjectTree = class extends SidePanel {
1670
+ constructor(host) {
1671
+ super(host, "Object tree", "vela-ot");
1672
+ this.chart = null;
1673
+ this.selectedDrawing = null;
1674
+ this.symbolName = "";
1675
+ /** Drawing bundles — a view-side grouping, held for the panel's lifetime and never persisted.
1676
+ * Kept per chart because a workspace points this one panel at whichever chart is active, and
1677
+ * each chart's bundles have to survive the switch. */
1678
+ this.groupsPerChart = /* @__PURE__ */ new WeakMap();
1679
+ /** Group ids currently folded shut. Ids are never reused, so entries left behind by a group
1680
+ * that is gone are inert and can outlive it. */
1681
+ this.collapsed = /* @__PURE__ */ new Set();
1682
+ /** Drawings picked IN THE PANEL — what the selection bar and "group selection" act on. */
1683
+ this.picked = /* @__PURE__ */ new Set();
1684
+ /** The group showing its rename field, if any. */
1685
+ this.renaming = null;
1686
+ /** Set while the panel pushes its pick to the chart, so the `drawing:selected` that comes
1687
+ * back is recognised as our own echo instead of a fresh chart-side selection. */
1688
+ this.syncingSelection = false;
1689
+ this.groupSeq = 0;
1690
+ this.unsubs = [];
1691
+ /** The last render's model, kept so a right-click reads the same state the rows show. */
1692
+ this.pass = null;
1693
+ this.menu = null;
1694
+ this.menuActions = /* @__PURE__ */ new Map();
1695
+ this.drag = null;
1696
+ injectStyles(STYLE_ID6, CSS6, host.ownerDocument);
1697
+ this.body.addEventListener("contextmenu", (e) => this.onContextMenu(e));
1698
+ this.body.addEventListener("pointerdown", (e) => this.onPointerDown(e));
1699
+ }
1700
+ get groups() {
1701
+ return (this.chart === null ? void 0 : this.groupsPerChart.get(this.chart)) ?? [];
1702
+ }
1703
+ set groups(next) {
1704
+ if (this.chart) this.groupsPerChart.set(this.chart, next);
1705
+ }
1706
+ toggle(open = this.el.hidden) {
1707
+ super.toggle(open);
1708
+ if (open) this.refresh();
1709
+ }
1710
+ setSymbol(symbol) {
1711
+ this.symbolName = parseSymbol(symbol).ticker;
1712
+ }
1713
+ /** (Re)bind to a chart instance — called after every widget rebuild. */
1714
+ onChart(chart) {
1715
+ this.detach();
1716
+ this.chart = chart;
1717
+ for (const ev of REFRESH_EVENTS) {
1718
+ this.unsubs.push(chart.on(ev, () => this.refresh()));
1719
+ }
1720
+ this.unsubs.push(
1721
+ chart.on("drawing:selected", ({ id }) => {
1722
+ this.selectedDrawing = id;
1723
+ if (!this.syncingSelection) {
1724
+ this.picked.clear();
1725
+ if (id) this.picked.add(id);
1726
+ }
1727
+ this.refresh();
1728
+ })
1729
+ );
1730
+ this.refresh();
1731
+ }
1732
+ destroy() {
1733
+ this.detach();
1734
+ this.endDrag(false);
1735
+ this.menu?.destroy();
1736
+ this.menu = null;
1737
+ super.destroy();
1738
+ }
1739
+ detach() {
1740
+ for (const u of this.unsubs) u();
1741
+ this.unsubs = [];
1742
+ this.chart = null;
1743
+ }
1744
+ /** Read the chart into the layout's input. Every chart query happens here, once a pass. */
1745
+ snapshot(chart, handle) {
1746
+ const stackable = chart.renderer.supports("seriesOrder") && chart.renderer.supports("candleZOrder");
1747
+ return {
1748
+ panes: chart.panes.list(),
1749
+ indicatorVisible: (id) => handle(id)?.visible !== false,
1750
+ handleTitle: (id) => handle(id)?.title,
1751
+ stackable,
1752
+ interleave: stackable && chart.drawings.supported && chart.renderer.capabilities.drawingDepth === true,
1753
+ zOrder: stackable ? chart.renderer.get("seriesOrder") ?? [] : [],
1754
+ candleZ: stackable ? Number(chart.renderer.get("candleZOrder")) || 0 : 0,
1755
+ priceLabel: this.symbolName || "Price",
1756
+ priceVisible: chart.renderer.get("candleVisible") !== false,
1757
+ drawings: chart.drawings.supported ? chart.drawings.all() : [],
1758
+ groups: this.groups
1759
+ };
1760
+ }
1761
+ /**
1762
+ * Repaint because something changed — unless the panel is mid-interaction. A live drag
1763
+ * resolves drops against the rows as currently rendered, and a rename lives in an input
1764
+ * that a repaint would destroy mid-word. Both end by painting themselves.
1765
+ */
1766
+ refresh() {
1767
+ if (this.drag?.active === true || this.renaming !== null) return;
1768
+ this.render();
1769
+ }
1770
+ render() {
1771
+ if (this.el.hidden || !this.chart) return;
1772
+ const chart = this.chart;
1773
+ this.prune(chart);
1774
+ const byId = new Map(chart.indicators().map((h) => [h.id, h]));
1775
+ const snap = this.snapshot(chart, (id) => byId.get(id));
1776
+ const panes = buildTree(snap);
1777
+ const pass = { chart, handle: (id) => byId.get(id), stackable: snap.stackable, repanable: chart.panes.supported, interleave: snap.interleave, panes };
1778
+ this.pass = pass;
1779
+ this.body.replaceChildren();
1780
+ const doc = this.el.ownerDocument;
1781
+ this.body.appendChild(this.selectionBar(pass));
1782
+ panes.forEach((pane, i) => {
1783
+ if (i > 0) this.body.appendChild(this.gapEl(pass, panes[i - 1].id, pane.id));
1784
+ this.body.appendChild(this.paneBlock(pass, pane));
1785
+ });
1786
+ const last = panes[panes.length - 1];
1787
+ if (last && pass.repanable) this.body.appendChild(this.gapEl(pass, last.id, void 0));
1788
+ if (treeIsEmpty(panes)) {
1789
+ const empty = doc.createElement("div");
1790
+ empty.className = "vela-ot-empty";
1791
+ empty.textContent = "Add an indicator or a drawing to populate the tree";
1792
+ this.body.appendChild(empty);
1793
+ }
1794
+ }
1795
+ /** What the panel itself can do to the selected drawings: bundle them into a group, or
1796
+ * duplicate all of them. Always in place, dim until there is a selection to act on. */
1797
+ selectionBar(pass) {
1798
+ const doc = this.el.ownerDocument;
1799
+ const bar = doc.createElement("div");
1800
+ bar.className = "vela-ot-selbar";
1801
+ const ids = [...this.picked];
1802
+ const groupable = canGroup(ids, this.groups);
1803
+ bar.appendChild(
1804
+ this.btn({
1805
+ icon: "group",
1806
+ title: ids.length === 0 ? "Group the selected drawings" : groupable ? `Group ${ids.length === 1 ? "this drawing" : "these drawings"}` : "Already in a group",
1807
+ disabled: !groupable,
1808
+ run: () => this.makeGroup(ids)
1809
+ })
1810
+ );
1811
+ bar.appendChild(
1812
+ this.btn({
1813
+ icon: "clone",
1814
+ title: ids.length === 0 ? "Duplicate the selected drawings" : "Duplicate",
1815
+ disabled: ids.length === 0,
1816
+ run: () => {
1817
+ for (const id of ids) this.cloneInto(pass, id);
1818
+ this.render();
1819
+ }
1820
+ })
1821
+ );
1822
+ const count = doc.createElement("span");
1823
+ count.className = "vela-ot-selcount";
1824
+ count.textContent = ids.length > 1 ? `${ids.length} selected` : "";
1825
+ bar.appendChild(count);
1826
+ return bar;
1827
+ }
1828
+ /** The band between two pane blocks. With re-paning available it is a drop zone that opens
1829
+ * a fresh pane there; otherwise it is just the separator. `before`/`after` name the panes
1830
+ * it sits between (either end is open at the edges of the list). */
1831
+ gapEl(pass, after, before) {
1832
+ const el = this.el.ownerDocument.createElement("div");
1833
+ if (!pass.repanable) {
1834
+ el.className = "vela-ot-panesep";
1835
+ return el;
1836
+ }
1837
+ el.className = "vela-ot-gap";
1838
+ if (before !== void 0) el.dataset.before = before;
1839
+ if (after !== void 0) el.dataset.after = after;
1840
+ return el;
1841
+ }
1842
+ /**
1843
+ * A pane, read top to bottom as front to back: ONE column holding its drawings, its
1844
+ * indicators and (in the main pane) the candles, in draw order. Every child of the stack
1845
+ * element is one slot, so a drop position is read straight off the rendered geometry.
1846
+ */
1847
+ paneBlock(pass, pane) {
1848
+ const doc = this.el.ownerDocument;
1849
+ const block = doc.createElement("div");
1850
+ block.className = "vela-ot-pane";
1851
+ block.dataset.pane = pane.id;
1852
+ block.dataset.kind = pane.kind;
1853
+ block.appendChild(this.paneHead(pass, pane));
1854
+ const stack = doc.createElement("div");
1855
+ stack.className = "vela-ot-stack";
1856
+ for (const item of pane.items) {
1857
+ stack.appendChild(item.kind === "row" ? this.rowEl(pass, pane, item.row) : this.unitEl(pass, pane, item.unit));
1858
+ }
1859
+ block.appendChild(stack);
1860
+ return block;
1861
+ }
1862
+ /** One top-level drawing entry: a lone drawing's row, or a group block — its header plus,
1863
+ * unfolded, a member row for each drawing it holds. */
1864
+ unitEl(pass, pane, unit) {
1865
+ const doc = this.el.ownerDocument;
1866
+ const wrap = doc.createElement("div");
1867
+ wrap.className = "vela-ot-unit";
1868
+ if (unit.kind === "draw") {
1869
+ wrap.appendChild(this.drawRow(pass, unit.drawing));
1870
+ } else {
1871
+ wrap.dataset.group = unit.group.id;
1872
+ wrap.appendChild(this.groupRow(pass, pane, unit.group));
1873
+ if (!this.collapsed.has(unit.group.id)) {
1874
+ for (const m of unit.members) wrap.appendChild(this.drawRow(pass, m, true));
1875
+ }
1876
+ }
1877
+ return wrap;
1878
+ }
1879
+ /** The pane's name plus its ops: a study pane can move in the stack and collapse; every
1880
+ * pane can be maximized. */
1881
+ paneHead(pass, pane) {
1882
+ const doc = this.el.ownerDocument;
1883
+ const { chart } = pass;
1884
+ const head = doc.createElement("div");
1885
+ head.className = "vela-ot-panehead";
1886
+ const name = doc.createElement("span");
1887
+ name.className = "vela-ot-panename";
1888
+ name.textContent = pane.label;
1889
+ name.title = pane.label;
1890
+ head.appendChild(name);
1891
+ if (!chart.panes.supported) return head;
1892
+ const acts = doc.createElement("span");
1893
+ acts.className = "vela-ot-acts";
1894
+ head.appendChild(acts);
1895
+ const op = (icon2, title, run) => {
1896
+ acts.appendChild(this.btn({ icon: icon2, title, run }));
1897
+ };
1898
+ if (pane.kind !== "price") {
1899
+ if (pane.order > 1) op("arrow-up", "Move pane up", () => chart.panes.move(pane.id, "up"));
1900
+ if (pane.order < pass.panes.length - 1) op("arrow-down", "Move pane down", () => chart.panes.move(pane.id, "down"));
1901
+ op(pane.collapsed ? "expand" : "collapse", pane.collapsed ? "Expand pane" : "Collapse pane", () => chart.panes.collapse(pane.id, !pane.collapsed));
1902
+ }
1903
+ op(pane.maximized ? "restore" : "maximize", pane.maximized ? "Restore panes" : "Maximize pane", () => chart.panes.maximize(pane.maximized ? null : pane.id));
1904
+ return head;
1905
+ }
1906
+ rowEl(pass, pane, row) {
1907
+ const doc = this.el.ownerDocument;
1908
+ const { chart } = pass;
1909
+ if (row.kind === "price") {
1910
+ const base = this.symbolName.replace(/[-_/]?(USDT|USDC|USD1|USDS|BUSD|USD|EUR|PERP)$/i, "") || this.symbolName;
1911
+ const icon2 = tickerIconEl(doc, base || "P", this.symbolName || "Price", "vela-ot-avatar");
1912
+ const el2 = this.row(icon2, row.label, row.visible, [
1913
+ {
1914
+ icon: row.visible ? "eye" : "eye-off",
1915
+ title: row.visible ? "Hide" : "Show",
1916
+ engaged: !row.visible,
1917
+ run: () => {
1918
+ chart.renderer.set("candleVisible", !row.visible);
1919
+ this.refresh();
1920
+ }
1921
+ }
1922
+ ]);
1923
+ el2.dataset.rowKind = "price";
1924
+ el2.dataset.pane = pane.id;
1925
+ if (pass.stackable) {
1926
+ el2.dataset.drag = "1";
1927
+ el2.title = "Drag to change what draws in front";
1928
+ }
1929
+ return el2;
1930
+ }
1931
+ const handle = pass.handle(row.id);
1932
+ const el = this.row(
1933
+ iconEl("indicators", doc),
1934
+ row.label,
1935
+ row.visible,
1936
+ [
1937
+ {
1938
+ icon: row.visible ? "eye" : "eye-off",
1939
+ title: row.visible ? "Hide" : "Show",
1940
+ engaged: !row.visible,
1941
+ run: () => {
1942
+ handle?.setVisible(!row.visible);
1943
+ this.refresh();
1944
+ }
1945
+ },
1946
+ {
1947
+ icon: "trash",
1948
+ title: "Remove",
1949
+ run: () => {
1950
+ handle?.remove();
1951
+ this.refresh();
1952
+ }
1953
+ }
1954
+ ],
1955
+ row.ownScale ? "scale" : void 0
1956
+ );
1957
+ el.dataset.rowKind = "indicator";
1958
+ el.dataset.id = row.id;
1959
+ el.dataset.pane = pane.id;
1960
+ if (pass.repanable || pass.stackable) {
1961
+ el.dataset.drag = "1";
1962
+ el.title = pass.repanable ? "Drag to another pane, or between panes for a new one" : "Drag to change what draws in front";
1963
+ }
1964
+ return el;
1965
+ }
1966
+ /** A group's header: fold arrow, its name (or the rename field), then actions that apply to
1967
+ * every member at once. Reports "all hidden"/"all locked" rather than a mixed state.
1968
+ *
1969
+ * The state comes from the whole bundle, not from the members listed under this pane: should
1970
+ * a member end up on another pane, the header still speaks for the group its actions affect.
1971
+ */
1972
+ groupRow(pass, pane, g) {
1973
+ const doc = this.el.ownerDocument;
1974
+ const folded = this.collapsed.has(g.id);
1975
+ const members = pass.chart.drawings.all().filter((d) => g.ids.includes(d.id));
1976
+ const { allHidden, allLocked } = groupState(members);
1977
+ const ids = [...g.ids];
1978
+ const el = this.row(iconEl(folded ? "chevron-right" : "chevron-down", doc), g.name, !allHidden, [
1979
+ {
1980
+ icon: allLocked ? "lock" : "unlock",
1981
+ title: allLocked ? "Unlock all" : "Lock all",
1982
+ engaged: allLocked,
1983
+ run: () => this.setMembers(pass, ids, { locked: !allLocked })
1984
+ },
1985
+ {
1986
+ icon: allHidden ? "eye-off" : "eye",
1987
+ title: allHidden ? "Show all" : "Hide all",
1988
+ engaged: allHidden,
1989
+ run: () => this.setMembers(pass, ids, { visible: allHidden })
1990
+ },
1991
+ { icon: "trash", title: "Remove all", run: () => this.removeGroup(pass, g.id, true) }
1992
+ ]);
1993
+ el.dataset.rowKind = "group";
1994
+ el.dataset.id = g.id;
1995
+ el.dataset.pane = pane.id;
1996
+ el.title = `${members.length} drawing${members.length === 1 ? "" : "s"} \u2014 drag to move them together`;
1997
+ if (this.renaming === g.id) {
1998
+ const name = el.querySelector(".vela-ot-name");
1999
+ const input = doc.createElement("input");
2000
+ input.className = "vela-ot-rename";
2001
+ input.value = g.name;
2002
+ name?.replaceWith(input);
2003
+ queueMicrotask(() => {
2004
+ input.focus();
2005
+ input.select();
2006
+ });
2007
+ input.addEventListener("keydown", (e) => {
2008
+ if (e.key === "Enter") {
2009
+ e.preventDefault();
2010
+ this.commitRename(g.id, input.value);
2011
+ } else if (e.key === "Escape") {
2012
+ this.renaming = null;
2013
+ this.render();
2014
+ } else if (e.key === " ") e.stopPropagation();
2015
+ });
2016
+ input.addEventListener("blur", () => this.commitRename(g.id, input.value));
2017
+ } else {
2018
+ el.dataset.drag = "1";
2019
+ el.addEventListener("click", () => {
2020
+ if (folded) this.collapsed.delete(g.id);
2021
+ else this.collapsed.add(g.id);
2022
+ this.render();
2023
+ });
2024
+ }
2025
+ return el;
2026
+ }
2027
+ drawRow(pass, d, inGroup = false) {
2028
+ const doc = this.el.ownerDocument;
2029
+ const { chart } = pass;
2030
+ const markup = drawingMeta(d.type).icon;
2031
+ let icon2;
2032
+ if (markup) {
2033
+ icon2 = doc.createElement("span");
2034
+ icon2.className = "vela-icon";
2035
+ icon2.setAttribute("aria-hidden", "true");
2036
+ icon2.innerHTML = markup;
2037
+ } else {
2038
+ icon2 = iconEl("pen", doc);
2039
+ }
2040
+ const el = this.row(icon2, drawingLabel(d), d.visible, [
2041
+ {
2042
+ icon: d.locked ? "lock" : "unlock",
2043
+ title: d.locked ? "Unlock" : "Lock",
2044
+ engaged: d.locked,
2045
+ run: () => {
2046
+ chart.drawings.lock(d.id, !d.locked);
2047
+ this.refresh();
2048
+ }
2049
+ },
2050
+ {
2051
+ icon: d.visible ? "eye" : "eye-off",
2052
+ title: d.visible ? "Hide" : "Show",
2053
+ engaged: !d.visible,
2054
+ run: () => {
2055
+ chart.drawings.show(d.id, !d.visible);
2056
+ this.refresh();
2057
+ }
2058
+ },
2059
+ {
2060
+ icon: "trash",
2061
+ title: "Remove",
2062
+ run: () => {
2063
+ chart.drawings.remove(d.id);
2064
+ this.refresh();
2065
+ }
2066
+ }
2067
+ ]);
2068
+ el.dataset.rowKind = "drawing";
2069
+ el.dataset.id = d.id;
2070
+ el.dataset.pane = d.paneId;
2071
+ el.dataset.drag = "1";
2072
+ el.title = "Drag to restack, or onto another pane to move it there";
2073
+ if (inGroup) el.classList.add("vela-ot-subrow");
2074
+ if (d.id === this.selectedDrawing) el.dataset.selected = "1";
2075
+ if (this.picked.has(d.id)) el.dataset.picked = "1";
2076
+ el.addEventListener("click", (e) => this.onDrawClick(e, d.id));
2077
+ return el;
2078
+ }
2079
+ /** Clicking a drawing picks it; holding the platform's modifier extends the pick, and
2080
+ * clicking the only picked row clears it. The chart mirrors whatever comes out. */
2081
+ onDrawClick(e, id) {
2082
+ if (e.ctrlKey || e.metaKey) {
2083
+ if (this.picked.has(id)) this.picked.delete(id);
2084
+ else this.picked.add(id);
2085
+ } else if (this.picked.has(id) && this.picked.size === 1) {
2086
+ this.picked.clear();
2087
+ } else {
2088
+ this.picked.clear();
2089
+ this.picked.add(id);
2090
+ }
2091
+ this.selectOnChart([...this.picked]);
2092
+ this.render();
2093
+ }
2094
+ /** Push the panel's pick onto the chart, muting the `drawing:selected` echo it causes. */
2095
+ selectOnChart(ids) {
2096
+ const chart = this.chart;
2097
+ if (!chart?.drawings.supported) return;
2098
+ this.syncingSelection = true;
2099
+ try {
2100
+ chart.drawings.select(ids);
2101
+ } finally {
2102
+ this.syncingSelection = false;
2103
+ }
2104
+ }
2105
+ /** The shared row shell: icon, name, optional tag, then the action buttons. */
2106
+ row(icon2, label, visible, actions, tag) {
2107
+ const doc = this.el.ownerDocument;
2108
+ const el = doc.createElement("div");
2109
+ el.className = "vela-ot-row";
2110
+ if (!visible) el.dataset.hidden = "1";
2111
+ const name = doc.createElement("span");
2112
+ name.className = "vela-ot-name";
2113
+ name.textContent = label;
2114
+ name.title = label;
2115
+ el.append(icon2, name);
2116
+ if (tag) {
2117
+ const t = doc.createElement("span");
2118
+ t.className = "vela-ot-tag";
2119
+ t.textContent = tag;
2120
+ t.title = "Draws against its own price scale";
2121
+ el.appendChild(t);
2122
+ }
2123
+ const acts = doc.createElement("span");
2124
+ acts.className = "vela-ot-acts";
2125
+ for (const a of actions) acts.appendChild(this.btn(a));
2126
+ el.appendChild(acts);
2127
+ return el;
2128
+ }
2129
+ btn(a) {
2130
+ const doc = this.el.ownerDocument;
2131
+ const b = doc.createElement("button");
2132
+ b.className = "vela-ot-btn";
2133
+ b.dataset.icon = a.icon;
2134
+ if (a.engaged) b.dataset.engaged = "1";
2135
+ if (a.disabled) b.disabled = true;
2136
+ b.title = a.title;
2137
+ b.appendChild(iconEl(a.icon, doc));
2138
+ b.addEventListener("click", (e) => {
2139
+ e.stopPropagation();
2140
+ if (!a.disabled) a.run();
2141
+ });
2142
+ return b;
2143
+ }
2144
+ // ── groups ─────────────────────────────────────────────────────────────
2145
+ // A group is the panel's own idea, not the chart's: it bundles drawings so they hide, lock,
2146
+ // delete and move as one. It lives as long as the panel does.
2147
+ /** Bundle drawings into a fresh group, which takes over the pick. */
2148
+ makeGroup(ids) {
2149
+ if (!canGroup(ids, this.groups)) return;
2150
+ this.groupSeq += 1;
2151
+ this.groups = [...this.groups, { id: `grp-${this.groupSeq}`, name: nextGroupName(this.groups), ids: [...ids] }];
2152
+ this.picked.clear();
2153
+ this.render();
2154
+ }
2155
+ commitRename(id, value) {
2156
+ if (this.renaming !== id) return;
2157
+ const name = value.trim();
2158
+ this.groups = this.groups.map((g) => g.id === id && name !== "" ? { ...g, name } : g);
2159
+ this.renaming = null;
2160
+ this.render();
2161
+ }
2162
+ /** Dissolve a group. `withMembers` also deletes the drawings it held. */
2163
+ removeGroup(pass, id, withMembers2) {
2164
+ const g = this.groups.find((x) => x.id === id);
2165
+ this.groups = this.groups.filter((x) => x.id !== id);
2166
+ this.collapsed.delete(id);
2167
+ if (withMembers2 && g) {
2168
+ for (const m of g.ids) this.picked.delete(m);
2169
+ pass.chart.drawings.removeMany(g.ids);
2170
+ }
2171
+ this.render();
2172
+ }
2173
+ /** Apply one patch to every member of a group as a single undo step. */
2174
+ setMembers(pass, ids, patch) {
2175
+ if (ids.length === 0) return;
2176
+ pass.chart.drawings.updateMany(ids.map((id) => ({ id, patch })));
2177
+ this.render();
2178
+ }
2179
+ /** Duplicate a drawing, keeping the copy in the same group as its original — the chart has
2180
+ * no notion of our groups, so the new id has to be spotted and filed here. */
2181
+ cloneInto(pass, id) {
2182
+ const { chart } = pass;
2183
+ if (!chart.drawings.supported) return;
2184
+ const before = new Set(chart.drawings.all().map((d) => d.id));
2185
+ chart.drawings.clone(id);
2186
+ const clone = chart.drawings.all().find((d) => !before.has(d.id));
2187
+ const g = groupOf(this.groups, id);
2188
+ if (g && clone) this.groups = assignToGroup(this.groups, g.id, [clone.id]);
2189
+ }
2190
+ // ── drag-and-drop ──────────────────────────────────────────────────────
2191
+ // Indicator rows move between panes, into a fresh pane, or to a slot in the main pane's
2192
+ // stack. The candles restack in place. Drawing rows take a slot among their pane's
2193
+ // drawings, and land in another pane's list to move there.
2194
+ onPointerDown(e) {
2195
+ if (e.button !== 0 || this.drag) return;
2196
+ const target = e.target;
2197
+ if (target.closest(".vela-ot-btn")) return;
2198
+ const rowEl = target.closest(".vela-ot-row[data-drag]");
2199
+ if (!rowEl) return;
2200
+ if (target.closest(".vela-ot-rename")) return;
2201
+ const kind = rowEl.dataset.rowKind;
2202
+ if (kind !== "price" && kind !== "indicator" && kind !== "drawing" && kind !== "group") return;
2203
+ const win = this.el.ownerDocument.defaultView;
2204
+ if (!win) return;
2205
+ const drag = {
2206
+ kind,
2207
+ id: rowEl.dataset.id ?? null,
2208
+ fromPane: rowEl.dataset.pane ?? PRICE_PANE_ID,
2209
+ label: rowEl.querySelector(".vela-ot-name")?.textContent ?? "object",
2210
+ startX: e.clientX,
2211
+ startY: e.clientY,
2212
+ active: false,
2213
+ ghost: null,
2214
+ drop: null,
2215
+ onMove: (ev) => this.onDragMove(ev),
2216
+ onUp: () => this.endDrag(true),
2217
+ onCancel: () => this.endDrag(false)
2218
+ };
2219
+ this.drag = drag;
2220
+ win.addEventListener("pointermove", drag.onMove);
2221
+ win.addEventListener("pointerup", drag.onUp);
2222
+ win.addEventListener("pointercancel", drag.onCancel);
2223
+ e.preventDefault();
2224
+ }
2225
+ onDragMove(e) {
2226
+ const drag = this.drag;
2227
+ if (!drag) return;
2228
+ if (!drag.active) {
2229
+ if (Math.hypot(e.clientX - drag.startX, e.clientY - drag.startY) < DRAG_SLOP) return;
2230
+ this.beginDrag(drag);
2231
+ }
2232
+ if (drag.ghost) {
2233
+ drag.ghost.style.left = `${e.clientX + 12}px`;
2234
+ drag.ghost.style.top = `${e.clientY + 8}px`;
2235
+ }
2236
+ drag.drop = this.resolveDrop(drag, e.clientX, e.clientY);
2237
+ this.paintDropHint(drag.drop);
2238
+ }
2239
+ beginDrag(drag) {
2240
+ drag.active = true;
2241
+ this.body.dataset.dragging = "1";
2242
+ const src = this.sourceRow(drag);
2243
+ if (src) src.dataset.source = "1";
2244
+ const ghost = this.el.ownerDocument.createElement("div");
2245
+ ghost.className = "vela-ot-ghost";
2246
+ ghost.textContent = drag.label;
2247
+ this.uiHost().appendChild(ghost);
2248
+ drag.ghost = ghost;
2249
+ }
2250
+ /** The rendered row a session started from — scanned rather than selected, so an id
2251
+ * carrying CSS-special characters needs no escaping. */
2252
+ sourceRow(drag) {
2253
+ for (const el of this.body.querySelectorAll(".vela-ot-row")) {
2254
+ if (el.dataset.rowKind !== drag.kind) continue;
2255
+ if (drag.id === null || el.dataset.id === drag.id) return el;
2256
+ }
2257
+ return null;
2258
+ }
2259
+ /** What a drop at this point would do. Measured off the live geometry, which is why a
2260
+ * refresh is suspended for the duration of the drag. */
2261
+ resolveDrop(drag, x, y) {
2262
+ const pass = this.pass;
2263
+ if (!pass) return null;
2264
+ if (drag.kind === "indicator") {
2265
+ for (const gap of this.body.querySelectorAll(".vela-ot-gap")) {
2266
+ const r = gap.getBoundingClientRect();
2267
+ if (y >= r.top - 4 && y <= r.bottom + 4 && x >= r.left && x <= r.right) {
2268
+ return { kind: "newPane", before: gap.dataset.before, after: gap.dataset.after, el: gap };
2269
+ }
2270
+ }
2271
+ }
2272
+ for (const block of this.body.querySelectorAll(".vela-ot-pane")) {
2273
+ const r = block.getBoundingClientRect();
2274
+ if (y < r.top || y > r.bottom) continue;
2275
+ const paneId = block.dataset.pane ?? PRICE_PANE_ID;
2276
+ if (drag.kind === "price" && block.dataset.kind !== "price") return null;
2277
+ if (drag.kind === "indicator" && !pass.stackable) {
2278
+ return paneId === drag.fromPane ? null : { kind: "merge", paneId, el: block };
2279
+ }
2280
+ const els = [...block.querySelector(":scope > .vela-ot-stack")?.children ?? []];
2281
+ if (drag.kind === "drawing") {
2282
+ for (const unit of els) {
2283
+ const groupId = unit.dataset.group;
2284
+ if (groupId === void 0) continue;
2285
+ const ur = unit.getBoundingClientRect();
2286
+ if (y < ur.top || y > ur.bottom) continue;
2287
+ const subrows = [...unit.querySelectorAll(":scope > .vela-ot-subrow")];
2288
+ return { kind: "intoGroup", paneId, groupId, memberSlot: insertionSlot(subrows, y), subrows, el: unit };
2289
+ }
2290
+ }
2291
+ let slot = insertionSlot(els, y);
2292
+ if ((drag.kind === "drawing" || drag.kind === "group") && !pass.interleave) {
2293
+ const pane = pass.panes.find((p) => p.id === paneId);
2294
+ const lead = pane ? pane.items.findIndex((it) => it.kind === "row") : -1;
2295
+ if (lead >= 0) slot = Math.min(slot, lead);
2296
+ }
2297
+ return { kind: "slot", paneId, slot, els, el: block };
2298
+ }
2299
+ return null;
2300
+ }
2301
+ paintDropHint(drop) {
2302
+ for (const el of this.body.querySelectorAll("[data-drop]")) delete el.dataset.drop;
2303
+ if (!drop) return;
2304
+ if (drop.kind === "newPane") {
2305
+ drop.el.dataset.drop = "gap";
2306
+ return;
2307
+ }
2308
+ if (drop.kind === "merge") {
2309
+ drop.el.dataset.drop = "target";
2310
+ return;
2311
+ }
2312
+ if (drop.kind === "intoGroup") {
2313
+ drop.el.dataset.drop = "target";
2314
+ const at2 = drop.subrows[drop.memberSlot];
2315
+ if (at2) at2.dataset.drop = "before";
2316
+ else if (drop.subrows.length > 0) drop.subrows[drop.subrows.length - 1].dataset.drop = "after";
2317
+ return;
2318
+ }
2319
+ const at = drop.els[drop.slot];
2320
+ if (drop.els.length === 0) drop.el.dataset.drop = "target";
2321
+ else if (at) at.dataset.drop = "before";
2322
+ else drop.els[drop.els.length - 1].dataset.drop = "after";
2323
+ }
2324
+ /** Close the session out: `apply` false abandons the move (a cancelled pointer, a teardown). */
2325
+ endDrag(apply) {
2326
+ const drag = this.drag;
2327
+ this.drag = null;
2328
+ if (!drag) return;
2329
+ const win = this.el.ownerDocument.defaultView;
2330
+ win?.removeEventListener("pointermove", drag.onMove);
2331
+ win?.removeEventListener("pointerup", drag.onUp);
2332
+ win?.removeEventListener("pointercancel", drag.onCancel);
2333
+ if (!drag.active) return;
2334
+ drag.ghost?.remove();
2335
+ delete this.body.dataset.dragging;
2336
+ const src = this.sourceRow(drag);
2337
+ if (src) delete src.dataset.source;
2338
+ this.paintDropHint(null);
2339
+ if (apply && drag.drop) this.applyDrop(drag, drag.drop);
2340
+ this.refresh();
2341
+ }
2342
+ applyDrop(drag, drop) {
2343
+ const pass = this.pass;
2344
+ if (!pass) return;
2345
+ const { chart } = pass;
2346
+ switch (drop.kind) {
2347
+ case "merge":
2348
+ if (drag.id !== null && drop.paneId !== drag.fromPane) {
2349
+ chart.panes.moveIndicator(drag.id, drop.paneId === PRICE_PANE_ID ? "price" : { pane: drop.paneId });
2350
+ }
2351
+ return;
2352
+ case "newPane": {
2353
+ const from = pass.panes.find((p) => p.id === drag.fromPane);
2354
+ const alone = from !== void 0 && from.kind !== "price" && paneRows(from).length <= 1;
2355
+ if (alone && (drop.before === drag.fromPane || drop.after === drag.fromPane)) return;
2356
+ if (drag.id !== null) chart.panes.moveIndicator(drag.id, { newPane: { before: drop.before, after: drop.after } });
2357
+ return;
2358
+ }
2359
+ case "slot":
2360
+ case "intoGroup":
2361
+ this.applySlotDrop(pass, drag, drop);
2362
+ return;
2363
+ }
2364
+ }
2365
+ /**
2366
+ * Land a drop in a pane's stack: place the dragged tokens (the candles, an indicator, a
2367
+ * drawing, or a group's whole run of drawings) at the measured position, then renormalize
2368
+ * the pane's z keys in one sweep — `candleZOrder`/`seriesOrder` for the series, a single
2369
+ * `updateMany` (one undo step) for the drawings, re-paning the ones that crossed panes.
2370
+ */
2371
+ applySlotDrop(pass, drag, drop) {
2372
+ const { chart } = pass;
2373
+ const target = pass.panes.find((p) => p.id === drop.paneId);
2374
+ if (!target) return;
2375
+ const isDrawing = drag.kind === "drawing" || drag.kind === "group";
2376
+ if (isDrawing && !chart.drawings.supported) return;
2377
+ const dragged = drag.kind === "price" ? [{ kind: "price" }] : drag.kind === "indicator" ? [{ kind: "indicator", id: drag.id }] : this.draggedDrawings(pass, drag).map((id) => ({ kind: "drawing", id }));
2378
+ if (dragged.length === 0 || drag.kind !== "price" && drag.id === null) return;
2379
+ if (drag.kind === "indicator" && drag.fromPane !== drop.paneId) {
2380
+ chart.panes.moveIndicator(drag.id, drop.paneId === PRICE_PANE_ID ? "price" : { pane: drop.paneId });
2381
+ }
2382
+ const tokens = paneTokens(target);
2383
+ const at = drop.kind === "intoGroup" ? groupTokenIndex(target, drop.groupId, drop.memberSlot) : tokenIndexOfSlot(target, drop.slot);
2384
+ const placed = placeTokens(tokens, dragged, at);
2385
+ if (drag.kind === "drawing") {
2386
+ this.groups = drop.kind === "intoGroup" ? assignToGroup(this.groups, drop.groupId, [drag.id]) : removeFromGroups(this.groups, [drag.id]);
2387
+ }
2388
+ const live = new Map(chart.drawings.all().map((d) => [d.id, d]));
2389
+ const draggedIds = new Set(dragged.flatMap((t) => t.kind === "drawing" ? [t.id] : []));
2390
+ const repaned = [...draggedIds].some((id) => live.get(id)?.paneId !== drop.paneId);
2391
+ if (tokensEqual(tokens, placed) && !repaned) return;
2392
+ this.writeStack(pass, drop.paneId, placed, draggedIds);
2393
+ }
2394
+ /** Renormalize one pane's z keys from a placed token stack: the series through the
2395
+ * renderer's order settings, the drawings as ONE `updateMany` (a single undo step),
2396
+ * re-paning the dragged ones that arrived from another pane. */
2397
+ writeStack(pass, paneId, placed, draggedIds) {
2398
+ const { chart } = pass;
2399
+ const live = new Map(chart.drawings.all().map((d) => [d.id, d]));
2400
+ const writes = stackWrites(placed);
2401
+ if (pass.stackable) {
2402
+ if (writes.candleZ !== null) chart.renderer.set("candleZOrder", writes.candleZ);
2403
+ for (const s of writes.series) chart.renderer.set("seriesOrder", { id: s.id, z: s.z });
2404
+ }
2405
+ if (writes.drawings.length > 0) {
2406
+ chart.drawings.updateMany(
2407
+ writes.drawings.map(({ id, z }) => ({
2408
+ id,
2409
+ patch: { zIndex: z, ...draggedIds.has(id) && live.get(id)?.paneId !== paneId ? { paneId } : {} }
2410
+ }))
2411
+ );
2412
+ }
2413
+ }
2414
+ /** Move a group's whole run to an end of its pane's stack — the menu twin of dragging it
2415
+ * to the top or the bottom of the column. */
2416
+ restackGroup(pass, g, edge) {
2417
+ const member = new Set(g.ids);
2418
+ const mine = pass.chart.drawings.all().filter((d) => member.has(d.id));
2419
+ const paneId = mine[0]?.paneId ?? PRICE_PANE_ID;
2420
+ const target = pass.panes.find((p) => p.id === paneId) ?? pass.panes.find((p) => p.kind === "price");
2421
+ if (!target || mine.length === 0) return;
2422
+ const dragged = mine.map((d) => ({ kind: "drawing", id: d.id })).reverse();
2423
+ const tokens = paneTokens(target);
2424
+ const placed = placeTokens(tokens, dragged, edge === "front" ? 0 : tokens.length);
2425
+ if (tokensEqual(tokens, placed)) return;
2426
+ this.writeStack(pass, target.id, placed, /* @__PURE__ */ new Set());
2427
+ this.refresh();
2428
+ }
2429
+ /** The drawings a session moves: a group carries every member, front-most first, so the
2430
+ * bundle keeps its own stacking wherever it lands. */
2431
+ draggedDrawings(pass, drag) {
2432
+ if (drag.id === null) return [];
2433
+ if (drag.kind !== "group") return [drag.id];
2434
+ const g = this.groups.find((x) => x.id === drag.id);
2435
+ if (!g) return [];
2436
+ const member = new Set(g.ids);
2437
+ return pass.chart.drawings.all().filter((d) => member.has(d.id)).map((d) => d.id).reverse();
2438
+ }
2439
+ // ── context menus ──────────────────────────────────────────────────────
2440
+ // Right-clicking a row opens the same actions its buttons expose, plus the ones with no
2441
+ // room on a row: re-paning an indicator, and reordering what draws in front.
2442
+ onContextMenu(e) {
2443
+ const pass = this.pass;
2444
+ if (!pass) return;
2445
+ const rowEl = e.target.closest(".vela-ot-row");
2446
+ if (!rowEl) return;
2447
+ e.preventDefault();
2448
+ const b = new MenuBuilder();
2449
+ const items = this.itemsForRow(b, pass, rowEl);
2450
+ if (items.length === 0) return;
2451
+ this.menuActions = b.actions;
2452
+ const menu = this.ensureMenu();
2453
+ menu.setItems(items);
2454
+ menu.openAt(e.clientX, e.clientY);
2455
+ }
2456
+ itemsForRow(b, pass, rowEl) {
2457
+ const paneId = rowEl.dataset.pane ?? PRICE_PANE_ID;
2458
+ const id = rowEl.dataset.id;
2459
+ switch (rowEl.dataset.rowKind) {
2460
+ case "price":
2461
+ return this.priceMenu(b, pass);
2462
+ case "indicator": {
2463
+ const row = id === void 0 ? void 0 : this.findIndicatorRow(pass, id);
2464
+ return row ? this.indicatorMenu(b, pass, row, paneId) : [];
2465
+ }
2466
+ case "drawing": {
2467
+ const d = id === void 0 ? void 0 : pass.chart.drawings.all().find((x) => x.id === id);
2468
+ return d ? this.drawingMenu(b, pass, d) : [];
2469
+ }
2470
+ case "group": {
2471
+ const g = id === void 0 ? void 0 : this.groups.find((x) => x.id === id);
2472
+ return g ? this.groupMenu(b, pass, g) : [];
2473
+ }
2474
+ default:
2475
+ return [];
2476
+ }
2477
+ }
2478
+ /** The nearest kit host, which is where anything floating has to mount: outside it the
2479
+ * theme's custom properties don't resolve and the surface renders unstyled. */
2480
+ uiHost() {
2481
+ return this.el.closest(".vela-ui") ?? this.el;
2482
+ }
2483
+ /** The menu, built on first use — by then the panel is mounted, so the theme host resolves. */
2484
+ ensureMenu() {
2485
+ if (!this.menu) {
2486
+ this.menu = new Menu({ host: this.uiHost(), items: [], onSelect: (id) => this.menuActions.get(id)?.() });
2487
+ }
2488
+ return this.menu;
2489
+ }
2490
+ findIndicatorRow(pass, id) {
2491
+ for (const pane of pass.panes) {
2492
+ for (const row of paneRows(pane)) if (row.kind === "indicator" && row.id === id) return row;
2493
+ }
2494
+ return void 0;
2495
+ }
2496
+ priceMenu(b, pass) {
2497
+ const { chart } = pass;
2498
+ const visible = chart.renderer.get("candleVisible") !== false;
2499
+ const items = [
2500
+ b.entry(visible ? "Hide" : "Show", visible ? "eye-off" : "eye", () => {
2501
+ chart.renderer.set("candleVisible", !visible);
2502
+ this.refresh();
2503
+ })
2504
+ ];
2505
+ if (pass.stackable) {
2506
+ items.push(
2507
+ b.entry("Bring to front", "arrow-up", () => {
2508
+ chart.renderer.set("candleZOrder", this.stackBounds(pass, PRICE_PANE_ID).top + 1);
2509
+ this.refresh();
2510
+ }, true)
2511
+ );
2512
+ items.push(
2513
+ b.entry("Send to back", "arrow-down", () => {
2514
+ chart.renderer.set("candleZOrder", this.stackBounds(pass, PRICE_PANE_ID).bottom - 1);
2515
+ this.refresh();
2516
+ })
2517
+ );
2518
+ }
2519
+ return items;
2520
+ }
2521
+ /** The pane's stacking extremes as of now — what a front/back command has to beat. The z
2522
+ * keys are plain numbers, not commands, so the writer beats the extremes itself; with a
2523
+ * shared draw-order space the drawings' keys count too. */
2524
+ stackBounds(pass, paneId) {
2525
+ const { chart } = pass;
2526
+ const drawingZ = pass.interleave ? chart.drawings.all().filter((d) => d.paneId === paneId).map((d) => d.zIndex) : [];
2527
+ return zStackBounds(
2528
+ chart.renderer.get("seriesOrder") ?? [],
2529
+ Number(chart.renderer.get("candleZOrder")) || 0,
2530
+ drawingZ
2531
+ );
2532
+ }
2533
+ indicatorMenu(b, pass, row, paneId) {
2534
+ const { chart } = pass;
2535
+ const handle = pass.handle(row.id);
2536
+ const items = [
2537
+ b.entry(row.visible ? "Hide" : "Show", row.visible ? "eye-off" : "eye", () => {
2538
+ handle?.setVisible(!row.visible);
2539
+ this.refresh();
2540
+ })
2541
+ ];
2542
+ if (chart.panes.supported) {
2543
+ const moves = this.moveItems(b, pass, row.id, paneId);
2544
+ if (moves.length > 0) items.push(b.submenu("Move to", "move-vertical", moves));
2545
+ }
2546
+ if (pass.stackable) {
2547
+ items.push(b.entry("Bring to front", "arrow-up", () => chart.renderer.set("seriesOrder", { id: row.id, z: this.stackBounds(pass, paneId).top + 1 }), true));
2548
+ items.push(b.entry("Send to back", "arrow-down", () => chart.renderer.set("seriesOrder", { id: row.id, z: this.stackBounds(pass, paneId).bottom - 1 })));
2549
+ }
2550
+ items.push(
2551
+ b.entry("Remove", "trash", () => {
2552
+ handle?.remove();
2553
+ this.refresh();
2554
+ }, true)
2555
+ );
2556
+ return items;
2557
+ }
2558
+ /** "Move to …" entries for an indicator — only the moves that would change something. */
2559
+ moveItems(b, pass, id, fromPane) {
2560
+ const { chart } = pass;
2561
+ const items = [];
2562
+ for (const p of pass.panes) {
2563
+ if (p.id === fromPane) continue;
2564
+ items.push(b.entry(p.label, void 0, () => chart.panes.moveIndicator(id, p.kind === "price" ? "price" : { pane: p.id })));
2565
+ }
2566
+ const current = pass.panes.find((p) => p.id === fromPane);
2567
+ const alone = current !== void 0 && current.kind !== "price" && paneRows(current).length <= 1;
2568
+ if (!alone) {
2569
+ if (fromPane !== PRICE_PANE_ID) items.push(b.entry("New pane above", void 0, () => chart.panes.moveIndicator(id, { newPane: { before: fromPane } })));
2570
+ items.push(b.entry("New pane below", void 0, () => chart.panes.moveIndicator(id, { newPane: { after: fromPane } })));
2571
+ }
2572
+ return items;
2573
+ }
2574
+ drawingMenu(b, pass, d) {
2575
+ const { chart } = pass;
2576
+ const items = [
2577
+ b.entry(d.visible ? "Hide" : "Show", d.visible ? "eye-off" : "eye", () => {
2578
+ chart.drawings.show(d.id, !d.visible);
2579
+ this.refresh();
2580
+ }),
2581
+ b.entry(d.locked ? "Unlock" : "Lock", d.locked ? "unlock" : "lock", () => {
2582
+ chart.drawings.lock(d.id, !d.locked);
2583
+ this.refresh();
2584
+ }),
2585
+ b.entry("Duplicate", "clone", () => {
2586
+ this.cloneInto(pass, d.id);
2587
+ this.render();
2588
+ }),
2589
+ // Front/back clear the WHOLE stack on a shared-z renderer — over or under the
2590
+ // candles and every indicator, not just the other drawings.
2591
+ b.entry("Bring to front", "arrow-up", () => chart.drawings.bringToFront(d.id), true),
2592
+ b.entry("Send to back", "arrow-down", () => chart.drawings.sendToBack(d.id))
2593
+ ];
2594
+ items.push(...this.groupingItems(b, d.id));
2595
+ items.push(b.entry("Remove", "trash", () => chart.drawings.remove(d.id), true));
2596
+ return items;
2597
+ }
2598
+ /** The grouping half of a drawing's menu: what it can do about the bundle it is (or isn't)
2599
+ * part of. Only the moves that mean something for this row are offered. */
2600
+ groupingItems(b, id) {
2601
+ const items = [];
2602
+ const mine = groupOf(this.groups, id);
2603
+ const pick = this.picked.has(id) && this.picked.size > 1 ? [...this.picked] : null;
2604
+ if (pick && canGroup(pick, this.groups)) {
2605
+ items.push(b.entry(`Group selection (${pick.length})`, "group", () => this.makeGroup(pick), true));
2606
+ } else if (!pick && mine === null) {
2607
+ items.push(b.entry("New group", "group", () => this.makeGroup([id]), true));
2608
+ }
2609
+ const others = this.groups.filter((g) => g.id !== mine?.id);
2610
+ if (!pick && others.length > 0) {
2611
+ items.push(
2612
+ b.submenu(
2613
+ mine === null ? "Add to group" : "Move to group",
2614
+ "folder-plus",
2615
+ others.map(
2616
+ (g) => b.entry(g.name, void 0, () => {
2617
+ this.groups = assignToGroup(this.groups, g.id, [id]);
2618
+ this.render();
2619
+ })
2620
+ ),
2621
+ items.length === 0
2622
+ )
2623
+ );
2624
+ }
2625
+ if (mine !== null) {
2626
+ items.push(
2627
+ b.entry(`Remove from ${mine.name}`, "folder-minus", () => {
2628
+ this.groups = removeFromGroups(this.groups, [id]);
2629
+ this.render();
2630
+ }, items.length === 0)
2631
+ );
2632
+ }
2633
+ return items;
2634
+ }
2635
+ groupMenu(b, pass, g) {
2636
+ const ids = [...g.ids];
2637
+ const members = pass.chart.drawings.all().filter((d) => g.ids.includes(d.id));
2638
+ const { allHidden, allLocked } = groupState(members);
2639
+ const items = [
2640
+ b.entry("Rename\u2026", "pen", () => {
2641
+ this.renaming = g.id;
2642
+ this.render();
2643
+ }),
2644
+ b.entry(allHidden ? "Show all" : "Hide all", allHidden ? "eye" : "eye-off", () => this.setMembers(pass, ids, { visible: allHidden }), true),
2645
+ b.entry(allLocked ? "Unlock all" : "Lock all", allLocked ? "unlock" : "lock", () => this.setMembers(pass, ids, { locked: !allLocked })),
2646
+ b.entry("Select all", "group", () => {
2647
+ this.picked = new Set(ids);
2648
+ this.selectOnChart(ids);
2649
+ this.render();
2650
+ })
2651
+ ];
2652
+ if (members.length > 0) {
2653
+ items.push(b.entry("Bring all to front", "arrow-up", () => this.restackGroup(pass, g, "front")));
2654
+ items.push(b.entry("Send all to back", "arrow-down", () => this.restackGroup(pass, g, "back")));
2655
+ }
2656
+ items.push(b.entry("Ungroup", "ungroup", () => this.removeGroup(pass, g.id, false), true));
2657
+ items.push(b.entry("Remove all", "trash", () => this.removeGroup(pass, g.id, true)));
2658
+ return items;
2659
+ }
2660
+ /** Forget everything that refers to a drawing or group the chart no longer has — otherwise a
2661
+ * deleted drawing would keep a group alive, or stay counted in the selection bar. */
2662
+ prune(chart) {
2663
+ const live = new Set((chart.drawings.supported ? chart.drawings.all() : []).map((d) => d.id));
2664
+ for (const id of [...this.picked]) if (!live.has(id)) this.picked.delete(id);
2665
+ if (this.groups.length === 0) {
2666
+ this.renaming = null;
2667
+ return;
2668
+ }
2669
+ this.groups = pruneGroups(this.groups, live);
2670
+ if (this.renaming !== null && !this.groups.some((g) => g.id === this.renaming)) this.renaming = null;
2671
+ }
2672
+ };
2673
+
2674
+ // src/widget/data-window.ts
2675
+ var STYLE_ID7 = "vela-widget-datawindow";
2676
+ var CSS7 = `
2677
+ .vela-dw-group {
2678
+ padding: 10px 8px 4px;
2679
+ margin-top: 4px;
2680
+ border-top: 1px solid var(--vela-border);
2681
+ color: var(--vela-fg-muted);
2682
+ font-size: 10px;
2683
+ font-weight: 700;
2684
+ text-transform: uppercase;
2685
+ letter-spacing: 0.08em;
2686
+ }
2687
+ .vela-dw-group:first-child { border-top: none; margin-top: 0; padding-top: 8px; }
2688
+ .vela-dw-row {
2689
+ display: flex;
2690
+ align-items: center;
2691
+ gap: 12px;
2692
+ padding: 4px 8px;
2693
+ border-radius: 4px;
2694
+ }
2695
+ .vela-dw-row:hover { background: var(--vela-hover); }
2696
+ .vela-dw-label { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--vela-fg-bright); }
2697
+ .vela-dw-value { margin-left: auto; font-variant-numeric: tabular-nums; white-space: nowrap; }
2698
+ .vela-dw-empty { padding: 20px 10px; text-align: center; color: var(--vela-fg-muted); font-size: 12px; }
2699
+ `;
2700
+ var REFRESH_EVENTS2 = ["bar", "context:changed", "indicator:added", "indicator:removed", "indicator:visibility", "indicator:moved", "pane:changed", "market:changed", "history:complete"];
2701
+ var EM_DASH = "\u2014";
2702
+ function dataWindowSections(readout) {
2703
+ const sections = [];
2704
+ if (readout.date || readout.time) {
2705
+ sections.push({
2706
+ title: "Time",
2707
+ lines: [
2708
+ { label: "Date", value: readout.date || EM_DASH, color: "" },
2709
+ { label: "Time", value: readout.time || EM_DASH, color: "" }
2710
+ ]
2711
+ });
2712
+ }
2713
+ const { ohlc } = readout;
2714
+ if (ohlc) {
2715
+ const color = ohlc.up ? "var(--vela-up)" : "var(--vela-down)";
2716
+ const lines = [
2717
+ { label: "Open", value: ohlc.o, color },
2718
+ { label: "High", value: ohlc.h, color },
2719
+ { label: "Low", value: ohlc.l, color },
2720
+ { label: "Close", value: ohlc.c, color }
2721
+ ];
2722
+ if (ohlc.vol !== void 0) lines.push({ label: "Volume", value: ohlc.vol, color });
2723
+ sections.push({ title: "Price", lines });
2724
+ }
2725
+ for (const group of readout.groups) {
2726
+ sections.push({ title: group.name, lines: group.rows.map((r) => ({ label: r.label, value: r.value, color: r.color })) });
2727
+ }
2728
+ return sections;
2729
+ }
2730
+ var DataWindow = class extends SidePanel {
2731
+ constructor(host) {
2732
+ super(host, "Data window", "vela-dw");
2733
+ this.chart = null;
2734
+ this.unsubs = [];
2735
+ injectStyles(STYLE_ID7, CSS7, host.ownerDocument);
2736
+ }
2737
+ toggle(open = this.el.hidden) {
2738
+ super.toggle(open);
2739
+ if (open) this.refresh();
2740
+ }
2741
+ /** (Re)bind to a chart instance — called after every widget rebuild. */
2742
+ onChart(chart) {
2743
+ this.detach();
2744
+ this.chart = chart;
2745
+ for (const ev of REFRESH_EVENTS2) this.unsubs.push(chart.on(ev, () => this.refresh()));
2746
+ this.unsubs.push(chart.renderer.onCrosshairMove(() => this.refresh()));
2747
+ this.refresh();
2748
+ }
2749
+ destroy() {
2750
+ this.detach();
2751
+ super.destroy();
2752
+ }
2753
+ detach() {
2754
+ for (const u of this.unsubs) u();
2755
+ this.unsubs = [];
2756
+ this.chart = null;
2757
+ }
2758
+ refresh() {
2759
+ if (this.el.hidden || !this.chart) return;
2760
+ const doc = this.el.ownerDocument;
2761
+ const readout = this.chart.renderer.dataWindowReadout();
2762
+ this.body.replaceChildren();
2763
+ const sections = readout ? dataWindowSections(readout) : [];
2764
+ if (sections.length === 0) {
2765
+ const empty = doc.createElement("div");
2766
+ empty.className = "vela-dw-empty";
2767
+ empty.textContent = readout ? "No data" : "This renderer provides no data readout.";
2768
+ this.body.appendChild(empty);
2769
+ return;
2770
+ }
2771
+ for (const section of sections) {
2772
+ const head = doc.createElement("div");
2773
+ head.className = "vela-dw-group";
2774
+ head.textContent = section.title;
2775
+ this.body.appendChild(head);
2776
+ for (const line of section.lines) {
2777
+ const row = doc.createElement("div");
2778
+ row.className = "vela-dw-row";
2779
+ const label = doc.createElement("span");
2780
+ label.className = "vela-dw-label";
2781
+ label.textContent = line.label;
2782
+ const value = doc.createElement("span");
2783
+ value.className = "vela-dw-value";
2784
+ value.textContent = line.value;
2785
+ if (line.color) value.style.color = line.color;
2786
+ row.append(label, value);
2787
+ this.body.appendChild(row);
2788
+ }
2789
+ }
2790
+ }
2791
+ };
2792
+
2793
+ // src/widget/panel-dock.ts
2794
+ var PanelDock = class {
2795
+ constructor(host, deps) {
2796
+ this.host = host;
2797
+ this.deps = deps;
2798
+ this.entries = [];
2799
+ /** Widths the USER settled, by panel id — the only ones worth persisting. */
2800
+ this.widths = /* @__PURE__ */ new Map();
2801
+ /** A restored `open` naming a panel that has not registered yet: honored when it docks. */
2802
+ this.pendingOpen = null;
2803
+ this.chart = null;
2804
+ }
2805
+ /** Dock a panel the shell owns (created and destroyed by it). */
2806
+ addBuiltIn(entry) {
2807
+ this.add({ ...entry, contributed: false });
2808
+ }
2809
+ /**
2810
+ * (Re)build the CONTRIBUTED panels from the registry — call once after the built-ins, and
2811
+ * again on `refreshActions()` so a late registration appears. Contributed panels that are
2812
+ * gone from the registry are dropped; the ones still there are rebuilt, so a replaced
2813
+ * descriptor takes effect.
2814
+ */
2815
+ refresh() {
2816
+ const openBefore = this.openId;
2817
+ for (const entry of [...this.entries]) if (entry.contributed) this.drop(entry);
2818
+ for (const desc of sidePanels()) {
2819
+ const panel = new SidePanel(this.host, desc.title, `vela-panel-${desc.id}`, {
2820
+ width: desc.width,
2821
+ resizable: desc.resizable,
2822
+ minWidth: desc.minWidth,
2823
+ maxWidth: desc.maxWidth
2824
+ });
2825
+ const entry = {
2826
+ id: desc.id,
2827
+ title: desc.title,
2828
+ icon: desc.icon,
2829
+ order: desc.order ?? DEFAULT_PANEL_ORDER,
2830
+ panel,
2831
+ contributed: true
2832
+ };
2833
+ try {
2834
+ entry.handle = desc.mount(this.deps.context(), panel.content, { slot: panel.headerSlot, setTitle: (t) => panel.setTitle(t) }) ?? void 0;
2835
+ if (this.chart) entry.handle?.onChart?.(this.chart);
2836
+ } catch (err) {
2837
+ console.warn(`[vela] side panel "${desc.id}" failed to mount`, err);
2838
+ }
2839
+ this.add(entry);
2840
+ }
2841
+ if (openBefore && !this.openId) this.toggle(openBefore, true);
2842
+ this.publish();
2843
+ }
2844
+ /** Bind (or rebind) every docked panel to a chart instance. */
2845
+ onChart(chart) {
2846
+ this.chart = chart;
2847
+ for (const entry of this.entries) {
2848
+ if (entry.onChart) entry.onChart(chart);
2849
+ entry.handle?.onChart?.(chart);
2850
+ }
2851
+ }
2852
+ /** Open/close one panel by id — a bare call flips it. Unknown ids are ignored. */
2853
+ toggle(id, open) {
2854
+ this.entries.find((e) => e.id === id)?.panel.toggle(open);
2855
+ }
2856
+ /** The open panel's id, or null when the column is closed. */
2857
+ get openId() {
2858
+ return this.entries.find((e) => e.panel.open)?.id ?? null;
2859
+ }
2860
+ /** The dock's persistable state, or null when there is nothing worth saving. */
2861
+ getState() {
2862
+ const out = {};
2863
+ const open = this.openId;
2864
+ if (open) out.open = open;
2865
+ if (this.widths.size > 0) out.widths = Object.fromEntries(this.widths);
2866
+ return out.open || out.widths ? out : null;
2867
+ }
2868
+ /**
2869
+ * Restore a persisted dock state. Widths apply to any panel present, and are remembered for
2870
+ * panels that register later; `open` opens that panel, and its absence closes the column —
2871
+ * a document that predates the dock has no `panels` field at all, so the shell never calls
2872
+ * this and the default (everything closed) stands. An `open` naming a panel that has not
2873
+ * registered yet is held until it docks (a plugin loaded after the restore), unless the user
2874
+ * opens something in the meantime.
2875
+ */
2876
+ applyState(state) {
2877
+ if (!state) return;
2878
+ if (state.widths) {
2879
+ for (const [id, px] of Object.entries(state.widths)) {
2880
+ this.widths.set(id, px);
2881
+ this.entries.find((e) => e.id === id)?.panel.setWidth(px);
2882
+ }
2883
+ }
2884
+ for (const entry of this.entries) entry.panel.toggle(entry.id === state.open);
2885
+ this.pendingOpen = state.open && !this.entries.some((e) => e.id === state.open) ? state.open : null;
2886
+ }
2887
+ /** Drop the contributed panels (the shell destroys its own). */
2888
+ destroy() {
2889
+ for (const entry of [...this.entries]) if (entry.contributed) this.drop(entry);
2890
+ this.entries.length = 0;
2891
+ }
2892
+ add(entry) {
2893
+ this.entries.push(entry);
2894
+ this.entries.sort((a, b) => a.order - b.order);
2895
+ const stored = this.widths.get(entry.id);
2896
+ if (stored !== void 0) entry.panel.setWidth(stored);
2897
+ entry.panel.onOpenChange = (open) => {
2898
+ if (open) {
2899
+ for (const other of this.entries) if (other !== entry) other.panel.toggle(false);
2900
+ this.pendingOpen = null;
2901
+ }
2902
+ this.deps.chrome.setPanelActive(entry.id, open);
2903
+ if (open) entry.handle?.onOpen?.();
2904
+ this.deps.changed?.();
2905
+ };
2906
+ entry.panel.onWidthChange = (px) => {
2907
+ this.widths.set(entry.id, px);
2908
+ this.deps.changed?.();
2909
+ };
2910
+ if (this.pendingOpen === entry.id) entry.panel.toggle(true);
2911
+ if (!entry.contributed) this.publish();
2912
+ }
2913
+ drop(entry) {
2914
+ const i = this.entries.indexOf(entry);
2915
+ if (i >= 0) this.entries.splice(i, 1);
2916
+ try {
2917
+ entry.handle?.destroy?.();
2918
+ } catch (err) {
2919
+ console.warn(`[vela] side panel "${entry.id}" failed to release`, err);
2920
+ }
2921
+ entry.panel.destroy();
2922
+ }
2923
+ /** Push the current toggle group to the chrome, pressed states included. */
2924
+ publish() {
2925
+ const buttons = this.entries.map((e) => ({ id: e.id, title: e.title, icon: e.icon }));
2926
+ this.deps.chrome.setPanelButtons(buttons, (id) => this.toggle(id));
2927
+ for (const entry of this.entries) this.deps.chrome.setPanelActive(entry.id, entry.panel.open);
2928
+ }
2929
+ };
2930
+
2931
+ // src/widget/shortcuts-help.ts
2932
+ var STYLE_ID8 = "vela-widget-shortcuts";
2933
+ var CSS8 = `
2934
+ .vela-sh-cat {
2935
+ color: var(--vela-fg-muted);
2936
+ font-size: var(--vela-font-size-sm);
2937
+ text-transform: uppercase;
2938
+ letter-spacing: 0.06em;
2939
+ padding: var(--vela-space-2) 0 var(--vela-space-1);
2940
+ }
2941
+ .vela-sh-row { display: flex; align-items: center; gap: var(--vela-space-3); padding: 3px 0; }
2942
+ .vela-sh-label { flex: 1; }
2943
+ .vela-sh-keys { display: flex; gap: 4px; }
2944
+ .vela-sh-key {
2945
+ background: var(--vela-surface-overlay);
2946
+ border: 1px solid var(--vela-border-soft);
2947
+ border-radius: var(--vela-radius-sm);
2948
+ padding: 1px 7px;
2949
+ font-size: var(--vela-font-size-sm);
2950
+ font-variant-numeric: tabular-nums;
2951
+ }
2952
+ .vela-sh-static { color: var(--vela-fg-muted); font-size: var(--vela-font-size-sm); margin-top: var(--vela-space-2); }
2953
+ `;
2954
+ var ShortcutsHelp = class {
2955
+ constructor(keymap, host, onOpenChange) {
2956
+ this.keymap = keymap;
2957
+ const doc = (host ?? document.body).ownerDocument;
2958
+ injectStyles(STYLE_ID8, CSS8, doc);
2959
+ this.list = doc.createElement("div");
2960
+ this.dialog = new Dialog({
2961
+ title: "Keyboard shortcuts",
2962
+ host,
2963
+ closeOnInteractOutside: true,
2964
+ content: (body) => body.appendChild(this.list),
2965
+ onOpenChange: (open) => {
2966
+ if (open) this.refresh();
2967
+ onOpenChange?.(open);
2968
+ }
2969
+ });
2970
+ }
2971
+ open() {
2972
+ this.dialog.show();
2973
+ }
2974
+ close() {
2975
+ this.dialog.hide();
2976
+ }
2977
+ destroy() {
2978
+ this.dialog.destroy();
2979
+ }
2980
+ refresh() {
2981
+ const doc = this.list.ownerDocument;
2982
+ this.list.replaceChildren();
2983
+ const addCategory = (text) => {
2984
+ const cat = doc.createElement("div");
2985
+ cat.className = "vela-sh-cat";
2986
+ cat.textContent = text;
2987
+ this.list.appendChild(cat);
2988
+ };
2989
+ const addRow = (label, keys) => {
2990
+ const row = doc.createElement("div");
2991
+ row.className = "vela-sh-row";
2992
+ const l = doc.createElement("span");
2993
+ l.className = "vela-sh-label";
2994
+ l.textContent = label;
2995
+ const ks = doc.createElement("span");
2996
+ ks.className = "vela-sh-keys";
2997
+ for (const key of keys) {
2998
+ const k = doc.createElement("span");
2999
+ k.className = "vela-sh-key";
3000
+ k.textContent = key;
3001
+ ks.appendChild(k);
3002
+ }
3003
+ row.append(l, ks);
3004
+ this.list.appendChild(row);
3005
+ };
3006
+ const byCategory = /* @__PURE__ */ new Map();
3007
+ for (const b of this.keymap.bindings()) {
3008
+ const bucket = byCategory.get(b.category) ?? [];
3009
+ bucket.push(b);
3010
+ byCategory.set(b.category, bucket);
3011
+ }
3012
+ for (const [category, bindings] of byCategory) {
3013
+ addCategory(category);
3014
+ for (const b of bindings) addRow(b.label, b.display);
3015
+ }
3016
+ addCategory("Mouse");
3017
+ addRow("Scroll through history", ["Shift+Scroll"]);
3018
+ addRow("Measure from the press point", ["Shift+Click"]);
3019
+ addRow("Delete the drawing under the cursor", ["Middle-click"]);
3020
+ const s = doc.createElement("div");
3021
+ s.className = "vela-sh-static";
3022
+ s.textContent = "Typing a letter opens the symbol search; typing a digit opens the timeframe entry.";
3023
+ this.list.appendChild(s);
3024
+ }
3025
+ };
3026
+
3027
+ // src/widget/context-menu-model.ts
3028
+ var SETTINGS_SECTION = {
3029
+ body: "Canvas",
3030
+ "price-axis": "Scales and lines",
3031
+ "time-axis": "Scales and lines"
3032
+ };
3033
+ function settingsItem(zone, label) {
3034
+ return { id: `settings:${SETTINGS_SECTION[zone]}`, label, separatorBefore: true };
3035
+ }
3036
+ function settingsSectionOf(id) {
3037
+ return id.slice("settings:".length) || void 0;
3038
+ }
3039
+ var SCALE_CHOICES = [
3040
+ ["regular", "Regular"],
3041
+ ["percent", "Percent"],
3042
+ ["indexed", "Indexed to 100"],
3043
+ ["log", "Logarithmic"]
3044
+ ];
3045
+ function paneScaleAt(panes, y) {
3046
+ return panes.find((p) => y >= p.top && y < p.top + p.height) ?? panes[0] ?? null;
3047
+ }
3048
+ function scaleChoiceOf(scale) {
3049
+ if (scale.log) return "log";
3050
+ if (scale.mode === "percent") return "percent";
3051
+ if (scale.mode === "indexed") return "indexed";
3052
+ return "regular";
3053
+ }
3054
+ function mainScale(pane) {
3055
+ return pane === null || pane.kind === "price";
3056
+ }
3057
+ function scaleWrites(choice, pane) {
3058
+ const mode = choice === "percent" ? "percent" : choice === "indexed" ? "indexed" : "price";
3059
+ const log = choice === "log";
3060
+ if (mainScale(pane)) {
3061
+ return [
3062
+ ["scaleMode", mode],
3063
+ ["logScale", log]
3064
+ ];
3065
+ }
3066
+ return [
3067
+ ["scaleMode", { pane: pane.id, mode }],
3068
+ ["logScale", { pane: pane.id, value: log }]
3069
+ ];
3070
+ }
3071
+ function invertWrite(next, pane) {
3072
+ return mainScale(pane) ? ["invertScale", next] : ["invertScale", { pane: pane.id, value: next }];
3073
+ }
3074
+ function priceAxisItems(s) {
3075
+ return [
3076
+ { id: "auto", label: "Auto (fits data to screen)", checked: s.auto },
3077
+ { id: "invert", label: "Invert scale", checked: s.invert },
3078
+ ...SCALE_CHOICES.map(([choice, label], i) => ({
3079
+ id: `scale:${choice}`,
3080
+ label,
3081
+ checked: s.choice === choice,
3082
+ separatorBefore: i === 0
3083
+ })),
3084
+ {
3085
+ id: "labels",
3086
+ label: "Labels",
3087
+ separatorBefore: true,
3088
+ submenu: [
3089
+ { id: "toggle:axisLabels", label: "Price axis labels", checked: s.axisLabels },
3090
+ { id: "toggle:priceLabel", label: "Last price label", checked: s.priceLabel },
3091
+ { id: "toggle:countdown", label: "Countdown to bar close", checked: s.countdown }
3092
+ ]
3093
+ },
3094
+ {
3095
+ id: "levels",
3096
+ label: "Levels",
3097
+ submenu: [{ id: "toggle:currentPriceLine", label: "Last Price Line", checked: s.priceLine }]
3098
+ },
3099
+ settingsItem("price-axis", "More settings\u2026")
3100
+ ];
3101
+ }
3102
+ function timeAxisItems(timezone) {
3103
+ const active = timezone === "UTC" ? "Etc/UTC" : timezone;
3104
+ return [
3105
+ {
3106
+ id: "timezone",
3107
+ label: "Time zone",
3108
+ submenu: TIMEZONES.map((t) => ({ id: `tz:${t.value}`, label: tzMenuLabel(t.value, t.label), checked: t.value === active }))
3109
+ },
3110
+ settingsItem("time-axis", "More settings\u2026")
3111
+ ];
3112
+ }
3113
+ function bodyItems(counts) {
3114
+ return [
3115
+ { id: "reset-view", label: "Reset chart view" },
3116
+ { id: "remove-drawings", label: "Remove drawings", disabled: counts.drawings === 0, separatorBefore: true },
3117
+ { id: "remove-indicators", label: "Remove indicators", disabled: counts.indicators === 0 },
3118
+ settingsItem("body", "Settings\u2026")
3119
+ ];
3120
+ }
3121
+
3122
+ // src/widget/context-menu.ts
3123
+ var PRICE_AXIS_W = 60;
3124
+ var TIME_AXIS_H = 26;
3125
+ var ChartContextMenu = class {
3126
+ constructor(host, cbs) {
3127
+ this.cbs = cbs;
3128
+ this.chart = null;
3129
+ this.lastZone = "body";
3130
+ /** The pane whose scale the open price-axis menu targets (null ⇒ the main scale). */
3131
+ this.lastPane = null;
3132
+ this.onContextMenu = (e) => {
3133
+ e.preventDefault();
3134
+ if (!this.chart) return;
3135
+ this.lastZone = this.zoneOf(e);
3136
+ this.lastPane = this.lastZone === "price-axis" ? this.paneAt(e) : null;
3137
+ this.menu.setItems(this.itemsFor(this.lastZone));
3138
+ this.menu.openAt(e.clientX, e.clientY);
3139
+ };
3140
+ this.host = host;
3141
+ this.menu = new Menu({
3142
+ host,
3143
+ items: [],
3144
+ placement: "bottom-start",
3145
+ onSelect: (id) => this.run(id)
3146
+ });
3147
+ host.addEventListener("contextmenu", this.onContextMenu);
3148
+ }
3149
+ /** (Re)bind to a chart instance — called after every widget rebuild. */
3150
+ onChart(chart) {
3151
+ this.chart = chart;
3152
+ }
3153
+ destroy() {
3154
+ this.host.removeEventListener("contextmenu", this.onContextMenu);
3155
+ this.menu.destroy();
3156
+ }
3157
+ zoneOf(e) {
3158
+ const rect = this.host.getBoundingClientRect();
3159
+ if (e.clientX - rect.left > rect.width - PRICE_AXIS_W) return "price-axis";
3160
+ if (e.clientY - rect.top > rect.height - TIME_AXIS_H) return "time-axis";
3161
+ return "body";
3162
+ }
3163
+ /** The pane under the pointer, so every pane's price scale has its own menu. */
3164
+ paneAt(e) {
3165
+ const panes = this.chart?.renderer.get("paneScales");
3166
+ if (!Array.isArray(panes)) return null;
3167
+ return paneScaleAt(panes, e.clientY - this.host.getBoundingClientRect().top);
3168
+ }
3169
+ flag(feature) {
3170
+ return Boolean(this.chart?.renderer.get(feature));
3171
+ }
3172
+ contributed(zone) {
3173
+ const ctx = this.cbs.getContext?.();
3174
+ return widgetActions(`context:${zone}`, ctx).map((a, i) => ({
3175
+ id: `action:${a.id}`,
3176
+ label: a.label,
3177
+ icon: a.icon,
3178
+ separatorBefore: i === 0
3179
+ }));
3180
+ }
3181
+ itemsFor(zone) {
3182
+ if (zone === "price-axis") {
3183
+ const pane = this.lastPane;
3184
+ return [
3185
+ ...priceAxisItems({
3186
+ auto: this.chart?.renderer.get("autoScale") !== false,
3187
+ invert: pane ? pane.invert : this.flag("invertScale"),
3188
+ choice: scaleChoiceOf(pane ?? { mode: String(this.chart?.renderer.get("scaleMode") ?? "price"), log: this.flag("logScale") }),
3189
+ axisLabels: this.flag("axisLabels"),
3190
+ priceLabel: this.flag("priceLabel"),
3191
+ countdown: this.flag("countdown"),
3192
+ priceLine: this.flag("currentPriceLine")
3193
+ }),
3194
+ ...this.contributed(zone)
3195
+ ];
3196
+ }
3197
+ if (zone === "time-axis") {
3198
+ const tz = this.cbs.timezone?.() ?? String(this.chart?.renderer.get("timezone") ?? "Etc/UTC");
3199
+ return [...timeAxisItems(tz), ...this.contributed("time-axis")];
3200
+ }
3201
+ const chart = this.chart;
3202
+ return [
3203
+ ...bodyItems({
3204
+ drawings: chart?.drawings.supported ? chart.drawings.all().length : 0,
3205
+ indicators: chart?.indicators().length ?? 0
3206
+ }),
3207
+ ...this.contributed("body")
3208
+ ];
3209
+ }
3210
+ run(id) {
3211
+ const chart = this.chart;
3212
+ if (!chart) return;
3213
+ if (id.startsWith("action:")) {
3214
+ const ctx = this.cbs.getContext?.();
3215
+ if (ctx) widgetActions(`context:${this.lastZone}`, ctx).find((a) => a.id === id.slice("action:".length))?.run(ctx);
3216
+ return;
3217
+ }
3218
+ if (id.startsWith("toggle:")) {
3219
+ const feature = id.slice("toggle:".length);
3220
+ chart.renderer.set(feature, !this.flag(feature));
3221
+ } else if (id.startsWith("scale:")) {
3222
+ for (const [feature, value] of scaleWrites(id.slice("scale:".length), this.lastPane)) chart.renderer.set(feature, value);
3223
+ } else if (id.startsWith("tz:")) {
3224
+ const zone = id.slice("tz:".length);
3225
+ if (this.cbs.setTimezone) this.cbs.setTimezone(zone);
3226
+ else chart.renderer.set("timezone", zone);
3227
+ } else if (id === "auto") {
3228
+ chart.renderer.set("autoScale", chart.renderer.get("autoScale") === false);
3229
+ } else if (id === "invert") {
3230
+ const pane = this.lastPane;
3231
+ const [feature, value] = invertWrite(!(pane ? pane.invert : this.flag("invertScale")), pane);
3232
+ chart.renderer.set(feature, value);
3233
+ } else if (id.startsWith("settings")) {
3234
+ chart.renderer.openSettings(settingsSectionOf(id));
3235
+ } else if (id === "reset-view") {
3236
+ this.cbs.resetView();
3237
+ } else if (id === "remove-drawings") {
3238
+ if (chart.drawings.supported) for (const d of chart.drawings.all()) chart.drawings.remove(d.id);
3239
+ } else if (id === "remove-indicators") {
3240
+ for (const handle of chart.indicators()) handle.remove();
3241
+ }
3242
+ }
3243
+ };
3244
+
3245
+ // src/widget/indicator-picker.ts
3246
+ var STYLE_ID9 = "vela-widget-indpicker";
3247
+ var CSS9 = `
3248
+ .vela-ip-searchrow {
3249
+ display: flex;
3250
+ align-items: center;
3251
+ gap: 8px;
3252
+ height: 40px;
3253
+ padding: 0 12px;
3254
+ margin-bottom: var(--vela-space-2);
3255
+ background: var(--vela-surface-elev);
3256
+ border: 1px solid var(--vela-border);
3257
+ border-radius: 8px;
3258
+ }
3259
+ .vela-ip-searchrow:focus-within { border-color: var(--vela-border-strong); }
3260
+ .vela-ip-searchrow .vela-icon { color: var(--vela-fg-muted); }
3261
+ .vela-ip-search { flex: 1; background: transparent; color: var(--vela-fg); border: none; font-size: 14px; outline: none; }
3262
+ .vela-ip-list { max-height: 50vh; overflow: auto; min-width: 380px; }
3263
+ .vela-ip-list::-webkit-scrollbar { width: 8px; }
3264
+ .vela-ip-list::-webkit-scrollbar-thumb { background: var(--vela-scroll); border-radius: 4px; border: 2px solid transparent; background-clip: padding-box; }
3265
+ .vela-ip-group {
3266
+ color: var(--vela-fg-muted);
3267
+ font-size: 10px;
3268
+ font-weight: 700;
3269
+ text-transform: uppercase;
3270
+ letter-spacing: 0.08em;
3271
+ padding: 10px 4px 4px;
3272
+ border-bottom: 1px solid var(--vela-border);
3273
+ margin-bottom: 2px;
3274
+ }
3275
+ .vela-ip-oncard {
3276
+ background: var(--vela-surface-elev);
3277
+ border: 1px solid var(--vela-border);
3278
+ border-radius: 8px;
3279
+ padding: 6px 8px;
3280
+ margin-bottom: var(--vela-space-2);
3281
+ }
3282
+ .vela-ip-oncard .vela-ip-group { border-bottom: none; padding-top: 2px; }
3283
+ .vela-ip-row {
3284
+ display: flex;
3285
+ align-items: center;
3286
+ gap: var(--vela-space-2);
3287
+ padding: 7px 6px;
3288
+ border-radius: var(--vela-radius-sm);
3289
+ cursor: pointer;
3290
+ }
3291
+ .vela-ip-row:hover { background: var(--vela-hover); }
3292
+ .vela-ip-name { flex: 1; font-weight: 600; color: var(--vela-fg-bright); font-size: 13px; }
3293
+ .vela-ip-badge {
3294
+ flex: none;
3295
+ padding: 1px 7px;
3296
+ border-radius: var(--vela-radius-sm);
3297
+ background: var(--vela-surface-elev);
3298
+ border: 1px solid var(--vela-border);
3299
+ color: var(--vela-accent);
3300
+ font-size: 9px;
3301
+ font-weight: 700;
3302
+ letter-spacing: 0.06em;
3303
+ text-transform: uppercase;
3304
+ }
3305
+ .vela-ip-trash {
3306
+ all: unset;
3307
+ cursor: pointer;
3308
+ color: var(--vela-fg-muted);
3309
+ padding: 2px 4px;
3310
+ border-radius: var(--vela-radius-sm);
3311
+ font-size: var(--vela-font-size-md);
3312
+ }
3313
+ .vela-ip-trash:hover { color: var(--vela-down); background: var(--vela-hover); }
3314
+ .vela-ip-empty { padding: var(--vela-space-3); color: var(--vela-fg-muted); text-align: center; }
3315
+ `;
3316
+ var IndicatorPicker = class {
3317
+ constructor(opts) {
3318
+ this.isOpen = false;
3319
+ this.opts = opts;
3320
+ const doc = (opts.host ?? document.body).ownerDocument;
3321
+ injectStyles(STYLE_ID9, CSS9, doc);
3322
+ this.search = doc.createElement("input");
3323
+ this.search.className = "vela-ip-search";
3324
+ this.search.placeholder = "Search\u2026";
3325
+ this.search.spellcheck = false;
3326
+ this.search.addEventListener("input", () => this.refresh());
3327
+ const searchRow = doc.createElement("div");
3328
+ searchRow.className = "vela-ip-searchrow";
3329
+ searchRow.append(iconEl("search", doc), this.search);
3330
+ this.list = doc.createElement("div");
3331
+ this.list.className = "vela-ip-list";
3332
+ this.dialog = new Dialog({
3333
+ title: "Indicators",
3334
+ host: opts.host,
3335
+ draggable: true,
3336
+ closeOnInteractOutside: true,
3337
+ // The picker adds/removes indicators live — keep the chart undimmed behind it.
3338
+ content: (body) => body.append(searchRow, this.list),
3339
+ onOpenChange: (open) => {
3340
+ this.isOpen = open;
3341
+ if (open) {
3342
+ this.search.value = "";
3343
+ this.refresh();
3344
+ setTimeout(() => this.search.focus(), 0);
3345
+ }
3346
+ opts.onOpenChange?.(open);
3347
+ }
3348
+ });
3349
+ this.list.addEventListener("click", (e) => {
3350
+ const trash = e.target.closest(".vela-ip-trash");
3351
+ const row = e.target.closest(".vela-ip-row");
3352
+ if (!row) return;
3353
+ if (trash) {
3354
+ this.opts.onRemove(Number(row.dataset.instance));
3355
+ } else if (row.dataset.library !== void 0) {
3356
+ this.opts.onAdd(Number(row.dataset.library));
3357
+ }
3358
+ this.refresh();
3359
+ });
3360
+ }
3361
+ open() {
3362
+ this.dialog.show();
3363
+ }
3364
+ /** Re-render the lists if the dialog is open (async catalog updates land late). */
3365
+ sync() {
3366
+ if (this.isOpen) this.refresh();
3367
+ }
3368
+ close() {
3369
+ this.dialog.hide();
3370
+ }
3371
+ destroy() {
3372
+ this.dialog.destroy();
3373
+ }
3374
+ refresh() {
3375
+ const doc = this.list.ownerDocument;
3376
+ const q = this.search.value.trim().toLowerCase();
3377
+ const library = this.opts.library();
3378
+ const instances = this.opts.onChart();
3379
+ this.list.replaceChildren();
3380
+ const match = (r) => !q || r.name.toLowerCase().includes(q);
3381
+ const rowEl = (r, opts) => {
3382
+ const row = doc.createElement("div");
3383
+ row.className = "vela-ip-row";
3384
+ if (opts.library !== void 0) row.dataset.library = String(opts.library);
3385
+ if (opts.instance !== void 0) row.dataset.instance = String(opts.instance);
3386
+ if (r.native) row.dataset.native = "1";
3387
+ const name = doc.createElement("span");
3388
+ name.className = "vela-ip-name";
3389
+ name.textContent = r.name;
3390
+ row.appendChild(name);
3391
+ if (r.beta) {
3392
+ const beta = doc.createElement("span");
3393
+ beta.className = "vela-ip-badge";
3394
+ beta.textContent = "beta";
3395
+ row.appendChild(beta);
3396
+ }
3397
+ if (opts.instance !== void 0) {
3398
+ const badgeText = r.native ? "vela" : r.language;
3399
+ if (badgeText) {
3400
+ const badge = doc.createElement("span");
3401
+ badge.className = "vela-ip-badge";
3402
+ badge.textContent = badgeText;
3403
+ row.appendChild(badge);
3404
+ }
3405
+ const trash = doc.createElement("button");
3406
+ trash.className = "vela-ip-trash";
3407
+ trash.appendChild(iconEl("trash", doc));
3408
+ trash.title = "Remove from chart";
3409
+ row.appendChild(trash);
3410
+ }
3411
+ return row;
3412
+ };
3413
+ const onChart = instances.map((r, i) => [r, i]).filter(([r]) => match(r));
3414
+ if (onChart.length) {
3415
+ const card = doc.createElement("div");
3416
+ card.className = "vela-ip-oncard";
3417
+ const title = doc.createElement("div");
3418
+ title.className = "vela-ip-group";
3419
+ title.textContent = `On chart \xB7 ${onChart.length}`;
3420
+ card.appendChild(title);
3421
+ for (const [r, i] of onChart) card.appendChild(rowEl(r, { instance: i }));
3422
+ this.list.appendChild(card);
3423
+ }
3424
+ const byCat = /* @__PURE__ */ new Map();
3425
+ library.forEach((r, i) => {
3426
+ if (!match(r)) return;
3427
+ const cat = r.category ?? "General";
3428
+ const bucket = byCat.get(cat) ?? [];
3429
+ bucket.push([r, i]);
3430
+ byCat.set(cat, bucket);
3431
+ });
3432
+ for (const [cat, bucket] of byCat) {
3433
+ const title = doc.createElement("div");
3434
+ title.className = "vela-ip-group";
3435
+ title.textContent = cat;
3436
+ this.list.appendChild(title);
3437
+ for (const [r, i] of bucket) this.list.appendChild(rowEl(r, { library: i }));
3438
+ }
3439
+ if (!onChart.length && byCat.size === 0) {
3440
+ const empty = doc.createElement("div");
3441
+ empty.className = "vela-ip-empty";
3442
+ empty.textContent = library.length ? "No indicators match." : "No indicators in the manifest.";
3443
+ this.list.appendChild(empty);
3444
+ }
3445
+ }
3446
+ };
3447
+
3448
+ // src/widget/timeframe-quick.ts
3449
+ var STYLE_ID10 = "vela-widget-tfquick";
3450
+ var CSS10 = `
3451
+ .vela-tq-input {
3452
+ display: block;
3453
+ margin: 0 auto;
3454
+ width: 220px;
3455
+ box-sizing: border-box;
3456
+ height: 40px;
3457
+ background: var(--vela-surface-elev);
3458
+ color: var(--vela-fg);
3459
+ border: 1px solid var(--vela-border);
3460
+ border-radius: 8px;
3461
+ padding: 0 12px;
3462
+ font-size: 18px;
3463
+ text-align: center;
3464
+ outline: none;
3465
+ }
3466
+ .vela-tq-input:focus { border-color: var(--vela-border-strong); }
3467
+ .vela-tq-hint { margin-top: var(--vela-space-2); text-align: center; color: var(--vela-fg-muted); min-height: 1.2em; }
3468
+ .vela-tq-hint[data-invalid] { color: var(--vela-danger); }
3469
+ `;
3470
+ var TimeframeQuick = class {
3471
+ constructor(opts) {
3472
+ const doc = (opts.host ?? document.body).ownerDocument;
3473
+ injectStyles(STYLE_ID10, CSS10, doc);
3474
+ this.input = doc.createElement("input");
3475
+ this.input.className = "vela-tq-input";
3476
+ this.input.setAttribute("spellcheck", "false");
3477
+ this.hint = doc.createElement("div");
3478
+ this.hint.className = "vela-tq-hint";
3479
+ this.dialog = new Dialog({
3480
+ title: "Change timeframe",
3481
+ host: opts.host,
3482
+ draggable: true,
3483
+ closeOnInteractOutside: true,
3484
+ content: (body) => body.append(this.input, this.hint),
3485
+ onOpenChange: (open) => opts.onOpenChange?.(open)
3486
+ });
3487
+ this.input.addEventListener("input", () => this.renderHint());
3488
+ this.input.addEventListener("keydown", (e) => {
3489
+ if (e.key !== "Enter") return;
3490
+ const parsed = parseTimeframe(this.input.value);
3491
+ if (parsed.valid && parsed.canonical !== void 0) {
3492
+ this.close();
3493
+ opts.onApply(parsed.canonical);
3494
+ }
3495
+ });
3496
+ }
3497
+ open(seed = "") {
3498
+ this.dialog.show();
3499
+ this.input.value = seed;
3500
+ this.renderHint();
3501
+ setTimeout(() => {
3502
+ this.input.focus();
3503
+ this.input.setSelectionRange(this.input.value.length, this.input.value.length);
3504
+ }, 0);
3505
+ }
3506
+ close() {
3507
+ this.dialog.hide();
3508
+ }
3509
+ destroy() {
3510
+ this.dialog.destroy();
3511
+ }
3512
+ renderHint() {
3513
+ const raw = this.input.value.trim();
3514
+ const parsed = parseTimeframe(raw);
3515
+ if (!raw) {
3516
+ this.hint.textContent = "e.g. 15, 4h, D, 3M";
3517
+ delete this.hint.dataset.invalid;
3518
+ } else if (parsed.valid) {
3519
+ this.hint.textContent = parsed.label ?? "";
3520
+ delete this.hint.dataset.invalid;
3521
+ } else {
3522
+ this.hint.textContent = "Not a timeframe";
3523
+ this.hint.dataset.invalid = "1";
3524
+ }
3525
+ }
3526
+ };
3527
+
3528
+ // src/widget/persist.ts
3529
+ function localStorageAdapter(storageKey) {
3530
+ return {
3531
+ get(key) {
3532
+ try {
3533
+ return localStorage.getItem(storageKey ?? key);
3534
+ } catch {
3535
+ return null;
3536
+ }
3537
+ },
3538
+ set(key, value) {
3539
+ try {
3540
+ localStorage.setItem(storageKey ?? key, value);
3541
+ } catch {
3542
+ }
3543
+ },
3544
+ remove(key) {
3545
+ try {
3546
+ localStorage.removeItem(storageKey ?? key);
3547
+ } catch {
3548
+ }
3549
+ }
3550
+ };
3551
+ }
3552
+ var KEYS = ["symbol", "timeframe", "priceStyle", "timezone", "bars", "watermark", "favorites"];
3553
+ function parsePersisted(raw) {
3554
+ if (!raw) return {};
3555
+ try {
3556
+ const doc = JSON.parse(raw);
3557
+ const out = {};
3558
+ for (const k of KEYS) if (typeof doc[k] === "string") out[k] = doc[k];
3559
+ return out;
3560
+ } catch {
3561
+ return {};
3562
+ }
3563
+ }
3564
+ var parseState = parsePersisted;
3565
+ function loadPersisted(storage, storageKey) {
3566
+ const raw = storage.get(storageKey);
3567
+ if (raw instanceof Promise) return raw.then(parseState).catch(() => ({}));
3568
+ return parseState(raw);
3569
+ }
3570
+ function savePersisted(storage, storageKey, state) {
3571
+ try {
3572
+ void storage.set(storageKey, JSON.stringify(state));
3573
+ } catch {
3574
+ }
3575
+ }
3576
+ function legacyWidgetState(prefs, rawConfig, rawDrawings) {
3577
+ const cell = {};
3578
+ if (prefs.symbol) cell.symbol = prefs.symbol;
3579
+ if (prefs.timeframe) cell.timeframe = prefs.timeframe;
3580
+ if (prefs.priceStyle) cell.priceStyle = prefs.priceStyle;
3581
+ const bars = Number(prefs.bars);
3582
+ if (Number.isFinite(bars) && bars > 0) cell.bars = bars;
3583
+ if (prefs.watermark !== void 0) cell.watermark = prefs.watermark === "1";
3584
+ try {
3585
+ if (rawConfig) cell.rendererConfig = JSON.parse(rawConfig);
3586
+ } catch {
3587
+ }
3588
+ try {
3589
+ if (rawDrawings) cell.drawings = JSON.parse(rawDrawings);
3590
+ } catch {
3591
+ }
3592
+ const favorites = prefs.favorites ? prefs.favorites.split(",").filter(Boolean) : [];
3593
+ if (Object.keys(cell).length === 0 && favorites.length === 0 && !prefs.timezone) return null;
3594
+ const doc = { version: 1, layout: "1", activeCellId: "c1", charts: [{ id: "c1", ...cell }] };
3595
+ if (prefs.timezone) doc.timezone = prefs.timezone;
3596
+ if (favorites.length > 0) doc.favorites = favorites;
3597
+ return doc;
3598
+ }
3599
+
3600
+ // src/state/document.ts
3601
+ function prefixedSymbol(cell) {
3602
+ if (!cell?.symbol) return void 0;
3603
+ if (cell.symbol.includes(":") || !cell.provider) return cell.symbol;
3604
+ return `${cell.provider}:${cell.symbol}`;
3605
+ }
3606
+ function encodeState(state) {
3607
+ return JSON.stringify(state);
3608
+ }
3609
+ function decodeState(raw) {
3610
+ try {
3611
+ return sanitizeState(JSON.parse(raw));
3612
+ } catch {
3613
+ return null;
3614
+ }
3615
+ }
3616
+ function sanitizeState(doc) {
3617
+ if (doc == null || typeof doc !== "object") return null;
3618
+ const d = doc;
3619
+ if (d.version !== 1 || typeof d.layout !== "string") return null;
3620
+ const out = { version: 1, layout: d.layout, charts: [] };
3621
+ if (Array.isArray(d.charts)) {
3622
+ const byId = /* @__PURE__ */ new Map();
3623
+ for (const raw of d.charts) {
3624
+ const id = raw != null && typeof raw === "object" ? raw.id : void 0;
3625
+ if (typeof id !== "string" || id.length === 0) continue;
3626
+ const cell = sanitizeCell(raw);
3627
+ if (cell) byId.set(id, { id, ...cell });
3628
+ }
3629
+ out.charts = [...byId.values()];
3630
+ }
3631
+ if (typeof d.activeCellId === "string") out.activeCellId = d.activeCellId;
3632
+ if (typeof d.timezone === "string" && d.timezone) out.timezone = d.timezone;
3633
+ if (Array.isArray(d.favorites)) {
3634
+ const favs = d.favorites.filter((f) => typeof f === "string");
3635
+ if (favs.length > 0) out.favorites = favs;
3636
+ }
3637
+ const sync = sanitizeSync(d.sync);
3638
+ if (sync) out.sync = sync;
3639
+ const tracks = sanitizeTrackSizes(d.trackSizes);
3640
+ if (tracks) out.trackSizes = tracks;
3641
+ const panels = sanitizePanels(d.panels);
3642
+ if (panels) out.panels = panels;
3643
+ return out;
3644
+ }
3645
+ function sanitizeCell(raw) {
3646
+ if (raw == null || typeof raw !== "object") return null;
3647
+ const c = raw;
3648
+ const out = {};
3649
+ if (typeof c.symbol === "string") out.symbol = c.symbol;
3650
+ if (typeof c.provider === "string") out.provider = c.provider;
3651
+ if (typeof c.timeframe === "string") out.timeframe = c.timeframe;
3652
+ if (typeof c.priceStyle === "string") out.priceStyle = c.priceStyle;
3653
+ if (typeof c.bars === "number" && Number.isFinite(c.bars) && c.bars > 0) out.bars = c.bars;
3654
+ if (typeof c.watermark === "boolean") out.watermark = c.watermark;
3655
+ if (typeof c.indicatorTitles === "boolean") out.indicatorTitles = c.indicatorTitles;
3656
+ if (c.rendererConfig != null && typeof c.rendererConfig === "object") out.rendererConfig = c.rendererConfig;
3657
+ if (c.drawings != null && typeof c.drawings === "object") out.drawings = c.drawings;
3658
+ const ind = c.indicators;
3659
+ if (ind != null && typeof ind === "object") {
3660
+ const manifest = Array.isArray(ind.manifest) ? ind.manifest.filter((n) => typeof n === "string") : [];
3661
+ const natives = Array.isArray(ind.natives) ? ind.natives.filter((n) => typeof n === "string") : [];
3662
+ out.indicators = { manifest, natives };
3663
+ }
3664
+ return out;
3665
+ }
3666
+ function sanitizeSync(raw) {
3667
+ if (raw == null || typeof raw !== "object") return null;
3668
+ const s = raw;
3669
+ const out = {};
3670
+ for (const kind of ["viewport", "symbol", "timeframe", "crosshair"]) {
3671
+ const v = s[kind];
3672
+ if (v === true) out[kind] = true;
3673
+ else if (v != null && typeof v === "object") {
3674
+ const groups = {};
3675
+ for (const [id, g] of Object.entries(v)) if (typeof g === "string") groups[id] = g;
3676
+ if (Object.keys(groups).length > 0) out[kind] = groups;
3677
+ }
3678
+ }
3679
+ return Object.keys(out).length > 0 ? out : null;
3680
+ }
3681
+ function sanitizePanels(raw) {
3682
+ if (raw == null || typeof raw !== "object") return null;
3683
+ const p = raw;
3684
+ const out = {};
3685
+ if (typeof p.open === "string" && p.open) out.open = p.open;
3686
+ if (p.widths != null && typeof p.widths === "object") {
3687
+ const widths = {};
3688
+ for (const [id, px] of Object.entries(p.widths)) {
3689
+ if (typeof px === "number" && Number.isFinite(px) && px > 0) widths[id] = px;
3690
+ }
3691
+ if (Object.keys(widths).length > 0) out.widths = widths;
3692
+ }
3693
+ return out.open || out.widths ? out : null;
3694
+ }
3695
+ function sanitizeTrackSizes(raw) {
3696
+ if (raw == null || typeof raw !== "object") return null;
3697
+ const out = {};
3698
+ for (const [layoutId, ts] of Object.entries(raw)) {
3699
+ if (ts == null || typeof ts !== "object") continue;
3700
+ const t = ts;
3701
+ const entry = {};
3702
+ for (const axis of ["cols", "rows"]) {
3703
+ const arr = t[axis];
3704
+ if (Array.isArray(arr) && arr.length > 0 && arr.every((w) => typeof w === "number" && Number.isFinite(w) && w > 0)) {
3705
+ entry[axis] = arr;
3706
+ }
3707
+ }
3708
+ if (entry.cols || entry.rows) out[layoutId] = entry;
3709
+ }
3710
+ return Object.keys(out).length > 0 ? out : null;
3711
+ }
3712
+
3713
+ // src/widget/indicators.ts
3714
+ function entriesOf(manifest) {
3715
+ return Array.isArray(manifest) ? manifest : manifest.indicators;
3716
+ }
3717
+ async function resolveIndicators(config, fetchImpl = fetch) {
3718
+ let manifest;
3719
+ let baseUrl;
3720
+ if (typeof config === "function") {
3721
+ manifest = await config();
3722
+ } else if (typeof config === "string") {
3723
+ baseUrl = config;
3724
+ const res = await fetchImpl(config);
3725
+ if (!res.ok) throw new Error(`indicator manifest ${config}: HTTP ${res.status}`);
3726
+ manifest = await res.json();
3727
+ } else {
3728
+ manifest = config;
3729
+ }
3730
+ const out = [];
3731
+ for (const entry of entriesOf(manifest)) {
3732
+ try {
3733
+ let script = entry.script;
3734
+ if (script === void 0 && entry.url !== void 0) {
3735
+ let url = entry.url;
3736
+ try {
3737
+ const base = typeof location !== "undefined" ? new URL(baseUrl ?? ".", location.href) : new URL(baseUrl ?? "");
3738
+ url = new URL(entry.url, base).href;
3739
+ } catch {
3740
+ }
3741
+ const res = await fetchImpl(url);
3742
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
3743
+ script = await res.text();
3744
+ }
3745
+ if (script === void 0) throw new Error("entry has neither script nor url");
3746
+ out.push({ name: entry.name, script, language: entry.language, enabled: entry.enabled !== false, category: entry.category });
3747
+ } catch (err) {
3748
+ console.warn(`[vela] indicator "${entry.name}" skipped:`, err);
3749
+ }
3750
+ }
3751
+ return out;
3752
+ }
3753
+ function indicatorLedger(i) {
3754
+ const natives = [...i.present];
3755
+ if (i.volumePending && !natives.includes("volume")) natives.push("volume");
3756
+ return {
3757
+ manifest: i.manifestSettled ? [...i.instanceNames] : [...i.pendingManifest ?? i.instanceNames],
3758
+ natives
3759
+ };
3760
+ }
3761
+
3762
+ // src/widget/glide.ts
3763
+ var MIN_SPAN_MS = 6e4;
3764
+ var FOLLOW = 0.25;
3765
+ var ZOOM_IN = 0.8;
3766
+ var ZOOM_OUT = 1.25;
3767
+ var PAN_FAST = 0.2;
3768
+ function zoomTarget(base, factor) {
3769
+ const newSpan = Math.max(MIN_SPAN_MS, (base.to - base.from) * factor);
3770
+ return { from: base.to - newSpan, to: base.to };
3771
+ }
3772
+ function followStep(cur, target, follow = FOLLOW, epsFrac = 15e-4) {
3773
+ const eps = Math.max(MIN_SPAN_MS, (target.to - target.from) * epsFrac);
3774
+ const nf = cur.from + (target.from - cur.from) * follow;
3775
+ const nt = cur.to + (target.to - cur.to) * follow;
3776
+ const done = Math.abs(target.from - nf) <= eps && Math.abs(target.to - nt) <= eps;
3777
+ return { cur: done ? { ...target } : { from: nf, to: nt }, done };
3778
+ }
3779
+ var Glider = class {
3780
+ constructor(chart) {
3781
+ this.chart = chart;
3782
+ // The glide eases ITS OWN range, never the chart's read-back: getVisibleRange()
3783
+ // is data-bounded (whole bars, `to` capped at the newest bar) and the renderer
3784
+ // clamps what gets applied — chasing that read-back can never converge on a
3785
+ // clamped step, leaving the rAF loop re-applying a stale target forever (and
3786
+ // grinding the span down when panning past the newest bar). The internal range
3787
+ // converges geometrically, so the glide always terminates; the renderer is free
3788
+ // to clamp each applied frame.
3789
+ this.cur = null;
3790
+ this.target = null;
3791
+ this.raf = 0;
3792
+ }
3793
+ zoom(factor) {
3794
+ this.to((base) => zoomTarget(base, factor));
3795
+ }
3796
+ stop() {
3797
+ if (this.raf) cancelAnimationFrame(this.raf);
3798
+ this.raf = 0;
3799
+ this.cur = null;
3800
+ this.target = null;
3801
+ }
3802
+ to(make) {
3803
+ const chart = this.chart();
3804
+ const base = this.target ?? chart?.getVisibleRange();
3805
+ if (!chart || !base) return;
3806
+ this.cur ?? (this.cur = { ...base });
3807
+ this.target = make(base);
3808
+ if (!this.raf) this.tick();
3809
+ }
3810
+ tick() {
3811
+ this.raf = requestAnimationFrame(() => {
3812
+ const chart = this.chart();
3813
+ if (!chart || !this.cur || !this.target) {
3814
+ this.stop();
3815
+ return;
3816
+ }
3817
+ const { cur: next, done } = followStep(this.cur, this.target);
3818
+ this.cur = next;
3819
+ chart.setVisibleRange(next);
3820
+ if (done) this.stop();
3821
+ else this.tick();
3822
+ });
3823
+ }
3824
+ };
3825
+
3826
+ // src/widget/tool-shortcuts.ts
3827
+ var TOOL_BINDINGS = [
3828
+ ["trendline", "drawings.trendline"],
3829
+ ["hline", "drawings.hline-cursor"],
3830
+ ["vline", "drawings.vline-cursor"]
3831
+ ];
3832
+ function toolShortcutHints(keymap) {
3833
+ const byId = new Map(keymap.bindings().map((b) => [b.id, b.display[0]]));
3834
+ const out = {};
3835
+ for (const [type, id] of TOOL_BINDINGS) {
3836
+ const display = byId.get(id);
3837
+ if (display) out[type] = display;
3838
+ }
3839
+ return out;
3840
+ }
3841
+
3842
+ // src/widget/history.ts
3843
+ var WidgetHistory = class {
3844
+ /** `getChart` late-resolves the CURRENT chart: drawing steps recorded before a chart
3845
+ * rebuild must undo on the chart that exists when the user presses Ctrl+Z, not on a
3846
+ * destroyed instance captured at record time. */
3847
+ constructor(getChart = () => null) {
3848
+ this.getChart = getChart;
3849
+ this.undoStack = [];
3850
+ this.redoStack = [];
3851
+ this.listeners = /* @__PURE__ */ new Set();
3852
+ this.unsubs = [];
3853
+ this.muted = false;
3854
+ }
3855
+ /** Record a reversible action (a fresh edit forks history: redo branch clears). */
3856
+ push(action) {
3857
+ if (this.muted) return;
3858
+ this.undoStack.push(action);
3859
+ this.redoStack.length = 0;
3860
+ this.notify();
3861
+ }
3862
+ undo() {
3863
+ const a = this.undoStack.pop();
3864
+ if (!a) return;
3865
+ this.mutedRun(() => a.undo());
3866
+ this.redoStack.push(a);
3867
+ this.notify();
3868
+ }
3869
+ redo() {
3870
+ const a = this.redoStack.pop();
3871
+ if (!a) return;
3872
+ this.mutedRun(() => a.redo());
3873
+ this.undoStack.push(a);
3874
+ this.notify();
3875
+ }
3876
+ /** Run `fn` without recording — for programmatic state application (setState,
3877
+ * ledger restore), whose indicator/drawing events are not user edits. */
3878
+ silently(fn) {
3879
+ this.mutedRun(fn);
3880
+ }
3881
+ get canUndo() {
3882
+ return this.undoStack.length > 0;
3883
+ }
3884
+ get canRedo() {
3885
+ return this.redoStack.length > 0;
3886
+ }
3887
+ onChange(cb) {
3888
+ this.listeners.add(cb);
3889
+ return () => this.listeners.delete(cb);
3890
+ }
3891
+ /** (Re)bind to a chart: drawing edits enter the unified stack as delegate steps. */
3892
+ onChart(chart) {
3893
+ for (const u of this.unsubs) u();
3894
+ this.unsubs = [
3895
+ chart.on("drawing:created", () => this.pushDrawingStep()),
3896
+ chart.on("drawing:edited", () => this.pushDrawingStep()),
3897
+ chart.on("drawing:removed", () => this.pushDrawingStep())
3898
+ ];
3899
+ }
3900
+ destroy() {
3901
+ for (const u of this.unsubs) u();
3902
+ this.unsubs = [];
3903
+ this.listeners.clear();
3904
+ }
3905
+ pushDrawingStep() {
3906
+ this.push({ undo: () => this.getChart()?.drawings.undo(), redo: () => this.getChart()?.drawings.redo() });
3907
+ }
3908
+ /** Replaying an action must not re-record the drawing events it triggers. */
3909
+ mutedRun(fn) {
3910
+ this.muted = true;
3911
+ try {
3912
+ fn();
3913
+ } finally {
3914
+ this.muted = false;
3915
+ }
3916
+ }
3917
+ notify() {
3918
+ for (const cb of this.listeners) cb();
3919
+ }
3920
+ };
3921
+
3922
+ // src/widget/toast.ts
3923
+ var STYLE_ID11 = "vela-widget-toast";
3924
+ var CSS11 = `
3925
+ .vela-toast {
3926
+ position: absolute;
3927
+ left: 50%;
3928
+ bottom: 34px;
3929
+ transform: translateX(-50%);
3930
+ z-index: 30;
3931
+ display: none;
3932
+ align-items: center;
3933
+ gap: 8px;
3934
+ max-width: 70%;
3935
+ padding: 6px 14px;
3936
+ border-radius: 999px;
3937
+ background: var(--vela-surface-elev);
3938
+ border: 1px solid var(--vela-border);
3939
+ color: var(--vela-fg);
3940
+ font-size: var(--vela-font-size-md);
3941
+ box-shadow: var(--vela-shadow);
3942
+ pointer-events: none;
3943
+ white-space: nowrap;
3944
+ overflow: hidden;
3945
+ text-overflow: ellipsis;
3946
+ }
3947
+ .vela-toast[data-open] { display: inline-flex; }
3948
+ .vela-toast[data-type='error'] { color: var(--vela-down); border-color: var(--vela-down); }
3949
+ .vela-toast[data-type='success'] { color: var(--vela-up); }
3950
+ `;
3951
+ var Toast = class {
3952
+ constructor(host) {
3953
+ this.timer = null;
3954
+ injectStyles(STYLE_ID11, CSS11, host.ownerDocument);
3955
+ this.el = host.ownerDocument.createElement("div");
3956
+ this.el.className = "vela-toast";
3957
+ host.appendChild(this.el);
3958
+ }
3959
+ /** Show a message; auto-hides after `ms` (0 = sticky until `hide()`). */
3960
+ show(message, type = "info", ms = 3e3) {
3961
+ this.el.textContent = message;
3962
+ this.el.dataset.type = type;
3963
+ this.el.dataset.open = "1";
3964
+ if (this.timer) clearTimeout(this.timer);
3965
+ this.timer = ms > 0 ? setTimeout(() => this.hide(), ms) : null;
3966
+ }
3967
+ hide() {
3968
+ delete this.el.dataset.open;
3969
+ if (this.timer) clearTimeout(this.timer);
3970
+ this.timer = null;
3971
+ }
3972
+ destroy() {
3973
+ this.hide();
3974
+ this.el.remove();
3975
+ }
3976
+ };
3977
+
3978
+ export { Bottombar, ChartContextMenu, DataWindow, Glider, IndicatorPicker, ObjectTree, PAN_FAST, PanelDock, RANGE_PRESETS, ShortcutsHelp, Statusline, SymbolPicker, TimeframeQuick, Toast, Topbar, Watermark, WidgetHistory, ZOOM_IN, ZOOM_OUT, dataWindowSections, decimalsFor, decodeState, encodeState, filterSymbols, fmtChange, fmtPrice, indicatorLedger, legacyWidgetState, loadPersisted, localStorageAdapter, parsePersisted, parseTimeframe, prefixedSymbol, priceStyleLabel, resolveIndicators, sanitizeState, savePersisted, statuslineInkOf, timeframeLabel, timeframeMs, toolShortcutHints };