@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
@@ -303,6 +303,28 @@ var ProviderRegistry = class {
303
303
  const d = this.entries.get(resolved.provider)?.byTicker?.get(normTicker(resolved.ticker));
304
304
  return `${d?.prefix ?? resolved.provider}:${d?.ticker ?? resolved.ticker}`;
305
305
  }
306
+ /** The icon URL for a DESCRIPTOR — routed to its OWNING provider's resolver (the
307
+ * provider owns the knowledge of where its asset class's icons live). A missing
308
+ * provider, an absent resolver, or a resolver that throws all mean "no icon" —
309
+ * the shells' initials badge takes over, never an error. */
310
+ symbolIconOf(d) {
311
+ const name = d.provider ? normName(d.provider) : [...this.entries.keys()][0];
312
+ const entry = name !== void 0 ? this.entries.get(name) : void 0;
313
+ try {
314
+ return entry?.provider.resolveSymbolIcon?.(d) ?? void 0;
315
+ } catch {
316
+ return void 0;
317
+ }
318
+ }
319
+ /** The icon URL for a raw SYMBOL string (the statusline/object-tree path): resolve
320
+ * to the owning provider, find its descriptor, route to the resolver. Before the
321
+ * index settles (or for a lenient sole-provider resolution) a minimal synthetic
322
+ * descriptor is offered — crypto resolvers can answer from the ticker alone. */
323
+ symbolIcon(resolved) {
324
+ if (!resolved) return void 0;
325
+ const d = this.entries.get(resolved.provider)?.byTicker?.get(normTicker(resolved.ticker));
326
+ return this.symbolIconOf({ ...d ?? { ticker: resolved.ticker }, provider: d?.provider ?? resolved.provider });
327
+ }
306
328
  /** Indexed symbols for one provider (or all, concatenated) — for autocomplete. */
307
329
  symbolsOf(rawName) {
308
330
  if (rawName != null) return this.entries.get(normName(rawName))?.descriptors ?? [];
@@ -655,6 +677,14 @@ var MultiProviderFeed = class {
655
677
  providerInstance(name) {
656
678
  return this.registry.get(name);
657
679
  }
680
+ /** The icon URL for a DESCRIPTOR — its owning provider's `resolveSymbolIcon` (picker rows). */
681
+ symbolIconOf(d) {
682
+ return this.registry.symbolIconOf(d);
683
+ }
684
+ /** The icon URL for a raw SYMBOL string — resolve, then route (statusline, object tree). */
685
+ symbolIcon(raw) {
686
+ return this.registry.symbolIcon(this.resolveSymbol(raw));
687
+ }
658
688
  symbols(name) {
659
689
  return this.registry.symbolsOf(name);
660
690
  }
@@ -4554,6 +4584,29 @@ function tickerModifierIds() {
4554
4584
  return [...registry2.values()].filter((d) => d.tickerModifier ?? d.barTransform != null).map((d) => d.id);
4555
4585
  }
4556
4586
 
4587
+ // src/widget/topbar-composition.ts
4588
+ var TOPBAR_BUILTIN_IDS = ["symbol", "timeframes", "style", "layout", "indicators", "actions", "undo-redo", "alerts", "panels", "screenshot"];
4589
+ var TOPBAR_DEFAULT_LEFT = ["symbol", "timeframes", "style", "layout", "indicators", "actions", "undo-redo"];
4590
+ var TOPBAR_DEFAULT_RIGHT = ["actions", "alerts", "panels", "screenshot"];
4591
+ function resolveTopbarComposition(opt) {
4592
+ const seen = /* @__PURE__ */ new Set();
4593
+ const take = (list) => {
4594
+ const out = [];
4595
+ const local = /* @__PURE__ */ new Set();
4596
+ for (const id of list) {
4597
+ if (!id || local.has(id) || id !== "actions" && seen.has(id)) continue;
4598
+ local.add(id);
4599
+ seen.add(id);
4600
+ out.push(id);
4601
+ }
4602
+ return out;
4603
+ };
4604
+ return { left: take(opt?.left ?? TOPBAR_DEFAULT_LEFT), right: take(opt?.right ?? TOPBAR_DEFAULT_RIGHT) };
4605
+ }
4606
+ function topbarHas(comp, id) {
4607
+ return comp.left.includes(id) || comp.right.includes(id);
4608
+ }
4609
+
4557
4610
  // src/widget/contributions.ts
4558
4611
  var registry3 = /* @__PURE__ */ new Map();
4559
4612
  var attachments = /* @__PURE__ */ new Map();
@@ -4565,6 +4618,13 @@ var DEFAULT_PANEL_ORDER = 100;
4565
4618
  function sidePanels() {
4566
4619
  return [...panels.values()].sort((a, b) => (a.order ?? DEFAULT_PANEL_ORDER) - (b.order ?? DEFAULT_PANEL_ORDER));
4567
4620
  }
4621
+ var OVERRIDABLE_TOPBAR_IDS = ["indicators", "screenshot"];
4622
+ var overridableSet = new Set(OVERRIDABLE_TOPBAR_IDS);
4623
+ new Set(TOPBAR_BUILTIN_IDS);
4624
+ function topbarActionOverride(id) {
4625
+ if (!overridableSet.has(id)) return void 0;
4626
+ return registry3.get(id);
4627
+ }
4568
4628
  function widgetActions(target, ctx) {
4569
4629
  const list = [...registry3.values()].filter((d) => d.target === target);
4570
4630
  list.sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
@@ -5068,9 +5128,14 @@ var CSS3 = `
5068
5128
  align-items: center;
5069
5129
  justify-content: center;
5070
5130
  }
5071
- .vela-widget-actions { margin-left: auto; display: inline-flex; gap: var(--vela-space-1); }
5131
+ /* The right side of the bar \u2014 whatever the composition puts there rides this one
5132
+ auto-margin push (the flow-actions host used to carry it; composition can omit it). */
5133
+ .vela-topbar-right { margin-left: auto; display: inline-flex; align-items: center; gap: var(--vela-space-1); }
5134
+ .vela-widget-actions { display: inline-flex; gap: var(--vela-space-1); }
5072
5135
  /* Left-aligned contributed actions \u2014 the primary-chrome cluster after the dropdowns. */
5073
5136
  .vela-widget-actions-left { display: inline-flex; align-items: center; gap: var(--vela-space-1); }
5137
+ /* One PINNED contributed action's slot (a composition entry naming the action's id). */
5138
+ .vela-widget-action-pin { display: inline-flex; align-items: center; }
5074
5139
  /* The side-panel toggles, one per docked panel \u2014 a group so the dock can rebuild them
5075
5140
  without disturbing the tools around it. */
5076
5141
  .vela-widget-panels { display: inline-flex; align-items: center; gap: var(--vela-space-1); }
@@ -5129,6 +5194,12 @@ var Topbar = class {
5129
5194
  this.tooltips = [];
5130
5195
  this.panelBtns = /* @__PURE__ */ new Map();
5131
5196
  this.panelTooltips = [];
5197
+ /** Pinned contributed-action slots, by action id (composition entries that name one,
5198
+ * plus built-in slots taken over by an override). */
5199
+ this.pinned = /* @__PURE__ */ new Map();
5200
+ /** Overrides that LEFT their native slot (default side + a declared `order`) — they
5201
+ * render through the flow cluster like ordinary actions. */
5202
+ this.flowingOverrides = /* @__PURE__ */ new Set();
5132
5203
  this.onHairlineSync = () => {
5133
5204
  if (this.hairlineRaf) return;
5134
5205
  const win = this.el.ownerDocument.defaultView;
@@ -5143,6 +5214,8 @@ var Topbar = class {
5143
5214
  this.host = host;
5144
5215
  this.timeframe = opts.timeframe;
5145
5216
  this.priceStyle = opts.priceStyle;
5217
+ this.comp = resolveTopbarComposition(opts.composition);
5218
+ const vis = (id) => topbarHas(this.comp, id);
5146
5219
  const doc = host.ownerDocument;
5147
5220
  injectStyles(STYLE_ID2, CSS3, doc);
5148
5221
  this.el = doc.createElement("div");
@@ -5165,7 +5238,7 @@ var Topbar = class {
5165
5238
  this.styleButton.className = "vela-widget-style";
5166
5239
  this.renderStyleButton(doc);
5167
5240
  let indicatorsBtn = null;
5168
- if (opts.onIndicatorsClick) {
5241
+ if (opts.onIndicatorsClick && !topbarActionOverride("indicators")) {
5169
5242
  indicatorsBtn = doc.createElement("button");
5170
5243
  indicatorsBtn.className = "vela-widget-indicators";
5171
5244
  indicatorsBtn.append(iconEl("indicators", doc), doc.createTextNode("Indicators"));
@@ -5178,18 +5251,23 @@ var Topbar = class {
5178
5251
  this.panelsHost = doc.createElement("span");
5179
5252
  this.panelsHost.className = "vela-widget-panels";
5180
5253
  const tool = (cls, icon2, tip, onClick) => this.toolButton(cls, icon2, tip, onClick, this.tooltips);
5181
- this.undoBtn = tool("vela-widget-undo", "undo", "Undo", opts.onUndoClick);
5182
- this.redoBtn = tool("vela-widget-redo", "redo", "Redo", opts.onRedoClick);
5254
+ if (vis("undo-redo")) {
5255
+ this.undoBtn = tool("vela-widget-undo", "undo", "Undo", opts.onUndoClick);
5256
+ this.redoBtn = tool("vela-widget-redo", "redo", "Redo", opts.onRedoClick);
5257
+ } else {
5258
+ this.undoBtn = doc.createElement("button");
5259
+ this.redoBtn = doc.createElement("button");
5260
+ }
5183
5261
  this.setHistoryState(false, false);
5184
- const screenshotBtn = tool("vela-widget-screenshot", "camera", "Download screenshot", opts.onScreenshotClick);
5185
- this.alertsBtn = tool("vela-widget-alerts", "bell", "Alerts");
5262
+ const screenshotBtn = vis("screenshot") && !topbarActionOverride("screenshot") ? tool("vela-widget-screenshot", "camera", "Download screenshot", opts.onScreenshotClick) : null;
5263
+ this.alertsBtn = vis("alerts") ? tool("vela-widget-alerts", "bell", "Alerts") : doc.createElement("button");
5186
5264
  this.alertsBtn.style.position = "relative";
5187
5265
  if (opts.onAlertsClick) this.alertsBtn.addEventListener("click", () => opts.onAlertsClick(this.alertsBtn));
5188
5266
  this.alertsBadge = doc.createElement("span");
5189
5267
  this.alertsBadge.className = "vela-alerts-badge";
5190
5268
  this.alertsBadge.style.display = "none";
5191
5269
  this.alertsBtn.appendChild(this.alertsBadge);
5192
- if (opts.layout) {
5270
+ if (opts.layout && vis("layout")) {
5193
5271
  this.layoutId = opts.layout.current;
5194
5272
  this.layoutButton = doc.createElement("button");
5195
5273
  this.layoutButton.className = "vela-widget-style";
@@ -5200,12 +5278,65 @@ var Topbar = class {
5200
5278
  d.className = "vela-sep";
5201
5279
  return d;
5202
5280
  };
5203
- const leading = [this.symbolEl, sep(), tfGroup, sep(), this.styleButton, sep()];
5204
- if (this.layoutButton) leading.push(this.layoutButton, sep());
5205
- if (indicatorsBtn) leading.push(indicatorsBtn, sep());
5206
5281
  this.leftActionsSep = sep();
5207
5282
  this.leftActionsSep.hidden = true;
5208
- this.el.append(...leading, this.leftActionsHost, this.leftActionsSep, this.undoBtn, this.redoBtn, this.actionsHost, this.alertsBtn, this.panelsHost, screenshotBtn);
5283
+ const primaries = /* @__PURE__ */ new Set(["symbol", "timeframes", "style", "layout", "indicators"]);
5284
+ const pinSlot = (id, left) => {
5285
+ const slot = doc.createElement("span");
5286
+ slot.className = "vela-widget-action-pin";
5287
+ this.pinned.set(id, { host: slot, left });
5288
+ return [slot];
5289
+ };
5290
+ const overridden = (id, left) => {
5291
+ const ov = topbarActionOverride(id);
5292
+ if (!ov) return null;
5293
+ const sideDeclared = left ? opts.composition?.left != null : opts.composition?.right != null;
5294
+ if (!sideDeclared && ov.order !== void 0) {
5295
+ this.flowingOverrides.add(id);
5296
+ return [];
5297
+ }
5298
+ return pinSlot(id, left);
5299
+ };
5300
+ const elementsFor = (id, left) => {
5301
+ switch (id) {
5302
+ case "symbol":
5303
+ return [this.symbolEl];
5304
+ case "timeframes":
5305
+ return [tfGroup];
5306
+ case "style":
5307
+ return [this.styleButton];
5308
+ case "layout":
5309
+ return this.layoutButton ? [this.layoutButton] : [];
5310
+ case "indicators":
5311
+ return overridden(id, left) ?? (indicatorsBtn ? [indicatorsBtn] : []);
5312
+ case "actions":
5313
+ return left ? [this.leftActionsHost, this.leftActionsSep] : [this.actionsHost];
5314
+ case "undo-redo":
5315
+ return [this.undoBtn, this.redoBtn];
5316
+ case "alerts":
5317
+ return [this.alertsBtn];
5318
+ case "panels":
5319
+ return [this.panelsHost];
5320
+ case "screenshot":
5321
+ return overridden(id, left) ?? (screenshotBtn ? [screenshotBtn] : []);
5322
+ default:
5323
+ return pinSlot(id, left);
5324
+ }
5325
+ };
5326
+ const sideEls = (list, left) => {
5327
+ const out = [];
5328
+ for (const [i, id] of list.entries()) {
5329
+ const els = elementsFor(id, left);
5330
+ if (els.length === 0) continue;
5331
+ out.push(...els);
5332
+ if (primaries.has(id) && i < list.length - 1) out.push(sep());
5333
+ }
5334
+ return out;
5335
+ };
5336
+ const right = doc.createElement("span");
5337
+ right.className = "vela-topbar-right";
5338
+ right.append(...sideEls(this.comp.right, false));
5339
+ this.el.append(...sideEls(this.comp.left, true), right);
5209
5340
  host.appendChild(this.el);
5210
5341
  this.renderTfChips();
5211
5342
  this.onHairlineSync();
@@ -5322,14 +5453,24 @@ var Topbar = class {
5322
5453
  else this.styleButton.appendChild(doc.createTextNode(priceStyleLabel(this.priceStyle)));
5323
5454
  this.styleButton.setAttribute("aria-label", `Chart style \u2014 ${priceStyleLabel(this.priceStyle)}`);
5324
5455
  }
5325
- /** Re-project the contributed topbar actions (call after registrations change). */
5456
+ /** Re-project the contributed topbar actions (call after registrations change).
5457
+ * An action PINNED by the composition renders into its named slot (list position
5458
+ * wins over `align`/`order`); the rest flow into the side's `actions` slot — or
5459
+ * not at all when an explicit list omits it (the list is the side's contract). */
5326
5460
  renderActions() {
5327
5461
  const ctx = this.opts.getContext?.();
5328
5462
  this.actionsHost.replaceChildren();
5329
5463
  this.leftActionsHost.replaceChildren();
5464
+ for (const pin of this.pinned.values()) pin.host.replaceChildren();
5330
5465
  const doc = this.actionsHost.ownerDocument;
5466
+ const flowLeft = this.comp.left.includes("actions");
5467
+ const flowRight = this.comp.right.includes("actions");
5468
+ const builtin = new Set(TOPBAR_BUILTIN_IDS);
5331
5469
  for (const action of widgetActions("topbar", ctx)) {
5332
- const left = action.align === "left";
5470
+ const pin = this.pinned.get(action.id);
5471
+ if (!pin && builtin.has(action.id) && !this.flowingOverrides.has(action.id)) continue;
5472
+ const left = pin ? pin.left : action.align === "left";
5473
+ if (!pin && !(left ? flowLeft : flowRight)) continue;
5333
5474
  const b = doc.createElement("button");
5334
5475
  b.className = left ? "vela-widget-action-left" : "vela-widget-action";
5335
5476
  if (action.icon) b.appendChild(iconEl(action.icon, doc));
@@ -5338,7 +5479,7 @@ var Topbar = class {
5338
5479
  const c = this.opts.getContext?.();
5339
5480
  if (c) action.run(c);
5340
5481
  });
5341
- (left ? this.leftActionsHost : this.actionsHost).appendChild(b);
5482
+ (pin ? pin.host : left ? this.leftActionsHost : this.actionsHost).appendChild(b);
5342
5483
  }
5343
5484
  this.leftActionsSep.hidden = this.leftActionsHost.childElementCount === 0;
5344
5485
  this.onHairlineSync();
@@ -5731,41 +5872,39 @@ var Bottombar = class {
5731
5872
  }
5732
5873
  };
5733
5874
 
5734
- // src/widget/symbol-icon.ts
5735
- var iconFailed = /* @__PURE__ */ new Set();
5736
- function initialsOf(name) {
5737
- return (name || "?").replace(/[^A-Za-z0-9]/g, "").slice(0, 2).toUpperCase() || "?";
5738
- }
5739
- function cryptoIconUrl(base) {
5740
- return `https://crypto-icons.ledger.com/${encodeURIComponent(base.toUpperCase())}.png`;
5741
- }
5875
+ // src/data/symbol-base.ts
5742
5876
  function baseOf(d) {
5743
5877
  const fromDesc = d.description?.split("/")[0]?.trim();
5744
5878
  if (fromDesc) return fromDesc.replace(/\s+Perpetual$/i, "");
5745
5879
  return d.ticker.replace(/[-_/]?(USDT|USDC|USD1|USDS|BUSD|USD|EUR|PERP)$/i, "") || d.ticker;
5746
5880
  }
5747
- function tickerIconEl(doc, base, name, className) {
5881
+
5882
+ // src/widget/symbol-icon.ts
5883
+ var iconFailed = /* @__PURE__ */ new Set();
5884
+ function initialsOf(name) {
5885
+ return (name || "?").replace(/[^A-Za-z0-9]/g, "").slice(0, 2).toUpperCase() || "?";
5886
+ }
5887
+ function tickerIconEl(doc, base, name, className, iconUrl) {
5748
5888
  const wrap = doc.createElement("span");
5749
5889
  wrap.className = className;
5750
- const key = base.toUpperCase();
5751
5890
  const fallback = () => {
5752
5891
  wrap.replaceChildren();
5753
5892
  wrap.style.background = categoricalColor(name);
5754
5893
  wrap.textContent = initialsOf(base || name);
5755
5894
  };
5756
- if (!key || iconFailed.has(key)) {
5895
+ if (!iconUrl || iconFailed.has(iconUrl)) {
5757
5896
  fallback();
5758
5897
  return wrap;
5759
5898
  }
5760
5899
  const img = doc.createElement("img");
5761
5900
  img.alt = "";
5762
5901
  img.crossOrigin = "anonymous";
5763
- img.src = cryptoIconUrl(key);
5902
+ img.src = iconUrl;
5764
5903
  img.style.cssText = "width:100%;height:100%;border-radius:50%;display:block;object-fit:cover;";
5765
5904
  img.addEventListener(
5766
5905
  "error",
5767
5906
  () => {
5768
- iconFailed.add(key);
5907
+ iconFailed.add(iconUrl);
5769
5908
  fallback();
5770
5909
  },
5771
5910
  { once: true }
@@ -12018,11 +12157,14 @@ var MenuBuilder = class {
12018
12157
  }
12019
12158
  };
12020
12159
  var ObjectTree = class extends SidePanel {
12021
- constructor(host) {
12160
+ constructor(host, iconFor) {
12022
12161
  super(host, "Object tree", "vela-ot");
12162
+ this.iconFor = iconFor;
12023
12163
  this.chart = null;
12024
12164
  this.selectedDrawing = null;
12025
12165
  this.symbolName = "";
12166
+ /** The raw (possibly venue-prefixed) symbol — what icon resolution routes on. */
12167
+ this.symbolRaw = "";
12026
12168
  /** Drawing bundles — a view-side grouping, held for the panel's lifetime and never persisted.
12027
12169
  * Kept per chart because a workspace points this one panel at whichever chart is active, and
12028
12170
  * each chart's bundles have to survive the switch. */
@@ -12059,6 +12201,7 @@ var ObjectTree = class extends SidePanel {
12059
12201
  if (open2) this.refresh();
12060
12202
  }
12061
12203
  setSymbol(symbol) {
12204
+ this.symbolRaw = symbol;
12062
12205
  this.symbolName = parseSymbol(symbol).ticker;
12063
12206
  }
12064
12207
  /** (Re)bind to a chart instance — called after every widget rebuild. */
@@ -12259,7 +12402,7 @@ var ObjectTree = class extends SidePanel {
12259
12402
  const { chart } = pass;
12260
12403
  if (row.kind === "price") {
12261
12404
  const base = this.symbolName.replace(/[-_/]?(USDT|USDC|USD1|USDS|BUSD|USD|EUR|PERP)$/i, "") || this.symbolName;
12262
- const icon2 = tickerIconEl(doc, base || "P", this.symbolName || "Price", "vela-ot-avatar");
12405
+ const icon2 = tickerIconEl(doc, base || "P", this.symbolName || "Price", "vela-ot-avatar", this.symbolRaw ? this.iconFor?.(this.symbolRaw) : void 0);
12263
12406
  const el2 = this.row(icon2, row.label, row.visible, [
12264
12407
  {
12265
12408
  icon: row.visible ? "eye" : "eye-off",
@@ -13289,8 +13432,6 @@ var PanelDock = class {
13289
13432
  for (const entry of this.entries) this.deps.chrome.setPanelActive(entry.id, entry.panel.open);
13290
13433
  }
13291
13434
  };
13292
-
13293
- // src/widget/symbol-picker.ts
13294
13435
  var TOP_TICKERS = ["BTCUSDT", "ETHUSDT", "SOLUSDT", "BNBUSDT", "XRPUSDT", "DOGEUSDT", "ADAUSDT", "LINKUSDT"];
13295
13436
  function parseQuery(raw, venues) {
13296
13437
  const m = raw.match(/^\s*([^\s:]+)\s*[:\s]\s*(.*)$/);
@@ -13305,17 +13446,18 @@ function onlyOne(matches) {
13305
13446
  return matches.length === 1 ? matches[0] : null;
13306
13447
  }
13307
13448
  var venueOf = (s) => s.prefix ?? s.provider;
13308
- function filterSymbols(list, query, limit = 100) {
13449
+ function filterSymbols(list, query, limit = 100, top = TOP_TICKERS) {
13309
13450
  const venues = [...new Set(list.flatMap((s) => [venueOf(s)?.toLowerCase(), s.provider?.toLowerCase()]).filter((p) => !!p))];
13310
13451
  const { scope, term } = parseQuery(query, venues);
13311
13452
  const pool = scope ? list.filter((s) => venueOf(s)?.toLowerCase() === scope || s.provider?.toLowerCase() === scope) : list;
13312
13453
  const q = term.toUpperCase();
13313
13454
  if (!q) {
13314
13455
  if (scope) return [...pool].sort((a, b) => a.ticker.localeCompare(b.ticker)).slice(0, limit);
13456
+ if (top === false) return pool.slice(0, limit);
13315
13457
  const byTicker = new Map(pool.map((s) => [s.ticker.toUpperCase(), s]));
13316
- const top = TOP_TICKERS.map((t) => byTicker.get(t)).filter((s) => s !== void 0);
13317
- const rest = pool.filter((s) => !TOP_TICKERS.includes(s.ticker.toUpperCase()));
13318
- return [...top, ...rest].slice(0, limit);
13458
+ const pinned = top.map((t) => byTicker.get(t)).filter((s) => s !== void 0);
13459
+ const rest = pool.filter((s) => !top.includes(s.ticker.toUpperCase()));
13460
+ return [...pinned, ...rest].slice(0, limit);
13319
13461
  }
13320
13462
  const qLower = term.toLowerCase();
13321
13463
  const prefix = [];
@@ -13425,12 +13567,16 @@ var CSS8 = `
13425
13567
  var PAGE = 100;
13426
13568
  var SymbolPicker = class {
13427
13569
  constructor(opts) {
13570
+ this.opts = opts;
13428
13571
  this.source = () => [];
13429
13572
  this.rows = [];
13430
13573
  this.highlighted = 0;
13431
13574
  this.seed = "";
13432
13575
  this.activeTab = "All";
13433
13576
  this.visible = PAGE;
13577
+ /** The ranked pool cache — `key` fingerprints the raw pool the ranking ran on. */
13578
+ this.ranked = null;
13579
+ this.ranking = false;
13434
13580
  const doc = (opts.host ?? document.body).ownerDocument;
13435
13581
  injectStyles(STYLE_ID7, CSS8, doc);
13436
13582
  this.input = doc.createElement("input");
@@ -13528,10 +13674,19 @@ var SymbolPicker = class {
13528
13674
  } else delete el.dataset.highlighted;
13529
13675
  });
13530
13676
  }
13677
+ /** The picker's pool: the source, shaped by the registered symbol ranking. Cached —
13678
+ * the hook runs when the pool CHANGES (an index lands or refreshes), never per
13679
+ * keystroke; an async hook resolves onto the next repaint (stale-while-revalidate
13680
+ * in between, the raw pool before the first resolve). */
13681
+ pool() {
13682
+ const raw = this.source();
13683
+ return raw;
13684
+ }
13531
13685
  computeRows() {
13532
13686
  const TAB_TYPES = { Crypto: ["crypto"], Stocks: ["stock"], ETFs: ["etf"], Forex: ["forex"], Commodities: ["commodity"] };
13533
- 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");
13534
- return filterSymbols(pool, this.input.value, this.visible);
13687
+ const all = this.pool();
13688
+ const pool = this.activeTab === "All" ? all : all.filter((s) => TAB_TYPES[this.activeTab]?.includes((s.type ?? "").toLowerCase()) || this.activeTab === "Crypto" && (s.type ?? "").toLowerCase() === "futures");
13689
+ return filterSymbols(pool, this.input.value, this.visible, TOP_TICKERS);
13535
13690
  }
13536
13691
  refresh() {
13537
13692
  const doc = this.list.ownerDocument;
@@ -13562,7 +13717,7 @@ var SymbolPicker = class {
13562
13717
  row.dataset.ticker = s.ticker;
13563
13718
  const venue = s.prefix ?? s.provider;
13564
13719
  if (venue) row.dataset.venue = venue;
13565
- const av = tickerIconEl(doc, baseOf(s), s.ticker, "vela-sp-avatar");
13720
+ const av = tickerIconEl(doc, baseOf(s), s.ticker, "vela-sp-avatar", this.opts.iconFor?.(s));
13566
13721
  const main = doc.createElement("span");
13567
13722
  main.className = "vela-sp-main";
13568
13723
  const t = doc.createElement("span");
@@ -14243,7 +14398,8 @@ var MobileBar = class {
14243
14398
  if (!ctx) return;
14244
14399
  const doc = this.el.ownerDocument;
14245
14400
  this.actionsHost.replaceChildren();
14246
- for (const action of widgetActions("topbar", ctx).filter((a) => a.align === "left")) {
14401
+ const builtin = new Set(TOPBAR_BUILTIN_IDS);
14402
+ for (const action of widgetActions("topbar", ctx).filter((a) => a.align === "left" && !builtin.has(a.id))) {
14247
14403
  const b = doc.createElement("button");
14248
14404
  b.className = "vela-mb-item";
14249
14405
  b.setAttribute("aria-label", action.label);
@@ -14772,10 +14928,10 @@ var MoreDrawer = class {
14772
14928
  });
14773
14929
  actions.appendChild(b);
14774
14930
  };
14775
- action("undo", "Undo", this.opts.canUndo(), this.opts.onUndo);
14776
- action("redo", "Redo", this.opts.canRedo(), this.opts.onRedo);
14777
- action("camera", "Screenshot", true, this.opts.onScreenshot);
14778
- this.drawer.body.appendChild(actions);
14931
+ if (this.opts.onUndo) action("undo", "Undo", this.opts.canUndo(), this.opts.onUndo);
14932
+ if (this.opts.onRedo) action("redo", "Redo", this.opts.canRedo(), this.opts.onRedo);
14933
+ if (this.opts.onScreenshot) action("camera", "Screenshot", true, this.opts.onScreenshot);
14934
+ if (actions.childElementCount > 0) this.drawer.body.appendChild(actions);
14779
14935
  const list = doc.createElement("div");
14780
14936
  list.className = "vela-md-list";
14781
14937
  const current = this.opts.priceStyles().find((s) => s.id === this.opts.priceStyle());
@@ -14796,8 +14952,10 @@ var MoreDrawer = class {
14796
14952
  })
14797
14953
  );
14798
14954
  }
14799
- const alertCount = this.opts.alerts().length;
14800
- list.appendChild(this.row(doc, "Alerts", { icon: "bell", value: alertCount > 0 ? String(alertCount) : void 0, chevron: true, onClick: () => this.show("alerts") }));
14955
+ if (this.opts.alerts) {
14956
+ const alertCount = this.opts.alerts().length;
14957
+ list.appendChild(this.row(doc, "Alerts", { icon: "bell", value: alertCount > 0 ? String(alertCount) : void 0, chevron: true, onClick: () => this.show("alerts") }));
14958
+ }
14801
14959
  for (const act of this.opts.actions()) {
14802
14960
  list.appendChild(
14803
14961
  this.row(doc, act.label, {
@@ -14878,7 +15036,7 @@ var MoreDrawer = class {
14878
15036
  this.drawer.body.appendChild(list);
14879
15037
  }
14880
15038
  renderAlerts(doc) {
14881
- const alerts = this.opts.alerts();
15039
+ const alerts = this.opts.alerts?.() ?? [];
14882
15040
  if (alerts.length === 0) {
14883
15041
  const empty = doc.createElement("div");
14884
15042
  empty.className = "vela-md-empty";
@@ -19277,6 +19435,12 @@ var DataControl = class {
19277
19435
  symbols(provider) {
19278
19436
  return this.registry?.symbols(provider) ?? [];
19279
19437
  }
19438
+ /** The icon URL for `symbol` — its owning provider's `resolveSymbolIcon`, routed
19439
+ * through resolution. Undefined while unresolvable, when the provider declares no
19440
+ * resolver, or on a custom `deps.dataFeed` — the shells then show initials. */
19441
+ symbolIcon(symbol) {
19442
+ return this.registry?.symbolIcon(symbol);
19443
+ }
19280
19444
  /** Per-symbol metadata (Pine `syminfo.*`), resolved through the owning provider. */
19281
19445
  symbolInfo(symbol) {
19282
19446
  return this.registry?.symbolInfoFor(symbol) ?? Promise.resolve(void 0);
@@ -35625,8 +35789,9 @@ function statuslineInkOf(renderer, priceStyle) {
35625
35789
  }
35626
35790
  }
35627
35791
  var Statusline = class {
35628
- constructor(host, symbol) {
35792
+ constructor(host, symbol, iconFor) {
35629
35793
  this.host = host;
35794
+ this.iconFor = iconFor;
35630
35795
  this.parts = { name: true, market: true, ohlc: true, change: true };
35631
35796
  this.lastBar = null;
35632
35797
  this.hoverBar = null;
@@ -35650,7 +35815,7 @@ var Statusline = class {
35650
35815
  this.el.className = "vela-statusline";
35651
35816
  this.el.dataset.velaScreenshot = "1";
35652
35817
  const ticker = parseSymbol(symbol).ticker;
35653
- this.avatarEl = tickerIconEl(doc, baseOfTicker(ticker), ticker, "vela-sl-avatar");
35818
+ this.avatarEl = tickerIconEl(doc, baseOfTicker(ticker), ticker, "vela-sl-avatar", this.iconFor?.(symbol));
35654
35819
  this.symbolEl = doc.createElement("span");
35655
35820
  this.symbolEl.className = "vela-sl-symbol";
35656
35821
  this.symbolEl.textContent = ticker;
@@ -35671,7 +35836,7 @@ var Statusline = class {
35671
35836
  setSymbol(symbol) {
35672
35837
  const ticker = parseSymbol(symbol).ticker;
35673
35838
  this.symbolEl.textContent = ticker;
35674
- const fresh = tickerIconEl(this.el.ownerDocument, baseOfTicker(ticker), ticker, "vela-sl-avatar");
35839
+ const fresh = tickerIconEl(this.el.ownerDocument, baseOfTicker(ticker), ticker, "vela-sl-avatar", this.iconFor?.(symbol));
35675
35840
  this.avatarEl.replaceWith(fresh);
35676
35841
  this.avatarEl = fresh;
35677
35842
  this.fit();
@@ -36419,12 +36584,15 @@ var ChartCell = class {
36419
36584
  this.watermarkOn = seed.watermark ?? deps.watermark;
36420
36585
  this.watermark = deps.watermark ? new Watermark(this.host, symbol ?? "", seed.timeframe ?? "60") : null;
36421
36586
  if (!this.watermarkOn) this.watermark?.setVisible(false);
36422
- this.statusline = deps.statusline ? new Statusline(this.host, symbol ?? "") : null;
36587
+ this.statusline = deps.statusline ? new Statusline(this.host, symbol ?? "", (sym) => this.inner?.data.symbolIcon(sym)) : null;
36423
36588
  this.statusline?.setMeta(seed.timeframe ?? "60", this.state.provider ?? "");
36424
36589
  this.statusline?.onChart(this.inner);
36425
36590
  this.marketStatus = this.statusline ? new MarketStatusTracker((s) => this.statusline?.setMarketStatus(s)) : null;
36426
36591
  void this.inner.data.ready().then(() => {
36427
- if (this.inner && this.state.symbol) this.statusline?.setMeta(this.state.timeframe ?? "60", this.inner.data.displayPrefix(this.state.symbol) ?? this.state.provider ?? "");
36592
+ if (this.inner && this.state.symbol) {
36593
+ this.statusline?.setSymbol(this.state.symbol);
36594
+ this.statusline?.setMeta(this.state.timeframe ?? "60", this.inner.data.displayPrefix(this.state.symbol) ?? this.state.provider ?? "");
36595
+ }
36428
36596
  this.refreshSessionAvailable();
36429
36597
  if (this.inner && this.state.symbol) this.marketStatus?.track(this.inner.data, this.state.symbol);
36430
36598
  });
@@ -37513,6 +37681,9 @@ var VelaWorkspace = class {
37513
37681
  const optLayout = opts.layout === false || opts.layout === void 0 ? "4" : opts.layout;
37514
37682
  this.def = this.resolveLayout(this.monoLayout ? "1" : boot?.layout && ensureLayout(boot.layout) ? boot.layout : optLayout);
37515
37683
  this.alertCap = Math.max(1, opts.alertCap ?? ALERT_CAP);
37684
+ this.topbarComp = resolveTopbarComposition(opts.topbar);
37685
+ this.indicatorsOverride = topbarActionOverride("indicators");
37686
+ this.screenshotOverride = topbarActionOverride("screenshot");
37516
37687
  if (boot?.trackSizes) for (const [id, ts] of Object.entries(boot.trackSizes)) this.trackSizes.set(id, ts);
37517
37688
  if (boot?.ext) this.extState = { ...boot.ext };
37518
37689
  if (boot?.charts) for (const { id, ...cs } of boot.charts) this.pool.set(id, cs);
@@ -37529,6 +37700,8 @@ var VelaWorkspace = class {
37529
37700
  });
37530
37701
  this.symbolPicker = new SymbolPicker({
37531
37702
  host: this.root,
37703
+ // Row icons come from each descriptor's OWNING provider (resolveSymbolIcon).
37704
+ iconFor: (d) => this.feed.symbolIconOf(d),
37532
37705
  onSelect: (ticker) => this.active.setSymbol(ticker),
37533
37706
  onOpenChange: (open2) => {
37534
37707
  if (open2) for (const cell of this.cells()) cell.chart.renderer.closeDialogs();
@@ -37536,7 +37709,7 @@ var VelaWorkspace = class {
37536
37709
  }
37537
37710
  });
37538
37711
  this.symbolPicker.setSource(() => this.feed.symbols());
37539
- this.indicatorPicker = opts.indicatorPicker !== false ? new IndicatorPicker({
37712
+ this.indicatorPicker = opts.indicatorPicker !== false && !this.indicatorsOverride && topbarHas(this.topbarComp, "indicators") ? new IndicatorPicker({
37540
37713
  host: this.root,
37541
37714
  library: () => this.active.libraryRows(),
37542
37715
  onChart: () => this.active.onChartRows(),
@@ -37555,6 +37728,9 @@ var VelaWorkspace = class {
37555
37728
  });
37556
37729
  this.topbar = new Topbar(this.root, {
37557
37730
  symbol: "",
37731
+ // RAW option, not the resolved lists — the bar distinguishes a host-declared
37732
+ // side (list is law) from a default one (an override's `order` may flow).
37733
+ composition: opts.topbar,
37558
37734
  onSymbolClick: () => this.symbolPicker.open(),
37559
37735
  ...picker ? { onIndicatorsClick: () => picker.open() } : {},
37560
37736
  onUndoClick: () => this.active.history.undo(),
@@ -37612,7 +37788,7 @@ var VelaWorkspace = class {
37612
37788
  context: () => this.context(),
37613
37789
  changed: () => this.markStateDirty()
37614
37790
  });
37615
- this.objectTree = new ObjectTree(main);
37791
+ this.objectTree = new ObjectTree(main, (sym) => this.feed.symbolIcon(sym));
37616
37792
  this.dataWindow = new DataWindow(main);
37617
37793
  this.dock.addBuiltIn({ id: "dataWindow", title: "Data window", icon: "datawindow", order: 10, panel: this.dataWindow, onChart: (c) => this.dataWindow.onChart(c) });
37618
37794
  this.dock.addBuiltIn({ id: "objects", title: "Object tree", icon: "objects", order: 20, panel: this.objectTree, onChart: (c) => this.objectTree.onChart(c) });
@@ -37684,7 +37860,9 @@ var VelaWorkspace = class {
37684
37860
  timeframe: "60",
37685
37861
  onSymbolClick: () => this.symbolPicker.open(),
37686
37862
  onTimeframeClick: () => this.openTimeframeDrawer(),
37687
- ...picker ? { onIndicatorsClick: () => picker.open() } : {},
37863
+ // Same visibility truth as the desktop bar: composition-hidden
37864
+ // indicators lose their mobile stop too; an override takes it over.
37865
+ ...topbarHas(this.topbarComp, "indicators") && (picker || this.indicatorsOverride) ? { onIndicatorsClick: this.indicatorsOverride ? () => this.runOverride(this.indicatorsOverride) : () => picker.open() } : {},
37688
37866
  getContext: () => this.context(),
37689
37867
  ...this.drawingsEnabled ? { onDrawingsClick: () => this.openDrawingsDrawer() } : {},
37690
37868
  onMoreClick: () => this.openMoreDrawer(),
@@ -38580,27 +38758,32 @@ var VelaWorkspace = class {
38580
38758
  this.drawingsDrawer.open();
38581
38759
  }
38582
38760
  openMoreDrawer() {
38761
+ const has = (id) => topbarHas(this.topbarComp, id);
38583
38762
  this.moreDrawer ?? (this.moreDrawer = new MoreDrawer({
38584
38763
  host: this.root,
38585
- onUndo: () => this.active.history.undo(),
38586
- onRedo: () => this.active.history.redo(),
38587
- onScreenshot: () => this.active.downloadScreenshot(),
38764
+ ...has("undo-redo") ? { onUndo: () => this.active.history.undo(), onRedo: () => this.active.history.redo() } : {},
38765
+ ...has("screenshot") ? { onScreenshot: this.screenshotOverride ? () => this.runOverride(this.screenshotOverride) : () => this.active.downloadScreenshot() } : {},
38588
38766
  canUndo: () => this.active.history.canUndo,
38589
38767
  canRedo: () => this.active.history.canRedo,
38590
38768
  priceStyles: () => priceStyleIds().map((id) => ({ id, label: priceStyleLabel(id), icon: priceStyleIcon(id) })),
38591
38769
  priceStyle: () => this.active.priceStyle,
38592
38770
  onPriceStyle: (id) => this.active.setPriceStyle(id),
38593
- panels: () => [...this.dock.list()],
38771
+ panels: () => has("panels") ? [...this.dock.list()] : [],
38594
38772
  onTogglePanel: (id) => this.dock.toggle(id),
38595
- alerts: () => this.alerts.map((a) => ({ title: `${a.source} \xB7 ${a.title}`, message: a.message, time: a.time })),
38773
+ ...has("alerts") ? { alerts: () => this.alerts.map((a) => ({ title: `${a.source} \xB7 ${a.title}`, message: a.message, time: a.time })) } : {},
38596
38774
  // Left-aligned actions have their own bottom-bar stop — only the rest
38597
- // lands in the drawer, or every left action would appear twice.
38598
- actions: () => widgetActions("topbar", this.context()).filter((a) => a.align !== "left").map((a) => ({ label: a.label, icon: a.icon, run: () => a.run(this.context()) })),
38775
+ // lands in the drawer, or every left action would appear twice. Built-in-id
38776
+ // actions are slot OVERRIDES: they reach the drawer through the slot's own
38777
+ // routed button (screenshot) or stop (indicators), never as an extra row.
38778
+ actions: () => {
38779
+ const builtin = new Set(TOPBAR_BUILTIN_IDS);
38780
+ 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()) }));
38781
+ },
38599
38782
  // The desktop layout dropdown's whole surface — the grid canvas, the
38600
38783
  // non-canvas presets and the sync switches — relocated into the kebab
38601
38784
  // drawer (the topbar is hidden on mobile). Same reads as the topbar block;
38602
- // single-chart mode omits it here too.
38603
- layout: this.monoLayout ? void 0 : {
38785
+ // single-chart mode and a composition without 'layout' omit it here too.
38786
+ layout: this.monoLayout || !has("layout") ? void 0 : {
38604
38787
  shape: () => layoutShape(this.def),
38605
38788
  presets: () => layouts().filter((l) => layoutShape(l) === null).map((l) => ({ id: l.id, label: l.label, checked: l.id === this.def.id })),
38606
38789
  onSelectGrid: (rows, cols) => this.setLayout(layoutForGrid(rows, cols)),
@@ -38667,9 +38850,23 @@ var VelaWorkspace = class {
38667
38850
  }
38668
38851
  }
38669
38852
  }
38853
+ /** Invoke a slot override the way its button would: fresh context, `when` respected. */
38854
+ runOverride(action) {
38855
+ const ctx = this.context();
38856
+ if (!action.when || action.when(ctx)) action.run(ctx);
38857
+ }
38670
38858
  /** The default shortcut set — every binding acts on the ACTIVE cell. */
38671
38859
  registerDefaultKeys() {
38672
- this.keymap.register({ id: "chart.screenshot", keys: "mod+alt+s", label: "Download a chart screenshot", category: "Chart", run: () => this.active.downloadScreenshot() });
38860
+ if (topbarHas(this.topbarComp, "screenshot")) {
38861
+ const ov = this.screenshotOverride;
38862
+ this.keymap.register({
38863
+ id: "chart.screenshot",
38864
+ keys: "mod+alt+s",
38865
+ label: ov ? ov.label : "Download a chart screenshot",
38866
+ category: "Chart",
38867
+ run: ov ? () => this.runOverride(ov) : () => this.active.downloadScreenshot()
38868
+ });
38869
+ }
38673
38870
  this.keymap.register({ id: "chart.reset-view", keys: "alt+r", label: "Reset view (all history)", category: "Chart", run: () => this.active.chart.setVisibleRangePreset("ALL") });
38674
38871
  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")) });
38675
38872
  this.keymap.register({
@@ -38709,7 +38906,10 @@ var VelaWorkspace = class {
38709
38906
  this.keymap.register({ id: "view.zoom-out", keys: "mod+arrowdown", label: "Zoom out", category: "Chart", run: () => this.glider.zoom(ZOOM_OUT) });
38710
38907
  this.keymap.register({ id: "view.pan-left", keys: "mod+arrowleft", label: "Pan toward history", category: "Chart", run: () => this.active.chart.panBy(-PAN_FAST) });
38711
38908
  this.keymap.register({ id: "view.pan-right", keys: "mod+arrowright", label: "Pan toward now", category: "Chart", run: () => this.active.chart.panBy(PAN_FAST) });
38712
- if (this.indicatorPicker) {
38909
+ const indOv = this.indicatorsOverride;
38910
+ if (indOv && topbarHas(this.topbarComp, "indicators")) {
38911
+ this.keymap.register({ id: "indicators.open", keys: "/", label: indOv.label, category: "Indicators", run: () => this.runOverride(indOv) });
38912
+ } else if (this.indicatorPicker) {
38713
38913
  this.keymap.register({ id: "indicators.open", keys: "/", label: "Open the indicator picker", category: "Indicators", run: () => this.indicatorPicker?.open() });
38714
38914
  }
38715
38915
  this.keymap.register({