@luxalgo/vela 0.8.1 → 0.8.3

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 (49) hide show
  1. package/README.md +1 -1
  2. package/dist/{DataProvider-DwFIoV4r.d.ts → DataProvider-oH78tYXL.d.ts} +1 -1
  3. package/dist/{DataProvider-BrlxntPN.d.cts → DataProvider-wbtJ3-Sa.d.cts} +1 -1
  4. package/dist/{chunk-DBDXKZUF.js → chunk-2IPXYM4B.js} +306 -177
  5. package/dist/{chunk-NELQJCGK.js → chunk-G7B7ZCBF.js} +167 -10
  6. package/dist/{chunk-ATGTCWHJ.js → chunk-HMHA733L.js} +473 -196
  7. package/dist/{chunk-EZ5FWVLA.js → chunk-KWI3YDBW.js} +343 -17
  8. package/dist/{chunk-LMBYEGUR.js → chunk-TO3GIK7Z.js} +71 -6
  9. package/dist/{chunk-BZQM2XO7.js → chunk-VHGACEHO.js} +62 -1
  10. package/dist/{contributions-tjNExo1o.d.ts → contributions-BhOBRenr.d.cts} +79 -12
  11. package/dist/{contributions-CtlptxR8.d.cts → contributions-HGqnNb_P.d.ts} +79 -12
  12. package/dist/index.cjs +733 -113
  13. package/dist/index.d.cts +34 -14
  14. package/dist/index.d.ts +34 -14
  15. package/dist/index.js +5 -5
  16. package/dist/{options-BnEPUuVV.d.ts → options-lMUwmRwh.d.cts} +69 -4
  17. package/dist/{options-BnEPUuVV.d.cts → options-lMUwmRwh.d.ts} +69 -4
  18. package/dist/{plugin-e2WUEtiI.d.ts → plugin-Cr2Pj8q8.d.ts} +4 -4
  19. package/dist/{plugin-BWxJRKzS.d.cts → plugin-Do5J5xuF.d.cts} +4 -4
  20. package/dist/plugin.cjs +38 -6
  21. package/dist/plugin.d.cts +5 -5
  22. package/dist/plugin.d.ts +5 -5
  23. package/dist/plugin.js +3 -3
  24. package/dist/providers/binance.d.cts +2 -2
  25. package/dist/providers/binance.d.ts +2 -2
  26. package/dist/providers/coinbase.d.cts +2 -2
  27. package/dist/providers/coinbase.d.ts +2 -2
  28. package/dist/providers/hyperliquid.d.cts +2 -2
  29. package/dist/providers/hyperliquid.d.ts +2 -2
  30. package/dist/{side-panel-HF0IAzwf.d.cts → side-panel-_2UK1gSm.d.cts} +58 -3
  31. package/dist/{side-panel-HF0IAzwf.d.ts → side-panel-_2UK1gSm.d.ts} +58 -3
  32. package/dist/{statusline-model-DFY7tuHj.d.cts → statusline-model-B-2xGlB_.d.cts} +3 -3
  33. package/dist/{statusline-model-zKXtGIjj.d.ts → statusline-model-CY1wzxHg.d.ts} +3 -3
  34. package/dist/ui.cjs +298 -13
  35. package/dist/ui.d.cts +132 -74
  36. package/dist/ui.d.ts +132 -74
  37. package/dist/ui.js +3 -3
  38. package/dist/vela.global.js +734 -114
  39. package/dist/vela.global.min.js +86 -52
  40. package/dist/widget.cjs +1227 -211
  41. package/dist/widget.d.cts +36 -12
  42. package/dist/widget.d.ts +36 -12
  43. package/dist/widget.js +7 -7
  44. package/dist/workspace.cjs +1226 -211
  45. package/dist/workspace.d.cts +47 -20
  46. package/dist/workspace.d.ts +47 -20
  47. package/dist/workspace.js +6 -6
  48. package/package.json +2 -2
  49. /package/dist/{chunk-JU4CWEBF.js → chunk-W5YPYZ2P.js} +0 -0
@@ -1,8 +1,8 @@
1
- import { parseSymbol, priceStyleIds, BUILTIN_PRICE_STYLES, tzButtonLabel, SecondClock, resolveTimezone, timezoneMenuRows, isGroupRow, defaultMemberOf, groupKeyOf, groupMembers, LEGEND_AT_TOP_ATTR, normalizeSession, Vela, normalizeTimezone, isExchangeTimezone, TypedEventBus, timeframeToMs, MultiProviderFeed, registerBuiltinChartTypes, resolveTheme, buildToolbar, createCustomMark, createAttributionMark, DrawingToolbar, applyAttributionMarkTheme, sharedBarStore } from './chunk-ATGTCWHJ.js';
2
- import { chartType, resolveTopbarComposition, topbarActionOverride, TOPBAR_BUILTIN_IDS, widgetActions, SidePanel, sidePanels, DEFAULT_PANEL_ORDER, symbolRanking, resolveEngines, legendActionsProviderFor, legendCalloutsProviderFor, statePersistenceHandlers, topbarHas, rendererDefaults, mobilePlacement, widgetAttachments, getDrawingType, inputDeltas } from './chunk-EZ5FWVLA.js';
3
- import { Tooltip, KeymapManager, isEditableTarget, Drawer } from './chunk-LMBYEGUR.js';
4
- import { Menu, Dialog, CalloutBubble, applyPlotOverlayTokens, ensureUIHost } from './chunk-NELQJCGK.js';
5
- import { registerIcon, svg16, injectStyles, iconEl, iconMarkup, iconAt, SESSION_OFF, SESSION_POST, SESSION_PRE, icon, categoricalColor } from './chunk-BZQM2XO7.js';
1
+ import { parseSymbol, priceStyleIds, BUILTIN_PRICE_STYLES, SecondClock, isGroupRow, defaultMemberOf, groupKeyOf, groupMembers, LEGEND_AT_TOP_ATTR, normalizeSession, Vela, TypedEventBus, timeframeToMs, MultiProviderFeed, registerBuiltinChartTypes, resolveTheme, buildToolbar, createCustomMark, createAttributionMark, DrawingToolbar, applyAttributionMarkTheme, sharedBarStore, CANDLE_OVERRIDE_KEYS } from './chunk-HMHA733L.js';
2
+ import { chartType, resolveTopbarComposition, topbarActionOverride, TOPBAR_BUILTIN_IDS, widgetActions, tzButtonLabel, resolveTimezone, timezoneMenuRows, SidePanel, sidePanels, DEFAULT_PANEL_ORDER, symbolRanking, paneScaleAt, composeMenu, priceAxisItems, scaleChoiceOf, timeAxisItems, bodyItems, scaleWrites, invertWrite, settingsSectionOf, resolveEngines, legendActionsProviderFor, legendCalloutsProviderFor, normalizeTimezone, isExchangeTimezone, statePersistenceHandlers, topbarHas, rendererDefaults, mobilePlacement, widgetAttachments, getDrawingType, inputDeltas } from './chunk-KWI3YDBW.js';
3
+ import { Tooltip, KeymapManager, isEditableTarget, Drawer } from './chunk-TO3GIK7Z.js';
4
+ import { Menu, Dialog, CalloutBubble, applyPlotOverlayTokens, ensureUIHost } from './chunk-G7B7ZCBF.js';
5
+ import { registerIcon, svg16, injectStyles, iconEl, iconMarkup, iconAt, SESSION_OFF, SESSION_POST, SESSION_PRE, announceSurface, holdForExit, icon, categoricalColor } from './chunk-VHGACEHO.js';
6
6
  import { baseOf } from './chunk-W4EJWLEO.js';
7
7
 
8
8
  // src/widget/timeframe.ts
@@ -108,11 +108,11 @@ function timeframeLabel(value) {
108
108
  }
109
109
 
110
110
  // src/widget/layout-picker.ts
111
- var STYLE_ID = "vela-widget-layout-picker-v14";
111
+ var STYLE_ID = "vela-widget-layout-picker-v15";
112
112
  var CSS = `
113
113
  .vela-lp-layer { position: absolute; z-index: var(--vela-z-menu); }
114
114
  .vela-lp {
115
- background: var(--vela-surface-elev);
115
+ background: var(--vela-surface);
116
116
  color: var(--vela-fg);
117
117
  border: 1px solid var(--vela-border-strong);
118
118
  border-radius: 8px;
@@ -241,6 +241,8 @@ var LayoutPicker = class {
241
241
  constructor(opts) {
242
242
  this.squares = [];
243
243
  this.isOpen = false;
244
+ /** The card's exit animation after a close; a reopen cancels it. */
245
+ this.exit = null;
244
246
  /** Hover preview (1-based rows/cols), null = show current shape. */
245
247
  this.hover = null;
246
248
  this.onDocPointerDown = (e) => {
@@ -261,6 +263,7 @@ var LayoutPicker = class {
261
263
  const panel = doc.createElement("div");
262
264
  panel.className = "vela-lp";
263
265
  this.layer.appendChild(panel);
266
+ this.panel = panel;
264
267
  const cols = doc.createElement("div");
265
268
  cols.className = "vela-lp-cols";
266
269
  panel.appendChild(cols);
@@ -334,6 +337,8 @@ var LayoutPicker = class {
334
337
  open() {
335
338
  if (this.isOpen) return;
336
339
  this.isOpen = true;
340
+ this.exit?.cancel();
341
+ this.exit = null;
337
342
  this.hover = null;
338
343
  this.refresh();
339
344
  this.layer.style.display = "";
@@ -342,11 +347,16 @@ var LayoutPicker = class {
342
347
  this.doc.addEventListener("pointerdown", this.onDocPointerDown, true);
343
348
  this.doc.addEventListener("keydown", this.onDocKeydown, true);
344
349
  this.opts.onOpenChange?.(true);
350
+ if (this.isOpen) announceSurface(this.panel, true, "popover", this.opts.trigger);
345
351
  }
346
352
  close() {
347
353
  if (!this.isOpen) return;
348
354
  this.isOpen = false;
349
- this.layer.style.display = "none";
355
+ announceSurface(this.panel, false, "popover", this.opts.trigger);
356
+ this.exit = holdForExit(this.panel, () => {
357
+ this.exit = null;
358
+ if (!this.isOpen) this.layer.style.display = "none";
359
+ });
350
360
  this.opts.trigger.setAttribute("aria-expanded", "false");
351
361
  this.doc.removeEventListener("pointerdown", this.onDocPointerDown, true);
352
362
  this.doc.removeEventListener("keydown", this.onDocKeydown, true);
@@ -361,6 +371,7 @@ var LayoutPicker = class {
361
371
  }
362
372
  destroy() {
363
373
  this.close();
374
+ this.exit?.finish();
364
375
  this.infoTip.destroy();
365
376
  this.layer.remove();
366
377
  }
@@ -427,7 +438,7 @@ var LayoutPicker = class {
427
438
  const hostRect = this.opts.host.getBoundingClientRect();
428
439
  const trigRect = this.opts.trigger.getBoundingClientRect();
429
440
  let left = trigRect.left - hostRect.left;
430
- const top = trigRect.bottom - hostRect.top + 4;
441
+ const top = trigRect.bottom - hostRect.top + 8;
431
442
  const width = this.layer.offsetWidth;
432
443
  if (left + width > hostRect.width - 8) left = Math.max(8, hostRect.width - 8 - width);
433
444
  this.layer.style.left = `${left}px`;
@@ -780,7 +791,8 @@ var Topbar = class {
780
791
  triggerId: "vela-topbar-style",
781
792
  host,
782
793
  items: this.styleItems(),
783
- onSelect: (id) => opts.onPriceStyle(id)
794
+ onSelect: (id) => opts.onPriceStyle(id),
795
+ iconBadges: true
784
796
  });
785
797
  }
786
798
  setSymbol(symbol) {
@@ -846,8 +858,14 @@ var Topbar = class {
846
858
  renderLayoutButton(doc) {
847
859
  if (!this.layoutButton) return;
848
860
  this.layoutButton.replaceChildren();
849
- if (iconMarkup("layout")) this.layoutButton.appendChild(iconEl("layout", doc));
850
- else this.layoutButton.appendChild(doc.createTextNode(this.layoutId ?? ""));
861
+ const glyph = this.opts.layout?.glyph?.();
862
+ if (glyph || iconMarkup("layout")) {
863
+ const icon2 = iconEl("layout", doc);
864
+ if (glyph) icon2.innerHTML = glyph;
865
+ this.layoutButton.appendChild(icon2);
866
+ } else {
867
+ this.layoutButton.appendChild(doc.createTextNode(this.layoutId ?? ""));
868
+ }
851
869
  this.layoutButton.setAttribute("aria-label", `Layout \u2014 ${this.layoutId ?? ""}`);
852
870
  }
853
871
  renderStyleButton(doc) {
@@ -1701,8 +1719,8 @@ var ObjectTree = class extends SidePanel {
1701
1719
  set groups(next) {
1702
1720
  if (this.chart) this.groupsPerChart.set(this.chart, next);
1703
1721
  }
1704
- toggle(open = this.el.hidden) {
1705
- super.toggle(open);
1722
+ toggle(open = !this.open, instant = false) {
1723
+ super.toggle(open, instant);
1706
1724
  if (open) this.refresh();
1707
1725
  }
1708
1726
  setSymbol(symbol) {
@@ -2736,8 +2754,8 @@ var DataWindow = class extends SidePanel {
2736
2754
  this.unsubs = [];
2737
2755
  injectStyles(STYLE_ID5, CSS5, host.ownerDocument);
2738
2756
  }
2739
- toggle(open = this.el.hidden) {
2740
- super.toggle(open);
2757
+ toggle(open = !this.open, instant = false) {
2758
+ super.toggle(open, instant);
2741
2759
  if (open) this.refresh();
2742
2760
  }
2743
2761
  /** (Re)bind to a chart instance — called after every widget rebuild. */
@@ -2813,19 +2831,22 @@ var PanelDock = class {
2813
2831
  /**
2814
2832
  * (Re)build the CONTRIBUTED panels from the registry — call once after the built-ins, and
2815
2833
  * again on `refreshActions()` so a late registration appears. Contributed panels that are
2816
- * gone from the registry are dropped; the ones still there are rebuilt, so a replaced
2817
- * descriptor takes effect.
2834
+ * gone from the registry are dropped and a REPLACED descriptor is rebuilt; a panel whose
2835
+ * descriptor is unchanged stays mounted as it is, so its content and state survive.
2818
2836
  */
2819
2837
  refresh() {
2820
2838
  const openBefore = this.openId;
2821
- for (const entry of [...this.entries]) if (entry.contributed) this.drop(entry);
2822
- for (const desc of sidePanels()) {
2839
+ const registered = sidePanels();
2840
+ for (const entry of [...this.entries]) if (entry.contributed && !registered.includes(entry.desc)) this.drop(entry);
2841
+ for (const desc of registered) {
2842
+ if (this.entries.some((e) => e.desc === desc)) continue;
2823
2843
  const panel = new SidePanel(this.host, desc.title, `vela-panel-${desc.id}`, {
2824
2844
  width: desc.width,
2825
2845
  resizable: desc.resizable,
2826
2846
  minWidth: desc.minWidth,
2827
2847
  maxWidth: desc.maxWidth,
2828
- overlay: desc.overlay
2848
+ overlay: desc.overlay,
2849
+ maximizable: desc.maximizable
2829
2850
  });
2830
2851
  const entry = {
2831
2852
  id: desc.id,
@@ -2833,10 +2854,19 @@ var PanelDock = class {
2833
2854
  icon: desc.icon,
2834
2855
  order: desc.order ?? DEFAULT_PANEL_ORDER,
2835
2856
  panel,
2836
- contributed: true
2857
+ contributed: true,
2858
+ desc
2837
2859
  };
2838
2860
  try {
2839
- entry.handle = desc.mount(this.deps.context(), panel.content, { slot: panel.headerSlot, setTitle: (t) => panel.setTitle(t) }) ?? void 0;
2861
+ const header = {
2862
+ slot: panel.headerSlot,
2863
+ setTitle: (t) => panel.setTitle(t),
2864
+ get maximized() {
2865
+ return panel.maximized;
2866
+ },
2867
+ setMaximized: (on) => panel.setMaximized(on)
2868
+ };
2869
+ entry.handle = desc.mount(this.deps.context(), panel.content, header) ?? void 0;
2840
2870
  if (this.chart) entry.handle?.onChart?.(this.chart);
2841
2871
  } catch (err) {
2842
2872
  console.warn(`[vela] side panel "${desc.id}" failed to mount`, err);
@@ -2895,7 +2925,7 @@ var PanelDock = class {
2895
2925
  }
2896
2926
  this.pinned = new Set(state.pinned ?? []);
2897
2927
  for (const entry of this.entries) entry.panel.setOverlay(!this.pinned.has(entry.id));
2898
- for (const entry of this.entries) entry.panel.toggle(entry.id === state.open);
2928
+ for (const entry of this.entries) entry.panel.toggle(entry.id === state.open, true);
2899
2929
  this.pendingOpen = state.open && !this.entries.some((e) => e.id === state.open) ? state.open : null;
2900
2930
  }
2901
2931
  /** Drop the contributed panels (the shell destroys its own). */
@@ -2911,7 +2941,7 @@ var PanelDock = class {
2911
2941
  if (this.pinned.has(entry.id)) entry.panel.setOverlay(false);
2912
2942
  entry.panel.onOpenChange = (open) => {
2913
2943
  if (open) {
2914
- for (const other of this.entries) if (other !== entry) other.panel.toggle(false);
2944
+ for (const other of this.entries) if (other !== entry) other.panel.toggle(false, true);
2915
2945
  this.pendingOpen = null;
2916
2946
  }
2917
2947
  this.deps.chrome.setPanelActive(entry.id, open);
@@ -4027,15 +4057,48 @@ function syncTargets(originId, setting, cellIds) {
4027
4057
  function rangesWithin(a, b, epsMs) {
4028
4058
  return Math.abs(a.from - b.from) <= epsMs && Math.abs(a.to - b.to) <= epsMs;
4029
4059
  }
4030
- var STYLE_SYNC_CONFIG_KEYS = ["layout", "panes", "grid", "priceScale", "crosshair"];
4031
- function styleConfigSlice(config) {
4032
- if (config == null || typeof config !== "object") return null;
4033
- const doc = config;
4034
- const out = {};
4060
+ var STYLE_SYNC_CONFIG_KEYS = [
4061
+ "layout",
4062
+ "panes",
4063
+ "grid",
4064
+ "margins",
4065
+ "priceScale",
4066
+ "crosshair",
4067
+ "animations",
4068
+ "candles",
4069
+ "bars",
4070
+ "line",
4071
+ "area",
4072
+ "baseline",
4073
+ "sessions"
4074
+ ];
4075
+ function styleConfigPatch(origin, follower) {
4076
+ const from = asRecord(origin);
4077
+ const to = asRecord(follower);
4078
+ if (!from || !to) return null;
4079
+ const patch = {};
4035
4080
  for (const key of STYLE_SYNC_CONFIG_KEYS) {
4036
- if (doc[key] != null && typeof doc[key] === "object") out[key] = doc[key];
4081
+ const block = asRecord(from[key]);
4082
+ if (block && JSON.stringify(block) !== JSON.stringify(to[key])) patch[key] = block;
4037
4083
  }
4038
- return Object.keys(out).length > 0 ? out : null;
4084
+ const spacing = asRecord(from.series)?.spacing;
4085
+ if (typeof spacing === "number" && spacing !== asRecord(to.series)?.spacing) patch.series = { spacing };
4086
+ const fromTypes = asRecord(from.chartTypes) ?? {};
4087
+ const toTypes = asRecord(to.chartTypes) ?? {};
4088
+ const types = {};
4089
+ for (const typeId of /* @__PURE__ */ new Set([...Object.keys(fromTypes), ...Object.keys(toTypes)])) {
4090
+ const src = asRecord(fromTypes[typeId]) ?? {};
4091
+ const dst = asRecord(toTypes[typeId]) ?? {};
4092
+ for (const key of CANDLE_OVERRIDE_KEYS) {
4093
+ const value = src[key] ?? null;
4094
+ if (value !== (dst[key] ?? null)) (types[typeId] ?? (types[typeId] = {}))[key] = value;
4095
+ }
4096
+ }
4097
+ if (Object.keys(types).length > 0) patch.chartTypes = types;
4098
+ return Object.keys(patch).length > 0 ? patch : null;
4099
+ }
4100
+ function asRecord(v) {
4101
+ return v != null && typeof v === "object" ? v : null;
4039
4102
  }
4040
4103
 
4041
4104
  // src/workspace/persist.ts
@@ -4223,6 +4286,9 @@ var CSS10 = `
4223
4286
  .vela-statusline .vela-sl-market { align-self: center; display: inline-flex; }
4224
4287
  .vela-statusline .vela-sl-market > [hidden] { display: none !important; }
4225
4288
  .vela-statusline .vela-sl-replay-badge, .vela-statusline .vela-sl-replay-badge svg { display: block; width: 16px; height: 16px; }
4289
+ /* Tabular figures: every digit has one advance, so a readout whose text changes only in
4290
+ * its digits keeps its width \u2014 render() skips the ladder for those (see widthKey). */
4291
+ .vela-statusline .vela-sl-values { font-variant-numeric: tabular-nums; }
4226
4292
  .vela-statusline .vela-sl-ohlc { display: flex; gap: var(--vela-space-1); color: var(--vela-fg-muted); }
4227
4293
  .vela-statusline .vela-sl-ohlc b { color: var(--vela-fg); font-weight: 500; }
4228
4294
  /* The change value wears the SAME ink as the OHLC values (set inline per render) \u2014
@@ -4340,6 +4406,10 @@ var Statusline = class {
4340
4406
  this.fitRO = null;
4341
4407
  /** The ladder rung currently applied — see {@link fit}. */
4342
4408
  this.layout = { stacked: false, level: "full" };
4409
+ /** The readout {@link widthKey} the applied rung was fitted to. */
4410
+ this.fitKey = null;
4411
+ /** Web fonts landing after the last fit change every width without a resize. */
4412
+ this.onFontsLoaded = () => this.fit();
4343
4413
  this.onContextMenu = (e) => {
4344
4414
  if (!this.menu || !this.menuHooks) return;
4345
4415
  e.preventDefault();
@@ -4403,6 +4473,7 @@ var Statusline = class {
4403
4473
  this.fitRO = new ResizeObserver(() => this.fit());
4404
4474
  this.fitRO.observe(host);
4405
4475
  }
4476
+ doc.fonts?.addEventListener("loadingdone", this.onFontsLoaded);
4406
4477
  this.marketTip = new Tooltip(this.marketEl, { content: MARKET_LABELS.open, placement: "bottom" });
4407
4478
  this.eyeTip = new Tooltip(this.eyeEl, { content: "Show chart", placement: "bottom" });
4408
4479
  this.setMarketStatus("open");
@@ -4455,6 +4526,7 @@ var Statusline = class {
4455
4526
  * (detached host, node tests) nothing overflows, so the widest rung stays.
4456
4527
  */
4457
4528
  fit() {
4529
+ this.fitKey = this.widthKey();
4458
4530
  this.syncParts();
4459
4531
  const seg = segmentVisibility(this.parts, this.chartHidden);
4460
4532
  const overflows = () => this.el.scrollWidth > this.el.clientWidth;
@@ -4611,6 +4683,7 @@ var Statusline = class {
4611
4683
  this.detach();
4612
4684
  this.fitRO?.disconnect();
4613
4685
  this.fitRO = null;
4686
+ this.el.ownerDocument.fonts?.removeEventListener("loadingdone", this.onFontsLoaded);
4614
4687
  this.el.removeEventListener("contextmenu", this.onContextMenu);
4615
4688
  this.menu?.destroy();
4616
4689
  this.menu = null;
@@ -4626,7 +4699,22 @@ var Statusline = class {
4626
4699
  }
4627
4700
  render() {
4628
4701
  if (this.menuHooks) this.setChartHidden(!this.menuHooks.chartVisible());
4629
- this.fit();
4702
+ if (this.widthKey() === this.fitKey) this.renderValues();
4703
+ else this.fit();
4704
+ }
4705
+ /** Everything the readout's width depends on, at every rung: its shape, and each
4706
+ * value's sign, digit count and decimals (the digits themselves are tabular — see
4707
+ * the CSS; the grouping commas follow from the digit count). `toFixed` carries all
4708
+ * three at a fraction of the locale formatter's cost — this runs per pointer move.
4709
+ * The parts, the host's size and the fonts re-fit through their own paths. */
4710
+ widthKey() {
4711
+ const bar = this.hoverBar ?? this.lastBar;
4712
+ if (!bar) return this.readout;
4713
+ const dp = decimalsFor(bar.close);
4714
+ const diff = bar.close - bar.open;
4715
+ const text = [bar.open, bar.high, bar.low, bar.close, diff].map((v) => v.toFixed(dp));
4716
+ text.push((diff / bar.open * 100).toFixed(2));
4717
+ return `${this.readout} ${text.join(" ")}`.replace(/\d/g, "0");
4630
4718
  }
4631
4719
  /** Write the value readout for the current bar at the current ladder level. */
4632
4720
  renderValues() {
@@ -4772,113 +4860,28 @@ var Watermark = class {
4772
4860
  }
4773
4861
  };
4774
4862
 
4775
- // src/widget/context-menu-model.ts
4776
- var SETTINGS_SECTION = {
4777
- body: "Canvas",
4778
- "price-axis": "Scales and lines",
4779
- "time-axis": "Scales and lines"
4780
- };
4781
- function settingsItem(zone, label) {
4782
- return { id: `settings:${SETTINGS_SECTION[zone]}`, label, separatorBefore: true };
4783
- }
4784
- function settingsSectionOf(id) {
4785
- return id.slice("settings:".length) || void 0;
4786
- }
4787
- var SCALE_CHOICES = [
4788
- ["regular", "Regular"],
4789
- ["percent", "Percent"],
4790
- ["indexed", "Indexed to 100"],
4791
- ["log", "Logarithmic"]
4792
- ];
4793
- function paneScaleAt(panes, y) {
4794
- return panes.find((p) => y >= p.top && y < p.top + p.height) ?? panes[0] ?? null;
4795
- }
4796
- function scaleChoiceOf(scale) {
4797
- if (scale.log) return "log";
4798
- if (scale.mode === "percent") return "percent";
4799
- if (scale.mode === "indexed") return "indexed";
4800
- return "regular";
4801
- }
4802
- function mainScale(pane) {
4803
- return pane === null || pane.kind === "price";
4804
- }
4805
- function scaleWrites(choice, pane) {
4806
- const mode = choice === "percent" ? "percent" : choice === "indexed" ? "indexed" : "price";
4807
- const log = choice === "log";
4808
- if (mainScale(pane)) {
4809
- return [
4810
- ["scaleMode", mode],
4811
- ["logScale", log]
4812
- ];
4813
- }
4814
- return [
4815
- ["scaleMode", { pane: pane.id, mode }],
4816
- ["logScale", { pane: pane.id, value: log }]
4817
- ];
4818
- }
4819
- function invertWrite(next, pane) {
4820
- return mainScale(pane) ? ["invertScale", next] : ["invertScale", { pane: pane.id, value: next }];
4821
- }
4822
- function priceAxisItems(s) {
4823
- return [
4824
- { id: "auto", label: "Auto (fits data to screen)", checked: s.auto },
4825
- { id: "invert", label: "Invert scale", checked: s.invert },
4826
- ...SCALE_CHOICES.map(([choice, label], i) => ({
4827
- id: `scale:${choice}`,
4828
- label,
4829
- checked: s.choice === choice,
4830
- separatorBefore: i === 0
4831
- })),
4832
- {
4833
- id: "labels",
4834
- label: "Labels",
4835
- separatorBefore: true,
4836
- submenu: [
4837
- { id: "toggle:axisLabels", label: "Price axis labels", checked: s.axisLabels },
4838
- { id: "toggle:priceLabel", label: "Last price label", checked: s.priceLabel },
4839
- { id: "toggle:countdown", label: "Countdown to bar close", checked: s.countdown }
4840
- ]
4841
- },
4842
- {
4843
- id: "levels",
4844
- label: "Levels",
4845
- submenu: [{ id: "toggle:currentPriceLine", label: "Last Price Line", checked: s.priceLine }]
4846
- },
4847
- settingsItem("price-axis", "More settings\u2026")
4848
- ];
4849
- }
4850
- function timeAxisItems(timezone) {
4851
- return [
4852
- {
4853
- id: "timezone",
4854
- label: "Time zone",
4855
- submenu: timezoneMenuRows(timezone).map((r) => ({ id: `tz:${r.value}`, label: r.label, checked: r.checked }))
4856
- },
4857
- settingsItem("time-axis", "More settings\u2026")
4858
- ];
4859
- }
4860
- function bodyItems(counts) {
4861
- return [
4862
- { id: "reset-view", label: "Reset chart view" },
4863
- { id: "remove-drawings", label: "Remove drawings", disabled: counts.drawings === 0, separatorBefore: true },
4864
- { id: "remove-indicators", label: "Remove indicators", disabled: counts.indicators === 0 },
4865
- settingsItem("body", "Settings\u2026")
4866
- ];
4867
- }
4868
-
4869
4863
  // src/widget/context-menu.ts
4870
4864
  var PRICE_AXIS_W = 60;
4871
4865
  var TIME_AXIS_H = 26;
4866
+ var OFF_PLOT = { price: null, time: null, paneKind: null };
4872
4867
  var ChartContextMenu = class {
4873
4868
  constructor(host, cbs) {
4874
4869
  this.cbs = cbs;
4875
4870
  this.chart = null;
4871
+ this.offCrosshair = null;
4872
+ /** The bound chart's latest crosshair report — a right-click always follows a pointer
4873
+ * move to its spot, so this is where it landed. */
4874
+ this.crosshair = OFF_PLOT;
4875
+ /** {@link crosshair} as it stood when the menu opened: the pointer moves on through the
4876
+ * menu before a row is picked, and `run` must see the right-clicked spot. */
4877
+ this.pointer = OFF_PLOT;
4876
4878
  this.lastZone = "body";
4877
4879
  /** The pane whose scale the open price-axis menu targets (null ⇒ the main scale). */
4878
4880
  this.lastPane = null;
4879
4881
  this.onContextMenu = (e) => {
4880
4882
  e.preventDefault();
4881
4883
  if (!this.chart) return;
4884
+ this.pointer = this.crosshair;
4882
4885
  this.lastZone = this.zoneOf(e);
4883
4886
  this.lastPane = this.lastZone === "price-axis" ? this.paneAt(e) : null;
4884
4887
  this.menu.setItems(this.itemsFor(this.lastZone));
@@ -4892,15 +4895,23 @@ var ChartContextMenu = class {
4892
4895
  // Pointer-anchored action menu: checked state reads as a leading ✓, not a
4893
4896
  // washed row (which would read as hover in a menu with no trigger button).
4894
4897
  checkmarks: true,
4898
+ iconBadges: true,
4895
4899
  onSelect: (id) => this.run(id)
4896
4900
  });
4897
4901
  host.addEventListener("contextmenu", this.onContextMenu);
4898
4902
  }
4899
4903
  /** (Re)bind to a chart instance — called after every widget rebuild. */
4900
4904
  onChart(chart) {
4905
+ this.offCrosshair?.();
4901
4906
  this.chart = chart;
4907
+ this.crosshair = OFF_PLOT;
4908
+ this.offCrosshair = chart.renderer.onCrosshairMove((e) => {
4909
+ this.crosshair = { price: e.price, time: e.time, paneKind: e.paneKind ?? null };
4910
+ });
4902
4911
  }
4903
4912
  destroy() {
4913
+ this.offCrosshair?.();
4914
+ this.offCrosshair = null;
4904
4915
  this.host.removeEventListener("contextmenu", this.onContextMenu);
4905
4916
  this.menu.destroy();
4906
4917
  }
@@ -4919,49 +4930,52 @@ var ChartContextMenu = class {
4919
4930
  flag(feature) {
4920
4931
  return Boolean(this.chart?.renderer.get(feature));
4921
4932
  }
4922
- contributed(zone) {
4933
+ /** A fresh widget context carrying the pointer captured at open. Copied by property
4934
+ * descriptor: the context's getters must stay LIVE, and a spread would freeze them. */
4935
+ context() {
4923
4936
  const ctx = this.cbs.getContext?.();
4924
- return widgetActions(`context:${zone}`, ctx).map((a, i) => ({
4925
- id: `action:${a.id}`,
4926
- label: a.label,
4927
- icon: a.icon,
4928
- separatorBefore: i === 0
4937
+ if (!ctx) return void 0;
4938
+ return Object.create(Object.getPrototypeOf(ctx), {
4939
+ ...Object.getOwnPropertyDescriptors(ctx),
4940
+ pointer: { value: this.pointer, enumerable: true }
4941
+ });
4942
+ }
4943
+ contributed(zone) {
4944
+ return widgetActions(`context:${zone}`, this.context()).map((a) => ({
4945
+ item: { id: `action:${a.id}`, label: a.label, icon: a.icon },
4946
+ order: a.order
4929
4947
  }));
4930
4948
  }
4931
4949
  itemsFor(zone) {
4950
+ return composeMenu(zone, this.builtinItems(zone), this.contributed(zone));
4951
+ }
4952
+ builtinItems(zone) {
4932
4953
  if (zone === "price-axis") {
4933
4954
  const pane = this.lastPane;
4934
- return [
4935
- ...priceAxisItems({
4936
- auto: this.chart?.renderer.get("autoScale") !== false,
4937
- invert: pane ? pane.invert : this.flag("invertScale"),
4938
- choice: scaleChoiceOf(pane ?? { mode: String(this.chart?.renderer.get("scaleMode") ?? "price"), log: this.flag("logScale") }),
4939
- axisLabels: this.flag("axisLabels"),
4940
- priceLabel: this.flag("priceLabel"),
4941
- countdown: this.flag("countdown"),
4942
- priceLine: this.flag("currentPriceLine")
4943
- }),
4944
- ...this.contributed(zone)
4945
- ];
4955
+ return priceAxisItems({
4956
+ auto: this.chart?.renderer.get("autoScale") !== false,
4957
+ invert: pane ? pane.invert : this.flag("invertScale"),
4958
+ choice: scaleChoiceOf(pane ?? { mode: String(this.chart?.renderer.get("scaleMode") ?? "price"), log: this.flag("logScale") }),
4959
+ axisLabels: this.flag("axisLabels"),
4960
+ priceLabel: this.flag("priceLabel"),
4961
+ countdown: this.flag("countdown"),
4962
+ priceLine: this.flag("currentPriceLine")
4963
+ });
4946
4964
  }
4947
4965
  if (zone === "time-axis") {
4948
- const tz = this.cbs.timezone?.() ?? String(this.chart?.renderer.get("timezone") ?? "Etc/UTC");
4949
- return [...timeAxisItems(tz), ...this.contributed("time-axis")];
4966
+ return timeAxisItems(this.cbs.timezone?.() ?? String(this.chart?.renderer.get("timezone") ?? "Etc/UTC"));
4950
4967
  }
4951
4968
  const chart = this.chart;
4952
- return [
4953
- ...bodyItems({
4954
- drawings: chart?.drawings.supported ? chart.drawings.all().length : 0,
4955
- indicators: chart?.indicators().length ?? 0
4956
- }),
4957
- ...this.contributed("body")
4958
- ];
4969
+ return bodyItems({
4970
+ drawings: chart?.drawings.supported ? chart.drawings.all().length : 0,
4971
+ indicators: chart?.indicators().length ?? 0
4972
+ });
4959
4973
  }
4960
4974
  run(id) {
4961
4975
  const chart = this.chart;
4962
4976
  if (!chart) return;
4963
4977
  if (id.startsWith("action:")) {
4964
- const ctx = this.cbs.getContext?.();
4978
+ const ctx = this.context();
4965
4979
  if (ctx) widgetActions(`context:${this.lastZone}`, ctx).find((a) => a.id === id.slice("action:".length))?.run(ctx);
4966
4980
  return;
4967
4981
  }
@@ -5611,8 +5625,8 @@ function seedDefaults(opts) {
5611
5625
  };
5612
5626
  }
5613
5627
  function cellChartDefaults(opts) {
5614
- const { renderer, defaultLanguage, currentPriceLine, logScale, animations, glow, upColor, downColor, drawings, settings } = opts;
5615
- return { renderer, defaultLanguage, currentPriceLine, logScale, animations, glow, upColor, downColor, drawings, settings };
5628
+ const { renderer, defaultLanguage, currentPriceLine, logScale, animations, glow, upColor, downColor, drawings, settings, priceAxis } = opts;
5629
+ return { renderer, defaultLanguage, currentPriceLine, logScale, animations, glow, upColor, downColor, drawings, settings, priceAxis };
5616
5630
  }
5617
5631
  function cellDrawings(opt) {
5618
5632
  if (opt === false) return false;
@@ -6182,12 +6196,14 @@ var ChartCell = class {
6182
6196
  this.watermarkOn = visible;
6183
6197
  this.watermark?.setVisible(visible);
6184
6198
  this.deps.onStateDirty();
6199
+ this.deps.onStatusPrefsChanged(this.id);
6185
6200
  }
6186
6201
  /** Show/hide the "Replay" line under this cell's watermark while it replays (persisted per cell). */
6187
6202
  setReplayWatermarkVisible(visible) {
6188
6203
  this.replayWatermarkOn = visible;
6189
6204
  this.watermark?.setReplayVisible(visible);
6190
6205
  this.deps.onStateDirty();
6206
+ this.deps.onStatusPrefsChanged(this.id);
6191
6207
  }
6192
6208
  /** Show/hide this cell's indicator titles — the in-chart legend rows (persisted per cell). */
6193
6209
  setIndicatorTitlesVisible(visible) {
@@ -6208,18 +6224,20 @@ var ChartCell = class {
6208
6224
  this.statusline?.setPartVisible(part, visible);
6209
6225
  this.deps.onStatusPrefsChanged(this.id);
6210
6226
  }
6211
- /** This cell's Status line tab prefs as one bundle (see {@link CellStatusPrefs}). */
6227
+ /** This cell's Status line tab and watermark prefs as one bundle (see {@link CellStatusPrefs}). */
6212
6228
  statusPrefs() {
6213
6229
  const sl = this.statusline;
6214
6230
  return {
6215
6231
  parts: sl ? { logo: sl.partVisible("logo"), name: sl.partVisible("name"), market: sl.partVisible("market"), ohlc: sl.partVisible("ohlc"), change: sl.partVisible("change") } : null,
6216
6232
  indicatorTitles: this.indicatorTitlesOn,
6217
- indicatorValues: this.indicatorValuesOn
6233
+ indicatorValues: this.indicatorValuesOn,
6234
+ watermark: this.watermarkOn,
6235
+ replayWatermark: this.replayWatermarkOn
6218
6236
  };
6219
6237
  }
6220
- /** Converge this cell's Status line tab prefs to `prefs` — the follower half of
6221
- * the workspace's style link. Idempotent: matching values change nothing, so a
6222
- * propagated echo dies on its own. */
6238
+ /** Converge this cell's Status line tab and watermark prefs to `prefs` — the
6239
+ * follower half of the workspace's style link. Idempotent: matching values change
6240
+ * nothing, so a propagated echo dies on its own. */
6223
6241
  applyStatusPrefs(prefs) {
6224
6242
  if (prefs.parts && this.statusline) {
6225
6243
  for (const part of Object.keys(prefs.parts)) {
@@ -6228,6 +6246,8 @@ var ChartCell = class {
6228
6246
  }
6229
6247
  if (prefs.indicatorTitles !== this.indicatorTitlesOn) this.setIndicatorTitlesVisible(prefs.indicatorTitles);
6230
6248
  if (prefs.indicatorValues !== this.indicatorValuesOn) this.setIndicatorValuesVisible(prefs.indicatorValues);
6249
+ if (prefs.watermark !== this.watermarkOn) this.setWatermarkVisible(prefs.watermark);
6250
+ if (prefs.replayWatermark !== this.replayWatermarkOn) this.setReplayWatermarkVisible(prefs.replayWatermark);
6231
6251
  }
6232
6252
  /** The LIVE chart of this cell — never cache it across a layout change (the cell's
6233
6253
  * identity is what endures; the chart dies with the cell). */
@@ -7099,6 +7119,55 @@ function occupancyGrid(def) {
7099
7119
  const cols = def.cols.length;
7100
7120
  return def.rows.map((_, r) => def.cols.map((_2, c) => def.cells[r * cols + c]?.id ?? `\xB7${r}x${c}`));
7101
7121
  }
7122
+ var GLYPH_MIN = 1.5;
7123
+ var GLYPH_SPAN = 13;
7124
+ function glyphStops(weights, n) {
7125
+ const w = Array.from({ length: n }, (_, i) => {
7126
+ const v = weights[i];
7127
+ return v !== void 0 && Number.isFinite(v) && v > 0 ? v : 1;
7128
+ });
7129
+ const total = w.reduce((a, b) => a + b, 0);
7130
+ const stops = [GLYPH_MIN];
7131
+ let acc = 0;
7132
+ for (const v of w) {
7133
+ acc += v;
7134
+ stops.push(Math.round((GLYPH_MIN + GLYPH_SPAN * acc / total) * 100) / 100);
7135
+ }
7136
+ return stops;
7137
+ }
7138
+ function layoutGlyph(def) {
7139
+ const grid = occupancyGrid(def);
7140
+ const rows = grid.length;
7141
+ const cols = grid.reduce((n, row) => Math.max(n, row.length), 0);
7142
+ const xs = glyphStops(def.cols, cols);
7143
+ const ys = glyphStops(def.rows, rows);
7144
+ const at = (r, c) => grid[r]?.[c];
7145
+ const seams = [];
7146
+ for (let c = 1; c < cols; c += 1) {
7147
+ let start = -1;
7148
+ for (let r = 0; r <= rows; r += 1) {
7149
+ const split = r < rows && at(r, c - 1) !== at(r, c);
7150
+ if (split && start < 0) start = r;
7151
+ if (!split && start >= 0) {
7152
+ seams.push(`M${xs[c]} ${ys[start]}V${ys[r]}`);
7153
+ start = -1;
7154
+ }
7155
+ }
7156
+ }
7157
+ for (let r = 1; r < rows; r += 1) {
7158
+ let start = -1;
7159
+ for (let c = 0; c <= cols; c += 1) {
7160
+ const split = c < cols && at(r - 1, c) !== at(r, c);
7161
+ if (split && start < 0) start = c;
7162
+ if (!split && start >= 0) {
7163
+ seams.push(`M${xs[start]} ${ys[r]}H${xs[c]}`);
7164
+ start = -1;
7165
+ }
7166
+ }
7167
+ }
7168
+ const frame = `<rect x="${GLYPH_MIN}" y="${GLYPH_MIN}" width="${GLYPH_SPAN}" height="${GLYPH_SPAN}" rx="1.5"/>`;
7169
+ return svg16(seams.length > 0 ? `${frame}<path d="${seams.join("")}"/>` : frame);
7170
+ }
7102
7171
  function gridStyles(def, trackSizes) {
7103
7172
  const cols = trackSizes?.cols?.length === def.cols.length ? trackSizes.cols : def.cols;
7104
7173
  const rows = trackSizes?.rows?.length === def.rows.length ? trackSizes.rows : def.rows;
@@ -8009,7 +8078,11 @@ var MoreDrawer = class {
8009
8078
  row(doc, label, opts) {
8010
8079
  const el = doc.createElement("div");
8011
8080
  el.className = "vela-md-row";
8012
- if (opts.icon) el.appendChild(iconEl(opts.icon, doc));
8081
+ if (opts.icon) {
8082
+ const icon2 = iconEl(opts.icon, doc);
8083
+ if (opts.glyph) icon2.innerHTML = opts.glyph;
8084
+ el.appendChild(icon2);
8085
+ }
8013
8086
  const text = doc.createElement("span");
8014
8087
  text.className = "vela-md-row-label";
8015
8088
  text.textContent = label;
@@ -8051,7 +8124,7 @@ var MoreDrawer = class {
8051
8124
  if (this.opts.layout) {
8052
8125
  const shape = this.opts.layout.shape();
8053
8126
  const value = shape ? `${shape.cols} \xD7 ${shape.rows}` : this.opts.layout.presets().find((p) => p.checked)?.label;
8054
- list.appendChild(this.row(doc, "Layout", { icon: "layout", value, chevron: true, onClick: () => this.show("layout") }));
8127
+ list.appendChild(this.row(doc, "Layout", { icon: "layout", glyph: this.opts.layout.glyph?.(), value, chevron: true, onClick: () => this.show("layout") }));
8055
8128
  }
8056
8129
  for (const act of this.opts.primaryActions?.() ?? []) list.appendChild(this.actionRow(doc, act));
8057
8130
  for (const panel of this.opts.panels()) {
@@ -8915,7 +8988,8 @@ var VelaWorkspace = class {
8915
8988
  onToggleSync: (id) => {
8916
8989
  const kind = id;
8917
8990
  this.sync.set(kind, this.syncOpts[kind] ? false : true);
8918
- }
8991
+ },
8992
+ glyph: () => layoutGlyph(this.def)
8919
8993
  },
8920
8994
  getContext: () => this.context()
8921
8995
  });
@@ -9414,6 +9488,7 @@ var VelaWorkspace = class {
9414
9488
  this.applyGrid();
9415
9489
  this.buildCells();
9416
9490
  this.alignNewCellStyles(preexisting);
9491
+ this.alignNewCellDrawings(preexisting);
9417
9492
  this.syncCellPresentation();
9418
9493
  this.refreshCellControls();
9419
9494
  this.topbar.setLayout(next.id);
@@ -9830,6 +9905,7 @@ var VelaWorkspace = class {
9830
9905
  const chart = cell.chart;
9831
9906
  chart.on("indicator:error", ({ error }) => this.toastHost.show(`[${cell.id}] ${error.message}`, "error", 5e3));
9832
9907
  chart.on("script:run", (run) => this.events.emit("script:run", { ...run, cell: cell.id }));
9908
+ chart.on("priceStyle:change", ({ from, to }) => this.events.emit("cell:priceStyle", { id: cell.id, from, to }));
9833
9909
  chart.on("alert", (alert) => {
9834
9910
  const source = [parseSymbol(cell.symbol).ticker || cell.symbol, timeframeLabel(cell.timeframe), alert.indicator].filter(Boolean).join(" ");
9835
9911
  this.alerts.unshift({ cellId: cell.id, source, title: alert.title ?? "Alert", message: alert.message, time: alert.time });
@@ -9947,12 +10023,66 @@ var VelaWorkspace = class {
9947
10023
  }
9948
10024
  }
9949
10025
  /**
9950
- * Mirror an origin cell's presentation — the Canvas + Scales-and-lines slice of
9951
- * its renderer config plus its Status line tab prefs — onto its same-group
9952
- * followers (the style link). Loop-safe two ways: the busy guard eats the
9953
- * followers' SYNCHRONOUS echoes (their `applyConfig` re-fires `onConfigChanged`
9954
- * in the same tick), and the equality short-circuits leave already-converged
9955
- * followers untouched, so nothing re-emits once the group agrees.
10026
+ * Bring cells minted by a layout change into their drawings group: with the link
10027
+ * on, a NEW cell (fresh slot, or one returning from the pool that missed edits
10028
+ * while dormant) receives the drawings of a pre-existing group peer — the active
10029
+ * cell when it is one. A drawing the arriving cell already holds a linked copy of
10030
+ * is refreshed in place; any other is copied and linked like a freshly synced
10031
+ * drawing, so later edits and removals follow both ways. The arrival is the cell's
10032
+ * starting state, not an edit: it stays out of the cell's undo timeline, and the
10033
+ * busy guard keeps the copies' own events from fanning back out.
10034
+ */
10035
+ alignNewCellDrawings(preexisting) {
10036
+ const setting = this.syncOpts.drawings;
10037
+ if (!setting) return;
10038
+ const ids = [...this.cellsById.keys()];
10039
+ for (const id of ids) {
10040
+ if (preexisting.has(id)) continue;
10041
+ const cell = this.cellsById.get(id);
10042
+ if (!cell?.chart.drawings.supported) continue;
10043
+ const peers = syncTargets(id, setting, ids).filter((p) => preexisting.has(p));
10044
+ if (peers.length === 0) continue;
10045
+ const sourceId = this.activeId && peers.includes(this.activeId) ? this.activeId : peers[0];
10046
+ const source = this.cellsById.get(sourceId);
10047
+ if (!source) continue;
10048
+ const drawings = cell.chart.drawings;
10049
+ this.drawingSyncBusy = true;
10050
+ try {
10051
+ cell.history.silently(() => {
10052
+ const held = new Set(drawings.all().map((d) => d.id));
10053
+ for (const doc of source.chart.drawings.all()) {
10054
+ const group = this.drawingLinks.get(`${sourceId}\0${doc.id}`) ?? /* @__PURE__ */ new Map([[sourceId, doc.id]]);
10055
+ const peerId = group.get(id);
10056
+ if (peerId != null && held.has(peerId)) {
10057
+ drawings.update(peerId, { anchors: doc.anchors, style: doc.style, text: doc.text, props: doc.props });
10058
+ continue;
10059
+ }
10060
+ const copy = drawings.add(doc.type, {
10061
+ paneId: doc.paneId,
10062
+ anchors: doc.anchors,
10063
+ style: doc.style,
10064
+ text: doc.text,
10065
+ props: doc.props,
10066
+ zIndex: doc.zIndex
10067
+ });
10068
+ if (!copy) continue;
10069
+ if (peerId != null) this.drawingLinks.delete(`${id}\0${peerId}`);
10070
+ group.set(id, copy.id);
10071
+ for (const [cellId, dId] of group) this.drawingLinks.set(`${cellId}\0${dId}`, group);
10072
+ }
10073
+ });
10074
+ } finally {
10075
+ this.drawingSyncBusy = false;
10076
+ }
10077
+ }
10078
+ }
10079
+ /**
10080
+ * Mirror an origin cell's presentation — the style-link slice of its renderer
10081
+ * config ({@link styleConfigPatch}) plus its Status line and watermark prefs —
10082
+ * onto its same-group followers (the style link). Loop-safe two ways: the busy
10083
+ * guard eats the followers' SYNCHRONOUS echoes (their `applyConfig` re-fires
10084
+ * `onConfigChanged` in the same tick), and the equality short-circuits leave
10085
+ * already-converged followers untouched, so nothing re-emits once the group agrees.
9956
10086
  */
9957
10087
  propagateStylePrefs(originId) {
9958
10088
  if (this.styleSyncBusy || this.destroyed) return;
@@ -9960,17 +10090,15 @@ var VelaWorkspace = class {
9960
10090
  if (targets.length === 0) return;
9961
10091
  const origin = this.cellsById.get(originId);
9962
10092
  if (!origin) return;
9963
- const slice = styleConfigSlice(origin.chart.renderer.getConfig());
9964
- const sliceJson = slice ? JSON.stringify(slice) : null;
10093
+ const config = origin.chart.renderer.getConfig();
9965
10094
  const prefs = origin.statusPrefs();
9966
10095
  this.styleSyncBusy = true;
9967
10096
  try {
9968
10097
  for (const id of targets) {
9969
10098
  const cell = this.cellsById.get(id);
9970
10099
  if (!cell) continue;
9971
- if (slice && sliceJson !== JSON.stringify(styleConfigSlice(cell.chart.renderer.getConfig()))) {
9972
- cell.chart.renderer.applyConfig(slice);
9973
- }
10100
+ const patch = styleConfigPatch(config, cell.chart.renderer.getConfig());
10101
+ if (patch) cell.chart.renderer.applyConfig(patch);
9974
10102
  cell.applyStatusPrefs(prefs);
9975
10103
  }
9976
10104
  } finally {
@@ -10291,7 +10419,8 @@ var VelaWorkspace = class {
10291
10419
  onToggleSync: (id) => {
10292
10420
  const kind = id;
10293
10421
  this.sync.set(kind, this.syncOpts[kind] ? false : true);
10294
- }
10422
+ },
10423
+ glyph: () => layoutGlyph(this.def)
10295
10424
  },
10296
10425
  onOpenChange: (open) => this.trackDialog(open)
10297
10426
  }));