@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.
Files changed (45) hide show
  1. package/dist/{DataProvider-Dzd-Erlk.d.ts → DataProvider-DTOEXKTI.d.ts} +1 -1
  2. package/dist/{DataProvider-DKNDHpNv.d.cts → DataProvider-gnzv-dgc.d.cts} +1 -1
  3. package/dist/{chunk-KM6LHB3Y.js → chunk-ATPU5CI6.js} +6 -1
  4. package/dist/{chunk-7UFX5ZIG.js → chunk-B5TIKQJ5.js} +65 -7
  5. package/dist/{chunk-GYD2THPV.js → chunk-LJLZR44Y.js} +9 -2
  6. package/dist/{chunk-RHIDOUFL.js → chunk-P556H6EA.js} +329 -10
  7. package/dist/{chunk-KCCZNKH7.js → chunk-RRFGWZNJ.js} +1915 -367
  8. package/dist/{chunk-OVQKKXLZ.js → chunk-Y7TK4ZWK.js} +1810 -153
  9. package/dist/{contributions-o1GRKPI_.d.cts → contributions-Ci_i3IN2.d.cts} +77 -31
  10. package/dist/{contributions-hX3EUyjG.d.ts → contributions-b6AVrqqG.d.ts} +77 -31
  11. package/dist/{history-LJkz4-sS.d.cts → history-Cf1lVsap.d.cts} +22 -5
  12. package/dist/{history-Dzxz-MQj.d.ts → history-Ci3M0xLV.d.ts} +22 -5
  13. package/dist/index.cjs +2010 -397
  14. package/dist/index.d.cts +83 -25
  15. package/dist/index.d.ts +83 -25
  16. package/dist/index.js +4 -4
  17. package/dist/{options-Q-576hIi.d.cts → options-XTBpbx0s.d.cts} +61 -1
  18. package/dist/{options-Q-576hIi.d.ts → options-XTBpbx0s.d.ts} +61 -1
  19. package/dist/{plugin-aGUD1epn.d.ts → plugin-CnNd7cuF.d.ts} +305 -8
  20. package/dist/{plugin-D94muTV-.d.cts → plugin-DfH4Y-Om.d.cts} +305 -8
  21. package/dist/plugin.cjs +58 -7
  22. package/dist/plugin.d.cts +4 -4
  23. package/dist/plugin.d.ts +4 -4
  24. package/dist/plugin.js +2 -2
  25. package/dist/providers/binance.d.cts +2 -2
  26. package/dist/providers/binance.d.ts +2 -2
  27. package/dist/providers/coinbase.d.cts +2 -2
  28. package/dist/providers/coinbase.d.ts +2 -2
  29. package/dist/providers/hyperliquid.d.cts +2 -2
  30. package/dist/providers/hyperliquid.d.ts +2 -2
  31. package/dist/ui.cjs +342 -9
  32. package/dist/ui.d.cts +64 -2
  33. package/dist/ui.d.ts +64 -2
  34. package/dist/ui.js +3 -3
  35. package/dist/vela.global.js +2010 -397
  36. package/dist/vela.global.min.js +115 -34
  37. package/dist/widget.cjs +5987 -2242
  38. package/dist/widget.d.cts +89 -17
  39. package/dist/widget.d.ts +89 -17
  40. package/dist/widget.js +169 -18
  41. package/dist/workspace.cjs +5627 -1587
  42. package/dist/workspace.d.cts +106 -12
  43. package/dist/workspace.d.ts +106 -12
  44. package/dist/workspace.js +494 -52
  45. package/package.json +1 -1
@@ -1,7 +1,7 @@
1
- import { parseSymbol, priceStyleIds, tzButtonLabel, TIMEZONES, normalizeTimezone, tzMenuLabel } from './chunk-KCCZNKH7.js';
2
- import { chartType, widgetActions, SidePanel, sidePanels, DEFAULT_PANEL_ORDER, getDrawingType } from './chunk-7UFX5ZIG.js';
3
- import { Tooltip, Menu, Dialog } from './chunk-RHIDOUFL.js';
4
- import { injectStyles, iconEl, iconMarkup, SESSION_PRE, SESSION_POST, SESSION_OFF, icon, registerIcon, categoricalColor } from './chunk-GYD2THPV.js';
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/topbar.ts
91
- var STYLE_ID = "vela-topbar";
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.layoutMenu = null;
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(STYLE_ID, CSS, doc);
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
- const indicatorsBtn = doc.createElement("button");
238
- indicatorsBtn.className = "vela-widget-indicators";
239
- indicatorsBtn.append(iconEl("indicators", doc), doc.createTextNode("Indicators"));
240
- if (opts.onIndicatorsClick) indicatorsBtn.addEventListener("click", opts.onIndicatorsClick);
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
- this.el.append(...leading, indicatorsBtn, sep(), this.undoBtn, this.redoBtn, this.actionsHost, this.alertsBtn, this.panelsHost, screenshotBtn);
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
- this.layoutMenu = new Menu({
619
+ const layout = opts.layout;
620
+ this.layoutPicker = new LayoutPicker({
282
621
  trigger: this.layoutButton,
283
- triggerId: "vela-topbar-layout",
284
622
  host,
285
- items: this.layoutItems(),
286
- onSelect: (id) => {
287
- if (id.startsWith("toggle:")) {
288
- opts.layout.onToggle?.(id.slice("toggle:".length));
289
- this.layoutMenu?.setItems(this.layoutItems());
290
- return;
291
- }
292
- opts.layout.onSelect(id);
293
- }
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.layoutMenu?.setItems(this.layoutItems());
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.layoutMenu?.destroy();
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 STYLE_ID2 = "vela-widget-statusline";
513
- var CSS2 = `
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(STYLE_ID2, CSS2, doc);
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.symbolEl.style.display = this.parts.name ? "" : "none";
708
- this.marketEl.style.display = this.parts.market ? "" : "none";
709
- this.ohlcEl.style.display = this.parts.ohlc ? "" : "none";
710
- this.changeEl.style.display = this.parts.change ? "" : "none";
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 STYLE_ID3 = "vela-widget-watermark";
772
- var CSS3 = `
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
- inset: 0;
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: clamp(28px, 7vw, 72px);
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
- injectStyles(STYLE_ID3, CSS3, host.ownerDocument);
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.el.style.display = visible ? "" : "none";
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.el.textContent = symbol ? `${parseSymbol(symbol).ticker} \xB7 ${timeframeLabel(timeframe)}` : "";
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 STYLE_ID4 = "vela-widget-bottombar";
831
- var CSS4 = `
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(STYLE_ID4, CSS4, doc);
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 STYLE_ID5 = "vela-widget-symbolpicker";
1045
- var CSS5 = `
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(STYLE_ID5, CSS5, doc);
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 STYLE_ID6 = "vela-widget-objtree";
1458
- var CSS6 = `
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(STYLE_ID6, CSS6, host.ownerDocument);
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 STYLE_ID7 = "vela-widget-datawindow";
2676
- var CSS7 = `
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(STYLE_ID7, CSS7, host.ownerDocument);
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 STYLE_ID8 = "vela-widget-shortcuts";
2933
- var CSS8 = `
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(STYLE_ID8, CSS8, doc);
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 STYLE_ID9 = "vela-widget-indpicker";
3247
- var CSS9 = `
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(STYLE_ID9, CSS9, doc);
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 STYLE_ID10 = "vela-widget-tfquick";
3450
- var CSS10 = `
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(STYLE_ID10, CSS10, doc);
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/glide.ts
3763
- var MIN_SPAN_MS = 6e4;
3764
- var FOLLOW = 0.25;
3765
- var ZOOM_IN = 0.8;
3766
- var ZOOM_OUT = 1.25;
3767
- var PAN_FAST = 0.2;
3768
- function zoomTarget(base, factor) {
3769
- const newSpan = Math.max(MIN_SPAN_MS, (base.to - base.from) * factor);
3770
- return { from: base.to - newSpan, to: base.to };
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
- function followStep(cur, target, follow = FOLLOW, epsFrac = 15e-4) {
3773
- const eps = Math.max(MIN_SPAN_MS, (target.to - target.from) * epsFrac);
3774
- const nf = cur.from + (target.from - cur.from) * follow;
3775
- const nt = cur.to + (target.to - cur.to) * follow;
3776
- const done = Math.abs(target.from - nf) <= eps && Math.abs(target.to - nt) <= eps;
3777
- return { cur: done ? { ...target } : { from: nf, to: nt }, done };
3778
- }
3779
- var Glider = class {
3780
- constructor(chart) {
3781
- this.chart = chart;
3782
- // The glide eases ITS OWN range, never the chart's read-back: getVisibleRange()
3783
- // is data-bounded (whole bars, `to` capped at the newest bar) and the renderer
3784
- // clamps what gets applied — chasing that read-back can never converge on a
3785
- // clamped step, leaving the rAF loop re-applying a stale target forever (and
3786
- // grinding the span down when panning past the newest bar). The internal range
3787
- // converges geometrically, so the glide always terminates; the renderer is free
3788
- // to clamp each applied frame.
3789
- this.cur = null;
3790
- this.target = null;
3791
- this.raf = 0;
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
- zoom(factor) {
3794
- this.to((base) => zoomTarget(base, factor));
4299
+ get current() {
4300
+ return this.mode;
3795
4301
  }
3796
- stop() {
3797
- if (this.raf) cancelAnimationFrame(this.raf);
3798
- this.raf = 0;
3799
- this.cur = null;
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
- to(make) {
3803
- const chart = this.chart();
3804
- const base = this.target ?? chart?.getVisibleRange();
3805
- if (!chart || !base) return;
3806
- this.cur ?? (this.cur = { ...base });
3807
- this.target = make(base);
3808
- if (!this.raf) this.tick();
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
- tick() {
3811
- this.raf = requestAnimationFrame(() => {
3812
- const chart = this.chart();
3813
- if (!chart || !this.cur || !this.target) {
3814
- this.stop();
3815
- return;
3816
- }
3817
- const { cur: next, done } = followStep(this.cur, this.target);
3818
- this.cur = next;
3819
- chart.setVisibleRange(next);
3820
- if (done) this.stop();
3821
- else this.tick();
3822
- });
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/tool-shortcuts.ts
3827
- var TOOL_BINDINGS = [
3828
- ["trendline", "drawings.trendline"],
3829
- ["hline", "drawings.hline-cursor"],
3830
- ["vline", "drawings.vline-cursor"]
3831
- ];
3832
- function toolShortcutHints(keymap) {
3833
- const byId = new Map(keymap.bindings().map((b) => [b.id, b.display[0]]));
3834
- const out = {};
3835
- for (const [type, id] of TOOL_BINDINGS) {
3836
- const display = byId.get(id);
3837
- if (display) out[type] = display;
3838
- }
3839
- return out;
3840
- }
3841
-
3842
- // src/widget/history.ts
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 STYLE_ID11 = "vela-widget-toast";
3924
- var CSS11 = `
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(STYLE_ID11, CSS11, host.ownerDocument);
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 };