@luxalgo/vela 0.5.3 → 0.6.0

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 (47) hide show
  1. package/README.md +3 -2
  2. package/dist/{DataProvider-gnzv-dgc.d.cts → DataProvider-Ce6PuCzQ.d.cts} +10 -1
  3. package/dist/{DataProvider-DTOEXKTI.d.ts → DataProvider-DNx9ntBD.d.ts} +10 -1
  4. package/dist/{chunk-EY63XBFZ.js → chunk-7ROONW6H.js} +132 -23
  5. package/dist/{chunk-L5NTMWPP.js → chunk-CA3N3PYT.js} +785 -857
  6. package/dist/{chunk-LJLZR44Y.js → chunk-EMS6PO2T.js} +5 -7
  7. package/dist/chunk-OGRQW3M6.js +1328 -0
  8. package/dist/{chunk-P556H6EA.js → chunk-QWIEKZRE.js} +59 -34
  9. package/dist/{chunk-WJKLBYRM.js → chunk-ZNL2X6U2.js} +1 -1
  10. package/dist/{contributions-Ci_i3IN2.d.cts → contributions-DToMhiw3.d.ts} +9 -2
  11. package/dist/{contributions-b6AVrqqG.d.ts → contributions-Dlc7ZWZz.d.cts} +9 -2
  12. package/dist/{history-Ci3M0xLV.d.ts → history-BZADxpKx.d.cts} +5 -3
  13. package/dist/{history-Cf1lVsap.d.cts → history-BuHXJZJ2.d.ts} +5 -3
  14. package/dist/index.cjs +2761 -1597
  15. package/dist/index.d.cts +14 -6
  16. package/dist/index.d.ts +14 -6
  17. package/dist/index.js +4 -4
  18. package/dist/{options-XTBpbx0s.d.ts → options-BlQ00Fju.d.cts} +3 -1
  19. package/dist/{options-XTBpbx0s.d.cts → options-BlQ00Fju.d.ts} +3 -1
  20. package/dist/{plugin-DlidwMj6.d.cts → plugin-BNPLOQO1.d.cts} +3 -3
  21. package/dist/{plugin-DP6A4t4o.d.ts → plugin-BqjEa5r3.d.ts} +3 -3
  22. package/dist/plugin.cjs +4 -0
  23. package/dist/plugin.d.cts +4 -4
  24. package/dist/plugin.d.ts +4 -4
  25. package/dist/plugin.js +2 -2
  26. package/dist/providers/binance.d.cts +2 -2
  27. package/dist/providers/binance.d.ts +2 -2
  28. package/dist/providers/coinbase.d.cts +2 -2
  29. package/dist/providers/coinbase.d.ts +2 -2
  30. package/dist/providers/hyperliquid.d.cts +2 -2
  31. package/dist/providers/hyperliquid.d.ts +2 -2
  32. package/dist/ui.cjs +1318 -4
  33. package/dist/ui.d.cts +383 -2
  34. package/dist/ui.d.ts +383 -2
  35. package/dist/ui.js +3 -3
  36. package/dist/vela.global.js +3729 -2565
  37. package/dist/vela.global.min.js +315 -66
  38. package/dist/widget.cjs +2359 -1054
  39. package/dist/widget.d.cts +30 -16
  40. package/dist/widget.d.ts +30 -16
  41. package/dist/widget.js +37 -14
  42. package/dist/workspace.cjs +2259 -944
  43. package/dist/workspace.d.cts +10 -5
  44. package/dist/workspace.d.ts +10 -5
  45. package/dist/workspace.js +29 -8
  46. package/package.json +1 -1
  47. package/dist/chunk-ATPU5CI6.js +0 -81
package/README.md CHANGED
@@ -7,8 +7,9 @@ renderer layers.
7
7
  - **`vela`** — the headless chart: data model, engines, drawings, providers, native renderer.
8
8
  - **`vela/widget`** — the full chart app: topbar (symbol / timeframe / style / indicators),
9
9
  status line, watermark, bottom bar (ranges, clock, timezone), object tree, keyboard-first UX.
10
- - **`vela/ui`** — the component kit the widget is built on (design tokens + headless
11
- [Zag.js](https://zagjs.com) machines + vanilla views) and the `KeymapManager`.
10
+ - **`vela/ui`** — the component kit the widget is built on: design tokens, overlay chrome
11
+ ([Zag.js](https://zagjs.com) menu/dialog/drawer/tooltip), form primitives (switch, select,
12
+ number, text, color, popover), and the `KeymapManager`.
12
13
  - **`vela/plugin`** — the extension SDK: chart types, renderer layers, native indicators.
13
14
  - **`vela/workspace`** — the multi-chart shell: a grid of full charts under one shared
14
15
  topbar, with named cells, sync groups and one persisted state document.
@@ -1,4 +1,4 @@
1
- import { M as MarketConfig, O as OHLCV, U as Unsubscribe } from './options-XTBpbx0s.cjs';
1
+ import { M as MarketConfig, O as OHLCV, U as Unsubscribe } from './options-BlQ00Fju.cjs';
2
2
 
3
3
  /**
4
4
  * Symbol metadata an engine may need (e.g. Pine `syminfo.*`). Free-form beyond
@@ -64,6 +64,15 @@ interface SymbolDescriptor {
64
64
  description?: string;
65
65
  /** Instrument class, free-form (e.g. `crypto`, `futures`, `stock`). */
66
66
  type?: string;
67
+ /**
68
+ * The instrument's LISTING-venue prefix (`NASDAQ`, `NYSE`, `AMEX`) — a property of the
69
+ * SYMBOL, not of the provider: AAPL is Nasdaq-listed and IBM NYSE-listed even when one
70
+ * provider supplies both tapes. When declared, it is what pickers/legends display, what
71
+ * `PREFIX:TICKER` strings resolve against (TradingView parity: `NYSE:AAPL` is NOT
72
+ * found), and what the canonical committed/persisted form carries. Absent on venues
73
+ * where the provider IS the identity (crypto, fx) — the provider name prefixes those.
74
+ */
75
+ prefix?: string;
67
76
  /** Owning provider name — annotated by the registry aggregation (badges in pickers). */
68
77
  provider?: string;
69
78
  }
@@ -1,4 +1,4 @@
1
- import { M as MarketConfig, O as OHLCV, U as Unsubscribe } from './options-XTBpbx0s.js';
1
+ import { M as MarketConfig, O as OHLCV, U as Unsubscribe } from './options-BlQ00Fju.js';
2
2
 
3
3
  /**
4
4
  * Symbol metadata an engine may need (e.g. Pine `syminfo.*`). Free-form beyond
@@ -64,6 +64,15 @@ interface SymbolDescriptor {
64
64
  description?: string;
65
65
  /** Instrument class, free-form (e.g. `crypto`, `futures`, `stock`). */
66
66
  type?: string;
67
+ /**
68
+ * The instrument's LISTING-venue prefix (`NASDAQ`, `NYSE`, `AMEX`) — a property of the
69
+ * SYMBOL, not of the provider: AAPL is Nasdaq-listed and IBM NYSE-listed even when one
70
+ * provider supplies both tapes. When declared, it is what pickers/legends display, what
71
+ * `PREFIX:TICKER` strings resolve against (TradingView parity: `NYSE:AAPL` is NOT
72
+ * found), and what the canonical committed/persisted form carries. Absent on venues
73
+ * where the provider IS the identity (crypto, fx) — the provider name prefixes those.
74
+ */
75
+ prefix?: string;
67
76
  /** Owning provider name — annotated by the registry aggregation (badges in pickers). */
68
77
  provider?: string;
69
78
  }
@@ -1,7 +1,7 @@
1
- import { parseSymbol, priceStyleIds, BUILTIN_PRICE_STYLES, tzButtonLabel, TIMEZONES, normalizeTimezone, tzMenuLabel } from './chunk-L5NTMWPP.js';
2
- import { chartType, widgetActions, SidePanel, sidePanels, DEFAULT_PANEL_ORDER, getDrawingType } from './chunk-WJKLBYRM.js';
3
- import { Tooltip, Menu, Dialog, Drawer } from './chunk-P556H6EA.js';
4
- import { iconMarkup, registerIcon, injectStyles, iconEl, SESSION_PRE, SESSION_POST, SESSION_OFF, icon, categoricalColor } from './chunk-LJLZR44Y.js';
1
+ import { parseSymbol, priceStyleIds, BUILTIN_PRICE_STYLES, tzButtonLabel, TIMEZONES, normalizeTimezone, tzMenuLabel } from './chunk-CA3N3PYT.js';
2
+ import { chartType, widgetActions, SidePanel, sidePanels, DEFAULT_PANEL_ORDER, getDrawingType } from './chunk-ZNL2X6U2.js';
3
+ import { Tooltip, Menu, Dialog, Drawer } from './chunk-QWIEKZRE.js';
4
+ import { iconMarkup, registerIcon, injectStyles, iconEl, SESSION_PRE, SESSION_POST, SESSION_OFF, icon, categoricalColor } from './chunk-EMS6PO2T.js';
5
5
 
6
6
  // src/widget/timeframe.ts
7
7
  var UNIT_MS = {
@@ -74,6 +74,24 @@ function timeframeMs(value) {
74
74
  if (!Number.isFinite(count) || count < 1) return NaN;
75
75
  return count * UNIT_MS[key];
76
76
  }
77
+ function favoriteTimeframeChips(favorites) {
78
+ const seen = /* @__PURE__ */ new Set();
79
+ const out = [];
80
+ for (const tf of favorites) {
81
+ if (seen.has(tf)) continue;
82
+ seen.add(tf);
83
+ out.push(tf);
84
+ }
85
+ return out.sort((a, b) => {
86
+ const ma = timeframeMs(a);
87
+ const mb = timeframeMs(b);
88
+ const aOk = Number.isFinite(ma);
89
+ const bOk = Number.isFinite(mb);
90
+ if (aOk && bOk && ma !== mb) return ma - mb;
91
+ if (aOk !== bOk) return aOk ? -1 : 1;
92
+ return 0;
93
+ });
94
+ }
77
95
  function timeframeLabel(value) {
78
96
  const parsed = parseTimeframe(value);
79
97
  if (!parsed.valid) return value;
@@ -454,6 +472,36 @@ var CSS2 = `
454
472
  color: var(--vela-fg-bright);
455
473
  }
456
474
  .vela-widget-symbol:hover, .vela-widget-tf:hover, .vela-widget-style:hover, .vela-widget-indicators:hover, .vela-widget-action-left:hover { background: var(--vela-hover); color: var(--vela-fg-bright); }
475
+ /* Timeframe cluster: duration-sorted favorite chips, highlight in place, caret
476
+ opening the full dropdown. With no favorites the caret is the merged trigger
477
+ (label + chevron). An unstarred current value sits as an extra chip by the caret. */
478
+ .vela-widget-tf-group { display: inline-flex; align-items: center; gap: 2px; }
479
+ .vela-widget-tf-chips { display: inline-flex; align-items: center; gap: 2px; }
480
+ .vela-widget-tf-chips:empty { display: none; }
481
+ .vela-widget-tf[data-current='1'] { background: var(--vela-hover-strong); color: var(--vela-fg-bright); }
482
+ .vela-widget-tf-caret {
483
+ all: unset;
484
+ display: inline-flex;
485
+ align-items: center;
486
+ justify-content: center;
487
+ width: 18px;
488
+ height: 30px;
489
+ border-radius: 4px;
490
+ cursor: pointer;
491
+ color: var(--vela-fg-muted);
492
+ }
493
+ .vela-widget-tf-caret:hover { background: var(--vela-hover); color: var(--vela-fg-bright); }
494
+ .vela-widget-tf-caret[data-solo='1'] {
495
+ width: auto;
496
+ padding: 0 6px 0 9px;
497
+ gap: 4px;
498
+ color: var(--vela-fg-bright);
499
+ background: var(--vela-hover-strong);
500
+ font-size: 13px;
501
+ font-weight: 550;
502
+ white-space: nowrap;
503
+ }
504
+ .vela-widget-topbar .vela-widget-tf-caret .vela-icon { font-size: 14px; width: 14px; height: 14px; }
457
505
  .vela-widget-topbar .vela-icon { color: inherit; font-size: 16px; width: 16px; height: 16px; }
458
506
  /* Width is set in syncHairlines() to exactly one device pixel \u2014 a CSS 1px at
459
507
  fractional DPR (1.25, 1.5\u2026) straddles two physical pixels and siblings end
@@ -558,9 +606,16 @@ var Topbar = class {
558
606
  this.symbolEl.className = "vela-widget-symbol";
559
607
  this.symbolEl.textContent = parseSymbol(opts.symbol).ticker;
560
608
  if (opts.onSymbolClick) this.symbolEl.addEventListener("click", opts.onSymbolClick);
561
- this.tfButton = doc.createElement("button");
562
- this.tfButton.className = "vela-widget-tf";
563
- this.tfButton.textContent = timeframeLabel(this.timeframe);
609
+ this.tfFavs = [...opts.timeframeFavorites ?? []];
610
+ this.tfChipsHost = doc.createElement("span");
611
+ this.tfChipsHost.className = "vela-widget-tf-chips";
612
+ this.tfCaret = doc.createElement("button");
613
+ this.tfCaret.className = "vela-widget-tf-caret";
614
+ this.tfCaret.appendChild(iconEl("chevron-down", doc));
615
+ this.tfCaret.setAttribute("aria-label", "Timeframes");
616
+ const tfGroup = doc.createElement("span");
617
+ tfGroup.className = "vela-widget-tf-group";
618
+ tfGroup.append(this.tfChipsHost, this.tfCaret);
564
619
  this.styleButton = doc.createElement("button");
565
620
  this.styleButton.className = "vela-widget-style";
566
621
  this.renderStyleButton(doc);
@@ -600,19 +655,20 @@ var Topbar = class {
600
655
  d.className = "vela-sep";
601
656
  return d;
602
657
  };
603
- const leading = [this.symbolEl, sep(), this.tfButton, sep(), this.styleButton, sep()];
658
+ const leading = [this.symbolEl, sep(), tfGroup, sep(), this.styleButton, sep()];
604
659
  if (this.layoutButton) leading.push(this.layoutButton, sep());
605
660
  if (indicatorsBtn) leading.push(indicatorsBtn, sep());
606
661
  this.leftActionsSep = sep();
607
662
  this.leftActionsSep.hidden = true;
608
663
  this.el.append(...leading, this.leftActionsHost, this.leftActionsSep, this.undoBtn, this.redoBtn, this.actionsHost, this.alertsBtn, this.panelsHost, screenshotBtn);
609
664
  host.appendChild(this.el);
665
+ this.renderTfChips();
610
666
  this.onHairlineSync();
611
667
  this.hairlineRo = new ResizeObserver(this.onHairlineSync);
612
668
  this.hairlineRo.observe(this.el);
613
669
  doc.defaultView?.addEventListener("resize", this.onHairlineSync);
614
670
  this.renderActions();
615
- this.tooltips.push(new Tooltip(this.tfButton, { content: "Timeframe", triggerId: "vela-topbar-tf", host }));
671
+ this.tooltips.push(new Tooltip(this.tfCaret, { content: "Timeframe", triggerId: "vela-topbar-tf", host }));
616
672
  this.tooltips.push(new Tooltip(this.styleButton, { content: "Chart style", triggerId: "vela-topbar-style", host }));
617
673
  if (this.layoutButton && opts.layout) {
618
674
  this.tooltips.push(new Tooltip(this.layoutButton, { content: "Layout", triggerId: "vela-topbar-layout", host }));
@@ -629,11 +685,12 @@ var Topbar = class {
629
685
  });
630
686
  }
631
687
  this.tfMenu = new Menu({
632
- trigger: this.tfButton,
688
+ trigger: this.tfCaret,
633
689
  triggerId: "vela-topbar-tf",
634
690
  host,
635
691
  items: this.tfItems(),
636
692
  onSelect: (id) => opts.onTimeframe(id),
693
+ onFavorite: (id, on) => opts.onTimeframeFavorite?.(id, on),
637
694
  // Timeframe labels are two-or-three characters ("1m", "4h", "1D") — the
638
695
  // stylesheet's default min-width would leave the list mostly empty.
639
696
  minWidth: "84px"
@@ -651,8 +708,48 @@ var Topbar = class {
651
708
  }
652
709
  setTimeframe(tf) {
653
710
  this.timeframe = tf;
654
- this.tfButton.textContent = timeframeLabel(tf);
655
711
  this.tfMenu.setItems(this.tfItems());
712
+ this.renderTfChips();
713
+ }
714
+ /** Reflect the favorite-timeframe set — the quick-switch chips and the dropdown stars. */
715
+ setTimeframeFavorites(favs) {
716
+ this.tfFavs = [...favs];
717
+ this.renderTfChips();
718
+ this.tfMenu.setItems(this.tfItems());
719
+ }
720
+ /** Rebuild the quick-switch chips (current value changed, or the favorite set did). */
721
+ renderTfChips() {
722
+ const doc = this.el.ownerDocument;
723
+ this.tfChipsHost.replaceChildren();
724
+ const stars = this.opts.onTimeframeFavorite !== void 0;
725
+ const chips = stars ? favoriteTimeframeChips(this.tfFavs) : [];
726
+ const currentIsFav = chips.includes(this.timeframe);
727
+ const shown = currentIsFav || chips.length === 0 ? chips : [...chips, this.timeframe];
728
+ for (const tf of shown) {
729
+ const b = doc.createElement("button");
730
+ b.className = "vela-widget-tf";
731
+ const label = timeframeLabel(tf);
732
+ b.textContent = label;
733
+ if (tf === this.timeframe) {
734
+ b.dataset.current = "1";
735
+ b.setAttribute("aria-current", "true");
736
+ } else {
737
+ b.setAttribute("aria-label", `Switch timeframe to ${label}`);
738
+ b.addEventListener("click", () => this.opts.onTimeframe(tf));
739
+ }
740
+ this.tfChipsHost.appendChild(b);
741
+ }
742
+ this.tfCaret.replaceChildren();
743
+ if (chips.length === 0) {
744
+ this.tfCaret.dataset.solo = "1";
745
+ this.tfCaret.append(doc.createTextNode(timeframeLabel(this.timeframe)), iconEl("chevron-down", doc));
746
+ this.tfCaret.setAttribute("aria-label", `Timeframe \u2014 ${timeframeLabel(this.timeframe)}`);
747
+ } else {
748
+ delete this.tfCaret.dataset.solo;
749
+ this.tfCaret.appendChild(iconEl("chevron-down", doc));
750
+ this.tfCaret.setAttribute("aria-label", "Timeframes");
751
+ }
752
+ this.onHairlineSync();
656
753
  }
657
754
  setPriceStyle(style) {
658
755
  this.priceStyle = style;
@@ -770,7 +867,13 @@ var Topbar = class {
770
867
  }
771
868
  }
772
869
  tfItems() {
773
- return this.opts.timeframes.map((tf) => ({ id: tf, label: timeframeLabel(tf), checked: tf === this.timeframe }));
870
+ const stars = this.opts.onTimeframeFavorite !== void 0;
871
+ return this.opts.timeframes.map((tf) => ({
872
+ id: tf,
873
+ label: timeframeLabel(tf),
874
+ checked: tf === this.timeframe,
875
+ ...stars ? { favorite: this.tfFavs.includes(tf) } : {}
876
+ }));
774
877
  }
775
878
  styleItems() {
776
879
  return priceStyleIds().map((id, i) => ({
@@ -1502,10 +1605,11 @@ function parseQuery(raw, venues) {
1502
1605
  function onlyOne(matches) {
1503
1606
  return matches.length === 1 ? matches[0] : null;
1504
1607
  }
1608
+ var venueOf = (s) => s.prefix ?? s.provider;
1505
1609
  function filterSymbols(list, query, limit = 100) {
1506
- const venues = [...new Set(list.map((s) => s.provider?.toLowerCase()).filter((p) => !!p))];
1610
+ const venues = [...new Set(list.flatMap((s) => [venueOf(s)?.toLowerCase(), s.provider?.toLowerCase()]).filter((p) => !!p))];
1507
1611
  const { scope, term } = parseQuery(query, venues);
1508
- const pool = scope ? list.filter((s) => s.provider?.toLowerCase() === scope) : list;
1612
+ const pool = scope ? list.filter((s) => venueOf(s)?.toLowerCase() === scope || s.provider?.toLowerCase() === scope) : list;
1509
1613
  const q = term.toUpperCase();
1510
1614
  if (!q) {
1511
1615
  if (scope) return [...pool].sort((a, b) => a.ticker.localeCompare(b.ticker)).slice(0, limit);
@@ -1524,7 +1628,7 @@ function filterSymbols(list, query, limit = 100) {
1524
1628
  if (t.startsWith(q)) prefix.push(s);
1525
1629
  else if (t.includes(q)) substr.push(s);
1526
1630
  else if ((s.description ?? "").toUpperCase().includes(q)) desc.push(s);
1527
- else if (!scope && s.provider?.toLowerCase().includes(qLower)) venue.push(s);
1631
+ else if (!scope && (venueOf(s)?.toLowerCase().includes(qLower) || s.provider?.toLowerCase().includes(qLower))) venue.push(s);
1528
1632
  if (prefix.length >= limit) break;
1529
1633
  }
1530
1634
  return [...prefix, ...substr, ...desc, ...venue].slice(0, limit);
@@ -1676,14 +1780,14 @@ var SymbolPicker = class {
1676
1780
  else if (e.key === "ArrowUp") this.moveHighlight(-1);
1677
1781
  else if (e.key === "Enter") {
1678
1782
  const pick = this.rows[this.highlighted];
1679
- if (pick) this.select(pick.ticker, pick.provider, opts.onSelect);
1783
+ if (pick) this.select(pick.ticker, pick.prefix ?? pick.provider, opts.onSelect);
1680
1784
  return;
1681
1785
  } else return;
1682
1786
  e.preventDefault();
1683
1787
  });
1684
1788
  this.list.addEventListener("click", (e) => {
1685
1789
  const row = e.target.closest(".vela-sp-row");
1686
- if (row?.dataset.ticker) this.select(row.dataset.ticker, row.dataset.provider, opts.onSelect);
1790
+ if (row?.dataset.ticker) this.select(row.dataset.ticker, row.dataset.venue, opts.onSelect);
1687
1791
  });
1688
1792
  }
1689
1793
  /** Wire where symbols come from (re-called on every widget rebuild). */
@@ -1700,9 +1804,9 @@ var SymbolPicker = class {
1700
1804
  destroy() {
1701
1805
  this.dialog.destroy();
1702
1806
  }
1703
- select(ticker, provider, onSelect) {
1807
+ select(ticker, venue, onSelect) {
1704
1808
  this.close();
1705
- onSelect(provider ? `${provider.toLowerCase()}:${ticker}` : ticker);
1809
+ onSelect(venue ? `${venue}:${ticker}` : ticker);
1706
1810
  }
1707
1811
  moveHighlight(delta) {
1708
1812
  if (!this.rows.length) return;
@@ -1735,7 +1839,8 @@ var SymbolPicker = class {
1735
1839
  const row = doc.createElement("div");
1736
1840
  row.className = "vela-sp-row";
1737
1841
  row.dataset.ticker = s.ticker;
1738
- if (s.provider) row.dataset.provider = s.provider;
1842
+ const venue = s.prefix ?? s.provider;
1843
+ if (venue) row.dataset.venue = venue;
1739
1844
  const av = tickerIconEl(doc, baseOf(s), s.ticker, "vela-sp-avatar");
1740
1845
  const main = doc.createElement("span");
1741
1846
  main.className = "vela-sp-main";
@@ -1747,11 +1852,11 @@ var SymbolPicker = class {
1747
1852
  d.textContent = s.description ?? (s.type ?? "");
1748
1853
  main.append(t, d);
1749
1854
  row.append(av, main);
1750
- if (s.provider) {
1855
+ if (venue) {
1751
1856
  const badge = doc.createElement("span");
1752
1857
  badge.className = "vela-sp-badge";
1753
- badge.dataset.p = s.provider;
1754
- badge.textContent = s.provider;
1858
+ badge.dataset.p = s.provider ?? venue;
1859
+ badge.textContent = venue;
1755
1860
  row.appendChild(badge);
1756
1861
  }
1757
1862
  this.list.appendChild(row);
@@ -4140,6 +4245,10 @@ function sanitizeState(doc) {
4140
4245
  const favs = d.favorites.filter((f) => typeof f === "string");
4141
4246
  if (favs.length > 0) out.favorites = favs;
4142
4247
  }
4248
+ if (Array.isArray(d.timeframeFavorites)) {
4249
+ const favs = d.timeframeFavorites.filter((f) => typeof f === "string");
4250
+ if (favs.length > 0) out.timeframeFavorites = favs;
4251
+ }
4143
4252
  const sync = sanitizeSync(d.sync);
4144
4253
  if (sync) out.sync = sync;
4145
4254
  const tracks = sanitizeTrackSizes(d.trackSizes);