@luxalgo/vela 0.6.11 → 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 (41) hide show
  1. package/README.md +110 -46
  2. package/dist/{DataProvider-BBf-jc6W.d.ts → DataProvider-DhzstpQb.d.ts} +1 -1
  3. package/dist/{DataProvider-p0TEyhlX.d.cts → DataProvider-DlMtrwqM.d.cts} +1 -1
  4. package/dist/{chunk-MTLJKZDZ.js → chunk-F4M24ANM.js} +122 -1
  5. package/dist/{chunk-IO3NYSQV.js → chunk-IZV3CW5N.js} +184 -25
  6. package/dist/{chunk-73PEA4MU.js → chunk-KV7FDWSL.js} +618 -38
  7. package/dist/{contributions-Bbe2R-mQ.d.ts → contributions-37nni40G.d.ts} +6 -4
  8. package/dist/{contributions-CO01zWve.d.cts → contributions-lPojhTxI.d.cts} +6 -4
  9. package/dist/index.cjs +727 -26
  10. package/dist/index.d.cts +6 -6
  11. package/dist/index.d.ts +6 -6
  12. package/dist/index.js +2 -2
  13. package/dist/{options-yp7sA96q.d.cts → options-CYS5Wmlx.d.cts} +71 -2
  14. package/dist/{options-yp7sA96q.d.ts → options-CYS5Wmlx.d.ts} +71 -2
  15. package/dist/{plugin-CkkH8QnX.d.cts → plugin-Cwikpz1m.d.cts} +10 -3
  16. package/dist/{plugin-DwxjM3Ni.d.ts → plugin-DHyaoMjW.d.ts} +10 -3
  17. package/dist/plugin.cjs +110 -0
  18. package/dist/plugin.d.cts +4 -4
  19. package/dist/plugin.d.ts +4 -4
  20. package/dist/plugin.js +1 -1
  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-CHPDuKNp.d.ts → statusline-model-CiJm-riV.d.cts} +7 -85
  28. package/dist/{statusline-DTHZUFqK.d.cts → statusline-model-D-q_OOx9.d.ts} +7 -85
  29. package/dist/ui.d.cts +1 -1
  30. package/dist/ui.d.ts +1 -1
  31. package/dist/vela.global.js +727 -26
  32. package/dist/vela.global.min.js +52 -52
  33. package/dist/widget.cjs +910 -50
  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 +910 -50
  38. package/dist/workspace.d.cts +5 -5
  39. package/dist/workspace.d.ts +5 -5
  40. package/dist/workspace.js +3 -3
  41. package/package.json +1 -1
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);
@@ -18141,6 +18269,180 @@ function presetToRange(preset, bars) {
18141
18269
  return { from: Math.max(first, from), to: last };
18142
18270
  }
18143
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
+
18144
18446
  // src/core/price-styles/BarTransform.ts
18145
18447
  function barTransformFor(style) {
18146
18448
  return chartType(style)?.barTransform ?? null;
@@ -18277,7 +18579,13 @@ var EngineOrchestrator = class _EngineOrchestrator {
18277
18579
  const initialStyle = this.renderer.readFeature("priceStyle");
18278
18580
  if (typeof initialStyle === "string") this.priceStyle = initialStyle;
18279
18581
  this.barTransform = barTransformFor(initialStyle);
18280
- 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);
18281
18589
  this.unresolvedUnsub = this.feed.onUnresolved?.((info) => {
18282
18590
  this.endLoad();
18283
18591
  this.events.emit("data:unresolved", info);
@@ -19270,8 +19578,8 @@ var EngineOrchestrator = class _EngineOrchestrator {
19270
19578
  onModel: (model) => {
19271
19579
  const first = !record.announced;
19272
19580
  const cause = record.pendingCause ?? "history";
19581
+ if (!this.applyModel(id, model)) return;
19273
19582
  record.pendingCause = void 0;
19274
- this.applyModel(id, model);
19275
19583
  this.emitContextChanged(id);
19276
19584
  this.emitScriptRun(id, cause, first);
19277
19585
  },
@@ -19564,10 +19872,16 @@ var EngineOrchestrator = class _EngineOrchestrator {
19564
19872
  * Apply an emitted model. First emission mounts (and routes the pane); a pending
19565
19873
  * structural change (after an input edit) remounts idempotently; everything else
19566
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.
19567
19880
  */
19568
19881
  applyModel(id, model) {
19569
19882
  const record = this.registry.get(id);
19570
- 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;
19571
19885
  const handle = this.handles.get(id);
19572
19886
  if (!record.renderHandle) {
19573
19887
  const paneId2 = this.routePane(id, model, record.options ?? {});
@@ -19577,7 +19891,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
19577
19891
  record.renderHandle = this.renderer.mountIndicator(model);
19578
19892
  record.pendingStructural = false;
19579
19893
  this.announce(record, handle);
19580
- return;
19894
+ return true;
19581
19895
  }
19582
19896
  let paneId = record.model?.paneId ?? "price";
19583
19897
  const prevOwnScale = record.model?.ownScale === true;
@@ -19605,6 +19919,11 @@ var EngineOrchestrator = class _EngineOrchestrator {
19605
19919
  }
19606
19920
  if (record.loading) this.setLoading(record, false);
19607
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;
19608
19927
  }
19609
19928
  routePane(id, model, options) {
19610
19929
  if (options.pane === "new") return `pane-${id}`;
@@ -25739,7 +26058,9 @@ var DrawingSceneRenderer = class {
25739
26058
  const lo = Math.min(from, to);
25740
26059
  const hi = Math.max(from, to);
25741
26060
  const visible = (a, b, extend) => {
25742
- 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;
25743
26064
  return Math.max(a, b) >= lo && Math.min(a, b) <= hi;
25744
26065
  };
25745
26066
  let min = Infinity;
@@ -28213,6 +28534,17 @@ var DrawingPainter = class {
28213
28534
  constructor() {
28214
28535
  /** The current `paintAll` call's interaction state, visible to the per-type painters. */
28215
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
+ };
28216
28548
  }
28217
28549
  /** Paint every visible drawing, then selection handles for the targeted ones.
28218
28550
  * Each drawing is clipped to its own pane's rect (and skipped entirely while that pane
@@ -28257,6 +28589,8 @@ var DrawingPainter = class {
28257
28589
  ctx.globalAlpha = GHOST_ALPHA;
28258
28590
  if (ghost instanceof RegressionChannel || ghost instanceof FixedRangeVolumeProfile) {
28259
28591
  this.paintTimeSpanGhost(ctx, ghost, proj);
28592
+ } else if (ghost instanceof Magnifier) {
28593
+ this.paintMagnifierGhost(ctx, ghost, proj, theme);
28260
28594
  } else this.paintOne(ctx, ghost, proj, theme);
28261
28595
  ctx.globalAlpha = 1;
28262
28596
  }
@@ -28390,6 +28724,10 @@ var DrawingPainter = class {
28390
28724
  this.paintLabel(ctx, d, proj, theme);
28391
28725
  return;
28392
28726
  }
28727
+ if (d instanceof Magnifier) {
28728
+ this.paintMagnifier(ctx, d, proj, theme);
28729
+ return;
28730
+ }
28393
28731
  if (d instanceof PatternDrawing) {
28394
28732
  this.paintPattern(ctx, d, proj, theme);
28395
28733
  return;
@@ -28882,6 +29220,216 @@ var DrawingPainter = class {
28882
29220
  ctx.textBaseline = "alphabetic";
28883
29221
  }
28884
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
+ }
28885
29433
  /** Paint a fixed-range volume profile: horizontal histogram rows (up/down split) anchored to
28886
29434
  * the left or right of the time span, optional VAH / VAL / POC levels across the range, and
28887
29435
  * optional developing POC / VA polylines. Recomputes from the two anchors on every paint. */
@@ -30297,8 +30845,9 @@ function ensureStyles5() {
30297
30845
  if (!existing) document.head.appendChild(s);
30298
30846
  }
30299
30847
  var DrawingSettingsPopup = class {
30300
- constructor(host, theme) {
30848
+ constructor(host, theme, chartBarMs = () => 0) {
30301
30849
  this.host = host;
30850
+ this.chartBarMs = chartBarMs;
30302
30851
  this.el = null;
30303
30852
  this.tipEl = null;
30304
30853
  // floating hover-label (above/below the toolbar)
@@ -30323,6 +30872,14 @@ var DrawingSettingsPopup = class {
30323
30872
  this.theme = theme;
30324
30873
  this.settingsDialog = new DrawingSettingsDialog(host, theme);
30325
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
+ }
30326
30883
  setTheme(theme) {
30327
30884
  this.theme = theme;
30328
30885
  this.settingsDialog.setTheme(theme);
@@ -30365,7 +30922,20 @@ var DrawingSettingsPopup = class {
30365
30922
  const sz = drawing.size ?? "normal";
30366
30923
  bar.appendChild(this.dropdown("Icon size", STAMP_SIZE_OPTIONS, sz, (s) => stampSizeIcon(s), (v) => actions.patch({ size: v }), { label: sizeLabel }));
30367
30924
  }
30368
- 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 })));
30369
30939
  if (paths.has("style.lineWidth")) {
30370
30940
  const wf = schema.fields.find((f) => f.path === "style.lineWidth");
30371
30941
  if (wf?.kind === "number" && (wf.min ?? 1) > 1) {
@@ -30687,6 +31257,7 @@ var DrawingSettingsPopup = class {
30687
31257
  this.colorPop = null;
30688
31258
  this.colorOwner = null;
30689
31259
  }
31260
+ opts.onClose?.();
30690
31261
  }
30691
31262
  });
30692
31263
  const el = pop.el;
@@ -30705,6 +31276,49 @@ var DrawingSettingsPopup = class {
30705
31276
  pop.show();
30706
31277
  return pop;
30707
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
+ }
30708
31322
  /** A floating list of one-shot actions (icon + label rows) opened by the kebab. */
30709
31323
  openActionMenu(anchor, rows) {
30710
31324
  this.menuPop = this.hostFloat(anchor, {
@@ -30815,10 +31429,13 @@ var DrawingSettingsPopup = class {
30815
31429
  let cur = current;
30816
31430
  const paint = (v) => {
30817
31431
  b.replaceChildren();
30818
- const ic = document.createElement("span");
30819
- ic.style.cssText = "display:flex;";
30820
- ic.innerHTML = sized(render(v));
30821
- 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
+ }
30822
31439
  if (opts.label && opts.labelInTrigger) {
30823
31440
  const tx = document.createElement("span");
30824
31441
  tx.textContent = opts.label(v);
@@ -30860,10 +31477,13 @@ var DrawingSettingsPopup = class {
30860
31477
  item.className = "vela-dpop-item";
30861
31478
  item.dataset.active = active ? "1" : "0";
30862
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;`;
30863
- const ic = document.createElement("span");
30864
- ic.style.cssText = "display:flex;flex:none;width:22px;justify-content:center;";
30865
- ic.innerHTML = sized(render(v), 18);
30866
- 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
+ }
30867
31487
  if (label) {
30868
31488
  const tx = document.createElement("span");
30869
31489
  tx.textContent = label(v);
@@ -31215,6 +31835,9 @@ var UserDrawingController = class {
31215
31835
  this.intentCb = null;
31216
31836
  /** Another chart's in-progress placement, mirrored here as a ghost (drawings sync). */
31217
31837
  this.externalGhost = null;
31838
+ /** Core-pushed series gateway (finer-timeframe bars for data-driven drawings). */
31839
+ this.seriesGw = null;
31840
+ this.seriesGwUnsub = null;
31218
31841
  /** Last draft fingerprint reported upstream — gates the per-render emission to actual changes. */
31219
31842
  this.lastDraftKey = null;
31220
31843
  this.measure = new MeasureOverlay();
@@ -31242,7 +31865,7 @@ var UserDrawingController = class {
31242
31865
  this.textEditor = null;
31243
31866
  this.painter = new DrawingPainter();
31244
31867
  this.ctx = canvas.getContext("2d");
31245
- this.popup = new DrawingSettingsPopup(overlayHost, deps.theme());
31868
+ this.popup = new DrawingSettingsPopup(overlayHost, deps.theme(), () => deps.chartBarMs());
31246
31869
  this.toolbar = new DrawingToolbar(
31247
31870
  toolbarHost,
31248
31871
  deps.theme(),
@@ -31300,6 +31923,22 @@ var UserDrawingController = class {
31300
31923
  const shown = this.toolbarVisible && !this.mobileLayout;
31301
31924
  this.deps.setToolbarGutter(shown ? this.toolbarCollapsed ? TOOLBAR_COLLAPSED_WIDTH : TOOLBAR_WIDTH : 0);
31302
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
+ }
31303
31942
  /** Core push: mirror (or clear) another chart's in-progress placement as a ghost. */
31304
31943
  setExternalGhost(doc) {
31305
31944
  this.externalGhost = doc ? deserializeDrawing(doc) : null;
@@ -31417,8 +32056,34 @@ var UserDrawingController = class {
31417
32056
  /** Should the drawing layer win this press (vs pan)? */
31418
32057
  claim(x, y) {
31419
32058
  if (this.measureMode || this.eraserMode) return true;
32059
+ if (this.magnifierChipAt(x, y)) return true;
31420
32060
  return this.interaction.claim(x, y);
31421
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
+ }
31422
32087
  /** Delete the (unlocked) drawing under the cursor. True when one was removed.
31423
32088
  * Shared by the eraser (click + drag) and the middle-click shortcut. */
31424
32089
  deleteAt(x, y) {
@@ -31464,6 +32129,13 @@ var UserDrawingController = class {
31464
32129
  this.render();
31465
32130
  return;
31466
32131
  }
32132
+ if (this.activeTool == null) {
32133
+ const chipOwner = this.magnifierChipAt(x, y);
32134
+ if (chipOwner) {
32135
+ this.openMagnifierChipMenu(chipOwner);
32136
+ return;
32137
+ }
32138
+ }
31467
32139
  this.interaction.down(x, y, snap, shift);
31468
32140
  }
31469
32141
  pointerMove(x, y, snap = "off", shift = false) {
@@ -31682,6 +32354,7 @@ var UserDrawingController = class {
31682
32354
  /** Cursor hint while hovering — `'pointer'` over a drawing/handle, else null. */
31683
32355
  cursorAt(x, y) {
31684
32356
  if (this.eraserMode) return "pointer";
32357
+ if (this.activeTool == null && this.magnifierChipAt(x, y)) return "pointer";
31685
32358
  return this.interaction.cursorAt(x, y);
31686
32359
  }
31687
32360
  /** Right-click: an explicit escape back to the pointer. Cancels an in-progress
@@ -31860,6 +32533,7 @@ var UserDrawingController = class {
31860
32533
  if (!sctx) continue;
31861
32534
  sctx.setTransform(dpr, 0, 0, dpr, 0, 0);
31862
32535
  sctx.clearRect(0, 0, canvas.width / dpr, canvas.height / dpr);
32536
+ this.painter.seriesLook = this.deps.seriesLook();
31863
32537
  this.painter.paintAll(sctx, drawings, proj, theme, EMPTY_TARGETS);
31864
32538
  const slices = out.get(paneId) ?? [];
31865
32539
  slices.push({ beforeZ, canvas });
@@ -31887,6 +32561,7 @@ var UserDrawingController = class {
31887
32561
  dragged: this.interaction.activeDragId(),
31888
32562
  mutedLabel: edited instanceof TextLabel ? edited.id : null
31889
32563
  };
32564
+ this.painter.seriesLook = this.deps.seriesLook();
31890
32565
  this.painter.paintAll(ctx, this.drawings.filter((d) => !this.isInterleaved(d)), proj, this.deps.theme(), targets);
31891
32566
  this.painter.paintHighlights(ctx, this.drawings.filter((d) => this.isInterleaved(d)), proj, handleIdsFor(targets));
31892
32567
  this.layoutTextEditor();
@@ -31894,6 +32569,10 @@ var UserDrawingController = class {
31894
32569
  if (ghost) this.painter.paintGhost(ctx, ghost, proj, this.deps.theme());
31895
32570
  if (this.externalGhost) this.painter.paintGhost(ctx, this.externalGhost, proj, this.deps.theme());
31896
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
+ }
31897
32576
  const markers = this.interaction.placingMarkers(proj);
31898
32577
  if (markers) this.painter.paintHandles(ctx, markers);
31899
32578
  const m = this.interaction.snapMarker();
@@ -31905,6 +32584,9 @@ var UserDrawingController = class {
31905
32584
  this.closeTextEditor();
31906
32585
  this.popup.destroy();
31907
32586
  this.toolbar.destroy();
32587
+ this.seriesGwUnsub?.();
32588
+ this.seriesGwUnsub = null;
32589
+ this.seriesGw = null;
31908
32590
  this.intentCb = null;
31909
32591
  this.drawings = [];
31910
32592
  }
@@ -32302,7 +32984,7 @@ function mergeSlices(indicator, user) {
32302
32984
  }
32303
32985
 
32304
32986
  // src/renderers/native/drawings/Projector.ts
32305
- function createProjector(coords, paneOf, paneIdAtY, barsInRange) {
32987
+ function createProjector(coords, paneOf, paneIdAtY, barsInRange, seriesInRange) {
32306
32988
  return {
32307
32989
  xOf: (time) => coords.timeToX(time),
32308
32990
  yOf: (price, paneId) => {
@@ -32323,6 +33005,7 @@ function createProjector(coords, paneOf, paneIdAtY, barsInRange) {
32323
33005
  },
32324
33006
  barsBetween: (t1, t2) => Math.abs(coords.timeToLogical(t2) - coords.timeToLogical(t1)),
32325
33007
  barsInRange: barsInRange ? (from, to) => barsInRange(from, to) : void 0,
33008
+ seriesInRange,
32326
33009
  width: coords.width,
32327
33010
  height: coords.height
32328
33011
  };
@@ -34192,6 +34875,23 @@ var NativeRenderer = class {
34192
34875
  seriesBoundaries: (paneId) => this.scene.seriesBoundaries(paneId),
34193
34876
  priceZ: (paneId) => paneId === PRICE_PANE_ID2 ? this.scene.candleZ : null,
34194
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,
34195
34895
  snap: (pt, paneId, mode, cursorPx) => this.snapToCandle(pt, paneId, mode, cursorPx),
34196
34896
  setSnapMode: (mode) => this.setSnapMode(mode),
34197
34897
  setToolbarGutter: (px) => this.setToolbarGutter(px)
@@ -35585,7 +36285,8 @@ var NativeRenderer = class {
35585
36285
  return p ? { scale: p.scale, bounds: p.bounds, collapsed: p.collapsed } : null;
35586
36286
  },
35587
36287
  (y) => this.paneNodeAtY(y)?.id ?? null,
35588
- (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
35589
36290
  );
35590
36291
  }
35591
36292
  /** OHLC bars whose open-time falls within `[from, to]` (inclusive) — the data a regression
@@ -36792,6 +37493,21 @@ function resolveElement(container) {
36792
37493
  // src/core/options.ts
36793
37494
  var normalizeSession = (v) => v === "regular" || v === "extended" ? v : void 0;
36794
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
+
36795
37511
  // src/widget/format.ts
36796
37512
  function decimalsFor(ref) {
36797
37513
  const a = Math.abs(ref);
@@ -36839,7 +37555,13 @@ var CSS21 = `
36839
37555
  padding: 2px 7px;
36840
37556
  margin-left: -7px;
36841
37557
  }
36842
- .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; }
36843
37565
  .vela-statusline .vela-sl-avatar {
36844
37566
  width: 18px;
36845
37567
  height: 18px;
@@ -36864,6 +37586,24 @@ var CSS21 = `
36864
37586
  * these are the pre-ink fallbacks only. */
36865
37587
  .vela-statusline .vela-sl-change[data-dir='up'] { color: var(--vela-up); }
36866
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); }
36867
37607
  /* Stack the TOP pane's legend below the status line (lower study panes stay put).
36868
37608
  * The renderer marks whichever legend sits at the plot's top edge \u2014 the price pane
36869
37609
  * normally, or a maximized study pane filling the plot \u2014 so the legend never merges
@@ -36973,7 +37713,12 @@ var Statusline = class {
36973
37713
  constructor(host, symbol, iconFor) {
36974
37714
  this.host = host;
36975
37715
  this.iconFor = iconFor;
36976
- 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;
36977
37722
  this.lastBar = null;
36978
37723
  this.hoverBar = null;
36979
37724
  this.unsubs = [];
@@ -36989,6 +37734,15 @@ var Statusline = class {
36989
37734
  /** Fit mode (multi-chart cells): one row, overflowing segments hidden — see {@link setFitMode}. */
36990
37735
  this.fitMode = false;
36991
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
+ };
36992
37746
  const doc = host.ownerDocument;
36993
37747
  injectStyles(STYLE_ID24, CSS21, doc);
36994
37748
  host.classList.add("vela-has-statusline");
@@ -37015,9 +37769,21 @@ var Statusline = class {
37015
37769
  this.ohlcEl.className = "vela-sl-ohlc";
37016
37770
  this.changeEl = doc.createElement("span");
37017
37771
  this.changeEl.className = "vela-sl-change";
37018
- 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);
37019
37784
  host.appendChild(this.el);
37020
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" });
37021
37787
  this.setMarketStatus("open");
37022
37788
  this.render();
37023
37789
  }
@@ -37027,6 +37793,7 @@ var Statusline = class {
37027
37793
  const fresh = tickerIconEl(this.el.ownerDocument, baseOfTicker(ticker), ticker, "vela-sl-avatar", this.iconFor?.(symbol));
37028
37794
  this.avatarEl.replaceWith(fresh);
37029
37795
  this.avatarEl = fresh;
37796
+ this.syncParts();
37030
37797
  this.fit();
37031
37798
  }
37032
37799
  /**
@@ -37054,20 +37821,25 @@ var Statusline = class {
37054
37821
  }
37055
37822
  /** Project the parts config onto the segments (the baseline fit() prunes from). */
37056
37823
  syncParts() {
37057
- this.symbolEl.style.display = this.parts.name ? "" : "none";
37058
- this.marketEl.style.display = this.parts.market ? "" : "none";
37059
- this.ohlcEl.style.display = this.parts.ohlc ? "" : "none";
37060
- 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";
37061
37832
  }
37062
37833
  /** Hide overflowing segments until the row fits its max-width (fit mode only). */
37063
37834
  fit() {
37064
37835
  if (!this.fitMode) return;
37065
37836
  this.syncParts();
37837
+ const seg = segmentVisibility(this.parts, this.chartHidden);
37066
37838
  const order = [
37067
- [this.ohlcEl, this.parts.ohlc],
37068
- [this.changeEl, this.parts.change],
37069
- [this.metaEl, true],
37070
- [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]
37071
37843
  ];
37072
37844
  for (const [el, shown] of order) {
37073
37845
  if (this.el.scrollWidth <= this.el.clientWidth) break;
@@ -37088,7 +37860,6 @@ var Statusline = class {
37088
37860
  this.readout = readout;
37089
37861
  this.render();
37090
37862
  }
37091
- /** Show/hide one part (the settings dialog's Status line tab drives these). */
37092
37863
  /** The "· BINANCE · 1h" segment after the symbol — venue first, then resolution. */
37093
37864
  setMeta(timeframe, provider) {
37094
37865
  this.metaEl.textContent = `${provider ? `\xB7 ${provider.toUpperCase()} ` : ""}\xB7 ${timeframeLabel(timeframe)}`;
@@ -37107,6 +37878,9 @@ var Statusline = class {
37107
37878
  });
37108
37879
  this.marketTip.setContent(MARKET_LABELS[status]);
37109
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}). */
37110
37884
  setPartVisible(part, visible) {
37111
37885
  this.parts[part] = visible;
37112
37886
  this.syncParts();
@@ -37115,6 +37889,47 @@ var Statusline = class {
37115
37889
  partVisible(part) {
37116
37890
  return this.parts[part];
37117
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
+ }
37118
37933
  /** (Re)bind to a chart instance — called after every widget rebuild. */
37119
37934
  onChart(chart) {
37120
37935
  this.detach();
@@ -37136,7 +37951,11 @@ var Statusline = class {
37136
37951
  this.detach();
37137
37952
  this.fitRO?.disconnect();
37138
37953
  this.fitRO = null;
37954
+ this.el.removeEventListener("contextmenu", this.onContextMenu);
37955
+ this.menu?.destroy();
37956
+ this.menu = null;
37139
37957
  this.marketTip.destroy();
37958
+ this.eyeTip.destroy();
37140
37959
  this.marketBubble.destroy();
37141
37960
  this.host.classList.remove("vela-has-statusline", "vela-sl-fit-host");
37142
37961
  this.el.remove();
@@ -37146,6 +37965,7 @@ var Statusline = class {
37146
37965
  this.unsubs = [];
37147
37966
  }
37148
37967
  render() {
37968
+ if (this.menuHooks) this.setChartHidden(!this.menuHooks.chartVisible());
37149
37969
  const bar = this.hoverBar ?? this.lastBar;
37150
37970
  if (!bar) {
37151
37971
  this.ohlcEl.replaceChildren();
@@ -37216,7 +38036,7 @@ var FETCH_BACK_MS = 4 * 864e5;
37216
38036
  var FETCH_AHEAD_MS = 10 * 864e5;
37217
38037
  var MIN_TIMER_MS = 15e3;
37218
38038
  var MAX_TIMER_MS = 36e5;
37219
- var RETRY_MS = 6e4;
38039
+ var RETRY_MS2 = 6e4;
37220
38040
  var MarketStatusTracker = class {
37221
38041
  constructor(onStatus) {
37222
38042
  this.onStatus = onStatus;
@@ -37253,7 +38073,7 @@ var MarketStatusTracker = class {
37253
38073
  ]);
37254
38074
  if (my !== this.epoch) return;
37255
38075
  if (!regular || !extended) {
37256
- this.arm(my, data, symbol, RETRY_MS);
38076
+ this.arm(my, data, symbol, RETRY_MS2);
37257
38077
  return;
37258
38078
  }
37259
38079
  const w = { regular, extended };
@@ -37964,7 +38784,14 @@ var ChartCell = class {
37964
38784
  this.destroyed = false;
37965
38785
  this.appTheme = deps.theme;
37966
38786
  const symbol = prefixedSymbol(seed);
37967
- 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
+ };
37968
38795
  const doc = gridHost.ownerDocument;
37969
38796
  this.host = doc.createElement("div");
37970
38797
  this.host.className = "vela-cell";
@@ -38052,6 +38879,11 @@ var ChartCell = class {
38052
38879
  this.statusline = deps.statusline ? new Statusline(this.host, symbol ?? "", (sym) => this.inner?.data.symbolIcon(sym)) : null;
38053
38880
  this.statusline?.setMeta(seed.timeframe ?? "60", this.state.provider ?? "");
38054
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
+ });
38055
38887
  this.marketStatus = this.statusline ? new MarketStatusTracker((s) => this.statusline?.setMarketStatus(s)) : null;
38056
38888
  void this.inner.data.ready().then(() => {
38057
38889
  if (this.inner && this.state.symbol) {
@@ -38259,7 +39091,7 @@ var ChartCell = class {
38259
39091
  kind: "select",
38260
39092
  label: "Bars to fetch",
38261
39093
  id: "bars",
38262
- options: ["500", "1000", "2000", "5000", "10000", "20000"],
39094
+ options: ["500", "1000", "2000", "5000", "10000", "20000", "50000", "60000", "80000", "100000"],
38263
39095
  get: () => String(this.state.bars ?? 1e3),
38264
39096
  set: (v) => {
38265
39097
  this.state.bars = Number(v);
@@ -38291,10 +39123,34 @@ var ChartCell = class {
38291
39123
  id: "status-line",
38292
39124
  rows: [
38293
39125
  { kind: "heading", label: "Status line", id: "parts" },
38294
- { kind: "toggle", label: "Symbol name", id: "name", get: () => sl.partVisible("name"), set: (v) => this.setStatuslinePart("name", v) },
38295
- { kind: "toggle", label: "Market status", id: "market", get: () => sl.partVisible("market"), set: (v) => this.setStatuslinePart("market", v) },
38296
- { kind: "toggle", label: "OHLC values", id: "ohlc", get: () => sl.partVisible("ohlc"), set: (v) => this.setStatuslinePart("ohlc", v) },
38297
- { 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
+ },
38298
39154
  { kind: "heading", label: "Indicators", id: "indicators" },
38299
39155
  {
38300
39156
  kind: "toggle",
@@ -38347,7 +39203,7 @@ var ChartCell = class {
38347
39203
  statusPrefs() {
38348
39204
  const sl = this.statusline;
38349
39205
  return {
38350
- 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,
38351
39207
  indicatorTitles: this.indicatorTitlesOn,
38352
39208
  indicatorValues: this.indicatorValuesOn
38353
39209
  };
@@ -38521,7 +39377,8 @@ var ChartCell = class {
38521
39377
  if (this.manifest.length > 0) {
38522
39378
  for (const item of led.manifest) {
38523
39379
  const entry = this.manifest.find((e) => e.name === ledgerEntryName(item));
38524
- if (entry) this.addManifestInstance(entry, { record: false, ...typeof item === "object" ? { inputs: item.inputs, props: item.props } : {} });
39380
+ if (entry)
39381
+ this.addManifestInstance(entry, { record: false, ...typeof item === "object" ? { inputs: item.inputs, props: item.props } : {} });
38525
39382
  }
38526
39383
  this.pendingManifestNames = null;
38527
39384
  } else if (!this.deps.manifestSettled()) {
@@ -38570,7 +39427,10 @@ var ChartCell = class {
38570
39427
  * a persistence handler's `restore` runs silently, a user-driven call records.
38571
39428
  */
38572
39429
  addExternalIndicator(entry) {
38573
- this.addManifestInstance({ ...entry, enabled: true }, { external: true, ...entry.inputs ? { inputs: entry.inputs } : {}, ...entry.props ? { props: entry.props } : {} });
39430
+ this.addManifestInstance(
39431
+ { ...entry, enabled: true },
39432
+ { external: true, ...entry.inputs ? { inputs: entry.inputs } : {}, ...entry.props ? { props: entry.props } : {} }
39433
+ );
38574
39434
  }
38575
39435
  /** Add ONE instance of a manifest entry (repeatable — duplicates are legitimate). */
38576
39436
  addManifestInstance(entry, opts = {}) {
@@ -39076,7 +39936,7 @@ var SplitterLayer = class {
39076
39936
  };
39077
39937
 
39078
39938
  // src/workspace/VelaWorkspace.ts
39079
- var DEFAULT_TIMEFRAMES = ["1", "5", "15", "60", "240", "D", "W"];
39939
+ var DEFAULT_TIMEFRAMES = ["1", "5", "15", "30", "60", "240", "D", "W", "M"];
39080
39940
  var GAP_PX = 2;
39081
39941
  var POOL_CAP = 16;
39082
39942
  var TIME_AXIS_H4 = 22;