@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
@@ -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
  }
@@ -11187,6 +11189,111 @@ function roundFloat(n) {
11187
11189
  return Math.round(n * 1e8) / 1e8;
11188
11190
  }
11189
11191
 
11192
+ // src/core/drawings/types/Magnifier.ts
11193
+ var MAGNIFIER_TIMEFRAME_OPTIONS = [
11194
+ { value: "auto", label: "Auto", ms: 0 },
11195
+ { value: "1", label: "1m", ms: 6e4 },
11196
+ { value: "5", label: "5m", ms: 3e5 },
11197
+ { value: "15", label: "15m", ms: 9e5 },
11198
+ { value: "30", label: "30m", ms: 18e5 },
11199
+ { value: "60", label: "1h", ms: 36e5 },
11200
+ { value: "240", label: "4h", ms: 144e5 },
11201
+ { value: "D", label: "1D", ms: 864e5 }
11202
+ ];
11203
+ function magnifierTimeframeLabel(value) {
11204
+ const opt = MAGNIFIER_TIMEFRAME_OPTIONS.find((o) => o.value === value);
11205
+ if (opt) return opt.label;
11206
+ const n = Number(value);
11207
+ if (Number.isFinite(n) && n > 0) {
11208
+ if (n % 1440 === 0) return `${n / 1440}D`;
11209
+ if (n % 60 === 0) return `${n / 60}h`;
11210
+ return `${n}m`;
11211
+ }
11212
+ return value;
11213
+ }
11214
+ function defaultMagnifierStyle() {
11215
+ return { timeframe: "auto", upColor: "", downColor: "" };
11216
+ }
11217
+ var Magnifier = class extends Drawing {
11218
+ constructor(init) {
11219
+ super(init);
11220
+ this.type = "magnifier";
11221
+ /** Pixel rect of the timeframe chip as painted last frame, caret included — the chip is
11222
+ * an interactive dropdown trigger, so the interaction layer needs the exact rect the
11223
+ * painter measured. Renderer-transient: never serialized, null while unpainted. */
11224
+ this.chipRect = null;
11225
+ if (!this.magnifier) this.magnifier = defaultMagnifierStyle();
11226
+ }
11227
+ anchorSchema() {
11228
+ return { min: 2, max: 2, slots: [{ role: "c1", free: "both" }, { role: "c2", free: "both" }] };
11229
+ }
11230
+ placementMode() {
11231
+ return "drag";
11232
+ }
11233
+ /** The pixel rectangle between the two corner anchors (painter + hit-test share it). */
11234
+ rect(proj) {
11235
+ const a = this.anchors[0];
11236
+ const b = this.anchors[1];
11237
+ if (!a || !b) return null;
11238
+ const ya = proj.yOf(a.price, this.paneId);
11239
+ const yb = proj.yOf(b.price, this.paneId);
11240
+ if (ya == null || yb == null) return null;
11241
+ return { x1: proj.xOf(a.time), y1: ya, x2: proj.xOf(b.time), y2: yb };
11242
+ }
11243
+ hitTest(px, py, proj, tol) {
11244
+ const r = this.rect(proj);
11245
+ if (!r) return false;
11246
+ if (pointInBox(px, py, r.x1, r.y1, r.x2, r.y2)) return true;
11247
+ const edges = [
11248
+ [r.x1, r.y1, r.x2, r.y1],
11249
+ [r.x2, r.y1, r.x2, r.y2],
11250
+ [r.x2, r.y2, r.x1, r.y2],
11251
+ [r.x1, r.y2, r.x1, r.y1]
11252
+ ];
11253
+ return edges.some((e) => distToSegment(px, py, e[0], e[1], e[2], e[3]) <= tol);
11254
+ }
11255
+ handlePoints(proj) {
11256
+ const r = this.rect(proj);
11257
+ return r ? [[r.x1, r.y1], [r.x2, r.y2]] : [];
11258
+ }
11259
+ hitHandle(px, py, proj, tol) {
11260
+ return handleAt(px, py, this.handlePoints(proj), tol + 3);
11261
+ }
11262
+ bounds(proj) {
11263
+ const r = this.rect(proj);
11264
+ if (!r) return null;
11265
+ 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) };
11266
+ }
11267
+ priceRange() {
11268
+ const a = this.anchors[0];
11269
+ const b = this.anchors[1];
11270
+ if (!a || !b) return null;
11271
+ return { min: Math.min(a.price, b.price), max: Math.max(a.price, b.price) };
11272
+ }
11273
+ schema() {
11274
+ return {
11275
+ fields: [
11276
+ {
11277
+ path: "magnifier.timeframe",
11278
+ label: "Timeframe",
11279
+ kind: "select",
11280
+ options: MAGNIFIER_TIMEFRAME_OPTIONS,
11281
+ group: "behavior"
11282
+ },
11283
+ ...LINE_FIELDS.map((f) => ({ ...f, label: f.label.replace("Line", "Border") })),
11284
+ { path: "magnifier.upColor", label: "Up candles", kind: "color", group: "fill" },
11285
+ { path: "magnifier.downColor", label: "Down candles", kind: "color", group: "fill" }
11286
+ ]
11287
+ };
11288
+ }
11289
+ writeProps() {
11290
+ return { ...this.magnifier };
11291
+ }
11292
+ readProps(props) {
11293
+ this.magnifier = { ...defaultMagnifierStyle(), ...props };
11294
+ }
11295
+ };
11296
+
11190
11297
  // src/core/drawings/registry.ts
11191
11298
  var REGISTRY = /* @__PURE__ */ new Map();
11192
11299
  function registerDrawingType(meta) {
@@ -11846,6 +11953,22 @@ registerDrawingType({
11846
11953
  defaultStyle: { lineColor: DEFAULT_DRAWING_COLOR, lineWidth: 1, lineStyle: "solid" },
11847
11954
  create: (init) => new PositionTool(init)
11848
11955
  });
11956
+ var MAGNIFIER_ICON = svg24(
11957
+ '<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"/>'
11958
+ );
11959
+ registerDrawingType({
11960
+ type: "magnifier",
11961
+ group: "measure",
11962
+ label: "Magnifier",
11963
+ icon: MAGNIFIER_ICON,
11964
+ // An empty border color means the THEME's contrast ink (white on dark, black on
11965
+ // light), resolved at paint time so it follows theme switches; a user pick wins.
11966
+ defaultStyle: { lineColor: "", lineWidth: 1, lineStyle: "solid" },
11967
+ coversSeries: true,
11968
+ // the inset's backdrop must sit over the base candles it replaces
11969
+ placementHint: "Drag an area on the chart to view it at a lower timeframe",
11970
+ create: (init) => new Magnifier(init)
11971
+ });
11849
11972
  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"/>');
11850
11973
  registerDrawingType({
11851
11974
  type: "anchoredvwap",
@@ -16051,7 +16174,7 @@ var GEOMETRY_TYPES = ["dedekind", "sonic", "supersonic", "goldensonic", "goldens
16051
16174
  var PATTERN_TYPES = ["xabcd", "abcd", "headshoulders"];
16052
16175
  var ELLIOTT_TYPES = ["elliottimpulse", "elliottcorrection"];
16053
16176
  var HARMONIC_TYPES = ["gartley", "bat", "butterfly", "crab", "shark", "cypher"];
16054
- var MEASUREMENT_TYPES = ["position", "datepricerange"];
16177
+ var MEASUREMENT_TYPES = ["position", "datepricerange", "magnifier"];
16055
16178
  var VOLUME_TYPES = ["anchoredvwap", "fixedrangevp"];
16056
16179
  var BRUSH_TYPES = ["freehand", "highlighter"];
16057
16180
  var ARROW_TYPES = ["arrow", "arrowmarkup", "arrowmarkdown"];
@@ -16187,7 +16310,7 @@ function buildToolbar(option) {
16187
16310
 
16188
16311
  // src/core/drawings/DrawingController.ts
16189
16312
  var DrawingController = class {
16190
- constructor(renderer, events, option) {
16313
+ constructor(renderer, events, option, seriesGateway) {
16191
16314
  this.events = events;
16192
16315
  this.store = new DrawingStore();
16193
16316
  this.history = new DrawingHistory();
@@ -16213,6 +16336,7 @@ var DrawingController = class {
16213
16336
  const { definition, visible } = buildToolbar(option);
16214
16337
  this.port.setToolbar(definition);
16215
16338
  this.port.showToolbar(visible);
16339
+ if (seriesGateway) this.port.setSeriesGateway?.(seriesGateway);
16216
16340
  this.subs.push(this.port.onDrawingIntent((i) => this.onIntent(i)));
16217
16341
  this.subs.push(this.store.onChange(() => this.sync()));
16218
16342
  }
@@ -16318,7 +16442,7 @@ var DrawingController = class {
16318
16442
  style,
16319
16443
  text: init.text,
16320
16444
  props: init.props,
16321
- zIndex: init.zIndex ?? this.startZ(init.paneId ?? "price")
16445
+ zIndex: init.zIndex ?? this.startZ(type, init.paneId ?? "price")
16322
16446
  });
16323
16447
  if (!d) return null;
16324
16448
  this.history.record(this.store.serialize());
@@ -16393,10 +16517,14 @@ var DrawingController = class {
16393
16517
  * (falling back to just under the pane's top series where there is no price — a study
16394
16518
  * pane). Half a key down never ties a series; drawings tying each other paint in insertion
16395
16519
  * order, so consecutive new drawings still stack newest-in-front. Undefined without a
16396
- * shared z space — the store then places it over the other drawings, its own layer's top. */
16397
- startZ(paneId) {
16520
+ * shared z space — the store then places it over the other drawings, its own layer's top.
16521
+ * A type that COVERS the series (an opaque inset, `coversSeries`) instead starts just
16522
+ * above the whole stack — under the candles its content would be buried. */
16523
+ startZ(type, paneId) {
16398
16524
  const range = this.port?.stackRange?.(paneId);
16399
- return range ? (range.price ?? range.front) - 0.5 : void 0;
16525
+ if (!range) return void 0;
16526
+ if (getDrawingType(type)?.coversSeries) return range.front + 0.5;
16527
+ return (range.price ?? range.front) - 0.5;
16400
16528
  }
16401
16529
  /** Programmatically select drawings (host UI → chart): shows the on-chart handles + toolbar.
16402
16530
  * `additive` toggles membership (matching shift-click) instead of replacing. */
@@ -16541,7 +16669,7 @@ var DrawingController = class {
16541
16669
  const style = last ? { ...i.doc.style, ...last } : i.doc.style;
16542
16670
  const d = deserializeDrawing({ ...i.doc, id: this.store.nextId(), style });
16543
16671
  if (!d) return;
16544
- if (!d.zIndex) d.zIndex = this.startZ(d.paneId) ?? 0;
16672
+ if (!d.zIndex) d.zIndex = this.startZ(d.type, d.paneId) ?? 0;
16545
16673
  this.history.record(before);
16546
16674
  this.store.add(d);
16547
16675
  this.captureStyle(d.id);
@@ -18073,6 +18201,180 @@ function presetToRange(preset, bars) {
18073
18201
  return { from: Math.max(first, from), to: last };
18074
18202
  }
18075
18203
 
18204
+ // src/core/engine/DrawingSeriesService.ts
18205
+ var MAX_BARS = 5e3;
18206
+ var PAD_FRAC = 0.25;
18207
+ var MAX_ENTRIES = 16;
18208
+ var RETRY_MS = 15e3;
18209
+ var AUTO_STEPS = ["240", "60", "30", "15", "5", "1"];
18210
+ var DrawingSeriesService = class {
18211
+ constructor(deps) {
18212
+ this.deps = deps;
18213
+ /** Cached windows per `market|timeframe` key, newest-used last (LRU across keys). */
18214
+ this.cache = /* @__PURE__ */ new Map();
18215
+ this.listeners = /* @__PURE__ */ new Set();
18216
+ }
18217
+ seriesInRange(timeframe, from, to) {
18218
+ if (!this.deps.canFetch()) return { state: "unavailable", reason: "no-source" };
18219
+ const resolved = this.resolveTimeframe(timeframe);
18220
+ if (typeof resolved !== "string") return { state: "unavailable", reason: resolved.reason };
18221
+ const barMs = timeframeToMs(resolved);
18222
+ const lo = Math.min(from, to);
18223
+ const hi = Math.max(from, to);
18224
+ if (!(hi > lo) || !(barMs > 0)) return { state: "unavailable", reason: "not-lower" };
18225
+ if ((hi - lo) / barMs > MAX_BARS) return { state: "unavailable", reason: "too-wide" };
18226
+ const key = `${this.deps.marketKey()}|${resolved}`;
18227
+ const entries = this.cache.get(key) ?? [];
18228
+ const covering = entries.find((e) => e.from <= lo && e.to >= hi);
18229
+ if (covering) {
18230
+ if (covering.pending) return this.loading(entries, resolved, barMs, lo, hi);
18231
+ if (covering.failedAt > 0) {
18232
+ if (Date.now() - covering.failedAt < RETRY_MS) return this.loading(entries, resolved, barMs, lo, hi);
18233
+ entries.splice(entries.indexOf(covering), 1);
18234
+ } else {
18235
+ this.maybeRefresh(key, covering, resolved, barMs, hi);
18236
+ return { state: "ready", bars: this.slice(covering.bars, lo, hi), timeframe: resolved, barMs };
18237
+ }
18238
+ }
18239
+ this.fetchWindow(key, entries, resolved, lo, hi);
18240
+ return this.loading(entries, resolved, barMs, lo, hi);
18241
+ }
18242
+ onUpdate(listener) {
18243
+ this.listeners.add(listener);
18244
+ return () => this.listeners.delete(listener);
18245
+ }
18246
+ // ── internals ──
18247
+ /** `'auto'` → the largest standard step at least 4× finer than the chart (else the finest
18248
+ * step still below it); an explicit timeframe passes only when strictly finer. Failures
18249
+ * distinguish "this pick isn't lower" from "NOTHING lower exists" (the chart is already
18250
+ * at the finest offered step) so the consumer can word its notice honestly. */
18251
+ resolveTimeframe(timeframe) {
18252
+ const chartMs = timeframeToMs(this.deps.chartTimeframe());
18253
+ const finest = AUTO_STEPS[AUTO_STEPS.length - 1];
18254
+ if (timeframeToMs(finest) >= chartMs) return { reason: "none-lower" };
18255
+ const tf = timeframe.trim() || "auto";
18256
+ if (tf === "auto") {
18257
+ for (const step of AUTO_STEPS) {
18258
+ if (timeframeToMs(step) <= chartMs / 4) return step;
18259
+ }
18260
+ return finest;
18261
+ }
18262
+ return timeframeToMs(tf) < chartMs ? tf : { reason: "not-lower" };
18263
+ }
18264
+ /** The `loading` answer, carrying best-effort PARTIAL bars from settled overlapping
18265
+ * windows — a widened window keeps painting what it already has while it fetches. */
18266
+ loading(entries, timeframe, barMs, lo, hi) {
18267
+ const partial = /* @__PURE__ */ new Map();
18268
+ for (const e of entries) {
18269
+ if (e.pending || e.failedAt > 0) continue;
18270
+ if (e.to < lo || e.from > hi) continue;
18271
+ for (const b of this.slice(e.bars, lo, hi)) partial.set(b.time, b);
18272
+ }
18273
+ if (partial.size === 0) return { state: "loading", timeframe, barMs };
18274
+ const bars = [...partial.values()].sort((a, b) => a.time - b.time);
18275
+ return { state: "loading", timeframe, barMs, bars };
18276
+ }
18277
+ /** Kick ONE background fetch for the padded window. Any OVERLAPPING in-flight fetch
18278
+ * defers this one (a corner drag repaints per pointer move — kicking a window per
18279
+ * frame would spam the provider); when it lands, the next paint re-evaluates. */
18280
+ fetchWindow(key, entries, timeframe, lo, hi) {
18281
+ if (entries.some((e) => e.pending && e.to >= lo && e.from <= hi)) return;
18282
+ const pad = (hi - lo) * PAD_FRAC;
18283
+ const entry = { from: lo - pad, to: hi + pad, bars: [], fetchedAt: 0, pending: true, failedAt: 0 };
18284
+ entries.push(entry);
18285
+ this.cache.set(key, entries);
18286
+ this.evict();
18287
+ void this.deps.fetchBars(timeframe, { from: entry.from, to: entry.to }).then((bars) => {
18288
+ entry.bars = bars;
18289
+ entry.fetchedAt = Date.now();
18290
+ entry.pending = false;
18291
+ this.absorbOverlaps(key, entry);
18292
+ this.fire();
18293
+ }).catch(() => {
18294
+ entry.pending = false;
18295
+ entry.failedAt = Date.now();
18296
+ this.fire();
18297
+ });
18298
+ }
18299
+ /** A window whose right edge reaches the newest fetched bar refreshes at most once per
18300
+ * bar interval — new closed bars ride the feed's cache, only the live tail re-fetches. */
18301
+ maybeRefresh(key, entry, timeframe, barMs, hi) {
18302
+ if (entry.pending) return;
18303
+ const lastBar = entry.bars.length > 0 ? entry.bars[entry.bars.length - 1].time : entry.from;
18304
+ if (hi < lastBar) return;
18305
+ if (Date.now() - entry.fetchedAt < barMs) return;
18306
+ entry.pending = true;
18307
+ void this.deps.fetchBars(timeframe, { from: entry.from, to: entry.to }).then((bars) => {
18308
+ entry.bars = bars;
18309
+ entry.fetchedAt = Date.now();
18310
+ entry.pending = false;
18311
+ this.fire();
18312
+ }).catch(() => {
18313
+ entry.pending = false;
18314
+ entry.fetchedAt = Date.now();
18315
+ });
18316
+ }
18317
+ /** Merge windows that overlap `entry` into it (dedupe by bar time) so a key's list
18318
+ * converges instead of accumulating slivers. */
18319
+ absorbOverlaps(key, entry) {
18320
+ const entries = this.cache.get(key);
18321
+ if (!entries) return;
18322
+ for (let i = entries.length - 1; i >= 0; i -= 1) {
18323
+ const other = entries[i];
18324
+ if (other === entry || other.pending || other.failedAt > 0) continue;
18325
+ if (other.to < entry.from || other.from > entry.to) continue;
18326
+ const byTime = /* @__PURE__ */ new Map();
18327
+ for (const b of other.bars) byTime.set(b.time, b);
18328
+ for (const b of entry.bars) byTime.set(b.time, b);
18329
+ entry.bars = [...byTime.values()].sort((a, b) => a.time - b.time);
18330
+ entry.from = Math.min(entry.from, other.from);
18331
+ entry.to = Math.max(entry.to, other.to);
18332
+ entry.fetchedAt = Math.min(entry.fetchedAt, other.fetchedAt || entry.fetchedAt);
18333
+ entries.splice(i, 1);
18334
+ }
18335
+ }
18336
+ /** Drop the oldest settled windows once the global count passes {@link MAX_ENTRIES}. */
18337
+ evict() {
18338
+ let total = 0;
18339
+ for (const entries of this.cache.values()) total += entries.length;
18340
+ while (total > MAX_ENTRIES) {
18341
+ let oldestKey = null;
18342
+ let oldestIdx = -1;
18343
+ let oldestAt = Infinity;
18344
+ for (const [key, entries2] of this.cache) {
18345
+ for (let i = 0; i < entries2.length; i += 1) {
18346
+ const e = entries2[i];
18347
+ if (e.pending) continue;
18348
+ const at = e.fetchedAt || e.failedAt;
18349
+ if (at < oldestAt) {
18350
+ oldestKey = key;
18351
+ oldestIdx = i;
18352
+ oldestAt = at;
18353
+ }
18354
+ }
18355
+ }
18356
+ if (oldestKey == null) return;
18357
+ const entries = this.cache.get(oldestKey);
18358
+ entries.splice(oldestIdx, 1);
18359
+ if (entries.length === 0) this.cache.delete(oldestKey);
18360
+ total -= 1;
18361
+ }
18362
+ }
18363
+ /** Bars whose open time falls within `[lo, hi]` (ascending input → linear scan is fine). */
18364
+ slice(bars, lo, hi) {
18365
+ const out = [];
18366
+ for (const b of bars) {
18367
+ if (b.time < lo) continue;
18368
+ if (b.time > hi) break;
18369
+ out.push(b);
18370
+ }
18371
+ return out;
18372
+ }
18373
+ fire() {
18374
+ for (const l of [...this.listeners]) l();
18375
+ }
18376
+ };
18377
+
18076
18378
  // src/core/price-styles/BarTransform.ts
18077
18379
  function barTransformFor(style) {
18078
18380
  return chartType(style)?.barTransform ?? null;
@@ -18209,7 +18511,13 @@ var EngineOrchestrator = class _EngineOrchestrator {
18209
18511
  const initialStyle = this.renderer.readFeature("priceStyle");
18210
18512
  if (typeof initialStyle === "string") this.priceStyle = initialStyle;
18211
18513
  this.barTransform = barTransformFor(initialStyle);
18212
- this.drawings = new DrawingController(this.renderer, this.events, config.drawings);
18514
+ const drawingSeries = new DrawingSeriesService({
18515
+ fetchBars: (tf, range) => this.fetchSeries(this.config.market.symbol ?? "", tf, range),
18516
+ canFetch: () => !!this.feed.loadRange && !this.config.market.data?.length && !!this.config.market.symbol,
18517
+ chartTimeframe: () => this.config.market.timeframe ?? "60",
18518
+ marketKey: () => `${this.config.market.symbol ?? ""}|${this.config.market.session ?? ""}`
18519
+ });
18520
+ this.drawings = new DrawingController(this.renderer, this.events, config.drawings, drawingSeries);
18213
18521
  this.unresolvedUnsub = this.feed.onUnresolved?.((info) => {
18214
18522
  this.endLoad();
18215
18523
  this.events.emit("data:unresolved", info);
@@ -19202,8 +19510,8 @@ var EngineOrchestrator = class _EngineOrchestrator {
19202
19510
  onModel: (model) => {
19203
19511
  const first = !record.announced;
19204
19512
  const cause = record.pendingCause ?? "history";
19513
+ if (!this.applyModel(id, model)) return;
19205
19514
  record.pendingCause = void 0;
19206
- this.applyModel(id, model);
19207
19515
  this.emitContextChanged(id);
19208
19516
  this.emitScriptRun(id, cause, first);
19209
19517
  },
@@ -19496,10 +19804,16 @@ var EngineOrchestrator = class _EngineOrchestrator {
19496
19804
  * Apply an emitted model. First emission mounts (and routes the pane); a pending
19497
19805
  * structural change (after an input edit) remounts idempotently; everything else
19498
19806
  * (live tick / viewport re-run) value-patches.
19807
+ *
19808
+ * Returns false when the model was DEFERRED — an output-free model arriving while
19809
+ * the record is still loading and the chart has no bars (see below); every other
19810
+ * outcome, including the hidden drop, returns true so the caller's event semantics
19811
+ * stay unchanged.
19499
19812
  */
19500
19813
  applyModel(id, model) {
19501
19814
  const record = this.registry.get(id);
19502
- if (!record || record.hidden) return;
19815
+ if (!record || record.hidden) return true;
19816
+ if (record.loading && this.bars.length === 0 && !_EngineOrchestrator.modelHasOutput(model)) return false;
19503
19817
  const handle = this.handles.get(id);
19504
19818
  if (!record.renderHandle) {
19505
19819
  const paneId2 = this.routePane(id, model, record.options ?? {});
@@ -19509,7 +19823,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
19509
19823
  record.renderHandle = this.renderer.mountIndicator(model);
19510
19824
  record.pendingStructural = false;
19511
19825
  this.announce(record, handle);
19512
- return;
19826
+ return true;
19513
19827
  }
19514
19828
  let paneId = record.model?.paneId ?? "price";
19515
19829
  const prevOwnScale = record.model?.ownScale === true;
@@ -19537,6 +19851,11 @@ var EngineOrchestrator = class _EngineOrchestrator {
19537
19851
  }
19538
19852
  if (record.loading) this.setLoading(record, false);
19539
19853
  this.announce(record, handle);
19854
+ return true;
19855
+ }
19856
+ /** True when the model carries ANY executed output — series, drawings, bar colors, or trades. */
19857
+ static modelHasOutput(model) {
19858
+ 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;
19540
19859
  }
19541
19860
  routePane(id, model, options) {
19542
19861
  if (options.pane === "new") return `pane-${id}`;
@@ -25671,7 +25990,9 @@ var DrawingSceneRenderer = class {
25671
25990
  const lo = Math.min(from, to);
25672
25991
  const hi = Math.max(from, to);
25673
25992
  const visible = (a, b, extend) => {
25674
- if (extend !== "none") return true;
25993
+ if (extend === "both") return true;
25994
+ if (extend === "left") return Math.max(a, b) >= lo;
25995
+ if (extend === "right") return Math.min(a, b) <= hi;
25675
25996
  return Math.max(a, b) >= lo && Math.min(a, b) <= hi;
25676
25997
  };
25677
25998
  let min = Infinity;
@@ -28145,6 +28466,17 @@ var DrawingPainter = class {
28145
28466
  constructor() {
28146
28467
  /** The current `paintAll` call's interaction state, visible to the per-type painters. */
28147
28468
  this.targets = {};
28469
+ /** The chart's active series LOOK — style + resolved series colors — pushed by the
28470
+ * controller before each paint. The magnifier's inset mirrors both: candles/bars/line/
28471
+ * area restyle the paint (bar-transform styles like Heikin Ashi transform the fetched
28472
+ * bars; unknown/custom styles fall back to candles), and the colors default to the main
28473
+ * series' own so the inset reads as a finer copy of the chart. */
28474
+ this.seriesLook = {
28475
+ style: "candles",
28476
+ upColor: BULLISH,
28477
+ downColor: BEARISH,
28478
+ lineColor: BULLISH
28479
+ };
28148
28480
  }
28149
28481
  /** Paint every visible drawing, then selection handles for the targeted ones.
28150
28482
  * Each drawing is clipped to its own pane's rect (and skipped entirely while that pane
@@ -28189,6 +28521,8 @@ var DrawingPainter = class {
28189
28521
  ctx.globalAlpha = GHOST_ALPHA;
28190
28522
  if (ghost instanceof RegressionChannel || ghost instanceof FixedRangeVolumeProfile) {
28191
28523
  this.paintTimeSpanGhost(ctx, ghost, proj);
28524
+ } else if (ghost instanceof Magnifier) {
28525
+ this.paintMagnifierGhost(ctx, ghost, proj, theme);
28192
28526
  } else this.paintOne(ctx, ghost, proj, theme);
28193
28527
  ctx.globalAlpha = 1;
28194
28528
  }
@@ -28322,6 +28656,10 @@ var DrawingPainter = class {
28322
28656
  this.paintLabel(ctx, d, proj, theme);
28323
28657
  return;
28324
28658
  }
28659
+ if (d instanceof Magnifier) {
28660
+ this.paintMagnifier(ctx, d, proj, theme);
28661
+ return;
28662
+ }
28325
28663
  if (d instanceof PatternDrawing) {
28326
28664
  this.paintPattern(ctx, d, proj, theme);
28327
28665
  return;
@@ -28814,6 +29152,216 @@ var DrawingPainter = class {
28814
29152
  ctx.textBaseline = "alphabetic";
28815
29153
  }
28816
29154
  }
29155
+ /** Placement preview for the magnifier: a dashed rectangle outline only — no backdrop and
29156
+ * no series read, so dragging the area open never kicks a fetch per cursor move. */
29157
+ paintMagnifierGhost(ctx, d, proj, theme) {
29158
+ const r = d.rect(proj);
29159
+ if (!r) return;
29160
+ ctx.save();
29161
+ ctx.strokeStyle = d.style.lineColor || contrastColor(theme.background);
29162
+ ctx.lineWidth = 1;
29163
+ ctx.setLineDash([4, 4]);
29164
+ 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));
29165
+ ctx.restore();
29166
+ }
29167
+ /** Paint a magnifier: an opaque theme-background inset whose interior shows the chart's
29168
+ * market at a finer timeframe — candles at their true time/price positions, clipped to
29169
+ * the rectangle. Bars come through `Projector.seriesInRange` (cache read; `loading` and
29170
+ * `unavailable` states paint a short notice instead). The lower-timeframe candles shift
29171
+ * half a chart bar LEFT of their raw time pixel so each chart candle's visual cell —
29172
+ * centered on its open time — subdivides in place. */
29173
+ paintMagnifier(ctx, d, proj, theme) {
29174
+ const r = d.rect(proj);
29175
+ const a = d.anchors[0];
29176
+ const b = d.anchors[1];
29177
+ if (!r || !a || !b) return;
29178
+ const x0 = Math.min(r.x1, r.x2);
29179
+ const x1 = Math.max(r.x1, r.x2);
29180
+ const y0 = Math.min(r.y1, r.y2);
29181
+ const y1 = Math.max(r.y1, r.y2);
29182
+ const w = x1 - x0;
29183
+ const h = y1 - y0;
29184
+ ctx.save();
29185
+ ctx.globalAlpha = 1;
29186
+ ctx.fillStyle = theme.background;
29187
+ ctx.fillRect(x0, y0, w, h);
29188
+ ctx.restore();
29189
+ const from = Math.min(a.time, b.time);
29190
+ const to = Math.max(a.time, b.time);
29191
+ const chartBars = proj.barsBetween ? proj.barsBetween(from, to) : 0;
29192
+ const chartMs = chartBars > 0 ? (to - from) / chartBars : 0;
29193
+ const res = proj.seriesInRange && chartMs > 0 && w > 1 && h > 1 ? proj.seriesInRange(d.magnifier.timeframe, from, to + chartMs) : void 0;
29194
+ let seriesBars = res?.state === "ready" || res?.state === "loading" ? res.bars ?? [] : [];
29195
+ if (res && (res.state === "ready" || res.state === "loading") && seriesBars.length > 0) {
29196
+ const look = this.seriesLook;
29197
+ const transform = look.style !== "candles" ? barTransformFor(look.style) : null;
29198
+ if (transform) seriesBars = transform.full(seriesBars);
29199
+ const mode = look.style === "bars" ? "bars" : look.style === "line" || look.style === "baseline" ? "line" : look.style === "area" ? "area" : "candles";
29200
+ const halfPitch = (proj.xOf(from + chartMs) - proj.xOf(from)) / 2;
29201
+ ctx.save();
29202
+ ctx.beginPath();
29203
+ ctx.rect(x0, y0, w, h);
29204
+ ctx.clip();
29205
+ if (mode === "line" || mode === "area") {
29206
+ this.paintMagnifierLine(ctx, d, proj, seriesBars, res.barMs, halfPitch, y1, mode === "area", d.magnifier.upColor || look.lineColor);
29207
+ } else {
29208
+ this.paintMagnifierBars(ctx, d, proj, seriesBars, res.barMs, halfPitch, x0, x1, mode, d.magnifier.upColor || look.upColor, d.magnifier.downColor || look.downColor);
29209
+ }
29210
+ ctx.restore();
29211
+ } else if (res) {
29212
+ 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";
29213
+ this.paintMagnifierNotice(ctx, notice, x0, y0, w, h, theme);
29214
+ }
29215
+ ctx.save();
29216
+ ctx.strokeStyle = d.style.lineColor || contrastColor(theme.background);
29217
+ ctx.lineWidth = d.style.lineWidth || 1;
29218
+ ctx.setLineDash(dashPattern(d.style.lineStyle, d.style.lineWidth || 1));
29219
+ ctx.strokeRect(x0, y0, w, h);
29220
+ ctx.restore();
29221
+ if (w > 44) {
29222
+ const label = magnifierTimeframeLabel(res?.state === "ready" || res?.state === "loading" ? res.timeframe : d.magnifier.timeframe);
29223
+ const chipH = 17;
29224
+ const gap = 4;
29225
+ const pane = proj.paneRect?.(d.paneId);
29226
+ const paneBottom = pane ? pane.top + pane.height : proj.height;
29227
+ const below = y1 + gap + chipH <= paneBottom;
29228
+ const chipY = below ? y1 + gap : y1 - gap - chipH;
29229
+ ctx.save();
29230
+ ctx.font = `10px ${theme.fontFamily}`;
29231
+ const tw = ctx.measureText(label).width;
29232
+ const caretW = 11;
29233
+ const chipW = tw + 12 + caretW;
29234
+ roundRect(ctx, x0, chipY, chipW, chipH, 3);
29235
+ ctx.fillStyle = theme.background;
29236
+ ctx.fill();
29237
+ ctx.strokeStyle = withAlpha(theme.textColor, 0.28);
29238
+ ctx.lineWidth = 1;
29239
+ ctx.setLineDash([]);
29240
+ ctx.stroke();
29241
+ ctx.fillStyle = theme.textColor;
29242
+ ctx.textAlign = "left";
29243
+ ctx.textBaseline = "middle";
29244
+ ctx.fillText(label, x0 + 6, chipY + chipH / 2 + 0.5);
29245
+ const cxr = x0 + 6 + tw + 5;
29246
+ const cyr = chipY + chipH / 2;
29247
+ ctx.strokeStyle = withAlpha(theme.textColor, 0.7);
29248
+ ctx.lineWidth = 1.2;
29249
+ ctx.beginPath();
29250
+ ctx.moveTo(cxr, cyr - 1.5);
29251
+ ctx.lineTo(cxr + 2.5, cyr + 1.5);
29252
+ ctx.lineTo(cxr + 5, cyr - 1.5);
29253
+ ctx.stroke();
29254
+ ctx.restore();
29255
+ d.chipRect = { x: x0, y: chipY, w: chipW, h: chipH };
29256
+ } else {
29257
+ d.chipRect = null;
29258
+ }
29259
+ }
29260
+ /** The magnifier's candle/bar loop: each bar's cell spans its open→close time (shifted left
29261
+ * by half a chart bar). Candles: wick always, body once the cell is wide enough to carry
29262
+ * one. OHLC bars: the high–low spine with open/close ticks once the cell has the room. */
29263
+ paintMagnifierBars(ctx, d, proj, bars, barMs, halfPitch, x0, x1, mode, upColor, downColor) {
29264
+ ctx.setLineDash([]);
29265
+ ctx.lineWidth = 1;
29266
+ for (const bar of bars) {
29267
+ const cx0 = proj.xOf(bar.time) - halfPitch;
29268
+ const cx1 = proj.xOf(bar.time + barMs) - halfPitch;
29269
+ if (cx1 < x0 || cx0 > x1) continue;
29270
+ const yHigh = proj.yOf(bar.high, d.paneId);
29271
+ const yLow = proj.yOf(bar.low, d.paneId);
29272
+ const yOpen = proj.yOf(bar.open, d.paneId);
29273
+ const yClose = proj.yOf(bar.close, d.paneId);
29274
+ if (yHigh == null || yLow == null || yOpen == null || yClose == null) continue;
29275
+ const color = bar.close >= bar.open ? upColor : downColor;
29276
+ const cellW = cx1 - cx0;
29277
+ const cx = (cx0 + cx1) / 2;
29278
+ ctx.strokeStyle = color;
29279
+ ctx.beginPath();
29280
+ ctx.moveTo(cx, yHigh);
29281
+ ctx.lineTo(cx, yLow);
29282
+ ctx.stroke();
29283
+ if (cellW < 3) continue;
29284
+ if (mode === "bars") {
29285
+ const tick = Math.max(1, cellW * 0.35);
29286
+ ctx.beginPath();
29287
+ ctx.moveTo(cx - tick, yOpen);
29288
+ ctx.lineTo(cx, yOpen);
29289
+ ctx.moveTo(cx, yClose);
29290
+ ctx.lineTo(cx + tick, yClose);
29291
+ ctx.stroke();
29292
+ } else {
29293
+ const bw = Math.max(1, cellW * 0.7);
29294
+ ctx.fillStyle = color;
29295
+ ctx.fillRect(cx - bw / 2, Math.min(yOpen, yClose), bw, Math.max(1, Math.abs(yClose - yOpen)));
29296
+ }
29297
+ }
29298
+ }
29299
+ /** The magnifier's line/area rendering: a close polyline through each cell's center (same
29300
+ * half-chart-bar shift as the candles), with an optional translucent fill down to the
29301
+ * rectangle's bottom edge for the area style. Colored like the chart's own line series. */
29302
+ paintMagnifierLine(ctx, d, proj, bars, barMs, halfPitch, yBottom, area, color) {
29303
+ const pts = [];
29304
+ for (const bar of bars) {
29305
+ const y = proj.yOf(bar.close, d.paneId);
29306
+ if (y == null) continue;
29307
+ pts.push([proj.xOf(bar.time + barMs / 2) - halfPitch, y]);
29308
+ }
29309
+ if (pts.length < 2) return;
29310
+ if (area) {
29311
+ ctx.beginPath();
29312
+ ctx.moveTo(pts[0][0], yBottom);
29313
+ for (const [px, py] of pts) ctx.lineTo(px, py);
29314
+ ctx.lineTo(pts[pts.length - 1][0], yBottom);
29315
+ ctx.closePath();
29316
+ ctx.fillStyle = withAlpha(color, 0.15);
29317
+ ctx.fill();
29318
+ }
29319
+ ctx.setLineDash([]);
29320
+ ctx.lineWidth = 1.5;
29321
+ ctx.strokeStyle = color;
29322
+ ctx.beginPath();
29323
+ ctx.moveTo(pts[0][0], pts[0][1]);
29324
+ for (let i = 1; i < pts.length; i += 1) ctx.lineTo(pts[i][0], pts[i][1]);
29325
+ ctx.stroke();
29326
+ }
29327
+ /** Centered muted notice inside the magnifier rect (loading / unavailable states). */
29328
+ paintMagnifierNotice(ctx, text, x0, y0, w, h, theme) {
29329
+ if (w < 60 || h < 20) return;
29330
+ ctx.save();
29331
+ ctx.beginPath();
29332
+ ctx.rect(x0, y0, w, h);
29333
+ ctx.clip();
29334
+ ctx.font = `11px ${theme.fontFamily}`;
29335
+ ctx.fillStyle = withAlpha(theme.textColor, 0.55);
29336
+ ctx.textAlign = "center";
29337
+ ctx.textBaseline = "middle";
29338
+ ctx.fillText(text, x0 + w / 2, y0 + h / 2);
29339
+ ctx.restore();
29340
+ }
29341
+ /** A bottom-center pill prompting the armed tool's placement gesture (e.g. the magnifier's
29342
+ * "drag an area"). Painted by the drawings layer while the tool is armed and no placement
29343
+ * is in progress; chart-background fill so it reads as chrome over any content. */
29344
+ paintPlacementHint(ctx, text, theme, width, height) {
29345
+ ctx.save();
29346
+ ctx.font = `11px ${theme.fontFamily}`;
29347
+ const tw = ctx.measureText(text).width;
29348
+ const pillW = tw + 24;
29349
+ const pillH = 24;
29350
+ const x = (width - pillW) / 2;
29351
+ const y = height - pillH - 14;
29352
+ roundRect(ctx, x, y, pillW, pillH, pillH / 2);
29353
+ ctx.fillStyle = theme.background;
29354
+ ctx.fill();
29355
+ ctx.strokeStyle = withAlpha(theme.textColor, 0.28);
29356
+ ctx.lineWidth = 1;
29357
+ ctx.setLineDash([]);
29358
+ ctx.stroke();
29359
+ ctx.fillStyle = theme.textColor;
29360
+ ctx.textAlign = "center";
29361
+ ctx.textBaseline = "middle";
29362
+ ctx.fillText(text, width / 2, y + pillH / 2 + 0.5);
29363
+ ctx.restore();
29364
+ }
28817
29365
  /** Paint a fixed-range volume profile: horizontal histogram rows (up/down split) anchored to
28818
29366
  * the left or right of the time span, optional VAH / VAL / POC levels across the range, and
28819
29367
  * optional developing POC / VA polylines. Recomputes from the two anchors on every paint. */
@@ -30229,8 +30777,9 @@ function ensureStyles5() {
30229
30777
  if (!existing) document.head.appendChild(s);
30230
30778
  }
30231
30779
  var DrawingSettingsPopup = class {
30232
- constructor(host, theme) {
30780
+ constructor(host, theme, chartBarMs = () => 0) {
30233
30781
  this.host = host;
30782
+ this.chartBarMs = chartBarMs;
30234
30783
  this.el = null;
30235
30784
  this.tipEl = null;
30236
30785
  // floating hover-label (above/below the toolbar)
@@ -30255,6 +30804,14 @@ var DrawingSettingsPopup = class {
30255
30804
  this.theme = theme;
30256
30805
  this.settingsDialog = new DrawingSettingsDialog(host, theme);
30257
30806
  }
30807
+ /** The magnifier timeframe choices strictly below the chart's own bar duration
30808
+ * (`auto` rides along while at least one concrete lower step exists). */
30809
+ lowerTimeframeOptions() {
30810
+ const chartMs = this.chartBarMs();
30811
+ if (!(chartMs > 0)) return [...MAGNIFIER_TIMEFRAME_OPTIONS];
30812
+ const lower = MAGNIFIER_TIMEFRAME_OPTIONS.filter((o) => o.ms > 0 && o.ms < chartMs);
30813
+ return lower.length > 0 ? [MAGNIFIER_TIMEFRAME_OPTIONS[0], ...lower] : [];
30814
+ }
30258
30815
  setTheme(theme) {
30259
30816
  this.theme = theme;
30260
30817
  this.settingsDialog.setTheme(theme);
@@ -30297,7 +30854,20 @@ var DrawingSettingsPopup = class {
30297
30854
  const sz = drawing.size ?? "normal";
30298
30855
  bar.appendChild(this.dropdown("Icon size", STAMP_SIZE_OPTIONS, sz, (s) => stampSizeIcon(s), (v) => actions.patch({ size: v }), { label: sizeLabel }));
30299
30856
  }
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 })));
30857
+ if (paths.has("magnifier.timeframe") && drawing instanceof Magnifier) {
30858
+ const options = this.lowerTimeframeOptions();
30859
+ if (options.length > 0) {
30860
+ bar.appendChild(
30861
+ this.dropdown("Lower timeframe", options.map((o) => o.value), drawing.magnifier.timeframe, () => "", (v) => actions.patch({ "magnifier.timeframe": v }), {
30862
+ label: (v) => magnifierTimeframeLabel(String(v)),
30863
+ labelInTrigger: true
30864
+ })
30865
+ );
30866
+ }
30867
+ bar.appendChild(this.colorButton("Up candles", BUCKET_ICON, drawing.magnifier.upColor || t.upColor, (v) => actions.patch({ "magnifier.upColor": v })));
30868
+ bar.appendChild(this.colorButton("Down candles", BUCKET_ICON, drawing.magnifier.downColor || t.downColor, (v) => actions.patch({ "magnifier.downColor": v })));
30869
+ }
30870
+ 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
30871
  if (paths.has("style.lineWidth")) {
30302
30872
  const wf = schema.fields.find((f) => f.path === "style.lineWidth");
30303
30873
  if (wf?.kind === "number" && (wf.min ?? 1) > 1) {
@@ -30619,6 +31189,7 @@ var DrawingSettingsPopup = class {
30619
31189
  this.colorPop = null;
30620
31190
  this.colorOwner = null;
30621
31191
  }
31192
+ opts.onClose?.();
30622
31193
  }
30623
31194
  });
30624
31195
  const el = pop.el;
@@ -30637,6 +31208,49 @@ var DrawingSettingsPopup = class {
30637
31208
  pop.show();
30638
31209
  return pop;
30639
31210
  }
31211
+ /**
31212
+ * A standalone timeframe menu for the magnifier's ON-CHART chip. The chip lives on
31213
+ * canvas, so a transient invisible anchor is dropped at its pixel rect for the popover
31214
+ * to position against, and removed again when the menu closes. Independent of the
31215
+ * quick toolbar — the chip works without selecting the drawing first.
31216
+ */
31217
+ openMagnifierTimeframeMenu(rect, current, onPick) {
31218
+ ensureStyles5();
31219
+ closeOpenPopovers();
31220
+ const options = this.lowerTimeframeOptions();
31221
+ const anchor = document.createElement("div");
31222
+ anchor.style.cssText = `position:absolute;left:${rect.x}px;top:${rect.y}px;width:${rect.w}px;height:${rect.h}px;pointer-events:none;`;
31223
+ this.host.appendChild(anchor);
31224
+ this.menuPop = this.hostFloat(anchor, {
31225
+ zIndex: 26,
31226
+ padding: "4px",
31227
+ onClose: () => anchor.remove(),
31228
+ fill: (menu2, pop) => {
31229
+ if (options.length === 0) {
31230
+ const note = document.createElement("div");
31231
+ note.style.cssText = "padding:6px 10px;opacity:0.65;white-space:nowrap;";
31232
+ note.textContent = "No lower timeframe available";
31233
+ menu2.appendChild(note);
31234
+ return;
31235
+ }
31236
+ for (const o of options) {
31237
+ const item = document.createElement("button");
31238
+ item.type = "button";
31239
+ item.className = "vela-dpop-item";
31240
+ item.dataset.active = o.value === current ? "1" : "0";
31241
+ 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;";
31242
+ item.textContent = o.label;
31243
+ item.addEventListener("click", (e) => {
31244
+ e.stopPropagation();
31245
+ pop.hide();
31246
+ onPick(o.value);
31247
+ });
31248
+ menu2.appendChild(item);
31249
+ }
31250
+ }
31251
+ });
31252
+ this.menuOwner = anchor;
31253
+ }
30640
31254
  /** A floating list of one-shot actions (icon + label rows) opened by the kebab. */
30641
31255
  openActionMenu(anchor, rows) {
30642
31256
  this.menuPop = this.hostFloat(anchor, {
@@ -30747,10 +31361,13 @@ var DrawingSettingsPopup = class {
30747
31361
  let cur = current;
30748
31362
  const paint = (v) => {
30749
31363
  b.replaceChildren();
30750
- const ic = document.createElement("span");
30751
- ic.style.cssText = "display:flex;";
30752
- ic.innerHTML = sized(render(v));
30753
- b.appendChild(ic);
31364
+ const glyph = render(v);
31365
+ if (glyph) {
31366
+ const ic = document.createElement("span");
31367
+ ic.style.cssText = "display:flex;";
31368
+ ic.innerHTML = sized(glyph);
31369
+ b.appendChild(ic);
31370
+ }
30754
31371
  if (opts.label && opts.labelInTrigger) {
30755
31372
  const tx = document.createElement("span");
30756
31373
  tx.textContent = opts.label(v);
@@ -30792,10 +31409,13 @@ var DrawingSettingsPopup = class {
30792
31409
  item.className = "vela-dpop-item";
30793
31410
  item.dataset.active = active ? "1" : "0";
30794
31411
  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);
31412
+ const glyph = render(v);
31413
+ if (glyph) {
31414
+ const ic = document.createElement("span");
31415
+ ic.style.cssText = "display:flex;flex:none;width:22px;justify-content:center;";
31416
+ ic.innerHTML = sized(glyph, 18);
31417
+ item.appendChild(ic);
31418
+ }
30799
31419
  if (label) {
30800
31420
  const tx = document.createElement("span");
30801
31421
  tx.textContent = label(v);
@@ -31147,6 +31767,9 @@ var UserDrawingController = class {
31147
31767
  this.intentCb = null;
31148
31768
  /** Another chart's in-progress placement, mirrored here as a ghost (drawings sync). */
31149
31769
  this.externalGhost = null;
31770
+ /** Core-pushed series gateway (finer-timeframe bars for data-driven drawings). */
31771
+ this.seriesGw = null;
31772
+ this.seriesGwUnsub = null;
31150
31773
  /** Last draft fingerprint reported upstream — gates the per-render emission to actual changes. */
31151
31774
  this.lastDraftKey = null;
31152
31775
  this.measure = new MeasureOverlay();
@@ -31174,7 +31797,7 @@ var UserDrawingController = class {
31174
31797
  this.textEditor = null;
31175
31798
  this.painter = new DrawingPainter();
31176
31799
  this.ctx = canvas.getContext("2d");
31177
- this.popup = new DrawingSettingsPopup(overlayHost, deps.theme());
31800
+ this.popup = new DrawingSettingsPopup(overlayHost, deps.theme(), () => deps.chartBarMs());
31178
31801
  this.toolbar = new DrawingToolbar(
31179
31802
  toolbarHost,
31180
31803
  deps.theme(),
@@ -31232,6 +31855,22 @@ var UserDrawingController = class {
31232
31855
  const shown = this.toolbarVisible && !this.mobileLayout;
31233
31856
  this.deps.setToolbarGutter(shown ? this.toolbarCollapsed ? TOOLBAR_COLLAPSED_WIDTH : TOOLBAR_WIDTH : 0);
31234
31857
  }
31858
+ /** Core push: the series gateway data-driven drawings read finer-timeframe bars
31859
+ * through (surfaced to them as `Projector.seriesInRange`). A landed background
31860
+ * fetch repaints both this layer and the interleave slices under the series. */
31861
+ setSeriesGateway(gateway) {
31862
+ this.seriesGwUnsub?.();
31863
+ this.seriesGw = gateway;
31864
+ this.seriesGwUnsub = gateway.onUpdate(() => {
31865
+ this.invalidateSlices();
31866
+ this.render();
31867
+ this.deps.requestDataPaint();
31868
+ });
31869
+ }
31870
+ /** The pushed series gateway, or null before the core provides one. */
31871
+ get seriesGateway() {
31872
+ return this.seriesGw;
31873
+ }
31235
31874
  /** Core push: mirror (or clear) another chart's in-progress placement as a ghost. */
31236
31875
  setExternalGhost(doc) {
31237
31876
  this.externalGhost = doc ? deserializeDrawing(doc) : null;
@@ -31349,8 +31988,34 @@ var UserDrawingController = class {
31349
31988
  /** Should the drawing layer win this press (vs pan)? */
31350
31989
  claim(x, y) {
31351
31990
  if (this.measureMode || this.eraserMode) return true;
31991
+ if (this.magnifierChipAt(x, y)) return true;
31352
31992
  return this.interaction.claim(x, y);
31353
31993
  }
31994
+ /** The topmost visible (unlocked) magnifier whose timeframe chip contains (x, y) —
31995
+ * the chip's rect is what the painter measured last frame. */
31996
+ magnifierChipAt(x, y) {
31997
+ for (let i = this.drawings.length - 1; i >= 0; i -= 1) {
31998
+ const d = this.drawings[i];
31999
+ if (!(d instanceof Magnifier) || !d.visible || d.locked) continue;
32000
+ const r = d.chipRect;
32001
+ if (r && x >= r.x && x <= r.x + r.w && y >= r.y && y <= r.y + r.h) return d;
32002
+ }
32003
+ return null;
32004
+ }
32005
+ /** Open the on-chart chip's timeframe menu; the pick patches the drawing like a
32006
+ * settings-popup edit (same intent, same undo step). */
32007
+ openMagnifierChipMenu(drawing) {
32008
+ const rect = drawing.chipRect;
32009
+ if (!rect) return;
32010
+ const id = drawing.id;
32011
+ this.popup.openMagnifierTimeframeMenu(rect, drawing.magnifier.timeframe, (value) => {
32012
+ const d = this.drawings.find((x) => x.id === id);
32013
+ if (!(d instanceof Magnifier)) return;
32014
+ d.applySettings({ "magnifier.timeframe": value });
32015
+ this.render();
32016
+ this.emit({ kind: "edit", doc: d.serialize() });
32017
+ });
32018
+ }
31354
32019
  /** Delete the (unlocked) drawing under the cursor. True when one was removed.
31355
32020
  * Shared by the eraser (click + drag) and the middle-click shortcut. */
31356
32021
  deleteAt(x, y) {
@@ -31396,6 +32061,13 @@ var UserDrawingController = class {
31396
32061
  this.render();
31397
32062
  return;
31398
32063
  }
32064
+ if (this.activeTool == null) {
32065
+ const chipOwner = this.magnifierChipAt(x, y);
32066
+ if (chipOwner) {
32067
+ this.openMagnifierChipMenu(chipOwner);
32068
+ return;
32069
+ }
32070
+ }
31399
32071
  this.interaction.down(x, y, snap, shift);
31400
32072
  }
31401
32073
  pointerMove(x, y, snap = "off", shift = false) {
@@ -31614,6 +32286,7 @@ var UserDrawingController = class {
31614
32286
  /** Cursor hint while hovering — `'pointer'` over a drawing/handle, else null. */
31615
32287
  cursorAt(x, y) {
31616
32288
  if (this.eraserMode) return "pointer";
32289
+ if (this.activeTool == null && this.magnifierChipAt(x, y)) return "pointer";
31617
32290
  return this.interaction.cursorAt(x, y);
31618
32291
  }
31619
32292
  /** Right-click: an explicit escape back to the pointer. Cancels an in-progress
@@ -31792,6 +32465,7 @@ var UserDrawingController = class {
31792
32465
  if (!sctx) continue;
31793
32466
  sctx.setTransform(dpr, 0, 0, dpr, 0, 0);
31794
32467
  sctx.clearRect(0, 0, canvas.width / dpr, canvas.height / dpr);
32468
+ this.painter.seriesLook = this.deps.seriesLook();
31795
32469
  this.painter.paintAll(sctx, drawings, proj, theme, EMPTY_TARGETS);
31796
32470
  const slices = out.get(paneId) ?? [];
31797
32471
  slices.push({ beforeZ, canvas });
@@ -31819,6 +32493,7 @@ var UserDrawingController = class {
31819
32493
  dragged: this.interaction.activeDragId(),
31820
32494
  mutedLabel: edited instanceof TextLabel ? edited.id : null
31821
32495
  };
32496
+ this.painter.seriesLook = this.deps.seriesLook();
31822
32497
  this.painter.paintAll(ctx, this.drawings.filter((d) => !this.isInterleaved(d)), proj, this.deps.theme(), targets);
31823
32498
  this.painter.paintHighlights(ctx, this.drawings.filter((d) => this.isInterleaved(d)), proj, handleIdsFor(targets));
31824
32499
  this.layoutTextEditor();
@@ -31826,6 +32501,10 @@ var UserDrawingController = class {
31826
32501
  if (ghost) this.painter.paintGhost(ctx, ghost, proj, this.deps.theme());
31827
32502
  if (this.externalGhost) this.painter.paintGhost(ctx, this.externalGhost, proj, this.deps.theme());
31828
32503
  this.emitDraft(ghost);
32504
+ if (this.activeTool && !ghost) {
32505
+ const hint = getDrawingType(this.activeTool)?.placementHint;
32506
+ if (hint) this.painter.paintPlacementHint(ctx, hint, this.deps.theme(), proj.width, proj.height);
32507
+ }
31829
32508
  const markers = this.interaction.placingMarkers(proj);
31830
32509
  if (markers) this.painter.paintHandles(ctx, markers);
31831
32510
  const m = this.interaction.snapMarker();
@@ -31837,6 +32516,9 @@ var UserDrawingController = class {
31837
32516
  this.closeTextEditor();
31838
32517
  this.popup.destroy();
31839
32518
  this.toolbar.destroy();
32519
+ this.seriesGwUnsub?.();
32520
+ this.seriesGwUnsub = null;
32521
+ this.seriesGw = null;
31840
32522
  this.intentCb = null;
31841
32523
  this.drawings = [];
31842
32524
  }
@@ -32234,7 +32916,7 @@ function mergeSlices(indicator, user) {
32234
32916
  }
32235
32917
 
32236
32918
  // src/renderers/native/drawings/Projector.ts
32237
- function createProjector(coords, paneOf, paneIdAtY, barsInRange) {
32919
+ function createProjector(coords, paneOf, paneIdAtY, barsInRange, seriesInRange) {
32238
32920
  return {
32239
32921
  xOf: (time) => coords.timeToX(time),
32240
32922
  yOf: (price, paneId) => {
@@ -32255,6 +32937,7 @@ function createProjector(coords, paneOf, paneIdAtY, barsInRange) {
32255
32937
  },
32256
32938
  barsBetween: (t1, t2) => Math.abs(coords.timeToLogical(t2) - coords.timeToLogical(t1)),
32257
32939
  barsInRange: barsInRange ? (from, to) => barsInRange(from, to) : void 0,
32940
+ seriesInRange,
32258
32941
  width: coords.width,
32259
32942
  height: coords.height
32260
32943
  };
@@ -34124,6 +34807,23 @@ var NativeRenderer = class {
34124
34807
  seriesBoundaries: (paneId) => this.scene.seriesBoundaries(paneId),
34125
34808
  priceZ: (paneId) => paneId === PRICE_PANE_ID2 ? this.scene.candleZ : null,
34126
34809
  requestDataPaint: () => this.scheduler.invalidate(3 /* Light */),
34810
+ // The look the price series ACTUALLY paints with: candle colors resolved through
34811
+ // the per-style override, line/area colors through their configured styles — so
34812
+ // series-mirroring content (the magnifier inset) matches the chart exactly.
34813
+ seriesLook: () => {
34814
+ const st = this.scene.style;
34815
+ const paint = effectiveCandlePaint(st.candle, this.scene.candleOverride, this.theme.upColor, this.theme.downColor);
34816
+ const barsUp = st.bars.upColor ?? this.theme.upColor;
34817
+ const barsDown = st.bars.downColor ?? this.theme.downColor;
34818
+ const style = this.scene.priceStyle;
34819
+ return {
34820
+ style,
34821
+ upColor: style === "bars" ? barsUp : paint.up,
34822
+ downColor: style === "bars" ? barsDown : paint.down,
34823
+ lineColor: style === "area" ? st.area.lineColor ?? this.theme.upColor : st.line.color ?? this.theme.upColor
34824
+ };
34825
+ },
34826
+ chartBarMs: () => this.coords.barInterval,
34127
34827
  snap: (pt, paneId, mode, cursorPx) => this.snapToCandle(pt, paneId, mode, cursorPx),
34128
34828
  setSnapMode: (mode) => this.setSnapMode(mode),
34129
34829
  setToolbarGutter: (px) => this.setToolbarGutter(px)
@@ -35517,7 +36217,8 @@ var NativeRenderer = class {
35517
36217
  return p ? { scale: p.scale, bounds: p.bounds, collapsed: p.collapsed } : null;
35518
36218
  },
35519
36219
  (y) => this.paneNodeAtY(y)?.id ?? null,
35520
- (from, to) => this.barsInTimeRange(from, to)
36220
+ (from, to) => this.barsInTimeRange(from, to),
36221
+ this.userDrawings?.seriesGateway ? (tf, from, to) => this.userDrawings.seriesGateway.seriesInRange(tf, from, to) : void 0
35521
36222
  );
35522
36223
  }
35523
36224
  /** OHLC bars whose open-time falls within `[from, to]` (inclusive) — the data a regression
@@ -36724,6 +37425,21 @@ function resolveElement(container) {
36724
37425
  // src/core/options.ts
36725
37426
  var normalizeSession = (v) => v === "regular" || v === "extended" ? v : void 0;
36726
37427
 
37428
+ // src/widget/statusline-model.ts
37429
+ function segmentVisibility(parts, chartHidden = false) {
37430
+ return { avatar: parts.logo, symbol: parts.name, meta: parts.name, market: parts.market, ohlc: parts.ohlc && !chartHidden, change: parts.change && !chartHidden, eye: chartHidden };
37431
+ }
37432
+ function statuslineMenuItems(parts, chartVisible) {
37433
+ return [
37434
+ { id: "part:logo", label: "Symbol logo", checked: parts.logo },
37435
+ { id: "part:name", label: "Symbol name", checked: parts.name },
37436
+ { id: "part:market", label: "Market status", checked: parts.market },
37437
+ { id: "part:ohlc", label: "OHLC values", checked: parts.ohlc },
37438
+ { id: "part:change", label: "Bar change values", checked: parts.change },
37439
+ { id: "chart", label: chartVisible ? "Hide chart" : "Show chart", separatorBefore: true }
37440
+ ];
37441
+ }
37442
+
36727
37443
  // src/widget/format.ts
36728
37444
  function decimalsFor(ref) {
36729
37445
  const a = Math.abs(ref);
@@ -36771,7 +37487,13 @@ var CSS21 = `
36771
37487
  padding: 2px 7px;
36772
37488
  margin-left: -7px;
36773
37489
  }
36774
- .vela-statusline:hover { background: var(--vela-bg); }
37490
+ /* Hovering opens the chip the same way a legend row opens: solid chart background
37491
+ * plus the same inset neutral outline the indicator rows wear (InputsUI's
37492
+ * setRowHighlighted) \u2014 the two columns read as one family. */
37493
+ .vela-statusline:hover { background: var(--vela-bg); box-shadow: inset 0 0 0 1px var(--vela-border); }
37494
+ /* Chart hidden (the price series' eye \u2014 renderer 'candleVisible'): the line dims to
37495
+ * the same 0.5 wash a hidden indicator's legend row wears. */
37496
+ .vela-statusline.vela-sl-chart-hidden { opacity: 0.5; }
36775
37497
  .vela-statusline .vela-sl-avatar {
36776
37498
  width: 18px;
36777
37499
  height: 18px;
@@ -36796,6 +37518,24 @@ var CSS21 = `
36796
37518
  * these are the pre-ink fallbacks only. */
36797
37519
  .vela-statusline .vela-sl-change[data-dir='up'] { color: var(--vela-up); }
36798
37520
  .vela-statusline .vela-sl-change[data-dir='down'] { color: var(--vela-down); }
37521
+ /* The show-chart eye \u2014 out only while the chart is hidden (syncParts drives display),
37522
+ * replacing the value readout it took away. Same footprint as a legend action button. */
37523
+ .vela-statusline .vela-sl-eye {
37524
+ align-self: center;
37525
+ align-items: center;
37526
+ justify-content: center;
37527
+ width: 18px;
37528
+ height: 18px;
37529
+ padding: 0;
37530
+ border: none;
37531
+ border-radius: 3px;
37532
+ background: none;
37533
+ color: var(--vela-fg-muted);
37534
+ cursor: pointer;
37535
+ line-height: 0;
37536
+ flex: none;
37537
+ }
37538
+ .vela-statusline .vela-sl-eye:hover { color: var(--vela-fg); background: color-mix(in srgb, var(--vela-fg) 12%, transparent); }
36799
37539
  /* Stack the TOP pane's legend below the status line (lower study panes stay put).
36800
37540
  * The renderer marks whichever legend sits at the plot's top edge \u2014 the price pane
36801
37541
  * normally, or a maximized study pane filling the plot \u2014 so the legend never merges
@@ -36905,7 +37645,12 @@ var Statusline = class {
36905
37645
  constructor(host, symbol, iconFor) {
36906
37646
  this.host = host;
36907
37647
  this.iconFor = iconFor;
36908
- this.parts = { name: true, market: true, ohlc: true, change: true };
37648
+ this.parts = { logo: true, name: true, market: true, ohlc: true, change: true };
37649
+ /** The right-click action menu — present once a host wires it via {@link attachMenu}. */
37650
+ this.menu = null;
37651
+ this.menuHooks = null;
37652
+ /** Mirror of the renderer's `candleVisible` — see {@link setChartHidden}. */
37653
+ this.chartHidden = false;
36909
37654
  this.lastBar = null;
36910
37655
  this.hoverBar = null;
36911
37656
  this.unsubs = [];
@@ -36921,6 +37666,15 @@ var Statusline = class {
36921
37666
  /** Fit mode (multi-chart cells): one row, overflowing segments hidden — see {@link setFitMode}. */
36922
37667
  this.fitMode = false;
36923
37668
  this.fitRO = null;
37669
+ this.onContextMenu = (e) => {
37670
+ if (!this.menu || !this.menuHooks) return;
37671
+ e.preventDefault();
37672
+ e.stopPropagation();
37673
+ const chartVisible = this.menuHooks.chartVisible();
37674
+ this.setChartHidden(!chartVisible);
37675
+ this.menu.setItems(statuslineMenuItems(this.parts, chartVisible));
37676
+ this.menu.openAt(e.clientX, e.clientY);
37677
+ };
36924
37678
  const doc = host.ownerDocument;
36925
37679
  injectStyles(STYLE_ID24, CSS21, doc);
36926
37680
  host.classList.add("vela-has-statusline");
@@ -36947,9 +37701,21 @@ var Statusline = class {
36947
37701
  this.ohlcEl.className = "vela-sl-ohlc";
36948
37702
  this.changeEl = doc.createElement("span");
36949
37703
  this.changeEl.className = "vela-sl-change";
36950
- this.el.append(this.avatarEl, this.symbolEl, this.metaEl, this.marketEl, this.ohlcEl, this.changeEl);
37704
+ this.eyeEl = doc.createElement("button");
37705
+ this.eyeEl.type = "button";
37706
+ this.eyeEl.className = "vela-sl-eye";
37707
+ this.eyeEl.innerHTML = iconAt("eye-off", 14);
37708
+ this.eyeEl.setAttribute("aria-label", "Show chart");
37709
+ this.eyeEl.style.display = "none";
37710
+ this.eyeEl.addEventListener("click", (e) => {
37711
+ e.stopPropagation();
37712
+ this.menuHooks?.setChartVisible(true);
37713
+ this.setChartHidden(false);
37714
+ });
37715
+ this.el.append(this.avatarEl, this.symbolEl, this.metaEl, this.marketEl, this.ohlcEl, this.changeEl, this.eyeEl);
36951
37716
  host.appendChild(this.el);
36952
37717
  this.marketTip = new Tooltip(this.marketEl, { content: MARKET_LABELS.open, placement: "bottom" });
37718
+ this.eyeTip = new Tooltip(this.eyeEl, { content: "Show chart", placement: "bottom" });
36953
37719
  this.setMarketStatus("open");
36954
37720
  this.render();
36955
37721
  }
@@ -36959,6 +37725,7 @@ var Statusline = class {
36959
37725
  const fresh = tickerIconEl(this.el.ownerDocument, baseOfTicker(ticker), ticker, "vela-sl-avatar", this.iconFor?.(symbol));
36960
37726
  this.avatarEl.replaceWith(fresh);
36961
37727
  this.avatarEl = fresh;
37728
+ this.syncParts();
36962
37729
  this.fit();
36963
37730
  }
36964
37731
  /**
@@ -36986,20 +37753,25 @@ var Statusline = class {
36986
37753
  }
36987
37754
  /** Project the parts config onto the segments (the baseline fit() prunes from). */
36988
37755
  syncParts() {
36989
- this.symbolEl.style.display = this.parts.name ? "" : "none";
36990
- this.marketEl.style.display = this.parts.market ? "" : "none";
36991
- this.ohlcEl.style.display = this.parts.ohlc ? "" : "none";
36992
- this.changeEl.style.display = this.parts.change ? "" : "none";
37756
+ const seg = segmentVisibility(this.parts, this.chartHidden);
37757
+ this.avatarEl.style.display = seg.avatar ? "" : "none";
37758
+ this.symbolEl.style.display = seg.symbol ? "" : "none";
37759
+ this.metaEl.style.display = seg.meta ? "" : "none";
37760
+ this.marketEl.style.display = seg.market ? "" : "none";
37761
+ this.ohlcEl.style.display = seg.ohlc ? "" : "none";
37762
+ this.changeEl.style.display = seg.change ? "" : "none";
37763
+ this.eyeEl.style.display = seg.eye ? "inline-flex" : "none";
36993
37764
  }
36994
37765
  /** Hide overflowing segments until the row fits its max-width (fit mode only). */
36995
37766
  fit() {
36996
37767
  if (!this.fitMode) return;
36997
37768
  this.syncParts();
37769
+ const seg = segmentVisibility(this.parts, this.chartHidden);
36998
37770
  const order = [
36999
- [this.ohlcEl, this.parts.ohlc],
37000
- [this.changeEl, this.parts.change],
37001
- [this.metaEl, true],
37002
- [this.marketEl, this.parts.market]
37771
+ [this.ohlcEl, seg.ohlc],
37772
+ [this.changeEl, seg.change],
37773
+ [this.metaEl, seg.meta],
37774
+ [this.marketEl, seg.market]
37003
37775
  ];
37004
37776
  for (const [el, shown] of order) {
37005
37777
  if (this.el.scrollWidth <= this.el.clientWidth) break;
@@ -37020,7 +37792,6 @@ var Statusline = class {
37020
37792
  this.readout = readout;
37021
37793
  this.render();
37022
37794
  }
37023
- /** Show/hide one part (the settings dialog's Status line tab drives these). */
37024
37795
  /** The "· BINANCE · 1h" segment after the symbol — venue first, then resolution. */
37025
37796
  setMeta(timeframe, provider) {
37026
37797
  this.metaEl.textContent = `${provider ? `\xB7 ${provider.toUpperCase()} ` : ""}\xB7 ${timeframeLabel(timeframe)}`;
@@ -37039,6 +37810,9 @@ var Statusline = class {
37039
37810
  });
37040
37811
  this.marketTip.setContent(MARKET_LABELS[status]);
37041
37812
  }
37813
+ /** Show/hide one part — the settings dialog's Status line tab and the right-click
37814
+ * menu both drive these. 'name' owns the venue/timeframe meta too (see
37815
+ * {@link segmentVisibility}). */
37042
37816
  setPartVisible(part, visible) {
37043
37817
  this.parts[part] = visible;
37044
37818
  this.syncParts();
@@ -37047,6 +37821,47 @@ var Statusline = class {
37047
37821
  partVisible(part) {
37048
37822
  return this.parts[part];
37049
37823
  }
37824
+ /** Mirror the chart's (price series') visibility: dim the whole line like a hidden
37825
+ * indicator's legend row, drop the value readout (OHLC + bar change — values of a
37826
+ * series that isn't painted), and put the show-chart eye out in its place. The
37827
+ * parts config is untouched, so showing the chart restores the readout exactly as
37828
+ * configured. Idempotent; {@link render} re-syncs it from the live renderer, so
37829
+ * toggles made elsewhere (the object tree's eye) converge too. */
37830
+ setChartHidden(hidden) {
37831
+ if (hidden === this.chartHidden) return;
37832
+ this.chartHidden = hidden;
37833
+ this.el.classList.toggle("vela-sl-chart-hidden", hidden);
37834
+ this.syncParts();
37835
+ this.fit();
37836
+ }
37837
+ /** Wire the right-click action menu: one checkable toggle per part plus hide/show
37838
+ * for the chart itself. The menu is built once; later calls just swap the hooks. */
37839
+ attachMenu(hooks) {
37840
+ this.menuHooks = hooks;
37841
+ if (this.menu) return;
37842
+ this.menu = new Menu({
37843
+ host: this.host,
37844
+ items: [],
37845
+ placement: "bottom-start",
37846
+ // Pointer-anchored action menu — checked state reads as a leading ✓ (the
37847
+ // same shape as the chart's own context menu).
37848
+ checkmarks: true,
37849
+ onSelect: (id) => this.runMenuItem(id)
37850
+ });
37851
+ this.el.addEventListener("contextmenu", this.onContextMenu);
37852
+ }
37853
+ runMenuItem(id) {
37854
+ const hooks = this.menuHooks;
37855
+ if (!hooks) return;
37856
+ if (id.startsWith("part:")) {
37857
+ const part = id.slice("part:".length);
37858
+ hooks.setPart(part, !this.parts[part]);
37859
+ } else if (id === "chart") {
37860
+ const next = !hooks.chartVisible();
37861
+ hooks.setChartVisible(next);
37862
+ this.setChartHidden(!next);
37863
+ }
37864
+ }
37050
37865
  /** (Re)bind to a chart instance — called after every widget rebuild. */
37051
37866
  onChart(chart) {
37052
37867
  this.detach();
@@ -37068,7 +37883,11 @@ var Statusline = class {
37068
37883
  this.detach();
37069
37884
  this.fitRO?.disconnect();
37070
37885
  this.fitRO = null;
37886
+ this.el.removeEventListener("contextmenu", this.onContextMenu);
37887
+ this.menu?.destroy();
37888
+ this.menu = null;
37071
37889
  this.marketTip.destroy();
37890
+ this.eyeTip.destroy();
37072
37891
  this.marketBubble.destroy();
37073
37892
  this.host.classList.remove("vela-has-statusline", "vela-sl-fit-host");
37074
37893
  this.el.remove();
@@ -37078,6 +37897,7 @@ var Statusline = class {
37078
37897
  this.unsubs = [];
37079
37898
  }
37080
37899
  render() {
37900
+ if (this.menuHooks) this.setChartHidden(!this.menuHooks.chartVisible());
37081
37901
  const bar = this.hoverBar ?? this.lastBar;
37082
37902
  if (!bar) {
37083
37903
  this.ohlcEl.replaceChildren();
@@ -37148,7 +37968,7 @@ var FETCH_BACK_MS = 4 * 864e5;
37148
37968
  var FETCH_AHEAD_MS = 10 * 864e5;
37149
37969
  var MIN_TIMER_MS = 15e3;
37150
37970
  var MAX_TIMER_MS = 36e5;
37151
- var RETRY_MS = 6e4;
37971
+ var RETRY_MS2 = 6e4;
37152
37972
  var MarketStatusTracker = class {
37153
37973
  constructor(onStatus) {
37154
37974
  this.onStatus = onStatus;
@@ -37185,7 +38005,7 @@ var MarketStatusTracker = class {
37185
38005
  ]);
37186
38006
  if (my !== this.epoch) return;
37187
38007
  if (!regular || !extended) {
37188
- this.arm(my, data, symbol, RETRY_MS);
38008
+ this.arm(my, data, symbol, RETRY_MS2);
37189
38009
  return;
37190
38010
  }
37191
38011
  const w = { regular, extended };
@@ -37896,7 +38716,14 @@ var ChartCell = class {
37896
38716
  this.destroyed = false;
37897
38717
  this.appTheme = deps.theme;
37898
38718
  const symbol = prefixedSymbol(seed);
37899
- this.state = { symbol, provider: parseSymbol(symbol ?? "").provider ?? void 0, timeframe: seed.timeframe, priceStyle: seed.priceStyle, bars: seed.bars, session: normalizeSession(seed.session) };
38719
+ this.state = {
38720
+ symbol,
38721
+ provider: parseSymbol(symbol ?? "").provider ?? void 0,
38722
+ timeframe: seed.timeframe,
38723
+ priceStyle: seed.priceStyle,
38724
+ bars: seed.bars,
38725
+ session: normalizeSession(seed.session)
38726
+ };
37900
38727
  const doc = gridHost.ownerDocument;
37901
38728
  this.host = doc.createElement("div");
37902
38729
  this.host.className = "vela-cell";
@@ -37984,6 +38811,11 @@ var ChartCell = class {
37984
38811
  this.statusline = deps.statusline ? new Statusline(this.host, symbol ?? "", (sym) => this.inner?.data.symbolIcon(sym)) : null;
37985
38812
  this.statusline?.setMeta(seed.timeframe ?? "60", this.state.provider ?? "");
37986
38813
  this.statusline?.onChart(this.inner);
38814
+ this.statusline?.attachMenu({
38815
+ setPart: (part, visible) => this.setStatuslinePart(part, visible),
38816
+ chartVisible: () => this.inner?.renderer.get("candleVisible") !== false,
38817
+ setChartVisible: (visible) => this.inner?.renderer.set("candleVisible", visible)
38818
+ });
37987
38819
  this.marketStatus = this.statusline ? new MarketStatusTracker((s) => this.statusline?.setMarketStatus(s)) : null;
37988
38820
  void this.inner.data.ready().then(() => {
37989
38821
  if (this.inner && this.state.symbol) {
@@ -38191,7 +39023,7 @@ var ChartCell = class {
38191
39023
  kind: "select",
38192
39024
  label: "Bars to fetch",
38193
39025
  id: "bars",
38194
- options: ["500", "1000", "2000", "5000", "10000", "20000"],
39026
+ options: ["500", "1000", "2000", "5000", "10000", "20000", "50000", "60000", "80000", "100000"],
38195
39027
  get: () => String(this.state.bars ?? 1e3),
38196
39028
  set: (v) => {
38197
39029
  this.state.bars = Number(v);
@@ -38223,10 +39055,34 @@ var ChartCell = class {
38223
39055
  id: "status-line",
38224
39056
  rows: [
38225
39057
  { kind: "heading", label: "Status line", id: "parts" },
38226
- { kind: "toggle", label: "Symbol name", id: "name", get: () => sl.partVisible("name"), set: (v) => this.setStatuslinePart("name", v) },
38227
- { kind: "toggle", label: "Market status", id: "market", get: () => sl.partVisible("market"), set: (v) => this.setStatuslinePart("market", v) },
38228
- { kind: "toggle", label: "OHLC values", id: "ohlc", get: () => sl.partVisible("ohlc"), set: (v) => this.setStatuslinePart("ohlc", v) },
38229
- { kind: "toggle", label: "Bar change values", id: "change", get: () => sl.partVisible("change"), set: (v) => this.setStatuslinePart("change", v) },
39058
+ {
39059
+ kind: "toggle",
39060
+ label: "Symbol name",
39061
+ id: "name",
39062
+ get: () => sl.partVisible("name"),
39063
+ set: (v) => this.setStatuslinePart("name", v)
39064
+ },
39065
+ {
39066
+ kind: "toggle",
39067
+ label: "Market status",
39068
+ id: "market",
39069
+ get: () => sl.partVisible("market"),
39070
+ set: (v) => this.setStatuslinePart("market", v)
39071
+ },
39072
+ {
39073
+ kind: "toggle",
39074
+ label: "OHLC values",
39075
+ id: "ohlc",
39076
+ get: () => sl.partVisible("ohlc"),
39077
+ set: (v) => this.setStatuslinePart("ohlc", v)
39078
+ },
39079
+ {
39080
+ kind: "toggle",
39081
+ label: "Bar change values",
39082
+ id: "change",
39083
+ get: () => sl.partVisible("change"),
39084
+ set: (v) => this.setStatuslinePart("change", v)
39085
+ },
38230
39086
  { kind: "heading", label: "Indicators", id: "indicators" },
38231
39087
  {
38232
39088
  kind: "toggle",
@@ -38279,7 +39135,7 @@ var ChartCell = class {
38279
39135
  statusPrefs() {
38280
39136
  const sl = this.statusline;
38281
39137
  return {
38282
- parts: sl ? { name: sl.partVisible("name"), market: sl.partVisible("market"), ohlc: sl.partVisible("ohlc"), change: sl.partVisible("change") } : null,
39138
+ parts: sl ? { logo: sl.partVisible("logo"), name: sl.partVisible("name"), market: sl.partVisible("market"), ohlc: sl.partVisible("ohlc"), change: sl.partVisible("change") } : null,
38283
39139
  indicatorTitles: this.indicatorTitlesOn,
38284
39140
  indicatorValues: this.indicatorValuesOn
38285
39141
  };
@@ -38453,7 +39309,8 @@ var ChartCell = class {
38453
39309
  if (this.manifest.length > 0) {
38454
39310
  for (const item of led.manifest) {
38455
39311
  const entry = this.manifest.find((e) => e.name === ledgerEntryName(item));
38456
- if (entry) this.addManifestInstance(entry, { record: false, ...typeof item === "object" ? { inputs: item.inputs, props: item.props } : {} });
39312
+ if (entry)
39313
+ this.addManifestInstance(entry, { record: false, ...typeof item === "object" ? { inputs: item.inputs, props: item.props } : {} });
38457
39314
  }
38458
39315
  this.pendingManifestNames = null;
38459
39316
  } else if (!this.deps.manifestSettled()) {
@@ -38502,7 +39359,10 @@ var ChartCell = class {
38502
39359
  * a persistence handler's `restore` runs silently, a user-driven call records.
38503
39360
  */
38504
39361
  addExternalIndicator(entry) {
38505
- this.addManifestInstance({ ...entry, enabled: true }, { external: true, ...entry.inputs ? { inputs: entry.inputs } : {}, ...entry.props ? { props: entry.props } : {} });
39362
+ this.addManifestInstance(
39363
+ { ...entry, enabled: true },
39364
+ { external: true, ...entry.inputs ? { inputs: entry.inputs } : {}, ...entry.props ? { props: entry.props } : {} }
39365
+ );
38506
39366
  }
38507
39367
  /** Add ONE instance of a manifest entry (repeatable — duplicates are legitimate). */
38508
39368
  addManifestInstance(entry, opts = {}) {
@@ -39014,7 +39874,7 @@ var SplitterLayer = class {
39014
39874
  };
39015
39875
 
39016
39876
  // src/workspace/VelaWorkspace.ts
39017
- var DEFAULT_TIMEFRAMES = ["1", "5", "15", "60", "240", "D", "W"];
39877
+ var DEFAULT_TIMEFRAMES = ["1", "5", "15", "30", "60", "240", "D", "W", "M"];
39018
39878
  var GAP_PX = 2;
39019
39879
  var POOL_CAP = 16;
39020
39880
  var TIME_AXIS_H4 = 22;