@joewinke/jatui 0.31.2 → 0.33.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 +1 -1
- package/dist/components/VideoPlayer.svelte +824 -67
- package/dist/components/VideoPlayer.svelte.d.ts +98 -0
- 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
|
@@ -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';
|
|
@@ -20,6 +73,23 @@ import type { ResumePoint } from '../video/advisors/resume-point';
|
|
|
20
73
|
import { type SettingValue, type VideoSettingsStore } from '../video/settings';
|
|
21
74
|
type $$ComponentProps = {
|
|
22
75
|
src: string;
|
|
76
|
+
/**
|
|
77
|
+
* What the media IS. `'audio'` (a podcast, a call recording, a brief)
|
|
78
|
+
* renders NO stage: there is no picture to frame, so the player is the
|
|
79
|
+
* band (the one timeline) plus its transport row. The media element
|
|
80
|
+
* still plays, just never shows. `aspect`/`poster` are ignored. Default
|
|
81
|
+
* `'video'`.
|
|
82
|
+
*/
|
|
83
|
+
kind?: 'video' | 'audio';
|
|
84
|
+
/**
|
|
85
|
+
* Who owns play/pause, time and volume. `'native'` keeps the browser's
|
|
86
|
+
* `<video controls>` bar (which carries its own timeline next to the
|
|
87
|
+
* band). `'band'` drops the native controls and puts a transport row
|
|
88
|
+
* on the band instead: play/pause, `current / total`, speed, mute +
|
|
89
|
+
* volume, and fullscreen for video. One timeline, one set of controls.
|
|
90
|
+
* Default: `'band'` for `kind="audio"`, `'native'` for video.
|
|
91
|
+
*/
|
|
92
|
+
transport?: 'native' | 'band';
|
|
23
93
|
poster?: string | null;
|
|
24
94
|
/**
|
|
25
95
|
* Aspect ratio for the video stage — presentation-primitive prop (PRD).
|
|
@@ -58,6 +128,33 @@ type $$ComponentProps = {
|
|
|
58
128
|
*/
|
|
59
129
|
captionsDefault?: boolean;
|
|
60
130
|
chapters?: VideoChapter[];
|
|
131
|
+
/**
|
|
132
|
+
* mos-777hj — the live chapter rail. `'side'` (with chapters present)
|
|
133
|
+
* renders a sticky chapter pane beside the main column at ≥64rem — one
|
|
134
|
+
* row per chapter with time, title, status dot and summary, the active
|
|
135
|
+
* row tracking the playhead (aria-current, auto-scroll), a progress
|
|
136
|
+
* fill inside the active row and a played check behind the playhead —
|
|
137
|
+
* and a horizontal chip strip under the player below 64rem. Rows seek
|
|
138
|
+
* to the chapter and play; the active row carries the replay affordance
|
|
139
|
+
* (clicking it again replays from its start). Keyboard: listNav j/k to
|
|
140
|
+
* move, Enter to play, while the rail holds focus. Default `'off'` —
|
|
141
|
+
* the rail is opt-in and leaves the player exactly as it was.
|
|
142
|
+
*/
|
|
143
|
+
chapterRail?: 'side' | 'off';
|
|
144
|
+
/**
|
|
145
|
+
* Fires when the viewer picks a chapter (row click / Enter / replay):
|
|
146
|
+
* the host pins its own view (e.g. a per-chapter visual panel) — the
|
|
147
|
+
* rail itself always keeps time-tracking the playhead.
|
|
148
|
+
*/
|
|
149
|
+
onChapterSelect?: (chapter: VideoChapter, index: number) => void;
|
|
150
|
+
/**
|
|
151
|
+
* Optional snippet rendered between the stage and the transcript — the
|
|
152
|
+
* host's own content in the player's main column (e.g. a "now
|
|
153
|
+
* discussing" panel bound to the active chapter). Designed for
|
|
154
|
+
* `transcriptPosition: 'below'`; beside a side transcript it renders
|
|
155
|
+
* under the stage in the stage's column.
|
|
156
|
+
*/
|
|
157
|
+
belowStage?: Snippet;
|
|
61
158
|
speakers?: VideoSpeakerTurn[];
|
|
62
159
|
transcript?: VideoTranscriptSegment[];
|
|
63
160
|
bookmarks?: VideoBookmark[];
|
|
@@ -321,6 +418,7 @@ type $$ComponentProps = {
|
|
|
321
418
|
};
|
|
322
419
|
declare const VideoPlayer: import("svelte").Component<$$ComponentProps, {
|
|
323
420
|
bandState: () => BandState;
|
|
421
|
+
seekToTime: (sec: number) => void;
|
|
324
422
|
}, "night" | "nightDepth">;
|
|
325
423
|
type VideoPlayer = ReturnType<typeof VideoPlayer>;
|
|
326
424
|
export default VideoPlayer;
|
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 {
|