@luxalgo/vela 0.7.6 → 0.7.7

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
@@ -3521,7 +3521,8 @@ function addRecent(hex6) {
3521
3521
  recents.unshift(hex6);
3522
3522
  if (recents.length > 10) recents.length = 10;
3523
3523
  }
3524
- function buildColorPicker(color, theme, onChange) {
3524
+ function buildColorPicker(color, theme, onChange, options = {}) {
3525
+ const commit = options.commit ?? "live";
3525
3526
  const parsed = splitColor(color);
3526
3527
  let curHex = parsed.hex6;
3527
3528
  let curAlpha = parsed.alpha;
@@ -3604,7 +3605,12 @@ function buildColorPicker(color, theme, onChange) {
3604
3605
  const r = track.getBoundingClientRect();
3605
3606
  curAlpha = Math.max(0, Math.min(1, (clientX - r.left) / r.width));
3606
3607
  paintOpacity();
3607
- emit();
3608
+ if (commit === "live") emit();
3609
+ };
3610
+ const endDrag = () => {
3611
+ if (!dragging) return;
3612
+ dragging = false;
3613
+ if (commit === "release") emit();
3608
3614
  };
3609
3615
  track.addEventListener("pointerdown", (e) => {
3610
3616
  e.stopPropagation();
@@ -3615,7 +3621,8 @@ function buildColorPicker(color, theme, onChange) {
3615
3621
  track.addEventListener("pointermove", (e) => {
3616
3622
  if (dragging) onDrag(e.clientX);
3617
3623
  });
3618
- track.addEventListener("pointerup", () => dragging = false);
3624
+ track.addEventListener("pointerup", endDrag);
3625
+ track.addEventListener("pointercancel", endDrag);
3619
3626
  function emit() {
3620
3627
  onChange(combineColor(curHex, curAlpha));
3621
3628
  }
@@ -3638,7 +3645,7 @@ function buildColorPicker(color, theme, onChange) {
3638
3645
  return root;
3639
3646
  }
3640
3647
  function colorField(theme, getVal, onVal, opts) {
3641
- return new ColorField({ theme, getVal, onVal, shape: opts?.shape, id: opts?.id, popover: opts?.popover }).el;
3648
+ return new ColorField({ theme, getVal, onVal, shape: opts?.shape, id: opts?.id, popover: opts?.popover, commit: opts?.commit }).el;
3642
3649
  }
3643
3650
  var ColorField = class {
3644
3651
  constructor(opts) {
@@ -3647,6 +3654,7 @@ var ColorField = class {
3647
3654
  this.getVal = opts.getVal;
3648
3655
  this.onVal = opts.onVal;
3649
3656
  this.popoverOpts = opts.popover;
3657
+ this.commit = opts.commit;
3650
3658
  const trigger = document.createElement("button");
3651
3659
  trigger.type = "button";
3652
3660
  if (opts.id) trigger.id = opts.id;
@@ -3686,10 +3694,15 @@ var ColorField = class {
3686
3694
  boundary: this.popoverOpts?.boundary,
3687
3695
  zIndex: this.popoverOpts?.zIndex,
3688
3696
  className: "vela-color-field-pop",
3689
- content: buildColorPicker(this.getVal(), this.theme, (val) => {
3690
- this.onVal(val);
3691
- this.paint();
3692
- })
3697
+ content: buildColorPicker(
3698
+ this.getVal(),
3699
+ this.theme,
3700
+ (val) => {
3701
+ this.onVal(val);
3702
+ this.paint();
3703
+ },
3704
+ { commit: this.commit }
3705
+ )
3693
3706
  });
3694
3707
  pop.show();
3695
3708
  }
@@ -4277,7 +4290,8 @@ function buildFieldControl(desc) {
4277
4290
  const el2 = colorField(desc.theme, desc.get, desc.onChange, {
4278
4291
  shape: "circle",
4279
4292
  id: desc.id,
4280
- popover: desc.popover
4293
+ popover: desc.popover,
4294
+ commit: desc.commit
4281
4295
  });
4282
4296
  if (desc.title) el2.title = desc.title;
4283
4297
  return { el: el2 };
@@ -5070,10 +5084,12 @@ var BASELINE_LEVEL_DEFAULT = 50;
5070
5084
  var PREMARKET_SHADE = withAlpha(WARNING, 0.08);
5071
5085
  var POSTMARKET_SHADE = withAlpha(ACCENT, 0.08);
5072
5086
  var EXTENDED_SHADE = POSTMARKET_SHADE;
5087
+ var DEFAULT_MARGINS = { top: 10, bottom: 10, right: 10 };
5073
5088
  function defaultChartStyle() {
5074
5089
  return {
5075
5090
  chartTypes: {},
5076
5091
  fontSize: 11,
5092
+ margins: { ...DEFAULT_MARGINS },
5077
5093
  gridVert: { visible: true, color: null },
5078
5094
  gridHorz: { visible: true, color: null },
5079
5095
  borderColor: null,
@@ -5197,6 +5213,12 @@ function clampLevel(v) {
5197
5213
  function clampSpacing(v) {
5198
5214
  return v < 0.1 ? 0.1 : v > 10 ? 10 : v;
5199
5215
  }
5216
+ function clampMarginPct(v) {
5217
+ return v < 0 ? 0 : v > 40 ? 40 : v;
5218
+ }
5219
+ function clampMarginBars(v) {
5220
+ return Math.round(v < 0 ? 0 : v > 200 ? 200 : v);
5221
+ }
5200
5222
  function factoryResetConfig(factory, priceStyle = factory.series.style) {
5201
5223
  const bag = {};
5202
5224
  for (const t of chartTypes()) {
@@ -5245,6 +5267,7 @@ function mergeConfig(base, patch) {
5245
5267
  const ps = asObject(p.priceScale);
5246
5268
  const anim = asObject(p.animations);
5247
5269
  const panes = asObject(p.panes);
5270
+ const margins = asObject(p.margins);
5248
5271
  const trades = asObject(p.trades);
5249
5272
  const ts = asObject(p.timeScale);
5250
5273
  const marks = asObject(p.marks);
@@ -5304,6 +5327,11 @@ function mergeConfig(base, patch) {
5304
5327
  panes: {
5305
5328
  separatorColor: isColor(panes.separatorColor) ? panes.separatorColor : base.panes.separatorColor
5306
5329
  },
5330
+ margins: {
5331
+ top: isNum(margins.top) ? clampMarginPct(margins.top) : base.margins.top,
5332
+ bottom: isNum(margins.bottom) ? clampMarginPct(margins.bottom) : base.margins.bottom,
5333
+ right: isNum(margins.right) ? clampMarginBars(margins.right) : base.margins.right
5334
+ },
5307
5335
  trades: {
5308
5336
  visible: isBool(trades.visible) ? trades.visible : base.trades.visible,
5309
5337
  labels: isBool(trades.labels) ? trades.labels : base.trades.labels,
@@ -6125,9 +6153,22 @@ var TIMEZONES = [
6125
6153
  { value: "Pacific/Apia", label: "Apia" },
6126
6154
  { value: "Pacific/Kiritimati", label: "Kiritimati" }
6127
6155
  ];
6156
+ var EXCHANGE_TIMEZONE = "exchange";
6157
+ function isExchangeTimezone(zone) {
6158
+ return zone === EXCHANGE_TIMEZONE;
6159
+ }
6160
+ function resolveTimezone(zone, exchangeZone) {
6161
+ if (!isExchangeTimezone(zone)) return zone;
6162
+ return exchangeZone && exchangeZone !== "" ? exchangeZone : "Etc/UTC";
6163
+ }
6128
6164
  function normalizeTimezone(zone) {
6129
6165
  return zone === "UTC" || zone === "Etc/UTC" || zone === "Etc/GMT" ? "Etc/UTC" : zone;
6130
6166
  }
6167
+ function timezoneMenuRows(current) {
6168
+ const active = normalizeTimezone(current);
6169
+ const [utc, ...zones] = TIMEZONES.map((t) => ({ value: t.value, label: tzMenuLabel(t.value, t.label), checked: t.value === active }));
6170
+ return [utc, { value: EXCHANGE_TIMEZONE, label: "Exchange", checked: isExchangeTimezone(current) }, ...zones];
6171
+ }
6131
6172
  function tzOffset(zone, date = /* @__PURE__ */ new Date()) {
6132
6173
  try {
6133
6174
  const parts = new Intl.DateTimeFormat("en-US", { timeZone: zone, timeZoneName: "shortOffset" }).formatToParts(date);
@@ -6250,6 +6291,7 @@ var Bottombar = class {
6250
6291
  this.sessionButtons = /* @__PURE__ */ new Map();
6251
6292
  this.sessionEl = null;
6252
6293
  this.timezone = opts.timezone;
6294
+ this.exchangeTimezone = opts.exchangeTimezone;
6253
6295
  const doc = host.ownerDocument;
6254
6296
  injectStyles(STYLE_ID3, CSS4, doc);
6255
6297
  this.el = doc.createElement("div");
@@ -6273,7 +6315,7 @@ var Bottombar = class {
6273
6315
  this.clockEl = doc.createElement("span");
6274
6316
  this.clockEl.className = "vela-bb-clock";
6275
6317
  this.tzLabelEl = doc.createElement("span");
6276
- this.tzLabelEl.textContent = tzButtonLabel(this.timezone);
6318
+ this.tzLabelEl.textContent = tzButtonLabel(this.displayZone);
6277
6319
  this.tzButton.append(this.clockEl, this.tzLabelEl);
6278
6320
  const session = doc.createElement("span");
6279
6321
  session.className = "vela-bb-session";
@@ -6308,19 +6350,32 @@ var Bottombar = class {
6308
6350
  placement: "top-end",
6309
6351
  items: this.tzItems(),
6310
6352
  onSelect: (zone) => {
6311
- this.setTimezone(zone);
6353
+ this.setTimezone(zone, this.exchangeTimezone);
6312
6354
  opts.onTimezone(zone);
6313
6355
  }
6314
6356
  });
6315
6357
  this.tick();
6316
6358
  this.unsubClock = (opts.clock ?? new SecondClock()).onTick(() => this.tick());
6317
6359
  }
6318
- setTimezone(zone) {
6360
+ /**
6361
+ * Reflect the stored choice AND the active chart's market zone. The clock and the
6362
+ * offset label read the RESOLVED zone, so a workspace on the exchange rule re-labels
6363
+ * when the active cell (or its symbol) changes market — the host re-projects on
6364
+ * both. Idempotent: unchanged inputs leave the menu alone.
6365
+ */
6366
+ setTimezone(zone, exchangeTimezone) {
6367
+ if (zone === this.timezone && exchangeTimezone === this.exchangeTimezone) return;
6368
+ const choiceChanged = zone !== this.timezone;
6319
6369
  this.timezone = zone;
6320
- this.tzLabelEl.textContent = tzButtonLabel(zone);
6321
- this.tzMenu.setItems(this.tzItems());
6370
+ this.exchangeTimezone = exchangeTimezone;
6371
+ this.tzLabelEl.textContent = tzButtonLabel(this.displayZone);
6372
+ if (choiceChanged) this.tzMenu.setItems(this.tzItems());
6322
6373
  this.tick();
6323
6374
  }
6375
+ /** The zone the bar's clock and label render in. */
6376
+ get displayZone() {
6377
+ return resolveTimezone(this.timezone, this.exchangeTimezone);
6378
+ }
6324
6379
  /** Highlight (or clear with null) the active range chip — cleared on manual tf changes. */
6325
6380
  setActiveRange(id) {
6326
6381
  for (const [key, b] of this.rangeButtons) {
@@ -6348,11 +6403,7 @@ var Bottombar = class {
6348
6403
  this.el.remove();
6349
6404
  }
6350
6405
  tzItems() {
6351
- return TIMEZONES.map((t) => ({
6352
- id: t.value,
6353
- label: tzMenuLabel(t.value, t.label),
6354
- checked: t.value === normalizeTimezone(this.timezone)
6355
- }));
6406
+ return timezoneMenuRows(this.timezone).map((r) => ({ id: r.value, label: r.label, checked: r.checked }));
6356
6407
  }
6357
6408
  tick() {
6358
6409
  try {
@@ -6361,7 +6412,7 @@ var Bottombar = class {
6361
6412
  minute: "2-digit",
6362
6413
  second: "2-digit",
6363
6414
  hour12: false,
6364
- timeZone: this.timezone
6415
+ timeZone: this.displayZone
6365
6416
  }).format(/* @__PURE__ */ new Date());
6366
6417
  } catch {
6367
6418
  this.clockEl.textContent = "";
@@ -16011,15 +16062,14 @@ var TimezoneDrawer = class {
16011
16062
  this.drawer.body.replaceChildren();
16012
16063
  const list = doc.createElement("div");
16013
16064
  list.className = "vela-tzd-list";
16014
- const current = normalizeTimezone(this.opts.timezone());
16015
- for (const tz of TIMEZONES) {
16065
+ for (const tz of timezoneMenuRows(this.opts.timezone())) {
16016
16066
  const row = doc.createElement("div");
16017
16067
  row.className = "vela-tzd-row";
16018
16068
  const label = doc.createElement("span");
16019
16069
  label.className = "vela-tzd-row-label";
16020
- label.textContent = tzMenuLabel(tz.value, tz.label);
16070
+ label.textContent = tz.label;
16021
16071
  row.appendChild(label);
16022
- if (tz.value === current) row.appendChild(iconEl("check", doc));
16072
+ if (tz.checked) row.appendChild(iconEl("check", doc));
16023
16073
  row.addEventListener("click", () => {
16024
16074
  this.opts.onTimezone(tz.value);
16025
16075
  this.drawer.hide();
@@ -16106,12 +16156,11 @@ function priceAxisItems(s) {
16106
16156
  ];
16107
16157
  }
16108
16158
  function timeAxisItems(timezone) {
16109
- const active = timezone === "UTC" ? "Etc/UTC" : timezone;
16110
16159
  return [
16111
16160
  {
16112
16161
  id: "timezone",
16113
16162
  label: "Time zone",
16114
- submenu: TIMEZONES.map((t) => ({ id: `tz:${t.value}`, label: tzMenuLabel(t.value, t.label), checked: t.value === active }))
16163
+ submenu: timezoneMenuRows(timezone).map((r) => ({ id: `tz:${r.value}`, label: r.label, checked: r.checked }))
16115
16164
  },
16116
16165
  settingsItem("time-axis", "More settings\u2026")
16117
16166
  ];
@@ -21770,7 +21819,10 @@ var IndicatorInputsDialog = class {
21770
21819
  id,
21771
21820
  theme: this.host.theme(),
21772
21821
  get: () => String(bagOf(row, inp)[inp.key] ?? inp.defval),
21773
- onChange: (v) => emit(v)
21822
+ onChange: (v) => emit(v),
21823
+ // An input change re-executes the script over its whole history: commit the
21824
+ // opacity drag once on release, not once per pointer move.
21825
+ commit: "release"
21774
21826
  }).el;
21775
21827
  }
21776
21828
  if (inp.type === "symbol") return this.buildSymbol(id, String(current), emit);
@@ -22017,10 +22069,6 @@ var IndicatorInputsDialog = class {
22017
22069
  }
22018
22070
  /** Open a themed month calendar under `anchor` — same surface + shadow as the choice list. */
22019
22071
  openCalendar(anchor, current, onPick) {
22020
- const parsed = parseIsoDate(current);
22021
- let year = parsed?.getFullYear() ?? (/* @__PURE__ */ new Date()).getFullYear();
22022
- let month = parsed?.getMonth() ?? (/* @__PURE__ */ new Date()).getMonth();
22023
- const selected = parsed ? isoDate(parsed) : current;
22024
22072
  const pop = new Popover({
22025
22073
  trigger: anchor,
22026
22074
  theme: this.host.theme(),
@@ -22032,80 +22080,10 @@ var IndicatorInputsDialog = class {
22032
22080
  this.calendarPop = null;
22033
22081
  this.calendarAnchor = null;
22034
22082
  },
22035
- content: (el) => {
22036
- const title = document.createElement("div");
22037
- title.className = "vela-ind-cal-title";
22038
- const prev2 = document.createElement("button");
22039
- prev2.type = "button";
22040
- prev2.className = "vela-ind-cal-nav";
22041
- prev2.setAttribute("aria-label", "Previous month");
22042
- prev2.innerHTML = iconAt("chevron-left", 14);
22043
- const next = document.createElement("button");
22044
- next.type = "button";
22045
- next.className = "vela-ind-cal-nav";
22046
- next.setAttribute("aria-label", "Next month");
22047
- next.innerHTML = iconAt("chevron-right", 14);
22048
- const head = document.createElement("div");
22049
- head.className = "vela-ind-cal-head";
22050
- head.append(prev2, title, next);
22051
- const week = document.createElement("div");
22052
- week.className = "vela-ind-cal-week";
22053
- for (const d of WEEKDAY_LABELS) {
22054
- const cell = document.createElement("span");
22055
- cell.textContent = d;
22056
- week.appendChild(cell);
22057
- }
22058
- const grid = document.createElement("div");
22059
- grid.className = "vela-ind-cal-grid";
22060
- const paint = () => {
22061
- title.textContent = `${MONTH_LABELS[month]} ${year}`;
22062
- grid.replaceChildren();
22063
- const first = new Date(year, month, 1);
22064
- const startPad = first.getDay();
22065
- const days = new Date(year, month + 1, 0).getDate();
22066
- const today = isoDate(/* @__PURE__ */ new Date());
22067
- for (let i = 0; i < startPad; i++) {
22068
- const blank = document.createElement("span");
22069
- blank.className = "vela-ind-cal-blank";
22070
- grid.appendChild(blank);
22071
- }
22072
- for (let day = 1; day <= days; day++) {
22073
- const iso = `${year}-${String(month + 1).padStart(2, "0")}-${String(day).padStart(2, "0")}`;
22074
- const b = document.createElement("button");
22075
- b.type = "button";
22076
- b.className = "vela-ind-cal-day";
22077
- b.textContent = String(day);
22078
- if (iso === selected) b.dataset.checked = "1";
22079
- if (iso === today) b.dataset.today = "1";
22080
- b.addEventListener("click", (e) => {
22081
- e.stopPropagation();
22082
- pop.hide();
22083
- onPick(iso);
22084
- });
22085
- grid.appendChild(b);
22086
- }
22087
- };
22088
- prev2.addEventListener("click", (e) => {
22089
- e.stopPropagation();
22090
- month -= 1;
22091
- if (month < 0) {
22092
- month = 11;
22093
- year -= 1;
22094
- }
22095
- paint();
22096
- });
22097
- next.addEventListener("click", (e) => {
22098
- e.stopPropagation();
22099
- month += 1;
22100
- if (month > 11) {
22101
- month = 0;
22102
- year += 1;
22103
- }
22104
- paint();
22105
- });
22106
- paint();
22107
- el.append(head, week, grid);
22108
- }
22083
+ content: (el) => fillCalendar(el, current, (iso) => {
22084
+ pop.hide();
22085
+ onPick(iso);
22086
+ })
22109
22087
  });
22110
22088
  this.calendarPop = pop;
22111
22089
  this.calendarAnchor = anchor;
@@ -22214,7 +22192,7 @@ function groupInputs(inputs) {
22214
22192
  var CALENDAR_SVG = iconAt("calendar", 14);
22215
22193
  var CLOCK_SVG = iconAt("clock", 14);
22216
22194
  var DIALOG_STYLE_ID2 = "vela-ind-dialog-styles";
22217
- var DIALOG_STYLE_REV = "29";
22195
+ var DIALOG_STYLE_REV = "30";
22218
22196
  var LEGEND_ICON_PX = 16;
22219
22197
  function ensureDialogStyles() {
22220
22198
  if (typeof document === "undefined") return;
@@ -22230,8 +22208,12 @@ function ensureDialogStyles() {
22230
22208
  .vela-ind-combo-chevron{position:absolute;right:0;top:0;bottom:0;width:26px;border:none;background:transparent;color:inherit;opacity:0.55;cursor:pointer;display:flex;align-items:center;justify-content:center;padding:0;}
22231
22209
  .vela-ind-combo-chevron:hover{opacity:0.9;}
22232
22210
  .vela-ind-cal{background:var(--vela-bg);color:var(--vela-fg);border:none;border-radius:6px;box-shadow:var(--vela-shadow);font:14px var(--vela-font);padding:10px 12px;user-select:none;}
22211
+ .vela-ind-cal [hidden]{display:none !important;}
22233
22212
  .vela-ind-cal-head{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:8px;}
22234
- .vela-ind-cal-title{flex:1;text-align:center;font-weight:600;font-size:14px;color:var(--vela-fg-bright);}
22213
+ .vela-ind-cal-title{flex:1;display:flex;align-items:center;justify-content:center;gap:2px;min-width:0;}
22214
+ .vela-ind-cal-switch{border:none;background:transparent;color:var(--vela-fg-bright);font:inherit;font-weight:600;font-size:14px;padding:2px 6px;border-radius:4px;cursor:pointer;}
22215
+ .vela-ind-cal-switch:not(:disabled):hover{background:var(--vela-hover);}
22216
+ .vela-ind-cal-switch:disabled{cursor:default;}
22235
22217
  .vela-ind-cal-nav{width:24px;height:24px;border:none;background:transparent;color:var(--vela-fg-muted);border-radius:4px;padding:0;cursor:pointer;display:inline-flex;align-items:center;justify-content:center;}
22236
22218
  .vela-ind-cal-nav:hover{background:var(--vela-hover);color:var(--vela-fg-bright);}
22237
22219
  .vela-ind-cal-week,.vela-ind-cal-grid{display:grid;grid-template-columns:repeat(7,28px);gap:2px;}
@@ -22242,6 +22224,12 @@ function ensureDialogStyles() {
22242
22224
  .vela-ind-cal-day:hover{background:var(--vela-hover);}
22243
22225
  .vela-ind-cal-day[data-checked]{background:var(--vela-hover-strong);color:var(--vela-fg-bright);}
22244
22226
  .vela-ind-cal-day[data-today]:not([data-checked]){box-shadow:inset 0 0 0 1px var(--vela-border-strong);}
22227
+ .vela-ind-cal-cells{display:grid;grid-template-columns:repeat(3,1fr);gap:4px;width:calc(7 * 28px + 6 * 2px);}
22228
+ .vela-ind-cal-cell{height:36px;border:none;background:transparent;color:inherit;border-radius:4px;padding:0 4px;cursor:pointer;font:inherit;font-size:14px;}
22229
+ .vela-ind-cal-cell:hover{background:var(--vela-hover);}
22230
+ .vela-ind-cal-cell[data-checked]{background:var(--vela-hover-strong);color:var(--vela-fg-bright);}
22231
+ .vela-ind-cal-cell[data-today]:not([data-checked]){box-shadow:inset 0 0 0 1px var(--vela-border-strong);}
22232
+ .vela-ind-cal-cell[data-outside]{opacity:0.45;}
22245
22233
  ${overlayScrollbarCss(".vela-dialog.vela-ind-dialog *", 9)}
22246
22234
  .vela-ind-tab{font-weight:600;font-size:13px;line-height:20px;transition:color var(--vela-dur-fast) ease,border-color var(--vela-dur-fast) ease;}
22247
22235
  .vela-ind-tab:not(.vela-ind-tab-active):hover{color:var(--vela-fg-bright);}
@@ -22276,6 +22264,7 @@ var TIME_OPTIONS = Array.from({ length: 48 }, (_, i) => {
22276
22264
  return { value: v, label: v };
22277
22265
  });
22278
22266
  var MONTH_LABELS = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"];
22267
+ var MONTH_SHORT = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
22279
22268
  var WEEKDAY_LABELS = ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"];
22280
22269
  function normalizeDateInput(raw) {
22281
22270
  const t = raw.trim();
@@ -22297,6 +22286,151 @@ function parseIsoDate(raw) {
22297
22286
  function isoDate(d) {
22298
22287
  return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
22299
22288
  }
22289
+ function fillCalendar(el, current, onPick) {
22290
+ const today = /* @__PURE__ */ new Date();
22291
+ const selected = parseIsoDate(current);
22292
+ let year = selected?.getFullYear() ?? today.getFullYear();
22293
+ let month = selected?.getMonth() ?? today.getMonth();
22294
+ let mode = "date";
22295
+ const prev2 = document.createElement("button");
22296
+ prev2.type = "button";
22297
+ prev2.className = "vela-ind-cal-nav";
22298
+ prev2.innerHTML = iconAt("chevron-left", 14);
22299
+ const next = document.createElement("button");
22300
+ next.type = "button";
22301
+ next.className = "vela-ind-cal-nav";
22302
+ next.innerHTML = iconAt("chevron-right", 14);
22303
+ const monthBtn = document.createElement("button");
22304
+ monthBtn.type = "button";
22305
+ monthBtn.className = "vela-ind-cal-switch";
22306
+ monthBtn.setAttribute("aria-label", "Choose month");
22307
+ const yearBtn = document.createElement("button");
22308
+ yearBtn.type = "button";
22309
+ yearBtn.className = "vela-ind-cal-switch";
22310
+ yearBtn.setAttribute("aria-label", "Choose year");
22311
+ const title = document.createElement("div");
22312
+ title.className = "vela-ind-cal-title";
22313
+ title.append(monthBtn, yearBtn);
22314
+ const head = document.createElement("div");
22315
+ head.className = "vela-ind-cal-head";
22316
+ head.append(prev2, title, next);
22317
+ const week = document.createElement("div");
22318
+ week.className = "vela-ind-cal-week";
22319
+ for (const d of WEEKDAY_LABELS) {
22320
+ const cell = document.createElement("span");
22321
+ cell.textContent = d;
22322
+ week.appendChild(cell);
22323
+ }
22324
+ const grid = document.createElement("div");
22325
+ const paint = () => {
22326
+ const decade = Math.floor(year / 10) * 10;
22327
+ monthBtn.hidden = mode !== "date";
22328
+ monthBtn.textContent = MONTH_LABELS[month] ?? "";
22329
+ yearBtn.textContent = mode === "year" ? `${decade}-${decade + 9}` : String(year);
22330
+ yearBtn.disabled = mode === "year";
22331
+ week.hidden = mode !== "date";
22332
+ prev2.setAttribute("aria-label", mode === "date" ? "Previous month" : mode === "month" ? "Previous year" : "Previous decade");
22333
+ next.setAttribute("aria-label", mode === "date" ? "Next month" : mode === "month" ? "Next year" : "Next decade");
22334
+ grid.className = mode === "date" ? "vela-ind-cal-grid" : "vela-ind-cal-cells";
22335
+ grid.replaceChildren();
22336
+ if (mode === "date") {
22337
+ const startPad = new Date(year, month, 1).getDay();
22338
+ const days = new Date(year, month + 1, 0).getDate();
22339
+ const todayIso = isoDate(today);
22340
+ const selectedIso = selected ? isoDate(selected) : "";
22341
+ for (let i = 0; i < startPad; i++) {
22342
+ const blank = document.createElement("span");
22343
+ blank.className = "vela-ind-cal-blank";
22344
+ grid.appendChild(blank);
22345
+ }
22346
+ for (let day = 1; day <= days; day++) {
22347
+ const iso = `${year}-${String(month + 1).padStart(2, "0")}-${String(day).padStart(2, "0")}`;
22348
+ const b = document.createElement("button");
22349
+ b.type = "button";
22350
+ b.className = "vela-ind-cal-day";
22351
+ b.textContent = String(day);
22352
+ if (iso === selectedIso) b.dataset.checked = "1";
22353
+ if (iso === todayIso) b.dataset.today = "1";
22354
+ b.addEventListener("click", (e) => {
22355
+ e.stopPropagation();
22356
+ onPick(iso);
22357
+ });
22358
+ grid.appendChild(b);
22359
+ }
22360
+ return;
22361
+ }
22362
+ if (mode === "month") {
22363
+ for (let m = 0; m < 12; m++) {
22364
+ const b = document.createElement("button");
22365
+ b.type = "button";
22366
+ b.className = "vela-ind-cal-cell";
22367
+ b.textContent = MONTH_SHORT[m] ?? "";
22368
+ if (selected?.getFullYear() === year && selected.getMonth() === m) b.dataset.checked = "1";
22369
+ if (today.getFullYear() === year && today.getMonth() === m) b.dataset.today = "1";
22370
+ b.addEventListener("click", (e) => {
22371
+ e.stopPropagation();
22372
+ month = m;
22373
+ mode = "date";
22374
+ paint();
22375
+ });
22376
+ grid.appendChild(b);
22377
+ }
22378
+ return;
22379
+ }
22380
+ for (let y = decade - 1; y <= decade + 10; y++) {
22381
+ const b = document.createElement("button");
22382
+ b.type = "button";
22383
+ b.className = "vela-ind-cal-cell";
22384
+ b.textContent = String(y);
22385
+ if (y < decade || y > decade + 9) b.dataset.outside = "1";
22386
+ if (selected?.getFullYear() === y) b.dataset.checked = "1";
22387
+ if (today.getFullYear() === y) b.dataset.today = "1";
22388
+ b.addEventListener("click", (e) => {
22389
+ e.stopPropagation();
22390
+ year = y;
22391
+ mode = "month";
22392
+ paint();
22393
+ });
22394
+ grid.appendChild(b);
22395
+ }
22396
+ };
22397
+ const step = (dir) => {
22398
+ if (mode === "date") {
22399
+ month += dir;
22400
+ if (month < 0) {
22401
+ month = 11;
22402
+ year -= 1;
22403
+ }
22404
+ if (month > 11) {
22405
+ month = 0;
22406
+ year += 1;
22407
+ }
22408
+ } else {
22409
+ year += mode === "month" ? dir : dir * 10;
22410
+ }
22411
+ paint();
22412
+ };
22413
+ prev2.addEventListener("click", (e) => {
22414
+ e.stopPropagation();
22415
+ step(-1);
22416
+ });
22417
+ next.addEventListener("click", (e) => {
22418
+ e.stopPropagation();
22419
+ step(1);
22420
+ });
22421
+ monthBtn.addEventListener("click", (e) => {
22422
+ e.stopPropagation();
22423
+ mode = "month";
22424
+ paint();
22425
+ });
22426
+ yearBtn.addEventListener("click", (e) => {
22427
+ e.stopPropagation();
22428
+ mode = "year";
22429
+ paint();
22430
+ });
22431
+ paint();
22432
+ el.append(head, week, grid);
22433
+ }
22300
22434
  function normalizeTimeInput(raw) {
22301
22435
  const t = raw.trim();
22302
22436
  const m = /^(\d{1,2}):(\d{2})$/.exec(t) ?? /^(\d{2})(\d{2})$/.exec(t);
@@ -28784,6 +28918,10 @@ var BUILTIN_SETTINGS_IDS = [
28784
28918
  "canvas.grid",
28785
28919
  "canvas.grid.vertical",
28786
28920
  "canvas.grid.horizontal",
28921
+ "canvas.margins",
28922
+ "canvas.margins.top",
28923
+ "canvas.margins.bottom",
28924
+ "canvas.margins.right",
28787
28925
  "canvas.theme"
28788
28926
  ];
28789
28927
  function settingsIdCatalog(hostSections, markGroups = []) {
@@ -29314,6 +29452,10 @@ var SettingsDialog = class {
29314
29452
  body.append(sid(this.toggleRow("Horizontal", config.grid.horzLines.visible, (v) => this.emit({ grid: { horzLines: { visible: v } } }), [
29315
29453
  this.swatch(config.grid.horzLines.color, (v) => this.emit({ grid: { horzLines: { color: v } } }))
29316
29454
  ]), "canvas.grid.horizontal"));
29455
+ body.append(sid(this.sectionTitle("Margins"), "canvas.margins"));
29456
+ body.append(sid(this.numberRow("Top", config.margins.top, 0, 40, 1, (v) => this.emit({ margins: { top: v } }), "%"), "canvas.margins.top"));
29457
+ body.append(sid(this.numberRow("Bottom", config.margins.bottom, 0, 40, 1, (v) => this.emit({ margins: { bottom: v } }), "%"), "canvas.margins.bottom"));
29458
+ body.append(sid(this.numberRow("Right", config.margins.right, 0, 200, 1, (v) => this.emit({ margins: { right: v } }), "bars"), "canvas.margins.right"));
29317
29459
  if (this.themeControl) {
29318
29460
  const tc = this.themeControl;
29319
29461
  body.append(sid(this.sectionTitle("Theme"), "canvas.theme"));
@@ -29885,23 +30027,16 @@ var SettingsDialog = class {
29885
30027
  this.hintTips.push(dispose);
29886
30028
  return el;
29887
30029
  }
29888
- numberRow(label, value, min, max, step, onChange) {
29889
- return fieldRow({
29890
- label,
29891
- labelSize: "sm",
29892
- className: "vela-sd-row",
29893
- control: buildFieldControl({
29894
- kind: "number",
29895
- value,
29896
- min,
29897
- max,
29898
- step,
29899
- fill: false,
29900
- commit: "live",
29901
- steppers: true,
29902
- onChange
29903
- }).el
29904
- });
30030
+ /** `unit` (optional) trails the input as muted text — "%", "bars". */
30031
+ numberRow(label, value, min, max, step, onChange, unit) {
30032
+ const controls = [buildFieldControl({ kind: "number", value, min, max, step, fill: false, commit: "live", steppers: true, onChange }).el];
30033
+ if (unit) {
30034
+ const u = document.createElement("span");
30035
+ u.textContent = unit;
30036
+ u.style.cssText = "color:var(--vela-fg-muted);";
30037
+ controls.push(u);
30038
+ }
30039
+ return this.rowWith(label, controls);
29905
30040
  }
29906
30041
  /** A dropdown whose option values differ from their display labels. */
29907
30042
  selectRowLabeled(label, value, options, onChange) {
@@ -34775,9 +34910,7 @@ function createProjector(coords, paneOf, paneIdAtY, barsInRange, seriesInRange)
34775
34910
  }
34776
34911
 
34777
34912
  // src/renderers/native/core/autoscale.ts
34778
- var MARGIN_TOP = 2 / 7;
34779
- var MARGIN_BOTTOM = 1 / 7;
34780
- function computePaneScale(models, bars, includeCandles, i0, i1, drawings, log = false, offsetOf = () => 0) {
34913
+ function computePaneScale(models, bars, includeCandles, i0, i1, drawings, log = false, offsetOf = () => 0, margins = DEFAULT_MARGINS) {
34781
34914
  let min = Infinity;
34782
34915
  let max = -Infinity;
34783
34916
  const consider = (v) => {
@@ -34812,14 +34945,17 @@ function computePaneScale(models, bars, includeCandles, i0, i1, drawings, log =
34812
34945
  const pad = Math.abs(min) * 0.1 || 1;
34813
34946
  return { min: min - pad, max: max + pad, log: log && min - pad > 0 };
34814
34947
  }
34948
+ const content = Math.max(0.1, 1 - (margins.top + margins.bottom) / 100);
34949
+ const above = margins.top / 100 / content;
34950
+ const below = margins.bottom / 100 / content;
34815
34951
  if (log && min > 0) {
34816
34952
  const lmin = Math.log(min);
34817
34953
  const lmax = Math.log(max);
34818
34954
  const lspan = lmax - lmin;
34819
- return { min: Math.exp(lmin - lspan * MARGIN_BOTTOM), max: Math.exp(lmax + lspan * MARGIN_TOP), log: true };
34955
+ return { min: Math.exp(lmin - lspan * below), max: Math.exp(lmax + lspan * above), log: true };
34820
34956
  }
34821
34957
  const span = max - min;
34822
- return { min: min - span * MARGIN_BOTTOM, max: max + span * MARGIN_TOP };
34958
+ return { min: min - span * below, max: max + span * above };
34823
34959
  }
34824
34960
  function considerSeries(s, i0, i1, off, consider) {
34825
34961
  if (!seriesInScale(s)) return;
@@ -36510,6 +36646,7 @@ var NativeRenderer = class {
36510
36646
  intro: this.intro.style !== false
36511
36647
  },
36512
36648
  panes: { separatorColor: s.separatorColor ?? t.borderColor },
36649
+ margins: { ...s.margins },
36513
36650
  trades: {
36514
36651
  visible: this.scene.tradeMarkers.visible,
36515
36652
  labels: this.scene.tradeMarkers.labels,
@@ -36622,6 +36759,10 @@ var NativeRenderer = class {
36622
36759
  this.animAutoscale.toggle(next.animations.autoscale);
36623
36760
  this.intro = { style: next.animations.intro ? this.introOnStyle : false, duration: this.intro.duration || INTRO_DURATION_DEFAULT_MS };
36624
36761
  s.separatorColor = keepInherit(s.separatorColor, next.panes.separatorColor, prevTheme.borderColor);
36762
+ if (next.margins.right !== s.margins.right && this.coords.barCount > 0) {
36763
+ this.applyViewport({ barSpacing: this.coords.getViewport().barSpacing, rightOffset: next.margins.right });
36764
+ }
36765
+ s.margins = { ...next.margins };
36625
36766
  this.scene.tradeMarkers = {
36626
36767
  visible: next.trades.visible,
36627
36768
  labels: next.trades.labels,
@@ -36865,7 +37006,7 @@ var NativeRenderer = class {
36865
37006
  /** Glide the view back to the most recent bars, keeping the current zoom (barSpacing). */
36866
37007
  scrollToRealtime() {
36867
37008
  if (this.coords.barCount === 0) return;
36868
- this.glideRightOffset(ZOOM_OUT_MARGIN_BARS);
37009
+ this.glideRightOffset(this.scene.style.margins.right);
36869
37010
  }
36870
37011
  /** Ease rightOffset to `target` at constant zoom (see animTick's scroll glide);
36871
37012
  * instant when the scroll glide is off. Shared by scroll-to-latest and panBy. */
@@ -38807,6 +38948,7 @@ var NativeRenderer = class {
38807
38948
  const pricePane = panes.find((p) => p.kind === "price") ?? null;
38808
38949
  this.chrome.prepare(this.scene, this.coords, this.theme);
38809
38950
  const animating = this.animator.active;
38951
+ const margins = this.scene.style.margins;
38810
38952
  for (const pane of panes) {
38811
38953
  if (pane.manualScale) {
38812
38954
  pane.scaleTarget = pane.manualScale;
@@ -38822,7 +38964,7 @@ var NativeRenderer = class {
38822
38964
  if (or) dr = dr ? { min: Math.min(dr.min, or.min), max: Math.max(dr.max, or.max) } : or;
38823
38965
  }
38824
38966
  const includeCandles = pane.kind === "price" && (!this.scene.candlesHidden || this.priceLayersAnchoredToBars(masterModels) || !this.paneHasMeasurableContent(masterModels, dr));
38825
- pane.scaleTarget = computePaneScale(masterModels, this.bars, includeCandles, i0, i1, dr, paneLogScale(this.scene, pane), (id) => this.scene.offsetOf(id));
38967
+ pane.scaleTarget = computePaneScale(masterModels, this.bars, includeCandles, i0, i1, dr, paneLogScale(this.scene, pane), (id) => this.scene.offsetOf(id), margins);
38826
38968
  pane.percentBaseline = pane.kind === "price" ? this.bars[i0]?.close ?? 0 : this.firstVisibleValue(masterModels, i0);
38827
38969
  pane.axisFormat = void 0;
38828
38970
  pane.axisBands = void 0;
@@ -38833,7 +38975,7 @@ var NativeRenderer = class {
38833
38975
  pane.axisFormat = "volume";
38834
38976
  }
38835
38977
  } else if (this.layerNativesOwnPane(pane, masterModels)) {
38836
- pane.scaleTarget = computePaneScale([], this.bars, true, i0, i1, dr, paneLogScale(this.scene, pane), (id) => this.scene.offsetOf(id));
38978
+ pane.scaleTarget = computePaneScale([], this.bars, true, i0, i1, dr, paneLogScale(this.scene, pane), (id) => this.scene.offsetOf(id), margins);
38837
38979
  pane.percentBaseline = this.bars[i0]?.close ?? 0;
38838
38980
  if (masterModels.every((m) => m.paneAxis != null)) {
38839
38981
  pane.axisFormat = "none";
@@ -38862,7 +39004,7 @@ var NativeRenderer = class {
38862
39004
  continue;
38863
39005
  }
38864
39006
  const mdr = this.chrome.paneDrawingsRange([model], this.scene, false, vr);
38865
- sl.scaleTarget = computePaneScale([model], this.bars, false, i0, i1, mdr, false, (id) => this.scene.offsetOf(id));
39007
+ sl.scaleTarget = computePaneScale([model], this.bars, false, i0, i1, mdr, false, (id) => this.scene.offsetOf(id), margins);
38866
39008
  if (!animating || !sl.initialized) {
38867
39009
  sl.scale = { ...sl.scaleTarget };
38868
39010
  sl.initialized = true;
@@ -38903,13 +39045,13 @@ var NativeRenderer = class {
38903
39045
  for (const pane of this.scene.panes.values()) pane.manualScale = null;
38904
39046
  for (const sl of this.scene.indicatorScales.values()) sl.manualScale = null;
38905
39047
  const visibleBars = Math.min(n, 200);
38906
- const rightOffset = 6;
39048
+ const rightOffset = this.scene.style.margins.right;
38907
39049
  const v = this.clampViewport(w / ((visibleBars + rightOffset) * this.coords.spacingScale), rightOffset);
38908
39050
  this.coords.setViewport(v);
38909
39051
  this.targetBarSpacing = v.barSpacing;
38910
39052
  }
38911
39053
  /** Re-frame after a series replacement (a symbol/timeframe switch): keep the user's
38912
- * zoom (bar spacing), re-anchor the newest bars at the default right offset.
39054
+ * zoom (bar spacing), re-anchor the newest bars at the configured right margin.
38913
39055
  * `clampViewport`'s fit-all-bars floor deliberately does NOT apply — a progressive
38914
39056
  * head may still be backfilling toward the previous depth, and raising the spacing
38915
39057
  * to its temporary bar count would lose the zoom this exists to keep. */
@@ -38918,7 +39060,7 @@ var NativeRenderer = class {
38918
39060
  this.panVelocity = 0;
38919
39061
  for (const pane of this.scene.panes.values()) pane.manualScale = null;
38920
39062
  for (const sl of this.scene.indicatorScales.values()) sl.manualScale = null;
38921
- const v = { barSpacing: clampBarSpacing(this.coords.getViewport().barSpacing), rightOffset: defaultViewport().rightOffset };
39063
+ const v = { barSpacing: clampBarSpacing(this.coords.getViewport().barSpacing), rightOffset: this.scene.style.margins.right };
38922
39064
  this.coords.setViewport(v);
38923
39065
  this.targetBarSpacing = v.barSpacing;
38924
39066
  }
@@ -43530,7 +43672,7 @@ var ChartContextMenu = class {
43530
43672
  } else if (id.startsWith("tz:")) {
43531
43673
  const zone = id.slice("tz:".length);
43532
43674
  if (this.cbs.setTimezone) this.cbs.setTimezone(zone);
43533
- else chart.renderer.set("timezone", zone);
43675
+ else chart.renderer.set("timezone", resolveTimezone(zone, void 0));
43534
43676
  } else if (id === "auto") {
43535
43677
  chart.renderer.set("autoScale", chart.renderer.get("autoScale") === false);
43536
43678
  } else if (id === "invert") {
@@ -43763,7 +43905,7 @@ var ChartCell = class {
43763
43905
  this.history.onChart(this.inner);
43764
43906
  this.inner.renderer.onConfigChanged(() => {
43765
43907
  const zone = this.inner?.renderer.get("timezone");
43766
- if (typeof zone === "string" && normalizeTimezone(zone) !== normalizeTimezone(this.deps.timezone())) {
43908
+ if (typeof zone === "string" && normalizeTimezone(zone) !== normalizeTimezone(this.displayTimezone)) {
43767
43909
  this.deps.setTimezone(normalizeTimezone(zone));
43768
43910
  }
43769
43911
  const style = this.priceStyle;
@@ -43805,8 +43947,7 @@ var ChartCell = class {
43805
43947
  this.refreshSessionShading();
43806
43948
  });
43807
43949
  this.inner.on("viewport:changed", (range) => this.sessionShading.updateRange(range));
43808
- const tz = deps.timezone();
43809
- if (tz !== "Etc/UTC") this.inner.renderer.set("timezone", tz);
43950
+ this.applyTimezone();
43810
43951
  this.indicatorTitlesOn = seed.indicatorTitles ?? true;
43811
43952
  if (!this.indicatorTitlesOn) this.inner.renderer.set("indicatorTitles", false);
43812
43953
  this.indicatorValuesOn = seed.indicatorValues ?? true;
@@ -43828,7 +43969,7 @@ var ChartCell = class {
43828
43969
  this.statusline?.setSymbol(this.state.symbol);
43829
43970
  this.statusline?.setMeta(this.state.timeframe ?? "60", this.inner.data.displayPrefix(this.state.symbol) ?? this.state.provider ?? "");
43830
43971
  }
43831
- this.refreshSessionAvailable();
43972
+ this.refreshSymbolMetadata();
43832
43973
  if (this.inner && this.state.symbol) this.marketStatus?.track(this.inner.data, this.state.symbol);
43833
43974
  });
43834
43975
  this.syncStatuslineColors();
@@ -43902,7 +44043,7 @@ var ChartCell = class {
43902
44043
  this.offMarket = this.inner.on("market:changed", ({ symbol: symbol2, timeframe }) => {
43903
44044
  this.projectMarket(symbol2, timeframe);
43904
44045
  this.refreshNativeCatalog();
43905
- this.refreshSessionAvailable();
44046
+ this.refreshSymbolMetadata();
43906
44047
  if (this.inner) this.marketStatus?.track(this.inner.data, symbol2);
43907
44048
  this.deps.onMarketChanged(this.id);
43908
44049
  });
@@ -43943,7 +44084,35 @@ var ChartCell = class {
43943
44084
  this.deps.onStateDirty();
43944
44085
  void this.inner?.setMarket({ session });
43945
44086
  }
43946
- refreshSessionAvailable() {
44087
+ /**
44088
+ * The symbol's own trading zone, once its metadata has landed — what the exchange
44089
+ * rule resolves to on this cell (the workspace labels the shared bottom bar with the
44090
+ * ACTIVE cell's). Undefined = unknown or undeclared (the rule then renders UTC).
44091
+ */
44092
+ get exchangeTimezone() {
44093
+ return this.exchangeZone;
44094
+ }
44095
+ /** The IANA zone this cell's axis renders in: the workspace choice, resolved. */
44096
+ get displayTimezone() {
44097
+ return resolveTimezone(this.deps.timezone(), this.exchangeZone);
44098
+ }
44099
+ /**
44100
+ * Push the resolved zone to the renderer — on the workspace choice changing, and on
44101
+ * this cell's market zone changing under the exchange rule. Skips the write when the
44102
+ * renderer already holds it (its config default is the bare `'UTC'` alias).
44103
+ */
44104
+ applyTimezone() {
44105
+ const chart = this.inner;
44106
+ if (!chart) return;
44107
+ const zone = this.displayTimezone;
44108
+ const current = chart.renderer.get("timezone");
44109
+ const held = typeof current === "string" && current ? current : "UTC";
44110
+ if (normalizeTimezone(held) === normalizeTimezone(zone)) return;
44111
+ chart.renderer.set("timezone", zone);
44112
+ }
44113
+ /** Re-read the symbol's metadata: session posture (RTH/ETH toggle, shading) and its
44114
+ * trading zone (the exchange rule). Async — the workspace re-projects when a verdict lands. */
44115
+ refreshSymbolMetadata() {
43947
44116
  const chart = this.inner;
43948
44117
  const symbol = this.state.symbol;
43949
44118
  if (!chart || !symbol) return;
@@ -43951,7 +44120,13 @@ var ChartCell = class {
43951
44120
  if (this.inner !== chart) return;
43952
44121
  const available = typeof si?.session === "string" && si.session !== "" && si.session !== "24x7";
43953
44122
  const overnight = parseSessionSpec(si)?.overnight === true;
43954
- if (available !== this.sessionAvailableFlag || overnight !== this.sessionOvernightFlag) {
44123
+ const zone = typeof si?.timezone === "string" && si.timezone !== "" ? si.timezone : void 0;
44124
+ const zoneChanged = zone !== this.exchangeZone;
44125
+ if (zoneChanged) {
44126
+ this.exchangeZone = zone;
44127
+ this.applyTimezone();
44128
+ }
44129
+ if (available !== this.sessionAvailableFlag || overnight !== this.sessionOvernightFlag || zoneChanged) {
43955
44130
  this.sessionAvailableFlag = available;
43956
44131
  this.sessionOvernightFlag = overnight;
43957
44132
  this.deps.onMarketChanged(this.id);
@@ -45721,13 +45896,23 @@ var VelaWorkspace = class {
45721
45896
  }
45722
45897
  }
45723
45898
  }
45724
- /** Set the workspace-global display timezone — applied to EVERY cell. */
45899
+ /**
45900
+ * Set the workspace-global display timezone — applied to EVERY cell. An IANA zone,
45901
+ * or `'exchange'` (the exchange rule): each cell then renders in its OWN market's
45902
+ * zone (Chicago for a CME future, New York for a US equity, UTC for crypto), and the
45903
+ * bottom bar follows the active cell's.
45904
+ */
45725
45905
  setTimezone(zone) {
45726
45906
  this.timezone = zone;
45727
- this.bottombar?.setTimezone(zone);
45728
- for (const cell of this.cellsById.values()) cell.chart.renderer.set("timezone", zone);
45907
+ this.projectTimezone();
45908
+ for (const cell of this.cellsById.values()) cell.applyTimezone();
45729
45909
  this.markStateDirty();
45730
45910
  }
45911
+ /** Bottom bar ⇐ the stored choice + the ACTIVE cell's market zone (labels the exchange row). */
45912
+ projectTimezone() {
45913
+ const active = this.activeId ? this.cellsById.get(this.activeId) : void 0;
45914
+ this.bottombar?.setTimezone(this.timezone, active?.exchangeTimezone);
45915
+ }
45731
45916
  /**
45732
45917
  * Swap the workspace theme at runtime — `'dark'`, `'light'`, or a full custom theme,
45733
45918
  * applied to the shared chrome (topbar, panels, drawing toolbar) and EVERY cell.
@@ -45932,6 +46117,7 @@ var VelaWorkspace = class {
45932
46117
  this.dock.onChart(cell.chart);
45933
46118
  this.bottombar?.setActiveRange(cell.activeRangeId);
45934
46119
  this.bottombar?.setSession({ session: cell.session, enabled: cell.sessionAvailable });
46120
+ this.projectTimezone();
45935
46121
  this.indicatorPicker?.sync();
45936
46122
  this.glider.stop();
45937
46123
  const d = cell.chart.drawings;
@@ -46516,6 +46702,7 @@ var VelaWorkspace = class {
46516
46702
  this.mobileBar?.setTimeframe(cell.timeframe);
46517
46703
  this.objectTree.setSymbol(cell.symbol);
46518
46704
  this.bottombar?.setSession({ session: cell.session, enabled: cell.sessionAvailable });
46705
+ this.projectTimezone();
46519
46706
  this.bottombar?.setActiveRange(cell.activeRangeId);
46520
46707
  }
46521
46708
  /** Trigger ② — a cell's price style changed: the topbar button/menu only if active.