@luxalgo/vela 0.8.1 → 0.8.3

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 (49) hide show
  1. package/README.md +1 -1
  2. package/dist/{DataProvider-DwFIoV4r.d.ts → DataProvider-oH78tYXL.d.ts} +1 -1
  3. package/dist/{DataProvider-BrlxntPN.d.cts → DataProvider-wbtJ3-Sa.d.cts} +1 -1
  4. package/dist/{chunk-DBDXKZUF.js → chunk-2IPXYM4B.js} +306 -177
  5. package/dist/{chunk-NELQJCGK.js → chunk-G7B7ZCBF.js} +167 -10
  6. package/dist/{chunk-ATGTCWHJ.js → chunk-HMHA733L.js} +473 -196
  7. package/dist/{chunk-EZ5FWVLA.js → chunk-KWI3YDBW.js} +343 -17
  8. package/dist/{chunk-LMBYEGUR.js → chunk-TO3GIK7Z.js} +71 -6
  9. package/dist/{chunk-BZQM2XO7.js → chunk-VHGACEHO.js} +62 -1
  10. package/dist/{contributions-tjNExo1o.d.ts → contributions-BhOBRenr.d.cts} +79 -12
  11. package/dist/{contributions-CtlptxR8.d.cts → contributions-HGqnNb_P.d.ts} +79 -12
  12. package/dist/index.cjs +733 -113
  13. package/dist/index.d.cts +34 -14
  14. package/dist/index.d.ts +34 -14
  15. package/dist/index.js +5 -5
  16. package/dist/{options-BnEPUuVV.d.ts → options-lMUwmRwh.d.cts} +69 -4
  17. package/dist/{options-BnEPUuVV.d.cts → options-lMUwmRwh.d.ts} +69 -4
  18. package/dist/{plugin-e2WUEtiI.d.ts → plugin-Cr2Pj8q8.d.ts} +4 -4
  19. package/dist/{plugin-BWxJRKzS.d.cts → plugin-Do5J5xuF.d.cts} +4 -4
  20. package/dist/plugin.cjs +38 -6
  21. package/dist/plugin.d.cts +5 -5
  22. package/dist/plugin.d.ts +5 -5
  23. package/dist/plugin.js +3 -3
  24. package/dist/providers/binance.d.cts +2 -2
  25. package/dist/providers/binance.d.ts +2 -2
  26. package/dist/providers/coinbase.d.cts +2 -2
  27. package/dist/providers/coinbase.d.ts +2 -2
  28. package/dist/providers/hyperliquid.d.cts +2 -2
  29. package/dist/providers/hyperliquid.d.ts +2 -2
  30. package/dist/{side-panel-HF0IAzwf.d.cts → side-panel-_2UK1gSm.d.cts} +58 -3
  31. package/dist/{side-panel-HF0IAzwf.d.ts → side-panel-_2UK1gSm.d.ts} +58 -3
  32. package/dist/{statusline-model-DFY7tuHj.d.cts → statusline-model-B-2xGlB_.d.cts} +3 -3
  33. package/dist/{statusline-model-zKXtGIjj.d.ts → statusline-model-CY1wzxHg.d.ts} +3 -3
  34. package/dist/ui.cjs +298 -13
  35. package/dist/ui.d.cts +132 -74
  36. package/dist/ui.d.ts +132 -74
  37. package/dist/ui.js +3 -3
  38. package/dist/vela.global.js +734 -114
  39. package/dist/vela.global.min.js +86 -52
  40. package/dist/widget.cjs +1227 -211
  41. package/dist/widget.d.cts +36 -12
  42. package/dist/widget.d.ts +36 -12
  43. package/dist/widget.js +7 -7
  44. package/dist/workspace.cjs +1226 -211
  45. package/dist/workspace.d.cts +47 -20
  46. package/dist/workspace.d.ts +47 -20
  47. package/dist/workspace.js +6 -6
  48. package/package.json +2 -2
  49. /package/dist/{chunk-JU4CWEBF.js → chunk-W5YPYZ2P.js} +0 -0
@@ -214,7 +214,7 @@ var Vela = (function (exports) {
214
214
  this.controller.applyProps(this.id, values);
215
215
  }
216
216
  updateCode(source) {
217
- this.controller.updateCode(this.id, source);
217
+ return this.controller.updateCode(this.id, source);
218
218
  }
219
219
  setVisible(visible) {
220
220
  this.controller.setVisible(this.id, visible);
@@ -617,6 +617,7 @@ var Vela = (function (exports) {
617
617
  registerIcon("plus", S('<path d="M8 2.8v10.4M2.8 8h10.4"/>'));
618
618
  registerIcon("minus", S('<path d="M2.8 8h10.4"/>'));
619
619
  registerIcon("maximize", S('<path d="M2.5 6V3a.5.5 0 0 1 .5-.5h3M10 2.5h3a.5.5 0 0 1 .5.5v3M13.5 10v3a.5.5 0 0 1-.5.5h-3M6 13.5H3a.5.5 0 0 1-.5-.5v-3"/>'));
620
+ registerIcon("restore", S('<path d="M6 2.5v3a.5.5 0 0 1-.5.5h-3M13.5 6h-3a.5.5 0 0 1-.5-.5v-3M10 13.5v-3a.5.5 0 0 1 .5-.5h3M2.5 10h3a.5.5 0 0 1 .5.5v3"/>'));
620
621
  registerIcon("restore", S('<path d="M6.2 2.5v3.7H2.5M9.8 13.5V9.8h3.7M13.5 6.2H9.8V2.5M2.5 9.8h3.7v3.7"/>'));
621
622
  registerIcon("star", S('<path d="M8 2.2l1.75 3.55 3.9.55-2.8 2.75.65 3.9L8 11.1l-3.5 1.85.65-3.9-2.8-2.75 3.9-.55z"/>'));
622
623
  registerIcon("star-filled", S('<path d="M8 2.2l1.75 3.55 3.9.55-2.8 2.75.65 3.9L8 11.1l-3.5 1.85.65-3.9-2.8-2.75 3.9-.55z"/>', 'fill="currentColor"'));
@@ -3275,6 +3276,10 @@ var Vela = (function (exports) {
3275
3276
 
3276
3277
  // src/core/drawings/types/FibLevels.ts
3277
3278
  var FibLevels = class extends FibRatios {
3279
+ /** Price of a level for the two anchor prices; subclasses override to change which anchor ratio 0 sits on. */
3280
+ levelPrice(ratio, p1, p2) {
3281
+ return p1 + ratio * (p2 - p1);
3282
+ }
3278
3283
  /** Per-level pixel line + price for the ENABLED levels, spanning the anchors' time range. */
3279
3284
  levelLines(proj) {
3280
3285
  const a = this.anchors[0];
@@ -3284,11 +3289,10 @@ var Vela = (function (exports) {
3284
3289
  const xb = proj.xOf(b.time);
3285
3290
  const x1 = Math.min(xa, xb);
3286
3291
  const x2 = Math.max(xa, xb);
3287
- const delta = b.price - a.price;
3288
3292
  const out = [];
3289
3293
  for (const lv of this.levels) {
3290
3294
  if (!lv.enabled) continue;
3291
- const price = a.price + lv.ratio * delta;
3295
+ const price = this.levelPrice(lv.ratio, a.price, b.price);
3292
3296
  const y = proj.yOf(price, this.paneId);
3293
3297
  if (y == null) continue;
3294
3298
  out.push({ ratio: lv.ratio, color: lv.color, label: lv.label, price, x1, x2, y });
@@ -3328,8 +3332,7 @@ var Vela = (function (exports) {
3328
3332
  const a = this.anchors[0];
3329
3333
  const b = this.anchors[1];
3330
3334
  if (!a || !b) return null;
3331
- const delta = b.price - a.price;
3332
- const prices = this.levels.filter((l) => l.enabled).map((l) => a.price + l.ratio * delta);
3335
+ const prices = this.levels.filter((l) => l.enabled).map((l) => this.levelPrice(l.ratio, a.price, b.price));
3333
3336
  if (prices.length === 0) return null;
3334
3337
  return { min: Math.min(...prices), max: Math.max(...prices) };
3335
3338
  }
@@ -3415,13 +3418,24 @@ var Vela = (function (exports) {
3415
3418
  // src/core/drawings/types/FibRetracement.ts
3416
3419
  var LEVELS = fibLevels([0, 0.236, 0.382, 0.5, 0.618, 0.786, 1]);
3417
3420
  var FibRetracement = class extends FibLevels {
3418
- constructor() {
3419
- super(...arguments);
3421
+ constructor(init) {
3422
+ super(init);
3420
3423
  this.type = "fibretracement";
3424
+ if (this.reverse === void 0) this.reverse = false;
3421
3425
  }
3422
3426
  defaultLevels() {
3423
3427
  return LEVELS;
3424
3428
  }
3429
+ levelPrice(ratio, p1, p2) {
3430
+ return this.reverse ? p1 + ratio * (p2 - p1) : p2 + ratio * (p1 - p2);
3431
+ }
3432
+ writeProps() {
3433
+ return { ...super.writeProps(), reverse: this.reverse };
3434
+ }
3435
+ readProps(props) {
3436
+ super.readProps(props);
3437
+ this.reverse = typeof props.reverse === "boolean" ? props.reverse : true;
3438
+ }
3425
3439
  };
3426
3440
 
3427
3441
  // src/core/drawings/types/FibExtension.ts
@@ -7850,6 +7864,7 @@ var Vela = (function (exports) {
7850
7864
  this.activeEngineStyle = null;
7851
7865
  /** The chart's current price style (tracked from the renderer's change events + initial read). */
7852
7866
  this.priceStyle = "candles";
7867
+ this.priceStyleWillChangeUnsub = null;
7853
7868
  this.unresolvedUnsub = null;
7854
7869
  /** Latest chart visible range (left/right bar times), fed to viewport-dependent scripts. */
7855
7870
  this.visibleRange = null;
@@ -7948,6 +7963,7 @@ var Vela = (function (exports) {
7948
7963
  this.renderer.onThemeSelect?.((name) => this.setTheme(resolveTheme(name)));
7949
7964
  this.viewportUnsub = this.renderer.onViewportChange((range) => this.onViewportChange(range));
7950
7965
  this.renderer.onPriceStyleChange?.((style) => this.syncPriceStyle(style));
7966
+ this.priceStyleWillChangeUnsub = this.renderer.onPriceStyleWillChange?.((from, to) => this.events.emit("priceStyle:change", { from, to })) ?? null;
7951
7967
  const initialStyle = this.renderer.readFeature("priceStyle");
7952
7968
  if (typeof initialStyle === "string") this.priceStyle = initialStyle;
7953
7969
  this.barTransform = barTransformFor(initialStyle);
@@ -9234,20 +9250,26 @@ var Vela = (function (exports) {
9234
9250
  /**
9235
9251
  * IndicatorController: replace a SCRIPT indicator's source in place (see
9236
9252
  * {@link IndicatorHandle.updateCode}). Prepare-first: the running session keeps
9237
- * computing until the new source has compiled, so a broken edit costs the user
9238
- * nothing but an `error` event. Natives have no script — warn and leave them alone.
9253
+ * computing until the new source has compiled; then the new code runs on TRIAL until
9254
+ * its first model lands, and a failure before that puts the previous code back.
9255
+ * Natives have no script — warn and leave them alone.
9239
9256
  */
9240
9257
  updateCode(id, source) {
9241
9258
  const record = this.registry.get(id);
9242
9259
  const handle = this.handles.get(id);
9243
- if (!record || !handle) return;
9260
+ if (!record || !handle) return Promise.resolve({ ok: false, error: new Error(`[vela] updateCode("${id}") \u2014 the indicator is no longer on the chart.`) });
9244
9261
  if (record.native) {
9245
9262
  console.warn(`[vela] updateCode("${id}") \u2014 a native indicator has no script to update.`);
9246
- return;
9263
+ return Promise.resolve({ ok: false, error: new Error(`[vela] updateCode("${id}") \u2014 a native indicator has no script to update.`) });
9247
9264
  }
9248
- if (source === record.source && record.pendingSource === void 0) return;
9249
- record.pendingSource = source;
9250
- void this.swapSource(id, source, handle);
9265
+ const busy = record.pendingSource !== void 0 || record.codeTrial !== void 0;
9266
+ if (!busy && source === record.source) return Promise.resolve({ ok: true, error: null });
9267
+ return new Promise((resolve) => {
9268
+ (record.codeWaiters ?? (record.codeWaiters = [])).push(resolve);
9269
+ if (record.pendingSource === source || record.pendingSource === void 0 && record.codeTrial && record.source === source) return;
9270
+ record.pendingSource = source;
9271
+ void this.swapSource(id, source, handle);
9272
+ });
9251
9273
  }
9252
9274
  async swapSource(id, source, handle) {
9253
9275
  const engine = this.registry.get(id)?.engine ?? this.engineFor(this.registry.get(id)?.options?.language);
@@ -9258,26 +9280,41 @@ var Vela = (function (exports) {
9258
9280
  const record2 = this.registry.get(id);
9259
9281
  if (record2?.pendingSource === source) {
9260
9282
  record2.pendingSource = void 0;
9261
- this.fail(id, handle, err);
9283
+ this.rejectCode(id, record2, handle, err);
9262
9284
  }
9263
9285
  return;
9264
9286
  }
9265
9287
  const record = this.registry.get(id);
9266
9288
  if (!record || record.pendingSource !== source) return;
9267
9289
  record.pendingSource = void 0;
9290
+ const previous = record.codeTrial?.previous ?? {
9291
+ source: record.source,
9292
+ engine: record.engine,
9293
+ prepared: record.prepared,
9294
+ inputValues: record.inputValues,
9295
+ propValues: record.propValues,
9296
+ inputs: [...handle.inputs],
9297
+ props: [...handle.props]
9298
+ };
9299
+ record.inputValues = valuesOnSchema(prepared.inputs, record.inputValues, record.prepared?.inputs);
9300
+ record.propValues = valuesOnSchema(prepared.props ?? [], record.propValues, record.prepared?.props);
9268
9301
  record.source = source;
9269
9302
  record.engine = engine;
9270
9303
  record.prepared = prepared;
9271
9304
  handle.setSource(source);
9272
- record.inputValues = valuesOnSchema(prepared.inputs, record.inputValues);
9273
- record.propValues = valuesOnSchema(prepared.props ?? [], record.propValues);
9274
9305
  handle.setSchema(prepared.inputs);
9275
9306
  handle.setPropsSchema(prepared.props ?? []);
9276
9307
  record.session?.stop();
9277
9308
  record.session = void 0;
9278
9309
  record.pendingStructural = true;
9279
9310
  record.pendingCause = "code";
9280
- if (record.hidden) return;
9311
+ if (record.hidden) {
9312
+ record.codeTrial = void 0;
9313
+ if (record.renderHandle) this.renderer.setIndicatorInputs(record.renderHandle, record.inputValues, record.propValues);
9314
+ this.settleCode(record, { ok: true, error: null });
9315
+ return;
9316
+ }
9317
+ record.codeTrial = { previous };
9281
9318
  if (record.renderHandle) {
9282
9319
  this.renderer.setIndicatorInputs(record.renderHandle, record.inputValues, record.propValues);
9283
9320
  this.setLoading(record, true);
@@ -9285,6 +9322,46 @@ var Vela = (function (exports) {
9285
9322
  this.mountLoadingPlaceholder(id, record);
9286
9323
  }
9287
9324
  this.executeIndicator(id, handle);
9325
+ if (this.bars.length === 0 && record.codeTrial) {
9326
+ record.codeTrial = void 0;
9327
+ this.settleCode(record, { ok: true, error: null });
9328
+ }
9329
+ }
9330
+ /** Resolve every `updateCode` caller waiting on this record's chain of edits. */
9331
+ settleCode(record, result) {
9332
+ const waiters = record.codeWaiters;
9333
+ record.codeWaiters = void 0;
9334
+ if (waiters) for (const resolve of waiters) resolve(result);
9335
+ }
9336
+ /**
9337
+ * An update failed — at prepare, or on trial before its first model. A trial puts the
9338
+ * previous code back (source, engine, prepared script, values, schema) and restarts it;
9339
+ * its visuals were never dropped, so the chart keeps painting throughout. Then the
9340
+ * failure reports the usual way and every waiting caller learns it.
9341
+ */
9342
+ rejectCode(id, record, handle, err) {
9343
+ const trial = record.codeTrial;
9344
+ record.codeTrial = void 0;
9345
+ this.fail(id, handle, err);
9346
+ if (trial) {
9347
+ const p = trial.previous;
9348
+ record.session?.stop();
9349
+ record.session = void 0;
9350
+ record.source = p.source;
9351
+ record.engine = p.engine;
9352
+ record.prepared = p.prepared;
9353
+ record.inputValues = p.inputValues;
9354
+ record.propValues = p.propValues;
9355
+ handle.setSource(p.source);
9356
+ handle.setSchema(p.inputs);
9357
+ handle.setPropsSchema(p.props);
9358
+ record.pendingStructural = true;
9359
+ record.pendingCause = void 0;
9360
+ if (record.renderHandle) this.renderer.setIndicatorInputs(record.renderHandle, record.inputValues, record.propValues);
9361
+ if (!record.hidden) this.executeIndicator(id, handle);
9362
+ }
9363
+ const error = err instanceof Error ? err : new Error(String(err));
9364
+ this.settleCode(record, { ok: false, error });
9288
9365
  }
9289
9366
  /** IndicatorController: tear down an indicator and (if now empty) its pane. */
9290
9367
  /** Live handles of every indicator on the chart (script + native), insertion order. */
@@ -9299,6 +9376,7 @@ var Vela = (function (exports) {
9299
9376
  removeIndicator(id) {
9300
9377
  const record = this.registry.remove(id);
9301
9378
  if (record?.native?.type === "volume") this.volumeOptedOut = true;
9379
+ if (record) this.settleCode(record, { ok: false, error: new Error(`[vela] updateCode("${id}") \u2014 the indicator was removed.`) });
9302
9380
  record?.session?.stop();
9303
9381
  if (record?.native?.started) record.native.instance.stop();
9304
9382
  this.handles.delete(id);
@@ -9326,6 +9404,10 @@ var Vela = (function (exports) {
9326
9404
  if (!visible) {
9327
9405
  record.session?.stop();
9328
9406
  record.session = void 0;
9407
+ if (record.codeTrial) {
9408
+ record.codeTrial = void 0;
9409
+ this.settleCode(record, { ok: true, error: null });
9410
+ }
9329
9411
  if (record.native?.started) record.native.instance.suspend();
9330
9412
  if (record.renderHandle) this.renderer.setIndicatorVisible?.(record.renderHandle, false);
9331
9413
  else if (record.native) this.mountHiddenNativeRow(id, record);
@@ -9412,7 +9494,10 @@ var Vela = (function (exports) {
9412
9494
  if (this.viewportTimer != null) clearTimeout(this.viewportTimer);
9413
9495
  this.viewportUnsub?.();
9414
9496
  this.paneActionUnsub?.();
9497
+ this.priceStyleWillChangeUnsub?.();
9498
+ this.priceStyleWillChangeUnsub = null;
9415
9499
  for (const record of this.registry.all()) {
9500
+ this.settleCode(record, { ok: false, error: new Error("[vela] updateCode \u2014 the chart was destroyed.") });
9416
9501
  record.session?.stop();
9417
9502
  if (record.native?.started) record.native.instance.stop();
9418
9503
  }
@@ -9481,7 +9566,8 @@ var Vela = (function (exports) {
9481
9566
  const record = this.registry.get(id);
9482
9567
  if (!record || !record.engine || !record.prepared || record.hidden) return;
9483
9568
  const mode = this.config.live && !record.prepared.reactsToViewport && record.engine.capabilities.streaming ? "live" : "static";
9484
- record.session = record.engine.execute(
9569
+ const prepared = record.prepared;
9570
+ const session = record.engine.execute(
9485
9571
  {
9486
9572
  prepared: record.prepared,
9487
9573
  market: this.market(),
@@ -9502,6 +9588,10 @@ var Vela = (function (exports) {
9502
9588
  const cause = record.pendingCause ?? "history";
9503
9589
  if (!this.applyModel(id, model)) return;
9504
9590
  record.pendingCause = void 0;
9591
+ if (record.codeTrial) {
9592
+ record.codeTrial = void 0;
9593
+ this.settleCode(record, { ok: true, error: null });
9594
+ }
9505
9595
  this.emitContextChanged(id);
9506
9596
  this.emitScriptRun(id, cause, first2);
9507
9597
  },
@@ -9512,9 +9602,17 @@ var Vela = (function (exports) {
9512
9602
  handle.emit("alert", { id: a.id, message: a.message, title: a.title, time: a.time });
9513
9603
  },
9514
9604
  onWarning: (w) => this.events.emit("warning", w),
9515
- onError: (err) => this.fail(id, handle, err)
9605
+ onError: (err) => {
9606
+ if (record.codeTrial) this.rejectCode(id, record, handle, err);
9607
+ else this.fail(id, handle, err);
9608
+ }
9516
9609
  }
9517
9610
  );
9611
+ if (record.prepared !== prepared) {
9612
+ session.stop();
9613
+ return;
9614
+ }
9615
+ record.session = session;
9518
9616
  this.emitContextChanged(id);
9519
9617
  }
9520
9618
  /**
@@ -10015,7 +10113,7 @@ var Vela = (function (exports) {
10015
10113
  }
10016
10114
  return new Promise((resolve) => setTimeout(resolve, 0));
10017
10115
  }
10018
- function valuesOnSchema(schema, previous) {
10116
+ function valuesOnSchema(schema, previous, oldSchema = []) {
10019
10117
  const out = {};
10020
10118
  const declared = /* @__PURE__ */ new Set();
10021
10119
  for (const s of schema) {
@@ -10023,9 +10121,22 @@ var Vela = (function (exports) {
10023
10121
  declared.add(s.key);
10024
10122
  declared.add(s.title);
10025
10123
  }
10026
- for (const [k, v] of Object.entries(previous)) if (declared.has(k)) out[k] = v;
10124
+ const oldDefaults = /* @__PURE__ */ new Map();
10125
+ for (const s of oldSchema) {
10126
+ oldDefaults.set(s.key, s.defval);
10127
+ if (!oldDefaults.has(s.title)) oldDefaults.set(s.title, s.defval);
10128
+ }
10129
+ for (const [k, v] of Object.entries(previous)) {
10130
+ if (!declared.has(k)) continue;
10131
+ if (oldDefaults.has(k) && sameInputValue(oldDefaults.get(k), v)) continue;
10132
+ out[k] = v;
10133
+ }
10027
10134
  return out;
10028
10135
  }
10136
+ function sameInputValue(a, b) {
10137
+ if (a === b) return true;
10138
+ return typeof a === "object" && a !== null && typeof b === "object" && b !== null && JSON.stringify(a) === JSON.stringify(b);
10139
+ }
10029
10140
  function blankedModel(model) {
10030
10141
  return {
10031
10142
  ...model,
@@ -13587,6 +13698,66 @@ ${STATIC_DECLS}
13587
13698
  return span;
13588
13699
  }
13589
13700
 
13701
+ // src/ui/surface-events.ts
13702
+ var SURFACE_OPEN_EVENT = "vela:surface-open";
13703
+ var SURFACE_CLOSE_EVENT = "vela:surface-close";
13704
+ function announceSurface(el, open2, kind, trigger = null) {
13705
+ const View = el.ownerDocument.defaultView;
13706
+ if (!View) return;
13707
+ el.dispatchEvent(
13708
+ new View.CustomEvent(open2 ? SURFACE_OPEN_EVENT : SURFACE_CLOSE_EVENT, { bubbles: true, composed: true, detail: { kind, trigger } })
13709
+ );
13710
+ }
13711
+
13712
+ // src/ui/surface-exit.ts
13713
+ var CLOSING_ATTR = "data-closing";
13714
+ var SURFACE_EXIT_CAP_MS = 1e3;
13715
+ function animationsOf(els) {
13716
+ return els.flatMap((el) => el.getAnimations({ subtree: true }));
13717
+ }
13718
+ function ends(a) {
13719
+ return Number.isFinite(a.effect?.getComputedTiming().endTime ?? Number.POSITIVE_INFINITY);
13720
+ }
13721
+ function holdForExit(els, done, opts = {}) {
13722
+ const list = Array.isArray(els) ? els : [els];
13723
+ if (list.length === 0 || !list.every((el) => typeof el.getAnimations === "function")) {
13724
+ done();
13725
+ return null;
13726
+ }
13727
+ const before = new Set(animationsOf(list));
13728
+ for (const el of list) el.setAttribute(CLOSING_ATTR, "");
13729
+ const exit = animationsOf(list).filter((a) => !before.has(a) && ends(a));
13730
+ if (exit.length === 0) {
13731
+ for (const el of list) el.removeAttribute(CLOSING_ATTR);
13732
+ done();
13733
+ return null;
13734
+ }
13735
+ const frozen = [...list, ...opts.inert ?? []];
13736
+ const wasInert = frozen.map((el) => el.inert);
13737
+ for (const el of frozen) el.inert = true;
13738
+ let settled = false;
13739
+ const settle = (complete) => {
13740
+ if (settled) return;
13741
+ settled = true;
13742
+ clearTimeout(cap);
13743
+ for (const el of list) el.removeAttribute(CLOSING_ATTR);
13744
+ frozen.forEach((el, i) => {
13745
+ el.inert = wasInert[i];
13746
+ });
13747
+ if (complete) done();
13748
+ };
13749
+ const cap = setTimeout(() => settle(true), SURFACE_EXIT_CAP_MS);
13750
+ let pending = exit.length;
13751
+ const one = () => {
13752
+ if (--pending === 0) settle(true);
13753
+ };
13754
+ for (const a of exit) a.finished.then(one, one);
13755
+ return {
13756
+ cancel: () => settle(false),
13757
+ finish: () => settle(true)
13758
+ };
13759
+ }
13760
+
13590
13761
  // node_modules/@zag-js/anatomy/dist/create-anatomy.mjs
13591
13762
  var createAnatomy = (name, parts3 = []) => ({
13592
13763
  parts: (...values) => {
@@ -17999,6 +18170,35 @@ ${STATIC_DECLS}
17999
18170
  color: var(--vela-fg-bright);
18000
18171
  }
18001
18172
  .vela-menu-item[data-checkmark] { color: var(--vela-fg-bright); }
18173
+ /* Badge mode (iconBadges): the row icon sits in a rounded tile and the row grows to fit
18174
+ it. An icon-less row of the same level keeps an invisible slot, so labels align. */
18175
+ .vela-menu[data-badges] > .vela-menu-item { padding: 5px 10px 5px 6px; }
18176
+ .vela-menu-item .vela-menu-badge {
18177
+ width: 24px;
18178
+ height: 24px;
18179
+ padding: 3px;
18180
+ flex: none;
18181
+ box-sizing: border-box;
18182
+ display: inline-flex;
18183
+ align-items: center;
18184
+ justify-content: center;
18185
+ border-radius: var(--vela-radius-md);
18186
+ background: var(--vela-hover);
18187
+ border: 1px solid var(--vela-border);
18188
+ color: var(--vela-fg-muted);
18189
+ transition: background var(--vela-dur-fast) var(--vela-ease), border-color var(--vela-dur-fast) var(--vela-ease), color var(--vela-dur-fast) var(--vela-ease);
18190
+ }
18191
+ .vela-menu-item .vela-menu-badge:empty { background: transparent; border-color: transparent; }
18192
+ .vela-menu-item .vela-menu-badge .vela-icon { color: inherit; }
18193
+ .vela-menu-item[data-highlighted] .vela-menu-badge:not(:empty) { color: var(--vela-fg-bright); }
18194
+ /* The selected tile inverts: a bright fill under the surface-colored glyph. After the
18195
+ hover rule, so a selected row keeps its tile while highlighted. */
18196
+ .vela-menu-item[data-checked] .vela-menu-badge:not(:empty),
18197
+ .vela-menu-item[data-checkmark] .vela-menu-badge:not(:empty) {
18198
+ background: color-mix(in srgb, var(--vela-fg-bright) 83%, var(--vela-surface));
18199
+ border-color: color-mix(in srgb, var(--vela-fg-bright) 40%, transparent);
18200
+ color: var(--vela-surface);
18201
+ }
18002
18202
  /* Switch rows (boolean settings in a dropdown): a right-aligned toggle pill \u2014 the
18003
18203
  same control language as the settings dialog's toggles. */
18004
18204
  .vela-menu-switch {
@@ -18065,11 +18265,16 @@ ${STATIC_DECLS}
18065
18265
  /** Trigger rect captured when this level opened — the list stays put if the
18066
18266
  * trigger then moves (favorite chips shifting the caret). */
18067
18267
  this.pinnedAnchor = null;
18268
+ /** The open state last announced — see {@link announce}. */
18269
+ this.shown = false;
18270
+ /** The list's exit animation after a close; a reopen cancels it. */
18271
+ this.exit = null;
18068
18272
  this.doc = doc;
18069
18273
  this.host = opts.host;
18070
18274
  this.onSelect = opts.onSelect;
18071
18275
  this.onFavorite = opts.onFavorite;
18072
18276
  this.checkmarks = opts.checkmarks === true;
18277
+ this.iconBadges = opts.iconBadges === true;
18073
18278
  this.positioner = doc.createElement("div");
18074
18279
  this.positioner.className = "vela-ui-layer";
18075
18280
  this.list = doc.createElement("ul");
@@ -18078,6 +18283,7 @@ ${STATIC_DECLS}
18078
18283
  this.positioner.appendChild(this.list);
18079
18284
  this.host.appendChild(this.positioner);
18080
18285
  const trigger = opts.trigger;
18286
+ this.opener = trigger ?? opts.opener ?? null;
18081
18287
  this.ctrl = menuController({
18082
18288
  items: [],
18083
18289
  id: opts.id,
@@ -18099,15 +18305,37 @@ ${STATIC_DECLS}
18099
18305
  if (opts.triggerId && trigger) trigger.id = opts.triggerId;
18100
18306
  this.handle = runMachine(this.ctrl.machine, this.ctrl.props, (service) => {
18101
18307
  const api = this.ctrl.connect(service);
18308
+ const closing = this.shown && !api.open;
18309
+ if (closing) this.announce(false);
18310
+ if (api.open && this.exit) {
18311
+ this.exit.cancel();
18312
+ this.exit = null;
18313
+ }
18102
18314
  if (trigger) spreadProps(trigger, api.getTriggerProps(), this.mid);
18103
18315
  spreadProps(this.positioner, api.getPositionerProps(), this.mid);
18104
18316
  spreadProps(this.list, api.getContentProps(), this.mid);
18105
18317
  this.project(api);
18318
+ if (closing) this.holdExit();
18319
+ if (api.open && !this.shown) this.announce(true);
18320
+ });
18321
+ }
18322
+ /** Keep the just-closed list up through its exit animation. `spreadProps` re-applies
18323
+ * `hidden` only when the machine's value for it changes, so the list stays shown until
18324
+ * the exit ends — or until a reopen, which projects `hidden: false` itself. */
18325
+ holdExit() {
18326
+ this.list.hidden = false;
18327
+ this.exit = holdForExit(this.list, () => {
18328
+ this.exit = null;
18329
+ if (!this.shown) this.list.hidden = true;
18106
18330
  });
18107
18331
  }
18108
18332
  get api() {
18109
18333
  return this.ctrl.connect(this.handle.service);
18110
18334
  }
18335
+ announce(open2) {
18336
+ this.shown = open2;
18337
+ announceSurface(this.list, open2, "menu", this.opener);
18338
+ }
18111
18339
  setItems(items) {
18112
18340
  this.items = items;
18113
18341
  for (const sub2 of this.subs.values()) sub2.destroy();
@@ -18124,7 +18352,9 @@ ${STATIC_DECLS}
18124
18352
  destroy() {
18125
18353
  for (const sub2 of this.subs.values()) sub2.destroy();
18126
18354
  this.subs.clear();
18355
+ this.exit?.finish();
18127
18356
  this.handle.stop();
18357
+ if (this.shown) this.announce(false);
18128
18358
  this.positioner.remove();
18129
18359
  }
18130
18360
  byId(id) {
@@ -18149,6 +18379,10 @@ ${STATIC_DECLS}
18149
18379
  const doc = this.doc;
18150
18380
  this.list.replaceChildren();
18151
18381
  const markable = this.checkmarks && this.items.some((i) => !(i.submenu && i.submenu.length > 0) && !i.toggle && i.checked !== void 0);
18382
+ const iconic = this.items.some((i) => !!i.icon);
18383
+ const badged = this.iconBadges && iconic;
18384
+ if (badged) this.list.dataset.badges = "1";
18385
+ else delete this.list.dataset.badges;
18152
18386
  for (const item of this.items) {
18153
18387
  if (item.separatorBefore) {
18154
18388
  const sep = doc.createElement("li");
@@ -18174,7 +18408,16 @@ ${STATIC_DECLS}
18174
18408
  } else if (!branch && !item.toggle && item.checked) {
18175
18409
  li.dataset.checked = "1";
18176
18410
  }
18177
- if (item.icon) li.appendChild(iconEl(item.icon, doc));
18411
+ if (badged) {
18412
+ const badge = doc.createElement("span");
18413
+ badge.className = "vela-menu-badge";
18414
+ if (item.icon) badge.appendChild(iconEl(item.icon, doc));
18415
+ li.appendChild(badge);
18416
+ } else if (item.icon) {
18417
+ li.appendChild(iconEl(item.icon, doc));
18418
+ } else if (iconic) {
18419
+ li.appendChild(iconEl("", doc));
18420
+ }
18178
18421
  const label = doc.createElement("span");
18179
18422
  label.className = "vela-menu-label";
18180
18423
  label.textContent = item.label;
@@ -18218,9 +18461,11 @@ ${STATIC_DECLS}
18218
18461
  const sub2 = new _Surface(doc, {
18219
18462
  host: this.host,
18220
18463
  placement: "right-start",
18464
+ opener: li,
18221
18465
  onSelect: this.onSelect,
18222
18466
  onFavorite: this.onFavorite,
18223
18467
  checkmarks: this.checkmarks,
18468
+ iconBadges: this.iconBadges,
18224
18469
  id: `${this.mid}--${item.id}`
18225
18470
  });
18226
18471
  sub2.setItems(item.submenu ?? []);
@@ -18246,7 +18491,8 @@ ${STATIC_DECLS}
18246
18491
  id: opts.id,
18247
18492
  minWidth: opts.minWidth,
18248
18493
  onFavorite: opts.onFavorite,
18249
- checkmarks: opts.checkmarks
18494
+ checkmarks: opts.checkmarks,
18495
+ iconBadges: opts.iconBadges
18250
18496
  });
18251
18497
  this.root.setItems(opts.items);
18252
18498
  }
@@ -18350,6 +18596,7 @@ ${STATIC_DECLS}
18350
18596
 
18351
18597
  // src/ui/components/popover/view.ts
18352
18598
  var open = null;
18599
+ var leaving = /* @__PURE__ */ new Set();
18353
18600
  function closeOpenPopovers() {
18354
18601
  open?.hide();
18355
18602
  }
@@ -18377,6 +18624,8 @@ ${STATIC_DECLS}
18377
18624
  this.shown = false;
18378
18625
  /** The pending removal of a fading-out shell; a show() that reuses the shell cancels it. */
18379
18626
  this.leaveTimer = null;
18627
+ /** The shell's exit animation before it leaves the DOM; a show() that reuses the shell cancels it. */
18628
+ this.exit = null;
18380
18629
  const doc = opts.trigger.ownerDocument;
18381
18630
  injectStyles(POPOVER_STYLE_ID, POPOVER_CSS, doc);
18382
18631
  this.trigger = opts.trigger;
@@ -18413,6 +18662,10 @@ ${STATIC_DECLS}
18413
18662
  clearTimeout(this.leaveTimer);
18414
18663
  this.leaveTimer = null;
18415
18664
  }
18665
+ this.exit?.cancel();
18666
+ this.exit = null;
18667
+ leaving.delete(this);
18668
+ for (const p of [...leaving]) if (p.trigger === this.trigger) p.exit?.finish();
18416
18669
  ensureUIHost(this.el, this.inheritsTokens ? void 0 : this.theme);
18417
18670
  if (this.fadeMs > 0) {
18418
18671
  this.el.style.transition = `opacity ${this.fadeMs}ms ease`;
@@ -18437,16 +18690,21 @@ ${STATIC_DECLS}
18437
18690
  this.hide();
18438
18691
  };
18439
18692
  const onReflow = () => this.place();
18440
- setTimeout(() => document.addEventListener("pointerdown", onOutside, true), 0);
18693
+ setTimeout(() => {
18694
+ if (this.onOutside === onOutside) document.addEventListener("pointerdown", onOutside, true);
18695
+ }, 0);
18441
18696
  document.addEventListener("keydown", onKey, true);
18442
18697
  window.addEventListener("resize", onReflow, true);
18443
18698
  document.addEventListener("scroll", onReflow, true);
18444
18699
  this.onOutside = onOutside;
18445
18700
  this.onKey = onKey;
18446
18701
  this.onReflow = onReflow;
18702
+ announceSurface(this.el, true, "popover", this.trigger);
18447
18703
  }
18448
18704
  hide() {
18449
18705
  if (!this.shown) return;
18706
+ this.shown = false;
18707
+ announceSurface(this.el, false, "popover", this.trigger);
18450
18708
  if (this.onOutside) document.removeEventListener("pointerdown", this.onOutside, true);
18451
18709
  if (this.onKey) document.removeEventListener("keydown", this.onKey, true);
18452
18710
  if (this.onReflow) {
@@ -18464,9 +18722,13 @@ ${STATIC_DECLS}
18464
18722
  this.el.remove();
18465
18723
  }, this.fadeMs);
18466
18724
  } else {
18467
- this.el.remove();
18725
+ this.exit = holdForExit(this.el, () => {
18726
+ this.exit = null;
18727
+ leaving.delete(this);
18728
+ this.el.remove();
18729
+ });
18730
+ if (this.exit) leaving.add(this);
18468
18731
  }
18469
- this.shown = false;
18470
18732
  if (open === this) open = null;
18471
18733
  this.ctrl.onClose?.();
18472
18734
  }
@@ -18477,6 +18739,7 @@ ${STATIC_DECLS}
18477
18739
  }
18478
18740
  destroy() {
18479
18741
  this.hide();
18742
+ this.exit?.finish();
18480
18743
  }
18481
18744
  reposition() {
18482
18745
  if (this.shown) this.place();
@@ -20356,6 +20619,11 @@ ${overlayScrollbarCss(".vela-dialog *")}
20356
20619
  // src/ui/components/dialog/view.ts
20357
20620
  var Dialog = class {
20358
20621
  constructor(opts = {}) {
20622
+ this.opener = null;
20623
+ this.wasOpen = false;
20624
+ /** The panel and scrim's exit animation after a close; a reopen cancels it. */
20625
+ this.exit = null;
20626
+ this.destroyed = false;
20359
20627
  const doc = (opts.host ?? document.body).ownerDocument;
20360
20628
  injectStyles(DIALOG_STYLE_ID, DIALOG_CSS, doc);
20361
20629
  const host = opts.host ?? doc.body;
@@ -20452,19 +20720,73 @@ ${overlayScrollbarCss(".vela-dialog *")}
20452
20720
  const mid = String(this.ctrl.props.id);
20453
20721
  this.handle = runMachine(this.ctrl.machine, this.ctrl.props, (service) => {
20454
20722
  const api = this.ctrl.connect(service);
20723
+ const closing = this.wasOpen && !api.open;
20724
+ if (closing) this.announce(false);
20455
20725
  spreadProps(this.backdrop, api.getBackdropProps(), mid);
20456
20726
  spreadProps(this.positioner, api.getPositionerProps(), mid);
20457
20727
  spreadProps(this.panel, api.getContentProps(), mid);
20458
20728
  spreadProps(title, api.getTitleProps(), mid);
20459
20729
  spreadProps(close, api.getCloseTriggerProps(), mid);
20460
- this.backdrop.style.display = api.open ? "" : "none";
20461
- this.positioner.style.display = api.open ? "" : "none";
20462
- });
20730
+ if (api.open) {
20731
+ this.exit?.cancel();
20732
+ this.exit = null;
20733
+ this.setShown(true);
20734
+ } else if (closing) {
20735
+ this.holdExit();
20736
+ } else if (!this.exit) {
20737
+ this.setShown(false);
20738
+ }
20739
+ if (api.open && !this.wasOpen) this.announce(true);
20740
+ if (this.wasOpen && !api.open) this.restoreOpener();
20741
+ this.wasOpen = api.open;
20742
+ });
20743
+ }
20744
+ setShown(shown) {
20745
+ this.backdrop.style.display = shown ? "" : "none";
20746
+ this.positioner.style.display = shown ? "" : "none";
20747
+ }
20748
+ /** Keep the just-closed dialog up through its exit animation, then hide it — or
20749
+ * remove it, when it was destroyed meanwhile. `spreadProps` re-applies the close
20750
+ * props' `hidden` only when its value changes, so lifting it keeps the panel and
20751
+ * scrim up until the exit ends. The positioner spans the viewport: inert, it lets
20752
+ * clicks through to the page while the panel animates out. */
20753
+ holdExit() {
20754
+ const held = [this.panel, this.backdrop];
20755
+ for (const el of held) el.hidden = false;
20756
+ this.exit = holdForExit(
20757
+ held,
20758
+ () => {
20759
+ this.exit = null;
20760
+ if (this.destroyed) {
20761
+ this.detach();
20762
+ } else if (!this.open) {
20763
+ for (const el of held) el.hidden = true;
20764
+ this.setShown(false);
20765
+ }
20766
+ },
20767
+ { inert: [this.positioner] }
20768
+ );
20769
+ }
20770
+ detach() {
20771
+ this.backdrop.remove();
20772
+ this.positioner.remove();
20773
+ }
20774
+ /** Closing hands focus back to whatever opened it (Escape, the ✕, hide(), or a
20775
+ * teardown that outruns the machine's close notification). */
20776
+ restoreOpener() {
20777
+ const opener = this.opener;
20778
+ this.opener = null;
20779
+ if (opener?.isConnected) opener.focus({ preventScroll: true });
20780
+ }
20781
+ announce(open2) {
20782
+ const opener = this.opener === this.panel.ownerDocument.body ? null : this.opener;
20783
+ announceSurface(this.panel, open2, "dialog", opener);
20463
20784
  }
20464
20785
  get open() {
20465
20786
  return this.ctrl.connect(this.handle.service).open;
20466
20787
  }
20467
20788
  show() {
20789
+ this.opener = this.panel.ownerDocument.activeElement;
20468
20790
  this.ctrl.connect(this.handle.service).setOpen(true);
20469
20791
  }
20470
20792
  hide() {
@@ -20477,10 +20799,16 @@ ${overlayScrollbarCss(".vela-dialog *")}
20477
20799
  return el?.closest(".vela-popover, .vela-menu") != null;
20478
20800
  }
20479
20801
  destroy() {
20802
+ if (this.wasOpen && !this.open) this.handle.flush();
20480
20803
  if (this.open) this.hide();
20481
20804
  this.handle.stop();
20482
- this.backdrop.remove();
20483
- this.positioner.remove();
20805
+ this.destroyed = true;
20806
+ if (this.wasOpen) {
20807
+ this.wasOpen = false;
20808
+ this.announce(false);
20809
+ }
20810
+ this.restoreOpener();
20811
+ if (!this.exit) this.detach();
20484
20812
  }
20485
20813
  };
20486
20814
 
@@ -22932,7 +23260,10 @@ ${overlayScrollbarCss(".vela-dialog.vela-ind-dialog *", 9)}
22932
23260
  var LEGEND_CTL_PX = 18;
22933
23261
  var LEGEND_ROW_PAD_Y = 2;
22934
23262
  var LEGEND_ROW_PAD_X = 6;
23263
+ var LEGEND_LEFT_PX = 10;
23264
+ var LEGEND_SCALE_GAP_PX = 6;
22935
23265
  var LEGEND_TITLE_VALUES_GAP_PX = 6;
23266
+ var LEGEND_VALUES_GAP_PX = 5;
22936
23267
  var LEGEND_TITLE_STATUS_GAP_PX = 8;
22937
23268
  var LEGEND_ACTION_GAP_PX = 6;
22938
23269
  var LEGEND_CTL_CSS = `cursor:pointer;display:inline-flex;align-items:center;justify-content:center;border:none;line-height:0;padding:0;width:${LEGEND_CTL_PX}px;height:${LEGEND_CTL_PX}px;border-radius:3px;box-sizing:border-box;flex:none;`;
@@ -23218,14 +23549,15 @@ ${overlayScrollbarCss(".vela-dialog.vela-ind-dialog *", 9)}
23218
23549
  row.valuesEl.replaceChildren();
23219
23550
  return;
23220
23551
  }
23221
- row.valuesEl.style.display = "inline-flex";
23552
+ row.valuesEl.style.display = "block";
23222
23553
  row.valuesEl.replaceChildren();
23223
- for (const v of row.plotValues) {
23554
+ row.plotValues.forEach((v, i) => {
23224
23555
  const span = document.createElement("span");
23225
23556
  span.textContent = v.value;
23226
23557
  span.style.color = v.color;
23558
+ if (i > 0) span.style.marginLeft = `${LEGEND_VALUES_GAP_PX}px`;
23227
23559
  row.valuesEl.appendChild(span);
23228
- }
23560
+ });
23229
23561
  }
23230
23562
  // ── legend move/merge (menu + drag) ─────────────────────────────────────
23231
23563
  /** Open the "Move to" menu for a row, anchored under its move button. */
@@ -23326,7 +23658,7 @@ ${overlayScrollbarCss(".vela-dialog.vela-ind-dialog *", 9)}
23326
23658
  if (!lg) {
23327
23659
  lg = document.createElement("div");
23328
23660
  lg.dataset.velaPane = paneId;
23329
- lg.style.cssText = "position:absolute;left:10px;z-index:5;display:flex;flex-direction:column;align-items:flex-start;gap:0;pointer-events:none;font:12px -apple-system,Segoe UI,sans-serif;";
23661
+ lg.style.cssText = `position:absolute;left:${LEGEND_LEFT_PX}px;z-index:5;display:flex;flex-direction:column;align-items:flex-start;gap:0;pointer-events:none;font:12px -apple-system,Segoe UI,sans-serif;`;
23330
23662
  applyChromeTokens(lg, this.theme);
23331
23663
  this.positionLegend(lg, paneId);
23332
23664
  this.container.appendChild(lg);
@@ -23338,6 +23670,7 @@ ${overlayScrollbarCss(".vela-dialog.vela-ind-dialog *", 9)}
23338
23670
  const bounds = this.paneBoundsOf ? this.paneBoundsOf(paneId) : { top: 0, height: Infinity };
23339
23671
  lg.toggleAttribute(LEGEND_AT_TOP_ATTR, bounds.top === 0);
23340
23672
  lg.style.display = bounds.height < 4 || !this.titlesVisible ? "none" : "flex";
23673
+ lg.style.maxWidth = `calc(100% - ${(bounds.rightAxis ?? 0) + LEGEND_LEFT_PX + LEGEND_SCALE_GAP_PX}px)`;
23341
23674
  const collapsed = this.paneCollapse.has(paneId);
23342
23675
  const masterId = this.paneCollapse.get(paneId) ?? null;
23343
23676
  for (const row of this.rows.values()) {
@@ -23429,7 +23762,7 @@ ${overlayScrollbarCss(".vela-dialog.vela-ind-dialog *", 9)}
23429
23762
  }
23430
23763
  ensureDialogStyles();
23431
23764
  const el = document.createElement("div");
23432
- el.style.cssText = `pointer-events:auto;display:flex;align-items:center;background:${this.idleRowFill()};border-radius:4px;padding:${LEGEND_ROW_PAD_Y}px ${LEGEND_ROW_PAD_X}px;margin-left:-${LEGEND_ROW_PAD_X}px;min-height:${LEGEND_ROW_MIN_H}px;box-sizing:border-box;color:${this.theme.textColor};user-select:none;-webkit-user-select:none;`;
23765
+ el.style.cssText = `pointer-events:auto;display:flex;align-items:center;background:${this.idleRowFill()};border-radius:4px;padding:${LEGEND_ROW_PAD_Y}px ${LEGEND_ROW_PAD_X}px;margin-left:-${LEGEND_ROW_PAD_X}px;max-width:calc(100% + ${LEGEND_ROW_PAD_X}px);min-height:${LEGEND_ROW_MIN_H}px;box-sizing:border-box;color:${this.theme.textColor};user-select:none;-webkit-user-select:none;`;
23433
23766
  el.addEventListener("mouseleave", () => {
23434
23767
  if (this.selectedId !== id) this.setRowHighlighted(id, false);
23435
23768
  });
@@ -23457,7 +23790,7 @@ ${overlayScrollbarCss(".vela-dialog.vela-ind-dialog *", 9)}
23457
23790
  const statusEl = document.createElement("span");
23458
23791
  statusEl.style.cssText = "display:none;box-sizing:border-box;flex:none;";
23459
23792
  const titleWrap = document.createElement("span");
23460
- titleWrap.style.cssText = "white-space:nowrap;";
23793
+ titleWrap.style.cssText = "white-space:nowrap;min-width:0;overflow:hidden;text-overflow:ellipsis;";
23461
23794
  titleWrap.addEventListener("mouseenter", () => this.setRowHighlighted(id, true));
23462
23795
  const titleEl = document.createElement("span");
23463
23796
  titleEl.textContent = title;
@@ -23475,7 +23808,7 @@ ${overlayScrollbarCss(".vela-dialog.vela-ind-dialog *", 9)}
23475
23808
  el.appendChild(calloutsEl);
23476
23809
  el.appendChild(statusEl);
23477
23810
  const valuesEl = document.createElement("span");
23478
- valuesEl.style.cssText = `display:none;align-items:center;gap:5px;margin-left:${LEGEND_TITLE_VALUES_GAP_PX}px;white-space:nowrap;font-variant-numeric:tabular-nums;`;
23811
+ valuesEl.style.cssText = `display:none;flex:1 1 0%;min-width:0;overflow:hidden;text-overflow:ellipsis;margin-left:${LEGEND_TITLE_VALUES_GAP_PX}px;white-space:nowrap;font-variant-numeric:tabular-nums;`;
23479
23812
  el.appendChild(valuesEl);
23480
23813
  const controlsEl = document.createElement("span");
23481
23814
  controlsEl.style.cssText = `display:none;align-items:center;gap:${LEGEND_ACTION_GAP_PX}px;flex:none;margin-left:${LEGEND_ACTION_GAP_PX}px;`;
@@ -24648,15 +24981,23 @@ ${overlayScrollbarCss(".vela-dialog.vela-ind-dialog *", 9)}
24648
24981
  this.seg(x + w, y + h, x, y + h, lineWidth, c);
24649
24982
  this.seg(x, y + h, x, y, lineWidth, c);
24650
24983
  }
24651
- /** Filled circle (triangle fan). */
24984
+ /**
24985
+ * Filled circle (triangle fan), feathered like a line: the rim is padded by {@link AA_PAD} and
24986
+ * edgeDist runs from 0 at the center to the padded radius at the rim, with edgeHalf = r. The
24987
+ * distance grows linearly along each spoke, so the shader's line feather gives a round soft edge.
24988
+ */
24652
24989
  circle(cx, cy, r, c, segments = 16) {
24653
- let px = cx + r;
24990
+ const ext = r + AA_PAD;
24991
+ let px = cx + ext;
24654
24992
  let py = cy;
24993
+ this.ensure(segments * 3);
24655
24994
  for (let i = 1; i <= segments; i += 1) {
24656
24995
  const a = i / segments * Math.PI * 2;
24657
- const nx = cx + Math.cos(a) * r;
24658
- const ny = cy + Math.sin(a) * r;
24659
- this.tri(cx, cy, c, px, py, c, nx, ny, c);
24996
+ const nx = cx + Math.cos(a) * ext;
24997
+ const ny = cy + Math.sin(a) * ext;
24998
+ this.v(cx, cy, c, 0, r);
24999
+ this.v(px, py, c, ext, r);
25000
+ this.v(nx, ny, c, ext, r);
24660
25001
  px = nx;
24661
25002
  py = ny;
24662
25003
  }
@@ -24837,7 +25178,7 @@ void main() {
24837
25178
  }
24838
25179
  mount(canvas) {
24839
25180
  this.canvas = canvas;
24840
- this.gl = canvas.getContext("webgl2", { alpha: true, antialias: true, depth: false, stencil: false, premultipliedAlpha: true });
25181
+ this.gl = canvas.getContext("webgl2", { alpha: true, antialias: false, depth: false, stencil: false, premultipliedAlpha: true });
24841
25182
  if (!this.gl) return;
24842
25183
  if (!this.initGL()) {
24843
25184
  this.gl = null;
@@ -26062,6 +26403,11 @@ void main() {
26062
26403
  /** When true (the default), the separators between stacked panes are draggable to
26063
26404
  * resize the panes above/below. When false, a press there is a normal data-area pan. */
26064
26405
  this.paneResize = true;
26406
+ /** When true (the default), the user drawings take part in pointer input: they claim a
26407
+ * press over a drawing or with a tool armed, and see hover moves. When false, every press
26408
+ * is a plain pan/click and hover leaves them alone; a drawing gesture already under way
26409
+ * still finishes. */
26410
+ this.drawings = true;
26065
26411
  this.el = null;
26066
26412
  this.dragging = false;
26067
26413
  this.moved = false;
@@ -26125,18 +26471,18 @@ void main() {
26125
26471
  this.onModifier = (e) => {
26126
26472
  if (e.repeat || e.key !== "Shift" && e.key !== "Control" && e.key !== "Meta") return;
26127
26473
  if (Number.isNaN(this.cursorX)) return;
26128
- if (this.dragging && this.region !== "drawing") return;
26474
+ if (this.dragging ? this.region !== "drawing" : !this.drawings) return;
26129
26475
  this.deps.drawingsPointerMove?.(this.cursorX, this.cursorY, this.snapMode(e), e.shiftKey, e.ctrlKey || e.metaKey);
26130
26476
  };
26131
26477
  this.onDown = (e) => {
26132
26478
  if (e.button === 1) {
26133
26479
  const { x: x2, y: y2 } = this.local(e);
26134
- this.middleDeleted = this.deps.drawingsDeleteAt?.(x2, y2) ?? false;
26480
+ this.middleDeleted = this.drawings && (this.deps.drawingsDeleteAt?.(x2, y2) ?? false);
26135
26481
  if (this.middleDeleted) e.preventDefault();
26136
26482
  return;
26137
26483
  }
26138
26484
  if (e.button === 2) {
26139
- this.rightCancelled = this.deps.drawingsCancelPlacement?.() ?? false;
26485
+ this.rightCancelled = this.drawings && (this.deps.drawingsCancelPlacement?.() ?? false);
26140
26486
  if (this.rightCancelled) e.preventDefault();
26141
26487
  return;
26142
26488
  }
@@ -26158,20 +26504,20 @@ void main() {
26158
26504
  this.startY = y;
26159
26505
  this.prevPressDrawing = this.lastPressDrawing;
26160
26506
  this.lastPressDrawing = false;
26161
- if (this.deps.drawingsClaim?.(x, y)) {
26507
+ if (this.drawings && this.deps.drawingsClaim?.(x, y)) {
26162
26508
  this.region = "drawing";
26163
26509
  this.lastPressDrawing = true;
26164
26510
  this.deps.drawingsPointerDown?.(x, y, this.snapMode(e), e.shiftKey, e.ctrlKey || e.metaKey);
26165
26511
  this.capture(e.pointerId);
26166
26512
  return;
26167
26513
  }
26168
- if (e.shiftKey && this.regionAt(x, y) === "data" && this.deps.drawingsMeasureStart?.(x, y, this.snapMode(e))) {
26514
+ if (this.drawings && e.shiftKey && this.regionAt(x, y) === "data" && this.deps.drawingsMeasureStart?.(x, y, this.snapMode(e))) {
26169
26515
  this.region = "drawing";
26170
26516
  this.lastPressDrawing = true;
26171
26517
  this.capture(e.pointerId);
26172
26518
  return;
26173
26519
  }
26174
- if ((e.ctrlKey || e.metaKey) && this.regionAt(x, y) === "data" && this.deps.drawingsMarqueeStart?.(x, y)) {
26520
+ if (this.drawings && (e.ctrlKey || e.metaKey) && this.regionAt(x, y) === "data" && this.deps.drawingsMarqueeStart?.(x, y)) {
26175
26521
  this.region = "drawing";
26176
26522
  this.lastPressDrawing = true;
26177
26523
  this.capture(e.pointerId);
@@ -26248,7 +26594,7 @@ void main() {
26248
26594
  const drawCursor = this.deps.drawingsCursor?.(x, y);
26249
26595
  const r = this.regionAt(x, y);
26250
26596
  this.el.style.cursor = drawCursor ?? (r === "price" ? "ns-resize" : r === "time" ? "ew-resize" : r === "separator" ? "row-resize" : "");
26251
- this.deps.drawingsPointerMove?.(x, y, this.snapMode(e), e.shiftKey, e.ctrlKey || e.metaKey);
26597
+ if (this.drawings) this.deps.drawingsPointerMove?.(x, y, this.snapMode(e), e.shiftKey, e.ctrlKey || e.metaKey);
26252
26598
  }
26253
26599
  if (e.pointerType !== "touch") this.deps.onPointerMove(x, y);
26254
26600
  };
@@ -26289,7 +26635,7 @@ void main() {
26289
26635
  this.deps.drawingsPointerUp?.(x, y, this.snapMode(e));
26290
26636
  } else if (tapRelease && this.region === "data") {
26291
26637
  this.deps.onClick(x, y);
26292
- } else if (this.dragging && this.region === "data") {
26638
+ } else if (this.dragging && this.region === "data" && wasTouch) {
26293
26639
  const stale = e.timeStamp - this.lastT > FLING_STALE_MS;
26294
26640
  if (!stale && Math.abs(this.vx) > FLING_MIN_SPEED) {
26295
26641
  const pitch = this.deps.getCoords().pxPerBar();
@@ -26490,7 +26836,7 @@ void main() {
26490
26836
  }
26491
26837
  /** Shared double-click / double-tap routing, by the region under the point. */
26492
26838
  doubleActivate(x, y) {
26493
- if (this.deps.drawingsDblClick?.(x, y)) return;
26839
+ if (this.drawings && this.deps.drawingsDblClick?.(x, y)) return;
26494
26840
  const region = this.regionAt(x, y);
26495
26841
  if (region === "price") this.deps.resetPriceScale(x, y);
26496
26842
  else if (region === "separator") this.deps.resetPaneSize(y);
@@ -28504,10 +28850,13 @@ void main() {
28504
28850
  const norm = raw / mag;
28505
28851
  const step = (norm < 1.5 ? 1 : norm < 3 ? 2 : norm < 7 ? 5 : 10) * mag;
28506
28852
  const decimals = Math.max(0, -Math.floor(Math.log10(step)) + 1);
28507
- const out = [];
28508
28853
  const start = Math.ceil(min2 / step) * step;
28509
- for (let v = start; v <= max2 + step * 1e-6; v += step) {
28510
- out.push(Number(v.toFixed(decimals)));
28854
+ if (!(step > 0) || start + step === start) return [];
28855
+ const count = Math.floor((max2 - start) / step + 1e-6) + 1;
28856
+ if (!(count > 0) || count > 1e3) return [];
28857
+ const out = [];
28858
+ for (let i = 0; i < count; i += 1) {
28859
+ out.push(Number((start + i * step).toFixed(decimals)));
28511
28860
  }
28512
28861
  return out;
28513
28862
  }
@@ -28705,6 +29054,100 @@ void main() {
28705
29054
  return out;
28706
29055
  }
28707
29056
 
29057
+ // src/renderers/native/chrome/priceAxisTicks.ts
29058
+ var PriceAxisTickSource = class {
29059
+ constructor() {
29060
+ this.fn = null;
29061
+ this.memo = /* @__PURE__ */ new WeakMap();
29062
+ this.warned = false;
29063
+ }
29064
+ /** The host function, or null for the built-in ticks. */
29065
+ get hook() {
29066
+ return this.fn;
29067
+ }
29068
+ /** Set (or clear) the host function. Drops every memoized result, so setting the same
29069
+ * function again recomputes — how a host refreshes ticks that depend on its own state. */
29070
+ setHook(fn) {
29071
+ this.fn = fn;
29072
+ this.memo = /* @__PURE__ */ new WeakMap();
29073
+ }
29074
+ /** The pane's master-scale ticks for this frame. An unscaled pane (`axisFormat: 'none'`)
29075
+ * gets none, and the host is never asked for it. */
29076
+ ticksFor(scene, pane, coords) {
29077
+ const format = pane.axisFormat;
29078
+ if (format === "none") return [];
29079
+ const pct = percentScaleFor(scene, pane);
29080
+ const height = pane.bounds.height;
29081
+ const fn = this.fn;
29082
+ if (!fn || !(height > 0)) return paneAxisTicks(pane.scale, height, pct, scene.priceMintick, format);
29083
+ const s = pane.scale;
29084
+ const m = this.memo.get(pane);
29085
+ const fontSize = scene.style.fontSize;
29086
+ if (m && m.kind === pane.kind && m.min === s.min && m.max === s.max && m.log === !!s.log && m.invert === !!s.invert && m.height === height && m.baseline === pct?.baseline && m.indexed === !!pct?.indexed && m.mintick === scene.priceMintick && m.format === format && m.fontSize === fontSize) {
29087
+ return m.ticks;
29088
+ }
29089
+ const defaults2 = paneAxisTicks(s, height, pct, scene.priceMintick, format);
29090
+ const ticks = this.callHook(fn, scene, pane, coords, defaults2, pct?.baseline, pct ? pct.indexed ? "indexed" : "percent" : "price");
29091
+ this.memo.set(pane, {
29092
+ kind: pane.kind,
29093
+ min: s.min,
29094
+ max: s.max,
29095
+ log: !!s.log,
29096
+ invert: !!s.invert,
29097
+ height,
29098
+ baseline: pct?.baseline,
29099
+ indexed: !!pct?.indexed,
29100
+ mintick: scene.priceMintick,
29101
+ format,
29102
+ fontSize,
29103
+ ticks
29104
+ });
29105
+ return ticks;
29106
+ }
29107
+ callHook(fn, scene, pane, coords, defaults2, baseline, mode) {
29108
+ const scale = { min: pane.scale.min, max: pane.scale.max, log: !!pane.scale.log, invert: !!pane.scale.invert };
29109
+ const bounds = { top: 0, height: pane.bounds.height };
29110
+ const ctx = {
29111
+ pane: { id: pane.id, kind: pane.kind },
29112
+ min: Math.min(scale.min, scale.max),
29113
+ max: Math.max(scale.min, scale.max),
29114
+ log: !!scale.log,
29115
+ height: bounds.height,
29116
+ mode,
29117
+ baseline,
29118
+ mintick: scene.priceMintick,
29119
+ fontSize: scene.style.fontSize,
29120
+ priceToY: (price) => coords.priceToY(price, scale, bounds),
29121
+ defaults: defaults2
29122
+ };
29123
+ let out;
29124
+ try {
29125
+ out = fn(ctx);
29126
+ } catch (err) {
29127
+ this.warnOnce("threw", err);
29128
+ return defaults2;
29129
+ }
29130
+ if (out == null) return defaults2;
29131
+ if (!Array.isArray(out)) {
29132
+ this.warnOnce("returned a non-array", out);
29133
+ return defaults2;
29134
+ }
29135
+ const ticks = [];
29136
+ for (const t of out) {
29137
+ if (!t || typeof t !== "object") continue;
29138
+ const { price, label, major } = t;
29139
+ if (typeof price !== "number" || !Number.isFinite(price) || typeof label !== "string") continue;
29140
+ ticks.push(typeof major === "boolean" ? { price, label, major } : { price, label });
29141
+ }
29142
+ return ticks;
29143
+ }
29144
+ warnOnce(what, detail) {
29145
+ if (this.warned) return;
29146
+ this.warned = true;
29147
+ console.warn(`[vela] the priceAxisTicks function ${what} \u2014 the default ticks are used instead.`, detail);
29148
+ }
29149
+ };
29150
+
28708
29151
  // src/renderers/native/chrome/countdown.ts
28709
29152
  function countdownText(barOpen, barMs, now2) {
28710
29153
  if (!(barMs > 0)) return null;
@@ -29057,8 +29500,11 @@ void main() {
29057
29500
  }
29058
29501
 
29059
29502
  // src/renderers/native/chrome/ChromeRenderer.ts
29503
+ var MINOR_LABEL_ALPHA = 0.5;
29060
29504
  var ChromeRenderer = class {
29061
- constructor() {
29505
+ /** `axisTicks` is shared with the backdrop layer, so price labels and gridlines agree. */
29506
+ constructor(axisTicks = new PriceAxisTickSource()) {
29507
+ this.axisTicks = axisTicks;
29062
29508
  this.canvas = null;
29063
29509
  this.ctx = null;
29064
29510
  // The color for axis tick labels — the host-passed surface text, set each frame in render().
@@ -29163,7 +29609,8 @@ void main() {
29163
29609
  this.drawTimeAxis(ctx, scene, coords, theme, dataW, dataH, fullH);
29164
29610
  this.drawMarkLane(ctx, scene, coords, theme, dataW, dataH);
29165
29611
  }
29166
- /** The timeline-mark lane — after the axis, so the tokens read over the plot's bottom edge. */
29612
+ /** The timeline-mark lane — after the axis, so the tokens read over the plot's bottom edge,
29613
+ * but clipped to the data area so a glyph on the edge bar never paints over the price scale. */
29167
29614
  drawMarkLane(ctx, scene, coords, theme, dataW, dataH) {
29168
29615
  if (!scene.marks.visible || scene.timelineMarks.length === 0) {
29169
29616
  this.markLayout = { glyphs: [], stacks: /* @__PURE__ */ new Map() };
@@ -29181,6 +29628,10 @@ void main() {
29181
29628
  expanded: scene.marksExpandedStack
29182
29629
  });
29183
29630
  const nowMs = typeof performance !== "undefined" ? performance.now() : Date.now();
29631
+ ctx.save();
29632
+ ctx.beginPath();
29633
+ ctx.rect(0, 0, dataW, dataH);
29634
+ ctx.clip();
29184
29635
  paintMarkLane(ctx, this.markLayout, {
29185
29636
  axisY: dataH,
29186
29637
  background: theme.background,
@@ -29194,6 +29645,7 @@ void main() {
29194
29645
  flashKey: scene.marksFlash && scene.marksFlash.until > nowMs ? scene.marksFlash.key : null,
29195
29646
  nowMs
29196
29647
  });
29648
+ ctx.restore();
29197
29649
  }
29198
29650
  barTimes(scene) {
29199
29651
  if (this.barTimesSrc !== scene.bars || this.barTimesCache.length !== scene.bars.length) {
@@ -29250,13 +29702,19 @@ void main() {
29250
29702
  if (!scene.showAxisLabels) return;
29251
29703
  ctx.fillStyle = this.axisTextColor;
29252
29704
  ctx.textAlign = "left";
29705
+ const font = `${scene.style.fontSize}px ${theme.fontFamily}`;
29706
+ const majorFont = `600 ${font}`;
29707
+ const alpha = ctx.globalAlpha;
29253
29708
  for (const pane of panes) {
29254
29709
  if (pane.collapsed) continue;
29255
- const pct = percentScaleFor(scene, pane);
29256
- for (const t of paneAxisTicks(pane.scale, pane.bounds.height, pct, scene.priceMintick, pane.axisFormat)) {
29710
+ for (const t of this.axisTicks.ticksFor(scene, pane, coords)) {
29257
29711
  const y = coords.priceToY(t.price, pane.scale, pane.bounds);
29258
29712
  if (y < pane.bounds.top + 6 || y > pane.bounds.top + pane.bounds.height - 4) continue;
29713
+ if (t.major === true) ctx.font = majorFont;
29714
+ else if (t.major === false) ctx.globalAlpha = alpha * MINOR_LABEL_ALPHA;
29259
29715
  ctx.fillText(t.label, dataW + 6, y);
29716
+ if (t.major === true) ctx.font = font;
29717
+ else if (t.major === false) ctx.globalAlpha = alpha;
29260
29718
  }
29261
29719
  if (pane.axisBands) {
29262
29720
  for (const b of pane.axisBands) {
@@ -29498,15 +29956,23 @@ void main() {
29498
29956
  constructor() {
29499
29957
  this.canvas = null;
29500
29958
  this.ctx = null;
29959
+ this.shade = null;
29501
29960
  }
29502
29961
  mount(canvas) {
29503
29962
  this.canvas = canvas;
29504
29963
  this.ctx = canvas.getContext("2d");
29505
29964
  }
29965
+ /** The plot area the last frame veiled for `crosshairOverride.shadeRight` — from the
29966
+ * picked bar's right edge to the price scale, full plot height (the union when both the
29967
+ * local and a synced crosshair veil) — or null when no veil was painted. */
29968
+ get shadeRightArea() {
29969
+ return this.shade;
29970
+ }
29506
29971
  /** Clear the cursor canvas and (re)draw the crosshair lines + axis chips. The optional
29507
29972
  * `separatorHoverY` highlights the draggable pane separator under the cursor;
29508
29973
  * `external` is a SYNCED ghost crosshair (another chart's pointer, pixel-resolved). */
29509
29974
  render(scene, coords, theme, separatorHoverY = null, external = null) {
29975
+ this.shade = null;
29510
29976
  const ctx = this.ctx;
29511
29977
  const canvas = this.canvas;
29512
29978
  if (!ctx || !canvas) return;
@@ -29529,12 +29995,7 @@ void main() {
29529
29995
  const x = crisp(coords.logicalToX(logical), ov?.width ?? cs.width);
29530
29996
  if (vertical && ov?.shadeRight) {
29531
29997
  const from = Math.max(0, Math.round(coords.logicalToX(logical + 0.5)));
29532
- if (from < dataW) {
29533
- ctx.fillStyle = ov.shadeRight.color;
29534
- ctx.globalAlpha = ov.shadeRight.opacity ?? 1;
29535
- ctx.fillRect(from, 0, dataW - from, dataH);
29536
- ctx.globalAlpha = 1;
29537
- }
29998
+ if (from < dataW) this.paintShade(ctx, ov.shadeRight, from, dataW, dataH);
29538
29999
  }
29539
30000
  ctx.strokeStyle = ov?.color ?? cs.color ?? theme.textColor;
29540
30001
  ctx.lineWidth = ov?.width ?? cs.width;
@@ -29570,6 +30031,7 @@ void main() {
29570
30031
  destroy() {
29571
30032
  this.canvas = null;
29572
30033
  this.ctx = null;
30034
+ this.shade = null;
29573
30035
  }
29574
30036
  /** The synced ghost: a dimmed vertical line at the bar the renderer resolved as
29575
30037
  * CONTAINING the foreign time (+ horizontal line when a comparable price came
@@ -29589,12 +30051,7 @@ void main() {
29589
30051
  if (ov?.vertical === false) return;
29590
30052
  if (ov?.shadeRight) {
29591
30053
  const from = Math.max(0, Math.round(coords.logicalToX(Math.round(coords.xToLogical(ext.x)) + 0.5)));
29592
- if (from < dataW) {
29593
- ctx.fillStyle = ov.shadeRight.color;
29594
- ctx.globalAlpha = ov.shadeRight.opacity ?? 1;
29595
- ctx.fillRect(from, 0, dataW - from, dataH);
29596
- ctx.globalAlpha = 1;
29597
- }
30054
+ if (from < dataW) this.paintShade(ctx, ov.shadeRight, from, dataW, dataH);
29598
30055
  }
29599
30056
  if (ext.line === false || x < 0 || x > dataW) return;
29600
30057
  ctx.font = `${scene.style.fontSize}px ${theme.fontFamily}`;
@@ -29630,6 +30087,15 @@ void main() {
29630
30087
  this.chip(ctx, x, dataH + 1, formatTimeStamp(ext.time, scene.timezone, coords.barInterval), chipBg, "center", true, theme.background);
29631
30088
  ctx.globalAlpha = 1;
29632
30089
  }
30090
+ /** Paint the `shadeRight` veil from `from` to the price scale and record the veiled area. */
30091
+ paintShade(ctx, shade, from, dataW, dataH) {
30092
+ ctx.fillStyle = shade.color;
30093
+ ctx.globalAlpha = shade.opacity ?? 1;
30094
+ ctx.fillRect(from, 0, dataW - from, dataH);
30095
+ ctx.globalAlpha = 1;
30096
+ const x = this.shade ? Math.min(this.shade.x, from) : from;
30097
+ this.shade = { x, width: dataW - x, height: dataH };
30098
+ }
29633
30099
  /** A soft band + a brighter crisp center line over the hovered separator, so it reads as
29634
30100
  * a draggable handle (the cursor is already `row-resize`). Spans the full width (data +
29635
30101
  * scale gutter) to match the separator itself. Theme-derived (textColor). */
@@ -32745,6 +33211,12 @@ ${overlayScrollbarCss(".vela-sd-pane")}
32745
33211
  activeDragId() {
32746
33212
  return this.state.kind === "pressed" && this.state.moved ? this.state.id : null;
32747
33213
  }
33214
+ /** The store drawings a live drag is moving — the pressed one and its riders — or none
33215
+ * before the drag passes the slop. A Ctrl-drag moves copies, so its sources stay put. */
33216
+ movingIds() {
33217
+ if (this.state.kind !== "pressed" || !this.state.moved || this.state.clones) return /* @__PURE__ */ new Set();
33218
+ return /* @__PURE__ */ new Set([this.state.id, ...this.state.riders.map((r) => r.id)]);
33219
+ }
32748
33220
  /** The drawing under a press that has not been released yet (drag or click undecided), or null. */
32749
33221
  pressedId() {
32750
33222
  return this.state.kind === "pressed" ? this.state.id : null;
@@ -33119,15 +33591,16 @@ ${overlayScrollbarCss(".vela-sd-pane")}
33119
33591
  }
33120
33592
  open(drawing, actions, kind) {
33121
33593
  this.close();
33122
- const snapshot2 = clonePlain(drawing.serialize());
33594
+ const live = actions.resolve() ?? drawing;
33595
+ const snapshot2 = clonePlain(live.serialize());
33123
33596
  const grid = fieldGrid({ variant: "inputs" });
33124
33597
  grid.style.padding = "16px 20px";
33125
33598
  grid.style.overflowY = "auto";
33126
33599
  grid.style.overflowX = "hidden";
33127
33600
  grid.style.flex = "1 1 auto";
33128
- if (kind === "position" && drawing instanceof PositionTool) this.buildPosition(grid, drawing, actions);
33129
- else if (kind === "frvp" && drawing instanceof FixedRangeVolumeProfile) this.buildFrvp(grid, drawing, actions);
33130
- else if (kind === "levels") this.buildLevels(grid, drawing, actions);
33601
+ if (kind === "position" && live instanceof PositionTool) this.buildPosition(grid, live, actions);
33602
+ else if (kind === "frvp" && live instanceof FixedRangeVolumeProfile) this.buildFrvp(grid, live, actions);
33603
+ else if (kind === "levels") this.buildLevels(grid, live, actions);
33131
33604
  else return;
33132
33605
  const ui = new Dialog({
33133
33606
  host: this.host,
@@ -33403,6 +33876,17 @@ ${overlayScrollbarCss(".vela-sd-pane")}
33403
33876
  }
33404
33877
  }));
33405
33878
  }
33879
+ if (drawing instanceof FibRetracement) {
33880
+ const fib = drawing;
33881
+ grid.appendChild(fieldRow({
33882
+ label: "Reverse",
33883
+ bool: true,
33884
+ toggle: {
33885
+ checked: fib.reverse,
33886
+ onChange: (v) => actions.patch({ reverse: v })
33887
+ }
33888
+ }));
33889
+ }
33406
33890
  levels.forEach((lv, i) => {
33407
33891
  const row = document.createElement("div");
33408
33892
  row.className = "vela-field-span";
@@ -33763,7 +34247,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
33763
34247
  this.reposition();
33764
34248
  return;
33765
34249
  }
33766
- const text = drawing.text;
34250
+ const text = (actions.resolve() ?? drawing).text;
33767
34251
  const panel = document.createElement("div");
33768
34252
  panel.style.cssText = `padding:6px;border-top:1px solid var(--vela-border);display:flex;flex-direction:column;gap:4px;`;
33769
34253
  const ta = new TextArea({
@@ -34359,6 +34843,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
34359
34843
  // src/renderers/native/drawings/DrawingToolbar.ts
34360
34844
  var TOOLBAR_WIDTH = 44;
34361
34845
  var TOOLBAR_COLLAPSED_WIDTH = 16;
34846
+ var FLYOUT_GAP = 3;
34362
34847
  var DrawingToolbar = class {
34363
34848
  constructor(host, theme, onArm, onMagnet = () => {
34364
34849
  }, onMeasure = () => {
@@ -34388,6 +34873,9 @@ ${overlayScrollbarCss(".vela-sd-pane")}
34388
34873
  // group id (or MAGNET_ID) whose flyout is open
34389
34874
  this.flyoutCell = null;
34390
34875
  // the cell the open flyout is anchored to
34876
+ /** Closed flyouts still playing their exit animation, by owner id — reopening the same
34877
+ * owner takes its element back instead of stacking a second one on it. */
34878
+ this.exitingFlyouts = /* @__PURE__ */ new Map();
34391
34879
  this.groupCells = /* @__PURE__ */ new Map();
34392
34880
  // the composite cell (hover/active bg + flyout anchor)
34393
34881
  this.groupIcons = /* @__PURE__ */ new Map();
@@ -34496,6 +34984,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
34496
34984
  }
34497
34985
  destroy() {
34498
34986
  this.closeFlyout();
34987
+ for (const { exit } of [...this.exitingFlyouts.values()]) exit.finish();
34499
34988
  for (const dispose of this.tipDisposers.splice(0)) dispose();
34500
34989
  this.root.remove();
34501
34990
  }
@@ -34723,16 +35212,22 @@ ${overlayScrollbarCss(".vela-sd-pane")}
34723
35212
  * outside-dismiss. Callers fill it with items. */
34724
35213
  beginFlyout(ownerId, cell) {
34725
35214
  const t = this.theme;
34726
- const fly = document.createElement("div");
35215
+ const back = this.exitingFlyouts.get(ownerId);
35216
+ if (back) {
35217
+ this.exitingFlyouts.delete(ownerId);
35218
+ back.exit.cancel();
35219
+ back.fly.replaceChildren();
35220
+ }
35221
+ const fly = back?.fly ?? document.createElement("div");
34727
35222
  fly.className = "vela-dtb-flyout";
34728
- fly.style.cssText = `position:absolute;z-index:23;display:flex;flex-direction:column;gap:2px;padding:4px;border-radius:0 8px 8px 0;background:var(--vela-surface-elev);border:1px solid ${this.borderColor};border-left:none;box-shadow:var(--vela-shadow);pointer-events:auto;overflow-y:auto;overscroll-behavior:contain;`;
35223
+ fly.style.cssText = `position:absolute;z-index:23;display:flex;flex-direction:column;gap:2px;padding:4px;border-radius:var(--vela-radius-md);background:var(--vela-surface);border:1px solid ${this.borderOverride ?? "var(--vela-border-strong)"};box-shadow:var(--vela-shadow);pointer-events:auto;overflow-y:auto;overscroll-behavior:contain;`;
34729
35224
  applyChromeTokens(fly, t);
34730
35225
  this.host.appendChild(fly);
34731
35226
  const r = cell.getBoundingClientRect();
34732
35227
  const rootR = this.root.getBoundingClientRect();
34733
35228
  const hostR = this.host.getBoundingClientRect();
34734
35229
  const top = r.top - hostR.top;
34735
- fly.style.left = `${rootR.right - hostR.left - 1}px`;
35230
+ fly.style.left = `${rootR.right - hostR.left + FLYOUT_GAP}px`;
34736
35231
  fly.style.top = `${top}px`;
34737
35232
  fly.style.maxHeight = `${Math.max(120, hostR.height - top - 8)}px`;
34738
35233
  this.flyout = fly;
@@ -34748,6 +35243,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
34748
35243
  if (!cell) return;
34749
35244
  const fly = this.beginFlyout(group.id, cell);
34750
35245
  const sections = group.sections ?? [{ label: "", tools: group.tools }];
35246
+ const badge = sections.some((s) => s.tools.some((tool) => !!tool.icon));
34751
35247
  for (let si = 0; si < sections.length; si++) {
34752
35248
  const section = sections[si];
34753
35249
  if (section.label) {
@@ -34758,6 +35254,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
34758
35254
  fly.appendChild(
34759
35255
  this.makeFlyoutItem({
34760
35256
  icon: tool.icon,
35257
+ badge,
34761
35258
  label: tool.label,
34762
35259
  selected: tool.type === this.active,
34763
35260
  shortcut: this.shortcuts.get(tool.type),
@@ -34770,6 +35267,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
34770
35267
  );
34771
35268
  }
34772
35269
  }
35270
+ announceSurface(fly, true, "menu", cell);
34773
35271
  }
34774
35272
  openMagnetFlyout() {
34775
35273
  const cell = this.magnetCell;
@@ -34793,6 +35291,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
34793
35291
  })
34794
35292
  );
34795
35293
  }
35294
+ announceSurface(fly, true, "menu", cell);
34796
35295
  }
34797
35296
  flyoutHeader(text) {
34798
35297
  const t = this.theme;
@@ -34806,21 +35305,24 @@ ${overlayScrollbarCss(".vela-sd-pane")}
34806
35305
  div.style.cssText = `height:1px;margin:4px 8px;background:${this.borderColor};`;
34807
35306
  return div;
34808
35307
  }
34809
- /** A flyout row, left to right: optional leading icon, label, a check when it's the selected
34810
- * entry, an optional shortcut hint, and — at the far right — the favorite star (tool rows).
34811
- * Hover tint is CSS (`.vela-dtb-item:hover`), so navigating the menu stays smooth. */
35308
+ /** A flyout row, left to right: optional leading icon badge, label, a check when it's the
35309
+ * selected entry, an optional shortcut hint, and — at the far right — the favorite star (tool
35310
+ * rows). Hover tint is CSS (`.vela-dtb-item:hover`), so navigating the menu stays smooth. */
34812
35311
  makeFlyoutItem(opts) {
34813
35312
  const t = this.theme;
34814
35313
  const item = document.createElement("button");
34815
35314
  item.type = "button";
34816
35315
  item.className = "vela-dtb-item";
34817
35316
  item.setAttribute("aria-label", opts.label);
34818
- item.style.cssText = `display:flex;align-items:center;gap:8px;padding:5px 10px 5px 8px;cursor:pointer;color:${t.textColor};border-radius:var(--vela-radius-sm);font:13px ${t.fontFamily};white-space:nowrap;min-width:148px;`;
34819
- if (opts.icon) {
34820
- const icon2 = document.createElement("span");
34821
- icon2.style.cssText = "width:18px;height:18px;display:flex;align-items:center;justify-content:center;flex:none;";
34822
- icon2.innerHTML = sizedIcon(opts.icon);
34823
- item.appendChild(icon2);
35317
+ const badged = opts.badge === true || !!opts.icon;
35318
+ const box = badged ? "gap:10px;padding:4px 10px 4px 6px;" : "gap:8px;padding:5px 10px 5px 8px;";
35319
+ item.style.cssText = `display:flex;align-items:center;${box}cursor:pointer;color:${t.textColor};border-radius:var(--vela-radius-sm);font:13px ${t.fontFamily};white-space:nowrap;min-width:148px;`;
35320
+ if (opts.selected) item.dataset.selected = "1";
35321
+ if (badged) {
35322
+ const badge = document.createElement("span");
35323
+ badge.className = "vela-dtb-badge";
35324
+ if (opts.icon) badge.innerHTML = opts.icon;
35325
+ item.appendChild(badge);
34824
35326
  }
34825
35327
  const label = document.createElement("span");
34826
35328
  label.textContent = opts.label;
@@ -34861,9 +35363,16 @@ ${overlayScrollbarCss(".vela-sd-pane")}
34861
35363
  }
34862
35364
  closeFlyout() {
34863
35365
  this.starEls.clear();
34864
- if (this.flyout) {
34865
- this.flyout.remove();
35366
+ const fly = this.flyout;
35367
+ if (fly) {
34866
35368
  this.flyout = null;
35369
+ announceSurface(fly, false, "menu", this.flyoutCell);
35370
+ const owner = this.flyoutOwnerId ?? "";
35371
+ const exit = holdForExit(fly, () => {
35372
+ if (this.exitingFlyouts.get(owner)?.fly === fly) this.exitingFlyouts.delete(owner);
35373
+ fly.remove();
35374
+ });
35375
+ if (exit) this.exitingFlyouts.set(owner, { fly, exit });
34867
35376
  this.flyoutCell?.classList.remove("vela-open");
34868
35377
  this.flyoutCell = null;
34869
35378
  this.flyoutOwnerId = null;
@@ -34947,6 +35456,11 @@ ${overlayScrollbarCss(".vela-sd-pane")}
34947
35456
  .vela-dtb[data-collapsed='1'] .vela-dtb-collapse .vela-dtb-hit{width:14px;}
34948
35457
  .vela-dtb-item{background:transparent;border:none;transition:background var(--vela-dur-fast) ease;}
34949
35458
  .vela-dtb-item:hover{background:var(--vela-hover-strong);}
35459
+ .vela-dtb-badge{width:24px;height:24px;padding:3px;box-sizing:border-box;display:flex;align-items:center;justify-content:center;flex:none;border-radius:var(--vela-radius-md);background:var(--vela-hover);border:1px solid var(--vela-border);color:var(--vela-fg-muted);transition:background var(--vela-dur-fast) ease,border-color var(--vela-dur-fast) ease,color var(--vela-dur-fast) ease;}
35460
+ .vela-dtb-badge:empty{background:transparent;border-color:transparent;}
35461
+ .vela-dtb-badge svg{width:100%;height:100%;}
35462
+ .vela-dtb-item:hover .vela-dtb-badge:not(:empty){background:var(--vela-active);color:var(--vela-fg-bright);}
35463
+ .vela-dtb-item[data-selected='1'] .vela-dtb-badge:not(:empty){background:color-mix(in srgb,var(--vela-fg-bright) 83%,var(--vela-surface));border-color:color-mix(in srgb,var(--vela-fg-bright) 40%,transparent);color:var(--vela-surface);}
34950
35464
  .vela-dtb-star{width:26px;height:22px;margin:-3px -5px -3px 0;padding:3px 5px;box-sizing:border-box;display:flex;align-items:center;justify-content:center;flex:none;opacity:0;color:inherit;border-radius:var(--vela-radius-sm);transition:opacity .1s ease,color .1s ease,background .1s ease;}
34951
35465
  .vela-dtb-star svg{width:16px;height:16px;}
34952
35466
  .vela-dtb-item:hover .vela-dtb-star{opacity:.55;}
@@ -35188,6 +35702,8 @@ ${overlayScrollbarCss(".vela-sd-pane")}
35188
35702
  this.drawings = [];
35189
35703
  /** Ids painted on an interleave layer this frame — the top canvas paints only their handles. */
35190
35704
  this.sliced = /* @__PURE__ */ new Set();
35705
+ /** Interleaved drawings mid-drag: painted on the top canvas, left out of their slice. */
35706
+ this.lifted = /* @__PURE__ */ new Set();
35191
35707
  /** Cached slice canvases, keyed `paneId|beforeZ`, reused across frames to avoid churn. */
35192
35708
  this.sliceCache = /* @__PURE__ */ new Map();
35193
35709
  /** Series boundaries per pane as of the last `prepareSlices` — lets a repaint between data
@@ -35275,7 +35791,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
35275
35791
  selectedIds: () => this.selectedIds,
35276
35792
  emit: (i) => this.emit(i),
35277
35793
  changed: () => {
35278
- this.invalidateSlices();
35794
+ this.syncLift();
35279
35795
  this.render();
35280
35796
  },
35281
35797
  openSettings: (id, x, y) => this.openSettingsById(id, x, y),
@@ -35564,6 +36080,12 @@ ${overlayScrollbarCss(".vela-sd-pane")}
35564
36080
  this.render();
35565
36081
  }
35566
36082
  }
36083
+ /** Drop the hover (its handles) — the pointer no longer reaches the drawings. */
36084
+ clearHover() {
36085
+ if (this.hoveredId == null) return;
36086
+ this.hoveredId = null;
36087
+ this.render();
36088
+ }
35567
36089
  pointerUp(x, y, snap = "off") {
35568
36090
  if (this.eraserMode) {
35569
36091
  this.erasing = false;
@@ -35901,6 +36423,20 @@ ${overlayScrollbarCss(".vela-sd-pane")}
35901
36423
  /** A drawing that paints inside the series stack changed (content, not hover): its pixels
35902
36424
  * live in the backend composite, so this layer alone can't show the change — ask for a
35903
36425
  * data frame, which re-runs `prepareSlices` before the backend composites. */
36426
+ /** A drawing being dragged leaves its interleave slice for the top canvas, so each pointer
36427
+ * move repaints one 2D layer instead of re-rasterizing and re-uploading a plot-sized slice
36428
+ * texture through the data frame. The slices rebuild once as the drag starts and once as it
36429
+ * ends (release, cancel), when the lifted set changes. */
36430
+ syncLift() {
36431
+ const moving = this.interaction.movingIds();
36432
+ let same = moving.size === this.lifted.size;
36433
+ if (same) {
36434
+ for (const id of moving) if (!this.lifted.has(id)) same = false;
36435
+ }
36436
+ if (same) return;
36437
+ this.lifted = new Set(moving);
36438
+ this.invalidateSlices();
36439
+ }
35904
36440
  invalidateSlices() {
35905
36441
  if (this.sliced.size > 0 || this.drawings.some((d) => this.isInterleaved(d))) this.deps.requestDataPaint();
35906
36442
  }
@@ -35919,7 +36455,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
35919
36455
  const theme = this.deps.theme();
35920
36456
  const buckets = /* @__PURE__ */ new Map();
35921
36457
  for (const d of this.drawings) {
35922
- if (!d.visible) continue;
36458
+ if (!d.visible || this.lifted.has(d.id)) continue;
35923
36459
  const beforeZ = sliceKeyFor(d.zIndex, this.lastBounds.get(d.paneId) ?? []);
35924
36460
  if (beforeZ === null) continue;
35925
36461
  const key = `${d.paneId}|${beforeZ}`;
@@ -35971,8 +36507,9 @@ ${overlayScrollbarCss(".vela-sd-pane")}
35971
36507
  mutedLabel: edited instanceof TextLabel ? edited.id : null
35972
36508
  };
35973
36509
  this.painter.seriesLook = this.deps.seriesLook();
35974
- this.painter.paintAll(ctx, this.drawings.filter((d) => !this.isInterleaved(d)), proj, this.deps.theme(), targets);
35975
- this.painter.paintHighlights(ctx, this.drawings.filter((d) => this.isInterleaved(d)), proj, handleIdsFor(targets));
36510
+ const onTop = (d) => !this.isInterleaved(d) || this.lifted.has(d.id);
36511
+ this.painter.paintAll(ctx, this.drawings.filter(onTop), proj, this.deps.theme(), targets);
36512
+ this.painter.paintHighlights(ctx, this.drawings.filter((d) => !onTop(d)), proj, handleIdsFor(targets));
35976
36513
  const clones = this.interaction.dragClones();
35977
36514
  if (clones) this.painter.paintAll(ctx, clones, proj, this.deps.theme(), { selected: new Set(clones.map((c) => c.id)) });
35978
36515
  this.layoutTextEditor();
@@ -36539,9 +37076,10 @@ ${overlayScrollbarCss(".vela-sd-pane")}
36539
37076
  consider(drawings.max);
36540
37077
  }
36541
37078
  if (min2 === Infinity || max2 === -Infinity) return { min: 0, max: 1 };
36542
- if (min2 === max2) {
36543
- const pad = Math.abs(min2) * 0.1 || 1;
36544
- return { min: min2 - pad, max: max2 + pad, log: log && min2 - pad > 0 };
37079
+ if (max2 - min2 <= Math.max(Math.abs(min2), Math.abs(max2)) * 1e-12) {
37080
+ const mid = (min2 + max2) / 2;
37081
+ const pad = Math.abs(mid) * 0.1 || 1;
37082
+ return { min: mid - pad, max: mid + pad, log: log && mid - pad > 0 };
36545
37083
  }
36546
37084
  const content = Math.max(0.1, 1 - (margins.top + margins.bottom) / 100);
36547
37085
  const above = margins.top / 100 / content;
@@ -37045,7 +37583,9 @@ ${overlayScrollbarCss(".vela-sd-pane")}
37045
37583
  return Math.ceil(logical) - 0.5;
37046
37584
  }
37047
37585
  var BackdropRenderer = class {
37048
- constructor() {
37586
+ /** `axisTicks` is shared with the chrome layer, so gridlines and price labels agree. */
37587
+ constructor(axisTicks = new PriceAxisTickSource()) {
37588
+ this.axisTicks = axisTicks;
37049
37589
  this.canvas = null;
37050
37590
  this.ctx = null;
37051
37591
  }
@@ -37122,9 +37662,8 @@ ${overlayScrollbarCss(".vela-sd-pane")}
37122
37662
  if (scene.showGrid && gridHorz.visible && !pane.collapsed) {
37123
37663
  ctx.globalAlpha = gridAlpha;
37124
37664
  ctx.strokeStyle = horzColor;
37125
- const pct = percentScaleFor(scene, pane);
37126
37665
  ctx.beginPath();
37127
- for (const t of paneAxisTicks(pane.scale, pane.bounds.height, pct, void 0, pane.axisFormat)) {
37666
+ for (const t of this.axisTicks.ticksFor(scene, pane, coords)) {
37128
37667
  const y = Math.round(coords.priceToY(t.price, pane.scale, pane.bounds)) + 0.5;
37129
37668
  if (y < pane.bounds.top || y > pane.bounds.top + pane.bounds.height) continue;
37130
37669
  ctx.moveTo(0, y);
@@ -37740,8 +38279,16 @@ ${overlayScrollbarCss(".vela-sd-pane")}
37740
38279
  this.toolbarGutter = 0;
37741
38280
  // px reserved on the left for the docked drawings toolbar (0 when hidden)
37742
38281
  this.mountContainer = null;
38282
+ /** The host-styleable twin of the `shadeRight` veil (`.vela-shade-right`), directly under
38283
+ * the cursor canvas; hidden whenever the crosshair layer paints no veil. */
38284
+ this.shadeRightEl = null;
38285
+ /** The veil geometry last written to {@link shadeRightEl} ('' = hidden) — the DOM is
38286
+ * touched only when it changes, never on a plain pointer move. */
38287
+ this.shadeRightSig = "";
37743
38288
  this.userDrawings = null;
37744
- this.backdropRenderer = new BackdropRenderer();
38289
+ /** One price-axis tick source for the gridlines (backdrop) and the labels (chrome). */
38290
+ this.priceAxisTicks = new PriceAxisTickSource();
38291
+ this.backdropRenderer = new BackdropRenderer(this.priceAxisTicks);
37745
38292
  this.volumeRenderer = new VolumeRenderer();
37746
38293
  /** SDK renderer layers instantiated at mount ({@link registerRendererLayer}). */
37747
38294
  /**
@@ -37774,7 +38321,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
37774
38321
  this.backendMode = "auto";
37775
38322
  this.glowAmount = 0;
37776
38323
  // WebGL2 neon-glow intensity (canvas2d ignores it)
37777
- this.chrome = new ChromeRenderer();
38324
+ this.chrome = new ChromeRenderer(this.priceAxisTicks);
37778
38325
  /** Prepaints each indicator's Pine drawings into interleave slices at the model's z. */
37779
38326
  this.indicatorSlices = new IndicatorDrawingSlices();
37780
38327
  /** Hover tooltips for Pine labels (canvas hit-rects collected by the chrome layer). */
@@ -37891,6 +38438,8 @@ ${overlayScrollbarCss(".vela-sd-pane")}
37891
38438
  // pane just below it
37892
38439
  this.resizeSplitStart = null;
37893
38440
  // the two panes' shared span when the drag began
38441
+ /** The user drawings take part in pointer input (the runtime `drawingsInteractive` feature). */
38442
+ this.drawingsInteractive = true;
37894
38443
  this.hoverSeparatorY = null;
37895
38444
  // pixel y of the separator under the cursor (drives its hover highlight)
37896
38445
  // ── pane management (merge / reorder / collapse / maximize) ──
@@ -37946,8 +38495,9 @@ ${overlayScrollbarCss(".vela-sd-pane")}
37946
38495
  this.toggleVisibleCbs = /* @__PURE__ */ new Set();
37947
38496
  this.moveIndicatorCbs = /* @__PURE__ */ new Set();
37948
38497
  this.priceStyleCbs = /* @__PURE__ */ new Set();
38498
+ this.priceStyleWillChangeCbs = /* @__PURE__ */ new Set();
37949
38499
  this.name = "native";
37950
- this.features = ["logScale", "currentPriceLine", "priceLabel", "countdown", "upColor", "downColor", "glow", "animZoom", "animPan", "animScroll", "animAutoscale", "animLiveBar", "intro", "zoomAnchor", "axisDrag", "paneResize", "candleZOrder", "candleVisible", "seriesOrder", "highlights", "sessionZones", "gridlines", "axisLabels", "scaleMode", "invertScale", "paneScales", "autoScale", "timezone", "keyboard", "historyChords", "priceStyle", "priceBaseline", "baselinePrice", "settings", "attribution", "dialogHost", "tradeMarkers", "marks", "indicatorTitles", "indicatorValues", "crosshairOverride"];
38500
+ this.features = ["logScale", "currentPriceLine", "priceLabel", "countdown", "upColor", "downColor", "glow", "animZoom", "animPan", "animScroll", "animAutoscale", "animLiveBar", "intro", "zoomAnchor", "axisDrag", "paneResize", "candleZOrder", "candleVisible", "seriesOrder", "highlights", "sessionZones", "gridlines", "axisLabels", "scaleMode", "invertScale", "paneScales", "autoScale", "timezone", "keyboard", "historyChords", "priceStyle", "priceBaseline", "baselinePrice", "settings", "attribution", "dialogHost", "tradeMarkers", "marks", "indicatorTitles", "indicatorValues", "crosshairOverride", "drawingsInteractive", "priceAxisTicks"];
37951
38501
  /** Track cursor proximity to the scroll button on the plot (bubbles from the button too,
37952
38502
  * so moving onto the button doesn't count as leaving). */
37953
38503
  this.onScrollProximityMove = (e) => {
@@ -37991,6 +38541,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
37991
38541
  this.scene.priceStyle = opts.priceStyle;
37992
38542
  this.scene.basePainting = basePaintingOf(opts.priceStyle);
37993
38543
  this.scene.candleOverride = candleOverrideFor(opts.priceStyle, this.scene.style.chartTypes);
38544
+ if (typeof opts.priceAxisTicks === "function") this.priceAxisTicks.setHook(opts.priceAxisTicks);
37994
38545
  }
37995
38546
  this.theme = this.deriveTheme(DARK_THEME);
37996
38547
  }
@@ -38129,6 +38680,15 @@ ${overlayScrollbarCss(".vela-sd-pane")}
38129
38680
  case "crosshairOverride":
38130
38681
  this.scene.crosshairOverride = sanitizeCrosshairOverride(value);
38131
38682
  break;
38683
+ case "drawingsInteractive":
38684
+ this.drawingsInteractive = value !== false;
38685
+ if (this.input) this.input.drawings = this.drawingsInteractive;
38686
+ if (!this.drawingsInteractive) this.userDrawings?.clearHover();
38687
+ return;
38688
+ // affects the next gesture only — clearHover repaints what it drops
38689
+ case "priceAxisTicks":
38690
+ this.priceAxisTicks.setHook(typeof value === "function" ? value : null);
38691
+ break;
38132
38692
  case "keyboard":
38133
38693
  this.setKeyboardEnabled(Boolean(value));
38134
38694
  return;
@@ -38244,6 +38804,10 @@ ${overlayScrollbarCss(".vela-sd-pane")}
38244
38804
  return { visible: this.scene.marks.visible, groups: { ...this.scene.marks.groups } };
38245
38805
  case "crosshairOverride":
38246
38806
  return this.scene.crosshairOverride ? { ...this.scene.crosshairOverride } : null;
38807
+ case "drawingsInteractive":
38808
+ return this.drawingsInteractive;
38809
+ case "priceAxisTicks":
38810
+ return this.priceAxisTicks.hook;
38247
38811
  case "keyboard":
38248
38812
  return this.keyboardEnabled;
38249
38813
  case "historyChords":
@@ -38917,6 +39481,12 @@ ${overlayScrollbarCss(".vela-sd-pane")}
38917
39481
  Object.assign(this.drawingsCanvas.style, { position: "absolute", inset: "0", width: "100%", height: "100%", pointerEvents: "none" });
38918
39482
  this.cursorCanvas = document.createElement("canvas");
38919
39483
  Object.assign(this.cursorCanvas.style, { position: "absolute", inset: "0", width: "100%", height: "100%", pointerEvents: "none" });
39484
+ this.shadeRightEl = document.createElement("div");
39485
+ this.shadeRightEl.className = "vela-shade-right";
39486
+ this.shadeRightEl.setAttribute("aria-hidden", "true");
39487
+ this.shadeRightEl.hidden = true;
39488
+ Object.assign(this.shadeRightEl.style, { position: "absolute", top: "0", left: "0", width: "0", height: "0", pointerEvents: "none", display: "none" });
39489
+ this.shadeRightSig = "";
38920
39490
  this.overlayRoot = document.createElement("div");
38921
39491
  Object.assign(this.overlayRoot.style, { position: "absolute", inset: "0", pointerEvents: "none" });
38922
39492
  this.plot = document.createElement("div");
@@ -38924,7 +39494,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
38924
39494
  this.extLayers = rendererLayers().map((def) => ({ def, instance: def.create(), canvas: this.createLayerCanvas(), channel: def.id, owner: null }));
38925
39495
  const below = this.extLayers.filter((l) => l.def.placement === "below-data").map((l) => l.canvas);
38926
39496
  const above = this.extLayers.filter((l) => l.def.placement !== "below-data").map((l) => l.canvas);
38927
- this.plot.append(this.backdropCanvas, ...below, this.dataCanvas, this.volumeCanvas, this.vpvrCanvas, ...above, this.chromeCanvas, this.drawingsCanvas, this.cursorCanvas, this.overlayRoot);
39497
+ this.plot.append(this.backdropCanvas, ...below, this.dataCanvas, this.volumeCanvas, this.vpvrCanvas, ...above, this.chromeCanvas, this.drawingsCanvas, this.shadeRightEl, this.cursorCanvas, this.overlayRoot);
38928
39498
  this.layerOrderSig = "";
38929
39499
  this.wrapper.appendChild(this.plot);
38930
39500
  this.factoryConfig = this.getConfig();
@@ -38982,13 +39552,14 @@ ${overlayScrollbarCss(".vela-sd-pane")}
38982
39552
  drawingsPointerDown: (x, y, snap, shift4, mod) => this.userDrawings?.pointerDown(x, y, snap, shift4, mod),
38983
39553
  drawingsPointerMove: (x, y, snap, shift4, mod) => this.userDrawings?.pointerMove(x, y, snap, shift4, mod),
38984
39554
  drawingsPointerUp: (x, y, snap) => this.userDrawings?.pointerUp(x, y, snap),
38985
- drawingsCursor: (x, y) => this.userDrawings?.cursorAt(x, y) ?? (this.chrome.markGlyphAt(x, y) ? "pointer" : null),
39555
+ drawingsCursor: (x, y) => (this.drawingsInteractive ? this.userDrawings?.cursorAt(x, y) : null) ?? (this.chrome.markGlyphAt(x, y) ? "pointer" : null),
38986
39556
  drawingsDblClick: (x, y) => this.userDrawings?.dblClick(x, y) ?? false,
38987
39557
  drawingsClearTransient: () => this.userDrawings?.clearTransient()
38988
39558
  });
38989
39559
  this.input.rightEdgeZoom = this.zoomAnchorMode === "right";
38990
39560
  this.input.axisDrag = this.axisDragEnabled;
38991
39561
  this.input.paneResize = this.paneResizeEnabled;
39562
+ this.input.drawings = this.drawingsInteractive;
38992
39563
  this.input.attach(this.dataCanvas);
38993
39564
  if (this.settingsEnabled) this.setSettingsEnabled(true);
38994
39565
  this.syncCountdownTimer();
@@ -39297,6 +39868,8 @@ ${overlayScrollbarCss(".vela-sd-pane")}
39297
39868
  this.backend.destroy();
39298
39869
  this.chrome.destroy();
39299
39870
  this.crosshairLayer.destroy();
39871
+ this.shadeRightEl?.remove();
39872
+ this.shadeRightEl = null;
39300
39873
  this.attributionEl?.remove();
39301
39874
  this.attributionEl = null;
39302
39875
  this.mountContainer?.style.removeProperty("--vela-toolbar-gutter");
@@ -39643,14 +40216,21 @@ ${overlayScrollbarCss(".vela-sd-pane")}
39643
40216
  this.priceStyleCbs.add(cb);
39644
40217
  return () => this.priceStyleCbs.delete(cb);
39645
40218
  }
40219
+ onPriceStyleWillChange(cb) {
40220
+ this.priceStyleWillChangeCbs.add(cb);
40221
+ return () => this.priceStyleWillChangeCbs.delete(cb);
40222
+ }
39646
40223
  /**
39647
40224
  * THE single write path for the base price style at runtime (feature set / settings dialog /
39648
- * config template — the constructor seeds the field directly, pre-listeners). Updates the
39649
- * scene, eases any reveal layer toward the new style's target, and notifies the core —
39650
- * which owns the DATA side of styles that need one (a chart type's SeriesDataEngine).
40225
+ * config template — the constructor seeds the field directly, pre-listeners). Announces the
40226
+ * switch while the scene still holds the old style (a listener may paint/capture that frame),
40227
+ * then updates the scene, eases any reveal layer toward the new style's target, and notifies
40228
+ * the core — which owns the DATA side of styles that need one (a chart type's SeriesDataEngine).
39651
40229
  */
39652
40230
  setPriceStyle(style) {
39653
40231
  if (style === this.scene.priceStyle) return;
40232
+ const from = this.scene.priceStyle;
40233
+ for (const cb of this.priceStyleWillChangeCbs) cb(from, style);
39654
40234
  this.scene.priceStyle = style;
39655
40235
  this.scene.basePainting = basePaintingOf(style);
39656
40236
  this.scene.candleOverride = candleOverrideFor(style, this.scene.style.chartTypes);
@@ -39880,7 +40460,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
39880
40460
  if (this.easeLiveBar(dtMs)) active = true;
39881
40461
  this.skeletonClockMs += dtMs;
39882
40462
  this.paintData();
39883
- this.crosshairLayer.render(this.scene, this.coords, this.theme, this.hoverSeparatorY, this.externalCrossPx());
40463
+ this.renderCrosshairLayer();
39884
40464
  this.updateLegendValues();
39885
40465
  this.emitViewportChange();
39886
40466
  return active;
@@ -40455,10 +41035,31 @@ ${overlayScrollbarCss(".vela-sd-pane")}
40455
41035
  this.chrome.render(this.scene, this.coords, this.theme, this.axisSurface());
40456
41036
  this.trackMarkPopover();
40457
41037
  }
40458
- this.crosshairLayer.render(this.scene, this.coords, this.theme, this.hoverSeparatorY, this.externalCrossPx());
41038
+ this.renderCrosshairLayer();
40459
41039
  if (!repaintsData(level) && this.paintedData) this.repaintCursorLayers();
40460
41040
  this.updateLegendValues();
40461
41041
  }
41042
+ /** Repaint the L2 crosshair canvas and keep the `.vela-shade-right` element on the veil it painted. */
41043
+ renderCrosshairLayer() {
41044
+ this.crosshairLayer.render(this.scene, this.coords, this.theme, this.hoverSeparatorY, this.externalCrossPx());
41045
+ this.syncShadeRight();
41046
+ }
41047
+ /** Position (or hide) the `.vela-shade-right` element over the veil of the last crosshair frame. */
41048
+ syncShadeRight() {
41049
+ const el = this.shadeRightEl;
41050
+ if (!el) return;
41051
+ const area = this.crosshairLayer.shadeRightArea;
41052
+ const sig = area ? `${area.x},${area.width},${area.height}` : "";
41053
+ if (sig === this.shadeRightSig) return;
41054
+ this.shadeRightSig = sig;
41055
+ if (!area) {
41056
+ el.hidden = true;
41057
+ el.style.display = "none";
41058
+ return;
41059
+ }
41060
+ Object.assign(el.style, { left: `${area.x}px`, width: `${area.width}px`, height: `${area.height}px`, display: "" });
41061
+ el.hidden = false;
41062
+ }
40462
41063
  /** Repaint the SDK layers that opted into cursor tracking (their own canvas only). */
40463
41064
  repaintCursorLayers() {
40464
41065
  const pane = this.scene.panes.get(PRICE_PANE_ID);
@@ -41252,7 +41853,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
41252
41853
  if (this.animator.active) {
41253
41854
  this.computeScales();
41254
41855
  this.paintData();
41255
- this.crosshairLayer.render(this.scene, this.coords, this.theme, this.hoverSeparatorY, this.externalCrossPx());
41856
+ this.renderCrosshairLayer();
41256
41857
  } else {
41257
41858
  this.scheduler.flushNow(4 /* Full */);
41258
41859
  }
@@ -45296,7 +45897,8 @@ ${overlayScrollbarCss(".vela-sd-pane")}
45296
45897
  glow: options.glow ?? 0,
45297
45898
  upColor: options.upColor ?? BULLISH,
45298
45899
  downColor: options.downColor ?? BEARISH,
45299
- priceStyle: options.priceStyle ?? "candles"
45900
+ priceStyle: options.priceStyle ?? "candles",
45901
+ priceAxisTicks: options.priceAxis?.ticks ?? null
45300
45902
  };
45301
45903
  const RendererClass = options.renderer ?? NativeRenderer;
45302
45904
  if (typeof RendererClass !== "function") {
@@ -45782,6 +46384,23 @@ ${overlayScrollbarCss(".vela-sd-pane")}
45782
46384
  return { ...Object.fromEntries(defaultEngines), ...overrides };
45783
46385
  }
45784
46386
 
46387
+ // src/widget/context-menu-model.ts
46388
+ var CONTEXT_MENU_BUILTIN_ORDER = {
46389
+ body: { "reset-view": -30, "remove-drawings": -20, "remove-indicators": -10, settings: 1e3 },
46390
+ "price-axis": {
46391
+ auto: -80,
46392
+ invert: -70,
46393
+ "scale:regular": -60,
46394
+ "scale:percent": -50,
46395
+ "scale:indexed": -40,
46396
+ "scale:log": -30,
46397
+ labels: -20,
46398
+ levels: -10,
46399
+ settings: 1e3
46400
+ },
46401
+ "time-axis": { timezone: -10, settings: 1e3 }
46402
+ };
46403
+
45785
46404
  // src/widget/side-panel.ts
45786
46405
  var DEFAULT_PANEL_WIDTH = 280;
45787
46406
  var DEFAULT_PANEL_MIN_WIDTH = 200;
@@ -47031,6 +47650,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
47031
47650
  exports.BinanceProvider = BinanceProvider;
47032
47651
  exports.CATEGORICAL = CATEGORICAL;
47033
47652
  exports.CHIP_PLATE = CHIP_PLATE;
47653
+ exports.CONTEXT_MENU_BUILTIN_ORDER = CONTEXT_MENU_BUILTIN_ORDER;
47034
47654
  exports.CROSSHAIR = CROSSHAIR;
47035
47655
  exports.CachingDataFeed = CachingDataFeed;
47036
47656
  exports.CoinbaseProvider = CoinbaseProvider;