@luxalgo/vela 0.7.6 → 0.7.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,4 @@
1
- import { normalizeProps, nextUid, runMachine, spreadProps } from './chunk-BKHSQ4YM.js';
1
+ import { normalizeProps, nextUid, runMachine, spreadProps } from './chunk-WTWGEB7K.js';
2
2
  import { injectStyles } from './chunk-CAFCLMPF.js';
3
3
  import * as tooltip from '@zag-js/tooltip';
4
4
  import * as dialog from '@zag-js/dialog';
@@ -1,5 +1,5 @@
1
1
  import { deserializeDrawing, chartTypes, rendererLayers, isLineLikeSeries, seriesShownOn, foldBaseModulation, registerChartType, rendererDefaults, getDrawingType, settingsRowVisible, normalizeSettingsRow, Magnifier, TextLabel, Callout, resetDrawingSettings, registerNativeIndicator, chartType, getNativeIndicator, nativeIndicatorDescriptors, nativeInstanceChannel, drawingTypes, settingsRowValueKeys, formatDuration, RegressionChannel, FixedRangeVolumeProfile, DEFAULT_DRAWING_COLOR, SegmentDrawing, ArrowMark, GlyphStamp, RadialFib, FibSpiral, DedekindTessellation, MachFigure, GannSquare, FibRatios, MeasureBox, PositionTool, AnchoredVwap, PatternDrawing, Comment, PriceNote, Signpost, Note, PriceLabel, GANN_SQUARE_ARCS, magnifierTimeframeLabel, lineSegmentIntersection, MAGNIFIER_TIMEFRAME_OPTIONS, GLYPH_OPTIONS, STAMP_SIZE_OPTIONS, LINE_STYLE_OPTIONS, DEDEKIND_CURVATURE_OPTIONS, MACH_NUMBER_OPTIONS, MACH_WAVE_COUNT_OPTIONS, TEXT_SIZE_OPTIONS, createDrawing, inputVisible, seriesInScale, tickerModifierIds, CalloutBase, DIRECTION_OPTIONS, stableSeriesId } from './chunk-BFA32GOU.js';
2
- import { themeTokens, Dialog, closeOpenPopovers, closeWidthPopover, fieldSection, buildFieldControl, fieldSeparator, fieldGrid, fieldRow, CALLOUT_STYLE_ID, CALLOUT_CSS, Popover, blendOver, splitColor, CalloutBubble, Menu, TextArea, NumberInput, buildColorPicker, isPopoverOpen, eventDismissedPopover, toggleSelectList, openPopoverTrigger, fieldGridColumns, FIELD_GAP_PX, STATIC_TOKENS } from './chunk-BKHSQ4YM.js';
2
+ import { themeTokens, Dialog, closeOpenPopovers, closeWidthPopover, fieldSection, buildFieldControl, fieldSeparator, fieldGrid, fieldRow, CALLOUT_STYLE_ID, CALLOUT_CSS, Popover, blendOver, splitColor, CalloutBubble, Menu, TextArea, NumberInput, buildColorPicker, isPopoverOpen, eventDismissedPopover, toggleSelectList, openPopoverTrigger, fieldGridColumns, FIELD_GAP_PX, STATIC_TOKENS } from './chunk-WTWGEB7K.js';
3
3
  import { icon, iconAt, withAlpha, WARNING, ACCENT, NEUTRAL, BULLISH, CATEGORICAL, SERIES_LINE, BEARISH, isDarkColor, INFO, injectStyles, CHIP_PLATE, CROSSHAIR, TRADE_EXIT, TRADE_SHORT, TRADE_LONG, iconMarkup, overlayScrollbarCss, SLATE_DEEP, VALID, INVALID, SLATE, FIELD_FOCUS_CSS, FIELD_FOCUS_RING, svg24, svg24Solid } from './chunk-CAFCLMPF.js';
4
4
 
5
5
  // src/core/events/EventBus.ts
@@ -1845,9 +1845,22 @@ var TIMEZONES = [
1845
1845
  { value: "Pacific/Apia", label: "Apia" },
1846
1846
  { value: "Pacific/Kiritimati", label: "Kiritimati" }
1847
1847
  ];
1848
+ var EXCHANGE_TIMEZONE = "exchange";
1849
+ function isExchangeTimezone(zone) {
1850
+ return zone === EXCHANGE_TIMEZONE;
1851
+ }
1852
+ function resolveTimezone(zone, exchangeZone) {
1853
+ if (!isExchangeTimezone(zone)) return zone;
1854
+ return exchangeZone && exchangeZone !== "" ? exchangeZone : "Etc/UTC";
1855
+ }
1848
1856
  function normalizeTimezone(zone) {
1849
1857
  return zone === "UTC" || zone === "Etc/UTC" || zone === "Etc/GMT" ? "Etc/UTC" : zone;
1850
1858
  }
1859
+ function timezoneMenuRows(current) {
1860
+ const active = normalizeTimezone(current);
1861
+ const [utc, ...zones] = TIMEZONES.map((t) => ({ value: t.value, label: tzMenuLabel(t.value, t.label), checked: t.value === active }));
1862
+ return [utc, { value: EXCHANGE_TIMEZONE, label: "Exchange", checked: isExchangeTimezone(current) }, ...zones];
1863
+ }
1851
1864
  function tzOffset(zone, date = /* @__PURE__ */ new Date()) {
1852
1865
  try {
1853
1866
  const parts = new Intl.DateTimeFormat("en-US", { timeZone: zone, timeZoneName: "shortOffset" }).formatToParts(date);
@@ -3503,7 +3516,10 @@ var IndicatorInputsDialog = class {
3503
3516
  id,
3504
3517
  theme: this.host.theme(),
3505
3518
  get: () => String(bagOf(row, inp)[inp.key] ?? inp.defval),
3506
- onChange: (v) => emit(v)
3519
+ onChange: (v) => emit(v),
3520
+ // An input change re-executes the script over its whole history: commit the
3521
+ // opacity drag once on release, not once per pointer move.
3522
+ commit: "release"
3507
3523
  }).el;
3508
3524
  }
3509
3525
  if (inp.type === "symbol") return this.buildSymbol(id, String(current), emit);
@@ -3750,10 +3766,6 @@ var IndicatorInputsDialog = class {
3750
3766
  }
3751
3767
  /** Open a themed month calendar under `anchor` — same surface + shadow as the choice list. */
3752
3768
  openCalendar(anchor, current, onPick) {
3753
- const parsed = parseIsoDate(current);
3754
- let year = parsed?.getFullYear() ?? (/* @__PURE__ */ new Date()).getFullYear();
3755
- let month = parsed?.getMonth() ?? (/* @__PURE__ */ new Date()).getMonth();
3756
- const selected = parsed ? isoDate(parsed) : current;
3757
3769
  const pop = new Popover({
3758
3770
  trigger: anchor,
3759
3771
  theme: this.host.theme(),
@@ -3765,80 +3777,10 @@ var IndicatorInputsDialog = class {
3765
3777
  this.calendarPop = null;
3766
3778
  this.calendarAnchor = null;
3767
3779
  },
3768
- content: (el) => {
3769
- const title = document.createElement("div");
3770
- title.className = "vela-ind-cal-title";
3771
- const prev2 = document.createElement("button");
3772
- prev2.type = "button";
3773
- prev2.className = "vela-ind-cal-nav";
3774
- prev2.setAttribute("aria-label", "Previous month");
3775
- prev2.innerHTML = iconAt("chevron-left", 14);
3776
- const next = document.createElement("button");
3777
- next.type = "button";
3778
- next.className = "vela-ind-cal-nav";
3779
- next.setAttribute("aria-label", "Next month");
3780
- next.innerHTML = iconAt("chevron-right", 14);
3781
- const head = document.createElement("div");
3782
- head.className = "vela-ind-cal-head";
3783
- head.append(prev2, title, next);
3784
- const week = document.createElement("div");
3785
- week.className = "vela-ind-cal-week";
3786
- for (const d of WEEKDAY_LABELS) {
3787
- const cell = document.createElement("span");
3788
- cell.textContent = d;
3789
- week.appendChild(cell);
3790
- }
3791
- const grid = document.createElement("div");
3792
- grid.className = "vela-ind-cal-grid";
3793
- const paint = () => {
3794
- title.textContent = `${MONTH_LABELS[month]} ${year}`;
3795
- grid.replaceChildren();
3796
- const first = new Date(year, month, 1);
3797
- const startPad = first.getDay();
3798
- const days = new Date(year, month + 1, 0).getDate();
3799
- const today = isoDate(/* @__PURE__ */ new Date());
3800
- for (let i = 0; i < startPad; i++) {
3801
- const blank = document.createElement("span");
3802
- blank.className = "vela-ind-cal-blank";
3803
- grid.appendChild(blank);
3804
- }
3805
- for (let day = 1; day <= days; day++) {
3806
- const iso = `${year}-${String(month + 1).padStart(2, "0")}-${String(day).padStart(2, "0")}`;
3807
- const b = document.createElement("button");
3808
- b.type = "button";
3809
- b.className = "vela-ind-cal-day";
3810
- b.textContent = String(day);
3811
- if (iso === selected) b.dataset.checked = "1";
3812
- if (iso === today) b.dataset.today = "1";
3813
- b.addEventListener("click", (e) => {
3814
- e.stopPropagation();
3815
- pop.hide();
3816
- onPick(iso);
3817
- });
3818
- grid.appendChild(b);
3819
- }
3820
- };
3821
- prev2.addEventListener("click", (e) => {
3822
- e.stopPropagation();
3823
- month -= 1;
3824
- if (month < 0) {
3825
- month = 11;
3826
- year -= 1;
3827
- }
3828
- paint();
3829
- });
3830
- next.addEventListener("click", (e) => {
3831
- e.stopPropagation();
3832
- month += 1;
3833
- if (month > 11) {
3834
- month = 0;
3835
- year += 1;
3836
- }
3837
- paint();
3838
- });
3839
- paint();
3840
- el.append(head, week, grid);
3841
- }
3780
+ content: (el) => fillCalendar(el, current, (iso) => {
3781
+ pop.hide();
3782
+ onPick(iso);
3783
+ })
3842
3784
  });
3843
3785
  this.calendarPop = pop;
3844
3786
  this.calendarAnchor = anchor;
@@ -3947,7 +3889,7 @@ function groupInputs(inputs) {
3947
3889
  var CALENDAR_SVG = iconAt("calendar", 14);
3948
3890
  var CLOCK_SVG = iconAt("clock", 14);
3949
3891
  var DIALOG_STYLE_ID = "vela-ind-dialog-styles";
3950
- var DIALOG_STYLE_REV = "29";
3892
+ var DIALOG_STYLE_REV = "30";
3951
3893
  var LEGEND_ICON_PX = 16;
3952
3894
  function ensureDialogStyles() {
3953
3895
  if (typeof document === "undefined") return;
@@ -3963,8 +3905,12 @@ function ensureDialogStyles() {
3963
3905
  .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;}
3964
3906
  .vela-ind-combo-chevron:hover{opacity:0.9;}
3965
3907
  .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;}
3908
+ .vela-ind-cal [hidden]{display:none !important;}
3966
3909
  .vela-ind-cal-head{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:8px;}
3967
- .vela-ind-cal-title{flex:1;text-align:center;font-weight:600;font-size:14px;color:var(--vela-fg-bright);}
3910
+ .vela-ind-cal-title{flex:1;display:flex;align-items:center;justify-content:center;gap:2px;min-width:0;}
3911
+ .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;}
3912
+ .vela-ind-cal-switch:not(:disabled):hover{background:var(--vela-hover);}
3913
+ .vela-ind-cal-switch:disabled{cursor:default;}
3968
3914
  .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;}
3969
3915
  .vela-ind-cal-nav:hover{background:var(--vela-hover);color:var(--vela-fg-bright);}
3970
3916
  .vela-ind-cal-week,.vela-ind-cal-grid{display:grid;grid-template-columns:repeat(7,28px);gap:2px;}
@@ -3975,6 +3921,12 @@ function ensureDialogStyles() {
3975
3921
  .vela-ind-cal-day:hover{background:var(--vela-hover);}
3976
3922
  .vela-ind-cal-day[data-checked]{background:var(--vela-hover-strong);color:var(--vela-fg-bright);}
3977
3923
  .vela-ind-cal-day[data-today]:not([data-checked]){box-shadow:inset 0 0 0 1px var(--vela-border-strong);}
3924
+ .vela-ind-cal-cells{display:grid;grid-template-columns:repeat(3,1fr);gap:4px;width:calc(7 * 28px + 6 * 2px);}
3925
+ .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;}
3926
+ .vela-ind-cal-cell:hover{background:var(--vela-hover);}
3927
+ .vela-ind-cal-cell[data-checked]{background:var(--vela-hover-strong);color:var(--vela-fg-bright);}
3928
+ .vela-ind-cal-cell[data-today]:not([data-checked]){box-shadow:inset 0 0 0 1px var(--vela-border-strong);}
3929
+ .vela-ind-cal-cell[data-outside]{opacity:0.45;}
3978
3930
  ${overlayScrollbarCss(".vela-dialog.vela-ind-dialog *", 9)}
3979
3931
  .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;}
3980
3932
  .vela-ind-tab:not(.vela-ind-tab-active):hover{color:var(--vela-fg-bright);}
@@ -4009,6 +3961,7 @@ var TIME_OPTIONS = Array.from({ length: 48 }, (_, i) => {
4009
3961
  return { value: v, label: v };
4010
3962
  });
4011
3963
  var MONTH_LABELS = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"];
3964
+ var MONTH_SHORT = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
4012
3965
  var WEEKDAY_LABELS = ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"];
4013
3966
  function normalizeDateInput(raw) {
4014
3967
  const t = raw.trim();
@@ -4030,6 +3983,151 @@ function parseIsoDate(raw) {
4030
3983
  function isoDate(d) {
4031
3984
  return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
4032
3985
  }
3986
+ function fillCalendar(el, current, onPick) {
3987
+ const today = /* @__PURE__ */ new Date();
3988
+ const selected = parseIsoDate(current);
3989
+ let year = selected?.getFullYear() ?? today.getFullYear();
3990
+ let month = selected?.getMonth() ?? today.getMonth();
3991
+ let mode = "date";
3992
+ const prev2 = document.createElement("button");
3993
+ prev2.type = "button";
3994
+ prev2.className = "vela-ind-cal-nav";
3995
+ prev2.innerHTML = iconAt("chevron-left", 14);
3996
+ const next = document.createElement("button");
3997
+ next.type = "button";
3998
+ next.className = "vela-ind-cal-nav";
3999
+ next.innerHTML = iconAt("chevron-right", 14);
4000
+ const monthBtn = document.createElement("button");
4001
+ monthBtn.type = "button";
4002
+ monthBtn.className = "vela-ind-cal-switch";
4003
+ monthBtn.setAttribute("aria-label", "Choose month");
4004
+ const yearBtn = document.createElement("button");
4005
+ yearBtn.type = "button";
4006
+ yearBtn.className = "vela-ind-cal-switch";
4007
+ yearBtn.setAttribute("aria-label", "Choose year");
4008
+ const title = document.createElement("div");
4009
+ title.className = "vela-ind-cal-title";
4010
+ title.append(monthBtn, yearBtn);
4011
+ const head = document.createElement("div");
4012
+ head.className = "vela-ind-cal-head";
4013
+ head.append(prev2, title, next);
4014
+ const week = document.createElement("div");
4015
+ week.className = "vela-ind-cal-week";
4016
+ for (const d of WEEKDAY_LABELS) {
4017
+ const cell = document.createElement("span");
4018
+ cell.textContent = d;
4019
+ week.appendChild(cell);
4020
+ }
4021
+ const grid = document.createElement("div");
4022
+ const paint = () => {
4023
+ const decade = Math.floor(year / 10) * 10;
4024
+ monthBtn.hidden = mode !== "date";
4025
+ monthBtn.textContent = MONTH_LABELS[month] ?? "";
4026
+ yearBtn.textContent = mode === "year" ? `${decade}-${decade + 9}` : String(year);
4027
+ yearBtn.disabled = mode === "year";
4028
+ week.hidden = mode !== "date";
4029
+ prev2.setAttribute("aria-label", mode === "date" ? "Previous month" : mode === "month" ? "Previous year" : "Previous decade");
4030
+ next.setAttribute("aria-label", mode === "date" ? "Next month" : mode === "month" ? "Next year" : "Next decade");
4031
+ grid.className = mode === "date" ? "vela-ind-cal-grid" : "vela-ind-cal-cells";
4032
+ grid.replaceChildren();
4033
+ if (mode === "date") {
4034
+ const startPad = new Date(year, month, 1).getDay();
4035
+ const days = new Date(year, month + 1, 0).getDate();
4036
+ const todayIso = isoDate(today);
4037
+ const selectedIso = selected ? isoDate(selected) : "";
4038
+ for (let i = 0; i < startPad; i++) {
4039
+ const blank = document.createElement("span");
4040
+ blank.className = "vela-ind-cal-blank";
4041
+ grid.appendChild(blank);
4042
+ }
4043
+ for (let day = 1; day <= days; day++) {
4044
+ const iso = `${year}-${String(month + 1).padStart(2, "0")}-${String(day).padStart(2, "0")}`;
4045
+ const b = document.createElement("button");
4046
+ b.type = "button";
4047
+ b.className = "vela-ind-cal-day";
4048
+ b.textContent = String(day);
4049
+ if (iso === selectedIso) b.dataset.checked = "1";
4050
+ if (iso === todayIso) b.dataset.today = "1";
4051
+ b.addEventListener("click", (e) => {
4052
+ e.stopPropagation();
4053
+ onPick(iso);
4054
+ });
4055
+ grid.appendChild(b);
4056
+ }
4057
+ return;
4058
+ }
4059
+ if (mode === "month") {
4060
+ for (let m = 0; m < 12; m++) {
4061
+ const b = document.createElement("button");
4062
+ b.type = "button";
4063
+ b.className = "vela-ind-cal-cell";
4064
+ b.textContent = MONTH_SHORT[m] ?? "";
4065
+ if (selected?.getFullYear() === year && selected.getMonth() === m) b.dataset.checked = "1";
4066
+ if (today.getFullYear() === year && today.getMonth() === m) b.dataset.today = "1";
4067
+ b.addEventListener("click", (e) => {
4068
+ e.stopPropagation();
4069
+ month = m;
4070
+ mode = "date";
4071
+ paint();
4072
+ });
4073
+ grid.appendChild(b);
4074
+ }
4075
+ return;
4076
+ }
4077
+ for (let y = decade - 1; y <= decade + 10; y++) {
4078
+ const b = document.createElement("button");
4079
+ b.type = "button";
4080
+ b.className = "vela-ind-cal-cell";
4081
+ b.textContent = String(y);
4082
+ if (y < decade || y > decade + 9) b.dataset.outside = "1";
4083
+ if (selected?.getFullYear() === y) b.dataset.checked = "1";
4084
+ if (today.getFullYear() === y) b.dataset.today = "1";
4085
+ b.addEventListener("click", (e) => {
4086
+ e.stopPropagation();
4087
+ year = y;
4088
+ mode = "month";
4089
+ paint();
4090
+ });
4091
+ grid.appendChild(b);
4092
+ }
4093
+ };
4094
+ const step = (dir) => {
4095
+ if (mode === "date") {
4096
+ month += dir;
4097
+ if (month < 0) {
4098
+ month = 11;
4099
+ year -= 1;
4100
+ }
4101
+ if (month > 11) {
4102
+ month = 0;
4103
+ year += 1;
4104
+ }
4105
+ } else {
4106
+ year += mode === "month" ? dir : dir * 10;
4107
+ }
4108
+ paint();
4109
+ };
4110
+ prev2.addEventListener("click", (e) => {
4111
+ e.stopPropagation();
4112
+ step(-1);
4113
+ });
4114
+ next.addEventListener("click", (e) => {
4115
+ e.stopPropagation();
4116
+ step(1);
4117
+ });
4118
+ monthBtn.addEventListener("click", (e) => {
4119
+ e.stopPropagation();
4120
+ mode = "month";
4121
+ paint();
4122
+ });
4123
+ yearBtn.addEventListener("click", (e) => {
4124
+ e.stopPropagation();
4125
+ mode = "year";
4126
+ paint();
4127
+ });
4128
+ paint();
4129
+ el.append(head, week, grid);
4130
+ }
4033
4131
  function normalizeTimeInput(raw) {
4034
4132
  const t = raw.trim();
4035
4133
  const m = /^(\d{1,2}):(\d{2})$/.exec(t) ?? /^(\d{2})(\d{2})$/.exec(t);
@@ -5311,10 +5409,12 @@ var BASELINE_LEVEL_DEFAULT = 50;
5311
5409
  var PREMARKET_SHADE = withAlpha(WARNING, 0.08);
5312
5410
  var POSTMARKET_SHADE = withAlpha(ACCENT, 0.08);
5313
5411
  var EXTENDED_SHADE = POSTMARKET_SHADE;
5412
+ var DEFAULT_MARGINS = { top: 10, bottom: 10, right: 10 };
5314
5413
  function defaultChartStyle() {
5315
5414
  return {
5316
5415
  chartTypes: {},
5317
5416
  fontSize: 11,
5417
+ margins: { ...DEFAULT_MARGINS },
5318
5418
  gridVert: { visible: true, color: null },
5319
5419
  gridHorz: { visible: true, color: null },
5320
5420
  borderColor: null,
@@ -5438,6 +5538,12 @@ function clampLevel(v) {
5438
5538
  function clampSpacing(v) {
5439
5539
  return v < 0.1 ? 0.1 : v > 10 ? 10 : v;
5440
5540
  }
5541
+ function clampMarginPct(v) {
5542
+ return v < 0 ? 0 : v > 40 ? 40 : v;
5543
+ }
5544
+ function clampMarginBars(v) {
5545
+ return Math.round(v < 0 ? 0 : v > 200 ? 200 : v);
5546
+ }
5441
5547
  function factoryResetConfig(factory, priceStyle = factory.series.style) {
5442
5548
  const bag = {};
5443
5549
  for (const t of chartTypes()) {
@@ -5486,6 +5592,7 @@ function mergeConfig(base, patch) {
5486
5592
  const ps = asObject(p.priceScale);
5487
5593
  const anim = asObject(p.animations);
5488
5594
  const panes = asObject(p.panes);
5595
+ const margins = asObject(p.margins);
5489
5596
  const trades = asObject(p.trades);
5490
5597
  const ts = asObject(p.timeScale);
5491
5598
  const marks = asObject(p.marks);
@@ -5545,6 +5652,11 @@ function mergeConfig(base, patch) {
5545
5652
  panes: {
5546
5653
  separatorColor: isColor(panes.separatorColor) ? panes.separatorColor : base.panes.separatorColor
5547
5654
  },
5655
+ margins: {
5656
+ top: isNum(margins.top) ? clampMarginPct(margins.top) : base.margins.top,
5657
+ bottom: isNum(margins.bottom) ? clampMarginPct(margins.bottom) : base.margins.bottom,
5658
+ right: isNum(margins.right) ? clampMarginBars(margins.right) : base.margins.right
5659
+ },
5548
5660
  trades: {
5549
5661
  visible: isBool(trades.visible) ? trades.visible : base.trades.visible,
5550
5662
  labels: isBool(trades.labels) ? trades.labels : base.trades.labels,
@@ -10874,6 +10986,10 @@ var BUILTIN_SETTINGS_IDS = [
10874
10986
  "canvas.grid",
10875
10987
  "canvas.grid.vertical",
10876
10988
  "canvas.grid.horizontal",
10989
+ "canvas.margins",
10990
+ "canvas.margins.top",
10991
+ "canvas.margins.bottom",
10992
+ "canvas.margins.right",
10877
10993
  "canvas.theme"
10878
10994
  ];
10879
10995
  function settingsIdCatalog(hostSections, markGroups = []) {
@@ -11404,6 +11520,10 @@ var SettingsDialog = class {
11404
11520
  body.append(sid(this.toggleRow("Horizontal", config.grid.horzLines.visible, (v) => this.emit({ grid: { horzLines: { visible: v } } }), [
11405
11521
  this.swatch(config.grid.horzLines.color, (v) => this.emit({ grid: { horzLines: { color: v } } }))
11406
11522
  ]), "canvas.grid.horizontal"));
11523
+ body.append(sid(this.sectionTitle("Margins"), "canvas.margins"));
11524
+ body.append(sid(this.numberRow("Top", config.margins.top, 0, 40, 1, (v) => this.emit({ margins: { top: v } }), "%"), "canvas.margins.top"));
11525
+ body.append(sid(this.numberRow("Bottom", config.margins.bottom, 0, 40, 1, (v) => this.emit({ margins: { bottom: v } }), "%"), "canvas.margins.bottom"));
11526
+ body.append(sid(this.numberRow("Right", config.margins.right, 0, 200, 1, (v) => this.emit({ margins: { right: v } }), "bars"), "canvas.margins.right"));
11407
11527
  if (this.themeControl) {
11408
11528
  const tc = this.themeControl;
11409
11529
  body.append(sid(this.sectionTitle("Theme"), "canvas.theme"));
@@ -11975,23 +12095,16 @@ var SettingsDialog = class {
11975
12095
  this.hintTips.push(dispose);
11976
12096
  return el;
11977
12097
  }
11978
- numberRow(label, value, min, max, step, onChange) {
11979
- return fieldRow({
11980
- label,
11981
- labelSize: "sm",
11982
- className: "vela-sd-row",
11983
- control: buildFieldControl({
11984
- kind: "number",
11985
- value,
11986
- min,
11987
- max,
11988
- step,
11989
- fill: false,
11990
- commit: "live",
11991
- steppers: true,
11992
- onChange
11993
- }).el
11994
- });
12098
+ /** `unit` (optional) trails the input as muted text — "%", "bars". */
12099
+ numberRow(label, value, min, max, step, onChange, unit) {
12100
+ const controls = [buildFieldControl({ kind: "number", value, min, max, step, fill: false, commit: "live", steppers: true, onChange }).el];
12101
+ if (unit) {
12102
+ const u = document.createElement("span");
12103
+ u.textContent = unit;
12104
+ u.style.cssText = "color:var(--vela-fg-muted);";
12105
+ controls.push(u);
12106
+ }
12107
+ return this.rowWith(label, controls);
11995
12108
  }
11996
12109
  /** A dropdown whose option values differ from their display labels. */
11997
12110
  selectRowLabeled(label, value, options, onChange) {
@@ -16877,9 +16990,7 @@ function createProjector(coords, paneOf, paneIdAtY, barsInRange, seriesInRange)
16877
16990
  }
16878
16991
 
16879
16992
  // src/renderers/native/core/autoscale.ts
16880
- var MARGIN_TOP = 2 / 7;
16881
- var MARGIN_BOTTOM = 1 / 7;
16882
- function computePaneScale(models, bars, includeCandles, i0, i1, drawings, log = false, offsetOf = () => 0) {
16993
+ function computePaneScale(models, bars, includeCandles, i0, i1, drawings, log = false, offsetOf = () => 0, margins = DEFAULT_MARGINS) {
16883
16994
  let min = Infinity;
16884
16995
  let max = -Infinity;
16885
16996
  const consider = (v) => {
@@ -16914,14 +17025,17 @@ function computePaneScale(models, bars, includeCandles, i0, i1, drawings, log =
16914
17025
  const pad = Math.abs(min) * 0.1 || 1;
16915
17026
  return { min: min - pad, max: max + pad, log: log && min - pad > 0 };
16916
17027
  }
17028
+ const content = Math.max(0.1, 1 - (margins.top + margins.bottom) / 100);
17029
+ const above = margins.top / 100 / content;
17030
+ const below = margins.bottom / 100 / content;
16917
17031
  if (log && min > 0) {
16918
17032
  const lmin = Math.log(min);
16919
17033
  const lmax = Math.log(max);
16920
17034
  const lspan = lmax - lmin;
16921
- return { min: Math.exp(lmin - lspan * MARGIN_BOTTOM), max: Math.exp(lmax + lspan * MARGIN_TOP), log: true };
17035
+ return { min: Math.exp(lmin - lspan * below), max: Math.exp(lmax + lspan * above), log: true };
16922
17036
  }
16923
17037
  const span = max - min;
16924
- return { min: min - span * MARGIN_BOTTOM, max: max + span * MARGIN_TOP };
17038
+ return { min: min - span * below, max: max + span * above };
16925
17039
  }
16926
17040
  function considerSeries(s, i0, i1, off, consider) {
16927
17041
  if (!seriesInScale(s)) return;
@@ -18722,6 +18836,7 @@ var NativeRenderer = class {
18722
18836
  intro: this.intro.style !== false
18723
18837
  },
18724
18838
  panes: { separatorColor: s.separatorColor ?? t.borderColor },
18839
+ margins: { ...s.margins },
18725
18840
  trades: {
18726
18841
  visible: this.scene.tradeMarkers.visible,
18727
18842
  labels: this.scene.tradeMarkers.labels,
@@ -18834,6 +18949,10 @@ var NativeRenderer = class {
18834
18949
  this.animAutoscale.toggle(next.animations.autoscale);
18835
18950
  this.intro = { style: next.animations.intro ? this.introOnStyle : false, duration: this.intro.duration || INTRO_DURATION_DEFAULT_MS };
18836
18951
  s.separatorColor = keepInherit(s.separatorColor, next.panes.separatorColor, prevTheme.borderColor);
18952
+ if (next.margins.right !== s.margins.right && this.coords.barCount > 0) {
18953
+ this.applyViewport({ barSpacing: this.coords.getViewport().barSpacing, rightOffset: next.margins.right });
18954
+ }
18955
+ s.margins = { ...next.margins };
18837
18956
  this.scene.tradeMarkers = {
18838
18957
  visible: next.trades.visible,
18839
18958
  labels: next.trades.labels,
@@ -19077,7 +19196,7 @@ var NativeRenderer = class {
19077
19196
  /** Glide the view back to the most recent bars, keeping the current zoom (barSpacing). */
19078
19197
  scrollToRealtime() {
19079
19198
  if (this.coords.barCount === 0) return;
19080
- this.glideRightOffset(ZOOM_OUT_MARGIN_BARS);
19199
+ this.glideRightOffset(this.scene.style.margins.right);
19081
19200
  }
19082
19201
  /** Ease rightOffset to `target` at constant zoom (see animTick's scroll glide);
19083
19202
  * instant when the scroll glide is off. Shared by scroll-to-latest and panBy. */
@@ -21019,6 +21138,7 @@ var NativeRenderer = class {
21019
21138
  const pricePane = panes.find((p) => p.kind === "price") ?? null;
21020
21139
  this.chrome.prepare(this.scene, this.coords, this.theme);
21021
21140
  const animating = this.animator.active;
21141
+ const margins = this.scene.style.margins;
21022
21142
  for (const pane of panes) {
21023
21143
  if (pane.manualScale) {
21024
21144
  pane.scaleTarget = pane.manualScale;
@@ -21034,7 +21154,7 @@ var NativeRenderer = class {
21034
21154
  if (or) dr = dr ? { min: Math.min(dr.min, or.min), max: Math.max(dr.max, or.max) } : or;
21035
21155
  }
21036
21156
  const includeCandles = pane.kind === "price" && (!this.scene.candlesHidden || this.priceLayersAnchoredToBars(masterModels) || !this.paneHasMeasurableContent(masterModels, dr));
21037
- pane.scaleTarget = computePaneScale(masterModels, this.bars, includeCandles, i0, i1, dr, paneLogScale(this.scene, pane), (id) => this.scene.offsetOf(id));
21157
+ pane.scaleTarget = computePaneScale(masterModels, this.bars, includeCandles, i0, i1, dr, paneLogScale(this.scene, pane), (id) => this.scene.offsetOf(id), margins);
21038
21158
  pane.percentBaseline = pane.kind === "price" ? this.bars[i0]?.close ?? 0 : this.firstVisibleValue(masterModels, i0);
21039
21159
  pane.axisFormat = void 0;
21040
21160
  pane.axisBands = void 0;
@@ -21045,7 +21165,7 @@ var NativeRenderer = class {
21045
21165
  pane.axisFormat = "volume";
21046
21166
  }
21047
21167
  } else if (this.layerNativesOwnPane(pane, masterModels)) {
21048
- pane.scaleTarget = computePaneScale([], this.bars, true, i0, i1, dr, paneLogScale(this.scene, pane), (id) => this.scene.offsetOf(id));
21168
+ pane.scaleTarget = computePaneScale([], this.bars, true, i0, i1, dr, paneLogScale(this.scene, pane), (id) => this.scene.offsetOf(id), margins);
21049
21169
  pane.percentBaseline = this.bars[i0]?.close ?? 0;
21050
21170
  if (masterModels.every((m) => m.paneAxis != null)) {
21051
21171
  pane.axisFormat = "none";
@@ -21074,7 +21194,7 @@ var NativeRenderer = class {
21074
21194
  continue;
21075
21195
  }
21076
21196
  const mdr = this.chrome.paneDrawingsRange([model], this.scene, false, vr);
21077
- sl.scaleTarget = computePaneScale([model], this.bars, false, i0, i1, mdr, false, (id) => this.scene.offsetOf(id));
21197
+ sl.scaleTarget = computePaneScale([model], this.bars, false, i0, i1, mdr, false, (id) => this.scene.offsetOf(id), margins);
21078
21198
  if (!animating || !sl.initialized) {
21079
21199
  sl.scale = { ...sl.scaleTarget };
21080
21200
  sl.initialized = true;
@@ -21115,13 +21235,13 @@ var NativeRenderer = class {
21115
21235
  for (const pane of this.scene.panes.values()) pane.manualScale = null;
21116
21236
  for (const sl of this.scene.indicatorScales.values()) sl.manualScale = null;
21117
21237
  const visibleBars = Math.min(n, 200);
21118
- const rightOffset = 6;
21238
+ const rightOffset = this.scene.style.margins.right;
21119
21239
  const v = this.clampViewport(w / ((visibleBars + rightOffset) * this.coords.spacingScale), rightOffset);
21120
21240
  this.coords.setViewport(v);
21121
21241
  this.targetBarSpacing = v.barSpacing;
21122
21242
  }
21123
21243
  /** Re-frame after a series replacement (a symbol/timeframe switch): keep the user's
21124
- * zoom (bar spacing), re-anchor the newest bars at the default right offset.
21244
+ * zoom (bar spacing), re-anchor the newest bars at the configured right margin.
21125
21245
  * `clampViewport`'s fit-all-bars floor deliberately does NOT apply — a progressive
21126
21246
  * head may still be backfilling toward the previous depth, and raising the spacing
21127
21247
  * to its temporary bar count would lose the zoom this exists to keep. */
@@ -21130,7 +21250,7 @@ var NativeRenderer = class {
21130
21250
  this.panVelocity = 0;
21131
21251
  for (const pane of this.scene.panes.values()) pane.manualScale = null;
21132
21252
  for (const sl of this.scene.indicatorScales.values()) sl.manualScale = null;
21133
- const v = { barSpacing: clampBarSpacing(this.coords.getViewport().barSpacing), rightOffset: defaultViewport().rightOffset };
21253
+ const v = { barSpacing: clampBarSpacing(this.coords.getViewport().barSpacing), rightOffset: this.scene.style.margins.right };
21134
21254
  this.coords.setViewport(v);
21135
21255
  this.targetBarSpacing = v.barSpacing;
21136
21256
  }
@@ -26911,4 +27031,4 @@ function resolveElement(container) {
26911
27031
  return element;
26912
27032
  }
26913
27033
 
26914
- export { BUILTIN_PRICE_STYLES, BarStore, CachingDataFeed, DARK_THEME, DataControl, DrawingStore, DrawingToolbar, DrawingsControl, LEGEND_AT_TOP_ATTR, LIGHT_THEME, MarksControl, MultiProviderFeed, NativeRenderer, RendererControl, SecondClock, TIMEZONES, TypedEventBus, Vela, applyAttributionMarkTheme, buildToolbar, createAttributionMark, createCustomMark, defaultMemberOf, defaultToolbar, groupKeyOf, groupMembers, isGroupRow, normalizeSession, normalizeTimezone, parseSymbol, priceStyleIds, registerBuiltinChartTypes, resolveTheme, sharedBarStore, timeframeToMs, tzButtonLabel, tzMenuLabel, tzOffset };
27034
+ export { BUILTIN_PRICE_STYLES, BarStore, CachingDataFeed, DARK_THEME, DataControl, DrawingStore, DrawingToolbar, DrawingsControl, LEGEND_AT_TOP_ATTR, LIGHT_THEME, MarksControl, MultiProviderFeed, NativeRenderer, RendererControl, SecondClock, TIMEZONES, TypedEventBus, Vela, applyAttributionMarkTheme, buildToolbar, createAttributionMark, createCustomMark, defaultMemberOf, defaultToolbar, groupKeyOf, groupMembers, isGroupRow, normalizeSession, normalizeTimezone, parseSymbol, priceStyleIds, registerBuiltinChartTypes, resolveTheme, resolveTimezone, sharedBarStore, timeframeToMs, timezoneMenuRows, tzButtonLabel, tzMenuLabel, tzOffset };
@@ -2177,7 +2177,8 @@ function addRecent(hex6) {
2177
2177
  recents.unshift(hex6);
2178
2178
  if (recents.length > 10) recents.length = 10;
2179
2179
  }
2180
- function buildColorPicker(color, theme, onChange) {
2180
+ function buildColorPicker(color, theme, onChange, options = {}) {
2181
+ const commit = options.commit ?? "live";
2181
2182
  const parsed = splitColor(color);
2182
2183
  let curHex = parsed.hex6;
2183
2184
  let curAlpha = parsed.alpha;
@@ -2260,7 +2261,12 @@ function buildColorPicker(color, theme, onChange) {
2260
2261
  const r = track.getBoundingClientRect();
2261
2262
  curAlpha = Math.max(0, Math.min(1, (clientX - r.left) / r.width));
2262
2263
  paintOpacity();
2263
- emit();
2264
+ if (commit === "live") emit();
2265
+ };
2266
+ const endDrag = () => {
2267
+ if (!dragging) return;
2268
+ dragging = false;
2269
+ if (commit === "release") emit();
2264
2270
  };
2265
2271
  track.addEventListener("pointerdown", (e) => {
2266
2272
  e.stopPropagation();
@@ -2271,7 +2277,8 @@ function buildColorPicker(color, theme, onChange) {
2271
2277
  track.addEventListener("pointermove", (e) => {
2272
2278
  if (dragging) onDrag(e.clientX);
2273
2279
  });
2274
- track.addEventListener("pointerup", () => dragging = false);
2280
+ track.addEventListener("pointerup", endDrag);
2281
+ track.addEventListener("pointercancel", endDrag);
2275
2282
  function emit() {
2276
2283
  onChange(combineColor(curHex, curAlpha));
2277
2284
  }
@@ -2294,7 +2301,7 @@ function buildColorPicker(color, theme, onChange) {
2294
2301
  return root;
2295
2302
  }
2296
2303
  function colorField(theme, getVal, onVal, opts) {
2297
- return new ColorField({ theme, getVal, onVal, shape: opts?.shape, id: opts?.id, popover: opts?.popover }).el;
2304
+ return new ColorField({ theme, getVal, onVal, shape: opts?.shape, id: opts?.id, popover: opts?.popover, commit: opts?.commit }).el;
2298
2305
  }
2299
2306
  var ColorField = class {
2300
2307
  constructor(opts) {
@@ -2303,6 +2310,7 @@ var ColorField = class {
2303
2310
  this.getVal = opts.getVal;
2304
2311
  this.onVal = opts.onVal;
2305
2312
  this.popoverOpts = opts.popover;
2313
+ this.commit = opts.commit;
2306
2314
  const trigger = document.createElement("button");
2307
2315
  trigger.type = "button";
2308
2316
  if (opts.id) trigger.id = opts.id;
@@ -2342,10 +2350,15 @@ var ColorField = class {
2342
2350
  boundary: this.popoverOpts?.boundary,
2343
2351
  zIndex: this.popoverOpts?.zIndex,
2344
2352
  className: "vela-color-field-pop",
2345
- content: buildColorPicker(this.getVal(), this.theme, (val) => {
2346
- this.onVal(val);
2347
- this.paint();
2348
- })
2353
+ content: buildColorPicker(
2354
+ this.getVal(),
2355
+ this.theme,
2356
+ (val) => {
2357
+ this.onVal(val);
2358
+ this.paint();
2359
+ },
2360
+ { commit: this.commit }
2361
+ )
2349
2362
  });
2350
2363
  pop.show();
2351
2364
  }
@@ -2829,7 +2842,8 @@ function buildFieldControl(desc) {
2829
2842
  const el2 = colorField(desc.theme, desc.get, desc.onChange, {
2830
2843
  shape: "circle",
2831
2844
  id: desc.id,
2832
- popover: desc.popover
2845
+ popover: desc.popover,
2846
+ commit: desc.commit
2833
2847
  });
2834
2848
  if (desc.title) el2.title = desc.title;
2835
2849
  return { el: el2 };