@waveform-playlist/ui-components 5.0.0-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs ADDED
@@ -0,0 +1,2320 @@
1
+ // src/components/AudioPosition.tsx
2
+ import styled from "styled-components";
3
+ import { jsx } from "react/jsx-runtime";
4
+ var PositionDisplay = styled.span`
5
+ font-family: 'Courier New', Monaco, monospace;
6
+ font-size: 1rem;
7
+ font-weight: 600;
8
+ color: ${(props) => props.theme?.textColor || "#333"};
9
+ user-select: none;
10
+ `;
11
+ var AudioPosition = ({
12
+ formattedTime,
13
+ className
14
+ }) => {
15
+ return /* @__PURE__ */ jsx(PositionDisplay, { className, "aria-label": "Audio position", children: formattedTime });
16
+ };
17
+
18
+ // src/styled/BaseButton.tsx
19
+ import styled2 from "styled-components";
20
+ var BaseButton = styled2.button`
21
+ display: inline-flex;
22
+ align-items: center;
23
+ justify-content: center;
24
+ padding: 0.5rem 1rem;
25
+ font-family: ${(props) => props.theme.fontFamily};
26
+ font-size: ${(props) => props.theme.fontSize};
27
+ font-weight: 500;
28
+ color: ${(props) => props.theme.buttonText};
29
+ background-color: ${(props) => props.theme.buttonBackground};
30
+ border: 1px solid ${(props) => props.theme.buttonBorder};
31
+ border-radius: ${(props) => props.theme.borderRadius};
32
+ cursor: pointer;
33
+ outline: none;
34
+ transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out,
35
+ box-shadow 0.15s ease-in-out;
36
+
37
+ &:hover:not(:disabled) {
38
+ background-color: ${(props) => props.theme.buttonHoverBackground};
39
+ }
40
+
41
+ &:focus {
42
+ box-shadow: 0 0 0 2px ${(props) => props.theme.inputFocusBorder}33;
43
+ }
44
+
45
+ &:disabled {
46
+ opacity: 0.6;
47
+ cursor: not-allowed;
48
+ }
49
+ `;
50
+ var BaseButtonSmall = styled2(BaseButton)`
51
+ padding: 0.25rem 0.5rem;
52
+ font-size: ${(props) => props.theme.fontSizeSmall};
53
+ `;
54
+ var IconButton = styled2(BaseButton)`
55
+ padding: 0.5rem;
56
+ min-width: 2.25rem;
57
+ min-height: 2.25rem;
58
+ `;
59
+ var IconButtonSmall = styled2(BaseButton)`
60
+ padding: 0.25rem;
61
+ min-width: 1.75rem;
62
+ min-height: 1.75rem;
63
+ font-size: ${(props) => props.theme.fontSizeSmall};
64
+ `;
65
+
66
+ // src/styled/BaseCheckbox.tsx
67
+ import styled3 from "styled-components";
68
+ var BaseCheckboxWrapper = styled3.div`
69
+ display: inline-flex;
70
+ align-items: center;
71
+ gap: 0.5rem;
72
+ `;
73
+ var BaseCheckbox = styled3.input`
74
+ cursor: pointer;
75
+ accent-color: ${(props) => props.theme.inputFocusBorder};
76
+
77
+ &:disabled {
78
+ cursor: not-allowed;
79
+ }
80
+ `;
81
+ var BaseCheckboxLabel = styled3.label`
82
+ margin: 0;
83
+ cursor: pointer;
84
+ user-select: none;
85
+ font-family: ${(props) => props.theme.fontFamily};
86
+ font-size: ${(props) => props.theme.fontSize};
87
+ color: ${(props) => props.theme.textColor};
88
+ `;
89
+
90
+ // src/styled/BaseControlButton.tsx
91
+ import styled4 from "styled-components";
92
+ var BaseControlButton = styled4.button`
93
+ padding: 0.5rem 1rem;
94
+ background: ${(props) => props.theme.buttonBackground || "#007bff"};
95
+ color: ${(props) => props.theme.buttonText || "white"};
96
+ border: none;
97
+ border-radius: ${(props) => props.theme.borderRadius};
98
+ cursor: pointer;
99
+ font-family: ${(props) => props.theme.fontFamily};
100
+ font-size: ${(props) => props.theme.fontSize};
101
+ font-weight: 500;
102
+ transition: background-color 0.15s ease-in-out;
103
+
104
+ &:hover:not(:disabled) {
105
+ background: ${(props) => props.theme.buttonHoverBackground || "#0056b3"};
106
+ }
107
+
108
+ &:focus {
109
+ outline: none;
110
+ box-shadow: 0 0 0 2px ${(props) => props.theme.buttonBackground || "#007bff"}66;
111
+ }
112
+
113
+ &:disabled {
114
+ background: #6c757d;
115
+ cursor: not-allowed;
116
+ opacity: 0.6;
117
+ }
118
+ `;
119
+
120
+ // src/styled/BaseInput.tsx
121
+ import styled5 from "styled-components";
122
+ var BaseInput = styled5.input`
123
+ padding: 0.5rem 0.75rem;
124
+ font-family: ${(props) => props.theme.fontFamily};
125
+ font-size: ${(props) => props.theme.fontSize};
126
+ color: ${(props) => props.theme.inputText};
127
+ background-color: ${(props) => props.theme.inputBackground};
128
+ border: 1px solid ${(props) => props.theme.inputBorder};
129
+ border-radius: ${(props) => props.theme.borderRadius};
130
+ outline: none;
131
+ transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
132
+
133
+ &::placeholder {
134
+ color: ${(props) => props.theme.inputPlaceholder};
135
+ }
136
+
137
+ &:focus {
138
+ border-color: ${(props) => props.theme.inputFocusBorder};
139
+ box-shadow: 0 0 0 2px ${(props) => props.theme.inputFocusBorder}33;
140
+ }
141
+
142
+ &:disabled {
143
+ opacity: 0.6;
144
+ cursor: not-allowed;
145
+ }
146
+ `;
147
+ var BaseInputSmall = styled5(BaseInput)`
148
+ padding: 0.25rem 0.5rem;
149
+ font-size: ${(props) => props.theme.fontSizeSmall};
150
+ `;
151
+
152
+ // src/styled/BaseLabel.tsx
153
+ import styled6 from "styled-components";
154
+ var BaseLabel = styled6.label`
155
+ font-family: ${(props) => props.theme.fontFamily};
156
+ font-size: ${(props) => props.theme.fontSizeSmall};
157
+ font-weight: 500;
158
+ color: ${(props) => props.theme.textColorMuted};
159
+ margin-bottom: 0.25rem;
160
+ display: block;
161
+ `;
162
+ var InlineLabel = styled6.label`
163
+ font-family: ${(props) => props.theme.fontFamily};
164
+ font-size: ${(props) => props.theme.fontSize};
165
+ color: ${(props) => props.theme.textColor};
166
+ display: inline-flex;
167
+ align-items: center;
168
+ gap: 0.5rem;
169
+ cursor: pointer;
170
+ `;
171
+ var ScreenReaderOnly = styled6.span`
172
+ position: absolute;
173
+ width: 1px;
174
+ height: 1px;
175
+ padding: 0;
176
+ margin: -1px;
177
+ overflow: hidden;
178
+ clip: rect(0, 0, 0, 0);
179
+ white-space: nowrap;
180
+ border: 0;
181
+ `;
182
+
183
+ // src/styled/BaseSelect.tsx
184
+ import styled7 from "styled-components";
185
+ var BaseSelect = styled7.select`
186
+ padding: 0.5rem 2rem 0.5rem 0.75rem;
187
+ font-family: ${(props) => props.theme.fontFamily};
188
+ font-size: ${(props) => props.theme.fontSize};
189
+ color: ${(props) => props.theme.inputText};
190
+ background-color: ${(props) => props.theme.inputBackground};
191
+ border: 1px solid ${(props) => props.theme.inputBorder};
192
+ border-radius: ${(props) => props.theme.borderRadius};
193
+ outline: none;
194
+ cursor: pointer;
195
+ appearance: none;
196
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%23666' d='M6 8L1 3h10z'/%3E%3C/svg%3E");
197
+ background-repeat: no-repeat;
198
+ background-position: right 0.75rem center;
199
+ transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
200
+
201
+ &:focus {
202
+ border-color: ${(props) => props.theme.inputFocusBorder};
203
+ box-shadow: 0 0 0 2px ${(props) => props.theme.inputFocusBorder}33;
204
+ }
205
+
206
+ &:disabled {
207
+ opacity: 0.6;
208
+ cursor: not-allowed;
209
+ }
210
+
211
+ /* Style native option elements for dark mode support */
212
+ option {
213
+ color: ${(props) => props.theme.inputText};
214
+ background-color: ${(props) => props.theme.inputBackground};
215
+ }
216
+ `;
217
+ var BaseSelectSmall = styled7(BaseSelect)`
218
+ padding: 0.25rem 1.75rem 0.25rem 0.5rem;
219
+ font-size: ${(props) => props.theme.fontSizeSmall};
220
+ `;
221
+
222
+ // src/styled/BaseSlider.tsx
223
+ import styled8 from "styled-components";
224
+ var BaseSlider = styled8.input.attrs({ type: "range" })`
225
+ -webkit-appearance: none;
226
+ appearance: none;
227
+ width: 100%;
228
+ height: 6px;
229
+ background: ${(props) => props.theme.sliderTrackColor};
230
+ border-radius: 3px;
231
+ cursor: pointer;
232
+ outline: none;
233
+
234
+ /* WebKit (Chrome, Safari) */
235
+ &::-webkit-slider-thumb {
236
+ -webkit-appearance: none;
237
+ appearance: none;
238
+ width: 16px;
239
+ height: 16px;
240
+ background: ${(props) => props.theme.sliderThumbColor};
241
+ border: 2px solid ${(props) => props.theme.inputBackground};
242
+ border-radius: 50%;
243
+ cursor: pointer;
244
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
245
+ transition: transform 0.15s ease, box-shadow 0.15s ease;
246
+ }
247
+
248
+ &::-webkit-slider-thumb:hover {
249
+ transform: scale(1.1);
250
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
251
+ }
252
+
253
+ /* Firefox */
254
+ &::-moz-range-thumb {
255
+ width: 16px;
256
+ height: 16px;
257
+ background: ${(props) => props.theme.sliderThumbColor};
258
+ border: 2px solid ${(props) => props.theme.inputBackground};
259
+ border-radius: 50%;
260
+ cursor: pointer;
261
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
262
+ transition: transform 0.15s ease, box-shadow 0.15s ease;
263
+ }
264
+
265
+ &::-moz-range-thumb:hover {
266
+ transform: scale(1.1);
267
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
268
+ }
269
+
270
+ &::-moz-range-track {
271
+ background: ${(props) => props.theme.sliderTrackColor};
272
+ border-radius: 3px;
273
+ height: 6px;
274
+ }
275
+
276
+ &:focus {
277
+ outline: none;
278
+ }
279
+
280
+ &:focus::-webkit-slider-thumb {
281
+ box-shadow: 0 0 0 3px ${(props) => props.theme.inputFocusBorder}33;
282
+ }
283
+
284
+ &:focus::-moz-range-thumb {
285
+ box-shadow: 0 0 0 3px ${(props) => props.theme.inputFocusBorder}33;
286
+ }
287
+
288
+ &:disabled {
289
+ cursor: not-allowed;
290
+ opacity: 0.5;
291
+ }
292
+
293
+ &:disabled::-webkit-slider-thumb {
294
+ cursor: not-allowed;
295
+ }
296
+
297
+ &:disabled::-moz-range-thumb {
298
+ cursor: not-allowed;
299
+ }
300
+ `;
301
+
302
+ // src/components/AutomaticScrollCheckbox.tsx
303
+ import { jsx as jsx2, jsxs } from "react/jsx-runtime";
304
+ var AutomaticScrollCheckbox = ({
305
+ checked,
306
+ onChange,
307
+ disabled = false,
308
+ className
309
+ }) => {
310
+ const handleChange = (e) => {
311
+ onChange(e.target.checked);
312
+ };
313
+ return /* @__PURE__ */ jsxs(BaseCheckboxWrapper, { className, children: [
314
+ /* @__PURE__ */ jsx2(
315
+ BaseCheckbox,
316
+ {
317
+ type: "checkbox",
318
+ id: "automatic-scroll",
319
+ className: "automatic-scroll",
320
+ checked,
321
+ onChange: handleChange,
322
+ disabled
323
+ }
324
+ ),
325
+ /* @__PURE__ */ jsx2(BaseCheckboxLabel, { htmlFor: "automatic-scroll", children: "Automatic Scroll" })
326
+ ] });
327
+ };
328
+
329
+ // src/components/Channel.tsx
330
+ import { useLayoutEffect, useCallback, useRef } from "react";
331
+ import styled9 from "styled-components";
332
+
333
+ // src/wfpl-theme.ts
334
+ function isWaveformGradient(color) {
335
+ return typeof color === "object" && color !== null && "type" in color;
336
+ }
337
+ function waveformColorToCss(color) {
338
+ if (!isWaveformGradient(color)) {
339
+ return color;
340
+ }
341
+ const direction = color.direction === "vertical" ? "to bottom" : "to right";
342
+ const stops = color.stops.map((stop) => `${stop.color} ${stop.offset * 100}%`).join(", ");
343
+ return `linear-gradient(${direction}, ${stops})`;
344
+ }
345
+ var defaultTheme = {
346
+ waveformDrawMode: "inverted",
347
+ waveOutlineColor: "#ffffff",
348
+ waveFillColor: "#1a7f8e",
349
+ // White background for crisp look
350
+ waveProgressColor: "rgba(0, 0, 0, 0.10)",
351
+ // Subtle dark overlay for light mode
352
+ selectedWaveOutlineColor: "#ffffff",
353
+ selectedWaveFillColor: "#00b4d8",
354
+ // Selected: brighter cyan
355
+ selectedTrackControlsBackground: "#d9e9ff",
356
+ // Light blue background for selected track controls
357
+ timeColor: "#000",
358
+ timescaleBackgroundColor: "#fff",
359
+ playheadColor: "#f00",
360
+ selectionColor: "rgba(255, 105, 180, 0.7)",
361
+ // hot pink - high contrast on light backgrounds
362
+ clipHeaderBackgroundColor: "rgba(0, 0, 0, 0.1)",
363
+ clipHeaderBorderColor: "rgba(0, 0, 0, 0.2)",
364
+ clipHeaderTextColor: "#333",
365
+ clipHeaderFontFamily: "inherit",
366
+ selectedClipHeaderBackgroundColor: "#b3d9ff",
367
+ // Brighter blue for selected track clip headers
368
+ // Fade overlay colors
369
+ fadeOverlayColor: "rgba(0, 0, 0, 0.4)",
370
+ // Semi-transparent overlay for fade regions
371
+ // UI component colors
372
+ backgroundColor: "#ffffff",
373
+ surfaceColor: "#f5f5f5",
374
+ borderColor: "#ddd",
375
+ textColor: "#333",
376
+ textColorMuted: "#666",
377
+ // Interactive element colors
378
+ inputBackground: "#ffffff",
379
+ inputBorder: "#ccc",
380
+ inputText: "#333",
381
+ inputPlaceholder: "#999",
382
+ inputFocusBorder: "#0066cc",
383
+ // Button colors - blue to match common UI patterns
384
+ buttonBackground: "#0091ff",
385
+ buttonText: "#ffffff",
386
+ buttonBorder: "#0081e6",
387
+ buttonHoverBackground: "#0081e6",
388
+ // Slider colors
389
+ sliderTrackColor: "#ddd",
390
+ sliderThumbColor: "#daa520",
391
+ // goldenrod
392
+ // Annotation colors
393
+ annotationBoxBackground: "rgba(255, 255, 255, 0.85)",
394
+ annotationBoxActiveBackground: "rgba(255, 255, 255, 0.95)",
395
+ annotationBoxHoverBackground: "rgba(255, 255, 255, 0.98)",
396
+ annotationBoxBorder: "#ff9800",
397
+ annotationBoxActiveBorder: "#d67600",
398
+ annotationLabelColor: "#2a2a2a",
399
+ annotationResizeHandleColor: "rgba(0, 0, 0, 0.4)",
400
+ annotationResizeHandleActiveColor: "rgba(0, 0, 0, 0.8)",
401
+ annotationTextItemHoverBackground: "rgba(0, 0, 0, 0.03)",
402
+ // Spacing and sizing
403
+ borderRadius: "4px",
404
+ fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, sans-serif',
405
+ fontSize: "14px",
406
+ fontSizeSmall: "12px"
407
+ };
408
+ var darkTheme = {
409
+ // Normal mode: waveOutlineColor = bars, waveFillColor = background
410
+ waveformDrawMode: "inverted",
411
+ // Dark bars on warm amber background
412
+ waveOutlineColor: "#c49a6c",
413
+ // Solid warm amber for background
414
+ waveFillColor: "#1a1612",
415
+ // Very dark warm brown for bars
416
+ waveProgressColor: "rgba(100, 70, 40, 0.6)",
417
+ // Warm brown progress overlay
418
+ // Selected: slightly lighter bars on brighter amber background
419
+ selectedWaveFillColor: "#241c14",
420
+ // Slightly lighter warm brown bars when selected
421
+ selectedWaveOutlineColor: "#e8c090",
422
+ // Brighter amber background when selected
423
+ selectedTrackControlsBackground: "#2a2218",
424
+ // Dark warm brown for selected track controls
425
+ timeColor: "#d8c0a8",
426
+ // Warm amber for timescale text
427
+ timescaleBackgroundColor: "#1a1612",
428
+ // Dark warm brown background
429
+ playheadColor: "#3a8838",
430
+ // Darker Ampelmännchen green playhead
431
+ selectionColor: "rgba(224, 160, 100, 0.5)",
432
+ // Warm amber selection
433
+ clipHeaderBackgroundColor: "rgba(20, 16, 12, 0.85)",
434
+ // Dark background for clip headers
435
+ clipHeaderBorderColor: "rgba(200, 160, 120, 0.25)",
436
+ clipHeaderTextColor: "#d8c0a8",
437
+ // Warm amber text
438
+ clipHeaderFontFamily: "inherit",
439
+ selectedClipHeaderBackgroundColor: "#3a2c20",
440
+ // Darker warm brown for selected clip headers
441
+ // Fade overlay colors
442
+ fadeOverlayColor: "rgba(200, 100, 80, 0.5)",
443
+ // Warm red-orange overlay visible on dark backgrounds
444
+ // UI component colors
445
+ backgroundColor: "#1e1e1e",
446
+ surfaceColor: "#2d2d2d",
447
+ borderColor: "#444",
448
+ textColor: "#e0e0e0",
449
+ textColorMuted: "#999",
450
+ // Interactive element colors
451
+ inputBackground: "#2d2d2d",
452
+ inputBorder: "#555",
453
+ inputText: "#e0e0e0",
454
+ inputPlaceholder: "#777",
455
+ inputFocusBorder: "#4A9EFF",
456
+ // Button colors - Ampelmännchen green (#63C75F) with black text
457
+ buttonBackground: "#63C75F",
458
+ buttonText: "#0a0a0f",
459
+ buttonBorder: "#52b84e",
460
+ buttonHoverBackground: "#78d074",
461
+ // Slider colors
462
+ sliderTrackColor: "#555",
463
+ sliderThumbColor: "#f0c040",
464
+ // brighter goldenrod for dark mode
465
+ // Annotation colors (dark mode - warm amber theme)
466
+ annotationBoxBackground: "rgba(40, 32, 24, 0.9)",
467
+ annotationBoxActiveBackground: "rgba(50, 40, 30, 0.95)",
468
+ annotationBoxHoverBackground: "rgba(60, 48, 36, 0.98)",
469
+ annotationBoxBorder: "#c49a6c",
470
+ annotationBoxActiveBorder: "#d4a87c",
471
+ annotationLabelColor: "#d8c0a8",
472
+ annotationResizeHandleColor: "rgba(200, 160, 120, 0.5)",
473
+ annotationResizeHandleActiveColor: "rgba(220, 180, 140, 0.8)",
474
+ annotationTextItemHoverBackground: "rgba(200, 160, 120, 0.08)",
475
+ // Spacing and sizing
476
+ borderRadius: "4px",
477
+ fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, sans-serif',
478
+ fontSize: "14px",
479
+ fontSizeSmall: "12px"
480
+ };
481
+
482
+ // src/components/Channel.tsx
483
+ import { jsx as jsx3 } from "react/jsx-runtime";
484
+ var MAX_CANVAS_WIDTH = 1e3;
485
+ function createCanvasFillStyle(ctx, color, width, height) {
486
+ if (!isWaveformGradient(color)) {
487
+ return color;
488
+ }
489
+ let gradient;
490
+ if (color.direction === "vertical") {
491
+ gradient = ctx.createLinearGradient(0, 0, 0, height);
492
+ } else {
493
+ gradient = ctx.createLinearGradient(0, 0, width, 0);
494
+ }
495
+ for (const stop of color.stops) {
496
+ gradient.addColorStop(stop.offset, stop.color);
497
+ }
498
+ return gradient;
499
+ }
500
+ var Waveform = styled9.canvas.attrs((props) => ({
501
+ style: {
502
+ width: `${props.$cssWidth}px`,
503
+ height: `${props.$waveHeight}px`
504
+ }
505
+ }))`
506
+ float: left;
507
+ position: relative;
508
+ /* Promote to own compositing layer for smoother scrolling */
509
+ will-change: transform;
510
+ /* Disable image rendering interpolation */
511
+ image-rendering: pixelated;
512
+ image-rendering: crisp-edges;
513
+ `;
514
+ var Wrapper = styled9.div.attrs((props) => ({
515
+ style: {
516
+ top: `${props.$waveHeight * props.$index}px`,
517
+ width: `${props.$cssWidth}px`,
518
+ height: `${props.$waveHeight}px`
519
+ }
520
+ }))`
521
+ position: absolute;
522
+ background: ${(props) => props.$waveFillColor};
523
+ /* Force GPU compositing layer to reduce scroll flickering */
524
+ transform: translateZ(0);
525
+ backface-visibility: hidden;
526
+ `;
527
+ var Channel = (props) => {
528
+ const {
529
+ data,
530
+ bits,
531
+ length,
532
+ index,
533
+ className,
534
+ devicePixelRatio = 1,
535
+ waveHeight = 80,
536
+ waveOutlineColor = "#E0EFF1",
537
+ waveFillColor = "grey",
538
+ barWidth = 1,
539
+ barGap = 0,
540
+ transparentBackground = false,
541
+ drawMode = "inverted"
542
+ } = props;
543
+ const canvasesRef = useRef([]);
544
+ const canvasRef = useCallback(
545
+ (canvas) => {
546
+ if (canvas !== null) {
547
+ const index2 = parseInt(canvas.dataset.index, 10);
548
+ canvasesRef.current[index2] = canvas;
549
+ }
550
+ },
551
+ []
552
+ );
553
+ useLayoutEffect(() => {
554
+ const canvases = canvasesRef.current;
555
+ const step = barWidth + barGap;
556
+ let globalPixelOffset = 0;
557
+ for (let i = 0; i < canvases.length; i++) {
558
+ const canvas = canvases[i];
559
+ const ctx = canvas.getContext("2d");
560
+ const h2 = Math.floor(waveHeight / 2);
561
+ const maxValue = 2 ** (bits - 1);
562
+ if (ctx) {
563
+ ctx.resetTransform();
564
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
565
+ ctx.imageSmoothingEnabled = false;
566
+ ctx.scale(devicePixelRatio, devicePixelRatio);
567
+ const canvasWidth = canvas.width / devicePixelRatio;
568
+ let fillColor;
569
+ if (drawMode === "normal") {
570
+ fillColor = waveFillColor;
571
+ } else {
572
+ fillColor = waveOutlineColor;
573
+ }
574
+ ctx.fillStyle = createCanvasFillStyle(
575
+ ctx,
576
+ fillColor,
577
+ canvasWidth,
578
+ waveHeight
579
+ );
580
+ const canvasStartGlobal = globalPixelOffset;
581
+ const canvasEndGlobal = globalPixelOffset + canvasWidth;
582
+ const firstBarGlobal = Math.floor((canvasStartGlobal - barWidth + step) / step) * step;
583
+ for (let barGlobal = Math.max(0, firstBarGlobal); barGlobal < canvasEndGlobal; barGlobal += step) {
584
+ const x = barGlobal - canvasStartGlobal;
585
+ if (x + barWidth <= 0) continue;
586
+ const peakIndex = barGlobal;
587
+ if (peakIndex * 2 + 1 < data.length) {
588
+ const minPeak = data[peakIndex * 2] / maxValue;
589
+ const maxPeak = data[peakIndex * 2 + 1] / maxValue;
590
+ const min = Math.abs(minPeak * h2);
591
+ const max = Math.abs(maxPeak * h2);
592
+ if (drawMode === "normal") {
593
+ ctx.fillRect(x, h2 - max, barWidth, max + min);
594
+ } else {
595
+ ctx.fillRect(x, 0, barWidth, h2 - max);
596
+ ctx.fillRect(x, h2 + min, barWidth, h2 - min);
597
+ }
598
+ }
599
+ }
600
+ }
601
+ globalPixelOffset += canvas.width / devicePixelRatio;
602
+ }
603
+ }, [
604
+ data,
605
+ bits,
606
+ waveHeight,
607
+ waveOutlineColor,
608
+ waveFillColor,
609
+ devicePixelRatio,
610
+ length,
611
+ barWidth,
612
+ barGap,
613
+ drawMode
614
+ ]);
615
+ let totalWidth = length;
616
+ let waveformCount = 0;
617
+ const waveforms = [];
618
+ while (totalWidth > 0) {
619
+ const currentWidth = Math.min(totalWidth, MAX_CANVAS_WIDTH);
620
+ const waveform = /* @__PURE__ */ jsx3(
621
+ Waveform,
622
+ {
623
+ $cssWidth: currentWidth,
624
+ width: currentWidth * devicePixelRatio,
625
+ height: waveHeight * devicePixelRatio,
626
+ $waveHeight: waveHeight,
627
+ "data-index": waveformCount,
628
+ ref: canvasRef
629
+ },
630
+ `${length}-${waveformCount}`
631
+ );
632
+ waveforms.push(waveform);
633
+ totalWidth -= currentWidth;
634
+ waveformCount += 1;
635
+ }
636
+ const bgColor = waveFillColor;
637
+ const backgroundCss = transparentBackground ? "transparent" : waveformColorToCss(bgColor);
638
+ return /* @__PURE__ */ jsx3(
639
+ Wrapper,
640
+ {
641
+ $index: index,
642
+ $cssWidth: length,
643
+ className,
644
+ $waveHeight: waveHeight,
645
+ $waveFillColor: backgroundCss,
646
+ children: waveforms
647
+ }
648
+ );
649
+ };
650
+
651
+ // src/components/Clip.tsx
652
+ import styled13 from "styled-components";
653
+ import { useDraggable } from "@dnd-kit/core";
654
+ import { CSS } from "@dnd-kit/utilities";
655
+
656
+ // src/components/ClipHeader.tsx
657
+ import styled10 from "styled-components";
658
+ import { jsx as jsx4 } from "react/jsx-runtime";
659
+ var CLIP_HEADER_HEIGHT = 22;
660
+ var HeaderContainer = styled10.div`
661
+ position: relative;
662
+ height: ${CLIP_HEADER_HEIGHT}px;
663
+ background: ${(props) => props.$isSelected ? props.theme.selectedClipHeaderBackgroundColor : props.theme.clipHeaderBackgroundColor};
664
+ border-bottom: 1px solid ${(props) => props.theme.clipHeaderBorderColor};
665
+ display: flex;
666
+ align-items: center;
667
+ padding: 0 8px;
668
+ cursor: ${(props) => props.$interactive ? props.$isDragging ? "grabbing" : "grab" : "default"};
669
+ user-select: none;
670
+ z-index: 110;
671
+ flex-shrink: 0;
672
+ pointer-events: auto; /* Re-enable pointer events (parent ClipContainer has pointer-events: none) */
673
+
674
+ ${(props) => props.$interactive && `
675
+ &:hover {
676
+ background: ${props.theme.clipHeaderBackgroundColor}dd;
677
+ }
678
+
679
+ &:active {
680
+ cursor: grabbing;
681
+ }
682
+ `}
683
+ `;
684
+ var TrackName = styled10.span`
685
+ font-size: 11px;
686
+ font-weight: 600;
687
+ font-family: ${(props) => props.theme.clipHeaderFontFamily};
688
+ color: ${(props) => props.theme.clipHeaderTextColor};
689
+ white-space: nowrap;
690
+ overflow: hidden;
691
+ text-overflow: ellipsis;
692
+ `;
693
+ var ClipHeaderPresentational = ({
694
+ trackName,
695
+ isSelected = false
696
+ }) => {
697
+ return /* @__PURE__ */ jsx4(
698
+ HeaderContainer,
699
+ {
700
+ $isDragging: false,
701
+ $interactive: false,
702
+ $isSelected: isSelected,
703
+ children: /* @__PURE__ */ jsx4(TrackName, { children: trackName })
704
+ }
705
+ );
706
+ };
707
+ var ClipHeader = ({
708
+ clipId,
709
+ trackIndex,
710
+ clipIndex,
711
+ trackName,
712
+ isSelected = false,
713
+ disableDrag = false,
714
+ dragHandleProps
715
+ }) => {
716
+ if (disableDrag || !dragHandleProps) {
717
+ return /* @__PURE__ */ jsx4(
718
+ ClipHeaderPresentational,
719
+ {
720
+ trackName,
721
+ isSelected
722
+ }
723
+ );
724
+ }
725
+ const { attributes, listeners, setActivatorNodeRef } = dragHandleProps;
726
+ return /* @__PURE__ */ jsx4(
727
+ HeaderContainer,
728
+ {
729
+ ref: setActivatorNodeRef,
730
+ "data-clip-id": clipId,
731
+ $interactive: true,
732
+ $isSelected: isSelected,
733
+ ...listeners,
734
+ ...attributes,
735
+ children: /* @__PURE__ */ jsx4(TrackName, { children: trackName })
736
+ }
737
+ );
738
+ };
739
+
740
+ // src/components/ClipBoundary.tsx
741
+ import React2 from "react";
742
+ import styled11 from "styled-components";
743
+ import { jsx as jsx5 } from "react/jsx-runtime";
744
+ var CLIP_BOUNDARY_WIDTH = 8;
745
+ var BoundaryContainer = styled11.div`
746
+ position: absolute;
747
+ ${(props) => props.$edge === "left" ? "left: 0;" : "right: 0;"}
748
+ top: 0;
749
+ bottom: 0;
750
+ width: ${CLIP_BOUNDARY_WIDTH}px;
751
+ cursor: col-resize;
752
+ user-select: none;
753
+ z-index: 105; /* Above waveform, below header */
754
+ pointer-events: auto; /* Re-enable pointer events (parent ClipContainer has pointer-events: none) */
755
+
756
+ /* Invisible by default, visible on hover */
757
+ background: ${(props) => props.$isDragging ? "rgba(255, 255, 255, 0.4)" : props.$isHovered ? "rgba(255, 255, 255, 0.2)" : "transparent"};
758
+
759
+ ${(props) => props.$edge === "left" ? `border-left: 2px solid ${props.$isDragging ? "rgba(255, 255, 255, 0.8)" : props.$isHovered ? "rgba(255, 255, 255, 0.5)" : "transparent"};` : `border-right: 2px solid ${props.$isDragging ? "rgba(255, 255, 255, 0.8)" : props.$isHovered ? "rgba(255, 255, 255, 0.5)" : "transparent"};`}
760
+
761
+ transition: background 0.15s ease, border-color 0.15s ease;
762
+
763
+ &:hover {
764
+ background: rgba(255, 255, 255, 0.2);
765
+ ${(props) => props.$edge === "left" ? "border-left: 2px solid rgba(255, 255, 255, 0.5);" : "border-right: 2px solid rgba(255, 255, 255, 0.5);"}
766
+ }
767
+
768
+ &:active {
769
+ background: rgba(255, 255, 255, 0.4);
770
+ ${(props) => props.$edge === "left" ? "border-left: 2px solid rgba(255, 255, 255, 0.8);" : "border-right: 2px solid rgba(255, 255, 255, 0.8);"}
771
+ }
772
+ `;
773
+ var ClipBoundary = ({
774
+ clipId,
775
+ trackIndex,
776
+ clipIndex,
777
+ edge,
778
+ dragHandleProps
779
+ }) => {
780
+ const [isHovered, setIsHovered] = React2.useState(false);
781
+ if (!dragHandleProps) {
782
+ return null;
783
+ }
784
+ const { attributes, listeners, setActivatorNodeRef, isDragging } = dragHandleProps;
785
+ return /* @__PURE__ */ jsx5(
786
+ BoundaryContainer,
787
+ {
788
+ ref: setActivatorNodeRef,
789
+ "data-clip-id": clipId,
790
+ "data-boundary-edge": edge,
791
+ $edge: edge,
792
+ $isDragging: isDragging,
793
+ $isHovered: isHovered,
794
+ onMouseEnter: () => setIsHovered(true),
795
+ onMouseLeave: () => setIsHovered(false),
796
+ ...listeners,
797
+ ...attributes
798
+ }
799
+ );
800
+ };
801
+
802
+ // src/components/FadeOverlay.tsx
803
+ import styled12, { useTheme } from "styled-components";
804
+ import { jsx as jsx6 } from "react/jsx-runtime";
805
+ var FadeContainer = styled12.div.attrs((props) => ({
806
+ style: {
807
+ left: `${props.$left}px`,
808
+ width: `${props.$width}px`
809
+ }
810
+ }))`
811
+ position: absolute;
812
+ top: 0;
813
+ bottom: 0;
814
+ pointer-events: none;
815
+ z-index: 50;
816
+ `;
817
+ var FadeSvg = styled12.svg`
818
+ width: 100%;
819
+ height: 100%;
820
+ display: block;
821
+ /* Flip horizontally for fadeOut - makes it mirror of fadeIn */
822
+ transform: ${(props) => props.$type === "fadeOut" ? "scaleX(-1)" : "none"};
823
+ `;
824
+ function generateFadePath(width, height, curveType = "logarithmic") {
825
+ const points = [];
826
+ const numPoints = Math.max(20, Math.min(width, 100));
827
+ for (let i = 0; i <= numPoints; i++) {
828
+ const x = i / numPoints * width;
829
+ const progress = i / numPoints;
830
+ let curvedProgress;
831
+ switch (curveType) {
832
+ case "linear":
833
+ curvedProgress = progress;
834
+ break;
835
+ case "exponential":
836
+ curvedProgress = progress * progress;
837
+ break;
838
+ case "sCurve":
839
+ curvedProgress = (1 - Math.cos(progress * Math.PI)) / 2;
840
+ break;
841
+ case "logarithmic":
842
+ default:
843
+ curvedProgress = Math.log10(1 + progress * 9) / Math.log10(10);
844
+ break;
845
+ }
846
+ const y = (1 - curvedProgress) * height;
847
+ points.push(`${x},${y}`);
848
+ }
849
+ return `M 0,${height} L ${points.join(" L ")} L ${width},0 L 0,0 Z`;
850
+ }
851
+ var FadeOverlay = ({
852
+ left,
853
+ width,
854
+ type,
855
+ curveType = "logarithmic",
856
+ color
857
+ }) => {
858
+ const theme = useTheme();
859
+ if (width < 1) return null;
860
+ const fillColor = color || theme?.fadeOverlayColor || "rgba(0, 0, 0, 0.4)";
861
+ return /* @__PURE__ */ jsx6(FadeContainer, { $left: left, $width: width, $type: type, children: /* @__PURE__ */ jsx6(FadeSvg, { $type: type, viewBox: `0 0 ${width} 100`, preserveAspectRatio: "none", children: /* @__PURE__ */ jsx6(
862
+ "path",
863
+ {
864
+ d: generateFadePath(width, 100, curveType),
865
+ fill: fillColor
866
+ }
867
+ ) }) });
868
+ };
869
+
870
+ // src/components/Clip.tsx
871
+ import { Fragment, jsx as jsx7, jsxs as jsxs2 } from "react/jsx-runtime";
872
+ var ClipContainer = styled13.div.attrs((props) => ({
873
+ style: props.$isOverlay ? {} : {
874
+ left: `${props.$left}px`,
875
+ width: `${props.$width}px`
876
+ }
877
+ }))`
878
+ position: ${(props) => props.$isOverlay ? "relative" : "absolute"};
879
+ top: 0;
880
+ height: ${(props) => props.$isOverlay ? "auto" : "100%"};
881
+ width: ${(props) => props.$isOverlay ? `${props.$width}px` : "auto"};
882
+ display: flex;
883
+ flex-direction: column;
884
+ background: rgba(255, 255, 255, 0.05);
885
+ z-index: 10; /* Above progress overlay (z-index: 2) but below controls/playhead */
886
+ pointer-events: none; /* Let clicks pass through to ClickOverlay for playhead positioning */
887
+
888
+ &:hover {
889
+ background: rgba(255, 255, 255, 0.08);
890
+ }
891
+ `;
892
+ var ChannelsWrapper = styled13.div`
893
+ flex: 1;
894
+ position: relative;
895
+ overflow: ${(props) => props.$isOverlay ? "visible" : "hidden"};
896
+ `;
897
+ var Clip = ({
898
+ children,
899
+ className,
900
+ clipId,
901
+ trackIndex,
902
+ clipIndex,
903
+ trackName,
904
+ startSample,
905
+ durationSamples,
906
+ samplesPerPixel,
907
+ showHeader = false,
908
+ disableHeaderDrag = false,
909
+ isOverlay = false,
910
+ isSelected = false,
911
+ onMouseDown,
912
+ trackId,
913
+ fadeIn,
914
+ fadeOut,
915
+ sampleRate = 44100,
916
+ showFades = false
917
+ }) => {
918
+ const left = Math.floor(startSample / samplesPerPixel);
919
+ const endPixel = Math.floor((startSample + durationSamples) / samplesPerPixel);
920
+ const width = endPixel - left;
921
+ const enableDrag = showHeader && !disableHeaderDrag && !isOverlay;
922
+ const draggableId = `clip-${trackIndex}-${clipIndex}`;
923
+ const { attributes, listeners, setNodeRef, setActivatorNodeRef, transform, isDragging } = useDraggable({
924
+ id: draggableId,
925
+ data: { clipId, trackIndex, clipIndex },
926
+ disabled: !enableDrag
927
+ });
928
+ const leftBoundaryId = `clip-boundary-left-${trackIndex}-${clipIndex}`;
929
+ const {
930
+ attributes: leftBoundaryAttributes,
931
+ listeners: leftBoundaryListeners,
932
+ setActivatorNodeRef: setLeftBoundaryActivatorRef,
933
+ isDragging: isLeftBoundaryDragging
934
+ } = useDraggable({
935
+ id: leftBoundaryId,
936
+ data: { clipId, trackIndex, clipIndex, boundary: "left" },
937
+ disabled: !enableDrag
938
+ });
939
+ const rightBoundaryId = `clip-boundary-right-${trackIndex}-${clipIndex}`;
940
+ const {
941
+ attributes: rightBoundaryAttributes,
942
+ listeners: rightBoundaryListeners,
943
+ setActivatorNodeRef: setRightBoundaryActivatorRef,
944
+ isDragging: isRightBoundaryDragging
945
+ } = useDraggable({
946
+ id: rightBoundaryId,
947
+ data: { clipId, trackIndex, clipIndex, boundary: "right" },
948
+ disabled: !enableDrag
949
+ });
950
+ const style = transform ? {
951
+ transform: CSS.Translate.toString(transform),
952
+ zIndex: isDragging ? 100 : void 0
953
+ // Below controls (z-index: 999) but above other clips
954
+ } : void 0;
955
+ return /* @__PURE__ */ jsxs2(
956
+ ClipContainer,
957
+ {
958
+ ref: setNodeRef,
959
+ style,
960
+ className,
961
+ $left: left,
962
+ $width: width,
963
+ $isOverlay: isOverlay,
964
+ "data-clip-container": "true",
965
+ "data-track-id": trackId,
966
+ onMouseDown,
967
+ children: [
968
+ showHeader && /* @__PURE__ */ jsx7(
969
+ ClipHeader,
970
+ {
971
+ clipId,
972
+ trackIndex,
973
+ clipIndex,
974
+ trackName,
975
+ isSelected,
976
+ disableDrag: disableHeaderDrag,
977
+ dragHandleProps: enableDrag ? { attributes, listeners, setActivatorNodeRef } : void 0
978
+ }
979
+ ),
980
+ /* @__PURE__ */ jsxs2(ChannelsWrapper, { $isOverlay: isOverlay, children: [
981
+ children,
982
+ showFades && fadeIn && fadeIn.duration > 0 && /* @__PURE__ */ jsx7(
983
+ FadeOverlay,
984
+ {
985
+ left: 0,
986
+ width: Math.floor(fadeIn.duration * sampleRate / samplesPerPixel),
987
+ type: "fadeIn",
988
+ curveType: fadeIn.type
989
+ }
990
+ ),
991
+ showFades && fadeOut && fadeOut.duration > 0 && /* @__PURE__ */ jsx7(
992
+ FadeOverlay,
993
+ {
994
+ left: width - Math.floor(fadeOut.duration * sampleRate / samplesPerPixel),
995
+ width: Math.floor(fadeOut.duration * sampleRate / samplesPerPixel),
996
+ type: "fadeOut",
997
+ curveType: fadeOut.type
998
+ }
999
+ )
1000
+ ] }),
1001
+ showHeader && !disableHeaderDrag && !isOverlay && /* @__PURE__ */ jsxs2(Fragment, { children: [
1002
+ /* @__PURE__ */ jsx7(
1003
+ ClipBoundary,
1004
+ {
1005
+ clipId,
1006
+ trackIndex,
1007
+ clipIndex,
1008
+ edge: "left",
1009
+ dragHandleProps: {
1010
+ attributes: leftBoundaryAttributes,
1011
+ listeners: leftBoundaryListeners,
1012
+ setActivatorNodeRef: setLeftBoundaryActivatorRef,
1013
+ isDragging: isLeftBoundaryDragging
1014
+ }
1015
+ }
1016
+ ),
1017
+ /* @__PURE__ */ jsx7(
1018
+ ClipBoundary,
1019
+ {
1020
+ clipId,
1021
+ trackIndex,
1022
+ clipIndex,
1023
+ edge: "right",
1024
+ dragHandleProps: {
1025
+ attributes: rightBoundaryAttributes,
1026
+ listeners: rightBoundaryListeners,
1027
+ setActivatorNodeRef: setRightBoundaryActivatorRef,
1028
+ isDragging: isRightBoundaryDragging
1029
+ }
1030
+ }
1031
+ )
1032
+ ] })
1033
+ ]
1034
+ }
1035
+ );
1036
+ };
1037
+
1038
+ // src/components/MasterVolumeControl.tsx
1039
+ import styled14 from "styled-components";
1040
+ import { jsx as jsx8, jsxs as jsxs3 } from "react/jsx-runtime";
1041
+ var VolumeContainer = styled14.div`
1042
+ display: inline-flex;
1043
+ align-items: center;
1044
+ gap: 0.5rem;
1045
+ `;
1046
+ var VolumeLabel = styled14(BaseLabel)`
1047
+ margin: 0;
1048
+ white-space: nowrap;
1049
+ `;
1050
+ var VolumeSlider = styled14(BaseSlider)`
1051
+ width: 120px;
1052
+ `;
1053
+ var MasterVolumeControl = ({
1054
+ volume,
1055
+ onChange,
1056
+ disabled = false,
1057
+ className
1058
+ }) => {
1059
+ const handleChange = (e) => {
1060
+ onChange(parseFloat(e.target.value) / 100);
1061
+ };
1062
+ return /* @__PURE__ */ jsxs3(VolumeContainer, { className, children: [
1063
+ /* @__PURE__ */ jsx8(VolumeLabel, { htmlFor: "master-gain", children: "Master Volume" }),
1064
+ /* @__PURE__ */ jsx8(
1065
+ VolumeSlider,
1066
+ {
1067
+ min: "0",
1068
+ max: "100",
1069
+ value: volume * 100,
1070
+ onChange: handleChange,
1071
+ disabled,
1072
+ id: "master-gain"
1073
+ }
1074
+ )
1075
+ ] });
1076
+ };
1077
+
1078
+ // src/components/Playhead.tsx
1079
+ import { useRef as useRef2, useEffect } from "react";
1080
+ import styled15 from "styled-components";
1081
+ import { jsx as jsx9, jsxs as jsxs4 } from "react/jsx-runtime";
1082
+ var PlayheadLine = styled15.div.attrs((props) => ({
1083
+ style: {
1084
+ transform: `translate3d(${props.$position}px, 0, 0)`
1085
+ }
1086
+ }))`
1087
+ position: absolute;
1088
+ top: 0;
1089
+ left: 0;
1090
+ width: 2px;
1091
+ background: ${(props) => props.$color};
1092
+ height: 100%;
1093
+ z-index: 100; /* Below sticky controls (z-index: 101) so playhead is hidden when scrolled behind controls */
1094
+ pointer-events: none;
1095
+ will-change: transform;
1096
+ `;
1097
+ var Playhead = ({ position, color = "#ff0000" }) => {
1098
+ return /* @__PURE__ */ jsx9(PlayheadLine, { $position: position, $color: color });
1099
+ };
1100
+ var PlayheadWithMarkerContainer = styled15.div`
1101
+ position: absolute;
1102
+ top: 0;
1103
+ left: 0;
1104
+ height: 100%;
1105
+ z-index: 100; /* Below sticky controls (z-index: 101) so playhead is hidden when scrolled behind controls */
1106
+ pointer-events: none;
1107
+ will-change: transform;
1108
+ `;
1109
+ var MarkerTriangle = styled15.div`
1110
+ position: absolute;
1111
+ top: -10px;
1112
+ left: -6px;
1113
+ width: 0;
1114
+ height: 0;
1115
+ border-left: 7px solid transparent;
1116
+ border-right: 7px solid transparent;
1117
+ border-top: 10px solid ${(props) => props.$color};
1118
+ `;
1119
+ var MarkerLine = styled15.div`
1120
+ position: absolute;
1121
+ top: 0;
1122
+ left: 0;
1123
+ width: 2px;
1124
+ height: 100%;
1125
+ background: ${(props) => props.$color};
1126
+ `;
1127
+ var PlayheadWithMarker = ({
1128
+ color = "#ff0000",
1129
+ isPlaying,
1130
+ currentTimeRef,
1131
+ playbackStartTimeRef,
1132
+ audioStartPositionRef,
1133
+ samplesPerPixel,
1134
+ sampleRate,
1135
+ controlsOffset,
1136
+ getAudioContextTime
1137
+ }) => {
1138
+ const containerRef = useRef2(null);
1139
+ const animationFrameRef = useRef2(null);
1140
+ useEffect(() => {
1141
+ const updatePosition = () => {
1142
+ if (containerRef.current) {
1143
+ let time;
1144
+ if (isPlaying && getAudioContextTime) {
1145
+ const elapsed = getAudioContextTime() - (playbackStartTimeRef.current ?? 0);
1146
+ time = (audioStartPositionRef.current ?? 0) + elapsed;
1147
+ } else {
1148
+ time = currentTimeRef.current ?? 0;
1149
+ }
1150
+ const pos = time * sampleRate / samplesPerPixel + controlsOffset;
1151
+ containerRef.current.style.transform = `translate3d(${pos}px, 0, 0)`;
1152
+ }
1153
+ if (isPlaying) {
1154
+ animationFrameRef.current = requestAnimationFrame(updatePosition);
1155
+ }
1156
+ };
1157
+ if (isPlaying) {
1158
+ animationFrameRef.current = requestAnimationFrame(updatePosition);
1159
+ } else {
1160
+ updatePosition();
1161
+ }
1162
+ return () => {
1163
+ if (animationFrameRef.current) {
1164
+ cancelAnimationFrame(animationFrameRef.current);
1165
+ animationFrameRef.current = null;
1166
+ }
1167
+ };
1168
+ }, [isPlaying, sampleRate, samplesPerPixel, controlsOffset, currentTimeRef, playbackStartTimeRef, audioStartPositionRef, getAudioContextTime]);
1169
+ useEffect(() => {
1170
+ if (!isPlaying && containerRef.current) {
1171
+ const time = currentTimeRef.current ?? 0;
1172
+ const pos = time * sampleRate / samplesPerPixel + controlsOffset;
1173
+ containerRef.current.style.transform = `translate3d(${pos}px, 0, 0)`;
1174
+ }
1175
+ });
1176
+ return /* @__PURE__ */ jsxs4(PlayheadWithMarkerContainer, { ref: containerRef, $color: color, children: [
1177
+ /* @__PURE__ */ jsx9(MarkerTriangle, { $color: color }),
1178
+ /* @__PURE__ */ jsx9(MarkerLine, { $color: color })
1179
+ ] });
1180
+ };
1181
+
1182
+ // src/components/Playlist.tsx
1183
+ import styled16, { withTheme } from "styled-components";
1184
+ import { jsx as jsx10, jsxs as jsxs5 } from "react/jsx-runtime";
1185
+ var Wrapper2 = styled16.div`
1186
+ overflow-y: hidden;
1187
+ overflow-x: auto;
1188
+ position: relative;
1189
+ `;
1190
+ var ScrollContainer = styled16.div.attrs((props) => ({
1191
+ style: props.$width !== void 0 ? { width: `${props.$width}px` } : {}
1192
+ }))`
1193
+ position: relative;
1194
+ background: ${(props) => props.$backgroundColor || "transparent"};
1195
+ `;
1196
+ var TimescaleWrapper = styled16.div.attrs((props) => ({
1197
+ style: props.$width ? { minWidth: `${props.$width}px` } : {}
1198
+ }))`
1199
+ background: ${(props) => props.$backgroundColor || "white"};
1200
+ width: 100%;
1201
+ overflow: visible;
1202
+ `;
1203
+ var TracksContainer = styled16.div.attrs((props) => ({
1204
+ style: props.$width !== void 0 ? { minWidth: `${props.$width}px` } : {}
1205
+ }))`
1206
+ position: relative;
1207
+ background: ${(props) => props.$backgroundColor || "transparent"};
1208
+ width: 100%;
1209
+ `;
1210
+ var ClickOverlay = styled16.div`
1211
+ position: absolute;
1212
+ top: 0;
1213
+ left: 0;
1214
+ right: 0;
1215
+ bottom: 0;
1216
+ cursor: crosshair;
1217
+ z-index: 1; /* Low z-index - clip headers and boundaries have higher z-index */
1218
+ `;
1219
+ var Playlist = ({
1220
+ children,
1221
+ backgroundColor,
1222
+ timescaleBackgroundColor,
1223
+ timescale,
1224
+ timescaleWidth,
1225
+ tracksWidth,
1226
+ scrollContainerWidth,
1227
+ controlsWidth,
1228
+ onTracksClick,
1229
+ onTracksMouseDown,
1230
+ onTracksMouseMove,
1231
+ onTracksMouseUp,
1232
+ scrollContainerRef
1233
+ }) => {
1234
+ return /* @__PURE__ */ jsx10(Wrapper2, { "data-scroll-container": "true", ref: scrollContainerRef, children: /* @__PURE__ */ jsxs5(
1235
+ ScrollContainer,
1236
+ {
1237
+ $backgroundColor: backgroundColor,
1238
+ $width: scrollContainerWidth,
1239
+ children: [
1240
+ timescale && /* @__PURE__ */ jsx10(TimescaleWrapper, { $width: timescaleWidth, $backgroundColor: timescaleBackgroundColor, children: timescale }),
1241
+ /* @__PURE__ */ jsxs5(TracksContainer, { $width: tracksWidth, $backgroundColor: backgroundColor, children: [
1242
+ children,
1243
+ (onTracksClick || onTracksMouseDown) && /* @__PURE__ */ jsx10(
1244
+ ClickOverlay,
1245
+ {
1246
+ $controlsWidth: controlsWidth,
1247
+ onClick: onTracksClick,
1248
+ onMouseDown: onTracksMouseDown,
1249
+ onMouseMove: onTracksMouseMove,
1250
+ onMouseUp: onTracksMouseUp
1251
+ }
1252
+ )
1253
+ ] })
1254
+ ]
1255
+ }
1256
+ ) });
1257
+ };
1258
+ var StyledPlaylist = withTheme(Playlist);
1259
+
1260
+ // src/components/Selection.tsx
1261
+ import styled17 from "styled-components";
1262
+ import { jsx as jsx11 } from "react/jsx-runtime";
1263
+ var SelectionOverlay = styled17.div.attrs((props) => ({
1264
+ style: {
1265
+ left: `${props.$left}px`,
1266
+ width: `${props.$width}px`
1267
+ }
1268
+ }))`
1269
+ position: absolute;
1270
+ top: 0;
1271
+ background: ${(props) => props.$color};
1272
+ height: 100%;
1273
+ z-index: 5;
1274
+ pointer-events: none;
1275
+ opacity: 0.3;
1276
+ `;
1277
+ var Selection = ({
1278
+ startPosition,
1279
+ endPosition,
1280
+ color = "#00ff00"
1281
+ }) => {
1282
+ const width = Math.max(0, endPosition - startPosition);
1283
+ if (width <= 0) {
1284
+ return null;
1285
+ }
1286
+ return /* @__PURE__ */ jsx11(SelectionOverlay, { $left: startPosition, $width: width, $color: color });
1287
+ };
1288
+
1289
+ // src/components/SelectionTimeInputs.tsx
1290
+ import { useEffect as useEffect3, useState as useState2 } from "react";
1291
+
1292
+ // src/components/TimeInput.tsx
1293
+ import { useEffect as useEffect2, useState } from "react";
1294
+
1295
+ // src/utils/timeFormat.ts
1296
+ function clockFormat(seconds, decimals) {
1297
+ const hours = Math.floor(seconds / 3600) % 24;
1298
+ const minutes = Math.floor(seconds / 60) % 60;
1299
+ const secs = (seconds % 60).toFixed(decimals);
1300
+ return String(hours).padStart(2, "0") + ":" + String(minutes).padStart(2, "0") + ":" + secs.padStart(decimals + 3, "0");
1301
+ }
1302
+ function formatTime(seconds, format) {
1303
+ switch (format) {
1304
+ case "seconds":
1305
+ return seconds.toFixed(0);
1306
+ case "thousandths":
1307
+ return seconds.toFixed(3);
1308
+ case "hh:mm:ss":
1309
+ return clockFormat(seconds, 0);
1310
+ case "hh:mm:ss.u":
1311
+ return clockFormat(seconds, 1);
1312
+ case "hh:mm:ss.uu":
1313
+ return clockFormat(seconds, 2);
1314
+ case "hh:mm:ss.uuu":
1315
+ return clockFormat(seconds, 3);
1316
+ default:
1317
+ return clockFormat(seconds, 3);
1318
+ }
1319
+ }
1320
+ function parseTime(timeStr, format) {
1321
+ if (!timeStr) return 0;
1322
+ switch (format) {
1323
+ case "seconds":
1324
+ case "thousandths":
1325
+ return parseFloat(timeStr) || 0;
1326
+ case "hh:mm:ss":
1327
+ case "hh:mm:ss.u":
1328
+ case "hh:mm:ss.uu":
1329
+ case "hh:mm:ss.uuu": {
1330
+ const parts = timeStr.split(":");
1331
+ if (parts.length !== 3) return 0;
1332
+ const hours = parseInt(parts[0], 10) || 0;
1333
+ const minutes = parseInt(parts[1], 10) || 0;
1334
+ const seconds = parseFloat(parts[2]) || 0;
1335
+ return hours * 3600 + minutes * 60 + seconds;
1336
+ }
1337
+ default:
1338
+ return 0;
1339
+ }
1340
+ }
1341
+
1342
+ // src/components/TimeInput.tsx
1343
+ import { Fragment as Fragment2, jsx as jsx12, jsxs as jsxs6 } from "react/jsx-runtime";
1344
+ var TimeInput = ({
1345
+ id,
1346
+ label,
1347
+ value,
1348
+ format,
1349
+ className,
1350
+ onChange,
1351
+ readOnly = false
1352
+ }) => {
1353
+ const [displayValue, setDisplayValue] = useState("");
1354
+ useEffect2(() => {
1355
+ const formatted = formatTime(value, format);
1356
+ setDisplayValue(formatted);
1357
+ }, [value, format, id]);
1358
+ const handleChange = (e) => {
1359
+ const newDisplayValue = e.target.value;
1360
+ setDisplayValue(newDisplayValue);
1361
+ };
1362
+ const handleBlur = () => {
1363
+ if (onChange) {
1364
+ const parsedValue = parseTime(displayValue, format);
1365
+ onChange(parsedValue);
1366
+ }
1367
+ setDisplayValue(formatTime(value, format));
1368
+ };
1369
+ const handleKeyDown = (e) => {
1370
+ if (e.key === "Enter") {
1371
+ e.currentTarget.blur();
1372
+ }
1373
+ };
1374
+ return /* @__PURE__ */ jsxs6(Fragment2, { children: [
1375
+ /* @__PURE__ */ jsx12(ScreenReaderOnly, { as: "label", htmlFor: id, children: label }),
1376
+ /* @__PURE__ */ jsx12(
1377
+ BaseInput,
1378
+ {
1379
+ type: "text",
1380
+ className,
1381
+ id,
1382
+ value: displayValue,
1383
+ onChange: handleChange,
1384
+ onBlur: handleBlur,
1385
+ onKeyDown: handleKeyDown,
1386
+ readOnly
1387
+ }
1388
+ )
1389
+ ] });
1390
+ };
1391
+
1392
+ // src/components/SelectionTimeInputs.tsx
1393
+ import { Fragment as Fragment3, jsx as jsx13, jsxs as jsxs7 } from "react/jsx-runtime";
1394
+ var SelectionTimeInputs = ({
1395
+ selectionStart,
1396
+ selectionEnd,
1397
+ onSelectionChange,
1398
+ className
1399
+ }) => {
1400
+ const [timeFormat, setTimeFormat] = useState2("hh:mm:ss.uuu");
1401
+ useEffect3(() => {
1402
+ const timeFormatSelect = document.querySelector(".time-format");
1403
+ const handleFormatChange = () => {
1404
+ if (timeFormatSelect) {
1405
+ setTimeFormat(timeFormatSelect.value);
1406
+ }
1407
+ };
1408
+ if (timeFormatSelect) {
1409
+ setTimeFormat(timeFormatSelect.value);
1410
+ timeFormatSelect.addEventListener("change", handleFormatChange);
1411
+ }
1412
+ return () => {
1413
+ timeFormatSelect?.removeEventListener("change", handleFormatChange);
1414
+ };
1415
+ }, []);
1416
+ const handleStartChange = (value) => {
1417
+ if (onSelectionChange) {
1418
+ onSelectionChange(value, selectionEnd);
1419
+ }
1420
+ };
1421
+ const handleEndChange = (value) => {
1422
+ if (onSelectionChange) {
1423
+ onSelectionChange(selectionStart, value);
1424
+ }
1425
+ };
1426
+ return /* @__PURE__ */ jsxs7(Fragment3, { children: [
1427
+ /* @__PURE__ */ jsx13(
1428
+ TimeInput,
1429
+ {
1430
+ id: "audio_start",
1431
+ label: "Start of audio selection",
1432
+ value: selectionStart,
1433
+ format: timeFormat,
1434
+ className: "audio-start form-control mr-sm-2",
1435
+ onChange: handleStartChange
1436
+ }
1437
+ ),
1438
+ /* @__PURE__ */ jsx13(
1439
+ TimeInput,
1440
+ {
1441
+ id: "audio_end",
1442
+ label: "End of audio selection",
1443
+ value: selectionEnd,
1444
+ format: timeFormat,
1445
+ className: "audio-end form-control mr-sm-2",
1446
+ onChange: handleEndChange
1447
+ }
1448
+ )
1449
+ ] });
1450
+ };
1451
+
1452
+ // src/contexts/DevicePixelRatio.tsx
1453
+ import { useState as useState3, createContext, useContext } from "react";
1454
+ import { jsx as jsx14 } from "react/jsx-runtime";
1455
+ function getScale() {
1456
+ return window.devicePixelRatio;
1457
+ }
1458
+ var DevicePixelRatioContext = createContext(getScale());
1459
+ var DevicePixelRatioProvider = ({ children }) => {
1460
+ const [scale, setScale] = useState3(getScale());
1461
+ matchMedia(`(resolution: ${getScale()}dppx)`).addEventListener(
1462
+ "change",
1463
+ () => {
1464
+ setScale(getScale());
1465
+ },
1466
+ { once: true }
1467
+ );
1468
+ return /* @__PURE__ */ jsx14(DevicePixelRatioContext.Provider, { value: Math.ceil(scale), children });
1469
+ };
1470
+ var useDevicePixelRatio = () => useContext(DevicePixelRatioContext);
1471
+
1472
+ // src/contexts/PlaylistInfo.tsx
1473
+ import { createContext as createContext2, useContext as useContext2 } from "react";
1474
+ var PlaylistInfoContext = createContext2({
1475
+ sampleRate: 48e3,
1476
+ samplesPerPixel: 1e3,
1477
+ zoomLevels: [1e3, 1500, 2e3, 2500],
1478
+ waveHeight: 80,
1479
+ timeScaleHeight: 15,
1480
+ controls: {
1481
+ show: false,
1482
+ width: 150
1483
+ },
1484
+ duration: 3e4,
1485
+ barWidth: 1,
1486
+ barGap: 0
1487
+ });
1488
+ var usePlaylistInfo = () => useContext2(PlaylistInfoContext);
1489
+
1490
+ // src/contexts/Theme.tsx
1491
+ import { useContext as useContext3 } from "react";
1492
+ import { ThemeContext } from "styled-components";
1493
+ var useTheme2 = () => useContext3(ThemeContext);
1494
+
1495
+ // src/contexts/TrackControls.tsx
1496
+ import { createContext as createContext3, useContext as useContext4, Fragment as Fragment4 } from "react";
1497
+ import { jsx as jsx15 } from "react/jsx-runtime";
1498
+ var TrackControlsContext = createContext3(/* @__PURE__ */ jsx15(Fragment4, {}));
1499
+ var useTrackControls = () => useContext4(TrackControlsContext);
1500
+
1501
+ // src/contexts/Playout.tsx
1502
+ import {
1503
+ useState as useState4,
1504
+ createContext as createContext4,
1505
+ useContext as useContext5
1506
+ } from "react";
1507
+ import { jsx as jsx16 } from "react/jsx-runtime";
1508
+ var defaultProgress = 0;
1509
+ var defaultIsPlaying = false;
1510
+ var defaultSelectionStart = 0;
1511
+ var defaultSelectionEnd = 0;
1512
+ var defaultPlayout = {
1513
+ progress: defaultProgress,
1514
+ isPlaying: defaultIsPlaying,
1515
+ selectionStart: defaultSelectionStart,
1516
+ selectionEnd: defaultSelectionEnd
1517
+ };
1518
+ var PlayoutStatusContext = createContext4(defaultPlayout);
1519
+ var PlayoutStatusUpdateContext = createContext4({
1520
+ setIsPlaying: () => {
1521
+ },
1522
+ setProgress: () => {
1523
+ },
1524
+ setSelection: () => {
1525
+ }
1526
+ });
1527
+ var PlayoutProvider = ({ children }) => {
1528
+ const [isPlaying, setIsPlaying] = useState4(defaultIsPlaying);
1529
+ const [progress, setProgress] = useState4(defaultProgress);
1530
+ const [selectionStart, setSelectionStart] = useState4(defaultSelectionStart);
1531
+ const [selectionEnd, setSelectionEnd] = useState4(defaultSelectionEnd);
1532
+ const setSelection = (start, end) => {
1533
+ setSelectionStart(start);
1534
+ setSelectionEnd(end);
1535
+ };
1536
+ return /* @__PURE__ */ jsx16(PlayoutStatusUpdateContext.Provider, { value: { setIsPlaying, setProgress, setSelection }, children: /* @__PURE__ */ jsx16(PlayoutStatusContext.Provider, { value: { isPlaying, progress, selectionStart, selectionEnd }, children }) });
1537
+ };
1538
+ var usePlayoutStatus = () => useContext5(PlayoutStatusContext);
1539
+ var usePlayoutStatusUpdate = () => useContext5(PlayoutStatusUpdateContext);
1540
+
1541
+ // src/components/SmartChannel.tsx
1542
+ import { jsx as jsx17 } from "react/jsx-runtime";
1543
+ var SmartChannel = ({ isSelected, transparentBackground, ...props }) => {
1544
+ const theme = useTheme2();
1545
+ const { waveHeight, barWidth, barGap } = usePlaylistInfo();
1546
+ const devicePixelRatio = useDevicePixelRatio();
1547
+ const waveOutlineColor = isSelected && theme ? theme.selectedWaveOutlineColor : theme?.waveOutlineColor;
1548
+ const waveFillColor = isSelected && theme ? theme.selectedWaveFillColor : theme?.waveFillColor;
1549
+ const drawMode = theme?.waveformDrawMode || "inverted";
1550
+ return /* @__PURE__ */ jsx17(
1551
+ Channel,
1552
+ {
1553
+ ...props,
1554
+ ...theme,
1555
+ waveOutlineColor,
1556
+ waveFillColor,
1557
+ waveHeight,
1558
+ devicePixelRatio,
1559
+ barWidth,
1560
+ barGap,
1561
+ transparentBackground,
1562
+ drawMode
1563
+ }
1564
+ );
1565
+ };
1566
+
1567
+ // src/components/SmartScale.tsx
1568
+ import { useContext as useContext7 } from "react";
1569
+
1570
+ // src/components/TimeScale.tsx
1571
+ import React9, { useRef as useRef3, useEffect as useEffect4, useContext as useContext6 } from "react";
1572
+ import styled18, { withTheme as withTheme2 } from "styled-components";
1573
+
1574
+ // src/utils/conversions.ts
1575
+ function samplesToSeconds(samples, sampleRate) {
1576
+ return samples / sampleRate;
1577
+ }
1578
+ function secondsToSamples(seconds, sampleRate) {
1579
+ return Math.ceil(seconds * sampleRate);
1580
+ }
1581
+ function samplesToPixels(samples, samplesPerPixel) {
1582
+ return Math.floor(samples / samplesPerPixel);
1583
+ }
1584
+ function pixelsToSamples(pixels, samplesPerPixel) {
1585
+ return Math.floor(pixels * samplesPerPixel);
1586
+ }
1587
+ function pixelsToSeconds(pixels, samplesPerPixel, sampleRate) {
1588
+ return pixels * samplesPerPixel / sampleRate;
1589
+ }
1590
+ function secondsToPixels(seconds, samplesPerPixel, sampleRate) {
1591
+ return Math.ceil(seconds * sampleRate / samplesPerPixel);
1592
+ }
1593
+
1594
+ // src/components/TimeScale.tsx
1595
+ import { jsx as jsx18, jsxs as jsxs8 } from "react/jsx-runtime";
1596
+ function formatTime2(milliseconds) {
1597
+ const seconds = Math.floor(milliseconds / 1e3);
1598
+ const s = seconds % 60;
1599
+ const m = (seconds - s) / 60;
1600
+ return `${m}:${String(s).padStart(2, "0")}`;
1601
+ }
1602
+ var PlaylistTimeScaleScroll = styled18.div.attrs((props) => ({
1603
+ style: {
1604
+ width: `${props.$cssWidth}px`,
1605
+ marginLeft: `${props.$controlWidth}px`,
1606
+ height: `${props.$timeScaleHeight}px`
1607
+ }
1608
+ }))`
1609
+ position: relative;
1610
+ overflow: visible; /* Allow time labels to render above the container */
1611
+ border-bottom: 1px solid ${(props) => props.theme.timeColor};
1612
+ box-sizing: border-box;
1613
+ `;
1614
+ var TimeTicks = styled18.canvas.attrs((props) => ({
1615
+ style: {
1616
+ width: `${props.$cssWidth}px`,
1617
+ height: `${props.$timeScaleHeight}px`
1618
+ }
1619
+ }))`
1620
+ position: absolute;
1621
+ left: 0;
1622
+ right: 0;
1623
+ bottom: 0;
1624
+ `;
1625
+ var TimeStamp = styled18.div.attrs((props) => ({
1626
+ style: {
1627
+ left: `${props.$left + 4}px`
1628
+ // Offset 4px to the right of the tick
1629
+ }
1630
+ }))`
1631
+ position: absolute;
1632
+ font-size: 0.75rem; /* Smaller font to prevent overflow */
1633
+ white-space: nowrap; /* Prevent text wrapping */
1634
+ color: ${(props) => props.theme.timeColor}; /* Use theme color instead of inheriting */
1635
+ `;
1636
+ var TimeScale = (props) => {
1637
+ const {
1638
+ theme: { timeColor },
1639
+ duration,
1640
+ marker,
1641
+ bigStep,
1642
+ secondStep,
1643
+ renderTimestamp
1644
+ } = props;
1645
+ const canvasInfo = /* @__PURE__ */ new Map();
1646
+ const timeMarkers = [];
1647
+ const canvasRef = useRef3(null);
1648
+ const {
1649
+ sampleRate,
1650
+ samplesPerPixel,
1651
+ timeScaleHeight,
1652
+ controls: { show: showControls, width: controlWidth }
1653
+ } = useContext6(PlaylistInfoContext);
1654
+ const devicePixelRatio = useDevicePixelRatio();
1655
+ useEffect4(() => {
1656
+ if (canvasRef.current !== null) {
1657
+ const canvas = canvasRef.current;
1658
+ const ctx = canvas.getContext("2d");
1659
+ if (ctx) {
1660
+ ctx.resetTransform();
1661
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
1662
+ ctx.imageSmoothingEnabled = false;
1663
+ ctx.fillStyle = timeColor;
1664
+ ctx.scale(devicePixelRatio, devicePixelRatio);
1665
+ for (const [pixLeft, scaleHeight] of canvasInfo.entries()) {
1666
+ const scaleY = timeScaleHeight - scaleHeight;
1667
+ ctx.fillRect(pixLeft, scaleY, 1, scaleHeight);
1668
+ }
1669
+ }
1670
+ }
1671
+ }, [
1672
+ duration,
1673
+ devicePixelRatio,
1674
+ timeColor,
1675
+ timeScaleHeight,
1676
+ bigStep,
1677
+ secondStep,
1678
+ marker,
1679
+ canvasInfo
1680
+ ]);
1681
+ const widthX = secondsToPixels(duration / 1e3, samplesPerPixel, sampleRate);
1682
+ const pixPerSec = sampleRate / samplesPerPixel;
1683
+ let counter = 0;
1684
+ for (let i = 0; i < widthX; i += pixPerSec * secondStep / 1e3) {
1685
+ const pix = Math.floor(i);
1686
+ if (counter % marker === 0) {
1687
+ const timeMs = counter;
1688
+ const timestamp = formatTime2(timeMs);
1689
+ const timestampContent = renderTimestamp ? /* @__PURE__ */ jsx18(React9.Fragment, { children: renderTimestamp(timeMs, pix) }, `timestamp-${counter}`) : /* @__PURE__ */ jsx18(TimeStamp, { $left: pix, children: timestamp }, timestamp);
1690
+ timeMarkers.push(timestampContent);
1691
+ canvasInfo.set(pix, timeScaleHeight);
1692
+ } else if (counter % bigStep === 0) {
1693
+ canvasInfo.set(pix, Math.floor(timeScaleHeight / 2));
1694
+ } else if (counter % secondStep === 0) {
1695
+ canvasInfo.set(pix, Math.floor(timeScaleHeight / 5));
1696
+ }
1697
+ counter += secondStep;
1698
+ }
1699
+ return /* @__PURE__ */ jsxs8(
1700
+ PlaylistTimeScaleScroll,
1701
+ {
1702
+ $cssWidth: widthX,
1703
+ $controlWidth: showControls ? controlWidth : 0,
1704
+ $timeScaleHeight: timeScaleHeight,
1705
+ children: [
1706
+ timeMarkers,
1707
+ /* @__PURE__ */ jsx18(
1708
+ TimeTicks,
1709
+ {
1710
+ $cssWidth: widthX,
1711
+ $timeScaleHeight: timeScaleHeight,
1712
+ width: widthX * devicePixelRatio,
1713
+ height: timeScaleHeight * devicePixelRatio,
1714
+ ref: canvasRef
1715
+ }
1716
+ )
1717
+ ]
1718
+ }
1719
+ );
1720
+ };
1721
+ var StyledTimeScale = withTheme2(TimeScale);
1722
+
1723
+ // src/components/SmartScale.tsx
1724
+ import { jsx as jsx19 } from "react/jsx-runtime";
1725
+ var timeinfo = /* @__PURE__ */ new Map([
1726
+ [
1727
+ 700,
1728
+ {
1729
+ marker: 1e3,
1730
+ bigStep: 500,
1731
+ smallStep: 100
1732
+ }
1733
+ ],
1734
+ [
1735
+ 1500,
1736
+ {
1737
+ marker: 2e3,
1738
+ bigStep: 1e3,
1739
+ smallStep: 200
1740
+ }
1741
+ ],
1742
+ [
1743
+ 2500,
1744
+ {
1745
+ marker: 2e3,
1746
+ bigStep: 1e3,
1747
+ smallStep: 500
1748
+ }
1749
+ ],
1750
+ [
1751
+ 5e3,
1752
+ {
1753
+ marker: 5e3,
1754
+ bigStep: 1e3,
1755
+ smallStep: 500
1756
+ }
1757
+ ],
1758
+ [
1759
+ 1e4,
1760
+ {
1761
+ marker: 1e4,
1762
+ bigStep: 5e3,
1763
+ smallStep: 1e3
1764
+ }
1765
+ ],
1766
+ [
1767
+ 12e3,
1768
+ {
1769
+ marker: 15e3,
1770
+ bigStep: 5e3,
1771
+ smallStep: 1e3
1772
+ }
1773
+ ],
1774
+ [
1775
+ Infinity,
1776
+ {
1777
+ marker: 3e4,
1778
+ bigStep: 1e4,
1779
+ smallStep: 5e3
1780
+ }
1781
+ ]
1782
+ ]);
1783
+ function getScaleInfo(samplesPerPixel) {
1784
+ const keys = timeinfo.keys();
1785
+ let config;
1786
+ for (const resolution of keys) {
1787
+ if (samplesPerPixel < resolution) {
1788
+ config = timeinfo.get(resolution);
1789
+ break;
1790
+ }
1791
+ }
1792
+ if (config === void 0) {
1793
+ config = { marker: 3e4, bigStep: 1e4, smallStep: 5e3 };
1794
+ }
1795
+ return config;
1796
+ }
1797
+ var SmartScale = () => {
1798
+ const { samplesPerPixel, duration } = useContext7(PlaylistInfoContext);
1799
+ let config = getScaleInfo(samplesPerPixel);
1800
+ return /* @__PURE__ */ jsx19(
1801
+ StyledTimeScale,
1802
+ {
1803
+ marker: config.marker,
1804
+ bigStep: config.bigStep,
1805
+ secondStep: config.smallStep,
1806
+ duration
1807
+ }
1808
+ );
1809
+ };
1810
+
1811
+ // src/components/TimeFormatSelect.tsx
1812
+ import styled19 from "styled-components";
1813
+ import { jsx as jsx20 } from "react/jsx-runtime";
1814
+ var SelectWrapper = styled19.div`
1815
+ display: inline-flex;
1816
+ align-items: center;
1817
+ gap: 0.5rem;
1818
+ `;
1819
+ var TIME_FORMAT_OPTIONS = [
1820
+ { value: "seconds", label: "seconds" },
1821
+ { value: "thousandths", label: "thousandths" },
1822
+ { value: "hh:mm:ss", label: "hh:mm:ss" },
1823
+ { value: "hh:mm:ss.u", label: "hh:mm:ss + tenths" },
1824
+ { value: "hh:mm:ss.uu", label: "hh:mm:ss + hundredths" },
1825
+ { value: "hh:mm:ss.uuu", label: "hh:mm:ss + milliseconds" }
1826
+ ];
1827
+ var TimeFormatSelect = ({
1828
+ value,
1829
+ onChange,
1830
+ disabled = false,
1831
+ className
1832
+ }) => {
1833
+ const handleChange = (e) => {
1834
+ onChange(e.target.value);
1835
+ };
1836
+ return /* @__PURE__ */ jsx20(SelectWrapper, { className, children: /* @__PURE__ */ jsx20(
1837
+ BaseSelect,
1838
+ {
1839
+ className: "time-format",
1840
+ value,
1841
+ onChange: handleChange,
1842
+ disabled,
1843
+ "aria-label": "Time format selection",
1844
+ children: TIME_FORMAT_OPTIONS.map((option) => /* @__PURE__ */ jsx20("option", { value: option.value, children: option.label }, option.value))
1845
+ }
1846
+ ) });
1847
+ };
1848
+
1849
+ // src/components/Track.tsx
1850
+ import styled20 from "styled-components";
1851
+ import { jsx as jsx21, jsxs as jsxs9 } from "react/jsx-runtime";
1852
+ var Container = styled20.div.attrs((props) => ({
1853
+ style: {
1854
+ height: `${props.$waveHeight * props.$numChannels + (props.$hasClipHeaders ? CLIP_HEADER_HEIGHT : 0)}px`
1855
+ }
1856
+ }))`
1857
+ position: relative;
1858
+ display: flex;
1859
+ ${(props) => props.$width !== void 0 && `width: ${props.$width}px;`}
1860
+ `;
1861
+ var ChannelContainer = styled20.div.attrs((props) => ({
1862
+ style: {
1863
+ paddingLeft: `${props.$offset || 0}px`
1864
+ }
1865
+ }))`
1866
+ position: relative;
1867
+ background: ${(props) => props.$backgroundColor || "transparent"};
1868
+ flex: 1;
1869
+ `;
1870
+ var ControlsWrapper = styled20.div.attrs((props) => ({
1871
+ style: {
1872
+ width: `${props.$controlWidth}px`
1873
+ }
1874
+ }))`
1875
+ position: sticky;
1876
+ z-index: 101; /* Above waveform content, below Docusaurus navbar (z-index: 200) */
1877
+ left: 0;
1878
+ height: 100%;
1879
+ flex-shrink: 0;
1880
+ pointer-events: auto;
1881
+ background: ${(props) => props.theme.surfaceColor};
1882
+ transition: background 0.15s ease-in-out;
1883
+
1884
+ /* Selected track: highlighted background */
1885
+ ${(props) => props.$isSelected && `
1886
+ background: ${props.theme.selectedTrackControlsBackground};
1887
+ `}
1888
+ `;
1889
+ var Track = ({
1890
+ numChannels,
1891
+ children,
1892
+ className,
1893
+ backgroundColor,
1894
+ offset = 0,
1895
+ width,
1896
+ hasClipHeaders = false,
1897
+ onClick,
1898
+ trackId,
1899
+ isSelected = false
1900
+ }) => {
1901
+ const {
1902
+ waveHeight,
1903
+ controls: { show, width: controlWidth }
1904
+ } = usePlaylistInfo();
1905
+ const controls = useTrackControls();
1906
+ return /* @__PURE__ */ jsxs9(
1907
+ Container,
1908
+ {
1909
+ $numChannels: numChannels,
1910
+ className,
1911
+ $waveHeight: waveHeight,
1912
+ $controlWidth: show ? controlWidth : 0,
1913
+ $width: width,
1914
+ $hasClipHeaders: hasClipHeaders,
1915
+ $isSelected: isSelected,
1916
+ children: [
1917
+ /* @__PURE__ */ jsx21(
1918
+ ControlsWrapper,
1919
+ {
1920
+ $controlWidth: show ? controlWidth : 0,
1921
+ $isSelected: isSelected,
1922
+ children: controls
1923
+ }
1924
+ ),
1925
+ /* @__PURE__ */ jsx21(
1926
+ ChannelContainer,
1927
+ {
1928
+ $controlWidth: show ? controlWidth : 0,
1929
+ $backgroundColor: backgroundColor,
1930
+ $offset: offset,
1931
+ onClick,
1932
+ "data-track-id": trackId,
1933
+ children
1934
+ }
1935
+ )
1936
+ ]
1937
+ }
1938
+ );
1939
+ };
1940
+
1941
+ // src/components/TrackControls/Button.tsx
1942
+ import styled21 from "styled-components";
1943
+ var Button = styled21.button.attrs({
1944
+ type: "button"
1945
+ })`
1946
+ display: inline-block;
1947
+ font-family: ${(props) => props.theme.fontFamily};
1948
+ font-weight: 500;
1949
+ text-align: center;
1950
+ vertical-align: middle;
1951
+ user-select: none;
1952
+ padding: 0.25rem 0.4rem;
1953
+ font-size: ${(props) => props.theme.fontSizeSmall};
1954
+ line-height: 1;
1955
+ border-radius: ${(props) => props.theme.borderRadius};
1956
+ transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out,
1957
+ border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
1958
+ cursor: pointer;
1959
+
1960
+ ${(props) => {
1961
+ if (props.$variant === "danger") {
1962
+ return `
1963
+ color: #fff;
1964
+ background-color: #dc3545;
1965
+ border: 1px solid #dc3545;
1966
+
1967
+ &:hover {
1968
+ background-color: #c82333;
1969
+ border-color: #bd2130;
1970
+ }
1971
+
1972
+ &:focus {
1973
+ outline: none;
1974
+ box-shadow: 0 0 0 0.2rem rgba(225, 83, 97, 0.5);
1975
+ }
1976
+ `;
1977
+ } else if (props.$variant === "info") {
1978
+ return `
1979
+ color: #fff;
1980
+ background-color: #17a2b8;
1981
+ border: 1px solid #17a2b8;
1982
+
1983
+ &:hover {
1984
+ background-color: #138496;
1985
+ border-color: #117a8b;
1986
+ }
1987
+
1988
+ &:focus {
1989
+ outline: none;
1990
+ box-shadow: 0 0 0 0.2rem rgba(58, 176, 195, 0.5);
1991
+ }
1992
+ `;
1993
+ } else {
1994
+ return `
1995
+ color: ${props.theme.textColor};
1996
+ background-color: transparent;
1997
+ border: 1px solid ${props.theme.borderColor};
1998
+
1999
+ &:hover {
2000
+ color: #fff;
2001
+ background-color: ${props.theme.textColor};
2002
+ border-color: ${props.theme.textColor};
2003
+ }
2004
+
2005
+ &:focus {
2006
+ outline: none;
2007
+ box-shadow: 0 0 0 0.2rem ${props.theme.inputFocusBorder}33;
2008
+ }
2009
+ `;
2010
+ }
2011
+ }}
2012
+ `;
2013
+
2014
+ // src/components/TrackControls/ButtonGroup.tsx
2015
+ import styled22 from "styled-components";
2016
+ var ButtonGroup = styled22.div`
2017
+ margin-bottom: 0.3rem;
2018
+
2019
+ button:not(:first-child) {
2020
+ border-top-left-radius: 0;
2021
+ border-bottom-left-radius: 0;
2022
+ }
2023
+
2024
+ button:not(:last-child) {
2025
+ border-top-right-radius: 0;
2026
+ border-bottom-right-radius: 0;
2027
+ }
2028
+ `;
2029
+
2030
+ // src/components/TrackControls/Controls.tsx
2031
+ import styled23 from "styled-components";
2032
+ var Controls = styled23.div`
2033
+ background: transparent;
2034
+ width: 100%;
2035
+ height: 100%;
2036
+ display: flex;
2037
+ flex-direction: column;
2038
+ align-items: center;
2039
+ justify-content: flex-start;
2040
+ overflow: hidden;
2041
+ box-sizing: border-box;
2042
+ text-align: center;
2043
+ border: 1px solid ${(props) => props.theme.borderColor};
2044
+ border-radius: ${(props) => props.theme.borderRadius};
2045
+ `;
2046
+
2047
+ // src/components/TrackControls/Header.tsx
2048
+ import styled24 from "styled-components";
2049
+ var Header = styled24.header`
2050
+ overflow: hidden;
2051
+ height: 26px;
2052
+ width: 100%;
2053
+ display: flex;
2054
+ align-items: center;
2055
+ justify-content: space-between;
2056
+ padding: 0 0.2rem;
2057
+ font-size: ${(props) => props.theme.fontSizeSmall};
2058
+ color: ${(props) => props.theme.textColor};
2059
+ background-color: transparent;
2060
+ `;
2061
+
2062
+ // src/components/TrackControls/VolumeDownIcon.tsx
2063
+ import { SpeakerLowIcon } from "@phosphor-icons/react";
2064
+ import { jsx as jsx22 } from "react/jsx-runtime";
2065
+ var VolumeDownIcon = (props) => /* @__PURE__ */ jsx22(SpeakerLowIcon, { weight: "light", ...props });
2066
+
2067
+ // src/components/TrackControls/VolumeUpIcon.tsx
2068
+ import { SpeakerHighIcon } from "@phosphor-icons/react";
2069
+ import { jsx as jsx23 } from "react/jsx-runtime";
2070
+ var VolumeUpIcon = (props) => /* @__PURE__ */ jsx23(SpeakerHighIcon, { weight: "light", ...props });
2071
+
2072
+ // src/components/TrackControls/TrashIcon.tsx
2073
+ import { TrashIcon as PhosphorTrashIcon } from "@phosphor-icons/react";
2074
+ import { jsx as jsx24 } from "react/jsx-runtime";
2075
+ var TrashIcon = (props) => /* @__PURE__ */ jsx24(PhosphorTrashIcon, { weight: "light", ...props });
2076
+
2077
+ // src/components/TrackControls/Slider.tsx
2078
+ import styled25 from "styled-components";
2079
+ var Slider = styled25(BaseSlider)`
2080
+ width: 75%;
2081
+ height: 5px;
2082
+ background: ${(props) => props.theme.sliderTrackColor};
2083
+
2084
+ &::-webkit-slider-thumb {
2085
+ width: 12px;
2086
+ height: 12px;
2087
+ background: ${(props) => props.theme.sliderThumbColor};
2088
+ border: none;
2089
+ margin-top: -4px;
2090
+ cursor: ew-resize;
2091
+ }
2092
+
2093
+ &::-moz-range-thumb {
2094
+ width: 12px;
2095
+ height: 12px;
2096
+ background: ${(props) => props.theme.sliderThumbColor};
2097
+ border: none;
2098
+ cursor: ew-resize;
2099
+ }
2100
+
2101
+ &::-webkit-slider-runnable-track {
2102
+ height: 5px;
2103
+ background: ${(props) => props.theme.sliderTrackColor};
2104
+ border-radius: 3px;
2105
+ }
2106
+
2107
+ &::-moz-range-track {
2108
+ height: 5px;
2109
+ background: ${(props) => props.theme.sliderTrackColor};
2110
+ border-radius: 3px;
2111
+ }
2112
+
2113
+ &:focus::-webkit-slider-runnable-track {
2114
+ background: ${(props) => props.theme.inputBorder};
2115
+ }
2116
+
2117
+ &:focus::-moz-range-track {
2118
+ background: ${(props) => props.theme.inputBorder};
2119
+ }
2120
+
2121
+ &:focus::-webkit-slider-thumb {
2122
+ border: 2px solid ${(props) => props.theme.textColor};
2123
+ }
2124
+
2125
+ &:focus::-moz-range-thumb {
2126
+ border: 2px solid ${(props) => props.theme.textColor};
2127
+ }
2128
+ `;
2129
+
2130
+ // src/components/TrackControls/SliderWrapper.tsx
2131
+ import styled26 from "styled-components";
2132
+ var SliderWrapper = styled26.label`
2133
+ width: 100%;
2134
+ display: flex;
2135
+ justify-content: space-between;
2136
+ align-items: center;
2137
+ padding: 0 1rem;
2138
+ margin-bottom: 0.2rem;
2139
+ font-size: 14px;
2140
+ `;
2141
+
2142
+ // src/components/TrackControlsWithDelete.tsx
2143
+ import styled27 from "styled-components";
2144
+ import { jsx as jsx25, jsxs as jsxs10 } from "react/jsx-runtime";
2145
+ var HeaderContainer2 = styled27.div`
2146
+ display: flex;
2147
+ align-items: center;
2148
+ gap: 0.25rem;
2149
+ padding: 0.5rem 0.5rem 0.25rem 0.5rem;
2150
+ `;
2151
+ var TrackNameSpan = styled27.span`
2152
+ flex: 1;
2153
+ font-weight: 600;
2154
+ font-size: 0.875rem;
2155
+ overflow: hidden;
2156
+ text-overflow: ellipsis;
2157
+ white-space: nowrap;
2158
+ margin: 0 0.25rem;
2159
+ `;
2160
+ var DeleteIconButton = styled27.button`
2161
+ display: flex;
2162
+ align-items: center;
2163
+ justify-content: center;
2164
+ width: 20px;
2165
+ height: 20px;
2166
+ padding: 0;
2167
+ border: none;
2168
+ background: transparent;
2169
+ color: #999;
2170
+ cursor: pointer;
2171
+ font-size: 16px;
2172
+ line-height: 1;
2173
+ border-radius: 3px;
2174
+ transition: all 0.2s ease-in-out;
2175
+ flex-shrink: 0;
2176
+
2177
+ &:hover {
2178
+ background: #dc3545;
2179
+ color: white;
2180
+ }
2181
+
2182
+ &:active {
2183
+ transform: scale(0.9);
2184
+ }
2185
+ `;
2186
+ var TrackControlsWithDelete = ({
2187
+ trackName,
2188
+ muted,
2189
+ soloed,
2190
+ volume,
2191
+ pan,
2192
+ onMuteChange,
2193
+ onSoloChange,
2194
+ onVolumeChange,
2195
+ onPanChange,
2196
+ onDelete
2197
+ }) => {
2198
+ return /* @__PURE__ */ jsxs10(Controls, { children: [
2199
+ /* @__PURE__ */ jsxs10(HeaderContainer2, { children: [
2200
+ /* @__PURE__ */ jsx25(DeleteIconButton, { onClick: onDelete, title: "Delete track", children: /* @__PURE__ */ jsx25(TrashIcon, {}) }),
2201
+ /* @__PURE__ */ jsx25(TrackNameSpan, { children: trackName })
2202
+ ] }),
2203
+ /* @__PURE__ */ jsxs10(ButtonGroup, { children: [
2204
+ /* @__PURE__ */ jsx25(
2205
+ Button,
2206
+ {
2207
+ $variant: muted ? "danger" : "outline",
2208
+ onClick: () => onMuteChange(!muted),
2209
+ children: "Mute"
2210
+ }
2211
+ ),
2212
+ /* @__PURE__ */ jsx25(
2213
+ Button,
2214
+ {
2215
+ $variant: soloed ? "info" : "outline",
2216
+ onClick: () => onSoloChange(!soloed),
2217
+ children: "Solo"
2218
+ }
2219
+ )
2220
+ ] }),
2221
+ /* @__PURE__ */ jsxs10(SliderWrapper, { children: [
2222
+ /* @__PURE__ */ jsx25(VolumeDownIcon, {}),
2223
+ /* @__PURE__ */ jsx25(
2224
+ Slider,
2225
+ {
2226
+ min: "0",
2227
+ max: "1",
2228
+ step: "0.01",
2229
+ value: volume,
2230
+ onChange: (e) => onVolumeChange(parseFloat(e.target.value))
2231
+ }
2232
+ ),
2233
+ /* @__PURE__ */ jsx25(VolumeUpIcon, {})
2234
+ ] }),
2235
+ /* @__PURE__ */ jsxs10(SliderWrapper, { children: [
2236
+ /* @__PURE__ */ jsx25("span", { children: "L" }),
2237
+ /* @__PURE__ */ jsx25(
2238
+ Slider,
2239
+ {
2240
+ min: "-1",
2241
+ max: "1",
2242
+ step: "0.01",
2243
+ value: pan,
2244
+ onChange: (e) => onPanChange(parseFloat(e.target.value))
2245
+ }
2246
+ ),
2247
+ /* @__PURE__ */ jsx25("span", { children: "R" })
2248
+ ] })
2249
+ ] });
2250
+ };
2251
+ export {
2252
+ AudioPosition,
2253
+ AutomaticScrollCheckbox,
2254
+ BaseButton,
2255
+ BaseCheckbox,
2256
+ BaseCheckboxLabel,
2257
+ BaseCheckboxWrapper,
2258
+ BaseControlButton,
2259
+ BaseInput,
2260
+ BaseLabel,
2261
+ BaseSelect,
2262
+ BaseSlider,
2263
+ Button,
2264
+ ButtonGroup,
2265
+ CLIP_BOUNDARY_WIDTH,
2266
+ CLIP_HEADER_HEIGHT,
2267
+ Channel,
2268
+ Clip,
2269
+ ClipBoundary,
2270
+ ClipHeader,
2271
+ ClipHeaderPresentational,
2272
+ Controls,
2273
+ DevicePixelRatioProvider,
2274
+ FadeOverlay,
2275
+ Header,
2276
+ InlineLabel,
2277
+ MasterVolumeControl,
2278
+ Playhead,
2279
+ PlayheadWithMarker,
2280
+ Playlist,
2281
+ PlaylistInfoContext,
2282
+ PlayoutProvider,
2283
+ ScreenReaderOnly,
2284
+ Selection,
2285
+ SelectionTimeInputs,
2286
+ Slider,
2287
+ SliderWrapper,
2288
+ SmartChannel,
2289
+ SmartScale,
2290
+ StyledPlaylist,
2291
+ StyledTimeScale,
2292
+ TimeFormatSelect,
2293
+ TimeInput,
2294
+ TimeScale,
2295
+ Track,
2296
+ TrackControlsContext,
2297
+ TrackControlsWithDelete,
2298
+ TrashIcon,
2299
+ VolumeDownIcon,
2300
+ VolumeUpIcon,
2301
+ darkTheme,
2302
+ defaultTheme,
2303
+ formatTime,
2304
+ isWaveformGradient,
2305
+ parseTime,
2306
+ pixelsToSamples,
2307
+ pixelsToSeconds,
2308
+ samplesToPixels,
2309
+ samplesToSeconds,
2310
+ secondsToPixels,
2311
+ secondsToSamples,
2312
+ useDevicePixelRatio,
2313
+ usePlaylistInfo,
2314
+ usePlayoutStatus,
2315
+ usePlayoutStatusUpdate,
2316
+ useTheme2 as useTheme,
2317
+ useTrackControls,
2318
+ waveformColorToCss
2319
+ };
2320
+ //# sourceMappingURL=index.mjs.map