@dawcore/components 0.0.19 → 0.0.21

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.
package/dist/index.js CHANGED
@@ -230,7 +230,7 @@ var DawTrackElement = class extends import_lit2.LitElement {
230
230
  this.pan = 0;
231
231
  this.muted = false;
232
232
  this.soloed = false;
233
- this.renderMode = "waveform";
233
+ this._renderMode = "waveform";
234
234
  this.spectrogramConfig = null;
235
235
  this.trackId = crypto.randomUUID();
236
236
  // Track removal is detected by the editor's MutationObserver,
@@ -238,6 +238,21 @@ var DawTrackElement = class extends import_lit2.LitElement {
238
238
  // cannot bubble events to ancestors).
239
239
  this._hasRendered = false;
240
240
  }
241
+ get renderMode() {
242
+ return this._renderMode;
243
+ }
244
+ set renderMode(value) {
245
+ const old = this._renderMode;
246
+ let next = value;
247
+ if (next === "both") {
248
+ console.warn(
249
+ `[dawcore] <daw-track render-mode="both"> is not yet supported; falling back to 'spectrogram'`
250
+ );
251
+ next = "spectrogram";
252
+ }
253
+ this._renderMode = next;
254
+ this.requestUpdate("renderMode", old);
255
+ }
241
256
  // Light DOM so <daw-clip> children are queryable.
242
257
  createRenderRoot() {
243
258
  return this;
@@ -300,8 +315,8 @@ __decorateClass([
300
315
  (0, import_decorators2.property)({ type: Boolean })
301
316
  ], DawTrackElement.prototype, "soloed", 2);
302
317
  __decorateClass([
303
- (0, import_decorators2.property)({ attribute: "render-mode" })
304
- ], DawTrackElement.prototype, "renderMode", 2);
318
+ (0, import_decorators2.property)({ attribute: "render-mode", noAccessor: true })
319
+ ], DawTrackElement.prototype, "renderMode", 1);
305
320
  __decorateClass([
306
321
  (0, import_decorators2.property)({ attribute: false })
307
322
  ], DawTrackElement.prototype, "spectrogramConfig", 2);
@@ -1237,8 +1252,8 @@ DawStopButtonElement = __decorateClass([
1237
1252
  ], DawStopButtonElement);
1238
1253
 
1239
1254
  // src/elements/daw-editor.ts
1240
- var import_lit14 = require("lit");
1241
- var import_decorators12 = require("lit/decorators.js");
1255
+ var import_lit15 = require("lit");
1256
+ var import_decorators13 = require("lit/decorators.js");
1242
1257
 
1243
1258
  // src/types.ts
1244
1259
  function isDomClip(desc) {
@@ -1246,7 +1261,7 @@ function isDomClip(desc) {
1246
1261
  }
1247
1262
 
1248
1263
  // src/elements/daw-editor.ts
1249
- var import_core8 = require("@waveform-playlist/core");
1264
+ var import_core9 = require("@waveform-playlist/core");
1250
1265
 
1251
1266
  // src/workers/peaksWorker.ts
1252
1267
  var import_waveform_data = __toESM(require("waveform-data"));
@@ -1717,129 +1732,399 @@ var PeakPipeline = class {
1717
1732
  }
1718
1733
  };
1719
1734
 
1720
- // src/elements/daw-track-controls.ts
1735
+ // src/elements/daw-ruler.ts
1721
1736
  var import_lit11 = require("lit");
1722
1737
  var import_decorators10 = require("lit/decorators.js");
1723
- var DawTrackControlsElement = class extends import_lit11.LitElement {
1738
+
1739
+ // src/utils/time-format.ts
1740
+ function formatTime(milliseconds) {
1741
+ const seconds = Math.floor(milliseconds / 1e3);
1742
+ const s = seconds % 60;
1743
+ const m = (seconds - s) / 60;
1744
+ return `${m}:${String(s).padStart(2, "0")}`;
1745
+ }
1746
+
1747
+ // src/utils/smart-scale.ts
1748
+ var timeinfo = /* @__PURE__ */ new Map([
1749
+ [700, { marker: 1e3, bigStep: 500, smallStep: 100 }],
1750
+ [1500, { marker: 2e3, bigStep: 1e3, smallStep: 200 }],
1751
+ [2500, { marker: 2e3, bigStep: 1e3, smallStep: 500 }],
1752
+ [5e3, { marker: 5e3, bigStep: 1e3, smallStep: 500 }],
1753
+ [1e4, { marker: 1e4, bigStep: 5e3, smallStep: 1e3 }],
1754
+ [12e3, { marker: 15e3, bigStep: 5e3, smallStep: 1e3 }],
1755
+ [Infinity, { marker: 3e4, bigStep: 1e4, smallStep: 5e3 }]
1756
+ ]);
1757
+ function getScaleInfo(samplesPerPixel) {
1758
+ for (const [resolution, config] of timeinfo) {
1759
+ if (samplesPerPixel < resolution) {
1760
+ return config;
1761
+ }
1762
+ }
1763
+ return { marker: 3e4, bigStep: 1e4, smallStep: 5e3 };
1764
+ }
1765
+ function computeTemporalTicks(samplesPerPixel, sampleRate, duration, rulerHeight) {
1766
+ const widthX = Math.ceil(duration * sampleRate / samplesPerPixel);
1767
+ const config = getScaleInfo(samplesPerPixel);
1768
+ const { marker, bigStep, smallStep } = config;
1769
+ const canvasInfo = /* @__PURE__ */ new Map();
1770
+ const labels = [];
1771
+ const pixPerSec = sampleRate / samplesPerPixel;
1772
+ for (let counter = 0; ; counter += smallStep) {
1773
+ const pix = Math.floor(counter / 1e3 * pixPerSec);
1774
+ if (pix >= widthX) break;
1775
+ if (counter % marker === 0) {
1776
+ canvasInfo.set(pix, rulerHeight);
1777
+ labels.push({ pix, text: formatTime(counter) });
1778
+ } else if (counter % bigStep === 0) {
1779
+ canvasInfo.set(pix, Math.floor(rulerHeight / 2));
1780
+ } else if (counter % smallStep === 0) {
1781
+ canvasInfo.set(pix, Math.floor(rulerHeight / 5));
1782
+ }
1783
+ }
1784
+ return { widthX, canvasInfo, labels };
1785
+ }
1786
+
1787
+ // src/utils/musical-tick-cache.ts
1788
+ var import_core = require("@waveform-playlist/core");
1789
+ var cachedParams = null;
1790
+ var cachedResult = null;
1791
+ function meterEntriesMatch(a, b) {
1792
+ if (a.length !== b.length) return false;
1793
+ for (let i = 0; i < a.length; i++) {
1794
+ if (a[i].tick !== b[i].tick || a[i].numerator !== b[i].numerator || a[i].denominator !== b[i].denominator)
1795
+ return false;
1796
+ }
1797
+ return true;
1798
+ }
1799
+ function paramsMatch(a, b) {
1800
+ return a.ticksPerPixel === b.ticksPerPixel && a.startPixel === b.startPixel && a.endPixel === b.endPixel && meterEntriesMatch(a.meterEntries, b.meterEntries) && (a.ppqn ?? 960) === (b.ppqn ?? 960);
1801
+ }
1802
+ function getCachedMusicalTicks(params) {
1803
+ if (cachedParams && cachedResult && paramsMatch(cachedParams, params)) {
1804
+ return cachedResult;
1805
+ }
1806
+ cachedResult = (0, import_core.computeMusicalTicks)(params);
1807
+ cachedParams = {
1808
+ ...params,
1809
+ meterEntries: params.meterEntries.map((e) => ({ ...e }))
1810
+ };
1811
+ return cachedResult;
1812
+ }
1813
+
1814
+ // src/elements/daw-ruler.ts
1815
+ var MAX_CANVAS_WIDTH3 = 1e3;
1816
+ var DawRulerElement = class extends import_lit11.LitElement {
1724
1817
  constructor() {
1725
1818
  super(...arguments);
1726
- this.trackId = null;
1727
- this.trackName = "";
1728
- this.volume = 1;
1729
- this.pan = 0;
1730
- this.muted = false;
1731
- this.soloed = false;
1732
- this._onVolumeInput = (e) => {
1733
- const value = Number(e.target.value);
1734
- if (Number.isFinite(value)) this._dispatchControl("volume", value);
1735
- };
1736
- this._onPanInput = (e) => {
1737
- const value = Number(e.target.value);
1738
- if (Number.isFinite(value)) this._dispatchControl("pan", value);
1739
- };
1740
- this._onMuteClick = () => {
1741
- this._dispatchControl("muted", !this.muted);
1742
- };
1743
- this._onSoloClick = () => {
1744
- this._dispatchControl("soloed", !this.soloed);
1745
- };
1746
- this._onRemoveClick = () => {
1747
- if (!this.trackId) return;
1748
- this.dispatchEvent(
1749
- new CustomEvent("daw-track-remove", {
1750
- bubbles: true,
1751
- composed: true,
1752
- detail: { trackId: this.trackId }
1753
- })
1754
- );
1755
- };
1819
+ this.samplesPerPixel = 1024;
1820
+ this.sampleRate = 48e3;
1821
+ this.duration = 0;
1822
+ this.rulerHeight = 30;
1823
+ this.scaleMode = "temporal";
1824
+ this.ticksPerPixel = 4;
1825
+ this.meterEntries = [
1826
+ { tick: 0, numerator: 4, denominator: 4 }
1827
+ ];
1828
+ this.ppqn = 960;
1829
+ this.totalWidth = 0;
1830
+ this._tickData = null;
1831
+ this._musicalTickData = null;
1756
1832
  }
1757
- _dispatchControl(prop, value) {
1758
- if (!this.trackId) return;
1759
- this.dispatchEvent(
1760
- new CustomEvent("daw-track-control", {
1761
- bubbles: true,
1762
- composed: true,
1763
- detail: { trackId: this.trackId, prop, value }
1764
- })
1765
- );
1833
+ willUpdate() {
1834
+ if (this.scaleMode === "beats" && this.totalWidth > 0) {
1835
+ this._musicalTickData = getCachedMusicalTicks({
1836
+ meterEntries: this.meterEntries,
1837
+ ticksPerPixel: this.ticksPerPixel,
1838
+ startPixel: 0,
1839
+ endPixel: this.totalWidth,
1840
+ ppqn: this.ppqn
1841
+ });
1842
+ this._tickData = null;
1843
+ } else if (this.duration > 0 || this.totalWidth > 0) {
1844
+ const widthDerivedDuration = this.totalWidth * this.samplesPerPixel / this.sampleRate;
1845
+ const effectiveDuration = Math.max(this.duration, widthDerivedDuration);
1846
+ this._musicalTickData = null;
1847
+ this._tickData = computeTemporalTicks(
1848
+ this.samplesPerPixel,
1849
+ this.sampleRate,
1850
+ effectiveDuration,
1851
+ this.rulerHeight
1852
+ );
1853
+ } else {
1854
+ this._musicalTickData = null;
1855
+ this._tickData = null;
1856
+ }
1766
1857
  }
1767
1858
  render() {
1768
- const volPercent = Math.round(this.volume * 100);
1769
- const panPercent = Math.round(Math.abs(this.pan) * 100);
1770
- const panDisplay = this.pan === 0 ? "C" : (this.pan > 0 ? "R" : "L") + panPercent;
1859
+ const widthX = this.scaleMode === "beats" ? this.totalWidth : this._tickData?.widthX ?? 0;
1860
+ if (widthX <= 0) return import_lit11.html``;
1861
+ const totalChunks = Math.ceil(widthX / MAX_CANVAS_WIDTH3);
1862
+ const indices = Array.from({ length: totalChunks }, (_, i) => i);
1863
+ const dpr = typeof devicePixelRatio !== "undefined" ? devicePixelRatio : 1;
1864
+ const beatsLabels = this.scaleMode === "beats" ? this._musicalTickData?.ticks.filter((t) => t.label) ?? [] : [];
1865
+ const temporalLabels = this.scaleMode !== "beats" ? this._tickData?.labels ?? [] : [];
1771
1866
  return import_lit11.html`
1772
- <div class="header">
1773
- <span class="name" title=${this.trackName}>${this.trackName || "Untitled"}</span>
1774
- <button class="remove-btn" @click=${this._onRemoveClick} title="Remove track">
1775
- &times;
1776
- </button>
1777
- </div>
1778
- <div class="buttons">
1779
- <button
1780
- class="btn ${this.muted ? "muted-active" : ""}"
1781
- @click=${this._onMuteClick}
1782
- title="Mute"
1783
- >
1784
- M
1785
- </button>
1786
- <button class="btn ${this.soloed ? "active" : ""}" @click=${this._onSoloClick} title="Solo">
1787
- S
1788
- </button>
1789
- </div>
1790
- <div class="slider-row">
1791
- <span class="slider-label">
1792
- <span class="slider-label-name">Vol</span>
1793
- <span class="slider-label-value">${volPercent}%</span>
1794
- </span>
1795
- <input
1796
- type="range"
1797
- min="0"
1798
- max="1"
1799
- step="0.01"
1800
- .value=${String(this.volume)}
1801
- @input=${this._onVolumeInput}
1802
- />
1803
- </div>
1804
- <div class="slider-row">
1805
- <span class="slider-label">
1806
- <span class="slider-label-name">Pan</span>
1807
- <span class="slider-label-value">${panDisplay}</span>
1808
- </span>
1809
- <input
1810
- type="range"
1811
- min="-1"
1812
- max="1"
1813
- step="0.01"
1814
- .value=${String(this.pan)}
1815
- @input=${this._onPanInput}
1816
- />
1867
+ <div class="container" style="width: ${widthX}px; height: ${this.rulerHeight}px;">
1868
+ ${indices.map((i) => {
1869
+ const width = Math.min(MAX_CANVAS_WIDTH3, widthX - i * MAX_CANVAS_WIDTH3);
1870
+ return import_lit11.html`
1871
+ <canvas
1872
+ data-index=${i}
1873
+ width=${width * dpr}
1874
+ height=${this.rulerHeight * dpr}
1875
+ style="left: ${i * MAX_CANVAS_WIDTH3}px; width: ${width}px; height: ${this.rulerHeight}px;"
1876
+ ></canvas>
1877
+ `;
1878
+ })}
1879
+ ${this.scaleMode === "beats" ? beatsLabels.map(
1880
+ (t) => import_lit11.html`<span
1881
+ class="label ${t.pixel > 0 ? "centered" : ""}"
1882
+ style="left: ${t.pixel > 0 ? t.pixel : t.pixel + 4}px;"
1883
+ >${t.label}</span
1884
+ >`
1885
+ ) : temporalLabels.map(
1886
+ ({ pix, text }) => import_lit11.html`<span class="label" style="left: ${pix + 4}px;">${text}</span>`
1887
+ )}
1817
1888
  </div>
1818
1889
  `;
1819
1890
  }
1820
- };
1821
- DawTrackControlsElement.styles = import_lit11.css`
1822
- :host {
1823
- display: flex;
1824
- flex-direction: column;
1825
- justify-content: flex-start;
1826
- box-sizing: border-box;
1827
- padding: 6px 8px;
1828
- background: var(--daw-controls-background, #0f0f1a);
1829
- color: var(--daw-controls-text, #c49a6c);
1830
- border-bottom: 1px solid rgba(255, 255, 255, 0.05);
1831
- font-family: system-ui, sans-serif;
1832
- font-size: 11px;
1833
- overflow: hidden;
1834
- }
1835
- .header {
1836
- display: flex;
1837
- align-items: center;
1838
- justify-content: space-between;
1839
- gap: 4px;
1840
- margin-bottom: 3px;
1841
- }
1842
- .name {
1891
+ updated() {
1892
+ this._drawTicks();
1893
+ }
1894
+ _drawTicks() {
1895
+ const canvases = this.shadowRoot?.querySelectorAll("canvas");
1896
+ if (!canvases) return;
1897
+ const dpr = typeof devicePixelRatio !== "undefined" ? devicePixelRatio : 1;
1898
+ const rulerColor = getComputedStyle(this).getPropertyValue("--daw-ruler-color").trim() || "#c49a6c";
1899
+ const widthX = this.scaleMode === "beats" ? this.totalWidth : this._tickData?.widthX ?? 0;
1900
+ for (const canvas of canvases) {
1901
+ const idx = Number(canvas.dataset.index);
1902
+ const ctx = canvas.getContext("2d");
1903
+ if (!ctx) continue;
1904
+ const canvasWidth = Math.min(MAX_CANVAS_WIDTH3, widthX - idx * MAX_CANVAS_WIDTH3);
1905
+ const globalOffset = idx * MAX_CANVAS_WIDTH3;
1906
+ ctx.resetTransform();
1907
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
1908
+ ctx.scale(dpr, dpr);
1909
+ ctx.strokeStyle = rulerColor;
1910
+ ctx.lineWidth = 1;
1911
+ if (this.scaleMode === "beats" && this._musicalTickData) {
1912
+ const h = this.rulerHeight;
1913
+ for (const tick of this._musicalTickData.ticks) {
1914
+ const localX = tick.pixel - globalOffset;
1915
+ if (localX < 0 || localX >= canvasWidth) continue;
1916
+ const tickH = tick.type === "major" ? h * 0.6 : tick.type === "minor" ? h * 0.35 : h * 0.15;
1917
+ ctx.globalAlpha = tick.type === "major" ? 1 : 0.5;
1918
+ ctx.beginPath();
1919
+ ctx.moveTo(localX + 0.5, h);
1920
+ ctx.lineTo(localX + 0.5, h - tickH);
1921
+ ctx.stroke();
1922
+ }
1923
+ ctx.globalAlpha = 1;
1924
+ } else if (this._tickData) {
1925
+ for (const [pix, height] of this._tickData.canvasInfo) {
1926
+ const localX = pix - globalOffset;
1927
+ if (localX < 0 || localX >= canvasWidth) continue;
1928
+ ctx.beginPath();
1929
+ ctx.moveTo(localX + 0.5, this.rulerHeight);
1930
+ ctx.lineTo(localX + 0.5, this.rulerHeight - height);
1931
+ ctx.stroke();
1932
+ }
1933
+ }
1934
+ }
1935
+ }
1936
+ };
1937
+ DawRulerElement.styles = import_lit11.css`
1938
+ :host {
1939
+ display: block;
1940
+ position: relative;
1941
+ background: var(--daw-ruler-background, #0f0f1a);
1942
+ }
1943
+ .container {
1944
+ position: relative;
1945
+ }
1946
+ canvas {
1947
+ position: absolute;
1948
+ top: 0;
1949
+ }
1950
+ .label {
1951
+ position: absolute;
1952
+ font-size: 0.7rem;
1953
+ line-height: 1;
1954
+ white-space: nowrap;
1955
+ color: var(--daw-ruler-color, #c49a6c);
1956
+ top: 1px;
1957
+ }
1958
+ .label.centered {
1959
+ transform: translateX(-50%);
1960
+ }
1961
+ `;
1962
+ __decorateClass([
1963
+ (0, import_decorators10.property)({ type: Number, attribute: false })
1964
+ ], DawRulerElement.prototype, "samplesPerPixel", 2);
1965
+ __decorateClass([
1966
+ (0, import_decorators10.property)({ type: Number, attribute: false })
1967
+ ], DawRulerElement.prototype, "sampleRate", 2);
1968
+ __decorateClass([
1969
+ (0, import_decorators10.property)({ type: Number, attribute: false })
1970
+ ], DawRulerElement.prototype, "duration", 2);
1971
+ __decorateClass([
1972
+ (0, import_decorators10.property)({ type: Number, attribute: false })
1973
+ ], DawRulerElement.prototype, "rulerHeight", 2);
1974
+ __decorateClass([
1975
+ (0, import_decorators10.property)({ type: String, attribute: false })
1976
+ ], DawRulerElement.prototype, "scaleMode", 2);
1977
+ __decorateClass([
1978
+ (0, import_decorators10.property)({ type: Number, attribute: false })
1979
+ ], DawRulerElement.prototype, "ticksPerPixel", 2);
1980
+ __decorateClass([
1981
+ (0, import_decorators10.property)({ attribute: false })
1982
+ ], DawRulerElement.prototype, "meterEntries", 2);
1983
+ __decorateClass([
1984
+ (0, import_decorators10.property)({ type: Number, attribute: false })
1985
+ ], DawRulerElement.prototype, "ppqn", 2);
1986
+ __decorateClass([
1987
+ (0, import_decorators10.property)({ type: Number, attribute: false })
1988
+ ], DawRulerElement.prototype, "totalWidth", 2);
1989
+ DawRulerElement = __decorateClass([
1990
+ (0, import_decorators10.customElement)("daw-ruler")
1991
+ ], DawRulerElement);
1992
+
1993
+ // src/elements/daw-track-controls.ts
1994
+ var import_lit12 = require("lit");
1995
+ var import_decorators11 = require("lit/decorators.js");
1996
+ var DawTrackControlsElement = class extends import_lit12.LitElement {
1997
+ constructor() {
1998
+ super(...arguments);
1999
+ this.trackId = null;
2000
+ this.trackName = "";
2001
+ this.volume = 1;
2002
+ this.pan = 0;
2003
+ this.muted = false;
2004
+ this.soloed = false;
2005
+ this._onVolumeInput = (e) => {
2006
+ const value = Number(e.target.value);
2007
+ if (Number.isFinite(value)) this._dispatchControl("volume", value);
2008
+ };
2009
+ this._onPanInput = (e) => {
2010
+ const value = Number(e.target.value);
2011
+ if (Number.isFinite(value)) this._dispatchControl("pan", value);
2012
+ };
2013
+ this._onMuteClick = () => {
2014
+ this._dispatchControl("muted", !this.muted);
2015
+ };
2016
+ this._onSoloClick = () => {
2017
+ this._dispatchControl("soloed", !this.soloed);
2018
+ };
2019
+ this._onRemoveClick = () => {
2020
+ if (!this.trackId) return;
2021
+ this.dispatchEvent(
2022
+ new CustomEvent("daw-track-remove", {
2023
+ bubbles: true,
2024
+ composed: true,
2025
+ detail: { trackId: this.trackId }
2026
+ })
2027
+ );
2028
+ };
2029
+ }
2030
+ firstUpdated() {
2031
+ requestAnimationFrame(() => {
2032
+ if (!this.isConnected) return;
2033
+ const rect = this.getBoundingClientRect();
2034
+ if (rect.width > 0 && rect.height === 0) {
2035
+ console.warn(
2036
+ "[dawcore] <daw-track-controls> has zero height: container-type: size requires an explicit height on the element (the editor sets one automatically; standalone usage must too). The controls are currently invisible."
2037
+ );
2038
+ }
2039
+ });
2040
+ }
2041
+ _dispatchControl(prop, value) {
2042
+ if (!this.trackId) return;
2043
+ this.dispatchEvent(
2044
+ new CustomEvent("daw-track-control", {
2045
+ bubbles: true,
2046
+ composed: true,
2047
+ detail: { trackId: this.trackId, prop, value }
2048
+ })
2049
+ );
2050
+ }
2051
+ render() {
2052
+ const volPercent = Math.round(this.volume * 100);
2053
+ const panPercent = Math.round(Math.abs(this.pan) * 100);
2054
+ const panDisplay = this.pan === 0 ? "C" : (this.pan > 0 ? "R" : "L") + panPercent;
2055
+ return import_lit12.html`
2056
+ <div class="header">
2057
+ <span class="name" title=${this.trackName}>${this.trackName || "Untitled"}</span>
2058
+ <button class="remove-btn" @click=${this._onRemoveClick} title="Remove track">
2059
+ &times;
2060
+ </button>
2061
+ </div>
2062
+ <div class="buttons">
2063
+ <button
2064
+ class="btn ${this.muted ? "muted-active" : ""}"
2065
+ @click=${this._onMuteClick}
2066
+ title="Mute"
2067
+ >
2068
+ M
2069
+ </button>
2070
+ <button class="btn ${this.soloed ? "active" : ""}" @click=${this._onSoloClick} title="Solo">
2071
+ S
2072
+ </button>
2073
+ </div>
2074
+ <div class="slider-row vol-row">
2075
+ <span class="slider-label">
2076
+ <span class="slider-label-name">Vol</span>
2077
+ <span class="slider-label-value">${volPercent}%</span>
2078
+ </span>
2079
+ <input
2080
+ type="range"
2081
+ min="0"
2082
+ max="1"
2083
+ step="0.01"
2084
+ .value=${String(this.volume)}
2085
+ @input=${this._onVolumeInput}
2086
+ />
2087
+ </div>
2088
+ <div class="slider-row pan-row">
2089
+ <span class="slider-label">
2090
+ <span class="slider-label-name">Pan</span>
2091
+ <span class="slider-label-value">${panDisplay}</span>
2092
+ </span>
2093
+ <input
2094
+ type="range"
2095
+ min="-1"
2096
+ max="1"
2097
+ step="0.01"
2098
+ .value=${String(this.pan)}
2099
+ @input=${this._onPanInput}
2100
+ />
2101
+ </div>
2102
+ `;
2103
+ }
2104
+ };
2105
+ DawTrackControlsElement.styles = import_lit12.css`
2106
+ :host {
2107
+ display: flex;
2108
+ flex-direction: column;
2109
+ justify-content: flex-start;
2110
+ box-sizing: border-box;
2111
+ padding: 6px 8px;
2112
+ background: var(--daw-controls-background, #0f0f1a);
2113
+ color: var(--daw-controls-text, #c49a6c);
2114
+ border-bottom: 1px solid rgba(255, 255, 255, 0.05);
2115
+ font-family: system-ui, sans-serif;
2116
+ font-size: 11px;
2117
+ overflow: hidden;
2118
+ container-type: size;
2119
+ }
2120
+ .header {
2121
+ display: flex;
2122
+ align-items: center;
2123
+ justify-content: space-between;
2124
+ gap: 4px;
2125
+ margin-bottom: 3px;
2126
+ }
2127
+ .name {
1843
2128
  flex: 1;
1844
2129
  overflow: hidden;
1845
2130
  text-overflow: ellipsis;
@@ -1950,64 +2235,52 @@ DawTrackControlsElement.styles = import_lit11.css`
1950
2235
  border: none;
1951
2236
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.4);
1952
2237
  }
2238
+ /* Compact modes: drop sliders when the row is too short for the full
2239
+ stack. Thresholds are CONTENT-BOX heights — the host is border-box
2240
+ with 12px vertical padding + 1px border, so an editor-given height H
2241
+ enters compact mode at H <= 89px (Pan hidden) and H <= 73px (Vol also
2242
+ hidden). NOTE: container-type: size requires an explicit height on
2243
+ the host — the editor always provides one; standalone consumers must
2244
+ too (see the firstUpdated guard). */
2245
+ @container (max-height: 76px) {
2246
+ .pan-row {
2247
+ display: none;
2248
+ }
2249
+ }
2250
+ @container (max-height: 60px) {
2251
+ .vol-row {
2252
+ display: none;
2253
+ }
2254
+ }
1953
2255
  `;
1954
2256
  __decorateClass([
1955
- (0, import_decorators10.property)({ attribute: false })
2257
+ (0, import_decorators11.property)({ attribute: false })
1956
2258
  ], DawTrackControlsElement.prototype, "trackId", 2);
1957
2259
  __decorateClass([
1958
- (0, import_decorators10.property)({ attribute: false })
2260
+ (0, import_decorators11.property)({ attribute: false })
1959
2261
  ], DawTrackControlsElement.prototype, "trackName", 2);
1960
2262
  __decorateClass([
1961
- (0, import_decorators10.property)({ type: Number, attribute: false })
2263
+ (0, import_decorators11.property)({ type: Number, attribute: false })
1962
2264
  ], DawTrackControlsElement.prototype, "volume", 2);
1963
2265
  __decorateClass([
1964
- (0, import_decorators10.property)({ type: Number, attribute: false })
2266
+ (0, import_decorators11.property)({ type: Number, attribute: false })
1965
2267
  ], DawTrackControlsElement.prototype, "pan", 2);
1966
2268
  __decorateClass([
1967
- (0, import_decorators10.property)({ type: Boolean, attribute: false })
2269
+ (0, import_decorators11.property)({ type: Boolean, attribute: false })
1968
2270
  ], DawTrackControlsElement.prototype, "muted", 2);
1969
2271
  __decorateClass([
1970
- (0, import_decorators10.property)({ type: Boolean, attribute: false })
2272
+ (0, import_decorators11.property)({ type: Boolean, attribute: false })
1971
2273
  ], DawTrackControlsElement.prototype, "soloed", 2);
1972
2274
  DawTrackControlsElement = __decorateClass([
1973
- (0, import_decorators10.customElement)("daw-track-controls")
2275
+ (0, import_decorators11.customElement)("daw-track-controls")
1974
2276
  ], DawTrackControlsElement);
1975
2277
 
1976
2278
  // src/elements/daw-grid.ts
1977
- var import_lit12 = require("lit");
1978
- var import_decorators11 = require("lit/decorators.js");
2279
+ var import_lit13 = require("lit");
2280
+ var import_decorators12 = require("lit/decorators.js");
1979
2281
  var import_core2 = require("@waveform-playlist/core");
1980
-
1981
- // src/utils/musical-tick-cache.ts
1982
- var import_core = require("@waveform-playlist/core");
1983
- var cachedParams = null;
1984
- var cachedResult = null;
1985
- function meterEntriesMatch(a, b) {
1986
- if (a.length !== b.length) return false;
1987
- for (let i = 0; i < a.length; i++) {
1988
- if (a[i].tick !== b[i].tick || a[i].numerator !== b[i].numerator || a[i].denominator !== b[i].denominator)
1989
- return false;
1990
- }
1991
- return true;
1992
- }
1993
- function paramsMatch(a, b) {
1994
- return a.ticksPerPixel === b.ticksPerPixel && a.startPixel === b.startPixel && a.endPixel === b.endPixel && meterEntriesMatch(a.meterEntries, b.meterEntries) && (a.ppqn ?? 960) === (b.ppqn ?? 960);
1995
- }
1996
- function getCachedMusicalTicks(params) {
1997
- if (cachedParams && cachedResult && paramsMatch(cachedParams, params)) {
1998
- return cachedResult;
1999
- }
2000
- cachedResult = (0, import_core.computeMusicalTicks)(params);
2001
- cachedParams = {
2002
- ...params,
2003
- meterEntries: params.meterEntries.map((e) => ({ ...e }))
2004
- };
2005
- return cachedResult;
2006
- }
2007
-
2008
- // src/elements/daw-grid.ts
2009
- var MAX_CANVAS_WIDTH3 = 1e3;
2010
- var DawGridElement = class extends import_lit12.LitElement {
2282
+ var MAX_CANVAS_WIDTH4 = 1e3;
2283
+ var DawGridElement = class extends import_lit13.LitElement {
2011
2284
  constructor() {
2012
2285
  super(...arguments);
2013
2286
  this.ticksPerPixel = 24;
@@ -2035,25 +2308,25 @@ var DawGridElement = class extends import_lit12.LitElement {
2035
2308
  }
2036
2309
  }
2037
2310
  render() {
2038
- if (!this._tickData) return import_lit12.html``;
2311
+ if (!this._tickData) return import_lit13.html``;
2039
2312
  const totalWidth = this.length;
2040
2313
  const dpr = typeof devicePixelRatio !== "undefined" ? devicePixelRatio : 1;
2041
2314
  const indices = getVisibleChunkIndices(
2042
2315
  totalWidth,
2043
- MAX_CANVAS_WIDTH3,
2316
+ MAX_CANVAS_WIDTH4,
2044
2317
  this.visibleStart,
2045
2318
  this.visibleEnd
2046
2319
  );
2047
- return import_lit12.html`
2320
+ return import_lit13.html`
2048
2321
  <div class="container" style="width: ${totalWidth}px; height: ${this.height}px;">
2049
2322
  ${indices.map((i) => {
2050
- const width = Math.min(MAX_CANVAS_WIDTH3, totalWidth - i * MAX_CANVAS_WIDTH3);
2051
- return import_lit12.html`
2323
+ const width = Math.min(MAX_CANVAS_WIDTH4, totalWidth - i * MAX_CANVAS_WIDTH4);
2324
+ return import_lit13.html`
2052
2325
  <canvas
2053
2326
  data-index=${i}
2054
2327
  width=${width * dpr}
2055
2328
  height=${this.height * dpr}
2056
- style="left: ${i * MAX_CANVAS_WIDTH3}px; width: ${width}px; height: ${this.height}px;"
2329
+ style="left: ${i * MAX_CANVAS_WIDTH4}px; width: ${width}px; height: ${this.height}px;"
2057
2330
  ></canvas>
2058
2331
  `;
2059
2332
  })}
@@ -2077,8 +2350,8 @@ var DawGridElement = class extends import_lit12.LitElement {
2077
2350
  const idx = Number(canvas.dataset.index);
2078
2351
  const ctx = canvas.getContext("2d");
2079
2352
  if (!ctx) continue;
2080
- const chunkLeft = idx * MAX_CANVAS_WIDTH3;
2081
- const canvasWidth = Math.min(MAX_CANVAS_WIDTH3, this.length - chunkLeft);
2353
+ const chunkLeft = idx * MAX_CANVAS_WIDTH4;
2354
+ const canvasWidth = Math.min(MAX_CANVAS_WIDTH4, this.length - chunkLeft);
2082
2355
  ctx.resetTransform();
2083
2356
  ctx.clearRect(0, 0, canvas.width, canvas.height);
2084
2357
  ctx.scale(dpr, dpr);
@@ -2109,7 +2382,7 @@ var DawGridElement = class extends import_lit12.LitElement {
2109
2382
  }
2110
2383
  }
2111
2384
  };
2112
- DawGridElement.styles = import_lit12.css`
2385
+ DawGridElement.styles = import_lit13.css`
2113
2386
  :host {
2114
2387
  display: block;
2115
2388
  position: absolute;
@@ -2127,33 +2400,33 @@ DawGridElement.styles = import_lit12.css`
2127
2400
  }
2128
2401
  `;
2129
2402
  __decorateClass([
2130
- (0, import_decorators11.property)({ type: Number, attribute: false })
2403
+ (0, import_decorators12.property)({ type: Number, attribute: false })
2131
2404
  ], DawGridElement.prototype, "ticksPerPixel", 2);
2132
2405
  __decorateClass([
2133
- (0, import_decorators11.property)({ attribute: false })
2406
+ (0, import_decorators12.property)({ attribute: false })
2134
2407
  ], DawGridElement.prototype, "meterEntries", 2);
2135
2408
  __decorateClass([
2136
- (0, import_decorators11.property)({ type: Number, attribute: false })
2409
+ (0, import_decorators12.property)({ type: Number, attribute: false })
2137
2410
  ], DawGridElement.prototype, "ppqn", 2);
2138
2411
  __decorateClass([
2139
- (0, import_decorators11.property)({ type: Number, attribute: false })
2412
+ (0, import_decorators12.property)({ type: Number, attribute: false })
2140
2413
  ], DawGridElement.prototype, "visibleStart", 2);
2141
2414
  __decorateClass([
2142
- (0, import_decorators11.property)({ type: Number, attribute: false })
2415
+ (0, import_decorators12.property)({ type: Number, attribute: false })
2143
2416
  ], DawGridElement.prototype, "visibleEnd", 2);
2144
2417
  __decorateClass([
2145
- (0, import_decorators11.property)({ type: Number, attribute: false })
2418
+ (0, import_decorators12.property)({ type: Number, attribute: false })
2146
2419
  ], DawGridElement.prototype, "length", 2);
2147
2420
  __decorateClass([
2148
- (0, import_decorators11.property)({ type: Number, attribute: false })
2421
+ (0, import_decorators12.property)({ type: Number, attribute: false })
2149
2422
  ], DawGridElement.prototype, "height", 2);
2150
2423
  DawGridElement = __decorateClass([
2151
- (0, import_decorators11.customElement)("daw-grid")
2424
+ (0, import_decorators12.customElement)("daw-grid")
2152
2425
  ], DawGridElement);
2153
2426
 
2154
2427
  // src/styles/theme.ts
2155
- var import_lit13 = require("lit");
2156
- var hostStyles = import_lit13.css`
2428
+ var import_lit14 = require("lit");
2429
+ var hostStyles = import_lit14.css`
2157
2430
  :host {
2158
2431
  --daw-wave-color: #c49a6c;
2159
2432
  --daw-progress-color: #63c75f;
@@ -2169,7 +2442,7 @@ var hostStyles = import_lit13.css`
2169
2442
  --daw-clip-header-text: #e0d4c8;
2170
2443
  }
2171
2444
  `;
2172
- var clipStyles = import_lit13.css`
2445
+ var clipStyles = import_lit14.css`
2173
2446
  .clip-container {
2174
2447
  position: absolute;
2175
2448
  overflow: hidden;
@@ -2774,15 +3047,7 @@ var RecordingController = class {
2774
3047
 
2775
3048
  // src/controllers/spectrogram-controller.ts
2776
3049
  var import_spectrogram = require("@dawcore/spectrogram");
2777
- var LIBRARY_DEFAULTS = {
2778
- fftSize: 2048,
2779
- windowFunction: "hann",
2780
- frequencyScale: "mel",
2781
- minFrequency: 0,
2782
- gainDb: 20,
2783
- rangeDb: 80
2784
- };
2785
- var LIBRARY_DEFAULT_COLOR_MAP = "viridis";
3050
+ var import_core4 = require("@waveform-playlist/core");
2786
3051
  var SpectrogramController = class {
2787
3052
  constructor(host, workerFactory) {
2788
3053
  this.orchestrator = null;
@@ -2856,7 +3121,21 @@ var SpectrogramController = class {
2856
3121
  const detail = e.detail;
2857
3122
  this.host.dispatchEvent(
2858
3123
  new CustomEvent("daw-spectrogram-ready", {
2859
- detail,
3124
+ detail: { trackId: detail.trackId, generation: detail.generation },
3125
+ bubbles: true,
3126
+ composed: true
3127
+ })
3128
+ );
3129
+ });
3130
+ this.orchestrator.addEventListener("viewport-error", (e) => {
3131
+ const detail = e.detail;
3132
+ this.host.dispatchEvent(
3133
+ new CustomEvent("daw-spectrogram-error", {
3134
+ detail: {
3135
+ trackId: detail.trackId,
3136
+ generation: detail.generation,
3137
+ error: detail.error
3138
+ },
2860
3139
  bubbles: true,
2861
3140
  composed: true
2862
3141
  })
@@ -2877,18 +3156,18 @@ var SpectrogramController = class {
2877
3156
  track = c;
2878
3157
  break;
2879
3158
  }
2880
- return { ...LIBRARY_DEFAULTS, ...this.editorConfig ?? {}, ...track ?? {} };
3159
+ return { ...import_core4.SPECTROGRAM_DEFAULTS, ...this.editorConfig ?? {}, ...track ?? {} };
2881
3160
  }
2882
3161
  mergedColorMap() {
2883
3162
  for (const c of this.trackColorMaps.values()) {
2884
- return c ?? LIBRARY_DEFAULT_COLOR_MAP;
3163
+ return c ?? import_core4.DEFAULT_SPECTROGRAM_COLOR_MAP;
2885
3164
  }
2886
- return this.editorColorMap ?? LIBRARY_DEFAULT_COLOR_MAP;
3165
+ return this.editorColorMap ?? import_core4.DEFAULT_SPECTROGRAM_COLOR_MAP;
2887
3166
  }
2888
3167
  };
2889
3168
 
2890
3169
  // src/interactions/pointer-handler.ts
2891
- var import_core4 = require("@waveform-playlist/core");
3170
+ var import_core5 = require("@waveform-playlist/core");
2892
3171
 
2893
3172
  // src/interactions/constants.ts
2894
3173
  var DRAG_THRESHOLD = 3;
@@ -2910,7 +3189,11 @@ var PointerHandler = class {
2910
3189
  e.preventDefault();
2911
3190
  this._timeline = this._host.shadowRoot?.querySelector(".timeline");
2912
3191
  if (this._timeline) {
2913
- this._timeline.setPointerCapture(e.pointerId);
3192
+ try {
3193
+ this._timeline.setPointerCapture(e.pointerId);
3194
+ } catch (err) {
3195
+ console.warn("[dawcore] setPointerCapture failed: " + String(err));
3196
+ }
2914
3197
  const onMove = (me) => clipHandler.onPointerMove(me);
2915
3198
  const onUp = (ue) => {
2916
3199
  clipHandler.onPointerUp(ue);
@@ -2932,11 +3215,20 @@ var PointerHandler = class {
2932
3215
  }
2933
3216
  }
2934
3217
  this._timeline = this._host.shadowRoot?.querySelector(".timeline");
2935
- if (!this._timeline) return;
3218
+ if (!this._timeline) {
3219
+ console.warn(
3220
+ "[dawcore] PointerHandler: .timeline not found in shadow root \u2014 seek/selection ignored"
3221
+ );
3222
+ return;
3223
+ }
2936
3224
  this._timelineRect = this._timeline.getBoundingClientRect();
2937
3225
  this._dragStartPx = this._pxFromPointer(e);
2938
3226
  this._isDragging = false;
2939
- this._timeline.setPointerCapture(e.pointerId);
3227
+ try {
3228
+ this._timeline.setPointerCapture(e.pointerId);
3229
+ } catch (err) {
3230
+ console.warn("[dawcore] setPointerCapture failed: " + String(err));
3231
+ }
2940
3232
  this._timeline.addEventListener("pointermove", this._onPointerMove);
2941
3233
  this._timeline.addEventListener("pointerup", this._onPointerUp);
2942
3234
  };
@@ -2998,10 +3290,10 @@ var PointerHandler = class {
2998
3290
  const h = this._host;
2999
3291
  if (h.scaleMode === "beats") {
3000
3292
  let tick = px * h.ticksPerPixel;
3001
- tick = (0, import_core4.snapTickToGrid)(tick, h.snapTo, h._meterEntries, h.ppqn);
3293
+ tick = (0, import_core5.snapTickToGrid)(tick, h.snapTo, h._meterEntries, h.ppqn);
3002
3294
  return h._ticksToSeconds(tick);
3003
3295
  }
3004
- return (0, import_core4.pixelsToSeconds)(px, h.samplesPerPixel, h.effectiveSampleRate);
3296
+ return (0, import_core5.pixelsToSeconds)(px, h.samplesPerPixel, h.effectiveSampleRate);
3005
3297
  }
3006
3298
  _timeToPx(time) {
3007
3299
  const h = this._host;
@@ -3093,7 +3385,7 @@ var PointerHandler = class {
3093
3385
  };
3094
3386
 
3095
3387
  // src/interactions/clip-pointer-handler.ts
3096
- var import_core5 = require("@waveform-playlist/core");
3388
+ var import_core6 = require("@waveform-playlist/core");
3097
3389
  var ClipPointerHandler = class {
3098
3390
  constructor(host) {
3099
3391
  this._mode = null;
@@ -3127,7 +3419,7 @@ var ClipPointerHandler = class {
3127
3419
  const anchorTick = h._secondsToTicks(anchorSeconds);
3128
3420
  const deltaTicks = totalDeltaPx * h.ticksPerPixel;
3129
3421
  const targetTick = anchorTick + deltaTicks;
3130
- const snappedTick = h.snapTo !== "off" ? (0, import_core5.snapTickToGrid)(targetTick, h.snapTo, h._meterEntries, h.ppqn) : targetTick;
3422
+ const snappedTick = h.snapTo !== "off" ? (0, import_core6.snapTickToGrid)(targetTick, h.snapTo, h._meterEntries, h.ppqn) : targetTick;
3131
3423
  const snappedSeconds = h._ticksToSeconds(snappedTick);
3132
3424
  const snappedSample = Math.round(snappedSeconds * h.effectiveSampleRate);
3133
3425
  return snappedSample - anchorSample;
@@ -3390,7 +3682,7 @@ var ClipPointerHandler = class {
3390
3682
  };
3391
3683
 
3392
3684
  // src/interactions/file-loader.ts
3393
- var import_core6 = require("@waveform-playlist/core");
3685
+ var import_core7 = require("@waveform-playlist/core");
3394
3686
  async function loadFiles(host, files) {
3395
3687
  if (!files) {
3396
3688
  console.warn("[dawcore] loadFiles called with null/undefined");
@@ -3412,7 +3704,7 @@ async function loadFiles(host, files) {
3412
3704
  host._audioCache.delete(blobUrl);
3413
3705
  host._resolvedSampleRate = audioBuffer.sampleRate;
3414
3706
  const name = file.name.replace(/\.\w+$/, "");
3415
- const clip = (0, import_core6.createClip)({
3707
+ const clip = (0, import_core7.createClip)({
3416
3708
  audioBuffer,
3417
3709
  startSample: 0,
3418
3710
  durationSamples: audioBuffer.length,
@@ -3436,7 +3728,7 @@ async function loadFiles(host, files) {
3436
3728
  );
3437
3729
  host._peaksData = new Map(host._peaksData).set(clip.id, peakData);
3438
3730
  const trackId = crypto.randomUUID();
3439
- const track = (0, import_core6.createTrack)({ name, clips: [clip] });
3731
+ const track = (0, import_core7.createTrack)({ name, clips: [clip] });
3440
3732
  track.id = trackId;
3441
3733
  host._tracks = new Map(host._tracks).set(trackId, {
3442
3734
  name,
@@ -3598,7 +3890,7 @@ function stringifyReason(reason) {
3598
3890
  }
3599
3891
 
3600
3892
  // src/interactions/recording-clip.ts
3601
- var import_core7 = require("@waveform-playlist/core");
3893
+ var import_core8 = require("@waveform-playlist/core");
3602
3894
  function addRecordedClip(host, trackId, buf, startSample, durSamples, offsetSamples = 0) {
3603
3895
  let trimmedBuf = buf;
3604
3896
  if (offsetSamples > 0 && offsetSamples < buf.length) {
@@ -3613,7 +3905,7 @@ function addRecordedClip(host, trackId, buf, startSample, durSamples, offsetSamp
3613
3905
  }
3614
3906
  trimmedBuf = trimmed;
3615
3907
  }
3616
- const clip = (0, import_core7.createClip)({
3908
+ const clip = (0, import_core8.createClip)({
3617
3909
  audioBuffer: trimmedBuf,
3618
3910
  startSample,
3619
3911
  durationSamples: durSamples,
@@ -3868,10 +4160,144 @@ async function loadWaveformDataFromUrl(src) {
3868
4160
  }
3869
4161
  }
3870
4162
 
4163
+ // src/controllers/scroll-sync-controller.ts
4164
+ var LINE_HEIGHT_PX = 16;
4165
+ var ScrollSyncController = class {
4166
+ constructor(host) {
4167
+ this._scrollContainer = null;
4168
+ this._wheelTargets = /* @__PURE__ */ new Set();
4169
+ this._warnedX = false;
4170
+ this._warnedY = false;
4171
+ /** Selector (in host shadow DOM) for the scroll container. */
4172
+ this.scrollSelector = "";
4173
+ /** Selector for the element receiving translate3d(-scrollLeft, 0, 0). */
4174
+ this.xTargetSelector = "";
4175
+ /** Selector for the element receiving translate3d(0, -scrollTop, 0). */
4176
+ this.yTargetSelector = "";
4177
+ /**
4178
+ * Selector (or comma-separated selectors) for elements whose wheel events
4179
+ * forward to the scroll container. All matching elements receive listeners.
4180
+ */
4181
+ this.wheelForwardSelector = "";
4182
+ this._onScroll = () => {
4183
+ this._apply();
4184
+ };
4185
+ this._onWheel = (e) => {
4186
+ const sc = this._scrollContainer;
4187
+ if (!sc) return;
4188
+ const scale = e.deltaMode === WheelEvent.DOM_DELTA_LINE ? LINE_HEIGHT_PX : e.deltaMode === WheelEvent.DOM_DELTA_PAGE ? sc.clientHeight : 1;
4189
+ const scaleX = e.deltaMode === WheelEvent.DOM_DELTA_PAGE ? sc.clientWidth : scale;
4190
+ const beforeLeft = sc.scrollLeft;
4191
+ const beforeTop = sc.scrollTop;
4192
+ sc.scrollLeft += e.deltaX * scaleX;
4193
+ sc.scrollTop += e.deltaY * scale;
4194
+ if (sc.scrollLeft !== beforeLeft || sc.scrollTop !== beforeTop) {
4195
+ e.preventDefault();
4196
+ }
4197
+ };
4198
+ this._host = host;
4199
+ host.addController(this);
4200
+ }
4201
+ hostConnected() {
4202
+ requestAnimationFrame(() => {
4203
+ if (!this._host.isConnected) return;
4204
+ this._attach();
4205
+ if (!this._scrollContainer && this.scrollSelector) {
4206
+ console.warn(
4207
+ '[dawcore] ScrollSyncController: scroll container not found for "' + this.scrollSelector + '"'
4208
+ );
4209
+ }
4210
+ });
4211
+ }
4212
+ hostDisconnected() {
4213
+ this._scrollContainer?.removeEventListener("scroll", this._onScroll);
4214
+ this._scrollContainer = null;
4215
+ for (const target of this._wheelTargets) {
4216
+ target.removeEventListener("wheel", this._onWheel);
4217
+ }
4218
+ this._wheelTargets.clear();
4219
+ }
4220
+ /**
4221
+ * Re-attach and re-apply transforms from the current scroll position.
4222
+ * Called from the host's updated() so elements created by a re-render
4223
+ * (e.g. the ruler appearing when the first track loads) pick up the
4224
+ * current offset and listeners.
4225
+ */
4226
+ sync() {
4227
+ this._attach();
4228
+ }
4229
+ _query(selector) {
4230
+ return selector ? this._host.shadowRoot?.querySelector(selector) : null;
4231
+ }
4232
+ _queryAll(selector) {
4233
+ if (!selector) return [];
4234
+ return Array.from(this._host.shadowRoot?.querySelectorAll(selector) ?? []);
4235
+ }
4236
+ _attach() {
4237
+ const container = this._query(this.scrollSelector);
4238
+ if (!container) {
4239
+ if (this._scrollContainer && !this._scrollContainer.isConnected) {
4240
+ console.warn(
4241
+ '[dawcore] ScrollSyncController: scroll container "' + this.scrollSelector + '" was removed from the DOM \u2014 detaching listeners until it reappears.'
4242
+ );
4243
+ this._scrollContainer.removeEventListener("scroll", this._onScroll);
4244
+ this._scrollContainer = null;
4245
+ for (const t of this._wheelTargets) t.removeEventListener("wheel", this._onWheel);
4246
+ this._wheelTargets.clear();
4247
+ }
4248
+ return;
4249
+ }
4250
+ if (container !== this._scrollContainer) {
4251
+ this._scrollContainer?.removeEventListener("scroll", this._onScroll);
4252
+ this._scrollContainer = container;
4253
+ container.addEventListener("scroll", this._onScroll, { passive: true });
4254
+ }
4255
+ const nextTargets = new Set(this._queryAll(this.wheelForwardSelector));
4256
+ for (const old of this._wheelTargets) {
4257
+ if (!nextTargets.has(old)) {
4258
+ old.removeEventListener("wheel", this._onWheel);
4259
+ this._wheelTargets.delete(old);
4260
+ }
4261
+ }
4262
+ for (const next of nextTargets) {
4263
+ if (!this._wheelTargets.has(next)) {
4264
+ next.addEventListener("wheel", this._onWheel, { passive: false });
4265
+ this._wheelTargets.add(next);
4266
+ }
4267
+ }
4268
+ this._apply();
4269
+ }
4270
+ _apply() {
4271
+ const sc = this._scrollContainer;
4272
+ if (!sc) return;
4273
+ const xTarget = this._query(this.xTargetSelector);
4274
+ if (xTarget) {
4275
+ xTarget.style.transform = `translate3d(${-sc.scrollLeft}px, 0, 0)`;
4276
+ this._warnedX = false;
4277
+ } else if (this.xTargetSelector && sc.scrollLeft !== 0 && !this._warnedX) {
4278
+ this._warnedX = true;
4279
+ console.warn(
4280
+ '[dawcore] ScrollSyncController: x target "' + this.xTargetSelector + '" not found while scrolled \u2014 the synced pane will appear frozen. Check the selector, or clear it if the target is intentionally not rendered.'
4281
+ );
4282
+ }
4283
+ const yTarget = this._query(this.yTargetSelector);
4284
+ if (yTarget) {
4285
+ yTarget.style.transform = `translate3d(0, ${-sc.scrollTop}px, 0)`;
4286
+ this._warnedY = false;
4287
+ } else if (this.yTargetSelector && sc.scrollTop !== 0 && !this._warnedY) {
4288
+ this._warnedY = true;
4289
+ console.warn(
4290
+ '[dawcore] ScrollSyncController: y target "' + this.yTargetSelector + '" not found while scrolled \u2014 the synced pane will appear frozen. Check the selector, or clear it if the target is intentionally not rendered.'
4291
+ );
4292
+ }
4293
+ }
4294
+ };
4295
+
3871
4296
  // src/elements/daw-editor.ts
3872
4297
  var import_meta = {};
4298
+ var RULER_HEIGHT = 30;
3873
4299
  var NO_ADAPTER_ERROR = "No PlayoutAdapter set on <daw-editor>. Set editor.adapter before use.\n\n // Option 1: Native Web Audio (no Tone.js)\n npm install @dawcore/transport\n import { NativePlayoutAdapter } from '@dawcore/transport';\n editor.adapter = new NativePlayoutAdapter(new AudioContext());\n\n // Option 2: Tone.js (effects, MIDI synths)\n npm install @waveform-playlist/playout\n import { createToneAdapter } from '@waveform-playlist/playout';\n editor.adapter = createToneAdapter();";
3874
- var DawEditorElement = class extends import_lit14.LitElement {
4300
+ var DawEditorElement = class extends import_lit15.LitElement {
3875
4301
  constructor() {
3876
4302
  super(...arguments);
3877
4303
  this._samplesPerPixel = 1024;
@@ -3930,13 +4356,21 @@ var DawEditorElement = class extends import_lit14.LitElement {
3930
4356
  v.scrollSelector = ".scroll-area";
3931
4357
  return v;
3932
4358
  })();
4359
+ this._scrollSync = (() => {
4360
+ const s = new ScrollSyncController(this);
4361
+ s.scrollSelector = ".scroll-area";
4362
+ return s;
4363
+ })();
3933
4364
  /**
3934
4365
  * Cache of the last ViewportState forwarded to the spectrogram controller.
3935
4366
  * Lit's `updated()` fires on every reactive state change (`_isPlaying`,
3936
4367
  * `_selectedTrackId`, etc.) — most of which don't affect the spectrogram
3937
4368
  * viewport. Skip the cross-controller call when nothing changed.
3938
4369
  *
3939
- * The orchestrator dedupes too, but this avoids the call entirely.
4370
+ * The orchestrator dedupes identical viewports too, so removing this cache
4371
+ * wouldn't change observable behavior — but it would push a fresh
4372
+ * `setViewport` call (with object allocation) into every Lit reactive
4373
+ * update for properties unrelated to the viewport.
3940
4374
  */
3941
4375
  this._lastSpectrogramViewport = null;
3942
4376
  // --- Track Events ---
@@ -4179,9 +4613,10 @@ var DawEditorElement = class extends import_lit14.LitElement {
4179
4613
  this._spectrogramController?.unregisterCanvas(canvasId);
4180
4614
  }
4181
4615
  /**
4182
- * Push a clip's decoded audio into the spectrogram controller. No-op
4183
- * unless the track is in spectrogram render-mode and the controller
4184
- * already exists (it bootstraps from canvas registration).
4616
+ * Forward a clip's AudioBuffer to the spectrogram controller if the parent
4617
+ * track is in spectrogram render-mode. Eagerly creates the controller via
4618
+ * `_ensureSpectrogramController` so the audio data is queued for the first
4619
+ * render — even if no canvases have been registered yet.
4185
4620
  */
4186
4621
  _maybeRegisterSpectrogramClipAudio(trackId, clip) {
4187
4622
  const descriptor = this._tracks.get(trackId);
@@ -4472,6 +4907,13 @@ var DawEditorElement = class extends import_lit14.LitElement {
4472
4907
  }
4473
4908
  }
4474
4909
  updated(_changed) {
4910
+ this._scrollSync.xTargetSelector = this._showRuler ? ".ruler-content" : "";
4911
+ this._scrollSync.yTargetSelector = this._showControls ? ".controls-column" : "";
4912
+ this._scrollSync.wheelForwardSelector = [
4913
+ this._showControls ? ".controls-viewport" : "",
4914
+ this._showRuler ? ".ruler-viewport" : ""
4915
+ ].filter(Boolean).join(", ");
4916
+ this._scrollSync.sync();
4475
4917
  if (this._spectrogramController) {
4476
4918
  const vs = this._viewport.visibleStart;
4477
4919
  const ve = this._viewport.visibleEnd;
@@ -4632,7 +5074,7 @@ var DawEditorElement = class extends import_lit14.LitElement {
4632
5074
  let clip;
4633
5075
  if (waveformData) {
4634
5076
  const wdRate = waveformData.sample_rate;
4635
- clip = (0, import_core8.createClip)({
5077
+ clip = (0, import_core9.createClip)({
4636
5078
  audioBuffer,
4637
5079
  waveformData,
4638
5080
  startSample: Math.round(clipDesc.start * wdRate),
@@ -4645,7 +5087,7 @@ var DawEditorElement = class extends import_lit14.LitElement {
4645
5087
  });
4646
5088
  this._peakPipeline.cacheWaveformData(audioBuffer, waveformData);
4647
5089
  } else {
4648
- clip = (0, import_core8.createClipFromSeconds)({
5090
+ clip = (0, import_core9.createClipFromSeconds)({
4649
5091
  audioBuffer,
4650
5092
  startTime: clipDesc.start,
4651
5093
  duration: clipDesc.duration || audioBuffer.duration,
@@ -4725,7 +5167,7 @@ var DawEditorElement = class extends import_lit14.LitElement {
4725
5167
  const noteSpanSeconds = notes.length ? notes.reduce((max, n) => Math.max(max, n.time + n.duration), 0) : 0;
4726
5168
  const sourceDurationSamples = Math.ceil(Math.max(noteSpanSeconds, clipDesc.duration, 1) * sr);
4727
5169
  const requestedDurationSamples = clipDesc.duration > 0 ? Math.round(clipDesc.duration * sr) : sourceDurationSamples;
4728
- const clip = (0, import_core8.createClip)({
5170
+ const clip = (0, import_core9.createClip)({
4729
5171
  startSample: Math.round(clipDesc.start * sr),
4730
5172
  durationSamples: requestedDurationSamples,
4731
5173
  offsetSamples: Math.round(clipDesc.offset * sr),
@@ -4934,7 +5376,7 @@ var DawEditorElement = class extends import_lit14.LitElement {
4934
5376
  const waveformData = await waveformDataPromise;
4935
5377
  if (waveformData) {
4936
5378
  const wdRate = waveformData.sample_rate;
4937
- const clip2 = (0, import_core8.createClip)({
5379
+ const clip2 = (0, import_core9.createClip)({
4938
5380
  waveformData,
4939
5381
  startSample: Math.round(clipDesc.start * wdRate),
4940
5382
  durationSamples: Math.round((clipDesc.duration || waveformData.duration) * wdRate),
@@ -4959,7 +5401,7 @@ var DawEditorElement = class extends import_lit14.LitElement {
4959
5401
  });
4960
5402
  this._peaksData = new Map(this._peaksData).set(clip2.id, peakData);
4961
5403
  this._minSamplesPerPixel = Math.max(this._minSamplesPerPixel, waveformData.scale);
4962
- const previewTrack = (0, import_core8.createTrack)({
5404
+ const previewTrack = (0, import_core9.createTrack)({
4963
5405
  name: descriptor.name,
4964
5406
  clips: [clip2],
4965
5407
  volume: descriptor.volume,
@@ -5015,7 +5457,7 @@ var DawEditorElement = class extends import_lit14.LitElement {
5015
5457
  }
5016
5458
  }
5017
5459
  }
5018
- const track = (0, import_core8.createTrack)({
5460
+ const track = (0, import_core9.createTrack)({
5019
5461
  name: descriptor.name,
5020
5462
  clips,
5021
5463
  volume: descriptor.volume,
@@ -5298,6 +5740,13 @@ var DawEditorElement = class extends import_lit14.LitElement {
5298
5740
  }
5299
5741
  const oldDesc = this._tracks.get(trackId);
5300
5742
  if (!oldDesc) return;
5743
+ let normalizedRenderMode = partial.renderMode;
5744
+ if (normalizedRenderMode === "both") {
5745
+ console.warn(
5746
+ `[dawcore] render-mode="both" is not yet supported; falling back to 'spectrogram'`
5747
+ );
5748
+ normalizedRenderMode = "spectrogram";
5749
+ }
5301
5750
  const newDesc = {
5302
5751
  ...oldDesc,
5303
5752
  ...partial.name !== void 0 && { name: partial.name },
@@ -5305,7 +5754,7 @@ var DawEditorElement = class extends import_lit14.LitElement {
5305
5754
  ...partial.pan !== void 0 && { pan: partial.pan },
5306
5755
  ...partial.muted !== void 0 && { muted: partial.muted },
5307
5756
  ...partial.soloed !== void 0 && { soloed: partial.soloed },
5308
- ...partial.renderMode !== void 0 && { renderMode: partial.renderMode }
5757
+ ...normalizedRenderMode !== void 0 && { renderMode: normalizedRenderMode }
5309
5758
  };
5310
5759
  this._tracks = new Map(this._tracks).set(trackId, newDesc);
5311
5760
  if (this._engine) {
@@ -5626,7 +6075,7 @@ var DawEditorElement = class extends import_lit14.LitElement {
5626
6075
  const w = Math.floor(audibleSamples / renderSpp);
5627
6076
  return rs.peaks.map((chPeaks, ch) => {
5628
6077
  const slicedPeaks = latencyPixels > 0 ? chPeaks.slice(latencyPixels * 2) : chPeaks;
5629
- return import_lit14.html`
6078
+ return import_lit15.html`
5630
6079
  <daw-waveform
5631
6080
  data-recording-track=${trackId}
5632
6081
  data-recording-channel=${ch}
@@ -5682,6 +6131,14 @@ var DawEditorElement = class extends import_lit14.LitElement {
5682
6131
  _getPlayhead() {
5683
6132
  return this.shadowRoot?.querySelector("daw-playhead");
5684
6133
  }
6134
+ /** True when the controls column should be rendered (and its selector is valid). */
6135
+ get _showControls() {
6136
+ return this._getOrderedTracks().length > 0 || this.indefinitePlayback;
6137
+ }
6138
+ /** True when the ruler header band should be rendered (and its selector is valid). */
6139
+ get _showRuler() {
6140
+ return (this._getOrderedTracks().length > 0 || this.scaleMode === "beats" || this.indefinitePlayback) && this.timescale;
6141
+ }
5685
6142
  _getOrderedTracks() {
5686
6143
  const domOrder = [...this.querySelectorAll("daw-track")].map(
5687
6144
  (el) => el.trackId
@@ -5723,64 +6180,79 @@ var DawEditorElement = class extends import_lit14.LitElement {
5723
6180
  trackHeight: this.waveHeight * numChannels + (this.clipHeaders ? this.clipHeaderHeight : 0)
5724
6181
  };
5725
6182
  });
5726
- return import_lit14.html`
5727
- ${orderedTracks.length > 0 || this.indefinitePlayback ? import_lit14.html`<div class="controls-column">
5728
- ${this.timescale ? import_lit14.html`<div style="height: 30px;"></div>` : ""}
5729
- ${orderedTracks.map(
5730
- (t) => import_lit14.html`
5731
- <daw-track-controls
5732
- style="height: ${t.trackHeight}px;"
5733
- .trackId=${t.trackId}
5734
- .trackName=${t.descriptor?.name ?? "Untitled"}
5735
- .volume=${t.descriptor?.volume ?? 1}
5736
- .pan=${t.descriptor?.pan ?? 0}
5737
- .muted=${t.descriptor?.muted ?? false}
5738
- .soloed=${t.descriptor?.soloed ?? false}
5739
- ></daw-track-controls>
5740
- `
5741
- )}
5742
- </div>` : ""}
5743
- <div class="scroll-area">
5744
- <div
5745
- class="timeline ${this._dragOver ? "drag-over" : ""}"
5746
- style="width: ${this._totalWidth > 0 ? this._totalWidth + "px" : "100%"};"
5747
- data-playing=${this._isPlaying}
5748
- @pointerdown=${this._pointer.onPointerDown}
5749
- @dragover=${this._onDragOver}
5750
- @dragleave=${this._onDragLeave}
5751
- @drop=${this._onDrop}
5752
- >
5753
- ${(orderedTracks.length > 0 || this.scaleMode === "beats" || this.indefinitePlayback) && this.timescale ? import_lit14.html`<daw-ruler
5754
- .samplesPerPixel=${spp}
5755
- .sampleRate=${this.effectiveSampleRate}
5756
- .duration=${this._duration}
5757
- .scaleMode=${this.scaleMode}
5758
- .ticksPerPixel=${this.ticksPerPixel}
5759
- .meterEntries=${this._meterEntries}
5760
- .ppqn=${this.ppqn}
5761
- .totalWidth=${this._totalWidth}
5762
- ></daw-ruler>` : ""}
5763
- ${this.scaleMode === "beats" ? import_lit14.html`<daw-grid
5764
- style="top: ${this.timescale ? 30 : 0}px;"
5765
- .ticksPerPixel=${this.ticksPerPixel}
5766
- .meterEntries=${this._meterEntries}
5767
- .ppqn=${this.ppqn}
5768
- .visibleStart=${this._viewport.visibleStart}
5769
- .visibleEnd=${this._viewport.visibleEnd}
5770
- .length=${this._totalWidth}
5771
- .height=${orderedTracks.length > 0 ? orderedTracks.reduce((sum, t) => sum + t.trackHeight + 1, 0) : this._emptyGridHeight}
5772
- ></daw-grid>` : ""}
5773
- ${orderedTracks.length > 0 || this.scaleMode === "beats" || this.indefinitePlayback ? import_lit14.html`<daw-selection .startPx=${selStartPx} .endPx=${selEndPx}></daw-selection>
5774
- <daw-playhead></daw-playhead>` : ""}
5775
- ${orderedTracks.map((t) => {
5776
- const channelHeight = this.waveHeight;
5777
- return import_lit14.html`
6183
+ const showControls = this._showControls;
6184
+ const showRuler = this._showRuler;
6185
+ return import_lit15.html`
6186
+ ${showRuler ? import_lit15.html`<div class="header-row" style="height: ${RULER_HEIGHT}px;">
6187
+ ${showControls ? import_lit15.html`<div class="ruler-gap"></div>` : ""}
6188
+ <div class="ruler-viewport" @pointerdown=${this._pointer.onPointerDown}>
5778
6189
  <div
5779
- class="track-row ${t.trackId === this._selectedTrackId ? "selected" : ""}"
5780
- style="height: ${t.trackHeight}px;"
5781
- data-track-id=${t.trackId}
6190
+ class="ruler-content"
6191
+ style="width: ${this._totalWidth > 0 ? this._totalWidth + "px" : "100%"};"
5782
6192
  >
5783
- ${t.track.clips.map((clip) => {
6193
+ <daw-ruler
6194
+ .samplesPerPixel=${spp}
6195
+ .sampleRate=${this.effectiveSampleRate}
6196
+ .duration=${this._duration}
6197
+ .scaleMode=${this.scaleMode}
6198
+ .ticksPerPixel=${this.ticksPerPixel}
6199
+ .meterEntries=${this._meterEntries}
6200
+ .ppqn=${this.ppqn}
6201
+ .totalWidth=${this._totalWidth}
6202
+ .rulerHeight=${RULER_HEIGHT}
6203
+ ></daw-ruler>
6204
+ </div>
6205
+ </div>
6206
+ </div>` : ""}
6207
+ <div class="body">
6208
+ ${showControls ? import_lit15.html`<div class="controls-viewport">
6209
+ <div class="controls-column">
6210
+ ${orderedTracks.map(
6211
+ (t) => import_lit15.html`
6212
+ <daw-track-controls
6213
+ style="height: ${t.trackHeight}px;"
6214
+ .trackId=${t.trackId}
6215
+ .trackName=${t.descriptor?.name ?? "Untitled"}
6216
+ .volume=${t.descriptor?.volume ?? 1}
6217
+ .pan=${t.descriptor?.pan ?? 0}
6218
+ .muted=${t.descriptor?.muted ?? false}
6219
+ .soloed=${t.descriptor?.soloed ?? false}
6220
+ ></daw-track-controls>
6221
+ `
6222
+ )}
6223
+ </div>
6224
+ </div>` : ""}
6225
+ <div class="scroll-area">
6226
+ <div
6227
+ class="timeline ${this._dragOver ? "drag-over" : ""}"
6228
+ style="width: ${this._totalWidth > 0 ? this._totalWidth + "px" : "100%"};"
6229
+ data-playing=${this._isPlaying}
6230
+ @pointerdown=${this._pointer.onPointerDown}
6231
+ @dragover=${this._onDragOver}
6232
+ @dragleave=${this._onDragLeave}
6233
+ @drop=${this._onDrop}
6234
+ >
6235
+ ${this.scaleMode === "beats" ? import_lit15.html`<daw-grid
6236
+ style="top: 0px;"
6237
+ .ticksPerPixel=${this.ticksPerPixel}
6238
+ .meterEntries=${this._meterEntries}
6239
+ .ppqn=${this.ppqn}
6240
+ .visibleStart=${this._viewport.visibleStart}
6241
+ .visibleEnd=${this._viewport.visibleEnd}
6242
+ .length=${this._totalWidth}
6243
+ .height=${orderedTracks.length > 0 ? orderedTracks.reduce((sum, t) => sum + t.trackHeight, 0) : this._emptyGridHeight}
6244
+ ></daw-grid>` : ""}
6245
+ ${orderedTracks.length > 0 || this.scaleMode === "beats" || this.indefinitePlayback ? import_lit15.html`<daw-selection .startPx=${selStartPx} .endPx=${selEndPx}></daw-selection>
6246
+ <daw-playhead></daw-playhead>` : ""}
6247
+ ${orderedTracks.map((t) => {
6248
+ const channelHeight = this.waveHeight;
6249
+ return import_lit15.html`
6250
+ <div
6251
+ class="track-row ${t.trackId === this._selectedTrackId ? "selected" : ""}"
6252
+ style="height: ${t.trackHeight}px;"
6253
+ data-track-id=${t.trackId}
6254
+ >
6255
+ ${t.track.clips.map((clip) => {
5784
6256
  const peakData = this._peaksData.get(clip.id);
5785
6257
  let clipLeft;
5786
6258
  let width;
@@ -5793,7 +6265,7 @@ var DawEditorElement = class extends import_lit14.LitElement {
5793
6265
  width = Math.round(endTick / this.ticksPerPixel) - clipLeft;
5794
6266
  } else {
5795
6267
  clipLeft = Math.floor(clip.startSample / spp);
5796
- width = (0, import_core8.clipPixelWidth)(clip.startSample, clip.durationSamples, spp);
6268
+ width = (0, import_core9.clipPixelWidth)(clip.startSample, clip.durationSamples, spp);
5797
6269
  }
5798
6270
  let clipSegments;
5799
6271
  let segmentChannels;
@@ -5823,7 +6295,10 @@ var DawEditorElement = class extends import_lit14.LitElement {
5823
6295
  const segEndSample = Math.round(segEndAudioSec * sr);
5824
6296
  const totalPeaks = clip.durationSamples / baseScale;
5825
6297
  clipSegments.push({
5826
- peakStart: Math.max(0, (segStartSample - clip.offsetSamples) / baseScale),
6298
+ peakStart: Math.max(
6299
+ 0,
6300
+ (segStartSample - clip.offsetSamples) / baseScale
6301
+ ),
5827
6302
  peakEnd: Math.min(
5828
6303
  totalPeaks,
5829
6304
  (segEndSample - clip.offsetSamples) / baseScale
@@ -5837,78 +6312,79 @@ var DawEditorElement = class extends import_lit14.LitElement {
5837
6312
  const channels = segmentChannels ?? peakData?.data ?? [new Int16Array(0)];
5838
6313
  const hdrH = this.clipHeaders ? this.clipHeaderHeight : 0;
5839
6314
  const chH = this.waveHeight;
5840
- return import_lit14.html` <div
5841
- class="clip-container"
5842
- style="left:${clipLeft}px;top:0;width:${width}px;height:${t.trackHeight}px;"
5843
- data-clip-id=${clip.id}
5844
- >
5845
- ${hdrH > 0 ? import_lit14.html`<div
5846
- class="clip-header"
5847
- data-clip-id=${clip.id}
5848
- data-track-id=${t.trackId}
5849
- ?data-interactive=${this.interactiveClips}
5850
- >
5851
- <span>${clip.name || t.descriptor?.name || ""}</span>
5852
- </div>` : ""}
5853
- ${t.descriptor?.renderMode === "piano-roll" ? import_lit14.html`<daw-piano-roll
5854
- style="position:absolute;left:0;top:${hdrH}px;"
5855
- .midiNotes=${clip.midiNotes ?? []}
5856
- .length=${peakData?.length ?? width}
5857
- .waveHeight=${chH * channels.length}
5858
- .samplesPerPixel=${this._renderSpp}
5859
- .sampleRate=${this.effectiveSampleRate}
5860
- .clipOffsetSeconds=${(clip.offsetSamples ?? 0) / this.effectiveSampleRate}
5861
- .visibleStart=${this._viewport.visibleStart}
5862
- .visibleEnd=${this._viewport.visibleEnd}
5863
- .originX=${clipLeft}
5864
- ?selected=${t.trackId === this._selectedTrackId}
5865
- ></daw-piano-roll>` : t.descriptor?.renderMode === "spectrogram" ? channels.map(
5866
- (_chPeaks, chIdx) => import_lit14.html`<daw-spectrogram
5867
- style="position:absolute;left:0;top:${hdrH + chIdx * chH}px;height:${chH}px;width:${peakData?.length ?? width}px;"
5868
- .clipId=${clip.id}
5869
- .trackId=${t.trackId}
5870
- .channelIndex=${chIdx}
5871
- .length=${peakData?.length ?? width}
5872
- .waveHeight=${chH}
5873
- .samplesPerPixel=${this._renderSpp}
5874
- .sampleRate=${this.effectiveSampleRate}
5875
- .clipOffsetSeconds=${(clip.offsetSamples ?? 0) / this.effectiveSampleRate}
5876
- .visibleStart=${this._viewport.visibleStart}
5877
- .visibleEnd=${this._viewport.visibleEnd}
5878
- .originX=${clipLeft}
5879
- ></daw-spectrogram>`
5880
- ) : channels.map(
5881
- (chPeaks, chIdx) => import_lit14.html` <daw-waveform
5882
- style="position:absolute;left:0;top:${hdrH + chIdx * chH}px;"
5883
- .peaks=${chPeaks}
5884
- .length=${peakData?.length ?? width}
5885
- .waveHeight=${chH}
5886
- .barWidth=${this.barWidth}
5887
- .barGap=${this.barGap}
5888
- .visibleStart=${this._viewport.visibleStart}
5889
- .visibleEnd=${this._viewport.visibleEnd}
5890
- .originX=${clipLeft}
5891
- .segments=${clipSegments}
5892
- ></daw-waveform>`
5893
- )}
5894
- ${this.interactiveClips ? import_lit14.html` <div
5895
- class="clip-boundary"
5896
- data-boundary-edge="left"
6315
+ return import_lit15.html` <div
6316
+ class="clip-container"
6317
+ style="left:${clipLeft}px;top:0;width:${width}px;height:${t.trackHeight}px;"
6318
+ data-clip-id=${clip.id}
6319
+ >
6320
+ ${hdrH > 0 ? import_lit15.html`<div
6321
+ class="clip-header"
5897
6322
  data-clip-id=${clip.id}
5898
6323
  data-track-id=${t.trackId}
5899
- ></div>
5900
- <div
5901
- class="clip-boundary"
5902
- data-boundary-edge="right"
5903
- data-clip-id=${clip.id}
5904
- data-track-id=${t.trackId}
5905
- ></div>` : ""}
5906
- </div>`;
6324
+ ?data-interactive=${this.interactiveClips}
6325
+ >
6326
+ <span>${clip.name || t.descriptor?.name || ""}</span>
6327
+ </div>` : ""}
6328
+ ${t.descriptor?.renderMode === "piano-roll" ? import_lit15.html`<daw-piano-roll
6329
+ style="position:absolute;left:0;top:${hdrH}px;"
6330
+ .midiNotes=${clip.midiNotes ?? []}
6331
+ .length=${peakData?.length ?? width}
6332
+ .waveHeight=${chH * channels.length}
6333
+ .samplesPerPixel=${this._renderSpp}
6334
+ .sampleRate=${this.effectiveSampleRate}
6335
+ .clipOffsetSeconds=${(clip.offsetSamples ?? 0) / this.effectiveSampleRate}
6336
+ .visibleStart=${this._viewport.visibleStart}
6337
+ .visibleEnd=${this._viewport.visibleEnd}
6338
+ .originX=${clipLeft}
6339
+ ?selected=${t.trackId === this._selectedTrackId}
6340
+ ></daw-piano-roll>` : t.descriptor?.renderMode === "spectrogram" ? channels.map(
6341
+ (_chPeaks, chIdx) => import_lit15.html`<daw-spectrogram
6342
+ style="position:absolute;left:0;top:${hdrH + chIdx * chH}px;height:${chH}px;width:${peakData?.length ?? width}px;"
6343
+ .clipId=${clip.id}
6344
+ .trackId=${t.trackId}
6345
+ .channelIndex=${chIdx}
6346
+ .length=${peakData?.length ?? width}
6347
+ .waveHeight=${chH}
6348
+ .samplesPerPixel=${this._renderSpp}
6349
+ .sampleRate=${this.effectiveSampleRate}
6350
+ .clipOffsetSeconds=${(clip.offsetSamples ?? 0) / this.effectiveSampleRate}
6351
+ .visibleStart=${this._viewport.visibleStart}
6352
+ .visibleEnd=${this._viewport.visibleEnd}
6353
+ .originX=${clipLeft}
6354
+ ></daw-spectrogram>`
6355
+ ) : channels.map(
6356
+ (chPeaks, chIdx) => import_lit15.html` <daw-waveform
6357
+ style="position:absolute;left:0;top:${hdrH + chIdx * chH}px;"
6358
+ .peaks=${chPeaks}
6359
+ .length=${peakData?.length ?? width}
6360
+ .waveHeight=${chH}
6361
+ .barWidth=${this.barWidth}
6362
+ .barGap=${this.barGap}
6363
+ .visibleStart=${this._viewport.visibleStart}
6364
+ .visibleEnd=${this._viewport.visibleEnd}
6365
+ .originX=${clipLeft}
6366
+ .segments=${clipSegments}
6367
+ ></daw-waveform>`
6368
+ )}
6369
+ ${this.interactiveClips ? import_lit15.html` <div
6370
+ class="clip-boundary"
6371
+ data-boundary-edge="left"
6372
+ data-clip-id=${clip.id}
6373
+ data-track-id=${t.trackId}
6374
+ ></div>
6375
+ <div
6376
+ class="clip-boundary"
6377
+ data-boundary-edge="right"
6378
+ data-clip-id=${clip.id}
6379
+ data-track-id=${t.trackId}
6380
+ ></div>` : ""}
6381
+ </div>`;
5907
6382
  })}
5908
- ${this._renderRecordingPreview(t.trackId, channelHeight)}
5909
- </div>
5910
- `;
6383
+ ${this._renderRecordingPreview(t.trackId, channelHeight)}
6384
+ </div>
6385
+ `;
5911
6386
  })}
6387
+ </div>
5912
6388
  </div>
5913
6389
  </div>
5914
6390
  <slot></slot>
@@ -5917,21 +6393,48 @@ var DawEditorElement = class extends import_lit14.LitElement {
5917
6393
  };
5918
6394
  DawEditorElement.styles = [
5919
6395
  hostStyles,
5920
- import_lit14.css`
6396
+ import_lit15.css`
5921
6397
  :host {
5922
6398
  display: flex;
6399
+ flex-direction: column;
5923
6400
  position: relative;
5924
6401
  background: var(--daw-background, #1a1a2e);
5925
6402
  overflow: hidden;
5926
6403
  }
5927
- .controls-column {
6404
+ .header-row {
6405
+ display: flex;
6406
+ flex-shrink: 0;
6407
+ }
6408
+ .ruler-gap {
6409
+ flex-shrink: 0;
6410
+ width: var(--daw-controls-width, 180px);
6411
+ }
6412
+ .ruler-viewport {
6413
+ flex: 1;
6414
+ position: relative;
6415
+ overflow: hidden;
6416
+ cursor: text;
6417
+ }
6418
+ .ruler-content {
6419
+ will-change: transform;
6420
+ }
6421
+ .body {
6422
+ flex: 1;
6423
+ min-height: 0;
6424
+ display: flex;
6425
+ }
6426
+ .controls-viewport {
5928
6427
  flex-shrink: 0;
5929
6428
  width: var(--daw-controls-width, 180px);
6429
+ overflow: hidden;
6430
+ }
6431
+ .controls-column {
6432
+ will-change: transform;
5930
6433
  }
5931
6434
  .scroll-area {
5932
6435
  flex: 1;
5933
- overflow-x: auto;
5934
- overflow-y: hidden;
6436
+ overflow: auto;
6437
+ overflow-anchor: none;
5935
6438
  min-height: var(--daw-min-height, 200px);
5936
6439
  }
5937
6440
  .timeline {
@@ -5941,6 +6444,7 @@ DawEditorElement.styles = [
5941
6444
  }
5942
6445
  .track-row {
5943
6446
  position: relative;
6447
+ box-sizing: border-box;
5944
6448
  background: var(--daw-track-background, #16213e);
5945
6449
  border-bottom: 1px solid rgba(255, 255, 255, 0.05);
5946
6450
  }
@@ -5965,333 +6469,102 @@ DawEditorElement.styles = [
5965
6469
  ];
5966
6470
  DawEditorElement._CONTROL_PROPS = /* @__PURE__ */ new Set(["volume", "pan", "muted", "soloed"]);
5967
6471
  __decorateClass([
5968
- (0, import_decorators12.property)({ type: Number, attribute: "samples-per-pixel", noAccessor: true })
6472
+ (0, import_decorators13.property)({ type: Number, attribute: "samples-per-pixel", noAccessor: true })
5969
6473
  ], DawEditorElement.prototype, "samplesPerPixel", 1);
5970
6474
  __decorateClass([
5971
- (0, import_decorators12.property)({ type: Number, attribute: "wave-height" })
6475
+ (0, import_decorators13.property)({ type: Number, attribute: "wave-height" })
5972
6476
  ], DawEditorElement.prototype, "waveHeight", 2);
5973
6477
  __decorateClass([
5974
- (0, import_decorators12.property)({ type: Boolean })
6478
+ (0, import_decorators13.property)({ type: Boolean })
5975
6479
  ], DawEditorElement.prototype, "timescale", 2);
5976
6480
  __decorateClass([
5977
- (0, import_decorators12.property)({ type: Boolean })
6481
+ (0, import_decorators13.property)({ type: Boolean })
5978
6482
  ], DawEditorElement.prototype, "mono", 2);
5979
6483
  __decorateClass([
5980
- (0, import_decorators12.property)({ type: Number, attribute: "bar-width" })
6484
+ (0, import_decorators13.property)({ type: Number, attribute: "bar-width" })
5981
6485
  ], DawEditorElement.prototype, "barWidth", 2);
5982
6486
  __decorateClass([
5983
- (0, import_decorators12.property)({ type: Number, attribute: "bar-gap" })
6487
+ (0, import_decorators13.property)({ type: Number, attribute: "bar-gap" })
5984
6488
  ], DawEditorElement.prototype, "barGap", 2);
5985
6489
  __decorateClass([
5986
- (0, import_decorators12.property)({ type: Boolean, attribute: "file-drop" })
6490
+ (0, import_decorators13.property)({ type: Boolean, attribute: "file-drop" })
5987
6491
  ], DawEditorElement.prototype, "fileDrop", 2);
5988
6492
  __decorateClass([
5989
- (0, import_decorators12.property)({ type: Boolean, attribute: "clip-headers" })
6493
+ (0, import_decorators13.property)({ type: Boolean, attribute: "clip-headers" })
5990
6494
  ], DawEditorElement.prototype, "clipHeaders", 2);
5991
6495
  __decorateClass([
5992
- (0, import_decorators12.property)({ type: Number, attribute: "clip-header-height" })
6496
+ (0, import_decorators13.property)({ type: Number, attribute: "clip-header-height" })
5993
6497
  ], DawEditorElement.prototype, "clipHeaderHeight", 2);
5994
6498
  __decorateClass([
5995
- (0, import_decorators12.property)({ type: Boolean, attribute: "interactive-clips" })
6499
+ (0, import_decorators13.property)({ type: Boolean, attribute: "interactive-clips" })
5996
6500
  ], DawEditorElement.prototype, "interactiveClips", 2);
5997
6501
  __decorateClass([
5998
- (0, import_decorators12.property)({ type: Boolean, attribute: "indefinite-playback" })
6502
+ (0, import_decorators13.property)({ type: Boolean, attribute: "indefinite-playback" })
5999
6503
  ], DawEditorElement.prototype, "indefinitePlayback", 2);
6000
6504
  __decorateClass([
6001
- (0, import_decorators12.property)({ attribute: false, noAccessor: true })
6505
+ (0, import_decorators13.property)({ attribute: false, noAccessor: true })
6002
6506
  ], DawEditorElement.prototype, "spectrogramConfig", 1);
6003
6507
  __decorateClass([
6004
- (0, import_decorators12.property)({ attribute: false, noAccessor: true })
6508
+ (0, import_decorators13.property)({ attribute: false, noAccessor: true })
6005
6509
  ], DawEditorElement.prototype, "spectrogramColorMap", 1);
6006
6510
  __decorateClass([
6007
- (0, import_decorators12.property)({ type: String, attribute: "scale-mode" })
6511
+ (0, import_decorators13.property)({ type: String, attribute: "scale-mode" })
6008
6512
  ], DawEditorElement.prototype, "scaleMode", 2);
6009
6513
  __decorateClass([
6010
- (0, import_decorators12.property)({ type: Number, attribute: "ticks-per-pixel", noAccessor: true })
6514
+ (0, import_decorators13.property)({ type: Number, attribute: "ticks-per-pixel", noAccessor: true })
6011
6515
  ], DawEditorElement.prototype, "ticksPerPixel", 1);
6012
6516
  __decorateClass([
6013
- (0, import_decorators12.property)({ type: Number, noAccessor: true })
6517
+ (0, import_decorators13.property)({ type: Number, noAccessor: true })
6014
6518
  ], DawEditorElement.prototype, "bpm", 1);
6015
6519
  __decorateClass([
6016
- (0, import_decorators12.property)({ attribute: false })
6520
+ (0, import_decorators13.property)({ attribute: false })
6017
6521
  ], DawEditorElement.prototype, "timeSignature", 2);
6018
6522
  __decorateClass([
6019
- (0, import_decorators12.property)({ attribute: false })
6523
+ (0, import_decorators13.property)({ attribute: false })
6020
6524
  ], DawEditorElement.prototype, "meterEntries", 2);
6021
6525
  __decorateClass([
6022
- (0, import_decorators12.property)({ type: Number, noAccessor: true })
6526
+ (0, import_decorators13.property)({ type: Number, noAccessor: true })
6023
6527
  ], DawEditorElement.prototype, "ppqn", 1);
6024
6528
  __decorateClass([
6025
- (0, import_decorators12.property)({ type: String, attribute: "snap-to" })
6529
+ (0, import_decorators13.property)({ type: String, attribute: "snap-to" })
6026
6530
  ], DawEditorElement.prototype, "snapTo", 2);
6027
6531
  __decorateClass([
6028
- (0, import_decorators12.property)({ attribute: false })
6532
+ (0, import_decorators13.property)({ attribute: false })
6029
6533
  ], DawEditorElement.prototype, "secondsToTicks", 2);
6030
6534
  __decorateClass([
6031
- (0, import_decorators12.property)({ attribute: false })
6535
+ (0, import_decorators13.property)({ attribute: false })
6032
6536
  ], DawEditorElement.prototype, "ticksToSeconds", 2);
6033
6537
  __decorateClass([
6034
- (0, import_decorators12.state)()
6538
+ (0, import_decorators13.state)()
6035
6539
  ], DawEditorElement.prototype, "_tracks", 2);
6036
6540
  __decorateClass([
6037
- (0, import_decorators12.state)()
6541
+ (0, import_decorators13.state)()
6038
6542
  ], DawEditorElement.prototype, "_engineTracks", 2);
6039
6543
  __decorateClass([
6040
- (0, import_decorators12.state)()
6544
+ (0, import_decorators13.state)()
6041
6545
  ], DawEditorElement.prototype, "_peaksData", 2);
6042
6546
  __decorateClass([
6043
- (0, import_decorators12.state)()
6547
+ (0, import_decorators13.state)()
6044
6548
  ], DawEditorElement.prototype, "_isPlaying", 2);
6045
6549
  __decorateClass([
6046
- (0, import_decorators12.state)()
6550
+ (0, import_decorators13.state)()
6047
6551
  ], DawEditorElement.prototype, "_duration", 2);
6048
6552
  __decorateClass([
6049
- (0, import_decorators12.state)()
6553
+ (0, import_decorators13.state)()
6050
6554
  ], DawEditorElement.prototype, "_selectedTrackId", 2);
6051
6555
  __decorateClass([
6052
- (0, import_decorators12.state)()
6556
+ (0, import_decorators13.state)()
6053
6557
  ], DawEditorElement.prototype, "_dragOver", 2);
6054
6558
  __decorateClass([
6055
- (0, import_decorators12.property)({ attribute: false })
6559
+ (0, import_decorators13.property)({ attribute: false })
6056
6560
  ], DawEditorElement.prototype, "adapter", 1);
6057
6561
  __decorateClass([
6058
- (0, import_decorators12.property)({ attribute: "eager-resume" })
6562
+ (0, import_decorators13.property)({ attribute: "eager-resume" })
6059
6563
  ], DawEditorElement.prototype, "eagerResume", 2);
6060
6564
  DawEditorElement = __decorateClass([
6061
- (0, import_decorators12.customElement)("daw-editor")
6565
+ (0, import_decorators13.customElement)("daw-editor")
6062
6566
  ], DawEditorElement);
6063
6567
 
6064
- // src/elements/daw-ruler.ts
6065
- var import_lit15 = require("lit");
6066
- var import_decorators13 = require("lit/decorators.js");
6067
-
6068
- // src/utils/time-format.ts
6069
- function formatTime(milliseconds) {
6070
- const seconds = Math.floor(milliseconds / 1e3);
6071
- const s = seconds % 60;
6072
- const m = (seconds - s) / 60;
6073
- return `${m}:${String(s).padStart(2, "0")}`;
6074
- }
6075
-
6076
- // src/utils/smart-scale.ts
6077
- var timeinfo = /* @__PURE__ */ new Map([
6078
- [700, { marker: 1e3, bigStep: 500, smallStep: 100 }],
6079
- [1500, { marker: 2e3, bigStep: 1e3, smallStep: 200 }],
6080
- [2500, { marker: 2e3, bigStep: 1e3, smallStep: 500 }],
6081
- [5e3, { marker: 5e3, bigStep: 1e3, smallStep: 500 }],
6082
- [1e4, { marker: 1e4, bigStep: 5e3, smallStep: 1e3 }],
6083
- [12e3, { marker: 15e3, bigStep: 5e3, smallStep: 1e3 }],
6084
- [Infinity, { marker: 3e4, bigStep: 1e4, smallStep: 5e3 }]
6085
- ]);
6086
- function getScaleInfo(samplesPerPixel) {
6087
- for (const [resolution, config] of timeinfo) {
6088
- if (samplesPerPixel < resolution) {
6089
- return config;
6090
- }
6091
- }
6092
- return { marker: 3e4, bigStep: 1e4, smallStep: 5e3 };
6093
- }
6094
- function computeTemporalTicks(samplesPerPixel, sampleRate, duration, rulerHeight) {
6095
- const widthX = Math.ceil(duration * sampleRate / samplesPerPixel);
6096
- const config = getScaleInfo(samplesPerPixel);
6097
- const { marker, bigStep, smallStep } = config;
6098
- const canvasInfo = /* @__PURE__ */ new Map();
6099
- const labels = [];
6100
- const pixPerSec = sampleRate / samplesPerPixel;
6101
- for (let counter = 0; ; counter += smallStep) {
6102
- const pix = Math.floor(counter / 1e3 * pixPerSec);
6103
- if (pix >= widthX) break;
6104
- if (counter % marker === 0) {
6105
- canvasInfo.set(pix, rulerHeight);
6106
- labels.push({ pix, text: formatTime(counter) });
6107
- } else if (counter % bigStep === 0) {
6108
- canvasInfo.set(pix, Math.floor(rulerHeight / 2));
6109
- } else if (counter % smallStep === 0) {
6110
- canvasInfo.set(pix, Math.floor(rulerHeight / 5));
6111
- }
6112
- }
6113
- return { widthX, canvasInfo, labels };
6114
- }
6115
-
6116
- // src/elements/daw-ruler.ts
6117
- var MAX_CANVAS_WIDTH4 = 1e3;
6118
- var DawRulerElement = class extends import_lit15.LitElement {
6119
- constructor() {
6120
- super(...arguments);
6121
- this.samplesPerPixel = 1024;
6122
- this.sampleRate = 48e3;
6123
- this.duration = 0;
6124
- this.rulerHeight = 30;
6125
- this.scaleMode = "temporal";
6126
- this.ticksPerPixel = 4;
6127
- this.meterEntries = [
6128
- { tick: 0, numerator: 4, denominator: 4 }
6129
- ];
6130
- this.ppqn = 960;
6131
- this.totalWidth = 0;
6132
- this._tickData = null;
6133
- this._musicalTickData = null;
6134
- }
6135
- willUpdate() {
6136
- if (this.scaleMode === "beats" && this.totalWidth > 0) {
6137
- this._musicalTickData = getCachedMusicalTicks({
6138
- meterEntries: this.meterEntries,
6139
- ticksPerPixel: this.ticksPerPixel,
6140
- startPixel: 0,
6141
- endPixel: this.totalWidth,
6142
- ppqn: this.ppqn
6143
- });
6144
- this._tickData = null;
6145
- } else if (this.duration > 0 || this.totalWidth > 0) {
6146
- const widthDerivedDuration = this.totalWidth * this.samplesPerPixel / this.sampleRate;
6147
- const effectiveDuration = Math.max(this.duration, widthDerivedDuration);
6148
- this._musicalTickData = null;
6149
- this._tickData = computeTemporalTicks(
6150
- this.samplesPerPixel,
6151
- this.sampleRate,
6152
- effectiveDuration,
6153
- this.rulerHeight
6154
- );
6155
- } else {
6156
- this._musicalTickData = null;
6157
- this._tickData = null;
6158
- }
6159
- }
6160
- render() {
6161
- const widthX = this.scaleMode === "beats" ? this.totalWidth : this._tickData?.widthX ?? 0;
6162
- if (widthX <= 0) return import_lit15.html``;
6163
- const totalChunks = Math.ceil(widthX / MAX_CANVAS_WIDTH4);
6164
- const indices = Array.from({ length: totalChunks }, (_, i) => i);
6165
- const dpr = typeof devicePixelRatio !== "undefined" ? devicePixelRatio : 1;
6166
- const beatsLabels = this.scaleMode === "beats" ? this._musicalTickData?.ticks.filter((t) => t.label) ?? [] : [];
6167
- const temporalLabels = this.scaleMode !== "beats" ? this._tickData?.labels ?? [] : [];
6168
- return import_lit15.html`
6169
- <div class="container" style="width: ${widthX}px; height: ${this.rulerHeight}px;">
6170
- ${indices.map((i) => {
6171
- const width = Math.min(MAX_CANVAS_WIDTH4, widthX - i * MAX_CANVAS_WIDTH4);
6172
- return import_lit15.html`
6173
- <canvas
6174
- data-index=${i}
6175
- width=${width * dpr}
6176
- height=${this.rulerHeight * dpr}
6177
- style="left: ${i * MAX_CANVAS_WIDTH4}px; width: ${width}px; height: ${this.rulerHeight}px;"
6178
- ></canvas>
6179
- `;
6180
- })}
6181
- ${this.scaleMode === "beats" ? beatsLabels.map(
6182
- (t) => import_lit15.html`<span
6183
- class="label ${t.pixel > 0 ? "centered" : ""}"
6184
- style="left: ${t.pixel > 0 ? t.pixel : t.pixel + 4}px;"
6185
- >${t.label}</span
6186
- >`
6187
- ) : temporalLabels.map(
6188
- ({ pix, text }) => import_lit15.html`<span class="label" style="left: ${pix + 4}px;">${text}</span>`
6189
- )}
6190
- </div>
6191
- `;
6192
- }
6193
- updated() {
6194
- this._drawTicks();
6195
- }
6196
- _drawTicks() {
6197
- const canvases = this.shadowRoot?.querySelectorAll("canvas");
6198
- if (!canvases) return;
6199
- const dpr = typeof devicePixelRatio !== "undefined" ? devicePixelRatio : 1;
6200
- const rulerColor = getComputedStyle(this).getPropertyValue("--daw-ruler-color").trim() || "#c49a6c";
6201
- const widthX = this.scaleMode === "beats" ? this.totalWidth : this._tickData?.widthX ?? 0;
6202
- for (const canvas of canvases) {
6203
- const idx = Number(canvas.dataset.index);
6204
- const ctx = canvas.getContext("2d");
6205
- if (!ctx) continue;
6206
- const canvasWidth = Math.min(MAX_CANVAS_WIDTH4, widthX - idx * MAX_CANVAS_WIDTH4);
6207
- const globalOffset = idx * MAX_CANVAS_WIDTH4;
6208
- ctx.resetTransform();
6209
- ctx.clearRect(0, 0, canvas.width, canvas.height);
6210
- ctx.scale(dpr, dpr);
6211
- ctx.strokeStyle = rulerColor;
6212
- ctx.lineWidth = 1;
6213
- if (this.scaleMode === "beats" && this._musicalTickData) {
6214
- const h = this.rulerHeight;
6215
- for (const tick of this._musicalTickData.ticks) {
6216
- const localX = tick.pixel - globalOffset;
6217
- if (localX < 0 || localX >= canvasWidth) continue;
6218
- const tickH = tick.type === "major" ? h * 0.6 : tick.type === "minor" ? h * 0.35 : h * 0.15;
6219
- ctx.globalAlpha = tick.type === "major" ? 1 : 0.5;
6220
- ctx.beginPath();
6221
- ctx.moveTo(localX + 0.5, h);
6222
- ctx.lineTo(localX + 0.5, h - tickH);
6223
- ctx.stroke();
6224
- }
6225
- ctx.globalAlpha = 1;
6226
- } else if (this._tickData) {
6227
- for (const [pix, height] of this._tickData.canvasInfo) {
6228
- const localX = pix - globalOffset;
6229
- if (localX < 0 || localX >= canvasWidth) continue;
6230
- ctx.beginPath();
6231
- ctx.moveTo(localX + 0.5, this.rulerHeight);
6232
- ctx.lineTo(localX + 0.5, this.rulerHeight - height);
6233
- ctx.stroke();
6234
- }
6235
- }
6236
- }
6237
- }
6238
- };
6239
- DawRulerElement.styles = import_lit15.css`
6240
- :host {
6241
- display: block;
6242
- position: relative;
6243
- background: var(--daw-ruler-background, #0f0f1a);
6244
- }
6245
- .container {
6246
- position: relative;
6247
- }
6248
- canvas {
6249
- position: absolute;
6250
- top: 0;
6251
- }
6252
- .label {
6253
- position: absolute;
6254
- font-size: 0.7rem;
6255
- line-height: 1;
6256
- white-space: nowrap;
6257
- color: var(--daw-ruler-color, #c49a6c);
6258
- top: 1px;
6259
- }
6260
- .label.centered {
6261
- transform: translateX(-50%);
6262
- }
6263
- `;
6264
- __decorateClass([
6265
- (0, import_decorators13.property)({ type: Number, attribute: false })
6266
- ], DawRulerElement.prototype, "samplesPerPixel", 2);
6267
- __decorateClass([
6268
- (0, import_decorators13.property)({ type: Number, attribute: false })
6269
- ], DawRulerElement.prototype, "sampleRate", 2);
6270
- __decorateClass([
6271
- (0, import_decorators13.property)({ type: Number, attribute: false })
6272
- ], DawRulerElement.prototype, "duration", 2);
6273
- __decorateClass([
6274
- (0, import_decorators13.property)({ type: Number, attribute: false })
6275
- ], DawRulerElement.prototype, "rulerHeight", 2);
6276
- __decorateClass([
6277
- (0, import_decorators13.property)({ type: String, attribute: false })
6278
- ], DawRulerElement.prototype, "scaleMode", 2);
6279
- __decorateClass([
6280
- (0, import_decorators13.property)({ type: Number, attribute: false })
6281
- ], DawRulerElement.prototype, "ticksPerPixel", 2);
6282
- __decorateClass([
6283
- (0, import_decorators13.property)({ attribute: false })
6284
- ], DawRulerElement.prototype, "meterEntries", 2);
6285
- __decorateClass([
6286
- (0, import_decorators13.property)({ type: Number, attribute: false })
6287
- ], DawRulerElement.prototype, "ppqn", 2);
6288
- __decorateClass([
6289
- (0, import_decorators13.property)({ type: Number, attribute: false })
6290
- ], DawRulerElement.prototype, "totalWidth", 2);
6291
- DawRulerElement = __decorateClass([
6292
- (0, import_decorators13.customElement)("daw-ruler")
6293
- ], DawRulerElement);
6294
-
6295
6568
  // src/elements/daw-selection.ts
6296
6569
  var import_lit16 = require("lit");
6297
6570
  var import_decorators14 = require("lit/decorators.js");
@@ -6415,7 +6688,7 @@ DawRecordButtonElement = __decorateClass([
6415
6688
  // src/elements/daw-keyboard-shortcuts.ts
6416
6689
  var import_lit18 = require("lit");
6417
6690
  var import_decorators16 = require("lit/decorators.js");
6418
- var import_core9 = require("@waveform-playlist/core");
6691
+ var import_core10 = require("@waveform-playlist/core");
6419
6692
  var DawKeyboardShortcutsElement = class extends import_lit18.LitElement {
6420
6693
  constructor() {
6421
6694
  super(...arguments);
@@ -6435,7 +6708,7 @@ var DawKeyboardShortcutsElement = class extends import_lit18.LitElement {
6435
6708
  const shortcuts = this.shortcuts;
6436
6709
  if (shortcuts.length === 0) return;
6437
6710
  try {
6438
- (0, import_core9.handleKeyboardEvent)(e, shortcuts, true);
6711
+ (0, import_core10.handleKeyboardEvent)(e, shortcuts, true);
6439
6712
  } catch (err) {
6440
6713
  console.warn("[dawcore] Keyboard shortcut failed (key=" + e.key + "): " + String(err));
6441
6714
  const target = this._editor ?? this;
@@ -6602,6 +6875,7 @@ var DawSpectrogramElement = class extends import_lit19.LitElement {
6602
6875
  this.originX = 0;
6603
6876
  this._canvases = [];
6604
6877
  this._registeredCanvasIds = [];
6878
+ this._warnedNoHost = false;
6605
6879
  }
6606
6880
  get samplesPerPixel() {
6607
6881
  return this._samplesPerPixel;
@@ -6668,7 +6942,15 @@ var DawSpectrogramElement = class extends import_lit19.LitElement {
6668
6942
  }
6669
6943
  _registerCanvases() {
6670
6944
  const editor = this._findHostEditor();
6671
- if (!editor || typeof editor._spectrogramRegisterCanvas !== "function") return;
6945
+ if (!editor || typeof editor._spectrogramRegisterCanvas !== "function") {
6946
+ if (!this._warnedNoHost) {
6947
+ this._warnedNoHost = true;
6948
+ console.warn(
6949
+ "[dawcore] <daw-spectrogram> (clip " + this.clipId + ") could not find host <daw-editor>. Canvases will not render. Ensure the element is mounted inside a <daw-editor>."
6950
+ );
6951
+ }
6952
+ return;
6953
+ }
6672
6954
  for (let i = 0; i < this._canvases.length; i++) {
6673
6955
  const canvas = this._canvases[i];
6674
6956
  const canvasId = this.clipId + "-ch" + this.channelIndex + "-chunk" + i;