@luxalgo/vela 0.6.8 → 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-SQETIBFU.js → chunk-6WDDVMBJ.js} +502 -139
  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-N7LGMKCE.js → chunk-STHSKXOR.js} +107 -84
  8. package/dist/{chunk-PN5KWFZ4.js → chunk-WFSZBX3R.js} +2 -2
  9. package/dist/{contributions-Vw-Hm58R.d.cts → contributions-C1U2Krwg.d.cts} +99 -7
  10. package/dist/{contributions-DLLdV9jD.d.ts → contributions-D7PVZO2i.d.ts} +99 -7
  11. package/dist/index.cjs +709 -144
  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-CC7rBrOY.d.ts → plugin-7bkF32Rk.d.ts} +3 -3
  18. package/dist/{plugin-BnJgjLAy.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/{statusline-DqzBqy42.d.cts → statusline-DOPiT6I6.d.cts} +5 -3
  30. package/dist/{statusline-CGkB2EOo.d.ts → statusline-zdF4eZLr.d.ts} +5 -3
  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 +709 -144
  36. package/dist/vela.global.min.js +93 -44
  37. package/dist/widget.cjs +798 -188
  38. package/dist/widget.d.cts +6 -6
  39. package/dist/widget.d.ts +6 -6
  40. package/dist/widget.js +7 -7
  41. package/dist/workspace.cjs +798 -188
  42. package/dist/workspace.d.cts +16 -6
  43. package/dist/workspace.d.ts +16 -6
  44. package/dist/workspace.js +6 -6
  45. package/package.json +1 -1
package/dist/widget.cjs CHANGED
@@ -1097,11 +1097,11 @@ registerIcon(
1097
1097
  );
1098
1098
  registerIcon(
1099
1099
  "market-pre",
1100
- svg24('<path d="M12 2v8"/><path d="m4.93 10.93 1.41 1.41"/><path d="M2 18h2"/><path d="M20 18h2"/><path d="m19.07 10.93-1.41 1.41"/><path d="M22 22H2"/><path d="m16 6-4 4-4-4"/><path d="M16 18a4 4 0 0 0-8 0"/>')
1100
+ svg24('<path d="M12 2v8"/><path d="m4.93 10.93 1.41 1.41"/><path d="M2 18h2"/><path d="M20 18h2"/><path d="m19.07 10.93-1.41 1.41"/><path d="M22 22H2"/><path d="m8 6 4-4 4 4"/><path d="M16 18a4 4 0 0 0-8 0"/>')
1101
1101
  );
1102
1102
  registerIcon(
1103
1103
  "market-post",
1104
- svg24('<path d="M12 10V2"/><path d="m4.93 10.93 1.41 1.41"/><path d="M2 18h2"/><path d="M20 18h2"/><path d="m19.07 10.93-1.41 1.41"/><path d="M22 22H2"/><path d="m16 18-4-4-4 4"/><path d="M16 6a4 4 0 0 0-8 0"/>')
1104
+ svg24('<path d="M12 10V2"/><path d="m4.93 10.93 1.41 1.41"/><path d="M2 18h2"/><path d="M20 18h2"/><path d="m19.07 10.93-1.41 1.41"/><path d="M22 22H2"/><path d="m16 14-4 4-4-4"/><path d="M16 6a4 4 0 0 0-8 0"/>')
1105
1105
  );
1106
1106
  registerIcon("market-closed", svg24('<path d="M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z"/>'));
1107
1107
  registerIcon(
@@ -4195,6 +4195,192 @@ function buildFieldControl(desc) {
4195
4195
  return { el };
4196
4196
  }
4197
4197
 
4198
+ // src/ui/components/callout-bubble/controller.ts
4199
+ function closesPanel(item) {
4200
+ return item.close !== false;
4201
+ }
4202
+ function calloutPanelRows(items) {
4203
+ const rows = [];
4204
+ for (const item of items) {
4205
+ if (item.type === "text") {
4206
+ rows.push({ type: "text", text: item.text });
4207
+ continue;
4208
+ }
4209
+ const last = rows[rows.length - 1];
4210
+ if (last && last.type === "buttons") last.buttons.push(item);
4211
+ else rows.push({ type: "buttons", buttons: [item] });
4212
+ }
4213
+ return rows;
4214
+ }
4215
+
4216
+ // src/ui/components/callout-bubble/styles.ts
4217
+ var CALLOUT_STYLE_ID = "vela-ui-callout-bubble";
4218
+ var CALLOUT_CSS = `
4219
+ .vela-callout {
4220
+ display: inline-grid;
4221
+ place-items: center;
4222
+ border-radius: 50%;
4223
+ flex: none;
4224
+ line-height: 0;
4225
+ box-sizing: border-box;
4226
+ cursor: default;
4227
+ user-select: none;
4228
+ -webkit-user-select: none;
4229
+ }
4230
+ .vela-callout[role='button'] { cursor: pointer; }
4231
+ .vela-callout svg { display: block; }
4232
+ /* The deployed panel \u2014 carries the kit's elevated-card look itself (the popover
4233
+ shell is bare positioning chrome). */
4234
+ .vela-callout-panel {
4235
+ display: flex;
4236
+ flex-direction: column;
4237
+ gap: 8px;
4238
+ padding: 10px 12px;
4239
+ max-width: 280px;
4240
+ box-sizing: border-box;
4241
+ background: var(--vela-surface-elev);
4242
+ border: 1px solid var(--vela-border-strong);
4243
+ border-radius: 6px;
4244
+ box-shadow: var(--vela-shadow);
4245
+ color: var(--vela-fg);
4246
+ font: var(--vela-font-size-md) var(--vela-font);
4247
+ }
4248
+ .vela-callout-title { font-weight: 600; color: var(--vela-fg-bright); }
4249
+ .vela-callout-text { color: var(--vela-fg-muted); line-height: 1.45; white-space: pre-line; }
4250
+ .vela-callout-actions { display: flex; flex-wrap: wrap; gap: 8px; }
4251
+ .vela-callout-btn {
4252
+ cursor: pointer;
4253
+ height: 26px;
4254
+ padding: 0 10px;
4255
+ border-radius: 5px;
4256
+ border: 1px solid var(--vela-border);
4257
+ background: transparent;
4258
+ color: var(--vela-fg);
4259
+ font-size: var(--vela-font-size-md);
4260
+ font-family: inherit;
4261
+ transition: background var(--vela-dur-fast) ease, color var(--vela-dur-fast) ease, opacity var(--vela-dur-fast) ease, border-color var(--vela-dur-fast) ease;
4262
+ }
4263
+ .vela-callout-btn:hover { background: var(--vela-hover); color: var(--vela-fg-bright); border-color: var(--vela-fg-muted); }
4264
+ .vela-callout-btn-primary { border-color: var(--vela-selected-bg); background: var(--vela-selected-bg); color: var(--vela-selected-fg); }
4265
+ .vela-callout-btn-primary:hover { background: var(--vela-selected-bg); color: var(--vela-selected-fg); opacity: 0.85; border-color: var(--vela-selected-bg); }
4266
+ `;
4267
+
4268
+ // src/ui/components/callout-bubble/view.ts
4269
+ var CalloutBubble = class {
4270
+ constructor(opts) {
4271
+ this.pop = null;
4272
+ this.spec = { icon: opts.icon, background: opts.background, label: opts.label };
4273
+ if (opts.color !== void 0) this.spec.color = opts.color;
4274
+ if (opts.panel !== void 0) this.spec.panel = opts.panel;
4275
+ this.size = opts.size ?? 16;
4276
+ this.host = opts.host;
4277
+ this.theme = opts.theme;
4278
+ this.boundary = opts.boundary;
4279
+ const doc = (opts.host ?? document.body).ownerDocument;
4280
+ injectStyles(CALLOUT_STYLE_ID, CALLOUT_CSS, doc);
4281
+ this.el = doc.createElement("span");
4282
+ this.el.className = "vela-callout";
4283
+ this.el.style.width = `${this.size}px`;
4284
+ this.el.style.height = `${this.size}px`;
4285
+ this.el.addEventListener("click", (e) => {
4286
+ if (!this.spec.panel) return;
4287
+ e.stopPropagation();
4288
+ this.toggle();
4289
+ });
4290
+ this.el.addEventListener("dblclick", (e) => {
4291
+ if (this.spec.panel) e.stopPropagation();
4292
+ });
4293
+ this.el.addEventListener("keydown", (e) => {
4294
+ if (!this.spec.panel || e.key !== "Enter" && e.key !== " ") return;
4295
+ e.preventDefault();
4296
+ this.toggle();
4297
+ });
4298
+ this.dress();
4299
+ }
4300
+ /** Re-dress the bubble (a status change: new icon, tint, label, panel). */
4301
+ set(spec) {
4302
+ this.spec = { ...this.spec, ...spec };
4303
+ if ("panel" in spec) this.pop?.hide();
4304
+ this.dress();
4305
+ }
4306
+ /** Whether the deployed panel is currently open. */
4307
+ get open() {
4308
+ return this.pop?.open ?? false;
4309
+ }
4310
+ destroy() {
4311
+ this.pop?.destroy();
4312
+ this.pop = null;
4313
+ this.el.remove();
4314
+ }
4315
+ dress() {
4316
+ const clickable = this.spec.panel !== void 0;
4317
+ this.el.style.background = this.spec.background;
4318
+ this.el.style.color = this.spec.color ?? "";
4319
+ this.el.innerHTML = iconAt(this.spec.icon, this.size - 4);
4320
+ this.el.setAttribute("aria-label", this.spec.label);
4321
+ if (clickable) {
4322
+ this.el.setAttribute("role", "button");
4323
+ this.el.tabIndex = 0;
4324
+ } else {
4325
+ this.el.removeAttribute("role");
4326
+ this.el.removeAttribute("tabindex");
4327
+ }
4328
+ }
4329
+ toggle() {
4330
+ if (this.pop?.open) {
4331
+ this.pop.hide();
4332
+ return;
4333
+ }
4334
+ const panel = this.spec.panel;
4335
+ if (!panel) return;
4336
+ this.pop?.destroy();
4337
+ this.pop = new Popover({
4338
+ trigger: this.el,
4339
+ gap: 6,
4340
+ content: (body) => this.buildPanel(body, panel),
4341
+ ...this.host ? { host: this.host } : {},
4342
+ ...this.theme ? { theme: this.theme() } : {},
4343
+ ...this.boundary ? { boundary: this.boundary } : {}
4344
+ });
4345
+ this.pop.show();
4346
+ }
4347
+ buildPanel(body, panel) {
4348
+ const doc = body.ownerDocument;
4349
+ const root = doc.createElement("div");
4350
+ root.className = "vela-callout-panel";
4351
+ if (panel.title) {
4352
+ const title = doc.createElement("div");
4353
+ title.className = "vela-callout-title";
4354
+ title.textContent = panel.title;
4355
+ root.appendChild(title);
4356
+ }
4357
+ for (const row of calloutPanelRows(panel.items)) {
4358
+ if (row.type === "text") {
4359
+ const text = doc.createElement("div");
4360
+ text.className = "vela-callout-text";
4361
+ text.textContent = row.text;
4362
+ root.appendChild(text);
4363
+ continue;
4364
+ }
4365
+ const actions = doc.createElement("div");
4366
+ actions.className = "vela-callout-actions";
4367
+ for (const item of row.buttons) {
4368
+ const btn2 = doc.createElement("button");
4369
+ btn2.type = "button";
4370
+ btn2.className = "vela-callout-btn" + (item.primary ? " vela-callout-btn-primary" : "");
4371
+ btn2.textContent = item.label;
4372
+ btn2.addEventListener("click", () => {
4373
+ item.run();
4374
+ if (closesPanel(item)) this.pop?.hide();
4375
+ });
4376
+ actions.appendChild(btn2);
4377
+ }
4378
+ root.appendChild(actions);
4379
+ }
4380
+ body.appendChild(root);
4381
+ }
4382
+ };
4383
+
4198
4384
  // src/widget/layout-picker.ts
4199
4385
  var STYLE_ID = "vela-widget-layout-picker-v14";
4200
4386
  var CSS2 = `
@@ -4677,6 +4863,44 @@ function legendActionsProviderFor(chart, context) {
4677
4863
  return legendActions().filter((d) => !d.when || d.when(info)).map((d) => ({ id: d.id, icon: d.icon, tooltip: d.tooltip, run: () => d.run(context(), info) }));
4678
4864
  };
4679
4865
  }
4866
+ var calloutRegistry = /* @__PURE__ */ new Map();
4867
+ function legendCallouts() {
4868
+ return [...calloutRegistry.values()].sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
4869
+ }
4870
+ function legendCalloutsProviderFor(chart, context) {
4871
+ return (indicatorId) => {
4872
+ const handle = chart.indicators().find((h) => h.id === indicatorId);
4873
+ if (!handle) return [];
4874
+ const info = { id: handle.id, title: handle.title, ...handle.source !== void 0 ? { source: handle.source } : {} };
4875
+ const views = [];
4876
+ for (const d of legendCallouts()) {
4877
+ const spec = d.callout(info);
4878
+ if (!spec) continue;
4879
+ views.push({
4880
+ id: d.id,
4881
+ icon: spec.icon,
4882
+ background: spec.background,
4883
+ ...spec.color !== void 0 ? { color: spec.color } : {},
4884
+ tooltip: spec.tooltip,
4885
+ ...spec.content !== void 0 ? {
4886
+ content: {
4887
+ ...spec.content.title !== void 0 ? { title: spec.content.title } : {},
4888
+ items: spec.content.items.map(
4889
+ (item) => item.type === "text" ? item : {
4890
+ type: "button",
4891
+ label: item.label,
4892
+ ...item.primary !== void 0 ? { primary: item.primary } : {},
4893
+ ...item.close !== void 0 ? { close: item.close } : {},
4894
+ run: () => item.run(context(), info)
4895
+ }
4896
+ )
4897
+ }
4898
+ } : {}
4899
+ });
4900
+ }
4901
+ return views;
4902
+ };
4903
+ }
4680
4904
  var stateHandlers = /* @__PURE__ */ new Map();
4681
4905
  function registerStatePersistence(handler) {
4682
4906
  stateHandlers.set(handler.key, handler);
@@ -17578,6 +17802,7 @@ var IndicatorHandleImpl = class {
17578
17802
  constructor(id, title, controller, source) {
17579
17803
  this.controller = controller;
17580
17804
  this.schema = [];
17805
+ this.propsSchema = [];
17581
17806
  this.visibleState = true;
17582
17807
  this.bus = new TypedEventBus();
17583
17808
  this.id = id;
@@ -17587,6 +17812,9 @@ var IndicatorHandleImpl = class {
17587
17812
  get inputs() {
17588
17813
  return this.schema;
17589
17814
  }
17815
+ get props() {
17816
+ return this.propsSchema;
17817
+ }
17590
17818
  get visible() {
17591
17819
  return this.visibleState;
17592
17820
  }
@@ -17596,6 +17824,12 @@ var IndicatorHandleImpl = class {
17596
17824
  setInputs(values) {
17597
17825
  this.controller.applyInputs(this.id, values);
17598
17826
  }
17827
+ setProp(key, value) {
17828
+ this.controller.applyProps(this.id, { [key]: value });
17829
+ }
17830
+ setProps(values) {
17831
+ this.controller.applyProps(this.id, values);
17832
+ }
17599
17833
  setVisible(visible) {
17600
17834
  this.controller.setVisible(this.id, visible);
17601
17835
  }
@@ -17615,6 +17849,9 @@ var IndicatorHandleImpl = class {
17615
17849
  setSchema(schema) {
17616
17850
  this.schema = schema;
17617
17851
  }
17852
+ setPropsSchema(schema) {
17853
+ this.propsSchema = schema;
17854
+ }
17618
17855
  /** Sync the public `visible` getter when the orchestrator changes visibility (API or legend eye). */
17619
17856
  setVisibleState(visible) {
17620
17857
  this.visibleState = visible;
@@ -17628,6 +17865,8 @@ var IndicatorHandleImpl = class {
17628
17865
  function summarizeModel(model) {
17629
17866
  const series = {};
17630
17867
  for (const s of model.series) series[s.kind] = (series[s.kind] ?? 0) + 1;
17868
+ const countOverlay = (items) => items?.filter((i) => i.overlay === true).length ?? 0;
17869
+ const forcedOverlay = countOverlay(model.series) + countOverlay(model.fills) + countOverlay(model.backgrounds) + countOverlay(model.lines) + countOverlay(model.boxes) + countOverlay(model.labels) + countOverlay(model.polylines) + countOverlay(model.linefills) + countOverlay(model.tables);
17631
17870
  return {
17632
17871
  id: model.id,
17633
17872
  title: model.title,
@@ -17647,7 +17886,8 @@ function summarizeModel(model) {
17647
17886
  tables: model.tables?.length ?? 0,
17648
17887
  barColors: model.barColors?.length ?? 0,
17649
17888
  trades: model.trades?.length ?? 0,
17650
- inputs: model.inputs.length
17889
+ inputs: model.inputs.length,
17890
+ forcedOverlay
17651
17891
  };
17652
17892
  }
17653
17893
  function inspectModels(models) {
@@ -17668,7 +17908,8 @@ function inspectModels(models) {
17668
17908
  linefills: sum((s) => s.linefills),
17669
17909
  tables: sum((s) => s.tables),
17670
17910
  barColors: sum((s) => s.barColors),
17671
- trades: sum((s) => s.trades)
17911
+ trades: sum((s) => s.trades),
17912
+ forcedOverlay: sum((s) => s.forcedOverlay)
17672
17913
  }
17673
17914
  };
17674
17915
  }
@@ -17812,7 +18053,9 @@ var EngineOrchestrator = class _EngineOrchestrator {
17812
18053
  for (const engine of engines) this.registerEngine(engine.language, engine);
17813
18054
  this.defaultLanguage = config.defaultLanguage;
17814
18055
  this.renderer.mount(container, config.theme);
17815
- this.renderer.onInputChange((e) => this.applyInputs(e.indicatorId, { [e.key]: e.value }));
18056
+ this.renderer.onInputChange(
18057
+ (e) => e.kind === "prop" ? this.applyProps(e.indicatorId, { [e.key]: e.value }) : this.applyInputs(e.indicatorId, { [e.key]: e.value })
18058
+ );
17816
18059
  this.renderer.onRemoveIndicator((id) => this.removeIndicator(id));
17817
18060
  this.renderer.onToggleIndicatorVisible?.((id, visible) => this.setVisible(id, visible));
17818
18061
  this.paneActionUnsub = this.renderer.onPaneAction?.((a) => this.handlePaneAction(a)) ?? null;
@@ -18492,7 +18735,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
18492
18735
  const title = options.title ?? "Indicator";
18493
18736
  const handle = new IndicatorHandleImpl(id, title, this, source);
18494
18737
  this.handles.set(id, handle);
18495
- this.registry.add({ id, title, source, options, inputValues: { ...options.inputs ?? {} } });
18738
+ this.registry.add({ id, title, source, options, inputValues: { ...options.inputs ?? {} }, propValues: { ...options.props ?? {} } });
18496
18739
  void this.startIndicator(id, source, options, handle);
18497
18740
  return handle;
18498
18741
  }
@@ -18515,7 +18758,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
18515
18758
  return handle;
18516
18759
  }
18517
18760
  const inputValues = { ...descriptor.defaultInputs(), ...options.inputs ?? {} };
18518
- this.registry.add({ id, title, source: type, inputValues, native: { type, instance: descriptor.create(), descriptor } });
18761
+ this.registry.add({ id, title, source: type, inputValues, propValues: {}, native: { type, instance: descriptor.create(), descriptor } });
18519
18762
  handle.setSchema(descriptor.inputsSchema());
18520
18763
  void this.startNativeIndicator(id, handle);
18521
18764
  return handle;
@@ -18568,6 +18811,19 @@ var EngineOrchestrator = class _EngineOrchestrator {
18568
18811
  if (record.session) record.session.update(record.inputValues);
18569
18812
  else if (record.native && !record.hidden) record.native.instance.setInputs(record.inputValues);
18570
18813
  }
18814
+ /** IndicatorController: re-run an indicator with merged declaration-prop overrides.
18815
+ * Same lifecycle as {@link applyInputs} — a prop change replays the whole script.
18816
+ * Script indicators only: natives have no declaration props. */
18817
+ applyProps(id, values) {
18818
+ const record = this.registry.get(id);
18819
+ if (!record?.session) return;
18820
+ record.propValues = { ...record.propValues, ...values };
18821
+ if (record.renderHandle) this.renderer.setIndicatorInputs(record.renderHandle, record.inputValues, record.propValues);
18822
+ record.pendingStructural = true;
18823
+ if (!record.hidden) this.setLoading(record, true);
18824
+ record.pendingCause = "inputs";
18825
+ record.session.update(record.inputValues, record.propValues);
18826
+ }
18571
18827
  /** IndicatorController: tear down an indicator and (if now empty) its pane. */
18572
18828
  /** Live handles of every indicator on the chart (script + native), insertion order. */
18573
18829
  listIndicators() {
@@ -18716,6 +18972,10 @@ var EngineOrchestrator = class _EngineOrchestrator {
18716
18972
  for (const input of prepared.inputs) defaults2[input.key] = input.defval;
18717
18973
  record.inputValues = { ...defaults2, ...record.inputValues };
18718
18974
  handle.setSchema(prepared.inputs);
18975
+ const propDefaults = {};
18976
+ for (const prop of prepared.props ?? []) propDefaults[prop.key] = prop.defval;
18977
+ record.propValues = { ...propDefaults, ...record.propValues };
18978
+ handle.setPropsSchema(prepared.props ?? []);
18719
18979
  this.mountLoadingPlaceholder(id, record);
18720
18980
  this.executeIndicator(id, handle);
18721
18981
  } catch (err) {
@@ -18739,6 +18999,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
18739
18999
  getBars: () => this.bars,
18740
19000
  fetchSeries: (sym, tf, range) => this.fetchSeries(sym, tf, range),
18741
19001
  inputs: record.inputValues,
19002
+ props: record.propValues,
18742
19003
  visibleRange: this.currentVisibleRange(),
18743
19004
  mode,
18744
19005
  // Mid-backfill session starts (add / re-show / price-style re-execute) let the
@@ -18832,6 +19093,9 @@ var EngineOrchestrator = class _EngineOrchestrator {
18832
19093
  const meta = record.prepared.meta;
18833
19094
  const model = {
18834
19095
  id,
19096
+ // Deliberately the FULL title, never a shorttitle: while the script loads the
19097
+ // legend identifies it by its full name; the compact shorttitle arrives with
19098
+ // the first computed model and takes over from there.
18835
19099
  title: record.options?.title ?? meta.title,
18836
19100
  overlay: meta.overlay,
18837
19101
  paneHint: meta.overlay ? "price" : "new",
@@ -18840,7 +19104,8 @@ var EngineOrchestrator = class _EngineOrchestrator {
18840
19104
  backgrounds: [],
18841
19105
  priceLines: [],
18842
19106
  inputs: record.prepared.inputs,
18843
- inputValues: record.inputValues
19107
+ inputValues: record.inputValues,
19108
+ ...record.prepared.props ? { props: record.prepared.props, propValues: record.propValues } : {}
18844
19109
  };
18845
19110
  const paneId = this.routePane(id, model, record.options ?? {});
18846
19111
  this.placeModel(model, id, paneId);
@@ -19073,7 +19338,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
19073
19338
  record.model = model;
19074
19339
  if (record.pendingStructural) {
19075
19340
  record.renderHandle = this.renderer.mountIndicator(model);
19076
- this.renderer.setIndicatorInputs(record.renderHandle, record.inputValues);
19341
+ this.renderer.setIndicatorInputs(record.renderHandle, record.inputValues, record.propValues);
19077
19342
  record.pendingStructural = false;
19078
19343
  } else {
19079
19344
  this.renderer.updateIndicator(record.renderHandle, modelToValuePatch(model));
@@ -19100,16 +19365,17 @@ var EngineOrchestrator = class _EngineOrchestrator {
19100
19365
  placeModel(model, id, paneId) {
19101
19366
  model.id = id;
19102
19367
  model.paneId = paneId;
19103
- for (const series of model.series) series.paneId = paneId;
19104
- for (const fill of model.fills) fill.paneId = paneId;
19105
- for (const bg of model.backgrounds) bg.paneId = paneId;
19368
+ const paneOf = (item) => item.overlay === true ? "price" : paneId;
19369
+ for (const series of model.series) series.paneId = paneOf(series);
19370
+ for (const fill of model.fills) fill.paneId = paneOf(fill);
19371
+ for (const bg of model.backgrounds) bg.paneId = paneOf(bg);
19106
19372
  for (const line of model.priceLines) line.paneId = paneId;
19107
- if (model.lines) for (const ln of model.lines) ln.paneId = paneId;
19108
- if (model.boxes) for (const bx of model.boxes) bx.paneId = paneId;
19109
- if (model.labels) for (const lb of model.labels) lb.paneId = paneId;
19110
- if (model.polylines) for (const pl of model.polylines) pl.paneId = paneId;
19111
- if (model.linefills) for (const lf of model.linefills) lf.paneId = paneId;
19112
- if (model.tables) for (const tb of model.tables) tb.paneId = paneId;
19373
+ if (model.lines) for (const ln of model.lines) ln.paneId = paneOf(ln);
19374
+ if (model.boxes) for (const bx of model.boxes) bx.paneId = paneOf(bx);
19375
+ if (model.labels) for (const lb of model.labels) lb.paneId = paneOf(lb);
19376
+ if (model.polylines) for (const pl of model.polylines) pl.paneId = paneOf(pl);
19377
+ if (model.linefills) for (const lf of model.linefills) lf.paneId = paneOf(lf);
19378
+ if (model.tables) for (const tb of model.tables) tb.paneId = paneOf(tb);
19113
19379
  }
19114
19380
  emitContextChanged(id) {
19115
19381
  if (!this.registry.get(id)?.session?.getContext) return;
@@ -19262,6 +19528,15 @@ var RendererControl = class {
19262
19528
  setLegendActions(provider) {
19263
19529
  this.renderer.setLegendActions?.(provider);
19264
19530
  }
19531
+ /**
19532
+ * Wire the legend rows' HOST-CONTRIBUTED callout bubbles (the shells route the
19533
+ * plugin registry through this; see `registerLegendCallout`). Silent on a renderer
19534
+ * without the seam — contributed callouts simply never show there, same graceful
19535
+ * degradation as {@link setLegendActions}.
19536
+ */
19537
+ setLegendCallouts(provider) {
19538
+ this.renderer.setLegendCallouts?.(provider);
19539
+ }
19265
19540
  /**
19266
19541
  * Replace the indicator legend's fold toggle with a host action (or restore it with
19267
19542
  * `null`) — multi-chart shells point the chip at their indicator overview instead of
@@ -19771,6 +20046,10 @@ function inputVisible(when, values) {
19771
20046
  }
19772
20047
 
19773
20048
  // src/renderers/shared/IndicatorInputsDialog.ts
20049
+ var PROPS_TAB = "Properties";
20050
+ function bagOf(row, decl) {
20051
+ return decl.prop ? row.propValues : row.values;
20052
+ }
19774
20053
  var SOURCES = ["close", "open", "high", "low", "hl2", "hlc3", "ohlc4", "volume"];
19775
20054
  var IndicatorInputsDialog = class {
19776
20055
  constructor(host) {
@@ -19781,6 +20060,7 @@ var IndicatorInputsDialog = class {
19781
20060
  this.openId = null;
19782
20061
  this.row = null;
19783
20062
  this.snapshot = null;
20063
+ this.propSnapshot = null;
19784
20064
  this.dialogTips = [];
19785
20065
  /** Re-applies every `when` gate against current values; set while the dialog is open. */
19786
20066
  this.refreshVisibility = null;
@@ -19803,10 +20083,11 @@ var IndicatorInputsDialog = class {
19803
20083
  }
19804
20084
  open(row) {
19805
20085
  this.close();
19806
- if (row.inputs.length === 0) return;
20086
+ if (row.inputs.length === 0 && row.props.length === 0) return;
19807
20087
  this.row = row;
19808
20088
  this.openId = row.id;
19809
20089
  this.snapshot = { ...row.values };
20090
+ this.propSnapshot = { ...row.propValues };
19810
20091
  ensureDialogStyles();
19811
20092
  const t = this.host.theme();
19812
20093
  const border = "var(--vela-border)";
@@ -19814,7 +20095,7 @@ var IndicatorInputsDialog = class {
19814
20095
  const host = this.host.dialogHost() ?? this.host.container;
19815
20096
  const ui = new Dialog({
19816
20097
  host,
19817
- title: row.settingsTitle,
20098
+ title: row.title,
19818
20099
  // Non-modal: live-edit dialog — a modal machine locks pointer events on the
19819
20100
  // whole body, killing the chart and the body-portaled popovers.
19820
20101
  modal: false,
@@ -19846,6 +20127,16 @@ var IndicatorInputsDialog = class {
19846
20127
  text: () => "Close"
19847
20128
  }));
19848
20129
  const tabDefs = tabInputs(row.inputs);
20130
+ if (row.props.length > 0) {
20131
+ const propDecls = row.props.map((p) => {
20132
+ const d = { ...p, prop: true };
20133
+ delete d.tab;
20134
+ return d;
20135
+ });
20136
+ const existing = tabDefs.find((t2) => t2.name === PROPS_TAB);
20137
+ if (existing) existing.inputs.push(...propDecls);
20138
+ else tabDefs.push({ name: PROPS_TAB, inputs: propDecls });
20139
+ }
19849
20140
  const tabs = document.createElement("div");
19850
20141
  tabs.style.cssText = `display:flex;gap:12px;padding:0 20px;border-bottom:1px solid ${border};flex:0 0 auto;`;
19851
20142
  const tabEls = [];
@@ -19948,9 +20239,10 @@ var IndicatorInputsDialog = class {
19948
20239
  this.openId = null;
19949
20240
  this.row = null;
19950
20241
  this.snapshot = null;
20242
+ this.propSnapshot = null;
19951
20243
  ui?.destroy();
19952
20244
  }
19953
- /** Restore every input to its open-time value (re-running the indicator), then close. */
20245
+ /** Restore every input and prop to its open-time value (re-running the indicator), then close. */
19954
20246
  revertAndClose() {
19955
20247
  const row = this.row;
19956
20248
  const snap = this.snapshot;
@@ -19964,6 +20256,17 @@ var IndicatorInputsDialog = class {
19964
20256
  }
19965
20257
  }
19966
20258
  }
20259
+ const propSnap = this.propSnapshot;
20260
+ if (row && propSnap) {
20261
+ for (const p of row.props) {
20262
+ const snapped = propSnap[p.key];
20263
+ const before = snapped !== void 0 ? snapped : p.defval;
20264
+ if (row.propValues[p.key] !== before) {
20265
+ row.propValues[p.key] = before;
20266
+ this.host.onChange?.({ indicatorId: row.id, key: p.key, value: before, kind: "prop" });
20267
+ }
20268
+ }
20269
+ }
19967
20270
  this.close();
19968
20271
  }
19969
20272
  /** The footer's reset button — same chip as Cancel, pinned to the LEFT edge
@@ -19981,19 +20284,27 @@ var IndicatorInputsDialog = class {
19981
20284
  const row = this.row;
19982
20285
  if (!row) return;
19983
20286
  const snap = this.snapshot;
20287
+ const propSnap = this.propSnapshot;
19984
20288
  for (const inp of row.inputs) {
19985
20289
  if (row.values[inp.key] !== inp.defval) {
19986
20290
  row.values[inp.key] = inp.defval;
19987
20291
  this.host.onChange?.({ indicatorId: row.id, key: inp.key, value: inp.defval });
19988
20292
  }
19989
20293
  }
20294
+ for (const p of row.props) {
20295
+ if (row.propValues[p.key] !== p.defval) {
20296
+ row.propValues[p.key] = p.defval;
20297
+ this.host.onChange?.({ indicatorId: row.id, key: p.key, value: p.defval, kind: "prop" });
20298
+ }
20299
+ }
19990
20300
  this.open(row);
19991
20301
  if (snap) this.snapshot = snap;
20302
+ if (propSnap) this.propSnapshot = propSnap;
19992
20303
  }
19993
20304
  /** Write one edit through: store it, notify the host, and re-apply the `when` gates. */
19994
- commit(row, key, value) {
19995
- row.values[key] = value;
19996
- this.host.onChange?.({ indicatorId: row.id, key, value });
20305
+ commit(row, decl, value) {
20306
+ bagOf(row, decl)[decl.key] = value;
20307
+ this.host.onChange?.({ indicatorId: row.id, key: decl.key, value, ...decl.prop ? { kind: "prop" } : {} });
19997
20308
  this.refreshVisibility?.();
19998
20309
  }
19999
20310
  /** One settings row (or several `inline=` inputs) placed into a section's grid. */
@@ -20020,7 +20331,7 @@ var IndicatorInputsDialog = class {
20020
20331
  const id = idOf(lead);
20021
20332
  const info = lead.tooltip ? this.infoButton(lead.tooltip) : void 0;
20022
20333
  if (lead.type === "bool") {
20023
- const current = Boolean(row.values[lead.key] ?? lead.defval);
20334
+ const current = Boolean(bagOf(row, lead)[lead.key] ?? lead.defval);
20024
20335
  return append(fieldRow({
20025
20336
  label: nameOf(lead),
20026
20337
  id,
@@ -20029,7 +20340,7 @@ var IndicatorInputsDialog = class {
20029
20340
  toggle: {
20030
20341
  id,
20031
20342
  checked: current,
20032
- onChange: (v) => this.commit(row, lead.key, v)
20343
+ onChange: (v) => this.commit(row, lead, v)
20033
20344
  }
20034
20345
  }));
20035
20346
  }
@@ -20054,8 +20365,8 @@ var IndicatorInputsDialog = class {
20054
20365
  }
20055
20366
  /** Build the typed control for one input, committing edits live via `onChange`. */
20056
20367
  buildControl(row, inp, id) {
20057
- const current = row.values[inp.key] ?? inp.defval;
20058
- const emit = (value) => this.commit(row, inp.key, value);
20368
+ const current = bagOf(row, inp)[inp.key] ?? inp.defval;
20369
+ const emit = (value) => this.commit(row, inp, value);
20059
20370
  if (inp.type === "bool") {
20060
20371
  return buildFieldControl({ kind: "switch", id, checked: Boolean(current), onChange: (v) => emit(v) }).el;
20061
20372
  }
@@ -20066,7 +20377,7 @@ var IndicatorInputsDialog = class {
20066
20377
  kind: "color",
20067
20378
  id,
20068
20379
  theme: this.host.theme(),
20069
- get: () => String(row.values[inp.key] ?? inp.defval),
20380
+ get: () => String(bagOf(row, inp)[inp.key] ?? inp.defval),
20070
20381
  onChange: (v) => emit(v)
20071
20382
  }).el;
20072
20383
  }
@@ -20629,6 +20940,7 @@ function timeParts(ts) {
20629
20940
  }
20630
20941
 
20631
20942
  // src/renderers/shared/InputsUI.ts
20943
+ var LEGEND_AT_TOP_ATTR = "data-vela-pane-at-top";
20632
20944
  var STATUS_KEYFRAMES = "@keyframes vela-ind-pulse{0%,100%{opacity:1;transform:scale(1)}50%{opacity:.3;transform:scale(.6)}}";
20633
20945
  var LEGEND_ICON_PX2 = 16;
20634
20946
  var LEGEND_CTL_PX = 18;
@@ -20667,10 +20979,14 @@ var InputsUI = class {
20667
20979
  this.moveApi = null;
20668
20980
  /** Host-contributed legend actions, resolved PER ROW at render time (see setLegendActions). */
20669
20981
  this.legendActions = null;
20982
+ /** Host-contributed callout bubbles, resolved PER ROW at render time (see setLegendCallouts). */
20983
+ this.legendCallouts = null;
20670
20984
  /** Chrome-tooltip disposers, per row id — a tip open at removal must not outlive its row. */
20671
20985
  this.rowTips = /* @__PURE__ */ new Map();
20672
20986
  /** Same, for the contributed extras only (rebuilt independently by setLegendActions). */
20673
20987
  this.extrasTips = /* @__PURE__ */ new Map();
20988
+ /** Same, for the contributed callouts only (rebuilt independently by setLegendCallouts). */
20989
+ this.calloutTips = /* @__PURE__ */ new Map();
20674
20990
  /** Open "Move to" menu (kept so it can be torn down). */
20675
20991
  this.moveMenu = null;
20676
20992
  this.moveTargets = [];
@@ -20828,6 +21144,42 @@ var InputsUI = class {
20828
21144
  extrasEl.appendChild(btn2);
20829
21145
  }
20830
21146
  }
21147
+ /**
21148
+ * Wire the host-contributed callout bubbles. Same contract as
21149
+ * {@link setLegendActions}: re-calling replaces the provider and re-projects the
21150
+ * rows already on screen.
21151
+ */
21152
+ setLegendCallouts(provider) {
21153
+ this.legendCallouts = provider;
21154
+ for (const row of this.rows.values()) this.renderCallouts(row);
21155
+ }
21156
+ /** (Re)build one row's callout bubbles — tinted icon circles beside the title whose
21157
+ * click (when the view carries content) deploys a panel of text and actions. */
21158
+ renderCallouts(row) {
21159
+ this.disposeTips(this.calloutTips, row.id);
21160
+ for (const bubble of row.callouts) bubble.destroy();
21161
+ row.callouts = [];
21162
+ row.calloutsEl.replaceChildren();
21163
+ const views = this.legendCallouts?.(row.id) ?? [];
21164
+ row.calloutsEl.style.display = views.length > 0 ? "inline-flex" : "none";
21165
+ for (const view of views) {
21166
+ const bubble = new CalloutBubble({
21167
+ icon: view.icon,
21168
+ background: view.background,
21169
+ ...view.color !== void 0 ? { color: view.color } : {},
21170
+ label: view.tooltip,
21171
+ ...view.content !== void 0 ? { panel: view.content } : {},
21172
+ // The panel portals into the plot host and self-tokens: it must work on
21173
+ // a bare chart, where no `.vela-ui` token ancestor exists.
21174
+ host: this.container,
21175
+ theme: () => this.theme
21176
+ });
21177
+ bubble.el.dataset.legendCallout = view.id;
21178
+ this.tip(this.calloutTips, row.id, bubble.el, view.tooltip);
21179
+ row.callouts.push(bubble);
21180
+ row.calloutsEl.appendChild(bubble.el);
21181
+ }
21182
+ }
20831
21183
  /** Reposition the per-pane legend containers after a layout change. */
20832
21184
  reposition() {
20833
21185
  for (const [paneId, lg] of this.legends) this.positionLegend(lg, paneId);
@@ -20995,6 +21347,7 @@ var InputsUI = class {
20995
21347
  }
20996
21348
  positionLegend(lg, paneId) {
20997
21349
  const bounds = this.paneBoundsOf ? this.paneBoundsOf(paneId) : { top: 0, height: Infinity };
21350
+ lg.toggleAttribute(LEGEND_AT_TOP_ATTR, bounds.top === 0);
20998
21351
  lg.style.display = bounds.height < 4 || !this.titlesVisible ? "none" : "flex";
20999
21352
  const collapsed = this.paneCollapse.has(paneId);
21000
21353
  const masterId = this.paneCollapse.get(paneId) ?? null;
@@ -21070,13 +21423,13 @@ var InputsUI = class {
21070
21423
  }
21071
21424
  /** Create or update an indicator's legend row (in the legend for its pane). */
21072
21425
  upsert(id, title, inputs, values, paneId = "price", opts = {}) {
21073
- const settingsTitle = opts.settingsTitle ?? title;
21074
21426
  const existing = this.rows.get(id);
21075
21427
  if (existing) {
21076
21428
  existing.title = title;
21077
- existing.settingsTitle = settingsTitle;
21078
21429
  existing.inputs = inputs;
21079
21430
  existing.values = { ...values };
21431
+ existing.props = opts.props ?? [];
21432
+ existing.propValues = { ...opts.propValues ?? {} };
21080
21433
  existing.titleEl.textContent = title;
21081
21434
  if (existing.paneId !== paneId) {
21082
21435
  existing.paneId = paneId;
@@ -21128,6 +21481,9 @@ var InputsUI = class {
21128
21481
  titleWrap.appendChild(beta);
21129
21482
  }
21130
21483
  el.appendChild(titleWrap);
21484
+ const calloutsEl = document.createElement("span");
21485
+ calloutsEl.style.cssText = `display:none;align-items:center;gap:4px;flex:none;margin-left:${LEGEND_TITLE_STATUS_GAP_PX}px;`;
21486
+ el.appendChild(calloutsEl);
21131
21487
  el.appendChild(statusEl);
21132
21488
  const valuesEl = document.createElement("span");
21133
21489
  valuesEl.style.cssText = `display:none;align-items:center;gap:5px;margin-left:${LEGEND_TITLE_VALUES_GAP_PX}px;white-space:nowrap;font-variant-numeric:tabular-nums;`;
@@ -21144,13 +21500,13 @@ var InputsUI = class {
21144
21500
  eye.className = "vela-ind-ctl";
21145
21501
  eye.style.cssText = LEGEND_CTL_CSS;
21146
21502
  eye.addEventListener("click", () => {
21147
- const row = this.rows.get(id);
21148
- this.onToggleVisible?.(id, Boolean(row?.hidden));
21503
+ const row2 = this.rows.get(id);
21504
+ this.onToggleVisible?.(id, Boolean(row2?.hidden));
21149
21505
  });
21150
21506
  controlsEl.appendChild(eye);
21151
21507
  eyeEl = eye;
21152
21508
  }
21153
- if (inputs.length > 0) {
21509
+ if (inputs.length > 0 || (opts.props ?? []).length > 0) {
21154
21510
  const gear = document.createElement("button");
21155
21511
  gear.type = "button";
21156
21512
  gear.setAttribute("aria-label", "Settings");
@@ -21190,7 +21546,9 @@ var InputsUI = class {
21190
21546
  controlsEl.appendChild(close);
21191
21547
  el.appendChild(controlsEl);
21192
21548
  this.attach(this.legendFor(paneId), el, !!opts.native);
21193
- this.rows.set(id, { id, title, settingsTitle, inputs, values: { ...values }, el, titleEl, statusEl, valuesEl, plotValues: [], plotValuesKey: "", showValues: null, highlighted: false, paneId, hidden: false, eyeEl, controlsEl, extrasEl, native: !!opts.native });
21549
+ const row = { id, title, inputs, values: { ...values }, props: opts.props ?? [], propValues: { ...opts.propValues ?? {} }, el, titleEl, statusEl, valuesEl, plotValues: [], plotValuesKey: "", showValues: null, highlighted: false, paneId, hidden: false, eyeEl, controlsEl, extrasEl, calloutsEl, callouts: [], native: !!opts.native };
21550
+ this.rows.set(id, row);
21551
+ this.renderCallouts(row);
21194
21552
  this.syncFoldToggle();
21195
21553
  }
21196
21554
  /** Place a row in its pane's legend — native rows PREPEND (pinned to the top), Pine rows append. */
@@ -21199,9 +21557,11 @@ var InputsUI = class {
21199
21557
  else container.appendChild(el);
21200
21558
  }
21201
21559
  /** Reflect programmatic input changes (so a re-opened dialog shows current values). */
21202
- setValues(id, values) {
21560
+ setValues(id, values, props) {
21203
21561
  const row = this.rows.get(id);
21204
- if (row) row.values = { ...row.values, ...values };
21562
+ if (!row) return;
21563
+ row.values = { ...row.values, ...values };
21564
+ if (props) row.propValues = { ...row.propValues, ...props };
21205
21565
  }
21206
21566
  /**
21207
21567
  * Reflect an indicator's live status in its legend row: `'loading'` shows three pulsing
@@ -21283,8 +21643,13 @@ var InputsUI = class {
21283
21643
  syncRowActions(row) {
21284
21644
  const open2 = row.highlighted;
21285
21645
  row.controlsEl.style.display = open2 || row.hidden ? "inline-flex" : "none";
21286
- if (open2) row.el.appendChild(row.statusEl);
21287
- else row.el.insertBefore(row.statusEl, row.valuesEl);
21646
+ if (open2) {
21647
+ row.el.appendChild(row.statusEl);
21648
+ row.el.appendChild(row.calloutsEl);
21649
+ } else {
21650
+ row.el.insertBefore(row.statusEl, row.valuesEl);
21651
+ row.el.insertBefore(row.calloutsEl, row.statusEl);
21652
+ }
21288
21653
  for (const child of Array.from(row.controlsEl.children)) {
21289
21654
  if (!(child instanceof HTMLElement) || child === row.eyeEl) continue;
21290
21655
  if (child === row.extrasEl) {
@@ -21302,10 +21667,12 @@ var InputsUI = class {
21302
21667
  }
21303
21668
  remove(id) {
21304
21669
  const row = this.rows.get(id);
21670
+ for (const bubble of row?.callouts ?? []) bubble.destroy();
21305
21671
  row?.el.remove();
21306
21672
  this.rows.delete(id);
21307
21673
  this.disposeTips(this.rowTips, id);
21308
21674
  this.disposeTips(this.extrasTips, id);
21675
+ this.disposeTips(this.calloutTips, id);
21309
21676
  if (this.selectedId === id) this.selectedId = null;
21310
21677
  if (this.inputsDialog.openId === id) this.inputsDialog.close();
21311
21678
  this.syncFoldToggle();
@@ -21325,6 +21692,8 @@ var InputsUI = class {
21325
21692
  this.rowMenu = null;
21326
21693
  for (const id of [...this.rowTips.keys()]) this.disposeTips(this.rowTips, id);
21327
21694
  for (const id of [...this.extrasTips.keys()]) this.disposeTips(this.extrasTips, id);
21695
+ for (const id of [...this.calloutTips.keys()]) this.disposeTips(this.calloutTips, id);
21696
+ for (const row of this.rows.values()) for (const bubble of row.callouts) bubble.destroy();
21328
21697
  for (const lg of this.legends.values()) lg.remove();
21329
21698
  this.legends.clear();
21330
21699
  this.rows.clear();
@@ -21679,6 +22048,32 @@ var SIZE_PX3 = {
21679
22048
  large: 16,
21680
22049
  huge: 20
21681
22050
  };
22051
+ function fontPxOf(size) {
22052
+ if (typeof size === "number") return size > 0 ? size : SIZE_PX3.auto;
22053
+ return SIZE_PX3[size] ?? SIZE_PX3.auto;
22054
+ }
22055
+ function tableHasContent(t) {
22056
+ return t.cells.some((row) => row?.some((c) => c != null && !c.merged));
22057
+ }
22058
+ function mergeRenderPlan(t) {
22059
+ const span = /* @__PURE__ */ new Map();
22060
+ const omit = /* @__PURE__ */ new Set();
22061
+ for (const m of t.merges) {
22062
+ span.set(`${m.startRow}:${m.startCol}`, { cs: m.endCol - m.startCol + 1, rs: m.endRow - m.startRow + 1 });
22063
+ for (let r = m.startRow; r <= m.endRow; r += 1) {
22064
+ for (let c = m.startCol; c <= m.endCol; c += 1) {
22065
+ if (r !== m.startRow || c !== m.startCol) omit.add(`${r}:${c}`);
22066
+ }
22067
+ }
22068
+ }
22069
+ for (let r = 0; r < t.rows; r += 1) {
22070
+ for (let c = 0; c < t.columns; c += 1) {
22071
+ if (t.cells[r]?.[c]?.merged && !span.has(`${r}:${c}`)) omit.add(`${r}:${c}`);
22072
+ }
22073
+ }
22074
+ for (const key of span.keys()) omit.delete(key);
22075
+ return { span, omit };
22076
+ }
21682
22077
  var TableOverlay = class {
21683
22078
  constructor(container, theme, paneBounds) {
21684
22079
  this.container = container;
@@ -21699,7 +22094,9 @@ var TableOverlay = class {
21699
22094
  update(tables) {
21700
22095
  this.lastTables = tables;
21701
22096
  this.root.replaceChildren();
21702
- for (const t of tables) this.root.appendChild(this.renderTable(t));
22097
+ for (const t of tables) {
22098
+ if (tableHasContent(t)) this.root.appendChild(this.renderTable(t));
22099
+ }
21703
22100
  }
21704
22101
  /** Re-render at the current pane geometry — after layout settles or on resize. */
21705
22102
  reposition() {
@@ -21714,39 +22111,36 @@ var TableOverlay = class {
21714
22111
  this.root.remove();
21715
22112
  }
21716
22113
  renderTable(t) {
22114
+ const b = this.paneBounds(t.paneId);
21717
22115
  const wrap = document.createElement("div");
21718
22116
  wrap.style.position = "absolute";
21719
- this.anchor(wrap, t.position, this.paneBounds(t.paneId));
22117
+ if (t.frameColor && t.frameWidth > 0) wrap.style.border = `${t.frameWidth}px solid ${t.frameColor}`;
22118
+ this.anchor(wrap, t.position, b);
21720
22119
  const table = document.createElement("table");
21721
22120
  Object.assign(table.style, {
21722
22121
  borderCollapse: "collapse",
21723
22122
  background: t.bgColor ?? "transparent",
21724
22123
  fontFamily: this.theme.fontFamily || "sans-serif",
21725
- // Frame as an inset shadow (not a `border`) so it stays independent of the
21726
- // collapsed cell borders even when frame_width != border_width.
21727
- boxShadow: t.frameColor && t.frameWidth > 0 ? `inset 0 0 0 ${t.frameWidth}px ${t.frameColor}` : "none",
21728
22124
  border: "none",
21729
22125
  tableLayout: "auto",
21730
22126
  // Re-enable pointer events on the table only (root is none) so cell tooltips work.
21731
22127
  pointerEvents: "auto"
21732
22128
  });
21733
- const span = /* @__PURE__ */ new Map();
21734
- const skip = /* @__PURE__ */ new Set();
21735
- for (const m of t.merges) {
21736
- span.set(`${m.startRow}:${m.startCol}`, { cs: m.endCol - m.startCol + 1, rs: m.endRow - m.startRow + 1 });
21737
- for (let r = m.startRow; r <= m.endRow; r += 1) {
21738
- for (let c = m.startCol; c <= m.endCol; c += 1) {
21739
- if (r !== m.startRow || c !== m.startCol) skip.add(`${r}:${c}`);
21740
- }
21741
- }
21742
- }
22129
+ const { span, omit } = mergeRenderPlan(t);
22130
+ const plotW = Math.max(0, (this.root.clientWidth || this.container.clientWidth) - b.rightAxis);
22131
+ const paneH = b.height;
21743
22132
  const cellBorder = t.borderColor && t.borderWidth > 0 ? `${t.borderWidth}px solid ${t.borderColor}` : "none";
21744
22133
  for (let r = 0; r < t.rows; r += 1) {
21745
22134
  const tr = document.createElement("tr");
21746
22135
  for (let c = 0; c < t.columns; c += 1) {
22136
+ if (omit.has(`${r}:${c}`)) continue;
21747
22137
  const cell = t.cells[r]?.[c] ?? null;
21748
- if (skip.has(`${r}:${c}`) || cell?.merged) continue;
21749
22138
  const td = document.createElement("td");
22139
+ if (cell === null) {
22140
+ Object.assign(td.style, { padding: "0", border: "none" });
22141
+ tr.appendChild(td);
22142
+ continue;
22143
+ }
21750
22144
  const sp = span.get(`${r}:${c}`);
21751
22145
  if (sp) {
21752
22146
  if (sp.cs > 1) td.colSpan = sp.cs;
@@ -21755,18 +22149,22 @@ var TableOverlay = class {
21755
22149
  Object.assign(td.style, {
21756
22150
  border: cellBorder,
21757
22151
  padding: "2px 6px",
21758
- background: cell?.bgColor ?? "transparent",
21759
- color: cell?.textColor ?? this.theme.textColor,
21760
- textAlign: cell?.hAlign ?? "center",
21761
- verticalAlign: cell?.vAlign === "top" ? "top" : cell?.vAlign === "bottom" ? "bottom" : "middle",
21762
- fontSize: `${SIZE_PX3[cell?.textSize ?? "normal"]}px`,
21763
- fontFamily: cell?.fontFamily === "monospace" ? "monospace" : "inherit",
21764
- fontWeight: cell?.bold ? "bold" : "normal",
21765
- fontStyle: cell?.italic ? "italic" : "normal",
21766
- whiteSpace: "pre-line"
22152
+ background: cell.bgColor ?? "transparent",
22153
+ color: cell.textColor ?? this.theme.textColor,
22154
+ textAlign: cell.hAlign,
22155
+ verticalAlign: cell.vAlign === "top" ? "top" : cell.vAlign === "bottom" ? "bottom" : "middle",
22156
+ fontSize: `${fontPxOf(cell.textSize)}px`,
22157
+ fontFamily: cell.fontFamily === "monospace" ? "monospace" : "inherit",
22158
+ fontWeight: cell.bold ? "bold" : "normal",
22159
+ fontStyle: cell.italic ? "italic" : "normal",
22160
+ // Pine cell text never wraps; `\n` still breaks lines. Wrapping
22161
+ // used to collapse unicode sparklines and ━━━ dividers.
22162
+ whiteSpace: "pre"
21767
22163
  });
21768
- if (cell?.tooltip) td.title = cell.tooltip;
21769
- td.textContent = cell?.text ?? "";
22164
+ if (cell.width) td.style.width = `${cell.width / 100 * plotW}px`;
22165
+ if (cell.height) td.style.height = `${cell.height / 100 * paneH}px`;
22166
+ if (cell.tooltip) td.title = cell.tooltip;
22167
+ td.textContent = cell.text ?? "";
21770
22168
  tr.appendChild(td);
21771
22169
  }
21772
22170
  table.appendChild(tr);
@@ -22368,8 +22766,14 @@ var WebGL2Backend = class {
22368
22766
  return sc === pane.scale ? pane : { ...pane, scale: sc };
22369
22767
  };
22370
22768
  b.alpha = this.modelAlpha;
22371
- for (const m of models) for (const bgSpan of m.backgrounds) this.emitBackground(b, bgSpan, pane, coords);
22372
- for (const m of models) for (const f of m.fills) this.emitFill(b, m, f, effPane(m), coords, i0, i1, scene.offsetOf(m.id));
22769
+ for (const m of models) for (const bgSpan of m.backgrounds) if (bgSpan.overlay !== true) this.emitBackground(b, bgSpan, pane, coords);
22770
+ for (const m of models) for (const f of m.fills) if (f.overlay !== true) this.emitFill(b, m, f, effPane(m), coords, i0, i1, scene.offsetOf(m.id));
22771
+ if (isPrice) {
22772
+ for (const m of scene.indicators.values()) {
22773
+ for (const bgSpan of m.backgrounds) if (bgSpan.overlay === true) this.emitBackground(b, bgSpan, pane, coords);
22774
+ for (const f of m.fills) if (f.overlay === true) this.emitFill(b, m, f, pane, coords, i0, i1, scene.offsetOf(m.id));
22775
+ }
22776
+ }
22373
22777
  const drawCandles = isPrice && !scene.candlesHidden;
22374
22778
  let candleDrawn = false;
22375
22779
  for (const m of models) {
@@ -22383,7 +22787,7 @@ var WebGL2Backend = class {
22383
22787
  b.alpha = this.modelAlpha;
22384
22788
  const off = scene.offsetOf(m.id);
22385
22789
  const mp = effPane(m);
22386
- for (const s of m.series) this.emitSeries(b, s, mp, coords, i0, i1, theme, off);
22790
+ for (const s of m.series) if (s.overlay !== true) this.emitSeries(b, s, mp, coords, i0, i1, theme, off);
22387
22791
  }
22388
22792
  if (drawCandles && !candleDrawn) {
22389
22793
  drawSlicesUpTo(scene.candleZ);
@@ -22392,6 +22796,12 @@ var WebGL2Backend = class {
22392
22796
  }
22393
22797
  drawSlicesUpTo(Infinity);
22394
22798
  b.alpha = this.modelAlpha;
22799
+ if (isPrice) {
22800
+ for (const m of scene.indicators.values()) {
22801
+ const off = scene.offsetOf(m.id);
22802
+ for (const s of m.series) if (s.overlay === true) this.emitSeries(b, s, pane, coords, i0, i1, theme, off);
22803
+ }
22804
+ }
22395
22805
  for (const m of models) {
22396
22806
  const mp = effPane(m);
22397
22807
  for (const pl of m.priceLines) this.emitHline(b, pl, mp, coords, dataW, theme);
@@ -22438,7 +22848,7 @@ var WebGL2Backend = class {
22438
22848
  for (const pane of scene.orderedPanes()) {
22439
22849
  const b = this.glowBatch;
22440
22850
  b.reset();
22441
- this.emitGlowSources(b, scene.indicatorsForPane(pane.id), pane, coords, i0, i1, (id) => scene.offsetOf(id));
22851
+ this.emitGlowSources(b, scene, pane, coords, i0, i1);
22442
22852
  if (b.vertexCount === 0) continue;
22443
22853
  const topH = Math.round(pane.bounds.top * dpr * 0.5);
22444
22854
  const botH = Math.round((pane.bounds.top + pane.bounds.height) * dpr * 0.5);
@@ -22507,15 +22917,23 @@ var WebGL2Backend = class {
22507
22917
  gl.bindVertexArray(null);
22508
22918
  return true;
22509
22919
  }
22510
- /** The "neon" elements that glow: line/area/step lines + point markers (not candles/fills/bars). */
22511
- emitGlowSources(b, models, pane, coords, i0, i1, offsetOf = () => 0) {
22512
- for (const m of models) {
22513
- const off = offsetOf(m.id);
22514
- for (const s of m.series) {
22515
- if (!isLineLikeSeries(s) || s.visible === false) continue;
22516
- if (s.kind === "histogram" || s.kind === "columns") continue;
22517
- if (s.kind === "circles" || s.kind === "cross") this.emitPointMarkers(b, s, pane, coords, i0, i1, off);
22518
- else this.emitPolyline(b, s, pane, coords, i0, i1, s.kind === "step", off);
22920
+ /** The "neon" elements that glow: line/area/step lines + point markers (not candles/fills/bars).
22921
+ * Routing mirrors the main pass: own series per pane, force_overlay series on the price pane. */
22922
+ emitGlowSources(b, scene, pane, coords, i0, i1) {
22923
+ const emitOne = (s, off) => {
22924
+ if (!isLineLikeSeries(s) || s.visible === false) return;
22925
+ if (s.kind === "histogram" || s.kind === "columns") return;
22926
+ if (s.kind === "circles" || s.kind === "cross") this.emitPointMarkers(b, s, pane, coords, i0, i1, off);
22927
+ else this.emitPolyline(b, s, pane, coords, i0, i1, s.kind === "step", off);
22928
+ };
22929
+ for (const m of scene.indicatorsForPane(pane.id)) {
22930
+ const off = scene.offsetOf(m.id);
22931
+ for (const s of m.series) if (s.overlay !== true) emitOne(s, off);
22932
+ }
22933
+ if (pane.kind === "price") {
22934
+ for (const m of scene.indicators.values()) {
22935
+ const off = scene.offsetOf(m.id);
22936
+ for (const s of m.series) if (s.overlay === true) emitOne(s, off);
22519
22937
  }
22520
22938
  }
22521
22939
  }
@@ -22798,9 +23216,9 @@ var WebGL2Backend = class {
22798
23216
  b.alpha = this.candleBodyAlpha;
22799
23217
  b.rect(g.bodyX, bodyTop, g.bodyW, bodyH, c);
22800
23218
  }
22801
- if (cs.borderVisible || bc || fading && cs.bodyVisible) {
23219
+ if (cs.borderVisible || fading && cs.bodyVisible) {
22802
23220
  b.alpha = this.candleStructureAlpha;
22803
- const bord = cs.borderVisible || bc ? parseColor((isUp ? cs.borderUpColor : cs.borderDownColor) ?? dir) : c;
23221
+ const bord = cs.borderVisible ? parseColor((isUp ? cs.borderUpColor : cs.borderDownColor) ?? bodyColorStr) : c;
22804
23222
  const bw = Math.max(0, g.bodyW - 1);
22805
23223
  const bh = Math.max(0, bodyH - 1);
22806
23224
  b.rectStroke(g.bodyX + 0.5, bodyTop + 0.5, bw, bh, 1, bord);
@@ -24352,8 +24770,14 @@ var Canvas2dBackend = class {
24352
24770
  return sc === pane.scale ? pane : { ...pane, scale: sc };
24353
24771
  };
24354
24772
  ctx.globalAlpha = this.modelAlpha;
24355
- for (const m of models) for (const bg of m.backgrounds) this.drawBackground(ctx, bg, pane, coords);
24356
- for (const m of models) for (const f of m.fills) this.drawFill(ctx, m, f, effPane(m), coords, i0, i1, scene.offsetOf(m.id));
24773
+ for (const m of models) for (const bg of m.backgrounds) if (bg.overlay !== true) this.drawBackground(ctx, bg, pane, coords);
24774
+ for (const m of models) for (const f of m.fills) if (f.overlay !== true) this.drawFill(ctx, m, f, effPane(m), coords, i0, i1, scene.offsetOf(m.id));
24775
+ if (isPrice) {
24776
+ for (const m of scene.indicators.values()) {
24777
+ for (const bg of m.backgrounds) if (bg.overlay === true) this.drawBackground(ctx, bg, pane, coords);
24778
+ for (const f of m.fills) if (f.overlay === true) this.drawFill(ctx, m, f, pane, coords, i0, i1, scene.offsetOf(m.id));
24779
+ }
24780
+ }
24357
24781
  const slices = scene.drawingSlices.get(pane.id) ?? [];
24358
24782
  let si = 0;
24359
24783
  const drawSlicesUpTo = (z) => {
@@ -24375,7 +24799,7 @@ var Canvas2dBackend = class {
24375
24799
  ctx.globalAlpha = this.modelAlpha;
24376
24800
  const off = scene.offsetOf(m.id);
24377
24801
  const mp = effPane(m);
24378
- for (const s of m.series) this.drawSeries(ctx, s, mp, coords, i0, i1, theme, off);
24802
+ for (const s of m.series) if (s.overlay !== true) this.drawSeries(ctx, s, mp, coords, i0, i1, theme, off);
24379
24803
  }
24380
24804
  if (drawCandles && !candleDrawn) {
24381
24805
  drawSlicesUpTo(scene.candleZ);
@@ -24383,6 +24807,13 @@ var Canvas2dBackend = class {
24383
24807
  this.drawPriceSeries(ctx, scene, i0, i1, coords, pane, theme, barColorMap, dataW);
24384
24808
  }
24385
24809
  drawSlicesUpTo(Infinity);
24810
+ if (isPrice) {
24811
+ ctx.globalAlpha = this.modelAlpha;
24812
+ for (const m of scene.indicators.values()) {
24813
+ const off = scene.offsetOf(m.id);
24814
+ for (const s of m.series) if (s.overlay === true) this.drawSeries(ctx, s, pane, coords, i0, i1, theme, off);
24815
+ }
24816
+ }
24386
24817
  ctx.globalAlpha = this.modelAlpha;
24387
24818
  for (const m of models) {
24388
24819
  const mp = effPane(m);
@@ -24654,9 +25085,9 @@ var Canvas2dBackend = class {
24654
25085
  ctx.fillStyle = color;
24655
25086
  ctx.fillRect(g.bodyX, top, g.bodyW, bodyH);
24656
25087
  }
24657
- if (cs.borderVisible || bc || fading && cs.bodyVisible) {
25088
+ if (cs.borderVisible || fading && cs.bodyVisible) {
24658
25089
  ctx.globalAlpha = this.candleStructureAlpha;
24659
- ctx.strokeStyle = cs.borderVisible || bc ? (up ? cs.borderUpColor : cs.borderDownColor) ?? dir : color;
25090
+ ctx.strokeStyle = cs.borderVisible ? (up ? cs.borderUpColor : cs.borderDownColor) ?? color : color;
24660
25091
  ctx.lineWidth = 1;
24661
25092
  const bw = Math.max(0, g.bodyW - 1);
24662
25093
  const bh = Math.max(0, bodyH - 1);
@@ -25088,6 +25519,8 @@ var DrawingSceneRenderer = class {
25088
25519
  this.set = set;
25089
25520
  /** measureText width cache, keyed by `${font} ${text}`, persists across frames. */
25090
25521
  this.widthCache = /* @__PURE__ */ new Map();
25522
+ /** Tooltip hit-rects captured while drawing the CURRENT set (rebuilt per render). */
25523
+ this.tipRegions = [];
25091
25524
  /**
25092
25525
  * Index offset of the CURRENT set's model: its `xloc:'bar_index'` coordinates count
25093
25526
  * from the model's anchor bar, so they shift by this to land on chart logical indices.
@@ -25108,8 +25541,13 @@ var DrawingSceneRenderer = class {
25108
25541
  const s = this.set;
25109
25542
  return !s.lines.length && !s.boxes.length && !s.labels.length && !s.polylines.length && !s.linefills.length;
25110
25543
  }
25544
+ /** Tooltip hit-rects of the labels drawn by the LAST `render` call (same coords as `ctx`). */
25545
+ labelTipRegions() {
25546
+ return this.tipRegions;
25547
+ }
25111
25548
  /** Draw the whole set into `ctx` using the supplied coordinate closures. */
25112
25549
  render(ctx, W, H, xOf, yOf) {
25550
+ this.tipRegions = [];
25113
25551
  if (this.isEmpty()) return;
25114
25552
  ctx.save();
25115
25553
  this.drawLinefills(ctx, W, H, xOf, yOf);
@@ -25457,13 +25895,35 @@ var DrawingSceneRenderer = class {
25457
25895
  if (this.isPointShape(lb.style)) {
25458
25896
  if (!lb.noFill) this.drawLabelShape(ctx, lb.style, px, py, fontPx, color);
25459
25897
  if (lb.text) this.drawLabelText(ctx, lb, px, py + fontPx, fontPx);
25460
- } else if (lb.style === "none" || lb.style === "text_outline" || lb.noFill) {
25461
- if (lb.text) this.drawLabelText(ctx, lb, px, py, fontPx, lb.style === "text_outline");
25898
+ if (lb.tooltip) {
25899
+ const r = Math.max(4, fontPx * 0.6) + 3;
25900
+ this.tipRegions.push({ left: px - r, top: py - r, right: px + r, bottom: py + r, text: lb.tooltip });
25901
+ }
25902
+ } else if (lb.style === "none" || lb.style === "text_outline") {
25903
+ if (lb.text) {
25904
+ this.drawLabelText(ctx, lb, px, py, fontPx, lb.style === "text_outline");
25905
+ if (lb.tooltip) this.tipRegions.push(this.textRegion(ctx, lb, px, py, fontPx, lb.tooltip));
25906
+ }
25462
25907
  } else {
25463
- this.drawBubble(ctx, lb, px, py, fontPx, color);
25908
+ const r = this.drawBubble(ctx, lb, px, py, fontPx, color);
25909
+ if (lb.tooltip) this.tipRegions.push({ left: r.x, top: r.y, right: r.x + r.w, bottom: r.y + r.h, text: lb.tooltip });
25464
25910
  }
25465
25911
  }
25466
25912
  }
25913
+ /** Canvas font for a label's text — family + Pine `text_formatting` (bold/italic). */
25914
+ labelFont(lb, fontPx) {
25915
+ const family = lb.fontFamily === "monospace" ? "monospace" : this.deps.theme.fontFamily || "sans-serif";
25916
+ return `${lb.italic ? "italic " : ""}${lb.bold ? "bold " : ""}${fontPx}px ${family}`;
25917
+ }
25918
+ /** Hover rect of a text-only label (style none/text_outline/noFill), centered like drawLabelText. */
25919
+ textRegion(ctx, lb, cx, cy, fontPx, text) {
25920
+ const font = this.labelFont(lb, fontPx);
25921
+ const lines = (lb.text ?? "").split("\n");
25922
+ const w = Math.max(1, ...lines.map((l) => this.measure(ctx, font, l)));
25923
+ const h = fontPx * 1.25 * lines.length;
25924
+ const left = lb.textAlign === "left" ? cx : lb.textAlign === "right" ? cx - w : cx - w / 2;
25925
+ return { left, top: cy - h / 2, right: left + w, bottom: cy + h / 2, text };
25926
+ }
25467
25927
  isPointShape(style) {
25468
25928
  switch (style) {
25469
25929
  case "circle":
@@ -25482,8 +25942,7 @@ var DrawingSceneRenderer = class {
25482
25942
  }
25483
25943
  }
25484
25944
  drawLabelText(ctx, lb, cx, cy, fontPx, outline = false) {
25485
- const family = lb.fontFamily === "monospace" ? "monospace" : this.deps.theme.fontFamily || "sans-serif";
25486
- ctx.font = `${fontPx}px ${family}`;
25945
+ ctx.font = this.labelFont(lb, fontPx);
25487
25946
  ctx.textAlign = lb.textAlign === "left" ? "left" : lb.textAlign === "right" ? "right" : "center";
25488
25947
  ctx.textBaseline = "middle";
25489
25948
  const lines = lb.text.split("\n");
@@ -25563,9 +26022,9 @@ var DrawingSceneRenderer = class {
25563
26022
  ctx.fill();
25564
26023
  }
25565
26024
  }
26025
+ /** Draw a bubble label; returns the bubble body rect (for tooltip hit-testing). */
25566
26026
  drawBubble(ctx, lb, px, py, fontPx, color) {
25567
- const family = lb.fontFamily === "monospace" ? "monospace" : this.deps.theme.fontFamily || "sans-serif";
25568
- ctx.font = `${fontPx}px ${family}`;
26027
+ ctx.font = this.labelFont(lb, fontPx);
25569
26028
  const lines = (lb.text ?? "").split("\n");
25570
26029
  const padX = 6;
25571
26030
  const padY = 4;
@@ -25623,38 +26082,51 @@ var DrawingSceneRenderer = class {
25623
26082
  by = py - h - ptr;
25624
26083
  pointer = "down";
25625
26084
  }
25626
- ctx.fillStyle = color;
25627
- this.roundRect(ctx, bx, by, w, h, 4);
25628
- ctx.fill();
25629
- if (pointer !== "none") {
25630
- ctx.beginPath();
25631
- if (pointer === "down") {
25632
- ctx.moveTo(px - ptr, by + h);
25633
- ctx.lineTo(px + ptr, by + h);
25634
- ctx.lineTo(px, py);
25635
- } else if (pointer === "up") {
25636
- ctx.moveTo(px - ptr, by);
25637
- ctx.lineTo(px + ptr, by);
25638
- ctx.lineTo(px, py);
25639
- } else if (pointer === "left") {
25640
- ctx.moveTo(bx, py - ptr);
25641
- ctx.lineTo(bx, py + ptr);
25642
- ctx.lineTo(px, py);
25643
- } else {
25644
- ctx.moveTo(bx + w, py - ptr);
25645
- ctx.lineTo(bx + w, py + ptr);
25646
- ctx.lineTo(px, py);
25647
- }
25648
- ctx.closePath();
26085
+ if (!lb.noFill) {
26086
+ ctx.fillStyle = color;
26087
+ this.roundRect(ctx, bx, by, w, h, 4);
25649
26088
  ctx.fill();
26089
+ if (pointer !== "none") {
26090
+ ctx.beginPath();
26091
+ if (pointer === "down") {
26092
+ ctx.moveTo(px - ptr, by + h);
26093
+ ctx.lineTo(px + ptr, by + h);
26094
+ ctx.lineTo(px, py);
26095
+ } else if (pointer === "up") {
26096
+ ctx.moveTo(px - ptr, by);
26097
+ ctx.lineTo(px + ptr, by);
26098
+ ctx.lineTo(px, py);
26099
+ } else if (pointer === "left") {
26100
+ ctx.moveTo(bx, py - ptr);
26101
+ ctx.lineTo(bx, py + ptr);
26102
+ ctx.lineTo(px, py);
26103
+ } else {
26104
+ ctx.moveTo(bx + w, py - ptr);
26105
+ ctx.lineTo(bx + w, py + ptr);
26106
+ ctx.lineTo(px, py);
26107
+ }
26108
+ ctx.closePath();
26109
+ ctx.fill();
26110
+ }
25650
26111
  }
25651
26112
  if (lb.text) {
25652
- ctx.fillStyle = lb.textColor ?? contrastColor(color);
25653
- ctx.textAlign = "center";
26113
+ ctx.fillStyle = lb.textColor ?? (lb.noFill ? this.deps.theme.textColor : contrastColor(color));
25654
26114
  ctx.textBaseline = "middle";
26115
+ let tx;
26116
+ if (lb.textAlign === "left") {
26117
+ ctx.textAlign = "left";
26118
+ tx = bx + padX;
26119
+ } else if (lb.textAlign === "right") {
26120
+ ctx.textAlign = "right";
26121
+ tx = bx + w - padX;
26122
+ } else {
26123
+ ctx.textAlign = "center";
26124
+ tx = bx + w / 2;
26125
+ }
25655
26126
  const startY = by + padY + lineH / 2;
25656
- for (let i = 0; i < lines.length; i += 1) ctx.fillText(lines[i], bx + w / 2, startY + i * lineH);
26127
+ for (let i = 0; i < lines.length; i += 1) ctx.fillText(lines[i], tx, startY + i * lineH);
25657
26128
  }
26129
+ return { x: bx, y: by, w, h };
25658
26130
  }
25659
26131
  roundRect(ctx, x, y, w, h, r) {
25660
26132
  const rr = Math.min(r, w / 2, h / 2);
@@ -25892,6 +26364,8 @@ var ChromeRenderer = class {
25892
26364
  this.axisTextColor = DARK_THEME.textColor;
25893
26365
  // Shared Pine-drawing renderer (line/box/label/polyline/linefill); widthCache persists.
25894
26366
  this.drawScene = new DrawingSceneRenderer({ timeToLogical: () => 0, barAt: () => null, theme: {} });
26367
+ // Tooltip hit-rects of every label drawn this frame, in plot coords (rebuilt per render).
26368
+ this.labelTips = [];
25895
26369
  }
25896
26370
  mount(canvas) {
25897
26371
  this.canvas = canvas;
@@ -25933,6 +26407,7 @@ var ChromeRenderer = class {
25933
26407
  const dataW = coords.width;
25934
26408
  const dataH = coords.height;
25935
26409
  this.axisTextColor = surface?.textColor ?? theme.textColor;
26410
+ this.labelTips = [];
25936
26411
  ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
25937
26412
  ctx.clearRect(0, 0, fullW, fullH);
25938
26413
  if (surface && (fullW > dataW || fullH > dataH)) {
@@ -26042,6 +26517,17 @@ var ChromeRenderer = class {
26042
26517
  (price) => coords.priceToY(price, pane.scale, pane.bounds) - pane.bounds.top
26043
26518
  );
26044
26519
  ctx.restore();
26520
+ for (const r of this.drawScene.labelTipRegions()) {
26521
+ this.labelTips.push({ ...r, top: r.top + pane.bounds.top, bottom: r.bottom + pane.bounds.top });
26522
+ }
26523
+ }
26524
+ /** Tooltip of the topmost label under a plot-space point, or null. Fed by the last render. */
26525
+ labelTooltipAt(x, y) {
26526
+ for (let i = this.labelTips.length - 1; i >= 0; i -= 1) {
26527
+ const r = this.labelTips[i];
26528
+ if (x >= r.left && x <= r.right && y >= r.top && y <= r.bottom) return r.text;
26529
+ }
26530
+ return null;
26045
26531
  }
26046
26532
  // ── axes ──
26047
26533
  drawPriceAxes(ctx, scene, coords, theme, dataW, panes) {
@@ -26258,6 +26744,68 @@ function formatCountdown(ms) {
26258
26744
  return h > 0 ? `${h}:${pad(m)}:${pad(s)}` : `${pad(m)}:${pad(s)}`;
26259
26745
  }
26260
26746
 
26747
+ // src/renderers/native/chrome/LabelTooltip.ts
26748
+ var HOVER_DELAY_MS = 350;
26749
+ var LabelTooltip = class {
26750
+ constructor(plot, deps) {
26751
+ this.plot = plot;
26752
+ this.deps = deps;
26753
+ this.tip = null;
26754
+ this.timer = null;
26755
+ /** Text of the currently open OR armed tip — dedupes moves inside one label. */
26756
+ this.current = null;
26757
+ this.onMove = (e) => {
26758
+ if (e.pointerType !== "mouse") return;
26759
+ const rect = this.plot.getBoundingClientRect();
26760
+ const x = e.clientX - rect.left;
26761
+ const y = e.clientY - rect.top;
26762
+ const text = this.deps.lookup(x, y);
26763
+ if (!text) {
26764
+ this.clear();
26765
+ return;
26766
+ }
26767
+ if (text === this.current) return;
26768
+ this.clear();
26769
+ this.current = text;
26770
+ this.timer = window.setTimeout(() => this.show(text, x, y), HOVER_DELAY_MS);
26771
+ };
26772
+ this.onLeave = () => {
26773
+ this.clear();
26774
+ };
26775
+ plot.addEventListener("pointermove", this.onMove);
26776
+ plot.addEventListener("pointerleave", this.onLeave);
26777
+ plot.addEventListener("pointerdown", this.onLeave);
26778
+ }
26779
+ destroy() {
26780
+ this.plot.removeEventListener("pointermove", this.onMove);
26781
+ this.plot.removeEventListener("pointerleave", this.onLeave);
26782
+ this.plot.removeEventListener("pointerdown", this.onLeave);
26783
+ this.clear();
26784
+ }
26785
+ show(text, x, y) {
26786
+ const doc = this.plot.ownerDocument;
26787
+ const tip = doc.createElement("div");
26788
+ tip.textContent = text;
26789
+ tip.style.cssText = "position:absolute;z-index:var(--vela-z-tooltip);pointer-events:none;background:var(--vela-bg);border:1px solid var(--vela-border);color:var(--vela-fg);border-radius:var(--vela-radius-md);padding:4px 9px;box-shadow:var(--vela-shadow);font:var(--vela-font-size-md) var(--vela-font);max-width:260px;";
26790
+ applyChromeTokens(tip, this.deps.theme());
26791
+ this.plot.appendChild(tip);
26792
+ const pw = this.plot.clientWidth;
26793
+ const ph = this.plot.clientHeight;
26794
+ tip.style.left = `${Math.max(0, Math.min(x + 12, pw - tip.offsetWidth - 4))}px`;
26795
+ tip.style.top = `${Math.max(0, Math.min(y + 16, ph - tip.offsetHeight - 4))}px`;
26796
+ this.tip = tip;
26797
+ }
26798
+ clear() {
26799
+ if (this.timer !== null) {
26800
+ clearTimeout(this.timer);
26801
+ this.timer = null;
26802
+ }
26803
+ this.tip?.remove();
26804
+ this.tip = null;
26805
+ this.current = null;
26806
+ }
26807
+ };
26808
+
26261
26809
  // src/renderers/native/chrome/CrosshairRenderer.ts
26262
26810
  var CrosshairRenderer = class {
26263
26811
  constructor() {
@@ -31335,19 +31883,8 @@ function computePaneScale(models, bars, includeCandles, i0, i1, drawings, log =
31335
31883
  for (const model of models) {
31336
31884
  const off = offsetOf(model.id);
31337
31885
  for (const s of model.series) {
31338
- if (s.kind === "candle" || s.kind === "bar") {
31339
- for (let i = i0; i <= i1; i += 1) {
31340
- const b = s.bars[i - off];
31341
- if (b) {
31342
- consider(b.high);
31343
- consider(b.low);
31344
- }
31345
- }
31346
- } else if (isLineLikeSeries(s)) {
31347
- for (let i = i0; i <= i1; i += 1) consider(s.points[i - off]?.value);
31348
- if (s.kind === "histogram" || s.kind === "columns") consider(s.style?.base ?? 0);
31349
- else if (s.style?.base != null) consider(s.style.base);
31350
- }
31886
+ if (s.overlay === true) continue;
31887
+ considerSeries(s, i0, i1, off, consider);
31351
31888
  }
31352
31889
  for (const pl of model.priceLines) consider(pl.price);
31353
31890
  }
@@ -31369,6 +31906,36 @@ function computePaneScale(models, bars, includeCandles, i0, i1, drawings, log =
31369
31906
  const span = max - min;
31370
31907
  return { min: min - span * MARGIN_BOTTOM, max: max + span * MARGIN_TOP };
31371
31908
  }
31909
+ function considerSeries(s, i0, i1, off, consider) {
31910
+ if (s.kind === "candle" || s.kind === "bar") {
31911
+ for (let i = i0; i <= i1; i += 1) {
31912
+ const b = s.bars[i - off];
31913
+ if (b) {
31914
+ consider(b.high);
31915
+ consider(b.low);
31916
+ }
31917
+ }
31918
+ } else if (isLineLikeSeries(s)) {
31919
+ for (let i = i0; i <= i1; i += 1) consider(s.points[i - off]?.value);
31920
+ if (s.kind === "histogram" || s.kind === "columns") consider(s.style?.base ?? 0);
31921
+ else if (s.style?.base != null) consider(s.style.base);
31922
+ }
31923
+ }
31924
+ function overlaySeriesRange(models, i0, i1, offsetOf = () => 0) {
31925
+ let min = Infinity;
31926
+ let max = -Infinity;
31927
+ const consider = (v) => {
31928
+ if (v != null && Number.isFinite(v)) {
31929
+ if (v < min) min = v;
31930
+ if (v > max) max = v;
31931
+ }
31932
+ };
31933
+ for (const model of models) {
31934
+ const off = offsetOf(model.id);
31935
+ for (const s of model.series) if (s.overlay === true) considerSeries(s, i0, i1, off, consider);
31936
+ }
31937
+ return min === Infinity ? null : { min, max };
31938
+ }
31372
31939
  function expandScaleByPixels(scale, heightPx, abovePx, belowPx) {
31373
31940
  if (abovePx <= 0 && belowPx <= 0) return scale;
31374
31941
  const content = heightPx - abovePx - belowPx;
@@ -32017,6 +32584,8 @@ var NativeRenderer = class {
32017
32584
  this.glowAmount = 0;
32018
32585
  // WebGL2 neon-glow intensity (canvas2d ignores it)
32019
32586
  this.chrome = new ChromeRenderer();
32587
+ /** Hover tooltips for Pine labels (canvas hit-rects collected by the chrome layer). */
32588
+ this.labelTooltip = null;
32020
32589
  this.crosshairLayer = new CrosshairRenderer();
32021
32590
  /** 1 Hz repaint pump so the price-axis countdown-to-bar-close ticks; null when off. */
32022
32591
  this.countdownTimer = null;
@@ -32027,6 +32596,8 @@ var NativeRenderer = class {
32027
32596
  this.indicatorValuesOn = true;
32028
32597
  /** Host-contributed legend actions — held here so a rebuild of the legend re-wires them. */
32029
32598
  this.legendActionsProvider = null;
32599
+ /** Host-contributed legend callouts — held here so a rebuild of the legend re-wires them. */
32600
+ this.legendCalloutsProvider = null;
32030
32601
  /** Host override of the legend's fold toggle — held here so a remount re-applies it. */
32031
32602
  this.legendOverviewAction = null;
32032
32603
  // ── keyboard navigation / accessibility (item 11) ──
@@ -33135,6 +33706,10 @@ var NativeRenderer = class {
33135
33706
  this.plot.appendChild(this.scrollButton);
33136
33707
  this.plot.addEventListener("pointermove", this.onScrollProximityMove);
33137
33708
  this.plot.addEventListener("pointerleave", this.onScrollProximityLeave);
33709
+ this.labelTooltip = new LabelTooltip(this.plot, {
33710
+ theme: () => this.chromeTheme(),
33711
+ lookup: (x, y) => this.chrome.labelTooltipAt(x, y)
33712
+ });
33138
33713
  this.userDrawings = new UserDrawingController(this.wrapper, this.plot, this.drawingsCanvas, {
33139
33714
  projector: () => this.drawingProjector(),
33140
33715
  dpr: () => this.coords.dpr,
@@ -33174,9 +33749,10 @@ var NativeRenderer = class {
33174
33749
  this.inputsUI.setDialogHost(this.dialogHost);
33175
33750
  this.inputsUI.setSymbolPicker(this.symbolPicker);
33176
33751
  this.inputsUI.setLegendActions(this.legendActionsProvider);
33752
+ this.inputsUI.setLegendCallouts(this.legendCalloutsProvider);
33177
33753
  this.inputsUI.setLegendOverviewAction(this.legendOverviewAction);
33178
33754
  this.inputsUI.setOnChange((c) => {
33179
- for (const cb of this.inputChangeCbs) cb({ indicatorId: c.indicatorId, key: c.key, value: c.value });
33755
+ for (const cb of this.inputChangeCbs) cb({ indicatorId: c.indicatorId, key: c.key, value: c.value, ...c.kind ? { kind: c.kind } : {} });
33180
33756
  });
33181
33757
  this.inputsUI.setOnRemove((id) => {
33182
33758
  for (const cb of this.removeIndicatorCbs) cb(id);
@@ -33366,6 +33942,8 @@ var NativeRenderer = class {
33366
33942
  this.settingsButton = null;
33367
33943
  this.plot?.removeEventListener("pointermove", this.onScrollProximityMove);
33368
33944
  this.plot?.removeEventListener("pointerleave", this.onScrollProximityLeave);
33945
+ this.labelTooltip?.destroy();
33946
+ this.labelTooltip = null;
33369
33947
  this.scrollButton?.remove();
33370
33948
  this.scrollButton = null;
33371
33949
  for (const l of this.extLayers) l.instance.destroy?.();
@@ -33606,7 +34184,7 @@ var NativeRenderer = class {
33606
34184
  else this.scene.assignIndicatorZ(model.id);
33607
34185
  this.inputsUI.upsert(model.id, model.shorttitle ?? model.title, model.inputs, model.inputValues, model.paneId, {
33608
34186
  native: !!model.native,
33609
- ...model.shorttitle ? { settingsTitle: model.title } : {}
34187
+ ...model.props ? { props: model.props, propValues: model.propValues ?? {} } : {}
33610
34188
  });
33611
34189
  this.syncTables(model);
33612
34190
  if (model.native?.type === "volume") {
@@ -33657,8 +34235,8 @@ var NativeRenderer = class {
33657
34235
  this.paneControls?.refresh();
33658
34236
  this.scheduler.invalidate(4 /* Full */);
33659
34237
  }
33660
- setIndicatorInputs(handle, values) {
33661
- this.inputsUI.setValues(handle.id, values);
34238
+ setIndicatorInputs(handle, values, props) {
34239
+ this.inputsUI.setValues(handle.id, values, props);
33662
34240
  }
33663
34241
  setSymbolPicker(picker) {
33664
34242
  this.symbolPicker = picker;
@@ -33668,6 +34246,10 @@ var NativeRenderer = class {
33668
34246
  this.legendActionsProvider = provider;
33669
34247
  this.inputsUI?.setLegendActions(provider);
33670
34248
  }
34249
+ setLegendCallouts(provider) {
34250
+ this.legendCalloutsProvider = provider;
34251
+ this.inputsUI?.setLegendCallouts(provider);
34252
+ }
33671
34253
  setLegendOverviewAction(action) {
33672
34254
  this.legendOverviewAction = action;
33673
34255
  this.inputsUI?.setLegendOverviewAction(action);
@@ -34648,7 +35230,11 @@ var NativeRenderer = class {
34648
35230
  }
34649
35231
  const models = this.scene.indicatorsForPane(pane.id);
34650
35232
  const masterModels = models.filter((m) => m.ownScale !== true);
34651
- const dr = this.chrome.paneDrawingsRange(masterModels, this.scene, pane === pricePane, vr);
35233
+ let dr = this.chrome.paneDrawingsRange(masterModels, this.scene, pane === pricePane, vr);
35234
+ if (pane === pricePane) {
35235
+ const or = overlaySeriesRange(this.scene.indicators.values(), i0, i1, (id) => this.scene.offsetOf(id));
35236
+ if (or) dr = dr ? { min: Math.min(dr.min, or.min), max: Math.max(dr.max, or.max) } : or;
35237
+ }
34652
35238
  const includeCandles = pane.kind === "price" && !this.scene.candlesHidden;
34653
35239
  pane.scaleTarget = computePaneScale(masterModels, this.bars, includeCandles, i0, i1, dr, paneLogScale(this.scene, pane), (id) => this.scene.offsetOf(id));
34654
35240
  pane.percentBaseline = pane.kind === "price" ? this.bars[i0]?.close ?? 0 : this.firstVisibleValue(masterModels, i0);
@@ -34922,6 +35508,7 @@ var NativeRenderer = class {
34922
35508
  for (const m of models) {
34923
35509
  const off = this.scene.offsetOf(m.id);
34924
35510
  for (const s of m.series) {
35511
+ if (s.overlay === true) continue;
34925
35512
  if (isLineLikeSeries(s)) {
34926
35513
  const v = s.points[i0 - off]?.value;
34927
35514
  if (v != null && Number.isFinite(v)) return v;
@@ -35794,45 +36381,24 @@ var CSS21 = `
35794
36381
  }
35795
36382
  .vela-statusline .vela-sl-symbol { font-weight: 600; font-size: var(--vela-font-size-lg); }
35796
36383
  .vela-statusline .vela-sl-meta { color: var(--vela-fg-muted); font-size: var(--vela-font-size-md); font-weight: 600; }
35797
- /* Market status badge \u2014 icon-only 16px circle, label on hover (kit tooltip). */
35798
- .vela-statusline .vela-sl-market {
35799
- display: inline-grid;
35800
- place-items: center;
35801
- width: 16px;
35802
- height: 16px;
35803
- border-radius: 50%;
35804
- flex: none;
35805
- align-self: center;
35806
- line-height: 0;
35807
- cursor: default;
35808
- }
35809
- .vela-statusline .vela-sl-market svg {
35810
- width: 12px;
35811
- height: 12px;
35812
- display: block;
35813
- }
35814
- /* Open wears the theme's up color; the other sessions are meaning constants from the
35815
- * palette (amber pre, sky post, gray closed/holiday). Same ink at 20% for the circle. */
35816
- .vela-statusline .vela-sl-market[data-status='open'] {
35817
- background: color-mix(in srgb, var(--vela-up) 20%, transparent);
35818
- color: var(--vela-up);
35819
- }
35820
- .vela-statusline .vela-sl-market[data-status='pre'] { background: color-mix(in srgb, ${SESSION_PRE} 20%, transparent); color: ${SESSION_PRE}; }
35821
- .vela-statusline .vela-sl-market[data-status='post'] { background: color-mix(in srgb, ${SESSION_POST} 20%, transparent); color: ${SESSION_POST}; }
35822
- .vela-statusline .vela-sl-market[data-status='closed'],
35823
- .vela-statusline .vela-sl-market[data-status='holiday'] { background: color-mix(in srgb, ${SESSION_OFF} 20%, transparent); color: ${SESSION_OFF}; }
36384
+ /* Market status badge \u2014 a kit callout bubble (icon-only 16px circle, label on hover
36385
+ * via the kit tooltip); the session tint is applied per status in setMarketStatus. */
36386
+ .vela-statusline .vela-sl-market { align-self: center; }
35824
36387
  .vela-statusline .vela-sl-ohlc { display: flex; gap: var(--vela-space-1); color: var(--vela-fg-muted); }
35825
36388
  .vela-statusline .vela-sl-ohlc b { color: var(--vela-fg); font-weight: 500; }
35826
36389
  /* The change value wears the SAME ink as the OHLC values (set inline per render) \u2014
35827
36390
  * these are the pre-ink fallbacks only. */
35828
36391
  .vela-statusline .vela-sl-change[data-dir='up'] { color: var(--vela-up); }
35829
36392
  .vela-statusline .vela-sl-change[data-dir='down'] { color: var(--vela-down); }
35830
- /* Stack the renderer's PRICE-pane legend below the status line (study panes stay put).
35831
- * The renderer sets the legend's inline top \u2014 shift with a transform, don't fight it.
35832
- * Scoped to hosts that actually CARRY a status line (the marker class set by the
35833
- * Statusline constructor) \u2014 the stylesheet is document-global, so a bare container
35834
- * class here would shift every chart on the page, including statusline-less ones. */
35835
- .vela-has-statusline [data-vela-pane='price'] { transform: translateY(26px); }
36393
+ /* Stack the TOP pane's legend below the status line (lower study panes stay put).
36394
+ * The renderer marks whichever legend sits at the plot's top edge \u2014 the price pane
36395
+ * normally, or a maximized study pane filling the plot \u2014 so the legend never merges
36396
+ * with the status line whichever pane owns the top. The renderer sets the legend's
36397
+ * inline top \u2014 shift with a transform, don't fight it. Scoped to hosts that actually
36398
+ * CARRY a status line (the marker class set by the Statusline constructor) \u2014 the
36399
+ * stylesheet is document-global, so a bare attribute selector here would shift every
36400
+ * chart on the page, including statusline-less ones. */
36401
+ .vela-has-statusline [${LEGEND_AT_TOP_ATTR}] { transform: translateY(26px); }
35836
36402
  /* Mobile: two-line chip \u2014 logo / symbol / meta / market status on one aligned row, the
35837
36403
  * bar change on the next. Full O/H/L/C stays hidden (too dense on a phone-width plot).
35838
36404
  * GRID, not a wrapping flexbox: an absolutely positioned wrapping flex container sizes
@@ -35868,7 +36434,7 @@ var CSS21 = `
35868
36434
  font-size: var(--vela-font-size-sm);
35869
36435
  line-height: 1.2;
35870
36436
  }
35871
- [data-layout='mobile'] .vela-has-statusline [data-vela-pane='price'] { transform: translateY(40px); }
36437
+ [data-layout='mobile'] .vela-has-statusline [${LEGEND_AT_TOP_ATTR}] { transform: translateY(40px); }
35872
36438
  /* FIT mode (multi-chart cells \u2014 see setFitMode): the line never wraps; segments that
35873
36439
  * don't fit are HIDDEN by fit() (change first, then meta, then the market badge), so
35874
36440
  * overflow:hidden only guards the transient between a resize and the next measure.
@@ -35885,7 +36451,7 @@ var CSS21 = `
35885
36451
  padding-left: 0;
35886
36452
  }
35887
36453
  /* One row again \u2014 the mobile two-line shift doesn't apply in fit mode. */
35888
- [data-layout='mobile'] .vela-sl-fit-host.vela-has-statusline [data-vela-pane='price'] { transform: translateY(26px); }
36454
+ [data-layout='mobile'] .vela-sl-fit-host.vela-has-statusline [${LEGEND_AT_TOP_ATTR}] { transform: translateY(26px); }
35889
36455
  `;
35890
36456
  function baseOfTicker(ticker) {
35891
36457
  return ticker.replace(/[-_/]?(USDT|USDC|USD1|USDS|BUSD|USD|EUR|PERP)$/i, "") || ticker;
@@ -35897,6 +36463,13 @@ var MARKET_LABELS = {
35897
36463
  closed: "Market Closed",
35898
36464
  holiday: "Market Holiday"
35899
36465
  };
36466
+ var MARKET_INKS = {
36467
+ open: "var(--vela-up)",
36468
+ pre: SESSION_PRE,
36469
+ post: SESSION_POST,
36470
+ closed: SESSION_OFF,
36471
+ holiday: SESSION_OFF
36472
+ };
35900
36473
  function statuslineInkOf(renderer, priceStyle) {
35901
36474
  const cfg = renderer.getConfig();
35902
36475
  const c = (v) => typeof v === "string" ? v : null;
@@ -35955,8 +36528,15 @@ var Statusline = class {
35955
36528
  this.symbolEl.textContent = ticker;
35956
36529
  this.metaEl = doc.createElement("span");
35957
36530
  this.metaEl.className = "vela-sl-meta";
35958
- this.marketEl = doc.createElement("span");
35959
- this.marketEl.className = "vela-sl-market";
36531
+ this.marketBubble = new CalloutBubble({
36532
+ icon: "market-open",
36533
+ background: `color-mix(in srgb, ${MARKET_INKS.open} 20%, transparent)`,
36534
+ color: MARKET_INKS.open,
36535
+ label: MARKET_LABELS.open,
36536
+ host
36537
+ });
36538
+ this.marketEl = this.marketBubble.el;
36539
+ this.marketEl.classList.add("vela-sl-market");
35960
36540
  this.ohlcEl = doc.createElement("span");
35961
36541
  this.ohlcEl.className = "vela-sl-ohlc";
35962
36542
  this.changeEl = doc.createElement("span");
@@ -36044,8 +36624,13 @@ var Statusline = class {
36044
36624
  * hover label. Callers with no session model leave the constructor's 'open'. */
36045
36625
  setMarketStatus(status) {
36046
36626
  this.marketEl.dataset.status = status;
36047
- this.marketEl.innerHTML = icon(`market-${status}`);
36048
- this.marketEl.setAttribute("aria-label", MARKET_LABELS[status]);
36627
+ const ink = MARKET_INKS[status];
36628
+ this.marketBubble.set({
36629
+ icon: `market-${status}`,
36630
+ background: `color-mix(in srgb, ${ink} 20%, transparent)`,
36631
+ color: ink,
36632
+ label: MARKET_LABELS[status]
36633
+ });
36049
36634
  this.marketTip.setContent(MARKET_LABELS[status]);
36050
36635
  }
36051
36636
  setPartVisible(part, visible) {
@@ -36078,6 +36663,7 @@ var Statusline = class {
36078
36663
  this.fitRO?.disconnect();
36079
36664
  this.fitRO = null;
36080
36665
  this.marketTip.destroy();
36666
+ this.marketBubble.destroy();
36081
36667
  this.host.classList.remove("vela-has-statusline", "vela-sl-fit-host");
36082
36668
  this.el.remove();
36083
36669
  }
@@ -36304,6 +36890,8 @@ var SessionShadingTracker = class {
36304
36890
  this.spec = null;
36305
36891
  this.ready = false;
36306
36892
  this.session = "regular";
36893
+ /** Whether one bar is shorter than a civil day — only then do pre/post bars exist. */
36894
+ this.intraday = false;
36307
36895
  this.covered = null;
36308
36896
  /** The newest range seen — viewport moves during metadata resolution (a load's fit
36309
36897
  * animation) must not be lost, so the resolution always expands the LATEST range. */
@@ -36316,6 +36904,8 @@ var SessionShadingTracker = class {
36316
36904
  this.spec = null;
36317
36905
  this.covered = null;
36318
36906
  this.session = opts.session;
36907
+ const tfMs = timeframeMs(opts.timeframe);
36908
+ this.intraday = Number.isFinite(tfMs) && tfMs < DAY_MS2;
36319
36909
  this.lastRange = opts.range;
36320
36910
  void data.symbolInfo(symbol).catch(() => void 0).then((si) => {
36321
36911
  if (my !== this.epoch) return;
@@ -36343,7 +36933,7 @@ var SessionShadingTracker = class {
36343
36933
  if (force) this.onZones(null);
36344
36934
  return;
36345
36935
  }
36346
- if (this.session !== "extended") {
36936
+ if (this.session !== "extended" || !this.intraday) {
36347
36937
  if (force) this.onZones({ pre: [], post: [] });
36348
36938
  return;
36349
36939
  }
@@ -36758,6 +37348,7 @@ var ChartCell = class {
36758
37348
  this.inner.renderer.set("attribution", false);
36759
37349
  this.inner.renderer.set("dialogHost", deps.dialogHost);
36760
37350
  this.inner.renderer.setLegendActions(legendActionsProviderFor(this.inner, () => deps.context()));
37351
+ this.inner.renderer.setLegendCallouts(legendCalloutsProviderFor(this.inner, () => deps.context()));
36761
37352
  if (this.inner.renderer.supports("historyChords")) this.inner.renderer.set("historyChords", false);
36762
37353
  this.history.onChart(this.inner);
36763
37354
  this.inner.renderer.onConfigChanged(() => {
@@ -36874,20 +37465,30 @@ var ChartCell = class {
36874
37465
  this.refreshNativeCatalog();
36875
37466
  this.pushSettingsSections();
36876
37467
  this.offMarket = this.inner.on("market:changed", ({ symbol: symbol2, timeframe }) => {
36877
- this.state.symbol = symbol2;
36878
- this.state.provider = parseSymbol(symbol2).provider ?? void 0;
36879
- this.state.timeframe = timeframe;
36880
- this.state.session = normalizeSession(this.inner?.market.session);
36881
- this.watermark?.update(symbol2, timeframe);
36882
- this.statusline?.setSymbol(symbol2);
36883
- this.statusline?.setMeta(timeframe, this.inner?.data.displayPrefix(symbol2) ?? this.state.provider ?? "");
36884
- if (this.inner) this.statusline?.onChart(this.inner);
37468
+ this.projectMarket(symbol2, timeframe);
36885
37469
  this.refreshNativeCatalog();
36886
37470
  this.refreshSessionAvailable();
36887
37471
  if (this.inner) this.marketStatus?.track(this.inner.data, symbol2);
36888
37472
  this.deps.onMarketChanged(this.id);
36889
37473
  });
36890
37474
  }
37475
+ /**
37476
+ * Project a market identity into the cell state and its display overlays (watermark,
37477
+ * statusline), WITHOUT the data-dependent bookkeeping. Runs twice per user pick: once
37478
+ * optimistically from the cell setters — the labels reflect the pick immediately, not
37479
+ * after the bars load — and again from `market:changed` (the committed pass, and the
37480
+ * only pass for host `chart.setMarket` calls). Idempotent, so the double run converges.
37481
+ */
37482
+ projectMarket(symbol, timeframe) {
37483
+ this.state.symbol = symbol;
37484
+ this.state.provider = parseSymbol(symbol).provider ?? void 0;
37485
+ this.state.timeframe = timeframe;
37486
+ this.state.session = normalizeSession(this.inner?.market.session);
37487
+ this.watermark?.update(symbol, timeframe);
37488
+ this.statusline?.setSymbol(symbol);
37489
+ this.statusline?.setMeta(timeframe, this.inner?.data.displayPrefix(symbol) ?? this.state.provider ?? "");
37490
+ if (this.inner) this.statusline?.onChart(this.inner);
37491
+ }
36891
37492
  /**
36892
37493
  * Does this cell's market HAVE sessions (RTH/ETH meaningful)? Derived from the
36893
37494
  * symbol's own metadata (`syminfo.session !== '24x7'`), asynchronously — the
@@ -36933,7 +37534,7 @@ var ChartCell = class {
36933
37534
  const requestedSpan = Math.max(this.state.bars ?? 1e3, this.rangeBars) * timeframeMs(this.state.timeframe ?? "60");
36934
37535
  const fallbackSpan = Number.isFinite(requestedSpan) ? Math.max(3 * 864e5, requestedSpan) : 3 * 864e5;
36935
37536
  const range = chart.getVisibleRange() ?? { from: now - fallbackSpan, to: now };
36936
- this.sessionShading.track(chart.data, symbol, { session: this.session, range });
37537
+ this.sessionShading.track(chart.data, symbol, { session: this.session, timeframe: this.timeframe, range });
36937
37538
  }
36938
37539
  /** The session-shade colors live in the renderer CONFIG (persisted with it, edited
36939
37540
  * live by the dialog swatch) — the cell only proxies them into its settings rows. */
@@ -37122,17 +37723,23 @@ var ChartCell = class {
37122
37723
  get indicatorCount() {
37123
37724
  return this.instances.length + this.nativeCatalog.filter((n) => n.present).length;
37124
37725
  }
37125
- /** Switch this cell's market in place (the chart instance survives). */
37726
+ /** Switch this cell's market in place (the chart instance survives). The projection
37727
+ * is OPTIMISTIC — labels and chrome show the pick before the bars load; it follows
37728
+ * the setMarket call so the statusline reads the already-blanked chart. */
37126
37729
  setSymbol(symbol) {
37127
37730
  if (!this.inner || symbol === this.symbol) return;
37128
37731
  this.unresolvedToasted = null;
37129
37732
  void this.inner.setMarket({ symbol });
37733
+ this.projectMarket(symbol, this.timeframe);
37734
+ this.deps.onMarketChanged(this.id);
37130
37735
  }
37131
37736
  setTimeframe(timeframe) {
37132
37737
  if (!this.inner || timeframe === this.timeframe) return;
37133
37738
  this.activeRangeId = null;
37134
37739
  this.rangeBars = 0;
37135
37740
  void this.inner.setMarket({ timeframe, bars: this.state.bars });
37741
+ this.projectMarket(this.symbol, timeframe);
37742
+ this.deps.onMarketChanged(this.id);
37136
37743
  }
37137
37744
  /** Applied live (renderer feature) — no reload. */
37138
37745
  setPriceStyle(style) {
@@ -38238,7 +38845,10 @@ var VelaWorkspace = class {
38238
38845
  this.topbar.renderActions();
38239
38846
  this.mobileBar?.renderActions();
38240
38847
  this.dock.refresh();
38241
- for (const cell of this.cells()) cell.chart.renderer.setLegendActions(legendActionsProviderFor(cell.chart, () => this.context()));
38848
+ for (const cell of this.cells()) {
38849
+ cell.chart.renderer.setLegendActions(legendActionsProviderFor(cell.chart, () => this.context()));
38850
+ cell.chart.renderer.setLegendCallouts(legendCalloutsProviderFor(cell.chart, () => this.context()));
38851
+ }
38242
38852
  }
38243
38853
  // ── state surface (the SDK's read/restore of the whole grid's config + content) ──
38244
38854
  /**