@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
package/dist/index.cjs CHANGED
@@ -238,7 +238,7 @@ var IndicatorHandleImpl = class {
238
238
  this.controller.applyProps(this.id, values);
239
239
  }
240
240
  updateCode(source) {
241
- this.controller.updateCode(this.id, source);
241
+ return this.controller.updateCode(this.id, source);
242
242
  }
243
243
  setVisible(visible) {
244
244
  this.controller.setVisible(this.id, visible);
@@ -641,6 +641,7 @@ registerIcon("expand", S('<rect x="2.2" y="2.2" width="11.6" height="11.6" rx="1
641
641
  registerIcon("plus", S('<path d="M8 2.8v10.4M2.8 8h10.4"/>'));
642
642
  registerIcon("minus", S('<path d="M2.8 8h10.4"/>'));
643
643
  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"/>'));
644
+ 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"/>'));
644
645
  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"/>'));
645
646
  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"/>'));
646
647
  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"'));
@@ -3299,6 +3300,10 @@ var FibRatios = class extends Drawing {
3299
3300
 
3300
3301
  // src/core/drawings/types/FibLevels.ts
3301
3302
  var FibLevels = class extends FibRatios {
3303
+ /** Price of a level for the two anchor prices; subclasses override to change which anchor ratio 0 sits on. */
3304
+ levelPrice(ratio, p1, p2) {
3305
+ return p1 + ratio * (p2 - p1);
3306
+ }
3302
3307
  /** Per-level pixel line + price for the ENABLED levels, spanning the anchors' time range. */
3303
3308
  levelLines(proj) {
3304
3309
  const a = this.anchors[0];
@@ -3308,11 +3313,10 @@ var FibLevels = class extends FibRatios {
3308
3313
  const xb = proj.xOf(b.time);
3309
3314
  const x1 = Math.min(xa, xb);
3310
3315
  const x2 = Math.max(xa, xb);
3311
- const delta = b.price - a.price;
3312
3316
  const out = [];
3313
3317
  for (const lv of this.levels) {
3314
3318
  if (!lv.enabled) continue;
3315
- const price = a.price + lv.ratio * delta;
3319
+ const price = this.levelPrice(lv.ratio, a.price, b.price);
3316
3320
  const y = proj.yOf(price, this.paneId);
3317
3321
  if (y == null) continue;
3318
3322
  out.push({ ratio: lv.ratio, color: lv.color, label: lv.label, price, x1, x2, y });
@@ -3352,8 +3356,7 @@ var FibLevels = class extends FibRatios {
3352
3356
  const a = this.anchors[0];
3353
3357
  const b = this.anchors[1];
3354
3358
  if (!a || !b) return null;
3355
- const delta = b.price - a.price;
3356
- const prices = this.levels.filter((l) => l.enabled).map((l) => a.price + l.ratio * delta);
3359
+ const prices = this.levels.filter((l) => l.enabled).map((l) => this.levelPrice(l.ratio, a.price, b.price));
3357
3360
  if (prices.length === 0) return null;
3358
3361
  return { min: Math.min(...prices), max: Math.max(...prices) };
3359
3362
  }
@@ -3439,13 +3442,24 @@ function cycleLevels(count, enabledCount = count) {
3439
3442
  // src/core/drawings/types/FibRetracement.ts
3440
3443
  var LEVELS = fibLevels([0, 0.236, 0.382, 0.5, 0.618, 0.786, 1]);
3441
3444
  var FibRetracement = class extends FibLevels {
3442
- constructor() {
3443
- super(...arguments);
3445
+ constructor(init) {
3446
+ super(init);
3444
3447
  this.type = "fibretracement";
3448
+ if (this.reverse === void 0) this.reverse = false;
3445
3449
  }
3446
3450
  defaultLevels() {
3447
3451
  return LEVELS;
3448
3452
  }
3453
+ levelPrice(ratio, p1, p2) {
3454
+ return this.reverse ? p1 + ratio * (p2 - p1) : p2 + ratio * (p1 - p2);
3455
+ }
3456
+ writeProps() {
3457
+ return { ...super.writeProps(), reverse: this.reverse };
3458
+ }
3459
+ readProps(props) {
3460
+ super.readProps(props);
3461
+ this.reverse = typeof props.reverse === "boolean" ? props.reverse : true;
3462
+ }
3449
3463
  };
3450
3464
 
3451
3465
  // src/core/drawings/types/FibExtension.ts
@@ -7874,6 +7888,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
7874
7888
  this.activeEngineStyle = null;
7875
7889
  /** The chart's current price style (tracked from the renderer's change events + initial read). */
7876
7890
  this.priceStyle = "candles";
7891
+ this.priceStyleWillChangeUnsub = null;
7877
7892
  this.unresolvedUnsub = null;
7878
7893
  /** Latest chart visible range (left/right bar times), fed to viewport-dependent scripts. */
7879
7894
  this.visibleRange = null;
@@ -7972,6 +7987,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
7972
7987
  this.renderer.onThemeSelect?.((name) => this.setTheme(resolveTheme(name)));
7973
7988
  this.viewportUnsub = this.renderer.onViewportChange((range) => this.onViewportChange(range));
7974
7989
  this.renderer.onPriceStyleChange?.((style) => this.syncPriceStyle(style));
7990
+ this.priceStyleWillChangeUnsub = this.renderer.onPriceStyleWillChange?.((from, to) => this.events.emit("priceStyle:change", { from, to })) ?? null;
7975
7991
  const initialStyle = this.renderer.readFeature("priceStyle");
7976
7992
  if (typeof initialStyle === "string") this.priceStyle = initialStyle;
7977
7993
  this.barTransform = barTransformFor(initialStyle);
@@ -9258,20 +9274,26 @@ var EngineOrchestrator = class _EngineOrchestrator {
9258
9274
  /**
9259
9275
  * IndicatorController: replace a SCRIPT indicator's source in place (see
9260
9276
  * {@link IndicatorHandle.updateCode}). Prepare-first: the running session keeps
9261
- * computing until the new source has compiled, so a broken edit costs the user
9262
- * nothing but an `error` event. Natives have no script — warn and leave them alone.
9277
+ * computing until the new source has compiled; then the new code runs on TRIAL until
9278
+ * its first model lands, and a failure before that puts the previous code back.
9279
+ * Natives have no script — warn and leave them alone.
9263
9280
  */
9264
9281
  updateCode(id, source) {
9265
9282
  const record = this.registry.get(id);
9266
9283
  const handle = this.handles.get(id);
9267
- if (!record || !handle) return;
9284
+ if (!record || !handle) return Promise.resolve({ ok: false, error: new Error(`[vela] updateCode("${id}") \u2014 the indicator is no longer on the chart.`) });
9268
9285
  if (record.native) {
9269
9286
  console.warn(`[vela] updateCode("${id}") \u2014 a native indicator has no script to update.`);
9270
- return;
9287
+ return Promise.resolve({ ok: false, error: new Error(`[vela] updateCode("${id}") \u2014 a native indicator has no script to update.`) });
9271
9288
  }
9272
- if (source === record.source && record.pendingSource === void 0) return;
9273
- record.pendingSource = source;
9274
- void this.swapSource(id, source, handle);
9289
+ const busy = record.pendingSource !== void 0 || record.codeTrial !== void 0;
9290
+ if (!busy && source === record.source) return Promise.resolve({ ok: true, error: null });
9291
+ return new Promise((resolve) => {
9292
+ (record.codeWaiters ?? (record.codeWaiters = [])).push(resolve);
9293
+ if (record.pendingSource === source || record.pendingSource === void 0 && record.codeTrial && record.source === source) return;
9294
+ record.pendingSource = source;
9295
+ void this.swapSource(id, source, handle);
9296
+ });
9275
9297
  }
9276
9298
  async swapSource(id, source, handle) {
9277
9299
  const engine = this.registry.get(id)?.engine ?? this.engineFor(this.registry.get(id)?.options?.language);
@@ -9282,26 +9304,41 @@ var EngineOrchestrator = class _EngineOrchestrator {
9282
9304
  const record2 = this.registry.get(id);
9283
9305
  if (record2?.pendingSource === source) {
9284
9306
  record2.pendingSource = void 0;
9285
- this.fail(id, handle, err);
9307
+ this.rejectCode(id, record2, handle, err);
9286
9308
  }
9287
9309
  return;
9288
9310
  }
9289
9311
  const record = this.registry.get(id);
9290
9312
  if (!record || record.pendingSource !== source) return;
9291
9313
  record.pendingSource = void 0;
9314
+ const previous = record.codeTrial?.previous ?? {
9315
+ source: record.source,
9316
+ engine: record.engine,
9317
+ prepared: record.prepared,
9318
+ inputValues: record.inputValues,
9319
+ propValues: record.propValues,
9320
+ inputs: [...handle.inputs],
9321
+ props: [...handle.props]
9322
+ };
9323
+ record.inputValues = valuesOnSchema(prepared.inputs, record.inputValues, record.prepared?.inputs);
9324
+ record.propValues = valuesOnSchema(prepared.props ?? [], record.propValues, record.prepared?.props);
9292
9325
  record.source = source;
9293
9326
  record.engine = engine;
9294
9327
  record.prepared = prepared;
9295
9328
  handle.setSource(source);
9296
- record.inputValues = valuesOnSchema(prepared.inputs, record.inputValues);
9297
- record.propValues = valuesOnSchema(prepared.props ?? [], record.propValues);
9298
9329
  handle.setSchema(prepared.inputs);
9299
9330
  handle.setPropsSchema(prepared.props ?? []);
9300
9331
  record.session?.stop();
9301
9332
  record.session = void 0;
9302
9333
  record.pendingStructural = true;
9303
9334
  record.pendingCause = "code";
9304
- if (record.hidden) return;
9335
+ if (record.hidden) {
9336
+ record.codeTrial = void 0;
9337
+ if (record.renderHandle) this.renderer.setIndicatorInputs(record.renderHandle, record.inputValues, record.propValues);
9338
+ this.settleCode(record, { ok: true, error: null });
9339
+ return;
9340
+ }
9341
+ record.codeTrial = { previous };
9305
9342
  if (record.renderHandle) {
9306
9343
  this.renderer.setIndicatorInputs(record.renderHandle, record.inputValues, record.propValues);
9307
9344
  this.setLoading(record, true);
@@ -9309,6 +9346,46 @@ var EngineOrchestrator = class _EngineOrchestrator {
9309
9346
  this.mountLoadingPlaceholder(id, record);
9310
9347
  }
9311
9348
  this.executeIndicator(id, handle);
9349
+ if (this.bars.length === 0 && record.codeTrial) {
9350
+ record.codeTrial = void 0;
9351
+ this.settleCode(record, { ok: true, error: null });
9352
+ }
9353
+ }
9354
+ /** Resolve every `updateCode` caller waiting on this record's chain of edits. */
9355
+ settleCode(record, result) {
9356
+ const waiters = record.codeWaiters;
9357
+ record.codeWaiters = void 0;
9358
+ if (waiters) for (const resolve of waiters) resolve(result);
9359
+ }
9360
+ /**
9361
+ * An update failed — at prepare, or on trial before its first model. A trial puts the
9362
+ * previous code back (source, engine, prepared script, values, schema) and restarts it;
9363
+ * its visuals were never dropped, so the chart keeps painting throughout. Then the
9364
+ * failure reports the usual way and every waiting caller learns it.
9365
+ */
9366
+ rejectCode(id, record, handle, err) {
9367
+ const trial = record.codeTrial;
9368
+ record.codeTrial = void 0;
9369
+ this.fail(id, handle, err);
9370
+ if (trial) {
9371
+ const p = trial.previous;
9372
+ record.session?.stop();
9373
+ record.session = void 0;
9374
+ record.source = p.source;
9375
+ record.engine = p.engine;
9376
+ record.prepared = p.prepared;
9377
+ record.inputValues = p.inputValues;
9378
+ record.propValues = p.propValues;
9379
+ handle.setSource(p.source);
9380
+ handle.setSchema(p.inputs);
9381
+ handle.setPropsSchema(p.props);
9382
+ record.pendingStructural = true;
9383
+ record.pendingCause = void 0;
9384
+ if (record.renderHandle) this.renderer.setIndicatorInputs(record.renderHandle, record.inputValues, record.propValues);
9385
+ if (!record.hidden) this.executeIndicator(id, handle);
9386
+ }
9387
+ const error = err instanceof Error ? err : new Error(String(err));
9388
+ this.settleCode(record, { ok: false, error });
9312
9389
  }
9313
9390
  /** IndicatorController: tear down an indicator and (if now empty) its pane. */
9314
9391
  /** Live handles of every indicator on the chart (script + native), insertion order. */
@@ -9323,6 +9400,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
9323
9400
  removeIndicator(id) {
9324
9401
  const record = this.registry.remove(id);
9325
9402
  if (record?.native?.type === "volume") this.volumeOptedOut = true;
9403
+ if (record) this.settleCode(record, { ok: false, error: new Error(`[vela] updateCode("${id}") \u2014 the indicator was removed.`) });
9326
9404
  record?.session?.stop();
9327
9405
  if (record?.native?.started) record.native.instance.stop();
9328
9406
  this.handles.delete(id);
@@ -9350,6 +9428,10 @@ var EngineOrchestrator = class _EngineOrchestrator {
9350
9428
  if (!visible) {
9351
9429
  record.session?.stop();
9352
9430
  record.session = void 0;
9431
+ if (record.codeTrial) {
9432
+ record.codeTrial = void 0;
9433
+ this.settleCode(record, { ok: true, error: null });
9434
+ }
9353
9435
  if (record.native?.started) record.native.instance.suspend();
9354
9436
  if (record.renderHandle) this.renderer.setIndicatorVisible?.(record.renderHandle, false);
9355
9437
  else if (record.native) this.mountHiddenNativeRow(id, record);
@@ -9436,7 +9518,10 @@ var EngineOrchestrator = class _EngineOrchestrator {
9436
9518
  if (this.viewportTimer != null) clearTimeout(this.viewportTimer);
9437
9519
  this.viewportUnsub?.();
9438
9520
  this.paneActionUnsub?.();
9521
+ this.priceStyleWillChangeUnsub?.();
9522
+ this.priceStyleWillChangeUnsub = null;
9439
9523
  for (const record of this.registry.all()) {
9524
+ this.settleCode(record, { ok: false, error: new Error("[vela] updateCode \u2014 the chart was destroyed.") });
9440
9525
  record.session?.stop();
9441
9526
  if (record.native?.started) record.native.instance.stop();
9442
9527
  }
@@ -9505,7 +9590,8 @@ var EngineOrchestrator = class _EngineOrchestrator {
9505
9590
  const record = this.registry.get(id);
9506
9591
  if (!record || !record.engine || !record.prepared || record.hidden) return;
9507
9592
  const mode = this.config.live && !record.prepared.reactsToViewport && record.engine.capabilities.streaming ? "live" : "static";
9508
- record.session = record.engine.execute(
9593
+ const prepared = record.prepared;
9594
+ const session = record.engine.execute(
9509
9595
  {
9510
9596
  prepared: record.prepared,
9511
9597
  market: this.market(),
@@ -9526,6 +9612,10 @@ var EngineOrchestrator = class _EngineOrchestrator {
9526
9612
  const cause = record.pendingCause ?? "history";
9527
9613
  if (!this.applyModel(id, model)) return;
9528
9614
  record.pendingCause = void 0;
9615
+ if (record.codeTrial) {
9616
+ record.codeTrial = void 0;
9617
+ this.settleCode(record, { ok: true, error: null });
9618
+ }
9529
9619
  this.emitContextChanged(id);
9530
9620
  this.emitScriptRun(id, cause, first);
9531
9621
  },
@@ -9536,9 +9626,17 @@ var EngineOrchestrator = class _EngineOrchestrator {
9536
9626
  handle.emit("alert", { id: a.id, message: a.message, title: a.title, time: a.time });
9537
9627
  },
9538
9628
  onWarning: (w) => this.events.emit("warning", w),
9539
- onError: (err) => this.fail(id, handle, err)
9629
+ onError: (err) => {
9630
+ if (record.codeTrial) this.rejectCode(id, record, handle, err);
9631
+ else this.fail(id, handle, err);
9632
+ }
9540
9633
  }
9541
9634
  );
9635
+ if (record.prepared !== prepared) {
9636
+ session.stop();
9637
+ return;
9638
+ }
9639
+ record.session = session;
9542
9640
  this.emitContextChanged(id);
9543
9641
  }
9544
9642
  /**
@@ -10039,7 +10137,7 @@ function yieldToPaint() {
10039
10137
  }
10040
10138
  return new Promise((resolve) => setTimeout(resolve, 0));
10041
10139
  }
10042
- function valuesOnSchema(schema, previous) {
10140
+ function valuesOnSchema(schema, previous, oldSchema = []) {
10043
10141
  const out = {};
10044
10142
  const declared = /* @__PURE__ */ new Set();
10045
10143
  for (const s of schema) {
@@ -10047,9 +10145,22 @@ function valuesOnSchema(schema, previous) {
10047
10145
  declared.add(s.key);
10048
10146
  declared.add(s.title);
10049
10147
  }
10050
- for (const [k, v] of Object.entries(previous)) if (declared.has(k)) out[k] = v;
10148
+ const oldDefaults = /* @__PURE__ */ new Map();
10149
+ for (const s of oldSchema) {
10150
+ oldDefaults.set(s.key, s.defval);
10151
+ if (!oldDefaults.has(s.title)) oldDefaults.set(s.title, s.defval);
10152
+ }
10153
+ for (const [k, v] of Object.entries(previous)) {
10154
+ if (!declared.has(k)) continue;
10155
+ if (oldDefaults.has(k) && sameInputValue(oldDefaults.get(k), v)) continue;
10156
+ out[k] = v;
10157
+ }
10051
10158
  return out;
10052
10159
  }
10160
+ function sameInputValue(a, b) {
10161
+ if (a === b) return true;
10162
+ return typeof a === "object" && a !== null && typeof b === "object" && b !== null && JSON.stringify(a) === JSON.stringify(b);
10163
+ }
10053
10164
  function blankedModel(model) {
10054
10165
  return {
10055
10166
  ...model,
@@ -11693,6 +11804,66 @@ function iconEl(id, doc = document) {
11693
11804
  if (svg) span.innerHTML = svg;
11694
11805
  return span;
11695
11806
  }
11807
+
11808
+ // src/ui/surface-events.ts
11809
+ var SURFACE_OPEN_EVENT = "vela:surface-open";
11810
+ var SURFACE_CLOSE_EVENT = "vela:surface-close";
11811
+ function announceSurface(el, open2, kind, trigger = null) {
11812
+ const View = el.ownerDocument.defaultView;
11813
+ if (!View) return;
11814
+ el.dispatchEvent(
11815
+ new View.CustomEvent(open2 ? SURFACE_OPEN_EVENT : SURFACE_CLOSE_EVENT, { bubbles: true, composed: true, detail: { kind, trigger } })
11816
+ );
11817
+ }
11818
+
11819
+ // src/ui/surface-exit.ts
11820
+ var CLOSING_ATTR = "data-closing";
11821
+ var SURFACE_EXIT_CAP_MS = 1e3;
11822
+ function animationsOf(els) {
11823
+ return els.flatMap((el) => el.getAnimations({ subtree: true }));
11824
+ }
11825
+ function ends(a) {
11826
+ return Number.isFinite(a.effect?.getComputedTiming().endTime ?? Number.POSITIVE_INFINITY);
11827
+ }
11828
+ function holdForExit(els, done, opts = {}) {
11829
+ const list = Array.isArray(els) ? els : [els];
11830
+ if (list.length === 0 || !list.every((el) => typeof el.getAnimations === "function")) {
11831
+ done();
11832
+ return null;
11833
+ }
11834
+ const before = new Set(animationsOf(list));
11835
+ for (const el of list) el.setAttribute(CLOSING_ATTR, "");
11836
+ const exit = animationsOf(list).filter((a) => !before.has(a) && ends(a));
11837
+ if (exit.length === 0) {
11838
+ for (const el of list) el.removeAttribute(CLOSING_ATTR);
11839
+ done();
11840
+ return null;
11841
+ }
11842
+ const frozen = [...list, ...opts.inert ?? []];
11843
+ const wasInert = frozen.map((el) => el.inert);
11844
+ for (const el of frozen) el.inert = true;
11845
+ let settled = false;
11846
+ const settle = (complete) => {
11847
+ if (settled) return;
11848
+ settled = true;
11849
+ clearTimeout(cap);
11850
+ for (const el of list) el.removeAttribute(CLOSING_ATTR);
11851
+ frozen.forEach((el, i) => {
11852
+ el.inert = wasInert[i];
11853
+ });
11854
+ if (complete) done();
11855
+ };
11856
+ const cap = setTimeout(() => settle(true), SURFACE_EXIT_CAP_MS);
11857
+ let pending = exit.length;
11858
+ const one = () => {
11859
+ if (--pending === 0) settle(true);
11860
+ };
11861
+ for (const a of exit) a.finished.then(one, one);
11862
+ return {
11863
+ cancel: () => settle(false),
11864
+ finish: () => settle(true)
11865
+ };
11866
+ }
11696
11867
  function menuController(opts) {
11697
11868
  return {
11698
11869
  machine: menu__namespace.machine,
@@ -11774,6 +11945,35 @@ var MENU_CSS = `
11774
11945
  color: var(--vela-fg-bright);
11775
11946
  }
11776
11947
  .vela-menu-item[data-checkmark] { color: var(--vela-fg-bright); }
11948
+ /* Badge mode (iconBadges): the row icon sits in a rounded tile and the row grows to fit
11949
+ it. An icon-less row of the same level keeps an invisible slot, so labels align. */
11950
+ .vela-menu[data-badges] > .vela-menu-item { padding: 5px 10px 5px 6px; }
11951
+ .vela-menu-item .vela-menu-badge {
11952
+ width: 24px;
11953
+ height: 24px;
11954
+ padding: 3px;
11955
+ flex: none;
11956
+ box-sizing: border-box;
11957
+ display: inline-flex;
11958
+ align-items: center;
11959
+ justify-content: center;
11960
+ border-radius: var(--vela-radius-md);
11961
+ background: var(--vela-hover);
11962
+ border: 1px solid var(--vela-border);
11963
+ color: var(--vela-fg-muted);
11964
+ 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);
11965
+ }
11966
+ .vela-menu-item .vela-menu-badge:empty { background: transparent; border-color: transparent; }
11967
+ .vela-menu-item .vela-menu-badge .vela-icon { color: inherit; }
11968
+ .vela-menu-item[data-highlighted] .vela-menu-badge:not(:empty) { color: var(--vela-fg-bright); }
11969
+ /* The selected tile inverts: a bright fill under the surface-colored glyph. After the
11970
+ hover rule, so a selected row keeps its tile while highlighted. */
11971
+ .vela-menu-item[data-checked] .vela-menu-badge:not(:empty),
11972
+ .vela-menu-item[data-checkmark] .vela-menu-badge:not(:empty) {
11973
+ background: color-mix(in srgb, var(--vela-fg-bright) 83%, var(--vela-surface));
11974
+ border-color: color-mix(in srgb, var(--vela-fg-bright) 40%, transparent);
11975
+ color: var(--vela-surface);
11976
+ }
11777
11977
  /* Switch rows (boolean settings in a dropdown): a right-aligned toggle pill \u2014 the
11778
11978
  same control language as the settings dialog's toggles. */
11779
11979
  .vela-menu-switch {
@@ -11838,11 +12038,16 @@ var Surface = class _Surface {
11838
12038
  /** Trigger rect captured when this level opened — the list stays put if the
11839
12039
  * trigger then moves (favorite chips shifting the caret). */
11840
12040
  this.pinnedAnchor = null;
12041
+ /** The open state last announced — see {@link announce}. */
12042
+ this.shown = false;
12043
+ /** The list's exit animation after a close; a reopen cancels it. */
12044
+ this.exit = null;
11841
12045
  this.doc = doc;
11842
12046
  this.host = opts.host;
11843
12047
  this.onSelect = opts.onSelect;
11844
12048
  this.onFavorite = opts.onFavorite;
11845
12049
  this.checkmarks = opts.checkmarks === true;
12050
+ this.iconBadges = opts.iconBadges === true;
11846
12051
  this.positioner = doc.createElement("div");
11847
12052
  this.positioner.className = "vela-ui-layer";
11848
12053
  this.list = doc.createElement("ul");
@@ -11851,6 +12056,7 @@ var Surface = class _Surface {
11851
12056
  this.positioner.appendChild(this.list);
11852
12057
  this.host.appendChild(this.positioner);
11853
12058
  const trigger = opts.trigger;
12059
+ this.opener = trigger ?? opts.opener ?? null;
11854
12060
  this.ctrl = menuController({
11855
12061
  items: [],
11856
12062
  id: opts.id,
@@ -11872,15 +12078,37 @@ var Surface = class _Surface {
11872
12078
  if (opts.triggerId && trigger) trigger.id = opts.triggerId;
11873
12079
  this.handle = runMachine(this.ctrl.machine, this.ctrl.props, (service) => {
11874
12080
  const api = this.ctrl.connect(service);
12081
+ const closing = this.shown && !api.open;
12082
+ if (closing) this.announce(false);
12083
+ if (api.open && this.exit) {
12084
+ this.exit.cancel();
12085
+ this.exit = null;
12086
+ }
11875
12087
  if (trigger) vanilla.spreadProps(trigger, api.getTriggerProps(), this.mid);
11876
12088
  vanilla.spreadProps(this.positioner, api.getPositionerProps(), this.mid);
11877
12089
  vanilla.spreadProps(this.list, api.getContentProps(), this.mid);
11878
12090
  this.project(api);
12091
+ if (closing) this.holdExit();
12092
+ if (api.open && !this.shown) this.announce(true);
12093
+ });
12094
+ }
12095
+ /** Keep the just-closed list up through its exit animation. `spreadProps` re-applies
12096
+ * `hidden` only when the machine's value for it changes, so the list stays shown until
12097
+ * the exit ends — or until a reopen, which projects `hidden: false` itself. */
12098
+ holdExit() {
12099
+ this.list.hidden = false;
12100
+ this.exit = holdForExit(this.list, () => {
12101
+ this.exit = null;
12102
+ if (!this.shown) this.list.hidden = true;
11879
12103
  });
11880
12104
  }
11881
12105
  get api() {
11882
12106
  return this.ctrl.connect(this.handle.service);
11883
12107
  }
12108
+ announce(open2) {
12109
+ this.shown = open2;
12110
+ announceSurface(this.list, open2, "menu", this.opener);
12111
+ }
11884
12112
  setItems(items) {
11885
12113
  this.items = items;
11886
12114
  for (const sub2 of this.subs.values()) sub2.destroy();
@@ -11897,7 +12125,9 @@ var Surface = class _Surface {
11897
12125
  destroy() {
11898
12126
  for (const sub2 of this.subs.values()) sub2.destroy();
11899
12127
  this.subs.clear();
12128
+ this.exit?.finish();
11900
12129
  this.handle.stop();
12130
+ if (this.shown) this.announce(false);
11901
12131
  this.positioner.remove();
11902
12132
  }
11903
12133
  byId(id) {
@@ -11922,6 +12152,10 @@ var Surface = class _Surface {
11922
12152
  const doc = this.doc;
11923
12153
  this.list.replaceChildren();
11924
12154
  const markable = this.checkmarks && this.items.some((i) => !(i.submenu && i.submenu.length > 0) && !i.toggle && i.checked !== void 0);
12155
+ const iconic = this.items.some((i) => !!i.icon);
12156
+ const badged = this.iconBadges && iconic;
12157
+ if (badged) this.list.dataset.badges = "1";
12158
+ else delete this.list.dataset.badges;
11925
12159
  for (const item of this.items) {
11926
12160
  if (item.separatorBefore) {
11927
12161
  const sep = doc.createElement("li");
@@ -11947,7 +12181,16 @@ var Surface = class _Surface {
11947
12181
  } else if (!branch && !item.toggle && item.checked) {
11948
12182
  li.dataset.checked = "1";
11949
12183
  }
11950
- if (item.icon) li.appendChild(iconEl(item.icon, doc));
12184
+ if (badged) {
12185
+ const badge = doc.createElement("span");
12186
+ badge.className = "vela-menu-badge";
12187
+ if (item.icon) badge.appendChild(iconEl(item.icon, doc));
12188
+ li.appendChild(badge);
12189
+ } else if (item.icon) {
12190
+ li.appendChild(iconEl(item.icon, doc));
12191
+ } else if (iconic) {
12192
+ li.appendChild(iconEl("", doc));
12193
+ }
11951
12194
  const label = doc.createElement("span");
11952
12195
  label.className = "vela-menu-label";
11953
12196
  label.textContent = item.label;
@@ -11991,9 +12234,11 @@ var Surface = class _Surface {
11991
12234
  const sub2 = new _Surface(doc, {
11992
12235
  host: this.host,
11993
12236
  placement: "right-start",
12237
+ opener: li,
11994
12238
  onSelect: this.onSelect,
11995
12239
  onFavorite: this.onFavorite,
11996
12240
  checkmarks: this.checkmarks,
12241
+ iconBadges: this.iconBadges,
11997
12242
  id: `${this.mid}--${item.id}`
11998
12243
  });
11999
12244
  sub2.setItems(item.submenu ?? []);
@@ -12019,7 +12264,8 @@ var Menu = class {
12019
12264
  id: opts.id,
12020
12265
  minWidth: opts.minWidth,
12021
12266
  onFavorite: opts.onFavorite,
12022
- checkmarks: opts.checkmarks
12267
+ checkmarks: opts.checkmarks,
12268
+ iconBadges: opts.iconBadges
12023
12269
  });
12024
12270
  this.root.setItems(opts.items);
12025
12271
  }
@@ -12123,6 +12369,7 @@ var POPOVER_CSS = `
12123
12369
 
12124
12370
  // src/ui/components/popover/view.ts
12125
12371
  var open = null;
12372
+ var leaving = /* @__PURE__ */ new Set();
12126
12373
  function closeOpenPopovers() {
12127
12374
  open?.hide();
12128
12375
  }
@@ -12150,6 +12397,8 @@ var Popover = class {
12150
12397
  this.shown = false;
12151
12398
  /** The pending removal of a fading-out shell; a show() that reuses the shell cancels it. */
12152
12399
  this.leaveTimer = null;
12400
+ /** The shell's exit animation before it leaves the DOM; a show() that reuses the shell cancels it. */
12401
+ this.exit = null;
12153
12402
  const doc = opts.trigger.ownerDocument;
12154
12403
  injectStyles(POPOVER_STYLE_ID, POPOVER_CSS, doc);
12155
12404
  this.trigger = opts.trigger;
@@ -12186,6 +12435,10 @@ var Popover = class {
12186
12435
  clearTimeout(this.leaveTimer);
12187
12436
  this.leaveTimer = null;
12188
12437
  }
12438
+ this.exit?.cancel();
12439
+ this.exit = null;
12440
+ leaving.delete(this);
12441
+ for (const p of [...leaving]) if (p.trigger === this.trigger) p.exit?.finish();
12189
12442
  ensureUIHost(this.el, this.inheritsTokens ? void 0 : this.theme);
12190
12443
  if (this.fadeMs > 0) {
12191
12444
  this.el.style.transition = `opacity ${this.fadeMs}ms ease`;
@@ -12210,16 +12463,21 @@ var Popover = class {
12210
12463
  this.hide();
12211
12464
  };
12212
12465
  const onReflow = () => this.place();
12213
- setTimeout(() => document.addEventListener("pointerdown", onOutside, true), 0);
12466
+ setTimeout(() => {
12467
+ if (this.onOutside === onOutside) document.addEventListener("pointerdown", onOutside, true);
12468
+ }, 0);
12214
12469
  document.addEventListener("keydown", onKey, true);
12215
12470
  window.addEventListener("resize", onReflow, true);
12216
12471
  document.addEventListener("scroll", onReflow, true);
12217
12472
  this.onOutside = onOutside;
12218
12473
  this.onKey = onKey;
12219
12474
  this.onReflow = onReflow;
12475
+ announceSurface(this.el, true, "popover", this.trigger);
12220
12476
  }
12221
12477
  hide() {
12222
12478
  if (!this.shown) return;
12479
+ this.shown = false;
12480
+ announceSurface(this.el, false, "popover", this.trigger);
12223
12481
  if (this.onOutside) document.removeEventListener("pointerdown", this.onOutside, true);
12224
12482
  if (this.onKey) document.removeEventListener("keydown", this.onKey, true);
12225
12483
  if (this.onReflow) {
@@ -12237,9 +12495,13 @@ var Popover = class {
12237
12495
  this.el.remove();
12238
12496
  }, this.fadeMs);
12239
12497
  } else {
12240
- this.el.remove();
12498
+ this.exit = holdForExit(this.el, () => {
12499
+ this.exit = null;
12500
+ leaving.delete(this);
12501
+ this.el.remove();
12502
+ });
12503
+ if (this.exit) leaving.add(this);
12241
12504
  }
12242
- this.shown = false;
12243
12505
  if (open === this) open = null;
12244
12506
  this.ctrl.onClose?.();
12245
12507
  }
@@ -12250,6 +12512,7 @@ var Popover = class {
12250
12512
  }
12251
12513
  destroy() {
12252
12514
  this.hide();
12515
+ this.exit?.finish();
12253
12516
  }
12254
12517
  reposition() {
12255
12518
  if (this.shown) this.place();
@@ -12940,6 +13203,11 @@ ${overlayScrollbarCss(".vela-dialog *")}
12940
13203
  `;
12941
13204
  var Dialog = class {
12942
13205
  constructor(opts = {}) {
13206
+ this.opener = null;
13207
+ this.wasOpen = false;
13208
+ /** The panel and scrim's exit animation after a close; a reopen cancels it. */
13209
+ this.exit = null;
13210
+ this.destroyed = false;
12943
13211
  const doc = (opts.host ?? document.body).ownerDocument;
12944
13212
  injectStyles(DIALOG_STYLE_ID, DIALOG_CSS, doc);
12945
13213
  const host = opts.host ?? doc.body;
@@ -13036,19 +13304,73 @@ var Dialog = class {
13036
13304
  const mid = String(this.ctrl.props.id);
13037
13305
  this.handle = runMachine(this.ctrl.machine, this.ctrl.props, (service) => {
13038
13306
  const api = this.ctrl.connect(service);
13307
+ const closing = this.wasOpen && !api.open;
13308
+ if (closing) this.announce(false);
13039
13309
  vanilla.spreadProps(this.backdrop, api.getBackdropProps(), mid);
13040
13310
  vanilla.spreadProps(this.positioner, api.getPositionerProps(), mid);
13041
13311
  vanilla.spreadProps(this.panel, api.getContentProps(), mid);
13042
13312
  vanilla.spreadProps(title, api.getTitleProps(), mid);
13043
13313
  vanilla.spreadProps(close, api.getCloseTriggerProps(), mid);
13044
- this.backdrop.style.display = api.open ? "" : "none";
13045
- this.positioner.style.display = api.open ? "" : "none";
13314
+ if (api.open) {
13315
+ this.exit?.cancel();
13316
+ this.exit = null;
13317
+ this.setShown(true);
13318
+ } else if (closing) {
13319
+ this.holdExit();
13320
+ } else if (!this.exit) {
13321
+ this.setShown(false);
13322
+ }
13323
+ if (api.open && !this.wasOpen) this.announce(true);
13324
+ if (this.wasOpen && !api.open) this.restoreOpener();
13325
+ this.wasOpen = api.open;
13046
13326
  });
13047
13327
  }
13328
+ setShown(shown) {
13329
+ this.backdrop.style.display = shown ? "" : "none";
13330
+ this.positioner.style.display = shown ? "" : "none";
13331
+ }
13332
+ /** Keep the just-closed dialog up through its exit animation, then hide it — or
13333
+ * remove it, when it was destroyed meanwhile. `spreadProps` re-applies the close
13334
+ * props' `hidden` only when its value changes, so lifting it keeps the panel and
13335
+ * scrim up until the exit ends. The positioner spans the viewport: inert, it lets
13336
+ * clicks through to the page while the panel animates out. */
13337
+ holdExit() {
13338
+ const held = [this.panel, this.backdrop];
13339
+ for (const el of held) el.hidden = false;
13340
+ this.exit = holdForExit(
13341
+ held,
13342
+ () => {
13343
+ this.exit = null;
13344
+ if (this.destroyed) {
13345
+ this.detach();
13346
+ } else if (!this.open) {
13347
+ for (const el of held) el.hidden = true;
13348
+ this.setShown(false);
13349
+ }
13350
+ },
13351
+ { inert: [this.positioner] }
13352
+ );
13353
+ }
13354
+ detach() {
13355
+ this.backdrop.remove();
13356
+ this.positioner.remove();
13357
+ }
13358
+ /** Closing hands focus back to whatever opened it (Escape, the ✕, hide(), or a
13359
+ * teardown that outruns the machine's close notification). */
13360
+ restoreOpener() {
13361
+ const opener = this.opener;
13362
+ this.opener = null;
13363
+ if (opener?.isConnected) opener.focus({ preventScroll: true });
13364
+ }
13365
+ announce(open2) {
13366
+ const opener = this.opener === this.panel.ownerDocument.body ? null : this.opener;
13367
+ announceSurface(this.panel, open2, "dialog", opener);
13368
+ }
13048
13369
  get open() {
13049
13370
  return this.ctrl.connect(this.handle.service).open;
13050
13371
  }
13051
13372
  show() {
13373
+ this.opener = this.panel.ownerDocument.activeElement;
13052
13374
  this.ctrl.connect(this.handle.service).setOpen(true);
13053
13375
  }
13054
13376
  hide() {
@@ -13061,10 +13383,16 @@ var Dialog = class {
13061
13383
  return el?.closest(".vela-popover, .vela-menu") != null;
13062
13384
  }
13063
13385
  destroy() {
13386
+ if (this.wasOpen && !this.open) this.handle.flush();
13064
13387
  if (this.open) this.hide();
13065
13388
  this.handle.stop();
13066
- this.backdrop.remove();
13067
- this.positioner.remove();
13389
+ this.destroyed = true;
13390
+ if (this.wasOpen) {
13391
+ this.wasOpen = false;
13392
+ this.announce(false);
13393
+ }
13394
+ this.restoreOpener();
13395
+ if (!this.exit) this.detach();
13068
13396
  }
13069
13397
  };
13070
13398
 
@@ -15516,7 +15844,10 @@ var LEGEND_ICON_PX2 = 16;
15516
15844
  var LEGEND_CTL_PX = 18;
15517
15845
  var LEGEND_ROW_PAD_Y = 2;
15518
15846
  var LEGEND_ROW_PAD_X = 6;
15847
+ var LEGEND_LEFT_PX = 10;
15848
+ var LEGEND_SCALE_GAP_PX = 6;
15519
15849
  var LEGEND_TITLE_VALUES_GAP_PX = 6;
15850
+ var LEGEND_VALUES_GAP_PX = 5;
15520
15851
  var LEGEND_TITLE_STATUS_GAP_PX = 8;
15521
15852
  var LEGEND_ACTION_GAP_PX = 6;
15522
15853
  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;`;
@@ -15802,14 +16133,15 @@ var InputsUI = class {
15802
16133
  row.valuesEl.replaceChildren();
15803
16134
  return;
15804
16135
  }
15805
- row.valuesEl.style.display = "inline-flex";
16136
+ row.valuesEl.style.display = "block";
15806
16137
  row.valuesEl.replaceChildren();
15807
- for (const v of row.plotValues) {
16138
+ row.plotValues.forEach((v, i) => {
15808
16139
  const span = document.createElement("span");
15809
16140
  span.textContent = v.value;
15810
16141
  span.style.color = v.color;
16142
+ if (i > 0) span.style.marginLeft = `${LEGEND_VALUES_GAP_PX}px`;
15811
16143
  row.valuesEl.appendChild(span);
15812
- }
16144
+ });
15813
16145
  }
15814
16146
  // ── legend move/merge (menu + drag) ─────────────────────────────────────
15815
16147
  /** Open the "Move to" menu for a row, anchored under its move button. */
@@ -15910,7 +16242,7 @@ var InputsUI = class {
15910
16242
  if (!lg) {
15911
16243
  lg = document.createElement("div");
15912
16244
  lg.dataset.velaPane = paneId;
15913
- 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;";
16245
+ 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;`;
15914
16246
  applyChromeTokens(lg, this.theme);
15915
16247
  this.positionLegend(lg, paneId);
15916
16248
  this.container.appendChild(lg);
@@ -15922,6 +16254,7 @@ var InputsUI = class {
15922
16254
  const bounds = this.paneBoundsOf ? this.paneBoundsOf(paneId) : { top: 0, height: Infinity };
15923
16255
  lg.toggleAttribute(LEGEND_AT_TOP_ATTR, bounds.top === 0);
15924
16256
  lg.style.display = bounds.height < 4 || !this.titlesVisible ? "none" : "flex";
16257
+ lg.style.maxWidth = `calc(100% - ${(bounds.rightAxis ?? 0) + LEGEND_LEFT_PX + LEGEND_SCALE_GAP_PX}px)`;
15925
16258
  const collapsed = this.paneCollapse.has(paneId);
15926
16259
  const masterId = this.paneCollapse.get(paneId) ?? null;
15927
16260
  for (const row of this.rows.values()) {
@@ -16013,7 +16346,7 @@ var InputsUI = class {
16013
16346
  }
16014
16347
  ensureDialogStyles();
16015
16348
  const el = document.createElement("div");
16016
- 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;`;
16349
+ 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;`;
16017
16350
  el.addEventListener("mouseleave", () => {
16018
16351
  if (this.selectedId !== id) this.setRowHighlighted(id, false);
16019
16352
  });
@@ -16041,7 +16374,7 @@ var InputsUI = class {
16041
16374
  const statusEl = document.createElement("span");
16042
16375
  statusEl.style.cssText = "display:none;box-sizing:border-box;flex:none;";
16043
16376
  const titleWrap = document.createElement("span");
16044
- titleWrap.style.cssText = "white-space:nowrap;";
16377
+ titleWrap.style.cssText = "white-space:nowrap;min-width:0;overflow:hidden;text-overflow:ellipsis;";
16045
16378
  titleWrap.addEventListener("mouseenter", () => this.setRowHighlighted(id, true));
16046
16379
  const titleEl = document.createElement("span");
16047
16380
  titleEl.textContent = title;
@@ -16059,7 +16392,7 @@ var InputsUI = class {
16059
16392
  el.appendChild(calloutsEl);
16060
16393
  el.appendChild(statusEl);
16061
16394
  const valuesEl = document.createElement("span");
16062
- 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;`;
16395
+ 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;`;
16063
16396
  el.appendChild(valuesEl);
16064
16397
  const controlsEl = document.createElement("span");
16065
16398
  controlsEl.style.cssText = `display:none;align-items:center;gap:${LEGEND_ACTION_GAP_PX}px;flex:none;margin-left:${LEGEND_ACTION_GAP_PX}px;`;
@@ -17232,15 +17565,23 @@ var Batch = class {
17232
17565
  this.seg(x + w, y + h, x, y + h, lineWidth, c);
17233
17566
  this.seg(x, y + h, x, y, lineWidth, c);
17234
17567
  }
17235
- /** Filled circle (triangle fan). */
17568
+ /**
17569
+ * Filled circle (triangle fan), feathered like a line: the rim is padded by {@link AA_PAD} and
17570
+ * edgeDist runs from 0 at the center to the padded radius at the rim, with edgeHalf = r. The
17571
+ * distance grows linearly along each spoke, so the shader's line feather gives a round soft edge.
17572
+ */
17236
17573
  circle(cx, cy, r, c, segments = 16) {
17237
- let px = cx + r;
17574
+ const ext = r + AA_PAD;
17575
+ let px = cx + ext;
17238
17576
  let py = cy;
17577
+ this.ensure(segments * 3);
17239
17578
  for (let i = 1; i <= segments; i += 1) {
17240
17579
  const a = i / segments * Math.PI * 2;
17241
- const nx = cx + Math.cos(a) * r;
17242
- const ny = cy + Math.sin(a) * r;
17243
- this.tri(cx, cy, c, px, py, c, nx, ny, c);
17580
+ const nx = cx + Math.cos(a) * ext;
17581
+ const ny = cy + Math.sin(a) * ext;
17582
+ this.v(cx, cy, c, 0, r);
17583
+ this.v(px, py, c, ext, r);
17584
+ this.v(nx, ny, c, ext, r);
17244
17585
  px = nx;
17245
17586
  py = ny;
17246
17587
  }
@@ -17421,7 +17762,7 @@ var WebGL2Backend = class {
17421
17762
  }
17422
17763
  mount(canvas) {
17423
17764
  this.canvas = canvas;
17424
- this.gl = canvas.getContext("webgl2", { alpha: true, antialias: true, depth: false, stencil: false, premultipliedAlpha: true });
17765
+ this.gl = canvas.getContext("webgl2", { alpha: true, antialias: false, depth: false, stencil: false, premultipliedAlpha: true });
17425
17766
  if (!this.gl) return;
17426
17767
  if (!this.initGL()) {
17427
17768
  this.gl = null;
@@ -18646,6 +18987,11 @@ var InputController = class {
18646
18987
  /** When true (the default), the separators between stacked panes are draggable to
18647
18988
  * resize the panes above/below. When false, a press there is a normal data-area pan. */
18648
18989
  this.paneResize = true;
18990
+ /** When true (the default), the user drawings take part in pointer input: they claim a
18991
+ * press over a drawing or with a tool armed, and see hover moves. When false, every press
18992
+ * is a plain pan/click and hover leaves them alone; a drawing gesture already under way
18993
+ * still finishes. */
18994
+ this.drawings = true;
18649
18995
  this.el = null;
18650
18996
  this.dragging = false;
18651
18997
  this.moved = false;
@@ -18709,18 +19055,18 @@ var InputController = class {
18709
19055
  this.onModifier = (e) => {
18710
19056
  if (e.repeat || e.key !== "Shift" && e.key !== "Control" && e.key !== "Meta") return;
18711
19057
  if (Number.isNaN(this.cursorX)) return;
18712
- if (this.dragging && this.region !== "drawing") return;
19058
+ if (this.dragging ? this.region !== "drawing" : !this.drawings) return;
18713
19059
  this.deps.drawingsPointerMove?.(this.cursorX, this.cursorY, this.snapMode(e), e.shiftKey, e.ctrlKey || e.metaKey);
18714
19060
  };
18715
19061
  this.onDown = (e) => {
18716
19062
  if (e.button === 1) {
18717
19063
  const { x: x2, y: y2 } = this.local(e);
18718
- this.middleDeleted = this.deps.drawingsDeleteAt?.(x2, y2) ?? false;
19064
+ this.middleDeleted = this.drawings && (this.deps.drawingsDeleteAt?.(x2, y2) ?? false);
18719
19065
  if (this.middleDeleted) e.preventDefault();
18720
19066
  return;
18721
19067
  }
18722
19068
  if (e.button === 2) {
18723
- this.rightCancelled = this.deps.drawingsCancelPlacement?.() ?? false;
19069
+ this.rightCancelled = this.drawings && (this.deps.drawingsCancelPlacement?.() ?? false);
18724
19070
  if (this.rightCancelled) e.preventDefault();
18725
19071
  return;
18726
19072
  }
@@ -18742,20 +19088,20 @@ var InputController = class {
18742
19088
  this.startY = y;
18743
19089
  this.prevPressDrawing = this.lastPressDrawing;
18744
19090
  this.lastPressDrawing = false;
18745
- if (this.deps.drawingsClaim?.(x, y)) {
19091
+ if (this.drawings && this.deps.drawingsClaim?.(x, y)) {
18746
19092
  this.region = "drawing";
18747
19093
  this.lastPressDrawing = true;
18748
19094
  this.deps.drawingsPointerDown?.(x, y, this.snapMode(e), e.shiftKey, e.ctrlKey || e.metaKey);
18749
19095
  this.capture(e.pointerId);
18750
19096
  return;
18751
19097
  }
18752
- if (e.shiftKey && this.regionAt(x, y) === "data" && this.deps.drawingsMeasureStart?.(x, y, this.snapMode(e))) {
19098
+ if (this.drawings && e.shiftKey && this.regionAt(x, y) === "data" && this.deps.drawingsMeasureStart?.(x, y, this.snapMode(e))) {
18753
19099
  this.region = "drawing";
18754
19100
  this.lastPressDrawing = true;
18755
19101
  this.capture(e.pointerId);
18756
19102
  return;
18757
19103
  }
18758
- if ((e.ctrlKey || e.metaKey) && this.regionAt(x, y) === "data" && this.deps.drawingsMarqueeStart?.(x, y)) {
19104
+ if (this.drawings && (e.ctrlKey || e.metaKey) && this.regionAt(x, y) === "data" && this.deps.drawingsMarqueeStart?.(x, y)) {
18759
19105
  this.region = "drawing";
18760
19106
  this.lastPressDrawing = true;
18761
19107
  this.capture(e.pointerId);
@@ -18832,7 +19178,7 @@ var InputController = class {
18832
19178
  const drawCursor = this.deps.drawingsCursor?.(x, y);
18833
19179
  const r = this.regionAt(x, y);
18834
19180
  this.el.style.cursor = drawCursor ?? (r === "price" ? "ns-resize" : r === "time" ? "ew-resize" : r === "separator" ? "row-resize" : "");
18835
- this.deps.drawingsPointerMove?.(x, y, this.snapMode(e), e.shiftKey, e.ctrlKey || e.metaKey);
19181
+ if (this.drawings) this.deps.drawingsPointerMove?.(x, y, this.snapMode(e), e.shiftKey, e.ctrlKey || e.metaKey);
18836
19182
  }
18837
19183
  if (e.pointerType !== "touch") this.deps.onPointerMove(x, y);
18838
19184
  };
@@ -18873,7 +19219,7 @@ var InputController = class {
18873
19219
  this.deps.drawingsPointerUp?.(x, y, this.snapMode(e));
18874
19220
  } else if (tapRelease && this.region === "data") {
18875
19221
  this.deps.onClick(x, y);
18876
- } else if (this.dragging && this.region === "data") {
19222
+ } else if (this.dragging && this.region === "data" && wasTouch) {
18877
19223
  const stale = e.timeStamp - this.lastT > FLING_STALE_MS;
18878
19224
  if (!stale && Math.abs(this.vx) > FLING_MIN_SPEED) {
18879
19225
  const pitch = this.deps.getCoords().pxPerBar();
@@ -19074,7 +19420,7 @@ var InputController = class {
19074
19420
  }
19075
19421
  /** Shared double-click / double-tap routing, by the region under the point. */
19076
19422
  doubleActivate(x, y) {
19077
- if (this.deps.drawingsDblClick?.(x, y)) return;
19423
+ if (this.drawings && this.deps.drawingsDblClick?.(x, y)) return;
19078
19424
  const region = this.regionAt(x, y);
19079
19425
  if (region === "price") this.deps.resetPriceScale(x, y);
19080
19426
  else if (region === "separator") this.deps.resetPaneSize(y);
@@ -21088,10 +21434,13 @@ function priceTicks(min, max, target = 6) {
21088
21434
  const norm = raw / mag;
21089
21435
  const step = (norm < 1.5 ? 1 : norm < 3 ? 2 : norm < 7 ? 5 : 10) * mag;
21090
21436
  const decimals = Math.max(0, -Math.floor(Math.log10(step)) + 1);
21091
- const out = [];
21092
21437
  const start = Math.ceil(min / step) * step;
21093
- for (let v = start; v <= max + step * 1e-6; v += step) {
21094
- out.push(Number(v.toFixed(decimals)));
21438
+ if (!(step > 0) || start + step === start) return [];
21439
+ const count = Math.floor((max - start) / step + 1e-6) + 1;
21440
+ if (!(count > 0) || count > 1e3) return [];
21441
+ const out = [];
21442
+ for (let i = 0; i < count; i += 1) {
21443
+ out.push(Number((start + i * step).toFixed(decimals)));
21095
21444
  }
21096
21445
  return out;
21097
21446
  }
@@ -21289,6 +21638,100 @@ function calendarTicks(zFrom, zTo, months, offsetMs) {
21289
21638
  return out;
21290
21639
  }
21291
21640
 
21641
+ // src/renderers/native/chrome/priceAxisTicks.ts
21642
+ var PriceAxisTickSource = class {
21643
+ constructor() {
21644
+ this.fn = null;
21645
+ this.memo = /* @__PURE__ */ new WeakMap();
21646
+ this.warned = false;
21647
+ }
21648
+ /** The host function, or null for the built-in ticks. */
21649
+ get hook() {
21650
+ return this.fn;
21651
+ }
21652
+ /** Set (or clear) the host function. Drops every memoized result, so setting the same
21653
+ * function again recomputes — how a host refreshes ticks that depend on its own state. */
21654
+ setHook(fn) {
21655
+ this.fn = fn;
21656
+ this.memo = /* @__PURE__ */ new WeakMap();
21657
+ }
21658
+ /** The pane's master-scale ticks for this frame. An unscaled pane (`axisFormat: 'none'`)
21659
+ * gets none, and the host is never asked for it. */
21660
+ ticksFor(scene, pane, coords) {
21661
+ const format = pane.axisFormat;
21662
+ if (format === "none") return [];
21663
+ const pct = percentScaleFor(scene, pane);
21664
+ const height = pane.bounds.height;
21665
+ const fn = this.fn;
21666
+ if (!fn || !(height > 0)) return paneAxisTicks(pane.scale, height, pct, scene.priceMintick, format);
21667
+ const s = pane.scale;
21668
+ const m = this.memo.get(pane);
21669
+ const fontSize = scene.style.fontSize;
21670
+ 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) {
21671
+ return m.ticks;
21672
+ }
21673
+ const defaults2 = paneAxisTicks(s, height, pct, scene.priceMintick, format);
21674
+ const ticks = this.callHook(fn, scene, pane, coords, defaults2, pct?.baseline, pct ? pct.indexed ? "indexed" : "percent" : "price");
21675
+ this.memo.set(pane, {
21676
+ kind: pane.kind,
21677
+ min: s.min,
21678
+ max: s.max,
21679
+ log: !!s.log,
21680
+ invert: !!s.invert,
21681
+ height,
21682
+ baseline: pct?.baseline,
21683
+ indexed: !!pct?.indexed,
21684
+ mintick: scene.priceMintick,
21685
+ format,
21686
+ fontSize,
21687
+ ticks
21688
+ });
21689
+ return ticks;
21690
+ }
21691
+ callHook(fn, scene, pane, coords, defaults2, baseline, mode) {
21692
+ const scale = { min: pane.scale.min, max: pane.scale.max, log: !!pane.scale.log, invert: !!pane.scale.invert };
21693
+ const bounds = { top: 0, height: pane.bounds.height };
21694
+ const ctx = {
21695
+ pane: { id: pane.id, kind: pane.kind },
21696
+ min: Math.min(scale.min, scale.max),
21697
+ max: Math.max(scale.min, scale.max),
21698
+ log: !!scale.log,
21699
+ height: bounds.height,
21700
+ mode,
21701
+ baseline,
21702
+ mintick: scene.priceMintick,
21703
+ fontSize: scene.style.fontSize,
21704
+ priceToY: (price) => coords.priceToY(price, scale, bounds),
21705
+ defaults: defaults2
21706
+ };
21707
+ let out;
21708
+ try {
21709
+ out = fn(ctx);
21710
+ } catch (err) {
21711
+ this.warnOnce("threw", err);
21712
+ return defaults2;
21713
+ }
21714
+ if (out == null) return defaults2;
21715
+ if (!Array.isArray(out)) {
21716
+ this.warnOnce("returned a non-array", out);
21717
+ return defaults2;
21718
+ }
21719
+ const ticks = [];
21720
+ for (const t of out) {
21721
+ if (!t || typeof t !== "object") continue;
21722
+ const { price, label, major } = t;
21723
+ if (typeof price !== "number" || !Number.isFinite(price) || typeof label !== "string") continue;
21724
+ ticks.push(typeof major === "boolean" ? { price, label, major } : { price, label });
21725
+ }
21726
+ return ticks;
21727
+ }
21728
+ warnOnce(what, detail) {
21729
+ if (this.warned) return;
21730
+ this.warned = true;
21731
+ console.warn(`[vela] the priceAxisTicks function ${what} \u2014 the default ticks are used instead.`, detail);
21732
+ }
21733
+ };
21734
+
21292
21735
  // src/renderers/native/chrome/countdown.ts
21293
21736
  function countdownText(barOpen, barMs, now2) {
21294
21737
  if (!(barMs > 0)) return null;
@@ -21641,8 +22084,11 @@ function standaloneSvg(markup, ink, px) {
21641
22084
  }
21642
22085
 
21643
22086
  // src/renderers/native/chrome/ChromeRenderer.ts
22087
+ var MINOR_LABEL_ALPHA = 0.5;
21644
22088
  var ChromeRenderer = class {
21645
- constructor() {
22089
+ /** `axisTicks` is shared with the backdrop layer, so price labels and gridlines agree. */
22090
+ constructor(axisTicks = new PriceAxisTickSource()) {
22091
+ this.axisTicks = axisTicks;
21646
22092
  this.canvas = null;
21647
22093
  this.ctx = null;
21648
22094
  // The color for axis tick labels — the host-passed surface text, set each frame in render().
@@ -21747,7 +22193,8 @@ var ChromeRenderer = class {
21747
22193
  this.drawTimeAxis(ctx, scene, coords, theme, dataW, dataH, fullH);
21748
22194
  this.drawMarkLane(ctx, scene, coords, theme, dataW, dataH);
21749
22195
  }
21750
- /** The timeline-mark lane — after the axis, so the tokens read over the plot's bottom edge. */
22196
+ /** The timeline-mark lane — after the axis, so the tokens read over the plot's bottom edge,
22197
+ * but clipped to the data area so a glyph on the edge bar never paints over the price scale. */
21751
22198
  drawMarkLane(ctx, scene, coords, theme, dataW, dataH) {
21752
22199
  if (!scene.marks.visible || scene.timelineMarks.length === 0) {
21753
22200
  this.markLayout = { glyphs: [], stacks: /* @__PURE__ */ new Map() };
@@ -21765,6 +22212,10 @@ var ChromeRenderer = class {
21765
22212
  expanded: scene.marksExpandedStack
21766
22213
  });
21767
22214
  const nowMs = typeof performance !== "undefined" ? performance.now() : Date.now();
22215
+ ctx.save();
22216
+ ctx.beginPath();
22217
+ ctx.rect(0, 0, dataW, dataH);
22218
+ ctx.clip();
21768
22219
  paintMarkLane(ctx, this.markLayout, {
21769
22220
  axisY: dataH,
21770
22221
  background: theme.background,
@@ -21778,6 +22229,7 @@ var ChromeRenderer = class {
21778
22229
  flashKey: scene.marksFlash && scene.marksFlash.until > nowMs ? scene.marksFlash.key : null,
21779
22230
  nowMs
21780
22231
  });
22232
+ ctx.restore();
21781
22233
  }
21782
22234
  barTimes(scene) {
21783
22235
  if (this.barTimesSrc !== scene.bars || this.barTimesCache.length !== scene.bars.length) {
@@ -21834,13 +22286,19 @@ var ChromeRenderer = class {
21834
22286
  if (!scene.showAxisLabels) return;
21835
22287
  ctx.fillStyle = this.axisTextColor;
21836
22288
  ctx.textAlign = "left";
22289
+ const font = `${scene.style.fontSize}px ${theme.fontFamily}`;
22290
+ const majorFont = `600 ${font}`;
22291
+ const alpha = ctx.globalAlpha;
21837
22292
  for (const pane of panes) {
21838
22293
  if (pane.collapsed) continue;
21839
- const pct = percentScaleFor(scene, pane);
21840
- for (const t of paneAxisTicks(pane.scale, pane.bounds.height, pct, scene.priceMintick, pane.axisFormat)) {
22294
+ for (const t of this.axisTicks.ticksFor(scene, pane, coords)) {
21841
22295
  const y = coords.priceToY(t.price, pane.scale, pane.bounds);
21842
22296
  if (y < pane.bounds.top + 6 || y > pane.bounds.top + pane.bounds.height - 4) continue;
22297
+ if (t.major === true) ctx.font = majorFont;
22298
+ else if (t.major === false) ctx.globalAlpha = alpha * MINOR_LABEL_ALPHA;
21843
22299
  ctx.fillText(t.label, dataW + 6, y);
22300
+ if (t.major === true) ctx.font = font;
22301
+ else if (t.major === false) ctx.globalAlpha = alpha;
21844
22302
  }
21845
22303
  if (pane.axisBands) {
21846
22304
  for (const b of pane.axisBands) {
@@ -22082,15 +22540,23 @@ var CrosshairRenderer = class {
22082
22540
  constructor() {
22083
22541
  this.canvas = null;
22084
22542
  this.ctx = null;
22543
+ this.shade = null;
22085
22544
  }
22086
22545
  mount(canvas) {
22087
22546
  this.canvas = canvas;
22088
22547
  this.ctx = canvas.getContext("2d");
22089
22548
  }
22549
+ /** The plot area the last frame veiled for `crosshairOverride.shadeRight` — from the
22550
+ * picked bar's right edge to the price scale, full plot height (the union when both the
22551
+ * local and a synced crosshair veil) — or null when no veil was painted. */
22552
+ get shadeRightArea() {
22553
+ return this.shade;
22554
+ }
22090
22555
  /** Clear the cursor canvas and (re)draw the crosshair lines + axis chips. The optional
22091
22556
  * `separatorHoverY` highlights the draggable pane separator under the cursor;
22092
22557
  * `external` is a SYNCED ghost crosshair (another chart's pointer, pixel-resolved). */
22093
22558
  render(scene, coords, theme, separatorHoverY = null, external = null) {
22559
+ this.shade = null;
22094
22560
  const ctx = this.ctx;
22095
22561
  const canvas = this.canvas;
22096
22562
  if (!ctx || !canvas) return;
@@ -22113,12 +22579,7 @@ var CrosshairRenderer = class {
22113
22579
  const x = crisp(coords.logicalToX(logical), ov?.width ?? cs.width);
22114
22580
  if (vertical && ov?.shadeRight) {
22115
22581
  const from = Math.max(0, Math.round(coords.logicalToX(logical + 0.5)));
22116
- if (from < dataW) {
22117
- ctx.fillStyle = ov.shadeRight.color;
22118
- ctx.globalAlpha = ov.shadeRight.opacity ?? 1;
22119
- ctx.fillRect(from, 0, dataW - from, dataH);
22120
- ctx.globalAlpha = 1;
22121
- }
22582
+ if (from < dataW) this.paintShade(ctx, ov.shadeRight, from, dataW, dataH);
22122
22583
  }
22123
22584
  ctx.strokeStyle = ov?.color ?? cs.color ?? theme.textColor;
22124
22585
  ctx.lineWidth = ov?.width ?? cs.width;
@@ -22154,6 +22615,7 @@ var CrosshairRenderer = class {
22154
22615
  destroy() {
22155
22616
  this.canvas = null;
22156
22617
  this.ctx = null;
22618
+ this.shade = null;
22157
22619
  }
22158
22620
  /** The synced ghost: a dimmed vertical line at the bar the renderer resolved as
22159
22621
  * CONTAINING the foreign time (+ horizontal line when a comparable price came
@@ -22173,12 +22635,7 @@ var CrosshairRenderer = class {
22173
22635
  if (ov?.vertical === false) return;
22174
22636
  if (ov?.shadeRight) {
22175
22637
  const from = Math.max(0, Math.round(coords.logicalToX(Math.round(coords.xToLogical(ext.x)) + 0.5)));
22176
- if (from < dataW) {
22177
- ctx.fillStyle = ov.shadeRight.color;
22178
- ctx.globalAlpha = ov.shadeRight.opacity ?? 1;
22179
- ctx.fillRect(from, 0, dataW - from, dataH);
22180
- ctx.globalAlpha = 1;
22181
- }
22638
+ if (from < dataW) this.paintShade(ctx, ov.shadeRight, from, dataW, dataH);
22182
22639
  }
22183
22640
  if (ext.line === false || x < 0 || x > dataW) return;
22184
22641
  ctx.font = `${scene.style.fontSize}px ${theme.fontFamily}`;
@@ -22214,6 +22671,15 @@ var CrosshairRenderer = class {
22214
22671
  this.chip(ctx, x, dataH + 1, formatTimeStamp(ext.time, scene.timezone, coords.barInterval), chipBg, "center", true, theme.background);
22215
22672
  ctx.globalAlpha = 1;
22216
22673
  }
22674
+ /** Paint the `shadeRight` veil from `from` to the price scale and record the veiled area. */
22675
+ paintShade(ctx, shade, from, dataW, dataH) {
22676
+ ctx.fillStyle = shade.color;
22677
+ ctx.globalAlpha = shade.opacity ?? 1;
22678
+ ctx.fillRect(from, 0, dataW - from, dataH);
22679
+ ctx.globalAlpha = 1;
22680
+ const x = this.shade ? Math.min(this.shade.x, from) : from;
22681
+ this.shade = { x, width: dataW - x, height: dataH };
22682
+ }
22217
22683
  /** A soft band + a brighter crisp center line over the hovered separator, so it reads as
22218
22684
  * a draggable handle (the cursor is already `row-resize`). Spans the full width (data +
22219
22685
  * scale gutter) to match the separator itself. Theme-derived (textColor). */
@@ -25329,6 +25795,12 @@ var DrawingInteraction = class {
25329
25795
  activeDragId() {
25330
25796
  return this.state.kind === "pressed" && this.state.moved ? this.state.id : null;
25331
25797
  }
25798
+ /** The store drawings a live drag is moving — the pressed one and its riders — or none
25799
+ * before the drag passes the slop. A Ctrl-drag moves copies, so its sources stay put. */
25800
+ movingIds() {
25801
+ if (this.state.kind !== "pressed" || !this.state.moved || this.state.clones) return /* @__PURE__ */ new Set();
25802
+ return /* @__PURE__ */ new Set([this.state.id, ...this.state.riders.map((r) => r.id)]);
25803
+ }
25332
25804
  /** The drawing under a press that has not been released yet (drag or click undecided), or null. */
25333
25805
  pressedId() {
25334
25806
  return this.state.kind === "pressed" ? this.state.id : null;
@@ -25703,15 +26175,16 @@ var DrawingSettingsDialog = class {
25703
26175
  }
25704
26176
  open(drawing, actions, kind) {
25705
26177
  this.close();
25706
- const snapshot = clonePlain(drawing.serialize());
26178
+ const live = actions.resolve() ?? drawing;
26179
+ const snapshot = clonePlain(live.serialize());
25707
26180
  const grid = fieldGrid({ variant: "inputs" });
25708
26181
  grid.style.padding = "16px 20px";
25709
26182
  grid.style.overflowY = "auto";
25710
26183
  grid.style.overflowX = "hidden";
25711
26184
  grid.style.flex = "1 1 auto";
25712
- if (kind === "position" && drawing instanceof PositionTool) this.buildPosition(grid, drawing, actions);
25713
- else if (kind === "frvp" && drawing instanceof FixedRangeVolumeProfile) this.buildFrvp(grid, drawing, actions);
25714
- else if (kind === "levels") this.buildLevels(grid, drawing, actions);
26185
+ if (kind === "position" && live instanceof PositionTool) this.buildPosition(grid, live, actions);
26186
+ else if (kind === "frvp" && live instanceof FixedRangeVolumeProfile) this.buildFrvp(grid, live, actions);
26187
+ else if (kind === "levels") this.buildLevels(grid, live, actions);
25715
26188
  else return;
25716
26189
  const ui = new Dialog({
25717
26190
  host: this.host,
@@ -25987,6 +26460,17 @@ var DrawingSettingsDialog = class {
25987
26460
  }
25988
26461
  }));
25989
26462
  }
26463
+ if (drawing instanceof FibRetracement) {
26464
+ const fib = drawing;
26465
+ grid.appendChild(fieldRow({
26466
+ label: "Reverse",
26467
+ bool: true,
26468
+ toggle: {
26469
+ checked: fib.reverse,
26470
+ onChange: (v) => actions.patch({ reverse: v })
26471
+ }
26472
+ }));
26473
+ }
25990
26474
  levels.forEach((lv, i) => {
25991
26475
  const row = document.createElement("div");
25992
26476
  row.className = "vela-field-span";
@@ -26347,7 +26831,7 @@ var DrawingSettingsPopup = class {
26347
26831
  this.reposition();
26348
26832
  return;
26349
26833
  }
26350
- const text = drawing.text;
26834
+ const text = (actions.resolve() ?? drawing).text;
26351
26835
  const panel = document.createElement("div");
26352
26836
  panel.style.cssText = `padding:6px;border-top:1px solid var(--vela-border);display:flex;flex-direction:column;gap:4px;`;
26353
26837
  const ta = new TextArea({
@@ -26943,6 +27427,7 @@ function sized(svg, size = ICON) {
26943
27427
  // src/renderers/native/drawings/DrawingToolbar.ts
26944
27428
  var TOOLBAR_WIDTH = 44;
26945
27429
  var TOOLBAR_COLLAPSED_WIDTH = 16;
27430
+ var FLYOUT_GAP = 3;
26946
27431
  var DrawingToolbar = class {
26947
27432
  constructor(host, theme, onArm, onMagnet = () => {
26948
27433
  }, onMeasure = () => {
@@ -26972,6 +27457,9 @@ var DrawingToolbar = class {
26972
27457
  // group id (or MAGNET_ID) whose flyout is open
26973
27458
  this.flyoutCell = null;
26974
27459
  // the cell the open flyout is anchored to
27460
+ /** Closed flyouts still playing their exit animation, by owner id — reopening the same
27461
+ * owner takes its element back instead of stacking a second one on it. */
27462
+ this.exitingFlyouts = /* @__PURE__ */ new Map();
26975
27463
  this.groupCells = /* @__PURE__ */ new Map();
26976
27464
  // the composite cell (hover/active bg + flyout anchor)
26977
27465
  this.groupIcons = /* @__PURE__ */ new Map();
@@ -27080,6 +27568,7 @@ var DrawingToolbar = class {
27080
27568
  }
27081
27569
  destroy() {
27082
27570
  this.closeFlyout();
27571
+ for (const { exit } of [...this.exitingFlyouts.values()]) exit.finish();
27083
27572
  for (const dispose of this.tipDisposers.splice(0)) dispose();
27084
27573
  this.root.remove();
27085
27574
  }
@@ -27307,16 +27796,22 @@ var DrawingToolbar = class {
27307
27796
  * outside-dismiss. Callers fill it with items. */
27308
27797
  beginFlyout(ownerId, cell) {
27309
27798
  const t = this.theme;
27310
- const fly = document.createElement("div");
27799
+ const back = this.exitingFlyouts.get(ownerId);
27800
+ if (back) {
27801
+ this.exitingFlyouts.delete(ownerId);
27802
+ back.exit.cancel();
27803
+ back.fly.replaceChildren();
27804
+ }
27805
+ const fly = back?.fly ?? document.createElement("div");
27311
27806
  fly.className = "vela-dtb-flyout";
27312
- 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;`;
27807
+ 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;`;
27313
27808
  applyChromeTokens(fly, t);
27314
27809
  this.host.appendChild(fly);
27315
27810
  const r = cell.getBoundingClientRect();
27316
27811
  const rootR = this.root.getBoundingClientRect();
27317
27812
  const hostR = this.host.getBoundingClientRect();
27318
27813
  const top = r.top - hostR.top;
27319
- fly.style.left = `${rootR.right - hostR.left - 1}px`;
27814
+ fly.style.left = `${rootR.right - hostR.left + FLYOUT_GAP}px`;
27320
27815
  fly.style.top = `${top}px`;
27321
27816
  fly.style.maxHeight = `${Math.max(120, hostR.height - top - 8)}px`;
27322
27817
  this.flyout = fly;
@@ -27332,6 +27827,7 @@ var DrawingToolbar = class {
27332
27827
  if (!cell) return;
27333
27828
  const fly = this.beginFlyout(group.id, cell);
27334
27829
  const sections = group.sections ?? [{ label: "", tools: group.tools }];
27830
+ const badge = sections.some((s) => s.tools.some((tool) => !!tool.icon));
27335
27831
  for (let si = 0; si < sections.length; si++) {
27336
27832
  const section = sections[si];
27337
27833
  if (section.label) {
@@ -27342,6 +27838,7 @@ var DrawingToolbar = class {
27342
27838
  fly.appendChild(
27343
27839
  this.makeFlyoutItem({
27344
27840
  icon: tool.icon,
27841
+ badge,
27345
27842
  label: tool.label,
27346
27843
  selected: tool.type === this.active,
27347
27844
  shortcut: this.shortcuts.get(tool.type),
@@ -27354,6 +27851,7 @@ var DrawingToolbar = class {
27354
27851
  );
27355
27852
  }
27356
27853
  }
27854
+ announceSurface(fly, true, "menu", cell);
27357
27855
  }
27358
27856
  openMagnetFlyout() {
27359
27857
  const cell = this.magnetCell;
@@ -27377,6 +27875,7 @@ var DrawingToolbar = class {
27377
27875
  })
27378
27876
  );
27379
27877
  }
27878
+ announceSurface(fly, true, "menu", cell);
27380
27879
  }
27381
27880
  flyoutHeader(text) {
27382
27881
  const t = this.theme;
@@ -27390,21 +27889,24 @@ var DrawingToolbar = class {
27390
27889
  div.style.cssText = `height:1px;margin:4px 8px;background:${this.borderColor};`;
27391
27890
  return div;
27392
27891
  }
27393
- /** A flyout row, left to right: optional leading icon, label, a check when it's the selected
27394
- * entry, an optional shortcut hint, and — at the far right — the favorite star (tool rows).
27395
- * Hover tint is CSS (`.vela-dtb-item:hover`), so navigating the menu stays smooth. */
27892
+ /** A flyout row, left to right: optional leading icon badge, label, a check when it's the
27893
+ * selected entry, an optional shortcut hint, and — at the far right — the favorite star (tool
27894
+ * rows). Hover tint is CSS (`.vela-dtb-item:hover`), so navigating the menu stays smooth. */
27396
27895
  makeFlyoutItem(opts) {
27397
27896
  const t = this.theme;
27398
27897
  const item = document.createElement("button");
27399
27898
  item.type = "button";
27400
27899
  item.className = "vela-dtb-item";
27401
27900
  item.setAttribute("aria-label", opts.label);
27402
- 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;`;
27403
- if (opts.icon) {
27404
- const icon2 = document.createElement("span");
27405
- icon2.style.cssText = "width:18px;height:18px;display:flex;align-items:center;justify-content:center;flex:none;";
27406
- icon2.innerHTML = sizedIcon(opts.icon);
27407
- item.appendChild(icon2);
27901
+ const badged = opts.badge === true || !!opts.icon;
27902
+ const box = badged ? "gap:10px;padding:4px 10px 4px 6px;" : "gap:8px;padding:5px 10px 5px 8px;";
27903
+ 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;`;
27904
+ if (opts.selected) item.dataset.selected = "1";
27905
+ if (badged) {
27906
+ const badge = document.createElement("span");
27907
+ badge.className = "vela-dtb-badge";
27908
+ if (opts.icon) badge.innerHTML = opts.icon;
27909
+ item.appendChild(badge);
27408
27910
  }
27409
27911
  const label = document.createElement("span");
27410
27912
  label.textContent = opts.label;
@@ -27445,9 +27947,16 @@ var DrawingToolbar = class {
27445
27947
  }
27446
27948
  closeFlyout() {
27447
27949
  this.starEls.clear();
27448
- if (this.flyout) {
27449
- this.flyout.remove();
27950
+ const fly = this.flyout;
27951
+ if (fly) {
27450
27952
  this.flyout = null;
27953
+ announceSurface(fly, false, "menu", this.flyoutCell);
27954
+ const owner = this.flyoutOwnerId ?? "";
27955
+ const exit = holdForExit(fly, () => {
27956
+ if (this.exitingFlyouts.get(owner)?.fly === fly) this.exitingFlyouts.delete(owner);
27957
+ fly.remove();
27958
+ });
27959
+ if (exit) this.exitingFlyouts.set(owner, { fly, exit });
27451
27960
  this.flyoutCell?.classList.remove("vela-open");
27452
27961
  this.flyoutCell = null;
27453
27962
  this.flyoutOwnerId = null;
@@ -27531,6 +28040,11 @@ function ensureStyles4() {
27531
28040
  .vela-dtb[data-collapsed='1'] .vela-dtb-collapse .vela-dtb-hit{width:14px;}
27532
28041
  .vela-dtb-item{background:transparent;border:none;transition:background var(--vela-dur-fast) ease;}
27533
28042
  .vela-dtb-item:hover{background:var(--vela-hover-strong);}
28043
+ .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;}
28044
+ .vela-dtb-badge:empty{background:transparent;border-color:transparent;}
28045
+ .vela-dtb-badge svg{width:100%;height:100%;}
28046
+ .vela-dtb-item:hover .vela-dtb-badge:not(:empty){background:var(--vela-active);color:var(--vela-fg-bright);}
28047
+ .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);}
27534
28048
  .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;}
27535
28049
  .vela-dtb-star svg{width:16px;height:16px;}
27536
28050
  .vela-dtb-item:hover .vela-dtb-star{opacity:.55;}
@@ -27772,6 +28286,8 @@ var UserDrawingController = class {
27772
28286
  this.drawings = [];
27773
28287
  /** Ids painted on an interleave layer this frame — the top canvas paints only their handles. */
27774
28288
  this.sliced = /* @__PURE__ */ new Set();
28289
+ /** Interleaved drawings mid-drag: painted on the top canvas, left out of their slice. */
28290
+ this.lifted = /* @__PURE__ */ new Set();
27775
28291
  /** Cached slice canvases, keyed `paneId|beforeZ`, reused across frames to avoid churn. */
27776
28292
  this.sliceCache = /* @__PURE__ */ new Map();
27777
28293
  /** Series boundaries per pane as of the last `prepareSlices` — lets a repaint between data
@@ -27859,7 +28375,7 @@ var UserDrawingController = class {
27859
28375
  selectedIds: () => this.selectedIds,
27860
28376
  emit: (i) => this.emit(i),
27861
28377
  changed: () => {
27862
- this.invalidateSlices();
28378
+ this.syncLift();
27863
28379
  this.render();
27864
28380
  },
27865
28381
  openSettings: (id, x, y) => this.openSettingsById(id, x, y),
@@ -28148,6 +28664,12 @@ var UserDrawingController = class {
28148
28664
  this.render();
28149
28665
  }
28150
28666
  }
28667
+ /** Drop the hover (its handles) — the pointer no longer reaches the drawings. */
28668
+ clearHover() {
28669
+ if (this.hoveredId == null) return;
28670
+ this.hoveredId = null;
28671
+ this.render();
28672
+ }
28151
28673
  pointerUp(x, y, snap = "off") {
28152
28674
  if (this.eraserMode) {
28153
28675
  this.erasing = false;
@@ -28485,6 +29007,20 @@ var UserDrawingController = class {
28485
29007
  /** A drawing that paints inside the series stack changed (content, not hover): its pixels
28486
29008
  * live in the backend composite, so this layer alone can't show the change — ask for a
28487
29009
  * data frame, which re-runs `prepareSlices` before the backend composites. */
29010
+ /** A drawing being dragged leaves its interleave slice for the top canvas, so each pointer
29011
+ * move repaints one 2D layer instead of re-rasterizing and re-uploading a plot-sized slice
29012
+ * texture through the data frame. The slices rebuild once as the drag starts and once as it
29013
+ * ends (release, cancel), when the lifted set changes. */
29014
+ syncLift() {
29015
+ const moving = this.interaction.movingIds();
29016
+ let same = moving.size === this.lifted.size;
29017
+ if (same) {
29018
+ for (const id of moving) if (!this.lifted.has(id)) same = false;
29019
+ }
29020
+ if (same) return;
29021
+ this.lifted = new Set(moving);
29022
+ this.invalidateSlices();
29023
+ }
28488
29024
  invalidateSlices() {
28489
29025
  if (this.sliced.size > 0 || this.drawings.some((d) => this.isInterleaved(d))) this.deps.requestDataPaint();
28490
29026
  }
@@ -28503,7 +29039,7 @@ var UserDrawingController = class {
28503
29039
  const theme = this.deps.theme();
28504
29040
  const buckets = /* @__PURE__ */ new Map();
28505
29041
  for (const d of this.drawings) {
28506
- if (!d.visible) continue;
29042
+ if (!d.visible || this.lifted.has(d.id)) continue;
28507
29043
  const beforeZ = sliceKeyFor(d.zIndex, this.lastBounds.get(d.paneId) ?? []);
28508
29044
  if (beforeZ === null) continue;
28509
29045
  const key = `${d.paneId}|${beforeZ}`;
@@ -28555,8 +29091,9 @@ var UserDrawingController = class {
28555
29091
  mutedLabel: edited instanceof TextLabel ? edited.id : null
28556
29092
  };
28557
29093
  this.painter.seriesLook = this.deps.seriesLook();
28558
- this.painter.paintAll(ctx, this.drawings.filter((d) => !this.isInterleaved(d)), proj, this.deps.theme(), targets);
28559
- this.painter.paintHighlights(ctx, this.drawings.filter((d) => this.isInterleaved(d)), proj, handleIdsFor(targets));
29094
+ const onTop = (d) => !this.isInterleaved(d) || this.lifted.has(d.id);
29095
+ this.painter.paintAll(ctx, this.drawings.filter(onTop), proj, this.deps.theme(), targets);
29096
+ this.painter.paintHighlights(ctx, this.drawings.filter((d) => !onTop(d)), proj, handleIdsFor(targets));
28560
29097
  const clones = this.interaction.dragClones();
28561
29098
  if (clones) this.painter.paintAll(ctx, clones, proj, this.deps.theme(), { selected: new Set(clones.map((c) => c.id)) });
28562
29099
  this.layoutTextEditor();
@@ -29123,9 +29660,10 @@ function computePaneScale(models, bars, includeCandles, i0, i1, drawings, log =
29123
29660
  consider(drawings.max);
29124
29661
  }
29125
29662
  if (min === Infinity || max === -Infinity) return { min: 0, max: 1 };
29126
- if (min === max) {
29127
- const pad = Math.abs(min) * 0.1 || 1;
29128
- return { min: min - pad, max: max + pad, log: log && min - pad > 0 };
29663
+ if (max - min <= Math.max(Math.abs(min), Math.abs(max)) * 1e-12) {
29664
+ const mid = (min + max) / 2;
29665
+ const pad = Math.abs(mid) * 0.1 || 1;
29666
+ return { min: mid - pad, max: mid + pad, log: log && mid - pad > 0 };
29129
29667
  }
29130
29668
  const content = Math.max(0.1, 1 - (margins.top + margins.bottom) / 100);
29131
29669
  const above = margins.top / 100 / content;
@@ -29629,7 +30167,9 @@ function bandEdgeSlot(logical) {
29629
30167
  return Math.ceil(logical) - 0.5;
29630
30168
  }
29631
30169
  var BackdropRenderer = class {
29632
- constructor() {
30170
+ /** `axisTicks` is shared with the chrome layer, so gridlines and price labels agree. */
30171
+ constructor(axisTicks = new PriceAxisTickSource()) {
30172
+ this.axisTicks = axisTicks;
29633
30173
  this.canvas = null;
29634
30174
  this.ctx = null;
29635
30175
  }
@@ -29706,9 +30246,8 @@ var BackdropRenderer = class {
29706
30246
  if (scene.showGrid && gridHorz.visible && !pane.collapsed) {
29707
30247
  ctx.globalAlpha = gridAlpha;
29708
30248
  ctx.strokeStyle = horzColor;
29709
- const pct = percentScaleFor(scene, pane);
29710
30249
  ctx.beginPath();
29711
- for (const t of paneAxisTicks(pane.scale, pane.bounds.height, pct, void 0, pane.axisFormat)) {
30250
+ for (const t of this.axisTicks.ticksFor(scene, pane, coords)) {
29712
30251
  const y = Math.round(coords.priceToY(t.price, pane.scale, pane.bounds)) + 0.5;
29713
30252
  if (y < pane.bounds.top || y > pane.bounds.top + pane.bounds.height) continue;
29714
30253
  ctx.moveTo(0, y);
@@ -30324,8 +30863,16 @@ var NativeRenderer = class {
30324
30863
  this.toolbarGutter = 0;
30325
30864
  // px reserved on the left for the docked drawings toolbar (0 when hidden)
30326
30865
  this.mountContainer = null;
30866
+ /** The host-styleable twin of the `shadeRight` veil (`.vela-shade-right`), directly under
30867
+ * the cursor canvas; hidden whenever the crosshair layer paints no veil. */
30868
+ this.shadeRightEl = null;
30869
+ /** The veil geometry last written to {@link shadeRightEl} ('' = hidden) — the DOM is
30870
+ * touched only when it changes, never on a plain pointer move. */
30871
+ this.shadeRightSig = "";
30327
30872
  this.userDrawings = null;
30328
- this.backdropRenderer = new BackdropRenderer();
30873
+ /** One price-axis tick source for the gridlines (backdrop) and the labels (chrome). */
30874
+ this.priceAxisTicks = new PriceAxisTickSource();
30875
+ this.backdropRenderer = new BackdropRenderer(this.priceAxisTicks);
30329
30876
  this.volumeRenderer = new VolumeRenderer();
30330
30877
  /** SDK renderer layers instantiated at mount ({@link registerRendererLayer}). */
30331
30878
  /**
@@ -30358,7 +30905,7 @@ var NativeRenderer = class {
30358
30905
  this.backendMode = "auto";
30359
30906
  this.glowAmount = 0;
30360
30907
  // WebGL2 neon-glow intensity (canvas2d ignores it)
30361
- this.chrome = new ChromeRenderer();
30908
+ this.chrome = new ChromeRenderer(this.priceAxisTicks);
30362
30909
  /** Prepaints each indicator's Pine drawings into interleave slices at the model's z. */
30363
30910
  this.indicatorSlices = new IndicatorDrawingSlices();
30364
30911
  /** Hover tooltips for Pine labels (canvas hit-rects collected by the chrome layer). */
@@ -30475,6 +31022,8 @@ var NativeRenderer = class {
30475
31022
  // pane just below it
30476
31023
  this.resizeSplitStart = null;
30477
31024
  // the two panes' shared span when the drag began
31025
+ /** The user drawings take part in pointer input (the runtime `drawingsInteractive` feature). */
31026
+ this.drawingsInteractive = true;
30478
31027
  this.hoverSeparatorY = null;
30479
31028
  // pixel y of the separator under the cursor (drives its hover highlight)
30480
31029
  // ── pane management (merge / reorder / collapse / maximize) ──
@@ -30530,8 +31079,9 @@ var NativeRenderer = class {
30530
31079
  this.toggleVisibleCbs = /* @__PURE__ */ new Set();
30531
31080
  this.moveIndicatorCbs = /* @__PURE__ */ new Set();
30532
31081
  this.priceStyleCbs = /* @__PURE__ */ new Set();
31082
+ this.priceStyleWillChangeCbs = /* @__PURE__ */ new Set();
30533
31083
  this.name = "native";
30534
- 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"];
31084
+ 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"];
30535
31085
  /** Track cursor proximity to the scroll button on the plot (bubbles from the button too,
30536
31086
  * so moving onto the button doesn't count as leaving). */
30537
31087
  this.onScrollProximityMove = (e) => {
@@ -30575,6 +31125,7 @@ var NativeRenderer = class {
30575
31125
  this.scene.priceStyle = opts.priceStyle;
30576
31126
  this.scene.basePainting = basePaintingOf(opts.priceStyle);
30577
31127
  this.scene.candleOverride = candleOverrideFor(opts.priceStyle, this.scene.style.chartTypes);
31128
+ if (typeof opts.priceAxisTicks === "function") this.priceAxisTicks.setHook(opts.priceAxisTicks);
30578
31129
  }
30579
31130
  this.theme = this.deriveTheme(DARK_THEME);
30580
31131
  }
@@ -30713,6 +31264,15 @@ var NativeRenderer = class {
30713
31264
  case "crosshairOverride":
30714
31265
  this.scene.crosshairOverride = sanitizeCrosshairOverride(value);
30715
31266
  break;
31267
+ case "drawingsInteractive":
31268
+ this.drawingsInteractive = value !== false;
31269
+ if (this.input) this.input.drawings = this.drawingsInteractive;
31270
+ if (!this.drawingsInteractive) this.userDrawings?.clearHover();
31271
+ return;
31272
+ // affects the next gesture only — clearHover repaints what it drops
31273
+ case "priceAxisTicks":
31274
+ this.priceAxisTicks.setHook(typeof value === "function" ? value : null);
31275
+ break;
30716
31276
  case "keyboard":
30717
31277
  this.setKeyboardEnabled(Boolean(value));
30718
31278
  return;
@@ -30828,6 +31388,10 @@ var NativeRenderer = class {
30828
31388
  return { visible: this.scene.marks.visible, groups: { ...this.scene.marks.groups } };
30829
31389
  case "crosshairOverride":
30830
31390
  return this.scene.crosshairOverride ? { ...this.scene.crosshairOverride } : null;
31391
+ case "drawingsInteractive":
31392
+ return this.drawingsInteractive;
31393
+ case "priceAxisTicks":
31394
+ return this.priceAxisTicks.hook;
30831
31395
  case "keyboard":
30832
31396
  return this.keyboardEnabled;
30833
31397
  case "historyChords":
@@ -31501,6 +32065,12 @@ var NativeRenderer = class {
31501
32065
  Object.assign(this.drawingsCanvas.style, { position: "absolute", inset: "0", width: "100%", height: "100%", pointerEvents: "none" });
31502
32066
  this.cursorCanvas = document.createElement("canvas");
31503
32067
  Object.assign(this.cursorCanvas.style, { position: "absolute", inset: "0", width: "100%", height: "100%", pointerEvents: "none" });
32068
+ this.shadeRightEl = document.createElement("div");
32069
+ this.shadeRightEl.className = "vela-shade-right";
32070
+ this.shadeRightEl.setAttribute("aria-hidden", "true");
32071
+ this.shadeRightEl.hidden = true;
32072
+ Object.assign(this.shadeRightEl.style, { position: "absolute", top: "0", left: "0", width: "0", height: "0", pointerEvents: "none", display: "none" });
32073
+ this.shadeRightSig = "";
31504
32074
  this.overlayRoot = document.createElement("div");
31505
32075
  Object.assign(this.overlayRoot.style, { position: "absolute", inset: "0", pointerEvents: "none" });
31506
32076
  this.plot = document.createElement("div");
@@ -31508,7 +32078,7 @@ var NativeRenderer = class {
31508
32078
  this.extLayers = rendererLayers().map((def) => ({ def, instance: def.create(), canvas: this.createLayerCanvas(), channel: def.id, owner: null }));
31509
32079
  const below = this.extLayers.filter((l) => l.def.placement === "below-data").map((l) => l.canvas);
31510
32080
  const above = this.extLayers.filter((l) => l.def.placement !== "below-data").map((l) => l.canvas);
31511
- this.plot.append(this.backdropCanvas, ...below, this.dataCanvas, this.volumeCanvas, this.vpvrCanvas, ...above, this.chromeCanvas, this.drawingsCanvas, this.cursorCanvas, this.overlayRoot);
32081
+ this.plot.append(this.backdropCanvas, ...below, this.dataCanvas, this.volumeCanvas, this.vpvrCanvas, ...above, this.chromeCanvas, this.drawingsCanvas, this.shadeRightEl, this.cursorCanvas, this.overlayRoot);
31512
32082
  this.layerOrderSig = "";
31513
32083
  this.wrapper.appendChild(this.plot);
31514
32084
  this.factoryConfig = this.getConfig();
@@ -31566,13 +32136,14 @@ var NativeRenderer = class {
31566
32136
  drawingsPointerDown: (x, y, snap, shift2, mod) => this.userDrawings?.pointerDown(x, y, snap, shift2, mod),
31567
32137
  drawingsPointerMove: (x, y, snap, shift2, mod) => this.userDrawings?.pointerMove(x, y, snap, shift2, mod),
31568
32138
  drawingsPointerUp: (x, y, snap) => this.userDrawings?.pointerUp(x, y, snap),
31569
- drawingsCursor: (x, y) => this.userDrawings?.cursorAt(x, y) ?? (this.chrome.markGlyphAt(x, y) ? "pointer" : null),
32139
+ drawingsCursor: (x, y) => (this.drawingsInteractive ? this.userDrawings?.cursorAt(x, y) : null) ?? (this.chrome.markGlyphAt(x, y) ? "pointer" : null),
31570
32140
  drawingsDblClick: (x, y) => this.userDrawings?.dblClick(x, y) ?? false,
31571
32141
  drawingsClearTransient: () => this.userDrawings?.clearTransient()
31572
32142
  });
31573
32143
  this.input.rightEdgeZoom = this.zoomAnchorMode === "right";
31574
32144
  this.input.axisDrag = this.axisDragEnabled;
31575
32145
  this.input.paneResize = this.paneResizeEnabled;
32146
+ this.input.drawings = this.drawingsInteractive;
31576
32147
  this.input.attach(this.dataCanvas);
31577
32148
  if (this.settingsEnabled) this.setSettingsEnabled(true);
31578
32149
  this.syncCountdownTimer();
@@ -31881,6 +32452,8 @@ var NativeRenderer = class {
31881
32452
  this.backend.destroy();
31882
32453
  this.chrome.destroy();
31883
32454
  this.crosshairLayer.destroy();
32455
+ this.shadeRightEl?.remove();
32456
+ this.shadeRightEl = null;
31884
32457
  this.attributionEl?.remove();
31885
32458
  this.attributionEl = null;
31886
32459
  this.mountContainer?.style.removeProperty("--vela-toolbar-gutter");
@@ -32227,14 +32800,21 @@ var NativeRenderer = class {
32227
32800
  this.priceStyleCbs.add(cb);
32228
32801
  return () => this.priceStyleCbs.delete(cb);
32229
32802
  }
32803
+ onPriceStyleWillChange(cb) {
32804
+ this.priceStyleWillChangeCbs.add(cb);
32805
+ return () => this.priceStyleWillChangeCbs.delete(cb);
32806
+ }
32230
32807
  /**
32231
32808
  * THE single write path for the base price style at runtime (feature set / settings dialog /
32232
- * config template — the constructor seeds the field directly, pre-listeners). Updates the
32233
- * scene, eases any reveal layer toward the new style's target, and notifies the core —
32234
- * which owns the DATA side of styles that need one (a chart type's SeriesDataEngine).
32809
+ * config template — the constructor seeds the field directly, pre-listeners). Announces the
32810
+ * switch while the scene still holds the old style (a listener may paint/capture that frame),
32811
+ * then updates the scene, eases any reveal layer toward the new style's target, and notifies
32812
+ * the core — which owns the DATA side of styles that need one (a chart type's SeriesDataEngine).
32235
32813
  */
32236
32814
  setPriceStyle(style) {
32237
32815
  if (style === this.scene.priceStyle) return;
32816
+ const from = this.scene.priceStyle;
32817
+ for (const cb of this.priceStyleWillChangeCbs) cb(from, style);
32238
32818
  this.scene.priceStyle = style;
32239
32819
  this.scene.basePainting = basePaintingOf(style);
32240
32820
  this.scene.candleOverride = candleOverrideFor(style, this.scene.style.chartTypes);
@@ -32464,7 +33044,7 @@ var NativeRenderer = class {
32464
33044
  if (this.easeLiveBar(dtMs)) active = true;
32465
33045
  this.skeletonClockMs += dtMs;
32466
33046
  this.paintData();
32467
- this.crosshairLayer.render(this.scene, this.coords, this.theme, this.hoverSeparatorY, this.externalCrossPx());
33047
+ this.renderCrosshairLayer();
32468
33048
  this.updateLegendValues();
32469
33049
  this.emitViewportChange();
32470
33050
  return active;
@@ -33039,10 +33619,31 @@ var NativeRenderer = class {
33039
33619
  this.chrome.render(this.scene, this.coords, this.theme, this.axisSurface());
33040
33620
  this.trackMarkPopover();
33041
33621
  }
33042
- this.crosshairLayer.render(this.scene, this.coords, this.theme, this.hoverSeparatorY, this.externalCrossPx());
33622
+ this.renderCrosshairLayer();
33043
33623
  if (!repaintsData(level) && this.paintedData) this.repaintCursorLayers();
33044
33624
  this.updateLegendValues();
33045
33625
  }
33626
+ /** Repaint the L2 crosshair canvas and keep the `.vela-shade-right` element on the veil it painted. */
33627
+ renderCrosshairLayer() {
33628
+ this.crosshairLayer.render(this.scene, this.coords, this.theme, this.hoverSeparatorY, this.externalCrossPx());
33629
+ this.syncShadeRight();
33630
+ }
33631
+ /** Position (or hide) the `.vela-shade-right` element over the veil of the last crosshair frame. */
33632
+ syncShadeRight() {
33633
+ const el = this.shadeRightEl;
33634
+ if (!el) return;
33635
+ const area = this.crosshairLayer.shadeRightArea;
33636
+ const sig = area ? `${area.x},${area.width},${area.height}` : "";
33637
+ if (sig === this.shadeRightSig) return;
33638
+ this.shadeRightSig = sig;
33639
+ if (!area) {
33640
+ el.hidden = true;
33641
+ el.style.display = "none";
33642
+ return;
33643
+ }
33644
+ Object.assign(el.style, { left: `${area.x}px`, width: `${area.width}px`, height: `${area.height}px`, display: "" });
33645
+ el.hidden = false;
33646
+ }
33046
33647
  /** Repaint the SDK layers that opted into cursor tracking (their own canvas only). */
33047
33648
  repaintCursorLayers() {
33048
33649
  const pane = this.scene.panes.get(PRICE_PANE_ID);
@@ -33836,7 +34437,7 @@ var NativeRenderer = class {
33836
34437
  if (this.animator.active) {
33837
34438
  this.computeScales();
33838
34439
  this.paintData();
33839
- this.crosshairLayer.render(this.scene, this.coords, this.theme, this.hoverSeparatorY, this.externalCrossPx());
34440
+ this.renderCrosshairLayer();
33840
34441
  } else {
33841
34442
  this.scheduler.flushNow(4 /* Full */);
33842
34443
  }
@@ -37880,7 +38481,8 @@ var Vela = class {
37880
38481
  glow: options.glow ?? 0,
37881
38482
  upColor: options.upColor ?? BULLISH,
37882
38483
  downColor: options.downColor ?? BEARISH,
37883
- priceStyle: options.priceStyle ?? "candles"
38484
+ priceStyle: options.priceStyle ?? "candles",
38485
+ priceAxisTicks: options.priceAxis?.ticks ?? null
37884
38486
  };
37885
38487
  const RendererClass = options.renderer ?? NativeRenderer;
37886
38488
  if (typeof RendererClass !== "function") {
@@ -38366,6 +38968,23 @@ function resolveEngines(overrides) {
38366
38968
  return { ...Object.fromEntries(defaultEngines), ...overrides };
38367
38969
  }
38368
38970
 
38971
+ // src/widget/context-menu-model.ts
38972
+ var CONTEXT_MENU_BUILTIN_ORDER = {
38973
+ body: { "reset-view": -30, "remove-drawings": -20, "remove-indicators": -10, settings: 1e3 },
38974
+ "price-axis": {
38975
+ auto: -80,
38976
+ invert: -70,
38977
+ "scale:regular": -60,
38978
+ "scale:percent": -50,
38979
+ "scale:indexed": -40,
38980
+ "scale:log": -30,
38981
+ labels: -20,
38982
+ levels: -10,
38983
+ settings: 1e3
38984
+ },
38985
+ "time-axis": { timezone: -10, settings: 1e3 }
38986
+ };
38987
+
38369
38988
  // src/widget/side-panel.ts
38370
38989
  var DEFAULT_PANEL_WIDTH = 280;
38371
38990
  var DEFAULT_PANEL_MIN_WIDTH = 200;
@@ -38384,6 +39003,7 @@ exports.BULLISH = BULLISH;
38384
39003
  exports.BarStore = BarStore;
38385
39004
  exports.CATEGORICAL = CATEGORICAL;
38386
39005
  exports.CHIP_PLATE = CHIP_PLATE;
39006
+ exports.CONTEXT_MENU_BUILTIN_ORDER = CONTEXT_MENU_BUILTIN_ORDER;
38387
39007
  exports.CROSSHAIR = CROSSHAIR;
38388
39008
  exports.CachingDataFeed = CachingDataFeed;
38389
39009
  exports.DARK_THEME = DARK_THEME;