@luxalgo/vela 0.6.6 → 0.6.7

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/{bottombar-COnL2Sk4.d.ts → bottombar-Br4rzx_R.d.ts} +52 -6
  4. package/dist/{bottombar-DeDXbF_K.d.cts → bottombar-Div5zibZ.d.cts} +52 -6
  5. package/dist/{chunk-JQFO2WMU.js → chunk-6WFKTAT4.js} +37 -1
  6. package/dist/{chunk-OEUGUXL7.js → chunk-L3I2CCYO.js} +49 -1
  7. package/dist/chunk-W4EJWLEO.js +12 -0
  8. package/dist/{chunk-TMXK2SJR.js → chunk-WVP4XV5A.js} +221 -65
  9. package/dist/{contributions-DNMqrAuw.d.cts → contributions-Hv4_cOJo.d.cts} +46 -3
  10. package/dist/{contributions-BqCIsbaP.d.ts → contributions-Z12BrWCy.d.ts} +46 -3
  11. package/dist/index.cjs +64 -0
  12. package/dist/index.d.cts +8 -4
  13. package/dist/index.d.ts +8 -4
  14. package/dist/index.js +2 -2
  15. package/dist/{plugin-WnvNNJOp.d.ts → plugin-9koBjc5H.d.ts} +2 -2
  16. package/dist/{plugin-CN8U2__Q.d.cts → plugin-BXhDHTYn.d.cts} +2 -2
  17. package/dist/plugin.cjs +28 -0
  18. package/dist/plugin.d.cts +3 -3
  19. package/dist/plugin.d.ts +3 -3
  20. package/dist/plugin.js +1 -1
  21. package/dist/providers/binance.cjs +16 -0
  22. package/dist/providers/binance.d.cts +4 -1
  23. package/dist/providers/binance.d.ts +4 -1
  24. package/dist/providers/binance.js +7 -0
  25. package/dist/providers/coinbase.cjs +16 -0
  26. package/dist/providers/coinbase.d.cts +4 -1
  27. package/dist/providers/coinbase.d.ts +4 -1
  28. package/dist/providers/coinbase.js +7 -0
  29. package/dist/providers/hyperliquid.cjs +16 -0
  30. package/dist/providers/hyperliquid.d.cts +4 -1
  31. package/dist/providers/hyperliquid.d.ts +4 -1
  32. package/dist/providers/hyperliquid.js +7 -0
  33. package/dist/vela.global.js +90 -0
  34. package/dist/vela.global.min.js +42 -42
  35. package/dist/widget.cjs +319 -63
  36. package/dist/widget.d.cts +52 -10
  37. package/dist/widget.d.ts +52 -10
  38. package/dist/widget.js +5 -4
  39. package/dist/workspace.cjs +265 -65
  40. package/dist/workspace.d.cts +13 -4
  41. package/dist/workspace.d.ts +13 -4
  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-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
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,12 @@ 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();
590
602
  this.onHairlineSync = () => {
591
603
  if (this.hairlineRaf) return;
592
604
  const win = this.el.ownerDocument.defaultView;
@@ -601,6 +613,8 @@ var Topbar = class {
601
613
  this.host = host;
602
614
  this.timeframe = opts.timeframe;
603
615
  this.priceStyle = opts.priceStyle;
616
+ this.comp = resolveTopbarComposition(opts.composition);
617
+ const vis = (id) => topbarHas(this.comp, id);
604
618
  const doc = host.ownerDocument;
605
619
  injectStyles(STYLE_ID2, CSS2, doc);
606
620
  this.el = doc.createElement("div");
@@ -623,7 +637,7 @@ var Topbar = class {
623
637
  this.styleButton.className = "vela-widget-style";
624
638
  this.renderStyleButton(doc);
625
639
  let indicatorsBtn = null;
626
- if (opts.onIndicatorsClick) {
640
+ if (opts.onIndicatorsClick && !topbarActionOverride("indicators")) {
627
641
  indicatorsBtn = doc.createElement("button");
628
642
  indicatorsBtn.className = "vela-widget-indicators";
629
643
  indicatorsBtn.append(iconEl("indicators", doc), doc.createTextNode("Indicators"));
@@ -636,18 +650,23 @@ var Topbar = class {
636
650
  this.panelsHost = doc.createElement("span");
637
651
  this.panelsHost.className = "vela-widget-panels";
638
652
  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);
653
+ if (vis("undo-redo")) {
654
+ this.undoBtn = tool("vela-widget-undo", "undo", "Undo", opts.onUndoClick);
655
+ this.redoBtn = tool("vela-widget-redo", "redo", "Redo", opts.onRedoClick);
656
+ } else {
657
+ this.undoBtn = doc.createElement("button");
658
+ this.redoBtn = doc.createElement("button");
659
+ }
641
660
  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");
661
+ const screenshotBtn = vis("screenshot") && !topbarActionOverride("screenshot") ? tool("vela-widget-screenshot", "camera", "Download screenshot", opts.onScreenshotClick) : null;
662
+ this.alertsBtn = vis("alerts") ? tool("vela-widget-alerts", "bell", "Alerts") : doc.createElement("button");
644
663
  this.alertsBtn.style.position = "relative";
645
664
  if (opts.onAlertsClick) this.alertsBtn.addEventListener("click", () => opts.onAlertsClick(this.alertsBtn));
646
665
  this.alertsBadge = doc.createElement("span");
647
666
  this.alertsBadge.className = "vela-alerts-badge";
648
667
  this.alertsBadge.style.display = "none";
649
668
  this.alertsBtn.appendChild(this.alertsBadge);
650
- if (opts.layout) {
669
+ if (opts.layout && vis("layout")) {
651
670
  this.layoutId = opts.layout.current;
652
671
  this.layoutButton = doc.createElement("button");
653
672
  this.layoutButton.className = "vela-widget-style";
@@ -658,12 +677,65 @@ var Topbar = class {
658
677
  d.className = "vela-sep";
659
678
  return d;
660
679
  };
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
680
  this.leftActionsSep = sep();
665
681
  this.leftActionsSep.hidden = true;
666
- this.el.append(...leading, this.leftActionsHost, this.leftActionsSep, this.undoBtn, this.redoBtn, this.actionsHost, this.alertsBtn, this.panelsHost, screenshotBtn);
682
+ const primaries = /* @__PURE__ */ new Set(["symbol", "timeframes", "style", "layout", "indicators"]);
683
+ const pinSlot = (id, left) => {
684
+ const slot = doc.createElement("span");
685
+ slot.className = "vela-widget-action-pin";
686
+ this.pinned.set(id, { host: slot, left });
687
+ return [slot];
688
+ };
689
+ const overridden = (id, left) => {
690
+ const ov = topbarActionOverride(id);
691
+ if (!ov) return null;
692
+ const sideDeclared = left ? opts.composition?.left != null : opts.composition?.right != null;
693
+ if (!sideDeclared && ov.order !== void 0) {
694
+ this.flowingOverrides.add(id);
695
+ return [];
696
+ }
697
+ return pinSlot(id, left);
698
+ };
699
+ const elementsFor = (id, left) => {
700
+ switch (id) {
701
+ case "symbol":
702
+ return [this.symbolEl];
703
+ case "timeframes":
704
+ return [tfGroup];
705
+ case "style":
706
+ return [this.styleButton];
707
+ case "layout":
708
+ return this.layoutButton ? [this.layoutButton] : [];
709
+ case "indicators":
710
+ return overridden(id, left) ?? (indicatorsBtn ? [indicatorsBtn] : []);
711
+ case "actions":
712
+ return left ? [this.leftActionsHost, this.leftActionsSep] : [this.actionsHost];
713
+ case "undo-redo":
714
+ return [this.undoBtn, this.redoBtn];
715
+ case "alerts":
716
+ return [this.alertsBtn];
717
+ case "panels":
718
+ return [this.panelsHost];
719
+ case "screenshot":
720
+ return overridden(id, left) ?? (screenshotBtn ? [screenshotBtn] : []);
721
+ default:
722
+ return pinSlot(id, left);
723
+ }
724
+ };
725
+ const sideEls = (list, left) => {
726
+ const out = [];
727
+ for (const [i, id] of list.entries()) {
728
+ const els = elementsFor(id, left);
729
+ if (els.length === 0) continue;
730
+ out.push(...els);
731
+ if (primaries.has(id) && i < list.length - 1) out.push(sep());
732
+ }
733
+ return out;
734
+ };
735
+ const right = doc.createElement("span");
736
+ right.className = "vela-topbar-right";
737
+ right.append(...sideEls(this.comp.right, false));
738
+ this.el.append(...sideEls(this.comp.left, true), right);
667
739
  host.appendChild(this.el);
668
740
  this.renderTfChips();
669
741
  this.onHairlineSync();
@@ -780,14 +852,24 @@ var Topbar = class {
780
852
  else this.styleButton.appendChild(doc.createTextNode(priceStyleLabel(this.priceStyle)));
781
853
  this.styleButton.setAttribute("aria-label", `Chart style \u2014 ${priceStyleLabel(this.priceStyle)}`);
782
854
  }
783
- /** Re-project the contributed topbar actions (call after registrations change). */
855
+ /** Re-project the contributed topbar actions (call after registrations change).
856
+ * An action PINNED by the composition renders into its named slot (list position
857
+ * wins over `align`/`order`); the rest flow into the side's `actions` slot — or
858
+ * not at all when an explicit list omits it (the list is the side's contract). */
784
859
  renderActions() {
785
860
  const ctx = this.opts.getContext?.();
786
861
  this.actionsHost.replaceChildren();
787
862
  this.leftActionsHost.replaceChildren();
863
+ for (const pin of this.pinned.values()) pin.host.replaceChildren();
788
864
  const doc = this.actionsHost.ownerDocument;
865
+ const flowLeft = this.comp.left.includes("actions");
866
+ const flowRight = this.comp.right.includes("actions");
867
+ const builtin = new Set(TOPBAR_BUILTIN_IDS);
789
868
  for (const action of widgetActions("topbar", ctx)) {
790
- const left = action.align === "left";
869
+ const pin = this.pinned.get(action.id);
870
+ if (!pin && builtin.has(action.id) && !this.flowingOverrides.has(action.id)) continue;
871
+ const left = pin ? pin.left : action.align === "left";
872
+ if (!pin && !(left ? flowLeft : flowRight)) continue;
791
873
  const b = doc.createElement("button");
792
874
  b.className = left ? "vela-widget-action-left" : "vela-widget-action";
793
875
  if (action.icon) b.appendChild(iconEl(action.icon, doc));
@@ -796,7 +878,7 @@ var Topbar = class {
796
878
  const c = this.opts.getContext?.();
797
879
  if (c) action.run(c);
798
880
  });
799
- (left ? this.leftActionsHost : this.actionsHost).appendChild(b);
881
+ (pin ? pin.host : left ? this.leftActionsHost : this.actionsHost).appendChild(b);
800
882
  }
801
883
  this.leftActionsSep.hidden = this.leftActionsHost.childElementCount === 0;
802
884
  this.onHairlineSync();
@@ -1113,36 +1195,27 @@ var iconFailed = /* @__PURE__ */ new Set();
1113
1195
  function initialsOf(name) {
1114
1196
  return (name || "?").replace(/[^A-Za-z0-9]/g, "").slice(0, 2).toUpperCase() || "?";
1115
1197
  }
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) {
1198
+ function tickerIconEl(doc, base, name, className, iconUrl) {
1125
1199
  const wrap = doc.createElement("span");
1126
1200
  wrap.className = className;
1127
- const key = base.toUpperCase();
1128
1201
  const fallback = () => {
1129
1202
  wrap.replaceChildren();
1130
1203
  wrap.style.background = categoricalColor(name);
1131
1204
  wrap.textContent = initialsOf(base || name);
1132
1205
  };
1133
- if (!key || iconFailed.has(key)) {
1206
+ if (!iconUrl || iconFailed.has(iconUrl)) {
1134
1207
  fallback();
1135
1208
  return wrap;
1136
1209
  }
1137
1210
  const img = doc.createElement("img");
1138
1211
  img.alt = "";
1139
1212
  img.crossOrigin = "anonymous";
1140
- img.src = cryptoIconUrl(key);
1213
+ img.src = iconUrl;
1141
1214
  img.style.cssText = "width:100%;height:100%;border-radius:50%;display:block;object-fit:cover;";
1142
1215
  img.addEventListener(
1143
1216
  "error",
1144
1217
  () => {
1145
- iconFailed.add(key);
1218
+ iconFailed.add(iconUrl);
1146
1219
  fallback();
1147
1220
  },
1148
1221
  { once: true }
@@ -1553,11 +1626,14 @@ var MenuBuilder = class {
1553
1626
  }
1554
1627
  };
1555
1628
  var ObjectTree = class extends SidePanel {
1556
- constructor(host) {
1629
+ constructor(host, iconFor) {
1557
1630
  super(host, "Object tree", "vela-ot");
1631
+ this.iconFor = iconFor;
1558
1632
  this.chart = null;
1559
1633
  this.selectedDrawing = null;
1560
1634
  this.symbolName = "";
1635
+ /** The raw (possibly venue-prefixed) symbol — what icon resolution routes on. */
1636
+ this.symbolRaw = "";
1561
1637
  /** Drawing bundles — a view-side grouping, held for the panel's lifetime and never persisted.
1562
1638
  * Kept per chart because a workspace points this one panel at whichever chart is active, and
1563
1639
  * each chart's bundles have to survive the switch. */
@@ -1594,6 +1670,7 @@ var ObjectTree = class extends SidePanel {
1594
1670
  if (open) this.refresh();
1595
1671
  }
1596
1672
  setSymbol(symbol) {
1673
+ this.symbolRaw = symbol;
1597
1674
  this.symbolName = parseSymbol(symbol).ticker;
1598
1675
  }
1599
1676
  /** (Re)bind to a chart instance — called after every widget rebuild. */
@@ -1794,7 +1871,7 @@ var ObjectTree = class extends SidePanel {
1794
1871
  const { chart } = pass;
1795
1872
  if (row.kind === "price") {
1796
1873
  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");
1874
+ const icon2 = tickerIconEl(doc, base || "P", this.symbolName || "Price", "vela-ot-avatar", this.symbolRaw ? this.iconFor?.(this.symbolRaw) : void 0);
1798
1875
  const el2 = this.row(icon2, row.label, row.visible, [
1799
1876
  {
1800
1877
  icon: row.visible ? "eye" : "eye-off",
@@ -2826,6 +2903,17 @@ var PanelDock = class {
2826
2903
  };
2827
2904
 
2828
2905
  // src/widget/symbol-picker.ts
2906
+ function dedupeSymbols(list) {
2907
+ const seen = /* @__PURE__ */ new Set();
2908
+ const out = [];
2909
+ for (const s of list) {
2910
+ const key = `${(s.prefix ?? s.provider ?? "").toLowerCase()}:${s.ticker.toUpperCase()}`;
2911
+ if (seen.has(key)) continue;
2912
+ seen.add(key);
2913
+ out.push(s);
2914
+ }
2915
+ return out;
2916
+ }
2829
2917
  var TOP_TICKERS = ["BTCUSDT", "ETHUSDT", "SOLUSDT", "BNBUSDT", "XRPUSDT", "DOGEUSDT", "ADAUSDT", "LINKUSDT"];
2830
2918
  function parseQuery(raw, venues) {
2831
2919
  const m = raw.match(/^\s*([^\s:]+)\s*[:\s]\s*(.*)$/);
@@ -2840,17 +2928,18 @@ function onlyOne(matches) {
2840
2928
  return matches.length === 1 ? matches[0] : null;
2841
2929
  }
2842
2930
  var venueOf = (s) => s.prefix ?? s.provider;
2843
- function filterSymbols(list, query, limit = 100) {
2931
+ function filterSymbols(list, query, limit = 100, top = TOP_TICKERS) {
2844
2932
  const venues = [...new Set(list.flatMap((s) => [venueOf(s)?.toLowerCase(), s.provider?.toLowerCase()]).filter((p) => !!p))];
2845
2933
  const { scope, term } = parseQuery(query, venues);
2846
2934
  const pool = scope ? list.filter((s) => venueOf(s)?.toLowerCase() === scope || s.provider?.toLowerCase() === scope) : list;
2847
2935
  const q = term.toUpperCase();
2848
2936
  if (!q) {
2849
2937
  if (scope) return [...pool].sort((a, b) => a.ticker.localeCompare(b.ticker)).slice(0, limit);
2938
+ if (top === false) return pool.slice(0, limit);
2850
2939
  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);
2940
+ const pinned = top.map((t) => byTicker.get(t)).filter((s) => s !== void 0);
2941
+ const rest = pool.filter((s) => !top.includes(s.ticker.toUpperCase()));
2942
+ return [...pinned, ...rest].slice(0, limit);
2854
2943
  }
2855
2944
  const qLower = term.toLowerCase();
2856
2945
  const prefix = [];
@@ -2960,12 +3049,16 @@ var CSS6 = `
2960
3049
  var PAGE = 100;
2961
3050
  var SymbolPicker = class {
2962
3051
  constructor(opts) {
3052
+ this.opts = opts;
2963
3053
  this.source = () => [];
2964
3054
  this.rows = [];
2965
3055
  this.highlighted = 0;
2966
3056
  this.seed = "";
2967
3057
  this.activeTab = "All";
2968
3058
  this.visible = PAGE;
3059
+ /** The ranked pool cache — `key` fingerprints the raw pool the ranking ran on. */
3060
+ this.ranked = null;
3061
+ this.ranking = false;
2969
3062
  const doc = (opts.host ?? document.body).ownerDocument;
2970
3063
  injectStyles(STYLE_ID6, CSS6, doc);
2971
3064
  this.input = doc.createElement("input");
@@ -3063,10 +3156,34 @@ var SymbolPicker = class {
3063
3156
  } else delete el.dataset.highlighted;
3064
3157
  });
3065
3158
  }
3159
+ /** The picker's pool: the source, shaped by the registered symbol ranking. Cached —
3160
+ * the hook runs when the pool CHANGES (an index lands or refreshes), never per
3161
+ * keystroke; an async hook resolves onto the next repaint (stale-while-revalidate
3162
+ * in between, the raw pool before the first resolve). */
3163
+ pool() {
3164
+ const raw = this.source();
3165
+ const hook = symbolRanking();
3166
+ if (!hook) return raw;
3167
+ const key = `${raw.length}:${raw[0]?.ticker ?? ""}:${raw[raw.length - 1]?.ticker ?? ""}`;
3168
+ if (this.ranked?.key !== key && !this.ranking) {
3169
+ this.ranking = true;
3170
+ Promise.resolve([...raw]).then((copy) => hook(copy)).then((out) => {
3171
+ this.ranked = { key, result: dedupeSymbols(out) };
3172
+ }).catch((err) => {
3173
+ console.warn("[vela] symbol ranking failed \u2014 pool order kept:", err);
3174
+ this.ranked = { key, result: [...raw] };
3175
+ }).finally(() => {
3176
+ this.ranking = false;
3177
+ this.refresh();
3178
+ });
3179
+ }
3180
+ return this.ranked?.result ?? raw;
3181
+ }
3066
3182
  computeRows() {
3067
3183
  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);
3184
+ const all = this.pool();
3185
+ const pool = this.activeTab === "All" ? all : all.filter((s) => TAB_TYPES[this.activeTab]?.includes((s.type ?? "").toLowerCase()) || this.activeTab === "Crypto" && (s.type ?? "").toLowerCase() === "futures");
3186
+ return filterSymbols(pool, this.input.value, this.visible, symbolRanking() ? false : TOP_TICKERS);
3070
3187
  }
3071
3188
  refresh() {
3072
3189
  const doc = this.list.ownerDocument;
@@ -3097,7 +3214,7 @@ var SymbolPicker = class {
3097
3214
  row.dataset.ticker = s.ticker;
3098
3215
  const venue = s.prefix ?? s.provider;
3099
3216
  if (venue) row.dataset.venue = venue;
3100
- const av = tickerIconEl(doc, baseOf(s), s.ticker, "vela-sp-avatar");
3217
+ const av = tickerIconEl(doc, baseOf(s), s.ticker, "vela-sp-avatar", this.opts.iconFor?.(s));
3101
3218
  const main = doc.createElement("span");
3102
3219
  main.className = "vela-sp-main";
3103
3220
  const t = doc.createElement("span");
@@ -3924,8 +4041,9 @@ function statuslineInkOf(renderer, priceStyle) {
3924
4041
  }
3925
4042
  }
3926
4043
  var Statusline = class {
3927
- constructor(host, symbol) {
4044
+ constructor(host, symbol, iconFor) {
3928
4045
  this.host = host;
4046
+ this.iconFor = iconFor;
3929
4047
  this.parts = { name: true, market: true, ohlc: true, change: true };
3930
4048
  this.lastBar = null;
3931
4049
  this.hoverBar = null;
@@ -3949,7 +4067,7 @@ var Statusline = class {
3949
4067
  this.el.className = "vela-statusline";
3950
4068
  this.el.dataset.velaScreenshot = "1";
3951
4069
  const ticker = parseSymbol(symbol).ticker;
3952
- this.avatarEl = tickerIconEl(doc, baseOfTicker(ticker), ticker, "vela-sl-avatar");
4070
+ this.avatarEl = tickerIconEl(doc, baseOfTicker(ticker), ticker, "vela-sl-avatar", this.iconFor?.(symbol));
3953
4071
  this.symbolEl = doc.createElement("span");
3954
4072
  this.symbolEl.className = "vela-sl-symbol";
3955
4073
  this.symbolEl.textContent = ticker;
@@ -3970,7 +4088,7 @@ var Statusline = class {
3970
4088
  setSymbol(symbol) {
3971
4089
  const ticker = parseSymbol(symbol).ticker;
3972
4090
  this.symbolEl.textContent = ticker;
3973
- const fresh = tickerIconEl(this.el.ownerDocument, baseOfTicker(ticker), ticker, "vela-sl-avatar");
4091
+ const fresh = tickerIconEl(this.el.ownerDocument, baseOfTicker(ticker), ticker, "vela-sl-avatar", this.iconFor?.(symbol));
3974
4092
  this.avatarEl.replaceWith(fresh);
3975
4093
  this.avatarEl = fresh;
3976
4094
  this.fit();
@@ -4816,12 +4934,15 @@ var ChartCell = class {
4816
4934
  this.watermarkOn = seed.watermark ?? deps.watermark;
4817
4935
  this.watermark = deps.watermark ? new Watermark(this.host, symbol ?? "", seed.timeframe ?? "60") : null;
4818
4936
  if (!this.watermarkOn) this.watermark?.setVisible(false);
4819
- this.statusline = deps.statusline ? new Statusline(this.host, symbol ?? "") : null;
4937
+ this.statusline = deps.statusline ? new Statusline(this.host, symbol ?? "", (sym) => this.inner?.data.symbolIcon(sym)) : null;
4820
4938
  this.statusline?.setMeta(seed.timeframe ?? "60", this.state.provider ?? "");
4821
4939
  this.statusline?.onChart(this.inner);
4822
4940
  this.marketStatus = this.statusline ? new MarketStatusTracker((s) => this.statusline?.setMarketStatus(s)) : null;
4823
4941
  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 ?? "");
4942
+ if (this.inner && this.state.symbol) {
4943
+ this.statusline?.setSymbol(this.state.symbol);
4944
+ this.statusline?.setMeta(this.state.timeframe ?? "60", this.inner.data.displayPrefix(this.state.symbol) ?? this.state.provider ?? "");
4945
+ }
4825
4946
  this.refreshSessionAvailable();
4826
4947
  if (this.inner && this.state.symbol) this.marketStatus?.track(this.inner.data, this.state.symbol);
4827
4948
  });
@@ -6003,7 +6124,8 @@ var MobileBar = class {
6003
6124
  if (!ctx) return;
6004
6125
  const doc = this.el.ownerDocument;
6005
6126
  this.actionsHost.replaceChildren();
6006
- for (const action of widgetActions("topbar", ctx).filter((a) => a.align === "left")) {
6127
+ const builtin = new Set(TOPBAR_BUILTIN_IDS);
6128
+ for (const action of widgetActions("topbar", ctx).filter((a) => a.align === "left" && !builtin.has(a.id))) {
6007
6129
  const b = doc.createElement("button");
6008
6130
  b.className = "vela-mb-item";
6009
6131
  b.setAttribute("aria-label", action.label);
@@ -6532,10 +6654,10 @@ var MoreDrawer = class {
6532
6654
  });
6533
6655
  actions.appendChild(b);
6534
6656
  };
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);
6657
+ if (this.opts.onUndo) action("undo", "Undo", this.opts.canUndo(), this.opts.onUndo);
6658
+ if (this.opts.onRedo) action("redo", "Redo", this.opts.canRedo(), this.opts.onRedo);
6659
+ if (this.opts.onScreenshot) action("camera", "Screenshot", true, this.opts.onScreenshot);
6660
+ if (actions.childElementCount > 0) this.drawer.body.appendChild(actions);
6539
6661
  const list = doc.createElement("div");
6540
6662
  list.className = "vela-md-list";
6541
6663
  const current = this.opts.priceStyles().find((s) => s.id === this.opts.priceStyle());
@@ -6556,8 +6678,10 @@ var MoreDrawer = class {
6556
6678
  })
6557
6679
  );
6558
6680
  }
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") }));
6681
+ if (this.opts.alerts) {
6682
+ const alertCount = this.opts.alerts().length;
6683
+ list.appendChild(this.row(doc, "Alerts", { icon: "bell", value: alertCount > 0 ? String(alertCount) : void 0, chevron: true, onClick: () => this.show("alerts") }));
6684
+ }
6561
6685
  for (const act of this.opts.actions()) {
6562
6686
  list.appendChild(
6563
6687
  this.row(doc, act.label, {
@@ -6638,7 +6762,7 @@ var MoreDrawer = class {
6638
6762
  this.drawer.body.appendChild(list);
6639
6763
  }
6640
6764
  renderAlerts(doc) {
6641
- const alerts = this.opts.alerts();
6765
+ const alerts = this.opts.alerts?.() ?? [];
6642
6766
  if (alerts.length === 0) {
6643
6767
  const empty = doc.createElement("div");
6644
6768
  empty.className = "vela-md-empty";
@@ -7197,6 +7321,9 @@ var VelaWorkspace = class {
7197
7321
  const optLayout = opts.layout === false || opts.layout === void 0 ? "4" : opts.layout;
7198
7322
  this.def = this.resolveLayout(this.monoLayout ? "1" : boot?.layout && ensureLayout(boot.layout) ? boot.layout : optLayout);
7199
7323
  this.alertCap = Math.max(1, opts.alertCap ?? ALERT_CAP);
7324
+ this.topbarComp = resolveTopbarComposition(opts.topbar);
7325
+ this.indicatorsOverride = topbarActionOverride("indicators");
7326
+ this.screenshotOverride = topbarActionOverride("screenshot");
7200
7327
  if (boot?.trackSizes) for (const [id, ts] of Object.entries(boot.trackSizes)) this.trackSizes.set(id, ts);
7201
7328
  if (boot?.ext) this.extState = { ...boot.ext };
7202
7329
  if (boot?.charts) for (const { id, ...cs } of boot.charts) this.pool.set(id, cs);
@@ -7213,6 +7340,8 @@ var VelaWorkspace = class {
7213
7340
  });
7214
7341
  this.symbolPicker = new SymbolPicker({
7215
7342
  host: this.root,
7343
+ // Row icons come from each descriptor's OWNING provider (resolveSymbolIcon).
7344
+ iconFor: (d) => this.feed.symbolIconOf(d),
7216
7345
  onSelect: (ticker) => this.active.setSymbol(ticker),
7217
7346
  onOpenChange: (open) => {
7218
7347
  if (open) for (const cell of this.cells()) cell.chart.renderer.closeDialogs();
@@ -7220,7 +7349,7 @@ var VelaWorkspace = class {
7220
7349
  }
7221
7350
  });
7222
7351
  this.symbolPicker.setSource(() => this.feed.symbols());
7223
- this.indicatorPicker = opts.indicatorPicker !== false ? new IndicatorPicker({
7352
+ this.indicatorPicker = opts.indicatorPicker !== false && !this.indicatorsOverride && topbarHas(this.topbarComp, "indicators") ? new IndicatorPicker({
7224
7353
  host: this.root,
7225
7354
  library: () => this.active.libraryRows(),
7226
7355
  onChart: () => this.active.onChartRows(),
@@ -7239,6 +7368,9 @@ var VelaWorkspace = class {
7239
7368
  });
7240
7369
  this.topbar = new Topbar(this.root, {
7241
7370
  symbol: "",
7371
+ // RAW option, not the resolved lists — the bar distinguishes a host-declared
7372
+ // side (list is law) from a default one (an override's `order` may flow).
7373
+ composition: opts.topbar,
7242
7374
  onSymbolClick: () => this.symbolPicker.open(),
7243
7375
  ...picker ? { onIndicatorsClick: () => picker.open() } : {},
7244
7376
  onUndoClick: () => this.active.history.undo(),
@@ -7296,7 +7428,7 @@ var VelaWorkspace = class {
7296
7428
  context: () => this.context(),
7297
7429
  changed: () => this.markStateDirty()
7298
7430
  });
7299
- this.objectTree = new ObjectTree(main);
7431
+ this.objectTree = new ObjectTree(main, (sym) => this.feed.symbolIcon(sym));
7300
7432
  this.dataWindow = new DataWindow(main);
7301
7433
  this.dock.addBuiltIn({ id: "dataWindow", title: "Data window", icon: "datawindow", order: 10, panel: this.dataWindow, onChart: (c) => this.dataWindow.onChart(c) });
7302
7434
  this.dock.addBuiltIn({ id: "objects", title: "Object tree", icon: "objects", order: 20, panel: this.objectTree, onChart: (c) => this.objectTree.onChart(c) });
@@ -7368,7 +7500,9 @@ var VelaWorkspace = class {
7368
7500
  timeframe: "60",
7369
7501
  onSymbolClick: () => this.symbolPicker.open(),
7370
7502
  onTimeframeClick: () => this.openTimeframeDrawer(),
7371
- ...picker ? { onIndicatorsClick: () => picker.open() } : {},
7503
+ // Same visibility truth as the desktop bar: composition-hidden
7504
+ // indicators lose their mobile stop too; an override takes it over.
7505
+ ...topbarHas(this.topbarComp, "indicators") && (picker || this.indicatorsOverride) ? { onIndicatorsClick: this.indicatorsOverride ? () => this.runOverride(this.indicatorsOverride) : () => picker.open() } : {},
7372
7506
  getContext: () => this.context(),
7373
7507
  ...this.drawingsEnabled ? { onDrawingsClick: () => this.openDrawingsDrawer() } : {},
7374
7508
  onMoreClick: () => this.openMoreDrawer(),
@@ -8264,27 +8398,32 @@ var VelaWorkspace = class {
8264
8398
  this.drawingsDrawer.open();
8265
8399
  }
8266
8400
  openMoreDrawer() {
8401
+ const has = (id) => topbarHas(this.topbarComp, id);
8267
8402
  this.moreDrawer ?? (this.moreDrawer = new MoreDrawer({
8268
8403
  host: this.root,
8269
- onUndo: () => this.active.history.undo(),
8270
- onRedo: () => this.active.history.redo(),
8271
- onScreenshot: () => this.active.downloadScreenshot(),
8404
+ ...has("undo-redo") ? { onUndo: () => this.active.history.undo(), onRedo: () => this.active.history.redo() } : {},
8405
+ ...has("screenshot") ? { onScreenshot: this.screenshotOverride ? () => this.runOverride(this.screenshotOverride) : () => this.active.downloadScreenshot() } : {},
8272
8406
  canUndo: () => this.active.history.canUndo,
8273
8407
  canRedo: () => this.active.history.canRedo,
8274
8408
  priceStyles: () => priceStyleIds().map((id) => ({ id, label: priceStyleLabel(id), icon: priceStyleIcon(id) })),
8275
8409
  priceStyle: () => this.active.priceStyle,
8276
8410
  onPriceStyle: (id) => this.active.setPriceStyle(id),
8277
- panels: () => [...this.dock.list()],
8411
+ panels: () => has("panels") ? [...this.dock.list()] : [],
8278
8412
  onTogglePanel: (id) => this.dock.toggle(id),
8279
- alerts: () => this.alerts.map((a) => ({ title: `${a.source} \xB7 ${a.title}`, message: a.message, time: a.time })),
8413
+ ...has("alerts") ? { alerts: () => this.alerts.map((a) => ({ title: `${a.source} \xB7 ${a.title}`, message: a.message, time: a.time })) } : {},
8280
8414
  // 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()) })),
8415
+ // lands in the drawer, or every left action would appear twice. Built-in-id
8416
+ // actions are slot OVERRIDES: they reach the drawer through the slot's own
8417
+ // routed button (screenshot) or stop (indicators), never as an extra row.
8418
+ actions: () => {
8419
+ const builtin = new Set(TOPBAR_BUILTIN_IDS);
8420
+ 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()) }));
8421
+ },
8283
8422
  // The desktop layout dropdown's whole surface — the grid canvas, the
8284
8423
  // non-canvas presets and the sync switches — relocated into the kebab
8285
8424
  // 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 : {
8425
+ // single-chart mode and a composition without 'layout' omit it here too.
8426
+ layout: this.monoLayout || !has("layout") ? void 0 : {
8288
8427
  shape: () => layoutShape(this.def),
8289
8428
  presets: () => layouts().filter((l) => layoutShape(l) === null).map((l) => ({ id: l.id, label: l.label, checked: l.id === this.def.id })),
8290
8429
  onSelectGrid: (rows, cols) => this.setLayout(layoutForGrid(rows, cols)),
@@ -8351,9 +8490,23 @@ var VelaWorkspace = class {
8351
8490
  }
8352
8491
  }
8353
8492
  }
8493
+ /** Invoke a slot override the way its button would: fresh context, `when` respected. */
8494
+ runOverride(action) {
8495
+ const ctx = this.context();
8496
+ if (!action.when || action.when(ctx)) action.run(ctx);
8497
+ }
8354
8498
  /** The default shortcut set — every binding acts on the ACTIVE cell. */
8355
8499
  registerDefaultKeys() {
8356
- this.keymap.register({ id: "chart.screenshot", keys: "mod+alt+s", label: "Download a chart screenshot", category: "Chart", run: () => this.active.downloadScreenshot() });
8500
+ if (topbarHas(this.topbarComp, "screenshot")) {
8501
+ const ov = this.screenshotOverride;
8502
+ this.keymap.register({
8503
+ id: "chart.screenshot",
8504
+ keys: "mod+alt+s",
8505
+ label: ov ? ov.label : "Download a chart screenshot",
8506
+ category: "Chart",
8507
+ run: ov ? () => this.runOverride(ov) : () => this.active.downloadScreenshot()
8508
+ });
8509
+ }
8357
8510
  this.keymap.register({ id: "chart.reset-view", keys: "alt+r", label: "Reset view (all history)", category: "Chart", run: () => this.active.chart.setVisibleRangePreset("ALL") });
8358
8511
  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
8512
  this.keymap.register({
@@ -8393,7 +8546,10 @@ var VelaWorkspace = class {
8393
8546
  this.keymap.register({ id: "view.zoom-out", keys: "mod+arrowdown", label: "Zoom out", category: "Chart", run: () => this.glider.zoom(ZOOM_OUT) });
8394
8547
  this.keymap.register({ id: "view.pan-left", keys: "mod+arrowleft", label: "Pan toward history", category: "Chart", run: () => this.active.chart.panBy(-PAN_FAST) });
8395
8548
  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) {
8549
+ const indOv = this.indicatorsOverride;
8550
+ if (indOv && topbarHas(this.topbarComp, "indicators")) {
8551
+ this.keymap.register({ id: "indicators.open", keys: "/", label: indOv.label, category: "Indicators", run: () => this.runOverride(indOv) });
8552
+ } else if (this.indicatorPicker) {
8397
8553
  this.keymap.register({ id: "indicators.open", keys: "/", label: "Open the indicator picker", category: "Indicators", run: () => this.indicatorPicker?.open() });
8398
8554
  }
8399
8555
  this.keymap.register({