@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
package/dist/widget.cjs CHANGED
@@ -3521,7 +3521,8 @@ function addRecent(hex6) {
3521
3521
  recents.unshift(hex6);
3522
3522
  if (recents.length > 10) recents.length = 10;
3523
3523
  }
3524
- function buildColorPicker(color, theme, onChange) {
3524
+ function buildColorPicker(color, theme, onChange, options = {}) {
3525
+ const commit = options.commit ?? "live";
3525
3526
  const parsed = splitColor(color);
3526
3527
  let curHex = parsed.hex6;
3527
3528
  let curAlpha = parsed.alpha;
@@ -3604,7 +3605,12 @@ function buildColorPicker(color, theme, onChange) {
3604
3605
  const r = track.getBoundingClientRect();
3605
3606
  curAlpha = Math.max(0, Math.min(1, (clientX - r.left) / r.width));
3606
3607
  paintOpacity();
3607
- emit();
3608
+ if (commit === "live") emit();
3609
+ };
3610
+ const endDrag = () => {
3611
+ if (!dragging) return;
3612
+ dragging = false;
3613
+ if (commit === "release") emit();
3608
3614
  };
3609
3615
  track.addEventListener("pointerdown", (e) => {
3610
3616
  e.stopPropagation();
@@ -3615,7 +3621,8 @@ function buildColorPicker(color, theme, onChange) {
3615
3621
  track.addEventListener("pointermove", (e) => {
3616
3622
  if (dragging) onDrag(e.clientX);
3617
3623
  });
3618
- track.addEventListener("pointerup", () => dragging = false);
3624
+ track.addEventListener("pointerup", endDrag);
3625
+ track.addEventListener("pointercancel", endDrag);
3619
3626
  function emit() {
3620
3627
  onChange(combineColor(curHex, curAlpha));
3621
3628
  }
@@ -3638,7 +3645,7 @@ function buildColorPicker(color, theme, onChange) {
3638
3645
  return root;
3639
3646
  }
3640
3647
  function colorField(theme, getVal, onVal, opts) {
3641
- return new ColorField({ theme, getVal, onVal, shape: opts?.shape, id: opts?.id, popover: opts?.popover }).el;
3648
+ return new ColorField({ theme, getVal, onVal, shape: opts?.shape, id: opts?.id, popover: opts?.popover, commit: opts?.commit }).el;
3642
3649
  }
3643
3650
  var ColorField = class {
3644
3651
  constructor(opts) {
@@ -3647,6 +3654,7 @@ var ColorField = class {
3647
3654
  this.getVal = opts.getVal;
3648
3655
  this.onVal = opts.onVal;
3649
3656
  this.popoverOpts = opts.popover;
3657
+ this.commit = opts.commit;
3650
3658
  const trigger = document.createElement("button");
3651
3659
  trigger.type = "button";
3652
3660
  if (opts.id) trigger.id = opts.id;
@@ -3686,10 +3694,15 @@ var ColorField = class {
3686
3694
  boundary: this.popoverOpts?.boundary,
3687
3695
  zIndex: this.popoverOpts?.zIndex,
3688
3696
  className: "vela-color-field-pop",
3689
- content: buildColorPicker(this.getVal(), this.theme, (val) => {
3690
- this.onVal(val);
3691
- this.paint();
3692
- })
3697
+ content: buildColorPicker(
3698
+ this.getVal(),
3699
+ this.theme,
3700
+ (val) => {
3701
+ this.onVal(val);
3702
+ this.paint();
3703
+ },
3704
+ { commit: this.commit }
3705
+ )
3693
3706
  });
3694
3707
  pop.show();
3695
3708
  }
@@ -4277,7 +4290,8 @@ function buildFieldControl(desc) {
4277
4290
  const el2 = colorField(desc.theme, desc.get, desc.onChange, {
4278
4291
  shape: "circle",
4279
4292
  id: desc.id,
4280
- popover: desc.popover
4293
+ popover: desc.popover,
4294
+ commit: desc.commit
4281
4295
  });
4282
4296
  if (desc.title) el2.title = desc.title;
4283
4297
  return { el: el2 };
@@ -5070,10 +5084,12 @@ var BASELINE_LEVEL_DEFAULT = 50;
5070
5084
  var PREMARKET_SHADE = withAlpha(WARNING, 0.08);
5071
5085
  var POSTMARKET_SHADE = withAlpha(ACCENT, 0.08);
5072
5086
  var EXTENDED_SHADE = POSTMARKET_SHADE;
5087
+ var DEFAULT_MARGINS = { top: 10, bottom: 10, right: 10 };
5073
5088
  function defaultChartStyle() {
5074
5089
  return {
5075
5090
  chartTypes: {},
5076
5091
  fontSize: 11,
5092
+ margins: { ...DEFAULT_MARGINS },
5077
5093
  gridVert: { visible: true, color: null },
5078
5094
  gridHorz: { visible: true, color: null },
5079
5095
  borderColor: null,
@@ -5197,6 +5213,12 @@ function clampLevel(v) {
5197
5213
  function clampSpacing(v) {
5198
5214
  return v < 0.1 ? 0.1 : v > 10 ? 10 : v;
5199
5215
  }
5216
+ function clampMarginPct(v) {
5217
+ return v < 0 ? 0 : v > 40 ? 40 : v;
5218
+ }
5219
+ function clampMarginBars(v) {
5220
+ return Math.round(v < 0 ? 0 : v > 200 ? 200 : v);
5221
+ }
5200
5222
  function factoryResetConfig(factory, priceStyle = factory.series.style) {
5201
5223
  const bag = {};
5202
5224
  for (const t of chartTypes()) {
@@ -5245,6 +5267,7 @@ function mergeConfig(base, patch) {
5245
5267
  const ps = asObject(p.priceScale);
5246
5268
  const anim = asObject(p.animations);
5247
5269
  const panes = asObject(p.panes);
5270
+ const margins = asObject(p.margins);
5248
5271
  const trades = asObject(p.trades);
5249
5272
  const ts = asObject(p.timeScale);
5250
5273
  const marks = asObject(p.marks);
@@ -5304,6 +5327,11 @@ function mergeConfig(base, patch) {
5304
5327
  panes: {
5305
5328
  separatorColor: isColor(panes.separatorColor) ? panes.separatorColor : base.panes.separatorColor
5306
5329
  },
5330
+ margins: {
5331
+ top: isNum(margins.top) ? clampMarginPct(margins.top) : base.margins.top,
5332
+ bottom: isNum(margins.bottom) ? clampMarginPct(margins.bottom) : base.margins.bottom,
5333
+ right: isNum(margins.right) ? clampMarginBars(margins.right) : base.margins.right
5334
+ },
5307
5335
  trades: {
5308
5336
  visible: isBool(trades.visible) ? trades.visible : base.trades.visible,
5309
5337
  labels: isBool(trades.labels) ? trades.labels : base.trades.labels,
@@ -6125,9 +6153,22 @@ var TIMEZONES = [
6125
6153
  { value: "Pacific/Apia", label: "Apia" },
6126
6154
  { value: "Pacific/Kiritimati", label: "Kiritimati" }
6127
6155
  ];
6156
+ var EXCHANGE_TIMEZONE = "exchange";
6157
+ function isExchangeTimezone(zone) {
6158
+ return zone === EXCHANGE_TIMEZONE;
6159
+ }
6160
+ function resolveTimezone(zone, exchangeZone) {
6161
+ if (!isExchangeTimezone(zone)) return zone;
6162
+ return exchangeZone && exchangeZone !== "" ? exchangeZone : "Etc/UTC";
6163
+ }
6128
6164
  function normalizeTimezone(zone) {
6129
6165
  return zone === "UTC" || zone === "Etc/UTC" || zone === "Etc/GMT" ? "Etc/UTC" : zone;
6130
6166
  }
6167
+ function timezoneMenuRows(current) {
6168
+ const active = normalizeTimezone(current);
6169
+ const [utc, ...zones] = TIMEZONES.map((t) => ({ value: t.value, label: tzMenuLabel(t.value, t.label), checked: t.value === active }));
6170
+ return [utc, { value: EXCHANGE_TIMEZONE, label: "Exchange", checked: isExchangeTimezone(current) }, ...zones];
6171
+ }
6131
6172
  function tzOffset(zone, date = /* @__PURE__ */ new Date()) {
6132
6173
  try {
6133
6174
  const parts = new Intl.DateTimeFormat("en-US", { timeZone: zone, timeZoneName: "shortOffset" }).formatToParts(date);
@@ -6250,6 +6291,7 @@ var Bottombar = class {
6250
6291
  this.sessionButtons = /* @__PURE__ */ new Map();
6251
6292
  this.sessionEl = null;
6252
6293
  this.timezone = opts.timezone;
6294
+ this.exchangeTimezone = opts.exchangeTimezone;
6253
6295
  const doc = host.ownerDocument;
6254
6296
  injectStyles(STYLE_ID3, CSS4, doc);
6255
6297
  this.el = doc.createElement("div");
@@ -6273,7 +6315,7 @@ var Bottombar = class {
6273
6315
  this.clockEl = doc.createElement("span");
6274
6316
  this.clockEl.className = "vela-bb-clock";
6275
6317
  this.tzLabelEl = doc.createElement("span");
6276
- this.tzLabelEl.textContent = tzButtonLabel(this.timezone);
6318
+ this.tzLabelEl.textContent = tzButtonLabel(this.displayZone);
6277
6319
  this.tzButton.append(this.clockEl, this.tzLabelEl);
6278
6320
  const session = doc.createElement("span");
6279
6321
  session.className = "vela-bb-session";
@@ -6308,19 +6350,32 @@ var Bottombar = class {
6308
6350
  placement: "top-end",
6309
6351
  items: this.tzItems(),
6310
6352
  onSelect: (zone) => {
6311
- this.setTimezone(zone);
6353
+ this.setTimezone(zone, this.exchangeTimezone);
6312
6354
  opts.onTimezone(zone);
6313
6355
  }
6314
6356
  });
6315
6357
  this.tick();
6316
6358
  this.unsubClock = (opts.clock ?? new SecondClock()).onTick(() => this.tick());
6317
6359
  }
6318
- setTimezone(zone) {
6360
+ /**
6361
+ * Reflect the stored choice AND the active chart's market zone. The clock and the
6362
+ * offset label read the RESOLVED zone, so a workspace on the exchange rule re-labels
6363
+ * when the active cell (or its symbol) changes market — the host re-projects on
6364
+ * both. Idempotent: unchanged inputs leave the menu alone.
6365
+ */
6366
+ setTimezone(zone, exchangeTimezone) {
6367
+ if (zone === this.timezone && exchangeTimezone === this.exchangeTimezone) return;
6368
+ const choiceChanged = zone !== this.timezone;
6319
6369
  this.timezone = zone;
6320
- this.tzLabelEl.textContent = tzButtonLabel(zone);
6321
- this.tzMenu.setItems(this.tzItems());
6370
+ this.exchangeTimezone = exchangeTimezone;
6371
+ this.tzLabelEl.textContent = tzButtonLabel(this.displayZone);
6372
+ if (choiceChanged) this.tzMenu.setItems(this.tzItems());
6322
6373
  this.tick();
6323
6374
  }
6375
+ /** The zone the bar's clock and label render in. */
6376
+ get displayZone() {
6377
+ return resolveTimezone(this.timezone, this.exchangeTimezone);
6378
+ }
6324
6379
  /** Highlight (or clear with null) the active range chip — cleared on manual tf changes. */
6325
6380
  setActiveRange(id) {
6326
6381
  for (const [key, b] of this.rangeButtons) {
@@ -6348,11 +6403,7 @@ var Bottombar = class {
6348
6403
  this.el.remove();
6349
6404
  }
6350
6405
  tzItems() {
6351
- return TIMEZONES.map((t) => ({
6352
- id: t.value,
6353
- label: tzMenuLabel(t.value, t.label),
6354
- checked: t.value === normalizeTimezone(this.timezone)
6355
- }));
6406
+ return timezoneMenuRows(this.timezone).map((r) => ({ id: r.value, label: r.label, checked: r.checked }));
6356
6407
  }
6357
6408
  tick() {
6358
6409
  try {
@@ -6361,7 +6412,7 @@ var Bottombar = class {
6361
6412
  minute: "2-digit",
6362
6413
  second: "2-digit",
6363
6414
  hour12: false,
6364
- timeZone: this.timezone
6415
+ timeZone: this.displayZone
6365
6416
  }).format(/* @__PURE__ */ new Date());
6366
6417
  } catch {
6367
6418
  this.clockEl.textContent = "";
@@ -16011,15 +16062,14 @@ var TimezoneDrawer = class {
16011
16062
  this.drawer.body.replaceChildren();
16012
16063
  const list = doc.createElement("div");
16013
16064
  list.className = "vela-tzd-list";
16014
- const current = normalizeTimezone(this.opts.timezone());
16015
- for (const tz of TIMEZONES) {
16065
+ for (const tz of timezoneMenuRows(this.opts.timezone())) {
16016
16066
  const row = doc.createElement("div");
16017
16067
  row.className = "vela-tzd-row";
16018
16068
  const label = doc.createElement("span");
16019
16069
  label.className = "vela-tzd-row-label";
16020
- label.textContent = tzMenuLabel(tz.value, tz.label);
16070
+ label.textContent = tz.label;
16021
16071
  row.appendChild(label);
16022
- if (tz.value === current) row.appendChild(iconEl("check", doc));
16072
+ if (tz.checked) row.appendChild(iconEl("check", doc));
16023
16073
  row.addEventListener("click", () => {
16024
16074
  this.opts.onTimezone(tz.value);
16025
16075
  this.drawer.hide();
@@ -16106,12 +16156,11 @@ function priceAxisItems(s) {
16106
16156
  ];
16107
16157
  }
16108
16158
  function timeAxisItems(timezone) {
16109
- const active = timezone === "UTC" ? "Etc/UTC" : timezone;
16110
16159
  return [
16111
16160
  {
16112
16161
  id: "timezone",
16113
16162
  label: "Time zone",
16114
- submenu: TIMEZONES.map((t) => ({ id: `tz:${t.value}`, label: tzMenuLabel(t.value, t.label), checked: t.value === active }))
16163
+ submenu: timezoneMenuRows(timezone).map((r) => ({ id: `tz:${r.value}`, label: r.label, checked: r.checked }))
16115
16164
  },
16116
16165
  settingsItem("time-axis", "More settings\u2026")
16117
16166
  ];
@@ -18460,10 +18509,18 @@ var IndicatorRegistry = class {
18460
18509
  this.records = /* @__PURE__ */ new Map();
18461
18510
  this.counter = 0;
18462
18511
  }
18463
- /** Allocate a unique, stable per-instance id. */
18464
- nextId(prefix = "ind") {
18465
- this.counter += 1;
18466
- return `${prefix}-${this.counter}`;
18512
+ /**
18513
+ * Mint a unique per-instance id. Host-supplied ids share the namespace, so a minted
18514
+ * id skips anything already recorded — and anything `taken` reports live elsewhere
18515
+ * (a handle the orchestrator holds outside the records, e.g. a fail-soft native).
18516
+ */
18517
+ nextId(prefix = "ind", taken = () => false) {
18518
+ let id;
18519
+ do {
18520
+ this.counter += 1;
18521
+ id = `${prefix}-${this.counter}`;
18522
+ } while (this.records.has(id) || taken(id));
18523
+ return id;
18467
18524
  }
18468
18525
  add(record) {
18469
18526
  this.records.set(record.id, record);
@@ -19846,7 +19903,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
19846
19903
  this.engines.set(language, engine);
19847
19904
  }
19848
19905
  addIndicator(source, options = {}) {
19849
- const id = this.registry.nextId();
19906
+ const id = this.claimIndicatorId(options.id);
19850
19907
  const title = options.title ?? "Indicator";
19851
19908
  const handle = new IndicatorHandleImpl(id, title, this, source);
19852
19909
  this.handles.set(id, handle);
@@ -19868,7 +19925,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
19868
19925
  const existing = this.registry.all().find((r) => r.native?.type === type);
19869
19926
  if (existing) return this.handles.get(existing.id) ?? new IndicatorHandleImpl(existing.id, existing.title, this, void 0, type);
19870
19927
  }
19871
- const id = this.registry.nextId("native");
19928
+ const id = this.registry.nextId("native", (candidate) => this.handles.has(candidate));
19872
19929
  const title = descriptor?.title ?? type;
19873
19930
  const handle = new IndicatorHandleImpl(id, title, this, void 0, type);
19874
19931
  this.handles.set(id, handle);
@@ -20146,6 +20203,24 @@ var EngineOrchestrator = class _EngineOrchestrator {
20146
20203
  this.events.clear();
20147
20204
  }
20148
20205
  // ── internals ───────────────────────────────────────────────
20206
+ /**
20207
+ * The id a new script indicator runs under: the host's when supplied, else a minted
20208
+ * one. A host id is opaque but must be a non-empty string, and it must not be live
20209
+ * on this chart — a duplicate is a programming error surfaced synchronously, never
20210
+ * renamed behind the caller's back (the whole point of supplying one is that
20211
+ * `handle.id` equals what was passed). "Live" reads both the records and the handle
20212
+ * map: a fail-soft handle never enters the registry but still owns its id.
20213
+ */
20214
+ claimIndicatorId(requested) {
20215
+ if (requested === void 0) return this.registry.nextId("ind", (candidate) => this.handles.has(candidate));
20216
+ if (typeof requested !== "string" || requested.length === 0) {
20217
+ throw new TypeError("[vela] addIndicator: `id` must be a non-empty string");
20218
+ }
20219
+ if (this.handles.has(requested) || this.registry.get(requested)) {
20220
+ throw new Error(`[vela] addIndicator: indicator id "${requested}" is already live on this chart`);
20221
+ }
20222
+ return requested;
20223
+ }
20149
20224
  async startIndicator(id, source, options, handle) {
20150
20225
  try {
20151
20226
  await this.readyPromise;
@@ -21744,7 +21819,10 @@ var IndicatorInputsDialog = class {
21744
21819
  id,
21745
21820
  theme: this.host.theme(),
21746
21821
  get: () => String(bagOf(row, inp)[inp.key] ?? inp.defval),
21747
- onChange: (v) => emit(v)
21822
+ onChange: (v) => emit(v),
21823
+ // An input change re-executes the script over its whole history: commit the
21824
+ // opacity drag once on release, not once per pointer move.
21825
+ commit: "release"
21748
21826
  }).el;
21749
21827
  }
21750
21828
  if (inp.type === "symbol") return this.buildSymbol(id, String(current), emit);
@@ -21991,10 +22069,6 @@ var IndicatorInputsDialog = class {
21991
22069
  }
21992
22070
  /** Open a themed month calendar under `anchor` — same surface + shadow as the choice list. */
21993
22071
  openCalendar(anchor, current, onPick) {
21994
- const parsed = parseIsoDate(current);
21995
- let year = parsed?.getFullYear() ?? (/* @__PURE__ */ new Date()).getFullYear();
21996
- let month = parsed?.getMonth() ?? (/* @__PURE__ */ new Date()).getMonth();
21997
- const selected = parsed ? isoDate(parsed) : current;
21998
22072
  const pop = new Popover({
21999
22073
  trigger: anchor,
22000
22074
  theme: this.host.theme(),
@@ -22006,80 +22080,10 @@ var IndicatorInputsDialog = class {
22006
22080
  this.calendarPop = null;
22007
22081
  this.calendarAnchor = null;
22008
22082
  },
22009
- content: (el) => {
22010
- const title = document.createElement("div");
22011
- title.className = "vela-ind-cal-title";
22012
- const prev2 = document.createElement("button");
22013
- prev2.type = "button";
22014
- prev2.className = "vela-ind-cal-nav";
22015
- prev2.setAttribute("aria-label", "Previous month");
22016
- prev2.innerHTML = iconAt("chevron-left", 14);
22017
- const next = document.createElement("button");
22018
- next.type = "button";
22019
- next.className = "vela-ind-cal-nav";
22020
- next.setAttribute("aria-label", "Next month");
22021
- next.innerHTML = iconAt("chevron-right", 14);
22022
- const head = document.createElement("div");
22023
- head.className = "vela-ind-cal-head";
22024
- head.append(prev2, title, next);
22025
- const week = document.createElement("div");
22026
- week.className = "vela-ind-cal-week";
22027
- for (const d of WEEKDAY_LABELS) {
22028
- const cell = document.createElement("span");
22029
- cell.textContent = d;
22030
- week.appendChild(cell);
22031
- }
22032
- const grid = document.createElement("div");
22033
- grid.className = "vela-ind-cal-grid";
22034
- const paint = () => {
22035
- title.textContent = `${MONTH_LABELS[month]} ${year}`;
22036
- grid.replaceChildren();
22037
- const first = new Date(year, month, 1);
22038
- const startPad = first.getDay();
22039
- const days = new Date(year, month + 1, 0).getDate();
22040
- const today = isoDate(/* @__PURE__ */ new Date());
22041
- for (let i = 0; i < startPad; i++) {
22042
- const blank = document.createElement("span");
22043
- blank.className = "vela-ind-cal-blank";
22044
- grid.appendChild(blank);
22045
- }
22046
- for (let day = 1; day <= days; day++) {
22047
- const iso = `${year}-${String(month + 1).padStart(2, "0")}-${String(day).padStart(2, "0")}`;
22048
- const b = document.createElement("button");
22049
- b.type = "button";
22050
- b.className = "vela-ind-cal-day";
22051
- b.textContent = String(day);
22052
- if (iso === selected) b.dataset.checked = "1";
22053
- if (iso === today) b.dataset.today = "1";
22054
- b.addEventListener("click", (e) => {
22055
- e.stopPropagation();
22056
- pop.hide();
22057
- onPick(iso);
22058
- });
22059
- grid.appendChild(b);
22060
- }
22061
- };
22062
- prev2.addEventListener("click", (e) => {
22063
- e.stopPropagation();
22064
- month -= 1;
22065
- if (month < 0) {
22066
- month = 11;
22067
- year -= 1;
22068
- }
22069
- paint();
22070
- });
22071
- next.addEventListener("click", (e) => {
22072
- e.stopPropagation();
22073
- month += 1;
22074
- if (month > 11) {
22075
- month = 0;
22076
- year += 1;
22077
- }
22078
- paint();
22079
- });
22080
- paint();
22081
- el.append(head, week, grid);
22082
- }
22083
+ content: (el) => fillCalendar(el, current, (iso) => {
22084
+ pop.hide();
22085
+ onPick(iso);
22086
+ })
22083
22087
  });
22084
22088
  this.calendarPop = pop;
22085
22089
  this.calendarAnchor = anchor;
@@ -22188,7 +22192,7 @@ function groupInputs(inputs) {
22188
22192
  var CALENDAR_SVG = iconAt("calendar", 14);
22189
22193
  var CLOCK_SVG = iconAt("clock", 14);
22190
22194
  var DIALOG_STYLE_ID2 = "vela-ind-dialog-styles";
22191
- var DIALOG_STYLE_REV = "29";
22195
+ var DIALOG_STYLE_REV = "30";
22192
22196
  var LEGEND_ICON_PX = 16;
22193
22197
  function ensureDialogStyles() {
22194
22198
  if (typeof document === "undefined") return;
@@ -22204,8 +22208,12 @@ function ensureDialogStyles() {
22204
22208
  .vela-ind-combo-chevron{position:absolute;right:0;top:0;bottom:0;width:26px;border:none;background:transparent;color:inherit;opacity:0.55;cursor:pointer;display:flex;align-items:center;justify-content:center;padding:0;}
22205
22209
  .vela-ind-combo-chevron:hover{opacity:0.9;}
22206
22210
  .vela-ind-cal{background:var(--vela-bg);color:var(--vela-fg);border:none;border-radius:6px;box-shadow:var(--vela-shadow);font:14px var(--vela-font);padding:10px 12px;user-select:none;}
22211
+ .vela-ind-cal [hidden]{display:none !important;}
22207
22212
  .vela-ind-cal-head{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:8px;}
22208
- .vela-ind-cal-title{flex:1;text-align:center;font-weight:600;font-size:14px;color:var(--vela-fg-bright);}
22213
+ .vela-ind-cal-title{flex:1;display:flex;align-items:center;justify-content:center;gap:2px;min-width:0;}
22214
+ .vela-ind-cal-switch{border:none;background:transparent;color:var(--vela-fg-bright);font:inherit;font-weight:600;font-size:14px;padding:2px 6px;border-radius:4px;cursor:pointer;}
22215
+ .vela-ind-cal-switch:not(:disabled):hover{background:var(--vela-hover);}
22216
+ .vela-ind-cal-switch:disabled{cursor:default;}
22209
22217
  .vela-ind-cal-nav{width:24px;height:24px;border:none;background:transparent;color:var(--vela-fg-muted);border-radius:4px;padding:0;cursor:pointer;display:inline-flex;align-items:center;justify-content:center;}
22210
22218
  .vela-ind-cal-nav:hover{background:var(--vela-hover);color:var(--vela-fg-bright);}
22211
22219
  .vela-ind-cal-week,.vela-ind-cal-grid{display:grid;grid-template-columns:repeat(7,28px);gap:2px;}
@@ -22216,6 +22224,12 @@ function ensureDialogStyles() {
22216
22224
  .vela-ind-cal-day:hover{background:var(--vela-hover);}
22217
22225
  .vela-ind-cal-day[data-checked]{background:var(--vela-hover-strong);color:var(--vela-fg-bright);}
22218
22226
  .vela-ind-cal-day[data-today]:not([data-checked]){box-shadow:inset 0 0 0 1px var(--vela-border-strong);}
22227
+ .vela-ind-cal-cells{display:grid;grid-template-columns:repeat(3,1fr);gap:4px;width:calc(7 * 28px + 6 * 2px);}
22228
+ .vela-ind-cal-cell{height:36px;border:none;background:transparent;color:inherit;border-radius:4px;padding:0 4px;cursor:pointer;font:inherit;font-size:14px;}
22229
+ .vela-ind-cal-cell:hover{background:var(--vela-hover);}
22230
+ .vela-ind-cal-cell[data-checked]{background:var(--vela-hover-strong);color:var(--vela-fg-bright);}
22231
+ .vela-ind-cal-cell[data-today]:not([data-checked]){box-shadow:inset 0 0 0 1px var(--vela-border-strong);}
22232
+ .vela-ind-cal-cell[data-outside]{opacity:0.45;}
22219
22233
  ${overlayScrollbarCss(".vela-dialog.vela-ind-dialog *", 9)}
22220
22234
  .vela-ind-tab{font-weight:600;font-size:13px;line-height:20px;transition:color var(--vela-dur-fast) ease,border-color var(--vela-dur-fast) ease;}
22221
22235
  .vela-ind-tab:not(.vela-ind-tab-active):hover{color:var(--vela-fg-bright);}
@@ -22250,6 +22264,7 @@ var TIME_OPTIONS = Array.from({ length: 48 }, (_, i) => {
22250
22264
  return { value: v, label: v };
22251
22265
  });
22252
22266
  var MONTH_LABELS = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"];
22267
+ var MONTH_SHORT = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
22253
22268
  var WEEKDAY_LABELS = ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"];
22254
22269
  function normalizeDateInput(raw) {
22255
22270
  const t = raw.trim();
@@ -22271,6 +22286,151 @@ function parseIsoDate(raw) {
22271
22286
  function isoDate(d) {
22272
22287
  return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
22273
22288
  }
22289
+ function fillCalendar(el, current, onPick) {
22290
+ const today = /* @__PURE__ */ new Date();
22291
+ const selected = parseIsoDate(current);
22292
+ let year = selected?.getFullYear() ?? today.getFullYear();
22293
+ let month = selected?.getMonth() ?? today.getMonth();
22294
+ let mode = "date";
22295
+ const prev2 = document.createElement("button");
22296
+ prev2.type = "button";
22297
+ prev2.className = "vela-ind-cal-nav";
22298
+ prev2.innerHTML = iconAt("chevron-left", 14);
22299
+ const next = document.createElement("button");
22300
+ next.type = "button";
22301
+ next.className = "vela-ind-cal-nav";
22302
+ next.innerHTML = iconAt("chevron-right", 14);
22303
+ const monthBtn = document.createElement("button");
22304
+ monthBtn.type = "button";
22305
+ monthBtn.className = "vela-ind-cal-switch";
22306
+ monthBtn.setAttribute("aria-label", "Choose month");
22307
+ const yearBtn = document.createElement("button");
22308
+ yearBtn.type = "button";
22309
+ yearBtn.className = "vela-ind-cal-switch";
22310
+ yearBtn.setAttribute("aria-label", "Choose year");
22311
+ const title = document.createElement("div");
22312
+ title.className = "vela-ind-cal-title";
22313
+ title.append(monthBtn, yearBtn);
22314
+ const head = document.createElement("div");
22315
+ head.className = "vela-ind-cal-head";
22316
+ head.append(prev2, title, next);
22317
+ const week = document.createElement("div");
22318
+ week.className = "vela-ind-cal-week";
22319
+ for (const d of WEEKDAY_LABELS) {
22320
+ const cell = document.createElement("span");
22321
+ cell.textContent = d;
22322
+ week.appendChild(cell);
22323
+ }
22324
+ const grid = document.createElement("div");
22325
+ const paint = () => {
22326
+ const decade = Math.floor(year / 10) * 10;
22327
+ monthBtn.hidden = mode !== "date";
22328
+ monthBtn.textContent = MONTH_LABELS[month] ?? "";
22329
+ yearBtn.textContent = mode === "year" ? `${decade}-${decade + 9}` : String(year);
22330
+ yearBtn.disabled = mode === "year";
22331
+ week.hidden = mode !== "date";
22332
+ prev2.setAttribute("aria-label", mode === "date" ? "Previous month" : mode === "month" ? "Previous year" : "Previous decade");
22333
+ next.setAttribute("aria-label", mode === "date" ? "Next month" : mode === "month" ? "Next year" : "Next decade");
22334
+ grid.className = mode === "date" ? "vela-ind-cal-grid" : "vela-ind-cal-cells";
22335
+ grid.replaceChildren();
22336
+ if (mode === "date") {
22337
+ const startPad = new Date(year, month, 1).getDay();
22338
+ const days = new Date(year, month + 1, 0).getDate();
22339
+ const todayIso = isoDate(today);
22340
+ const selectedIso = selected ? isoDate(selected) : "";
22341
+ for (let i = 0; i < startPad; i++) {
22342
+ const blank = document.createElement("span");
22343
+ blank.className = "vela-ind-cal-blank";
22344
+ grid.appendChild(blank);
22345
+ }
22346
+ for (let day = 1; day <= days; day++) {
22347
+ const iso = `${year}-${String(month + 1).padStart(2, "0")}-${String(day).padStart(2, "0")}`;
22348
+ const b = document.createElement("button");
22349
+ b.type = "button";
22350
+ b.className = "vela-ind-cal-day";
22351
+ b.textContent = String(day);
22352
+ if (iso === selectedIso) b.dataset.checked = "1";
22353
+ if (iso === todayIso) b.dataset.today = "1";
22354
+ b.addEventListener("click", (e) => {
22355
+ e.stopPropagation();
22356
+ onPick(iso);
22357
+ });
22358
+ grid.appendChild(b);
22359
+ }
22360
+ return;
22361
+ }
22362
+ if (mode === "month") {
22363
+ for (let m = 0; m < 12; m++) {
22364
+ const b = document.createElement("button");
22365
+ b.type = "button";
22366
+ b.className = "vela-ind-cal-cell";
22367
+ b.textContent = MONTH_SHORT[m] ?? "";
22368
+ if (selected?.getFullYear() === year && selected.getMonth() === m) b.dataset.checked = "1";
22369
+ if (today.getFullYear() === year && today.getMonth() === m) b.dataset.today = "1";
22370
+ b.addEventListener("click", (e) => {
22371
+ e.stopPropagation();
22372
+ month = m;
22373
+ mode = "date";
22374
+ paint();
22375
+ });
22376
+ grid.appendChild(b);
22377
+ }
22378
+ return;
22379
+ }
22380
+ for (let y = decade - 1; y <= decade + 10; y++) {
22381
+ const b = document.createElement("button");
22382
+ b.type = "button";
22383
+ b.className = "vela-ind-cal-cell";
22384
+ b.textContent = String(y);
22385
+ if (y < decade || y > decade + 9) b.dataset.outside = "1";
22386
+ if (selected?.getFullYear() === y) b.dataset.checked = "1";
22387
+ if (today.getFullYear() === y) b.dataset.today = "1";
22388
+ b.addEventListener("click", (e) => {
22389
+ e.stopPropagation();
22390
+ year = y;
22391
+ mode = "month";
22392
+ paint();
22393
+ });
22394
+ grid.appendChild(b);
22395
+ }
22396
+ };
22397
+ const step = (dir) => {
22398
+ if (mode === "date") {
22399
+ month += dir;
22400
+ if (month < 0) {
22401
+ month = 11;
22402
+ year -= 1;
22403
+ }
22404
+ if (month > 11) {
22405
+ month = 0;
22406
+ year += 1;
22407
+ }
22408
+ } else {
22409
+ year += mode === "month" ? dir : dir * 10;
22410
+ }
22411
+ paint();
22412
+ };
22413
+ prev2.addEventListener("click", (e) => {
22414
+ e.stopPropagation();
22415
+ step(-1);
22416
+ });
22417
+ next.addEventListener("click", (e) => {
22418
+ e.stopPropagation();
22419
+ step(1);
22420
+ });
22421
+ monthBtn.addEventListener("click", (e) => {
22422
+ e.stopPropagation();
22423
+ mode = "month";
22424
+ paint();
22425
+ });
22426
+ yearBtn.addEventListener("click", (e) => {
22427
+ e.stopPropagation();
22428
+ mode = "year";
22429
+ paint();
22430
+ });
22431
+ paint();
22432
+ el.append(head, week, grid);
22433
+ }
22274
22434
  function normalizeTimeInput(raw) {
22275
22435
  const t = raw.trim();
22276
22436
  const m = /^(\d{1,2}):(\d{2})$/.exec(t) ?? /^(\d{2})(\d{2})$/.exec(t);
@@ -28758,6 +28918,10 @@ var BUILTIN_SETTINGS_IDS = [
28758
28918
  "canvas.grid",
28759
28919
  "canvas.grid.vertical",
28760
28920
  "canvas.grid.horizontal",
28921
+ "canvas.margins",
28922
+ "canvas.margins.top",
28923
+ "canvas.margins.bottom",
28924
+ "canvas.margins.right",
28761
28925
  "canvas.theme"
28762
28926
  ];
28763
28927
  function settingsIdCatalog(hostSections, markGroups = []) {
@@ -29288,6 +29452,10 @@ var SettingsDialog = class {
29288
29452
  body.append(sid(this.toggleRow("Horizontal", config.grid.horzLines.visible, (v) => this.emit({ grid: { horzLines: { visible: v } } }), [
29289
29453
  this.swatch(config.grid.horzLines.color, (v) => this.emit({ grid: { horzLines: { color: v } } }))
29290
29454
  ]), "canvas.grid.horizontal"));
29455
+ body.append(sid(this.sectionTitle("Margins"), "canvas.margins"));
29456
+ body.append(sid(this.numberRow("Top", config.margins.top, 0, 40, 1, (v) => this.emit({ margins: { top: v } }), "%"), "canvas.margins.top"));
29457
+ body.append(sid(this.numberRow("Bottom", config.margins.bottom, 0, 40, 1, (v) => this.emit({ margins: { bottom: v } }), "%"), "canvas.margins.bottom"));
29458
+ body.append(sid(this.numberRow("Right", config.margins.right, 0, 200, 1, (v) => this.emit({ margins: { right: v } }), "bars"), "canvas.margins.right"));
29291
29459
  if (this.themeControl) {
29292
29460
  const tc = this.themeControl;
29293
29461
  body.append(sid(this.sectionTitle("Theme"), "canvas.theme"));
@@ -29859,23 +30027,16 @@ var SettingsDialog = class {
29859
30027
  this.hintTips.push(dispose);
29860
30028
  return el;
29861
30029
  }
29862
- numberRow(label, value, min, max, step, onChange) {
29863
- return fieldRow({
29864
- label,
29865
- labelSize: "sm",
29866
- className: "vela-sd-row",
29867
- control: buildFieldControl({
29868
- kind: "number",
29869
- value,
29870
- min,
29871
- max,
29872
- step,
29873
- fill: false,
29874
- commit: "live",
29875
- steppers: true,
29876
- onChange
29877
- }).el
29878
- });
30030
+ /** `unit` (optional) trails the input as muted text — "%", "bars". */
30031
+ numberRow(label, value, min, max, step, onChange, unit) {
30032
+ const controls = [buildFieldControl({ kind: "number", value, min, max, step, fill: false, commit: "live", steppers: true, onChange }).el];
30033
+ if (unit) {
30034
+ const u = document.createElement("span");
30035
+ u.textContent = unit;
30036
+ u.style.cssText = "color:var(--vela-fg-muted);";
30037
+ controls.push(u);
30038
+ }
30039
+ return this.rowWith(label, controls);
29879
30040
  }
29880
30041
  /** A dropdown whose option values differ from their display labels. */
29881
30042
  selectRowLabeled(label, value, options, onChange) {
@@ -34749,9 +34910,7 @@ function createProjector(coords, paneOf, paneIdAtY, barsInRange, seriesInRange)
34749
34910
  }
34750
34911
 
34751
34912
  // src/renderers/native/core/autoscale.ts
34752
- var MARGIN_TOP = 2 / 7;
34753
- var MARGIN_BOTTOM = 1 / 7;
34754
- function computePaneScale(models, bars, includeCandles, i0, i1, drawings, log = false, offsetOf = () => 0) {
34913
+ function computePaneScale(models, bars, includeCandles, i0, i1, drawings, log = false, offsetOf = () => 0, margins = DEFAULT_MARGINS) {
34755
34914
  let min = Infinity;
34756
34915
  let max = -Infinity;
34757
34916
  const consider = (v) => {
@@ -34786,14 +34945,17 @@ function computePaneScale(models, bars, includeCandles, i0, i1, drawings, log =
34786
34945
  const pad = Math.abs(min) * 0.1 || 1;
34787
34946
  return { min: min - pad, max: max + pad, log: log && min - pad > 0 };
34788
34947
  }
34948
+ const content = Math.max(0.1, 1 - (margins.top + margins.bottom) / 100);
34949
+ const above = margins.top / 100 / content;
34950
+ const below = margins.bottom / 100 / content;
34789
34951
  if (log && min > 0) {
34790
34952
  const lmin = Math.log(min);
34791
34953
  const lmax = Math.log(max);
34792
34954
  const lspan = lmax - lmin;
34793
- return { min: Math.exp(lmin - lspan * MARGIN_BOTTOM), max: Math.exp(lmax + lspan * MARGIN_TOP), log: true };
34955
+ return { min: Math.exp(lmin - lspan * below), max: Math.exp(lmax + lspan * above), log: true };
34794
34956
  }
34795
34957
  const span = max - min;
34796
- return { min: min - span * MARGIN_BOTTOM, max: max + span * MARGIN_TOP };
34958
+ return { min: min - span * below, max: max + span * above };
34797
34959
  }
34798
34960
  function considerSeries(s, i0, i1, off, consider) {
34799
34961
  if (!seriesInScale(s)) return;
@@ -36484,6 +36646,7 @@ var NativeRenderer = class {
36484
36646
  intro: this.intro.style !== false
36485
36647
  },
36486
36648
  panes: { separatorColor: s.separatorColor ?? t.borderColor },
36649
+ margins: { ...s.margins },
36487
36650
  trades: {
36488
36651
  visible: this.scene.tradeMarkers.visible,
36489
36652
  labels: this.scene.tradeMarkers.labels,
@@ -36596,6 +36759,10 @@ var NativeRenderer = class {
36596
36759
  this.animAutoscale.toggle(next.animations.autoscale);
36597
36760
  this.intro = { style: next.animations.intro ? this.introOnStyle : false, duration: this.intro.duration || INTRO_DURATION_DEFAULT_MS };
36598
36761
  s.separatorColor = keepInherit(s.separatorColor, next.panes.separatorColor, prevTheme.borderColor);
36762
+ if (next.margins.right !== s.margins.right && this.coords.barCount > 0) {
36763
+ this.applyViewport({ barSpacing: this.coords.getViewport().barSpacing, rightOffset: next.margins.right });
36764
+ }
36765
+ s.margins = { ...next.margins };
36599
36766
  this.scene.tradeMarkers = {
36600
36767
  visible: next.trades.visible,
36601
36768
  labels: next.trades.labels,
@@ -36839,7 +37006,7 @@ var NativeRenderer = class {
36839
37006
  /** Glide the view back to the most recent bars, keeping the current zoom (barSpacing). */
36840
37007
  scrollToRealtime() {
36841
37008
  if (this.coords.barCount === 0) return;
36842
- this.glideRightOffset(ZOOM_OUT_MARGIN_BARS);
37009
+ this.glideRightOffset(this.scene.style.margins.right);
36843
37010
  }
36844
37011
  /** Ease rightOffset to `target` at constant zoom (see animTick's scroll glide);
36845
37012
  * instant when the scroll glide is off. Shared by scroll-to-latest and panBy. */
@@ -38781,6 +38948,7 @@ var NativeRenderer = class {
38781
38948
  const pricePane = panes.find((p) => p.kind === "price") ?? null;
38782
38949
  this.chrome.prepare(this.scene, this.coords, this.theme);
38783
38950
  const animating = this.animator.active;
38951
+ const margins = this.scene.style.margins;
38784
38952
  for (const pane of panes) {
38785
38953
  if (pane.manualScale) {
38786
38954
  pane.scaleTarget = pane.manualScale;
@@ -38796,7 +38964,7 @@ var NativeRenderer = class {
38796
38964
  if (or) dr = dr ? { min: Math.min(dr.min, or.min), max: Math.max(dr.max, or.max) } : or;
38797
38965
  }
38798
38966
  const includeCandles = pane.kind === "price" && (!this.scene.candlesHidden || this.priceLayersAnchoredToBars(masterModels) || !this.paneHasMeasurableContent(masterModels, dr));
38799
- pane.scaleTarget = computePaneScale(masterModels, this.bars, includeCandles, i0, i1, dr, paneLogScale(this.scene, pane), (id) => this.scene.offsetOf(id));
38967
+ pane.scaleTarget = computePaneScale(masterModels, this.bars, includeCandles, i0, i1, dr, paneLogScale(this.scene, pane), (id) => this.scene.offsetOf(id), margins);
38800
38968
  pane.percentBaseline = pane.kind === "price" ? this.bars[i0]?.close ?? 0 : this.firstVisibleValue(masterModels, i0);
38801
38969
  pane.axisFormat = void 0;
38802
38970
  pane.axisBands = void 0;
@@ -38807,7 +38975,7 @@ var NativeRenderer = class {
38807
38975
  pane.axisFormat = "volume";
38808
38976
  }
38809
38977
  } else if (this.layerNativesOwnPane(pane, masterModels)) {
38810
- pane.scaleTarget = computePaneScale([], this.bars, true, i0, i1, dr, paneLogScale(this.scene, pane), (id) => this.scene.offsetOf(id));
38978
+ pane.scaleTarget = computePaneScale([], this.bars, true, i0, i1, dr, paneLogScale(this.scene, pane), (id) => this.scene.offsetOf(id), margins);
38811
38979
  pane.percentBaseline = this.bars[i0]?.close ?? 0;
38812
38980
  if (masterModels.every((m) => m.paneAxis != null)) {
38813
38981
  pane.axisFormat = "none";
@@ -38836,7 +39004,7 @@ var NativeRenderer = class {
38836
39004
  continue;
38837
39005
  }
38838
39006
  const mdr = this.chrome.paneDrawingsRange([model], this.scene, false, vr);
38839
- sl.scaleTarget = computePaneScale([model], this.bars, false, i0, i1, mdr, false, (id) => this.scene.offsetOf(id));
39007
+ sl.scaleTarget = computePaneScale([model], this.bars, false, i0, i1, mdr, false, (id) => this.scene.offsetOf(id), margins);
38840
39008
  if (!animating || !sl.initialized) {
38841
39009
  sl.scale = { ...sl.scaleTarget };
38842
39010
  sl.initialized = true;
@@ -38877,13 +39045,13 @@ var NativeRenderer = class {
38877
39045
  for (const pane of this.scene.panes.values()) pane.manualScale = null;
38878
39046
  for (const sl of this.scene.indicatorScales.values()) sl.manualScale = null;
38879
39047
  const visibleBars = Math.min(n, 200);
38880
- const rightOffset = 6;
39048
+ const rightOffset = this.scene.style.margins.right;
38881
39049
  const v = this.clampViewport(w / ((visibleBars + rightOffset) * this.coords.spacingScale), rightOffset);
38882
39050
  this.coords.setViewport(v);
38883
39051
  this.targetBarSpacing = v.barSpacing;
38884
39052
  }
38885
39053
  /** Re-frame after a series replacement (a symbol/timeframe switch): keep the user's
38886
- * zoom (bar spacing), re-anchor the newest bars at the default right offset.
39054
+ * zoom (bar spacing), re-anchor the newest bars at the configured right margin.
38887
39055
  * `clampViewport`'s fit-all-bars floor deliberately does NOT apply — a progressive
38888
39056
  * head may still be backfilling toward the previous depth, and raising the spacing
38889
39057
  * to its temporary bar count would lose the zoom this exists to keep. */
@@ -38892,7 +39060,7 @@ var NativeRenderer = class {
38892
39060
  this.panVelocity = 0;
38893
39061
  for (const pane of this.scene.panes.values()) pane.manualScale = null;
38894
39062
  for (const sl of this.scene.indicatorScales.values()) sl.manualScale = null;
38895
- const v = { barSpacing: clampBarSpacing(this.coords.getViewport().barSpacing), rightOffset: defaultViewport().rightOffset };
39063
+ const v = { barSpacing: clampBarSpacing(this.coords.getViewport().barSpacing), rightOffset: this.scene.style.margins.right };
38896
39064
  this.coords.setViewport(v);
38897
39065
  this.targetBarSpacing = v.barSpacing;
38898
39066
  }
@@ -41977,6 +42145,7 @@ function registerClassicIndicators() {
41977
42145
  }
41978
42146
 
41979
42147
  // src/Vela.ts
42148
+ var asError = (err) => err instanceof Error ? err : new Error(String(err));
41980
42149
  var Vela = class {
41981
42150
  constructor(container, options = {}, deps = {}) {
41982
42151
  registerBuiltinChartTypes();
@@ -42100,7 +42269,12 @@ var Vela = class {
42100
42269
  * fetch from — the same relationship `script:run` has to `context:changed`.
42101
42270
  */
42102
42271
  runScript(source, options) {
42103
- const handle = this.addIndicator(source, options);
42272
+ let handle;
42273
+ try {
42274
+ handle = this.addIndicator(source, options);
42275
+ } catch (err) {
42276
+ return Promise.resolve({ ok: false, run: null, error: asError(err), onUpdate: () => () => void 0, remove: () => void 0 });
42277
+ }
42104
42278
  const updates = /* @__PURE__ */ new Set();
42105
42279
  const drop = () => {
42106
42280
  try {
@@ -42144,7 +42318,12 @@ var Vela = class {
42144
42318
  });
42145
42319
  }
42146
42320
  runIndicator(source, options) {
42147
- const handle = this.addIndicator(source, options);
42321
+ let handle;
42322
+ try {
42323
+ handle = this.addIndicator(source, options);
42324
+ } catch (err) {
42325
+ return Promise.resolve({ ok: false, handle: null, error: asError(err), context: null });
42326
+ }
42148
42327
  return new Promise((resolve) => {
42149
42328
  const offReady = handle.on("ready", () => {
42150
42329
  offReady();
@@ -43493,7 +43672,7 @@ var ChartContextMenu = class {
43493
43672
  } else if (id.startsWith("tz:")) {
43494
43673
  const zone = id.slice("tz:".length);
43495
43674
  if (this.cbs.setTimezone) this.cbs.setTimezone(zone);
43496
- else chart.renderer.set("timezone", zone);
43675
+ else chart.renderer.set("timezone", resolveTimezone(zone, void 0));
43497
43676
  } else if (id === "auto") {
43498
43677
  chart.renderer.set("autoScale", chart.renderer.get("autoScale") === false);
43499
43678
  } else if (id === "invert") {
@@ -43726,7 +43905,7 @@ var ChartCell = class {
43726
43905
  this.history.onChart(this.inner);
43727
43906
  this.inner.renderer.onConfigChanged(() => {
43728
43907
  const zone = this.inner?.renderer.get("timezone");
43729
- if (typeof zone === "string" && normalizeTimezone(zone) !== normalizeTimezone(this.deps.timezone())) {
43908
+ if (typeof zone === "string" && normalizeTimezone(zone) !== normalizeTimezone(this.displayTimezone)) {
43730
43909
  this.deps.setTimezone(normalizeTimezone(zone));
43731
43910
  }
43732
43911
  const style = this.priceStyle;
@@ -43768,8 +43947,7 @@ var ChartCell = class {
43768
43947
  this.refreshSessionShading();
43769
43948
  });
43770
43949
  this.inner.on("viewport:changed", (range) => this.sessionShading.updateRange(range));
43771
- const tz = deps.timezone();
43772
- if (tz !== "Etc/UTC") this.inner.renderer.set("timezone", tz);
43950
+ this.applyTimezone();
43773
43951
  this.indicatorTitlesOn = seed.indicatorTitles ?? true;
43774
43952
  if (!this.indicatorTitlesOn) this.inner.renderer.set("indicatorTitles", false);
43775
43953
  this.indicatorValuesOn = seed.indicatorValues ?? true;
@@ -43791,7 +43969,7 @@ var ChartCell = class {
43791
43969
  this.statusline?.setSymbol(this.state.symbol);
43792
43970
  this.statusline?.setMeta(this.state.timeframe ?? "60", this.inner.data.displayPrefix(this.state.symbol) ?? this.state.provider ?? "");
43793
43971
  }
43794
- this.refreshSessionAvailable();
43972
+ this.refreshSymbolMetadata();
43795
43973
  if (this.inner && this.state.symbol) this.marketStatus?.track(this.inner.data, this.state.symbol);
43796
43974
  });
43797
43975
  this.syncStatuslineColors();
@@ -43865,7 +44043,7 @@ var ChartCell = class {
43865
44043
  this.offMarket = this.inner.on("market:changed", ({ symbol: symbol2, timeframe }) => {
43866
44044
  this.projectMarket(symbol2, timeframe);
43867
44045
  this.refreshNativeCatalog();
43868
- this.refreshSessionAvailable();
44046
+ this.refreshSymbolMetadata();
43869
44047
  if (this.inner) this.marketStatus?.track(this.inner.data, symbol2);
43870
44048
  this.deps.onMarketChanged(this.id);
43871
44049
  });
@@ -43906,7 +44084,35 @@ var ChartCell = class {
43906
44084
  this.deps.onStateDirty();
43907
44085
  void this.inner?.setMarket({ session });
43908
44086
  }
43909
- refreshSessionAvailable() {
44087
+ /**
44088
+ * The symbol's own trading zone, once its metadata has landed — what the exchange
44089
+ * rule resolves to on this cell (the workspace labels the shared bottom bar with the
44090
+ * ACTIVE cell's). Undefined = unknown or undeclared (the rule then renders UTC).
44091
+ */
44092
+ get exchangeTimezone() {
44093
+ return this.exchangeZone;
44094
+ }
44095
+ /** The IANA zone this cell's axis renders in: the workspace choice, resolved. */
44096
+ get displayTimezone() {
44097
+ return resolveTimezone(this.deps.timezone(), this.exchangeZone);
44098
+ }
44099
+ /**
44100
+ * Push the resolved zone to the renderer — on the workspace choice changing, and on
44101
+ * this cell's market zone changing under the exchange rule. Skips the write when the
44102
+ * renderer already holds it (its config default is the bare `'UTC'` alias).
44103
+ */
44104
+ applyTimezone() {
44105
+ const chart = this.inner;
44106
+ if (!chart) return;
44107
+ const zone = this.displayTimezone;
44108
+ const current = chart.renderer.get("timezone");
44109
+ const held = typeof current === "string" && current ? current : "UTC";
44110
+ if (normalizeTimezone(held) === normalizeTimezone(zone)) return;
44111
+ chart.renderer.set("timezone", zone);
44112
+ }
44113
+ /** Re-read the symbol's metadata: session posture (RTH/ETH toggle, shading) and its
44114
+ * trading zone (the exchange rule). Async — the workspace re-projects when a verdict lands. */
44115
+ refreshSymbolMetadata() {
43910
44116
  const chart = this.inner;
43911
44117
  const symbol = this.state.symbol;
43912
44118
  if (!chart || !symbol) return;
@@ -43914,7 +44120,13 @@ var ChartCell = class {
43914
44120
  if (this.inner !== chart) return;
43915
44121
  const available = typeof si?.session === "string" && si.session !== "" && si.session !== "24x7";
43916
44122
  const overnight = parseSessionSpec(si)?.overnight === true;
43917
- if (available !== this.sessionAvailableFlag || overnight !== this.sessionOvernightFlag) {
44123
+ const zone = typeof si?.timezone === "string" && si.timezone !== "" ? si.timezone : void 0;
44124
+ const zoneChanged = zone !== this.exchangeZone;
44125
+ if (zoneChanged) {
44126
+ this.exchangeZone = zone;
44127
+ this.applyTimezone();
44128
+ }
44129
+ if (available !== this.sessionAvailableFlag || overnight !== this.sessionOvernightFlag || zoneChanged) {
43918
44130
  this.sessionAvailableFlag = available;
43919
44131
  this.sessionOvernightFlag = overnight;
43920
44132
  this.deps.onMarketChanged(this.id);
@@ -44373,16 +44585,19 @@ var ChartCell = class {
44373
44585
  * a persistence handler's `restore` runs silently, a user-driven call records.
44374
44586
  */
44375
44587
  addExternalIndicator(entry) {
44588
+ const { id, inputs, props, hidden, ...script } = entry;
44376
44589
  this.addManifestInstance(
44377
- { ...entry, enabled: true },
44378
- { external: true, ...entry.inputs ? { inputs: entry.inputs } : {}, ...entry.props ? { props: entry.props } : {}, ...entry.hidden ? { hidden: true } : {} }
44590
+ { ...script, enabled: true },
44591
+ { external: true, ...id !== void 0 ? { id } : {}, ...inputs ? { inputs } : {}, ...props ? { props } : {}, ...hidden ? { hidden: true } : {} }
44379
44592
  );
44380
44593
  }
44381
44594
  /** Add ONE instance of a manifest entry (repeatable — duplicates are legitimate). */
44382
44595
  addManifestInstance(entry, opts = {}) {
44383
44596
  if (this.destroyed) return;
44384
44597
  const values = opts.inputs || opts.props ? { inputs: opts.inputs, props: opts.props } : void 0;
44385
- const it = { entry, handle: this.addToChart(entry, values), ...opts.external ? { external: true } : {}, ...values ? { values } : {} };
44598
+ const handle = this.addToChart(entry, values, opts.id);
44599
+ if (!handle) return;
44600
+ const it = { entry, handle, id: handle.id, ...opts.external ? { external: true } : {}, ...values ? { values } : {} };
44386
44601
  if (opts.hidden) it.handle?.setVisible(false);
44387
44602
  this.instances.push(it);
44388
44603
  this.deps.onIndicatorsChanged(this.id);
@@ -44391,7 +44606,7 @@ var ChartCell = class {
44391
44606
  this.history.push({
44392
44607
  undo: () => this.dropInstance(snapshot),
44393
44608
  redo: () => {
44394
- snapshot.handle = this.addToChart(snapshot.entry, snapshot.values);
44609
+ snapshot.handle = this.addToChart(snapshot.entry, snapshot.values, snapshot.id);
44395
44610
  this.instances.push(snapshot);
44396
44611
  this.deps.onIndicatorsChanged(this.id);
44397
44612
  }
@@ -44404,7 +44619,7 @@ var ChartCell = class {
44404
44619
  const snapshot = it;
44405
44620
  this.history.push({
44406
44621
  undo: () => {
44407
- snapshot.handle = this.addToChart(snapshot.entry, snapshot.values);
44622
+ snapshot.handle = this.addToChart(snapshot.entry, snapshot.values, snapshot.id);
44408
44623
  this.instances.push(snapshot);
44409
44624
  this.deps.onIndicatorsChanged(this.id);
44410
44625
  },
@@ -44494,9 +44709,10 @@ var ChartCell = class {
44494
44709
  this.deps.onIndicatorsChanged(this.id);
44495
44710
  });
44496
44711
  }
44497
- addToChart(entry, values) {
44712
+ addToChart(entry, values, id) {
44498
44713
  try {
44499
44714
  return this.inner?.addIndicator(entry.script, {
44715
+ ...id !== void 0 ? { id } : {},
44500
44716
  ...entry.language !== void 0 ? { language: entry.language } : {},
44501
44717
  ...values?.inputs ? { inputs: values.inputs } : {},
44502
44718
  ...values?.props ? { props: values.props } : {}
@@ -45680,13 +45896,23 @@ var VelaWorkspace = class {
45680
45896
  }
45681
45897
  }
45682
45898
  }
45683
- /** Set the workspace-global display timezone — applied to EVERY cell. */
45899
+ /**
45900
+ * Set the workspace-global display timezone — applied to EVERY cell. An IANA zone,
45901
+ * or `'exchange'` (the exchange rule): each cell then renders in its OWN market's
45902
+ * zone (Chicago for a CME future, New York for a US equity, UTC for crypto), and the
45903
+ * bottom bar follows the active cell's.
45904
+ */
45684
45905
  setTimezone(zone) {
45685
45906
  this.timezone = zone;
45686
- this.bottombar?.setTimezone(zone);
45687
- for (const cell of this.cellsById.values()) cell.chart.renderer.set("timezone", zone);
45907
+ this.projectTimezone();
45908
+ for (const cell of this.cellsById.values()) cell.applyTimezone();
45688
45909
  this.markStateDirty();
45689
45910
  }
45911
+ /** Bottom bar ⇐ the stored choice + the ACTIVE cell's market zone (labels the exchange row). */
45912
+ projectTimezone() {
45913
+ const active = this.activeId ? this.cellsById.get(this.activeId) : void 0;
45914
+ this.bottombar?.setTimezone(this.timezone, active?.exchangeTimezone);
45915
+ }
45690
45916
  /**
45691
45917
  * Swap the workspace theme at runtime — `'dark'`, `'light'`, or a full custom theme,
45692
45918
  * applied to the shared chrome (topbar, panels, drawing toolbar) and EVERY cell.
@@ -45891,6 +46117,7 @@ var VelaWorkspace = class {
45891
46117
  this.dock.onChart(cell.chart);
45892
46118
  this.bottombar?.setActiveRange(cell.activeRangeId);
45893
46119
  this.bottombar?.setSession({ session: cell.session, enabled: cell.sessionAvailable });
46120
+ this.projectTimezone();
45894
46121
  this.indicatorPicker?.sync();
45895
46122
  this.glider.stop();
45896
46123
  const d = cell.chart.drawings;
@@ -46475,6 +46702,7 @@ var VelaWorkspace = class {
46475
46702
  this.mobileBar?.setTimeframe(cell.timeframe);
46476
46703
  this.objectTree.setSymbol(cell.symbol);
46477
46704
  this.bottombar?.setSession({ session: cell.session, enabled: cell.sessionAvailable });
46705
+ this.projectTimezone();
46478
46706
  this.bottombar?.setActiveRange(cell.activeRangeId);
46479
46707
  }
46480
46708
  /** Trigger ② — a cell's price style changed: the topbar button/menu only if active.