@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
@@ -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,17 @@ 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();
5203
+ /** Tooltips of the CURRENT icon-only action buttons — rebuilt with every
5204
+ * renderActions pass (contributed buttons are replaceChildren'd away). */
5205
+ this.actionTooltips = [];
5206
+ /** `iconOnly` misuse warned once per action id (renderActions re-runs freely). */
5207
+ this.warnedIconless = /* @__PURE__ */ new Set();
5132
5208
  this.onHairlineSync = () => {
5133
5209
  if (this.hairlineRaf) return;
5134
5210
  const win = this.el.ownerDocument.defaultView;
@@ -5143,6 +5219,8 @@ var Topbar = class {
5143
5219
  this.host = host;
5144
5220
  this.timeframe = opts.timeframe;
5145
5221
  this.priceStyle = opts.priceStyle;
5222
+ this.comp = resolveTopbarComposition(opts.composition);
5223
+ const vis = (id) => topbarHas(this.comp, id);
5146
5224
  const doc = host.ownerDocument;
5147
5225
  injectStyles(STYLE_ID2, CSS3, doc);
5148
5226
  this.el = doc.createElement("div");
@@ -5165,7 +5243,7 @@ var Topbar = class {
5165
5243
  this.styleButton.className = "vela-widget-style";
5166
5244
  this.renderStyleButton(doc);
5167
5245
  let indicatorsBtn = null;
5168
- if (opts.onIndicatorsClick) {
5246
+ if (opts.onIndicatorsClick && !topbarActionOverride("indicators")) {
5169
5247
  indicatorsBtn = doc.createElement("button");
5170
5248
  indicatorsBtn.className = "vela-widget-indicators";
5171
5249
  indicatorsBtn.append(iconEl("indicators", doc), doc.createTextNode("Indicators"));
@@ -5178,18 +5256,23 @@ var Topbar = class {
5178
5256
  this.panelsHost = doc.createElement("span");
5179
5257
  this.panelsHost.className = "vela-widget-panels";
5180
5258
  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);
5259
+ if (vis("undo-redo")) {
5260
+ this.undoBtn = tool("vela-widget-undo", "undo", "Undo", opts.onUndoClick);
5261
+ this.redoBtn = tool("vela-widget-redo", "redo", "Redo", opts.onRedoClick);
5262
+ } else {
5263
+ this.undoBtn = doc.createElement("button");
5264
+ this.redoBtn = doc.createElement("button");
5265
+ }
5183
5266
  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");
5267
+ const screenshotBtn = vis("screenshot") && !topbarActionOverride("screenshot") ? tool("vela-widget-screenshot", "camera", "Download screenshot", opts.onScreenshotClick) : null;
5268
+ this.alertsBtn = vis("alerts") ? tool("vela-widget-alerts", "bell", "Alerts") : doc.createElement("button");
5186
5269
  this.alertsBtn.style.position = "relative";
5187
5270
  if (opts.onAlertsClick) this.alertsBtn.addEventListener("click", () => opts.onAlertsClick(this.alertsBtn));
5188
5271
  this.alertsBadge = doc.createElement("span");
5189
5272
  this.alertsBadge.className = "vela-alerts-badge";
5190
5273
  this.alertsBadge.style.display = "none";
5191
5274
  this.alertsBtn.appendChild(this.alertsBadge);
5192
- if (opts.layout) {
5275
+ if (opts.layout && vis("layout")) {
5193
5276
  this.layoutId = opts.layout.current;
5194
5277
  this.layoutButton = doc.createElement("button");
5195
5278
  this.layoutButton.className = "vela-widget-style";
@@ -5200,12 +5283,65 @@ var Topbar = class {
5200
5283
  d.className = "vela-sep";
5201
5284
  return d;
5202
5285
  };
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
5286
  this.leftActionsSep = sep();
5207
5287
  this.leftActionsSep.hidden = true;
5208
- this.el.append(...leading, this.leftActionsHost, this.leftActionsSep, this.undoBtn, this.redoBtn, this.actionsHost, this.alertsBtn, this.panelsHost, screenshotBtn);
5288
+ const primaries = /* @__PURE__ */ new Set(["symbol", "timeframes", "style", "layout", "indicators"]);
5289
+ const pinSlot = (id, left) => {
5290
+ const slot = doc.createElement("span");
5291
+ slot.className = "vela-widget-action-pin";
5292
+ this.pinned.set(id, { host: slot, left });
5293
+ return [slot];
5294
+ };
5295
+ const overridden = (id, left) => {
5296
+ const ov = topbarActionOverride(id);
5297
+ if (!ov) return null;
5298
+ const sideDeclared = left ? opts.composition?.left != null : opts.composition?.right != null;
5299
+ if (!sideDeclared && ov.order !== void 0) {
5300
+ this.flowingOverrides.add(id);
5301
+ return [];
5302
+ }
5303
+ return pinSlot(id, left);
5304
+ };
5305
+ const elementsFor = (id, left) => {
5306
+ switch (id) {
5307
+ case "symbol":
5308
+ return [this.symbolEl];
5309
+ case "timeframes":
5310
+ return [tfGroup];
5311
+ case "style":
5312
+ return [this.styleButton];
5313
+ case "layout":
5314
+ return this.layoutButton ? [this.layoutButton] : [];
5315
+ case "indicators":
5316
+ return overridden(id, left) ?? (indicatorsBtn ? [indicatorsBtn] : []);
5317
+ case "actions":
5318
+ return left ? [this.leftActionsHost, this.leftActionsSep] : [this.actionsHost];
5319
+ case "undo-redo":
5320
+ return [this.undoBtn, this.redoBtn];
5321
+ case "alerts":
5322
+ return [this.alertsBtn];
5323
+ case "panels":
5324
+ return [this.panelsHost];
5325
+ case "screenshot":
5326
+ return overridden(id, left) ?? (screenshotBtn ? [screenshotBtn] : []);
5327
+ default:
5328
+ return pinSlot(id, left);
5329
+ }
5330
+ };
5331
+ const sideEls = (list, left) => {
5332
+ const out = [];
5333
+ for (const [i, id] of list.entries()) {
5334
+ const els = elementsFor(id, left);
5335
+ if (els.length === 0) continue;
5336
+ out.push(...els);
5337
+ if (primaries.has(id) && i < list.length - 1) out.push(sep());
5338
+ }
5339
+ return out;
5340
+ };
5341
+ const right = doc.createElement("span");
5342
+ right.className = "vela-topbar-right";
5343
+ right.append(...sideEls(this.comp.right, false));
5344
+ this.el.append(...sideEls(this.comp.left, true), right);
5209
5345
  host.appendChild(this.el);
5210
5346
  this.renderTfChips();
5211
5347
  this.onHairlineSync();
@@ -5322,23 +5458,45 @@ var Topbar = class {
5322
5458
  else this.styleButton.appendChild(doc.createTextNode(priceStyleLabel(this.priceStyle)));
5323
5459
  this.styleButton.setAttribute("aria-label", `Chart style \u2014 ${priceStyleLabel(this.priceStyle)}`);
5324
5460
  }
5325
- /** Re-project the contributed topbar actions (call after registrations change). */
5461
+ /** Re-project the contributed topbar actions (call after registrations change).
5462
+ * An action PINNED by the composition renders into its named slot (list position
5463
+ * wins over `align`/`order`); the rest flow into the side's `actions` slot — or
5464
+ * not at all when an explicit list omits it (the list is the side's contract). */
5326
5465
  renderActions() {
5327
5466
  const ctx = this.opts.getContext?.();
5328
5467
  this.actionsHost.replaceChildren();
5329
5468
  this.leftActionsHost.replaceChildren();
5469
+ for (const pin of this.pinned.values()) pin.host.replaceChildren();
5470
+ for (const t of this.actionTooltips) t.destroy();
5471
+ this.actionTooltips = [];
5330
5472
  const doc = this.actionsHost.ownerDocument;
5473
+ const flowLeft = this.comp.left.includes("actions");
5474
+ const flowRight = this.comp.right.includes("actions");
5475
+ const builtin = new Set(TOPBAR_BUILTIN_IDS);
5331
5476
  for (const action of widgetActions("topbar", ctx)) {
5332
- const left = action.align === "left";
5477
+ const pin = this.pinned.get(action.id);
5478
+ if (!pin && builtin.has(action.id) && !this.flowingOverrides.has(action.id)) continue;
5479
+ const left = pin ? pin.left : action.align === "left";
5480
+ if (!pin && !(left ? flowLeft : flowRight)) continue;
5481
+ const iconOnly = action.iconOnly === true && !!action.icon;
5482
+ if (action.iconOnly === true && !action.icon && !this.warnedIconless.has(action.id)) {
5483
+ this.warnedIconless.add(action.id);
5484
+ console.warn(`[vela] widget action "${action.id}": iconOnly needs an \`icon\` \u2014 rendering the label instead.`);
5485
+ }
5333
5486
  const b = doc.createElement("button");
5334
- b.className = left ? "vela-widget-action-left" : "vela-widget-action";
5487
+ b.className = left ? "vela-widget-action-left" : iconOnly ? "vela-widget-tool" : "vela-widget-action";
5335
5488
  if (action.icon) b.appendChild(iconEl(action.icon, doc));
5336
- b.appendChild(doc.createTextNode(action.label));
5489
+ if (iconOnly) {
5490
+ b.setAttribute("aria-label", action.label);
5491
+ this.actionTooltips.push(new Tooltip(b, { content: action.label, triggerId: `vela-action-${action.id}`, host: this.host }));
5492
+ } else {
5493
+ b.appendChild(doc.createTextNode(action.label));
5494
+ }
5337
5495
  b.addEventListener("click", () => {
5338
5496
  const c = this.opts.getContext?.();
5339
5497
  if (c) action.run(c);
5340
5498
  });
5341
- (left ? this.leftActionsHost : this.actionsHost).appendChild(b);
5499
+ (pin ? pin.host : left ? this.leftActionsHost : this.actionsHost).appendChild(b);
5342
5500
  }
5343
5501
  this.leftActionsSep.hidden = this.leftActionsHost.childElementCount === 0;
5344
5502
  this.onHairlineSync();
@@ -5384,7 +5542,7 @@ var Topbar = class {
5384
5542
  this.tfMenu.destroy();
5385
5543
  this.styleMenu.destroy();
5386
5544
  this.layoutPicker?.destroy();
5387
- for (const t of [...this.tooltips, ...this.panelTooltips]) t.destroy();
5545
+ for (const t of [...this.tooltips, ...this.panelTooltips, ...this.actionTooltips]) t.destroy();
5388
5546
  this.el.remove();
5389
5547
  }
5390
5548
  /** One icon-only tool button with its kit tooltip, parked in `sink` for disposal. */
@@ -5731,41 +5889,39 @@ var Bottombar = class {
5731
5889
  }
5732
5890
  };
5733
5891
 
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
- }
5892
+ // src/data/symbol-base.ts
5742
5893
  function baseOf(d) {
5743
5894
  const fromDesc = d.description?.split("/")[0]?.trim();
5744
5895
  if (fromDesc) return fromDesc.replace(/\s+Perpetual$/i, "");
5745
5896
  return d.ticker.replace(/[-_/]?(USDT|USDC|USD1|USDS|BUSD|USD|EUR|PERP)$/i, "") || d.ticker;
5746
5897
  }
5747
- function tickerIconEl(doc, base, name, className) {
5898
+
5899
+ // src/widget/symbol-icon.ts
5900
+ var iconFailed = /* @__PURE__ */ new Set();
5901
+ function initialsOf(name) {
5902
+ return (name || "?").replace(/[^A-Za-z0-9]/g, "").slice(0, 2).toUpperCase() || "?";
5903
+ }
5904
+ function tickerIconEl(doc, base, name, className, iconUrl) {
5748
5905
  const wrap = doc.createElement("span");
5749
5906
  wrap.className = className;
5750
- const key = base.toUpperCase();
5751
5907
  const fallback = () => {
5752
5908
  wrap.replaceChildren();
5753
5909
  wrap.style.background = categoricalColor(name);
5754
5910
  wrap.textContent = initialsOf(base || name);
5755
5911
  };
5756
- if (!key || iconFailed.has(key)) {
5912
+ if (!iconUrl || iconFailed.has(iconUrl)) {
5757
5913
  fallback();
5758
5914
  return wrap;
5759
5915
  }
5760
5916
  const img = doc.createElement("img");
5761
5917
  img.alt = "";
5762
5918
  img.crossOrigin = "anonymous";
5763
- img.src = cryptoIconUrl(key);
5919
+ img.src = iconUrl;
5764
5920
  img.style.cssText = "width:100%;height:100%;border-radius:50%;display:block;object-fit:cover;";
5765
5921
  img.addEventListener(
5766
5922
  "error",
5767
5923
  () => {
5768
- iconFailed.add(key);
5924
+ iconFailed.add(iconUrl);
5769
5925
  fallback();
5770
5926
  },
5771
5927
  { once: true }
@@ -12018,11 +12174,14 @@ var MenuBuilder = class {
12018
12174
  }
12019
12175
  };
12020
12176
  var ObjectTree = class extends SidePanel {
12021
- constructor(host) {
12177
+ constructor(host, iconFor) {
12022
12178
  super(host, "Object tree", "vela-ot");
12179
+ this.iconFor = iconFor;
12023
12180
  this.chart = null;
12024
12181
  this.selectedDrawing = null;
12025
12182
  this.symbolName = "";
12183
+ /** The raw (possibly venue-prefixed) symbol — what icon resolution routes on. */
12184
+ this.symbolRaw = "";
12026
12185
  /** Drawing bundles — a view-side grouping, held for the panel's lifetime and never persisted.
12027
12186
  * Kept per chart because a workspace points this one panel at whichever chart is active, and
12028
12187
  * each chart's bundles have to survive the switch. */
@@ -12059,6 +12218,7 @@ var ObjectTree = class extends SidePanel {
12059
12218
  if (open2) this.refresh();
12060
12219
  }
12061
12220
  setSymbol(symbol) {
12221
+ this.symbolRaw = symbol;
12062
12222
  this.symbolName = parseSymbol(symbol).ticker;
12063
12223
  }
12064
12224
  /** (Re)bind to a chart instance — called after every widget rebuild. */
@@ -12259,7 +12419,7 @@ var ObjectTree = class extends SidePanel {
12259
12419
  const { chart } = pass;
12260
12420
  if (row.kind === "price") {
12261
12421
  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");
12422
+ const icon2 = tickerIconEl(doc, base || "P", this.symbolName || "Price", "vela-ot-avatar", this.symbolRaw ? this.iconFor?.(this.symbolRaw) : void 0);
12263
12423
  const el2 = this.row(icon2, row.label, row.visible, [
12264
12424
  {
12265
12425
  icon: row.visible ? "eye" : "eye-off",
@@ -13289,8 +13449,6 @@ var PanelDock = class {
13289
13449
  for (const entry of this.entries) this.deps.chrome.setPanelActive(entry.id, entry.panel.open);
13290
13450
  }
13291
13451
  };
13292
-
13293
- // src/widget/symbol-picker.ts
13294
13452
  var TOP_TICKERS = ["BTCUSDT", "ETHUSDT", "SOLUSDT", "BNBUSDT", "XRPUSDT", "DOGEUSDT", "ADAUSDT", "LINKUSDT"];
13295
13453
  function parseQuery(raw, venues) {
13296
13454
  const m = raw.match(/^\s*([^\s:]+)\s*[:\s]\s*(.*)$/);
@@ -13305,17 +13463,18 @@ function onlyOne(matches) {
13305
13463
  return matches.length === 1 ? matches[0] : null;
13306
13464
  }
13307
13465
  var venueOf = (s) => s.prefix ?? s.provider;
13308
- function filterSymbols(list, query, limit = 100) {
13466
+ function filterSymbols(list, query, limit = 100, top = TOP_TICKERS) {
13309
13467
  const venues = [...new Set(list.flatMap((s) => [venueOf(s)?.toLowerCase(), s.provider?.toLowerCase()]).filter((p) => !!p))];
13310
13468
  const { scope, term } = parseQuery(query, venues);
13311
13469
  const pool = scope ? list.filter((s) => venueOf(s)?.toLowerCase() === scope || s.provider?.toLowerCase() === scope) : list;
13312
13470
  const q = term.toUpperCase();
13313
13471
  if (!q) {
13314
13472
  if (scope) return [...pool].sort((a, b) => a.ticker.localeCompare(b.ticker)).slice(0, limit);
13473
+ if (top === false) return pool.slice(0, limit);
13315
13474
  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);
13475
+ const pinned = top.map((t) => byTicker.get(t)).filter((s) => s !== void 0);
13476
+ const rest = pool.filter((s) => !top.includes(s.ticker.toUpperCase()));
13477
+ return [...pinned, ...rest].slice(0, limit);
13319
13478
  }
13320
13479
  const qLower = term.toLowerCase();
13321
13480
  const prefix = [];
@@ -13425,12 +13584,16 @@ var CSS8 = `
13425
13584
  var PAGE = 100;
13426
13585
  var SymbolPicker = class {
13427
13586
  constructor(opts) {
13587
+ this.opts = opts;
13428
13588
  this.source = () => [];
13429
13589
  this.rows = [];
13430
13590
  this.highlighted = 0;
13431
13591
  this.seed = "";
13432
13592
  this.activeTab = "All";
13433
13593
  this.visible = PAGE;
13594
+ /** The ranked pool cache — `key` fingerprints the raw pool the ranking ran on. */
13595
+ this.ranked = null;
13596
+ this.ranking = false;
13434
13597
  const doc = (opts.host ?? document.body).ownerDocument;
13435
13598
  injectStyles(STYLE_ID7, CSS8, doc);
13436
13599
  this.input = doc.createElement("input");
@@ -13528,10 +13691,19 @@ var SymbolPicker = class {
13528
13691
  } else delete el.dataset.highlighted;
13529
13692
  });
13530
13693
  }
13694
+ /** The picker's pool: the source, shaped by the registered symbol ranking. Cached —
13695
+ * the hook runs when the pool CHANGES (an index lands or refreshes), never per
13696
+ * keystroke; an async hook resolves onto the next repaint (stale-while-revalidate
13697
+ * in between, the raw pool before the first resolve). */
13698
+ pool() {
13699
+ const raw = this.source();
13700
+ return raw;
13701
+ }
13531
13702
  computeRows() {
13532
13703
  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);
13704
+ const all = this.pool();
13705
+ const pool = this.activeTab === "All" ? all : all.filter((s) => TAB_TYPES[this.activeTab]?.includes((s.type ?? "").toLowerCase()) || this.activeTab === "Crypto" && (s.type ?? "").toLowerCase() === "futures");
13706
+ return filterSymbols(pool, this.input.value, this.visible, TOP_TICKERS);
13535
13707
  }
13536
13708
  refresh() {
13537
13709
  const doc = this.list.ownerDocument;
@@ -13562,7 +13734,7 @@ var SymbolPicker = class {
13562
13734
  row.dataset.ticker = s.ticker;
13563
13735
  const venue = s.prefix ?? s.provider;
13564
13736
  if (venue) row.dataset.venue = venue;
13565
- const av = tickerIconEl(doc, baseOf(s), s.ticker, "vela-sp-avatar");
13737
+ const av = tickerIconEl(doc, baseOf(s), s.ticker, "vela-sp-avatar", this.opts.iconFor?.(s));
13566
13738
  const main = doc.createElement("span");
13567
13739
  main.className = "vela-sp-main";
13568
13740
  const t = doc.createElement("span");
@@ -14243,7 +14415,8 @@ var MobileBar = class {
14243
14415
  if (!ctx) return;
14244
14416
  const doc = this.el.ownerDocument;
14245
14417
  this.actionsHost.replaceChildren();
14246
- for (const action of widgetActions("topbar", ctx).filter((a) => a.align === "left")) {
14418
+ const builtin = new Set(TOPBAR_BUILTIN_IDS);
14419
+ for (const action of widgetActions("topbar", ctx).filter((a) => a.align === "left" && !builtin.has(a.id))) {
14247
14420
  const b = doc.createElement("button");
14248
14421
  b.className = "vela-mb-item";
14249
14422
  b.setAttribute("aria-label", action.label);
@@ -14772,10 +14945,10 @@ var MoreDrawer = class {
14772
14945
  });
14773
14946
  actions.appendChild(b);
14774
14947
  };
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);
14948
+ if (this.opts.onUndo) action("undo", "Undo", this.opts.canUndo(), this.opts.onUndo);
14949
+ if (this.opts.onRedo) action("redo", "Redo", this.opts.canRedo(), this.opts.onRedo);
14950
+ if (this.opts.onScreenshot) action("camera", "Screenshot", true, this.opts.onScreenshot);
14951
+ if (actions.childElementCount > 0) this.drawer.body.appendChild(actions);
14779
14952
  const list = doc.createElement("div");
14780
14953
  list.className = "vela-md-list";
14781
14954
  const current = this.opts.priceStyles().find((s) => s.id === this.opts.priceStyle());
@@ -14796,8 +14969,10 @@ var MoreDrawer = class {
14796
14969
  })
14797
14970
  );
14798
14971
  }
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") }));
14972
+ if (this.opts.alerts) {
14973
+ const alertCount = this.opts.alerts().length;
14974
+ list.appendChild(this.row(doc, "Alerts", { icon: "bell", value: alertCount > 0 ? String(alertCount) : void 0, chevron: true, onClick: () => this.show("alerts") }));
14975
+ }
14801
14976
  for (const act of this.opts.actions()) {
14802
14977
  list.appendChild(
14803
14978
  this.row(doc, act.label, {
@@ -14878,7 +15053,7 @@ var MoreDrawer = class {
14878
15053
  this.drawer.body.appendChild(list);
14879
15054
  }
14880
15055
  renderAlerts(doc) {
14881
- const alerts = this.opts.alerts();
15056
+ const alerts = this.opts.alerts?.() ?? [];
14882
15057
  if (alerts.length === 0) {
14883
15058
  const empty = doc.createElement("div");
14884
15059
  empty.className = "vela-md-empty";
@@ -17075,19 +17250,8 @@ function registerBuiltinChartTypes() {
17075
17250
  registerChartType({ id: "heikinashi", label: "Heikin Ashi", barTransform: HEIKIN_ASHI });
17076
17251
  }
17077
17252
 
17078
- // src/workspace/sync.ts
17079
- function syncTargets(originId, setting, cellIds) {
17080
- if (setting == null || setting === false) return [];
17081
- if (setting === true) return cellIds.filter((id) => id !== originId);
17082
- const group = setting[originId];
17083
- if (group == null) return [];
17084
- return cellIds.filter((id) => id !== originId && setting[id] === group);
17085
- }
17086
- function rangesWithin(a, b, epsMs) {
17087
- return Math.abs(a.from - b.from) <= epsMs && Math.abs(a.to - b.to) <= epsMs;
17088
- }
17089
-
17090
17253
  // src/state/document.ts
17254
+ var SYNC_KINDS = ["viewport", "symbol", "timeframe", "crosshair", "drawings", "style"];
17091
17255
  function prefixedSymbol(cell) {
17092
17256
  if (!cell?.symbol) return void 0;
17093
17257
  if (cell.symbol.includes(":") || !cell.provider) return cell.symbol;
@@ -17175,7 +17339,7 @@ function sanitizeSync(raw) {
17175
17339
  if (raw == null || typeof raw !== "object") return null;
17176
17340
  const s = raw;
17177
17341
  const out = {};
17178
- for (const kind of ["viewport", "symbol", "timeframe", "crosshair", "drawings"]) {
17342
+ for (const kind of SYNC_KINDS) {
17179
17343
  const v = s[kind];
17180
17344
  if (v === true) out[kind] = true;
17181
17345
  else if (v != null && typeof v === "object") {
@@ -17218,6 +17382,28 @@ function sanitizeTrackSizes(raw) {
17218
17382
  return Object.keys(out).length > 0 ? out : null;
17219
17383
  }
17220
17384
 
17385
+ // src/workspace/sync.ts
17386
+ function syncTargets(originId, setting, cellIds) {
17387
+ if (setting == null || setting === false) return [];
17388
+ if (setting === true) return cellIds.filter((id) => id !== originId);
17389
+ const group = setting[originId];
17390
+ if (group == null) return [];
17391
+ return cellIds.filter((id) => id !== originId && setting[id] === group);
17392
+ }
17393
+ function rangesWithin(a, b, epsMs) {
17394
+ return Math.abs(a.from - b.from) <= epsMs && Math.abs(a.to - b.to) <= epsMs;
17395
+ }
17396
+ var STYLE_SYNC_CONFIG_KEYS = ["layout", "panes", "grid", "priceScale", "crosshair"];
17397
+ function styleConfigSlice(config) {
17398
+ if (config == null || typeof config !== "object") return null;
17399
+ const doc = config;
17400
+ const out = {};
17401
+ for (const key of STYLE_SYNC_CONFIG_KEYS) {
17402
+ if (doc[key] != null && typeof doc[key] === "object") out[key] = doc[key];
17403
+ }
17404
+ return Object.keys(out).length > 0 ? out : null;
17405
+ }
17406
+
17221
17407
  // src/workspace/persist.ts
17222
17408
  var memoryStore = /* @__PURE__ */ new Map();
17223
17409
  function memoryStorageAdapter() {
@@ -19277,6 +19463,12 @@ var DataControl = class {
19277
19463
  symbols(provider) {
19278
19464
  return this.registry?.symbols(provider) ?? [];
19279
19465
  }
19466
+ /** The icon URL for `symbol` — its owning provider's `resolveSymbolIcon`, routed
19467
+ * through resolution. Undefined while unresolvable, when the provider declares no
19468
+ * resolver, or on a custom `deps.dataFeed` — the shells then show initials. */
19469
+ symbolIcon(symbol) {
19470
+ return this.registry?.symbolIcon(symbol);
19471
+ }
19280
19472
  /** Per-symbol metadata (Pine `syminfo.*`), resolved through the owning provider. */
19281
19473
  symbolInfo(symbol) {
19282
19474
  return this.registry?.symbolInfoFor(symbol) ?? Promise.resolve(void 0);
@@ -19566,12 +19758,16 @@ var IndicatorInputsDialog = class {
19566
19758
  closeOnEscape: false,
19567
19759
  footer: (foot) => {
19568
19760
  foot.append(
19761
+ this.resetAction(),
19569
19762
  this.dialogButton("Cancel", false, () => this.revertAndClose()),
19570
19763
  this.dialogButton("Ok", true, () => this.close())
19571
19764
  );
19572
19765
  },
19766
+ // Stale-guard: destroying a dialog fires its machine's onOpenChange(false)
19767
+ // asynchronously — after a reset rebuild that notification must not close
19768
+ // the replacement dialog.
19573
19769
  onOpenChange: (open2) => {
19574
- if (!open2) this.close();
19770
+ if (!open2 && this.uiDialog === ui) this.close();
19575
19771
  }
19576
19772
  });
19577
19773
  applyChromeTokens(ui.panel, t);
@@ -19702,6 +19898,30 @@ var IndicatorInputsDialog = class {
19702
19898
  }
19703
19899
  this.close();
19704
19900
  }
19901
+ /** The footer's reset button — same chip as Cancel, pinned to the LEFT edge
19902
+ * (`margin-right:auto` against the footer's flex-end keeps Cancel/Ok right). */
19903
+ resetAction() {
19904
+ const b = this.dialogButton("Reset defaults", false, () => this.resetToDefaults());
19905
+ b.style.marginRight = "auto";
19906
+ return b;
19907
+ }
19908
+ /** Restore every input to its declared default (re-running the indicator), then
19909
+ * re-open the form so each control re-reads the restored values — the same
19910
+ * rebuild-after-reset move as the chart-settings dialog. The open-time snapshot
19911
+ * survives the rebuild, so Cancel after a reset still reverts the whole session. */
19912
+ resetToDefaults() {
19913
+ const row = this.row;
19914
+ if (!row) return;
19915
+ const snap = this.snapshot;
19916
+ for (const inp of row.inputs) {
19917
+ if (row.values[inp.key] !== inp.defval) {
19918
+ row.values[inp.key] = inp.defval;
19919
+ this.host.onChange?.({ indicatorId: row.id, key: inp.key, value: inp.defval });
19920
+ }
19921
+ }
19922
+ this.open(row);
19923
+ if (snap) this.snapshot = snap;
19924
+ }
19705
19925
  /** Write one edit through: store it, notify the host, and re-apply the `when` gates. */
19706
19926
  commit(row, key, value) {
19707
19927
  row.values[key] = value;
@@ -31135,6 +31355,11 @@ function resizeSplit(split, dyTotal, minPx = MIN_PANE_PX) {
31135
31355
  }
31136
31356
 
31137
31357
  // src/renderers/native/backdrop/BackdropRenderer.ts
31358
+ function clipHighlightRect(x1, x2, left, right) {
31359
+ const x = Math.max(left, x1);
31360
+ const end = Math.min(right, x2);
31361
+ return end > x ? { x, width: end - x } : null;
31362
+ }
31138
31363
  var BackdropRenderer = class {
31139
31364
  constructor() {
31140
31365
  this.canvas = null;
@@ -31167,17 +31392,22 @@ var BackdropRenderer = class {
31167
31392
  /** Renderer-owned session highlight bands: full-height (all panes), behind the grid.
31168
31393
  * Session-zone washes (pre/post-market) paint first, host highlights on top. */
31169
31394
  drawHighlights(ctx, scene, coords) {
31170
- const bands = [...scene.sessionHighlightBands(), ...scene.highlights];
31171
- if (bands.length === 0) return;
31395
+ const sessions = scene.sessionHighlightBands();
31396
+ if (sessions.length > 0) {
31397
+ const left = Math.max(0, coords.logicalToX(-0.5));
31398
+ const right = Math.min(coords.width, coords.logicalToX(coords.barCount - 0.5));
31399
+ this.drawHighlightSet(ctx, sessions, coords, left, right);
31400
+ }
31401
+ this.drawHighlightSet(ctx, scene.highlights, coords, 0, coords.width);
31402
+ }
31403
+ drawHighlightSet(ctx, bands, coords, left, right) {
31172
31404
  for (const band of bands) {
31173
31405
  const x1 = coords.timeToX(band.from);
31174
31406
  const x2 = coords.timeToX(band.to);
31175
- if (x2 < 0 || x1 > coords.width || x2 <= x1) continue;
31176
- const cx = Math.max(0, x1);
31177
- const cw = Math.min(coords.width, x2) - cx;
31178
- if (cw <= 0) continue;
31407
+ const rect = clipHighlightRect(x1, x2, left, right);
31408
+ if (!rect) continue;
31179
31409
  ctx.fillStyle = band.color;
31180
- ctx.fillRect(cx, 0, cw, coords.height);
31410
+ ctx.fillRect(rect.x, 0, rect.width, coords.height);
31181
31411
  }
31182
31412
  }
31183
31413
  // ── grid ── vert/horz gate on `scene.showGrid` AND their own per-axis visibility
@@ -35625,8 +35855,9 @@ function statuslineInkOf(renderer, priceStyle) {
35625
35855
  }
35626
35856
  }
35627
35857
  var Statusline = class {
35628
- constructor(host, symbol) {
35858
+ constructor(host, symbol, iconFor) {
35629
35859
  this.host = host;
35860
+ this.iconFor = iconFor;
35630
35861
  this.parts = { name: true, market: true, ohlc: true, change: true };
35631
35862
  this.lastBar = null;
35632
35863
  this.hoverBar = null;
@@ -35650,7 +35881,7 @@ var Statusline = class {
35650
35881
  this.el.className = "vela-statusline";
35651
35882
  this.el.dataset.velaScreenshot = "1";
35652
35883
  const ticker = parseSymbol(symbol).ticker;
35653
- this.avatarEl = tickerIconEl(doc, baseOfTicker(ticker), ticker, "vela-sl-avatar");
35884
+ this.avatarEl = tickerIconEl(doc, baseOfTicker(ticker), ticker, "vela-sl-avatar", this.iconFor?.(symbol));
35654
35885
  this.symbolEl = doc.createElement("span");
35655
35886
  this.symbolEl.className = "vela-sl-symbol";
35656
35887
  this.symbolEl.textContent = ticker;
@@ -35671,7 +35902,7 @@ var Statusline = class {
35671
35902
  setSymbol(symbol) {
35672
35903
  const ticker = parseSymbol(symbol).ticker;
35673
35904
  this.symbolEl.textContent = ticker;
35674
- const fresh = tickerIconEl(this.el.ownerDocument, baseOfTicker(ticker), ticker, "vela-sl-avatar");
35905
+ const fresh = tickerIconEl(this.el.ownerDocument, baseOfTicker(ticker), ticker, "vela-sl-avatar", this.iconFor?.(symbol));
35675
35906
  this.avatarEl.replaceWith(fresh);
35676
35907
  this.avatarEl = fresh;
35677
35908
  this.fit();
@@ -35918,61 +36149,144 @@ var MarketStatusTracker = class {
35918
36149
  };
35919
36150
 
35920
36151
  // src/widget/session-shading.ts
35921
- function deriveSessionZones(regular, extended) {
36152
+ var DAY_MS2 = 864e5;
36153
+ var MINUTE_MS = 6e4;
36154
+ function parseWindow(text) {
36155
+ if (typeof text !== "string") return null;
36156
+ const m = /^(\d{2})(\d{2})-(\d{2})(\d{2})$/.exec(text);
36157
+ if (!m) return null;
36158
+ const start = Number(m[1]) * 60 + Number(m[2]);
36159
+ const end = Number(m[3]) * 60 + Number(m[4]);
36160
+ if (start >= end || end > 1440) return null;
36161
+ return { start, end };
36162
+ }
36163
+ function parseSessionSpec(si) {
36164
+ const session = si?.["session"];
36165
+ if (typeof session !== "string" || session === "" || session === "24x7") return null;
36166
+ const regular = parseWindow(session);
36167
+ if (!regular) return null;
36168
+ const tz = si?.["timezone"];
36169
+ const timezone = typeof tz === "string" && tz !== "" ? tz : "Etc/UTC";
36170
+ const ext = parseWindow(si?.["session_extended"]);
36171
+ const extended = ext && ext.start <= regular.start && ext.end >= regular.end ? ext : { start: 0, end: 1440 };
36172
+ return { regular, extended, timezone };
36173
+ }
36174
+ var dtfCache = /* @__PURE__ */ new Map();
36175
+ function civilFormatter(tz) {
36176
+ const cached = dtfCache.get(tz);
36177
+ if (cached !== void 0) return cached;
36178
+ let dtf = null;
36179
+ try {
36180
+ dtf = new Intl.DateTimeFormat("en-US", {
36181
+ timeZone: tz,
36182
+ hourCycle: "h23",
36183
+ weekday: "short",
36184
+ year: "numeric",
36185
+ month: "2-digit",
36186
+ day: "2-digit",
36187
+ hour: "2-digit",
36188
+ minute: "2-digit"
36189
+ });
36190
+ } catch {
36191
+ dtf = null;
36192
+ }
36193
+ dtfCache.set(tz, dtf);
36194
+ return dtf;
36195
+ }
36196
+ function civilParts(dtf, ms) {
36197
+ const parts = dtf.formatToParts(ms);
36198
+ const get = (t) => parts.find((p) => p.type === t)?.value ?? "";
36199
+ return {
36200
+ weekday: get("weekday"),
36201
+ year: Number(get("year")),
36202
+ month: Number(get("month")),
36203
+ day: Number(get("day")),
36204
+ hour: Number(get("hour")) % 24,
36205
+ minute: Number(get("minute"))
36206
+ };
36207
+ }
36208
+ function expandSessionZones(spec, from, to) {
35922
36209
  const pre = [];
35923
36210
  const post = [];
35924
- for (const [extStart, extEnd] of extended) {
35925
- const inside = regular.filter(([s, e]) => e > extStart && s < extEnd).sort((a, b) => a[0] - b[0]);
35926
- let cursor = extStart;
35927
- for (const [regStart, regEnd] of inside) {
35928
- if (regStart > cursor) pre.push([cursor, Math.min(regStart, extEnd)]);
35929
- cursor = Math.max(cursor, regEnd);
35930
- }
35931
- if (cursor < extEnd) post.push([cursor, extEnd]);
36211
+ const dtf = civilFormatter(spec.timezone);
36212
+ if (!dtf || !Number.isFinite(from) || !Number.isFinite(to)) return { pre, post };
36213
+ const seen = /* @__PURE__ */ new Set();
36214
+ for (let cursor = from - DAY_MS2; cursor < to + DAY_MS2; cursor += DAY_MS2) {
36215
+ const civil = civilParts(dtf, cursor);
36216
+ const key = civil.year * 1e4 + civil.month * 100 + civil.day;
36217
+ if (!Number.isFinite(key) || seen.has(key)) continue;
36218
+ seen.add(key);
36219
+ if (civil.weekday === "Sat" || civil.weekday === "Sun") continue;
36220
+ const naive = Date.UTC(civil.year, civil.month - 1, civil.day);
36221
+ const noonGuess = naive + 720 * MINUTE_MS;
36222
+ const atNoon = civilParts(dtf, noonGuess);
36223
+ const offset = Date.UTC(atNoon.year, atNoon.month - 1, atNoon.day, atNoon.hour, atNoon.minute) - noonGuess;
36224
+ const at = (minutes) => naive + minutes * MINUTE_MS - offset;
36225
+ if (spec.extended.start < spec.regular.start) pre.push([at(spec.extended.start), at(spec.regular.start)]);
36226
+ if (spec.regular.end < spec.extended.end) post.push([at(spec.regular.end), at(spec.extended.end)]);
35932
36227
  }
35933
36228
  return { pre, post };
35934
36229
  }
35935
- var FETCH_AHEAD_MS2 = 10 * 864e5;
35936
- var MIN_LOOKBACK_MS = 3 * 864e5;
35937
- var MAX_LOOKBACK_MS = 120 * 864e5;
36230
+ var COVER_PAD_MIN_MS = 2 * DAY_MS2;
35938
36231
  var SessionShadingTracker = class {
35939
36232
  constructor(onZones) {
35940
36233
  this.onZones = onZones;
35941
- /** Invalidates detached async work — bumped by every track()/stop(). */
36234
+ /** Invalidates the one async step (metadata resolution) — bumped by track()/stop(). */
35942
36235
  this.epoch = 0;
35943
- }
35944
- /** (Re)bind to a chart's data surface + market and evaluate once. */
36236
+ this.spec = null;
36237
+ this.ready = false;
36238
+ this.session = "regular";
36239
+ this.covered = null;
36240
+ /** The newest range seen — viewport moves during metadata resolution (a load's fit
36241
+ * animation) must not be lost, so the resolution always expands the LATEST range. */
36242
+ this.lastRange = null;
36243
+ }
36244
+ /** (Re)bind to a chart's data surface + market and expand once metadata lands. */
35945
36245
  track(data, symbol, opts) {
35946
36246
  const my = ++this.epoch;
35947
- void this.evaluate(my, data, symbol, opts);
36247
+ this.ready = false;
36248
+ this.spec = null;
36249
+ this.covered = null;
36250
+ this.session = opts.session;
36251
+ this.lastRange = opts.range;
36252
+ void data.symbolInfo(symbol).catch(() => void 0).then((si) => {
36253
+ if (my !== this.epoch) return;
36254
+ this.ready = true;
36255
+ this.spec = parseSessionSpec(si);
36256
+ this.emit(this.lastRange ?? opts.range, true);
36257
+ });
36258
+ }
36259
+ /** Follow a pan/zoom synchronously: bands are epoch-anchored, so only a range that
36260
+ * leaves the last expansion's coverage needs a recompute — no fetch, no debounce. */
36261
+ updateRange(range) {
36262
+ this.lastRange = range;
36263
+ if (!this.ready) return;
36264
+ this.emit(range, false);
35948
36265
  }
35949
36266
  stop() {
35950
36267
  this.epoch += 1;
35951
- }
35952
- async evaluate(my, data, symbol, opts) {
35953
- const resolved = data.resolve(symbol);
35954
- const provider = resolved ? data.providerInstance(resolved.provider) : void 0;
35955
- const si = await data.symbolInfo(symbol).catch(() => void 0);
35956
- if (my !== this.epoch) return;
35957
- const hasSessions = typeof si?.session === "string" && si.session !== "" && si.session !== "24x7";
35958
- if (!provider?.getCalendar || !hasSessions || !resolved) {
35959
- this.onZones(null);
36268
+ this.ready = false;
36269
+ this.spec = null;
36270
+ this.covered = null;
36271
+ this.lastRange = null;
36272
+ }
36273
+ emit(range, force) {
36274
+ if (!this.spec) {
36275
+ if (force) this.onZones(null);
35960
36276
  return;
35961
36277
  }
35962
- if (opts.session !== "extended") {
35963
- this.onZones({ pre: [], post: [] });
36278
+ if (this.session !== "extended") {
36279
+ if (force) this.onZones({ pre: [], post: [] });
35964
36280
  return;
35965
36281
  }
35966
- const now = Date.now();
35967
- const lookback = Math.max(MIN_LOOKBACK_MS, Math.min(MAX_LOOKBACK_MS, opts.lookbackMs));
35968
- const range = { from: now - lookback, to: now + FETCH_AHEAD_MS2 };
35969
- const [regular, extended] = await Promise.all([
35970
- provider.getCalendar(resolved.ticker, { ...range, session: "regular" }).catch(() => null),
35971
- provider.getCalendar(resolved.ticker, { ...range, session: "extended" }).catch(() => null)
35972
- ]);
35973
- if (my !== this.epoch) return;
35974
- if (!regular || !extended) return;
35975
- this.onZones(deriveSessionZones(regular, extended));
36282
+ const from = Math.min(range.from, range.to);
36283
+ const to = Math.max(range.from, range.to);
36284
+ if (!Number.isFinite(from) || !Number.isFinite(to)) return;
36285
+ if (!force && this.covered && from >= this.covered.from && to <= this.covered.to) return;
36286
+ const pad = Math.max(to - from, COVER_PAD_MIN_MS);
36287
+ const covered = { from: from - pad, to: to + pad };
36288
+ this.covered = covered;
36289
+ this.onZones(expandSessionZones(this.spec, covered.from, covered.to));
35976
36290
  }
35977
36291
  };
35978
36292
 
@@ -36409,7 +36723,11 @@ var ChartCell = class {
36409
36723
  this.deps.toast(`No registered provider serves "${symbol2}" (registered: ${list})`, "error", 6e3);
36410
36724
  });
36411
36725
  this.inner.on("load:start", () => this.watermark?.setLoading(true));
36412
- this.inner.on("load:end", () => this.watermark?.setLoading(false));
36726
+ this.inner.on("load:end", () => {
36727
+ this.watermark?.setLoading(false);
36728
+ this.refreshSessionShading();
36729
+ });
36730
+ this.inner.on("viewport:changed", (range) => this.sessionShading.updateRange(range));
36413
36731
  const tz = deps.timezone();
36414
36732
  if (tz !== "Etc/UTC") this.inner.renderer.set("timezone", tz);
36415
36733
  this.indicatorTitlesOn = seed.indicatorTitles ?? true;
@@ -36419,12 +36737,15 @@ var ChartCell = class {
36419
36737
  this.watermarkOn = seed.watermark ?? deps.watermark;
36420
36738
  this.watermark = deps.watermark ? new Watermark(this.host, symbol ?? "", seed.timeframe ?? "60") : null;
36421
36739
  if (!this.watermarkOn) this.watermark?.setVisible(false);
36422
- this.statusline = deps.statusline ? new Statusline(this.host, symbol ?? "") : null;
36740
+ this.statusline = deps.statusline ? new Statusline(this.host, symbol ?? "", (sym) => this.inner?.data.symbolIcon(sym)) : null;
36423
36741
  this.statusline?.setMeta(seed.timeframe ?? "60", this.state.provider ?? "");
36424
36742
  this.statusline?.onChart(this.inner);
36425
36743
  this.marketStatus = this.statusline ? new MarketStatusTracker((s) => this.statusline?.setMarketStatus(s)) : null;
36426
36744
  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 ?? "");
36745
+ if (this.inner && this.state.symbol) {
36746
+ this.statusline?.setSymbol(this.state.symbol);
36747
+ this.statusline?.setMeta(this.state.timeframe ?? "60", this.inner.data.displayPrefix(this.state.symbol) ?? this.state.provider ?? "");
36748
+ }
36428
36749
  this.refreshSessionAvailable();
36429
36750
  if (this.inner && this.state.symbol) this.marketStatus?.track(this.inner.data, this.state.symbol);
36430
36751
  });
@@ -36533,14 +36854,18 @@ var ChartCell = class {
36533
36854
  this.refreshSessionShading();
36534
36855
  });
36535
36856
  }
36536
- /** (Re)derive the pre/post-market shading bands for this cell's market. The loaded
36537
- * depth (bars × timeframe) bounds the calendar fetch; the tracker clamps it. */
36857
+ /** (Re)derive the pre/post-market shading bands for this cell's market. The bands
36858
+ * expand locally from the symbol's session vocabulary, so they paint as soon as
36859
+ * metadata is known and follow any pan depth without provider round trips. */
36538
36860
  refreshSessionShading() {
36539
36861
  const chart = this.inner;
36540
36862
  const symbol = this.state.symbol;
36541
36863
  if (!chart || !symbol) return;
36542
- const lookbackMs = Math.max(this.state.bars ?? 1e3, this.rangeBars) * timeframeMs(this.state.timeframe ?? "60");
36543
- this.sessionShading.track(chart.data, symbol, { session: this.session, lookbackMs });
36864
+ const now = Date.now();
36865
+ const requestedSpan = Math.max(this.state.bars ?? 1e3, this.rangeBars) * timeframeMs(this.state.timeframe ?? "60");
36866
+ const fallbackSpan = Number.isFinite(requestedSpan) ? Math.max(3 * 864e5, requestedSpan) : 3 * 864e5;
36867
+ const range = chart.getVisibleRange() ?? { from: now - fallbackSpan, to: now };
36868
+ this.sessionShading.track(chart.data, symbol, { session: this.session, range });
36544
36869
  }
36545
36870
  /** The session-shade colors live in the renderer CONFIG (persisted with it, edited
36546
36871
  * live by the dialog swatch) — the cell only proxies them into its settings rows. */
@@ -36636,10 +36961,10 @@ var ChartCell = class {
36636
36961
  id: "status-line",
36637
36962
  rows: [
36638
36963
  { kind: "heading", label: "Status line", id: "parts" },
36639
- { kind: "toggle", label: "Symbol name", id: "name", get: () => sl.partVisible("name"), set: (v) => sl.setPartVisible("name", v) },
36640
- { kind: "toggle", label: "Market status", id: "market", get: () => sl.partVisible("market"), set: (v) => sl.setPartVisible("market", v) },
36641
- { kind: "toggle", label: "OHLC values", id: "ohlc", get: () => sl.partVisible("ohlc"), set: (v) => sl.setPartVisible("ohlc", v) },
36642
- { kind: "toggle", label: "Bar change values", id: "change", get: () => sl.partVisible("change"), set: (v) => sl.setPartVisible("change", v) },
36964
+ { kind: "toggle", label: "Symbol name", id: "name", get: () => sl.partVisible("name"), set: (v) => this.setStatuslinePart("name", v) },
36965
+ { kind: "toggle", label: "Market status", id: "market", get: () => sl.partVisible("market"), set: (v) => this.setStatuslinePart("market", v) },
36966
+ { kind: "toggle", label: "OHLC values", id: "ohlc", get: () => sl.partVisible("ohlc"), set: (v) => this.setStatuslinePart("ohlc", v) },
36967
+ { kind: "toggle", label: "Bar change values", id: "change", get: () => sl.partVisible("change"), set: (v) => this.setStatuslinePart("change", v) },
36643
36968
  { kind: "heading", label: "Indicators", id: "indicators" },
36644
36969
  {
36645
36970
  kind: "toggle",
@@ -36674,12 +36999,40 @@ var ChartCell = class {
36674
36999
  this.indicatorTitlesOn = visible;
36675
37000
  this.inner?.renderer.set("indicatorTitles", visible);
36676
37001
  this.deps.onStateDirty();
37002
+ this.deps.onStatusPrefsChanged(this.id);
36677
37003
  }
36678
37004
  /** Show/hide the plot values beside this cell's legend titles (persisted per cell). */
36679
37005
  setIndicatorValuesVisible(visible) {
36680
37006
  this.indicatorValuesOn = visible;
36681
37007
  this.inner?.renderer.set("indicatorValues", visible);
36682
37008
  this.deps.onStateDirty();
37009
+ this.deps.onStatusPrefsChanged(this.id);
37010
+ }
37011
+ /** Show/hide one status-line segment (the settings dialog's Status line tab). */
37012
+ setStatuslinePart(part, visible) {
37013
+ this.statusline?.setPartVisible(part, visible);
37014
+ this.deps.onStatusPrefsChanged(this.id);
37015
+ }
37016
+ /** This cell's Status line tab prefs as one bundle (see {@link CellStatusPrefs}). */
37017
+ statusPrefs() {
37018
+ const sl = this.statusline;
37019
+ return {
37020
+ parts: sl ? { name: sl.partVisible("name"), market: sl.partVisible("market"), ohlc: sl.partVisible("ohlc"), change: sl.partVisible("change") } : null,
37021
+ indicatorTitles: this.indicatorTitlesOn,
37022
+ indicatorValues: this.indicatorValuesOn
37023
+ };
37024
+ }
37025
+ /** Converge this cell's Status line tab prefs to `prefs` — the follower half of
37026
+ * the workspace's style link. Idempotent: matching values change nothing, so a
37027
+ * propagated echo dies on its own. */
37028
+ applyStatusPrefs(prefs) {
37029
+ if (prefs.parts && this.statusline) {
37030
+ for (const part of Object.keys(prefs.parts)) {
37031
+ if (this.statusline.partVisible(part) !== prefs.parts[part]) this.statusline.setPartVisible(part, prefs.parts[part]);
37032
+ }
37033
+ }
37034
+ if (prefs.indicatorTitles !== this.indicatorTitlesOn) this.setIndicatorTitlesVisible(prefs.indicatorTitles);
37035
+ if (prefs.indicatorValues !== this.indicatorValuesOn) this.setIndicatorValuesVisible(prefs.indicatorValues);
36683
37036
  }
36684
37037
  /** The LIVE chart of this cell — never cache it across a layout change (the cell's
36685
37038
  * identity is what endures; the chart dies with the cell). */
@@ -37086,16 +37439,21 @@ var ChartCell = class {
37086
37439
 
37087
37440
  // src/workspace/context.ts
37088
37441
  function buildContext(host) {
37089
- const active = host.active();
37090
37442
  return {
37091
37443
  get chart() {
37092
37444
  const cell = host.active();
37093
37445
  if (!cell) throw new Error("VelaWorkspace has no active cell yet");
37094
37446
  return cell.chart;
37095
37447
  },
37096
- symbol: active?.symbol ?? "",
37097
- timeframe: active?.timeframe ?? "60",
37098
- priceStyle: active?.priceStyle ?? "candles",
37448
+ get symbol() {
37449
+ return host.active()?.symbol ?? "";
37450
+ },
37451
+ get timeframe() {
37452
+ return host.active()?.timeframe ?? "60";
37453
+ },
37454
+ get priceStyle() {
37455
+ return host.active()?.priceStyle ?? "candles";
37456
+ },
37099
37457
  setSymbol: (symbol) => host.active()?.setSymbol(symbol),
37100
37458
  setTimeframe: (tf) => host.active()?.setTimeframe(tf),
37101
37459
  setPriceStyle: (style) => host.active()?.setPriceStyle(style),
@@ -37106,8 +37464,12 @@ function buildContext(host) {
37106
37464
  addIndicator: (entry) => host.active()?.addExternalIndicator(entry),
37107
37465
  addNativeIndicator: (type) => host.active()?.addNative(type),
37108
37466
  stateChanged: () => host.stateDirty(),
37109
- cells: host.cells().map((c) => ({ id: c.id, chart: c.chart, symbol: c.symbol, timeframe: c.timeframe })),
37110
- activeCellId: active?.id ?? "",
37467
+ get cells() {
37468
+ return host.cells().map((c) => ({ id: c.id, chart: c.chart, symbol: c.symbol, timeframe: c.timeframe }));
37469
+ },
37470
+ get activeCellId() {
37471
+ return host.active()?.id ?? "";
37472
+ },
37111
37473
  setActiveCell: (id) => host.setActiveCell(id)
37112
37474
  };
37113
37475
  }
@@ -37446,6 +37808,10 @@ var VelaWorkspace = class {
37446
37808
  /** Same guard for the drawings link: the propagated mutations' own `drawing:*`
37447
37809
  * events fire synchronously inside the propagation loop and must not fan out again. */
37448
37810
  this.drawingSyncBusy = false;
37811
+ /** Same guard for the style link: a follower's `applyConfig` re-fires its
37812
+ * `onConfigChanged` in the same tick, and a state restore applies per-cell
37813
+ * configs that legitimately differ — neither must propagate. */
37814
+ this.styleSyncBusy = false;
37449
37815
  /** LINKED drawings (the drawings sync): one map per synced set (cellId → that
37450
37816
  * cell's drawing id), reachable from every member under its `cellId\0drawingId`
37451
37817
  * key — any member finds its peers to push edits/removals onto. Survives a
@@ -37506,13 +37872,14 @@ var VelaWorkspace = class {
37506
37872
  if (boot?.favorites) this.favs = [...boot.favorites];
37507
37873
  if (boot?.timeframeFavorites) this.tfFavs = [...boot.timeframeFavorites];
37508
37874
  const sync = boot?.sync ?? opts.sync;
37509
- for (const kind of ["viewport", "symbol", "timeframe", "crosshair", "drawings"]) {
37510
- this.applySyncSetting(kind, sync?.[kind]);
37511
- }
37875
+ for (const kind of SYNC_KINDS) this.applySyncSetting(kind, sync?.[kind]);
37512
37876
  this.monoLayout = opts.layout === false;
37513
37877
  const optLayout = opts.layout === false || opts.layout === void 0 ? "4" : opts.layout;
37514
37878
  this.def = this.resolveLayout(this.monoLayout ? "1" : boot?.layout && ensureLayout(boot.layout) ? boot.layout : optLayout);
37515
37879
  this.alertCap = Math.max(1, opts.alertCap ?? ALERT_CAP);
37880
+ this.topbarComp = resolveTopbarComposition(opts.topbar);
37881
+ this.indicatorsOverride = topbarActionOverride("indicators");
37882
+ this.screenshotOverride = topbarActionOverride("screenshot");
37516
37883
  if (boot?.trackSizes) for (const [id, ts] of Object.entries(boot.trackSizes)) this.trackSizes.set(id, ts);
37517
37884
  if (boot?.ext) this.extState = { ...boot.ext };
37518
37885
  if (boot?.charts) for (const { id, ...cs } of boot.charts) this.pool.set(id, cs);
@@ -37529,6 +37896,8 @@ var VelaWorkspace = class {
37529
37896
  });
37530
37897
  this.symbolPicker = new SymbolPicker({
37531
37898
  host: this.root,
37899
+ // Row icons come from each descriptor's OWNING provider (resolveSymbolIcon).
37900
+ iconFor: (d) => this.feed.symbolIconOf(d),
37532
37901
  onSelect: (ticker) => this.active.setSymbol(ticker),
37533
37902
  onOpenChange: (open2) => {
37534
37903
  if (open2) for (const cell of this.cells()) cell.chart.renderer.closeDialogs();
@@ -37536,7 +37905,7 @@ var VelaWorkspace = class {
37536
37905
  }
37537
37906
  });
37538
37907
  this.symbolPicker.setSource(() => this.feed.symbols());
37539
- this.indicatorPicker = opts.indicatorPicker !== false ? new IndicatorPicker({
37908
+ this.indicatorPicker = opts.indicatorPicker !== false && !this.indicatorsOverride && topbarHas(this.topbarComp, "indicators") ? new IndicatorPicker({
37540
37909
  host: this.root,
37541
37910
  library: () => this.active.libraryRows(),
37542
37911
  onChart: () => this.active.onChartRows(),
@@ -37555,6 +37924,9 @@ var VelaWorkspace = class {
37555
37924
  });
37556
37925
  this.topbar = new Topbar(this.root, {
37557
37926
  symbol: "",
37927
+ // RAW option, not the resolved lists — the bar distinguishes a host-declared
37928
+ // side (list is law) from a default one (an override's `order` may flow).
37929
+ composition: opts.topbar,
37558
37930
  onSymbolClick: () => this.symbolPicker.open(),
37559
37931
  ...picker ? { onIndicatorsClick: () => picker.open() } : {},
37560
37932
  onUndoClick: () => this.active.history.undo(),
@@ -37584,7 +37956,8 @@ var VelaWorkspace = class {
37584
37956
  syncs: () => [
37585
37957
  { id: "symbol", label: "Symbol", checked: this.syncOpts.symbol === true },
37586
37958
  { id: "timeframe", label: "Interval", checked: this.syncOpts.timeframe === true },
37587
- { id: "crosshair", label: "Crosshair", checked: this.syncOpts.crosshair === true }
37959
+ { id: "crosshair", label: "Crosshair", checked: this.syncOpts.crosshair === true },
37960
+ { id: "style", label: "Style", checked: this.syncOpts.style === true }
37588
37961
  ],
37589
37962
  onToggleSync: (id) => {
37590
37963
  const kind = id;
@@ -37612,7 +37985,7 @@ var VelaWorkspace = class {
37612
37985
  context: () => this.context(),
37613
37986
  changed: () => this.markStateDirty()
37614
37987
  });
37615
- this.objectTree = new ObjectTree(main);
37988
+ this.objectTree = new ObjectTree(main, (sym) => this.feed.symbolIcon(sym));
37616
37989
  this.dataWindow = new DataWindow(main);
37617
37990
  this.dock.addBuiltIn({ id: "dataWindow", title: "Data window", icon: "datawindow", order: 10, panel: this.dataWindow, onChart: (c) => this.dataWindow.onChart(c) });
37618
37991
  this.dock.addBuiltIn({ id: "objects", title: "Object tree", icon: "objects", order: 20, panel: this.objectTree, onChart: (c) => this.objectTree.onChart(c) });
@@ -37684,7 +38057,9 @@ var VelaWorkspace = class {
37684
38057
  timeframe: "60",
37685
38058
  onSymbolClick: () => this.symbolPicker.open(),
37686
38059
  onTimeframeClick: () => this.openTimeframeDrawer(),
37687
- ...picker ? { onIndicatorsClick: () => picker.open() } : {},
38060
+ // Same visibility truth as the desktop bar: composition-hidden
38061
+ // indicators lose their mobile stop too; an override takes it over.
38062
+ ...topbarHas(this.topbarComp, "indicators") && (picker || this.indicatorsOverride) ? { onIndicatorsClick: this.indicatorsOverride ? () => this.runOverride(this.indicatorsOverride) : () => picker.open() } : {},
37688
38063
  getContext: () => this.context(),
37689
38064
  ...this.drawingsEnabled ? { onDrawingsClick: () => this.openDrawingsDrawer() } : {},
37690
38065
  onMoreClick: () => this.openMoreDrawer(),
@@ -37866,7 +38241,7 @@ var VelaWorkspace = class {
37866
38241
  this.topbar.setTimeframeFavorites(this.tfFavs);
37867
38242
  }
37868
38243
  this.dock.applyState(st.panels);
37869
- for (const kind of ["viewport", "symbol", "timeframe", "crosshair", "drawings"]) this.applySyncSetting(kind, st.sync?.[kind]);
38244
+ for (const kind of SYNC_KINDS) this.applySyncSetting(kind, st.sync?.[kind]);
37870
38245
  this.trackSizes.clear();
37871
38246
  if (st.trackSizes) for (const [id, ts] of Object.entries(st.trackSizes)) this.trackSizes.set(id, ts);
37872
38247
  const targetDef = this.monoLayout ? this.def : ensureLayout(st.layout) ?? this.def;
@@ -37877,9 +38252,14 @@ var VelaWorkspace = class {
37877
38252
  for (const cell of this.cellsById.values()) cell.chart.drawings.setFavorites(this.favs);
37878
38253
  }
37879
38254
  this.drawingLinks.clear();
37880
- for (const [i] of this.def.cells.entries()) {
37881
- const { id, ...cs } = st.charts[i];
37882
- this.cellsById.get(id)?.rehydrate(cs);
38255
+ this.styleSyncBusy = true;
38256
+ try {
38257
+ for (const [i] of this.def.cells.entries()) {
38258
+ const { id, ...cs } = st.charts[i];
38259
+ this.cellsById.get(id)?.rehydrate(cs);
38260
+ }
38261
+ } finally {
38262
+ this.styleSyncBusy = false;
37883
38263
  }
37884
38264
  if (st.timezone) this.setTimezone(st.timezone);
37885
38265
  this.pool.clear();
@@ -37979,6 +38359,7 @@ var VelaWorkspace = class {
37979
38359
  const rebuildAll = nextBackend !== this.cellBackend;
37980
38360
  this.order = orderAfterLayout(this.order, next.cells.length, this.activeId);
37981
38361
  const keep = new Set(this.order.slice(0, next.cells.length));
38362
+ const preexisting = new Set(this.cellsById.keys());
37982
38363
  for (const [id, cell] of [...this.cellsById]) {
37983
38364
  if (!keep.has(id) || rebuildAll) {
37984
38365
  this.poolSet(id, cell.dehydrate());
@@ -37991,6 +38372,7 @@ var VelaWorkspace = class {
37991
38372
  this.cellBackend = nextBackend;
37992
38373
  this.applyGrid();
37993
38374
  this.buildCells();
38375
+ this.alignNewCellStyles(preexisting);
37994
38376
  this.syncCellPresentation();
37995
38377
  this.topbar.setLayout(next.id);
37996
38378
  const nextActive = activeAfterLayout(this.activeId, this.order.slice(0, next.cells.length));
@@ -38194,6 +38576,7 @@ var VelaWorkspace = class {
38194
38576
  onMarketChanged: (id2) => this.onCellMarketChanged(id2),
38195
38577
  onPriceStyleChanged: (id2) => this.onCellPriceStyleChanged(id2),
38196
38578
  onIndicatorsChanged: (id2) => this.onCellIndicatorsChanged(id2),
38579
+ onStatusPrefsChanged: (id2) => this.propagateStylePrefs(id2),
38197
38580
  onStateDirty: () => this.markStateDirty(),
38198
38581
  manifestSettled: () => this.manifestSettled,
38199
38582
  toast: (message, kind, durationMs) => this.toastHost.show(message, kind, durationMs)
@@ -38269,6 +38652,7 @@ var VelaWorkspace = class {
38269
38652
  this.drawToolbar?.setEraserActive(mode === "eraser");
38270
38653
  });
38271
38654
  chart.on("viewport:changed", (range) => this.propagateViewport(cell.id, range));
38655
+ chart.renderer.onConfigChanged(() => this.propagateStylePrefs(cell.id));
38272
38656
  chart.on("theme:changed", (t) => this.setTheme(t));
38273
38657
  chart.renderer.onAxisLongPress((e) => {
38274
38658
  if (this.layoutCtl.current !== "mobile") return;
@@ -38301,11 +38685,67 @@ var VelaWorkspace = class {
38301
38685
  if (kind === "viewport") {
38302
38686
  const range = this.cellsById.get(this.activeId)?.chart.getVisibleRange();
38303
38687
  if (range) this.propagateViewport(this.activeId, range);
38688
+ } else if (kind === "style") {
38689
+ this.propagateStylePrefs(this.activeId);
38304
38690
  } else {
38305
38691
  this.propagateMarket(this.activeId);
38306
38692
  }
38307
38693
  }
38308
38694
  }
38695
+ /**
38696
+ * Align cells minted by a layout change to their style group: with the link on, a
38697
+ * NEW cell (fresh slot or one returning from the pool, which missed edits while
38698
+ * dormant) inherits the presentation of a pre-existing group peer — the active
38699
+ * cell when it is one — instead of sitting on its own state beside a styled
38700
+ * group. Propagation runs FROM the peer, so a newborn's defaults never overwrite
38701
+ * the group, and the equality short-circuits keep converged peers untouched.
38702
+ */
38703
+ alignNewCellStyles(preexisting) {
38704
+ const setting = this.syncOpts.style;
38705
+ if (!setting) return;
38706
+ const ids = [...this.cellsById.keys()];
38707
+ const propagated = /* @__PURE__ */ new Set();
38708
+ for (const id of ids) {
38709
+ if (preexisting.has(id)) continue;
38710
+ const peers = syncTargets(id, setting, ids).filter((p) => preexisting.has(p));
38711
+ if (peers.length === 0) continue;
38712
+ const source = this.activeId && peers.includes(this.activeId) ? this.activeId : peers[0];
38713
+ if (propagated.has(source)) continue;
38714
+ propagated.add(source);
38715
+ this.propagateStylePrefs(source);
38716
+ }
38717
+ }
38718
+ /**
38719
+ * Mirror an origin cell's presentation — the Canvas + Scales-and-lines slice of
38720
+ * its renderer config plus its Status line tab prefs — onto its same-group
38721
+ * followers (the style link). Loop-safe two ways: the busy guard eats the
38722
+ * followers' SYNCHRONOUS echoes (their `applyConfig` re-fires `onConfigChanged`
38723
+ * in the same tick), and the equality short-circuits leave already-converged
38724
+ * followers untouched, so nothing re-emits once the group agrees.
38725
+ */
38726
+ propagateStylePrefs(originId) {
38727
+ if (this.styleSyncBusy || this.destroyed) return;
38728
+ const targets = syncTargets(originId, this.syncOpts.style, [...this.cellsById.keys()]);
38729
+ if (targets.length === 0) return;
38730
+ const origin = this.cellsById.get(originId);
38731
+ if (!origin) return;
38732
+ const slice = styleConfigSlice(origin.chart.renderer.getConfig());
38733
+ const sliceJson = slice ? JSON.stringify(slice) : null;
38734
+ const prefs = origin.statusPrefs();
38735
+ this.styleSyncBusy = true;
38736
+ try {
38737
+ for (const id of targets) {
38738
+ const cell = this.cellsById.get(id);
38739
+ if (!cell) continue;
38740
+ if (slice && sliceJson !== JSON.stringify(styleConfigSlice(cell.chart.renderer.getConfig()))) {
38741
+ cell.chart.renderer.applyConfig(slice);
38742
+ }
38743
+ cell.applyStatusPrefs(prefs);
38744
+ }
38745
+ } finally {
38746
+ this.styleSyncBusy = false;
38747
+ }
38748
+ }
38309
38749
  /**
38310
38750
  * Mirror an origin cell's pointer time onto its same-group followers as GHOST
38311
38751
  * crosshairs (`renderer.setExternalCrosshair`). The horizontal price level rides
@@ -38580,27 +39020,32 @@ var VelaWorkspace = class {
38580
39020
  this.drawingsDrawer.open();
38581
39021
  }
38582
39022
  openMoreDrawer() {
39023
+ const has = (id) => topbarHas(this.topbarComp, id);
38583
39024
  this.moreDrawer ?? (this.moreDrawer = new MoreDrawer({
38584
39025
  host: this.root,
38585
- onUndo: () => this.active.history.undo(),
38586
- onRedo: () => this.active.history.redo(),
38587
- onScreenshot: () => this.active.downloadScreenshot(),
39026
+ ...has("undo-redo") ? { onUndo: () => this.active.history.undo(), onRedo: () => this.active.history.redo() } : {},
39027
+ ...has("screenshot") ? { onScreenshot: this.screenshotOverride ? () => this.runOverride(this.screenshotOverride) : () => this.active.downloadScreenshot() } : {},
38588
39028
  canUndo: () => this.active.history.canUndo,
38589
39029
  canRedo: () => this.active.history.canRedo,
38590
39030
  priceStyles: () => priceStyleIds().map((id) => ({ id, label: priceStyleLabel(id), icon: priceStyleIcon(id) })),
38591
39031
  priceStyle: () => this.active.priceStyle,
38592
39032
  onPriceStyle: (id) => this.active.setPriceStyle(id),
38593
- panels: () => [...this.dock.list()],
39033
+ panels: () => has("panels") ? [...this.dock.list()] : [],
38594
39034
  onTogglePanel: (id) => this.dock.toggle(id),
38595
- alerts: () => this.alerts.map((a) => ({ title: `${a.source} \xB7 ${a.title}`, message: a.message, time: a.time })),
39035
+ ...has("alerts") ? { alerts: () => this.alerts.map((a) => ({ title: `${a.source} \xB7 ${a.title}`, message: a.message, time: a.time })) } : {},
38596
39036
  // 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()) })),
39037
+ // lands in the drawer, or every left action would appear twice. Built-in-id
39038
+ // actions are slot OVERRIDES: they reach the drawer through the slot's own
39039
+ // routed button (screenshot) or stop (indicators), never as an extra row.
39040
+ actions: () => {
39041
+ const builtin = new Set(TOPBAR_BUILTIN_IDS);
39042
+ 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()) }));
39043
+ },
38599
39044
  // The desktop layout dropdown's whole surface — the grid canvas, the
38600
39045
  // non-canvas presets and the sync switches — relocated into the kebab
38601
39046
  // 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 : {
39047
+ // single-chart mode and a composition without 'layout' omit it here too.
39048
+ layout: this.monoLayout || !has("layout") ? void 0 : {
38604
39049
  shape: () => layoutShape(this.def),
38605
39050
  presets: () => layouts().filter((l) => layoutShape(l) === null).map((l) => ({ id: l.id, label: l.label, checked: l.id === this.def.id })),
38606
39051
  onSelectGrid: (rows, cols) => this.setLayout(layoutForGrid(rows, cols)),
@@ -38608,7 +39053,8 @@ var VelaWorkspace = class {
38608
39053
  syncs: () => [
38609
39054
  { id: "symbol", label: "Symbol", checked: this.syncOpts.symbol === true },
38610
39055
  { id: "timeframe", label: "Interval", checked: this.syncOpts.timeframe === true },
38611
- { id: "crosshair", label: "Crosshair", checked: this.syncOpts.crosshair === true }
39056
+ { id: "crosshair", label: "Crosshair", checked: this.syncOpts.crosshair === true },
39057
+ { id: "style", label: "Style", checked: this.syncOpts.style === true }
38612
39058
  ],
38613
39059
  onToggleSync: (id) => {
38614
39060
  const kind = id;
@@ -38667,9 +39113,23 @@ var VelaWorkspace = class {
38667
39113
  }
38668
39114
  }
38669
39115
  }
39116
+ /** Invoke a slot override the way its button would: fresh context, `when` respected. */
39117
+ runOverride(action) {
39118
+ const ctx = this.context();
39119
+ if (!action.when || action.when(ctx)) action.run(ctx);
39120
+ }
38670
39121
  /** The default shortcut set — every binding acts on the ACTIVE cell. */
38671
39122
  registerDefaultKeys() {
38672
- this.keymap.register({ id: "chart.screenshot", keys: "mod+alt+s", label: "Download a chart screenshot", category: "Chart", run: () => this.active.downloadScreenshot() });
39123
+ if (topbarHas(this.topbarComp, "screenshot")) {
39124
+ const ov = this.screenshotOverride;
39125
+ this.keymap.register({
39126
+ id: "chart.screenshot",
39127
+ keys: "mod+alt+s",
39128
+ label: ov ? ov.label : "Download a chart screenshot",
39129
+ category: "Chart",
39130
+ run: ov ? () => this.runOverride(ov) : () => this.active.downloadScreenshot()
39131
+ });
39132
+ }
38673
39133
  this.keymap.register({ id: "chart.reset-view", keys: "alt+r", label: "Reset view (all history)", category: "Chart", run: () => this.active.chart.setVisibleRangePreset("ALL") });
38674
39134
  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
39135
  this.keymap.register({
@@ -38709,7 +39169,10 @@ var VelaWorkspace = class {
38709
39169
  this.keymap.register({ id: "view.zoom-out", keys: "mod+arrowdown", label: "Zoom out", category: "Chart", run: () => this.glider.zoom(ZOOM_OUT) });
38710
39170
  this.keymap.register({ id: "view.pan-left", keys: "mod+arrowleft", label: "Pan toward history", category: "Chart", run: () => this.active.chart.panBy(-PAN_FAST) });
38711
39171
  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) {
39172
+ const indOv = this.indicatorsOverride;
39173
+ if (indOv && topbarHas(this.topbarComp, "indicators")) {
39174
+ this.keymap.register({ id: "indicators.open", keys: "/", label: indOv.label, category: "Indicators", run: () => this.runOverride(indOv) });
39175
+ } else if (this.indicatorPicker) {
38713
39176
  this.keymap.register({ id: "indicators.open", keys: "/", label: "Open the indicator picker", category: "Indicators", run: () => this.indicatorPicker?.open() });
38714
39177
  }
38715
39178
  this.keymap.register({