@joewinke/jatui 0.31.1 → 0.32.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/VideoPlayer.svelte +390 -2
- package/dist/components/VideoPlayer.svelte.d.ts +81 -0
- package/dist/sound/SoundSettings.svelte +1 -1
- package/dist/sound/player.d.ts +8 -6
- package/dist/sound/unlock.js +1 -1
- package/dist/video/README.md +2 -1
- package/dist/video/chapterRail.d.ts +49 -0
- package/dist/video/chapterRail.js +79 -0
- package/dist/video/keys.d.ts +1 -1
- package/dist/video/keys.js +3 -0
- package/dist/video/types.d.ts +10 -0
- package/package.json +1 -1
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
VideoWaveform,
|
|
10
10
|
VideoSpeakerTalkTime
|
|
11
11
|
} from '../video/types';
|
|
12
|
+
// mos-777hj — chapter rail public types.
|
|
13
|
+
export type { RailChapter, RailChapterStatus } from '../video/chapterRail';
|
|
12
14
|
// mos-5lzns.30 — one-axis state, the light track, social markers.
|
|
13
15
|
export type { BandAxis, BandState } from '../video/band/axis';
|
|
14
16
|
export type { VideoLumaTrack } from '../video/band/luma';
|
|
@@ -79,8 +81,11 @@
|
|
|
79
81
|
* colour, cycled per speaker — not brand colour).
|
|
80
82
|
*/
|
|
81
83
|
|
|
82
|
-
import { onMount, untrack } from 'svelte';
|
|
84
|
+
import { onMount, untrack, type Snippet } from 'svelte';
|
|
83
85
|
import { getDaisyUIColor, watchThemeChanges, clearColorCache } from '../utils/daisyuiColors';
|
|
86
|
+
import { listNav } from '../actions/listNav';
|
|
87
|
+
// mos-777hj — the chapter rail's pure state (ends, played checks, progress).
|
|
88
|
+
import { railChapters, activeRailIndex, type RailChapter } from '../video/chapterRail';
|
|
84
89
|
import type {
|
|
85
90
|
VideoBookmark,
|
|
86
91
|
VideoChapter,
|
|
@@ -255,6 +260,9 @@
|
|
|
255
260
|
captionsLang = 'en',
|
|
256
261
|
captionsDefault = true,
|
|
257
262
|
chapters = [],
|
|
263
|
+
chapterRail: chapterRailProp = 'off',
|
|
264
|
+
onChapterSelect = undefined,
|
|
265
|
+
belowStage = undefined,
|
|
258
266
|
speakers = [],
|
|
259
267
|
transcript = [],
|
|
260
268
|
bookmarks = [],
|
|
@@ -369,6 +377,33 @@
|
|
|
369
377
|
*/
|
|
370
378
|
captionsDefault?: boolean;
|
|
371
379
|
chapters?: VideoChapter[];
|
|
380
|
+
/**
|
|
381
|
+
* mos-777hj — the live chapter rail. `'side'` (with chapters present)
|
|
382
|
+
* renders a sticky chapter pane beside the main column at ≥64rem — one
|
|
383
|
+
* row per chapter with time, title, status dot and summary, the active
|
|
384
|
+
* row tracking the playhead (aria-current, auto-scroll), a progress
|
|
385
|
+
* fill inside the active row and a played check behind the playhead —
|
|
386
|
+
* and a horizontal chip strip under the player below 64rem. Rows seek
|
|
387
|
+
* to the chapter and play; the active row carries the replay affordance
|
|
388
|
+
* (clicking it again replays from its start). Keyboard: listNav j/k to
|
|
389
|
+
* move, Enter to play, while the rail holds focus. Default `'off'` —
|
|
390
|
+
* the rail is opt-in and leaves the player exactly as it was.
|
|
391
|
+
*/
|
|
392
|
+
chapterRail?: 'side' | 'off';
|
|
393
|
+
/**
|
|
394
|
+
* Fires when the viewer picks a chapter (row click / Enter / replay):
|
|
395
|
+
* the host pins its own view (e.g. a per-chapter visual panel) — the
|
|
396
|
+
* rail itself always keeps time-tracking the playhead.
|
|
397
|
+
*/
|
|
398
|
+
onChapterSelect?: (chapter: VideoChapter, index: number) => void;
|
|
399
|
+
/**
|
|
400
|
+
* Optional snippet rendered between the stage and the transcript — the
|
|
401
|
+
* host's own content in the player's main column (e.g. a "now
|
|
402
|
+
* discussing" panel bound to the active chapter). Designed for
|
|
403
|
+
* `transcriptPosition: 'below'`; beside a side transcript it renders
|
|
404
|
+
* under the stage in the stage's column.
|
|
405
|
+
*/
|
|
406
|
+
belowStage?: Snippet;
|
|
372
407
|
speakers?: VideoSpeakerTurn[];
|
|
373
408
|
transcript?: VideoTranscriptSegment[];
|
|
374
409
|
bookmarks?: VideoBookmark[];
|
|
@@ -960,6 +995,42 @@
|
|
|
960
995
|
const useHours = $derived(effectiveDuration >= 3600);
|
|
961
996
|
|
|
962
997
|
const sortedChapters = $derived(sortChapters(chapters));
|
|
998
|
+
|
|
999
|
+
// ── Chapter rail (mos-777hj) ─────────────────────────────────────────
|
|
1000
|
+
/** The rail pane exists only when opted in AND there is something to rail. */
|
|
1001
|
+
const railOn = $derived(chapterRailProp === 'side' && sortedChapters.length > 0);
|
|
1002
|
+
/** Rows recomputed per playhead tick — ends, played checks, progress fill. */
|
|
1003
|
+
const railRows = $derived(railOn ? railChapters(sortedChapters, effectiveDuration, currentTime) : []);
|
|
1004
|
+
const railActive = $derived(railOn ? activeRailIndex(sortedChapters, currentTime) : -1);
|
|
1005
|
+
let chapterRailEl: HTMLElement | null = $state(null);
|
|
1006
|
+
|
|
1007
|
+
/** Row click / Enter / replay: seek to the chapter and play it. */
|
|
1008
|
+
function chapterGo(index: number) {
|
|
1009
|
+
const ch = sortedChapters[index];
|
|
1010
|
+
if (!ch) return;
|
|
1011
|
+
seekTo(ch.startSec);
|
|
1012
|
+
videoEl?.play()?.catch(() => {});
|
|
1013
|
+
onChapterSelect?.(ch, index);
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
/** listNav onSelect — the element carries its row index. */
|
|
1017
|
+
function onRailSelect(el: HTMLElement) {
|
|
1018
|
+
const idx = Number(el.dataset.chapterIndex);
|
|
1019
|
+
if (Number.isInteger(idx)) chapterGo(idx);
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
/** Keep the active row visible while playback tracks chapters. Nearest
|
|
1023
|
+
* only ever scrolls the rail's own overflow (never the page); smooth is
|
|
1024
|
+
* dropped under prefers-reduced-motion. */
|
|
1025
|
+
$effect(() => {
|
|
1026
|
+
if (!railOn || railActive < 0) return;
|
|
1027
|
+
const row = chapterRailEl?.querySelector<HTMLElement>('.vp-chapter-row.active');
|
|
1028
|
+
if (!row) return;
|
|
1029
|
+
row.scrollIntoView({
|
|
1030
|
+
block: 'nearest',
|
|
1031
|
+
behavior: reduceMotion ? 'auto' : 'smooth'
|
|
1032
|
+
});
|
|
1033
|
+
});
|
|
963
1034
|
const sortedTurns = $derived(sortTurns(speakers));
|
|
964
1035
|
const sortedSegments = $derived(sortSegments(transcript));
|
|
965
1036
|
const sortedBookmarks = $derived(sortBookmarks(bookmarks));
|
|
@@ -1094,6 +1165,16 @@
|
|
|
1094
1165
|
return { ...currentBandState };
|
|
1095
1166
|
}
|
|
1096
1167
|
|
|
1168
|
+
/**
|
|
1169
|
+
* Callable on the instance (`bind:this`) — seek to an absolute second.
|
|
1170
|
+
* The host-side half of deep links (mos-777hj): a host that resolves its
|
|
1171
|
+
* own anchors (e.g. #chapter-<id>) seeks through this after mount, the
|
|
1172
|
+
* same place `startAt`/`?t=` seed from.
|
|
1173
|
+
*/
|
|
1174
|
+
export function seekToTime(sec: number) {
|
|
1175
|
+
seekTo(sec);
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1097
1178
|
const reportBand = createBandReporter(() => onBandChange);
|
|
1098
1179
|
$effect(() => {
|
|
1099
1180
|
const st = currentBandState;
|
|
@@ -1105,6 +1186,7 @@
|
|
|
1105
1186
|
PLAYER_KEYS.filter((row) => {
|
|
1106
1187
|
if (!row.when) return true;
|
|
1107
1188
|
if (row.when === 'chapters') return sortedChapters.length > 0;
|
|
1189
|
+
if (row.when === 'rail') return railOn;
|
|
1108
1190
|
if (row.when === 'bookmarks') return onAddBookmark != null;
|
|
1109
1191
|
if (row.when === 'transcript') return sortedSegments.length > 0;
|
|
1110
1192
|
if (row.when === 'comments') return socialMarkers.comments.length > 0;
|
|
@@ -2532,6 +2614,10 @@
|
|
|
2532
2614
|
const target = e.target as HTMLElement | null;
|
|
2533
2615
|
const tag = target?.tagName;
|
|
2534
2616
|
if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT' || target?.isContentEditable) return;
|
|
2617
|
+
// mos-777hj — the chapter rail owns j/k (listNav) and Enter/Space (row
|
|
2618
|
+
// activation) while focus is inside it; without this guard a rail `k`
|
|
2619
|
+
// would also toggle play/pause and a rail `j` also seek ±5s.
|
|
2620
|
+
if (chapterRailEl && target && chapterRailEl.contains(target)) return;
|
|
2535
2621
|
if (e.metaKey || e.ctrlKey || e.altKey) return;
|
|
2536
2622
|
const v = videoEl;
|
|
2537
2623
|
if (!v) return;
|
|
@@ -2786,6 +2872,7 @@
|
|
|
2786
2872
|
class="vp-root {className}"
|
|
2787
2873
|
bind:this={rootEl}
|
|
2788
2874
|
class:vp-side={transcriptPosition === 'side' && sortedSegments.length > 0 && transcriptVisible}
|
|
2875
|
+
class:vp-rail-side={railOn}
|
|
2789
2876
|
class:vp-one-axis={nativeBarHidden}
|
|
2790
2877
|
class:vp-night-capable={nightPresent}
|
|
2791
2878
|
class:vp-night-surround={surroundMix > 0.001}
|
|
@@ -3144,6 +3231,79 @@
|
|
|
3144
3231
|
{/if}
|
|
3145
3232
|
</div>
|
|
3146
3233
|
|
|
3234
|
+
{#if railOn}
|
|
3235
|
+
<!-- mos-777hj — the live chapter rail: one row per chapter with time,
|
|
3236
|
+
title, status dot and summary. The active row tracks the playhead
|
|
3237
|
+
(aria-current + auto-scroll), fills its thin progress bar, and
|
|
3238
|
+
carries the replay affordance; played chapters wear a check. A row
|
|
3239
|
+
click seeks there and plays (clicking the active row again replays
|
|
3240
|
+
it from its start). listNav owns j/k + Enter while the rail holds
|
|
3241
|
+
focus (global:false scopes the listener to this pane; the root
|
|
3242
|
+
keydown handler skips events from inside it). ≥64rem the pane is a
|
|
3243
|
+
sticky column; below it collapses to a horizontal chip strip. -->
|
|
3244
|
+
<nav
|
|
3245
|
+
class="vp-chapters"
|
|
3246
|
+
bind:this={chapterRailEl}
|
|
3247
|
+
aria-label="Chapters"
|
|
3248
|
+
use:listNav={{
|
|
3249
|
+
itemSelector: '.vp-chapter-row',
|
|
3250
|
+
onSelect: onRailSelect,
|
|
3251
|
+
focusedClass: 'jk-focused',
|
|
3252
|
+
global: false
|
|
3253
|
+
}}
|
|
3254
|
+
>
|
|
3255
|
+
{#each railRows as ch, i (i)}
|
|
3256
|
+
<button
|
|
3257
|
+
type="button"
|
|
3258
|
+
class="vp-chapter-row"
|
|
3259
|
+
class:active={railActive === i}
|
|
3260
|
+
class:done={ch.played}
|
|
3261
|
+
data-chapter-index={i}
|
|
3262
|
+
aria-current={railActive === i ? 'true' : undefined}
|
|
3263
|
+
onclick={() => chapterGo(i)}
|
|
3264
|
+
>
|
|
3265
|
+
<span class="vp-chapter-head">
|
|
3266
|
+
{#if ch.status}
|
|
3267
|
+
<span class="vp-chapter-dot vp-chapter-dot-{ch.status}" aria-hidden="true"></span>
|
|
3268
|
+
{/if}
|
|
3269
|
+
<span class="vp-chapter-time">{formatTime(ch.startSec, useHours)}</span>
|
|
3270
|
+
<span class="vp-chapter-title">{ch.title}</span>
|
|
3271
|
+
{#if ch.played}
|
|
3272
|
+
<svg class="vp-chapter-check" viewBox="0 0 16 16" width="11" height="11" aria-hidden="true">
|
|
3273
|
+
<path d="M2.5 8.5 6 12l7.5-8" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" />
|
|
3274
|
+
</svg>
|
|
3275
|
+
{/if}
|
|
3276
|
+
{#if railActive === i}
|
|
3277
|
+
<!-- Replay affordance: the active row's click already replays
|
|
3278
|
+
from its start, so the icon is the affordance's label. -->
|
|
3279
|
+
<svg class="vp-chapter-replay" viewBox="0 0 24 24" width="12" height="12" aria-hidden="true">
|
|
3280
|
+
<path d="M3 12a9 9 0 1 0 2.64-6.36M3 4v4h4" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" />
|
|
3281
|
+
</svg>
|
|
3282
|
+
{/if}
|
|
3283
|
+
</span>
|
|
3284
|
+
{#if ch.summary}
|
|
3285
|
+
<span class="vp-chapter-summary">{ch.summary}</span>
|
|
3286
|
+
{/if}
|
|
3287
|
+
{#if railActive === i}
|
|
3288
|
+
<span
|
|
3289
|
+
class="vp-chapter-bar"
|
|
3290
|
+
aria-hidden="true"
|
|
3291
|
+
style="--vp-chapter-progress: {ch.progress}"
|
|
3292
|
+
>
|
|
3293
|
+
<span class="vp-chapter-fill"></span>
|
|
3294
|
+
</span>
|
|
3295
|
+
{/if}
|
|
3296
|
+
</button>
|
|
3297
|
+
{/each}
|
|
3298
|
+
</nav>
|
|
3299
|
+
{/if}
|
|
3300
|
+
|
|
3301
|
+
{#if belowStage}
|
|
3302
|
+
<!-- mos-777hj — the host's content in the main column: under the stage
|
|
3303
|
+
(and its band), above the transcript. -->
|
|
3304
|
+
<div class="vp-below-stage">{@render belowStage()}</div>
|
|
3305
|
+
{/if}
|
|
3306
|
+
|
|
3147
3307
|
{#if sortedSegments.length > 0 && transcriptVisible}
|
|
3148
3308
|
<div
|
|
3149
3309
|
class="vp-transcript"
|
|
@@ -3276,6 +3436,232 @@
|
|
|
3276
3436
|
}
|
|
3277
3437
|
}
|
|
3278
3438
|
|
|
3439
|
+
/* ── Chapter rail (mos-777hj) ────────────────────────────────────────
|
|
3440
|
+
Opt-in (`chapterRail="side"` + chapters). ≥64rem the pane is a sticky
|
|
3441
|
+
column on the right of the main content (below the stage when the
|
|
3442
|
+
transcript sits below, a third column beside a side transcript);
|
|
3443
|
+
below 64rem the same DOM flows as a horizontal chip strip between the
|
|
3444
|
+
player and the below-stage content — grid areas re-place the panes,
|
|
3445
|
+
the DOM order never changes. Token-only chrome, dot tones resolve
|
|
3446
|
+
DaisyUI semantic vars. */
|
|
3447
|
+
.vp-below-stage {
|
|
3448
|
+
min-width: 0;
|
|
3449
|
+
}
|
|
3450
|
+
|
|
3451
|
+
.vp-chapters {
|
|
3452
|
+
display: flex;
|
|
3453
|
+
flex-direction: column;
|
|
3454
|
+
gap: 1px;
|
|
3455
|
+
min-width: 0;
|
|
3456
|
+
background: var(--color-base-100);
|
|
3457
|
+
border: var(--jatui-border-w, 1px) solid
|
|
3458
|
+
color-mix(in oklch, var(--color-base-content) 12%, transparent);
|
|
3459
|
+
border-radius: var(--jatui-radius-box, 0.5rem);
|
|
3460
|
+
padding: var(--jatui-space-sm, 4px);
|
|
3461
|
+
overflow-y: auto;
|
|
3462
|
+
}
|
|
3463
|
+
|
|
3464
|
+
.vp-chapter-row {
|
|
3465
|
+
position: relative;
|
|
3466
|
+
display: flex;
|
|
3467
|
+
flex-direction: column;
|
|
3468
|
+
gap: 2px;
|
|
3469
|
+
background: none;
|
|
3470
|
+
border: none;
|
|
3471
|
+
border-radius: var(--jatui-radius-field, 0.25rem);
|
|
3472
|
+
padding: var(--jatui-space-sm, 4px) var(--jatui-space-md, 6px);
|
|
3473
|
+
text-align: left;
|
|
3474
|
+
cursor: pointer;
|
|
3475
|
+
transition: background var(--motion-fast, 150ms) var(--motion-glide, ease-out);
|
|
3476
|
+
}
|
|
3477
|
+
|
|
3478
|
+
.vp-chapter-row:hover {
|
|
3479
|
+
background: color-mix(in oklch, var(--color-base-content) 8%, transparent);
|
|
3480
|
+
}
|
|
3481
|
+
|
|
3482
|
+
.vp-chapter-row.active {
|
|
3483
|
+
background: color-mix(in oklch, var(--color-primary) 14%, transparent);
|
|
3484
|
+
box-shadow: inset 2px 0 0 var(--color-primary);
|
|
3485
|
+
}
|
|
3486
|
+
|
|
3487
|
+
/* listNav's cursor (j/k) — same family as the hover/active surfaces. */
|
|
3488
|
+
.vp-chapter-row.jk-focused {
|
|
3489
|
+
outline: 2px solid color-mix(in oklch, var(--color-primary) 55%, transparent);
|
|
3490
|
+
outline-offset: -2px;
|
|
3491
|
+
}
|
|
3492
|
+
|
|
3493
|
+
.vp-chapter-row.done .vp-chapter-title {
|
|
3494
|
+
color: color-mix(in oklch, var(--color-base-content) 55%, transparent);
|
|
3495
|
+
}
|
|
3496
|
+
|
|
3497
|
+
.vp-chapter-head {
|
|
3498
|
+
display: flex;
|
|
3499
|
+
align-items: baseline;
|
|
3500
|
+
gap: var(--jatui-space-sm, 4px);
|
|
3501
|
+
min-width: 0;
|
|
3502
|
+
}
|
|
3503
|
+
|
|
3504
|
+
.vp-chapter-dot {
|
|
3505
|
+
width: 0.5em;
|
|
3506
|
+
height: 0.5em;
|
|
3507
|
+
border-radius: var(--jatui-radius-selector, 0.375rem);
|
|
3508
|
+
flex: none;
|
|
3509
|
+
align-self: center;
|
|
3510
|
+
background: var(--color-base-content);
|
|
3511
|
+
opacity: 0.4;
|
|
3512
|
+
}
|
|
3513
|
+
|
|
3514
|
+
.vp-chapter-dot-info {
|
|
3515
|
+
background: var(--color-info);
|
|
3516
|
+
opacity: 1;
|
|
3517
|
+
}
|
|
3518
|
+
|
|
3519
|
+
.vp-chapter-dot-success {
|
|
3520
|
+
background: var(--color-success);
|
|
3521
|
+
opacity: 1;
|
|
3522
|
+
}
|
|
3523
|
+
|
|
3524
|
+
.vp-chapter-dot-warning {
|
|
3525
|
+
background: var(--color-warning);
|
|
3526
|
+
opacity: 1;
|
|
3527
|
+
}
|
|
3528
|
+
|
|
3529
|
+
.vp-chapter-dot-error {
|
|
3530
|
+
background: var(--color-error);
|
|
3531
|
+
opacity: 1;
|
|
3532
|
+
}
|
|
3533
|
+
|
|
3534
|
+
.vp-chapter-time {
|
|
3535
|
+
font-family: ui-monospace, monospace;
|
|
3536
|
+
font-size: 0.66rem;
|
|
3537
|
+
color: color-mix(in oklch, var(--color-base-content) 50%, transparent);
|
|
3538
|
+
flex-shrink: 0;
|
|
3539
|
+
}
|
|
3540
|
+
|
|
3541
|
+
.vp-chapter-title {
|
|
3542
|
+
font-size: 0.76rem;
|
|
3543
|
+
font-weight: 600;
|
|
3544
|
+
color: var(--color-base-content);
|
|
3545
|
+
flex: 1;
|
|
3546
|
+
min-width: 0;
|
|
3547
|
+
overflow: hidden;
|
|
3548
|
+
text-overflow: ellipsis;
|
|
3549
|
+
white-space: nowrap;
|
|
3550
|
+
}
|
|
3551
|
+
|
|
3552
|
+
.vp-chapter-check {
|
|
3553
|
+
color: var(--color-success);
|
|
3554
|
+
flex: none;
|
|
3555
|
+
align-self: center;
|
|
3556
|
+
}
|
|
3557
|
+
|
|
3558
|
+
.vp-chapter-replay {
|
|
3559
|
+
color: color-mix(in oklch, var(--color-primary) 80%, transparent);
|
|
3560
|
+
flex: none;
|
|
3561
|
+
align-self: center;
|
|
3562
|
+
}
|
|
3563
|
+
|
|
3564
|
+
.vp-chapter-summary {
|
|
3565
|
+
font-size: 0.68rem;
|
|
3566
|
+
line-height: 1.35;
|
|
3567
|
+
color: color-mix(in oklch, var(--color-base-content) 60%, transparent);
|
|
3568
|
+
overflow: hidden;
|
|
3569
|
+
display: -webkit-box;
|
|
3570
|
+
-webkit-box-orient: vertical;
|
|
3571
|
+
-webkit-line-clamp: 2;
|
|
3572
|
+
}
|
|
3573
|
+
|
|
3574
|
+
/* The active row's thin fill: transform, not width — a compositor move
|
|
3575
|
+
that never relayouts the row, stepping with the playhead tick. */
|
|
3576
|
+
.vp-chapter-bar {
|
|
3577
|
+
position: absolute;
|
|
3578
|
+
left: var(--jatui-space-md, 6px);
|
|
3579
|
+
right: var(--jatui-space-md, 6px);
|
|
3580
|
+
bottom: 1px;
|
|
3581
|
+
height: 2px;
|
|
3582
|
+
border-radius: 999px;
|
|
3583
|
+
background: color-mix(in oklch, var(--color-base-content) 10%, transparent);
|
|
3584
|
+
overflow: hidden;
|
|
3585
|
+
pointer-events: none;
|
|
3586
|
+
}
|
|
3587
|
+
|
|
3588
|
+
.vp-chapter-fill {
|
|
3589
|
+
display: block;
|
|
3590
|
+
width: 100%;
|
|
3591
|
+
height: 100%;
|
|
3592
|
+
border-radius: inherit;
|
|
3593
|
+
background: var(--color-primary);
|
|
3594
|
+
transform: scaleX(var(--vp-chapter-progress, 0));
|
|
3595
|
+
transform-origin: left center;
|
|
3596
|
+
transition: transform 120ms linear;
|
|
3597
|
+
}
|
|
3598
|
+
|
|
3599
|
+
@media (max-width: 63.999rem) {
|
|
3600
|
+
/* The chip strip: same rows, laid horizontally under the player. */
|
|
3601
|
+
.vp-chapters {
|
|
3602
|
+
flex-direction: row;
|
|
3603
|
+
gap: var(--jatui-space-xs, 2px);
|
|
3604
|
+
overflow-x: auto;
|
|
3605
|
+
overflow-y: hidden;
|
|
3606
|
+
padding: var(--jatui-space-xs, 2px);
|
|
3607
|
+
}
|
|
3608
|
+
|
|
3609
|
+
.vp-chapter-row {
|
|
3610
|
+
flex: none;
|
|
3611
|
+
max-width: 16rem;
|
|
3612
|
+
padding: var(--jatui-space-xs, 2px) var(--jatui-space-sm, 4px);
|
|
3613
|
+
}
|
|
3614
|
+
|
|
3615
|
+
.vp-chapter-summary,
|
|
3616
|
+
.vp-chapter-bar {
|
|
3617
|
+
display: none;
|
|
3618
|
+
}
|
|
3619
|
+
|
|
3620
|
+
.vp-chapter-title {
|
|
3621
|
+
max-width: 12rem;
|
|
3622
|
+
}
|
|
3623
|
+
}
|
|
3624
|
+
|
|
3625
|
+
@media (min-width: 64rem) {
|
|
3626
|
+
.vp-root.vp-rail-side {
|
|
3627
|
+
display: grid;
|
|
3628
|
+
grid-template-columns: minmax(0, 1fr) minmax(13rem, 16rem);
|
|
3629
|
+
grid-template-areas:
|
|
3630
|
+
'main rail'
|
|
3631
|
+
'below rail'
|
|
3632
|
+
'transcript rail';
|
|
3633
|
+
gap: var(--jatui-space-lg, 12px);
|
|
3634
|
+
align-items: start;
|
|
3635
|
+
}
|
|
3636
|
+
|
|
3637
|
+
.vp-root.vp-rail-side > .vp-main {
|
|
3638
|
+
grid-area: main;
|
|
3639
|
+
}
|
|
3640
|
+
|
|
3641
|
+
.vp-root.vp-rail-side > .vp-chapters {
|
|
3642
|
+
grid-area: rail;
|
|
3643
|
+
position: sticky;
|
|
3644
|
+
top: var(--jatui-space-lg, 12px);
|
|
3645
|
+
max-height: calc(100vh - 2 * var(--jatui-space-lg, 12px));
|
|
3646
|
+
}
|
|
3647
|
+
|
|
3648
|
+
.vp-root.vp-rail-side > .vp-below-stage {
|
|
3649
|
+
grid-area: below;
|
|
3650
|
+
}
|
|
3651
|
+
|
|
3652
|
+
.vp-root.vp-rail-side > .vp-transcript {
|
|
3653
|
+
grid-area: transcript;
|
|
3654
|
+
}
|
|
3655
|
+
|
|
3656
|
+
/* Rail beside a side transcript (theater): three columns. */
|
|
3657
|
+
.vp-root.vp-side.vp-rail-side {
|
|
3658
|
+
grid-template-columns: minmax(0, 3fr) minmax(16rem, 2fr) minmax(11rem, 14rem);
|
|
3659
|
+
grid-template-areas:
|
|
3660
|
+
'main transcript rail'
|
|
3661
|
+
'below transcript rail';
|
|
3662
|
+
}
|
|
3663
|
+
}
|
|
3664
|
+
|
|
3279
3665
|
.vp-stage {
|
|
3280
3666
|
position: relative;
|
|
3281
3667
|
}
|
|
@@ -3838,7 +4224,9 @@
|
|
|
3838
4224
|
.vp-lane-span,
|
|
3839
4225
|
.vp-chip,
|
|
3840
4226
|
.vp-add-btn,
|
|
3841
|
-
.vp-row
|
|
4227
|
+
.vp-row,
|
|
4228
|
+
.vp-chapter-row,
|
|
4229
|
+
.vp-chapter-fill {
|
|
3842
4230
|
transition: none;
|
|
3843
4231
|
}
|
|
3844
4232
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export type { VideoChapter, VideoSpeakerTurn, VideoTranscriptSegment, VideoBookmark, VideoWaveform, VideoSpeakerTalkTime } from '../video/types';
|
|
2
|
+
export type { RailChapter, RailChapterStatus } from '../video/chapterRail';
|
|
2
3
|
export type { BandAxis, BandState } from '../video/band/axis';
|
|
3
4
|
export type { VideoLumaTrack } from '../video/band/luma';
|
|
4
5
|
export type { VideoMarker } from '../video/band/markers';
|
|
@@ -8,6 +9,58 @@ export type { VideoAdSpan, RateReport } from '../video/rate';
|
|
|
8
9
|
export type { WindDownState, WindDownPhase, PresenceSource, VideoResumeStore, FlipSource } from '../video/presence';
|
|
9
10
|
export type { ResumePoint } from '../video/advisors/resume-point';
|
|
10
11
|
export type { VideoSettingsStore, SettingValue, SettingSpec } from '../video/settings';
|
|
12
|
+
/**
|
|
13
|
+
* VideoPlayer — one video surface for everything, from a bare mp4 to a
|
|
14
|
+
* fully-analysed recording.
|
|
15
|
+
*
|
|
16
|
+
* Marries two prior implementations rather than inventing a third:
|
|
17
|
+
*
|
|
18
|
+
* 1. extension-library's framework-agnostic `mountPlayer` (player.js) —
|
|
19
|
+
* the timeline band: waveform envelope as the scrubber, chapters as
|
|
20
|
+
* structure inside it, diarized speaker colouring, bookmark diamonds,
|
|
21
|
+
* scroll-to-zoom, hover card with "what's being said here".
|
|
22
|
+
* 2. jatui's TranscriptPlayer — the transcript pane: click a line to seek,
|
|
23
|
+
* active line auto-scroll, solo-speaker playback, talk-time bars.
|
|
24
|
+
*
|
|
25
|
+
* EVERY intelligence prop is optional. A bare `<VideoPlayer {src} />` still
|
|
26
|
+
* gets the band (a real scrubber with hover-time, click-to-seek, keyboard
|
|
27
|
+
* seek), wheel volume and the keyboard map; chapters, speakers, waveform,
|
|
28
|
+
* transcript, bookmarks and talk-time each light up their own pane as the
|
|
29
|
+
* data exists. Nothing shifts layout when data arrives late (diarization is
|
|
30
|
+
* slow) — the band's slots are reserved from the first frame.
|
|
31
|
+
*
|
|
32
|
+
* ── Design notes carried over from player.js ────────────────────────────
|
|
33
|
+
* · ONE time axis. Every position — waveform bars, chapter flags, bookmarks,
|
|
34
|
+
* the speaker lane, click/hover-to-seek — maps through the band window's
|
|
35
|
+
* secToFrac/fracToSec (video/band.ts), so scroll-to-zoom is one piece of
|
|
36
|
+
* state everything else respects for free.
|
|
37
|
+
* · ONE speaker surface. With a waveform, the envelope's own bars wear the
|
|
38
|
+
* turn colours; the slim lane under it paints only as the no-waveform
|
|
39
|
+
* fallback. Never two stacked time-parallel strips.
|
|
40
|
+
* · Chapter flags sit on the TOP edge, bookmark diamonds on the BOTTOM edge —
|
|
41
|
+
* same idea (a marker on the timeline), different origin (the model's vs.
|
|
42
|
+
* yours), so the two families are never confused at a glance.
|
|
43
|
+
* · The wheel means different things in different regions, and each region is
|
|
44
|
+
* its own hover target so nothing is ambiguous: over the video it's volume
|
|
45
|
+
* (shift = seek, alt = speed); over the band it's zoom.
|
|
46
|
+
* · The keyboard map (video/keys.ts) is the single source the keydown
|
|
47
|
+
* handler and the `?` overlay both read — a key that exists is documented,
|
|
48
|
+
* a documented key exists.
|
|
49
|
+
*
|
|
50
|
+
* Internals live in `src/lib/video/` (band / keys / rate / deepLink /
|
|
51
|
+
* settings + types/time/model; audio, night and presence are contracts for
|
|
52
|
+
* mos-5lzns.31/.33/.34 — see that directory's README for the ownership map
|
|
53
|
+
* and the advisor doctrine).
|
|
54
|
+
*
|
|
55
|
+
* Deliberately NOT ported: player.js's trim region (in/out handles). That's
|
|
56
|
+
* clip-editing, not playback, and belongs in an editor component.
|
|
57
|
+
*
|
|
58
|
+
* Theming is token-only — DaisyUI semantic vars for chrome, resolved at
|
|
59
|
+
* runtime via `getDaisyUIColor` for the canvas, repainted on theme change.
|
|
60
|
+
* Speaker hues follow TranscriptPlayer's oklch palette (data-driven identity
|
|
61
|
+
* colour, cycled per speaker — not brand colour).
|
|
62
|
+
*/
|
|
63
|
+
import { type Snippet } from 'svelte';
|
|
11
64
|
import type { VideoBookmark, VideoChapter, VideoSpeakerTalkTime, VideoSpeakerTurn, VideoTranscriptSegment, VideoWaveform } from '../video/types';
|
|
12
65
|
import { type BandState } from '../video/band/axis';
|
|
13
66
|
import { type VideoLumaTrack } from '../video/band/luma';
|
|
@@ -58,6 +111,33 @@ type $$ComponentProps = {
|
|
|
58
111
|
*/
|
|
59
112
|
captionsDefault?: boolean;
|
|
60
113
|
chapters?: VideoChapter[];
|
|
114
|
+
/**
|
|
115
|
+
* mos-777hj — the live chapter rail. `'side'` (with chapters present)
|
|
116
|
+
* renders a sticky chapter pane beside the main column at ≥64rem — one
|
|
117
|
+
* row per chapter with time, title, status dot and summary, the active
|
|
118
|
+
* row tracking the playhead (aria-current, auto-scroll), a progress
|
|
119
|
+
* fill inside the active row and a played check behind the playhead —
|
|
120
|
+
* and a horizontal chip strip under the player below 64rem. Rows seek
|
|
121
|
+
* to the chapter and play; the active row carries the replay affordance
|
|
122
|
+
* (clicking it again replays from its start). Keyboard: listNav j/k to
|
|
123
|
+
* move, Enter to play, while the rail holds focus. Default `'off'` —
|
|
124
|
+
* the rail is opt-in and leaves the player exactly as it was.
|
|
125
|
+
*/
|
|
126
|
+
chapterRail?: 'side' | 'off';
|
|
127
|
+
/**
|
|
128
|
+
* Fires when the viewer picks a chapter (row click / Enter / replay):
|
|
129
|
+
* the host pins its own view (e.g. a per-chapter visual panel) — the
|
|
130
|
+
* rail itself always keeps time-tracking the playhead.
|
|
131
|
+
*/
|
|
132
|
+
onChapterSelect?: (chapter: VideoChapter, index: number) => void;
|
|
133
|
+
/**
|
|
134
|
+
* Optional snippet rendered between the stage and the transcript — the
|
|
135
|
+
* host's own content in the player's main column (e.g. a "now
|
|
136
|
+
* discussing" panel bound to the active chapter). Designed for
|
|
137
|
+
* `transcriptPosition: 'below'`; beside a side transcript it renders
|
|
138
|
+
* under the stage in the stage's column.
|
|
139
|
+
*/
|
|
140
|
+
belowStage?: Snippet;
|
|
61
141
|
speakers?: VideoSpeakerTurn[];
|
|
62
142
|
transcript?: VideoTranscriptSegment[];
|
|
63
143
|
bookmarks?: VideoBookmark[];
|
|
@@ -321,6 +401,7 @@ type $$ComponentProps = {
|
|
|
321
401
|
};
|
|
322
402
|
declare const VideoPlayer: import("svelte").Component<$$ComponentProps, {
|
|
323
403
|
bandState: () => BandState;
|
|
404
|
+
seekToTime: (sec: number) => void;
|
|
324
405
|
}, "night" | "nightDepth">;
|
|
325
406
|
type VideoPlayer = ReturnType<typeof VideoPlayer>;
|
|
326
407
|
export default VideoPlayer;
|
|
@@ -71,7 +71,7 @@
|
|
|
71
71
|
const p = player ?? getSoundPlayer();
|
|
72
72
|
// A preview click is a real gesture: unlock here too, so a first-ever
|
|
73
73
|
// preview on iOS actually sounds instead of resuming a dead context.
|
|
74
|
-
void p.unlock();
|
|
74
|
+
void p.unlock?.();
|
|
75
75
|
previewing = p.play(name, { preview: true, loop: false });
|
|
76
76
|
previewTimer = setTimeout(stopPreview, PREVIEW_MAX_MS);
|
|
77
77
|
}
|
package/dist/sound/player.d.ts
CHANGED
|
@@ -53,15 +53,17 @@ export interface SoundPlayer {
|
|
|
53
53
|
/**
|
|
54
54
|
* Unlock audio output from inside a user-gesture handler (iOS/Safari
|
|
55
55
|
* keeps a context suspended unless the gesture created/resumed it —
|
|
56
|
-
* see `sound/unlock.ts`).
|
|
57
|
-
*
|
|
56
|
+
* see `sound/unlock.ts`). OPTIONAL: a custom player without one is
|
|
57
|
+
* assumed fine — unlock resolves and `unlocked` reads undefined, which
|
|
58
|
+
* hosts treat as "no unlock concept". A cheap no-op once unlocked.
|
|
58
59
|
*/
|
|
59
|
-
unlock(): Promise<void>;
|
|
60
|
+
unlock?(): Promise<void>;
|
|
60
61
|
/**
|
|
61
|
-
* True when the backend reports a running output context;
|
|
62
|
-
*
|
|
62
|
+
* True when the player's backend reports a running output context;
|
|
63
|
+
* false before any unlock, undefined when the player has no unlock
|
|
64
|
+
* concept.
|
|
63
65
|
*/
|
|
64
|
-
readonly unlocked
|
|
66
|
+
readonly unlocked?: boolean;
|
|
65
67
|
/** Decode clips ahead of time (defaults to every semantic name). */
|
|
66
68
|
preload(names?: readonly SoundId[]): Promise<void>;
|
|
67
69
|
stopAll(): void;
|
package/dist/sound/unlock.js
CHANGED
|
@@ -30,7 +30,7 @@ import { getSoundPlayer } from './player';
|
|
|
30
30
|
* is a cheap no-op once the context is running.
|
|
31
31
|
*/
|
|
32
32
|
export async function unlockSound() {
|
|
33
|
-
await getSoundPlayer().unlock();
|
|
33
|
+
await getSoundPlayer().unlock?.();
|
|
34
34
|
}
|
|
35
35
|
const GESTURES = ['pointerup', 'touchend', 'click', 'keydown'];
|
|
36
36
|
/** The target `armSoundUnlock` installed its one-shot listener on, if any. */
|
package/dist/video/README.md
CHANGED
|
@@ -11,7 +11,8 @@ props + callback surfaces.
|
|
|
11
11
|
|
|
12
12
|
| File | Owner | Contents |
|
|
13
13
|
|------|-------|----------|
|
|
14
|
-
| `types.ts` | .28 (done) | The public data contracts — `VideoChapter`, `VideoSpeakerTurn`, `VideoTranscriptSegment`, `VideoBookmark`, `VideoWaveform`, `VideoSpeakerTalkTime`. Re-exported by the component, so `import type … from 'jatui'` is unchanged. |
|
|
14
|
+
| `types.ts` | .28 (done) | The public data contracts — `VideoChapter`, `VideoSpeakerTurn`, `VideoTranscriptSegment`, `VideoBookmark`, `VideoWaveform`, `VideoSpeakerTalkTime`. Re-exported by the component, so `import type … from 'jatui'` is unchanged. .777hj adds `VideoChapter.endSec/summary/status` (all optional — the band ignores them). |
|
|
15
|
+
| `chapterRail.ts` | **mos-777hj** | The opt-in `chapterRail: 'side'` pane's pure state: `railChapters(chapters, duration, t)` derives row ends (explicit `endSec` → next start → duration), the played check and the 0–1 progress fill for the row containing `t`; `activeRailIndex` is the row-highlight index. The component owns the DOM: sticky right column ≥64rem (chip strip below), rows seek+play on click, replay affordance on the active row, listNav j/k+Enter (the root keydown handler skips events from inside the rail), `onChapterSelect` for host-side pinning, `belowStage` snippet slot between stage and transcript, `seekToTime()` instance export for host deep links. |
|
|
15
16
|
| `time.ts` | .28 (done) | Pure formatting: `formatTime`, `formatDuration`, `truncate`, `aspectRatioCss`. |
|
|
16
17
|
| `model.ts` | .28 (done) | Speaker registry + identity colours + talk-time derivation + the sort normalisations. `ALL_KEY`/`UNKNOWN_KEY` live here. |
|
|
17
18
|
| `band.ts` | .28 (done) — extended by **.30** | The timeline band: zoom window (`BandWindow`, `zoomAt`, `followPlayhead`), hit tests (`segmentAt`, `chapterAt`, `biasToSegmentStart`, `nearestBookmark`), and `paintBand` (canvas). .30 adds one-axis arbitration + the light ribbon; its state lives HERE. |
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Chapter rail state (mos-777hj) — the pure half of VideoPlayer's opt-in
|
|
3
|
+
* `chapterRail: 'side'` pane: one row per chapter (title, time, status dot,
|
|
4
|
+
* summary), the active row tracking the playhead, a progress fill inside the
|
|
5
|
+
* active row and a played check once the playhead moves past a chapter's end.
|
|
6
|
+
*
|
|
7
|
+
* The component owns the DOM (rows, click-to-seek-and-play, listNav, sticky
|
|
8
|
+
* column, small-screen chip strip); everything derivable from
|
|
9
|
+
* (chapters, duration, playhead) lives HERE so it is node-testable under
|
|
10
|
+
* `npm run test:unit` — same discipline as band.ts / transcriptWindow.ts.
|
|
11
|
+
*
|
|
12
|
+
* Chapter ends: a chapter without an explicit `endSec` runs to the NEXT
|
|
13
|
+
* chapter's start; the last one runs to the media duration when known. With
|
|
14
|
+
* no duration and no following chapter the chapter is open-ended — its row
|
|
15
|
+
* simply never shows a played check or progress (division guards to 0).
|
|
16
|
+
*/
|
|
17
|
+
import type { VideoChapter } from './types';
|
|
18
|
+
/** Dot tones, resolved to DaisyUI semantic vars by the component's CSS. */
|
|
19
|
+
export type RailChapterStatus = 'info' | 'success' | 'warning' | 'error';
|
|
20
|
+
/** A chapter as the rail renders it: end derived when absent. */
|
|
21
|
+
export interface RailChapter {
|
|
22
|
+
startSec: number;
|
|
23
|
+
endSec: number;
|
|
24
|
+
title: string;
|
|
25
|
+
summary: string | null;
|
|
26
|
+
status: RailChapterStatus | null;
|
|
27
|
+
/** True once the playhead has moved past this chapter's end. */
|
|
28
|
+
played: boolean;
|
|
29
|
+
/** 0..1 fill for the row containing `t`; exactly 0 for every other row. */
|
|
30
|
+
progress: number;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* The rail's rows for one playhead instant. `chapters` must be sorted by
|
|
34
|
+
* startSec (the component passes its `sortedChapters`); entries with a
|
|
35
|
+
* non-finite start are dropped, mirroring sortChapters' own filter.
|
|
36
|
+
*
|
|
37
|
+
* `t` before the first chapter (or exactly on it) marks that first chapter
|
|
38
|
+
* active with progress from its own start; there is no "before the program"
|
|
39
|
+
* row — the program starts at its first chapter.
|
|
40
|
+
*/
|
|
41
|
+
export declare function railChapters(chapters: VideoChapter[], durationSec: number | null, t: number): RailChapter[];
|
|
42
|
+
/**
|
|
43
|
+
* Index of the row containing `t` (the LAST chapter whose startSec <= t), or
|
|
44
|
+
* -1 when `t` sits before every chapter / the list is empty. The component
|
|
45
|
+
* uses this for the row highlight, aria-current and the auto-scroll.
|
|
46
|
+
*/
|
|
47
|
+
export declare function activeRailIndex(chapters: {
|
|
48
|
+
startSec: number;
|
|
49
|
+
}[], t: number): number;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Chapter rail state (mos-777hj) — the pure half of VideoPlayer's opt-in
|
|
3
|
+
* `chapterRail: 'side'` pane: one row per chapter (title, time, status dot,
|
|
4
|
+
* summary), the active row tracking the playhead, a progress fill inside the
|
|
5
|
+
* active row and a played check once the playhead moves past a chapter's end.
|
|
6
|
+
*
|
|
7
|
+
* The component owns the DOM (rows, click-to-seek-and-play, listNav, sticky
|
|
8
|
+
* column, small-screen chip strip); everything derivable from
|
|
9
|
+
* (chapters, duration, playhead) lives HERE so it is node-testable under
|
|
10
|
+
* `npm run test:unit` — same discipline as band.ts / transcriptWindow.ts.
|
|
11
|
+
*
|
|
12
|
+
* Chapter ends: a chapter without an explicit `endSec` runs to the NEXT
|
|
13
|
+
* chapter's start; the last one runs to the media duration when known. With
|
|
14
|
+
* no duration and no following chapter the chapter is open-ended — its row
|
|
15
|
+
* simply never shows a played check or progress (division guards to 0).
|
|
16
|
+
*/
|
|
17
|
+
const STATUS_VALUES = ['info', 'success', 'warning', 'error'];
|
|
18
|
+
function statusOf(value) {
|
|
19
|
+
return value != null && STATUS_VALUES.includes(value)
|
|
20
|
+
? value
|
|
21
|
+
: null;
|
|
22
|
+
}
|
|
23
|
+
function clamp01(n) {
|
|
24
|
+
if (!Number.isFinite(n))
|
|
25
|
+
return 0;
|
|
26
|
+
return n < 0 ? 0 : n > 1 ? 1 : n;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* The rail's rows for one playhead instant. `chapters` must be sorted by
|
|
30
|
+
* startSec (the component passes its `sortedChapters`); entries with a
|
|
31
|
+
* non-finite start are dropped, mirroring sortChapters' own filter.
|
|
32
|
+
*
|
|
33
|
+
* `t` before the first chapter (or exactly on it) marks that first chapter
|
|
34
|
+
* active with progress from its own start; there is no "before the program"
|
|
35
|
+
* row — the program starts at its first chapter.
|
|
36
|
+
*/
|
|
37
|
+
export function railChapters(chapters, durationSec, t) {
|
|
38
|
+
const rows = [];
|
|
39
|
+
for (let i = 0; i < chapters.length; i++) {
|
|
40
|
+
const c = chapters[i];
|
|
41
|
+
if (!c || !Number.isFinite(c.startSec) || c.startSec < 0)
|
|
42
|
+
continue;
|
|
43
|
+
const next = chapters[i + 1];
|
|
44
|
+
let end = c.endSec;
|
|
45
|
+
if (end == null || !Number.isFinite(end) || end <= c.startSec) {
|
|
46
|
+
end = next && Number.isFinite(next.startSec) ? next.startSec : durationSec;
|
|
47
|
+
}
|
|
48
|
+
if (end == null || !Number.isFinite(end) || end < c.startSec)
|
|
49
|
+
end = c.startSec;
|
|
50
|
+
const active = t >= c.startSec && (i === chapters.length - 1 || t < chapters[i + 1].startSec);
|
|
51
|
+
const span = end - c.startSec;
|
|
52
|
+
const progress = active && span > 0 ? clamp01((t - c.startSec) / span) : 0;
|
|
53
|
+
rows.push({
|
|
54
|
+
startSec: c.startSec,
|
|
55
|
+
endSec: end,
|
|
56
|
+
title: c.title,
|
|
57
|
+
summary: c.summary ?? null,
|
|
58
|
+
status: statusOf(c.status),
|
|
59
|
+
played: t >= end && end > c.startSec,
|
|
60
|
+
progress
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
return rows;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Index of the row containing `t` (the LAST chapter whose startSec <= t), or
|
|
67
|
+
* -1 when `t` sits before every chapter / the list is empty. The component
|
|
68
|
+
* uses this for the row highlight, aria-current and the auto-scroll.
|
|
69
|
+
*/
|
|
70
|
+
export function activeRailIndex(chapters, t) {
|
|
71
|
+
let idx = -1;
|
|
72
|
+
for (let i = 0; i < chapters.length; i++) {
|
|
73
|
+
if (chapters[i].startSec <= t)
|
|
74
|
+
idx = i;
|
|
75
|
+
else
|
|
76
|
+
break;
|
|
77
|
+
}
|
|
78
|
+
return idx;
|
|
79
|
+
}
|
package/dist/video/keys.d.ts
CHANGED
|
@@ -20,7 +20,7 @@ export interface PlayerKeyRow {
|
|
|
20
20
|
* Availability gate: the row is only advertised (and the key only bound)
|
|
21
21
|
* when the feature's data exists. Undefined ⇒ always active.
|
|
22
22
|
*/
|
|
23
|
-
when?: 'chapters' | 'bookmarks' | 'transcript' | 'comments' | 'night' | 'settings';
|
|
23
|
+
when?: 'chapters' | 'rail' | 'bookmarks' | 'transcript' | 'comments' | 'night' | 'settings';
|
|
24
24
|
}
|
|
25
25
|
/** Order follows the help overlay: transport, seek, volume, speed, structure. */
|
|
26
26
|
export declare const PLAYER_KEYS: PlayerKeyRow[];
|
package/dist/video/keys.js
CHANGED
|
@@ -23,6 +23,9 @@ export const PLAYER_KEYS = [
|
|
|
23
23
|
{ keys: 'Backspace', description: 'Reset speed to 1×' },
|
|
24
24
|
{ keys: '< / >', description: 'Frame step (pauses; ~1/30s per press)' },
|
|
25
25
|
{ keys: '[ / ]', description: 'Previous / next chapter', when: 'chapters' },
|
|
26
|
+
// mos-777hj — the rail's own listNav motions, active only while the rail
|
|
27
|
+
// holds focus (the same keys drive seek/play-pause everywhere else).
|
|
28
|
+
{ keys: 'j / k / Enter', description: 'Chapter rail: move / play chapter', when: 'rail' },
|
|
26
29
|
// Shift+[ / Shift+] — the chapter keys' shifted twins, so "structure"
|
|
27
30
|
// and "people's reactions to it" sit under one hand (mos-5lzns.30).
|
|
28
31
|
{ keys: '{ / }', description: 'Previous / next comment', when: 'comments' },
|
package/dist/video/types.d.ts
CHANGED
|
@@ -17,6 +17,16 @@ export interface VideoChapter {
|
|
|
17
17
|
title: string;
|
|
18
18
|
/** Optional captured still, shown in the band's hover card. */
|
|
19
19
|
thumbUrl?: string | null;
|
|
20
|
+
/**
|
|
21
|
+
* Chapter end, seconds. Absent (mos-777hj rail) the chapter runs to the
|
|
22
|
+
* NEXT chapter's start — or the media duration for the last one. The rail
|
|
23
|
+
* uses it for the played check and the progress fill; the band ignores it.
|
|
24
|
+
*/
|
|
25
|
+
endSec?: number | null;
|
|
26
|
+
/** One-line summary shown under the title in the chapter rail. */
|
|
27
|
+
summary?: string | null;
|
|
28
|
+
/** Rail status dot tone (DaisyUI semantic) — null renders no dot. */
|
|
29
|
+
status?: 'info' | 'success' | 'warning' | 'error' | null;
|
|
20
30
|
}
|
|
21
31
|
/** A diarized turn (or an ad read, `ad: true`). Structurally accepts meeting data. */
|
|
22
32
|
export interface VideoSpeakerTurn {
|