@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
package/dist/workspace.js CHANGED
@@ -1,10 +1,10 @@
1
- import { WidgetHistory, prefixedSymbol, Watermark, Statusline, ChartContextMenu, statuslineInkOf, indicatorLedger, Glider, localStorageAdapter, decodeState, SymbolPicker, IndicatorPicker, TimeframeQuick, Topbar, PanelDock, ObjectTree, DataWindow, Toast, Bottombar, toolShortcutHints, resolveIndicators, sanitizeState, encodeState, ZOOM_IN, ZOOM_OUT, PAN_FAST, ShortcutsHelp } from './chunk-OVQKKXLZ.js';
2
- export { decodeState, encodeState, sanitizeState } from './chunk-OVQKKXLZ.js';
3
- import { parseSymbol, Vela, normalizeTimezone, TypedEventBus, MultiProviderFeed, resolveTheme, createCustomMark, createAttributionMark, DrawingToolbar, defaultToolbar, applyAttributionMarkTheme, sharedBarStore, timeframeToMs } from './chunk-KCCZNKH7.js';
4
- import { resolveEngines, legendActionsProviderFor, rendererDefaults, widgetAttachments } from './chunk-7UFX5ZIG.js';
5
- import { applyPlotOverlayTokens, ensureUIHost, KeymapManager, Menu, isEditableTarget } from './chunk-RHIDOUFL.js';
6
- import './chunk-KM6LHB3Y.js';
7
- import { registerIcon, svg16, injectStyles } from './chunk-GYD2THPV.js';
1
+ import { WidgetHistory, prefixedSymbol, Watermark, Statusline, ChartContextMenu, statuslineInkOf, indicatorLedger, Glider, localStorageAdapter, decodeState, SymbolPicker, IndicatorPicker, TimeframeQuick, Topbar, PanelDock, ObjectTree, DataWindow, Toast, Bottombar, MobileBar, DrawingPill, LayoutModeController, toolShortcutHints, resolveIndicators, sanitizeState, encodeState, TimeframeDrawer, RANGE_PRESETS, DrawingsDrawer, MoreDrawer, priceStyleIcon, priceStyleLabel, TimezoneDrawer, PriceScaleDrawer, ZOOM_IN, ZOOM_OUT, PAN_FAST, ShortcutsHelp } from './chunk-Y7TK4ZWK.js';
2
+ export { decodeState, encodeState, sanitizeState } from './chunk-Y7TK4ZWK.js';
3
+ import { parseSymbol, Vela, normalizeTimezone, TypedEventBus, MultiProviderFeed, resolveTheme, createCustomMark, createAttributionMark, DrawingToolbar, defaultToolbar, applyAttributionMarkTheme, sharedBarStore, timeframeToMs, priceStyleIds } from './chunk-RRFGWZNJ.js';
4
+ import { resolveEngines, legendActionsProviderFor, rendererDefaults, widgetActions, widgetAttachments } from './chunk-B5TIKQJ5.js';
5
+ import { applyPlotOverlayTokens, ensureUIHost, KeymapManager, Menu, isEditableTarget } from './chunk-P556H6EA.js';
6
+ import './chunk-ATPU5CI6.js';
7
+ import { registerIcon, svg16, injectStyles } from './chunk-LJLZR44Y.js';
8
8
 
9
9
  // src/workspace/sync.ts
10
10
  function syncTargets(originId, setting, cellIds) {
@@ -81,6 +81,8 @@ var ChartCell = class {
81
81
  this.pendingRange = null;
82
82
  /** Indicator titles (this cell's in-chart legend rows) shown. */
83
83
  this.indicatorTitlesOn = true;
84
+ /** Plot values beside this cell's legend titles shown. */
85
+ this.indicatorValuesOn = true;
84
86
  this.destroyed = false;
85
87
  this.appTheme = deps.theme;
86
88
  const symbol = prefixedSymbol(seed);
@@ -145,10 +147,14 @@ var ChartCell = class {
145
147
  this.inner.on("load:end", () => {
146
148
  this.volumeMayBePending = false;
147
149
  });
150
+ this.inner.on("load:start", () => this.watermark?.setLoading(true));
151
+ this.inner.on("load:end", () => this.watermark?.setLoading(false));
148
152
  const tz = deps.timezone();
149
153
  if (tz !== "Etc/UTC") this.inner.renderer.set("timezone", tz);
150
154
  this.indicatorTitlesOn = seed.indicatorTitles ?? true;
151
155
  if (!this.indicatorTitlesOn) this.inner.renderer.set("indicatorTitles", false);
156
+ this.indicatorValuesOn = seed.indicatorValues ?? true;
157
+ if (!this.indicatorValuesOn) this.inner.renderer.set("indicatorValues", false);
152
158
  this.watermarkOn = seed.watermark ?? deps.watermark;
153
159
  this.watermark = deps.watermark ? new Watermark(this.host, symbol ?? "", seed.timeframe ?? "60") : null;
154
160
  if (!this.watermarkOn) this.watermark?.setVisible(false);
@@ -256,6 +262,12 @@ var ChartCell = class {
256
262
  label: "Titles",
257
263
  get: () => this.indicatorTitlesOn,
258
264
  set: (v) => this.setIndicatorTitlesVisible(v)
265
+ },
266
+ {
267
+ kind: "toggle",
268
+ label: "Values",
269
+ get: () => this.indicatorValuesOn,
270
+ set: (v) => this.setIndicatorValuesVisible(v)
259
271
  }
260
272
  ]
261
273
  },
@@ -289,6 +301,12 @@ var ChartCell = class {
289
301
  this.inner?.renderer.set("indicatorTitles", visible);
290
302
  this.deps.onStateDirty();
291
303
  }
304
+ /** Show/hide the plot values beside this cell's legend titles (persisted per cell). */
305
+ setIndicatorValuesVisible(visible) {
306
+ this.indicatorValuesOn = visible;
307
+ this.inner?.renderer.set("indicatorValues", visible);
308
+ this.deps.onStateDirty();
309
+ }
292
310
  /** The LIVE chart of this cell — never cache it across a layout change (the cell's
293
311
  * identity is what endures; the chart dies with the cell). */
294
312
  get chart() {
@@ -333,6 +351,11 @@ var ChartCell = class {
333
351
  if (!this.statusline || !this.inner) return;
334
352
  this.statusline.setDirectionColors(...statuslineInkOf(this.inner.renderer, this.priceStyle));
335
353
  }
354
+ /** Multi-cell grids keep the status line on one row and hide what doesn't fit —
355
+ * the workspace flips this with the layout (see Statusline.setFitMode). */
356
+ setStatuslineFit(on) {
357
+ this.statusline?.setFitMode(on);
358
+ }
336
359
  /** The workspace shell keeps the app theme; the cell host's tokens re-derive from
337
360
  * the LIVE plot surface (see {@link applyPlotOverlayTokens}). */
338
361
  syncPlotOverlayTokens() {
@@ -521,6 +544,7 @@ var ChartCell = class {
521
544
  priceStyle: this.priceStyle,
522
545
  watermark: this.watermarkOn,
523
546
  indicatorTitles: this.indicatorTitlesOn,
547
+ indicatorValues: this.indicatorValuesOn,
524
548
  rendererConfig: this.inner?.renderer.getConfig() ?? void 0,
525
549
  drawings: this.inner ? this.inner.drawings.toJSON() : void 0,
526
550
  // Natives from the chart's SYNC registry read — an async catalog mirror here
@@ -598,6 +622,49 @@ function registerBuiltinLayouts() {
598
622
  registerLayout({ id: "4", label: "4 grid", cols: [1, 1], rows: [1, 1], cells: slots(4) });
599
623
  registerLayout({ id: "8", label: "8 grid", cols: [1, 1, 1, 1], rows: [1, 1], cells: slots(8) });
600
624
  }
625
+ var GRID_PICKER_MAX = 4;
626
+ var GRID_BUILTIN_IDS = {
627
+ "1x1": "1",
628
+ "1x2": "2h",
629
+ "2x1": "2v",
630
+ "2x2": "4",
631
+ "2x4": "8"
632
+ };
633
+ var clampTrack = (n) => Math.max(1, Math.min(GRID_PICKER_MAX, Math.round(n)));
634
+ function layoutForGrid(rows, cols) {
635
+ const r = clampTrack(rows);
636
+ const c = clampTrack(cols);
637
+ const builtin = GRID_BUILTIN_IDS[`${r}x${c}`];
638
+ const registered = builtin ? registry.get(builtin) : void 0;
639
+ if (registered) return registered;
640
+ return {
641
+ id: `g${r}x${c}`,
642
+ // Width-first label — matches the picker's caption ("3 × 2" = 3 wide, 2 tall).
643
+ label: `${c} \xD7 ${r} grid`,
644
+ cols: Array.from({ length: c }, () => 1),
645
+ rows: Array.from({ length: r }, () => 1),
646
+ cells: slots(r * c)
647
+ };
648
+ }
649
+ var GRID_ID_RE = /^g([1-4])x([1-4])$/;
650
+ function ensureLayout(id) {
651
+ const registered = registry.get(id);
652
+ if (registered) return registered;
653
+ const g = GRID_ID_RE.exec(id);
654
+ if (g) return layoutForGrid(Number(g[1]), Number(g[2]));
655
+ return void 0;
656
+ }
657
+ function layoutShape(def) {
658
+ if (!def.areas && def.rows.length <= GRID_PICKER_MAX && def.cols.length <= GRID_PICKER_MAX && def.cells.length === def.rows.length * def.cols.length) {
659
+ return { rows: def.rows.length, cols: def.cols.length };
660
+ }
661
+ return null;
662
+ }
663
+ function occupancyGrid(def) {
664
+ if (def.areas) return def.areas.map((row) => row.trim().split(/\s+/));
665
+ const cols = def.cols.length;
666
+ return def.rows.map((_, r) => def.cols.map((_2, c) => def.cells[r * cols + c]?.id ?? `\xB7${r}x${c}`));
667
+ }
601
668
  function gridStyles(def, trackSizes) {
602
669
  const cols = trackSizes?.cols?.length === def.cols.length ? trackSizes.cols : def.cols;
603
670
  const rows = trackSizes?.rows?.length === def.rows.length ? trackSizes.rows : def.rows;
@@ -617,6 +684,15 @@ function activeAfterLayout(current, cellIds) {
617
684
  if (current != null && cellIds.includes(current)) return current;
618
685
  return cellIds[0] ?? null;
619
686
  }
687
+ function orderAfterLayout(order, slots2, active) {
688
+ const next = [...order];
689
+ if (active == null || slots2 <= 0) return next;
690
+ const idx = next.indexOf(active);
691
+ if (idx < 0 || idx < slots2) return next;
692
+ next.splice(idx, 1);
693
+ next.splice(slots2 - 1, 0, active);
694
+ return next;
695
+ }
620
696
 
621
697
  // src/workspace/splitters.ts
622
698
  var MIN_TRACK_FRAC = 0.1;
@@ -649,6 +725,27 @@ function trackOffsets(weights, sizePx, gapPx) {
649
725
  }
650
726
  return out;
651
727
  }
728
+ function seamSegments(grid, axis, index) {
729
+ const count = axis === "cols" ? grid.length : grid[0]?.length ?? 0;
730
+ const isSeam = (k) => axis === "cols" ? grid[k]?.[index] !== grid[k]?.[index + 1] : grid[index]?.[k] !== grid[index + 1]?.[k];
731
+ const out = [];
732
+ for (let k = 0; k < count; k += 1) {
733
+ if (!isSeam(k)) continue;
734
+ const last = out[out.length - 1];
735
+ if (last && last[1] === k - 1) last[1] = k;
736
+ else out.push([k, k]);
737
+ }
738
+ return out;
739
+ }
740
+ function segmentSpanPx(weights, sizePx, gapPx, from, to) {
741
+ const total = weights.reduce((s, w) => s + w, 0);
742
+ if (total <= 0) return { start: 0, end: 0 };
743
+ const content = sizePx - gapPx * (weights.length - 1);
744
+ const startOf = (k) => weights.slice(0, k).reduce((s, w) => s + w / total * content, 0) + gapPx * k;
745
+ const start = from === 0 ? 0 : startOf(from) - gapPx / 2;
746
+ const end = to === weights.length - 1 ? sizePx : startOf(to + 1) - gapPx / 2;
747
+ return { start, end };
748
+ }
652
749
  var HIT_PX = 8;
653
750
  var SplitterLayer = class {
654
751
  constructor(container, deps) {
@@ -657,23 +754,36 @@ var SplitterLayer = class {
657
754
  this.strips = [];
658
755
  this.drag = null;
659
756
  }
660
- /** Rebuild + reposition the strips for the current tracks/size. */
757
+ /** Rebuild + reposition the strips for the current tracks/size/occupancy. */
661
758
  layout() {
662
759
  const { cols, rows } = this.deps.tracks();
760
+ const grid = this.deps.grid();
663
761
  const rect = this.container.getBoundingClientRect();
664
762
  const gap = this.deps.gapPx();
665
- const wanted = Math.max(0, cols.length - 1) + Math.max(0, rows.length - 1);
666
- while (this.strips.length > wanted) this.strips.pop().remove();
667
- while (this.strips.length < wanted) this.strips.push(this.makeStrip());
668
- let k = 0;
763
+ const placements = [];
669
764
  for (const [i, x] of trackOffsets(cols, rect.width, gap).entries()) {
670
- this.place(this.strips[k], "cols", i, `left:${Math.round(x - HIT_PX / 2)}px;top:0;width:${HIT_PX}px;height:100%;cursor:col-resize;`);
671
- k += 1;
765
+ for (const [from, to] of seamSegments(grid, "cols", i)) {
766
+ const { start, end } = segmentSpanPx(rows, rect.height, gap, from, to);
767
+ placements.push({
768
+ axis: "cols",
769
+ index: i,
770
+ css: `left:${Math.round(x - HIT_PX / 2)}px;top:${Math.round(start)}px;width:${HIT_PX}px;height:${Math.round(end - start)}px;cursor:col-resize;`
771
+ });
772
+ }
672
773
  }
673
774
  for (const [i, y] of trackOffsets(rows, rect.height, gap).entries()) {
674
- this.place(this.strips[k], "rows", i, `left:0;top:${Math.round(y - HIT_PX / 2)}px;width:100%;height:${HIT_PX}px;cursor:row-resize;`);
675
- k += 1;
775
+ for (const [from, to] of seamSegments(grid, "rows", i)) {
776
+ const { start, end } = segmentSpanPx(cols, rect.width, gap, from, to);
777
+ placements.push({
778
+ axis: "rows",
779
+ index: i,
780
+ css: `left:${Math.round(start)}px;top:${Math.round(y - HIT_PX / 2)}px;width:${Math.round(end - start)}px;height:${HIT_PX}px;cursor:row-resize;`
781
+ });
782
+ }
676
783
  }
784
+ while (this.strips.length > placements.length) this.strips.pop().remove();
785
+ while (this.strips.length < placements.length) this.strips.push(this.makeStrip());
786
+ for (const [k, p] of placements.entries()) this.place(this.strips[k], p.axis, p.index, p.css);
677
787
  }
678
788
  destroy() {
679
789
  for (const s of this.strips.splice(0)) s.remove();
@@ -747,8 +857,10 @@ var CSS = `
747
857
  .vela-ws-grid { position: relative; flex: 1 1 auto; min-width: 0; display: grid; gap: ${GAP_PX}px; background: var(--vela-border-soft); }
748
858
  .vela-cell { background: var(--vela-bg); position: relative; }
749
859
  /* Active-cell highlight: an overlay ring ABOVE the chart's own canvas stack (a plain
750
- outline on the cell is painted under them) \u2014 inert to the pointer. */
751
- .vela-cell[data-active='1']::after {
860
+ outline on the cell is painted under them) \u2014 inert to the pointer. Scoped to
861
+ multi-cell grids ([data-multi]): a single-cell layout always has an active cell,
862
+ and ringing the only chart would just be noise. */
863
+ .vela-ws-grid[data-multi='1'] .vela-cell[data-active='1']::after {
752
864
  content: '';
753
865
  position: absolute;
754
866
  inset: 0;
@@ -756,7 +868,15 @@ var CSS = `
756
868
  pointer-events: none;
757
869
  z-index: 10;
758
870
  }
759
- .vela-ws-splitter:hover { background: var(--vela-accent); opacity: 0.35; }
871
+ /* Splitter hover mirrors the in-chart pane separator hover (CrosshairRenderer):
872
+ a soft band over the whole grab target + a solid 2px line on the seam center. */
873
+ .vela-ws-splitter:hover { background: var(--vela-separator-hover-band); }
874
+ .vela-ws-splitter:hover::after { content: ''; position: absolute; background: var(--vela-separator-hover-line); }
875
+ .vela-ws-splitter[data-axis='cols']:hover::after { left: calc(50% - 1px); top: 0; width: 2px; height: 100%; }
876
+ .vela-ws-splitter[data-axis='rows']:hover::after { top: calc(50% - 1px); left: 0; height: 2px; width: 100%; }
877
+ /* Mobile: the docked drawing-toolbar column would eat a phone-width grid \u2014 the shell's
878
+ drawings drawer + on-chart pill replace it (same policy as the widget's in-chart bar). */
879
+ [data-layout='mobile'] .vela-ws-toolbar { display: none; }
760
880
  `;
761
881
  registerIcon("layout", svg16('<rect x="1.5" y="1.5" width="13" height="13" rx="1.5"/><path d="M8 1.5v13M1.5 8h13"/>'));
762
882
  function declaredOrder(cells) {
@@ -807,6 +927,15 @@ var VelaWorkspace = class {
807
927
  /** Re-entrance guard around one propagation tick: followers' synchronous echoes
808
928
  * (their setVisibleRange re-emits viewport:changed) must not re-propagate. */
809
929
  this.syncBusy = false;
930
+ /** Same guard for the drawings link: the propagated mutations' own `drawing:*`
931
+ * events fire synchronously inside the propagation loop and must not fan out again. */
932
+ this.drawingSyncBusy = false;
933
+ /** LINKED drawings (the drawings sync): one map per synced set (cellId → that
934
+ * cell's drawing id), reachable from every member under its `cellId\0drawingId`
935
+ * key — any member finds its peers to push edits/removals onto. Survives a
936
+ * toggle-off (propagation freezes while the setting is off; re-enabling resumes
937
+ * edit/delete for these pairs). Cleared on reload / `applyState`. */
938
+ this.drawingLinks = /* @__PURE__ */ new Map();
810
939
  this.manifest = [];
811
940
  /** The shared manifest can no longer change instance sets — resolved, or no
812
941
  * `indicators` option so nothing ever will. Gates the cells' ledger fallback. */
@@ -814,6 +943,13 @@ var VelaWorkspace = class {
814
943
  this.openDialogs = 0;
815
944
  this.alerts = [];
816
945
  this.alertsMenu = null;
946
+ this.tfDrawer = null;
947
+ this.drawingsDrawer = null;
948
+ this.moreDrawer = null;
949
+ this.timezoneDrawer = null;
950
+ this.priceScaleDrawer = null;
951
+ /** Plot-local y of the last price-axis long-press (targets the pane under the finger). */
952
+ this.priceScalePressY = 0;
817
953
  this.attachmentDisposers = /* @__PURE__ */ new Map();
818
954
  /** The single grid-wide attribution mark — re-inked on a live theme swap. */
819
955
  this.attributionMark = null;
@@ -848,10 +984,10 @@ var VelaWorkspace = class {
848
984
  this.timezone = boot?.timezone ?? opts.timezone ?? "Etc/UTC";
849
985
  if (boot?.favorites) this.favs = [...boot.favorites];
850
986
  const sync = boot?.sync ?? opts.sync;
851
- for (const kind of ["viewport", "symbol", "timeframe", "crosshair"]) {
987
+ for (const kind of ["viewport", "symbol", "timeframe", "crosshair", "drawings"]) {
852
988
  this.applySyncSetting(kind, sync?.[kind]);
853
989
  }
854
- this.def = this.resolveLayout(boot?.layout && layoutDefinition(boot.layout) ? boot.layout : opts.layout ?? "4");
990
+ this.def = this.resolveLayout(boot?.layout && ensureLayout(boot.layout) ? boot.layout : opts.layout ?? "4");
855
991
  if (boot?.trackSizes) for (const [id, ts] of Object.entries(boot.trackSizes)) this.trackSizes.set(id, ts);
856
992
  if (boot?.charts) for (const { id, ...cs } of boot.charts) this.pool.set(id, cs);
857
993
  this.order = boot?.charts ? boot.charts.map((c) => c.id) : declaredOrder(opts.cells);
@@ -874,14 +1010,18 @@ var VelaWorkspace = class {
874
1010
  }
875
1011
  });
876
1012
  this.symbolPicker.setSource(() => this.feed.symbols());
877
- this.indicatorPicker = new IndicatorPicker({
1013
+ this.indicatorPicker = opts.indicatorPicker !== false ? new IndicatorPicker({
878
1014
  host: this.root,
879
1015
  library: () => this.active.libraryRows(),
880
1016
  onChart: () => this.active.onChartRows(),
881
1017
  onAdd: (i) => this.active.addFromLibrary(i),
882
1018
  onRemove: (i) => this.active.removeFromChart(i),
883
- onOpenChange: (open) => this.trackDialog(open)
884
- });
1019
+ onOpenChange: (open) => {
1020
+ if (open) for (const cell of this.cells()) cell.chart.renderer.closeDialogs();
1021
+ this.trackDialog(open);
1022
+ }
1023
+ }) : null;
1024
+ const picker = this.indicatorPicker;
885
1025
  this.tfQuick = new TimeframeQuick({
886
1026
  host: this.root,
887
1027
  onApply: (tf) => this.setActiveTimeframe(tf),
@@ -890,7 +1030,7 @@ var VelaWorkspace = class {
890
1030
  this.topbar = new Topbar(this.root, {
891
1031
  symbol: "",
892
1032
  onSymbolClick: () => this.symbolPicker.open(),
893
- onIndicatorsClick: () => this.indicatorPicker.open(),
1033
+ ...picker ? { onIndicatorsClick: () => picker.open() } : {},
894
1034
  onUndoClick: () => this.active.history.undo(),
895
1035
  onRedoClick: () => this.active.history.redo(),
896
1036
  onScreenshotClick: () => this.active.downloadScreenshot(),
@@ -902,14 +1042,23 @@ var VelaWorkspace = class {
902
1042
  onPriceStyle: (style) => this.active.setPriceStyle(style),
903
1043
  layout: {
904
1044
  current: this.def.id,
905
- options: () => layouts().map((l) => ({ id: l.id, label: l.label })),
906
- onSelect: (id) => this.setLayout(id),
907
- // Workspace-wide view toggles live under the grid presets. The check
908
- // reflects the simple all-cells form; flipping OVERRIDES a host-set
909
- // group record with plain on/off (groups stay an API-only shape).
910
- toggles: () => [{ id: "crosshair-sync", label: "Sync crosshair", checked: this.syncOpts.crosshair === true }],
911
- onToggle: (id) => {
912
- if (id === "crosshair-sync") this.sync.set("crosshair", this.syncOpts.crosshair ? false : true);
1045
+ // The picker composes dynamic layouts on its grid canvas; registered
1046
+ // presets the canvas cannot express (bespoke plugin areas) list as rows.
1047
+ shape: () => layoutShape(this.def),
1048
+ presets: () => layouts().filter((l) => layoutShape(l) === null).map((l) => ({ id: l.id, label: l.label })),
1049
+ onSelectGrid: (rows, cols) => this.setLayout(layoutForGrid(rows, cols)),
1050
+ onSelectPreset: (id) => this.setLayout(id),
1051
+ // The SYNC switches reflect the simple all-cells form; flipping one
1052
+ // OVERRIDES a host-set group record with plain on/off (groups stay an
1053
+ // API-only shape).
1054
+ syncs: () => [
1055
+ { id: "symbol", label: "Symbol", checked: this.syncOpts.symbol === true },
1056
+ { id: "timeframe", label: "Interval", checked: this.syncOpts.timeframe === true },
1057
+ { id: "crosshair", label: "Crosshair", checked: this.syncOpts.crosshair === true }
1058
+ ],
1059
+ onToggleSync: (id) => {
1060
+ const kind = id;
1061
+ this.sync.set(kind, this.syncOpts[kind] ? false : true);
913
1062
  }
914
1063
  },
915
1064
  getContext: () => this.context()
@@ -972,10 +1121,19 @@ var VelaWorkspace = class {
972
1121
  this.active.chart.drawings.setStayMode(on);
973
1122
  this.refocusActive();
974
1123
  },
975
- { dock: "static" }
1124
+ {
1125
+ dock: "static",
1126
+ // Drawings sync is a WORKSPACE link (same model as the layout
1127
+ // dropdown's switches) — the bar only hosts its toggle.
1128
+ onDrawingsSync: (on) => {
1129
+ this.sync.set("drawings", on);
1130
+ this.refocusActive();
1131
+ }
1132
+ }
976
1133
  ) : null;
977
1134
  this.drawToolbar?.setDefinition(defaultToolbar());
978
1135
  this.drawToolbar?.setVisible(true);
1136
+ this.drawToolbar?.setDrawingsSyncMode(!!this.syncOpts.drawings);
979
1137
  this.bottombar = opts.bottombar !== false ? new Bottombar(this.root, {
980
1138
  timezone: this.timezone,
981
1139
  onRange: (preset) => {
@@ -985,9 +1143,24 @@ var VelaWorkspace = class {
985
1143
  onTimezone: (zone) => this.setTimezone(zone),
986
1144
  onSettingsClick: () => this.active.chart.renderer.openSettings()
987
1145
  }) : null;
1146
+ this.mobileBar = opts.bottombar !== false ? new MobileBar(this.root, {
1147
+ symbol: "",
1148
+ timeframe: "60",
1149
+ onSymbolClick: () => this.symbolPicker.open(),
1150
+ onTimeframeClick: () => this.openTimeframeDrawer(),
1151
+ ...picker ? { onIndicatorsClick: () => picker.open() } : {},
1152
+ getContext: () => this.context(),
1153
+ onDrawingsClick: () => this.openDrawingsDrawer(),
1154
+ onMoreClick: () => this.openMoreDrawer(),
1155
+ onSettingsClick: () => this.active.chart.renderer.openSettings()
1156
+ }) : null;
1157
+ this.drawingPill = new DrawingPill(this.gridEl);
988
1158
  hostEl.appendChild(this.root);
1159
+ this.layoutCtl = new LayoutModeController(this.root, opts.layoutMode ?? "auto");
1160
+ this.layoutCtl.onChange((mode) => this.onLayoutModeChange(mode));
989
1161
  this.splitters = new SplitterLayer(this.gridEl, {
990
1162
  tracks: () => this.currentTracks(),
1163
+ grid: () => occupancyGrid(this.def),
991
1164
  apply: (axis, weights) => this.applyTracks(axis, weights),
992
1165
  reset: (axis) => this.applyTracks(axis, evenTracks(this.currentTracks()[axis].length)),
993
1166
  gapPx: () => GAP_PX
@@ -1005,6 +1178,7 @@ var VelaWorkspace = class {
1005
1178
  this.cellBackend = this.backendFor(this.def);
1006
1179
  this.applyGrid();
1007
1180
  this.buildCells();
1181
+ this.syncCellPresentation();
1008
1182
  this.setActiveCell(bootActive != null && this.cellsById.has(bootActive) ? bootActive : this.order[0] ?? null);
1009
1183
  if (opts.autofocus) this.refocusActive();
1010
1184
  if (opts.indicators !== void 0) {
@@ -1087,6 +1261,7 @@ var VelaWorkspace = class {
1087
1261
  refreshActions() {
1088
1262
  this.mountAttachments();
1089
1263
  this.topbar.renderActions();
1264
+ this.mobileBar?.renderActions();
1090
1265
  this.dock.refresh();
1091
1266
  for (const cell of this.cells()) cell.chart.renderer.setLegendActions(legendActionsProviderFor(cell.chart, () => this.context()));
1092
1267
  }
@@ -1135,7 +1310,7 @@ var VelaWorkspace = class {
1135
1310
  }
1136
1311
  if (st.favorites) this.favs = [...st.favorites];
1137
1312
  this.dock.applyState(st.panels);
1138
- for (const kind of ["viewport", "symbol", "timeframe", "crosshair"]) this.applySyncSetting(kind, st.sync?.[kind]);
1313
+ for (const kind of ["viewport", "symbol", "timeframe", "crosshair", "drawings"]) this.applySyncSetting(kind, st.sync?.[kind]);
1139
1314
  this.trackSizes.clear();
1140
1315
  if (st.trackSizes) for (const [id, ts] of Object.entries(st.trackSizes)) this.trackSizes.set(id, ts);
1141
1316
  for (const [id, cell] of [...this.cellsById]) {
@@ -1144,13 +1319,15 @@ var VelaWorkspace = class {
1144
1319
  this.events.emit("cell:destroyed", { id });
1145
1320
  }
1146
1321
  this.pool.clear();
1322
+ this.drawingLinks.clear();
1147
1323
  for (const { id, ...cs } of st.charts) this.pool.set(id, cs);
1148
1324
  this.order = st.charts.map((c) => c.id);
1149
- const def = layoutDefinition(st.layout);
1325
+ const def = ensureLayout(st.layout);
1150
1326
  if (def) this.def = def;
1151
1327
  this.cellBackend = this.backendFor(this.def);
1152
1328
  this.applyGrid();
1153
1329
  this.buildCells();
1330
+ this.syncCellPresentation();
1154
1331
  this.topbar.setLayout(this.def.id);
1155
1332
  const nextActive = st.activeCellId && this.cellsById.has(st.activeCellId) ? st.activeCellId : this.order[0] ?? null;
1156
1333
  if (nextActive === this.activeId) this.projectActiveCell();
@@ -1197,6 +1374,7 @@ var VelaWorkspace = class {
1197
1374
  const next = this.resolveLayout(layout);
1198
1375
  const nextBackend = this.backendFor(next);
1199
1376
  const rebuildAll = nextBackend !== this.cellBackend;
1377
+ this.order = orderAfterLayout(this.order, next.cells.length, this.activeId);
1200
1378
  const keep = new Set(this.order.slice(0, next.cells.length));
1201
1379
  for (const [id, cell] of [...this.cellsById]) {
1202
1380
  if (!keep.has(id) || rebuildAll) {
@@ -1210,6 +1388,7 @@ var VelaWorkspace = class {
1210
1388
  this.cellBackend = nextBackend;
1211
1389
  this.applyGrid();
1212
1390
  this.buildCells();
1391
+ this.syncCellPresentation();
1213
1392
  this.topbar.setLayout(next.id);
1214
1393
  const nextActive = activeAfterLayout(this.activeId, this.order.slice(0, next.cells.length));
1215
1394
  if (nextActive === this.activeId) this.projectActiveCell();
@@ -1245,11 +1424,19 @@ var VelaWorkspace = class {
1245
1424
  this.drawToolbar?.destroy();
1246
1425
  this.topbar.destroy();
1247
1426
  this.bottombar?.destroy();
1427
+ this.mobileBar?.destroy();
1428
+ this.drawingPill.destroy();
1429
+ this.tfDrawer?.destroy();
1430
+ this.drawingsDrawer?.destroy();
1431
+ this.moreDrawer?.destroy();
1432
+ this.timezoneDrawer?.destroy();
1433
+ this.priceScaleDrawer?.destroy();
1434
+ this.layoutCtl.destroy();
1248
1435
  this.dock.destroy();
1249
1436
  this.objectTree.destroy();
1250
1437
  this.dataWindow.destroy();
1251
1438
  this.symbolPicker.destroy();
1252
- this.indicatorPicker.destroy();
1439
+ this.indicatorPicker?.destroy();
1253
1440
  this.tfQuick.destroy();
1254
1441
  this.shortcutsHelp?.destroy();
1255
1442
  this.toast.destroy();
@@ -1270,6 +1457,10 @@ var VelaWorkspace = class {
1270
1457
  this.topbar.setPriceStyle(cell.priceStyle);
1271
1458
  this.topbar.setIndicatorCount(cell.indicatorCount);
1272
1459
  this.topbar.renderActions();
1460
+ this.mobileBar?.renderActions();
1461
+ this.mobileBar?.setSymbol(cell.symbol);
1462
+ this.mobileBar?.setTimeframe(cell.timeframe);
1463
+ this.drawingPill.onChart(cell.chart);
1273
1464
  const pushHistory = () => this.topbar.setHistoryState(cell.history.canUndo, cell.history.canRedo);
1274
1465
  this.historyUnsub?.();
1275
1466
  this.historyUnsub = cell.history.onChange(pushHistory);
@@ -1277,7 +1468,7 @@ var VelaWorkspace = class {
1277
1468
  this.objectTree.setSymbol(cell.symbol);
1278
1469
  this.dock.onChart(cell.chart);
1279
1470
  this.bottombar?.setActiveRange(cell.activeRangeId);
1280
- this.indicatorPicker.sync();
1471
+ this.indicatorPicker?.sync();
1281
1472
  this.glider.stop();
1282
1473
  const d = cell.chart.drawings;
1283
1474
  if (d.getTool() !== this.globalTool) d.setTool(this.globalTool);
@@ -1320,7 +1511,7 @@ var VelaWorkspace = class {
1320
1511
  // ── internals ───────────────────────────────────────────────
1321
1512
  resolveLayout(layout) {
1322
1513
  if (typeof layout !== "string") return layout;
1323
- const def = layoutDefinition(layout);
1514
+ const def = ensureLayout(layout);
1324
1515
  if (!def) throw new Error(`[vela] unknown workspace layout "${layout}" \u2014 register it with registerLayout().`);
1325
1516
  return def;
1326
1517
  }
@@ -1348,6 +1539,8 @@ var VelaWorkspace = class {
1348
1539
  * `order[i]` — the identity/position decoupling in one line. */
1349
1540
  applyGrid() {
1350
1541
  const { container, perCell } = gridStyles(this.def, this.trackSizes.get(this.def.id));
1542
+ if (this.def.cells.length > 1) this.gridEl.dataset.multi = "1";
1543
+ else delete this.gridEl.dataset.multi;
1351
1544
  this.gridEl.style.gridTemplateColumns = container.gridTemplateColumns ?? "";
1352
1545
  this.gridEl.style.gridTemplateRows = container.gridTemplateRows ?? "";
1353
1546
  this.gridEl.style.gridTemplateAreas = container.gridTemplateAreas ?? "";
@@ -1395,7 +1588,9 @@ var VelaWorkspace = class {
1395
1588
  });
1396
1589
  cell.host.style.gridArea = perCell[slot.id]?.gridArea ?? "";
1397
1590
  this.cellsById.set(id, cell);
1591
+ if (id === this.activeId) cell.host.dataset.active = "1";
1398
1592
  this.wireCell(cell);
1593
+ cell.chart.renderer.setLayoutMode(this.layoutCtl.current);
1399
1594
  if (this.favs.length > 0) cell.chart.drawings.setFavorites(this.favs);
1400
1595
  cell.setManifest(this.manifest, pooled?.indicators == null);
1401
1596
  this.events.emit("cell:created", { id });
@@ -1425,10 +1620,20 @@ var VelaWorkspace = class {
1425
1620
  this.drawToolbar?.setFavorites(favorites);
1426
1621
  this.markStateDirty();
1427
1622
  });
1428
- chart.renderer.onCrosshairMove((e) => this.propagateCrosshair(cell.id, e.time));
1429
- chart.on("drawing:created", () => this.markStateDirty());
1430
- chart.on("drawing:edited", () => this.markStateDirty());
1431
- chart.on("drawing:removed", () => this.markStateDirty());
1623
+ chart.renderer.onCrosshairMove((e) => this.propagateCrosshair(cell.id, e.time, e.paneKind === "price" ? e.price : null));
1624
+ chart.on("drawing:created", ({ id }) => {
1625
+ this.propagateDrawing(cell.id, id);
1626
+ this.markStateDirty();
1627
+ });
1628
+ chart.on("drawing:draft", ({ doc }) => this.propagateDraft(cell.id, doc));
1629
+ chart.on("drawing:edited", ({ id }) => {
1630
+ this.propagateDrawingEdit(cell.id, id);
1631
+ this.markStateDirty();
1632
+ });
1633
+ chart.on("drawing:removed", ({ id }) => {
1634
+ this.propagateDrawingRemoval(cell.id, id);
1635
+ this.markStateDirty();
1636
+ });
1432
1637
  chart.on("drawing:tool", ({ type }) => {
1433
1638
  if (cell.id !== this.activeId) return;
1434
1639
  this.globalTool = type;
@@ -1451,11 +1656,22 @@ var VelaWorkspace = class {
1451
1656
  });
1452
1657
  chart.on("viewport:changed", (range) => this.propagateViewport(cell.id, range));
1453
1658
  chart.on("theme:changed", (t) => this.setTheme(t));
1659
+ chart.renderer.onAxisLongPress((e) => {
1660
+ if (this.layoutCtl.current !== "mobile") return;
1661
+ if (e.axis === "time") this.openTimezoneDrawer();
1662
+ else this.openPriceScaleDrawer(e.y);
1663
+ });
1454
1664
  }
1455
1665
  // ── sync links ──────────────────────────────────────────────
1456
1666
  applySyncSetting(kind, setting, align = false) {
1457
1667
  if (setting == null || setting === false) delete this.syncOpts[kind];
1458
1668
  else this.syncOpts[kind] = setting;
1669
+ if (kind === "drawings") {
1670
+ for (const cell of this.cellsById.values()) cell.chart.drawings.setExternalGhost(null);
1671
+ this.drawToolbar?.setDrawingsSyncMode(!!setting);
1672
+ this.markStateDirty();
1673
+ return;
1674
+ }
1459
1675
  if (kind === "crosshair") {
1460
1676
  for (const cell of this.cellsById.values()) cell.chart.renderer.setExternalCrosshair(null);
1461
1677
  if (setting && ![...this.cellsById.values()].some((c) => c.chart.renderer.supportsExternalCrosshair)) {
@@ -1465,6 +1681,7 @@ var VelaWorkspace = class {
1465
1681
  this.markStateDirty();
1466
1682
  return;
1467
1683
  }
1684
+ if (this.topbar && this.def) this.topbar.setLayout(this.def.id);
1468
1685
  this.markStateDirty();
1469
1686
  if (align && setting && this.activeId) {
1470
1687
  if (kind === "viewport") {
@@ -1477,17 +1694,24 @@ var VelaWorkspace = class {
1477
1694
  }
1478
1695
  /**
1479
1696
  * Mirror an origin cell's pointer time onto its same-group followers as GHOST
1480
- * crosshairs (`renderer.setExternalCrosshair`). Leaving the origin propagates
1481
- * `null` (the event already carries it) and clears every ghost. No busy guard
1482
- * needed: an external crosshair never re-emits `onCrosshairMove` the flow is
1483
- * one-way by port contract, so no echo loop can exist.
1697
+ * crosshairs (`renderer.setExternalCrosshair`). The horizontal price level rides
1698
+ * along ONLY to followers showing the same ticker price scales are comparable
1699
+ * there, while an origin's price painted on another market's pane would be noise.
1700
+ * Leaving the origin propagates `null` (the event already carries it) and clears
1701
+ * every ghost. No busy guard needed: an external crosshair never re-emits
1702
+ * `onCrosshairMove` — the flow is one-way by port contract, so no echo loop can
1703
+ * exist.
1484
1704
  */
1485
- propagateCrosshair(originId, time) {
1705
+ propagateCrosshair(originId, time, price = null) {
1486
1706
  if (this.destroyed) return;
1487
1707
  const setting = this.syncOpts.crosshair;
1488
1708
  if (!setting) return;
1709
+ const originTicker = parseSymbol(this.cellsById.get(originId)?.symbol ?? "").ticker;
1489
1710
  for (const id of syncTargets(originId, setting, [...this.cellsById.keys()])) {
1490
- this.cellsById.get(id)?.chart.renderer.setExternalCrosshair(time);
1711
+ const follower = this.cellsById.get(id);
1712
+ if (!follower) continue;
1713
+ const comparable = originTicker !== "" && parseSymbol(follower.symbol).ticker === originTicker;
1714
+ follower.chart.renderer.setExternalCrosshair(time, comparable ? price : null);
1491
1715
  }
1492
1716
  }
1493
1717
  /**
@@ -1516,6 +1740,101 @@ var VelaWorkspace = class {
1516
1740
  this.syncBusy = false;
1517
1741
  }
1518
1742
  }
1743
+ /**
1744
+ * Copy a freshly created drawing onto the origin's same-group followers (fresh ids
1745
+ * through `drawings.add` — anchors are time+price, so the copy lands at the same
1746
+ * spot whatever the follower shows) and register the whole set as LINKED: edits
1747
+ * and removals of any member follow while the link is on ({@link drawingLinks}).
1748
+ * The busy guard stops the copies' own `drawing:created` events from fanning out.
1749
+ */
1750
+ propagateDrawing(originId, drawingId) {
1751
+ if (this.drawingSyncBusy || this.destroyed) return;
1752
+ const setting = this.syncOpts.drawings;
1753
+ if (!setting) return;
1754
+ const doc = this.cellsById.get(originId)?.chart.drawings.all().find((d) => d.id === drawingId);
1755
+ if (!doc) return;
1756
+ this.drawingSyncBusy = true;
1757
+ try {
1758
+ const group = /* @__PURE__ */ new Map([[originId, drawingId]]);
1759
+ for (const id of syncTargets(originId, setting, [...this.cellsById.keys()])) {
1760
+ const copy = this.cellsById.get(id)?.chart.drawings.add(doc.type, {
1761
+ paneId: doc.paneId,
1762
+ anchors: doc.anchors,
1763
+ style: doc.style,
1764
+ text: doc.text,
1765
+ props: doc.props
1766
+ });
1767
+ if (copy) group.set(id, copy.id);
1768
+ }
1769
+ if (group.size > 1) {
1770
+ for (const [cellId, dId] of group) this.drawingLinks.set(`${cellId}\0${dId}`, group);
1771
+ }
1772
+ } finally {
1773
+ this.drawingSyncBusy = false;
1774
+ }
1775
+ }
1776
+ /** Mirror an in-progress placement (its current ghost, `null` = placement ended)
1777
+ * onto the origin's same-group followers — the live half of the drawings link;
1778
+ * the created copy replaces the ghosts when the placement completes. One-way by
1779
+ * contract (an external ghost never re-emits drafts), so no busy guard needed. */
1780
+ propagateDraft(originId, doc) {
1781
+ if (this.destroyed) return;
1782
+ const setting = this.syncOpts.drawings;
1783
+ if (!setting) return;
1784
+ for (const id of syncTargets(originId, setting, [...this.cellsById.keys()])) {
1785
+ this.cellsById.get(id)?.chart.drawings.setExternalGhost(doc);
1786
+ }
1787
+ }
1788
+ /** Push a linked drawing's edited CONTENT (anchors, style, text, per-type props)
1789
+ * onto its same-group peers — any member propagates, not just the original. */
1790
+ propagateDrawingEdit(originId, drawingId) {
1791
+ if (this.drawingSyncBusy || this.destroyed) return;
1792
+ const setting = this.syncOpts.drawings;
1793
+ if (!setting) return;
1794
+ const group = this.drawingLinks.get(`${originId}\0${drawingId}`);
1795
+ if (!group) return;
1796
+ const doc = this.cellsById.get(originId)?.chart.drawings.all().find((d) => d.id === drawingId);
1797
+ if (!doc) return;
1798
+ this.drawingSyncBusy = true;
1799
+ try {
1800
+ for (const id of syncTargets(originId, setting, [...this.cellsById.keys()])) {
1801
+ const peerId = group.get(id);
1802
+ if (peerId == null) continue;
1803
+ this.cellsById.get(id)?.chart.drawings.update(peerId, {
1804
+ anchors: doc.anchors,
1805
+ style: doc.style,
1806
+ text: doc.text,
1807
+ props: doc.props
1808
+ });
1809
+ }
1810
+ } finally {
1811
+ this.drawingSyncBusy = false;
1812
+ }
1813
+ }
1814
+ /** Remove a linked drawing's peers with it. The removed member always leaves its
1815
+ * link group (whatever the setting); peers are only deleted while the link is on.
1816
+ * A propagated peer removal re-enters here under the busy guard and just cleans
1817
+ * its own link key. */
1818
+ propagateDrawingRemoval(originId, drawingId) {
1819
+ if (this.destroyed) return;
1820
+ const key = `${originId}\0${drawingId}`;
1821
+ const group = this.drawingLinks.get(key);
1822
+ if (!group) return;
1823
+ this.drawingLinks.delete(key);
1824
+ group.delete(originId);
1825
+ if (this.drawingSyncBusy) return;
1826
+ const setting = this.syncOpts.drawings;
1827
+ if (!setting) return;
1828
+ this.drawingSyncBusy = true;
1829
+ try {
1830
+ for (const id of syncTargets(originId, setting, [...this.cellsById.keys()])) {
1831
+ const peerId = group.get(id);
1832
+ if (peerId != null) this.cellsById.get(id)?.chart.drawings.remove(peerId);
1833
+ }
1834
+ } finally {
1835
+ this.drawingSyncBusy = false;
1836
+ }
1837
+ }
1519
1838
  /**
1520
1839
  * Push an origin cell's symbol/timeframe onto its same-group followers (fired from
1521
1840
  * `market:changed`). Convergence comes from IDEMPOTENCE, not the guard: a follower's
@@ -1547,6 +1866,8 @@ var VelaWorkspace = class {
1547
1866
  if (!cell) return;
1548
1867
  this.topbar.setSymbol(cell.symbol);
1549
1868
  this.topbar.setTimeframe(cell.timeframe);
1869
+ this.mobileBar?.setSymbol(cell.symbol);
1870
+ this.mobileBar?.setTimeframe(cell.timeframe);
1550
1871
  this.objectTree.setSymbol(cell.symbol);
1551
1872
  }
1552
1873
  /** Trigger ② — a cell's indicator ledger changed: count + picker only if active. */
@@ -1556,13 +1877,132 @@ var VelaWorkspace = class {
1556
1877
  const cell = this.cellsById.get(id);
1557
1878
  if (!cell) return;
1558
1879
  this.topbar.setIndicatorCount(cell.indicatorCount);
1559
- this.indicatorPicker.sync();
1880
+ this.indicatorPicker?.sync();
1560
1881
  }
1561
1882
  /** Timeframe changes routed from the topbar menu / quick entry (chip state follows). */
1562
1883
  setActiveTimeframe(tf) {
1563
1884
  this.bottombar?.setActiveRange(null);
1564
1885
  this.active.setTimeframe(tf);
1565
1886
  }
1887
+ /** The mode flipped (container resized across the breakpoint, or a coarse-pointer
1888
+ * change). Close the open surfaces — a desktop card must not linger over the
1889
+ * mobile chrome (and vice versa) — and re-present every cell's own chrome. */
1890
+ onLayoutModeChange(mode) {
1891
+ this.symbolPicker.close();
1892
+ this.indicatorPicker?.close();
1893
+ this.tfDrawer?.close();
1894
+ this.drawingsDrawer?.close();
1895
+ this.moreDrawer?.close();
1896
+ this.timezoneDrawer?.close();
1897
+ this.priceScaleDrawer?.close();
1898
+ this.alertsMenu?.destroy();
1899
+ this.alertsMenu = null;
1900
+ for (const cell of this.cellsById.values()) {
1901
+ cell.chart.renderer.closeDialogs();
1902
+ cell.chart.renderer.setLayoutMode(mode);
1903
+ }
1904
+ this.syncCellPresentation();
1905
+ }
1906
+ /** Push the layout-shape-dependent chrome onto every cell: multi-cell grids keep
1907
+ * the status line on one row (segments that don't fit hide), and on MOBILE their
1908
+ * legends' fold chip routes to the object tree instead of unfolding in place —
1909
+ * per-indicator controls live there (the legend rows have no room in a grid cell). */
1910
+ syncCellPresentation() {
1911
+ const multi = this.def.cells.length > 1;
1912
+ const overview = multi && this.layoutCtl.current === "mobile" ? () => this.dock.toggle("objects", true) : null;
1913
+ for (const cell of this.cellsById.values()) {
1914
+ cell.setStatuslineFit(multi);
1915
+ cell.chart.renderer.setLegendOverviewAction(overview);
1916
+ }
1917
+ }
1918
+ // ── mobile drawers (built on first open; every read is live and hits the ACTIVE cell) ──
1919
+ openTimeframeDrawer() {
1920
+ this.tfDrawer ?? (this.tfDrawer = new TimeframeDrawer({
1921
+ host: this.root,
1922
+ timeframes: this.opts.timeframes ?? DEFAULT_TIMEFRAMES,
1923
+ ranges: RANGE_PRESETS,
1924
+ currentTimeframe: () => this.active.timeframe,
1925
+ activeRange: () => this.active.activeRangeId,
1926
+ onTimeframe: (tf) => this.setActiveTimeframe(tf),
1927
+ onRange: (preset) => {
1928
+ this.active.applyRange(preset);
1929
+ this.bottombar?.setActiveRange(preset.id);
1930
+ },
1931
+ onOpenChange: (open) => this.trackDialog(open)
1932
+ }));
1933
+ this.tfDrawer.open();
1934
+ }
1935
+ openDrawingsDrawer() {
1936
+ this.drawingsDrawer ?? (this.drawingsDrawer = new DrawingsDrawer({
1937
+ host: this.root,
1938
+ toolbar: () => defaultToolbar(),
1939
+ // the shared static toolbar's definition (see constructor)
1940
+ currentTool: () => this.active.chart.drawings.getTool(),
1941
+ isFavorite: (type) => this.active.chart.drawings.isFavorite(type),
1942
+ onFavorite: (type, on) => this.active.chart.drawings.setFavorite(type, on),
1943
+ onSelect: (type) => this.active.chart.drawings.setTool(type),
1944
+ onOpenChange: (open) => this.trackDialog(open)
1945
+ }));
1946
+ this.drawingsDrawer.open();
1947
+ }
1948
+ openMoreDrawer() {
1949
+ this.moreDrawer ?? (this.moreDrawer = new MoreDrawer({
1950
+ host: this.root,
1951
+ onUndo: () => this.active.history.undo(),
1952
+ onRedo: () => this.active.history.redo(),
1953
+ onScreenshot: () => this.active.downloadScreenshot(),
1954
+ canUndo: () => this.active.history.canUndo,
1955
+ canRedo: () => this.active.history.canRedo,
1956
+ priceStyles: () => priceStyleIds().map((id) => ({ id, label: priceStyleLabel(id), icon: priceStyleIcon(id) })),
1957
+ priceStyle: () => this.active.priceStyle,
1958
+ onPriceStyle: (id) => this.active.setPriceStyle(id),
1959
+ panels: () => [...this.dock.list()],
1960
+ onTogglePanel: (id) => this.dock.toggle(id),
1961
+ alerts: () => this.alerts.map((a) => ({ title: `[${a.cellId} \xB7 ${a.symbol}] ${a.title}`, message: a.message, time: a.time })),
1962
+ // Left-aligned actions have their own bottom-bar stop — only the rest
1963
+ // lands in the drawer, or every left action would appear twice.
1964
+ actions: () => widgetActions("topbar", this.context()).filter((a) => a.align !== "left").map((a) => ({ label: a.label, icon: a.icon, run: () => a.run(this.context()) })),
1965
+ // The desktop layout dropdown's whole surface — the grid canvas, the
1966
+ // non-canvas presets and the sync switches — relocated into the kebab
1967
+ // drawer (the topbar is hidden on mobile). Same reads as the topbar block.
1968
+ layout: {
1969
+ shape: () => layoutShape(this.def),
1970
+ presets: () => layouts().filter((l) => layoutShape(l) === null).map((l) => ({ id: l.id, label: l.label, checked: l.id === this.def.id })),
1971
+ onSelectGrid: (rows, cols) => this.setLayout(layoutForGrid(rows, cols)),
1972
+ onSelectPreset: (id) => this.setLayout(id),
1973
+ syncs: () => [
1974
+ { id: "symbol", label: "Symbol", checked: this.syncOpts.symbol === true },
1975
+ { id: "timeframe", label: "Interval", checked: this.syncOpts.timeframe === true },
1976
+ { id: "crosshair", label: "Crosshair", checked: this.syncOpts.crosshair === true }
1977
+ ],
1978
+ onToggleSync: (id) => {
1979
+ const kind = id;
1980
+ this.sync.set(kind, this.syncOpts[kind] ? false : true);
1981
+ }
1982
+ },
1983
+ onOpenChange: (open) => this.trackDialog(open)
1984
+ }));
1985
+ this.moreDrawer.open();
1986
+ }
1987
+ openTimezoneDrawer() {
1988
+ this.timezoneDrawer ?? (this.timezoneDrawer = new TimezoneDrawer({
1989
+ host: this.root,
1990
+ timezone: () => this.timezone,
1991
+ onTimezone: (zone) => this.setTimezone(zone),
1992
+ onOpenChange: (open) => this.trackDialog(open)
1993
+ }));
1994
+ this.timezoneDrawer.open();
1995
+ }
1996
+ openPriceScaleDrawer(y) {
1997
+ this.priceScalePressY = y;
1998
+ this.priceScaleDrawer ?? (this.priceScaleDrawer = new PriceScaleDrawer({
1999
+ host: this.root,
2000
+ chart: () => this.activeId ? this.cellsById.get(this.activeId)?.chart ?? null : null,
2001
+ pressY: () => this.priceScalePressY,
2002
+ onOpenChange: (open) => this.trackDialog(open)
2003
+ }));
2004
+ this.priceScaleDrawer.open();
2005
+ }
1566
2006
  /** The aggregated alerts bell — entries carry their cell; selecting one activates it. */
1567
2007
  openAlertsMenu(anchor) {
1568
2008
  this.alertsMenu?.destroy();
@@ -1634,7 +2074,9 @@ var VelaWorkspace = class {
1634
2074
  this.keymap.register({ id: "view.zoom-out", keys: "mod+arrowdown", label: "Zoom out", category: "Chart", run: () => this.glider.zoom(ZOOM_OUT) });
1635
2075
  this.keymap.register({ id: "view.pan-left", keys: "mod+arrowleft", label: "Pan toward history", category: "Chart", run: () => this.active.chart.panBy(-PAN_FAST) });
1636
2076
  this.keymap.register({ id: "view.pan-right", keys: "mod+arrowright", label: "Pan toward now", category: "Chart", run: () => this.active.chart.panBy(PAN_FAST) });
1637
- this.keymap.register({ id: "indicators.open", keys: "/", label: "Open the indicator picker", category: "Indicators", run: () => this.indicatorPicker.open() });
2077
+ if (this.indicatorPicker) {
2078
+ this.keymap.register({ id: "indicators.open", keys: "/", label: "Open the indicator picker", category: "Indicators", run: () => this.indicatorPicker?.open() });
2079
+ }
1638
2080
  this.keymap.register({
1639
2081
  id: "help.shortcuts",
1640
2082
  keys: "?",
@@ -1691,4 +2133,4 @@ var VelaWorkspace = class {
1691
2133
  }
1692
2134
  };
1693
2135
 
1694
- export { ChartCell, VelaWorkspace, activeAfterLayout, evenTracks, gridStyles, layoutDefinition, layouts, memoryStorageAdapter, rangesWithin, registerBuiltinLayouts, registerLayout, resizeTracks, syncTargets, trackOffsets, unregisterLayout };
2136
+ export { ChartCell, GRID_PICKER_MAX, VelaWorkspace, activeAfterLayout, ensureLayout, evenTracks, gridStyles, layoutDefinition, layoutForGrid, layoutShape, layouts, memoryStorageAdapter, rangesWithin, registerBuiltinLayouts, registerLayout, resizeTracks, syncTargets, trackOffsets, unregisterLayout };