@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
@@ -51,6 +51,13 @@ var Vela = (function (exports) {
51
51
  function isLineLikeSeries(spec) {
52
52
  return spec.kind !== "candle" && spec.kind !== "bar" && spec.kind !== "markers";
53
53
  }
54
+ function seriesShownOn(spec, surface) {
55
+ if (spec.display) return spec.display[surface] !== false;
56
+ return spec.visible !== false;
57
+ }
58
+ function seriesInScale(spec) {
59
+ return seriesShownOn(spec, "pane") || seriesShownOn(spec, "priceScale");
60
+ }
54
61
 
55
62
  // src/core/events/EventBus.ts
56
63
  var TypedEventBus = class {
@@ -120,6 +127,9 @@ var Vela = (function (exports) {
120
127
  };
121
128
 
122
129
  // src/core/native-indicators/NativeIndicator.ts
130
+ function nativeInstanceChannel(type, instanceId) {
131
+ return `${type}#${instanceId}`;
132
+ }
123
133
  var REGISTRY = /* @__PURE__ */ new Map();
124
134
  function registerNativeIndicator(descriptor) {
125
135
  REGISTRY.set(descriptor.type, descriptor);
@@ -195,6 +205,9 @@ var Vela = (function (exports) {
195
205
  setProps(values) {
196
206
  this.controller.applyProps(this.id, values);
197
207
  }
208
+ updateCode(source) {
209
+ this.controller.updateCode(this.id, source);
210
+ }
198
211
  setVisible(visible) {
199
212
  this.controller.setVisible(this.id, visible);
200
213
  }
@@ -217,6 +230,10 @@ var Vela = (function (exports) {
217
230
  setPropsSchema(schema) {
218
231
  this.propsSchema = schema;
219
232
  }
233
+ /** Sync the public `source` once a code update has been prepared and taken over. */
234
+ setSource(source) {
235
+ this.source = source;
236
+ }
220
237
  /** Sync the public `visible` getter when the orchestrator changes visibility (API or legend eye). */
221
238
  setVisibleState(visible) {
222
239
  this.visibleState = visible;
@@ -2323,16 +2340,18 @@ var Vela = (function (exports) {
2323
2340
  geometry(proj) {
2324
2341
  const a = this.anchors[0];
2325
2342
  const b = this.anchors[1];
2326
- const c = this.anchors[2];
2327
- if (!a || !b || !c) return null;
2343
+ if (!a || !b) return null;
2328
2344
  const px = (p) => {
2329
2345
  const y = proj.yOf(p.price, this.paneId);
2330
2346
  return y == null ? null : [proj.xOf(p.time), y];
2331
2347
  };
2332
2348
  const P0 = px(a);
2333
2349
  const P1 = px(b);
2350
+ if (!P0 || !P1) return null;
2351
+ const c = this.anchors[2];
2352
+ if (!c) return { segments: [[P0[0], P0[1], P1[0], P1[1]]], fill: null };
2334
2353
  const P2 = px(c);
2335
- if (!P0 || !P1 || !P2) return null;
2354
+ if (!P2) return null;
2336
2355
  const mx = (P1[0] + P2[0]) / 2;
2337
2356
  const my = (P1[1] + P2[1]) / 2;
2338
2357
  const dx = mx - P0[0];
@@ -2379,16 +2398,19 @@ var Vela = (function (exports) {
2379
2398
  geometry(proj) {
2380
2399
  const a = this.anchors[0];
2381
2400
  const b = this.anchors[1];
2382
- const c = this.anchors[2];
2383
- if (!a || !b || !c) return null;
2401
+ if (!a || !b) return null;
2384
2402
  const px = (p) => {
2385
2403
  const y = proj.yOf(p.price, this.paneId);
2386
2404
  return y == null ? null : [proj.xOf(p.time), y];
2387
2405
  };
2388
- const S2 = px(this.medianStart(a, b, c));
2406
+ const P0 = px(a);
2389
2407
  const P1 = px(b);
2408
+ if (!P0 || !P1) return null;
2409
+ const c = this.anchors[2];
2410
+ if (!c) return { segments: [[P0[0], P0[1], P1[0], P1[1]]], fill: null };
2411
+ const S2 = px(this.medianStart(a, b, c));
2390
2412
  const P2 = px(c);
2391
- if (!S2 || !P1 || !P2) return null;
2413
+ if (!S2 || !P2) return null;
2392
2414
  const mx = (P1[0] + P2[0]) / 2;
2393
2415
  const my = (P1[1] + P2[1]) / 2;
2394
2416
  const dx = mx - S2[0];
@@ -2403,8 +2425,12 @@ var Vela = (function (exports) {
2403
2425
  // upper tine
2404
2426
  extendRay(P2[0], P2[1], P2[0] + dx, P2[1] + dy, "right", w, h),
2405
2427
  // lower tine
2406
- [P1[0], P1[1], P2[0], P2[1]]
2428
+ [P1[0], P1[1], P2[0], P2[1]],
2407
2429
  // base line
2430
+ // The median leaves from the shifted origin, not the pivot — without this construction
2431
+ // line the pivot handle would float unattached to the shape it defines.
2432
+ [P0[0], P0[1], P1[0], P1[1]]
2433
+ // pivot → first tine anchor
2408
2434
  ],
2409
2435
  fill: null
2410
2436
  };
@@ -2461,17 +2487,19 @@ var Vela = (function (exports) {
2461
2487
  geometry(proj) {
2462
2488
  const a = this.anchors[0];
2463
2489
  const b = this.anchors[1];
2464
- const c = this.anchors[2];
2465
- if (!a || !b || !c) return null;
2490
+ if (!a || !b) return null;
2466
2491
  const px = (p) => {
2467
2492
  const y = proj.yOf(p.price, this.paneId);
2468
2493
  return y == null ? null : [proj.xOf(p.time), y];
2469
2494
  };
2470
- const B = px({ time: (a.time + b.time) / 2, price: (a.price + b.price) / 2 });
2471
2495
  const P0 = px(a);
2472
2496
  const P1 = px(b);
2497
+ if (!P0 || !P1) return null;
2498
+ const c = this.anchors[2];
2499
+ if (!c) return { segments: [[P0[0], P0[1], P1[0], P1[1]]], fill: null };
2500
+ const B = px({ time: (a.time + b.time) / 2, price: (a.price + b.price) / 2 });
2473
2501
  const P2 = px(c);
2474
- if (!B || !P0 || !P1 || !P2) return null;
2502
+ if (!B || !P2) return null;
2475
2503
  const mx = (P1[0] + P2[0]) / 2;
2476
2504
  const my = (P1[1] + P2[1]) / 2;
2477
2505
  const dx = P2[0] - B[0];
@@ -2642,16 +2670,18 @@ var Vela = (function (exports) {
2642
2670
  geometry(proj) {
2643
2671
  const a = this.anchors[0];
2644
2672
  const b = this.anchors[1];
2645
- const c = this.anchors[2];
2646
- if (!a || !b || !c) return null;
2673
+ if (!a || !b) return null;
2647
2674
  const px = (p) => {
2648
2675
  const y = proj.yOf(p.price, this.paneId);
2649
2676
  return y == null ? null : [proj.xOf(p.time), y];
2650
2677
  };
2651
2678
  const A = px(a);
2652
2679
  const B = px(b);
2680
+ if (!A || !B) return null;
2681
+ const c = this.anchors[2];
2682
+ if (!c) return { segments: [[A[0], A[1], B[0], B[1]]], fill: null };
2653
2683
  const C = px(c);
2654
- if (!A || !B || !C) return null;
2684
+ if (!C) return null;
2655
2685
  return {
2656
2686
  segments: [
2657
2687
  [A[0], A[1], B[0], B[1]],
@@ -8180,6 +8210,7 @@ var Vela = (function (exports) {
8180
8210
  if (identityChanged) {
8181
8211
  this.beginLoad(false);
8182
8212
  this.setBarSeries([], { clearing: true });
8213
+ this.blankIndicatorVisuals();
8183
8214
  if (this.activeEngineStyle) {
8184
8215
  this.typeEngines.get(this.activeEngineStyle)?.suspend();
8185
8216
  this.renderer.setNativeData?.(this.activeEngineStyle, void 0);
@@ -8222,7 +8253,7 @@ var Vela = (function (exports) {
8222
8253
  restartNativeIndicators() {
8223
8254
  for (const record of this.registry.all()) {
8224
8255
  if (!record.native) continue;
8225
- record.native.instance.stop();
8256
+ if (record.native.started) record.native.instance.stop();
8226
8257
  record.native.instance = record.native.descriptor.create();
8227
8258
  record.native.started = false;
8228
8259
  record.pendingStructural = true;
@@ -8430,6 +8461,23 @@ var Vela = (function (exports) {
8430
8461
  this.renderer.setBars(this.bars, { preserveView: true });
8431
8462
  this.reexecuteIndicators();
8432
8463
  }
8464
+ /**
8465
+ * Blank every mounted indicator's painted output — series data, fills, backgrounds,
8466
+ * price lines, drawings, bar colors, trades — while keeping the mount (legend row,
8467
+ * pane, settings) intact, via the same idempotent-by-id remount an input edit uses.
8468
+ * Called on a market identity switch: the fresh models arrive only after the new
8469
+ * bars load, so nothing blanked here is ever restored — each consumer's re-run
8470
+ * remounts a full model over it.
8471
+ */
8472
+ blankIndicatorVisuals() {
8473
+ for (const record of this.registry.all()) {
8474
+ if (record.hidden || !record.renderHandle || !record.model) continue;
8475
+ record.model = blankedModel(record.model);
8476
+ record.renderHandle = this.renderer.mountIndicator(record.model);
8477
+ record.pendingStructural = true;
8478
+ this.setLoading(record, true);
8479
+ }
8480
+ }
8433
8481
  /** Stop + re-run every visible Pine indicator (its input bars changed wholesale). */
8434
8482
  reexecuteIndicators() {
8435
8483
  for (const record of this.registry.all()) {
@@ -8674,6 +8722,61 @@ var Vela = (function (exports) {
8674
8722
  record.session.update(record.inputValues, record.propValues);
8675
8723
  this.events.emit("indicator:inputs", { id });
8676
8724
  }
8725
+ /**
8726
+ * IndicatorController: replace a SCRIPT indicator's source in place (see
8727
+ * {@link IndicatorHandle.updateCode}). Prepare-first: the running session keeps
8728
+ * computing until the new source has compiled, so a broken edit costs the user
8729
+ * nothing but an `error` event. Natives have no script — warn and leave them alone.
8730
+ */
8731
+ updateCode(id, source) {
8732
+ const record = this.registry.get(id);
8733
+ const handle = this.handles.get(id);
8734
+ if (!record || !handle) return;
8735
+ if (record.native) {
8736
+ console.warn(`[vela] updateCode("${id}") \u2014 a native indicator has no script to update.`);
8737
+ return;
8738
+ }
8739
+ if (source === record.source && record.pendingSource === void 0) return;
8740
+ record.pendingSource = source;
8741
+ void this.swapSource(id, source, handle);
8742
+ }
8743
+ async swapSource(id, source, handle) {
8744
+ const engine = this.registry.get(id)?.engine ?? this.engineFor(this.registry.get(id)?.options?.language);
8745
+ let prepared;
8746
+ try {
8747
+ prepared = await engine.prepare(source, id);
8748
+ } catch (err) {
8749
+ const record2 = this.registry.get(id);
8750
+ if (record2?.pendingSource === source) {
8751
+ record2.pendingSource = void 0;
8752
+ this.fail(id, handle, err);
8753
+ }
8754
+ return;
8755
+ }
8756
+ const record = this.registry.get(id);
8757
+ if (!record || record.pendingSource !== source) return;
8758
+ record.pendingSource = void 0;
8759
+ record.source = source;
8760
+ record.engine = engine;
8761
+ record.prepared = prepared;
8762
+ handle.setSource(source);
8763
+ record.inputValues = valuesOnSchema(prepared.inputs, record.inputValues);
8764
+ record.propValues = valuesOnSchema(prepared.props ?? [], record.propValues);
8765
+ handle.setSchema(prepared.inputs);
8766
+ handle.setPropsSchema(prepared.props ?? []);
8767
+ record.session?.stop();
8768
+ record.session = void 0;
8769
+ record.pendingStructural = true;
8770
+ record.pendingCause = "code";
8771
+ if (record.hidden) return;
8772
+ if (record.renderHandle) {
8773
+ this.renderer.setIndicatorInputs(record.renderHandle, record.inputValues, record.propValues);
8774
+ this.setLoading(record, true);
8775
+ } else {
8776
+ this.mountLoadingPlaceholder(id, record);
8777
+ }
8778
+ this.executeIndicator(id, handle);
8779
+ }
8677
8780
  /** IndicatorController: tear down an indicator and (if now empty) its pane. */
8678
8781
  /** Live handles of every indicator on the chart (script + native), insertion order. */
8679
8782
  listIndicators() {
@@ -8688,7 +8791,7 @@ var Vela = (function (exports) {
8688
8791
  const record = this.registry.remove(id);
8689
8792
  if (record?.native?.type === "volume") this.volumeOptedOut = true;
8690
8793
  record?.session?.stop();
8691
- record?.native?.instance.stop();
8794
+ if (record?.native?.started) record.native.instance.stop();
8692
8795
  this.handles.delete(id);
8693
8796
  if (record?.renderHandle) this.renderer.removeIndicator(record.renderHandle);
8694
8797
  const paneId = record?.model?.paneId;
@@ -8714,7 +8817,7 @@ var Vela = (function (exports) {
8714
8817
  if (!visible) {
8715
8818
  record.session?.stop();
8716
8819
  record.session = void 0;
8717
- record.native?.instance.suspend();
8820
+ if (record.native?.started) record.native.instance.suspend();
8718
8821
  if (record.renderHandle) this.renderer.setIndicatorVisible?.(record.renderHandle, false);
8719
8822
  else if (record.native) this.mountHiddenNativeRow(id, record);
8720
8823
  } else {
@@ -8795,7 +8898,7 @@ var Vela = (function (exports) {
8795
8898
  this.paneActionUnsub?.();
8796
8899
  for (const record of this.registry.all()) {
8797
8900
  record.session?.stop();
8798
- record.native?.instance.stop();
8901
+ if (record.native?.started) record.native.instance.stop();
8799
8902
  }
8800
8903
  for (const engine of this.typeEngines.values()) engine.stop();
8801
8904
  this.typeEngines.clear();
@@ -8819,6 +8922,7 @@ var Vela = (function (exports) {
8819
8922
  const engine = this.engineFor(options.language);
8820
8923
  record.engine = engine;
8821
8924
  const prepared = await engine.prepare(source, id);
8925
+ if (record.source !== source) return;
8822
8926
  record.prepared = prepared;
8823
8927
  const defaults2 = {};
8824
8928
  for (const input of prepared.inputs) defaults2[input.key] = input.defval;
@@ -8889,7 +8993,9 @@ var Vela = (function (exports) {
8889
8993
  await this.readyPromise;
8890
8994
  const record = this.registry.get(id);
8891
8995
  if (!record?.native || record.hidden) return;
8996
+ const channel = this.nativeChannel(record);
8892
8997
  const ctx = {
8998
+ id,
8893
8999
  symbol: this.config.market.symbol ?? "TEST",
8894
9000
  timeframe: this.config.market.timeframe ?? "60",
8895
9001
  live: this.config.live,
@@ -8897,7 +9003,7 @@ var Vela = (function (exports) {
8897
9003
  bars: () => this.bars,
8898
9004
  data: this.dataControl,
8899
9005
  emit: (out) => this.applyModel(id, this.buildNativeModel(record, out)),
8900
- pushData: (data) => this.renderer.setNativeData?.(record.native.type, data),
9006
+ pushData: (data) => this.renderer.setNativeData?.(channel, data),
8901
9007
  setStatus: (status) => {
8902
9008
  if (record.renderHandle && !record.hidden) this.renderer.setIndicatorStatus?.(record.renderHandle, status);
8903
9009
  }
@@ -8908,16 +9014,26 @@ var Vela = (function (exports) {
8908
9014
  this.fail(id, handle, err);
8909
9015
  }
8910
9016
  }
9017
+ /** The renderer-layer channel a native instance's `pushData` lands on: the type itself
9018
+ * for a single-instance type (the layer id doubles as the channel), a per-instance
9019
+ * channel for a `multiInstance` type — otherwise every instance would overwrite the
9020
+ * one layer. Stamped on the model so the renderer mounts a dedicated layer for it. */
9021
+ nativeChannel(record) {
9022
+ const { type, descriptor } = record.native;
9023
+ return descriptor.multiInstance ? nativeInstanceChannel(type, record.id) : type;
9024
+ }
8911
9025
  /** Wrap a native indicator's emitted visuals into a full IndicatorModel, tagged `native`. */
8912
9026
  buildNativeModel(record, out) {
8913
9027
  const d = record.native.descriptor;
9028
+ const type = record.native.type;
9029
+ const channel = this.nativeChannel(record);
8914
9030
  return {
8915
9031
  id: record.id,
8916
9032
  title: record.title,
8917
9033
  ...d.shortTitle ? { shorttitle: d.shortTitle } : {},
8918
9034
  overlay: d.overlay,
8919
9035
  paneHint: d.paneHint,
8920
- native: { type: record.native.type },
9036
+ native: { type, ...channel !== type ? { channel } : {} },
8921
9037
  ...d.legend === false ? { legend: false } : {},
8922
9038
  ...out.paneAxis != null ? { paneAxis: out.paneAxis } : {},
8923
9039
  series: out.series ?? [],
@@ -9207,6 +9323,7 @@ var Vela = (function (exports) {
9207
9323
  applyModel(id, model) {
9208
9324
  const record = this.registry.get(id);
9209
9325
  if (!record || record.hidden) return true;
9326
+ if (this.switchingMarket) return false;
9210
9327
  if (record.loading && this.bars.length === 0 && !_EngineOrchestrator.modelHasOutput(model)) return false;
9211
9328
  const handle = this.handles.get(id);
9212
9329
  if (!record.renderHandle) {
@@ -9224,7 +9341,7 @@ var Vela = (function (exports) {
9224
9341
  if (record.loading && !record.paneLocked) {
9225
9342
  const routed = this.routePane(id, model, record.options ?? {});
9226
9343
  if (routed !== paneId) {
9227
- if (paneId !== "price") {
9344
+ if (paneId !== "price" && !this.registry.all().some((r) => r.id !== id && r.model?.paneId === paneId)) {
9228
9345
  this.renderer.removePane(paneId);
9229
9346
  this.forgetPane(paneId);
9230
9347
  }
@@ -9353,6 +9470,34 @@ var Vela = (function (exports) {
9353
9470
  }
9354
9471
  return new Promise((resolve) => setTimeout(resolve, 0));
9355
9472
  }
9473
+ function valuesOnSchema(schema, previous) {
9474
+ const out = {};
9475
+ const declared = /* @__PURE__ */ new Set();
9476
+ for (const s of schema) {
9477
+ out[s.key] = s.defval;
9478
+ declared.add(s.key);
9479
+ declared.add(s.title);
9480
+ }
9481
+ for (const [k, v] of Object.entries(previous)) if (declared.has(k)) out[k] = v;
9482
+ return out;
9483
+ }
9484
+ function blankedModel(model) {
9485
+ return {
9486
+ ...model,
9487
+ series: model.series.map((s) => s.kind === "candle" || s.kind === "bar" ? { ...s, bars: [] } : s.kind === "markers" ? { ...s, markers: [] } : { ...s, points: [] }),
9488
+ fills: [],
9489
+ backgrounds: [],
9490
+ priceLines: [],
9491
+ lines: [],
9492
+ boxes: [],
9493
+ labels: [],
9494
+ polylines: [],
9495
+ linefills: [],
9496
+ tables: [],
9497
+ barColors: [],
9498
+ trades: []
9499
+ };
9500
+ }
9356
9501
  function modelToValuePatch(model) {
9357
9502
  const series = [];
9358
9503
  let from = Number.POSITIVE_INFINITY;
@@ -12755,6 +12900,43 @@ var Vela = (function (exports) {
12755
12900
  host.appendChild(s);
12756
12901
  }
12757
12902
 
12903
+ // src/ui/tokens.ts
12904
+ var STATIC_ID = "vela-ui-tokens";
12905
+ var STATIC_DECLS = Object.entries(STATIC_TOKENS).map(([k, v]) => ` ${k}: ${v};`).join("\n");
12906
+ var STATIC_CSS = `
12907
+ .vela-ui, .vela-ui-layer {
12908
+ ${STATIC_DECLS}
12909
+ font-family: var(--vela-font, -apple-system, system-ui, sans-serif);
12910
+ box-sizing: border-box;
12911
+ /* Chrome text (titles, buttons, menus, readouts) is UI, not copy \u2014 never selectable. */
12912
+ user-select: none;
12913
+ -webkit-user-select: none;
12914
+ }
12915
+ .vela-ui *, .vela-ui-layer * { box-sizing: border-box; }
12916
+ /* Text ENTRY is the one exception: selection is part of editing. */
12917
+ .vela-ui :is(input, textarea), .vela-ui-layer :is(input, textarea) { user-select: text; -webkit-user-select: text; }
12918
+ /* iOS Safari zooms the page when a focused field is under 16px, and often
12919
+ keeps that zoom after the field blurs or a dialog closes. The shell's
12920
+ mobile size class is the honest gate \u2014 not a media query \u2014 so an embedded
12921
+ chart on a wide desktop still gets the rule when it is in the phone layout.
12922
+ Scoped to the kit root so a host page's own [data-layout] is never touched. */
12923
+ .vela-ui[data-layout='mobile'] :is(input, textarea, select) { font-size: 16px; }
12924
+ .vela-icon { display: inline-flex; align-items: center; flex: none; }
12925
+ .vela-icon svg { display: block; }
12926
+ `;
12927
+ function applyThemeTokens(el, t) {
12928
+ const tokens = themeTokens(t);
12929
+ for (const key in tokens) el.style.setProperty(key, tokens[key]);
12930
+ }
12931
+ function floatingLayerHost(from, fallback) {
12932
+ return from.closest(".vela-ui") ?? fallback ?? from;
12933
+ }
12934
+ function ensureUIHost(el, theme) {
12935
+ injectStyles(STATIC_ID, STATIC_CSS, el.getRootNode());
12936
+ el.classList.add("vela-ui");
12937
+ if (theme) applyThemeTokens(el, theme);
12938
+ }
12939
+
12758
12940
  // src/ui/icons.ts
12759
12941
  function iconEl(id, doc = document) {
12760
12942
  const span = doc.createElement("span");
@@ -17120,7 +17302,7 @@ var Vela = (function (exports) {
17120
17302
  /* The list is a <ul>: without this, block rows (the separator) paint a ::marker dot. */
17121
17303
  list-style: none;
17122
17304
  margin: 0;
17123
- background: var(--vela-surface-elev);
17305
+ background: var(--vela-surface);
17124
17306
  color: var(--vela-fg);
17125
17307
  border: 1px solid var(--vela-border-strong);
17126
17308
  border-radius: 6px;
@@ -17413,7 +17595,7 @@ var Vela = (function (exports) {
17413
17595
  const anchor = opts.trigger ?? opts.host ?? document.body;
17414
17596
  const doc = anchor.ownerDocument;
17415
17597
  injectStyles(MENU_STYLE_ID, MENU_CSS, doc);
17416
- const host = opts.host ?? anchor.closest?.(".vela-ui") ?? doc.body;
17598
+ const host = floatingLayerHost(opts.host ?? anchor, doc.body);
17417
17599
  this.root = new Surface(doc, {
17418
17600
  host,
17419
17601
  placement: opts.placement,
@@ -17452,40 +17634,6 @@ var Vela = (function (exports) {
17452
17634
  }
17453
17635
  };
17454
17636
 
17455
- // src/ui/tokens.ts
17456
- var STATIC_ID = "vela-ui-tokens";
17457
- var STATIC_DECLS = Object.entries(STATIC_TOKENS).map(([k, v]) => ` ${k}: ${v};`).join("\n");
17458
- var STATIC_CSS = `
17459
- .vela-ui, .vela-ui-layer {
17460
- ${STATIC_DECLS}
17461
- font-family: var(--vela-font, -apple-system, system-ui, sans-serif);
17462
- box-sizing: border-box;
17463
- /* Chrome text (titles, buttons, menus, readouts) is UI, not copy \u2014 never selectable. */
17464
- user-select: none;
17465
- -webkit-user-select: none;
17466
- }
17467
- .vela-ui *, .vela-ui-layer * { box-sizing: border-box; }
17468
- /* Text ENTRY is the one exception: selection is part of editing. */
17469
- .vela-ui :is(input, textarea), .vela-ui-layer :is(input, textarea) { user-select: text; -webkit-user-select: text; }
17470
- /* iOS Safari zooms the page when a focused field is under 16px, and often
17471
- keeps that zoom after the field blurs or a dialog closes. The shell's
17472
- mobile size class is the honest gate \u2014 not a media query \u2014 so an embedded
17473
- chart on a wide desktop still gets the rule when it is in the phone layout.
17474
- Scoped to the kit root so a host page's own [data-layout] is never touched. */
17475
- .vela-ui[data-layout='mobile'] :is(input, textarea, select) { font-size: 16px; }
17476
- .vela-icon { display: inline-flex; align-items: center; flex: none; }
17477
- .vela-icon svg { display: block; }
17478
- `;
17479
- function applyThemeTokens(el, t) {
17480
- const tokens = themeTokens(t);
17481
- for (const key in tokens) el.style.setProperty(key, tokens[key]);
17482
- }
17483
- function ensureUIHost(el, theme) {
17484
- injectStyles(STATIC_ID, STATIC_CSS, el.getRootNode());
17485
- el.classList.add("vela-ui");
17486
- if (theme) applyThemeTokens(el, theme);
17487
- }
17488
-
17489
17637
  // src/ui/components/popover/controller.ts
17490
17638
  function insetRect(r, inset) {
17491
17639
  return {
@@ -17592,7 +17740,8 @@ ${STATIC_DECLS}
17592
17740
  const doc = opts.trigger.ownerDocument;
17593
17741
  injectStyles(POPOVER_STYLE_ID, POPOVER_CSS, doc);
17594
17742
  this.trigger = opts.trigger;
17595
- this.host = opts.host ?? doc.body;
17743
+ this.host = opts.host ?? floatingLayerHost(opts.trigger, doc.body);
17744
+ this.inheritsTokens = !opts.host && this.host !== doc.body;
17596
17745
  this.ctrl = popoverController(opts);
17597
17746
  this.boundary = opts.boundary ?? "viewport";
17598
17747
  this.theme = opts.theme;
@@ -17624,7 +17773,7 @@ ${STATIC_DECLS}
17624
17773
  clearTimeout(this.leaveTimer);
17625
17774
  this.leaveTimer = null;
17626
17775
  }
17627
- ensureUIHost(this.el, this.theme);
17776
+ ensureUIHost(this.el, this.inheritsTokens ? void 0 : this.theme);
17628
17777
  if (this.fadeMs > 0) {
17629
17778
  this.el.style.transition = `opacity ${this.fadeMs}ms ease`;
17630
17779
  this.el.style.opacity = "0";
@@ -23119,33 +23268,32 @@ ${overlayScrollbarCss(".vela-dialog.vela-ind-dialog *", 9)}
23119
23268
  center: (wickLeftDev + wickDev / 2) / dpr
23120
23269
  };
23121
23270
  }
23122
- function aggregateCandleColumns(bars, i0, i1, xOf, yOf) {
23271
+ function aggregateCandleColumns(bars, i0, i1, xOf, yOf, colorOf2) {
23123
23272
  const out = [];
23124
23273
  let col = NaN;
23125
- let intervals = [];
23274
+ let runs = [];
23126
23275
  const flush = () => {
23127
- if (intervals.length === 0) return;
23128
- const merged = [intervals[0]];
23129
- for (let k = 1; k < intervals.length; k += 1) {
23130
- const prev3 = merged[merged.length - 1];
23131
- const next2 = intervals[k];
23132
- if (Math.abs(yOf(prev3.hi) - yOf(next2.lo)) < 1) {
23133
- prev3.hi = next2.hi;
23134
- if (next2.headIdx < prev3.headIdx) {
23135
- prev3.headIdx = next2.headIdx;
23136
- prev3.headTime = next2.headTime;
23137
- prev3.open = next2.open;
23138
- }
23139
- if (next2.lastIdx > prev3.lastIdx) {
23140
- prev3.lastIdx = next2.lastIdx;
23141
- prev3.close = next2.close;
23276
+ if (runs.length === 0) return;
23277
+ runs.sort((a, b) => a.lo - b.lo);
23278
+ const merged = [];
23279
+ for (const next2 of runs) {
23280
+ let prev3;
23281
+ for (let k = merged.length - 1; k >= 0; k -= 1) {
23282
+ if (merged[k].color === next2.color) {
23283
+ prev3 = merged[k];
23284
+ break;
23142
23285
  }
23286
+ }
23287
+ if (prev3 && Math.abs(yOf(prev3.hi) - yOf(next2.lo)) < 1) {
23288
+ prev3.hi = next2.hi;
23289
+ if (next2.lastIdx > prev3.lastIdx) prev3.lastIdx = next2.lastIdx;
23143
23290
  } else {
23144
23291
  merged.push(next2);
23145
23292
  }
23146
23293
  }
23147
- for (const iv of merged) out.push({ x: col, hi: iv.hi, lo: iv.lo, headTime: iv.headTime, open: iv.open, close: iv.close });
23148
- intervals = [];
23294
+ merged.sort((a, b) => a.lastIdx - b.lastIdx);
23295
+ for (const r of merged) out.push({ x: col, hi: r.hi, lo: r.lo, color: r.color });
23296
+ runs = [];
23149
23297
  };
23150
23298
  for (let i = i0; i <= i1; i += 1) {
23151
23299
  const b = bars[i];
@@ -23155,26 +23303,17 @@ ${overlayScrollbarCss(".vela-dialog.vela-ind-dialog *", 9)}
23155
23303
  flush();
23156
23304
  col = x;
23157
23305
  }
23306
+ const color = colorOf2(b);
23158
23307
  let lo = b.low;
23159
23308
  let hi = b.high;
23160
- let headIdx = i;
23161
- let headTime = b.time;
23162
- let open2 = b.open;
23163
- for (let k = intervals.length - 1; k >= 0; k -= 1) {
23164
- const iv = intervals[k];
23165
- if (iv.lo > hi || iv.hi < lo) continue;
23166
- if (iv.lo < lo) lo = iv.lo;
23167
- if (iv.hi > hi) hi = iv.hi;
23168
- if (iv.headIdx < headIdx) {
23169
- headIdx = iv.headIdx;
23170
- headTime = iv.headTime;
23171
- open2 = iv.open;
23172
- }
23173
- intervals.splice(k, 1);
23174
- }
23175
- let insertAt = 0;
23176
- while (insertAt < intervals.length && intervals[insertAt].lo < lo) insertAt += 1;
23177
- intervals.splice(insertAt, 0, { lo, hi, headIdx, headTime, open: open2, lastIdx: i, close: b.close });
23309
+ for (let k = runs.length - 1; k >= 0; k -= 1) {
23310
+ const r = runs[k];
23311
+ if (r.color !== color || r.lo > hi || r.hi < lo) continue;
23312
+ if (r.lo < lo) lo = r.lo;
23313
+ if (r.hi > hi) hi = r.hi;
23314
+ runs.splice(k, 1);
23315
+ }
23316
+ runs.push({ lo, hi, color, lastIdx: i });
23178
23317
  }
23179
23318
  flush();
23180
23319
  return out;
@@ -24149,7 +24288,7 @@ void main() {
24149
24288
  * Routing mirrors the main pass: own series per pane, force_overlay series on the price pane. */
24150
24289
  emitGlowSources(b, scene, pane, coords, i0, i1) {
24151
24290
  const emitOne = (s, off) => {
24152
- if (!isLineLikeSeries(s) || s.visible === false) return;
24291
+ if (!isLineLikeSeries(s) || !seriesShownOn(s, "pane")) return;
24153
24292
  if (s.kind === "histogram" || s.kind === "columns") return;
24154
24293
  if (s.kind === "circles" || s.kind === "cross") this.emitPointMarkers(b, s, pane, coords, i0, i1, off);
24155
24294
  else this.emitPolyline(b, s, pane, coords, i0, i1, s.kind === "step", off);
@@ -24260,7 +24399,7 @@ void main() {
24260
24399
  const spacing = coords.bodySpacing();
24261
24400
  const tier = candleTier(spacing);
24262
24401
  if (tier === "aggregate") {
24263
- this.emitCandlesAggregated(b, bars, i0, i1, coords, pane, up, down, barColors);
24402
+ this.emitCandlesAggregated(b, bars, i0, i1, coords, pane, (bar) => barColors.get(bar.time) ?? (bar.close >= bar.open ? up : down));
24264
24403
  return;
24265
24404
  }
24266
24405
  const tickW = Math.max(1, Math.round(spacing * 0.35));
@@ -24401,14 +24540,17 @@ void main() {
24401
24540
  const spacing = coords.bodySpacing();
24402
24541
  const tier = candleTier(spacing);
24403
24542
  const paint = effectiveCandlePaint(scene.style.candle, scene.candleOverride, theme.upColor, theme.downColor);
24543
+ const up = paint.up;
24544
+ const down = paint.down;
24545
+ const cs = paint.candle;
24404
24546
  if (tier === "aggregate") {
24405
- this.emitCandlesAggregated(b, bars, i0, i1, coords, pane, paint.up, paint.down, barColors);
24547
+ this.emitCandlesAggregated(b, bars, i0, i1, coords, pane, (bar) => {
24548
+ const isUp = bar.close >= bar.open;
24549
+ return (isUp ? cs.wickUpColor : cs.wickDownColor) ?? barColors.get(bar.time) ?? (isUp ? up : down);
24550
+ });
24406
24551
  return;
24407
24552
  }
24408
24553
  const drawBody = tier === "full";
24409
- const up = paint.up;
24410
- const down = paint.down;
24411
- const cs = paint.candle;
24412
24554
  const fading = this.candleStructureAlpha > this.candleBodyAlpha + 1e-3;
24413
24555
  for (let i = i0; i <= i1; i += 1) {
24414
24556
  const bar = bars[i];
@@ -24457,24 +24599,26 @@ void main() {
24457
24599
  /**
24458
24600
  * Sub-pixel LOD (mirrors Canvas2dBackend.drawCandlesAggregated): bars sharing a
24459
24601
  * rounded pixel column collapse into high-low sticks — one per contiguous
24460
- * coverage run (see {@link aggregateCandleColumns}), so a price gap inside the
24461
- * column stays a void. Draw cost stays bounded by screen width, not bar count;
24462
- * stick color follows its own first-open→last-close.
24602
+ * SAME-COLOR coverage run (see {@link aggregateCandleColumns}), so a price gap
24603
+ * inside the column stays a void and every bar's extent keeps its own color. Draw
24604
+ * cost stays bounded by screen width, not bar count. `colorOf` is the calling
24605
+ * style's per-bar paint rule (candles and OHLC bars resolve theirs differently).
24463
24606
  */
24464
- emitCandlesAggregated(b, bars, i0, i1, coords, pane, up, down, barColors) {
24607
+ emitCandlesAggregated(b, bars, i0, i1, coords, pane, colorOf2) {
24465
24608
  const yOf = (price) => coords.priceToY(price, pane.scale, pane.bounds);
24466
- for (const s of aggregateCandleColumns(bars, i0, i1, (i) => coords.logicalToX(i), yOf)) {
24467
- const c = parseColor(barColors.get(s.headTime) ?? (s.close >= s.open ? up : down));
24609
+ for (const s of aggregateCandleColumns(bars, i0, i1, (i) => coords.logicalToX(i), yOf, colorOf2)) {
24610
+ const c = parseColor(s.color);
24468
24611
  const hY = yOf(s.hi);
24469
24612
  b.rect(s.x - 0.5, hY, 1, yOf(s.lo) - hY, c);
24470
24613
  }
24471
24614
  }
24472
24615
  emitSeries(b, spec, pane, coords, i0, i1, theme, off = 0) {
24616
+ if (!seriesShownOn(spec, "pane")) return;
24473
24617
  if (spec.kind === "candle" || spec.kind === "bar") {
24474
24618
  this.emitPlotCandles(b, spec, pane, coords, i0, i1, theme, off);
24475
24619
  return;
24476
24620
  }
24477
- if (!isLineLikeSeries(spec) || spec.visible === false) return;
24621
+ if (!isLineLikeSeries(spec)) return;
24478
24622
  switch (spec.kind) {
24479
24623
  case "histogram":
24480
24624
  case "columns":
@@ -26138,7 +26282,7 @@ void main() {
26138
26282
  const spacing = coords.bodySpacing();
26139
26283
  const tier = candleTier(spacing);
26140
26284
  if (tier === "aggregate") {
26141
- this.drawCandlesAggregated(ctx, bars, i0, i1, coords, pane, up, down, barColors);
26285
+ this.drawCandlesAggregated(ctx, bars, i0, i1, coords, pane, (b) => barColors.get(b.time) ?? (b.close >= b.open ? up : down));
26142
26286
  return;
26143
26287
  }
26144
26288
  const tickW = Math.max(1, Math.round(spacing * 0.35));
@@ -26314,12 +26458,15 @@ void main() {
26314
26458
  const spacing = coords.bodySpacing();
26315
26459
  const tier = candleTier(spacing);
26316
26460
  const paint = effectiveCandlePaint(scene.style.candle, scene.candleOverride, theme.upColor, theme.downColor);
26461
+ const cs = paint.candle;
26317
26462
  if (tier === "aggregate") {
26318
- this.drawCandlesAggregated(ctx, bars, i0, i1, coords, pane, paint.up, paint.down, barColors);
26463
+ this.drawCandlesAggregated(ctx, bars, i0, i1, coords, pane, (b) => {
26464
+ const up = b.close >= b.open;
26465
+ return (up ? cs.wickUpColor : cs.wickDownColor) ?? barColors.get(b.time) ?? (up ? paint.up : paint.down);
26466
+ });
26319
26467
  return;
26320
26468
  }
26321
26469
  const drawBody = tier === "full";
26322
- const cs = paint.candle;
26323
26470
  const fading = this.candleStructureAlpha > this.candleBodyAlpha + 1e-3;
26324
26471
  for (let i = i0; i <= i1; i += 1) {
26325
26472
  const b = bars[i];
@@ -26376,17 +26523,18 @@ void main() {
26376
26523
  }
26377
26524
  /**
26378
26525
  * Sub-pixel LOD: bars sharing a rounded pixel column collapse into high-low
26379
- * sticks (one per contiguous coverage run — see {@link aggregateCandleColumns}),
26380
- * so draw cost is bounded by screen width (not bar count) when zoomed far out
26381
- * and a price gap inside the column stays a void. Each stick's color follows
26382
- * its first-open→last-close direction (a barcolor() on the head bar still wins).
26526
+ * sticks (one per contiguous SAME-COLOR coverage run — see
26527
+ * {@link aggregateCandleColumns}), so draw cost is bounded by screen width (not
26528
+ * bar count) when zoomed far out, a price gap inside the column stays a void, and
26529
+ * every bar's extent keeps its own color. `colorOf` is the calling style's per-bar
26530
+ * paint rule (candles and OHLC bars resolve theirs differently).
26383
26531
  */
26384
- drawCandlesAggregated(ctx, bars, i0, i1, coords, pane, up, down, barColors) {
26532
+ drawCandlesAggregated(ctx, bars, i0, i1, coords, pane, colorOf2) {
26385
26533
  const yOf = (price) => coords.priceToY(price, pane.scale, pane.bounds);
26386
26534
  ctx.lineWidth = 1;
26387
- for (const s of aggregateCandleColumns(bars, i0, i1, (i) => coords.logicalToX(i), yOf)) {
26535
+ for (const s of aggregateCandleColumns(bars, i0, i1, (i) => coords.logicalToX(i), yOf, colorOf2)) {
26388
26536
  const x = s.x + 0.5;
26389
- ctx.strokeStyle = barColors.get(s.headTime) ?? (s.close >= s.open ? up : down);
26537
+ ctx.strokeStyle = s.color;
26390
26538
  ctx.beginPath();
26391
26539
  ctx.moveTo(x, yOf(s.hi));
26392
26540
  ctx.lineTo(x, yOf(s.lo));
@@ -26394,11 +26542,12 @@ void main() {
26394
26542
  }
26395
26543
  }
26396
26544
  drawSeries(ctx, spec, pane, coords, i0, i1, theme, off = 0) {
26545
+ if (!seriesShownOn(spec, "pane")) return;
26397
26546
  if (spec.kind === "candle" || spec.kind === "bar") {
26398
26547
  this.drawPlotCandles(ctx, spec, pane, coords, i0, i1, theme, off);
26399
26548
  return;
26400
26549
  }
26401
- if (!isLineLikeSeries(spec) || spec.visible === false) return;
26550
+ if (!isLineLikeSeries(spec)) return;
26402
26551
  switch (spec.kind) {
26403
26552
  case "histogram":
26404
26553
  case "columns":
@@ -35346,6 +35495,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
35346
35495
  return { min: min2 - span * MARGIN_BOTTOM, max: max2 + span * MARGIN_TOP };
35347
35496
  }
35348
35497
  function considerSeries(s, i0, i1, off, consider) {
35498
+ if (!seriesInScale(s)) return;
35349
35499
  if (s.kind === "candle" || s.kind === "bar") {
35350
35500
  for (let i = i0; i <= i1; i += 1) {
35351
35501
  const b = s.bars[i - off];
@@ -35565,6 +35715,22 @@ ${overlayScrollbarCss(".vela-sd-pane")}
35565
35715
  this.openKey = null;
35566
35716
  /** Bumped per open/close — a lazy content resolving after its popup went away is dropped. */
35567
35717
  this.generation = 0;
35718
+ /**
35719
+ * The cluster whose popup was open when the current pointer press began. The anchor is
35720
+ * hit-transparent (the glyph is canvas pixels), so the shell's outside-dismiss closes the
35721
+ * popup on pointerdown; the click the renderer derives from the same pointerup must then
35722
+ * read as "close", not reopen that cluster. Captured on the document ahead of the shell's
35723
+ * (deferred) listener, cleared once the release has dispatched.
35724
+ */
35725
+ this.pressKey = null;
35726
+ this.onPress = () => {
35727
+ if (this.openKey === null) return;
35728
+ this.pressKey = this.openKey;
35729
+ const doc = this.deps.plot.ownerDocument;
35730
+ const clear = () => void setTimeout(() => this.pressKey = null, 0);
35731
+ doc.addEventListener("pointerup", clear, { capture: true, once: true });
35732
+ doc.addEventListener("pointercancel", clear, { capture: true, once: true });
35733
+ };
35568
35734
  const doc = deps.plot.ownerDocument;
35569
35735
  injectStyles(CALLOUT_STYLE_ID, CALLOUT_CSS, doc);
35570
35736
  injectStyles(MARKS_STYLE_ID, MARKS_CSS, doc);
@@ -35572,12 +35738,14 @@ ${overlayScrollbarCss(".vela-sd-pane")}
35572
35738
  this.anchor.className = "vela-marks-anchor";
35573
35739
  Object.assign(this.anchor.style, { position: "absolute", pointerEvents: "none", left: "0", top: "0", width: "0", height: "0" });
35574
35740
  deps.plot.appendChild(this.anchor);
35741
+ doc.addEventListener("pointerdown", this.onPress, true);
35575
35742
  }
35576
35743
  /** The cluster key the open popup belongs to, or null. */
35577
35744
  get key() {
35578
35745
  return this.openKey;
35579
35746
  }
35580
35747
  open(cluster, rect) {
35748
+ if (this.pressKey === cluster.key) return;
35581
35749
  this.close();
35582
35750
  this.place(rect);
35583
35751
  const gen = ++this.generation;
@@ -35625,6 +35793,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
35625
35793
  }
35626
35794
  destroy() {
35627
35795
  this.close();
35796
+ this.deps.plot.ownerDocument.removeEventListener("pointerdown", this.onPress, true);
35628
35797
  this.anchor.remove();
35629
35798
  }
35630
35799
  place(rect) {
@@ -36521,6 +36690,13 @@ ${overlayScrollbarCss(".vela-sd-pane")}
36521
36690
  this.backdropRenderer = new BackdropRenderer();
36522
36691
  this.volumeRenderer = new VolumeRenderer();
36523
36692
  /** SDK renderer layers instantiated at mount ({@link registerRendererLayer}). */
36693
+ /**
36694
+ * The mounted SDK layers. One BASE entry per registered definition (channel = the layer
36695
+ * id, no owner of its own), plus one DEDICATED entry per mounted native indicator whose
36696
+ * model names its own channel (`native.channel` — an instance of a multi-instance type):
36697
+ * same definition, its own canvas, reading that instance's channel and following that
36698
+ * instance's pane and z key. `channel` is unique across the array.
36699
+ */
36524
36700
  this.extLayers = [];
36525
36701
  /** Last applied layer-canvas order (ids below + above the data canvas) — re-slotted only on change. */
36526
36702
  this.layerOrderSig = "";
@@ -37666,11 +37842,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
37666
37842
  Object.assign(this.overlayRoot.style, { position: "absolute", inset: "0", pointerEvents: "none" });
37667
37843
  this.plot = document.createElement("div");
37668
37844
  Object.assign(this.plot.style, { position: "absolute", top: "0", right: "0", bottom: "0", left: "0" });
37669
- this.extLayers = rendererLayers().map((def) => {
37670
- const canvas = document.createElement("canvas");
37671
- Object.assign(canvas.style, { position: "absolute", inset: "0", width: "100%", height: "100%", pointerEvents: "none" });
37672
- return { def, instance: def.create(), canvas };
37673
- });
37845
+ this.extLayers = rendererLayers().map((def) => ({ def, instance: def.create(), canvas: this.createLayerCanvas(), channel: def.id, owner: null }));
37674
37846
  const below = this.extLayers.filter((l) => l.def.placement === "below-data").map((l) => l.canvas);
37675
37847
  const above = this.extLayers.filter((l) => l.def.placement !== "below-data").map((l) => l.canvas);
37676
37848
  this.plot.append(this.backdropCanvas, ...below, this.dataCanvas, this.volumeCanvas, this.vpvrCanvas, ...above, this.chromeCanvas, this.drawingsCanvas, this.cursorCanvas, this.overlayRoot);
@@ -37687,6 +37859,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
37687
37859
  this.volumeRenderer.mount(this.volumeCanvas);
37688
37860
  this.vpvrRenderer.mount(this.vpvrCanvas);
37689
37861
  for (const l of this.extLayers) l.instance.mount(l.canvas);
37862
+ for (const m of this.scene.indicators.values()) this.ensureInstanceLayer(m);
37690
37863
  this.chrome.mount(this.chromeCanvas);
37691
37864
  this.crosshairLayer.mount(this.cursorCanvas);
37692
37865
  this.coords.setViewport(defaultViewport());
@@ -38279,6 +38452,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
38279
38452
  this.refreshAnchorOffset(model);
38280
38453
  if (model.native && this.extLayers.some((l) => l.def.id === model.native.type)) this.scene.assignIndicatorZTop(model.id);
38281
38454
  else this.scene.assignIndicatorZ(model.id);
38455
+ this.ensureInstanceLayer(model);
38282
38456
  if (model.legend !== false) {
38283
38457
  this.inputsUI.upsert(model.id, model.shorttitle ?? model.title, model.inputs, model.inputValues, model.paneId, {
38284
38458
  native: !!model.native,
@@ -38322,6 +38496,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
38322
38496
  this.scene.vpvrLayer = null;
38323
38497
  }
38324
38498
  this.scene.indicators.delete(handle.id);
38499
+ this.dropInstanceLayer(handle.id);
38325
38500
  this.scene.forgetIndicatorZ(handle.id);
38326
38501
  this.scene.forgetAnchorOffset(handle.id);
38327
38502
  this.scene.dropIndicatorScale(handle.id);
@@ -39115,29 +39290,31 @@ ${overlayScrollbarCss(".vela-sd-pane")}
39115
39290
  up: bar.close >= bar.open
39116
39291
  };
39117
39292
  }
39118
- /** One group per indicator (name = indicator title), each with a row per drawable plot. */
39293
+ /** One group per indicator (name = indicator title), each with a row per plot shown in the data window. */
39119
39294
  dataWindowGroups(idx, pricePane) {
39120
39295
  const groups = [];
39121
39296
  for (const model of this.scene.indicators.values()) {
39122
- const rows = this.dataWindowRowsFor(model, idx, pricePane);
39297
+ const rows = this.readoutRowsFor(model, idx, pricePane, "dataWindow");
39123
39298
  if (rows.length) groups.push({ name: model.title, rows });
39124
39299
  }
39125
39300
  return groups;
39126
39301
  }
39127
- /** One indicator's readout at bar `idx`: a row per drawable plot, formatted on its pane's scale. */
39128
- dataWindowRowsFor(model, idx, pricePane) {
39302
+ /** One indicator's readout at bar `idx` for one value surface (the legend or the data window):
39303
+ * a row per plot shown on that surface, formatted on its pane's scale. A plot's `display`
39304
+ * decides per surface — a data-window-only plot has a row here and none in the legend. */
39305
+ readoutRowsFor(model, idx, pricePane, surface) {
39129
39306
  if (model.native?.type === "volume") return this.volumeReadoutRows(model, idx);
39130
39307
  const pane = (model.paneId ? this.scene.panes.get(model.paneId) : null) ?? pricePane;
39131
39308
  const off = this.scene.offsetOf(model.id);
39132
39309
  const rows = [];
39133
39310
  for (const s of model.series) {
39311
+ if (!seriesShownOn(s, surface)) continue;
39134
39312
  let value;
39135
39313
  let color;
39136
39314
  if (s.kind === "candle" || s.kind === "bar") {
39137
39315
  value = s.bars[idx - off]?.close;
39138
39316
  color = s.style?.up ?? this.theme.upColor;
39139
39317
  } else if (isLineLikeSeries(s)) {
39140
- if (s.visible === false) continue;
39141
39318
  value = s.points[idx - off]?.value;
39142
39319
  color = s.points[idx - off]?.color ?? s.style.color;
39143
39320
  } else {
@@ -39160,9 +39337,10 @@ ${overlayScrollbarCss(".vela-sd-pane")}
39160
39337
  const color = bar.close >= bar.open ? cfg?.upColor ?? this.theme.upColor : cfg?.downColor ?? this.theme.downColor;
39161
39338
  return [{ label: model.title, value: formatVolume(vol), color }];
39162
39339
  }
39163
- /** Refresh the plot values beside every legend title — the same readout the data window
39164
- * shows (crosshair bar, else the latest bar), pushed per paint. A hidden indicator has
39165
- * no scene model, so its row is absent from the map and its readout clears. */
39340
+ /** Refresh the plot values beside every legend title — the same bar the data window reads
39341
+ * (crosshair bar, else the latest bar), pushed per paint, filtered to the plots whose
39342
+ * `display` includes the legend. A hidden indicator has no scene model, so its row is
39343
+ * absent from the map and its readout clears. */
39166
39344
  updateLegendValues() {
39167
39345
  if (!this.inputsUI) return;
39168
39346
  const n = this.bars.length;
@@ -39171,7 +39349,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
39171
39349
  const idx = this.hoverLogical != null ? this.hoverLogical : n - 1;
39172
39350
  const pricePane = this.dataWindowPricePane();
39173
39351
  for (const model of this.scene.indicators.values()) {
39174
- values.set(model.id, this.dataWindowRowsFor(model, idx, pricePane).map((r) => ({ value: r.value, color: r.color })));
39352
+ values.set(model.id, this.readoutRowsFor(model, idx, pricePane, "legend").map((r) => ({ value: r.value, color: r.color })));
39175
39353
  }
39176
39354
  }
39177
39355
  this.inputsUI.setPlotValues(values);
@@ -39197,9 +39375,9 @@ ${overlayScrollbarCss(".vela-sd-pane")}
39197
39375
  const nowMs = typeof performance !== "undefined" ? performance.now() : Date.now();
39198
39376
  for (const l of this.extLayers) {
39199
39377
  if (!l.def.repaintOnCursor) continue;
39200
- const lp = this.layerPane(l.def.id) ?? pane;
39378
+ const lp = this.layerPane(l) ?? pane;
39201
39379
  if (lp.collapsed) continue;
39202
- l.instance.render(this.extLayerArgs(l.def.id, lp.scale, lp.bounds, nowMs));
39380
+ l.instance.render(this.extLayerArgs(l, lp.scale, lp.bounds, nowMs));
39203
39381
  if (this.animZoom.on && l.instance.animating?.()) this.animator.start();
39204
39382
  }
39205
39383
  }
@@ -39208,13 +39386,15 @@ ${overlayScrollbarCss(".vela-sd-pane")}
39208
39386
  if (canvas.width === 0 || canvas.height === 0) return;
39209
39387
  canvas.getContext("2d")?.clearRect(0, 0, canvas.width, canvas.height);
39210
39388
  }
39211
- /** One frame's args for an SDK renderer layer (shared by the data + cursor paint paths). */
39212
- extLayerArgs(id, scale, bounds, nowMs) {
39389
+ /** One frame's args for an SDK renderer layer (shared by the data + cursor paint paths).
39390
+ * Data + pending come from the entry's channel (per instance for a dedicated entry);
39391
+ * the settings bag is the TYPE's — a chart type's dialog values, one bag per type. */
39392
+ extLayerArgs(l, scale, bounds, nowMs) {
39213
39393
  return {
39214
39394
  bars: this.scene.bars,
39215
- data: this.scene.nativeData.get(id),
39216
- settings: this.scene.nativeData.get(`${id}-settings`) ?? {},
39217
- pending: this.scene.nativePending.get(id) ?? [],
39395
+ data: this.scene.nativeData.get(l.channel),
39396
+ settings: this.scene.nativeData.get(`${l.def.id}-settings`) ?? {},
39397
+ pending: this.scene.nativePending.get(l.channel) ?? [],
39218
39398
  coords: this.coords,
39219
39399
  scale,
39220
39400
  bounds,
@@ -39257,12 +39437,12 @@ ${overlayScrollbarCss(".vela-sd-pane")}
39257
39437
  const nowMs = typeof performance !== "undefined" ? performance.now() : Date.now();
39258
39438
  let folded = null;
39259
39439
  for (const l of this.extLayers) {
39260
- const lp = this.layerPane(l.def.id) ?? pane;
39440
+ const lp = this.layerPane(l) ?? pane;
39261
39441
  if (lp.collapsed) {
39262
39442
  this.clearLayerCanvas(l.canvas);
39263
39443
  continue;
39264
39444
  }
39265
- const args = this.extLayerArgs(l.def.id, lp.scale, lp.bounds, nowMs);
39445
+ const args = this.extLayerArgs(l, lp.scale, lp.bounds, nowMs);
39266
39446
  l.instance.render(args);
39267
39447
  if (lp === pane) folded = foldBaseModulation(folded, l.instance.modulateBase?.(args) ?? null);
39268
39448
  if (this.animZoom.on && l.instance.animating?.()) this.animator.start();
@@ -39426,7 +39606,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
39426
39606
  const or2 = overlaySeriesRange(this.scene.indicators.values(), i0, i1, (id) => this.scene.offsetOf(id));
39427
39607
  if (or2) dr = dr ? { min: Math.min(dr.min, or2.min), max: Math.max(dr.max, or2.max) } : or2;
39428
39608
  }
39429
- const includeCandles = pane.kind === "price" && !this.scene.candlesHidden;
39609
+ const includeCandles = pane.kind === "price" && (!this.scene.candlesHidden || this.priceLayersAnchoredToBars(masterModels));
39430
39610
  pane.scaleTarget = computePaneScale(masterModels, this.bars, includeCandles, i0, i1, dr, paneLogScale(this.scene, pane), (id) => this.scene.offsetOf(id));
39431
39611
  pane.percentBaseline = pane.kind === "price" ? this.bars[i0]?.close ?? 0 : this.firstVisibleValue(masterModels, i0);
39432
39612
  pane.axisFormat = void 0;
@@ -39545,19 +39725,60 @@ ${overlayScrollbarCss(".vela-sd-pane")}
39545
39725
  }
39546
39726
  return null;
39547
39727
  }
39548
- /** The mounted native indicator that OWNS an SDK layer the one whose type equals the
39549
- * layer id (the id doubles as the data channel, so the pairing is the SDK's own
39550
- * contract). Null for chart-type channels and while the owner is hidden (a hidden
39551
- * indicator leaves the scene; its cleared data channel paints nothing anyway). */
39552
- layerOwner(layerId) {
39728
+ /** One transparent, pointer-transparent SDK layer canvas covering the plot. */
39729
+ createLayerCanvas() {
39730
+ const canvas = document.createElement("canvas");
39731
+ Object.assign(canvas.style, { position: "absolute", inset: "0", width: "100%", height: "100%", pointerEvents: "none" });
39732
+ return canvas;
39733
+ }
39734
+ /**
39735
+ * Mount the DEDICATED layer of a native indicator that names its own channel (an
39736
+ * instance of a multi-instance type painting through a registered layer): a fresh
39737
+ * instance of the layer's definition on its own canvas, reading that channel. Idempotent
39738
+ * — a show after hide re-mounts the model, and the layer (kept across the hide, like the
39739
+ * z key) is simply reused. Skipped while the renderer itself is unmounted (`mount()`
39740
+ * catches up from the scene).
39741
+ */
39742
+ ensureInstanceLayer(model) {
39743
+ const channel = model.native?.channel;
39744
+ if (!channel || !this.plot) return;
39745
+ if (this.extLayers.some((l) => l.owner === model.id)) return;
39746
+ const base = this.extLayers.find((l) => l.owner === null && l.def.id === model.native.type);
39747
+ if (!base) return;
39748
+ const canvas = this.createLayerCanvas();
39749
+ canvas.width = this.dataCanvas.width;
39750
+ canvas.height = this.dataCanvas.height;
39751
+ canvas.style.width = this.dataCanvas.style.width || "100%";
39752
+ canvas.style.height = this.dataCanvas.style.height || "100%";
39753
+ const entry = { def: base.def, instance: base.def.create(), canvas, channel, owner: model.id };
39754
+ this.extLayers.push(entry);
39755
+ this.plot.insertBefore(canvas, this.chromeCanvas);
39756
+ entry.instance.mount(canvas);
39757
+ }
39758
+ /** Tear down the dedicated layer of a removed indicator (a hide keeps it). */
39759
+ dropInstanceLayer(indicatorId) {
39760
+ const i = this.extLayers.findIndex((l2) => l2.owner === indicatorId);
39761
+ if (i < 0) return;
39762
+ const [l] = this.extLayers.splice(i, 1);
39763
+ l.instance.destroy?.();
39764
+ l.canvas.remove();
39765
+ }
39766
+ /** The mounted native indicator that OWNS an SDK layer entry. A dedicated entry is
39767
+ * owned by the instance it was mounted for; the base entry by the mounted indicator
39768
+ * whose type equals the layer id and that has no channel of its own (the id doubles
39769
+ * as the data channel, so the pairing is the SDK's own contract). Null for chart-type
39770
+ * channels and while the owner is hidden (a hidden indicator leaves the scene; its
39771
+ * cleared data channel paints nothing anyway). */
39772
+ layerOwner(l) {
39773
+ if (l.owner !== null) return this.scene.indicators.get(l.owner) ?? null;
39553
39774
  for (const m of this.scene.indicators.values()) {
39554
- if (m.native?.type === layerId) return m;
39775
+ if (m.native?.type === l.def.id && !m.native.channel) return m;
39555
39776
  }
39556
39777
  return null;
39557
39778
  }
39558
39779
  /** The pane an SDK layer paints on: its owner's pane, else the price pane. */
39559
- layerPane(layerId) {
39560
- const owner = this.layerOwner(layerId);
39780
+ layerPane(l) {
39781
+ const owner = this.layerOwner(l);
39561
39782
  const paneId = owner ? owner.paneId ?? PRICE_PANE_ID : PRICE_PANE_ID;
39562
39783
  return this.scene.panes.get(paneId) ?? null;
39563
39784
  }
@@ -39565,12 +39786,12 @@ ${overlayScrollbarCss(".vela-sd-pane")}
39565
39786
  * owned layers by their owner's z key against the candles' (an indicator restacked
39566
39787
  * below the candles takes its layer canvas along), unowned by declared placement. */
39567
39788
  orderedLayerCanvases() {
39568
- const byId = new Map(this.extLayers.map((l) => [l.def.id, l.canvas]));
39789
+ const byId = new Map(this.extLayers.map((l) => [l.channel, l.canvas]));
39569
39790
  const { below, above } = stackLayers(
39570
39791
  this.extLayers.map((l) => {
39571
- const owner = this.layerOwner(l.def.id);
39792
+ const owner = this.layerOwner(l);
39572
39793
  return {
39573
- id: l.def.id,
39794
+ id: l.channel,
39574
39795
  placement: l.def.placement === "below-data" ? "below-data" : "above-data",
39575
39796
  ownerZ: owner ? this.scene.zOf(owner.id) : null
39576
39797
  };
@@ -39592,7 +39813,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
39592
39813
  syncLayerCanvasOrder() {
39593
39814
  if (this.extLayers.length === 0 || !this.plot) return;
39594
39815
  const { below, above } = this.orderedLayerCanvases();
39595
- 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(",");
39816
+ 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(",");
39596
39817
  if (sig === this.layerOrderSig) return;
39597
39818
  this.layerOrderSig = sig;
39598
39819
  for (const c of below) this.plot.insertBefore(c, this.dataCanvas);
@@ -39616,6 +39837,17 @@ ${overlayScrollbarCss(".vela-sd-pane")}
39616
39837
  (m) => m.series.length === 0 && !!m.native && this.extLayers.some((l) => l.def.id === m.native.type)
39617
39838
  );
39618
39839
  }
39840
+ /**
39841
+ * True when SDK-layer content on the price pane paints at BAR PRICES without
39842
+ * contributing a series: the active chart type's own layer, or an overlay layer native
39843
+ * (a series-less model whose type names a mounted layer). Hiding the candles must then
39844
+ * keep the bars in the price scale — with nothing else on the pane the scale would fall
39845
+ * to the {0,1} placeholder and every such layer would paint off-screen and vanish.
39846
+ */
39847
+ priceLayersAnchoredToBars(masterModels) {
39848
+ if (this.extLayers.some((l) => l.owner === null && l.def.id === this.scene.priceStyle)) return true;
39849
+ return masterModels.some((m) => m.series.length === 0 && !!m.native && this.extLayers.some((l) => l.def.id === m.native.type));
39850
+ }
39619
39851
  /** Per-pane scale state for a host UI (e.g. a price-axis context menu): the pane's pixel
39620
39852
  * band (`top`/`height`, so a click y maps to a pane) plus its current axis `mode`/`log`.
39621
39853
  * Top-to-bottom order. Every pane is independent — the price pane from the scene setting,
@@ -44355,6 +44587,8 @@ ${overlayScrollbarCss(".vela-sd-pane")}
44355
44587
  exports.rendererLayers = rendererLayers;
44356
44588
  exports.resolveEngines = resolveEngines;
44357
44589
  exports.resolveTheme = resolveTheme;
44590
+ exports.seriesInScale = seriesInScale;
44591
+ exports.seriesShownOn = seriesShownOn;
44358
44592
  exports.settingsRowValueKeys = settingsRowValueKeys;
44359
44593
  exports.sharedBarStore = sharedBarStore;
44360
44594
  exports.sidePanels = sidePanels;