@luxalgo/vela 0.5.0 → 0.5.1

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 (45) hide show
  1. package/dist/{DataProvider-DKNDHpNv.d.cts → DataProvider-BBQ3Hr21.d.cts} +1 -1
  2. package/dist/{DataProvider-Dzd-Erlk.d.ts → DataProvider-DGesjOJT.d.ts} +1 -1
  3. package/dist/{chunk-OVQKKXLZ.js → chunk-5SE4PL3H.js} +402 -60
  4. package/dist/{chunk-RHIDOUFL.js → chunk-AIW3EJUE.js} +2 -2
  5. package/dist/{chunk-GYD2THPV.js → chunk-EP6NDJYK.js} +5 -0
  6. package/dist/{chunk-KCCZNKH7.js → chunk-FNOMKHEZ.js} +668 -78
  7. package/dist/{chunk-7UFX5ZIG.js → chunk-S6F6SQ2L.js} +7 -2
  8. package/dist/{chunk-KM6LHB3Y.js → chunk-ULIX6AE2.js} +6 -1
  9. package/dist/{contributions-hX3EUyjG.d.ts → contributions-6N0rbrPC.d.ts} +46 -31
  10. package/dist/{contributions-o1GRKPI_.d.cts → contributions-BlV3kb0O.d.cts} +46 -31
  11. package/dist/{history-Dzxz-MQj.d.ts → history-BTd52YMA.d.ts} +10 -5
  12. package/dist/{history-LJkz4-sS.d.cts → history-DX3Zne3a.d.cts} +10 -5
  13. package/dist/index.cjs +680 -75
  14. package/dist/index.d.cts +37 -12
  15. package/dist/index.d.ts +37 -12
  16. package/dist/index.js +4 -4
  17. package/dist/{options-Q-576hIi.d.cts → options-DbQholFV.d.cts} +14 -0
  18. package/dist/{options-Q-576hIi.d.ts → options-DbQholFV.d.ts} +14 -0
  19. package/dist/{plugin-D94muTV-.d.cts → plugin-O34MbXf-.d.cts} +157 -7
  20. package/dist/{plugin-aGUD1epn.d.ts → plugin-TN3n-9YN.d.ts} +157 -7
  21. package/dist/plugin.cjs +5 -0
  22. package/dist/plugin.d.cts +4 -4
  23. package/dist/plugin.d.ts +4 -4
  24. package/dist/plugin.js +2 -2
  25. package/dist/providers/binance.d.cts +2 -2
  26. package/dist/providers/binance.d.ts +2 -2
  27. package/dist/providers/coinbase.d.cts +2 -2
  28. package/dist/providers/coinbase.d.ts +2 -2
  29. package/dist/providers/hyperliquid.d.cts +2 -2
  30. package/dist/providers/hyperliquid.d.ts +2 -2
  31. package/dist/ui.cjs +10 -0
  32. package/dist/ui.d.cts +1 -1
  33. package/dist/ui.d.ts +1 -1
  34. package/dist/ui.js +3 -3
  35. package/dist/vela.global.js +680 -75
  36. package/dist/vela.global.min.js +59 -28
  37. package/dist/widget.cjs +1081 -134
  38. package/dist/widget.d.cts +30 -16
  39. package/dist/widget.d.ts +30 -16
  40. package/dist/widget.js +9 -9
  41. package/dist/workspace.cjs +1372 -183
  42. package/dist/workspace.d.cts +66 -8
  43. package/dist/workspace.d.ts +66 -8
  44. package/dist/workspace.js +275 -37
  45. package/package.json +1 -1
@@ -881,6 +881,11 @@ function themeTokens(t) {
881
881
  "--vela-hover-strong": wash(0.16),
882
882
  "--vela-focus": withAlpha(t.textColor, 0.5),
883
883
  "--vela-focus-soft": withAlpha(t.textColor, 0.12),
884
+ // Separator hover — the SAME wash the chart's pane separators paint on hover
885
+ // (soft full-thickness band + solid 2px center line), so DOM-drawn dividers
886
+ // (the workspace grid) and canvas-drawn ones read as one family.
887
+ "--vela-separator-hover-band": withAlpha(t.textColor, 0.1),
888
+ "--vela-separator-hover-line": withAlpha(t.textColor, 0.55),
884
889
  "--vela-scroll": withAlpha(t.textColor, 0.3),
885
890
  "--vela-accent": ACCENT,
886
891
  "--vela-accent-bright": ACCENT_BRIGHT,
@@ -1058,6 +1063,11 @@ registerIcon(
1058
1063
  "pen-lock",
1059
1064
  svg24('<path d="M13.8 4.2a2.1 2.1 0 0 1 3 3L8.5 15.5l-3.5 1 1-3.5Z"/><rect x="13" y="14.5" width="8" height="6" rx="1.2"/><path d="M15 14.5v-1.6a2 2 0 0 1 4 0v1.6"/>')
1060
1065
  );
1066
+ registerIcon(
1067
+ "pen-sync",
1068
+ // Pen + two stacked panes — drawing onto every linked chart at once.
1069
+ svg24('<path d="M13.8 4.2a2.1 2.1 0 0 1 3 3L8.5 15.5l-3.5 1 1-3.5Z"/><rect x="12.5" y="13.5" width="6" height="4.8" rx="1"/><path d="M15.5 18.3v1a1 1 0 0 0 1 1h4a1 1 0 0 0 1-1v-2.8a1 1 0 0 0-1-1h-1"/>')
1070
+ );
1061
1071
  registerIcon("brush", svg24('<path d="M9.5 12 17 4.5a2.12 2.12 0 0 1 3 3L12.5 15"/><path d="M7 14a3 3 0 0 0-3 3c0 1.3-1.2 1.5-1.5 2 .8.9 2 1.5 3.5 1.5a3.5 3.5 0 0 0 3.5-3.5 3 3 0 0 0-2.5-3Z"/>'));
1062
1072
  registerIcon(
1063
1073
  "bucket",
@@ -1750,7 +1760,332 @@ var Dialog = class {
1750
1760
  }
1751
1761
  };
1752
1762
 
1763
+ // src/widget/layout-picker.ts
1764
+ var STYLE_ID = "vela-widget-layout-picker-v14";
1765
+ var CSS2 = `
1766
+ .vela-lp-layer { position: absolute; z-index: var(--vela-z-menu); }
1767
+ .vela-lp {
1768
+ background: var(--vela-surface-elev);
1769
+ color: var(--vela-fg);
1770
+ border: 1px solid var(--vela-border-strong);
1771
+ border-radius: 8px;
1772
+ box-shadow: var(--vela-shadow);
1773
+ padding: 10px 12px 10px;
1774
+ font-size: 13px;
1775
+ user-select: none;
1776
+ transform-origin: top;
1777
+ animation: vela-lp-in var(--vela-dur-fast) var(--vela-ease);
1778
+ }
1779
+ @keyframes vela-lp-in {
1780
+ from { opacity: 0; transform: translateY(-4px) scale(0.98); }
1781
+ to { opacity: 1; transform: translateY(0) scale(1); }
1782
+ }
1783
+ .vela-lp-cols { display: flex; align-items: stretch; gap: 14px; }
1784
+ .vela-lp-heading {
1785
+ display: flex;
1786
+ align-items: center;
1787
+ gap: 8px;
1788
+ font-size: 10px;
1789
+ font-weight: 600;
1790
+ letter-spacing: 1.2px;
1791
+ text-transform: uppercase;
1792
+ color: var(--vela-fg-faint);
1793
+ margin-bottom: 10px;
1794
+ }
1795
+ /* "?" help badge: hover it for the canvas explainer. */
1796
+ .vela-lp-badge {
1797
+ display: inline-flex;
1798
+ align-items: center;
1799
+ justify-content: center;
1800
+ width: 14px;
1801
+ height: 14px;
1802
+ border-radius: 50%;
1803
+ border: 1px solid var(--vela-border-strong);
1804
+ color: var(--vela-fg-muted);
1805
+ font-size: 9px;
1806
+ font-weight: 600;
1807
+ letter-spacing: 0;
1808
+ cursor: default;
1809
+ transition: color var(--vela-dur-fast) var(--vela-ease), border-color var(--vela-dur-fast) var(--vela-ease);
1810
+ }
1811
+ .vela-lp-badge:hover { color: var(--vela-fg-bright); border-color: var(--vela-fg-muted); }
1812
+ .vela-lp-tip { display: flex; flex-direction: column; gap: 4px; max-width: 230px; white-space: normal; }
1813
+ .vela-lp-vsep { width: 1px; flex: none; align-self: stretch; background: var(--vela-border-faint); }
1814
+ .vela-lp-layout { width: 132px; display: flex; flex-direction: column; align-items: center; }
1815
+ .vela-lp-layout > .vela-lp-heading,
1816
+ .vela-lp-layout > .vela-lp-presets { align-self: stretch; }
1817
+ .vela-lp-grid { display: grid; grid-template-columns: repeat(4, 24px); grid-auto-rows: 24px; gap: 4px; }
1818
+ .vela-lp-sq {
1819
+ all: unset;
1820
+ box-sizing: border-box;
1821
+ border-radius: 4px;
1822
+ background: var(--vela-hover);
1823
+ border: 1px solid var(--vela-border-faint);
1824
+ cursor: pointer;
1825
+ transition: background var(--vela-dur-fast) var(--vela-ease), border-color var(--vela-dur-fast) var(--vela-ease);
1826
+ }
1827
+ .vela-lp-sq:hover { background: var(--vela-hover-strong); border-color: var(--vela-border-strong); }
1828
+ /* Selected/previewed squares \u2014 the shared monochrome selection chip. */
1829
+ .vela-lp-sq[data-on='1'] {
1830
+ background: var(--vela-selected-bg);
1831
+ border-color: var(--vela-selected-bg);
1832
+ }
1833
+ .vela-lp-presets { display: flex; flex-direction: column; gap: 2px; margin-top: 8px; }
1834
+ .vela-lp-preset { all: unset; padding: 5px 8px; border-radius: 4px; cursor: pointer; color: var(--vela-fg-muted); font-size: 12px; white-space: nowrap; transition: transform 120ms var(--vela-ease); }
1835
+ .vela-lp-preset:hover { background: var(--vela-hover); }
1836
+ .vela-lp-preset:active { transform: scale(0.98); }
1837
+ .vela-lp-preset[data-checked='1'] { background: var(--vela-hover-strong); color: var(--vela-fg-bright); }
1838
+ .vela-lp-sync { display: flex; flex-direction: column; gap: 4px; min-width: 118px; }
1839
+ .vela-lp-sync-row { all: unset; display: flex; align-items: center; gap: 14px; padding: 6px 8px; border-radius: 5px; cursor: pointer; }
1840
+ .vela-lp-sync-row:hover { background: var(--vela-hover); }
1841
+ .vela-lp-sync-row .vela-lp-label { flex: 1 1 auto; }
1842
+ /* Toggle pill \u2014 same control language as the menu's switch rows, monochrome ON state. */
1843
+ .vela-lp-switch {
1844
+ position: relative;
1845
+ flex: none;
1846
+ width: 34px;
1847
+ height: 18px;
1848
+ border-radius: 9px;
1849
+ background: var(--vela-hover);
1850
+ border: 1px solid var(--vela-border-soft);
1851
+ transition: background 0.16s ease, border-color 0.16s ease;
1852
+ }
1853
+ .vela-lp-switch::after {
1854
+ content: '';
1855
+ position: absolute;
1856
+ top: 2px;
1857
+ left: 2px;
1858
+ width: 12px;
1859
+ height: 12px;
1860
+ border-radius: 50%;
1861
+ background: var(--vela-fg-muted);
1862
+ transition: transform 0.16s ease, background 0.16s ease;
1863
+ }
1864
+ .vela-lp-switch.on { background: var(--vela-selected-bg); border-color: var(--vela-selected-bg); }
1865
+ .vela-lp-switch.on::after { transform: translateX(16px); background: var(--vela-selected-fg); }
1866
+ `;
1867
+ var GRID = 4;
1868
+ var LayoutPicker = class {
1869
+ constructor(opts) {
1870
+ this.squares = [];
1871
+ this.isOpen = false;
1872
+ /** Hover preview (1-based rows/cols), null = show current shape. */
1873
+ this.hover = null;
1874
+ this.onDocPointerDown = (e) => {
1875
+ const t = e.target;
1876
+ if (t && (this.layer.contains(t) || this.opts.trigger.contains(t))) return;
1877
+ this.close();
1878
+ };
1879
+ this.onDocKeydown = (e) => {
1880
+ if (e.key === "Escape") this.close();
1881
+ };
1882
+ this.opts = opts;
1883
+ this.doc = opts.host.ownerDocument;
1884
+ injectStyles(STYLE_ID, CSS2, this.doc);
1885
+ const doc = this.doc;
1886
+ this.layer = doc.createElement("div");
1887
+ this.layer.className = "vela-ui-layer vela-lp-layer";
1888
+ this.layer.style.display = "none";
1889
+ const panel = doc.createElement("div");
1890
+ panel.className = "vela-lp";
1891
+ this.layer.appendChild(panel);
1892
+ const cols = doc.createElement("div");
1893
+ cols.className = "vela-lp-cols";
1894
+ panel.appendChild(cols);
1895
+ const layoutCol = doc.createElement("div");
1896
+ layoutCol.className = "vela-lp-layout";
1897
+ const layoutHeading = doc.createElement("div");
1898
+ layoutHeading.className = "vela-lp-heading";
1899
+ const headingText = doc.createElement("span");
1900
+ headingText.textContent = "Layout";
1901
+ const badge = doc.createElement("span");
1902
+ badge.className = "vela-lp-badge";
1903
+ badge.textContent = "?";
1904
+ layoutHeading.append(headingText, badge);
1905
+ layoutCol.appendChild(layoutHeading);
1906
+ this.infoTip = new Tooltip(badge, {
1907
+ host: opts.host,
1908
+ placement: "bottom",
1909
+ content: () => this.tipNode()
1910
+ });
1911
+ const grid = doc.createElement("div");
1912
+ grid.className = "vela-lp-grid";
1913
+ for (let r = 0; r < GRID; r += 1) {
1914
+ for (let c = 0; c < GRID; c += 1) {
1915
+ const sq = doc.createElement("button");
1916
+ sq.className = "vela-lp-sq";
1917
+ sq.dataset.r = String(r);
1918
+ sq.dataset.c = String(c);
1919
+ sq.setAttribute("aria-label", `${c + 1} \xD7 ${r + 1}`);
1920
+ this.squares.push(sq);
1921
+ grid.appendChild(sq);
1922
+ }
1923
+ }
1924
+ layoutCol.appendChild(grid);
1925
+ this.presetsEl = doc.createElement("div");
1926
+ this.presetsEl.className = "vela-lp-presets";
1927
+ layoutCol.appendChild(this.presetsEl);
1928
+ cols.appendChild(layoutCol);
1929
+ const vsep = doc.createElement("div");
1930
+ vsep.className = "vela-lp-vsep";
1931
+ cols.appendChild(vsep);
1932
+ const syncCol = doc.createElement("div");
1933
+ const syncHeading = doc.createElement("div");
1934
+ syncHeading.className = "vela-lp-heading";
1935
+ syncHeading.textContent = "Sync";
1936
+ syncCol.appendChild(syncHeading);
1937
+ this.syncEl = doc.createElement("div");
1938
+ this.syncEl.className = "vela-lp-sync";
1939
+ syncCol.appendChild(this.syncEl);
1940
+ cols.appendChild(syncCol);
1941
+ grid.addEventListener("pointerdown", (e) => {
1942
+ const sq = this.squareAt(e);
1943
+ if (!sq) return;
1944
+ e.preventDefault();
1945
+ const { r, c } = this.squarePos(sq);
1946
+ this.close();
1947
+ this.opts.onSelectGrid(r + 1, c + 1);
1948
+ });
1949
+ grid.addEventListener("pointermove", (e) => {
1950
+ const sq = this.squareAt(e);
1951
+ if (!sq) return;
1952
+ const { r, c } = this.squarePos(sq);
1953
+ if (this.hover?.rows !== r + 1 || this.hover?.cols !== c + 1) {
1954
+ this.hover = { rows: r + 1, cols: c + 1 };
1955
+ this.render();
1956
+ }
1957
+ });
1958
+ grid.addEventListener("pointerleave", () => {
1959
+ if (this.hover) {
1960
+ this.hover = null;
1961
+ this.render();
1962
+ }
1963
+ });
1964
+ opts.trigger.addEventListener("click", () => this.toggle());
1965
+ opts.trigger.setAttribute("aria-haspopup", "true");
1966
+ opts.trigger.setAttribute("aria-expanded", "false");
1967
+ opts.host.appendChild(this.layer);
1968
+ }
1969
+ toggle() {
1970
+ if (this.isOpen) this.close();
1971
+ else this.open();
1972
+ }
1973
+ open() {
1974
+ if (this.isOpen) return;
1975
+ this.isOpen = true;
1976
+ this.hover = null;
1977
+ this.refresh();
1978
+ this.layer.style.display = "";
1979
+ this.position();
1980
+ this.opts.trigger.setAttribute("aria-expanded", "true");
1981
+ this.doc.addEventListener("pointerdown", this.onDocPointerDown, true);
1982
+ this.doc.addEventListener("keydown", this.onDocKeydown, true);
1983
+ this.opts.onOpenChange?.(true);
1984
+ }
1985
+ close() {
1986
+ if (!this.isOpen) return;
1987
+ this.isOpen = false;
1988
+ this.layer.style.display = "none";
1989
+ this.opts.trigger.setAttribute("aria-expanded", "false");
1990
+ this.doc.removeEventListener("pointerdown", this.onDocPointerDown, true);
1991
+ this.doc.removeEventListener("keydown", this.onDocKeydown, true);
1992
+ this.opts.onOpenChange?.(false);
1993
+ }
1994
+ /** Re-read shape/presets/syncs and re-render (no-op while closed — `open` re-reads). */
1995
+ refresh() {
1996
+ if (!this.isOpen) return;
1997
+ this.renderPresets();
1998
+ this.renderSyncs();
1999
+ this.render();
2000
+ }
2001
+ destroy() {
2002
+ this.close();
2003
+ this.infoTip.destroy();
2004
+ this.layer.remove();
2005
+ }
2006
+ // ── internals ──
2007
+ squareAt(e) {
2008
+ const sq = e.target?.closest?.(".vela-lp-sq");
2009
+ return sq instanceof HTMLButtonElement ? sq : null;
2010
+ }
2011
+ squarePos(sq) {
2012
+ return { r: Number(sq.dataset.r), c: Number(sq.dataset.c) };
2013
+ }
2014
+ /** Help copy for the LAYOUT "?" badge. */
2015
+ tipNode() {
2016
+ const tip = this.doc.createElement("div");
2017
+ tip.className = "vela-lp-tip";
2018
+ tip.textContent = "Click a square to apply that columns \xD7 rows layout.";
2019
+ return tip;
2020
+ }
2021
+ /** Project the interaction state onto the DOM (the hover/current rectangle). */
2022
+ render() {
2023
+ const preview = this.hover ?? this.opts.shape();
2024
+ for (const sq of this.squares) {
2025
+ const { r, c } = this.squarePos(sq);
2026
+ const on = preview !== null && r < preview.rows && c < preview.cols;
2027
+ if (on) sq.dataset.on = "1";
2028
+ else delete sq.dataset.on;
2029
+ }
2030
+ }
2031
+ renderPresets() {
2032
+ const doc = this.doc;
2033
+ this.presetsEl.replaceChildren();
2034
+ const presets = this.opts.presets();
2035
+ this.presetsEl.style.display = presets.length > 0 ? "" : "none";
2036
+ for (const p of presets) {
2037
+ const b = doc.createElement("button");
2038
+ b.className = "vela-lp-preset";
2039
+ b.textContent = p.label;
2040
+ if (p.checked) b.dataset.checked = "1";
2041
+ b.addEventListener("click", () => {
2042
+ this.close();
2043
+ this.opts.onSelectPreset(p.id);
2044
+ });
2045
+ this.presetsEl.appendChild(b);
2046
+ }
2047
+ }
2048
+ renderSyncs() {
2049
+ const doc = this.doc;
2050
+ this.syncEl.replaceChildren();
2051
+ for (const s of this.opts.syncs()) {
2052
+ const row = doc.createElement("button");
2053
+ row.className = "vela-lp-sync-row";
2054
+ const label = doc.createElement("span");
2055
+ label.className = "vela-lp-label";
2056
+ label.textContent = s.label;
2057
+ const pill = doc.createElement("span");
2058
+ pill.className = "vela-lp-switch" + (s.checked ? " on" : "");
2059
+ pill.setAttribute("aria-hidden", "true");
2060
+ row.setAttribute("role", "switch");
2061
+ row.setAttribute("aria-checked", String(s.checked));
2062
+ row.append(label, pill);
2063
+ row.addEventListener("click", () => {
2064
+ this.opts.onToggleSync(s.id);
2065
+ this.renderSyncs();
2066
+ });
2067
+ this.syncEl.appendChild(row);
2068
+ }
2069
+ }
2070
+ /** Anchor under the trigger, clamped to the host's right edge. */
2071
+ position() {
2072
+ const hostRect = this.opts.host.getBoundingClientRect();
2073
+ const trigRect = this.opts.trigger.getBoundingClientRect();
2074
+ let left = trigRect.left - hostRect.left;
2075
+ const top = trigRect.bottom - hostRect.top + 4;
2076
+ const width = this.layer.offsetWidth;
2077
+ if (left + width > hostRect.width - 8) left = Math.max(8, hostRect.width - 8 - width);
2078
+ this.layer.style.left = `${left}px`;
2079
+ this.layer.style.top = `${top}px`;
2080
+ }
2081
+ };
2082
+
1753
2083
  // src/chart-types/registry.ts
2084
+ function settingsRowVisible(when, bag) {
2085
+ if (!when) return true;
2086
+ const conds = Array.isArray(when) ? when : [when];
2087
+ return conds.every((c) => c.anyOf ? c.anyOf.some((x) => x === bag[c.key]) : bag[c.key] === c.equals);
2088
+ }
1754
2089
  var registry2 = /* @__PURE__ */ new Map();
1755
2090
  function registerChartType(def) {
1756
2091
  registry2.set(def.id, def);
@@ -1844,6 +2179,44 @@ function basePaintingOf(style) {
1844
2179
  for (const t of chartTypes()) if (t.id === style) return t.basePainting ?? "candles";
1845
2180
  return "candles";
1846
2181
  }
2182
+ function hasOwnCandlePaint(style) {
2183
+ if (BUILTIN_PRICE_STYLES.includes(style)) return false;
2184
+ for (const t of chartTypes()) if (t.id === style) return (t.basePainting ?? "candles") === "candles";
2185
+ return false;
2186
+ }
2187
+ function candleOverrideFor(style, bags) {
2188
+ if (!hasOwnCandlePaint(style)) return null;
2189
+ const bag = bags[style] ?? {};
2190
+ const color = (v) => isColor(v) ? v : null;
2191
+ const bool = (v) => isBool(v) ? v : null;
2192
+ return {
2193
+ upColor: color(bag.candleUpColor),
2194
+ downColor: color(bag.candleDownColor),
2195
+ bodyVisible: bool(bag.candleBodyVisible),
2196
+ borderVisible: bool(bag.candleBorderVisible),
2197
+ borderUpColor: color(bag.candleBorderUpColor),
2198
+ borderDownColor: color(bag.candleBorderDownColor),
2199
+ wickVisible: bool(bag.candleWickVisible),
2200
+ wickUpColor: color(bag.candleWickUpColor),
2201
+ wickDownColor: color(bag.candleWickDownColor)
2202
+ };
2203
+ }
2204
+ function effectiveCandlePaint(base, override, themeUp, themeDown) {
2205
+ if (!override) return { up: themeUp, down: themeDown, candle: base };
2206
+ return {
2207
+ up: override.upColor ?? themeUp,
2208
+ down: override.downColor ?? themeDown,
2209
+ candle: {
2210
+ bodyVisible: override.bodyVisible ?? base.bodyVisible,
2211
+ borderVisible: override.borderVisible ?? base.borderVisible,
2212
+ borderUpColor: override.borderUpColor ?? base.borderUpColor,
2213
+ borderDownColor: override.borderDownColor ?? base.borderDownColor,
2214
+ wickVisible: override.wickVisible ?? base.wickVisible,
2215
+ wickUpColor: override.wickUpColor ?? base.wickUpColor,
2216
+ wickDownColor: override.wickDownColor ?? base.wickDownColor
2217
+ }
2218
+ };
2219
+ }
1847
2220
  function priceStyleIds() {
1848
2221
  const out = [...BUILTIN_PRICE_STYLES];
1849
2222
  for (const t of chartTypes()) if (!out.includes(t.id)) out.push(t.id);
@@ -1882,6 +2255,40 @@ function clampLevel(v) {
1882
2255
  function clampSpacing(v) {
1883
2256
  return v < 0.1 ? 0.1 : v > 10 ? 10 : v;
1884
2257
  }
2258
+ function factoryResetConfig(factory) {
2259
+ const bag = {};
2260
+ for (const t of chartTypes()) {
2261
+ const section = t.settings;
2262
+ if (!section) continue;
2263
+ const defaults2 = {};
2264
+ const addRows = (rows) => {
2265
+ for (const r of rows ?? []) {
2266
+ if (r.kind === "heading" || r.kind === "header") continue;
2267
+ if (r.kind === "range") {
2268
+ defaults2[r.minKey] = r.defval;
2269
+ defaults2[r.maxKey] = r.defval;
2270
+ continue;
2271
+ }
2272
+ defaults2[r.key] = r.defval;
2273
+ if (r.kind === "toggle") for (const c of r.colors ?? []) defaults2[c.key] = c.defval;
2274
+ }
2275
+ };
2276
+ for (const inst of section.instances ?? []) {
2277
+ if (inst.enableKey) defaults2[inst.enableKey] = false;
2278
+ addRows(inst.rows);
2279
+ }
2280
+ for (const sub of section.subsections ?? []) {
2281
+ if (sub.enableKey) defaults2[sub.enableKey] = false;
2282
+ addRows(sub.rows);
2283
+ }
2284
+ if (!section.instances) addRows(section.rows);
2285
+ bag[t.id] = defaults2;
2286
+ }
2287
+ for (const [typeId, vals] of Object.entries(factory.chartTypes)) {
2288
+ bag[typeId] = { ...bag[typeId] ?? {}, ...vals };
2289
+ }
2290
+ return { ...factory, chartTypes: bag };
2291
+ }
1885
2292
  function mergeConfig(base, patch) {
1886
2293
  const p = asObject(patch);
1887
2294
  const ctPatch = asObject(p.chartTypes);
@@ -2081,8 +2488,8 @@ function timeframeLabel(value) {
2081
2488
  }
2082
2489
 
2083
2490
  // src/widget/topbar.ts
2084
- var STYLE_ID = "vela-topbar";
2085
- var CSS2 = `
2491
+ var STYLE_ID2 = "vela-topbar";
2492
+ var CSS3 = `
2086
2493
  .vela-widget-topbar {
2087
2494
  display: flex;
2088
2495
  align-items: center;
@@ -2194,7 +2601,7 @@ function priceStyleIcon(id) {
2194
2601
  var Topbar = class {
2195
2602
  constructor(host, opts) {
2196
2603
  this.layoutButton = null;
2197
- this.layoutMenu = null;
2604
+ this.layoutPicker = null;
2198
2605
  this.layoutId = null;
2199
2606
  this.tooltips = [];
2200
2607
  this.panelBtns = /* @__PURE__ */ new Map();
@@ -2214,7 +2621,7 @@ var Topbar = class {
2214
2621
  this.timeframe = opts.timeframe;
2215
2622
  this.priceStyle = opts.priceStyle;
2216
2623
  const doc = host.ownerDocument;
2217
- injectStyles(STYLE_ID, CSS2, doc);
2624
+ injectStyles(STYLE_ID2, CSS3, doc);
2218
2625
  this.el = doc.createElement("div");
2219
2626
  this.el.className = "vela-widget-topbar";
2220
2627
  this.symbolEl = doc.createElement("button");
@@ -2271,19 +2678,16 @@ var Topbar = class {
2271
2678
  this.tooltips.push(new Tooltip(this.styleButton, { content: "Chart style", triggerId: "vela-topbar-style", host }));
2272
2679
  if (this.layoutButton && opts.layout) {
2273
2680
  this.tooltips.push(new Tooltip(this.layoutButton, { content: "Layout", triggerId: "vela-topbar-layout", host }));
2274
- this.layoutMenu = new Menu({
2681
+ const layout = opts.layout;
2682
+ this.layoutPicker = new LayoutPicker({
2275
2683
  trigger: this.layoutButton,
2276
- triggerId: "vela-topbar-layout",
2277
2684
  host,
2278
- items: this.layoutItems(),
2279
- onSelect: (id) => {
2280
- if (id.startsWith("toggle:")) {
2281
- opts.layout.onToggle?.(id.slice("toggle:".length));
2282
- this.layoutMenu?.setItems(this.layoutItems());
2283
- return;
2284
- }
2285
- opts.layout.onSelect(id);
2286
- }
2685
+ shape: () => layout.shape(),
2686
+ presets: () => layout.presets().map((p) => ({ ...p, checked: p.id === this.layoutId })),
2687
+ onSelectGrid: (rows, cols) => layout.onSelectGrid(rows, cols),
2688
+ onSelectPreset: (id) => layout.onSelectPreset(id),
2689
+ syncs: () => layout.syncs(),
2690
+ onToggleSync: (id) => layout.onToggleSync(id)
2287
2691
  });
2288
2692
  }
2289
2693
  this.tfMenu = new Menu({
@@ -2322,7 +2726,7 @@ var Topbar = class {
2322
2726
  if (!this.layoutButton) return;
2323
2727
  this.layoutId = id;
2324
2728
  this.renderLayoutButton(this.layoutButton.ownerDocument);
2325
- this.layoutMenu?.setItems(this.layoutItems());
2729
+ this.layoutPicker?.refresh();
2326
2730
  }
2327
2731
  renderLayoutButton(doc) {
2328
2732
  if (!this.layoutButton) return;
@@ -2331,11 +2735,6 @@ var Topbar = class {
2331
2735
  else this.layoutButton.appendChild(doc.createTextNode(this.layoutId ?? ""));
2332
2736
  this.layoutButton.setAttribute("aria-label", `Layout \u2014 ${this.layoutId ?? ""}`);
2333
2737
  }
2334
- layoutItems() {
2335
- const presets = (this.opts.layout?.options() ?? []).map((l) => ({ id: l.id, label: l.label, checked: l.id === this.layoutId }));
2336
- const toggles = (this.opts.layout?.toggles?.() ?? []).map((t, i) => ({ id: `toggle:${t.id}`, label: t.label, checked: t.checked, toggle: true, separatorBefore: i === 0 }));
2337
- return [...presets, ...toggles];
2338
- }
2339
2738
  renderStyleButton(doc) {
2340
2739
  this.styleButton.replaceChildren();
2341
2740
  const icon2 = priceStyleIcon(this.priceStyle);
@@ -2400,7 +2799,7 @@ var Topbar = class {
2400
2799
  if (this.hairlineRaf) win?.cancelAnimationFrame(this.hairlineRaf);
2401
2800
  this.tfMenu.destroy();
2402
2801
  this.styleMenu.destroy();
2403
- this.layoutMenu?.destroy();
2802
+ this.layoutPicker?.destroy();
2404
2803
  for (const t of [...this.tooltips, ...this.panelTooltips]) t.destroy();
2405
2804
  this.el.remove();
2406
2805
  }
@@ -2547,8 +2946,8 @@ var RANGE_PRESETS = [
2547
2946
  { id: "ALL", tf: "W", preset: "ALL", bars: 5e3 }
2548
2947
  // everything the provider serves
2549
2948
  ];
2550
- var STYLE_ID2 = "vela-widget-bottombar";
2551
- var CSS3 = `
2949
+ var STYLE_ID3 = "vela-widget-bottombar";
2950
+ var CSS4 = `
2552
2951
  .vela-widget-bottombar {
2553
2952
  display: flex;
2554
2953
  align-items: center;
@@ -2623,7 +3022,7 @@ var Bottombar = class {
2623
3022
  this.timer = null;
2624
3023
  this.timezone = opts.timezone;
2625
3024
  const doc = host.ownerDocument;
2626
- injectStyles(STYLE_ID2, CSS3, doc);
3025
+ injectStyles(STYLE_ID3, CSS4, doc);
2627
3026
  this.el = doc.createElement("div");
2628
3027
  this.el.className = "vela-widget-bottombar";
2629
3028
  for (const preset of RANGE_PRESETS) {
@@ -8579,11 +8978,11 @@ function treeIsEmpty(panes) {
8579
8978
  }
8580
8979
 
8581
8980
  // src/widget/side-panel.ts
8582
- var STYLE_ID3 = "vela-widget-sidepanel";
8981
+ var STYLE_ID4 = "vela-widget-sidepanel";
8583
8982
  var DEFAULT_PANEL_WIDTH = 280;
8584
8983
  var DEFAULT_PANEL_MIN_WIDTH = 200;
8585
8984
  var DEFAULT_PANEL_MAX_WIDTH = 640;
8586
- var CSS4 = `
8985
+ var CSS5 = `
8587
8986
  .vela-panel[hidden] { display: none !important; }
8588
8987
  .vela-panel {
8589
8988
  position: relative;
@@ -8672,7 +9071,7 @@ var SidePanel = class {
8672
9071
  * a programmatic {@link setWidth}, so restoring a persisted width raises no change. */
8673
9072
  this.onWidthChange = null;
8674
9073
  const doc = host.ownerDocument;
8675
- injectStyles(STYLE_ID3, CSS4, doc);
9074
+ injectStyles(STYLE_ID4, CSS5, doc);
8676
9075
  this.minWidth = opts.resizable ? opts.minWidth ?? DEFAULT_PANEL_MIN_WIDTH : 1;
8677
9076
  this.maxWidth = opts.resizable ? opts.maxWidth ?? DEFAULT_PANEL_MAX_WIDTH : Number.POSITIVE_INFINITY;
8678
9077
  this.declaredWidth = clampPanelWidth(opts.width ?? DEFAULT_PANEL_WIDTH, this.minWidth, this.maxWidth);
@@ -8781,8 +9180,8 @@ var SidePanel = class {
8781
9180
  };
8782
9181
 
8783
9182
  // src/widget/object-tree.ts
8784
- var STYLE_ID4 = "vela-widget-objtree";
8785
- var CSS5 = `
9183
+ var STYLE_ID5 = "vela-widget-objtree";
9184
+ var CSS6 = `
8786
9185
  /* One pane's block. The transparent border reserves the drop-target outline. */
8787
9186
  .vela-ot-pane {
8788
9187
  border: 1px solid transparent;
@@ -9020,7 +9419,7 @@ var ObjectTree = class extends SidePanel {
9020
9419
  this.menu = null;
9021
9420
  this.menuActions = /* @__PURE__ */ new Map();
9022
9421
  this.drag = null;
9023
- injectStyles(STYLE_ID4, CSS5, host.ownerDocument);
9422
+ injectStyles(STYLE_ID5, CSS6, host.ownerDocument);
9024
9423
  this.body.addEventListener("contextmenu", (e) => this.onContextMenu(e));
9025
9424
  this.body.addEventListener("pointerdown", (e) => this.onPointerDown(e));
9026
9425
  }
@@ -9999,8 +10398,8 @@ var ObjectTree = class extends SidePanel {
9999
10398
  };
10000
10399
 
10001
10400
  // src/widget/data-window.ts
10002
- var STYLE_ID5 = "vela-widget-datawindow";
10003
- var CSS6 = `
10401
+ var STYLE_ID6 = "vela-widget-datawindow";
10402
+ var CSS7 = `
10004
10403
  .vela-dw-group {
10005
10404
  padding: 10px 8px 4px;
10006
10405
  margin-top: 4px;
@@ -10059,7 +10458,7 @@ var DataWindow = class extends SidePanel {
10059
10458
  super(host, "Data window", "vela-dw");
10060
10459
  this.chart = null;
10061
10460
  this.unsubs = [];
10062
- injectStyles(STYLE_ID5, CSS6, host.ownerDocument);
10461
+ injectStyles(STYLE_ID6, CSS7, host.ownerDocument);
10063
10462
  }
10064
10463
  toggle(open = this.el.hidden) {
10065
10464
  super.toggle(open);
@@ -10296,8 +10695,8 @@ function filterSymbols(list, query, limit = 100) {
10296
10695
  }
10297
10696
  return [...prefix, ...substr, ...desc, ...venue].slice(0, limit);
10298
10697
  }
10299
- var STYLE_ID6 = "vela-widget-symbolpicker";
10300
- var CSS7 = `
10698
+ var STYLE_ID7 = "vela-widget-symbolpicker";
10699
+ var CSS8 = `
10301
10700
  .vela-sp-searchrow {
10302
10701
  display: flex;
10303
10702
  align-items: center;
@@ -10387,7 +10786,7 @@ var SymbolPicker = class {
10387
10786
  this.seed = "";
10388
10787
  this.activeTab = "All";
10389
10788
  const doc = (opts.host ?? document.body).ownerDocument;
10390
- injectStyles(STYLE_ID6, CSS7, doc);
10789
+ injectStyles(STYLE_ID7, CSS8, doc);
10391
10790
  this.input = doc.createElement("input");
10392
10791
  this.input.className = "vela-sp-input";
10393
10792
  this.input.placeholder = "Search symbol\u2026";
@@ -10521,8 +10920,8 @@ var SymbolPicker = class {
10521
10920
  };
10522
10921
 
10523
10922
  // src/widget/indicator-picker.ts
10524
- var STYLE_ID7 = "vela-widget-indpicker";
10525
- var CSS8 = `
10923
+ var STYLE_ID8 = "vela-widget-indpicker";
10924
+ var CSS9 = `
10526
10925
  .vela-ip-searchrow {
10527
10926
  display: flex;
10528
10927
  align-items: center;
@@ -10596,7 +10995,7 @@ var IndicatorPicker = class {
10596
10995
  this.isOpen = false;
10597
10996
  this.opts = opts;
10598
10997
  const doc = (opts.host ?? document.body).ownerDocument;
10599
- injectStyles(STYLE_ID7, CSS8, doc);
10998
+ injectStyles(STYLE_ID8, CSS9, doc);
10600
10999
  this.search = doc.createElement("input");
10601
11000
  this.search.className = "vela-ip-search";
10602
11001
  this.search.placeholder = "Search\u2026";
@@ -10724,8 +11123,8 @@ var IndicatorPicker = class {
10724
11123
  };
10725
11124
 
10726
11125
  // src/widget/timeframe-quick.ts
10727
- var STYLE_ID8 = "vela-widget-tfquick";
10728
- var CSS9 = `
11126
+ var STYLE_ID9 = "vela-widget-tfquick";
11127
+ var CSS10 = `
10729
11128
  .vela-tq-input {
10730
11129
  display: block;
10731
11130
  margin: 0 auto;
@@ -10748,7 +11147,7 @@ var CSS9 = `
10748
11147
  var TimeframeQuick = class {
10749
11148
  constructor(opts) {
10750
11149
  const doc = (opts.host ?? document.body).ownerDocument;
10751
- injectStyles(STYLE_ID8, CSS9, doc);
11150
+ injectStyles(STYLE_ID9, CSS10, doc);
10752
11151
  this.input = doc.createElement("input");
10753
11152
  this.input.className = "vela-tq-input";
10754
11153
  this.input.setAttribute("spellcheck", "false");
@@ -10804,8 +11203,8 @@ var TimeframeQuick = class {
10804
11203
  };
10805
11204
 
10806
11205
  // src/widget/shortcuts-help.ts
10807
- var STYLE_ID9 = "vela-widget-shortcuts";
10808
- var CSS10 = `
11206
+ var STYLE_ID10 = "vela-widget-shortcuts";
11207
+ var CSS11 = `
10809
11208
  .vela-sh-cat {
10810
11209
  color: var(--vela-fg-muted);
10811
11210
  font-size: var(--vela-font-size-sm);
@@ -10830,7 +11229,7 @@ var ShortcutsHelp = class {
10830
11229
  constructor(keymap, host, onOpenChange) {
10831
11230
  this.keymap = keymap;
10832
11231
  const doc = (host ?? document.body).ownerDocument;
10833
- injectStyles(STYLE_ID9, CSS10, doc);
11232
+ injectStyles(STYLE_ID10, CSS11, doc);
10834
11233
  this.list = doc.createElement("div");
10835
11234
  this.dialog = new Dialog({
10836
11235
  title: "Keyboard shortcuts",
@@ -10900,8 +11299,8 @@ var ShortcutsHelp = class {
10900
11299
  };
10901
11300
 
10902
11301
  // src/widget/toast.ts
10903
- var STYLE_ID10 = "vela-widget-toast";
10904
- var CSS11 = `
11302
+ var STYLE_ID11 = "vela-widget-toast";
11303
+ var CSS12 = `
10905
11304
  .vela-toast {
10906
11305
  position: absolute;
10907
11306
  left: 50%;
@@ -10931,7 +11330,7 @@ var CSS11 = `
10931
11330
  var Toast = class {
10932
11331
  constructor(host) {
10933
11332
  this.timer = null;
10934
- injectStyles(STYLE_ID10, CSS11, host.ownerDocument);
11333
+ injectStyles(STYLE_ID11, CSS12, host.ownerDocument);
10935
11334
  this.el = host.ownerDocument.createElement("div");
10936
11335
  this.el.className = "vela-toast";
10937
11336
  host.appendChild(this.el);
@@ -11188,6 +11587,8 @@ var DrawingToolbar = class {
11188
11587
  this.eraserActive = false;
11189
11588
  this.stayBtn = null;
11190
11589
  this.stayActive = false;
11590
+ this.syncBtn = null;
11591
+ this.syncActive = false;
11191
11592
  this.collapseBtn = null;
11192
11593
  this.collapsed = false;
11193
11594
  this.visible = false;
@@ -11204,6 +11605,7 @@ var DrawingToolbar = class {
11204
11605
  this.dock = options.dock ?? "absolute";
11205
11606
  this.onCollapse = options.onCollapse ?? (() => {
11206
11607
  });
11608
+ this.onDrawingsSync = options.onDrawingsSync ?? null;
11207
11609
  ensureStyles();
11208
11610
  this.root = document.createElement("div");
11209
11611
  this.root.className = "vela-dtb";
@@ -11302,6 +11704,10 @@ var DrawingToolbar = class {
11302
11704
  this.root.appendChild(this.makeMagnetCell());
11303
11705
  this.stayBtn = this.makeButton(STAY_ICON, "Stay in drawing mode", () => this.toggleStay());
11304
11706
  this.root.appendChild(this.stayBtn);
11707
+ if (this.onDrawingsSync) {
11708
+ this.syncBtn = this.makeButton(SYNC_ICON, "Sync drawings on all charts", () => this.toggleDrawingsSync());
11709
+ this.root.appendChild(this.syncBtn);
11710
+ }
11305
11711
  this.collapseBtn = this.makeButton(COLLAPSE_ICON, "Collapse toolbar", () => this.toggleCollapsed());
11306
11712
  this.collapseBtn.classList.add("vela-dtb-collapse");
11307
11713
  this.root.appendChild(this.collapseBtn);
@@ -11309,6 +11715,7 @@ var DrawingToolbar = class {
11309
11715
  this.paintEraser();
11310
11716
  this.paintMagnet();
11311
11717
  this.paintStay();
11718
+ this.paintDrawingsSync();
11312
11719
  this.paintCollapse();
11313
11720
  this.highlight();
11314
11721
  }
@@ -11359,7 +11766,7 @@ var DrawingToolbar = class {
11359
11766
  cell.appendChild(arrow);
11360
11767
  }
11361
11768
  this.tipDisposers.push(
11362
- attachChromeTooltip(cell, { host: this.host, theme: () => this.theme, text: () => this.tipText.get(cell) ?? label, placement: "right", delayMs: 2e3 })
11769
+ attachChromeTooltip(cell, { host: this.host, theme: () => this.theme, text: () => this.tipText.get(cell) ?? label, placement: "right" })
11363
11770
  );
11364
11771
  return { cell, icon: icon2, arrow };
11365
11772
  }
@@ -11469,6 +11876,23 @@ var DrawingToolbar = class {
11469
11876
  b.dataset.active = this.stayActive ? "1" : "";
11470
11877
  b.style.opacity = this.stayActive ? "1" : "0.5";
11471
11878
  }
11879
+ /** Toggle drawings sync on/off and notify the host. */
11880
+ toggleDrawingsSync() {
11881
+ this.syncActive = !this.syncActive;
11882
+ this.onDrawingsSync?.(this.syncActive);
11883
+ this.paintDrawingsSync();
11884
+ }
11885
+ /** Reflect drawings sync externally (e.g. set through the API) without notifying back. */
11886
+ setDrawingsSyncMode(on) {
11887
+ this.syncActive = on;
11888
+ this.paintDrawingsSync();
11889
+ }
11890
+ paintDrawingsSync() {
11891
+ const b = this.syncBtn;
11892
+ if (!b) return;
11893
+ b.dataset.active = this.syncActive ? "1" : "";
11894
+ b.style.opacity = this.syncActive ? "1" : "0.5";
11895
+ }
11472
11896
  // ── flyout ──
11473
11897
  /** Create the positioned flyout panel anchored to a cell, register it as open, and start the
11474
11898
  * outside-dismiss. Callers fill it with items. */
@@ -11628,6 +12052,8 @@ var DrawingToolbar = class {
11628
12052
  const type = this.lastUsed.get(group.id) ?? group.tools[0]?.type;
11629
12053
  const tool = group.tools.find((t) => t.type === type) ?? group.tools[0];
11630
12054
  iconBtn.innerHTML = hitHtml(tool?.icon ?? "");
12055
+ const cell = this.groupCells.get(group.id);
12056
+ if (cell) this.tipText.set(cell, tool?.label ?? group.label);
11631
12057
  }
11632
12058
  /** Reconcile active tints from state (armed group / open flyout / cursor / modes). Active is a
11633
12059
  * `data-active` attribute the stylesheet paints — no inline background, so hover never sticks. */
@@ -11651,7 +12077,7 @@ var DrawingToolbar = class {
11651
12077
  this.tipText.set(btn, title);
11652
12078
  if (icon2) btn.innerHTML = hitHtml(icon2);
11653
12079
  this.tipDisposers.push(
11654
- attachChromeTooltip(btn, { host: this.host, theme: () => this.theme, text: () => this.tipText.get(btn) ?? title, placement: "right", delayMs: 2e3 })
12080
+ attachChromeTooltip(btn, { host: this.host, theme: () => this.theme, text: () => this.tipText.get(btn) ?? title, placement: "right" })
11655
12081
  );
11656
12082
  btn.addEventListener("click", onClick);
11657
12083
  return btn;
@@ -11663,13 +12089,13 @@ var DrawingToolbar = class {
11663
12089
  }
11664
12090
  };
11665
12091
  var MAGNET_ID = "__magnet__";
11666
- var STYLE_ID11 = "vela-drawing-toolbar-styles";
12092
+ var STYLE_ID12 = "vela-drawing-toolbar-styles";
11667
12093
  function ensureStyles() {
11668
12094
  if (typeof document === "undefined") return;
11669
- let s = document.getElementById(STYLE_ID11);
12095
+ let s = document.getElementById(STYLE_ID12);
11670
12096
  if (!s) {
11671
12097
  s = document.createElement("style");
11672
- s.id = STYLE_ID11;
12098
+ s.id = STYLE_ID12;
11673
12099
  document.head.appendChild(s);
11674
12100
  }
11675
12101
  s.textContent = `
@@ -11717,6 +12143,7 @@ var COLLAPSE_ICON = icon("chevrons-left");
11717
12143
  var EXPAND_ICON = icon("chevrons-right");
11718
12144
  var MAGNET_ICON = icon("magnet");
11719
12145
  var STAY_ICON = icon("pen-lock");
12146
+ var SYNC_ICON = icon("pen-sync");
11720
12147
  var ERASER_ICON = icon("eraser");
11721
12148
  var ARROW_ICON2 = icon("chevron-right");
11722
12149
  var CHECK_ICON = icon("check");
@@ -11742,8 +12169,8 @@ var LUXALGO_WORDMARK_SVG = '<svg viewBox="0 0 964 252" aria-hidden="true"><path
11742
12169
 
11743
12170
  // src/renderers/native/chrome/AttributionMark.ts
11744
12171
  var ATTRIBUTION_URL = "https://luxalgo.com/vela";
11745
- var STYLE_ID12 = "vela-attribution-styles";
11746
- var CSS12 = `
12172
+ var STYLE_ID13 = "vela-attribution-styles";
12173
+ var CSS13 = `
11747
12174
  .vela-attribution {
11748
12175
  text-decoration: none;
11749
12176
  display: flex;
@@ -11790,13 +12217,13 @@ var CSS12 = `
11790
12217
  transform: translateX(0);
11791
12218
  }`;
11792
12219
  function ensureStyles2(doc) {
11793
- let s = doc.getElementById(STYLE_ID12);
12220
+ let s = doc.getElementById(STYLE_ID13);
11794
12221
  if (!s) {
11795
12222
  s = doc.createElement("style");
11796
- s.id = STYLE_ID12;
12223
+ s.id = STYLE_ID13;
11797
12224
  doc.head.appendChild(s);
11798
12225
  }
11799
- s.textContent = CSS12;
12226
+ s.textContent = CSS13;
11800
12227
  }
11801
12228
  function attributionMarkColor(background) {
11802
12229
  return isDarkColor(background) ? "#ffffff" : "#000000";
@@ -12361,6 +12788,11 @@ var DrawingController = class {
12361
12788
  setToolbar(option) {
12362
12789
  this.port?.setToolbar(buildToolbar(option).definition);
12363
12790
  }
12791
+ /** Display (or clear) another chart's in-progress placement as a ghost — silently
12792
+ * inert on a renderer without the optional `setExternalGhost` seam. */
12793
+ setExternalGhost(doc) {
12794
+ this.port?.setExternalGhost?.(doc);
12795
+ }
12364
12796
  /** Push per-tool shortcut hints (pre-formatted display strings) to the toolbar flyouts. */
12365
12797
  setToolShortcuts(map) {
12366
12798
  this.port?.setToolShortcuts?.(map);
@@ -12591,6 +13023,9 @@ var DrawingController = class {
12591
13023
  case "arm":
12592
13024
  this.setTool(i.type);
12593
13025
  break;
13026
+ case "draft":
13027
+ this.events.emit("drawing:draft", { doc: i.doc });
13028
+ break;
12594
13029
  case "create": {
12595
13030
  const before = this.store.serialize();
12596
13031
  const last = this.lastStyle.get(i.doc.type);
@@ -12762,7 +13197,7 @@ function sanitizeSync(raw) {
12762
13197
  if (raw == null || typeof raw !== "object") return null;
12763
13198
  const s = raw;
12764
13199
  const out = {};
12765
- for (const kind of ["viewport", "symbol", "timeframe", "crosshair"]) {
13200
+ for (const kind of ["viewport", "symbol", "timeframe", "crosshair", "drawings"]) {
12766
13201
  const v = s[kind];
12767
13202
  if (v === true) out[kind] = true;
12768
13203
  else if (v != null && typeof v === "object") {
@@ -14856,6 +15291,14 @@ var DrawingsControl = class {
14856
15291
  if (this.ok("setToolShortcuts")) this.ctrl.setToolShortcuts(map);
14857
15292
  return this;
14858
15293
  }
15294
+ /** Display another chart's in-progress placement as a GHOST at reduced opacity
15295
+ * (`null` clears it) — how a multi-chart host mirrors `drawing:draft` onto linked
15296
+ * charts. Never a real drawing: no store entry, no selection, no persistence.
15297
+ * Silently inert on a renderer without the optional seam. */
15298
+ setExternalGhost(doc) {
15299
+ this.ctrl.setExternalGhost(doc);
15300
+ return this;
15301
+ }
14859
15302
  /** Create a drawing programmatically (no clicking). Returns it, or null if unsupported. */
14860
15303
  add(type, init = {}) {
14861
15304
  if (!this.ok("add")) return null;
@@ -16201,13 +16644,13 @@ var ICONS = {
16201
16644
  maximize: icon("maximize"),
16202
16645
  restore: icon("restore")
16203
16646
  };
16204
- var STYLE_ID13 = "vela-pane-controls";
16647
+ var STYLE_ID14 = "vela-pane-controls";
16205
16648
  var ICON_PX = 12;
16206
16649
  var CLUSTER_PILL = "rgba(0,0,0,0.28)";
16207
16650
  function ensureStyles3() {
16208
- if (typeof document === "undefined" || document.getElementById(STYLE_ID13)) return;
16651
+ if (typeof document === "undefined" || document.getElementById(STYLE_ID14)) return;
16209
16652
  const st = document.createElement("style");
16210
- st.id = STYLE_ID13;
16653
+ st.id = STYLE_ID14;
16211
16654
  st.textContent = `
16212
16655
  .vela-pc-btn{display:inline-flex;align-items:center;justify-content:center;padding:0;border:none;border-radius:var(--vela-radius-sm);background:transparent;line-height:0;font-size:${ICON_PX}px;color:var(--vela-fg-muted);}
16213
16656
  .vela-pc-btn svg{display:block;}
@@ -16899,6 +17342,9 @@ var SceneGraph = class {
16899
17342
  this.priceStyle = "candles";
16900
17343
  /** Price-series base painting for the ACTIVE style (see ChartTypeDefinition.basePainting). */
16901
17344
  this.basePainting = "candles";
17345
+ /** The ACTIVE style's own candle cosmetics (`chartTypes.<id>.candle*`) when it is a
17346
+ * candle-based plugin type; null ⇒ paint with the shared `style.candle` block. */
17347
+ this.candleOverride = null;
16902
17348
  /** Explicit baseline reference price for `priceStyle:'baseline'`; when null the
16903
17349
  * baseline follows `style.baseline.baselineLevel` as a percent of the visible pane
16904
17350
  * range (resolved per frame via `baselinePriceFor`). */
@@ -18128,14 +18574,15 @@ var WebGL2Backend = class {
18128
18574
  emitCandles(b, scene, bars, i0, i1, coords, pane, theme, barColors) {
18129
18575
  const spacing = coords.bodySpacing();
18130
18576
  const tier = candleTier(spacing);
18577
+ const paint = effectiveCandlePaint(scene.style.candle, scene.candleOverride, theme.upColor, theme.downColor);
18131
18578
  if (tier === "aggregate") {
18132
- this.emitCandlesAggregated(b, bars, i0, i1, coords, pane, theme.upColor, theme.downColor, barColors);
18579
+ this.emitCandlesAggregated(b, bars, i0, i1, coords, pane, paint.up, paint.down, barColors);
18133
18580
  return;
18134
18581
  }
18135
18582
  const drawBody = tier === "full";
18136
- const up = theme.upColor;
18137
- const down = theme.downColor;
18138
- const cs = scene.style.candle;
18583
+ const up = paint.up;
18584
+ const down = paint.down;
18585
+ const cs = paint.candle;
18139
18586
  const fading = this.candleStructureAlpha > this.candleBodyAlpha + 1e-3;
18140
18587
  for (let i = i0; i <= i1; i += 1) {
18141
18588
  const bar = bars[i];
@@ -18727,7 +19174,6 @@ function effectiveSnapMode(momentaryOverride, sticky) {
18727
19174
  return momentaryOverride ? "strong" : sticky;
18728
19175
  }
18729
19176
  var InputController = class {
18730
- // the last middle press deleted a drawing (suppress autoscroll/paste)
18731
19177
  constructor(deps) {
18732
19178
  this.deps = deps;
18733
19179
  /** When true (the default), wheel-zoom pins the right edge instead of the bar
@@ -18755,9 +19201,23 @@ var InputController = class {
18755
19201
  this.vx = 0;
18756
19202
  // smoothed pointer velocity, px/ms
18757
19203
  this.middleDeleted = false;
19204
+ // the last middle press deleted a drawing (suppress autoscroll/paste)
19205
+ // Last cursor position over the element (NaN once it leaves) — lets a modifier
19206
+ // press/release re-shape the drawings preview with the pointer stationary.
19207
+ this.cursorX = NaN;
19208
+ this.cursorY = NaN;
18758
19209
  this.onMiddleGuard = (e) => {
18759
19210
  if (e.button === 1 && this.middleDeleted) e.preventDefault();
18760
19211
  };
19212
+ /** A modifier press/release with the pointer stationary: re-issue the last cursor
19213
+ * position so the placing ghost / drag preview reflects the new state immediately
19214
+ * (Shift's 45° angle lock, Ctrl/Cmd's momentary strong magnet). */
19215
+ this.onModifier = (e) => {
19216
+ if (e.repeat || e.key !== "Shift" && e.key !== "Control" && e.key !== "Meta") return;
19217
+ if (Number.isNaN(this.cursorX)) return;
19218
+ if (this.dragging && this.region !== "drawing") return;
19219
+ this.deps.drawingsPointerMove?.(this.cursorX, this.cursorY, this.snapMode(e), e.shiftKey);
19220
+ };
18761
19221
  this.onDown = (e) => {
18762
19222
  if (e.button === 1) {
18763
19223
  const { x: x2, y: y2 } = this.local(e);
@@ -18799,6 +19259,8 @@ var InputController = class {
18799
19259
  };
18800
19260
  this.onMove = (e) => {
18801
19261
  const { x, y } = this.local(e);
19262
+ this.cursorX = x;
19263
+ this.cursorY = y;
18802
19264
  if (this.dragging) {
18803
19265
  if (this.region === "price") {
18804
19266
  if (Math.abs(y - this.startY) > DRAG_SLOP2) this.moved = true;
@@ -18812,7 +19274,7 @@ var InputController = class {
18812
19274
  this.deps.apply({ barSpacing, rightOffset: this.startRightOffset });
18813
19275
  } else if (this.region === "drawing") {
18814
19276
  if (Math.abs(x - this.startX) > DRAG_SLOP2 || Math.abs(y - this.startY) > DRAG_SLOP2) this.moved = true;
18815
- this.deps.drawingsPointerMove?.(x, y, this.snapMode(e));
19277
+ this.deps.drawingsPointerMove?.(x, y, this.snapMode(e), e.shiftKey);
18816
19278
  } else {
18817
19279
  const coords = this.deps.getCoords();
18818
19280
  const vp = coords.getViewport();
@@ -18835,7 +19297,7 @@ var InputController = class {
18835
19297
  const drawCursor = this.deps.drawingsCursor?.(x, y);
18836
19298
  const r = this.regionAt(x, y);
18837
19299
  this.el.style.cursor = drawCursor ?? (r === "price" ? "ns-resize" : r === "time" ? "ew-resize" : r === "separator" ? "row-resize" : "");
18838
- this.deps.drawingsPointerMove?.(x, y, this.snapMode(e));
19300
+ this.deps.drawingsPointerMove?.(x, y, this.snapMode(e), e.shiftKey);
18839
19301
  }
18840
19302
  this.deps.onPointerMove(x, y);
18841
19303
  };
@@ -18860,6 +19322,8 @@ var InputController = class {
18860
19322
  }
18861
19323
  };
18862
19324
  this.onLeave = () => {
19325
+ this.cursorX = NaN;
19326
+ this.cursorY = NaN;
18863
19327
  this.deps.onPointerMove(null, null);
18864
19328
  };
18865
19329
  this.onDblClick = (e) => {
@@ -18898,10 +19362,16 @@ var InputController = class {
18898
19362
  el.addEventListener("wheel", this.onWheel, { passive: false });
18899
19363
  el.addEventListener("mousedown", this.onMiddleGuard);
18900
19364
  el.addEventListener("auxclick", this.onMiddleGuard);
19365
+ const win = el.ownerDocument?.defaultView;
19366
+ win?.addEventListener("keydown", this.onModifier);
19367
+ win?.addEventListener("keyup", this.onModifier);
18901
19368
  }
18902
19369
  detach() {
18903
19370
  const el = this.el;
18904
19371
  if (!el) return;
19372
+ const win = el.ownerDocument?.defaultView;
19373
+ win?.removeEventListener("keydown", this.onModifier);
19374
+ win?.removeEventListener("keyup", this.onModifier);
18905
19375
  el.removeEventListener("pointerdown", this.onDown);
18906
19376
  el.removeEventListener("pointermove", this.onMove);
18907
19377
  el.removeEventListener("pointerup", this.onUp);
@@ -19319,12 +19789,13 @@ var Canvas2dBackend = class {
19319
19789
  drawCandles(ctx, scene, bars, i0, i1, coords, pane, theme, barColors) {
19320
19790
  const spacing = coords.bodySpacing();
19321
19791
  const tier = candleTier(spacing);
19792
+ const paint = effectiveCandlePaint(scene.style.candle, scene.candleOverride, theme.upColor, theme.downColor);
19322
19793
  if (tier === "aggregate") {
19323
- this.drawCandlesAggregated(ctx, bars, i0, i1, coords, pane, theme.upColor, theme.downColor, barColors);
19794
+ this.drawCandlesAggregated(ctx, bars, i0, i1, coords, pane, paint.up, paint.down, barColors);
19324
19795
  return;
19325
19796
  }
19326
19797
  const drawBody = tier === "full";
19327
- const cs = scene.style.candle;
19798
+ const cs = paint.candle;
19328
19799
  const fading = this.candleStructureAlpha > this.candleBodyAlpha + 1e-3;
19329
19800
  for (let i = i0; i <= i1; i += 1) {
19330
19801
  const b = bars[i];
@@ -19332,7 +19803,7 @@ var Canvas2dBackend = class {
19332
19803
  const g = candleGeometry(coords.logicalToX(i), spacing, coords.dpr, this.candleBodyScale);
19333
19804
  const x = g.center;
19334
19805
  const up = b.close >= b.open;
19335
- const dir = up ? theme.upColor : theme.downColor;
19806
+ const dir = up ? paint.up : paint.down;
19336
19807
  const bc = barColors.get(b.time);
19337
19808
  const color = bc ?? dir;
19338
19809
  let top = 0;
@@ -20443,7 +20914,7 @@ var ChromeRenderer = class {
20443
20914
  constructor() {
20444
20915
  this.canvas = null;
20445
20916
  this.ctx = null;
20446
- // The color for axis tick labels — the stable chrome surface text, set each frame in render().
20917
+ // The color for axis tick labels — the host-passed surface text, set each frame in render().
20447
20918
  this.axisTextColor = DARK_THEME.textColor;
20448
20919
  // Shared Pine-drawing renderer (line/box/label/polyline/linefill); widthCache persists.
20449
20920
  this.drawScene = new DrawingSceneRenderer({ timeToLogical: () => 0, barAt: () => null, theme: {} });
@@ -20475,8 +20946,8 @@ var ChromeRenderer = class {
20475
20946
  return dr;
20476
20947
  }
20477
20948
  /** Clear the chrome canvas and draw drawings + axes + current-price line.
20478
- * `surface` (background + text) paints the axis-scale gutters so they keep the app
20479
- * chrome color and stay readable, rather than tracking the user-editable plot cosmetics.
20949
+ * `surface` (background + text) paints the axis-scale gutters the host passes the live
20950
+ * chart background so the scales read as part of the plot, with contrast-corrected text.
20480
20951
  * Falls back to the theme's own colors when no surface is supplied. */
20481
20952
  render(scene, coords, theme, surface) {
20482
20953
  const ctx = this.ctx;
@@ -21110,11 +21581,11 @@ function buildColorPicker(color, theme, onChange) {
21110
21581
 
21111
21582
  // src/renderers/native/chrome/ColorField.ts
21112
21583
  var CHECKER2 = transparencyChecker(8);
21113
- var STYLE_ID14 = "vela-color-field";
21584
+ var STYLE_ID15 = "vela-color-field";
21114
21585
  function ensureStyles4() {
21115
- if (typeof document === "undefined" || document.getElementById(STYLE_ID14)) return;
21586
+ if (typeof document === "undefined" || document.getElementById(STYLE_ID15)) return;
21116
21587
  const st = document.createElement("style");
21117
- st.id = STYLE_ID14;
21588
+ st.id = STYLE_ID15;
21118
21589
  st.textContent = `
21119
21590
  .vela-color-field{width:24px;height:24px;padding:2px;border:1px solid var(--vela-border);border-radius:0;background:var(--vela-surface-sunken);cursor:pointer;display:inline-flex;flex:none;}
21120
21591
  .vela-color-field:hover{border-color:var(--vela-fg-muted);}
@@ -21243,6 +21714,37 @@ function ensureControlStyles() {
21243
21714
  .vela-sd-btn:hover{background:var(--vela-hover);border-color:var(--vela-border-strong);color:var(--vela-fg-bright);}
21244
21715
  .vela-sd-close{cursor:pointer;display:inline-flex;align-items:center;justify-content:center;background:transparent;border:none;color:var(--vela-fg-muted);line-height:0;width:30px;height:30px;border-radius:var(--vela-radius-sm);transition:background var(--vela-dur-fast) ease,color var(--vela-dur-fast) ease;}
21245
21716
  .vela-sd-close:hover{background:var(--vela-hover);color:var(--vela-fg-bright);}
21717
+ /* Rows/blocks gated away by chart-type conditions, TOC filters, or the instance strip.
21718
+ !important: the pane grid rewrites inline display ('contents') AFTER the initial
21719
+ visibility pass, so a class must win. */
21720
+ .vela-sd-hide{display:none !important;}
21721
+ /* Indented rail sub-entry (a chart-type section's subsection tab). */
21722
+ .vela-sd-tab-sub{padding-left:28px;font-weight:600;font-size:12px;}
21723
+ /* Instance strip: a tab per present instance (label, \u2715 on the active removable one)
21724
+ and a dashed + that turns on the next absent instance. The rule under it separates
21725
+ the strip from the instance's TOC + rows area. */
21726
+ .vela-sd-itabs{display:flex;align-items:center;gap:6px;flex-wrap:wrap;margin:14px 0 0;padding-bottom:12px;border-bottom:1px solid var(--vela-border);}
21727
+ .vela-sd-itab{display:inline-flex;align-items:center;gap:7px;height:30px;padding:0 11px;background:transparent;border:1px solid var(--vela-border);border-radius:var(--vela-radius-md);color:var(--vela-fg-muted);font-family:inherit;font-size:var(--vela-font-size-md);font-weight:600;cursor:pointer;transition:background var(--vela-dur-fast) ease,color var(--vela-dur-fast) ease,border-color var(--vela-dur-fast) ease;}
21728
+ .vela-sd-itab:hover{background:var(--vela-hover);color:var(--vela-fg-bright);}
21729
+ .vela-sd-itab.on{background:var(--vela-active);color:var(--vela-fg-bright);border-color:var(--vela-border-strong);}
21730
+ .vela-sd-ix{display:inline-flex;align-items:center;justify-content:center;width:16px;height:16px;margin-right:-4px;border-radius:var(--vela-radius-sm);color:var(--vela-fg-muted);font-size:10px;line-height:1;}
21731
+ .vela-sd-ix:hover{background:var(--vela-hover);color:var(--vela-fg-bright);}
21732
+ .vela-sd-itab-add{border-style:dashed;min-width:30px;justify-content:center;padding:0;}
21733
+ /* Structured pane: group TOC column + rows column, TOP-ALIGNED (the shared padding-top
21734
+ lives on the wrap, never on one column). The TOC sticks while the pane scrolls; the
21735
+ vertical rule sits on the rows column so it spans the full content height. When every
21736
+ group gates out the TOC hides and .no-toc drops the rule with it. */
21737
+ .vela-sd-struct{display:flex;align-items:flex-start;padding-top:14px;}
21738
+ .vela-sd-toc{position:sticky;top:0;flex:0 0 auto;min-width:104px;display:flex;flex-direction:column;gap:2px;padding:2px 14px 0 0;}
21739
+ .vela-sd-struct>[data-sd-rows-host]{border-left:1px solid var(--vela-border);padding-left:18px;}
21740
+ .vela-sd-struct.no-toc>[data-sd-rows-host]{border-left:none;padding-left:0;}
21741
+ .vela-sd-toc-btn{text-align:left;padding:6px 10px;background:transparent;border:none;border-radius:var(--vela-radius-sm);color:var(--vela-fg-muted);font-family:inherit;font-size:12px;font-weight:600;cursor:pointer;transition:background var(--vela-dur-fast) ease,color var(--vela-dur-fast) ease;}
21742
+ .vela-sd-toc-btn:hover{background:var(--vela-hover);color:var(--vela-fg-bright);}
21743
+ .vela-sd-toc-btn.on{background:var(--vela-active);color:var(--vela-fg-bright);}
21744
+ /* Soft-disable: a subsection's enableKey is off \u2014 rows stay visible (browseable) but
21745
+ muted and non-interactive. Applied to each row's children so it survives display:contents;
21746
+ !important beats the inline opacity on labels. */
21747
+ .vela-sd-soft>*{opacity:0.4 !important;pointer-events:none !important;}
21246
21748
  `;
21247
21749
  document.head.appendChild(st);
21248
21750
  }
@@ -21263,6 +21765,10 @@ var SettingsDialog = class {
21263
21765
  this.themeControl = null;
21264
21766
  /** The built tabs, by title — how `showSection` reaches a pane while the dialog is open. */
21265
21767
  this.tabs = [];
21768
+ /** Active instance-strip tab per chart type — remembered across dialog rebuilds. */
21769
+ this.typeActiveInstance = /* @__PURE__ */ new Map();
21770
+ /** Active TOC group per structured pane (`<typeId>/<pane>` → group label). */
21771
+ this.typeActiveGroup = /* @__PURE__ */ new Map();
21266
21772
  /** The tab currently shown, so a theme change (which rebuilds) lands back on it. */
21267
21773
  this.activeSection = null;
21268
21774
  if (getComputedStyle(container).position === "static") container.style.position = "relative";
@@ -21424,6 +21930,29 @@ var SettingsDialog = class {
21424
21930
  baseline.append(this.numberRow("Width", config.baseline.width, 1, 10, 1, (v) => this.emit({ baseline: { width: v } })));
21425
21931
  groups.baseline = baseline;
21426
21932
  body.append(baseline);
21933
+ for (const def of chartTypes()) {
21934
+ if (!hasOwnCandlePaint(def.id)) continue;
21935
+ const bag = config.chartTypes[def.id] ?? {};
21936
+ const colorOf2 = (key, shared) => typeof bag[key] === "string" && bag[key] !== "" ? bag[key] : shared;
21937
+ const boolOf = (key, shared) => typeof bag[key] === "boolean" ? bag[key] : shared;
21938
+ const g = this.group();
21939
+ g.append(this.sectionTitle("Candles"));
21940
+ g.append(this.toggleRow("Body", boolOf("candleBodyVisible", config.candles.bodyVisible), (v) => this.emitType(def.id, "candleBodyVisible", v), [
21941
+ this.swatch(colorOf2("candleUpColor", config.candles.upColor), (v) => this.emitType(def.id, "candleUpColor", v)),
21942
+ this.swatch(colorOf2("candleDownColor", config.candles.downColor), (v) => this.emitType(def.id, "candleDownColor", v))
21943
+ ]));
21944
+ g.append(this.toggleRow("Borders", boolOf("candleBorderVisible", config.candles.borderVisible), (v) => this.emitType(def.id, "candleBorderVisible", v), [
21945
+ this.swatch(colorOf2("candleBorderUpColor", config.candles.borderUpColor), (v) => this.emitType(def.id, "candleBorderUpColor", v)),
21946
+ this.swatch(colorOf2("candleBorderDownColor", config.candles.borderDownColor), (v) => this.emitType(def.id, "candleBorderDownColor", v))
21947
+ ]));
21948
+ g.append(this.toggleRow("Wick", boolOf("candleWickVisible", config.candles.wickVisible), (v) => this.emitType(def.id, "candleWickVisible", v), [
21949
+ this.swatch(colorOf2("candleWickUpColor", config.candles.wickUpColor), (v) => this.emitType(def.id, "candleWickUpColor", v)),
21950
+ this.swatch(colorOf2("candleWickDownColor", config.candles.wickDownColor), (v) => this.emitType(def.id, "candleWickDownColor", v))
21951
+ ]));
21952
+ g.append(this.numberRow("Spacing", config.series.spacing, 0.1, 10, 0.1, (v) => this.emit({ series: { spacing: v } })));
21953
+ groups[def.id] = g;
21954
+ body.append(g);
21955
+ }
21427
21956
  showActive(config.series.style);
21428
21957
  body.append(this.sectionTitle("Time zone"));
21429
21958
  body.append(this.selectRowLabeled("Time zone", normalizeTimezone(config.timeScale.timezone), timezoneOptions(config.timeScale.timezone), (v) => this.emit({ timeScale: { timezone: v } })));
@@ -21438,7 +21967,16 @@ var SettingsDialog = class {
21438
21967
  }
21439
21968
  }
21440
21969
  };
21970
+ const renderChartTypeSections = (placement) => {
21971
+ for (const def of chartTypes()) {
21972
+ const typeSettings = def.settings;
21973
+ if (!typeSettings) continue;
21974
+ if ((typeSettings.placement ?? "end") !== placement) continue;
21975
+ this.chartTypeSection(def.id, typeSettings, config, body);
21976
+ }
21977
+ };
21441
21978
  renderHostSections("symbol");
21979
+ renderChartTypeSections("after-symbol");
21442
21980
  renderHostSections("after-symbol");
21443
21981
  body.append(this.section("Scales and lines"));
21444
21982
  body.append(this.sectionTitle("Price scale"));
@@ -21479,27 +22017,7 @@ var SettingsDialog = class {
21479
22017
  body.append(this.sectionTitle("Theme"));
21480
22018
  body.append(this.selectRow("Color theme", tc.current === "dark" ? "Dark" : "Light", ["Dark", "Light"], (v) => tc.onSelect(v === "Dark" ? "dark" : "light")));
21481
22019
  }
21482
- for (const def of chartTypes()) {
21483
- const typeSettings = def.settings;
21484
- if (!typeSettings) continue;
21485
- const marker = this.section(typeSettings.title);
21486
- marker.dataset.sdStyle = def.id;
21487
- marker.dataset.sdVisibility = typeSettings.visibility ?? "active";
21488
- body.append(marker);
21489
- const values = config.chartTypes[def.id] ?? {};
21490
- for (const r of typeSettings.rows) {
21491
- const current2 = values[r.key];
21492
- if (r.kind === "toggle") {
21493
- body.append(this.boolRow(r.label, typeof current2 === "boolean" ? current2 : r.defval, (v) => this.emitType(def.id, r.key, v)));
21494
- } else if (r.kind === "number") {
21495
- body.append(this.numberRow(r.label, typeof current2 === "number" ? current2 : r.defval, r.min ?? 0, r.max ?? 1e6, r.step ?? 1, (v) => this.emitType(def.id, r.key, v)));
21496
- } else if (r.kind === "color") {
21497
- body.append(this.colorRow(r.label, typeof current2 === "string" ? current2 : r.defval, (v) => this.emitType(def.id, r.key, v)));
21498
- } else {
21499
- body.append(this.selectRow(r.label, typeof current2 === "string" ? current2 : r.defval, [...r.options], (v) => this.emitType(def.id, r.key, v)));
21500
- }
21501
- }
21502
- }
22020
+ renderChartTypeSections("end");
21503
22021
  renderHostSections("end");
21504
22022
  const shell = document.createElement("div");
21505
22023
  shell.style.cssText = "display:flex;min-height:360px;max-height:calc(70vh - 100px);flex:1 1 auto;";
@@ -21518,7 +22036,7 @@ var SettingsDialog = class {
21518
22036
  const tab = document.createElement("button");
21519
22037
  tab.type = "button";
21520
22038
  tab.textContent = title;
21521
- tab.className = "vela-sd-tab";
22039
+ tab.className = "vela-sd-tab" + (child.dataset.sdSub !== void 0 ? " vela-sd-tab-sub" : "");
21522
22040
  panes.push({ title, el, tab, style: child.dataset.sdStyle, visibility: child.dataset.sdVisibility });
21523
22041
  current = el;
21524
22042
  child.remove();
@@ -21558,7 +22076,14 @@ var SettingsDialog = class {
21558
22076
  scrim.appendChild(dlg);
21559
22077
  this.container.appendChild(scrim);
21560
22078
  this.root = scrim;
21561
- for (const p of panes) this.layoutSettingsGrids(p.el, dlg);
22079
+ for (const p of panes) {
22080
+ const hosts = [...p.el.querySelectorAll("[data-sd-rows-host]")];
22081
+ if (hosts.length === 0) {
22082
+ this.layoutSettingsGrids(p.el, dlg);
22083
+ continue;
22084
+ }
22085
+ for (const h of hosts) this.layoutSettingsGrids(h, dlg);
22086
+ }
21562
22087
  }
21563
22088
  close() {
21564
22089
  closeColorPopover();
@@ -21588,6 +22113,291 @@ var SettingsDialog = class {
21588
22113
  el.textContent = text;
21589
22114
  return el;
21590
22115
  }
22116
+ /**
22117
+ * One chart type's settings tab (and its subsection tabs), appended to the linear
22118
+ * `body` for the pane splitter to file. One live values BAG serves the whole section
22119
+ * — instances and subsections share the per-type store, so a `when` gate anywhere
22120
+ * can reference a key edited anywhere else; every edit runs every registered
22121
+ * refresher.
22122
+ */
22123
+ chartTypeSection(typeId, section, config, body) {
22124
+ const marker = this.section(section.title);
22125
+ marker.dataset.sdStyle = typeId;
22126
+ marker.dataset.sdVisibility = section.visibility ?? "active";
22127
+ body.append(marker);
22128
+ const values = config.chartTypes[typeId] ?? {};
22129
+ const bag = {};
22130
+ const seedKey = (key, want, defval) => {
22131
+ const v = values[key];
22132
+ bag[key] = typeof v === want ? v : defval;
22133
+ };
22134
+ const seed = (rows) => {
22135
+ for (const r of rows) {
22136
+ if (r.kind === "heading" || r.kind === "header") continue;
22137
+ if (r.kind === "range") {
22138
+ seedKey(r.minKey, "number", r.defval);
22139
+ seedKey(r.maxKey, "number", r.defval);
22140
+ continue;
22141
+ }
22142
+ seedKey(r.key, r.kind === "toggle" ? "boolean" : r.kind === "number" ? "number" : "string", r.defval);
22143
+ if (r.kind === "toggle") for (const c of r.colors ?? []) seedKey(c.key, "string", c.defval);
22144
+ }
22145
+ };
22146
+ if (section.rows) seed(section.rows);
22147
+ for (const inst of section.instances ?? []) seed(inst.rows);
22148
+ for (const sub of section.subsections ?? []) seed(sub.rows);
22149
+ for (const inst of section.instances ?? []) {
22150
+ if (!inst.enableKey || inst.enableKey in bag) continue;
22151
+ bag[inst.enableKey] = values[inst.enableKey] === true;
22152
+ }
22153
+ for (const sub of section.subsections ?? []) {
22154
+ if (!sub.enableKey || sub.enableKey in bag) continue;
22155
+ bag[sub.enableKey] = values[sub.enableKey] === true;
22156
+ }
22157
+ const refreshers = [];
22158
+ const put = (key, v) => {
22159
+ bag[key] = v;
22160
+ this.emitType(typeId, key, v);
22161
+ for (const r of refreshers) r();
22162
+ };
22163
+ if (section.instances && section.instances.length > 0) {
22164
+ body.append(this.instancesBlock(typeId, section.instances, bag, put, refreshers));
22165
+ } else if (section.rows) {
22166
+ this.flatTypeRows(section.rows, bag, put, refreshers, body);
22167
+ }
22168
+ for (const sub of section.subsections ?? []) {
22169
+ const subMarker = this.section(sub.title);
22170
+ subMarker.dataset.sdStyle = typeId;
22171
+ subMarker.dataset.sdVisibility = section.visibility ?? "active";
22172
+ subMarker.dataset.sdSub = "1";
22173
+ body.append(subMarker);
22174
+ body.append(this.groupedRows(`${typeId}/${sub.title}`, sub.rows, bag, put, refreshers, sub.enableKey));
22175
+ }
22176
+ for (const r of refreshers) r();
22177
+ }
22178
+ /** The FLAT chart-type form: rows appended straight to the body (the pane grid wraps
22179
+ * them), headings/headers as inline group titles, `when` gates refreshed live. */
22180
+ flatTypeRows(rows, bag, put, refreshers, body) {
22181
+ const entries = [];
22182
+ for (const r of rows) {
22183
+ const el = r.kind === "heading" || r.kind === "header" ? this.sectionTitle(r.label) : this.typeRow(r, bag, put);
22184
+ entries.push({ el, when: r.when });
22185
+ body.append(el);
22186
+ }
22187
+ refreshers.push(() => {
22188
+ for (const e of entries) e.el.classList.toggle("vela-sd-hide", !settingsRowVisible(e.when, bag));
22189
+ });
22190
+ }
22191
+ /** One value row for a chart-type descriptor, writing through `put`. */
22192
+ typeRow(r, bag, put) {
22193
+ if (r.kind === "toggle") {
22194
+ const swatches = (r.colors ?? []).map((c) => {
22195
+ const sw = this.swatch(bag[c.key], (v) => put(c.key, v));
22196
+ sw.title = c.label;
22197
+ return sw;
22198
+ });
22199
+ return this.toggleRow(r.label, bag[r.key], (v) => put(r.key, v), swatches);
22200
+ }
22201
+ if (r.kind === "number") return this.numberRow(r.label, bag[r.key], r.min ?? 0, r.max ?? 1e6, r.step ?? 1, (v) => put(r.key, v));
22202
+ if (r.kind === "color") return this.colorRow(r.label, bag[r.key], (v) => put(r.key, v));
22203
+ if (r.kind === "range") return this.rangeRow(r, bag, put);
22204
+ return this.selectRowLabeled(r.label, bag[r.key], normalizeSelectOptions(r.options), (v) => put(r.key, v));
22205
+ }
22206
+ /**
22207
+ * A MIN–MAX row: two number inputs on one row (stored under the descriptor's
22208
+ * `minKey`/`maxKey`). With a `placeholder`, an input at the DEFAULT value renders
22209
+ * empty showing it, and clearing an input stores the default back — the
22210
+ * placeholder names the unset state ('Off' for 0-disables bounds).
22211
+ */
22212
+ rangeRow(r, bag, put) {
22213
+ const { wrap } = this.row(r.label);
22214
+ const input = (key, title) => {
22215
+ const ni = document.createElement("input");
22216
+ ni.type = "number";
22217
+ ni.className = "vela-sd-number";
22218
+ ni.min = String(r.min ?? 0);
22219
+ ni.max = String(r.max ?? 1e6);
22220
+ ni.step = String(r.step ?? 1);
22221
+ ni.title = title;
22222
+ const current = bag[key];
22223
+ if (r.placeholder !== void 0) {
22224
+ ni.placeholder = r.placeholder;
22225
+ if (current !== r.defval) ni.value = String(current);
22226
+ } else {
22227
+ ni.value = String(current);
22228
+ }
22229
+ ni.addEventListener("change", () => {
22230
+ const raw = ni.value.trim() === "" ? r.defval : Number(ni.value);
22231
+ const v = Number.isFinite(raw) ? Math.min(r.max ?? Infinity, Math.max(r.min ?? -Infinity, raw)) : r.defval;
22232
+ ni.value = r.placeholder !== void 0 && v === r.defval ? "" : String(v);
22233
+ put(key, v);
22234
+ });
22235
+ return ni;
22236
+ };
22237
+ const box = document.createElement("div");
22238
+ box.style.cssText = "display:flex;align-items:center;gap:6px;flex:0 0 auto;";
22239
+ const dash = document.createElement("span");
22240
+ dash.textContent = "\u2013";
22241
+ dash.style.cssText = "color:var(--vela-fg-muted);";
22242
+ box.append(input(r.minKey, `${r.label} \u2014 min`), dash, input(r.maxKey, `${r.label} \u2014 max`));
22243
+ wrap.appendChild(box);
22244
+ return wrap;
22245
+ }
22246
+ /**
22247
+ * The INSTANCE STRIP block: a tab per present instance (label, `×` on the active
22248
+ * removable one), a dashed `+` while an instance is still absent, and one
22249
+ * grouped-rows content per instance below — only the active tab's content shows.
22250
+ * Presence is the boolean at each instance's `enableKey`; the strip rebuilds on
22251
+ * every section edit, so gates elsewhere stay coherent.
22252
+ */
22253
+ instancesBlock(typeId, instances, bag, put, refreshers) {
22254
+ const wrap = document.createElement("div");
22255
+ const strip = document.createElement("div");
22256
+ strip.className = "vela-sd-itabs";
22257
+ wrap.append(strip);
22258
+ const contents = instances.map((inst, i) => {
22259
+ const content = this.groupedRows(`${typeId}/#${i}`, inst.rows, bag, put, refreshers);
22260
+ wrap.append(content);
22261
+ return content;
22262
+ });
22263
+ const present = (inst) => !inst.enableKey || bag[inst.enableKey] === true;
22264
+ const refresh = () => {
22265
+ let active = this.typeActiveInstance.get(typeId) ?? 0;
22266
+ if (!present(instances[active] ?? instances[0])) active = 0;
22267
+ this.typeActiveInstance.set(typeId, active);
22268
+ strip.replaceChildren();
22269
+ instances.forEach((inst, i) => {
22270
+ if (!present(inst)) return;
22271
+ const tab = document.createElement("button");
22272
+ tab.type = "button";
22273
+ tab.className = "vela-sd-itab" + (i === active ? " on" : "");
22274
+ const lbl = document.createElement("span");
22275
+ lbl.textContent = inst.label;
22276
+ tab.append(lbl);
22277
+ if (i === active && inst.enableKey) {
22278
+ const x = document.createElement("span");
22279
+ x.className = "vela-sd-ix";
22280
+ x.textContent = "\u2715";
22281
+ x.title = `Remove ${inst.label}`;
22282
+ x.addEventListener("click", (e) => {
22283
+ e.stopPropagation();
22284
+ this.typeActiveInstance.set(typeId, 0);
22285
+ put(inst.enableKey, false);
22286
+ });
22287
+ tab.append(x);
22288
+ }
22289
+ tab.addEventListener("click", () => {
22290
+ this.typeActiveInstance.set(typeId, i);
22291
+ refresh();
22292
+ });
22293
+ strip.append(tab);
22294
+ });
22295
+ const absent = instances.findIndex((inst) => !present(inst));
22296
+ if (absent >= 0) {
22297
+ const add = document.createElement("button");
22298
+ add.type = "button";
22299
+ add.className = "vela-sd-itab vela-sd-itab-add";
22300
+ add.textContent = "+";
22301
+ add.title = `Add ${instances[absent].label}`;
22302
+ add.addEventListener("click", () => {
22303
+ this.typeActiveInstance.set(typeId, absent);
22304
+ put(instances[absent].enableKey, true);
22305
+ });
22306
+ strip.append(add);
22307
+ }
22308
+ contents.forEach((c, i) => c.classList.toggle("vela-sd-hide", i !== active));
22309
+ };
22310
+ refreshers.push(refresh);
22311
+ return wrap;
22312
+ }
22313
+ /**
22314
+ * A GROUPED rows pane: a TOC column of the group labels (from `heading` rows) and
22315
+ * ONE rows host holding every row — the active group's rows show, the rest hide.
22316
+ * Rows BEFORE the first heading are the always block, visible above every group.
22317
+ * `header` rows stay in the rows column as in-group subgroup titles (not TOC
22318
+ * entries). A group whose value rows are all gated out (or whose heading's own
22319
+ * `when` fails) leaves the TOC; the whole TOC hides when no group is live. The
22320
+ * rows host is tagged for `layoutSettingsGrids`, keeping one shared label column
22321
+ * across all groups.
22322
+ *
22323
+ * `enableKey` (optional): while that bag boolean is false, every row except the one
22324
+ * whose key matches is soft-disabled (visible but grayed / non-interactive) so the
22325
+ * pane stays browseable with the feature off.
22326
+ */
22327
+ groupedRows(paneKey, rows, bag, put, refreshers, enableKey) {
22328
+ const wrap = document.createElement("div");
22329
+ wrap.className = "vela-sd-struct";
22330
+ const toc = document.createElement("div");
22331
+ toc.className = "vela-sd-toc";
22332
+ const rowsHost = document.createElement("div");
22333
+ rowsHost.dataset.sdRowsHost = "1";
22334
+ rowsHost.style.cssText = "flex:1 1 auto;min-width:0;";
22335
+ wrap.append(toc, rowsHost);
22336
+ const entries = [];
22337
+ const groups = [];
22338
+ const groupWhens = [];
22339
+ let g = -1;
22340
+ for (const r of rows) {
22341
+ if (r.kind === "heading") {
22342
+ g = groups.length;
22343
+ groups.push(r.label);
22344
+ groupWhens.push(r.when);
22345
+ continue;
22346
+ }
22347
+ if (r.kind === "header") {
22348
+ entries.push({ el: this.sectionTitle(r.label), when: r.when, group: g, header: true });
22349
+ rowsHost.append(entries[entries.length - 1].el);
22350
+ continue;
22351
+ }
22352
+ const el = this.typeRow(r, bag, put);
22353
+ const key = r.kind === "range" ? void 0 : r.key;
22354
+ entries.push({ el, when: r.when, group: g, key });
22355
+ rowsHost.append(el);
22356
+ }
22357
+ const refresh = () => {
22358
+ const live = groups.map((_, gi) => settingsRowVisible(groupWhens[gi], bag) && entries.some((e) => e.group === gi && !e.header && settingsRowVisible(e.when, bag)));
22359
+ let activeIdx = groups.indexOf(this.typeActiveGroup.get(paneKey) ?? "");
22360
+ if (activeIdx < 0 || !live[activeIdx]) activeIdx = live.findIndex(Boolean);
22361
+ if (activeIdx >= 0) this.typeActiveGroup.set(paneKey, groups[activeIdx]);
22362
+ toc.replaceChildren();
22363
+ groups.forEach((label, gi) => {
22364
+ if (!live[gi]) return;
22365
+ const b = document.createElement("button");
22366
+ b.type = "button";
22367
+ b.className = "vela-sd-toc-btn" + (gi === activeIdx ? " on" : "");
22368
+ b.textContent = label;
22369
+ b.addEventListener("click", () => {
22370
+ this.typeActiveGroup.set(paneKey, label);
22371
+ refresh();
22372
+ });
22373
+ toc.append(b);
22374
+ });
22375
+ const anyGroup = live.some(Boolean);
22376
+ toc.classList.toggle("vela-sd-hide", !anyGroup);
22377
+ wrap.classList.toggle("no-toc", !anyGroup);
22378
+ const enabled = !enableKey || bag[enableKey] === true;
22379
+ for (let i = 0; i < entries.length; i++) {
22380
+ const e = entries[i];
22381
+ let visible = (e.group === -1 || e.group === activeIdx) && settingsRowVisible(e.when, bag);
22382
+ if (visible && e.header) {
22383
+ let hasContent = false;
22384
+ for (let j = i + 1; j < entries.length; j++) {
22385
+ const n = entries[j];
22386
+ if (n.group !== e.group || n.header) break;
22387
+ if (settingsRowVisible(n.when, bag)) {
22388
+ hasContent = true;
22389
+ break;
22390
+ }
22391
+ }
22392
+ visible = hasContent;
22393
+ }
22394
+ e.el.classList.toggle("vela-sd-hide", !visible);
22395
+ e.el.classList.toggle("vela-sd-soft", visible && !enabled && e.key !== enableKey);
22396
+ }
22397
+ };
22398
+ refreshers.push(refresh);
22399
+ return wrap;
22400
+ }
21591
22401
  /** Vertical breathing space between row clusters — grouping reads from whitespace,
21592
22402
  * so no drawn rule. */
21593
22403
  separator() {
@@ -21850,7 +22660,7 @@ var SettingsDialog = class {
21850
22660
  const { wrap } = this.row(label);
21851
22661
  const sel = document.createElement("select");
21852
22662
  sel.className = "vela-sd-select";
21853
- sel.style.cssText = "max-width:140px;flex:0 0 auto;";
22663
+ sel.style.cssText = "max-width:200px;flex:0 0 auto;";
21854
22664
  for (const [val, lbl] of options) {
21855
22665
  const o = document.createElement("option");
21856
22666
  o.value = val;
@@ -21866,7 +22676,7 @@ var SettingsDialog = class {
21866
22676
  const { wrap } = this.row(label);
21867
22677
  const sel = document.createElement("select");
21868
22678
  sel.className = "vela-sd-select";
21869
- sel.style.cssText = "max-width:140px;flex:0 0 auto;";
22679
+ sel.style.cssText = "max-width:200px;flex:0 0 auto;";
21870
22680
  for (const opt of options) {
21871
22681
  const o = document.createElement("option");
21872
22682
  o.value = opt;
@@ -21903,6 +22713,9 @@ var SettingsDialog = class {
21903
22713
  }
21904
22714
  };
21905
22715
  var AUTO_MANUAL_OPTS = [["auto", "Auto"], ["manual", "Manual"]];
22716
+ function normalizeSelectOptions(options) {
22717
+ return options.map((o) => typeof o === "string" ? [o, o] : o);
22718
+ }
21906
22719
  function timezoneOptions(current) {
21907
22720
  const options = TIMEZONES.map((t) => [t.value, tzMenuLabel(t.value, t.label)]);
21908
22721
  const normalized = normalizeTimezone(current);
@@ -23261,6 +24074,8 @@ function topDrawingAt(drawings, x, y, proj, tol = HIT_TOLERANCE) {
23261
24074
  // src/renderers/native/drawings/DrawingInteraction.ts
23262
24075
  var DRAG_SLOP3 = 3;
23263
24076
  var FREEHAND_SAMPLE_PX = 4;
24077
+ var ANGLE_SNAP_STEP = Math.PI / 4;
24078
+ var ANGLE_SNAP_TYPES = /* @__PURE__ */ new Set(["trendline", "ray", "extendedline", "infoline", "trendangle", "arrow"]);
23264
24079
  var DrawingInteraction = class {
23265
24080
  // magnet target for the ring
23266
24081
  constructor(deps) {
@@ -23301,6 +24116,27 @@ var DrawingInteraction = class {
23301
24116
  this.snapAt = changed ? { point: snapped, paneId } : null;
23302
24117
  return snapped;
23303
24118
  }
24119
+ /** Resolve a pixel to a data point with the segment angle locked to 45° steps around
24120
+ * `pivot` (Shift held on a line tool). Works in PIXEL space — the user reasons about
24121
+ * the angle they see, not about time/price units. The magnet is bypassed: snapping
24122
+ * the result to a candle afterwards would break the exact angle just enforced. */
24123
+ resolveAngleSnapped(pivot, paneId, x, y) {
24124
+ const proj = this.deps.projector();
24125
+ const px = proj.xOf(pivot.time);
24126
+ const py = proj.yOf(pivot.price, paneId);
24127
+ this.snapAt = null;
24128
+ if (py == null) return proj.pxToPoint(x, y, paneId);
24129
+ const r = Math.hypot(x - px, y - py);
24130
+ if (r < 1) return proj.pxToPoint(x, y, paneId);
24131
+ const angle = Math.round(Math.atan2(y - py, x - px) / ANGLE_SNAP_STEP) * ANGLE_SNAP_STEP;
24132
+ return proj.pxToPoint(px + r * Math.cos(angle), py + r * Math.sin(angle), paneId);
24133
+ }
24134
+ /** The pivot the free endpoint angle-snaps around while placing `draft`, or null when
24135
+ * the tool is not a two-point segment (or no anchor is committed yet). */
24136
+ placingPivot(draft) {
24137
+ if (!ANGLE_SNAP_TYPES.has(draft.type)) return null;
24138
+ return draft.anchors[draft.anchors.length - 1] ?? null;
24139
+ }
23304
24140
  /** Should the drawing layer win this press (vs pan)? Armed/placing/pressed, or over a drawing/handle. */
23305
24141
  claim(x, y) {
23306
24142
  if (this.deps.activeTool() != null || this.state.kind !== "idle") return true;
@@ -23322,12 +24158,15 @@ var DrawingInteraction = class {
23322
24158
  return this.state.kind === "pressed" && this.state.moved ? this.state.id : null;
23323
24159
  }
23324
24160
  /** A press: place the next anchor, or grab a drawing/handle (resolved as click or drag on release).
23325
- * `shift` over a drawing toggles it in/out of the selection (multi-select) instead of dragging. */
24161
+ * `shift` over a drawing toggles it in/out of the selection (multi-select) instead of dragging;
24162
+ * while placing a line tool it locks the segment angle to 45° steps. */
23326
24163
  down(x, y, mode = "off", shift = false) {
23327
24164
  const proj = this.deps.projector();
23328
24165
  if (this.state.kind === "placing") {
23329
24166
  if (this.state.draft.placementMode() !== "click") return;
23330
- this.state.draft.anchors.push(this.placementAnchor(this.state.draft, this.resolve(x, y, this.state.draft.paneId, mode)));
24167
+ const pivot = shift ? this.placingPivot(this.state.draft) : null;
24168
+ const point = pivot ? this.resolveAngleSnapped(pivot, this.state.draft.paneId, x, y) : this.resolve(x, y, this.state.draft.paneId, mode);
24169
+ this.state.draft.anchors.push(this.placementAnchor(this.state.draft, point));
23331
24170
  if (this.state.draft.anchors.length >= this.state.need) this.finalize();
23332
24171
  else this.deps.changed();
23333
24172
  return;
@@ -23352,8 +24191,9 @@ var DrawingInteraction = class {
23352
24191
  this.state = { kind: "pressed", id: hit.id, handle, grab: proj.pxToPoint(x, y, hit.paneId), orig: cloneAnchors(hit), px: x, py: y, moved: false };
23353
24192
  }
23354
24193
  }
23355
- /** Pointer move: advance the placing ghost, or live-preview a drag once past the slop. */
23356
- move(x, y, mode = "off") {
24194
+ /** Pointer move: advance the placing ghost, or live-preview a drag once past the slop.
24195
+ * `shift` on a line tool locks the segment angle to 45° steps (placing + handle drags). */
24196
+ move(x, y, mode = "off", shift = false) {
23357
24197
  if (this.state.kind === "idle") {
23358
24198
  const tool = this.deps.activeTool();
23359
24199
  const had = this.snapAt != null;
@@ -23367,7 +24207,8 @@ var DrawingInteraction = class {
23367
24207
  this.captureFreehand(x, y);
23368
24208
  return;
23369
24209
  }
23370
- this.state.cursor = this.resolve(x, y, this.state.draft.paneId, mode);
24210
+ const pivot = shift ? this.placingPivot(this.state.draft) : null;
24211
+ this.state.cursor = pivot ? this.resolveAngleSnapped(pivot, this.state.draft.paneId, x, y) : this.resolve(x, y, this.state.draft.paneId, mode);
23371
24212
  this.deps.changed();
23372
24213
  return;
23373
24214
  }
@@ -23378,7 +24219,11 @@ var DrawingInteraction = class {
23378
24219
  this.state.moved = true;
23379
24220
  if (this.state.handle >= 0) {
23380
24221
  const a = d.anchors[this.state.handle];
23381
- if (a) applyFree(a, this.resolve(x, y, d.paneId, mode), d.anchorSchema().slots[this.state.handle]?.free ?? "both");
24222
+ if (a) {
24223
+ const pivot = shift && ANGLE_SNAP_TYPES.has(d.type) && d.anchors.length === 2 ? d.anchors[1 - this.state.handle] : void 0;
24224
+ const point = pivot ? this.resolveAngleSnapped(pivot, d.paneId, x, y) : this.resolve(x, y, d.paneId, mode);
24225
+ applyFree(a, point, d.anchorSchema().slots[this.state.handle]?.free ?? "both");
24226
+ }
23382
24227
  d.constrainHandleDrag(this.state.handle);
23383
24228
  } else {
23384
24229
  const pt = this.deps.projector().pxToPoint(x, y, d.paneId);
@@ -23547,11 +24392,11 @@ function applyFree(anchor, pt, free) {
23547
24392
  // src/renderers/native/drawings/DrawingSettingsPopup.ts
23548
24393
  var BTN = 30;
23549
24394
  var ICON = 21;
23550
- var STYLE_ID15 = "vela-drawing-popup-styles";
24395
+ var STYLE_ID16 = "vela-drawing-popup-styles";
23551
24396
  function ensureStyles5() {
23552
- if (typeof document === "undefined" || document.getElementById(STYLE_ID15)) return;
24397
+ if (typeof document === "undefined" || document.getElementById(STYLE_ID16)) return;
23553
24398
  const s = document.createElement("style");
23554
- s.id = STYLE_ID15;
24399
+ s.id = STYLE_ID16;
23555
24400
  s.textContent = `
23556
24401
  .vela-dpop input[type=color]{-webkit-appearance:none;appearance:none;border:none;padding:0;background:none;cursor:pointer;border-radius:0;}
23557
24402
  .vela-dpop input[type=color]::-webkit-color-swatch-wrapper{padding:0;}
@@ -24887,6 +25732,10 @@ var UserDrawingController = class {
24887
25732
  this.activeTool = null;
24888
25733
  // last-used style for the armed tool (seeds the placement ghost)
24889
25734
  this.intentCb = null;
25735
+ /** Another chart's in-progress placement, mirrored here as a ghost (drawings sync). */
25736
+ this.externalGhost = null;
25737
+ /** Last draft fingerprint reported upstream — gates the per-render emission to actual changes. */
25738
+ this.lastDraftKey = null;
24890
25739
  this.measure = new MeasureOverlay();
24891
25740
  // transient ruler — not a persistent drawing
24892
25741
  this.toolbarVisible = false;
@@ -24958,6 +25807,11 @@ var UserDrawingController = class {
24958
25807
  syncToolbarGutter() {
24959
25808
  this.deps.setToolbarGutter(this.toolbarVisible ? this.toolbarCollapsed ? TOOLBAR_COLLAPSED_WIDTH : TOOLBAR_WIDTH : 0);
24960
25809
  }
25810
+ /** Core push: mirror (or clear) another chart's in-progress placement as a ghost. */
25811
+ setExternalGhost(doc) {
25812
+ this.externalGhost = doc ? deserializeDrawing(doc) : null;
25813
+ this.render();
25814
+ }
24961
25815
  syncDrawings(docs) {
24962
25816
  this.drawings = docs.map((d) => deserializeDrawing(d)).filter((d) => d != null);
24963
25817
  if (this.textEditor && !this.editedDrawing(this.textEditor.id)) this.closeTextEditor();
@@ -25116,7 +25970,7 @@ var UserDrawingController = class {
25116
25970
  }
25117
25971
  this.interaction.down(x, y, snap, shift);
25118
25972
  }
25119
- pointerMove(x, y, snap = "off") {
25973
+ pointerMove(x, y, snap = "off", shift = false) {
25120
25974
  if (this.eraserMode) {
25121
25975
  if (this.erasing) this.deleteAt(x, y);
25122
25976
  return;
@@ -25126,7 +25980,7 @@ var UserDrawingController = class {
25126
25980
  this.render();
25127
25981
  return;
25128
25982
  }
25129
- this.interaction.move(x, y, snap);
25983
+ this.interaction.move(x, y, snap, shift);
25130
25984
  this.updateHover(x, y);
25131
25985
  }
25132
25986
  /** Track which drawing is hovered so its handles appear on hover (and only then). */
@@ -25512,6 +26366,8 @@ var UserDrawingController = class {
25512
26366
  this.layoutTextEditor();
25513
26367
  const ghost = this.interaction.ghost();
25514
26368
  if (ghost) this.painter.paintGhost(ctx, ghost, proj, this.deps.theme());
26369
+ if (this.externalGhost) this.painter.paintGhost(ctx, this.externalGhost, proj, this.deps.theme());
26370
+ this.emitDraft(ghost);
25515
26371
  const markers = this.interaction.placingMarkers(proj);
25516
26372
  if (markers) this.painter.paintHandles(ctx, markers);
25517
26373
  const m = this.interaction.snapMarker();
@@ -25573,6 +26429,15 @@ var UserDrawingController = class {
25573
26429
  }
25574
26430
  }, () => this.clearSelection());
25575
26431
  }
26432
+ /** Report placement progress upstream (`draft` intent) so the drawings sync can
26433
+ * mirror the ghost live on linked charts. Called from every render; only actual
26434
+ * shape changes emit, and the end of a placement emits one `null`. */
26435
+ emitDraft(ghost) {
26436
+ const key = ghost ? JSON.stringify({ t: ghost.type, p: ghost.paneId, a: ghost.anchors, s: ghost.style, x: ghost.text }) : null;
26437
+ if (key === this.lastDraftKey) return;
26438
+ this.lastDraftKey = key;
26439
+ this.intentCb?.({ kind: "draft", doc: ghost ? ghost.serialize() : null });
26440
+ }
25576
26441
  emit(i) {
25577
26442
  if (i.kind === "create") {
25578
26443
  if (i.doc.text && i.doc.text.color === void 0) {
@@ -25998,10 +26863,10 @@ var SEPARATOR_HIT_PX = 4;
25998
26863
  var NativeRenderer = class {
25999
26864
  constructor(opts) {
26000
26865
  this.capabilities = NATIVE_CAPABILITIES;
26001
- // The app "chrome" surface (drawing toolbar + axis-scale gutters): background + text.
26866
+ // The app "chrome" surface (drawing toolbar + in-chart dialogs): background + text.
26002
26867
  // Owned by the app theme alone (mount/setTheme) — config-level background/text edits
26003
- // (settings dialog, persisted configs) recolor only the plot, keeping the toolbar and
26004
- // scales on a consistent, always-readable surface.
26868
+ // (settings dialog, persisted configs) recolor the plot AND the axis scales (see
26869
+ // axisSurface), but never the toolbar/dialog chrome.
26005
26870
  this.surfaceBackground = DARK_THEME.background;
26006
26871
  this.surfaceTextColor = DARK_THEME.textColor;
26007
26872
  // the plot area (canvases + DOM overlays), inset to the right of the toolbar gutter
@@ -26201,6 +27066,7 @@ var NativeRenderer = class {
26201
27066
  this.candleDown = opts.downColor;
26202
27067
  this.scene.priceStyle = opts.priceStyle;
26203
27068
  this.scene.basePainting = basePaintingOf(opts.priceStyle);
27069
+ this.scene.candleOverride = candleOverrideFor(opts.priceStyle, this.scene.style.chartTypes);
26204
27070
  }
26205
27071
  this.theme = this.deriveTheme(DARK_THEME);
26206
27072
  }
@@ -26678,6 +27544,7 @@ var NativeRenderer = class {
26678
27544
  baselineLevel: next.baseline.baselineLevel
26679
27545
  };
26680
27546
  this.setPriceStyle(next.series.style);
27547
+ this.scene.candleOverride = candleOverrideFor(this.scene.priceStyle, s.chartTypes);
26681
27548
  this.scene.baselineValue = next.series.baseline;
26682
27549
  this.coords.setPitchScale(next.series.spacing);
26683
27550
  this.coords.setViewport(this.clampViewport(this.coords.getViewport().barSpacing, this.coords.getViewport().rightOffset));
@@ -26783,7 +27650,7 @@ var NativeRenderer = class {
26783
27650
  (patch) => this.applyConfig(patch),
26784
27651
  (json) => this.applyConfig(json),
26785
27652
  () => {
26786
- if (this.factoryConfig) this.applyConfig(this.factoryConfig);
27653
+ if (this.factoryConfig) this.applyConfig(factoryResetConfig(this.factoryConfig));
26787
27654
  this.settingsDialog?.close();
26788
27655
  this.openSettingsDialog();
26789
27656
  },
@@ -26984,10 +27851,16 @@ var NativeRenderer = class {
26984
27851
  borderColor: isDarkColor(this.surfaceBackground) ? DARK_THEME.borderColor : LIGHT_THEME.borderColor
26985
27852
  };
26986
27853
  }
27854
+ /** The colors the axis-scale gutters (price + time) paint with: the LIVE chart background
27855
+ * (`layout.background`) and its contrast-corrected text, so the scales read as part of the
27856
+ * plot. Only the toolbar/dialog chrome stays on the stable app-theme surface. */
27857
+ axisSurface() {
27858
+ return { background: this.theme.background, textColor: this.theme.textColor };
27859
+ }
26987
27860
  // ── lifecycle ──
26988
27861
  mount(container, theme) {
26989
27862
  this.mountContainer = container;
26990
- this.publishToolbarGutter();
27863
+ this.publishGutters();
26991
27864
  this.theme = this.deriveTheme(theme);
26992
27865
  this.surfaceBackground = theme.background;
26993
27866
  this.surfaceTextColor = theme.textColor;
@@ -27059,7 +27932,7 @@ var NativeRenderer = class {
27059
27932
  drawingsDeleteAt: (x, y) => this.userDrawings?.deleteAt(x, y) ?? false,
27060
27933
  drawingsSnapMode: () => this.snapMode,
27061
27934
  drawingsPointerDown: (x, y, snap, shift) => this.userDrawings?.pointerDown(x, y, snap, shift),
27062
- drawingsPointerMove: (x, y, snap) => this.userDrawings?.pointerMove(x, y, snap),
27935
+ drawingsPointerMove: (x, y, snap, shift) => this.userDrawings?.pointerMove(x, y, snap, shift),
27063
27936
  drawingsPointerUp: (x, y) => this.userDrawings?.pointerUp(x, y),
27064
27937
  drawingsCursor: (x, y) => this.userDrawings?.cursorAt(x, y) ?? null,
27065
27938
  drawingsDblClick: (x, y) => this.userDrawings?.dblClick(x, y) ?? false,
@@ -27296,6 +28169,7 @@ var NativeRenderer = class {
27296
28169
  this.attributionEl?.remove();
27297
28170
  this.attributionEl = null;
27298
28171
  this.mountContainer?.style.removeProperty("--vela-toolbar-gutter");
28172
+ this.mountContainer?.style.removeProperty("--vela-scale-gutter");
27299
28173
  this.mountContainer = null;
27300
28174
  this.wrapper?.remove();
27301
28175
  }
@@ -27464,6 +28338,7 @@ var NativeRenderer = class {
27464
28338
  const w = RIGHT_AXIS_W + AXIS_COL_W * this.maxOwnScaleColumns();
27465
28339
  if (w === this.rightAxisW) return false;
27466
28340
  this.rightAxisW = w;
28341
+ this.publishGutters();
27467
28342
  if (this.coords.width > 0) this.syncSize();
27468
28343
  return true;
27469
28344
  }
@@ -27623,6 +28498,7 @@ var NativeRenderer = class {
27623
28498
  if (style === this.scene.priceStyle) return;
27624
28499
  this.scene.priceStyle = style;
27625
28500
  this.scene.basePainting = basePaintingOf(style);
28501
+ this.scene.candleOverride = candleOverrideFor(style, this.scene.style.chartTypes);
27626
28502
  for (const cb of this.priceStyleCbs) cb(style);
27627
28503
  }
27628
28504
  onRemoveIndicator(cb) {
@@ -27784,7 +28660,10 @@ var NativeRenderer = class {
27784
28660
  active = true;
27785
28661
  }
27786
28662
  }
27787
- this.coords.setViewport(this.clampViewport(barSpacing, rightOffset));
28663
+ const clamped = this.clampViewport(barSpacing, rightOffset);
28664
+ if (clamped.barSpacing !== barSpacing) this.targetBarSpacing = clamped.barSpacing;
28665
+ if (this.scrollTargetRO != null && clamped.rightOffset !== rightOffset) this.scrollTargetRO = clamped.rightOffset;
28666
+ this.coords.setViewport(clamped);
27788
28667
  this.computeScales();
27789
28668
  if (this.easeScales(dtMs)) active = true;
27790
28669
  if (this.easeLiveBar(dtMs)) active = true;
@@ -28236,9 +29115,37 @@ var NativeRenderer = class {
28236
29115
  this.paintData();
28237
29116
  } else if (repaintsChrome(level) && this.paintedData) {
28238
29117
  this.chrome.prepare(this.scene, this.coords, this.theme);
28239
- this.chrome.render(this.scene, this.coords, this.theme, { background: this.surfaceBackground, textColor: this.surfaceTextColor });
29118
+ this.chrome.render(this.scene, this.coords, this.theme, this.axisSurface());
28240
29119
  }
28241
29120
  this.crosshairLayer.render(this.scene, this.coords, this.theme, this.hoverSeparatorY, this.externalCrossPx());
29121
+ if (!repaintsData(level) && this.paintedData) this.repaintCursorLayers();
29122
+ }
29123
+ /** Repaint the SDK layers that opted into cursor tracking (their own canvas only). */
29124
+ repaintCursorLayers() {
29125
+ const pane = this.scene.panes.get(PRICE_PANE_ID2);
29126
+ if (!pane) return;
29127
+ const nowMs = typeof performance !== "undefined" ? performance.now() : Date.now();
29128
+ for (const l of this.extLayers) {
29129
+ if (!l.def.repaintOnCursor) continue;
29130
+ l.instance.render(this.extLayerArgs(l.def.id, pane.scale, pane.bounds, nowMs));
29131
+ if (this.animZoom && l.instance.animating?.()) this.animator.start();
29132
+ }
29133
+ }
29134
+ /** One frame's args for an SDK renderer layer (shared by the data + cursor paint paths). */
29135
+ extLayerArgs(id, scale, bounds, nowMs) {
29136
+ return {
29137
+ bars: this.scene.bars,
29138
+ data: this.scene.nativeData.get(id),
29139
+ settings: this.scene.nativeData.get(`${id}-settings`) ?? {},
29140
+ pending: this.scene.nativePending.get(id) ?? [],
29141
+ coords: this.coords,
29142
+ scale,
29143
+ bounds,
29144
+ theme: this.theme,
29145
+ priceStyle: this.scene.priceStyle,
29146
+ nowMs,
29147
+ cursor: this.lastPointer
29148
+ };
28242
29149
  }
28243
29150
  /** Paint the below-data (L-1) + geometry (L0) + chrome (L1) layers from the current scene/coords. */
28244
29151
  paintData() {
@@ -28271,18 +29178,16 @@ var NativeRenderer = class {
28271
29178
  });
28272
29179
  const nowMs = typeof performance !== "undefined" ? performance.now() : Date.now();
28273
29180
  for (const l of this.extLayers) {
28274
- l.instance.render({
28275
- bars: this.scene.bars,
28276
- data: this.scene.nativeData.get(l.def.id),
28277
- settings: this.scene.nativeData.get(`${l.def.id}-settings`) ?? {},
28278
- pending: this.scene.nativePending.get(l.def.id) ?? [],
28279
- coords: this.coords,
28280
- scale: pane.scale,
28281
- bounds: pane.bounds,
28282
- theme: this.theme,
28283
- priceStyle: this.scene.priceStyle,
28284
- nowMs
28285
- });
29181
+ const args = this.extLayerArgs(l.def.id, pane.scale, pane.bounds, nowMs);
29182
+ l.instance.render(args);
29183
+ if (l.def.id === this.scene.priceStyle && l.instance.modulateBase) {
29184
+ const mod = l.instance.modulateBase(args);
29185
+ if (mod) {
29186
+ if (mod.candleBodyScale != null) this.backend.candleBodyScale = clamp012(mod.candleBodyScale) || 0.01;
29187
+ if (mod.candleBodyAlpha != null) this.backend.candleBodyAlpha = clamp012(mod.candleBodyAlpha) * this.candleBodyAlpha;
29188
+ if (mod.gridAlpha != null) this.backend.gridAlpha = clamp012(mod.gridAlpha);
29189
+ }
29190
+ }
28286
29191
  if (this.animZoom && l.instance.animating?.()) this.animator.start();
28287
29192
  }
28288
29193
  }
@@ -28292,7 +29197,7 @@ var NativeRenderer = class {
28292
29197
  if (easeLive && liveActual) this.bars[li] = { ...liveActual, high: this.liveEaseHigh, low: this.liveEaseLow, close: this.liveEaseClose };
28293
29198
  this.scene.drawingSlices = this.userDrawings?.prepareSlices(this.scene.orderedPanes().map((p) => p.id)) ?? /* @__PURE__ */ new Map();
28294
29199
  this.backend.render(this.scene, this.coords, this.theme);
28295
- this.chrome.render(this.scene, this.coords, this.theme, { background: this.surfaceBackground, textColor: this.surfaceTextColor });
29200
+ this.chrome.render(this.scene, this.coords, this.theme, this.axisSurface());
28296
29201
  this.userDrawings?.render();
28297
29202
  if (easeLive && liveActual) this.bars[li] = liveActual;
28298
29203
  this.paintedData = true;
@@ -28726,15 +29631,18 @@ var NativeRenderer = class {
28726
29631
  setToolbarGutter(px) {
28727
29632
  if (px === this.toolbarGutter) return;
28728
29633
  this.toolbarGutter = px;
28729
- this.publishToolbarGutter();
29634
+ this.publishGutters();
28730
29635
  this.positionAttribution();
28731
29636
  this.syncSize();
28732
29637
  }
28733
- /** Publish the gutter on the mount container as `--vela-toolbar-gutter`, so host
28734
- * overlays sharing that container (a status line, a custom legend) can anchor to
28735
- * the plot's left edge without reaching into the renderer's DOM. */
28736
- publishToolbarGutter() {
29638
+ /** Publish the gutters on the mount container as `--vela-toolbar-gutter` (left,
29639
+ * drawings toolbar) and `--vela-scale-gutter` (right, the full price-scale width
29640
+ * incl. merged own-scale columns), so host overlays sharing that container (a
29641
+ * status line, a watermark, a custom legend) can anchor to the plot's edges
29642
+ * without reaching into the renderer's DOM. */
29643
+ publishGutters() {
28737
29644
  this.mountContainer?.style.setProperty("--vela-toolbar-gutter", `${this.toolbarGutter}px`);
29645
+ this.mountContainer?.style.setProperty("--vela-scale-gutter", `${this.rightAxisW}px`);
28738
29646
  }
28739
29647
  /** The built-in mark, or the host's own when one is set. */
28740
29648
  buildAttributionEl() {
@@ -28804,7 +29712,13 @@ var NativeRenderer = class {
28804
29712
  this.fitContent();
28805
29713
  this.didInitialFit = true;
28806
29714
  }
28807
- this.scheduler.flushNow(4 /* Full */);
29715
+ if (this.animator.active) {
29716
+ this.computeScales();
29717
+ this.paintData();
29718
+ this.crosshairLayer.render(this.scene, this.coords, this.theme, this.hoverSeparatorY, this.externalCrossPx());
29719
+ } else {
29720
+ this.scheduler.flushNow(4 /* Full */);
29721
+ }
28808
29722
  }
28809
29723
  applyBackground() {
28810
29724
  this.wrapper.style.background = this.theme.background;
@@ -28873,6 +29787,9 @@ function sanitizeHighlights(value) {
28873
29787
  }
28874
29788
  return out.sort((a, b) => a.from - b.from);
28875
29789
  }
29790
+ function clamp012(v) {
29791
+ return Math.max(0, Math.min(1, v));
29792
+ }
28876
29793
  function isPriceStyle2(v) {
28877
29794
  return typeof v === "string" && priceStyleIds().includes(v);
28878
29795
  }
@@ -29418,8 +30335,8 @@ function fmtChange(open, close) {
29418
30335
  }
29419
30336
 
29420
30337
  // src/widget/statusline.ts
29421
- var STYLE_ID16 = "vela-widget-statusline";
29422
- var CSS13 = `
30338
+ var STYLE_ID17 = "vela-widget-statusline";
30339
+ var CSS14 = `
29423
30340
  .vela-statusline {
29424
30341
  position: absolute;
29425
30342
  top: var(--vela-space-2);
@@ -29554,7 +30471,7 @@ var Statusline = class {
29554
30471
  /** 'ohlc' for bar-shaped styles; 'value' (the single plotted close) for line styles. */
29555
30472
  this.readout = "ohlc";
29556
30473
  const doc = host.ownerDocument;
29557
- injectStyles(STYLE_ID16, CSS13, doc);
30474
+ injectStyles(STYLE_ID17, CSS14, doc);
29558
30475
  host.classList.add("vela-has-statusline");
29559
30476
  this.el = doc.createElement("div");
29560
30477
  this.el.className = "vela-statusline";
@@ -29677,11 +30594,21 @@ var Statusline = class {
29677
30594
  };
29678
30595
 
29679
30596
  // src/widget/watermark.ts
29680
- var STYLE_ID17 = "vela-widget-watermark";
29681
- var CSS14 = `
30597
+ var MAX_FONT_PX = 72;
30598
+ var MIN_FONT_PX = 12;
30599
+ var FILL = 0.9;
30600
+ var STYLE_ID18 = "vela-widget-watermark";
30601
+ var CSS15 = `
29682
30602
  .vela-watermark {
29683
30603
  position: absolute;
29684
- inset: 0;
30604
+ /* Insets follow the renderer-published gutters (mount container), so the mark
30605
+ * centers and fits within the PLOT \u2014 never bleeding into the drawings toolbar
30606
+ * on the left or the price scale on the right (visible in small multi-chart
30607
+ * cells, where the scale is a large share of the width). */
30608
+ top: 0;
30609
+ bottom: 0;
30610
+ left: var(--vela-toolbar-gutter, 0px);
30611
+ right: var(--vela-scale-gutter, 0px);
29685
30612
  display: flex;
29686
30613
  align-items: center;
29687
30614
  justify-content: center;
@@ -29689,28 +30616,48 @@ var CSS14 = `
29689
30616
  z-index: 1;
29690
30617
  color: var(--vela-fg);
29691
30618
  opacity: 0.05;
29692
- font-size: clamp(28px, 7vw, 72px);
30619
+ font-size: ${MAX_FONT_PX}px;
29693
30620
  font-weight: 700;
29694
30621
  letter-spacing: 0.04em;
29695
30622
  user-select: none;
29696
30623
  white-space: nowrap;
29697
30624
  }
29698
30625
  `;
30626
+ function watermarkFontPx(availPx, textPxAtMax) {
30627
+ if (textPxAtMax <= 0) return MAX_FONT_PX;
30628
+ return Math.max(MIN_FONT_PX, Math.min(MAX_FONT_PX, Math.floor(MAX_FONT_PX * availPx * FILL / textPxAtMax)));
30629
+ }
29699
30630
  var Watermark = class {
29700
30631
  constructor(host, symbol, timeframe) {
29701
- injectStyles(STYLE_ID17, CSS14, host.ownerDocument);
30632
+ this.resizeObserver = null;
30633
+ injectStyles(STYLE_ID18, CSS15, host.ownerDocument);
29702
30634
  this.el = host.ownerDocument.createElement("div");
29703
30635
  this.el.className = "vela-watermark";
30636
+ this.text = host.ownerDocument.createElement("span");
30637
+ this.el.appendChild(this.text);
29704
30638
  host.appendChild(this.el);
30639
+ if (typeof ResizeObserver !== "undefined") {
30640
+ this.resizeObserver = new ResizeObserver(() => this.fit());
30641
+ this.resizeObserver.observe(this.el);
30642
+ }
29705
30643
  this.update(symbol, timeframe);
29706
30644
  }
29707
30645
  setVisible(visible) {
29708
30646
  this.el.style.display = visible ? "" : "none";
29709
30647
  }
29710
30648
  update(symbol, timeframe) {
29711
- this.el.textContent = symbol ? `${parseSymbol(symbol).ticker} \xB7 ${timeframeLabel(timeframe)}` : "";
30649
+ this.text.textContent = symbol ? `${parseSymbol(symbol).ticker} \xB7 ${timeframeLabel(timeframe)}` : "";
30650
+ this.fit();
30651
+ }
30652
+ /** Measure the text at the cap and shrink it to the chart's own width. */
30653
+ fit() {
30654
+ if (this.el.clientWidth <= 0 || !this.text.textContent) return;
30655
+ this.el.style.fontSize = `${MAX_FONT_PX}px`;
30656
+ const px = watermarkFontPx(this.el.clientWidth, this.text.getBoundingClientRect().width);
30657
+ if (px !== MAX_FONT_PX) this.el.style.fontSize = `${px}px`;
29712
30658
  }
29713
30659
  destroy() {
30660
+ this.resizeObserver?.disconnect();
29714
30661
  this.el.remove();
29715
30662
  }
29716
30663
  };
@@ -30579,6 +31526,49 @@ function registerBuiltinLayouts() {
30579
31526
  registerLayout({ id: "4", label: "4 grid", cols: [1, 1], rows: [1, 1], cells: slots(4) });
30580
31527
  registerLayout({ id: "8", label: "8 grid", cols: [1, 1, 1, 1], rows: [1, 1], cells: slots(8) });
30581
31528
  }
31529
+ var GRID_PICKER_MAX = 4;
31530
+ var GRID_BUILTIN_IDS = {
31531
+ "1x1": "1",
31532
+ "1x2": "2h",
31533
+ "2x1": "2v",
31534
+ "2x2": "4",
31535
+ "2x4": "8"
31536
+ };
31537
+ var clampTrack = (n) => Math.max(1, Math.min(GRID_PICKER_MAX, Math.round(n)));
31538
+ function layoutForGrid(rows, cols) {
31539
+ const r = clampTrack(rows);
31540
+ const c = clampTrack(cols);
31541
+ const builtin = GRID_BUILTIN_IDS[`${r}x${c}`];
31542
+ const registered = builtin ? registry5.get(builtin) : void 0;
31543
+ if (registered) return registered;
31544
+ return {
31545
+ id: `g${r}x${c}`,
31546
+ // Width-first label — matches the picker's caption ("3 × 2" = 3 wide, 2 tall).
31547
+ label: `${c} \xD7 ${r} grid`,
31548
+ cols: Array.from({ length: c }, () => 1),
31549
+ rows: Array.from({ length: r }, () => 1),
31550
+ cells: slots(r * c)
31551
+ };
31552
+ }
31553
+ var GRID_ID_RE = /^g([1-4])x([1-4])$/;
31554
+ function ensureLayout(id) {
31555
+ const registered = registry5.get(id);
31556
+ if (registered) return registered;
31557
+ const g = GRID_ID_RE.exec(id);
31558
+ if (g) return layoutForGrid(Number(g[1]), Number(g[2]));
31559
+ return void 0;
31560
+ }
31561
+ function layoutShape(def) {
31562
+ if (!def.areas && def.rows.length <= GRID_PICKER_MAX && def.cols.length <= GRID_PICKER_MAX && def.cells.length === def.rows.length * def.cols.length) {
31563
+ return { rows: def.rows.length, cols: def.cols.length };
31564
+ }
31565
+ return null;
31566
+ }
31567
+ function occupancyGrid(def) {
31568
+ if (def.areas) return def.areas.map((row) => row.trim().split(/\s+/));
31569
+ const cols = def.cols.length;
31570
+ return def.rows.map((_, r) => def.cols.map((_2, c) => def.cells[r * cols + c]?.id ?? `\xB7${r}x${c}`));
31571
+ }
30582
31572
  function gridStyles(def, trackSizes) {
30583
31573
  const cols = trackSizes?.cols?.length === def.cols.length ? trackSizes.cols : def.cols;
30584
31574
  const rows = trackSizes?.rows?.length === def.rows.length ? trackSizes.rows : def.rows;
@@ -30598,6 +31588,15 @@ function activeAfterLayout(current, cellIds) {
30598
31588
  if (current != null && cellIds.includes(current)) return current;
30599
31589
  return cellIds[0] ?? null;
30600
31590
  }
31591
+ function orderAfterLayout(order, slots2, active) {
31592
+ const next = [...order];
31593
+ if (active == null || slots2 <= 0) return next;
31594
+ const idx = next.indexOf(active);
31595
+ if (idx < 0 || idx < slots2) return next;
31596
+ next.splice(idx, 1);
31597
+ next.splice(slots2 - 1, 0, active);
31598
+ return next;
31599
+ }
30601
31600
 
30602
31601
  // src/workspace/splitters.ts
30603
31602
  var MIN_TRACK_FRAC = 0.1;
@@ -30630,6 +31629,27 @@ function trackOffsets(weights, sizePx, gapPx) {
30630
31629
  }
30631
31630
  return out;
30632
31631
  }
31632
+ function seamSegments(grid, axis, index) {
31633
+ const count = axis === "cols" ? grid.length : grid[0]?.length ?? 0;
31634
+ const isSeam = (k) => axis === "cols" ? grid[k]?.[index] !== grid[k]?.[index + 1] : grid[index]?.[k] !== grid[index + 1]?.[k];
31635
+ const out = [];
31636
+ for (let k = 0; k < count; k += 1) {
31637
+ if (!isSeam(k)) continue;
31638
+ const last = out[out.length - 1];
31639
+ if (last && last[1] === k - 1) last[1] = k;
31640
+ else out.push([k, k]);
31641
+ }
31642
+ return out;
31643
+ }
31644
+ function segmentSpanPx(weights, sizePx, gapPx, from, to) {
31645
+ const total = weights.reduce((s, w) => s + w, 0);
31646
+ if (total <= 0) return { start: 0, end: 0 };
31647
+ const content = sizePx - gapPx * (weights.length - 1);
31648
+ const startOf = (k) => weights.slice(0, k).reduce((s, w) => s + w / total * content, 0) + gapPx * k;
31649
+ const start = from === 0 ? 0 : startOf(from) - gapPx / 2;
31650
+ const end = to === weights.length - 1 ? sizePx : startOf(to + 1) - gapPx / 2;
31651
+ return { start, end };
31652
+ }
30633
31653
  var HIT_PX = 8;
30634
31654
  var SplitterLayer = class {
30635
31655
  constructor(container, deps) {
@@ -30638,23 +31658,36 @@ var SplitterLayer = class {
30638
31658
  this.strips = [];
30639
31659
  this.drag = null;
30640
31660
  }
30641
- /** Rebuild + reposition the strips for the current tracks/size. */
31661
+ /** Rebuild + reposition the strips for the current tracks/size/occupancy. */
30642
31662
  layout() {
30643
31663
  const { cols, rows } = this.deps.tracks();
31664
+ const grid = this.deps.grid();
30644
31665
  const rect = this.container.getBoundingClientRect();
30645
31666
  const gap = this.deps.gapPx();
30646
- const wanted = Math.max(0, cols.length - 1) + Math.max(0, rows.length - 1);
30647
- while (this.strips.length > wanted) this.strips.pop().remove();
30648
- while (this.strips.length < wanted) this.strips.push(this.makeStrip());
30649
- let k = 0;
31667
+ const placements = [];
30650
31668
  for (const [i, x] of trackOffsets(cols, rect.width, gap).entries()) {
30651
- this.place(this.strips[k], "cols", i, `left:${Math.round(x - HIT_PX / 2)}px;top:0;width:${HIT_PX}px;height:100%;cursor:col-resize;`);
30652
- k += 1;
31669
+ for (const [from, to] of seamSegments(grid, "cols", i)) {
31670
+ const { start, end } = segmentSpanPx(rows, rect.height, gap, from, to);
31671
+ placements.push({
31672
+ axis: "cols",
31673
+ index: i,
31674
+ css: `left:${Math.round(x - HIT_PX / 2)}px;top:${Math.round(start)}px;width:${HIT_PX}px;height:${Math.round(end - start)}px;cursor:col-resize;`
31675
+ });
31676
+ }
30653
31677
  }
30654
31678
  for (const [i, y] of trackOffsets(rows, rect.height, gap).entries()) {
30655
- this.place(this.strips[k], "rows", i, `left:0;top:${Math.round(y - HIT_PX / 2)}px;width:100%;height:${HIT_PX}px;cursor:row-resize;`);
30656
- k += 1;
31679
+ for (const [from, to] of seamSegments(grid, "rows", i)) {
31680
+ const { start, end } = segmentSpanPx(cols, rect.width, gap, from, to);
31681
+ placements.push({
31682
+ axis: "rows",
31683
+ index: i,
31684
+ css: `left:${Math.round(start)}px;top:${Math.round(y - HIT_PX / 2)}px;width:${Math.round(end - start)}px;height:${HIT_PX}px;cursor:row-resize;`
31685
+ });
31686
+ }
30657
31687
  }
31688
+ while (this.strips.length > placements.length) this.strips.pop().remove();
31689
+ while (this.strips.length < placements.length) this.strips.push(this.makeStrip());
31690
+ for (const [k, p] of placements.entries()) this.place(this.strips[k], p.axis, p.index, p.css);
30658
31691
  }
30659
31692
  destroy() {
30660
31693
  for (const s of this.strips.splice(0)) s.remove();
@@ -30720,16 +31753,18 @@ var GAP_PX = 2;
30720
31753
  var POOL_CAP = 16;
30721
31754
  var TIME_AXIS_H3 = 22;
30722
31755
  var ALERT_CAP = 50;
30723
- var STYLE_ID18 = "vela-workspace";
30724
- var CSS15 = `
31756
+ var STYLE_ID19 = "vela-workspace";
31757
+ var CSS16 = `
30725
31758
  .vela-workspace { position: relative; width: 100%; height: 100%; display: flex; flex-direction: column; background: var(--vela-bg); }
30726
31759
  .vela-ws-main { position: relative; display: flex; flex-direction: row; flex: 1 1 auto; min-height: 0; }
30727
31760
  .vela-ws-toolbar { position: relative; flex: none; }
30728
31761
  .vela-ws-grid { position: relative; flex: 1 1 auto; min-width: 0; display: grid; gap: ${GAP_PX}px; background: var(--vela-border-soft); }
30729
31762
  .vela-cell { background: var(--vela-bg); position: relative; }
30730
31763
  /* Active-cell highlight: an overlay ring ABOVE the chart's own canvas stack (a plain
30731
- outline on the cell is painted under them) \u2014 inert to the pointer. */
30732
- .vela-cell[data-active='1']::after {
31764
+ outline on the cell is painted under them) \u2014 inert to the pointer. Scoped to
31765
+ multi-cell grids ([data-multi]): a single-cell layout always has an active cell,
31766
+ and ringing the only chart would just be noise. */
31767
+ .vela-ws-grid[data-multi='1'] .vela-cell[data-active='1']::after {
30733
31768
  content: '';
30734
31769
  position: absolute;
30735
31770
  inset: 0;
@@ -30737,7 +31772,12 @@ var CSS15 = `
30737
31772
  pointer-events: none;
30738
31773
  z-index: 10;
30739
31774
  }
30740
- .vela-ws-splitter:hover { background: var(--vela-accent); opacity: 0.35; }
31775
+ /* Splitter hover mirrors the in-chart pane separator hover (CrosshairRenderer):
31776
+ a soft band over the whole grab target + a solid 2px line on the seam center. */
31777
+ .vela-ws-splitter:hover { background: var(--vela-separator-hover-band); }
31778
+ .vela-ws-splitter:hover::after { content: ''; position: absolute; background: var(--vela-separator-hover-line); }
31779
+ .vela-ws-splitter[data-axis='cols']:hover::after { left: calc(50% - 1px); top: 0; width: 2px; height: 100%; }
31780
+ .vela-ws-splitter[data-axis='rows']:hover::after { top: calc(50% - 1px); left: 0; height: 2px; width: 100%; }
30741
31781
  `;
30742
31782
  registerIcon("layout", svg16('<rect x="1.5" y="1.5" width="13" height="13" rx="1.5"/><path d="M8 1.5v13M1.5 8h13"/>'));
30743
31783
  function declaredOrder(cells) {
@@ -30788,6 +31828,15 @@ var VelaWorkspace = class {
30788
31828
  /** Re-entrance guard around one propagation tick: followers' synchronous echoes
30789
31829
  * (their setVisibleRange re-emits viewport:changed) must not re-propagate. */
30790
31830
  this.syncBusy = false;
31831
+ /** Same guard for the drawings link: the propagated mutations' own `drawing:*`
31832
+ * events fire synchronously inside the propagation loop and must not fan out again. */
31833
+ this.drawingSyncBusy = false;
31834
+ /** LINKED drawings (the drawings sync): one map per synced set (cellId → that
31835
+ * cell's drawing id), reachable from every member under its `cellId\0drawingId`
31836
+ * key — any member finds its peers to push edits/removals onto. Survives a
31837
+ * toggle-off (propagation freezes while the setting is off; re-enabling resumes
31838
+ * edit/delete for these pairs). Cleared on reload / `applyState`. */
31839
+ this.drawingLinks = /* @__PURE__ */ new Map();
30791
31840
  this.manifest = [];
30792
31841
  /** The shared manifest can no longer change instance sets — resolved, or no
30793
31842
  * `indicators` option so nothing ever will. Gates the cells' ledger fallback. */
@@ -30829,16 +31878,16 @@ var VelaWorkspace = class {
30829
31878
  this.timezone = boot?.timezone ?? opts.timezone ?? "Etc/UTC";
30830
31879
  if (boot?.favorites) this.favs = [...boot.favorites];
30831
31880
  const sync = boot?.sync ?? opts.sync;
30832
- for (const kind of ["viewport", "symbol", "timeframe", "crosshair"]) {
31881
+ for (const kind of ["viewport", "symbol", "timeframe", "crosshair", "drawings"]) {
30833
31882
  this.applySyncSetting(kind, sync?.[kind]);
30834
31883
  }
30835
- this.def = this.resolveLayout(boot?.layout && layoutDefinition(boot.layout) ? boot.layout : opts.layout ?? "4");
31884
+ this.def = this.resolveLayout(boot?.layout && ensureLayout(boot.layout) ? boot.layout : opts.layout ?? "4");
30836
31885
  if (boot?.trackSizes) for (const [id, ts] of Object.entries(boot.trackSizes)) this.trackSizes.set(id, ts);
30837
31886
  if (boot?.charts) for (const { id, ...cs } of boot.charts) this.pool.set(id, cs);
30838
31887
  this.order = boot?.charts ? boot.charts.map((c) => c.id) : declaredOrder(opts.cells);
30839
31888
  const bootActive = boot?.activeCellId ?? null;
30840
31889
  const doc = hostEl.ownerDocument;
30841
- injectStyles(STYLE_ID18, CSS15, doc);
31890
+ injectStyles(STYLE_ID19, CSS16, doc);
30842
31891
  this.root = doc.createElement("div");
30843
31892
  this.root.className = "vela-workspace";
30844
31893
  ensureUIHost(this.root, resolveTheme(opts.theme));
@@ -30883,14 +31932,23 @@ var VelaWorkspace = class {
30883
31932
  onPriceStyle: (style) => this.active.setPriceStyle(style),
30884
31933
  layout: {
30885
31934
  current: this.def.id,
30886
- options: () => layouts().map((l) => ({ id: l.id, label: l.label })),
30887
- onSelect: (id) => this.setLayout(id),
30888
- // Workspace-wide view toggles live under the grid presets. The check
30889
- // reflects the simple all-cells form; flipping OVERRIDES a host-set
30890
- // group record with plain on/off (groups stay an API-only shape).
30891
- toggles: () => [{ id: "crosshair-sync", label: "Sync crosshair", checked: this.syncOpts.crosshair === true }],
30892
- onToggle: (id) => {
30893
- if (id === "crosshair-sync") this.sync.set("crosshair", this.syncOpts.crosshair ? false : true);
31935
+ // The picker composes dynamic layouts on its grid canvas; registered
31936
+ // presets the canvas cannot express (bespoke plugin areas) list as rows.
31937
+ shape: () => layoutShape(this.def),
31938
+ presets: () => layouts().filter((l) => layoutShape(l) === null).map((l) => ({ id: l.id, label: l.label })),
31939
+ onSelectGrid: (rows, cols) => this.setLayout(layoutForGrid(rows, cols)),
31940
+ onSelectPreset: (id) => this.setLayout(id),
31941
+ // The SYNC switches reflect the simple all-cells form; flipping one
31942
+ // OVERRIDES a host-set group record with plain on/off (groups stay an
31943
+ // API-only shape).
31944
+ syncs: () => [
31945
+ { id: "symbol", label: "Symbol", checked: this.syncOpts.symbol === true },
31946
+ { id: "timeframe", label: "Interval", checked: this.syncOpts.timeframe === true },
31947
+ { id: "crosshair", label: "Crosshair", checked: this.syncOpts.crosshair === true }
31948
+ ],
31949
+ onToggleSync: (id) => {
31950
+ const kind = id;
31951
+ this.sync.set(kind, this.syncOpts[kind] ? false : true);
30894
31952
  }
30895
31953
  },
30896
31954
  getContext: () => this.context()
@@ -30953,10 +32011,19 @@ var VelaWorkspace = class {
30953
32011
  this.active.chart.drawings.setStayMode(on);
30954
32012
  this.refocusActive();
30955
32013
  },
30956
- { dock: "static" }
32014
+ {
32015
+ dock: "static",
32016
+ // Drawings sync is a WORKSPACE link (same model as the layout
32017
+ // dropdown's switches) — the bar only hosts its toggle.
32018
+ onDrawingsSync: (on) => {
32019
+ this.sync.set("drawings", on);
32020
+ this.refocusActive();
32021
+ }
32022
+ }
30957
32023
  ) : null;
30958
32024
  this.drawToolbar?.setDefinition(defaultToolbar());
30959
32025
  this.drawToolbar?.setVisible(true);
32026
+ this.drawToolbar?.setDrawingsSyncMode(!!this.syncOpts.drawings);
30960
32027
  this.bottombar = opts.bottombar !== false ? new Bottombar(this.root, {
30961
32028
  timezone: this.timezone,
30962
32029
  onRange: (preset) => {
@@ -30969,6 +32036,7 @@ var VelaWorkspace = class {
30969
32036
  hostEl.appendChild(this.root);
30970
32037
  this.splitters = new SplitterLayer(this.gridEl, {
30971
32038
  tracks: () => this.currentTracks(),
32039
+ grid: () => occupancyGrid(this.def),
30972
32040
  apply: (axis, weights) => this.applyTracks(axis, weights),
30973
32041
  reset: (axis) => this.applyTracks(axis, evenTracks(this.currentTracks()[axis].length)),
30974
32042
  gapPx: () => GAP_PX
@@ -31116,7 +32184,7 @@ var VelaWorkspace = class {
31116
32184
  }
31117
32185
  if (st.favorites) this.favs = [...st.favorites];
31118
32186
  this.dock.applyState(st.panels);
31119
- for (const kind of ["viewport", "symbol", "timeframe", "crosshair"]) this.applySyncSetting(kind, st.sync?.[kind]);
32187
+ for (const kind of ["viewport", "symbol", "timeframe", "crosshair", "drawings"]) this.applySyncSetting(kind, st.sync?.[kind]);
31120
32188
  this.trackSizes.clear();
31121
32189
  if (st.trackSizes) for (const [id, ts] of Object.entries(st.trackSizes)) this.trackSizes.set(id, ts);
31122
32190
  for (const [id, cell] of [...this.cellsById]) {
@@ -31125,9 +32193,10 @@ var VelaWorkspace = class {
31125
32193
  this.events.emit("cell:destroyed", { id });
31126
32194
  }
31127
32195
  this.pool.clear();
32196
+ this.drawingLinks.clear();
31128
32197
  for (const { id, ...cs } of st.charts) this.pool.set(id, cs);
31129
32198
  this.order = st.charts.map((c) => c.id);
31130
- const def = layoutDefinition(st.layout);
32199
+ const def = ensureLayout(st.layout);
31131
32200
  if (def) this.def = def;
31132
32201
  this.cellBackend = this.backendFor(this.def);
31133
32202
  this.applyGrid();
@@ -31178,6 +32247,7 @@ var VelaWorkspace = class {
31178
32247
  const next = this.resolveLayout(layout);
31179
32248
  const nextBackend = this.backendFor(next);
31180
32249
  const rebuildAll = nextBackend !== this.cellBackend;
32250
+ this.order = orderAfterLayout(this.order, next.cells.length, this.activeId);
31181
32251
  const keep = new Set(this.order.slice(0, next.cells.length));
31182
32252
  for (const [id, cell] of [...this.cellsById]) {
31183
32253
  if (!keep.has(id) || rebuildAll) {
@@ -31301,7 +32371,7 @@ var VelaWorkspace = class {
31301
32371
  // ── internals ───────────────────────────────────────────────
31302
32372
  resolveLayout(layout) {
31303
32373
  if (typeof layout !== "string") return layout;
31304
- const def = layoutDefinition(layout);
32374
+ const def = ensureLayout(layout);
31305
32375
  if (!def) throw new Error(`[vela] unknown workspace layout "${layout}" \u2014 register it with registerLayout().`);
31306
32376
  return def;
31307
32377
  }
@@ -31329,6 +32399,8 @@ var VelaWorkspace = class {
31329
32399
  * `order[i]` — the identity/position decoupling in one line. */
31330
32400
  applyGrid() {
31331
32401
  const { container, perCell } = gridStyles(this.def, this.trackSizes.get(this.def.id));
32402
+ if (this.def.cells.length > 1) this.gridEl.dataset.multi = "1";
32403
+ else delete this.gridEl.dataset.multi;
31332
32404
  this.gridEl.style.gridTemplateColumns = container.gridTemplateColumns ?? "";
31333
32405
  this.gridEl.style.gridTemplateRows = container.gridTemplateRows ?? "";
31334
32406
  this.gridEl.style.gridTemplateAreas = container.gridTemplateAreas ?? "";
@@ -31376,6 +32448,7 @@ var VelaWorkspace = class {
31376
32448
  });
31377
32449
  cell.host.style.gridArea = perCell[slot.id]?.gridArea ?? "";
31378
32450
  this.cellsById.set(id, cell);
32451
+ if (id === this.activeId) cell.host.dataset.active = "1";
31379
32452
  this.wireCell(cell);
31380
32453
  if (this.favs.length > 0) cell.chart.drawings.setFavorites(this.favs);
31381
32454
  cell.setManifest(this.manifest, pooled?.indicators == null);
@@ -31407,9 +32480,19 @@ var VelaWorkspace = class {
31407
32480
  this.markStateDirty();
31408
32481
  });
31409
32482
  chart.renderer.onCrosshairMove((e) => this.propagateCrosshair(cell.id, e.time));
31410
- chart.on("drawing:created", () => this.markStateDirty());
31411
- chart.on("drawing:edited", () => this.markStateDirty());
31412
- chart.on("drawing:removed", () => this.markStateDirty());
32483
+ chart.on("drawing:created", ({ id }) => {
32484
+ this.propagateDrawing(cell.id, id);
32485
+ this.markStateDirty();
32486
+ });
32487
+ chart.on("drawing:draft", ({ doc }) => this.propagateDraft(cell.id, doc));
32488
+ chart.on("drawing:edited", ({ id }) => {
32489
+ this.propagateDrawingEdit(cell.id, id);
32490
+ this.markStateDirty();
32491
+ });
32492
+ chart.on("drawing:removed", ({ id }) => {
32493
+ this.propagateDrawingRemoval(cell.id, id);
32494
+ this.markStateDirty();
32495
+ });
31413
32496
  chart.on("drawing:tool", ({ type }) => {
31414
32497
  if (cell.id !== this.activeId) return;
31415
32498
  this.globalTool = type;
@@ -31437,6 +32520,12 @@ var VelaWorkspace = class {
31437
32520
  applySyncSetting(kind, setting, align = false) {
31438
32521
  if (setting == null || setting === false) delete this.syncOpts[kind];
31439
32522
  else this.syncOpts[kind] = setting;
32523
+ if (kind === "drawings") {
32524
+ for (const cell of this.cellsById.values()) cell.chart.drawings.setExternalGhost(null);
32525
+ this.drawToolbar?.setDrawingsSyncMode(!!setting);
32526
+ this.markStateDirty();
32527
+ return;
32528
+ }
31440
32529
  if (kind === "crosshair") {
31441
32530
  for (const cell of this.cellsById.values()) cell.chart.renderer.setExternalCrosshair(null);
31442
32531
  if (setting && ![...this.cellsById.values()].some((c) => c.chart.renderer.supportsExternalCrosshair)) {
@@ -31446,6 +32535,7 @@ var VelaWorkspace = class {
31446
32535
  this.markStateDirty();
31447
32536
  return;
31448
32537
  }
32538
+ if (this.topbar && this.def) this.topbar.setLayout(this.def.id);
31449
32539
  this.markStateDirty();
31450
32540
  if (align && setting && this.activeId) {
31451
32541
  if (kind === "viewport") {
@@ -31497,6 +32587,101 @@ var VelaWorkspace = class {
31497
32587
  this.syncBusy = false;
31498
32588
  }
31499
32589
  }
32590
+ /**
32591
+ * Copy a freshly created drawing onto the origin's same-group followers (fresh ids
32592
+ * through `drawings.add` — anchors are time+price, so the copy lands at the same
32593
+ * spot whatever the follower shows) and register the whole set as LINKED: edits
32594
+ * and removals of any member follow while the link is on ({@link drawingLinks}).
32595
+ * The busy guard stops the copies' own `drawing:created` events from fanning out.
32596
+ */
32597
+ propagateDrawing(originId, drawingId) {
32598
+ if (this.drawingSyncBusy || this.destroyed) return;
32599
+ const setting = this.syncOpts.drawings;
32600
+ if (!setting) return;
32601
+ const doc = this.cellsById.get(originId)?.chart.drawings.all().find((d) => d.id === drawingId);
32602
+ if (!doc) return;
32603
+ this.drawingSyncBusy = true;
32604
+ try {
32605
+ const group = /* @__PURE__ */ new Map([[originId, drawingId]]);
32606
+ for (const id of syncTargets(originId, setting, [...this.cellsById.keys()])) {
32607
+ const copy = this.cellsById.get(id)?.chart.drawings.add(doc.type, {
32608
+ paneId: doc.paneId,
32609
+ anchors: doc.anchors,
32610
+ style: doc.style,
32611
+ text: doc.text,
32612
+ props: doc.props
32613
+ });
32614
+ if (copy) group.set(id, copy.id);
32615
+ }
32616
+ if (group.size > 1) {
32617
+ for (const [cellId, dId] of group) this.drawingLinks.set(`${cellId}\0${dId}`, group);
32618
+ }
32619
+ } finally {
32620
+ this.drawingSyncBusy = false;
32621
+ }
32622
+ }
32623
+ /** Mirror an in-progress placement (its current ghost, `null` = placement ended)
32624
+ * onto the origin's same-group followers — the live half of the drawings link;
32625
+ * the created copy replaces the ghosts when the placement completes. One-way by
32626
+ * contract (an external ghost never re-emits drafts), so no busy guard needed. */
32627
+ propagateDraft(originId, doc) {
32628
+ if (this.destroyed) return;
32629
+ const setting = this.syncOpts.drawings;
32630
+ if (!setting) return;
32631
+ for (const id of syncTargets(originId, setting, [...this.cellsById.keys()])) {
32632
+ this.cellsById.get(id)?.chart.drawings.setExternalGhost(doc);
32633
+ }
32634
+ }
32635
+ /** Push a linked drawing's edited CONTENT (anchors, style, text, per-type props)
32636
+ * onto its same-group peers — any member propagates, not just the original. */
32637
+ propagateDrawingEdit(originId, drawingId) {
32638
+ if (this.drawingSyncBusy || this.destroyed) return;
32639
+ const setting = this.syncOpts.drawings;
32640
+ if (!setting) return;
32641
+ const group = this.drawingLinks.get(`${originId}\0${drawingId}`);
32642
+ if (!group) return;
32643
+ const doc = this.cellsById.get(originId)?.chart.drawings.all().find((d) => d.id === drawingId);
32644
+ if (!doc) return;
32645
+ this.drawingSyncBusy = true;
32646
+ try {
32647
+ for (const id of syncTargets(originId, setting, [...this.cellsById.keys()])) {
32648
+ const peerId = group.get(id);
32649
+ if (peerId == null) continue;
32650
+ this.cellsById.get(id)?.chart.drawings.update(peerId, {
32651
+ anchors: doc.anchors,
32652
+ style: doc.style,
32653
+ text: doc.text,
32654
+ props: doc.props
32655
+ });
32656
+ }
32657
+ } finally {
32658
+ this.drawingSyncBusy = false;
32659
+ }
32660
+ }
32661
+ /** Remove a linked drawing's peers with it. The removed member always leaves its
32662
+ * link group (whatever the setting); peers are only deleted while the link is on.
32663
+ * A propagated peer removal re-enters here under the busy guard and just cleans
32664
+ * its own link key. */
32665
+ propagateDrawingRemoval(originId, drawingId) {
32666
+ if (this.destroyed) return;
32667
+ const key = `${originId}\0${drawingId}`;
32668
+ const group = this.drawingLinks.get(key);
32669
+ if (!group) return;
32670
+ this.drawingLinks.delete(key);
32671
+ group.delete(originId);
32672
+ if (this.drawingSyncBusy) return;
32673
+ const setting = this.syncOpts.drawings;
32674
+ if (!setting) return;
32675
+ this.drawingSyncBusy = true;
32676
+ try {
32677
+ for (const id of syncTargets(originId, setting, [...this.cellsById.keys()])) {
32678
+ const peerId = group.get(id);
32679
+ if (peerId != null) this.cellsById.get(id)?.chart.drawings.remove(peerId);
32680
+ }
32681
+ } finally {
32682
+ this.drawingSyncBusy = false;
32683
+ }
32684
+ }
31500
32685
  /**
31501
32686
  * Push an origin cell's symbol/timeframe onto its same-group followers (fired from
31502
32687
  * `market:changed`). Convergence comes from IDEMPOTENCE, not the guard: a follower's
@@ -31673,13 +32858,17 @@ var VelaWorkspace = class {
31673
32858
  };
31674
32859
 
31675
32860
  exports.ChartCell = ChartCell;
32861
+ exports.GRID_PICKER_MAX = GRID_PICKER_MAX;
31676
32862
  exports.VelaWorkspace = VelaWorkspace;
31677
32863
  exports.activeAfterLayout = activeAfterLayout;
31678
32864
  exports.decodeState = decodeState;
31679
32865
  exports.encodeState = encodeState;
32866
+ exports.ensureLayout = ensureLayout;
31680
32867
  exports.evenTracks = evenTracks;
31681
32868
  exports.gridStyles = gridStyles;
31682
32869
  exports.layoutDefinition = layoutDefinition;
32870
+ exports.layoutForGrid = layoutForGrid;
32871
+ exports.layoutShape = layoutShape;
31683
32872
  exports.layouts = layouts;
31684
32873
  exports.memoryStorageAdapter = memoryStorageAdapter;
31685
32874
  exports.rangesWithin = rangesWithin;