@luxalgo/vela 0.6.7 → 0.6.9

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-BsQM2WNH.d.cts → DataProvider-8Z95Q-RJ.d.cts} +1 -1
  2. package/dist/{DataProvider-BSLlBpB9.d.ts → DataProvider-DExJrfut.d.ts} +1 -1
  3. package/dist/{chunk-2R7BANEM.js → chunk-62SVGONC.js} +2 -2
  4. package/dist/{chunk-6WFKTAT4.js → chunk-6WDDVMBJ.js} +548 -147
  5. package/dist/{chunk-L3I2CCYO.js → chunk-EQCHJZOT.js} +49 -2
  6. package/dist/{chunk-OICPLNU7.js → chunk-FCVIG7JG.js} +188 -2
  7. package/dist/{chunk-WVP4XV5A.js → chunk-STHSKXOR.js} +407 -159
  8. package/dist/{chunk-PN5KWFZ4.js → chunk-WFSZBX3R.js} +2 -2
  9. package/dist/{contributions-Z12BrWCy.d.ts → contributions-C1U2Krwg.d.cts} +112 -7
  10. package/dist/{contributions-Hv4_cOJo.d.cts → contributions-D7PVZO2i.d.ts} +112 -7
  11. package/dist/index.cjs +755 -152
  12. package/dist/index.d.cts +12 -7
  13. package/dist/index.d.ts +12 -7
  14. package/dist/index.js +4 -4
  15. package/dist/{options-BaVTMXaO.d.ts → options-FM0peknS.d.cts} +84 -7
  16. package/dist/{options-BaVTMXaO.d.cts → options-FM0peknS.d.ts} +84 -7
  17. package/dist/{plugin-9koBjc5H.d.ts → plugin-7bkF32Rk.d.ts} +3 -3
  18. package/dist/{plugin-BXhDHTYn.d.cts → plugin-DfVqBz9p.d.cts} +3 -3
  19. package/dist/plugin.cjs +18 -2
  20. package/dist/plugin.d.cts +4 -4
  21. package/dist/plugin.d.ts +4 -4
  22. package/dist/plugin.js +2 -2
  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/{bottombar-Br4rzx_R.d.ts → statusline-DOPiT6I6.d.cts} +91 -6
  30. package/dist/{bottombar-Div5zibZ.d.cts → statusline-zdF4eZLr.d.ts} +91 -6
  31. package/dist/ui.cjs +201 -4
  32. package/dist/ui.d.cts +89 -2
  33. package/dist/ui.d.ts +89 -2
  34. package/dist/ui.js +3 -3
  35. package/dist/vela.global.js +755 -152
  36. package/dist/vela.global.min.js +93 -44
  37. package/dist/widget.cjs +1144 -271
  38. package/dist/widget.d.cts +12 -85
  39. package/dist/widget.d.ts +12 -85
  40. package/dist/widget.js +7 -7
  41. package/dist/workspace.cjs +1144 -271
  42. package/dist/workspace.d.cts +65 -11
  43. package/dist/workspace.d.ts +65 -11
  44. package/dist/workspace.js +6 -6
  45. package/package.json +1 -1
@@ -1,8 +1,8 @@
1
- import { parseSymbol, priceStyleIds, BUILTIN_PRICE_STYLES, tzButtonLabel, TIMEZONES, normalizeTimezone, tzMenuLabel, Vela, TypedEventBus, MultiProviderFeed, registerBuiltinChartTypes, resolveTheme, buildToolbar, createCustomMark, createAttributionMark, DrawingToolbar, applyAttributionMarkTheme, sharedBarStore, timeframeToMs } from './chunk-6WFKTAT4.js';
2
- import { chartType, resolveTopbarComposition, topbarActionOverride, TOPBAR_BUILTIN_IDS, widgetActions, SidePanel, sidePanels, DEFAULT_PANEL_ORDER, symbolRanking, resolveEngines, legendActionsProviderFor, statePersistenceHandlers, topbarHas, rendererDefaults, widgetAttachments, getDrawingType } from './chunk-L3I2CCYO.js';
3
- import { Tooltip, KeymapManager, isEditableTarget, Drawer } from './chunk-2R7BANEM.js';
4
- import { Menu, Dialog, applyPlotOverlayTokens, ensureUIHost } from './chunk-OICPLNU7.js';
5
- import { registerIcon, svg16, injectStyles, iconEl, iconMarkup, SESSION_PRE, SESSION_POST, SESSION_OFF, icon, categoricalColor } from './chunk-PN5KWFZ4.js';
1
+ import { parseSymbol, priceStyleIds, BUILTIN_PRICE_STYLES, tzButtonLabel, TIMEZONES, normalizeTimezone, tzMenuLabel, LEGEND_AT_TOP_ATTR, Vela, TypedEventBus, MultiProviderFeed, registerBuiltinChartTypes, resolveTheme, buildToolbar, createCustomMark, createAttributionMark, DrawingToolbar, applyAttributionMarkTheme, sharedBarStore, timeframeToMs } from './chunk-6WDDVMBJ.js';
2
+ import { chartType, resolveTopbarComposition, topbarActionOverride, TOPBAR_BUILTIN_IDS, widgetActions, SidePanel, sidePanels, DEFAULT_PANEL_ORDER, symbolRanking, resolveEngines, legendActionsProviderFor, legendCalloutsProviderFor, statePersistenceHandlers, topbarHas, rendererDefaults, widgetAttachments, getDrawingType } from './chunk-EQCHJZOT.js';
3
+ import { Tooltip, KeymapManager, isEditableTarget, Drawer } from './chunk-62SVGONC.js';
4
+ import { Menu, Dialog, CalloutBubble, applyPlotOverlayTokens, ensureUIHost } from './chunk-FCVIG7JG.js';
5
+ import { registerIcon, svg16, injectStyles, iconEl, iconMarkup, SESSION_OFF, SESSION_POST, SESSION_PRE, categoricalColor } from './chunk-WFSZBX3R.js';
6
6
  import { baseOf } from './chunk-W4EJWLEO.js';
7
7
 
8
8
  // src/widget/timeframe.ts
@@ -599,6 +599,11 @@ var Topbar = class {
599
599
  /** Overrides that LEFT their native slot (default side + a declared `order`) — they
600
600
  * render through the flow cluster like ordinary actions. */
601
601
  this.flowingOverrides = /* @__PURE__ */ new Set();
602
+ /** Tooltips of the CURRENT icon-only action buttons — rebuilt with every
603
+ * renderActions pass (contributed buttons are replaceChildren'd away). */
604
+ this.actionTooltips = [];
605
+ /** `iconOnly` misuse warned once per action id (renderActions re-runs freely). */
606
+ this.warnedIconless = /* @__PURE__ */ new Set();
602
607
  this.onHairlineSync = () => {
603
608
  if (this.hairlineRaf) return;
604
609
  const win = this.el.ownerDocument.defaultView;
@@ -649,7 +654,7 @@ var Topbar = class {
649
654
  this.leftActionsHost.className = "vela-widget-actions-left";
650
655
  this.panelsHost = doc.createElement("span");
651
656
  this.panelsHost.className = "vela-widget-panels";
652
- const tool = (cls, icon2, tip, onClick) => this.toolButton(cls, icon2, tip, onClick, this.tooltips);
657
+ const tool = (cls, icon, tip, onClick) => this.toolButton(cls, icon, tip, onClick, this.tooltips);
653
658
  if (vis("undo-redo")) {
654
659
  this.undoBtn = tool("vela-widget-undo", "undo", "Undo", opts.onUndoClick);
655
660
  this.redoBtn = tool("vela-widget-redo", "redo", "Redo", opts.onRedoClick);
@@ -847,8 +852,8 @@ var Topbar = class {
847
852
  }
848
853
  renderStyleButton(doc) {
849
854
  this.styleButton.replaceChildren();
850
- const icon2 = priceStyleIcon(this.priceStyle);
851
- if (icon2) this.styleButton.appendChild(iconEl(icon2, doc));
855
+ const icon = priceStyleIcon(this.priceStyle);
856
+ if (icon) this.styleButton.appendChild(iconEl(icon, doc));
852
857
  else this.styleButton.appendChild(doc.createTextNode(priceStyleLabel(this.priceStyle)));
853
858
  this.styleButton.setAttribute("aria-label", `Chart style \u2014 ${priceStyleLabel(this.priceStyle)}`);
854
859
  }
@@ -861,6 +866,8 @@ var Topbar = class {
861
866
  this.actionsHost.replaceChildren();
862
867
  this.leftActionsHost.replaceChildren();
863
868
  for (const pin of this.pinned.values()) pin.host.replaceChildren();
869
+ for (const t of this.actionTooltips) t.destroy();
870
+ this.actionTooltips = [];
864
871
  const doc = this.actionsHost.ownerDocument;
865
872
  const flowLeft = this.comp.left.includes("actions");
866
873
  const flowRight = this.comp.right.includes("actions");
@@ -870,10 +877,20 @@ var Topbar = class {
870
877
  if (!pin && builtin.has(action.id) && !this.flowingOverrides.has(action.id)) continue;
871
878
  const left = pin ? pin.left : action.align === "left";
872
879
  if (!pin && !(left ? flowLeft : flowRight)) continue;
880
+ const iconOnly = action.iconOnly === true && !!action.icon;
881
+ if (action.iconOnly === true && !action.icon && !this.warnedIconless.has(action.id)) {
882
+ this.warnedIconless.add(action.id);
883
+ console.warn(`[vela] widget action "${action.id}": iconOnly needs an \`icon\` \u2014 rendering the label instead.`);
884
+ }
873
885
  const b = doc.createElement("button");
874
- b.className = left ? "vela-widget-action-left" : "vela-widget-action";
886
+ b.className = left ? "vela-widget-action-left" : iconOnly ? "vela-widget-tool" : "vela-widget-action";
875
887
  if (action.icon) b.appendChild(iconEl(action.icon, doc));
876
- b.appendChild(doc.createTextNode(action.label));
888
+ if (iconOnly) {
889
+ b.setAttribute("aria-label", action.label);
890
+ this.actionTooltips.push(new Tooltip(b, { content: action.label, triggerId: `vela-action-${action.id}`, host: this.host }));
891
+ } else {
892
+ b.appendChild(doc.createTextNode(action.label));
893
+ }
877
894
  b.addEventListener("click", () => {
878
895
  const c = this.opts.getContext?.();
879
896
  if (c) action.run(c);
@@ -924,15 +941,15 @@ var Topbar = class {
924
941
  this.tfMenu.destroy();
925
942
  this.styleMenu.destroy();
926
943
  this.layoutPicker?.destroy();
927
- for (const t of [...this.tooltips, ...this.panelTooltips]) t.destroy();
944
+ for (const t of [...this.tooltips, ...this.panelTooltips, ...this.actionTooltips]) t.destroy();
928
945
  this.el.remove();
929
946
  }
930
947
  /** One icon-only tool button with its kit tooltip, parked in `sink` for disposal. */
931
- toolButton(cls, icon2, tip, onClick, sink) {
948
+ toolButton(cls, icon, tip, onClick, sink) {
932
949
  const doc = this.el.ownerDocument;
933
950
  const b = doc.createElement("button");
934
951
  b.className = `vela-widget-tool ${cls}`;
935
- b.appendChild(iconEl(icon2, doc));
952
+ b.appendChild(iconEl(icon, doc));
936
953
  b.setAttribute("aria-label", tip);
937
954
  sink.push(new Tooltip(b, { content: tip, triggerId: `vela-tool-${cls}`, host: this.host }));
938
955
  if (onClick) b.addEventListener("click", onClick);
@@ -1612,13 +1629,13 @@ var MenuBuilder = class {
1612
1629
  this.actions = /* @__PURE__ */ new Map();
1613
1630
  this.seq = 0;
1614
1631
  }
1615
- entry(label, icon2, run, separatorBefore = false) {
1632
+ entry(label, icon, run, separatorBefore = false) {
1616
1633
  const id = this.next();
1617
1634
  this.actions.set(id, run);
1618
- return { id, label, icon: icon2, separatorBefore };
1635
+ return { id, label, icon, separatorBefore };
1619
1636
  }
1620
- submenu(label, icon2, submenu, separatorBefore = false) {
1621
- return { id: this.next(), label, icon: icon2, submenu, separatorBefore };
1637
+ submenu(label, icon, submenu, separatorBefore = false) {
1638
+ return { id: this.next(), label, icon, submenu, separatorBefore };
1622
1639
  }
1623
1640
  next() {
1624
1641
  this.seq += 1;
@@ -1855,8 +1872,8 @@ var ObjectTree = class extends SidePanel {
1855
1872
  const acts = doc.createElement("span");
1856
1873
  acts.className = "vela-ot-acts";
1857
1874
  head.appendChild(acts);
1858
- const op = (icon2, title, run) => {
1859
- acts.appendChild(this.btn({ icon: icon2, title, run }));
1875
+ const op = (icon, title, run) => {
1876
+ acts.appendChild(this.btn({ icon, title, run }));
1860
1877
  };
1861
1878
  if (pane.kind !== "price") {
1862
1879
  if (pane.order > 1) op("arrow-up", "Move pane up", () => chart.panes.move(pane.id, "up"));
@@ -1871,8 +1888,8 @@ var ObjectTree = class extends SidePanel {
1871
1888
  const { chart } = pass;
1872
1889
  if (row.kind === "price") {
1873
1890
  const base = this.symbolName.replace(/[-_/]?(USDT|USDC|USD1|USDS|BUSD|USD|EUR|PERP)$/i, "") || this.symbolName;
1874
- const icon2 = tickerIconEl(doc, base || "P", this.symbolName || "Price", "vela-ot-avatar", this.symbolRaw ? this.iconFor?.(this.symbolRaw) : void 0);
1875
- const el2 = this.row(icon2, row.label, row.visible, [
1891
+ const icon = tickerIconEl(doc, base || "P", this.symbolName || "Price", "vela-ot-avatar", this.symbolRaw ? this.iconFor?.(this.symbolRaw) : void 0);
1892
+ const el2 = this.row(icon, row.label, row.visible, [
1876
1893
  {
1877
1894
  icon: row.visible ? "eye" : "eye-off",
1878
1895
  title: row.visible ? "Hide" : "Show",
@@ -1991,16 +2008,16 @@ var ObjectTree = class extends SidePanel {
1991
2008
  const doc = this.el.ownerDocument;
1992
2009
  const { chart } = pass;
1993
2010
  const markup = drawingMeta(d.type).icon;
1994
- let icon2;
2011
+ let icon;
1995
2012
  if (markup) {
1996
- icon2 = doc.createElement("span");
1997
- icon2.className = "vela-icon";
1998
- icon2.setAttribute("aria-hidden", "true");
1999
- icon2.innerHTML = markup;
2013
+ icon = doc.createElement("span");
2014
+ icon.className = "vela-icon";
2015
+ icon.setAttribute("aria-hidden", "true");
2016
+ icon.innerHTML = markup;
2000
2017
  } else {
2001
- icon2 = iconEl("pen", doc);
2018
+ icon = iconEl("pen", doc);
2002
2019
  }
2003
- const el = this.row(icon2, drawingLabel(d), d.visible, [
2020
+ const el = this.row(icon, drawingLabel(d), d.visible, [
2004
2021
  {
2005
2022
  icon: d.locked ? "lock" : "unlock",
2006
2023
  title: d.locked ? "Unlock" : "Lock",
@@ -2066,7 +2083,7 @@ var ObjectTree = class extends SidePanel {
2066
2083
  }
2067
2084
  }
2068
2085
  /** The shared row shell: icon, name, optional tag, then the action buttons. */
2069
- row(icon2, label, visible, actions, tag) {
2086
+ row(icon, label, visible, actions, tag) {
2070
2087
  const doc = this.el.ownerDocument;
2071
2088
  const el = doc.createElement("div");
2072
2089
  el.className = "vela-ot-row";
@@ -2075,7 +2092,7 @@ var ObjectTree = class extends SidePanel {
2075
2092
  name.className = "vela-ot-name";
2076
2093
  name.textContent = label;
2077
2094
  name.title = label;
2078
- el.append(icon2, name);
2095
+ el.append(icon, name);
2079
2096
  if (tag) {
2080
2097
  const t = doc.createElement("span");
2081
2098
  t.className = "vela-ot-tag";
@@ -3667,19 +3684,8 @@ function indicatorLedger(i) {
3667
3684
  };
3668
3685
  }
3669
3686
 
3670
- // src/workspace/sync.ts
3671
- function syncTargets(originId, setting, cellIds) {
3672
- if (setting == null || setting === false) return [];
3673
- if (setting === true) return cellIds.filter((id) => id !== originId);
3674
- const group = setting[originId];
3675
- if (group == null) return [];
3676
- return cellIds.filter((id) => id !== originId && setting[id] === group);
3677
- }
3678
- function rangesWithin(a, b, epsMs) {
3679
- return Math.abs(a.from - b.from) <= epsMs && Math.abs(a.to - b.to) <= epsMs;
3680
- }
3681
-
3682
3687
  // src/state/document.ts
3688
+ var SYNC_KINDS = ["viewport", "symbol", "timeframe", "crosshair", "drawings", "style"];
3683
3689
  function prefixedSymbol(cell) {
3684
3690
  if (!cell?.symbol) return void 0;
3685
3691
  if (cell.symbol.includes(":") || !cell.provider) return cell.symbol;
@@ -3767,7 +3773,7 @@ function sanitizeSync(raw) {
3767
3773
  if (raw == null || typeof raw !== "object") return null;
3768
3774
  const s = raw;
3769
3775
  const out = {};
3770
- for (const kind of ["viewport", "symbol", "timeframe", "crosshair", "drawings"]) {
3776
+ for (const kind of SYNC_KINDS) {
3771
3777
  const v = s[kind];
3772
3778
  if (v === true) out[kind] = true;
3773
3779
  else if (v != null && typeof v === "object") {
@@ -3810,6 +3816,28 @@ function sanitizeTrackSizes(raw) {
3810
3816
  return Object.keys(out).length > 0 ? out : null;
3811
3817
  }
3812
3818
 
3819
+ // src/workspace/sync.ts
3820
+ function syncTargets(originId, setting, cellIds) {
3821
+ if (setting == null || setting === false) return [];
3822
+ if (setting === true) return cellIds.filter((id) => id !== originId);
3823
+ const group = setting[originId];
3824
+ if (group == null) return [];
3825
+ return cellIds.filter((id) => id !== originId && setting[id] === group);
3826
+ }
3827
+ function rangesWithin(a, b, epsMs) {
3828
+ return Math.abs(a.from - b.from) <= epsMs && Math.abs(a.to - b.to) <= epsMs;
3829
+ }
3830
+ var STYLE_SYNC_CONFIG_KEYS = ["layout", "panes", "grid", "priceScale", "crosshair"];
3831
+ function styleConfigSlice(config) {
3832
+ if (config == null || typeof config !== "object") return null;
3833
+ const doc = config;
3834
+ const out = {};
3835
+ for (const key of STYLE_SYNC_CONFIG_KEYS) {
3836
+ if (doc[key] != null && typeof doc[key] === "object") out[key] = doc[key];
3837
+ }
3838
+ return Object.keys(out).length > 0 ? out : null;
3839
+ }
3840
+
3813
3841
  // src/workspace/persist.ts
3814
3842
  var memoryStore = /* @__PURE__ */ new Map();
3815
3843
  function memoryStorageAdapter() {
@@ -3912,45 +3940,24 @@ var CSS10 = `
3912
3940
  }
3913
3941
  .vela-statusline .vela-sl-symbol { font-weight: 600; font-size: var(--vela-font-size-lg); }
3914
3942
  .vela-statusline .vela-sl-meta { color: var(--vela-fg-muted); font-size: var(--vela-font-size-md); font-weight: 600; }
3915
- /* Market status badge \u2014 icon-only 16px circle, label on hover (kit tooltip). */
3916
- .vela-statusline .vela-sl-market {
3917
- display: inline-grid;
3918
- place-items: center;
3919
- width: 16px;
3920
- height: 16px;
3921
- border-radius: 50%;
3922
- flex: none;
3923
- align-self: center;
3924
- line-height: 0;
3925
- cursor: default;
3926
- }
3927
- .vela-statusline .vela-sl-market svg {
3928
- width: 12px;
3929
- height: 12px;
3930
- display: block;
3931
- }
3932
- /* Open wears the theme's up color; the other sessions are meaning constants from the
3933
- * palette (amber pre, sky post, gray closed/holiday). Same ink at 20% for the circle. */
3934
- .vela-statusline .vela-sl-market[data-status='open'] {
3935
- background: color-mix(in srgb, var(--vela-up) 20%, transparent);
3936
- color: var(--vela-up);
3937
- }
3938
- .vela-statusline .vela-sl-market[data-status='pre'] { background: color-mix(in srgb, ${SESSION_PRE} 20%, transparent); color: ${SESSION_PRE}; }
3939
- .vela-statusline .vela-sl-market[data-status='post'] { background: color-mix(in srgb, ${SESSION_POST} 20%, transparent); color: ${SESSION_POST}; }
3940
- .vela-statusline .vela-sl-market[data-status='closed'],
3941
- .vela-statusline .vela-sl-market[data-status='holiday'] { background: color-mix(in srgb, ${SESSION_OFF} 20%, transparent); color: ${SESSION_OFF}; }
3943
+ /* Market status badge \u2014 a kit callout bubble (icon-only 16px circle, label on hover
3944
+ * via the kit tooltip); the session tint is applied per status in setMarketStatus. */
3945
+ .vela-statusline .vela-sl-market { align-self: center; }
3942
3946
  .vela-statusline .vela-sl-ohlc { display: flex; gap: var(--vela-space-1); color: var(--vela-fg-muted); }
3943
3947
  .vela-statusline .vela-sl-ohlc b { color: var(--vela-fg); font-weight: 500; }
3944
3948
  /* The change value wears the SAME ink as the OHLC values (set inline per render) \u2014
3945
3949
  * these are the pre-ink fallbacks only. */
3946
3950
  .vela-statusline .vela-sl-change[data-dir='up'] { color: var(--vela-up); }
3947
3951
  .vela-statusline .vela-sl-change[data-dir='down'] { color: var(--vela-down); }
3948
- /* Stack the renderer's PRICE-pane legend below the status line (study panes stay put).
3949
- * The renderer sets the legend's inline top \u2014 shift with a transform, don't fight it.
3950
- * Scoped to hosts that actually CARRY a status line (the marker class set by the
3951
- * Statusline constructor) \u2014 the stylesheet is document-global, so a bare container
3952
- * class here would shift every chart on the page, including statusline-less ones. */
3953
- .vela-has-statusline [data-vela-pane='price'] { transform: translateY(26px); }
3952
+ /* Stack the TOP pane's legend below the status line (lower study panes stay put).
3953
+ * The renderer marks whichever legend sits at the plot's top edge \u2014 the price pane
3954
+ * normally, or a maximized study pane filling the plot \u2014 so the legend never merges
3955
+ * with the status line whichever pane owns the top. The renderer sets the legend's
3956
+ * inline top \u2014 shift with a transform, don't fight it. Scoped to hosts that actually
3957
+ * CARRY a status line (the marker class set by the Statusline constructor) \u2014 the
3958
+ * stylesheet is document-global, so a bare attribute selector here would shift every
3959
+ * chart on the page, including statusline-less ones. */
3960
+ .vela-has-statusline [${LEGEND_AT_TOP_ATTR}] { transform: translateY(26px); }
3954
3961
  /* Mobile: two-line chip \u2014 logo / symbol / meta / market status on one aligned row, the
3955
3962
  * bar change on the next. Full O/H/L/C stays hidden (too dense on a phone-width plot).
3956
3963
  * GRID, not a wrapping flexbox: an absolutely positioned wrapping flex container sizes
@@ -3986,7 +3993,7 @@ var CSS10 = `
3986
3993
  font-size: var(--vela-font-size-sm);
3987
3994
  line-height: 1.2;
3988
3995
  }
3989
- [data-layout='mobile'] .vela-has-statusline [data-vela-pane='price'] { transform: translateY(40px); }
3996
+ [data-layout='mobile'] .vela-has-statusline [${LEGEND_AT_TOP_ATTR}] { transform: translateY(40px); }
3990
3997
  /* FIT mode (multi-chart cells \u2014 see setFitMode): the line never wraps; segments that
3991
3998
  * don't fit are HIDDEN by fit() (change first, then meta, then the market badge), so
3992
3999
  * overflow:hidden only guards the transient between a resize and the next measure.
@@ -4003,7 +4010,7 @@ var CSS10 = `
4003
4010
  padding-left: 0;
4004
4011
  }
4005
4012
  /* One row again \u2014 the mobile two-line shift doesn't apply in fit mode. */
4006
- [data-layout='mobile'] .vela-sl-fit-host.vela-has-statusline [data-vela-pane='price'] { transform: translateY(26px); }
4013
+ [data-layout='mobile'] .vela-sl-fit-host.vela-has-statusline [${LEGEND_AT_TOP_ATTR}] { transform: translateY(26px); }
4007
4014
  `;
4008
4015
  function baseOfTicker(ticker) {
4009
4016
  return ticker.replace(/[-_/]?(USDT|USDC|USD1|USDS|BUSD|USD|EUR|PERP)$/i, "") || ticker;
@@ -4015,6 +4022,13 @@ var MARKET_LABELS = {
4015
4022
  closed: "Market Closed",
4016
4023
  holiday: "Market Holiday"
4017
4024
  };
4025
+ var MARKET_INKS = {
4026
+ open: "var(--vela-up)",
4027
+ pre: SESSION_PRE,
4028
+ post: SESSION_POST,
4029
+ closed: SESSION_OFF,
4030
+ holiday: SESSION_OFF
4031
+ };
4018
4032
  function statuslineInkOf(renderer, priceStyle) {
4019
4033
  const cfg = renderer.getConfig();
4020
4034
  const c = (v) => typeof v === "string" ? v : null;
@@ -4073,8 +4087,15 @@ var Statusline = class {
4073
4087
  this.symbolEl.textContent = ticker;
4074
4088
  this.metaEl = doc.createElement("span");
4075
4089
  this.metaEl.className = "vela-sl-meta";
4076
- this.marketEl = doc.createElement("span");
4077
- this.marketEl.className = "vela-sl-market";
4090
+ this.marketBubble = new CalloutBubble({
4091
+ icon: "market-open",
4092
+ background: `color-mix(in srgb, ${MARKET_INKS.open} 20%, transparent)`,
4093
+ color: MARKET_INKS.open,
4094
+ label: MARKET_LABELS.open,
4095
+ host
4096
+ });
4097
+ this.marketEl = this.marketBubble.el;
4098
+ this.marketEl.classList.add("vela-sl-market");
4078
4099
  this.ohlcEl = doc.createElement("span");
4079
4100
  this.ohlcEl.className = "vela-sl-ohlc";
4080
4101
  this.changeEl = doc.createElement("span");
@@ -4162,8 +4183,13 @@ var Statusline = class {
4162
4183
  * hover label. Callers with no session model leave the constructor's 'open'. */
4163
4184
  setMarketStatus(status) {
4164
4185
  this.marketEl.dataset.status = status;
4165
- this.marketEl.innerHTML = icon(`market-${status}`);
4166
- this.marketEl.setAttribute("aria-label", MARKET_LABELS[status]);
4186
+ const ink = MARKET_INKS[status];
4187
+ this.marketBubble.set({
4188
+ icon: `market-${status}`,
4189
+ background: `color-mix(in srgb, ${ink} 20%, transparent)`,
4190
+ color: ink,
4191
+ label: MARKET_LABELS[status]
4192
+ });
4167
4193
  this.marketTip.setContent(MARKET_LABELS[status]);
4168
4194
  }
4169
4195
  setPartVisible(part, visible) {
@@ -4196,6 +4222,7 @@ var Statusline = class {
4196
4222
  this.fitRO?.disconnect();
4197
4223
  this.fitRO = null;
4198
4224
  this.marketTip.destroy();
4225
+ this.marketBubble.destroy();
4199
4226
  this.host.classList.remove("vela-has-statusline", "vela-sl-fit-host");
4200
4227
  this.el.remove();
4201
4228
  }
@@ -4645,61 +4672,148 @@ var MarketStatusTracker = class {
4645
4672
  };
4646
4673
 
4647
4674
  // src/widget/session-shading.ts
4648
- function deriveSessionZones(regular, extended) {
4675
+ var DAY_MS = 864e5;
4676
+ var MINUTE_MS = 6e4;
4677
+ function parseWindow(text) {
4678
+ if (typeof text !== "string") return null;
4679
+ const m = /^(\d{2})(\d{2})-(\d{2})(\d{2})$/.exec(text);
4680
+ if (!m) return null;
4681
+ const start = Number(m[1]) * 60 + Number(m[2]);
4682
+ const end = Number(m[3]) * 60 + Number(m[4]);
4683
+ if (start >= end || end > 1440) return null;
4684
+ return { start, end };
4685
+ }
4686
+ function parseSessionSpec(si) {
4687
+ const session = si?.["session"];
4688
+ if (typeof session !== "string" || session === "" || session === "24x7") return null;
4689
+ const regular = parseWindow(session);
4690
+ if (!regular) return null;
4691
+ const tz = si?.["timezone"];
4692
+ const timezone = typeof tz === "string" && tz !== "" ? tz : "Etc/UTC";
4693
+ const ext = parseWindow(si?.["session_extended"]);
4694
+ const extended = ext && ext.start <= regular.start && ext.end >= regular.end ? ext : { start: 0, end: 1440 };
4695
+ return { regular, extended, timezone };
4696
+ }
4697
+ var dtfCache = /* @__PURE__ */ new Map();
4698
+ function civilFormatter(tz) {
4699
+ const cached = dtfCache.get(tz);
4700
+ if (cached !== void 0) return cached;
4701
+ let dtf = null;
4702
+ try {
4703
+ dtf = new Intl.DateTimeFormat("en-US", {
4704
+ timeZone: tz,
4705
+ hourCycle: "h23",
4706
+ weekday: "short",
4707
+ year: "numeric",
4708
+ month: "2-digit",
4709
+ day: "2-digit",
4710
+ hour: "2-digit",
4711
+ minute: "2-digit"
4712
+ });
4713
+ } catch {
4714
+ dtf = null;
4715
+ }
4716
+ dtfCache.set(tz, dtf);
4717
+ return dtf;
4718
+ }
4719
+ function civilParts(dtf, ms) {
4720
+ const parts = dtf.formatToParts(ms);
4721
+ const get = (t) => parts.find((p) => p.type === t)?.value ?? "";
4722
+ return {
4723
+ weekday: get("weekday"),
4724
+ year: Number(get("year")),
4725
+ month: Number(get("month")),
4726
+ day: Number(get("day")),
4727
+ hour: Number(get("hour")) % 24,
4728
+ minute: Number(get("minute"))
4729
+ };
4730
+ }
4731
+ function expandSessionZones(spec, from, to) {
4649
4732
  const pre = [];
4650
4733
  const post = [];
4651
- for (const [extStart, extEnd] of extended) {
4652
- const inside = regular.filter(([s, e]) => e > extStart && s < extEnd).sort((a, b) => a[0] - b[0]);
4653
- let cursor = extStart;
4654
- for (const [regStart, regEnd] of inside) {
4655
- if (regStart > cursor) pre.push([cursor, Math.min(regStart, extEnd)]);
4656
- cursor = Math.max(cursor, regEnd);
4657
- }
4658
- if (cursor < extEnd) post.push([cursor, extEnd]);
4734
+ const dtf = civilFormatter(spec.timezone);
4735
+ if (!dtf || !Number.isFinite(from) || !Number.isFinite(to)) return { pre, post };
4736
+ const seen = /* @__PURE__ */ new Set();
4737
+ for (let cursor = from - DAY_MS; cursor < to + DAY_MS; cursor += DAY_MS) {
4738
+ const civil = civilParts(dtf, cursor);
4739
+ const key = civil.year * 1e4 + civil.month * 100 + civil.day;
4740
+ if (!Number.isFinite(key) || seen.has(key)) continue;
4741
+ seen.add(key);
4742
+ if (civil.weekday === "Sat" || civil.weekday === "Sun") continue;
4743
+ const naive = Date.UTC(civil.year, civil.month - 1, civil.day);
4744
+ const noonGuess = naive + 720 * MINUTE_MS;
4745
+ const atNoon = civilParts(dtf, noonGuess);
4746
+ const offset = Date.UTC(atNoon.year, atNoon.month - 1, atNoon.day, atNoon.hour, atNoon.minute) - noonGuess;
4747
+ const at = (minutes) => naive + minutes * MINUTE_MS - offset;
4748
+ if (spec.extended.start < spec.regular.start) pre.push([at(spec.extended.start), at(spec.regular.start)]);
4749
+ if (spec.regular.end < spec.extended.end) post.push([at(spec.regular.end), at(spec.extended.end)]);
4659
4750
  }
4660
4751
  return { pre, post };
4661
4752
  }
4662
- var FETCH_AHEAD_MS2 = 10 * 864e5;
4663
- var MIN_LOOKBACK_MS = 3 * 864e5;
4664
- var MAX_LOOKBACK_MS = 120 * 864e5;
4753
+ var COVER_PAD_MIN_MS = 2 * DAY_MS;
4665
4754
  var SessionShadingTracker = class {
4666
4755
  constructor(onZones) {
4667
4756
  this.onZones = onZones;
4668
- /** Invalidates detached async work — bumped by every track()/stop(). */
4757
+ /** Invalidates the one async step (metadata resolution) — bumped by track()/stop(). */
4669
4758
  this.epoch = 0;
4670
- }
4671
- /** (Re)bind to a chart's data surface + market and evaluate once. */
4759
+ this.spec = null;
4760
+ this.ready = false;
4761
+ this.session = "regular";
4762
+ /** Whether one bar is shorter than a civil day — only then do pre/post bars exist. */
4763
+ this.intraday = false;
4764
+ this.covered = null;
4765
+ /** The newest range seen — viewport moves during metadata resolution (a load's fit
4766
+ * animation) must not be lost, so the resolution always expands the LATEST range. */
4767
+ this.lastRange = null;
4768
+ }
4769
+ /** (Re)bind to a chart's data surface + market and expand once metadata lands. */
4672
4770
  track(data, symbol, opts) {
4673
4771
  const my = ++this.epoch;
4674
- void this.evaluate(my, data, symbol, opts);
4772
+ this.ready = false;
4773
+ this.spec = null;
4774
+ this.covered = null;
4775
+ this.session = opts.session;
4776
+ const tfMs = timeframeMs(opts.timeframe);
4777
+ this.intraday = Number.isFinite(tfMs) && tfMs < DAY_MS;
4778
+ this.lastRange = opts.range;
4779
+ void data.symbolInfo(symbol).catch(() => void 0).then((si) => {
4780
+ if (my !== this.epoch) return;
4781
+ this.ready = true;
4782
+ this.spec = parseSessionSpec(si);
4783
+ this.emit(this.lastRange ?? opts.range, true);
4784
+ });
4785
+ }
4786
+ /** Follow a pan/zoom synchronously: bands are epoch-anchored, so only a range that
4787
+ * leaves the last expansion's coverage needs a recompute — no fetch, no debounce. */
4788
+ updateRange(range) {
4789
+ this.lastRange = range;
4790
+ if (!this.ready) return;
4791
+ this.emit(range, false);
4675
4792
  }
4676
4793
  stop() {
4677
4794
  this.epoch += 1;
4678
- }
4679
- async evaluate(my, data, symbol, opts) {
4680
- const resolved = data.resolve(symbol);
4681
- const provider = resolved ? data.providerInstance(resolved.provider) : void 0;
4682
- const si = await data.symbolInfo(symbol).catch(() => void 0);
4683
- if (my !== this.epoch) return;
4684
- const hasSessions = typeof si?.session === "string" && si.session !== "" && si.session !== "24x7";
4685
- if (!provider?.getCalendar || !hasSessions || !resolved) {
4686
- this.onZones(null);
4795
+ this.ready = false;
4796
+ this.spec = null;
4797
+ this.covered = null;
4798
+ this.lastRange = null;
4799
+ }
4800
+ emit(range, force) {
4801
+ if (!this.spec) {
4802
+ if (force) this.onZones(null);
4687
4803
  return;
4688
4804
  }
4689
- if (opts.session !== "extended") {
4690
- this.onZones({ pre: [], post: [] });
4805
+ if (this.session !== "extended" || !this.intraday) {
4806
+ if (force) this.onZones({ pre: [], post: [] });
4691
4807
  return;
4692
4808
  }
4693
- const now = Date.now();
4694
- const lookback = Math.max(MIN_LOOKBACK_MS, Math.min(MAX_LOOKBACK_MS, opts.lookbackMs));
4695
- const range = { from: now - lookback, to: now + FETCH_AHEAD_MS2 };
4696
- const [regular, extended] = await Promise.all([
4697
- provider.getCalendar(resolved.ticker, { ...range, session: "regular" }).catch(() => null),
4698
- provider.getCalendar(resolved.ticker, { ...range, session: "extended" }).catch(() => null)
4699
- ]);
4700
- if (my !== this.epoch) return;
4701
- if (!regular || !extended) return;
4702
- this.onZones(deriveSessionZones(regular, extended));
4809
+ const from = Math.min(range.from, range.to);
4810
+ const to = Math.max(range.from, range.to);
4811
+ if (!Number.isFinite(from) || !Number.isFinite(to)) return;
4812
+ if (!force && this.covered && from >= this.covered.from && to <= this.covered.to) return;
4813
+ const pad = Math.max(to - from, COVER_PAD_MIN_MS);
4814
+ const covered = { from: from - pad, to: to + pad };
4815
+ this.covered = covered;
4816
+ this.onZones(expandSessionZones(this.spec, covered.from, covered.to));
4703
4817
  }
4704
4818
  };
4705
4819
 
@@ -4891,6 +5005,7 @@ var ChartCell = class {
4891
5005
  this.inner.renderer.set("attribution", false);
4892
5006
  this.inner.renderer.set("dialogHost", deps.dialogHost);
4893
5007
  this.inner.renderer.setLegendActions(legendActionsProviderFor(this.inner, () => deps.context()));
5008
+ this.inner.renderer.setLegendCallouts(legendCalloutsProviderFor(this.inner, () => deps.context()));
4894
5009
  if (this.inner.renderer.supports("historyChords")) this.inner.renderer.set("historyChords", false);
4895
5010
  this.history.onChart(this.inner);
4896
5011
  this.inner.renderer.onConfigChanged(() => {
@@ -4924,7 +5039,11 @@ var ChartCell = class {
4924
5039
  this.deps.toast(`No registered provider serves "${symbol2}" (registered: ${list})`, "error", 6e3);
4925
5040
  });
4926
5041
  this.inner.on("load:start", () => this.watermark?.setLoading(true));
4927
- this.inner.on("load:end", () => this.watermark?.setLoading(false));
5042
+ this.inner.on("load:end", () => {
5043
+ this.watermark?.setLoading(false);
5044
+ this.refreshSessionShading();
5045
+ });
5046
+ this.inner.on("viewport:changed", (range) => this.sessionShading.updateRange(range));
4928
5047
  const tz = deps.timezone();
4929
5048
  if (tz !== "Etc/UTC") this.inner.renderer.set("timezone", tz);
4930
5049
  this.indicatorTitlesOn = seed.indicatorTitles ?? true;
@@ -5003,20 +5122,30 @@ var ChartCell = class {
5003
5122
  this.refreshNativeCatalog();
5004
5123
  this.pushSettingsSections();
5005
5124
  this.offMarket = this.inner.on("market:changed", ({ symbol: symbol2, timeframe }) => {
5006
- this.state.symbol = symbol2;
5007
- this.state.provider = parseSymbol(symbol2).provider ?? void 0;
5008
- this.state.timeframe = timeframe;
5009
- this.state.session = normalizeSession(this.inner?.market.session);
5010
- this.watermark?.update(symbol2, timeframe);
5011
- this.statusline?.setSymbol(symbol2);
5012
- this.statusline?.setMeta(timeframe, this.inner?.data.displayPrefix(symbol2) ?? this.state.provider ?? "");
5013
- if (this.inner) this.statusline?.onChart(this.inner);
5125
+ this.projectMarket(symbol2, timeframe);
5014
5126
  this.refreshNativeCatalog();
5015
5127
  this.refreshSessionAvailable();
5016
5128
  if (this.inner) this.marketStatus?.track(this.inner.data, symbol2);
5017
5129
  this.deps.onMarketChanged(this.id);
5018
5130
  });
5019
5131
  }
5132
+ /**
5133
+ * Project a market identity into the cell state and its display overlays (watermark,
5134
+ * statusline), WITHOUT the data-dependent bookkeeping. Runs twice per user pick: once
5135
+ * optimistically from the cell setters — the labels reflect the pick immediately, not
5136
+ * after the bars load — and again from `market:changed` (the committed pass, and the
5137
+ * only pass for host `chart.setMarket` calls). Idempotent, so the double run converges.
5138
+ */
5139
+ projectMarket(symbol, timeframe) {
5140
+ this.state.symbol = symbol;
5141
+ this.state.provider = parseSymbol(symbol).provider ?? void 0;
5142
+ this.state.timeframe = timeframe;
5143
+ this.state.session = normalizeSession(this.inner?.market.session);
5144
+ this.watermark?.update(symbol, timeframe);
5145
+ this.statusline?.setSymbol(symbol);
5146
+ this.statusline?.setMeta(timeframe, this.inner?.data.displayPrefix(symbol) ?? this.state.provider ?? "");
5147
+ if (this.inner) this.statusline?.onChart(this.inner);
5148
+ }
5020
5149
  /**
5021
5150
  * Does this cell's market HAVE sessions (RTH/ETH meaningful)? Derived from the
5022
5151
  * symbol's own metadata (`syminfo.session !== '24x7'`), asynchronously — the
@@ -5051,14 +5180,18 @@ var ChartCell = class {
5051
5180
  this.refreshSessionShading();
5052
5181
  });
5053
5182
  }
5054
- /** (Re)derive the pre/post-market shading bands for this cell's market. The loaded
5055
- * depth (bars × timeframe) bounds the calendar fetch; the tracker clamps it. */
5183
+ /** (Re)derive the pre/post-market shading bands for this cell's market. The bands
5184
+ * expand locally from the symbol's session vocabulary, so they paint as soon as
5185
+ * metadata is known and follow any pan depth without provider round trips. */
5056
5186
  refreshSessionShading() {
5057
5187
  const chart = this.inner;
5058
5188
  const symbol = this.state.symbol;
5059
5189
  if (!chart || !symbol) return;
5060
- const lookbackMs = Math.max(this.state.bars ?? 1e3, this.rangeBars) * timeframeMs(this.state.timeframe ?? "60");
5061
- this.sessionShading.track(chart.data, symbol, { session: this.session, lookbackMs });
5190
+ const now = Date.now();
5191
+ const requestedSpan = Math.max(this.state.bars ?? 1e3, this.rangeBars) * timeframeMs(this.state.timeframe ?? "60");
5192
+ const fallbackSpan = Number.isFinite(requestedSpan) ? Math.max(3 * 864e5, requestedSpan) : 3 * 864e5;
5193
+ const range = chart.getVisibleRange() ?? { from: now - fallbackSpan, to: now };
5194
+ this.sessionShading.track(chart.data, symbol, { session: this.session, timeframe: this.timeframe, range });
5062
5195
  }
5063
5196
  /** The session-shade colors live in the renderer CONFIG (persisted with it, edited
5064
5197
  * live by the dialog swatch) — the cell only proxies them into its settings rows. */
@@ -5154,10 +5287,10 @@ var ChartCell = class {
5154
5287
  id: "status-line",
5155
5288
  rows: [
5156
5289
  { kind: "heading", label: "Status line", id: "parts" },
5157
- { kind: "toggle", label: "Symbol name", id: "name", get: () => sl.partVisible("name"), set: (v) => sl.setPartVisible("name", v) },
5158
- { kind: "toggle", label: "Market status", id: "market", get: () => sl.partVisible("market"), set: (v) => sl.setPartVisible("market", v) },
5159
- { kind: "toggle", label: "OHLC values", id: "ohlc", get: () => sl.partVisible("ohlc"), set: (v) => sl.setPartVisible("ohlc", v) },
5160
- { kind: "toggle", label: "Bar change values", id: "change", get: () => sl.partVisible("change"), set: (v) => sl.setPartVisible("change", v) },
5290
+ { kind: "toggle", label: "Symbol name", id: "name", get: () => sl.partVisible("name"), set: (v) => this.setStatuslinePart("name", v) },
5291
+ { kind: "toggle", label: "Market status", id: "market", get: () => sl.partVisible("market"), set: (v) => this.setStatuslinePart("market", v) },
5292
+ { kind: "toggle", label: "OHLC values", id: "ohlc", get: () => sl.partVisible("ohlc"), set: (v) => this.setStatuslinePart("ohlc", v) },
5293
+ { kind: "toggle", label: "Bar change values", id: "change", get: () => sl.partVisible("change"), set: (v) => this.setStatuslinePart("change", v) },
5161
5294
  { kind: "heading", label: "Indicators", id: "indicators" },
5162
5295
  {
5163
5296
  kind: "toggle",
@@ -5192,12 +5325,40 @@ var ChartCell = class {
5192
5325
  this.indicatorTitlesOn = visible;
5193
5326
  this.inner?.renderer.set("indicatorTitles", visible);
5194
5327
  this.deps.onStateDirty();
5328
+ this.deps.onStatusPrefsChanged(this.id);
5195
5329
  }
5196
5330
  /** Show/hide the plot values beside this cell's legend titles (persisted per cell). */
5197
5331
  setIndicatorValuesVisible(visible) {
5198
5332
  this.indicatorValuesOn = visible;
5199
5333
  this.inner?.renderer.set("indicatorValues", visible);
5200
5334
  this.deps.onStateDirty();
5335
+ this.deps.onStatusPrefsChanged(this.id);
5336
+ }
5337
+ /** Show/hide one status-line segment (the settings dialog's Status line tab). */
5338
+ setStatuslinePart(part, visible) {
5339
+ this.statusline?.setPartVisible(part, visible);
5340
+ this.deps.onStatusPrefsChanged(this.id);
5341
+ }
5342
+ /** This cell's Status line tab prefs as one bundle (see {@link CellStatusPrefs}). */
5343
+ statusPrefs() {
5344
+ const sl = this.statusline;
5345
+ return {
5346
+ parts: sl ? { name: sl.partVisible("name"), market: sl.partVisible("market"), ohlc: sl.partVisible("ohlc"), change: sl.partVisible("change") } : null,
5347
+ indicatorTitles: this.indicatorTitlesOn,
5348
+ indicatorValues: this.indicatorValuesOn
5349
+ };
5350
+ }
5351
+ /** Converge this cell's Status line tab prefs to `prefs` — the follower half of
5352
+ * the workspace's style link. Idempotent: matching values change nothing, so a
5353
+ * propagated echo dies on its own. */
5354
+ applyStatusPrefs(prefs) {
5355
+ if (prefs.parts && this.statusline) {
5356
+ for (const part of Object.keys(prefs.parts)) {
5357
+ if (this.statusline.partVisible(part) !== prefs.parts[part]) this.statusline.setPartVisible(part, prefs.parts[part]);
5358
+ }
5359
+ }
5360
+ if (prefs.indicatorTitles !== this.indicatorTitlesOn) this.setIndicatorTitlesVisible(prefs.indicatorTitles);
5361
+ if (prefs.indicatorValues !== this.indicatorValuesOn) this.setIndicatorValuesVisible(prefs.indicatorValues);
5201
5362
  }
5202
5363
  /** The LIVE chart of this cell — never cache it across a layout change (the cell's
5203
5364
  * identity is what endures; the chart dies with the cell). */
@@ -5219,17 +5380,23 @@ var ChartCell = class {
5219
5380
  get indicatorCount() {
5220
5381
  return this.instances.length + this.nativeCatalog.filter((n) => n.present).length;
5221
5382
  }
5222
- /** Switch this cell's market in place (the chart instance survives). */
5383
+ /** Switch this cell's market in place (the chart instance survives). The projection
5384
+ * is OPTIMISTIC — labels and chrome show the pick before the bars load; it follows
5385
+ * the setMarket call so the statusline reads the already-blanked chart. */
5223
5386
  setSymbol(symbol) {
5224
5387
  if (!this.inner || symbol === this.symbol) return;
5225
5388
  this.unresolvedToasted = null;
5226
5389
  void this.inner.setMarket({ symbol });
5390
+ this.projectMarket(symbol, this.timeframe);
5391
+ this.deps.onMarketChanged(this.id);
5227
5392
  }
5228
5393
  setTimeframe(timeframe) {
5229
5394
  if (!this.inner || timeframe === this.timeframe) return;
5230
5395
  this.activeRangeId = null;
5231
5396
  this.rangeBars = 0;
5232
5397
  void this.inner.setMarket({ timeframe, bars: this.state.bars });
5398
+ this.projectMarket(this.symbol, timeframe);
5399
+ this.deps.onMarketChanged(this.id);
5233
5400
  }
5234
5401
  /** Applied live (renderer feature) — no reload. */
5235
5402
  setPriceStyle(style) {
@@ -6092,11 +6259,11 @@ var MobileBar = class {
6092
6259
  injectStyles(STYLE_ID13, CSS13, doc);
6093
6260
  this.el = doc.createElement("div");
6094
6261
  this.el.className = "vela-mobilebar";
6095
- const item = (cls, label, onClick, icon2) => {
6262
+ const item = (cls, label, onClick, icon) => {
6096
6263
  const b = doc.createElement("button");
6097
6264
  b.className = `vela-mb-item ${cls}`;
6098
6265
  b.setAttribute("aria-label", label);
6099
- if (icon2) b.appendChild(iconEl(icon2, doc));
6266
+ if (icon) b.appendChild(iconEl(icon, doc));
6100
6267
  b.addEventListener("click", onClick);
6101
6268
  return b;
6102
6269
  };
@@ -6642,11 +6809,11 @@ var MoreDrawer = class {
6642
6809
  renderMain(doc) {
6643
6810
  const actions = doc.createElement("div");
6644
6811
  actions.className = "vela-md-actions";
6645
- const action = (icon2, label, enabled, run) => {
6812
+ const action = (icon, label, enabled, run) => {
6646
6813
  const b = doc.createElement("button");
6647
6814
  b.className = "vela-md-action";
6648
6815
  b.disabled = !enabled;
6649
- b.appendChild(iconEl(icon2, doc));
6816
+ b.appendChild(iconEl(icon, doc));
6650
6817
  b.appendChild(doc.createTextNode(label));
6651
6818
  b.addEventListener("click", () => {
6652
6819
  run();
@@ -7071,11 +7238,11 @@ var DrawingPill = class {
7071
7238
  this.el.hidden = true;
7072
7239
  this.toolGlyph = doc.createElement("span");
7073
7240
  this.toolGlyph.className = "vela-drawpill-tool";
7074
- const btn = (icon2, label, onClick) => {
7241
+ const btn = (icon, label, onClick) => {
7075
7242
  const b = doc.createElement("button");
7076
7243
  b.className = "vela-drawpill-btn";
7077
7244
  b.setAttribute("aria-label", label);
7078
- b.appendChild(iconEl(icon2, doc));
7245
+ b.appendChild(iconEl(icon, doc));
7079
7246
  b.addEventListener("click", onClick);
7080
7247
  return b;
7081
7248
  };
@@ -7139,16 +7306,21 @@ var DrawingPill = class {
7139
7306
 
7140
7307
  // src/workspace/context.ts
7141
7308
  function buildContext(host) {
7142
- const active = host.active();
7143
7309
  return {
7144
7310
  get chart() {
7145
7311
  const cell = host.active();
7146
7312
  if (!cell) throw new Error("VelaWorkspace has no active cell yet");
7147
7313
  return cell.chart;
7148
7314
  },
7149
- symbol: active?.symbol ?? "",
7150
- timeframe: active?.timeframe ?? "60",
7151
- priceStyle: active?.priceStyle ?? "candles",
7315
+ get symbol() {
7316
+ return host.active()?.symbol ?? "";
7317
+ },
7318
+ get timeframe() {
7319
+ return host.active()?.timeframe ?? "60";
7320
+ },
7321
+ get priceStyle() {
7322
+ return host.active()?.priceStyle ?? "candles";
7323
+ },
7152
7324
  setSymbol: (symbol) => host.active()?.setSymbol(symbol),
7153
7325
  setTimeframe: (tf) => host.active()?.setTimeframe(tf),
7154
7326
  setPriceStyle: (style) => host.active()?.setPriceStyle(style),
@@ -7159,8 +7331,12 @@ function buildContext(host) {
7159
7331
  addIndicator: (entry) => host.active()?.addExternalIndicator(entry),
7160
7332
  addNativeIndicator: (type) => host.active()?.addNative(type),
7161
7333
  stateChanged: () => host.stateDirty(),
7162
- cells: host.cells().map((c) => ({ id: c.id, chart: c.chart, symbol: c.symbol, timeframe: c.timeframe })),
7163
- activeCellId: active?.id ?? "",
7334
+ get cells() {
7335
+ return host.cells().map((c) => ({ id: c.id, chart: c.chart, symbol: c.symbol, timeframe: c.timeframe }));
7336
+ },
7337
+ get activeCellId() {
7338
+ return host.active()?.id ?? "";
7339
+ },
7164
7340
  setActiveCell: (id) => host.setActiveCell(id)
7165
7341
  };
7166
7342
  }
@@ -7254,6 +7430,10 @@ var VelaWorkspace = class {
7254
7430
  /** Same guard for the drawings link: the propagated mutations' own `drawing:*`
7255
7431
  * events fire synchronously inside the propagation loop and must not fan out again. */
7256
7432
  this.drawingSyncBusy = false;
7433
+ /** Same guard for the style link: a follower's `applyConfig` re-fires its
7434
+ * `onConfigChanged` in the same tick, and a state restore applies per-cell
7435
+ * configs that legitimately differ — neither must propagate. */
7436
+ this.styleSyncBusy = false;
7257
7437
  /** LINKED drawings (the drawings sync): one map per synced set (cellId → that
7258
7438
  * cell's drawing id), reachable from every member under its `cellId\0drawingId`
7259
7439
  * key — any member finds its peers to push edits/removals onto. Survives a
@@ -7314,9 +7494,7 @@ var VelaWorkspace = class {
7314
7494
  if (boot?.favorites) this.favs = [...boot.favorites];
7315
7495
  if (boot?.timeframeFavorites) this.tfFavs = [...boot.timeframeFavorites];
7316
7496
  const sync = boot?.sync ?? opts.sync;
7317
- for (const kind of ["viewport", "symbol", "timeframe", "crosshair", "drawings"]) {
7318
- this.applySyncSetting(kind, sync?.[kind]);
7319
- }
7497
+ for (const kind of SYNC_KINDS) this.applySyncSetting(kind, sync?.[kind]);
7320
7498
  this.monoLayout = opts.layout === false;
7321
7499
  const optLayout = opts.layout === false || opts.layout === void 0 ? "4" : opts.layout;
7322
7500
  this.def = this.resolveLayout(this.monoLayout ? "1" : boot?.layout && ensureLayout(boot.layout) ? boot.layout : optLayout);
@@ -7400,7 +7578,8 @@ var VelaWorkspace = class {
7400
7578
  syncs: () => [
7401
7579
  { id: "symbol", label: "Symbol", checked: this.syncOpts.symbol === true },
7402
7580
  { id: "timeframe", label: "Interval", checked: this.syncOpts.timeframe === true },
7403
- { id: "crosshair", label: "Crosshair", checked: this.syncOpts.crosshair === true }
7581
+ { id: "crosshair", label: "Crosshair", checked: this.syncOpts.crosshair === true },
7582
+ { id: "style", label: "Style", checked: this.syncOpts.style === true }
7404
7583
  ],
7405
7584
  onToggleSync: (id) => {
7406
7585
  const kind = id;
@@ -7619,7 +7798,10 @@ var VelaWorkspace = class {
7619
7798
  this.topbar.renderActions();
7620
7799
  this.mobileBar?.renderActions();
7621
7800
  this.dock.refresh();
7622
- for (const cell of this.cells()) cell.chart.renderer.setLegendActions(legendActionsProviderFor(cell.chart, () => this.context()));
7801
+ for (const cell of this.cells()) {
7802
+ cell.chart.renderer.setLegendActions(legendActionsProviderFor(cell.chart, () => this.context()));
7803
+ cell.chart.renderer.setLegendCallouts(legendCalloutsProviderFor(cell.chart, () => this.context()));
7804
+ }
7623
7805
  }
7624
7806
  // ── state surface (the SDK's read/restore of the whole grid's config + content) ──
7625
7807
  /**
@@ -7684,7 +7866,7 @@ var VelaWorkspace = class {
7684
7866
  this.topbar.setTimeframeFavorites(this.tfFavs);
7685
7867
  }
7686
7868
  this.dock.applyState(st.panels);
7687
- for (const kind of ["viewport", "symbol", "timeframe", "crosshair", "drawings"]) this.applySyncSetting(kind, st.sync?.[kind]);
7869
+ for (const kind of SYNC_KINDS) this.applySyncSetting(kind, st.sync?.[kind]);
7688
7870
  this.trackSizes.clear();
7689
7871
  if (st.trackSizes) for (const [id, ts] of Object.entries(st.trackSizes)) this.trackSizes.set(id, ts);
7690
7872
  const targetDef = this.monoLayout ? this.def : ensureLayout(st.layout) ?? this.def;
@@ -7695,9 +7877,14 @@ var VelaWorkspace = class {
7695
7877
  for (const cell of this.cellsById.values()) cell.chart.drawings.setFavorites(this.favs);
7696
7878
  }
7697
7879
  this.drawingLinks.clear();
7698
- for (const [i] of this.def.cells.entries()) {
7699
- const { id, ...cs } = st.charts[i];
7700
- this.cellsById.get(id)?.rehydrate(cs);
7880
+ this.styleSyncBusy = true;
7881
+ try {
7882
+ for (const [i] of this.def.cells.entries()) {
7883
+ const { id, ...cs } = st.charts[i];
7884
+ this.cellsById.get(id)?.rehydrate(cs);
7885
+ }
7886
+ } finally {
7887
+ this.styleSyncBusy = false;
7701
7888
  }
7702
7889
  if (st.timezone) this.setTimezone(st.timezone);
7703
7890
  this.pool.clear();
@@ -7797,6 +7984,7 @@ var VelaWorkspace = class {
7797
7984
  const rebuildAll = nextBackend !== this.cellBackend;
7798
7985
  this.order = orderAfterLayout(this.order, next.cells.length, this.activeId);
7799
7986
  const keep = new Set(this.order.slice(0, next.cells.length));
7987
+ const preexisting = new Set(this.cellsById.keys());
7800
7988
  for (const [id, cell] of [...this.cellsById]) {
7801
7989
  if (!keep.has(id) || rebuildAll) {
7802
7990
  this.poolSet(id, cell.dehydrate());
@@ -7809,6 +7997,7 @@ var VelaWorkspace = class {
7809
7997
  this.cellBackend = nextBackend;
7810
7998
  this.applyGrid();
7811
7999
  this.buildCells();
8000
+ this.alignNewCellStyles(preexisting);
7812
8001
  this.syncCellPresentation();
7813
8002
  this.topbar.setLayout(next.id);
7814
8003
  const nextActive = activeAfterLayout(this.activeId, this.order.slice(0, next.cells.length));
@@ -8012,6 +8201,7 @@ var VelaWorkspace = class {
8012
8201
  onMarketChanged: (id2) => this.onCellMarketChanged(id2),
8013
8202
  onPriceStyleChanged: (id2) => this.onCellPriceStyleChanged(id2),
8014
8203
  onIndicatorsChanged: (id2) => this.onCellIndicatorsChanged(id2),
8204
+ onStatusPrefsChanged: (id2) => this.propagateStylePrefs(id2),
8015
8205
  onStateDirty: () => this.markStateDirty(),
8016
8206
  manifestSettled: () => this.manifestSettled,
8017
8207
  toast: (message, kind, durationMs) => this.toastHost.show(message, kind, durationMs)
@@ -8087,6 +8277,7 @@ var VelaWorkspace = class {
8087
8277
  this.drawToolbar?.setEraserActive(mode === "eraser");
8088
8278
  });
8089
8279
  chart.on("viewport:changed", (range) => this.propagateViewport(cell.id, range));
8280
+ chart.renderer.onConfigChanged(() => this.propagateStylePrefs(cell.id));
8090
8281
  chart.on("theme:changed", (t) => this.setTheme(t));
8091
8282
  chart.renderer.onAxisLongPress((e) => {
8092
8283
  if (this.layoutCtl.current !== "mobile") return;
@@ -8119,11 +8310,67 @@ var VelaWorkspace = class {
8119
8310
  if (kind === "viewport") {
8120
8311
  const range = this.cellsById.get(this.activeId)?.chart.getVisibleRange();
8121
8312
  if (range) this.propagateViewport(this.activeId, range);
8313
+ } else if (kind === "style") {
8314
+ this.propagateStylePrefs(this.activeId);
8122
8315
  } else {
8123
8316
  this.propagateMarket(this.activeId);
8124
8317
  }
8125
8318
  }
8126
8319
  }
8320
+ /**
8321
+ * Align cells minted by a layout change to their style group: with the link on, a
8322
+ * NEW cell (fresh slot or one returning from the pool, which missed edits while
8323
+ * dormant) inherits the presentation of a pre-existing group peer — the active
8324
+ * cell when it is one — instead of sitting on its own state beside a styled
8325
+ * group. Propagation runs FROM the peer, so a newborn's defaults never overwrite
8326
+ * the group, and the equality short-circuits keep converged peers untouched.
8327
+ */
8328
+ alignNewCellStyles(preexisting) {
8329
+ const setting = this.syncOpts.style;
8330
+ if (!setting) return;
8331
+ const ids = [...this.cellsById.keys()];
8332
+ const propagated = /* @__PURE__ */ new Set();
8333
+ for (const id of ids) {
8334
+ if (preexisting.has(id)) continue;
8335
+ const peers = syncTargets(id, setting, ids).filter((p) => preexisting.has(p));
8336
+ if (peers.length === 0) continue;
8337
+ const source = this.activeId && peers.includes(this.activeId) ? this.activeId : peers[0];
8338
+ if (propagated.has(source)) continue;
8339
+ propagated.add(source);
8340
+ this.propagateStylePrefs(source);
8341
+ }
8342
+ }
8343
+ /**
8344
+ * Mirror an origin cell's presentation — the Canvas + Scales-and-lines slice of
8345
+ * its renderer config plus its Status line tab prefs — onto its same-group
8346
+ * followers (the style link). Loop-safe two ways: the busy guard eats the
8347
+ * followers' SYNCHRONOUS echoes (their `applyConfig` re-fires `onConfigChanged`
8348
+ * in the same tick), and the equality short-circuits leave already-converged
8349
+ * followers untouched, so nothing re-emits once the group agrees.
8350
+ */
8351
+ propagateStylePrefs(originId) {
8352
+ if (this.styleSyncBusy || this.destroyed) return;
8353
+ const targets = syncTargets(originId, this.syncOpts.style, [...this.cellsById.keys()]);
8354
+ if (targets.length === 0) return;
8355
+ const origin = this.cellsById.get(originId);
8356
+ if (!origin) return;
8357
+ const slice = styleConfigSlice(origin.chart.renderer.getConfig());
8358
+ const sliceJson = slice ? JSON.stringify(slice) : null;
8359
+ const prefs = origin.statusPrefs();
8360
+ this.styleSyncBusy = true;
8361
+ try {
8362
+ for (const id of targets) {
8363
+ const cell = this.cellsById.get(id);
8364
+ if (!cell) continue;
8365
+ if (slice && sliceJson !== JSON.stringify(styleConfigSlice(cell.chart.renderer.getConfig()))) {
8366
+ cell.chart.renderer.applyConfig(slice);
8367
+ }
8368
+ cell.applyStatusPrefs(prefs);
8369
+ }
8370
+ } finally {
8371
+ this.styleSyncBusy = false;
8372
+ }
8373
+ }
8127
8374
  /**
8128
8375
  * Mirror an origin cell's pointer time onto its same-group followers as GHOST
8129
8376
  * crosshairs (`renderer.setExternalCrosshair`). The horizontal price level rides
@@ -8431,7 +8678,8 @@ var VelaWorkspace = class {
8431
8678
  syncs: () => [
8432
8679
  { id: "symbol", label: "Symbol", checked: this.syncOpts.symbol === true },
8433
8680
  { id: "timeframe", label: "Interval", checked: this.syncOpts.timeframe === true },
8434
- { id: "crosshair", label: "Crosshair", checked: this.syncOpts.crosshair === true }
8681
+ { id: "crosshair", label: "Crosshair", checked: this.syncOpts.crosshair === true },
8682
+ { id: "style", label: "Style", checked: this.syncOpts.style === true }
8435
8683
  ],
8436
8684
  onToggleSync: (id) => {
8437
8685
  const kind = id;