@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
@@ -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";
@@ -8154,16 +8158,18 @@ var Pitchfork = class extends SegmentDrawing {
8154
8158
  geometry(proj) {
8155
8159
  const a = this.anchors[0];
8156
8160
  const b = this.anchors[1];
8157
- const c = this.anchors[2];
8158
- if (!a || !b || !c) return null;
8161
+ if (!a || !b) return null;
8159
8162
  const px = (p) => {
8160
8163
  const y = proj.yOf(p.price, this.paneId);
8161
8164
  return y == null ? null : [proj.xOf(p.time), y];
8162
8165
  };
8163
8166
  const P0 = px(a);
8164
8167
  const P1 = px(b);
8168
+ if (!P0 || !P1) return null;
8169
+ const c = this.anchors[2];
8170
+ if (!c) return { segments: [[P0[0], P0[1], P1[0], P1[1]]], fill: null };
8165
8171
  const P2 = px(c);
8166
- if (!P0 || !P1 || !P2) return null;
8172
+ if (!P2) return null;
8167
8173
  const mx = (P1[0] + P2[0]) / 2;
8168
8174
  const my = (P1[1] + P2[1]) / 2;
8169
8175
  const dx = mx - P0[0];
@@ -8210,16 +8216,19 @@ var PitchforkVariant = class extends SegmentDrawing {
8210
8216
  geometry(proj) {
8211
8217
  const a = this.anchors[0];
8212
8218
  const b = this.anchors[1];
8213
- const c = this.anchors[2];
8214
- if (!a || !b || !c) return null;
8219
+ if (!a || !b) return null;
8215
8220
  const px = (p) => {
8216
8221
  const y = proj.yOf(p.price, this.paneId);
8217
8222
  return y == null ? null : [proj.xOf(p.time), y];
8218
8223
  };
8219
- const S2 = px(this.medianStart(a, b, c));
8224
+ const P0 = px(a);
8220
8225
  const P1 = px(b);
8226
+ if (!P0 || !P1) return null;
8227
+ const c = this.anchors[2];
8228
+ if (!c) return { segments: [[P0[0], P0[1], P1[0], P1[1]]], fill: null };
8229
+ const S2 = px(this.medianStart(a, b, c));
8221
8230
  const P2 = px(c);
8222
- if (!S2 || !P1 || !P2) return null;
8231
+ if (!S2 || !P2) return null;
8223
8232
  const mx = (P1[0] + P2[0]) / 2;
8224
8233
  const my = (P1[1] + P2[1]) / 2;
8225
8234
  const dx = mx - S2[0];
@@ -8234,8 +8243,12 @@ var PitchforkVariant = class extends SegmentDrawing {
8234
8243
  // upper tine
8235
8244
  extendRay(P2[0], P2[1], P2[0] + dx, P2[1] + dy, "right", w, h),
8236
8245
  // lower tine
8237
- [P1[0], P1[1], P2[0], P2[1]]
8246
+ [P1[0], P1[1], P2[0], P2[1]],
8238
8247
  // base line
8248
+ // The median leaves from the shifted origin, not the pivot — without this construction
8249
+ // line the pivot handle would float unattached to the shape it defines.
8250
+ [P0[0], P0[1], P1[0], P1[1]]
8251
+ // pivot → first tine anchor
8239
8252
  ],
8240
8253
  fill: null
8241
8254
  };
@@ -8292,17 +8305,19 @@ var InsidePitchfork = class extends SegmentDrawing {
8292
8305
  geometry(proj) {
8293
8306
  const a = this.anchors[0];
8294
8307
  const b = this.anchors[1];
8295
- const c = this.anchors[2];
8296
- if (!a || !b || !c) return null;
8308
+ if (!a || !b) return null;
8297
8309
  const px = (p) => {
8298
8310
  const y = proj.yOf(p.price, this.paneId);
8299
8311
  return y == null ? null : [proj.xOf(p.time), y];
8300
8312
  };
8301
- const B = px({ time: (a.time + b.time) / 2, price: (a.price + b.price) / 2 });
8302
8313
  const P0 = px(a);
8303
8314
  const P1 = px(b);
8315
+ if (!P0 || !P1) return null;
8316
+ const c = this.anchors[2];
8317
+ if (!c) return { segments: [[P0[0], P0[1], P1[0], P1[1]]], fill: null };
8318
+ const B = px({ time: (a.time + b.time) / 2, price: (a.price + b.price) / 2 });
8304
8319
  const P2 = px(c);
8305
- if (!B || !P0 || !P1 || !P2) return null;
8320
+ if (!B || !P2) return null;
8306
8321
  const mx = (P1[0] + P2[0]) / 2;
8307
8322
  const my = (P1[1] + P2[1]) / 2;
8308
8323
  const dx = P2[0] - B[0];
@@ -8473,16 +8488,18 @@ var Triangle = class extends SegmentDrawing {
8473
8488
  geometry(proj) {
8474
8489
  const a = this.anchors[0];
8475
8490
  const b = this.anchors[1];
8476
- const c = this.anchors[2];
8477
- if (!a || !b || !c) return null;
8491
+ if (!a || !b) return null;
8478
8492
  const px = (p) => {
8479
8493
  const y = proj.yOf(p.price, this.paneId);
8480
8494
  return y == null ? null : [proj.xOf(p.time), y];
8481
8495
  };
8482
8496
  const A = px(a);
8483
8497
  const B = px(b);
8498
+ if (!A || !B) return null;
8499
+ const c = this.anchors[2];
8500
+ if (!c) return { segments: [[A[0], A[1], B[0], B[1]]], fill: null };
8484
8501
  const C = px(c);
8485
- if (!A || !B || !C) return null;
8502
+ if (!C) return null;
8486
8503
  return {
8487
8504
  segments: [
8488
8505
  [A[0], A[1], B[0], B[1]],
@@ -18344,6 +18361,13 @@ function resolveAnimations(animations) {
18344
18361
  function isLineLikeSeries(spec) {
18345
18362
  return spec.kind !== "candle" && spec.kind !== "bar" && spec.kind !== "markers";
18346
18363
  }
18364
+ function seriesShownOn(spec, surface) {
18365
+ if (spec.display) return spec.display[surface] !== false;
18366
+ return spec.visible !== false;
18367
+ }
18368
+ function seriesInScale(spec) {
18369
+ return seriesShownOn(spec, "pane") || seriesShownOn(spec, "priceScale");
18370
+ }
18347
18371
 
18348
18372
  // src/core/engine/IndicatorRegistry.ts
18349
18373
  var IndicatorRegistry = class {
@@ -18373,6 +18397,9 @@ var IndicatorRegistry = class {
18373
18397
  };
18374
18398
 
18375
18399
  // src/core/native-indicators/NativeIndicator.ts
18400
+ function nativeInstanceChannel(type, instanceId) {
18401
+ return `${type}#${instanceId}`;
18402
+ }
18376
18403
  var REGISTRY2 = /* @__PURE__ */ new Map();
18377
18404
  function registerNativeIndicator(descriptor) {
18378
18405
  REGISTRY2.set(descriptor.type, descriptor);
@@ -18442,6 +18469,9 @@ var IndicatorHandleImpl = class {
18442
18469
  setProps(values) {
18443
18470
  this.controller.applyProps(this.id, values);
18444
18471
  }
18472
+ updateCode(source) {
18473
+ this.controller.updateCode(this.id, source);
18474
+ }
18445
18475
  setVisible(visible) {
18446
18476
  this.controller.setVisible(this.id, visible);
18447
18477
  }
@@ -18464,6 +18494,10 @@ var IndicatorHandleImpl = class {
18464
18494
  setPropsSchema(schema) {
18465
18495
  this.propsSchema = schema;
18466
18496
  }
18497
+ /** Sync the public `source` once a code update has been prepared and taken over. */
18498
+ setSource(source) {
18499
+ this.source = source;
18500
+ }
18467
18501
  /** Sync the public `visible` getter when the orchestrator changes visibility (API or legend eye). */
18468
18502
  setVisibleState(visible) {
18469
18503
  this.visibleState = visible;
@@ -19284,6 +19318,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
19284
19318
  if (identityChanged) {
19285
19319
  this.beginLoad(false);
19286
19320
  this.setBarSeries([], { clearing: true });
19321
+ this.blankIndicatorVisuals();
19287
19322
  if (this.activeEngineStyle) {
19288
19323
  this.typeEngines.get(this.activeEngineStyle)?.suspend();
19289
19324
  this.renderer.setNativeData?.(this.activeEngineStyle, void 0);
@@ -19326,7 +19361,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
19326
19361
  restartNativeIndicators() {
19327
19362
  for (const record of this.registry.all()) {
19328
19363
  if (!record.native) continue;
19329
- record.native.instance.stop();
19364
+ if (record.native.started) record.native.instance.stop();
19330
19365
  record.native.instance = record.native.descriptor.create();
19331
19366
  record.native.started = false;
19332
19367
  record.pendingStructural = true;
@@ -19534,6 +19569,23 @@ var EngineOrchestrator = class _EngineOrchestrator {
19534
19569
  this.renderer.setBars(this.bars, { preserveView: true });
19535
19570
  this.reexecuteIndicators();
19536
19571
  }
19572
+ /**
19573
+ * Blank every mounted indicator's painted output — series data, fills, backgrounds,
19574
+ * price lines, drawings, bar colors, trades — while keeping the mount (legend row,
19575
+ * pane, settings) intact, via the same idempotent-by-id remount an input edit uses.
19576
+ * Called on a market identity switch: the fresh models arrive only after the new
19577
+ * bars load, so nothing blanked here is ever restored — each consumer's re-run
19578
+ * remounts a full model over it.
19579
+ */
19580
+ blankIndicatorVisuals() {
19581
+ for (const record of this.registry.all()) {
19582
+ if (record.hidden || !record.renderHandle || !record.model) continue;
19583
+ record.model = blankedModel(record.model);
19584
+ record.renderHandle = this.renderer.mountIndicator(record.model);
19585
+ record.pendingStructural = true;
19586
+ this.setLoading(record, true);
19587
+ }
19588
+ }
19537
19589
  /** Stop + re-run every visible Pine indicator (its input bars changed wholesale). */
19538
19590
  reexecuteIndicators() {
19539
19591
  for (const record of this.registry.all()) {
@@ -19778,6 +19830,61 @@ var EngineOrchestrator = class _EngineOrchestrator {
19778
19830
  record.session.update(record.inputValues, record.propValues);
19779
19831
  this.events.emit("indicator:inputs", { id });
19780
19832
  }
19833
+ /**
19834
+ * IndicatorController: replace a SCRIPT indicator's source in place (see
19835
+ * {@link IndicatorHandle.updateCode}). Prepare-first: the running session keeps
19836
+ * computing until the new source has compiled, so a broken edit costs the user
19837
+ * nothing but an `error` event. Natives have no script — warn and leave them alone.
19838
+ */
19839
+ updateCode(id, source) {
19840
+ const record = this.registry.get(id);
19841
+ const handle = this.handles.get(id);
19842
+ if (!record || !handle) return;
19843
+ if (record.native) {
19844
+ console.warn(`[vela] updateCode("${id}") \u2014 a native indicator has no script to update.`);
19845
+ return;
19846
+ }
19847
+ if (source === record.source && record.pendingSource === void 0) return;
19848
+ record.pendingSource = source;
19849
+ void this.swapSource(id, source, handle);
19850
+ }
19851
+ async swapSource(id, source, handle) {
19852
+ const engine = this.registry.get(id)?.engine ?? this.engineFor(this.registry.get(id)?.options?.language);
19853
+ let prepared;
19854
+ try {
19855
+ prepared = await engine.prepare(source, id);
19856
+ } catch (err) {
19857
+ const record2 = this.registry.get(id);
19858
+ if (record2?.pendingSource === source) {
19859
+ record2.pendingSource = void 0;
19860
+ this.fail(id, handle, err);
19861
+ }
19862
+ return;
19863
+ }
19864
+ const record = this.registry.get(id);
19865
+ if (!record || record.pendingSource !== source) return;
19866
+ record.pendingSource = void 0;
19867
+ record.source = source;
19868
+ record.engine = engine;
19869
+ record.prepared = prepared;
19870
+ handle.setSource(source);
19871
+ record.inputValues = valuesOnSchema(prepared.inputs, record.inputValues);
19872
+ record.propValues = valuesOnSchema(prepared.props ?? [], record.propValues);
19873
+ handle.setSchema(prepared.inputs);
19874
+ handle.setPropsSchema(prepared.props ?? []);
19875
+ record.session?.stop();
19876
+ record.session = void 0;
19877
+ record.pendingStructural = true;
19878
+ record.pendingCause = "code";
19879
+ if (record.hidden) return;
19880
+ if (record.renderHandle) {
19881
+ this.renderer.setIndicatorInputs(record.renderHandle, record.inputValues, record.propValues);
19882
+ this.setLoading(record, true);
19883
+ } else {
19884
+ this.mountLoadingPlaceholder(id, record);
19885
+ }
19886
+ this.executeIndicator(id, handle);
19887
+ }
19781
19888
  /** IndicatorController: tear down an indicator and (if now empty) its pane. */
19782
19889
  /** Live handles of every indicator on the chart (script + native), insertion order. */
19783
19890
  listIndicators() {
@@ -19792,7 +19899,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
19792
19899
  const record = this.registry.remove(id);
19793
19900
  if (record?.native?.type === "volume") this.volumeOptedOut = true;
19794
19901
  record?.session?.stop();
19795
- record?.native?.instance.stop();
19902
+ if (record?.native?.started) record.native.instance.stop();
19796
19903
  this.handles.delete(id);
19797
19904
  if (record?.renderHandle) this.renderer.removeIndicator(record.renderHandle);
19798
19905
  const paneId = record?.model?.paneId;
@@ -19818,7 +19925,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
19818
19925
  if (!visible) {
19819
19926
  record.session?.stop();
19820
19927
  record.session = void 0;
19821
- record.native?.instance.suspend();
19928
+ if (record.native?.started) record.native.instance.suspend();
19822
19929
  if (record.renderHandle) this.renderer.setIndicatorVisible?.(record.renderHandle, false);
19823
19930
  else if (record.native) this.mountHiddenNativeRow(id, record);
19824
19931
  } else {
@@ -19899,7 +20006,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
19899
20006
  this.paneActionUnsub?.();
19900
20007
  for (const record of this.registry.all()) {
19901
20008
  record.session?.stop();
19902
- record.native?.instance.stop();
20009
+ if (record.native?.started) record.native.instance.stop();
19903
20010
  }
19904
20011
  for (const engine of this.typeEngines.values()) engine.stop();
19905
20012
  this.typeEngines.clear();
@@ -19923,6 +20030,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
19923
20030
  const engine = this.engineFor(options.language);
19924
20031
  record.engine = engine;
19925
20032
  const prepared = await engine.prepare(source, id);
20033
+ if (record.source !== source) return;
19926
20034
  record.prepared = prepared;
19927
20035
  const defaults2 = {};
19928
20036
  for (const input of prepared.inputs) defaults2[input.key] = input.defval;
@@ -19993,7 +20101,9 @@ var EngineOrchestrator = class _EngineOrchestrator {
19993
20101
  await this.readyPromise;
19994
20102
  const record = this.registry.get(id);
19995
20103
  if (!record?.native || record.hidden) return;
20104
+ const channel = this.nativeChannel(record);
19996
20105
  const ctx = {
20106
+ id,
19997
20107
  symbol: this.config.market.symbol ?? "TEST",
19998
20108
  timeframe: this.config.market.timeframe ?? "60",
19999
20109
  live: this.config.live,
@@ -20001,7 +20111,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
20001
20111
  bars: () => this.bars,
20002
20112
  data: this.dataControl,
20003
20113
  emit: (out) => this.applyModel(id, this.buildNativeModel(record, out)),
20004
- pushData: (data) => this.renderer.setNativeData?.(record.native.type, data),
20114
+ pushData: (data) => this.renderer.setNativeData?.(channel, data),
20005
20115
  setStatus: (status) => {
20006
20116
  if (record.renderHandle && !record.hidden) this.renderer.setIndicatorStatus?.(record.renderHandle, status);
20007
20117
  }
@@ -20012,16 +20122,26 @@ var EngineOrchestrator = class _EngineOrchestrator {
20012
20122
  this.fail(id, handle, err);
20013
20123
  }
20014
20124
  }
20125
+ /** The renderer-layer channel a native instance's `pushData` lands on: the type itself
20126
+ * for a single-instance type (the layer id doubles as the channel), a per-instance
20127
+ * channel for a `multiInstance` type — otherwise every instance would overwrite the
20128
+ * one layer. Stamped on the model so the renderer mounts a dedicated layer for it. */
20129
+ nativeChannel(record) {
20130
+ const { type, descriptor } = record.native;
20131
+ return descriptor.multiInstance ? nativeInstanceChannel(type, record.id) : type;
20132
+ }
20015
20133
  /** Wrap a native indicator's emitted visuals into a full IndicatorModel, tagged `native`. */
20016
20134
  buildNativeModel(record, out) {
20017
20135
  const d = record.native.descriptor;
20136
+ const type = record.native.type;
20137
+ const channel = this.nativeChannel(record);
20018
20138
  return {
20019
20139
  id: record.id,
20020
20140
  title: record.title,
20021
20141
  ...d.shortTitle ? { shorttitle: d.shortTitle } : {},
20022
20142
  overlay: d.overlay,
20023
20143
  paneHint: d.paneHint,
20024
- native: { type: record.native.type },
20144
+ native: { type, ...channel !== type ? { channel } : {} },
20025
20145
  ...d.legend === false ? { legend: false } : {},
20026
20146
  ...out.paneAxis != null ? { paneAxis: out.paneAxis } : {},
20027
20147
  series: out.series ?? [],
@@ -20311,6 +20431,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
20311
20431
  applyModel(id, model) {
20312
20432
  const record = this.registry.get(id);
20313
20433
  if (!record || record.hidden) return true;
20434
+ if (this.switchingMarket) return false;
20314
20435
  if (record.loading && this.bars.length === 0 && !_EngineOrchestrator.modelHasOutput(model)) return false;
20315
20436
  const handle = this.handles.get(id);
20316
20437
  if (!record.renderHandle) {
@@ -20328,7 +20449,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
20328
20449
  if (record.loading && !record.paneLocked) {
20329
20450
  const routed = this.routePane(id, model, record.options ?? {});
20330
20451
  if (routed !== paneId) {
20331
- if (paneId !== "price") {
20452
+ if (paneId !== "price" && !this.registry.all().some((r) => r.id !== id && r.model?.paneId === paneId)) {
20332
20453
  this.renderer.removePane(paneId);
20333
20454
  this.forgetPane(paneId);
20334
20455
  }
@@ -20457,6 +20578,34 @@ function yieldToPaint() {
20457
20578
  }
20458
20579
  return new Promise((resolve) => setTimeout(resolve, 0));
20459
20580
  }
20581
+ function valuesOnSchema(schema, previous) {
20582
+ const out = {};
20583
+ const declared = /* @__PURE__ */ new Set();
20584
+ for (const s of schema) {
20585
+ out[s.key] = s.defval;
20586
+ declared.add(s.key);
20587
+ declared.add(s.title);
20588
+ }
20589
+ for (const [k, v] of Object.entries(previous)) if (declared.has(k)) out[k] = v;
20590
+ return out;
20591
+ }
20592
+ function blankedModel(model) {
20593
+ return {
20594
+ ...model,
20595
+ series: model.series.map((s) => s.kind === "candle" || s.kind === "bar" ? { ...s, bars: [] } : s.kind === "markers" ? { ...s, markers: [] } : { ...s, points: [] }),
20596
+ fills: [],
20597
+ backgrounds: [],
20598
+ priceLines: [],
20599
+ lines: [],
20600
+ boxes: [],
20601
+ labels: [],
20602
+ polylines: [],
20603
+ linefills: [],
20604
+ tables: [],
20605
+ barColors: [],
20606
+ trades: []
20607
+ };
20608
+ }
20460
20609
  function modelToValuePatch(model) {
20461
20610
  const series = [];
20462
20611
  let from = Number.POSITIVE_INFINITY;
@@ -23216,33 +23365,32 @@ function candleGeometry(xCss, spacing, dpr, bodyScale = 1) {
23216
23365
  center: (wickLeftDev + wickDev / 2) / dpr
23217
23366
  };
23218
23367
  }
23219
- function aggregateCandleColumns(bars, i0, i1, xOf, yOf) {
23368
+ function aggregateCandleColumns(bars, i0, i1, xOf, yOf, colorOf2) {
23220
23369
  const out = [];
23221
23370
  let col = NaN;
23222
- let intervals = [];
23371
+ let runs = [];
23223
23372
  const flush = () => {
23224
- if (intervals.length === 0) return;
23225
- const merged = [intervals[0]];
23226
- for (let k = 1; k < intervals.length; k += 1) {
23227
- const prev2 = merged[merged.length - 1];
23228
- const next = intervals[k];
23229
- if (Math.abs(yOf(prev2.hi) - yOf(next.lo)) < 1) {
23230
- prev2.hi = next.hi;
23231
- if (next.headIdx < prev2.headIdx) {
23232
- prev2.headIdx = next.headIdx;
23233
- prev2.headTime = next.headTime;
23234
- prev2.open = next.open;
23235
- }
23236
- if (next.lastIdx > prev2.lastIdx) {
23237
- prev2.lastIdx = next.lastIdx;
23238
- prev2.close = next.close;
23373
+ if (runs.length === 0) return;
23374
+ runs.sort((a, b) => a.lo - b.lo);
23375
+ const merged = [];
23376
+ for (const next of runs) {
23377
+ let prev2;
23378
+ for (let k = merged.length - 1; k >= 0; k -= 1) {
23379
+ if (merged[k].color === next.color) {
23380
+ prev2 = merged[k];
23381
+ break;
23239
23382
  }
23383
+ }
23384
+ if (prev2 && Math.abs(yOf(prev2.hi) - yOf(next.lo)) < 1) {
23385
+ prev2.hi = next.hi;
23386
+ if (next.lastIdx > prev2.lastIdx) prev2.lastIdx = next.lastIdx;
23240
23387
  } else {
23241
23388
  merged.push(next);
23242
23389
  }
23243
23390
  }
23244
- for (const iv of merged) out.push({ x: col, hi: iv.hi, lo: iv.lo, headTime: iv.headTime, open: iv.open, close: iv.close });
23245
- intervals = [];
23391
+ merged.sort((a, b) => a.lastIdx - b.lastIdx);
23392
+ for (const r of merged) out.push({ x: col, hi: r.hi, lo: r.lo, color: r.color });
23393
+ runs = [];
23246
23394
  };
23247
23395
  for (let i = i0; i <= i1; i += 1) {
23248
23396
  const b = bars[i];
@@ -23252,26 +23400,17 @@ function aggregateCandleColumns(bars, i0, i1, xOf, yOf) {
23252
23400
  flush();
23253
23401
  col = x;
23254
23402
  }
23403
+ const color = colorOf2(b);
23255
23404
  let lo = b.low;
23256
23405
  let hi = b.high;
23257
- let headIdx = i;
23258
- let headTime = b.time;
23259
- let open2 = b.open;
23260
- for (let k = intervals.length - 1; k >= 0; k -= 1) {
23261
- const iv = intervals[k];
23262
- if (iv.lo > hi || iv.hi < lo) continue;
23263
- if (iv.lo < lo) lo = iv.lo;
23264
- if (iv.hi > hi) hi = iv.hi;
23265
- if (iv.headIdx < headIdx) {
23266
- headIdx = iv.headIdx;
23267
- headTime = iv.headTime;
23268
- open2 = iv.open;
23269
- }
23270
- intervals.splice(k, 1);
23271
- }
23272
- let insertAt = 0;
23273
- while (insertAt < intervals.length && intervals[insertAt].lo < lo) insertAt += 1;
23274
- intervals.splice(insertAt, 0, { lo, hi, headIdx, headTime, open: open2, lastIdx: i, close: b.close });
23406
+ for (let k = runs.length - 1; k >= 0; k -= 1) {
23407
+ const r = runs[k];
23408
+ if (r.color !== color || r.lo > hi || r.hi < lo) continue;
23409
+ if (r.lo < lo) lo = r.lo;
23410
+ if (r.hi > hi) hi = r.hi;
23411
+ runs.splice(k, 1);
23412
+ }
23413
+ runs.push({ lo, hi, color, lastIdx: i });
23275
23414
  }
23276
23415
  flush();
23277
23416
  return out;
@@ -23942,7 +24081,7 @@ var WebGL2Backend = class {
23942
24081
  * Routing mirrors the main pass: own series per pane, force_overlay series on the price pane. */
23943
24082
  emitGlowSources(b, scene, pane, coords, i0, i1) {
23944
24083
  const emitOne = (s, off) => {
23945
- if (!isLineLikeSeries(s) || s.visible === false) return;
24084
+ if (!isLineLikeSeries(s) || !seriesShownOn(s, "pane")) return;
23946
24085
  if (s.kind === "histogram" || s.kind === "columns") return;
23947
24086
  if (s.kind === "circles" || s.kind === "cross") this.emitPointMarkers(b, s, pane, coords, i0, i1, off);
23948
24087
  else this.emitPolyline(b, s, pane, coords, i0, i1, s.kind === "step", off);
@@ -24053,7 +24192,7 @@ var WebGL2Backend = class {
24053
24192
  const spacing = coords.bodySpacing();
24054
24193
  const tier = candleTier(spacing);
24055
24194
  if (tier === "aggregate") {
24056
- this.emitCandlesAggregated(b, bars, i0, i1, coords, pane, up, down, barColors);
24195
+ this.emitCandlesAggregated(b, bars, i0, i1, coords, pane, (bar) => barColors.get(bar.time) ?? (bar.close >= bar.open ? up : down));
24057
24196
  return;
24058
24197
  }
24059
24198
  const tickW = Math.max(1, Math.round(spacing * 0.35));
@@ -24194,14 +24333,17 @@ var WebGL2Backend = class {
24194
24333
  const spacing = coords.bodySpacing();
24195
24334
  const tier = candleTier(spacing);
24196
24335
  const paint = effectiveCandlePaint(scene.style.candle, scene.candleOverride, theme.upColor, theme.downColor);
24336
+ const up = paint.up;
24337
+ const down = paint.down;
24338
+ const cs = paint.candle;
24197
24339
  if (tier === "aggregate") {
24198
- this.emitCandlesAggregated(b, bars, i0, i1, coords, pane, paint.up, paint.down, barColors);
24340
+ this.emitCandlesAggregated(b, bars, i0, i1, coords, pane, (bar) => {
24341
+ const isUp = bar.close >= bar.open;
24342
+ return (isUp ? cs.wickUpColor : cs.wickDownColor) ?? barColors.get(bar.time) ?? (isUp ? up : down);
24343
+ });
24199
24344
  return;
24200
24345
  }
24201
24346
  const drawBody = tier === "full";
24202
- const up = paint.up;
24203
- const down = paint.down;
24204
- const cs = paint.candle;
24205
24347
  const fading = this.candleStructureAlpha > this.candleBodyAlpha + 1e-3;
24206
24348
  for (let i = i0; i <= i1; i += 1) {
24207
24349
  const bar = bars[i];
@@ -24250,24 +24392,26 @@ var WebGL2Backend = class {
24250
24392
  /**
24251
24393
  * Sub-pixel LOD (mirrors Canvas2dBackend.drawCandlesAggregated): bars sharing a
24252
24394
  * rounded pixel column collapse into high-low sticks — one per contiguous
24253
- * coverage run (see {@link aggregateCandleColumns}), so a price gap inside the
24254
- * column stays a void. Draw cost stays bounded by screen width, not bar count;
24255
- * stick color follows its own first-open→last-close.
24395
+ * SAME-COLOR coverage run (see {@link aggregateCandleColumns}), so a price gap
24396
+ * inside the column stays a void and every bar's extent keeps its own color. Draw
24397
+ * cost stays bounded by screen width, not bar count. `colorOf` is the calling
24398
+ * style's per-bar paint rule (candles and OHLC bars resolve theirs differently).
24256
24399
  */
24257
- emitCandlesAggregated(b, bars, i0, i1, coords, pane, up, down, barColors) {
24400
+ emitCandlesAggregated(b, bars, i0, i1, coords, pane, colorOf2) {
24258
24401
  const yOf = (price) => coords.priceToY(price, pane.scale, pane.bounds);
24259
- for (const s of aggregateCandleColumns(bars, i0, i1, (i) => coords.logicalToX(i), yOf)) {
24260
- const c = parseColor(barColors.get(s.headTime) ?? (s.close >= s.open ? up : down));
24402
+ for (const s of aggregateCandleColumns(bars, i0, i1, (i) => coords.logicalToX(i), yOf, colorOf2)) {
24403
+ const c = parseColor(s.color);
24261
24404
  const hY = yOf(s.hi);
24262
24405
  b.rect(s.x - 0.5, hY, 1, yOf(s.lo) - hY, c);
24263
24406
  }
24264
24407
  }
24265
24408
  emitSeries(b, spec, pane, coords, i0, i1, theme, off = 0) {
24409
+ if (!seriesShownOn(spec, "pane")) return;
24266
24410
  if (spec.kind === "candle" || spec.kind === "bar") {
24267
24411
  this.emitPlotCandles(b, spec, pane, coords, i0, i1, theme, off);
24268
24412
  return;
24269
24413
  }
24270
- if (!isLineLikeSeries(spec) || spec.visible === false) return;
24414
+ if (!isLineLikeSeries(spec)) return;
24271
24415
  switch (spec.kind) {
24272
24416
  case "histogram":
24273
24417
  case "columns":
@@ -25931,7 +26075,7 @@ var Canvas2dBackend = class {
25931
26075
  const spacing = coords.bodySpacing();
25932
26076
  const tier = candleTier(spacing);
25933
26077
  if (tier === "aggregate") {
25934
- this.drawCandlesAggregated(ctx, bars, i0, i1, coords, pane, up, down, barColors);
26078
+ this.drawCandlesAggregated(ctx, bars, i0, i1, coords, pane, (b) => barColors.get(b.time) ?? (b.close >= b.open ? up : down));
25935
26079
  return;
25936
26080
  }
25937
26081
  const tickW = Math.max(1, Math.round(spacing * 0.35));
@@ -26107,12 +26251,15 @@ var Canvas2dBackend = class {
26107
26251
  const spacing = coords.bodySpacing();
26108
26252
  const tier = candleTier(spacing);
26109
26253
  const paint = effectiveCandlePaint(scene.style.candle, scene.candleOverride, theme.upColor, theme.downColor);
26254
+ const cs = paint.candle;
26110
26255
  if (tier === "aggregate") {
26111
- this.drawCandlesAggregated(ctx, bars, i0, i1, coords, pane, paint.up, paint.down, barColors);
26256
+ this.drawCandlesAggregated(ctx, bars, i0, i1, coords, pane, (b) => {
26257
+ const up = b.close >= b.open;
26258
+ return (up ? cs.wickUpColor : cs.wickDownColor) ?? barColors.get(b.time) ?? (up ? paint.up : paint.down);
26259
+ });
26112
26260
  return;
26113
26261
  }
26114
26262
  const drawBody = tier === "full";
26115
- const cs = paint.candle;
26116
26263
  const fading = this.candleStructureAlpha > this.candleBodyAlpha + 1e-3;
26117
26264
  for (let i = i0; i <= i1; i += 1) {
26118
26265
  const b = bars[i];
@@ -26169,17 +26316,18 @@ var Canvas2dBackend = class {
26169
26316
  }
26170
26317
  /**
26171
26318
  * Sub-pixel LOD: bars sharing a rounded pixel column collapse into high-low
26172
- * sticks (one per contiguous coverage run — see {@link aggregateCandleColumns}),
26173
- * so draw cost is bounded by screen width (not bar count) when zoomed far out
26174
- * and a price gap inside the column stays a void. Each stick's color follows
26175
- * its first-open→last-close direction (a barcolor() on the head bar still wins).
26319
+ * sticks (one per contiguous SAME-COLOR coverage run — see
26320
+ * {@link aggregateCandleColumns}), so draw cost is bounded by screen width (not
26321
+ * bar count) when zoomed far out, a price gap inside the column stays a void, and
26322
+ * every bar's extent keeps its own color. `colorOf` is the calling style's per-bar
26323
+ * paint rule (candles and OHLC bars resolve theirs differently).
26176
26324
  */
26177
- drawCandlesAggregated(ctx, bars, i0, i1, coords, pane, up, down, barColors) {
26325
+ drawCandlesAggregated(ctx, bars, i0, i1, coords, pane, colorOf2) {
26178
26326
  const yOf = (price) => coords.priceToY(price, pane.scale, pane.bounds);
26179
26327
  ctx.lineWidth = 1;
26180
- for (const s of aggregateCandleColumns(bars, i0, i1, (i) => coords.logicalToX(i), yOf)) {
26328
+ for (const s of aggregateCandleColumns(bars, i0, i1, (i) => coords.logicalToX(i), yOf, colorOf2)) {
26181
26329
  const x = s.x + 0.5;
26182
- ctx.strokeStyle = barColors.get(s.headTime) ?? (s.close >= s.open ? up : down);
26330
+ ctx.strokeStyle = s.color;
26183
26331
  ctx.beginPath();
26184
26332
  ctx.moveTo(x, yOf(s.hi));
26185
26333
  ctx.lineTo(x, yOf(s.lo));
@@ -26187,11 +26335,12 @@ var Canvas2dBackend = class {
26187
26335
  }
26188
26336
  }
26189
26337
  drawSeries(ctx, spec, pane, coords, i0, i1, theme, off = 0) {
26338
+ if (!seriesShownOn(spec, "pane")) return;
26190
26339
  if (spec.kind === "candle" || spec.kind === "bar") {
26191
26340
  this.drawPlotCandles(ctx, spec, pane, coords, i0, i1, theme, off);
26192
26341
  return;
26193
26342
  }
26194
- if (!isLineLikeSeries(spec) || spec.visible === false) return;
26343
+ if (!isLineLikeSeries(spec)) return;
26195
26344
  switch (spec.kind) {
26196
26345
  case "histogram":
26197
26346
  case "columns":
@@ -34417,6 +34566,7 @@ function computePaneScale(models, bars, includeCandles, i0, i1, drawings, log =
34417
34566
  return { min: min - span * MARGIN_BOTTOM, max: max + span * MARGIN_TOP };
34418
34567
  }
34419
34568
  function considerSeries(s, i0, i1, off, consider) {
34569
+ if (!seriesInScale(s)) return;
34420
34570
  if (s.kind === "candle" || s.kind === "bar") {
34421
34571
  for (let i = i0; i <= i1; i += 1) {
34422
34572
  const b = s.bars[i - off];
@@ -34636,6 +34786,22 @@ var MarkPopover = class {
34636
34786
  this.openKey = null;
34637
34787
  /** Bumped per open/close — a lazy content resolving after its popup went away is dropped. */
34638
34788
  this.generation = 0;
34789
+ /**
34790
+ * The cluster whose popup was open when the current pointer press began. The anchor is
34791
+ * hit-transparent (the glyph is canvas pixels), so the shell's outside-dismiss closes the
34792
+ * popup on pointerdown; the click the renderer derives from the same pointerup must then
34793
+ * read as "close", not reopen that cluster. Captured on the document ahead of the shell's
34794
+ * (deferred) listener, cleared once the release has dispatched.
34795
+ */
34796
+ this.pressKey = null;
34797
+ this.onPress = () => {
34798
+ if (this.openKey === null) return;
34799
+ this.pressKey = this.openKey;
34800
+ const doc = this.deps.plot.ownerDocument;
34801
+ const clear = () => void setTimeout(() => this.pressKey = null, 0);
34802
+ doc.addEventListener("pointerup", clear, { capture: true, once: true });
34803
+ doc.addEventListener("pointercancel", clear, { capture: true, once: true });
34804
+ };
34639
34805
  const doc = deps.plot.ownerDocument;
34640
34806
  injectStyles(CALLOUT_STYLE_ID, CALLOUT_CSS, doc);
34641
34807
  injectStyles(MARKS_STYLE_ID, MARKS_CSS, doc);
@@ -34643,12 +34809,14 @@ var MarkPopover = class {
34643
34809
  this.anchor.className = "vela-marks-anchor";
34644
34810
  Object.assign(this.anchor.style, { position: "absolute", pointerEvents: "none", left: "0", top: "0", width: "0", height: "0" });
34645
34811
  deps.plot.appendChild(this.anchor);
34812
+ doc.addEventListener("pointerdown", this.onPress, true);
34646
34813
  }
34647
34814
  /** The cluster key the open popup belongs to, or null. */
34648
34815
  get key() {
34649
34816
  return this.openKey;
34650
34817
  }
34651
34818
  open(cluster, rect) {
34819
+ if (this.pressKey === cluster.key) return;
34652
34820
  this.close();
34653
34821
  this.place(rect);
34654
34822
  const gen = ++this.generation;
@@ -34696,6 +34864,7 @@ var MarkPopover = class {
34696
34864
  }
34697
34865
  destroy() {
34698
34866
  this.close();
34867
+ this.deps.plot.ownerDocument.removeEventListener("pointerdown", this.onPress, true);
34699
34868
  this.anchor.remove();
34700
34869
  }
34701
34870
  place(rect) {
@@ -35456,6 +35625,13 @@ var NativeRenderer = class {
35456
35625
  this.backdropRenderer = new BackdropRenderer();
35457
35626
  this.volumeRenderer = new VolumeRenderer();
35458
35627
  /** SDK renderer layers instantiated at mount ({@link registerRendererLayer}). */
35628
+ /**
35629
+ * The mounted SDK layers. One BASE entry per registered definition (channel = the layer
35630
+ * id, no owner of its own), plus one DEDICATED entry per mounted native indicator whose
35631
+ * model names its own channel (`native.channel` — an instance of a multi-instance type):
35632
+ * same definition, its own canvas, reading that instance's channel and following that
35633
+ * instance's pane and z key. `channel` is unique across the array.
35634
+ */
35459
35635
  this.extLayers = [];
35460
35636
  /** Last applied layer-canvas order (ids below + above the data canvas) — re-slotted only on change. */
35461
35637
  this.layerOrderSig = "";
@@ -36601,11 +36777,7 @@ var NativeRenderer = class {
36601
36777
  Object.assign(this.overlayRoot.style, { position: "absolute", inset: "0", pointerEvents: "none" });
36602
36778
  this.plot = document.createElement("div");
36603
36779
  Object.assign(this.plot.style, { position: "absolute", top: "0", right: "0", bottom: "0", left: "0" });
36604
- this.extLayers = rendererLayers().map((def) => {
36605
- const canvas = document.createElement("canvas");
36606
- Object.assign(canvas.style, { position: "absolute", inset: "0", width: "100%", height: "100%", pointerEvents: "none" });
36607
- return { def, instance: def.create(), canvas };
36608
- });
36780
+ this.extLayers = rendererLayers().map((def) => ({ def, instance: def.create(), canvas: this.createLayerCanvas(), channel: def.id, owner: null }));
36609
36781
  const below = this.extLayers.filter((l) => l.def.placement === "below-data").map((l) => l.canvas);
36610
36782
  const above = this.extLayers.filter((l) => l.def.placement !== "below-data").map((l) => l.canvas);
36611
36783
  this.plot.append(this.backdropCanvas, ...below, this.dataCanvas, this.volumeCanvas, this.vpvrCanvas, ...above, this.chromeCanvas, this.drawingsCanvas, this.cursorCanvas, this.overlayRoot);
@@ -36622,6 +36794,7 @@ var NativeRenderer = class {
36622
36794
  this.volumeRenderer.mount(this.volumeCanvas);
36623
36795
  this.vpvrRenderer.mount(this.vpvrCanvas);
36624
36796
  for (const l of this.extLayers) l.instance.mount(l.canvas);
36797
+ for (const m of this.scene.indicators.values()) this.ensureInstanceLayer(m);
36625
36798
  this.chrome.mount(this.chromeCanvas);
36626
36799
  this.crosshairLayer.mount(this.cursorCanvas);
36627
36800
  this.coords.setViewport(defaultViewport());
@@ -37214,6 +37387,7 @@ var NativeRenderer = class {
37214
37387
  this.refreshAnchorOffset(model);
37215
37388
  if (model.native && this.extLayers.some((l) => l.def.id === model.native.type)) this.scene.assignIndicatorZTop(model.id);
37216
37389
  else this.scene.assignIndicatorZ(model.id);
37390
+ this.ensureInstanceLayer(model);
37217
37391
  if (model.legend !== false) {
37218
37392
  this.inputsUI.upsert(model.id, model.shorttitle ?? model.title, model.inputs, model.inputValues, model.paneId, {
37219
37393
  native: !!model.native,
@@ -37257,6 +37431,7 @@ var NativeRenderer = class {
37257
37431
  this.scene.vpvrLayer = null;
37258
37432
  }
37259
37433
  this.scene.indicators.delete(handle.id);
37434
+ this.dropInstanceLayer(handle.id);
37260
37435
  this.scene.forgetIndicatorZ(handle.id);
37261
37436
  this.scene.forgetAnchorOffset(handle.id);
37262
37437
  this.scene.dropIndicatorScale(handle.id);
@@ -38050,29 +38225,31 @@ var NativeRenderer = class {
38050
38225
  up: bar.close >= bar.open
38051
38226
  };
38052
38227
  }
38053
- /** One group per indicator (name = indicator title), each with a row per drawable plot. */
38228
+ /** One group per indicator (name = indicator title), each with a row per plot shown in the data window. */
38054
38229
  dataWindowGroups(idx, pricePane) {
38055
38230
  const groups = [];
38056
38231
  for (const model of this.scene.indicators.values()) {
38057
- const rows = this.dataWindowRowsFor(model, idx, pricePane);
38232
+ const rows = this.readoutRowsFor(model, idx, pricePane, "dataWindow");
38058
38233
  if (rows.length) groups.push({ name: model.title, rows });
38059
38234
  }
38060
38235
  return groups;
38061
38236
  }
38062
- /** One indicator's readout at bar `idx`: a row per drawable plot, formatted on its pane's scale. */
38063
- dataWindowRowsFor(model, idx, pricePane) {
38237
+ /** One indicator's readout at bar `idx` for one value surface (the legend or the data window):
38238
+ * a row per plot shown on that surface, formatted on its pane's scale. A plot's `display`
38239
+ * decides per surface — a data-window-only plot has a row here and none in the legend. */
38240
+ readoutRowsFor(model, idx, pricePane, surface) {
38064
38241
  if (model.native?.type === "volume") return this.volumeReadoutRows(model, idx);
38065
38242
  const pane = (model.paneId ? this.scene.panes.get(model.paneId) : null) ?? pricePane;
38066
38243
  const off = this.scene.offsetOf(model.id);
38067
38244
  const rows = [];
38068
38245
  for (const s of model.series) {
38246
+ if (!seriesShownOn(s, surface)) continue;
38069
38247
  let value;
38070
38248
  let color;
38071
38249
  if (s.kind === "candle" || s.kind === "bar") {
38072
38250
  value = s.bars[idx - off]?.close;
38073
38251
  color = s.style?.up ?? this.theme.upColor;
38074
38252
  } else if (isLineLikeSeries(s)) {
38075
- if (s.visible === false) continue;
38076
38253
  value = s.points[idx - off]?.value;
38077
38254
  color = s.points[idx - off]?.color ?? s.style.color;
38078
38255
  } else {
@@ -38095,9 +38272,10 @@ var NativeRenderer = class {
38095
38272
  const color = bar.close >= bar.open ? cfg?.upColor ?? this.theme.upColor : cfg?.downColor ?? this.theme.downColor;
38096
38273
  return [{ label: model.title, value: formatVolume(vol), color }];
38097
38274
  }
38098
- /** Refresh the plot values beside every legend title — the same readout the data window
38099
- * shows (crosshair bar, else the latest bar), pushed per paint. A hidden indicator has
38100
- * no scene model, so its row is absent from the map and its readout clears. */
38275
+ /** Refresh the plot values beside every legend title — the same bar the data window reads
38276
+ * (crosshair bar, else the latest bar), pushed per paint, filtered to the plots whose
38277
+ * `display` includes the legend. A hidden indicator has no scene model, so its row is
38278
+ * absent from the map and its readout clears. */
38101
38279
  updateLegendValues() {
38102
38280
  if (!this.inputsUI) return;
38103
38281
  const n = this.bars.length;
@@ -38106,7 +38284,7 @@ var NativeRenderer = class {
38106
38284
  const idx = this.hoverLogical != null ? this.hoverLogical : n - 1;
38107
38285
  const pricePane = this.dataWindowPricePane();
38108
38286
  for (const model of this.scene.indicators.values()) {
38109
- values.set(model.id, this.dataWindowRowsFor(model, idx, pricePane).map((r) => ({ value: r.value, color: r.color })));
38287
+ values.set(model.id, this.readoutRowsFor(model, idx, pricePane, "legend").map((r) => ({ value: r.value, color: r.color })));
38110
38288
  }
38111
38289
  }
38112
38290
  this.inputsUI.setPlotValues(values);
@@ -38132,9 +38310,9 @@ var NativeRenderer = class {
38132
38310
  const nowMs = typeof performance !== "undefined" ? performance.now() : Date.now();
38133
38311
  for (const l of this.extLayers) {
38134
38312
  if (!l.def.repaintOnCursor) continue;
38135
- const lp = this.layerPane(l.def.id) ?? pane;
38313
+ const lp = this.layerPane(l) ?? pane;
38136
38314
  if (lp.collapsed) continue;
38137
- l.instance.render(this.extLayerArgs(l.def.id, lp.scale, lp.bounds, nowMs));
38315
+ l.instance.render(this.extLayerArgs(l, lp.scale, lp.bounds, nowMs));
38138
38316
  if (this.animZoom.on && l.instance.animating?.()) this.animator.start();
38139
38317
  }
38140
38318
  }
@@ -38143,13 +38321,15 @@ var NativeRenderer = class {
38143
38321
  if (canvas.width === 0 || canvas.height === 0) return;
38144
38322
  canvas.getContext("2d")?.clearRect(0, 0, canvas.width, canvas.height);
38145
38323
  }
38146
- /** One frame's args for an SDK renderer layer (shared by the data + cursor paint paths). */
38147
- extLayerArgs(id, scale, bounds, nowMs) {
38324
+ /** One frame's args for an SDK renderer layer (shared by the data + cursor paint paths).
38325
+ * Data + pending come from the entry's channel (per instance for a dedicated entry);
38326
+ * the settings bag is the TYPE's — a chart type's dialog values, one bag per type. */
38327
+ extLayerArgs(l, scale, bounds, nowMs) {
38148
38328
  return {
38149
38329
  bars: this.scene.bars,
38150
- data: this.scene.nativeData.get(id),
38151
- settings: this.scene.nativeData.get(`${id}-settings`) ?? {},
38152
- pending: this.scene.nativePending.get(id) ?? [],
38330
+ data: this.scene.nativeData.get(l.channel),
38331
+ settings: this.scene.nativeData.get(`${l.def.id}-settings`) ?? {},
38332
+ pending: this.scene.nativePending.get(l.channel) ?? [],
38153
38333
  coords: this.coords,
38154
38334
  scale,
38155
38335
  bounds,
@@ -38192,12 +38372,12 @@ var NativeRenderer = class {
38192
38372
  const nowMs = typeof performance !== "undefined" ? performance.now() : Date.now();
38193
38373
  let folded = null;
38194
38374
  for (const l of this.extLayers) {
38195
- const lp = this.layerPane(l.def.id) ?? pane;
38375
+ const lp = this.layerPane(l) ?? pane;
38196
38376
  if (lp.collapsed) {
38197
38377
  this.clearLayerCanvas(l.canvas);
38198
38378
  continue;
38199
38379
  }
38200
- const args = this.extLayerArgs(l.def.id, lp.scale, lp.bounds, nowMs);
38380
+ const args = this.extLayerArgs(l, lp.scale, lp.bounds, nowMs);
38201
38381
  l.instance.render(args);
38202
38382
  if (lp === pane) folded = foldBaseModulation(folded, l.instance.modulateBase?.(args) ?? null);
38203
38383
  if (this.animZoom.on && l.instance.animating?.()) this.animator.start();
@@ -38361,7 +38541,7 @@ var NativeRenderer = class {
38361
38541
  const or = overlaySeriesRange(this.scene.indicators.values(), i0, i1, (id) => this.scene.offsetOf(id));
38362
38542
  if (or) dr = dr ? { min: Math.min(dr.min, or.min), max: Math.max(dr.max, or.max) } : or;
38363
38543
  }
38364
- const includeCandles = pane.kind === "price" && !this.scene.candlesHidden;
38544
+ const includeCandles = pane.kind === "price" && (!this.scene.candlesHidden || this.priceLayersAnchoredToBars(masterModels));
38365
38545
  pane.scaleTarget = computePaneScale(masterModels, this.bars, includeCandles, i0, i1, dr, paneLogScale(this.scene, pane), (id) => this.scene.offsetOf(id));
38366
38546
  pane.percentBaseline = pane.kind === "price" ? this.bars[i0]?.close ?? 0 : this.firstVisibleValue(masterModels, i0);
38367
38547
  pane.axisFormat = void 0;
@@ -38480,19 +38660,60 @@ var NativeRenderer = class {
38480
38660
  }
38481
38661
  return null;
38482
38662
  }
38483
- /** The mounted native indicator that OWNS an SDK layer the one whose type equals the
38484
- * layer id (the id doubles as the data channel, so the pairing is the SDK's own
38485
- * contract). Null for chart-type channels and while the owner is hidden (a hidden
38486
- * indicator leaves the scene; its cleared data channel paints nothing anyway). */
38487
- layerOwner(layerId) {
38663
+ /** One transparent, pointer-transparent SDK layer canvas covering the plot. */
38664
+ createLayerCanvas() {
38665
+ const canvas = document.createElement("canvas");
38666
+ Object.assign(canvas.style, { position: "absolute", inset: "0", width: "100%", height: "100%", pointerEvents: "none" });
38667
+ return canvas;
38668
+ }
38669
+ /**
38670
+ * Mount the DEDICATED layer of a native indicator that names its own channel (an
38671
+ * instance of a multi-instance type painting through a registered layer): a fresh
38672
+ * instance of the layer's definition on its own canvas, reading that channel. Idempotent
38673
+ * — a show after hide re-mounts the model, and the layer (kept across the hide, like the
38674
+ * z key) is simply reused. Skipped while the renderer itself is unmounted (`mount()`
38675
+ * catches up from the scene).
38676
+ */
38677
+ ensureInstanceLayer(model) {
38678
+ const channel = model.native?.channel;
38679
+ if (!channel || !this.plot) return;
38680
+ if (this.extLayers.some((l) => l.owner === model.id)) return;
38681
+ const base = this.extLayers.find((l) => l.owner === null && l.def.id === model.native.type);
38682
+ if (!base) return;
38683
+ const canvas = this.createLayerCanvas();
38684
+ canvas.width = this.dataCanvas.width;
38685
+ canvas.height = this.dataCanvas.height;
38686
+ canvas.style.width = this.dataCanvas.style.width || "100%";
38687
+ canvas.style.height = this.dataCanvas.style.height || "100%";
38688
+ const entry = { def: base.def, instance: base.def.create(), canvas, channel, owner: model.id };
38689
+ this.extLayers.push(entry);
38690
+ this.plot.insertBefore(canvas, this.chromeCanvas);
38691
+ entry.instance.mount(canvas);
38692
+ }
38693
+ /** Tear down the dedicated layer of a removed indicator (a hide keeps it). */
38694
+ dropInstanceLayer(indicatorId) {
38695
+ const i = this.extLayers.findIndex((l2) => l2.owner === indicatorId);
38696
+ if (i < 0) return;
38697
+ const [l] = this.extLayers.splice(i, 1);
38698
+ l.instance.destroy?.();
38699
+ l.canvas.remove();
38700
+ }
38701
+ /** The mounted native indicator that OWNS an SDK layer entry. A dedicated entry is
38702
+ * owned by the instance it was mounted for; the base entry by the mounted indicator
38703
+ * whose type equals the layer id and that has no channel of its own (the id doubles
38704
+ * as the data channel, so the pairing is the SDK's own contract). Null for chart-type
38705
+ * channels and while the owner is hidden (a hidden indicator leaves the scene; its
38706
+ * cleared data channel paints nothing anyway). */
38707
+ layerOwner(l) {
38708
+ if (l.owner !== null) return this.scene.indicators.get(l.owner) ?? null;
38488
38709
  for (const m of this.scene.indicators.values()) {
38489
- if (m.native?.type === layerId) return m;
38710
+ if (m.native?.type === l.def.id && !m.native.channel) return m;
38490
38711
  }
38491
38712
  return null;
38492
38713
  }
38493
38714
  /** The pane an SDK layer paints on: its owner's pane, else the price pane. */
38494
- layerPane(layerId) {
38495
- const owner = this.layerOwner(layerId);
38715
+ layerPane(l) {
38716
+ const owner = this.layerOwner(l);
38496
38717
  const paneId = owner ? owner.paneId ?? PRICE_PANE_ID2 : PRICE_PANE_ID2;
38497
38718
  return this.scene.panes.get(paneId) ?? null;
38498
38719
  }
@@ -38500,12 +38721,12 @@ var NativeRenderer = class {
38500
38721
  * owned layers by their owner's z key against the candles' (an indicator restacked
38501
38722
  * below the candles takes its layer canvas along), unowned by declared placement. */
38502
38723
  orderedLayerCanvases() {
38503
- const byId = new Map(this.extLayers.map((l) => [l.def.id, l.canvas]));
38724
+ const byId = new Map(this.extLayers.map((l) => [l.channel, l.canvas]));
38504
38725
  const { below, above } = stackLayers(
38505
38726
  this.extLayers.map((l) => {
38506
- const owner = this.layerOwner(l.def.id);
38727
+ const owner = this.layerOwner(l);
38507
38728
  return {
38508
- id: l.def.id,
38729
+ id: l.channel,
38509
38730
  placement: l.def.placement === "below-data" ? "below-data" : "above-data",
38510
38731
  ownerZ: owner ? this.scene.zOf(owner.id) : null
38511
38732
  };
@@ -38527,7 +38748,7 @@ var NativeRenderer = class {
38527
38748
  syncLayerCanvasOrder() {
38528
38749
  if (this.extLayers.length === 0 || !this.plot) return;
38529
38750
  const { below, above } = this.orderedLayerCanvases();
38530
- 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(",");
38751
+ 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(",");
38531
38752
  if (sig === this.layerOrderSig) return;
38532
38753
  this.layerOrderSig = sig;
38533
38754
  for (const c of below) this.plot.insertBefore(c, this.dataCanvas);
@@ -38551,6 +38772,17 @@ var NativeRenderer = class {
38551
38772
  (m) => m.series.length === 0 && !!m.native && this.extLayers.some((l) => l.def.id === m.native.type)
38552
38773
  );
38553
38774
  }
38775
+ /**
38776
+ * True when SDK-layer content on the price pane paints at BAR PRICES without
38777
+ * contributing a series: the active chart type's own layer, or an overlay layer native
38778
+ * (a series-less model whose type names a mounted layer). Hiding the candles must then
38779
+ * keep the bars in the price scale — with nothing else on the pane the scale would fall
38780
+ * to the {0,1} placeholder and every such layer would paint off-screen and vanish.
38781
+ */
38782
+ priceLayersAnchoredToBars(masterModels) {
38783
+ if (this.extLayers.some((l) => l.owner === null && l.def.id === this.scene.priceStyle)) return true;
38784
+ return masterModels.some((m) => m.series.length === 0 && !!m.native && this.extLayers.some((l) => l.def.id === m.native.type));
38785
+ }
38554
38786
  /** Per-pane scale state for a host UI (e.g. a price-axis context menu): the pane's pixel
38555
38787
  * band (`top`/`height`, so a click y maps to a pane) plus its current axis `mode`/`log`.
38556
38788
  * Top-to-bottom order. Every pane is independent — the price pane from the scene setting,
@@ -43691,7 +43923,8 @@ var ChartCell = class {
43691
43923
  this.statusline?.setFitMode(on);
43692
43924
  }
43693
43925
  /** The workspace shell keeps the app theme; the cell host's tokens re-derive from
43694
- * the LIVE plot surface (see {@link applyPlotOverlayTokens}). */
43926
+ * the LIVE plot surface for overlay ink (statusline, watermark). Menus portal to
43927
+ * `.vela-ui` and keep the app-theme surface (see {@link applyPlotOverlayTokens}). */
43695
43928
  syncPlotOverlayTokens() {
43696
43929
  applyPlotOverlayTokens(this.host, this.appTheme, this.inner?.renderer.getConfig() ?? null);
43697
43930
  }
@@ -44581,7 +44814,21 @@ var VelaWorkspace = class {
44581
44814
  /** Live sync configuration (mutable copy of the option). */
44582
44815
  this.syncOpts = {};
44583
44816
  this.stateTimer = null;
44584
- this.onUnload = () => this.persistNow();
44817
+ /** Re-entrance guard for {@link flushPendingState}: a `state:changed` subscriber that
44818
+ * calls back into the workspace (`destroy()` on save is the obvious one) must not
44819
+ * re-enter the flush and recurse. */
44820
+ this.emitting = false;
44821
+ /** False until the constructor finishes. Boot is not an edit: seeding the sync
44822
+ * settings marks state dirty, so without this a workspace mounted and torn down
44823
+ * inside the debounce window would flush and tell the host the user had changed
44824
+ * something — a save of the default document over whatever was stored. */
44825
+ this.booted = false;
44826
+ /** A page unload gets the same flush as a teardown — the pending edit reaches the
44827
+ * host, the timer is cleared, and a `destroy()` that follows adds no second write. */
44828
+ this.onUnload = () => {
44829
+ this.flushPendingState();
44830
+ this.persistNow();
44831
+ };
44585
44832
  /** Re-entrance guard around one propagation tick: followers' synchronous echoes
44586
44833
  * (their setVisibleRange re-emits viewport:changed) must not re-propagate. */
44587
44834
  this.syncBusy = false;
@@ -44895,6 +45142,7 @@ var VelaWorkspace = class {
44895
45142
  }
44896
45143
  this.mountAttachments();
44897
45144
  this.restoreGlobalExt();
45145
+ this.booted = true;
44898
45146
  }
44899
45147
  // ── access ──────────────────────────────────────────────────
44900
45148
  /** The cell with identity `id` (its declared name, or `c<N>` when undeclared), or
@@ -45303,13 +45551,17 @@ var VelaWorkspace = class {
45303
45551
  }
45304
45552
  destroy() {
45305
45553
  if (this.destroyed) return;
45306
- this.persistNow();
45554
+ this.flushPendingState();
45307
45555
  this.destroyed = true;
45308
- if (this.stateTimer != null) clearTimeout(this.stateTimer);
45309
45556
  if (this.persistKey !== null && typeof window !== "undefined") window.removeEventListener("beforeunload", this.onUnload);
45310
45557
  this.resizeObserver?.disconnect();
45311
45558
  this.splitters.destroy();
45312
45559
  for (const [id, cell] of [...this.cellsById]) {
45560
+ try {
45561
+ this.poolSet(id, cell.dehydrate());
45562
+ } catch (err) {
45563
+ console.warn(`[vela] could not snapshot cell "${id}" while destroying:`, err);
45564
+ }
45313
45565
  cell.destroy();
45314
45566
  this.cellsById.delete(id);
45315
45567
  }
@@ -45395,7 +45647,7 @@ var VelaWorkspace = class {
45395
45647
  /** Debounced dirty mark: one `state:changed` (+ one storage write in persist mode)
45396
45648
  * per burst of edits, flushed hard on unload/destroy. */
45397
45649
  markStateDirty() {
45398
- if (this.destroyed) return;
45650
+ if (this.destroyed || !this.booted) return;
45399
45651
  if (this.stateTimer != null) clearTimeout(this.stateTimer);
45400
45652
  this.stateTimer = setTimeout(() => {
45401
45653
  this.stateTimer = null;
@@ -45403,6 +45655,44 @@ var VelaWorkspace = class {
45403
45655
  this.persistNow();
45404
45656
  }, 500);
45405
45657
  }
45658
+ /**
45659
+ * Push a pending debounced change out NOW, for a teardown or a page unload — the
45660
+ * two moments the 500ms timer would otherwise never reach.
45661
+ *
45662
+ * Dropping it was a silent data loss: the timer carries BOTH halves of the dirty
45663
+ * signal — the `state:changed` event and the storage write — so a host that saves
45664
+ * from that event (a server-backed host, where `persistNow` is a no-op for want of
45665
+ * a `persistKey`) simply never heard about the user's last edit.
45666
+ *
45667
+ * Idempotent and re-entrancy safe, which the ordering here buys:
45668
+ * - a no-op unless a timer is actually pending, so a teardown with nothing
45669
+ * outstanding stays silent and never invents an edit;
45670
+ * - the timer is cleared FIRST, so nothing survives this call and a second call
45671
+ * does nothing — `destroy()` after an unload flush adds no second write;
45672
+ * - the state is snapshotted BEFORE the handlers run, and that snapshot is what
45673
+ * gets persisted, so storage and the host are told the same thing even if a
45674
+ * handler mutates state while it runs;
45675
+ * - `emitting` is held across the emit, so a handler calling back in (a host that
45676
+ * tears down on save is the obvious one) cannot re-enter this and recurse.
45677
+ */
45678
+ flushPendingState() {
45679
+ if (this.stateTimer == null || this.emitting) return;
45680
+ clearTimeout(this.stateTimer);
45681
+ this.stateTimer = null;
45682
+ const snapshot = this.persistKey !== null ? encodeState(this.getState()) : null;
45683
+ this.emitting = true;
45684
+ try {
45685
+ this.events.emit("state:changed", void 0);
45686
+ } finally {
45687
+ this.emitting = false;
45688
+ }
45689
+ if (snapshot !== null && this.persistKey !== null) {
45690
+ try {
45691
+ void this.storage.set(this.persistKey, snapshot);
45692
+ } catch {
45693
+ }
45694
+ }
45695
+ }
45406
45696
  /** Write the current state through the storage adapter now (fire-and-forget). */
45407
45697
  persistNow() {
45408
45698
  if (this.persistKey === null || this.destroyed) return;