@waveform-playlist/annotations 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,995 @@
1
+ // src/parsers/aeneas.ts
2
+ function parseAeneas(data) {
3
+ return {
4
+ id: data.id,
5
+ start: parseFloat(data.begin),
6
+ end: parseFloat(data.end),
7
+ lines: data.lines,
8
+ lang: data.language
9
+ };
10
+ }
11
+ function serializeAeneas(annotation) {
12
+ return {
13
+ id: annotation.id,
14
+ begin: annotation.start.toFixed(3),
15
+ end: annotation.end.toFixed(3),
16
+ lines: annotation.lines,
17
+ language: annotation.lang || "en"
18
+ };
19
+ }
20
+
21
+ // src/components/Annotation.tsx
22
+ import { useState } from "react";
23
+ import styled from "styled-components";
24
+ import { jsx, jsxs } from "react/jsx-runtime";
25
+ var AnnotationOverlay = styled.div.attrs((props) => ({
26
+ style: {
27
+ left: `${props.$left}px`,
28
+ width: `${props.$width}px`
29
+ }
30
+ }))`
31
+ position: absolute;
32
+ top: 0;
33
+ background: ${(props) => props.$color};
34
+ height: 100%;
35
+ z-index: 10;
36
+ pointer-events: auto;
37
+ opacity: 0.3;
38
+ border: 2px solid ${(props) => props.$color};
39
+ border-radius: 4px;
40
+ cursor: pointer;
41
+
42
+ &:hover {
43
+ opacity: 0.5;
44
+ border-color: ${(props) => props.$color};
45
+ }
46
+ `;
47
+ var AnnotationText = styled.div`
48
+ position: absolute;
49
+ bottom: 0;
50
+ left: 0;
51
+ right: 0;
52
+ background: rgba(0, 0, 0, 0.7);
53
+ color: white;
54
+ padding: 4px 8px;
55
+ font-size: 12px;
56
+ line-height: 1.3;
57
+ max-height: 60%;
58
+ overflow: hidden;
59
+ text-overflow: ellipsis;
60
+ pointer-events: none;
61
+ white-space: pre-wrap;
62
+ word-break: break-word;
63
+ `;
64
+ var EditableText = styled.textarea`
65
+ position: absolute;
66
+ bottom: 0;
67
+ left: 0;
68
+ right: 0;
69
+ background: rgba(0, 0, 0, 0.9);
70
+ color: white;
71
+ padding: 4px 8px;
72
+ font-size: 12px;
73
+ line-height: 1.3;
74
+ max-height: 60%;
75
+ overflow: auto;
76
+ border: 1px solid #fff;
77
+ resize: none;
78
+ font-family: inherit;
79
+
80
+ &:focus {
81
+ outline: none;
82
+ border-color: #4CAF50;
83
+ }
84
+ `;
85
+ var ControlsBar = styled.div`
86
+ position: absolute;
87
+ top: 0;
88
+ left: 0;
89
+ right: 0;
90
+ background: rgba(0, 0, 0, 0.8);
91
+ display: flex;
92
+ gap: 4px;
93
+ padding: 4px;
94
+ justify-content: flex-start;
95
+ align-items: center;
96
+ `;
97
+ var ControlButton = styled.button`
98
+ background: transparent;
99
+ border: 1px solid rgba(255, 255, 255, 0.5);
100
+ color: white;
101
+ padding: 4px 8px;
102
+ font-size: 10px;
103
+ cursor: pointer;
104
+ border-radius: 3px;
105
+ display: flex;
106
+ align-items: center;
107
+ justify-content: center;
108
+ min-width: 24px;
109
+ height: 24px;
110
+
111
+ &:hover {
112
+ background: rgba(255, 255, 255, 0.2);
113
+ border-color: white;
114
+ }
115
+
116
+ &:active {
117
+ background: rgba(255, 255, 255, 0.3);
118
+ }
119
+ `;
120
+ var Annotation = ({
121
+ annotation,
122
+ index,
123
+ allAnnotations,
124
+ startPosition,
125
+ endPosition,
126
+ color = "#ff9800",
127
+ editable = false,
128
+ controls = [],
129
+ onAnnotationUpdate,
130
+ annotationListConfig,
131
+ onClick
132
+ }) => {
133
+ const [isEditing, setIsEditing] = useState(false);
134
+ const [editedText, setEditedText] = useState(annotation.lines.join("\n"));
135
+ const width = Math.max(0, endPosition - startPosition);
136
+ if (width <= 0) {
137
+ return null;
138
+ }
139
+ const handleClick = () => {
140
+ if (onClick) {
141
+ onClick(annotation);
142
+ }
143
+ };
144
+ const handleDoubleClick = () => {
145
+ if (editable) {
146
+ setIsEditing(true);
147
+ }
148
+ };
149
+ const handleTextChange = (e) => {
150
+ setEditedText(e.target.value);
151
+ };
152
+ const handleTextBlur = () => {
153
+ setIsEditing(false);
154
+ const newLines = editedText.split("\n");
155
+ if (newLines.join("\n") !== annotation.lines.join("\n")) {
156
+ const updatedAnnotations = [...allAnnotations];
157
+ updatedAnnotations[index] = { ...annotation, lines: newLines };
158
+ if (onAnnotationUpdate) {
159
+ onAnnotationUpdate(updatedAnnotations);
160
+ }
161
+ }
162
+ };
163
+ const handleControlClick = (control) => {
164
+ const annotationsCopy = [...allAnnotations];
165
+ control.action(annotationsCopy[index], index, annotationsCopy, annotationListConfig || {});
166
+ if (onAnnotationUpdate) {
167
+ onAnnotationUpdate(annotationsCopy);
168
+ }
169
+ };
170
+ const getIconClass = (classString) => {
171
+ return classString.replace(/\./g, " ");
172
+ };
173
+ return /* @__PURE__ */ jsxs(
174
+ AnnotationOverlay,
175
+ {
176
+ $left: startPosition,
177
+ $width: width,
178
+ $color: color,
179
+ onClick: handleClick,
180
+ onDoubleClick: handleDoubleClick,
181
+ children: [
182
+ controls.length > 0 && /* @__PURE__ */ jsx(ControlsBar, { children: controls.map((control, idx) => /* @__PURE__ */ jsx(
183
+ ControlButton,
184
+ {
185
+ title: control.title,
186
+ onClick: (e) => {
187
+ e.stopPropagation();
188
+ handleControlClick(control);
189
+ },
190
+ children: control.text ? control.text : /* @__PURE__ */ jsx("i", { className: getIconClass(control.class || "") })
191
+ },
192
+ idx
193
+ )) }),
194
+ isEditing ? /* @__PURE__ */ jsx(
195
+ EditableText,
196
+ {
197
+ value: editedText,
198
+ onChange: handleTextChange,
199
+ onBlur: handleTextBlur,
200
+ autoFocus: true,
201
+ onClick: (e) => e.stopPropagation(),
202
+ onDoubleClick: (e) => e.stopPropagation()
203
+ }
204
+ ) : /* @__PURE__ */ jsx(AnnotationText, { children: annotation.lines.join("\n") })
205
+ ]
206
+ }
207
+ );
208
+ };
209
+
210
+ // src/components/AnnotationBox.tsx
211
+ import styled2 from "styled-components";
212
+ import { useDraggable } from "@dnd-kit/core";
213
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
214
+ var Wrapper = styled2.div.attrs((props) => ({
215
+ style: {
216
+ left: `${props.$left}px`,
217
+ width: `${props.$width}px`
218
+ }
219
+ }))`
220
+ position: absolute;
221
+ top: 0;
222
+ height: 100%;
223
+ pointer-events: none; /* Let events pass through to children */
224
+ `;
225
+ var Box = styled2.div`
226
+ position: absolute;
227
+ top: 0;
228
+ left: 0;
229
+ right: 0;
230
+ height: 100%;
231
+ background: ${(props) => props.$isActive ? props.theme?.annotationBoxActiveBackground || "rgba(255, 200, 100, 0.95)" : props.theme?.annotationBoxBackground || "rgba(255, 255, 255, 0.85)"};
232
+ border: ${(props) => props.$isActive ? "3px" : "2px"} solid ${(props) => props.$isActive ? props.theme?.annotationBoxActiveBorder || "#ff9800" : props.$color};
233
+ border-radius: 4px;
234
+ cursor: pointer;
235
+ pointer-events: auto;
236
+ display: flex;
237
+ align-items: center;
238
+ justify-content: center;
239
+ overflow: hidden;
240
+ transition: all 0.2s ease;
241
+ box-shadow: ${(props) => props.$isActive ? "0 2px 8px rgba(255, 152, 0, 0.4), inset 0 0 0 1px rgba(255, 152, 0, 0.2)" : "0 1px 3px rgba(0, 0, 0, 0.1)"};
242
+
243
+ &:hover {
244
+ background: ${(props) => props.theme?.annotationBoxHoverBackground || "rgba(255, 255, 255, 0.98)"};
245
+ border-color: ${(props) => props.theme?.annotationBoxActiveBorder || "#ff9800"};
246
+ border-width: 3px;
247
+ box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15);
248
+ }
249
+ `;
250
+ var Label = styled2.span`
251
+ font-size: 12px;
252
+ font-weight: 600;
253
+ color: ${(props) => props.theme?.annotationLabelColor || "#2a2a2a"};
254
+ white-space: nowrap;
255
+ overflow: hidden;
256
+ text-overflow: ellipsis;
257
+ padding: 0 6px;
258
+ letter-spacing: 0.3px;
259
+ user-select: none;
260
+ `;
261
+ var ResizeHandle = styled2.div`
262
+ position: absolute;
263
+ top: 0;
264
+ ${(props) => props.$position === "left" ? "left: -8px" : "right: -8px"};
265
+ width: 16px;
266
+ height: 100%;
267
+ cursor: ew-resize;
268
+ z-index: 120; /* Above ClickOverlay (z-index: 100) and AnnotationBoxesWrapper (z-index: 110) */
269
+ background: ${(props) => props.$isDragging ? props.theme?.annotationResizeHandleColor || "rgba(0, 0, 0, 0.2)" : "transparent"};
270
+ border-radius: 4px;
271
+ touch-action: none; /* Important for @dnd-kit on touch devices */
272
+ pointer-events: auto;
273
+
274
+ &::before {
275
+ content: '';
276
+ position: absolute;
277
+ top: 50%;
278
+ left: 50%;
279
+ transform: translate(-50%, -50%);
280
+ width: 4px;
281
+ height: 60%;
282
+ background: ${(props) => props.$isDragging ? props.theme?.annotationResizeHandleActiveColor || "rgba(0, 0, 0, 0.8)" : props.theme?.annotationResizeHandleColor || "rgba(0, 0, 0, 0.4)"};
283
+ border-radius: 2px;
284
+ opacity: ${(props) => props.$isDragging ? 1 : 0.6};
285
+ transition: opacity 0.2s, background 0.2s;
286
+ }
287
+
288
+ &:hover {
289
+ background: ${(props) => props.theme?.annotationResizeHandleColor || "rgba(0, 0, 0, 0.1)"};
290
+ }
291
+
292
+ &:hover::before {
293
+ opacity: 1;
294
+ background: ${(props) => props.theme?.annotationResizeHandleActiveColor || "rgba(0, 0, 0, 0.7)"};
295
+ }
296
+ `;
297
+ var AnnotationBox = ({
298
+ annotationId,
299
+ annotationIndex,
300
+ startPosition,
301
+ endPosition,
302
+ label,
303
+ color = "#ff9800",
304
+ isActive = false,
305
+ onClick,
306
+ editable = true
307
+ }) => {
308
+ const width = Math.max(0, endPosition - startPosition);
309
+ const leftBoundaryId = `annotation-boundary-start-${annotationIndex}`;
310
+ const {
311
+ attributes: leftAttributes,
312
+ listeners: leftListeners,
313
+ setActivatorNodeRef: setLeftActivatorRef,
314
+ isDragging: isLeftDragging
315
+ } = useDraggable({
316
+ id: leftBoundaryId,
317
+ data: { annotationId, annotationIndex, edge: "start" },
318
+ disabled: !editable
319
+ });
320
+ const rightBoundaryId = `annotation-boundary-end-${annotationIndex}`;
321
+ const {
322
+ attributes: rightAttributes,
323
+ listeners: rightListeners,
324
+ setActivatorNodeRef: setRightActivatorRef,
325
+ isDragging: isRightDragging
326
+ } = useDraggable({
327
+ id: rightBoundaryId,
328
+ data: { annotationId, annotationIndex, edge: "end" },
329
+ disabled: !editable
330
+ });
331
+ if (width <= 0) {
332
+ return null;
333
+ }
334
+ const createPointerDownHandler = (dndKitHandler) => {
335
+ return (e) => {
336
+ e.stopPropagation();
337
+ dndKitHandler?.(e);
338
+ };
339
+ };
340
+ const handleHandleClick = (e) => {
341
+ e.stopPropagation();
342
+ };
343
+ return /* @__PURE__ */ jsxs2(Wrapper, { $left: startPosition, $width: width, children: [
344
+ /* @__PURE__ */ jsx2(
345
+ Box,
346
+ {
347
+ $color: color,
348
+ $isActive: isActive,
349
+ onClick,
350
+ children: label && /* @__PURE__ */ jsx2(Label, { children: label })
351
+ }
352
+ ),
353
+ editable && /* @__PURE__ */ jsx2(
354
+ ResizeHandle,
355
+ {
356
+ ref: setLeftActivatorRef,
357
+ $position: "left",
358
+ $isDragging: isLeftDragging,
359
+ onClick: handleHandleClick,
360
+ ...leftListeners,
361
+ onPointerDown: createPointerDownHandler(leftListeners?.onPointerDown),
362
+ ...leftAttributes
363
+ }
364
+ ),
365
+ editable && /* @__PURE__ */ jsx2(
366
+ ResizeHandle,
367
+ {
368
+ ref: setRightActivatorRef,
369
+ $position: "right",
370
+ $isDragging: isRightDragging,
371
+ onClick: handleHandleClick,
372
+ ...rightListeners,
373
+ onPointerDown: createPointerDownHandler(rightListeners?.onPointerDown),
374
+ ...rightAttributes
375
+ }
376
+ )
377
+ ] });
378
+ };
379
+
380
+ // src/components/AnnotationBoxesWrapper.tsx
381
+ import styled3 from "styled-components";
382
+ import { usePlaylistInfo } from "@waveform-playlist/ui-components";
383
+ import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
384
+ var Container = styled3.div.attrs((props) => ({
385
+ style: {
386
+ height: `${props.$height}px`
387
+ }
388
+ }))`
389
+ position: relative;
390
+ display: flex;
391
+ ${(props) => props.$width !== void 0 && `width: ${props.$width}px;`}
392
+ background: transparent;
393
+ z-index: 110;
394
+ `;
395
+ var ControlsPlaceholder = styled3.div`
396
+ position: sticky;
397
+ z-index: 200;
398
+ left: 0;
399
+ height: 100%;
400
+ width: ${(props) => props.$controlWidth}px;
401
+ flex-shrink: 0;
402
+ background: transparent;
403
+ `;
404
+ var BoxesContainer = styled3.div`
405
+ position: relative;
406
+ flex: 1;
407
+ padding-left: ${(props) => props.$offset || 0}px;
408
+ `;
409
+ var AnnotationBoxesWrapper = ({
410
+ children,
411
+ className,
412
+ height = 30,
413
+ offset = 0,
414
+ width
415
+ }) => {
416
+ const {
417
+ controls: { show, width: controlWidth }
418
+ } = usePlaylistInfo();
419
+ return /* @__PURE__ */ jsxs3(
420
+ Container,
421
+ {
422
+ className,
423
+ $height: height,
424
+ $controlWidth: show ? controlWidth : 0,
425
+ $width: width,
426
+ children: [
427
+ /* @__PURE__ */ jsx3(ControlsPlaceholder, { $controlWidth: show ? controlWidth : 0 }),
428
+ /* @__PURE__ */ jsx3(BoxesContainer, { $offset: offset, children })
429
+ ]
430
+ }
431
+ );
432
+ };
433
+
434
+ // src/components/AnnotationsTrack.tsx
435
+ import styled4 from "styled-components";
436
+ import { usePlaylistInfo as usePlaylistInfo2 } from "@waveform-playlist/ui-components";
437
+ import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
438
+ var Container2 = styled4.div.attrs((props) => ({
439
+ style: {
440
+ height: `${props.$height}px`
441
+ }
442
+ }))`
443
+ position: relative;
444
+ display: flex;
445
+ ${(props) => props.$width !== void 0 && `width: ${props.$width}px;`}
446
+ background: transparent;
447
+ `;
448
+ var ControlsPlaceholder2 = styled4.div`
449
+ position: sticky;
450
+ z-index: 200;
451
+ left: 0;
452
+ height: 100%;
453
+ width: ${(props) => props.$controlWidth}px;
454
+ flex-shrink: 0;
455
+ background: transparent;
456
+ display: flex;
457
+ align-items: center;
458
+ justify-content: center;
459
+ font-size: 12px;
460
+ color: ${(props) => props.theme?.textColorMuted || "#666"};
461
+ font-weight: bold;
462
+ `;
463
+ var AnnotationsContainer = styled4.div`
464
+ position: relative;
465
+ flex: 1;
466
+ padding-left: ${(props) => props.$offset || 0}px;
467
+ `;
468
+ var AnnotationsTrack = ({
469
+ children,
470
+ className,
471
+ height = 100,
472
+ offset = 0,
473
+ width
474
+ }) => {
475
+ const {
476
+ controls: { show, width: controlWidth }
477
+ } = usePlaylistInfo2();
478
+ return /* @__PURE__ */ jsxs4(
479
+ Container2,
480
+ {
481
+ className,
482
+ $height: height,
483
+ $controlWidth: show ? controlWidth : 0,
484
+ $width: width,
485
+ children: [
486
+ /* @__PURE__ */ jsx4(ControlsPlaceholder2, { $controlWidth: show ? controlWidth : 0, children: "Annotations" }),
487
+ /* @__PURE__ */ jsx4(AnnotationsContainer, { $offset: offset, children })
488
+ ]
489
+ }
490
+ );
491
+ };
492
+
493
+ // src/components/AnnotationText.tsx
494
+ import React2, { useRef, useEffect } from "react";
495
+ import styled5 from "styled-components";
496
+ import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
497
+ var Container3 = styled5.div`
498
+ background: ${(props) => props.theme?.backgroundColor || "#fff"};
499
+ ${(props) => props.$height ? `height: ${props.$height}px;` : "max-height: 200px;"}
500
+ overflow-y: auto;
501
+ padding: 8px;
502
+ `;
503
+ var AnnotationItem = styled5.div`
504
+ padding: 12px;
505
+ margin-bottom: 6px;
506
+ border-left: 4px solid ${(props) => props.$isActive ? "#ff9800" : "transparent"};
507
+ background: ${(props) => props.$isActive ? "rgba(255, 152, 0, 0.15)" : "transparent"};
508
+ border-radius: 4px;
509
+ transition: all 0.2s;
510
+ cursor: pointer;
511
+ box-shadow: ${(props) => props.$isActive ? "0 2px 8px rgba(255, 152, 0, 0.25), inset 0 0 0 1px rgba(255, 152, 0, 0.3)" : "none"};
512
+
513
+ &:hover {
514
+ background: ${(props) => props.$isActive ? "rgba(255, 152, 0, 0.2)" : props.theme?.annotationTextItemHoverBackground || "rgba(0, 0, 0, 0.05)"};
515
+ border-left-color: ${(props) => props.$isActive ? "#ff9800" : props.theme?.borderColor || "#ddd"};
516
+ }
517
+
518
+ &:focus-visible {
519
+ outline: 2px solid #ff9800;
520
+ outline-offset: 2px;
521
+ }
522
+ `;
523
+ var AnnotationHeader = styled5.div`
524
+ display: flex;
525
+ justify-content: space-between;
526
+ align-items: center;
527
+ margin-bottom: 6px;
528
+ `;
529
+ var AnnotationInfo = styled5.div`
530
+ display: flex;
531
+ align-items: center;
532
+ gap: 8px;
533
+ `;
534
+ var AnnotationIdLabel = styled5.span`
535
+ font-size: 11px;
536
+ font-weight: 600;
537
+ color: ${(props) => props.theme?.textColorMuted || "#666"};
538
+ background: transparent;
539
+ padding: 2px 6px;
540
+ border-radius: 3px;
541
+ min-width: 20px;
542
+ outline: ${(props) => props.$isEditable ? `1px dashed ${props.theme?.borderColor || "#ddd"}` : "none"};
543
+
544
+ &[contenteditable='true']:focus {
545
+ outline: 2px solid #ff9800;
546
+ background: rgba(255, 152, 0, 0.1);
547
+ }
548
+ `;
549
+ var TimeRange = styled5.span`
550
+ font-size: 12px;
551
+ font-weight: 500;
552
+ color: ${(props) => props.theme?.textColorMuted || "#555"};
553
+ font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
554
+ letter-spacing: 0.5px;
555
+ `;
556
+ var AnnotationControls = styled5.div`
557
+ display: flex;
558
+ gap: 6px;
559
+ `;
560
+ var ControlButton2 = styled5.button`
561
+ background: ${(props) => props.theme?.surfaceColor || "#f5f5f5"};
562
+ border: 1px solid ${(props) => props.theme?.borderColor || "#ccc"};
563
+ color: ${(props) => props.theme?.textColor || "#333"};
564
+ padding: 4px 8px;
565
+ font-size: 14px;
566
+ cursor: pointer;
567
+ border-radius: 4px;
568
+ transition: all 0.15s ease;
569
+
570
+ &:hover {
571
+ background: ${(props) => props.theme?.inputBackground || "#3d3d3d"};
572
+ border-color: ${(props) => props.theme?.textColorMuted || "#999"};
573
+ transform: scale(1.05);
574
+ }
575
+
576
+ &:active {
577
+ transform: scale(0.95);
578
+ }
579
+ `;
580
+ var AnnotationTextContent = styled5.div`
581
+ font-size: 14px;
582
+ line-height: 1.6;
583
+ color: ${(props) => props.theme?.textColor || "#2a2a2a"};
584
+ white-space: pre-wrap;
585
+ word-break: break-word;
586
+ outline: ${(props) => props.$isEditable ? `1px dashed ${props.theme?.borderColor || "#ddd"}` : "none"};
587
+ padding: ${(props) => props.$isEditable ? "6px" : "0"};
588
+ border-radius: 3px;
589
+ min-height: 20px;
590
+
591
+ &[contenteditable='true']:focus {
592
+ outline: 2px solid #ff9800;
593
+ background: rgba(255, 152, 0, 0.1);
594
+ }
595
+ `;
596
+ var AnnotationTextComponent = ({
597
+ annotations,
598
+ activeAnnotationId,
599
+ shouldScrollToActive = false,
600
+ editable = false,
601
+ controls = [],
602
+ annotationListConfig,
603
+ height,
604
+ onAnnotationClick,
605
+ onAnnotationUpdate
606
+ }) => {
607
+ const activeAnnotationRef = useRef(null);
608
+ const containerRef = useRef(null);
609
+ const prevActiveIdRef = useRef(void 0);
610
+ useEffect(() => {
611
+ });
612
+ useEffect(() => {
613
+ const container = containerRef.current;
614
+ if (!container) return;
615
+ const handleScroll = () => {
616
+ };
617
+ container.addEventListener("scroll", handleScroll);
618
+ return () => container.removeEventListener("scroll", handleScroll);
619
+ }, []);
620
+ useEffect(() => {
621
+ if (activeAnnotationId && activeAnnotationRef.current && shouldScrollToActive) {
622
+ activeAnnotationRef.current.scrollIntoView({
623
+ behavior: "smooth",
624
+ block: "nearest"
625
+ });
626
+ }
627
+ prevActiveIdRef.current = activeAnnotationId;
628
+ }, [activeAnnotationId, shouldScrollToActive]);
629
+ const formatTime = (seconds) => {
630
+ if (isNaN(seconds) || !isFinite(seconds)) {
631
+ return "0:00.000";
632
+ }
633
+ const mins = Math.floor(seconds / 60);
634
+ const secs = (seconds % 60).toFixed(3);
635
+ return `${mins}:${secs.padStart(6, "0")}`;
636
+ };
637
+ const handleTextEdit = (index, newText) => {
638
+ if (!editable || !onAnnotationUpdate) return;
639
+ const updatedAnnotations = [...annotations];
640
+ updatedAnnotations[index] = {
641
+ ...updatedAnnotations[index],
642
+ lines: newText.split("\n")
643
+ };
644
+ onAnnotationUpdate(updatedAnnotations);
645
+ };
646
+ const handleIdEdit = (index, newId) => {
647
+ if (!editable || !onAnnotationUpdate) return;
648
+ const trimmedId = newId.trim();
649
+ if (!trimmedId) return;
650
+ const updatedAnnotations = [...annotations];
651
+ updatedAnnotations[index] = {
652
+ ...updatedAnnotations[index],
653
+ id: trimmedId
654
+ };
655
+ onAnnotationUpdate(updatedAnnotations);
656
+ };
657
+ const handleControlClick = (control, annotation, index) => {
658
+ if (!onAnnotationUpdate) return;
659
+ const annotationsCopy = [...annotations];
660
+ control.action(annotationsCopy[index], index, annotationsCopy, annotationListConfig || {});
661
+ onAnnotationUpdate(annotationsCopy);
662
+ };
663
+ const getIconClass = (classString) => {
664
+ return classString.replace(/\./g, " ");
665
+ };
666
+ return /* @__PURE__ */ jsx5(Container3, { ref: containerRef, $height: height, children: annotations.map((annotation, index) => {
667
+ const isActive = annotation.id === activeAnnotationId;
668
+ return /* @__PURE__ */ jsxs5(
669
+ AnnotationItem,
670
+ {
671
+ ref: isActive ? activeAnnotationRef : null,
672
+ $isActive: isActive,
673
+ onClick: () => onAnnotationClick?.(annotation),
674
+ children: [
675
+ /* @__PURE__ */ jsxs5(AnnotationHeader, { children: [
676
+ /* @__PURE__ */ jsxs5(AnnotationInfo, { children: [
677
+ /* @__PURE__ */ jsx5(
678
+ AnnotationIdLabel,
679
+ {
680
+ $isEditable: editable,
681
+ contentEditable: editable,
682
+ suppressContentEditableWarning: true,
683
+ onBlur: (e) => handleIdEdit(index, e.currentTarget.textContent || ""),
684
+ children: annotation.id
685
+ }
686
+ ),
687
+ /* @__PURE__ */ jsxs5(TimeRange, { children: [
688
+ formatTime(annotation.start),
689
+ " - ",
690
+ formatTime(annotation.end)
691
+ ] })
692
+ ] }),
693
+ controls.length > 0 && /* @__PURE__ */ jsx5(AnnotationControls, { onClick: (e) => e.stopPropagation(), children: controls.map((control, idx) => /* @__PURE__ */ jsx5(
694
+ ControlButton2,
695
+ {
696
+ title: control.title,
697
+ onClick: () => handleControlClick(control, annotation, index),
698
+ children: control.text ? control.text : /* @__PURE__ */ jsx5("i", { className: getIconClass(control.class || "") })
699
+ },
700
+ idx
701
+ )) })
702
+ ] }),
703
+ /* @__PURE__ */ jsx5(
704
+ AnnotationTextContent,
705
+ {
706
+ $isEditable: editable,
707
+ contentEditable: editable,
708
+ suppressContentEditableWarning: true,
709
+ onBlur: (e) => handleTextEdit(index, e.currentTarget.textContent || ""),
710
+ children: annotation.lines.join("\n")
711
+ }
712
+ )
713
+ ]
714
+ },
715
+ annotation.id
716
+ );
717
+ }) });
718
+ };
719
+ var AnnotationText2 = React2.memo(AnnotationTextComponent);
720
+
721
+ // src/components/ContinuousPlayCheckbox.tsx
722
+ import { BaseCheckboxWrapper, BaseCheckbox, BaseCheckboxLabel } from "@waveform-playlist/ui-components";
723
+ import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
724
+ var ContinuousPlayCheckbox = ({
725
+ checked,
726
+ onChange,
727
+ disabled = false,
728
+ className
729
+ }) => {
730
+ const handleChange = (e) => {
731
+ onChange(e.target.checked);
732
+ };
733
+ return /* @__PURE__ */ jsxs6(BaseCheckboxWrapper, { className, children: [
734
+ /* @__PURE__ */ jsx6(
735
+ BaseCheckbox,
736
+ {
737
+ type: "checkbox",
738
+ id: "continuous-play",
739
+ className: "continuous-play",
740
+ checked,
741
+ onChange: handleChange,
742
+ disabled
743
+ }
744
+ ),
745
+ /* @__PURE__ */ jsx6(BaseCheckboxLabel, { htmlFor: "continuous-play", children: "Continuous Play" })
746
+ ] });
747
+ };
748
+
749
+ // src/components/LinkEndpointsCheckbox.tsx
750
+ import { BaseCheckboxWrapper as BaseCheckboxWrapper2, BaseCheckbox as BaseCheckbox2, BaseCheckboxLabel as BaseCheckboxLabel2 } from "@waveform-playlist/ui-components";
751
+ import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
752
+ var LinkEndpointsCheckbox = ({
753
+ checked,
754
+ onChange,
755
+ disabled = false,
756
+ className
757
+ }) => {
758
+ const handleChange = (e) => {
759
+ onChange(e.target.checked);
760
+ };
761
+ return /* @__PURE__ */ jsxs7(BaseCheckboxWrapper2, { className, children: [
762
+ /* @__PURE__ */ jsx7(
763
+ BaseCheckbox2,
764
+ {
765
+ type: "checkbox",
766
+ id: "link-endpoints",
767
+ className: "link-endpoints",
768
+ checked,
769
+ onChange: handleChange,
770
+ disabled
771
+ }
772
+ ),
773
+ /* @__PURE__ */ jsx7(BaseCheckboxLabel2, { htmlFor: "link-endpoints", children: "Link Endpoints" })
774
+ ] });
775
+ };
776
+
777
+ // src/components/EditableCheckbox.tsx
778
+ import { BaseCheckboxWrapper as BaseCheckboxWrapper3, BaseCheckbox as BaseCheckbox3, BaseCheckboxLabel as BaseCheckboxLabel3 } from "@waveform-playlist/ui-components";
779
+ import { jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
780
+ var EditableCheckbox = ({
781
+ checked,
782
+ onChange,
783
+ className
784
+ }) => {
785
+ return /* @__PURE__ */ jsxs8(BaseCheckboxWrapper3, { className, children: [
786
+ /* @__PURE__ */ jsx8(
787
+ BaseCheckbox3,
788
+ {
789
+ type: "checkbox",
790
+ id: "editable-annotations",
791
+ checked,
792
+ onChange: (e) => onChange(e.target.checked)
793
+ }
794
+ ),
795
+ /* @__PURE__ */ jsx8(BaseCheckboxLabel3, { htmlFor: "editable-annotations", children: "Editable Annotations" })
796
+ ] });
797
+ };
798
+
799
+ // src/components/DownloadAnnotationsButton.tsx
800
+ import styled6 from "styled-components";
801
+ import { jsx as jsx9 } from "react/jsx-runtime";
802
+ var StyledButton = styled6.button`
803
+ padding: 0.5rem 1rem;
804
+ background: ${(props) => props.theme?.surfaceColor || "#f5f5f5"};
805
+ color: ${(props) => props.theme?.textColor || "#333"};
806
+ border: 1px solid ${(props) => props.theme?.borderColor || "#ccc"};
807
+ border-radius: ${(props) => props.theme?.borderRadius || "4px"};
808
+ cursor: pointer;
809
+ font-family: ${(props) => props.theme?.fontFamily || "inherit"};
810
+ font-size: ${(props) => props.theme?.fontSize || "14px"};
811
+ font-weight: 500;
812
+ transition: all 0.15s ease;
813
+
814
+ &:hover:not(:disabled) {
815
+ background: ${(props) => props.theme?.inputBackground || "#3d3d3d"};
816
+ border-color: ${(props) => props.theme?.textColorMuted || "#999"};
817
+ }
818
+
819
+ &:focus {
820
+ outline: none;
821
+ box-shadow: 0 0 0 2px ${(props) => props.theme?.inputFocusBorder || "#007bff"}44;
822
+ }
823
+
824
+ &:disabled {
825
+ opacity: 0.6;
826
+ cursor: not-allowed;
827
+ }
828
+ `;
829
+ var DownloadAnnotationsButton = ({
830
+ annotations,
831
+ filename = "annotations.json",
832
+ disabled = false,
833
+ className,
834
+ children = "Download JSON"
835
+ }) => {
836
+ const handleDownload = () => {
837
+ if (annotations.length === 0) {
838
+ return;
839
+ }
840
+ const jsonData = annotations.map((annotation) => serializeAeneas(annotation));
841
+ const jsonString = JSON.stringify(jsonData, null, 2);
842
+ const blob = new Blob([jsonString], { type: "application/json" });
843
+ const url = URL.createObjectURL(blob);
844
+ const link = document.createElement("a");
845
+ link.href = url;
846
+ link.download = filename;
847
+ document.body.appendChild(link);
848
+ link.click();
849
+ document.body.removeChild(link);
850
+ URL.revokeObjectURL(url);
851
+ };
852
+ return /* @__PURE__ */ jsx9(
853
+ StyledButton,
854
+ {
855
+ onClick: handleDownload,
856
+ disabled: disabled || annotations.length === 0,
857
+ className,
858
+ title: annotations.length === 0 ? "No annotations to download" : "Download the annotations as JSON",
859
+ children
860
+ }
861
+ );
862
+ };
863
+
864
+ // src/hooks/useAnnotationControls.ts
865
+ import { useState as useState2, useCallback } from "react";
866
+ var LINK_THRESHOLD = 0.01;
867
+ var useAnnotationControls = (options = {}) => {
868
+ const {
869
+ initialContinuousPlay = false,
870
+ initialLinkEndpoints = true
871
+ } = options;
872
+ const [continuousPlay, setContinuousPlay] = useState2(initialContinuousPlay);
873
+ const [linkEndpoints, setLinkEndpoints] = useState2(initialLinkEndpoints);
874
+ const updateAnnotationBoundaries = useCallback(
875
+ ({
876
+ annotationIndex,
877
+ newTime,
878
+ isDraggingStart,
879
+ annotations,
880
+ duration,
881
+ linkEndpoints: shouldLinkEndpoints
882
+ }) => {
883
+ const updatedAnnotations = [...annotations];
884
+ const annotation = annotations[annotationIndex];
885
+ if (isDraggingStart) {
886
+ const constrainedStart = Math.min(annotation.end - 0.1, Math.max(0, newTime));
887
+ const delta = constrainedStart - annotation.start;
888
+ updatedAnnotations[annotationIndex] = {
889
+ ...annotation,
890
+ start: constrainedStart
891
+ };
892
+ if (shouldLinkEndpoints && annotationIndex > 0) {
893
+ const prevAnnotation = updatedAnnotations[annotationIndex - 1];
894
+ if (Math.abs(prevAnnotation.end - annotation.start) < LINK_THRESHOLD) {
895
+ updatedAnnotations[annotationIndex - 1] = {
896
+ ...prevAnnotation,
897
+ end: Math.max(prevAnnotation.start + 0.1, prevAnnotation.end + delta)
898
+ };
899
+ } else if (constrainedStart <= prevAnnotation.end) {
900
+ updatedAnnotations[annotationIndex] = {
901
+ ...updatedAnnotations[annotationIndex],
902
+ start: prevAnnotation.end
903
+ };
904
+ }
905
+ } else if (!shouldLinkEndpoints && annotationIndex > 0 && constrainedStart < updatedAnnotations[annotationIndex - 1].end) {
906
+ updatedAnnotations[annotationIndex - 1] = {
907
+ ...updatedAnnotations[annotationIndex - 1],
908
+ end: constrainedStart
909
+ };
910
+ }
911
+ } else {
912
+ const constrainedEnd = Math.max(annotation.start + 0.1, Math.min(newTime, duration));
913
+ const delta = constrainedEnd - annotation.end;
914
+ updatedAnnotations[annotationIndex] = {
915
+ ...annotation,
916
+ end: constrainedEnd
917
+ };
918
+ if (shouldLinkEndpoints && annotationIndex < updatedAnnotations.length - 1) {
919
+ const nextAnnotation = updatedAnnotations[annotationIndex + 1];
920
+ if (Math.abs(nextAnnotation.start - annotation.end) < LINK_THRESHOLD) {
921
+ const newStart = nextAnnotation.start + delta;
922
+ updatedAnnotations[annotationIndex + 1] = {
923
+ ...nextAnnotation,
924
+ start: Math.min(nextAnnotation.end - 0.1, newStart)
925
+ };
926
+ let currentIndex = annotationIndex + 1;
927
+ while (currentIndex < updatedAnnotations.length - 1) {
928
+ const current = updatedAnnotations[currentIndex];
929
+ const next = updatedAnnotations[currentIndex + 1];
930
+ if (Math.abs(next.start - current.end) < LINK_THRESHOLD) {
931
+ const nextDelta = current.end - annotations[currentIndex].end;
932
+ updatedAnnotations[currentIndex + 1] = {
933
+ ...next,
934
+ start: Math.min(next.end - 0.1, next.start + nextDelta)
935
+ };
936
+ currentIndex++;
937
+ } else {
938
+ break;
939
+ }
940
+ }
941
+ } else if (constrainedEnd >= nextAnnotation.start) {
942
+ updatedAnnotations[annotationIndex] = {
943
+ ...updatedAnnotations[annotationIndex],
944
+ end: nextAnnotation.start
945
+ };
946
+ }
947
+ } else if (!shouldLinkEndpoints && annotationIndex < updatedAnnotations.length - 1 && constrainedEnd > updatedAnnotations[annotationIndex + 1].start) {
948
+ const nextAnnotation = updatedAnnotations[annotationIndex + 1];
949
+ updatedAnnotations[annotationIndex + 1] = {
950
+ ...nextAnnotation,
951
+ start: constrainedEnd
952
+ };
953
+ let currentIndex = annotationIndex + 1;
954
+ while (currentIndex < updatedAnnotations.length - 1) {
955
+ const current = updatedAnnotations[currentIndex];
956
+ const next = updatedAnnotations[currentIndex + 1];
957
+ if (current.end > next.start) {
958
+ updatedAnnotations[currentIndex + 1] = {
959
+ ...next,
960
+ start: current.end
961
+ };
962
+ currentIndex++;
963
+ } else {
964
+ break;
965
+ }
966
+ }
967
+ }
968
+ }
969
+ return updatedAnnotations;
970
+ },
971
+ []
972
+ );
973
+ return {
974
+ continuousPlay,
975
+ linkEndpoints,
976
+ setContinuousPlay,
977
+ setLinkEndpoints,
978
+ updateAnnotationBoundaries
979
+ };
980
+ };
981
+ export {
982
+ Annotation,
983
+ AnnotationBox,
984
+ AnnotationBoxesWrapper,
985
+ AnnotationText2 as AnnotationText,
986
+ AnnotationsTrack,
987
+ ContinuousPlayCheckbox,
988
+ DownloadAnnotationsButton,
989
+ EditableCheckbox,
990
+ LinkEndpointsCheckbox,
991
+ parseAeneas,
992
+ serializeAeneas,
993
+ useAnnotationControls
994
+ };
995
+ //# sourceMappingURL=index.mjs.map