@luxalgo/vela 0.6.21 → 0.6.22

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 (41) hide show
  1. package/dist/{DataProvider-Cb-2lC5O.d.ts → DataProvider-Bu9E5Zh5.d.ts} +1 -1
  2. package/dist/{DataProvider-UtWw2CgJ.d.cts → DataProvider-Ck2qyS_9.d.cts} +1 -1
  3. package/dist/{chunk-BRPFYKTT.js → chunk-A347YL2P.js} +30 -7
  4. package/dist/{chunk-TRZQQUTR.js → chunk-A4G64KVF.js} +1442 -126
  5. package/dist/{chunk-T5Z5YUCF.js → chunk-CZRNTHD6.js} +24 -2
  6. package/dist/{chunk-XCBJU674.js → chunk-UHXG5J7C.js} +1 -1
  7. package/dist/{contributions-D8HdlKmp.d.cts → contributions-BjQ7hyIx.d.ts} +108 -3
  8. package/dist/{contributions-FqIWhN4p.d.ts → contributions-Cv2Mutvn.d.cts} +108 -3
  9. package/dist/index.cjs +1465 -125
  10. package/dist/index.d.cts +76 -20
  11. package/dist/index.d.ts +76 -20
  12. package/dist/index.js +2 -2
  13. package/dist/{options-BCRmYALw.d.cts → options-ex2_gtKp.d.cts} +200 -12
  14. package/dist/{options-BCRmYALw.d.ts → options-ex2_gtKp.d.ts} +200 -12
  15. package/dist/{plugin-B4mnNAeh.d.ts → plugin-CiyhU7pi.d.ts} +3 -3
  16. package/dist/{plugin-Ccupy_BV.d.cts → plugin-absNH7Yn.d.cts} +3 -3
  17. package/dist/plugin.d.cts +4 -4
  18. package/dist/plugin.d.ts +4 -4
  19. package/dist/providers/binance.d.cts +2 -2
  20. package/dist/providers/binance.d.ts +2 -2
  21. package/dist/providers/coinbase.d.cts +2 -2
  22. package/dist/providers/coinbase.d.ts +2 -2
  23. package/dist/providers/hyperliquid.d.cts +2 -2
  24. package/dist/providers/hyperliquid.d.ts +2 -2
  25. package/dist/{statusline-model-CCP1zm_Z.d.ts → statusline-model-BXoU4Kua.d.ts} +8 -4
  26. package/dist/{statusline-model-E7CMw5pQ.d.cts → statusline-model-cRhPBLPO.d.cts} +8 -4
  27. package/dist/ui.cjs +24 -2
  28. package/dist/ui.d.cts +9 -3
  29. package/dist/ui.d.ts +9 -3
  30. package/dist/ui.js +2 -2
  31. package/dist/vela.global.js +1465 -125
  32. package/dist/vela.global.min.js +77 -53
  33. package/dist/widget.cjs +1490 -129
  34. package/dist/widget.d.cts +6 -6
  35. package/dist/widget.d.ts +6 -6
  36. package/dist/widget.js +5 -5
  37. package/dist/workspace.cjs +1490 -129
  38. package/dist/workspace.d.cts +8 -5
  39. package/dist/workspace.d.ts +8 -5
  40. package/dist/workspace.js +4 -4
  41. package/package.json +1 -1
@@ -2,19 +2,48 @@ var Vela = (function (exports) {
2
2
  'use strict';
3
3
 
4
4
  // src/core/options.ts
5
+ var ZOOM_EASE_DEFAULT_MS = 70;
6
+ var PAN_INERTIA_DEFAULT_MS = 110;
7
+ var SCROLL_EASE_DEFAULT_MS = 130;
8
+ var AUTOSCALE_EASE_DEFAULT_MS = 80;
5
9
  var LIVE_BAR_EASE_DEFAULT_MS = 90;
6
- var LIVE_BAR_EASE_MAX_MS = 1e3;
7
- function resolveLiveBarEaseMs(value) {
8
- if (value === true) return LIVE_BAR_EASE_DEFAULT_MS;
10
+ var INTRO_DURATION_DEFAULT_MS = 650;
11
+ var ANIMATION_EASE_MAX_MS = 1e3;
12
+ var LIVE_BAR_EASE_MAX_MS = ANIMATION_EASE_MAX_MS;
13
+ var INTRO_DURATION_MAX_MS = 5e3;
14
+ function resolveEaseMs(value, defaultMs, maxMs = ANIMATION_EASE_MAX_MS) {
15
+ if (value === true) return defaultMs;
9
16
  if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) return 0;
10
- return Math.min(value, LIVE_BAR_EASE_MAX_MS);
17
+ return Math.min(value, maxMs);
18
+ }
19
+ function resolveLiveBarEaseMs(value) {
20
+ return resolveEaseMs(value, LIVE_BAR_EASE_DEFAULT_MS, LIVE_BAR_EASE_MAX_MS);
21
+ }
22
+ function resolveIntro(value) {
23
+ const off = { style: false, duration: 0 };
24
+ if (value === true) return { style: "settle", duration: INTRO_DURATION_DEFAULT_MS };
25
+ if (value === "settle" || value === "grow") return { style: value, duration: INTRO_DURATION_DEFAULT_MS };
26
+ if (value && typeof value === "object") {
27
+ const o = value;
28
+ const d = o.duration;
29
+ return {
30
+ style: o.style === "grow" ? "grow" : "settle",
31
+ duration: typeof d === "number" && Number.isFinite(d) && d > 0 ? Math.min(d, INTRO_DURATION_MAX_MS) : INTRO_DURATION_DEFAULT_MS
32
+ };
33
+ }
34
+ return off;
11
35
  }
12
36
  function resolveAnimations(animations) {
13
- if (typeof animations === "boolean") return { animZoom: animations, animPan: animations, animLiveBar: 0 };
37
+ if (animations === false) return { animZoom: 0, animPan: 0, animScroll: 0, animAutoscale: 0, animLiveBar: 0, animIntro: { style: false, duration: 0 } };
38
+ const cfg = animations === true || animations == null ? {} : animations;
39
+ const animPan = resolveEaseMs(cfg.pan ?? true, PAN_INERTIA_DEFAULT_MS);
14
40
  return {
15
- animZoom: animations?.zoom ?? true,
16
- animPan: animations?.pan ?? true,
17
- animLiveBar: resolveLiveBarEaseMs(animations?.liveBar)
41
+ animZoom: resolveEaseMs(cfg.zoom ?? true, ZOOM_EASE_DEFAULT_MS),
42
+ animPan,
43
+ animScroll: cfg.scroll === void 0 ? animPan > 0 ? SCROLL_EASE_DEFAULT_MS : 0 : resolveEaseMs(cfg.scroll, SCROLL_EASE_DEFAULT_MS),
44
+ animAutoscale: resolveEaseMs(cfg.autoscale ?? true, AUTOSCALE_EASE_DEFAULT_MS),
45
+ animLiveBar: resolveLiveBarEaseMs(cfg.liveBar),
46
+ animIntro: resolveIntro(cfg.intro ?? true)
18
47
  };
19
48
  }
20
49
 
@@ -7057,6 +7086,93 @@ var Vela = (function (exports) {
7057
7086
  }
7058
7087
  };
7059
7088
 
7089
+ // src/core/marks/MarksController.ts
7090
+ var MarksController = class {
7091
+ constructor(renderer, events) {
7092
+ this.renderer = renderer;
7093
+ /** Insertion-ordered — the order a cluster falls back to for equal times. */
7094
+ this.marks = /* @__PURE__ */ new Map();
7095
+ this.groups = /* @__PURE__ */ new Map();
7096
+ this.subs = [];
7097
+ this.enabled = !!renderer.capabilities.timelineMarks && typeof renderer.setTimelineMarks === "function";
7098
+ if (this.enabled && renderer.onMarkClick) this.subs.push(renderer.onMarkClick((e) => events.emit("mark:click", e)));
7099
+ }
7100
+ /** Whether the active renderer paints timeline marks. */
7101
+ get supported() {
7102
+ return this.enabled;
7103
+ }
7104
+ /** Add (or replace, by id) one mark. */
7105
+ add(mark) {
7106
+ this.marks.set(mark.id, validateMark(mark));
7107
+ this.sync();
7108
+ }
7109
+ /** Replace the whole set — a market switch. */
7110
+ set(marks) {
7111
+ this.marks.clear();
7112
+ for (const m of marks) this.marks.set(m.id, validateMark(m));
7113
+ this.sync();
7114
+ }
7115
+ remove(id) {
7116
+ const had = this.marks.delete(id);
7117
+ if (had) this.sync();
7118
+ return had;
7119
+ }
7120
+ clear() {
7121
+ if (this.marks.size === 0) return;
7122
+ this.marks.clear();
7123
+ this.sync();
7124
+ }
7125
+ /** Every mark, in insertion order (shallow copies — mutating one changes nothing). */
7126
+ all() {
7127
+ return [...this.marks.values()].map((m) => ({ ...m, glyph: { ...m.glyph } }));
7128
+ }
7129
+ /** Define (or replace) a group's presentation — its settings label and default visibility. */
7130
+ defineGroup(group) {
7131
+ if (!group || typeof group.id !== "string" || group.id.length === 0) throw new Error("[vela] marks.defineGroup: `id` must be a non-empty string");
7132
+ if (typeof group.label !== "string") throw new Error(`[vela] marks.defineGroup: group "${group.id}" needs a string \`label\``);
7133
+ this.groups.set(group.id, { ...group });
7134
+ this.sync();
7135
+ }
7136
+ /** The defined groups, in definition order. */
7137
+ groupDefinitions() {
7138
+ return [...this.groups.values()].map((g) => ({ ...g }));
7139
+ }
7140
+ /**
7141
+ * Show or hide one group's marks. The choice lives in the renderer's cosmetic config
7142
+ * (the `marks` feature) — what the settings dialog's Events checkboxes edit and what
7143
+ * a persisted chart restores — so it warns + no-ops on a renderer without it.
7144
+ */
7145
+ setGroupVisible(id, visible) {
7146
+ if (!this.renderer.features.includes("marks")) {
7147
+ console.warn(`[vela] renderer "${this.renderer.name}" does not paint timeline marks \u2014 setGroupVisible ignored.`);
7148
+ return;
7149
+ }
7150
+ this.renderer.applyFeature("marks", { groups: { [id]: visible } });
7151
+ }
7152
+ /** A group's effective visibility: the user's (persisted) choice, else the group's declared default, else visible. */
7153
+ isGroupVisible(id) {
7154
+ const state = this.renderer.readFeature("marks");
7155
+ const chosen = state?.groups?.[id];
7156
+ if (typeof chosen === "boolean") return chosen;
7157
+ return this.groups.get(id)?.visible !== false;
7158
+ }
7159
+ destroy() {
7160
+ for (const unsub of this.subs) unsub();
7161
+ this.subs.length = 0;
7162
+ }
7163
+ sync() {
7164
+ if (!this.enabled) return;
7165
+ this.renderer.setTimelineMarks([...this.marks.values()], [...this.groups.values()]);
7166
+ }
7167
+ };
7168
+ function validateMark(mark) {
7169
+ if (!mark || typeof mark !== "object") throw new Error("[vela] marks: a mark must be an object");
7170
+ if (typeof mark.id !== "string" || mark.id.length === 0) throw new Error("[vela] marks: `id` must be a non-empty string");
7171
+ if (typeof mark.time !== "number" || !Number.isFinite(mark.time)) throw new Error(`[vela] marks: mark "${mark.id}" needs a finite epoch-ms \`time\``);
7172
+ if (!mark.glyph || typeof mark.glyph !== "object" || typeof mark.glyph.color !== "string") throw new Error(`[vela] marks: mark "${mark.id}" needs a glyph with a \`color\``);
7173
+ return { ...mark, glyph: { ...mark.glyph } };
7174
+ }
7175
+
7060
7176
  // src/data/timeframe.ts
7061
7177
  var NAMED_TF_MS = { D: 864e5, W: 6048e5, M: 2592e6 };
7062
7178
  function timeframeToMs(timeframe) {
@@ -7735,6 +7851,7 @@ var Vela = (function (exports) {
7735
7851
  marketKey: () => `${this.config.market.symbol ?? ""}|${this.config.market.session ?? ""}`
7736
7852
  });
7737
7853
  this.drawings = new DrawingController(this.renderer, this.events, config.drawings, drawingSeries);
7854
+ this.marks = new MarksController(this.renderer, this.events);
7738
7855
  this.unresolvedUnsub = this.feed.onUnresolved?.((info) => {
7739
7856
  this.endLoad();
7740
7857
  this.events.emit("data:unresolved", info);
@@ -8599,6 +8716,7 @@ var Vela = (function (exports) {
8599
8716
  record.session = void 0;
8600
8717
  record.native?.instance.suspend();
8601
8718
  if (record.renderHandle) this.renderer.setIndicatorVisible?.(record.renderHandle, false);
8719
+ else if (record.native) this.mountHiddenNativeRow(id, record);
8602
8720
  } else {
8603
8721
  if (record.renderHandle) this.renderer.setIndicatorVisible?.(record.renderHandle, true);
8604
8722
  record.pendingStructural = true;
@@ -8686,6 +8804,7 @@ var Vela = (function (exports) {
8686
8804
  this.unresolvedUnsub = null;
8687
8805
  this.feed.destroy?.();
8688
8806
  this.drawings.destroy();
8807
+ this.marks.destroy();
8689
8808
  this.renderer.destroy();
8690
8809
  this.events.clear();
8691
8810
  }
@@ -8799,6 +8918,7 @@ var Vela = (function (exports) {
8799
8918
  overlay: d.overlay,
8800
8919
  paneHint: d.paneHint,
8801
8920
  native: { type: record.native.type },
8921
+ ...d.legend === false ? { legend: false } : {},
8802
8922
  ...out.paneAxis != null ? { paneAxis: out.paneAxis } : {},
8803
8923
  series: out.series ?? [],
8804
8924
  fills: out.fills ?? [],
@@ -8821,9 +8941,17 @@ var Vela = (function (exports) {
8821
8941
  * over it in place (`pendingStructural`), clears the spinner, and only THEN fires
8822
8942
  * `indicator:added`/`ready` — so event semantics and `inspect()` (which skips
8823
8943
  * loading records) still mean "the indicator produced output".
8944
+ *
8945
+ * A HIDDEN record mounts too — dimmed, no spinner (its session never starts while
8946
+ * hidden, so nothing is computing and no model will ever arrive to mount the row
8947
+ * later). Without this an indicator ADDED hidden (a restored ledger/ext entry) had
8948
+ * no legend row at all: invisible AND unreachable — the eye that unhides it never
8949
+ * existed. The hidden mount announces immediately for the same reason: the "first
8950
+ * computed model" that normally announces cannot come until the indicator is shown,
8951
+ * and host UIs (object tree, landing watchers) must know it exists NOW.
8824
8952
  */
8825
8953
  mountLoadingPlaceholder(id, record) {
8826
- if (record.renderHandle || record.hidden || !record.prepared) return;
8954
+ if (record.renderHandle || !record.prepared) return;
8827
8955
  const meta = record.prepared.meta;
8828
8956
  const model = {
8829
8957
  id,
@@ -8847,8 +8975,34 @@ var Vela = (function (exports) {
8847
8975
  this.ensurePaneFor(paneId);
8848
8976
  record.renderHandle = this.renderer.mountIndicator(model);
8849
8977
  record.pendingStructural = true;
8978
+ if (record.hidden) {
8979
+ this.renderer.setIndicatorVisible?.(record.renderHandle, false);
8980
+ this.announce(record, this.handles.get(id));
8981
+ return;
8982
+ }
8850
8983
  this.setLoading(record, true);
8851
8984
  }
8985
+ /**
8986
+ * Mount the legend row for a NATIVE indicator that is being hidden BEFORE it ever
8987
+ * started (a restored-hidden ledger entry: `startNativeIndicator` bails on hidden
8988
+ * records, so no model — and therefore no row — would ever mount). The native
8989
+ * counterpart of {@link mountLoadingPlaceholder}'s hidden branch: an empty model
8990
+ * carries the title + inputs schema, the renderer marks the row hidden, and the
8991
+ * announce makes the indicator visible to host UIs. Showing later STARTS the
8992
+ * instance (the `started` flag path) and its first emit remounts over this row.
8993
+ */
8994
+ mountHiddenNativeRow(id, record) {
8995
+ if (record.renderHandle || !record.native) return;
8996
+ const model = this.buildNativeModel(record, {});
8997
+ const paneId = this.routePane(id, model, record.options ?? {});
8998
+ this.placeModel(model, id, paneId);
8999
+ record.model = model;
9000
+ this.ensurePaneFor(paneId);
9001
+ record.renderHandle = this.renderer.mountIndicator(model);
9002
+ record.pendingStructural = true;
9003
+ this.renderer.setIndicatorVisible?.(record.renderHandle, false);
9004
+ this.announce(record, this.handles.get(id));
9005
+ }
8852
9006
  /** Flip the record's loading state and reflect it in the legend row (spinner on/off). */
8853
9007
  setLoading(record, loading) {
8854
9008
  record.loading = loading;
@@ -8883,6 +9037,7 @@ var Vela = (function (exports) {
8883
9037
  for (const r of this.registry.all()) {
8884
9038
  const model = r.model;
8885
9039
  if (!model) continue;
9040
+ if (model.legend === false) continue;
8886
9041
  const paneId = model.paneId ?? "price";
8887
9042
  if (!byPane.has(paneId)) byPane.set(paneId, []);
8888
9043
  byPane.get(paneId).push({
@@ -9458,6 +9613,16 @@ var Vela = (function (exports) {
9458
9613
  this.renderer.setLayoutMode?.(mode);
9459
9614
  return this;
9460
9615
  }
9616
+ /**
9617
+ * Drive the renderer's time-of-day chrome (the countdown-to-bar-close chip) from the
9618
+ * host's own second pulse, so it ticks in step with a host clock display instead of
9619
+ * on a separate timer that can read a different second. `null` hands the pulse back
9620
+ * to the renderer. Silent no-op on a renderer without time-of-day chrome.
9621
+ */
9622
+ setWallClock(clock) {
9623
+ this.renderer.setWallClock?.(clock);
9624
+ return this;
9625
+ }
9461
9626
  };
9462
9627
 
9463
9628
  // src/core/renderer-defaults.ts
@@ -10350,6 +10515,95 @@ var Vela = (function (exports) {
10350
10515
  }
10351
10516
  };
10352
10517
 
10518
+ // src/core/MarksControl.ts
10519
+ var MarksControl = class {
10520
+ constructor(ctrl) {
10521
+ this.ctrl = ctrl;
10522
+ }
10523
+ /** Whether the active renderer paints timeline marks. */
10524
+ get supported() {
10525
+ return this.ctrl.supported;
10526
+ }
10527
+ /** Add one mark; an existing id is replaced in place. */
10528
+ add(mark) {
10529
+ this.ctrl.add(mark);
10530
+ return this;
10531
+ }
10532
+ /** Replace the whole set (a market switch). */
10533
+ set(marks) {
10534
+ this.ctrl.set(marks);
10535
+ return this;
10536
+ }
10537
+ remove(id) {
10538
+ this.ctrl.remove(id);
10539
+ return this;
10540
+ }
10541
+ clear() {
10542
+ this.ctrl.clear();
10543
+ return this;
10544
+ }
10545
+ /** Every mark, in insertion order. */
10546
+ all() {
10547
+ return this.ctrl.all();
10548
+ }
10549
+ /**
10550
+ * Define a visibility group's presentation: the label of its checkbox in chart
10551
+ * settings (the Events tab) and its default visibility. Marks may name a group
10552
+ * that was never defined — it then shows its capitalized id.
10553
+ */
10554
+ defineGroup(group) {
10555
+ this.ctrl.defineGroup(group);
10556
+ return this;
10557
+ }
10558
+ /** The defined groups, in definition order. */
10559
+ groups() {
10560
+ return this.ctrl.groupDefinitions();
10561
+ }
10562
+ /** Show or hide one group's marks — the same switch as the settings checkbox, persisted with the chart's config. */
10563
+ setGroupVisible(id, visible = true) {
10564
+ this.ctrl.setGroupVisible(id, visible);
10565
+ return this;
10566
+ }
10567
+ /** A group's effective visibility (the user's choice, else the group's declared default). */
10568
+ isGroupVisible(id) {
10569
+ return this.ctrl.isGroupVisible(id);
10570
+ }
10571
+ };
10572
+
10573
+ // src/core/util/wall-clock.ts
10574
+ var BOUNDARY_SLACK_MS = 5;
10575
+ var SecondClock = class _SecondClock {
10576
+ constructor(now = () => Date.now()) {
10577
+ this.now = now;
10578
+ this.subs = /* @__PURE__ */ new Set();
10579
+ this.timer = null;
10580
+ }
10581
+ onTick(cb) {
10582
+ this.subs.add(cb);
10583
+ if (this.timer == null) this.arm();
10584
+ return () => {
10585
+ this.subs.delete(cb);
10586
+ if (this.subs.size === 0 && this.timer != null) {
10587
+ clearTimeout(this.timer);
10588
+ this.timer = null;
10589
+ }
10590
+ };
10591
+ }
10592
+ /** Milliseconds from `now` to just past the next second boundary. */
10593
+ static delayToNextSecond(now) {
10594
+ const intoSecond = (now % 1e3 + 1e3) % 1e3;
10595
+ return 1e3 - intoSecond + BOUNDARY_SLACK_MS;
10596
+ }
10597
+ arm() {
10598
+ this.timer = setTimeout(() => {
10599
+ this.timer = null;
10600
+ const now = this.now();
10601
+ for (const cb of this.subs) cb(now);
10602
+ if (this.subs.size > 0) this.arm();
10603
+ }, _SecondClock.delayToNextSecond(this.now()));
10604
+ }
10605
+ };
10606
+
10353
10607
  // src/core/color.ts
10354
10608
  function parseRgb(color) {
10355
10609
  const s = color.trim();
@@ -17261,7 +17515,7 @@ ${STATIC_DECLS}
17261
17515
  return { left, top, right, bottom, width: right - left, height: bottom - top };
17262
17516
  }
17263
17517
  function placePopover(a) {
17264
- let left = a.align === "end" ? a.trigger.right - a.pop.width : a.trigger.left;
17518
+ let left = a.align === "end" ? a.trigger.right - a.pop.width : a.align === "center" ? a.trigger.left + a.trigger.width / 2 - a.pop.width / 2 : a.trigger.left;
17265
17519
  const below = a.trigger.bottom + a.gap;
17266
17520
  const above = a.trigger.top - a.pop.height - a.gap;
17267
17521
  const fitsBelow = below + a.pop.height <= a.clamp.bottom;
@@ -17333,6 +17587,8 @@ ${STATIC_DECLS}
17333
17587
  this.onKey = null;
17334
17588
  this.onReflow = null;
17335
17589
  this.shown = false;
17590
+ /** The pending removal of a fading-out shell; a show() that reuses the shell cancels it. */
17591
+ this.leaveTimer = null;
17336
17592
  const doc = opts.trigger.ownerDocument;
17337
17593
  injectStyles(POPOVER_STYLE_ID, POPOVER_CSS, doc);
17338
17594
  this.trigger = opts.trigger;
@@ -17340,6 +17596,7 @@ ${STATIC_DECLS}
17340
17596
  this.ctrl = popoverController(opts);
17341
17597
  this.boundary = opts.boundary ?? "viewport";
17342
17598
  this.theme = opts.theme;
17599
+ this.fadeMs = Math.max(0, opts.fadeMs ?? 0);
17343
17600
  this.el = doc.createElement("div");
17344
17601
  this.el.className = "vela-popover vela-ui-layer" + (opts.className ? ` ${opts.className}` : "");
17345
17602
  this.el.dataset.position = this.ctrl.position;
@@ -17363,11 +17620,21 @@ ${STATIC_DECLS}
17363
17620
  return;
17364
17621
  }
17365
17622
  if (open && open !== this) open.hide();
17623
+ if (this.leaveTimer !== null) {
17624
+ clearTimeout(this.leaveTimer);
17625
+ this.leaveTimer = null;
17626
+ }
17366
17627
  ensureUIHost(this.el, this.theme);
17628
+ if (this.fadeMs > 0) {
17629
+ this.el.style.transition = `opacity ${this.fadeMs}ms ease`;
17630
+ this.el.style.opacity = "0";
17631
+ this.el.style.pointerEvents = "";
17632
+ }
17367
17633
  this.host.appendChild(this.el);
17368
17634
  this.shown = true;
17369
17635
  open = this;
17370
17636
  this.place();
17637
+ if (this.fadeMs > 0) this.el.style.opacity = "1";
17371
17638
  const onOutside = (ev) => {
17372
17639
  const t = ev.target;
17373
17640
  if (this.el.contains(t) || this.trigger.contains(t)) return;
@@ -17400,7 +17667,16 @@ ${STATIC_DECLS}
17400
17667
  this.onOutside = null;
17401
17668
  this.onKey = null;
17402
17669
  this.onReflow = null;
17403
- this.el.remove();
17670
+ if (this.fadeMs > 0) {
17671
+ this.el.style.opacity = "0";
17672
+ this.el.style.pointerEvents = "none";
17673
+ this.leaveTimer = setTimeout(() => {
17674
+ this.leaveTimer = null;
17675
+ this.el.remove();
17676
+ }, this.fadeMs);
17677
+ } else {
17678
+ this.el.remove();
17679
+ }
17404
17680
  this.shown = false;
17405
17681
  if (open === this) open = null;
17406
17682
  this.ctrl.onClose?.();
@@ -22796,6 +23072,8 @@ ${overlayScrollbarCss(".vela-dialog.vela-ind-dialog *", 9)}
22796
23072
  // canvas-painted into the owning indicator's interleave slice
22797
23073
  trades: true,
22798
23074
  // strategy order-fill markers (arrows + labels + fill-price ticks)
23075
+ timelineMarks: true,
23076
+ // host events on a lane above the time axis (glyphs + detail popup)
22799
23077
  inputsUI: true
22800
23078
  // reuses the DOM InputsUI
22801
23079
  };
@@ -23068,9 +23346,12 @@ ${overlayScrollbarCss(".vela-dialog.vela-ind-dialog *", 9)}
23068
23346
  const gh = asObject(grid.horzLines);
23069
23347
  const cross = asObject(p.crosshair);
23070
23348
  const ps = asObject(p.priceScale);
23349
+ const anim = asObject(p.animations);
23071
23350
  const panes = asObject(p.panes);
23072
23351
  const trades = asObject(p.trades);
23073
23352
  const ts = asObject(p.timeScale);
23353
+ const marks = asObject(p.marks);
23354
+ const markGroups = asObject(marks.groups);
23074
23355
  const candles = asObject(p.candles);
23075
23356
  const bars = asObject(p.bars);
23076
23357
  const line = asObject(p.line);
@@ -23117,6 +23398,12 @@ ${overlayScrollbarCss(".vela-dialog.vela-ind-dialog *", 9)}
23117
23398
  countdown: isBool(ps.countdown) ? ps.countdown : base.priceScale.countdown,
23118
23399
  animateLastPrice: isBool(ps.animateLastPrice) ? ps.animateLastPrice : base.priceScale.animateLastPrice
23119
23400
  },
23401
+ animations: {
23402
+ zoom: isBool(anim.zoom) ? anim.zoom : base.animations.zoom,
23403
+ pan: isBool(anim.pan) ? anim.pan : base.animations.pan,
23404
+ autoscale: isBool(anim.autoscale) ? anim.autoscale : base.animations.autoscale,
23405
+ intro: isBool(anim.intro) ? anim.intro : base.animations.intro
23406
+ },
23120
23407
  panes: {
23121
23408
  separatorColor: isColor(panes.separatorColor) ? panes.separatorColor : base.panes.separatorColor
23122
23409
  },
@@ -23131,6 +23418,15 @@ ${overlayScrollbarCss(".vela-dialog.vela-ind-dialog *", 9)}
23131
23418
  timeScale: {
23132
23419
  timezone: typeof ts.timezone === "string" && ts.timezone ? ts.timezone : base.timeScale.timezone
23133
23420
  },
23421
+ marks: {
23422
+ visible: isBool(marks.visible) ? marks.visible : base.marks.visible,
23423
+ // Additive like `stacking.series`: a patch names only the groups it carries, so a
23424
+ // choice stored for a group the host has not registered yet survives verbatim.
23425
+ groups: {
23426
+ ...base.marks.groups,
23427
+ ...Object.fromEntries(Object.entries(markGroups).filter(([, v]) => isBool(v)))
23428
+ }
23429
+ },
23134
23430
  candles: {
23135
23431
  upColor: isColor(candles.upColor) ? candles.upColor : base.candles.upColor,
23136
23432
  downColor: isColor(candles.downColor) ? candles.downColor : base.candles.downColor,
@@ -24649,6 +24945,31 @@ void main() {
24649
24945
  }
24650
24946
  }
24651
24947
  };
24948
+ var EaseSetting = class {
24949
+ /** `defaultMs` is what the on/off switch restores when nothing else was configured;
24950
+ * `initialMs` (default: `defaultMs`) is the starting value — 0 for a motion that
24951
+ * ships off. */
24952
+ constructor(defaultMs, initialMs = defaultMs) {
24953
+ this.onMs = defaultMs;
24954
+ this.ms = initialMs;
24955
+ }
24956
+ /** The active time-constant; 0 when off. */
24957
+ get tau() {
24958
+ return this.ms;
24959
+ }
24960
+ get on() {
24961
+ return this.ms > 0;
24962
+ }
24963
+ /** Set the time-constant (0 = off). A non-zero value becomes what `toggle(true)` restores. */
24964
+ set(ms) {
24965
+ this.ms = ms;
24966
+ if (ms > 0) this.onMs = ms;
24967
+ }
24968
+ /** On/off only — the duration stays the last one configured. */
24969
+ toggle(on) {
24970
+ this.ms = on ? this.onMs : 0;
24971
+ }
24972
+ };
24652
24973
  function easeToward(current, target, dtMs, tauMs) {
24653
24974
  if (tauMs <= 0) return target;
24654
24975
  return current + (target - current) * (1 - Math.exp(-dtMs / tauMs));
@@ -25372,6 +25693,25 @@ void main() {
25372
25693
  for (let i = 0; i < lines.length; i += 1) ctx.fillText(lines[i], x, firstY + i * step);
25373
25694
  }
25374
25695
 
25696
+ // src/renderers/shared/marks-state.ts
25697
+ function defaultMarksState() {
25698
+ return { visible: true, groups: {} };
25699
+ }
25700
+ function mergeMarksState(base, patch) {
25701
+ if (typeof patch === "boolean") return { visible: patch, groups: { ...base.groups } };
25702
+ const p = patch && typeof patch === "object" ? patch : {};
25703
+ const g = p.groups && typeof p.groups === "object" ? p.groups : {};
25704
+ const groups = { ...base.groups };
25705
+ for (const [id, v] of Object.entries(g)) if (typeof v === "boolean") groups[id] = v;
25706
+ return { visible: typeof p.visible === "boolean" ? p.visible : base.visible, groups };
25707
+ }
25708
+ function markGroupVisible(state, groupId, groups) {
25709
+ if (groupId === void 0) return true;
25710
+ const chosen = state.groups[groupId];
25711
+ if (typeof chosen === "boolean") return chosen;
25712
+ return groups.find((g) => g.id === groupId)?.visible !== false;
25713
+ }
25714
+
25375
25715
  // src/renderers/native/core/SceneGraph.ts
25376
25716
  var SceneGraph = class {
25377
25717
  constructor() {
@@ -25435,6 +25775,20 @@ void main() {
25435
25775
  /** Strategy trade-marker display (the `tradeMarkers` feature): master toggle, the
25436
25776
  * two text lines, and the palette. Trade markers always paint on the price pane. */
25437
25777
  this.tradeMarkers = defaultTradeMarkersState();
25778
+ /** Timeline-mark display (the `marks` feature): the lane's master toggle + per-group visibility. */
25779
+ this.marks = defaultMarksState();
25780
+ /** The host's timeline marks + group definitions (`setTimelineMarks`), painted on the lane above the time axis. */
25781
+ this.timelineMarks = [];
25782
+ this.markGroups = [];
25783
+ /** The mark stack (bar index) fanned out by hover or tap, if any. */
25784
+ this.marksExpandedStack = null;
25785
+ /** The lane glyph (cluster key) under the pointer — it pulses — and when the hover began (frame-clock ms). */
25786
+ this.marksHoverKey = null;
25787
+ this.marksHoverSince = 0;
25788
+ /** The cluster whose popup is open: its glyph paints filled ("active"). */
25789
+ this.marksActiveKey = null;
25790
+ /** A content-less click's brief filled flash — the cluster key and the frame-clock time it ends. */
25791
+ this.marksFlash = null;
25438
25792
  /** Renderer-owned shaded time bands (session highlighting), behind grid + data. */
25439
25793
  this.highlights = [];
25440
25794
  /** Pre/post-market bands pushed by the host (`sessionZones` feature); null ⇒ no sessions. */
@@ -27283,6 +27637,315 @@ void main() {
27283
27637
  return out;
27284
27638
  }
27285
27639
 
27640
+ // src/renderers/native/chrome/countdown.ts
27641
+ function countdownText(barOpen, barMs, now) {
27642
+ if (!(barMs > 0)) return null;
27643
+ const remaining = barOpen + barMs - now;
27644
+ if (remaining <= 0) return null;
27645
+ return formatCountdown(remaining);
27646
+ }
27647
+ function formatCountdown(ms) {
27648
+ const total = Math.max(0, Math.ceil(ms / 1e3));
27649
+ const s = total % 60;
27650
+ const m = Math.floor(total / 60) % 60;
27651
+ const h = Math.floor(total / 3600);
27652
+ const pad = (v) => String(v).padStart(2, "0");
27653
+ return h > 0 ? `${h}:${pad(m)}:${pad(s)}` : `${pad(m)}:${pad(s)}`;
27654
+ }
27655
+
27656
+ // src/renderers/native/chrome/contrast.ts
27657
+ function tagTextColor(bg, over) {
27658
+ const [r, g, b, a] = parseColor(bg);
27659
+ let R = r;
27660
+ let G = g;
27661
+ let B = b;
27662
+ if (a < 1) {
27663
+ const [or2, og, ob] = parseColor(over);
27664
+ R = r * a + or2 * (1 - a);
27665
+ G = g * a + og * (1 - a);
27666
+ B = b * a + ob * (1 - a);
27667
+ }
27668
+ const lin = (c) => c <= 0.04045 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
27669
+ const L = 0.2126 * lin(R) + 0.7152 * lin(G) + 0.0722 * lin(B);
27670
+ return L >= 0.4 ? "#000000" : "#ffffff";
27671
+ }
27672
+
27673
+ // src/renderers/native/chrome/marks/layout.ts
27674
+ var MARK_GLYPH_PX = 16;
27675
+ var MARK_CLUSTER_PX = 20;
27676
+ var MARK_LANE_INSET = 4;
27677
+ var MARK_DECK_STEP = 3;
27678
+ var MARK_FAN_GAP = 4;
27679
+ var MARK_HIT_PAD = 3;
27680
+ var MARK_FAN_HOLD = 8;
27681
+ function snapMarkBar(time, barTimes, intervalMs2) {
27682
+ const n = barTimes.length;
27683
+ if (n === 0 || !(intervalMs2 > 0) || !Number.isFinite(time)) return null;
27684
+ if (time < barTimes[0]) return null;
27685
+ const last2 = barTimes[n - 1];
27686
+ if (time >= last2 + intervalMs2) return n - 1 + Math.floor((time - last2) / intervalMs2);
27687
+ let lo = 0;
27688
+ let hi = n - 1;
27689
+ while (lo < hi) {
27690
+ const mid = lo + hi + 1 >> 1;
27691
+ if (barTimes[mid] <= time) lo = mid;
27692
+ else hi = mid - 1;
27693
+ }
27694
+ if (time < barTimes[lo] + intervalMs2) return lo;
27695
+ return lo + 1;
27696
+ }
27697
+ function clusterMarks(marks, barTimes, intervalMs2, hidden) {
27698
+ const byKey = /* @__PURE__ */ new Map();
27699
+ marks.forEach((m, seq) => {
27700
+ if (m.group !== void 0 && hidden(m.group)) return;
27701
+ const bar = snapMarkBar(m.time, barTimes, intervalMs2);
27702
+ if (bar === null) return;
27703
+ const key = `${bar}|${m.group ?? ""}`;
27704
+ let c = byKey.get(key);
27705
+ if (!c) {
27706
+ c = { key, bar, group: m.group, marks: [], seq: [] };
27707
+ byKey.set(key, c);
27708
+ }
27709
+ c.marks.push(m);
27710
+ c.seq.push(seq);
27711
+ });
27712
+ const out = [];
27713
+ for (const c of byKey.values()) {
27714
+ const order = c.marks.map((m, i) => ({ m, seq: c.seq[i] })).sort((a, b) => a.m.time - b.m.time || a.seq - b.seq);
27715
+ out.push({ key: c.key, bar: c.bar, group: c.group, marks: order.map((o) => o.m) });
27716
+ }
27717
+ return out;
27718
+ }
27719
+ function groupRank(groups, clusters) {
27720
+ const rank = /* @__PURE__ */ new Map();
27721
+ groups.forEach((g, i) => rank.set(g.id, i));
27722
+ for (const c of clusters) {
27723
+ if (c.group !== void 0 && !rank.has(c.group)) rank.set(c.group, rank.size);
27724
+ }
27725
+ return (group) => group === void 0 ? Number.MAX_SAFE_INTEGER : rank.get(group) ?? Number.MAX_SAFE_INTEGER - 1;
27726
+ }
27727
+ function layoutMarkLane(input) {
27728
+ const clusters = clusterMarks(input.marks, input.barTimes, input.intervalMs, input.hidden);
27729
+ const rankOf = groupRank(input.groups, clusters);
27730
+ const byBar = /* @__PURE__ */ new Map();
27731
+ for (const c of clusters) {
27732
+ const list = byBar.get(c.bar);
27733
+ if (list) list.push(c);
27734
+ else byBar.set(c.bar, [c]);
27735
+ }
27736
+ const glyphs = [];
27737
+ const stacks = /* @__PURE__ */ new Map();
27738
+ for (const [bar, list] of byBar) {
27739
+ const x = input.xOf(bar);
27740
+ if (!Number.isFinite(x) || x < -MARK_CLUSTER_PX || x > input.dataW + MARK_CLUSTER_PX) continue;
27741
+ list.sort((a, b) => rankOf(a.group) - rankOf(b.group));
27742
+ const multi = list.length > 1;
27743
+ const expanded = multi && input.expanded === bar;
27744
+ const decked = multi && !expanded;
27745
+ const placed = [];
27746
+ const deckSize = list[0].marks.length > 1 ? MARK_CLUSTER_PX : MARK_GLYPH_PX;
27747
+ let bottom = input.axisY - MARK_LANE_INSET;
27748
+ list.forEach((cluster, depth) => {
27749
+ const size3 = decked ? deckSize : cluster.marks.length > 1 ? MARK_CLUSTER_PX : MARK_GLYPH_PX;
27750
+ let y;
27751
+ if (expanded) {
27752
+ y = bottom - size3 / 2;
27753
+ bottom -= size3 + MARK_FAN_GAP;
27754
+ } else {
27755
+ y = input.axisY - MARK_LANE_INSET - size3 / 2 - depth * MARK_DECK_STEP;
27756
+ }
27757
+ placed.push({ cluster, x, y, size: size3, stack: bar, depth, decked });
27758
+ });
27759
+ for (let i = placed.length - 1; i >= 0; i--) glyphs.push(placed[i]);
27760
+ stacks.set(bar, placed);
27761
+ }
27762
+ return { glyphs, stacks };
27763
+ }
27764
+ function markGlyphAt(layout, x, y) {
27765
+ for (let i = layout.glyphs.length - 1; i >= 0; i--) {
27766
+ const g = layout.glyphs[i];
27767
+ if (g.decked && g.depth !== 0) continue;
27768
+ const r = g.size / 2 + MARK_HIT_PAD;
27769
+ if (Math.abs(x - g.x) <= r && Math.abs(y - g.y) <= r) return g;
27770
+ }
27771
+ return null;
27772
+ }
27773
+ function markStackAt(layout, x, y) {
27774
+ for (const [bar, placed] of layout.stacks) {
27775
+ for (const g of placed) {
27776
+ const r = g.size / 2 + MARK_HIT_PAD;
27777
+ if (Math.abs(x - g.x) <= r && Math.abs(y - g.y) <= r) return bar;
27778
+ }
27779
+ if (placed.length > 1 && !placed[0].decked) {
27780
+ const top = placed[placed.length - 1];
27781
+ const base = placed[0];
27782
+ const r = Math.max(top.size, base.size) / 2 + MARK_HIT_PAD;
27783
+ if (Math.abs(x - base.x) <= r && y >= top.y - top.size / 2 - MARK_FAN_HOLD && y <= base.y + base.size / 2 + MARK_HIT_PAD) return bar;
27784
+ }
27785
+ }
27786
+ return null;
27787
+ }
27788
+ function clusterTooltip(cluster, groups) {
27789
+ const first2 = cluster.marks[0];
27790
+ if (!first2) return null;
27791
+ if (cluster.marks.length === 1) return first2.tooltip ?? first2.title ?? null;
27792
+ const label = cluster.group !== void 0 ? markGroupLabel(cluster.group, groups) : first2.title ?? first2.tooltip ?? "Marks";
27793
+ return `${label} \xB7 ${cluster.marks.length}`;
27794
+ }
27795
+ function markGroupLabel(groupId, groups) {
27796
+ const def = groups.find((g) => g.id === groupId);
27797
+ if (def) return def.label;
27798
+ return groupId.charAt(0).toUpperCase() + groupId.slice(1);
27799
+ }
27800
+ function effectiveMarkGroups(marks, groups) {
27801
+ const out = groups.map((g) => ({ ...g }));
27802
+ const seen = new Set(out.map((g) => g.id));
27803
+ for (const m of marks) {
27804
+ if (m.group === void 0 || seen.has(m.group)) continue;
27805
+ seen.add(m.group);
27806
+ out.push({ id: m.group, label: markGroupLabel(m.group, groups) });
27807
+ }
27808
+ return out;
27809
+ }
27810
+
27811
+ // src/renderers/native/chrome/marks/paint.ts
27812
+ var MARK_PULSE_MS = 360;
27813
+ var MARK_PULSE_AMPLITUDE = 0.1;
27814
+ var ACTIVE_INK = "#ffffff";
27815
+ function pulseScale(elapsedMs) {
27816
+ if (!(elapsedMs > 0) || elapsedMs >= MARK_PULSE_MS) return 1;
27817
+ return 1 + MARK_PULSE_AMPLITUDE * Math.sin(Math.PI * elapsedMs / MARK_PULSE_MS);
27818
+ }
27819
+ function paintMarkLane(ctx, layout, deps) {
27820
+ if (layout.glyphs.length === 0) return;
27821
+ ctx.save();
27822
+ ctx.setLineDash([]);
27823
+ ctx.textAlign = "center";
27824
+ ctx.textBaseline = "middle";
27825
+ for (const g of layout.glyphs) {
27826
+ const mark = g.cluster.marks[0];
27827
+ if (!mark) continue;
27828
+ const key = g.cluster.key;
27829
+ const color = mark.glyph.color;
27830
+ const active = key === deps.activeKey || key === deps.flashKey;
27831
+ const size3 = g.size * (key === deps.hoverKey ? pulseScale(deps.nowMs - deps.hoverSince) : 1);
27832
+ if (g.depth === 0) {
27833
+ const sx = Math.round(g.x) + 0.5;
27834
+ ctx.lineWidth = 1;
27835
+ ctx.strokeStyle = deps.stemColor;
27836
+ ctx.beginPath();
27837
+ ctx.moveTo(sx, g.y + g.size / 2);
27838
+ ctx.lineTo(sx, deps.axisY);
27839
+ ctx.stroke();
27840
+ }
27841
+ const shape = mark.glyph.shape ?? "circle";
27842
+ const center = traceShape(ctx, shape, g.x, g.y, size3);
27843
+ ctx.lineWidth = 4;
27844
+ ctx.strokeStyle = deps.background;
27845
+ ctx.stroke();
27846
+ ctx.fillStyle = active ? color : deps.background;
27847
+ ctx.fill();
27848
+ ctx.lineWidth = 1.5;
27849
+ ctx.strokeStyle = color;
27850
+ ctx.stroke();
27851
+ const ink = active ? ACTIVE_INK : color;
27852
+ const symbolPx = Math.round(size3 * 0.62);
27853
+ if (mark.glyph.icon) {
27854
+ const img = deps.icons.get(mark.glyph.icon, ink, symbolPx, deps.dpr);
27855
+ if (img) ctx.drawImage(img, center.x - symbolPx / 2, center.y - symbolPx / 2, symbolPx, symbolPx);
27856
+ } else if (mark.glyph.letter) {
27857
+ ctx.fillStyle = ink;
27858
+ ctx.font = `600 ${Math.round(size3 * 0.58)}px ${deps.fontFamily}`;
27859
+ ctx.fillText(mark.glyph.letter.slice(0, 2), center.x, center.y + 0.5);
27860
+ }
27861
+ }
27862
+ ctx.restore();
27863
+ }
27864
+ function traceShape(ctx, shape, x, y, size3) {
27865
+ const r = size3 / 2;
27866
+ ctx.beginPath();
27867
+ switch (shape) {
27868
+ case "square": {
27869
+ const c = Math.min(3, r / 2);
27870
+ roundedRect(ctx, x - r, y - r, size3, size3, c);
27871
+ return { x, y };
27872
+ }
27873
+ case "diamond":
27874
+ ctx.moveTo(x, y - r);
27875
+ ctx.lineTo(x + r, y);
27876
+ ctx.lineTo(x, y + r);
27877
+ ctx.lineTo(x - r, y);
27878
+ ctx.closePath();
27879
+ return { x, y };
27880
+ case "pin": {
27881
+ const hr = r * 0.82;
27882
+ const hy = y - r + hr;
27883
+ ctx.arc(x, hy, hr, Math.PI * 0.75, Math.PI * 0.25, false);
27884
+ ctx.lineTo(x, y + r);
27885
+ ctx.closePath();
27886
+ return { x, y: hy };
27887
+ }
27888
+ case "circle":
27889
+ default:
27890
+ ctx.arc(x, y, r, 0, Math.PI * 2);
27891
+ return { x, y };
27892
+ }
27893
+ }
27894
+ function roundedRect(ctx, x, y, w, h, radius) {
27895
+ ctx.moveTo(x + radius, y);
27896
+ ctx.lineTo(x + w - radius, y);
27897
+ ctx.quadraticCurveTo(x + w, y, x + w, y + radius);
27898
+ ctx.lineTo(x + w, y + h - radius);
27899
+ ctx.quadraticCurveTo(x + w, y + h, x + w - radius, y + h);
27900
+ ctx.lineTo(x + radius, y + h);
27901
+ ctx.quadraticCurveTo(x, y + h, x, y + h - radius);
27902
+ ctx.lineTo(x, y + radius);
27903
+ ctx.quadraticCurveTo(x, y, x + radius, y);
27904
+ ctx.closePath();
27905
+ }
27906
+ var MarkIconRaster = class {
27907
+ constructor(onReady) {
27908
+ this.onReady = onReady;
27909
+ this.cache = /* @__PURE__ */ new Map();
27910
+ }
27911
+ get(icon2, ink, px, dpr) {
27912
+ const key = `${icon2}|${ink}|${px}|${dpr}`;
27913
+ if (this.cache.has(key)) {
27914
+ const img2 = this.cache.get(key);
27915
+ return img2 && img2.complete && img2.naturalWidth > 0 ? img2 : null;
27916
+ }
27917
+ const markup = iconMarkup(icon2);
27918
+ if (!markup || typeof document === "undefined" || typeof Image === "undefined" || typeof XMLSerializer === "undefined") {
27919
+ this.cache.set(key, null);
27920
+ return null;
27921
+ }
27922
+ const svg = standaloneSvg(markup, ink, Math.max(1, Math.ceil(px * dpr)));
27923
+ if (!svg) {
27924
+ this.cache.set(key, null);
27925
+ return null;
27926
+ }
27927
+ const img = new Image();
27928
+ img.onload = () => this.onReady();
27929
+ img.src = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}`;
27930
+ this.cache.set(key, img);
27931
+ return null;
27932
+ }
27933
+ clear() {
27934
+ this.cache.clear();
27935
+ }
27936
+ };
27937
+ function standaloneSvg(markup, ink, px) {
27938
+ const tpl = document.createElement("template");
27939
+ tpl.innerHTML = markup;
27940
+ const svg = tpl.content.firstElementChild;
27941
+ if (!svg || svg.tagName.toLowerCase() !== "svg") return null;
27942
+ svg.setAttribute("xmlns", "http://www.w3.org/2000/svg");
27943
+ svg.setAttribute("width", String(px));
27944
+ svg.setAttribute("height", String(px));
27945
+ svg.setAttribute("color", ink);
27946
+ return new XMLSerializer().serializeToString(svg);
27947
+ }
27948
+
27286
27949
  // src/renderers/native/chrome/ChromeRenderer.ts
27287
27950
  var ChromeRenderer = class {
27288
27951
  constructor() {
@@ -27292,11 +27955,40 @@ void main() {
27292
27955
  this.axisTextColor = DARK_THEME.textColor;
27293
27956
  // Shared Pine-drawing renderer, used here for autoscale geometry only; widthCache persists.
27294
27957
  this.drawScene = new DrawingSceneRenderer({ timeToLogical: () => 0, barAt: () => null, theme: {} });
27958
+ /** The timeline-mark lane as laid out by the last frame — what hover/click hit-test against. */
27959
+ this.markLayout = { glyphs: [], stacks: /* @__PURE__ */ new Map() };
27960
+ /** Registry icons rasterized for the lane; the owner is asked for a chrome repaint when one lands. */
27961
+ this.markIcons = new MarkIconRaster(() => this.onMarkIconReady?.());
27962
+ this.onMarkIconReady = null;
27963
+ /** Bar open times of the current series, rebuilt only when the array or its length changes (a live tick keeps both). */
27964
+ this.barTimesSrc = null;
27965
+ this.barTimesCache = [];
27295
27966
  }
27296
27967
  mount(canvas) {
27297
27968
  this.canvas = canvas;
27298
27969
  this.ctx = canvas.getContext("2d");
27299
27970
  }
27971
+ /** Where to ask for a chrome repaint when a lane icon finishes rasterizing. */
27972
+ setMarkIconReady(cb) {
27973
+ this.onMarkIconReady = cb;
27974
+ }
27975
+ /** The interactive mark glyph under a plot point (last frame's layout), or null. */
27976
+ markGlyphAt(x, y) {
27977
+ return markGlyphAt(this.markLayout, x, y);
27978
+ }
27979
+ /** The mark stack (bar index) whose glyphs — or the gaps of its fan — cover a plot point. */
27980
+ markStackAt(x, y) {
27981
+ return markStackAt(this.markLayout, x, y);
27982
+ }
27983
+ /** A glyph of the last frame by its cluster key — how an open popup follows its anchor. */
27984
+ markGlyphByKey(key) {
27985
+ return this.markLayout.glyphs.find((g) => g.cluster.key === key) ?? null;
27986
+ }
27987
+ /** Hover text of the mark glyph under a plot point, or null. */
27988
+ markTooltipAt(x, y, groups) {
27989
+ const g = this.markGlyphAt(x, y);
27990
+ return g ? clusterTooltip(g.cluster, groups) : null;
27991
+ }
27300
27992
  /** Wire the drawing coordinate resolvers + theme (call once per frame before use). */
27301
27993
  prepare(scene, coords, theme) {
27302
27994
  this.drawScene.setDeps({
@@ -27345,6 +28037,7 @@ void main() {
27345
28037
  const panes = scene.orderedPanes();
27346
28038
  if (coords.barCount === 0) {
27347
28039
  this.drawPaneSeparators(ctx, scene, theme, fullW, panes);
28040
+ this.markLayout = { glyphs: [], stacks: /* @__PURE__ */ new Map() };
27348
28041
  return;
27349
28042
  }
27350
28043
  const pricePane = panes.find((p) => p.kind === "price") ?? null;
@@ -27358,6 +28051,46 @@ void main() {
27358
28051
  this.drawPaneSeparators(ctx, scene, theme, fullW, panes);
27359
28052
  this.drawPriceLineAndCountdown(ctx, scene, coords, theme, dataW, pricePane);
27360
28053
  this.drawTimeAxis(ctx, scene, coords, theme, dataW, dataH, fullH);
28054
+ this.drawMarkLane(ctx, scene, coords, theme, dataW, dataH);
28055
+ }
28056
+ /** The timeline-mark lane — after the axis, so the tokens read over the plot's bottom edge. */
28057
+ drawMarkLane(ctx, scene, coords, theme, dataW, dataH) {
28058
+ if (!scene.marks.visible || scene.timelineMarks.length === 0) {
28059
+ this.markLayout = { glyphs: [], stacks: /* @__PURE__ */ new Map() };
28060
+ return;
28061
+ }
28062
+ this.markLayout = layoutMarkLane({
28063
+ marks: scene.timelineMarks,
28064
+ groups: scene.markGroups,
28065
+ hidden: (groupId) => !markGroupVisible(scene.marks, groupId, scene.markGroups),
28066
+ barTimes: this.barTimes(scene),
28067
+ intervalMs: coords.barInterval,
28068
+ xOf: (bar) => coords.logicalToX(bar),
28069
+ axisY: dataH,
28070
+ dataW,
28071
+ expanded: scene.marksExpandedStack
28072
+ });
28073
+ const nowMs = typeof performance !== "undefined" ? performance.now() : Date.now();
28074
+ paintMarkLane(ctx, this.markLayout, {
28075
+ axisY: dataH,
28076
+ background: theme.background,
28077
+ stemColor: scene.style.borderColor ?? theme.borderColor,
28078
+ fontFamily: theme.fontFamily,
28079
+ dpr: coords.dpr,
28080
+ icons: this.markIcons,
28081
+ hoverKey: scene.marksHoverKey,
28082
+ hoverSince: scene.marksHoverSince,
28083
+ activeKey: scene.marksActiveKey,
28084
+ flashKey: scene.marksFlash && scene.marksFlash.until > nowMs ? scene.marksFlash.key : null,
28085
+ nowMs
28086
+ });
28087
+ }
28088
+ barTimes(scene) {
28089
+ if (this.barTimesSrc !== scene.bars || this.barTimesCache.length !== scene.bars.length) {
28090
+ this.barTimesSrc = scene.bars;
28091
+ this.barTimesCache = scene.bars.map((b) => b.time);
28092
+ }
28093
+ return this.barTimesCache;
27361
28094
  }
27362
28095
  destroy() {
27363
28096
  this.canvas = null;
@@ -27473,7 +28206,8 @@ void main() {
27473
28206
  * - the countdown-to-bar-close chip (`showCountdown`).
27474
28207
  * When the label and countdown are both on they merge into one stacked block (countdown
27475
28208
  * under the label, text flushed left); a lone label or countdown is centered on the
27476
- * price level with centered text. The countdown ticks once per second (repaint scheduled).
28209
+ * price level with centered text. The countdown repaints on the renderer's second pulse
28210
+ * and disappears once the bar has closed, until the next bar arrives.
27477
28211
  */
27478
28212
  drawPriceLineAndCountdown(ctx, scene, coords, theme, dataW, pricePane) {
27479
28213
  const n = scene.bars.length;
@@ -27493,17 +28227,16 @@ void main() {
27493
28227
  ctx.stroke();
27494
28228
  setDash2(ctx, "solid");
27495
28229
  }
27496
- const interval = coords.barInterval;
27497
- const showCountdown = scene.showCountdown && interval > 0;
28230
+ const cdText = scene.showCountdown ? countdownText(last2.time, coords.barInterval, Date.now()) : null;
28231
+ const showCountdown = cdText !== null;
27498
28232
  const showLabel = scene.showPriceLabel;
27499
28233
  if (!showLabel && !showCountdown) return;
27500
28234
  const priceText = formatAxisValue(pricePane.scale, pricePane.bounds.height, last2.close, percentScaleFor(scene, pricePane), scene.priceMintick);
27501
- const cdText = showCountdown ? formatCountdown(last2.time + interval - Date.now()) : "";
27502
28235
  const PAD = 8;
27503
28236
  const x = dataW + 1;
27504
28237
  const textColor = tagTextColor(color, theme.background);
27505
28238
  ctx.textBaseline = "middle";
27506
- if (showLabel && showCountdown) {
28239
+ if (showLabel && cdText !== null) {
27507
28240
  const w2 = Math.max(ctx.measureText(priceText).width, ctx.measureText(cdText).width) + PAD;
27508
28241
  const top = y - 8;
27509
28242
  const tx = x + PAD / 2;
@@ -27516,7 +28249,7 @@ void main() {
27516
28249
  ctx.textAlign = "start";
27517
28250
  return;
27518
28251
  }
27519
- const text = showLabel ? priceText : cdText;
28252
+ const text = showLabel ? priceText : cdText ?? "";
27520
28253
  const w = ctx.measureText(text).width + PAD;
27521
28254
  ctx.fillStyle = color;
27522
28255
  ctx.fillRect(x, y - 8, w, 16);
@@ -27587,29 +28320,6 @@ void main() {
27587
28320
  else if (style === "dotted") ctx.setLineDash([2, 3]);
27588
28321
  else ctx.setLineDash([]);
27589
28322
  }
27590
- function tagTextColor(bg, over) {
27591
- const [r, g, b, a] = parseColor(bg);
27592
- let R = r;
27593
- let G = g;
27594
- let B = b;
27595
- if (a < 1) {
27596
- const [or2, og, ob] = parseColor(over);
27597
- R = r * a + or2 * (1 - a);
27598
- G = g * a + og * (1 - a);
27599
- B = b * a + ob * (1 - a);
27600
- }
27601
- const lin = (c) => c <= 0.04045 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
27602
- const L = 0.2126 * lin(R) + 0.7152 * lin(G) + 0.0722 * lin(B);
27603
- return L >= 0.4 ? "#000000" : "#ffffff";
27604
- }
27605
- function formatCountdown(ms) {
27606
- const total = Math.max(0, Math.floor(ms / 1e3));
27607
- const s = total % 60;
27608
- const m = Math.floor(total / 60) % 60;
27609
- const h = Math.floor(total / 3600);
27610
- const pad = (v) => String(v).padStart(2, "0");
27611
- return h > 0 ? `${h}:${pad(m)}:${pad(s)}` : `${pad(m)}:${pad(s)}`;
27612
- }
27613
28323
 
27614
28324
  // src/renderers/native/chrome/LabelTooltip.ts
27615
28325
  var HOVER_DELAY_MS = 350;
@@ -27892,6 +28602,11 @@ void main() {
27892
28602
  function settingsIdSlug(label) {
27893
28603
  return label.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
27894
28604
  }
28605
+ var MARKS_SETTINGS_ID = "events";
28606
+ var MARKS_GROUPS_SETTINGS_ID = "events.groups";
28607
+ function markGroupSettingsId(groupId) {
28608
+ return `${MARKS_GROUPS_SETTINGS_ID}.${settingsIdSlug(groupId)}`;
28609
+ }
27895
28610
  function settingsIdHidden(id, hidden) {
27896
28611
  if (hidden.size === 0) return false;
27897
28612
  let path = id;
@@ -27985,7 +28700,11 @@ void main() {
27985
28700
  "symbol.style.baseline.base-level",
27986
28701
  "symbol.style.baseline.width",
27987
28702
  "symbol.animation",
28703
+ "symbol.animation.zoom",
28704
+ "symbol.animation.pan",
28705
+ "symbol.animation.autoscale",
27988
28706
  "symbol.animation.price-changes",
28707
+ "symbol.animation.intro",
27989
28708
  "symbol.timezone",
27990
28709
  "scales",
27991
28710
  "scales.price-scale",
@@ -28011,8 +28730,13 @@ void main() {
28011
28730
  "canvas.grid.horizontal",
28012
28731
  "canvas.theme"
28013
28732
  ];
28014
- function settingsIdCatalog(hostSections) {
28733
+ function settingsIdCatalog(hostSections, markGroups = []) {
28015
28734
  const ids = new Set(BUILTIN_SETTINGS_IDS);
28735
+ if (markGroups.length > 0) {
28736
+ ids.add(MARKS_SETTINGS_ID);
28737
+ ids.add(MARKS_GROUPS_SETTINGS_ID);
28738
+ for (const g of markGroups) ids.add(markGroupSettingsId(g.id));
28739
+ }
28016
28740
  for (const def of chartTypes()) {
28017
28741
  if (hasOwnCandlePaint(def.id)) {
28018
28742
  const style = `symbol.style.${def.id}`;
@@ -28052,7 +28776,7 @@ void main() {
28052
28776
  return chartType(id)?.label ?? BUILTIN_STYLE_LABELS[id] ?? id;
28053
28777
  }
28054
28778
  var SD_STYLE_ID = "vela-settings-controls";
28055
- var SD_STYLE_REV = "5";
28779
+ var SD_STYLE_REV = "6";
28056
28780
  var SETTINGS_BORDER = "var(--vela-border)";
28057
28781
  function ensureControlStyles() {
28058
28782
  if (typeof document === "undefined") return;
@@ -28128,7 +28852,15 @@ ${overlayScrollbarCss(".vela-sd-pane")}
28128
28852
  .vela-sd-mobile .vela-select-trigger,.vela-sd-mobile .vela-num input,.vela-sd-mobile .vela-width-field{height:34px;}
28129
28853
  .vela-sd-mobile .vela-sd-close{width:40px;height:40px;}
28130
28854
  .vela-sd-mobile .vela-sd-btn{height:38px;}
28131
- .vela-sd-mobile .vela-sd-row span,.vela-sd-mobile .vela-sd-bool span,.vela-sd-mobile .vela-field-label{white-space:normal !important;}`;
28855
+ .vela-sd-mobile .vela-sd-row span,.vela-sd-mobile .vela-sd-bool span,.vela-sd-mobile .vela-field-label{white-space:normal !important;}
28856
+ /* The wrap rule above is for ROW LABELS only: a select's closed value must keep its
28857
+ single-line ellipsis, or a long option wraps to several lines inside the 34px
28858
+ trigger and spills over the rows around it. Three classes so it outranks the
28859
+ two-classes-plus-element selector above. The kit's fixed 100px column is a desktop
28860
+ alignment device; on mobile the trigger hugs its value instead (the grid's control
28861
+ column is max-content), capped so a long option still ellipsizes before the label. */
28862
+ .vela-sd-mobile .vela-select-trigger .vela-select-label{white-space:nowrap !important;}
28863
+ .vela-sd-mobile .vela-select:not([data-fill]){width:auto;min-width:100px;max-width:min(220px,55vw);}`;
28132
28864
  if (!existing) document.head.appendChild(st);
28133
28865
  }
28134
28866
  var SettingsDialog = class {
@@ -28143,6 +28875,9 @@ ${overlayScrollbarCss(".vela-sd-pane")}
28143
28875
  this.config = null;
28144
28876
  this.syncTypeTabs = null;
28145
28877
  this.hostSections = [];
28878
+ /** The timeline-mark groups (defined + named by marks) — one checkbox each on the Events tab. */
28879
+ this.markGroups = [];
28880
+ this.markGroupVisible = () => true;
28146
28881
  /** The Canvas → Theme row: current app theme + where a pick is raised. The row is a
28147
28882
  * host callback, NOT a config patch — the app theme stays out of the persisted
28148
28883
  * `ChartConfig`, so exported templates never carry it. */
@@ -28167,6 +28902,11 @@ ${overlayScrollbarCss(".vela-sd-pane")}
28167
28902
  setHostSections(sections) {
28168
28903
  this.hostSections = sections;
28169
28904
  }
28905
+ /** The timeline-mark groups and their current visibility — the Events tab's rows on next open. */
28906
+ setMarkGroups(groups, visible) {
28907
+ this.markGroups = groups;
28908
+ this.markGroupVisible = visible;
28909
+ }
28170
28910
  /** Replace the visibility policy — an open dialog rebuilds in place to honor it. */
28171
28911
  setHiddenSettings(ids) {
28172
28912
  const next2 = new Set(ids);
@@ -28340,12 +29080,36 @@ ${overlayScrollbarCss(".vela-sd-pane")}
28340
29080
  }
28341
29081
  showActive(config.series.style);
28342
29082
  body.append(sid(this.sectionTitle("Animation"), "symbol.animation"));
29083
+ body.append(sid(this.boolRow(
29084
+ "Animate zoom",
29085
+ config.animations.zoom,
29086
+ (v) => this.emit({ animations: { zoom: v } }),
29087
+ this.hint("Glide the chart to each zoom step instead of jumping.")
29088
+ ), "symbol.animation.zoom"));
29089
+ body.append(sid(this.boolRow(
29090
+ "Pan momentum",
29091
+ config.animations.pan,
29092
+ (v) => this.emit({ animations: { pan: v } }),
29093
+ this.hint("Keep gliding briefly after a drag release, and ease scroll-to-latest and keyboard pans.")
29094
+ ), "symbol.animation.pan"));
29095
+ body.append(sid(this.boolRow(
29096
+ "Animate price scale",
29097
+ config.animations.autoscale,
29098
+ (v) => this.emit({ animations: { autoscale: v } }),
29099
+ this.hint("Glide the price scale to its new range while zooming or panning.")
29100
+ ), "symbol.animation.autoscale"));
28343
29101
  body.append(sid(this.boolRow(
28344
29102
  "Animate price changes",
28345
29103
  config.priceScale.animateLastPrice,
28346
29104
  (v) => this.emit({ priceScale: { animateLastPrice: v } }),
28347
29105
  this.hint("Glide the live bar to each new price instead of snapping.")
28348
29106
  ), "symbol.animation.price-changes"));
29107
+ body.append(sid(this.boolRow(
29108
+ "Reveal on load",
29109
+ config.animations.intro,
29110
+ (v) => this.emit({ animations: { intro: v } }),
29111
+ this.hint("Draw the candles in when a chart first loads. Takes effect on the next load.")
29112
+ ), "symbol.animation.intro"));
28349
29113
  body.append(sid(this.sectionTitle("Time zone"), "symbol.timezone"));
28350
29114
  body.append(sid(this.selectRowLabeled("Time zone", normalizeTimezone(config.timeScale.timezone), timezoneOptions(config.timeScale.timezone), (v) => this.emit({ timeScale: { timezone: v } })), "symbol.timezone"));
28351
29115
  const renderHostSections = (placement) => {
@@ -28415,6 +29179,13 @@ ${overlayScrollbarCss(".vela-sd-pane")}
28415
29179
  body.append(sid(this.sectionTitle("Theme"), "canvas.theme"));
28416
29180
  body.append(sid(this.selectRow("Color theme", tc.current === "dark" ? "Dark" : "Light", ["Dark", "Light"], (v) => tc.onSelect(v === "Dark" ? "dark" : "light")), "canvas.theme"));
28417
29181
  }
29182
+ if (this.markGroups.length > 0) {
29183
+ body.append(sid(this.section("Events"), MARKS_SETTINGS_ID));
29184
+ body.append(sid(this.sectionTitle("Visible events"), MARKS_GROUPS_SETTINGS_ID));
29185
+ for (const g of this.markGroups) {
29186
+ body.append(sid(this.boolRow(g.label, this.markGroupVisible(g.id), (v) => this.emit({ marks: { groups: { [g.id]: v } } })), markGroupSettingsId(g.id)));
29187
+ }
29188
+ }
28418
29189
  renderChartTypeSections("end");
28419
29190
  renderHostSections("end");
28420
29191
  if (this.hiddenSettings.size > 0) {
@@ -34619,6 +35390,383 @@ ${overlayScrollbarCss(".vela-sd-pane")}
34619
35390
  return { ...scale, min: scale.min - belowPx * perPx, max: scale.max + abovePx * perPx };
34620
35391
  }
34621
35392
 
35393
+ // src/ui/sanitize-html.ts
35394
+ var ALLOWED_TAGS = /* @__PURE__ */ new Set([
35395
+ "a",
35396
+ "abbr",
35397
+ "b",
35398
+ "blockquote",
35399
+ "br",
35400
+ "code",
35401
+ "dd",
35402
+ "del",
35403
+ "div",
35404
+ "dl",
35405
+ "dt",
35406
+ "em",
35407
+ "h1",
35408
+ "h2",
35409
+ "h3",
35410
+ "h4",
35411
+ "h5",
35412
+ "h6",
35413
+ "hr",
35414
+ "i",
35415
+ "img",
35416
+ "ins",
35417
+ "kbd",
35418
+ "li",
35419
+ "mark",
35420
+ "ol",
35421
+ "p",
35422
+ "pre",
35423
+ "q",
35424
+ "s",
35425
+ "small",
35426
+ "span",
35427
+ "strong",
35428
+ "sub",
35429
+ "sup",
35430
+ "table",
35431
+ "tbody",
35432
+ "td",
35433
+ "tfoot",
35434
+ "th",
35435
+ "thead",
35436
+ "tr",
35437
+ "u",
35438
+ "ul"
35439
+ ]);
35440
+ var DROPPED_TAGS = /* @__PURE__ */ new Set([
35441
+ "script",
35442
+ "style",
35443
+ "iframe",
35444
+ "frame",
35445
+ "frameset",
35446
+ "object",
35447
+ "embed",
35448
+ "applet",
35449
+ "form",
35450
+ "input",
35451
+ "textarea",
35452
+ "button",
35453
+ "select",
35454
+ "option",
35455
+ "link",
35456
+ "meta",
35457
+ "base",
35458
+ "svg",
35459
+ "math",
35460
+ "template",
35461
+ "noscript",
35462
+ "audio",
35463
+ "video",
35464
+ "canvas",
35465
+ "dialog",
35466
+ "head",
35467
+ "title"
35468
+ ]);
35469
+ var ALLOWED_ATTRS = {
35470
+ a: /* @__PURE__ */ new Set(["href"]),
35471
+ img: /* @__PURE__ */ new Set(["src", "alt", "width", "height"]),
35472
+ td: /* @__PURE__ */ new Set(["colspan", "rowspan"]),
35473
+ th: /* @__PURE__ */ new Set(["colspan", "rowspan"]),
35474
+ ol: /* @__PURE__ */ new Set(["start"])
35475
+ };
35476
+ var BLOCKED_SCHEMES = /* @__PURE__ */ new Set(["javascript", "vbscript", "data", "file", "blob"]);
35477
+ function tagDisposition(tag) {
35478
+ const t = tag.toLowerCase();
35479
+ if (DROPPED_TAGS.has(t)) return "drop";
35480
+ return ALLOWED_TAGS.has(t) ? "keep" : "unwrap";
35481
+ }
35482
+ function attributeAllowed(tag, name) {
35483
+ const n = name.toLowerCase();
35484
+ if (n.startsWith("on") || n === "style") return false;
35485
+ if (n === "title") return true;
35486
+ return ALLOWED_ATTRS[tag.toLowerCase()]?.has(n) ?? false;
35487
+ }
35488
+ function safeUrl(value, absoluteOnly = false) {
35489
+ let url = "";
35490
+ for (const ch of value) if (ch.charCodeAt(0) > 32) url += ch;
35491
+ const m = /^([a-z][a-z0-9+.-]*):/i.exec(url);
35492
+ const scheme = m ? m[1].toLowerCase() : null;
35493
+ if (scheme !== null && BLOCKED_SCHEMES.has(scheme)) return null;
35494
+ if (absoluteOnly && scheme !== "http" && scheme !== "https") return null;
35495
+ return url;
35496
+ }
35497
+ var ELEMENT_NODE2 = 1;
35498
+ var TEXT_NODE = 3;
35499
+ function sanitizeHtml(html, doc) {
35500
+ const tpl = doc.createElement("template");
35501
+ tpl.innerHTML = html;
35502
+ const out = doc.createDocumentFragment();
35503
+ copyChildren(tpl.content, out, doc);
35504
+ return out;
35505
+ }
35506
+ function copyChildren(from, to, doc) {
35507
+ for (const child of Array.from(from.childNodes)) {
35508
+ if (child.nodeType === TEXT_NODE) {
35509
+ to.appendChild(doc.createTextNode(child.textContent ?? ""));
35510
+ continue;
35511
+ }
35512
+ if (child.nodeType !== ELEMENT_NODE2) continue;
35513
+ const el = child;
35514
+ const tag = el.tagName.toLowerCase();
35515
+ const disposition = tagDisposition(tag);
35516
+ if (disposition === "drop") continue;
35517
+ if (disposition === "unwrap") {
35518
+ copyChildren(el, to, doc);
35519
+ continue;
35520
+ }
35521
+ const clean = doc.createElement(tag);
35522
+ for (const attr of Array.from(el.attributes)) {
35523
+ const name = attr.name.toLowerCase();
35524
+ if (!attributeAllowed(tag, name)) continue;
35525
+ let value = attr.value;
35526
+ if (name === "href" || name === "src") {
35527
+ const safe = safeUrl(value, name === "src");
35528
+ if (safe === null) continue;
35529
+ value = safe;
35530
+ }
35531
+ clean.setAttribute(name, value);
35532
+ }
35533
+ if (tag === "a") {
35534
+ clean.setAttribute("target", "_blank");
35535
+ clean.setAttribute("rel", "noopener noreferrer");
35536
+ }
35537
+ copyChildren(el, clean, doc);
35538
+ to.appendChild(clean);
35539
+ }
35540
+ }
35541
+
35542
+ // src/renderers/native/chrome/marks/MarkPopover.ts
35543
+ var MARKS_STYLE_ID = "vela-marks-popover";
35544
+ var MARKS_CSS = `
35545
+ .vela-marks-panel { padding: 0; gap: 0; min-width: 220px; max-width: 320px; max-height: 320px; overflow-y: auto; overscroll-behavior: contain; }
35546
+ .vela-marks-section { display: flex; flex-direction: column; gap: 8px; padding: 10px 12px; }
35547
+ .vela-marks-section + .vela-marks-section { border-top: 1px solid var(--vela-border); }
35548
+ .vela-marks-field { display: flex; justify-content: space-between; gap: 16px; line-height: 1.45; }
35549
+ .vela-marks-field-label { color: var(--vela-fg-muted); }
35550
+ .vela-marks-field-value { color: var(--vela-fg-bright); text-align: right; font-variant-numeric: tabular-nums; }
35551
+ .vela-marks-html { color: var(--vela-fg); line-height: 1.45; overflow-wrap: anywhere; }
35552
+ .vela-marks-html p { margin: 0 0 6px; }
35553
+ .vela-marks-html p:last-child { margin-bottom: 0; }
35554
+ .vela-marks-html a { color: var(--vela-accent); }
35555
+ .vela-marks-html img { max-width: 100%; height: auto; }
35556
+ .vela-marks-html table { border-collapse: collapse; }
35557
+ .vela-marks-html td, .vela-marks-html th { padding: 2px 6px; border: 1px solid var(--vela-border); }
35558
+ .vela-marks-html pre, .vela-marks-html code { font-family: var(--vela-font-mono, monospace); font-size: 0.92em; }
35559
+ .vela-marks-loading, .vela-marks-error { color: var(--vela-fg-muted); font-style: italic; }
35560
+ `;
35561
+ var MarkPopover = class {
35562
+ constructor(deps) {
35563
+ this.deps = deps;
35564
+ this.pop = null;
35565
+ this.openKey = null;
35566
+ /** Bumped per open/close — a lazy content resolving after its popup went away is dropped. */
35567
+ this.generation = 0;
35568
+ const doc = deps.plot.ownerDocument;
35569
+ injectStyles(CALLOUT_STYLE_ID, CALLOUT_CSS, doc);
35570
+ injectStyles(MARKS_STYLE_ID, MARKS_CSS, doc);
35571
+ this.anchor = doc.createElement("div");
35572
+ this.anchor.className = "vela-marks-anchor";
35573
+ Object.assign(this.anchor.style, { position: "absolute", pointerEvents: "none", left: "0", top: "0", width: "0", height: "0" });
35574
+ deps.plot.appendChild(this.anchor);
35575
+ }
35576
+ /** The cluster key the open popup belongs to, or null. */
35577
+ get key() {
35578
+ return this.openKey;
35579
+ }
35580
+ open(cluster, rect) {
35581
+ this.close();
35582
+ this.place(rect);
35583
+ const gen = ++this.generation;
35584
+ this.openKey = cluster.key;
35585
+ this.pop = new Popover({
35586
+ trigger: this.anchor,
35587
+ host: this.deps.host(),
35588
+ theme: this.deps.theme(),
35589
+ gap: 8,
35590
+ align: "center",
35591
+ // centered on the glyph
35592
+ fadeMs: 120,
35593
+ // a short, discreet fade in and out
35594
+ className: "vela-marks-pop",
35595
+ content: (body) => this.build(body, cluster, gen),
35596
+ onClose: () => {
35597
+ if (this.generation === gen) {
35598
+ this.openKey = null;
35599
+ this.pop = null;
35600
+ this.deps.onOpenChange?.(null);
35601
+ }
35602
+ }
35603
+ });
35604
+ this.pop.show();
35605
+ this.deps.onOpenChange?.(cluster.key);
35606
+ }
35607
+ /** Follow the anchor glyph after a repaint; `null` (glyph gone — hidden, scrolled off, marks replaced) closes. */
35608
+ track(rect) {
35609
+ if (!this.pop) return;
35610
+ if (!rect) {
35611
+ this.close();
35612
+ return;
35613
+ }
35614
+ this.place(rect);
35615
+ this.pop.reposition();
35616
+ }
35617
+ close() {
35618
+ const pop = this.pop;
35619
+ const wasOpen = this.openKey !== null;
35620
+ this.pop = null;
35621
+ this.openKey = null;
35622
+ this.generation++;
35623
+ pop?.destroy();
35624
+ if (wasOpen) this.deps.onOpenChange?.(null);
35625
+ }
35626
+ destroy() {
35627
+ this.close();
35628
+ this.anchor.remove();
35629
+ }
35630
+ place(rect) {
35631
+ Object.assign(this.anchor.style, {
35632
+ left: `${rect.x - rect.size / 2}px`,
35633
+ top: `${rect.y - rect.size / 2}px`,
35634
+ width: `${rect.size}px`,
35635
+ height: `${rect.size}px`
35636
+ });
35637
+ }
35638
+ build(body, cluster, gen) {
35639
+ const doc = body.ownerDocument;
35640
+ const root = doc.createElement("div");
35641
+ root.className = "vela-callout-panel vela-marks-panel";
35642
+ const pending = [];
35643
+ for (const mark of cluster.marks) {
35644
+ const section = doc.createElement("section");
35645
+ section.className = "vela-marks-section";
35646
+ if (mark.title) {
35647
+ const title = doc.createElement("div");
35648
+ title.className = "vela-callout-title";
35649
+ title.textContent = mark.title;
35650
+ section.appendChild(title);
35651
+ }
35652
+ const content = mark.content;
35653
+ if (typeof content === "function") {
35654
+ const slot = doc.createElement("div");
35655
+ slot.className = "vela-marks-loading";
35656
+ slot.textContent = "Loading\u2026";
35657
+ section.appendChild(slot);
35658
+ pending.push({ el: section, resolve: () => this.resolveLazy(content, slot, gen) });
35659
+ } else if (content !== void 0) {
35660
+ this.renderContent(section, content);
35661
+ }
35662
+ if (section.childElementCount > 0) root.appendChild(section);
35663
+ }
35664
+ body.appendChild(root);
35665
+ if (pending.length > 0) scheduleLazy(root, pending);
35666
+ }
35667
+ resolveLazy(source, slot, gen) {
35668
+ let result;
35669
+ try {
35670
+ result = Promise.resolve(source());
35671
+ } catch (err) {
35672
+ result = Promise.reject(err instanceof Error ? err : new Error(String(err)));
35673
+ }
35674
+ void result.then(
35675
+ (content) => {
35676
+ if (gen !== this.generation) return;
35677
+ const section = slot.parentElement;
35678
+ if (!section) return;
35679
+ slot.remove();
35680
+ this.renderContent(section, content);
35681
+ this.pop?.reposition();
35682
+ },
35683
+ () => {
35684
+ if (gen !== this.generation) return;
35685
+ slot.className = "vela-marks-error";
35686
+ slot.textContent = "Couldn\u2019t load this entry.";
35687
+ }
35688
+ );
35689
+ }
35690
+ renderContent(section, content) {
35691
+ const doc = section.ownerDocument;
35692
+ if (!content || typeof content !== "object") return;
35693
+ if ("text" in content) {
35694
+ const text = doc.createElement("div");
35695
+ text.className = "vela-callout-text";
35696
+ text.textContent = String(content.text);
35697
+ section.appendChild(text);
35698
+ return;
35699
+ }
35700
+ if ("html" in content) {
35701
+ const html = doc.createElement("div");
35702
+ html.className = "vela-marks-html";
35703
+ html.appendChild(sanitizeHtml(String(content.html), doc));
35704
+ section.appendChild(html);
35705
+ return;
35706
+ }
35707
+ if ("panel" in content && content.panel && Array.isArray(content.panel.items)) {
35708
+ let actions = null;
35709
+ for (const item of content.panel.items) {
35710
+ if (!item || typeof item !== "object") continue;
35711
+ if (item.type === "button") {
35712
+ if (!actions) {
35713
+ actions = doc.createElement("div");
35714
+ actions.className = "vela-callout-actions";
35715
+ section.appendChild(actions);
35716
+ }
35717
+ const btn2 = doc.createElement("button");
35718
+ btn2.type = "button";
35719
+ btn2.className = "vela-callout-btn" + (item.primary ? " vela-callout-btn-primary" : "");
35720
+ btn2.textContent = item.label;
35721
+ btn2.addEventListener("click", () => {
35722
+ item.run();
35723
+ if (item.close !== false) this.close();
35724
+ });
35725
+ actions.appendChild(btn2);
35726
+ continue;
35727
+ }
35728
+ actions = null;
35729
+ if (item.type === "text") {
35730
+ const text = doc.createElement("div");
35731
+ text.className = "vela-callout-text";
35732
+ text.textContent = item.text;
35733
+ section.appendChild(text);
35734
+ } else if (item.type === "field") {
35735
+ const row = doc.createElement("div");
35736
+ row.className = "vela-marks-field";
35737
+ const label = doc.createElement("span");
35738
+ label.className = "vela-marks-field-label";
35739
+ label.textContent = item.label;
35740
+ const value = doc.createElement("span");
35741
+ value.className = "vela-marks-field-value";
35742
+ value.textContent = item.value;
35743
+ row.append(label, value);
35744
+ section.appendChild(row);
35745
+ }
35746
+ }
35747
+ }
35748
+ }
35749
+ };
35750
+ function scheduleLazy(scroller, pending) {
35751
+ if (typeof IntersectionObserver === "undefined") {
35752
+ for (const p of pending) p.resolve();
35753
+ return;
35754
+ }
35755
+ const io = new IntersectionObserver(
35756
+ (entries) => {
35757
+ for (const e of entries) {
35758
+ if (!e.isIntersecting) continue;
35759
+ const p = pending.find((q) => q.el === e.target);
35760
+ if (!p) continue;
35761
+ io.unobserve(e.target);
35762
+ p.resolve();
35763
+ }
35764
+ },
35765
+ { root: scroller }
35766
+ );
35767
+ for (const p of pending) io.observe(p.el);
35768
+ }
35769
+
34622
35770
  // src/renderers/native/core/manualScale.ts
34623
35771
  function rescaleAround(start, factor) {
34624
35772
  if (start.log && start.min > 0 && start.max > start.min) {
@@ -35347,11 +36495,12 @@ ${overlayScrollbarCss(".vela-sd-pane")}
35347
36495
  var SCROLL_BTN_PROXIMITY_PX = 120;
35348
36496
  var MIN_VISIBLE_BARS = 2;
35349
36497
  var ZOOM_OUT_MARGIN_BARS = 6;
35350
- var ZOOM_TAU_MS = 70;
35351
- var SCALE_TAU_MS = 80;
35352
- var FLING_TAU_MS = 110;
35353
- var SCROLL_TO_TAU_MS = 130;
36498
+ var INTRO_MODEL_FADE_MS = 350;
35354
36499
  var FLING_STOP_PX = 0.02;
36500
+ var MARK_FLASH_MS = 220;
36501
+ function frameNow() {
36502
+ return typeof performance !== "undefined" ? performance.now() : Date.now();
36503
+ }
35355
36504
  var PRICE_SCALE_K = 4e-3;
35356
36505
  var KEY_ZOOM_STEP = 0.2;
35357
36506
  var SEPARATOR_HIT_PX = 4;
@@ -35400,9 +36549,20 @@ ${overlayScrollbarCss(".vela-sd-pane")}
35400
36549
  this.indicatorSlices = new IndicatorDrawingSlices();
35401
36550
  /** Hover tooltips for Pine labels (canvas hit-rects collected by the chrome layer). */
35402
36551
  this.labelTooltip = null;
36552
+ /** The timeline-mark popup (a kit Popover anchored on a lane glyph); null before mount. */
36553
+ this.markPopover = null;
36554
+ /** How the fanned mark stack was opened: a hover folds when the pointer leaves, a tap only on a tap elsewhere. */
36555
+ this.marksExpandedBy = "hover";
36556
+ /** The rAF loop keeping the chrome repainting while a lane glyph pulses (hover) or flashes (click); null when idle. */
36557
+ this.markPulseRaf = null;
36558
+ this.markClickCbs = /* @__PURE__ */ new Set();
35403
36559
  this.crosshairLayer = new CrosshairRenderer();
35404
- /** 1 Hz repaint pump so the price-axis countdown-to-bar-close ticks; null when off. */
35405
- this.countdownTimer = null;
36560
+ /** The second pulse the countdown-to-bar-close chip ticks on: the host's (`setWallClock`)
36561
+ * when one is wired, else the renderer's own second-aligned clock. */
36562
+ this.hostClock = null;
36563
+ this.ownClock = null;
36564
+ /** Live subscription to the pulse while the countdown is on; null when off. */
36565
+ this.countdownUnsub = null;
35406
36566
  this.symbolPicker = null;
35407
36567
  /** Indicator titles (the legend rows) shown — held here so a remount re-applies it. */
35408
36568
  this.indicatorTitlesOn = true;
@@ -35420,19 +36580,25 @@ ${overlayScrollbarCss(".vela-sd-pane")}
35420
36580
  /** Drawings layer self-serves Ctrl+Z/Y (see the `historyChords` feature). */
35421
36581
  this.historyChordsEnabled = true;
35422
36582
  this.liveRegion = null;
35423
- // ── animation state (eased zoom + inertial pan + live-bar glide) ──
35424
- this.animZoom = true;
35425
- this.animPan = true;
35426
- this.animLiveBarMs = 0;
35427
- // forming-bar OHLC glide time-constant; 0 = each tick snaps
35428
- this.animLiveBarOnMs = LIVE_BAR_EASE_DEFAULT_MS;
35429
- // the duration the settings-dialog on/off toggle restores (last non-zero value configured)
36583
+ // ── animation state: one ease time-constant per motion (0 = off), each remembering the
36584
+ // host's duration so the config's on/off switches restore it (see EaseSetting) ──
36585
+ this.animZoom = new EaseSetting(ZOOM_EASE_DEFAULT_MS);
36586
+ // wheel-zoom glide
36587
+ this.animPan = new EaseSetting(PAN_INERTIA_DEFAULT_MS);
36588
+ // inertial-pan velocity decay
36589
+ this.animScroll = new EaseSetting(SCROLL_EASE_DEFAULT_MS);
36590
+ // scroll-to-latest / panBy glide
36591
+ this.animAutoscale = new EaseSetting(AUTOSCALE_EASE_DEFAULT_MS);
36592
+ // autoscale glide during zoom/fling
36593
+ this.animLiveBar = new EaseSetting(LIVE_BAR_EASE_DEFAULT_MS, 0);
36594
+ // forming-bar OHLC glide; ships off
35430
36595
  // Brand default candles.
35431
36596
  this.candleUp = BULLISH;
35432
36597
  this.candleDown = BEARISH;
35433
36598
  // ── intro reveal (plays once when candles first appear) ──
35434
- this.introStyle = "settle";
35435
- // 'grow' | 'settle' | '' (off)
36599
+ this.intro = { style: "settle", duration: INTRO_DURATION_DEFAULT_MS };
36600
+ this.introOnStyle = "settle";
36601
+ // the style the config's on/off switch restores
35436
36602
  this.introPlayed = false;
35437
36603
  this.introRaf = null;
35438
36604
  /** The load affordance (three pulsing dots) — up while the host reports a bar load in
@@ -35551,7 +36717,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
35551
36717
  this.moveIndicatorCbs = /* @__PURE__ */ new Set();
35552
36718
  this.priceStyleCbs = /* @__PURE__ */ new Set();
35553
36719
  this.name = "native";
35554
- this.features = ["logScale", "currentPriceLine", "priceLabel", "countdown", "upColor", "downColor", "glow", "animZoom", "animPan", "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", "indicatorTitles", "indicatorValues"];
36720
+ 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"];
35555
36721
  /** Track cursor proximity to the scroll button on the plot (bubbles from the button too,
35556
36722
  * so moving onto the button doesn't count as leaving). */
35557
36723
  this.onScrollProximityMove = (e) => {
@@ -35583,9 +36749,12 @@ ${overlayScrollbarCss(".vela-sd-pane")}
35583
36749
  this.scene.showPriceLine = opts.currentPriceLine;
35584
36750
  this.scene.logScale = opts.logScale;
35585
36751
  this.backendMode = opts.nativeBackend;
35586
- this.animZoom = opts.animZoom;
35587
- this.animPan = opts.animPan;
35588
- this.setLiveBarEase(resolveLiveBarEaseMs(opts.animLiveBar));
36752
+ this.animZoom.set(opts.animZoom);
36753
+ this.animPan.set(opts.animPan);
36754
+ this.animScroll.set(opts.animScroll);
36755
+ this.animAutoscale.set(opts.animAutoscale);
36756
+ this.animLiveBar.set(opts.animLiveBar);
36757
+ this.setIntro(opts.animIntro);
35589
36758
  this.glowAmount = opts.glow;
35590
36759
  this.candleUp = opts.upColor;
35591
36760
  this.candleDown = opts.downColor;
@@ -35629,20 +36798,29 @@ ${overlayScrollbarCss(".vela-sd-pane")}
35629
36798
  if (this.backend && "glow" in this.backend) this.backend.glow = this.glowAmount;
35630
36799
  break;
35631
36800
  case "animZoom":
35632
- this.animZoom = Boolean(value);
36801
+ this.animZoom.set(resolveEaseMs(value, ZOOM_EASE_DEFAULT_MS));
35633
36802
  return;
35634
36803
  // affects the next interaction only — nothing to repaint
35635
- case "animPan":
35636
- this.animPan = Boolean(value);
36804
+ case "animPan": {
36805
+ const ms = resolveEaseMs(value, PAN_INERTIA_DEFAULT_MS);
36806
+ this.animPan.set(ms);
36807
+ this.animScroll.toggle(ms > 0);
36808
+ return;
36809
+ }
36810
+ case "animScroll":
36811
+ this.animScroll.set(resolveEaseMs(value, SCROLL_EASE_DEFAULT_MS));
36812
+ return;
36813
+ case "animAutoscale":
36814
+ this.animAutoscale.set(resolveEaseMs(value, AUTOSCALE_EASE_DEFAULT_MS));
35637
36815
  return;
36816
+ // a glide in flight finishes at the new rate (or snaps at 0)
35638
36817
  case "animLiveBar":
35639
- this.setLiveBarEase(resolveLiveBarEaseMs(value));
36818
+ this.animLiveBar.set(resolveLiveBarEaseMs(value));
35640
36819
  return;
35641
36820
  // affects the next tick only; a glide in flight finishes at the new rate (or snaps at 0)
35642
36821
  case "intro": {
35643
- const s = value === false || value === "none" || value === "off" || value == null ? "" : String(value);
35644
- this.introStyle = s;
35645
- if (s) this.playIntro(s);
36822
+ this.setIntro(resolveIntro(value));
36823
+ if (this.intro.style) this.playIntro();
35646
36824
  return;
35647
36825
  }
35648
36826
  case "zoomAnchor":
@@ -35714,6 +36892,10 @@ ${overlayScrollbarCss(".vela-sd-pane")}
35714
36892
  case "tradeMarkers":
35715
36893
  this.scene.tradeMarkers = mergeTradeMarkersState(this.scene.tradeMarkers, value);
35716
36894
  break;
36895
+ case "marks":
36896
+ this.scene.marks = mergeMarksState(this.scene.marks, value);
36897
+ this.markPopover?.close();
36898
+ break;
35717
36899
  case "keyboard":
35718
36900
  this.setKeyboardEnabled(Boolean(value));
35719
36901
  return;
@@ -35772,13 +36954,17 @@ ${overlayScrollbarCss(".vela-sd-pane")}
35772
36954
  case "glow":
35773
36955
  return this.glowAmount;
35774
36956
  case "animZoom":
35775
- return this.animZoom;
36957
+ return this.animZoom.tau;
35776
36958
  case "animPan":
35777
- return this.animPan;
36959
+ return this.animPan.tau;
36960
+ case "animScroll":
36961
+ return this.animScroll.tau;
36962
+ case "animAutoscale":
36963
+ return this.animAutoscale.tau;
35778
36964
  case "animLiveBar":
35779
- return this.animLiveBarMs;
36965
+ return this.animLiveBar.tau;
35780
36966
  case "intro":
35781
- return this.introStyle;
36967
+ return this.intro.style;
35782
36968
  case "zoomAnchor":
35783
36969
  return this.zoomAnchorMode;
35784
36970
  case "axisDrag":
@@ -35821,6 +37007,8 @@ ${overlayScrollbarCss(".vela-sd-pane")}
35821
37007
  }
35822
37008
  case "tradeMarkers":
35823
37009
  return { ...this.scene.tradeMarkers, colors: { ...this.scene.tradeMarkers.colors } };
37010
+ case "marks":
37011
+ return { visible: this.scene.marks.visible, groups: { ...this.scene.marks.groups } };
35824
37012
  case "keyboard":
35825
37013
  return this.keyboardEnabled;
35826
37014
  case "historyChords":
@@ -35946,7 +37134,13 @@ ${overlayScrollbarCss(".vela-sd-pane")}
35946
37134
  currentPriceLine: this.scene.showPriceLine,
35947
37135
  priceLabel: this.scene.showPriceLabel,
35948
37136
  countdown: this.scene.showCountdown,
35949
- animateLastPrice: this.animLiveBarMs > 0
37137
+ animateLastPrice: this.animLiveBar.on
37138
+ },
37139
+ animations: {
37140
+ zoom: this.animZoom.on,
37141
+ pan: this.animPan.on,
37142
+ autoscale: this.animAutoscale.on,
37143
+ intro: this.intro.style !== false
35950
37144
  },
35951
37145
  panes: { separatorColor: s.separatorColor ?? t.borderColor },
35952
37146
  trades: {
@@ -35958,6 +37152,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
35958
37152
  exitColor: this.scene.tradeMarkers.colors.exit
35959
37153
  },
35960
37154
  timeScale: { timezone: this.scene.timezone },
37155
+ marks: { visible: this.scene.marks.visible, groups: { ...this.scene.marks.groups } },
35961
37156
  candles: {
35962
37157
  upColor: this.candleUp,
35963
37158
  downColor: this.candleDown,
@@ -36053,7 +37248,12 @@ ${overlayScrollbarCss(".vela-sd-pane")}
36053
37248
  this.scene.showPriceLabel = next2.priceScale.priceLabel;
36054
37249
  this.scene.showCountdown = next2.priceScale.countdown;
36055
37250
  this.syncCountdownTimer();
36056
- this.animLiveBarMs = next2.priceScale.animateLastPrice ? this.animLiveBarOnMs : 0;
37251
+ this.animLiveBar.toggle(next2.priceScale.animateLastPrice);
37252
+ this.animZoom.toggle(next2.animations.zoom);
37253
+ this.animPan.toggle(next2.animations.pan);
37254
+ this.animScroll.toggle(next2.animations.pan);
37255
+ this.animAutoscale.toggle(next2.animations.autoscale);
37256
+ this.intro = { style: next2.animations.intro ? this.introOnStyle : false, duration: this.intro.duration || INTRO_DURATION_DEFAULT_MS };
36057
37257
  s.separatorColor = keepInherit(s.separatorColor, next2.panes.separatorColor, prevTheme.borderColor);
36058
37258
  this.scene.tradeMarkers = {
36059
37259
  visible: next2.trades.visible,
@@ -36062,6 +37262,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
36062
37262
  colors: { long: next2.trades.longColor, short: next2.trades.shortColor, exit: next2.trades.exitColor }
36063
37263
  };
36064
37264
  this.scene.timezone = next2.timeScale.timezone;
37265
+ this.scene.marks = { visible: next2.marks.visible, groups: { ...next2.marks.groups } };
36065
37266
  this.candleUp = next2.candles.upColor;
36066
37267
  this.candleDown = next2.candles.downColor;
36067
37268
  s.candle = {
@@ -36194,6 +37395,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
36194
37395
  }
36195
37396
  this.settingsDialog.setTheme(this.theme);
36196
37397
  this.settingsDialog.setHostSections(this.hostSettingsSections);
37398
+ this.settingsDialog.setMarkGroups(this.markGroupsInUse(), (id) => markGroupVisible(this.scene.marks, id, this.scene.markGroups));
36197
37399
  this.settingsDialog.setHiddenSettings(this.hiddenSettings);
36198
37400
  this.syncThemeControl();
36199
37401
  this.settingsDialog.toggle(
@@ -36284,10 +37486,10 @@ ${overlayScrollbarCss(".vela-sd-pane")}
36284
37486
  this.glideRightOffset(ZOOM_OUT_MARGIN_BARS);
36285
37487
  }
36286
37488
  /** Ease rightOffset to `target` at constant zoom (see animTick's scroll glide);
36287
- * instant when pan animation is off. Shared by scroll-to-latest and panBy. */
37489
+ * instant when the scroll glide is off. Shared by scroll-to-latest and panBy. */
36288
37490
  glideRightOffset(target) {
36289
37491
  const vp = this.coords.getViewport();
36290
- if (!this.animPan) {
37492
+ if (!this.animScroll.on) {
36291
37493
  this.applyViewport({ barSpacing: vp.barSpacing, rightOffset: target });
36292
37494
  return;
36293
37495
  }
@@ -36352,20 +37554,21 @@ ${overlayScrollbarCss(".vela-sd-pane")}
36352
37554
  * full size, eased, with a left→right stagger so the chart draws itself; `settle`
36353
37555
  * adds an ease-out-back overshoot. Autoscale stays on the real bars so the frame
36354
37556
  * never moves. Re-callable, so styles can be compared live from the console.
37557
+ * Style and sweep duration come from the resolved `intro` setting.
36355
37558
  */
36356
- playIntro(style) {
37559
+ playIntro() {
36357
37560
  if (this.introRaf != null) cancelAnimationFrame(this.introRaf);
36358
37561
  this.introRaf = null;
37562
+ const { style, duration } = this.intro;
36359
37563
  const real = this.bars;
36360
37564
  const n = real.length;
36361
- if (n === 0) return;
37565
+ if (n === 0 || !style) return;
36362
37566
  this.computeScales();
36363
37567
  for (const pane of this.scene.panes.values()) pane.scale = { ...pane.scaleTarget };
36364
37568
  this.modelAlpha = 0;
36365
- const DURATION = 650;
36366
37569
  const start = performance.now();
36367
37570
  const step = (now) => {
36368
- const p = Math.min(1, (now - start) / DURATION);
37571
+ const p = Math.min(1, (now - start) / duration);
36369
37572
  this.scene.bars = p >= 1 ? real : real.map((b, i) => this.revealCandle(b, i, p, n, style));
36370
37573
  this.paintData();
36371
37574
  if (p < 1) {
@@ -36379,10 +37582,10 @@ ${overlayScrollbarCss(".vela-sd-pane")}
36379
37582
  }
36380
37583
  /** After the candle reveal, fade the indicator models (series/fills/…) from hidden to full. */
36381
37584
  fadeInModels() {
36382
- const FADE = 350;
37585
+ const fade = Math.min(INTRO_MODEL_FADE_MS, this.intro.duration || INTRO_MODEL_FADE_MS);
36383
37586
  const start = performance.now();
36384
37587
  const step = (now) => {
36385
- this.modelAlpha = Math.min(1, (now - start) / FADE);
37588
+ this.modelAlpha = Math.min(1, (now - start) / fade);
36386
37589
  this.paintData();
36387
37590
  if (this.modelAlpha < 1) {
36388
37591
  this.introRaf = requestAnimationFrame(step);
@@ -36497,7 +37700,8 @@ ${overlayScrollbarCss(".vela-sd-pane")}
36497
37700
  zoomTo: (target, anchorLogical, anchorX) => this.zoomTo(target, anchorLogical, anchorX),
36498
37701
  fling: (v) => this.fling(v),
36499
37702
  onPointerMove: (x, y) => this.handlePointerMove(x, y),
36500
- onClick: (x) => {
37703
+ onClick: (x, y) => {
37704
+ if (this.handleMarkClick(x, y)) return;
36501
37705
  this.userDrawings?.deselect();
36502
37706
  this.handleClick(x);
36503
37707
  },
@@ -36526,7 +37730,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
36526
37730
  drawingsPointerDown: (x, y, snap, shift4, mod) => this.userDrawings?.pointerDown(x, y, snap, shift4, mod),
36527
37731
  drawingsPointerMove: (x, y, snap, shift4, mod) => this.userDrawings?.pointerMove(x, y, snap, shift4, mod),
36528
37732
  drawingsPointerUp: (x, y, snap) => this.userDrawings?.pointerUp(x, y, snap),
36529
- drawingsCursor: (x, y) => this.userDrawings?.cursorAt(x, y) ?? null,
37733
+ drawingsCursor: (x, y) => this.userDrawings?.cursorAt(x, y) ?? (this.chrome.markGlyphAt(x, y) ? "pointer" : null),
36530
37734
  drawingsDblClick: (x, y) => this.userDrawings?.dblClick(x, y) ?? false,
36531
37735
  drawingsClearTransient: () => this.userDrawings?.clearTransient()
36532
37736
  });
@@ -36542,8 +37746,18 @@ ${overlayScrollbarCss(".vela-sd-pane")}
36542
37746
  this.plot.addEventListener("pointerleave", this.onScrollProximityLeave);
36543
37747
  this.labelTooltip = new LabelTooltip(this.plot, {
36544
37748
  theme: () => this.chromeTheme(),
36545
- lookup: (x, y) => this.indicatorSlices.labelTooltipAt(x, y)
37749
+ lookup: (x, y) => this.indicatorSlices.labelTooltipAt(x, y) ?? this.chrome.markTooltipAt(x, y, this.markGroupsInUse())
37750
+ });
37751
+ this.markPopover = new MarkPopover({
37752
+ plot: this.plot,
37753
+ host: () => this.dialogHost ?? this.plot,
37754
+ theme: () => this.chromeTheme(),
37755
+ onOpenChange: (key) => {
37756
+ this.scene.marksActiveKey = key;
37757
+ this.scheduler?.invalidate(2 /* Chrome */);
37758
+ }
36546
37759
  });
37760
+ this.chrome.setMarkIconReady(() => this.scheduler?.invalidate(2 /* Chrome */));
36547
37761
  this.userDrawings = new UserDrawingController(this.wrapper, this.plot, this.drawingsCanvas, {
36548
37762
  projector: () => this.drawingProjector(),
36549
37763
  dpr: () => this.coords.dpr,
@@ -36758,29 +37972,40 @@ ${overlayScrollbarCss(".vela-sd-pane")}
36758
37972
  resize() {
36759
37973
  this.syncSize();
36760
37974
  }
36761
- /** Run a 1 Hz repaint pump while the countdown chip is on (so it ticks); stop it otherwise.
36762
- * Chrome tier: only the chip's wall-clock text moves — an idle chart must not recompute
37975
+ /** Drive the countdown chip from the host's second pulse (`null` the renderer's own). */
37976
+ setWallClock(clock) {
37977
+ if (clock === this.hostClock) return;
37978
+ this.hostClock = clock;
37979
+ if (this.countdownUnsub != null) {
37980
+ this.countdownUnsub();
37981
+ this.countdownUnsub = null;
37982
+ }
37983
+ this.syncCountdownTimer();
37984
+ }
37985
+ /** Subscribe to the second pulse while the countdown chip is on (so it ticks); unsubscribe
37986
+ * otherwise. Chrome tier: only the chip's text moves — an idle chart must not recompute
36763
37987
  * scales or repaint the geometry/volume/VPVR/SDK layers once a second (that cost
36764
37988
  * multiplies by the cell count in a multi-chart workspace). */
36765
37989
  syncCountdownTimer() {
36766
37990
  if (this.scene.showCountdown) {
36767
- if (this.countdownTimer == null) {
36768
- this.countdownTimer = setInterval(() => {
37991
+ if (this.countdownUnsub == null) {
37992
+ const clock = this.hostClock ?? (this.ownClock ?? (this.ownClock = new SecondClock()));
37993
+ this.countdownUnsub = clock.onTick(() => {
36769
37994
  if (this.scene.showCountdown && this.scene.bars.length > 0) this.scheduler?.invalidate(2 /* Chrome */);
36770
- }, 1e3);
37995
+ });
36771
37996
  }
36772
- } else if (this.countdownTimer != null) {
36773
- clearInterval(this.countdownTimer);
36774
- this.countdownTimer = null;
37997
+ } else if (this.countdownUnsub != null) {
37998
+ this.countdownUnsub();
37999
+ this.countdownUnsub = null;
36775
38000
  }
36776
38001
  }
36777
38002
  destroy() {
36778
38003
  if (this.introRaf != null) cancelAnimationFrame(this.introRaf);
36779
38004
  this.loadingEl?.remove();
36780
38005
  this.loadingEl = null;
36781
- if (this.countdownTimer != null) {
36782
- clearInterval(this.countdownTimer);
36783
- this.countdownTimer = null;
38006
+ if (this.countdownUnsub != null) {
38007
+ this.countdownUnsub();
38008
+ this.countdownUnsub = null;
36784
38009
  }
36785
38010
  this.scheduler?.destroy();
36786
38011
  this.animator?.stop();
@@ -36805,6 +38030,11 @@ ${overlayScrollbarCss(".vela-sd-pane")}
36805
38030
  this.plot?.removeEventListener("pointerleave", this.onScrollProximityLeave);
36806
38031
  this.labelTooltip?.destroy();
36807
38032
  this.labelTooltip = null;
38033
+ this.markPopover?.destroy();
38034
+ this.markPopover = null;
38035
+ this.chrome.setMarkIconReady(null);
38036
+ if (this.markPulseRaf !== null) cancelAnimationFrame(this.markPulseRaf);
38037
+ this.markPulseRaf = null;
36808
38038
  this.scrollButton?.remove();
36809
38039
  this.scrollButton = null;
36810
38040
  for (const l of this.extLayers) l.instance.destroy?.();
@@ -36863,8 +38093,8 @@ ${overlayScrollbarCss(".vela-sd-pane")}
36863
38093
  }
36864
38094
  if (!this.introPlayed && this.bars.length > 0) {
36865
38095
  this.introPlayed = true;
36866
- if (this.introStyle) {
36867
- this.playIntro(this.introStyle);
38096
+ if (this.intro.style) {
38097
+ this.playIntro();
36868
38098
  return;
36869
38099
  }
36870
38100
  }
@@ -36875,7 +38105,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
36875
38105
  const last2 = this.bars[n - 1];
36876
38106
  if (last2 && bar.time === last2.time) {
36877
38107
  this.bars[n - 1] = bar;
36878
- if (this.animLiveBarMs <= 0 || this.liveEaseTime !== bar.time) {
38108
+ if (!this.animLiveBar.on || this.liveEaseTime !== bar.time) {
36879
38109
  this.syncLiveEase(bar);
36880
38110
  } else {
36881
38111
  this.animator.start();
@@ -36891,11 +38121,11 @@ ${overlayScrollbarCss(".vela-sd-pane")}
36891
38121
  this.scene.bars = this.bars;
36892
38122
  this.scheduler.invalidate(4 /* Full */);
36893
38123
  }
36894
- /** Set the live-bar glide duration (0 = off). A non-zero value is also remembered as
36895
- * what the config's on/off toggle (`priceScale.animateLastPrice`) switches back on to. */
36896
- setLiveBarEase(ms) {
36897
- this.animLiveBarMs = ms;
36898
- if (ms > 0) this.animLiveBarOnMs = ms;
38124
+ /** Set the reveal (style + duration). A non-off style is also remembered as what the
38125
+ * config's on/off toggle (`animations.intro`) switches back on to. */
38126
+ setIntro(next2) {
38127
+ this.intro = next2;
38128
+ if (next2.style) this.introOnStyle = next2.style;
36899
38129
  }
36900
38130
  /** Snap the eased forming-bar state to `bar` — no glide (a fresh bar or the first tick of one). */
36901
38131
  syncLiveEase(bar) {
@@ -36909,7 +38139,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
36909
38139
  const target = this.bars[this.bars.length - 1];
36910
38140
  if (!target || this.liveEaseTime !== target.time) return false;
36911
38141
  const eps = Math.max(1e-9, Math.abs(target.close) * 1e-6);
36912
- const tau = this.animLiveBarMs;
38142
+ const tau = this.animLiveBar.tau;
36913
38143
  const nh = easeToward(this.liveEaseHigh, target.high, dtMs, tau);
36914
38144
  const nl = easeToward(this.liveEaseLow, target.low, dtMs, tau);
36915
38145
  const nc = easeToward(this.liveEaseClose, target.close, dtMs, tau);
@@ -37049,10 +38279,12 @@ ${overlayScrollbarCss(".vela-sd-pane")}
37049
38279
  this.refreshAnchorOffset(model);
37050
38280
  if (model.native && this.extLayers.some((l) => l.def.id === model.native.type)) this.scene.assignIndicatorZTop(model.id);
37051
38281
  else this.scene.assignIndicatorZ(model.id);
37052
- this.inputsUI.upsert(model.id, model.shorttitle ?? model.title, model.inputs, model.inputValues, model.paneId, {
37053
- native: !!model.native,
37054
- ...model.props ? { props: model.props, propValues: model.propValues ?? {} } : {}
37055
- });
38282
+ if (model.legend !== false) {
38283
+ this.inputsUI.upsert(model.id, model.shorttitle ?? model.title, model.inputs, model.inputValues, model.paneId, {
38284
+ native: !!model.native,
38285
+ ...model.props ? { props: model.props, propValues: model.propValues ?? {} } : {}
38286
+ });
38287
+ }
37056
38288
  if (model.native?.type === "volume") {
37057
38289
  this.volumeActive = true;
37058
38290
  this.volumeHidden = false;
@@ -37199,7 +38431,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
37199
38431
  this.settingsDialog?.setHiddenSettings(this.hiddenSettings);
37200
38432
  }
37201
38433
  listSettingsIds() {
37202
- return settingsIdCatalog(this.hostSettingsSections);
38434
+ return settingsIdCatalog(this.hostSettingsSections, this.markGroupsInUse());
37203
38435
  }
37204
38436
  onChartTypeSettingsChange(cb) {
37205
38437
  this.chartTypeSettingsCbs.add(cb);
@@ -37225,6 +38457,22 @@ ${overlayScrollbarCss(".vela-sd-pane")}
37225
38457
  this.axisLongPressCbs.add(cb);
37226
38458
  return () => this.axisLongPressCbs.delete(cb);
37227
38459
  }
38460
+ // ── timeline marks (the `chart.marks` model; see the port) ──
38461
+ setTimelineMarks(marks, groups) {
38462
+ this.scene.timelineMarks = marks;
38463
+ this.scene.markGroups = groups;
38464
+ this.scene.marksExpandedStack = null;
38465
+ this.markPopover?.close();
38466
+ this.scheduler?.invalidate(2 /* Chrome */);
38467
+ }
38468
+ onMarkClick(cb) {
38469
+ this.markClickCbs.add(cb);
38470
+ return () => this.markClickCbs.delete(cb);
38471
+ }
38472
+ /** Every group the lane knows: the defined ones, then those marks name without a definition. */
38473
+ markGroupsInUse() {
38474
+ return effectiveMarkGroups(this.scene.timelineMarks, this.scene.markGroups);
38475
+ }
37228
38476
  onViewportChange(cb) {
37229
38477
  this.viewportCbs.add(cb);
37230
38478
  return () => this.viewportCbs.delete(cb);
@@ -37278,7 +38526,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
37278
38526
  this.zoomAnchorX = anchorX;
37279
38527
  this.panVelocity = 0;
37280
38528
  this.scrollTargetRO = null;
37281
- if (!this.animZoom) {
38529
+ if (!this.animZoom.on) {
37282
38530
  const v = this.clampViewport(barSpacing, this.anchoredRightOffset(barSpacing));
37283
38531
  this.coords.setViewport(v);
37284
38532
  this.targetBarSpacing = v.barSpacing;
@@ -37291,7 +38539,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
37291
38539
  }
37292
38540
  /** Inertial pan: continue with a rightOffset velocity (logical units / ms) that decays. */
37293
38541
  fling(velocity) {
37294
- if (!this.animPan) return;
38542
+ if (!this.animPan.on) return;
37295
38543
  this.scrollTargetRO = null;
37296
38544
  this.panVelocity = velocity;
37297
38545
  this.animator.start();
@@ -37331,7 +38579,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
37331
38579
  let active = false;
37332
38580
  const tbs = this.targetBarSpacing;
37333
38581
  if (Math.abs(barSpacing - tbs) > tbs * 1e-3) {
37334
- barSpacing = clampBarSpacing(easeToward(barSpacing, tbs, dtMs, ZOOM_TAU_MS));
38582
+ barSpacing = clampBarSpacing(easeToward(barSpacing, tbs, dtMs, this.animZoom.tau));
37335
38583
  rightOffset = this.anchoredRightOffset(barSpacing);
37336
38584
  active = true;
37337
38585
  } else if (barSpacing !== tbs) {
@@ -37341,13 +38589,14 @@ ${overlayScrollbarCss(".vela-sd-pane")}
37341
38589
  const stopVel = FLING_STOP_PX / Math.max(1e-6, barSpacing * this.coords.spacingScale);
37342
38590
  if (Math.abs(this.panVelocity) > stopVel) {
37343
38591
  rightOffset += this.panVelocity * dtMs;
37344
- this.panVelocity *= Math.exp(-dtMs / FLING_TAU_MS);
38592
+ const tau = this.animPan.tau;
38593
+ this.panVelocity = tau > 0 ? this.panVelocity * Math.exp(-dtMs / tau) : 0;
37345
38594
  if (Math.abs(this.panVelocity) <= stopVel) this.panVelocity = 0;
37346
38595
  else active = true;
37347
38596
  }
37348
38597
  if (this.scrollTargetRO != null) {
37349
38598
  const target = this.scrollTargetRO;
37350
- const next2 = easeToward(rightOffset, target, dtMs, SCROLL_TO_TAU_MS);
38599
+ const next2 = easeToward(rightOffset, target, dtMs, this.animScroll.tau);
37351
38600
  if (Math.abs(next2 - target) < 1e-3) {
37352
38601
  rightOffset = target;
37353
38602
  this.scrollTargetRO = null;
@@ -37375,6 +38624,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
37375
38624
  * through Math.log), not a non-linear jump. */
37376
38625
  easeScales(dtMs) {
37377
38626
  let moving = false;
38627
+ const tau = this.animAutoscale.tau;
37378
38628
  for (const pane of this.scene.panes.values()) {
37379
38629
  const t = pane.scaleTarget;
37380
38630
  const s = pane.scale;
@@ -37382,8 +38632,8 @@ ${overlayScrollbarCss(".vela-sd-pane")}
37382
38632
  const lt0 = Math.log(t.min);
37383
38633
  const lt1 = Math.log(t.max);
37384
38634
  const lspan = Math.max(1e-9, Math.abs(lt1 - lt0));
37385
- const n0 = easeToward(Math.log(s.min), lt0, dtMs, SCALE_TAU_MS);
37386
- const n1 = easeToward(Math.log(s.max), lt1, dtMs, SCALE_TAU_MS);
38635
+ const n0 = easeToward(Math.log(s.min), lt0, dtMs, tau);
38636
+ const n1 = easeToward(Math.log(s.max), lt1, dtMs, tau);
37387
38637
  if (Math.abs(n0 - lt0) <= lspan * 1e-3 && Math.abs(n1 - lt1) <= lspan * 1e-3) {
37388
38638
  pane.scale = { min: t.min, max: t.max, log: true };
37389
38639
  } else {
@@ -37393,8 +38643,8 @@ ${overlayScrollbarCss(".vela-sd-pane")}
37393
38643
  continue;
37394
38644
  }
37395
38645
  const span = Math.max(1e-9, Math.abs(t.max - t.min));
37396
- let nmin = easeToward(s.min, t.min, dtMs, SCALE_TAU_MS);
37397
- let nmax = easeToward(s.max, t.max, dtMs, SCALE_TAU_MS);
38646
+ let nmin = easeToward(s.min, t.min, dtMs, tau);
38647
+ let nmax = easeToward(s.max, t.max, dtMs, tau);
37398
38648
  if (Math.abs(nmin - t.min) <= span * 1e-3 && Math.abs(nmax - t.max) <= span * 1e-3) {
37399
38649
  nmin = t.min;
37400
38650
  nmax = t.max;
@@ -37407,8 +38657,8 @@ ${overlayScrollbarCss(".vela-sd-pane")}
37407
38657
  const t = sl.scaleTarget;
37408
38658
  const s = sl.scale;
37409
38659
  const span = Math.max(1e-9, Math.abs(t.max - t.min));
37410
- let nmin = easeToward(s.min, t.min, dtMs, SCALE_TAU_MS);
37411
- let nmax = easeToward(s.max, t.max, dtMs, SCALE_TAU_MS);
38660
+ let nmin = easeToward(s.min, t.min, dtMs, tau);
38661
+ let nmax = easeToward(s.max, t.max, dtMs, tau);
37412
38662
  if (Math.abs(nmin - t.min) <= span * 1e-3 && Math.abs(nmax - t.max) <= span * 1e-3) {
37413
38663
  nmin = t.min;
37414
38664
  nmax = t.max;
@@ -37429,6 +38679,8 @@ ${overlayScrollbarCss(".vela-sd-pane")}
37429
38679
  this.scene.crosshair = null;
37430
38680
  this.hoverSeparatorY = null;
37431
38681
  this.lastPointer = null;
38682
+ if (this.marksExpandedBy === "hover") this.setMarksExpanded(null);
38683
+ this.setMarkHover(null);
37432
38684
  this.scheduler.invalidate(1 /* Cursor */);
37433
38685
  this.hoverLogical = null;
37434
38686
  const empty = { time: null, price: null, paneKind: null, values: /* @__PURE__ */ new Map(), ohlc: null };
@@ -37440,6 +38692,8 @@ ${overlayScrollbarCss(".vela-sd-pane")}
37440
38692
  this.lastPointer = inData ? { x, y } : null;
37441
38693
  this.hoverSeparatorY = x >= 0 && y >= 0 && y <= this.coords.height ? this.separatorHoverY(y) : null;
37442
38694
  this.scheduler.invalidate(1 /* Cursor */);
38695
+ this.setMarksExpanded(inData ? this.chrome.markStackAt(x, y) : null);
38696
+ this.setMarkHover(inData ? this.chrome.markGlyphAt(x, y)?.cluster.key ?? null : null);
37443
38697
  const logical = Math.round(this.coords.xToLogical(x));
37444
38698
  const onBar = logical >= 0 && logical < this.coords.barCount;
37445
38699
  const time = onBar ? this.coords.logicalToTime(logical) : null;
@@ -37469,6 +38723,76 @@ ${overlayScrollbarCss(".vela-sd-pane")}
37469
38723
  const onBar = logical >= 0 && logical < this.coords.barCount;
37470
38724
  for (const cb of this.clickCbs) cb({ time: onBar ? this.coords.logicalToTime(logical) : null, price: null });
37471
38725
  }
38726
+ /**
38727
+ * Fan out (or collapse, with null) a multi-group mark stack; repaints the chrome tier when
38728
+ * it changes. A stack whose glyph holds the open popup stays fanned — the pointer leaving
38729
+ * the plot for the popup must not bury the glyph under the deck (which would close it).
38730
+ */
38731
+ setMarksExpanded(stack, by = "hover") {
38732
+ if (this.scene.marksExpandedStack === stack) return;
38733
+ if (stack === null && this.markPopover?.key) {
38734
+ const open2 = this.chrome.markGlyphByKey(this.markPopover.key);
38735
+ if (open2 && open2.stack === this.scene.marksExpandedStack) return;
38736
+ }
38737
+ this.scene.marksExpandedStack = stack;
38738
+ this.marksExpandedBy = by;
38739
+ this.scheduler?.invalidate(2 /* Chrome */);
38740
+ }
38741
+ /**
38742
+ * A click on the mark lane: a collapsed deck fans out (the touch path — a mouse already
38743
+ * fanned it by hovering), a glyph reports its cluster (`onMarkClick`) and opens the popup
38744
+ * when any of its marks carries content. True when the click landed on the lane.
38745
+ */
38746
+ handleMarkClick(x, y) {
38747
+ const glyph = this.chrome.markGlyphAt(x, y);
38748
+ if (!glyph) {
38749
+ if (this.scene.marksExpandedStack !== null && this.chrome.markStackAt(x, y) === null) this.setMarksExpanded(null);
38750
+ return false;
38751
+ }
38752
+ if (glyph.decked) {
38753
+ this.setMarksExpanded(glyph.stack, "tap");
38754
+ return true;
38755
+ }
38756
+ const marks = glyph.cluster.marks;
38757
+ const first2 = marks[0];
38758
+ const event = { id: first2.id, ids: marks.map((m) => m.id), time: first2.time, ...glyph.cluster.group !== void 0 ? { group: glyph.cluster.group } : {} };
38759
+ for (const cb of this.markClickCbs) cb(event);
38760
+ if (marks.some((m) => m.content !== void 0)) {
38761
+ this.markPopover?.open(glyph.cluster, { x: glyph.x, y: glyph.y, size: glyph.size });
38762
+ } else {
38763
+ this.scene.marksFlash = { key: glyph.cluster.key, until: frameNow() + MARK_FLASH_MS };
38764
+ this.syncMarkPulse();
38765
+ }
38766
+ return true;
38767
+ }
38768
+ /** After a chrome frame: keep the open mark popup on its glyph, or close it once the glyph is gone. */
38769
+ trackMarkPopover() {
38770
+ const key = this.markPopover?.key;
38771
+ if (!key) return;
38772
+ const g = this.chrome.markGlyphByKey(key);
38773
+ this.markPopover.track(g && !(g.decked && g.depth !== 0) ? { x: g.x, y: g.y, size: g.size } : null);
38774
+ }
38775
+ /** The lane glyph under the pointer — it swells once as the pointer lands (a rAF-driven chrome repaint for the pulse's duration). */
38776
+ setMarkHover(key) {
38777
+ if (this.scene.marksHoverKey === key) return;
38778
+ this.scene.marksHoverKey = key;
38779
+ this.scene.marksHoverSince = frameNow();
38780
+ this.scheduler?.invalidate(2 /* Chrome */);
38781
+ this.syncMarkPulse();
38782
+ }
38783
+ /** Run a chrome-tier repaint loop while a glyph's hover pulse or click flash plays; it stops itself once both are over. */
38784
+ syncMarkPulse() {
38785
+ if (this.markPulseRaf !== null || typeof requestAnimationFrame !== "function") return;
38786
+ const tick = () => {
38787
+ this.markPulseRaf = null;
38788
+ const now = frameNow();
38789
+ if (this.scene.marksFlash && this.scene.marksFlash.until <= now) this.scene.marksFlash = null;
38790
+ this.scheduler?.invalidate(2 /* Chrome */);
38791
+ const pulsing = this.scene.marksHoverKey !== null && now - this.scene.marksHoverSince < MARK_PULSE_MS;
38792
+ if (pulsing || this.scene.marksFlash !== null) this.markPulseRaf = requestAnimationFrame(tick);
38793
+ };
38794
+ this.markPulseRaf = requestAnimationFrame(tick);
38795
+ }
37472
38796
  paneAtY(y) {
37473
38797
  return this.paneNodeAtY(y);
37474
38798
  }
@@ -37860,6 +39184,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
37860
39184
  } else if (repaintsChrome(level) && this.paintedData) {
37861
39185
  this.chrome.prepare(this.scene, this.coords, this.theme);
37862
39186
  this.chrome.render(this.scene, this.coords, this.theme, this.axisSurface());
39187
+ this.trackMarkPopover();
37863
39188
  }
37864
39189
  this.crosshairLayer.render(this.scene, this.coords, this.theme, this.hoverSeparatorY, this.externalCrossPx());
37865
39190
  if (!repaintsData(level) && this.paintedData) this.repaintCursorLayers();
@@ -37875,7 +39200,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
37875
39200
  const lp = this.layerPane(l.def.id) ?? pane;
37876
39201
  if (lp.collapsed) continue;
37877
39202
  l.instance.render(this.extLayerArgs(l.def.id, lp.scale, lp.bounds, nowMs));
37878
- if (this.animZoom && l.instance.animating?.()) this.animator.start();
39203
+ if (this.animZoom.on && l.instance.animating?.()) this.animator.start();
37879
39204
  }
37880
39205
  }
37881
39206
  /** Blank one SDK layer canvas (a collapsed host pane suppresses the layer's painting). */
@@ -37940,7 +39265,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
37940
39265
  const args = this.extLayerArgs(l.def.id, lp.scale, lp.bounds, nowMs);
37941
39266
  l.instance.render(args);
37942
39267
  if (lp === pane) folded = foldBaseModulation(folded, l.instance.modulateBase?.(args) ?? null);
37943
- if (this.animZoom && l.instance.animating?.()) this.animator.start();
39268
+ if (this.animZoom.on && l.instance.animating?.()) this.animator.start();
37944
39269
  }
37945
39270
  if (folded) {
37946
39271
  if (folded.candleBodyScale != null) this.backend.candleBodyScale = clamp012(folded.candleBodyScale) || 0.01;
@@ -37959,6 +39284,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
37959
39284
  this.backdropRenderer.render(this.scene, this.coords, this.theme, gridAlpha);
37960
39285
  this.backend.render(this.scene, this.coords, this.theme);
37961
39286
  this.chrome.render(this.scene, this.coords, this.theme, this.axisSurface());
39287
+ this.trackMarkPopover();
37962
39288
  this.userDrawings?.render();
37963
39289
  if (easeLive && liveActual) this.bars[li] = liveActual;
37964
39290
  this.paintedData = true;
@@ -38471,7 +39797,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
38471
39797
  const map2 = /* @__PURE__ */ new Map();
38472
39798
  for (const pane of this.scene.panes.values()) {
38473
39799
  if (!pane.collapsed) continue;
38474
- const models = this.scene.orderedIndicatorsForPane(pane.id);
39800
+ const models = this.scene.orderedIndicatorsForPane(pane.id).filter((m) => m.legend !== false);
38475
39801
  const merged = new Set(this.scene.ownScaleIndicatorsForPane(pane.id).map((m) => m.id));
38476
39802
  const master = models.find((m) => !merged.has(m.id)) ?? models[0];
38477
39803
  map2.set(pane.id, master?.id ?? null);
@@ -41295,6 +42621,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
41295
42621
  if (Object.keys(defaults2).length > 0) this.rendererControl.set(defaults2);
41296
42622
  this.panesControl = new PanesControl(this.orchestrator);
41297
42623
  this.drawingsControl = new DrawingsControl(this.orchestrator.drawings);
42624
+ this.marksControl = new MarksControl(this.orchestrator.marks);
41298
42625
  }
41299
42626
  /**
41300
42627
  * Register a scripting engine so `addIndicator({ language })` can run that
@@ -41513,6 +42840,17 @@ ${overlayScrollbarCss(".vela-sd-pane")}
41513
42840
  get drawings() {
41514
42841
  return this.drawingsControl;
41515
42842
  }
42843
+ /**
42844
+ * The chart's timeline-marks control surface: host events pinned to a bar and shown
42845
+ * as glyphs on a lane above the time axis, each opening a popup on click —
42846
+ * `chart.marks.add({ id, time, glyph, title, content })`, `chart.marks.set(list)`,
42847
+ * `chart.marks.defineGroup({ id, label })`. Marks are data, not user state: re-supply
42848
+ * them on `market:changed`. On a renderer without the `timelineMarks` capability the
42849
+ * model still fills but nothing paints (`chart.marks.supported`).
42850
+ */
42851
+ get marks() {
42852
+ return this.marksControl;
42853
+ }
41516
42854
  on(event, handler) {
41517
42855
  return this.orchestrator.events.on(event, handler);
41518
42856
  }
@@ -42958,6 +44296,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
42958
44296
  exports.INVALID = INVALID;
42959
44297
  exports.LIGHT_THEME = LIGHT_THEME;
42960
44298
  exports.MARKER = MARKER;
44299
+ exports.MarksControl = MarksControl;
42961
44300
  exports.MultiProviderFeed = MultiProviderFeed;
42962
44301
  exports.NEUTRAL = NEUTRAL;
42963
44302
  exports.NativeRenderer = NativeRenderer;
@@ -42969,6 +44308,7 @@ ${overlayScrollbarCss(".vela-sd-pane")}
42969
44308
  exports.SESSION_PRE = SESSION_PRE;
42970
44309
  exports.SLATE = SLATE;
42971
44310
  exports.SLATE_DEEP = SLATE_DEEP;
44311
+ exports.SecondClock = SecondClock;
42972
44312
  exports.TRADE_EXIT = TRADE_EXIT;
42973
44313
  exports.TRADE_LONG = TRADE_LONG;
42974
44314
  exports.TRADE_SHORT = TRADE_SHORT;