@luxalgo/vela 0.6.10 → 0.6.12

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 (42) hide show
  1. package/README.md +110 -46
  2. package/dist/{DataProvider-CWmp31dA.d.ts → DataProvider-DhzstpQb.d.ts} +1 -1
  3. package/dist/{DataProvider-l_eLMly_.d.cts → DataProvider-DlMtrwqM.d.cts} +1 -1
  4. package/dist/{chunk-4KZVZ7QQ.js → chunk-F4M24ANM.js} +130 -1
  5. package/dist/{chunk-NMTQXNT4.js → chunk-IZV3CW5N.js} +245 -43
  6. package/dist/{chunk-FFOP37FQ.js → chunk-KV7FDWSL.js} +748 -70
  7. package/dist/{contributions-BCz6Dr6a.d.ts → contributions-37nni40G.d.ts} +20 -4
  8. package/dist/{contributions-D9vTzm5p.d.cts → contributions-lPojhTxI.d.cts} +20 -4
  9. package/dist/index.cjs +866 -58
  10. package/dist/index.d.cts +12 -6
  11. package/dist/index.d.ts +12 -6
  12. package/dist/index.js +3 -3
  13. package/dist/{options-DSqHsQyN.d.cts → options-CYS5Wmlx.d.cts} +78 -2
  14. package/dist/{options-DSqHsQyN.d.ts → options-CYS5Wmlx.d.ts} +78 -2
  15. package/dist/{plugin-Bt8hLR8Z.d.cts → plugin-Cwikpz1m.d.cts} +10 -3
  16. package/dist/{plugin-CH7pfMrE.d.ts → plugin-DHyaoMjW.d.ts} +10 -3
  17. package/dist/plugin.cjs +119 -0
  18. package/dist/plugin.d.cts +4 -4
  19. package/dist/plugin.d.ts +4 -4
  20. package/dist/plugin.js +2 -2
  21. package/dist/providers/binance.d.cts +2 -2
  22. package/dist/providers/binance.d.ts +2 -2
  23. package/dist/providers/coinbase.d.cts +2 -2
  24. package/dist/providers/coinbase.d.ts +2 -2
  25. package/dist/providers/hyperliquid.d.cts +2 -2
  26. package/dist/providers/hyperliquid.d.ts +2 -2
  27. package/dist/{statusline-5K7y4Ya2.d.ts → statusline-model-CiJm-riV.d.cts} +17 -87
  28. package/dist/{statusline-TYLQmoeh.d.cts → statusline-model-D-q_OOx9.d.ts} +17 -87
  29. package/dist/ui.d.cts +1 -1
  30. package/dist/ui.d.ts +1 -1
  31. package/dist/vela.global.js +866 -58
  32. package/dist/vela.global.min.js +52 -52
  33. package/dist/widget.cjs +1109 -100
  34. package/dist/widget.d.cts +118 -7
  35. package/dist/widget.d.ts +118 -7
  36. package/dist/widget.js +4 -4
  37. package/dist/workspace.cjs +1109 -100
  38. package/dist/workspace.d.cts +20 -11
  39. package/dist/workspace.d.ts +20 -11
  40. package/dist/workspace.js +3 -3
  41. package/package.json +1 -1
  42. /package/dist/{chunk-AOGZBKUE.js → chunk-H26BEHF4.js} +0 -0
package/dist/widget.cjs CHANGED
@@ -677,6 +677,7 @@ function intervalMs(timeframe) {
677
677
  "4h": 144e5,
678
678
  "1d": 864e5,
679
679
  "1w": 6048e5,
680
+ "1M": 2592e6,
680
681
  "1": 6e4,
681
682
  "5": 3e5,
682
683
  "15": 9e5,
@@ -684,7 +685,8 @@ function intervalMs(timeframe) {
684
685
  "60": 36e5,
685
686
  "240": 144e5,
686
687
  D: 864e5,
687
- W: 6048e5
688
+ W: 6048e5,
689
+ M: 2592e6
688
690
  };
689
691
  return map[timeframe] ?? 36e5;
690
692
  }
@@ -11241,6 +11243,111 @@ function roundFloat(n) {
11241
11243
  return Math.round(n * 1e8) / 1e8;
11242
11244
  }
11243
11245
 
11246
+ // src/core/drawings/types/Magnifier.ts
11247
+ var MAGNIFIER_TIMEFRAME_OPTIONS = [
11248
+ { value: "auto", label: "Auto", ms: 0 },
11249
+ { value: "1", label: "1m", ms: 6e4 },
11250
+ { value: "5", label: "5m", ms: 3e5 },
11251
+ { value: "15", label: "15m", ms: 9e5 },
11252
+ { value: "30", label: "30m", ms: 18e5 },
11253
+ { value: "60", label: "1h", ms: 36e5 },
11254
+ { value: "240", label: "4h", ms: 144e5 },
11255
+ { value: "D", label: "1D", ms: 864e5 }
11256
+ ];
11257
+ function magnifierTimeframeLabel(value) {
11258
+ const opt = MAGNIFIER_TIMEFRAME_OPTIONS.find((o) => o.value === value);
11259
+ if (opt) return opt.label;
11260
+ const n = Number(value);
11261
+ if (Number.isFinite(n) && n > 0) {
11262
+ if (n % 1440 === 0) return `${n / 1440}D`;
11263
+ if (n % 60 === 0) return `${n / 60}h`;
11264
+ return `${n}m`;
11265
+ }
11266
+ return value;
11267
+ }
11268
+ function defaultMagnifierStyle() {
11269
+ return { timeframe: "auto", upColor: "", downColor: "" };
11270
+ }
11271
+ var Magnifier = class extends Drawing {
11272
+ constructor(init) {
11273
+ super(init);
11274
+ this.type = "magnifier";
11275
+ /** Pixel rect of the timeframe chip as painted last frame, caret included — the chip is
11276
+ * an interactive dropdown trigger, so the interaction layer needs the exact rect the
11277
+ * painter measured. Renderer-transient: never serialized, null while unpainted. */
11278
+ this.chipRect = null;
11279
+ if (!this.magnifier) this.magnifier = defaultMagnifierStyle();
11280
+ }
11281
+ anchorSchema() {
11282
+ return { min: 2, max: 2, slots: [{ role: "c1", free: "both" }, { role: "c2", free: "both" }] };
11283
+ }
11284
+ placementMode() {
11285
+ return "drag";
11286
+ }
11287
+ /** The pixel rectangle between the two corner anchors (painter + hit-test share it). */
11288
+ rect(proj) {
11289
+ const a = this.anchors[0];
11290
+ const b = this.anchors[1];
11291
+ if (!a || !b) return null;
11292
+ const ya = proj.yOf(a.price, this.paneId);
11293
+ const yb = proj.yOf(b.price, this.paneId);
11294
+ if (ya == null || yb == null) return null;
11295
+ return { x1: proj.xOf(a.time), y1: ya, x2: proj.xOf(b.time), y2: yb };
11296
+ }
11297
+ hitTest(px, py, proj, tol) {
11298
+ const r = this.rect(proj);
11299
+ if (!r) return false;
11300
+ if (pointInBox(px, py, r.x1, r.y1, r.x2, r.y2)) return true;
11301
+ const edges = [
11302
+ [r.x1, r.y1, r.x2, r.y1],
11303
+ [r.x2, r.y1, r.x2, r.y2],
11304
+ [r.x2, r.y2, r.x1, r.y2],
11305
+ [r.x1, r.y2, r.x1, r.y1]
11306
+ ];
11307
+ return edges.some((e) => distToSegment(px, py, e[0], e[1], e[2], e[3]) <= tol);
11308
+ }
11309
+ handlePoints(proj) {
11310
+ const r = this.rect(proj);
11311
+ return r ? [[r.x1, r.y1], [r.x2, r.y2]] : [];
11312
+ }
11313
+ hitHandle(px, py, proj, tol) {
11314
+ return handleAt(px, py, this.handlePoints(proj), tol + 3);
11315
+ }
11316
+ bounds(proj) {
11317
+ const r = this.rect(proj);
11318
+ if (!r) return null;
11319
+ return { x: Math.min(r.x1, r.x2), y: Math.min(r.y1, r.y2), w: Math.abs(r.x2 - r.x1), h: Math.abs(r.y2 - r.y1) };
11320
+ }
11321
+ priceRange() {
11322
+ const a = this.anchors[0];
11323
+ const b = this.anchors[1];
11324
+ if (!a || !b) return null;
11325
+ return { min: Math.min(a.price, b.price), max: Math.max(a.price, b.price) };
11326
+ }
11327
+ schema() {
11328
+ return {
11329
+ fields: [
11330
+ {
11331
+ path: "magnifier.timeframe",
11332
+ label: "Timeframe",
11333
+ kind: "select",
11334
+ options: MAGNIFIER_TIMEFRAME_OPTIONS,
11335
+ group: "behavior"
11336
+ },
11337
+ ...LINE_FIELDS.map((f) => ({ ...f, label: f.label.replace("Line", "Border") })),
11338
+ { path: "magnifier.upColor", label: "Up candles", kind: "color", group: "fill" },
11339
+ { path: "magnifier.downColor", label: "Down candles", kind: "color", group: "fill" }
11340
+ ]
11341
+ };
11342
+ }
11343
+ writeProps() {
11344
+ return { ...this.magnifier };
11345
+ }
11346
+ readProps(props) {
11347
+ this.magnifier = { ...defaultMagnifierStyle(), ...props };
11348
+ }
11349
+ };
11350
+
11244
11351
  // src/core/drawings/registry.ts
11245
11352
  var REGISTRY = /* @__PURE__ */ new Map();
11246
11353
  function registerDrawingType(meta) {
@@ -11900,6 +12007,22 @@ registerDrawingType({
11900
12007
  defaultStyle: { lineColor: DEFAULT_DRAWING_COLOR, lineWidth: 1, lineStyle: "solid" },
11901
12008
  create: (init) => new PositionTool(init)
11902
12009
  });
12010
+ var MAGNIFIER_ICON = svg24(
12011
+ '<circle cx="10.5" cy="10.5" r="6.5"/><path d="m15.3 15.3 5.2 5.2"/><path d="M8 12.5v-3M10.5 13.5v-5.5M13 12v-2"/>'
12012
+ );
12013
+ registerDrawingType({
12014
+ type: "magnifier",
12015
+ group: "measure",
12016
+ label: "Magnifier",
12017
+ icon: MAGNIFIER_ICON,
12018
+ // An empty border color means the THEME's contrast ink (white on dark, black on
12019
+ // light), resolved at paint time so it follows theme switches; a user pick wins.
12020
+ defaultStyle: { lineColor: "", lineWidth: 1, lineStyle: "solid" },
12021
+ coversSeries: true,
12022
+ // the inset's backdrop must sit over the base candles it replaces
12023
+ placementHint: "Drag an area on the chart to view it at a lower timeframe",
12024
+ create: (init) => new Magnifier(init)
12025
+ });
11903
12026
  var VWAP_ICON = svg24('<line x1="5" y1="3" x2="5" y2="21"/><path d="M5 16c4 0 5-9 8-9s3 5 8 3"/>');
11904
12027
  registerDrawingType({
11905
12028
  type: "anchoredvwap",
@@ -16133,7 +16256,7 @@ var GEOMETRY_TYPES = ["dedekind", "sonic", "supersonic", "goldensonic", "goldens
16133
16256
  var PATTERN_TYPES = ["xabcd", "abcd", "headshoulders"];
16134
16257
  var ELLIOTT_TYPES = ["elliottimpulse", "elliottcorrection"];
16135
16258
  var HARMONIC_TYPES = ["gartley", "bat", "butterfly", "crab", "shark", "cypher"];
16136
- var MEASUREMENT_TYPES = ["position", "datepricerange"];
16259
+ var MEASUREMENT_TYPES = ["position", "datepricerange", "magnifier"];
16137
16260
  var VOLUME_TYPES = ["anchoredvwap", "fixedrangevp"];
16138
16261
  var BRUSH_TYPES = ["freehand", "highlighter"];
16139
16262
  var ARROW_TYPES = ["arrow", "arrowmarkup", "arrowmarkdown"];
@@ -16269,7 +16392,7 @@ function buildToolbar(option) {
16269
16392
 
16270
16393
  // src/core/drawings/DrawingController.ts
16271
16394
  var DrawingController = class {
16272
- constructor(renderer, events, option) {
16395
+ constructor(renderer, events, option, seriesGateway) {
16273
16396
  this.events = events;
16274
16397
  this.store = new DrawingStore();
16275
16398
  this.history = new DrawingHistory();
@@ -16295,6 +16418,7 @@ var DrawingController = class {
16295
16418
  const { definition, visible } = buildToolbar(option);
16296
16419
  this.port.setToolbar(definition);
16297
16420
  this.port.showToolbar(visible);
16421
+ if (seriesGateway) this.port.setSeriesGateway?.(seriesGateway);
16298
16422
  this.subs.push(this.port.onDrawingIntent((i) => this.onIntent(i)));
16299
16423
  this.subs.push(this.store.onChange(() => this.sync()));
16300
16424
  }
@@ -16400,7 +16524,7 @@ var DrawingController = class {
16400
16524
  style,
16401
16525
  text: init.text,
16402
16526
  props: init.props,
16403
- zIndex: init.zIndex ?? this.startZ(init.paneId ?? "price")
16527
+ zIndex: init.zIndex ?? this.startZ(type, init.paneId ?? "price")
16404
16528
  });
16405
16529
  if (!d) return null;
16406
16530
  this.history.record(this.store.serialize());
@@ -16475,10 +16599,14 @@ var DrawingController = class {
16475
16599
  * (falling back to just under the pane's top series where there is no price — a study
16476
16600
  * pane). Half a key down never ties a series; drawings tying each other paint in insertion
16477
16601
  * order, so consecutive new drawings still stack newest-in-front. Undefined without a
16478
- * shared z space — the store then places it over the other drawings, its own layer's top. */
16479
- startZ(paneId) {
16602
+ * shared z space — the store then places it over the other drawings, its own layer's top.
16603
+ * A type that COVERS the series (an opaque inset, `coversSeries`) instead starts just
16604
+ * above the whole stack — under the candles its content would be buried. */
16605
+ startZ(type, paneId) {
16480
16606
  const range = this.port?.stackRange?.(paneId);
16481
- return range ? (range.price ?? range.front) - 0.5 : void 0;
16607
+ if (!range) return void 0;
16608
+ if (getDrawingType(type)?.coversSeries) return range.front + 0.5;
16609
+ return (range.price ?? range.front) - 0.5;
16482
16610
  }
16483
16611
  /** Programmatically select drawings (host UI → chart): shows the on-chart handles + toolbar.
16484
16612
  * `additive` toggles membership (matching shift-click) instead of replacing. */
@@ -16623,7 +16751,7 @@ var DrawingController = class {
16623
16751
  const style = last ? { ...i.doc.style, ...last } : i.doc.style;
16624
16752
  const d = deserializeDrawing({ ...i.doc, id: this.store.nextId(), style });
16625
16753
  if (!d) return;
16626
- if (!d.zIndex) d.zIndex = this.startZ(d.paneId) ?? 0;
16754
+ if (!d.zIndex) d.zIndex = this.startZ(d.type, d.paneId) ?? 0;
16627
16755
  this.history.record(before);
16628
16756
  this.store.add(d);
16629
16757
  this.captureStyle(d.id);
@@ -16880,11 +17008,12 @@ async function resolveIndicators(config, fetchImpl = fetch) {
16880
17008
  }
16881
17009
  return out;
16882
17010
  }
17011
+ var ledgerEntryName = (e) => typeof e === "string" ? e : e.name;
16883
17012
  function indicatorLedger(i) {
16884
17013
  const natives = [...i.present];
16885
17014
  if (i.volumePending && !natives.includes("volume")) natives.push("volume");
16886
17015
  return {
16887
- manifest: i.manifestSettled ? [...i.instanceNames] : [...i.pendingManifest ?? i.instanceNames],
17016
+ manifest: i.manifestSettled ? [...i.instanceEntries] : [...i.pendingManifest ?? i.instanceEntries],
16888
17017
  natives
16889
17018
  };
16890
17019
  }
@@ -17818,7 +17947,17 @@ function sanitizeCell(raw) {
17818
17947
  if (c.drawings != null && typeof c.drawings === "object") out.drawings = c.drawings;
17819
17948
  const ind = c.indicators;
17820
17949
  if (ind != null && typeof ind === "object") {
17821
- const manifest = Array.isArray(ind.manifest) ? ind.manifest.filter((n) => typeof n === "string") : [];
17950
+ const manifest = Array.isArray(ind.manifest) ? ind.manifest.flatMap((n) => {
17951
+ if (typeof n === "string") return [n];
17952
+ if (n != null && typeof n === "object" && typeof n.name === "string") {
17953
+ const e = n;
17954
+ const bag = (v) => v != null && typeof v === "object" && !Array.isArray(v) ? v : void 0;
17955
+ const inputs = bag(e.inputs);
17956
+ const props = bag(e.props);
17957
+ return [inputs || props ? { name: e.name, ...inputs ? { inputs } : {}, ...props ? { props } : {} } : e.name];
17958
+ }
17959
+ return [];
17960
+ }) : [];
17822
17961
  const natives = Array.isArray(ind.natives) ? ind.natives.filter((n) => typeof n === "string") : [];
17823
17962
  out.indicators = { manifest, natives };
17824
17963
  }
@@ -18003,6 +18142,12 @@ var IndicatorHandleImpl = class {
18003
18142
  get visible() {
18004
18143
  return this.visibleState;
18005
18144
  }
18145
+ inputValues() {
18146
+ return this.controller.inputValuesOf(this.id);
18147
+ }
18148
+ propValues() {
18149
+ return this.controller.propValuesOf(this.id);
18150
+ }
18006
18151
  setInput(key, value) {
18007
18152
  this.controller.applyInputs(this.id, { [key]: value });
18008
18153
  }
@@ -18124,6 +18269,180 @@ function presetToRange(preset, bars) {
18124
18269
  return { from: Math.max(first, from), to: last };
18125
18270
  }
18126
18271
 
18272
+ // src/core/engine/DrawingSeriesService.ts
18273
+ var MAX_BARS = 5e3;
18274
+ var PAD_FRAC = 0.25;
18275
+ var MAX_ENTRIES = 16;
18276
+ var RETRY_MS = 15e3;
18277
+ var AUTO_STEPS = ["240", "60", "30", "15", "5", "1"];
18278
+ var DrawingSeriesService = class {
18279
+ constructor(deps) {
18280
+ this.deps = deps;
18281
+ /** Cached windows per `market|timeframe` key, newest-used last (LRU across keys). */
18282
+ this.cache = /* @__PURE__ */ new Map();
18283
+ this.listeners = /* @__PURE__ */ new Set();
18284
+ }
18285
+ seriesInRange(timeframe, from, to) {
18286
+ if (!this.deps.canFetch()) return { state: "unavailable", reason: "no-source" };
18287
+ const resolved = this.resolveTimeframe(timeframe);
18288
+ if (typeof resolved !== "string") return { state: "unavailable", reason: resolved.reason };
18289
+ const barMs = timeframeToMs(resolved);
18290
+ const lo = Math.min(from, to);
18291
+ const hi = Math.max(from, to);
18292
+ if (!(hi > lo) || !(barMs > 0)) return { state: "unavailable", reason: "not-lower" };
18293
+ if ((hi - lo) / barMs > MAX_BARS) return { state: "unavailable", reason: "too-wide" };
18294
+ const key = `${this.deps.marketKey()}|${resolved}`;
18295
+ const entries = this.cache.get(key) ?? [];
18296
+ const covering = entries.find((e) => e.from <= lo && e.to >= hi);
18297
+ if (covering) {
18298
+ if (covering.pending) return this.loading(entries, resolved, barMs, lo, hi);
18299
+ if (covering.failedAt > 0) {
18300
+ if (Date.now() - covering.failedAt < RETRY_MS) return this.loading(entries, resolved, barMs, lo, hi);
18301
+ entries.splice(entries.indexOf(covering), 1);
18302
+ } else {
18303
+ this.maybeRefresh(key, covering, resolved, barMs, hi);
18304
+ return { state: "ready", bars: this.slice(covering.bars, lo, hi), timeframe: resolved, barMs };
18305
+ }
18306
+ }
18307
+ this.fetchWindow(key, entries, resolved, lo, hi);
18308
+ return this.loading(entries, resolved, barMs, lo, hi);
18309
+ }
18310
+ onUpdate(listener) {
18311
+ this.listeners.add(listener);
18312
+ return () => this.listeners.delete(listener);
18313
+ }
18314
+ // ── internals ──
18315
+ /** `'auto'` → the largest standard step at least 4× finer than the chart (else the finest
18316
+ * step still below it); an explicit timeframe passes only when strictly finer. Failures
18317
+ * distinguish "this pick isn't lower" from "NOTHING lower exists" (the chart is already
18318
+ * at the finest offered step) so the consumer can word its notice honestly. */
18319
+ resolveTimeframe(timeframe) {
18320
+ const chartMs = timeframeToMs(this.deps.chartTimeframe());
18321
+ const finest = AUTO_STEPS[AUTO_STEPS.length - 1];
18322
+ if (timeframeToMs(finest) >= chartMs) return { reason: "none-lower" };
18323
+ const tf = timeframe.trim() || "auto";
18324
+ if (tf === "auto") {
18325
+ for (const step of AUTO_STEPS) {
18326
+ if (timeframeToMs(step) <= chartMs / 4) return step;
18327
+ }
18328
+ return finest;
18329
+ }
18330
+ return timeframeToMs(tf) < chartMs ? tf : { reason: "not-lower" };
18331
+ }
18332
+ /** The `loading` answer, carrying best-effort PARTIAL bars from settled overlapping
18333
+ * windows — a widened window keeps painting what it already has while it fetches. */
18334
+ loading(entries, timeframe, barMs, lo, hi) {
18335
+ const partial = /* @__PURE__ */ new Map();
18336
+ for (const e of entries) {
18337
+ if (e.pending || e.failedAt > 0) continue;
18338
+ if (e.to < lo || e.from > hi) continue;
18339
+ for (const b of this.slice(e.bars, lo, hi)) partial.set(b.time, b);
18340
+ }
18341
+ if (partial.size === 0) return { state: "loading", timeframe, barMs };
18342
+ const bars = [...partial.values()].sort((a, b) => a.time - b.time);
18343
+ return { state: "loading", timeframe, barMs, bars };
18344
+ }
18345
+ /** Kick ONE background fetch for the padded window. Any OVERLAPPING in-flight fetch
18346
+ * defers this one (a corner drag repaints per pointer move — kicking a window per
18347
+ * frame would spam the provider); when it lands, the next paint re-evaluates. */
18348
+ fetchWindow(key, entries, timeframe, lo, hi) {
18349
+ if (entries.some((e) => e.pending && e.to >= lo && e.from <= hi)) return;
18350
+ const pad = (hi - lo) * PAD_FRAC;
18351
+ const entry = { from: lo - pad, to: hi + pad, bars: [], fetchedAt: 0, pending: true, failedAt: 0 };
18352
+ entries.push(entry);
18353
+ this.cache.set(key, entries);
18354
+ this.evict();
18355
+ void this.deps.fetchBars(timeframe, { from: entry.from, to: entry.to }).then((bars) => {
18356
+ entry.bars = bars;
18357
+ entry.fetchedAt = Date.now();
18358
+ entry.pending = false;
18359
+ this.absorbOverlaps(key, entry);
18360
+ this.fire();
18361
+ }).catch(() => {
18362
+ entry.pending = false;
18363
+ entry.failedAt = Date.now();
18364
+ this.fire();
18365
+ });
18366
+ }
18367
+ /** A window whose right edge reaches the newest fetched bar refreshes at most once per
18368
+ * bar interval — new closed bars ride the feed's cache, only the live tail re-fetches. */
18369
+ maybeRefresh(key, entry, timeframe, barMs, hi) {
18370
+ if (entry.pending) return;
18371
+ const lastBar = entry.bars.length > 0 ? entry.bars[entry.bars.length - 1].time : entry.from;
18372
+ if (hi < lastBar) return;
18373
+ if (Date.now() - entry.fetchedAt < barMs) return;
18374
+ entry.pending = true;
18375
+ void this.deps.fetchBars(timeframe, { from: entry.from, to: entry.to }).then((bars) => {
18376
+ entry.bars = bars;
18377
+ entry.fetchedAt = Date.now();
18378
+ entry.pending = false;
18379
+ this.fire();
18380
+ }).catch(() => {
18381
+ entry.pending = false;
18382
+ entry.fetchedAt = Date.now();
18383
+ });
18384
+ }
18385
+ /** Merge windows that overlap `entry` into it (dedupe by bar time) so a key's list
18386
+ * converges instead of accumulating slivers. */
18387
+ absorbOverlaps(key, entry) {
18388
+ const entries = this.cache.get(key);
18389
+ if (!entries) return;
18390
+ for (let i = entries.length - 1; i >= 0; i -= 1) {
18391
+ const other = entries[i];
18392
+ if (other === entry || other.pending || other.failedAt > 0) continue;
18393
+ if (other.to < entry.from || other.from > entry.to) continue;
18394
+ const byTime = /* @__PURE__ */ new Map();
18395
+ for (const b of other.bars) byTime.set(b.time, b);
18396
+ for (const b of entry.bars) byTime.set(b.time, b);
18397
+ entry.bars = [...byTime.values()].sort((a, b) => a.time - b.time);
18398
+ entry.from = Math.min(entry.from, other.from);
18399
+ entry.to = Math.max(entry.to, other.to);
18400
+ entry.fetchedAt = Math.min(entry.fetchedAt, other.fetchedAt || entry.fetchedAt);
18401
+ entries.splice(i, 1);
18402
+ }
18403
+ }
18404
+ /** Drop the oldest settled windows once the global count passes {@link MAX_ENTRIES}. */
18405
+ evict() {
18406
+ let total = 0;
18407
+ for (const entries of this.cache.values()) total += entries.length;
18408
+ while (total > MAX_ENTRIES) {
18409
+ let oldestKey = null;
18410
+ let oldestIdx = -1;
18411
+ let oldestAt = Infinity;
18412
+ for (const [key, entries2] of this.cache) {
18413
+ for (let i = 0; i < entries2.length; i += 1) {
18414
+ const e = entries2[i];
18415
+ if (e.pending) continue;
18416
+ const at = e.fetchedAt || e.failedAt;
18417
+ if (at < oldestAt) {
18418
+ oldestKey = key;
18419
+ oldestIdx = i;
18420
+ oldestAt = at;
18421
+ }
18422
+ }
18423
+ }
18424
+ if (oldestKey == null) return;
18425
+ const entries = this.cache.get(oldestKey);
18426
+ entries.splice(oldestIdx, 1);
18427
+ if (entries.length === 0) this.cache.delete(oldestKey);
18428
+ total -= 1;
18429
+ }
18430
+ }
18431
+ /** Bars whose open time falls within `[lo, hi]` (ascending input → linear scan is fine). */
18432
+ slice(bars, lo, hi) {
18433
+ const out = [];
18434
+ for (const b of bars) {
18435
+ if (b.time < lo) continue;
18436
+ if (b.time > hi) break;
18437
+ out.push(b);
18438
+ }
18439
+ return out;
18440
+ }
18441
+ fire() {
18442
+ for (const l of [...this.listeners]) l();
18443
+ }
18444
+ };
18445
+
18127
18446
  // src/core/price-styles/BarTransform.ts
18128
18447
  function barTransformFor(style) {
18129
18448
  return chartType(style)?.barTransform ?? null;
@@ -18142,6 +18461,7 @@ var RUN_EMIT_THROTTLE_MS = 1e3;
18142
18461
  var PREVIEW_BARS = 300;
18143
18462
  var SINGLE_LOAD_BARS = 5e3;
18144
18463
  var CHUNK_BARS = 1e4;
18464
+ var FIRST_PAINT_BARS = 100;
18145
18465
  var GAP_FACTOR = 1.5;
18146
18466
  var HEAL_COOLDOWN_MS = 5e3;
18147
18467
  var EngineOrchestrator = class _EngineOrchestrator {
@@ -18259,7 +18579,13 @@ var EngineOrchestrator = class _EngineOrchestrator {
18259
18579
  const initialStyle = this.renderer.readFeature("priceStyle");
18260
18580
  if (typeof initialStyle === "string") this.priceStyle = initialStyle;
18261
18581
  this.barTransform = barTransformFor(initialStyle);
18262
- this.drawings = new DrawingController(this.renderer, this.events, config.drawings);
18582
+ const drawingSeries = new DrawingSeriesService({
18583
+ fetchBars: (tf, range) => this.fetchSeries(this.config.market.symbol ?? "", tf, range),
18584
+ canFetch: () => !!this.feed.loadRange && !this.config.market.data?.length && !!this.config.market.symbol,
18585
+ chartTimeframe: () => this.config.market.timeframe ?? "60",
18586
+ marketKey: () => `${this.config.market.symbol ?? ""}|${this.config.market.session ?? ""}`
18587
+ });
18588
+ this.drawings = new DrawingController(this.renderer, this.events, config.drawings, drawingSeries);
18263
18589
  this.unresolvedUnsub = this.feed.onUnresolved?.((info) => {
18264
18590
  this.endLoad();
18265
18591
  this.events.emit("data:unresolved", info);
@@ -18411,6 +18737,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
18411
18737
  let painted = false;
18412
18738
  const paint = (bars, final) => {
18413
18739
  if (this.generation !== gen || !final && bars.length === 0) return;
18740
+ if (!painted && !final && bars.length < Math.min(requested, FIRST_PAINT_BARS)) return;
18414
18741
  this.setBarSeries(bars, painted ? { preserveView: true } : void 0);
18415
18742
  if (!painted && bars.length > 0) {
18416
18743
  painted = true;
@@ -19041,6 +19368,15 @@ var EngineOrchestrator = class _EngineOrchestrator {
19041
19368
  record.pendingCause = "inputs";
19042
19369
  if (record.session) record.session.update(record.inputValues);
19043
19370
  else if (record.native && !record.hidden) record.native.instance.setInputs(record.inputValues);
19371
+ this.events.emit("indicator:inputs", { id });
19372
+ }
19373
+ /** IndicatorController: the CURRENT stored input values (defaults merged with edits). */
19374
+ inputValuesOf(id) {
19375
+ return { ...this.registry.get(id)?.inputValues };
19376
+ }
19377
+ /** IndicatorController: the CURRENT declaration-prop overrides. */
19378
+ propValuesOf(id) {
19379
+ return { ...this.registry.get(id)?.propValues };
19044
19380
  }
19045
19381
  /** IndicatorController: re-run an indicator with merged declaration-prop overrides.
19046
19382
  * Same lifecycle as {@link applyInputs} — a prop change replays the whole script.
@@ -19054,6 +19390,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
19054
19390
  if (!record.hidden) this.setLoading(record, true);
19055
19391
  record.pendingCause = "inputs";
19056
19392
  record.session.update(record.inputValues, record.propValues);
19393
+ this.events.emit("indicator:inputs", { id });
19057
19394
  }
19058
19395
  /** IndicatorController: tear down an indicator and (if now empty) its pane. */
19059
19396
  /** Live handles of every indicator on the chart (script + native), insertion order. */
@@ -19241,8 +19578,8 @@ var EngineOrchestrator = class _EngineOrchestrator {
19241
19578
  onModel: (model) => {
19242
19579
  const first = !record.announced;
19243
19580
  const cause = record.pendingCause ?? "history";
19581
+ if (!this.applyModel(id, model)) return;
19244
19582
  record.pendingCause = void 0;
19245
- this.applyModel(id, model);
19246
19583
  this.emitContextChanged(id);
19247
19584
  this.emitScriptRun(id, cause, first);
19248
19585
  },
@@ -19535,10 +19872,16 @@ var EngineOrchestrator = class _EngineOrchestrator {
19535
19872
  * Apply an emitted model. First emission mounts (and routes the pane); a pending
19536
19873
  * structural change (after an input edit) remounts idempotently; everything else
19537
19874
  * (live tick / viewport re-run) value-patches.
19875
+ *
19876
+ * Returns false when the model was DEFERRED — an output-free model arriving while
19877
+ * the record is still loading and the chart has no bars (see below); every other
19878
+ * outcome, including the hidden drop, returns true so the caller's event semantics
19879
+ * stay unchanged.
19538
19880
  */
19539
19881
  applyModel(id, model) {
19540
19882
  const record = this.registry.get(id);
19541
- if (!record || record.hidden) return;
19883
+ if (!record || record.hidden) return true;
19884
+ if (record.loading && this.bars.length === 0 && !_EngineOrchestrator.modelHasOutput(model)) return false;
19542
19885
  const handle = this.handles.get(id);
19543
19886
  if (!record.renderHandle) {
19544
19887
  const paneId2 = this.routePane(id, model, record.options ?? {});
@@ -19548,7 +19891,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
19548
19891
  record.renderHandle = this.renderer.mountIndicator(model);
19549
19892
  record.pendingStructural = false;
19550
19893
  this.announce(record, handle);
19551
- return;
19894
+ return true;
19552
19895
  }
19553
19896
  let paneId = record.model?.paneId ?? "price";
19554
19897
  const prevOwnScale = record.model?.ownScale === true;
@@ -19576,6 +19919,11 @@ var EngineOrchestrator = class _EngineOrchestrator {
19576
19919
  }
19577
19920
  if (record.loading) this.setLoading(record, false);
19578
19921
  this.announce(record, handle);
19922
+ return true;
19923
+ }
19924
+ /** True when the model carries ANY executed output — series, drawings, bar colors, or trades. */
19925
+ static modelHasOutput(model) {
19926
+ return model.series.length > 0 || model.fills.length > 0 || model.backgrounds.length > 0 || model.priceLines.length > 0 || (model.lines?.length ?? 0) > 0 || (model.boxes?.length ?? 0) > 0 || (model.labels?.length ?? 0) > 0 || (model.polylines?.length ?? 0) > 0 || (model.linefills?.length ?? 0) > 0 || (model.tables?.length ?? 0) > 0 || (model.barColors?.length ?? 0) > 0 || (model.trades?.length ?? 0) > 0;
19579
19927
  }
19580
19928
  routePane(id, model, options) {
19581
19929
  if (options.pane === "new") return `pane-${id}`;
@@ -20275,6 +20623,14 @@ function inputVisible(when, values) {
20275
20623
  const conds = Array.isArray(when) ? when : [when];
20276
20624
  return conds.every((c) => c.anyOf ? c.anyOf.some((x) => x === values[c.key]) : values[c.key] === c.equals);
20277
20625
  }
20626
+ function inputDeltas(schema, values) {
20627
+ const out = {};
20628
+ for (const s of schema) {
20629
+ const v = values[s.key];
20630
+ if (v !== void 0 && JSON.stringify(v) !== JSON.stringify(s.defval)) out[s.key] = v;
20631
+ }
20632
+ return Object.keys(out).length > 0 ? out : void 0;
20633
+ }
20278
20634
 
20279
20635
  // src/renderers/shared/IndicatorInputsDialog.ts
20280
20636
  var PROPS_TAB = "Properties";
@@ -23862,6 +24218,7 @@ var DOUBLE_TAP_MS = 350;
23862
24218
  var DOUBLE_TAP_SLOP = 30;
23863
24219
  var TIME_SCALE_K = 4e-3;
23864
24220
  var WHEEL_ZOOM_K = 4e-3;
24221
+ var WHEEL_PRICE_DRAG_PX = 0.25;
23865
24222
  function wheelZoomAnchor(coords, cursorX, rightEdge) {
23866
24223
  if (rightEdge) return { logical: coords.rightEdgeLogical, x: coords.width };
23867
24224
  return { logical: coords.xToLogical(cursorX), x: cursorX };
@@ -23992,7 +24349,7 @@ var InputController = class {
23992
24349
  this.capture(e.pointerId);
23993
24350
  return;
23994
24351
  }
23995
- if (e.shiftKey && this.regionAt(x, y) === "data" && this.deps.drawingsMeasureStart?.(x, y)) {
24352
+ if (e.shiftKey && this.regionAt(x, y) === "data" && this.deps.drawingsMeasureStart?.(x, y, this.snapMode(e))) {
23996
24353
  this.region = "drawing";
23997
24354
  this.capture(e.pointerId);
23998
24355
  return;
@@ -24106,7 +24463,7 @@ var InputController = class {
24106
24463
  const wasTouch = e.pointerType === "touch";
24107
24464
  const tapRelease = this.dragging && !this.moved && (!wasTouch || Math.hypot(x - this.startX, y - this.startY) <= TOUCH_TAP_SLOP);
24108
24465
  if (this.dragging && this.region === "drawing") {
24109
- this.deps.drawingsPointerUp?.(x, y);
24466
+ this.deps.drawingsPointerUp?.(x, y, this.snapMode(e));
24110
24467
  } else if (tapRelease && this.region === "data") {
24111
24468
  this.deps.onClick(x, y);
24112
24469
  } else if (this.dragging && this.region === "data") {
@@ -24127,7 +24484,7 @@ var InputController = class {
24127
24484
  if (e.pointerType === "touch") this.touches.delete(e.pointerId);
24128
24485
  this.cancelLongPress();
24129
24486
  if (!this.dragging) return;
24130
- if (this.region === "drawing" && !Number.isNaN(this.cursorX)) this.deps.drawingsPointerUp?.(this.cursorX, this.cursorY);
24487
+ if (this.region === "drawing" && !Number.isNaN(this.cursorX)) this.deps.drawingsPointerUp?.(this.cursorX, this.cursorY, this.snapMode(e));
24131
24488
  if (this.region === "crosshair" || e.pointerType === "touch") this.deps.onPointerMove(null, null);
24132
24489
  this.endGesture(e);
24133
24490
  };
@@ -24143,6 +24500,12 @@ var InputController = class {
24143
24500
  this.onWheel = (e) => {
24144
24501
  e.preventDefault();
24145
24502
  this.deps.drawingsClearTransient?.();
24503
+ const { x, y } = this.local(e);
24504
+ if (this.regionAt(x, y) === "price" && e.deltaY !== 0) {
24505
+ this.deps.beginPriceScale(x, y);
24506
+ this.deps.priceScaleBy(e.deltaY * WHEEL_PRICE_DRAG_PX);
24507
+ return;
24508
+ }
24146
24509
  const coords = this.deps.getCoords();
24147
24510
  const vp = coords.getViewport();
24148
24511
  const pan = wheelPanDelta(e.deltaX, e.deltaY, e.shiftKey);
@@ -24150,9 +24513,8 @@ var InputController = class {
24150
24513
  this.deps.apply({ barSpacing: vp.barSpacing, rightOffset: wheelPanRightOffset(vp.rightOffset, pan, coords.pxPerBar()) });
24151
24514
  return;
24152
24515
  }
24153
- const cursorX = this.local(e).x;
24154
24516
  const rightEdge = this.rightEdgeZoom && !(e.ctrlKey || e.metaKey);
24155
- const anchor = wheelZoomAnchor(coords, cursorX, rightEdge);
24517
+ const anchor = wheelZoomAnchor(coords, x, rightEdge);
24156
24518
  const target = clampBarSpacing(vp.barSpacing * Math.exp(-e.deltaY * WHEEL_ZOOM_K));
24157
24519
  this.deps.zoomTo(target, anchor.logical, anchor.x);
24158
24520
  };
@@ -25634,6 +25996,15 @@ function drawingSetEmpty(s) {
25634
25996
  function fontSizePx(size) {
25635
25997
  return size === "auto" ? 12 : namedFontSize(size);
25636
25998
  }
25999
+ function lineCoversWindow(a, b, extend, lo, hi) {
26000
+ const minX = Math.min(a, b);
26001
+ const maxX = Math.max(a, b);
26002
+ if (a === b) return a >= lo && a <= hi;
26003
+ if (extend === "both") return true;
26004
+ if (extend === "left") return maxX >= lo;
26005
+ if (extend === "right") return minX <= hi;
26006
+ return maxX >= lo && minX <= hi;
26007
+ }
25637
26008
  var DrawingSceneRenderer = class {
25638
26009
  constructor(deps, set = EMPTY_DRAWING_SET) {
25639
26010
  this.deps = deps;
@@ -25687,7 +26058,9 @@ var DrawingSceneRenderer = class {
25687
26058
  const lo = Math.min(from, to);
25688
26059
  const hi = Math.max(from, to);
25689
26060
  const visible = (a, b, extend) => {
25690
- if (extend !== "none") return true;
26061
+ if (extend === "both") return true;
26062
+ if (extend === "left") return Math.max(a, b) >= lo;
26063
+ if (extend === "right") return Math.min(a, b) <= hi;
25691
26064
  return Math.max(a, b) >= lo && Math.min(a, b) <= hi;
25692
26065
  };
25693
26066
  let min = Infinity;
@@ -25698,7 +26071,7 @@ var DrawingSceneRenderer = class {
25698
26071
  };
25699
26072
  for (const ln of this.set.lines) {
25700
26073
  if (ln.invisible) continue;
25701
- if (!visible(this.logicalOf(ln.xloc, ln.x1), this.logicalOf(ln.xloc, ln.x2), ln.extend)) continue;
26074
+ if (!lineCoversWindow(this.logicalOf(ln.xloc, ln.x1), this.logicalOf(ln.xloc, ln.x2), ln.extend, lo, hi)) continue;
25702
26075
  fold(ln.y1);
25703
26076
  fold(ln.y2);
25704
26077
  }
@@ -28161,6 +28534,17 @@ var DrawingPainter = class {
28161
28534
  constructor() {
28162
28535
  /** The current `paintAll` call's interaction state, visible to the per-type painters. */
28163
28536
  this.targets = {};
28537
+ /** The chart's active series LOOK — style + resolved series colors — pushed by the
28538
+ * controller before each paint. The magnifier's inset mirrors both: candles/bars/line/
28539
+ * area restyle the paint (bar-transform styles like Heikin Ashi transform the fetched
28540
+ * bars; unknown/custom styles fall back to candles), and the colors default to the main
28541
+ * series' own so the inset reads as a finer copy of the chart. */
28542
+ this.seriesLook = {
28543
+ style: "candles",
28544
+ upColor: BULLISH,
28545
+ downColor: BEARISH,
28546
+ lineColor: BULLISH
28547
+ };
28164
28548
  }
28165
28549
  /** Paint every visible drawing, then selection handles for the targeted ones.
28166
28550
  * Each drawing is clipped to its own pane's rect (and skipped entirely while that pane
@@ -28205,6 +28589,8 @@ var DrawingPainter = class {
28205
28589
  ctx.globalAlpha = GHOST_ALPHA;
28206
28590
  if (ghost instanceof RegressionChannel || ghost instanceof FixedRangeVolumeProfile) {
28207
28591
  this.paintTimeSpanGhost(ctx, ghost, proj);
28592
+ } else if (ghost instanceof Magnifier) {
28593
+ this.paintMagnifierGhost(ctx, ghost, proj, theme);
28208
28594
  } else this.paintOne(ctx, ghost, proj, theme);
28209
28595
  ctx.globalAlpha = 1;
28210
28596
  }
@@ -28338,6 +28724,10 @@ var DrawingPainter = class {
28338
28724
  this.paintLabel(ctx, d, proj, theme);
28339
28725
  return;
28340
28726
  }
28727
+ if (d instanceof Magnifier) {
28728
+ this.paintMagnifier(ctx, d, proj, theme);
28729
+ return;
28730
+ }
28341
28731
  if (d instanceof PatternDrawing) {
28342
28732
  this.paintPattern(ctx, d, proj, theme);
28343
28733
  return;
@@ -28830,6 +29220,216 @@ var DrawingPainter = class {
28830
29220
  ctx.textBaseline = "alphabetic";
28831
29221
  }
28832
29222
  }
29223
+ /** Placement preview for the magnifier: a dashed rectangle outline only — no backdrop and
29224
+ * no series read, so dragging the area open never kicks a fetch per cursor move. */
29225
+ paintMagnifierGhost(ctx, d, proj, theme) {
29226
+ const r = d.rect(proj);
29227
+ if (!r) return;
29228
+ ctx.save();
29229
+ ctx.strokeStyle = d.style.lineColor || contrastColor(theme.background);
29230
+ ctx.lineWidth = 1;
29231
+ ctx.setLineDash([4, 4]);
29232
+ ctx.strokeRect(Math.min(r.x1, r.x2), Math.min(r.y1, r.y2), Math.abs(r.x2 - r.x1), Math.abs(r.y2 - r.y1));
29233
+ ctx.restore();
29234
+ }
29235
+ /** Paint a magnifier: an opaque theme-background inset whose interior shows the chart's
29236
+ * market at a finer timeframe — candles at their true time/price positions, clipped to
29237
+ * the rectangle. Bars come through `Projector.seriesInRange` (cache read; `loading` and
29238
+ * `unavailable` states paint a short notice instead). The lower-timeframe candles shift
29239
+ * half a chart bar LEFT of their raw time pixel so each chart candle's visual cell —
29240
+ * centered on its open time — subdivides in place. */
29241
+ paintMagnifier(ctx, d, proj, theme) {
29242
+ const r = d.rect(proj);
29243
+ const a = d.anchors[0];
29244
+ const b = d.anchors[1];
29245
+ if (!r || !a || !b) return;
29246
+ const x0 = Math.min(r.x1, r.x2);
29247
+ const x1 = Math.max(r.x1, r.x2);
29248
+ const y0 = Math.min(r.y1, r.y2);
29249
+ const y1 = Math.max(r.y1, r.y2);
29250
+ const w = x1 - x0;
29251
+ const h = y1 - y0;
29252
+ ctx.save();
29253
+ ctx.globalAlpha = 1;
29254
+ ctx.fillStyle = theme.background;
29255
+ ctx.fillRect(x0, y0, w, h);
29256
+ ctx.restore();
29257
+ const from = Math.min(a.time, b.time);
29258
+ const to = Math.max(a.time, b.time);
29259
+ const chartBars = proj.barsBetween ? proj.barsBetween(from, to) : 0;
29260
+ const chartMs = chartBars > 0 ? (to - from) / chartBars : 0;
29261
+ const res = proj.seriesInRange && chartMs > 0 && w > 1 && h > 1 ? proj.seriesInRange(d.magnifier.timeframe, from, to + chartMs) : void 0;
29262
+ let seriesBars = res?.state === "ready" || res?.state === "loading" ? res.bars ?? [] : [];
29263
+ if (res && (res.state === "ready" || res.state === "loading") && seriesBars.length > 0) {
29264
+ const look = this.seriesLook;
29265
+ const transform = look.style !== "candles" ? barTransformFor(look.style) : null;
29266
+ if (transform) seriesBars = transform.full(seriesBars);
29267
+ const mode = look.style === "bars" ? "bars" : look.style === "line" || look.style === "baseline" ? "line" : look.style === "area" ? "area" : "candles";
29268
+ const halfPitch = (proj.xOf(from + chartMs) - proj.xOf(from)) / 2;
29269
+ ctx.save();
29270
+ ctx.beginPath();
29271
+ ctx.rect(x0, y0, w, h);
29272
+ ctx.clip();
29273
+ if (mode === "line" || mode === "area") {
29274
+ this.paintMagnifierLine(ctx, d, proj, seriesBars, res.barMs, halfPitch, y1, mode === "area", d.magnifier.upColor || look.lineColor);
29275
+ } else {
29276
+ this.paintMagnifierBars(ctx, d, proj, seriesBars, res.barMs, halfPitch, x0, x1, mode, d.magnifier.upColor || look.upColor, d.magnifier.downColor || look.downColor);
29277
+ }
29278
+ ctx.restore();
29279
+ } else if (res) {
29280
+ const notice = res.state === "loading" ? "Loading\u2026" : res.state === "ready" ? "No lower-timeframe data" : res.reason === "too-wide" ? "Area too wide for this timeframe" : res.reason === "none-lower" ? "No lower timeframe available" : res.reason === "not-lower" ? "Pick a timeframe below the chart" : "No data source";
29281
+ this.paintMagnifierNotice(ctx, notice, x0, y0, w, h, theme);
29282
+ }
29283
+ ctx.save();
29284
+ ctx.strokeStyle = d.style.lineColor || contrastColor(theme.background);
29285
+ ctx.lineWidth = d.style.lineWidth || 1;
29286
+ ctx.setLineDash(dashPattern(d.style.lineStyle, d.style.lineWidth || 1));
29287
+ ctx.strokeRect(x0, y0, w, h);
29288
+ ctx.restore();
29289
+ if (w > 44) {
29290
+ const label = magnifierTimeframeLabel(res?.state === "ready" || res?.state === "loading" ? res.timeframe : d.magnifier.timeframe);
29291
+ const chipH = 17;
29292
+ const gap = 4;
29293
+ const pane = proj.paneRect?.(d.paneId);
29294
+ const paneBottom = pane ? pane.top + pane.height : proj.height;
29295
+ const below = y1 + gap + chipH <= paneBottom;
29296
+ const chipY = below ? y1 + gap : y1 - gap - chipH;
29297
+ ctx.save();
29298
+ ctx.font = `10px ${theme.fontFamily}`;
29299
+ const tw = ctx.measureText(label).width;
29300
+ const caretW = 11;
29301
+ const chipW = tw + 12 + caretW;
29302
+ roundRect(ctx, x0, chipY, chipW, chipH, 3);
29303
+ ctx.fillStyle = theme.background;
29304
+ ctx.fill();
29305
+ ctx.strokeStyle = withAlpha(theme.textColor, 0.28);
29306
+ ctx.lineWidth = 1;
29307
+ ctx.setLineDash([]);
29308
+ ctx.stroke();
29309
+ ctx.fillStyle = theme.textColor;
29310
+ ctx.textAlign = "left";
29311
+ ctx.textBaseline = "middle";
29312
+ ctx.fillText(label, x0 + 6, chipY + chipH / 2 + 0.5);
29313
+ const cxr = x0 + 6 + tw + 5;
29314
+ const cyr = chipY + chipH / 2;
29315
+ ctx.strokeStyle = withAlpha(theme.textColor, 0.7);
29316
+ ctx.lineWidth = 1.2;
29317
+ ctx.beginPath();
29318
+ ctx.moveTo(cxr, cyr - 1.5);
29319
+ ctx.lineTo(cxr + 2.5, cyr + 1.5);
29320
+ ctx.lineTo(cxr + 5, cyr - 1.5);
29321
+ ctx.stroke();
29322
+ ctx.restore();
29323
+ d.chipRect = { x: x0, y: chipY, w: chipW, h: chipH };
29324
+ } else {
29325
+ d.chipRect = null;
29326
+ }
29327
+ }
29328
+ /** The magnifier's candle/bar loop: each bar's cell spans its open→close time (shifted left
29329
+ * by half a chart bar). Candles: wick always, body once the cell is wide enough to carry
29330
+ * one. OHLC bars: the high–low spine with open/close ticks once the cell has the room. */
29331
+ paintMagnifierBars(ctx, d, proj, bars, barMs, halfPitch, x0, x1, mode, upColor, downColor) {
29332
+ ctx.setLineDash([]);
29333
+ ctx.lineWidth = 1;
29334
+ for (const bar of bars) {
29335
+ const cx0 = proj.xOf(bar.time) - halfPitch;
29336
+ const cx1 = proj.xOf(bar.time + barMs) - halfPitch;
29337
+ if (cx1 < x0 || cx0 > x1) continue;
29338
+ const yHigh = proj.yOf(bar.high, d.paneId);
29339
+ const yLow = proj.yOf(bar.low, d.paneId);
29340
+ const yOpen = proj.yOf(bar.open, d.paneId);
29341
+ const yClose = proj.yOf(bar.close, d.paneId);
29342
+ if (yHigh == null || yLow == null || yOpen == null || yClose == null) continue;
29343
+ const color = bar.close >= bar.open ? upColor : downColor;
29344
+ const cellW = cx1 - cx0;
29345
+ const cx = (cx0 + cx1) / 2;
29346
+ ctx.strokeStyle = color;
29347
+ ctx.beginPath();
29348
+ ctx.moveTo(cx, yHigh);
29349
+ ctx.lineTo(cx, yLow);
29350
+ ctx.stroke();
29351
+ if (cellW < 3) continue;
29352
+ if (mode === "bars") {
29353
+ const tick = Math.max(1, cellW * 0.35);
29354
+ ctx.beginPath();
29355
+ ctx.moveTo(cx - tick, yOpen);
29356
+ ctx.lineTo(cx, yOpen);
29357
+ ctx.moveTo(cx, yClose);
29358
+ ctx.lineTo(cx + tick, yClose);
29359
+ ctx.stroke();
29360
+ } else {
29361
+ const bw = Math.max(1, cellW * 0.7);
29362
+ ctx.fillStyle = color;
29363
+ ctx.fillRect(cx - bw / 2, Math.min(yOpen, yClose), bw, Math.max(1, Math.abs(yClose - yOpen)));
29364
+ }
29365
+ }
29366
+ }
29367
+ /** The magnifier's line/area rendering: a close polyline through each cell's center (same
29368
+ * half-chart-bar shift as the candles), with an optional translucent fill down to the
29369
+ * rectangle's bottom edge for the area style. Colored like the chart's own line series. */
29370
+ paintMagnifierLine(ctx, d, proj, bars, barMs, halfPitch, yBottom, area, color) {
29371
+ const pts = [];
29372
+ for (const bar of bars) {
29373
+ const y = proj.yOf(bar.close, d.paneId);
29374
+ if (y == null) continue;
29375
+ pts.push([proj.xOf(bar.time + barMs / 2) - halfPitch, y]);
29376
+ }
29377
+ if (pts.length < 2) return;
29378
+ if (area) {
29379
+ ctx.beginPath();
29380
+ ctx.moveTo(pts[0][0], yBottom);
29381
+ for (const [px, py] of pts) ctx.lineTo(px, py);
29382
+ ctx.lineTo(pts[pts.length - 1][0], yBottom);
29383
+ ctx.closePath();
29384
+ ctx.fillStyle = withAlpha(color, 0.15);
29385
+ ctx.fill();
29386
+ }
29387
+ ctx.setLineDash([]);
29388
+ ctx.lineWidth = 1.5;
29389
+ ctx.strokeStyle = color;
29390
+ ctx.beginPath();
29391
+ ctx.moveTo(pts[0][0], pts[0][1]);
29392
+ for (let i = 1; i < pts.length; i += 1) ctx.lineTo(pts[i][0], pts[i][1]);
29393
+ ctx.stroke();
29394
+ }
29395
+ /** Centered muted notice inside the magnifier rect (loading / unavailable states). */
29396
+ paintMagnifierNotice(ctx, text, x0, y0, w, h, theme) {
29397
+ if (w < 60 || h < 20) return;
29398
+ ctx.save();
29399
+ ctx.beginPath();
29400
+ ctx.rect(x0, y0, w, h);
29401
+ ctx.clip();
29402
+ ctx.font = `11px ${theme.fontFamily}`;
29403
+ ctx.fillStyle = withAlpha(theme.textColor, 0.55);
29404
+ ctx.textAlign = "center";
29405
+ ctx.textBaseline = "middle";
29406
+ ctx.fillText(text, x0 + w / 2, y0 + h / 2);
29407
+ ctx.restore();
29408
+ }
29409
+ /** A bottom-center pill prompting the armed tool's placement gesture (e.g. the magnifier's
29410
+ * "drag an area"). Painted by the drawings layer while the tool is armed and no placement
29411
+ * is in progress; chart-background fill so it reads as chrome over any content. */
29412
+ paintPlacementHint(ctx, text, theme, width, height) {
29413
+ ctx.save();
29414
+ ctx.font = `11px ${theme.fontFamily}`;
29415
+ const tw = ctx.measureText(text).width;
29416
+ const pillW = tw + 24;
29417
+ const pillH = 24;
29418
+ const x = (width - pillW) / 2;
29419
+ const y = height - pillH - 14;
29420
+ roundRect(ctx, x, y, pillW, pillH, pillH / 2);
29421
+ ctx.fillStyle = theme.background;
29422
+ ctx.fill();
29423
+ ctx.strokeStyle = withAlpha(theme.textColor, 0.28);
29424
+ ctx.lineWidth = 1;
29425
+ ctx.setLineDash([]);
29426
+ ctx.stroke();
29427
+ ctx.fillStyle = theme.textColor;
29428
+ ctx.textAlign = "center";
29429
+ ctx.textBaseline = "middle";
29430
+ ctx.fillText(text, width / 2, y + pillH / 2 + 0.5);
29431
+ ctx.restore();
29432
+ }
28833
29433
  /** Paint a fixed-range volume profile: horizontal histogram rows (up/down split) anchored to
28834
29434
  * the left or right of the time span, optional VAH / VAL / POC levels across the range, and
28835
29435
  * optional developing POC / VA polylines. Recomputes from the two anchors on every paint. */
@@ -29548,6 +30148,22 @@ var DrawingInteraction = class {
29548
30148
  this.snapAt = changed ? { point: snapped, paneId } : null;
29549
30149
  return snapped;
29550
30150
  }
30151
+ /**
30152
+ * Resolve a cursor pixel through the magnet and return the snapped pixel — the same
30153
+ * conversion drawing placement uses. Updates the snap-ring marker. The measure
30154
+ * ruler goes through this so its endpoints follow weak/strong/Ctrl magnet too.
30155
+ */
30156
+ snapCursor(x, y, mode) {
30157
+ const proj = this.deps.projector();
30158
+ const paneId = proj.paneIdAtY(y) ?? "price";
30159
+ const point = this.resolve(x, y, paneId, mode);
30160
+ const sy = proj.yOf(point.price, paneId);
30161
+ return { x: proj.xOf(point.time), y: sy ?? y };
30162
+ }
30163
+ /** Drop the snap-ring marker (a transient mode ended without going through `up`). */
30164
+ clearSnapMarker() {
30165
+ this.snapAt = null;
30166
+ }
29551
30167
  /** Resolve a pixel to a data point with the segment angle locked to 45° steps around
29552
30168
  * `pivot` (Shift held on a line tool). Works in PIXEL space — the user reasons about
29553
30169
  * the angle they see, not about time/price units. The magnet is bypassed: snapping
@@ -30229,8 +30845,9 @@ function ensureStyles5() {
30229
30845
  if (!existing) document.head.appendChild(s);
30230
30846
  }
30231
30847
  var DrawingSettingsPopup = class {
30232
- constructor(host, theme) {
30848
+ constructor(host, theme, chartBarMs = () => 0) {
30233
30849
  this.host = host;
30850
+ this.chartBarMs = chartBarMs;
30234
30851
  this.el = null;
30235
30852
  this.tipEl = null;
30236
30853
  // floating hover-label (above/below the toolbar)
@@ -30255,6 +30872,14 @@ var DrawingSettingsPopup = class {
30255
30872
  this.theme = theme;
30256
30873
  this.settingsDialog = new DrawingSettingsDialog(host, theme);
30257
30874
  }
30875
+ /** The magnifier timeframe choices strictly below the chart's own bar duration
30876
+ * (`auto` rides along while at least one concrete lower step exists). */
30877
+ lowerTimeframeOptions() {
30878
+ const chartMs = this.chartBarMs();
30879
+ if (!(chartMs > 0)) return [...MAGNIFIER_TIMEFRAME_OPTIONS];
30880
+ const lower = MAGNIFIER_TIMEFRAME_OPTIONS.filter((o) => o.ms > 0 && o.ms < chartMs);
30881
+ return lower.length > 0 ? [MAGNIFIER_TIMEFRAME_OPTIONS[0], ...lower] : [];
30882
+ }
30258
30883
  setTheme(theme) {
30259
30884
  this.theme = theme;
30260
30885
  this.settingsDialog.setTheme(theme);
@@ -30297,7 +30922,20 @@ var DrawingSettingsPopup = class {
30297
30922
  const sz = drawing.size ?? "normal";
30298
30923
  bar.appendChild(this.dropdown("Icon size", STAMP_SIZE_OPTIONS, sz, (s) => stampSizeIcon(s), (v) => actions.patch({ size: v }), { label: sizeLabel }));
30299
30924
  }
30300
- if (paths.has("style.lineColor")) bar.appendChild(this.colorButton("Line color", BRUSH_ICON, drawing.style.lineColor || DEFAULT_DRAWING_COLOR, (v) => actions.patch({ "style.lineColor": v })));
30925
+ if (paths.has("magnifier.timeframe") && drawing instanceof Magnifier) {
30926
+ const options = this.lowerTimeframeOptions();
30927
+ if (options.length > 0) {
30928
+ bar.appendChild(
30929
+ this.dropdown("Lower timeframe", options.map((o) => o.value), drawing.magnifier.timeframe, () => "", (v) => actions.patch({ "magnifier.timeframe": v }), {
30930
+ label: (v) => magnifierTimeframeLabel(String(v)),
30931
+ labelInTrigger: true
30932
+ })
30933
+ );
30934
+ }
30935
+ bar.appendChild(this.colorButton("Up candles", BUCKET_ICON, drawing.magnifier.upColor || t.upColor, (v) => actions.patch({ "magnifier.upColor": v })));
30936
+ bar.appendChild(this.colorButton("Down candles", BUCKET_ICON, drawing.magnifier.downColor || t.downColor, (v) => actions.patch({ "magnifier.downColor": v })));
30937
+ }
30938
+ if (paths.has("style.lineColor")) bar.appendChild(this.colorButton("Line color", BRUSH_ICON, drawing.style.lineColor || (drawing instanceof Magnifier ? contrastColor(this.theme.background) : DEFAULT_DRAWING_COLOR), (v) => actions.patch({ "style.lineColor": v })));
30301
30939
  if (paths.has("style.lineWidth")) {
30302
30940
  const wf = schema.fields.find((f) => f.path === "style.lineWidth");
30303
30941
  if (wf?.kind === "number" && (wf.min ?? 1) > 1) {
@@ -30619,6 +31257,7 @@ var DrawingSettingsPopup = class {
30619
31257
  this.colorPop = null;
30620
31258
  this.colorOwner = null;
30621
31259
  }
31260
+ opts.onClose?.();
30622
31261
  }
30623
31262
  });
30624
31263
  const el = pop.el;
@@ -30637,6 +31276,49 @@ var DrawingSettingsPopup = class {
30637
31276
  pop.show();
30638
31277
  return pop;
30639
31278
  }
31279
+ /**
31280
+ * A standalone timeframe menu for the magnifier's ON-CHART chip. The chip lives on
31281
+ * canvas, so a transient invisible anchor is dropped at its pixel rect for the popover
31282
+ * to position against, and removed again when the menu closes. Independent of the
31283
+ * quick toolbar — the chip works without selecting the drawing first.
31284
+ */
31285
+ openMagnifierTimeframeMenu(rect, current, onPick) {
31286
+ ensureStyles5();
31287
+ closeOpenPopovers();
31288
+ const options = this.lowerTimeframeOptions();
31289
+ const anchor = document.createElement("div");
31290
+ anchor.style.cssText = `position:absolute;left:${rect.x}px;top:${rect.y}px;width:${rect.w}px;height:${rect.h}px;pointer-events:none;`;
31291
+ this.host.appendChild(anchor);
31292
+ this.menuPop = this.hostFloat(anchor, {
31293
+ zIndex: 26,
31294
+ padding: "4px",
31295
+ onClose: () => anchor.remove(),
31296
+ fill: (menu2, pop) => {
31297
+ if (options.length === 0) {
31298
+ const note = document.createElement("div");
31299
+ note.style.cssText = "padding:6px 10px;opacity:0.65;white-space:nowrap;";
31300
+ note.textContent = "No lower timeframe available";
31301
+ menu2.appendChild(note);
31302
+ return;
31303
+ }
31304
+ for (const o of options) {
31305
+ const item = document.createElement("button");
31306
+ item.type = "button";
31307
+ item.className = "vela-dpop-item";
31308
+ item.dataset.active = o.value === current ? "1" : "0";
31309
+ item.style.cssText = "display:flex;align-items:center;min-width:88px;padding:5px 10px;border:none;border-radius:5px;color:inherit;cursor:pointer;text-align:left;font:inherit;font-variant-numeric:tabular-nums;";
31310
+ item.textContent = o.label;
31311
+ item.addEventListener("click", (e) => {
31312
+ e.stopPropagation();
31313
+ pop.hide();
31314
+ onPick(o.value);
31315
+ });
31316
+ menu2.appendChild(item);
31317
+ }
31318
+ }
31319
+ });
31320
+ this.menuOwner = anchor;
31321
+ }
30640
31322
  /** A floating list of one-shot actions (icon + label rows) opened by the kebab. */
30641
31323
  openActionMenu(anchor, rows) {
30642
31324
  this.menuPop = this.hostFloat(anchor, {
@@ -30747,10 +31429,13 @@ var DrawingSettingsPopup = class {
30747
31429
  let cur = current;
30748
31430
  const paint = (v) => {
30749
31431
  b.replaceChildren();
30750
- const ic = document.createElement("span");
30751
- ic.style.cssText = "display:flex;";
30752
- ic.innerHTML = sized(render(v));
30753
- b.appendChild(ic);
31432
+ const glyph = render(v);
31433
+ if (glyph) {
31434
+ const ic = document.createElement("span");
31435
+ ic.style.cssText = "display:flex;";
31436
+ ic.innerHTML = sized(glyph);
31437
+ b.appendChild(ic);
31438
+ }
30754
31439
  if (opts.label && opts.labelInTrigger) {
30755
31440
  const tx = document.createElement("span");
30756
31441
  tx.textContent = opts.label(v);
@@ -30792,10 +31477,13 @@ var DrawingSettingsPopup = class {
30792
31477
  item.className = "vela-dpop-item";
30793
31478
  item.dataset.active = active ? "1" : "0";
30794
31479
  item.style.cssText = `display:flex;align-items:center;gap:8px;${label ? "min-width:118px;" : ""}padding:5px 8px;border:none;border-radius:5px;color:inherit;cursor:pointer;text-align:left;font:inherit;`;
30795
- const ic = document.createElement("span");
30796
- ic.style.cssText = "display:flex;flex:none;width:22px;justify-content:center;";
30797
- ic.innerHTML = sized(render(v), 18);
30798
- item.appendChild(ic);
31480
+ const glyph = render(v);
31481
+ if (glyph) {
31482
+ const ic = document.createElement("span");
31483
+ ic.style.cssText = "display:flex;flex:none;width:22px;justify-content:center;";
31484
+ ic.innerHTML = sized(glyph, 18);
31485
+ item.appendChild(ic);
31486
+ }
30799
31487
  if (label) {
30800
31488
  const tx = document.createElement("span");
30801
31489
  tx.textContent = label(v);
@@ -30953,30 +31641,39 @@ var MeasureOverlay = class {
30953
31641
  isFinished() {
30954
31642
  return this.state === "finished";
30955
31643
  }
30956
- /** A press: begin the measurement, or finish it on the second click. */
30957
- down(x, y) {
31644
+ /** A press: begin the measurement, or finish it on the second click.
31645
+ * `x,y` are the raw cursor (drag-slop vs click-move-click). `gx,gy` are the
31646
+ * graphic endpoints — magnet-snapped when the magnet is on, else the same as `x,y`. */
31647
+ down(x, y, gx = x, gy = y) {
30958
31648
  if (this.state === "measuring") {
30959
- this.end = { x, y };
31649
+ this.end = { x: gx, y: gy };
30960
31650
  this.state = "finished";
30961
31651
  return;
30962
31652
  }
30963
- this.start = { x, y };
30964
- this.end = { x, y };
31653
+ this.start = { x: gx, y: gy };
31654
+ this.end = { x: gx, y: gy };
30965
31655
  this.pressX = x;
30966
31656
  this.pressY = y;
30967
31657
  this.state = "measuring";
30968
31658
  }
31659
+ /** Size the in-progress ruler. `x,y` are the graphic (magnet-snapped) cursor. */
30969
31660
  move(x, y) {
30970
31661
  if (this.state === "measuring") this.end = { x, y };
30971
31662
  }
30972
31663
  /** A release: finish if the press was actually dragged (press-drag-release), else wait
30973
- * for the second click (click-move-click). */
30974
- up(x, y) {
31664
+ * for the second click (click-move-click). `x,y` are the raw cursor (slop); `gx,gy`
31665
+ * are the graphic end (magnet-snapped when the magnet is on). */
31666
+ up(x, y, gx = x, gy = y) {
30975
31667
  if (this.state === "measuring" && Math.hypot(x - this.pressX, y - this.pressY) > DRAG_SLOP4) {
30976
- this.end = { x, y };
31668
+ this.end = { x: gx, y: gy };
30977
31669
  this.state = "finished";
30978
31670
  }
30979
31671
  }
31672
+ /** Current graphic endpoints in media pixels, or null when idle. */
31673
+ points() {
31674
+ if (!this.start || !this.end) return null;
31675
+ return { start: this.start, end: this.end };
31676
+ }
30980
31677
  clear() {
30981
31678
  this.state = "idle";
30982
31679
  this.start = null;
@@ -31138,6 +31835,9 @@ var UserDrawingController = class {
31138
31835
  this.intentCb = null;
31139
31836
  /** Another chart's in-progress placement, mirrored here as a ghost (drawings sync). */
31140
31837
  this.externalGhost = null;
31838
+ /** Core-pushed series gateway (finer-timeframe bars for data-driven drawings). */
31839
+ this.seriesGw = null;
31840
+ this.seriesGwUnsub = null;
31141
31841
  /** Last draft fingerprint reported upstream — gates the per-render emission to actual changes. */
31142
31842
  this.lastDraftKey = null;
31143
31843
  this.measure = new MeasureOverlay();
@@ -31165,7 +31865,7 @@ var UserDrawingController = class {
31165
31865
  this.textEditor = null;
31166
31866
  this.painter = new DrawingPainter();
31167
31867
  this.ctx = canvas.getContext("2d");
31168
- this.popup = new DrawingSettingsPopup(overlayHost, deps.theme());
31868
+ this.popup = new DrawingSettingsPopup(overlayHost, deps.theme(), () => deps.chartBarMs());
31169
31869
  this.toolbar = new DrawingToolbar(
31170
31870
  toolbarHost,
31171
31871
  deps.theme(),
@@ -31223,6 +31923,22 @@ var UserDrawingController = class {
31223
31923
  const shown = this.toolbarVisible && !this.mobileLayout;
31224
31924
  this.deps.setToolbarGutter(shown ? this.toolbarCollapsed ? TOOLBAR_COLLAPSED_WIDTH : TOOLBAR_WIDTH : 0);
31225
31925
  }
31926
+ /** Core push: the series gateway data-driven drawings read finer-timeframe bars
31927
+ * through (surfaced to them as `Projector.seriesInRange`). A landed background
31928
+ * fetch repaints both this layer and the interleave slices under the series. */
31929
+ setSeriesGateway(gateway) {
31930
+ this.seriesGwUnsub?.();
31931
+ this.seriesGw = gateway;
31932
+ this.seriesGwUnsub = gateway.onUpdate(() => {
31933
+ this.invalidateSlices();
31934
+ this.render();
31935
+ this.deps.requestDataPaint();
31936
+ });
31937
+ }
31938
+ /** The pushed series gateway, or null before the core provides one. */
31939
+ get seriesGateway() {
31940
+ return this.seriesGw;
31941
+ }
31226
31942
  /** Core push: mirror (or clear) another chart's in-progress placement as a ghost. */
31227
31943
  setExternalGhost(doc) {
31228
31944
  this.externalGhost = doc ? deserializeDrawing(doc) : null;
@@ -31340,8 +32056,34 @@ var UserDrawingController = class {
31340
32056
  /** Should the drawing layer win this press (vs pan)? */
31341
32057
  claim(x, y) {
31342
32058
  if (this.measureMode || this.eraserMode) return true;
32059
+ if (this.magnifierChipAt(x, y)) return true;
31343
32060
  return this.interaction.claim(x, y);
31344
32061
  }
32062
+ /** The topmost visible (unlocked) magnifier whose timeframe chip contains (x, y) —
32063
+ * the chip's rect is what the painter measured last frame. */
32064
+ magnifierChipAt(x, y) {
32065
+ for (let i = this.drawings.length - 1; i >= 0; i -= 1) {
32066
+ const d = this.drawings[i];
32067
+ if (!(d instanceof Magnifier) || !d.visible || d.locked) continue;
32068
+ const r = d.chipRect;
32069
+ if (r && x >= r.x && x <= r.x + r.w && y >= r.y && y <= r.y + r.h) return d;
32070
+ }
32071
+ return null;
32072
+ }
32073
+ /** Open the on-chart chip's timeframe menu; the pick patches the drawing like a
32074
+ * settings-popup edit (same intent, same undo step). */
32075
+ openMagnifierChipMenu(drawing) {
32076
+ const rect = drawing.chipRect;
32077
+ if (!rect) return;
32078
+ const id = drawing.id;
32079
+ this.popup.openMagnifierTimeframeMenu(rect, drawing.magnifier.timeframe, (value) => {
32080
+ const d = this.drawings.find((x) => x.id === id);
32081
+ if (!(d instanceof Magnifier)) return;
32082
+ d.applySettings({ "magnifier.timeframe": value });
32083
+ this.render();
32084
+ this.emit({ kind: "edit", doc: d.serialize() });
32085
+ });
32086
+ }
31345
32087
  /** Delete the (unlocked) drawing under the cursor. True when one was removed.
31346
32088
  * Shared by the eraser (click + drag) and the middle-click shortcut. */
31347
32089
  deleteAt(x, y) {
@@ -31353,11 +32095,13 @@ var UserDrawingController = class {
31353
32095
  }
31354
32096
  /** Shift+press on the empty plot: arm the measure ruler AND start it at (x, y) in one
31355
32097
  * gesture — the equivalent of clicking the toolbar's Measure button, then pressing.
31356
- * Returns false when a mode/tool is already active (the normal press path owns it). */
31357
- beginMeasureAt(x, y) {
32098
+ * `snap` is the effective magnet (sticky mode, or Ctrl/Cmd-forced strong). Returns
32099
+ * false when a mode/tool is already active (the normal press path owns it). */
32100
+ beginMeasureAt(x, y, snap = "off") {
31358
32101
  if (this.measureMode || this.eraserMode || this.activeTool != null) return false;
31359
32102
  this.withModeIntent(() => this.toggleMeasure());
31360
- this.measure.down(x, y);
32103
+ const g = this.interaction.snapCursor(x, y, snap);
32104
+ this.measure.down(x, y, g.x, g.y);
31361
32105
  this.render();
31362
32106
  return true;
31363
32107
  }
@@ -31379,11 +32123,19 @@ var UserDrawingController = class {
31379
32123
  return;
31380
32124
  }
31381
32125
  if (this.measureMode) {
31382
- this.measure.down(x, y);
32126
+ const g = this.interaction.snapCursor(x, y, snap);
32127
+ this.measure.down(x, y, g.x, g.y);
31383
32128
  if (this.measure.isFinished()) this.withModeIntent(() => this.exitMeasure(false));
31384
32129
  this.render();
31385
32130
  return;
31386
32131
  }
32132
+ if (this.activeTool == null) {
32133
+ const chipOwner = this.magnifierChipAt(x, y);
32134
+ if (chipOwner) {
32135
+ this.openMagnifierChipMenu(chipOwner);
32136
+ return;
32137
+ }
32138
+ }
31387
32139
  this.interaction.down(x, y, snap, shift);
31388
32140
  }
31389
32141
  pointerMove(x, y, snap = "off", shift = false) {
@@ -31392,7 +32144,8 @@ var UserDrawingController = class {
31392
32144
  return;
31393
32145
  }
31394
32146
  if (this.measureMode) {
31395
- this.measure.move(x, y);
32147
+ const g = this.interaction.snapCursor(x, y, snap);
32148
+ this.measure.move(g.x, g.y);
31396
32149
  this.render();
31397
32150
  return;
31398
32151
  }
@@ -31410,13 +32163,14 @@ var UserDrawingController = class {
31410
32163
  this.render();
31411
32164
  }
31412
32165
  }
31413
- pointerUp(x, y) {
32166
+ pointerUp(x, y, snap = "off") {
31414
32167
  if (this.eraserMode) {
31415
32168
  this.erasing = false;
31416
32169
  return;
31417
32170
  }
31418
32171
  if (this.measureMode) {
31419
- this.measure.up(x, y);
32172
+ const g = this.interaction.snapCursor(x, y, snap);
32173
+ this.measure.up(x, y, g.x, g.y);
31420
32174
  if (this.measure.isFinished()) this.withModeIntent(() => this.exitMeasure(false));
31421
32175
  this.render();
31422
32176
  return;
@@ -31461,6 +32215,7 @@ var UserDrawingController = class {
31461
32215
  /** Leave ruler mode. `clearGraphic` keeps a just-finished measurement on screen (false). */
31462
32216
  exitMeasure(clearGraphic = true) {
31463
32217
  this.measureMode = false;
32218
+ this.interaction.clearSnapMarker();
31464
32219
  if (clearGraphic) this.measure.clear();
31465
32220
  this.toolbar.setMeasureActive(false);
31466
32221
  this.render();
@@ -31599,17 +32354,31 @@ var UserDrawingController = class {
31599
32354
  /** Cursor hint while hovering — `'pointer'` over a drawing/handle, else null. */
31600
32355
  cursorAt(x, y) {
31601
32356
  if (this.eraserMode) return "pointer";
32357
+ if (this.activeTool == null && this.magnifierChipAt(x, y)) return "pointer";
31602
32358
  return this.interaction.cursorAt(x, y);
31603
32359
  }
31604
- /** Right-click while placing: cancel the in-progress drawing and revert to the
31605
- * pointer — the gesture is an explicit escape, so it disarms even in
31606
- * stay-in-drawing-mode (where Escape would leave the tool armed). Returns whether
31607
- * the press was consumed; false lets the host's context menu open normally. */
32360
+ /** Right-click: an explicit escape back to the pointer. Cancels an in-progress
32361
+ * placement or measurement, and also plain-disarms an armed-but-idle drawing
32362
+ * tool or the eraser — so a right-click ALWAYS reverts to the pointer, even in
32363
+ * stay-in-drawing-mode (where Escape would leave a drawing tool armed).
32364
+ * Persistent toggles (magnet, stay-mode, favorites) are untouched. Returns
32365
+ * whether the press was consumed; false lets the host's context menu open
32366
+ * normally. */
31608
32367
  cancelPlacement() {
31609
- if (!this.interaction.isPlacing()) return false;
31610
- this.interaction.cancel();
31611
- if (this.activeTool != null) this.emit({ kind: "arm", type: null });
31612
- return true;
32368
+ if (this.measureMode || this.eraserMode) {
32369
+ this.withModeIntent(() => this.measureMode ? this.exitMeasure() : this.exitEraser());
32370
+ return true;
32371
+ }
32372
+ if (this.interaction.isPlacing()) {
32373
+ this.interaction.cancel();
32374
+ if (this.activeTool != null) this.emit({ kind: "arm", type: null });
32375
+ return true;
32376
+ }
32377
+ if (this.activeTool != null) {
32378
+ this.emit({ kind: "arm", type: null });
32379
+ return true;
32380
+ }
32381
+ return false;
31613
32382
  }
31614
32383
  /** Double-click over a drawing → suppress the chart's view reset (single-click already
31615
32384
  * opens settings). Returns true only when a drawing is under the cursor. */
@@ -31635,6 +32404,10 @@ var UserDrawingController = class {
31635
32404
  return true;
31636
32405
  }
31637
32406
  if (this.interaction.cancel()) return true;
32407
+ if (this.measureMode) {
32408
+ this.withModeIntent(() => this.exitMeasure());
32409
+ return true;
32410
+ }
31638
32411
  if (this.selectedIds.size) {
31639
32412
  this.clearSelection();
31640
32413
  return true;
@@ -31760,6 +32533,7 @@ var UserDrawingController = class {
31760
32533
  if (!sctx) continue;
31761
32534
  sctx.setTransform(dpr, 0, 0, dpr, 0, 0);
31762
32535
  sctx.clearRect(0, 0, canvas.width / dpr, canvas.height / dpr);
32536
+ this.painter.seriesLook = this.deps.seriesLook();
31763
32537
  this.painter.paintAll(sctx, drawings, proj, theme, EMPTY_TARGETS);
31764
32538
  const slices = out.get(paneId) ?? [];
31765
32539
  slices.push({ beforeZ, canvas });
@@ -31787,6 +32561,7 @@ var UserDrawingController = class {
31787
32561
  dragged: this.interaction.activeDragId(),
31788
32562
  mutedLabel: edited instanceof TextLabel ? edited.id : null
31789
32563
  };
32564
+ this.painter.seriesLook = this.deps.seriesLook();
31790
32565
  this.painter.paintAll(ctx, this.drawings.filter((d) => !this.isInterleaved(d)), proj, this.deps.theme(), targets);
31791
32566
  this.painter.paintHighlights(ctx, this.drawings.filter((d) => this.isInterleaved(d)), proj, handleIdsFor(targets));
31792
32567
  this.layoutTextEditor();
@@ -31794,6 +32569,10 @@ var UserDrawingController = class {
31794
32569
  if (ghost) this.painter.paintGhost(ctx, ghost, proj, this.deps.theme());
31795
32570
  if (this.externalGhost) this.painter.paintGhost(ctx, this.externalGhost, proj, this.deps.theme());
31796
32571
  this.emitDraft(ghost);
32572
+ if (this.activeTool && !ghost) {
32573
+ const hint = getDrawingType(this.activeTool)?.placementHint;
32574
+ if (hint) this.painter.paintPlacementHint(ctx, hint, this.deps.theme(), proj.width, proj.height);
32575
+ }
31797
32576
  const markers = this.interaction.placingMarkers(proj);
31798
32577
  if (markers) this.painter.paintHandles(ctx, markers);
31799
32578
  const m = this.interaction.snapMarker();
@@ -31805,6 +32584,9 @@ var UserDrawingController = class {
31805
32584
  this.closeTextEditor();
31806
32585
  this.popup.destroy();
31807
32586
  this.toolbar.destroy();
32587
+ this.seriesGwUnsub?.();
32588
+ this.seriesGwUnsub = null;
32589
+ this.seriesGw = null;
31808
32590
  this.intentCb = null;
31809
32591
  this.drawings = [];
31810
32592
  }
@@ -32202,7 +32984,7 @@ function mergeSlices(indicator, user) {
32202
32984
  }
32203
32985
 
32204
32986
  // src/renderers/native/drawings/Projector.ts
32205
- function createProjector(coords, paneOf, paneIdAtY, barsInRange) {
32987
+ function createProjector(coords, paneOf, paneIdAtY, barsInRange, seriesInRange) {
32206
32988
  return {
32207
32989
  xOf: (time) => coords.timeToX(time),
32208
32990
  yOf: (price, paneId) => {
@@ -32223,6 +33005,7 @@ function createProjector(coords, paneOf, paneIdAtY, barsInRange) {
32223
33005
  },
32224
33006
  barsBetween: (t1, t2) => Math.abs(coords.timeToLogical(t2) - coords.timeToLogical(t1)),
32225
33007
  barsInRange: barsInRange ? (from, to) => barsInRange(from, to) : void 0,
33008
+ seriesInRange,
32226
33009
  width: coords.width,
32227
33010
  height: coords.height
32228
33011
  };
@@ -34059,13 +34842,13 @@ var NativeRenderer = class {
34059
34842
  resetView: () => this.resetView(),
34060
34843
  // User drawings claim a gesture before pan when armed / over a drawing.
34061
34844
  drawingsClaim: (x, y) => this.userDrawings?.claim(x, y) ?? false,
34062
- drawingsMeasureStart: (x, y) => this.userDrawings?.beginMeasureAt(x, y) ?? false,
34845
+ drawingsMeasureStart: (x, y, snap) => this.userDrawings?.beginMeasureAt(x, y, snap) ?? false,
34063
34846
  drawingsDeleteAt: (x, y) => this.userDrawings?.deleteAt(x, y) ?? false,
34064
34847
  drawingsCancelPlacement: () => this.userDrawings?.cancelPlacement() ?? false,
34065
34848
  drawingsSnapMode: () => this.snapMode,
34066
34849
  drawingsPointerDown: (x, y, snap, shift) => this.userDrawings?.pointerDown(x, y, snap, shift),
34067
34850
  drawingsPointerMove: (x, y, snap, shift) => this.userDrawings?.pointerMove(x, y, snap, shift),
34068
- drawingsPointerUp: (x, y) => this.userDrawings?.pointerUp(x, y),
34851
+ drawingsPointerUp: (x, y, snap) => this.userDrawings?.pointerUp(x, y, snap),
34069
34852
  drawingsCursor: (x, y) => this.userDrawings?.cursorAt(x, y) ?? null,
34070
34853
  drawingsDblClick: (x, y) => this.userDrawings?.dblClick(x, y) ?? false,
34071
34854
  drawingsClearTransient: () => this.userDrawings?.clearTransient()
@@ -34092,6 +34875,23 @@ var NativeRenderer = class {
34092
34875
  seriesBoundaries: (paneId) => this.scene.seriesBoundaries(paneId),
34093
34876
  priceZ: (paneId) => paneId === PRICE_PANE_ID2 ? this.scene.candleZ : null,
34094
34877
  requestDataPaint: () => this.scheduler.invalidate(3 /* Light */),
34878
+ // The look the price series ACTUALLY paints with: candle colors resolved through
34879
+ // the per-style override, line/area colors through their configured styles — so
34880
+ // series-mirroring content (the magnifier inset) matches the chart exactly.
34881
+ seriesLook: () => {
34882
+ const st = this.scene.style;
34883
+ const paint = effectiveCandlePaint(st.candle, this.scene.candleOverride, this.theme.upColor, this.theme.downColor);
34884
+ const barsUp = st.bars.upColor ?? this.theme.upColor;
34885
+ const barsDown = st.bars.downColor ?? this.theme.downColor;
34886
+ const style = this.scene.priceStyle;
34887
+ return {
34888
+ style,
34889
+ upColor: style === "bars" ? barsUp : paint.up,
34890
+ downColor: style === "bars" ? barsDown : paint.down,
34891
+ lineColor: style === "area" ? st.area.lineColor ?? this.theme.upColor : st.line.color ?? this.theme.upColor
34892
+ };
34893
+ },
34894
+ chartBarMs: () => this.coords.barInterval,
34095
34895
  snap: (pt, paneId, mode, cursorPx) => this.snapToCandle(pt, paneId, mode, cursorPx),
34096
34896
  setSnapMode: (mode) => this.setSnapMode(mode),
34097
34897
  setToolbarGutter: (px) => this.setToolbarGutter(px)
@@ -34380,7 +35180,8 @@ var NativeRenderer = class {
34380
35180
  }
34381
35181
  const skipFit = opts?.preserveView === true && this.didInitialFit;
34382
35182
  if (this.coords.width > 0 && !skipFit) {
34383
- this.fitContent();
35183
+ if (this.didInitialFit) this.reframeKeepZoom();
35184
+ else this.fitContent();
34384
35185
  this.didInitialFit = true;
34385
35186
  }
34386
35187
  if (!this.introPlayed && this.bars.length > 0) {
@@ -35028,6 +35829,7 @@ var NativeRenderer = class {
35028
35829
  this.scaleDragHeight = res.height;
35029
35830
  this.scaleDragStart = { ...res.holder.scale };
35030
35831
  res.holder.manualScale = { ...res.holder.scale };
35832
+ this.axisScaleButtons?.reposition();
35031
35833
  this.scheduler.invalidate(4 /* Full */);
35032
35834
  }
35033
35835
  /** Rescale the grabbed scale around its center by the total drag (down ⇒ zoom out). */
@@ -35059,6 +35861,7 @@ var NativeRenderer = class {
35059
35861
  const res = this.resolveScaleHolder(x, y);
35060
35862
  if (!res) return;
35061
35863
  res.holder.manualScale = null;
35864
+ this.axisScaleButtons?.reposition();
35062
35865
  this.scheduler.invalidate(4 /* Full */);
35063
35866
  }
35064
35867
  /**
@@ -35482,7 +36285,8 @@ var NativeRenderer = class {
35482
36285
  return p ? { scale: p.scale, bounds: p.bounds, collapsed: p.collapsed } : null;
35483
36286
  },
35484
36287
  (y) => this.paneNodeAtY(y)?.id ?? null,
35485
- (from, to) => this.barsInTimeRange(from, to)
36288
+ (from, to) => this.barsInTimeRange(from, to),
36289
+ this.userDrawings?.seriesGateway ? (tf, from, to) => this.userDrawings.seriesGateway.seriesInRange(tf, from, to) : void 0
35486
36290
  );
35487
36291
  }
35488
36292
  /** OHLC bars whose open-time falls within `[from, to]` (inclusive) — the data a regression
@@ -35696,6 +36500,20 @@ var NativeRenderer = class {
35696
36500
  this.coords.setViewport(v);
35697
36501
  this.targetBarSpacing = v.barSpacing;
35698
36502
  }
36503
+ /** Re-frame after a series replacement (a symbol/timeframe switch): keep the user's
36504
+ * zoom (bar spacing), re-anchor the newest bars at the default right offset.
36505
+ * `clampViewport`'s fit-all-bars floor deliberately does NOT apply — a progressive
36506
+ * head may still be backfilling toward the previous depth, and raising the spacing
36507
+ * to its temporary bar count would lose the zoom this exists to keep. */
36508
+ reframeKeepZoom() {
36509
+ this.animator?.stop();
36510
+ this.panVelocity = 0;
36511
+ for (const pane of this.scene.panes.values()) pane.manualScale = null;
36512
+ for (const sl of this.scene.indicatorScales.values()) sl.manualScale = null;
36513
+ const v = { barSpacing: clampBarSpacing(this.coords.getViewport().barSpacing), rightOffset: defaultViewport().rightOffset };
36514
+ this.coords.setViewport(v);
36515
+ this.targetBarSpacing = v.barSpacing;
36516
+ }
35699
36517
  paneBoundsFor(paneId) {
35700
36518
  const p = this.scene.panes.get(paneId);
35701
36519
  return { top: p?.bounds.top ?? 0, height: p?.bounds.height ?? 0, rightAxis: this.rightAxisW };
@@ -36675,6 +37493,21 @@ function resolveElement(container) {
36675
37493
  // src/core/options.ts
36676
37494
  var normalizeSession = (v) => v === "regular" || v === "extended" ? v : void 0;
36677
37495
 
37496
+ // src/widget/statusline-model.ts
37497
+ function segmentVisibility(parts, chartHidden = false) {
37498
+ return { avatar: parts.logo, symbol: parts.name, meta: parts.name, market: parts.market, ohlc: parts.ohlc && !chartHidden, change: parts.change && !chartHidden, eye: chartHidden };
37499
+ }
37500
+ function statuslineMenuItems(parts, chartVisible) {
37501
+ return [
37502
+ { id: "part:logo", label: "Symbol logo", checked: parts.logo },
37503
+ { id: "part:name", label: "Symbol name", checked: parts.name },
37504
+ { id: "part:market", label: "Market status", checked: parts.market },
37505
+ { id: "part:ohlc", label: "OHLC values", checked: parts.ohlc },
37506
+ { id: "part:change", label: "Bar change values", checked: parts.change },
37507
+ { id: "chart", label: chartVisible ? "Hide chart" : "Show chart", separatorBefore: true }
37508
+ ];
37509
+ }
37510
+
36678
37511
  // src/widget/format.ts
36679
37512
  function decimalsFor(ref) {
36680
37513
  const a = Math.abs(ref);
@@ -36722,7 +37555,13 @@ var CSS21 = `
36722
37555
  padding: 2px 7px;
36723
37556
  margin-left: -7px;
36724
37557
  }
36725
- .vela-statusline:hover { background: var(--vela-bg); }
37558
+ /* Hovering opens the chip the same way a legend row opens: solid chart background
37559
+ * plus the same inset neutral outline the indicator rows wear (InputsUI's
37560
+ * setRowHighlighted) \u2014 the two columns read as one family. */
37561
+ .vela-statusline:hover { background: var(--vela-bg); box-shadow: inset 0 0 0 1px var(--vela-border); }
37562
+ /* Chart hidden (the price series' eye \u2014 renderer 'candleVisible'): the line dims to
37563
+ * the same 0.5 wash a hidden indicator's legend row wears. */
37564
+ .vela-statusline.vela-sl-chart-hidden { opacity: 0.5; }
36726
37565
  .vela-statusline .vela-sl-avatar {
36727
37566
  width: 18px;
36728
37567
  height: 18px;
@@ -36747,6 +37586,24 @@ var CSS21 = `
36747
37586
  * these are the pre-ink fallbacks only. */
36748
37587
  .vela-statusline .vela-sl-change[data-dir='up'] { color: var(--vela-up); }
36749
37588
  .vela-statusline .vela-sl-change[data-dir='down'] { color: var(--vela-down); }
37589
+ /* The show-chart eye \u2014 out only while the chart is hidden (syncParts drives display),
37590
+ * replacing the value readout it took away. Same footprint as a legend action button. */
37591
+ .vela-statusline .vela-sl-eye {
37592
+ align-self: center;
37593
+ align-items: center;
37594
+ justify-content: center;
37595
+ width: 18px;
37596
+ height: 18px;
37597
+ padding: 0;
37598
+ border: none;
37599
+ border-radius: 3px;
37600
+ background: none;
37601
+ color: var(--vela-fg-muted);
37602
+ cursor: pointer;
37603
+ line-height: 0;
37604
+ flex: none;
37605
+ }
37606
+ .vela-statusline .vela-sl-eye:hover { color: var(--vela-fg); background: color-mix(in srgb, var(--vela-fg) 12%, transparent); }
36750
37607
  /* Stack the TOP pane's legend below the status line (lower study panes stay put).
36751
37608
  * The renderer marks whichever legend sits at the plot's top edge \u2014 the price pane
36752
37609
  * normally, or a maximized study pane filling the plot \u2014 so the legend never merges
@@ -36856,7 +37713,12 @@ var Statusline = class {
36856
37713
  constructor(host, symbol, iconFor) {
36857
37714
  this.host = host;
36858
37715
  this.iconFor = iconFor;
36859
- this.parts = { name: true, market: true, ohlc: true, change: true };
37716
+ this.parts = { logo: true, name: true, market: true, ohlc: true, change: true };
37717
+ /** The right-click action menu — present once a host wires it via {@link attachMenu}. */
37718
+ this.menu = null;
37719
+ this.menuHooks = null;
37720
+ /** Mirror of the renderer's `candleVisible` — see {@link setChartHidden}. */
37721
+ this.chartHidden = false;
36860
37722
  this.lastBar = null;
36861
37723
  this.hoverBar = null;
36862
37724
  this.unsubs = [];
@@ -36872,6 +37734,15 @@ var Statusline = class {
36872
37734
  /** Fit mode (multi-chart cells): one row, overflowing segments hidden — see {@link setFitMode}. */
36873
37735
  this.fitMode = false;
36874
37736
  this.fitRO = null;
37737
+ this.onContextMenu = (e) => {
37738
+ if (!this.menu || !this.menuHooks) return;
37739
+ e.preventDefault();
37740
+ e.stopPropagation();
37741
+ const chartVisible = this.menuHooks.chartVisible();
37742
+ this.setChartHidden(!chartVisible);
37743
+ this.menu.setItems(statuslineMenuItems(this.parts, chartVisible));
37744
+ this.menu.openAt(e.clientX, e.clientY);
37745
+ };
36875
37746
  const doc = host.ownerDocument;
36876
37747
  injectStyles(STYLE_ID24, CSS21, doc);
36877
37748
  host.classList.add("vela-has-statusline");
@@ -36898,9 +37769,21 @@ var Statusline = class {
36898
37769
  this.ohlcEl.className = "vela-sl-ohlc";
36899
37770
  this.changeEl = doc.createElement("span");
36900
37771
  this.changeEl.className = "vela-sl-change";
36901
- this.el.append(this.avatarEl, this.symbolEl, this.metaEl, this.marketEl, this.ohlcEl, this.changeEl);
37772
+ this.eyeEl = doc.createElement("button");
37773
+ this.eyeEl.type = "button";
37774
+ this.eyeEl.className = "vela-sl-eye";
37775
+ this.eyeEl.innerHTML = iconAt("eye-off", 14);
37776
+ this.eyeEl.setAttribute("aria-label", "Show chart");
37777
+ this.eyeEl.style.display = "none";
37778
+ this.eyeEl.addEventListener("click", (e) => {
37779
+ e.stopPropagation();
37780
+ this.menuHooks?.setChartVisible(true);
37781
+ this.setChartHidden(false);
37782
+ });
37783
+ this.el.append(this.avatarEl, this.symbolEl, this.metaEl, this.marketEl, this.ohlcEl, this.changeEl, this.eyeEl);
36902
37784
  host.appendChild(this.el);
36903
37785
  this.marketTip = new Tooltip(this.marketEl, { content: MARKET_LABELS.open, placement: "bottom" });
37786
+ this.eyeTip = new Tooltip(this.eyeEl, { content: "Show chart", placement: "bottom" });
36904
37787
  this.setMarketStatus("open");
36905
37788
  this.render();
36906
37789
  }
@@ -36910,6 +37793,7 @@ var Statusline = class {
36910
37793
  const fresh = tickerIconEl(this.el.ownerDocument, baseOfTicker(ticker), ticker, "vela-sl-avatar", this.iconFor?.(symbol));
36911
37794
  this.avatarEl.replaceWith(fresh);
36912
37795
  this.avatarEl = fresh;
37796
+ this.syncParts();
36913
37797
  this.fit();
36914
37798
  }
36915
37799
  /**
@@ -36937,20 +37821,25 @@ var Statusline = class {
36937
37821
  }
36938
37822
  /** Project the parts config onto the segments (the baseline fit() prunes from). */
36939
37823
  syncParts() {
36940
- this.symbolEl.style.display = this.parts.name ? "" : "none";
36941
- this.marketEl.style.display = this.parts.market ? "" : "none";
36942
- this.ohlcEl.style.display = this.parts.ohlc ? "" : "none";
36943
- this.changeEl.style.display = this.parts.change ? "" : "none";
37824
+ const seg = segmentVisibility(this.parts, this.chartHidden);
37825
+ this.avatarEl.style.display = seg.avatar ? "" : "none";
37826
+ this.symbolEl.style.display = seg.symbol ? "" : "none";
37827
+ this.metaEl.style.display = seg.meta ? "" : "none";
37828
+ this.marketEl.style.display = seg.market ? "" : "none";
37829
+ this.ohlcEl.style.display = seg.ohlc ? "" : "none";
37830
+ this.changeEl.style.display = seg.change ? "" : "none";
37831
+ this.eyeEl.style.display = seg.eye ? "inline-flex" : "none";
36944
37832
  }
36945
37833
  /** Hide overflowing segments until the row fits its max-width (fit mode only). */
36946
37834
  fit() {
36947
37835
  if (!this.fitMode) return;
36948
37836
  this.syncParts();
37837
+ const seg = segmentVisibility(this.parts, this.chartHidden);
36949
37838
  const order = [
36950
- [this.ohlcEl, this.parts.ohlc],
36951
- [this.changeEl, this.parts.change],
36952
- [this.metaEl, true],
36953
- [this.marketEl, this.parts.market]
37839
+ [this.ohlcEl, seg.ohlc],
37840
+ [this.changeEl, seg.change],
37841
+ [this.metaEl, seg.meta],
37842
+ [this.marketEl, seg.market]
36954
37843
  ];
36955
37844
  for (const [el, shown] of order) {
36956
37845
  if (this.el.scrollWidth <= this.el.clientWidth) break;
@@ -36971,7 +37860,6 @@ var Statusline = class {
36971
37860
  this.readout = readout;
36972
37861
  this.render();
36973
37862
  }
36974
- /** Show/hide one part (the settings dialog's Status line tab drives these). */
36975
37863
  /** The "· BINANCE · 1h" segment after the symbol — venue first, then resolution. */
36976
37864
  setMeta(timeframe, provider) {
36977
37865
  this.metaEl.textContent = `${provider ? `\xB7 ${provider.toUpperCase()} ` : ""}\xB7 ${timeframeLabel(timeframe)}`;
@@ -36990,6 +37878,9 @@ var Statusline = class {
36990
37878
  });
36991
37879
  this.marketTip.setContent(MARKET_LABELS[status]);
36992
37880
  }
37881
+ /** Show/hide one part — the settings dialog's Status line tab and the right-click
37882
+ * menu both drive these. 'name' owns the venue/timeframe meta too (see
37883
+ * {@link segmentVisibility}). */
36993
37884
  setPartVisible(part, visible) {
36994
37885
  this.parts[part] = visible;
36995
37886
  this.syncParts();
@@ -36998,6 +37889,47 @@ var Statusline = class {
36998
37889
  partVisible(part) {
36999
37890
  return this.parts[part];
37000
37891
  }
37892
+ /** Mirror the chart's (price series') visibility: dim the whole line like a hidden
37893
+ * indicator's legend row, drop the value readout (OHLC + bar change — values of a
37894
+ * series that isn't painted), and put the show-chart eye out in its place. The
37895
+ * parts config is untouched, so showing the chart restores the readout exactly as
37896
+ * configured. Idempotent; {@link render} re-syncs it from the live renderer, so
37897
+ * toggles made elsewhere (the object tree's eye) converge too. */
37898
+ setChartHidden(hidden) {
37899
+ if (hidden === this.chartHidden) return;
37900
+ this.chartHidden = hidden;
37901
+ this.el.classList.toggle("vela-sl-chart-hidden", hidden);
37902
+ this.syncParts();
37903
+ this.fit();
37904
+ }
37905
+ /** Wire the right-click action menu: one checkable toggle per part plus hide/show
37906
+ * for the chart itself. The menu is built once; later calls just swap the hooks. */
37907
+ attachMenu(hooks) {
37908
+ this.menuHooks = hooks;
37909
+ if (this.menu) return;
37910
+ this.menu = new Menu({
37911
+ host: this.host,
37912
+ items: [],
37913
+ placement: "bottom-start",
37914
+ // Pointer-anchored action menu — checked state reads as a leading ✓ (the
37915
+ // same shape as the chart's own context menu).
37916
+ checkmarks: true,
37917
+ onSelect: (id) => this.runMenuItem(id)
37918
+ });
37919
+ this.el.addEventListener("contextmenu", this.onContextMenu);
37920
+ }
37921
+ runMenuItem(id) {
37922
+ const hooks = this.menuHooks;
37923
+ if (!hooks) return;
37924
+ if (id.startsWith("part:")) {
37925
+ const part = id.slice("part:".length);
37926
+ hooks.setPart(part, !this.parts[part]);
37927
+ } else if (id === "chart") {
37928
+ const next = !hooks.chartVisible();
37929
+ hooks.setChartVisible(next);
37930
+ this.setChartHidden(!next);
37931
+ }
37932
+ }
37001
37933
  /** (Re)bind to a chart instance — called after every widget rebuild. */
37002
37934
  onChart(chart) {
37003
37935
  this.detach();
@@ -37019,7 +37951,11 @@ var Statusline = class {
37019
37951
  this.detach();
37020
37952
  this.fitRO?.disconnect();
37021
37953
  this.fitRO = null;
37954
+ this.el.removeEventListener("contextmenu", this.onContextMenu);
37955
+ this.menu?.destroy();
37956
+ this.menu = null;
37022
37957
  this.marketTip.destroy();
37958
+ this.eyeTip.destroy();
37023
37959
  this.marketBubble.destroy();
37024
37960
  this.host.classList.remove("vela-has-statusline", "vela-sl-fit-host");
37025
37961
  this.el.remove();
@@ -37029,6 +37965,7 @@ var Statusline = class {
37029
37965
  this.unsubs = [];
37030
37966
  }
37031
37967
  render() {
37968
+ if (this.menuHooks) this.setChartHidden(!this.menuHooks.chartVisible());
37032
37969
  const bar = this.hoverBar ?? this.lastBar;
37033
37970
  if (!bar) {
37034
37971
  this.ohlcEl.replaceChildren();
@@ -37099,7 +38036,7 @@ var FETCH_BACK_MS = 4 * 864e5;
37099
38036
  var FETCH_AHEAD_MS = 10 * 864e5;
37100
38037
  var MIN_TIMER_MS = 15e3;
37101
38038
  var MAX_TIMER_MS = 36e5;
37102
- var RETRY_MS = 6e4;
38039
+ var RETRY_MS2 = 6e4;
37103
38040
  var MarketStatusTracker = class {
37104
38041
  constructor(onStatus) {
37105
38042
  this.onStatus = onStatus;
@@ -37136,7 +38073,7 @@ var MarketStatusTracker = class {
37136
38073
  ]);
37137
38074
  if (my !== this.epoch) return;
37138
38075
  if (!regular || !extended) {
37139
- this.arm(my, data, symbol, RETRY_MS);
38076
+ this.arm(my, data, symbol, RETRY_MS2);
37140
38077
  return;
37141
38078
  }
37142
38079
  const w = { regular, extended };
@@ -37397,6 +38334,7 @@ var TIME_AXIS_H2 = 22;
37397
38334
  var CONTROLS_BOTTOM_PX = TIME_AXIS_H2 + 12;
37398
38335
  var CLUSTER_H2 = 24;
37399
38336
  var CLUSTER_PILL2 = "rgba(0,0,0,0.65)";
38337
+ var CLUSTER_LEFT_CSS = "calc((100% + var(--vela-toolbar-gutter, 0px) - var(--vela-scale-gutter, 0px)) / 2)";
37400
38338
  var STYLE_ID26 = "vela-cell-controls";
37401
38339
  var CSS23 = `
37402
38340
  .vela-cc-btn{display:inline-flex;align-items:center;justify-content:center;width:20px;height:20px;padding:0;border:none;border-radius:var(--vela-radius-sm);background:transparent;line-height:0;font-size:12px;color:var(--vela-fg-muted);cursor:pointer;}
@@ -37406,8 +38344,11 @@ var CSS23 = `
37406
38344
  .vela-cc-grip{cursor:grab;touch-action:none;}
37407
38345
  .vela-cc-grip:active{cursor:grabbing;}
37408
38346
  `;
37409
- function nearBottomCenter(x, y, width, height, proximityPx = CELL_CONTROLS_PROXIMITY_PX) {
37410
- const cx = width / 2;
38347
+ function plotCenterX(width, toolbarGutter = 0, scaleGutter = 0) {
38348
+ return (width + toolbarGutter - scaleGutter) / 2;
38349
+ }
38350
+ function nearBottomCenter(x, y, width, height, proximityPx = CELL_CONTROLS_PROXIMITY_PX, gutters = {}) {
38351
+ const cx = plotCenterX(width, gutters.toolbar ?? 0, gutters.scale ?? 0);
37411
38352
  const cy = height - CONTROLS_BOTTOM_PX - CLUSTER_H2 / 2;
37412
38353
  return Math.hypot(x - cx, y - cy) <= proximityPx;
37413
38354
  }
@@ -37426,7 +38367,7 @@ var CellControls = class {
37426
38367
  if (this.suspended) return;
37427
38368
  if (this.dragging) return;
37428
38369
  const rect = this.host.getBoundingClientRect();
37429
- this.setNear(nearBottomCenter(e.clientX - rect.left, e.clientY - rect.top, rect.width, rect.height));
38370
+ this.setNear(nearBottomCenter(e.clientX - rect.left, e.clientY - rect.top, rect.width, rect.height, CELL_CONTROLS_PROXIMITY_PX, this.hostGutters()));
37430
38371
  };
37431
38372
  this.onHostLeave = () => {
37432
38373
  if (this.dragging) return;
@@ -37437,7 +38378,7 @@ var CellControls = class {
37437
38378
  this.root = host.ownerDocument.createElement("div");
37438
38379
  Object.assign(this.root.style, {
37439
38380
  position: "absolute",
37440
- left: "50%",
38381
+ left: CLUSTER_LEFT_CSS,
37441
38382
  bottom: `${CONTROLS_BOTTOM_PX}px`,
37442
38383
  transform: "translateX(-50%)",
37443
38384
  zIndex: "6",
@@ -37537,6 +38478,16 @@ var CellControls = class {
37537
38478
  this.suspended = on;
37538
38479
  if (on) this.setNear(false);
37539
38480
  }
38481
+ /** Live renderer gutters on the cell host (0 when unpublished — a test stub). */
38482
+ hostGutters() {
38483
+ const view = this.host.ownerDocument.defaultView;
38484
+ if (!view) return { toolbar: 0, scale: 0 };
38485
+ const cs = view.getComputedStyle(this.host);
38486
+ return {
38487
+ toolbar: Number.parseFloat(cs.getPropertyValue("--vela-toolbar-gutter")) || 0,
38488
+ scale: Number.parseFloat(cs.getPropertyValue("--vela-scale-gutter")) || 0
38489
+ };
38490
+ }
37540
38491
  setNear(near) {
37541
38492
  if (near === this.near) return;
37542
38493
  this.near = near;
@@ -37777,6 +38728,12 @@ function cellDrawings(opt) {
37777
38728
  if (opt === true || opt == null) return { toolbar: false };
37778
38729
  return { ...opt, toolbar: false };
37779
38730
  }
38731
+ function instanceDeltas(handle) {
38732
+ if (!handle) return void 0;
38733
+ const inputs = inputDeltas(handle.inputs, handle.inputValues());
38734
+ const props = inputDeltas(handle.props, handle.propValues());
38735
+ return inputs || props ? { ...inputs ? { inputs } : {}, ...props ? { props } : {} } : void 0;
38736
+ }
37780
38737
  var ChartCell = class {
37781
38738
  constructor(id, gridHost, seed, deps) {
37782
38739
  this.id = id;
@@ -37827,7 +38784,14 @@ var ChartCell = class {
37827
38784
  this.destroyed = false;
37828
38785
  this.appTheme = deps.theme;
37829
38786
  const symbol = prefixedSymbol(seed);
37830
- this.state = { symbol, provider: parseSymbol(symbol ?? "").provider ?? void 0, timeframe: seed.timeframe, priceStyle: seed.priceStyle, bars: seed.bars, session: normalizeSession(seed.session) };
38787
+ this.state = {
38788
+ symbol,
38789
+ provider: parseSymbol(symbol ?? "").provider ?? void 0,
38790
+ timeframe: seed.timeframe,
38791
+ priceStyle: seed.priceStyle,
38792
+ bars: seed.bars,
38793
+ session: normalizeSession(seed.session)
38794
+ };
37831
38795
  const doc = gridHost.ownerDocument;
37832
38796
  this.host = doc.createElement("div");
37833
38797
  this.host.className = "vela-cell";
@@ -37915,6 +38879,11 @@ var ChartCell = class {
37915
38879
  this.statusline = deps.statusline ? new Statusline(this.host, symbol ?? "", (sym) => this.inner?.data.symbolIcon(sym)) : null;
37916
38880
  this.statusline?.setMeta(seed.timeframe ?? "60", this.state.provider ?? "");
37917
38881
  this.statusline?.onChart(this.inner);
38882
+ this.statusline?.attachMenu({
38883
+ setPart: (part, visible) => this.setStatuslinePart(part, visible),
38884
+ chartVisible: () => this.inner?.renderer.get("candleVisible") !== false,
38885
+ setChartVisible: (visible) => this.inner?.renderer.set("candleVisible", visible)
38886
+ });
37918
38887
  this.marketStatus = this.statusline ? new MarketStatusTracker((s) => this.statusline?.setMarketStatus(s)) : null;
37919
38888
  void this.inner.data.ready().then(() => {
37920
38889
  if (this.inner && this.state.symbol) {
@@ -37952,6 +38921,7 @@ var ChartCell = class {
37952
38921
  this.syncPresentNatives();
37953
38922
  this.refreshNativeCatalog();
37954
38923
  });
38924
+ this.inner.on("indicator:inputs", () => this.deps.onStateDirty());
37955
38925
  this.inner.on("indicator:removed", ({ id: id2 }) => {
37956
38926
  if (this.destroyed) return;
37957
38927
  const idx = this.instances.findIndex((it) => it.handle?.id === id2);
@@ -37960,7 +38930,7 @@ var ChartCell = class {
37960
38930
  this.instances.splice(idx, 1);
37961
38931
  this.history.push({
37962
38932
  undo: () => {
37963
- snapshot.handle = this.addToChart(snapshot.entry);
38933
+ snapshot.handle = this.addToChart(snapshot.entry, snapshot.values);
37964
38934
  this.instances.push(snapshot);
37965
38935
  this.deps.onIndicatorsChanged(this.id);
37966
38936
  },
@@ -38121,7 +39091,7 @@ var ChartCell = class {
38121
39091
  kind: "select",
38122
39092
  label: "Bars to fetch",
38123
39093
  id: "bars",
38124
- options: ["500", "1000", "2000", "5000", "10000", "20000"],
39094
+ options: ["500", "1000", "2000", "5000", "10000", "20000", "50000", "60000", "80000", "100000"],
38125
39095
  get: () => String(this.state.bars ?? 1e3),
38126
39096
  set: (v) => {
38127
39097
  this.state.bars = Number(v);
@@ -38153,10 +39123,34 @@ var ChartCell = class {
38153
39123
  id: "status-line",
38154
39124
  rows: [
38155
39125
  { kind: "heading", label: "Status line", id: "parts" },
38156
- { kind: "toggle", label: "Symbol name", id: "name", get: () => sl.partVisible("name"), set: (v) => this.setStatuslinePart("name", v) },
38157
- { kind: "toggle", label: "Market status", id: "market", get: () => sl.partVisible("market"), set: (v) => this.setStatuslinePart("market", v) },
38158
- { kind: "toggle", label: "OHLC values", id: "ohlc", get: () => sl.partVisible("ohlc"), set: (v) => this.setStatuslinePart("ohlc", v) },
38159
- { kind: "toggle", label: "Bar change values", id: "change", get: () => sl.partVisible("change"), set: (v) => this.setStatuslinePart("change", v) },
39126
+ {
39127
+ kind: "toggle",
39128
+ label: "Symbol name",
39129
+ id: "name",
39130
+ get: () => sl.partVisible("name"),
39131
+ set: (v) => this.setStatuslinePart("name", v)
39132
+ },
39133
+ {
39134
+ kind: "toggle",
39135
+ label: "Market status",
39136
+ id: "market",
39137
+ get: () => sl.partVisible("market"),
39138
+ set: (v) => this.setStatuslinePart("market", v)
39139
+ },
39140
+ {
39141
+ kind: "toggle",
39142
+ label: "OHLC values",
39143
+ id: "ohlc",
39144
+ get: () => sl.partVisible("ohlc"),
39145
+ set: (v) => this.setStatuslinePart("ohlc", v)
39146
+ },
39147
+ {
39148
+ kind: "toggle",
39149
+ label: "Bar change values",
39150
+ id: "change",
39151
+ get: () => sl.partVisible("change"),
39152
+ set: (v) => this.setStatuslinePart("change", v)
39153
+ },
38160
39154
  { kind: "heading", label: "Indicators", id: "indicators" },
38161
39155
  {
38162
39156
  kind: "toggle",
@@ -38209,7 +39203,7 @@ var ChartCell = class {
38209
39203
  statusPrefs() {
38210
39204
  const sl = this.statusline;
38211
39205
  return {
38212
- parts: sl ? { name: sl.partVisible("name"), market: sl.partVisible("market"), ohlc: sl.partVisible("ohlc"), change: sl.partVisible("change") } : null,
39206
+ parts: sl ? { logo: sl.partVisible("logo"), name: sl.partVisible("name"), market: sl.partVisible("market"), ohlc: sl.partVisible("ohlc"), change: sl.partVisible("change") } : null,
38213
39207
  indicatorTitles: this.indicatorTitlesOn,
38214
39208
  indicatorValues: this.indicatorValuesOn
38215
39209
  };
@@ -38349,9 +39343,9 @@ var ChartCell = class {
38349
39343
  this.manifest = list;
38350
39344
  if (this.pendingManifestNames) {
38351
39345
  if (list.length === 0) return;
38352
- for (const name of this.pendingManifestNames) {
38353
- const entry = list.find((e) => e.name === name);
38354
- if (entry) this.addManifestInstance(entry, { record: false });
39346
+ for (const led of this.pendingManifestNames) {
39347
+ const entry = list.find((e) => e.name === ledgerEntryName(led));
39348
+ if (entry) this.addManifestInstance(entry, { record: false, ...typeof led === "object" ? { inputs: led.inputs, props: led.props } : {} });
38355
39349
  }
38356
39350
  this.pendingManifestNames = null;
38357
39351
  return;
@@ -38381,9 +39375,10 @@ var ChartCell = class {
38381
39375
  }
38382
39376
  for (const it of [...this.instances]) this.dropInstance(it);
38383
39377
  if (this.manifest.length > 0) {
38384
- for (const name of led.manifest) {
38385
- const entry = this.manifest.find((e) => e.name === name);
38386
- if (entry) this.addManifestInstance(entry, { record: false });
39378
+ for (const item of led.manifest) {
39379
+ const entry = this.manifest.find((e) => e.name === ledgerEntryName(item));
39380
+ if (entry)
39381
+ this.addManifestInstance(entry, { record: false, ...typeof item === "object" ? { inputs: item.inputs, props: item.props } : {} });
38387
39382
  }
38388
39383
  this.pendingManifestNames = null;
38389
39384
  } else if (!this.deps.manifestSettled()) {
@@ -38432,12 +39427,16 @@ var ChartCell = class {
38432
39427
  * a persistence handler's `restore` runs silently, a user-driven call records.
38433
39428
  */
38434
39429
  addExternalIndicator(entry) {
38435
- this.addManifestInstance({ ...entry, enabled: true }, { external: true });
39430
+ this.addManifestInstance(
39431
+ { ...entry, enabled: true },
39432
+ { external: true, ...entry.inputs ? { inputs: entry.inputs } : {}, ...entry.props ? { props: entry.props } : {} }
39433
+ );
38436
39434
  }
38437
39435
  /** Add ONE instance of a manifest entry (repeatable — duplicates are legitimate). */
38438
39436
  addManifestInstance(entry, opts = {}) {
38439
39437
  if (this.destroyed) return;
38440
- const it = { entry, handle: this.addToChart(entry), ...opts.external ? { external: true } : {} };
39438
+ const values = opts.inputs || opts.props ? { inputs: opts.inputs, props: opts.props } : void 0;
39439
+ const it = { entry, handle: this.addToChart(entry, values), ...opts.external ? { external: true } : {}, ...values ? { values } : {} };
38441
39440
  this.instances.push(it);
38442
39441
  this.deps.onIndicatorsChanged(this.id);
38443
39442
  if (opts.record === false) return;
@@ -38445,7 +39444,7 @@ var ChartCell = class {
38445
39444
  this.history.push({
38446
39445
  undo: () => this.dropInstance(snapshot),
38447
39446
  redo: () => {
38448
- snapshot.handle = this.addToChart(snapshot.entry);
39447
+ snapshot.handle = this.addToChart(snapshot.entry, snapshot.values);
38449
39448
  this.instances.push(snapshot);
38450
39449
  this.deps.onIndicatorsChanged(this.id);
38451
39450
  }
@@ -38458,7 +39457,7 @@ var ChartCell = class {
38458
39457
  const snapshot = it;
38459
39458
  this.history.push({
38460
39459
  undo: () => {
38461
- snapshot.handle = this.addToChart(snapshot.entry);
39460
+ snapshot.handle = this.addToChart(snapshot.entry, snapshot.values);
38462
39461
  this.instances.push(snapshot);
38463
39462
  this.deps.onIndicatorsChanged(this.id);
38464
39463
  },
@@ -38468,6 +39467,9 @@ var ChartCell = class {
38468
39467
  dropInstance(it) {
38469
39468
  const idx = this.instances.indexOf(it);
38470
39469
  if (idx >= 0) this.instances.splice(idx, 1);
39470
+ const captured = instanceDeltas(it.handle);
39471
+ if (captured) it.values = captured;
39472
+ else delete it.values;
38471
39473
  try {
38472
39474
  it.handle?.remove();
38473
39475
  } catch {
@@ -38510,9 +39512,13 @@ var ChartCell = class {
38510
39512
  this.deps.onIndicatorsChanged(this.id);
38511
39513
  });
38512
39514
  }
38513
- addToChart(entry) {
39515
+ addToChart(entry, values) {
38514
39516
  try {
38515
- return this.inner?.addIndicator(entry.script, entry.language !== void 0 ? { language: entry.language } : void 0) ?? null;
39517
+ return this.inner?.addIndicator(entry.script, {
39518
+ ...entry.language !== void 0 ? { language: entry.language } : {},
39519
+ ...values?.inputs ? { inputs: values.inputs } : {},
39520
+ ...values?.props ? { props: values.props } : {}
39521
+ }) ?? null;
38516
39522
  } catch (err) {
38517
39523
  console.warn(`[vela] indicator "${entry.name}" failed to add:`, err);
38518
39524
  return null;
@@ -38626,7 +39632,10 @@ var ChartCell = class {
38626
39632
  // manifest — their plugin persists them via the `ext` seam instead.
38627
39633
  indicators: indicatorLedger({
38628
39634
  present: this.inner ? this.inner.presentNativeIndicators() : [],
38629
- instanceNames: this.instances.filter((it) => !it.external).map((it) => it.entry.name),
39635
+ instanceEntries: this.instances.filter((it) => !it.external).map((it) => {
39636
+ const d = it.handle ? instanceDeltas(it.handle) : it.values;
39637
+ return d ? { name: it.entry.name, ...d } : it.entry.name;
39638
+ }),
38630
39639
  pendingManifest: this.pendingManifestNames,
38631
39640
  manifestSettled: this.deps.manifestSettled(),
38632
39641
  volumePending: this.volumeMayBePending && this.volumeIntent
@@ -38927,7 +39936,7 @@ var SplitterLayer = class {
38927
39936
  };
38928
39937
 
38929
39938
  // src/workspace/VelaWorkspace.ts
38930
- var DEFAULT_TIMEFRAMES = ["1", "5", "15", "60", "240", "D", "W"];
39939
+ var DEFAULT_TIMEFRAMES = ["1", "5", "15", "30", "60", "240", "D", "W", "M"];
38931
39940
  var GAP_PX = 2;
38932
39941
  var POOL_CAP = 16;
38933
39942
  var TIME_AXIS_H4 = 22;