@luxalgo/vela 0.6.7 → 0.6.8

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.
@@ -5200,6 +5200,11 @@ var Topbar = class {
5200
5200
  /** Overrides that LEFT their native slot (default side + a declared `order`) — they
5201
5201
  * render through the flow cluster like ordinary actions. */
5202
5202
  this.flowingOverrides = /* @__PURE__ */ new Set();
5203
+ /** Tooltips of the CURRENT icon-only action buttons — rebuilt with every
5204
+ * renderActions pass (contributed buttons are replaceChildren'd away). */
5205
+ this.actionTooltips = [];
5206
+ /** `iconOnly` misuse warned once per action id (renderActions re-runs freely). */
5207
+ this.warnedIconless = /* @__PURE__ */ new Set();
5203
5208
  this.onHairlineSync = () => {
5204
5209
  if (this.hairlineRaf) return;
5205
5210
  const win = this.el.ownerDocument.defaultView;
@@ -5462,6 +5467,8 @@ var Topbar = class {
5462
5467
  this.actionsHost.replaceChildren();
5463
5468
  this.leftActionsHost.replaceChildren();
5464
5469
  for (const pin of this.pinned.values()) pin.host.replaceChildren();
5470
+ for (const t of this.actionTooltips) t.destroy();
5471
+ this.actionTooltips = [];
5465
5472
  const doc = this.actionsHost.ownerDocument;
5466
5473
  const flowLeft = this.comp.left.includes("actions");
5467
5474
  const flowRight = this.comp.right.includes("actions");
@@ -5471,10 +5478,20 @@ var Topbar = class {
5471
5478
  if (!pin && builtin.has(action.id) && !this.flowingOverrides.has(action.id)) continue;
5472
5479
  const left = pin ? pin.left : action.align === "left";
5473
5480
  if (!pin && !(left ? flowLeft : flowRight)) continue;
5481
+ const iconOnly = action.iconOnly === true && !!action.icon;
5482
+ if (action.iconOnly === true && !action.icon && !this.warnedIconless.has(action.id)) {
5483
+ this.warnedIconless.add(action.id);
5484
+ console.warn(`[vela] widget action "${action.id}": iconOnly needs an \`icon\` \u2014 rendering the label instead.`);
5485
+ }
5474
5486
  const b = doc.createElement("button");
5475
- b.className = left ? "vela-widget-action-left" : "vela-widget-action";
5487
+ b.className = left ? "vela-widget-action-left" : iconOnly ? "vela-widget-tool" : "vela-widget-action";
5476
5488
  if (action.icon) b.appendChild(iconEl(action.icon, doc));
5477
- b.appendChild(doc.createTextNode(action.label));
5489
+ if (iconOnly) {
5490
+ b.setAttribute("aria-label", action.label);
5491
+ this.actionTooltips.push(new Tooltip(b, { content: action.label, triggerId: `vela-action-${action.id}`, host: this.host }));
5492
+ } else {
5493
+ b.appendChild(doc.createTextNode(action.label));
5494
+ }
5478
5495
  b.addEventListener("click", () => {
5479
5496
  const c = this.opts.getContext?.();
5480
5497
  if (c) action.run(c);
@@ -5525,7 +5542,7 @@ var Topbar = class {
5525
5542
  this.tfMenu.destroy();
5526
5543
  this.styleMenu.destroy();
5527
5544
  this.layoutPicker?.destroy();
5528
- for (const t of [...this.tooltips, ...this.panelTooltips]) t.destroy();
5545
+ for (const t of [...this.tooltips, ...this.panelTooltips, ...this.actionTooltips]) t.destroy();
5529
5546
  this.el.remove();
5530
5547
  }
5531
5548
  /** One icon-only tool button with its kit tooltip, parked in `sink` for disposal. */
@@ -17233,19 +17250,8 @@ function registerBuiltinChartTypes() {
17233
17250
  registerChartType({ id: "heikinashi", label: "Heikin Ashi", barTransform: HEIKIN_ASHI });
17234
17251
  }
17235
17252
 
17236
- // src/workspace/sync.ts
17237
- function syncTargets(originId, setting, cellIds) {
17238
- if (setting == null || setting === false) return [];
17239
- if (setting === true) return cellIds.filter((id) => id !== originId);
17240
- const group = setting[originId];
17241
- if (group == null) return [];
17242
- return cellIds.filter((id) => id !== originId && setting[id] === group);
17243
- }
17244
- function rangesWithin(a, b, epsMs) {
17245
- return Math.abs(a.from - b.from) <= epsMs && Math.abs(a.to - b.to) <= epsMs;
17246
- }
17247
-
17248
17253
  // src/state/document.ts
17254
+ var SYNC_KINDS = ["viewport", "symbol", "timeframe", "crosshair", "drawings", "style"];
17249
17255
  function prefixedSymbol(cell) {
17250
17256
  if (!cell?.symbol) return void 0;
17251
17257
  if (cell.symbol.includes(":") || !cell.provider) return cell.symbol;
@@ -17333,7 +17339,7 @@ function sanitizeSync(raw) {
17333
17339
  if (raw == null || typeof raw !== "object") return null;
17334
17340
  const s = raw;
17335
17341
  const out = {};
17336
- for (const kind of ["viewport", "symbol", "timeframe", "crosshair", "drawings"]) {
17342
+ for (const kind of SYNC_KINDS) {
17337
17343
  const v = s[kind];
17338
17344
  if (v === true) out[kind] = true;
17339
17345
  else if (v != null && typeof v === "object") {
@@ -17376,6 +17382,28 @@ function sanitizeTrackSizes(raw) {
17376
17382
  return Object.keys(out).length > 0 ? out : null;
17377
17383
  }
17378
17384
 
17385
+ // src/workspace/sync.ts
17386
+ function syncTargets(originId, setting, cellIds) {
17387
+ if (setting == null || setting === false) return [];
17388
+ if (setting === true) return cellIds.filter((id) => id !== originId);
17389
+ const group = setting[originId];
17390
+ if (group == null) return [];
17391
+ return cellIds.filter((id) => id !== originId && setting[id] === group);
17392
+ }
17393
+ function rangesWithin(a, b, epsMs) {
17394
+ return Math.abs(a.from - b.from) <= epsMs && Math.abs(a.to - b.to) <= epsMs;
17395
+ }
17396
+ var STYLE_SYNC_CONFIG_KEYS = ["layout", "panes", "grid", "priceScale", "crosshair"];
17397
+ function styleConfigSlice(config) {
17398
+ if (config == null || typeof config !== "object") return null;
17399
+ const doc = config;
17400
+ const out = {};
17401
+ for (const key of STYLE_SYNC_CONFIG_KEYS) {
17402
+ if (doc[key] != null && typeof doc[key] === "object") out[key] = doc[key];
17403
+ }
17404
+ return Object.keys(out).length > 0 ? out : null;
17405
+ }
17406
+
17379
17407
  // src/workspace/persist.ts
17380
17408
  var memoryStore = /* @__PURE__ */ new Map();
17381
17409
  function memoryStorageAdapter() {
@@ -19730,12 +19758,16 @@ var IndicatorInputsDialog = class {
19730
19758
  closeOnEscape: false,
19731
19759
  footer: (foot) => {
19732
19760
  foot.append(
19761
+ this.resetAction(),
19733
19762
  this.dialogButton("Cancel", false, () => this.revertAndClose()),
19734
19763
  this.dialogButton("Ok", true, () => this.close())
19735
19764
  );
19736
19765
  },
19766
+ // Stale-guard: destroying a dialog fires its machine's onOpenChange(false)
19767
+ // asynchronously — after a reset rebuild that notification must not close
19768
+ // the replacement dialog.
19737
19769
  onOpenChange: (open2) => {
19738
- if (!open2) this.close();
19770
+ if (!open2 && this.uiDialog === ui) this.close();
19739
19771
  }
19740
19772
  });
19741
19773
  applyChromeTokens(ui.panel, t);
@@ -19866,6 +19898,30 @@ var IndicatorInputsDialog = class {
19866
19898
  }
19867
19899
  this.close();
19868
19900
  }
19901
+ /** The footer's reset button — same chip as Cancel, pinned to the LEFT edge
19902
+ * (`margin-right:auto` against the footer's flex-end keeps Cancel/Ok right). */
19903
+ resetAction() {
19904
+ const b = this.dialogButton("Reset defaults", false, () => this.resetToDefaults());
19905
+ b.style.marginRight = "auto";
19906
+ return b;
19907
+ }
19908
+ /** Restore every input to its declared default (re-running the indicator), then
19909
+ * re-open the form so each control re-reads the restored values — the same
19910
+ * rebuild-after-reset move as the chart-settings dialog. The open-time snapshot
19911
+ * survives the rebuild, so Cancel after a reset still reverts the whole session. */
19912
+ resetToDefaults() {
19913
+ const row = this.row;
19914
+ if (!row) return;
19915
+ const snap = this.snapshot;
19916
+ for (const inp of row.inputs) {
19917
+ if (row.values[inp.key] !== inp.defval) {
19918
+ row.values[inp.key] = inp.defval;
19919
+ this.host.onChange?.({ indicatorId: row.id, key: inp.key, value: inp.defval });
19920
+ }
19921
+ }
19922
+ this.open(row);
19923
+ if (snap) this.snapshot = snap;
19924
+ }
19869
19925
  /** Write one edit through: store it, notify the host, and re-apply the `when` gates. */
19870
19926
  commit(row, key, value) {
19871
19927
  row.values[key] = value;
@@ -31299,6 +31355,11 @@ function resizeSplit(split, dyTotal, minPx = MIN_PANE_PX) {
31299
31355
  }
31300
31356
 
31301
31357
  // src/renderers/native/backdrop/BackdropRenderer.ts
31358
+ function clipHighlightRect(x1, x2, left, right) {
31359
+ const x = Math.max(left, x1);
31360
+ const end = Math.min(right, x2);
31361
+ return end > x ? { x, width: end - x } : null;
31362
+ }
31302
31363
  var BackdropRenderer = class {
31303
31364
  constructor() {
31304
31365
  this.canvas = null;
@@ -31331,17 +31392,22 @@ var BackdropRenderer = class {
31331
31392
  /** Renderer-owned session highlight bands: full-height (all panes), behind the grid.
31332
31393
  * Session-zone washes (pre/post-market) paint first, host highlights on top. */
31333
31394
  drawHighlights(ctx, scene, coords) {
31334
- const bands = [...scene.sessionHighlightBands(), ...scene.highlights];
31335
- if (bands.length === 0) return;
31395
+ const sessions = scene.sessionHighlightBands();
31396
+ if (sessions.length > 0) {
31397
+ const left = Math.max(0, coords.logicalToX(-0.5));
31398
+ const right = Math.min(coords.width, coords.logicalToX(coords.barCount - 0.5));
31399
+ this.drawHighlightSet(ctx, sessions, coords, left, right);
31400
+ }
31401
+ this.drawHighlightSet(ctx, scene.highlights, coords, 0, coords.width);
31402
+ }
31403
+ drawHighlightSet(ctx, bands, coords, left, right) {
31336
31404
  for (const band of bands) {
31337
31405
  const x1 = coords.timeToX(band.from);
31338
31406
  const x2 = coords.timeToX(band.to);
31339
- if (x2 < 0 || x1 > coords.width || x2 <= x1) continue;
31340
- const cx = Math.max(0, x1);
31341
- const cw = Math.min(coords.width, x2) - cx;
31342
- if (cw <= 0) continue;
31407
+ const rect = clipHighlightRect(x1, x2, left, right);
31408
+ if (!rect) continue;
31343
31409
  ctx.fillStyle = band.color;
31344
- ctx.fillRect(cx, 0, cw, coords.height);
31410
+ ctx.fillRect(rect.x, 0, rect.width, coords.height);
31345
31411
  }
31346
31412
  }
31347
31413
  // ── grid ── vert/horz gate on `scene.showGrid` AND their own per-axis visibility
@@ -36083,61 +36149,144 @@ var MarketStatusTracker = class {
36083
36149
  };
36084
36150
 
36085
36151
  // src/widget/session-shading.ts
36086
- function deriveSessionZones(regular, extended) {
36152
+ var DAY_MS2 = 864e5;
36153
+ var MINUTE_MS = 6e4;
36154
+ function parseWindow(text) {
36155
+ if (typeof text !== "string") return null;
36156
+ const m = /^(\d{2})(\d{2})-(\d{2})(\d{2})$/.exec(text);
36157
+ if (!m) return null;
36158
+ const start = Number(m[1]) * 60 + Number(m[2]);
36159
+ const end = Number(m[3]) * 60 + Number(m[4]);
36160
+ if (start >= end || end > 1440) return null;
36161
+ return { start, end };
36162
+ }
36163
+ function parseSessionSpec(si) {
36164
+ const session = si?.["session"];
36165
+ if (typeof session !== "string" || session === "" || session === "24x7") return null;
36166
+ const regular = parseWindow(session);
36167
+ if (!regular) return null;
36168
+ const tz = si?.["timezone"];
36169
+ const timezone = typeof tz === "string" && tz !== "" ? tz : "Etc/UTC";
36170
+ const ext = parseWindow(si?.["session_extended"]);
36171
+ const extended = ext && ext.start <= regular.start && ext.end >= regular.end ? ext : { start: 0, end: 1440 };
36172
+ return { regular, extended, timezone };
36173
+ }
36174
+ var dtfCache = /* @__PURE__ */ new Map();
36175
+ function civilFormatter(tz) {
36176
+ const cached = dtfCache.get(tz);
36177
+ if (cached !== void 0) return cached;
36178
+ let dtf = null;
36179
+ try {
36180
+ dtf = new Intl.DateTimeFormat("en-US", {
36181
+ timeZone: tz,
36182
+ hourCycle: "h23",
36183
+ weekday: "short",
36184
+ year: "numeric",
36185
+ month: "2-digit",
36186
+ day: "2-digit",
36187
+ hour: "2-digit",
36188
+ minute: "2-digit"
36189
+ });
36190
+ } catch {
36191
+ dtf = null;
36192
+ }
36193
+ dtfCache.set(tz, dtf);
36194
+ return dtf;
36195
+ }
36196
+ function civilParts(dtf, ms) {
36197
+ const parts = dtf.formatToParts(ms);
36198
+ const get = (t) => parts.find((p) => p.type === t)?.value ?? "";
36199
+ return {
36200
+ weekday: get("weekday"),
36201
+ year: Number(get("year")),
36202
+ month: Number(get("month")),
36203
+ day: Number(get("day")),
36204
+ hour: Number(get("hour")) % 24,
36205
+ minute: Number(get("minute"))
36206
+ };
36207
+ }
36208
+ function expandSessionZones(spec, from, to) {
36087
36209
  const pre = [];
36088
36210
  const post = [];
36089
- for (const [extStart, extEnd] of extended) {
36090
- const inside = regular.filter(([s, e]) => e > extStart && s < extEnd).sort((a, b) => a[0] - b[0]);
36091
- let cursor = extStart;
36092
- for (const [regStart, regEnd] of inside) {
36093
- if (regStart > cursor) pre.push([cursor, Math.min(regStart, extEnd)]);
36094
- cursor = Math.max(cursor, regEnd);
36095
- }
36096
- if (cursor < extEnd) post.push([cursor, extEnd]);
36211
+ const dtf = civilFormatter(spec.timezone);
36212
+ if (!dtf || !Number.isFinite(from) || !Number.isFinite(to)) return { pre, post };
36213
+ const seen = /* @__PURE__ */ new Set();
36214
+ for (let cursor = from - DAY_MS2; cursor < to + DAY_MS2; cursor += DAY_MS2) {
36215
+ const civil = civilParts(dtf, cursor);
36216
+ const key = civil.year * 1e4 + civil.month * 100 + civil.day;
36217
+ if (!Number.isFinite(key) || seen.has(key)) continue;
36218
+ seen.add(key);
36219
+ if (civil.weekday === "Sat" || civil.weekday === "Sun") continue;
36220
+ const naive = Date.UTC(civil.year, civil.month - 1, civil.day);
36221
+ const noonGuess = naive + 720 * MINUTE_MS;
36222
+ const atNoon = civilParts(dtf, noonGuess);
36223
+ const offset = Date.UTC(atNoon.year, atNoon.month - 1, atNoon.day, atNoon.hour, atNoon.minute) - noonGuess;
36224
+ const at = (minutes) => naive + minutes * MINUTE_MS - offset;
36225
+ if (spec.extended.start < spec.regular.start) pre.push([at(spec.extended.start), at(spec.regular.start)]);
36226
+ if (spec.regular.end < spec.extended.end) post.push([at(spec.regular.end), at(spec.extended.end)]);
36097
36227
  }
36098
36228
  return { pre, post };
36099
36229
  }
36100
- var FETCH_AHEAD_MS2 = 10 * 864e5;
36101
- var MIN_LOOKBACK_MS = 3 * 864e5;
36102
- var MAX_LOOKBACK_MS = 120 * 864e5;
36230
+ var COVER_PAD_MIN_MS = 2 * DAY_MS2;
36103
36231
  var SessionShadingTracker = class {
36104
36232
  constructor(onZones) {
36105
36233
  this.onZones = onZones;
36106
- /** Invalidates detached async work — bumped by every track()/stop(). */
36234
+ /** Invalidates the one async step (metadata resolution) — bumped by track()/stop(). */
36107
36235
  this.epoch = 0;
36108
- }
36109
- /** (Re)bind to a chart's data surface + market and evaluate once. */
36236
+ this.spec = null;
36237
+ this.ready = false;
36238
+ this.session = "regular";
36239
+ this.covered = null;
36240
+ /** The newest range seen — viewport moves during metadata resolution (a load's fit
36241
+ * animation) must not be lost, so the resolution always expands the LATEST range. */
36242
+ this.lastRange = null;
36243
+ }
36244
+ /** (Re)bind to a chart's data surface + market and expand once metadata lands. */
36110
36245
  track(data, symbol, opts) {
36111
36246
  const my = ++this.epoch;
36112
- void this.evaluate(my, data, symbol, opts);
36247
+ this.ready = false;
36248
+ this.spec = null;
36249
+ this.covered = null;
36250
+ this.session = opts.session;
36251
+ this.lastRange = opts.range;
36252
+ void data.symbolInfo(symbol).catch(() => void 0).then((si) => {
36253
+ if (my !== this.epoch) return;
36254
+ this.ready = true;
36255
+ this.spec = parseSessionSpec(si);
36256
+ this.emit(this.lastRange ?? opts.range, true);
36257
+ });
36258
+ }
36259
+ /** Follow a pan/zoom synchronously: bands are epoch-anchored, so only a range that
36260
+ * leaves the last expansion's coverage needs a recompute — no fetch, no debounce. */
36261
+ updateRange(range) {
36262
+ this.lastRange = range;
36263
+ if (!this.ready) return;
36264
+ this.emit(range, false);
36113
36265
  }
36114
36266
  stop() {
36115
36267
  this.epoch += 1;
36116
- }
36117
- async evaluate(my, data, symbol, opts) {
36118
- const resolved = data.resolve(symbol);
36119
- const provider = resolved ? data.providerInstance(resolved.provider) : void 0;
36120
- const si = await data.symbolInfo(symbol).catch(() => void 0);
36121
- if (my !== this.epoch) return;
36122
- const hasSessions = typeof si?.session === "string" && si.session !== "" && si.session !== "24x7";
36123
- if (!provider?.getCalendar || !hasSessions || !resolved) {
36124
- this.onZones(null);
36268
+ this.ready = false;
36269
+ this.spec = null;
36270
+ this.covered = null;
36271
+ this.lastRange = null;
36272
+ }
36273
+ emit(range, force) {
36274
+ if (!this.spec) {
36275
+ if (force) this.onZones(null);
36125
36276
  return;
36126
36277
  }
36127
- if (opts.session !== "extended") {
36128
- this.onZones({ pre: [], post: [] });
36278
+ if (this.session !== "extended") {
36279
+ if (force) this.onZones({ pre: [], post: [] });
36129
36280
  return;
36130
36281
  }
36131
- const now = Date.now();
36132
- const lookback = Math.max(MIN_LOOKBACK_MS, Math.min(MAX_LOOKBACK_MS, opts.lookbackMs));
36133
- const range = { from: now - lookback, to: now + FETCH_AHEAD_MS2 };
36134
- const [regular, extended] = await Promise.all([
36135
- provider.getCalendar(resolved.ticker, { ...range, session: "regular" }).catch(() => null),
36136
- provider.getCalendar(resolved.ticker, { ...range, session: "extended" }).catch(() => null)
36137
- ]);
36138
- if (my !== this.epoch) return;
36139
- if (!regular || !extended) return;
36140
- this.onZones(deriveSessionZones(regular, extended));
36282
+ const from = Math.min(range.from, range.to);
36283
+ const to = Math.max(range.from, range.to);
36284
+ if (!Number.isFinite(from) || !Number.isFinite(to)) return;
36285
+ if (!force && this.covered && from >= this.covered.from && to <= this.covered.to) return;
36286
+ const pad = Math.max(to - from, COVER_PAD_MIN_MS);
36287
+ const covered = { from: from - pad, to: to + pad };
36288
+ this.covered = covered;
36289
+ this.onZones(expandSessionZones(this.spec, covered.from, covered.to));
36141
36290
  }
36142
36291
  };
36143
36292
 
@@ -36574,7 +36723,11 @@ var ChartCell = class {
36574
36723
  this.deps.toast(`No registered provider serves "${symbol2}" (registered: ${list})`, "error", 6e3);
36575
36724
  });
36576
36725
  this.inner.on("load:start", () => this.watermark?.setLoading(true));
36577
- this.inner.on("load:end", () => this.watermark?.setLoading(false));
36726
+ this.inner.on("load:end", () => {
36727
+ this.watermark?.setLoading(false);
36728
+ this.refreshSessionShading();
36729
+ });
36730
+ this.inner.on("viewport:changed", (range) => this.sessionShading.updateRange(range));
36578
36731
  const tz = deps.timezone();
36579
36732
  if (tz !== "Etc/UTC") this.inner.renderer.set("timezone", tz);
36580
36733
  this.indicatorTitlesOn = seed.indicatorTitles ?? true;
@@ -36701,14 +36854,18 @@ var ChartCell = class {
36701
36854
  this.refreshSessionShading();
36702
36855
  });
36703
36856
  }
36704
- /** (Re)derive the pre/post-market shading bands for this cell's market. The loaded
36705
- * depth (bars × timeframe) bounds the calendar fetch; the tracker clamps it. */
36857
+ /** (Re)derive the pre/post-market shading bands for this cell's market. The bands
36858
+ * expand locally from the symbol's session vocabulary, so they paint as soon as
36859
+ * metadata is known and follow any pan depth without provider round trips. */
36706
36860
  refreshSessionShading() {
36707
36861
  const chart = this.inner;
36708
36862
  const symbol = this.state.symbol;
36709
36863
  if (!chart || !symbol) return;
36710
- const lookbackMs = Math.max(this.state.bars ?? 1e3, this.rangeBars) * timeframeMs(this.state.timeframe ?? "60");
36711
- this.sessionShading.track(chart.data, symbol, { session: this.session, lookbackMs });
36864
+ const now = Date.now();
36865
+ const requestedSpan = Math.max(this.state.bars ?? 1e3, this.rangeBars) * timeframeMs(this.state.timeframe ?? "60");
36866
+ const fallbackSpan = Number.isFinite(requestedSpan) ? Math.max(3 * 864e5, requestedSpan) : 3 * 864e5;
36867
+ const range = chart.getVisibleRange() ?? { from: now - fallbackSpan, to: now };
36868
+ this.sessionShading.track(chart.data, symbol, { session: this.session, range });
36712
36869
  }
36713
36870
  /** The session-shade colors live in the renderer CONFIG (persisted with it, edited
36714
36871
  * live by the dialog swatch) — the cell only proxies them into its settings rows. */
@@ -36804,10 +36961,10 @@ var ChartCell = class {
36804
36961
  id: "status-line",
36805
36962
  rows: [
36806
36963
  { kind: "heading", label: "Status line", id: "parts" },
36807
- { kind: "toggle", label: "Symbol name", id: "name", get: () => sl.partVisible("name"), set: (v) => sl.setPartVisible("name", v) },
36808
- { kind: "toggle", label: "Market status", id: "market", get: () => sl.partVisible("market"), set: (v) => sl.setPartVisible("market", v) },
36809
- { kind: "toggle", label: "OHLC values", id: "ohlc", get: () => sl.partVisible("ohlc"), set: (v) => sl.setPartVisible("ohlc", v) },
36810
- { kind: "toggle", label: "Bar change values", id: "change", get: () => sl.partVisible("change"), set: (v) => sl.setPartVisible("change", v) },
36964
+ { kind: "toggle", label: "Symbol name", id: "name", get: () => sl.partVisible("name"), set: (v) => this.setStatuslinePart("name", v) },
36965
+ { kind: "toggle", label: "Market status", id: "market", get: () => sl.partVisible("market"), set: (v) => this.setStatuslinePart("market", v) },
36966
+ { kind: "toggle", label: "OHLC values", id: "ohlc", get: () => sl.partVisible("ohlc"), set: (v) => this.setStatuslinePart("ohlc", v) },
36967
+ { kind: "toggle", label: "Bar change values", id: "change", get: () => sl.partVisible("change"), set: (v) => this.setStatuslinePart("change", v) },
36811
36968
  { kind: "heading", label: "Indicators", id: "indicators" },
36812
36969
  {
36813
36970
  kind: "toggle",
@@ -36842,12 +36999,40 @@ var ChartCell = class {
36842
36999
  this.indicatorTitlesOn = visible;
36843
37000
  this.inner?.renderer.set("indicatorTitles", visible);
36844
37001
  this.deps.onStateDirty();
37002
+ this.deps.onStatusPrefsChanged(this.id);
36845
37003
  }
36846
37004
  /** Show/hide the plot values beside this cell's legend titles (persisted per cell). */
36847
37005
  setIndicatorValuesVisible(visible) {
36848
37006
  this.indicatorValuesOn = visible;
36849
37007
  this.inner?.renderer.set("indicatorValues", visible);
36850
37008
  this.deps.onStateDirty();
37009
+ this.deps.onStatusPrefsChanged(this.id);
37010
+ }
37011
+ /** Show/hide one status-line segment (the settings dialog's Status line tab). */
37012
+ setStatuslinePart(part, visible) {
37013
+ this.statusline?.setPartVisible(part, visible);
37014
+ this.deps.onStatusPrefsChanged(this.id);
37015
+ }
37016
+ /** This cell's Status line tab prefs as one bundle (see {@link CellStatusPrefs}). */
37017
+ statusPrefs() {
37018
+ const sl = this.statusline;
37019
+ return {
37020
+ parts: sl ? { name: sl.partVisible("name"), market: sl.partVisible("market"), ohlc: sl.partVisible("ohlc"), change: sl.partVisible("change") } : null,
37021
+ indicatorTitles: this.indicatorTitlesOn,
37022
+ indicatorValues: this.indicatorValuesOn
37023
+ };
37024
+ }
37025
+ /** Converge this cell's Status line tab prefs to `prefs` — the follower half of
37026
+ * the workspace's style link. Idempotent: matching values change nothing, so a
37027
+ * propagated echo dies on its own. */
37028
+ applyStatusPrefs(prefs) {
37029
+ if (prefs.parts && this.statusline) {
37030
+ for (const part of Object.keys(prefs.parts)) {
37031
+ if (this.statusline.partVisible(part) !== prefs.parts[part]) this.statusline.setPartVisible(part, prefs.parts[part]);
37032
+ }
37033
+ }
37034
+ if (prefs.indicatorTitles !== this.indicatorTitlesOn) this.setIndicatorTitlesVisible(prefs.indicatorTitles);
37035
+ if (prefs.indicatorValues !== this.indicatorValuesOn) this.setIndicatorValuesVisible(prefs.indicatorValues);
36851
37036
  }
36852
37037
  /** The LIVE chart of this cell — never cache it across a layout change (the cell's
36853
37038
  * identity is what endures; the chart dies with the cell). */
@@ -37254,16 +37439,21 @@ var ChartCell = class {
37254
37439
 
37255
37440
  // src/workspace/context.ts
37256
37441
  function buildContext(host) {
37257
- const active = host.active();
37258
37442
  return {
37259
37443
  get chart() {
37260
37444
  const cell = host.active();
37261
37445
  if (!cell) throw new Error("VelaWorkspace has no active cell yet");
37262
37446
  return cell.chart;
37263
37447
  },
37264
- symbol: active?.symbol ?? "",
37265
- timeframe: active?.timeframe ?? "60",
37266
- priceStyle: active?.priceStyle ?? "candles",
37448
+ get symbol() {
37449
+ return host.active()?.symbol ?? "";
37450
+ },
37451
+ get timeframe() {
37452
+ return host.active()?.timeframe ?? "60";
37453
+ },
37454
+ get priceStyle() {
37455
+ return host.active()?.priceStyle ?? "candles";
37456
+ },
37267
37457
  setSymbol: (symbol) => host.active()?.setSymbol(symbol),
37268
37458
  setTimeframe: (tf) => host.active()?.setTimeframe(tf),
37269
37459
  setPriceStyle: (style) => host.active()?.setPriceStyle(style),
@@ -37274,8 +37464,12 @@ function buildContext(host) {
37274
37464
  addIndicator: (entry) => host.active()?.addExternalIndicator(entry),
37275
37465
  addNativeIndicator: (type) => host.active()?.addNative(type),
37276
37466
  stateChanged: () => host.stateDirty(),
37277
- cells: host.cells().map((c) => ({ id: c.id, chart: c.chart, symbol: c.symbol, timeframe: c.timeframe })),
37278
- activeCellId: active?.id ?? "",
37467
+ get cells() {
37468
+ return host.cells().map((c) => ({ id: c.id, chart: c.chart, symbol: c.symbol, timeframe: c.timeframe }));
37469
+ },
37470
+ get activeCellId() {
37471
+ return host.active()?.id ?? "";
37472
+ },
37279
37473
  setActiveCell: (id) => host.setActiveCell(id)
37280
37474
  };
37281
37475
  }
@@ -37614,6 +37808,10 @@ var VelaWorkspace = class {
37614
37808
  /** Same guard for the drawings link: the propagated mutations' own `drawing:*`
37615
37809
  * events fire synchronously inside the propagation loop and must not fan out again. */
37616
37810
  this.drawingSyncBusy = false;
37811
+ /** Same guard for the style link: a follower's `applyConfig` re-fires its
37812
+ * `onConfigChanged` in the same tick, and a state restore applies per-cell
37813
+ * configs that legitimately differ — neither must propagate. */
37814
+ this.styleSyncBusy = false;
37617
37815
  /** LINKED drawings (the drawings sync): one map per synced set (cellId → that
37618
37816
  * cell's drawing id), reachable from every member under its `cellId\0drawingId`
37619
37817
  * key — any member finds its peers to push edits/removals onto. Survives a
@@ -37674,9 +37872,7 @@ var VelaWorkspace = class {
37674
37872
  if (boot?.favorites) this.favs = [...boot.favorites];
37675
37873
  if (boot?.timeframeFavorites) this.tfFavs = [...boot.timeframeFavorites];
37676
37874
  const sync = boot?.sync ?? opts.sync;
37677
- for (const kind of ["viewport", "symbol", "timeframe", "crosshair", "drawings"]) {
37678
- this.applySyncSetting(kind, sync?.[kind]);
37679
- }
37875
+ for (const kind of SYNC_KINDS) this.applySyncSetting(kind, sync?.[kind]);
37680
37876
  this.monoLayout = opts.layout === false;
37681
37877
  const optLayout = opts.layout === false || opts.layout === void 0 ? "4" : opts.layout;
37682
37878
  this.def = this.resolveLayout(this.monoLayout ? "1" : boot?.layout && ensureLayout(boot.layout) ? boot.layout : optLayout);
@@ -37760,7 +37956,8 @@ var VelaWorkspace = class {
37760
37956
  syncs: () => [
37761
37957
  { id: "symbol", label: "Symbol", checked: this.syncOpts.symbol === true },
37762
37958
  { id: "timeframe", label: "Interval", checked: this.syncOpts.timeframe === true },
37763
- { id: "crosshair", label: "Crosshair", checked: this.syncOpts.crosshair === true }
37959
+ { id: "crosshair", label: "Crosshair", checked: this.syncOpts.crosshair === true },
37960
+ { id: "style", label: "Style", checked: this.syncOpts.style === true }
37764
37961
  ],
37765
37962
  onToggleSync: (id) => {
37766
37963
  const kind = id;
@@ -38044,7 +38241,7 @@ var VelaWorkspace = class {
38044
38241
  this.topbar.setTimeframeFavorites(this.tfFavs);
38045
38242
  }
38046
38243
  this.dock.applyState(st.panels);
38047
- for (const kind of ["viewport", "symbol", "timeframe", "crosshair", "drawings"]) this.applySyncSetting(kind, st.sync?.[kind]);
38244
+ for (const kind of SYNC_KINDS) this.applySyncSetting(kind, st.sync?.[kind]);
38048
38245
  this.trackSizes.clear();
38049
38246
  if (st.trackSizes) for (const [id, ts] of Object.entries(st.trackSizes)) this.trackSizes.set(id, ts);
38050
38247
  const targetDef = this.monoLayout ? this.def : ensureLayout(st.layout) ?? this.def;
@@ -38055,9 +38252,14 @@ var VelaWorkspace = class {
38055
38252
  for (const cell of this.cellsById.values()) cell.chart.drawings.setFavorites(this.favs);
38056
38253
  }
38057
38254
  this.drawingLinks.clear();
38058
- for (const [i] of this.def.cells.entries()) {
38059
- const { id, ...cs } = st.charts[i];
38060
- this.cellsById.get(id)?.rehydrate(cs);
38255
+ this.styleSyncBusy = true;
38256
+ try {
38257
+ for (const [i] of this.def.cells.entries()) {
38258
+ const { id, ...cs } = st.charts[i];
38259
+ this.cellsById.get(id)?.rehydrate(cs);
38260
+ }
38261
+ } finally {
38262
+ this.styleSyncBusy = false;
38061
38263
  }
38062
38264
  if (st.timezone) this.setTimezone(st.timezone);
38063
38265
  this.pool.clear();
@@ -38157,6 +38359,7 @@ var VelaWorkspace = class {
38157
38359
  const rebuildAll = nextBackend !== this.cellBackend;
38158
38360
  this.order = orderAfterLayout(this.order, next.cells.length, this.activeId);
38159
38361
  const keep = new Set(this.order.slice(0, next.cells.length));
38362
+ const preexisting = new Set(this.cellsById.keys());
38160
38363
  for (const [id, cell] of [...this.cellsById]) {
38161
38364
  if (!keep.has(id) || rebuildAll) {
38162
38365
  this.poolSet(id, cell.dehydrate());
@@ -38169,6 +38372,7 @@ var VelaWorkspace = class {
38169
38372
  this.cellBackend = nextBackend;
38170
38373
  this.applyGrid();
38171
38374
  this.buildCells();
38375
+ this.alignNewCellStyles(preexisting);
38172
38376
  this.syncCellPresentation();
38173
38377
  this.topbar.setLayout(next.id);
38174
38378
  const nextActive = activeAfterLayout(this.activeId, this.order.slice(0, next.cells.length));
@@ -38372,6 +38576,7 @@ var VelaWorkspace = class {
38372
38576
  onMarketChanged: (id2) => this.onCellMarketChanged(id2),
38373
38577
  onPriceStyleChanged: (id2) => this.onCellPriceStyleChanged(id2),
38374
38578
  onIndicatorsChanged: (id2) => this.onCellIndicatorsChanged(id2),
38579
+ onStatusPrefsChanged: (id2) => this.propagateStylePrefs(id2),
38375
38580
  onStateDirty: () => this.markStateDirty(),
38376
38581
  manifestSettled: () => this.manifestSettled,
38377
38582
  toast: (message, kind, durationMs) => this.toastHost.show(message, kind, durationMs)
@@ -38447,6 +38652,7 @@ var VelaWorkspace = class {
38447
38652
  this.drawToolbar?.setEraserActive(mode === "eraser");
38448
38653
  });
38449
38654
  chart.on("viewport:changed", (range) => this.propagateViewport(cell.id, range));
38655
+ chart.renderer.onConfigChanged(() => this.propagateStylePrefs(cell.id));
38450
38656
  chart.on("theme:changed", (t) => this.setTheme(t));
38451
38657
  chart.renderer.onAxisLongPress((e) => {
38452
38658
  if (this.layoutCtl.current !== "mobile") return;
@@ -38479,11 +38685,67 @@ var VelaWorkspace = class {
38479
38685
  if (kind === "viewport") {
38480
38686
  const range = this.cellsById.get(this.activeId)?.chart.getVisibleRange();
38481
38687
  if (range) this.propagateViewport(this.activeId, range);
38688
+ } else if (kind === "style") {
38689
+ this.propagateStylePrefs(this.activeId);
38482
38690
  } else {
38483
38691
  this.propagateMarket(this.activeId);
38484
38692
  }
38485
38693
  }
38486
38694
  }
38695
+ /**
38696
+ * Align cells minted by a layout change to their style group: with the link on, a
38697
+ * NEW cell (fresh slot or one returning from the pool, which missed edits while
38698
+ * dormant) inherits the presentation of a pre-existing group peer — the active
38699
+ * cell when it is one — instead of sitting on its own state beside a styled
38700
+ * group. Propagation runs FROM the peer, so a newborn's defaults never overwrite
38701
+ * the group, and the equality short-circuits keep converged peers untouched.
38702
+ */
38703
+ alignNewCellStyles(preexisting) {
38704
+ const setting = this.syncOpts.style;
38705
+ if (!setting) return;
38706
+ const ids = [...this.cellsById.keys()];
38707
+ const propagated = /* @__PURE__ */ new Set();
38708
+ for (const id of ids) {
38709
+ if (preexisting.has(id)) continue;
38710
+ const peers = syncTargets(id, setting, ids).filter((p) => preexisting.has(p));
38711
+ if (peers.length === 0) continue;
38712
+ const source = this.activeId && peers.includes(this.activeId) ? this.activeId : peers[0];
38713
+ if (propagated.has(source)) continue;
38714
+ propagated.add(source);
38715
+ this.propagateStylePrefs(source);
38716
+ }
38717
+ }
38718
+ /**
38719
+ * Mirror an origin cell's presentation — the Canvas + Scales-and-lines slice of
38720
+ * its renderer config plus its Status line tab prefs — onto its same-group
38721
+ * followers (the style link). Loop-safe two ways: the busy guard eats the
38722
+ * followers' SYNCHRONOUS echoes (their `applyConfig` re-fires `onConfigChanged`
38723
+ * in the same tick), and the equality short-circuits leave already-converged
38724
+ * followers untouched, so nothing re-emits once the group agrees.
38725
+ */
38726
+ propagateStylePrefs(originId) {
38727
+ if (this.styleSyncBusy || this.destroyed) return;
38728
+ const targets = syncTargets(originId, this.syncOpts.style, [...this.cellsById.keys()]);
38729
+ if (targets.length === 0) return;
38730
+ const origin = this.cellsById.get(originId);
38731
+ if (!origin) return;
38732
+ const slice = styleConfigSlice(origin.chart.renderer.getConfig());
38733
+ const sliceJson = slice ? JSON.stringify(slice) : null;
38734
+ const prefs = origin.statusPrefs();
38735
+ this.styleSyncBusy = true;
38736
+ try {
38737
+ for (const id of targets) {
38738
+ const cell = this.cellsById.get(id);
38739
+ if (!cell) continue;
38740
+ if (slice && sliceJson !== JSON.stringify(styleConfigSlice(cell.chart.renderer.getConfig()))) {
38741
+ cell.chart.renderer.applyConfig(slice);
38742
+ }
38743
+ cell.applyStatusPrefs(prefs);
38744
+ }
38745
+ } finally {
38746
+ this.styleSyncBusy = false;
38747
+ }
38748
+ }
38487
38749
  /**
38488
38750
  * Mirror an origin cell's pointer time onto its same-group followers as GHOST
38489
38751
  * crosshairs (`renderer.setExternalCrosshair`). The horizontal price level rides
@@ -38791,7 +39053,8 @@ var VelaWorkspace = class {
38791
39053
  syncs: () => [
38792
39054
  { id: "symbol", label: "Symbol", checked: this.syncOpts.symbol === true },
38793
39055
  { id: "timeframe", label: "Interval", checked: this.syncOpts.timeframe === true },
38794
- { id: "crosshair", label: "Crosshair", checked: this.syncOpts.crosshair === true }
39056
+ { id: "crosshair", label: "Crosshair", checked: this.syncOpts.crosshair === true },
39057
+ { id: "style", label: "Style", checked: this.syncOpts.style === true }
38795
39058
  ],
38796
39059
  onToggleSync: (id) => {
38797
39060
  const kind = id;