@mieweb/ui 0.6.1-dev.169 → 0.6.1-dev.171

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. package/dist/brands/index.cjs +18 -18
  2. package/dist/brands/index.js +3 -3
  3. package/dist/chunk-3Q2VWPML.cjs +1802 -0
  4. package/dist/chunk-3Q2VWPML.cjs.map +1 -0
  5. package/dist/{chunk-6W3HLFAJ.cjs → chunk-3YRDZBW6.cjs} +3 -3
  6. package/dist/{chunk-6W3HLFAJ.cjs.map → chunk-3YRDZBW6.cjs.map} +1 -1
  7. package/dist/{chunk-UVQEHZRF.js → chunk-6VGXX6L6.js} +2 -2
  8. package/dist/{chunk-UVQEHZRF.js.map → chunk-6VGXX6L6.js.map} +1 -1
  9. package/dist/chunk-CH26CHPH.js +1776 -0
  10. package/dist/chunk-CH26CHPH.js.map +1 -0
  11. package/dist/{chunk-TXRQQMG5.js → chunk-CYMZ2QS5.js} +2 -2
  12. package/dist/{chunk-TXRQQMG5.js.map → chunk-CYMZ2QS5.js.map} +1 -1
  13. package/dist/chunk-FGP64Z24.js +131 -0
  14. package/dist/chunk-FGP64Z24.js.map +1 -0
  15. package/dist/chunk-FIWKHLWY.js +197 -0
  16. package/dist/chunk-FIWKHLWY.js.map +1 -0
  17. package/dist/chunk-GLXTVCHN.cjs +4 -0
  18. package/dist/chunk-GLXTVCHN.cjs.map +1 -0
  19. package/dist/chunk-J7KWQJOB.js +14 -0
  20. package/dist/chunk-J7KWQJOB.js.map +1 -0
  21. package/dist/chunk-JYZW2PZS.cjs +166 -0
  22. package/dist/chunk-JYZW2PZS.cjs.map +1 -0
  23. package/dist/{chunk-XVF472GT.js → chunk-NSLR3B7K.js} +2 -2
  24. package/dist/{chunk-XVF472GT.js.map → chunk-NSLR3B7K.js.map} +1 -1
  25. package/dist/chunk-NTYGCBCL.js +543 -0
  26. package/dist/chunk-NTYGCBCL.js.map +1 -0
  27. package/dist/chunk-RISZWWEQ.js +3 -0
  28. package/dist/chunk-RISZWWEQ.js.map +1 -0
  29. package/dist/chunk-S7JQ3FV5.cjs +16 -0
  30. package/dist/chunk-S7JQ3FV5.cjs.map +1 -0
  31. package/dist/{chunk-JWTCEWQ4.cjs → chunk-SNEQXSC5.cjs} +2 -2
  32. package/dist/{chunk-JWTCEWQ4.cjs.map → chunk-SNEQXSC5.cjs.map} +1 -1
  33. package/dist/chunk-SQHMGT4W.js +164 -0
  34. package/dist/chunk-SQHMGT4W.js.map +1 -0
  35. package/dist/{chunk-JHPFOG2N.cjs → chunk-ST2RE5UB.cjs} +30 -18
  36. package/dist/chunk-ST2RE5UB.cjs.map +1 -0
  37. package/dist/chunk-UO3KEUYK.cjs +220 -0
  38. package/dist/chunk-UO3KEUYK.cjs.map +1 -0
  39. package/dist/{chunk-FETRH4V5.js → chunk-X763OLF2.js} +3 -3
  40. package/dist/{chunk-FETRH4V5.js.map → chunk-X763OLF2.js.map} +1 -1
  41. package/dist/{chunk-DKGUOMBC.cjs → chunk-X7J45FU5.cjs} +2 -2
  42. package/dist/{chunk-DKGUOMBC.cjs.map → chunk-X7J45FU5.cjs.map} +1 -1
  43. package/dist/{chunk-I56VBDSC.js → chunk-XDFDVF2O.js} +30 -18
  44. package/dist/chunk-XDFDVF2O.js.map +1 -0
  45. package/dist/chunk-Y4ESBDQ5.cjs +552 -0
  46. package/dist/chunk-Y4ESBDQ5.cjs.map +1 -0
  47. package/dist/{chunk-MJ7YITLN.cjs → chunk-Y65SK5Y2.cjs} +2 -2
  48. package/dist/{chunk-MJ7YITLN.cjs.map → chunk-Y65SK5Y2.cjs.map} +1 -1
  49. package/dist/chunk-YD23ZGRU.cjs +155 -0
  50. package/dist/chunk-YD23ZGRU.cjs.map +1 -0
  51. package/dist/components/Markdown/index.cjs +10 -10
  52. package/dist/components/Markdown/index.js +2 -2
  53. package/dist/components/MediaEditor/index.cjs +42 -0
  54. package/dist/components/MediaEditor/index.cjs.map +1 -0
  55. package/dist/components/MediaEditor/index.d.cts +151 -0
  56. package/dist/components/MediaEditor/index.d.ts +151 -0
  57. package/dist/components/MediaEditor/index.js +17 -0
  58. package/dist/components/MediaEditor/index.js.map +1 -0
  59. package/dist/components/MediaPlayer/index.cjs +24 -0
  60. package/dist/components/MediaPlayer/index.cjs.map +1 -0
  61. package/dist/components/MediaPlayer/index.d.cts +71 -0
  62. package/dist/components/MediaPlayer/index.d.ts +71 -0
  63. package/dist/components/MediaPlayer/index.js +7 -0
  64. package/dist/components/MediaPlayer/index.js.map +1 -0
  65. package/dist/components/ThemeProvider/index.cjs +7 -7
  66. package/dist/components/ThemeProvider/index.js +1 -1
  67. package/dist/components/TranscriptView/index.cjs +21 -0
  68. package/dist/components/TranscriptView/index.cjs.map +1 -0
  69. package/dist/components/TranscriptView/index.d.cts +55 -0
  70. package/dist/components/TranscriptView/index.d.ts +55 -0
  71. package/dist/components/TranscriptView/index.js +4 -0
  72. package/dist/components/TranscriptView/index.js.map +1 -0
  73. package/dist/esheet.cjs +5 -5
  74. package/dist/esheet.js +2 -2
  75. package/dist/hooks/index.cjs +49 -11
  76. package/dist/hooks/index.d.cts +88 -1
  77. package/dist/hooks/index.d.ts +88 -1
  78. package/dist/hooks/index.js +3 -1
  79. package/dist/index.cjs +380 -253
  80. package/dist/index.cjs.map +1 -1
  81. package/dist/index.d.cts +6 -1
  82. package/dist/index.d.ts +6 -1
  83. package/dist/index.js +87 -40
  84. package/dist/index.js.map +1 -1
  85. package/dist/styles.css +1 -1
  86. package/dist/transcript-oaWqY3yr.d.cts +81 -0
  87. package/dist/transcript-oaWqY3yr.d.ts +81 -0
  88. package/dist/useTranscriptEdits-BUylQFg0.d.cts +168 -0
  89. package/dist/useTranscriptEdits-CWYnjbGP.d.ts +168 -0
  90. package/package.json +1 -1
  91. package/dist/chunk-I56VBDSC.js.map +0 -1
  92. package/dist/chunk-JHPFOG2N.cjs.map +0 -1
@@ -0,0 +1,1802 @@
1
+ 'use strict';
2
+
3
+ var chunkS7JQ3FV5_cjs = require('./chunk-S7JQ3FV5.cjs');
4
+ var chunkGOKC4CCZ_cjs = require('./chunk-GOKC4CCZ.cjs');
5
+ var chunkYD23ZGRU_cjs = require('./chunk-YD23ZGRU.cjs');
6
+ var chunkDFCZPVG4_cjs = require('./chunk-DFCZPVG4.cjs');
7
+ var chunkFRT6RQND_cjs = require('./chunk-FRT6RQND.cjs');
8
+ var chunkKJPOFUF5_cjs = require('./chunk-KJPOFUF5.cjs');
9
+ var chunkMSRU3XBH_cjs = require('./chunk-MSRU3XBH.cjs');
10
+ var chunkA2QVQF54_cjs = require('./chunk-A2QVQF54.cjs');
11
+ var chunkY4ESBDQ5_cjs = require('./chunk-Y4ESBDQ5.cjs');
12
+ var React6 = require('react');
13
+ var classVarianceAuthority = require('class-variance-authority');
14
+ var jsxRuntime = require('react/jsx-runtime');
15
+
16
+ function _interopNamespace(e) {
17
+ if (e && e.__esModule) return e;
18
+ var n = Object.create(null);
19
+ if (e) {
20
+ Object.keys(e).forEach(function (k) {
21
+ if (k !== 'default') {
22
+ var d = Object.getOwnPropertyDescriptor(e, k);
23
+ Object.defineProperty(n, k, d.get ? d : {
24
+ enumerable: true,
25
+ get: function () { return e[k]; }
26
+ });
27
+ }
28
+ });
29
+ }
30
+ n.default = e;
31
+ return Object.freeze(n);
32
+ }
33
+
34
+ var React6__namespace = /*#__PURE__*/_interopNamespace(React6);
35
+
36
+ var SILENCE_THRESHOLD_OPTIONS = [0.3, 0.4, 0.5, 0.75, 1, 1.5, 2];
37
+ function formatDuration(ms) {
38
+ const totalSeconds = Math.round(ms / 1e3);
39
+ if (totalSeconds < 90) {
40
+ return `${totalSeconds}s`;
41
+ }
42
+ const minutes = Math.floor(totalSeconds / 60);
43
+ const seconds = totalSeconds % 60;
44
+ return `${minutes}:${String(seconds).padStart(2, "0")}`;
45
+ }
46
+ var FillerWordsModal = ({
47
+ isOpen,
48
+ onClose,
49
+ onApply,
50
+ matchingCounts,
51
+ fillerDurations,
52
+ silenceCounts,
53
+ currentDurationMs
54
+ }) => {
55
+ const [searchTerm, setSearchTerm] = React6__namespace.useState("");
56
+ const [selectedFillers, setSelectedFillers] = React6__namespace.useState(
57
+ new Set(chunkY4ESBDQ5_cjs.DEFAULT_FILLER_WORDS)
58
+ );
59
+ const [customWord, setCustomWord] = React6__namespace.useState("");
60
+ const [removeSilence, setRemoveSilence] = React6__namespace.useState(false);
61
+ const [silenceThreshold, setSilenceThreshold] = React6__namespace.useState(0.4);
62
+ const filteredFillers = chunkY4ESBDQ5_cjs.DEFAULT_FILLER_WORDS.filter(
63
+ (word) => word.toLowerCase().includes(searchTerm.toLowerCase())
64
+ ).sort((a, b) => {
65
+ const countA = matchingCounts.get(a) || 0;
66
+ const countB = matchingCounts.get(b) || 0;
67
+ if (countB !== countA) return countB - countA;
68
+ return a.localeCompare(b);
69
+ });
70
+ const customFillers = Array.from(selectedFillers).filter((word) => !chunkY4ESBDQ5_cjs.DEFAULT_FILLER_WORDS.includes(word)).filter((word) => word.toLowerCase().includes(searchTerm.toLowerCase())).sort((a, b) => {
71
+ const countA = matchingCounts.get(a) || 0;
72
+ const countB = matchingCounts.get(b) || 0;
73
+ if (countB !== countA) return countB - countA;
74
+ return a.localeCompare(b);
75
+ });
76
+ const toggleFiller = (word) => {
77
+ setSelectedFillers((prev) => {
78
+ const next = new Set(prev);
79
+ if (next.has(word)) {
80
+ next.delete(word);
81
+ } else {
82
+ next.add(word);
83
+ }
84
+ return next;
85
+ });
86
+ };
87
+ const handleAddCustom = () => {
88
+ const trimmed = customWord.trim().toLowerCase();
89
+ if (trimmed && !selectedFillers.has(trimmed)) {
90
+ setSelectedFillers((prev) => /* @__PURE__ */ new Set([...prev, trimmed]));
91
+ setCustomWord("");
92
+ }
93
+ };
94
+ const silenceData = removeSilence ? silenceCounts.find((s) => s.threshold === silenceThreshold) || {
95
+ count: 0,
96
+ durationMs: 0
97
+ } : { count: 0, durationMs: 0 };
98
+ const silenceRemovalCount = silenceData.count;
99
+ const silenceDurationMs = silenceData.durationMs;
100
+ const handleApply = () => {
101
+ onApply(
102
+ Array.from(selectedFillers),
103
+ removeSilence ? silenceThreshold : null
104
+ );
105
+ onClose();
106
+ };
107
+ const selectAll = () => {
108
+ setSelectedFillers(/* @__PURE__ */ new Set([...chunkY4ESBDQ5_cjs.DEFAULT_FILLER_WORDS, ...customFillers]));
109
+ };
110
+ const selectNone = () => {
111
+ setSelectedFillers(/* @__PURE__ */ new Set());
112
+ };
113
+ const fillerMatches = Array.from(selectedFillers).reduce(
114
+ (sum, word) => sum + (matchingCounts.get(word) || 0),
115
+ 0
116
+ );
117
+ const totalMatches = fillerMatches + silenceRemovalCount;
118
+ const fillerTimeSavedMs = Array.from(selectedFillers).reduce(
119
+ (sum, word) => sum + (fillerDurations.get(word) || 0),
120
+ 0
121
+ );
122
+ const totalTimeSavedMs = fillerTimeSavedMs + silenceDurationMs;
123
+ const newDurationMs = currentDurationMs - totalTimeSavedMs;
124
+ const renderFillerItem = (word, isCustom) => {
125
+ const count = matchingCounts.get(word) || 0;
126
+ return /* @__PURE__ */ jsxRuntime.jsxs(
127
+ "label",
128
+ {
129
+ className: "hover:bg-muted flex cursor-pointer items-center gap-2 rounded-md px-1.5 py-1 text-sm transition-colors",
130
+ children: [
131
+ /* @__PURE__ */ jsxRuntime.jsx(
132
+ chunkDFCZPVG4_cjs.Checkbox,
133
+ {
134
+ size: "sm",
135
+ checked: selectedFillers.has(word),
136
+ onChange: () => toggleFiller(word),
137
+ "aria-label": `Remove "${word}"`
138
+ }
139
+ ),
140
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-foreground min-w-0 flex-1 truncate", children: word }),
141
+ count > 0 && /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-muted-foreground text-xs", children: [
142
+ "(",
143
+ count,
144
+ ")"
145
+ ] }),
146
+ isCustom && /* @__PURE__ */ jsxRuntime.jsx(
147
+ "button",
148
+ {
149
+ type: "button",
150
+ className: "text-muted-foreground hover:bg-destructive/10 hover:text-destructive focus-visible:ring-ring rounded px-1 transition-colors focus-visible:ring-2 focus-visible:outline-none",
151
+ onClick: (e) => {
152
+ e.preventDefault();
153
+ setSelectedFillers((prev) => {
154
+ const next = new Set(prev);
155
+ next.delete(word);
156
+ return next;
157
+ });
158
+ },
159
+ "aria-label": `Remove ${word}`,
160
+ children: "\xD7"
161
+ }
162
+ )
163
+ ]
164
+ },
165
+ word
166
+ );
167
+ };
168
+ return /* @__PURE__ */ jsxRuntime.jsxs(
169
+ chunkKJPOFUF5_cjs.Modal,
170
+ {
171
+ open: isOpen,
172
+ onOpenChange: (open) => {
173
+ if (!open) onClose();
174
+ },
175
+ size: "lg",
176
+ children: [
177
+ /* @__PURE__ */ jsxRuntime.jsxs(chunkKJPOFUF5_cjs.ModalHeader, { children: [
178
+ /* @__PURE__ */ jsxRuntime.jsx(chunkKJPOFUF5_cjs.ModalTitle, { children: "Remove Filler Words" }),
179
+ /* @__PURE__ */ jsxRuntime.jsx(chunkKJPOFUF5_cjs.ModalClose, { "aria-label": "Close modal" })
180
+ ] }),
181
+ /* @__PURE__ */ jsxRuntime.jsxs(chunkKJPOFUF5_cjs.ModalBody, { className: "flex flex-col gap-3", children: [
182
+ /* @__PURE__ */ jsxRuntime.jsx(
183
+ chunkMSRU3XBH_cjs.Input,
184
+ {
185
+ size: "sm",
186
+ type: "text",
187
+ placeholder: "Search filler words...",
188
+ value: searchTerm,
189
+ onChange: (e) => setSearchTerm(e.target.value),
190
+ "aria-label": "Search filler words"
191
+ }
192
+ ),
193
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
194
+ /* @__PURE__ */ jsxRuntime.jsx(chunkA2QVQF54_cjs.Button, { variant: "ghost", size: "sm", onClick: selectAll, children: "Select All" }),
195
+ /* @__PURE__ */ jsxRuntime.jsx(chunkA2QVQF54_cjs.Button, { variant: "ghost", size: "sm", onClick: selectNone, children: "Select None" })
196
+ ] }),
197
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "border-border bg-muted/40 flex flex-wrap items-center gap-2 rounded-lg border px-3 py-2", children: [
198
+ /* @__PURE__ */ jsxRuntime.jsx(
199
+ chunkDFCZPVG4_cjs.Checkbox,
200
+ {
201
+ size: "sm",
202
+ label: "Remove silences greater than",
203
+ checked: removeSilence,
204
+ onChange: (e) => setRemoveSilence(e.target.checked)
205
+ }
206
+ ),
207
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-24", children: /* @__PURE__ */ jsxRuntime.jsx(
208
+ chunkFRT6RQND_cjs.Select,
209
+ {
210
+ size: "sm",
211
+ options: SILENCE_THRESHOLD_OPTIONS.map((t) => ({
212
+ value: String(t),
213
+ label: `${t}s`
214
+ })),
215
+ value: String(silenceThreshold),
216
+ onValueChange: (value) => setSilenceThreshold(parseFloat(value)),
217
+ disabled: !removeSilence,
218
+ "aria-label": "Silence threshold in seconds"
219
+ }
220
+ ) }),
221
+ removeSilence && silenceRemovalCount > 0 && /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-muted-foreground text-xs", children: [
222
+ "(",
223
+ silenceRemovalCount,
224
+ " silences)"
225
+ ] })
226
+ ] }),
227
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "border-border grid max-h-56 grid-cols-2 gap-x-3 gap-y-0.5 overflow-y-auto rounded-lg border p-2 sm:grid-cols-3", children: [
228
+ filteredFillers.map((word) => renderFillerItem(word, false)),
229
+ customFillers.map((word) => renderFillerItem(word, true))
230
+ ] }),
231
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
232
+ /* @__PURE__ */ jsxRuntime.jsx(
233
+ chunkMSRU3XBH_cjs.Input,
234
+ {
235
+ size: "sm",
236
+ type: "text",
237
+ placeholder: "Add custom filler word...",
238
+ value: customWord,
239
+ onChange: (e) => setCustomWord(e.target.value),
240
+ onKeyDown: (e) => {
241
+ if (e.key === "Enter") {
242
+ e.preventDefault();
243
+ handleAddCustom();
244
+ }
245
+ },
246
+ "aria-label": "Add custom filler word"
247
+ }
248
+ ),
249
+ /* @__PURE__ */ jsxRuntime.jsx(
250
+ chunkA2QVQF54_cjs.Button,
251
+ {
252
+ variant: "secondary",
253
+ size: "sm",
254
+ onClick: handleAddCustom,
255
+ disabled: !customWord.trim(),
256
+ children: "Add"
257
+ }
258
+ )
259
+ ] })
260
+ ] }),
261
+ /* @__PURE__ */ jsxRuntime.jsxs(chunkKJPOFUF5_cjs.ModalFooter, { className: "justify-between", children: [
262
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-muted-foreground text-sm", children: [
263
+ totalMatches,
264
+ " items \u2022 saves ",
265
+ formatDuration(totalTimeSavedMs),
266
+ " \u2192",
267
+ " ",
268
+ formatDuration(newDurationMs)
269
+ ] }),
270
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
271
+ /* @__PURE__ */ jsxRuntime.jsx(chunkA2QVQF54_cjs.Button, { variant: "secondary", size: "sm", onClick: onClose, children: "Cancel" }),
272
+ /* @__PURE__ */ jsxRuntime.jsxs(
273
+ chunkA2QVQF54_cjs.Button,
274
+ {
275
+ variant: "primary",
276
+ size: "sm",
277
+ onClick: handleApply,
278
+ disabled: totalMatches === 0,
279
+ children: [
280
+ "Mark as Deleted (",
281
+ totalMatches,
282
+ ")"
283
+ ]
284
+ }
285
+ )
286
+ ] })
287
+ ] })
288
+ ]
289
+ }
290
+ );
291
+ };
292
+ FillerWordsModal.displayName = "FillerWordsModal";
293
+ var WordEditorModal = ({
294
+ isOpen,
295
+ editableWord,
296
+ onClose,
297
+ onSave,
298
+ onSplitSilence,
299
+ onDelete
300
+ }) => {
301
+ const [inputValue, setInputValue] = React6__namespace.useState("");
302
+ const [error, setError] = React6__namespace.useState(null);
303
+ const inputRef = React6__namespace.useRef(null);
304
+ const isSilence = editableWord?.word.wordType === "silence";
305
+ const durationMs = editableWord ? editableWord.word.endMs - editableWord.word.startMs : 0;
306
+ const durationSec = durationMs / 1e3;
307
+ React6__namespace.useEffect(() => {
308
+ if (isOpen && editableWord) {
309
+ if (isSilence) {
310
+ setInputValue("");
311
+ } else {
312
+ setInputValue(editableWord.word.text);
313
+ }
314
+ setError(null);
315
+ setTimeout(() => inputRef.current?.focus(), 50);
316
+ }
317
+ }, [isOpen, editableWord, isSilence]);
318
+ const parseSilenceSplit = (input) => {
319
+ const trimmed = input.trim();
320
+ if (!trimmed) return null;
321
+ const parts = trimmed.split(/\s+/);
322
+ const durations = [];
323
+ let total = 0;
324
+ for (const part of parts) {
325
+ const num = parseFloat(part);
326
+ if (isNaN(num) || num <= 0) {
327
+ return {
328
+ durations: [],
329
+ remainder: 0,
330
+ error: `Invalid number: "${part}"`
331
+ };
332
+ }
333
+ durations.push(num);
334
+ total += num;
335
+ }
336
+ if (total >= durationSec) {
337
+ return {
338
+ durations: [],
339
+ remainder: 0,
340
+ error: `Total (${total.toFixed(1)}s) exceeds silence duration (${durationSec.toFixed(1)}s)`
341
+ };
342
+ }
343
+ const remainder = durationSec - total;
344
+ return { durations, remainder };
345
+ };
346
+ const handleApply = () => {
347
+ if (isSilence) {
348
+ const parsed = parseSilenceSplit(inputValue);
349
+ if (parsed?.error) {
350
+ setError(parsed.error);
351
+ return;
352
+ }
353
+ if (parsed && parsed.durations.length > 0) {
354
+ const allDurations = [...parsed.durations, parsed.remainder];
355
+ onSplitSilence(allDurations);
356
+ onClose();
357
+ }
358
+ } else {
359
+ const trimmed = inputValue.trim();
360
+ if (trimmed && trimmed !== editableWord?.word.text) {
361
+ onSave(trimmed);
362
+ }
363
+ onClose();
364
+ }
365
+ };
366
+ const handleKeyDown = (e) => {
367
+ if (e.key === "Enter") {
368
+ e.preventDefault();
369
+ handleApply();
370
+ }
371
+ };
372
+ const preview = isSilence ? parseSilenceSplit(inputValue) : null;
373
+ const applyDisabled = isSilence ? !inputValue.trim() || !!preview?.error || !preview || preview.durations.length === 0 : !inputValue.trim();
374
+ return /* @__PURE__ */ jsxRuntime.jsxs(
375
+ chunkKJPOFUF5_cjs.Modal,
376
+ {
377
+ open: isOpen && editableWord !== null,
378
+ onOpenChange: (open) => {
379
+ if (!open) onClose();
380
+ },
381
+ size: "md",
382
+ children: [
383
+ /* @__PURE__ */ jsxRuntime.jsxs(chunkKJPOFUF5_cjs.ModalHeader, { children: [
384
+ /* @__PURE__ */ jsxRuntime.jsx(chunkKJPOFUF5_cjs.ModalTitle, { children: isSilence ? "Split Silence" : "Edit Word" }),
385
+ /* @__PURE__ */ jsxRuntime.jsx(chunkKJPOFUF5_cjs.ModalClose, { "aria-label": "Close editor" })
386
+ ] }),
387
+ /* @__PURE__ */ jsxRuntime.jsx(chunkKJPOFUF5_cjs.ModalBody, { className: "flex flex-col gap-2", children: isSilence ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
388
+ /* @__PURE__ */ jsxRuntime.jsxs("p", { className: "text-foreground m-0 text-sm", children: [
389
+ "Silence duration:",
390
+ " ",
391
+ /* @__PURE__ */ jsxRuntime.jsxs("strong", { className: "font-semibold", children: [
392
+ durationSec.toFixed(1),
393
+ "s"
394
+ ] })
395
+ ] }),
396
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-muted-foreground m-0 text-xs", children: 'Enter space-separated durations to split (e.g., "1 1 4 1")' }),
397
+ /* @__PURE__ */ jsxRuntime.jsx(
398
+ chunkMSRU3XBH_cjs.Input,
399
+ {
400
+ ref: inputRef,
401
+ type: "text",
402
+ value: inputValue,
403
+ onChange: (e) => {
404
+ setInputValue(e.target.value);
405
+ setError(null);
406
+ },
407
+ onKeyDown: handleKeyDown,
408
+ placeholder: "1 2 3...",
409
+ "aria-label": "Split durations"
410
+ }
411
+ ),
412
+ error && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-destructive m-0 text-sm", role: "alert", children: error }),
413
+ preview && !preview.error && preview.durations.length > 0 && !error && /* @__PURE__ */ jsxRuntime.jsxs("p", { className: "text-muted-foreground m-0 text-sm", children: [
414
+ "Preview:",
415
+ " ",
416
+ [...preview.durations, preview.remainder].map((d) => `[${d.toFixed(1)}s]`).join(" ")
417
+ ] }),
418
+ preview?.error && !error && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-destructive m-0 text-sm", role: "alert", children: preview.error })
419
+ ] }) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
420
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-foreground m-0 text-sm", children: "Edit the word text:" }),
421
+ /* @__PURE__ */ jsxRuntime.jsx(
422
+ chunkMSRU3XBH_cjs.Input,
423
+ {
424
+ ref: inputRef,
425
+ type: "text",
426
+ value: inputValue,
427
+ onChange: (e) => setInputValue(e.target.value),
428
+ onKeyDown: handleKeyDown,
429
+ "aria-label": "Word text"
430
+ }
431
+ )
432
+ ] }) }),
433
+ /* @__PURE__ */ jsxRuntime.jsxs(chunkKJPOFUF5_cjs.ModalFooter, { className: "justify-between", children: [
434
+ /* @__PURE__ */ jsxRuntime.jsx(
435
+ chunkA2QVQF54_cjs.Button,
436
+ {
437
+ variant: editableWord?.deleted ? "secondary" : "danger",
438
+ size: "sm",
439
+ onClick: onDelete,
440
+ children: editableWord?.deleted ? "Restore" : "Delete"
441
+ }
442
+ ),
443
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
444
+ /* @__PURE__ */ jsxRuntime.jsx(chunkA2QVQF54_cjs.Button, { variant: "secondary", size: "sm", onClick: onClose, children: "Cancel" }),
445
+ /* @__PURE__ */ jsxRuntime.jsx(
446
+ chunkA2QVQF54_cjs.Button,
447
+ {
448
+ variant: "primary",
449
+ size: "sm",
450
+ onClick: handleApply,
451
+ disabled: applyDisabled,
452
+ children: isSilence ? "Split" : "Save"
453
+ }
454
+ )
455
+ ] })
456
+ ] })
457
+ ]
458
+ }
459
+ );
460
+ };
461
+ WordEditorModal.displayName = "WordEditorModal";
462
+ var SilenceSettingsModal = ({
463
+ isOpen,
464
+ onClose,
465
+ minSilenceMs,
466
+ nlSilenceMs,
467
+ onApply
468
+ }) => {
469
+ const [threshold, setThreshold] = React6__namespace.useState(minSilenceMs / 1e3);
470
+ const [nlThreshold, setNlThreshold] = React6__namespace.useState(nlSilenceMs / 1e3);
471
+ React6__namespace.useEffect(() => {
472
+ if (isOpen) {
473
+ setThreshold(minSilenceMs / 1e3);
474
+ setNlThreshold(nlSilenceMs / 1e3);
475
+ }
476
+ }, [isOpen, minSilenceMs, nlSilenceMs]);
477
+ const handleApply = () => {
478
+ const finalNlThreshold = Math.max(nlThreshold, threshold);
479
+ onApply(Math.round(threshold * 1e3), Math.round(finalNlThreshold * 1e3));
480
+ onClose();
481
+ };
482
+ const handleThresholdChange = (value) => {
483
+ setThreshold(value);
484
+ if (nlThreshold < value) {
485
+ setNlThreshold(value);
486
+ }
487
+ };
488
+ const formatSeconds = (v) => `${v.toFixed(1)}s`;
489
+ return /* @__PURE__ */ jsxRuntime.jsxs(
490
+ chunkKJPOFUF5_cjs.Modal,
491
+ {
492
+ open: isOpen,
493
+ onOpenChange: (open) => {
494
+ if (!open) onClose();
495
+ },
496
+ size: "md",
497
+ children: [
498
+ /* @__PURE__ */ jsxRuntime.jsxs(chunkKJPOFUF5_cjs.ModalHeader, { children: [
499
+ /* @__PURE__ */ jsxRuntime.jsx(chunkKJPOFUF5_cjs.ModalTitle, { children: "Silence Settings" }),
500
+ /* @__PURE__ */ jsxRuntime.jsx(chunkKJPOFUF5_cjs.ModalClose, { "aria-label": "Close modal" })
501
+ ] }),
502
+ /* @__PURE__ */ jsxRuntime.jsxs(chunkKJPOFUF5_cjs.ModalBody, { className: "flex flex-col gap-6", children: [
503
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-1", children: [
504
+ /* @__PURE__ */ jsxRuntime.jsx(
505
+ chunkGOKC4CCZ_cjs.Slider,
506
+ {
507
+ label: "Minimum silence duration",
508
+ min: 0.1,
509
+ max: 2,
510
+ step: 0.1,
511
+ value: threshold,
512
+ onValueChange: handleThresholdChange,
513
+ showValue: true,
514
+ formatValue: formatSeconds,
515
+ "aria-label": "Minimum silence duration"
516
+ }
517
+ ),
518
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-muted-foreground m-0 text-xs", children: "Gaps shorter than this will not be shown as silences." })
519
+ ] }),
520
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-1", children: [
521
+ /* @__PURE__ */ jsxRuntime.jsx(
522
+ chunkGOKC4CCZ_cjs.Slider,
523
+ {
524
+ label: "Paragraph break threshold",
525
+ min: threshold,
526
+ max: 5,
527
+ step: 0.1,
528
+ value: nlThreshold,
529
+ onValueChange: setNlThreshold,
530
+ showValue: true,
531
+ formatValue: formatSeconds,
532
+ "aria-label": "Paragraph break threshold"
533
+ }
534
+ ),
535
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-muted-foreground m-0 text-xs", children: "Silences longer than this will create paragraph breaks." })
536
+ ] })
537
+ ] }),
538
+ /* @__PURE__ */ jsxRuntime.jsxs(chunkKJPOFUF5_cjs.ModalFooter, { children: [
539
+ /* @__PURE__ */ jsxRuntime.jsx(chunkA2QVQF54_cjs.Button, { variant: "secondary", size: "sm", onClick: onClose, children: "Cancel" }),
540
+ /* @__PURE__ */ jsxRuntime.jsx(chunkA2QVQF54_cjs.Button, { variant: "primary", size: "sm", onClick: handleApply, children: "Apply" })
541
+ ] })
542
+ ]
543
+ }
544
+ );
545
+ };
546
+ SilenceSettingsModal.displayName = "SilenceSettingsModal";
547
+ var SpeedMarkerMenu = ({
548
+ isOpen,
549
+ position,
550
+ currentMarker,
551
+ defaultSpeed,
552
+ onSetSpeed,
553
+ onRemoveMarker,
554
+ onClose
555
+ }) => {
556
+ const menuRef = React6__namespace.useRef(null);
557
+ React6__namespace.useEffect(() => {
558
+ const handleKeyDown = (e) => {
559
+ if (e.key === "Escape") onClose();
560
+ };
561
+ const handleClickOutside = (e) => {
562
+ if (menuRef.current && !menuRef.current.contains(e.target)) {
563
+ onClose();
564
+ }
565
+ };
566
+ if (isOpen) {
567
+ document.addEventListener("keydown", handleKeyDown);
568
+ document.addEventListener("mousedown", handleClickOutside);
569
+ return () => {
570
+ document.removeEventListener("keydown", handleKeyDown);
571
+ document.removeEventListener("mousedown", handleClickOutside);
572
+ };
573
+ }
574
+ }, [isOpen, onClose]);
575
+ if (!isOpen || !position) return null;
576
+ return /* @__PURE__ */ jsxRuntime.jsxs(
577
+ "div",
578
+ {
579
+ ref: menuRef,
580
+ className: "border-border bg-card text-card-foreground fixed z-50 min-w-36 overflow-hidden rounded-lg border shadow-lg",
581
+ style: { left: position.x, top: position.y },
582
+ role: "menu",
583
+ "aria-label": "Set speed marker",
584
+ children: [
585
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "border-border text-muted-foreground border-b px-3 py-1.5 text-xs font-semibold", children: "Set Speed Marker" }),
586
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex flex-col p-1", children: chunkS7JQ3FV5_cjs.PLAYBACK_SPEEDS.map((speed) => {
587
+ const isActive = currentMarker?.speed === speed;
588
+ const isDefault = speed === defaultSpeed && !currentMarker;
589
+ return /* @__PURE__ */ jsxRuntime.jsxs(
590
+ "button",
591
+ {
592
+ type: "button",
593
+ className: `focus-visible:ring-ring flex items-center justify-between gap-2 rounded-md px-2.5 py-1 text-left text-sm transition-colors focus-visible:ring-2 focus-visible:outline-none ${isActive ? "bg-primary-600 font-medium text-white" : "text-foreground hover:bg-muted"}`,
594
+ onClick: () => {
595
+ onSetSpeed(speed);
596
+ onClose();
597
+ },
598
+ role: "menuitem",
599
+ children: [
600
+ speed,
601
+ "x",
602
+ isDefault && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-muted-foreground text-xs", children: "default" })
603
+ ]
604
+ },
605
+ speed
606
+ );
607
+ }) }),
608
+ currentMarker && /* @__PURE__ */ jsxRuntime.jsx(
609
+ "button",
610
+ {
611
+ type: "button",
612
+ className: "border-border text-destructive hover:bg-destructive/10 focus-visible:ring-ring w-full border-t px-2.5 py-1.5 text-left text-sm transition-colors focus-visible:ring-2 focus-visible:outline-none",
613
+ onClick: () => {
614
+ onRemoveMarker();
615
+ onClose();
616
+ },
617
+ role: "menuitem",
618
+ children: "Remove Marker"
619
+ }
620
+ )
621
+ ]
622
+ }
623
+ );
624
+ };
625
+ SpeedMarkerMenu.displayName = "SpeedMarkerMenu";
626
+ var yamlPromise = null;
627
+ function loadYaml() {
628
+ yamlPromise ??= import(
629
+ /* @vite-ignore */
630
+ 'js-yaml'
631
+ );
632
+ return yamlPromise;
633
+ }
634
+ async function parseScript(text, format) {
635
+ let data;
636
+ if (format === "json") {
637
+ data = JSON.parse(text);
638
+ } else {
639
+ const yaml = await loadYaml();
640
+ data = yaml.load(text);
641
+ }
642
+ if (!Array.isArray(data)) {
643
+ throw new Error("Script must be a list of word entries.");
644
+ }
645
+ return data.map((raw, i) => {
646
+ const ew = raw;
647
+ const w = ew.word;
648
+ if (!w || typeof w.text !== "string" || typeof w.startMs !== "number" || typeof w.endMs !== "number") {
649
+ throw new Error(
650
+ `Entry ${i + 1}: each entry needs word.text, word.startMs and word.endMs.`
651
+ );
652
+ }
653
+ const word = {
654
+ text: w.text,
655
+ startMs: w.startMs,
656
+ endMs: w.endMs,
657
+ ...typeof w.wordType === "string" ? { wordType: w.wordType } : {},
658
+ ...typeof w.speakerId === "string" ? { speakerId: w.speakerId } : {}
659
+ };
660
+ return {
661
+ originalIndex: typeof ew.originalIndex === "number" ? ew.originalIndex : -1,
662
+ deleted: Boolean(ew.deleted),
663
+ ...ew.inserted !== void 0 ? { inserted: Boolean(ew.inserted) } : {},
664
+ word
665
+ };
666
+ });
667
+ }
668
+ var ScriptPanel = ({
669
+ transcript,
670
+ editedWords,
671
+ onApply,
672
+ onClose,
673
+ className
674
+ }) => {
675
+ const [source, setSource] = React6__namespace.useState("editor");
676
+ const [format, setFormat] = React6__namespace.useState("yaml");
677
+ const [text, setText] = React6__namespace.useState("");
678
+ const [dirty, setDirty] = React6__namespace.useState(false);
679
+ const [error, setError] = React6__namespace.useState(null);
680
+ const readOnly = source === "original";
681
+ React6__namespace.useEffect(() => {
682
+ if (dirty) return;
683
+ let cancelled = false;
684
+ const data = source === "original" ? transcript : editedWords;
685
+ (async () => {
686
+ const serialized = format === "json" ? JSON.stringify(data, null, 2) : (await loadYaml()).dump(data, { lineWidth: 120 });
687
+ if (!cancelled) {
688
+ setText(serialized);
689
+ setError(null);
690
+ }
691
+ })();
692
+ return () => {
693
+ cancelled = true;
694
+ };
695
+ }, [source, format, transcript, editedWords, dirty]);
696
+ const handleApply = async () => {
697
+ try {
698
+ const words = await parseScript(text, format);
699
+ onApply(words);
700
+ setDirty(false);
701
+ setError(null);
702
+ } catch (err) {
703
+ setError(
704
+ err instanceof Error ? err.message : "Could not parse the script."
705
+ );
706
+ }
707
+ };
708
+ const handleReset = () => {
709
+ setDirty(false);
710
+ setError(null);
711
+ };
712
+ const sourceButton = (value, label) => /* @__PURE__ */ jsxRuntime.jsx(
713
+ "button",
714
+ {
715
+ type: "button",
716
+ onClick: () => {
717
+ setSource(value);
718
+ setDirty(false);
719
+ setError(null);
720
+ },
721
+ "aria-pressed": source === value,
722
+ className: `focus-visible:ring-ring rounded px-1.5 py-0.5 text-xs transition-colors focus-visible:ring-2 focus-visible:outline-none ${source === value ? "bg-primary-800 font-medium text-white" : "text-muted-foreground hover:bg-muted hover:text-foreground"}`,
723
+ children: label
724
+ }
725
+ );
726
+ return /* @__PURE__ */ jsxRuntime.jsxs(
727
+ "aside",
728
+ {
729
+ className: `border-border bg-card flex w-80 shrink-0 flex-col border-l lg:w-96 ${className ?? ""}`,
730
+ "aria-label": "Transcript script",
731
+ children: [
732
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "border-border flex shrink-0 flex-wrap items-center gap-1.5 border-b px-3 py-2", children: [
733
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-foreground text-xs font-semibold", children: "Script" }),
734
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "bg-border mx-1 h-4 w-px", "aria-hidden": "true" }),
735
+ sourceButton("editor", "Editor"),
736
+ sourceButton("original", "Original"),
737
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "bg-border mx-1 h-4 w-px", "aria-hidden": "true" }),
738
+ /* @__PURE__ */ jsxRuntime.jsx(
739
+ "button",
740
+ {
741
+ type: "button",
742
+ onClick: () => {
743
+ setFormat(format === "yaml" ? "json" : "yaml");
744
+ setDirty(false);
745
+ setError(null);
746
+ },
747
+ className: "text-muted-foreground hover:bg-muted hover:text-foreground focus-visible:ring-ring rounded px-1.5 py-0.5 text-xs transition-colors focus-visible:ring-2 focus-visible:outline-none",
748
+ title: "Toggle YAML / JSON",
749
+ children: format.toUpperCase()
750
+ }
751
+ ),
752
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-muted-foreground ml-auto text-[10px]", children: readOnly ? "read-only" : dirty ? "edited" : "live" }),
753
+ /* @__PURE__ */ jsxRuntime.jsx(
754
+ chunkA2QVQF54_cjs.Button,
755
+ {
756
+ variant: "ghost",
757
+ size: "sm",
758
+ onClick: onClose,
759
+ "aria-label": "Close script panel",
760
+ children: "\u2715"
761
+ }
762
+ )
763
+ ] }),
764
+ /* @__PURE__ */ jsxRuntime.jsx(
765
+ "textarea",
766
+ {
767
+ value: text,
768
+ readOnly,
769
+ spellCheck: false,
770
+ onChange: (e) => {
771
+ setText(e.target.value);
772
+ setDirty(true);
773
+ },
774
+ className: "text-foreground min-h-0 flex-1 resize-none bg-transparent p-3 font-mono text-xs leading-relaxed focus:outline-none",
775
+ "aria-label": `Script source (${source}, ${format.toUpperCase()})`
776
+ }
777
+ ),
778
+ error && /* @__PURE__ */ jsxRuntime.jsx(
779
+ "p",
780
+ {
781
+ className: "border-border text-destructive m-0 shrink-0 border-t px-3 py-2 text-xs",
782
+ role: "alert",
783
+ children: error
784
+ }
785
+ ),
786
+ !readOnly && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "border-border flex shrink-0 items-center gap-2 border-t px-3 py-2", children: [
787
+ /* @__PURE__ */ jsxRuntime.jsx(chunkA2QVQF54_cjs.Button, { size: "sm", onClick: handleApply, disabled: !dirty, children: "Apply" }),
788
+ /* @__PURE__ */ jsxRuntime.jsx(
789
+ chunkA2QVQF54_cjs.Button,
790
+ {
791
+ variant: "ghost",
792
+ size: "sm",
793
+ onClick: handleReset,
794
+ disabled: !dirty,
795
+ title: "Throw away unapplied typing and re-sync from the editor",
796
+ children: "Discard"
797
+ }
798
+ ),
799
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-muted-foreground text-[10px]", children: "Apply pushes into the editor \u2014 revert an applied change with Undo" })
800
+ ] })
801
+ ]
802
+ }
803
+ );
804
+ };
805
+ var mediaEditorVariants = classVarianceAuthority.cva(
806
+ // Root is always a row so the ScriptPanel can dock on the right; the
807
+ // media/transcript split direction lives on the inner wrapper below.
808
+ "flex h-full min-h-0 w-full overflow-hidden rounded-xl border border-border bg-card text-card-foreground",
809
+ {
810
+ variants: {
811
+ splitLayout: {
812
+ horizontal: "",
813
+ vertical: ""
814
+ }
815
+ },
816
+ defaultVariants: {
817
+ splitLayout: "horizontal"
818
+ }
819
+ }
820
+ );
821
+ function formatDuration2(ms) {
822
+ const totalSeconds = Math.round(ms / 1e3);
823
+ if (totalSeconds < 90) {
824
+ return `${totalSeconds}s`;
825
+ }
826
+ const hours = Math.floor(totalSeconds / 3600);
827
+ const minutes = Math.floor(totalSeconds % 3600 / 60);
828
+ const seconds = totalSeconds % 60;
829
+ if (hours > 0) {
830
+ return `${hours}:${String(minutes).padStart(2, "0")}:${String(seconds).padStart(2, "0")}`;
831
+ }
832
+ return `${minutes}:${String(seconds).padStart(2, "0")}`;
833
+ }
834
+ function wordClassName(s) {
835
+ const parts = [
836
+ "relative cursor-pointer rounded-sm px-1 py-0.5 transition-colors duration-150"
837
+ ];
838
+ if (s.isSilence) {
839
+ parts.push("text-xs font-medium italic tracking-wide");
840
+ }
841
+ if (s.isSilenceNewline) {
842
+ parts.push("mt-1.5 block w-fit");
843
+ }
844
+ if (s.isAnchor) {
845
+ parts.push("bg-warning/40 outline outline-2 outline-warning");
846
+ } else if (s.isActive) {
847
+ parts.push("bg-primary-800 font-medium text-white");
848
+ } else if (s.isSelected && s.isFocused) {
849
+ parts.push("bg-primary-500/40");
850
+ } else if (s.isDeleted) {
851
+ parts.push(
852
+ "bg-destructive/10 text-destructive line-through opacity-40 hover:bg-destructive/20 hover:opacity-70"
853
+ );
854
+ } else if (s.isInserted) {
855
+ parts.push("bg-success/15 text-success hover:bg-success/25");
856
+ } else if (s.isSilenceNewline) {
857
+ parts.push("text-primary-500 hover:bg-primary-500/15");
858
+ } else if (s.isSilence) {
859
+ parts.push(
860
+ "border border-dashed border-border bg-muted text-muted-foreground hover:bg-muted/70"
861
+ );
862
+ } else {
863
+ parts.push("hover:bg-primary-500/30");
864
+ }
865
+ if (s.isDeleted && (s.isActive || s.isSelected && s.isFocused || s.isAnchor)) {
866
+ parts.push("line-through opacity-60");
867
+ }
868
+ if (s.isCursor && s.isFocused && !s.isAnchor) {
869
+ if (s.isDeleted) {
870
+ parts.push("outline outline-2 outline-offset-1 outline-destructive");
871
+ } else if (s.isActive) {
872
+ parts.push("outline outline-2 outline-offset-1 outline-background");
873
+ } else {
874
+ parts.push("outline outline-2 outline-offset-1 outline-primary-500");
875
+ }
876
+ }
877
+ if (s.hasSpeedMarker) {
878
+ parts.push("ml-0.5 border-l-[3px] border-l-warning pl-1.5");
879
+ }
880
+ return parts.join(" ");
881
+ }
882
+ var Caret = ({ side }) => /* @__PURE__ */ jsxRuntime.jsx(
883
+ "span",
884
+ {
885
+ "aria-hidden": "true",
886
+ className: `bg-primary-500 absolute top-0.5 bottom-0.5 w-0.5 motion-safe:animate-pulse ${side === "before" ? "-left-px" : "right-0.5"}`
887
+ }
888
+ );
889
+ var MediaEditor = React6__namespace.forwardRef(
890
+ ({
891
+ src,
892
+ kind,
893
+ transcript,
894
+ initialEditedWords,
895
+ initialUndoStack,
896
+ onEditorStateChange,
897
+ onHasEditsChange,
898
+ onCursorTimestampChange,
899
+ onEditedWordsRender,
900
+ playerRef: externalPlayerRef,
901
+ className,
902
+ splitLayout
903
+ }, ref) => {
904
+ const edits = chunkY4ESBDQ5_cjs.useTranscriptEdits({
905
+ transcript,
906
+ initialEditedWords,
907
+ initialUndoStack,
908
+ onChange: onEditorStateChange
909
+ });
910
+ const {
911
+ editedWords,
912
+ undoStack,
913
+ clipboard,
914
+ hasEdits,
915
+ undo,
916
+ toggleWordDeleted,
917
+ deleteRange,
918
+ cut,
919
+ copy,
920
+ paste,
921
+ setWordText,
922
+ splitSilence,
923
+ deleteWord,
924
+ replaceEditedWords,
925
+ removeFillers,
926
+ setSilenceThresholds,
927
+ silenceThresholds,
928
+ speedMarkers,
929
+ defaultSpeed,
930
+ setDefaultSpeed,
931
+ toggleSpeedMarker,
932
+ removeSpeedMarker,
933
+ getSpeedAtIndex,
934
+ getSpeedMarkerAtIndex,
935
+ playbackSegments,
936
+ stats,
937
+ fillerAnalysis
938
+ } = edits;
939
+ const [activeWordIndex, setActiveWordIndex] = React6__namespace.useState(
940
+ null
941
+ );
942
+ const [cursorIndex, setCursorIndex] = React6__namespace.useState(0);
943
+ const [cursorPosition, setCursorPosition] = React6__namespace.useState("before");
944
+ const [selectionAnchor, setSelectionAnchor] = React6__namespace.useState(
945
+ null
946
+ );
947
+ const [selection, setSelection] = React6__namespace.useState(
948
+ null
949
+ );
950
+ const [doubleClickAnchor, setDoubleClickAnchor] = React6__namespace.useState(null);
951
+ const [isPlayingSequence, setIsPlayingSequence] = React6__namespace.useState(false);
952
+ const [isFocused, setIsFocused] = React6__namespace.useState(false);
953
+ const [showFillerModal, setShowFillerModal] = React6__namespace.useState(false);
954
+ const [showSilenceModal, setShowSilenceModal] = React6__namespace.useState(false);
955
+ const [showScriptPanel, setShowScriptPanel] = React6__namespace.useState(false);
956
+ const [editorWordIndex, setEditorWordIndex] = React6__namespace.useState(
957
+ null
958
+ );
959
+ const [speedMenuWordIndex, setSpeedMenuWordIndex] = React6__namespace.useState(null);
960
+ const [speedMenuPosition, setSpeedMenuPosition] = React6__namespace.useState(null);
961
+ const playerRef = React6__namespace.useRef(null);
962
+ React6__namespace.useImperativeHandle(
963
+ externalPlayerRef,
964
+ () => playerRef.current,
965
+ []
966
+ );
967
+ const contentRef = React6__namespace.useRef(null);
968
+ const userSetCursor = React6__namespace.useRef(null);
969
+ const isDragging = React6__namespace.useRef(false);
970
+ const longPressTimer = React6__namespace.useRef(
971
+ null
972
+ );
973
+ const longPressTriggered = React6__namespace.useRef(false);
974
+ const wordPlaybackStartMs = React6__namespace.useRef(null);
975
+ const wordPlaybackEndMs = React6__namespace.useRef(null);
976
+ const sequenceSegments = React6__namespace.useRef([]);
977
+ const currentSegmentIndex = React6__namespace.useRef(0);
978
+ React6__namespace.useEffect(() => {
979
+ onHasEditsChange?.(hasEdits);
980
+ }, [hasEdits, onHasEditsChange]);
981
+ React6__namespace.useEffect(() => {
982
+ onEditedWordsRender?.(editedWords);
983
+ }, [editedWords, onEditedWordsRender]);
984
+ React6__namespace.useEffect(() => {
985
+ if (onCursorTimestampChange) {
986
+ const word = editedWords[cursorIndex];
987
+ if (word && !word.deleted) {
988
+ onCursorTimestampChange(word.word.startMs);
989
+ } else {
990
+ onCursorTimestampChange(null);
991
+ }
992
+ }
993
+ }, [cursorIndex, editedWords, onCursorTimestampChange]);
994
+ const prevTranscriptRef = React6__namespace.useRef(transcript);
995
+ React6__namespace.useEffect(() => {
996
+ if (prevTranscriptRef.current === transcript) return;
997
+ prevTranscriptRef.current = transcript;
998
+ setCursorIndex(0);
999
+ setCursorPosition("before");
1000
+ setSelection(null);
1001
+ setSelectionAnchor(null);
1002
+ setDoubleClickAnchor(null);
1003
+ setActiveWordIndex(null);
1004
+ setIsPlayingSequence(false);
1005
+ sequenceSegments.current = [];
1006
+ currentSegmentIndex.current = 0;
1007
+ }, [transcript]);
1008
+ React6__namespace.useEffect(() => {
1009
+ if (userSetCursor.current !== null) {
1010
+ return;
1011
+ }
1012
+ if (activeWordIndex !== null && activeWordIndex >= 0) {
1013
+ setCursorIndex(activeWordIndex);
1014
+ setCursorPosition("before");
1015
+ setSelection(null);
1016
+ setSelectionAnchor(null);
1017
+ }
1018
+ }, [activeWordIndex]);
1019
+ React6__namespace.useEffect(() => {
1020
+ if (activeWordIndex === null) return;
1021
+ const el = contentRef.current?.querySelector(
1022
+ `[data-word-index="${activeWordIndex}"]`
1023
+ );
1024
+ el?.scrollIntoView({ block: "nearest", behavior: "smooth" });
1025
+ }, [activeWordIndex]);
1026
+ React6__namespace.useEffect(() => {
1027
+ contentRef.current?.focus();
1028
+ }, [transcript]);
1029
+ React6__namespace.useEffect(() => {
1030
+ const player = playerRef.current;
1031
+ if (!player) return;
1032
+ const timeMs = player.getCurrentTimeMs();
1033
+ const currentWordIndex = editedWords.findIndex(
1034
+ (ew) => !ew.deleted && timeMs >= ew.word.startMs && timeMs < ew.word.endMs
1035
+ );
1036
+ const targetSpeed = currentWordIndex >= 0 ? getSpeedAtIndex(currentWordIndex) : defaultSpeed;
1037
+ player.setPlaybackRate(targetSpeed);
1038
+ }, [defaultSpeed, speedMarkers, editedWords, getSpeedAtIndex]);
1039
+ const handleTimeUpdate = (timeMs) => {
1040
+ const player = playerRef.current;
1041
+ if (!player) return;
1042
+ if (isPlayingSequence && sequenceSegments.current.length > 0) {
1043
+ const currentSeg = sequenceSegments.current[currentSegmentIndex.current];
1044
+ if (currentSeg && timeMs >= currentSeg.endMs) {
1045
+ const nextIndex = currentSegmentIndex.current + 1;
1046
+ if (nextIndex < sequenceSegments.current.length) {
1047
+ currentSegmentIndex.current = nextIndex;
1048
+ player.seekToMs(sequenceSegments.current[nextIndex].startMs);
1049
+ } else {
1050
+ player.pause();
1051
+ setIsPlayingSequence(false);
1052
+ sequenceSegments.current = [];
1053
+ currentSegmentIndex.current = 0;
1054
+ }
1055
+ }
1056
+ } else if (hasEdits && !player.isPaused()) {
1057
+ const inDeletedWord = editedWords.some(
1058
+ (ew) => ew.deleted && timeMs >= ew.word.startMs && timeMs < ew.word.endMs
1059
+ );
1060
+ if (inDeletedWord) {
1061
+ const nextNonDeleted = editedWords.find(
1062
+ (ew) => !ew.deleted && ew.word.startMs >= timeMs
1063
+ );
1064
+ if (nextNonDeleted) {
1065
+ player.seekToMs(nextNonDeleted.word.startMs);
1066
+ }
1067
+ }
1068
+ }
1069
+ if (wordPlaybackEndMs.current !== null && timeMs >= wordPlaybackEndMs.current) {
1070
+ player.pause();
1071
+ if (wordPlaybackStartMs.current !== null) {
1072
+ player.seekToMs(wordPlaybackStartMs.current);
1073
+ wordPlaybackStartMs.current = null;
1074
+ }
1075
+ wordPlaybackEndMs.current = null;
1076
+ }
1077
+ let editedIndex = -1;
1078
+ if (isPlayingSequence && sequenceSegments.current.length > 0) {
1079
+ const currentSeg = sequenceSegments.current[currentSegmentIndex.current];
1080
+ if (currentSeg) {
1081
+ for (const idx of currentSeg.editedIndices) {
1082
+ const ew = editedWords[idx];
1083
+ if (ew && !ew.deleted && timeMs >= ew.word.startMs && timeMs < ew.word.endMs) {
1084
+ editedIndex = idx;
1085
+ break;
1086
+ }
1087
+ }
1088
+ }
1089
+ }
1090
+ if (editedIndex === -1) {
1091
+ editedIndex = editedWords.findIndex(
1092
+ (ew) => !ew.deleted && timeMs >= ew.word.startMs && timeMs < ew.word.endMs
1093
+ );
1094
+ }
1095
+ if (editedIndex !== activeWordIndex && editedIndex >= 0) {
1096
+ player.setPlaybackRate(getSpeedAtIndex(editedIndex));
1097
+ }
1098
+ setActiveWordIndex(editedIndex >= 0 ? editedIndex : null);
1099
+ userSetCursor.current = null;
1100
+ };
1101
+ const stopSequencePlayback = () => {
1102
+ setIsPlayingSequence(false);
1103
+ sequenceSegments.current = [];
1104
+ currentSegmentIndex.current = 0;
1105
+ };
1106
+ const selectionIndices = React6__namespace.useCallback(() => {
1107
+ if (selection) {
1108
+ const indices = [];
1109
+ for (let i = selection.start; i <= selection.end; i++) indices.push(i);
1110
+ return indices;
1111
+ }
1112
+ return [cursorIndex];
1113
+ }, [selection, cursorIndex]);
1114
+ const handleCut = React6__namespace.useCallback(() => {
1115
+ cut(selectionIndices());
1116
+ }, [cut, selectionIndices]);
1117
+ const handlePaste = React6__namespace.useCallback(() => {
1118
+ if (!clipboard) return;
1119
+ const pastedCount = clipboard.words.length;
1120
+ const insertIndex = paste(cursorIndex, cursorPosition !== "after");
1121
+ if (insertIndex !== null) {
1122
+ setCursorIndex(insertIndex + pastedCount - 1);
1123
+ setCursorPosition("after");
1124
+ setSelection(null);
1125
+ setSelectionAnchor(null);
1126
+ }
1127
+ }, [clipboard, paste, cursorIndex, cursorPosition]);
1128
+ const handleDeleteToggle = React6__namespace.useCallback(() => {
1129
+ if (selection) {
1130
+ const anchorIndex = selectionAnchor ?? selection.start;
1131
+ const indices = [anchorIndex];
1132
+ for (let i = selection.start; i <= selection.end; i++) {
1133
+ if (i !== anchorIndex) indices.push(i);
1134
+ }
1135
+ deleteRange(indices);
1136
+ } else {
1137
+ toggleWordDeleted(cursorIndex);
1138
+ }
1139
+ }, [
1140
+ selection,
1141
+ selectionAnchor,
1142
+ cursorIndex,
1143
+ deleteRange,
1144
+ toggleWordDeleted
1145
+ ]);
1146
+ const handleCopy = React6__namespace.useCallback(
1147
+ (e) => {
1148
+ const indices = selectionIndices();
1149
+ const selectedWords = indices.map((i) => editedWords[i]).filter((ew) => ew !== void 0 && !ew.deleted);
1150
+ if (selectedWords.length === 0) return;
1151
+ const selectedText = selectedWords.filter((ew) => ew.word.wordType !== "silence").map((ew) => ew.word.text).join(" ");
1152
+ e.preventDefault();
1153
+ e.clipboardData.setData("text/plain", selectedText);
1154
+ copy(indices);
1155
+ },
1156
+ [selectionIndices, editedWords, copy]
1157
+ );
1158
+ const openWordEditor = React6__namespace.useCallback((index) => {
1159
+ setEditorWordIndex(index);
1160
+ }, []);
1161
+ const closeWordEditor = React6__namespace.useCallback(() => {
1162
+ setEditorWordIndex(null);
1163
+ setTimeout(() => contentRef.current?.focus(), 0);
1164
+ }, []);
1165
+ const handleEditorSave = React6__namespace.useCallback(
1166
+ (newText) => {
1167
+ if (editorWordIndex === null) return;
1168
+ setWordText(editorWordIndex, newText);
1169
+ },
1170
+ [editorWordIndex, setWordText]
1171
+ );
1172
+ const handleEditorSplitSilence = React6__namespace.useCallback(
1173
+ (durations) => {
1174
+ if (editorWordIndex === null) return;
1175
+ splitSilence(editorWordIndex, durations);
1176
+ },
1177
+ [editorWordIndex, splitSilence]
1178
+ );
1179
+ const handleEditorDelete = React6__namespace.useCallback(() => {
1180
+ if (editorWordIndex !== null) {
1181
+ deleteWord(editorWordIndex);
1182
+ closeWordEditor();
1183
+ }
1184
+ }, [editorWordIndex, deleteWord, closeWordEditor]);
1185
+ const handleWordClick = (word, index, e) => {
1186
+ setCursorIndex(index);
1187
+ setCursorPosition("before");
1188
+ userSetCursor.current = index;
1189
+ if (e.shiftKey && selectionAnchor !== null) {
1190
+ setSelection({
1191
+ start: Math.min(selectionAnchor, index),
1192
+ end: Math.max(selectionAnchor, index)
1193
+ });
1194
+ } else if (doubleClickAnchor !== null && doubleClickAnchor !== index && !isDragging.current) {
1195
+ const start = Math.min(doubleClickAnchor, index);
1196
+ const end = Math.max(doubleClickAnchor, index);
1197
+ setSelection({ start, end });
1198
+ setSelectionAnchor(doubleClickAnchor);
1199
+ setDoubleClickAnchor(null);
1200
+ } else if (doubleClickAnchor === index) ; else if (!isDragging.current) {
1201
+ setSelectionAnchor(index);
1202
+ setSelection(null);
1203
+ const player = playerRef.current;
1204
+ if (player) {
1205
+ player.pause();
1206
+ player.seekToMs(word.startMs);
1207
+ wordPlaybackStartMs.current = word.startMs;
1208
+ wordPlaybackEndMs.current = word.endMs;
1209
+ player.play();
1210
+ }
1211
+ }
1212
+ };
1213
+ const handleWordDoubleClick = (index, e) => {
1214
+ e.preventDefault();
1215
+ if (longPressTriggered.current) return;
1216
+ if (doubleClickAnchor === index) {
1217
+ setDoubleClickAnchor(null);
1218
+ } else {
1219
+ setDoubleClickAnchor(index);
1220
+ setSelection(null);
1221
+ }
1222
+ };
1223
+ const handleWordMouseDown = (index, e) => {
1224
+ if (e.button !== 0) return;
1225
+ longPressTriggered.current = false;
1226
+ longPressTimer.current = setTimeout(() => {
1227
+ longPressTriggered.current = true;
1228
+ openWordEditor(index);
1229
+ }, 500);
1230
+ isDragging.current = true;
1231
+ setSelectionAnchor(index);
1232
+ setCursorIndex(index);
1233
+ setCursorPosition("before");
1234
+ setSelection(null);
1235
+ };
1236
+ const handleWordMouseEnter = (index) => {
1237
+ if (longPressTimer.current) {
1238
+ clearTimeout(longPressTimer.current);
1239
+ longPressTimer.current = null;
1240
+ }
1241
+ if (!isDragging.current || selectionAnchor === null) return;
1242
+ setSelection({
1243
+ start: Math.min(selectionAnchor, index),
1244
+ end: Math.max(selectionAnchor, index)
1245
+ });
1246
+ setCursorIndex(index);
1247
+ };
1248
+ const handleMouseUp = () => {
1249
+ if (longPressTimer.current) {
1250
+ clearTimeout(longPressTimer.current);
1251
+ longPressTimer.current = null;
1252
+ }
1253
+ if (isDragging.current) {
1254
+ isDragging.current = false;
1255
+ }
1256
+ };
1257
+ const handleWordContextMenu = (index, e) => {
1258
+ e.preventDefault();
1259
+ const rect = e.currentTarget.getBoundingClientRect();
1260
+ const menuHeight = 280;
1261
+ const menuWidth = 150;
1262
+ let x = rect.left;
1263
+ let y = rect.bottom + 4;
1264
+ if (x + menuWidth > window.innerWidth) {
1265
+ x = window.innerWidth - menuWidth - 8;
1266
+ }
1267
+ if (y + menuHeight > window.innerHeight) {
1268
+ y = rect.top - menuHeight - 4;
1269
+ if (y < 0) {
1270
+ y = 8;
1271
+ }
1272
+ }
1273
+ setSpeedMenuPosition({ x, y });
1274
+ setSpeedMenuWordIndex(index);
1275
+ };
1276
+ const handleKeyDown = React6__namespace.useCallback(
1277
+ (e) => {
1278
+ const wordCount = editedWords.length;
1279
+ if (wordCount === 0) return;
1280
+ let newIndex = cursorIndex;
1281
+ let handled = false;
1282
+ if (e.key === "Delete" || e.key === "Backspace") {
1283
+ handleDeleteToggle();
1284
+ handled = true;
1285
+ } else if ((e.metaKey || e.ctrlKey) && e.key === "x") {
1286
+ handleCut();
1287
+ handled = true;
1288
+ } else if ((e.metaKey || e.ctrlKey) && e.key === "v" && clipboard) {
1289
+ handlePaste();
1290
+ handled = true;
1291
+ } else if ((e.metaKey || e.ctrlKey) && e.key === "z" && !e.shiftKey) {
1292
+ undo();
1293
+ handled = true;
1294
+ } else if (e.key === "ArrowLeft") {
1295
+ if (cursorPosition === "after") {
1296
+ setCursorPosition("before");
1297
+ } else {
1298
+ newIndex = Math.max(0, cursorIndex - 1);
1299
+ }
1300
+ handled = true;
1301
+ } else if (e.key === "ArrowRight") {
1302
+ if (cursorIndex === wordCount - 1 && cursorPosition === "before") {
1303
+ setCursorPosition("after");
1304
+ } else if (cursorPosition !== "after") {
1305
+ newIndex = Math.min(wordCount - 1, cursorIndex + 1);
1306
+ }
1307
+ handled = true;
1308
+ } else if (e.key === "ArrowUp" || e.key === "ArrowDown") {
1309
+ const currentElement = contentRef.current?.querySelector(
1310
+ `[data-word-index="${cursorIndex}"]`
1311
+ );
1312
+ if (currentElement) {
1313
+ const currentRect = currentElement.getBoundingClientRect();
1314
+ const currentCenterX = currentRect.left + currentRect.width / 2;
1315
+ const direction = e.key === "ArrowUp" ? -1 : 1;
1316
+ let bestMatch = cursorIndex;
1317
+ let bestDistance = Infinity;
1318
+ let targetRowTop = null;
1319
+ for (let i = cursorIndex + direction; i >= 0 && i < wordCount; i += direction) {
1320
+ const el = contentRef.current?.querySelector(
1321
+ `[data-word-index="${i}"]`
1322
+ );
1323
+ if (!el) continue;
1324
+ const rect = el.getBoundingClientRect();
1325
+ const isOnDifferentRow = direction === -1 ? rect.bottom <= currentRect.top : rect.top >= currentRect.bottom;
1326
+ if (isOnDifferentRow) {
1327
+ if (targetRowTop === null) {
1328
+ targetRowTop = rect.top;
1329
+ }
1330
+ const isSameTargetRow = Math.abs(rect.top - targetRowTop) < 5;
1331
+ if (isSameTargetRow) {
1332
+ const centerX = rect.left + rect.width / 2;
1333
+ const distance = Math.abs(centerX - currentCenterX);
1334
+ if (distance < bestDistance) {
1335
+ bestDistance = distance;
1336
+ bestMatch = i;
1337
+ }
1338
+ } else {
1339
+ break;
1340
+ }
1341
+ }
1342
+ }
1343
+ newIndex = bestMatch;
1344
+ }
1345
+ handled = true;
1346
+ } else if (e.key === "Home") {
1347
+ newIndex = 0;
1348
+ handled = true;
1349
+ } else if (e.key === "End") {
1350
+ newIndex = wordCount - 1;
1351
+ handled = true;
1352
+ } else if (e.key === "Enter") {
1353
+ openWordEditor(cursorIndex);
1354
+ handled = true;
1355
+ } else if (e.key === " ") {
1356
+ const ew = editedWords[cursorIndex];
1357
+ const player = playerRef.current;
1358
+ if (player && ew && !ew.deleted) {
1359
+ if (player.isPaused()) {
1360
+ if (hasEdits) {
1361
+ const segments = playbackSegments;
1362
+ if (segments.length > 0) {
1363
+ let startSegmentIdx = 0;
1364
+ for (let i = 0; i < segments.length; i++) {
1365
+ if (segments[i].editedIndices.includes(cursorIndex)) {
1366
+ startSegmentIdx = i;
1367
+ break;
1368
+ }
1369
+ }
1370
+ sequenceSegments.current = segments;
1371
+ currentSegmentIndex.current = startSegmentIdx;
1372
+ setIsPlayingSequence(true);
1373
+ player.seekToMs(ew.word.startMs);
1374
+ player.play();
1375
+ }
1376
+ } else {
1377
+ player.seekToMs(ew.word.startMs);
1378
+ player.play();
1379
+ }
1380
+ } else {
1381
+ player.pause();
1382
+ setIsPlayingSequence(false);
1383
+ }
1384
+ }
1385
+ handled = true;
1386
+ }
1387
+ if (handled) {
1388
+ e.preventDefault();
1389
+ e.stopPropagation();
1390
+ if (newIndex !== cursorIndex) {
1391
+ setCursorIndex(newIndex);
1392
+ setCursorPosition("before");
1393
+ }
1394
+ if (e.shiftKey && e.key !== "Delete" && e.key !== "Backspace") {
1395
+ const anchor = selectionAnchor ?? cursorIndex;
1396
+ if (selectionAnchor === null) {
1397
+ setSelectionAnchor(cursorIndex);
1398
+ }
1399
+ setSelection({
1400
+ start: Math.min(anchor, newIndex),
1401
+ end: Math.max(anchor, newIndex)
1402
+ });
1403
+ } else if (!e.metaKey && !e.ctrlKey) {
1404
+ setSelection(null);
1405
+ setSelectionAnchor(null);
1406
+ }
1407
+ const wordElement = contentRef.current?.querySelector(
1408
+ `[data-word-index="${newIndex}"]`
1409
+ );
1410
+ wordElement?.scrollIntoView({ block: "nearest", behavior: "smooth" });
1411
+ }
1412
+ },
1413
+ [
1414
+ editedWords,
1415
+ cursorIndex,
1416
+ cursorPosition,
1417
+ selectionAnchor,
1418
+ clipboard,
1419
+ hasEdits,
1420
+ playbackSegments,
1421
+ handleDeleteToggle,
1422
+ handleCut,
1423
+ handlePaste,
1424
+ undo,
1425
+ openWordEditor
1426
+ ]
1427
+ );
1428
+ const isWordSelected = (index) => {
1429
+ if (!selection) return false;
1430
+ return index >= selection.start && index <= selection.end;
1431
+ };
1432
+ const renderWord = (ew, index) => {
1433
+ const isActive = index === activeWordIndex;
1434
+ const isCursor = index === cursorIndex;
1435
+ const isSelected = isWordSelected(index);
1436
+ const isDeleted = ew.deleted;
1437
+ const isInserted = ew.inserted ?? false;
1438
+ const isSilence = ew.word.wordType === "silence" || ew.word.wordType === "silence-newline";
1439
+ const isSilenceNewline = ew.word.wordType === "silence-newline";
1440
+ const isAnchor = index === doubleClickAnchor;
1441
+ const showCursorBefore = isCursor && cursorPosition === "before";
1442
+ const showCursorAfter = isCursor && cursorPosition === "after" && index === editedWords.length - 1;
1443
+ const speedMarker = getSpeedMarkerAtIndex(index);
1444
+ const hasSpeedMarker = !!speedMarker;
1445
+ const effectiveSpeed = getSpeedAtIndex(index);
1446
+ let displayText = ew.word.text;
1447
+ if (isSilence && effectiveSpeed !== 1) {
1448
+ const originalDurationMs = ew.word.endMs - ew.word.startMs;
1449
+ const adjustedDurationSec = originalDurationMs / effectiveSpeed / 1e3;
1450
+ displayText = `[${adjustedDurationSec.toFixed(1)}s]`;
1451
+ }
1452
+ const title = `${ew.word.startMs}ms - ${ew.word.endMs}ms${ew.word.confidence ? ` (${Math.round(ew.word.confidence * 100)}%)` : ""}${isSilence ? ` [SILENCE: ${((ew.word.endMs - ew.word.startMs) / 1e3).toFixed(1)}s${effectiveSpeed !== 1 ? ` \u2192 ${((ew.word.endMs - ew.word.startMs) / effectiveSpeed / 1e3).toFixed(1)}s @ ${effectiveSpeed}x` : ""}]` : ""}${isDeleted ? " [DELETED]" : ""}${isInserted ? " [INSERTED]" : ""}${hasSpeedMarker ? ` [SPEED: ${speedMarker.speed}x]` : ""}${isAnchor ? " [ANCHOR - click another word to select range, double-click to clear]" : ""}${!isAnchor && !isDeleted ? " (right-click to set speed marker)" : ""}`;
1453
+ return (
1454
+ // Keyboard interaction is owned by the parent listbox via roving focus
1455
+ // (aria-activedescendant); options are pointer affordances only.
1456
+ // eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/interactive-supports-focus
1457
+ /* @__PURE__ */ jsxRuntime.jsxs(
1458
+ "span",
1459
+ {
1460
+ id: `word-${index}`,
1461
+ "data-word-index": index,
1462
+ className: wordClassName({
1463
+ isActive,
1464
+ isCursor,
1465
+ isSelected,
1466
+ isDeleted,
1467
+ isInserted,
1468
+ isSilence,
1469
+ isSilenceNewline,
1470
+ isAnchor,
1471
+ isFocused,
1472
+ hasSpeedMarker
1473
+ }),
1474
+ onClick: (e) => handleWordClick(ew.word, index, e),
1475
+ onDoubleClick: (e) => handleWordDoubleClick(index, e),
1476
+ onMouseDown: (e) => handleWordMouseDown(index, e),
1477
+ onMouseEnter: () => handleWordMouseEnter(index),
1478
+ onContextMenu: (e) => handleWordContextMenu(index, e),
1479
+ role: "option",
1480
+ "aria-selected": isSelected || isCursor,
1481
+ title,
1482
+ children: [
1483
+ showCursorBefore && isFocused && /* @__PURE__ */ jsxRuntime.jsx(Caret, { side: "before" }),
1484
+ hasSpeedMarker && /* @__PURE__ */ jsxRuntime.jsxs(
1485
+ "span",
1486
+ {
1487
+ className: "border-warning/50 bg-warning/20 text-warning-foreground mr-1 inline-block rounded-sm border px-0.5 align-middle text-[9px] leading-snug font-semibold",
1488
+ title: `Speed: ${speedMarker.speed}x from here`,
1489
+ children: [
1490
+ speedMarker.speed,
1491
+ "x"
1492
+ ]
1493
+ }
1494
+ ),
1495
+ displayText,
1496
+ " ",
1497
+ showCursorAfter && isFocused && /* @__PURE__ */ jsxRuntime.jsx(Caret, { side: "after" })
1498
+ ]
1499
+ },
1500
+ `${ew.originalIndex}-${index}`
1501
+ )
1502
+ );
1503
+ };
1504
+ const showEditBar = hasEdits || doubleClickAnchor !== null || selection !== null;
1505
+ return /* @__PURE__ */ jsxRuntime.jsxs(
1506
+ "div",
1507
+ {
1508
+ ref,
1509
+ className: mediaEditorVariants({ splitLayout, className }),
1510
+ children: [
1511
+ /* @__PURE__ */ jsxRuntime.jsxs(
1512
+ "div",
1513
+ {
1514
+ className: `flex min-h-0 min-w-0 flex-1 ${splitLayout === "vertical" ? "flex-col md:flex-row" : "flex-col"}`,
1515
+ children: [
1516
+ /* @__PURE__ */ jsxRuntime.jsx(
1517
+ "div",
1518
+ {
1519
+ className: `border-border bg-background min-h-0 shrink-0 ${splitLayout === "vertical" ? "md:w-1/2 md:border-r" : "max-h-[50%] border-b"}`,
1520
+ children: /* @__PURE__ */ jsxRuntime.jsx(
1521
+ chunkYD23ZGRU_cjs.MediaPlayer,
1522
+ {
1523
+ ref: playerRef,
1524
+ src,
1525
+ kind,
1526
+ "aria-label": "Media player",
1527
+ onTimeUpdate: handleTimeUpdate,
1528
+ onEnded: stopSequencePlayback
1529
+ }
1530
+ )
1531
+ }
1532
+ ),
1533
+ /* @__PURE__ */ jsxRuntime.jsxs(
1534
+ "div",
1535
+ {
1536
+ className: `flex min-h-0 flex-1 flex-col ${splitLayout === "vertical" ? "md:w-1/2" : ""}`,
1537
+ children: [
1538
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "border-border flex shrink-0 flex-wrap items-center justify-between gap-2 border-b px-3 py-2", children: [
1539
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
1540
+ hasEdits && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "bg-warning/20 text-warning-foreground rounded px-1.5 py-0.5 text-xs font-semibold", children: "Edited" }),
1541
+ /* @__PURE__ */ jsxRuntime.jsx(
1542
+ "label",
1543
+ {
1544
+ className: "text-muted-foreground text-xs",
1545
+ htmlFor: "media-editor-default-speed",
1546
+ children: "Speed:"
1547
+ }
1548
+ ),
1549
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-20", children: /* @__PURE__ */ jsxRuntime.jsx(
1550
+ chunkFRT6RQND_cjs.Select,
1551
+ {
1552
+ id: "media-editor-default-speed",
1553
+ size: "sm",
1554
+ options: chunkS7JQ3FV5_cjs.PLAYBACK_SPEEDS.map((speed) => ({
1555
+ value: String(speed),
1556
+ label: `${speed}x`
1557
+ })),
1558
+ value: String(defaultSpeed),
1559
+ onValueChange: (value) => setDefaultSpeed(parseFloat(value)),
1560
+ "aria-label": "Default playback speed"
1561
+ }
1562
+ ) }),
1563
+ speedMarkers.length > 0 && /* @__PURE__ */ jsxRuntime.jsxs(
1564
+ "span",
1565
+ {
1566
+ className: "text-muted-foreground text-xs",
1567
+ title: `${speedMarkers.length} speed marker(s) set. Right-click on a word to add/remove speed markers.`,
1568
+ children: [
1569
+ speedMarkers.length,
1570
+ " marker",
1571
+ speedMarkers.length !== 1 ? "s" : ""
1572
+ ]
1573
+ }
1574
+ ),
1575
+ /* @__PURE__ */ jsxRuntime.jsx(
1576
+ chunkA2QVQF54_cjs.Button,
1577
+ {
1578
+ variant: "ghost",
1579
+ size: "sm",
1580
+ onClick: () => setShowFillerModal(true),
1581
+ "aria-label": "Remove filler words",
1582
+ title: "Remove filler words",
1583
+ children: "\u2702\uFE0F"
1584
+ }
1585
+ ),
1586
+ undoStack.length > 0 && /* @__PURE__ */ jsxRuntime.jsxs(
1587
+ chunkA2QVQF54_cjs.Button,
1588
+ {
1589
+ variant: "ghost",
1590
+ size: "sm",
1591
+ onClick: undo,
1592
+ "aria-label": `Undo (${undoStack.length} available)`,
1593
+ title: "Undo (\u2318Z)",
1594
+ children: [
1595
+ "Undo (",
1596
+ undoStack.length,
1597
+ ")"
1598
+ ]
1599
+ }
1600
+ )
1601
+ ] }),
1602
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "text-muted-foreground flex items-center gap-2 text-xs", children: [
1603
+ /* @__PURE__ */ jsxRuntime.jsx(
1604
+ chunkA2QVQF54_cjs.Button,
1605
+ {
1606
+ variant: "ghost",
1607
+ size: "sm",
1608
+ onClick: () => setShowScriptPanel((v) => !v),
1609
+ "aria-pressed": showScriptPanel,
1610
+ "aria-label": "Show script",
1611
+ title: "Show the transcript script (editable YAML/JSON)",
1612
+ children: "Script"
1613
+ }
1614
+ ),
1615
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { children: [
1616
+ stats.activeWordCount,
1617
+ " words"
1618
+ ] }),
1619
+ /* @__PURE__ */ jsxRuntime.jsxs(
1620
+ "button",
1621
+ {
1622
+ type: "button",
1623
+ className: "hover:bg-muted hover:text-foreground focus-visible:ring-ring rounded px-1 underline decoration-dotted transition-colors focus-visible:ring-2 focus-visible:outline-none",
1624
+ onClick: () => setShowSilenceModal(true),
1625
+ title: stats.activeSilenceCount > 0 ? `Click to configure silence detection (currently \u2265${(silenceThresholds.minSilenceMs / 1e3).toFixed(1)}s)` : `No silences detected (threshold: ${(silenceThresholds.minSilenceMs / 1e3).toFixed(1)}s). Click to configure.`,
1626
+ "aria-label": "Configure silence detection",
1627
+ children: [
1628
+ stats.activeSilenceCount,
1629
+ " silences"
1630
+ ]
1631
+ }
1632
+ ),
1633
+ (stats.deletedWordCount > 0 || stats.deletedSilenceCount > 0) && /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-destructive", children: [
1634
+ stats.deletedWordCount + stats.deletedSilenceCount,
1635
+ " deleted"
1636
+ ] }),
1637
+ transcript.speakers && /* @__PURE__ */ jsxRuntime.jsxs("span", { children: [
1638
+ transcript.speakers.length,
1639
+ " speakers"
1640
+ ] }),
1641
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: formatDuration2(stats.editedDurationMs) })
1642
+ ] })
1643
+ ] }),
1644
+ showEditBar && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "border-border bg-muted/40 flex shrink-0 items-center gap-1 border-b px-3 py-1", children: [
1645
+ /* @__PURE__ */ jsxRuntime.jsxs(
1646
+ chunkA2QVQF54_cjs.Button,
1647
+ {
1648
+ variant: "ghost",
1649
+ size: "sm",
1650
+ onClick: () => openWordEditor(cursorIndex),
1651
+ title: "Edit selected word (Enter)",
1652
+ children: [
1653
+ "Edit ",
1654
+ /* @__PURE__ */ jsxRuntime.jsx("kbd", { className: "text-[10px] opacity-60", children: "\u21B5" })
1655
+ ]
1656
+ }
1657
+ ),
1658
+ /* @__PURE__ */ jsxRuntime.jsxs(
1659
+ chunkA2QVQF54_cjs.Button,
1660
+ {
1661
+ variant: "ghost",
1662
+ size: "sm",
1663
+ onClick: handleDeleteToggle,
1664
+ title: "Delete selected word (Del)",
1665
+ children: [
1666
+ "Del ",
1667
+ /* @__PURE__ */ jsxRuntime.jsx("kbd", { className: "text-[10px] opacity-60", children: "\u232B" })
1668
+ ]
1669
+ }
1670
+ ),
1671
+ /* @__PURE__ */ jsxRuntime.jsxs(
1672
+ chunkA2QVQF54_cjs.Button,
1673
+ {
1674
+ variant: "ghost",
1675
+ size: "sm",
1676
+ onClick: handleCut,
1677
+ title: "Cut selection (\u2318X)",
1678
+ children: [
1679
+ "Cut ",
1680
+ /* @__PURE__ */ jsxRuntime.jsx("kbd", { className: "text-[10px] opacity-60", children: "\u2318X" })
1681
+ ]
1682
+ }
1683
+ ),
1684
+ /* @__PURE__ */ jsxRuntime.jsxs(
1685
+ chunkA2QVQF54_cjs.Button,
1686
+ {
1687
+ variant: "ghost",
1688
+ size: "sm",
1689
+ onClick: handlePaste,
1690
+ disabled: !clipboard,
1691
+ title: "Paste (\u2318V)",
1692
+ children: [
1693
+ "Paste ",
1694
+ /* @__PURE__ */ jsxRuntime.jsx("kbd", { className: "text-[10px] opacity-60", children: "\u2318V" })
1695
+ ]
1696
+ }
1697
+ )
1698
+ ] }),
1699
+ /* @__PURE__ */ jsxRuntime.jsx(
1700
+ "div",
1701
+ {
1702
+ ref: contentRef,
1703
+ className: `focus-visible:ring-ring min-h-0 flex-1 overflow-y-auto p-3 text-sm leading-relaxed select-none focus:outline-none focus-visible:ring-2 focus-visible:ring-inset ${isFocused ? "bg-muted/20" : ""}`,
1704
+ tabIndex: 0,
1705
+ onKeyDown: handleKeyDown,
1706
+ onCopy: handleCopy,
1707
+ onFocus: () => setIsFocused(true),
1708
+ onBlur: () => setIsFocused(false),
1709
+ onMouseUp: handleMouseUp,
1710
+ onMouseLeave: handleMouseUp,
1711
+ role: "listbox",
1712
+ "aria-label": "Transcript words",
1713
+ "aria-activedescendant": `word-${cursorIndex}`,
1714
+ children: editedWords.map(renderWord)
1715
+ }
1716
+ )
1717
+ ]
1718
+ }
1719
+ )
1720
+ ]
1721
+ }
1722
+ ),
1723
+ showScriptPanel && /* @__PURE__ */ jsxRuntime.jsx(
1724
+ ScriptPanel,
1725
+ {
1726
+ transcript,
1727
+ editedWords,
1728
+ onApply: replaceEditedWords,
1729
+ onClose: () => setShowScriptPanel(false)
1730
+ }
1731
+ ),
1732
+ /* @__PURE__ */ jsxRuntime.jsx(
1733
+ FillerWordsModal,
1734
+ {
1735
+ isOpen: showFillerModal,
1736
+ onClose: () => setShowFillerModal(false),
1737
+ onApply: removeFillers,
1738
+ matchingCounts: fillerAnalysis.matchCounts,
1739
+ fillerDurations: fillerAnalysis.durations,
1740
+ silenceCounts: fillerAnalysis.silenceCounts,
1741
+ currentDurationMs: stats.editedDurationMs
1742
+ }
1743
+ ),
1744
+ /* @__PURE__ */ jsxRuntime.jsx(
1745
+ SilenceSettingsModal,
1746
+ {
1747
+ isOpen: showSilenceModal,
1748
+ onClose: () => setShowSilenceModal(false),
1749
+ minSilenceMs: silenceThresholds.minSilenceMs,
1750
+ nlSilenceMs: silenceThresholds.nlSilenceMs,
1751
+ onApply: setSilenceThresholds
1752
+ }
1753
+ ),
1754
+ /* @__PURE__ */ jsxRuntime.jsx(
1755
+ WordEditorModal,
1756
+ {
1757
+ isOpen: editorWordIndex !== null,
1758
+ editableWord: editorWordIndex !== null ? editedWords[editorWordIndex] ?? null : null,
1759
+ onClose: closeWordEditor,
1760
+ onSave: handleEditorSave,
1761
+ onSplitSilence: handleEditorSplitSilence,
1762
+ onDelete: handleEditorDelete
1763
+ }
1764
+ ),
1765
+ /* @__PURE__ */ jsxRuntime.jsx(
1766
+ SpeedMarkerMenu,
1767
+ {
1768
+ isOpen: speedMenuWordIndex !== null,
1769
+ position: speedMenuPosition,
1770
+ wordIndex: speedMenuWordIndex,
1771
+ currentMarker: speedMenuWordIndex !== null ? getSpeedMarkerAtIndex(speedMenuWordIndex) : void 0,
1772
+ defaultSpeed,
1773
+ onSetSpeed: (speed) => {
1774
+ if (speedMenuWordIndex !== null) {
1775
+ toggleSpeedMarker(speedMenuWordIndex, speed);
1776
+ }
1777
+ },
1778
+ onRemoveMarker: () => {
1779
+ if (speedMenuWordIndex !== null) {
1780
+ removeSpeedMarker(speedMenuWordIndex);
1781
+ }
1782
+ },
1783
+ onClose: () => {
1784
+ setSpeedMenuWordIndex(null);
1785
+ setSpeedMenuPosition(null);
1786
+ }
1787
+ }
1788
+ )
1789
+ ]
1790
+ }
1791
+ );
1792
+ }
1793
+ );
1794
+ MediaEditor.displayName = "MediaEditor";
1795
+
1796
+ exports.FillerWordsModal = FillerWordsModal;
1797
+ exports.MediaEditor = MediaEditor;
1798
+ exports.SilenceSettingsModal = SilenceSettingsModal;
1799
+ exports.SpeedMarkerMenu = SpeedMarkerMenu;
1800
+ exports.WordEditorModal = WordEditorModal;
1801
+ //# sourceMappingURL=chunk-3Q2VWPML.cjs.map
1802
+ //# sourceMappingURL=chunk-3Q2VWPML.cjs.map