@luxalgo/vela 0.8.2 → 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 (48) hide show
  1. package/dist/{DataProvider-DpOWZD8F.d.ts → DataProvider-oH78tYXL.d.ts} +1 -1
  2. package/dist/{DataProvider-COGDs9Xg.d.cts → DataProvider-wbtJ3-Sa.d.cts} +1 -1
  3. package/dist/{chunk-R2JMNIHY.js → chunk-2IPXYM4B.js} +258 -169
  4. package/dist/{chunk-NLDTVFOI.js → chunk-G7B7ZCBF.js} +129 -10
  5. package/dist/{chunk-T3DZIR5Y.js → chunk-HMHA733L.js} +306 -168
  6. package/dist/{chunk-DPAA3SRT.js → chunk-KWI3YDBW.js} +254 -9
  7. package/dist/{chunk-27UTYE7T.js → chunk-TO3GIK7Z.js} +49 -7
  8. package/dist/{chunk-HKNO24GD.js → chunk-VHGACEHO.js} +50 -1
  9. package/dist/{contributions-Cgm9RJYP.d.cts → contributions-BhOBRenr.d.cts} +44 -4
  10. package/dist/{contributions-bWgwyiVi.d.ts → contributions-HGqnNb_P.d.ts} +44 -4
  11. package/dist/index.cjs +497 -79
  12. package/dist/index.d.cts +34 -14
  13. package/dist/index.d.ts +34 -14
  14. package/dist/index.js +5 -5
  15. package/dist/{options-NAUHiQEC.d.ts → options-lMUwmRwh.d.cts} +66 -1
  16. package/dist/{options-NAUHiQEC.d.cts → options-lMUwmRwh.d.ts} +66 -1
  17. package/dist/{plugin-CqxWoDKS.d.ts → plugin-Cr2Pj8q8.d.ts} +4 -4
  18. package/dist/{plugin-CjjzZX2F.d.cts → plugin-Do5J5xuF.d.cts} +4 -4
  19. package/dist/plugin.cjs +18 -0
  20. package/dist/plugin.d.cts +5 -5
  21. package/dist/plugin.d.ts +5 -5
  22. package/dist/plugin.js +3 -3
  23. package/dist/providers/binance.d.cts +2 -2
  24. package/dist/providers/binance.d.ts +2 -2
  25. package/dist/providers/coinbase.d.cts +2 -2
  26. package/dist/providers/coinbase.d.ts +2 -2
  27. package/dist/providers/hyperliquid.d.cts +2 -2
  28. package/dist/providers/hyperliquid.d.ts +2 -2
  29. package/dist/{side-panel-wMNfgyZb.d.cts → side-panel-_2UK1gSm.d.cts} +45 -3
  30. package/dist/{side-panel-wMNfgyZb.d.ts → side-panel-_2UK1gSm.d.ts} +45 -3
  31. package/dist/{statusline-model-Dpfpq54A.d.cts → statusline-model-B-2xGlB_.d.cts} +3 -3
  32. package/dist/{statusline-model-Dih6FGWP.d.ts → statusline-model-CY1wzxHg.d.ts} +3 -3
  33. package/dist/ui.cjs +224 -14
  34. package/dist/ui.d.cts +119 -87
  35. package/dist/ui.d.ts +119 -87
  36. package/dist/ui.js +3 -3
  37. package/dist/vela.global.js +497 -79
  38. package/dist/vela.global.min.js +86 -52
  39. package/dist/widget.cjs +850 -167
  40. package/dist/widget.d.cts +24 -10
  41. package/dist/widget.d.ts +24 -10
  42. package/dist/widget.js +7 -7
  43. package/dist/workspace.cjs +849 -167
  44. package/dist/workspace.d.cts +47 -20
  45. package/dist/workspace.d.ts +47 -20
  46. package/dist/workspace.js +6 -6
  47. package/package.json +1 -1
  48. /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-T3DZIR5Y.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-DPAA3SRT.js';
3
- import { Tooltip, KeymapManager, isEditableTarget, Drawer } from './chunk-27UTYE7T.js';
4
- import { Menu, Dialog, CalloutBubble, applyPlotOverlayTokens, ensureUIHost } from './chunk-NLDTVFOI.js';
5
- import { registerIcon, svg16, injectStyles, iconEl, iconMarkup, iconAt, SESSION_OFF, SESSION_POST, SESSION_PRE, announceSurface, icon, categoricalColor } from './chunk-HKNO24GD.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) => {
@@ -335,6 +337,8 @@ var LayoutPicker = class {
335
337
  open() {
336
338
  if (this.isOpen) return;
337
339
  this.isOpen = true;
340
+ this.exit?.cancel();
341
+ this.exit = null;
338
342
  this.hover = null;
339
343
  this.refresh();
340
344
  this.layer.style.display = "";
@@ -349,7 +353,10 @@ var LayoutPicker = class {
349
353
  if (!this.isOpen) return;
350
354
  this.isOpen = false;
351
355
  announceSurface(this.panel, false, "popover", this.opts.trigger);
352
- this.layer.style.display = "none";
356
+ this.exit = holdForExit(this.panel, () => {
357
+ this.exit = null;
358
+ if (!this.isOpen) this.layer.style.display = "none";
359
+ });
353
360
  this.opts.trigger.setAttribute("aria-expanded", "false");
354
361
  this.doc.removeEventListener("pointerdown", this.onDocPointerDown, true);
355
362
  this.doc.removeEventListener("keydown", this.onDocKeydown, true);
@@ -364,6 +371,7 @@ var LayoutPicker = class {
364
371
  }
365
372
  destroy() {
366
373
  this.close();
374
+ this.exit?.finish();
367
375
  this.infoTip.destroy();
368
376
  this.layer.remove();
369
377
  }
@@ -430,7 +438,7 @@ var LayoutPicker = class {
430
438
  const hostRect = this.opts.host.getBoundingClientRect();
431
439
  const trigRect = this.opts.trigger.getBoundingClientRect();
432
440
  let left = trigRect.left - hostRect.left;
433
- const top = trigRect.bottom - hostRect.top + 4;
441
+ const top = trigRect.bottom - hostRect.top + 8;
434
442
  const width = this.layer.offsetWidth;
435
443
  if (left + width > hostRect.width - 8) left = Math.max(8, hostRect.width - 8 - width);
436
444
  this.layer.style.left = `${left}px`;
@@ -783,7 +791,8 @@ var Topbar = class {
783
791
  triggerId: "vela-topbar-style",
784
792
  host,
785
793
  items: this.styleItems(),
786
- onSelect: (id) => opts.onPriceStyle(id)
794
+ onSelect: (id) => opts.onPriceStyle(id),
795
+ iconBadges: true
787
796
  });
788
797
  }
789
798
  setSymbol(symbol) {
@@ -849,8 +858,14 @@ var Topbar = class {
849
858
  renderLayoutButton(doc) {
850
859
  if (!this.layoutButton) return;
851
860
  this.layoutButton.replaceChildren();
852
- if (iconMarkup("layout")) this.layoutButton.appendChild(iconEl("layout", doc));
853
- 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
+ }
854
869
  this.layoutButton.setAttribute("aria-label", `Layout \u2014 ${this.layoutId ?? ""}`);
855
870
  }
856
871
  renderStyleButton(doc) {
@@ -1704,8 +1719,8 @@ var ObjectTree = class extends SidePanel {
1704
1719
  set groups(next) {
1705
1720
  if (this.chart) this.groupsPerChart.set(this.chart, next);
1706
1721
  }
1707
- toggle(open = this.el.hidden) {
1708
- super.toggle(open);
1722
+ toggle(open = !this.open, instant = false) {
1723
+ super.toggle(open, instant);
1709
1724
  if (open) this.refresh();
1710
1725
  }
1711
1726
  setSymbol(symbol) {
@@ -2739,8 +2754,8 @@ var DataWindow = class extends SidePanel {
2739
2754
  this.unsubs = [];
2740
2755
  injectStyles(STYLE_ID5, CSS5, host.ownerDocument);
2741
2756
  }
2742
- toggle(open = this.el.hidden) {
2743
- super.toggle(open);
2757
+ toggle(open = !this.open, instant = false) {
2758
+ super.toggle(open, instant);
2744
2759
  if (open) this.refresh();
2745
2760
  }
2746
2761
  /** (Re)bind to a chart instance — called after every widget rebuild. */
@@ -2910,7 +2925,7 @@ var PanelDock = class {
2910
2925
  }
2911
2926
  this.pinned = new Set(state.pinned ?? []);
2912
2927
  for (const entry of this.entries) entry.panel.setOverlay(!this.pinned.has(entry.id));
2913
- 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);
2914
2929
  this.pendingOpen = state.open && !this.entries.some((e) => e.id === state.open) ? state.open : null;
2915
2930
  }
2916
2931
  /** Drop the contributed panels (the shell destroys its own). */
@@ -2926,7 +2941,7 @@ var PanelDock = class {
2926
2941
  if (this.pinned.has(entry.id)) entry.panel.setOverlay(false);
2927
2942
  entry.panel.onOpenChange = (open) => {
2928
2943
  if (open) {
2929
- 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);
2930
2945
  this.pendingOpen = null;
2931
2946
  }
2932
2947
  this.deps.chrome.setPanelActive(entry.id, open);
@@ -4042,15 +4057,48 @@ function syncTargets(originId, setting, cellIds) {
4042
4057
  function rangesWithin(a, b, epsMs) {
4043
4058
  return Math.abs(a.from - b.from) <= epsMs && Math.abs(a.to - b.to) <= epsMs;
4044
4059
  }
4045
- var STYLE_SYNC_CONFIG_KEYS = ["layout", "panes", "grid", "priceScale", "crosshair"];
4046
- function styleConfigSlice(config) {
4047
- if (config == null || typeof config !== "object") return null;
4048
- const doc = config;
4049
- 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 = {};
4050
4080
  for (const key of STYLE_SYNC_CONFIG_KEYS) {
4051
- 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;
4052
4083
  }
4053
- 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;
4054
4102
  }
4055
4103
 
4056
4104
  // src/workspace/persist.ts
@@ -4812,113 +4860,28 @@ var Watermark = class {
4812
4860
  }
4813
4861
  };
4814
4862
 
4815
- // src/widget/context-menu-model.ts
4816
- var SETTINGS_SECTION = {
4817
- body: "Canvas",
4818
- "price-axis": "Scales and lines",
4819
- "time-axis": "Scales and lines"
4820
- };
4821
- function settingsItem(zone, label) {
4822
- return { id: `settings:${SETTINGS_SECTION[zone]}`, label, separatorBefore: true };
4823
- }
4824
- function settingsSectionOf(id) {
4825
- return id.slice("settings:".length) || void 0;
4826
- }
4827
- var SCALE_CHOICES = [
4828
- ["regular", "Regular"],
4829
- ["percent", "Percent"],
4830
- ["indexed", "Indexed to 100"],
4831
- ["log", "Logarithmic"]
4832
- ];
4833
- function paneScaleAt(panes, y) {
4834
- return panes.find((p) => y >= p.top && y < p.top + p.height) ?? panes[0] ?? null;
4835
- }
4836
- function scaleChoiceOf(scale) {
4837
- if (scale.log) return "log";
4838
- if (scale.mode === "percent") return "percent";
4839
- if (scale.mode === "indexed") return "indexed";
4840
- return "regular";
4841
- }
4842
- function mainScale(pane) {
4843
- return pane === null || pane.kind === "price";
4844
- }
4845
- function scaleWrites(choice, pane) {
4846
- const mode = choice === "percent" ? "percent" : choice === "indexed" ? "indexed" : "price";
4847
- const log = choice === "log";
4848
- if (mainScale(pane)) {
4849
- return [
4850
- ["scaleMode", mode],
4851
- ["logScale", log]
4852
- ];
4853
- }
4854
- return [
4855
- ["scaleMode", { pane: pane.id, mode }],
4856
- ["logScale", { pane: pane.id, value: log }]
4857
- ];
4858
- }
4859
- function invertWrite(next, pane) {
4860
- return mainScale(pane) ? ["invertScale", next] : ["invertScale", { pane: pane.id, value: next }];
4861
- }
4862
- function priceAxisItems(s) {
4863
- return [
4864
- { id: "auto", label: "Auto (fits data to screen)", checked: s.auto },
4865
- { id: "invert", label: "Invert scale", checked: s.invert },
4866
- ...SCALE_CHOICES.map(([choice, label], i) => ({
4867
- id: `scale:${choice}`,
4868
- label,
4869
- checked: s.choice === choice,
4870
- separatorBefore: i === 0
4871
- })),
4872
- {
4873
- id: "labels",
4874
- label: "Labels",
4875
- separatorBefore: true,
4876
- submenu: [
4877
- { id: "toggle:axisLabels", label: "Price axis labels", checked: s.axisLabels },
4878
- { id: "toggle:priceLabel", label: "Last price label", checked: s.priceLabel },
4879
- { id: "toggle:countdown", label: "Countdown to bar close", checked: s.countdown }
4880
- ]
4881
- },
4882
- {
4883
- id: "levels",
4884
- label: "Levels",
4885
- submenu: [{ id: "toggle:currentPriceLine", label: "Last Price Line", checked: s.priceLine }]
4886
- },
4887
- settingsItem("price-axis", "More settings\u2026")
4888
- ];
4889
- }
4890
- function timeAxisItems(timezone) {
4891
- return [
4892
- {
4893
- id: "timezone",
4894
- label: "Time zone",
4895
- submenu: timezoneMenuRows(timezone).map((r) => ({ id: `tz:${r.value}`, label: r.label, checked: r.checked }))
4896
- },
4897
- settingsItem("time-axis", "More settings\u2026")
4898
- ];
4899
- }
4900
- function bodyItems(counts) {
4901
- return [
4902
- { id: "reset-view", label: "Reset chart view" },
4903
- { id: "remove-drawings", label: "Remove drawings", disabled: counts.drawings === 0, separatorBefore: true },
4904
- { id: "remove-indicators", label: "Remove indicators", disabled: counts.indicators === 0 },
4905
- settingsItem("body", "Settings\u2026")
4906
- ];
4907
- }
4908
-
4909
4863
  // src/widget/context-menu.ts
4910
4864
  var PRICE_AXIS_W = 60;
4911
4865
  var TIME_AXIS_H = 26;
4866
+ var OFF_PLOT = { price: null, time: null, paneKind: null };
4912
4867
  var ChartContextMenu = class {
4913
4868
  constructor(host, cbs) {
4914
4869
  this.cbs = cbs;
4915
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;
4916
4878
  this.lastZone = "body";
4917
4879
  /** The pane whose scale the open price-axis menu targets (null ⇒ the main scale). */
4918
4880
  this.lastPane = null;
4919
4881
  this.onContextMenu = (e) => {
4920
4882
  e.preventDefault();
4921
4883
  if (!this.chart) return;
4884
+ this.pointer = this.crosshair;
4922
4885
  this.lastZone = this.zoneOf(e);
4923
4886
  this.lastPane = this.lastZone === "price-axis" ? this.paneAt(e) : null;
4924
4887
  this.menu.setItems(this.itemsFor(this.lastZone));
@@ -4932,15 +4895,23 @@ var ChartContextMenu = class {
4932
4895
  // Pointer-anchored action menu: checked state reads as a leading ✓, not a
4933
4896
  // washed row (which would read as hover in a menu with no trigger button).
4934
4897
  checkmarks: true,
4898
+ iconBadges: true,
4935
4899
  onSelect: (id) => this.run(id)
4936
4900
  });
4937
4901
  host.addEventListener("contextmenu", this.onContextMenu);
4938
4902
  }
4939
4903
  /** (Re)bind to a chart instance — called after every widget rebuild. */
4940
4904
  onChart(chart) {
4905
+ this.offCrosshair?.();
4941
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
+ });
4942
4911
  }
4943
4912
  destroy() {
4913
+ this.offCrosshair?.();
4914
+ this.offCrosshair = null;
4944
4915
  this.host.removeEventListener("contextmenu", this.onContextMenu);
4945
4916
  this.menu.destroy();
4946
4917
  }
@@ -4959,49 +4930,52 @@ var ChartContextMenu = class {
4959
4930
  flag(feature) {
4960
4931
  return Boolean(this.chart?.renderer.get(feature));
4961
4932
  }
4962
- 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() {
4963
4936
  const ctx = this.cbs.getContext?.();
4964
- return widgetActions(`context:${zone}`, ctx).map((a, i) => ({
4965
- id: `action:${a.id}`,
4966
- label: a.label,
4967
- icon: a.icon,
4968
- 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
4969
4947
  }));
4970
4948
  }
4971
4949
  itemsFor(zone) {
4950
+ return composeMenu(zone, this.builtinItems(zone), this.contributed(zone));
4951
+ }
4952
+ builtinItems(zone) {
4972
4953
  if (zone === "price-axis") {
4973
4954
  const pane = this.lastPane;
4974
- return [
4975
- ...priceAxisItems({
4976
- auto: this.chart?.renderer.get("autoScale") !== false,
4977
- invert: pane ? pane.invert : this.flag("invertScale"),
4978
- choice: scaleChoiceOf(pane ?? { mode: String(this.chart?.renderer.get("scaleMode") ?? "price"), log: this.flag("logScale") }),
4979
- axisLabels: this.flag("axisLabels"),
4980
- priceLabel: this.flag("priceLabel"),
4981
- countdown: this.flag("countdown"),
4982
- priceLine: this.flag("currentPriceLine")
4983
- }),
4984
- ...this.contributed(zone)
4985
- ];
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
+ });
4986
4964
  }
4987
4965
  if (zone === "time-axis") {
4988
- const tz = this.cbs.timezone?.() ?? String(this.chart?.renderer.get("timezone") ?? "Etc/UTC");
4989
- return [...timeAxisItems(tz), ...this.contributed("time-axis")];
4966
+ return timeAxisItems(this.cbs.timezone?.() ?? String(this.chart?.renderer.get("timezone") ?? "Etc/UTC"));
4990
4967
  }
4991
4968
  const chart = this.chart;
4992
- return [
4993
- ...bodyItems({
4994
- drawings: chart?.drawings.supported ? chart.drawings.all().length : 0,
4995
- indicators: chart?.indicators().length ?? 0
4996
- }),
4997
- ...this.contributed("body")
4998
- ];
4969
+ return bodyItems({
4970
+ drawings: chart?.drawings.supported ? chart.drawings.all().length : 0,
4971
+ indicators: chart?.indicators().length ?? 0
4972
+ });
4999
4973
  }
5000
4974
  run(id) {
5001
4975
  const chart = this.chart;
5002
4976
  if (!chart) return;
5003
4977
  if (id.startsWith("action:")) {
5004
- const ctx = this.cbs.getContext?.();
4978
+ const ctx = this.context();
5005
4979
  if (ctx) widgetActions(`context:${this.lastZone}`, ctx).find((a) => a.id === id.slice("action:".length))?.run(ctx);
5006
4980
  return;
5007
4981
  }
@@ -5651,8 +5625,8 @@ function seedDefaults(opts) {
5651
5625
  };
5652
5626
  }
5653
5627
  function cellChartDefaults(opts) {
5654
- const { renderer, defaultLanguage, currentPriceLine, logScale, animations, glow, upColor, downColor, drawings, settings } = opts;
5655
- 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 };
5656
5630
  }
5657
5631
  function cellDrawings(opt) {
5658
5632
  if (opt === false) return false;
@@ -6222,12 +6196,14 @@ var ChartCell = class {
6222
6196
  this.watermarkOn = visible;
6223
6197
  this.watermark?.setVisible(visible);
6224
6198
  this.deps.onStateDirty();
6199
+ this.deps.onStatusPrefsChanged(this.id);
6225
6200
  }
6226
6201
  /** Show/hide the "Replay" line under this cell's watermark while it replays (persisted per cell). */
6227
6202
  setReplayWatermarkVisible(visible) {
6228
6203
  this.replayWatermarkOn = visible;
6229
6204
  this.watermark?.setReplayVisible(visible);
6230
6205
  this.deps.onStateDirty();
6206
+ this.deps.onStatusPrefsChanged(this.id);
6231
6207
  }
6232
6208
  /** Show/hide this cell's indicator titles — the in-chart legend rows (persisted per cell). */
6233
6209
  setIndicatorTitlesVisible(visible) {
@@ -6248,18 +6224,20 @@ var ChartCell = class {
6248
6224
  this.statusline?.setPartVisible(part, visible);
6249
6225
  this.deps.onStatusPrefsChanged(this.id);
6250
6226
  }
6251
- /** 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}). */
6252
6228
  statusPrefs() {
6253
6229
  const sl = this.statusline;
6254
6230
  return {
6255
6231
  parts: sl ? { logo: sl.partVisible("logo"), name: sl.partVisible("name"), market: sl.partVisible("market"), ohlc: sl.partVisible("ohlc"), change: sl.partVisible("change") } : null,
6256
6232
  indicatorTitles: this.indicatorTitlesOn,
6257
- indicatorValues: this.indicatorValuesOn
6233
+ indicatorValues: this.indicatorValuesOn,
6234
+ watermark: this.watermarkOn,
6235
+ replayWatermark: this.replayWatermarkOn
6258
6236
  };
6259
6237
  }
6260
- /** Converge this cell's Status line tab prefs to `prefs` — the follower half of
6261
- * the workspace's style link. Idempotent: matching values change nothing, so a
6262
- * 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. */
6263
6241
  applyStatusPrefs(prefs) {
6264
6242
  if (prefs.parts && this.statusline) {
6265
6243
  for (const part of Object.keys(prefs.parts)) {
@@ -6268,6 +6246,8 @@ var ChartCell = class {
6268
6246
  }
6269
6247
  if (prefs.indicatorTitles !== this.indicatorTitlesOn) this.setIndicatorTitlesVisible(prefs.indicatorTitles);
6270
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);
6271
6251
  }
6272
6252
  /** The LIVE chart of this cell — never cache it across a layout change (the cell's
6273
6253
  * identity is what endures; the chart dies with the cell). */
@@ -7139,6 +7119,55 @@ function occupancyGrid(def) {
7139
7119
  const cols = def.cols.length;
7140
7120
  return def.rows.map((_, r) => def.cols.map((_2, c) => def.cells[r * cols + c]?.id ?? `\xB7${r}x${c}`));
7141
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
+ }
7142
7171
  function gridStyles(def, trackSizes) {
7143
7172
  const cols = trackSizes?.cols?.length === def.cols.length ? trackSizes.cols : def.cols;
7144
7173
  const rows = trackSizes?.rows?.length === def.rows.length ? trackSizes.rows : def.rows;
@@ -8049,7 +8078,11 @@ var MoreDrawer = class {
8049
8078
  row(doc, label, opts) {
8050
8079
  const el = doc.createElement("div");
8051
8080
  el.className = "vela-md-row";
8052
- 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
+ }
8053
8086
  const text = doc.createElement("span");
8054
8087
  text.className = "vela-md-row-label";
8055
8088
  text.textContent = label;
@@ -8091,7 +8124,7 @@ var MoreDrawer = class {
8091
8124
  if (this.opts.layout) {
8092
8125
  const shape = this.opts.layout.shape();
8093
8126
  const value = shape ? `${shape.cols} \xD7 ${shape.rows}` : this.opts.layout.presets().find((p) => p.checked)?.label;
8094
- 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") }));
8095
8128
  }
8096
8129
  for (const act of this.opts.primaryActions?.() ?? []) list.appendChild(this.actionRow(doc, act));
8097
8130
  for (const panel of this.opts.panels()) {
@@ -8955,7 +8988,8 @@ var VelaWorkspace = class {
8955
8988
  onToggleSync: (id) => {
8956
8989
  const kind = id;
8957
8990
  this.sync.set(kind, this.syncOpts[kind] ? false : true);
8958
- }
8991
+ },
8992
+ glyph: () => layoutGlyph(this.def)
8959
8993
  },
8960
8994
  getContext: () => this.context()
8961
8995
  });
@@ -9454,6 +9488,7 @@ var VelaWorkspace = class {
9454
9488
  this.applyGrid();
9455
9489
  this.buildCells();
9456
9490
  this.alignNewCellStyles(preexisting);
9491
+ this.alignNewCellDrawings(preexisting);
9457
9492
  this.syncCellPresentation();
9458
9493
  this.refreshCellControls();
9459
9494
  this.topbar.setLayout(next.id);
@@ -9870,6 +9905,7 @@ var VelaWorkspace = class {
9870
9905
  const chart = cell.chart;
9871
9906
  chart.on("indicator:error", ({ error }) => this.toastHost.show(`[${cell.id}] ${error.message}`, "error", 5e3));
9872
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 }));
9873
9909
  chart.on("alert", (alert) => {
9874
9910
  const source = [parseSymbol(cell.symbol).ticker || cell.symbol, timeframeLabel(cell.timeframe), alert.indicator].filter(Boolean).join(" ");
9875
9911
  this.alerts.unshift({ cellId: cell.id, source, title: alert.title ?? "Alert", message: alert.message, time: alert.time });
@@ -9987,12 +10023,66 @@ var VelaWorkspace = class {
9987
10023
  }
9988
10024
  }
9989
10025
  /**
9990
- * Mirror an origin cell's presentation — the Canvas + Scales-and-lines slice of
9991
- * its renderer config plus its Status line tab prefs — onto its same-group
9992
- * followers (the style link). Loop-safe two ways: the busy guard eats the
9993
- * followers' SYNCHRONOUS echoes (their `applyConfig` re-fires `onConfigChanged`
9994
- * in the same tick), and the equality short-circuits leave already-converged
9995
- * 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.
9996
10086
  */
9997
10087
  propagateStylePrefs(originId) {
9998
10088
  if (this.styleSyncBusy || this.destroyed) return;
@@ -10000,17 +10090,15 @@ var VelaWorkspace = class {
10000
10090
  if (targets.length === 0) return;
10001
10091
  const origin = this.cellsById.get(originId);
10002
10092
  if (!origin) return;
10003
- const slice = styleConfigSlice(origin.chart.renderer.getConfig());
10004
- const sliceJson = slice ? JSON.stringify(slice) : null;
10093
+ const config = origin.chart.renderer.getConfig();
10005
10094
  const prefs = origin.statusPrefs();
10006
10095
  this.styleSyncBusy = true;
10007
10096
  try {
10008
10097
  for (const id of targets) {
10009
10098
  const cell = this.cellsById.get(id);
10010
10099
  if (!cell) continue;
10011
- if (slice && sliceJson !== JSON.stringify(styleConfigSlice(cell.chart.renderer.getConfig()))) {
10012
- cell.chart.renderer.applyConfig(slice);
10013
- }
10100
+ const patch = styleConfigPatch(config, cell.chart.renderer.getConfig());
10101
+ if (patch) cell.chart.renderer.applyConfig(patch);
10014
10102
  cell.applyStatusPrefs(prefs);
10015
10103
  }
10016
10104
  } finally {
@@ -10331,7 +10419,8 @@ var VelaWorkspace = class {
10331
10419
  onToggleSync: (id) => {
10332
10420
  const kind = id;
10333
10421
  this.sync.set(kind, this.syncOpts[kind] ? false : true);
10334
- }
10422
+ },
10423
+ glyph: () => layoutGlyph(this.def)
10335
10424
  },
10336
10425
  onOpenChange: (open) => this.trackDialog(open)
10337
10426
  }));