@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.
package/dist/widget.cjs CHANGED
@@ -5254,6 +5254,11 @@ var Topbar = class {
5254
5254
  /** Overrides that LEFT their native slot (default side + a declared `order`) — they
5255
5255
  * render through the flow cluster like ordinary actions. */
5256
5256
  this.flowingOverrides = /* @__PURE__ */ new Set();
5257
+ /** Tooltips of the CURRENT icon-only action buttons — rebuilt with every
5258
+ * renderActions pass (contributed buttons are replaceChildren'd away). */
5259
+ this.actionTooltips = [];
5260
+ /** `iconOnly` misuse warned once per action id (renderActions re-runs freely). */
5261
+ this.warnedIconless = /* @__PURE__ */ new Set();
5257
5262
  this.onHairlineSync = () => {
5258
5263
  if (this.hairlineRaf) return;
5259
5264
  const win = this.el.ownerDocument.defaultView;
@@ -5516,6 +5521,8 @@ var Topbar = class {
5516
5521
  this.actionsHost.replaceChildren();
5517
5522
  this.leftActionsHost.replaceChildren();
5518
5523
  for (const pin of this.pinned.values()) pin.host.replaceChildren();
5524
+ for (const t of this.actionTooltips) t.destroy();
5525
+ this.actionTooltips = [];
5519
5526
  const doc = this.actionsHost.ownerDocument;
5520
5527
  const flowLeft = this.comp.left.includes("actions");
5521
5528
  const flowRight = this.comp.right.includes("actions");
@@ -5525,10 +5532,20 @@ var Topbar = class {
5525
5532
  if (!pin && builtin.has(action.id) && !this.flowingOverrides.has(action.id)) continue;
5526
5533
  const left = pin ? pin.left : action.align === "left";
5527
5534
  if (!pin && !(left ? flowLeft : flowRight)) continue;
5535
+ const iconOnly = action.iconOnly === true && !!action.icon;
5536
+ if (action.iconOnly === true && !action.icon && !this.warnedIconless.has(action.id)) {
5537
+ this.warnedIconless.add(action.id);
5538
+ console.warn(`[vela] widget action "${action.id}": iconOnly needs an \`icon\` \u2014 rendering the label instead.`);
5539
+ }
5528
5540
  const b = doc.createElement("button");
5529
- b.className = left ? "vela-widget-action-left" : "vela-widget-action";
5541
+ b.className = left ? "vela-widget-action-left" : iconOnly ? "vela-widget-tool" : "vela-widget-action";
5530
5542
  if (action.icon) b.appendChild(iconEl(action.icon, doc));
5531
- b.appendChild(doc.createTextNode(action.label));
5543
+ if (iconOnly) {
5544
+ b.setAttribute("aria-label", action.label);
5545
+ this.actionTooltips.push(new Tooltip(b, { content: action.label, triggerId: `vela-action-${action.id}`, host: this.host }));
5546
+ } else {
5547
+ b.appendChild(doc.createTextNode(action.label));
5548
+ }
5532
5549
  b.addEventListener("click", () => {
5533
5550
  const c = this.opts.getContext?.();
5534
5551
  if (c) action.run(c);
@@ -5579,7 +5596,7 @@ var Topbar = class {
5579
5596
  this.tfMenu.destroy();
5580
5597
  this.styleMenu.destroy();
5581
5598
  this.layoutPicker?.destroy();
5582
- for (const t of [...this.tooltips, ...this.panelTooltips]) t.destroy();
5599
+ for (const t of [...this.tooltips, ...this.panelTooltips, ...this.actionTooltips]) t.destroy();
5583
5600
  this.el.remove();
5584
5601
  }
5585
5602
  /** One icon-only tool button with its kit tooltip, parked in `sink` for disposal. */
@@ -17315,19 +17332,8 @@ function registerBuiltinChartTypes() {
17315
17332
  registerChartType({ id: "heikinashi", label: "Heikin Ashi", barTransform: HEIKIN_ASHI });
17316
17333
  }
17317
17334
 
17318
- // src/workspace/sync.ts
17319
- function syncTargets(originId, setting, cellIds) {
17320
- if (setting == null || setting === false) return [];
17321
- if (setting === true) return cellIds.filter((id) => id !== originId);
17322
- const group = setting[originId];
17323
- if (group == null) return [];
17324
- return cellIds.filter((id) => id !== originId && setting[id] === group);
17325
- }
17326
- function rangesWithin(a, b, epsMs) {
17327
- return Math.abs(a.from - b.from) <= epsMs && Math.abs(a.to - b.to) <= epsMs;
17328
- }
17329
-
17330
17335
  // src/state/document.ts
17336
+ var SYNC_KINDS = ["viewport", "symbol", "timeframe", "crosshair", "drawings", "style"];
17331
17337
  function prefixedSymbol(cell) {
17332
17338
  if (!cell?.symbol) return void 0;
17333
17339
  if (cell.symbol.includes(":") || !cell.provider) return cell.symbol;
@@ -17415,7 +17421,7 @@ function sanitizeSync(raw) {
17415
17421
  if (raw == null || typeof raw !== "object") return null;
17416
17422
  const s = raw;
17417
17423
  const out = {};
17418
- for (const kind of ["viewport", "symbol", "timeframe", "crosshair", "drawings"]) {
17424
+ for (const kind of SYNC_KINDS) {
17419
17425
  const v = s[kind];
17420
17426
  if (v === true) out[kind] = true;
17421
17427
  else if (v != null && typeof v === "object") {
@@ -17458,6 +17464,28 @@ function sanitizeTrackSizes(raw) {
17458
17464
  return Object.keys(out).length > 0 ? out : null;
17459
17465
  }
17460
17466
 
17467
+ // src/workspace/sync.ts
17468
+ function syncTargets(originId, setting, cellIds) {
17469
+ if (setting == null || setting === false) return [];
17470
+ if (setting === true) return cellIds.filter((id) => id !== originId);
17471
+ const group = setting[originId];
17472
+ if (group == null) return [];
17473
+ return cellIds.filter((id) => id !== originId && setting[id] === group);
17474
+ }
17475
+ function rangesWithin(a, b, epsMs) {
17476
+ return Math.abs(a.from - b.from) <= epsMs && Math.abs(a.to - b.to) <= epsMs;
17477
+ }
17478
+ var STYLE_SYNC_CONFIG_KEYS = ["layout", "panes", "grid", "priceScale", "crosshair"];
17479
+ function styleConfigSlice(config) {
17480
+ if (config == null || typeof config !== "object") return null;
17481
+ const doc = config;
17482
+ const out = {};
17483
+ for (const key of STYLE_SYNC_CONFIG_KEYS) {
17484
+ if (doc[key] != null && typeof doc[key] === "object") out[key] = doc[key];
17485
+ }
17486
+ return Object.keys(out).length > 0 ? out : null;
17487
+ }
17488
+
17461
17489
  // src/widget/persist.ts
17462
17490
  function localStorageAdapter(storageKey) {
17463
17491
  return {
@@ -19798,12 +19826,16 @@ var IndicatorInputsDialog = class {
19798
19826
  closeOnEscape: false,
19799
19827
  footer: (foot) => {
19800
19828
  foot.append(
19829
+ this.resetAction(),
19801
19830
  this.dialogButton("Cancel", false, () => this.revertAndClose()),
19802
19831
  this.dialogButton("Ok", true, () => this.close())
19803
19832
  );
19804
19833
  },
19834
+ // Stale-guard: destroying a dialog fires its machine's onOpenChange(false)
19835
+ // asynchronously — after a reset rebuild that notification must not close
19836
+ // the replacement dialog.
19805
19837
  onOpenChange: (open2) => {
19806
- if (!open2) this.close();
19838
+ if (!open2 && this.uiDialog === ui) this.close();
19807
19839
  }
19808
19840
  });
19809
19841
  applyChromeTokens(ui.panel, t);
@@ -19934,6 +19966,30 @@ var IndicatorInputsDialog = class {
19934
19966
  }
19935
19967
  this.close();
19936
19968
  }
19969
+ /** The footer's reset button — same chip as Cancel, pinned to the LEFT edge
19970
+ * (`margin-right:auto` against the footer's flex-end keeps Cancel/Ok right). */
19971
+ resetAction() {
19972
+ const b = this.dialogButton("Reset defaults", false, () => this.resetToDefaults());
19973
+ b.style.marginRight = "auto";
19974
+ return b;
19975
+ }
19976
+ /** Restore every input to its declared default (re-running the indicator), then
19977
+ * re-open the form so each control re-reads the restored values — the same
19978
+ * rebuild-after-reset move as the chart-settings dialog. The open-time snapshot
19979
+ * survives the rebuild, so Cancel after a reset still reverts the whole session. */
19980
+ resetToDefaults() {
19981
+ const row = this.row;
19982
+ if (!row) return;
19983
+ const snap = this.snapshot;
19984
+ for (const inp of row.inputs) {
19985
+ if (row.values[inp.key] !== inp.defval) {
19986
+ row.values[inp.key] = inp.defval;
19987
+ this.host.onChange?.({ indicatorId: row.id, key: inp.key, value: inp.defval });
19988
+ }
19989
+ }
19990
+ this.open(row);
19991
+ if (snap) this.snapshot = snap;
19992
+ }
19937
19993
  /** Write one edit through: store it, notify the host, and re-apply the `when` gates. */
19938
19994
  commit(row, key, value) {
19939
19995
  row.values[key] = value;
@@ -31367,6 +31423,11 @@ function resizeSplit(split, dyTotal, minPx = MIN_PANE_PX) {
31367
31423
  }
31368
31424
 
31369
31425
  // src/renderers/native/backdrop/BackdropRenderer.ts
31426
+ function clipHighlightRect(x1, x2, left, right) {
31427
+ const x = Math.max(left, x1);
31428
+ const end = Math.min(right, x2);
31429
+ return end > x ? { x, width: end - x } : null;
31430
+ }
31370
31431
  var BackdropRenderer = class {
31371
31432
  constructor() {
31372
31433
  this.canvas = null;
@@ -31399,17 +31460,22 @@ var BackdropRenderer = class {
31399
31460
  /** Renderer-owned session highlight bands: full-height (all panes), behind the grid.
31400
31461
  * Session-zone washes (pre/post-market) paint first, host highlights on top. */
31401
31462
  drawHighlights(ctx, scene, coords) {
31402
- const bands = [...scene.sessionHighlightBands(), ...scene.highlights];
31403
- if (bands.length === 0) return;
31463
+ const sessions = scene.sessionHighlightBands();
31464
+ if (sessions.length > 0) {
31465
+ const left = Math.max(0, coords.logicalToX(-0.5));
31466
+ const right = Math.min(coords.width, coords.logicalToX(coords.barCount - 0.5));
31467
+ this.drawHighlightSet(ctx, sessions, coords, left, right);
31468
+ }
31469
+ this.drawHighlightSet(ctx, scene.highlights, coords, 0, coords.width);
31470
+ }
31471
+ drawHighlightSet(ctx, bands, coords, left, right) {
31404
31472
  for (const band of bands) {
31405
31473
  const x1 = coords.timeToX(band.from);
31406
31474
  const x2 = coords.timeToX(band.to);
31407
- if (x2 < 0 || x1 > coords.width || x2 <= x1) continue;
31408
- const cx = Math.max(0, x1);
31409
- const cw = Math.min(coords.width, x2) - cx;
31410
- if (cw <= 0) continue;
31475
+ const rect = clipHighlightRect(x1, x2, left, right);
31476
+ if (!rect) continue;
31411
31477
  ctx.fillStyle = band.color;
31412
- ctx.fillRect(cx, 0, cw, coords.height);
31478
+ ctx.fillRect(rect.x, 0, rect.width, coords.height);
31413
31479
  }
31414
31480
  }
31415
31481
  // ── grid ── vert/horz gate on `scene.showGrid` AND their own per-axis visibility
@@ -36151,61 +36217,144 @@ var MarketStatusTracker = class {
36151
36217
  };
36152
36218
 
36153
36219
  // src/widget/session-shading.ts
36154
- function deriveSessionZones(regular, extended) {
36220
+ var DAY_MS2 = 864e5;
36221
+ var MINUTE_MS = 6e4;
36222
+ function parseWindow(text) {
36223
+ if (typeof text !== "string") return null;
36224
+ const m = /^(\d{2})(\d{2})-(\d{2})(\d{2})$/.exec(text);
36225
+ if (!m) return null;
36226
+ const start = Number(m[1]) * 60 + Number(m[2]);
36227
+ const end = Number(m[3]) * 60 + Number(m[4]);
36228
+ if (start >= end || end > 1440) return null;
36229
+ return { start, end };
36230
+ }
36231
+ function parseSessionSpec(si) {
36232
+ const session = si?.["session"];
36233
+ if (typeof session !== "string" || session === "" || session === "24x7") return null;
36234
+ const regular = parseWindow(session);
36235
+ if (!regular) return null;
36236
+ const tz = si?.["timezone"];
36237
+ const timezone = typeof tz === "string" && tz !== "" ? tz : "Etc/UTC";
36238
+ const ext = parseWindow(si?.["session_extended"]);
36239
+ const extended = ext && ext.start <= regular.start && ext.end >= regular.end ? ext : { start: 0, end: 1440 };
36240
+ return { regular, extended, timezone };
36241
+ }
36242
+ var dtfCache = /* @__PURE__ */ new Map();
36243
+ function civilFormatter(tz) {
36244
+ const cached = dtfCache.get(tz);
36245
+ if (cached !== void 0) return cached;
36246
+ let dtf = null;
36247
+ try {
36248
+ dtf = new Intl.DateTimeFormat("en-US", {
36249
+ timeZone: tz,
36250
+ hourCycle: "h23",
36251
+ weekday: "short",
36252
+ year: "numeric",
36253
+ month: "2-digit",
36254
+ day: "2-digit",
36255
+ hour: "2-digit",
36256
+ minute: "2-digit"
36257
+ });
36258
+ } catch {
36259
+ dtf = null;
36260
+ }
36261
+ dtfCache.set(tz, dtf);
36262
+ return dtf;
36263
+ }
36264
+ function civilParts(dtf, ms) {
36265
+ const parts = dtf.formatToParts(ms);
36266
+ const get = (t) => parts.find((p) => p.type === t)?.value ?? "";
36267
+ return {
36268
+ weekday: get("weekday"),
36269
+ year: Number(get("year")),
36270
+ month: Number(get("month")),
36271
+ day: Number(get("day")),
36272
+ hour: Number(get("hour")) % 24,
36273
+ minute: Number(get("minute"))
36274
+ };
36275
+ }
36276
+ function expandSessionZones(spec, from, to) {
36155
36277
  const pre = [];
36156
36278
  const post = [];
36157
- for (const [extStart, extEnd] of extended) {
36158
- const inside = regular.filter(([s, e]) => e > extStart && s < extEnd).sort((a, b) => a[0] - b[0]);
36159
- let cursor = extStart;
36160
- for (const [regStart, regEnd] of inside) {
36161
- if (regStart > cursor) pre.push([cursor, Math.min(regStart, extEnd)]);
36162
- cursor = Math.max(cursor, regEnd);
36163
- }
36164
- if (cursor < extEnd) post.push([cursor, extEnd]);
36279
+ const dtf = civilFormatter(spec.timezone);
36280
+ if (!dtf || !Number.isFinite(from) || !Number.isFinite(to)) return { pre, post };
36281
+ const seen = /* @__PURE__ */ new Set();
36282
+ for (let cursor = from - DAY_MS2; cursor < to + DAY_MS2; cursor += DAY_MS2) {
36283
+ const civil = civilParts(dtf, cursor);
36284
+ const key = civil.year * 1e4 + civil.month * 100 + civil.day;
36285
+ if (!Number.isFinite(key) || seen.has(key)) continue;
36286
+ seen.add(key);
36287
+ if (civil.weekday === "Sat" || civil.weekday === "Sun") continue;
36288
+ const naive = Date.UTC(civil.year, civil.month - 1, civil.day);
36289
+ const noonGuess = naive + 720 * MINUTE_MS;
36290
+ const atNoon = civilParts(dtf, noonGuess);
36291
+ const offset = Date.UTC(atNoon.year, atNoon.month - 1, atNoon.day, atNoon.hour, atNoon.minute) - noonGuess;
36292
+ const at = (minutes) => naive + minutes * MINUTE_MS - offset;
36293
+ if (spec.extended.start < spec.regular.start) pre.push([at(spec.extended.start), at(spec.regular.start)]);
36294
+ if (spec.regular.end < spec.extended.end) post.push([at(spec.regular.end), at(spec.extended.end)]);
36165
36295
  }
36166
36296
  return { pre, post };
36167
36297
  }
36168
- var FETCH_AHEAD_MS2 = 10 * 864e5;
36169
- var MIN_LOOKBACK_MS = 3 * 864e5;
36170
- var MAX_LOOKBACK_MS = 120 * 864e5;
36298
+ var COVER_PAD_MIN_MS = 2 * DAY_MS2;
36171
36299
  var SessionShadingTracker = class {
36172
36300
  constructor(onZones) {
36173
36301
  this.onZones = onZones;
36174
- /** Invalidates detached async work — bumped by every track()/stop(). */
36302
+ /** Invalidates the one async step (metadata resolution) — bumped by track()/stop(). */
36175
36303
  this.epoch = 0;
36176
- }
36177
- /** (Re)bind to a chart's data surface + market and evaluate once. */
36304
+ this.spec = null;
36305
+ this.ready = false;
36306
+ this.session = "regular";
36307
+ this.covered = null;
36308
+ /** The newest range seen — viewport moves during metadata resolution (a load's fit
36309
+ * animation) must not be lost, so the resolution always expands the LATEST range. */
36310
+ this.lastRange = null;
36311
+ }
36312
+ /** (Re)bind to a chart's data surface + market and expand once metadata lands. */
36178
36313
  track(data, symbol, opts) {
36179
36314
  const my = ++this.epoch;
36180
- void this.evaluate(my, data, symbol, opts);
36315
+ this.ready = false;
36316
+ this.spec = null;
36317
+ this.covered = null;
36318
+ this.session = opts.session;
36319
+ this.lastRange = opts.range;
36320
+ void data.symbolInfo(symbol).catch(() => void 0).then((si) => {
36321
+ if (my !== this.epoch) return;
36322
+ this.ready = true;
36323
+ this.spec = parseSessionSpec(si);
36324
+ this.emit(this.lastRange ?? opts.range, true);
36325
+ });
36326
+ }
36327
+ /** Follow a pan/zoom synchronously: bands are epoch-anchored, so only a range that
36328
+ * leaves the last expansion's coverage needs a recompute — no fetch, no debounce. */
36329
+ updateRange(range) {
36330
+ this.lastRange = range;
36331
+ if (!this.ready) return;
36332
+ this.emit(range, false);
36181
36333
  }
36182
36334
  stop() {
36183
36335
  this.epoch += 1;
36184
- }
36185
- async evaluate(my, data, symbol, opts) {
36186
- const resolved = data.resolve(symbol);
36187
- const provider = resolved ? data.providerInstance(resolved.provider) : void 0;
36188
- const si = await data.symbolInfo(symbol).catch(() => void 0);
36189
- if (my !== this.epoch) return;
36190
- const hasSessions = typeof si?.session === "string" && si.session !== "" && si.session !== "24x7";
36191
- if (!provider?.getCalendar || !hasSessions || !resolved) {
36192
- this.onZones(null);
36336
+ this.ready = false;
36337
+ this.spec = null;
36338
+ this.covered = null;
36339
+ this.lastRange = null;
36340
+ }
36341
+ emit(range, force) {
36342
+ if (!this.spec) {
36343
+ if (force) this.onZones(null);
36193
36344
  return;
36194
36345
  }
36195
- if (opts.session !== "extended") {
36196
- this.onZones({ pre: [], post: [] });
36346
+ if (this.session !== "extended") {
36347
+ if (force) this.onZones({ pre: [], post: [] });
36197
36348
  return;
36198
36349
  }
36199
- const now = Date.now();
36200
- const lookback = Math.max(MIN_LOOKBACK_MS, Math.min(MAX_LOOKBACK_MS, opts.lookbackMs));
36201
- const range = { from: now - lookback, to: now + FETCH_AHEAD_MS2 };
36202
- const [regular, extended] = await Promise.all([
36203
- provider.getCalendar(resolved.ticker, { ...range, session: "regular" }).catch(() => null),
36204
- provider.getCalendar(resolved.ticker, { ...range, session: "extended" }).catch(() => null)
36205
- ]);
36206
- if (my !== this.epoch) return;
36207
- if (!regular || !extended) return;
36208
- this.onZones(deriveSessionZones(regular, extended));
36350
+ const from = Math.min(range.from, range.to);
36351
+ const to = Math.max(range.from, range.to);
36352
+ if (!Number.isFinite(from) || !Number.isFinite(to)) return;
36353
+ if (!force && this.covered && from >= this.covered.from && to <= this.covered.to) return;
36354
+ const pad = Math.max(to - from, COVER_PAD_MIN_MS);
36355
+ const covered = { from: from - pad, to: to + pad };
36356
+ this.covered = covered;
36357
+ this.onZones(expandSessionZones(this.spec, covered.from, covered.to));
36209
36358
  }
36210
36359
  };
36211
36360
 
@@ -36642,7 +36791,11 @@ var ChartCell = class {
36642
36791
  this.deps.toast(`No registered provider serves "${symbol2}" (registered: ${list})`, "error", 6e3);
36643
36792
  });
36644
36793
  this.inner.on("load:start", () => this.watermark?.setLoading(true));
36645
- this.inner.on("load:end", () => this.watermark?.setLoading(false));
36794
+ this.inner.on("load:end", () => {
36795
+ this.watermark?.setLoading(false);
36796
+ this.refreshSessionShading();
36797
+ });
36798
+ this.inner.on("viewport:changed", (range) => this.sessionShading.updateRange(range));
36646
36799
  const tz = deps.timezone();
36647
36800
  if (tz !== "Etc/UTC") this.inner.renderer.set("timezone", tz);
36648
36801
  this.indicatorTitlesOn = seed.indicatorTitles ?? true;
@@ -36769,14 +36922,18 @@ var ChartCell = class {
36769
36922
  this.refreshSessionShading();
36770
36923
  });
36771
36924
  }
36772
- /** (Re)derive the pre/post-market shading bands for this cell's market. The loaded
36773
- * depth (bars × timeframe) bounds the calendar fetch; the tracker clamps it. */
36925
+ /** (Re)derive the pre/post-market shading bands for this cell's market. The bands
36926
+ * expand locally from the symbol's session vocabulary, so they paint as soon as
36927
+ * metadata is known and follow any pan depth without provider round trips. */
36774
36928
  refreshSessionShading() {
36775
36929
  const chart = this.inner;
36776
36930
  const symbol = this.state.symbol;
36777
36931
  if (!chart || !symbol) return;
36778
- const lookbackMs = Math.max(this.state.bars ?? 1e3, this.rangeBars) * timeframeMs(this.state.timeframe ?? "60");
36779
- this.sessionShading.track(chart.data, symbol, { session: this.session, lookbackMs });
36932
+ const now = Date.now();
36933
+ const requestedSpan = Math.max(this.state.bars ?? 1e3, this.rangeBars) * timeframeMs(this.state.timeframe ?? "60");
36934
+ const fallbackSpan = Number.isFinite(requestedSpan) ? Math.max(3 * 864e5, requestedSpan) : 3 * 864e5;
36935
+ const range = chart.getVisibleRange() ?? { from: now - fallbackSpan, to: now };
36936
+ this.sessionShading.track(chart.data, symbol, { session: this.session, range });
36780
36937
  }
36781
36938
  /** The session-shade colors live in the renderer CONFIG (persisted with it, edited
36782
36939
  * live by the dialog swatch) — the cell only proxies them into its settings rows. */
@@ -36872,10 +37029,10 @@ var ChartCell = class {
36872
37029
  id: "status-line",
36873
37030
  rows: [
36874
37031
  { kind: "heading", label: "Status line", id: "parts" },
36875
- { kind: "toggle", label: "Symbol name", id: "name", get: () => sl.partVisible("name"), set: (v) => sl.setPartVisible("name", v) },
36876
- { kind: "toggle", label: "Market status", id: "market", get: () => sl.partVisible("market"), set: (v) => sl.setPartVisible("market", v) },
36877
- { kind: "toggle", label: "OHLC values", id: "ohlc", get: () => sl.partVisible("ohlc"), set: (v) => sl.setPartVisible("ohlc", v) },
36878
- { kind: "toggle", label: "Bar change values", id: "change", get: () => sl.partVisible("change"), set: (v) => sl.setPartVisible("change", v) },
37032
+ { kind: "toggle", label: "Symbol name", id: "name", get: () => sl.partVisible("name"), set: (v) => this.setStatuslinePart("name", v) },
37033
+ { kind: "toggle", label: "Market status", id: "market", get: () => sl.partVisible("market"), set: (v) => this.setStatuslinePart("market", v) },
37034
+ { kind: "toggle", label: "OHLC values", id: "ohlc", get: () => sl.partVisible("ohlc"), set: (v) => this.setStatuslinePart("ohlc", v) },
37035
+ { kind: "toggle", label: "Bar change values", id: "change", get: () => sl.partVisible("change"), set: (v) => this.setStatuslinePart("change", v) },
36879
37036
  { kind: "heading", label: "Indicators", id: "indicators" },
36880
37037
  {
36881
37038
  kind: "toggle",
@@ -36910,12 +37067,40 @@ var ChartCell = class {
36910
37067
  this.indicatorTitlesOn = visible;
36911
37068
  this.inner?.renderer.set("indicatorTitles", visible);
36912
37069
  this.deps.onStateDirty();
37070
+ this.deps.onStatusPrefsChanged(this.id);
36913
37071
  }
36914
37072
  /** Show/hide the plot values beside this cell's legend titles (persisted per cell). */
36915
37073
  setIndicatorValuesVisible(visible) {
36916
37074
  this.indicatorValuesOn = visible;
36917
37075
  this.inner?.renderer.set("indicatorValues", visible);
36918
37076
  this.deps.onStateDirty();
37077
+ this.deps.onStatusPrefsChanged(this.id);
37078
+ }
37079
+ /** Show/hide one status-line segment (the settings dialog's Status line tab). */
37080
+ setStatuslinePart(part, visible) {
37081
+ this.statusline?.setPartVisible(part, visible);
37082
+ this.deps.onStatusPrefsChanged(this.id);
37083
+ }
37084
+ /** This cell's Status line tab prefs as one bundle (see {@link CellStatusPrefs}). */
37085
+ statusPrefs() {
37086
+ const sl = this.statusline;
37087
+ return {
37088
+ parts: sl ? { name: sl.partVisible("name"), market: sl.partVisible("market"), ohlc: sl.partVisible("ohlc"), change: sl.partVisible("change") } : null,
37089
+ indicatorTitles: this.indicatorTitlesOn,
37090
+ indicatorValues: this.indicatorValuesOn
37091
+ };
37092
+ }
37093
+ /** Converge this cell's Status line tab prefs to `prefs` — the follower half of
37094
+ * the workspace's style link. Idempotent: matching values change nothing, so a
37095
+ * propagated echo dies on its own. */
37096
+ applyStatusPrefs(prefs) {
37097
+ if (prefs.parts && this.statusline) {
37098
+ for (const part of Object.keys(prefs.parts)) {
37099
+ if (this.statusline.partVisible(part) !== prefs.parts[part]) this.statusline.setPartVisible(part, prefs.parts[part]);
37100
+ }
37101
+ }
37102
+ if (prefs.indicatorTitles !== this.indicatorTitlesOn) this.setIndicatorTitlesVisible(prefs.indicatorTitles);
37103
+ if (prefs.indicatorValues !== this.indicatorValuesOn) this.setIndicatorValuesVisible(prefs.indicatorValues);
36919
37104
  }
36920
37105
  /** The LIVE chart of this cell — never cache it across a layout change (the cell's
36921
37106
  * identity is what endures; the chart dies with the cell). */
@@ -37322,16 +37507,21 @@ var ChartCell = class {
37322
37507
 
37323
37508
  // src/workspace/context.ts
37324
37509
  function buildContext(host) {
37325
- const active = host.active();
37326
37510
  return {
37327
37511
  get chart() {
37328
37512
  const cell = host.active();
37329
37513
  if (!cell) throw new Error("VelaWorkspace has no active cell yet");
37330
37514
  return cell.chart;
37331
37515
  },
37332
- symbol: active?.symbol ?? "",
37333
- timeframe: active?.timeframe ?? "60",
37334
- priceStyle: active?.priceStyle ?? "candles",
37516
+ get symbol() {
37517
+ return host.active()?.symbol ?? "";
37518
+ },
37519
+ get timeframe() {
37520
+ return host.active()?.timeframe ?? "60";
37521
+ },
37522
+ get priceStyle() {
37523
+ return host.active()?.priceStyle ?? "candles";
37524
+ },
37335
37525
  setSymbol: (symbol) => host.active()?.setSymbol(symbol),
37336
37526
  setTimeframe: (tf) => host.active()?.setTimeframe(tf),
37337
37527
  setPriceStyle: (style) => host.active()?.setPriceStyle(style),
@@ -37342,8 +37532,12 @@ function buildContext(host) {
37342
37532
  addIndicator: (entry) => host.active()?.addExternalIndicator(entry),
37343
37533
  addNativeIndicator: (type) => host.active()?.addNative(type),
37344
37534
  stateChanged: () => host.stateDirty(),
37345
- cells: host.cells().map((c) => ({ id: c.id, chart: c.chart, symbol: c.symbol, timeframe: c.timeframe })),
37346
- activeCellId: active?.id ?? "",
37535
+ get cells() {
37536
+ return host.cells().map((c) => ({ id: c.id, chart: c.chart, symbol: c.symbol, timeframe: c.timeframe }));
37537
+ },
37538
+ get activeCellId() {
37539
+ return host.active()?.id ?? "";
37540
+ },
37347
37541
  setActiveCell: (id) => host.setActiveCell(id)
37348
37542
  };
37349
37543
  }
@@ -37676,6 +37870,10 @@ var VelaWorkspace = class {
37676
37870
  /** Same guard for the drawings link: the propagated mutations' own `drawing:*`
37677
37871
  * events fire synchronously inside the propagation loop and must not fan out again. */
37678
37872
  this.drawingSyncBusy = false;
37873
+ /** Same guard for the style link: a follower's `applyConfig` re-fires its
37874
+ * `onConfigChanged` in the same tick, and a state restore applies per-cell
37875
+ * configs that legitimately differ — neither must propagate. */
37876
+ this.styleSyncBusy = false;
37679
37877
  /** LINKED drawings (the drawings sync): one map per synced set (cellId → that
37680
37878
  * cell's drawing id), reachable from every member under its `cellId\0drawingId`
37681
37879
  * key — any member finds its peers to push edits/removals onto. Survives a
@@ -37736,9 +37934,7 @@ var VelaWorkspace = class {
37736
37934
  if (boot?.favorites) this.favs = [...boot.favorites];
37737
37935
  if (boot?.timeframeFavorites) this.tfFavs = [...boot.timeframeFavorites];
37738
37936
  const sync = boot?.sync ?? opts.sync;
37739
- for (const kind of ["viewport", "symbol", "timeframe", "crosshair", "drawings"]) {
37740
- this.applySyncSetting(kind, sync?.[kind]);
37741
- }
37937
+ for (const kind of SYNC_KINDS) this.applySyncSetting(kind, sync?.[kind]);
37742
37938
  this.monoLayout = opts.layout === false;
37743
37939
  const optLayout = opts.layout === false || opts.layout === void 0 ? "4" : opts.layout;
37744
37940
  this.def = this.resolveLayout(this.monoLayout ? "1" : boot?.layout && ensureLayout(boot.layout) ? boot.layout : optLayout);
@@ -37822,7 +38018,8 @@ var VelaWorkspace = class {
37822
38018
  syncs: () => [
37823
38019
  { id: "symbol", label: "Symbol", checked: this.syncOpts.symbol === true },
37824
38020
  { id: "timeframe", label: "Interval", checked: this.syncOpts.timeframe === true },
37825
- { id: "crosshair", label: "Crosshair", checked: this.syncOpts.crosshair === true }
38021
+ { id: "crosshair", label: "Crosshair", checked: this.syncOpts.crosshair === true },
38022
+ { id: "style", label: "Style", checked: this.syncOpts.style === true }
37826
38023
  ],
37827
38024
  onToggleSync: (id) => {
37828
38025
  const kind = id;
@@ -38106,7 +38303,7 @@ var VelaWorkspace = class {
38106
38303
  this.topbar.setTimeframeFavorites(this.tfFavs);
38107
38304
  }
38108
38305
  this.dock.applyState(st.panels);
38109
- for (const kind of ["viewport", "symbol", "timeframe", "crosshair", "drawings"]) this.applySyncSetting(kind, st.sync?.[kind]);
38306
+ for (const kind of SYNC_KINDS) this.applySyncSetting(kind, st.sync?.[kind]);
38110
38307
  this.trackSizes.clear();
38111
38308
  if (st.trackSizes) for (const [id, ts] of Object.entries(st.trackSizes)) this.trackSizes.set(id, ts);
38112
38309
  const targetDef = this.monoLayout ? this.def : ensureLayout(st.layout) ?? this.def;
@@ -38117,9 +38314,14 @@ var VelaWorkspace = class {
38117
38314
  for (const cell of this.cellsById.values()) cell.chart.drawings.setFavorites(this.favs);
38118
38315
  }
38119
38316
  this.drawingLinks.clear();
38120
- for (const [i] of this.def.cells.entries()) {
38121
- const { id, ...cs } = st.charts[i];
38122
- this.cellsById.get(id)?.rehydrate(cs);
38317
+ this.styleSyncBusy = true;
38318
+ try {
38319
+ for (const [i] of this.def.cells.entries()) {
38320
+ const { id, ...cs } = st.charts[i];
38321
+ this.cellsById.get(id)?.rehydrate(cs);
38322
+ }
38323
+ } finally {
38324
+ this.styleSyncBusy = false;
38123
38325
  }
38124
38326
  if (st.timezone) this.setTimezone(st.timezone);
38125
38327
  this.pool.clear();
@@ -38219,6 +38421,7 @@ var VelaWorkspace = class {
38219
38421
  const rebuildAll = nextBackend !== this.cellBackend;
38220
38422
  this.order = orderAfterLayout(this.order, next.cells.length, this.activeId);
38221
38423
  const keep = new Set(this.order.slice(0, next.cells.length));
38424
+ const preexisting = new Set(this.cellsById.keys());
38222
38425
  for (const [id, cell] of [...this.cellsById]) {
38223
38426
  if (!keep.has(id) || rebuildAll) {
38224
38427
  this.poolSet(id, cell.dehydrate());
@@ -38231,6 +38434,7 @@ var VelaWorkspace = class {
38231
38434
  this.cellBackend = nextBackend;
38232
38435
  this.applyGrid();
38233
38436
  this.buildCells();
38437
+ this.alignNewCellStyles(preexisting);
38234
38438
  this.syncCellPresentation();
38235
38439
  this.topbar.setLayout(next.id);
38236
38440
  const nextActive = activeAfterLayout(this.activeId, this.order.slice(0, next.cells.length));
@@ -38434,6 +38638,7 @@ var VelaWorkspace = class {
38434
38638
  onMarketChanged: (id2) => this.onCellMarketChanged(id2),
38435
38639
  onPriceStyleChanged: (id2) => this.onCellPriceStyleChanged(id2),
38436
38640
  onIndicatorsChanged: (id2) => this.onCellIndicatorsChanged(id2),
38641
+ onStatusPrefsChanged: (id2) => this.propagateStylePrefs(id2),
38437
38642
  onStateDirty: () => this.markStateDirty(),
38438
38643
  manifestSettled: () => this.manifestSettled,
38439
38644
  toast: (message, kind, durationMs) => this.toastHost.show(message, kind, durationMs)
@@ -38509,6 +38714,7 @@ var VelaWorkspace = class {
38509
38714
  this.drawToolbar?.setEraserActive(mode === "eraser");
38510
38715
  });
38511
38716
  chart.on("viewport:changed", (range) => this.propagateViewport(cell.id, range));
38717
+ chart.renderer.onConfigChanged(() => this.propagateStylePrefs(cell.id));
38512
38718
  chart.on("theme:changed", (t) => this.setTheme(t));
38513
38719
  chart.renderer.onAxisLongPress((e) => {
38514
38720
  if (this.layoutCtl.current !== "mobile") return;
@@ -38541,11 +38747,67 @@ var VelaWorkspace = class {
38541
38747
  if (kind === "viewport") {
38542
38748
  const range = this.cellsById.get(this.activeId)?.chart.getVisibleRange();
38543
38749
  if (range) this.propagateViewport(this.activeId, range);
38750
+ } else if (kind === "style") {
38751
+ this.propagateStylePrefs(this.activeId);
38544
38752
  } else {
38545
38753
  this.propagateMarket(this.activeId);
38546
38754
  }
38547
38755
  }
38548
38756
  }
38757
+ /**
38758
+ * Align cells minted by a layout change to their style group: with the link on, a
38759
+ * NEW cell (fresh slot or one returning from the pool, which missed edits while
38760
+ * dormant) inherits the presentation of a pre-existing group peer — the active
38761
+ * cell when it is one — instead of sitting on its own state beside a styled
38762
+ * group. Propagation runs FROM the peer, so a newborn's defaults never overwrite
38763
+ * the group, and the equality short-circuits keep converged peers untouched.
38764
+ */
38765
+ alignNewCellStyles(preexisting) {
38766
+ const setting = this.syncOpts.style;
38767
+ if (!setting) return;
38768
+ const ids = [...this.cellsById.keys()];
38769
+ const propagated = /* @__PURE__ */ new Set();
38770
+ for (const id of ids) {
38771
+ if (preexisting.has(id)) continue;
38772
+ const peers = syncTargets(id, setting, ids).filter((p) => preexisting.has(p));
38773
+ if (peers.length === 0) continue;
38774
+ const source = this.activeId && peers.includes(this.activeId) ? this.activeId : peers[0];
38775
+ if (propagated.has(source)) continue;
38776
+ propagated.add(source);
38777
+ this.propagateStylePrefs(source);
38778
+ }
38779
+ }
38780
+ /**
38781
+ * Mirror an origin cell's presentation — the Canvas + Scales-and-lines slice of
38782
+ * its renderer config plus its Status line tab prefs — onto its same-group
38783
+ * followers (the style link). Loop-safe two ways: the busy guard eats the
38784
+ * followers' SYNCHRONOUS echoes (their `applyConfig` re-fires `onConfigChanged`
38785
+ * in the same tick), and the equality short-circuits leave already-converged
38786
+ * followers untouched, so nothing re-emits once the group agrees.
38787
+ */
38788
+ propagateStylePrefs(originId) {
38789
+ if (this.styleSyncBusy || this.destroyed) return;
38790
+ const targets = syncTargets(originId, this.syncOpts.style, [...this.cellsById.keys()]);
38791
+ if (targets.length === 0) return;
38792
+ const origin = this.cellsById.get(originId);
38793
+ if (!origin) return;
38794
+ const slice = styleConfigSlice(origin.chart.renderer.getConfig());
38795
+ const sliceJson = slice ? JSON.stringify(slice) : null;
38796
+ const prefs = origin.statusPrefs();
38797
+ this.styleSyncBusy = true;
38798
+ try {
38799
+ for (const id of targets) {
38800
+ const cell = this.cellsById.get(id);
38801
+ if (!cell) continue;
38802
+ if (slice && sliceJson !== JSON.stringify(styleConfigSlice(cell.chart.renderer.getConfig()))) {
38803
+ cell.chart.renderer.applyConfig(slice);
38804
+ }
38805
+ cell.applyStatusPrefs(prefs);
38806
+ }
38807
+ } finally {
38808
+ this.styleSyncBusy = false;
38809
+ }
38810
+ }
38549
38811
  /**
38550
38812
  * Mirror an origin cell's pointer time onto its same-group followers as GHOST
38551
38813
  * crosshairs (`renderer.setExternalCrosshair`). The horizontal price level rides
@@ -38853,7 +39115,8 @@ var VelaWorkspace = class {
38853
39115
  syncs: () => [
38854
39116
  { id: "symbol", label: "Symbol", checked: this.syncOpts.symbol === true },
38855
39117
  { id: "timeframe", label: "Interval", checked: this.syncOpts.timeframe === true },
38856
- { id: "crosshair", label: "Crosshair", checked: this.syncOpts.crosshair === true }
39118
+ { id: "crosshair", label: "Crosshair", checked: this.syncOpts.crosshair === true },
39119
+ { id: "style", label: "Style", checked: this.syncOpts.style === true }
38857
39120
  ],
38858
39121
  onToggleSync: (id) => {
38859
39122
  const kind = id;