@luxalgo/vela 0.7.5 → 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.
Files changed (41) hide show
  1. package/dist/{DataProvider-Dut6lXGM.d.cts → DataProvider-CGa4KHRa.d.cts} +1 -1
  2. package/dist/{DataProvider-DLeFs1gE.d.ts → DataProvider-CmWUtDVL.d.ts} +1 -1
  3. package/dist/{chunk-ZADTVRUO.js → chunk-PVGWNF7V.js} +1 -1
  4. package/dist/{chunk-MCLI6B3S.js → chunk-RVQWJOEE.js} +277 -120
  5. package/dist/{chunk-BKHSQ4YM.js → chunk-WTWGEB7K.js} +23 -9
  6. package/dist/{chunk-MNG5XPLV.js → chunk-ZA2XWTGH.js} +94 -37
  7. package/dist/{contributions-DYtiRtES.d.cts → contributions-DF_Ea07m.d.cts} +25 -2
  8. package/dist/{contributions-CEkgJKTU.d.ts → contributions-nKOSeLy8.d.ts} +25 -2
  9. package/dist/index.cjs +285 -127
  10. package/dist/index.d.cts +22 -7
  11. package/dist/index.d.ts +22 -7
  12. package/dist/index.js +2 -2
  13. package/dist/{options-DNBoMKkX.d.ts → options-D1AJKsn_.d.cts} +10 -0
  14. package/dist/{options-DNBoMKkX.d.cts → options-D1AJKsn_.d.ts} +10 -0
  15. package/dist/{plugin-CLn--oP6.d.ts → plugin-0oFDnexb.d.ts} +3 -3
  16. package/dist/{plugin-DtkeQVAO.d.cts → plugin-DPyVJFoA.d.cts} +3 -3
  17. package/dist/plugin.d.cts +4 -4
  18. package/dist/plugin.d.ts +4 -4
  19. package/dist/providers/binance.d.cts +2 -2
  20. package/dist/providers/binance.d.ts +2 -2
  21. package/dist/providers/coinbase.d.cts +2 -2
  22. package/dist/providers/coinbase.d.ts +2 -2
  23. package/dist/providers/hyperliquid.d.cts +2 -2
  24. package/dist/providers/hyperliquid.d.ts +2 -2
  25. package/dist/{statusline-model-gA__yaog.d.cts → statusline-model-BoWJ3cWQ.d.cts} +18 -5
  26. package/dist/{statusline-model-DypGXjtr.d.ts → statusline-model-DAFyqVmR.d.ts} +18 -5
  27. package/dist/ui.cjs +23 -9
  28. package/dist/ui.d.cts +20 -3
  29. package/dist/ui.d.ts +20 -3
  30. package/dist/ui.js +2 -2
  31. package/dist/vela.global.js +285 -127
  32. package/dist/vela.global.min.js +59 -49
  33. package/dist/widget.cjs +389 -161
  34. package/dist/widget.d.cts +7 -7
  35. package/dist/widget.d.ts +7 -7
  36. package/dist/widget.js +5 -5
  37. package/dist/workspace.cjs +389 -161
  38. package/dist/workspace.d.cts +40 -8
  39. package/dist/workspace.d.ts +40 -8
  40. package/dist/workspace.js +4 -4
  41. package/package.json +21 -1
@@ -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
  ];
@@ -18392,10 +18441,18 @@ var IndicatorRegistry = class {
18392
18441
  this.records = /* @__PURE__ */ new Map();
18393
18442
  this.counter = 0;
18394
18443
  }
18395
- /** Allocate a unique, stable per-instance id. */
18396
- nextId(prefix = "ind") {
18397
- this.counter += 1;
18398
- return `${prefix}-${this.counter}`;
18444
+ /**
18445
+ * Mint a unique per-instance id. Host-supplied ids share the namespace, so a minted
18446
+ * id skips anything already recorded — and anything `taken` reports live elsewhere
18447
+ * (a handle the orchestrator holds outside the records, e.g. a fail-soft native).
18448
+ */
18449
+ nextId(prefix = "ind", taken = () => false) {
18450
+ let id;
18451
+ do {
18452
+ this.counter += 1;
18453
+ id = `${prefix}-${this.counter}`;
18454
+ } while (this.records.has(id) || taken(id));
18455
+ return id;
18399
18456
  }
18400
18457
  add(record) {
18401
18458
  this.records.set(record.id, record);
@@ -19778,7 +19835,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
19778
19835
  this.engines.set(language, engine);
19779
19836
  }
19780
19837
  addIndicator(source, options = {}) {
19781
- const id = this.registry.nextId();
19838
+ const id = this.claimIndicatorId(options.id);
19782
19839
  const title = options.title ?? "Indicator";
19783
19840
  const handle = new IndicatorHandleImpl(id, title, this, source);
19784
19841
  this.handles.set(id, handle);
@@ -19800,7 +19857,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
19800
19857
  const existing = this.registry.all().find((r) => r.native?.type === type);
19801
19858
  if (existing) return this.handles.get(existing.id) ?? new IndicatorHandleImpl(existing.id, existing.title, this, void 0, type);
19802
19859
  }
19803
- const id = this.registry.nextId("native");
19860
+ const id = this.registry.nextId("native", (candidate) => this.handles.has(candidate));
19804
19861
  const title = descriptor?.title ?? type;
19805
19862
  const handle = new IndicatorHandleImpl(id, title, this, void 0, type);
19806
19863
  this.handles.set(id, handle);
@@ -20078,6 +20135,24 @@ var EngineOrchestrator = class _EngineOrchestrator {
20078
20135
  this.events.clear();
20079
20136
  }
20080
20137
  // ── internals ───────────────────────────────────────────────
20138
+ /**
20139
+ * The id a new script indicator runs under: the host's when supplied, else a minted
20140
+ * one. A host id is opaque but must be a non-empty string, and it must not be live
20141
+ * on this chart — a duplicate is a programming error surfaced synchronously, never
20142
+ * renamed behind the caller's back (the whole point of supplying one is that
20143
+ * `handle.id` equals what was passed). "Live" reads both the records and the handle
20144
+ * map: a fail-soft handle never enters the registry but still owns its id.
20145
+ */
20146
+ claimIndicatorId(requested) {
20147
+ if (requested === void 0) return this.registry.nextId("ind", (candidate) => this.handles.has(candidate));
20148
+ if (typeof requested !== "string" || requested.length === 0) {
20149
+ throw new TypeError("[vela] addIndicator: `id` must be a non-empty string");
20150
+ }
20151
+ if (this.handles.has(requested) || this.registry.get(requested)) {
20152
+ throw new Error(`[vela] addIndicator: indicator id "${requested}" is already live on this chart`);
20153
+ }
20154
+ return requested;
20155
+ }
20081
20156
  async startIndicator(id, source, options, handle) {
20082
20157
  try {
20083
20158
  await this.readyPromise;
@@ -21676,7 +21751,10 @@ var IndicatorInputsDialog = class {
21676
21751
  id,
21677
21752
  theme: this.host.theme(),
21678
21753
  get: () => String(bagOf(row, inp)[inp.key] ?? inp.defval),
21679
- 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"
21680
21758
  }).el;
21681
21759
  }
21682
21760
  if (inp.type === "symbol") return this.buildSymbol(id, String(current), emit);
@@ -21923,10 +22001,6 @@ var IndicatorInputsDialog = class {
21923
22001
  }
21924
22002
  /** Open a themed month calendar under `anchor` — same surface + shadow as the choice list. */
21925
22003
  openCalendar(anchor, current, onPick) {
21926
- const parsed = parseIsoDate(current);
21927
- let year = parsed?.getFullYear() ?? (/* @__PURE__ */ new Date()).getFullYear();
21928
- let month = parsed?.getMonth() ?? (/* @__PURE__ */ new Date()).getMonth();
21929
- const selected = parsed ? isoDate(parsed) : current;
21930
22004
  const pop = new Popover({
21931
22005
  trigger: anchor,
21932
22006
  theme: this.host.theme(),
@@ -21938,80 +22012,10 @@ var IndicatorInputsDialog = class {
21938
22012
  this.calendarPop = null;
21939
22013
  this.calendarAnchor = null;
21940
22014
  },
21941
- content: (el) => {
21942
- const title = document.createElement("div");
21943
- title.className = "vela-ind-cal-title";
21944
- const prev2 = document.createElement("button");
21945
- prev2.type = "button";
21946
- prev2.className = "vela-ind-cal-nav";
21947
- prev2.setAttribute("aria-label", "Previous month");
21948
- prev2.innerHTML = iconAt("chevron-left", 14);
21949
- const next = document.createElement("button");
21950
- next.type = "button";
21951
- next.className = "vela-ind-cal-nav";
21952
- next.setAttribute("aria-label", "Next month");
21953
- next.innerHTML = iconAt("chevron-right", 14);
21954
- const head = document.createElement("div");
21955
- head.className = "vela-ind-cal-head";
21956
- head.append(prev2, title, next);
21957
- const week = document.createElement("div");
21958
- week.className = "vela-ind-cal-week";
21959
- for (const d of WEEKDAY_LABELS) {
21960
- const cell = document.createElement("span");
21961
- cell.textContent = d;
21962
- week.appendChild(cell);
21963
- }
21964
- const grid = document.createElement("div");
21965
- grid.className = "vela-ind-cal-grid";
21966
- const paint = () => {
21967
- title.textContent = `${MONTH_LABELS[month]} ${year}`;
21968
- grid.replaceChildren();
21969
- const first = new Date(year, month, 1);
21970
- const startPad = first.getDay();
21971
- const days = new Date(year, month + 1, 0).getDate();
21972
- const today = isoDate(/* @__PURE__ */ new Date());
21973
- for (let i = 0; i < startPad; i++) {
21974
- const blank = document.createElement("span");
21975
- blank.className = "vela-ind-cal-blank";
21976
- grid.appendChild(blank);
21977
- }
21978
- for (let day = 1; day <= days; day++) {
21979
- const iso = `${year}-${String(month + 1).padStart(2, "0")}-${String(day).padStart(2, "0")}`;
21980
- const b = document.createElement("button");
21981
- b.type = "button";
21982
- b.className = "vela-ind-cal-day";
21983
- b.textContent = String(day);
21984
- if (iso === selected) b.dataset.checked = "1";
21985
- if (iso === today) b.dataset.today = "1";
21986
- b.addEventListener("click", (e) => {
21987
- e.stopPropagation();
21988
- pop.hide();
21989
- onPick(iso);
21990
- });
21991
- grid.appendChild(b);
21992
- }
21993
- };
21994
- prev2.addEventListener("click", (e) => {
21995
- e.stopPropagation();
21996
- month -= 1;
21997
- if (month < 0) {
21998
- month = 11;
21999
- year -= 1;
22000
- }
22001
- paint();
22002
- });
22003
- next.addEventListener("click", (e) => {
22004
- e.stopPropagation();
22005
- month += 1;
22006
- if (month > 11) {
22007
- month = 0;
22008
- year += 1;
22009
- }
22010
- paint();
22011
- });
22012
- paint();
22013
- el.append(head, week, grid);
22014
- }
22015
+ content: (el) => fillCalendar(el, current, (iso) => {
22016
+ pop.hide();
22017
+ onPick(iso);
22018
+ })
22015
22019
  });
22016
22020
  this.calendarPop = pop;
22017
22021
  this.calendarAnchor = anchor;
@@ -22120,7 +22124,7 @@ function groupInputs(inputs) {
22120
22124
  var CALENDAR_SVG = iconAt("calendar", 14);
22121
22125
  var CLOCK_SVG = iconAt("clock", 14);
22122
22126
  var DIALOG_STYLE_ID2 = "vela-ind-dialog-styles";
22123
- var DIALOG_STYLE_REV = "29";
22127
+ var DIALOG_STYLE_REV = "30";
22124
22128
  var LEGEND_ICON_PX = 16;
22125
22129
  function ensureDialogStyles() {
22126
22130
  if (typeof document === "undefined") return;
@@ -22136,8 +22140,12 @@ function ensureDialogStyles() {
22136
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;}
22137
22141
  .vela-ind-combo-chevron:hover{opacity:0.9;}
22138
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;}
22139
22144
  .vela-ind-cal-head{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:8px;}
22140
- .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;}
22141
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;}
22142
22150
  .vela-ind-cal-nav:hover{background:var(--vela-hover);color:var(--vela-fg-bright);}
22143
22151
  .vela-ind-cal-week,.vela-ind-cal-grid{display:grid;grid-template-columns:repeat(7,28px);gap:2px;}
@@ -22148,6 +22156,12 @@ function ensureDialogStyles() {
22148
22156
  .vela-ind-cal-day:hover{background:var(--vela-hover);}
22149
22157
  .vela-ind-cal-day[data-checked]{background:var(--vela-hover-strong);color:var(--vela-fg-bright);}
22150
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;}
22151
22165
  ${overlayScrollbarCss(".vela-dialog.vela-ind-dialog *", 9)}
22152
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;}
22153
22167
  .vela-ind-tab:not(.vela-ind-tab-active):hover{color:var(--vela-fg-bright);}
@@ -22182,6 +22196,7 @@ var TIME_OPTIONS = Array.from({ length: 48 }, (_, i) => {
22182
22196
  return { value: v, label: v };
22183
22197
  });
22184
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"];
22185
22200
  var WEEKDAY_LABELS = ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"];
22186
22201
  function normalizeDateInput(raw) {
22187
22202
  const t = raw.trim();
@@ -22203,6 +22218,151 @@ function parseIsoDate(raw) {
22203
22218
  function isoDate(d) {
22204
22219
  return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
22205
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
+ }
22206
22366
  function normalizeTimeInput(raw) {
22207
22367
  const t = raw.trim();
22208
22368
  const m = /^(\d{1,2}):(\d{2})$/.exec(t) ?? /^(\d{2})(\d{2})$/.exec(t);
@@ -28690,6 +28850,10 @@ var BUILTIN_SETTINGS_IDS = [
28690
28850
  "canvas.grid",
28691
28851
  "canvas.grid.vertical",
28692
28852
  "canvas.grid.horizontal",
28853
+ "canvas.margins",
28854
+ "canvas.margins.top",
28855
+ "canvas.margins.bottom",
28856
+ "canvas.margins.right",
28693
28857
  "canvas.theme"
28694
28858
  ];
28695
28859
  function settingsIdCatalog(hostSections, markGroups = []) {
@@ -29220,6 +29384,10 @@ var SettingsDialog = class {
29220
29384
  body.append(sid(this.toggleRow("Horizontal", config.grid.horzLines.visible, (v) => this.emit({ grid: { horzLines: { visible: v } } }), [
29221
29385
  this.swatch(config.grid.horzLines.color, (v) => this.emit({ grid: { horzLines: { color: v } } }))
29222
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"));
29223
29391
  if (this.themeControl) {
29224
29392
  const tc = this.themeControl;
29225
29393
  body.append(sid(this.sectionTitle("Theme"), "canvas.theme"));
@@ -29791,23 +29959,16 @@ var SettingsDialog = class {
29791
29959
  this.hintTips.push(dispose);
29792
29960
  return el;
29793
29961
  }
29794
- numberRow(label, value, min, max, step, onChange) {
29795
- return fieldRow({
29796
- label,
29797
- labelSize: "sm",
29798
- className: "vela-sd-row",
29799
- control: buildFieldControl({
29800
- kind: "number",
29801
- value,
29802
- min,
29803
- max,
29804
- step,
29805
- fill: false,
29806
- commit: "live",
29807
- steppers: true,
29808
- onChange
29809
- }).el
29810
- });
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);
29811
29972
  }
29812
29973
  /** A dropdown whose option values differ from their display labels. */
29813
29974
  selectRowLabeled(label, value, options, onChange) {
@@ -34681,9 +34842,7 @@ function createProjector(coords, paneOf, paneIdAtY, barsInRange, seriesInRange)
34681
34842
  }
34682
34843
 
34683
34844
  // src/renderers/native/core/autoscale.ts
34684
- var MARGIN_TOP = 2 / 7;
34685
- var MARGIN_BOTTOM = 1 / 7;
34686
- 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) {
34687
34846
  let min = Infinity;
34688
34847
  let max = -Infinity;
34689
34848
  const consider = (v) => {
@@ -34718,14 +34877,17 @@ function computePaneScale(models, bars, includeCandles, i0, i1, drawings, log =
34718
34877
  const pad = Math.abs(min) * 0.1 || 1;
34719
34878
  return { min: min - pad, max: max + pad, log: log && min - pad > 0 };
34720
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;
34721
34883
  if (log && min > 0) {
34722
34884
  const lmin = Math.log(min);
34723
34885
  const lmax = Math.log(max);
34724
34886
  const lspan = lmax - lmin;
34725
- 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 };
34726
34888
  }
34727
34889
  const span = max - min;
34728
- return { min: min - span * MARGIN_BOTTOM, max: max + span * MARGIN_TOP };
34890
+ return { min: min - span * below, max: max + span * above };
34729
34891
  }
34730
34892
  function considerSeries(s, i0, i1, off, consider) {
34731
34893
  if (!seriesInScale(s)) return;
@@ -36416,6 +36578,7 @@ var NativeRenderer = class {
36416
36578
  intro: this.intro.style !== false
36417
36579
  },
36418
36580
  panes: { separatorColor: s.separatorColor ?? t.borderColor },
36581
+ margins: { ...s.margins },
36419
36582
  trades: {
36420
36583
  visible: this.scene.tradeMarkers.visible,
36421
36584
  labels: this.scene.tradeMarkers.labels,
@@ -36528,6 +36691,10 @@ var NativeRenderer = class {
36528
36691
  this.animAutoscale.toggle(next.animations.autoscale);
36529
36692
  this.intro = { style: next.animations.intro ? this.introOnStyle : false, duration: this.intro.duration || INTRO_DURATION_DEFAULT_MS };
36530
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 };
36531
36698
  this.scene.tradeMarkers = {
36532
36699
  visible: next.trades.visible,
36533
36700
  labels: next.trades.labels,
@@ -36771,7 +36938,7 @@ var NativeRenderer = class {
36771
36938
  /** Glide the view back to the most recent bars, keeping the current zoom (barSpacing). */
36772
36939
  scrollToRealtime() {
36773
36940
  if (this.coords.barCount === 0) return;
36774
- this.glideRightOffset(ZOOM_OUT_MARGIN_BARS);
36941
+ this.glideRightOffset(this.scene.style.margins.right);
36775
36942
  }
36776
36943
  /** Ease rightOffset to `target` at constant zoom (see animTick's scroll glide);
36777
36944
  * instant when the scroll glide is off. Shared by scroll-to-latest and panBy. */
@@ -38713,6 +38880,7 @@ var NativeRenderer = class {
38713
38880
  const pricePane = panes.find((p) => p.kind === "price") ?? null;
38714
38881
  this.chrome.prepare(this.scene, this.coords, this.theme);
38715
38882
  const animating = this.animator.active;
38883
+ const margins = this.scene.style.margins;
38716
38884
  for (const pane of panes) {
38717
38885
  if (pane.manualScale) {
38718
38886
  pane.scaleTarget = pane.manualScale;
@@ -38728,7 +38896,7 @@ var NativeRenderer = class {
38728
38896
  if (or) dr = dr ? { min: Math.min(dr.min, or.min), max: Math.max(dr.max, or.max) } : or;
38729
38897
  }
38730
38898
  const includeCandles = pane.kind === "price" && (!this.scene.candlesHidden || this.priceLayersAnchoredToBars(masterModels) || !this.paneHasMeasurableContent(masterModels, dr));
38731
- 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);
38732
38900
  pane.percentBaseline = pane.kind === "price" ? this.bars[i0]?.close ?? 0 : this.firstVisibleValue(masterModels, i0);
38733
38901
  pane.axisFormat = void 0;
38734
38902
  pane.axisBands = void 0;
@@ -38739,7 +38907,7 @@ var NativeRenderer = class {
38739
38907
  pane.axisFormat = "volume";
38740
38908
  }
38741
38909
  } else if (this.layerNativesOwnPane(pane, masterModels)) {
38742
- 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);
38743
38911
  pane.percentBaseline = this.bars[i0]?.close ?? 0;
38744
38912
  if (masterModels.every((m) => m.paneAxis != null)) {
38745
38913
  pane.axisFormat = "none";
@@ -38768,7 +38936,7 @@ var NativeRenderer = class {
38768
38936
  continue;
38769
38937
  }
38770
38938
  const mdr = this.chrome.paneDrawingsRange([model], this.scene, false, vr);
38771
- 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);
38772
38940
  if (!animating || !sl.initialized) {
38773
38941
  sl.scale = { ...sl.scaleTarget };
38774
38942
  sl.initialized = true;
@@ -38809,13 +38977,13 @@ var NativeRenderer = class {
38809
38977
  for (const pane of this.scene.panes.values()) pane.manualScale = null;
38810
38978
  for (const sl of this.scene.indicatorScales.values()) sl.manualScale = null;
38811
38979
  const visibleBars = Math.min(n, 200);
38812
- const rightOffset = 6;
38980
+ const rightOffset = this.scene.style.margins.right;
38813
38981
  const v = this.clampViewport(w / ((visibleBars + rightOffset) * this.coords.spacingScale), rightOffset);
38814
38982
  this.coords.setViewport(v);
38815
38983
  this.targetBarSpacing = v.barSpacing;
38816
38984
  }
38817
38985
  /** Re-frame after a series replacement (a symbol/timeframe switch): keep the user's
38818
- * 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.
38819
38987
  * `clampViewport`'s fit-all-bars floor deliberately does NOT apply — a progressive
38820
38988
  * head may still be backfilling toward the previous depth, and raising the spacing
38821
38989
  * to its temporary bar count would lose the zoom this exists to keep. */
@@ -38824,7 +38992,7 @@ var NativeRenderer = class {
38824
38992
  this.panVelocity = 0;
38825
38993
  for (const pane of this.scene.panes.values()) pane.manualScale = null;
38826
38994
  for (const sl of this.scene.indicatorScales.values()) sl.manualScale = null;
38827
- 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 };
38828
38996
  this.coords.setViewport(v);
38829
38997
  this.targetBarSpacing = v.barSpacing;
38830
38998
  }
@@ -41909,6 +42077,7 @@ function registerClassicIndicators() {
41909
42077
  }
41910
42078
 
41911
42079
  // src/Vela.ts
42080
+ var asError = (err) => err instanceof Error ? err : new Error(String(err));
41912
42081
  var Vela = class {
41913
42082
  constructor(container, options = {}, deps = {}) {
41914
42083
  registerBuiltinChartTypes();
@@ -42032,7 +42201,12 @@ var Vela = class {
42032
42201
  * fetch from — the same relationship `script:run` has to `context:changed`.
42033
42202
  */
42034
42203
  runScript(source, options) {
42035
- const handle = this.addIndicator(source, options);
42204
+ let handle;
42205
+ try {
42206
+ handle = this.addIndicator(source, options);
42207
+ } catch (err) {
42208
+ return Promise.resolve({ ok: false, run: null, error: asError(err), onUpdate: () => () => void 0, remove: () => void 0 });
42209
+ }
42036
42210
  const updates = /* @__PURE__ */ new Set();
42037
42211
  const drop = () => {
42038
42212
  try {
@@ -42076,7 +42250,12 @@ var Vela = class {
42076
42250
  });
42077
42251
  }
42078
42252
  runIndicator(source, options) {
42079
- const handle = this.addIndicator(source, options);
42253
+ let handle;
42254
+ try {
42255
+ handle = this.addIndicator(source, options);
42256
+ } catch (err) {
42257
+ return Promise.resolve({ ok: false, handle: null, error: asError(err), context: null });
42258
+ }
42080
42259
  return new Promise((resolve) => {
42081
42260
  const offReady = handle.on("ready", () => {
42082
42261
  offReady();
@@ -43425,7 +43604,7 @@ var ChartContextMenu = class {
43425
43604
  } else if (id.startsWith("tz:")) {
43426
43605
  const zone = id.slice("tz:".length);
43427
43606
  if (this.cbs.setTimezone) this.cbs.setTimezone(zone);
43428
- else chart.renderer.set("timezone", zone);
43607
+ else chart.renderer.set("timezone", resolveTimezone(zone, void 0));
43429
43608
  } else if (id === "auto") {
43430
43609
  chart.renderer.set("autoScale", chart.renderer.get("autoScale") === false);
43431
43610
  } else if (id === "invert") {
@@ -43658,7 +43837,7 @@ var ChartCell = class {
43658
43837
  this.history.onChart(this.inner);
43659
43838
  this.inner.renderer.onConfigChanged(() => {
43660
43839
  const zone = this.inner?.renderer.get("timezone");
43661
- if (typeof zone === "string" && normalizeTimezone(zone) !== normalizeTimezone(this.deps.timezone())) {
43840
+ if (typeof zone === "string" && normalizeTimezone(zone) !== normalizeTimezone(this.displayTimezone)) {
43662
43841
  this.deps.setTimezone(normalizeTimezone(zone));
43663
43842
  }
43664
43843
  const style = this.priceStyle;
@@ -43700,8 +43879,7 @@ var ChartCell = class {
43700
43879
  this.refreshSessionShading();
43701
43880
  });
43702
43881
  this.inner.on("viewport:changed", (range) => this.sessionShading.updateRange(range));
43703
- const tz = deps.timezone();
43704
- if (tz !== "Etc/UTC") this.inner.renderer.set("timezone", tz);
43882
+ this.applyTimezone();
43705
43883
  this.indicatorTitlesOn = seed.indicatorTitles ?? true;
43706
43884
  if (!this.indicatorTitlesOn) this.inner.renderer.set("indicatorTitles", false);
43707
43885
  this.indicatorValuesOn = seed.indicatorValues ?? true;
@@ -43723,7 +43901,7 @@ var ChartCell = class {
43723
43901
  this.statusline?.setSymbol(this.state.symbol);
43724
43902
  this.statusline?.setMeta(this.state.timeframe ?? "60", this.inner.data.displayPrefix(this.state.symbol) ?? this.state.provider ?? "");
43725
43903
  }
43726
- this.refreshSessionAvailable();
43904
+ this.refreshSymbolMetadata();
43727
43905
  if (this.inner && this.state.symbol) this.marketStatus?.track(this.inner.data, this.state.symbol);
43728
43906
  });
43729
43907
  this.syncStatuslineColors();
@@ -43797,7 +43975,7 @@ var ChartCell = class {
43797
43975
  this.offMarket = this.inner.on("market:changed", ({ symbol: symbol2, timeframe }) => {
43798
43976
  this.projectMarket(symbol2, timeframe);
43799
43977
  this.refreshNativeCatalog();
43800
- this.refreshSessionAvailable();
43978
+ this.refreshSymbolMetadata();
43801
43979
  if (this.inner) this.marketStatus?.track(this.inner.data, symbol2);
43802
43980
  this.deps.onMarketChanged(this.id);
43803
43981
  });
@@ -43838,7 +44016,35 @@ var ChartCell = class {
43838
44016
  this.deps.onStateDirty();
43839
44017
  void this.inner?.setMarket({ session });
43840
44018
  }
43841
- 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() {
43842
44048
  const chart = this.inner;
43843
44049
  const symbol = this.state.symbol;
43844
44050
  if (!chart || !symbol) return;
@@ -43846,7 +44052,13 @@ var ChartCell = class {
43846
44052
  if (this.inner !== chart) return;
43847
44053
  const available = typeof si?.session === "string" && si.session !== "" && si.session !== "24x7";
43848
44054
  const overnight = parseSessionSpec(si)?.overnight === true;
43849
- 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) {
43850
44062
  this.sessionAvailableFlag = available;
43851
44063
  this.sessionOvernightFlag = overnight;
43852
44064
  this.deps.onMarketChanged(this.id);
@@ -44305,16 +44517,19 @@ var ChartCell = class {
44305
44517
  * a persistence handler's `restore` runs silently, a user-driven call records.
44306
44518
  */
44307
44519
  addExternalIndicator(entry) {
44520
+ const { id, inputs, props, hidden, ...script } = entry;
44308
44521
  this.addManifestInstance(
44309
- { ...entry, enabled: true },
44310
- { external: true, ...entry.inputs ? { inputs: entry.inputs } : {}, ...entry.props ? { props: entry.props } : {}, ...entry.hidden ? { hidden: true } : {} }
44522
+ { ...script, enabled: true },
44523
+ { external: true, ...id !== void 0 ? { id } : {}, ...inputs ? { inputs } : {}, ...props ? { props } : {}, ...hidden ? { hidden: true } : {} }
44311
44524
  );
44312
44525
  }
44313
44526
  /** Add ONE instance of a manifest entry (repeatable — duplicates are legitimate). */
44314
44527
  addManifestInstance(entry, opts = {}) {
44315
44528
  if (this.destroyed) return;
44316
44529
  const values = opts.inputs || opts.props ? { inputs: opts.inputs, props: opts.props } : void 0;
44317
- const it = { entry, handle: this.addToChart(entry, values), ...opts.external ? { external: true } : {}, ...values ? { values } : {} };
44530
+ const handle = this.addToChart(entry, values, opts.id);
44531
+ if (!handle) return;
44532
+ const it = { entry, handle, id: handle.id, ...opts.external ? { external: true } : {}, ...values ? { values } : {} };
44318
44533
  if (opts.hidden) it.handle?.setVisible(false);
44319
44534
  this.instances.push(it);
44320
44535
  this.deps.onIndicatorsChanged(this.id);
@@ -44323,7 +44538,7 @@ var ChartCell = class {
44323
44538
  this.history.push({
44324
44539
  undo: () => this.dropInstance(snapshot),
44325
44540
  redo: () => {
44326
- snapshot.handle = this.addToChart(snapshot.entry, snapshot.values);
44541
+ snapshot.handle = this.addToChart(snapshot.entry, snapshot.values, snapshot.id);
44327
44542
  this.instances.push(snapshot);
44328
44543
  this.deps.onIndicatorsChanged(this.id);
44329
44544
  }
@@ -44336,7 +44551,7 @@ var ChartCell = class {
44336
44551
  const snapshot = it;
44337
44552
  this.history.push({
44338
44553
  undo: () => {
44339
- snapshot.handle = this.addToChart(snapshot.entry, snapshot.values);
44554
+ snapshot.handle = this.addToChart(snapshot.entry, snapshot.values, snapshot.id);
44340
44555
  this.instances.push(snapshot);
44341
44556
  this.deps.onIndicatorsChanged(this.id);
44342
44557
  },
@@ -44426,9 +44641,10 @@ var ChartCell = class {
44426
44641
  this.deps.onIndicatorsChanged(this.id);
44427
44642
  });
44428
44643
  }
44429
- addToChart(entry, values) {
44644
+ addToChart(entry, values, id) {
44430
44645
  try {
44431
44646
  return this.inner?.addIndicator(entry.script, {
44647
+ ...id !== void 0 ? { id } : {},
44432
44648
  ...entry.language !== void 0 ? { language: entry.language } : {},
44433
44649
  ...values?.inputs ? { inputs: values.inputs } : {},
44434
44650
  ...values?.props ? { props: values.props } : {}
@@ -45618,13 +45834,23 @@ var VelaWorkspace = class {
45618
45834
  }
45619
45835
  }
45620
45836
  }
45621
- /** 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
+ */
45622
45843
  setTimezone(zone) {
45623
45844
  this.timezone = zone;
45624
- this.bottombar?.setTimezone(zone);
45625
- 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();
45626
45847
  this.markStateDirty();
45627
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
+ }
45628
45854
  /**
45629
45855
  * Swap the workspace theme at runtime — `'dark'`, `'light'`, or a full custom theme,
45630
45856
  * applied to the shared chrome (topbar, panels, drawing toolbar) and EVERY cell.
@@ -45829,6 +46055,7 @@ var VelaWorkspace = class {
45829
46055
  this.dock.onChart(cell.chart);
45830
46056
  this.bottombar?.setActiveRange(cell.activeRangeId);
45831
46057
  this.bottombar?.setSession({ session: cell.session, enabled: cell.sessionAvailable });
46058
+ this.projectTimezone();
45832
46059
  this.indicatorPicker?.sync();
45833
46060
  this.glider.stop();
45834
46061
  const d = cell.chart.drawings;
@@ -46413,6 +46640,7 @@ var VelaWorkspace = class {
46413
46640
  this.mobileBar?.setTimeframe(cell.timeframe);
46414
46641
  this.objectTree.setSymbol(cell.symbol);
46415
46642
  this.bottombar?.setSession({ session: cell.session, enabled: cell.sessionAvailable });
46643
+ this.projectTimezone();
46416
46644
  this.bottombar?.setActiveRange(cell.activeRangeId);
46417
46645
  }
46418
46646
  /** Trigger ② — a cell's price style changed: the topbar button/menu only if active.