11'use client' ;
22
3- import type { CSSProperties , MouseEvent } from 'react' ;
3+ import type { CSSProperties , MouseEvent , ReactNode } from 'react' ;
44import { CLIP_SECONDS , PEAKS } from './clips' ;
55
66const TOTAL = CLIP_SECONDS * PEAKS . length ;
@@ -15,6 +15,7 @@ export function Waveform({
1515 showPlayhead,
1616 onSeek,
1717 marker,
18+ seamless = false ,
1819 buffered = [ ] ,
1920 className = 'h-20' ,
2021} : {
@@ -24,6 +25,8 @@ export function Waveform({
2425 onSeek : ( seconds : number ) => void ;
2526 /** The line between the two files, and what to say about it. */
2627 marker : { label : string ; tone : 'good' | 'bad' } ;
28+ /** Join the two panels with no space between them, telling them apart by tint. */
29+ seamless ?: boolean ;
2730 /** Buffered ranges in combined seconds, drawn brighter than unloaded audio. */
2831 buffered ?: readonly ( readonly [ number , number ] ) [ ] ;
2932 className ?: string ;
@@ -46,7 +49,7 @@ export function Waveform({
4649 if ( e . key === 'ArrowRight' ) onSeek ( Math . min ( TOTAL - 0.5 , position + 2 ) ) ;
4750 if ( e . key === 'ArrowLeft' ) onSeek ( Math . max ( 0 , position - 2 ) ) ;
4851 } }
49- className = { `relative grid cursor-pointer grid-cols-2 gap-2 focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-zinc-950 dark:focus-visible:outline-white ${ className } ` }
52+ className = { `relative grid cursor-pointer grid-cols-2 ${ seamless ? '' : ' gap-2' } focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-zinc-950 dark:focus-visible:outline-white ${ className } ` }
5053 >
5154 { PEAKS . map ( ( peaks , clip ) => {
5255 const start = clip * CLIP_SECONDS ;
@@ -58,7 +61,16 @@ export function Waveform({
5861 ) ;
5962 } ;
6063 return (
61- < div key = { clip } className = "flex rounded-md bg-zinc-950/4 px-1.5 dark:bg-white/4" >
64+ < div
65+ key = { clip }
66+ className = { `flex ${
67+ ! seamless
68+ ? 'rounded-md bg-zinc-950/4 px-1.5 dark:bg-white/4'
69+ : clip === 0
70+ ? 'rounded-l-md bg-zinc-950/4 pr-px pl-1.5 dark:bg-white/4'
71+ : 'rounded-r-md bg-zinc-950/8 pr-1.5 dark:bg-white/8'
72+ } `}
73+ >
6274 < div
6375 onClick = { onClick }
6476 className = "relative flex flex-1 items-center gap-px"
@@ -93,10 +105,41 @@ export function Waveform({
93105 className = { `pointer-events-none absolute -inset-y-2 left-1/2 w-0.5 -translate-x-1/2 rounded-full ${ markerColor [ 0 ] } ` }
94106 />
95107 < div
96- className = { `pointer-events-none absolute -top-5 left-1/2 -translate-x-1/2 font-mono text-xs/5 font-medium whitespace-nowrap ${ markerColor [ 1 ] } ` }
108+ className = { `pointer-events-none absolute -top-6 left-1/2 -translate-x-1/2 font-mono text-xs/5 font-medium whitespace-nowrap ${ markerColor [ 1 ] } ` }
97109 >
98110 { marker . label }
99111 </ div >
100112 </ div >
101113 ) ;
102114}
115+
116+ /**
117+ * A bracket under each clip's half of the waveform, with a stem down to its
118+ * label, so the strip reads as two tracks. Spacing matches `Waveform`'s
119+ * panels: pass `seamless` when the waveform does.
120+ */
121+ export function TrackBrackets ( {
122+ labels,
123+ seamless = false ,
124+ small = false ,
125+ } : {
126+ labels : readonly ReactNode [ ] ;
127+ seamless ?: boolean ;
128+ small ?: boolean ;
129+ } ) {
130+ const line = 'border-zinc-950/20 dark:border-white/20' ;
131+ return (
132+ < div className = { `grid grid-cols-2 ${ seamless ? '' : 'gap-2' } ${ small ? 'mt-1.5' : 'mt-2' } ` } >
133+ { labels . map ( ( label , i ) => (
134+ < div key = { i } className = "flex min-w-0 flex-col items-center" >
135+ < div
136+ aria-hidden
137+ className = { `mx-0.5 self-stretch rounded-b-xs border-x border-b ${ line } ${ small ? 'h-1' : 'h-1.5' } ` }
138+ />
139+ < div aria-hidden className = { `w-px border-l ${ line } ${ small ? 'h-1' : 'h-2' } ` } />
140+ { label }
141+ </ div >
142+ ) ) }
143+ </ div >
144+ ) ;
145+ }
0 commit comments