@luxalgo/vela 0.5.0 → 0.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{DataProvider-Dzd-Erlk.d.ts → DataProvider-DTOEXKTI.d.ts} +1 -1
- package/dist/{DataProvider-DKNDHpNv.d.cts → DataProvider-gnzv-dgc.d.cts} +1 -1
- package/dist/{chunk-KM6LHB3Y.js → chunk-ATPU5CI6.js} +6 -1
- package/dist/{chunk-7UFX5ZIG.js → chunk-B5TIKQJ5.js} +65 -7
- package/dist/{chunk-GYD2THPV.js → chunk-LJLZR44Y.js} +9 -2
- package/dist/{chunk-RHIDOUFL.js → chunk-P556H6EA.js} +329 -10
- package/dist/{chunk-KCCZNKH7.js → chunk-RRFGWZNJ.js} +1915 -367
- package/dist/{chunk-OVQKKXLZ.js → chunk-Y7TK4ZWK.js} +1810 -153
- package/dist/{contributions-o1GRKPI_.d.cts → contributions-Ci_i3IN2.d.cts} +77 -31
- package/dist/{contributions-hX3EUyjG.d.ts → contributions-b6AVrqqG.d.ts} +77 -31
- package/dist/{history-LJkz4-sS.d.cts → history-Cf1lVsap.d.cts} +22 -5
- package/dist/{history-Dzxz-MQj.d.ts → history-Ci3M0xLV.d.ts} +22 -5
- package/dist/index.cjs +2010 -397
- package/dist/index.d.cts +83 -25
- package/dist/index.d.ts +83 -25
- package/dist/index.js +4 -4
- package/dist/{options-Q-576hIi.d.cts → options-XTBpbx0s.d.cts} +61 -1
- package/dist/{options-Q-576hIi.d.ts → options-XTBpbx0s.d.ts} +61 -1
- package/dist/{plugin-aGUD1epn.d.ts → plugin-CnNd7cuF.d.ts} +305 -8
- package/dist/{plugin-D94muTV-.d.cts → plugin-DfH4Y-Om.d.cts} +305 -8
- package/dist/plugin.cjs +58 -7
- package/dist/plugin.d.cts +4 -4
- package/dist/plugin.d.ts +4 -4
- package/dist/plugin.js +2 -2
- package/dist/providers/binance.d.cts +2 -2
- package/dist/providers/binance.d.ts +2 -2
- package/dist/providers/coinbase.d.cts +2 -2
- package/dist/providers/coinbase.d.ts +2 -2
- package/dist/providers/hyperliquid.d.cts +2 -2
- package/dist/providers/hyperliquid.d.ts +2 -2
- package/dist/ui.cjs +342 -9
- package/dist/ui.d.cts +64 -2
- package/dist/ui.d.ts +64 -2
- package/dist/ui.js +3 -3
- package/dist/vela.global.js +2010 -397
- package/dist/vela.global.min.js +115 -34
- package/dist/widget.cjs +5987 -2242
- package/dist/widget.d.cts +89 -17
- package/dist/widget.d.ts +89 -17
- package/dist/widget.js +169 -18
- package/dist/workspace.cjs +5627 -1587
- package/dist/workspace.d.cts +106 -12
- package/dist/workspace.d.ts +106 -12
- package/dist/workspace.js +494 -52
- package/package.json +1 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { parseSymbol, priceStyleIds, tzButtonLabel, TIMEZONES, normalizeTimezone, tzMenuLabel } from './chunk-
|
|
2
|
-
import { chartType, widgetActions, SidePanel, sidePanels, DEFAULT_PANEL_ORDER, getDrawingType } from './chunk-
|
|
3
|
-
import { Tooltip, Menu, Dialog } from './chunk-
|
|
4
|
-
import { injectStyles, iconEl,
|
|
1
|
+
import { parseSymbol, priceStyleIds, BUILTIN_PRICE_STYLES, tzButtonLabel, TIMEZONES, normalizeTimezone, tzMenuLabel } from './chunk-RRFGWZNJ.js';
|
|
2
|
+
import { chartType, widgetActions, SidePanel, sidePanels, DEFAULT_PANEL_ORDER, getDrawingType } from './chunk-B5TIKQJ5.js';
|
|
3
|
+
import { Tooltip, Menu, Dialog, Drawer } from './chunk-P556H6EA.js';
|
|
4
|
+
import { iconMarkup, registerIcon, injectStyles, iconEl, SESSION_PRE, SESSION_POST, SESSION_OFF, icon, categoricalColor } from './chunk-LJLZR44Y.js';
|
|
5
5
|
|
|
6
6
|
// src/widget/timeframe.ts
|
|
7
7
|
var UNIT_MS = {
|
|
@@ -87,9 +87,337 @@ function timeframeLabel(value) {
|
|
|
87
87
|
return parsed.short;
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
-
// src/widget/
|
|
91
|
-
var STYLE_ID = "vela-
|
|
90
|
+
// src/widget/layout-picker.ts
|
|
91
|
+
var STYLE_ID = "vela-widget-layout-picker-v14";
|
|
92
92
|
var CSS = `
|
|
93
|
+
.vela-lp-layer { position: absolute; z-index: var(--vela-z-menu); }
|
|
94
|
+
.vela-lp {
|
|
95
|
+
background: var(--vela-surface-elev);
|
|
96
|
+
color: var(--vela-fg);
|
|
97
|
+
border: 1px solid var(--vela-border-strong);
|
|
98
|
+
border-radius: 8px;
|
|
99
|
+
box-shadow: var(--vela-shadow);
|
|
100
|
+
padding: 10px 12px 10px;
|
|
101
|
+
font-size: 13px;
|
|
102
|
+
user-select: none;
|
|
103
|
+
transform-origin: top;
|
|
104
|
+
animation: vela-lp-in var(--vela-dur-fast) var(--vela-ease);
|
|
105
|
+
}
|
|
106
|
+
@keyframes vela-lp-in {
|
|
107
|
+
from { opacity: 0; transform: translateY(-4px) scale(0.98); }
|
|
108
|
+
to { opacity: 1; transform: translateY(0) scale(1); }
|
|
109
|
+
}
|
|
110
|
+
.vela-lp-cols { display: flex; align-items: stretch; gap: 14px; }
|
|
111
|
+
.vela-lp-heading {
|
|
112
|
+
display: flex;
|
|
113
|
+
align-items: center;
|
|
114
|
+
gap: 8px;
|
|
115
|
+
font-size: 10px;
|
|
116
|
+
font-weight: 600;
|
|
117
|
+
letter-spacing: 1.2px;
|
|
118
|
+
text-transform: uppercase;
|
|
119
|
+
color: var(--vela-fg-faint);
|
|
120
|
+
margin-bottom: 10px;
|
|
121
|
+
}
|
|
122
|
+
/* "?" help badge: hover it for the canvas explainer. */
|
|
123
|
+
.vela-lp-badge {
|
|
124
|
+
display: inline-flex;
|
|
125
|
+
align-items: center;
|
|
126
|
+
justify-content: center;
|
|
127
|
+
width: 14px;
|
|
128
|
+
height: 14px;
|
|
129
|
+
border-radius: 50%;
|
|
130
|
+
border: 1px solid var(--vela-border-strong);
|
|
131
|
+
color: var(--vela-fg-muted);
|
|
132
|
+
font-size: 9px;
|
|
133
|
+
font-weight: 600;
|
|
134
|
+
letter-spacing: 0;
|
|
135
|
+
cursor: default;
|
|
136
|
+
transition: color var(--vela-dur-fast) var(--vela-ease), border-color var(--vela-dur-fast) var(--vela-ease);
|
|
137
|
+
}
|
|
138
|
+
.vela-lp-badge:hover { color: var(--vela-fg-bright); border-color: var(--vela-fg-muted); }
|
|
139
|
+
.vela-lp-tip { display: flex; flex-direction: column; gap: 4px; max-width: 230px; white-space: normal; }
|
|
140
|
+
.vela-lp-vsep { width: 1px; flex: none; align-self: stretch; background: var(--vela-border-faint); }
|
|
141
|
+
.vela-lp-layout { width: 132px; display: flex; flex-direction: column; align-items: center; }
|
|
142
|
+
.vela-lp-layout > .vela-lp-heading,
|
|
143
|
+
.vela-lp-layout > .vela-lp-presets { align-self: stretch; }
|
|
144
|
+
.vela-lp-grid { display: grid; grid-template-columns: repeat(4, 24px); grid-auto-rows: 24px; gap: 4px; }
|
|
145
|
+
.vela-lp-sq {
|
|
146
|
+
all: unset;
|
|
147
|
+
box-sizing: border-box;
|
|
148
|
+
border-radius: 4px;
|
|
149
|
+
background: var(--vela-hover);
|
|
150
|
+
border: 1px solid var(--vela-border-faint);
|
|
151
|
+
cursor: pointer;
|
|
152
|
+
transition: background var(--vela-dur-fast) var(--vela-ease), border-color var(--vela-dur-fast) var(--vela-ease);
|
|
153
|
+
}
|
|
154
|
+
.vela-lp-sq:hover { background: var(--vela-hover-strong); border-color: var(--vela-border-strong); }
|
|
155
|
+
/* Selected/previewed squares \u2014 the shared monochrome selection chip. */
|
|
156
|
+
.vela-lp-sq[data-on='1'] {
|
|
157
|
+
background: var(--vela-selected-bg);
|
|
158
|
+
border-color: var(--vela-selected-bg);
|
|
159
|
+
}
|
|
160
|
+
.vela-lp-presets { display: flex; flex-direction: column; gap: 2px; margin-top: 8px; }
|
|
161
|
+
.vela-lp-preset { all: unset; padding: 5px 8px; border-radius: 4px; cursor: pointer; color: var(--vela-fg-muted); font-size: 12px; white-space: nowrap; transition: transform 120ms var(--vela-ease); }
|
|
162
|
+
.vela-lp-preset:hover { background: var(--vela-hover); }
|
|
163
|
+
.vela-lp-preset:active { transform: scale(0.98); }
|
|
164
|
+
.vela-lp-preset[data-checked='1'] { background: var(--vela-hover-strong); color: var(--vela-fg-bright); }
|
|
165
|
+
.vela-lp-sync { display: flex; flex-direction: column; gap: 4px; min-width: 118px; }
|
|
166
|
+
.vela-lp-sync-row { all: unset; display: flex; align-items: center; gap: 14px; padding: 6px 8px; border-radius: 5px; cursor: pointer; }
|
|
167
|
+
.vela-lp-sync-row:hover { background: var(--vela-hover); }
|
|
168
|
+
.vela-lp-sync-row .vela-lp-label { flex: 1 1 auto; }
|
|
169
|
+
/* Toggle pill \u2014 same control language as the menu's switch rows, monochrome ON state. */
|
|
170
|
+
.vela-lp-switch {
|
|
171
|
+
position: relative;
|
|
172
|
+
flex: none;
|
|
173
|
+
width: 34px;
|
|
174
|
+
height: 18px;
|
|
175
|
+
border-radius: 9px;
|
|
176
|
+
background: var(--vela-hover);
|
|
177
|
+
border: 1px solid var(--vela-border-soft);
|
|
178
|
+
transition: background 0.16s ease, border-color 0.16s ease;
|
|
179
|
+
}
|
|
180
|
+
.vela-lp-switch::after {
|
|
181
|
+
content: '';
|
|
182
|
+
position: absolute;
|
|
183
|
+
top: 2px;
|
|
184
|
+
left: 2px;
|
|
185
|
+
width: 12px;
|
|
186
|
+
height: 12px;
|
|
187
|
+
border-radius: 50%;
|
|
188
|
+
background: var(--vela-fg-muted);
|
|
189
|
+
transition: transform 0.16s ease, background 0.16s ease;
|
|
190
|
+
}
|
|
191
|
+
.vela-lp-switch.on { background: var(--vela-selected-bg); border-color: var(--vela-selected-bg); }
|
|
192
|
+
.vela-lp-switch.on::after { transform: translateX(16px); background: var(--vela-selected-fg); }
|
|
193
|
+
`;
|
|
194
|
+
var GRID = 4;
|
|
195
|
+
function layoutGridCanvas(doc) {
|
|
196
|
+
injectStyles(STYLE_ID, CSS, doc);
|
|
197
|
+
const el = doc.createElement("div");
|
|
198
|
+
el.className = "vela-lp-grid";
|
|
199
|
+
const squares = [];
|
|
200
|
+
for (let r = 0; r < GRID; r += 1) {
|
|
201
|
+
for (let c = 0; c < GRID; c += 1) {
|
|
202
|
+
const sq = doc.createElement("button");
|
|
203
|
+
sq.className = "vela-lp-sq";
|
|
204
|
+
sq.dataset.r = String(r);
|
|
205
|
+
sq.dataset.c = String(c);
|
|
206
|
+
sq.setAttribute("aria-label", `${c + 1} \xD7 ${r + 1}`);
|
|
207
|
+
squares.push(sq);
|
|
208
|
+
el.appendChild(sq);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
return { el, squares };
|
|
212
|
+
}
|
|
213
|
+
function paintLayoutGrid(squares, shape) {
|
|
214
|
+
for (const sq of squares) {
|
|
215
|
+
const on = shape !== null && Number(sq.dataset.r) < shape.rows && Number(sq.dataset.c) < shape.cols;
|
|
216
|
+
if (on) sq.dataset.on = "1";
|
|
217
|
+
else delete sq.dataset.on;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
var LayoutPicker = class {
|
|
221
|
+
constructor(opts) {
|
|
222
|
+
this.squares = [];
|
|
223
|
+
this.isOpen = false;
|
|
224
|
+
/** Hover preview (1-based rows/cols), null = show current shape. */
|
|
225
|
+
this.hover = null;
|
|
226
|
+
this.onDocPointerDown = (e) => {
|
|
227
|
+
const t = e.target;
|
|
228
|
+
if (t && (this.layer.contains(t) || this.opts.trigger.contains(t))) return;
|
|
229
|
+
this.close();
|
|
230
|
+
};
|
|
231
|
+
this.onDocKeydown = (e) => {
|
|
232
|
+
if (e.key === "Escape") this.close();
|
|
233
|
+
};
|
|
234
|
+
this.opts = opts;
|
|
235
|
+
this.doc = opts.host.ownerDocument;
|
|
236
|
+
injectStyles(STYLE_ID, CSS, this.doc);
|
|
237
|
+
const doc = this.doc;
|
|
238
|
+
this.layer = doc.createElement("div");
|
|
239
|
+
this.layer.className = "vela-ui-layer vela-lp-layer";
|
|
240
|
+
this.layer.style.display = "none";
|
|
241
|
+
const panel = doc.createElement("div");
|
|
242
|
+
panel.className = "vela-lp";
|
|
243
|
+
this.layer.appendChild(panel);
|
|
244
|
+
const cols = doc.createElement("div");
|
|
245
|
+
cols.className = "vela-lp-cols";
|
|
246
|
+
panel.appendChild(cols);
|
|
247
|
+
const layoutCol = doc.createElement("div");
|
|
248
|
+
layoutCol.className = "vela-lp-layout";
|
|
249
|
+
const layoutHeading = doc.createElement("div");
|
|
250
|
+
layoutHeading.className = "vela-lp-heading";
|
|
251
|
+
const headingText = doc.createElement("span");
|
|
252
|
+
headingText.textContent = "Layout";
|
|
253
|
+
const badge = doc.createElement("span");
|
|
254
|
+
badge.className = "vela-lp-badge";
|
|
255
|
+
badge.textContent = "?";
|
|
256
|
+
layoutHeading.append(headingText, badge);
|
|
257
|
+
layoutCol.appendChild(layoutHeading);
|
|
258
|
+
this.infoTip = new Tooltip(badge, {
|
|
259
|
+
host: opts.host,
|
|
260
|
+
placement: "bottom",
|
|
261
|
+
content: () => this.tipNode()
|
|
262
|
+
});
|
|
263
|
+
const { el: grid, squares } = layoutGridCanvas(doc);
|
|
264
|
+
this.squares.push(...squares);
|
|
265
|
+
layoutCol.appendChild(grid);
|
|
266
|
+
this.presetsEl = doc.createElement("div");
|
|
267
|
+
this.presetsEl.className = "vela-lp-presets";
|
|
268
|
+
layoutCol.appendChild(this.presetsEl);
|
|
269
|
+
cols.appendChild(layoutCol);
|
|
270
|
+
const vsep = doc.createElement("div");
|
|
271
|
+
vsep.className = "vela-lp-vsep";
|
|
272
|
+
cols.appendChild(vsep);
|
|
273
|
+
const syncCol = doc.createElement("div");
|
|
274
|
+
const syncHeading = doc.createElement("div");
|
|
275
|
+
syncHeading.className = "vela-lp-heading";
|
|
276
|
+
syncHeading.textContent = "Sync";
|
|
277
|
+
syncCol.appendChild(syncHeading);
|
|
278
|
+
this.syncEl = doc.createElement("div");
|
|
279
|
+
this.syncEl.className = "vela-lp-sync";
|
|
280
|
+
syncCol.appendChild(this.syncEl);
|
|
281
|
+
cols.appendChild(syncCol);
|
|
282
|
+
grid.addEventListener("pointerdown", (e) => {
|
|
283
|
+
const sq = this.squareAt(e);
|
|
284
|
+
if (!sq) return;
|
|
285
|
+
e.preventDefault();
|
|
286
|
+
const { r, c } = this.squarePos(sq);
|
|
287
|
+
this.close();
|
|
288
|
+
this.opts.onSelectGrid(r + 1, c + 1);
|
|
289
|
+
});
|
|
290
|
+
grid.addEventListener("pointermove", (e) => {
|
|
291
|
+
const sq = this.squareAt(e);
|
|
292
|
+
if (!sq) return;
|
|
293
|
+
const { r, c } = this.squarePos(sq);
|
|
294
|
+
if (this.hover?.rows !== r + 1 || this.hover?.cols !== c + 1) {
|
|
295
|
+
this.hover = { rows: r + 1, cols: c + 1 };
|
|
296
|
+
this.render();
|
|
297
|
+
}
|
|
298
|
+
});
|
|
299
|
+
grid.addEventListener("pointerleave", () => {
|
|
300
|
+
if (this.hover) {
|
|
301
|
+
this.hover = null;
|
|
302
|
+
this.render();
|
|
303
|
+
}
|
|
304
|
+
});
|
|
305
|
+
opts.trigger.addEventListener("click", () => this.toggle());
|
|
306
|
+
opts.trigger.setAttribute("aria-haspopup", "true");
|
|
307
|
+
opts.trigger.setAttribute("aria-expanded", "false");
|
|
308
|
+
opts.host.appendChild(this.layer);
|
|
309
|
+
}
|
|
310
|
+
toggle() {
|
|
311
|
+
if (this.isOpen) this.close();
|
|
312
|
+
else this.open();
|
|
313
|
+
}
|
|
314
|
+
open() {
|
|
315
|
+
if (this.isOpen) return;
|
|
316
|
+
this.isOpen = true;
|
|
317
|
+
this.hover = null;
|
|
318
|
+
this.refresh();
|
|
319
|
+
this.layer.style.display = "";
|
|
320
|
+
this.position();
|
|
321
|
+
this.opts.trigger.setAttribute("aria-expanded", "true");
|
|
322
|
+
this.doc.addEventListener("pointerdown", this.onDocPointerDown, true);
|
|
323
|
+
this.doc.addEventListener("keydown", this.onDocKeydown, true);
|
|
324
|
+
this.opts.onOpenChange?.(true);
|
|
325
|
+
}
|
|
326
|
+
close() {
|
|
327
|
+
if (!this.isOpen) return;
|
|
328
|
+
this.isOpen = false;
|
|
329
|
+
this.layer.style.display = "none";
|
|
330
|
+
this.opts.trigger.setAttribute("aria-expanded", "false");
|
|
331
|
+
this.doc.removeEventListener("pointerdown", this.onDocPointerDown, true);
|
|
332
|
+
this.doc.removeEventListener("keydown", this.onDocKeydown, true);
|
|
333
|
+
this.opts.onOpenChange?.(false);
|
|
334
|
+
}
|
|
335
|
+
/** Re-read shape/presets/syncs and re-render (no-op while closed — `open` re-reads). */
|
|
336
|
+
refresh() {
|
|
337
|
+
if (!this.isOpen) return;
|
|
338
|
+
this.renderPresets();
|
|
339
|
+
this.renderSyncs();
|
|
340
|
+
this.render();
|
|
341
|
+
}
|
|
342
|
+
destroy() {
|
|
343
|
+
this.close();
|
|
344
|
+
this.infoTip.destroy();
|
|
345
|
+
this.layer.remove();
|
|
346
|
+
}
|
|
347
|
+
// ── internals ──
|
|
348
|
+
squareAt(e) {
|
|
349
|
+
const sq = e.target?.closest?.(".vela-lp-sq");
|
|
350
|
+
return sq instanceof HTMLButtonElement ? sq : null;
|
|
351
|
+
}
|
|
352
|
+
squarePos(sq) {
|
|
353
|
+
return { r: Number(sq.dataset.r), c: Number(sq.dataset.c) };
|
|
354
|
+
}
|
|
355
|
+
/** Help copy for the LAYOUT "?" badge. */
|
|
356
|
+
tipNode() {
|
|
357
|
+
const tip = this.doc.createElement("div");
|
|
358
|
+
tip.className = "vela-lp-tip";
|
|
359
|
+
tip.textContent = "Click a square to apply that columns \xD7 rows layout.";
|
|
360
|
+
return tip;
|
|
361
|
+
}
|
|
362
|
+
/** Project the interaction state onto the DOM (the hover/current rectangle). */
|
|
363
|
+
render() {
|
|
364
|
+
paintLayoutGrid(this.squares, this.hover ?? this.opts.shape());
|
|
365
|
+
}
|
|
366
|
+
renderPresets() {
|
|
367
|
+
const doc = this.doc;
|
|
368
|
+
this.presetsEl.replaceChildren();
|
|
369
|
+
const presets = this.opts.presets();
|
|
370
|
+
this.presetsEl.style.display = presets.length > 0 ? "" : "none";
|
|
371
|
+
for (const p of presets) {
|
|
372
|
+
const b = doc.createElement("button");
|
|
373
|
+
b.className = "vela-lp-preset";
|
|
374
|
+
b.textContent = p.label;
|
|
375
|
+
if (p.checked) b.dataset.checked = "1";
|
|
376
|
+
b.addEventListener("click", () => {
|
|
377
|
+
this.close();
|
|
378
|
+
this.opts.onSelectPreset(p.id);
|
|
379
|
+
});
|
|
380
|
+
this.presetsEl.appendChild(b);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
renderSyncs() {
|
|
384
|
+
const doc = this.doc;
|
|
385
|
+
this.syncEl.replaceChildren();
|
|
386
|
+
for (const s of this.opts.syncs()) {
|
|
387
|
+
const row = doc.createElement("button");
|
|
388
|
+
row.className = "vela-lp-sync-row";
|
|
389
|
+
const label = doc.createElement("span");
|
|
390
|
+
label.className = "vela-lp-label";
|
|
391
|
+
label.textContent = s.label;
|
|
392
|
+
const pill = doc.createElement("span");
|
|
393
|
+
pill.className = "vela-lp-switch" + (s.checked ? " on" : "");
|
|
394
|
+
pill.setAttribute("aria-hidden", "true");
|
|
395
|
+
row.setAttribute("role", "switch");
|
|
396
|
+
row.setAttribute("aria-checked", String(s.checked));
|
|
397
|
+
row.append(label, pill);
|
|
398
|
+
row.addEventListener("click", () => {
|
|
399
|
+
this.opts.onToggleSync(s.id);
|
|
400
|
+
this.renderSyncs();
|
|
401
|
+
});
|
|
402
|
+
this.syncEl.appendChild(row);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
/** Anchor under the trigger, clamped to the host's right edge. */
|
|
406
|
+
position() {
|
|
407
|
+
const hostRect = this.opts.host.getBoundingClientRect();
|
|
408
|
+
const trigRect = this.opts.trigger.getBoundingClientRect();
|
|
409
|
+
let left = trigRect.left - hostRect.left;
|
|
410
|
+
const top = trigRect.bottom - hostRect.top + 4;
|
|
411
|
+
const width = this.layer.offsetWidth;
|
|
412
|
+
if (left + width > hostRect.width - 8) left = Math.max(8, hostRect.width - 8 - width);
|
|
413
|
+
this.layer.style.left = `${left}px`;
|
|
414
|
+
this.layer.style.top = `${top}px`;
|
|
415
|
+
}
|
|
416
|
+
};
|
|
417
|
+
|
|
418
|
+
// src/widget/topbar.ts
|
|
419
|
+
var STYLE_ID2 = "vela-topbar";
|
|
420
|
+
var CSS2 = `
|
|
93
421
|
.vela-widget-topbar {
|
|
94
422
|
display: flex;
|
|
95
423
|
align-items: center;
|
|
@@ -100,7 +428,7 @@ var CSS = `
|
|
|
100
428
|
font-size: var(--vela-font-size-md);
|
|
101
429
|
flex: none;
|
|
102
430
|
}
|
|
103
|
-
.vela-widget-symbol, .vela-widget-tf, .vela-widget-style, .vela-widget-indicators {
|
|
431
|
+
.vela-widget-symbol, .vela-widget-tf, .vela-widget-style, .vela-widget-indicators, .vela-widget-action-left {
|
|
104
432
|
all: unset;
|
|
105
433
|
display: inline-flex;
|
|
106
434
|
align-items: center;
|
|
@@ -122,10 +450,10 @@ var CSS = `
|
|
|
122
450
|
padding: 0 10px;
|
|
123
451
|
gap: 7px;
|
|
124
452
|
}
|
|
125
|
-
.vela-widget-tf, .vela-widget-style, .vela-widget-indicators {
|
|
453
|
+
.vela-widget-tf, .vela-widget-style, .vela-widget-indicators, .vela-widget-action-left {
|
|
126
454
|
color: var(--vela-fg-bright);
|
|
127
455
|
}
|
|
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); }
|
|
456
|
+
.vela-widget-symbol:hover, .vela-widget-tf:hover, .vela-widget-style:hover, .vela-widget-indicators:hover, .vela-widget-action-left:hover { background: var(--vela-hover); color: var(--vela-fg-bright); }
|
|
129
457
|
.vela-widget-topbar .vela-icon { color: inherit; font-size: 16px; width: 16px; height: 16px; }
|
|
130
458
|
/* Width is set in syncHairlines() to exactly one device pixel \u2014 a CSS 1px at
|
|
131
459
|
fractional DPR (1.25, 1.5\u2026) straddles two physical pixels and siblings end
|
|
@@ -148,6 +476,8 @@ var CSS = `
|
|
|
148
476
|
justify-content: center;
|
|
149
477
|
}
|
|
150
478
|
.vela-widget-actions { margin-left: auto; display: inline-flex; gap: var(--vela-space-1); }
|
|
479
|
+
/* Left-aligned contributed actions \u2014 the primary-chrome cluster after the dropdowns. */
|
|
480
|
+
.vela-widget-actions-left { display: inline-flex; align-items: center; gap: var(--vela-space-1); }
|
|
151
481
|
/* The side-panel toggles, one per docked panel \u2014 a group so the dock can rebuild them
|
|
152
482
|
without disturbing the tools around it. */
|
|
153
483
|
.vela-widget-panels { display: inline-flex; align-items: center; gap: var(--vela-space-1); }
|
|
@@ -201,7 +531,7 @@ function priceStyleIcon(id) {
|
|
|
201
531
|
var Topbar = class {
|
|
202
532
|
constructor(host, opts) {
|
|
203
533
|
this.layoutButton = null;
|
|
204
|
-
this.
|
|
534
|
+
this.layoutPicker = null;
|
|
205
535
|
this.layoutId = null;
|
|
206
536
|
this.tooltips = [];
|
|
207
537
|
this.panelBtns = /* @__PURE__ */ new Map();
|
|
@@ -221,7 +551,7 @@ var Topbar = class {
|
|
|
221
551
|
this.timeframe = opts.timeframe;
|
|
222
552
|
this.priceStyle = opts.priceStyle;
|
|
223
553
|
const doc = host.ownerDocument;
|
|
224
|
-
injectStyles(
|
|
554
|
+
injectStyles(STYLE_ID2, CSS2, doc);
|
|
225
555
|
this.el = doc.createElement("div");
|
|
226
556
|
this.el.className = "vela-widget-topbar";
|
|
227
557
|
this.symbolEl = doc.createElement("button");
|
|
@@ -234,12 +564,17 @@ var Topbar = class {
|
|
|
234
564
|
this.styleButton = doc.createElement("button");
|
|
235
565
|
this.styleButton.className = "vela-widget-style";
|
|
236
566
|
this.renderStyleButton(doc);
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
567
|
+
let indicatorsBtn = null;
|
|
568
|
+
if (opts.onIndicatorsClick) {
|
|
569
|
+
indicatorsBtn = doc.createElement("button");
|
|
570
|
+
indicatorsBtn.className = "vela-widget-indicators";
|
|
571
|
+
indicatorsBtn.append(iconEl("indicators", doc), doc.createTextNode("Indicators"));
|
|
572
|
+
indicatorsBtn.addEventListener("click", opts.onIndicatorsClick);
|
|
573
|
+
}
|
|
241
574
|
this.actionsHost = doc.createElement("span");
|
|
242
575
|
this.actionsHost.className = "vela-widget-actions";
|
|
576
|
+
this.leftActionsHost = doc.createElement("span");
|
|
577
|
+
this.leftActionsHost.className = "vela-widget-actions-left";
|
|
243
578
|
this.panelsHost = doc.createElement("span");
|
|
244
579
|
this.panelsHost.className = "vela-widget-panels";
|
|
245
580
|
const tool = (cls, icon2, tip, onClick) => this.toolButton(cls, icon2, tip, onClick, this.tooltips);
|
|
@@ -267,7 +602,10 @@ var Topbar = class {
|
|
|
267
602
|
};
|
|
268
603
|
const leading = [this.symbolEl, sep(), this.tfButton, sep(), this.styleButton, sep()];
|
|
269
604
|
if (this.layoutButton) leading.push(this.layoutButton, sep());
|
|
270
|
-
|
|
605
|
+
if (indicatorsBtn) leading.push(indicatorsBtn, sep());
|
|
606
|
+
this.leftActionsSep = sep();
|
|
607
|
+
this.leftActionsSep.hidden = true;
|
|
608
|
+
this.el.append(...leading, this.leftActionsHost, this.leftActionsSep, this.undoBtn, this.redoBtn, this.actionsHost, this.alertsBtn, this.panelsHost, screenshotBtn);
|
|
271
609
|
host.appendChild(this.el);
|
|
272
610
|
this.onHairlineSync();
|
|
273
611
|
this.hairlineRo = new ResizeObserver(this.onHairlineSync);
|
|
@@ -278,19 +616,16 @@ var Topbar = class {
|
|
|
278
616
|
this.tooltips.push(new Tooltip(this.styleButton, { content: "Chart style", triggerId: "vela-topbar-style", host }));
|
|
279
617
|
if (this.layoutButton && opts.layout) {
|
|
280
618
|
this.tooltips.push(new Tooltip(this.layoutButton, { content: "Layout", triggerId: "vela-topbar-layout", host }));
|
|
281
|
-
|
|
619
|
+
const layout = opts.layout;
|
|
620
|
+
this.layoutPicker = new LayoutPicker({
|
|
282
621
|
trigger: this.layoutButton,
|
|
283
|
-
triggerId: "vela-topbar-layout",
|
|
284
622
|
host,
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
}
|
|
292
|
-
opts.layout.onSelect(id);
|
|
293
|
-
}
|
|
623
|
+
shape: () => layout.shape(),
|
|
624
|
+
presets: () => layout.presets().map((p) => ({ ...p, checked: p.id === this.layoutId })),
|
|
625
|
+
onSelectGrid: (rows, cols) => layout.onSelectGrid(rows, cols),
|
|
626
|
+
onSelectPreset: (id) => layout.onSelectPreset(id),
|
|
627
|
+
syncs: () => layout.syncs(),
|
|
628
|
+
onToggleSync: (id) => layout.onToggleSync(id)
|
|
294
629
|
});
|
|
295
630
|
}
|
|
296
631
|
this.tfMenu = new Menu({
|
|
@@ -329,7 +664,7 @@ var Topbar = class {
|
|
|
329
664
|
if (!this.layoutButton) return;
|
|
330
665
|
this.layoutId = id;
|
|
331
666
|
this.renderLayoutButton(this.layoutButton.ownerDocument);
|
|
332
|
-
this.
|
|
667
|
+
this.layoutPicker?.refresh();
|
|
333
668
|
}
|
|
334
669
|
renderLayoutButton(doc) {
|
|
335
670
|
if (!this.layoutButton) return;
|
|
@@ -338,11 +673,6 @@ var Topbar = class {
|
|
|
338
673
|
else this.layoutButton.appendChild(doc.createTextNode(this.layoutId ?? ""));
|
|
339
674
|
this.layoutButton.setAttribute("aria-label", `Layout \u2014 ${this.layoutId ?? ""}`);
|
|
340
675
|
}
|
|
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
676
|
renderStyleButton(doc) {
|
|
347
677
|
this.styleButton.replaceChildren();
|
|
348
678
|
const icon2 = priceStyleIcon(this.priceStyle);
|
|
@@ -354,18 +684,22 @@ var Topbar = class {
|
|
|
354
684
|
renderActions() {
|
|
355
685
|
const ctx = this.opts.getContext?.();
|
|
356
686
|
this.actionsHost.replaceChildren();
|
|
687
|
+
this.leftActionsHost.replaceChildren();
|
|
357
688
|
const doc = this.actionsHost.ownerDocument;
|
|
358
689
|
for (const action of widgetActions("topbar", ctx)) {
|
|
690
|
+
const left = action.align === "left";
|
|
359
691
|
const b = doc.createElement("button");
|
|
360
|
-
b.className = "vela-widget-action";
|
|
692
|
+
b.className = left ? "vela-widget-action-left" : "vela-widget-action";
|
|
361
693
|
if (action.icon) b.appendChild(iconEl(action.icon, doc));
|
|
362
694
|
b.appendChild(doc.createTextNode(action.label));
|
|
363
695
|
b.addEventListener("click", () => {
|
|
364
696
|
const c = this.opts.getContext?.();
|
|
365
697
|
if (c) action.run(c);
|
|
366
698
|
});
|
|
367
|
-
this.actionsHost.appendChild(b);
|
|
699
|
+
(left ? this.leftActionsHost : this.actionsHost).appendChild(b);
|
|
368
700
|
}
|
|
701
|
+
this.leftActionsSep.hidden = this.leftActionsHost.childElementCount === 0;
|
|
702
|
+
this.onHairlineSync();
|
|
369
703
|
}
|
|
370
704
|
setIndicatorCount(_n) {
|
|
371
705
|
}
|
|
@@ -407,7 +741,7 @@ var Topbar = class {
|
|
|
407
741
|
if (this.hairlineRaf) win?.cancelAnimationFrame(this.hairlineRaf);
|
|
408
742
|
this.tfMenu.destroy();
|
|
409
743
|
this.styleMenu.destroy();
|
|
410
|
-
this.
|
|
744
|
+
this.layoutPicker?.destroy();
|
|
411
745
|
for (const t of [...this.tooltips, ...this.panelTooltips]) t.destroy();
|
|
412
746
|
this.el.remove();
|
|
413
747
|
}
|
|
@@ -439,11 +773,12 @@ var Topbar = class {
|
|
|
439
773
|
return this.opts.timeframes.map((tf) => ({ id: tf, label: timeframeLabel(tf), checked: tf === this.timeframe }));
|
|
440
774
|
}
|
|
441
775
|
styleItems() {
|
|
442
|
-
return priceStyleIds().map((id) => ({
|
|
776
|
+
return priceStyleIds().map((id, i) => ({
|
|
443
777
|
id,
|
|
444
778
|
label: priceStyleLabel(id),
|
|
445
779
|
icon: priceStyleIcon(id),
|
|
446
|
-
checked: id === this.priceStyle
|
|
780
|
+
checked: id === this.priceStyle,
|
|
781
|
+
separatorBefore: i === BUILTIN_PRICE_STYLES.length
|
|
447
782
|
}));
|
|
448
783
|
}
|
|
449
784
|
};
|
|
@@ -494,6 +829,7 @@ function tickerIconEl(doc, base, name, className) {
|
|
|
494
829
|
}
|
|
495
830
|
const img = doc.createElement("img");
|
|
496
831
|
img.alt = "";
|
|
832
|
+
img.crossOrigin = "anonymous";
|
|
497
833
|
img.src = cryptoIconUrl(key);
|
|
498
834
|
img.style.cssText = "width:100%;height:100%;border-radius:50%;display:block;object-fit:cover;";
|
|
499
835
|
img.addEventListener(
|
|
@@ -509,8 +845,8 @@ function tickerIconEl(doc, base, name, className) {
|
|
|
509
845
|
}
|
|
510
846
|
|
|
511
847
|
// src/widget/statusline.ts
|
|
512
|
-
var
|
|
513
|
-
var
|
|
848
|
+
var STYLE_ID3 = "vela-widget-statusline";
|
|
849
|
+
var CSS3 = `
|
|
514
850
|
.vela-statusline {
|
|
515
851
|
position: absolute;
|
|
516
852
|
top: var(--vela-space-2);
|
|
@@ -592,6 +928,59 @@ var CSS2 = `
|
|
|
592
928
|
* Statusline constructor) \u2014 the stylesheet is document-global, so a bare container
|
|
593
929
|
* class here would shift every chart on the page, including statusline-less ones. */
|
|
594
930
|
.vela-has-statusline [data-vela-pane='price'] { transform: translateY(26px); }
|
|
931
|
+
/* Mobile: two-line chip \u2014 logo / symbol / meta / market status on one aligned row, the
|
|
932
|
+
* bar change on the next. Full O/H/L/C stays hidden (too dense on a phone-width plot).
|
|
933
|
+
* GRID, not a wrapping flexbox: an absolutely positioned wrapping flex container sizes
|
|
934
|
+
* to the one-line sum of ALL segments (max-content), so its background used to stretch
|
|
935
|
+
* far past the market badge; a grid hugs the widest actual row. The meta column may
|
|
936
|
+
* shrink (it carries the ellipsis), the others wrap their content. */
|
|
937
|
+
[data-layout='mobile'] .vela-statusline {
|
|
938
|
+
display: grid;
|
|
939
|
+
grid-template-columns: auto auto minmax(0, auto) auto;
|
|
940
|
+
justify-content: start;
|
|
941
|
+
align-items: center;
|
|
942
|
+
row-gap: 1px;
|
|
943
|
+
max-width: calc(100% - var(--vela-toolbar-gutter, 0px) - var(--vela-scale-gutter, 0px) - 24px);
|
|
944
|
+
font-size: var(--vela-font-size-sm);
|
|
945
|
+
}
|
|
946
|
+
[data-layout='mobile'] .vela-statusline .vela-sl-symbol {
|
|
947
|
+
font-size: var(--vela-font-size-md);
|
|
948
|
+
line-height: 1;
|
|
949
|
+
}
|
|
950
|
+
[data-layout='mobile'] .vela-statusline .vela-sl-meta {
|
|
951
|
+
min-width: 0;
|
|
952
|
+
overflow: hidden;
|
|
953
|
+
text-overflow: ellipsis;
|
|
954
|
+
white-space: nowrap;
|
|
955
|
+
line-height: 1;
|
|
956
|
+
}
|
|
957
|
+
[data-layout='mobile'] .vela-statusline .vela-sl-avatar,
|
|
958
|
+
[data-layout='mobile'] .vela-statusline .vela-sl-market { align-self: center; }
|
|
959
|
+
[data-layout='mobile'] .vela-statusline .vela-sl-ohlc { display: none !important; }
|
|
960
|
+
[data-layout='mobile'] .vela-statusline .vela-sl-change {
|
|
961
|
+
/* Second row, under the text column \u2014 the avatar keeps the first column. */
|
|
962
|
+
grid-column: 2 / -1;
|
|
963
|
+
font-size: var(--vela-font-size-sm);
|
|
964
|
+
line-height: 1.2;
|
|
965
|
+
}
|
|
966
|
+
[data-layout='mobile'] .vela-has-statusline [data-vela-pane='price'] { transform: translateY(40px); }
|
|
967
|
+
/* FIT mode (multi-chart cells \u2014 see setFitMode): the line never wraps; segments that
|
|
968
|
+
* don't fit are HIDDEN by fit() (change first, then meta, then the market badge), so
|
|
969
|
+
* overflow:hidden only guards the transient between a resize and the next measure.
|
|
970
|
+
* Placed after the mobile block on purpose: same specificity, later wins. */
|
|
971
|
+
.vela-statusline.vela-sl-fit {
|
|
972
|
+
display: flex; /* undo the mobile grid \u2014 fit mode is one flex row again */
|
|
973
|
+
flex-wrap: nowrap;
|
|
974
|
+
align-items: center;
|
|
975
|
+
max-width: calc(100% - var(--vela-toolbar-gutter, 0px) - var(--vela-scale-gutter, 0px) - 24px);
|
|
976
|
+
overflow: hidden;
|
|
977
|
+
}
|
|
978
|
+
.vela-statusline.vela-sl-fit .vela-sl-change {
|
|
979
|
+
flex-basis: auto;
|
|
980
|
+
padding-left: 0;
|
|
981
|
+
}
|
|
982
|
+
/* One row again \u2014 the mobile two-line shift doesn't apply in fit mode. */
|
|
983
|
+
[data-layout='mobile'] .vela-sl-fit-host.vela-has-statusline [data-vela-pane='price'] { transform: translateY(26px); }
|
|
595
984
|
`;
|
|
596
985
|
function baseOfTicker(ticker) {
|
|
597
986
|
return ticker.replace(/[-_/]?(USDT|USDC|USD1|USDS|BUSD|USD|EUR|PERP)$/i, "") || ticker;
|
|
@@ -644,11 +1033,15 @@ var Statusline = class {
|
|
|
644
1033
|
this.isUp = null;
|
|
645
1034
|
/** 'ohlc' for bar-shaped styles; 'value' (the single plotted close) for line styles. */
|
|
646
1035
|
this.readout = "ohlc";
|
|
1036
|
+
/** Fit mode (multi-chart cells): one row, overflowing segments hidden — see {@link setFitMode}. */
|
|
1037
|
+
this.fitMode = false;
|
|
1038
|
+
this.fitRO = null;
|
|
647
1039
|
const doc = host.ownerDocument;
|
|
648
|
-
injectStyles(
|
|
1040
|
+
injectStyles(STYLE_ID3, CSS3, doc);
|
|
649
1041
|
host.classList.add("vela-has-statusline");
|
|
650
1042
|
this.el = doc.createElement("div");
|
|
651
1043
|
this.el.className = "vela-statusline";
|
|
1044
|
+
this.el.dataset.velaScreenshot = "1";
|
|
652
1045
|
const ticker = parseSymbol(symbol).ticker;
|
|
653
1046
|
this.avatarEl = tickerIconEl(doc, baseOfTicker(ticker), ticker, "vela-sl-avatar");
|
|
654
1047
|
this.symbolEl = doc.createElement("span");
|
|
@@ -674,6 +1067,52 @@ var Statusline = class {
|
|
|
674
1067
|
const fresh = tickerIconEl(this.el.ownerDocument, baseOfTicker(ticker), ticker, "vela-sl-avatar");
|
|
675
1068
|
this.avatarEl.replaceWith(fresh);
|
|
676
1069
|
this.avatarEl = fresh;
|
|
1070
|
+
this.fit();
|
|
1071
|
+
}
|
|
1072
|
+
/**
|
|
1073
|
+
* Multi-chart cells: keep the line on ONE row whatever the cell width — never wrap.
|
|
1074
|
+
* Segments that don't fit are hidden outright rather than clipped mid-glyph, least
|
|
1075
|
+
* important first: OHLC, then the bar change, the venue/timeframe meta, and the
|
|
1076
|
+
* market badge; the logo + ticker always stay. Re-fits live on host resizes.
|
|
1077
|
+
*/
|
|
1078
|
+
setFitMode(on) {
|
|
1079
|
+
if (on === this.fitMode) return;
|
|
1080
|
+
this.fitMode = on;
|
|
1081
|
+
this.el.classList.toggle("vela-sl-fit", on);
|
|
1082
|
+
this.host.classList.toggle("vela-sl-fit-host", on);
|
|
1083
|
+
if (on) {
|
|
1084
|
+
if (typeof ResizeObserver !== "undefined" && !this.fitRO) {
|
|
1085
|
+
this.fitRO = new ResizeObserver(() => this.fit());
|
|
1086
|
+
this.fitRO.observe(this.host);
|
|
1087
|
+
}
|
|
1088
|
+
this.fit();
|
|
1089
|
+
} else {
|
|
1090
|
+
this.fitRO?.disconnect();
|
|
1091
|
+
this.fitRO = null;
|
|
1092
|
+
this.syncParts();
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
/** Project the parts config onto the segments (the baseline fit() prunes from). */
|
|
1096
|
+
syncParts() {
|
|
1097
|
+
this.symbolEl.style.display = this.parts.name ? "" : "none";
|
|
1098
|
+
this.marketEl.style.display = this.parts.market ? "" : "none";
|
|
1099
|
+
this.ohlcEl.style.display = this.parts.ohlc ? "" : "none";
|
|
1100
|
+
this.changeEl.style.display = this.parts.change ? "" : "none";
|
|
1101
|
+
}
|
|
1102
|
+
/** Hide overflowing segments until the row fits its max-width (fit mode only). */
|
|
1103
|
+
fit() {
|
|
1104
|
+
if (!this.fitMode) return;
|
|
1105
|
+
this.syncParts();
|
|
1106
|
+
const order = [
|
|
1107
|
+
[this.ohlcEl, this.parts.ohlc],
|
|
1108
|
+
[this.changeEl, this.parts.change],
|
|
1109
|
+
[this.metaEl, true],
|
|
1110
|
+
[this.marketEl, this.parts.market]
|
|
1111
|
+
];
|
|
1112
|
+
for (const [el, shown] of order) {
|
|
1113
|
+
if (this.el.scrollWidth <= this.el.clientWidth) break;
|
|
1114
|
+
if (shown) el.style.display = "none";
|
|
1115
|
+
}
|
|
677
1116
|
}
|
|
678
1117
|
/** Shape + color the value readout after the active price style: its own up/down
|
|
679
1118
|
* colors (candle bodies, bar ticks, the line color, …), the direction rule that
|
|
@@ -693,6 +1132,7 @@ var Statusline = class {
|
|
|
693
1132
|
/** The "· BINANCE · 1h" segment after the symbol — venue first, then resolution. */
|
|
694
1133
|
setMeta(timeframe, provider) {
|
|
695
1134
|
this.metaEl.textContent = `${provider ? `\xB7 ${provider.toUpperCase()} ` : ""}\xB7 ${timeframeLabel(timeframe)}`;
|
|
1135
|
+
this.fit();
|
|
696
1136
|
}
|
|
697
1137
|
/** Dress the market badge for a session state: its icon, tinted circle, and the
|
|
698
1138
|
* hover label. Callers with no session model leave the constructor's 'open'. */
|
|
@@ -704,10 +1144,8 @@ var Statusline = class {
|
|
|
704
1144
|
}
|
|
705
1145
|
setPartVisible(part, visible) {
|
|
706
1146
|
this.parts[part] = visible;
|
|
707
|
-
this.
|
|
708
|
-
this.
|
|
709
|
-
this.ohlcEl.style.display = this.parts.ohlc ? "" : "none";
|
|
710
|
-
this.changeEl.style.display = this.parts.change ? "" : "none";
|
|
1147
|
+
this.syncParts();
|
|
1148
|
+
this.fit();
|
|
711
1149
|
}
|
|
712
1150
|
partVisible(part) {
|
|
713
1151
|
return this.parts[part];
|
|
@@ -731,8 +1169,10 @@ var Statusline = class {
|
|
|
731
1169
|
}
|
|
732
1170
|
destroy() {
|
|
733
1171
|
this.detach();
|
|
1172
|
+
this.fitRO?.disconnect();
|
|
1173
|
+
this.fitRO = null;
|
|
734
1174
|
this.marketTip.destroy();
|
|
735
|
-
this.host.classList.remove("vela-has-statusline");
|
|
1175
|
+
this.host.classList.remove("vela-has-statusline", "vela-sl-fit-host");
|
|
736
1176
|
this.el.remove();
|
|
737
1177
|
}
|
|
738
1178
|
detach() {
|
|
@@ -764,15 +1204,26 @@ var Statusline = class {
|
|
|
764
1204
|
this.changeEl.textContent = fmtChange(bar.open, bar.close);
|
|
765
1205
|
this.changeEl.dataset.dir = up ? "up" : "down";
|
|
766
1206
|
this.changeEl.style.color = ink;
|
|
1207
|
+
this.fit();
|
|
767
1208
|
}
|
|
768
1209
|
};
|
|
769
1210
|
|
|
770
1211
|
// src/widget/watermark.ts
|
|
771
|
-
var
|
|
772
|
-
var
|
|
1212
|
+
var MAX_FONT_PX = 36;
|
|
1213
|
+
var MIN_FONT_PX = 12;
|
|
1214
|
+
var FILL = 0.9;
|
|
1215
|
+
var STYLE_ID4 = "vela-widget-watermark";
|
|
1216
|
+
var CSS4 = `
|
|
773
1217
|
.vela-watermark {
|
|
774
1218
|
position: absolute;
|
|
775
|
-
|
|
1219
|
+
/* Insets follow the renderer-published gutters (mount container), so the mark
|
|
1220
|
+
* centers and fits within the PLOT \u2014 never bleeding into the drawings toolbar
|
|
1221
|
+
* on the left or the price scale on the right (visible in small multi-chart
|
|
1222
|
+
* cells, where the scale is a large share of the width). */
|
|
1223
|
+
top: 0;
|
|
1224
|
+
bottom: 0;
|
|
1225
|
+
left: var(--vela-toolbar-gutter, 0px);
|
|
1226
|
+
right: var(--vela-scale-gutter, 0px);
|
|
776
1227
|
display: flex;
|
|
777
1228
|
align-items: center;
|
|
778
1229
|
justify-content: center;
|
|
@@ -780,28 +1231,65 @@ var CSS3 = `
|
|
|
780
1231
|
z-index: 1;
|
|
781
1232
|
color: var(--vela-fg);
|
|
782
1233
|
opacity: 0.05;
|
|
783
|
-
font-size:
|
|
1234
|
+
font-size: ${MAX_FONT_PX}px;
|
|
784
1235
|
font-weight: 700;
|
|
785
1236
|
letter-spacing: 0.04em;
|
|
786
1237
|
user-select: none;
|
|
787
1238
|
white-space: nowrap;
|
|
788
1239
|
}
|
|
789
1240
|
`;
|
|
1241
|
+
function watermarkFontPx(availPx, textPxAtMax) {
|
|
1242
|
+
if (textPxAtMax <= 0) return MAX_FONT_PX;
|
|
1243
|
+
return Math.max(MIN_FONT_PX, Math.min(MAX_FONT_PX, Math.floor(MAX_FONT_PX * availPx * FILL / textPxAtMax)));
|
|
1244
|
+
}
|
|
790
1245
|
var Watermark = class {
|
|
791
1246
|
constructor(host, symbol, timeframe) {
|
|
792
|
-
|
|
1247
|
+
this.resizeObserver = null;
|
|
1248
|
+
/** The host's visibility preference (the persisted watermark toggle). */
|
|
1249
|
+
this.shown = true;
|
|
1250
|
+
/** A bar load is in flight with nothing painted — the loading affordance owns the
|
|
1251
|
+
* canvas, so the mark stays out of its way. Starts true: the FIRST `load:start`
|
|
1252
|
+
* fires during chart construction, before any subscriber can see it. */
|
|
1253
|
+
this.loading = true;
|
|
1254
|
+
injectStyles(STYLE_ID4, CSS4, host.ownerDocument);
|
|
793
1255
|
this.el = host.ownerDocument.createElement("div");
|
|
794
1256
|
this.el.className = "vela-watermark";
|
|
1257
|
+
this.el.dataset.velaScreenshot = "under";
|
|
1258
|
+
this.text = host.ownerDocument.createElement("span");
|
|
1259
|
+
this.el.appendChild(this.text);
|
|
795
1260
|
host.appendChild(this.el);
|
|
1261
|
+
if (typeof ResizeObserver !== "undefined") {
|
|
1262
|
+
this.resizeObserver = new ResizeObserver(() => this.fit());
|
|
1263
|
+
this.resizeObserver.observe(this.el);
|
|
1264
|
+
}
|
|
796
1265
|
this.update(symbol, timeframe);
|
|
1266
|
+
this.sync();
|
|
797
1267
|
}
|
|
798
1268
|
setVisible(visible) {
|
|
799
|
-
this.
|
|
1269
|
+
this.shown = visible;
|
|
1270
|
+
this.sync();
|
|
1271
|
+
}
|
|
1272
|
+
/** Loading and the watermark never share the canvas — hidden while a load is up. */
|
|
1273
|
+
setLoading(loading) {
|
|
1274
|
+
this.loading = loading;
|
|
1275
|
+
this.sync();
|
|
1276
|
+
}
|
|
1277
|
+
sync() {
|
|
1278
|
+
this.el.style.display = this.shown && !this.loading ? "" : "none";
|
|
800
1279
|
}
|
|
801
1280
|
update(symbol, timeframe) {
|
|
802
|
-
this.
|
|
1281
|
+
this.text.textContent = symbol ? `${parseSymbol(symbol).ticker} \xB7 ${timeframeLabel(timeframe)}` : "";
|
|
1282
|
+
this.fit();
|
|
1283
|
+
}
|
|
1284
|
+
/** Measure the text at the cap and shrink it to the chart's own width. */
|
|
1285
|
+
fit() {
|
|
1286
|
+
if (this.el.clientWidth <= 0 || !this.text.textContent) return;
|
|
1287
|
+
this.el.style.fontSize = `${MAX_FONT_PX}px`;
|
|
1288
|
+
const px = watermarkFontPx(this.el.clientWidth, this.text.getBoundingClientRect().width);
|
|
1289
|
+
if (px !== MAX_FONT_PX) this.el.style.fontSize = `${px}px`;
|
|
803
1290
|
}
|
|
804
1291
|
destroy() {
|
|
1292
|
+
this.resizeObserver?.disconnect();
|
|
805
1293
|
this.el.remove();
|
|
806
1294
|
}
|
|
807
1295
|
};
|
|
@@ -827,8 +1315,8 @@ var RANGE_PRESETS = [
|
|
|
827
1315
|
{ id: "ALL", tf: "W", preset: "ALL", bars: 5e3 }
|
|
828
1316
|
// everything the provider serves
|
|
829
1317
|
];
|
|
830
|
-
var
|
|
831
|
-
var
|
|
1318
|
+
var STYLE_ID5 = "vela-widget-bottombar";
|
|
1319
|
+
var CSS5 = `
|
|
832
1320
|
.vela-widget-bottombar {
|
|
833
1321
|
display: flex;
|
|
834
1322
|
align-items: center;
|
|
@@ -903,7 +1391,7 @@ var Bottombar = class {
|
|
|
903
1391
|
this.timer = null;
|
|
904
1392
|
this.timezone = opts.timezone;
|
|
905
1393
|
const doc = host.ownerDocument;
|
|
906
|
-
injectStyles(
|
|
1394
|
+
injectStyles(STYLE_ID5, CSS5, doc);
|
|
907
1395
|
this.el = doc.createElement("div");
|
|
908
1396
|
this.el.className = "vela-widget-bottombar";
|
|
909
1397
|
for (const preset of RANGE_PRESETS) {
|
|
@@ -1041,8 +1529,8 @@ function filterSymbols(list, query, limit = 100) {
|
|
|
1041
1529
|
}
|
|
1042
1530
|
return [...prefix, ...substr, ...desc, ...venue].slice(0, limit);
|
|
1043
1531
|
}
|
|
1044
|
-
var
|
|
1045
|
-
var
|
|
1532
|
+
var STYLE_ID6 = "vela-widget-symbolpicker";
|
|
1533
|
+
var CSS6 = `
|
|
1046
1534
|
.vela-sp-searchrow {
|
|
1047
1535
|
display: flex;
|
|
1048
1536
|
align-items: center;
|
|
@@ -1064,6 +1552,13 @@ var CSS5 = `
|
|
|
1064
1552
|
outline: none;
|
|
1065
1553
|
}
|
|
1066
1554
|
.vela-sp-tabs { display: flex; gap: 14px; margin: 12px 2px 6px; border-bottom: 1px solid var(--vela-border); padding-bottom: 8px; }
|
|
1555
|
+
/* Mobile (fullscreen dialog): the asset-class strip scrolls sideways instead of
|
|
1556
|
+
overflowing the body, and the result list stops capping itself \u2014 the body owns
|
|
1557
|
+
the scrolling in the fullscreen presentation. */
|
|
1558
|
+
[data-layout='mobile'] .vela-sp-tabs { overflow-x: auto; scrollbar-width: none; gap: 6px; }
|
|
1559
|
+
[data-layout='mobile'] .vela-sp-tabs::-webkit-scrollbar { display: none; }
|
|
1560
|
+
[data-layout='mobile'] .vela-sp-tab { flex: none; padding: 7px 10px; }
|
|
1561
|
+
[data-layout='mobile'] .vela-sp-list { max-height: none; }
|
|
1067
1562
|
.vela-sp-tab {
|
|
1068
1563
|
all: unset;
|
|
1069
1564
|
padding: 3px 10px;
|
|
@@ -1132,7 +1627,7 @@ var SymbolPicker = class {
|
|
|
1132
1627
|
this.seed = "";
|
|
1133
1628
|
this.activeTab = "All";
|
|
1134
1629
|
const doc = (opts.host ?? document.body).ownerDocument;
|
|
1135
|
-
injectStyles(
|
|
1630
|
+
injectStyles(STYLE_ID6, CSS6, doc);
|
|
1136
1631
|
this.input = doc.createElement("input");
|
|
1137
1632
|
this.input.className = "vela-sp-input";
|
|
1138
1633
|
this.input.placeholder = "Search symbol\u2026";
|
|
@@ -1454,8 +1949,8 @@ function treeIsEmpty(panes) {
|
|
|
1454
1949
|
}
|
|
1455
1950
|
|
|
1456
1951
|
// src/widget/object-tree.ts
|
|
1457
|
-
var
|
|
1458
|
-
var
|
|
1952
|
+
var STYLE_ID7 = "vela-widget-objtree";
|
|
1953
|
+
var CSS7 = `
|
|
1459
1954
|
/* One pane's block. The transparent border reserves the drop-target outline. */
|
|
1460
1955
|
.vela-ot-pane {
|
|
1461
1956
|
border: 1px solid transparent;
|
|
@@ -1693,7 +2188,7 @@ var ObjectTree = class extends SidePanel {
|
|
|
1693
2188
|
this.menu = null;
|
|
1694
2189
|
this.menuActions = /* @__PURE__ */ new Map();
|
|
1695
2190
|
this.drag = null;
|
|
1696
|
-
injectStyles(
|
|
2191
|
+
injectStyles(STYLE_ID7, CSS7, host.ownerDocument);
|
|
1697
2192
|
this.body.addEventListener("contextmenu", (e) => this.onContextMenu(e));
|
|
1698
2193
|
this.body.addEventListener("pointerdown", (e) => this.onPointerDown(e));
|
|
1699
2194
|
}
|
|
@@ -2539,6 +3034,9 @@ var ObjectTree = class extends SidePanel {
|
|
|
2539
3034
|
this.refresh();
|
|
2540
3035
|
})
|
|
2541
3036
|
];
|
|
3037
|
+
if (chart.renderer.supportsIndicatorSettings) {
|
|
3038
|
+
items.push(b.entry("Indicator settings", "gear", () => chart.renderer.openIndicatorSettings(row.id)));
|
|
3039
|
+
}
|
|
2542
3040
|
if (chart.panes.supported) {
|
|
2543
3041
|
const moves = this.moveItems(b, pass, row.id, paneId);
|
|
2544
3042
|
if (moves.length > 0) items.push(b.submenu("Move to", "move-vertical", moves));
|
|
@@ -2672,8 +3170,8 @@ var ObjectTree = class extends SidePanel {
|
|
|
2672
3170
|
};
|
|
2673
3171
|
|
|
2674
3172
|
// src/widget/data-window.ts
|
|
2675
|
-
var
|
|
2676
|
-
var
|
|
3173
|
+
var STYLE_ID8 = "vela-widget-datawindow";
|
|
3174
|
+
var CSS8 = `
|
|
2677
3175
|
.vela-dw-group {
|
|
2678
3176
|
padding: 10px 8px 4px;
|
|
2679
3177
|
margin-top: 4px;
|
|
@@ -2732,7 +3230,7 @@ var DataWindow = class extends SidePanel {
|
|
|
2732
3230
|
super(host, "Data window", "vela-dw");
|
|
2733
3231
|
this.chart = null;
|
|
2734
3232
|
this.unsubs = [];
|
|
2735
|
-
injectStyles(
|
|
3233
|
+
injectStyles(STYLE_ID8, CSS8, host.ownerDocument);
|
|
2736
3234
|
}
|
|
2737
3235
|
toggle(open = this.el.hidden) {
|
|
2738
3236
|
super.toggle(open);
|
|
@@ -2857,6 +3355,11 @@ var PanelDock = class {
|
|
|
2857
3355
|
get openId() {
|
|
2858
3356
|
return this.entries.find((e) => e.panel.open)?.id ?? null;
|
|
2859
3357
|
}
|
|
3358
|
+
/** The docked panels in dock order — what a non-topbar chrome (the mobile
|
|
3359
|
+
* three-dots drawer) lists so every panel stays reachable there too. */
|
|
3360
|
+
list() {
|
|
3361
|
+
return this.entries.map((e) => ({ id: e.id, title: e.title, icon: e.icon }));
|
|
3362
|
+
}
|
|
2860
3363
|
/** The dock's persistable state, or null when there is nothing worth saving. */
|
|
2861
3364
|
getState() {
|
|
2862
3365
|
const out = {};
|
|
@@ -2929,8 +3432,8 @@ var PanelDock = class {
|
|
|
2929
3432
|
};
|
|
2930
3433
|
|
|
2931
3434
|
// src/widget/shortcuts-help.ts
|
|
2932
|
-
var
|
|
2933
|
-
var
|
|
3435
|
+
var STYLE_ID9 = "vela-widget-shortcuts";
|
|
3436
|
+
var CSS9 = `
|
|
2934
3437
|
.vela-sh-cat {
|
|
2935
3438
|
color: var(--vela-fg-muted);
|
|
2936
3439
|
font-size: var(--vela-font-size-sm);
|
|
@@ -2955,7 +3458,7 @@ var ShortcutsHelp = class {
|
|
|
2955
3458
|
constructor(keymap, host, onOpenChange) {
|
|
2956
3459
|
this.keymap = keymap;
|
|
2957
3460
|
const doc = (host ?? document.body).ownerDocument;
|
|
2958
|
-
injectStyles(
|
|
3461
|
+
injectStyles(STYLE_ID9, CSS9, doc);
|
|
2959
3462
|
this.list = doc.createElement("div");
|
|
2960
3463
|
this.dialog = new Dialog({
|
|
2961
3464
|
title: "Keyboard shortcuts",
|
|
@@ -3243,8 +3746,8 @@ var ChartContextMenu = class {
|
|
|
3243
3746
|
};
|
|
3244
3747
|
|
|
3245
3748
|
// src/widget/indicator-picker.ts
|
|
3246
|
-
var
|
|
3247
|
-
var
|
|
3749
|
+
var STYLE_ID10 = "vela-widget-indpicker";
|
|
3750
|
+
var CSS10 = `
|
|
3248
3751
|
.vela-ip-searchrow {
|
|
3249
3752
|
display: flex;
|
|
3250
3753
|
align-items: center;
|
|
@@ -3260,6 +3763,9 @@ var CSS9 = `
|
|
|
3260
3763
|
.vela-ip-searchrow .vela-icon { color: var(--vela-fg-muted); }
|
|
3261
3764
|
.vela-ip-search { flex: 1; background: transparent; color: var(--vela-fg); border: none; font-size: 14px; outline: none; }
|
|
3262
3765
|
.vela-ip-list { max-height: 50vh; overflow: auto; min-width: 380px; }
|
|
3766
|
+
/* Mobile (fullscreen dialog): no width floor \u2014 380px would overflow a phone \u2014
|
|
3767
|
+
and no height cap; the fullscreen body owns the scrolling. */
|
|
3768
|
+
[data-layout='mobile'] .vela-ip-list { min-width: 0; max-height: none; }
|
|
3263
3769
|
.vela-ip-list::-webkit-scrollbar { width: 8px; }
|
|
3264
3770
|
.vela-ip-list::-webkit-scrollbar-thumb { background: var(--vela-scroll); border-radius: 4px; border: 2px solid transparent; background-clip: padding-box; }
|
|
3265
3771
|
.vela-ip-group {
|
|
@@ -3318,7 +3824,7 @@ var IndicatorPicker = class {
|
|
|
3318
3824
|
this.isOpen = false;
|
|
3319
3825
|
this.opts = opts;
|
|
3320
3826
|
const doc = (opts.host ?? document.body).ownerDocument;
|
|
3321
|
-
injectStyles(
|
|
3827
|
+
injectStyles(STYLE_ID10, CSS10, doc);
|
|
3322
3828
|
this.search = doc.createElement("input");
|
|
3323
3829
|
this.search.className = "vela-ip-search";
|
|
3324
3830
|
this.search.placeholder = "Search\u2026";
|
|
@@ -3341,7 +3847,7 @@ var IndicatorPicker = class {
|
|
|
3341
3847
|
if (open) {
|
|
3342
3848
|
this.search.value = "";
|
|
3343
3849
|
this.refresh();
|
|
3344
|
-
setTimeout(() => this.search.focus(), 0);
|
|
3850
|
+
if (!this.search.closest('[data-layout="mobile"]')) setTimeout(() => this.search.focus(), 0);
|
|
3345
3851
|
}
|
|
3346
3852
|
opts.onOpenChange?.(open);
|
|
3347
3853
|
}
|
|
@@ -3446,8 +3952,8 @@ var IndicatorPicker = class {
|
|
|
3446
3952
|
};
|
|
3447
3953
|
|
|
3448
3954
|
// src/widget/timeframe-quick.ts
|
|
3449
|
-
var
|
|
3450
|
-
var
|
|
3955
|
+
var STYLE_ID11 = "vela-widget-tfquick";
|
|
3956
|
+
var CSS11 = `
|
|
3451
3957
|
.vela-tq-input {
|
|
3452
3958
|
display: block;
|
|
3453
3959
|
margin: 0 auto;
|
|
@@ -3470,7 +3976,7 @@ var CSS10 = `
|
|
|
3470
3976
|
var TimeframeQuick = class {
|
|
3471
3977
|
constructor(opts) {
|
|
3472
3978
|
const doc = (opts.host ?? document.body).ownerDocument;
|
|
3473
|
-
injectStyles(
|
|
3979
|
+
injectStyles(STYLE_ID11, CSS11, doc);
|
|
3474
3980
|
this.input = doc.createElement("input");
|
|
3475
3981
|
this.input.className = "vela-tq-input";
|
|
3476
3982
|
this.input.setAttribute("spellcheck", "false");
|
|
@@ -3653,6 +4159,7 @@ function sanitizeCell(raw) {
|
|
|
3653
4159
|
if (typeof c.bars === "number" && Number.isFinite(c.bars) && c.bars > 0) out.bars = c.bars;
|
|
3654
4160
|
if (typeof c.watermark === "boolean") out.watermark = c.watermark;
|
|
3655
4161
|
if (typeof c.indicatorTitles === "boolean") out.indicatorTitles = c.indicatorTitles;
|
|
4162
|
+
if (typeof c.indicatorValues === "boolean") out.indicatorValues = c.indicatorValues;
|
|
3656
4163
|
if (c.rendererConfig != null && typeof c.rendererConfig === "object") out.rendererConfig = c.rendererConfig;
|
|
3657
4164
|
if (c.drawings != null && typeof c.drawings === "object") out.drawings = c.drawings;
|
|
3658
4165
|
const ind = c.indicators;
|
|
@@ -3667,7 +4174,7 @@ function sanitizeSync(raw) {
|
|
|
3667
4174
|
if (raw == null || typeof raw !== "object") return null;
|
|
3668
4175
|
const s = raw;
|
|
3669
4176
|
const out = {};
|
|
3670
|
-
for (const kind of ["viewport", "symbol", "timeframe", "crosshair"]) {
|
|
4177
|
+
for (const kind of ["viewport", "symbol", "timeframe", "crosshair", "drawings"]) {
|
|
3671
4178
|
const v = s[kind];
|
|
3672
4179
|
if (v === true) out[kind] = true;
|
|
3673
4180
|
else if (v != null && typeof v === "object") {
|
|
@@ -3759,87 +4266,1237 @@ function indicatorLedger(i) {
|
|
|
3759
4266
|
};
|
|
3760
4267
|
}
|
|
3761
4268
|
|
|
3762
|
-
// src/widget/
|
|
3763
|
-
var
|
|
3764
|
-
var
|
|
3765
|
-
|
|
3766
|
-
|
|
3767
|
-
|
|
3768
|
-
|
|
3769
|
-
|
|
3770
|
-
return
|
|
4269
|
+
// src/widget/layout-mode.ts
|
|
4270
|
+
var MOBILE_BREAKPOINT_PX = 640;
|
|
4271
|
+
var COARSE_BREAKPOINT_PX = 920;
|
|
4272
|
+
function resolveLayoutMode(option, width, coarsePointer) {
|
|
4273
|
+
if (option !== "auto") return option;
|
|
4274
|
+
if (width <= 0) return "desktop";
|
|
4275
|
+
if (width < MOBILE_BREAKPOINT_PX) return "mobile";
|
|
4276
|
+
if (coarsePointer && width < COARSE_BREAKPOINT_PX) return "mobile";
|
|
4277
|
+
return "desktop";
|
|
3771
4278
|
}
|
|
3772
|
-
|
|
3773
|
-
|
|
3774
|
-
|
|
3775
|
-
|
|
3776
|
-
|
|
3777
|
-
|
|
3778
|
-
|
|
3779
|
-
|
|
3780
|
-
|
|
3781
|
-
|
|
3782
|
-
|
|
3783
|
-
|
|
3784
|
-
|
|
3785
|
-
|
|
3786
|
-
|
|
3787
|
-
|
|
3788
|
-
|
|
3789
|
-
|
|
3790
|
-
|
|
3791
|
-
this.raf = 0;
|
|
4279
|
+
var LayoutModeController = class {
|
|
4280
|
+
constructor(el, option = "auto") {
|
|
4281
|
+
this.el = el;
|
|
4282
|
+
this.option = option;
|
|
4283
|
+
this.listeners = /* @__PURE__ */ new Set();
|
|
4284
|
+
this.ro = null;
|
|
4285
|
+
this.mql = null;
|
|
4286
|
+
this.onMediaChange = () => this.evaluate();
|
|
4287
|
+
const win = el.ownerDocument.defaultView;
|
|
4288
|
+
if (win && typeof win.matchMedia === "function") {
|
|
4289
|
+
this.mql = win.matchMedia("(pointer: coarse)");
|
|
4290
|
+
if (typeof this.mql.addEventListener === "function") this.mql.addEventListener("change", this.onMediaChange);
|
|
4291
|
+
}
|
|
4292
|
+
this.mode = resolveLayoutMode(option, el.getBoundingClientRect().width, this.mql?.matches ?? false);
|
|
4293
|
+
el.dataset.layout = this.mode;
|
|
4294
|
+
if (option === "auto" && win && typeof win.ResizeObserver === "function") {
|
|
4295
|
+
this.ro = new win.ResizeObserver(() => this.evaluate());
|
|
4296
|
+
this.ro.observe(el);
|
|
4297
|
+
}
|
|
3792
4298
|
}
|
|
3793
|
-
|
|
3794
|
-
this.
|
|
4299
|
+
get current() {
|
|
4300
|
+
return this.mode;
|
|
3795
4301
|
}
|
|
3796
|
-
|
|
3797
|
-
|
|
3798
|
-
this.
|
|
3799
|
-
this.
|
|
3800
|
-
this.target = null;
|
|
4302
|
+
/** Subscribe to mode transitions (fires with the NEW mode). */
|
|
4303
|
+
onChange(cb) {
|
|
4304
|
+
this.listeners.add(cb);
|
|
4305
|
+
return () => this.listeners.delete(cb);
|
|
3801
4306
|
}
|
|
3802
|
-
|
|
3803
|
-
|
|
3804
|
-
|
|
3805
|
-
if (
|
|
3806
|
-
this.
|
|
3807
|
-
this.
|
|
3808
|
-
if (!this.raf) this.tick();
|
|
4307
|
+
destroy() {
|
|
4308
|
+
this.ro?.disconnect();
|
|
4309
|
+
this.ro = null;
|
|
4310
|
+
if (this.mql && typeof this.mql.removeEventListener === "function") this.mql.removeEventListener("change", this.onMediaChange);
|
|
4311
|
+
this.mql = null;
|
|
4312
|
+
this.listeners.clear();
|
|
3809
4313
|
}
|
|
3810
|
-
|
|
3811
|
-
|
|
3812
|
-
|
|
3813
|
-
|
|
3814
|
-
|
|
3815
|
-
|
|
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
|
-
});
|
|
4314
|
+
evaluate() {
|
|
4315
|
+
const next = resolveLayoutMode(this.option, this.el.getBoundingClientRect().width, this.mql?.matches ?? false);
|
|
4316
|
+
if (next === this.mode) return;
|
|
4317
|
+
this.mode = next;
|
|
4318
|
+
this.el.dataset.layout = next;
|
|
4319
|
+
for (const cb of [...this.listeners]) cb(next);
|
|
3823
4320
|
}
|
|
3824
4321
|
};
|
|
3825
4322
|
|
|
3826
|
-
// src/widget/
|
|
3827
|
-
var
|
|
3828
|
-
|
|
3829
|
-
|
|
3830
|
-
|
|
3831
|
-
|
|
3832
|
-
|
|
3833
|
-
|
|
3834
|
-
|
|
3835
|
-
|
|
3836
|
-
|
|
3837
|
-
|
|
3838
|
-
|
|
3839
|
-
|
|
3840
|
-
}
|
|
3841
|
-
|
|
3842
|
-
|
|
4323
|
+
// src/widget/mobile-bar.ts
|
|
4324
|
+
var STYLE_ID12 = "vela-widget-mobilebar";
|
|
4325
|
+
var CSS12 = `
|
|
4326
|
+
.vela-mobilebar {
|
|
4327
|
+
display: none;
|
|
4328
|
+
align-items: stretch;
|
|
4329
|
+
gap: 2px;
|
|
4330
|
+
padding: 4px 6px calc(4px + env(safe-area-inset-bottom, 0px));
|
|
4331
|
+
border-top: 1px solid var(--vela-border);
|
|
4332
|
+
color: var(--vela-fg-muted);
|
|
4333
|
+
flex: none;
|
|
4334
|
+
}
|
|
4335
|
+
[data-layout='mobile'] .vela-mobilebar { display: flex; }
|
|
4336
|
+
[data-layout='mobile'] .vela-widget-topbar { display: none; }
|
|
4337
|
+
[data-layout='mobile'] .vela-widget-bottombar { display: none; }
|
|
4338
|
+
.vela-mb-item {
|
|
4339
|
+
all: unset;
|
|
4340
|
+
flex: 1 1 0;
|
|
4341
|
+
min-width: 0;
|
|
4342
|
+
min-height: 44px;
|
|
4343
|
+
display: inline-flex;
|
|
4344
|
+
align-items: center;
|
|
4345
|
+
justify-content: center;
|
|
4346
|
+
gap: 6px;
|
|
4347
|
+
border-radius: 6px;
|
|
4348
|
+
cursor: pointer;
|
|
4349
|
+
color: var(--vela-fg-bright);
|
|
4350
|
+
font-size: 13px;
|
|
4351
|
+
font-weight: 600;
|
|
4352
|
+
-webkit-tap-highlight-color: transparent;
|
|
4353
|
+
}
|
|
4354
|
+
.vela-mb-item:active { background: var(--vela-hover); }
|
|
4355
|
+
.vela-mb-item .vela-icon { font-size: 18px; width: 18px; height: 18px; }
|
|
4356
|
+
/* Left-aligned contributed actions get their own stops (the built-in indicators
|
|
4357
|
+
slot) \u2014 the wrapper is layout-transparent so each stop flexes like a sibling. */
|
|
4358
|
+
.vela-mb-actions { display: contents; }
|
|
4359
|
+
.vela-mb-symbol {
|
|
4360
|
+
flex: 1.6 1 0;
|
|
4361
|
+
font-size: 14px;
|
|
4362
|
+
letter-spacing: 0.3px;
|
|
4363
|
+
white-space: nowrap;
|
|
4364
|
+
overflow: hidden;
|
|
4365
|
+
text-overflow: ellipsis;
|
|
4366
|
+
display: block;
|
|
4367
|
+
line-height: 44px;
|
|
4368
|
+
text-align: center;
|
|
4369
|
+
}
|
|
4370
|
+
`;
|
|
4371
|
+
var MobileBar = class {
|
|
4372
|
+
constructor(host, opts) {
|
|
4373
|
+
this.opts = opts;
|
|
4374
|
+
const doc = host.ownerDocument;
|
|
4375
|
+
injectStyles(STYLE_ID12, CSS12, doc);
|
|
4376
|
+
this.el = doc.createElement("div");
|
|
4377
|
+
this.el.className = "vela-mobilebar";
|
|
4378
|
+
const item = (cls, label, onClick, icon2) => {
|
|
4379
|
+
const b = doc.createElement("button");
|
|
4380
|
+
b.className = `vela-mb-item ${cls}`;
|
|
4381
|
+
b.setAttribute("aria-label", label);
|
|
4382
|
+
if (icon2) b.appendChild(iconEl(icon2, doc));
|
|
4383
|
+
b.addEventListener("click", onClick);
|
|
4384
|
+
return b;
|
|
4385
|
+
};
|
|
4386
|
+
this.symbolEl = item("vela-mb-symbol", "Symbol search", opts.onSymbolClick);
|
|
4387
|
+
this.symbolEl.textContent = parseSymbol(opts.symbol).ticker;
|
|
4388
|
+
this.tfEl = item("vela-mb-tf", "Timeframe", opts.onTimeframeClick);
|
|
4389
|
+
this.tfEl.textContent = timeframeLabel(opts.timeframe);
|
|
4390
|
+
const onIndicators = opts.onIndicatorsClick;
|
|
4391
|
+
const indicators = onIndicators ? item("vela-mb-indicators", "Indicators", onIndicators, "indicators") : null;
|
|
4392
|
+
this.actionsHost = doc.createElement("span");
|
|
4393
|
+
this.actionsHost.className = "vela-mb-actions";
|
|
4394
|
+
const drawings = item("vela-mb-drawings", "Drawings", opts.onDrawingsClick, "pen");
|
|
4395
|
+
const more = item("vela-mb-more", "More", opts.onMoreClick, "kebab");
|
|
4396
|
+
const settings = item("vela-mb-settings", "Chart settings", opts.onSettingsClick, "gear");
|
|
4397
|
+
this.el.append(this.symbolEl, this.tfEl, ...indicators ? [indicators] : [], this.actionsHost, drawings, more, settings);
|
|
4398
|
+
host.appendChild(this.el);
|
|
4399
|
+
this.renderActions();
|
|
4400
|
+
}
|
|
4401
|
+
/** Re-project the left-aligned contributed actions as icon-only stops in the
|
|
4402
|
+
* indicators slot (call after registrations change). Right-aligned actions stay
|
|
4403
|
+
* in the three-dots drawer — a primary stop is what `align: 'left'` opts into. */
|
|
4404
|
+
renderActions() {
|
|
4405
|
+
const ctx = this.opts.getContext?.();
|
|
4406
|
+
if (!ctx) return;
|
|
4407
|
+
const doc = this.el.ownerDocument;
|
|
4408
|
+
this.actionsHost.replaceChildren();
|
|
4409
|
+
for (const action of widgetActions("topbar", ctx).filter((a) => a.align === "left")) {
|
|
4410
|
+
const b = doc.createElement("button");
|
|
4411
|
+
b.className = "vela-mb-item";
|
|
4412
|
+
b.setAttribute("aria-label", action.label);
|
|
4413
|
+
if (action.icon) b.appendChild(iconEl(action.icon, doc));
|
|
4414
|
+
else b.appendChild(doc.createTextNode(action.label));
|
|
4415
|
+
b.addEventListener("click", () => {
|
|
4416
|
+
const c = this.opts.getContext?.();
|
|
4417
|
+
if (c) action.run(c);
|
|
4418
|
+
});
|
|
4419
|
+
this.actionsHost.appendChild(b);
|
|
4420
|
+
}
|
|
4421
|
+
}
|
|
4422
|
+
setSymbol(symbol) {
|
|
4423
|
+
this.symbolEl.textContent = parseSymbol(symbol).ticker;
|
|
4424
|
+
}
|
|
4425
|
+
setTimeframe(tf) {
|
|
4426
|
+
this.tfEl.textContent = timeframeLabel(tf);
|
|
4427
|
+
}
|
|
4428
|
+
destroy() {
|
|
4429
|
+
this.el.remove();
|
|
4430
|
+
}
|
|
4431
|
+
};
|
|
4432
|
+
|
|
4433
|
+
// src/widget/timeframe-drawer.ts
|
|
4434
|
+
var STYLE_ID13 = "vela-widget-tf-drawer";
|
|
4435
|
+
var CSS13 = `
|
|
4436
|
+
.vela-tfd-heading {
|
|
4437
|
+
padding: 4px 2px 8px;
|
|
4438
|
+
font-size: 12px;
|
|
4439
|
+
font-weight: 600;
|
|
4440
|
+
letter-spacing: 0.4px;
|
|
4441
|
+
text-transform: uppercase;
|
|
4442
|
+
color: var(--vela-fg-bright);
|
|
4443
|
+
}
|
|
4444
|
+
.vela-tfd-heading + .vela-tfd-ranges,
|
|
4445
|
+
.vela-tfd-heading + .vela-tfd-grid { padding-top: 0; }
|
|
4446
|
+
.vela-tfd-ranges {
|
|
4447
|
+
display: flex;
|
|
4448
|
+
gap: 6px;
|
|
4449
|
+
overflow-x: auto;
|
|
4450
|
+
scrollbar-width: none;
|
|
4451
|
+
/* Sideways-scrolling strip: keep its touches native scroll (the drawer body is pan-y). */
|
|
4452
|
+
touch-action: pan-x;
|
|
4453
|
+
padding: 0 2px 14px;
|
|
4454
|
+
}
|
|
4455
|
+
.vela-tfd-ranges::-webkit-scrollbar { display: none; }
|
|
4456
|
+
.vela-tfd-grid {
|
|
4457
|
+
display: grid;
|
|
4458
|
+
grid-template-columns: repeat(auto-fill, minmax(64px, 1fr));
|
|
4459
|
+
gap: 6px;
|
|
4460
|
+
padding-bottom: 4px;
|
|
4461
|
+
}
|
|
4462
|
+
.vela-tfd-chip {
|
|
4463
|
+
all: unset;
|
|
4464
|
+
min-height: 40px;
|
|
4465
|
+
padding: 0 14px;
|
|
4466
|
+
display: inline-flex;
|
|
4467
|
+
align-items: center;
|
|
4468
|
+
justify-content: center;
|
|
4469
|
+
flex: none;
|
|
4470
|
+
border: 1px solid var(--vela-border);
|
|
4471
|
+
border-radius: 8px;
|
|
4472
|
+
font-size: 13px;
|
|
4473
|
+
font-weight: 600;
|
|
4474
|
+
color: var(--vela-fg);
|
|
4475
|
+
cursor: pointer;
|
|
4476
|
+
-webkit-tap-highlight-color: transparent;
|
|
4477
|
+
}
|
|
4478
|
+
.vela-tfd-chip:active { background: var(--vela-hover); }
|
|
4479
|
+
.vela-tfd-chip[data-active='1'] {
|
|
4480
|
+
color: var(--vela-fg-bright);
|
|
4481
|
+
border-color: var(--vela-fg-bright);
|
|
4482
|
+
background: var(--vela-hover);
|
|
4483
|
+
}
|
|
4484
|
+
`;
|
|
4485
|
+
var TimeframeDrawer = class {
|
|
4486
|
+
constructor(opts) {
|
|
4487
|
+
this.opts = opts;
|
|
4488
|
+
this.rangeChips = /* @__PURE__ */ new Map();
|
|
4489
|
+
this.tfChips = /* @__PURE__ */ new Map();
|
|
4490
|
+
const doc = opts.host.ownerDocument;
|
|
4491
|
+
injectStyles(STYLE_ID13, CSS13, doc);
|
|
4492
|
+
this.drawer = new Drawer({ host: opts.host, onOpenChange: opts.onOpenChange });
|
|
4493
|
+
const rangeHeading = doc.createElement("div");
|
|
4494
|
+
rangeHeading.className = "vela-tfd-heading";
|
|
4495
|
+
rangeHeading.textContent = "Date Range";
|
|
4496
|
+
const ranges = doc.createElement("div");
|
|
4497
|
+
ranges.className = "vela-tfd-ranges";
|
|
4498
|
+
for (const preset of opts.ranges) {
|
|
4499
|
+
const chip = doc.createElement("button");
|
|
4500
|
+
chip.className = "vela-tfd-chip";
|
|
4501
|
+
chip.textContent = preset.id;
|
|
4502
|
+
chip.addEventListener("click", () => {
|
|
4503
|
+
opts.onRange(preset);
|
|
4504
|
+
this.drawer.hide();
|
|
4505
|
+
});
|
|
4506
|
+
this.rangeChips.set(preset.id, chip);
|
|
4507
|
+
ranges.appendChild(chip);
|
|
4508
|
+
}
|
|
4509
|
+
const tfHeading = doc.createElement("div");
|
|
4510
|
+
tfHeading.className = "vela-tfd-heading";
|
|
4511
|
+
tfHeading.textContent = "Timeframes";
|
|
4512
|
+
const grid = doc.createElement("div");
|
|
4513
|
+
grid.className = "vela-tfd-grid";
|
|
4514
|
+
for (const tf of opts.timeframes) {
|
|
4515
|
+
const chip = doc.createElement("button");
|
|
4516
|
+
chip.className = "vela-tfd-chip";
|
|
4517
|
+
chip.textContent = timeframeLabel(tf);
|
|
4518
|
+
chip.addEventListener("click", () => {
|
|
4519
|
+
opts.onTimeframe(tf);
|
|
4520
|
+
this.drawer.hide();
|
|
4521
|
+
});
|
|
4522
|
+
this.tfChips.set(tf, chip);
|
|
4523
|
+
grid.appendChild(chip);
|
|
4524
|
+
}
|
|
4525
|
+
this.drawer.body.append(rangeHeading, ranges, tfHeading, grid);
|
|
4526
|
+
}
|
|
4527
|
+
open() {
|
|
4528
|
+
const tf = this.opts.currentTimeframe();
|
|
4529
|
+
const range = this.opts.activeRange();
|
|
4530
|
+
for (const [id, chip] of this.rangeChips) {
|
|
4531
|
+
if (range !== null && id === range) chip.dataset.active = "1";
|
|
4532
|
+
else delete chip.dataset.active;
|
|
4533
|
+
}
|
|
4534
|
+
for (const [id, chip] of this.tfChips) {
|
|
4535
|
+
if (range === null && id === tf) chip.dataset.active = "1";
|
|
4536
|
+
else delete chip.dataset.active;
|
|
4537
|
+
}
|
|
4538
|
+
this.drawer.show();
|
|
4539
|
+
}
|
|
4540
|
+
close() {
|
|
4541
|
+
this.drawer.hide();
|
|
4542
|
+
}
|
|
4543
|
+
destroy() {
|
|
4544
|
+
this.drawer.destroy();
|
|
4545
|
+
}
|
|
4546
|
+
};
|
|
4547
|
+
|
|
4548
|
+
// src/widget/drawings-drawer.ts
|
|
4549
|
+
var STYLE_ID14 = "vela-widget-drawings-drawer";
|
|
4550
|
+
var CSS14 = `
|
|
4551
|
+
/* Search + tabs stay pinned while the tool list scrolls underneath. */
|
|
4552
|
+
.vela-dd-sticky {
|
|
4553
|
+
position: sticky;
|
|
4554
|
+
top: 0;
|
|
4555
|
+
z-index: 1;
|
|
4556
|
+
background: var(--vela-surface);
|
|
4557
|
+
padding-top: 2px;
|
|
4558
|
+
}
|
|
4559
|
+
.vela-dd-search {
|
|
4560
|
+
display: flex;
|
|
4561
|
+
align-items: center;
|
|
4562
|
+
gap: 8px;
|
|
4563
|
+
padding: 8px 12px;
|
|
4564
|
+
margin: 0 2px 8px;
|
|
4565
|
+
border: 1px solid var(--vela-border);
|
|
4566
|
+
border-radius: 8px;
|
|
4567
|
+
color: var(--vela-fg-muted);
|
|
4568
|
+
background: var(--vela-surface);
|
|
4569
|
+
}
|
|
4570
|
+
.vela-dd-search input {
|
|
4571
|
+
all: unset;
|
|
4572
|
+
flex: 1 1 auto;
|
|
4573
|
+
min-width: 0;
|
|
4574
|
+
font-size: 14px;
|
|
4575
|
+
color: var(--vela-fg-bright);
|
|
4576
|
+
}
|
|
4577
|
+
.vela-dd-tabs {
|
|
4578
|
+
display: flex;
|
|
4579
|
+
gap: 4px;
|
|
4580
|
+
overflow-x: auto;
|
|
4581
|
+
scrollbar-width: none;
|
|
4582
|
+
/* The strip scrolls sideways itself \u2014 its touches are native scroll, not tab swipes
|
|
4583
|
+
(the drawer body is pan-y so everywhere ELSE a sideways move swipes the tabs). */
|
|
4584
|
+
touch-action: pan-x;
|
|
4585
|
+
padding: 0 2px 8px;
|
|
4586
|
+
border-bottom: 1px solid var(--vela-border);
|
|
4587
|
+
background: var(--vela-surface);
|
|
4588
|
+
}
|
|
4589
|
+
.vela-dd-tabs::-webkit-scrollbar { display: none; }
|
|
4590
|
+
.vela-dd-tab {
|
|
4591
|
+
all: unset;
|
|
4592
|
+
flex: none;
|
|
4593
|
+
min-height: 36px;
|
|
4594
|
+
padding: 0 12px;
|
|
4595
|
+
display: inline-flex;
|
|
4596
|
+
align-items: center;
|
|
4597
|
+
border-radius: 8px;
|
|
4598
|
+
font-size: 13px;
|
|
4599
|
+
font-weight: 600;
|
|
4600
|
+
color: var(--vela-fg-muted);
|
|
4601
|
+
cursor: pointer;
|
|
4602
|
+
-webkit-tap-highlight-color: transparent;
|
|
4603
|
+
}
|
|
4604
|
+
.vela-dd-tab[data-active='1'] { color: var(--vela-fg-bright); background: var(--vela-hover); }
|
|
4605
|
+
.vela-dd-list { padding: 6px 0 4px; }
|
|
4606
|
+
.vela-dd-section {
|
|
4607
|
+
padding: 10px 2px 6px;
|
|
4608
|
+
font-size: 12px;
|
|
4609
|
+
font-weight: 600;
|
|
4610
|
+
letter-spacing: 0.4px;
|
|
4611
|
+
text-transform: uppercase;
|
|
4612
|
+
color: var(--vela-fg-muted);
|
|
4613
|
+
}
|
|
4614
|
+
.vela-dd-row {
|
|
4615
|
+
display: flex;
|
|
4616
|
+
align-items: center;
|
|
4617
|
+
gap: 12px;
|
|
4618
|
+
min-height: 44px;
|
|
4619
|
+
padding: 0 2px;
|
|
4620
|
+
border-radius: 8px;
|
|
4621
|
+
cursor: pointer;
|
|
4622
|
+
-webkit-tap-highlight-color: transparent;
|
|
4623
|
+
}
|
|
4624
|
+
.vela-dd-row:active { background: var(--vela-hover); }
|
|
4625
|
+
.vela-dd-row[data-active='1'] { background: var(--vela-hover); }
|
|
4626
|
+
.vela-dd-row[data-active='1'] .vela-dd-label { color: var(--vela-accent); }
|
|
4627
|
+
.vela-dd-glyph { flex: none; width: 24px; height: 24px; color: var(--vela-fg); }
|
|
4628
|
+
.vela-dd-glyph svg { width: 24px; height: 24px; }
|
|
4629
|
+
.vela-dd-label { flex: 1 1 auto; min-width: 0; font-size: 14px; color: var(--vela-fg-bright); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
4630
|
+
.vela-dd-star {
|
|
4631
|
+
all: unset;
|
|
4632
|
+
flex: none;
|
|
4633
|
+
width: 40px;
|
|
4634
|
+
height: 40px;
|
|
4635
|
+
display: inline-flex;
|
|
4636
|
+
align-items: center;
|
|
4637
|
+
justify-content: center;
|
|
4638
|
+
border-radius: 8px;
|
|
4639
|
+
color: var(--vela-fg-muted);
|
|
4640
|
+
cursor: pointer;
|
|
4641
|
+
-webkit-tap-highlight-color: transparent;
|
|
4642
|
+
}
|
|
4643
|
+
.vela-dd-star[data-on='1'] { color: var(--vela-highlight); }
|
|
4644
|
+
.vela-dd-empty { padding: 18px 2px; color: var(--vela-fg-muted); font-size: 13px; }
|
|
4645
|
+
`;
|
|
4646
|
+
var DrawingsDrawer = class {
|
|
4647
|
+
constructor(opts) {
|
|
4648
|
+
this.opts = opts;
|
|
4649
|
+
this.definition = { groups: [] };
|
|
4650
|
+
this.activeGroup = "";
|
|
4651
|
+
const doc = opts.host.ownerDocument;
|
|
4652
|
+
injectStyles(STYLE_ID14, CSS14, doc);
|
|
4653
|
+
this.drawer = new Drawer({
|
|
4654
|
+
host: opts.host,
|
|
4655
|
+
title: "Drawings",
|
|
4656
|
+
onOpenChange: opts.onOpenChange,
|
|
4657
|
+
// Swiping across the tool list pages through the group tabs.
|
|
4658
|
+
onSwipe: (dir) => this.stepGroup(dir === "left" ? 1 : -1)
|
|
4659
|
+
});
|
|
4660
|
+
const sticky = doc.createElement("div");
|
|
4661
|
+
sticky.className = "vela-dd-sticky";
|
|
4662
|
+
const search = doc.createElement("div");
|
|
4663
|
+
search.className = "vela-dd-search";
|
|
4664
|
+
search.appendChild(iconEl("search", doc));
|
|
4665
|
+
this.input = doc.createElement("input");
|
|
4666
|
+
this.input.type = "text";
|
|
4667
|
+
this.input.placeholder = "Search tools\u2026";
|
|
4668
|
+
this.input.addEventListener("input", () => this.renderList());
|
|
4669
|
+
search.appendChild(this.input);
|
|
4670
|
+
this.tabs = doc.createElement("div");
|
|
4671
|
+
this.tabs.className = "vela-dd-tabs";
|
|
4672
|
+
sticky.append(search, this.tabs);
|
|
4673
|
+
this.list = doc.createElement("div");
|
|
4674
|
+
this.list.className = "vela-dd-list";
|
|
4675
|
+
this.drawer.body.append(sticky, this.list);
|
|
4676
|
+
}
|
|
4677
|
+
open() {
|
|
4678
|
+
this.definition = this.opts.toolbar();
|
|
4679
|
+
if (!this.definition.groups.some((g) => g.id === this.activeGroup)) this.activeGroup = this.definition.groups[0]?.id ?? "";
|
|
4680
|
+
this.input.value = "";
|
|
4681
|
+
this.renderTabs();
|
|
4682
|
+
this.renderList();
|
|
4683
|
+
this.drawer.show();
|
|
4684
|
+
}
|
|
4685
|
+
close() {
|
|
4686
|
+
this.drawer.hide();
|
|
4687
|
+
}
|
|
4688
|
+
destroy() {
|
|
4689
|
+
this.drawer.destroy();
|
|
4690
|
+
}
|
|
4691
|
+
/** Move the active group tab by `step` (a horizontal swipe on the list). Search mode
|
|
4692
|
+
* shows the flattened results — no tabs to page through, so the swipe is inert. */
|
|
4693
|
+
stepGroup(step) {
|
|
4694
|
+
if (this.input.value.trim()) return;
|
|
4695
|
+
const groups = this.definition.groups;
|
|
4696
|
+
const next = groups[groups.findIndex((g) => g.id === this.activeGroup) + step];
|
|
4697
|
+
if (!next) return;
|
|
4698
|
+
this.activeGroup = next.id;
|
|
4699
|
+
this.renderTabs();
|
|
4700
|
+
this.renderList();
|
|
4701
|
+
this.tabs.querySelector('[data-active="1"]')?.scrollIntoView?.({ block: "nearest", inline: "nearest" });
|
|
4702
|
+
}
|
|
4703
|
+
renderTabs() {
|
|
4704
|
+
const doc = this.tabs.ownerDocument;
|
|
4705
|
+
this.tabs.replaceChildren();
|
|
4706
|
+
for (const group of this.definition.groups) {
|
|
4707
|
+
const tab = doc.createElement("button");
|
|
4708
|
+
tab.className = "vela-dd-tab";
|
|
4709
|
+
tab.textContent = group.label;
|
|
4710
|
+
if (group.id === this.activeGroup) tab.dataset.active = "1";
|
|
4711
|
+
tab.addEventListener("click", () => {
|
|
4712
|
+
this.activeGroup = group.id;
|
|
4713
|
+
this.input.value = "";
|
|
4714
|
+
this.renderTabs();
|
|
4715
|
+
this.renderList();
|
|
4716
|
+
});
|
|
4717
|
+
this.tabs.appendChild(tab);
|
|
4718
|
+
}
|
|
4719
|
+
}
|
|
4720
|
+
renderList() {
|
|
4721
|
+
const doc = this.list.ownerDocument;
|
|
4722
|
+
this.list.replaceChildren();
|
|
4723
|
+
const query = this.input.value.trim().toLowerCase();
|
|
4724
|
+
const active = this.opts.currentTool();
|
|
4725
|
+
const section = (label) => {
|
|
4726
|
+
const el = doc.createElement("div");
|
|
4727
|
+
el.className = "vela-dd-section";
|
|
4728
|
+
el.textContent = label;
|
|
4729
|
+
this.list.appendChild(el);
|
|
4730
|
+
};
|
|
4731
|
+
const row = (tool) => {
|
|
4732
|
+
const el = doc.createElement("div");
|
|
4733
|
+
el.className = "vela-dd-row";
|
|
4734
|
+
if (tool.type === active) el.dataset.active = "1";
|
|
4735
|
+
const glyph = doc.createElement("span");
|
|
4736
|
+
glyph.className = "vela-dd-glyph";
|
|
4737
|
+
glyph.innerHTML = tool.icon;
|
|
4738
|
+
const label = doc.createElement("span");
|
|
4739
|
+
label.className = "vela-dd-label";
|
|
4740
|
+
label.textContent = tool.label;
|
|
4741
|
+
const star = doc.createElement("button");
|
|
4742
|
+
star.className = "vela-dd-star";
|
|
4743
|
+
const paintStar = (on) => {
|
|
4744
|
+
star.replaceChildren(iconEl(on ? "star-filled" : "star", doc));
|
|
4745
|
+
if (on) star.dataset.on = "1";
|
|
4746
|
+
else delete star.dataset.on;
|
|
4747
|
+
};
|
|
4748
|
+
paintStar(this.opts.isFavorite(tool.type));
|
|
4749
|
+
star.setAttribute("aria-label", `Favorite ${tool.label}`);
|
|
4750
|
+
star.addEventListener("click", (e) => {
|
|
4751
|
+
e.stopPropagation();
|
|
4752
|
+
const on = !this.opts.isFavorite(tool.type);
|
|
4753
|
+
this.opts.onFavorite(tool.type, on);
|
|
4754
|
+
paintStar(on);
|
|
4755
|
+
});
|
|
4756
|
+
el.append(glyph, label, star);
|
|
4757
|
+
el.addEventListener("click", () => {
|
|
4758
|
+
this.opts.onSelect(tool.type);
|
|
4759
|
+
this.drawer.hide();
|
|
4760
|
+
});
|
|
4761
|
+
this.list.appendChild(el);
|
|
4762
|
+
};
|
|
4763
|
+
if (query) {
|
|
4764
|
+
let any = false;
|
|
4765
|
+
for (const group2 of this.definition.groups) {
|
|
4766
|
+
const hits = group2.tools.filter((t) => t.label.toLowerCase().includes(query));
|
|
4767
|
+
if (hits.length === 0) continue;
|
|
4768
|
+
any = true;
|
|
4769
|
+
section(group2.label);
|
|
4770
|
+
for (const tool of hits) row(tool);
|
|
4771
|
+
}
|
|
4772
|
+
if (!any) {
|
|
4773
|
+
const empty = doc.createElement("div");
|
|
4774
|
+
empty.className = "vela-dd-empty";
|
|
4775
|
+
empty.textContent = "No tools match.";
|
|
4776
|
+
this.list.appendChild(empty);
|
|
4777
|
+
}
|
|
4778
|
+
return;
|
|
4779
|
+
}
|
|
4780
|
+
const group = this.definition.groups.find((g) => g.id === this.activeGroup);
|
|
4781
|
+
if (!group) return;
|
|
4782
|
+
if (group.sections && group.sections.length > 0) {
|
|
4783
|
+
for (const s of group.sections) {
|
|
4784
|
+
section(s.label);
|
|
4785
|
+
for (const tool of s.tools) row(tool);
|
|
4786
|
+
}
|
|
4787
|
+
} else {
|
|
4788
|
+
for (const tool of group.tools) row(tool);
|
|
4789
|
+
}
|
|
4790
|
+
}
|
|
4791
|
+
};
|
|
4792
|
+
|
|
4793
|
+
// src/widget/more-drawer.ts
|
|
4794
|
+
var STYLE_ID15 = "vela-widget-more-drawer";
|
|
4795
|
+
var CSS15 = `
|
|
4796
|
+
.vela-md-actions {
|
|
4797
|
+
display: flex;
|
|
4798
|
+
gap: 6px;
|
|
4799
|
+
padding: 2px 2px 10px;
|
|
4800
|
+
border-bottom: 1px solid var(--vela-border);
|
|
4801
|
+
}
|
|
4802
|
+
.vela-md-action {
|
|
4803
|
+
all: unset;
|
|
4804
|
+
flex: 1 1 0;
|
|
4805
|
+
min-height: 48px;
|
|
4806
|
+
display: inline-flex;
|
|
4807
|
+
flex-direction: column;
|
|
4808
|
+
align-items: center;
|
|
4809
|
+
justify-content: center;
|
|
4810
|
+
gap: 4px;
|
|
4811
|
+
border-radius: 8px;
|
|
4812
|
+
font-size: 11px;
|
|
4813
|
+
color: var(--vela-fg);
|
|
4814
|
+
cursor: pointer;
|
|
4815
|
+
-webkit-tap-highlight-color: transparent;
|
|
4816
|
+
}
|
|
4817
|
+
.vela-md-action:active { background: var(--vela-hover); }
|
|
4818
|
+
.vela-md-action:disabled { opacity: 0.35; cursor: default; }
|
|
4819
|
+
.vela-md-action .vela-icon { font-size: 17px; width: 17px; height: 17px; }
|
|
4820
|
+
.vela-md-list { padding: 6px 0 4px; }
|
|
4821
|
+
.vela-md-row {
|
|
4822
|
+
display: flex;
|
|
4823
|
+
align-items: center;
|
|
4824
|
+
gap: 12px;
|
|
4825
|
+
min-height: 46px;
|
|
4826
|
+
padding: 0 2px;
|
|
4827
|
+
border-radius: 8px;
|
|
4828
|
+
cursor: pointer;
|
|
4829
|
+
-webkit-tap-highlight-color: transparent;
|
|
4830
|
+
}
|
|
4831
|
+
.vela-md-row:active { background: var(--vela-hover); }
|
|
4832
|
+
.vela-md-row[data-checked='1'] { background: var(--vela-hover-strong); }
|
|
4833
|
+
.vela-md-row .vela-icon { flex: none; font-size: 17px; width: 17px; height: 17px; color: var(--vela-fg-muted); }
|
|
4834
|
+
.vela-md-row-label { flex: 1 1 auto; min-width: 0; font-size: 14px; color: var(--vela-fg-bright); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
4835
|
+
.vela-md-row-value { flex: none; font-size: 13px; color: var(--vela-fg-muted); }
|
|
4836
|
+
.vela-md-back {
|
|
4837
|
+
all: unset;
|
|
4838
|
+
display: flex;
|
|
4839
|
+
align-items: center;
|
|
4840
|
+
gap: 8px;
|
|
4841
|
+
min-height: 42px;
|
|
4842
|
+
padding: 0 2px;
|
|
4843
|
+
font-size: 14px;
|
|
4844
|
+
font-weight: 600;
|
|
4845
|
+
color: var(--vela-fg-bright);
|
|
4846
|
+
cursor: pointer;
|
|
4847
|
+
border-bottom: 1px solid var(--vela-border);
|
|
4848
|
+
width: 100%;
|
|
4849
|
+
box-sizing: border-box;
|
|
4850
|
+
-webkit-tap-highlight-color: transparent;
|
|
4851
|
+
}
|
|
4852
|
+
/* The layout sub-view's grid canvas, centered and touch-sized (the base .vela-lp-grid
|
|
4853
|
+
squares are popover-sized for a mouse). */
|
|
4854
|
+
.vela-md-gridwrap { display: flex; justify-content: center; padding: 14px 0 10px; }
|
|
4855
|
+
.vela-md-gridwrap .vela-lp-grid { grid-template-columns: repeat(4, 44px); grid-auto-rows: 44px; gap: 8px; }
|
|
4856
|
+
.vela-md-section {
|
|
4857
|
+
padding: 12px 2px 4px;
|
|
4858
|
+
font-size: 12px;
|
|
4859
|
+
font-weight: 600;
|
|
4860
|
+
letter-spacing: 0.4px;
|
|
4861
|
+
text-transform: uppercase;
|
|
4862
|
+
color: var(--vela-fg-muted);
|
|
4863
|
+
}
|
|
4864
|
+
.vela-md-empty { padding: 18px 2px; color: var(--vela-fg-muted); font-size: 13px; }
|
|
4865
|
+
`;
|
|
4866
|
+
var MoreDrawer = class {
|
|
4867
|
+
constructor(opts) {
|
|
4868
|
+
this.opts = opts;
|
|
4869
|
+
this.view = "main";
|
|
4870
|
+
injectStyles(STYLE_ID15, CSS15, opts.host.ownerDocument);
|
|
4871
|
+
this.drawer = new Drawer({ host: opts.host, onOpenChange: opts.onOpenChange });
|
|
4872
|
+
}
|
|
4873
|
+
open() {
|
|
4874
|
+
this.view = "main";
|
|
4875
|
+
this.render();
|
|
4876
|
+
this.drawer.show();
|
|
4877
|
+
}
|
|
4878
|
+
close() {
|
|
4879
|
+
this.drawer.hide();
|
|
4880
|
+
}
|
|
4881
|
+
destroy() {
|
|
4882
|
+
this.drawer.destroy();
|
|
4883
|
+
}
|
|
4884
|
+
show(view) {
|
|
4885
|
+
this.view = view;
|
|
4886
|
+
this.render();
|
|
4887
|
+
}
|
|
4888
|
+
render() {
|
|
4889
|
+
const doc = this.drawer.body.ownerDocument;
|
|
4890
|
+
this.drawer.body.replaceChildren();
|
|
4891
|
+
if (this.view !== "main") {
|
|
4892
|
+
const back = doc.createElement("button");
|
|
4893
|
+
back.className = "vela-md-back";
|
|
4894
|
+
back.appendChild(iconEl("chevron-left", doc));
|
|
4895
|
+
back.appendChild(doc.createTextNode(this.view === "style" ? "Chart type" : this.view === "layout" ? "Layout" : "Alerts"));
|
|
4896
|
+
back.addEventListener("click", () => this.show("main"));
|
|
4897
|
+
this.drawer.body.appendChild(back);
|
|
4898
|
+
}
|
|
4899
|
+
if (this.view === "main") this.renderMain(doc);
|
|
4900
|
+
else if (this.view === "style") this.renderStyle(doc);
|
|
4901
|
+
else if (this.view === "layout") this.renderLayout(doc);
|
|
4902
|
+
else this.renderAlerts(doc);
|
|
4903
|
+
}
|
|
4904
|
+
row(doc, label, opts) {
|
|
4905
|
+
const el = doc.createElement("div");
|
|
4906
|
+
el.className = "vela-md-row";
|
|
4907
|
+
if (opts.icon) el.appendChild(iconEl(opts.icon, doc));
|
|
4908
|
+
const text = doc.createElement("span");
|
|
4909
|
+
text.className = "vela-md-row-label";
|
|
4910
|
+
text.textContent = label;
|
|
4911
|
+
el.appendChild(text);
|
|
4912
|
+
if (opts.value) {
|
|
4913
|
+
const value = doc.createElement("span");
|
|
4914
|
+
value.className = "vela-md-row-value";
|
|
4915
|
+
value.textContent = opts.value;
|
|
4916
|
+
el.appendChild(value);
|
|
4917
|
+
}
|
|
4918
|
+
if (opts.checked) el.dataset.checked = "1";
|
|
4919
|
+
if (opts.chevron) el.appendChild(iconEl("chevron-right", doc));
|
|
4920
|
+
el.addEventListener("click", opts.onClick);
|
|
4921
|
+
return el;
|
|
4922
|
+
}
|
|
4923
|
+
renderMain(doc) {
|
|
4924
|
+
const actions = doc.createElement("div");
|
|
4925
|
+
actions.className = "vela-md-actions";
|
|
4926
|
+
const action = (icon2, label, enabled, run) => {
|
|
4927
|
+
const b = doc.createElement("button");
|
|
4928
|
+
b.className = "vela-md-action";
|
|
4929
|
+
b.disabled = !enabled;
|
|
4930
|
+
b.appendChild(iconEl(icon2, doc));
|
|
4931
|
+
b.appendChild(doc.createTextNode(label));
|
|
4932
|
+
b.addEventListener("click", () => {
|
|
4933
|
+
run();
|
|
4934
|
+
this.drawer.hide();
|
|
4935
|
+
});
|
|
4936
|
+
actions.appendChild(b);
|
|
4937
|
+
};
|
|
4938
|
+
action("undo", "Undo", this.opts.canUndo(), this.opts.onUndo);
|
|
4939
|
+
action("redo", "Redo", this.opts.canRedo(), this.opts.onRedo);
|
|
4940
|
+
action("camera", "Screenshot", true, this.opts.onScreenshot);
|
|
4941
|
+
this.drawer.body.appendChild(actions);
|
|
4942
|
+
const list = doc.createElement("div");
|
|
4943
|
+
list.className = "vela-md-list";
|
|
4944
|
+
const current = this.opts.priceStyles().find((s) => s.id === this.opts.priceStyle());
|
|
4945
|
+
list.appendChild(this.row(doc, "Chart type", { icon: "style-line", value: current?.label, chevron: true, onClick: () => this.show("style") }));
|
|
4946
|
+
if (this.opts.layout) {
|
|
4947
|
+
const shape = this.opts.layout.shape();
|
|
4948
|
+
const value = shape ? `${shape.cols} \xD7 ${shape.rows}` : this.opts.layout.presets().find((p) => p.checked)?.label;
|
|
4949
|
+
list.appendChild(this.row(doc, "Layout", { icon: "layout", value, chevron: true, onClick: () => this.show("layout") }));
|
|
4950
|
+
}
|
|
4951
|
+
for (const panel of this.opts.panels()) {
|
|
4952
|
+
list.appendChild(
|
|
4953
|
+
this.row(doc, panel.title, {
|
|
4954
|
+
icon: panel.icon,
|
|
4955
|
+
onClick: () => {
|
|
4956
|
+
this.opts.onTogglePanel(panel.id);
|
|
4957
|
+
this.drawer.hide();
|
|
4958
|
+
}
|
|
4959
|
+
})
|
|
4960
|
+
);
|
|
4961
|
+
}
|
|
4962
|
+
const alertCount = this.opts.alerts().length;
|
|
4963
|
+
list.appendChild(this.row(doc, "Alerts", { icon: "bell", value: alertCount > 0 ? String(alertCount) : void 0, chevron: true, onClick: () => this.show("alerts") }));
|
|
4964
|
+
for (const act of this.opts.actions()) {
|
|
4965
|
+
list.appendChild(
|
|
4966
|
+
this.row(doc, act.label, {
|
|
4967
|
+
icon: act.icon,
|
|
4968
|
+
onClick: () => {
|
|
4969
|
+
act.run();
|
|
4970
|
+
this.drawer.hide();
|
|
4971
|
+
}
|
|
4972
|
+
})
|
|
4973
|
+
);
|
|
4974
|
+
}
|
|
4975
|
+
this.drawer.body.appendChild(list);
|
|
4976
|
+
}
|
|
4977
|
+
renderStyle(doc) {
|
|
4978
|
+
const list = doc.createElement("div");
|
|
4979
|
+
list.className = "vela-md-list";
|
|
4980
|
+
const current = this.opts.priceStyle();
|
|
4981
|
+
for (const style of this.opts.priceStyles()) {
|
|
4982
|
+
list.appendChild(
|
|
4983
|
+
this.row(doc, style.label, {
|
|
4984
|
+
icon: style.icon,
|
|
4985
|
+
checked: style.id === current,
|
|
4986
|
+
onClick: () => {
|
|
4987
|
+
this.opts.onPriceStyle(style.id);
|
|
4988
|
+
this.drawer.hide();
|
|
4989
|
+
}
|
|
4990
|
+
})
|
|
4991
|
+
);
|
|
4992
|
+
}
|
|
4993
|
+
this.drawer.body.appendChild(list);
|
|
4994
|
+
}
|
|
4995
|
+
renderLayout(doc) {
|
|
4996
|
+
const layout = this.opts.layout;
|
|
4997
|
+
if (!layout) return;
|
|
4998
|
+
const list = doc.createElement("div");
|
|
4999
|
+
list.className = "vela-md-list";
|
|
5000
|
+
const wrap = doc.createElement("div");
|
|
5001
|
+
wrap.className = "vela-md-gridwrap";
|
|
5002
|
+
const { el: grid, squares } = layoutGridCanvas(doc);
|
|
5003
|
+
paintLayoutGrid(squares, layout.shape());
|
|
5004
|
+
grid.addEventListener("click", (e) => {
|
|
5005
|
+
const sq = e.target?.closest?.(".vela-lp-sq");
|
|
5006
|
+
if (!(sq instanceof HTMLButtonElement)) return;
|
|
5007
|
+
layout.onSelectGrid(Number(sq.dataset.r) + 1, Number(sq.dataset.c) + 1);
|
|
5008
|
+
this.drawer.hide();
|
|
5009
|
+
});
|
|
5010
|
+
wrap.appendChild(grid);
|
|
5011
|
+
list.appendChild(wrap);
|
|
5012
|
+
for (const preset of layout.presets()) {
|
|
5013
|
+
list.appendChild(
|
|
5014
|
+
this.row(doc, preset.label, {
|
|
5015
|
+
checked: preset.checked,
|
|
5016
|
+
onClick: () => {
|
|
5017
|
+
layout.onSelectPreset(preset.id);
|
|
5018
|
+
this.drawer.hide();
|
|
5019
|
+
}
|
|
5020
|
+
})
|
|
5021
|
+
);
|
|
5022
|
+
}
|
|
5023
|
+
const syncs = layout.syncs?.() ?? [];
|
|
5024
|
+
if (syncs.length > 0) {
|
|
5025
|
+
const heading = doc.createElement("div");
|
|
5026
|
+
heading.className = "vela-md-section";
|
|
5027
|
+
heading.textContent = "Sync";
|
|
5028
|
+
list.appendChild(heading);
|
|
5029
|
+
for (const s of syncs) {
|
|
5030
|
+
list.appendChild(
|
|
5031
|
+
this.row(doc, s.label, {
|
|
5032
|
+
checked: s.checked,
|
|
5033
|
+
onClick: () => {
|
|
5034
|
+
layout.onToggleSync?.(s.id);
|
|
5035
|
+
this.render();
|
|
5036
|
+
}
|
|
5037
|
+
})
|
|
5038
|
+
);
|
|
5039
|
+
}
|
|
5040
|
+
}
|
|
5041
|
+
this.drawer.body.appendChild(list);
|
|
5042
|
+
}
|
|
5043
|
+
renderAlerts(doc) {
|
|
5044
|
+
const alerts = this.opts.alerts();
|
|
5045
|
+
if (alerts.length === 0) {
|
|
5046
|
+
const empty = doc.createElement("div");
|
|
5047
|
+
empty.className = "vela-md-empty";
|
|
5048
|
+
empty.textContent = "No alerts yet.";
|
|
5049
|
+
this.drawer.body.appendChild(empty);
|
|
5050
|
+
return;
|
|
5051
|
+
}
|
|
5052
|
+
const list = doc.createElement("div");
|
|
5053
|
+
list.className = "vela-md-list";
|
|
5054
|
+
for (const a of alerts) {
|
|
5055
|
+
list.appendChild(this.row(doc, `${a.title}: ${a.message}`, { value: new Date(a.time).toLocaleTimeString(), onClick: () => void 0 }));
|
|
5056
|
+
}
|
|
5057
|
+
this.drawer.body.appendChild(list);
|
|
5058
|
+
}
|
|
5059
|
+
};
|
|
5060
|
+
|
|
5061
|
+
// src/widget/timezone-drawer.ts
|
|
5062
|
+
var STYLE_ID16 = "vela-widget-timezone-drawer";
|
|
5063
|
+
var CSS16 = `
|
|
5064
|
+
.vela-tzd-list { padding: 2px 0 4px; }
|
|
5065
|
+
.vela-tzd-row {
|
|
5066
|
+
display: flex;
|
|
5067
|
+
align-items: center;
|
|
5068
|
+
gap: 12px;
|
|
5069
|
+
min-height: 46px;
|
|
5070
|
+
padding: 0 2px;
|
|
5071
|
+
border-radius: 8px;
|
|
5072
|
+
cursor: pointer;
|
|
5073
|
+
-webkit-tap-highlight-color: transparent;
|
|
5074
|
+
}
|
|
5075
|
+
.vela-tzd-row:active { background: var(--vela-hover); }
|
|
5076
|
+
.vela-tzd-row-label { flex: 1 1 auto; min-width: 0; font-size: 14px; color: var(--vela-fg-bright); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
5077
|
+
.vela-tzd-row .vela-icon { flex: none; color: var(--vela-fg-bright); }
|
|
5078
|
+
`;
|
|
5079
|
+
var TimezoneDrawer = class {
|
|
5080
|
+
constructor(opts) {
|
|
5081
|
+
this.opts = opts;
|
|
5082
|
+
injectStyles(STYLE_ID16, CSS16, opts.host.ownerDocument);
|
|
5083
|
+
this.drawer = new Drawer({ host: opts.host, title: "Time zone", onOpenChange: opts.onOpenChange });
|
|
5084
|
+
}
|
|
5085
|
+
open() {
|
|
5086
|
+
this.render();
|
|
5087
|
+
this.drawer.show();
|
|
5088
|
+
}
|
|
5089
|
+
close() {
|
|
5090
|
+
this.drawer.hide();
|
|
5091
|
+
}
|
|
5092
|
+
destroy() {
|
|
5093
|
+
this.drawer.destroy();
|
|
5094
|
+
}
|
|
5095
|
+
render() {
|
|
5096
|
+
const doc = this.drawer.body.ownerDocument;
|
|
5097
|
+
this.drawer.body.replaceChildren();
|
|
5098
|
+
const list = doc.createElement("div");
|
|
5099
|
+
list.className = "vela-tzd-list";
|
|
5100
|
+
const current = normalizeTimezone(this.opts.timezone());
|
|
5101
|
+
for (const tz of TIMEZONES) {
|
|
5102
|
+
const row = doc.createElement("div");
|
|
5103
|
+
row.className = "vela-tzd-row";
|
|
5104
|
+
const label = doc.createElement("span");
|
|
5105
|
+
label.className = "vela-tzd-row-label";
|
|
5106
|
+
label.textContent = tzMenuLabel(tz.value, tz.label);
|
|
5107
|
+
row.appendChild(label);
|
|
5108
|
+
if (tz.value === current) row.appendChild(iconEl("check", doc));
|
|
5109
|
+
row.addEventListener("click", () => {
|
|
5110
|
+
this.opts.onTimezone(tz.value);
|
|
5111
|
+
this.drawer.hide();
|
|
5112
|
+
});
|
|
5113
|
+
list.appendChild(row);
|
|
5114
|
+
}
|
|
5115
|
+
this.drawer.body.appendChild(list);
|
|
5116
|
+
}
|
|
5117
|
+
};
|
|
5118
|
+
|
|
5119
|
+
// src/widget/price-scale-drawer.ts
|
|
5120
|
+
var STYLE_ID17 = "vela-widget-price-scale-drawer";
|
|
5121
|
+
var CSS17 = `
|
|
5122
|
+
.vela-psd-list { padding: 2px 0 4px; }
|
|
5123
|
+
.vela-psd-row {
|
|
5124
|
+
display: flex;
|
|
5125
|
+
align-items: center;
|
|
5126
|
+
gap: 12px;
|
|
5127
|
+
min-height: 46px;
|
|
5128
|
+
padding: 0 2px;
|
|
5129
|
+
border-radius: 8px;
|
|
5130
|
+
cursor: pointer;
|
|
5131
|
+
-webkit-tap-highlight-color: transparent;
|
|
5132
|
+
}
|
|
5133
|
+
.vela-psd-row:active { background: var(--vela-hover); }
|
|
5134
|
+
.vela-psd-row[data-sep='1'] { margin-top: 6px; padding-top: 6px; border-top: 1px solid var(--vela-border); border-radius: 0 0 8px 8px; }
|
|
5135
|
+
.vela-psd-row-label { flex: 1 1 auto; min-width: 0; font-size: 14px; color: var(--vela-fg-bright); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
5136
|
+
.vela-psd-row .vela-icon { flex: none; color: var(--vela-fg-bright); }
|
|
5137
|
+
.vela-psd-section {
|
|
5138
|
+
padding: 12px 2px 4px;
|
|
5139
|
+
font-size: 12px;
|
|
5140
|
+
font-weight: 600;
|
|
5141
|
+
letter-spacing: 0.4px;
|
|
5142
|
+
text-transform: uppercase;
|
|
5143
|
+
color: var(--vela-fg-muted);
|
|
5144
|
+
}
|
|
5145
|
+
`;
|
|
5146
|
+
var PriceScaleDrawer = class {
|
|
5147
|
+
constructor(opts) {
|
|
5148
|
+
this.opts = opts;
|
|
5149
|
+
this.pane = null;
|
|
5150
|
+
injectStyles(STYLE_ID17, CSS17, opts.host.ownerDocument);
|
|
5151
|
+
this.drawer = new Drawer({ host: opts.host, title: "Price scale", onOpenChange: opts.onOpenChange });
|
|
5152
|
+
}
|
|
5153
|
+
open() {
|
|
5154
|
+
const chart = this.opts.chart();
|
|
5155
|
+
const panes = chart?.renderer.get("paneScales");
|
|
5156
|
+
this.pane = Array.isArray(panes) ? paneScaleAt(panes, this.opts.pressY()) : null;
|
|
5157
|
+
this.render();
|
|
5158
|
+
this.drawer.show();
|
|
5159
|
+
}
|
|
5160
|
+
close() {
|
|
5161
|
+
this.drawer.hide();
|
|
5162
|
+
}
|
|
5163
|
+
destroy() {
|
|
5164
|
+
this.drawer.destroy();
|
|
5165
|
+
}
|
|
5166
|
+
flag(feature) {
|
|
5167
|
+
return Boolean(this.opts.chart()?.renderer.get(feature));
|
|
5168
|
+
}
|
|
5169
|
+
render() {
|
|
5170
|
+
const chart = this.opts.chart();
|
|
5171
|
+
const doc = this.drawer.body.ownerDocument;
|
|
5172
|
+
this.drawer.body.replaceChildren();
|
|
5173
|
+
if (!chart) return;
|
|
5174
|
+
const pane = this.pane;
|
|
5175
|
+
const items = priceAxisItems({
|
|
5176
|
+
auto: chart.renderer.get("autoScale") !== false,
|
|
5177
|
+
invert: pane ? pane.invert : this.flag("invertScale"),
|
|
5178
|
+
choice: scaleChoiceOf(pane ?? { mode: String(chart.renderer.get("scaleMode") ?? "price"), log: this.flag("logScale") }),
|
|
5179
|
+
axisLabels: this.flag("axisLabels"),
|
|
5180
|
+
priceLabel: this.flag("priceLabel"),
|
|
5181
|
+
countdown: this.flag("countdown"),
|
|
5182
|
+
priceLine: this.flag("currentPriceLine")
|
|
5183
|
+
});
|
|
5184
|
+
const list = doc.createElement("div");
|
|
5185
|
+
list.className = "vela-psd-list";
|
|
5186
|
+
const row = (label, opts) => {
|
|
5187
|
+
const el = doc.createElement("div");
|
|
5188
|
+
el.className = "vela-psd-row";
|
|
5189
|
+
if (opts.sep) el.dataset.sep = "1";
|
|
5190
|
+
const text = doc.createElement("span");
|
|
5191
|
+
text.className = "vela-psd-row-label";
|
|
5192
|
+
text.textContent = label;
|
|
5193
|
+
el.appendChild(text);
|
|
5194
|
+
if (opts.checked) el.appendChild(iconEl("check", doc));
|
|
5195
|
+
el.addEventListener("click", opts.onClick);
|
|
5196
|
+
list.appendChild(el);
|
|
5197
|
+
};
|
|
5198
|
+
const section = (label) => {
|
|
5199
|
+
const el = doc.createElement("div");
|
|
5200
|
+
el.className = "vela-psd-section";
|
|
5201
|
+
el.textContent = label;
|
|
5202
|
+
list.appendChild(el);
|
|
5203
|
+
};
|
|
5204
|
+
for (const item of items) {
|
|
5205
|
+
if (item.id === "labels" && item.submenu) {
|
|
5206
|
+
section("Labels");
|
|
5207
|
+
for (const sub of item.submenu) {
|
|
5208
|
+
row(sub.label, {
|
|
5209
|
+
checked: sub.checked,
|
|
5210
|
+
onClick: () => {
|
|
5211
|
+
const feature = sub.id.slice("toggle:".length);
|
|
5212
|
+
chart.renderer.set(feature, !this.flag(feature));
|
|
5213
|
+
this.render();
|
|
5214
|
+
}
|
|
5215
|
+
});
|
|
5216
|
+
}
|
|
5217
|
+
continue;
|
|
5218
|
+
}
|
|
5219
|
+
if (item.id === "levels" && item.submenu) {
|
|
5220
|
+
section("Levels");
|
|
5221
|
+
for (const sub of item.submenu) {
|
|
5222
|
+
row(sub.label, {
|
|
5223
|
+
checked: sub.checked,
|
|
5224
|
+
onClick: () => {
|
|
5225
|
+
const feature = sub.id.slice("toggle:".length);
|
|
5226
|
+
chart.renderer.set(feature, !this.flag(feature));
|
|
5227
|
+
this.render();
|
|
5228
|
+
}
|
|
5229
|
+
});
|
|
5230
|
+
}
|
|
5231
|
+
continue;
|
|
5232
|
+
}
|
|
5233
|
+
if (item.id.startsWith("settings")) {
|
|
5234
|
+
row(item.label, {
|
|
5235
|
+
sep: true,
|
|
5236
|
+
onClick: () => {
|
|
5237
|
+
chart.renderer.openSettings(settingsSectionOf(item.id));
|
|
5238
|
+
this.drawer.hide();
|
|
5239
|
+
}
|
|
5240
|
+
});
|
|
5241
|
+
continue;
|
|
5242
|
+
}
|
|
5243
|
+
if (item.id === "auto") {
|
|
5244
|
+
row(item.label, {
|
|
5245
|
+
checked: item.checked,
|
|
5246
|
+
onClick: () => {
|
|
5247
|
+
chart.renderer.set("autoScale", chart.renderer.get("autoScale") === false);
|
|
5248
|
+
this.render();
|
|
5249
|
+
}
|
|
5250
|
+
});
|
|
5251
|
+
continue;
|
|
5252
|
+
}
|
|
5253
|
+
if (item.id === "invert") {
|
|
5254
|
+
row(item.label, {
|
|
5255
|
+
checked: item.checked,
|
|
5256
|
+
onClick: () => {
|
|
5257
|
+
const [feature, value] = invertWrite(!(pane ? pane.invert : this.flag("invertScale")), pane);
|
|
5258
|
+
chart.renderer.set(feature, value);
|
|
5259
|
+
const panes = chart.renderer.get("paneScales");
|
|
5260
|
+
this.pane = Array.isArray(panes) ? paneScaleAt(panes, this.opts.pressY()) : pane;
|
|
5261
|
+
this.render();
|
|
5262
|
+
}
|
|
5263
|
+
});
|
|
5264
|
+
continue;
|
|
5265
|
+
}
|
|
5266
|
+
if (item.id.startsWith("scale:")) {
|
|
5267
|
+
row(item.label, {
|
|
5268
|
+
checked: item.checked,
|
|
5269
|
+
sep: item.separatorBefore,
|
|
5270
|
+
onClick: () => {
|
|
5271
|
+
for (const [feature, value] of scaleWrites(item.id.slice("scale:".length), pane)) {
|
|
5272
|
+
chart.renderer.set(feature, value);
|
|
5273
|
+
}
|
|
5274
|
+
const panes = chart.renderer.get("paneScales");
|
|
5275
|
+
this.pane = Array.isArray(panes) ? paneScaleAt(panes, this.opts.pressY()) : pane;
|
|
5276
|
+
this.render();
|
|
5277
|
+
}
|
|
5278
|
+
});
|
|
5279
|
+
}
|
|
5280
|
+
}
|
|
5281
|
+
this.drawer.body.appendChild(list);
|
|
5282
|
+
}
|
|
5283
|
+
};
|
|
5284
|
+
|
|
5285
|
+
// src/widget/drawing-pill.ts
|
|
5286
|
+
var STYLE_ID18 = "vela-widget-drawing-pill";
|
|
5287
|
+
var CSS18 = `
|
|
5288
|
+
.vela-drawpill { display: none; }
|
|
5289
|
+
[data-layout='mobile'] .vela-drawpill {
|
|
5290
|
+
position: absolute;
|
|
5291
|
+
left: 50%;
|
|
5292
|
+
bottom: 10px;
|
|
5293
|
+
transform: translateX(-50%);
|
|
5294
|
+
z-index: 7;
|
|
5295
|
+
display: flex;
|
|
5296
|
+
align-items: center;
|
|
5297
|
+
gap: 2px;
|
|
5298
|
+
padding: 4px;
|
|
5299
|
+
border: 1px solid var(--vela-border-strong);
|
|
5300
|
+
border-radius: 999px;
|
|
5301
|
+
background: var(--vela-surface);
|
|
5302
|
+
box-shadow: var(--vela-shadow-dialog);
|
|
5303
|
+
color: var(--vela-fg);
|
|
5304
|
+
}
|
|
5305
|
+
[data-layout='mobile'] .vela-drawpill[hidden] { display: none !important; }
|
|
5306
|
+
.vela-drawpill-tool {
|
|
5307
|
+
display: inline-flex;
|
|
5308
|
+
align-items: center;
|
|
5309
|
+
justify-content: center;
|
|
5310
|
+
width: 36px;
|
|
5311
|
+
height: 36px;
|
|
5312
|
+
border-radius: 999px;
|
|
5313
|
+
background: var(--vela-hover);
|
|
5314
|
+
color: var(--vela-accent);
|
|
5315
|
+
}
|
|
5316
|
+
.vela-drawpill-tool svg { width: 22px; height: 22px; }
|
|
5317
|
+
.vela-drawpill-btn {
|
|
5318
|
+
all: unset;
|
|
5319
|
+
width: 36px;
|
|
5320
|
+
height: 36px;
|
|
5321
|
+
display: inline-flex;
|
|
5322
|
+
align-items: center;
|
|
5323
|
+
justify-content: center;
|
|
5324
|
+
border-radius: 999px;
|
|
5325
|
+
color: var(--vela-fg-muted);
|
|
5326
|
+
cursor: pointer;
|
|
5327
|
+
-webkit-tap-highlight-color: transparent;
|
|
5328
|
+
position: relative;
|
|
5329
|
+
}
|
|
5330
|
+
.vela-drawpill-btn:active { background: var(--vela-hover); }
|
|
5331
|
+
.vela-drawpill-btn[data-on='1'] { color: var(--vela-accent); background: var(--vela-hover); }
|
|
5332
|
+
.vela-drawpill-btn .vela-icon { font-size: 17px; width: 17px; height: 17px; }
|
|
5333
|
+
.vela-drawpill-badge {
|
|
5334
|
+
position: absolute;
|
|
5335
|
+
top: 2px;
|
|
5336
|
+
right: 2px;
|
|
5337
|
+
font-size: 8px;
|
|
5338
|
+
font-weight: 700;
|
|
5339
|
+
color: var(--vela-accent);
|
|
5340
|
+
}
|
|
5341
|
+
`;
|
|
5342
|
+
var DrawingPill = class {
|
|
5343
|
+
constructor(host) {
|
|
5344
|
+
this.chart = null;
|
|
5345
|
+
this.chartSubs = [];
|
|
5346
|
+
const doc = host.ownerDocument;
|
|
5347
|
+
injectStyles(STYLE_ID18, CSS18, doc);
|
|
5348
|
+
this.el = doc.createElement("div");
|
|
5349
|
+
this.el.className = "vela-drawpill";
|
|
5350
|
+
this.el.hidden = true;
|
|
5351
|
+
this.toolGlyph = doc.createElement("span");
|
|
5352
|
+
this.toolGlyph.className = "vela-drawpill-tool";
|
|
5353
|
+
const btn = (icon2, label, onClick) => {
|
|
5354
|
+
const b = doc.createElement("button");
|
|
5355
|
+
b.className = "vela-drawpill-btn";
|
|
5356
|
+
b.setAttribute("aria-label", label);
|
|
5357
|
+
b.appendChild(iconEl(icon2, doc));
|
|
5358
|
+
b.addEventListener("click", onClick);
|
|
5359
|
+
return b;
|
|
5360
|
+
};
|
|
5361
|
+
this.magnetBtn = btn("magnet", "Magnet snap", () => {
|
|
5362
|
+
const order = ["off", "weak", "strong"];
|
|
5363
|
+
const cur = this.chart?.drawings.getSnapMode() ?? "off";
|
|
5364
|
+
this.chart?.drawings.setSnapMode(order[(order.indexOf(cur) + 1) % order.length]);
|
|
5365
|
+
});
|
|
5366
|
+
this.magnetBadge = doc.createElement("span");
|
|
5367
|
+
this.magnetBadge.className = "vela-drawpill-badge";
|
|
5368
|
+
this.magnetBtn.appendChild(this.magnetBadge);
|
|
5369
|
+
this.stayBtn = btn(iconMarkup("pen-lock") ? "pen-lock" : "pen", "Stay in drawing mode", () => {
|
|
5370
|
+
this.chart?.drawings.setStayMode(!this.chart.drawings.getStayMode());
|
|
5371
|
+
});
|
|
5372
|
+
this.eraserBtn = btn("eraser", "Eraser", () => {
|
|
5373
|
+
this.chart?.drawings.setMode(this.chart.drawings.getMode() === "eraser" ? null : "eraser");
|
|
5374
|
+
});
|
|
5375
|
+
const close = btn("close", "Exit drawing mode", () => {
|
|
5376
|
+
this.chart?.drawings.setTool(null);
|
|
5377
|
+
this.chart?.drawings.setMode(null);
|
|
5378
|
+
});
|
|
5379
|
+
this.el.append(this.toolGlyph, this.magnetBtn, this.stayBtn, this.eraserBtn, close);
|
|
5380
|
+
host.appendChild(this.el);
|
|
5381
|
+
}
|
|
5382
|
+
/** Rebind to another chart (a rebuild, or the workspace's active cell changing) —
|
|
5383
|
+
* the previous chart's subscriptions are dropped so only one chart drives the pill. */
|
|
5384
|
+
onChart(chart) {
|
|
5385
|
+
for (const off of this.chartSubs) off();
|
|
5386
|
+
this.chart = chart;
|
|
5387
|
+
this.chartSubs = [
|
|
5388
|
+
chart.on("drawing:tool", () => this.sync()),
|
|
5389
|
+
chart.on("drawing:snap", () => this.sync()),
|
|
5390
|
+
chart.on("drawing:stay", () => this.sync()),
|
|
5391
|
+
chart.on("drawing:mode", () => this.sync())
|
|
5392
|
+
];
|
|
5393
|
+
this.sync();
|
|
5394
|
+
}
|
|
5395
|
+
sync() {
|
|
5396
|
+
const drawings = this.chart?.drawings;
|
|
5397
|
+
if (!drawings || !drawings.supported) {
|
|
5398
|
+
this.el.hidden = true;
|
|
5399
|
+
return;
|
|
5400
|
+
}
|
|
5401
|
+
const tool = drawings.getTool();
|
|
5402
|
+
const eraser = drawings.getMode() === "eraser";
|
|
5403
|
+
this.el.hidden = tool === null && !eraser;
|
|
5404
|
+
if (this.el.hidden) return;
|
|
5405
|
+
this.toolGlyph.innerHTML = tool !== null ? getDrawingType(tool)?.icon ?? "" : iconMarkup("eraser") ?? "";
|
|
5406
|
+
const snap = drawings.getSnapMode();
|
|
5407
|
+
this.magnetBtn.dataset.on = snap !== "off" ? "1" : "";
|
|
5408
|
+
this.magnetBadge.textContent = snap === "weak" ? "W" : snap === "strong" ? "S" : "";
|
|
5409
|
+
this.stayBtn.dataset.on = drawings.getStayMode() ? "1" : "";
|
|
5410
|
+
this.eraserBtn.dataset.on = eraser ? "1" : "";
|
|
5411
|
+
}
|
|
5412
|
+
destroy() {
|
|
5413
|
+
for (const off of this.chartSubs) off();
|
|
5414
|
+
this.chartSubs = [];
|
|
5415
|
+
this.el.remove();
|
|
5416
|
+
}
|
|
5417
|
+
};
|
|
5418
|
+
|
|
5419
|
+
// src/widget/glide.ts
|
|
5420
|
+
var MIN_SPAN_MS = 6e4;
|
|
5421
|
+
var FOLLOW = 0.25;
|
|
5422
|
+
var ZOOM_IN = 0.8;
|
|
5423
|
+
var ZOOM_OUT = 1.25;
|
|
5424
|
+
var PAN_FAST = 0.2;
|
|
5425
|
+
function zoomTarget(base, factor) {
|
|
5426
|
+
const newSpan = Math.max(MIN_SPAN_MS, (base.to - base.from) * factor);
|
|
5427
|
+
return { from: base.to - newSpan, to: base.to };
|
|
5428
|
+
}
|
|
5429
|
+
function followStep(cur, target, follow = FOLLOW, epsFrac = 15e-4) {
|
|
5430
|
+
const eps = Math.max(MIN_SPAN_MS, (target.to - target.from) * epsFrac);
|
|
5431
|
+
const nf = cur.from + (target.from - cur.from) * follow;
|
|
5432
|
+
const nt = cur.to + (target.to - cur.to) * follow;
|
|
5433
|
+
const done = Math.abs(target.from - nf) <= eps && Math.abs(target.to - nt) <= eps;
|
|
5434
|
+
return { cur: done ? { ...target } : { from: nf, to: nt }, done };
|
|
5435
|
+
}
|
|
5436
|
+
var Glider = class {
|
|
5437
|
+
constructor(chart) {
|
|
5438
|
+
this.chart = chart;
|
|
5439
|
+
// The glide eases ITS OWN range, never the chart's read-back: getVisibleRange()
|
|
5440
|
+
// is data-bounded (whole bars, `to` capped at the newest bar) and the renderer
|
|
5441
|
+
// clamps what gets applied — chasing that read-back can never converge on a
|
|
5442
|
+
// clamped step, leaving the rAF loop re-applying a stale target forever (and
|
|
5443
|
+
// grinding the span down when panning past the newest bar). The internal range
|
|
5444
|
+
// converges geometrically, so the glide always terminates; the renderer is free
|
|
5445
|
+
// to clamp each applied frame.
|
|
5446
|
+
this.cur = null;
|
|
5447
|
+
this.target = null;
|
|
5448
|
+
this.raf = 0;
|
|
5449
|
+
}
|
|
5450
|
+
zoom(factor) {
|
|
5451
|
+
this.to((base) => zoomTarget(base, factor));
|
|
5452
|
+
}
|
|
5453
|
+
stop() {
|
|
5454
|
+
if (this.raf) cancelAnimationFrame(this.raf);
|
|
5455
|
+
this.raf = 0;
|
|
5456
|
+
this.cur = null;
|
|
5457
|
+
this.target = null;
|
|
5458
|
+
}
|
|
5459
|
+
to(make) {
|
|
5460
|
+
const chart = this.chart();
|
|
5461
|
+
const base = this.target ?? chart?.getVisibleRange();
|
|
5462
|
+
if (!chart || !base) return;
|
|
5463
|
+
this.cur ?? (this.cur = { ...base });
|
|
5464
|
+
this.target = make(base);
|
|
5465
|
+
if (!this.raf) this.tick();
|
|
5466
|
+
}
|
|
5467
|
+
tick() {
|
|
5468
|
+
this.raf = requestAnimationFrame(() => {
|
|
5469
|
+
const chart = this.chart();
|
|
5470
|
+
if (!chart || !this.cur || !this.target) {
|
|
5471
|
+
this.stop();
|
|
5472
|
+
return;
|
|
5473
|
+
}
|
|
5474
|
+
const { cur: next, done } = followStep(this.cur, this.target);
|
|
5475
|
+
this.cur = next;
|
|
5476
|
+
chart.setVisibleRange(next);
|
|
5477
|
+
if (done) this.stop();
|
|
5478
|
+
else this.tick();
|
|
5479
|
+
});
|
|
5480
|
+
}
|
|
5481
|
+
};
|
|
5482
|
+
|
|
5483
|
+
// src/widget/tool-shortcuts.ts
|
|
5484
|
+
var TOOL_BINDINGS = [
|
|
5485
|
+
["trendline", "drawings.trendline"],
|
|
5486
|
+
["hline", "drawings.hline-cursor"],
|
|
5487
|
+
["vline", "drawings.vline-cursor"]
|
|
5488
|
+
];
|
|
5489
|
+
function toolShortcutHints(keymap) {
|
|
5490
|
+
const byId = new Map(keymap.bindings().map((b) => [b.id, b.display[0]]));
|
|
5491
|
+
const out = {};
|
|
5492
|
+
for (const [type, id] of TOOL_BINDINGS) {
|
|
5493
|
+
const display = byId.get(id);
|
|
5494
|
+
if (display) out[type] = display;
|
|
5495
|
+
}
|
|
5496
|
+
return out;
|
|
5497
|
+
}
|
|
5498
|
+
|
|
5499
|
+
// src/widget/history.ts
|
|
3843
5500
|
var WidgetHistory = class {
|
|
3844
5501
|
/** `getChart` late-resolves the CURRENT chart: drawing steps recorded before a chart
|
|
3845
5502
|
* rebuild must undo on the chart that exists when the user presses Ctrl+Z, not on a
|
|
@@ -3920,8 +5577,8 @@ var WidgetHistory = class {
|
|
|
3920
5577
|
};
|
|
3921
5578
|
|
|
3922
5579
|
// src/widget/toast.ts
|
|
3923
|
-
var
|
|
3924
|
-
var
|
|
5580
|
+
var STYLE_ID19 = "vela-widget-toast";
|
|
5581
|
+
var CSS19 = `
|
|
3925
5582
|
.vela-toast {
|
|
3926
5583
|
position: absolute;
|
|
3927
5584
|
left: 50%;
|
|
@@ -3951,7 +5608,7 @@ var CSS11 = `
|
|
|
3951
5608
|
var Toast = class {
|
|
3952
5609
|
constructor(host) {
|
|
3953
5610
|
this.timer = null;
|
|
3954
|
-
injectStyles(
|
|
5611
|
+
injectStyles(STYLE_ID19, CSS19, host.ownerDocument);
|
|
3955
5612
|
this.el = host.ownerDocument.createElement("div");
|
|
3956
5613
|
this.el.className = "vela-toast";
|
|
3957
5614
|
host.appendChild(this.el);
|
|
@@ -3975,4 +5632,4 @@ var Toast = class {
|
|
|
3975
5632
|
}
|
|
3976
5633
|
};
|
|
3977
5634
|
|
|
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 };
|
|
5635
|
+
export { Bottombar, ChartContextMenu, DataWindow, DrawingPill, DrawingsDrawer, Glider, IndicatorPicker, LayoutModeController, MobileBar, MoreDrawer, ObjectTree, PAN_FAST, PanelDock, PriceScaleDrawer, RANGE_PRESETS, ShortcutsHelp, Statusline, SymbolPicker, TimeframeDrawer, TimeframeQuick, TimezoneDrawer, Toast, Topbar, Watermark, WidgetHistory, ZOOM_IN, ZOOM_OUT, dataWindowSections, decimalsFor, decodeState, encodeState, filterSymbols, fmtChange, fmtPrice, indicatorLedger, legacyWidgetState, loadPersisted, localStorageAdapter, parsePersisted, parseTimeframe, prefixedSymbol, priceStyleIcon, priceStyleLabel, resolveIndicators, sanitizeState, savePersisted, statuslineInkOf, timeframeLabel, timeframeMs, toolShortcutHints };
|