@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.
@@ -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;
@@ -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
+ }
@@ -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[];
@@ -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' },
@@ -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 {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@joewinke/jatui",
3
- "version": "0.31.2",
3
+ "version": "0.33.0",
4
4
  "private": false,
5
5
  "description": "Shared Svelte 5 component library for JAT projects",
6
6
  "type": "module",