@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.
@@ -1,4 +1,4 @@
1
- import { parseSymbol, priceStyleIds, BUILTIN_PRICE_STYLES, tzButtonLabel, TIMEZONES, normalizeTimezone, tzMenuLabel, Vela, TypedEventBus, MultiProviderFeed, registerBuiltinChartTypes, resolveTheme, buildToolbar, createCustomMark, createAttributionMark, DrawingToolbar, applyAttributionMarkTheme, sharedBarStore, timeframeToMs } from './chunk-6WFKTAT4.js';
1
+ import { parseSymbol, priceStyleIds, BUILTIN_PRICE_STYLES, tzButtonLabel, TIMEZONES, normalizeTimezone, tzMenuLabel, Vela, TypedEventBus, MultiProviderFeed, registerBuiltinChartTypes, resolveTheme, buildToolbar, createCustomMark, createAttributionMark, DrawingToolbar, applyAttributionMarkTheme, sharedBarStore, timeframeToMs } from './chunk-SQETIBFU.js';
2
2
  import { chartType, resolveTopbarComposition, topbarActionOverride, TOPBAR_BUILTIN_IDS, widgetActions, SidePanel, sidePanels, DEFAULT_PANEL_ORDER, symbolRanking, resolveEngines, legendActionsProviderFor, statePersistenceHandlers, topbarHas, rendererDefaults, widgetAttachments, getDrawingType } from './chunk-L3I2CCYO.js';
3
3
  import { Tooltip, KeymapManager, isEditableTarget, Drawer } from './chunk-2R7BANEM.js';
4
4
  import { Menu, Dialog, applyPlotOverlayTokens, ensureUIHost } from './chunk-OICPLNU7.js';
@@ -599,6 +599,11 @@ var Topbar = class {
599
599
  /** Overrides that LEFT their native slot (default side + a declared `order`) — they
600
600
  * render through the flow cluster like ordinary actions. */
601
601
  this.flowingOverrides = /* @__PURE__ */ new Set();
602
+ /** Tooltips of the CURRENT icon-only action buttons — rebuilt with every
603
+ * renderActions pass (contributed buttons are replaceChildren'd away). */
604
+ this.actionTooltips = [];
605
+ /** `iconOnly` misuse warned once per action id (renderActions re-runs freely). */
606
+ this.warnedIconless = /* @__PURE__ */ new Set();
602
607
  this.onHairlineSync = () => {
603
608
  if (this.hairlineRaf) return;
604
609
  const win = this.el.ownerDocument.defaultView;
@@ -861,6 +866,8 @@ var Topbar = class {
861
866
  this.actionsHost.replaceChildren();
862
867
  this.leftActionsHost.replaceChildren();
863
868
  for (const pin of this.pinned.values()) pin.host.replaceChildren();
869
+ for (const t of this.actionTooltips) t.destroy();
870
+ this.actionTooltips = [];
864
871
  const doc = this.actionsHost.ownerDocument;
865
872
  const flowLeft = this.comp.left.includes("actions");
866
873
  const flowRight = this.comp.right.includes("actions");
@@ -870,10 +877,20 @@ var Topbar = class {
870
877
  if (!pin && builtin.has(action.id) && !this.flowingOverrides.has(action.id)) continue;
871
878
  const left = pin ? pin.left : action.align === "left";
872
879
  if (!pin && !(left ? flowLeft : flowRight)) continue;
880
+ const iconOnly = action.iconOnly === true && !!action.icon;
881
+ if (action.iconOnly === true && !action.icon && !this.warnedIconless.has(action.id)) {
882
+ this.warnedIconless.add(action.id);
883
+ console.warn(`[vela] widget action "${action.id}": iconOnly needs an \`icon\` \u2014 rendering the label instead.`);
884
+ }
873
885
  const b = doc.createElement("button");
874
- b.className = left ? "vela-widget-action-left" : "vela-widget-action";
886
+ b.className = left ? "vela-widget-action-left" : iconOnly ? "vela-widget-tool" : "vela-widget-action";
875
887
  if (action.icon) b.appendChild(iconEl(action.icon, doc));
876
- b.appendChild(doc.createTextNode(action.label));
888
+ if (iconOnly) {
889
+ b.setAttribute("aria-label", action.label);
890
+ this.actionTooltips.push(new Tooltip(b, { content: action.label, triggerId: `vela-action-${action.id}`, host: this.host }));
891
+ } else {
892
+ b.appendChild(doc.createTextNode(action.label));
893
+ }
877
894
  b.addEventListener("click", () => {
878
895
  const c = this.opts.getContext?.();
879
896
  if (c) action.run(c);
@@ -924,7 +941,7 @@ var Topbar = class {
924
941
  this.tfMenu.destroy();
925
942
  this.styleMenu.destroy();
926
943
  this.layoutPicker?.destroy();
927
- for (const t of [...this.tooltips, ...this.panelTooltips]) t.destroy();
944
+ for (const t of [...this.tooltips, ...this.panelTooltips, ...this.actionTooltips]) t.destroy();
928
945
  this.el.remove();
929
946
  }
930
947
  /** One icon-only tool button with its kit tooltip, parked in `sink` for disposal. */
@@ -3667,19 +3684,8 @@ function indicatorLedger(i) {
3667
3684
  };
3668
3685
  }
3669
3686
 
3670
- // src/workspace/sync.ts
3671
- function syncTargets(originId, setting, cellIds) {
3672
- if (setting == null || setting === false) return [];
3673
- if (setting === true) return cellIds.filter((id) => id !== originId);
3674
- const group = setting[originId];
3675
- if (group == null) return [];
3676
- return cellIds.filter((id) => id !== originId && setting[id] === group);
3677
- }
3678
- function rangesWithin(a, b, epsMs) {
3679
- return Math.abs(a.from - b.from) <= epsMs && Math.abs(a.to - b.to) <= epsMs;
3680
- }
3681
-
3682
3687
  // src/state/document.ts
3688
+ var SYNC_KINDS = ["viewport", "symbol", "timeframe", "crosshair", "drawings", "style"];
3683
3689
  function prefixedSymbol(cell) {
3684
3690
  if (!cell?.symbol) return void 0;
3685
3691
  if (cell.symbol.includes(":") || !cell.provider) return cell.symbol;
@@ -3767,7 +3773,7 @@ function sanitizeSync(raw) {
3767
3773
  if (raw == null || typeof raw !== "object") return null;
3768
3774
  const s = raw;
3769
3775
  const out = {};
3770
- for (const kind of ["viewport", "symbol", "timeframe", "crosshair", "drawings"]) {
3776
+ for (const kind of SYNC_KINDS) {
3771
3777
  const v = s[kind];
3772
3778
  if (v === true) out[kind] = true;
3773
3779
  else if (v != null && typeof v === "object") {
@@ -3810,6 +3816,28 @@ function sanitizeTrackSizes(raw) {
3810
3816
  return Object.keys(out).length > 0 ? out : null;
3811
3817
  }
3812
3818
 
3819
+ // src/workspace/sync.ts
3820
+ function syncTargets(originId, setting, cellIds) {
3821
+ if (setting == null || setting === false) return [];
3822
+ if (setting === true) return cellIds.filter((id) => id !== originId);
3823
+ const group = setting[originId];
3824
+ if (group == null) return [];
3825
+ return cellIds.filter((id) => id !== originId && setting[id] === group);
3826
+ }
3827
+ function rangesWithin(a, b, epsMs) {
3828
+ return Math.abs(a.from - b.from) <= epsMs && Math.abs(a.to - b.to) <= epsMs;
3829
+ }
3830
+ var STYLE_SYNC_CONFIG_KEYS = ["layout", "panes", "grid", "priceScale", "crosshair"];
3831
+ function styleConfigSlice(config) {
3832
+ if (config == null || typeof config !== "object") return null;
3833
+ const doc = config;
3834
+ const out = {};
3835
+ for (const key of STYLE_SYNC_CONFIG_KEYS) {
3836
+ if (doc[key] != null && typeof doc[key] === "object") out[key] = doc[key];
3837
+ }
3838
+ return Object.keys(out).length > 0 ? out : null;
3839
+ }
3840
+
3813
3841
  // src/workspace/persist.ts
3814
3842
  var memoryStore = /* @__PURE__ */ new Map();
3815
3843
  function memoryStorageAdapter() {
@@ -4645,61 +4673,144 @@ var MarketStatusTracker = class {
4645
4673
  };
4646
4674
 
4647
4675
  // src/widget/session-shading.ts
4648
- function deriveSessionZones(regular, extended) {
4676
+ var DAY_MS = 864e5;
4677
+ var MINUTE_MS = 6e4;
4678
+ function parseWindow(text) {
4679
+ if (typeof text !== "string") return null;
4680
+ const m = /^(\d{2})(\d{2})-(\d{2})(\d{2})$/.exec(text);
4681
+ if (!m) return null;
4682
+ const start = Number(m[1]) * 60 + Number(m[2]);
4683
+ const end = Number(m[3]) * 60 + Number(m[4]);
4684
+ if (start >= end || end > 1440) return null;
4685
+ return { start, end };
4686
+ }
4687
+ function parseSessionSpec(si) {
4688
+ const session = si?.["session"];
4689
+ if (typeof session !== "string" || session === "" || session === "24x7") return null;
4690
+ const regular = parseWindow(session);
4691
+ if (!regular) return null;
4692
+ const tz = si?.["timezone"];
4693
+ const timezone = typeof tz === "string" && tz !== "" ? tz : "Etc/UTC";
4694
+ const ext = parseWindow(si?.["session_extended"]);
4695
+ const extended = ext && ext.start <= regular.start && ext.end >= regular.end ? ext : { start: 0, end: 1440 };
4696
+ return { regular, extended, timezone };
4697
+ }
4698
+ var dtfCache = /* @__PURE__ */ new Map();
4699
+ function civilFormatter(tz) {
4700
+ const cached = dtfCache.get(tz);
4701
+ if (cached !== void 0) return cached;
4702
+ let dtf = null;
4703
+ try {
4704
+ dtf = new Intl.DateTimeFormat("en-US", {
4705
+ timeZone: tz,
4706
+ hourCycle: "h23",
4707
+ weekday: "short",
4708
+ year: "numeric",
4709
+ month: "2-digit",
4710
+ day: "2-digit",
4711
+ hour: "2-digit",
4712
+ minute: "2-digit"
4713
+ });
4714
+ } catch {
4715
+ dtf = null;
4716
+ }
4717
+ dtfCache.set(tz, dtf);
4718
+ return dtf;
4719
+ }
4720
+ function civilParts(dtf, ms) {
4721
+ const parts = dtf.formatToParts(ms);
4722
+ const get = (t) => parts.find((p) => p.type === t)?.value ?? "";
4723
+ return {
4724
+ weekday: get("weekday"),
4725
+ year: Number(get("year")),
4726
+ month: Number(get("month")),
4727
+ day: Number(get("day")),
4728
+ hour: Number(get("hour")) % 24,
4729
+ minute: Number(get("minute"))
4730
+ };
4731
+ }
4732
+ function expandSessionZones(spec, from, to) {
4649
4733
  const pre = [];
4650
4734
  const post = [];
4651
- for (const [extStart, extEnd] of extended) {
4652
- const inside = regular.filter(([s, e]) => e > extStart && s < extEnd).sort((a, b) => a[0] - b[0]);
4653
- let cursor = extStart;
4654
- for (const [regStart, regEnd] of inside) {
4655
- if (regStart > cursor) pre.push([cursor, Math.min(regStart, extEnd)]);
4656
- cursor = Math.max(cursor, regEnd);
4657
- }
4658
- if (cursor < extEnd) post.push([cursor, extEnd]);
4735
+ const dtf = civilFormatter(spec.timezone);
4736
+ if (!dtf || !Number.isFinite(from) || !Number.isFinite(to)) return { pre, post };
4737
+ const seen = /* @__PURE__ */ new Set();
4738
+ for (let cursor = from - DAY_MS; cursor < to + DAY_MS; cursor += DAY_MS) {
4739
+ const civil = civilParts(dtf, cursor);
4740
+ const key = civil.year * 1e4 + civil.month * 100 + civil.day;
4741
+ if (!Number.isFinite(key) || seen.has(key)) continue;
4742
+ seen.add(key);
4743
+ if (civil.weekday === "Sat" || civil.weekday === "Sun") continue;
4744
+ const naive = Date.UTC(civil.year, civil.month - 1, civil.day);
4745
+ const noonGuess = naive + 720 * MINUTE_MS;
4746
+ const atNoon = civilParts(dtf, noonGuess);
4747
+ const offset = Date.UTC(atNoon.year, atNoon.month - 1, atNoon.day, atNoon.hour, atNoon.minute) - noonGuess;
4748
+ const at = (minutes) => naive + minutes * MINUTE_MS - offset;
4749
+ if (spec.extended.start < spec.regular.start) pre.push([at(spec.extended.start), at(spec.regular.start)]);
4750
+ if (spec.regular.end < spec.extended.end) post.push([at(spec.regular.end), at(spec.extended.end)]);
4659
4751
  }
4660
4752
  return { pre, post };
4661
4753
  }
4662
- var FETCH_AHEAD_MS2 = 10 * 864e5;
4663
- var MIN_LOOKBACK_MS = 3 * 864e5;
4664
- var MAX_LOOKBACK_MS = 120 * 864e5;
4754
+ var COVER_PAD_MIN_MS = 2 * DAY_MS;
4665
4755
  var SessionShadingTracker = class {
4666
4756
  constructor(onZones) {
4667
4757
  this.onZones = onZones;
4668
- /** Invalidates detached async work — bumped by every track()/stop(). */
4758
+ /** Invalidates the one async step (metadata resolution) — bumped by track()/stop(). */
4669
4759
  this.epoch = 0;
4670
- }
4671
- /** (Re)bind to a chart's data surface + market and evaluate once. */
4760
+ this.spec = null;
4761
+ this.ready = false;
4762
+ this.session = "regular";
4763
+ this.covered = null;
4764
+ /** The newest range seen — viewport moves during metadata resolution (a load's fit
4765
+ * animation) must not be lost, so the resolution always expands the LATEST range. */
4766
+ this.lastRange = null;
4767
+ }
4768
+ /** (Re)bind to a chart's data surface + market and expand once metadata lands. */
4672
4769
  track(data, symbol, opts) {
4673
4770
  const my = ++this.epoch;
4674
- void this.evaluate(my, data, symbol, opts);
4771
+ this.ready = false;
4772
+ this.spec = null;
4773
+ this.covered = null;
4774
+ this.session = opts.session;
4775
+ this.lastRange = opts.range;
4776
+ void data.symbolInfo(symbol).catch(() => void 0).then((si) => {
4777
+ if (my !== this.epoch) return;
4778
+ this.ready = true;
4779
+ this.spec = parseSessionSpec(si);
4780
+ this.emit(this.lastRange ?? opts.range, true);
4781
+ });
4782
+ }
4783
+ /** Follow a pan/zoom synchronously: bands are epoch-anchored, so only a range that
4784
+ * leaves the last expansion's coverage needs a recompute — no fetch, no debounce. */
4785
+ updateRange(range) {
4786
+ this.lastRange = range;
4787
+ if (!this.ready) return;
4788
+ this.emit(range, false);
4675
4789
  }
4676
4790
  stop() {
4677
4791
  this.epoch += 1;
4678
- }
4679
- async evaluate(my, data, symbol, opts) {
4680
- const resolved = data.resolve(symbol);
4681
- const provider = resolved ? data.providerInstance(resolved.provider) : void 0;
4682
- const si = await data.symbolInfo(symbol).catch(() => void 0);
4683
- if (my !== this.epoch) return;
4684
- const hasSessions = typeof si?.session === "string" && si.session !== "" && si.session !== "24x7";
4685
- if (!provider?.getCalendar || !hasSessions || !resolved) {
4686
- this.onZones(null);
4792
+ this.ready = false;
4793
+ this.spec = null;
4794
+ this.covered = null;
4795
+ this.lastRange = null;
4796
+ }
4797
+ emit(range, force) {
4798
+ if (!this.spec) {
4799
+ if (force) this.onZones(null);
4687
4800
  return;
4688
4801
  }
4689
- if (opts.session !== "extended") {
4690
- this.onZones({ pre: [], post: [] });
4802
+ if (this.session !== "extended") {
4803
+ if (force) this.onZones({ pre: [], post: [] });
4691
4804
  return;
4692
4805
  }
4693
- const now = Date.now();
4694
- const lookback = Math.max(MIN_LOOKBACK_MS, Math.min(MAX_LOOKBACK_MS, opts.lookbackMs));
4695
- const range = { from: now - lookback, to: now + FETCH_AHEAD_MS2 };
4696
- const [regular, extended] = await Promise.all([
4697
- provider.getCalendar(resolved.ticker, { ...range, session: "regular" }).catch(() => null),
4698
- provider.getCalendar(resolved.ticker, { ...range, session: "extended" }).catch(() => null)
4699
- ]);
4700
- if (my !== this.epoch) return;
4701
- if (!regular || !extended) return;
4702
- this.onZones(deriveSessionZones(regular, extended));
4806
+ const from = Math.min(range.from, range.to);
4807
+ const to = Math.max(range.from, range.to);
4808
+ if (!Number.isFinite(from) || !Number.isFinite(to)) return;
4809
+ if (!force && this.covered && from >= this.covered.from && to <= this.covered.to) return;
4810
+ const pad = Math.max(to - from, COVER_PAD_MIN_MS);
4811
+ const covered = { from: from - pad, to: to + pad };
4812
+ this.covered = covered;
4813
+ this.onZones(expandSessionZones(this.spec, covered.from, covered.to));
4703
4814
  }
4704
4815
  };
4705
4816
 
@@ -4924,7 +5035,11 @@ var ChartCell = class {
4924
5035
  this.deps.toast(`No registered provider serves "${symbol2}" (registered: ${list})`, "error", 6e3);
4925
5036
  });
4926
5037
  this.inner.on("load:start", () => this.watermark?.setLoading(true));
4927
- this.inner.on("load:end", () => this.watermark?.setLoading(false));
5038
+ this.inner.on("load:end", () => {
5039
+ this.watermark?.setLoading(false);
5040
+ this.refreshSessionShading();
5041
+ });
5042
+ this.inner.on("viewport:changed", (range) => this.sessionShading.updateRange(range));
4928
5043
  const tz = deps.timezone();
4929
5044
  if (tz !== "Etc/UTC") this.inner.renderer.set("timezone", tz);
4930
5045
  this.indicatorTitlesOn = seed.indicatorTitles ?? true;
@@ -5051,14 +5166,18 @@ var ChartCell = class {
5051
5166
  this.refreshSessionShading();
5052
5167
  });
5053
5168
  }
5054
- /** (Re)derive the pre/post-market shading bands for this cell's market. The loaded
5055
- * depth (bars × timeframe) bounds the calendar fetch; the tracker clamps it. */
5169
+ /** (Re)derive the pre/post-market shading bands for this cell's market. The bands
5170
+ * expand locally from the symbol's session vocabulary, so they paint as soon as
5171
+ * metadata is known and follow any pan depth without provider round trips. */
5056
5172
  refreshSessionShading() {
5057
5173
  const chart = this.inner;
5058
5174
  const symbol = this.state.symbol;
5059
5175
  if (!chart || !symbol) return;
5060
- const lookbackMs = Math.max(this.state.bars ?? 1e3, this.rangeBars) * timeframeMs(this.state.timeframe ?? "60");
5061
- this.sessionShading.track(chart.data, symbol, { session: this.session, lookbackMs });
5176
+ const now = Date.now();
5177
+ const requestedSpan = Math.max(this.state.bars ?? 1e3, this.rangeBars) * timeframeMs(this.state.timeframe ?? "60");
5178
+ const fallbackSpan = Number.isFinite(requestedSpan) ? Math.max(3 * 864e5, requestedSpan) : 3 * 864e5;
5179
+ const range = chart.getVisibleRange() ?? { from: now - fallbackSpan, to: now };
5180
+ this.sessionShading.track(chart.data, symbol, { session: this.session, range });
5062
5181
  }
5063
5182
  /** The session-shade colors live in the renderer CONFIG (persisted with it, edited
5064
5183
  * live by the dialog swatch) — the cell only proxies them into its settings rows. */
@@ -5154,10 +5273,10 @@ var ChartCell = class {
5154
5273
  id: "status-line",
5155
5274
  rows: [
5156
5275
  { kind: "heading", label: "Status line", id: "parts" },
5157
- { kind: "toggle", label: "Symbol name", id: "name", get: () => sl.partVisible("name"), set: (v) => sl.setPartVisible("name", v) },
5158
- { kind: "toggle", label: "Market status", id: "market", get: () => sl.partVisible("market"), set: (v) => sl.setPartVisible("market", v) },
5159
- { kind: "toggle", label: "OHLC values", id: "ohlc", get: () => sl.partVisible("ohlc"), set: (v) => sl.setPartVisible("ohlc", v) },
5160
- { kind: "toggle", label: "Bar change values", id: "change", get: () => sl.partVisible("change"), set: (v) => sl.setPartVisible("change", v) },
5276
+ { kind: "toggle", label: "Symbol name", id: "name", get: () => sl.partVisible("name"), set: (v) => this.setStatuslinePart("name", v) },
5277
+ { kind: "toggle", label: "Market status", id: "market", get: () => sl.partVisible("market"), set: (v) => this.setStatuslinePart("market", v) },
5278
+ { kind: "toggle", label: "OHLC values", id: "ohlc", get: () => sl.partVisible("ohlc"), set: (v) => this.setStatuslinePart("ohlc", v) },
5279
+ { kind: "toggle", label: "Bar change values", id: "change", get: () => sl.partVisible("change"), set: (v) => this.setStatuslinePart("change", v) },
5161
5280
  { kind: "heading", label: "Indicators", id: "indicators" },
5162
5281
  {
5163
5282
  kind: "toggle",
@@ -5192,12 +5311,40 @@ var ChartCell = class {
5192
5311
  this.indicatorTitlesOn = visible;
5193
5312
  this.inner?.renderer.set("indicatorTitles", visible);
5194
5313
  this.deps.onStateDirty();
5314
+ this.deps.onStatusPrefsChanged(this.id);
5195
5315
  }
5196
5316
  /** Show/hide the plot values beside this cell's legend titles (persisted per cell). */
5197
5317
  setIndicatorValuesVisible(visible) {
5198
5318
  this.indicatorValuesOn = visible;
5199
5319
  this.inner?.renderer.set("indicatorValues", visible);
5200
5320
  this.deps.onStateDirty();
5321
+ this.deps.onStatusPrefsChanged(this.id);
5322
+ }
5323
+ /** Show/hide one status-line segment (the settings dialog's Status line tab). */
5324
+ setStatuslinePart(part, visible) {
5325
+ this.statusline?.setPartVisible(part, visible);
5326
+ this.deps.onStatusPrefsChanged(this.id);
5327
+ }
5328
+ /** This cell's Status line tab prefs as one bundle (see {@link CellStatusPrefs}). */
5329
+ statusPrefs() {
5330
+ const sl = this.statusline;
5331
+ return {
5332
+ parts: sl ? { name: sl.partVisible("name"), market: sl.partVisible("market"), ohlc: sl.partVisible("ohlc"), change: sl.partVisible("change") } : null,
5333
+ indicatorTitles: this.indicatorTitlesOn,
5334
+ indicatorValues: this.indicatorValuesOn
5335
+ };
5336
+ }
5337
+ /** Converge this cell's Status line tab prefs to `prefs` — the follower half of
5338
+ * the workspace's style link. Idempotent: matching values change nothing, so a
5339
+ * propagated echo dies on its own. */
5340
+ applyStatusPrefs(prefs) {
5341
+ if (prefs.parts && this.statusline) {
5342
+ for (const part of Object.keys(prefs.parts)) {
5343
+ if (this.statusline.partVisible(part) !== prefs.parts[part]) this.statusline.setPartVisible(part, prefs.parts[part]);
5344
+ }
5345
+ }
5346
+ if (prefs.indicatorTitles !== this.indicatorTitlesOn) this.setIndicatorTitlesVisible(prefs.indicatorTitles);
5347
+ if (prefs.indicatorValues !== this.indicatorValuesOn) this.setIndicatorValuesVisible(prefs.indicatorValues);
5201
5348
  }
5202
5349
  /** The LIVE chart of this cell — never cache it across a layout change (the cell's
5203
5350
  * identity is what endures; the chart dies with the cell). */
@@ -7139,16 +7286,21 @@ var DrawingPill = class {
7139
7286
 
7140
7287
  // src/workspace/context.ts
7141
7288
  function buildContext(host) {
7142
- const active = host.active();
7143
7289
  return {
7144
7290
  get chart() {
7145
7291
  const cell = host.active();
7146
7292
  if (!cell) throw new Error("VelaWorkspace has no active cell yet");
7147
7293
  return cell.chart;
7148
7294
  },
7149
- symbol: active?.symbol ?? "",
7150
- timeframe: active?.timeframe ?? "60",
7151
- priceStyle: active?.priceStyle ?? "candles",
7295
+ get symbol() {
7296
+ return host.active()?.symbol ?? "";
7297
+ },
7298
+ get timeframe() {
7299
+ return host.active()?.timeframe ?? "60";
7300
+ },
7301
+ get priceStyle() {
7302
+ return host.active()?.priceStyle ?? "candles";
7303
+ },
7152
7304
  setSymbol: (symbol) => host.active()?.setSymbol(symbol),
7153
7305
  setTimeframe: (tf) => host.active()?.setTimeframe(tf),
7154
7306
  setPriceStyle: (style) => host.active()?.setPriceStyle(style),
@@ -7159,8 +7311,12 @@ function buildContext(host) {
7159
7311
  addIndicator: (entry) => host.active()?.addExternalIndicator(entry),
7160
7312
  addNativeIndicator: (type) => host.active()?.addNative(type),
7161
7313
  stateChanged: () => host.stateDirty(),
7162
- cells: host.cells().map((c) => ({ id: c.id, chart: c.chart, symbol: c.symbol, timeframe: c.timeframe })),
7163
- activeCellId: active?.id ?? "",
7314
+ get cells() {
7315
+ return host.cells().map((c) => ({ id: c.id, chart: c.chart, symbol: c.symbol, timeframe: c.timeframe }));
7316
+ },
7317
+ get activeCellId() {
7318
+ return host.active()?.id ?? "";
7319
+ },
7164
7320
  setActiveCell: (id) => host.setActiveCell(id)
7165
7321
  };
7166
7322
  }
@@ -7254,6 +7410,10 @@ var VelaWorkspace = class {
7254
7410
  /** Same guard for the drawings link: the propagated mutations' own `drawing:*`
7255
7411
  * events fire synchronously inside the propagation loop and must not fan out again. */
7256
7412
  this.drawingSyncBusy = false;
7413
+ /** Same guard for the style link: a follower's `applyConfig` re-fires its
7414
+ * `onConfigChanged` in the same tick, and a state restore applies per-cell
7415
+ * configs that legitimately differ — neither must propagate. */
7416
+ this.styleSyncBusy = false;
7257
7417
  /** LINKED drawings (the drawings sync): one map per synced set (cellId → that
7258
7418
  * cell's drawing id), reachable from every member under its `cellId\0drawingId`
7259
7419
  * key — any member finds its peers to push edits/removals onto. Survives a
@@ -7314,9 +7474,7 @@ var VelaWorkspace = class {
7314
7474
  if (boot?.favorites) this.favs = [...boot.favorites];
7315
7475
  if (boot?.timeframeFavorites) this.tfFavs = [...boot.timeframeFavorites];
7316
7476
  const sync = boot?.sync ?? opts.sync;
7317
- for (const kind of ["viewport", "symbol", "timeframe", "crosshair", "drawings"]) {
7318
- this.applySyncSetting(kind, sync?.[kind]);
7319
- }
7477
+ for (const kind of SYNC_KINDS) this.applySyncSetting(kind, sync?.[kind]);
7320
7478
  this.monoLayout = opts.layout === false;
7321
7479
  const optLayout = opts.layout === false || opts.layout === void 0 ? "4" : opts.layout;
7322
7480
  this.def = this.resolveLayout(this.monoLayout ? "1" : boot?.layout && ensureLayout(boot.layout) ? boot.layout : optLayout);
@@ -7400,7 +7558,8 @@ var VelaWorkspace = class {
7400
7558
  syncs: () => [
7401
7559
  { id: "symbol", label: "Symbol", checked: this.syncOpts.symbol === true },
7402
7560
  { id: "timeframe", label: "Interval", checked: this.syncOpts.timeframe === true },
7403
- { id: "crosshair", label: "Crosshair", checked: this.syncOpts.crosshair === true }
7561
+ { id: "crosshair", label: "Crosshair", checked: this.syncOpts.crosshair === true },
7562
+ { id: "style", label: "Style", checked: this.syncOpts.style === true }
7404
7563
  ],
7405
7564
  onToggleSync: (id) => {
7406
7565
  const kind = id;
@@ -7684,7 +7843,7 @@ var VelaWorkspace = class {
7684
7843
  this.topbar.setTimeframeFavorites(this.tfFavs);
7685
7844
  }
7686
7845
  this.dock.applyState(st.panels);
7687
- for (const kind of ["viewport", "symbol", "timeframe", "crosshair", "drawings"]) this.applySyncSetting(kind, st.sync?.[kind]);
7846
+ for (const kind of SYNC_KINDS) this.applySyncSetting(kind, st.sync?.[kind]);
7688
7847
  this.trackSizes.clear();
7689
7848
  if (st.trackSizes) for (const [id, ts] of Object.entries(st.trackSizes)) this.trackSizes.set(id, ts);
7690
7849
  const targetDef = this.monoLayout ? this.def : ensureLayout(st.layout) ?? this.def;
@@ -7695,9 +7854,14 @@ var VelaWorkspace = class {
7695
7854
  for (const cell of this.cellsById.values()) cell.chart.drawings.setFavorites(this.favs);
7696
7855
  }
7697
7856
  this.drawingLinks.clear();
7698
- for (const [i] of this.def.cells.entries()) {
7699
- const { id, ...cs } = st.charts[i];
7700
- this.cellsById.get(id)?.rehydrate(cs);
7857
+ this.styleSyncBusy = true;
7858
+ try {
7859
+ for (const [i] of this.def.cells.entries()) {
7860
+ const { id, ...cs } = st.charts[i];
7861
+ this.cellsById.get(id)?.rehydrate(cs);
7862
+ }
7863
+ } finally {
7864
+ this.styleSyncBusy = false;
7701
7865
  }
7702
7866
  if (st.timezone) this.setTimezone(st.timezone);
7703
7867
  this.pool.clear();
@@ -7797,6 +7961,7 @@ var VelaWorkspace = class {
7797
7961
  const rebuildAll = nextBackend !== this.cellBackend;
7798
7962
  this.order = orderAfterLayout(this.order, next.cells.length, this.activeId);
7799
7963
  const keep = new Set(this.order.slice(0, next.cells.length));
7964
+ const preexisting = new Set(this.cellsById.keys());
7800
7965
  for (const [id, cell] of [...this.cellsById]) {
7801
7966
  if (!keep.has(id) || rebuildAll) {
7802
7967
  this.poolSet(id, cell.dehydrate());
@@ -7809,6 +7974,7 @@ var VelaWorkspace = class {
7809
7974
  this.cellBackend = nextBackend;
7810
7975
  this.applyGrid();
7811
7976
  this.buildCells();
7977
+ this.alignNewCellStyles(preexisting);
7812
7978
  this.syncCellPresentation();
7813
7979
  this.topbar.setLayout(next.id);
7814
7980
  const nextActive = activeAfterLayout(this.activeId, this.order.slice(0, next.cells.length));
@@ -8012,6 +8178,7 @@ var VelaWorkspace = class {
8012
8178
  onMarketChanged: (id2) => this.onCellMarketChanged(id2),
8013
8179
  onPriceStyleChanged: (id2) => this.onCellPriceStyleChanged(id2),
8014
8180
  onIndicatorsChanged: (id2) => this.onCellIndicatorsChanged(id2),
8181
+ onStatusPrefsChanged: (id2) => this.propagateStylePrefs(id2),
8015
8182
  onStateDirty: () => this.markStateDirty(),
8016
8183
  manifestSettled: () => this.manifestSettled,
8017
8184
  toast: (message, kind, durationMs) => this.toastHost.show(message, kind, durationMs)
@@ -8087,6 +8254,7 @@ var VelaWorkspace = class {
8087
8254
  this.drawToolbar?.setEraserActive(mode === "eraser");
8088
8255
  });
8089
8256
  chart.on("viewport:changed", (range) => this.propagateViewport(cell.id, range));
8257
+ chart.renderer.onConfigChanged(() => this.propagateStylePrefs(cell.id));
8090
8258
  chart.on("theme:changed", (t) => this.setTheme(t));
8091
8259
  chart.renderer.onAxisLongPress((e) => {
8092
8260
  if (this.layoutCtl.current !== "mobile") return;
@@ -8119,11 +8287,67 @@ var VelaWorkspace = class {
8119
8287
  if (kind === "viewport") {
8120
8288
  const range = this.cellsById.get(this.activeId)?.chart.getVisibleRange();
8121
8289
  if (range) this.propagateViewport(this.activeId, range);
8290
+ } else if (kind === "style") {
8291
+ this.propagateStylePrefs(this.activeId);
8122
8292
  } else {
8123
8293
  this.propagateMarket(this.activeId);
8124
8294
  }
8125
8295
  }
8126
8296
  }
8297
+ /**
8298
+ * Align cells minted by a layout change to their style group: with the link on, a
8299
+ * NEW cell (fresh slot or one returning from the pool, which missed edits while
8300
+ * dormant) inherits the presentation of a pre-existing group peer — the active
8301
+ * cell when it is one — instead of sitting on its own state beside a styled
8302
+ * group. Propagation runs FROM the peer, so a newborn's defaults never overwrite
8303
+ * the group, and the equality short-circuits keep converged peers untouched.
8304
+ */
8305
+ alignNewCellStyles(preexisting) {
8306
+ const setting = this.syncOpts.style;
8307
+ if (!setting) return;
8308
+ const ids = [...this.cellsById.keys()];
8309
+ const propagated = /* @__PURE__ */ new Set();
8310
+ for (const id of ids) {
8311
+ if (preexisting.has(id)) continue;
8312
+ const peers = syncTargets(id, setting, ids).filter((p) => preexisting.has(p));
8313
+ if (peers.length === 0) continue;
8314
+ const source = this.activeId && peers.includes(this.activeId) ? this.activeId : peers[0];
8315
+ if (propagated.has(source)) continue;
8316
+ propagated.add(source);
8317
+ this.propagateStylePrefs(source);
8318
+ }
8319
+ }
8320
+ /**
8321
+ * Mirror an origin cell's presentation — the Canvas + Scales-and-lines slice of
8322
+ * its renderer config plus its Status line tab prefs — onto its same-group
8323
+ * followers (the style link). Loop-safe two ways: the busy guard eats the
8324
+ * followers' SYNCHRONOUS echoes (their `applyConfig` re-fires `onConfigChanged`
8325
+ * in the same tick), and the equality short-circuits leave already-converged
8326
+ * followers untouched, so nothing re-emits once the group agrees.
8327
+ */
8328
+ propagateStylePrefs(originId) {
8329
+ if (this.styleSyncBusy || this.destroyed) return;
8330
+ const targets = syncTargets(originId, this.syncOpts.style, [...this.cellsById.keys()]);
8331
+ if (targets.length === 0) return;
8332
+ const origin = this.cellsById.get(originId);
8333
+ if (!origin) return;
8334
+ const slice = styleConfigSlice(origin.chart.renderer.getConfig());
8335
+ const sliceJson = slice ? JSON.stringify(slice) : null;
8336
+ const prefs = origin.statusPrefs();
8337
+ this.styleSyncBusy = true;
8338
+ try {
8339
+ for (const id of targets) {
8340
+ const cell = this.cellsById.get(id);
8341
+ if (!cell) continue;
8342
+ if (slice && sliceJson !== JSON.stringify(styleConfigSlice(cell.chart.renderer.getConfig()))) {
8343
+ cell.chart.renderer.applyConfig(slice);
8344
+ }
8345
+ cell.applyStatusPrefs(prefs);
8346
+ }
8347
+ } finally {
8348
+ this.styleSyncBusy = false;
8349
+ }
8350
+ }
8127
8351
  /**
8128
8352
  * Mirror an origin cell's pointer time onto its same-group followers as GHOST
8129
8353
  * crosshairs (`renderer.setExternalCrosshair`). The horizontal price level rides
@@ -8431,7 +8655,8 @@ var VelaWorkspace = class {
8431
8655
  syncs: () => [
8432
8656
  { id: "symbol", label: "Symbol", checked: this.syncOpts.symbol === true },
8433
8657
  { id: "timeframe", label: "Interval", checked: this.syncOpts.timeframe === true },
8434
- { id: "crosshair", label: "Crosshair", checked: this.syncOpts.crosshair === true }
8658
+ { id: "crosshair", label: "Crosshair", checked: this.syncOpts.crosshair === true },
8659
+ { id: "style", label: "Style", checked: this.syncOpts.style === true }
8435
8660
  ],
8436
8661
  onToggleSync: (id) => {
8437
8662
  const kind = id;