@luxalgo/vela 0.5.0 → 0.5.1

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-DKNDHpNv.d.cts → DataProvider-BBQ3Hr21.d.cts} +1 -1
  2. package/dist/{DataProvider-Dzd-Erlk.d.ts → DataProvider-DGesjOJT.d.ts} +1 -1
  3. package/dist/{chunk-OVQKKXLZ.js → chunk-5SE4PL3H.js} +402 -60
  4. package/dist/{chunk-RHIDOUFL.js → chunk-AIW3EJUE.js} +2 -2
  5. package/dist/{chunk-GYD2THPV.js → chunk-EP6NDJYK.js} +5 -0
  6. package/dist/{chunk-KCCZNKH7.js → chunk-FNOMKHEZ.js} +668 -78
  7. package/dist/{chunk-7UFX5ZIG.js → chunk-S6F6SQ2L.js} +7 -2
  8. package/dist/{chunk-KM6LHB3Y.js → chunk-ULIX6AE2.js} +6 -1
  9. package/dist/{contributions-hX3EUyjG.d.ts → contributions-6N0rbrPC.d.ts} +46 -31
  10. package/dist/{contributions-o1GRKPI_.d.cts → contributions-BlV3kb0O.d.cts} +46 -31
  11. package/dist/{history-Dzxz-MQj.d.ts → history-BTd52YMA.d.ts} +10 -5
  12. package/dist/{history-LJkz4-sS.d.cts → history-DX3Zne3a.d.cts} +10 -5
  13. package/dist/index.cjs +680 -75
  14. package/dist/index.d.cts +37 -12
  15. package/dist/index.d.ts +37 -12
  16. package/dist/index.js +4 -4
  17. package/dist/{options-Q-576hIi.d.cts → options-DbQholFV.d.cts} +14 -0
  18. package/dist/{options-Q-576hIi.d.ts → options-DbQholFV.d.ts} +14 -0
  19. package/dist/{plugin-D94muTV-.d.cts → plugin-O34MbXf-.d.cts} +157 -7
  20. package/dist/{plugin-aGUD1epn.d.ts → plugin-TN3n-9YN.d.ts} +157 -7
  21. package/dist/plugin.cjs +5 -0
  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 +10 -0
  32. package/dist/ui.d.cts +1 -1
  33. package/dist/ui.d.ts +1 -1
  34. package/dist/ui.js +3 -3
  35. package/dist/vela.global.js +680 -75
  36. package/dist/vela.global.min.js +59 -28
  37. package/dist/widget.cjs +1081 -134
  38. package/dist/widget.d.cts +30 -16
  39. package/dist/widget.d.ts +30 -16
  40. package/dist/widget.js +9 -9
  41. package/dist/workspace.cjs +1372 -183
  42. package/dist/workspace.d.cts +66 -8
  43. package/dist/workspace.d.ts +66 -8
  44. package/dist/workspace.js +275 -37
  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, toolShortcutHints, resolveIndicators, sanitizeState, encodeState, ZOOM_IN, ZOOM_OUT, PAN_FAST, ShortcutsHelp } from './chunk-5SE4PL3H.js';
2
+ export { decodeState, encodeState, sanitizeState } from './chunk-5SE4PL3H.js';
3
+ import { parseSymbol, Vela, normalizeTimezone, TypedEventBus, MultiProviderFeed, resolveTheme, createCustomMark, createAttributionMark, DrawingToolbar, defaultToolbar, applyAttributionMarkTheme, sharedBarStore, timeframeToMs } from './chunk-FNOMKHEZ.js';
4
+ import { resolveEngines, legendActionsProviderFor, rendererDefaults, widgetAttachments } from './chunk-S6F6SQ2L.js';
5
+ import { applyPlotOverlayTokens, ensureUIHost, KeymapManager, Menu, isEditableTarget } from './chunk-AIW3EJUE.js';
6
+ import './chunk-ULIX6AE2.js';
7
+ import { registerIcon, svg16, injectStyles } from './chunk-EP6NDJYK.js';
8
8
 
9
9
  // src/workspace/sync.ts
10
10
  function syncTargets(originId, setting, cellIds) {
@@ -598,6 +598,49 @@ function registerBuiltinLayouts() {
598
598
  registerLayout({ id: "4", label: "4 grid", cols: [1, 1], rows: [1, 1], cells: slots(4) });
599
599
  registerLayout({ id: "8", label: "8 grid", cols: [1, 1, 1, 1], rows: [1, 1], cells: slots(8) });
600
600
  }
601
+ var GRID_PICKER_MAX = 4;
602
+ var GRID_BUILTIN_IDS = {
603
+ "1x1": "1",
604
+ "1x2": "2h",
605
+ "2x1": "2v",
606
+ "2x2": "4",
607
+ "2x4": "8"
608
+ };
609
+ var clampTrack = (n) => Math.max(1, Math.min(GRID_PICKER_MAX, Math.round(n)));
610
+ function layoutForGrid(rows, cols) {
611
+ const r = clampTrack(rows);
612
+ const c = clampTrack(cols);
613
+ const builtin = GRID_BUILTIN_IDS[`${r}x${c}`];
614
+ const registered = builtin ? registry.get(builtin) : void 0;
615
+ if (registered) return registered;
616
+ return {
617
+ id: `g${r}x${c}`,
618
+ // Width-first label — matches the picker's caption ("3 × 2" = 3 wide, 2 tall).
619
+ label: `${c} \xD7 ${r} grid`,
620
+ cols: Array.from({ length: c }, () => 1),
621
+ rows: Array.from({ length: r }, () => 1),
622
+ cells: slots(r * c)
623
+ };
624
+ }
625
+ var GRID_ID_RE = /^g([1-4])x([1-4])$/;
626
+ function ensureLayout(id) {
627
+ const registered = registry.get(id);
628
+ if (registered) return registered;
629
+ const g = GRID_ID_RE.exec(id);
630
+ if (g) return layoutForGrid(Number(g[1]), Number(g[2]));
631
+ return void 0;
632
+ }
633
+ function layoutShape(def) {
634
+ if (!def.areas && def.rows.length <= GRID_PICKER_MAX && def.cols.length <= GRID_PICKER_MAX && def.cells.length === def.rows.length * def.cols.length) {
635
+ return { rows: def.rows.length, cols: def.cols.length };
636
+ }
637
+ return null;
638
+ }
639
+ function occupancyGrid(def) {
640
+ if (def.areas) return def.areas.map((row) => row.trim().split(/\s+/));
641
+ const cols = def.cols.length;
642
+ return def.rows.map((_, r) => def.cols.map((_2, c) => def.cells[r * cols + c]?.id ?? `\xB7${r}x${c}`));
643
+ }
601
644
  function gridStyles(def, trackSizes) {
602
645
  const cols = trackSizes?.cols?.length === def.cols.length ? trackSizes.cols : def.cols;
603
646
  const rows = trackSizes?.rows?.length === def.rows.length ? trackSizes.rows : def.rows;
@@ -617,6 +660,15 @@ function activeAfterLayout(current, cellIds) {
617
660
  if (current != null && cellIds.includes(current)) return current;
618
661
  return cellIds[0] ?? null;
619
662
  }
663
+ function orderAfterLayout(order, slots2, active) {
664
+ const next = [...order];
665
+ if (active == null || slots2 <= 0) return next;
666
+ const idx = next.indexOf(active);
667
+ if (idx < 0 || idx < slots2) return next;
668
+ next.splice(idx, 1);
669
+ next.splice(slots2 - 1, 0, active);
670
+ return next;
671
+ }
620
672
 
621
673
  // src/workspace/splitters.ts
622
674
  var MIN_TRACK_FRAC = 0.1;
@@ -649,6 +701,27 @@ function trackOffsets(weights, sizePx, gapPx) {
649
701
  }
650
702
  return out;
651
703
  }
704
+ function seamSegments(grid, axis, index) {
705
+ const count = axis === "cols" ? grid.length : grid[0]?.length ?? 0;
706
+ const isSeam = (k) => axis === "cols" ? grid[k]?.[index] !== grid[k]?.[index + 1] : grid[index]?.[k] !== grid[index + 1]?.[k];
707
+ const out = [];
708
+ for (let k = 0; k < count; k += 1) {
709
+ if (!isSeam(k)) continue;
710
+ const last = out[out.length - 1];
711
+ if (last && last[1] === k - 1) last[1] = k;
712
+ else out.push([k, k]);
713
+ }
714
+ return out;
715
+ }
716
+ function segmentSpanPx(weights, sizePx, gapPx, from, to) {
717
+ const total = weights.reduce((s, w) => s + w, 0);
718
+ if (total <= 0) return { start: 0, end: 0 };
719
+ const content = sizePx - gapPx * (weights.length - 1);
720
+ const startOf = (k) => weights.slice(0, k).reduce((s, w) => s + w / total * content, 0) + gapPx * k;
721
+ const start = from === 0 ? 0 : startOf(from) - gapPx / 2;
722
+ const end = to === weights.length - 1 ? sizePx : startOf(to + 1) - gapPx / 2;
723
+ return { start, end };
724
+ }
652
725
  var HIT_PX = 8;
653
726
  var SplitterLayer = class {
654
727
  constructor(container, deps) {
@@ -657,23 +730,36 @@ var SplitterLayer = class {
657
730
  this.strips = [];
658
731
  this.drag = null;
659
732
  }
660
- /** Rebuild + reposition the strips for the current tracks/size. */
733
+ /** Rebuild + reposition the strips for the current tracks/size/occupancy. */
661
734
  layout() {
662
735
  const { cols, rows } = this.deps.tracks();
736
+ const grid = this.deps.grid();
663
737
  const rect = this.container.getBoundingClientRect();
664
738
  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;
739
+ const placements = [];
669
740
  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;
741
+ for (const [from, to] of seamSegments(grid, "cols", i)) {
742
+ const { start, end } = segmentSpanPx(rows, rect.height, gap, from, to);
743
+ placements.push({
744
+ axis: "cols",
745
+ index: i,
746
+ 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;`
747
+ });
748
+ }
672
749
  }
673
750
  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;
751
+ for (const [from, to] of seamSegments(grid, "rows", i)) {
752
+ const { start, end } = segmentSpanPx(cols, rect.width, gap, from, to);
753
+ placements.push({
754
+ axis: "rows",
755
+ index: i,
756
+ 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;`
757
+ });
758
+ }
676
759
  }
760
+ while (this.strips.length > placements.length) this.strips.pop().remove();
761
+ while (this.strips.length < placements.length) this.strips.push(this.makeStrip());
762
+ for (const [k, p] of placements.entries()) this.place(this.strips[k], p.axis, p.index, p.css);
677
763
  }
678
764
  destroy() {
679
765
  for (const s of this.strips.splice(0)) s.remove();
@@ -747,8 +833,10 @@ var CSS = `
747
833
  .vela-ws-grid { position: relative; flex: 1 1 auto; min-width: 0; display: grid; gap: ${GAP_PX}px; background: var(--vela-border-soft); }
748
834
  .vela-cell { background: var(--vela-bg); position: relative; }
749
835
  /* 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 {
836
+ outline on the cell is painted under them) \u2014 inert to the pointer. Scoped to
837
+ multi-cell grids ([data-multi]): a single-cell layout always has an active cell,
838
+ and ringing the only chart would just be noise. */
839
+ .vela-ws-grid[data-multi='1'] .vela-cell[data-active='1']::after {
752
840
  content: '';
753
841
  position: absolute;
754
842
  inset: 0;
@@ -756,7 +844,12 @@ var CSS = `
756
844
  pointer-events: none;
757
845
  z-index: 10;
758
846
  }
759
- .vela-ws-splitter:hover { background: var(--vela-accent); opacity: 0.35; }
847
+ /* Splitter hover mirrors the in-chart pane separator hover (CrosshairRenderer):
848
+ a soft band over the whole grab target + a solid 2px line on the seam center. */
849
+ .vela-ws-splitter:hover { background: var(--vela-separator-hover-band); }
850
+ .vela-ws-splitter:hover::after { content: ''; position: absolute; background: var(--vela-separator-hover-line); }
851
+ .vela-ws-splitter[data-axis='cols']:hover::after { left: calc(50% - 1px); top: 0; width: 2px; height: 100%; }
852
+ .vela-ws-splitter[data-axis='rows']:hover::after { top: calc(50% - 1px); left: 0; height: 2px; width: 100%; }
760
853
  `;
761
854
  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
855
  function declaredOrder(cells) {
@@ -807,6 +900,15 @@ var VelaWorkspace = class {
807
900
  /** Re-entrance guard around one propagation tick: followers' synchronous echoes
808
901
  * (their setVisibleRange re-emits viewport:changed) must not re-propagate. */
809
902
  this.syncBusy = false;
903
+ /** Same guard for the drawings link: the propagated mutations' own `drawing:*`
904
+ * events fire synchronously inside the propagation loop and must not fan out again. */
905
+ this.drawingSyncBusy = false;
906
+ /** LINKED drawings (the drawings sync): one map per synced set (cellId → that
907
+ * cell's drawing id), reachable from every member under its `cellId\0drawingId`
908
+ * key — any member finds its peers to push edits/removals onto. Survives a
909
+ * toggle-off (propagation freezes while the setting is off; re-enabling resumes
910
+ * edit/delete for these pairs). Cleared on reload / `applyState`. */
911
+ this.drawingLinks = /* @__PURE__ */ new Map();
810
912
  this.manifest = [];
811
913
  /** The shared manifest can no longer change instance sets — resolved, or no
812
914
  * `indicators` option so nothing ever will. Gates the cells' ledger fallback. */
@@ -848,10 +950,10 @@ var VelaWorkspace = class {
848
950
  this.timezone = boot?.timezone ?? opts.timezone ?? "Etc/UTC";
849
951
  if (boot?.favorites) this.favs = [...boot.favorites];
850
952
  const sync = boot?.sync ?? opts.sync;
851
- for (const kind of ["viewport", "symbol", "timeframe", "crosshair"]) {
953
+ for (const kind of ["viewport", "symbol", "timeframe", "crosshair", "drawings"]) {
852
954
  this.applySyncSetting(kind, sync?.[kind]);
853
955
  }
854
- this.def = this.resolveLayout(boot?.layout && layoutDefinition(boot.layout) ? boot.layout : opts.layout ?? "4");
956
+ this.def = this.resolveLayout(boot?.layout && ensureLayout(boot.layout) ? boot.layout : opts.layout ?? "4");
855
957
  if (boot?.trackSizes) for (const [id, ts] of Object.entries(boot.trackSizes)) this.trackSizes.set(id, ts);
856
958
  if (boot?.charts) for (const { id, ...cs } of boot.charts) this.pool.set(id, cs);
857
959
  this.order = boot?.charts ? boot.charts.map((c) => c.id) : declaredOrder(opts.cells);
@@ -902,14 +1004,23 @@ var VelaWorkspace = class {
902
1004
  onPriceStyle: (style) => this.active.setPriceStyle(style),
903
1005
  layout: {
904
1006
  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);
1007
+ // The picker composes dynamic layouts on its grid canvas; registered
1008
+ // presets the canvas cannot express (bespoke plugin areas) list as rows.
1009
+ shape: () => layoutShape(this.def),
1010
+ presets: () => layouts().filter((l) => layoutShape(l) === null).map((l) => ({ id: l.id, label: l.label })),
1011
+ onSelectGrid: (rows, cols) => this.setLayout(layoutForGrid(rows, cols)),
1012
+ onSelectPreset: (id) => this.setLayout(id),
1013
+ // The SYNC switches reflect the simple all-cells form; flipping one
1014
+ // OVERRIDES a host-set group record with plain on/off (groups stay an
1015
+ // API-only shape).
1016
+ syncs: () => [
1017
+ { id: "symbol", label: "Symbol", checked: this.syncOpts.symbol === true },
1018
+ { id: "timeframe", label: "Interval", checked: this.syncOpts.timeframe === true },
1019
+ { id: "crosshair", label: "Crosshair", checked: this.syncOpts.crosshair === true }
1020
+ ],
1021
+ onToggleSync: (id) => {
1022
+ const kind = id;
1023
+ this.sync.set(kind, this.syncOpts[kind] ? false : true);
913
1024
  }
914
1025
  },
915
1026
  getContext: () => this.context()
@@ -972,10 +1083,19 @@ var VelaWorkspace = class {
972
1083
  this.active.chart.drawings.setStayMode(on);
973
1084
  this.refocusActive();
974
1085
  },
975
- { dock: "static" }
1086
+ {
1087
+ dock: "static",
1088
+ // Drawings sync is a WORKSPACE link (same model as the layout
1089
+ // dropdown's switches) — the bar only hosts its toggle.
1090
+ onDrawingsSync: (on) => {
1091
+ this.sync.set("drawings", on);
1092
+ this.refocusActive();
1093
+ }
1094
+ }
976
1095
  ) : null;
977
1096
  this.drawToolbar?.setDefinition(defaultToolbar());
978
1097
  this.drawToolbar?.setVisible(true);
1098
+ this.drawToolbar?.setDrawingsSyncMode(!!this.syncOpts.drawings);
979
1099
  this.bottombar = opts.bottombar !== false ? new Bottombar(this.root, {
980
1100
  timezone: this.timezone,
981
1101
  onRange: (preset) => {
@@ -988,6 +1108,7 @@ var VelaWorkspace = class {
988
1108
  hostEl.appendChild(this.root);
989
1109
  this.splitters = new SplitterLayer(this.gridEl, {
990
1110
  tracks: () => this.currentTracks(),
1111
+ grid: () => occupancyGrid(this.def),
991
1112
  apply: (axis, weights) => this.applyTracks(axis, weights),
992
1113
  reset: (axis) => this.applyTracks(axis, evenTracks(this.currentTracks()[axis].length)),
993
1114
  gapPx: () => GAP_PX
@@ -1135,7 +1256,7 @@ var VelaWorkspace = class {
1135
1256
  }
1136
1257
  if (st.favorites) this.favs = [...st.favorites];
1137
1258
  this.dock.applyState(st.panels);
1138
- for (const kind of ["viewport", "symbol", "timeframe", "crosshair"]) this.applySyncSetting(kind, st.sync?.[kind]);
1259
+ for (const kind of ["viewport", "symbol", "timeframe", "crosshair", "drawings"]) this.applySyncSetting(kind, st.sync?.[kind]);
1139
1260
  this.trackSizes.clear();
1140
1261
  if (st.trackSizes) for (const [id, ts] of Object.entries(st.trackSizes)) this.trackSizes.set(id, ts);
1141
1262
  for (const [id, cell] of [...this.cellsById]) {
@@ -1144,9 +1265,10 @@ var VelaWorkspace = class {
1144
1265
  this.events.emit("cell:destroyed", { id });
1145
1266
  }
1146
1267
  this.pool.clear();
1268
+ this.drawingLinks.clear();
1147
1269
  for (const { id, ...cs } of st.charts) this.pool.set(id, cs);
1148
1270
  this.order = st.charts.map((c) => c.id);
1149
- const def = layoutDefinition(st.layout);
1271
+ const def = ensureLayout(st.layout);
1150
1272
  if (def) this.def = def;
1151
1273
  this.cellBackend = this.backendFor(this.def);
1152
1274
  this.applyGrid();
@@ -1197,6 +1319,7 @@ var VelaWorkspace = class {
1197
1319
  const next = this.resolveLayout(layout);
1198
1320
  const nextBackend = this.backendFor(next);
1199
1321
  const rebuildAll = nextBackend !== this.cellBackend;
1322
+ this.order = orderAfterLayout(this.order, next.cells.length, this.activeId);
1200
1323
  const keep = new Set(this.order.slice(0, next.cells.length));
1201
1324
  for (const [id, cell] of [...this.cellsById]) {
1202
1325
  if (!keep.has(id) || rebuildAll) {
@@ -1320,7 +1443,7 @@ var VelaWorkspace = class {
1320
1443
  // ── internals ───────────────────────────────────────────────
1321
1444
  resolveLayout(layout) {
1322
1445
  if (typeof layout !== "string") return layout;
1323
- const def = layoutDefinition(layout);
1446
+ const def = ensureLayout(layout);
1324
1447
  if (!def) throw new Error(`[vela] unknown workspace layout "${layout}" \u2014 register it with registerLayout().`);
1325
1448
  return def;
1326
1449
  }
@@ -1348,6 +1471,8 @@ var VelaWorkspace = class {
1348
1471
  * `order[i]` — the identity/position decoupling in one line. */
1349
1472
  applyGrid() {
1350
1473
  const { container, perCell } = gridStyles(this.def, this.trackSizes.get(this.def.id));
1474
+ if (this.def.cells.length > 1) this.gridEl.dataset.multi = "1";
1475
+ else delete this.gridEl.dataset.multi;
1351
1476
  this.gridEl.style.gridTemplateColumns = container.gridTemplateColumns ?? "";
1352
1477
  this.gridEl.style.gridTemplateRows = container.gridTemplateRows ?? "";
1353
1478
  this.gridEl.style.gridTemplateAreas = container.gridTemplateAreas ?? "";
@@ -1395,6 +1520,7 @@ var VelaWorkspace = class {
1395
1520
  });
1396
1521
  cell.host.style.gridArea = perCell[slot.id]?.gridArea ?? "";
1397
1522
  this.cellsById.set(id, cell);
1523
+ if (id === this.activeId) cell.host.dataset.active = "1";
1398
1524
  this.wireCell(cell);
1399
1525
  if (this.favs.length > 0) cell.chart.drawings.setFavorites(this.favs);
1400
1526
  cell.setManifest(this.manifest, pooled?.indicators == null);
@@ -1426,9 +1552,19 @@ var VelaWorkspace = class {
1426
1552
  this.markStateDirty();
1427
1553
  });
1428
1554
  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());
1555
+ chart.on("drawing:created", ({ id }) => {
1556
+ this.propagateDrawing(cell.id, id);
1557
+ this.markStateDirty();
1558
+ });
1559
+ chart.on("drawing:draft", ({ doc }) => this.propagateDraft(cell.id, doc));
1560
+ chart.on("drawing:edited", ({ id }) => {
1561
+ this.propagateDrawingEdit(cell.id, id);
1562
+ this.markStateDirty();
1563
+ });
1564
+ chart.on("drawing:removed", ({ id }) => {
1565
+ this.propagateDrawingRemoval(cell.id, id);
1566
+ this.markStateDirty();
1567
+ });
1432
1568
  chart.on("drawing:tool", ({ type }) => {
1433
1569
  if (cell.id !== this.activeId) return;
1434
1570
  this.globalTool = type;
@@ -1456,6 +1592,12 @@ var VelaWorkspace = class {
1456
1592
  applySyncSetting(kind, setting, align = false) {
1457
1593
  if (setting == null || setting === false) delete this.syncOpts[kind];
1458
1594
  else this.syncOpts[kind] = setting;
1595
+ if (kind === "drawings") {
1596
+ for (const cell of this.cellsById.values()) cell.chart.drawings.setExternalGhost(null);
1597
+ this.drawToolbar?.setDrawingsSyncMode(!!setting);
1598
+ this.markStateDirty();
1599
+ return;
1600
+ }
1459
1601
  if (kind === "crosshair") {
1460
1602
  for (const cell of this.cellsById.values()) cell.chart.renderer.setExternalCrosshair(null);
1461
1603
  if (setting && ![...this.cellsById.values()].some((c) => c.chart.renderer.supportsExternalCrosshair)) {
@@ -1465,6 +1607,7 @@ var VelaWorkspace = class {
1465
1607
  this.markStateDirty();
1466
1608
  return;
1467
1609
  }
1610
+ if (this.topbar && this.def) this.topbar.setLayout(this.def.id);
1468
1611
  this.markStateDirty();
1469
1612
  if (align && setting && this.activeId) {
1470
1613
  if (kind === "viewport") {
@@ -1516,6 +1659,101 @@ var VelaWorkspace = class {
1516
1659
  this.syncBusy = false;
1517
1660
  }
1518
1661
  }
1662
+ /**
1663
+ * Copy a freshly created drawing onto the origin's same-group followers (fresh ids
1664
+ * through `drawings.add` — anchors are time+price, so the copy lands at the same
1665
+ * spot whatever the follower shows) and register the whole set as LINKED: edits
1666
+ * and removals of any member follow while the link is on ({@link drawingLinks}).
1667
+ * The busy guard stops the copies' own `drawing:created` events from fanning out.
1668
+ */
1669
+ propagateDrawing(originId, drawingId) {
1670
+ if (this.drawingSyncBusy || this.destroyed) return;
1671
+ const setting = this.syncOpts.drawings;
1672
+ if (!setting) return;
1673
+ const doc = this.cellsById.get(originId)?.chart.drawings.all().find((d) => d.id === drawingId);
1674
+ if (!doc) return;
1675
+ this.drawingSyncBusy = true;
1676
+ try {
1677
+ const group = /* @__PURE__ */ new Map([[originId, drawingId]]);
1678
+ for (const id of syncTargets(originId, setting, [...this.cellsById.keys()])) {
1679
+ const copy = this.cellsById.get(id)?.chart.drawings.add(doc.type, {
1680
+ paneId: doc.paneId,
1681
+ anchors: doc.anchors,
1682
+ style: doc.style,
1683
+ text: doc.text,
1684
+ props: doc.props
1685
+ });
1686
+ if (copy) group.set(id, copy.id);
1687
+ }
1688
+ if (group.size > 1) {
1689
+ for (const [cellId, dId] of group) this.drawingLinks.set(`${cellId}\0${dId}`, group);
1690
+ }
1691
+ } finally {
1692
+ this.drawingSyncBusy = false;
1693
+ }
1694
+ }
1695
+ /** Mirror an in-progress placement (its current ghost, `null` = placement ended)
1696
+ * onto the origin's same-group followers — the live half of the drawings link;
1697
+ * the created copy replaces the ghosts when the placement completes. One-way by
1698
+ * contract (an external ghost never re-emits drafts), so no busy guard needed. */
1699
+ propagateDraft(originId, doc) {
1700
+ if (this.destroyed) return;
1701
+ const setting = this.syncOpts.drawings;
1702
+ if (!setting) return;
1703
+ for (const id of syncTargets(originId, setting, [...this.cellsById.keys()])) {
1704
+ this.cellsById.get(id)?.chart.drawings.setExternalGhost(doc);
1705
+ }
1706
+ }
1707
+ /** Push a linked drawing's edited CONTENT (anchors, style, text, per-type props)
1708
+ * onto its same-group peers — any member propagates, not just the original. */
1709
+ propagateDrawingEdit(originId, drawingId) {
1710
+ if (this.drawingSyncBusy || this.destroyed) return;
1711
+ const setting = this.syncOpts.drawings;
1712
+ if (!setting) return;
1713
+ const group = this.drawingLinks.get(`${originId}\0${drawingId}`);
1714
+ if (!group) return;
1715
+ const doc = this.cellsById.get(originId)?.chart.drawings.all().find((d) => d.id === drawingId);
1716
+ if (!doc) return;
1717
+ this.drawingSyncBusy = true;
1718
+ try {
1719
+ for (const id of syncTargets(originId, setting, [...this.cellsById.keys()])) {
1720
+ const peerId = group.get(id);
1721
+ if (peerId == null) continue;
1722
+ this.cellsById.get(id)?.chart.drawings.update(peerId, {
1723
+ anchors: doc.anchors,
1724
+ style: doc.style,
1725
+ text: doc.text,
1726
+ props: doc.props
1727
+ });
1728
+ }
1729
+ } finally {
1730
+ this.drawingSyncBusy = false;
1731
+ }
1732
+ }
1733
+ /** Remove a linked drawing's peers with it. The removed member always leaves its
1734
+ * link group (whatever the setting); peers are only deleted while the link is on.
1735
+ * A propagated peer removal re-enters here under the busy guard and just cleans
1736
+ * its own link key. */
1737
+ propagateDrawingRemoval(originId, drawingId) {
1738
+ if (this.destroyed) return;
1739
+ const key = `${originId}\0${drawingId}`;
1740
+ const group = this.drawingLinks.get(key);
1741
+ if (!group) return;
1742
+ this.drawingLinks.delete(key);
1743
+ group.delete(originId);
1744
+ if (this.drawingSyncBusy) return;
1745
+ const setting = this.syncOpts.drawings;
1746
+ if (!setting) return;
1747
+ this.drawingSyncBusy = true;
1748
+ try {
1749
+ for (const id of syncTargets(originId, setting, [...this.cellsById.keys()])) {
1750
+ const peerId = group.get(id);
1751
+ if (peerId != null) this.cellsById.get(id)?.chart.drawings.remove(peerId);
1752
+ }
1753
+ } finally {
1754
+ this.drawingSyncBusy = false;
1755
+ }
1756
+ }
1519
1757
  /**
1520
1758
  * Push an origin cell's symbol/timeframe onto its same-group followers (fired from
1521
1759
  * `market:changed`). Convergence comes from IDEMPOTENCE, not the guard: a follower's
@@ -1691,4 +1929,4 @@ var VelaWorkspace = class {
1691
1929
  }
1692
1930
  };
1693
1931
 
1694
- export { ChartCell, VelaWorkspace, activeAfterLayout, evenTracks, gridStyles, layoutDefinition, layouts, memoryStorageAdapter, rangesWithin, registerBuiltinLayouts, registerLayout, resizeTracks, syncTargets, trackOffsets, unregisterLayout };
1932
+ export { ChartCell, GRID_PICKER_MAX, VelaWorkspace, activeAfterLayout, ensureLayout, evenTracks, gridStyles, layoutDefinition, layoutForGrid, layoutShape, layouts, memoryStorageAdapter, rangesWithin, registerBuiltinLayouts, registerLayout, resizeTracks, syncTargets, trackOffsets, unregisterLayout };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@luxalgo/vela",
3
- "version": "0.5.0",
3
+ "version": "0.5.1",
4
4
  "description": "Open-source charting library with a native high-performance renderer, drawing tools, pluggable chart types and pluggable scripting engines.",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://luxalgo.com/vela",