eprec 1.7.0 → 1.8.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.
@@ -199,6 +199,34 @@ p {
199
199
  gap: var(--spacing-md);
200
200
  }
201
201
 
202
+ .stacked-list {
203
+ display: flex;
204
+ flex-direction: column;
205
+ gap: 0;
206
+ margin: 0;
207
+ padding: 0;
208
+ list-style: none;
209
+ }
210
+
211
+ .stacked-item {
212
+ border-radius: 0;
213
+ }
214
+
215
+ .stacked-item:first-child {
216
+ border-top-left-radius: var(--radius-lg);
217
+ border-top-right-radius: var(--radius-lg);
218
+ }
219
+
220
+ .stacked-item:last-child {
221
+ border-bottom-left-radius: var(--radius-lg);
222
+ border-bottom-right-radius: var(--radius-lg);
223
+ }
224
+
225
+ .stacked-item.is-selected {
226
+ background: var(--color-info-surface);
227
+ box-shadow: inset 0 0 0 1px var(--color-border-accent);
228
+ }
229
+
202
230
  .app-list {
203
231
  margin: 0;
204
232
  padding-left: var(--spacing-lg);
@@ -296,6 +324,93 @@ p {
296
324
  line-height: 1.4;
297
325
  }
298
326
 
327
+ .actions-card {
328
+ gap: var(--spacing-xl);
329
+ }
330
+
331
+ .actions-header {
332
+ display: flex;
333
+ flex-wrap: wrap;
334
+ align-items: flex-start;
335
+ justify-content: space-between;
336
+ gap: var(--spacing-lg);
337
+ }
338
+
339
+ .actions-meta {
340
+ min-width: 180px;
341
+ }
342
+
343
+ .actions-buttons {
344
+ display: flex;
345
+ flex-wrap: wrap;
346
+ gap: var(--spacing-sm);
347
+ }
348
+
349
+ .actions-grid {
350
+ display: grid;
351
+ grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
352
+ gap: var(--spacing-lg);
353
+ }
354
+
355
+ .actions-panel {
356
+ border: 1px solid var(--color-border);
357
+ border-radius: var(--radius-lg);
358
+ padding: var(--spacing-lg);
359
+ background: var(--color-surface-muted);
360
+ display: flex;
361
+ flex-direction: column;
362
+ gap: var(--spacing-md);
363
+ }
364
+
365
+ .actions-button-row {
366
+ display: flex;
367
+ flex-wrap: wrap;
368
+ gap: var(--spacing-sm);
369
+ }
370
+
371
+ .actions-queue {
372
+ display: flex;
373
+ flex-direction: column;
374
+ gap: var(--spacing-md);
375
+ }
376
+
377
+ .processing-list .stacked-item {
378
+ background: var(--color-surface);
379
+ }
380
+
381
+ .processing-row {
382
+ border: 1px solid var(--color-border);
383
+ }
384
+
385
+ .processing-row-header {
386
+ display: flex;
387
+ align-items: flex-start;
388
+ justify-content: space-between;
389
+ gap: var(--spacing-md);
390
+ padding: var(--spacing-md) var(--spacing-md) var(--spacing-sm);
391
+ }
392
+
393
+ .processing-row-header h4 {
394
+ margin: 0 0 var(--spacing-xs);
395
+ font-size: var(--font-size-base);
396
+ }
397
+
398
+ .processing-row-meta {
399
+ display: flex;
400
+ align-items: center;
401
+ justify-content: space-between;
402
+ gap: var(--spacing-md);
403
+ padding: 0 var(--spacing-md) var(--spacing-md);
404
+ }
405
+
406
+ .processing-row.is-running {
407
+ box-shadow: inset 0 0 0 1px var(--color-warning-border);
408
+ }
409
+
410
+ .processing-row.is-complete {
411
+ background: var(--color-success-surface);
412
+ }
413
+
299
414
  .timeline-card {
300
415
  gap: var(--spacing-xl);
301
416
  }
@@ -540,7 +655,7 @@ p {
540
655
  margin: 0;
541
656
  display: flex;
542
657
  flex-direction: column;
543
- gap: var(--spacing-sm);
658
+ gap: 0;
544
659
  }
545
660
 
546
661
  .cut-row {
@@ -549,14 +664,14 @@ p {
549
664
  gap: var(--spacing-md);
550
665
  justify-content: space-between;
551
666
  border: 1px solid var(--color-border);
552
- border-radius: var(--radius-lg);
667
+ border-radius: 0;
553
668
  padding: var(--spacing-sm) var(--spacing-md);
554
669
  background: var(--color-surface-muted);
555
670
  }
556
671
 
557
672
  .cut-row.is-selected {
558
- border-color: var(--color-border-accent);
559
673
  background: var(--color-info-surface);
674
+ box-shadow: inset 0 0 0 1px var(--color-border-accent);
560
675
  }
561
676
 
562
677
  .cut-select {
@@ -591,13 +706,13 @@ p {
591
706
  .command-list {
592
707
  display: flex;
593
708
  flex-direction: column;
594
- gap: var(--spacing-md);
709
+ gap: 0;
595
710
  }
596
711
 
597
712
  .chapter-row,
598
713
  .command-row {
599
714
  border: 1px solid var(--color-border);
600
- border-radius: var(--radius-lg);
715
+ border-radius: 0;
601
716
  padding: var(--spacing-md);
602
717
  background: var(--color-surface-muted);
603
718
  display: flex;
@@ -673,16 +788,22 @@ p {
673
788
  list-style: none;
674
789
  margin: 0;
675
790
  padding: 0;
676
- display: grid;
677
- gap: var(--spacing-sm);
791
+ display: flex;
792
+ flex-direction: column;
793
+ gap: 0;
794
+ }
795
+
796
+ .transcript-results .stacked-item {
797
+ border: 1px solid var(--color-border);
798
+ background: var(--color-surface);
678
799
  }
679
800
 
680
801
  .transcript-result {
681
802
  width: 100%;
682
- border: 1px solid var(--color-border);
683
- border-radius: var(--radius-md);
684
803
  padding: var(--spacing-sm) var(--spacing-md);
685
- background: var(--color-surface);
804
+ border: none;
805
+ border-radius: 0;
806
+ background: transparent;
686
807
  display: grid;
687
808
  grid-template-columns: auto 1fr;
688
809
  gap: var(--spacing-md);
@@ -713,6 +834,10 @@ p {
713
834
  overflow-x: auto;
714
835
  }
715
836
 
837
+ .stacked-list .stacked-item + .stacked-item {
838
+ border-top: none;
839
+ }
840
+
716
841
  @keyframes shimmer {
717
842
  0% {
718
843
  background-position: 0% 50%;
@@ -1,3 +1,4 @@
1
+ import { matchSorter } from 'match-sorter'
1
2
  import type { Handle } from 'remix/component'
2
3
  import {
3
4
  sampleEditSession,
@@ -12,15 +13,34 @@ const MIN_CUT_LENGTH = 0.2
12
13
  const DEFAULT_CUT_LENGTH = 2.4
13
14
  const PLAYHEAD_STEP = 0.1
14
15
 
16
+ type ProcessingStatus = 'queued' | 'running' | 'done'
17
+ type ProcessingCategory = 'chapter' | 'transcript' | 'export'
18
+ type ProcessingTask = {
19
+ id: string
20
+ title: string
21
+ detail: string
22
+ status: ProcessingStatus
23
+ category: ProcessingCategory
24
+ }
25
+
15
26
  export function EditingWorkspace(handle: Handle) {
16
27
  const duration = sampleEditSession.duration
17
28
  const transcript = sampleEditSession.transcript
18
29
  const commands = sampleEditSession.commands
30
+ const transcriptIndex = transcript.map((word) => ({
31
+ ...word,
32
+ context: buildContext(transcript, word.index, 3),
33
+ }))
19
34
  let cutRanges = sampleEditSession.cuts.map((range) => ({ ...range }))
20
35
  let chapters = sampleEditSession.chapters.map((chapter) => ({ ...chapter }))
21
36
  let playhead = 18.2
22
37
  let selectedRangeId = cutRanges[0]?.id ?? null
23
38
  let searchQuery = ''
39
+ let primaryChapterId = chapters[0]?.id ?? ''
40
+ let secondaryChapterId = chapters[1]?.id ?? chapters[0]?.id ?? ''
41
+ let processingQueue: ProcessingTask[] = []
42
+ let activeTaskId: string | null = null
43
+ let processingCount = 1
24
44
  let manualCutId = 1
25
45
  let previewDuration = 0
26
46
  let previewReady = false
@@ -144,6 +164,128 @@ export function EditingWorkspace(handle: Handle) {
144
164
  handle.update()
145
165
  }
146
166
 
167
+ const findChapter = (chapterId: string) =>
168
+ chapters.find((chapter) => chapter.id === chapterId) ?? null
169
+
170
+ const updatePrimaryChapter = (chapterId: string) => {
171
+ primaryChapterId = chapterId
172
+ if (secondaryChapterId === chapterId) {
173
+ secondaryChapterId =
174
+ chapters.find((chapter) => chapter.id !== chapterId)?.id ??
175
+ chapterId
176
+ }
177
+ handle.update()
178
+ }
179
+
180
+ const updateSecondaryChapter = (chapterId: string) => {
181
+ secondaryChapterId = chapterId
182
+ handle.update()
183
+ }
184
+
185
+ const queueTask = (
186
+ title: string,
187
+ detail: string,
188
+ category: ProcessingCategory,
189
+ ) => {
190
+ const task: ProcessingTask = {
191
+ id: `task-${processingCount++}`,
192
+ title,
193
+ detail,
194
+ status: 'queued',
195
+ category,
196
+ }
197
+ processingQueue = [...processingQueue, task]
198
+ handle.update()
199
+ }
200
+
201
+ const queueChapterEdit = () => {
202
+ const chapter = findChapter(primaryChapterId)
203
+ if (!chapter) return
204
+ queueTask(
205
+ `Edit ${chapter.title}`,
206
+ `Review trims for ${formatTimestamp(chapter.start)} - ${formatTimestamp(
207
+ chapter.end,
208
+ )}.`,
209
+ 'chapter',
210
+ )
211
+ }
212
+
213
+ const queueCombineChapters = () => {
214
+ const primary = findChapter(primaryChapterId)
215
+ const secondary = findChapter(secondaryChapterId)
216
+ if (!primary || !secondary || primary.id === secondary.id) return
217
+ queueTask(
218
+ `Combine ${primary.title} + ${secondary.title}`,
219
+ 'Merge both chapters into a single preview export.',
220
+ 'chapter',
221
+ )
222
+ }
223
+
224
+ const queueTranscriptRegeneration = () => {
225
+ queueTask(
226
+ 'Regenerate transcript',
227
+ 'Run Whisper alignment and refresh search cues.',
228
+ 'transcript',
229
+ )
230
+ }
231
+
232
+ const queueCommandScan = () => {
233
+ queueTask(
234
+ 'Detect command windows',
235
+ 'Scan for Jarvis commands and update cut ranges.',
236
+ 'transcript',
237
+ )
238
+ }
239
+
240
+ const queuePreviewRender = () => {
241
+ queueTask(
242
+ 'Render preview clip',
243
+ 'Bake a short MP4 with current edits applied.',
244
+ 'export',
245
+ )
246
+ }
247
+
248
+ const queueFinalExport = () => {
249
+ queueTask(
250
+ 'Export edited chapters',
251
+ 'Render final chapters and write the export package.',
252
+ 'export',
253
+ )
254
+ }
255
+
256
+ const startNextTask = () => {
257
+ if (activeTaskId) return
258
+ const next = processingQueue.find((task) => task.status === 'queued')
259
+ if (!next) return
260
+ activeTaskId = next.id
261
+ processingQueue = processingQueue.map((task) =>
262
+ task.id === next.id ? { ...task, status: 'running' } : task,
263
+ )
264
+ handle.update()
265
+ }
266
+
267
+ const markActiveDone = () => {
268
+ if (!activeTaskId) return
269
+ processingQueue = processingQueue.map((task) =>
270
+ task.id === activeTaskId ? { ...task, status: 'done' } : task,
271
+ )
272
+ activeTaskId = null
273
+ handle.update()
274
+ }
275
+
276
+ const clearCompletedTasks = () => {
277
+ processingQueue = processingQueue.filter((task) => task.status !== 'done')
278
+ handle.update()
279
+ }
280
+
281
+ const removeTask = (taskId: string) => {
282
+ processingQueue = processingQueue.filter((task) => task.id !== taskId)
283
+ if (activeTaskId === taskId) {
284
+ activeTaskId = null
285
+ }
286
+ handle.update()
287
+ }
288
+
147
289
  const syncVideoToPlayhead = (value: number) => {
148
290
  if (
149
291
  !previewNode ||
@@ -179,12 +321,24 @@ export function EditingWorkspace(handle: Handle) {
179
321
  const currentContext = currentWord
180
322
  ? buildContext(transcript, currentWord.index, 4)
181
323
  : 'No transcript cues found for the playhead.'
182
- const query = searchQuery.trim().toLowerCase()
324
+ const query = searchQuery.trim()
183
325
  const searchResults = query
184
- ? transcript
185
- .filter((word) => word.word.toLowerCase().includes(query))
186
- .slice(0, 12)
326
+ ? matchSorter(transcriptIndex, query, {
327
+ keys: ['word'],
328
+ }).slice(0, 12)
187
329
  : []
330
+ const queuedCount = processingQueue.filter(
331
+ (task) => task.status === 'queued',
332
+ ).length
333
+ const completedCount = processingQueue.filter(
334
+ (task) => task.status === 'done',
335
+ ).length
336
+ const runningTask =
337
+ processingQueue.find((task) => task.status === 'running') ?? null
338
+ const canCombineChapters =
339
+ primaryChapterId.length > 0 &&
340
+ secondaryChapterId.length > 0 &&
341
+ primaryChapterId !== secondaryChapterId
188
342
  const commandPreview = buildCommandPreview(
189
343
  sampleEditSession.sourceName,
190
344
  chapters,
@@ -246,6 +400,227 @@ export function EditingWorkspace(handle: Handle) {
246
400
  </div>
247
401
  </section>
248
402
 
403
+ <section class="app-card app-card--full actions-card">
404
+ <div class="actions-header">
405
+ <div>
406
+ <h2>Processing actions</h2>
407
+ <p class="app-muted">
408
+ Queue chapter edits, transcript cleanup, and export jobs directly
409
+ from the workspace.
410
+ </p>
411
+ </div>
412
+ <div class="actions-meta">
413
+ <div class="summary-item">
414
+ <span class="summary-label">Queue</span>
415
+ <span class="summary-value">{queuedCount} queued</span>
416
+ <span class="summary-subtext">
417
+ {runningTask ? `Running: ${runningTask.title}` : 'Idle'}
418
+ </span>
419
+ </div>
420
+ </div>
421
+ <div class="actions-buttons">
422
+ <button
423
+ class="button button--primary"
424
+ type="button"
425
+ disabled={queuedCount === 0 || Boolean(runningTask)}
426
+ on={{ click: startNextTask }}
427
+ >
428
+ Run next
429
+ </button>
430
+ <button
431
+ class="button button--ghost"
432
+ type="button"
433
+ disabled={!runningTask}
434
+ on={{ click: markActiveDone }}
435
+ >
436
+ Mark running done
437
+ </button>
438
+ <button
439
+ class="button button--ghost"
440
+ type="button"
441
+ disabled={completedCount === 0}
442
+ on={{ click: clearCompletedTasks }}
443
+ >
444
+ Clear completed
445
+ </button>
446
+ </div>
447
+ </div>
448
+
449
+ <div class="actions-grid">
450
+ <article class="actions-panel">
451
+ <div class="panel-header">
452
+ <h3>Chapter processing</h3>
453
+ <span class="status-pill status-pill--info">Chapter</span>
454
+ </div>
455
+ <label class="input-label">
456
+ Primary chapter
457
+ <select
458
+ class="text-input"
459
+ value={primaryChapterId}
460
+ on={{
461
+ change: (event) => {
462
+ const target = event.currentTarget as HTMLSelectElement
463
+ updatePrimaryChapter(target.value)
464
+ },
465
+ }}
466
+ >
467
+ {chapters.map((chapter) => (
468
+ <option value={chapter.id}>{chapter.title}</option>
469
+ ))}
470
+ </select>
471
+ </label>
472
+ <label class="input-label">
473
+ Secondary chapter
474
+ <select
475
+ class="text-input"
476
+ value={secondaryChapterId}
477
+ on={{
478
+ change: (event) => {
479
+ const target = event.currentTarget as HTMLSelectElement
480
+ updateSecondaryChapter(target.value)
481
+ },
482
+ }}
483
+ >
484
+ {chapters.map((chapter) => (
485
+ <option value={chapter.id}>{chapter.title}</option>
486
+ ))}
487
+ </select>
488
+ </label>
489
+ <div class="actions-button-row">
490
+ <button
491
+ class="button button--primary"
492
+ type="button"
493
+ disabled={!primaryChapterId}
494
+ on={{ click: queueChapterEdit }}
495
+ >
496
+ Edit chapter
497
+ </button>
498
+ <button
499
+ class="button button--ghost"
500
+ type="button"
501
+ disabled={!canCombineChapters}
502
+ on={{ click: queueCombineChapters }}
503
+ >
504
+ Combine chapters
505
+ </button>
506
+ </div>
507
+ <p class="app-muted">
508
+ Stage edits or merge two chapters without leaving this view.
509
+ </p>
510
+ </article>
511
+
512
+ <article class="actions-panel">
513
+ <div class="panel-header">
514
+ <h3>Transcript utilities</h3>
515
+ <span class="status-pill status-pill--warning">Transcript</span>
516
+ </div>
517
+ <div class="actions-button-row">
518
+ <button
519
+ class="button button--ghost"
520
+ type="button"
521
+ on={{ click: queueTranscriptRegeneration }}
522
+ >
523
+ Regenerate transcript
524
+ </button>
525
+ <button
526
+ class="button button--ghost"
527
+ type="button"
528
+ on={{ click: queueCommandScan }}
529
+ >
530
+ Detect command windows
531
+ </button>
532
+ </div>
533
+ <p class="app-muted">
534
+ Refresh the transcript or scan for command markers on demand.
535
+ </p>
536
+ </article>
537
+
538
+ <article class="actions-panel">
539
+ <div class="panel-header">
540
+ <h3>Exports</h3>
541
+ <span class="status-pill status-pill--success">Export</span>
542
+ </div>
543
+ <div class="actions-button-row">
544
+ <button
545
+ class="button button--ghost"
546
+ type="button"
547
+ on={{ click: queuePreviewRender }}
548
+ >
549
+ Render preview clip
550
+ </button>
551
+ <button
552
+ class="button button--ghost"
553
+ type="button"
554
+ on={{ click: queueFinalExport }}
555
+ >
556
+ Export edited chapters
557
+ </button>
558
+ </div>
559
+ <p class="app-muted">
560
+ Trigger preview renders or finalize exports for the pipeline.
561
+ </p>
562
+ </article>
563
+ </div>
564
+
565
+ <div class="actions-queue">
566
+ <div class="panel-header">
567
+ <h3>Processing queue</h3>
568
+ <span class="summary-subtext">
569
+ {processingQueue.length} total
570
+ </span>
571
+ </div>
572
+ {processingQueue.length === 0 ? (
573
+ <p class="app-muted">
574
+ No actions queued yet. Use the buttons above to stage work.
575
+ </p>
576
+ ) : (
577
+ <ul class="stacked-list processing-list">
578
+ {processingQueue.map((task) => (
579
+ <li
580
+ class={classNames(
581
+ 'stacked-item',
582
+ 'processing-row',
583
+ task.status === 'running' && 'is-running',
584
+ task.status === 'done' && 'is-complete',
585
+ )}
586
+ >
587
+ <div class="processing-row-header">
588
+ <div>
589
+ <h4>{task.title}</h4>
590
+ <p class="app-muted">{task.detail}</p>
591
+ </div>
592
+ <span
593
+ class={classNames(
594
+ 'status-pill',
595
+ task.status === 'queued' && 'status-pill--info',
596
+ task.status === 'running' && 'status-pill--warning',
597
+ task.status === 'done' && 'status-pill--success',
598
+ )}
599
+ >
600
+ {task.status}
601
+ </span>
602
+ </div>
603
+ <div class="processing-row-meta">
604
+ <span class="summary-subtext">
605
+ {formatProcessingCategory(task.category)}
606
+ </span>
607
+ {task.status === 'queued' ? (
608
+ <button
609
+ class="button button--ghost"
610
+ type="button"
611
+ on={{ click: () => removeTask(task.id) }}
612
+ >
613
+ Remove
614
+ </button>
615
+ ) : null}
616
+ </div>
617
+ </li>
618
+ ))}
619
+ </ul>
620
+ )}
621
+ </div>
622
+ </section>
623
+
249
624
  <section class="app-card app-card--full timeline-card">
250
625
  <div class="timeline-header">
251
626
  <div>
@@ -523,10 +898,11 @@ export function EditingWorkspace(handle: Handle) {
523
898
  )}
524
899
 
525
900
  <h3>Cut list</h3>
526
- <ul class="cut-list">
901
+ <ul class="cut-list stacked-list">
527
902
  {sortedCuts.map((range) => (
528
903
  <li
529
904
  class={classNames(
905
+ 'stacked-item',
530
906
  'cut-row',
531
907
  range.id === selectedRangeId && 'is-selected',
532
908
  )}
@@ -562,9 +938,9 @@ export function EditingWorkspace(handle: Handle) {
562
938
  <p class="app-muted">
563
939
  Update output names and mark chapters to skip before export.
564
940
  </p>
565
- <div class="chapter-list">
941
+ <div class="chapter-list stacked-list">
566
942
  {chapters.map((chapter) => (
567
- <article class="chapter-row">
943
+ <article class="chapter-row stacked-item">
568
944
  <div class="chapter-header">
569
945
  <div>
570
946
  <h3>{chapter.title}</h3>
@@ -630,11 +1006,11 @@ export function EditingWorkspace(handle: Handle) {
630
1006
  <p class="app-muted">
631
1007
  Apply Jarvis commands to your cut list or chapter metadata.
632
1008
  </p>
633
- <div class="command-list">
1009
+ <div class="command-list stacked-list">
634
1010
  {commands.map((command) => {
635
1011
  const applied = isCommandApplied(command, sortedCuts, chapters)
636
1012
  return (
637
- <article class="command-row">
1013
+ <article class="command-row stacked-item">
638
1014
  <div class="command-header">
639
1015
  <h3>{command.label}</h3>
640
1016
  <span
@@ -705,10 +1081,14 @@ export function EditingWorkspace(handle: Handle) {
705
1081
  <p class="app-muted transcript-empty">
706
1082
  Type to search the transcript words. Click a result to jump to it.
707
1083
  </p>
1084
+ ) : searchResults.length === 0 ? (
1085
+ <p class="app-muted transcript-empty">
1086
+ No results found for "{query}".
1087
+ </p>
708
1088
  ) : (
709
- <ul class="transcript-results">
1089
+ <ul class="transcript-results stacked-list">
710
1090
  {searchResults.map((word) => (
711
- <li>
1091
+ <li class="stacked-item">
712
1092
  <button
713
1093
  class="transcript-result"
714
1094
  type="button"
@@ -717,9 +1097,7 @@ export function EditingWorkspace(handle: Handle) {
717
1097
  <span class="transcript-time">
718
1098
  {formatTimestamp(word.start)}
719
1099
  </span>
720
- <span class="transcript-snippet">
721
- {buildContext(transcript, word.index, 3)}
722
- </span>
1100
+ <span class="transcript-snippet">{word.context}</span>
723
1101
  </button>
724
1102
  </li>
725
1103
  ))}
@@ -786,6 +1164,12 @@ function formatSeconds(value: number) {
786
1164
  return `${value.toFixed(1)}s`
787
1165
  }
788
1166
 
1167
+ function formatProcessingCategory(category: ProcessingCategory) {
1168
+ if (category === 'chapter') return 'Chapter task'
1169
+ if (category === 'transcript') return 'Transcript task'
1170
+ return 'Export task'
1171
+ }
1172
+
789
1173
  function classNames(...values: Array<string | false | null | undefined>) {
790
1174
  return values.filter(Boolean).join(' ')
791
1175
  }
@@ -6,4 +6,5 @@ export const baseImportMap = {
6
6
  'remix/component/jsx-runtime': '/node_modules/remix/component/jsx-runtime',
7
7
  'remix/component/jsx-dev-runtime':
8
8
  '/node_modules/remix/component/jsx-dev-runtime',
9
+ 'match-sorter': '/node_modules/match-sorter',
9
10
  } as const
@@ -17,6 +17,18 @@ const indexHandler = {
17
17
  exports.
18
18
  </p>
19
19
  </header>
20
+ <section class="app-card app-card--full">
21
+ <h2>Processing actions</h2>
22
+ <p class="app-muted">
23
+ Queue chapter edits, transcript cleanup, and export jobs once the
24
+ client loads.
25
+ </p>
26
+ <ul class="app-list">
27
+ <li>Edit a chapter with the latest cut list.</li>
28
+ <li>Combine two chapters into a merged preview.</li>
29
+ <li>Regenerate the transcript or detect command windows.</li>
30
+ </ul>
31
+ </section>
20
32
  <section class="app-card app-card--full">
21
33
  <h2>Timeline editor</h2>
22
34
  <p class="app-muted">
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "eprec",
3
3
  "type": "module",
4
- "version": "1.7.0",
4
+ "version": "1.8.0",
5
5
  "license": "MIT",
6
6
  "repository": {
7
7
  "type": "git",