@luxalgo/vela 0.6.7 → 0.6.9

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 (45) hide show
  1. package/dist/{DataProvider-BsQM2WNH.d.cts → DataProvider-8Z95Q-RJ.d.cts} +1 -1
  2. package/dist/{DataProvider-BSLlBpB9.d.ts → DataProvider-DExJrfut.d.ts} +1 -1
  3. package/dist/{chunk-2R7BANEM.js → chunk-62SVGONC.js} +2 -2
  4. package/dist/{chunk-6WFKTAT4.js → chunk-6WDDVMBJ.js} +548 -147
  5. package/dist/{chunk-L3I2CCYO.js → chunk-EQCHJZOT.js} +49 -2
  6. package/dist/{chunk-OICPLNU7.js → chunk-FCVIG7JG.js} +188 -2
  7. package/dist/{chunk-WVP4XV5A.js → chunk-STHSKXOR.js} +407 -159
  8. package/dist/{chunk-PN5KWFZ4.js → chunk-WFSZBX3R.js} +2 -2
  9. package/dist/{contributions-Z12BrWCy.d.ts → contributions-C1U2Krwg.d.cts} +112 -7
  10. package/dist/{contributions-Hv4_cOJo.d.cts → contributions-D7PVZO2i.d.ts} +112 -7
  11. package/dist/index.cjs +755 -152
  12. package/dist/index.d.cts +12 -7
  13. package/dist/index.d.ts +12 -7
  14. package/dist/index.js +4 -4
  15. package/dist/{options-BaVTMXaO.d.ts → options-FM0peknS.d.cts} +84 -7
  16. package/dist/{options-BaVTMXaO.d.cts → options-FM0peknS.d.ts} +84 -7
  17. package/dist/{plugin-9koBjc5H.d.ts → plugin-7bkF32Rk.d.ts} +3 -3
  18. package/dist/{plugin-BXhDHTYn.d.cts → plugin-DfVqBz9p.d.cts} +3 -3
  19. package/dist/plugin.cjs +18 -2
  20. package/dist/plugin.d.cts +4 -4
  21. package/dist/plugin.d.ts +4 -4
  22. package/dist/plugin.js +2 -2
  23. package/dist/providers/binance.d.cts +2 -2
  24. package/dist/providers/binance.d.ts +2 -2
  25. package/dist/providers/coinbase.d.cts +2 -2
  26. package/dist/providers/coinbase.d.ts +2 -2
  27. package/dist/providers/hyperliquid.d.cts +2 -2
  28. package/dist/providers/hyperliquid.d.ts +2 -2
  29. package/dist/{bottombar-Br4rzx_R.d.ts → statusline-DOPiT6I6.d.cts} +91 -6
  30. package/dist/{bottombar-Div5zibZ.d.cts → statusline-zdF4eZLr.d.ts} +91 -6
  31. package/dist/ui.cjs +201 -4
  32. package/dist/ui.d.cts +89 -2
  33. package/dist/ui.d.ts +89 -2
  34. package/dist/ui.js +3 -3
  35. package/dist/vela.global.js +755 -152
  36. package/dist/vela.global.min.js +93 -44
  37. package/dist/widget.cjs +1144 -271
  38. package/dist/widget.d.cts +12 -85
  39. package/dist/widget.d.ts +12 -85
  40. package/dist/widget.js +7 -7
  41. package/dist/workspace.cjs +1144 -271
  42. package/dist/workspace.d.cts +65 -11
  43. package/dist/workspace.d.ts +65 -11
  44. package/dist/workspace.js +6 -6
  45. package/package.json +1 -1
package/dist/widget.cjs CHANGED
@@ -1097,11 +1097,11 @@ registerIcon(
1097
1097
  );
1098
1098
  registerIcon(
1099
1099
  "market-pre",
1100
- svg24('<path d="M12 2v8"/><path d="m4.93 10.93 1.41 1.41"/><path d="M2 18h2"/><path d="M20 18h2"/><path d="m19.07 10.93-1.41 1.41"/><path d="M22 22H2"/><path d="m16 6-4 4-4-4"/><path d="M16 18a4 4 0 0 0-8 0"/>')
1100
+ svg24('<path d="M12 2v8"/><path d="m4.93 10.93 1.41 1.41"/><path d="M2 18h2"/><path d="M20 18h2"/><path d="m19.07 10.93-1.41 1.41"/><path d="M22 22H2"/><path d="m8 6 4-4 4 4"/><path d="M16 18a4 4 0 0 0-8 0"/>')
1101
1101
  );
1102
1102
  registerIcon(
1103
1103
  "market-post",
1104
- svg24('<path d="M12 10V2"/><path d="m4.93 10.93 1.41 1.41"/><path d="M2 18h2"/><path d="M20 18h2"/><path d="m19.07 10.93-1.41 1.41"/><path d="M22 22H2"/><path d="m16 18-4-4-4 4"/><path d="M16 6a4 4 0 0 0-8 0"/>')
1104
+ svg24('<path d="M12 10V2"/><path d="m4.93 10.93 1.41 1.41"/><path d="M2 18h2"/><path d="M20 18h2"/><path d="m19.07 10.93-1.41 1.41"/><path d="M22 22H2"/><path d="m16 14-4 4-4-4"/><path d="M16 6a4 4 0 0 0-8 0"/>')
1105
1105
  );
1106
1106
  registerIcon("market-closed", svg24('<path d="M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z"/>'));
1107
1107
  registerIcon(
@@ -4195,6 +4195,192 @@ function buildFieldControl(desc) {
4195
4195
  return { el };
4196
4196
  }
4197
4197
 
4198
+ // src/ui/components/callout-bubble/controller.ts
4199
+ function closesPanel(item) {
4200
+ return item.close !== false;
4201
+ }
4202
+ function calloutPanelRows(items) {
4203
+ const rows = [];
4204
+ for (const item of items) {
4205
+ if (item.type === "text") {
4206
+ rows.push({ type: "text", text: item.text });
4207
+ continue;
4208
+ }
4209
+ const last = rows[rows.length - 1];
4210
+ if (last && last.type === "buttons") last.buttons.push(item);
4211
+ else rows.push({ type: "buttons", buttons: [item] });
4212
+ }
4213
+ return rows;
4214
+ }
4215
+
4216
+ // src/ui/components/callout-bubble/styles.ts
4217
+ var CALLOUT_STYLE_ID = "vela-ui-callout-bubble";
4218
+ var CALLOUT_CSS = `
4219
+ .vela-callout {
4220
+ display: inline-grid;
4221
+ place-items: center;
4222
+ border-radius: 50%;
4223
+ flex: none;
4224
+ line-height: 0;
4225
+ box-sizing: border-box;
4226
+ cursor: default;
4227
+ user-select: none;
4228
+ -webkit-user-select: none;
4229
+ }
4230
+ .vela-callout[role='button'] { cursor: pointer; }
4231
+ .vela-callout svg { display: block; }
4232
+ /* The deployed panel \u2014 carries the kit's elevated-card look itself (the popover
4233
+ shell is bare positioning chrome). */
4234
+ .vela-callout-panel {
4235
+ display: flex;
4236
+ flex-direction: column;
4237
+ gap: 8px;
4238
+ padding: 10px 12px;
4239
+ max-width: 280px;
4240
+ box-sizing: border-box;
4241
+ background: var(--vela-surface-elev);
4242
+ border: 1px solid var(--vela-border-strong);
4243
+ border-radius: 6px;
4244
+ box-shadow: var(--vela-shadow);
4245
+ color: var(--vela-fg);
4246
+ font: var(--vela-font-size-md) var(--vela-font);
4247
+ }
4248
+ .vela-callout-title { font-weight: 600; color: var(--vela-fg-bright); }
4249
+ .vela-callout-text { color: var(--vela-fg-muted); line-height: 1.45; white-space: pre-line; }
4250
+ .vela-callout-actions { display: flex; flex-wrap: wrap; gap: 8px; }
4251
+ .vela-callout-btn {
4252
+ cursor: pointer;
4253
+ height: 26px;
4254
+ padding: 0 10px;
4255
+ border-radius: 5px;
4256
+ border: 1px solid var(--vela-border);
4257
+ background: transparent;
4258
+ color: var(--vela-fg);
4259
+ font-size: var(--vela-font-size-md);
4260
+ font-family: inherit;
4261
+ transition: background var(--vela-dur-fast) ease, color var(--vela-dur-fast) ease, opacity var(--vela-dur-fast) ease, border-color var(--vela-dur-fast) ease;
4262
+ }
4263
+ .vela-callout-btn:hover { background: var(--vela-hover); color: var(--vela-fg-bright); border-color: var(--vela-fg-muted); }
4264
+ .vela-callout-btn-primary { border-color: var(--vela-selected-bg); background: var(--vela-selected-bg); color: var(--vela-selected-fg); }
4265
+ .vela-callout-btn-primary:hover { background: var(--vela-selected-bg); color: var(--vela-selected-fg); opacity: 0.85; border-color: var(--vela-selected-bg); }
4266
+ `;
4267
+
4268
+ // src/ui/components/callout-bubble/view.ts
4269
+ var CalloutBubble = class {
4270
+ constructor(opts) {
4271
+ this.pop = null;
4272
+ this.spec = { icon: opts.icon, background: opts.background, label: opts.label };
4273
+ if (opts.color !== void 0) this.spec.color = opts.color;
4274
+ if (opts.panel !== void 0) this.spec.panel = opts.panel;
4275
+ this.size = opts.size ?? 16;
4276
+ this.host = opts.host;
4277
+ this.theme = opts.theme;
4278
+ this.boundary = opts.boundary;
4279
+ const doc = (opts.host ?? document.body).ownerDocument;
4280
+ injectStyles(CALLOUT_STYLE_ID, CALLOUT_CSS, doc);
4281
+ this.el = doc.createElement("span");
4282
+ this.el.className = "vela-callout";
4283
+ this.el.style.width = `${this.size}px`;
4284
+ this.el.style.height = `${this.size}px`;
4285
+ this.el.addEventListener("click", (e) => {
4286
+ if (!this.spec.panel) return;
4287
+ e.stopPropagation();
4288
+ this.toggle();
4289
+ });
4290
+ this.el.addEventListener("dblclick", (e) => {
4291
+ if (this.spec.panel) e.stopPropagation();
4292
+ });
4293
+ this.el.addEventListener("keydown", (e) => {
4294
+ if (!this.spec.panel || e.key !== "Enter" && e.key !== " ") return;
4295
+ e.preventDefault();
4296
+ this.toggle();
4297
+ });
4298
+ this.dress();
4299
+ }
4300
+ /** Re-dress the bubble (a status change: new icon, tint, label, panel). */
4301
+ set(spec) {
4302
+ this.spec = { ...this.spec, ...spec };
4303
+ if ("panel" in spec) this.pop?.hide();
4304
+ this.dress();
4305
+ }
4306
+ /** Whether the deployed panel is currently open. */
4307
+ get open() {
4308
+ return this.pop?.open ?? false;
4309
+ }
4310
+ destroy() {
4311
+ this.pop?.destroy();
4312
+ this.pop = null;
4313
+ this.el.remove();
4314
+ }
4315
+ dress() {
4316
+ const clickable = this.spec.panel !== void 0;
4317
+ this.el.style.background = this.spec.background;
4318
+ this.el.style.color = this.spec.color ?? "";
4319
+ this.el.innerHTML = iconAt(this.spec.icon, this.size - 4);
4320
+ this.el.setAttribute("aria-label", this.spec.label);
4321
+ if (clickable) {
4322
+ this.el.setAttribute("role", "button");
4323
+ this.el.tabIndex = 0;
4324
+ } else {
4325
+ this.el.removeAttribute("role");
4326
+ this.el.removeAttribute("tabindex");
4327
+ }
4328
+ }
4329
+ toggle() {
4330
+ if (this.pop?.open) {
4331
+ this.pop.hide();
4332
+ return;
4333
+ }
4334
+ const panel = this.spec.panel;
4335
+ if (!panel) return;
4336
+ this.pop?.destroy();
4337
+ this.pop = new Popover({
4338
+ trigger: this.el,
4339
+ gap: 6,
4340
+ content: (body) => this.buildPanel(body, panel),
4341
+ ...this.host ? { host: this.host } : {},
4342
+ ...this.theme ? { theme: this.theme() } : {},
4343
+ ...this.boundary ? { boundary: this.boundary } : {}
4344
+ });
4345
+ this.pop.show();
4346
+ }
4347
+ buildPanel(body, panel) {
4348
+ const doc = body.ownerDocument;
4349
+ const root = doc.createElement("div");
4350
+ root.className = "vela-callout-panel";
4351
+ if (panel.title) {
4352
+ const title = doc.createElement("div");
4353
+ title.className = "vela-callout-title";
4354
+ title.textContent = panel.title;
4355
+ root.appendChild(title);
4356
+ }
4357
+ for (const row of calloutPanelRows(panel.items)) {
4358
+ if (row.type === "text") {
4359
+ const text = doc.createElement("div");
4360
+ text.className = "vela-callout-text";
4361
+ text.textContent = row.text;
4362
+ root.appendChild(text);
4363
+ continue;
4364
+ }
4365
+ const actions = doc.createElement("div");
4366
+ actions.className = "vela-callout-actions";
4367
+ for (const item of row.buttons) {
4368
+ const btn2 = doc.createElement("button");
4369
+ btn2.type = "button";
4370
+ btn2.className = "vela-callout-btn" + (item.primary ? " vela-callout-btn-primary" : "");
4371
+ btn2.textContent = item.label;
4372
+ btn2.addEventListener("click", () => {
4373
+ item.run();
4374
+ if (closesPanel(item)) this.pop?.hide();
4375
+ });
4376
+ actions.appendChild(btn2);
4377
+ }
4378
+ root.appendChild(actions);
4379
+ }
4380
+ body.appendChild(root);
4381
+ }
4382
+ };
4383
+
4198
4384
  // src/widget/layout-picker.ts
4199
4385
  var STYLE_ID = "vela-widget-layout-picker-v14";
4200
4386
  var CSS2 = `
@@ -4677,6 +4863,44 @@ function legendActionsProviderFor(chart, context) {
4677
4863
  return legendActions().filter((d) => !d.when || d.when(info)).map((d) => ({ id: d.id, icon: d.icon, tooltip: d.tooltip, run: () => d.run(context(), info) }));
4678
4864
  };
4679
4865
  }
4866
+ var calloutRegistry = /* @__PURE__ */ new Map();
4867
+ function legendCallouts() {
4868
+ return [...calloutRegistry.values()].sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
4869
+ }
4870
+ function legendCalloutsProviderFor(chart, context) {
4871
+ return (indicatorId) => {
4872
+ const handle = chart.indicators().find((h) => h.id === indicatorId);
4873
+ if (!handle) return [];
4874
+ const info = { id: handle.id, title: handle.title, ...handle.source !== void 0 ? { source: handle.source } : {} };
4875
+ const views = [];
4876
+ for (const d of legendCallouts()) {
4877
+ const spec = d.callout(info);
4878
+ if (!spec) continue;
4879
+ views.push({
4880
+ id: d.id,
4881
+ icon: spec.icon,
4882
+ background: spec.background,
4883
+ ...spec.color !== void 0 ? { color: spec.color } : {},
4884
+ tooltip: spec.tooltip,
4885
+ ...spec.content !== void 0 ? {
4886
+ content: {
4887
+ ...spec.content.title !== void 0 ? { title: spec.content.title } : {},
4888
+ items: spec.content.items.map(
4889
+ (item) => item.type === "text" ? item : {
4890
+ type: "button",
4891
+ label: item.label,
4892
+ ...item.primary !== void 0 ? { primary: item.primary } : {},
4893
+ ...item.close !== void 0 ? { close: item.close } : {},
4894
+ run: () => item.run(context(), info)
4895
+ }
4896
+ )
4897
+ }
4898
+ } : {}
4899
+ });
4900
+ }
4901
+ return views;
4902
+ };
4903
+ }
4680
4904
  var stateHandlers = /* @__PURE__ */ new Map();
4681
4905
  function registerStatePersistence(handler) {
4682
4906
  stateHandlers.set(handler.key, handler);
@@ -5254,6 +5478,11 @@ var Topbar = class {
5254
5478
  /** Overrides that LEFT their native slot (default side + a declared `order`) — they
5255
5479
  * render through the flow cluster like ordinary actions. */
5256
5480
  this.flowingOverrides = /* @__PURE__ */ new Set();
5481
+ /** Tooltips of the CURRENT icon-only action buttons — rebuilt with every
5482
+ * renderActions pass (contributed buttons are replaceChildren'd away). */
5483
+ this.actionTooltips = [];
5484
+ /** `iconOnly` misuse warned once per action id (renderActions re-runs freely). */
5485
+ this.warnedIconless = /* @__PURE__ */ new Set();
5257
5486
  this.onHairlineSync = () => {
5258
5487
  if (this.hairlineRaf) return;
5259
5488
  const win = this.el.ownerDocument.defaultView;
@@ -5516,6 +5745,8 @@ var Topbar = class {
5516
5745
  this.actionsHost.replaceChildren();
5517
5746
  this.leftActionsHost.replaceChildren();
5518
5747
  for (const pin of this.pinned.values()) pin.host.replaceChildren();
5748
+ for (const t of this.actionTooltips) t.destroy();
5749
+ this.actionTooltips = [];
5519
5750
  const doc = this.actionsHost.ownerDocument;
5520
5751
  const flowLeft = this.comp.left.includes("actions");
5521
5752
  const flowRight = this.comp.right.includes("actions");
@@ -5525,10 +5756,20 @@ var Topbar = class {
5525
5756
  if (!pin && builtin.has(action.id) && !this.flowingOverrides.has(action.id)) continue;
5526
5757
  const left = pin ? pin.left : action.align === "left";
5527
5758
  if (!pin && !(left ? flowLeft : flowRight)) continue;
5759
+ const iconOnly = action.iconOnly === true && !!action.icon;
5760
+ if (action.iconOnly === true && !action.icon && !this.warnedIconless.has(action.id)) {
5761
+ this.warnedIconless.add(action.id);
5762
+ console.warn(`[vela] widget action "${action.id}": iconOnly needs an \`icon\` \u2014 rendering the label instead.`);
5763
+ }
5528
5764
  const b = doc.createElement("button");
5529
- b.className = left ? "vela-widget-action-left" : "vela-widget-action";
5765
+ b.className = left ? "vela-widget-action-left" : iconOnly ? "vela-widget-tool" : "vela-widget-action";
5530
5766
  if (action.icon) b.appendChild(iconEl(action.icon, doc));
5531
- b.appendChild(doc.createTextNode(action.label));
5767
+ if (iconOnly) {
5768
+ b.setAttribute("aria-label", action.label);
5769
+ this.actionTooltips.push(new Tooltip(b, { content: action.label, triggerId: `vela-action-${action.id}`, host: this.host }));
5770
+ } else {
5771
+ b.appendChild(doc.createTextNode(action.label));
5772
+ }
5532
5773
  b.addEventListener("click", () => {
5533
5774
  const c = this.opts.getContext?.();
5534
5775
  if (c) action.run(c);
@@ -5579,7 +5820,7 @@ var Topbar = class {
5579
5820
  this.tfMenu.destroy();
5580
5821
  this.styleMenu.destroy();
5581
5822
  this.layoutPicker?.destroy();
5582
- for (const t of [...this.tooltips, ...this.panelTooltips]) t.destroy();
5823
+ for (const t of [...this.tooltips, ...this.panelTooltips, ...this.actionTooltips]) t.destroy();
5583
5824
  this.el.remove();
5584
5825
  }
5585
5826
  /** One icon-only tool button with its kit tooltip, parked in `sink` for disposal. */
@@ -17315,19 +17556,8 @@ function registerBuiltinChartTypes() {
17315
17556
  registerChartType({ id: "heikinashi", label: "Heikin Ashi", barTransform: HEIKIN_ASHI });
17316
17557
  }
17317
17558
 
17318
- // src/workspace/sync.ts
17319
- function syncTargets(originId, setting, cellIds) {
17320
- if (setting == null || setting === false) return [];
17321
- if (setting === true) return cellIds.filter((id) => id !== originId);
17322
- const group = setting[originId];
17323
- if (group == null) return [];
17324
- return cellIds.filter((id) => id !== originId && setting[id] === group);
17325
- }
17326
- function rangesWithin(a, b, epsMs) {
17327
- return Math.abs(a.from - b.from) <= epsMs && Math.abs(a.to - b.to) <= epsMs;
17328
- }
17329
-
17330
17559
  // src/state/document.ts
17560
+ var SYNC_KINDS = ["viewport", "symbol", "timeframe", "crosshair", "drawings", "style"];
17331
17561
  function prefixedSymbol(cell) {
17332
17562
  if (!cell?.symbol) return void 0;
17333
17563
  if (cell.symbol.includes(":") || !cell.provider) return cell.symbol;
@@ -17415,7 +17645,7 @@ function sanitizeSync(raw) {
17415
17645
  if (raw == null || typeof raw !== "object") return null;
17416
17646
  const s = raw;
17417
17647
  const out = {};
17418
- for (const kind of ["viewport", "symbol", "timeframe", "crosshair", "drawings"]) {
17648
+ for (const kind of SYNC_KINDS) {
17419
17649
  const v = s[kind];
17420
17650
  if (v === true) out[kind] = true;
17421
17651
  else if (v != null && typeof v === "object") {
@@ -17458,6 +17688,28 @@ function sanitizeTrackSizes(raw) {
17458
17688
  return Object.keys(out).length > 0 ? out : null;
17459
17689
  }
17460
17690
 
17691
+ // src/workspace/sync.ts
17692
+ function syncTargets(originId, setting, cellIds) {
17693
+ if (setting == null || setting === false) return [];
17694
+ if (setting === true) return cellIds.filter((id) => id !== originId);
17695
+ const group = setting[originId];
17696
+ if (group == null) return [];
17697
+ return cellIds.filter((id) => id !== originId && setting[id] === group);
17698
+ }
17699
+ function rangesWithin(a, b, epsMs) {
17700
+ return Math.abs(a.from - b.from) <= epsMs && Math.abs(a.to - b.to) <= epsMs;
17701
+ }
17702
+ var STYLE_SYNC_CONFIG_KEYS = ["layout", "panes", "grid", "priceScale", "crosshair"];
17703
+ function styleConfigSlice(config) {
17704
+ if (config == null || typeof config !== "object") return null;
17705
+ const doc = config;
17706
+ const out = {};
17707
+ for (const key of STYLE_SYNC_CONFIG_KEYS) {
17708
+ if (doc[key] != null && typeof doc[key] === "object") out[key] = doc[key];
17709
+ }
17710
+ return Object.keys(out).length > 0 ? out : null;
17711
+ }
17712
+
17461
17713
  // src/widget/persist.ts
17462
17714
  function localStorageAdapter(storageKey) {
17463
17715
  return {
@@ -17550,6 +17802,7 @@ var IndicatorHandleImpl = class {
17550
17802
  constructor(id, title, controller, source) {
17551
17803
  this.controller = controller;
17552
17804
  this.schema = [];
17805
+ this.propsSchema = [];
17553
17806
  this.visibleState = true;
17554
17807
  this.bus = new TypedEventBus();
17555
17808
  this.id = id;
@@ -17559,6 +17812,9 @@ var IndicatorHandleImpl = class {
17559
17812
  get inputs() {
17560
17813
  return this.schema;
17561
17814
  }
17815
+ get props() {
17816
+ return this.propsSchema;
17817
+ }
17562
17818
  get visible() {
17563
17819
  return this.visibleState;
17564
17820
  }
@@ -17568,6 +17824,12 @@ var IndicatorHandleImpl = class {
17568
17824
  setInputs(values) {
17569
17825
  this.controller.applyInputs(this.id, values);
17570
17826
  }
17827
+ setProp(key, value) {
17828
+ this.controller.applyProps(this.id, { [key]: value });
17829
+ }
17830
+ setProps(values) {
17831
+ this.controller.applyProps(this.id, values);
17832
+ }
17571
17833
  setVisible(visible) {
17572
17834
  this.controller.setVisible(this.id, visible);
17573
17835
  }
@@ -17587,6 +17849,9 @@ var IndicatorHandleImpl = class {
17587
17849
  setSchema(schema) {
17588
17850
  this.schema = schema;
17589
17851
  }
17852
+ setPropsSchema(schema) {
17853
+ this.propsSchema = schema;
17854
+ }
17590
17855
  /** Sync the public `visible` getter when the orchestrator changes visibility (API or legend eye). */
17591
17856
  setVisibleState(visible) {
17592
17857
  this.visibleState = visible;
@@ -17600,6 +17865,8 @@ var IndicatorHandleImpl = class {
17600
17865
  function summarizeModel(model) {
17601
17866
  const series = {};
17602
17867
  for (const s of model.series) series[s.kind] = (series[s.kind] ?? 0) + 1;
17868
+ const countOverlay = (items) => items?.filter((i) => i.overlay === true).length ?? 0;
17869
+ const forcedOverlay = countOverlay(model.series) + countOverlay(model.fills) + countOverlay(model.backgrounds) + countOverlay(model.lines) + countOverlay(model.boxes) + countOverlay(model.labels) + countOverlay(model.polylines) + countOverlay(model.linefills) + countOverlay(model.tables);
17603
17870
  return {
17604
17871
  id: model.id,
17605
17872
  title: model.title,
@@ -17619,7 +17886,8 @@ function summarizeModel(model) {
17619
17886
  tables: model.tables?.length ?? 0,
17620
17887
  barColors: model.barColors?.length ?? 0,
17621
17888
  trades: model.trades?.length ?? 0,
17622
- inputs: model.inputs.length
17889
+ inputs: model.inputs.length,
17890
+ forcedOverlay
17623
17891
  };
17624
17892
  }
17625
17893
  function inspectModels(models) {
@@ -17640,7 +17908,8 @@ function inspectModels(models) {
17640
17908
  linefills: sum((s) => s.linefills),
17641
17909
  tables: sum((s) => s.tables),
17642
17910
  barColors: sum((s) => s.barColors),
17643
- trades: sum((s) => s.trades)
17911
+ trades: sum((s) => s.trades),
17912
+ forcedOverlay: sum((s) => s.forcedOverlay)
17644
17913
  }
17645
17914
  };
17646
17915
  }
@@ -17784,7 +18053,9 @@ var EngineOrchestrator = class _EngineOrchestrator {
17784
18053
  for (const engine of engines) this.registerEngine(engine.language, engine);
17785
18054
  this.defaultLanguage = config.defaultLanguage;
17786
18055
  this.renderer.mount(container, config.theme);
17787
- this.renderer.onInputChange((e) => this.applyInputs(e.indicatorId, { [e.key]: e.value }));
18056
+ this.renderer.onInputChange(
18057
+ (e) => e.kind === "prop" ? this.applyProps(e.indicatorId, { [e.key]: e.value }) : this.applyInputs(e.indicatorId, { [e.key]: e.value })
18058
+ );
17788
18059
  this.renderer.onRemoveIndicator((id) => this.removeIndicator(id));
17789
18060
  this.renderer.onToggleIndicatorVisible?.((id, visible) => this.setVisible(id, visible));
17790
18061
  this.paneActionUnsub = this.renderer.onPaneAction?.((a) => this.handlePaneAction(a)) ?? null;
@@ -18464,7 +18735,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
18464
18735
  const title = options.title ?? "Indicator";
18465
18736
  const handle = new IndicatorHandleImpl(id, title, this, source);
18466
18737
  this.handles.set(id, handle);
18467
- this.registry.add({ id, title, source, options, inputValues: { ...options.inputs ?? {} } });
18738
+ this.registry.add({ id, title, source, options, inputValues: { ...options.inputs ?? {} }, propValues: { ...options.props ?? {} } });
18468
18739
  void this.startIndicator(id, source, options, handle);
18469
18740
  return handle;
18470
18741
  }
@@ -18487,7 +18758,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
18487
18758
  return handle;
18488
18759
  }
18489
18760
  const inputValues = { ...descriptor.defaultInputs(), ...options.inputs ?? {} };
18490
- this.registry.add({ id, title, source: type, inputValues, native: { type, instance: descriptor.create(), descriptor } });
18761
+ this.registry.add({ id, title, source: type, inputValues, propValues: {}, native: { type, instance: descriptor.create(), descriptor } });
18491
18762
  handle.setSchema(descriptor.inputsSchema());
18492
18763
  void this.startNativeIndicator(id, handle);
18493
18764
  return handle;
@@ -18540,6 +18811,19 @@ var EngineOrchestrator = class _EngineOrchestrator {
18540
18811
  if (record.session) record.session.update(record.inputValues);
18541
18812
  else if (record.native && !record.hidden) record.native.instance.setInputs(record.inputValues);
18542
18813
  }
18814
+ /** IndicatorController: re-run an indicator with merged declaration-prop overrides.
18815
+ * Same lifecycle as {@link applyInputs} — a prop change replays the whole script.
18816
+ * Script indicators only: natives have no declaration props. */
18817
+ applyProps(id, values) {
18818
+ const record = this.registry.get(id);
18819
+ if (!record?.session) return;
18820
+ record.propValues = { ...record.propValues, ...values };
18821
+ if (record.renderHandle) this.renderer.setIndicatorInputs(record.renderHandle, record.inputValues, record.propValues);
18822
+ record.pendingStructural = true;
18823
+ if (!record.hidden) this.setLoading(record, true);
18824
+ record.pendingCause = "inputs";
18825
+ record.session.update(record.inputValues, record.propValues);
18826
+ }
18543
18827
  /** IndicatorController: tear down an indicator and (if now empty) its pane. */
18544
18828
  /** Live handles of every indicator on the chart (script + native), insertion order. */
18545
18829
  listIndicators() {
@@ -18688,6 +18972,10 @@ var EngineOrchestrator = class _EngineOrchestrator {
18688
18972
  for (const input of prepared.inputs) defaults2[input.key] = input.defval;
18689
18973
  record.inputValues = { ...defaults2, ...record.inputValues };
18690
18974
  handle.setSchema(prepared.inputs);
18975
+ const propDefaults = {};
18976
+ for (const prop of prepared.props ?? []) propDefaults[prop.key] = prop.defval;
18977
+ record.propValues = { ...propDefaults, ...record.propValues };
18978
+ handle.setPropsSchema(prepared.props ?? []);
18691
18979
  this.mountLoadingPlaceholder(id, record);
18692
18980
  this.executeIndicator(id, handle);
18693
18981
  } catch (err) {
@@ -18711,6 +18999,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
18711
18999
  getBars: () => this.bars,
18712
19000
  fetchSeries: (sym, tf, range) => this.fetchSeries(sym, tf, range),
18713
19001
  inputs: record.inputValues,
19002
+ props: record.propValues,
18714
19003
  visibleRange: this.currentVisibleRange(),
18715
19004
  mode,
18716
19005
  // Mid-backfill session starts (add / re-show / price-style re-execute) let the
@@ -18804,6 +19093,9 @@ var EngineOrchestrator = class _EngineOrchestrator {
18804
19093
  const meta = record.prepared.meta;
18805
19094
  const model = {
18806
19095
  id,
19096
+ // Deliberately the FULL title, never a shorttitle: while the script loads the
19097
+ // legend identifies it by its full name; the compact shorttitle arrives with
19098
+ // the first computed model and takes over from there.
18807
19099
  title: record.options?.title ?? meta.title,
18808
19100
  overlay: meta.overlay,
18809
19101
  paneHint: meta.overlay ? "price" : "new",
@@ -18812,7 +19104,8 @@ var EngineOrchestrator = class _EngineOrchestrator {
18812
19104
  backgrounds: [],
18813
19105
  priceLines: [],
18814
19106
  inputs: record.prepared.inputs,
18815
- inputValues: record.inputValues
19107
+ inputValues: record.inputValues,
19108
+ ...record.prepared.props ? { props: record.prepared.props, propValues: record.propValues } : {}
18816
19109
  };
18817
19110
  const paneId = this.routePane(id, model, record.options ?? {});
18818
19111
  this.placeModel(model, id, paneId);
@@ -19045,7 +19338,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
19045
19338
  record.model = model;
19046
19339
  if (record.pendingStructural) {
19047
19340
  record.renderHandle = this.renderer.mountIndicator(model);
19048
- this.renderer.setIndicatorInputs(record.renderHandle, record.inputValues);
19341
+ this.renderer.setIndicatorInputs(record.renderHandle, record.inputValues, record.propValues);
19049
19342
  record.pendingStructural = false;
19050
19343
  } else {
19051
19344
  this.renderer.updateIndicator(record.renderHandle, modelToValuePatch(model));
@@ -19072,16 +19365,17 @@ var EngineOrchestrator = class _EngineOrchestrator {
19072
19365
  placeModel(model, id, paneId) {
19073
19366
  model.id = id;
19074
19367
  model.paneId = paneId;
19075
- for (const series of model.series) series.paneId = paneId;
19076
- for (const fill of model.fills) fill.paneId = paneId;
19077
- for (const bg of model.backgrounds) bg.paneId = paneId;
19368
+ const paneOf = (item) => item.overlay === true ? "price" : paneId;
19369
+ for (const series of model.series) series.paneId = paneOf(series);
19370
+ for (const fill of model.fills) fill.paneId = paneOf(fill);
19371
+ for (const bg of model.backgrounds) bg.paneId = paneOf(bg);
19078
19372
  for (const line of model.priceLines) line.paneId = paneId;
19079
- if (model.lines) for (const ln of model.lines) ln.paneId = paneId;
19080
- if (model.boxes) for (const bx of model.boxes) bx.paneId = paneId;
19081
- if (model.labels) for (const lb of model.labels) lb.paneId = paneId;
19082
- if (model.polylines) for (const pl of model.polylines) pl.paneId = paneId;
19083
- if (model.linefills) for (const lf of model.linefills) lf.paneId = paneId;
19084
- if (model.tables) for (const tb of model.tables) tb.paneId = paneId;
19373
+ if (model.lines) for (const ln of model.lines) ln.paneId = paneOf(ln);
19374
+ if (model.boxes) for (const bx of model.boxes) bx.paneId = paneOf(bx);
19375
+ if (model.labels) for (const lb of model.labels) lb.paneId = paneOf(lb);
19376
+ if (model.polylines) for (const pl of model.polylines) pl.paneId = paneOf(pl);
19377
+ if (model.linefills) for (const lf of model.linefills) lf.paneId = paneOf(lf);
19378
+ if (model.tables) for (const tb of model.tables) tb.paneId = paneOf(tb);
19085
19379
  }
19086
19380
  emitContextChanged(id) {
19087
19381
  if (!this.registry.get(id)?.session?.getContext) return;
@@ -19234,6 +19528,15 @@ var RendererControl = class {
19234
19528
  setLegendActions(provider) {
19235
19529
  this.renderer.setLegendActions?.(provider);
19236
19530
  }
19531
+ /**
19532
+ * Wire the legend rows' HOST-CONTRIBUTED callout bubbles (the shells route the
19533
+ * plugin registry through this; see `registerLegendCallout`). Silent on a renderer
19534
+ * without the seam — contributed callouts simply never show there, same graceful
19535
+ * degradation as {@link setLegendActions}.
19536
+ */
19537
+ setLegendCallouts(provider) {
19538
+ this.renderer.setLegendCallouts?.(provider);
19539
+ }
19237
19540
  /**
19238
19541
  * Replace the indicator legend's fold toggle with a host action (or restore it with
19239
19542
  * `null`) — multi-chart shells point the chip at their indicator overview instead of
@@ -19743,6 +20046,10 @@ function inputVisible(when, values) {
19743
20046
  }
19744
20047
 
19745
20048
  // src/renderers/shared/IndicatorInputsDialog.ts
20049
+ var PROPS_TAB = "Properties";
20050
+ function bagOf(row, decl) {
20051
+ return decl.prop ? row.propValues : row.values;
20052
+ }
19746
20053
  var SOURCES = ["close", "open", "high", "low", "hl2", "hlc3", "ohlc4", "volume"];
19747
20054
  var IndicatorInputsDialog = class {
19748
20055
  constructor(host) {
@@ -19753,6 +20060,7 @@ var IndicatorInputsDialog = class {
19753
20060
  this.openId = null;
19754
20061
  this.row = null;
19755
20062
  this.snapshot = null;
20063
+ this.propSnapshot = null;
19756
20064
  this.dialogTips = [];
19757
20065
  /** Re-applies every `when` gate against current values; set while the dialog is open. */
19758
20066
  this.refreshVisibility = null;
@@ -19775,10 +20083,11 @@ var IndicatorInputsDialog = class {
19775
20083
  }
19776
20084
  open(row) {
19777
20085
  this.close();
19778
- if (row.inputs.length === 0) return;
20086
+ if (row.inputs.length === 0 && row.props.length === 0) return;
19779
20087
  this.row = row;
19780
20088
  this.openId = row.id;
19781
20089
  this.snapshot = { ...row.values };
20090
+ this.propSnapshot = { ...row.propValues };
19782
20091
  ensureDialogStyles();
19783
20092
  const t = this.host.theme();
19784
20093
  const border = "var(--vela-border)";
@@ -19786,7 +20095,7 @@ var IndicatorInputsDialog = class {
19786
20095
  const host = this.host.dialogHost() ?? this.host.container;
19787
20096
  const ui = new Dialog({
19788
20097
  host,
19789
- title: row.settingsTitle,
20098
+ title: row.title,
19790
20099
  // Non-modal: live-edit dialog — a modal machine locks pointer events on the
19791
20100
  // whole body, killing the chart and the body-portaled popovers.
19792
20101
  modal: false,
@@ -19798,12 +20107,16 @@ var IndicatorInputsDialog = class {
19798
20107
  closeOnEscape: false,
19799
20108
  footer: (foot) => {
19800
20109
  foot.append(
20110
+ this.resetAction(),
19801
20111
  this.dialogButton("Cancel", false, () => this.revertAndClose()),
19802
20112
  this.dialogButton("Ok", true, () => this.close())
19803
20113
  );
19804
20114
  },
20115
+ // Stale-guard: destroying a dialog fires its machine's onOpenChange(false)
20116
+ // asynchronously — after a reset rebuild that notification must not close
20117
+ // the replacement dialog.
19805
20118
  onOpenChange: (open2) => {
19806
- if (!open2) this.close();
20119
+ if (!open2 && this.uiDialog === ui) this.close();
19807
20120
  }
19808
20121
  });
19809
20122
  applyChromeTokens(ui.panel, t);
@@ -19814,6 +20127,16 @@ var IndicatorInputsDialog = class {
19814
20127
  text: () => "Close"
19815
20128
  }));
19816
20129
  const tabDefs = tabInputs(row.inputs);
20130
+ if (row.props.length > 0) {
20131
+ const propDecls = row.props.map((p) => {
20132
+ const d = { ...p, prop: true };
20133
+ delete d.tab;
20134
+ return d;
20135
+ });
20136
+ const existing = tabDefs.find((t2) => t2.name === PROPS_TAB);
20137
+ if (existing) existing.inputs.push(...propDecls);
20138
+ else tabDefs.push({ name: PROPS_TAB, inputs: propDecls });
20139
+ }
19817
20140
  const tabs = document.createElement("div");
19818
20141
  tabs.style.cssText = `display:flex;gap:12px;padding:0 20px;border-bottom:1px solid ${border};flex:0 0 auto;`;
19819
20142
  const tabEls = [];
@@ -19916,9 +20239,10 @@ var IndicatorInputsDialog = class {
19916
20239
  this.openId = null;
19917
20240
  this.row = null;
19918
20241
  this.snapshot = null;
20242
+ this.propSnapshot = null;
19919
20243
  ui?.destroy();
19920
20244
  }
19921
- /** Restore every input to its open-time value (re-running the indicator), then close. */
20245
+ /** Restore every input and prop to its open-time value (re-running the indicator), then close. */
19922
20246
  revertAndClose() {
19923
20247
  const row = this.row;
19924
20248
  const snap = this.snapshot;
@@ -19932,12 +20256,55 @@ var IndicatorInputsDialog = class {
19932
20256
  }
19933
20257
  }
19934
20258
  }
20259
+ const propSnap = this.propSnapshot;
20260
+ if (row && propSnap) {
20261
+ for (const p of row.props) {
20262
+ const snapped = propSnap[p.key];
20263
+ const before = snapped !== void 0 ? snapped : p.defval;
20264
+ if (row.propValues[p.key] !== before) {
20265
+ row.propValues[p.key] = before;
20266
+ this.host.onChange?.({ indicatorId: row.id, key: p.key, value: before, kind: "prop" });
20267
+ }
20268
+ }
20269
+ }
19935
20270
  this.close();
19936
20271
  }
20272
+ /** The footer's reset button — same chip as Cancel, pinned to the LEFT edge
20273
+ * (`margin-right:auto` against the footer's flex-end keeps Cancel/Ok right). */
20274
+ resetAction() {
20275
+ const b = this.dialogButton("Reset defaults", false, () => this.resetToDefaults());
20276
+ b.style.marginRight = "auto";
20277
+ return b;
20278
+ }
20279
+ /** Restore every input to its declared default (re-running the indicator), then
20280
+ * re-open the form so each control re-reads the restored values — the same
20281
+ * rebuild-after-reset move as the chart-settings dialog. The open-time snapshot
20282
+ * survives the rebuild, so Cancel after a reset still reverts the whole session. */
20283
+ resetToDefaults() {
20284
+ const row = this.row;
20285
+ if (!row) return;
20286
+ const snap = this.snapshot;
20287
+ const propSnap = this.propSnapshot;
20288
+ for (const inp of row.inputs) {
20289
+ if (row.values[inp.key] !== inp.defval) {
20290
+ row.values[inp.key] = inp.defval;
20291
+ this.host.onChange?.({ indicatorId: row.id, key: inp.key, value: inp.defval });
20292
+ }
20293
+ }
20294
+ for (const p of row.props) {
20295
+ if (row.propValues[p.key] !== p.defval) {
20296
+ row.propValues[p.key] = p.defval;
20297
+ this.host.onChange?.({ indicatorId: row.id, key: p.key, value: p.defval, kind: "prop" });
20298
+ }
20299
+ }
20300
+ this.open(row);
20301
+ if (snap) this.snapshot = snap;
20302
+ if (propSnap) this.propSnapshot = propSnap;
20303
+ }
19937
20304
  /** Write one edit through: store it, notify the host, and re-apply the `when` gates. */
19938
- commit(row, key, value) {
19939
- row.values[key] = value;
19940
- this.host.onChange?.({ indicatorId: row.id, key, value });
20305
+ commit(row, decl, value) {
20306
+ bagOf(row, decl)[decl.key] = value;
20307
+ this.host.onChange?.({ indicatorId: row.id, key: decl.key, value, ...decl.prop ? { kind: "prop" } : {} });
19941
20308
  this.refreshVisibility?.();
19942
20309
  }
19943
20310
  /** One settings row (or several `inline=` inputs) placed into a section's grid. */
@@ -19964,7 +20331,7 @@ var IndicatorInputsDialog = class {
19964
20331
  const id = idOf(lead);
19965
20332
  const info = lead.tooltip ? this.infoButton(lead.tooltip) : void 0;
19966
20333
  if (lead.type === "bool") {
19967
- const current = Boolean(row.values[lead.key] ?? lead.defval);
20334
+ const current = Boolean(bagOf(row, lead)[lead.key] ?? lead.defval);
19968
20335
  return append(fieldRow({
19969
20336
  label: nameOf(lead),
19970
20337
  id,
@@ -19973,7 +20340,7 @@ var IndicatorInputsDialog = class {
19973
20340
  toggle: {
19974
20341
  id,
19975
20342
  checked: current,
19976
- onChange: (v) => this.commit(row, lead.key, v)
20343
+ onChange: (v) => this.commit(row, lead, v)
19977
20344
  }
19978
20345
  }));
19979
20346
  }
@@ -19998,8 +20365,8 @@ var IndicatorInputsDialog = class {
19998
20365
  }
19999
20366
  /** Build the typed control for one input, committing edits live via `onChange`. */
20000
20367
  buildControl(row, inp, id) {
20001
- const current = row.values[inp.key] ?? inp.defval;
20002
- const emit = (value) => this.commit(row, inp.key, value);
20368
+ const current = bagOf(row, inp)[inp.key] ?? inp.defval;
20369
+ const emit = (value) => this.commit(row, inp, value);
20003
20370
  if (inp.type === "bool") {
20004
20371
  return buildFieldControl({ kind: "switch", id, checked: Boolean(current), onChange: (v) => emit(v) }).el;
20005
20372
  }
@@ -20010,7 +20377,7 @@ var IndicatorInputsDialog = class {
20010
20377
  kind: "color",
20011
20378
  id,
20012
20379
  theme: this.host.theme(),
20013
- get: () => String(row.values[inp.key] ?? inp.defval),
20380
+ get: () => String(bagOf(row, inp)[inp.key] ?? inp.defval),
20014
20381
  onChange: (v) => emit(v)
20015
20382
  }).el;
20016
20383
  }
@@ -20573,6 +20940,7 @@ function timeParts(ts) {
20573
20940
  }
20574
20941
 
20575
20942
  // src/renderers/shared/InputsUI.ts
20943
+ var LEGEND_AT_TOP_ATTR = "data-vela-pane-at-top";
20576
20944
  var STATUS_KEYFRAMES = "@keyframes vela-ind-pulse{0%,100%{opacity:1;transform:scale(1)}50%{opacity:.3;transform:scale(.6)}}";
20577
20945
  var LEGEND_ICON_PX2 = 16;
20578
20946
  var LEGEND_CTL_PX = 18;
@@ -20611,10 +20979,14 @@ var InputsUI = class {
20611
20979
  this.moveApi = null;
20612
20980
  /** Host-contributed legend actions, resolved PER ROW at render time (see setLegendActions). */
20613
20981
  this.legendActions = null;
20982
+ /** Host-contributed callout bubbles, resolved PER ROW at render time (see setLegendCallouts). */
20983
+ this.legendCallouts = null;
20614
20984
  /** Chrome-tooltip disposers, per row id — a tip open at removal must not outlive its row. */
20615
20985
  this.rowTips = /* @__PURE__ */ new Map();
20616
20986
  /** Same, for the contributed extras only (rebuilt independently by setLegendActions). */
20617
20987
  this.extrasTips = /* @__PURE__ */ new Map();
20988
+ /** Same, for the contributed callouts only (rebuilt independently by setLegendCallouts). */
20989
+ this.calloutTips = /* @__PURE__ */ new Map();
20618
20990
  /** Open "Move to" menu (kept so it can be torn down). */
20619
20991
  this.moveMenu = null;
20620
20992
  this.moveTargets = [];
@@ -20772,6 +21144,42 @@ var InputsUI = class {
20772
21144
  extrasEl.appendChild(btn2);
20773
21145
  }
20774
21146
  }
21147
+ /**
21148
+ * Wire the host-contributed callout bubbles. Same contract as
21149
+ * {@link setLegendActions}: re-calling replaces the provider and re-projects the
21150
+ * rows already on screen.
21151
+ */
21152
+ setLegendCallouts(provider) {
21153
+ this.legendCallouts = provider;
21154
+ for (const row of this.rows.values()) this.renderCallouts(row);
21155
+ }
21156
+ /** (Re)build one row's callout bubbles — tinted icon circles beside the title whose
21157
+ * click (when the view carries content) deploys a panel of text and actions. */
21158
+ renderCallouts(row) {
21159
+ this.disposeTips(this.calloutTips, row.id);
21160
+ for (const bubble of row.callouts) bubble.destroy();
21161
+ row.callouts = [];
21162
+ row.calloutsEl.replaceChildren();
21163
+ const views = this.legendCallouts?.(row.id) ?? [];
21164
+ row.calloutsEl.style.display = views.length > 0 ? "inline-flex" : "none";
21165
+ for (const view of views) {
21166
+ const bubble = new CalloutBubble({
21167
+ icon: view.icon,
21168
+ background: view.background,
21169
+ ...view.color !== void 0 ? { color: view.color } : {},
21170
+ label: view.tooltip,
21171
+ ...view.content !== void 0 ? { panel: view.content } : {},
21172
+ // The panel portals into the plot host and self-tokens: it must work on
21173
+ // a bare chart, where no `.vela-ui` token ancestor exists.
21174
+ host: this.container,
21175
+ theme: () => this.theme
21176
+ });
21177
+ bubble.el.dataset.legendCallout = view.id;
21178
+ this.tip(this.calloutTips, row.id, bubble.el, view.tooltip);
21179
+ row.callouts.push(bubble);
21180
+ row.calloutsEl.appendChild(bubble.el);
21181
+ }
21182
+ }
20775
21183
  /** Reposition the per-pane legend containers after a layout change. */
20776
21184
  reposition() {
20777
21185
  for (const [paneId, lg] of this.legends) this.positionLegend(lg, paneId);
@@ -20939,6 +21347,7 @@ var InputsUI = class {
20939
21347
  }
20940
21348
  positionLegend(lg, paneId) {
20941
21349
  const bounds = this.paneBoundsOf ? this.paneBoundsOf(paneId) : { top: 0, height: Infinity };
21350
+ lg.toggleAttribute(LEGEND_AT_TOP_ATTR, bounds.top === 0);
20942
21351
  lg.style.display = bounds.height < 4 || !this.titlesVisible ? "none" : "flex";
20943
21352
  const collapsed = this.paneCollapse.has(paneId);
20944
21353
  const masterId = this.paneCollapse.get(paneId) ?? null;
@@ -21014,13 +21423,13 @@ var InputsUI = class {
21014
21423
  }
21015
21424
  /** Create or update an indicator's legend row (in the legend for its pane). */
21016
21425
  upsert(id, title, inputs, values, paneId = "price", opts = {}) {
21017
- const settingsTitle = opts.settingsTitle ?? title;
21018
21426
  const existing = this.rows.get(id);
21019
21427
  if (existing) {
21020
21428
  existing.title = title;
21021
- existing.settingsTitle = settingsTitle;
21022
21429
  existing.inputs = inputs;
21023
21430
  existing.values = { ...values };
21431
+ existing.props = opts.props ?? [];
21432
+ existing.propValues = { ...opts.propValues ?? {} };
21024
21433
  existing.titleEl.textContent = title;
21025
21434
  if (existing.paneId !== paneId) {
21026
21435
  existing.paneId = paneId;
@@ -21072,6 +21481,9 @@ var InputsUI = class {
21072
21481
  titleWrap.appendChild(beta);
21073
21482
  }
21074
21483
  el.appendChild(titleWrap);
21484
+ const calloutsEl = document.createElement("span");
21485
+ calloutsEl.style.cssText = `display:none;align-items:center;gap:4px;flex:none;margin-left:${LEGEND_TITLE_STATUS_GAP_PX}px;`;
21486
+ el.appendChild(calloutsEl);
21075
21487
  el.appendChild(statusEl);
21076
21488
  const valuesEl = document.createElement("span");
21077
21489
  valuesEl.style.cssText = `display:none;align-items:center;gap:5px;margin-left:${LEGEND_TITLE_VALUES_GAP_PX}px;white-space:nowrap;font-variant-numeric:tabular-nums;`;
@@ -21088,13 +21500,13 @@ var InputsUI = class {
21088
21500
  eye.className = "vela-ind-ctl";
21089
21501
  eye.style.cssText = LEGEND_CTL_CSS;
21090
21502
  eye.addEventListener("click", () => {
21091
- const row = this.rows.get(id);
21092
- this.onToggleVisible?.(id, Boolean(row?.hidden));
21503
+ const row2 = this.rows.get(id);
21504
+ this.onToggleVisible?.(id, Boolean(row2?.hidden));
21093
21505
  });
21094
21506
  controlsEl.appendChild(eye);
21095
21507
  eyeEl = eye;
21096
21508
  }
21097
- if (inputs.length > 0) {
21509
+ if (inputs.length > 0 || (opts.props ?? []).length > 0) {
21098
21510
  const gear = document.createElement("button");
21099
21511
  gear.type = "button";
21100
21512
  gear.setAttribute("aria-label", "Settings");
@@ -21134,7 +21546,9 @@ var InputsUI = class {
21134
21546
  controlsEl.appendChild(close);
21135
21547
  el.appendChild(controlsEl);
21136
21548
  this.attach(this.legendFor(paneId), el, !!opts.native);
21137
- this.rows.set(id, { id, title, settingsTitle, inputs, values: { ...values }, el, titleEl, statusEl, valuesEl, plotValues: [], plotValuesKey: "", showValues: null, highlighted: false, paneId, hidden: false, eyeEl, controlsEl, extrasEl, native: !!opts.native });
21549
+ const row = { id, title, inputs, values: { ...values }, props: opts.props ?? [], propValues: { ...opts.propValues ?? {} }, el, titleEl, statusEl, valuesEl, plotValues: [], plotValuesKey: "", showValues: null, highlighted: false, paneId, hidden: false, eyeEl, controlsEl, extrasEl, calloutsEl, callouts: [], native: !!opts.native };
21550
+ this.rows.set(id, row);
21551
+ this.renderCallouts(row);
21138
21552
  this.syncFoldToggle();
21139
21553
  }
21140
21554
  /** Place a row in its pane's legend — native rows PREPEND (pinned to the top), Pine rows append. */
@@ -21143,9 +21557,11 @@ var InputsUI = class {
21143
21557
  else container.appendChild(el);
21144
21558
  }
21145
21559
  /** Reflect programmatic input changes (so a re-opened dialog shows current values). */
21146
- setValues(id, values) {
21560
+ setValues(id, values, props) {
21147
21561
  const row = this.rows.get(id);
21148
- if (row) row.values = { ...row.values, ...values };
21562
+ if (!row) return;
21563
+ row.values = { ...row.values, ...values };
21564
+ if (props) row.propValues = { ...row.propValues, ...props };
21149
21565
  }
21150
21566
  /**
21151
21567
  * Reflect an indicator's live status in its legend row: `'loading'` shows three pulsing
@@ -21227,8 +21643,13 @@ var InputsUI = class {
21227
21643
  syncRowActions(row) {
21228
21644
  const open2 = row.highlighted;
21229
21645
  row.controlsEl.style.display = open2 || row.hidden ? "inline-flex" : "none";
21230
- if (open2) row.el.appendChild(row.statusEl);
21231
- else row.el.insertBefore(row.statusEl, row.valuesEl);
21646
+ if (open2) {
21647
+ row.el.appendChild(row.statusEl);
21648
+ row.el.appendChild(row.calloutsEl);
21649
+ } else {
21650
+ row.el.insertBefore(row.statusEl, row.valuesEl);
21651
+ row.el.insertBefore(row.calloutsEl, row.statusEl);
21652
+ }
21232
21653
  for (const child of Array.from(row.controlsEl.children)) {
21233
21654
  if (!(child instanceof HTMLElement) || child === row.eyeEl) continue;
21234
21655
  if (child === row.extrasEl) {
@@ -21246,10 +21667,12 @@ var InputsUI = class {
21246
21667
  }
21247
21668
  remove(id) {
21248
21669
  const row = this.rows.get(id);
21670
+ for (const bubble of row?.callouts ?? []) bubble.destroy();
21249
21671
  row?.el.remove();
21250
21672
  this.rows.delete(id);
21251
21673
  this.disposeTips(this.rowTips, id);
21252
21674
  this.disposeTips(this.extrasTips, id);
21675
+ this.disposeTips(this.calloutTips, id);
21253
21676
  if (this.selectedId === id) this.selectedId = null;
21254
21677
  if (this.inputsDialog.openId === id) this.inputsDialog.close();
21255
21678
  this.syncFoldToggle();
@@ -21269,6 +21692,8 @@ var InputsUI = class {
21269
21692
  this.rowMenu = null;
21270
21693
  for (const id of [...this.rowTips.keys()]) this.disposeTips(this.rowTips, id);
21271
21694
  for (const id of [...this.extrasTips.keys()]) this.disposeTips(this.extrasTips, id);
21695
+ for (const id of [...this.calloutTips.keys()]) this.disposeTips(this.calloutTips, id);
21696
+ for (const row of this.rows.values()) for (const bubble of row.callouts) bubble.destroy();
21272
21697
  for (const lg of this.legends.values()) lg.remove();
21273
21698
  this.legends.clear();
21274
21699
  this.rows.clear();
@@ -21623,6 +22048,32 @@ var SIZE_PX3 = {
21623
22048
  large: 16,
21624
22049
  huge: 20
21625
22050
  };
22051
+ function fontPxOf(size) {
22052
+ if (typeof size === "number") return size > 0 ? size : SIZE_PX3.auto;
22053
+ return SIZE_PX3[size] ?? SIZE_PX3.auto;
22054
+ }
22055
+ function tableHasContent(t) {
22056
+ return t.cells.some((row) => row?.some((c) => c != null && !c.merged));
22057
+ }
22058
+ function mergeRenderPlan(t) {
22059
+ const span = /* @__PURE__ */ new Map();
22060
+ const omit = /* @__PURE__ */ new Set();
22061
+ for (const m of t.merges) {
22062
+ span.set(`${m.startRow}:${m.startCol}`, { cs: m.endCol - m.startCol + 1, rs: m.endRow - m.startRow + 1 });
22063
+ for (let r = m.startRow; r <= m.endRow; r += 1) {
22064
+ for (let c = m.startCol; c <= m.endCol; c += 1) {
22065
+ if (r !== m.startRow || c !== m.startCol) omit.add(`${r}:${c}`);
22066
+ }
22067
+ }
22068
+ }
22069
+ for (let r = 0; r < t.rows; r += 1) {
22070
+ for (let c = 0; c < t.columns; c += 1) {
22071
+ if (t.cells[r]?.[c]?.merged && !span.has(`${r}:${c}`)) omit.add(`${r}:${c}`);
22072
+ }
22073
+ }
22074
+ for (const key of span.keys()) omit.delete(key);
22075
+ return { span, omit };
22076
+ }
21626
22077
  var TableOverlay = class {
21627
22078
  constructor(container, theme, paneBounds) {
21628
22079
  this.container = container;
@@ -21643,7 +22094,9 @@ var TableOverlay = class {
21643
22094
  update(tables) {
21644
22095
  this.lastTables = tables;
21645
22096
  this.root.replaceChildren();
21646
- for (const t of tables) this.root.appendChild(this.renderTable(t));
22097
+ for (const t of tables) {
22098
+ if (tableHasContent(t)) this.root.appendChild(this.renderTable(t));
22099
+ }
21647
22100
  }
21648
22101
  /** Re-render at the current pane geometry — after layout settles or on resize. */
21649
22102
  reposition() {
@@ -21658,39 +22111,36 @@ var TableOverlay = class {
21658
22111
  this.root.remove();
21659
22112
  }
21660
22113
  renderTable(t) {
22114
+ const b = this.paneBounds(t.paneId);
21661
22115
  const wrap = document.createElement("div");
21662
22116
  wrap.style.position = "absolute";
21663
- this.anchor(wrap, t.position, this.paneBounds(t.paneId));
22117
+ if (t.frameColor && t.frameWidth > 0) wrap.style.border = `${t.frameWidth}px solid ${t.frameColor}`;
22118
+ this.anchor(wrap, t.position, b);
21664
22119
  const table = document.createElement("table");
21665
22120
  Object.assign(table.style, {
21666
22121
  borderCollapse: "collapse",
21667
22122
  background: t.bgColor ?? "transparent",
21668
22123
  fontFamily: this.theme.fontFamily || "sans-serif",
21669
- // Frame as an inset shadow (not a `border`) so it stays independent of the
21670
- // collapsed cell borders even when frame_width != border_width.
21671
- boxShadow: t.frameColor && t.frameWidth > 0 ? `inset 0 0 0 ${t.frameWidth}px ${t.frameColor}` : "none",
21672
22124
  border: "none",
21673
22125
  tableLayout: "auto",
21674
22126
  // Re-enable pointer events on the table only (root is none) so cell tooltips work.
21675
22127
  pointerEvents: "auto"
21676
22128
  });
21677
- const span = /* @__PURE__ */ new Map();
21678
- const skip = /* @__PURE__ */ new Set();
21679
- for (const m of t.merges) {
21680
- span.set(`${m.startRow}:${m.startCol}`, { cs: m.endCol - m.startCol + 1, rs: m.endRow - m.startRow + 1 });
21681
- for (let r = m.startRow; r <= m.endRow; r += 1) {
21682
- for (let c = m.startCol; c <= m.endCol; c += 1) {
21683
- if (r !== m.startRow || c !== m.startCol) skip.add(`${r}:${c}`);
21684
- }
21685
- }
21686
- }
22129
+ const { span, omit } = mergeRenderPlan(t);
22130
+ const plotW = Math.max(0, (this.root.clientWidth || this.container.clientWidth) - b.rightAxis);
22131
+ const paneH = b.height;
21687
22132
  const cellBorder = t.borderColor && t.borderWidth > 0 ? `${t.borderWidth}px solid ${t.borderColor}` : "none";
21688
22133
  for (let r = 0; r < t.rows; r += 1) {
21689
22134
  const tr = document.createElement("tr");
21690
22135
  for (let c = 0; c < t.columns; c += 1) {
22136
+ if (omit.has(`${r}:${c}`)) continue;
21691
22137
  const cell = t.cells[r]?.[c] ?? null;
21692
- if (skip.has(`${r}:${c}`) || cell?.merged) continue;
21693
22138
  const td = document.createElement("td");
22139
+ if (cell === null) {
22140
+ Object.assign(td.style, { padding: "0", border: "none" });
22141
+ tr.appendChild(td);
22142
+ continue;
22143
+ }
21694
22144
  const sp = span.get(`${r}:${c}`);
21695
22145
  if (sp) {
21696
22146
  if (sp.cs > 1) td.colSpan = sp.cs;
@@ -21699,18 +22149,22 @@ var TableOverlay = class {
21699
22149
  Object.assign(td.style, {
21700
22150
  border: cellBorder,
21701
22151
  padding: "2px 6px",
21702
- background: cell?.bgColor ?? "transparent",
21703
- color: cell?.textColor ?? this.theme.textColor,
21704
- textAlign: cell?.hAlign ?? "center",
21705
- verticalAlign: cell?.vAlign === "top" ? "top" : cell?.vAlign === "bottom" ? "bottom" : "middle",
21706
- fontSize: `${SIZE_PX3[cell?.textSize ?? "normal"]}px`,
21707
- fontFamily: cell?.fontFamily === "monospace" ? "monospace" : "inherit",
21708
- fontWeight: cell?.bold ? "bold" : "normal",
21709
- fontStyle: cell?.italic ? "italic" : "normal",
21710
- whiteSpace: "pre-line"
22152
+ background: cell.bgColor ?? "transparent",
22153
+ color: cell.textColor ?? this.theme.textColor,
22154
+ textAlign: cell.hAlign,
22155
+ verticalAlign: cell.vAlign === "top" ? "top" : cell.vAlign === "bottom" ? "bottom" : "middle",
22156
+ fontSize: `${fontPxOf(cell.textSize)}px`,
22157
+ fontFamily: cell.fontFamily === "monospace" ? "monospace" : "inherit",
22158
+ fontWeight: cell.bold ? "bold" : "normal",
22159
+ fontStyle: cell.italic ? "italic" : "normal",
22160
+ // Pine cell text never wraps; `\n` still breaks lines. Wrapping
22161
+ // used to collapse unicode sparklines and ━━━ dividers.
22162
+ whiteSpace: "pre"
21711
22163
  });
21712
- if (cell?.tooltip) td.title = cell.tooltip;
21713
- td.textContent = cell?.text ?? "";
22164
+ if (cell.width) td.style.width = `${cell.width / 100 * plotW}px`;
22165
+ if (cell.height) td.style.height = `${cell.height / 100 * paneH}px`;
22166
+ if (cell.tooltip) td.title = cell.tooltip;
22167
+ td.textContent = cell.text ?? "";
21714
22168
  tr.appendChild(td);
21715
22169
  }
21716
22170
  table.appendChild(tr);
@@ -22312,8 +22766,14 @@ var WebGL2Backend = class {
22312
22766
  return sc === pane.scale ? pane : { ...pane, scale: sc };
22313
22767
  };
22314
22768
  b.alpha = this.modelAlpha;
22315
- for (const m of models) for (const bgSpan of m.backgrounds) this.emitBackground(b, bgSpan, pane, coords);
22316
- for (const m of models) for (const f of m.fills) this.emitFill(b, m, f, effPane(m), coords, i0, i1, scene.offsetOf(m.id));
22769
+ for (const m of models) for (const bgSpan of m.backgrounds) if (bgSpan.overlay !== true) this.emitBackground(b, bgSpan, pane, coords);
22770
+ for (const m of models) for (const f of m.fills) if (f.overlay !== true) this.emitFill(b, m, f, effPane(m), coords, i0, i1, scene.offsetOf(m.id));
22771
+ if (isPrice) {
22772
+ for (const m of scene.indicators.values()) {
22773
+ for (const bgSpan of m.backgrounds) if (bgSpan.overlay === true) this.emitBackground(b, bgSpan, pane, coords);
22774
+ for (const f of m.fills) if (f.overlay === true) this.emitFill(b, m, f, pane, coords, i0, i1, scene.offsetOf(m.id));
22775
+ }
22776
+ }
22317
22777
  const drawCandles = isPrice && !scene.candlesHidden;
22318
22778
  let candleDrawn = false;
22319
22779
  for (const m of models) {
@@ -22327,7 +22787,7 @@ var WebGL2Backend = class {
22327
22787
  b.alpha = this.modelAlpha;
22328
22788
  const off = scene.offsetOf(m.id);
22329
22789
  const mp = effPane(m);
22330
- for (const s of m.series) this.emitSeries(b, s, mp, coords, i0, i1, theme, off);
22790
+ for (const s of m.series) if (s.overlay !== true) this.emitSeries(b, s, mp, coords, i0, i1, theme, off);
22331
22791
  }
22332
22792
  if (drawCandles && !candleDrawn) {
22333
22793
  drawSlicesUpTo(scene.candleZ);
@@ -22336,6 +22796,12 @@ var WebGL2Backend = class {
22336
22796
  }
22337
22797
  drawSlicesUpTo(Infinity);
22338
22798
  b.alpha = this.modelAlpha;
22799
+ if (isPrice) {
22800
+ for (const m of scene.indicators.values()) {
22801
+ const off = scene.offsetOf(m.id);
22802
+ for (const s of m.series) if (s.overlay === true) this.emitSeries(b, s, pane, coords, i0, i1, theme, off);
22803
+ }
22804
+ }
22339
22805
  for (const m of models) {
22340
22806
  const mp = effPane(m);
22341
22807
  for (const pl of m.priceLines) this.emitHline(b, pl, mp, coords, dataW, theme);
@@ -22382,7 +22848,7 @@ var WebGL2Backend = class {
22382
22848
  for (const pane of scene.orderedPanes()) {
22383
22849
  const b = this.glowBatch;
22384
22850
  b.reset();
22385
- this.emitGlowSources(b, scene.indicatorsForPane(pane.id), pane, coords, i0, i1, (id) => scene.offsetOf(id));
22851
+ this.emitGlowSources(b, scene, pane, coords, i0, i1);
22386
22852
  if (b.vertexCount === 0) continue;
22387
22853
  const topH = Math.round(pane.bounds.top * dpr * 0.5);
22388
22854
  const botH = Math.round((pane.bounds.top + pane.bounds.height) * dpr * 0.5);
@@ -22451,15 +22917,23 @@ var WebGL2Backend = class {
22451
22917
  gl.bindVertexArray(null);
22452
22918
  return true;
22453
22919
  }
22454
- /** The "neon" elements that glow: line/area/step lines + point markers (not candles/fills/bars). */
22455
- emitGlowSources(b, models, pane, coords, i0, i1, offsetOf = () => 0) {
22456
- for (const m of models) {
22457
- const off = offsetOf(m.id);
22458
- for (const s of m.series) {
22459
- if (!isLineLikeSeries(s) || s.visible === false) continue;
22460
- if (s.kind === "histogram" || s.kind === "columns") continue;
22461
- if (s.kind === "circles" || s.kind === "cross") this.emitPointMarkers(b, s, pane, coords, i0, i1, off);
22462
- else this.emitPolyline(b, s, pane, coords, i0, i1, s.kind === "step", off);
22920
+ /** The "neon" elements that glow: line/area/step lines + point markers (not candles/fills/bars).
22921
+ * Routing mirrors the main pass: own series per pane, force_overlay series on the price pane. */
22922
+ emitGlowSources(b, scene, pane, coords, i0, i1) {
22923
+ const emitOne = (s, off) => {
22924
+ if (!isLineLikeSeries(s) || s.visible === false) return;
22925
+ if (s.kind === "histogram" || s.kind === "columns") return;
22926
+ if (s.kind === "circles" || s.kind === "cross") this.emitPointMarkers(b, s, pane, coords, i0, i1, off);
22927
+ else this.emitPolyline(b, s, pane, coords, i0, i1, s.kind === "step", off);
22928
+ };
22929
+ for (const m of scene.indicatorsForPane(pane.id)) {
22930
+ const off = scene.offsetOf(m.id);
22931
+ for (const s of m.series) if (s.overlay !== true) emitOne(s, off);
22932
+ }
22933
+ if (pane.kind === "price") {
22934
+ for (const m of scene.indicators.values()) {
22935
+ const off = scene.offsetOf(m.id);
22936
+ for (const s of m.series) if (s.overlay === true) emitOne(s, off);
22463
22937
  }
22464
22938
  }
22465
22939
  }
@@ -22742,9 +23216,9 @@ var WebGL2Backend = class {
22742
23216
  b.alpha = this.candleBodyAlpha;
22743
23217
  b.rect(g.bodyX, bodyTop, g.bodyW, bodyH, c);
22744
23218
  }
22745
- if (cs.borderVisible || bc || fading && cs.bodyVisible) {
23219
+ if (cs.borderVisible || fading && cs.bodyVisible) {
22746
23220
  b.alpha = this.candleStructureAlpha;
22747
- const bord = cs.borderVisible || bc ? parseColor((isUp ? cs.borderUpColor : cs.borderDownColor) ?? dir) : c;
23221
+ const bord = cs.borderVisible ? parseColor((isUp ? cs.borderUpColor : cs.borderDownColor) ?? bodyColorStr) : c;
22748
23222
  const bw = Math.max(0, g.bodyW - 1);
22749
23223
  const bh = Math.max(0, bodyH - 1);
22750
23224
  b.rectStroke(g.bodyX + 0.5, bodyTop + 0.5, bw, bh, 1, bord);
@@ -24296,8 +24770,14 @@ var Canvas2dBackend = class {
24296
24770
  return sc === pane.scale ? pane : { ...pane, scale: sc };
24297
24771
  };
24298
24772
  ctx.globalAlpha = this.modelAlpha;
24299
- for (const m of models) for (const bg of m.backgrounds) this.drawBackground(ctx, bg, pane, coords);
24300
- for (const m of models) for (const f of m.fills) this.drawFill(ctx, m, f, effPane(m), coords, i0, i1, scene.offsetOf(m.id));
24773
+ for (const m of models) for (const bg of m.backgrounds) if (bg.overlay !== true) this.drawBackground(ctx, bg, pane, coords);
24774
+ for (const m of models) for (const f of m.fills) if (f.overlay !== true) this.drawFill(ctx, m, f, effPane(m), coords, i0, i1, scene.offsetOf(m.id));
24775
+ if (isPrice) {
24776
+ for (const m of scene.indicators.values()) {
24777
+ for (const bg of m.backgrounds) if (bg.overlay === true) this.drawBackground(ctx, bg, pane, coords);
24778
+ for (const f of m.fills) if (f.overlay === true) this.drawFill(ctx, m, f, pane, coords, i0, i1, scene.offsetOf(m.id));
24779
+ }
24780
+ }
24301
24781
  const slices = scene.drawingSlices.get(pane.id) ?? [];
24302
24782
  let si = 0;
24303
24783
  const drawSlicesUpTo = (z) => {
@@ -24319,7 +24799,7 @@ var Canvas2dBackend = class {
24319
24799
  ctx.globalAlpha = this.modelAlpha;
24320
24800
  const off = scene.offsetOf(m.id);
24321
24801
  const mp = effPane(m);
24322
- for (const s of m.series) this.drawSeries(ctx, s, mp, coords, i0, i1, theme, off);
24802
+ for (const s of m.series) if (s.overlay !== true) this.drawSeries(ctx, s, mp, coords, i0, i1, theme, off);
24323
24803
  }
24324
24804
  if (drawCandles && !candleDrawn) {
24325
24805
  drawSlicesUpTo(scene.candleZ);
@@ -24327,6 +24807,13 @@ var Canvas2dBackend = class {
24327
24807
  this.drawPriceSeries(ctx, scene, i0, i1, coords, pane, theme, barColorMap, dataW);
24328
24808
  }
24329
24809
  drawSlicesUpTo(Infinity);
24810
+ if (isPrice) {
24811
+ ctx.globalAlpha = this.modelAlpha;
24812
+ for (const m of scene.indicators.values()) {
24813
+ const off = scene.offsetOf(m.id);
24814
+ for (const s of m.series) if (s.overlay === true) this.drawSeries(ctx, s, pane, coords, i0, i1, theme, off);
24815
+ }
24816
+ }
24330
24817
  ctx.globalAlpha = this.modelAlpha;
24331
24818
  for (const m of models) {
24332
24819
  const mp = effPane(m);
@@ -24598,9 +25085,9 @@ var Canvas2dBackend = class {
24598
25085
  ctx.fillStyle = color;
24599
25086
  ctx.fillRect(g.bodyX, top, g.bodyW, bodyH);
24600
25087
  }
24601
- if (cs.borderVisible || bc || fading && cs.bodyVisible) {
25088
+ if (cs.borderVisible || fading && cs.bodyVisible) {
24602
25089
  ctx.globalAlpha = this.candleStructureAlpha;
24603
- ctx.strokeStyle = cs.borderVisible || bc ? (up ? cs.borderUpColor : cs.borderDownColor) ?? dir : color;
25090
+ ctx.strokeStyle = cs.borderVisible ? (up ? cs.borderUpColor : cs.borderDownColor) ?? color : color;
24604
25091
  ctx.lineWidth = 1;
24605
25092
  const bw = Math.max(0, g.bodyW - 1);
24606
25093
  const bh = Math.max(0, bodyH - 1);
@@ -25032,6 +25519,8 @@ var DrawingSceneRenderer = class {
25032
25519
  this.set = set;
25033
25520
  /** measureText width cache, keyed by `${font} ${text}`, persists across frames. */
25034
25521
  this.widthCache = /* @__PURE__ */ new Map();
25522
+ /** Tooltip hit-rects captured while drawing the CURRENT set (rebuilt per render). */
25523
+ this.tipRegions = [];
25035
25524
  /**
25036
25525
  * Index offset of the CURRENT set's model: its `xloc:'bar_index'` coordinates count
25037
25526
  * from the model's anchor bar, so they shift by this to land on chart logical indices.
@@ -25052,8 +25541,13 @@ var DrawingSceneRenderer = class {
25052
25541
  const s = this.set;
25053
25542
  return !s.lines.length && !s.boxes.length && !s.labels.length && !s.polylines.length && !s.linefills.length;
25054
25543
  }
25544
+ /** Tooltip hit-rects of the labels drawn by the LAST `render` call (same coords as `ctx`). */
25545
+ labelTipRegions() {
25546
+ return this.tipRegions;
25547
+ }
25055
25548
  /** Draw the whole set into `ctx` using the supplied coordinate closures. */
25056
25549
  render(ctx, W, H, xOf, yOf) {
25550
+ this.tipRegions = [];
25057
25551
  if (this.isEmpty()) return;
25058
25552
  ctx.save();
25059
25553
  this.drawLinefills(ctx, W, H, xOf, yOf);
@@ -25401,13 +25895,35 @@ var DrawingSceneRenderer = class {
25401
25895
  if (this.isPointShape(lb.style)) {
25402
25896
  if (!lb.noFill) this.drawLabelShape(ctx, lb.style, px, py, fontPx, color);
25403
25897
  if (lb.text) this.drawLabelText(ctx, lb, px, py + fontPx, fontPx);
25404
- } else if (lb.style === "none" || lb.style === "text_outline" || lb.noFill) {
25405
- if (lb.text) this.drawLabelText(ctx, lb, px, py, fontPx, lb.style === "text_outline");
25898
+ if (lb.tooltip) {
25899
+ const r = Math.max(4, fontPx * 0.6) + 3;
25900
+ this.tipRegions.push({ left: px - r, top: py - r, right: px + r, bottom: py + r, text: lb.tooltip });
25901
+ }
25902
+ } else if (lb.style === "none" || lb.style === "text_outline") {
25903
+ if (lb.text) {
25904
+ this.drawLabelText(ctx, lb, px, py, fontPx, lb.style === "text_outline");
25905
+ if (lb.tooltip) this.tipRegions.push(this.textRegion(ctx, lb, px, py, fontPx, lb.tooltip));
25906
+ }
25406
25907
  } else {
25407
- this.drawBubble(ctx, lb, px, py, fontPx, color);
25908
+ const r = this.drawBubble(ctx, lb, px, py, fontPx, color);
25909
+ if (lb.tooltip) this.tipRegions.push({ left: r.x, top: r.y, right: r.x + r.w, bottom: r.y + r.h, text: lb.tooltip });
25408
25910
  }
25409
25911
  }
25410
25912
  }
25913
+ /** Canvas font for a label's text — family + Pine `text_formatting` (bold/italic). */
25914
+ labelFont(lb, fontPx) {
25915
+ const family = lb.fontFamily === "monospace" ? "monospace" : this.deps.theme.fontFamily || "sans-serif";
25916
+ return `${lb.italic ? "italic " : ""}${lb.bold ? "bold " : ""}${fontPx}px ${family}`;
25917
+ }
25918
+ /** Hover rect of a text-only label (style none/text_outline/noFill), centered like drawLabelText. */
25919
+ textRegion(ctx, lb, cx, cy, fontPx, text) {
25920
+ const font = this.labelFont(lb, fontPx);
25921
+ const lines = (lb.text ?? "").split("\n");
25922
+ const w = Math.max(1, ...lines.map((l) => this.measure(ctx, font, l)));
25923
+ const h = fontPx * 1.25 * lines.length;
25924
+ const left = lb.textAlign === "left" ? cx : lb.textAlign === "right" ? cx - w : cx - w / 2;
25925
+ return { left, top: cy - h / 2, right: left + w, bottom: cy + h / 2, text };
25926
+ }
25411
25927
  isPointShape(style) {
25412
25928
  switch (style) {
25413
25929
  case "circle":
@@ -25426,8 +25942,7 @@ var DrawingSceneRenderer = class {
25426
25942
  }
25427
25943
  }
25428
25944
  drawLabelText(ctx, lb, cx, cy, fontPx, outline = false) {
25429
- const family = lb.fontFamily === "monospace" ? "monospace" : this.deps.theme.fontFamily || "sans-serif";
25430
- ctx.font = `${fontPx}px ${family}`;
25945
+ ctx.font = this.labelFont(lb, fontPx);
25431
25946
  ctx.textAlign = lb.textAlign === "left" ? "left" : lb.textAlign === "right" ? "right" : "center";
25432
25947
  ctx.textBaseline = "middle";
25433
25948
  const lines = lb.text.split("\n");
@@ -25507,9 +26022,9 @@ var DrawingSceneRenderer = class {
25507
26022
  ctx.fill();
25508
26023
  }
25509
26024
  }
26025
+ /** Draw a bubble label; returns the bubble body rect (for tooltip hit-testing). */
25510
26026
  drawBubble(ctx, lb, px, py, fontPx, color) {
25511
- const family = lb.fontFamily === "monospace" ? "monospace" : this.deps.theme.fontFamily || "sans-serif";
25512
- ctx.font = `${fontPx}px ${family}`;
26027
+ ctx.font = this.labelFont(lb, fontPx);
25513
26028
  const lines = (lb.text ?? "").split("\n");
25514
26029
  const padX = 6;
25515
26030
  const padY = 4;
@@ -25567,38 +26082,51 @@ var DrawingSceneRenderer = class {
25567
26082
  by = py - h - ptr;
25568
26083
  pointer = "down";
25569
26084
  }
25570
- ctx.fillStyle = color;
25571
- this.roundRect(ctx, bx, by, w, h, 4);
25572
- ctx.fill();
25573
- if (pointer !== "none") {
25574
- ctx.beginPath();
25575
- if (pointer === "down") {
25576
- ctx.moveTo(px - ptr, by + h);
25577
- ctx.lineTo(px + ptr, by + h);
25578
- ctx.lineTo(px, py);
25579
- } else if (pointer === "up") {
25580
- ctx.moveTo(px - ptr, by);
25581
- ctx.lineTo(px + ptr, by);
25582
- ctx.lineTo(px, py);
25583
- } else if (pointer === "left") {
25584
- ctx.moveTo(bx, py - ptr);
25585
- ctx.lineTo(bx, py + ptr);
25586
- ctx.lineTo(px, py);
25587
- } else {
25588
- ctx.moveTo(bx + w, py - ptr);
25589
- ctx.lineTo(bx + w, py + ptr);
25590
- ctx.lineTo(px, py);
25591
- }
25592
- ctx.closePath();
26085
+ if (!lb.noFill) {
26086
+ ctx.fillStyle = color;
26087
+ this.roundRect(ctx, bx, by, w, h, 4);
25593
26088
  ctx.fill();
26089
+ if (pointer !== "none") {
26090
+ ctx.beginPath();
26091
+ if (pointer === "down") {
26092
+ ctx.moveTo(px - ptr, by + h);
26093
+ ctx.lineTo(px + ptr, by + h);
26094
+ ctx.lineTo(px, py);
26095
+ } else if (pointer === "up") {
26096
+ ctx.moveTo(px - ptr, by);
26097
+ ctx.lineTo(px + ptr, by);
26098
+ ctx.lineTo(px, py);
26099
+ } else if (pointer === "left") {
26100
+ ctx.moveTo(bx, py - ptr);
26101
+ ctx.lineTo(bx, py + ptr);
26102
+ ctx.lineTo(px, py);
26103
+ } else {
26104
+ ctx.moveTo(bx + w, py - ptr);
26105
+ ctx.lineTo(bx + w, py + ptr);
26106
+ ctx.lineTo(px, py);
26107
+ }
26108
+ ctx.closePath();
26109
+ ctx.fill();
26110
+ }
25594
26111
  }
25595
26112
  if (lb.text) {
25596
- ctx.fillStyle = lb.textColor ?? contrastColor(color);
25597
- ctx.textAlign = "center";
26113
+ ctx.fillStyle = lb.textColor ?? (lb.noFill ? this.deps.theme.textColor : contrastColor(color));
25598
26114
  ctx.textBaseline = "middle";
26115
+ let tx;
26116
+ if (lb.textAlign === "left") {
26117
+ ctx.textAlign = "left";
26118
+ tx = bx + padX;
26119
+ } else if (lb.textAlign === "right") {
26120
+ ctx.textAlign = "right";
26121
+ tx = bx + w - padX;
26122
+ } else {
26123
+ ctx.textAlign = "center";
26124
+ tx = bx + w / 2;
26125
+ }
25599
26126
  const startY = by + padY + lineH / 2;
25600
- for (let i = 0; i < lines.length; i += 1) ctx.fillText(lines[i], bx + w / 2, startY + i * lineH);
26127
+ for (let i = 0; i < lines.length; i += 1) ctx.fillText(lines[i], tx, startY + i * lineH);
25601
26128
  }
26129
+ return { x: bx, y: by, w, h };
25602
26130
  }
25603
26131
  roundRect(ctx, x, y, w, h, r) {
25604
26132
  const rr = Math.min(r, w / 2, h / 2);
@@ -25836,6 +26364,8 @@ var ChromeRenderer = class {
25836
26364
  this.axisTextColor = DARK_THEME.textColor;
25837
26365
  // Shared Pine-drawing renderer (line/box/label/polyline/linefill); widthCache persists.
25838
26366
  this.drawScene = new DrawingSceneRenderer({ timeToLogical: () => 0, barAt: () => null, theme: {} });
26367
+ // Tooltip hit-rects of every label drawn this frame, in plot coords (rebuilt per render).
26368
+ this.labelTips = [];
25839
26369
  }
25840
26370
  mount(canvas) {
25841
26371
  this.canvas = canvas;
@@ -25877,6 +26407,7 @@ var ChromeRenderer = class {
25877
26407
  const dataW = coords.width;
25878
26408
  const dataH = coords.height;
25879
26409
  this.axisTextColor = surface?.textColor ?? theme.textColor;
26410
+ this.labelTips = [];
25880
26411
  ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
25881
26412
  ctx.clearRect(0, 0, fullW, fullH);
25882
26413
  if (surface && (fullW > dataW || fullH > dataH)) {
@@ -25986,6 +26517,17 @@ var ChromeRenderer = class {
25986
26517
  (price) => coords.priceToY(price, pane.scale, pane.bounds) - pane.bounds.top
25987
26518
  );
25988
26519
  ctx.restore();
26520
+ for (const r of this.drawScene.labelTipRegions()) {
26521
+ this.labelTips.push({ ...r, top: r.top + pane.bounds.top, bottom: r.bottom + pane.bounds.top });
26522
+ }
26523
+ }
26524
+ /** Tooltip of the topmost label under a plot-space point, or null. Fed by the last render. */
26525
+ labelTooltipAt(x, y) {
26526
+ for (let i = this.labelTips.length - 1; i >= 0; i -= 1) {
26527
+ const r = this.labelTips[i];
26528
+ if (x >= r.left && x <= r.right && y >= r.top && y <= r.bottom) return r.text;
26529
+ }
26530
+ return null;
25989
26531
  }
25990
26532
  // ── axes ──
25991
26533
  drawPriceAxes(ctx, scene, coords, theme, dataW, panes) {
@@ -26202,6 +26744,68 @@ function formatCountdown(ms) {
26202
26744
  return h > 0 ? `${h}:${pad(m)}:${pad(s)}` : `${pad(m)}:${pad(s)}`;
26203
26745
  }
26204
26746
 
26747
+ // src/renderers/native/chrome/LabelTooltip.ts
26748
+ var HOVER_DELAY_MS = 350;
26749
+ var LabelTooltip = class {
26750
+ constructor(plot, deps) {
26751
+ this.plot = plot;
26752
+ this.deps = deps;
26753
+ this.tip = null;
26754
+ this.timer = null;
26755
+ /** Text of the currently open OR armed tip — dedupes moves inside one label. */
26756
+ this.current = null;
26757
+ this.onMove = (e) => {
26758
+ if (e.pointerType !== "mouse") return;
26759
+ const rect = this.plot.getBoundingClientRect();
26760
+ const x = e.clientX - rect.left;
26761
+ const y = e.clientY - rect.top;
26762
+ const text = this.deps.lookup(x, y);
26763
+ if (!text) {
26764
+ this.clear();
26765
+ return;
26766
+ }
26767
+ if (text === this.current) return;
26768
+ this.clear();
26769
+ this.current = text;
26770
+ this.timer = window.setTimeout(() => this.show(text, x, y), HOVER_DELAY_MS);
26771
+ };
26772
+ this.onLeave = () => {
26773
+ this.clear();
26774
+ };
26775
+ plot.addEventListener("pointermove", this.onMove);
26776
+ plot.addEventListener("pointerleave", this.onLeave);
26777
+ plot.addEventListener("pointerdown", this.onLeave);
26778
+ }
26779
+ destroy() {
26780
+ this.plot.removeEventListener("pointermove", this.onMove);
26781
+ this.plot.removeEventListener("pointerleave", this.onLeave);
26782
+ this.plot.removeEventListener("pointerdown", this.onLeave);
26783
+ this.clear();
26784
+ }
26785
+ show(text, x, y) {
26786
+ const doc = this.plot.ownerDocument;
26787
+ const tip = doc.createElement("div");
26788
+ tip.textContent = text;
26789
+ tip.style.cssText = "position:absolute;z-index:var(--vela-z-tooltip);pointer-events:none;background:var(--vela-bg);border:1px solid var(--vela-border);color:var(--vela-fg);border-radius:var(--vela-radius-md);padding:4px 9px;box-shadow:var(--vela-shadow);font:var(--vela-font-size-md) var(--vela-font);max-width:260px;";
26790
+ applyChromeTokens(tip, this.deps.theme());
26791
+ this.plot.appendChild(tip);
26792
+ const pw = this.plot.clientWidth;
26793
+ const ph = this.plot.clientHeight;
26794
+ tip.style.left = `${Math.max(0, Math.min(x + 12, pw - tip.offsetWidth - 4))}px`;
26795
+ tip.style.top = `${Math.max(0, Math.min(y + 16, ph - tip.offsetHeight - 4))}px`;
26796
+ this.tip = tip;
26797
+ }
26798
+ clear() {
26799
+ if (this.timer !== null) {
26800
+ clearTimeout(this.timer);
26801
+ this.timer = null;
26802
+ }
26803
+ this.tip?.remove();
26804
+ this.tip = null;
26805
+ this.current = null;
26806
+ }
26807
+ };
26808
+
26205
26809
  // src/renderers/native/chrome/CrosshairRenderer.ts
26206
26810
  var CrosshairRenderer = class {
26207
26811
  constructor() {
@@ -31279,19 +31883,8 @@ function computePaneScale(models, bars, includeCandles, i0, i1, drawings, log =
31279
31883
  for (const model of models) {
31280
31884
  const off = offsetOf(model.id);
31281
31885
  for (const s of model.series) {
31282
- if (s.kind === "candle" || s.kind === "bar") {
31283
- for (let i = i0; i <= i1; i += 1) {
31284
- const b = s.bars[i - off];
31285
- if (b) {
31286
- consider(b.high);
31287
- consider(b.low);
31288
- }
31289
- }
31290
- } else if (isLineLikeSeries(s)) {
31291
- for (let i = i0; i <= i1; i += 1) consider(s.points[i - off]?.value);
31292
- if (s.kind === "histogram" || s.kind === "columns") consider(s.style?.base ?? 0);
31293
- else if (s.style?.base != null) consider(s.style.base);
31294
- }
31886
+ if (s.overlay === true) continue;
31887
+ considerSeries(s, i0, i1, off, consider);
31295
31888
  }
31296
31889
  for (const pl of model.priceLines) consider(pl.price);
31297
31890
  }
@@ -31313,6 +31906,36 @@ function computePaneScale(models, bars, includeCandles, i0, i1, drawings, log =
31313
31906
  const span = max - min;
31314
31907
  return { min: min - span * MARGIN_BOTTOM, max: max + span * MARGIN_TOP };
31315
31908
  }
31909
+ function considerSeries(s, i0, i1, off, consider) {
31910
+ if (s.kind === "candle" || s.kind === "bar") {
31911
+ for (let i = i0; i <= i1; i += 1) {
31912
+ const b = s.bars[i - off];
31913
+ if (b) {
31914
+ consider(b.high);
31915
+ consider(b.low);
31916
+ }
31917
+ }
31918
+ } else if (isLineLikeSeries(s)) {
31919
+ for (let i = i0; i <= i1; i += 1) consider(s.points[i - off]?.value);
31920
+ if (s.kind === "histogram" || s.kind === "columns") consider(s.style?.base ?? 0);
31921
+ else if (s.style?.base != null) consider(s.style.base);
31922
+ }
31923
+ }
31924
+ function overlaySeriesRange(models, i0, i1, offsetOf = () => 0) {
31925
+ let min = Infinity;
31926
+ let max = -Infinity;
31927
+ const consider = (v) => {
31928
+ if (v != null && Number.isFinite(v)) {
31929
+ if (v < min) min = v;
31930
+ if (v > max) max = v;
31931
+ }
31932
+ };
31933
+ for (const model of models) {
31934
+ const off = offsetOf(model.id);
31935
+ for (const s of model.series) if (s.overlay === true) considerSeries(s, i0, i1, off, consider);
31936
+ }
31937
+ return min === Infinity ? null : { min, max };
31938
+ }
31316
31939
  function expandScaleByPixels(scale, heightPx, abovePx, belowPx) {
31317
31940
  if (abovePx <= 0 && belowPx <= 0) return scale;
31318
31941
  const content = heightPx - abovePx - belowPx;
@@ -31367,6 +31990,11 @@ function resizeSplit(split, dyTotal, minPx = MIN_PANE_PX) {
31367
31990
  }
31368
31991
 
31369
31992
  // src/renderers/native/backdrop/BackdropRenderer.ts
31993
+ function clipHighlightRect(x1, x2, left, right) {
31994
+ const x = Math.max(left, x1);
31995
+ const end = Math.min(right, x2);
31996
+ return end > x ? { x, width: end - x } : null;
31997
+ }
31370
31998
  var BackdropRenderer = class {
31371
31999
  constructor() {
31372
32000
  this.canvas = null;
@@ -31399,17 +32027,22 @@ var BackdropRenderer = class {
31399
32027
  /** Renderer-owned session highlight bands: full-height (all panes), behind the grid.
31400
32028
  * Session-zone washes (pre/post-market) paint first, host highlights on top. */
31401
32029
  drawHighlights(ctx, scene, coords) {
31402
- const bands = [...scene.sessionHighlightBands(), ...scene.highlights];
31403
- if (bands.length === 0) return;
32030
+ const sessions = scene.sessionHighlightBands();
32031
+ if (sessions.length > 0) {
32032
+ const left = Math.max(0, coords.logicalToX(-0.5));
32033
+ const right = Math.min(coords.width, coords.logicalToX(coords.barCount - 0.5));
32034
+ this.drawHighlightSet(ctx, sessions, coords, left, right);
32035
+ }
32036
+ this.drawHighlightSet(ctx, scene.highlights, coords, 0, coords.width);
32037
+ }
32038
+ drawHighlightSet(ctx, bands, coords, left, right) {
31404
32039
  for (const band of bands) {
31405
32040
  const x1 = coords.timeToX(band.from);
31406
32041
  const x2 = coords.timeToX(band.to);
31407
- if (x2 < 0 || x1 > coords.width || x2 <= x1) continue;
31408
- const cx = Math.max(0, x1);
31409
- const cw = Math.min(coords.width, x2) - cx;
31410
- if (cw <= 0) continue;
32042
+ const rect = clipHighlightRect(x1, x2, left, right);
32043
+ if (!rect) continue;
31411
32044
  ctx.fillStyle = band.color;
31412
- ctx.fillRect(cx, 0, cw, coords.height);
32045
+ ctx.fillRect(rect.x, 0, rect.width, coords.height);
31413
32046
  }
31414
32047
  }
31415
32048
  // ── grid ── vert/horz gate on `scene.showGrid` AND their own per-axis visibility
@@ -31951,6 +32584,8 @@ var NativeRenderer = class {
31951
32584
  this.glowAmount = 0;
31952
32585
  // WebGL2 neon-glow intensity (canvas2d ignores it)
31953
32586
  this.chrome = new ChromeRenderer();
32587
+ /** Hover tooltips for Pine labels (canvas hit-rects collected by the chrome layer). */
32588
+ this.labelTooltip = null;
31954
32589
  this.crosshairLayer = new CrosshairRenderer();
31955
32590
  /** 1 Hz repaint pump so the price-axis countdown-to-bar-close ticks; null when off. */
31956
32591
  this.countdownTimer = null;
@@ -31961,6 +32596,8 @@ var NativeRenderer = class {
31961
32596
  this.indicatorValuesOn = true;
31962
32597
  /** Host-contributed legend actions — held here so a rebuild of the legend re-wires them. */
31963
32598
  this.legendActionsProvider = null;
32599
+ /** Host-contributed legend callouts — held here so a rebuild of the legend re-wires them. */
32600
+ this.legendCalloutsProvider = null;
31964
32601
  /** Host override of the legend's fold toggle — held here so a remount re-applies it. */
31965
32602
  this.legendOverviewAction = null;
31966
32603
  // ── keyboard navigation / accessibility (item 11) ──
@@ -33069,6 +33706,10 @@ var NativeRenderer = class {
33069
33706
  this.plot.appendChild(this.scrollButton);
33070
33707
  this.plot.addEventListener("pointermove", this.onScrollProximityMove);
33071
33708
  this.plot.addEventListener("pointerleave", this.onScrollProximityLeave);
33709
+ this.labelTooltip = new LabelTooltip(this.plot, {
33710
+ theme: () => this.chromeTheme(),
33711
+ lookup: (x, y) => this.chrome.labelTooltipAt(x, y)
33712
+ });
33072
33713
  this.userDrawings = new UserDrawingController(this.wrapper, this.plot, this.drawingsCanvas, {
33073
33714
  projector: () => this.drawingProjector(),
33074
33715
  dpr: () => this.coords.dpr,
@@ -33108,9 +33749,10 @@ var NativeRenderer = class {
33108
33749
  this.inputsUI.setDialogHost(this.dialogHost);
33109
33750
  this.inputsUI.setSymbolPicker(this.symbolPicker);
33110
33751
  this.inputsUI.setLegendActions(this.legendActionsProvider);
33752
+ this.inputsUI.setLegendCallouts(this.legendCalloutsProvider);
33111
33753
  this.inputsUI.setLegendOverviewAction(this.legendOverviewAction);
33112
33754
  this.inputsUI.setOnChange((c) => {
33113
- for (const cb of this.inputChangeCbs) cb({ indicatorId: c.indicatorId, key: c.key, value: c.value });
33755
+ for (const cb of this.inputChangeCbs) cb({ indicatorId: c.indicatorId, key: c.key, value: c.value, ...c.kind ? { kind: c.kind } : {} });
33114
33756
  });
33115
33757
  this.inputsUI.setOnRemove((id) => {
33116
33758
  for (const cb of this.removeIndicatorCbs) cb(id);
@@ -33300,6 +33942,8 @@ var NativeRenderer = class {
33300
33942
  this.settingsButton = null;
33301
33943
  this.plot?.removeEventListener("pointermove", this.onScrollProximityMove);
33302
33944
  this.plot?.removeEventListener("pointerleave", this.onScrollProximityLeave);
33945
+ this.labelTooltip?.destroy();
33946
+ this.labelTooltip = null;
33303
33947
  this.scrollButton?.remove();
33304
33948
  this.scrollButton = null;
33305
33949
  for (const l of this.extLayers) l.instance.destroy?.();
@@ -33540,7 +34184,7 @@ var NativeRenderer = class {
33540
34184
  else this.scene.assignIndicatorZ(model.id);
33541
34185
  this.inputsUI.upsert(model.id, model.shorttitle ?? model.title, model.inputs, model.inputValues, model.paneId, {
33542
34186
  native: !!model.native,
33543
- ...model.shorttitle ? { settingsTitle: model.title } : {}
34187
+ ...model.props ? { props: model.props, propValues: model.propValues ?? {} } : {}
33544
34188
  });
33545
34189
  this.syncTables(model);
33546
34190
  if (model.native?.type === "volume") {
@@ -33591,8 +34235,8 @@ var NativeRenderer = class {
33591
34235
  this.paneControls?.refresh();
33592
34236
  this.scheduler.invalidate(4 /* Full */);
33593
34237
  }
33594
- setIndicatorInputs(handle, values) {
33595
- this.inputsUI.setValues(handle.id, values);
34238
+ setIndicatorInputs(handle, values, props) {
34239
+ this.inputsUI.setValues(handle.id, values, props);
33596
34240
  }
33597
34241
  setSymbolPicker(picker) {
33598
34242
  this.symbolPicker = picker;
@@ -33602,6 +34246,10 @@ var NativeRenderer = class {
33602
34246
  this.legendActionsProvider = provider;
33603
34247
  this.inputsUI?.setLegendActions(provider);
33604
34248
  }
34249
+ setLegendCallouts(provider) {
34250
+ this.legendCalloutsProvider = provider;
34251
+ this.inputsUI?.setLegendCallouts(provider);
34252
+ }
33605
34253
  setLegendOverviewAction(action) {
33606
34254
  this.legendOverviewAction = action;
33607
34255
  this.inputsUI?.setLegendOverviewAction(action);
@@ -34582,7 +35230,11 @@ var NativeRenderer = class {
34582
35230
  }
34583
35231
  const models = this.scene.indicatorsForPane(pane.id);
34584
35232
  const masterModels = models.filter((m) => m.ownScale !== true);
34585
- const dr = this.chrome.paneDrawingsRange(masterModels, this.scene, pane === pricePane, vr);
35233
+ let dr = this.chrome.paneDrawingsRange(masterModels, this.scene, pane === pricePane, vr);
35234
+ if (pane === pricePane) {
35235
+ const or = overlaySeriesRange(this.scene.indicators.values(), i0, i1, (id) => this.scene.offsetOf(id));
35236
+ if (or) dr = dr ? { min: Math.min(dr.min, or.min), max: Math.max(dr.max, or.max) } : or;
35237
+ }
34586
35238
  const includeCandles = pane.kind === "price" && !this.scene.candlesHidden;
34587
35239
  pane.scaleTarget = computePaneScale(masterModels, this.bars, includeCandles, i0, i1, dr, paneLogScale(this.scene, pane), (id) => this.scene.offsetOf(id));
34588
35240
  pane.percentBaseline = pane.kind === "price" ? this.bars[i0]?.close ?? 0 : this.firstVisibleValue(masterModels, i0);
@@ -34856,6 +35508,7 @@ var NativeRenderer = class {
34856
35508
  for (const m of models) {
34857
35509
  const off = this.scene.offsetOf(m.id);
34858
35510
  for (const s of m.series) {
35511
+ if (s.overlay === true) continue;
34859
35512
  if (isLineLikeSeries(s)) {
34860
35513
  const v = s.points[i0 - off]?.value;
34861
35514
  if (v != null && Number.isFinite(v)) return v;
@@ -35728,45 +36381,24 @@ var CSS21 = `
35728
36381
  }
35729
36382
  .vela-statusline .vela-sl-symbol { font-weight: 600; font-size: var(--vela-font-size-lg); }
35730
36383
  .vela-statusline .vela-sl-meta { color: var(--vela-fg-muted); font-size: var(--vela-font-size-md); font-weight: 600; }
35731
- /* Market status badge \u2014 icon-only 16px circle, label on hover (kit tooltip). */
35732
- .vela-statusline .vela-sl-market {
35733
- display: inline-grid;
35734
- place-items: center;
35735
- width: 16px;
35736
- height: 16px;
35737
- border-radius: 50%;
35738
- flex: none;
35739
- align-self: center;
35740
- line-height: 0;
35741
- cursor: default;
35742
- }
35743
- .vela-statusline .vela-sl-market svg {
35744
- width: 12px;
35745
- height: 12px;
35746
- display: block;
35747
- }
35748
- /* Open wears the theme's up color; the other sessions are meaning constants from the
35749
- * palette (amber pre, sky post, gray closed/holiday). Same ink at 20% for the circle. */
35750
- .vela-statusline .vela-sl-market[data-status='open'] {
35751
- background: color-mix(in srgb, var(--vela-up) 20%, transparent);
35752
- color: var(--vela-up);
35753
- }
35754
- .vela-statusline .vela-sl-market[data-status='pre'] { background: color-mix(in srgb, ${SESSION_PRE} 20%, transparent); color: ${SESSION_PRE}; }
35755
- .vela-statusline .vela-sl-market[data-status='post'] { background: color-mix(in srgb, ${SESSION_POST} 20%, transparent); color: ${SESSION_POST}; }
35756
- .vela-statusline .vela-sl-market[data-status='closed'],
35757
- .vela-statusline .vela-sl-market[data-status='holiday'] { background: color-mix(in srgb, ${SESSION_OFF} 20%, transparent); color: ${SESSION_OFF}; }
36384
+ /* Market status badge \u2014 a kit callout bubble (icon-only 16px circle, label on hover
36385
+ * via the kit tooltip); the session tint is applied per status in setMarketStatus. */
36386
+ .vela-statusline .vela-sl-market { align-self: center; }
35758
36387
  .vela-statusline .vela-sl-ohlc { display: flex; gap: var(--vela-space-1); color: var(--vela-fg-muted); }
35759
36388
  .vela-statusline .vela-sl-ohlc b { color: var(--vela-fg); font-weight: 500; }
35760
36389
  /* The change value wears the SAME ink as the OHLC values (set inline per render) \u2014
35761
36390
  * these are the pre-ink fallbacks only. */
35762
36391
  .vela-statusline .vela-sl-change[data-dir='up'] { color: var(--vela-up); }
35763
36392
  .vela-statusline .vela-sl-change[data-dir='down'] { color: var(--vela-down); }
35764
- /* Stack the renderer's PRICE-pane legend below the status line (study panes stay put).
35765
- * The renderer sets the legend's inline top \u2014 shift with a transform, don't fight it.
35766
- * Scoped to hosts that actually CARRY a status line (the marker class set by the
35767
- * Statusline constructor) \u2014 the stylesheet is document-global, so a bare container
35768
- * class here would shift every chart on the page, including statusline-less ones. */
35769
- .vela-has-statusline [data-vela-pane='price'] { transform: translateY(26px); }
36393
+ /* Stack the TOP pane's legend below the status line (lower study panes stay put).
36394
+ * The renderer marks whichever legend sits at the plot's top edge \u2014 the price pane
36395
+ * normally, or a maximized study pane filling the plot \u2014 so the legend never merges
36396
+ * with the status line whichever pane owns the top. The renderer sets the legend's
36397
+ * inline top \u2014 shift with a transform, don't fight it. Scoped to hosts that actually
36398
+ * CARRY a status line (the marker class set by the Statusline constructor) \u2014 the
36399
+ * stylesheet is document-global, so a bare attribute selector here would shift every
36400
+ * chart on the page, including statusline-less ones. */
36401
+ .vela-has-statusline [${LEGEND_AT_TOP_ATTR}] { transform: translateY(26px); }
35770
36402
  /* Mobile: two-line chip \u2014 logo / symbol / meta / market status on one aligned row, the
35771
36403
  * bar change on the next. Full O/H/L/C stays hidden (too dense on a phone-width plot).
35772
36404
  * GRID, not a wrapping flexbox: an absolutely positioned wrapping flex container sizes
@@ -35802,7 +36434,7 @@ var CSS21 = `
35802
36434
  font-size: var(--vela-font-size-sm);
35803
36435
  line-height: 1.2;
35804
36436
  }
35805
- [data-layout='mobile'] .vela-has-statusline [data-vela-pane='price'] { transform: translateY(40px); }
36437
+ [data-layout='mobile'] .vela-has-statusline [${LEGEND_AT_TOP_ATTR}] { transform: translateY(40px); }
35806
36438
  /* FIT mode (multi-chart cells \u2014 see setFitMode): the line never wraps; segments that
35807
36439
  * don't fit are HIDDEN by fit() (change first, then meta, then the market badge), so
35808
36440
  * overflow:hidden only guards the transient between a resize and the next measure.
@@ -35819,7 +36451,7 @@ var CSS21 = `
35819
36451
  padding-left: 0;
35820
36452
  }
35821
36453
  /* One row again \u2014 the mobile two-line shift doesn't apply in fit mode. */
35822
- [data-layout='mobile'] .vela-sl-fit-host.vela-has-statusline [data-vela-pane='price'] { transform: translateY(26px); }
36454
+ [data-layout='mobile'] .vela-sl-fit-host.vela-has-statusline [${LEGEND_AT_TOP_ATTR}] { transform: translateY(26px); }
35823
36455
  `;
35824
36456
  function baseOfTicker(ticker) {
35825
36457
  return ticker.replace(/[-_/]?(USDT|USDC|USD1|USDS|BUSD|USD|EUR|PERP)$/i, "") || ticker;
@@ -35831,6 +36463,13 @@ var MARKET_LABELS = {
35831
36463
  closed: "Market Closed",
35832
36464
  holiday: "Market Holiday"
35833
36465
  };
36466
+ var MARKET_INKS = {
36467
+ open: "var(--vela-up)",
36468
+ pre: SESSION_PRE,
36469
+ post: SESSION_POST,
36470
+ closed: SESSION_OFF,
36471
+ holiday: SESSION_OFF
36472
+ };
35834
36473
  function statuslineInkOf(renderer, priceStyle) {
35835
36474
  const cfg = renderer.getConfig();
35836
36475
  const c = (v) => typeof v === "string" ? v : null;
@@ -35889,8 +36528,15 @@ var Statusline = class {
35889
36528
  this.symbolEl.textContent = ticker;
35890
36529
  this.metaEl = doc.createElement("span");
35891
36530
  this.metaEl.className = "vela-sl-meta";
35892
- this.marketEl = doc.createElement("span");
35893
- this.marketEl.className = "vela-sl-market";
36531
+ this.marketBubble = new CalloutBubble({
36532
+ icon: "market-open",
36533
+ background: `color-mix(in srgb, ${MARKET_INKS.open} 20%, transparent)`,
36534
+ color: MARKET_INKS.open,
36535
+ label: MARKET_LABELS.open,
36536
+ host
36537
+ });
36538
+ this.marketEl = this.marketBubble.el;
36539
+ this.marketEl.classList.add("vela-sl-market");
35894
36540
  this.ohlcEl = doc.createElement("span");
35895
36541
  this.ohlcEl.className = "vela-sl-ohlc";
35896
36542
  this.changeEl = doc.createElement("span");
@@ -35978,8 +36624,13 @@ var Statusline = class {
35978
36624
  * hover label. Callers with no session model leave the constructor's 'open'. */
35979
36625
  setMarketStatus(status) {
35980
36626
  this.marketEl.dataset.status = status;
35981
- this.marketEl.innerHTML = icon(`market-${status}`);
35982
- this.marketEl.setAttribute("aria-label", MARKET_LABELS[status]);
36627
+ const ink = MARKET_INKS[status];
36628
+ this.marketBubble.set({
36629
+ icon: `market-${status}`,
36630
+ background: `color-mix(in srgb, ${ink} 20%, transparent)`,
36631
+ color: ink,
36632
+ label: MARKET_LABELS[status]
36633
+ });
35983
36634
  this.marketTip.setContent(MARKET_LABELS[status]);
35984
36635
  }
35985
36636
  setPartVisible(part, visible) {
@@ -36012,6 +36663,7 @@ var Statusline = class {
36012
36663
  this.fitRO?.disconnect();
36013
36664
  this.fitRO = null;
36014
36665
  this.marketTip.destroy();
36666
+ this.marketBubble.destroy();
36015
36667
  this.host.classList.remove("vela-has-statusline", "vela-sl-fit-host");
36016
36668
  this.el.remove();
36017
36669
  }
@@ -36151,61 +36803,148 @@ var MarketStatusTracker = class {
36151
36803
  };
36152
36804
 
36153
36805
  // src/widget/session-shading.ts
36154
- function deriveSessionZones(regular, extended) {
36806
+ var DAY_MS2 = 864e5;
36807
+ var MINUTE_MS = 6e4;
36808
+ function parseWindow(text) {
36809
+ if (typeof text !== "string") return null;
36810
+ const m = /^(\d{2})(\d{2})-(\d{2})(\d{2})$/.exec(text);
36811
+ if (!m) return null;
36812
+ const start = Number(m[1]) * 60 + Number(m[2]);
36813
+ const end = Number(m[3]) * 60 + Number(m[4]);
36814
+ if (start >= end || end > 1440) return null;
36815
+ return { start, end };
36816
+ }
36817
+ function parseSessionSpec(si) {
36818
+ const session = si?.["session"];
36819
+ if (typeof session !== "string" || session === "" || session === "24x7") return null;
36820
+ const regular = parseWindow(session);
36821
+ if (!regular) return null;
36822
+ const tz = si?.["timezone"];
36823
+ const timezone = typeof tz === "string" && tz !== "" ? tz : "Etc/UTC";
36824
+ const ext = parseWindow(si?.["session_extended"]);
36825
+ const extended = ext && ext.start <= regular.start && ext.end >= regular.end ? ext : { start: 0, end: 1440 };
36826
+ return { regular, extended, timezone };
36827
+ }
36828
+ var dtfCache = /* @__PURE__ */ new Map();
36829
+ function civilFormatter(tz) {
36830
+ const cached = dtfCache.get(tz);
36831
+ if (cached !== void 0) return cached;
36832
+ let dtf = null;
36833
+ try {
36834
+ dtf = new Intl.DateTimeFormat("en-US", {
36835
+ timeZone: tz,
36836
+ hourCycle: "h23",
36837
+ weekday: "short",
36838
+ year: "numeric",
36839
+ month: "2-digit",
36840
+ day: "2-digit",
36841
+ hour: "2-digit",
36842
+ minute: "2-digit"
36843
+ });
36844
+ } catch {
36845
+ dtf = null;
36846
+ }
36847
+ dtfCache.set(tz, dtf);
36848
+ return dtf;
36849
+ }
36850
+ function civilParts(dtf, ms) {
36851
+ const parts = dtf.formatToParts(ms);
36852
+ const get = (t) => parts.find((p) => p.type === t)?.value ?? "";
36853
+ return {
36854
+ weekday: get("weekday"),
36855
+ year: Number(get("year")),
36856
+ month: Number(get("month")),
36857
+ day: Number(get("day")),
36858
+ hour: Number(get("hour")) % 24,
36859
+ minute: Number(get("minute"))
36860
+ };
36861
+ }
36862
+ function expandSessionZones(spec, from, to) {
36155
36863
  const pre = [];
36156
36864
  const post = [];
36157
- for (const [extStart, extEnd] of extended) {
36158
- const inside = regular.filter(([s, e]) => e > extStart && s < extEnd).sort((a, b) => a[0] - b[0]);
36159
- let cursor = extStart;
36160
- for (const [regStart, regEnd] of inside) {
36161
- if (regStart > cursor) pre.push([cursor, Math.min(regStart, extEnd)]);
36162
- cursor = Math.max(cursor, regEnd);
36163
- }
36164
- if (cursor < extEnd) post.push([cursor, extEnd]);
36865
+ const dtf = civilFormatter(spec.timezone);
36866
+ if (!dtf || !Number.isFinite(from) || !Number.isFinite(to)) return { pre, post };
36867
+ const seen = /* @__PURE__ */ new Set();
36868
+ for (let cursor = from - DAY_MS2; cursor < to + DAY_MS2; cursor += DAY_MS2) {
36869
+ const civil = civilParts(dtf, cursor);
36870
+ const key = civil.year * 1e4 + civil.month * 100 + civil.day;
36871
+ if (!Number.isFinite(key) || seen.has(key)) continue;
36872
+ seen.add(key);
36873
+ if (civil.weekday === "Sat" || civil.weekday === "Sun") continue;
36874
+ const naive = Date.UTC(civil.year, civil.month - 1, civil.day);
36875
+ const noonGuess = naive + 720 * MINUTE_MS;
36876
+ const atNoon = civilParts(dtf, noonGuess);
36877
+ const offset = Date.UTC(atNoon.year, atNoon.month - 1, atNoon.day, atNoon.hour, atNoon.minute) - noonGuess;
36878
+ const at = (minutes) => naive + minutes * MINUTE_MS - offset;
36879
+ if (spec.extended.start < spec.regular.start) pre.push([at(spec.extended.start), at(spec.regular.start)]);
36880
+ if (spec.regular.end < spec.extended.end) post.push([at(spec.regular.end), at(spec.extended.end)]);
36165
36881
  }
36166
36882
  return { pre, post };
36167
36883
  }
36168
- var FETCH_AHEAD_MS2 = 10 * 864e5;
36169
- var MIN_LOOKBACK_MS = 3 * 864e5;
36170
- var MAX_LOOKBACK_MS = 120 * 864e5;
36884
+ var COVER_PAD_MIN_MS = 2 * DAY_MS2;
36171
36885
  var SessionShadingTracker = class {
36172
36886
  constructor(onZones) {
36173
36887
  this.onZones = onZones;
36174
- /** Invalidates detached async work — bumped by every track()/stop(). */
36888
+ /** Invalidates the one async step (metadata resolution) — bumped by track()/stop(). */
36175
36889
  this.epoch = 0;
36176
- }
36177
- /** (Re)bind to a chart's data surface + market and evaluate once. */
36890
+ this.spec = null;
36891
+ this.ready = false;
36892
+ this.session = "regular";
36893
+ /** Whether one bar is shorter than a civil day — only then do pre/post bars exist. */
36894
+ this.intraday = false;
36895
+ this.covered = null;
36896
+ /** The newest range seen — viewport moves during metadata resolution (a load's fit
36897
+ * animation) must not be lost, so the resolution always expands the LATEST range. */
36898
+ this.lastRange = null;
36899
+ }
36900
+ /** (Re)bind to a chart's data surface + market and expand once metadata lands. */
36178
36901
  track(data, symbol, opts) {
36179
36902
  const my = ++this.epoch;
36180
- void this.evaluate(my, data, symbol, opts);
36903
+ this.ready = false;
36904
+ this.spec = null;
36905
+ this.covered = null;
36906
+ this.session = opts.session;
36907
+ const tfMs = timeframeMs(opts.timeframe);
36908
+ this.intraday = Number.isFinite(tfMs) && tfMs < DAY_MS2;
36909
+ this.lastRange = opts.range;
36910
+ void data.symbolInfo(symbol).catch(() => void 0).then((si) => {
36911
+ if (my !== this.epoch) return;
36912
+ this.ready = true;
36913
+ this.spec = parseSessionSpec(si);
36914
+ this.emit(this.lastRange ?? opts.range, true);
36915
+ });
36916
+ }
36917
+ /** Follow a pan/zoom synchronously: bands are epoch-anchored, so only a range that
36918
+ * leaves the last expansion's coverage needs a recompute — no fetch, no debounce. */
36919
+ updateRange(range) {
36920
+ this.lastRange = range;
36921
+ if (!this.ready) return;
36922
+ this.emit(range, false);
36181
36923
  }
36182
36924
  stop() {
36183
36925
  this.epoch += 1;
36184
- }
36185
- async evaluate(my, data, symbol, opts) {
36186
- const resolved = data.resolve(symbol);
36187
- const provider = resolved ? data.providerInstance(resolved.provider) : void 0;
36188
- const si = await data.symbolInfo(symbol).catch(() => void 0);
36189
- if (my !== this.epoch) return;
36190
- const hasSessions = typeof si?.session === "string" && si.session !== "" && si.session !== "24x7";
36191
- if (!provider?.getCalendar || !hasSessions || !resolved) {
36192
- this.onZones(null);
36926
+ this.ready = false;
36927
+ this.spec = null;
36928
+ this.covered = null;
36929
+ this.lastRange = null;
36930
+ }
36931
+ emit(range, force) {
36932
+ if (!this.spec) {
36933
+ if (force) this.onZones(null);
36193
36934
  return;
36194
36935
  }
36195
- if (opts.session !== "extended") {
36196
- this.onZones({ pre: [], post: [] });
36936
+ if (this.session !== "extended" || !this.intraday) {
36937
+ if (force) this.onZones({ pre: [], post: [] });
36197
36938
  return;
36198
36939
  }
36199
- const now = Date.now();
36200
- const lookback = Math.max(MIN_LOOKBACK_MS, Math.min(MAX_LOOKBACK_MS, opts.lookbackMs));
36201
- const range = { from: now - lookback, to: now + FETCH_AHEAD_MS2 };
36202
- const [regular, extended] = await Promise.all([
36203
- provider.getCalendar(resolved.ticker, { ...range, session: "regular" }).catch(() => null),
36204
- provider.getCalendar(resolved.ticker, { ...range, session: "extended" }).catch(() => null)
36205
- ]);
36206
- if (my !== this.epoch) return;
36207
- if (!regular || !extended) return;
36208
- this.onZones(deriveSessionZones(regular, extended));
36940
+ const from = Math.min(range.from, range.to);
36941
+ const to = Math.max(range.from, range.to);
36942
+ if (!Number.isFinite(from) || !Number.isFinite(to)) return;
36943
+ if (!force && this.covered && from >= this.covered.from && to <= this.covered.to) return;
36944
+ const pad = Math.max(to - from, COVER_PAD_MIN_MS);
36945
+ const covered = { from: from - pad, to: to + pad };
36946
+ this.covered = covered;
36947
+ this.onZones(expandSessionZones(this.spec, covered.from, covered.to));
36209
36948
  }
36210
36949
  };
36211
36950
 
@@ -36609,6 +37348,7 @@ var ChartCell = class {
36609
37348
  this.inner.renderer.set("attribution", false);
36610
37349
  this.inner.renderer.set("dialogHost", deps.dialogHost);
36611
37350
  this.inner.renderer.setLegendActions(legendActionsProviderFor(this.inner, () => deps.context()));
37351
+ this.inner.renderer.setLegendCallouts(legendCalloutsProviderFor(this.inner, () => deps.context()));
36612
37352
  if (this.inner.renderer.supports("historyChords")) this.inner.renderer.set("historyChords", false);
36613
37353
  this.history.onChart(this.inner);
36614
37354
  this.inner.renderer.onConfigChanged(() => {
@@ -36642,7 +37382,11 @@ var ChartCell = class {
36642
37382
  this.deps.toast(`No registered provider serves "${symbol2}" (registered: ${list})`, "error", 6e3);
36643
37383
  });
36644
37384
  this.inner.on("load:start", () => this.watermark?.setLoading(true));
36645
- this.inner.on("load:end", () => this.watermark?.setLoading(false));
37385
+ this.inner.on("load:end", () => {
37386
+ this.watermark?.setLoading(false);
37387
+ this.refreshSessionShading();
37388
+ });
37389
+ this.inner.on("viewport:changed", (range) => this.sessionShading.updateRange(range));
36646
37390
  const tz = deps.timezone();
36647
37391
  if (tz !== "Etc/UTC") this.inner.renderer.set("timezone", tz);
36648
37392
  this.indicatorTitlesOn = seed.indicatorTitles ?? true;
@@ -36721,20 +37465,30 @@ var ChartCell = class {
36721
37465
  this.refreshNativeCatalog();
36722
37466
  this.pushSettingsSections();
36723
37467
  this.offMarket = this.inner.on("market:changed", ({ symbol: symbol2, timeframe }) => {
36724
- this.state.symbol = symbol2;
36725
- this.state.provider = parseSymbol(symbol2).provider ?? void 0;
36726
- this.state.timeframe = timeframe;
36727
- this.state.session = normalizeSession(this.inner?.market.session);
36728
- this.watermark?.update(symbol2, timeframe);
36729
- this.statusline?.setSymbol(symbol2);
36730
- this.statusline?.setMeta(timeframe, this.inner?.data.displayPrefix(symbol2) ?? this.state.provider ?? "");
36731
- if (this.inner) this.statusline?.onChart(this.inner);
37468
+ this.projectMarket(symbol2, timeframe);
36732
37469
  this.refreshNativeCatalog();
36733
37470
  this.refreshSessionAvailable();
36734
37471
  if (this.inner) this.marketStatus?.track(this.inner.data, symbol2);
36735
37472
  this.deps.onMarketChanged(this.id);
36736
37473
  });
36737
37474
  }
37475
+ /**
37476
+ * Project a market identity into the cell state and its display overlays (watermark,
37477
+ * statusline), WITHOUT the data-dependent bookkeeping. Runs twice per user pick: once
37478
+ * optimistically from the cell setters — the labels reflect the pick immediately, not
37479
+ * after the bars load — and again from `market:changed` (the committed pass, and the
37480
+ * only pass for host `chart.setMarket` calls). Idempotent, so the double run converges.
37481
+ */
37482
+ projectMarket(symbol, timeframe) {
37483
+ this.state.symbol = symbol;
37484
+ this.state.provider = parseSymbol(symbol).provider ?? void 0;
37485
+ this.state.timeframe = timeframe;
37486
+ this.state.session = normalizeSession(this.inner?.market.session);
37487
+ this.watermark?.update(symbol, timeframe);
37488
+ this.statusline?.setSymbol(symbol);
37489
+ this.statusline?.setMeta(timeframe, this.inner?.data.displayPrefix(symbol) ?? this.state.provider ?? "");
37490
+ if (this.inner) this.statusline?.onChart(this.inner);
37491
+ }
36738
37492
  /**
36739
37493
  * Does this cell's market HAVE sessions (RTH/ETH meaningful)? Derived from the
36740
37494
  * symbol's own metadata (`syminfo.session !== '24x7'`), asynchronously — the
@@ -36769,14 +37523,18 @@ var ChartCell = class {
36769
37523
  this.refreshSessionShading();
36770
37524
  });
36771
37525
  }
36772
- /** (Re)derive the pre/post-market shading bands for this cell's market. The loaded
36773
- * depth (bars × timeframe) bounds the calendar fetch; the tracker clamps it. */
37526
+ /** (Re)derive the pre/post-market shading bands for this cell's market. The bands
37527
+ * expand locally from the symbol's session vocabulary, so they paint as soon as
37528
+ * metadata is known and follow any pan depth without provider round trips. */
36774
37529
  refreshSessionShading() {
36775
37530
  const chart = this.inner;
36776
37531
  const symbol = this.state.symbol;
36777
37532
  if (!chart || !symbol) return;
36778
- const lookbackMs = Math.max(this.state.bars ?? 1e3, this.rangeBars) * timeframeMs(this.state.timeframe ?? "60");
36779
- this.sessionShading.track(chart.data, symbol, { session: this.session, lookbackMs });
37533
+ const now = Date.now();
37534
+ const requestedSpan = Math.max(this.state.bars ?? 1e3, this.rangeBars) * timeframeMs(this.state.timeframe ?? "60");
37535
+ const fallbackSpan = Number.isFinite(requestedSpan) ? Math.max(3 * 864e5, requestedSpan) : 3 * 864e5;
37536
+ const range = chart.getVisibleRange() ?? { from: now - fallbackSpan, to: now };
37537
+ this.sessionShading.track(chart.data, symbol, { session: this.session, timeframe: this.timeframe, range });
36780
37538
  }
36781
37539
  /** The session-shade colors live in the renderer CONFIG (persisted with it, edited
36782
37540
  * live by the dialog swatch) — the cell only proxies them into its settings rows. */
@@ -36872,10 +37630,10 @@ var ChartCell = class {
36872
37630
  id: "status-line",
36873
37631
  rows: [
36874
37632
  { kind: "heading", label: "Status line", id: "parts" },
36875
- { kind: "toggle", label: "Symbol name", id: "name", get: () => sl.partVisible("name"), set: (v) => sl.setPartVisible("name", v) },
36876
- { kind: "toggle", label: "Market status", id: "market", get: () => sl.partVisible("market"), set: (v) => sl.setPartVisible("market", v) },
36877
- { kind: "toggle", label: "OHLC values", id: "ohlc", get: () => sl.partVisible("ohlc"), set: (v) => sl.setPartVisible("ohlc", v) },
36878
- { kind: "toggle", label: "Bar change values", id: "change", get: () => sl.partVisible("change"), set: (v) => sl.setPartVisible("change", v) },
37633
+ { kind: "toggle", label: "Symbol name", id: "name", get: () => sl.partVisible("name"), set: (v) => this.setStatuslinePart("name", v) },
37634
+ { kind: "toggle", label: "Market status", id: "market", get: () => sl.partVisible("market"), set: (v) => this.setStatuslinePart("market", v) },
37635
+ { kind: "toggle", label: "OHLC values", id: "ohlc", get: () => sl.partVisible("ohlc"), set: (v) => this.setStatuslinePart("ohlc", v) },
37636
+ { kind: "toggle", label: "Bar change values", id: "change", get: () => sl.partVisible("change"), set: (v) => this.setStatuslinePart("change", v) },
36879
37637
  { kind: "heading", label: "Indicators", id: "indicators" },
36880
37638
  {
36881
37639
  kind: "toggle",
@@ -36910,12 +37668,40 @@ var ChartCell = class {
36910
37668
  this.indicatorTitlesOn = visible;
36911
37669
  this.inner?.renderer.set("indicatorTitles", visible);
36912
37670
  this.deps.onStateDirty();
37671
+ this.deps.onStatusPrefsChanged(this.id);
36913
37672
  }
36914
37673
  /** Show/hide the plot values beside this cell's legend titles (persisted per cell). */
36915
37674
  setIndicatorValuesVisible(visible) {
36916
37675
  this.indicatorValuesOn = visible;
36917
37676
  this.inner?.renderer.set("indicatorValues", visible);
36918
37677
  this.deps.onStateDirty();
37678
+ this.deps.onStatusPrefsChanged(this.id);
37679
+ }
37680
+ /** Show/hide one status-line segment (the settings dialog's Status line tab). */
37681
+ setStatuslinePart(part, visible) {
37682
+ this.statusline?.setPartVisible(part, visible);
37683
+ this.deps.onStatusPrefsChanged(this.id);
37684
+ }
37685
+ /** This cell's Status line tab prefs as one bundle (see {@link CellStatusPrefs}). */
37686
+ statusPrefs() {
37687
+ const sl = this.statusline;
37688
+ return {
37689
+ parts: sl ? { name: sl.partVisible("name"), market: sl.partVisible("market"), ohlc: sl.partVisible("ohlc"), change: sl.partVisible("change") } : null,
37690
+ indicatorTitles: this.indicatorTitlesOn,
37691
+ indicatorValues: this.indicatorValuesOn
37692
+ };
37693
+ }
37694
+ /** Converge this cell's Status line tab prefs to `prefs` — the follower half of
37695
+ * the workspace's style link. Idempotent: matching values change nothing, so a
37696
+ * propagated echo dies on its own. */
37697
+ applyStatusPrefs(prefs) {
37698
+ if (prefs.parts && this.statusline) {
37699
+ for (const part of Object.keys(prefs.parts)) {
37700
+ if (this.statusline.partVisible(part) !== prefs.parts[part]) this.statusline.setPartVisible(part, prefs.parts[part]);
37701
+ }
37702
+ }
37703
+ if (prefs.indicatorTitles !== this.indicatorTitlesOn) this.setIndicatorTitlesVisible(prefs.indicatorTitles);
37704
+ if (prefs.indicatorValues !== this.indicatorValuesOn) this.setIndicatorValuesVisible(prefs.indicatorValues);
36919
37705
  }
36920
37706
  /** The LIVE chart of this cell — never cache it across a layout change (the cell's
36921
37707
  * identity is what endures; the chart dies with the cell). */
@@ -36937,17 +37723,23 @@ var ChartCell = class {
36937
37723
  get indicatorCount() {
36938
37724
  return this.instances.length + this.nativeCatalog.filter((n) => n.present).length;
36939
37725
  }
36940
- /** Switch this cell's market in place (the chart instance survives). */
37726
+ /** Switch this cell's market in place (the chart instance survives). The projection
37727
+ * is OPTIMISTIC — labels and chrome show the pick before the bars load; it follows
37728
+ * the setMarket call so the statusline reads the already-blanked chart. */
36941
37729
  setSymbol(symbol) {
36942
37730
  if (!this.inner || symbol === this.symbol) return;
36943
37731
  this.unresolvedToasted = null;
36944
37732
  void this.inner.setMarket({ symbol });
37733
+ this.projectMarket(symbol, this.timeframe);
37734
+ this.deps.onMarketChanged(this.id);
36945
37735
  }
36946
37736
  setTimeframe(timeframe) {
36947
37737
  if (!this.inner || timeframe === this.timeframe) return;
36948
37738
  this.activeRangeId = null;
36949
37739
  this.rangeBars = 0;
36950
37740
  void this.inner.setMarket({ timeframe, bars: this.state.bars });
37741
+ this.projectMarket(this.symbol, timeframe);
37742
+ this.deps.onMarketChanged(this.id);
36951
37743
  }
36952
37744
  /** Applied live (renderer feature) — no reload. */
36953
37745
  setPriceStyle(style) {
@@ -37322,16 +38114,21 @@ var ChartCell = class {
37322
38114
 
37323
38115
  // src/workspace/context.ts
37324
38116
  function buildContext(host) {
37325
- const active = host.active();
37326
38117
  return {
37327
38118
  get chart() {
37328
38119
  const cell = host.active();
37329
38120
  if (!cell) throw new Error("VelaWorkspace has no active cell yet");
37330
38121
  return cell.chart;
37331
38122
  },
37332
- symbol: active?.symbol ?? "",
37333
- timeframe: active?.timeframe ?? "60",
37334
- priceStyle: active?.priceStyle ?? "candles",
38123
+ get symbol() {
38124
+ return host.active()?.symbol ?? "";
38125
+ },
38126
+ get timeframe() {
38127
+ return host.active()?.timeframe ?? "60";
38128
+ },
38129
+ get priceStyle() {
38130
+ return host.active()?.priceStyle ?? "candles";
38131
+ },
37335
38132
  setSymbol: (symbol) => host.active()?.setSymbol(symbol),
37336
38133
  setTimeframe: (tf) => host.active()?.setTimeframe(tf),
37337
38134
  setPriceStyle: (style) => host.active()?.setPriceStyle(style),
@@ -37342,8 +38139,12 @@ function buildContext(host) {
37342
38139
  addIndicator: (entry) => host.active()?.addExternalIndicator(entry),
37343
38140
  addNativeIndicator: (type) => host.active()?.addNative(type),
37344
38141
  stateChanged: () => host.stateDirty(),
37345
- cells: host.cells().map((c) => ({ id: c.id, chart: c.chart, symbol: c.symbol, timeframe: c.timeframe })),
37346
- activeCellId: active?.id ?? "",
38142
+ get cells() {
38143
+ return host.cells().map((c) => ({ id: c.id, chart: c.chart, symbol: c.symbol, timeframe: c.timeframe }));
38144
+ },
38145
+ get activeCellId() {
38146
+ return host.active()?.id ?? "";
38147
+ },
37347
38148
  setActiveCell: (id) => host.setActiveCell(id)
37348
38149
  };
37349
38150
  }
@@ -37676,6 +38477,10 @@ var VelaWorkspace = class {
37676
38477
  /** Same guard for the drawings link: the propagated mutations' own `drawing:*`
37677
38478
  * events fire synchronously inside the propagation loop and must not fan out again. */
37678
38479
  this.drawingSyncBusy = false;
38480
+ /** Same guard for the style link: a follower's `applyConfig` re-fires its
38481
+ * `onConfigChanged` in the same tick, and a state restore applies per-cell
38482
+ * configs that legitimately differ — neither must propagate. */
38483
+ this.styleSyncBusy = false;
37679
38484
  /** LINKED drawings (the drawings sync): one map per synced set (cellId → that
37680
38485
  * cell's drawing id), reachable from every member under its `cellId\0drawingId`
37681
38486
  * key — any member finds its peers to push edits/removals onto. Survives a
@@ -37736,9 +38541,7 @@ var VelaWorkspace = class {
37736
38541
  if (boot?.favorites) this.favs = [...boot.favorites];
37737
38542
  if (boot?.timeframeFavorites) this.tfFavs = [...boot.timeframeFavorites];
37738
38543
  const sync = boot?.sync ?? opts.sync;
37739
- for (const kind of ["viewport", "symbol", "timeframe", "crosshair", "drawings"]) {
37740
- this.applySyncSetting(kind, sync?.[kind]);
37741
- }
38544
+ for (const kind of SYNC_KINDS) this.applySyncSetting(kind, sync?.[kind]);
37742
38545
  this.monoLayout = opts.layout === false;
37743
38546
  const optLayout = opts.layout === false || opts.layout === void 0 ? "4" : opts.layout;
37744
38547
  this.def = this.resolveLayout(this.monoLayout ? "1" : boot?.layout && ensureLayout(boot.layout) ? boot.layout : optLayout);
@@ -37822,7 +38625,8 @@ var VelaWorkspace = class {
37822
38625
  syncs: () => [
37823
38626
  { id: "symbol", label: "Symbol", checked: this.syncOpts.symbol === true },
37824
38627
  { id: "timeframe", label: "Interval", checked: this.syncOpts.timeframe === true },
37825
- { id: "crosshair", label: "Crosshair", checked: this.syncOpts.crosshair === true }
38628
+ { id: "crosshair", label: "Crosshair", checked: this.syncOpts.crosshair === true },
38629
+ { id: "style", label: "Style", checked: this.syncOpts.style === true }
37826
38630
  ],
37827
38631
  onToggleSync: (id) => {
37828
38632
  const kind = id;
@@ -38041,7 +38845,10 @@ var VelaWorkspace = class {
38041
38845
  this.topbar.renderActions();
38042
38846
  this.mobileBar?.renderActions();
38043
38847
  this.dock.refresh();
38044
- for (const cell of this.cells()) cell.chart.renderer.setLegendActions(legendActionsProviderFor(cell.chart, () => this.context()));
38848
+ for (const cell of this.cells()) {
38849
+ cell.chart.renderer.setLegendActions(legendActionsProviderFor(cell.chart, () => this.context()));
38850
+ cell.chart.renderer.setLegendCallouts(legendCalloutsProviderFor(cell.chart, () => this.context()));
38851
+ }
38045
38852
  }
38046
38853
  // ── state surface (the SDK's read/restore of the whole grid's config + content) ──
38047
38854
  /**
@@ -38106,7 +38913,7 @@ var VelaWorkspace = class {
38106
38913
  this.topbar.setTimeframeFavorites(this.tfFavs);
38107
38914
  }
38108
38915
  this.dock.applyState(st.panels);
38109
- for (const kind of ["viewport", "symbol", "timeframe", "crosshair", "drawings"]) this.applySyncSetting(kind, st.sync?.[kind]);
38916
+ for (const kind of SYNC_KINDS) this.applySyncSetting(kind, st.sync?.[kind]);
38110
38917
  this.trackSizes.clear();
38111
38918
  if (st.trackSizes) for (const [id, ts] of Object.entries(st.trackSizes)) this.trackSizes.set(id, ts);
38112
38919
  const targetDef = this.monoLayout ? this.def : ensureLayout(st.layout) ?? this.def;
@@ -38117,9 +38924,14 @@ var VelaWorkspace = class {
38117
38924
  for (const cell of this.cellsById.values()) cell.chart.drawings.setFavorites(this.favs);
38118
38925
  }
38119
38926
  this.drawingLinks.clear();
38120
- for (const [i] of this.def.cells.entries()) {
38121
- const { id, ...cs } = st.charts[i];
38122
- this.cellsById.get(id)?.rehydrate(cs);
38927
+ this.styleSyncBusy = true;
38928
+ try {
38929
+ for (const [i] of this.def.cells.entries()) {
38930
+ const { id, ...cs } = st.charts[i];
38931
+ this.cellsById.get(id)?.rehydrate(cs);
38932
+ }
38933
+ } finally {
38934
+ this.styleSyncBusy = false;
38123
38935
  }
38124
38936
  if (st.timezone) this.setTimezone(st.timezone);
38125
38937
  this.pool.clear();
@@ -38219,6 +39031,7 @@ var VelaWorkspace = class {
38219
39031
  const rebuildAll = nextBackend !== this.cellBackend;
38220
39032
  this.order = orderAfterLayout(this.order, next.cells.length, this.activeId);
38221
39033
  const keep = new Set(this.order.slice(0, next.cells.length));
39034
+ const preexisting = new Set(this.cellsById.keys());
38222
39035
  for (const [id, cell] of [...this.cellsById]) {
38223
39036
  if (!keep.has(id) || rebuildAll) {
38224
39037
  this.poolSet(id, cell.dehydrate());
@@ -38231,6 +39044,7 @@ var VelaWorkspace = class {
38231
39044
  this.cellBackend = nextBackend;
38232
39045
  this.applyGrid();
38233
39046
  this.buildCells();
39047
+ this.alignNewCellStyles(preexisting);
38234
39048
  this.syncCellPresentation();
38235
39049
  this.topbar.setLayout(next.id);
38236
39050
  const nextActive = activeAfterLayout(this.activeId, this.order.slice(0, next.cells.length));
@@ -38434,6 +39248,7 @@ var VelaWorkspace = class {
38434
39248
  onMarketChanged: (id2) => this.onCellMarketChanged(id2),
38435
39249
  onPriceStyleChanged: (id2) => this.onCellPriceStyleChanged(id2),
38436
39250
  onIndicatorsChanged: (id2) => this.onCellIndicatorsChanged(id2),
39251
+ onStatusPrefsChanged: (id2) => this.propagateStylePrefs(id2),
38437
39252
  onStateDirty: () => this.markStateDirty(),
38438
39253
  manifestSettled: () => this.manifestSettled,
38439
39254
  toast: (message, kind, durationMs) => this.toastHost.show(message, kind, durationMs)
@@ -38509,6 +39324,7 @@ var VelaWorkspace = class {
38509
39324
  this.drawToolbar?.setEraserActive(mode === "eraser");
38510
39325
  });
38511
39326
  chart.on("viewport:changed", (range) => this.propagateViewport(cell.id, range));
39327
+ chart.renderer.onConfigChanged(() => this.propagateStylePrefs(cell.id));
38512
39328
  chart.on("theme:changed", (t) => this.setTheme(t));
38513
39329
  chart.renderer.onAxisLongPress((e) => {
38514
39330
  if (this.layoutCtl.current !== "mobile") return;
@@ -38541,11 +39357,67 @@ var VelaWorkspace = class {
38541
39357
  if (kind === "viewport") {
38542
39358
  const range = this.cellsById.get(this.activeId)?.chart.getVisibleRange();
38543
39359
  if (range) this.propagateViewport(this.activeId, range);
39360
+ } else if (kind === "style") {
39361
+ this.propagateStylePrefs(this.activeId);
38544
39362
  } else {
38545
39363
  this.propagateMarket(this.activeId);
38546
39364
  }
38547
39365
  }
38548
39366
  }
39367
+ /**
39368
+ * Align cells minted by a layout change to their style group: with the link on, a
39369
+ * NEW cell (fresh slot or one returning from the pool, which missed edits while
39370
+ * dormant) inherits the presentation of a pre-existing group peer — the active
39371
+ * cell when it is one — instead of sitting on its own state beside a styled
39372
+ * group. Propagation runs FROM the peer, so a newborn's defaults never overwrite
39373
+ * the group, and the equality short-circuits keep converged peers untouched.
39374
+ */
39375
+ alignNewCellStyles(preexisting) {
39376
+ const setting = this.syncOpts.style;
39377
+ if (!setting) return;
39378
+ const ids = [...this.cellsById.keys()];
39379
+ const propagated = /* @__PURE__ */ new Set();
39380
+ for (const id of ids) {
39381
+ if (preexisting.has(id)) continue;
39382
+ const peers = syncTargets(id, setting, ids).filter((p) => preexisting.has(p));
39383
+ if (peers.length === 0) continue;
39384
+ const source = this.activeId && peers.includes(this.activeId) ? this.activeId : peers[0];
39385
+ if (propagated.has(source)) continue;
39386
+ propagated.add(source);
39387
+ this.propagateStylePrefs(source);
39388
+ }
39389
+ }
39390
+ /**
39391
+ * Mirror an origin cell's presentation — the Canvas + Scales-and-lines slice of
39392
+ * its renderer config plus its Status line tab prefs — onto its same-group
39393
+ * followers (the style link). Loop-safe two ways: the busy guard eats the
39394
+ * followers' SYNCHRONOUS echoes (their `applyConfig` re-fires `onConfigChanged`
39395
+ * in the same tick), and the equality short-circuits leave already-converged
39396
+ * followers untouched, so nothing re-emits once the group agrees.
39397
+ */
39398
+ propagateStylePrefs(originId) {
39399
+ if (this.styleSyncBusy || this.destroyed) return;
39400
+ const targets = syncTargets(originId, this.syncOpts.style, [...this.cellsById.keys()]);
39401
+ if (targets.length === 0) return;
39402
+ const origin = this.cellsById.get(originId);
39403
+ if (!origin) return;
39404
+ const slice = styleConfigSlice(origin.chart.renderer.getConfig());
39405
+ const sliceJson = slice ? JSON.stringify(slice) : null;
39406
+ const prefs = origin.statusPrefs();
39407
+ this.styleSyncBusy = true;
39408
+ try {
39409
+ for (const id of targets) {
39410
+ const cell = this.cellsById.get(id);
39411
+ if (!cell) continue;
39412
+ if (slice && sliceJson !== JSON.stringify(styleConfigSlice(cell.chart.renderer.getConfig()))) {
39413
+ cell.chart.renderer.applyConfig(slice);
39414
+ }
39415
+ cell.applyStatusPrefs(prefs);
39416
+ }
39417
+ } finally {
39418
+ this.styleSyncBusy = false;
39419
+ }
39420
+ }
38549
39421
  /**
38550
39422
  * Mirror an origin cell's pointer time onto its same-group followers as GHOST
38551
39423
  * crosshairs (`renderer.setExternalCrosshair`). The horizontal price level rides
@@ -38853,7 +39725,8 @@ var VelaWorkspace = class {
38853
39725
  syncs: () => [
38854
39726
  { id: "symbol", label: "Symbol", checked: this.syncOpts.symbol === true },
38855
39727
  { id: "timeframe", label: "Interval", checked: this.syncOpts.timeframe === true },
38856
- { id: "crosshair", label: "Crosshair", checked: this.syncOpts.crosshair === true }
39728
+ { id: "crosshair", label: "Crosshair", checked: this.syncOpts.crosshair === true },
39729
+ { id: "style", label: "Style", checked: this.syncOpts.style === true }
38857
39730
  ],
38858
39731
  onToggleSync: (id) => {
38859
39732
  const kind = id;