@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
package/dist/widget.cjs CHANGED
@@ -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,33 @@ 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
+ function pinnedTopbarActionIds(comp) {
4610
+ const builtin = new Set(TOPBAR_BUILTIN_IDS);
4611
+ return [...comp.left, ...comp.right].filter((id) => !builtin.has(id));
4612
+ }
4613
+
4557
4614
  // src/widget/contributions.ts
4558
4615
  var registry3 = /* @__PURE__ */ new Map();
4559
4616
  var attachments = /* @__PURE__ */ new Map();
@@ -4583,7 +4640,18 @@ function unregisterSidePanel(id) {
4583
4640
  function sidePanels() {
4584
4641
  return [...panels.values()].sort((a, b) => (a.order ?? DEFAULT_PANEL_ORDER) - (b.order ?? DEFAULT_PANEL_ORDER));
4585
4642
  }
4643
+ var OVERRIDABLE_TOPBAR_IDS = ["indicators", "screenshot"];
4644
+ var overridableSet = new Set(OVERRIDABLE_TOPBAR_IDS);
4645
+ var builtinTopbarSet = new Set(TOPBAR_BUILTIN_IDS);
4646
+ function topbarActionOverride(id) {
4647
+ if (!overridableSet.has(id)) return void 0;
4648
+ return registry3.get(id);
4649
+ }
4586
4650
  function registerWidgetAction(desc) {
4651
+ if (builtinTopbarSet.has(desc.id) && !overridableSet.has(desc.id)) {
4652
+ console.warn(`[vela] widget action "${desc.id}": this built-in topbar slot is a stateful control and cannot be overridden \u2014 ignored. Overridable slots: ${OVERRIDABLE_TOPBAR_IDS.join(", ")}.`);
4653
+ return () => void 0;
4654
+ }
4587
4655
  registry3.set(desc.id, desc);
4588
4656
  return () => {
4589
4657
  if (registry3.get(desc.id) === desc) registry3.delete(desc.id);
@@ -4622,6 +4690,16 @@ function unregisterStatePersistence(key) {
4622
4690
  function statePersistenceHandlers(scope) {
4623
4691
  return [...stateHandlers.values()].filter((h) => h.scope === scope);
4624
4692
  }
4693
+ var symbolRankingHook;
4694
+ function registerSymbolRanking(hook) {
4695
+ symbolRankingHook = hook;
4696
+ return () => {
4697
+ if (symbolRankingHook === hook) symbolRankingHook = void 0;
4698
+ };
4699
+ }
4700
+ function symbolRanking() {
4701
+ return symbolRankingHook;
4702
+ }
4625
4703
  var defaultEngines = /* @__PURE__ */ new Map();
4626
4704
  function resolveEngines(overrides) {
4627
4705
  return { ...Object.fromEntries(defaultEngines), ...overrides };
@@ -5104,9 +5182,14 @@ var CSS3 = `
5104
5182
  align-items: center;
5105
5183
  justify-content: center;
5106
5184
  }
5107
- .vela-widget-actions { margin-left: auto; display: inline-flex; gap: var(--vela-space-1); }
5185
+ /* The right side of the bar \u2014 whatever the composition puts there rides this one
5186
+ auto-margin push (the flow-actions host used to carry it; composition can omit it). */
5187
+ .vela-topbar-right { margin-left: auto; display: inline-flex; align-items: center; gap: var(--vela-space-1); }
5188
+ .vela-widget-actions { display: inline-flex; gap: var(--vela-space-1); }
5108
5189
  /* Left-aligned contributed actions \u2014 the primary-chrome cluster after the dropdowns. */
5109
5190
  .vela-widget-actions-left { display: inline-flex; align-items: center; gap: var(--vela-space-1); }
5191
+ /* One PINNED contributed action's slot (a composition entry naming the action's id). */
5192
+ .vela-widget-action-pin { display: inline-flex; align-items: center; }
5110
5193
  /* The side-panel toggles, one per docked panel \u2014 a group so the dock can rebuild them
5111
5194
  without disturbing the tools around it. */
5112
5195
  .vela-widget-panels { display: inline-flex; align-items: center; gap: var(--vela-space-1); }
@@ -5165,6 +5248,17 @@ var Topbar = class {
5165
5248
  this.tooltips = [];
5166
5249
  this.panelBtns = /* @__PURE__ */ new Map();
5167
5250
  this.panelTooltips = [];
5251
+ /** Pinned contributed-action slots, by action id (composition entries that name one,
5252
+ * plus built-in slots taken over by an override). */
5253
+ this.pinned = /* @__PURE__ */ new Map();
5254
+ /** Overrides that LEFT their native slot (default side + a declared `order`) — they
5255
+ * render through the flow cluster like ordinary actions. */
5256
+ this.flowingOverrides = /* @__PURE__ */ new Set();
5257
+ /** Tooltips of the CURRENT icon-only action buttons — rebuilt with every
5258
+ * renderActions pass (contributed buttons are replaceChildren'd away). */
5259
+ this.actionTooltips = [];
5260
+ /** `iconOnly` misuse warned once per action id (renderActions re-runs freely). */
5261
+ this.warnedIconless = /* @__PURE__ */ new Set();
5168
5262
  this.onHairlineSync = () => {
5169
5263
  if (this.hairlineRaf) return;
5170
5264
  const win = this.el.ownerDocument.defaultView;
@@ -5179,6 +5273,8 @@ var Topbar = class {
5179
5273
  this.host = host;
5180
5274
  this.timeframe = opts.timeframe;
5181
5275
  this.priceStyle = opts.priceStyle;
5276
+ this.comp = resolveTopbarComposition(opts.composition);
5277
+ const vis = (id) => topbarHas(this.comp, id);
5182
5278
  const doc = host.ownerDocument;
5183
5279
  injectStyles(STYLE_ID2, CSS3, doc);
5184
5280
  this.el = doc.createElement("div");
@@ -5201,7 +5297,7 @@ var Topbar = class {
5201
5297
  this.styleButton.className = "vela-widget-style";
5202
5298
  this.renderStyleButton(doc);
5203
5299
  let indicatorsBtn = null;
5204
- if (opts.onIndicatorsClick) {
5300
+ if (opts.onIndicatorsClick && !topbarActionOverride("indicators")) {
5205
5301
  indicatorsBtn = doc.createElement("button");
5206
5302
  indicatorsBtn.className = "vela-widget-indicators";
5207
5303
  indicatorsBtn.append(iconEl("indicators", doc), doc.createTextNode("Indicators"));
@@ -5214,18 +5310,23 @@ var Topbar = class {
5214
5310
  this.panelsHost = doc.createElement("span");
5215
5311
  this.panelsHost.className = "vela-widget-panels";
5216
5312
  const tool = (cls, icon2, tip, onClick) => this.toolButton(cls, icon2, tip, onClick, this.tooltips);
5217
- this.undoBtn = tool("vela-widget-undo", "undo", "Undo", opts.onUndoClick);
5218
- this.redoBtn = tool("vela-widget-redo", "redo", "Redo", opts.onRedoClick);
5313
+ if (vis("undo-redo")) {
5314
+ this.undoBtn = tool("vela-widget-undo", "undo", "Undo", opts.onUndoClick);
5315
+ this.redoBtn = tool("vela-widget-redo", "redo", "Redo", opts.onRedoClick);
5316
+ } else {
5317
+ this.undoBtn = doc.createElement("button");
5318
+ this.redoBtn = doc.createElement("button");
5319
+ }
5219
5320
  this.setHistoryState(false, false);
5220
- const screenshotBtn = tool("vela-widget-screenshot", "camera", "Download screenshot", opts.onScreenshotClick);
5221
- this.alertsBtn = tool("vela-widget-alerts", "bell", "Alerts");
5321
+ const screenshotBtn = vis("screenshot") && !topbarActionOverride("screenshot") ? tool("vela-widget-screenshot", "camera", "Download screenshot", opts.onScreenshotClick) : null;
5322
+ this.alertsBtn = vis("alerts") ? tool("vela-widget-alerts", "bell", "Alerts") : doc.createElement("button");
5222
5323
  this.alertsBtn.style.position = "relative";
5223
5324
  if (opts.onAlertsClick) this.alertsBtn.addEventListener("click", () => opts.onAlertsClick(this.alertsBtn));
5224
5325
  this.alertsBadge = doc.createElement("span");
5225
5326
  this.alertsBadge.className = "vela-alerts-badge";
5226
5327
  this.alertsBadge.style.display = "none";
5227
5328
  this.alertsBtn.appendChild(this.alertsBadge);
5228
- if (opts.layout) {
5329
+ if (opts.layout && vis("layout")) {
5229
5330
  this.layoutId = opts.layout.current;
5230
5331
  this.layoutButton = doc.createElement("button");
5231
5332
  this.layoutButton.className = "vela-widget-style";
@@ -5236,12 +5337,65 @@ var Topbar = class {
5236
5337
  d.className = "vela-sep";
5237
5338
  return d;
5238
5339
  };
5239
- const leading = [this.symbolEl, sep(), tfGroup, sep(), this.styleButton, sep()];
5240
- if (this.layoutButton) leading.push(this.layoutButton, sep());
5241
- if (indicatorsBtn) leading.push(indicatorsBtn, sep());
5242
5340
  this.leftActionsSep = sep();
5243
5341
  this.leftActionsSep.hidden = true;
5244
- this.el.append(...leading, this.leftActionsHost, this.leftActionsSep, this.undoBtn, this.redoBtn, this.actionsHost, this.alertsBtn, this.panelsHost, screenshotBtn);
5342
+ const primaries = /* @__PURE__ */ new Set(["symbol", "timeframes", "style", "layout", "indicators"]);
5343
+ const pinSlot = (id, left) => {
5344
+ const slot = doc.createElement("span");
5345
+ slot.className = "vela-widget-action-pin";
5346
+ this.pinned.set(id, { host: slot, left });
5347
+ return [slot];
5348
+ };
5349
+ const overridden = (id, left) => {
5350
+ const ov = topbarActionOverride(id);
5351
+ if (!ov) return null;
5352
+ const sideDeclared = left ? opts.composition?.left != null : opts.composition?.right != null;
5353
+ if (!sideDeclared && ov.order !== void 0) {
5354
+ this.flowingOverrides.add(id);
5355
+ return [];
5356
+ }
5357
+ return pinSlot(id, left);
5358
+ };
5359
+ const elementsFor = (id, left) => {
5360
+ switch (id) {
5361
+ case "symbol":
5362
+ return [this.symbolEl];
5363
+ case "timeframes":
5364
+ return [tfGroup];
5365
+ case "style":
5366
+ return [this.styleButton];
5367
+ case "layout":
5368
+ return this.layoutButton ? [this.layoutButton] : [];
5369
+ case "indicators":
5370
+ return overridden(id, left) ?? (indicatorsBtn ? [indicatorsBtn] : []);
5371
+ case "actions":
5372
+ return left ? [this.leftActionsHost, this.leftActionsSep] : [this.actionsHost];
5373
+ case "undo-redo":
5374
+ return [this.undoBtn, this.redoBtn];
5375
+ case "alerts":
5376
+ return [this.alertsBtn];
5377
+ case "panels":
5378
+ return [this.panelsHost];
5379
+ case "screenshot":
5380
+ return overridden(id, left) ?? (screenshotBtn ? [screenshotBtn] : []);
5381
+ default:
5382
+ return pinSlot(id, left);
5383
+ }
5384
+ };
5385
+ const sideEls = (list, left) => {
5386
+ const out = [];
5387
+ for (const [i, id] of list.entries()) {
5388
+ const els = elementsFor(id, left);
5389
+ if (els.length === 0) continue;
5390
+ out.push(...els);
5391
+ if (primaries.has(id) && i < list.length - 1) out.push(sep());
5392
+ }
5393
+ return out;
5394
+ };
5395
+ const right = doc.createElement("span");
5396
+ right.className = "vela-topbar-right";
5397
+ right.append(...sideEls(this.comp.right, false));
5398
+ this.el.append(...sideEls(this.comp.left, true), right);
5245
5399
  host.appendChild(this.el);
5246
5400
  this.renderTfChips();
5247
5401
  this.onHairlineSync();
@@ -5358,23 +5512,45 @@ var Topbar = class {
5358
5512
  else this.styleButton.appendChild(doc.createTextNode(priceStyleLabel(this.priceStyle)));
5359
5513
  this.styleButton.setAttribute("aria-label", `Chart style \u2014 ${priceStyleLabel(this.priceStyle)}`);
5360
5514
  }
5361
- /** Re-project the contributed topbar actions (call after registrations change). */
5515
+ /** Re-project the contributed topbar actions (call after registrations change).
5516
+ * An action PINNED by the composition renders into its named slot (list position
5517
+ * wins over `align`/`order`); the rest flow into the side's `actions` slot — or
5518
+ * not at all when an explicit list omits it (the list is the side's contract). */
5362
5519
  renderActions() {
5363
5520
  const ctx = this.opts.getContext?.();
5364
5521
  this.actionsHost.replaceChildren();
5365
5522
  this.leftActionsHost.replaceChildren();
5523
+ for (const pin of this.pinned.values()) pin.host.replaceChildren();
5524
+ for (const t of this.actionTooltips) t.destroy();
5525
+ this.actionTooltips = [];
5366
5526
  const doc = this.actionsHost.ownerDocument;
5527
+ const flowLeft = this.comp.left.includes("actions");
5528
+ const flowRight = this.comp.right.includes("actions");
5529
+ const builtin = new Set(TOPBAR_BUILTIN_IDS);
5367
5530
  for (const action of widgetActions("topbar", ctx)) {
5368
- const left = action.align === "left";
5531
+ const pin = this.pinned.get(action.id);
5532
+ if (!pin && builtin.has(action.id) && !this.flowingOverrides.has(action.id)) continue;
5533
+ const left = pin ? pin.left : action.align === "left";
5534
+ if (!pin && !(left ? flowLeft : flowRight)) continue;
5535
+ const iconOnly = action.iconOnly === true && !!action.icon;
5536
+ if (action.iconOnly === true && !action.icon && !this.warnedIconless.has(action.id)) {
5537
+ this.warnedIconless.add(action.id);
5538
+ console.warn(`[vela] widget action "${action.id}": iconOnly needs an \`icon\` \u2014 rendering the label instead.`);
5539
+ }
5369
5540
  const b = doc.createElement("button");
5370
- b.className = left ? "vela-widget-action-left" : "vela-widget-action";
5541
+ b.className = left ? "vela-widget-action-left" : iconOnly ? "vela-widget-tool" : "vela-widget-action";
5371
5542
  if (action.icon) b.appendChild(iconEl(action.icon, doc));
5372
- b.appendChild(doc.createTextNode(action.label));
5543
+ if (iconOnly) {
5544
+ b.setAttribute("aria-label", action.label);
5545
+ this.actionTooltips.push(new Tooltip(b, { content: action.label, triggerId: `vela-action-${action.id}`, host: this.host }));
5546
+ } else {
5547
+ b.appendChild(doc.createTextNode(action.label));
5548
+ }
5373
5549
  b.addEventListener("click", () => {
5374
5550
  const c = this.opts.getContext?.();
5375
5551
  if (c) action.run(c);
5376
5552
  });
5377
- (left ? this.leftActionsHost : this.actionsHost).appendChild(b);
5553
+ (pin ? pin.host : left ? this.leftActionsHost : this.actionsHost).appendChild(b);
5378
5554
  }
5379
5555
  this.leftActionsSep.hidden = this.leftActionsHost.childElementCount === 0;
5380
5556
  this.onHairlineSync();
@@ -5420,7 +5596,7 @@ var Topbar = class {
5420
5596
  this.tfMenu.destroy();
5421
5597
  this.styleMenu.destroy();
5422
5598
  this.layoutPicker?.destroy();
5423
- for (const t of [...this.tooltips, ...this.panelTooltips]) t.destroy();
5599
+ for (const t of [...this.tooltips, ...this.panelTooltips, ...this.actionTooltips]) t.destroy();
5424
5600
  this.el.remove();
5425
5601
  }
5426
5602
  /** One icon-only tool button with its kit tooltip, parked in `sink` for disposal. */
@@ -5767,41 +5943,39 @@ var Bottombar = class {
5767
5943
  }
5768
5944
  };
5769
5945
 
5770
- // src/widget/symbol-icon.ts
5771
- var iconFailed = /* @__PURE__ */ new Set();
5772
- function initialsOf(name) {
5773
- return (name || "?").replace(/[^A-Za-z0-9]/g, "").slice(0, 2).toUpperCase() || "?";
5774
- }
5775
- function cryptoIconUrl(base) {
5776
- return `https://crypto-icons.ledger.com/${encodeURIComponent(base.toUpperCase())}.png`;
5777
- }
5946
+ // src/data/symbol-base.ts
5778
5947
  function baseOf(d) {
5779
5948
  const fromDesc = d.description?.split("/")[0]?.trim();
5780
5949
  if (fromDesc) return fromDesc.replace(/\s+Perpetual$/i, "");
5781
5950
  return d.ticker.replace(/[-_/]?(USDT|USDC|USD1|USDS|BUSD|USD|EUR|PERP)$/i, "") || d.ticker;
5782
5951
  }
5783
- function tickerIconEl(doc, base, name, className) {
5952
+
5953
+ // src/widget/symbol-icon.ts
5954
+ var iconFailed = /* @__PURE__ */ new Set();
5955
+ function initialsOf(name) {
5956
+ return (name || "?").replace(/[^A-Za-z0-9]/g, "").slice(0, 2).toUpperCase() || "?";
5957
+ }
5958
+ function tickerIconEl(doc, base, name, className, iconUrl) {
5784
5959
  const wrap = doc.createElement("span");
5785
5960
  wrap.className = className;
5786
- const key = base.toUpperCase();
5787
5961
  const fallback = () => {
5788
5962
  wrap.replaceChildren();
5789
5963
  wrap.style.background = categoricalColor(name);
5790
5964
  wrap.textContent = initialsOf(base || name);
5791
5965
  };
5792
- if (!key || iconFailed.has(key)) {
5966
+ if (!iconUrl || iconFailed.has(iconUrl)) {
5793
5967
  fallback();
5794
5968
  return wrap;
5795
5969
  }
5796
5970
  const img = doc.createElement("img");
5797
5971
  img.alt = "";
5798
5972
  img.crossOrigin = "anonymous";
5799
- img.src = cryptoIconUrl(key);
5973
+ img.src = iconUrl;
5800
5974
  img.style.cssText = "width:100%;height:100%;border-radius:50%;display:block;object-fit:cover;";
5801
5975
  img.addEventListener(
5802
5976
  "error",
5803
5977
  () => {
5804
- iconFailed.add(key);
5978
+ iconFailed.add(iconUrl);
5805
5979
  fallback();
5806
5980
  },
5807
5981
  { once: true }
@@ -12054,11 +12228,14 @@ var MenuBuilder = class {
12054
12228
  }
12055
12229
  };
12056
12230
  var ObjectTree = class extends SidePanel {
12057
- constructor(host) {
12231
+ constructor(host, iconFor) {
12058
12232
  super(host, "Object tree", "vela-ot");
12233
+ this.iconFor = iconFor;
12059
12234
  this.chart = null;
12060
12235
  this.selectedDrawing = null;
12061
12236
  this.symbolName = "";
12237
+ /** The raw (possibly venue-prefixed) symbol — what icon resolution routes on. */
12238
+ this.symbolRaw = "";
12062
12239
  /** Drawing bundles — a view-side grouping, held for the panel's lifetime and never persisted.
12063
12240
  * Kept per chart because a workspace points this one panel at whichever chart is active, and
12064
12241
  * each chart's bundles have to survive the switch. */
@@ -12095,6 +12272,7 @@ var ObjectTree = class extends SidePanel {
12095
12272
  if (open2) this.refresh();
12096
12273
  }
12097
12274
  setSymbol(symbol) {
12275
+ this.symbolRaw = symbol;
12098
12276
  this.symbolName = parseSymbol(symbol).ticker;
12099
12277
  }
12100
12278
  /** (Re)bind to a chart instance — called after every widget rebuild. */
@@ -12295,7 +12473,7 @@ var ObjectTree = class extends SidePanel {
12295
12473
  const { chart } = pass;
12296
12474
  if (row.kind === "price") {
12297
12475
  const base = this.symbolName.replace(/[-_/]?(USDT|USDC|USD1|USDS|BUSD|USD|EUR|PERP)$/i, "") || this.symbolName;
12298
- const icon2 = tickerIconEl(doc, base || "P", this.symbolName || "Price", "vela-ot-avatar");
12476
+ const icon2 = tickerIconEl(doc, base || "P", this.symbolName || "Price", "vela-ot-avatar", this.symbolRaw ? this.iconFor?.(this.symbolRaw) : void 0);
12299
12477
  const el2 = this.row(icon2, row.label, row.visible, [
12300
12478
  {
12301
12479
  icon: row.visible ? "eye" : "eye-off",
@@ -13327,6 +13505,17 @@ var PanelDock = class {
13327
13505
  };
13328
13506
 
13329
13507
  // src/widget/symbol-picker.ts
13508
+ function dedupeSymbols(list) {
13509
+ const seen = /* @__PURE__ */ new Set();
13510
+ const out = [];
13511
+ for (const s of list) {
13512
+ const key = `${(s.prefix ?? s.provider ?? "").toLowerCase()}:${s.ticker.toUpperCase()}`;
13513
+ if (seen.has(key)) continue;
13514
+ seen.add(key);
13515
+ out.push(s);
13516
+ }
13517
+ return out;
13518
+ }
13330
13519
  var TOP_TICKERS = ["BTCUSDT", "ETHUSDT", "SOLUSDT", "BNBUSDT", "XRPUSDT", "DOGEUSDT", "ADAUSDT", "LINKUSDT"];
13331
13520
  function parseQuery(raw, venues) {
13332
13521
  const m = raw.match(/^\s*([^\s:]+)\s*[:\s]\s*(.*)$/);
@@ -13341,17 +13530,18 @@ function onlyOne(matches) {
13341
13530
  return matches.length === 1 ? matches[0] : null;
13342
13531
  }
13343
13532
  var venueOf = (s) => s.prefix ?? s.provider;
13344
- function filterSymbols(list, query, limit = 100) {
13533
+ function filterSymbols(list, query, limit = 100, top = TOP_TICKERS) {
13345
13534
  const venues = [...new Set(list.flatMap((s) => [venueOf(s)?.toLowerCase(), s.provider?.toLowerCase()]).filter((p) => !!p))];
13346
13535
  const { scope, term } = parseQuery(query, venues);
13347
13536
  const pool = scope ? list.filter((s) => venueOf(s)?.toLowerCase() === scope || s.provider?.toLowerCase() === scope) : list;
13348
13537
  const q = term.toUpperCase();
13349
13538
  if (!q) {
13350
13539
  if (scope) return [...pool].sort((a, b) => a.ticker.localeCompare(b.ticker)).slice(0, limit);
13540
+ if (top === false) return pool.slice(0, limit);
13351
13541
  const byTicker = new Map(pool.map((s) => [s.ticker.toUpperCase(), s]));
13352
- const top = TOP_TICKERS.map((t) => byTicker.get(t)).filter((s) => s !== void 0);
13353
- const rest = pool.filter((s) => !TOP_TICKERS.includes(s.ticker.toUpperCase()));
13354
- return [...top, ...rest].slice(0, limit);
13542
+ const pinned = top.map((t) => byTicker.get(t)).filter((s) => s !== void 0);
13543
+ const rest = pool.filter((s) => !top.includes(s.ticker.toUpperCase()));
13544
+ return [...pinned, ...rest].slice(0, limit);
13355
13545
  }
13356
13546
  const qLower = term.toLowerCase();
13357
13547
  const prefix = [];
@@ -13461,12 +13651,16 @@ var CSS8 = `
13461
13651
  var PAGE = 100;
13462
13652
  var SymbolPicker = class {
13463
13653
  constructor(opts) {
13654
+ this.opts = opts;
13464
13655
  this.source = () => [];
13465
13656
  this.rows = [];
13466
13657
  this.highlighted = 0;
13467
13658
  this.seed = "";
13468
13659
  this.activeTab = "All";
13469
13660
  this.visible = PAGE;
13661
+ /** The ranked pool cache — `key` fingerprints the raw pool the ranking ran on. */
13662
+ this.ranked = null;
13663
+ this.ranking = false;
13470
13664
  const doc = (opts.host ?? document.body).ownerDocument;
13471
13665
  injectStyles(STYLE_ID7, CSS8, doc);
13472
13666
  this.input = doc.createElement("input");
@@ -13564,10 +13758,34 @@ var SymbolPicker = class {
13564
13758
  } else delete el.dataset.highlighted;
13565
13759
  });
13566
13760
  }
13761
+ /** The picker's pool: the source, shaped by the registered symbol ranking. Cached —
13762
+ * the hook runs when the pool CHANGES (an index lands or refreshes), never per
13763
+ * keystroke; an async hook resolves onto the next repaint (stale-while-revalidate
13764
+ * in between, the raw pool before the first resolve). */
13765
+ pool() {
13766
+ const raw = this.source();
13767
+ const hook = symbolRanking();
13768
+ if (!hook) return raw;
13769
+ const key = `${raw.length}:${raw[0]?.ticker ?? ""}:${raw[raw.length - 1]?.ticker ?? ""}`;
13770
+ if (this.ranked?.key !== key && !this.ranking) {
13771
+ this.ranking = true;
13772
+ Promise.resolve([...raw]).then((copy) => hook(copy)).then((out) => {
13773
+ this.ranked = { key, result: dedupeSymbols(out) };
13774
+ }).catch((err) => {
13775
+ console.warn("[vela] symbol ranking failed \u2014 pool order kept:", err);
13776
+ this.ranked = { key, result: [...raw] };
13777
+ }).finally(() => {
13778
+ this.ranking = false;
13779
+ this.refresh();
13780
+ });
13781
+ }
13782
+ return this.ranked?.result ?? raw;
13783
+ }
13567
13784
  computeRows() {
13568
13785
  const TAB_TYPES = { Crypto: ["crypto"], Stocks: ["stock"], ETFs: ["etf"], Forex: ["forex"], Commodities: ["commodity"] };
13569
- 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");
13570
- return filterSymbols(pool, this.input.value, this.visible);
13786
+ const all = this.pool();
13787
+ const pool = this.activeTab === "All" ? all : all.filter((s) => TAB_TYPES[this.activeTab]?.includes((s.type ?? "").toLowerCase()) || this.activeTab === "Crypto" && (s.type ?? "").toLowerCase() === "futures");
13788
+ return filterSymbols(pool, this.input.value, this.visible, symbolRanking() ? false : TOP_TICKERS);
13571
13789
  }
13572
13790
  refresh() {
13573
13791
  const doc = this.list.ownerDocument;
@@ -13598,7 +13816,7 @@ var SymbolPicker = class {
13598
13816
  row.dataset.ticker = s.ticker;
13599
13817
  const venue = s.prefix ?? s.provider;
13600
13818
  if (venue) row.dataset.venue = venue;
13601
- const av = tickerIconEl(doc, baseOf(s), s.ticker, "vela-sp-avatar");
13819
+ const av = tickerIconEl(doc, baseOf(s), s.ticker, "vela-sp-avatar", this.opts.iconFor?.(s));
13602
13820
  const main = doc.createElement("span");
13603
13821
  main.className = "vela-sp-main";
13604
13822
  const t = doc.createElement("span");
@@ -14279,7 +14497,8 @@ var MobileBar = class {
14279
14497
  if (!ctx) return;
14280
14498
  const doc = this.el.ownerDocument;
14281
14499
  this.actionsHost.replaceChildren();
14282
- for (const action of widgetActions("topbar", ctx).filter((a) => a.align === "left")) {
14500
+ const builtin = new Set(TOPBAR_BUILTIN_IDS);
14501
+ for (const action of widgetActions("topbar", ctx).filter((a) => a.align === "left" && !builtin.has(a.id))) {
14283
14502
  const b = doc.createElement("button");
14284
14503
  b.className = "vela-mb-item";
14285
14504
  b.setAttribute("aria-label", action.label);
@@ -14808,10 +15027,10 @@ var MoreDrawer = class {
14808
15027
  });
14809
15028
  actions.appendChild(b);
14810
15029
  };
14811
- action("undo", "Undo", this.opts.canUndo(), this.opts.onUndo);
14812
- action("redo", "Redo", this.opts.canRedo(), this.opts.onRedo);
14813
- action("camera", "Screenshot", true, this.opts.onScreenshot);
14814
- this.drawer.body.appendChild(actions);
15030
+ if (this.opts.onUndo) action("undo", "Undo", this.opts.canUndo(), this.opts.onUndo);
15031
+ if (this.opts.onRedo) action("redo", "Redo", this.opts.canRedo(), this.opts.onRedo);
15032
+ if (this.opts.onScreenshot) action("camera", "Screenshot", true, this.opts.onScreenshot);
15033
+ if (actions.childElementCount > 0) this.drawer.body.appendChild(actions);
14815
15034
  const list = doc.createElement("div");
14816
15035
  list.className = "vela-md-list";
14817
15036
  const current = this.opts.priceStyles().find((s) => s.id === this.opts.priceStyle());
@@ -14832,8 +15051,10 @@ var MoreDrawer = class {
14832
15051
  })
14833
15052
  );
14834
15053
  }
14835
- const alertCount = this.opts.alerts().length;
14836
- list.appendChild(this.row(doc, "Alerts", { icon: "bell", value: alertCount > 0 ? String(alertCount) : void 0, chevron: true, onClick: () => this.show("alerts") }));
15054
+ if (this.opts.alerts) {
15055
+ const alertCount = this.opts.alerts().length;
15056
+ list.appendChild(this.row(doc, "Alerts", { icon: "bell", value: alertCount > 0 ? String(alertCount) : void 0, chevron: true, onClick: () => this.show("alerts") }));
15057
+ }
14837
15058
  for (const act of this.opts.actions()) {
14838
15059
  list.appendChild(
14839
15060
  this.row(doc, act.label, {
@@ -14914,7 +15135,7 @@ var MoreDrawer = class {
14914
15135
  this.drawer.body.appendChild(list);
14915
15136
  }
14916
15137
  renderAlerts(doc) {
14917
- const alerts = this.opts.alerts();
15138
+ const alerts = this.opts.alerts?.() ?? [];
14918
15139
  if (alerts.length === 0) {
14919
15140
  const empty = doc.createElement("div");
14920
15141
  empty.className = "vela-md-empty";
@@ -17111,19 +17332,8 @@ function registerBuiltinChartTypes() {
17111
17332
  registerChartType({ id: "heikinashi", label: "Heikin Ashi", barTransform: HEIKIN_ASHI });
17112
17333
  }
17113
17334
 
17114
- // src/workspace/sync.ts
17115
- function syncTargets(originId, setting, cellIds) {
17116
- if (setting == null || setting === false) return [];
17117
- if (setting === true) return cellIds.filter((id) => id !== originId);
17118
- const group = setting[originId];
17119
- if (group == null) return [];
17120
- return cellIds.filter((id) => id !== originId && setting[id] === group);
17121
- }
17122
- function rangesWithin(a, b, epsMs) {
17123
- return Math.abs(a.from - b.from) <= epsMs && Math.abs(a.to - b.to) <= epsMs;
17124
- }
17125
-
17126
17335
  // src/state/document.ts
17336
+ var SYNC_KINDS = ["viewport", "symbol", "timeframe", "crosshair", "drawings", "style"];
17127
17337
  function prefixedSymbol(cell) {
17128
17338
  if (!cell?.symbol) return void 0;
17129
17339
  if (cell.symbol.includes(":") || !cell.provider) return cell.symbol;
@@ -17211,7 +17421,7 @@ function sanitizeSync(raw) {
17211
17421
  if (raw == null || typeof raw !== "object") return null;
17212
17422
  const s = raw;
17213
17423
  const out = {};
17214
- for (const kind of ["viewport", "symbol", "timeframe", "crosshair", "drawings"]) {
17424
+ for (const kind of SYNC_KINDS) {
17215
17425
  const v = s[kind];
17216
17426
  if (v === true) out[kind] = true;
17217
17427
  else if (v != null && typeof v === "object") {
@@ -17254,6 +17464,28 @@ function sanitizeTrackSizes(raw) {
17254
17464
  return Object.keys(out).length > 0 ? out : null;
17255
17465
  }
17256
17466
 
17467
+ // src/workspace/sync.ts
17468
+ function syncTargets(originId, setting, cellIds) {
17469
+ if (setting == null || setting === false) return [];
17470
+ if (setting === true) return cellIds.filter((id) => id !== originId);
17471
+ const group = setting[originId];
17472
+ if (group == null) return [];
17473
+ return cellIds.filter((id) => id !== originId && setting[id] === group);
17474
+ }
17475
+ function rangesWithin(a, b, epsMs) {
17476
+ return Math.abs(a.from - b.from) <= epsMs && Math.abs(a.to - b.to) <= epsMs;
17477
+ }
17478
+ var STYLE_SYNC_CONFIG_KEYS = ["layout", "panes", "grid", "priceScale", "crosshair"];
17479
+ function styleConfigSlice(config) {
17480
+ if (config == null || typeof config !== "object") return null;
17481
+ const doc = config;
17482
+ const out = {};
17483
+ for (const key of STYLE_SYNC_CONFIG_KEYS) {
17484
+ if (doc[key] != null && typeof doc[key] === "object") out[key] = doc[key];
17485
+ }
17486
+ return Object.keys(out).length > 0 ? out : null;
17487
+ }
17488
+
17257
17489
  // src/widget/persist.ts
17258
17490
  function localStorageAdapter(storageKey) {
17259
17491
  return {
@@ -19299,6 +19531,12 @@ var DataControl = class {
19299
19531
  symbols(provider) {
19300
19532
  return this.registry?.symbols(provider) ?? [];
19301
19533
  }
19534
+ /** The icon URL for `symbol` — its owning provider's `resolveSymbolIcon`, routed
19535
+ * through resolution. Undefined while unresolvable, when the provider declares no
19536
+ * resolver, or on a custom `deps.dataFeed` — the shells then show initials. */
19537
+ symbolIcon(symbol) {
19538
+ return this.registry?.symbolIcon(symbol);
19539
+ }
19302
19540
  /** Per-symbol metadata (Pine `syminfo.*`), resolved through the owning provider. */
19303
19541
  symbolInfo(symbol) {
19304
19542
  return this.registry?.symbolInfoFor(symbol) ?? Promise.resolve(void 0);
@@ -19588,12 +19826,16 @@ var IndicatorInputsDialog = class {
19588
19826
  closeOnEscape: false,
19589
19827
  footer: (foot) => {
19590
19828
  foot.append(
19829
+ this.resetAction(),
19591
19830
  this.dialogButton("Cancel", false, () => this.revertAndClose()),
19592
19831
  this.dialogButton("Ok", true, () => this.close())
19593
19832
  );
19594
19833
  },
19834
+ // Stale-guard: destroying a dialog fires its machine's onOpenChange(false)
19835
+ // asynchronously — after a reset rebuild that notification must not close
19836
+ // the replacement dialog.
19595
19837
  onOpenChange: (open2) => {
19596
- if (!open2) this.close();
19838
+ if (!open2 && this.uiDialog === ui) this.close();
19597
19839
  }
19598
19840
  });
19599
19841
  applyChromeTokens(ui.panel, t);
@@ -19724,6 +19966,30 @@ var IndicatorInputsDialog = class {
19724
19966
  }
19725
19967
  this.close();
19726
19968
  }
19969
+ /** The footer's reset button — same chip as Cancel, pinned to the LEFT edge
19970
+ * (`margin-right:auto` against the footer's flex-end keeps Cancel/Ok right). */
19971
+ resetAction() {
19972
+ const b = this.dialogButton("Reset defaults", false, () => this.resetToDefaults());
19973
+ b.style.marginRight = "auto";
19974
+ return b;
19975
+ }
19976
+ /** Restore every input to its declared default (re-running the indicator), then
19977
+ * re-open the form so each control re-reads the restored values — the same
19978
+ * rebuild-after-reset move as the chart-settings dialog. The open-time snapshot
19979
+ * survives the rebuild, so Cancel after a reset still reverts the whole session. */
19980
+ resetToDefaults() {
19981
+ const row = this.row;
19982
+ if (!row) return;
19983
+ const snap = this.snapshot;
19984
+ for (const inp of row.inputs) {
19985
+ if (row.values[inp.key] !== inp.defval) {
19986
+ row.values[inp.key] = inp.defval;
19987
+ this.host.onChange?.({ indicatorId: row.id, key: inp.key, value: inp.defval });
19988
+ }
19989
+ }
19990
+ this.open(row);
19991
+ if (snap) this.snapshot = snap;
19992
+ }
19727
19993
  /** Write one edit through: store it, notify the host, and re-apply the `when` gates. */
19728
19994
  commit(row, key, value) {
19729
19995
  row.values[key] = value;
@@ -31157,6 +31423,11 @@ function resizeSplit(split, dyTotal, minPx = MIN_PANE_PX) {
31157
31423
  }
31158
31424
 
31159
31425
  // src/renderers/native/backdrop/BackdropRenderer.ts
31426
+ function clipHighlightRect(x1, x2, left, right) {
31427
+ const x = Math.max(left, x1);
31428
+ const end = Math.min(right, x2);
31429
+ return end > x ? { x, width: end - x } : null;
31430
+ }
31160
31431
  var BackdropRenderer = class {
31161
31432
  constructor() {
31162
31433
  this.canvas = null;
@@ -31189,17 +31460,22 @@ var BackdropRenderer = class {
31189
31460
  /** Renderer-owned session highlight bands: full-height (all panes), behind the grid.
31190
31461
  * Session-zone washes (pre/post-market) paint first, host highlights on top. */
31191
31462
  drawHighlights(ctx, scene, coords) {
31192
- const bands = [...scene.sessionHighlightBands(), ...scene.highlights];
31193
- if (bands.length === 0) return;
31463
+ const sessions = scene.sessionHighlightBands();
31464
+ if (sessions.length > 0) {
31465
+ const left = Math.max(0, coords.logicalToX(-0.5));
31466
+ const right = Math.min(coords.width, coords.logicalToX(coords.barCount - 0.5));
31467
+ this.drawHighlightSet(ctx, sessions, coords, left, right);
31468
+ }
31469
+ this.drawHighlightSet(ctx, scene.highlights, coords, 0, coords.width);
31470
+ }
31471
+ drawHighlightSet(ctx, bands, coords, left, right) {
31194
31472
  for (const band of bands) {
31195
31473
  const x1 = coords.timeToX(band.from);
31196
31474
  const x2 = coords.timeToX(band.to);
31197
- if (x2 < 0 || x1 > coords.width || x2 <= x1) continue;
31198
- const cx = Math.max(0, x1);
31199
- const cw = Math.min(coords.width, x2) - cx;
31200
- if (cw <= 0) continue;
31475
+ const rect = clipHighlightRect(x1, x2, left, right);
31476
+ if (!rect) continue;
31201
31477
  ctx.fillStyle = band.color;
31202
- ctx.fillRect(cx, 0, cw, coords.height);
31478
+ ctx.fillRect(rect.x, 0, rect.width, coords.height);
31203
31479
  }
31204
31480
  }
31205
31481
  // ── grid ── vert/horz gate on `scene.showGrid` AND their own per-axis visibility
@@ -35647,8 +35923,9 @@ function statuslineInkOf(renderer, priceStyle) {
35647
35923
  }
35648
35924
  }
35649
35925
  var Statusline = class {
35650
- constructor(host, symbol) {
35926
+ constructor(host, symbol, iconFor) {
35651
35927
  this.host = host;
35928
+ this.iconFor = iconFor;
35652
35929
  this.parts = { name: true, market: true, ohlc: true, change: true };
35653
35930
  this.lastBar = null;
35654
35931
  this.hoverBar = null;
@@ -35672,7 +35949,7 @@ var Statusline = class {
35672
35949
  this.el.className = "vela-statusline";
35673
35950
  this.el.dataset.velaScreenshot = "1";
35674
35951
  const ticker = parseSymbol(symbol).ticker;
35675
- this.avatarEl = tickerIconEl(doc, baseOfTicker(ticker), ticker, "vela-sl-avatar");
35952
+ this.avatarEl = tickerIconEl(doc, baseOfTicker(ticker), ticker, "vela-sl-avatar", this.iconFor?.(symbol));
35676
35953
  this.symbolEl = doc.createElement("span");
35677
35954
  this.symbolEl.className = "vela-sl-symbol";
35678
35955
  this.symbolEl.textContent = ticker;
@@ -35693,7 +35970,7 @@ var Statusline = class {
35693
35970
  setSymbol(symbol) {
35694
35971
  const ticker = parseSymbol(symbol).ticker;
35695
35972
  this.symbolEl.textContent = ticker;
35696
- const fresh = tickerIconEl(this.el.ownerDocument, baseOfTicker(ticker), ticker, "vela-sl-avatar");
35973
+ const fresh = tickerIconEl(this.el.ownerDocument, baseOfTicker(ticker), ticker, "vela-sl-avatar", this.iconFor?.(symbol));
35697
35974
  this.avatarEl.replaceWith(fresh);
35698
35975
  this.avatarEl = fresh;
35699
35976
  this.fit();
@@ -35940,61 +36217,144 @@ var MarketStatusTracker = class {
35940
36217
  };
35941
36218
 
35942
36219
  // src/widget/session-shading.ts
35943
- function deriveSessionZones(regular, extended) {
36220
+ var DAY_MS2 = 864e5;
36221
+ var MINUTE_MS = 6e4;
36222
+ function parseWindow(text) {
36223
+ if (typeof text !== "string") return null;
36224
+ const m = /^(\d{2})(\d{2})-(\d{2})(\d{2})$/.exec(text);
36225
+ if (!m) return null;
36226
+ const start = Number(m[1]) * 60 + Number(m[2]);
36227
+ const end = Number(m[3]) * 60 + Number(m[4]);
36228
+ if (start >= end || end > 1440) return null;
36229
+ return { start, end };
36230
+ }
36231
+ function parseSessionSpec(si) {
36232
+ const session = si?.["session"];
36233
+ if (typeof session !== "string" || session === "" || session === "24x7") return null;
36234
+ const regular = parseWindow(session);
36235
+ if (!regular) return null;
36236
+ const tz = si?.["timezone"];
36237
+ const timezone = typeof tz === "string" && tz !== "" ? tz : "Etc/UTC";
36238
+ const ext = parseWindow(si?.["session_extended"]);
36239
+ const extended = ext && ext.start <= regular.start && ext.end >= regular.end ? ext : { start: 0, end: 1440 };
36240
+ return { regular, extended, timezone };
36241
+ }
36242
+ var dtfCache = /* @__PURE__ */ new Map();
36243
+ function civilFormatter(tz) {
36244
+ const cached = dtfCache.get(tz);
36245
+ if (cached !== void 0) return cached;
36246
+ let dtf = null;
36247
+ try {
36248
+ dtf = new Intl.DateTimeFormat("en-US", {
36249
+ timeZone: tz,
36250
+ hourCycle: "h23",
36251
+ weekday: "short",
36252
+ year: "numeric",
36253
+ month: "2-digit",
36254
+ day: "2-digit",
36255
+ hour: "2-digit",
36256
+ minute: "2-digit"
36257
+ });
36258
+ } catch {
36259
+ dtf = null;
36260
+ }
36261
+ dtfCache.set(tz, dtf);
36262
+ return dtf;
36263
+ }
36264
+ function civilParts(dtf, ms) {
36265
+ const parts = dtf.formatToParts(ms);
36266
+ const get = (t) => parts.find((p) => p.type === t)?.value ?? "";
36267
+ return {
36268
+ weekday: get("weekday"),
36269
+ year: Number(get("year")),
36270
+ month: Number(get("month")),
36271
+ day: Number(get("day")),
36272
+ hour: Number(get("hour")) % 24,
36273
+ minute: Number(get("minute"))
36274
+ };
36275
+ }
36276
+ function expandSessionZones(spec, from, to) {
35944
36277
  const pre = [];
35945
36278
  const post = [];
35946
- for (const [extStart, extEnd] of extended) {
35947
- const inside = regular.filter(([s, e]) => e > extStart && s < extEnd).sort((a, b) => a[0] - b[0]);
35948
- let cursor = extStart;
35949
- for (const [regStart, regEnd] of inside) {
35950
- if (regStart > cursor) pre.push([cursor, Math.min(regStart, extEnd)]);
35951
- cursor = Math.max(cursor, regEnd);
35952
- }
35953
- if (cursor < extEnd) post.push([cursor, extEnd]);
36279
+ const dtf = civilFormatter(spec.timezone);
36280
+ if (!dtf || !Number.isFinite(from) || !Number.isFinite(to)) return { pre, post };
36281
+ const seen = /* @__PURE__ */ new Set();
36282
+ for (let cursor = from - DAY_MS2; cursor < to + DAY_MS2; cursor += DAY_MS2) {
36283
+ const civil = civilParts(dtf, cursor);
36284
+ const key = civil.year * 1e4 + civil.month * 100 + civil.day;
36285
+ if (!Number.isFinite(key) || seen.has(key)) continue;
36286
+ seen.add(key);
36287
+ if (civil.weekday === "Sat" || civil.weekday === "Sun") continue;
36288
+ const naive = Date.UTC(civil.year, civil.month - 1, civil.day);
36289
+ const noonGuess = naive + 720 * MINUTE_MS;
36290
+ const atNoon = civilParts(dtf, noonGuess);
36291
+ const offset = Date.UTC(atNoon.year, atNoon.month - 1, atNoon.day, atNoon.hour, atNoon.minute) - noonGuess;
36292
+ const at = (minutes) => naive + minutes * MINUTE_MS - offset;
36293
+ if (spec.extended.start < spec.regular.start) pre.push([at(spec.extended.start), at(spec.regular.start)]);
36294
+ if (spec.regular.end < spec.extended.end) post.push([at(spec.regular.end), at(spec.extended.end)]);
35954
36295
  }
35955
36296
  return { pre, post };
35956
36297
  }
35957
- var FETCH_AHEAD_MS2 = 10 * 864e5;
35958
- var MIN_LOOKBACK_MS = 3 * 864e5;
35959
- var MAX_LOOKBACK_MS = 120 * 864e5;
36298
+ var COVER_PAD_MIN_MS = 2 * DAY_MS2;
35960
36299
  var SessionShadingTracker = class {
35961
36300
  constructor(onZones) {
35962
36301
  this.onZones = onZones;
35963
- /** Invalidates detached async work — bumped by every track()/stop(). */
36302
+ /** Invalidates the one async step (metadata resolution) — bumped by track()/stop(). */
35964
36303
  this.epoch = 0;
35965
- }
35966
- /** (Re)bind to a chart's data surface + market and evaluate once. */
36304
+ this.spec = null;
36305
+ this.ready = false;
36306
+ this.session = "regular";
36307
+ this.covered = null;
36308
+ /** The newest range seen — viewport moves during metadata resolution (a load's fit
36309
+ * animation) must not be lost, so the resolution always expands the LATEST range. */
36310
+ this.lastRange = null;
36311
+ }
36312
+ /** (Re)bind to a chart's data surface + market and expand once metadata lands. */
35967
36313
  track(data, symbol, opts) {
35968
36314
  const my = ++this.epoch;
35969
- void this.evaluate(my, data, symbol, opts);
36315
+ this.ready = false;
36316
+ this.spec = null;
36317
+ this.covered = null;
36318
+ this.session = opts.session;
36319
+ this.lastRange = opts.range;
36320
+ void data.symbolInfo(symbol).catch(() => void 0).then((si) => {
36321
+ if (my !== this.epoch) return;
36322
+ this.ready = true;
36323
+ this.spec = parseSessionSpec(si);
36324
+ this.emit(this.lastRange ?? opts.range, true);
36325
+ });
36326
+ }
36327
+ /** Follow a pan/zoom synchronously: bands are epoch-anchored, so only a range that
36328
+ * leaves the last expansion's coverage needs a recompute — no fetch, no debounce. */
36329
+ updateRange(range) {
36330
+ this.lastRange = range;
36331
+ if (!this.ready) return;
36332
+ this.emit(range, false);
35970
36333
  }
35971
36334
  stop() {
35972
36335
  this.epoch += 1;
35973
- }
35974
- async evaluate(my, data, symbol, opts) {
35975
- const resolved = data.resolve(symbol);
35976
- const provider = resolved ? data.providerInstance(resolved.provider) : void 0;
35977
- const si = await data.symbolInfo(symbol).catch(() => void 0);
35978
- if (my !== this.epoch) return;
35979
- const hasSessions = typeof si?.session === "string" && si.session !== "" && si.session !== "24x7";
35980
- if (!provider?.getCalendar || !hasSessions || !resolved) {
35981
- this.onZones(null);
36336
+ this.ready = false;
36337
+ this.spec = null;
36338
+ this.covered = null;
36339
+ this.lastRange = null;
36340
+ }
36341
+ emit(range, force) {
36342
+ if (!this.spec) {
36343
+ if (force) this.onZones(null);
35982
36344
  return;
35983
36345
  }
35984
- if (opts.session !== "extended") {
35985
- this.onZones({ pre: [], post: [] });
36346
+ if (this.session !== "extended") {
36347
+ if (force) this.onZones({ pre: [], post: [] });
35986
36348
  return;
35987
36349
  }
35988
- const now = Date.now();
35989
- const lookback = Math.max(MIN_LOOKBACK_MS, Math.min(MAX_LOOKBACK_MS, opts.lookbackMs));
35990
- const range = { from: now - lookback, to: now + FETCH_AHEAD_MS2 };
35991
- const [regular, extended] = await Promise.all([
35992
- provider.getCalendar(resolved.ticker, { ...range, session: "regular" }).catch(() => null),
35993
- provider.getCalendar(resolved.ticker, { ...range, session: "extended" }).catch(() => null)
35994
- ]);
35995
- if (my !== this.epoch) return;
35996
- if (!regular || !extended) return;
35997
- this.onZones(deriveSessionZones(regular, extended));
36350
+ const from = Math.min(range.from, range.to);
36351
+ const to = Math.max(range.from, range.to);
36352
+ if (!Number.isFinite(from) || !Number.isFinite(to)) return;
36353
+ if (!force && this.covered && from >= this.covered.from && to <= this.covered.to) return;
36354
+ const pad = Math.max(to - from, COVER_PAD_MIN_MS);
36355
+ const covered = { from: from - pad, to: to + pad };
36356
+ this.covered = covered;
36357
+ this.onZones(expandSessionZones(this.spec, covered.from, covered.to));
35998
36358
  }
35999
36359
  };
36000
36360
 
@@ -36431,7 +36791,11 @@ var ChartCell = class {
36431
36791
  this.deps.toast(`No registered provider serves "${symbol2}" (registered: ${list})`, "error", 6e3);
36432
36792
  });
36433
36793
  this.inner.on("load:start", () => this.watermark?.setLoading(true));
36434
- this.inner.on("load:end", () => this.watermark?.setLoading(false));
36794
+ this.inner.on("load:end", () => {
36795
+ this.watermark?.setLoading(false);
36796
+ this.refreshSessionShading();
36797
+ });
36798
+ this.inner.on("viewport:changed", (range) => this.sessionShading.updateRange(range));
36435
36799
  const tz = deps.timezone();
36436
36800
  if (tz !== "Etc/UTC") this.inner.renderer.set("timezone", tz);
36437
36801
  this.indicatorTitlesOn = seed.indicatorTitles ?? true;
@@ -36441,12 +36805,15 @@ var ChartCell = class {
36441
36805
  this.watermarkOn = seed.watermark ?? deps.watermark;
36442
36806
  this.watermark = deps.watermark ? new Watermark(this.host, symbol ?? "", seed.timeframe ?? "60") : null;
36443
36807
  if (!this.watermarkOn) this.watermark?.setVisible(false);
36444
- this.statusline = deps.statusline ? new Statusline(this.host, symbol ?? "") : null;
36808
+ this.statusline = deps.statusline ? new Statusline(this.host, symbol ?? "", (sym) => this.inner?.data.symbolIcon(sym)) : null;
36445
36809
  this.statusline?.setMeta(seed.timeframe ?? "60", this.state.provider ?? "");
36446
36810
  this.statusline?.onChart(this.inner);
36447
36811
  this.marketStatus = this.statusline ? new MarketStatusTracker((s) => this.statusline?.setMarketStatus(s)) : null;
36448
36812
  void this.inner.data.ready().then(() => {
36449
- if (this.inner && this.state.symbol) this.statusline?.setMeta(this.state.timeframe ?? "60", this.inner.data.displayPrefix(this.state.symbol) ?? this.state.provider ?? "");
36813
+ if (this.inner && this.state.symbol) {
36814
+ this.statusline?.setSymbol(this.state.symbol);
36815
+ this.statusline?.setMeta(this.state.timeframe ?? "60", this.inner.data.displayPrefix(this.state.symbol) ?? this.state.provider ?? "");
36816
+ }
36450
36817
  this.refreshSessionAvailable();
36451
36818
  if (this.inner && this.state.symbol) this.marketStatus?.track(this.inner.data, this.state.symbol);
36452
36819
  });
@@ -36555,14 +36922,18 @@ var ChartCell = class {
36555
36922
  this.refreshSessionShading();
36556
36923
  });
36557
36924
  }
36558
- /** (Re)derive the pre/post-market shading bands for this cell's market. The loaded
36559
- * depth (bars × timeframe) bounds the calendar fetch; the tracker clamps it. */
36925
+ /** (Re)derive the pre/post-market shading bands for this cell's market. The bands
36926
+ * expand locally from the symbol's session vocabulary, so they paint as soon as
36927
+ * metadata is known and follow any pan depth without provider round trips. */
36560
36928
  refreshSessionShading() {
36561
36929
  const chart = this.inner;
36562
36930
  const symbol = this.state.symbol;
36563
36931
  if (!chart || !symbol) return;
36564
- const lookbackMs = Math.max(this.state.bars ?? 1e3, this.rangeBars) * timeframeMs(this.state.timeframe ?? "60");
36565
- this.sessionShading.track(chart.data, symbol, { session: this.session, lookbackMs });
36932
+ const now = Date.now();
36933
+ const requestedSpan = Math.max(this.state.bars ?? 1e3, this.rangeBars) * timeframeMs(this.state.timeframe ?? "60");
36934
+ const fallbackSpan = Number.isFinite(requestedSpan) ? Math.max(3 * 864e5, requestedSpan) : 3 * 864e5;
36935
+ const range = chart.getVisibleRange() ?? { from: now - fallbackSpan, to: now };
36936
+ this.sessionShading.track(chart.data, symbol, { session: this.session, range });
36566
36937
  }
36567
36938
  /** The session-shade colors live in the renderer CONFIG (persisted with it, edited
36568
36939
  * live by the dialog swatch) — the cell only proxies them into its settings rows. */
@@ -36658,10 +37029,10 @@ var ChartCell = class {
36658
37029
  id: "status-line",
36659
37030
  rows: [
36660
37031
  { kind: "heading", label: "Status line", id: "parts" },
36661
- { kind: "toggle", label: "Symbol name", id: "name", get: () => sl.partVisible("name"), set: (v) => sl.setPartVisible("name", v) },
36662
- { kind: "toggle", label: "Market status", id: "market", get: () => sl.partVisible("market"), set: (v) => sl.setPartVisible("market", v) },
36663
- { kind: "toggle", label: "OHLC values", id: "ohlc", get: () => sl.partVisible("ohlc"), set: (v) => sl.setPartVisible("ohlc", v) },
36664
- { kind: "toggle", label: "Bar change values", id: "change", get: () => sl.partVisible("change"), set: (v) => sl.setPartVisible("change", v) },
37032
+ { kind: "toggle", label: "Symbol name", id: "name", get: () => sl.partVisible("name"), set: (v) => this.setStatuslinePart("name", v) },
37033
+ { kind: "toggle", label: "Market status", id: "market", get: () => sl.partVisible("market"), set: (v) => this.setStatuslinePart("market", v) },
37034
+ { kind: "toggle", label: "OHLC values", id: "ohlc", get: () => sl.partVisible("ohlc"), set: (v) => this.setStatuslinePart("ohlc", v) },
37035
+ { kind: "toggle", label: "Bar change values", id: "change", get: () => sl.partVisible("change"), set: (v) => this.setStatuslinePart("change", v) },
36665
37036
  { kind: "heading", label: "Indicators", id: "indicators" },
36666
37037
  {
36667
37038
  kind: "toggle",
@@ -36696,12 +37067,40 @@ var ChartCell = class {
36696
37067
  this.indicatorTitlesOn = visible;
36697
37068
  this.inner?.renderer.set("indicatorTitles", visible);
36698
37069
  this.deps.onStateDirty();
37070
+ this.deps.onStatusPrefsChanged(this.id);
36699
37071
  }
36700
37072
  /** Show/hide the plot values beside this cell's legend titles (persisted per cell). */
36701
37073
  setIndicatorValuesVisible(visible) {
36702
37074
  this.indicatorValuesOn = visible;
36703
37075
  this.inner?.renderer.set("indicatorValues", visible);
36704
37076
  this.deps.onStateDirty();
37077
+ this.deps.onStatusPrefsChanged(this.id);
37078
+ }
37079
+ /** Show/hide one status-line segment (the settings dialog's Status line tab). */
37080
+ setStatuslinePart(part, visible) {
37081
+ this.statusline?.setPartVisible(part, visible);
37082
+ this.deps.onStatusPrefsChanged(this.id);
37083
+ }
37084
+ /** This cell's Status line tab prefs as one bundle (see {@link CellStatusPrefs}). */
37085
+ statusPrefs() {
37086
+ const sl = this.statusline;
37087
+ return {
37088
+ parts: sl ? { name: sl.partVisible("name"), market: sl.partVisible("market"), ohlc: sl.partVisible("ohlc"), change: sl.partVisible("change") } : null,
37089
+ indicatorTitles: this.indicatorTitlesOn,
37090
+ indicatorValues: this.indicatorValuesOn
37091
+ };
37092
+ }
37093
+ /** Converge this cell's Status line tab prefs to `prefs` — the follower half of
37094
+ * the workspace's style link. Idempotent: matching values change nothing, so a
37095
+ * propagated echo dies on its own. */
37096
+ applyStatusPrefs(prefs) {
37097
+ if (prefs.parts && this.statusline) {
37098
+ for (const part of Object.keys(prefs.parts)) {
37099
+ if (this.statusline.partVisible(part) !== prefs.parts[part]) this.statusline.setPartVisible(part, prefs.parts[part]);
37100
+ }
37101
+ }
37102
+ if (prefs.indicatorTitles !== this.indicatorTitlesOn) this.setIndicatorTitlesVisible(prefs.indicatorTitles);
37103
+ if (prefs.indicatorValues !== this.indicatorValuesOn) this.setIndicatorValuesVisible(prefs.indicatorValues);
36705
37104
  }
36706
37105
  /** The LIVE chart of this cell — never cache it across a layout change (the cell's
36707
37106
  * identity is what endures; the chart dies with the cell). */
@@ -37108,16 +37507,21 @@ var ChartCell = class {
37108
37507
 
37109
37508
  // src/workspace/context.ts
37110
37509
  function buildContext(host) {
37111
- const active = host.active();
37112
37510
  return {
37113
37511
  get chart() {
37114
37512
  const cell = host.active();
37115
37513
  if (!cell) throw new Error("VelaWorkspace has no active cell yet");
37116
37514
  return cell.chart;
37117
37515
  },
37118
- symbol: active?.symbol ?? "",
37119
- timeframe: active?.timeframe ?? "60",
37120
- priceStyle: active?.priceStyle ?? "candles",
37516
+ get symbol() {
37517
+ return host.active()?.symbol ?? "";
37518
+ },
37519
+ get timeframe() {
37520
+ return host.active()?.timeframe ?? "60";
37521
+ },
37522
+ get priceStyle() {
37523
+ return host.active()?.priceStyle ?? "candles";
37524
+ },
37121
37525
  setSymbol: (symbol) => host.active()?.setSymbol(symbol),
37122
37526
  setTimeframe: (tf) => host.active()?.setTimeframe(tf),
37123
37527
  setPriceStyle: (style) => host.active()?.setPriceStyle(style),
@@ -37128,8 +37532,12 @@ function buildContext(host) {
37128
37532
  addIndicator: (entry) => host.active()?.addExternalIndicator(entry),
37129
37533
  addNativeIndicator: (type) => host.active()?.addNative(type),
37130
37534
  stateChanged: () => host.stateDirty(),
37131
- cells: host.cells().map((c) => ({ id: c.id, chart: c.chart, symbol: c.symbol, timeframe: c.timeframe })),
37132
- activeCellId: active?.id ?? "",
37535
+ get cells() {
37536
+ return host.cells().map((c) => ({ id: c.id, chart: c.chart, symbol: c.symbol, timeframe: c.timeframe }));
37537
+ },
37538
+ get activeCellId() {
37539
+ return host.active()?.id ?? "";
37540
+ },
37133
37541
  setActiveCell: (id) => host.setActiveCell(id)
37134
37542
  };
37135
37543
  }
@@ -37462,6 +37870,10 @@ var VelaWorkspace = class {
37462
37870
  /** Same guard for the drawings link: the propagated mutations' own `drawing:*`
37463
37871
  * events fire synchronously inside the propagation loop and must not fan out again. */
37464
37872
  this.drawingSyncBusy = false;
37873
+ /** Same guard for the style link: a follower's `applyConfig` re-fires its
37874
+ * `onConfigChanged` in the same tick, and a state restore applies per-cell
37875
+ * configs that legitimately differ — neither must propagate. */
37876
+ this.styleSyncBusy = false;
37465
37877
  /** LINKED drawings (the drawings sync): one map per synced set (cellId → that
37466
37878
  * cell's drawing id), reachable from every member under its `cellId\0drawingId`
37467
37879
  * key — any member finds its peers to push edits/removals onto. Survives a
@@ -37522,13 +37934,14 @@ var VelaWorkspace = class {
37522
37934
  if (boot?.favorites) this.favs = [...boot.favorites];
37523
37935
  if (boot?.timeframeFavorites) this.tfFavs = [...boot.timeframeFavorites];
37524
37936
  const sync = boot?.sync ?? opts.sync;
37525
- for (const kind of ["viewport", "symbol", "timeframe", "crosshair", "drawings"]) {
37526
- this.applySyncSetting(kind, sync?.[kind]);
37527
- }
37937
+ for (const kind of SYNC_KINDS) this.applySyncSetting(kind, sync?.[kind]);
37528
37938
  this.monoLayout = opts.layout === false;
37529
37939
  const optLayout = opts.layout === false || opts.layout === void 0 ? "4" : opts.layout;
37530
37940
  this.def = this.resolveLayout(this.monoLayout ? "1" : boot?.layout && ensureLayout(boot.layout) ? boot.layout : optLayout);
37531
37941
  this.alertCap = Math.max(1, opts.alertCap ?? ALERT_CAP);
37942
+ this.topbarComp = resolveTopbarComposition(opts.topbar);
37943
+ this.indicatorsOverride = topbarActionOverride("indicators");
37944
+ this.screenshotOverride = topbarActionOverride("screenshot");
37532
37945
  if (boot?.trackSizes) for (const [id, ts] of Object.entries(boot.trackSizes)) this.trackSizes.set(id, ts);
37533
37946
  if (boot?.ext) this.extState = { ...boot.ext };
37534
37947
  if (boot?.charts) for (const { id, ...cs } of boot.charts) this.pool.set(id, cs);
@@ -37545,6 +37958,8 @@ var VelaWorkspace = class {
37545
37958
  });
37546
37959
  this.symbolPicker = new SymbolPicker({
37547
37960
  host: this.root,
37961
+ // Row icons come from each descriptor's OWNING provider (resolveSymbolIcon).
37962
+ iconFor: (d) => this.feed.symbolIconOf(d),
37548
37963
  onSelect: (ticker) => this.active.setSymbol(ticker),
37549
37964
  onOpenChange: (open2) => {
37550
37965
  if (open2) for (const cell of this.cells()) cell.chart.renderer.closeDialogs();
@@ -37552,7 +37967,7 @@ var VelaWorkspace = class {
37552
37967
  }
37553
37968
  });
37554
37969
  this.symbolPicker.setSource(() => this.feed.symbols());
37555
- this.indicatorPicker = opts.indicatorPicker !== false ? new IndicatorPicker({
37970
+ this.indicatorPicker = opts.indicatorPicker !== false && !this.indicatorsOverride && topbarHas(this.topbarComp, "indicators") ? new IndicatorPicker({
37556
37971
  host: this.root,
37557
37972
  library: () => this.active.libraryRows(),
37558
37973
  onChart: () => this.active.onChartRows(),
@@ -37571,6 +37986,9 @@ var VelaWorkspace = class {
37571
37986
  });
37572
37987
  this.topbar = new Topbar(this.root, {
37573
37988
  symbol: "",
37989
+ // RAW option, not the resolved lists — the bar distinguishes a host-declared
37990
+ // side (list is law) from a default one (an override's `order` may flow).
37991
+ composition: opts.topbar,
37574
37992
  onSymbolClick: () => this.symbolPicker.open(),
37575
37993
  ...picker ? { onIndicatorsClick: () => picker.open() } : {},
37576
37994
  onUndoClick: () => this.active.history.undo(),
@@ -37600,7 +38018,8 @@ var VelaWorkspace = class {
37600
38018
  syncs: () => [
37601
38019
  { id: "symbol", label: "Symbol", checked: this.syncOpts.symbol === true },
37602
38020
  { id: "timeframe", label: "Interval", checked: this.syncOpts.timeframe === true },
37603
- { id: "crosshair", label: "Crosshair", checked: this.syncOpts.crosshair === true }
38021
+ { id: "crosshair", label: "Crosshair", checked: this.syncOpts.crosshair === true },
38022
+ { id: "style", label: "Style", checked: this.syncOpts.style === true }
37604
38023
  ],
37605
38024
  onToggleSync: (id) => {
37606
38025
  const kind = id;
@@ -37628,7 +38047,7 @@ var VelaWorkspace = class {
37628
38047
  context: () => this.context(),
37629
38048
  changed: () => this.markStateDirty()
37630
38049
  });
37631
- this.objectTree = new ObjectTree(main);
38050
+ this.objectTree = new ObjectTree(main, (sym) => this.feed.symbolIcon(sym));
37632
38051
  this.dataWindow = new DataWindow(main);
37633
38052
  this.dock.addBuiltIn({ id: "dataWindow", title: "Data window", icon: "datawindow", order: 10, panel: this.dataWindow, onChart: (c) => this.dataWindow.onChart(c) });
37634
38053
  this.dock.addBuiltIn({ id: "objects", title: "Object tree", icon: "objects", order: 20, panel: this.objectTree, onChart: (c) => this.objectTree.onChart(c) });
@@ -37700,7 +38119,9 @@ var VelaWorkspace = class {
37700
38119
  timeframe: "60",
37701
38120
  onSymbolClick: () => this.symbolPicker.open(),
37702
38121
  onTimeframeClick: () => this.openTimeframeDrawer(),
37703
- ...picker ? { onIndicatorsClick: () => picker.open() } : {},
38122
+ // Same visibility truth as the desktop bar: composition-hidden
38123
+ // indicators lose their mobile stop too; an override takes it over.
38124
+ ...topbarHas(this.topbarComp, "indicators") && (picker || this.indicatorsOverride) ? { onIndicatorsClick: this.indicatorsOverride ? () => this.runOverride(this.indicatorsOverride) : () => picker.open() } : {},
37704
38125
  getContext: () => this.context(),
37705
38126
  ...this.drawingsEnabled ? { onDrawingsClick: () => this.openDrawingsDrawer() } : {},
37706
38127
  onMoreClick: () => this.openMoreDrawer(),
@@ -37882,7 +38303,7 @@ var VelaWorkspace = class {
37882
38303
  this.topbar.setTimeframeFavorites(this.tfFavs);
37883
38304
  }
37884
38305
  this.dock.applyState(st.panels);
37885
- for (const kind of ["viewport", "symbol", "timeframe", "crosshair", "drawings"]) this.applySyncSetting(kind, st.sync?.[kind]);
38306
+ for (const kind of SYNC_KINDS) this.applySyncSetting(kind, st.sync?.[kind]);
37886
38307
  this.trackSizes.clear();
37887
38308
  if (st.trackSizes) for (const [id, ts] of Object.entries(st.trackSizes)) this.trackSizes.set(id, ts);
37888
38309
  const targetDef = this.monoLayout ? this.def : ensureLayout(st.layout) ?? this.def;
@@ -37893,9 +38314,14 @@ var VelaWorkspace = class {
37893
38314
  for (const cell of this.cellsById.values()) cell.chart.drawings.setFavorites(this.favs);
37894
38315
  }
37895
38316
  this.drawingLinks.clear();
37896
- for (const [i] of this.def.cells.entries()) {
37897
- const { id, ...cs } = st.charts[i];
37898
- this.cellsById.get(id)?.rehydrate(cs);
38317
+ this.styleSyncBusy = true;
38318
+ try {
38319
+ for (const [i] of this.def.cells.entries()) {
38320
+ const { id, ...cs } = st.charts[i];
38321
+ this.cellsById.get(id)?.rehydrate(cs);
38322
+ }
38323
+ } finally {
38324
+ this.styleSyncBusy = false;
37899
38325
  }
37900
38326
  if (st.timezone) this.setTimezone(st.timezone);
37901
38327
  this.pool.clear();
@@ -37995,6 +38421,7 @@ var VelaWorkspace = class {
37995
38421
  const rebuildAll = nextBackend !== this.cellBackend;
37996
38422
  this.order = orderAfterLayout(this.order, next.cells.length, this.activeId);
37997
38423
  const keep = new Set(this.order.slice(0, next.cells.length));
38424
+ const preexisting = new Set(this.cellsById.keys());
37998
38425
  for (const [id, cell] of [...this.cellsById]) {
37999
38426
  if (!keep.has(id) || rebuildAll) {
38000
38427
  this.poolSet(id, cell.dehydrate());
@@ -38007,6 +38434,7 @@ var VelaWorkspace = class {
38007
38434
  this.cellBackend = nextBackend;
38008
38435
  this.applyGrid();
38009
38436
  this.buildCells();
38437
+ this.alignNewCellStyles(preexisting);
38010
38438
  this.syncCellPresentation();
38011
38439
  this.topbar.setLayout(next.id);
38012
38440
  const nextActive = activeAfterLayout(this.activeId, this.order.slice(0, next.cells.length));
@@ -38210,6 +38638,7 @@ var VelaWorkspace = class {
38210
38638
  onMarketChanged: (id2) => this.onCellMarketChanged(id2),
38211
38639
  onPriceStyleChanged: (id2) => this.onCellPriceStyleChanged(id2),
38212
38640
  onIndicatorsChanged: (id2) => this.onCellIndicatorsChanged(id2),
38641
+ onStatusPrefsChanged: (id2) => this.propagateStylePrefs(id2),
38213
38642
  onStateDirty: () => this.markStateDirty(),
38214
38643
  manifestSettled: () => this.manifestSettled,
38215
38644
  toast: (message, kind, durationMs) => this.toastHost.show(message, kind, durationMs)
@@ -38285,6 +38714,7 @@ var VelaWorkspace = class {
38285
38714
  this.drawToolbar?.setEraserActive(mode === "eraser");
38286
38715
  });
38287
38716
  chart.on("viewport:changed", (range) => this.propagateViewport(cell.id, range));
38717
+ chart.renderer.onConfigChanged(() => this.propagateStylePrefs(cell.id));
38288
38718
  chart.on("theme:changed", (t) => this.setTheme(t));
38289
38719
  chart.renderer.onAxisLongPress((e) => {
38290
38720
  if (this.layoutCtl.current !== "mobile") return;
@@ -38317,11 +38747,67 @@ var VelaWorkspace = class {
38317
38747
  if (kind === "viewport") {
38318
38748
  const range = this.cellsById.get(this.activeId)?.chart.getVisibleRange();
38319
38749
  if (range) this.propagateViewport(this.activeId, range);
38750
+ } else if (kind === "style") {
38751
+ this.propagateStylePrefs(this.activeId);
38320
38752
  } else {
38321
38753
  this.propagateMarket(this.activeId);
38322
38754
  }
38323
38755
  }
38324
38756
  }
38757
+ /**
38758
+ * Align cells minted by a layout change to their style group: with the link on, a
38759
+ * NEW cell (fresh slot or one returning from the pool, which missed edits while
38760
+ * dormant) inherits the presentation of a pre-existing group peer — the active
38761
+ * cell when it is one — instead of sitting on its own state beside a styled
38762
+ * group. Propagation runs FROM the peer, so a newborn's defaults never overwrite
38763
+ * the group, and the equality short-circuits keep converged peers untouched.
38764
+ */
38765
+ alignNewCellStyles(preexisting) {
38766
+ const setting = this.syncOpts.style;
38767
+ if (!setting) return;
38768
+ const ids = [...this.cellsById.keys()];
38769
+ const propagated = /* @__PURE__ */ new Set();
38770
+ for (const id of ids) {
38771
+ if (preexisting.has(id)) continue;
38772
+ const peers = syncTargets(id, setting, ids).filter((p) => preexisting.has(p));
38773
+ if (peers.length === 0) continue;
38774
+ const source = this.activeId && peers.includes(this.activeId) ? this.activeId : peers[0];
38775
+ if (propagated.has(source)) continue;
38776
+ propagated.add(source);
38777
+ this.propagateStylePrefs(source);
38778
+ }
38779
+ }
38780
+ /**
38781
+ * Mirror an origin cell's presentation — the Canvas + Scales-and-lines slice of
38782
+ * its renderer config plus its Status line tab prefs — onto its same-group
38783
+ * followers (the style link). Loop-safe two ways: the busy guard eats the
38784
+ * followers' SYNCHRONOUS echoes (their `applyConfig` re-fires `onConfigChanged`
38785
+ * in the same tick), and the equality short-circuits leave already-converged
38786
+ * followers untouched, so nothing re-emits once the group agrees.
38787
+ */
38788
+ propagateStylePrefs(originId) {
38789
+ if (this.styleSyncBusy || this.destroyed) return;
38790
+ const targets = syncTargets(originId, this.syncOpts.style, [...this.cellsById.keys()]);
38791
+ if (targets.length === 0) return;
38792
+ const origin = this.cellsById.get(originId);
38793
+ if (!origin) return;
38794
+ const slice = styleConfigSlice(origin.chart.renderer.getConfig());
38795
+ const sliceJson = slice ? JSON.stringify(slice) : null;
38796
+ const prefs = origin.statusPrefs();
38797
+ this.styleSyncBusy = true;
38798
+ try {
38799
+ for (const id of targets) {
38800
+ const cell = this.cellsById.get(id);
38801
+ if (!cell) continue;
38802
+ if (slice && sliceJson !== JSON.stringify(styleConfigSlice(cell.chart.renderer.getConfig()))) {
38803
+ cell.chart.renderer.applyConfig(slice);
38804
+ }
38805
+ cell.applyStatusPrefs(prefs);
38806
+ }
38807
+ } finally {
38808
+ this.styleSyncBusy = false;
38809
+ }
38810
+ }
38325
38811
  /**
38326
38812
  * Mirror an origin cell's pointer time onto its same-group followers as GHOST
38327
38813
  * crosshairs (`renderer.setExternalCrosshair`). The horizontal price level rides
@@ -38596,27 +39082,32 @@ var VelaWorkspace = class {
38596
39082
  this.drawingsDrawer.open();
38597
39083
  }
38598
39084
  openMoreDrawer() {
39085
+ const has = (id) => topbarHas(this.topbarComp, id);
38599
39086
  this.moreDrawer ?? (this.moreDrawer = new MoreDrawer({
38600
39087
  host: this.root,
38601
- onUndo: () => this.active.history.undo(),
38602
- onRedo: () => this.active.history.redo(),
38603
- onScreenshot: () => this.active.downloadScreenshot(),
39088
+ ...has("undo-redo") ? { onUndo: () => this.active.history.undo(), onRedo: () => this.active.history.redo() } : {},
39089
+ ...has("screenshot") ? { onScreenshot: this.screenshotOverride ? () => this.runOverride(this.screenshotOverride) : () => this.active.downloadScreenshot() } : {},
38604
39090
  canUndo: () => this.active.history.canUndo,
38605
39091
  canRedo: () => this.active.history.canRedo,
38606
39092
  priceStyles: () => priceStyleIds().map((id) => ({ id, label: priceStyleLabel(id), icon: priceStyleIcon(id) })),
38607
39093
  priceStyle: () => this.active.priceStyle,
38608
39094
  onPriceStyle: (id) => this.active.setPriceStyle(id),
38609
- panels: () => [...this.dock.list()],
39095
+ panels: () => has("panels") ? [...this.dock.list()] : [],
38610
39096
  onTogglePanel: (id) => this.dock.toggle(id),
38611
- alerts: () => this.alerts.map((a) => ({ title: `${a.source} \xB7 ${a.title}`, message: a.message, time: a.time })),
39097
+ ...has("alerts") ? { alerts: () => this.alerts.map((a) => ({ title: `${a.source} \xB7 ${a.title}`, message: a.message, time: a.time })) } : {},
38612
39098
  // Left-aligned actions have their own bottom-bar stop — only the rest
38613
- // lands in the drawer, or every left action would appear twice.
38614
- actions: () => widgetActions("topbar", this.context()).filter((a) => a.align !== "left").map((a) => ({ label: a.label, icon: a.icon, run: () => a.run(this.context()) })),
39099
+ // lands in the drawer, or every left action would appear twice. Built-in-id
39100
+ // actions are slot OVERRIDES: they reach the drawer through the slot's own
39101
+ // routed button (screenshot) or stop (indicators), never as an extra row.
39102
+ actions: () => {
39103
+ const builtin = new Set(TOPBAR_BUILTIN_IDS);
39104
+ 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()) }));
39105
+ },
38615
39106
  // The desktop layout dropdown's whole surface — the grid canvas, the
38616
39107
  // non-canvas presets and the sync switches — relocated into the kebab
38617
39108
  // drawer (the topbar is hidden on mobile). Same reads as the topbar block;
38618
- // single-chart mode omits it here too.
38619
- layout: this.monoLayout ? void 0 : {
39109
+ // single-chart mode and a composition without 'layout' omit it here too.
39110
+ layout: this.monoLayout || !has("layout") ? void 0 : {
38620
39111
  shape: () => layoutShape(this.def),
38621
39112
  presets: () => layouts().filter((l) => layoutShape(l) === null).map((l) => ({ id: l.id, label: l.label, checked: l.id === this.def.id })),
38622
39113
  onSelectGrid: (rows, cols) => this.setLayout(layoutForGrid(rows, cols)),
@@ -38624,7 +39115,8 @@ var VelaWorkspace = class {
38624
39115
  syncs: () => [
38625
39116
  { id: "symbol", label: "Symbol", checked: this.syncOpts.symbol === true },
38626
39117
  { id: "timeframe", label: "Interval", checked: this.syncOpts.timeframe === true },
38627
- { id: "crosshair", label: "Crosshair", checked: this.syncOpts.crosshair === true }
39118
+ { id: "crosshair", label: "Crosshair", checked: this.syncOpts.crosshair === true },
39119
+ { id: "style", label: "Style", checked: this.syncOpts.style === true }
38628
39120
  ],
38629
39121
  onToggleSync: (id) => {
38630
39122
  const kind = id;
@@ -38683,9 +39175,23 @@ var VelaWorkspace = class {
38683
39175
  }
38684
39176
  }
38685
39177
  }
39178
+ /** Invoke a slot override the way its button would: fresh context, `when` respected. */
39179
+ runOverride(action) {
39180
+ const ctx = this.context();
39181
+ if (!action.when || action.when(ctx)) action.run(ctx);
39182
+ }
38686
39183
  /** The default shortcut set — every binding acts on the ACTIVE cell. */
38687
39184
  registerDefaultKeys() {
38688
- this.keymap.register({ id: "chart.screenshot", keys: "mod+alt+s", label: "Download a chart screenshot", category: "Chart", run: () => this.active.downloadScreenshot() });
39185
+ if (topbarHas(this.topbarComp, "screenshot")) {
39186
+ const ov = this.screenshotOverride;
39187
+ this.keymap.register({
39188
+ id: "chart.screenshot",
39189
+ keys: "mod+alt+s",
39190
+ label: ov ? ov.label : "Download a chart screenshot",
39191
+ category: "Chart",
39192
+ run: ov ? () => this.runOverride(ov) : () => this.active.downloadScreenshot()
39193
+ });
39194
+ }
38689
39195
  this.keymap.register({ id: "chart.reset-view", keys: "alt+r", label: "Reset view (all history)", category: "Chart", run: () => this.active.chart.setVisibleRangePreset("ALL") });
38690
39196
  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")) });
38691
39197
  this.keymap.register({
@@ -38725,7 +39231,10 @@ var VelaWorkspace = class {
38725
39231
  this.keymap.register({ id: "view.zoom-out", keys: "mod+arrowdown", label: "Zoom out", category: "Chart", run: () => this.glider.zoom(ZOOM_OUT) });
38726
39232
  this.keymap.register({ id: "view.pan-left", keys: "mod+arrowleft", label: "Pan toward history", category: "Chart", run: () => this.active.chart.panBy(-PAN_FAST) });
38727
39233
  this.keymap.register({ id: "view.pan-right", keys: "mod+arrowright", label: "Pan toward now", category: "Chart", run: () => this.active.chart.panBy(PAN_FAST) });
38728
- if (this.indicatorPicker) {
39234
+ const indOv = this.indicatorsOverride;
39235
+ if (indOv && topbarHas(this.topbarComp, "indicators")) {
39236
+ this.keymap.register({ id: "indicators.open", keys: "/", label: indOv.label, category: "Indicators", run: () => this.runOverride(indOv) });
39237
+ } else if (this.indicatorPicker) {
38729
39238
  this.keymap.register({ id: "indicators.open", keys: "/", label: "Open the indicator picker", category: "Indicators", run: () => this.indicatorPicker?.open() });
38730
39239
  }
38731
39240
  this.keymap.register({
@@ -38881,6 +39390,7 @@ exports.DEFAULT_PANEL_ORDER = DEFAULT_PANEL_ORDER;
38881
39390
  exports.DEFAULT_PANEL_WIDTH = DEFAULT_PANEL_WIDTH;
38882
39391
  exports.DataWindow = DataWindow;
38883
39392
  exports.IndicatorPicker = IndicatorPicker;
39393
+ exports.OVERRIDABLE_TOPBAR_IDS = OVERRIDABLE_TOPBAR_IDS;
38884
39394
  exports.ObjectTree = ObjectTree;
38885
39395
  exports.PanelDock = PanelDock;
38886
39396
  exports.RANGE_PRESETS = RANGE_PRESETS;
@@ -38889,6 +39399,9 @@ exports.SidePanel = SidePanel;
38889
39399
  exports.Statusline = Statusline;
38890
39400
  exports.SymbolPicker = SymbolPicker;
38891
39401
  exports.TIMEZONES = TIMEZONES;
39402
+ exports.TOPBAR_BUILTIN_IDS = TOPBAR_BUILTIN_IDS;
39403
+ exports.TOPBAR_DEFAULT_LEFT = TOPBAR_DEFAULT_LEFT;
39404
+ exports.TOPBAR_DEFAULT_RIGHT = TOPBAR_DEFAULT_RIGHT;
38892
39405
  exports.TimeframeQuick = TimeframeQuick;
38893
39406
  exports.Topbar = Topbar;
38894
39407
  exports.VelaWidget = VelaWidget;
@@ -38904,17 +39417,23 @@ exports.fmtPrice = fmtPrice;
38904
39417
  exports.localStorageAdapter = localStorageAdapter;
38905
39418
  exports.normalizeTimezone = normalizeTimezone;
38906
39419
  exports.parseTimeframe = parseTimeframe;
39420
+ exports.pinnedTopbarActionIds = pinnedTopbarActionIds;
38907
39421
  exports.priceStyleLabel = priceStyleLabel;
38908
39422
  exports.registerSidePanel = registerSidePanel;
38909
39423
  exports.registerStatePersistence = registerStatePersistence;
39424
+ exports.registerSymbolRanking = registerSymbolRanking;
38910
39425
  exports.registerWidgetAction = registerWidgetAction;
38911
39426
  exports.registerWidgetAttachment = registerWidgetAttachment;
38912
39427
  exports.resolveIndicators = resolveIndicators;
39428
+ exports.resolveTopbarComposition = resolveTopbarComposition;
38913
39429
  exports.sanitizeState = sanitizeState;
38914
39430
  exports.sidePanels = sidePanels;
38915
39431
  exports.statePersistenceHandlers = statePersistenceHandlers;
39432
+ exports.symbolRanking = symbolRanking;
38916
39433
  exports.timeframeLabel = timeframeLabel;
38917
39434
  exports.timeframeMs = timeframeMs;
39435
+ exports.topbarActionOverride = topbarActionOverride;
39436
+ exports.topbarHas = topbarHas;
38918
39437
  exports.tzButtonLabel = tzButtonLabel;
38919
39438
  exports.tzMenuLabel = tzMenuLabel;
38920
39439
  exports.tzOffset = tzOffset;