@luxalgo/vela 0.6.6 → 0.6.8

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 (43) hide show
  1. package/dist/{DataProvider-JJq7M2it.d.ts → DataProvider-BSLlBpB9.d.ts} +10 -0
  2. package/dist/{DataProvider-DDUYw2qP.d.cts → DataProvider-BsQM2WNH.d.cts} +10 -0
  3. package/dist/{chunk-OEUGUXL7.js → chunk-L3I2CCYO.js} +49 -1
  4. package/dist/{chunk-TMXK2SJR.js → chunk-N7LGMKCE.js} +523 -142
  5. package/dist/{chunk-JQFO2WMU.js → chunk-SQETIBFU.js} +83 -9
  6. package/dist/chunk-W4EJWLEO.js +12 -0
  7. package/dist/{contributions-BqCIsbaP.d.ts → contributions-DLLdV9jD.d.ts} +59 -3
  8. package/dist/{contributions-DNMqrAuw.d.cts → contributions-Vw-Hm58R.d.cts} +59 -3
  9. package/dist/index.cjs +110 -8
  10. package/dist/index.d.cts +8 -4
  11. package/dist/index.d.ts +8 -4
  12. package/dist/index.js +2 -2
  13. package/dist/{plugin-CN8U2__Q.d.cts → plugin-BnJgjLAy.d.cts} +2 -2
  14. package/dist/{plugin-WnvNNJOp.d.ts → plugin-CC7rBrOY.d.ts} +2 -2
  15. package/dist/plugin.cjs +28 -0
  16. package/dist/plugin.d.cts +3 -3
  17. package/dist/plugin.d.ts +3 -3
  18. package/dist/plugin.js +1 -1
  19. package/dist/providers/binance.cjs +16 -0
  20. package/dist/providers/binance.d.cts +4 -1
  21. package/dist/providers/binance.d.ts +4 -1
  22. package/dist/providers/binance.js +7 -0
  23. package/dist/providers/coinbase.cjs +16 -0
  24. package/dist/providers/coinbase.d.cts +4 -1
  25. package/dist/providers/coinbase.d.ts +4 -1
  26. package/dist/providers/coinbase.js +7 -0
  27. package/dist/providers/hyperliquid.cjs +16 -0
  28. package/dist/providers/hyperliquid.d.cts +4 -1
  29. package/dist/providers/hyperliquid.d.ts +4 -1
  30. package/dist/providers/hyperliquid.js +7 -0
  31. package/dist/{bottombar-COnL2Sk4.d.ts → statusline-CGkB2EOo.d.ts} +137 -8
  32. package/dist/{bottombar-DeDXbF_K.d.cts → statusline-DqzBqy42.d.cts} +137 -8
  33. package/dist/vela.global.js +136 -8
  34. package/dist/vela.global.min.js +42 -42
  35. package/dist/widget.cjs +667 -148
  36. package/dist/widget.d.cts +50 -81
  37. package/dist/widget.d.ts +50 -81
  38. package/dist/widget.js +5 -4
  39. package/dist/workspace.cjs +613 -150
  40. package/dist/workspace.d.cts +62 -9
  41. package/dist/workspace.d.ts +62 -9
  42. package/dist/workspace.js +4 -3
  43. package/package.json +1 -1
@@ -1,8 +1,9 @@
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-JQFO2WMU.js';
2
- import { chartType, widgetActions, SidePanel, sidePanels, DEFAULT_PANEL_ORDER, resolveEngines, legendActionsProviderFor, statePersistenceHandlers, rendererDefaults, widgetAttachments, getDrawingType } from './chunk-OEUGUXL7.js';
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-SQETIBFU.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
3
  import { Tooltip, KeymapManager, isEditableTarget, Drawer } from './chunk-2R7BANEM.js';
4
4
  import { Menu, Dialog, applyPlotOverlayTokens, ensureUIHost } from './chunk-OICPLNU7.js';
5
5
  import { registerIcon, svg16, injectStyles, iconEl, iconMarkup, SESSION_PRE, SESSION_POST, SESSION_OFF, icon, categoricalColor } from './chunk-PN5KWFZ4.js';
6
+ import { baseOf } from './chunk-W4EJWLEO.js';
6
7
 
7
8
  // src/widget/timeframe.ts
8
9
  var UNIT_MS = {
@@ -526,9 +527,14 @@ var CSS2 = `
526
527
  align-items: center;
527
528
  justify-content: center;
528
529
  }
529
- .vela-widget-actions { margin-left: auto; display: inline-flex; gap: var(--vela-space-1); }
530
+ /* The right side of the bar \u2014 whatever the composition puts there rides this one
531
+ auto-margin push (the flow-actions host used to carry it; composition can omit it). */
532
+ .vela-topbar-right { margin-left: auto; display: inline-flex; align-items: center; gap: var(--vela-space-1); }
533
+ .vela-widget-actions { display: inline-flex; gap: var(--vela-space-1); }
530
534
  /* Left-aligned contributed actions \u2014 the primary-chrome cluster after the dropdowns. */
531
535
  .vela-widget-actions-left { display: inline-flex; align-items: center; gap: var(--vela-space-1); }
536
+ /* One PINNED contributed action's slot (a composition entry naming the action's id). */
537
+ .vela-widget-action-pin { display: inline-flex; align-items: center; }
532
538
  /* The side-panel toggles, one per docked panel \u2014 a group so the dock can rebuild them
533
539
  without disturbing the tools around it. */
534
540
  .vela-widget-panels { display: inline-flex; align-items: center; gap: var(--vela-space-1); }
@@ -587,6 +593,17 @@ var Topbar = class {
587
593
  this.tooltips = [];
588
594
  this.panelBtns = /* @__PURE__ */ new Map();
589
595
  this.panelTooltips = [];
596
+ /** Pinned contributed-action slots, by action id (composition entries that name one,
597
+ * plus built-in slots taken over by an override). */
598
+ this.pinned = /* @__PURE__ */ new Map();
599
+ /** Overrides that LEFT their native slot (default side + a declared `order`) — they
600
+ * render through the flow cluster like ordinary actions. */
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();
590
607
  this.onHairlineSync = () => {
591
608
  if (this.hairlineRaf) return;
592
609
  const win = this.el.ownerDocument.defaultView;
@@ -601,6 +618,8 @@ var Topbar = class {
601
618
  this.host = host;
602
619
  this.timeframe = opts.timeframe;
603
620
  this.priceStyle = opts.priceStyle;
621
+ this.comp = resolveTopbarComposition(opts.composition);
622
+ const vis = (id) => topbarHas(this.comp, id);
604
623
  const doc = host.ownerDocument;
605
624
  injectStyles(STYLE_ID2, CSS2, doc);
606
625
  this.el = doc.createElement("div");
@@ -623,7 +642,7 @@ var Topbar = class {
623
642
  this.styleButton.className = "vela-widget-style";
624
643
  this.renderStyleButton(doc);
625
644
  let indicatorsBtn = null;
626
- if (opts.onIndicatorsClick) {
645
+ if (opts.onIndicatorsClick && !topbarActionOverride("indicators")) {
627
646
  indicatorsBtn = doc.createElement("button");
628
647
  indicatorsBtn.className = "vela-widget-indicators";
629
648
  indicatorsBtn.append(iconEl("indicators", doc), doc.createTextNode("Indicators"));
@@ -636,18 +655,23 @@ var Topbar = class {
636
655
  this.panelsHost = doc.createElement("span");
637
656
  this.panelsHost.className = "vela-widget-panels";
638
657
  const tool = (cls, icon2, tip, onClick) => this.toolButton(cls, icon2, tip, onClick, this.tooltips);
639
- this.undoBtn = tool("vela-widget-undo", "undo", "Undo", opts.onUndoClick);
640
- this.redoBtn = tool("vela-widget-redo", "redo", "Redo", opts.onRedoClick);
658
+ if (vis("undo-redo")) {
659
+ this.undoBtn = tool("vela-widget-undo", "undo", "Undo", opts.onUndoClick);
660
+ this.redoBtn = tool("vela-widget-redo", "redo", "Redo", opts.onRedoClick);
661
+ } else {
662
+ this.undoBtn = doc.createElement("button");
663
+ this.redoBtn = doc.createElement("button");
664
+ }
641
665
  this.setHistoryState(false, false);
642
- const screenshotBtn = tool("vela-widget-screenshot", "camera", "Download screenshot", opts.onScreenshotClick);
643
- this.alertsBtn = tool("vela-widget-alerts", "bell", "Alerts");
666
+ const screenshotBtn = vis("screenshot") && !topbarActionOverride("screenshot") ? tool("vela-widget-screenshot", "camera", "Download screenshot", opts.onScreenshotClick) : null;
667
+ this.alertsBtn = vis("alerts") ? tool("vela-widget-alerts", "bell", "Alerts") : doc.createElement("button");
644
668
  this.alertsBtn.style.position = "relative";
645
669
  if (opts.onAlertsClick) this.alertsBtn.addEventListener("click", () => opts.onAlertsClick(this.alertsBtn));
646
670
  this.alertsBadge = doc.createElement("span");
647
671
  this.alertsBadge.className = "vela-alerts-badge";
648
672
  this.alertsBadge.style.display = "none";
649
673
  this.alertsBtn.appendChild(this.alertsBadge);
650
- if (opts.layout) {
674
+ if (opts.layout && vis("layout")) {
651
675
  this.layoutId = opts.layout.current;
652
676
  this.layoutButton = doc.createElement("button");
653
677
  this.layoutButton.className = "vela-widget-style";
@@ -658,12 +682,65 @@ var Topbar = class {
658
682
  d.className = "vela-sep";
659
683
  return d;
660
684
  };
661
- const leading = [this.symbolEl, sep(), tfGroup, sep(), this.styleButton, sep()];
662
- if (this.layoutButton) leading.push(this.layoutButton, sep());
663
- if (indicatorsBtn) leading.push(indicatorsBtn, sep());
664
685
  this.leftActionsSep = sep();
665
686
  this.leftActionsSep.hidden = true;
666
- this.el.append(...leading, this.leftActionsHost, this.leftActionsSep, this.undoBtn, this.redoBtn, this.actionsHost, this.alertsBtn, this.panelsHost, screenshotBtn);
687
+ const primaries = /* @__PURE__ */ new Set(["symbol", "timeframes", "style", "layout", "indicators"]);
688
+ const pinSlot = (id, left) => {
689
+ const slot = doc.createElement("span");
690
+ slot.className = "vela-widget-action-pin";
691
+ this.pinned.set(id, { host: slot, left });
692
+ return [slot];
693
+ };
694
+ const overridden = (id, left) => {
695
+ const ov = topbarActionOverride(id);
696
+ if (!ov) return null;
697
+ const sideDeclared = left ? opts.composition?.left != null : opts.composition?.right != null;
698
+ if (!sideDeclared && ov.order !== void 0) {
699
+ this.flowingOverrides.add(id);
700
+ return [];
701
+ }
702
+ return pinSlot(id, left);
703
+ };
704
+ const elementsFor = (id, left) => {
705
+ switch (id) {
706
+ case "symbol":
707
+ return [this.symbolEl];
708
+ case "timeframes":
709
+ return [tfGroup];
710
+ case "style":
711
+ return [this.styleButton];
712
+ case "layout":
713
+ return this.layoutButton ? [this.layoutButton] : [];
714
+ case "indicators":
715
+ return overridden(id, left) ?? (indicatorsBtn ? [indicatorsBtn] : []);
716
+ case "actions":
717
+ return left ? [this.leftActionsHost, this.leftActionsSep] : [this.actionsHost];
718
+ case "undo-redo":
719
+ return [this.undoBtn, this.redoBtn];
720
+ case "alerts":
721
+ return [this.alertsBtn];
722
+ case "panels":
723
+ return [this.panelsHost];
724
+ case "screenshot":
725
+ return overridden(id, left) ?? (screenshotBtn ? [screenshotBtn] : []);
726
+ default:
727
+ return pinSlot(id, left);
728
+ }
729
+ };
730
+ const sideEls = (list, left) => {
731
+ const out = [];
732
+ for (const [i, id] of list.entries()) {
733
+ const els = elementsFor(id, left);
734
+ if (els.length === 0) continue;
735
+ out.push(...els);
736
+ if (primaries.has(id) && i < list.length - 1) out.push(sep());
737
+ }
738
+ return out;
739
+ };
740
+ const right = doc.createElement("span");
741
+ right.className = "vela-topbar-right";
742
+ right.append(...sideEls(this.comp.right, false));
743
+ this.el.append(...sideEls(this.comp.left, true), right);
667
744
  host.appendChild(this.el);
668
745
  this.renderTfChips();
669
746
  this.onHairlineSync();
@@ -780,23 +857,45 @@ var Topbar = class {
780
857
  else this.styleButton.appendChild(doc.createTextNode(priceStyleLabel(this.priceStyle)));
781
858
  this.styleButton.setAttribute("aria-label", `Chart style \u2014 ${priceStyleLabel(this.priceStyle)}`);
782
859
  }
783
- /** Re-project the contributed topbar actions (call after registrations change). */
860
+ /** Re-project the contributed topbar actions (call after registrations change).
861
+ * An action PINNED by the composition renders into its named slot (list position
862
+ * wins over `align`/`order`); the rest flow into the side's `actions` slot — or
863
+ * not at all when an explicit list omits it (the list is the side's contract). */
784
864
  renderActions() {
785
865
  const ctx = this.opts.getContext?.();
786
866
  this.actionsHost.replaceChildren();
787
867
  this.leftActionsHost.replaceChildren();
868
+ for (const pin of this.pinned.values()) pin.host.replaceChildren();
869
+ for (const t of this.actionTooltips) t.destroy();
870
+ this.actionTooltips = [];
788
871
  const doc = this.actionsHost.ownerDocument;
872
+ const flowLeft = this.comp.left.includes("actions");
873
+ const flowRight = this.comp.right.includes("actions");
874
+ const builtin = new Set(TOPBAR_BUILTIN_IDS);
789
875
  for (const action of widgetActions("topbar", ctx)) {
790
- const left = action.align === "left";
876
+ const pin = this.pinned.get(action.id);
877
+ if (!pin && builtin.has(action.id) && !this.flowingOverrides.has(action.id)) continue;
878
+ const left = pin ? pin.left : action.align === "left";
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
+ }
791
885
  const b = doc.createElement("button");
792
- 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";
793
887
  if (action.icon) b.appendChild(iconEl(action.icon, doc));
794
- 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
+ }
795
894
  b.addEventListener("click", () => {
796
895
  const c = this.opts.getContext?.();
797
896
  if (c) action.run(c);
798
897
  });
799
- (left ? this.leftActionsHost : this.actionsHost).appendChild(b);
898
+ (pin ? pin.host : left ? this.leftActionsHost : this.actionsHost).appendChild(b);
800
899
  }
801
900
  this.leftActionsSep.hidden = this.leftActionsHost.childElementCount === 0;
802
901
  this.onHairlineSync();
@@ -842,7 +941,7 @@ var Topbar = class {
842
941
  this.tfMenu.destroy();
843
942
  this.styleMenu.destroy();
844
943
  this.layoutPicker?.destroy();
845
- for (const t of [...this.tooltips, ...this.panelTooltips]) t.destroy();
944
+ for (const t of [...this.tooltips, ...this.panelTooltips, ...this.actionTooltips]) t.destroy();
846
945
  this.el.remove();
847
946
  }
848
947
  /** One icon-only tool button with its kit tooltip, parked in `sink` for disposal. */
@@ -1113,36 +1212,27 @@ var iconFailed = /* @__PURE__ */ new Set();
1113
1212
  function initialsOf(name) {
1114
1213
  return (name || "?").replace(/[^A-Za-z0-9]/g, "").slice(0, 2).toUpperCase() || "?";
1115
1214
  }
1116
- function cryptoIconUrl(base) {
1117
- return `https://crypto-icons.ledger.com/${encodeURIComponent(base.toUpperCase())}.png`;
1118
- }
1119
- function baseOf(d) {
1120
- const fromDesc = d.description?.split("/")[0]?.trim();
1121
- if (fromDesc) return fromDesc.replace(/\s+Perpetual$/i, "");
1122
- return d.ticker.replace(/[-_/]?(USDT|USDC|USD1|USDS|BUSD|USD|EUR|PERP)$/i, "") || d.ticker;
1123
- }
1124
- function tickerIconEl(doc, base, name, className) {
1215
+ function tickerIconEl(doc, base, name, className, iconUrl) {
1125
1216
  const wrap = doc.createElement("span");
1126
1217
  wrap.className = className;
1127
- const key = base.toUpperCase();
1128
1218
  const fallback = () => {
1129
1219
  wrap.replaceChildren();
1130
1220
  wrap.style.background = categoricalColor(name);
1131
1221
  wrap.textContent = initialsOf(base || name);
1132
1222
  };
1133
- if (!key || iconFailed.has(key)) {
1223
+ if (!iconUrl || iconFailed.has(iconUrl)) {
1134
1224
  fallback();
1135
1225
  return wrap;
1136
1226
  }
1137
1227
  const img = doc.createElement("img");
1138
1228
  img.alt = "";
1139
1229
  img.crossOrigin = "anonymous";
1140
- img.src = cryptoIconUrl(key);
1230
+ img.src = iconUrl;
1141
1231
  img.style.cssText = "width:100%;height:100%;border-radius:50%;display:block;object-fit:cover;";
1142
1232
  img.addEventListener(
1143
1233
  "error",
1144
1234
  () => {
1145
- iconFailed.add(key);
1235
+ iconFailed.add(iconUrl);
1146
1236
  fallback();
1147
1237
  },
1148
1238
  { once: true }
@@ -1553,11 +1643,14 @@ var MenuBuilder = class {
1553
1643
  }
1554
1644
  };
1555
1645
  var ObjectTree = class extends SidePanel {
1556
- constructor(host) {
1646
+ constructor(host, iconFor) {
1557
1647
  super(host, "Object tree", "vela-ot");
1648
+ this.iconFor = iconFor;
1558
1649
  this.chart = null;
1559
1650
  this.selectedDrawing = null;
1560
1651
  this.symbolName = "";
1652
+ /** The raw (possibly venue-prefixed) symbol — what icon resolution routes on. */
1653
+ this.symbolRaw = "";
1561
1654
  /** Drawing bundles — a view-side grouping, held for the panel's lifetime and never persisted.
1562
1655
  * Kept per chart because a workspace points this one panel at whichever chart is active, and
1563
1656
  * each chart's bundles have to survive the switch. */
@@ -1594,6 +1687,7 @@ var ObjectTree = class extends SidePanel {
1594
1687
  if (open) this.refresh();
1595
1688
  }
1596
1689
  setSymbol(symbol) {
1690
+ this.symbolRaw = symbol;
1597
1691
  this.symbolName = parseSymbol(symbol).ticker;
1598
1692
  }
1599
1693
  /** (Re)bind to a chart instance — called after every widget rebuild. */
@@ -1794,7 +1888,7 @@ var ObjectTree = class extends SidePanel {
1794
1888
  const { chart } = pass;
1795
1889
  if (row.kind === "price") {
1796
1890
  const base = this.symbolName.replace(/[-_/]?(USDT|USDC|USD1|USDS|BUSD|USD|EUR|PERP)$/i, "") || this.symbolName;
1797
- const icon2 = tickerIconEl(doc, base || "P", this.symbolName || "Price", "vela-ot-avatar");
1891
+ const icon2 = tickerIconEl(doc, base || "P", this.symbolName || "Price", "vela-ot-avatar", this.symbolRaw ? this.iconFor?.(this.symbolRaw) : void 0);
1798
1892
  const el2 = this.row(icon2, row.label, row.visible, [
1799
1893
  {
1800
1894
  icon: row.visible ? "eye" : "eye-off",
@@ -2826,6 +2920,17 @@ var PanelDock = class {
2826
2920
  };
2827
2921
 
2828
2922
  // src/widget/symbol-picker.ts
2923
+ function dedupeSymbols(list) {
2924
+ const seen = /* @__PURE__ */ new Set();
2925
+ const out = [];
2926
+ for (const s of list) {
2927
+ const key = `${(s.prefix ?? s.provider ?? "").toLowerCase()}:${s.ticker.toUpperCase()}`;
2928
+ if (seen.has(key)) continue;
2929
+ seen.add(key);
2930
+ out.push(s);
2931
+ }
2932
+ return out;
2933
+ }
2829
2934
  var TOP_TICKERS = ["BTCUSDT", "ETHUSDT", "SOLUSDT", "BNBUSDT", "XRPUSDT", "DOGEUSDT", "ADAUSDT", "LINKUSDT"];
2830
2935
  function parseQuery(raw, venues) {
2831
2936
  const m = raw.match(/^\s*([^\s:]+)\s*[:\s]\s*(.*)$/);
@@ -2840,17 +2945,18 @@ function onlyOne(matches) {
2840
2945
  return matches.length === 1 ? matches[0] : null;
2841
2946
  }
2842
2947
  var venueOf = (s) => s.prefix ?? s.provider;
2843
- function filterSymbols(list, query, limit = 100) {
2948
+ function filterSymbols(list, query, limit = 100, top = TOP_TICKERS) {
2844
2949
  const venues = [...new Set(list.flatMap((s) => [venueOf(s)?.toLowerCase(), s.provider?.toLowerCase()]).filter((p) => !!p))];
2845
2950
  const { scope, term } = parseQuery(query, venues);
2846
2951
  const pool = scope ? list.filter((s) => venueOf(s)?.toLowerCase() === scope || s.provider?.toLowerCase() === scope) : list;
2847
2952
  const q = term.toUpperCase();
2848
2953
  if (!q) {
2849
2954
  if (scope) return [...pool].sort((a, b) => a.ticker.localeCompare(b.ticker)).slice(0, limit);
2955
+ if (top === false) return pool.slice(0, limit);
2850
2956
  const byTicker = new Map(pool.map((s) => [s.ticker.toUpperCase(), s]));
2851
- const top = TOP_TICKERS.map((t) => byTicker.get(t)).filter((s) => s !== void 0);
2852
- const rest = pool.filter((s) => !TOP_TICKERS.includes(s.ticker.toUpperCase()));
2853
- return [...top, ...rest].slice(0, limit);
2957
+ const pinned = top.map((t) => byTicker.get(t)).filter((s) => s !== void 0);
2958
+ const rest = pool.filter((s) => !top.includes(s.ticker.toUpperCase()));
2959
+ return [...pinned, ...rest].slice(0, limit);
2854
2960
  }
2855
2961
  const qLower = term.toLowerCase();
2856
2962
  const prefix = [];
@@ -2960,12 +3066,16 @@ var CSS6 = `
2960
3066
  var PAGE = 100;
2961
3067
  var SymbolPicker = class {
2962
3068
  constructor(opts) {
3069
+ this.opts = opts;
2963
3070
  this.source = () => [];
2964
3071
  this.rows = [];
2965
3072
  this.highlighted = 0;
2966
3073
  this.seed = "";
2967
3074
  this.activeTab = "All";
2968
3075
  this.visible = PAGE;
3076
+ /** The ranked pool cache — `key` fingerprints the raw pool the ranking ran on. */
3077
+ this.ranked = null;
3078
+ this.ranking = false;
2969
3079
  const doc = (opts.host ?? document.body).ownerDocument;
2970
3080
  injectStyles(STYLE_ID6, CSS6, doc);
2971
3081
  this.input = doc.createElement("input");
@@ -3063,10 +3173,34 @@ var SymbolPicker = class {
3063
3173
  } else delete el.dataset.highlighted;
3064
3174
  });
3065
3175
  }
3176
+ /** The picker's pool: the source, shaped by the registered symbol ranking. Cached —
3177
+ * the hook runs when the pool CHANGES (an index lands or refreshes), never per
3178
+ * keystroke; an async hook resolves onto the next repaint (stale-while-revalidate
3179
+ * in between, the raw pool before the first resolve). */
3180
+ pool() {
3181
+ const raw = this.source();
3182
+ const hook = symbolRanking();
3183
+ if (!hook) return raw;
3184
+ const key = `${raw.length}:${raw[0]?.ticker ?? ""}:${raw[raw.length - 1]?.ticker ?? ""}`;
3185
+ if (this.ranked?.key !== key && !this.ranking) {
3186
+ this.ranking = true;
3187
+ Promise.resolve([...raw]).then((copy) => hook(copy)).then((out) => {
3188
+ this.ranked = { key, result: dedupeSymbols(out) };
3189
+ }).catch((err) => {
3190
+ console.warn("[vela] symbol ranking failed \u2014 pool order kept:", err);
3191
+ this.ranked = { key, result: [...raw] };
3192
+ }).finally(() => {
3193
+ this.ranking = false;
3194
+ this.refresh();
3195
+ });
3196
+ }
3197
+ return this.ranked?.result ?? raw;
3198
+ }
3066
3199
  computeRows() {
3067
3200
  const TAB_TYPES = { Crypto: ["crypto"], Stocks: ["stock"], ETFs: ["etf"], Forex: ["forex"], Commodities: ["commodity"] };
3068
- const pool = this.activeTab === "All" ? this.source() : this.source().filter((s) => TAB_TYPES[this.activeTab]?.includes((s.type ?? "").toLowerCase()) || this.activeTab === "Crypto" && (s.type ?? "").toLowerCase() === "futures");
3069
- return filterSymbols(pool, this.input.value, this.visible);
3201
+ const all = this.pool();
3202
+ const pool = this.activeTab === "All" ? all : all.filter((s) => TAB_TYPES[this.activeTab]?.includes((s.type ?? "").toLowerCase()) || this.activeTab === "Crypto" && (s.type ?? "").toLowerCase() === "futures");
3203
+ return filterSymbols(pool, this.input.value, this.visible, symbolRanking() ? false : TOP_TICKERS);
3070
3204
  }
3071
3205
  refresh() {
3072
3206
  const doc = this.list.ownerDocument;
@@ -3097,7 +3231,7 @@ var SymbolPicker = class {
3097
3231
  row.dataset.ticker = s.ticker;
3098
3232
  const venue = s.prefix ?? s.provider;
3099
3233
  if (venue) row.dataset.venue = venue;
3100
- const av = tickerIconEl(doc, baseOf(s), s.ticker, "vela-sp-avatar");
3234
+ const av = tickerIconEl(doc, baseOf(s), s.ticker, "vela-sp-avatar", this.opts.iconFor?.(s));
3101
3235
  const main = doc.createElement("span");
3102
3236
  main.className = "vela-sp-main";
3103
3237
  const t = doc.createElement("span");
@@ -3550,19 +3684,8 @@ function indicatorLedger(i) {
3550
3684
  };
3551
3685
  }
3552
3686
 
3553
- // src/workspace/sync.ts
3554
- function syncTargets(originId, setting, cellIds) {
3555
- if (setting == null || setting === false) return [];
3556
- if (setting === true) return cellIds.filter((id) => id !== originId);
3557
- const group = setting[originId];
3558
- if (group == null) return [];
3559
- return cellIds.filter((id) => id !== originId && setting[id] === group);
3560
- }
3561
- function rangesWithin(a, b, epsMs) {
3562
- return Math.abs(a.from - b.from) <= epsMs && Math.abs(a.to - b.to) <= epsMs;
3563
- }
3564
-
3565
3687
  // src/state/document.ts
3688
+ var SYNC_KINDS = ["viewport", "symbol", "timeframe", "crosshair", "drawings", "style"];
3566
3689
  function prefixedSymbol(cell) {
3567
3690
  if (!cell?.symbol) return void 0;
3568
3691
  if (cell.symbol.includes(":") || !cell.provider) return cell.symbol;
@@ -3650,7 +3773,7 @@ function sanitizeSync(raw) {
3650
3773
  if (raw == null || typeof raw !== "object") return null;
3651
3774
  const s = raw;
3652
3775
  const out = {};
3653
- for (const kind of ["viewport", "symbol", "timeframe", "crosshair", "drawings"]) {
3776
+ for (const kind of SYNC_KINDS) {
3654
3777
  const v = s[kind];
3655
3778
  if (v === true) out[kind] = true;
3656
3779
  else if (v != null && typeof v === "object") {
@@ -3693,6 +3816,28 @@ function sanitizeTrackSizes(raw) {
3693
3816
  return Object.keys(out).length > 0 ? out : null;
3694
3817
  }
3695
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
+
3696
3841
  // src/workspace/persist.ts
3697
3842
  var memoryStore = /* @__PURE__ */ new Map();
3698
3843
  function memoryStorageAdapter() {
@@ -3924,8 +4069,9 @@ function statuslineInkOf(renderer, priceStyle) {
3924
4069
  }
3925
4070
  }
3926
4071
  var Statusline = class {
3927
- constructor(host, symbol) {
4072
+ constructor(host, symbol, iconFor) {
3928
4073
  this.host = host;
4074
+ this.iconFor = iconFor;
3929
4075
  this.parts = { name: true, market: true, ohlc: true, change: true };
3930
4076
  this.lastBar = null;
3931
4077
  this.hoverBar = null;
@@ -3949,7 +4095,7 @@ var Statusline = class {
3949
4095
  this.el.className = "vela-statusline";
3950
4096
  this.el.dataset.velaScreenshot = "1";
3951
4097
  const ticker = parseSymbol(symbol).ticker;
3952
- this.avatarEl = tickerIconEl(doc, baseOfTicker(ticker), ticker, "vela-sl-avatar");
4098
+ this.avatarEl = tickerIconEl(doc, baseOfTicker(ticker), ticker, "vela-sl-avatar", this.iconFor?.(symbol));
3953
4099
  this.symbolEl = doc.createElement("span");
3954
4100
  this.symbolEl.className = "vela-sl-symbol";
3955
4101
  this.symbolEl.textContent = ticker;
@@ -3970,7 +4116,7 @@ var Statusline = class {
3970
4116
  setSymbol(symbol) {
3971
4117
  const ticker = parseSymbol(symbol).ticker;
3972
4118
  this.symbolEl.textContent = ticker;
3973
- const fresh = tickerIconEl(this.el.ownerDocument, baseOfTicker(ticker), ticker, "vela-sl-avatar");
4119
+ const fresh = tickerIconEl(this.el.ownerDocument, baseOfTicker(ticker), ticker, "vela-sl-avatar", this.iconFor?.(symbol));
3974
4120
  this.avatarEl.replaceWith(fresh);
3975
4121
  this.avatarEl = fresh;
3976
4122
  this.fit();
@@ -4527,61 +4673,144 @@ var MarketStatusTracker = class {
4527
4673
  };
4528
4674
 
4529
4675
  // src/widget/session-shading.ts
4530
- function deriveSessionZones(regular, extended) {
4676
+ var DAY_MS = 864e5;
4677
+ var MINUTE_MS = 6e4;
4678
+ function parseWindow(text) {
4679
+ if (typeof text !== "string") return null;
4680
+ const m = /^(\d{2})(\d{2})-(\d{2})(\d{2})$/.exec(text);
4681
+ if (!m) return null;
4682
+ const start = Number(m[1]) * 60 + Number(m[2]);
4683
+ const end = Number(m[3]) * 60 + Number(m[4]);
4684
+ if (start >= end || end > 1440) return null;
4685
+ return { start, end };
4686
+ }
4687
+ function parseSessionSpec(si) {
4688
+ const session = si?.["session"];
4689
+ if (typeof session !== "string" || session === "" || session === "24x7") return null;
4690
+ const regular = parseWindow(session);
4691
+ if (!regular) return null;
4692
+ const tz = si?.["timezone"];
4693
+ const timezone = typeof tz === "string" && tz !== "" ? tz : "Etc/UTC";
4694
+ const ext = parseWindow(si?.["session_extended"]);
4695
+ const extended = ext && ext.start <= regular.start && ext.end >= regular.end ? ext : { start: 0, end: 1440 };
4696
+ return { regular, extended, timezone };
4697
+ }
4698
+ var dtfCache = /* @__PURE__ */ new Map();
4699
+ function civilFormatter(tz) {
4700
+ const cached = dtfCache.get(tz);
4701
+ if (cached !== void 0) return cached;
4702
+ let dtf = null;
4703
+ try {
4704
+ dtf = new Intl.DateTimeFormat("en-US", {
4705
+ timeZone: tz,
4706
+ hourCycle: "h23",
4707
+ weekday: "short",
4708
+ year: "numeric",
4709
+ month: "2-digit",
4710
+ day: "2-digit",
4711
+ hour: "2-digit",
4712
+ minute: "2-digit"
4713
+ });
4714
+ } catch {
4715
+ dtf = null;
4716
+ }
4717
+ dtfCache.set(tz, dtf);
4718
+ return dtf;
4719
+ }
4720
+ function civilParts(dtf, ms) {
4721
+ const parts = dtf.formatToParts(ms);
4722
+ const get = (t) => parts.find((p) => p.type === t)?.value ?? "";
4723
+ return {
4724
+ weekday: get("weekday"),
4725
+ year: Number(get("year")),
4726
+ month: Number(get("month")),
4727
+ day: Number(get("day")),
4728
+ hour: Number(get("hour")) % 24,
4729
+ minute: Number(get("minute"))
4730
+ };
4731
+ }
4732
+ function expandSessionZones(spec, from, to) {
4531
4733
  const pre = [];
4532
4734
  const post = [];
4533
- for (const [extStart, extEnd] of extended) {
4534
- const inside = regular.filter(([s, e]) => e > extStart && s < extEnd).sort((a, b) => a[0] - b[0]);
4535
- let cursor = extStart;
4536
- for (const [regStart, regEnd] of inside) {
4537
- if (regStart > cursor) pre.push([cursor, Math.min(regStart, extEnd)]);
4538
- cursor = Math.max(cursor, regEnd);
4539
- }
4540
- if (cursor < extEnd) post.push([cursor, extEnd]);
4735
+ const dtf = civilFormatter(spec.timezone);
4736
+ if (!dtf || !Number.isFinite(from) || !Number.isFinite(to)) return { pre, post };
4737
+ const seen = /* @__PURE__ */ new Set();
4738
+ for (let cursor = from - DAY_MS; cursor < to + DAY_MS; cursor += DAY_MS) {
4739
+ const civil = civilParts(dtf, cursor);
4740
+ const key = civil.year * 1e4 + civil.month * 100 + civil.day;
4741
+ if (!Number.isFinite(key) || seen.has(key)) continue;
4742
+ seen.add(key);
4743
+ if (civil.weekday === "Sat" || civil.weekday === "Sun") continue;
4744
+ const naive = Date.UTC(civil.year, civil.month - 1, civil.day);
4745
+ const noonGuess = naive + 720 * MINUTE_MS;
4746
+ const atNoon = civilParts(dtf, noonGuess);
4747
+ const offset = Date.UTC(atNoon.year, atNoon.month - 1, atNoon.day, atNoon.hour, atNoon.minute) - noonGuess;
4748
+ const at = (minutes) => naive + minutes * MINUTE_MS - offset;
4749
+ if (spec.extended.start < spec.regular.start) pre.push([at(spec.extended.start), at(spec.regular.start)]);
4750
+ if (spec.regular.end < spec.extended.end) post.push([at(spec.regular.end), at(spec.extended.end)]);
4541
4751
  }
4542
4752
  return { pre, post };
4543
4753
  }
4544
- var FETCH_AHEAD_MS2 = 10 * 864e5;
4545
- var MIN_LOOKBACK_MS = 3 * 864e5;
4546
- var MAX_LOOKBACK_MS = 120 * 864e5;
4754
+ var COVER_PAD_MIN_MS = 2 * DAY_MS;
4547
4755
  var SessionShadingTracker = class {
4548
4756
  constructor(onZones) {
4549
4757
  this.onZones = onZones;
4550
- /** Invalidates detached async work — bumped by every track()/stop(). */
4758
+ /** Invalidates the one async step (metadata resolution) — bumped by track()/stop(). */
4551
4759
  this.epoch = 0;
4552
- }
4553
- /** (Re)bind to a chart's data surface + market and evaluate once. */
4760
+ this.spec = null;
4761
+ this.ready = false;
4762
+ this.session = "regular";
4763
+ this.covered = null;
4764
+ /** The newest range seen — viewport moves during metadata resolution (a load's fit
4765
+ * animation) must not be lost, so the resolution always expands the LATEST range. */
4766
+ this.lastRange = null;
4767
+ }
4768
+ /** (Re)bind to a chart's data surface + market and expand once metadata lands. */
4554
4769
  track(data, symbol, opts) {
4555
4770
  const my = ++this.epoch;
4556
- void this.evaluate(my, data, symbol, opts);
4771
+ this.ready = false;
4772
+ this.spec = null;
4773
+ this.covered = null;
4774
+ this.session = opts.session;
4775
+ this.lastRange = opts.range;
4776
+ void data.symbolInfo(symbol).catch(() => void 0).then((si) => {
4777
+ if (my !== this.epoch) return;
4778
+ this.ready = true;
4779
+ this.spec = parseSessionSpec(si);
4780
+ this.emit(this.lastRange ?? opts.range, true);
4781
+ });
4782
+ }
4783
+ /** Follow a pan/zoom synchronously: bands are epoch-anchored, so only a range that
4784
+ * leaves the last expansion's coverage needs a recompute — no fetch, no debounce. */
4785
+ updateRange(range) {
4786
+ this.lastRange = range;
4787
+ if (!this.ready) return;
4788
+ this.emit(range, false);
4557
4789
  }
4558
4790
  stop() {
4559
4791
  this.epoch += 1;
4560
- }
4561
- async evaluate(my, data, symbol, opts) {
4562
- const resolved = data.resolve(symbol);
4563
- const provider = resolved ? data.providerInstance(resolved.provider) : void 0;
4564
- const si = await data.symbolInfo(symbol).catch(() => void 0);
4565
- if (my !== this.epoch) return;
4566
- const hasSessions = typeof si?.session === "string" && si.session !== "" && si.session !== "24x7";
4567
- if (!provider?.getCalendar || !hasSessions || !resolved) {
4568
- this.onZones(null);
4792
+ this.ready = false;
4793
+ this.spec = null;
4794
+ this.covered = null;
4795
+ this.lastRange = null;
4796
+ }
4797
+ emit(range, force) {
4798
+ if (!this.spec) {
4799
+ if (force) this.onZones(null);
4569
4800
  return;
4570
4801
  }
4571
- if (opts.session !== "extended") {
4572
- this.onZones({ pre: [], post: [] });
4802
+ if (this.session !== "extended") {
4803
+ if (force) this.onZones({ pre: [], post: [] });
4573
4804
  return;
4574
4805
  }
4575
- const now = Date.now();
4576
- const lookback = Math.max(MIN_LOOKBACK_MS, Math.min(MAX_LOOKBACK_MS, opts.lookbackMs));
4577
- const range = { from: now - lookback, to: now + FETCH_AHEAD_MS2 };
4578
- const [regular, extended] = await Promise.all([
4579
- provider.getCalendar(resolved.ticker, { ...range, session: "regular" }).catch(() => null),
4580
- provider.getCalendar(resolved.ticker, { ...range, session: "extended" }).catch(() => null)
4581
- ]);
4582
- if (my !== this.epoch) return;
4583
- if (!regular || !extended) return;
4584
- this.onZones(deriveSessionZones(regular, extended));
4806
+ const from = Math.min(range.from, range.to);
4807
+ const to = Math.max(range.from, range.to);
4808
+ if (!Number.isFinite(from) || !Number.isFinite(to)) return;
4809
+ if (!force && this.covered && from >= this.covered.from && to <= this.covered.to) return;
4810
+ const pad = Math.max(to - from, COVER_PAD_MIN_MS);
4811
+ const covered = { from: from - pad, to: to + pad };
4812
+ this.covered = covered;
4813
+ this.onZones(expandSessionZones(this.spec, covered.from, covered.to));
4585
4814
  }
4586
4815
  };
4587
4816
 
@@ -4806,7 +5035,11 @@ var ChartCell = class {
4806
5035
  this.deps.toast(`No registered provider serves "${symbol2}" (registered: ${list})`, "error", 6e3);
4807
5036
  });
4808
5037
  this.inner.on("load:start", () => this.watermark?.setLoading(true));
4809
- this.inner.on("load:end", () => this.watermark?.setLoading(false));
5038
+ this.inner.on("load:end", () => {
5039
+ this.watermark?.setLoading(false);
5040
+ this.refreshSessionShading();
5041
+ });
5042
+ this.inner.on("viewport:changed", (range) => this.sessionShading.updateRange(range));
4810
5043
  const tz = deps.timezone();
4811
5044
  if (tz !== "Etc/UTC") this.inner.renderer.set("timezone", tz);
4812
5045
  this.indicatorTitlesOn = seed.indicatorTitles ?? true;
@@ -4816,12 +5049,15 @@ var ChartCell = class {
4816
5049
  this.watermarkOn = seed.watermark ?? deps.watermark;
4817
5050
  this.watermark = deps.watermark ? new Watermark(this.host, symbol ?? "", seed.timeframe ?? "60") : null;
4818
5051
  if (!this.watermarkOn) this.watermark?.setVisible(false);
4819
- this.statusline = deps.statusline ? new Statusline(this.host, symbol ?? "") : null;
5052
+ this.statusline = deps.statusline ? new Statusline(this.host, symbol ?? "", (sym) => this.inner?.data.symbolIcon(sym)) : null;
4820
5053
  this.statusline?.setMeta(seed.timeframe ?? "60", this.state.provider ?? "");
4821
5054
  this.statusline?.onChart(this.inner);
4822
5055
  this.marketStatus = this.statusline ? new MarketStatusTracker((s) => this.statusline?.setMarketStatus(s)) : null;
4823
5056
  void this.inner.data.ready().then(() => {
4824
- if (this.inner && this.state.symbol) this.statusline?.setMeta(this.state.timeframe ?? "60", this.inner.data.displayPrefix(this.state.symbol) ?? this.state.provider ?? "");
5057
+ if (this.inner && this.state.symbol) {
5058
+ this.statusline?.setSymbol(this.state.symbol);
5059
+ this.statusline?.setMeta(this.state.timeframe ?? "60", this.inner.data.displayPrefix(this.state.symbol) ?? this.state.provider ?? "");
5060
+ }
4825
5061
  this.refreshSessionAvailable();
4826
5062
  if (this.inner && this.state.symbol) this.marketStatus?.track(this.inner.data, this.state.symbol);
4827
5063
  });
@@ -4930,14 +5166,18 @@ var ChartCell = class {
4930
5166
  this.refreshSessionShading();
4931
5167
  });
4932
5168
  }
4933
- /** (Re)derive the pre/post-market shading bands for this cell's market. The loaded
4934
- * depth (bars × timeframe) bounds the calendar fetch; the tracker clamps it. */
5169
+ /** (Re)derive the pre/post-market shading bands for this cell's market. The bands
5170
+ * expand locally from the symbol's session vocabulary, so they paint as soon as
5171
+ * metadata is known and follow any pan depth without provider round trips. */
4935
5172
  refreshSessionShading() {
4936
5173
  const chart = this.inner;
4937
5174
  const symbol = this.state.symbol;
4938
5175
  if (!chart || !symbol) return;
4939
- const lookbackMs = Math.max(this.state.bars ?? 1e3, this.rangeBars) * timeframeMs(this.state.timeframe ?? "60");
4940
- this.sessionShading.track(chart.data, symbol, { session: this.session, lookbackMs });
5176
+ const now = Date.now();
5177
+ const requestedSpan = Math.max(this.state.bars ?? 1e3, this.rangeBars) * timeframeMs(this.state.timeframe ?? "60");
5178
+ const fallbackSpan = Number.isFinite(requestedSpan) ? Math.max(3 * 864e5, requestedSpan) : 3 * 864e5;
5179
+ const range = chart.getVisibleRange() ?? { from: now - fallbackSpan, to: now };
5180
+ this.sessionShading.track(chart.data, symbol, { session: this.session, range });
4941
5181
  }
4942
5182
  /** The session-shade colors live in the renderer CONFIG (persisted with it, edited
4943
5183
  * live by the dialog swatch) — the cell only proxies them into its settings rows. */
@@ -5033,10 +5273,10 @@ var ChartCell = class {
5033
5273
  id: "status-line",
5034
5274
  rows: [
5035
5275
  { kind: "heading", label: "Status line", id: "parts" },
5036
- { kind: "toggle", label: "Symbol name", id: "name", get: () => sl.partVisible("name"), set: (v) => sl.setPartVisible("name", v) },
5037
- { kind: "toggle", label: "Market status", id: "market", get: () => sl.partVisible("market"), set: (v) => sl.setPartVisible("market", v) },
5038
- { kind: "toggle", label: "OHLC values", id: "ohlc", get: () => sl.partVisible("ohlc"), set: (v) => sl.setPartVisible("ohlc", v) },
5039
- { kind: "toggle", label: "Bar change values", id: "change", get: () => sl.partVisible("change"), set: (v) => sl.setPartVisible("change", v) },
5276
+ { kind: "toggle", label: "Symbol name", id: "name", get: () => sl.partVisible("name"), set: (v) => this.setStatuslinePart("name", v) },
5277
+ { kind: "toggle", label: "Market status", id: "market", get: () => sl.partVisible("market"), set: (v) => this.setStatuslinePart("market", v) },
5278
+ { kind: "toggle", label: "OHLC values", id: "ohlc", get: () => sl.partVisible("ohlc"), set: (v) => this.setStatuslinePart("ohlc", v) },
5279
+ { kind: "toggle", label: "Bar change values", id: "change", get: () => sl.partVisible("change"), set: (v) => this.setStatuslinePart("change", v) },
5040
5280
  { kind: "heading", label: "Indicators", id: "indicators" },
5041
5281
  {
5042
5282
  kind: "toggle",
@@ -5071,12 +5311,40 @@ var ChartCell = class {
5071
5311
  this.indicatorTitlesOn = visible;
5072
5312
  this.inner?.renderer.set("indicatorTitles", visible);
5073
5313
  this.deps.onStateDirty();
5314
+ this.deps.onStatusPrefsChanged(this.id);
5074
5315
  }
5075
5316
  /** Show/hide the plot values beside this cell's legend titles (persisted per cell). */
5076
5317
  setIndicatorValuesVisible(visible) {
5077
5318
  this.indicatorValuesOn = visible;
5078
5319
  this.inner?.renderer.set("indicatorValues", visible);
5079
5320
  this.deps.onStateDirty();
5321
+ this.deps.onStatusPrefsChanged(this.id);
5322
+ }
5323
+ /** Show/hide one status-line segment (the settings dialog's Status line tab). */
5324
+ setStatuslinePart(part, visible) {
5325
+ this.statusline?.setPartVisible(part, visible);
5326
+ this.deps.onStatusPrefsChanged(this.id);
5327
+ }
5328
+ /** This cell's Status line tab prefs as one bundle (see {@link CellStatusPrefs}). */
5329
+ statusPrefs() {
5330
+ const sl = this.statusline;
5331
+ return {
5332
+ parts: sl ? { name: sl.partVisible("name"), market: sl.partVisible("market"), ohlc: sl.partVisible("ohlc"), change: sl.partVisible("change") } : null,
5333
+ indicatorTitles: this.indicatorTitlesOn,
5334
+ indicatorValues: this.indicatorValuesOn
5335
+ };
5336
+ }
5337
+ /** Converge this cell's Status line tab prefs to `prefs` — the follower half of
5338
+ * the workspace's style link. Idempotent: matching values change nothing, so a
5339
+ * propagated echo dies on its own. */
5340
+ applyStatusPrefs(prefs) {
5341
+ if (prefs.parts && this.statusline) {
5342
+ for (const part of Object.keys(prefs.parts)) {
5343
+ if (this.statusline.partVisible(part) !== prefs.parts[part]) this.statusline.setPartVisible(part, prefs.parts[part]);
5344
+ }
5345
+ }
5346
+ if (prefs.indicatorTitles !== this.indicatorTitlesOn) this.setIndicatorTitlesVisible(prefs.indicatorTitles);
5347
+ if (prefs.indicatorValues !== this.indicatorValuesOn) this.setIndicatorValuesVisible(prefs.indicatorValues);
5080
5348
  }
5081
5349
  /** The LIVE chart of this cell — never cache it across a layout change (the cell's
5082
5350
  * identity is what endures; the chart dies with the cell). */
@@ -6003,7 +6271,8 @@ var MobileBar = class {
6003
6271
  if (!ctx) return;
6004
6272
  const doc = this.el.ownerDocument;
6005
6273
  this.actionsHost.replaceChildren();
6006
- for (const action of widgetActions("topbar", ctx).filter((a) => a.align === "left")) {
6274
+ const builtin = new Set(TOPBAR_BUILTIN_IDS);
6275
+ for (const action of widgetActions("topbar", ctx).filter((a) => a.align === "left" && !builtin.has(a.id))) {
6007
6276
  const b = doc.createElement("button");
6008
6277
  b.className = "vela-mb-item";
6009
6278
  b.setAttribute("aria-label", action.label);
@@ -6532,10 +6801,10 @@ var MoreDrawer = class {
6532
6801
  });
6533
6802
  actions.appendChild(b);
6534
6803
  };
6535
- action("undo", "Undo", this.opts.canUndo(), this.opts.onUndo);
6536
- action("redo", "Redo", this.opts.canRedo(), this.opts.onRedo);
6537
- action("camera", "Screenshot", true, this.opts.onScreenshot);
6538
- this.drawer.body.appendChild(actions);
6804
+ if (this.opts.onUndo) action("undo", "Undo", this.opts.canUndo(), this.opts.onUndo);
6805
+ if (this.opts.onRedo) action("redo", "Redo", this.opts.canRedo(), this.opts.onRedo);
6806
+ if (this.opts.onScreenshot) action("camera", "Screenshot", true, this.opts.onScreenshot);
6807
+ if (actions.childElementCount > 0) this.drawer.body.appendChild(actions);
6539
6808
  const list = doc.createElement("div");
6540
6809
  list.className = "vela-md-list";
6541
6810
  const current = this.opts.priceStyles().find((s) => s.id === this.opts.priceStyle());
@@ -6556,8 +6825,10 @@ var MoreDrawer = class {
6556
6825
  })
6557
6826
  );
6558
6827
  }
6559
- const alertCount = this.opts.alerts().length;
6560
- list.appendChild(this.row(doc, "Alerts", { icon: "bell", value: alertCount > 0 ? String(alertCount) : void 0, chevron: true, onClick: () => this.show("alerts") }));
6828
+ if (this.opts.alerts) {
6829
+ const alertCount = this.opts.alerts().length;
6830
+ list.appendChild(this.row(doc, "Alerts", { icon: "bell", value: alertCount > 0 ? String(alertCount) : void 0, chevron: true, onClick: () => this.show("alerts") }));
6831
+ }
6561
6832
  for (const act of this.opts.actions()) {
6562
6833
  list.appendChild(
6563
6834
  this.row(doc, act.label, {
@@ -6638,7 +6909,7 @@ var MoreDrawer = class {
6638
6909
  this.drawer.body.appendChild(list);
6639
6910
  }
6640
6911
  renderAlerts(doc) {
6641
- const alerts = this.opts.alerts();
6912
+ const alerts = this.opts.alerts?.() ?? [];
6642
6913
  if (alerts.length === 0) {
6643
6914
  const empty = doc.createElement("div");
6644
6915
  empty.className = "vela-md-empty";
@@ -7015,16 +7286,21 @@ var DrawingPill = class {
7015
7286
 
7016
7287
  // src/workspace/context.ts
7017
7288
  function buildContext(host) {
7018
- const active = host.active();
7019
7289
  return {
7020
7290
  get chart() {
7021
7291
  const cell = host.active();
7022
7292
  if (!cell) throw new Error("VelaWorkspace has no active cell yet");
7023
7293
  return cell.chart;
7024
7294
  },
7025
- symbol: active?.symbol ?? "",
7026
- timeframe: active?.timeframe ?? "60",
7027
- priceStyle: active?.priceStyle ?? "candles",
7295
+ get symbol() {
7296
+ return host.active()?.symbol ?? "";
7297
+ },
7298
+ get timeframe() {
7299
+ return host.active()?.timeframe ?? "60";
7300
+ },
7301
+ get priceStyle() {
7302
+ return host.active()?.priceStyle ?? "candles";
7303
+ },
7028
7304
  setSymbol: (symbol) => host.active()?.setSymbol(symbol),
7029
7305
  setTimeframe: (tf) => host.active()?.setTimeframe(tf),
7030
7306
  setPriceStyle: (style) => host.active()?.setPriceStyle(style),
@@ -7035,8 +7311,12 @@ function buildContext(host) {
7035
7311
  addIndicator: (entry) => host.active()?.addExternalIndicator(entry),
7036
7312
  addNativeIndicator: (type) => host.active()?.addNative(type),
7037
7313
  stateChanged: () => host.stateDirty(),
7038
- cells: host.cells().map((c) => ({ id: c.id, chart: c.chart, symbol: c.symbol, timeframe: c.timeframe })),
7039
- activeCellId: active?.id ?? "",
7314
+ get cells() {
7315
+ return host.cells().map((c) => ({ id: c.id, chart: c.chart, symbol: c.symbol, timeframe: c.timeframe }));
7316
+ },
7317
+ get activeCellId() {
7318
+ return host.active()?.id ?? "";
7319
+ },
7040
7320
  setActiveCell: (id) => host.setActiveCell(id)
7041
7321
  };
7042
7322
  }
@@ -7130,6 +7410,10 @@ var VelaWorkspace = class {
7130
7410
  /** Same guard for the drawings link: the propagated mutations' own `drawing:*`
7131
7411
  * events fire synchronously inside the propagation loop and must not fan out again. */
7132
7412
  this.drawingSyncBusy = false;
7413
+ /** Same guard for the style link: a follower's `applyConfig` re-fires its
7414
+ * `onConfigChanged` in the same tick, and a state restore applies per-cell
7415
+ * configs that legitimately differ — neither must propagate. */
7416
+ this.styleSyncBusy = false;
7133
7417
  /** LINKED drawings (the drawings sync): one map per synced set (cellId → that
7134
7418
  * cell's drawing id), reachable from every member under its `cellId\0drawingId`
7135
7419
  * key — any member finds its peers to push edits/removals onto. Survives a
@@ -7190,13 +7474,14 @@ var VelaWorkspace = class {
7190
7474
  if (boot?.favorites) this.favs = [...boot.favorites];
7191
7475
  if (boot?.timeframeFavorites) this.tfFavs = [...boot.timeframeFavorites];
7192
7476
  const sync = boot?.sync ?? opts.sync;
7193
- for (const kind of ["viewport", "symbol", "timeframe", "crosshair", "drawings"]) {
7194
- this.applySyncSetting(kind, sync?.[kind]);
7195
- }
7477
+ for (const kind of SYNC_KINDS) this.applySyncSetting(kind, sync?.[kind]);
7196
7478
  this.monoLayout = opts.layout === false;
7197
7479
  const optLayout = opts.layout === false || opts.layout === void 0 ? "4" : opts.layout;
7198
7480
  this.def = this.resolveLayout(this.monoLayout ? "1" : boot?.layout && ensureLayout(boot.layout) ? boot.layout : optLayout);
7199
7481
  this.alertCap = Math.max(1, opts.alertCap ?? ALERT_CAP);
7482
+ this.topbarComp = resolveTopbarComposition(opts.topbar);
7483
+ this.indicatorsOverride = topbarActionOverride("indicators");
7484
+ this.screenshotOverride = topbarActionOverride("screenshot");
7200
7485
  if (boot?.trackSizes) for (const [id, ts] of Object.entries(boot.trackSizes)) this.trackSizes.set(id, ts);
7201
7486
  if (boot?.ext) this.extState = { ...boot.ext };
7202
7487
  if (boot?.charts) for (const { id, ...cs } of boot.charts) this.pool.set(id, cs);
@@ -7213,6 +7498,8 @@ var VelaWorkspace = class {
7213
7498
  });
7214
7499
  this.symbolPicker = new SymbolPicker({
7215
7500
  host: this.root,
7501
+ // Row icons come from each descriptor's OWNING provider (resolveSymbolIcon).
7502
+ iconFor: (d) => this.feed.symbolIconOf(d),
7216
7503
  onSelect: (ticker) => this.active.setSymbol(ticker),
7217
7504
  onOpenChange: (open) => {
7218
7505
  if (open) for (const cell of this.cells()) cell.chart.renderer.closeDialogs();
@@ -7220,7 +7507,7 @@ var VelaWorkspace = class {
7220
7507
  }
7221
7508
  });
7222
7509
  this.symbolPicker.setSource(() => this.feed.symbols());
7223
- this.indicatorPicker = opts.indicatorPicker !== false ? new IndicatorPicker({
7510
+ this.indicatorPicker = opts.indicatorPicker !== false && !this.indicatorsOverride && topbarHas(this.topbarComp, "indicators") ? new IndicatorPicker({
7224
7511
  host: this.root,
7225
7512
  library: () => this.active.libraryRows(),
7226
7513
  onChart: () => this.active.onChartRows(),
@@ -7239,6 +7526,9 @@ var VelaWorkspace = class {
7239
7526
  });
7240
7527
  this.topbar = new Topbar(this.root, {
7241
7528
  symbol: "",
7529
+ // RAW option, not the resolved lists — the bar distinguishes a host-declared
7530
+ // side (list is law) from a default one (an override's `order` may flow).
7531
+ composition: opts.topbar,
7242
7532
  onSymbolClick: () => this.symbolPicker.open(),
7243
7533
  ...picker ? { onIndicatorsClick: () => picker.open() } : {},
7244
7534
  onUndoClick: () => this.active.history.undo(),
@@ -7268,7 +7558,8 @@ var VelaWorkspace = class {
7268
7558
  syncs: () => [
7269
7559
  { id: "symbol", label: "Symbol", checked: this.syncOpts.symbol === true },
7270
7560
  { id: "timeframe", label: "Interval", checked: this.syncOpts.timeframe === true },
7271
- { id: "crosshair", label: "Crosshair", checked: this.syncOpts.crosshair === true }
7561
+ { id: "crosshair", label: "Crosshair", checked: this.syncOpts.crosshair === true },
7562
+ { id: "style", label: "Style", checked: this.syncOpts.style === true }
7272
7563
  ],
7273
7564
  onToggleSync: (id) => {
7274
7565
  const kind = id;
@@ -7296,7 +7587,7 @@ var VelaWorkspace = class {
7296
7587
  context: () => this.context(),
7297
7588
  changed: () => this.markStateDirty()
7298
7589
  });
7299
- this.objectTree = new ObjectTree(main);
7590
+ this.objectTree = new ObjectTree(main, (sym) => this.feed.symbolIcon(sym));
7300
7591
  this.dataWindow = new DataWindow(main);
7301
7592
  this.dock.addBuiltIn({ id: "dataWindow", title: "Data window", icon: "datawindow", order: 10, panel: this.dataWindow, onChart: (c) => this.dataWindow.onChart(c) });
7302
7593
  this.dock.addBuiltIn({ id: "objects", title: "Object tree", icon: "objects", order: 20, panel: this.objectTree, onChart: (c) => this.objectTree.onChart(c) });
@@ -7368,7 +7659,9 @@ var VelaWorkspace = class {
7368
7659
  timeframe: "60",
7369
7660
  onSymbolClick: () => this.symbolPicker.open(),
7370
7661
  onTimeframeClick: () => this.openTimeframeDrawer(),
7371
- ...picker ? { onIndicatorsClick: () => picker.open() } : {},
7662
+ // Same visibility truth as the desktop bar: composition-hidden
7663
+ // indicators lose their mobile stop too; an override takes it over.
7664
+ ...topbarHas(this.topbarComp, "indicators") && (picker || this.indicatorsOverride) ? { onIndicatorsClick: this.indicatorsOverride ? () => this.runOverride(this.indicatorsOverride) : () => picker.open() } : {},
7372
7665
  getContext: () => this.context(),
7373
7666
  ...this.drawingsEnabled ? { onDrawingsClick: () => this.openDrawingsDrawer() } : {},
7374
7667
  onMoreClick: () => this.openMoreDrawer(),
@@ -7550,7 +7843,7 @@ var VelaWorkspace = class {
7550
7843
  this.topbar.setTimeframeFavorites(this.tfFavs);
7551
7844
  }
7552
7845
  this.dock.applyState(st.panels);
7553
- for (const kind of ["viewport", "symbol", "timeframe", "crosshair", "drawings"]) this.applySyncSetting(kind, st.sync?.[kind]);
7846
+ for (const kind of SYNC_KINDS) this.applySyncSetting(kind, st.sync?.[kind]);
7554
7847
  this.trackSizes.clear();
7555
7848
  if (st.trackSizes) for (const [id, ts] of Object.entries(st.trackSizes)) this.trackSizes.set(id, ts);
7556
7849
  const targetDef = this.monoLayout ? this.def : ensureLayout(st.layout) ?? this.def;
@@ -7561,9 +7854,14 @@ var VelaWorkspace = class {
7561
7854
  for (const cell of this.cellsById.values()) cell.chart.drawings.setFavorites(this.favs);
7562
7855
  }
7563
7856
  this.drawingLinks.clear();
7564
- for (const [i] of this.def.cells.entries()) {
7565
- const { id, ...cs } = st.charts[i];
7566
- this.cellsById.get(id)?.rehydrate(cs);
7857
+ this.styleSyncBusy = true;
7858
+ try {
7859
+ for (const [i] of this.def.cells.entries()) {
7860
+ const { id, ...cs } = st.charts[i];
7861
+ this.cellsById.get(id)?.rehydrate(cs);
7862
+ }
7863
+ } finally {
7864
+ this.styleSyncBusy = false;
7567
7865
  }
7568
7866
  if (st.timezone) this.setTimezone(st.timezone);
7569
7867
  this.pool.clear();
@@ -7663,6 +7961,7 @@ var VelaWorkspace = class {
7663
7961
  const rebuildAll = nextBackend !== this.cellBackend;
7664
7962
  this.order = orderAfterLayout(this.order, next.cells.length, this.activeId);
7665
7963
  const keep = new Set(this.order.slice(0, next.cells.length));
7964
+ const preexisting = new Set(this.cellsById.keys());
7666
7965
  for (const [id, cell] of [...this.cellsById]) {
7667
7966
  if (!keep.has(id) || rebuildAll) {
7668
7967
  this.poolSet(id, cell.dehydrate());
@@ -7675,6 +7974,7 @@ var VelaWorkspace = class {
7675
7974
  this.cellBackend = nextBackend;
7676
7975
  this.applyGrid();
7677
7976
  this.buildCells();
7977
+ this.alignNewCellStyles(preexisting);
7678
7978
  this.syncCellPresentation();
7679
7979
  this.topbar.setLayout(next.id);
7680
7980
  const nextActive = activeAfterLayout(this.activeId, this.order.slice(0, next.cells.length));
@@ -7878,6 +8178,7 @@ var VelaWorkspace = class {
7878
8178
  onMarketChanged: (id2) => this.onCellMarketChanged(id2),
7879
8179
  onPriceStyleChanged: (id2) => this.onCellPriceStyleChanged(id2),
7880
8180
  onIndicatorsChanged: (id2) => this.onCellIndicatorsChanged(id2),
8181
+ onStatusPrefsChanged: (id2) => this.propagateStylePrefs(id2),
7881
8182
  onStateDirty: () => this.markStateDirty(),
7882
8183
  manifestSettled: () => this.manifestSettled,
7883
8184
  toast: (message, kind, durationMs) => this.toastHost.show(message, kind, durationMs)
@@ -7953,6 +8254,7 @@ var VelaWorkspace = class {
7953
8254
  this.drawToolbar?.setEraserActive(mode === "eraser");
7954
8255
  });
7955
8256
  chart.on("viewport:changed", (range) => this.propagateViewport(cell.id, range));
8257
+ chart.renderer.onConfigChanged(() => this.propagateStylePrefs(cell.id));
7956
8258
  chart.on("theme:changed", (t) => this.setTheme(t));
7957
8259
  chart.renderer.onAxisLongPress((e) => {
7958
8260
  if (this.layoutCtl.current !== "mobile") return;
@@ -7985,11 +8287,67 @@ var VelaWorkspace = class {
7985
8287
  if (kind === "viewport") {
7986
8288
  const range = this.cellsById.get(this.activeId)?.chart.getVisibleRange();
7987
8289
  if (range) this.propagateViewport(this.activeId, range);
8290
+ } else if (kind === "style") {
8291
+ this.propagateStylePrefs(this.activeId);
7988
8292
  } else {
7989
8293
  this.propagateMarket(this.activeId);
7990
8294
  }
7991
8295
  }
7992
8296
  }
8297
+ /**
8298
+ * Align cells minted by a layout change to their style group: with the link on, a
8299
+ * NEW cell (fresh slot or one returning from the pool, which missed edits while
8300
+ * dormant) inherits the presentation of a pre-existing group peer — the active
8301
+ * cell when it is one — instead of sitting on its own state beside a styled
8302
+ * group. Propagation runs FROM the peer, so a newborn's defaults never overwrite
8303
+ * the group, and the equality short-circuits keep converged peers untouched.
8304
+ */
8305
+ alignNewCellStyles(preexisting) {
8306
+ const setting = this.syncOpts.style;
8307
+ if (!setting) return;
8308
+ const ids = [...this.cellsById.keys()];
8309
+ const propagated = /* @__PURE__ */ new Set();
8310
+ for (const id of ids) {
8311
+ if (preexisting.has(id)) continue;
8312
+ const peers = syncTargets(id, setting, ids).filter((p) => preexisting.has(p));
8313
+ if (peers.length === 0) continue;
8314
+ const source = this.activeId && peers.includes(this.activeId) ? this.activeId : peers[0];
8315
+ if (propagated.has(source)) continue;
8316
+ propagated.add(source);
8317
+ this.propagateStylePrefs(source);
8318
+ }
8319
+ }
8320
+ /**
8321
+ * Mirror an origin cell's presentation — the Canvas + Scales-and-lines slice of
8322
+ * its renderer config plus its Status line tab prefs — onto its same-group
8323
+ * followers (the style link). Loop-safe two ways: the busy guard eats the
8324
+ * followers' SYNCHRONOUS echoes (their `applyConfig` re-fires `onConfigChanged`
8325
+ * in the same tick), and the equality short-circuits leave already-converged
8326
+ * followers untouched, so nothing re-emits once the group agrees.
8327
+ */
8328
+ propagateStylePrefs(originId) {
8329
+ if (this.styleSyncBusy || this.destroyed) return;
8330
+ const targets = syncTargets(originId, this.syncOpts.style, [...this.cellsById.keys()]);
8331
+ if (targets.length === 0) return;
8332
+ const origin = this.cellsById.get(originId);
8333
+ if (!origin) return;
8334
+ const slice = styleConfigSlice(origin.chart.renderer.getConfig());
8335
+ const sliceJson = slice ? JSON.stringify(slice) : null;
8336
+ const prefs = origin.statusPrefs();
8337
+ this.styleSyncBusy = true;
8338
+ try {
8339
+ for (const id of targets) {
8340
+ const cell = this.cellsById.get(id);
8341
+ if (!cell) continue;
8342
+ if (slice && sliceJson !== JSON.stringify(styleConfigSlice(cell.chart.renderer.getConfig()))) {
8343
+ cell.chart.renderer.applyConfig(slice);
8344
+ }
8345
+ cell.applyStatusPrefs(prefs);
8346
+ }
8347
+ } finally {
8348
+ this.styleSyncBusy = false;
8349
+ }
8350
+ }
7993
8351
  /**
7994
8352
  * Mirror an origin cell's pointer time onto its same-group followers as GHOST
7995
8353
  * crosshairs (`renderer.setExternalCrosshair`). The horizontal price level rides
@@ -8264,27 +8622,32 @@ var VelaWorkspace = class {
8264
8622
  this.drawingsDrawer.open();
8265
8623
  }
8266
8624
  openMoreDrawer() {
8625
+ const has = (id) => topbarHas(this.topbarComp, id);
8267
8626
  this.moreDrawer ?? (this.moreDrawer = new MoreDrawer({
8268
8627
  host: this.root,
8269
- onUndo: () => this.active.history.undo(),
8270
- onRedo: () => this.active.history.redo(),
8271
- onScreenshot: () => this.active.downloadScreenshot(),
8628
+ ...has("undo-redo") ? { onUndo: () => this.active.history.undo(), onRedo: () => this.active.history.redo() } : {},
8629
+ ...has("screenshot") ? { onScreenshot: this.screenshotOverride ? () => this.runOverride(this.screenshotOverride) : () => this.active.downloadScreenshot() } : {},
8272
8630
  canUndo: () => this.active.history.canUndo,
8273
8631
  canRedo: () => this.active.history.canRedo,
8274
8632
  priceStyles: () => priceStyleIds().map((id) => ({ id, label: priceStyleLabel(id), icon: priceStyleIcon(id) })),
8275
8633
  priceStyle: () => this.active.priceStyle,
8276
8634
  onPriceStyle: (id) => this.active.setPriceStyle(id),
8277
- panels: () => [...this.dock.list()],
8635
+ panels: () => has("panels") ? [...this.dock.list()] : [],
8278
8636
  onTogglePanel: (id) => this.dock.toggle(id),
8279
- alerts: () => this.alerts.map((a) => ({ title: `${a.source} \xB7 ${a.title}`, message: a.message, time: a.time })),
8637
+ ...has("alerts") ? { alerts: () => this.alerts.map((a) => ({ title: `${a.source} \xB7 ${a.title}`, message: a.message, time: a.time })) } : {},
8280
8638
  // Left-aligned actions have their own bottom-bar stop — only the rest
8281
- // lands in the drawer, or every left action would appear twice.
8282
- actions: () => widgetActions("topbar", this.context()).filter((a) => a.align !== "left").map((a) => ({ label: a.label, icon: a.icon, run: () => a.run(this.context()) })),
8639
+ // lands in the drawer, or every left action would appear twice. Built-in-id
8640
+ // actions are slot OVERRIDES: they reach the drawer through the slot's own
8641
+ // routed button (screenshot) or stop (indicators), never as an extra row.
8642
+ actions: () => {
8643
+ const builtin = new Set(TOPBAR_BUILTIN_IDS);
8644
+ return widgetActions("topbar", this.context()).filter((a) => a.align !== "left" && !builtin.has(a.id)).map((a) => ({ label: a.label, icon: a.icon, run: () => a.run(this.context()) }));
8645
+ },
8283
8646
  // The desktop layout dropdown's whole surface — the grid canvas, the
8284
8647
  // non-canvas presets and the sync switches — relocated into the kebab
8285
8648
  // drawer (the topbar is hidden on mobile). Same reads as the topbar block;
8286
- // single-chart mode omits it here too.
8287
- layout: this.monoLayout ? void 0 : {
8649
+ // single-chart mode and a composition without 'layout' omit it here too.
8650
+ layout: this.monoLayout || !has("layout") ? void 0 : {
8288
8651
  shape: () => layoutShape(this.def),
8289
8652
  presets: () => layouts().filter((l) => layoutShape(l) === null).map((l) => ({ id: l.id, label: l.label, checked: l.id === this.def.id })),
8290
8653
  onSelectGrid: (rows, cols) => this.setLayout(layoutForGrid(rows, cols)),
@@ -8292,7 +8655,8 @@ var VelaWorkspace = class {
8292
8655
  syncs: () => [
8293
8656
  { id: "symbol", label: "Symbol", checked: this.syncOpts.symbol === true },
8294
8657
  { id: "timeframe", label: "Interval", checked: this.syncOpts.timeframe === true },
8295
- { id: "crosshair", label: "Crosshair", checked: this.syncOpts.crosshair === true }
8658
+ { id: "crosshair", label: "Crosshair", checked: this.syncOpts.crosshair === true },
8659
+ { id: "style", label: "Style", checked: this.syncOpts.style === true }
8296
8660
  ],
8297
8661
  onToggleSync: (id) => {
8298
8662
  const kind = id;
@@ -8351,9 +8715,23 @@ var VelaWorkspace = class {
8351
8715
  }
8352
8716
  }
8353
8717
  }
8718
+ /** Invoke a slot override the way its button would: fresh context, `when` respected. */
8719
+ runOverride(action) {
8720
+ const ctx = this.context();
8721
+ if (!action.when || action.when(ctx)) action.run(ctx);
8722
+ }
8354
8723
  /** The default shortcut set — every binding acts on the ACTIVE cell. */
8355
8724
  registerDefaultKeys() {
8356
- this.keymap.register({ id: "chart.screenshot", keys: "mod+alt+s", label: "Download a chart screenshot", category: "Chart", run: () => this.active.downloadScreenshot() });
8725
+ if (topbarHas(this.topbarComp, "screenshot")) {
8726
+ const ov = this.screenshotOverride;
8727
+ this.keymap.register({
8728
+ id: "chart.screenshot",
8729
+ keys: "mod+alt+s",
8730
+ label: ov ? ov.label : "Download a chart screenshot",
8731
+ category: "Chart",
8732
+ run: ov ? () => this.runOverride(ov) : () => this.active.downloadScreenshot()
8733
+ });
8734
+ }
8357
8735
  this.keymap.register({ id: "chart.reset-view", keys: "alt+r", label: "Reset view (all history)", category: "Chart", run: () => this.active.chart.setVisibleRangePreset("ALL") });
8358
8736
  this.keymap.register({ id: "chart.toggle-log", keys: "alt+l", label: "Toggle logarithmic scale", category: "Chart", run: () => this.active.chart.renderer.set("logScale", !this.active.chart.renderer.get("logScale")) });
8359
8737
  this.keymap.register({
@@ -8393,7 +8771,10 @@ var VelaWorkspace = class {
8393
8771
  this.keymap.register({ id: "view.zoom-out", keys: "mod+arrowdown", label: "Zoom out", category: "Chart", run: () => this.glider.zoom(ZOOM_OUT) });
8394
8772
  this.keymap.register({ id: "view.pan-left", keys: "mod+arrowleft", label: "Pan toward history", category: "Chart", run: () => this.active.chart.panBy(-PAN_FAST) });
8395
8773
  this.keymap.register({ id: "view.pan-right", keys: "mod+arrowright", label: "Pan toward now", category: "Chart", run: () => this.active.chart.panBy(PAN_FAST) });
8396
- if (this.indicatorPicker) {
8774
+ const indOv = this.indicatorsOverride;
8775
+ if (indOv && topbarHas(this.topbarComp, "indicators")) {
8776
+ this.keymap.register({ id: "indicators.open", keys: "/", label: indOv.label, category: "Indicators", run: () => this.runOverride(indOv) });
8777
+ } else if (this.indicatorPicker) {
8397
8778
  this.keymap.register({ id: "indicators.open", keys: "/", label: "Open the indicator picker", category: "Indicators", run: () => this.indicatorPicker?.open() });
8398
8779
  }
8399
8780
  this.keymap.register({