@joewinke/jatui 0.32.0 → 0.34.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +16 -1
- package/dist/actions/reviewKeys.d.ts +159 -0
- package/dist/actions/reviewKeys.js +353 -0
- package/dist/components/VideoPlayer.svelte +437 -68
- package/dist/components/VideoPlayer.svelte.d.ts +17 -0
- package/dist/components/review/MediaZoomLightbox.svelte +628 -0
- package/dist/components/review/MediaZoomLightbox.svelte.d.ts +56 -0
- package/dist/components/review/ReviewFilterBar.svelte +296 -0
- package/dist/components/review/ReviewFilterBar.svelte.d.ts +45 -0
- package/dist/components/review/ReviewNote.svelte +213 -0
- package/dist/components/review/ReviewNote.svelte.d.ts +37 -0
- package/dist/components/review/ReviewProgress.svelte +99 -0
- package/dist/components/review/ReviewProgress.svelte.d.ts +32 -0
- package/dist/components/review/ReviewShortcuts.svelte +80 -0
- package/dist/components/review/ReviewShortcuts.svelte.d.ts +15 -0
- package/dist/components/review/RoundHistory.svelte +151 -0
- package/dist/components/review/RoundHistory.svelte.d.ts +35 -0
- package/dist/components/review/VerdictControl.svelte +163 -0
- package/dist/components/review/VerdictControl.svelte.d.ts +23 -0
- package/dist/components/review/index.d.ts +24 -0
- package/dist/components/review/index.js +23 -0
- package/dist/components/review/mediaZoom.d.ts +115 -0
- package/dist/components/review/mediaZoom.js +161 -0
- package/dist/components/review/reviewSummary.d.ts +101 -0
- package/dist/components/review/reviewSummary.js +122 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +12 -0
- package/package.json +6 -1
|
@@ -251,6 +251,8 @@
|
|
|
251
251
|
|
|
252
252
|
let {
|
|
253
253
|
src,
|
|
254
|
+
kind = 'video',
|
|
255
|
+
transport: transportProp = undefined,
|
|
254
256
|
poster = null,
|
|
255
257
|
aspect = '16:9',
|
|
256
258
|
autoplay = false,
|
|
@@ -339,6 +341,23 @@
|
|
|
339
341
|
class: className = ''
|
|
340
342
|
}: {
|
|
341
343
|
src: string;
|
|
344
|
+
/**
|
|
345
|
+
* What the media IS. `'audio'` (a podcast, a call recording, a brief)
|
|
346
|
+
* renders NO stage: there is no picture to frame, so the player is the
|
|
347
|
+
* band (the one timeline) plus its transport row. The media element
|
|
348
|
+
* still plays, just never shows. `aspect`/`poster` are ignored. Default
|
|
349
|
+
* `'video'`.
|
|
350
|
+
*/
|
|
351
|
+
kind?: 'video' | 'audio';
|
|
352
|
+
/**
|
|
353
|
+
* Who owns play/pause, time and volume. `'native'` keeps the browser's
|
|
354
|
+
* `<video controls>` bar (which carries its own timeline next to the
|
|
355
|
+
* band). `'band'` drops the native controls and puts a transport row
|
|
356
|
+
* on the band instead: play/pause, `current / total`, speed, mute +
|
|
357
|
+
* volume, and fullscreen for video. One timeline, one set of controls.
|
|
358
|
+
* Default: `'band'` for `kind="audio"`, `'native'` for video.
|
|
359
|
+
*/
|
|
360
|
+
transport?: 'native' | 'band';
|
|
342
361
|
poster?: string | null;
|
|
343
362
|
/**
|
|
344
363
|
* Aspect ratio for the video stage — presentation-primitive prop (PRD).
|
|
@@ -723,6 +742,53 @@
|
|
|
723
742
|
let currentTime = $state(0);
|
|
724
743
|
let duration = $state(0);
|
|
725
744
|
|
|
745
|
+
// ── Band transport (kind="audio" / transport="band") ────────────────────
|
|
746
|
+
// Audio has no picture, so no stage; the band is the only timeline and
|
|
747
|
+
// the transport row under it carries play/pause, time, speed and volume.
|
|
748
|
+
const isAudio = $derived(kind === 'audio');
|
|
749
|
+
const bandTransport = $derived(isAudio || transportProp === 'band');
|
|
750
|
+
let paused = $state(true);
|
|
751
|
+
let volumeLevel = $state(1);
|
|
752
|
+
let mutedNow = $state(false);
|
|
753
|
+
let isFullscreen = $state(false);
|
|
754
|
+
function syncVolume() {
|
|
755
|
+
if (!videoEl) return;
|
|
756
|
+
volumeLevel = videoEl.volume;
|
|
757
|
+
mutedNow = videoEl.muted;
|
|
758
|
+
}
|
|
759
|
+
function togglePlay() {
|
|
760
|
+
const v = videoEl;
|
|
761
|
+
if (!v) return;
|
|
762
|
+
if (v.paused) void v.play().catch(() => {});
|
|
763
|
+
else v.pause();
|
|
764
|
+
}
|
|
765
|
+
function toggleVolumeMute() {
|
|
766
|
+
const v = videoEl;
|
|
767
|
+
if (!v) return;
|
|
768
|
+
v.muted = !v.muted;
|
|
769
|
+
if (!v.muted && v.volume === 0) v.volume = 0.5;
|
|
770
|
+
}
|
|
771
|
+
function onVolumeInput(e: Event) {
|
|
772
|
+
const v = videoEl;
|
|
773
|
+
if (!v) return;
|
|
774
|
+
const next = Number((e.currentTarget as HTMLInputElement).value);
|
|
775
|
+
v.volume = Math.min(1, Math.max(0, next));
|
|
776
|
+
v.muted = next === 0;
|
|
777
|
+
}
|
|
778
|
+
function toggleFullscreen() {
|
|
779
|
+
if (typeof document === 'undefined') return;
|
|
780
|
+
if (document.fullscreenElement) void document.exitFullscreen?.();
|
|
781
|
+
else void stageWrapEl?.requestFullscreen?.();
|
|
782
|
+
}
|
|
783
|
+
/** The speed button: the listening speeds people actually pick, in
|
|
784
|
+
* order, wrapping back to 1×. `-`/`=` still walk the fine staircase. */
|
|
785
|
+
const SPEED_CYCLE = [1, 1.25, 1.5, 1.75, 2];
|
|
786
|
+
function cycleSpeed() {
|
|
787
|
+
baseRate = SPEED_CYCLE.find((s) => s > baseRate + 0.001) ?? 1;
|
|
788
|
+
applyRate();
|
|
789
|
+
flashOsd(formatSpeedLabel(baseRate));
|
|
790
|
+
}
|
|
791
|
+
|
|
726
792
|
// Canvas box, tracked so a resize (panel toggle, window resize) repaints.
|
|
727
793
|
let canvasW = $state(0);
|
|
728
794
|
let canvasH = $state(0);
|
|
@@ -1059,6 +1125,9 @@
|
|
|
1059
1125
|
const speakerIndex = $derived(buildSpeakerIndex(sortedTurns, sortedSegments));
|
|
1060
1126
|
|
|
1061
1127
|
const hasSpeakers = $derived(speakerIndex.size > 0);
|
|
1128
|
+
/** Two or more voices: only then do talk time, solo and skip mean
|
|
1129
|
+
* anything, and only then does a transcript row need its speaker name. */
|
|
1130
|
+
const multiSpeaker = $derived(speakerIndex.size > 1);
|
|
1062
1131
|
|
|
1063
1132
|
/**
|
|
1064
1133
|
* Theme-derived speaker identity: colours + the fallback cycle in one read
|
|
@@ -2614,6 +2683,9 @@
|
|
|
2614
2683
|
const target = e.target as HTMLElement | null;
|
|
2615
2684
|
const tag = target?.tagName;
|
|
2616
2685
|
if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT' || target?.isContentEditable) return;
|
|
2686
|
+
// Space/Enter on a focused button activates THAT button (play, a
|
|
2687
|
+
// speaker chip, a transcript row); toggling playback too would undo it.
|
|
2688
|
+
if (tag === 'BUTTON' && (e.key === ' ' || e.key === 'Enter')) return;
|
|
2617
2689
|
// mos-777hj — the chapter rail owns j/k (listNav) and Enter/Space (row
|
|
2618
2690
|
// activation) while focus is inside it; without this guard a rail `k`
|
|
2619
2691
|
// would also toggle play/pause and a rail `j` also seek ±5s.
|
|
@@ -2874,12 +2946,14 @@
|
|
|
2874
2946
|
class:vp-side={transcriptPosition === 'side' && sortedSegments.length > 0 && transcriptVisible}
|
|
2875
2947
|
class:vp-rail-side={railOn}
|
|
2876
2948
|
class:vp-one-axis={nativeBarHidden}
|
|
2949
|
+
class:vp-band-transport={bandTransport}
|
|
2950
|
+
class:vp-audio={isAudio}
|
|
2877
2951
|
class:vp-night-capable={nightPresent}
|
|
2878
2952
|
class:vp-night-surround={surroundMix > 0.001}
|
|
2879
2953
|
style:--vp-night-surround={surroundMix > 0.001 ? surroundMix.toFixed(3) : undefined}
|
|
2880
2954
|
data-band-axis={currentBandState.axis}
|
|
2881
2955
|
role="region"
|
|
2882
|
-
aria-label=
|
|
2956
|
+
aria-label={isAudio ? 'Audio player' : 'Video player'}
|
|
2883
2957
|
tabindex="-1"
|
|
2884
2958
|
onkeydown={onRootKeyDown}
|
|
2885
2959
|
onpointerdowncapture={maybeAskIdle}
|
|
@@ -2888,7 +2962,12 @@
|
|
|
2888
2962
|
onkeydowncapture={maybeAskIdle}
|
|
2889
2963
|
>
|
|
2890
2964
|
<div class="vp-main">
|
|
2891
|
-
<div
|
|
2965
|
+
<div
|
|
2966
|
+
class="vp-stage-wrap"
|
|
2967
|
+
class:vp-stage-audio={isAudio}
|
|
2968
|
+
bind:this={stageWrapEl}
|
|
2969
|
+
onfullscreenchange={() => (isFullscreen = document.fullscreenElement === stageWrapEl)}
|
|
2970
|
+
>
|
|
2892
2971
|
{#if nightPresent}
|
|
2893
2972
|
<!-- Bias light: the frame's own colours, blurred, spilled behind the
|
|
2894
2973
|
picture. Earlier in the DOM than the (positioned) stage, so the
|
|
@@ -2900,9 +2979,13 @@
|
|
|
2900
2979
|
aria-hidden="true"
|
|
2901
2980
|
></div>
|
|
2902
2981
|
{/if}
|
|
2903
|
-
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
2982
|
+
<!-- svelte-ignore a11y_no_static_element_interactions, a11y_click_events_have_key_events -->
|
|
2983
|
+
<!-- With the band transport the native controls are gone, so a click
|
|
2984
|
+
on the picture plays/pauses (Space/K do the same from the keyboard). -->
|
|
2904
2985
|
<div
|
|
2905
2986
|
class="vp-stage"
|
|
2987
|
+
class:vp-stage-clickable={bandTransport && !isAudio}
|
|
2988
|
+
onclick={bandTransport && !isAudio ? togglePlay : undefined}
|
|
2906
2989
|
class:vp-stage-locked={stageAspect != null}
|
|
2907
2990
|
class:vp-night-capable={nightPresent || guardPresent}
|
|
2908
2991
|
class:vp-night-on={nightFrame != null}
|
|
@@ -2919,6 +3002,8 @@
|
|
|
2919
3002
|
bind:this={videoEl}
|
|
2920
3003
|
bind:currentTime
|
|
2921
3004
|
bind:duration
|
|
3005
|
+
bind:paused
|
|
3006
|
+
onvolumechange={syncVolume}
|
|
2922
3007
|
ontimeupdate={handleTimeUpdate}
|
|
2923
3008
|
onloadedmetadata={onLoadedMetadata}
|
|
2924
3009
|
onseeked={() => {
|
|
@@ -2944,8 +3029,8 @@
|
|
|
2944
3029
|
{loop}
|
|
2945
3030
|
crossorigin={crossorigin ?? undefined}
|
|
2946
3031
|
muted={effectiveAutoplay || undefined}
|
|
2947
|
-
poster={poster ?? undefined}
|
|
2948
|
-
controls
|
|
3032
|
+
poster={isAudio ? undefined : (poster ?? undefined)}
|
|
3033
|
+
controls={!bandTransport}
|
|
2949
3034
|
playsinline
|
|
2950
3035
|
class="vp-video"
|
|
2951
3036
|
>
|
|
@@ -3070,6 +3155,10 @@
|
|
|
3070
3155
|
</div>
|
|
3071
3156
|
|
|
3072
3157
|
<div class="vp-zoom" class:on={zoomLabelOn} aria-hidden="true">{zoomLabel}</div>
|
|
3158
|
+
{#if isAudio}
|
|
3159
|
+
<!-- No stage to float the readout over: it rides the band. -->
|
|
3160
|
+
<div class="vp-osd vp-osd-band" class:on={osdOn} aria-hidden="true">{osd}</div>
|
|
3161
|
+
{/if}
|
|
3073
3162
|
|
|
3074
3163
|
{#if hover}
|
|
3075
3164
|
<div
|
|
@@ -3099,8 +3188,7 @@
|
|
|
3099
3188
|
{/if}
|
|
3100
3189
|
</div>
|
|
3101
3190
|
|
|
3102
|
-
|
|
3103
|
-
<span>{formatTime(currentTime, useHours)}</span>
|
|
3191
|
+
{#snippet timesMid()}
|
|
3104
3192
|
<span class="vp-times-mid">
|
|
3105
3193
|
{#if isZoomedNow()}
|
|
3106
3194
|
<button type="button" class="vp-reset-zoom" onclick={() => { resetZoom(); flashZoomLabel(); }}>
|
|
@@ -3145,28 +3233,118 @@
|
|
|
3145
3233
|
</button>
|
|
3146
3234
|
{/if}
|
|
3147
3235
|
</span>
|
|
3148
|
-
|
|
3149
|
-
|
|
3236
|
+
{/snippet}
|
|
3237
|
+
|
|
3238
|
+
{#if bandTransport}
|
|
3239
|
+
<!-- The band transport: the band above is the one timeline; this row
|
|
3240
|
+
carries everything the native bar used to (play/pause, time,
|
|
3241
|
+
speed, volume, fullscreen for video). -->
|
|
3242
|
+
<div class="vp-times vp-transport">
|
|
3243
|
+
<span class="vp-transport-start">
|
|
3244
|
+
<button
|
|
3245
|
+
type="button"
|
|
3246
|
+
class="vp-play"
|
|
3247
|
+
aria-label={paused ? 'Play' : 'Pause'}
|
|
3248
|
+
title="{paused ? 'Play' : 'Pause'} · Space"
|
|
3249
|
+
onclick={togglePlay}
|
|
3250
|
+
>
|
|
3251
|
+
{#if paused}
|
|
3252
|
+
<svg viewBox="0 0 24 24" width="16" height="16" aria-hidden="true">
|
|
3253
|
+
<path d="M8 5.5v13l10.5-6.5z" fill="currentColor" />
|
|
3254
|
+
</svg>
|
|
3255
|
+
{:else}
|
|
3256
|
+
<svg viewBox="0 0 24 24" width="16" height="16" aria-hidden="true">
|
|
3257
|
+
<path d="M7 5h3.5v14H7zM13.5 5H17v14h-3.5z" fill="currentColor" />
|
|
3258
|
+
</svg>
|
|
3259
|
+
{/if}
|
|
3260
|
+
</button>
|
|
3261
|
+
<span class="vp-clock">
|
|
3262
|
+
<span class="vp-clock-now">{formatTime(currentTime, useHours)}</span>
|
|
3263
|
+
<span class="vp-clock-sep" aria-hidden="true">/</span>
|
|
3264
|
+
<span class="vp-sr">of</span>
|
|
3265
|
+
<span>{formatTime(effectiveDuration, useHours)}</span>
|
|
3266
|
+
</span>
|
|
3267
|
+
</span>
|
|
3268
|
+
{@render timesMid()}
|
|
3269
|
+
<span class="vp-transport-end">
|
|
3270
|
+
<button
|
|
3271
|
+
type="button"
|
|
3272
|
+
class="vp-tbtn vp-speed"
|
|
3273
|
+
aria-label="Playback speed {formatSpeedLabel(baseRate)}"
|
|
3274
|
+
title="Speed · click to change, - and = step"
|
|
3275
|
+
onclick={cycleSpeed}
|
|
3276
|
+
>
|
|
3277
|
+
{formatSpeedLabel(baseRate)}
|
|
3278
|
+
</button>
|
|
3279
|
+
<button
|
|
3280
|
+
type="button"
|
|
3281
|
+
class="vp-tbtn"
|
|
3282
|
+
aria-label={mutedNow || volumeLevel === 0 ? 'Unmute' : 'Mute'}
|
|
3283
|
+
aria-pressed={mutedNow}
|
|
3284
|
+
title="{mutedNow ? 'Unmute' : 'Mute'} · M"
|
|
3285
|
+
onclick={toggleVolumeMute}
|
|
3286
|
+
>
|
|
3287
|
+
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
3288
|
+
<path d="M4 9.5h3.5L12 5.5v13l-4.5-4H4z" fill="currentColor" stroke="none" />
|
|
3289
|
+
{#if mutedNow || volumeLevel === 0}
|
|
3290
|
+
<path d="M16 9.5l5 5M21 9.5l-5 5" />
|
|
3291
|
+
{:else}
|
|
3292
|
+
<path d="M15.5 9a4 4 0 0 1 0 6" />
|
|
3293
|
+
{#if volumeLevel > 0.5}<path d="M18 6.5a7.5 7.5 0 0 1 0 11" />{/if}
|
|
3294
|
+
{/if}
|
|
3295
|
+
</svg>
|
|
3296
|
+
</button>
|
|
3297
|
+
<input
|
|
3298
|
+
type="range"
|
|
3299
|
+
class="vp-volume"
|
|
3300
|
+
min="0"
|
|
3301
|
+
max="1"
|
|
3302
|
+
step="0.05"
|
|
3303
|
+
value={mutedNow ? 0 : volumeLevel}
|
|
3304
|
+
aria-label="Volume"
|
|
3305
|
+
aria-valuetext="{Math.round((mutedNow ? 0 : volumeLevel) * 100)}%"
|
|
3306
|
+
oninput={onVolumeInput}
|
|
3307
|
+
/>
|
|
3308
|
+
{#if !isAudio}
|
|
3309
|
+
<button
|
|
3310
|
+
type="button"
|
|
3311
|
+
class="vp-tbtn"
|
|
3312
|
+
aria-label={isFullscreen ? 'Exit full screen' : 'Full screen'}
|
|
3313
|
+
title={isFullscreen ? 'Exit full screen' : 'Full screen'}
|
|
3314
|
+
onclick={toggleFullscreen}
|
|
3315
|
+
>
|
|
3316
|
+
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
3317
|
+
{#if isFullscreen}
|
|
3318
|
+
<path d="M9 4v5H4M15 4v5h5M9 20v-5H4M15 20v-5h5" />
|
|
3319
|
+
{:else}
|
|
3320
|
+
<path d="M4 9V4h5M20 9V4h-5M4 15v5h5M20 15v5h-5" />
|
|
3321
|
+
{/if}
|
|
3322
|
+
</svg>
|
|
3323
|
+
</button>
|
|
3324
|
+
{/if}
|
|
3325
|
+
</span>
|
|
3326
|
+
</div>
|
|
3327
|
+
{:else}
|
|
3328
|
+
<div class="vp-times">
|
|
3329
|
+
<span>{formatTime(currentTime, useHours)}</span>
|
|
3330
|
+
{@render timesMid()}
|
|
3331
|
+
<span>{formatTime(effectiveDuration, useHours)}</span>
|
|
3332
|
+
</div>
|
|
3333
|
+
{/if}
|
|
3150
3334
|
|
|
3151
|
-
{#if
|
|
3335
|
+
{#if multiSpeaker}
|
|
3152
3336
|
<!-- Talk-time doubles as the speaker legend: hover a chip to raise that
|
|
3153
|
-
speaker on the timeline, click to solo, and use its
|
|
3154
|
-
skip them. Identification through brightness, never a glow.
|
|
3155
|
-
|
|
3156
|
-
|
|
3157
|
-
|
|
3158
|
-
|
|
3159
|
-
|
|
3160
|
-
style="width: {(tt.seconds / talkTimeTotal) * 100}%; background: {speakerColor(
|
|
3161
|
-
talkTimeKey(tt)
|
|
3162
|
-
)}"
|
|
3163
|
-
title="{tt.speakerName ?? 'Unknown speaker'} — {formatDuration(tt.seconds)}"
|
|
3164
|
-
></div>
|
|
3165
|
-
{/each}
|
|
3166
|
-
</div>
|
|
3337
|
+
speaker on the timeline, click to solo, and use its skip toggle to
|
|
3338
|
+
skip them. Identification through brightness, never a glow. Only
|
|
3339
|
+
with 2+ speakers: one voice has nothing to compare, solo or skip,
|
|
3340
|
+
and its talk time reads as a second, different duration. Labelled
|
|
3341
|
+
"Talk time" so a chip's seconds never read as the media's length. -->
|
|
3342
|
+
<div class="vp-talktime" role="group" aria-label="Talk time by speaker">
|
|
3343
|
+
<span class="vp-talktime-label" aria-hidden="true">Talk time</span>
|
|
3167
3344
|
<div class="vp-legend">
|
|
3168
3345
|
{#each effectiveTalkTime as tt, i (talkTimeKey(tt) + ':' + i)}
|
|
3169
3346
|
{@const key = talkTimeKey(tt)}
|
|
3347
|
+
{@const name = tt.speakerName ?? 'Unknown speaker'}
|
|
3170
3348
|
<span class="vp-legend-item">
|
|
3171
3349
|
<button
|
|
3172
3350
|
type="button"
|
|
@@ -3179,10 +3357,11 @@
|
|
|
3179
3357
|
onblur={() => (highlightKey = null)}
|
|
3180
3358
|
onclick={() => toggleSolo(key)}
|
|
3181
3359
|
aria-pressed={soloKey === key}
|
|
3182
|
-
|
|
3360
|
+
aria-label="{name}, talked {formatDuration(tt.seconds)}. Play only {name}"
|
|
3361
|
+
title="Play only {name}"
|
|
3183
3362
|
>
|
|
3184
3363
|
<span class="vp-chip-dot" style="background: {speakerColor(key)}"></span>
|
|
3185
|
-
{
|
|
3364
|
+
{name}
|
|
3186
3365
|
<span class="vp-chip-time">{formatDuration(tt.seconds)}</span>
|
|
3187
3366
|
</button>
|
|
3188
3367
|
<button
|
|
@@ -3191,11 +3370,12 @@
|
|
|
3191
3370
|
class:on={mutedKeys.has(key)}
|
|
3192
3371
|
onclick={() => toggleMute(key)}
|
|
3193
3372
|
aria-pressed={mutedKeys.has(key)}
|
|
3194
|
-
aria-label="
|
|
3195
|
-
|
|
3196
|
-
title="{mutedKeys.has(key) ? 'Unskip' : 'Skip'} this speaker"
|
|
3373
|
+
aria-label="Skip {name}"
|
|
3374
|
+
title="{mutedKeys.has(key) ? 'Stop skipping' : 'Skip'} {name}"
|
|
3197
3375
|
>
|
|
3198
|
-
|
|
3376
|
+
<svg viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
3377
|
+
<path d="M5 6l8 6-8 6zM18 6v12" />
|
|
3378
|
+
</svg>
|
|
3199
3379
|
</button>
|
|
3200
3380
|
</span>
|
|
3201
3381
|
{/each}
|
|
@@ -3208,7 +3388,7 @@
|
|
|
3208
3388
|
mutedKeys = new Set();
|
|
3209
3389
|
}}
|
|
3210
3390
|
>
|
|
3211
|
-
|
|
3391
|
+
Show all speakers
|
|
3212
3392
|
</button>
|
|
3213
3393
|
{/if}
|
|
3214
3394
|
</div>
|
|
@@ -3320,6 +3500,7 @@
|
|
|
3320
3500
|
<button
|
|
3321
3501
|
type="button"
|
|
3322
3502
|
class="vp-row"
|
|
3503
|
+
class:vp-row-one={!multiSpeaker}
|
|
3323
3504
|
class:active={activeSegment?.id === seg.id}
|
|
3324
3505
|
class:vp-dim={highlightKey != null
|
|
3325
3506
|
? highlightKey !== speakerKeyOf(seg)
|
|
@@ -3328,9 +3509,11 @@
|
|
|
3328
3509
|
onclick={() => seekTo(seg.startSec)}
|
|
3329
3510
|
>
|
|
3330
3511
|
<span class="vp-row-time">{formatTime(seg.startSec, useHours)}</span>
|
|
3331
|
-
|
|
3332
|
-
|
|
3333
|
-
|
|
3512
|
+
{#if multiSpeaker}
|
|
3513
|
+
<span class="vp-row-speaker" style="color: {speakerColor(speakerKeyOf(seg))}">
|
|
3514
|
+
{seg.speakerName ?? 'Unknown speaker'}
|
|
3515
|
+
</span>
|
|
3516
|
+
{/if}
|
|
3334
3517
|
<span class="vp-row-text">{seg.text}</span>
|
|
3335
3518
|
</button>
|
|
3336
3519
|
{/each}
|
|
@@ -3340,6 +3523,7 @@
|
|
|
3340
3523
|
<button
|
|
3341
3524
|
type="button"
|
|
3342
3525
|
class="vp-row"
|
|
3526
|
+
class:vp-row-one={!multiSpeaker}
|
|
3343
3527
|
class:active={activeSegment?.id === seg.id}
|
|
3344
3528
|
class:vp-dim={highlightKey != null
|
|
3345
3529
|
? highlightKey !== speakerKeyOf(seg)
|
|
@@ -3348,9 +3532,11 @@
|
|
|
3348
3532
|
onclick={() => seekTo(seg.startSec)}
|
|
3349
3533
|
>
|
|
3350
3534
|
<span class="vp-row-time">{formatTime(seg.startSec, useHours)}</span>
|
|
3351
|
-
|
|
3352
|
-
|
|
3353
|
-
|
|
3535
|
+
{#if multiSpeaker}
|
|
3536
|
+
<span class="vp-row-speaker" style="color: {speakerColor(speakerKeyOf(seg))}">
|
|
3537
|
+
{seg.speakerName ?? 'Unknown speaker'}
|
|
3538
|
+
</span>
|
|
3539
|
+
{/if}
|
|
3354
3540
|
<span class="vp-row-text">{seg.text}</span>
|
|
3355
3541
|
</button>
|
|
3356
3542
|
{/each}
|
|
@@ -3465,6 +3651,7 @@
|
|
|
3465
3651
|
position: relative;
|
|
3466
3652
|
display: flex;
|
|
3467
3653
|
flex-direction: column;
|
|
3654
|
+
flex-shrink: 0;
|
|
3468
3655
|
gap: 2px;
|
|
3469
3656
|
background: none;
|
|
3470
3657
|
border: none;
|
|
@@ -3479,9 +3666,13 @@
|
|
|
3479
3666
|
background: color-mix(in oklch, var(--color-base-content) 8%, transparent);
|
|
3480
3667
|
}
|
|
3481
3668
|
|
|
3669
|
+
/* Active = a tinted row plus a primary title; no side stripe. */
|
|
3482
3670
|
.vp-chapter-row.active {
|
|
3483
|
-
background: color-mix(in oklch, var(--color-primary)
|
|
3484
|
-
|
|
3671
|
+
background: color-mix(in oklch, var(--color-primary) 12%, transparent);
|
|
3672
|
+
}
|
|
3673
|
+
|
|
3674
|
+
.vp-chapter-row.active .vp-chapter-title {
|
|
3675
|
+
color: var(--color-primary);
|
|
3485
3676
|
}
|
|
3486
3677
|
|
|
3487
3678
|
/* listNav's cursor (j/k) — same family as the hover/active surfaces. */
|
|
@@ -3494,6 +3685,12 @@
|
|
|
3494
3685
|
color: color-mix(in oklch, var(--color-base-content) 55%, transparent);
|
|
3495
3686
|
}
|
|
3496
3687
|
|
|
3688
|
+
/* The playhead's chapter reads as current even once its span is played
|
|
3689
|
+
(playback ran past the last tagged line). */
|
|
3690
|
+
.vp-chapter-row.done.active .vp-chapter-title {
|
|
3691
|
+
color: var(--color-primary);
|
|
3692
|
+
}
|
|
3693
|
+
|
|
3497
3694
|
.vp-chapter-head {
|
|
3498
3695
|
display: flex;
|
|
3499
3696
|
align-items: baseline;
|
|
@@ -3608,6 +3805,7 @@
|
|
|
3608
3805
|
|
|
3609
3806
|
.vp-chapter-row {
|
|
3610
3807
|
flex: none;
|
|
3808
|
+
justify-content: center;
|
|
3611
3809
|
max-width: 16rem;
|
|
3612
3810
|
padding: var(--jatui-space-xs, 2px) var(--jatui-space-sm, 4px);
|
|
3613
3811
|
}
|
|
@@ -3726,10 +3924,133 @@
|
|
|
3726
3924
|
}
|
|
3727
3925
|
|
|
3728
3926
|
.vp-root:hover .vp-band,
|
|
3729
|
-
.vp-band:focus-within
|
|
3927
|
+
.vp-band:focus-within,
|
|
3928
|
+
.vp-band-transport .vp-band {
|
|
3929
|
+
/* With the band transport the band is the ONLY timeline: never ambient. */
|
|
3730
3930
|
opacity: 1;
|
|
3731
3931
|
}
|
|
3732
3932
|
|
|
3933
|
+
/* Audio: no picture, so no stage at all. The element still plays. */
|
|
3934
|
+
.vp-stage-audio {
|
|
3935
|
+
display: none;
|
|
3936
|
+
}
|
|
3937
|
+
|
|
3938
|
+
.vp-stage-clickable {
|
|
3939
|
+
cursor: pointer;
|
|
3940
|
+
}
|
|
3941
|
+
|
|
3942
|
+
/* The OSD's band seat (audio has no stage to float it over). */
|
|
3943
|
+
.vp-osd-band {
|
|
3944
|
+
top: 0;
|
|
3945
|
+
left: 0;
|
|
3946
|
+
max-width: 100%;
|
|
3947
|
+
overflow: hidden;
|
|
3948
|
+
text-overflow: ellipsis;
|
|
3949
|
+
white-space: nowrap;
|
|
3950
|
+
z-index: 4;
|
|
3951
|
+
}
|
|
3952
|
+
|
|
3953
|
+
/* ── Band transport row ─────────────────────────────────────────────── */
|
|
3954
|
+
.vp-transport {
|
|
3955
|
+
font-size: 0.75rem;
|
|
3956
|
+
}
|
|
3957
|
+
|
|
3958
|
+
.vp-transport-start,
|
|
3959
|
+
.vp-transport-end {
|
|
3960
|
+
display: inline-flex;
|
|
3961
|
+
align-items: center;
|
|
3962
|
+
gap: var(--jatui-space-sm, 4px);
|
|
3963
|
+
min-width: 0;
|
|
3964
|
+
}
|
|
3965
|
+
|
|
3966
|
+
.vp-play {
|
|
3967
|
+
display: inline-grid;
|
|
3968
|
+
place-items: center;
|
|
3969
|
+
width: 2rem;
|
|
3970
|
+
height: 2rem;
|
|
3971
|
+
flex: none;
|
|
3972
|
+
border: none;
|
|
3973
|
+
border-radius: 999px;
|
|
3974
|
+
background: var(--color-primary);
|
|
3975
|
+
color: var(--color-primary-content);
|
|
3976
|
+
cursor: pointer;
|
|
3977
|
+
transition: transform var(--motion-fast, 150ms) var(--motion-glide, ease-out);
|
|
3978
|
+
}
|
|
3979
|
+
|
|
3980
|
+
.vp-play:hover {
|
|
3981
|
+
transform: scale(1.06);
|
|
3982
|
+
}
|
|
3983
|
+
|
|
3984
|
+
.vp-play:active {
|
|
3985
|
+
transform: scale(0.96);
|
|
3986
|
+
}
|
|
3987
|
+
|
|
3988
|
+
.vp-clock {
|
|
3989
|
+
display: inline-flex;
|
|
3990
|
+
gap: 0.3em;
|
|
3991
|
+
margin-left: var(--jatui-space-xs, 2px);
|
|
3992
|
+
white-space: nowrap;
|
|
3993
|
+
}
|
|
3994
|
+
|
|
3995
|
+
.vp-clock-now {
|
|
3996
|
+
color: var(--color-base-content);
|
|
3997
|
+
font-weight: 600;
|
|
3998
|
+
}
|
|
3999
|
+
|
|
4000
|
+
.vp-clock-sep {
|
|
4001
|
+
opacity: 0.5;
|
|
4002
|
+
}
|
|
4003
|
+
|
|
4004
|
+
.vp-tbtn {
|
|
4005
|
+
display: inline-grid;
|
|
4006
|
+
place-items: center;
|
|
4007
|
+
min-width: 1.75rem;
|
|
4008
|
+
height: 1.75rem;
|
|
4009
|
+
padding: 0 var(--jatui-space-xs, 2px);
|
|
4010
|
+
border: none;
|
|
4011
|
+
border-radius: var(--jatui-radius-field, 0.25rem);
|
|
4012
|
+
background: none;
|
|
4013
|
+
color: color-mix(in oklch, var(--color-base-content) 70%, transparent);
|
|
4014
|
+
font-size: inherit;
|
|
4015
|
+
font-variant-numeric: tabular-nums;
|
|
4016
|
+
cursor: pointer;
|
|
4017
|
+
}
|
|
4018
|
+
|
|
4019
|
+
.vp-tbtn:hover {
|
|
4020
|
+
color: var(--color-base-content);
|
|
4021
|
+
background: color-mix(in oklch, var(--color-base-content) 8%, transparent);
|
|
4022
|
+
}
|
|
4023
|
+
|
|
4024
|
+
.vp-play:focus-visible,
|
|
4025
|
+
.vp-tbtn:focus-visible,
|
|
4026
|
+
.vp-volume:focus-visible {
|
|
4027
|
+
outline: 2px solid var(--color-primary);
|
|
4028
|
+
outline-offset: 2px;
|
|
4029
|
+
}
|
|
4030
|
+
|
|
4031
|
+
.vp-speed {
|
|
4032
|
+
min-width: 2.5rem;
|
|
4033
|
+
}
|
|
4034
|
+
|
|
4035
|
+
.vp-volume {
|
|
4036
|
+
width: 4.5rem;
|
|
4037
|
+
accent-color: var(--color-primary);
|
|
4038
|
+
cursor: pointer;
|
|
4039
|
+
}
|
|
4040
|
+
|
|
4041
|
+
/* A fingertip sets volume with the device buttons; the slider is noise.
|
|
4042
|
+
The play button grows to a full 44px target (and stays round). */
|
|
4043
|
+
@media (pointer: coarse) {
|
|
4044
|
+
.vp-volume {
|
|
4045
|
+
display: none;
|
|
4046
|
+
}
|
|
4047
|
+
|
|
4048
|
+
.vp-play {
|
|
4049
|
+
width: 2.75rem;
|
|
4050
|
+
height: 2.75rem;
|
|
4051
|
+
}
|
|
4052
|
+
}
|
|
4053
|
+
|
|
3733
4054
|
/* One time axis: while the band owns it, the native timeline stands down.
|
|
3734
4055
|
Only Chromium exposes it; elsewhere this matches nothing and the native
|
|
3735
4056
|
bar simply stays (see video/band/axis.ts). */
|
|
@@ -3949,20 +4270,14 @@
|
|
|
3949
4270
|
/* Talk-time + legend */
|
|
3950
4271
|
.vp-talktime {
|
|
3951
4272
|
display: flex;
|
|
3952
|
-
flex-
|
|
3953
|
-
|
|
3954
|
-
|
|
3955
|
-
|
|
3956
|
-
.vp-talktime-bar {
|
|
3957
|
-
display: flex;
|
|
3958
|
-
height: 0.6rem;
|
|
3959
|
-
border-radius: 999px;
|
|
3960
|
-
overflow: hidden;
|
|
3961
|
-
background: color-mix(in oklch, var(--color-base-content) 10%, transparent);
|
|
4273
|
+
flex-wrap: wrap;
|
|
4274
|
+
align-items: center;
|
|
4275
|
+
gap: var(--jatui-space-sm, 4px) var(--jatui-space-base, 8px);
|
|
3962
4276
|
}
|
|
3963
4277
|
|
|
3964
|
-
.vp-talktime-
|
|
3965
|
-
|
|
4278
|
+
.vp-talktime-label {
|
|
4279
|
+
font-size: 0.68rem;
|
|
4280
|
+
color: color-mix(in oklch, var(--color-base-content) 60%, transparent);
|
|
3966
4281
|
}
|
|
3967
4282
|
|
|
3968
4283
|
.vp-legend {
|
|
@@ -3999,7 +4314,10 @@
|
|
|
3999
4314
|
}
|
|
4000
4315
|
|
|
4001
4316
|
.vp-chip-clear {
|
|
4317
|
+
background: none;
|
|
4002
4318
|
color: var(--color-primary);
|
|
4319
|
+
text-decoration: underline;
|
|
4320
|
+
text-underline-offset: 2px;
|
|
4003
4321
|
}
|
|
4004
4322
|
|
|
4005
4323
|
.vp-chip-dot {
|
|
@@ -4015,18 +4333,31 @@
|
|
|
4015
4333
|
}
|
|
4016
4334
|
|
|
4017
4335
|
.vp-mute {
|
|
4336
|
+
display: inline-grid;
|
|
4337
|
+
place-items: center;
|
|
4338
|
+
width: 1.5rem;
|
|
4339
|
+
height: 1.5rem;
|
|
4018
4340
|
background: none;
|
|
4019
4341
|
border: none;
|
|
4020
|
-
|
|
4021
|
-
|
|
4022
|
-
|
|
4342
|
+
border-radius: var(--jatui-radius-field, 0.25rem);
|
|
4343
|
+
padding: 0;
|
|
4344
|
+
color: color-mix(in oklch, var(--color-base-content) 50%, transparent);
|
|
4023
4345
|
cursor: pointer;
|
|
4024
|
-
opacity: 0.5;
|
|
4025
4346
|
}
|
|
4026
4347
|
|
|
4027
|
-
.vp-mute.on,
|
|
4028
4348
|
.vp-mute:hover {
|
|
4029
|
-
|
|
4349
|
+
color: var(--color-base-content);
|
|
4350
|
+
background: color-mix(in oklch, var(--color-base-content) 8%, transparent);
|
|
4351
|
+
}
|
|
4352
|
+
|
|
4353
|
+
.vp-mute.on {
|
|
4354
|
+
color: var(--color-primary);
|
|
4355
|
+
}
|
|
4356
|
+
|
|
4357
|
+
.vp-mute:focus-visible,
|
|
4358
|
+
.vp-chip:focus-visible {
|
|
4359
|
+
outline: 2px solid var(--color-primary);
|
|
4360
|
+
outline-offset: 1px;
|
|
4030
4361
|
}
|
|
4031
4362
|
|
|
4032
4363
|
/* Add-bookmark row — only rendered when the host wired onAddBookmark. */
|
|
@@ -4090,6 +4421,7 @@
|
|
|
4090
4421
|
border-radius: var(--jatui-radius-box, 0.5rem);
|
|
4091
4422
|
padding: var(--jatui-space-sm, 4px);
|
|
4092
4423
|
min-width: 0;
|
|
4424
|
+
container: vp-transcript / inline-size;
|
|
4093
4425
|
}
|
|
4094
4426
|
|
|
4095
4427
|
/* Windowing stand-ins (mos-5lzns.95): fixed-height blocks sized by the
|
|
@@ -4100,11 +4432,21 @@
|
|
|
4100
4432
|
pointer-events: none;
|
|
4101
4433
|
}
|
|
4102
4434
|
|
|
4435
|
+
/* Rows are a three-column grid (time | speaker | text) so every line's
|
|
4436
|
+
text starts on one edge whatever the speaker's name length; one-voice
|
|
4437
|
+
transcripts drop the speaker column. A narrow pane (container query,
|
|
4438
|
+
not viewport: the player often sits in a narrow column) stacks the
|
|
4439
|
+
speaker above the text. */
|
|
4103
4440
|
.vp-row {
|
|
4104
|
-
display:
|
|
4441
|
+
display: grid;
|
|
4442
|
+
grid-template-columns: 2.75rem minmax(0, 6.5rem) minmax(0, 1fr);
|
|
4105
4443
|
align-items: baseline;
|
|
4106
|
-
|
|
4107
|
-
|
|
4444
|
+
/* Never shrink inside the capped pane: a host's touch min-height
|
|
4445
|
+
replaces min-height:auto, and the flex column would then squash
|
|
4446
|
+
rows to that floor and overlap their text. */
|
|
4447
|
+
flex-shrink: 0;
|
|
4448
|
+
column-gap: var(--jatui-space-base, 8px);
|
|
4449
|
+
row-gap: 0;
|
|
4108
4450
|
background: none;
|
|
4109
4451
|
border: none;
|
|
4110
4452
|
border-radius: var(--jatui-radius-field, 0.25rem);
|
|
@@ -4119,8 +4461,11 @@
|
|
|
4119
4461
|
}
|
|
4120
4462
|
|
|
4121
4463
|
.vp-row.active {
|
|
4122
|
-
background: color-mix(in oklch, var(--color-primary)
|
|
4123
|
-
|
|
4464
|
+
background: color-mix(in oklch, var(--color-primary) 12%, transparent);
|
|
4465
|
+
}
|
|
4466
|
+
|
|
4467
|
+
.vp-row.active .vp-row-time {
|
|
4468
|
+
color: var(--color-primary);
|
|
4124
4469
|
}
|
|
4125
4470
|
|
|
4126
4471
|
.vp-row-time {
|
|
@@ -4133,14 +4478,32 @@
|
|
|
4133
4478
|
.vp-row-speaker {
|
|
4134
4479
|
font-size: 0.72rem;
|
|
4135
4480
|
font-weight: 600;
|
|
4136
|
-
|
|
4481
|
+
min-width: 0;
|
|
4482
|
+
overflow: hidden;
|
|
4483
|
+
text-overflow: ellipsis;
|
|
4484
|
+
white-space: nowrap;
|
|
4137
4485
|
}
|
|
4138
4486
|
|
|
4139
4487
|
.vp-row-text {
|
|
4140
|
-
font-size: 0.
|
|
4488
|
+
font-size: 0.8125rem;
|
|
4489
|
+
line-height: 1.45;
|
|
4141
4490
|
color: color-mix(in oklch, var(--color-base-content) 85%, transparent);
|
|
4142
|
-
|
|
4143
|
-
|
|
4491
|
+
min-width: 0;
|
|
4492
|
+
}
|
|
4493
|
+
|
|
4494
|
+
.vp-row.vp-row-one {
|
|
4495
|
+
grid-template-columns: 2.75rem minmax(0, 1fr);
|
|
4496
|
+
}
|
|
4497
|
+
|
|
4498
|
+
@container vp-transcript (max-width: 26rem) {
|
|
4499
|
+
.vp-row {
|
|
4500
|
+
grid-template-columns: 2.75rem minmax(0, 1fr);
|
|
4501
|
+
}
|
|
4502
|
+
|
|
4503
|
+
.vp-row-speaker,
|
|
4504
|
+
.vp-row-text {
|
|
4505
|
+
grid-column: 2;
|
|
4506
|
+
}
|
|
4144
4507
|
}
|
|
4145
4508
|
|
|
4146
4509
|
/* Keyboard help overlay — the `?` surface. Keycaps first, description after;
|
|
@@ -4226,9 +4589,15 @@
|
|
|
4226
4589
|
.vp-add-btn,
|
|
4227
4590
|
.vp-row,
|
|
4228
4591
|
.vp-chapter-row,
|
|
4229
|
-
.vp-chapter-fill
|
|
4592
|
+
.vp-chapter-fill,
|
|
4593
|
+
.vp-play {
|
|
4230
4594
|
transition: none;
|
|
4231
4595
|
}
|
|
4596
|
+
|
|
4597
|
+
.vp-play:hover,
|
|
4598
|
+
.vp-play:active {
|
|
4599
|
+
transform: none;
|
|
4600
|
+
}
|
|
4232
4601
|
}
|
|
4233
4602
|
|
|
4234
4603
|
/* ── Night (mos-5lzns.33) ─────────────────────────────────────────────
|