@luxalgo/vela 0.7.0 → 0.7.2

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-Bu9E5Zh5.d.ts → DataProvider-BALR0qPq.d.ts} +1 -1
  2. package/dist/{DataProvider-Ck2qyS_9.d.cts → DataProvider-D5uMiJYz.d.cts} +1 -1
  3. package/dist/{chunk-IFJJPXSV.js → chunk-BFA32GOU.js} +44 -16
  4. package/dist/{chunk-CZRNTHD6.js → chunk-BKHSQ4YM.js} +129 -125
  5. package/dist/{chunk-A4G64KVF.js → chunk-PTNO3YK2.js} +311 -111
  6. package/dist/{chunk-A347YL2P.js → chunk-QBWLINWL.js} +67 -9
  7. package/dist/{chunk-UHXG5J7C.js → chunk-ZADTVRUO.js} +1 -1
  8. package/dist/{contributions-BjQ7hyIx.d.ts → contributions-BbhbZd3c.d.ts} +31 -9
  9. package/dist/{contributions-Cv2Mutvn.d.cts → contributions-CcBFe-91.d.cts} +31 -9
  10. package/dist/index.cjs +391 -157
  11. package/dist/index.d.cts +52 -17
  12. package/dist/index.d.ts +52 -17
  13. package/dist/index.js +4 -4
  14. package/dist/{options-ex2_gtKp.d.ts → options-C7b_Qssu.d.cts} +42 -1
  15. package/dist/{options-ex2_gtKp.d.cts → options-C7b_Qssu.d.ts} +42 -1
  16. package/dist/{plugin-CiyhU7pi.d.ts → plugin-Cnix5ZBh.d.ts} +6 -4
  17. package/dist/{plugin-absNH7Yn.d.cts → plugin-D6MlX4--.d.cts} +6 -4
  18. package/dist/plugin.cjs +39 -15
  19. package/dist/plugin.d.cts +4 -4
  20. package/dist/plugin.d.ts +4 -4
  21. package/dist/plugin.js +2 -2
  22. package/dist/providers/binance.d.cts +2 -2
  23. package/dist/providers/binance.d.ts +2 -2
  24. package/dist/providers/coinbase.d.cts +2 -2
  25. package/dist/providers/coinbase.d.ts +2 -2
  26. package/dist/providers/hyperliquid.d.cts +2 -2
  27. package/dist/providers/hyperliquid.d.ts +2 -2
  28. package/dist/{statusline-model-BXoU4Kua.d.ts → statusline-model-D8NL7o4L.d.ts} +3 -3
  29. package/dist/{statusline-model-cRhPBLPO.d.cts → statusline-model-DUKt7_TQ.d.cts} +3 -3
  30. package/dist/ui.cjs +8 -4
  31. package/dist/ui.d.cts +10 -7
  32. package/dist/ui.d.ts +10 -7
  33. package/dist/ui.js +2 -2
  34. package/dist/vela.global.js +391 -157
  35. package/dist/vela.global.min.js +70 -70
  36. package/dist/widget.cjs +418 -128
  37. package/dist/widget.d.cts +6 -6
  38. package/dist/widget.d.ts +6 -6
  39. package/dist/widget.js +6 -6
  40. package/dist/workspace.cjs +418 -128
  41. package/dist/workspace.d.cts +39 -6
  42. package/dist/workspace.d.ts +39 -6
  43. package/dist/workspace.js +5 -5
  44. package/package.json +1 -1
  45. /package/dist/{chunk-FE6V2YAQ.js → chunk-JU4CWEBF.js} +0 -0
package/dist/widget.cjs CHANGED
@@ -1105,6 +1105,9 @@ function applyPlotOverlayTokens(host, base, config) {
1105
1105
  const t = layout?.background && layout.textColor ? { ...base, background: layout.background, textColor: layout.textColor } : base;
1106
1106
  applyThemeTokens(host, t);
1107
1107
  }
1108
+ function floatingLayerHost(from, fallback) {
1109
+ return from.closest(".vela-ui") ?? fallback ?? from;
1110
+ }
1108
1111
  function ensureUIHost(el, theme) {
1109
1112
  injectStyles(STATIC_ID, STATIC_CSS, el.getRootNode());
1110
1113
  el.classList.add("vela-ui");
@@ -1549,7 +1552,7 @@ var MENU_CSS = `
1549
1552
  /* The list is a <ul>: without this, block rows (the separator) paint a ::marker dot. */
1550
1553
  list-style: none;
1551
1554
  margin: 0;
1552
- background: var(--vela-surface-elev);
1555
+ background: var(--vela-surface);
1553
1556
  color: var(--vela-fg);
1554
1557
  border: 1px solid var(--vela-border-strong);
1555
1558
  border-radius: 6px;
@@ -1840,7 +1843,7 @@ var Menu = class {
1840
1843
  const anchor = opts.trigger ?? opts.host ?? document.body;
1841
1844
  const doc = anchor.ownerDocument;
1842
1845
  injectStyles(MENU_STYLE_ID, MENU_CSS, doc);
1843
- const host = opts.host ?? anchor.closest?.(".vela-ui") ?? doc.body;
1846
+ const host = floatingLayerHost(opts.host ?? anchor, doc.body);
1844
1847
  this.root = new Surface(doc, {
1845
1848
  host,
1846
1849
  placement: opts.placement,
@@ -1985,7 +1988,8 @@ var Popover = class {
1985
1988
  const doc = opts.trigger.ownerDocument;
1986
1989
  injectStyles(POPOVER_STYLE_ID, POPOVER_CSS, doc);
1987
1990
  this.trigger = opts.trigger;
1988
- this.host = opts.host ?? doc.body;
1991
+ this.host = opts.host ?? floatingLayerHost(opts.trigger, doc.body);
1992
+ this.inheritsTokens = !opts.host && this.host !== doc.body;
1989
1993
  this.ctrl = popoverController(opts);
1990
1994
  this.boundary = opts.boundary ?? "viewport";
1991
1995
  this.theme = opts.theme;
@@ -2017,7 +2021,7 @@ var Popover = class {
2017
2021
  clearTimeout(this.leaveTimer);
2018
2022
  this.leaveTimer = null;
2019
2023
  }
2020
- ensureUIHost(this.el, this.theme);
2024
+ ensureUIHost(this.el, this.inheritsTokens ? void 0 : this.theme);
2021
2025
  if (this.fadeMs > 0) {
2022
2026
  this.el.style.transition = `opacity ${this.fadeMs}ms ease`;
2023
2027
  this.el.style.opacity = "0";
@@ -8208,16 +8212,18 @@ var Pitchfork = class extends SegmentDrawing {
8208
8212
  geometry(proj) {
8209
8213
  const a = this.anchors[0];
8210
8214
  const b = this.anchors[1];
8211
- const c = this.anchors[2];
8212
- if (!a || !b || !c) return null;
8215
+ if (!a || !b) return null;
8213
8216
  const px = (p) => {
8214
8217
  const y = proj.yOf(p.price, this.paneId);
8215
8218
  return y == null ? null : [proj.xOf(p.time), y];
8216
8219
  };
8217
8220
  const P0 = px(a);
8218
8221
  const P1 = px(b);
8222
+ if (!P0 || !P1) return null;
8223
+ const c = this.anchors[2];
8224
+ if (!c) return { segments: [[P0[0], P0[1], P1[0], P1[1]]], fill: null };
8219
8225
  const P2 = px(c);
8220
- if (!P0 || !P1 || !P2) return null;
8226
+ if (!P2) return null;
8221
8227
  const mx = (P1[0] + P2[0]) / 2;
8222
8228
  const my = (P1[1] + P2[1]) / 2;
8223
8229
  const dx = mx - P0[0];
@@ -8264,16 +8270,19 @@ var PitchforkVariant = class extends SegmentDrawing {
8264
8270
  geometry(proj) {
8265
8271
  const a = this.anchors[0];
8266
8272
  const b = this.anchors[1];
8267
- const c = this.anchors[2];
8268
- if (!a || !b || !c) return null;
8273
+ if (!a || !b) return null;
8269
8274
  const px = (p) => {
8270
8275
  const y = proj.yOf(p.price, this.paneId);
8271
8276
  return y == null ? null : [proj.xOf(p.time), y];
8272
8277
  };
8273
- const S2 = px(this.medianStart(a, b, c));
8278
+ const P0 = px(a);
8274
8279
  const P1 = px(b);
8280
+ if (!P0 || !P1) return null;
8281
+ const c = this.anchors[2];
8282
+ if (!c) return { segments: [[P0[0], P0[1], P1[0], P1[1]]], fill: null };
8283
+ const S2 = px(this.medianStart(a, b, c));
8275
8284
  const P2 = px(c);
8276
- if (!S2 || !P1 || !P2) return null;
8285
+ if (!S2 || !P2) return null;
8277
8286
  const mx = (P1[0] + P2[0]) / 2;
8278
8287
  const my = (P1[1] + P2[1]) / 2;
8279
8288
  const dx = mx - S2[0];
@@ -8288,8 +8297,12 @@ var PitchforkVariant = class extends SegmentDrawing {
8288
8297
  // upper tine
8289
8298
  extendRay(P2[0], P2[1], P2[0] + dx, P2[1] + dy, "right", w, h),
8290
8299
  // lower tine
8291
- [P1[0], P1[1], P2[0], P2[1]]
8300
+ [P1[0], P1[1], P2[0], P2[1]],
8292
8301
  // base line
8302
+ // The median leaves from the shifted origin, not the pivot — without this construction
8303
+ // line the pivot handle would float unattached to the shape it defines.
8304
+ [P0[0], P0[1], P1[0], P1[1]]
8305
+ // pivot → first tine anchor
8293
8306
  ],
8294
8307
  fill: null
8295
8308
  };
@@ -8346,17 +8359,19 @@ var InsidePitchfork = class extends SegmentDrawing {
8346
8359
  geometry(proj) {
8347
8360
  const a = this.anchors[0];
8348
8361
  const b = this.anchors[1];
8349
- const c = this.anchors[2];
8350
- if (!a || !b || !c) return null;
8362
+ if (!a || !b) return null;
8351
8363
  const px = (p) => {
8352
8364
  const y = proj.yOf(p.price, this.paneId);
8353
8365
  return y == null ? null : [proj.xOf(p.time), y];
8354
8366
  };
8355
- const B = px({ time: (a.time + b.time) / 2, price: (a.price + b.price) / 2 });
8356
8367
  const P0 = px(a);
8357
8368
  const P1 = px(b);
8369
+ if (!P0 || !P1) return null;
8370
+ const c = this.anchors[2];
8371
+ if (!c) return { segments: [[P0[0], P0[1], P1[0], P1[1]]], fill: null };
8372
+ const B = px({ time: (a.time + b.time) / 2, price: (a.price + b.price) / 2 });
8358
8373
  const P2 = px(c);
8359
- if (!B || !P0 || !P1 || !P2) return null;
8374
+ if (!B || !P2) return null;
8360
8375
  const mx = (P1[0] + P2[0]) / 2;
8361
8376
  const my = (P1[1] + P2[1]) / 2;
8362
8377
  const dx = P2[0] - B[0];
@@ -8527,16 +8542,18 @@ var Triangle = class extends SegmentDrawing {
8527
8542
  geometry(proj) {
8528
8543
  const a = this.anchors[0];
8529
8544
  const b = this.anchors[1];
8530
- const c = this.anchors[2];
8531
- if (!a || !b || !c) return null;
8545
+ if (!a || !b) return null;
8532
8546
  const px = (p) => {
8533
8547
  const y = proj.yOf(p.price, this.paneId);
8534
8548
  return y == null ? null : [proj.xOf(p.time), y];
8535
8549
  };
8536
8550
  const A = px(a);
8537
8551
  const B = px(b);
8552
+ if (!A || !B) return null;
8553
+ const c = this.anchors[2];
8554
+ if (!c) return { segments: [[A[0], A[1], B[0], B[1]]], fill: null };
8538
8555
  const C = px(c);
8539
- if (!A || !B || !C) return null;
8556
+ if (!C) return null;
8540
8557
  return {
8541
8558
  segments: [
8542
8559
  [A[0], A[1], B[0], B[1]],
@@ -18412,6 +18429,13 @@ function resolveAnimations(animations) {
18412
18429
  function isLineLikeSeries(spec) {
18413
18430
  return spec.kind !== "candle" && spec.kind !== "bar" && spec.kind !== "markers";
18414
18431
  }
18432
+ function seriesShownOn(spec, surface) {
18433
+ if (spec.display) return spec.display[surface] !== false;
18434
+ return spec.visible !== false;
18435
+ }
18436
+ function seriesInScale(spec) {
18437
+ return seriesShownOn(spec, "pane") || seriesShownOn(spec, "priceScale");
18438
+ }
18415
18439
 
18416
18440
  // src/core/engine/IndicatorRegistry.ts
18417
18441
  var IndicatorRegistry = class {
@@ -18441,6 +18465,9 @@ var IndicatorRegistry = class {
18441
18465
  };
18442
18466
 
18443
18467
  // src/core/native-indicators/NativeIndicator.ts
18468
+ function nativeInstanceChannel(type, instanceId) {
18469
+ return `${type}#${instanceId}`;
18470
+ }
18444
18471
  var REGISTRY2 = /* @__PURE__ */ new Map();
18445
18472
  function registerNativeIndicator(descriptor) {
18446
18473
  REGISTRY2.set(descriptor.type, descriptor);
@@ -18510,6 +18537,9 @@ var IndicatorHandleImpl = class {
18510
18537
  setProps(values) {
18511
18538
  this.controller.applyProps(this.id, values);
18512
18539
  }
18540
+ updateCode(source) {
18541
+ this.controller.updateCode(this.id, source);
18542
+ }
18513
18543
  setVisible(visible) {
18514
18544
  this.controller.setVisible(this.id, visible);
18515
18545
  }
@@ -18532,6 +18562,10 @@ var IndicatorHandleImpl = class {
18532
18562
  setPropsSchema(schema) {
18533
18563
  this.propsSchema = schema;
18534
18564
  }
18565
+ /** Sync the public `source` once a code update has been prepared and taken over. */
18566
+ setSource(source) {
18567
+ this.source = source;
18568
+ }
18535
18569
  /** Sync the public `visible` getter when the orchestrator changes visibility (API or legend eye). */
18536
18570
  setVisibleState(visible) {
18537
18571
  this.visibleState = visible;
@@ -19352,6 +19386,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
19352
19386
  if (identityChanged) {
19353
19387
  this.beginLoad(false);
19354
19388
  this.setBarSeries([], { clearing: true });
19389
+ this.blankIndicatorVisuals();
19355
19390
  if (this.activeEngineStyle) {
19356
19391
  this.typeEngines.get(this.activeEngineStyle)?.suspend();
19357
19392
  this.renderer.setNativeData?.(this.activeEngineStyle, void 0);
@@ -19394,7 +19429,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
19394
19429
  restartNativeIndicators() {
19395
19430
  for (const record of this.registry.all()) {
19396
19431
  if (!record.native) continue;
19397
- record.native.instance.stop();
19432
+ if (record.native.started) record.native.instance.stop();
19398
19433
  record.native.instance = record.native.descriptor.create();
19399
19434
  record.native.started = false;
19400
19435
  record.pendingStructural = true;
@@ -19602,6 +19637,23 @@ var EngineOrchestrator = class _EngineOrchestrator {
19602
19637
  this.renderer.setBars(this.bars, { preserveView: true });
19603
19638
  this.reexecuteIndicators();
19604
19639
  }
19640
+ /**
19641
+ * Blank every mounted indicator's painted output — series data, fills, backgrounds,
19642
+ * price lines, drawings, bar colors, trades — while keeping the mount (legend row,
19643
+ * pane, settings) intact, via the same idempotent-by-id remount an input edit uses.
19644
+ * Called on a market identity switch: the fresh models arrive only after the new
19645
+ * bars load, so nothing blanked here is ever restored — each consumer's re-run
19646
+ * remounts a full model over it.
19647
+ */
19648
+ blankIndicatorVisuals() {
19649
+ for (const record of this.registry.all()) {
19650
+ if (record.hidden || !record.renderHandle || !record.model) continue;
19651
+ record.model = blankedModel(record.model);
19652
+ record.renderHandle = this.renderer.mountIndicator(record.model);
19653
+ record.pendingStructural = true;
19654
+ this.setLoading(record, true);
19655
+ }
19656
+ }
19605
19657
  /** Stop + re-run every visible Pine indicator (its input bars changed wholesale). */
19606
19658
  reexecuteIndicators() {
19607
19659
  for (const record of this.registry.all()) {
@@ -19846,6 +19898,61 @@ var EngineOrchestrator = class _EngineOrchestrator {
19846
19898
  record.session.update(record.inputValues, record.propValues);
19847
19899
  this.events.emit("indicator:inputs", { id });
19848
19900
  }
19901
+ /**
19902
+ * IndicatorController: replace a SCRIPT indicator's source in place (see
19903
+ * {@link IndicatorHandle.updateCode}). Prepare-first: the running session keeps
19904
+ * computing until the new source has compiled, so a broken edit costs the user
19905
+ * nothing but an `error` event. Natives have no script — warn and leave them alone.
19906
+ */
19907
+ updateCode(id, source) {
19908
+ const record = this.registry.get(id);
19909
+ const handle = this.handles.get(id);
19910
+ if (!record || !handle) return;
19911
+ if (record.native) {
19912
+ console.warn(`[vela] updateCode("${id}") \u2014 a native indicator has no script to update.`);
19913
+ return;
19914
+ }
19915
+ if (source === record.source && record.pendingSource === void 0) return;
19916
+ record.pendingSource = source;
19917
+ void this.swapSource(id, source, handle);
19918
+ }
19919
+ async swapSource(id, source, handle) {
19920
+ const engine = this.registry.get(id)?.engine ?? this.engineFor(this.registry.get(id)?.options?.language);
19921
+ let prepared;
19922
+ try {
19923
+ prepared = await engine.prepare(source, id);
19924
+ } catch (err) {
19925
+ const record2 = this.registry.get(id);
19926
+ if (record2?.pendingSource === source) {
19927
+ record2.pendingSource = void 0;
19928
+ this.fail(id, handle, err);
19929
+ }
19930
+ return;
19931
+ }
19932
+ const record = this.registry.get(id);
19933
+ if (!record || record.pendingSource !== source) return;
19934
+ record.pendingSource = void 0;
19935
+ record.source = source;
19936
+ record.engine = engine;
19937
+ record.prepared = prepared;
19938
+ handle.setSource(source);
19939
+ record.inputValues = valuesOnSchema(prepared.inputs, record.inputValues);
19940
+ record.propValues = valuesOnSchema(prepared.props ?? [], record.propValues);
19941
+ handle.setSchema(prepared.inputs);
19942
+ handle.setPropsSchema(prepared.props ?? []);
19943
+ record.session?.stop();
19944
+ record.session = void 0;
19945
+ record.pendingStructural = true;
19946
+ record.pendingCause = "code";
19947
+ if (record.hidden) return;
19948
+ if (record.renderHandle) {
19949
+ this.renderer.setIndicatorInputs(record.renderHandle, record.inputValues, record.propValues);
19950
+ this.setLoading(record, true);
19951
+ } else {
19952
+ this.mountLoadingPlaceholder(id, record);
19953
+ }
19954
+ this.executeIndicator(id, handle);
19955
+ }
19849
19956
  /** IndicatorController: tear down an indicator and (if now empty) its pane. */
19850
19957
  /** Live handles of every indicator on the chart (script + native), insertion order. */
19851
19958
  listIndicators() {
@@ -19860,7 +19967,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
19860
19967
  const record = this.registry.remove(id);
19861
19968
  if (record?.native?.type === "volume") this.volumeOptedOut = true;
19862
19969
  record?.session?.stop();
19863
- record?.native?.instance.stop();
19970
+ if (record?.native?.started) record.native.instance.stop();
19864
19971
  this.handles.delete(id);
19865
19972
  if (record?.renderHandle) this.renderer.removeIndicator(record.renderHandle);
19866
19973
  const paneId = record?.model?.paneId;
@@ -19886,7 +19993,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
19886
19993
  if (!visible) {
19887
19994
  record.session?.stop();
19888
19995
  record.session = void 0;
19889
- record.native?.instance.suspend();
19996
+ if (record.native?.started) record.native.instance.suspend();
19890
19997
  if (record.renderHandle) this.renderer.setIndicatorVisible?.(record.renderHandle, false);
19891
19998
  else if (record.native) this.mountHiddenNativeRow(id, record);
19892
19999
  } else {
@@ -19967,7 +20074,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
19967
20074
  this.paneActionUnsub?.();
19968
20075
  for (const record of this.registry.all()) {
19969
20076
  record.session?.stop();
19970
- record.native?.instance.stop();
20077
+ if (record.native?.started) record.native.instance.stop();
19971
20078
  }
19972
20079
  for (const engine of this.typeEngines.values()) engine.stop();
19973
20080
  this.typeEngines.clear();
@@ -19991,6 +20098,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
19991
20098
  const engine = this.engineFor(options.language);
19992
20099
  record.engine = engine;
19993
20100
  const prepared = await engine.prepare(source, id);
20101
+ if (record.source !== source) return;
19994
20102
  record.prepared = prepared;
19995
20103
  const defaults2 = {};
19996
20104
  for (const input of prepared.inputs) defaults2[input.key] = input.defval;
@@ -20061,7 +20169,9 @@ var EngineOrchestrator = class _EngineOrchestrator {
20061
20169
  await this.readyPromise;
20062
20170
  const record = this.registry.get(id);
20063
20171
  if (!record?.native || record.hidden) return;
20172
+ const channel = this.nativeChannel(record);
20064
20173
  const ctx = {
20174
+ id,
20065
20175
  symbol: this.config.market.symbol ?? "TEST",
20066
20176
  timeframe: this.config.market.timeframe ?? "60",
20067
20177
  live: this.config.live,
@@ -20069,7 +20179,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
20069
20179
  bars: () => this.bars,
20070
20180
  data: this.dataControl,
20071
20181
  emit: (out) => this.applyModel(id, this.buildNativeModel(record, out)),
20072
- pushData: (data) => this.renderer.setNativeData?.(record.native.type, data),
20182
+ pushData: (data) => this.renderer.setNativeData?.(channel, data),
20073
20183
  setStatus: (status) => {
20074
20184
  if (record.renderHandle && !record.hidden) this.renderer.setIndicatorStatus?.(record.renderHandle, status);
20075
20185
  }
@@ -20080,16 +20190,26 @@ var EngineOrchestrator = class _EngineOrchestrator {
20080
20190
  this.fail(id, handle, err);
20081
20191
  }
20082
20192
  }
20193
+ /** The renderer-layer channel a native instance's `pushData` lands on: the type itself
20194
+ * for a single-instance type (the layer id doubles as the channel), a per-instance
20195
+ * channel for a `multiInstance` type — otherwise every instance would overwrite the
20196
+ * one layer. Stamped on the model so the renderer mounts a dedicated layer for it. */
20197
+ nativeChannel(record) {
20198
+ const { type, descriptor } = record.native;
20199
+ return descriptor.multiInstance ? nativeInstanceChannel(type, record.id) : type;
20200
+ }
20083
20201
  /** Wrap a native indicator's emitted visuals into a full IndicatorModel, tagged `native`. */
20084
20202
  buildNativeModel(record, out) {
20085
20203
  const d = record.native.descriptor;
20204
+ const type = record.native.type;
20205
+ const channel = this.nativeChannel(record);
20086
20206
  return {
20087
20207
  id: record.id,
20088
20208
  title: record.title,
20089
20209
  ...d.shortTitle ? { shorttitle: d.shortTitle } : {},
20090
20210
  overlay: d.overlay,
20091
20211
  paneHint: d.paneHint,
20092
- native: { type: record.native.type },
20212
+ native: { type, ...channel !== type ? { channel } : {} },
20093
20213
  ...d.legend === false ? { legend: false } : {},
20094
20214
  ...out.paneAxis != null ? { paneAxis: out.paneAxis } : {},
20095
20215
  series: out.series ?? [],
@@ -20379,6 +20499,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
20379
20499
  applyModel(id, model) {
20380
20500
  const record = this.registry.get(id);
20381
20501
  if (!record || record.hidden) return true;
20502
+ if (this.switchingMarket) return false;
20382
20503
  if (record.loading && this.bars.length === 0 && !_EngineOrchestrator.modelHasOutput(model)) return false;
20383
20504
  const handle = this.handles.get(id);
20384
20505
  if (!record.renderHandle) {
@@ -20396,7 +20517,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
20396
20517
  if (record.loading && !record.paneLocked) {
20397
20518
  const routed = this.routePane(id, model, record.options ?? {});
20398
20519
  if (routed !== paneId) {
20399
- if (paneId !== "price") {
20520
+ if (paneId !== "price" && !this.registry.all().some((r) => r.id !== id && r.model?.paneId === paneId)) {
20400
20521
  this.renderer.removePane(paneId);
20401
20522
  this.forgetPane(paneId);
20402
20523
  }
@@ -20525,6 +20646,34 @@ function yieldToPaint() {
20525
20646
  }
20526
20647
  return new Promise((resolve) => setTimeout(resolve, 0));
20527
20648
  }
20649
+ function valuesOnSchema(schema, previous) {
20650
+ const out = {};
20651
+ const declared = /* @__PURE__ */ new Set();
20652
+ for (const s of schema) {
20653
+ out[s.key] = s.defval;
20654
+ declared.add(s.key);
20655
+ declared.add(s.title);
20656
+ }
20657
+ for (const [k, v] of Object.entries(previous)) if (declared.has(k)) out[k] = v;
20658
+ return out;
20659
+ }
20660
+ function blankedModel(model) {
20661
+ return {
20662
+ ...model,
20663
+ series: model.series.map((s) => s.kind === "candle" || s.kind === "bar" ? { ...s, bars: [] } : s.kind === "markers" ? { ...s, markers: [] } : { ...s, points: [] }),
20664
+ fills: [],
20665
+ backgrounds: [],
20666
+ priceLines: [],
20667
+ lines: [],
20668
+ boxes: [],
20669
+ labels: [],
20670
+ polylines: [],
20671
+ linefills: [],
20672
+ tables: [],
20673
+ barColors: [],
20674
+ trades: []
20675
+ };
20676
+ }
20528
20677
  function modelToValuePatch(model) {
20529
20678
  const series = [];
20530
20679
  let from = Number.POSITIVE_INFINITY;
@@ -23284,33 +23433,32 @@ function candleGeometry(xCss, spacing, dpr, bodyScale = 1) {
23284
23433
  center: (wickLeftDev + wickDev / 2) / dpr
23285
23434
  };
23286
23435
  }
23287
- function aggregateCandleColumns(bars, i0, i1, xOf, yOf) {
23436
+ function aggregateCandleColumns(bars, i0, i1, xOf, yOf, colorOf2) {
23288
23437
  const out = [];
23289
23438
  let col = NaN;
23290
- let intervals = [];
23439
+ let runs = [];
23291
23440
  const flush = () => {
23292
- if (intervals.length === 0) return;
23293
- const merged = [intervals[0]];
23294
- for (let k = 1; k < intervals.length; k += 1) {
23295
- const prev2 = merged[merged.length - 1];
23296
- const next = intervals[k];
23297
- if (Math.abs(yOf(prev2.hi) - yOf(next.lo)) < 1) {
23298
- prev2.hi = next.hi;
23299
- if (next.headIdx < prev2.headIdx) {
23300
- prev2.headIdx = next.headIdx;
23301
- prev2.headTime = next.headTime;
23302
- prev2.open = next.open;
23303
- }
23304
- if (next.lastIdx > prev2.lastIdx) {
23305
- prev2.lastIdx = next.lastIdx;
23306
- prev2.close = next.close;
23441
+ if (runs.length === 0) return;
23442
+ runs.sort((a, b) => a.lo - b.lo);
23443
+ const merged = [];
23444
+ for (const next of runs) {
23445
+ let prev2;
23446
+ for (let k = merged.length - 1; k >= 0; k -= 1) {
23447
+ if (merged[k].color === next.color) {
23448
+ prev2 = merged[k];
23449
+ break;
23307
23450
  }
23451
+ }
23452
+ if (prev2 && Math.abs(yOf(prev2.hi) - yOf(next.lo)) < 1) {
23453
+ prev2.hi = next.hi;
23454
+ if (next.lastIdx > prev2.lastIdx) prev2.lastIdx = next.lastIdx;
23308
23455
  } else {
23309
23456
  merged.push(next);
23310
23457
  }
23311
23458
  }
23312
- for (const iv of merged) out.push({ x: col, hi: iv.hi, lo: iv.lo, headTime: iv.headTime, open: iv.open, close: iv.close });
23313
- intervals = [];
23459
+ merged.sort((a, b) => a.lastIdx - b.lastIdx);
23460
+ for (const r of merged) out.push({ x: col, hi: r.hi, lo: r.lo, color: r.color });
23461
+ runs = [];
23314
23462
  };
23315
23463
  for (let i = i0; i <= i1; i += 1) {
23316
23464
  const b = bars[i];
@@ -23320,26 +23468,17 @@ function aggregateCandleColumns(bars, i0, i1, xOf, yOf) {
23320
23468
  flush();
23321
23469
  col = x;
23322
23470
  }
23471
+ const color = colorOf2(b);
23323
23472
  let lo = b.low;
23324
23473
  let hi = b.high;
23325
- let headIdx = i;
23326
- let headTime = b.time;
23327
- let open2 = b.open;
23328
- for (let k = intervals.length - 1; k >= 0; k -= 1) {
23329
- const iv = intervals[k];
23330
- if (iv.lo > hi || iv.hi < lo) continue;
23331
- if (iv.lo < lo) lo = iv.lo;
23332
- if (iv.hi > hi) hi = iv.hi;
23333
- if (iv.headIdx < headIdx) {
23334
- headIdx = iv.headIdx;
23335
- headTime = iv.headTime;
23336
- open2 = iv.open;
23337
- }
23338
- intervals.splice(k, 1);
23339
- }
23340
- let insertAt = 0;
23341
- while (insertAt < intervals.length && intervals[insertAt].lo < lo) insertAt += 1;
23342
- intervals.splice(insertAt, 0, { lo, hi, headIdx, headTime, open: open2, lastIdx: i, close: b.close });
23474
+ for (let k = runs.length - 1; k >= 0; k -= 1) {
23475
+ const r = runs[k];
23476
+ if (r.color !== color || r.lo > hi || r.hi < lo) continue;
23477
+ if (r.lo < lo) lo = r.lo;
23478
+ if (r.hi > hi) hi = r.hi;
23479
+ runs.splice(k, 1);
23480
+ }
23481
+ runs.push({ lo, hi, color, lastIdx: i });
23343
23482
  }
23344
23483
  flush();
23345
23484
  return out;
@@ -24010,7 +24149,7 @@ var WebGL2Backend = class {
24010
24149
  * Routing mirrors the main pass: own series per pane, force_overlay series on the price pane. */
24011
24150
  emitGlowSources(b, scene, pane, coords, i0, i1) {
24012
24151
  const emitOne = (s, off) => {
24013
- if (!isLineLikeSeries(s) || s.visible === false) return;
24152
+ if (!isLineLikeSeries(s) || !seriesShownOn(s, "pane")) return;
24014
24153
  if (s.kind === "histogram" || s.kind === "columns") return;
24015
24154
  if (s.kind === "circles" || s.kind === "cross") this.emitPointMarkers(b, s, pane, coords, i0, i1, off);
24016
24155
  else this.emitPolyline(b, s, pane, coords, i0, i1, s.kind === "step", off);
@@ -24121,7 +24260,7 @@ var WebGL2Backend = class {
24121
24260
  const spacing = coords.bodySpacing();
24122
24261
  const tier = candleTier(spacing);
24123
24262
  if (tier === "aggregate") {
24124
- this.emitCandlesAggregated(b, bars, i0, i1, coords, pane, up, down, barColors);
24263
+ this.emitCandlesAggregated(b, bars, i0, i1, coords, pane, (bar) => barColors.get(bar.time) ?? (bar.close >= bar.open ? up : down));
24125
24264
  return;
24126
24265
  }
24127
24266
  const tickW = Math.max(1, Math.round(spacing * 0.35));
@@ -24262,14 +24401,17 @@ var WebGL2Backend = class {
24262
24401
  const spacing = coords.bodySpacing();
24263
24402
  const tier = candleTier(spacing);
24264
24403
  const paint = effectiveCandlePaint(scene.style.candle, scene.candleOverride, theme.upColor, theme.downColor);
24404
+ const up = paint.up;
24405
+ const down = paint.down;
24406
+ const cs = paint.candle;
24265
24407
  if (tier === "aggregate") {
24266
- this.emitCandlesAggregated(b, bars, i0, i1, coords, pane, paint.up, paint.down, barColors);
24408
+ this.emitCandlesAggregated(b, bars, i0, i1, coords, pane, (bar) => {
24409
+ const isUp = bar.close >= bar.open;
24410
+ return (isUp ? cs.wickUpColor : cs.wickDownColor) ?? barColors.get(bar.time) ?? (isUp ? up : down);
24411
+ });
24267
24412
  return;
24268
24413
  }
24269
24414
  const drawBody = tier === "full";
24270
- const up = paint.up;
24271
- const down = paint.down;
24272
- const cs = paint.candle;
24273
24415
  const fading = this.candleStructureAlpha > this.candleBodyAlpha + 1e-3;
24274
24416
  for (let i = i0; i <= i1; i += 1) {
24275
24417
  const bar = bars[i];
@@ -24318,24 +24460,26 @@ var WebGL2Backend = class {
24318
24460
  /**
24319
24461
  * Sub-pixel LOD (mirrors Canvas2dBackend.drawCandlesAggregated): bars sharing a
24320
24462
  * rounded pixel column collapse into high-low sticks — one per contiguous
24321
- * coverage run (see {@link aggregateCandleColumns}), so a price gap inside the
24322
- * column stays a void. Draw cost stays bounded by screen width, not bar count;
24323
- * stick color follows its own first-open→last-close.
24463
+ * SAME-COLOR coverage run (see {@link aggregateCandleColumns}), so a price gap
24464
+ * inside the column stays a void and every bar's extent keeps its own color. Draw
24465
+ * cost stays bounded by screen width, not bar count. `colorOf` is the calling
24466
+ * style's per-bar paint rule (candles and OHLC bars resolve theirs differently).
24324
24467
  */
24325
- emitCandlesAggregated(b, bars, i0, i1, coords, pane, up, down, barColors) {
24468
+ emitCandlesAggregated(b, bars, i0, i1, coords, pane, colorOf2) {
24326
24469
  const yOf = (price) => coords.priceToY(price, pane.scale, pane.bounds);
24327
- for (const s of aggregateCandleColumns(bars, i0, i1, (i) => coords.logicalToX(i), yOf)) {
24328
- const c = parseColor(barColors.get(s.headTime) ?? (s.close >= s.open ? up : down));
24470
+ for (const s of aggregateCandleColumns(bars, i0, i1, (i) => coords.logicalToX(i), yOf, colorOf2)) {
24471
+ const c = parseColor(s.color);
24329
24472
  const hY = yOf(s.hi);
24330
24473
  b.rect(s.x - 0.5, hY, 1, yOf(s.lo) - hY, c);
24331
24474
  }
24332
24475
  }
24333
24476
  emitSeries(b, spec, pane, coords, i0, i1, theme, off = 0) {
24477
+ if (!seriesShownOn(spec, "pane")) return;
24334
24478
  if (spec.kind === "candle" || spec.kind === "bar") {
24335
24479
  this.emitPlotCandles(b, spec, pane, coords, i0, i1, theme, off);
24336
24480
  return;
24337
24481
  }
24338
- if (!isLineLikeSeries(spec) || spec.visible === false) return;
24482
+ if (!isLineLikeSeries(spec)) return;
24339
24483
  switch (spec.kind) {
24340
24484
  case "histogram":
24341
24485
  case "columns":
@@ -25999,7 +26143,7 @@ var Canvas2dBackend = class {
25999
26143
  const spacing = coords.bodySpacing();
26000
26144
  const tier = candleTier(spacing);
26001
26145
  if (tier === "aggregate") {
26002
- this.drawCandlesAggregated(ctx, bars, i0, i1, coords, pane, up, down, barColors);
26146
+ this.drawCandlesAggregated(ctx, bars, i0, i1, coords, pane, (b) => barColors.get(b.time) ?? (b.close >= b.open ? up : down));
26003
26147
  return;
26004
26148
  }
26005
26149
  const tickW = Math.max(1, Math.round(spacing * 0.35));
@@ -26175,12 +26319,15 @@ var Canvas2dBackend = class {
26175
26319
  const spacing = coords.bodySpacing();
26176
26320
  const tier = candleTier(spacing);
26177
26321
  const paint = effectiveCandlePaint(scene.style.candle, scene.candleOverride, theme.upColor, theme.downColor);
26322
+ const cs = paint.candle;
26178
26323
  if (tier === "aggregate") {
26179
- this.drawCandlesAggregated(ctx, bars, i0, i1, coords, pane, paint.up, paint.down, barColors);
26324
+ this.drawCandlesAggregated(ctx, bars, i0, i1, coords, pane, (b) => {
26325
+ const up = b.close >= b.open;
26326
+ return (up ? cs.wickUpColor : cs.wickDownColor) ?? barColors.get(b.time) ?? (up ? paint.up : paint.down);
26327
+ });
26180
26328
  return;
26181
26329
  }
26182
26330
  const drawBody = tier === "full";
26183
- const cs = paint.candle;
26184
26331
  const fading = this.candleStructureAlpha > this.candleBodyAlpha + 1e-3;
26185
26332
  for (let i = i0; i <= i1; i += 1) {
26186
26333
  const b = bars[i];
@@ -26237,17 +26384,18 @@ var Canvas2dBackend = class {
26237
26384
  }
26238
26385
  /**
26239
26386
  * Sub-pixel LOD: bars sharing a rounded pixel column collapse into high-low
26240
- * sticks (one per contiguous coverage run — see {@link aggregateCandleColumns}),
26241
- * so draw cost is bounded by screen width (not bar count) when zoomed far out
26242
- * and a price gap inside the column stays a void. Each stick's color follows
26243
- * its first-open→last-close direction (a barcolor() on the head bar still wins).
26387
+ * sticks (one per contiguous SAME-COLOR coverage run — see
26388
+ * {@link aggregateCandleColumns}), so draw cost is bounded by screen width (not
26389
+ * bar count) when zoomed far out, a price gap inside the column stays a void, and
26390
+ * every bar's extent keeps its own color. `colorOf` is the calling style's per-bar
26391
+ * paint rule (candles and OHLC bars resolve theirs differently).
26244
26392
  */
26245
- drawCandlesAggregated(ctx, bars, i0, i1, coords, pane, up, down, barColors) {
26393
+ drawCandlesAggregated(ctx, bars, i0, i1, coords, pane, colorOf2) {
26246
26394
  const yOf = (price) => coords.priceToY(price, pane.scale, pane.bounds);
26247
26395
  ctx.lineWidth = 1;
26248
- for (const s of aggregateCandleColumns(bars, i0, i1, (i) => coords.logicalToX(i), yOf)) {
26396
+ for (const s of aggregateCandleColumns(bars, i0, i1, (i) => coords.logicalToX(i), yOf, colorOf2)) {
26249
26397
  const x = s.x + 0.5;
26250
- ctx.strokeStyle = barColors.get(s.headTime) ?? (s.close >= s.open ? up : down);
26398
+ ctx.strokeStyle = s.color;
26251
26399
  ctx.beginPath();
26252
26400
  ctx.moveTo(x, yOf(s.hi));
26253
26401
  ctx.lineTo(x, yOf(s.lo));
@@ -26255,11 +26403,12 @@ var Canvas2dBackend = class {
26255
26403
  }
26256
26404
  }
26257
26405
  drawSeries(ctx, spec, pane, coords, i0, i1, theme, off = 0) {
26406
+ if (!seriesShownOn(spec, "pane")) return;
26258
26407
  if (spec.kind === "candle" || spec.kind === "bar") {
26259
26408
  this.drawPlotCandles(ctx, spec, pane, coords, i0, i1, theme, off);
26260
26409
  return;
26261
26410
  }
26262
- if (!isLineLikeSeries(spec) || spec.visible === false) return;
26411
+ if (!isLineLikeSeries(spec)) return;
26263
26412
  switch (spec.kind) {
26264
26413
  case "histogram":
26265
26414
  case "columns":
@@ -34485,6 +34634,7 @@ function computePaneScale(models, bars, includeCandles, i0, i1, drawings, log =
34485
34634
  return { min: min - span * MARGIN_BOTTOM, max: max + span * MARGIN_TOP };
34486
34635
  }
34487
34636
  function considerSeries(s, i0, i1, off, consider) {
34637
+ if (!seriesInScale(s)) return;
34488
34638
  if (s.kind === "candle" || s.kind === "bar") {
34489
34639
  for (let i = i0; i <= i1; i += 1) {
34490
34640
  const b = s.bars[i - off];
@@ -34704,6 +34854,22 @@ var MarkPopover = class {
34704
34854
  this.openKey = null;
34705
34855
  /** Bumped per open/close — a lazy content resolving after its popup went away is dropped. */
34706
34856
  this.generation = 0;
34857
+ /**
34858
+ * The cluster whose popup was open when the current pointer press began. The anchor is
34859
+ * hit-transparent (the glyph is canvas pixels), so the shell's outside-dismiss closes the
34860
+ * popup on pointerdown; the click the renderer derives from the same pointerup must then
34861
+ * read as "close", not reopen that cluster. Captured on the document ahead of the shell's
34862
+ * (deferred) listener, cleared once the release has dispatched.
34863
+ */
34864
+ this.pressKey = null;
34865
+ this.onPress = () => {
34866
+ if (this.openKey === null) return;
34867
+ this.pressKey = this.openKey;
34868
+ const doc = this.deps.plot.ownerDocument;
34869
+ const clear = () => void setTimeout(() => this.pressKey = null, 0);
34870
+ doc.addEventListener("pointerup", clear, { capture: true, once: true });
34871
+ doc.addEventListener("pointercancel", clear, { capture: true, once: true });
34872
+ };
34707
34873
  const doc = deps.plot.ownerDocument;
34708
34874
  injectStyles(CALLOUT_STYLE_ID, CALLOUT_CSS, doc);
34709
34875
  injectStyles(MARKS_STYLE_ID, MARKS_CSS, doc);
@@ -34711,12 +34877,14 @@ var MarkPopover = class {
34711
34877
  this.anchor.className = "vela-marks-anchor";
34712
34878
  Object.assign(this.anchor.style, { position: "absolute", pointerEvents: "none", left: "0", top: "0", width: "0", height: "0" });
34713
34879
  deps.plot.appendChild(this.anchor);
34880
+ doc.addEventListener("pointerdown", this.onPress, true);
34714
34881
  }
34715
34882
  /** The cluster key the open popup belongs to, or null. */
34716
34883
  get key() {
34717
34884
  return this.openKey;
34718
34885
  }
34719
34886
  open(cluster, rect) {
34887
+ if (this.pressKey === cluster.key) return;
34720
34888
  this.close();
34721
34889
  this.place(rect);
34722
34890
  const gen = ++this.generation;
@@ -34764,6 +34932,7 @@ var MarkPopover = class {
34764
34932
  }
34765
34933
  destroy() {
34766
34934
  this.close();
34935
+ this.deps.plot.ownerDocument.removeEventListener("pointerdown", this.onPress, true);
34767
34936
  this.anchor.remove();
34768
34937
  }
34769
34938
  place(rect) {
@@ -35524,6 +35693,13 @@ var NativeRenderer = class {
35524
35693
  this.backdropRenderer = new BackdropRenderer();
35525
35694
  this.volumeRenderer = new VolumeRenderer();
35526
35695
  /** SDK renderer layers instantiated at mount ({@link registerRendererLayer}). */
35696
+ /**
35697
+ * The mounted SDK layers. One BASE entry per registered definition (channel = the layer
35698
+ * id, no owner of its own), plus one DEDICATED entry per mounted native indicator whose
35699
+ * model names its own channel (`native.channel` — an instance of a multi-instance type):
35700
+ * same definition, its own canvas, reading that instance's channel and following that
35701
+ * instance's pane and z key. `channel` is unique across the array.
35702
+ */
35527
35703
  this.extLayers = [];
35528
35704
  /** Last applied layer-canvas order (ids below + above the data canvas) — re-slotted only on change. */
35529
35705
  this.layerOrderSig = "";
@@ -36669,11 +36845,7 @@ var NativeRenderer = class {
36669
36845
  Object.assign(this.overlayRoot.style, { position: "absolute", inset: "0", pointerEvents: "none" });
36670
36846
  this.plot = document.createElement("div");
36671
36847
  Object.assign(this.plot.style, { position: "absolute", top: "0", right: "0", bottom: "0", left: "0" });
36672
- this.extLayers = rendererLayers().map((def) => {
36673
- const canvas = document.createElement("canvas");
36674
- Object.assign(canvas.style, { position: "absolute", inset: "0", width: "100%", height: "100%", pointerEvents: "none" });
36675
- return { def, instance: def.create(), canvas };
36676
- });
36848
+ this.extLayers = rendererLayers().map((def) => ({ def, instance: def.create(), canvas: this.createLayerCanvas(), channel: def.id, owner: null }));
36677
36849
  const below = this.extLayers.filter((l) => l.def.placement === "below-data").map((l) => l.canvas);
36678
36850
  const above = this.extLayers.filter((l) => l.def.placement !== "below-data").map((l) => l.canvas);
36679
36851
  this.plot.append(this.backdropCanvas, ...below, this.dataCanvas, this.volumeCanvas, this.vpvrCanvas, ...above, this.chromeCanvas, this.drawingsCanvas, this.cursorCanvas, this.overlayRoot);
@@ -36690,6 +36862,7 @@ var NativeRenderer = class {
36690
36862
  this.volumeRenderer.mount(this.volumeCanvas);
36691
36863
  this.vpvrRenderer.mount(this.vpvrCanvas);
36692
36864
  for (const l of this.extLayers) l.instance.mount(l.canvas);
36865
+ for (const m of this.scene.indicators.values()) this.ensureInstanceLayer(m);
36693
36866
  this.chrome.mount(this.chromeCanvas);
36694
36867
  this.crosshairLayer.mount(this.cursorCanvas);
36695
36868
  this.coords.setViewport(defaultViewport());
@@ -37282,6 +37455,7 @@ var NativeRenderer = class {
37282
37455
  this.refreshAnchorOffset(model);
37283
37456
  if (model.native && this.extLayers.some((l) => l.def.id === model.native.type)) this.scene.assignIndicatorZTop(model.id);
37284
37457
  else this.scene.assignIndicatorZ(model.id);
37458
+ this.ensureInstanceLayer(model);
37285
37459
  if (model.legend !== false) {
37286
37460
  this.inputsUI.upsert(model.id, model.shorttitle ?? model.title, model.inputs, model.inputValues, model.paneId, {
37287
37461
  native: !!model.native,
@@ -37325,6 +37499,7 @@ var NativeRenderer = class {
37325
37499
  this.scene.vpvrLayer = null;
37326
37500
  }
37327
37501
  this.scene.indicators.delete(handle.id);
37502
+ this.dropInstanceLayer(handle.id);
37328
37503
  this.scene.forgetIndicatorZ(handle.id);
37329
37504
  this.scene.forgetAnchorOffset(handle.id);
37330
37505
  this.scene.dropIndicatorScale(handle.id);
@@ -38118,29 +38293,31 @@ var NativeRenderer = class {
38118
38293
  up: bar.close >= bar.open
38119
38294
  };
38120
38295
  }
38121
- /** One group per indicator (name = indicator title), each with a row per drawable plot. */
38296
+ /** One group per indicator (name = indicator title), each with a row per plot shown in the data window. */
38122
38297
  dataWindowGroups(idx, pricePane) {
38123
38298
  const groups = [];
38124
38299
  for (const model of this.scene.indicators.values()) {
38125
- const rows = this.dataWindowRowsFor(model, idx, pricePane);
38300
+ const rows = this.readoutRowsFor(model, idx, pricePane, "dataWindow");
38126
38301
  if (rows.length) groups.push({ name: model.title, rows });
38127
38302
  }
38128
38303
  return groups;
38129
38304
  }
38130
- /** One indicator's readout at bar `idx`: a row per drawable plot, formatted on its pane's scale. */
38131
- dataWindowRowsFor(model, idx, pricePane) {
38305
+ /** One indicator's readout at bar `idx` for one value surface (the legend or the data window):
38306
+ * a row per plot shown on that surface, formatted on its pane's scale. A plot's `display`
38307
+ * decides per surface — a data-window-only plot has a row here and none in the legend. */
38308
+ readoutRowsFor(model, idx, pricePane, surface) {
38132
38309
  if (model.native?.type === "volume") return this.volumeReadoutRows(model, idx);
38133
38310
  const pane = (model.paneId ? this.scene.panes.get(model.paneId) : null) ?? pricePane;
38134
38311
  const off = this.scene.offsetOf(model.id);
38135
38312
  const rows = [];
38136
38313
  for (const s of model.series) {
38314
+ if (!seriesShownOn(s, surface)) continue;
38137
38315
  let value;
38138
38316
  let color;
38139
38317
  if (s.kind === "candle" || s.kind === "bar") {
38140
38318
  value = s.bars[idx - off]?.close;
38141
38319
  color = s.style?.up ?? this.theme.upColor;
38142
38320
  } else if (isLineLikeSeries(s)) {
38143
- if (s.visible === false) continue;
38144
38321
  value = s.points[idx - off]?.value;
38145
38322
  color = s.points[idx - off]?.color ?? s.style.color;
38146
38323
  } else {
@@ -38163,9 +38340,10 @@ var NativeRenderer = class {
38163
38340
  const color = bar.close >= bar.open ? cfg?.upColor ?? this.theme.upColor : cfg?.downColor ?? this.theme.downColor;
38164
38341
  return [{ label: model.title, value: formatVolume(vol), color }];
38165
38342
  }
38166
- /** Refresh the plot values beside every legend title — the same readout the data window
38167
- * shows (crosshair bar, else the latest bar), pushed per paint. A hidden indicator has
38168
- * no scene model, so its row is absent from the map and its readout clears. */
38343
+ /** Refresh the plot values beside every legend title — the same bar the data window reads
38344
+ * (crosshair bar, else the latest bar), pushed per paint, filtered to the plots whose
38345
+ * `display` includes the legend. A hidden indicator has no scene model, so its row is
38346
+ * absent from the map and its readout clears. */
38169
38347
  updateLegendValues() {
38170
38348
  if (!this.inputsUI) return;
38171
38349
  const n = this.bars.length;
@@ -38174,7 +38352,7 @@ var NativeRenderer = class {
38174
38352
  const idx = this.hoverLogical != null ? this.hoverLogical : n - 1;
38175
38353
  const pricePane = this.dataWindowPricePane();
38176
38354
  for (const model of this.scene.indicators.values()) {
38177
- values.set(model.id, this.dataWindowRowsFor(model, idx, pricePane).map((r) => ({ value: r.value, color: r.color })));
38355
+ values.set(model.id, this.readoutRowsFor(model, idx, pricePane, "legend").map((r) => ({ value: r.value, color: r.color })));
38178
38356
  }
38179
38357
  }
38180
38358
  this.inputsUI.setPlotValues(values);
@@ -38200,9 +38378,9 @@ var NativeRenderer = class {
38200
38378
  const nowMs = typeof performance !== "undefined" ? performance.now() : Date.now();
38201
38379
  for (const l of this.extLayers) {
38202
38380
  if (!l.def.repaintOnCursor) continue;
38203
- const lp = this.layerPane(l.def.id) ?? pane;
38381
+ const lp = this.layerPane(l) ?? pane;
38204
38382
  if (lp.collapsed) continue;
38205
- l.instance.render(this.extLayerArgs(l.def.id, lp.scale, lp.bounds, nowMs));
38383
+ l.instance.render(this.extLayerArgs(l, lp.scale, lp.bounds, nowMs));
38206
38384
  if (this.animZoom.on && l.instance.animating?.()) this.animator.start();
38207
38385
  }
38208
38386
  }
@@ -38211,13 +38389,15 @@ var NativeRenderer = class {
38211
38389
  if (canvas.width === 0 || canvas.height === 0) return;
38212
38390
  canvas.getContext("2d")?.clearRect(0, 0, canvas.width, canvas.height);
38213
38391
  }
38214
- /** One frame's args for an SDK renderer layer (shared by the data + cursor paint paths). */
38215
- extLayerArgs(id, scale, bounds, nowMs) {
38392
+ /** One frame's args for an SDK renderer layer (shared by the data + cursor paint paths).
38393
+ * Data + pending come from the entry's channel (per instance for a dedicated entry);
38394
+ * the settings bag is the TYPE's — a chart type's dialog values, one bag per type. */
38395
+ extLayerArgs(l, scale, bounds, nowMs) {
38216
38396
  return {
38217
38397
  bars: this.scene.bars,
38218
- data: this.scene.nativeData.get(id),
38219
- settings: this.scene.nativeData.get(`${id}-settings`) ?? {},
38220
- pending: this.scene.nativePending.get(id) ?? [],
38398
+ data: this.scene.nativeData.get(l.channel),
38399
+ settings: this.scene.nativeData.get(`${l.def.id}-settings`) ?? {},
38400
+ pending: this.scene.nativePending.get(l.channel) ?? [],
38221
38401
  coords: this.coords,
38222
38402
  scale,
38223
38403
  bounds,
@@ -38260,12 +38440,12 @@ var NativeRenderer = class {
38260
38440
  const nowMs = typeof performance !== "undefined" ? performance.now() : Date.now();
38261
38441
  let folded = null;
38262
38442
  for (const l of this.extLayers) {
38263
- const lp = this.layerPane(l.def.id) ?? pane;
38443
+ const lp = this.layerPane(l) ?? pane;
38264
38444
  if (lp.collapsed) {
38265
38445
  this.clearLayerCanvas(l.canvas);
38266
38446
  continue;
38267
38447
  }
38268
- const args = this.extLayerArgs(l.def.id, lp.scale, lp.bounds, nowMs);
38448
+ const args = this.extLayerArgs(l, lp.scale, lp.bounds, nowMs);
38269
38449
  l.instance.render(args);
38270
38450
  if (lp === pane) folded = foldBaseModulation(folded, l.instance.modulateBase?.(args) ?? null);
38271
38451
  if (this.animZoom.on && l.instance.animating?.()) this.animator.start();
@@ -38429,7 +38609,7 @@ var NativeRenderer = class {
38429
38609
  const or = overlaySeriesRange(this.scene.indicators.values(), i0, i1, (id) => this.scene.offsetOf(id));
38430
38610
  if (or) dr = dr ? { min: Math.min(dr.min, or.min), max: Math.max(dr.max, or.max) } : or;
38431
38611
  }
38432
- const includeCandles = pane.kind === "price" && !this.scene.candlesHidden;
38612
+ const includeCandles = pane.kind === "price" && (!this.scene.candlesHidden || this.priceLayersAnchoredToBars(masterModels));
38433
38613
  pane.scaleTarget = computePaneScale(masterModels, this.bars, includeCandles, i0, i1, dr, paneLogScale(this.scene, pane), (id) => this.scene.offsetOf(id));
38434
38614
  pane.percentBaseline = pane.kind === "price" ? this.bars[i0]?.close ?? 0 : this.firstVisibleValue(masterModels, i0);
38435
38615
  pane.axisFormat = void 0;
@@ -38548,19 +38728,60 @@ var NativeRenderer = class {
38548
38728
  }
38549
38729
  return null;
38550
38730
  }
38551
- /** The mounted native indicator that OWNS an SDK layer — the one whose type equals the
38552
- * layer id (the id doubles as the data channel, so the pairing is the SDK's own
38553
- * contract). Null for chart-type channels and while the owner is hidden (a hidden
38554
- * indicator leaves the scene; its cleared data channel paints nothing anyway). */
38555
- layerOwner(layerId) {
38731
+ /** One transparent, pointer-transparent SDK layer canvas covering the plot. */
38732
+ createLayerCanvas() {
38733
+ const canvas = document.createElement("canvas");
38734
+ Object.assign(canvas.style, { position: "absolute", inset: "0", width: "100%", height: "100%", pointerEvents: "none" });
38735
+ return canvas;
38736
+ }
38737
+ /**
38738
+ * Mount the DEDICATED layer of a native indicator that names its own channel (an
38739
+ * instance of a multi-instance type painting through a registered layer): a fresh
38740
+ * instance of the layer's definition on its own canvas, reading that channel. Idempotent
38741
+ * — a show after hide re-mounts the model, and the layer (kept across the hide, like the
38742
+ * z key) is simply reused. Skipped while the renderer itself is unmounted (`mount()`
38743
+ * catches up from the scene).
38744
+ */
38745
+ ensureInstanceLayer(model) {
38746
+ const channel = model.native?.channel;
38747
+ if (!channel || !this.plot) return;
38748
+ if (this.extLayers.some((l) => l.owner === model.id)) return;
38749
+ const base = this.extLayers.find((l) => l.owner === null && l.def.id === model.native.type);
38750
+ if (!base) return;
38751
+ const canvas = this.createLayerCanvas();
38752
+ canvas.width = this.dataCanvas.width;
38753
+ canvas.height = this.dataCanvas.height;
38754
+ canvas.style.width = this.dataCanvas.style.width || "100%";
38755
+ canvas.style.height = this.dataCanvas.style.height || "100%";
38756
+ const entry = { def: base.def, instance: base.def.create(), canvas, channel, owner: model.id };
38757
+ this.extLayers.push(entry);
38758
+ this.plot.insertBefore(canvas, this.chromeCanvas);
38759
+ entry.instance.mount(canvas);
38760
+ }
38761
+ /** Tear down the dedicated layer of a removed indicator (a hide keeps it). */
38762
+ dropInstanceLayer(indicatorId) {
38763
+ const i = this.extLayers.findIndex((l2) => l2.owner === indicatorId);
38764
+ if (i < 0) return;
38765
+ const [l] = this.extLayers.splice(i, 1);
38766
+ l.instance.destroy?.();
38767
+ l.canvas.remove();
38768
+ }
38769
+ /** The mounted native indicator that OWNS an SDK layer entry. A dedicated entry is
38770
+ * owned by the instance it was mounted for; the base entry by the mounted indicator
38771
+ * whose type equals the layer id and that has no channel of its own (the id doubles
38772
+ * as the data channel, so the pairing is the SDK's own contract). Null for chart-type
38773
+ * channels and while the owner is hidden (a hidden indicator leaves the scene; its
38774
+ * cleared data channel paints nothing anyway). */
38775
+ layerOwner(l) {
38776
+ if (l.owner !== null) return this.scene.indicators.get(l.owner) ?? null;
38556
38777
  for (const m of this.scene.indicators.values()) {
38557
- if (m.native?.type === layerId) return m;
38778
+ if (m.native?.type === l.def.id && !m.native.channel) return m;
38558
38779
  }
38559
38780
  return null;
38560
38781
  }
38561
38782
  /** The pane an SDK layer paints on: its owner's pane, else the price pane. */
38562
- layerPane(layerId) {
38563
- const owner = this.layerOwner(layerId);
38783
+ layerPane(l) {
38784
+ const owner = this.layerOwner(l);
38564
38785
  const paneId = owner ? owner.paneId ?? PRICE_PANE_ID2 : PRICE_PANE_ID2;
38565
38786
  return this.scene.panes.get(paneId) ?? null;
38566
38787
  }
@@ -38568,12 +38789,12 @@ var NativeRenderer = class {
38568
38789
  * owned layers by their owner's z key against the candles' (an indicator restacked
38569
38790
  * below the candles takes its layer canvas along), unowned by declared placement. */
38570
38791
  orderedLayerCanvases() {
38571
- const byId = new Map(this.extLayers.map((l) => [l.def.id, l.canvas]));
38792
+ const byId = new Map(this.extLayers.map((l) => [l.channel, l.canvas]));
38572
38793
  const { below, above } = stackLayers(
38573
38794
  this.extLayers.map((l) => {
38574
- const owner = this.layerOwner(l.def.id);
38795
+ const owner = this.layerOwner(l);
38575
38796
  return {
38576
- id: l.def.id,
38797
+ id: l.channel,
38577
38798
  placement: l.def.placement === "below-data" ? "below-data" : "above-data",
38578
38799
  ownerZ: owner ? this.scene.zOf(owner.id) : null
38579
38800
  };
@@ -38595,7 +38816,7 @@ var NativeRenderer = class {
38595
38816
  syncLayerCanvasOrder() {
38596
38817
  if (this.extLayers.length === 0 || !this.plot) return;
38597
38818
  const { below, above } = this.orderedLayerCanvases();
38598
- const sig = [...below.map((c) => this.extLayers.find((l) => l.canvas === c).def.id), "|", ...above.map((c) => this.extLayers.find((l) => l.canvas === c).def.id)].join(",");
38819
+ const sig = [...below.map((c) => this.extLayers.find((l) => l.canvas === c).channel), "|", ...above.map((c) => this.extLayers.find((l) => l.canvas === c).channel)].join(",");
38599
38820
  if (sig === this.layerOrderSig) return;
38600
38821
  this.layerOrderSig = sig;
38601
38822
  for (const c of below) this.plot.insertBefore(c, this.dataCanvas);
@@ -38619,6 +38840,17 @@ var NativeRenderer = class {
38619
38840
  (m) => m.series.length === 0 && !!m.native && this.extLayers.some((l) => l.def.id === m.native.type)
38620
38841
  );
38621
38842
  }
38843
+ /**
38844
+ * True when SDK-layer content on the price pane paints at BAR PRICES without
38845
+ * contributing a series: the active chart type's own layer, or an overlay layer native
38846
+ * (a series-less model whose type names a mounted layer). Hiding the candles must then
38847
+ * keep the bars in the price scale — with nothing else on the pane the scale would fall
38848
+ * to the {0,1} placeholder and every such layer would paint off-screen and vanish.
38849
+ */
38850
+ priceLayersAnchoredToBars(masterModels) {
38851
+ if (this.extLayers.some((l) => l.owner === null && l.def.id === this.scene.priceStyle)) return true;
38852
+ return masterModels.some((m) => m.series.length === 0 && !!m.native && this.extLayers.some((l) => l.def.id === m.native.type));
38853
+ }
38622
38854
  /** Per-pane scale state for a host UI (e.g. a price-axis context menu): the pane's pixel
38623
38855
  * band (`top`/`height`, so a click y maps to a pane) plus its current axis `mode`/`log`.
38624
38856
  * Top-to-bottom order. Every pane is independent — the price pane from the scene setting,
@@ -43759,7 +43991,8 @@ var ChartCell = class {
43759
43991
  this.statusline?.setFitMode(on);
43760
43992
  }
43761
43993
  /** The workspace shell keeps the app theme; the cell host's tokens re-derive from
43762
- * the LIVE plot surface (see {@link applyPlotOverlayTokens}). */
43994
+ * the LIVE plot surface for overlay ink (statusline, watermark). Menus portal to
43995
+ * `.vela-ui` and keep the app-theme surface (see {@link applyPlotOverlayTokens}). */
43763
43996
  syncPlotOverlayTokens() {
43764
43997
  applyPlotOverlayTokens(this.host, this.appTheme, this.inner?.renderer.getConfig() ?? null);
43765
43998
  }
@@ -44643,7 +44876,21 @@ var VelaWorkspace = class {
44643
44876
  /** Live sync configuration (mutable copy of the option). */
44644
44877
  this.syncOpts = {};
44645
44878
  this.stateTimer = null;
44646
- this.onUnload = () => this.persistNow();
44879
+ /** Re-entrance guard for {@link flushPendingState}: a `state:changed` subscriber that
44880
+ * calls back into the workspace (`destroy()` on save is the obvious one) must not
44881
+ * re-enter the flush and recurse. */
44882
+ this.emitting = false;
44883
+ /** False until the constructor finishes. Boot is not an edit: seeding the sync
44884
+ * settings marks state dirty, so without this a workspace mounted and torn down
44885
+ * inside the debounce window would flush and tell the host the user had changed
44886
+ * something — a save of the default document over whatever was stored. */
44887
+ this.booted = false;
44888
+ /** A page unload gets the same flush as a teardown — the pending edit reaches the
44889
+ * host, the timer is cleared, and a `destroy()` that follows adds no second write. */
44890
+ this.onUnload = () => {
44891
+ this.flushPendingState();
44892
+ this.persistNow();
44893
+ };
44647
44894
  /** Re-entrance guard around one propagation tick: followers' synchronous echoes
44648
44895
  * (their setVisibleRange re-emits viewport:changed) must not re-propagate. */
44649
44896
  this.syncBusy = false;
@@ -44957,6 +45204,7 @@ var VelaWorkspace = class {
44957
45204
  }
44958
45205
  this.mountAttachments();
44959
45206
  this.restoreGlobalExt();
45207
+ this.booted = true;
44960
45208
  }
44961
45209
  // ── access ──────────────────────────────────────────────────
44962
45210
  /** The cell with identity `id` (its declared name, or `c<N>` when undeclared), or
@@ -45365,13 +45613,17 @@ var VelaWorkspace = class {
45365
45613
  }
45366
45614
  destroy() {
45367
45615
  if (this.destroyed) return;
45368
- this.persistNow();
45616
+ this.flushPendingState();
45369
45617
  this.destroyed = true;
45370
- if (this.stateTimer != null) clearTimeout(this.stateTimer);
45371
45618
  if (this.persistKey !== null && typeof window !== "undefined") window.removeEventListener("beforeunload", this.onUnload);
45372
45619
  this.resizeObserver?.disconnect();
45373
45620
  this.splitters.destroy();
45374
45621
  for (const [id, cell] of [...this.cellsById]) {
45622
+ try {
45623
+ this.poolSet(id, cell.dehydrate());
45624
+ } catch (err) {
45625
+ console.warn(`[vela] could not snapshot cell "${id}" while destroying:`, err);
45626
+ }
45375
45627
  cell.destroy();
45376
45628
  this.cellsById.delete(id);
45377
45629
  }
@@ -45457,7 +45709,7 @@ var VelaWorkspace = class {
45457
45709
  /** Debounced dirty mark: one `state:changed` (+ one storage write in persist mode)
45458
45710
  * per burst of edits, flushed hard on unload/destroy. */
45459
45711
  markStateDirty() {
45460
- if (this.destroyed) return;
45712
+ if (this.destroyed || !this.booted) return;
45461
45713
  if (this.stateTimer != null) clearTimeout(this.stateTimer);
45462
45714
  this.stateTimer = setTimeout(() => {
45463
45715
  this.stateTimer = null;
@@ -45465,6 +45717,44 @@ var VelaWorkspace = class {
45465
45717
  this.persistNow();
45466
45718
  }, 500);
45467
45719
  }
45720
+ /**
45721
+ * Push a pending debounced change out NOW, for a teardown or a page unload — the
45722
+ * two moments the 500ms timer would otherwise never reach.
45723
+ *
45724
+ * Dropping it was a silent data loss: the timer carries BOTH halves of the dirty
45725
+ * signal — the `state:changed` event and the storage write — so a host that saves
45726
+ * from that event (a server-backed host, where `persistNow` is a no-op for want of
45727
+ * a `persistKey`) simply never heard about the user's last edit.
45728
+ *
45729
+ * Idempotent and re-entrancy safe, which the ordering here buys:
45730
+ * - a no-op unless a timer is actually pending, so a teardown with nothing
45731
+ * outstanding stays silent and never invents an edit;
45732
+ * - the timer is cleared FIRST, so nothing survives this call and a second call
45733
+ * does nothing — `destroy()` after an unload flush adds no second write;
45734
+ * - the state is snapshotted BEFORE the handlers run, and that snapshot is what
45735
+ * gets persisted, so storage and the host are told the same thing even if a
45736
+ * handler mutates state while it runs;
45737
+ * - `emitting` is held across the emit, so a handler calling back in (a host that
45738
+ * tears down on save is the obvious one) cannot re-enter this and recurse.
45739
+ */
45740
+ flushPendingState() {
45741
+ if (this.stateTimer == null || this.emitting) return;
45742
+ clearTimeout(this.stateTimer);
45743
+ this.stateTimer = null;
45744
+ const snapshot = this.persistKey !== null ? encodeState(this.getState()) : null;
45745
+ this.emitting = true;
45746
+ try {
45747
+ this.events.emit("state:changed", void 0);
45748
+ } finally {
45749
+ this.emitting = false;
45750
+ }
45751
+ if (snapshot !== null && this.persistKey !== null) {
45752
+ try {
45753
+ void this.storage.set(this.persistKey, snapshot);
45754
+ } catch {
45755
+ }
45756
+ }
45757
+ }
45468
45758
  /** Write the current state through the storage adapter now (fire-and-forget). */
45469
45759
  persistNow() {
45470
45760
  if (this.persistKey === null || this.destroyed) return;