@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/index.cjs CHANGED
@@ -129,10 +129,18 @@ var IndicatorRegistry = class {
129
129
  this.records = /* @__PURE__ */ new Map();
130
130
  this.counter = 0;
131
131
  }
132
- /** Allocate a unique, stable per-instance id. */
133
- nextId(prefix = "ind") {
134
- this.counter += 1;
135
- return `${prefix}-${this.counter}`;
132
+ /**
133
+ * Mint a unique per-instance id. Host-supplied ids share the namespace, so a minted
134
+ * id skips anything already recorded — and anything `taken` reports live elsewhere
135
+ * (a handle the orchestrator holds outside the records, e.g. a fail-soft native).
136
+ */
137
+ nextId(prefix = "ind", taken = () => false) {
138
+ let id;
139
+ do {
140
+ this.counter += 1;
141
+ id = `${prefix}-${this.counter}`;
142
+ } while (this.records.has(id) || taken(id));
143
+ return id;
136
144
  }
137
145
  add(record) {
138
146
  this.records.set(record.id, record);
@@ -8677,7 +8685,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
8677
8685
  this.engines.set(language, engine);
8678
8686
  }
8679
8687
  addIndicator(source, options = {}) {
8680
- const id = this.registry.nextId();
8688
+ const id = this.claimIndicatorId(options.id);
8681
8689
  const title = options.title ?? "Indicator";
8682
8690
  const handle = new IndicatorHandleImpl(id, title, this, source);
8683
8691
  this.handles.set(id, handle);
@@ -8699,7 +8707,7 @@ var EngineOrchestrator = class _EngineOrchestrator {
8699
8707
  const existing = this.registry.all().find((r) => r.native?.type === type);
8700
8708
  if (existing) return this.handles.get(existing.id) ?? new IndicatorHandleImpl(existing.id, existing.title, this, void 0, type);
8701
8709
  }
8702
- const id = this.registry.nextId("native");
8710
+ const id = this.registry.nextId("native", (candidate) => this.handles.has(candidate));
8703
8711
  const title = descriptor?.title ?? type;
8704
8712
  const handle = new IndicatorHandleImpl(id, title, this, void 0, type);
8705
8713
  this.handles.set(id, handle);
@@ -8977,6 +8985,24 @@ var EngineOrchestrator = class _EngineOrchestrator {
8977
8985
  this.events.clear();
8978
8986
  }
8979
8987
  // ── internals ───────────────────────────────────────────────
8988
+ /**
8989
+ * The id a new script indicator runs under: the host's when supplied, else a minted
8990
+ * one. A host id is opaque but must be a non-empty string, and it must not be live
8991
+ * on this chart — a duplicate is a programming error surfaced synchronously, never
8992
+ * renamed behind the caller's back (the whole point of supplying one is that
8993
+ * `handle.id` equals what was passed). "Live" reads both the records and the handle
8994
+ * map: a fail-soft handle never enters the registry but still owns its id.
8995
+ */
8996
+ claimIndicatorId(requested) {
8997
+ if (requested === void 0) return this.registry.nextId("ind", (candidate) => this.handles.has(candidate));
8998
+ if (typeof requested !== "string" || requested.length === 0) {
8999
+ throw new TypeError("[vela] addIndicator: `id` must be a non-empty string");
9000
+ }
9001
+ if (this.handles.has(requested) || this.registry.get(requested)) {
9002
+ throw new Error(`[vela] addIndicator: indicator id "${requested}" is already live on this chart`);
9003
+ }
9004
+ return requested;
9005
+ }
8980
9006
  async startIndicator(id, source, options, handle) {
8981
9007
  try {
8982
9008
  await this.readyPromise;
@@ -13119,7 +13145,8 @@ function addRecent(hex6) {
13119
13145
  recents.unshift(hex6);
13120
13146
  if (recents.length > 10) recents.length = 10;
13121
13147
  }
13122
- function buildColorPicker(color, theme, onChange) {
13148
+ function buildColorPicker(color, theme, onChange, options = {}) {
13149
+ const commit = options.commit ?? "live";
13123
13150
  const parsed = splitColor(color);
13124
13151
  let curHex = parsed.hex6;
13125
13152
  let curAlpha = parsed.alpha;
@@ -13202,7 +13229,12 @@ function buildColorPicker(color, theme, onChange) {
13202
13229
  const r = track.getBoundingClientRect();
13203
13230
  curAlpha = Math.max(0, Math.min(1, (clientX - r.left) / r.width));
13204
13231
  paintOpacity();
13205
- emit();
13232
+ if (commit === "live") emit();
13233
+ };
13234
+ const endDrag = () => {
13235
+ if (!dragging) return;
13236
+ dragging = false;
13237
+ if (commit === "release") emit();
13206
13238
  };
13207
13239
  track.addEventListener("pointerdown", (e) => {
13208
13240
  e.stopPropagation();
@@ -13213,7 +13245,8 @@ function buildColorPicker(color, theme, onChange) {
13213
13245
  track.addEventListener("pointermove", (e) => {
13214
13246
  if (dragging) onDrag(e.clientX);
13215
13247
  });
13216
- track.addEventListener("pointerup", () => dragging = false);
13248
+ track.addEventListener("pointerup", endDrag);
13249
+ track.addEventListener("pointercancel", endDrag);
13217
13250
  function emit() {
13218
13251
  onChange(combineColor(curHex, curAlpha));
13219
13252
  }
@@ -13236,7 +13269,7 @@ function buildColorPicker(color, theme, onChange) {
13236
13269
  return root;
13237
13270
  }
13238
13271
  function colorField(theme, getVal, onVal, opts) {
13239
- return new ColorField({ theme, getVal, onVal, shape: opts?.shape, id: opts?.id, popover: opts?.popover }).el;
13272
+ return new ColorField({ theme, getVal, onVal, shape: opts?.shape, id: opts?.id, popover: opts?.popover, commit: opts?.commit }).el;
13240
13273
  }
13241
13274
  var ColorField = class {
13242
13275
  constructor(opts) {
@@ -13245,6 +13278,7 @@ var ColorField = class {
13245
13278
  this.getVal = opts.getVal;
13246
13279
  this.onVal = opts.onVal;
13247
13280
  this.popoverOpts = opts.popover;
13281
+ this.commit = opts.commit;
13248
13282
  const trigger = document.createElement("button");
13249
13283
  trigger.type = "button";
13250
13284
  if (opts.id) trigger.id = opts.id;
@@ -13284,10 +13318,15 @@ var ColorField = class {
13284
13318
  boundary: this.popoverOpts?.boundary,
13285
13319
  zIndex: this.popoverOpts?.zIndex,
13286
13320
  className: "vela-color-field-pop",
13287
- content: buildColorPicker(this.getVal(), this.theme, (val) => {
13288
- this.onVal(val);
13289
- this.paint();
13290
- })
13321
+ content: buildColorPicker(
13322
+ this.getVal(),
13323
+ this.theme,
13324
+ (val) => {
13325
+ this.onVal(val);
13326
+ this.paint();
13327
+ },
13328
+ { commit: this.commit }
13329
+ )
13291
13330
  });
13292
13331
  pop.show();
13293
13332
  }
@@ -13768,7 +13807,8 @@ function buildFieldControl(desc) {
13768
13807
  const el2 = colorField(desc.theme, desc.get, desc.onChange, {
13769
13808
  shape: "circle",
13770
13809
  id: desc.id,
13771
- popover: desc.popover
13810
+ popover: desc.popover,
13811
+ commit: desc.commit
13772
13812
  });
13773
13813
  if (desc.title) el2.title = desc.title;
13774
13814
  return { el: el2 };
@@ -14146,7 +14186,10 @@ var IndicatorInputsDialog = class {
14146
14186
  id,
14147
14187
  theme: this.host.theme(),
14148
14188
  get: () => String(bagOf(row, inp)[inp.key] ?? inp.defval),
14149
- onChange: (v) => emit(v)
14189
+ onChange: (v) => emit(v),
14190
+ // An input change re-executes the script over its whole history: commit the
14191
+ // opacity drag once on release, not once per pointer move.
14192
+ commit: "release"
14150
14193
  }).el;
14151
14194
  }
14152
14195
  if (inp.type === "symbol") return this.buildSymbol(id, String(current), emit);
@@ -14393,10 +14436,6 @@ var IndicatorInputsDialog = class {
14393
14436
  }
14394
14437
  /** Open a themed month calendar under `anchor` — same surface + shadow as the choice list. */
14395
14438
  openCalendar(anchor, current, onPick) {
14396
- const parsed = parseIsoDate(current);
14397
- let year = parsed?.getFullYear() ?? (/* @__PURE__ */ new Date()).getFullYear();
14398
- let month = parsed?.getMonth() ?? (/* @__PURE__ */ new Date()).getMonth();
14399
- const selected = parsed ? isoDate(parsed) : current;
14400
14439
  const pop = new Popover({
14401
14440
  trigger: anchor,
14402
14441
  theme: this.host.theme(),
@@ -14408,80 +14447,10 @@ var IndicatorInputsDialog = class {
14408
14447
  this.calendarPop = null;
14409
14448
  this.calendarAnchor = null;
14410
14449
  },
14411
- content: (el) => {
14412
- const title = document.createElement("div");
14413
- title.className = "vela-ind-cal-title";
14414
- const prev2 = document.createElement("button");
14415
- prev2.type = "button";
14416
- prev2.className = "vela-ind-cal-nav";
14417
- prev2.setAttribute("aria-label", "Previous month");
14418
- prev2.innerHTML = iconAt("chevron-left", 14);
14419
- const next = document.createElement("button");
14420
- next.type = "button";
14421
- next.className = "vela-ind-cal-nav";
14422
- next.setAttribute("aria-label", "Next month");
14423
- next.innerHTML = iconAt("chevron-right", 14);
14424
- const head = document.createElement("div");
14425
- head.className = "vela-ind-cal-head";
14426
- head.append(prev2, title, next);
14427
- const week = document.createElement("div");
14428
- week.className = "vela-ind-cal-week";
14429
- for (const d of WEEKDAY_LABELS) {
14430
- const cell = document.createElement("span");
14431
- cell.textContent = d;
14432
- week.appendChild(cell);
14433
- }
14434
- const grid = document.createElement("div");
14435
- grid.className = "vela-ind-cal-grid";
14436
- const paint = () => {
14437
- title.textContent = `${MONTH_LABELS[month]} ${year}`;
14438
- grid.replaceChildren();
14439
- const first = new Date(year, month, 1);
14440
- const startPad = first.getDay();
14441
- const days = new Date(year, month + 1, 0).getDate();
14442
- const today = isoDate(/* @__PURE__ */ new Date());
14443
- for (let i = 0; i < startPad; i++) {
14444
- const blank = document.createElement("span");
14445
- blank.className = "vela-ind-cal-blank";
14446
- grid.appendChild(blank);
14447
- }
14448
- for (let day = 1; day <= days; day++) {
14449
- const iso = `${year}-${String(month + 1).padStart(2, "0")}-${String(day).padStart(2, "0")}`;
14450
- const b = document.createElement("button");
14451
- b.type = "button";
14452
- b.className = "vela-ind-cal-day";
14453
- b.textContent = String(day);
14454
- if (iso === selected) b.dataset.checked = "1";
14455
- if (iso === today) b.dataset.today = "1";
14456
- b.addEventListener("click", (e) => {
14457
- e.stopPropagation();
14458
- pop.hide();
14459
- onPick(iso);
14460
- });
14461
- grid.appendChild(b);
14462
- }
14463
- };
14464
- prev2.addEventListener("click", (e) => {
14465
- e.stopPropagation();
14466
- month -= 1;
14467
- if (month < 0) {
14468
- month = 11;
14469
- year -= 1;
14470
- }
14471
- paint();
14472
- });
14473
- next.addEventListener("click", (e) => {
14474
- e.stopPropagation();
14475
- month += 1;
14476
- if (month > 11) {
14477
- month = 0;
14478
- year += 1;
14479
- }
14480
- paint();
14481
- });
14482
- paint();
14483
- el.append(head, week, grid);
14484
- }
14450
+ content: (el) => fillCalendar(el, current, (iso) => {
14451
+ pop.hide();
14452
+ onPick(iso);
14453
+ })
14485
14454
  });
14486
14455
  this.calendarPop = pop;
14487
14456
  this.calendarAnchor = anchor;
@@ -14590,7 +14559,7 @@ function groupInputs(inputs) {
14590
14559
  var CALENDAR_SVG = iconAt("calendar", 14);
14591
14560
  var CLOCK_SVG = iconAt("clock", 14);
14592
14561
  var DIALOG_STYLE_ID2 = "vela-ind-dialog-styles";
14593
- var DIALOG_STYLE_REV = "29";
14562
+ var DIALOG_STYLE_REV = "30";
14594
14563
  var LEGEND_ICON_PX = 16;
14595
14564
  function ensureDialogStyles() {
14596
14565
  if (typeof document === "undefined") return;
@@ -14606,8 +14575,12 @@ function ensureDialogStyles() {
14606
14575
  .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;}
14607
14576
  .vela-ind-combo-chevron:hover{opacity:0.9;}
14608
14577
  .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;}
14578
+ .vela-ind-cal [hidden]{display:none !important;}
14609
14579
  .vela-ind-cal-head{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:8px;}
14610
- .vela-ind-cal-title{flex:1;text-align:center;font-weight:600;font-size:14px;color:var(--vela-fg-bright);}
14580
+ .vela-ind-cal-title{flex:1;display:flex;align-items:center;justify-content:center;gap:2px;min-width:0;}
14581
+ .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;}
14582
+ .vela-ind-cal-switch:not(:disabled):hover{background:var(--vela-hover);}
14583
+ .vela-ind-cal-switch:disabled{cursor:default;}
14611
14584
  .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;}
14612
14585
  .vela-ind-cal-nav:hover{background:var(--vela-hover);color:var(--vela-fg-bright);}
14613
14586
  .vela-ind-cal-week,.vela-ind-cal-grid{display:grid;grid-template-columns:repeat(7,28px);gap:2px;}
@@ -14618,6 +14591,12 @@ function ensureDialogStyles() {
14618
14591
  .vela-ind-cal-day:hover{background:var(--vela-hover);}
14619
14592
  .vela-ind-cal-day[data-checked]{background:var(--vela-hover-strong);color:var(--vela-fg-bright);}
14620
14593
  .vela-ind-cal-day[data-today]:not([data-checked]){box-shadow:inset 0 0 0 1px var(--vela-border-strong);}
14594
+ .vela-ind-cal-cells{display:grid;grid-template-columns:repeat(3,1fr);gap:4px;width:calc(7 * 28px + 6 * 2px);}
14595
+ .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;}
14596
+ .vela-ind-cal-cell:hover{background:var(--vela-hover);}
14597
+ .vela-ind-cal-cell[data-checked]{background:var(--vela-hover-strong);color:var(--vela-fg-bright);}
14598
+ .vela-ind-cal-cell[data-today]:not([data-checked]){box-shadow:inset 0 0 0 1px var(--vela-border-strong);}
14599
+ .vela-ind-cal-cell[data-outside]{opacity:0.45;}
14621
14600
  ${overlayScrollbarCss(".vela-dialog.vela-ind-dialog *", 9)}
14622
14601
  .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;}
14623
14602
  .vela-ind-tab:not(.vela-ind-tab-active):hover{color:var(--vela-fg-bright);}
@@ -14652,6 +14631,7 @@ var TIME_OPTIONS = Array.from({ length: 48 }, (_, i) => {
14652
14631
  return { value: v, label: v };
14653
14632
  });
14654
14633
  var MONTH_LABELS = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"];
14634
+ var MONTH_SHORT = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
14655
14635
  var WEEKDAY_LABELS = ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"];
14656
14636
  function normalizeDateInput(raw) {
14657
14637
  const t = raw.trim();
@@ -14673,6 +14653,151 @@ function parseIsoDate(raw) {
14673
14653
  function isoDate(d) {
14674
14654
  return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
14675
14655
  }
14656
+ function fillCalendar(el, current, onPick) {
14657
+ const today = /* @__PURE__ */ new Date();
14658
+ const selected = parseIsoDate(current);
14659
+ let year = selected?.getFullYear() ?? today.getFullYear();
14660
+ let month = selected?.getMonth() ?? today.getMonth();
14661
+ let mode = "date";
14662
+ const prev2 = document.createElement("button");
14663
+ prev2.type = "button";
14664
+ prev2.className = "vela-ind-cal-nav";
14665
+ prev2.innerHTML = iconAt("chevron-left", 14);
14666
+ const next = document.createElement("button");
14667
+ next.type = "button";
14668
+ next.className = "vela-ind-cal-nav";
14669
+ next.innerHTML = iconAt("chevron-right", 14);
14670
+ const monthBtn = document.createElement("button");
14671
+ monthBtn.type = "button";
14672
+ monthBtn.className = "vela-ind-cal-switch";
14673
+ monthBtn.setAttribute("aria-label", "Choose month");
14674
+ const yearBtn = document.createElement("button");
14675
+ yearBtn.type = "button";
14676
+ yearBtn.className = "vela-ind-cal-switch";
14677
+ yearBtn.setAttribute("aria-label", "Choose year");
14678
+ const title = document.createElement("div");
14679
+ title.className = "vela-ind-cal-title";
14680
+ title.append(monthBtn, yearBtn);
14681
+ const head = document.createElement("div");
14682
+ head.className = "vela-ind-cal-head";
14683
+ head.append(prev2, title, next);
14684
+ const week = document.createElement("div");
14685
+ week.className = "vela-ind-cal-week";
14686
+ for (const d of WEEKDAY_LABELS) {
14687
+ const cell = document.createElement("span");
14688
+ cell.textContent = d;
14689
+ week.appendChild(cell);
14690
+ }
14691
+ const grid = document.createElement("div");
14692
+ const paint = () => {
14693
+ const decade = Math.floor(year / 10) * 10;
14694
+ monthBtn.hidden = mode !== "date";
14695
+ monthBtn.textContent = MONTH_LABELS[month] ?? "";
14696
+ yearBtn.textContent = mode === "year" ? `${decade}-${decade + 9}` : String(year);
14697
+ yearBtn.disabled = mode === "year";
14698
+ week.hidden = mode !== "date";
14699
+ prev2.setAttribute("aria-label", mode === "date" ? "Previous month" : mode === "month" ? "Previous year" : "Previous decade");
14700
+ next.setAttribute("aria-label", mode === "date" ? "Next month" : mode === "month" ? "Next year" : "Next decade");
14701
+ grid.className = mode === "date" ? "vela-ind-cal-grid" : "vela-ind-cal-cells";
14702
+ grid.replaceChildren();
14703
+ if (mode === "date") {
14704
+ const startPad = new Date(year, month, 1).getDay();
14705
+ const days = new Date(year, month + 1, 0).getDate();
14706
+ const todayIso = isoDate(today);
14707
+ const selectedIso = selected ? isoDate(selected) : "";
14708
+ for (let i = 0; i < startPad; i++) {
14709
+ const blank = document.createElement("span");
14710
+ blank.className = "vela-ind-cal-blank";
14711
+ grid.appendChild(blank);
14712
+ }
14713
+ for (let day = 1; day <= days; day++) {
14714
+ const iso = `${year}-${String(month + 1).padStart(2, "0")}-${String(day).padStart(2, "0")}`;
14715
+ const b = document.createElement("button");
14716
+ b.type = "button";
14717
+ b.className = "vela-ind-cal-day";
14718
+ b.textContent = String(day);
14719
+ if (iso === selectedIso) b.dataset.checked = "1";
14720
+ if (iso === todayIso) b.dataset.today = "1";
14721
+ b.addEventListener("click", (e) => {
14722
+ e.stopPropagation();
14723
+ onPick(iso);
14724
+ });
14725
+ grid.appendChild(b);
14726
+ }
14727
+ return;
14728
+ }
14729
+ if (mode === "month") {
14730
+ for (let m = 0; m < 12; m++) {
14731
+ const b = document.createElement("button");
14732
+ b.type = "button";
14733
+ b.className = "vela-ind-cal-cell";
14734
+ b.textContent = MONTH_SHORT[m] ?? "";
14735
+ if (selected?.getFullYear() === year && selected.getMonth() === m) b.dataset.checked = "1";
14736
+ if (today.getFullYear() === year && today.getMonth() === m) b.dataset.today = "1";
14737
+ b.addEventListener("click", (e) => {
14738
+ e.stopPropagation();
14739
+ month = m;
14740
+ mode = "date";
14741
+ paint();
14742
+ });
14743
+ grid.appendChild(b);
14744
+ }
14745
+ return;
14746
+ }
14747
+ for (let y = decade - 1; y <= decade + 10; y++) {
14748
+ const b = document.createElement("button");
14749
+ b.type = "button";
14750
+ b.className = "vela-ind-cal-cell";
14751
+ b.textContent = String(y);
14752
+ if (y < decade || y > decade + 9) b.dataset.outside = "1";
14753
+ if (selected?.getFullYear() === y) b.dataset.checked = "1";
14754
+ if (today.getFullYear() === y) b.dataset.today = "1";
14755
+ b.addEventListener("click", (e) => {
14756
+ e.stopPropagation();
14757
+ year = y;
14758
+ mode = "month";
14759
+ paint();
14760
+ });
14761
+ grid.appendChild(b);
14762
+ }
14763
+ };
14764
+ const step = (dir) => {
14765
+ if (mode === "date") {
14766
+ month += dir;
14767
+ if (month < 0) {
14768
+ month = 11;
14769
+ year -= 1;
14770
+ }
14771
+ if (month > 11) {
14772
+ month = 0;
14773
+ year += 1;
14774
+ }
14775
+ } else {
14776
+ year += mode === "month" ? dir : dir * 10;
14777
+ }
14778
+ paint();
14779
+ };
14780
+ prev2.addEventListener("click", (e) => {
14781
+ e.stopPropagation();
14782
+ step(-1);
14783
+ });
14784
+ next.addEventListener("click", (e) => {
14785
+ e.stopPropagation();
14786
+ step(1);
14787
+ });
14788
+ monthBtn.addEventListener("click", (e) => {
14789
+ e.stopPropagation();
14790
+ mode = "month";
14791
+ paint();
14792
+ });
14793
+ yearBtn.addEventListener("click", (e) => {
14794
+ e.stopPropagation();
14795
+ mode = "year";
14796
+ paint();
14797
+ });
14798
+ paint();
14799
+ el.append(head, week, grid);
14800
+ }
14676
14801
  function normalizeTimeInput(raw) {
14677
14802
  const t = raw.trim();
14678
14803
  const m = /^(\d{1,2}):(\d{2})$/.exec(t) ?? /^(\d{2})(\d{2})$/.exec(t);
@@ -15954,10 +16079,12 @@ var BASELINE_LEVEL_DEFAULT = 50;
15954
16079
  var PREMARKET_SHADE = withAlpha(WARNING, 0.08);
15955
16080
  var POSTMARKET_SHADE = withAlpha(ACCENT, 0.08);
15956
16081
  var EXTENDED_SHADE = POSTMARKET_SHADE;
16082
+ var DEFAULT_MARGINS = { top: 10, bottom: 10, right: 10 };
15957
16083
  function defaultChartStyle() {
15958
16084
  return {
15959
16085
  chartTypes: {},
15960
16086
  fontSize: 11,
16087
+ margins: { ...DEFAULT_MARGINS },
15961
16088
  gridVert: { visible: true, color: null },
15962
16089
  gridHorz: { visible: true, color: null },
15963
16090
  borderColor: null,
@@ -16081,6 +16208,12 @@ function clampLevel(v) {
16081
16208
  function clampSpacing(v) {
16082
16209
  return v < 0.1 ? 0.1 : v > 10 ? 10 : v;
16083
16210
  }
16211
+ function clampMarginPct(v) {
16212
+ return v < 0 ? 0 : v > 40 ? 40 : v;
16213
+ }
16214
+ function clampMarginBars(v) {
16215
+ return Math.round(v < 0 ? 0 : v > 200 ? 200 : v);
16216
+ }
16084
16217
  function factoryResetConfig(factory, priceStyle = factory.series.style) {
16085
16218
  const bag = {};
16086
16219
  for (const t of chartTypes()) {
@@ -16129,6 +16262,7 @@ function mergeConfig(base, patch) {
16129
16262
  const ps = asObject(p.priceScale);
16130
16263
  const anim = asObject(p.animations);
16131
16264
  const panes = asObject(p.panes);
16265
+ const margins = asObject(p.margins);
16132
16266
  const trades = asObject(p.trades);
16133
16267
  const ts = asObject(p.timeScale);
16134
16268
  const marks = asObject(p.marks);
@@ -16188,6 +16322,11 @@ function mergeConfig(base, patch) {
16188
16322
  panes: {
16189
16323
  separatorColor: isColor(panes.separatorColor) ? panes.separatorColor : base.panes.separatorColor
16190
16324
  },
16325
+ margins: {
16326
+ top: isNum(margins.top) ? clampMarginPct(margins.top) : base.margins.top,
16327
+ bottom: isNum(margins.bottom) ? clampMarginPct(margins.bottom) : base.margins.bottom,
16328
+ right: isNum(margins.right) ? clampMarginBars(margins.right) : base.margins.right
16329
+ },
16191
16330
  trades: {
16192
16331
  visible: isBool(trades.visible) ? trades.visible : base.trades.visible,
16193
16332
  labels: isBool(trades.labels) ? trades.labels : base.trades.labels,
@@ -21558,6 +21697,10 @@ var BUILTIN_SETTINGS_IDS = [
21558
21697
  "canvas.grid",
21559
21698
  "canvas.grid.vertical",
21560
21699
  "canvas.grid.horizontal",
21700
+ "canvas.margins",
21701
+ "canvas.margins.top",
21702
+ "canvas.margins.bottom",
21703
+ "canvas.margins.right",
21561
21704
  "canvas.theme"
21562
21705
  ];
21563
21706
  function settingsIdCatalog(hostSections, markGroups = []) {
@@ -22088,6 +22231,10 @@ var SettingsDialog = class {
22088
22231
  body.append(sid(this.toggleRow("Horizontal", config.grid.horzLines.visible, (v) => this.emit({ grid: { horzLines: { visible: v } } }), [
22089
22232
  this.swatch(config.grid.horzLines.color, (v) => this.emit({ grid: { horzLines: { color: v } } }))
22090
22233
  ]), "canvas.grid.horizontal"));
22234
+ body.append(sid(this.sectionTitle("Margins"), "canvas.margins"));
22235
+ body.append(sid(this.numberRow("Top", config.margins.top, 0, 40, 1, (v) => this.emit({ margins: { top: v } }), "%"), "canvas.margins.top"));
22236
+ body.append(sid(this.numberRow("Bottom", config.margins.bottom, 0, 40, 1, (v) => this.emit({ margins: { bottom: v } }), "%"), "canvas.margins.bottom"));
22237
+ body.append(sid(this.numberRow("Right", config.margins.right, 0, 200, 1, (v) => this.emit({ margins: { right: v } }), "bars"), "canvas.margins.right"));
22091
22238
  if (this.themeControl) {
22092
22239
  const tc = this.themeControl;
22093
22240
  body.append(sid(this.sectionTitle("Theme"), "canvas.theme"));
@@ -22659,23 +22806,16 @@ var SettingsDialog = class {
22659
22806
  this.hintTips.push(dispose);
22660
22807
  return el;
22661
22808
  }
22662
- numberRow(label, value, min, max, step, onChange) {
22663
- return fieldRow({
22664
- label,
22665
- labelSize: "sm",
22666
- className: "vela-sd-row",
22667
- control: buildFieldControl({
22668
- kind: "number",
22669
- value,
22670
- min,
22671
- max,
22672
- step,
22673
- fill: false,
22674
- commit: "live",
22675
- steppers: true,
22676
- onChange
22677
- }).el
22678
- });
22809
+ /** `unit` (optional) trails the input as muted text — "%", "bars". */
22810
+ numberRow(label, value, min, max, step, onChange, unit) {
22811
+ const controls = [buildFieldControl({ kind: "number", value, min, max, step, fill: false, commit: "live", steppers: true, onChange }).el];
22812
+ if (unit) {
22813
+ const u = document.createElement("span");
22814
+ u.textContent = unit;
22815
+ u.style.cssText = "color:var(--vela-fg-muted);";
22816
+ controls.push(u);
22817
+ }
22818
+ return this.rowWith(label, controls);
22679
22819
  }
22680
22820
  /** A dropdown whose option values differ from their display labels. */
22681
22821
  selectRowLabeled(label, value, options, onChange) {
@@ -28194,9 +28334,7 @@ function createProjector(coords, paneOf, paneIdAtY, barsInRange, seriesInRange)
28194
28334
  }
28195
28335
 
28196
28336
  // src/renderers/native/core/autoscale.ts
28197
- var MARGIN_TOP = 2 / 7;
28198
- var MARGIN_BOTTOM = 1 / 7;
28199
- function computePaneScale(models, bars, includeCandles, i0, i1, drawings, log = false, offsetOf = () => 0) {
28337
+ function computePaneScale(models, bars, includeCandles, i0, i1, drawings, log = false, offsetOf = () => 0, margins = DEFAULT_MARGINS) {
28200
28338
  let min = Infinity;
28201
28339
  let max = -Infinity;
28202
28340
  const consider = (v) => {
@@ -28231,14 +28369,17 @@ function computePaneScale(models, bars, includeCandles, i0, i1, drawings, log =
28231
28369
  const pad = Math.abs(min) * 0.1 || 1;
28232
28370
  return { min: min - pad, max: max + pad, log: log && min - pad > 0 };
28233
28371
  }
28372
+ const content = Math.max(0.1, 1 - (margins.top + margins.bottom) / 100);
28373
+ const above = margins.top / 100 / content;
28374
+ const below = margins.bottom / 100 / content;
28234
28375
  if (log && min > 0) {
28235
28376
  const lmin = Math.log(min);
28236
28377
  const lmax = Math.log(max);
28237
28378
  const lspan = lmax - lmin;
28238
- return { min: Math.exp(lmin - lspan * MARGIN_BOTTOM), max: Math.exp(lmax + lspan * MARGIN_TOP), log: true };
28379
+ return { min: Math.exp(lmin - lspan * below), max: Math.exp(lmax + lspan * above), log: true };
28239
28380
  }
28240
28381
  const span = max - min;
28241
- return { min: min - span * MARGIN_BOTTOM, max: max + span * MARGIN_TOP };
28382
+ return { min: min - span * below, max: max + span * above };
28242
28383
  }
28243
28384
  function considerSeries(s, i0, i1, off, consider) {
28244
28385
  if (!seriesInScale(s)) return;
@@ -30065,6 +30206,7 @@ var NativeRenderer = class {
30065
30206
  intro: this.intro.style !== false
30066
30207
  },
30067
30208
  panes: { separatorColor: s.separatorColor ?? t.borderColor },
30209
+ margins: { ...s.margins },
30068
30210
  trades: {
30069
30211
  visible: this.scene.tradeMarkers.visible,
30070
30212
  labels: this.scene.tradeMarkers.labels,
@@ -30177,6 +30319,10 @@ var NativeRenderer = class {
30177
30319
  this.animAutoscale.toggle(next.animations.autoscale);
30178
30320
  this.intro = { style: next.animations.intro ? this.introOnStyle : false, duration: this.intro.duration || INTRO_DURATION_DEFAULT_MS };
30179
30321
  s.separatorColor = keepInherit(s.separatorColor, next.panes.separatorColor, prevTheme.borderColor);
30322
+ if (next.margins.right !== s.margins.right && this.coords.barCount > 0) {
30323
+ this.applyViewport({ barSpacing: this.coords.getViewport().barSpacing, rightOffset: next.margins.right });
30324
+ }
30325
+ s.margins = { ...next.margins };
30180
30326
  this.scene.tradeMarkers = {
30181
30327
  visible: next.trades.visible,
30182
30328
  labels: next.trades.labels,
@@ -30420,7 +30566,7 @@ var NativeRenderer = class {
30420
30566
  /** Glide the view back to the most recent bars, keeping the current zoom (barSpacing). */
30421
30567
  scrollToRealtime() {
30422
30568
  if (this.coords.barCount === 0) return;
30423
- this.glideRightOffset(ZOOM_OUT_MARGIN_BARS);
30569
+ this.glideRightOffset(this.scene.style.margins.right);
30424
30570
  }
30425
30571
  /** Ease rightOffset to `target` at constant zoom (see animTick's scroll glide);
30426
30572
  * instant when the scroll glide is off. Shared by scroll-to-latest and panBy. */
@@ -32362,6 +32508,7 @@ var NativeRenderer = class {
32362
32508
  const pricePane = panes.find((p) => p.kind === "price") ?? null;
32363
32509
  this.chrome.prepare(this.scene, this.coords, this.theme);
32364
32510
  const animating = this.animator.active;
32511
+ const margins = this.scene.style.margins;
32365
32512
  for (const pane of panes) {
32366
32513
  if (pane.manualScale) {
32367
32514
  pane.scaleTarget = pane.manualScale;
@@ -32377,7 +32524,7 @@ var NativeRenderer = class {
32377
32524
  if (or) dr = dr ? { min: Math.min(dr.min, or.min), max: Math.max(dr.max, or.max) } : or;
32378
32525
  }
32379
32526
  const includeCandles = pane.kind === "price" && (!this.scene.candlesHidden || this.priceLayersAnchoredToBars(masterModels) || !this.paneHasMeasurableContent(masterModels, dr));
32380
- pane.scaleTarget = computePaneScale(masterModels, this.bars, includeCandles, i0, i1, dr, paneLogScale(this.scene, pane), (id) => this.scene.offsetOf(id));
32527
+ pane.scaleTarget = computePaneScale(masterModels, this.bars, includeCandles, i0, i1, dr, paneLogScale(this.scene, pane), (id) => this.scene.offsetOf(id), margins);
32381
32528
  pane.percentBaseline = pane.kind === "price" ? this.bars[i0]?.close ?? 0 : this.firstVisibleValue(masterModels, i0);
32382
32529
  pane.axisFormat = void 0;
32383
32530
  pane.axisBands = void 0;
@@ -32388,7 +32535,7 @@ var NativeRenderer = class {
32388
32535
  pane.axisFormat = "volume";
32389
32536
  }
32390
32537
  } else if (this.layerNativesOwnPane(pane, masterModels)) {
32391
- pane.scaleTarget = computePaneScale([], this.bars, true, i0, i1, dr, paneLogScale(this.scene, pane), (id) => this.scene.offsetOf(id));
32538
+ pane.scaleTarget = computePaneScale([], this.bars, true, i0, i1, dr, paneLogScale(this.scene, pane), (id) => this.scene.offsetOf(id), margins);
32392
32539
  pane.percentBaseline = this.bars[i0]?.close ?? 0;
32393
32540
  if (masterModels.every((m) => m.paneAxis != null)) {
32394
32541
  pane.axisFormat = "none";
@@ -32417,7 +32564,7 @@ var NativeRenderer = class {
32417
32564
  continue;
32418
32565
  }
32419
32566
  const mdr = this.chrome.paneDrawingsRange([model], this.scene, false, vr);
32420
- sl.scaleTarget = computePaneScale([model], this.bars, false, i0, i1, mdr, false, (id) => this.scene.offsetOf(id));
32567
+ sl.scaleTarget = computePaneScale([model], this.bars, false, i0, i1, mdr, false, (id) => this.scene.offsetOf(id), margins);
32421
32568
  if (!animating || !sl.initialized) {
32422
32569
  sl.scale = { ...sl.scaleTarget };
32423
32570
  sl.initialized = true;
@@ -32458,13 +32605,13 @@ var NativeRenderer = class {
32458
32605
  for (const pane of this.scene.panes.values()) pane.manualScale = null;
32459
32606
  for (const sl of this.scene.indicatorScales.values()) sl.manualScale = null;
32460
32607
  const visibleBars = Math.min(n, 200);
32461
- const rightOffset = 6;
32608
+ const rightOffset = this.scene.style.margins.right;
32462
32609
  const v = this.clampViewport(w / ((visibleBars + rightOffset) * this.coords.spacingScale), rightOffset);
32463
32610
  this.coords.setViewport(v);
32464
32611
  this.targetBarSpacing = v.barSpacing;
32465
32612
  }
32466
32613
  /** Re-frame after a series replacement (a symbol/timeframe switch): keep the user's
32467
- * zoom (bar spacing), re-anchor the newest bars at the default right offset.
32614
+ * zoom (bar spacing), re-anchor the newest bars at the configured right margin.
32468
32615
  * `clampViewport`'s fit-all-bars floor deliberately does NOT apply — a progressive
32469
32616
  * head may still be backfilling toward the previous depth, and raising the spacing
32470
32617
  * to its temporary bar count would lose the zoom this exists to keep. */
@@ -32473,7 +32620,7 @@ var NativeRenderer = class {
32473
32620
  this.panVelocity = 0;
32474
32621
  for (const pane of this.scene.panes.values()) pane.manualScale = null;
32475
32622
  for (const sl of this.scene.indicatorScales.values()) sl.manualScale = null;
32476
- const v = { barSpacing: clampBarSpacing(this.coords.getViewport().barSpacing), rightOffset: defaultViewport().rightOffset };
32623
+ const v = { barSpacing: clampBarSpacing(this.coords.getViewport().barSpacing), rightOffset: this.scene.style.margins.right };
32477
32624
  this.coords.setViewport(v);
32478
32625
  this.targetBarSpacing = v.barSpacing;
32479
32626
  }
@@ -35587,6 +35734,7 @@ function registerClassicIndicators() {
35587
35734
  }
35588
35735
 
35589
35736
  // src/Vela.ts
35737
+ var asError = (err) => err instanceof Error ? err : new Error(String(err));
35590
35738
  var Vela = class {
35591
35739
  constructor(container, options = {}, deps = {}) {
35592
35740
  registerBuiltinChartTypes();
@@ -35710,7 +35858,12 @@ var Vela = class {
35710
35858
  * fetch from — the same relationship `script:run` has to `context:changed`.
35711
35859
  */
35712
35860
  runScript(source, options) {
35713
- const handle = this.addIndicator(source, options);
35861
+ let handle;
35862
+ try {
35863
+ handle = this.addIndicator(source, options);
35864
+ } catch (err) {
35865
+ return Promise.resolve({ ok: false, run: null, error: asError(err), onUpdate: () => () => void 0, remove: () => void 0 });
35866
+ }
35714
35867
  const updates = /* @__PURE__ */ new Set();
35715
35868
  const drop = () => {
35716
35869
  try {
@@ -35754,7 +35907,12 @@ var Vela = class {
35754
35907
  });
35755
35908
  }
35756
35909
  runIndicator(source, options) {
35757
- const handle = this.addIndicator(source, options);
35910
+ let handle;
35911
+ try {
35912
+ handle = this.addIndicator(source, options);
35913
+ } catch (err) {
35914
+ return Promise.resolve({ ok: false, handle: null, error: asError(err), context: null });
35915
+ }
35758
35916
  return new Promise((resolve) => {
35759
35917
  const offReady = handle.on("ready", () => {
35760
35918
  offReady();