@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.
@@ -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 };
@@ -5016,10 +5030,12 @@ var BASELINE_LEVEL_DEFAULT = 50;
5016
5030
  var PREMARKET_SHADE = withAlpha(WARNING, 0.08);
5017
5031
  var POSTMARKET_SHADE = withAlpha(ACCENT, 0.08);
5018
5032
  var EXTENDED_SHADE = POSTMARKET_SHADE;
5033
+ var DEFAULT_MARGINS = { top: 10, bottom: 10, right: 10 };
5019
5034
  function defaultChartStyle() {
5020
5035
  return {
5021
5036
  chartTypes: {},
5022
5037
  fontSize: 11,
5038
+ margins: { ...DEFAULT_MARGINS },
5023
5039
  gridVert: { visible: true, color: null },
5024
5040
  gridHorz: { visible: true, color: null },
5025
5041
  borderColor: null,
@@ -5143,6 +5159,12 @@ function clampLevel(v) {
5143
5159
  function clampSpacing(v) {
5144
5160
  return v < 0.1 ? 0.1 : v > 10 ? 10 : v;
5145
5161
  }
5162
+ function clampMarginPct(v) {
5163
+ return v < 0 ? 0 : v > 40 ? 40 : v;
5164
+ }
5165
+ function clampMarginBars(v) {
5166
+ return Math.round(v < 0 ? 0 : v > 200 ? 200 : v);
5167
+ }
5146
5168
  function factoryResetConfig(factory, priceStyle = factory.series.style) {
5147
5169
  const bag = {};
5148
5170
  for (const t of chartTypes()) {
@@ -5191,6 +5213,7 @@ function mergeConfig(base, patch) {
5191
5213
  const ps = asObject(p.priceScale);
5192
5214
  const anim = asObject(p.animations);
5193
5215
  const panes = asObject(p.panes);
5216
+ const margins = asObject(p.margins);
5194
5217
  const trades = asObject(p.trades);
5195
5218
  const ts = asObject(p.timeScale);
5196
5219
  const marks = asObject(p.marks);
@@ -5250,6 +5273,11 @@ function mergeConfig(base, patch) {
5250
5273
  panes: {
5251
5274
  separatorColor: isColor(panes.separatorColor) ? panes.separatorColor : base.panes.separatorColor
5252
5275
  },
5276
+ margins: {
5277
+ top: isNum(margins.top) ? clampMarginPct(margins.top) : base.margins.top,
5278
+ bottom: isNum(margins.bottom) ? clampMarginPct(margins.bottom) : base.margins.bottom,
5279
+ right: isNum(margins.right) ? clampMarginBars(margins.right) : base.margins.right
5280
+ },
5253
5281
  trades: {
5254
5282
  visible: isBool(trades.visible) ? trades.visible : base.trades.visible,
5255
5283
  labels: isBool(trades.labels) ? trades.labels : base.trades.labels,
@@ -6071,9 +6099,22 @@ var TIMEZONES = [
6071
6099
  { value: "Pacific/Apia", label: "Apia" },
6072
6100
  { value: "Pacific/Kiritimati", label: "Kiritimati" }
6073
6101
  ];
6102
+ var EXCHANGE_TIMEZONE = "exchange";
6103
+ function isExchangeTimezone(zone) {
6104
+ return zone === EXCHANGE_TIMEZONE;
6105
+ }
6106
+ function resolveTimezone(zone, exchangeZone) {
6107
+ if (!isExchangeTimezone(zone)) return zone;
6108
+ return exchangeZone && exchangeZone !== "" ? exchangeZone : "Etc/UTC";
6109
+ }
6074
6110
  function normalizeTimezone(zone) {
6075
6111
  return zone === "UTC" || zone === "Etc/UTC" || zone === "Etc/GMT" ? "Etc/UTC" : zone;
6076
6112
  }
6113
+ function timezoneMenuRows(current) {
6114
+ const active = normalizeTimezone(current);
6115
+ const [utc, ...zones] = TIMEZONES.map((t) => ({ value: t.value, label: tzMenuLabel(t.value, t.label), checked: t.value === active }));
6116
+ return [utc, { value: EXCHANGE_TIMEZONE, label: "Exchange", checked: isExchangeTimezone(current) }, ...zones];
6117
+ }
6077
6118
  function tzOffset(zone, date = /* @__PURE__ */ new Date()) {
6078
6119
  try {
6079
6120
  const parts = new Intl.DateTimeFormat("en-US", { timeZone: zone, timeZoneName: "shortOffset" }).formatToParts(date);
@@ -6196,6 +6237,7 @@ var Bottombar = class {
6196
6237
  this.sessionButtons = /* @__PURE__ */ new Map();
6197
6238
  this.sessionEl = null;
6198
6239
  this.timezone = opts.timezone;
6240
+ this.exchangeTimezone = opts.exchangeTimezone;
6199
6241
  const doc = host.ownerDocument;
6200
6242
  injectStyles(STYLE_ID3, CSS4, doc);
6201
6243
  this.el = doc.createElement("div");
@@ -6219,7 +6261,7 @@ var Bottombar = class {
6219
6261
  this.clockEl = doc.createElement("span");
6220
6262
  this.clockEl.className = "vela-bb-clock";
6221
6263
  this.tzLabelEl = doc.createElement("span");
6222
- this.tzLabelEl.textContent = tzButtonLabel(this.timezone);
6264
+ this.tzLabelEl.textContent = tzButtonLabel(this.displayZone);
6223
6265
  this.tzButton.append(this.clockEl, this.tzLabelEl);
6224
6266
  const session = doc.createElement("span");
6225
6267
  session.className = "vela-bb-session";
@@ -6254,19 +6296,32 @@ var Bottombar = class {
6254
6296
  placement: "top-end",
6255
6297
  items: this.tzItems(),
6256
6298
  onSelect: (zone) => {
6257
- this.setTimezone(zone);
6299
+ this.setTimezone(zone, this.exchangeTimezone);
6258
6300
  opts.onTimezone(zone);
6259
6301
  }
6260
6302
  });
6261
6303
  this.tick();
6262
6304
  this.unsubClock = (opts.clock ?? new SecondClock()).onTick(() => this.tick());
6263
6305
  }
6264
- setTimezone(zone) {
6306
+ /**
6307
+ * Reflect the stored choice AND the active chart's market zone. The clock and the
6308
+ * offset label read the RESOLVED zone, so a workspace on the exchange rule re-labels
6309
+ * when the active cell (or its symbol) changes market — the host re-projects on
6310
+ * both. Idempotent: unchanged inputs leave the menu alone.
6311
+ */
6312
+ setTimezone(zone, exchangeTimezone) {
6313
+ if (zone === this.timezone && exchangeTimezone === this.exchangeTimezone) return;
6314
+ const choiceChanged = zone !== this.timezone;
6265
6315
  this.timezone = zone;
6266
- this.tzLabelEl.textContent = tzButtonLabel(zone);
6267
- this.tzMenu.setItems(this.tzItems());
6316
+ this.exchangeTimezone = exchangeTimezone;
6317
+ this.tzLabelEl.textContent = tzButtonLabel(this.displayZone);
6318
+ if (choiceChanged) this.tzMenu.setItems(this.tzItems());
6268
6319
  this.tick();
6269
6320
  }
6321
+ /** The zone the bar's clock and label render in. */
6322
+ get displayZone() {
6323
+ return resolveTimezone(this.timezone, this.exchangeTimezone);
6324
+ }
6270
6325
  /** Highlight (or clear with null) the active range chip — cleared on manual tf changes. */
6271
6326
  setActiveRange(id) {
6272
6327
  for (const [key, b] of this.rangeButtons) {
@@ -6294,11 +6349,7 @@ var Bottombar = class {
6294
6349
  this.el.remove();
6295
6350
  }
6296
6351
  tzItems() {
6297
- return TIMEZONES.map((t) => ({
6298
- id: t.value,
6299
- label: tzMenuLabel(t.value, t.label),
6300
- checked: t.value === normalizeTimezone(this.timezone)
6301
- }));
6352
+ return timezoneMenuRows(this.timezone).map((r) => ({ id: r.value, label: r.label, checked: r.checked }));
6302
6353
  }
6303
6354
  tick() {
6304
6355
  try {
@@ -6307,7 +6358,7 @@ var Bottombar = class {
6307
6358
  minute: "2-digit",
6308
6359
  second: "2-digit",
6309
6360
  hour12: false,
6310
- timeZone: this.timezone
6361
+ timeZone: this.displayZone
6311
6362
  }).format(/* @__PURE__ */ new Date());
6312
6363
  } catch {
6313
6364
  this.clockEl.textContent = "";
@@ -15929,15 +15980,14 @@ var TimezoneDrawer = class {
15929
15980
  this.drawer.body.replaceChildren();
15930
15981
  const list = doc.createElement("div");
15931
15982
  list.className = "vela-tzd-list";
15932
- const current = normalizeTimezone(this.opts.timezone());
15933
- for (const tz of TIMEZONES) {
15983
+ for (const tz of timezoneMenuRows(this.opts.timezone())) {
15934
15984
  const row = doc.createElement("div");
15935
15985
  row.className = "vela-tzd-row";
15936
15986
  const label = doc.createElement("span");
15937
15987
  label.className = "vela-tzd-row-label";
15938
- label.textContent = tzMenuLabel(tz.value, tz.label);
15988
+ label.textContent = tz.label;
15939
15989
  row.appendChild(label);
15940
- if (tz.value === current) row.appendChild(iconEl("check", doc));
15990
+ if (tz.checked) row.appendChild(iconEl("check", doc));
15941
15991
  row.addEventListener("click", () => {
15942
15992
  this.opts.onTimezone(tz.value);
15943
15993
  this.drawer.hide();
@@ -16024,12 +16074,11 @@ function priceAxisItems(s) {
16024
16074
  ];
16025
16075
  }
16026
16076
  function timeAxisItems(timezone) {
16027
- const active = timezone === "UTC" ? "Etc/UTC" : timezone;
16028
16077
  return [
16029
16078
  {
16030
16079
  id: "timezone",
16031
16080
  label: "Time zone",
16032
- submenu: TIMEZONES.map((t) => ({ id: `tz:${t.value}`, label: tzMenuLabel(t.value, t.label), checked: t.value === active }))
16081
+ submenu: timezoneMenuRows(timezone).map((r) => ({ id: `tz:${r.value}`, label: r.label, checked: r.checked }))
16033
16082
  },
16034
16083
  settingsItem("time-axis", "More settings\u2026")
16035
16084
  ];
@@ -21702,7 +21751,10 @@ var IndicatorInputsDialog = class {
21702
21751
  id,
21703
21752
  theme: this.host.theme(),
21704
21753
  get: () => String(bagOf(row, inp)[inp.key] ?? inp.defval),
21705
- onChange: (v) => emit(v)
21754
+ onChange: (v) => emit(v),
21755
+ // An input change re-executes the script over its whole history: commit the
21756
+ // opacity drag once on release, not once per pointer move.
21757
+ commit: "release"
21706
21758
  }).el;
21707
21759
  }
21708
21760
  if (inp.type === "symbol") return this.buildSymbol(id, String(current), emit);
@@ -21949,10 +22001,6 @@ var IndicatorInputsDialog = class {
21949
22001
  }
21950
22002
  /** Open a themed month calendar under `anchor` — same surface + shadow as the choice list. */
21951
22003
  openCalendar(anchor, current, onPick) {
21952
- const parsed = parseIsoDate(current);
21953
- let year = parsed?.getFullYear() ?? (/* @__PURE__ */ new Date()).getFullYear();
21954
- let month = parsed?.getMonth() ?? (/* @__PURE__ */ new Date()).getMonth();
21955
- const selected = parsed ? isoDate(parsed) : current;
21956
22004
  const pop = new Popover({
21957
22005
  trigger: anchor,
21958
22006
  theme: this.host.theme(),
@@ -21964,80 +22012,10 @@ var IndicatorInputsDialog = class {
21964
22012
  this.calendarPop = null;
21965
22013
  this.calendarAnchor = null;
21966
22014
  },
21967
- content: (el) => {
21968
- const title = document.createElement("div");
21969
- title.className = "vela-ind-cal-title";
21970
- const prev2 = document.createElement("button");
21971
- prev2.type = "button";
21972
- prev2.className = "vela-ind-cal-nav";
21973
- prev2.setAttribute("aria-label", "Previous month");
21974
- prev2.innerHTML = iconAt("chevron-left", 14);
21975
- const next = document.createElement("button");
21976
- next.type = "button";
21977
- next.className = "vela-ind-cal-nav";
21978
- next.setAttribute("aria-label", "Next month");
21979
- next.innerHTML = iconAt("chevron-right", 14);
21980
- const head = document.createElement("div");
21981
- head.className = "vela-ind-cal-head";
21982
- head.append(prev2, title, next);
21983
- const week = document.createElement("div");
21984
- week.className = "vela-ind-cal-week";
21985
- for (const d of WEEKDAY_LABELS) {
21986
- const cell = document.createElement("span");
21987
- cell.textContent = d;
21988
- week.appendChild(cell);
21989
- }
21990
- const grid = document.createElement("div");
21991
- grid.className = "vela-ind-cal-grid";
21992
- const paint = () => {
21993
- title.textContent = `${MONTH_LABELS[month]} ${year}`;
21994
- grid.replaceChildren();
21995
- const first = new Date(year, month, 1);
21996
- const startPad = first.getDay();
21997
- const days = new Date(year, month + 1, 0).getDate();
21998
- const today = isoDate(/* @__PURE__ */ new Date());
21999
- for (let i = 0; i < startPad; i++) {
22000
- const blank = document.createElement("span");
22001
- blank.className = "vela-ind-cal-blank";
22002
- grid.appendChild(blank);
22003
- }
22004
- for (let day = 1; day <= days; day++) {
22005
- const iso = `${year}-${String(month + 1).padStart(2, "0")}-${String(day).padStart(2, "0")}`;
22006
- const b = document.createElement("button");
22007
- b.type = "button";
22008
- b.className = "vela-ind-cal-day";
22009
- b.textContent = String(day);
22010
- if (iso === selected) b.dataset.checked = "1";
22011
- if (iso === today) b.dataset.today = "1";
22012
- b.addEventListener("click", (e) => {
22013
- e.stopPropagation();
22014
- pop.hide();
22015
- onPick(iso);
22016
- });
22017
- grid.appendChild(b);
22018
- }
22019
- };
22020
- prev2.addEventListener("click", (e) => {
22021
- e.stopPropagation();
22022
- month -= 1;
22023
- if (month < 0) {
22024
- month = 11;
22025
- year -= 1;
22026
- }
22027
- paint();
22028
- });
22029
- next.addEventListener("click", (e) => {
22030
- e.stopPropagation();
22031
- month += 1;
22032
- if (month > 11) {
22033
- month = 0;
22034
- year += 1;
22035
- }
22036
- paint();
22037
- });
22038
- paint();
22039
- el.append(head, week, grid);
22040
- }
22015
+ content: (el) => fillCalendar(el, current, (iso) => {
22016
+ pop.hide();
22017
+ onPick(iso);
22018
+ })
22041
22019
  });
22042
22020
  this.calendarPop = pop;
22043
22021
  this.calendarAnchor = anchor;
@@ -22146,7 +22124,7 @@ function groupInputs(inputs) {
22146
22124
  var CALENDAR_SVG = iconAt("calendar", 14);
22147
22125
  var CLOCK_SVG = iconAt("clock", 14);
22148
22126
  var DIALOG_STYLE_ID2 = "vela-ind-dialog-styles";
22149
- var DIALOG_STYLE_REV = "29";
22127
+ var DIALOG_STYLE_REV = "30";
22150
22128
  var LEGEND_ICON_PX = 16;
22151
22129
  function ensureDialogStyles() {
22152
22130
  if (typeof document === "undefined") return;
@@ -22162,8 +22140,12 @@ function ensureDialogStyles() {
22162
22140
  .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;}
22163
22141
  .vela-ind-combo-chevron:hover{opacity:0.9;}
22164
22142
  .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;}
22143
+ .vela-ind-cal [hidden]{display:none !important;}
22165
22144
  .vela-ind-cal-head{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:8px;}
22166
- .vela-ind-cal-title{flex:1;text-align:center;font-weight:600;font-size:14px;color:var(--vela-fg-bright);}
22145
+ .vela-ind-cal-title{flex:1;display:flex;align-items:center;justify-content:center;gap:2px;min-width:0;}
22146
+ .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;}
22147
+ .vela-ind-cal-switch:not(:disabled):hover{background:var(--vela-hover);}
22148
+ .vela-ind-cal-switch:disabled{cursor:default;}
22167
22149
  .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;}
22168
22150
  .vela-ind-cal-nav:hover{background:var(--vela-hover);color:var(--vela-fg-bright);}
22169
22151
  .vela-ind-cal-week,.vela-ind-cal-grid{display:grid;grid-template-columns:repeat(7,28px);gap:2px;}
@@ -22174,6 +22156,12 @@ function ensureDialogStyles() {
22174
22156
  .vela-ind-cal-day:hover{background:var(--vela-hover);}
22175
22157
  .vela-ind-cal-day[data-checked]{background:var(--vela-hover-strong);color:var(--vela-fg-bright);}
22176
22158
  .vela-ind-cal-day[data-today]:not([data-checked]){box-shadow:inset 0 0 0 1px var(--vela-border-strong);}
22159
+ .vela-ind-cal-cells{display:grid;grid-template-columns:repeat(3,1fr);gap:4px;width:calc(7 * 28px + 6 * 2px);}
22160
+ .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;}
22161
+ .vela-ind-cal-cell:hover{background:var(--vela-hover);}
22162
+ .vela-ind-cal-cell[data-checked]{background:var(--vela-hover-strong);color:var(--vela-fg-bright);}
22163
+ .vela-ind-cal-cell[data-today]:not([data-checked]){box-shadow:inset 0 0 0 1px var(--vela-border-strong);}
22164
+ .vela-ind-cal-cell[data-outside]{opacity:0.45;}
22177
22165
  ${overlayScrollbarCss(".vela-dialog.vela-ind-dialog *", 9)}
22178
22166
  .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;}
22179
22167
  .vela-ind-tab:not(.vela-ind-tab-active):hover{color:var(--vela-fg-bright);}
@@ -22208,6 +22196,7 @@ var TIME_OPTIONS = Array.from({ length: 48 }, (_, i) => {
22208
22196
  return { value: v, label: v };
22209
22197
  });
22210
22198
  var MONTH_LABELS = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"];
22199
+ var MONTH_SHORT = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
22211
22200
  var WEEKDAY_LABELS = ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"];
22212
22201
  function normalizeDateInput(raw) {
22213
22202
  const t = raw.trim();
@@ -22229,6 +22218,151 @@ function parseIsoDate(raw) {
22229
22218
  function isoDate(d) {
22230
22219
  return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
22231
22220
  }
22221
+ function fillCalendar(el, current, onPick) {
22222
+ const today = /* @__PURE__ */ new Date();
22223
+ const selected = parseIsoDate(current);
22224
+ let year = selected?.getFullYear() ?? today.getFullYear();
22225
+ let month = selected?.getMonth() ?? today.getMonth();
22226
+ let mode = "date";
22227
+ const prev2 = document.createElement("button");
22228
+ prev2.type = "button";
22229
+ prev2.className = "vela-ind-cal-nav";
22230
+ prev2.innerHTML = iconAt("chevron-left", 14);
22231
+ const next = document.createElement("button");
22232
+ next.type = "button";
22233
+ next.className = "vela-ind-cal-nav";
22234
+ next.innerHTML = iconAt("chevron-right", 14);
22235
+ const monthBtn = document.createElement("button");
22236
+ monthBtn.type = "button";
22237
+ monthBtn.className = "vela-ind-cal-switch";
22238
+ monthBtn.setAttribute("aria-label", "Choose month");
22239
+ const yearBtn = document.createElement("button");
22240
+ yearBtn.type = "button";
22241
+ yearBtn.className = "vela-ind-cal-switch";
22242
+ yearBtn.setAttribute("aria-label", "Choose year");
22243
+ const title = document.createElement("div");
22244
+ title.className = "vela-ind-cal-title";
22245
+ title.append(monthBtn, yearBtn);
22246
+ const head = document.createElement("div");
22247
+ head.className = "vela-ind-cal-head";
22248
+ head.append(prev2, title, next);
22249
+ const week = document.createElement("div");
22250
+ week.className = "vela-ind-cal-week";
22251
+ for (const d of WEEKDAY_LABELS) {
22252
+ const cell = document.createElement("span");
22253
+ cell.textContent = d;
22254
+ week.appendChild(cell);
22255
+ }
22256
+ const grid = document.createElement("div");
22257
+ const paint = () => {
22258
+ const decade = Math.floor(year / 10) * 10;
22259
+ monthBtn.hidden = mode !== "date";
22260
+ monthBtn.textContent = MONTH_LABELS[month] ?? "";
22261
+ yearBtn.textContent = mode === "year" ? `${decade}-${decade + 9}` : String(year);
22262
+ yearBtn.disabled = mode === "year";
22263
+ week.hidden = mode !== "date";
22264
+ prev2.setAttribute("aria-label", mode === "date" ? "Previous month" : mode === "month" ? "Previous year" : "Previous decade");
22265
+ next.setAttribute("aria-label", mode === "date" ? "Next month" : mode === "month" ? "Next year" : "Next decade");
22266
+ grid.className = mode === "date" ? "vela-ind-cal-grid" : "vela-ind-cal-cells";
22267
+ grid.replaceChildren();
22268
+ if (mode === "date") {
22269
+ const startPad = new Date(year, month, 1).getDay();
22270
+ const days = new Date(year, month + 1, 0).getDate();
22271
+ const todayIso = isoDate(today);
22272
+ const selectedIso = selected ? isoDate(selected) : "";
22273
+ for (let i = 0; i < startPad; i++) {
22274
+ const blank = document.createElement("span");
22275
+ blank.className = "vela-ind-cal-blank";
22276
+ grid.appendChild(blank);
22277
+ }
22278
+ for (let day = 1; day <= days; day++) {
22279
+ const iso = `${year}-${String(month + 1).padStart(2, "0")}-${String(day).padStart(2, "0")}`;
22280
+ const b = document.createElement("button");
22281
+ b.type = "button";
22282
+ b.className = "vela-ind-cal-day";
22283
+ b.textContent = String(day);
22284
+ if (iso === selectedIso) b.dataset.checked = "1";
22285
+ if (iso === todayIso) b.dataset.today = "1";
22286
+ b.addEventListener("click", (e) => {
22287
+ e.stopPropagation();
22288
+ onPick(iso);
22289
+ });
22290
+ grid.appendChild(b);
22291
+ }
22292
+ return;
22293
+ }
22294
+ if (mode === "month") {
22295
+ for (let m = 0; m < 12; m++) {
22296
+ const b = document.createElement("button");
22297
+ b.type = "button";
22298
+ b.className = "vela-ind-cal-cell";
22299
+ b.textContent = MONTH_SHORT[m] ?? "";
22300
+ if (selected?.getFullYear() === year && selected.getMonth() === m) b.dataset.checked = "1";
22301
+ if (today.getFullYear() === year && today.getMonth() === m) b.dataset.today = "1";
22302
+ b.addEventListener("click", (e) => {
22303
+ e.stopPropagation();
22304
+ month = m;
22305
+ mode = "date";
22306
+ paint();
22307
+ });
22308
+ grid.appendChild(b);
22309
+ }
22310
+ return;
22311
+ }
22312
+ for (let y = decade - 1; y <= decade + 10; y++) {
22313
+ const b = document.createElement("button");
22314
+ b.type = "button";
22315
+ b.className = "vela-ind-cal-cell";
22316
+ b.textContent = String(y);
22317
+ if (y < decade || y > decade + 9) b.dataset.outside = "1";
22318
+ if (selected?.getFullYear() === y) b.dataset.checked = "1";
22319
+ if (today.getFullYear() === y) b.dataset.today = "1";
22320
+ b.addEventListener("click", (e) => {
22321
+ e.stopPropagation();
22322
+ year = y;
22323
+ mode = "month";
22324
+ paint();
22325
+ });
22326
+ grid.appendChild(b);
22327
+ }
22328
+ };
22329
+ const step = (dir) => {
22330
+ if (mode === "date") {
22331
+ month += dir;
22332
+ if (month < 0) {
22333
+ month = 11;
22334
+ year -= 1;
22335
+ }
22336
+ if (month > 11) {
22337
+ month = 0;
22338
+ year += 1;
22339
+ }
22340
+ } else {
22341
+ year += mode === "month" ? dir : dir * 10;
22342
+ }
22343
+ paint();
22344
+ };
22345
+ prev2.addEventListener("click", (e) => {
22346
+ e.stopPropagation();
22347
+ step(-1);
22348
+ });
22349
+ next.addEventListener("click", (e) => {
22350
+ e.stopPropagation();
22351
+ step(1);
22352
+ });
22353
+ monthBtn.addEventListener("click", (e) => {
22354
+ e.stopPropagation();
22355
+ mode = "month";
22356
+ paint();
22357
+ });
22358
+ yearBtn.addEventListener("click", (e) => {
22359
+ e.stopPropagation();
22360
+ mode = "year";
22361
+ paint();
22362
+ });
22363
+ paint();
22364
+ el.append(head, week, grid);
22365
+ }
22232
22366
  function normalizeTimeInput(raw) {
22233
22367
  const t = raw.trim();
22234
22368
  const m = /^(\d{1,2}):(\d{2})$/.exec(t) ?? /^(\d{2})(\d{2})$/.exec(t);
@@ -28716,6 +28850,10 @@ var BUILTIN_SETTINGS_IDS = [
28716
28850
  "canvas.grid",
28717
28851
  "canvas.grid.vertical",
28718
28852
  "canvas.grid.horizontal",
28853
+ "canvas.margins",
28854
+ "canvas.margins.top",
28855
+ "canvas.margins.bottom",
28856
+ "canvas.margins.right",
28719
28857
  "canvas.theme"
28720
28858
  ];
28721
28859
  function settingsIdCatalog(hostSections, markGroups = []) {
@@ -29246,6 +29384,10 @@ var SettingsDialog = class {
29246
29384
  body.append(sid(this.toggleRow("Horizontal", config.grid.horzLines.visible, (v) => this.emit({ grid: { horzLines: { visible: v } } }), [
29247
29385
  this.swatch(config.grid.horzLines.color, (v) => this.emit({ grid: { horzLines: { color: v } } }))
29248
29386
  ]), "canvas.grid.horizontal"));
29387
+ body.append(sid(this.sectionTitle("Margins"), "canvas.margins"));
29388
+ body.append(sid(this.numberRow("Top", config.margins.top, 0, 40, 1, (v) => this.emit({ margins: { top: v } }), "%"), "canvas.margins.top"));
29389
+ body.append(sid(this.numberRow("Bottom", config.margins.bottom, 0, 40, 1, (v) => this.emit({ margins: { bottom: v } }), "%"), "canvas.margins.bottom"));
29390
+ body.append(sid(this.numberRow("Right", config.margins.right, 0, 200, 1, (v) => this.emit({ margins: { right: v } }), "bars"), "canvas.margins.right"));
29249
29391
  if (this.themeControl) {
29250
29392
  const tc = this.themeControl;
29251
29393
  body.append(sid(this.sectionTitle("Theme"), "canvas.theme"));
@@ -29817,23 +29959,16 @@ var SettingsDialog = class {
29817
29959
  this.hintTips.push(dispose);
29818
29960
  return el;
29819
29961
  }
29820
- numberRow(label, value, min, max, step, onChange) {
29821
- return fieldRow({
29822
- label,
29823
- labelSize: "sm",
29824
- className: "vela-sd-row",
29825
- control: buildFieldControl({
29826
- kind: "number",
29827
- value,
29828
- min,
29829
- max,
29830
- step,
29831
- fill: false,
29832
- commit: "live",
29833
- steppers: true,
29834
- onChange
29835
- }).el
29836
- });
29962
+ /** `unit` (optional) trails the input as muted text — "%", "bars". */
29963
+ numberRow(label, value, min, max, step, onChange, unit) {
29964
+ const controls = [buildFieldControl({ kind: "number", value, min, max, step, fill: false, commit: "live", steppers: true, onChange }).el];
29965
+ if (unit) {
29966
+ const u = document.createElement("span");
29967
+ u.textContent = unit;
29968
+ u.style.cssText = "color:var(--vela-fg-muted);";
29969
+ controls.push(u);
29970
+ }
29971
+ return this.rowWith(label, controls);
29837
29972
  }
29838
29973
  /** A dropdown whose option values differ from their display labels. */
29839
29974
  selectRowLabeled(label, value, options, onChange) {
@@ -34707,9 +34842,7 @@ function createProjector(coords, paneOf, paneIdAtY, barsInRange, seriesInRange)
34707
34842
  }
34708
34843
 
34709
34844
  // src/renderers/native/core/autoscale.ts
34710
- var MARGIN_TOP = 2 / 7;
34711
- var MARGIN_BOTTOM = 1 / 7;
34712
- function computePaneScale(models, bars, includeCandles, i0, i1, drawings, log = false, offsetOf = () => 0) {
34845
+ function computePaneScale(models, bars, includeCandles, i0, i1, drawings, log = false, offsetOf = () => 0, margins = DEFAULT_MARGINS) {
34713
34846
  let min = Infinity;
34714
34847
  let max = -Infinity;
34715
34848
  const consider = (v) => {
@@ -34744,14 +34877,17 @@ function computePaneScale(models, bars, includeCandles, i0, i1, drawings, log =
34744
34877
  const pad = Math.abs(min) * 0.1 || 1;
34745
34878
  return { min: min - pad, max: max + pad, log: log && min - pad > 0 };
34746
34879
  }
34880
+ const content = Math.max(0.1, 1 - (margins.top + margins.bottom) / 100);
34881
+ const above = margins.top / 100 / content;
34882
+ const below = margins.bottom / 100 / content;
34747
34883
  if (log && min > 0) {
34748
34884
  const lmin = Math.log(min);
34749
34885
  const lmax = Math.log(max);
34750
34886
  const lspan = lmax - lmin;
34751
- return { min: Math.exp(lmin - lspan * MARGIN_BOTTOM), max: Math.exp(lmax + lspan * MARGIN_TOP), log: true };
34887
+ return { min: Math.exp(lmin - lspan * below), max: Math.exp(lmax + lspan * above), log: true };
34752
34888
  }
34753
34889
  const span = max - min;
34754
- return { min: min - span * MARGIN_BOTTOM, max: max + span * MARGIN_TOP };
34890
+ return { min: min - span * below, max: max + span * above };
34755
34891
  }
34756
34892
  function considerSeries(s, i0, i1, off, consider) {
34757
34893
  if (!seriesInScale(s)) return;
@@ -36442,6 +36578,7 @@ var NativeRenderer = class {
36442
36578
  intro: this.intro.style !== false
36443
36579
  },
36444
36580
  panes: { separatorColor: s.separatorColor ?? t.borderColor },
36581
+ margins: { ...s.margins },
36445
36582
  trades: {
36446
36583
  visible: this.scene.tradeMarkers.visible,
36447
36584
  labels: this.scene.tradeMarkers.labels,
@@ -36554,6 +36691,10 @@ var NativeRenderer = class {
36554
36691
  this.animAutoscale.toggle(next.animations.autoscale);
36555
36692
  this.intro = { style: next.animations.intro ? this.introOnStyle : false, duration: this.intro.duration || INTRO_DURATION_DEFAULT_MS };
36556
36693
  s.separatorColor = keepInherit(s.separatorColor, next.panes.separatorColor, prevTheme.borderColor);
36694
+ if (next.margins.right !== s.margins.right && this.coords.barCount > 0) {
36695
+ this.applyViewport({ barSpacing: this.coords.getViewport().barSpacing, rightOffset: next.margins.right });
36696
+ }
36697
+ s.margins = { ...next.margins };
36557
36698
  this.scene.tradeMarkers = {
36558
36699
  visible: next.trades.visible,
36559
36700
  labels: next.trades.labels,
@@ -36797,7 +36938,7 @@ var NativeRenderer = class {
36797
36938
  /** Glide the view back to the most recent bars, keeping the current zoom (barSpacing). */
36798
36939
  scrollToRealtime() {
36799
36940
  if (this.coords.barCount === 0) return;
36800
- this.glideRightOffset(ZOOM_OUT_MARGIN_BARS);
36941
+ this.glideRightOffset(this.scene.style.margins.right);
36801
36942
  }
36802
36943
  /** Ease rightOffset to `target` at constant zoom (see animTick's scroll glide);
36803
36944
  * instant when the scroll glide is off. Shared by scroll-to-latest and panBy. */
@@ -38739,6 +38880,7 @@ var NativeRenderer = class {
38739
38880
  const pricePane = panes.find((p) => p.kind === "price") ?? null;
38740
38881
  this.chrome.prepare(this.scene, this.coords, this.theme);
38741
38882
  const animating = this.animator.active;
38883
+ const margins = this.scene.style.margins;
38742
38884
  for (const pane of panes) {
38743
38885
  if (pane.manualScale) {
38744
38886
  pane.scaleTarget = pane.manualScale;
@@ -38754,7 +38896,7 @@ var NativeRenderer = class {
38754
38896
  if (or) dr = dr ? { min: Math.min(dr.min, or.min), max: Math.max(dr.max, or.max) } : or;
38755
38897
  }
38756
38898
  const includeCandles = pane.kind === "price" && (!this.scene.candlesHidden || this.priceLayersAnchoredToBars(masterModels) || !this.paneHasMeasurableContent(masterModels, dr));
38757
- pane.scaleTarget = computePaneScale(masterModels, this.bars, includeCandles, i0, i1, dr, paneLogScale(this.scene, pane), (id) => this.scene.offsetOf(id));
38899
+ pane.scaleTarget = computePaneScale(masterModels, this.bars, includeCandles, i0, i1, dr, paneLogScale(this.scene, pane), (id) => this.scene.offsetOf(id), margins);
38758
38900
  pane.percentBaseline = pane.kind === "price" ? this.bars[i0]?.close ?? 0 : this.firstVisibleValue(masterModels, i0);
38759
38901
  pane.axisFormat = void 0;
38760
38902
  pane.axisBands = void 0;
@@ -38765,7 +38907,7 @@ var NativeRenderer = class {
38765
38907
  pane.axisFormat = "volume";
38766
38908
  }
38767
38909
  } else if (this.layerNativesOwnPane(pane, masterModels)) {
38768
- pane.scaleTarget = computePaneScale([], this.bars, true, i0, i1, dr, paneLogScale(this.scene, pane), (id) => this.scene.offsetOf(id));
38910
+ pane.scaleTarget = computePaneScale([], this.bars, true, i0, i1, dr, paneLogScale(this.scene, pane), (id) => this.scene.offsetOf(id), margins);
38769
38911
  pane.percentBaseline = this.bars[i0]?.close ?? 0;
38770
38912
  if (masterModels.every((m) => m.paneAxis != null)) {
38771
38913
  pane.axisFormat = "none";
@@ -38794,7 +38936,7 @@ var NativeRenderer = class {
38794
38936
  continue;
38795
38937
  }
38796
38938
  const mdr = this.chrome.paneDrawingsRange([model], this.scene, false, vr);
38797
- sl.scaleTarget = computePaneScale([model], this.bars, false, i0, i1, mdr, false, (id) => this.scene.offsetOf(id));
38939
+ sl.scaleTarget = computePaneScale([model], this.bars, false, i0, i1, mdr, false, (id) => this.scene.offsetOf(id), margins);
38798
38940
  if (!animating || !sl.initialized) {
38799
38941
  sl.scale = { ...sl.scaleTarget };
38800
38942
  sl.initialized = true;
@@ -38835,13 +38977,13 @@ var NativeRenderer = class {
38835
38977
  for (const pane of this.scene.panes.values()) pane.manualScale = null;
38836
38978
  for (const sl of this.scene.indicatorScales.values()) sl.manualScale = null;
38837
38979
  const visibleBars = Math.min(n, 200);
38838
- const rightOffset = 6;
38980
+ const rightOffset = this.scene.style.margins.right;
38839
38981
  const v = this.clampViewport(w / ((visibleBars + rightOffset) * this.coords.spacingScale), rightOffset);
38840
38982
  this.coords.setViewport(v);
38841
38983
  this.targetBarSpacing = v.barSpacing;
38842
38984
  }
38843
38985
  /** Re-frame after a series replacement (a symbol/timeframe switch): keep the user's
38844
- * zoom (bar spacing), re-anchor the newest bars at the default right offset.
38986
+ * zoom (bar spacing), re-anchor the newest bars at the configured right margin.
38845
38987
  * `clampViewport`'s fit-all-bars floor deliberately does NOT apply — a progressive
38846
38988
  * head may still be backfilling toward the previous depth, and raising the spacing
38847
38989
  * to its temporary bar count would lose the zoom this exists to keep. */
@@ -38850,7 +38992,7 @@ var NativeRenderer = class {
38850
38992
  this.panVelocity = 0;
38851
38993
  for (const pane of this.scene.panes.values()) pane.manualScale = null;
38852
38994
  for (const sl of this.scene.indicatorScales.values()) sl.manualScale = null;
38853
- const v = { barSpacing: clampBarSpacing(this.coords.getViewport().barSpacing), rightOffset: defaultViewport().rightOffset };
38995
+ const v = { barSpacing: clampBarSpacing(this.coords.getViewport().barSpacing), rightOffset: this.scene.style.margins.right };
38854
38996
  this.coords.setViewport(v);
38855
38997
  this.targetBarSpacing = v.barSpacing;
38856
38998
  }
@@ -43462,7 +43604,7 @@ var ChartContextMenu = class {
43462
43604
  } else if (id.startsWith("tz:")) {
43463
43605
  const zone = id.slice("tz:".length);
43464
43606
  if (this.cbs.setTimezone) this.cbs.setTimezone(zone);
43465
- else chart.renderer.set("timezone", zone);
43607
+ else chart.renderer.set("timezone", resolveTimezone(zone, void 0));
43466
43608
  } else if (id === "auto") {
43467
43609
  chart.renderer.set("autoScale", chart.renderer.get("autoScale") === false);
43468
43610
  } else if (id === "invert") {
@@ -43695,7 +43837,7 @@ var ChartCell = class {
43695
43837
  this.history.onChart(this.inner);
43696
43838
  this.inner.renderer.onConfigChanged(() => {
43697
43839
  const zone = this.inner?.renderer.get("timezone");
43698
- if (typeof zone === "string" && normalizeTimezone(zone) !== normalizeTimezone(this.deps.timezone())) {
43840
+ if (typeof zone === "string" && normalizeTimezone(zone) !== normalizeTimezone(this.displayTimezone)) {
43699
43841
  this.deps.setTimezone(normalizeTimezone(zone));
43700
43842
  }
43701
43843
  const style = this.priceStyle;
@@ -43737,8 +43879,7 @@ var ChartCell = class {
43737
43879
  this.refreshSessionShading();
43738
43880
  });
43739
43881
  this.inner.on("viewport:changed", (range) => this.sessionShading.updateRange(range));
43740
- const tz = deps.timezone();
43741
- if (tz !== "Etc/UTC") this.inner.renderer.set("timezone", tz);
43882
+ this.applyTimezone();
43742
43883
  this.indicatorTitlesOn = seed.indicatorTitles ?? true;
43743
43884
  if (!this.indicatorTitlesOn) this.inner.renderer.set("indicatorTitles", false);
43744
43885
  this.indicatorValuesOn = seed.indicatorValues ?? true;
@@ -43760,7 +43901,7 @@ var ChartCell = class {
43760
43901
  this.statusline?.setSymbol(this.state.symbol);
43761
43902
  this.statusline?.setMeta(this.state.timeframe ?? "60", this.inner.data.displayPrefix(this.state.symbol) ?? this.state.provider ?? "");
43762
43903
  }
43763
- this.refreshSessionAvailable();
43904
+ this.refreshSymbolMetadata();
43764
43905
  if (this.inner && this.state.symbol) this.marketStatus?.track(this.inner.data, this.state.symbol);
43765
43906
  });
43766
43907
  this.syncStatuslineColors();
@@ -43834,7 +43975,7 @@ var ChartCell = class {
43834
43975
  this.offMarket = this.inner.on("market:changed", ({ symbol: symbol2, timeframe }) => {
43835
43976
  this.projectMarket(symbol2, timeframe);
43836
43977
  this.refreshNativeCatalog();
43837
- this.refreshSessionAvailable();
43978
+ this.refreshSymbolMetadata();
43838
43979
  if (this.inner) this.marketStatus?.track(this.inner.data, symbol2);
43839
43980
  this.deps.onMarketChanged(this.id);
43840
43981
  });
@@ -43875,7 +44016,35 @@ var ChartCell = class {
43875
44016
  this.deps.onStateDirty();
43876
44017
  void this.inner?.setMarket({ session });
43877
44018
  }
43878
- refreshSessionAvailable() {
44019
+ /**
44020
+ * The symbol's own trading zone, once its metadata has landed — what the exchange
44021
+ * rule resolves to on this cell (the workspace labels the shared bottom bar with the
44022
+ * ACTIVE cell's). Undefined = unknown or undeclared (the rule then renders UTC).
44023
+ */
44024
+ get exchangeTimezone() {
44025
+ return this.exchangeZone;
44026
+ }
44027
+ /** The IANA zone this cell's axis renders in: the workspace choice, resolved. */
44028
+ get displayTimezone() {
44029
+ return resolveTimezone(this.deps.timezone(), this.exchangeZone);
44030
+ }
44031
+ /**
44032
+ * Push the resolved zone to the renderer — on the workspace choice changing, and on
44033
+ * this cell's market zone changing under the exchange rule. Skips the write when the
44034
+ * renderer already holds it (its config default is the bare `'UTC'` alias).
44035
+ */
44036
+ applyTimezone() {
44037
+ const chart = this.inner;
44038
+ if (!chart) return;
44039
+ const zone = this.displayTimezone;
44040
+ const current = chart.renderer.get("timezone");
44041
+ const held = typeof current === "string" && current ? current : "UTC";
44042
+ if (normalizeTimezone(held) === normalizeTimezone(zone)) return;
44043
+ chart.renderer.set("timezone", zone);
44044
+ }
44045
+ /** Re-read the symbol's metadata: session posture (RTH/ETH toggle, shading) and its
44046
+ * trading zone (the exchange rule). Async — the workspace re-projects when a verdict lands. */
44047
+ refreshSymbolMetadata() {
43879
44048
  const chart = this.inner;
43880
44049
  const symbol = this.state.symbol;
43881
44050
  if (!chart || !symbol) return;
@@ -43883,7 +44052,13 @@ var ChartCell = class {
43883
44052
  if (this.inner !== chart) return;
43884
44053
  const available = typeof si?.session === "string" && si.session !== "" && si.session !== "24x7";
43885
44054
  const overnight = parseSessionSpec(si)?.overnight === true;
43886
- if (available !== this.sessionAvailableFlag || overnight !== this.sessionOvernightFlag) {
44055
+ const zone = typeof si?.timezone === "string" && si.timezone !== "" ? si.timezone : void 0;
44056
+ const zoneChanged = zone !== this.exchangeZone;
44057
+ if (zoneChanged) {
44058
+ this.exchangeZone = zone;
44059
+ this.applyTimezone();
44060
+ }
44061
+ if (available !== this.sessionAvailableFlag || overnight !== this.sessionOvernightFlag || zoneChanged) {
43887
44062
  this.sessionAvailableFlag = available;
43888
44063
  this.sessionOvernightFlag = overnight;
43889
44064
  this.deps.onMarketChanged(this.id);
@@ -45659,13 +45834,23 @@ var VelaWorkspace = class {
45659
45834
  }
45660
45835
  }
45661
45836
  }
45662
- /** Set the workspace-global display timezone — applied to EVERY cell. */
45837
+ /**
45838
+ * Set the workspace-global display timezone — applied to EVERY cell. An IANA zone,
45839
+ * or `'exchange'` (the exchange rule): each cell then renders in its OWN market's
45840
+ * zone (Chicago for a CME future, New York for a US equity, UTC for crypto), and the
45841
+ * bottom bar follows the active cell's.
45842
+ */
45663
45843
  setTimezone(zone) {
45664
45844
  this.timezone = zone;
45665
- this.bottombar?.setTimezone(zone);
45666
- for (const cell of this.cellsById.values()) cell.chart.renderer.set("timezone", zone);
45845
+ this.projectTimezone();
45846
+ for (const cell of this.cellsById.values()) cell.applyTimezone();
45667
45847
  this.markStateDirty();
45668
45848
  }
45849
+ /** Bottom bar ⇐ the stored choice + the ACTIVE cell's market zone (labels the exchange row). */
45850
+ projectTimezone() {
45851
+ const active = this.activeId ? this.cellsById.get(this.activeId) : void 0;
45852
+ this.bottombar?.setTimezone(this.timezone, active?.exchangeTimezone);
45853
+ }
45669
45854
  /**
45670
45855
  * Swap the workspace theme at runtime — `'dark'`, `'light'`, or a full custom theme,
45671
45856
  * applied to the shared chrome (topbar, panels, drawing toolbar) and EVERY cell.
@@ -45870,6 +46055,7 @@ var VelaWorkspace = class {
45870
46055
  this.dock.onChart(cell.chart);
45871
46056
  this.bottombar?.setActiveRange(cell.activeRangeId);
45872
46057
  this.bottombar?.setSession({ session: cell.session, enabled: cell.sessionAvailable });
46058
+ this.projectTimezone();
45873
46059
  this.indicatorPicker?.sync();
45874
46060
  this.glider.stop();
45875
46061
  const d = cell.chart.drawings;
@@ -46454,6 +46640,7 @@ var VelaWorkspace = class {
46454
46640
  this.mobileBar?.setTimeframe(cell.timeframe);
46455
46641
  this.objectTree.setSymbol(cell.symbol);
46456
46642
  this.bottombar?.setSession({ session: cell.session, enabled: cell.sessionAvailable });
46643
+ this.projectTimezone();
46457
46644
  this.bottombar?.setActiveRange(cell.activeRangeId);
46458
46645
  }
46459
46646
  /** Trigger ② — a cell's price style changed: the topbar button/menu only if active.