@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
@@ -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 = `
@@ -4642,6 +4828,44 @@ function legendActionsProviderFor(chart, context) {
4642
4828
  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) }));
4643
4829
  };
4644
4830
  }
4831
+ var calloutRegistry = /* @__PURE__ */ new Map();
4832
+ function legendCallouts() {
4833
+ return [...calloutRegistry.values()].sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
4834
+ }
4835
+ function legendCalloutsProviderFor(chart, context) {
4836
+ return (indicatorId) => {
4837
+ const handle = chart.indicators().find((h) => h.id === indicatorId);
4838
+ if (!handle) return [];
4839
+ const info = { id: handle.id, title: handle.title, ...handle.source !== void 0 ? { source: handle.source } : {} };
4840
+ const views = [];
4841
+ for (const d of legendCallouts()) {
4842
+ const spec = d.callout(info);
4843
+ if (!spec) continue;
4844
+ views.push({
4845
+ id: d.id,
4846
+ icon: spec.icon,
4847
+ background: spec.background,
4848
+ ...spec.color !== void 0 ? { color: spec.color } : {},
4849
+ tooltip: spec.tooltip,
4850
+ ...spec.content !== void 0 ? {
4851
+ content: {
4852
+ ...spec.content.title !== void 0 ? { title: spec.content.title } : {},
4853
+ items: spec.content.items.map(
4854
+ (item) => item.type === "text" ? item : {
4855
+ type: "button",
4856
+ label: item.label,
4857
+ ...item.primary !== void 0 ? { primary: item.primary } : {},
4858
+ ...item.close !== void 0 ? { close: item.close } : {},
4859
+ run: () => item.run(context(), info)
4860
+ }
4861
+ )
4862
+ }
4863
+ } : {}
4864
+ });
4865
+ }
4866
+ return views;
4867
+ };
4868
+ }
4645
4869
  var stateHandlers = /* @__PURE__ */ new Map();
4646
4870
  function statePersistenceHandlers(scope) {
4647
4871
  return [...stateHandlers.values()].filter((h) => h.scope === scope);
@@ -5200,6 +5424,11 @@ var Topbar = class {
5200
5424
  /** Overrides that LEFT their native slot (default side + a declared `order`) — they
5201
5425
  * render through the flow cluster like ordinary actions. */
5202
5426
  this.flowingOverrides = /* @__PURE__ */ new Set();
5427
+ /** Tooltips of the CURRENT icon-only action buttons — rebuilt with every
5428
+ * renderActions pass (contributed buttons are replaceChildren'd away). */
5429
+ this.actionTooltips = [];
5430
+ /** `iconOnly` misuse warned once per action id (renderActions re-runs freely). */
5431
+ this.warnedIconless = /* @__PURE__ */ new Set();
5203
5432
  this.onHairlineSync = () => {
5204
5433
  if (this.hairlineRaf) return;
5205
5434
  const win = this.el.ownerDocument.defaultView;
@@ -5462,6 +5691,8 @@ var Topbar = class {
5462
5691
  this.actionsHost.replaceChildren();
5463
5692
  this.leftActionsHost.replaceChildren();
5464
5693
  for (const pin of this.pinned.values()) pin.host.replaceChildren();
5694
+ for (const t of this.actionTooltips) t.destroy();
5695
+ this.actionTooltips = [];
5465
5696
  const doc = this.actionsHost.ownerDocument;
5466
5697
  const flowLeft = this.comp.left.includes("actions");
5467
5698
  const flowRight = this.comp.right.includes("actions");
@@ -5471,10 +5702,20 @@ var Topbar = class {
5471
5702
  if (!pin && builtin.has(action.id) && !this.flowingOverrides.has(action.id)) continue;
5472
5703
  const left = pin ? pin.left : action.align === "left";
5473
5704
  if (!pin && !(left ? flowLeft : flowRight)) continue;
5705
+ const iconOnly = action.iconOnly === true && !!action.icon;
5706
+ if (action.iconOnly === true && !action.icon && !this.warnedIconless.has(action.id)) {
5707
+ this.warnedIconless.add(action.id);
5708
+ console.warn(`[vela] widget action "${action.id}": iconOnly needs an \`icon\` \u2014 rendering the label instead.`);
5709
+ }
5474
5710
  const b = doc.createElement("button");
5475
- b.className = left ? "vela-widget-action-left" : "vela-widget-action";
5711
+ b.className = left ? "vela-widget-action-left" : iconOnly ? "vela-widget-tool" : "vela-widget-action";
5476
5712
  if (action.icon) b.appendChild(iconEl(action.icon, doc));
5477
- b.appendChild(doc.createTextNode(action.label));
5713
+ if (iconOnly) {
5714
+ b.setAttribute("aria-label", action.label);
5715
+ this.actionTooltips.push(new Tooltip(b, { content: action.label, triggerId: `vela-action-${action.id}`, host: this.host }));
5716
+ } else {
5717
+ b.appendChild(doc.createTextNode(action.label));
5718
+ }
5478
5719
  b.addEventListener("click", () => {
5479
5720
  const c = this.opts.getContext?.();
5480
5721
  if (c) action.run(c);
@@ -5525,7 +5766,7 @@ var Topbar = class {
5525
5766
  this.tfMenu.destroy();
5526
5767
  this.styleMenu.destroy();
5527
5768
  this.layoutPicker?.destroy();
5528
- for (const t of [...this.tooltips, ...this.panelTooltips]) t.destroy();
5769
+ for (const t of [...this.tooltips, ...this.panelTooltips, ...this.actionTooltips]) t.destroy();
5529
5770
  this.el.remove();
5530
5771
  }
5531
5772
  /** One icon-only tool button with its kit tooltip, parked in `sink` for disposal. */
@@ -17233,19 +17474,8 @@ function registerBuiltinChartTypes() {
17233
17474
  registerChartType({ id: "heikinashi", label: "Heikin Ashi", barTransform: HEIKIN_ASHI });
17234
17475
  }
17235
17476
 
17236
- // src/workspace/sync.ts
17237
- function syncTargets(originId, setting, cellIds) {
17238
- if (setting == null || setting === false) return [];
17239
- if (setting === true) return cellIds.filter((id) => id !== originId);
17240
- const group = setting[originId];
17241
- if (group == null) return [];
17242
- return cellIds.filter((id) => id !== originId && setting[id] === group);
17243
- }
17244
- function rangesWithin(a, b, epsMs) {
17245
- return Math.abs(a.from - b.from) <= epsMs && Math.abs(a.to - b.to) <= epsMs;
17246
- }
17247
-
17248
17477
  // src/state/document.ts
17478
+ var SYNC_KINDS = ["viewport", "symbol", "timeframe", "crosshair", "drawings", "style"];
17249
17479
  function prefixedSymbol(cell) {
17250
17480
  if (!cell?.symbol) return void 0;
17251
17481
  if (cell.symbol.includes(":") || !cell.provider) return cell.symbol;
@@ -17333,7 +17563,7 @@ function sanitizeSync(raw) {
17333
17563
  if (raw == null || typeof raw !== "object") return null;
17334
17564
  const s = raw;
17335
17565
  const out = {};
17336
- for (const kind of ["viewport", "symbol", "timeframe", "crosshair", "drawings"]) {
17566
+ for (const kind of SYNC_KINDS) {
17337
17567
  const v = s[kind];
17338
17568
  if (v === true) out[kind] = true;
17339
17569
  else if (v != null && typeof v === "object") {
@@ -17376,6 +17606,28 @@ function sanitizeTrackSizes(raw) {
17376
17606
  return Object.keys(out).length > 0 ? out : null;
17377
17607
  }
17378
17608
 
17609
+ // src/workspace/sync.ts
17610
+ function syncTargets(originId, setting, cellIds) {
17611
+ if (setting == null || setting === false) return [];
17612
+ if (setting === true) return cellIds.filter((id) => id !== originId);
17613
+ const group = setting[originId];
17614
+ if (group == null) return [];
17615
+ return cellIds.filter((id) => id !== originId && setting[id] === group);
17616
+ }
17617
+ function rangesWithin(a, b, epsMs) {
17618
+ return Math.abs(a.from - b.from) <= epsMs && Math.abs(a.to - b.to) <= epsMs;
17619
+ }
17620
+ var STYLE_SYNC_CONFIG_KEYS = ["layout", "panes", "grid", "priceScale", "crosshair"];
17621
+ function styleConfigSlice(config) {
17622
+ if (config == null || typeof config !== "object") return null;
17623
+ const doc = config;
17624
+ const out = {};
17625
+ for (const key of STYLE_SYNC_CONFIG_KEYS) {
17626
+ if (doc[key] != null && typeof doc[key] === "object") out[key] = doc[key];
17627
+ }
17628
+ return Object.keys(out).length > 0 ? out : null;
17629
+ }
17630
+
17379
17631
  // src/workspace/persist.ts
17380
17632
  var memoryStore = /* @__PURE__ */ new Map();
17381
17633
  function memoryStorageAdapter() {
@@ -17482,6 +17734,7 @@ var IndicatorHandleImpl = class {
17482
17734
  constructor(id, title, controller, source) {
17483
17735
  this.controller = controller;
17484
17736
  this.schema = [];
17737
+ this.propsSchema = [];
17485
17738
  this.visibleState = true;
17486
17739
  this.bus = new TypedEventBus();
17487
17740
  this.id = id;
@@ -17491,6 +17744,9 @@ var IndicatorHandleImpl = class {
17491
17744
  get inputs() {
17492
17745
  return this.schema;
17493
17746
  }
17747
+ get props() {
17748
+ return this.propsSchema;
17749
+ }
17494
17750
  get visible() {
17495
17751
  return this.visibleState;
17496
17752
  }
@@ -17500,6 +17756,12 @@ var IndicatorHandleImpl = class {
17500
17756
  setInputs(values) {
17501
17757
  this.controller.applyInputs(this.id, values);
17502
17758
  }
17759
+ setProp(key, value) {
17760
+ this.controller.applyProps(this.id, { [key]: value });
17761
+ }
17762
+ setProps(values) {
17763
+ this.controller.applyProps(this.id, values);
17764
+ }
17503
17765
  setVisible(visible) {
17504
17766
  this.controller.setVisible(this.id, visible);
17505
17767
  }
@@ -17519,6 +17781,9 @@ var IndicatorHandleImpl = class {
17519
17781
  setSchema(schema) {
17520
17782
  this.schema = schema;
17521
17783
  }
17784
+ setPropsSchema(schema) {
17785
+ this.propsSchema = schema;
17786
+ }
17522
17787
  /** Sync the public `visible` getter when the orchestrator changes visibility (API or legend eye). */
17523
17788
  setVisibleState(visible) {
17524
17789
  this.visibleState = visible;
@@ -17532,6 +17797,8 @@ var IndicatorHandleImpl = class {
17532
17797
  function summarizeModel(model) {
17533
17798
  const series = {};
17534
17799
  for (const s of model.series) series[s.kind] = (series[s.kind] ?? 0) + 1;
17800
+ const countOverlay = (items) => items?.filter((i) => i.overlay === true).length ?? 0;
17801
+ 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);
17535
17802
  return {
17536
17803
  id: model.id,
17537
17804
  title: model.title,
@@ -17551,7 +17818,8 @@ function summarizeModel(model) {
17551
17818
  tables: model.tables?.length ?? 0,
17552
17819
  barColors: model.barColors?.length ?? 0,
17553
17820
  trades: model.trades?.length ?? 0,
17554
- inputs: model.inputs.length
17821
+ inputs: model.inputs.length,
17822
+ forcedOverlay
17555
17823
  };
17556
17824
  }
17557
17825
  function inspectModels(models) {
@@ -17572,7 +17840,8 @@ function inspectModels(models) {
17572
17840
  linefills: sum((s) => s.linefills),
17573
17841
  tables: sum((s) => s.tables),
17574
17842
  barColors: sum((s) => s.barColors),
17575
- trades: sum((s) => s.trades)
17843
+ trades: sum((s) => s.trades),
17844
+ forcedOverlay: sum((s) => s.forcedOverlay)
17576
17845
  }
17577
17846
  };
17578
17847
  }
@@ -17716,7 +17985,9 @@ var EngineOrchestrator = class _EngineOrchestrator {
17716
17985
  for (const engine of engines) this.registerEngine(engine.language, engine);
17717
17986
  this.defaultLanguage = config.defaultLanguage;
17718
17987
  this.renderer.mount(container, config.theme);
17719
- this.renderer.onInputChange((e) => this.applyInputs(e.indicatorId, { [e.key]: e.value }));
17988
+ this.renderer.onInputChange(
17989
+ (e) => e.kind === "prop" ? this.applyProps(e.indicatorId, { [e.key]: e.value }) : this.applyInputs(e.indicatorId, { [e.key]: e.value })
17990
+ );
17720
17991
  this.renderer.onRemoveIndicator((id) => this.removeIndicator(id));
17721
17992
  this.renderer.onToggleIndicatorVisible?.((id, visible) => this.setVisible(id, visible));
17722
17993
  this.paneActionUnsub = this.renderer.onPaneAction?.((a) => this.handlePaneAction(a)) ?? null;
@@ -18396,7 +18667,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
18396
18667
  const title = options.title ?? "Indicator";
18397
18668
  const handle = new IndicatorHandleImpl(id, title, this, source);
18398
18669
  this.handles.set(id, handle);
18399
- this.registry.add({ id, title, source, options, inputValues: { ...options.inputs ?? {} } });
18670
+ this.registry.add({ id, title, source, options, inputValues: { ...options.inputs ?? {} }, propValues: { ...options.props ?? {} } });
18400
18671
  void this.startIndicator(id, source, options, handle);
18401
18672
  return handle;
18402
18673
  }
@@ -18419,7 +18690,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
18419
18690
  return handle;
18420
18691
  }
18421
18692
  const inputValues = { ...descriptor.defaultInputs(), ...options.inputs ?? {} };
18422
- this.registry.add({ id, title, source: type, inputValues, native: { type, instance: descriptor.create(), descriptor } });
18693
+ this.registry.add({ id, title, source: type, inputValues, propValues: {}, native: { type, instance: descriptor.create(), descriptor } });
18423
18694
  handle.setSchema(descriptor.inputsSchema());
18424
18695
  void this.startNativeIndicator(id, handle);
18425
18696
  return handle;
@@ -18472,6 +18743,19 @@ var EngineOrchestrator = class _EngineOrchestrator {
18472
18743
  if (record.session) record.session.update(record.inputValues);
18473
18744
  else if (record.native && !record.hidden) record.native.instance.setInputs(record.inputValues);
18474
18745
  }
18746
+ /** IndicatorController: re-run an indicator with merged declaration-prop overrides.
18747
+ * Same lifecycle as {@link applyInputs} — a prop change replays the whole script.
18748
+ * Script indicators only: natives have no declaration props. */
18749
+ applyProps(id, values) {
18750
+ const record = this.registry.get(id);
18751
+ if (!record?.session) return;
18752
+ record.propValues = { ...record.propValues, ...values };
18753
+ if (record.renderHandle) this.renderer.setIndicatorInputs(record.renderHandle, record.inputValues, record.propValues);
18754
+ record.pendingStructural = true;
18755
+ if (!record.hidden) this.setLoading(record, true);
18756
+ record.pendingCause = "inputs";
18757
+ record.session.update(record.inputValues, record.propValues);
18758
+ }
18475
18759
  /** IndicatorController: tear down an indicator and (if now empty) its pane. */
18476
18760
  /** Live handles of every indicator on the chart (script + native), insertion order. */
18477
18761
  listIndicators() {
@@ -18620,6 +18904,10 @@ var EngineOrchestrator = class _EngineOrchestrator {
18620
18904
  for (const input of prepared.inputs) defaults2[input.key] = input.defval;
18621
18905
  record.inputValues = { ...defaults2, ...record.inputValues };
18622
18906
  handle.setSchema(prepared.inputs);
18907
+ const propDefaults = {};
18908
+ for (const prop of prepared.props ?? []) propDefaults[prop.key] = prop.defval;
18909
+ record.propValues = { ...propDefaults, ...record.propValues };
18910
+ handle.setPropsSchema(prepared.props ?? []);
18623
18911
  this.mountLoadingPlaceholder(id, record);
18624
18912
  this.executeIndicator(id, handle);
18625
18913
  } catch (err) {
@@ -18643,6 +18931,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
18643
18931
  getBars: () => this.bars,
18644
18932
  fetchSeries: (sym, tf, range) => this.fetchSeries(sym, tf, range),
18645
18933
  inputs: record.inputValues,
18934
+ props: record.propValues,
18646
18935
  visibleRange: this.currentVisibleRange(),
18647
18936
  mode,
18648
18937
  // Mid-backfill session starts (add / re-show / price-style re-execute) let the
@@ -18736,6 +19025,9 @@ var EngineOrchestrator = class _EngineOrchestrator {
18736
19025
  const meta = record.prepared.meta;
18737
19026
  const model = {
18738
19027
  id,
19028
+ // Deliberately the FULL title, never a shorttitle: while the script loads the
19029
+ // legend identifies it by its full name; the compact shorttitle arrives with
19030
+ // the first computed model and takes over from there.
18739
19031
  title: record.options?.title ?? meta.title,
18740
19032
  overlay: meta.overlay,
18741
19033
  paneHint: meta.overlay ? "price" : "new",
@@ -18744,7 +19036,8 @@ var EngineOrchestrator = class _EngineOrchestrator {
18744
19036
  backgrounds: [],
18745
19037
  priceLines: [],
18746
19038
  inputs: record.prepared.inputs,
18747
- inputValues: record.inputValues
19039
+ inputValues: record.inputValues,
19040
+ ...record.prepared.props ? { props: record.prepared.props, propValues: record.propValues } : {}
18748
19041
  };
18749
19042
  const paneId = this.routePane(id, model, record.options ?? {});
18750
19043
  this.placeModel(model, id, paneId);
@@ -18977,7 +19270,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
18977
19270
  record.model = model;
18978
19271
  if (record.pendingStructural) {
18979
19272
  record.renderHandle = this.renderer.mountIndicator(model);
18980
- this.renderer.setIndicatorInputs(record.renderHandle, record.inputValues);
19273
+ this.renderer.setIndicatorInputs(record.renderHandle, record.inputValues, record.propValues);
18981
19274
  record.pendingStructural = false;
18982
19275
  } else {
18983
19276
  this.renderer.updateIndicator(record.renderHandle, modelToValuePatch(model));
@@ -19004,16 +19297,17 @@ var EngineOrchestrator = class _EngineOrchestrator {
19004
19297
  placeModel(model, id, paneId) {
19005
19298
  model.id = id;
19006
19299
  model.paneId = paneId;
19007
- for (const series of model.series) series.paneId = paneId;
19008
- for (const fill of model.fills) fill.paneId = paneId;
19009
- for (const bg of model.backgrounds) bg.paneId = paneId;
19300
+ const paneOf = (item) => item.overlay === true ? "price" : paneId;
19301
+ for (const series of model.series) series.paneId = paneOf(series);
19302
+ for (const fill of model.fills) fill.paneId = paneOf(fill);
19303
+ for (const bg of model.backgrounds) bg.paneId = paneOf(bg);
19010
19304
  for (const line of model.priceLines) line.paneId = paneId;
19011
- if (model.lines) for (const ln of model.lines) ln.paneId = paneId;
19012
- if (model.boxes) for (const bx of model.boxes) bx.paneId = paneId;
19013
- if (model.labels) for (const lb of model.labels) lb.paneId = paneId;
19014
- if (model.polylines) for (const pl of model.polylines) pl.paneId = paneId;
19015
- if (model.linefills) for (const lf of model.linefills) lf.paneId = paneId;
19016
- if (model.tables) for (const tb of model.tables) tb.paneId = paneId;
19305
+ if (model.lines) for (const ln of model.lines) ln.paneId = paneOf(ln);
19306
+ if (model.boxes) for (const bx of model.boxes) bx.paneId = paneOf(bx);
19307
+ if (model.labels) for (const lb of model.labels) lb.paneId = paneOf(lb);
19308
+ if (model.polylines) for (const pl of model.polylines) pl.paneId = paneOf(pl);
19309
+ if (model.linefills) for (const lf of model.linefills) lf.paneId = paneOf(lf);
19310
+ if (model.tables) for (const tb of model.tables) tb.paneId = paneOf(tb);
19017
19311
  }
19018
19312
  emitContextChanged(id) {
19019
19313
  if (!this.registry.get(id)?.session?.getContext) return;
@@ -19166,6 +19460,15 @@ var RendererControl = class {
19166
19460
  setLegendActions(provider) {
19167
19461
  this.renderer.setLegendActions?.(provider);
19168
19462
  }
19463
+ /**
19464
+ * Wire the legend rows' HOST-CONTRIBUTED callout bubbles (the shells route the
19465
+ * plugin registry through this; see `registerLegendCallout`). Silent on a renderer
19466
+ * without the seam — contributed callouts simply never show there, same graceful
19467
+ * degradation as {@link setLegendActions}.
19468
+ */
19469
+ setLegendCallouts(provider) {
19470
+ this.renderer.setLegendCallouts?.(provider);
19471
+ }
19169
19472
  /**
19170
19473
  * Replace the indicator legend's fold toggle with a host action (or restore it with
19171
19474
  * `null`) — multi-chart shells point the chip at their indicator overview instead of
@@ -19675,6 +19978,10 @@ function inputVisible(when, values) {
19675
19978
  }
19676
19979
 
19677
19980
  // src/renderers/shared/IndicatorInputsDialog.ts
19981
+ var PROPS_TAB = "Properties";
19982
+ function bagOf(row, decl) {
19983
+ return decl.prop ? row.propValues : row.values;
19984
+ }
19678
19985
  var SOURCES = ["close", "open", "high", "low", "hl2", "hlc3", "ohlc4", "volume"];
19679
19986
  var IndicatorInputsDialog = class {
19680
19987
  constructor(host) {
@@ -19685,6 +19992,7 @@ var IndicatorInputsDialog = class {
19685
19992
  this.openId = null;
19686
19993
  this.row = null;
19687
19994
  this.snapshot = null;
19995
+ this.propSnapshot = null;
19688
19996
  this.dialogTips = [];
19689
19997
  /** Re-applies every `when` gate against current values; set while the dialog is open. */
19690
19998
  this.refreshVisibility = null;
@@ -19707,10 +20015,11 @@ var IndicatorInputsDialog = class {
19707
20015
  }
19708
20016
  open(row) {
19709
20017
  this.close();
19710
- if (row.inputs.length === 0) return;
20018
+ if (row.inputs.length === 0 && row.props.length === 0) return;
19711
20019
  this.row = row;
19712
20020
  this.openId = row.id;
19713
20021
  this.snapshot = { ...row.values };
20022
+ this.propSnapshot = { ...row.propValues };
19714
20023
  ensureDialogStyles();
19715
20024
  const t = this.host.theme();
19716
20025
  const border = "var(--vela-border)";
@@ -19718,7 +20027,7 @@ var IndicatorInputsDialog = class {
19718
20027
  const host = this.host.dialogHost() ?? this.host.container;
19719
20028
  const ui = new Dialog({
19720
20029
  host,
19721
- title: row.settingsTitle,
20030
+ title: row.title,
19722
20031
  // Non-modal: live-edit dialog — a modal machine locks pointer events on the
19723
20032
  // whole body, killing the chart and the body-portaled popovers.
19724
20033
  modal: false,
@@ -19730,12 +20039,16 @@ var IndicatorInputsDialog = class {
19730
20039
  closeOnEscape: false,
19731
20040
  footer: (foot) => {
19732
20041
  foot.append(
20042
+ this.resetAction(),
19733
20043
  this.dialogButton("Cancel", false, () => this.revertAndClose()),
19734
20044
  this.dialogButton("Ok", true, () => this.close())
19735
20045
  );
19736
20046
  },
20047
+ // Stale-guard: destroying a dialog fires its machine's onOpenChange(false)
20048
+ // asynchronously — after a reset rebuild that notification must not close
20049
+ // the replacement dialog.
19737
20050
  onOpenChange: (open2) => {
19738
- if (!open2) this.close();
20051
+ if (!open2 && this.uiDialog === ui) this.close();
19739
20052
  }
19740
20053
  });
19741
20054
  applyChromeTokens(ui.panel, t);
@@ -19746,6 +20059,16 @@ var IndicatorInputsDialog = class {
19746
20059
  text: () => "Close"
19747
20060
  }));
19748
20061
  const tabDefs = tabInputs(row.inputs);
20062
+ if (row.props.length > 0) {
20063
+ const propDecls = row.props.map((p) => {
20064
+ const d = { ...p, prop: true };
20065
+ delete d.tab;
20066
+ return d;
20067
+ });
20068
+ const existing = tabDefs.find((t2) => t2.name === PROPS_TAB);
20069
+ if (existing) existing.inputs.push(...propDecls);
20070
+ else tabDefs.push({ name: PROPS_TAB, inputs: propDecls });
20071
+ }
19749
20072
  const tabs = document.createElement("div");
19750
20073
  tabs.style.cssText = `display:flex;gap:12px;padding:0 20px;border-bottom:1px solid ${border};flex:0 0 auto;`;
19751
20074
  const tabEls = [];
@@ -19848,9 +20171,10 @@ var IndicatorInputsDialog = class {
19848
20171
  this.openId = null;
19849
20172
  this.row = null;
19850
20173
  this.snapshot = null;
20174
+ this.propSnapshot = null;
19851
20175
  ui?.destroy();
19852
20176
  }
19853
- /** Restore every input to its open-time value (re-running the indicator), then close. */
20177
+ /** Restore every input and prop to its open-time value (re-running the indicator), then close. */
19854
20178
  revertAndClose() {
19855
20179
  const row = this.row;
19856
20180
  const snap = this.snapshot;
@@ -19864,12 +20188,55 @@ var IndicatorInputsDialog = class {
19864
20188
  }
19865
20189
  }
19866
20190
  }
20191
+ const propSnap = this.propSnapshot;
20192
+ if (row && propSnap) {
20193
+ for (const p of row.props) {
20194
+ const snapped = propSnap[p.key];
20195
+ const before = snapped !== void 0 ? snapped : p.defval;
20196
+ if (row.propValues[p.key] !== before) {
20197
+ row.propValues[p.key] = before;
20198
+ this.host.onChange?.({ indicatorId: row.id, key: p.key, value: before, kind: "prop" });
20199
+ }
20200
+ }
20201
+ }
19867
20202
  this.close();
19868
20203
  }
20204
+ /** The footer's reset button — same chip as Cancel, pinned to the LEFT edge
20205
+ * (`margin-right:auto` against the footer's flex-end keeps Cancel/Ok right). */
20206
+ resetAction() {
20207
+ const b = this.dialogButton("Reset defaults", false, () => this.resetToDefaults());
20208
+ b.style.marginRight = "auto";
20209
+ return b;
20210
+ }
20211
+ /** Restore every input to its declared default (re-running the indicator), then
20212
+ * re-open the form so each control re-reads the restored values — the same
20213
+ * rebuild-after-reset move as the chart-settings dialog. The open-time snapshot
20214
+ * survives the rebuild, so Cancel after a reset still reverts the whole session. */
20215
+ resetToDefaults() {
20216
+ const row = this.row;
20217
+ if (!row) return;
20218
+ const snap = this.snapshot;
20219
+ const propSnap = this.propSnapshot;
20220
+ for (const inp of row.inputs) {
20221
+ if (row.values[inp.key] !== inp.defval) {
20222
+ row.values[inp.key] = inp.defval;
20223
+ this.host.onChange?.({ indicatorId: row.id, key: inp.key, value: inp.defval });
20224
+ }
20225
+ }
20226
+ for (const p of row.props) {
20227
+ if (row.propValues[p.key] !== p.defval) {
20228
+ row.propValues[p.key] = p.defval;
20229
+ this.host.onChange?.({ indicatorId: row.id, key: p.key, value: p.defval, kind: "prop" });
20230
+ }
20231
+ }
20232
+ this.open(row);
20233
+ if (snap) this.snapshot = snap;
20234
+ if (propSnap) this.propSnapshot = propSnap;
20235
+ }
19869
20236
  /** Write one edit through: store it, notify the host, and re-apply the `when` gates. */
19870
- commit(row, key, value) {
19871
- row.values[key] = value;
19872
- this.host.onChange?.({ indicatorId: row.id, key, value });
20237
+ commit(row, decl, value) {
20238
+ bagOf(row, decl)[decl.key] = value;
20239
+ this.host.onChange?.({ indicatorId: row.id, key: decl.key, value, ...decl.prop ? { kind: "prop" } : {} });
19873
20240
  this.refreshVisibility?.();
19874
20241
  }
19875
20242
  /** One settings row (or several `inline=` inputs) placed into a section's grid. */
@@ -19896,7 +20263,7 @@ var IndicatorInputsDialog = class {
19896
20263
  const id = idOf(lead);
19897
20264
  const info = lead.tooltip ? this.infoButton(lead.tooltip) : void 0;
19898
20265
  if (lead.type === "bool") {
19899
- const current = Boolean(row.values[lead.key] ?? lead.defval);
20266
+ const current = Boolean(bagOf(row, lead)[lead.key] ?? lead.defval);
19900
20267
  return append(fieldRow({
19901
20268
  label: nameOf(lead),
19902
20269
  id,
@@ -19905,7 +20272,7 @@ var IndicatorInputsDialog = class {
19905
20272
  toggle: {
19906
20273
  id,
19907
20274
  checked: current,
19908
- onChange: (v) => this.commit(row, lead.key, v)
20275
+ onChange: (v) => this.commit(row, lead, v)
19909
20276
  }
19910
20277
  }));
19911
20278
  }
@@ -19930,8 +20297,8 @@ var IndicatorInputsDialog = class {
19930
20297
  }
19931
20298
  /** Build the typed control for one input, committing edits live via `onChange`. */
19932
20299
  buildControl(row, inp, id) {
19933
- const current = row.values[inp.key] ?? inp.defval;
19934
- const emit = (value) => this.commit(row, inp.key, value);
20300
+ const current = bagOf(row, inp)[inp.key] ?? inp.defval;
20301
+ const emit = (value) => this.commit(row, inp, value);
19935
20302
  if (inp.type === "bool") {
19936
20303
  return buildFieldControl({ kind: "switch", id, checked: Boolean(current), onChange: (v) => emit(v) }).el;
19937
20304
  }
@@ -19942,7 +20309,7 @@ var IndicatorInputsDialog = class {
19942
20309
  kind: "color",
19943
20310
  id,
19944
20311
  theme: this.host.theme(),
19945
- get: () => String(row.values[inp.key] ?? inp.defval),
20312
+ get: () => String(bagOf(row, inp)[inp.key] ?? inp.defval),
19946
20313
  onChange: (v) => emit(v)
19947
20314
  }).el;
19948
20315
  }
@@ -20505,6 +20872,7 @@ function timeParts(ts) {
20505
20872
  }
20506
20873
 
20507
20874
  // src/renderers/shared/InputsUI.ts
20875
+ var LEGEND_AT_TOP_ATTR = "data-vela-pane-at-top";
20508
20876
  var STATUS_KEYFRAMES = "@keyframes vela-ind-pulse{0%,100%{opacity:1;transform:scale(1)}50%{opacity:.3;transform:scale(.6)}}";
20509
20877
  var LEGEND_ICON_PX2 = 16;
20510
20878
  var LEGEND_CTL_PX = 18;
@@ -20543,10 +20911,14 @@ var InputsUI = class {
20543
20911
  this.moveApi = null;
20544
20912
  /** Host-contributed legend actions, resolved PER ROW at render time (see setLegendActions). */
20545
20913
  this.legendActions = null;
20914
+ /** Host-contributed callout bubbles, resolved PER ROW at render time (see setLegendCallouts). */
20915
+ this.legendCallouts = null;
20546
20916
  /** Chrome-tooltip disposers, per row id — a tip open at removal must not outlive its row. */
20547
20917
  this.rowTips = /* @__PURE__ */ new Map();
20548
20918
  /** Same, for the contributed extras only (rebuilt independently by setLegendActions). */
20549
20919
  this.extrasTips = /* @__PURE__ */ new Map();
20920
+ /** Same, for the contributed callouts only (rebuilt independently by setLegendCallouts). */
20921
+ this.calloutTips = /* @__PURE__ */ new Map();
20550
20922
  /** Open "Move to" menu (kept so it can be torn down). */
20551
20923
  this.moveMenu = null;
20552
20924
  this.moveTargets = [];
@@ -20704,6 +21076,42 @@ var InputsUI = class {
20704
21076
  extrasEl.appendChild(btn2);
20705
21077
  }
20706
21078
  }
21079
+ /**
21080
+ * Wire the host-contributed callout bubbles. Same contract as
21081
+ * {@link setLegendActions}: re-calling replaces the provider and re-projects the
21082
+ * rows already on screen.
21083
+ */
21084
+ setLegendCallouts(provider) {
21085
+ this.legendCallouts = provider;
21086
+ for (const row of this.rows.values()) this.renderCallouts(row);
21087
+ }
21088
+ /** (Re)build one row's callout bubbles — tinted icon circles beside the title whose
21089
+ * click (when the view carries content) deploys a panel of text and actions. */
21090
+ renderCallouts(row) {
21091
+ this.disposeTips(this.calloutTips, row.id);
21092
+ for (const bubble of row.callouts) bubble.destroy();
21093
+ row.callouts = [];
21094
+ row.calloutsEl.replaceChildren();
21095
+ const views = this.legendCallouts?.(row.id) ?? [];
21096
+ row.calloutsEl.style.display = views.length > 0 ? "inline-flex" : "none";
21097
+ for (const view of views) {
21098
+ const bubble = new CalloutBubble({
21099
+ icon: view.icon,
21100
+ background: view.background,
21101
+ ...view.color !== void 0 ? { color: view.color } : {},
21102
+ label: view.tooltip,
21103
+ ...view.content !== void 0 ? { panel: view.content } : {},
21104
+ // The panel portals into the plot host and self-tokens: it must work on
21105
+ // a bare chart, where no `.vela-ui` token ancestor exists.
21106
+ host: this.container,
21107
+ theme: () => this.theme
21108
+ });
21109
+ bubble.el.dataset.legendCallout = view.id;
21110
+ this.tip(this.calloutTips, row.id, bubble.el, view.tooltip);
21111
+ row.callouts.push(bubble);
21112
+ row.calloutsEl.appendChild(bubble.el);
21113
+ }
21114
+ }
20707
21115
  /** Reposition the per-pane legend containers after a layout change. */
20708
21116
  reposition() {
20709
21117
  for (const [paneId, lg] of this.legends) this.positionLegend(lg, paneId);
@@ -20871,6 +21279,7 @@ var InputsUI = class {
20871
21279
  }
20872
21280
  positionLegend(lg, paneId) {
20873
21281
  const bounds = this.paneBoundsOf ? this.paneBoundsOf(paneId) : { top: 0, height: Infinity };
21282
+ lg.toggleAttribute(LEGEND_AT_TOP_ATTR, bounds.top === 0);
20874
21283
  lg.style.display = bounds.height < 4 || !this.titlesVisible ? "none" : "flex";
20875
21284
  const collapsed = this.paneCollapse.has(paneId);
20876
21285
  const masterId = this.paneCollapse.get(paneId) ?? null;
@@ -20946,13 +21355,13 @@ var InputsUI = class {
20946
21355
  }
20947
21356
  /** Create or update an indicator's legend row (in the legend for its pane). */
20948
21357
  upsert(id, title, inputs, values, paneId = "price", opts = {}) {
20949
- const settingsTitle = opts.settingsTitle ?? title;
20950
21358
  const existing = this.rows.get(id);
20951
21359
  if (existing) {
20952
21360
  existing.title = title;
20953
- existing.settingsTitle = settingsTitle;
20954
21361
  existing.inputs = inputs;
20955
21362
  existing.values = { ...values };
21363
+ existing.props = opts.props ?? [];
21364
+ existing.propValues = { ...opts.propValues ?? {} };
20956
21365
  existing.titleEl.textContent = title;
20957
21366
  if (existing.paneId !== paneId) {
20958
21367
  existing.paneId = paneId;
@@ -21004,6 +21413,9 @@ var InputsUI = class {
21004
21413
  titleWrap.appendChild(beta);
21005
21414
  }
21006
21415
  el.appendChild(titleWrap);
21416
+ const calloutsEl = document.createElement("span");
21417
+ calloutsEl.style.cssText = `display:none;align-items:center;gap:4px;flex:none;margin-left:${LEGEND_TITLE_STATUS_GAP_PX}px;`;
21418
+ el.appendChild(calloutsEl);
21007
21419
  el.appendChild(statusEl);
21008
21420
  const valuesEl = document.createElement("span");
21009
21421
  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;`;
@@ -21020,13 +21432,13 @@ var InputsUI = class {
21020
21432
  eye.className = "vela-ind-ctl";
21021
21433
  eye.style.cssText = LEGEND_CTL_CSS;
21022
21434
  eye.addEventListener("click", () => {
21023
- const row = this.rows.get(id);
21024
- this.onToggleVisible?.(id, Boolean(row?.hidden));
21435
+ const row2 = this.rows.get(id);
21436
+ this.onToggleVisible?.(id, Boolean(row2?.hidden));
21025
21437
  });
21026
21438
  controlsEl.appendChild(eye);
21027
21439
  eyeEl = eye;
21028
21440
  }
21029
- if (inputs.length > 0) {
21441
+ if (inputs.length > 0 || (opts.props ?? []).length > 0) {
21030
21442
  const gear = document.createElement("button");
21031
21443
  gear.type = "button";
21032
21444
  gear.setAttribute("aria-label", "Settings");
@@ -21066,7 +21478,9 @@ var InputsUI = class {
21066
21478
  controlsEl.appendChild(close);
21067
21479
  el.appendChild(controlsEl);
21068
21480
  this.attach(this.legendFor(paneId), el, !!opts.native);
21069
- 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 });
21481
+ 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 };
21482
+ this.rows.set(id, row);
21483
+ this.renderCallouts(row);
21070
21484
  this.syncFoldToggle();
21071
21485
  }
21072
21486
  /** Place a row in its pane's legend — native rows PREPEND (pinned to the top), Pine rows append. */
@@ -21075,9 +21489,11 @@ var InputsUI = class {
21075
21489
  else container.appendChild(el);
21076
21490
  }
21077
21491
  /** Reflect programmatic input changes (so a re-opened dialog shows current values). */
21078
- setValues(id, values) {
21492
+ setValues(id, values, props) {
21079
21493
  const row = this.rows.get(id);
21080
- if (row) row.values = { ...row.values, ...values };
21494
+ if (!row) return;
21495
+ row.values = { ...row.values, ...values };
21496
+ if (props) row.propValues = { ...row.propValues, ...props };
21081
21497
  }
21082
21498
  /**
21083
21499
  * Reflect an indicator's live status in its legend row: `'loading'` shows three pulsing
@@ -21159,8 +21575,13 @@ var InputsUI = class {
21159
21575
  syncRowActions(row) {
21160
21576
  const open2 = row.highlighted;
21161
21577
  row.controlsEl.style.display = open2 || row.hidden ? "inline-flex" : "none";
21162
- if (open2) row.el.appendChild(row.statusEl);
21163
- else row.el.insertBefore(row.statusEl, row.valuesEl);
21578
+ if (open2) {
21579
+ row.el.appendChild(row.statusEl);
21580
+ row.el.appendChild(row.calloutsEl);
21581
+ } else {
21582
+ row.el.insertBefore(row.statusEl, row.valuesEl);
21583
+ row.el.insertBefore(row.calloutsEl, row.statusEl);
21584
+ }
21164
21585
  for (const child of Array.from(row.controlsEl.children)) {
21165
21586
  if (!(child instanceof HTMLElement) || child === row.eyeEl) continue;
21166
21587
  if (child === row.extrasEl) {
@@ -21178,10 +21599,12 @@ var InputsUI = class {
21178
21599
  }
21179
21600
  remove(id) {
21180
21601
  const row = this.rows.get(id);
21602
+ for (const bubble of row?.callouts ?? []) bubble.destroy();
21181
21603
  row?.el.remove();
21182
21604
  this.rows.delete(id);
21183
21605
  this.disposeTips(this.rowTips, id);
21184
21606
  this.disposeTips(this.extrasTips, id);
21607
+ this.disposeTips(this.calloutTips, id);
21185
21608
  if (this.selectedId === id) this.selectedId = null;
21186
21609
  if (this.inputsDialog.openId === id) this.inputsDialog.close();
21187
21610
  this.syncFoldToggle();
@@ -21201,6 +21624,8 @@ var InputsUI = class {
21201
21624
  this.rowMenu = null;
21202
21625
  for (const id of [...this.rowTips.keys()]) this.disposeTips(this.rowTips, id);
21203
21626
  for (const id of [...this.extrasTips.keys()]) this.disposeTips(this.extrasTips, id);
21627
+ for (const id of [...this.calloutTips.keys()]) this.disposeTips(this.calloutTips, id);
21628
+ for (const row of this.rows.values()) for (const bubble of row.callouts) bubble.destroy();
21204
21629
  for (const lg of this.legends.values()) lg.remove();
21205
21630
  this.legends.clear();
21206
21631
  this.rows.clear();
@@ -21555,6 +21980,32 @@ var SIZE_PX3 = {
21555
21980
  large: 16,
21556
21981
  huge: 20
21557
21982
  };
21983
+ function fontPxOf(size) {
21984
+ if (typeof size === "number") return size > 0 ? size : SIZE_PX3.auto;
21985
+ return SIZE_PX3[size] ?? SIZE_PX3.auto;
21986
+ }
21987
+ function tableHasContent(t) {
21988
+ return t.cells.some((row) => row?.some((c) => c != null && !c.merged));
21989
+ }
21990
+ function mergeRenderPlan(t) {
21991
+ const span = /* @__PURE__ */ new Map();
21992
+ const omit = /* @__PURE__ */ new Set();
21993
+ for (const m of t.merges) {
21994
+ span.set(`${m.startRow}:${m.startCol}`, { cs: m.endCol - m.startCol + 1, rs: m.endRow - m.startRow + 1 });
21995
+ for (let r = m.startRow; r <= m.endRow; r += 1) {
21996
+ for (let c = m.startCol; c <= m.endCol; c += 1) {
21997
+ if (r !== m.startRow || c !== m.startCol) omit.add(`${r}:${c}`);
21998
+ }
21999
+ }
22000
+ }
22001
+ for (let r = 0; r < t.rows; r += 1) {
22002
+ for (let c = 0; c < t.columns; c += 1) {
22003
+ if (t.cells[r]?.[c]?.merged && !span.has(`${r}:${c}`)) omit.add(`${r}:${c}`);
22004
+ }
22005
+ }
22006
+ for (const key of span.keys()) omit.delete(key);
22007
+ return { span, omit };
22008
+ }
21558
22009
  var TableOverlay = class {
21559
22010
  constructor(container, theme, paneBounds) {
21560
22011
  this.container = container;
@@ -21575,7 +22026,9 @@ var TableOverlay = class {
21575
22026
  update(tables) {
21576
22027
  this.lastTables = tables;
21577
22028
  this.root.replaceChildren();
21578
- for (const t of tables) this.root.appendChild(this.renderTable(t));
22029
+ for (const t of tables) {
22030
+ if (tableHasContent(t)) this.root.appendChild(this.renderTable(t));
22031
+ }
21579
22032
  }
21580
22033
  /** Re-render at the current pane geometry — after layout settles or on resize. */
21581
22034
  reposition() {
@@ -21590,39 +22043,36 @@ var TableOverlay = class {
21590
22043
  this.root.remove();
21591
22044
  }
21592
22045
  renderTable(t) {
22046
+ const b = this.paneBounds(t.paneId);
21593
22047
  const wrap = document.createElement("div");
21594
22048
  wrap.style.position = "absolute";
21595
- this.anchor(wrap, t.position, this.paneBounds(t.paneId));
22049
+ if (t.frameColor && t.frameWidth > 0) wrap.style.border = `${t.frameWidth}px solid ${t.frameColor}`;
22050
+ this.anchor(wrap, t.position, b);
21596
22051
  const table = document.createElement("table");
21597
22052
  Object.assign(table.style, {
21598
22053
  borderCollapse: "collapse",
21599
22054
  background: t.bgColor ?? "transparent",
21600
22055
  fontFamily: this.theme.fontFamily || "sans-serif",
21601
- // Frame as an inset shadow (not a `border`) so it stays independent of the
21602
- // collapsed cell borders even when frame_width != border_width.
21603
- boxShadow: t.frameColor && t.frameWidth > 0 ? `inset 0 0 0 ${t.frameWidth}px ${t.frameColor}` : "none",
21604
22056
  border: "none",
21605
22057
  tableLayout: "auto",
21606
22058
  // Re-enable pointer events on the table only (root is none) so cell tooltips work.
21607
22059
  pointerEvents: "auto"
21608
22060
  });
21609
- const span = /* @__PURE__ */ new Map();
21610
- const skip = /* @__PURE__ */ new Set();
21611
- for (const m of t.merges) {
21612
- span.set(`${m.startRow}:${m.startCol}`, { cs: m.endCol - m.startCol + 1, rs: m.endRow - m.startRow + 1 });
21613
- for (let r = m.startRow; r <= m.endRow; r += 1) {
21614
- for (let c = m.startCol; c <= m.endCol; c += 1) {
21615
- if (r !== m.startRow || c !== m.startCol) skip.add(`${r}:${c}`);
21616
- }
21617
- }
21618
- }
22061
+ const { span, omit } = mergeRenderPlan(t);
22062
+ const plotW = Math.max(0, (this.root.clientWidth || this.container.clientWidth) - b.rightAxis);
22063
+ const paneH = b.height;
21619
22064
  const cellBorder = t.borderColor && t.borderWidth > 0 ? `${t.borderWidth}px solid ${t.borderColor}` : "none";
21620
22065
  for (let r = 0; r < t.rows; r += 1) {
21621
22066
  const tr = document.createElement("tr");
21622
22067
  for (let c = 0; c < t.columns; c += 1) {
22068
+ if (omit.has(`${r}:${c}`)) continue;
21623
22069
  const cell = t.cells[r]?.[c] ?? null;
21624
- if (skip.has(`${r}:${c}`) || cell?.merged) continue;
21625
22070
  const td = document.createElement("td");
22071
+ if (cell === null) {
22072
+ Object.assign(td.style, { padding: "0", border: "none" });
22073
+ tr.appendChild(td);
22074
+ continue;
22075
+ }
21626
22076
  const sp = span.get(`${r}:${c}`);
21627
22077
  if (sp) {
21628
22078
  if (sp.cs > 1) td.colSpan = sp.cs;
@@ -21631,18 +22081,22 @@ var TableOverlay = class {
21631
22081
  Object.assign(td.style, {
21632
22082
  border: cellBorder,
21633
22083
  padding: "2px 6px",
21634
- background: cell?.bgColor ?? "transparent",
21635
- color: cell?.textColor ?? this.theme.textColor,
21636
- textAlign: cell?.hAlign ?? "center",
21637
- verticalAlign: cell?.vAlign === "top" ? "top" : cell?.vAlign === "bottom" ? "bottom" : "middle",
21638
- fontSize: `${SIZE_PX3[cell?.textSize ?? "normal"]}px`,
21639
- fontFamily: cell?.fontFamily === "monospace" ? "monospace" : "inherit",
21640
- fontWeight: cell?.bold ? "bold" : "normal",
21641
- fontStyle: cell?.italic ? "italic" : "normal",
21642
- whiteSpace: "pre-line"
22084
+ background: cell.bgColor ?? "transparent",
22085
+ color: cell.textColor ?? this.theme.textColor,
22086
+ textAlign: cell.hAlign,
22087
+ verticalAlign: cell.vAlign === "top" ? "top" : cell.vAlign === "bottom" ? "bottom" : "middle",
22088
+ fontSize: `${fontPxOf(cell.textSize)}px`,
22089
+ fontFamily: cell.fontFamily === "monospace" ? "monospace" : "inherit",
22090
+ fontWeight: cell.bold ? "bold" : "normal",
22091
+ fontStyle: cell.italic ? "italic" : "normal",
22092
+ // Pine cell text never wraps; `\n` still breaks lines. Wrapping
22093
+ // used to collapse unicode sparklines and ━━━ dividers.
22094
+ whiteSpace: "pre"
21643
22095
  });
21644
- if (cell?.tooltip) td.title = cell.tooltip;
21645
- td.textContent = cell?.text ?? "";
22096
+ if (cell.width) td.style.width = `${cell.width / 100 * plotW}px`;
22097
+ if (cell.height) td.style.height = `${cell.height / 100 * paneH}px`;
22098
+ if (cell.tooltip) td.title = cell.tooltip;
22099
+ td.textContent = cell.text ?? "";
21646
22100
  tr.appendChild(td);
21647
22101
  }
21648
22102
  table.appendChild(tr);
@@ -22244,8 +22698,14 @@ var WebGL2Backend = class {
22244
22698
  return sc === pane.scale ? pane : { ...pane, scale: sc };
22245
22699
  };
22246
22700
  b.alpha = this.modelAlpha;
22247
- for (const m of models) for (const bgSpan of m.backgrounds) this.emitBackground(b, bgSpan, pane, coords);
22248
- 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));
22701
+ for (const m of models) for (const bgSpan of m.backgrounds) if (bgSpan.overlay !== true) this.emitBackground(b, bgSpan, pane, coords);
22702
+ 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));
22703
+ if (isPrice) {
22704
+ for (const m of scene.indicators.values()) {
22705
+ for (const bgSpan of m.backgrounds) if (bgSpan.overlay === true) this.emitBackground(b, bgSpan, pane, coords);
22706
+ for (const f of m.fills) if (f.overlay === true) this.emitFill(b, m, f, pane, coords, i0, i1, scene.offsetOf(m.id));
22707
+ }
22708
+ }
22249
22709
  const drawCandles = isPrice && !scene.candlesHidden;
22250
22710
  let candleDrawn = false;
22251
22711
  for (const m of models) {
@@ -22259,7 +22719,7 @@ var WebGL2Backend = class {
22259
22719
  b.alpha = this.modelAlpha;
22260
22720
  const off = scene.offsetOf(m.id);
22261
22721
  const mp = effPane(m);
22262
- for (const s of m.series) this.emitSeries(b, s, mp, coords, i0, i1, theme, off);
22722
+ for (const s of m.series) if (s.overlay !== true) this.emitSeries(b, s, mp, coords, i0, i1, theme, off);
22263
22723
  }
22264
22724
  if (drawCandles && !candleDrawn) {
22265
22725
  drawSlicesUpTo(scene.candleZ);
@@ -22268,6 +22728,12 @@ var WebGL2Backend = class {
22268
22728
  }
22269
22729
  drawSlicesUpTo(Infinity);
22270
22730
  b.alpha = this.modelAlpha;
22731
+ if (isPrice) {
22732
+ for (const m of scene.indicators.values()) {
22733
+ const off = scene.offsetOf(m.id);
22734
+ for (const s of m.series) if (s.overlay === true) this.emitSeries(b, s, pane, coords, i0, i1, theme, off);
22735
+ }
22736
+ }
22271
22737
  for (const m of models) {
22272
22738
  const mp = effPane(m);
22273
22739
  for (const pl of m.priceLines) this.emitHline(b, pl, mp, coords, dataW, theme);
@@ -22314,7 +22780,7 @@ var WebGL2Backend = class {
22314
22780
  for (const pane of scene.orderedPanes()) {
22315
22781
  const b = this.glowBatch;
22316
22782
  b.reset();
22317
- this.emitGlowSources(b, scene.indicatorsForPane(pane.id), pane, coords, i0, i1, (id) => scene.offsetOf(id));
22783
+ this.emitGlowSources(b, scene, pane, coords, i0, i1);
22318
22784
  if (b.vertexCount === 0) continue;
22319
22785
  const topH = Math.round(pane.bounds.top * dpr * 0.5);
22320
22786
  const botH = Math.round((pane.bounds.top + pane.bounds.height) * dpr * 0.5);
@@ -22383,15 +22849,23 @@ var WebGL2Backend = class {
22383
22849
  gl.bindVertexArray(null);
22384
22850
  return true;
22385
22851
  }
22386
- /** The "neon" elements that glow: line/area/step lines + point markers (not candles/fills/bars). */
22387
- emitGlowSources(b, models, pane, coords, i0, i1, offsetOf = () => 0) {
22388
- for (const m of models) {
22389
- const off = offsetOf(m.id);
22390
- for (const s of m.series) {
22391
- if (!isLineLikeSeries(s) || s.visible === false) continue;
22392
- if (s.kind === "histogram" || s.kind === "columns") continue;
22393
- if (s.kind === "circles" || s.kind === "cross") this.emitPointMarkers(b, s, pane, coords, i0, i1, off);
22394
- else this.emitPolyline(b, s, pane, coords, i0, i1, s.kind === "step", off);
22852
+ /** The "neon" elements that glow: line/area/step lines + point markers (not candles/fills/bars).
22853
+ * Routing mirrors the main pass: own series per pane, force_overlay series on the price pane. */
22854
+ emitGlowSources(b, scene, pane, coords, i0, i1) {
22855
+ const emitOne = (s, off) => {
22856
+ if (!isLineLikeSeries(s) || s.visible === false) return;
22857
+ if (s.kind === "histogram" || s.kind === "columns") return;
22858
+ if (s.kind === "circles" || s.kind === "cross") this.emitPointMarkers(b, s, pane, coords, i0, i1, off);
22859
+ else this.emitPolyline(b, s, pane, coords, i0, i1, s.kind === "step", off);
22860
+ };
22861
+ for (const m of scene.indicatorsForPane(pane.id)) {
22862
+ const off = scene.offsetOf(m.id);
22863
+ for (const s of m.series) if (s.overlay !== true) emitOne(s, off);
22864
+ }
22865
+ if (pane.kind === "price") {
22866
+ for (const m of scene.indicators.values()) {
22867
+ const off = scene.offsetOf(m.id);
22868
+ for (const s of m.series) if (s.overlay === true) emitOne(s, off);
22395
22869
  }
22396
22870
  }
22397
22871
  }
@@ -22674,9 +23148,9 @@ var WebGL2Backend = class {
22674
23148
  b.alpha = this.candleBodyAlpha;
22675
23149
  b.rect(g.bodyX, bodyTop, g.bodyW, bodyH, c);
22676
23150
  }
22677
- if (cs.borderVisible || bc || fading && cs.bodyVisible) {
23151
+ if (cs.borderVisible || fading && cs.bodyVisible) {
22678
23152
  b.alpha = this.candleStructureAlpha;
22679
- const bord = cs.borderVisible || bc ? parseColor((isUp ? cs.borderUpColor : cs.borderDownColor) ?? dir) : c;
23153
+ const bord = cs.borderVisible ? parseColor((isUp ? cs.borderUpColor : cs.borderDownColor) ?? bodyColorStr) : c;
22680
23154
  const bw = Math.max(0, g.bodyW - 1);
22681
23155
  const bh = Math.max(0, bodyH - 1);
22682
23156
  b.rectStroke(g.bodyX + 0.5, bodyTop + 0.5, bw, bh, 1, bord);
@@ -24228,8 +24702,14 @@ var Canvas2dBackend = class {
24228
24702
  return sc === pane.scale ? pane : { ...pane, scale: sc };
24229
24703
  };
24230
24704
  ctx.globalAlpha = this.modelAlpha;
24231
- for (const m of models) for (const bg of m.backgrounds) this.drawBackground(ctx, bg, pane, coords);
24232
- 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));
24705
+ for (const m of models) for (const bg of m.backgrounds) if (bg.overlay !== true) this.drawBackground(ctx, bg, pane, coords);
24706
+ 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));
24707
+ if (isPrice) {
24708
+ for (const m of scene.indicators.values()) {
24709
+ for (const bg of m.backgrounds) if (bg.overlay === true) this.drawBackground(ctx, bg, pane, coords);
24710
+ for (const f of m.fills) if (f.overlay === true) this.drawFill(ctx, m, f, pane, coords, i0, i1, scene.offsetOf(m.id));
24711
+ }
24712
+ }
24233
24713
  const slices = scene.drawingSlices.get(pane.id) ?? [];
24234
24714
  let si = 0;
24235
24715
  const drawSlicesUpTo = (z) => {
@@ -24251,7 +24731,7 @@ var Canvas2dBackend = class {
24251
24731
  ctx.globalAlpha = this.modelAlpha;
24252
24732
  const off = scene.offsetOf(m.id);
24253
24733
  const mp = effPane(m);
24254
- for (const s of m.series) this.drawSeries(ctx, s, mp, coords, i0, i1, theme, off);
24734
+ for (const s of m.series) if (s.overlay !== true) this.drawSeries(ctx, s, mp, coords, i0, i1, theme, off);
24255
24735
  }
24256
24736
  if (drawCandles && !candleDrawn) {
24257
24737
  drawSlicesUpTo(scene.candleZ);
@@ -24259,6 +24739,13 @@ var Canvas2dBackend = class {
24259
24739
  this.drawPriceSeries(ctx, scene, i0, i1, coords, pane, theme, barColorMap, dataW);
24260
24740
  }
24261
24741
  drawSlicesUpTo(Infinity);
24742
+ if (isPrice) {
24743
+ ctx.globalAlpha = this.modelAlpha;
24744
+ for (const m of scene.indicators.values()) {
24745
+ const off = scene.offsetOf(m.id);
24746
+ for (const s of m.series) if (s.overlay === true) this.drawSeries(ctx, s, pane, coords, i0, i1, theme, off);
24747
+ }
24748
+ }
24262
24749
  ctx.globalAlpha = this.modelAlpha;
24263
24750
  for (const m of models) {
24264
24751
  const mp = effPane(m);
@@ -24530,9 +25017,9 @@ var Canvas2dBackend = class {
24530
25017
  ctx.fillStyle = color;
24531
25018
  ctx.fillRect(g.bodyX, top, g.bodyW, bodyH);
24532
25019
  }
24533
- if (cs.borderVisible || bc || fading && cs.bodyVisible) {
25020
+ if (cs.borderVisible || fading && cs.bodyVisible) {
24534
25021
  ctx.globalAlpha = this.candleStructureAlpha;
24535
- ctx.strokeStyle = cs.borderVisible || bc ? (up ? cs.borderUpColor : cs.borderDownColor) ?? dir : color;
25022
+ ctx.strokeStyle = cs.borderVisible ? (up ? cs.borderUpColor : cs.borderDownColor) ?? color : color;
24536
25023
  ctx.lineWidth = 1;
24537
25024
  const bw = Math.max(0, g.bodyW - 1);
24538
25025
  const bh = Math.max(0, bodyH - 1);
@@ -24964,6 +25451,8 @@ var DrawingSceneRenderer = class {
24964
25451
  this.set = set;
24965
25452
  /** measureText width cache, keyed by `${font} ${text}`, persists across frames. */
24966
25453
  this.widthCache = /* @__PURE__ */ new Map();
25454
+ /** Tooltip hit-rects captured while drawing the CURRENT set (rebuilt per render). */
25455
+ this.tipRegions = [];
24967
25456
  /**
24968
25457
  * Index offset of the CURRENT set's model: its `xloc:'bar_index'` coordinates count
24969
25458
  * from the model's anchor bar, so they shift by this to land on chart logical indices.
@@ -24984,8 +25473,13 @@ var DrawingSceneRenderer = class {
24984
25473
  const s = this.set;
24985
25474
  return !s.lines.length && !s.boxes.length && !s.labels.length && !s.polylines.length && !s.linefills.length;
24986
25475
  }
25476
+ /** Tooltip hit-rects of the labels drawn by the LAST `render` call (same coords as `ctx`). */
25477
+ labelTipRegions() {
25478
+ return this.tipRegions;
25479
+ }
24987
25480
  /** Draw the whole set into `ctx` using the supplied coordinate closures. */
24988
25481
  render(ctx, W, H, xOf, yOf) {
25482
+ this.tipRegions = [];
24989
25483
  if (this.isEmpty()) return;
24990
25484
  ctx.save();
24991
25485
  this.drawLinefills(ctx, W, H, xOf, yOf);
@@ -25333,13 +25827,35 @@ var DrawingSceneRenderer = class {
25333
25827
  if (this.isPointShape(lb.style)) {
25334
25828
  if (!lb.noFill) this.drawLabelShape(ctx, lb.style, px, py, fontPx, color);
25335
25829
  if (lb.text) this.drawLabelText(ctx, lb, px, py + fontPx, fontPx);
25336
- } else if (lb.style === "none" || lb.style === "text_outline" || lb.noFill) {
25337
- if (lb.text) this.drawLabelText(ctx, lb, px, py, fontPx, lb.style === "text_outline");
25830
+ if (lb.tooltip) {
25831
+ const r = Math.max(4, fontPx * 0.6) + 3;
25832
+ this.tipRegions.push({ left: px - r, top: py - r, right: px + r, bottom: py + r, text: lb.tooltip });
25833
+ }
25834
+ } else if (lb.style === "none" || lb.style === "text_outline") {
25835
+ if (lb.text) {
25836
+ this.drawLabelText(ctx, lb, px, py, fontPx, lb.style === "text_outline");
25837
+ if (lb.tooltip) this.tipRegions.push(this.textRegion(ctx, lb, px, py, fontPx, lb.tooltip));
25838
+ }
25338
25839
  } else {
25339
- this.drawBubble(ctx, lb, px, py, fontPx, color);
25840
+ const r = this.drawBubble(ctx, lb, px, py, fontPx, color);
25841
+ 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 });
25340
25842
  }
25341
25843
  }
25342
25844
  }
25845
+ /** Canvas font for a label's text — family + Pine `text_formatting` (bold/italic). */
25846
+ labelFont(lb, fontPx) {
25847
+ const family = lb.fontFamily === "monospace" ? "monospace" : this.deps.theme.fontFamily || "sans-serif";
25848
+ return `${lb.italic ? "italic " : ""}${lb.bold ? "bold " : ""}${fontPx}px ${family}`;
25849
+ }
25850
+ /** Hover rect of a text-only label (style none/text_outline/noFill), centered like drawLabelText. */
25851
+ textRegion(ctx, lb, cx, cy, fontPx, text) {
25852
+ const font = this.labelFont(lb, fontPx);
25853
+ const lines = (lb.text ?? "").split("\n");
25854
+ const w = Math.max(1, ...lines.map((l) => this.measure(ctx, font, l)));
25855
+ const h = fontPx * 1.25 * lines.length;
25856
+ const left = lb.textAlign === "left" ? cx : lb.textAlign === "right" ? cx - w : cx - w / 2;
25857
+ return { left, top: cy - h / 2, right: left + w, bottom: cy + h / 2, text };
25858
+ }
25343
25859
  isPointShape(style) {
25344
25860
  switch (style) {
25345
25861
  case "circle":
@@ -25358,8 +25874,7 @@ var DrawingSceneRenderer = class {
25358
25874
  }
25359
25875
  }
25360
25876
  drawLabelText(ctx, lb, cx, cy, fontPx, outline = false) {
25361
- const family = lb.fontFamily === "monospace" ? "monospace" : this.deps.theme.fontFamily || "sans-serif";
25362
- ctx.font = `${fontPx}px ${family}`;
25877
+ ctx.font = this.labelFont(lb, fontPx);
25363
25878
  ctx.textAlign = lb.textAlign === "left" ? "left" : lb.textAlign === "right" ? "right" : "center";
25364
25879
  ctx.textBaseline = "middle";
25365
25880
  const lines = lb.text.split("\n");
@@ -25439,9 +25954,9 @@ var DrawingSceneRenderer = class {
25439
25954
  ctx.fill();
25440
25955
  }
25441
25956
  }
25957
+ /** Draw a bubble label; returns the bubble body rect (for tooltip hit-testing). */
25442
25958
  drawBubble(ctx, lb, px, py, fontPx, color) {
25443
- const family = lb.fontFamily === "monospace" ? "monospace" : this.deps.theme.fontFamily || "sans-serif";
25444
- ctx.font = `${fontPx}px ${family}`;
25959
+ ctx.font = this.labelFont(lb, fontPx);
25445
25960
  const lines = (lb.text ?? "").split("\n");
25446
25961
  const padX = 6;
25447
25962
  const padY = 4;
@@ -25499,38 +26014,51 @@ var DrawingSceneRenderer = class {
25499
26014
  by = py - h - ptr;
25500
26015
  pointer = "down";
25501
26016
  }
25502
- ctx.fillStyle = color;
25503
- this.roundRect(ctx, bx, by, w, h, 4);
25504
- ctx.fill();
25505
- if (pointer !== "none") {
25506
- ctx.beginPath();
25507
- if (pointer === "down") {
25508
- ctx.moveTo(px - ptr, by + h);
25509
- ctx.lineTo(px + ptr, by + h);
25510
- ctx.lineTo(px, py);
25511
- } else if (pointer === "up") {
25512
- ctx.moveTo(px - ptr, by);
25513
- ctx.lineTo(px + ptr, by);
25514
- ctx.lineTo(px, py);
25515
- } else if (pointer === "left") {
25516
- ctx.moveTo(bx, py - ptr);
25517
- ctx.lineTo(bx, py + ptr);
25518
- ctx.lineTo(px, py);
25519
- } else {
25520
- ctx.moveTo(bx + w, py - ptr);
25521
- ctx.lineTo(bx + w, py + ptr);
25522
- ctx.lineTo(px, py);
25523
- }
25524
- ctx.closePath();
26017
+ if (!lb.noFill) {
26018
+ ctx.fillStyle = color;
26019
+ this.roundRect(ctx, bx, by, w, h, 4);
25525
26020
  ctx.fill();
26021
+ if (pointer !== "none") {
26022
+ ctx.beginPath();
26023
+ if (pointer === "down") {
26024
+ ctx.moveTo(px - ptr, by + h);
26025
+ ctx.lineTo(px + ptr, by + h);
26026
+ ctx.lineTo(px, py);
26027
+ } else if (pointer === "up") {
26028
+ ctx.moveTo(px - ptr, by);
26029
+ ctx.lineTo(px + ptr, by);
26030
+ ctx.lineTo(px, py);
26031
+ } else if (pointer === "left") {
26032
+ ctx.moveTo(bx, py - ptr);
26033
+ ctx.lineTo(bx, py + ptr);
26034
+ ctx.lineTo(px, py);
26035
+ } else {
26036
+ ctx.moveTo(bx + w, py - ptr);
26037
+ ctx.lineTo(bx + w, py + ptr);
26038
+ ctx.lineTo(px, py);
26039
+ }
26040
+ ctx.closePath();
26041
+ ctx.fill();
26042
+ }
25526
26043
  }
25527
26044
  if (lb.text) {
25528
- ctx.fillStyle = lb.textColor ?? contrastColor(color);
25529
- ctx.textAlign = "center";
26045
+ ctx.fillStyle = lb.textColor ?? (lb.noFill ? this.deps.theme.textColor : contrastColor(color));
25530
26046
  ctx.textBaseline = "middle";
26047
+ let tx;
26048
+ if (lb.textAlign === "left") {
26049
+ ctx.textAlign = "left";
26050
+ tx = bx + padX;
26051
+ } else if (lb.textAlign === "right") {
26052
+ ctx.textAlign = "right";
26053
+ tx = bx + w - padX;
26054
+ } else {
26055
+ ctx.textAlign = "center";
26056
+ tx = bx + w / 2;
26057
+ }
25531
26058
  const startY = by + padY + lineH / 2;
25532
- for (let i = 0; i < lines.length; i += 1) ctx.fillText(lines[i], bx + w / 2, startY + i * lineH);
26059
+ for (let i = 0; i < lines.length; i += 1) ctx.fillText(lines[i], tx, startY + i * lineH);
25533
26060
  }
26061
+ return { x: bx, y: by, w, h };
25534
26062
  }
25535
26063
  roundRect(ctx, x, y, w, h, r) {
25536
26064
  const rr = Math.min(r, w / 2, h / 2);
@@ -25768,6 +26296,8 @@ var ChromeRenderer = class {
25768
26296
  this.axisTextColor = DARK_THEME.textColor;
25769
26297
  // Shared Pine-drawing renderer (line/box/label/polyline/linefill); widthCache persists.
25770
26298
  this.drawScene = new DrawingSceneRenderer({ timeToLogical: () => 0, barAt: () => null, theme: {} });
26299
+ // Tooltip hit-rects of every label drawn this frame, in plot coords (rebuilt per render).
26300
+ this.labelTips = [];
25771
26301
  }
25772
26302
  mount(canvas) {
25773
26303
  this.canvas = canvas;
@@ -25809,6 +26339,7 @@ var ChromeRenderer = class {
25809
26339
  const dataW = coords.width;
25810
26340
  const dataH = coords.height;
25811
26341
  this.axisTextColor = surface?.textColor ?? theme.textColor;
26342
+ this.labelTips = [];
25812
26343
  ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
25813
26344
  ctx.clearRect(0, 0, fullW, fullH);
25814
26345
  if (surface && (fullW > dataW || fullH > dataH)) {
@@ -25918,6 +26449,17 @@ var ChromeRenderer = class {
25918
26449
  (price) => coords.priceToY(price, pane.scale, pane.bounds) - pane.bounds.top
25919
26450
  );
25920
26451
  ctx.restore();
26452
+ for (const r of this.drawScene.labelTipRegions()) {
26453
+ this.labelTips.push({ ...r, top: r.top + pane.bounds.top, bottom: r.bottom + pane.bounds.top });
26454
+ }
26455
+ }
26456
+ /** Tooltip of the topmost label under a plot-space point, or null. Fed by the last render. */
26457
+ labelTooltipAt(x, y) {
26458
+ for (let i = this.labelTips.length - 1; i >= 0; i -= 1) {
26459
+ const r = this.labelTips[i];
26460
+ if (x >= r.left && x <= r.right && y >= r.top && y <= r.bottom) return r.text;
26461
+ }
26462
+ return null;
25921
26463
  }
25922
26464
  // ── axes ──
25923
26465
  drawPriceAxes(ctx, scene, coords, theme, dataW, panes) {
@@ -26134,6 +26676,68 @@ function formatCountdown(ms) {
26134
26676
  return h > 0 ? `${h}:${pad(m)}:${pad(s)}` : `${pad(m)}:${pad(s)}`;
26135
26677
  }
26136
26678
 
26679
+ // src/renderers/native/chrome/LabelTooltip.ts
26680
+ var HOVER_DELAY_MS = 350;
26681
+ var LabelTooltip = class {
26682
+ constructor(plot, deps) {
26683
+ this.plot = plot;
26684
+ this.deps = deps;
26685
+ this.tip = null;
26686
+ this.timer = null;
26687
+ /** Text of the currently open OR armed tip — dedupes moves inside one label. */
26688
+ this.current = null;
26689
+ this.onMove = (e) => {
26690
+ if (e.pointerType !== "mouse") return;
26691
+ const rect = this.plot.getBoundingClientRect();
26692
+ const x = e.clientX - rect.left;
26693
+ const y = e.clientY - rect.top;
26694
+ const text = this.deps.lookup(x, y);
26695
+ if (!text) {
26696
+ this.clear();
26697
+ return;
26698
+ }
26699
+ if (text === this.current) return;
26700
+ this.clear();
26701
+ this.current = text;
26702
+ this.timer = window.setTimeout(() => this.show(text, x, y), HOVER_DELAY_MS);
26703
+ };
26704
+ this.onLeave = () => {
26705
+ this.clear();
26706
+ };
26707
+ plot.addEventListener("pointermove", this.onMove);
26708
+ plot.addEventListener("pointerleave", this.onLeave);
26709
+ plot.addEventListener("pointerdown", this.onLeave);
26710
+ }
26711
+ destroy() {
26712
+ this.plot.removeEventListener("pointermove", this.onMove);
26713
+ this.plot.removeEventListener("pointerleave", this.onLeave);
26714
+ this.plot.removeEventListener("pointerdown", this.onLeave);
26715
+ this.clear();
26716
+ }
26717
+ show(text, x, y) {
26718
+ const doc = this.plot.ownerDocument;
26719
+ const tip = doc.createElement("div");
26720
+ tip.textContent = text;
26721
+ 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;";
26722
+ applyChromeTokens(tip, this.deps.theme());
26723
+ this.plot.appendChild(tip);
26724
+ const pw = this.plot.clientWidth;
26725
+ const ph = this.plot.clientHeight;
26726
+ tip.style.left = `${Math.max(0, Math.min(x + 12, pw - tip.offsetWidth - 4))}px`;
26727
+ tip.style.top = `${Math.max(0, Math.min(y + 16, ph - tip.offsetHeight - 4))}px`;
26728
+ this.tip = tip;
26729
+ }
26730
+ clear() {
26731
+ if (this.timer !== null) {
26732
+ clearTimeout(this.timer);
26733
+ this.timer = null;
26734
+ }
26735
+ this.tip?.remove();
26736
+ this.tip = null;
26737
+ this.current = null;
26738
+ }
26739
+ };
26740
+
26137
26741
  // src/renderers/native/chrome/CrosshairRenderer.ts
26138
26742
  var CrosshairRenderer = class {
26139
26743
  constructor() {
@@ -31211,19 +31815,8 @@ function computePaneScale(models, bars, includeCandles, i0, i1, drawings, log =
31211
31815
  for (const model of models) {
31212
31816
  const off = offsetOf(model.id);
31213
31817
  for (const s of model.series) {
31214
- if (s.kind === "candle" || s.kind === "bar") {
31215
- for (let i = i0; i <= i1; i += 1) {
31216
- const b = s.bars[i - off];
31217
- if (b) {
31218
- consider(b.high);
31219
- consider(b.low);
31220
- }
31221
- }
31222
- } else if (isLineLikeSeries(s)) {
31223
- for (let i = i0; i <= i1; i += 1) consider(s.points[i - off]?.value);
31224
- if (s.kind === "histogram" || s.kind === "columns") consider(s.style?.base ?? 0);
31225
- else if (s.style?.base != null) consider(s.style.base);
31226
- }
31818
+ if (s.overlay === true) continue;
31819
+ considerSeries(s, i0, i1, off, consider);
31227
31820
  }
31228
31821
  for (const pl of model.priceLines) consider(pl.price);
31229
31822
  }
@@ -31245,6 +31838,36 @@ function computePaneScale(models, bars, includeCandles, i0, i1, drawings, log =
31245
31838
  const span = max - min;
31246
31839
  return { min: min - span * MARGIN_BOTTOM, max: max + span * MARGIN_TOP };
31247
31840
  }
31841
+ function considerSeries(s, i0, i1, off, consider) {
31842
+ if (s.kind === "candle" || s.kind === "bar") {
31843
+ for (let i = i0; i <= i1; i += 1) {
31844
+ const b = s.bars[i - off];
31845
+ if (b) {
31846
+ consider(b.high);
31847
+ consider(b.low);
31848
+ }
31849
+ }
31850
+ } else if (isLineLikeSeries(s)) {
31851
+ for (let i = i0; i <= i1; i += 1) consider(s.points[i - off]?.value);
31852
+ if (s.kind === "histogram" || s.kind === "columns") consider(s.style?.base ?? 0);
31853
+ else if (s.style?.base != null) consider(s.style.base);
31854
+ }
31855
+ }
31856
+ function overlaySeriesRange(models, i0, i1, offsetOf = () => 0) {
31857
+ let min = Infinity;
31858
+ let max = -Infinity;
31859
+ const consider = (v) => {
31860
+ if (v != null && Number.isFinite(v)) {
31861
+ if (v < min) min = v;
31862
+ if (v > max) max = v;
31863
+ }
31864
+ };
31865
+ for (const model of models) {
31866
+ const off = offsetOf(model.id);
31867
+ for (const s of model.series) if (s.overlay === true) considerSeries(s, i0, i1, off, consider);
31868
+ }
31869
+ return min === Infinity ? null : { min, max };
31870
+ }
31248
31871
  function expandScaleByPixels(scale, heightPx, abovePx, belowPx) {
31249
31872
  if (abovePx <= 0 && belowPx <= 0) return scale;
31250
31873
  const content = heightPx - abovePx - belowPx;
@@ -31299,6 +31922,11 @@ function resizeSplit(split, dyTotal, minPx = MIN_PANE_PX) {
31299
31922
  }
31300
31923
 
31301
31924
  // src/renderers/native/backdrop/BackdropRenderer.ts
31925
+ function clipHighlightRect(x1, x2, left, right) {
31926
+ const x = Math.max(left, x1);
31927
+ const end = Math.min(right, x2);
31928
+ return end > x ? { x, width: end - x } : null;
31929
+ }
31302
31930
  var BackdropRenderer = class {
31303
31931
  constructor() {
31304
31932
  this.canvas = null;
@@ -31331,17 +31959,22 @@ var BackdropRenderer = class {
31331
31959
  /** Renderer-owned session highlight bands: full-height (all panes), behind the grid.
31332
31960
  * Session-zone washes (pre/post-market) paint first, host highlights on top. */
31333
31961
  drawHighlights(ctx, scene, coords) {
31334
- const bands = [...scene.sessionHighlightBands(), ...scene.highlights];
31335
- if (bands.length === 0) return;
31962
+ const sessions = scene.sessionHighlightBands();
31963
+ if (sessions.length > 0) {
31964
+ const left = Math.max(0, coords.logicalToX(-0.5));
31965
+ const right = Math.min(coords.width, coords.logicalToX(coords.barCount - 0.5));
31966
+ this.drawHighlightSet(ctx, sessions, coords, left, right);
31967
+ }
31968
+ this.drawHighlightSet(ctx, scene.highlights, coords, 0, coords.width);
31969
+ }
31970
+ drawHighlightSet(ctx, bands, coords, left, right) {
31336
31971
  for (const band of bands) {
31337
31972
  const x1 = coords.timeToX(band.from);
31338
31973
  const x2 = coords.timeToX(band.to);
31339
- if (x2 < 0 || x1 > coords.width || x2 <= x1) continue;
31340
- const cx = Math.max(0, x1);
31341
- const cw = Math.min(coords.width, x2) - cx;
31342
- if (cw <= 0) continue;
31974
+ const rect = clipHighlightRect(x1, x2, left, right);
31975
+ if (!rect) continue;
31343
31976
  ctx.fillStyle = band.color;
31344
- ctx.fillRect(cx, 0, cw, coords.height);
31977
+ ctx.fillRect(rect.x, 0, rect.width, coords.height);
31345
31978
  }
31346
31979
  }
31347
31980
  // ── grid ── vert/horz gate on `scene.showGrid` AND their own per-axis visibility
@@ -31883,6 +32516,8 @@ var NativeRenderer = class {
31883
32516
  this.glowAmount = 0;
31884
32517
  // WebGL2 neon-glow intensity (canvas2d ignores it)
31885
32518
  this.chrome = new ChromeRenderer();
32519
+ /** Hover tooltips for Pine labels (canvas hit-rects collected by the chrome layer). */
32520
+ this.labelTooltip = null;
31886
32521
  this.crosshairLayer = new CrosshairRenderer();
31887
32522
  /** 1 Hz repaint pump so the price-axis countdown-to-bar-close ticks; null when off. */
31888
32523
  this.countdownTimer = null;
@@ -31893,6 +32528,8 @@ var NativeRenderer = class {
31893
32528
  this.indicatorValuesOn = true;
31894
32529
  /** Host-contributed legend actions — held here so a rebuild of the legend re-wires them. */
31895
32530
  this.legendActionsProvider = null;
32531
+ /** Host-contributed legend callouts — held here so a rebuild of the legend re-wires them. */
32532
+ this.legendCalloutsProvider = null;
31896
32533
  /** Host override of the legend's fold toggle — held here so a remount re-applies it. */
31897
32534
  this.legendOverviewAction = null;
31898
32535
  // ── keyboard navigation / accessibility (item 11) ──
@@ -33001,6 +33638,10 @@ var NativeRenderer = class {
33001
33638
  this.plot.appendChild(this.scrollButton);
33002
33639
  this.plot.addEventListener("pointermove", this.onScrollProximityMove);
33003
33640
  this.plot.addEventListener("pointerleave", this.onScrollProximityLeave);
33641
+ this.labelTooltip = new LabelTooltip(this.plot, {
33642
+ theme: () => this.chromeTheme(),
33643
+ lookup: (x, y) => this.chrome.labelTooltipAt(x, y)
33644
+ });
33004
33645
  this.userDrawings = new UserDrawingController(this.wrapper, this.plot, this.drawingsCanvas, {
33005
33646
  projector: () => this.drawingProjector(),
33006
33647
  dpr: () => this.coords.dpr,
@@ -33040,9 +33681,10 @@ var NativeRenderer = class {
33040
33681
  this.inputsUI.setDialogHost(this.dialogHost);
33041
33682
  this.inputsUI.setSymbolPicker(this.symbolPicker);
33042
33683
  this.inputsUI.setLegendActions(this.legendActionsProvider);
33684
+ this.inputsUI.setLegendCallouts(this.legendCalloutsProvider);
33043
33685
  this.inputsUI.setLegendOverviewAction(this.legendOverviewAction);
33044
33686
  this.inputsUI.setOnChange((c) => {
33045
- for (const cb of this.inputChangeCbs) cb({ indicatorId: c.indicatorId, key: c.key, value: c.value });
33687
+ for (const cb of this.inputChangeCbs) cb({ indicatorId: c.indicatorId, key: c.key, value: c.value, ...c.kind ? { kind: c.kind } : {} });
33046
33688
  });
33047
33689
  this.inputsUI.setOnRemove((id) => {
33048
33690
  for (const cb of this.removeIndicatorCbs) cb(id);
@@ -33232,6 +33874,8 @@ var NativeRenderer = class {
33232
33874
  this.settingsButton = null;
33233
33875
  this.plot?.removeEventListener("pointermove", this.onScrollProximityMove);
33234
33876
  this.plot?.removeEventListener("pointerleave", this.onScrollProximityLeave);
33877
+ this.labelTooltip?.destroy();
33878
+ this.labelTooltip = null;
33235
33879
  this.scrollButton?.remove();
33236
33880
  this.scrollButton = null;
33237
33881
  for (const l of this.extLayers) l.instance.destroy?.();
@@ -33472,7 +34116,7 @@ var NativeRenderer = class {
33472
34116
  else this.scene.assignIndicatorZ(model.id);
33473
34117
  this.inputsUI.upsert(model.id, model.shorttitle ?? model.title, model.inputs, model.inputValues, model.paneId, {
33474
34118
  native: !!model.native,
33475
- ...model.shorttitle ? { settingsTitle: model.title } : {}
34119
+ ...model.props ? { props: model.props, propValues: model.propValues ?? {} } : {}
33476
34120
  });
33477
34121
  this.syncTables(model);
33478
34122
  if (model.native?.type === "volume") {
@@ -33523,8 +34167,8 @@ var NativeRenderer = class {
33523
34167
  this.paneControls?.refresh();
33524
34168
  this.scheduler.invalidate(4 /* Full */);
33525
34169
  }
33526
- setIndicatorInputs(handle, values) {
33527
- this.inputsUI.setValues(handle.id, values);
34170
+ setIndicatorInputs(handle, values, props) {
34171
+ this.inputsUI.setValues(handle.id, values, props);
33528
34172
  }
33529
34173
  setSymbolPicker(picker) {
33530
34174
  this.symbolPicker = picker;
@@ -33534,6 +34178,10 @@ var NativeRenderer = class {
33534
34178
  this.legendActionsProvider = provider;
33535
34179
  this.inputsUI?.setLegendActions(provider);
33536
34180
  }
34181
+ setLegendCallouts(provider) {
34182
+ this.legendCalloutsProvider = provider;
34183
+ this.inputsUI?.setLegendCallouts(provider);
34184
+ }
33537
34185
  setLegendOverviewAction(action) {
33538
34186
  this.legendOverviewAction = action;
33539
34187
  this.inputsUI?.setLegendOverviewAction(action);
@@ -34514,7 +35162,11 @@ var NativeRenderer = class {
34514
35162
  }
34515
35163
  const models = this.scene.indicatorsForPane(pane.id);
34516
35164
  const masterModels = models.filter((m) => m.ownScale !== true);
34517
- const dr = this.chrome.paneDrawingsRange(masterModels, this.scene, pane === pricePane, vr);
35165
+ let dr = this.chrome.paneDrawingsRange(masterModels, this.scene, pane === pricePane, vr);
35166
+ if (pane === pricePane) {
35167
+ const or = overlaySeriesRange(this.scene.indicators.values(), i0, i1, (id) => this.scene.offsetOf(id));
35168
+ if (or) dr = dr ? { min: Math.min(dr.min, or.min), max: Math.max(dr.max, or.max) } : or;
35169
+ }
34518
35170
  const includeCandles = pane.kind === "price" && !this.scene.candlesHidden;
34519
35171
  pane.scaleTarget = computePaneScale(masterModels, this.bars, includeCandles, i0, i1, dr, paneLogScale(this.scene, pane), (id) => this.scene.offsetOf(id));
34520
35172
  pane.percentBaseline = pane.kind === "price" ? this.bars[i0]?.close ?? 0 : this.firstVisibleValue(masterModels, i0);
@@ -34788,6 +35440,7 @@ var NativeRenderer = class {
34788
35440
  for (const m of models) {
34789
35441
  const off = this.scene.offsetOf(m.id);
34790
35442
  for (const s of m.series) {
35443
+ if (s.overlay === true) continue;
34791
35444
  if (isLineLikeSeries(s)) {
34792
35445
  const v = s.points[i0 - off]?.value;
34793
35446
  if (v != null && Number.isFinite(v)) return v;
@@ -35660,45 +36313,24 @@ var CSS21 = `
35660
36313
  }
35661
36314
  .vela-statusline .vela-sl-symbol { font-weight: 600; font-size: var(--vela-font-size-lg); }
35662
36315
  .vela-statusline .vela-sl-meta { color: var(--vela-fg-muted); font-size: var(--vela-font-size-md); font-weight: 600; }
35663
- /* Market status badge \u2014 icon-only 16px circle, label on hover (kit tooltip). */
35664
- .vela-statusline .vela-sl-market {
35665
- display: inline-grid;
35666
- place-items: center;
35667
- width: 16px;
35668
- height: 16px;
35669
- border-radius: 50%;
35670
- flex: none;
35671
- align-self: center;
35672
- line-height: 0;
35673
- cursor: default;
35674
- }
35675
- .vela-statusline .vela-sl-market svg {
35676
- width: 12px;
35677
- height: 12px;
35678
- display: block;
35679
- }
35680
- /* Open wears the theme's up color; the other sessions are meaning constants from the
35681
- * palette (amber pre, sky post, gray closed/holiday). Same ink at 20% for the circle. */
35682
- .vela-statusline .vela-sl-market[data-status='open'] {
35683
- background: color-mix(in srgb, var(--vela-up) 20%, transparent);
35684
- color: var(--vela-up);
35685
- }
35686
- .vela-statusline .vela-sl-market[data-status='pre'] { background: color-mix(in srgb, ${SESSION_PRE} 20%, transparent); color: ${SESSION_PRE}; }
35687
- .vela-statusline .vela-sl-market[data-status='post'] { background: color-mix(in srgb, ${SESSION_POST} 20%, transparent); color: ${SESSION_POST}; }
35688
- .vela-statusline .vela-sl-market[data-status='closed'],
35689
- .vela-statusline .vela-sl-market[data-status='holiday'] { background: color-mix(in srgb, ${SESSION_OFF} 20%, transparent); color: ${SESSION_OFF}; }
36316
+ /* Market status badge \u2014 a kit callout bubble (icon-only 16px circle, label on hover
36317
+ * via the kit tooltip); the session tint is applied per status in setMarketStatus. */
36318
+ .vela-statusline .vela-sl-market { align-self: center; }
35690
36319
  .vela-statusline .vela-sl-ohlc { display: flex; gap: var(--vela-space-1); color: var(--vela-fg-muted); }
35691
36320
  .vela-statusline .vela-sl-ohlc b { color: var(--vela-fg); font-weight: 500; }
35692
36321
  /* The change value wears the SAME ink as the OHLC values (set inline per render) \u2014
35693
36322
  * these are the pre-ink fallbacks only. */
35694
36323
  .vela-statusline .vela-sl-change[data-dir='up'] { color: var(--vela-up); }
35695
36324
  .vela-statusline .vela-sl-change[data-dir='down'] { color: var(--vela-down); }
35696
- /* Stack the renderer's PRICE-pane legend below the status line (study panes stay put).
35697
- * The renderer sets the legend's inline top \u2014 shift with a transform, don't fight it.
35698
- * Scoped to hosts that actually CARRY a status line (the marker class set by the
35699
- * Statusline constructor) \u2014 the stylesheet is document-global, so a bare container
35700
- * class here would shift every chart on the page, including statusline-less ones. */
35701
- .vela-has-statusline [data-vela-pane='price'] { transform: translateY(26px); }
36325
+ /* Stack the TOP pane's legend below the status line (lower study panes stay put).
36326
+ * The renderer marks whichever legend sits at the plot's top edge \u2014 the price pane
36327
+ * normally, or a maximized study pane filling the plot \u2014 so the legend never merges
36328
+ * with the status line whichever pane owns the top. The renderer sets the legend's
36329
+ * inline top \u2014 shift with a transform, don't fight it. Scoped to hosts that actually
36330
+ * CARRY a status line (the marker class set by the Statusline constructor) \u2014 the
36331
+ * stylesheet is document-global, so a bare attribute selector here would shift every
36332
+ * chart on the page, including statusline-less ones. */
36333
+ .vela-has-statusline [${LEGEND_AT_TOP_ATTR}] { transform: translateY(26px); }
35702
36334
  /* Mobile: two-line chip \u2014 logo / symbol / meta / market status on one aligned row, the
35703
36335
  * bar change on the next. Full O/H/L/C stays hidden (too dense on a phone-width plot).
35704
36336
  * GRID, not a wrapping flexbox: an absolutely positioned wrapping flex container sizes
@@ -35734,7 +36366,7 @@ var CSS21 = `
35734
36366
  font-size: var(--vela-font-size-sm);
35735
36367
  line-height: 1.2;
35736
36368
  }
35737
- [data-layout='mobile'] .vela-has-statusline [data-vela-pane='price'] { transform: translateY(40px); }
36369
+ [data-layout='mobile'] .vela-has-statusline [${LEGEND_AT_TOP_ATTR}] { transform: translateY(40px); }
35738
36370
  /* FIT mode (multi-chart cells \u2014 see setFitMode): the line never wraps; segments that
35739
36371
  * don't fit are HIDDEN by fit() (change first, then meta, then the market badge), so
35740
36372
  * overflow:hidden only guards the transient between a resize and the next measure.
@@ -35751,7 +36383,7 @@ var CSS21 = `
35751
36383
  padding-left: 0;
35752
36384
  }
35753
36385
  /* One row again \u2014 the mobile two-line shift doesn't apply in fit mode. */
35754
- [data-layout='mobile'] .vela-sl-fit-host.vela-has-statusline [data-vela-pane='price'] { transform: translateY(26px); }
36386
+ [data-layout='mobile'] .vela-sl-fit-host.vela-has-statusline [${LEGEND_AT_TOP_ATTR}] { transform: translateY(26px); }
35755
36387
  `;
35756
36388
  function baseOfTicker(ticker) {
35757
36389
  return ticker.replace(/[-_/]?(USDT|USDC|USD1|USDS|BUSD|USD|EUR|PERP)$/i, "") || ticker;
@@ -35763,6 +36395,13 @@ var MARKET_LABELS = {
35763
36395
  closed: "Market Closed",
35764
36396
  holiday: "Market Holiday"
35765
36397
  };
36398
+ var MARKET_INKS = {
36399
+ open: "var(--vela-up)",
36400
+ pre: SESSION_PRE,
36401
+ post: SESSION_POST,
36402
+ closed: SESSION_OFF,
36403
+ holiday: SESSION_OFF
36404
+ };
35766
36405
  function statuslineInkOf(renderer, priceStyle) {
35767
36406
  const cfg = renderer.getConfig();
35768
36407
  const c = (v) => typeof v === "string" ? v : null;
@@ -35821,8 +36460,15 @@ var Statusline = class {
35821
36460
  this.symbolEl.textContent = ticker;
35822
36461
  this.metaEl = doc.createElement("span");
35823
36462
  this.metaEl.className = "vela-sl-meta";
35824
- this.marketEl = doc.createElement("span");
35825
- this.marketEl.className = "vela-sl-market";
36463
+ this.marketBubble = new CalloutBubble({
36464
+ icon: "market-open",
36465
+ background: `color-mix(in srgb, ${MARKET_INKS.open} 20%, transparent)`,
36466
+ color: MARKET_INKS.open,
36467
+ label: MARKET_LABELS.open,
36468
+ host
36469
+ });
36470
+ this.marketEl = this.marketBubble.el;
36471
+ this.marketEl.classList.add("vela-sl-market");
35826
36472
  this.ohlcEl = doc.createElement("span");
35827
36473
  this.ohlcEl.className = "vela-sl-ohlc";
35828
36474
  this.changeEl = doc.createElement("span");
@@ -35910,8 +36556,13 @@ var Statusline = class {
35910
36556
  * hover label. Callers with no session model leave the constructor's 'open'. */
35911
36557
  setMarketStatus(status) {
35912
36558
  this.marketEl.dataset.status = status;
35913
- this.marketEl.innerHTML = icon(`market-${status}`);
35914
- this.marketEl.setAttribute("aria-label", MARKET_LABELS[status]);
36559
+ const ink = MARKET_INKS[status];
36560
+ this.marketBubble.set({
36561
+ icon: `market-${status}`,
36562
+ background: `color-mix(in srgb, ${ink} 20%, transparent)`,
36563
+ color: ink,
36564
+ label: MARKET_LABELS[status]
36565
+ });
35915
36566
  this.marketTip.setContent(MARKET_LABELS[status]);
35916
36567
  }
35917
36568
  setPartVisible(part, visible) {
@@ -35944,6 +36595,7 @@ var Statusline = class {
35944
36595
  this.fitRO?.disconnect();
35945
36596
  this.fitRO = null;
35946
36597
  this.marketTip.destroy();
36598
+ this.marketBubble.destroy();
35947
36599
  this.host.classList.remove("vela-has-statusline", "vela-sl-fit-host");
35948
36600
  this.el.remove();
35949
36601
  }
@@ -36083,61 +36735,148 @@ var MarketStatusTracker = class {
36083
36735
  };
36084
36736
 
36085
36737
  // src/widget/session-shading.ts
36086
- function deriveSessionZones(regular, extended) {
36738
+ var DAY_MS2 = 864e5;
36739
+ var MINUTE_MS = 6e4;
36740
+ function parseWindow(text) {
36741
+ if (typeof text !== "string") return null;
36742
+ const m = /^(\d{2})(\d{2})-(\d{2})(\d{2})$/.exec(text);
36743
+ if (!m) return null;
36744
+ const start = Number(m[1]) * 60 + Number(m[2]);
36745
+ const end = Number(m[3]) * 60 + Number(m[4]);
36746
+ if (start >= end || end > 1440) return null;
36747
+ return { start, end };
36748
+ }
36749
+ function parseSessionSpec(si) {
36750
+ const session = si?.["session"];
36751
+ if (typeof session !== "string" || session === "" || session === "24x7") return null;
36752
+ const regular = parseWindow(session);
36753
+ if (!regular) return null;
36754
+ const tz = si?.["timezone"];
36755
+ const timezone = typeof tz === "string" && tz !== "" ? tz : "Etc/UTC";
36756
+ const ext = parseWindow(si?.["session_extended"]);
36757
+ const extended = ext && ext.start <= regular.start && ext.end >= regular.end ? ext : { start: 0, end: 1440 };
36758
+ return { regular, extended, timezone };
36759
+ }
36760
+ var dtfCache = /* @__PURE__ */ new Map();
36761
+ function civilFormatter(tz) {
36762
+ const cached = dtfCache.get(tz);
36763
+ if (cached !== void 0) return cached;
36764
+ let dtf = null;
36765
+ try {
36766
+ dtf = new Intl.DateTimeFormat("en-US", {
36767
+ timeZone: tz,
36768
+ hourCycle: "h23",
36769
+ weekday: "short",
36770
+ year: "numeric",
36771
+ month: "2-digit",
36772
+ day: "2-digit",
36773
+ hour: "2-digit",
36774
+ minute: "2-digit"
36775
+ });
36776
+ } catch {
36777
+ dtf = null;
36778
+ }
36779
+ dtfCache.set(tz, dtf);
36780
+ return dtf;
36781
+ }
36782
+ function civilParts(dtf, ms) {
36783
+ const parts = dtf.formatToParts(ms);
36784
+ const get = (t) => parts.find((p) => p.type === t)?.value ?? "";
36785
+ return {
36786
+ weekday: get("weekday"),
36787
+ year: Number(get("year")),
36788
+ month: Number(get("month")),
36789
+ day: Number(get("day")),
36790
+ hour: Number(get("hour")) % 24,
36791
+ minute: Number(get("minute"))
36792
+ };
36793
+ }
36794
+ function expandSessionZones(spec, from, to) {
36087
36795
  const pre = [];
36088
36796
  const post = [];
36089
- for (const [extStart, extEnd] of extended) {
36090
- const inside = regular.filter(([s, e]) => e > extStart && s < extEnd).sort((a, b) => a[0] - b[0]);
36091
- let cursor = extStart;
36092
- for (const [regStart, regEnd] of inside) {
36093
- if (regStart > cursor) pre.push([cursor, Math.min(regStart, extEnd)]);
36094
- cursor = Math.max(cursor, regEnd);
36095
- }
36096
- if (cursor < extEnd) post.push([cursor, extEnd]);
36797
+ const dtf = civilFormatter(spec.timezone);
36798
+ if (!dtf || !Number.isFinite(from) || !Number.isFinite(to)) return { pre, post };
36799
+ const seen = /* @__PURE__ */ new Set();
36800
+ for (let cursor = from - DAY_MS2; cursor < to + DAY_MS2; cursor += DAY_MS2) {
36801
+ const civil = civilParts(dtf, cursor);
36802
+ const key = civil.year * 1e4 + civil.month * 100 + civil.day;
36803
+ if (!Number.isFinite(key) || seen.has(key)) continue;
36804
+ seen.add(key);
36805
+ if (civil.weekday === "Sat" || civil.weekday === "Sun") continue;
36806
+ const naive = Date.UTC(civil.year, civil.month - 1, civil.day);
36807
+ const noonGuess = naive + 720 * MINUTE_MS;
36808
+ const atNoon = civilParts(dtf, noonGuess);
36809
+ const offset = Date.UTC(atNoon.year, atNoon.month - 1, atNoon.day, atNoon.hour, atNoon.minute) - noonGuess;
36810
+ const at = (minutes) => naive + minutes * MINUTE_MS - offset;
36811
+ if (spec.extended.start < spec.regular.start) pre.push([at(spec.extended.start), at(spec.regular.start)]);
36812
+ if (spec.regular.end < spec.extended.end) post.push([at(spec.regular.end), at(spec.extended.end)]);
36097
36813
  }
36098
36814
  return { pre, post };
36099
36815
  }
36100
- var FETCH_AHEAD_MS2 = 10 * 864e5;
36101
- var MIN_LOOKBACK_MS = 3 * 864e5;
36102
- var MAX_LOOKBACK_MS = 120 * 864e5;
36816
+ var COVER_PAD_MIN_MS = 2 * DAY_MS2;
36103
36817
  var SessionShadingTracker = class {
36104
36818
  constructor(onZones) {
36105
36819
  this.onZones = onZones;
36106
- /** Invalidates detached async work — bumped by every track()/stop(). */
36820
+ /** Invalidates the one async step (metadata resolution) — bumped by track()/stop(). */
36107
36821
  this.epoch = 0;
36108
- }
36109
- /** (Re)bind to a chart's data surface + market and evaluate once. */
36822
+ this.spec = null;
36823
+ this.ready = false;
36824
+ this.session = "regular";
36825
+ /** Whether one bar is shorter than a civil day — only then do pre/post bars exist. */
36826
+ this.intraday = false;
36827
+ this.covered = null;
36828
+ /** The newest range seen — viewport moves during metadata resolution (a load's fit
36829
+ * animation) must not be lost, so the resolution always expands the LATEST range. */
36830
+ this.lastRange = null;
36831
+ }
36832
+ /** (Re)bind to a chart's data surface + market and expand once metadata lands. */
36110
36833
  track(data, symbol, opts) {
36111
36834
  const my = ++this.epoch;
36112
- void this.evaluate(my, data, symbol, opts);
36835
+ this.ready = false;
36836
+ this.spec = null;
36837
+ this.covered = null;
36838
+ this.session = opts.session;
36839
+ const tfMs = timeframeMs(opts.timeframe);
36840
+ this.intraday = Number.isFinite(tfMs) && tfMs < DAY_MS2;
36841
+ this.lastRange = opts.range;
36842
+ void data.symbolInfo(symbol).catch(() => void 0).then((si) => {
36843
+ if (my !== this.epoch) return;
36844
+ this.ready = true;
36845
+ this.spec = parseSessionSpec(si);
36846
+ this.emit(this.lastRange ?? opts.range, true);
36847
+ });
36848
+ }
36849
+ /** Follow a pan/zoom synchronously: bands are epoch-anchored, so only a range that
36850
+ * leaves the last expansion's coverage needs a recompute — no fetch, no debounce. */
36851
+ updateRange(range) {
36852
+ this.lastRange = range;
36853
+ if (!this.ready) return;
36854
+ this.emit(range, false);
36113
36855
  }
36114
36856
  stop() {
36115
36857
  this.epoch += 1;
36116
- }
36117
- async evaluate(my, data, symbol, opts) {
36118
- const resolved = data.resolve(symbol);
36119
- const provider = resolved ? data.providerInstance(resolved.provider) : void 0;
36120
- const si = await data.symbolInfo(symbol).catch(() => void 0);
36121
- if (my !== this.epoch) return;
36122
- const hasSessions = typeof si?.session === "string" && si.session !== "" && si.session !== "24x7";
36123
- if (!provider?.getCalendar || !hasSessions || !resolved) {
36124
- this.onZones(null);
36858
+ this.ready = false;
36859
+ this.spec = null;
36860
+ this.covered = null;
36861
+ this.lastRange = null;
36862
+ }
36863
+ emit(range, force) {
36864
+ if (!this.spec) {
36865
+ if (force) this.onZones(null);
36125
36866
  return;
36126
36867
  }
36127
- if (opts.session !== "extended") {
36128
- this.onZones({ pre: [], post: [] });
36868
+ if (this.session !== "extended" || !this.intraday) {
36869
+ if (force) this.onZones({ pre: [], post: [] });
36129
36870
  return;
36130
36871
  }
36131
- const now = Date.now();
36132
- const lookback = Math.max(MIN_LOOKBACK_MS, Math.min(MAX_LOOKBACK_MS, opts.lookbackMs));
36133
- const range = { from: now - lookback, to: now + FETCH_AHEAD_MS2 };
36134
- const [regular, extended] = await Promise.all([
36135
- provider.getCalendar(resolved.ticker, { ...range, session: "regular" }).catch(() => null),
36136
- provider.getCalendar(resolved.ticker, { ...range, session: "extended" }).catch(() => null)
36137
- ]);
36138
- if (my !== this.epoch) return;
36139
- if (!regular || !extended) return;
36140
- this.onZones(deriveSessionZones(regular, extended));
36872
+ const from = Math.min(range.from, range.to);
36873
+ const to = Math.max(range.from, range.to);
36874
+ if (!Number.isFinite(from) || !Number.isFinite(to)) return;
36875
+ if (!force && this.covered && from >= this.covered.from && to <= this.covered.to) return;
36876
+ const pad = Math.max(to - from, COVER_PAD_MIN_MS);
36877
+ const covered = { from: from - pad, to: to + pad };
36878
+ this.covered = covered;
36879
+ this.onZones(expandSessionZones(this.spec, covered.from, covered.to));
36141
36880
  }
36142
36881
  };
36143
36882
 
@@ -36541,6 +37280,7 @@ var ChartCell = class {
36541
37280
  this.inner.renderer.set("attribution", false);
36542
37281
  this.inner.renderer.set("dialogHost", deps.dialogHost);
36543
37282
  this.inner.renderer.setLegendActions(legendActionsProviderFor(this.inner, () => deps.context()));
37283
+ this.inner.renderer.setLegendCallouts(legendCalloutsProviderFor(this.inner, () => deps.context()));
36544
37284
  if (this.inner.renderer.supports("historyChords")) this.inner.renderer.set("historyChords", false);
36545
37285
  this.history.onChart(this.inner);
36546
37286
  this.inner.renderer.onConfigChanged(() => {
@@ -36574,7 +37314,11 @@ var ChartCell = class {
36574
37314
  this.deps.toast(`No registered provider serves "${symbol2}" (registered: ${list})`, "error", 6e3);
36575
37315
  });
36576
37316
  this.inner.on("load:start", () => this.watermark?.setLoading(true));
36577
- this.inner.on("load:end", () => this.watermark?.setLoading(false));
37317
+ this.inner.on("load:end", () => {
37318
+ this.watermark?.setLoading(false);
37319
+ this.refreshSessionShading();
37320
+ });
37321
+ this.inner.on("viewport:changed", (range) => this.sessionShading.updateRange(range));
36578
37322
  const tz = deps.timezone();
36579
37323
  if (tz !== "Etc/UTC") this.inner.renderer.set("timezone", tz);
36580
37324
  this.indicatorTitlesOn = seed.indicatorTitles ?? true;
@@ -36653,20 +37397,30 @@ var ChartCell = class {
36653
37397
  this.refreshNativeCatalog();
36654
37398
  this.pushSettingsSections();
36655
37399
  this.offMarket = this.inner.on("market:changed", ({ symbol: symbol2, timeframe }) => {
36656
- this.state.symbol = symbol2;
36657
- this.state.provider = parseSymbol(symbol2).provider ?? void 0;
36658
- this.state.timeframe = timeframe;
36659
- this.state.session = normalizeSession(this.inner?.market.session);
36660
- this.watermark?.update(symbol2, timeframe);
36661
- this.statusline?.setSymbol(symbol2);
36662
- this.statusline?.setMeta(timeframe, this.inner?.data.displayPrefix(symbol2) ?? this.state.provider ?? "");
36663
- if (this.inner) this.statusline?.onChart(this.inner);
37400
+ this.projectMarket(symbol2, timeframe);
36664
37401
  this.refreshNativeCatalog();
36665
37402
  this.refreshSessionAvailable();
36666
37403
  if (this.inner) this.marketStatus?.track(this.inner.data, symbol2);
36667
37404
  this.deps.onMarketChanged(this.id);
36668
37405
  });
36669
37406
  }
37407
+ /**
37408
+ * Project a market identity into the cell state and its display overlays (watermark,
37409
+ * statusline), WITHOUT the data-dependent bookkeeping. Runs twice per user pick: once
37410
+ * optimistically from the cell setters — the labels reflect the pick immediately, not
37411
+ * after the bars load — and again from `market:changed` (the committed pass, and the
37412
+ * only pass for host `chart.setMarket` calls). Idempotent, so the double run converges.
37413
+ */
37414
+ projectMarket(symbol, timeframe) {
37415
+ this.state.symbol = symbol;
37416
+ this.state.provider = parseSymbol(symbol).provider ?? void 0;
37417
+ this.state.timeframe = timeframe;
37418
+ this.state.session = normalizeSession(this.inner?.market.session);
37419
+ this.watermark?.update(symbol, timeframe);
37420
+ this.statusline?.setSymbol(symbol);
37421
+ this.statusline?.setMeta(timeframe, this.inner?.data.displayPrefix(symbol) ?? this.state.provider ?? "");
37422
+ if (this.inner) this.statusline?.onChart(this.inner);
37423
+ }
36670
37424
  /**
36671
37425
  * Does this cell's market HAVE sessions (RTH/ETH meaningful)? Derived from the
36672
37426
  * symbol's own metadata (`syminfo.session !== '24x7'`), asynchronously — the
@@ -36701,14 +37455,18 @@ var ChartCell = class {
36701
37455
  this.refreshSessionShading();
36702
37456
  });
36703
37457
  }
36704
- /** (Re)derive the pre/post-market shading bands for this cell's market. The loaded
36705
- * depth (bars × timeframe) bounds the calendar fetch; the tracker clamps it. */
37458
+ /** (Re)derive the pre/post-market shading bands for this cell's market. The bands
37459
+ * expand locally from the symbol's session vocabulary, so they paint as soon as
37460
+ * metadata is known and follow any pan depth without provider round trips. */
36706
37461
  refreshSessionShading() {
36707
37462
  const chart = this.inner;
36708
37463
  const symbol = this.state.symbol;
36709
37464
  if (!chart || !symbol) return;
36710
- const lookbackMs = Math.max(this.state.bars ?? 1e3, this.rangeBars) * timeframeMs(this.state.timeframe ?? "60");
36711
- this.sessionShading.track(chart.data, symbol, { session: this.session, lookbackMs });
37465
+ const now = Date.now();
37466
+ const requestedSpan = Math.max(this.state.bars ?? 1e3, this.rangeBars) * timeframeMs(this.state.timeframe ?? "60");
37467
+ const fallbackSpan = Number.isFinite(requestedSpan) ? Math.max(3 * 864e5, requestedSpan) : 3 * 864e5;
37468
+ const range = chart.getVisibleRange() ?? { from: now - fallbackSpan, to: now };
37469
+ this.sessionShading.track(chart.data, symbol, { session: this.session, timeframe: this.timeframe, range });
36712
37470
  }
36713
37471
  /** The session-shade colors live in the renderer CONFIG (persisted with it, edited
36714
37472
  * live by the dialog swatch) — the cell only proxies them into its settings rows. */
@@ -36804,10 +37562,10 @@ var ChartCell = class {
36804
37562
  id: "status-line",
36805
37563
  rows: [
36806
37564
  { kind: "heading", label: "Status line", id: "parts" },
36807
- { kind: "toggle", label: "Symbol name", id: "name", get: () => sl.partVisible("name"), set: (v) => sl.setPartVisible("name", v) },
36808
- { kind: "toggle", label: "Market status", id: "market", get: () => sl.partVisible("market"), set: (v) => sl.setPartVisible("market", v) },
36809
- { kind: "toggle", label: "OHLC values", id: "ohlc", get: () => sl.partVisible("ohlc"), set: (v) => sl.setPartVisible("ohlc", v) },
36810
- { kind: "toggle", label: "Bar change values", id: "change", get: () => sl.partVisible("change"), set: (v) => sl.setPartVisible("change", v) },
37565
+ { kind: "toggle", label: "Symbol name", id: "name", get: () => sl.partVisible("name"), set: (v) => this.setStatuslinePart("name", v) },
37566
+ { kind: "toggle", label: "Market status", id: "market", get: () => sl.partVisible("market"), set: (v) => this.setStatuslinePart("market", v) },
37567
+ { kind: "toggle", label: "OHLC values", id: "ohlc", get: () => sl.partVisible("ohlc"), set: (v) => this.setStatuslinePart("ohlc", v) },
37568
+ { kind: "toggle", label: "Bar change values", id: "change", get: () => sl.partVisible("change"), set: (v) => this.setStatuslinePart("change", v) },
36811
37569
  { kind: "heading", label: "Indicators", id: "indicators" },
36812
37570
  {
36813
37571
  kind: "toggle",
@@ -36842,12 +37600,40 @@ var ChartCell = class {
36842
37600
  this.indicatorTitlesOn = visible;
36843
37601
  this.inner?.renderer.set("indicatorTitles", visible);
36844
37602
  this.deps.onStateDirty();
37603
+ this.deps.onStatusPrefsChanged(this.id);
36845
37604
  }
36846
37605
  /** Show/hide the plot values beside this cell's legend titles (persisted per cell). */
36847
37606
  setIndicatorValuesVisible(visible) {
36848
37607
  this.indicatorValuesOn = visible;
36849
37608
  this.inner?.renderer.set("indicatorValues", visible);
36850
37609
  this.deps.onStateDirty();
37610
+ this.deps.onStatusPrefsChanged(this.id);
37611
+ }
37612
+ /** Show/hide one status-line segment (the settings dialog's Status line tab). */
37613
+ setStatuslinePart(part, visible) {
37614
+ this.statusline?.setPartVisible(part, visible);
37615
+ this.deps.onStatusPrefsChanged(this.id);
37616
+ }
37617
+ /** This cell's Status line tab prefs as one bundle (see {@link CellStatusPrefs}). */
37618
+ statusPrefs() {
37619
+ const sl = this.statusline;
37620
+ return {
37621
+ parts: sl ? { name: sl.partVisible("name"), market: sl.partVisible("market"), ohlc: sl.partVisible("ohlc"), change: sl.partVisible("change") } : null,
37622
+ indicatorTitles: this.indicatorTitlesOn,
37623
+ indicatorValues: this.indicatorValuesOn
37624
+ };
37625
+ }
37626
+ /** Converge this cell's Status line tab prefs to `prefs` — the follower half of
37627
+ * the workspace's style link. Idempotent: matching values change nothing, so a
37628
+ * propagated echo dies on its own. */
37629
+ applyStatusPrefs(prefs) {
37630
+ if (prefs.parts && this.statusline) {
37631
+ for (const part of Object.keys(prefs.parts)) {
37632
+ if (this.statusline.partVisible(part) !== prefs.parts[part]) this.statusline.setPartVisible(part, prefs.parts[part]);
37633
+ }
37634
+ }
37635
+ if (prefs.indicatorTitles !== this.indicatorTitlesOn) this.setIndicatorTitlesVisible(prefs.indicatorTitles);
37636
+ if (prefs.indicatorValues !== this.indicatorValuesOn) this.setIndicatorValuesVisible(prefs.indicatorValues);
36851
37637
  }
36852
37638
  /** The LIVE chart of this cell — never cache it across a layout change (the cell's
36853
37639
  * identity is what endures; the chart dies with the cell). */
@@ -36869,17 +37655,23 @@ var ChartCell = class {
36869
37655
  get indicatorCount() {
36870
37656
  return this.instances.length + this.nativeCatalog.filter((n) => n.present).length;
36871
37657
  }
36872
- /** Switch this cell's market in place (the chart instance survives). */
37658
+ /** Switch this cell's market in place (the chart instance survives). The projection
37659
+ * is OPTIMISTIC — labels and chrome show the pick before the bars load; it follows
37660
+ * the setMarket call so the statusline reads the already-blanked chart. */
36873
37661
  setSymbol(symbol) {
36874
37662
  if (!this.inner || symbol === this.symbol) return;
36875
37663
  this.unresolvedToasted = null;
36876
37664
  void this.inner.setMarket({ symbol });
37665
+ this.projectMarket(symbol, this.timeframe);
37666
+ this.deps.onMarketChanged(this.id);
36877
37667
  }
36878
37668
  setTimeframe(timeframe) {
36879
37669
  if (!this.inner || timeframe === this.timeframe) return;
36880
37670
  this.activeRangeId = null;
36881
37671
  this.rangeBars = 0;
36882
37672
  void this.inner.setMarket({ timeframe, bars: this.state.bars });
37673
+ this.projectMarket(this.symbol, timeframe);
37674
+ this.deps.onMarketChanged(this.id);
36883
37675
  }
36884
37676
  /** Applied live (renderer feature) — no reload. */
36885
37677
  setPriceStyle(style) {
@@ -37254,16 +38046,21 @@ var ChartCell = class {
37254
38046
 
37255
38047
  // src/workspace/context.ts
37256
38048
  function buildContext(host) {
37257
- const active = host.active();
37258
38049
  return {
37259
38050
  get chart() {
37260
38051
  const cell = host.active();
37261
38052
  if (!cell) throw new Error("VelaWorkspace has no active cell yet");
37262
38053
  return cell.chart;
37263
38054
  },
37264
- symbol: active?.symbol ?? "",
37265
- timeframe: active?.timeframe ?? "60",
37266
- priceStyle: active?.priceStyle ?? "candles",
38055
+ get symbol() {
38056
+ return host.active()?.symbol ?? "";
38057
+ },
38058
+ get timeframe() {
38059
+ return host.active()?.timeframe ?? "60";
38060
+ },
38061
+ get priceStyle() {
38062
+ return host.active()?.priceStyle ?? "candles";
38063
+ },
37267
38064
  setSymbol: (symbol) => host.active()?.setSymbol(symbol),
37268
38065
  setTimeframe: (tf) => host.active()?.setTimeframe(tf),
37269
38066
  setPriceStyle: (style) => host.active()?.setPriceStyle(style),
@@ -37274,8 +38071,12 @@ function buildContext(host) {
37274
38071
  addIndicator: (entry) => host.active()?.addExternalIndicator(entry),
37275
38072
  addNativeIndicator: (type) => host.active()?.addNative(type),
37276
38073
  stateChanged: () => host.stateDirty(),
37277
- cells: host.cells().map((c) => ({ id: c.id, chart: c.chart, symbol: c.symbol, timeframe: c.timeframe })),
37278
- activeCellId: active?.id ?? "",
38074
+ get cells() {
38075
+ return host.cells().map((c) => ({ id: c.id, chart: c.chart, symbol: c.symbol, timeframe: c.timeframe }));
38076
+ },
38077
+ get activeCellId() {
38078
+ return host.active()?.id ?? "";
38079
+ },
37279
38080
  setActiveCell: (id) => host.setActiveCell(id)
37280
38081
  };
37281
38082
  }
@@ -37614,6 +38415,10 @@ var VelaWorkspace = class {
37614
38415
  /** Same guard for the drawings link: the propagated mutations' own `drawing:*`
37615
38416
  * events fire synchronously inside the propagation loop and must not fan out again. */
37616
38417
  this.drawingSyncBusy = false;
38418
+ /** Same guard for the style link: a follower's `applyConfig` re-fires its
38419
+ * `onConfigChanged` in the same tick, and a state restore applies per-cell
38420
+ * configs that legitimately differ — neither must propagate. */
38421
+ this.styleSyncBusy = false;
37617
38422
  /** LINKED drawings (the drawings sync): one map per synced set (cellId → that
37618
38423
  * cell's drawing id), reachable from every member under its `cellId\0drawingId`
37619
38424
  * key — any member finds its peers to push edits/removals onto. Survives a
@@ -37674,9 +38479,7 @@ var VelaWorkspace = class {
37674
38479
  if (boot?.favorites) this.favs = [...boot.favorites];
37675
38480
  if (boot?.timeframeFavorites) this.tfFavs = [...boot.timeframeFavorites];
37676
38481
  const sync = boot?.sync ?? opts.sync;
37677
- for (const kind of ["viewport", "symbol", "timeframe", "crosshair", "drawings"]) {
37678
- this.applySyncSetting(kind, sync?.[kind]);
37679
- }
38482
+ for (const kind of SYNC_KINDS) this.applySyncSetting(kind, sync?.[kind]);
37680
38483
  this.monoLayout = opts.layout === false;
37681
38484
  const optLayout = opts.layout === false || opts.layout === void 0 ? "4" : opts.layout;
37682
38485
  this.def = this.resolveLayout(this.monoLayout ? "1" : boot?.layout && ensureLayout(boot.layout) ? boot.layout : optLayout);
@@ -37760,7 +38563,8 @@ var VelaWorkspace = class {
37760
38563
  syncs: () => [
37761
38564
  { id: "symbol", label: "Symbol", checked: this.syncOpts.symbol === true },
37762
38565
  { id: "timeframe", label: "Interval", checked: this.syncOpts.timeframe === true },
37763
- { id: "crosshair", label: "Crosshair", checked: this.syncOpts.crosshair === true }
38566
+ { id: "crosshair", label: "Crosshair", checked: this.syncOpts.crosshair === true },
38567
+ { id: "style", label: "Style", checked: this.syncOpts.style === true }
37764
38568
  ],
37765
38569
  onToggleSync: (id) => {
37766
38570
  const kind = id;
@@ -37979,7 +38783,10 @@ var VelaWorkspace = class {
37979
38783
  this.topbar.renderActions();
37980
38784
  this.mobileBar?.renderActions();
37981
38785
  this.dock.refresh();
37982
- for (const cell of this.cells()) cell.chart.renderer.setLegendActions(legendActionsProviderFor(cell.chart, () => this.context()));
38786
+ for (const cell of this.cells()) {
38787
+ cell.chart.renderer.setLegendActions(legendActionsProviderFor(cell.chart, () => this.context()));
38788
+ cell.chart.renderer.setLegendCallouts(legendCalloutsProviderFor(cell.chart, () => this.context()));
38789
+ }
37983
38790
  }
37984
38791
  // ── state surface (the SDK's read/restore of the whole grid's config + content) ──
37985
38792
  /**
@@ -38044,7 +38851,7 @@ var VelaWorkspace = class {
38044
38851
  this.topbar.setTimeframeFavorites(this.tfFavs);
38045
38852
  }
38046
38853
  this.dock.applyState(st.panels);
38047
- for (const kind of ["viewport", "symbol", "timeframe", "crosshair", "drawings"]) this.applySyncSetting(kind, st.sync?.[kind]);
38854
+ for (const kind of SYNC_KINDS) this.applySyncSetting(kind, st.sync?.[kind]);
38048
38855
  this.trackSizes.clear();
38049
38856
  if (st.trackSizes) for (const [id, ts] of Object.entries(st.trackSizes)) this.trackSizes.set(id, ts);
38050
38857
  const targetDef = this.monoLayout ? this.def : ensureLayout(st.layout) ?? this.def;
@@ -38055,9 +38862,14 @@ var VelaWorkspace = class {
38055
38862
  for (const cell of this.cellsById.values()) cell.chart.drawings.setFavorites(this.favs);
38056
38863
  }
38057
38864
  this.drawingLinks.clear();
38058
- for (const [i] of this.def.cells.entries()) {
38059
- const { id, ...cs } = st.charts[i];
38060
- this.cellsById.get(id)?.rehydrate(cs);
38865
+ this.styleSyncBusy = true;
38866
+ try {
38867
+ for (const [i] of this.def.cells.entries()) {
38868
+ const { id, ...cs } = st.charts[i];
38869
+ this.cellsById.get(id)?.rehydrate(cs);
38870
+ }
38871
+ } finally {
38872
+ this.styleSyncBusy = false;
38061
38873
  }
38062
38874
  if (st.timezone) this.setTimezone(st.timezone);
38063
38875
  this.pool.clear();
@@ -38157,6 +38969,7 @@ var VelaWorkspace = class {
38157
38969
  const rebuildAll = nextBackend !== this.cellBackend;
38158
38970
  this.order = orderAfterLayout(this.order, next.cells.length, this.activeId);
38159
38971
  const keep = new Set(this.order.slice(0, next.cells.length));
38972
+ const preexisting = new Set(this.cellsById.keys());
38160
38973
  for (const [id, cell] of [...this.cellsById]) {
38161
38974
  if (!keep.has(id) || rebuildAll) {
38162
38975
  this.poolSet(id, cell.dehydrate());
@@ -38169,6 +38982,7 @@ var VelaWorkspace = class {
38169
38982
  this.cellBackend = nextBackend;
38170
38983
  this.applyGrid();
38171
38984
  this.buildCells();
38985
+ this.alignNewCellStyles(preexisting);
38172
38986
  this.syncCellPresentation();
38173
38987
  this.topbar.setLayout(next.id);
38174
38988
  const nextActive = activeAfterLayout(this.activeId, this.order.slice(0, next.cells.length));
@@ -38372,6 +39186,7 @@ var VelaWorkspace = class {
38372
39186
  onMarketChanged: (id2) => this.onCellMarketChanged(id2),
38373
39187
  onPriceStyleChanged: (id2) => this.onCellPriceStyleChanged(id2),
38374
39188
  onIndicatorsChanged: (id2) => this.onCellIndicatorsChanged(id2),
39189
+ onStatusPrefsChanged: (id2) => this.propagateStylePrefs(id2),
38375
39190
  onStateDirty: () => this.markStateDirty(),
38376
39191
  manifestSettled: () => this.manifestSettled,
38377
39192
  toast: (message, kind, durationMs) => this.toastHost.show(message, kind, durationMs)
@@ -38447,6 +39262,7 @@ var VelaWorkspace = class {
38447
39262
  this.drawToolbar?.setEraserActive(mode === "eraser");
38448
39263
  });
38449
39264
  chart.on("viewport:changed", (range) => this.propagateViewport(cell.id, range));
39265
+ chart.renderer.onConfigChanged(() => this.propagateStylePrefs(cell.id));
38450
39266
  chart.on("theme:changed", (t) => this.setTheme(t));
38451
39267
  chart.renderer.onAxisLongPress((e) => {
38452
39268
  if (this.layoutCtl.current !== "mobile") return;
@@ -38479,11 +39295,67 @@ var VelaWorkspace = class {
38479
39295
  if (kind === "viewport") {
38480
39296
  const range = this.cellsById.get(this.activeId)?.chart.getVisibleRange();
38481
39297
  if (range) this.propagateViewport(this.activeId, range);
39298
+ } else if (kind === "style") {
39299
+ this.propagateStylePrefs(this.activeId);
38482
39300
  } else {
38483
39301
  this.propagateMarket(this.activeId);
38484
39302
  }
38485
39303
  }
38486
39304
  }
39305
+ /**
39306
+ * Align cells minted by a layout change to their style group: with the link on, a
39307
+ * NEW cell (fresh slot or one returning from the pool, which missed edits while
39308
+ * dormant) inherits the presentation of a pre-existing group peer — the active
39309
+ * cell when it is one — instead of sitting on its own state beside a styled
39310
+ * group. Propagation runs FROM the peer, so a newborn's defaults never overwrite
39311
+ * the group, and the equality short-circuits keep converged peers untouched.
39312
+ */
39313
+ alignNewCellStyles(preexisting) {
39314
+ const setting = this.syncOpts.style;
39315
+ if (!setting) return;
39316
+ const ids = [...this.cellsById.keys()];
39317
+ const propagated = /* @__PURE__ */ new Set();
39318
+ for (const id of ids) {
39319
+ if (preexisting.has(id)) continue;
39320
+ const peers = syncTargets(id, setting, ids).filter((p) => preexisting.has(p));
39321
+ if (peers.length === 0) continue;
39322
+ const source = this.activeId && peers.includes(this.activeId) ? this.activeId : peers[0];
39323
+ if (propagated.has(source)) continue;
39324
+ propagated.add(source);
39325
+ this.propagateStylePrefs(source);
39326
+ }
39327
+ }
39328
+ /**
39329
+ * Mirror an origin cell's presentation — the Canvas + Scales-and-lines slice of
39330
+ * its renderer config plus its Status line tab prefs — onto its same-group
39331
+ * followers (the style link). Loop-safe two ways: the busy guard eats the
39332
+ * followers' SYNCHRONOUS echoes (their `applyConfig` re-fires `onConfigChanged`
39333
+ * in the same tick), and the equality short-circuits leave already-converged
39334
+ * followers untouched, so nothing re-emits once the group agrees.
39335
+ */
39336
+ propagateStylePrefs(originId) {
39337
+ if (this.styleSyncBusy || this.destroyed) return;
39338
+ const targets = syncTargets(originId, this.syncOpts.style, [...this.cellsById.keys()]);
39339
+ if (targets.length === 0) return;
39340
+ const origin = this.cellsById.get(originId);
39341
+ if (!origin) return;
39342
+ const slice = styleConfigSlice(origin.chart.renderer.getConfig());
39343
+ const sliceJson = slice ? JSON.stringify(slice) : null;
39344
+ const prefs = origin.statusPrefs();
39345
+ this.styleSyncBusy = true;
39346
+ try {
39347
+ for (const id of targets) {
39348
+ const cell = this.cellsById.get(id);
39349
+ if (!cell) continue;
39350
+ if (slice && sliceJson !== JSON.stringify(styleConfigSlice(cell.chart.renderer.getConfig()))) {
39351
+ cell.chart.renderer.applyConfig(slice);
39352
+ }
39353
+ cell.applyStatusPrefs(prefs);
39354
+ }
39355
+ } finally {
39356
+ this.styleSyncBusy = false;
39357
+ }
39358
+ }
38487
39359
  /**
38488
39360
  * Mirror an origin cell's pointer time onto its same-group followers as GHOST
38489
39361
  * crosshairs (`renderer.setExternalCrosshair`). The horizontal price level rides
@@ -38791,7 +39663,8 @@ var VelaWorkspace = class {
38791
39663
  syncs: () => [
38792
39664
  { id: "symbol", label: "Symbol", checked: this.syncOpts.symbol === true },
38793
39665
  { id: "timeframe", label: "Interval", checked: this.syncOpts.timeframe === true },
38794
- { id: "crosshair", label: "Crosshair", checked: this.syncOpts.crosshair === true }
39666
+ { id: "crosshair", label: "Crosshair", checked: this.syncOpts.crosshair === true },
39667
+ { id: "style", label: "Style", checked: this.syncOpts.style === true }
38795
39668
  ],
38796
39669
  onToggleSync: (id) => {
38797
39670
  const kind = id;