@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
@@ -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);
@@ -17510,6 +17734,7 @@ var IndicatorHandleImpl = class {
17510
17734
  constructor(id, title, controller, source) {
17511
17735
  this.controller = controller;
17512
17736
  this.schema = [];
17737
+ this.propsSchema = [];
17513
17738
  this.visibleState = true;
17514
17739
  this.bus = new TypedEventBus();
17515
17740
  this.id = id;
@@ -17519,6 +17744,9 @@ var IndicatorHandleImpl = class {
17519
17744
  get inputs() {
17520
17745
  return this.schema;
17521
17746
  }
17747
+ get props() {
17748
+ return this.propsSchema;
17749
+ }
17522
17750
  get visible() {
17523
17751
  return this.visibleState;
17524
17752
  }
@@ -17528,6 +17756,12 @@ var IndicatorHandleImpl = class {
17528
17756
  setInputs(values) {
17529
17757
  this.controller.applyInputs(this.id, values);
17530
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
+ }
17531
17765
  setVisible(visible) {
17532
17766
  this.controller.setVisible(this.id, visible);
17533
17767
  }
@@ -17547,6 +17781,9 @@ var IndicatorHandleImpl = class {
17547
17781
  setSchema(schema) {
17548
17782
  this.schema = schema;
17549
17783
  }
17784
+ setPropsSchema(schema) {
17785
+ this.propsSchema = schema;
17786
+ }
17550
17787
  /** Sync the public `visible` getter when the orchestrator changes visibility (API or legend eye). */
17551
17788
  setVisibleState(visible) {
17552
17789
  this.visibleState = visible;
@@ -17560,6 +17797,8 @@ var IndicatorHandleImpl = class {
17560
17797
  function summarizeModel(model) {
17561
17798
  const series = {};
17562
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);
17563
17802
  return {
17564
17803
  id: model.id,
17565
17804
  title: model.title,
@@ -17579,7 +17818,8 @@ function summarizeModel(model) {
17579
17818
  tables: model.tables?.length ?? 0,
17580
17819
  barColors: model.barColors?.length ?? 0,
17581
17820
  trades: model.trades?.length ?? 0,
17582
- inputs: model.inputs.length
17821
+ inputs: model.inputs.length,
17822
+ forcedOverlay
17583
17823
  };
17584
17824
  }
17585
17825
  function inspectModels(models) {
@@ -17600,7 +17840,8 @@ function inspectModels(models) {
17600
17840
  linefills: sum((s) => s.linefills),
17601
17841
  tables: sum((s) => s.tables),
17602
17842
  barColors: sum((s) => s.barColors),
17603
- trades: sum((s) => s.trades)
17843
+ trades: sum((s) => s.trades),
17844
+ forcedOverlay: sum((s) => s.forcedOverlay)
17604
17845
  }
17605
17846
  };
17606
17847
  }
@@ -17744,7 +17985,9 @@ var EngineOrchestrator = class _EngineOrchestrator {
17744
17985
  for (const engine of engines) this.registerEngine(engine.language, engine);
17745
17986
  this.defaultLanguage = config.defaultLanguage;
17746
17987
  this.renderer.mount(container, config.theme);
17747
- 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
+ );
17748
17991
  this.renderer.onRemoveIndicator((id) => this.removeIndicator(id));
17749
17992
  this.renderer.onToggleIndicatorVisible?.((id, visible) => this.setVisible(id, visible));
17750
17993
  this.paneActionUnsub = this.renderer.onPaneAction?.((a) => this.handlePaneAction(a)) ?? null;
@@ -18424,7 +18667,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
18424
18667
  const title = options.title ?? "Indicator";
18425
18668
  const handle = new IndicatorHandleImpl(id, title, this, source);
18426
18669
  this.handles.set(id, handle);
18427
- this.registry.add({ id, title, source, options, inputValues: { ...options.inputs ?? {} } });
18670
+ this.registry.add({ id, title, source, options, inputValues: { ...options.inputs ?? {} }, propValues: { ...options.props ?? {} } });
18428
18671
  void this.startIndicator(id, source, options, handle);
18429
18672
  return handle;
18430
18673
  }
@@ -18447,7 +18690,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
18447
18690
  return handle;
18448
18691
  }
18449
18692
  const inputValues = { ...descriptor.defaultInputs(), ...options.inputs ?? {} };
18450
- 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 } });
18451
18694
  handle.setSchema(descriptor.inputsSchema());
18452
18695
  void this.startNativeIndicator(id, handle);
18453
18696
  return handle;
@@ -18500,6 +18743,19 @@ var EngineOrchestrator = class _EngineOrchestrator {
18500
18743
  if (record.session) record.session.update(record.inputValues);
18501
18744
  else if (record.native && !record.hidden) record.native.instance.setInputs(record.inputValues);
18502
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
+ }
18503
18759
  /** IndicatorController: tear down an indicator and (if now empty) its pane. */
18504
18760
  /** Live handles of every indicator on the chart (script + native), insertion order. */
18505
18761
  listIndicators() {
@@ -18648,6 +18904,10 @@ var EngineOrchestrator = class _EngineOrchestrator {
18648
18904
  for (const input of prepared.inputs) defaults2[input.key] = input.defval;
18649
18905
  record.inputValues = { ...defaults2, ...record.inputValues };
18650
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 ?? []);
18651
18911
  this.mountLoadingPlaceholder(id, record);
18652
18912
  this.executeIndicator(id, handle);
18653
18913
  } catch (err) {
@@ -18671,6 +18931,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
18671
18931
  getBars: () => this.bars,
18672
18932
  fetchSeries: (sym, tf, range) => this.fetchSeries(sym, tf, range),
18673
18933
  inputs: record.inputValues,
18934
+ props: record.propValues,
18674
18935
  visibleRange: this.currentVisibleRange(),
18675
18936
  mode,
18676
18937
  // Mid-backfill session starts (add / re-show / price-style re-execute) let the
@@ -18764,6 +19025,9 @@ var EngineOrchestrator = class _EngineOrchestrator {
18764
19025
  const meta = record.prepared.meta;
18765
19026
  const model = {
18766
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.
18767
19031
  title: record.options?.title ?? meta.title,
18768
19032
  overlay: meta.overlay,
18769
19033
  paneHint: meta.overlay ? "price" : "new",
@@ -18772,7 +19036,8 @@ var EngineOrchestrator = class _EngineOrchestrator {
18772
19036
  backgrounds: [],
18773
19037
  priceLines: [],
18774
19038
  inputs: record.prepared.inputs,
18775
- inputValues: record.inputValues
19039
+ inputValues: record.inputValues,
19040
+ ...record.prepared.props ? { props: record.prepared.props, propValues: record.propValues } : {}
18776
19041
  };
18777
19042
  const paneId = this.routePane(id, model, record.options ?? {});
18778
19043
  this.placeModel(model, id, paneId);
@@ -19005,7 +19270,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
19005
19270
  record.model = model;
19006
19271
  if (record.pendingStructural) {
19007
19272
  record.renderHandle = this.renderer.mountIndicator(model);
19008
- this.renderer.setIndicatorInputs(record.renderHandle, record.inputValues);
19273
+ this.renderer.setIndicatorInputs(record.renderHandle, record.inputValues, record.propValues);
19009
19274
  record.pendingStructural = false;
19010
19275
  } else {
19011
19276
  this.renderer.updateIndicator(record.renderHandle, modelToValuePatch(model));
@@ -19032,16 +19297,17 @@ var EngineOrchestrator = class _EngineOrchestrator {
19032
19297
  placeModel(model, id, paneId) {
19033
19298
  model.id = id;
19034
19299
  model.paneId = paneId;
19035
- for (const series of model.series) series.paneId = paneId;
19036
- for (const fill of model.fills) fill.paneId = paneId;
19037
- 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);
19038
19304
  for (const line of model.priceLines) line.paneId = paneId;
19039
- if (model.lines) for (const ln of model.lines) ln.paneId = paneId;
19040
- if (model.boxes) for (const bx of model.boxes) bx.paneId = paneId;
19041
- if (model.labels) for (const lb of model.labels) lb.paneId = paneId;
19042
- if (model.polylines) for (const pl of model.polylines) pl.paneId = paneId;
19043
- if (model.linefills) for (const lf of model.linefills) lf.paneId = paneId;
19044
- 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);
19045
19311
  }
19046
19312
  emitContextChanged(id) {
19047
19313
  if (!this.registry.get(id)?.session?.getContext) return;
@@ -19194,6 +19460,15 @@ var RendererControl = class {
19194
19460
  setLegendActions(provider) {
19195
19461
  this.renderer.setLegendActions?.(provider);
19196
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
+ }
19197
19472
  /**
19198
19473
  * Replace the indicator legend's fold toggle with a host action (or restore it with
19199
19474
  * `null`) — multi-chart shells point the chip at their indicator overview instead of
@@ -19703,6 +19978,10 @@ function inputVisible(when, values) {
19703
19978
  }
19704
19979
 
19705
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
+ }
19706
19985
  var SOURCES = ["close", "open", "high", "low", "hl2", "hlc3", "ohlc4", "volume"];
19707
19986
  var IndicatorInputsDialog = class {
19708
19987
  constructor(host) {
@@ -19713,6 +19992,7 @@ var IndicatorInputsDialog = class {
19713
19992
  this.openId = null;
19714
19993
  this.row = null;
19715
19994
  this.snapshot = null;
19995
+ this.propSnapshot = null;
19716
19996
  this.dialogTips = [];
19717
19997
  /** Re-applies every `when` gate against current values; set while the dialog is open. */
19718
19998
  this.refreshVisibility = null;
@@ -19735,10 +20015,11 @@ var IndicatorInputsDialog = class {
19735
20015
  }
19736
20016
  open(row) {
19737
20017
  this.close();
19738
- if (row.inputs.length === 0) return;
20018
+ if (row.inputs.length === 0 && row.props.length === 0) return;
19739
20019
  this.row = row;
19740
20020
  this.openId = row.id;
19741
20021
  this.snapshot = { ...row.values };
20022
+ this.propSnapshot = { ...row.propValues };
19742
20023
  ensureDialogStyles();
19743
20024
  const t = this.host.theme();
19744
20025
  const border = "var(--vela-border)";
@@ -19746,7 +20027,7 @@ var IndicatorInputsDialog = class {
19746
20027
  const host = this.host.dialogHost() ?? this.host.container;
19747
20028
  const ui = new Dialog({
19748
20029
  host,
19749
- title: row.settingsTitle,
20030
+ title: row.title,
19750
20031
  // Non-modal: live-edit dialog — a modal machine locks pointer events on the
19751
20032
  // whole body, killing the chart and the body-portaled popovers.
19752
20033
  modal: false,
@@ -19778,6 +20059,16 @@ var IndicatorInputsDialog = class {
19778
20059
  text: () => "Close"
19779
20060
  }));
19780
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
+ }
19781
20072
  const tabs = document.createElement("div");
19782
20073
  tabs.style.cssText = `display:flex;gap:12px;padding:0 20px;border-bottom:1px solid ${border};flex:0 0 auto;`;
19783
20074
  const tabEls = [];
@@ -19880,9 +20171,10 @@ var IndicatorInputsDialog = class {
19880
20171
  this.openId = null;
19881
20172
  this.row = null;
19882
20173
  this.snapshot = null;
20174
+ this.propSnapshot = null;
19883
20175
  ui?.destroy();
19884
20176
  }
19885
- /** 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. */
19886
20178
  revertAndClose() {
19887
20179
  const row = this.row;
19888
20180
  const snap = this.snapshot;
@@ -19896,6 +20188,17 @@ var IndicatorInputsDialog = class {
19896
20188
  }
19897
20189
  }
19898
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
+ }
19899
20202
  this.close();
19900
20203
  }
19901
20204
  /** The footer's reset button — same chip as Cancel, pinned to the LEFT edge
@@ -19913,19 +20216,27 @@ var IndicatorInputsDialog = class {
19913
20216
  const row = this.row;
19914
20217
  if (!row) return;
19915
20218
  const snap = this.snapshot;
20219
+ const propSnap = this.propSnapshot;
19916
20220
  for (const inp of row.inputs) {
19917
20221
  if (row.values[inp.key] !== inp.defval) {
19918
20222
  row.values[inp.key] = inp.defval;
19919
20223
  this.host.onChange?.({ indicatorId: row.id, key: inp.key, value: inp.defval });
19920
20224
  }
19921
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
+ }
19922
20232
  this.open(row);
19923
20233
  if (snap) this.snapshot = snap;
20234
+ if (propSnap) this.propSnapshot = propSnap;
19924
20235
  }
19925
20236
  /** Write one edit through: store it, notify the host, and re-apply the `when` gates. */
19926
- commit(row, key, value) {
19927
- row.values[key] = value;
19928
- 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" } : {} });
19929
20240
  this.refreshVisibility?.();
19930
20241
  }
19931
20242
  /** One settings row (or several `inline=` inputs) placed into a section's grid. */
@@ -19952,7 +20263,7 @@ var IndicatorInputsDialog = class {
19952
20263
  const id = idOf(lead);
19953
20264
  const info = lead.tooltip ? this.infoButton(lead.tooltip) : void 0;
19954
20265
  if (lead.type === "bool") {
19955
- const current = Boolean(row.values[lead.key] ?? lead.defval);
20266
+ const current = Boolean(bagOf(row, lead)[lead.key] ?? lead.defval);
19956
20267
  return append(fieldRow({
19957
20268
  label: nameOf(lead),
19958
20269
  id,
@@ -19961,7 +20272,7 @@ var IndicatorInputsDialog = class {
19961
20272
  toggle: {
19962
20273
  id,
19963
20274
  checked: current,
19964
- onChange: (v) => this.commit(row, lead.key, v)
20275
+ onChange: (v) => this.commit(row, lead, v)
19965
20276
  }
19966
20277
  }));
19967
20278
  }
@@ -19986,8 +20297,8 @@ var IndicatorInputsDialog = class {
19986
20297
  }
19987
20298
  /** Build the typed control for one input, committing edits live via `onChange`. */
19988
20299
  buildControl(row, inp, id) {
19989
- const current = row.values[inp.key] ?? inp.defval;
19990
- 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);
19991
20302
  if (inp.type === "bool") {
19992
20303
  return buildFieldControl({ kind: "switch", id, checked: Boolean(current), onChange: (v) => emit(v) }).el;
19993
20304
  }
@@ -19998,7 +20309,7 @@ var IndicatorInputsDialog = class {
19998
20309
  kind: "color",
19999
20310
  id,
20000
20311
  theme: this.host.theme(),
20001
- get: () => String(row.values[inp.key] ?? inp.defval),
20312
+ get: () => String(bagOf(row, inp)[inp.key] ?? inp.defval),
20002
20313
  onChange: (v) => emit(v)
20003
20314
  }).el;
20004
20315
  }
@@ -20561,6 +20872,7 @@ function timeParts(ts) {
20561
20872
  }
20562
20873
 
20563
20874
  // src/renderers/shared/InputsUI.ts
20875
+ var LEGEND_AT_TOP_ATTR = "data-vela-pane-at-top";
20564
20876
  var STATUS_KEYFRAMES = "@keyframes vela-ind-pulse{0%,100%{opacity:1;transform:scale(1)}50%{opacity:.3;transform:scale(.6)}}";
20565
20877
  var LEGEND_ICON_PX2 = 16;
20566
20878
  var LEGEND_CTL_PX = 18;
@@ -20599,10 +20911,14 @@ var InputsUI = class {
20599
20911
  this.moveApi = null;
20600
20912
  /** Host-contributed legend actions, resolved PER ROW at render time (see setLegendActions). */
20601
20913
  this.legendActions = null;
20914
+ /** Host-contributed callout bubbles, resolved PER ROW at render time (see setLegendCallouts). */
20915
+ this.legendCallouts = null;
20602
20916
  /** Chrome-tooltip disposers, per row id — a tip open at removal must not outlive its row. */
20603
20917
  this.rowTips = /* @__PURE__ */ new Map();
20604
20918
  /** Same, for the contributed extras only (rebuilt independently by setLegendActions). */
20605
20919
  this.extrasTips = /* @__PURE__ */ new Map();
20920
+ /** Same, for the contributed callouts only (rebuilt independently by setLegendCallouts). */
20921
+ this.calloutTips = /* @__PURE__ */ new Map();
20606
20922
  /** Open "Move to" menu (kept so it can be torn down). */
20607
20923
  this.moveMenu = null;
20608
20924
  this.moveTargets = [];
@@ -20760,6 +21076,42 @@ var InputsUI = class {
20760
21076
  extrasEl.appendChild(btn2);
20761
21077
  }
20762
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
+ }
20763
21115
  /** Reposition the per-pane legend containers after a layout change. */
20764
21116
  reposition() {
20765
21117
  for (const [paneId, lg] of this.legends) this.positionLegend(lg, paneId);
@@ -20927,6 +21279,7 @@ var InputsUI = class {
20927
21279
  }
20928
21280
  positionLegend(lg, paneId) {
20929
21281
  const bounds = this.paneBoundsOf ? this.paneBoundsOf(paneId) : { top: 0, height: Infinity };
21282
+ lg.toggleAttribute(LEGEND_AT_TOP_ATTR, bounds.top === 0);
20930
21283
  lg.style.display = bounds.height < 4 || !this.titlesVisible ? "none" : "flex";
20931
21284
  const collapsed = this.paneCollapse.has(paneId);
20932
21285
  const masterId = this.paneCollapse.get(paneId) ?? null;
@@ -21002,13 +21355,13 @@ var InputsUI = class {
21002
21355
  }
21003
21356
  /** Create or update an indicator's legend row (in the legend for its pane). */
21004
21357
  upsert(id, title, inputs, values, paneId = "price", opts = {}) {
21005
- const settingsTitle = opts.settingsTitle ?? title;
21006
21358
  const existing = this.rows.get(id);
21007
21359
  if (existing) {
21008
21360
  existing.title = title;
21009
- existing.settingsTitle = settingsTitle;
21010
21361
  existing.inputs = inputs;
21011
21362
  existing.values = { ...values };
21363
+ existing.props = opts.props ?? [];
21364
+ existing.propValues = { ...opts.propValues ?? {} };
21012
21365
  existing.titleEl.textContent = title;
21013
21366
  if (existing.paneId !== paneId) {
21014
21367
  existing.paneId = paneId;
@@ -21060,6 +21413,9 @@ var InputsUI = class {
21060
21413
  titleWrap.appendChild(beta);
21061
21414
  }
21062
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);
21063
21419
  el.appendChild(statusEl);
21064
21420
  const valuesEl = document.createElement("span");
21065
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;`;
@@ -21076,13 +21432,13 @@ var InputsUI = class {
21076
21432
  eye.className = "vela-ind-ctl";
21077
21433
  eye.style.cssText = LEGEND_CTL_CSS;
21078
21434
  eye.addEventListener("click", () => {
21079
- const row = this.rows.get(id);
21080
- this.onToggleVisible?.(id, Boolean(row?.hidden));
21435
+ const row2 = this.rows.get(id);
21436
+ this.onToggleVisible?.(id, Boolean(row2?.hidden));
21081
21437
  });
21082
21438
  controlsEl.appendChild(eye);
21083
21439
  eyeEl = eye;
21084
21440
  }
21085
- if (inputs.length > 0) {
21441
+ if (inputs.length > 0 || (opts.props ?? []).length > 0) {
21086
21442
  const gear = document.createElement("button");
21087
21443
  gear.type = "button";
21088
21444
  gear.setAttribute("aria-label", "Settings");
@@ -21122,7 +21478,9 @@ var InputsUI = class {
21122
21478
  controlsEl.appendChild(close);
21123
21479
  el.appendChild(controlsEl);
21124
21480
  this.attach(this.legendFor(paneId), el, !!opts.native);
21125
- 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);
21126
21484
  this.syncFoldToggle();
21127
21485
  }
21128
21486
  /** Place a row in its pane's legend — native rows PREPEND (pinned to the top), Pine rows append. */
@@ -21131,9 +21489,11 @@ var InputsUI = class {
21131
21489
  else container.appendChild(el);
21132
21490
  }
21133
21491
  /** Reflect programmatic input changes (so a re-opened dialog shows current values). */
21134
- setValues(id, values) {
21492
+ setValues(id, values, props) {
21135
21493
  const row = this.rows.get(id);
21136
- 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 };
21137
21497
  }
21138
21498
  /**
21139
21499
  * Reflect an indicator's live status in its legend row: `'loading'` shows three pulsing
@@ -21215,8 +21575,13 @@ var InputsUI = class {
21215
21575
  syncRowActions(row) {
21216
21576
  const open2 = row.highlighted;
21217
21577
  row.controlsEl.style.display = open2 || row.hidden ? "inline-flex" : "none";
21218
- if (open2) row.el.appendChild(row.statusEl);
21219
- 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
+ }
21220
21585
  for (const child of Array.from(row.controlsEl.children)) {
21221
21586
  if (!(child instanceof HTMLElement) || child === row.eyeEl) continue;
21222
21587
  if (child === row.extrasEl) {
@@ -21234,10 +21599,12 @@ var InputsUI = class {
21234
21599
  }
21235
21600
  remove(id) {
21236
21601
  const row = this.rows.get(id);
21602
+ for (const bubble of row?.callouts ?? []) bubble.destroy();
21237
21603
  row?.el.remove();
21238
21604
  this.rows.delete(id);
21239
21605
  this.disposeTips(this.rowTips, id);
21240
21606
  this.disposeTips(this.extrasTips, id);
21607
+ this.disposeTips(this.calloutTips, id);
21241
21608
  if (this.selectedId === id) this.selectedId = null;
21242
21609
  if (this.inputsDialog.openId === id) this.inputsDialog.close();
21243
21610
  this.syncFoldToggle();
@@ -21257,6 +21624,8 @@ var InputsUI = class {
21257
21624
  this.rowMenu = null;
21258
21625
  for (const id of [...this.rowTips.keys()]) this.disposeTips(this.rowTips, id);
21259
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();
21260
21629
  for (const lg of this.legends.values()) lg.remove();
21261
21630
  this.legends.clear();
21262
21631
  this.rows.clear();
@@ -21611,6 +21980,32 @@ var SIZE_PX3 = {
21611
21980
  large: 16,
21612
21981
  huge: 20
21613
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
+ }
21614
22009
  var TableOverlay = class {
21615
22010
  constructor(container, theme, paneBounds) {
21616
22011
  this.container = container;
@@ -21631,7 +22026,9 @@ var TableOverlay = class {
21631
22026
  update(tables) {
21632
22027
  this.lastTables = tables;
21633
22028
  this.root.replaceChildren();
21634
- 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
+ }
21635
22032
  }
21636
22033
  /** Re-render at the current pane geometry — after layout settles or on resize. */
21637
22034
  reposition() {
@@ -21646,39 +22043,36 @@ var TableOverlay = class {
21646
22043
  this.root.remove();
21647
22044
  }
21648
22045
  renderTable(t) {
22046
+ const b = this.paneBounds(t.paneId);
21649
22047
  const wrap = document.createElement("div");
21650
22048
  wrap.style.position = "absolute";
21651
- 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);
21652
22051
  const table = document.createElement("table");
21653
22052
  Object.assign(table.style, {
21654
22053
  borderCollapse: "collapse",
21655
22054
  background: t.bgColor ?? "transparent",
21656
22055
  fontFamily: this.theme.fontFamily || "sans-serif",
21657
- // Frame as an inset shadow (not a `border`) so it stays independent of the
21658
- // collapsed cell borders even when frame_width != border_width.
21659
- boxShadow: t.frameColor && t.frameWidth > 0 ? `inset 0 0 0 ${t.frameWidth}px ${t.frameColor}` : "none",
21660
22056
  border: "none",
21661
22057
  tableLayout: "auto",
21662
22058
  // Re-enable pointer events on the table only (root is none) so cell tooltips work.
21663
22059
  pointerEvents: "auto"
21664
22060
  });
21665
- const span = /* @__PURE__ */ new Map();
21666
- const skip = /* @__PURE__ */ new Set();
21667
- for (const m of t.merges) {
21668
- span.set(`${m.startRow}:${m.startCol}`, { cs: m.endCol - m.startCol + 1, rs: m.endRow - m.startRow + 1 });
21669
- for (let r = m.startRow; r <= m.endRow; r += 1) {
21670
- for (let c = m.startCol; c <= m.endCol; c += 1) {
21671
- if (r !== m.startRow || c !== m.startCol) skip.add(`${r}:${c}`);
21672
- }
21673
- }
21674
- }
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;
21675
22064
  const cellBorder = t.borderColor && t.borderWidth > 0 ? `${t.borderWidth}px solid ${t.borderColor}` : "none";
21676
22065
  for (let r = 0; r < t.rows; r += 1) {
21677
22066
  const tr = document.createElement("tr");
21678
22067
  for (let c = 0; c < t.columns; c += 1) {
22068
+ if (omit.has(`${r}:${c}`)) continue;
21679
22069
  const cell = t.cells[r]?.[c] ?? null;
21680
- if (skip.has(`${r}:${c}`) || cell?.merged) continue;
21681
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
+ }
21682
22076
  const sp = span.get(`${r}:${c}`);
21683
22077
  if (sp) {
21684
22078
  if (sp.cs > 1) td.colSpan = sp.cs;
@@ -21687,18 +22081,22 @@ var TableOverlay = class {
21687
22081
  Object.assign(td.style, {
21688
22082
  border: cellBorder,
21689
22083
  padding: "2px 6px",
21690
- background: cell?.bgColor ?? "transparent",
21691
- color: cell?.textColor ?? this.theme.textColor,
21692
- textAlign: cell?.hAlign ?? "center",
21693
- verticalAlign: cell?.vAlign === "top" ? "top" : cell?.vAlign === "bottom" ? "bottom" : "middle",
21694
- fontSize: `${SIZE_PX3[cell?.textSize ?? "normal"]}px`,
21695
- fontFamily: cell?.fontFamily === "monospace" ? "monospace" : "inherit",
21696
- fontWeight: cell?.bold ? "bold" : "normal",
21697
- fontStyle: cell?.italic ? "italic" : "normal",
21698
- 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"
21699
22095
  });
21700
- if (cell?.tooltip) td.title = cell.tooltip;
21701
- 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 ?? "";
21702
22100
  tr.appendChild(td);
21703
22101
  }
21704
22102
  table.appendChild(tr);
@@ -22300,8 +22698,14 @@ var WebGL2Backend = class {
22300
22698
  return sc === pane.scale ? pane : { ...pane, scale: sc };
22301
22699
  };
22302
22700
  b.alpha = this.modelAlpha;
22303
- for (const m of models) for (const bgSpan of m.backgrounds) this.emitBackground(b, bgSpan, pane, coords);
22304
- 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
+ }
22305
22709
  const drawCandles = isPrice && !scene.candlesHidden;
22306
22710
  let candleDrawn = false;
22307
22711
  for (const m of models) {
@@ -22315,7 +22719,7 @@ var WebGL2Backend = class {
22315
22719
  b.alpha = this.modelAlpha;
22316
22720
  const off = scene.offsetOf(m.id);
22317
22721
  const mp = effPane(m);
22318
- 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);
22319
22723
  }
22320
22724
  if (drawCandles && !candleDrawn) {
22321
22725
  drawSlicesUpTo(scene.candleZ);
@@ -22324,6 +22728,12 @@ var WebGL2Backend = class {
22324
22728
  }
22325
22729
  drawSlicesUpTo(Infinity);
22326
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
+ }
22327
22737
  for (const m of models) {
22328
22738
  const mp = effPane(m);
22329
22739
  for (const pl of m.priceLines) this.emitHline(b, pl, mp, coords, dataW, theme);
@@ -22370,7 +22780,7 @@ var WebGL2Backend = class {
22370
22780
  for (const pane of scene.orderedPanes()) {
22371
22781
  const b = this.glowBatch;
22372
22782
  b.reset();
22373
- this.emitGlowSources(b, scene.indicatorsForPane(pane.id), pane, coords, i0, i1, (id) => scene.offsetOf(id));
22783
+ this.emitGlowSources(b, scene, pane, coords, i0, i1);
22374
22784
  if (b.vertexCount === 0) continue;
22375
22785
  const topH = Math.round(pane.bounds.top * dpr * 0.5);
22376
22786
  const botH = Math.round((pane.bounds.top + pane.bounds.height) * dpr * 0.5);
@@ -22439,15 +22849,23 @@ var WebGL2Backend = class {
22439
22849
  gl.bindVertexArray(null);
22440
22850
  return true;
22441
22851
  }
22442
- /** The "neon" elements that glow: line/area/step lines + point markers (not candles/fills/bars). */
22443
- emitGlowSources(b, models, pane, coords, i0, i1, offsetOf = () => 0) {
22444
- for (const m of models) {
22445
- const off = offsetOf(m.id);
22446
- for (const s of m.series) {
22447
- if (!isLineLikeSeries(s) || s.visible === false) continue;
22448
- if (s.kind === "histogram" || s.kind === "columns") continue;
22449
- if (s.kind === "circles" || s.kind === "cross") this.emitPointMarkers(b, s, pane, coords, i0, i1, off);
22450
- 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);
22451
22869
  }
22452
22870
  }
22453
22871
  }
@@ -22730,9 +23148,9 @@ var WebGL2Backend = class {
22730
23148
  b.alpha = this.candleBodyAlpha;
22731
23149
  b.rect(g.bodyX, bodyTop, g.bodyW, bodyH, c);
22732
23150
  }
22733
- if (cs.borderVisible || bc || fading && cs.bodyVisible) {
23151
+ if (cs.borderVisible || fading && cs.bodyVisible) {
22734
23152
  b.alpha = this.candleStructureAlpha;
22735
- 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;
22736
23154
  const bw = Math.max(0, g.bodyW - 1);
22737
23155
  const bh = Math.max(0, bodyH - 1);
22738
23156
  b.rectStroke(g.bodyX + 0.5, bodyTop + 0.5, bw, bh, 1, bord);
@@ -24284,8 +24702,14 @@ var Canvas2dBackend = class {
24284
24702
  return sc === pane.scale ? pane : { ...pane, scale: sc };
24285
24703
  };
24286
24704
  ctx.globalAlpha = this.modelAlpha;
24287
- for (const m of models) for (const bg of m.backgrounds) this.drawBackground(ctx, bg, pane, coords);
24288
- 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
+ }
24289
24713
  const slices = scene.drawingSlices.get(pane.id) ?? [];
24290
24714
  let si = 0;
24291
24715
  const drawSlicesUpTo = (z) => {
@@ -24307,7 +24731,7 @@ var Canvas2dBackend = class {
24307
24731
  ctx.globalAlpha = this.modelAlpha;
24308
24732
  const off = scene.offsetOf(m.id);
24309
24733
  const mp = effPane(m);
24310
- 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);
24311
24735
  }
24312
24736
  if (drawCandles && !candleDrawn) {
24313
24737
  drawSlicesUpTo(scene.candleZ);
@@ -24315,6 +24739,13 @@ var Canvas2dBackend = class {
24315
24739
  this.drawPriceSeries(ctx, scene, i0, i1, coords, pane, theme, barColorMap, dataW);
24316
24740
  }
24317
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
+ }
24318
24749
  ctx.globalAlpha = this.modelAlpha;
24319
24750
  for (const m of models) {
24320
24751
  const mp = effPane(m);
@@ -24586,9 +25017,9 @@ var Canvas2dBackend = class {
24586
25017
  ctx.fillStyle = color;
24587
25018
  ctx.fillRect(g.bodyX, top, g.bodyW, bodyH);
24588
25019
  }
24589
- if (cs.borderVisible || bc || fading && cs.bodyVisible) {
25020
+ if (cs.borderVisible || fading && cs.bodyVisible) {
24590
25021
  ctx.globalAlpha = this.candleStructureAlpha;
24591
- ctx.strokeStyle = cs.borderVisible || bc ? (up ? cs.borderUpColor : cs.borderDownColor) ?? dir : color;
25022
+ ctx.strokeStyle = cs.borderVisible ? (up ? cs.borderUpColor : cs.borderDownColor) ?? color : color;
24592
25023
  ctx.lineWidth = 1;
24593
25024
  const bw = Math.max(0, g.bodyW - 1);
24594
25025
  const bh = Math.max(0, bodyH - 1);
@@ -25020,6 +25451,8 @@ var DrawingSceneRenderer = class {
25020
25451
  this.set = set;
25021
25452
  /** measureText width cache, keyed by `${font} ${text}`, persists across frames. */
25022
25453
  this.widthCache = /* @__PURE__ */ new Map();
25454
+ /** Tooltip hit-rects captured while drawing the CURRENT set (rebuilt per render). */
25455
+ this.tipRegions = [];
25023
25456
  /**
25024
25457
  * Index offset of the CURRENT set's model: its `xloc:'bar_index'` coordinates count
25025
25458
  * from the model's anchor bar, so they shift by this to land on chart logical indices.
@@ -25040,8 +25473,13 @@ var DrawingSceneRenderer = class {
25040
25473
  const s = this.set;
25041
25474
  return !s.lines.length && !s.boxes.length && !s.labels.length && !s.polylines.length && !s.linefills.length;
25042
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
+ }
25043
25480
  /** Draw the whole set into `ctx` using the supplied coordinate closures. */
25044
25481
  render(ctx, W, H, xOf, yOf) {
25482
+ this.tipRegions = [];
25045
25483
  if (this.isEmpty()) return;
25046
25484
  ctx.save();
25047
25485
  this.drawLinefills(ctx, W, H, xOf, yOf);
@@ -25389,13 +25827,35 @@ var DrawingSceneRenderer = class {
25389
25827
  if (this.isPointShape(lb.style)) {
25390
25828
  if (!lb.noFill) this.drawLabelShape(ctx, lb.style, px, py, fontPx, color);
25391
25829
  if (lb.text) this.drawLabelText(ctx, lb, px, py + fontPx, fontPx);
25392
- } else if (lb.style === "none" || lb.style === "text_outline" || lb.noFill) {
25393
- 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
+ }
25394
25839
  } else {
25395
- 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 });
25396
25842
  }
25397
25843
  }
25398
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
+ }
25399
25859
  isPointShape(style) {
25400
25860
  switch (style) {
25401
25861
  case "circle":
@@ -25414,8 +25874,7 @@ var DrawingSceneRenderer = class {
25414
25874
  }
25415
25875
  }
25416
25876
  drawLabelText(ctx, lb, cx, cy, fontPx, outline = false) {
25417
- const family = lb.fontFamily === "monospace" ? "monospace" : this.deps.theme.fontFamily || "sans-serif";
25418
- ctx.font = `${fontPx}px ${family}`;
25877
+ ctx.font = this.labelFont(lb, fontPx);
25419
25878
  ctx.textAlign = lb.textAlign === "left" ? "left" : lb.textAlign === "right" ? "right" : "center";
25420
25879
  ctx.textBaseline = "middle";
25421
25880
  const lines = lb.text.split("\n");
@@ -25495,9 +25954,9 @@ var DrawingSceneRenderer = class {
25495
25954
  ctx.fill();
25496
25955
  }
25497
25956
  }
25957
+ /** Draw a bubble label; returns the bubble body rect (for tooltip hit-testing). */
25498
25958
  drawBubble(ctx, lb, px, py, fontPx, color) {
25499
- const family = lb.fontFamily === "monospace" ? "monospace" : this.deps.theme.fontFamily || "sans-serif";
25500
- ctx.font = `${fontPx}px ${family}`;
25959
+ ctx.font = this.labelFont(lb, fontPx);
25501
25960
  const lines = (lb.text ?? "").split("\n");
25502
25961
  const padX = 6;
25503
25962
  const padY = 4;
@@ -25555,38 +26014,51 @@ var DrawingSceneRenderer = class {
25555
26014
  by = py - h - ptr;
25556
26015
  pointer = "down";
25557
26016
  }
25558
- ctx.fillStyle = color;
25559
- this.roundRect(ctx, bx, by, w, h, 4);
25560
- ctx.fill();
25561
- if (pointer !== "none") {
25562
- ctx.beginPath();
25563
- if (pointer === "down") {
25564
- ctx.moveTo(px - ptr, by + h);
25565
- ctx.lineTo(px + ptr, by + h);
25566
- ctx.lineTo(px, py);
25567
- } else if (pointer === "up") {
25568
- ctx.moveTo(px - ptr, by);
25569
- ctx.lineTo(px + ptr, by);
25570
- ctx.lineTo(px, py);
25571
- } else if (pointer === "left") {
25572
- ctx.moveTo(bx, py - ptr);
25573
- ctx.lineTo(bx, py + ptr);
25574
- ctx.lineTo(px, py);
25575
- } else {
25576
- ctx.moveTo(bx + w, py - ptr);
25577
- ctx.lineTo(bx + w, py + ptr);
25578
- ctx.lineTo(px, py);
25579
- }
25580
- ctx.closePath();
26017
+ if (!lb.noFill) {
26018
+ ctx.fillStyle = color;
26019
+ this.roundRect(ctx, bx, by, w, h, 4);
25581
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
+ }
25582
26043
  }
25583
26044
  if (lb.text) {
25584
- ctx.fillStyle = lb.textColor ?? contrastColor(color);
25585
- ctx.textAlign = "center";
26045
+ ctx.fillStyle = lb.textColor ?? (lb.noFill ? this.deps.theme.textColor : contrastColor(color));
25586
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
+ }
25587
26058
  const startY = by + padY + lineH / 2;
25588
- 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);
25589
26060
  }
26061
+ return { x: bx, y: by, w, h };
25590
26062
  }
25591
26063
  roundRect(ctx, x, y, w, h, r) {
25592
26064
  const rr = Math.min(r, w / 2, h / 2);
@@ -25824,6 +26296,8 @@ var ChromeRenderer = class {
25824
26296
  this.axisTextColor = DARK_THEME.textColor;
25825
26297
  // Shared Pine-drawing renderer (line/box/label/polyline/linefill); widthCache persists.
25826
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 = [];
25827
26301
  }
25828
26302
  mount(canvas) {
25829
26303
  this.canvas = canvas;
@@ -25865,6 +26339,7 @@ var ChromeRenderer = class {
25865
26339
  const dataW = coords.width;
25866
26340
  const dataH = coords.height;
25867
26341
  this.axisTextColor = surface?.textColor ?? theme.textColor;
26342
+ this.labelTips = [];
25868
26343
  ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
25869
26344
  ctx.clearRect(0, 0, fullW, fullH);
25870
26345
  if (surface && (fullW > dataW || fullH > dataH)) {
@@ -25974,6 +26449,17 @@ var ChromeRenderer = class {
25974
26449
  (price) => coords.priceToY(price, pane.scale, pane.bounds) - pane.bounds.top
25975
26450
  );
25976
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;
25977
26463
  }
25978
26464
  // ── axes ──
25979
26465
  drawPriceAxes(ctx, scene, coords, theme, dataW, panes) {
@@ -26190,6 +26676,68 @@ function formatCountdown(ms) {
26190
26676
  return h > 0 ? `${h}:${pad(m)}:${pad(s)}` : `${pad(m)}:${pad(s)}`;
26191
26677
  }
26192
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
+
26193
26741
  // src/renderers/native/chrome/CrosshairRenderer.ts
26194
26742
  var CrosshairRenderer = class {
26195
26743
  constructor() {
@@ -31267,19 +31815,8 @@ function computePaneScale(models, bars, includeCandles, i0, i1, drawings, log =
31267
31815
  for (const model of models) {
31268
31816
  const off = offsetOf(model.id);
31269
31817
  for (const s of model.series) {
31270
- if (s.kind === "candle" || s.kind === "bar") {
31271
- for (let i = i0; i <= i1; i += 1) {
31272
- const b = s.bars[i - off];
31273
- if (b) {
31274
- consider(b.high);
31275
- consider(b.low);
31276
- }
31277
- }
31278
- } else if (isLineLikeSeries(s)) {
31279
- for (let i = i0; i <= i1; i += 1) consider(s.points[i - off]?.value);
31280
- if (s.kind === "histogram" || s.kind === "columns") consider(s.style?.base ?? 0);
31281
- else if (s.style?.base != null) consider(s.style.base);
31282
- }
31818
+ if (s.overlay === true) continue;
31819
+ considerSeries(s, i0, i1, off, consider);
31283
31820
  }
31284
31821
  for (const pl of model.priceLines) consider(pl.price);
31285
31822
  }
@@ -31301,6 +31838,36 @@ function computePaneScale(models, bars, includeCandles, i0, i1, drawings, log =
31301
31838
  const span = max - min;
31302
31839
  return { min: min - span * MARGIN_BOTTOM, max: max + span * MARGIN_TOP };
31303
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
+ }
31304
31871
  function expandScaleByPixels(scale, heightPx, abovePx, belowPx) {
31305
31872
  if (abovePx <= 0 && belowPx <= 0) return scale;
31306
31873
  const content = heightPx - abovePx - belowPx;
@@ -31949,6 +32516,8 @@ var NativeRenderer = class {
31949
32516
  this.glowAmount = 0;
31950
32517
  // WebGL2 neon-glow intensity (canvas2d ignores it)
31951
32518
  this.chrome = new ChromeRenderer();
32519
+ /** Hover tooltips for Pine labels (canvas hit-rects collected by the chrome layer). */
32520
+ this.labelTooltip = null;
31952
32521
  this.crosshairLayer = new CrosshairRenderer();
31953
32522
  /** 1 Hz repaint pump so the price-axis countdown-to-bar-close ticks; null when off. */
31954
32523
  this.countdownTimer = null;
@@ -31959,6 +32528,8 @@ var NativeRenderer = class {
31959
32528
  this.indicatorValuesOn = true;
31960
32529
  /** Host-contributed legend actions — held here so a rebuild of the legend re-wires them. */
31961
32530
  this.legendActionsProvider = null;
32531
+ /** Host-contributed legend callouts — held here so a rebuild of the legend re-wires them. */
32532
+ this.legendCalloutsProvider = null;
31962
32533
  /** Host override of the legend's fold toggle — held here so a remount re-applies it. */
31963
32534
  this.legendOverviewAction = null;
31964
32535
  // ── keyboard navigation / accessibility (item 11) ──
@@ -33067,6 +33638,10 @@ var NativeRenderer = class {
33067
33638
  this.plot.appendChild(this.scrollButton);
33068
33639
  this.plot.addEventListener("pointermove", this.onScrollProximityMove);
33069
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
+ });
33070
33645
  this.userDrawings = new UserDrawingController(this.wrapper, this.plot, this.drawingsCanvas, {
33071
33646
  projector: () => this.drawingProjector(),
33072
33647
  dpr: () => this.coords.dpr,
@@ -33106,9 +33681,10 @@ var NativeRenderer = class {
33106
33681
  this.inputsUI.setDialogHost(this.dialogHost);
33107
33682
  this.inputsUI.setSymbolPicker(this.symbolPicker);
33108
33683
  this.inputsUI.setLegendActions(this.legendActionsProvider);
33684
+ this.inputsUI.setLegendCallouts(this.legendCalloutsProvider);
33109
33685
  this.inputsUI.setLegendOverviewAction(this.legendOverviewAction);
33110
33686
  this.inputsUI.setOnChange((c) => {
33111
- 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 } : {} });
33112
33688
  });
33113
33689
  this.inputsUI.setOnRemove((id) => {
33114
33690
  for (const cb of this.removeIndicatorCbs) cb(id);
@@ -33298,6 +33874,8 @@ var NativeRenderer = class {
33298
33874
  this.settingsButton = null;
33299
33875
  this.plot?.removeEventListener("pointermove", this.onScrollProximityMove);
33300
33876
  this.plot?.removeEventListener("pointerleave", this.onScrollProximityLeave);
33877
+ this.labelTooltip?.destroy();
33878
+ this.labelTooltip = null;
33301
33879
  this.scrollButton?.remove();
33302
33880
  this.scrollButton = null;
33303
33881
  for (const l of this.extLayers) l.instance.destroy?.();
@@ -33538,7 +34116,7 @@ var NativeRenderer = class {
33538
34116
  else this.scene.assignIndicatorZ(model.id);
33539
34117
  this.inputsUI.upsert(model.id, model.shorttitle ?? model.title, model.inputs, model.inputValues, model.paneId, {
33540
34118
  native: !!model.native,
33541
- ...model.shorttitle ? { settingsTitle: model.title } : {}
34119
+ ...model.props ? { props: model.props, propValues: model.propValues ?? {} } : {}
33542
34120
  });
33543
34121
  this.syncTables(model);
33544
34122
  if (model.native?.type === "volume") {
@@ -33589,8 +34167,8 @@ var NativeRenderer = class {
33589
34167
  this.paneControls?.refresh();
33590
34168
  this.scheduler.invalidate(4 /* Full */);
33591
34169
  }
33592
- setIndicatorInputs(handle, values) {
33593
- this.inputsUI.setValues(handle.id, values);
34170
+ setIndicatorInputs(handle, values, props) {
34171
+ this.inputsUI.setValues(handle.id, values, props);
33594
34172
  }
33595
34173
  setSymbolPicker(picker) {
33596
34174
  this.symbolPicker = picker;
@@ -33600,6 +34178,10 @@ var NativeRenderer = class {
33600
34178
  this.legendActionsProvider = provider;
33601
34179
  this.inputsUI?.setLegendActions(provider);
33602
34180
  }
34181
+ setLegendCallouts(provider) {
34182
+ this.legendCalloutsProvider = provider;
34183
+ this.inputsUI?.setLegendCallouts(provider);
34184
+ }
33603
34185
  setLegendOverviewAction(action) {
33604
34186
  this.legendOverviewAction = action;
33605
34187
  this.inputsUI?.setLegendOverviewAction(action);
@@ -34580,7 +35162,11 @@ var NativeRenderer = class {
34580
35162
  }
34581
35163
  const models = this.scene.indicatorsForPane(pane.id);
34582
35164
  const masterModels = models.filter((m) => m.ownScale !== true);
34583
- 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
+ }
34584
35170
  const includeCandles = pane.kind === "price" && !this.scene.candlesHidden;
34585
35171
  pane.scaleTarget = computePaneScale(masterModels, this.bars, includeCandles, i0, i1, dr, paneLogScale(this.scene, pane), (id) => this.scene.offsetOf(id));
34586
35172
  pane.percentBaseline = pane.kind === "price" ? this.bars[i0]?.close ?? 0 : this.firstVisibleValue(masterModels, i0);
@@ -34854,6 +35440,7 @@ var NativeRenderer = class {
34854
35440
  for (const m of models) {
34855
35441
  const off = this.scene.offsetOf(m.id);
34856
35442
  for (const s of m.series) {
35443
+ if (s.overlay === true) continue;
34857
35444
  if (isLineLikeSeries(s)) {
34858
35445
  const v = s.points[i0 - off]?.value;
34859
35446
  if (v != null && Number.isFinite(v)) return v;
@@ -35726,45 +36313,24 @@ var CSS21 = `
35726
36313
  }
35727
36314
  .vela-statusline .vela-sl-symbol { font-weight: 600; font-size: var(--vela-font-size-lg); }
35728
36315
  .vela-statusline .vela-sl-meta { color: var(--vela-fg-muted); font-size: var(--vela-font-size-md); font-weight: 600; }
35729
- /* Market status badge \u2014 icon-only 16px circle, label on hover (kit tooltip). */
35730
- .vela-statusline .vela-sl-market {
35731
- display: inline-grid;
35732
- place-items: center;
35733
- width: 16px;
35734
- height: 16px;
35735
- border-radius: 50%;
35736
- flex: none;
35737
- align-self: center;
35738
- line-height: 0;
35739
- cursor: default;
35740
- }
35741
- .vela-statusline .vela-sl-market svg {
35742
- width: 12px;
35743
- height: 12px;
35744
- display: block;
35745
- }
35746
- /* Open wears the theme's up color; the other sessions are meaning constants from the
35747
- * palette (amber pre, sky post, gray closed/holiday). Same ink at 20% for the circle. */
35748
- .vela-statusline .vela-sl-market[data-status='open'] {
35749
- background: color-mix(in srgb, var(--vela-up) 20%, transparent);
35750
- color: var(--vela-up);
35751
- }
35752
- .vela-statusline .vela-sl-market[data-status='pre'] { background: color-mix(in srgb, ${SESSION_PRE} 20%, transparent); color: ${SESSION_PRE}; }
35753
- .vela-statusline .vela-sl-market[data-status='post'] { background: color-mix(in srgb, ${SESSION_POST} 20%, transparent); color: ${SESSION_POST}; }
35754
- .vela-statusline .vela-sl-market[data-status='closed'],
35755
- .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; }
35756
36319
  .vela-statusline .vela-sl-ohlc { display: flex; gap: var(--vela-space-1); color: var(--vela-fg-muted); }
35757
36320
  .vela-statusline .vela-sl-ohlc b { color: var(--vela-fg); font-weight: 500; }
35758
36321
  /* The change value wears the SAME ink as the OHLC values (set inline per render) \u2014
35759
36322
  * these are the pre-ink fallbacks only. */
35760
36323
  .vela-statusline .vela-sl-change[data-dir='up'] { color: var(--vela-up); }
35761
36324
  .vela-statusline .vela-sl-change[data-dir='down'] { color: var(--vela-down); }
35762
- /* Stack the renderer's PRICE-pane legend below the status line (study panes stay put).
35763
- * The renderer sets the legend's inline top \u2014 shift with a transform, don't fight it.
35764
- * Scoped to hosts that actually CARRY a status line (the marker class set by the
35765
- * Statusline constructor) \u2014 the stylesheet is document-global, so a bare container
35766
- * class here would shift every chart on the page, including statusline-less ones. */
35767
- .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); }
35768
36334
  /* Mobile: two-line chip \u2014 logo / symbol / meta / market status on one aligned row, the
35769
36335
  * bar change on the next. Full O/H/L/C stays hidden (too dense on a phone-width plot).
35770
36336
  * GRID, not a wrapping flexbox: an absolutely positioned wrapping flex container sizes
@@ -35800,7 +36366,7 @@ var CSS21 = `
35800
36366
  font-size: var(--vela-font-size-sm);
35801
36367
  line-height: 1.2;
35802
36368
  }
35803
- [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); }
35804
36370
  /* FIT mode (multi-chart cells \u2014 see setFitMode): the line never wraps; segments that
35805
36371
  * don't fit are HIDDEN by fit() (change first, then meta, then the market badge), so
35806
36372
  * overflow:hidden only guards the transient between a resize and the next measure.
@@ -35817,7 +36383,7 @@ var CSS21 = `
35817
36383
  padding-left: 0;
35818
36384
  }
35819
36385
  /* One row again \u2014 the mobile two-line shift doesn't apply in fit mode. */
35820
- [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); }
35821
36387
  `;
35822
36388
  function baseOfTicker(ticker) {
35823
36389
  return ticker.replace(/[-_/]?(USDT|USDC|USD1|USDS|BUSD|USD|EUR|PERP)$/i, "") || ticker;
@@ -35829,6 +36395,13 @@ var MARKET_LABELS = {
35829
36395
  closed: "Market Closed",
35830
36396
  holiday: "Market Holiday"
35831
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
+ };
35832
36405
  function statuslineInkOf(renderer, priceStyle) {
35833
36406
  const cfg = renderer.getConfig();
35834
36407
  const c = (v) => typeof v === "string" ? v : null;
@@ -35887,8 +36460,15 @@ var Statusline = class {
35887
36460
  this.symbolEl.textContent = ticker;
35888
36461
  this.metaEl = doc.createElement("span");
35889
36462
  this.metaEl.className = "vela-sl-meta";
35890
- this.marketEl = doc.createElement("span");
35891
- 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");
35892
36472
  this.ohlcEl = doc.createElement("span");
35893
36473
  this.ohlcEl.className = "vela-sl-ohlc";
35894
36474
  this.changeEl = doc.createElement("span");
@@ -35976,8 +36556,13 @@ var Statusline = class {
35976
36556
  * hover label. Callers with no session model leave the constructor's 'open'. */
35977
36557
  setMarketStatus(status) {
35978
36558
  this.marketEl.dataset.status = status;
35979
- this.marketEl.innerHTML = icon(`market-${status}`);
35980
- 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
+ });
35981
36566
  this.marketTip.setContent(MARKET_LABELS[status]);
35982
36567
  }
35983
36568
  setPartVisible(part, visible) {
@@ -36010,6 +36595,7 @@ var Statusline = class {
36010
36595
  this.fitRO?.disconnect();
36011
36596
  this.fitRO = null;
36012
36597
  this.marketTip.destroy();
36598
+ this.marketBubble.destroy();
36013
36599
  this.host.classList.remove("vela-has-statusline", "vela-sl-fit-host");
36014
36600
  this.el.remove();
36015
36601
  }
@@ -36236,6 +36822,8 @@ var SessionShadingTracker = class {
36236
36822
  this.spec = null;
36237
36823
  this.ready = false;
36238
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;
36239
36827
  this.covered = null;
36240
36828
  /** The newest range seen — viewport moves during metadata resolution (a load's fit
36241
36829
  * animation) must not be lost, so the resolution always expands the LATEST range. */
@@ -36248,6 +36836,8 @@ var SessionShadingTracker = class {
36248
36836
  this.spec = null;
36249
36837
  this.covered = null;
36250
36838
  this.session = opts.session;
36839
+ const tfMs = timeframeMs(opts.timeframe);
36840
+ this.intraday = Number.isFinite(tfMs) && tfMs < DAY_MS2;
36251
36841
  this.lastRange = opts.range;
36252
36842
  void data.symbolInfo(symbol).catch(() => void 0).then((si) => {
36253
36843
  if (my !== this.epoch) return;
@@ -36275,7 +36865,7 @@ var SessionShadingTracker = class {
36275
36865
  if (force) this.onZones(null);
36276
36866
  return;
36277
36867
  }
36278
- if (this.session !== "extended") {
36868
+ if (this.session !== "extended" || !this.intraday) {
36279
36869
  if (force) this.onZones({ pre: [], post: [] });
36280
36870
  return;
36281
36871
  }
@@ -36690,6 +37280,7 @@ var ChartCell = class {
36690
37280
  this.inner.renderer.set("attribution", false);
36691
37281
  this.inner.renderer.set("dialogHost", deps.dialogHost);
36692
37282
  this.inner.renderer.setLegendActions(legendActionsProviderFor(this.inner, () => deps.context()));
37283
+ this.inner.renderer.setLegendCallouts(legendCalloutsProviderFor(this.inner, () => deps.context()));
36693
37284
  if (this.inner.renderer.supports("historyChords")) this.inner.renderer.set("historyChords", false);
36694
37285
  this.history.onChart(this.inner);
36695
37286
  this.inner.renderer.onConfigChanged(() => {
@@ -36806,20 +37397,30 @@ var ChartCell = class {
36806
37397
  this.refreshNativeCatalog();
36807
37398
  this.pushSettingsSections();
36808
37399
  this.offMarket = this.inner.on("market:changed", ({ symbol: symbol2, timeframe }) => {
36809
- this.state.symbol = symbol2;
36810
- this.state.provider = parseSymbol(symbol2).provider ?? void 0;
36811
- this.state.timeframe = timeframe;
36812
- this.state.session = normalizeSession(this.inner?.market.session);
36813
- this.watermark?.update(symbol2, timeframe);
36814
- this.statusline?.setSymbol(symbol2);
36815
- this.statusline?.setMeta(timeframe, this.inner?.data.displayPrefix(symbol2) ?? this.state.provider ?? "");
36816
- if (this.inner) this.statusline?.onChart(this.inner);
37400
+ this.projectMarket(symbol2, timeframe);
36817
37401
  this.refreshNativeCatalog();
36818
37402
  this.refreshSessionAvailable();
36819
37403
  if (this.inner) this.marketStatus?.track(this.inner.data, symbol2);
36820
37404
  this.deps.onMarketChanged(this.id);
36821
37405
  });
36822
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
+ }
36823
37424
  /**
36824
37425
  * Does this cell's market HAVE sessions (RTH/ETH meaningful)? Derived from the
36825
37426
  * symbol's own metadata (`syminfo.session !== '24x7'`), asynchronously — the
@@ -36865,7 +37466,7 @@ var ChartCell = class {
36865
37466
  const requestedSpan = Math.max(this.state.bars ?? 1e3, this.rangeBars) * timeframeMs(this.state.timeframe ?? "60");
36866
37467
  const fallbackSpan = Number.isFinite(requestedSpan) ? Math.max(3 * 864e5, requestedSpan) : 3 * 864e5;
36867
37468
  const range = chart.getVisibleRange() ?? { from: now - fallbackSpan, to: now };
36868
- this.sessionShading.track(chart.data, symbol, { session: this.session, range });
37469
+ this.sessionShading.track(chart.data, symbol, { session: this.session, timeframe: this.timeframe, range });
36869
37470
  }
36870
37471
  /** The session-shade colors live in the renderer CONFIG (persisted with it, edited
36871
37472
  * live by the dialog swatch) — the cell only proxies them into its settings rows. */
@@ -37054,17 +37655,23 @@ var ChartCell = class {
37054
37655
  get indicatorCount() {
37055
37656
  return this.instances.length + this.nativeCatalog.filter((n) => n.present).length;
37056
37657
  }
37057
- /** 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. */
37058
37661
  setSymbol(symbol) {
37059
37662
  if (!this.inner || symbol === this.symbol) return;
37060
37663
  this.unresolvedToasted = null;
37061
37664
  void this.inner.setMarket({ symbol });
37665
+ this.projectMarket(symbol, this.timeframe);
37666
+ this.deps.onMarketChanged(this.id);
37062
37667
  }
37063
37668
  setTimeframe(timeframe) {
37064
37669
  if (!this.inner || timeframe === this.timeframe) return;
37065
37670
  this.activeRangeId = null;
37066
37671
  this.rangeBars = 0;
37067
37672
  void this.inner.setMarket({ timeframe, bars: this.state.bars });
37673
+ this.projectMarket(this.symbol, timeframe);
37674
+ this.deps.onMarketChanged(this.id);
37068
37675
  }
37069
37676
  /** Applied live (renderer feature) — no reload. */
37070
37677
  setPriceStyle(style) {
@@ -38176,7 +38783,10 @@ var VelaWorkspace = class {
38176
38783
  this.topbar.renderActions();
38177
38784
  this.mobileBar?.renderActions();
38178
38785
  this.dock.refresh();
38179
- 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
+ }
38180
38790
  }
38181
38791
  // ── state surface (the SDK's read/restore of the whole grid's config + content) ──
38182
38792
  /**