@joewinke/jatui 0.49.0 → 0.50.1
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 +130 -1
- package/dist/components/VideoPlayer.svelte.d.ts +18 -0
- package/dist/video/README.md +3 -1
- package/dist/video/deepLink.d.ts +9 -0
- package/dist/video/deepLink.js +20 -0
- package/dist/video/keys.js +4 -0
- package/dist/video/lineMenu.d.ts +26 -0
- package/dist/video/lineMenu.js +28 -0
- package/package.json +1 -1
package/CLAUDE.md
CHANGED
|
@@ -177,7 +177,7 @@ Item-by-item review surfaces (MOS review-ux, design mos-z8vgx). One `VerdictOpti
|
|
|
177
177
|
| `ThumbnailGrid` | Generic thumbnail strip (chimaro port) — image tiles with text-tile fallback, +N overflow counter, remove/add affordances. Decoupled from any media library: pass `{id, url, alt}` items, or a `cell` snippet for full control of tile contents. `removeMode` — `'hover'` (default) reveals the × on hover/focus and keeps it visible on coarse (touch) pointers (ContextMenuTrigger pattern); `'always'` pins it for touch-first flows. `gridCols` (e.g. `'grid-cols-2 sm:grid-cols-4'`) switches the strip to a responsive CSS grid with column-filling tiles. Items take optional `progress` (0–100): an upload ring draws around the tile and a tiny % occupies the remove-× corner, cross-fading into the × at 100 (same slot, no layout shift; per-tile `role="progressbar"`). | `items: ThumbnailItem[]` (`{id, url?, alt, progress?}`), `max`, `size` (`xs\|sm\|md\|lg`), `removable`+`onRemove`, `removeMode` (`hover\|always`), `addable`+`onAdd`, `onSelect`, `gridCols`, `class`, `cell` snippet |
|
|
178
178
|
| `MediaWorkbench` | Full-featured image editor modal — upload, AI generate, AI edit, background removal, annotation canvas. **Preferred over `SmartImageEditor` for new image fields.** Two-column modal: preview left, 4 action tabs right (Generate, Edit, Remove BG, Annotate). Save uploads blob to `uploadEndpoint` or emits `onComplete`. Requires proxy endpoints from jst-23w1l.2. | `value` (bindable `string \| null`), `uploadEndpoint?` (default `/api/media`), `onComplete?`, `accept?` |
|
|
179
179
|
| `MediaLightbox` | Full-screen media preview — z-9040 backdrop, arrow-key paging when given an array, `+/-/0` zoom, download action, Escape/backdrop close. **Use this for the general "just show it big" case** — replaces hand-rolled `fixed inset-0 z-[9040]` modals. For annotated / markup-overlaid image previews, use `ImageLightbox` instead. | `src?`, `filename?`, `kind?: 'image' \| 'video'`, `downloadUrl?`, `items?: MediaItem[]`, `initialIndex?`, `onClose?`, `zIndex?` |
|
|
180
|
-
| `VideoPlayer` | **The default video surface — reach for this over a bare `<video controls>`.** One timeline band carrying a waveform-envelope scrubber, chapter flags (top edge), bookmark diamonds (bottom edge), diarized speaker colouring and scroll-to-zoom, plus an optional transcript pane (click to seek, active line auto-scroll, solo/skip per speaker, `Shift+T` toggle) and a "Talk time" legend (2+ speakers only; one voice gets no legend and no per-row name). **`kind="audio"` renders no stage**, and **`transport="band"`** (the audio default) drops the native `<video controls>` bar for a transport row on the band (play/pause, `now / total`, speed, mute + volume, fullscreen for video), so the band is the ONE timeline. A double-click on the picture toggles fullscreen (band transport; with native controls the browser owns the gesture), a buffering spinner shows while the media is waiting, and volume/mute survive a reload through `settingsStore` (else guarded localStorage). **Every intelligence prop is optional** — a bare mp4 still gets the scrubber, hover-time card, wheel volume and the full keyboard map (Space/K, arrows/J/L, vol/mute, −/= speed + Backspace reset, </> frame step, [/] chapters, B bookmark, t theater, ? help overlay), and each pane lights up as its data arrives (nothing shifts layout when diarization lands late). Wheel means volume over the video (shift = seek, alt = speed; a wheel/keyboard volume change also flashes a clear volume OSD — glyph, level bar and %, "Muted" at 0, opacity-only fade) and zoom over the band; Shift+wheel over the transcript steps line by line (a plain wheel over the transcript scrolls the pane natively — the page is never trapped). Internals live in `src/lib/video/` (band/keys/rate/deepLink/settings + contracts for audio/night/presence — see that README; gap audit vs the extension: `VIDEO-PLAYER-GAP-AUDIT.md`). | `src`, `kind?` (`'video' \| 'audio'`), `transport?` (`'native' \| 'band'`), `chapters?`, `speakers?`, `transcript?`, `bookmarks?`, `waveform?`, `talkTime?`, `startAt?`, `poster?`, `captionsSrc?`, `crossorigin?` (`'anonymous'` — what frame-reading features need), `syncUrlTime?` (`?t=` deep-link, opt-in), `transcriptPosition?` (`'below' \| 'side'`), `transcriptMaxHeight?` (pane bound — default `min(40vh, 28rem)` below, the player's height beside; the active-line follow scrolls only the pane; only a gesture that actually moves the PANE pauses it — a wheel that scrolls the page doesn't — behind a "Resume following" pill, and it resumes by itself on seek / line or chapter click / play after pause / `followResumeMs` (6000) idle while playing; a hovered chapter fills its whole span in the theme accent with full-height edges), `followResumeMs?`, `theater?` (bindable — theater mode, NOT fullscreen: control-bar button + `t`, the HOST lays the page out full-width; a side transcript moves below; remembered per viewer as the `player.theater` setting via `settingsStore`, else guarded localStorage), `theaterMode?` (`'width'` default — the host widens its page \| `'fill'` — the player becomes a fixed full-viewport dialog: `role="dialog"` + `aria-modal`, page scroll locked, Tab trapped, Esc/the button exits and returns focus, largest 16:9 stage that fits, transcript beside it ≥64rem else below, z from `--jatui-z-theater`; a remembered theater never fills on load, only a viewer gesture), `theaterButton?` (true), `onTheaterChange?`, `onTimeUpdate?`, `onAddBookmark?`, `onBookmarkContext?` |
|
|
180
|
+
| `VideoPlayer` | **The default video surface — reach for this over a bare `<video controls>`.** One timeline band carrying a waveform-envelope scrubber, chapter flags (top edge), bookmark diamonds (bottom edge), diarized speaker colouring and scroll-to-zoom, plus an optional transcript pane (click to seek, active line auto-scroll, solo/skip per speaker, `Shift+T` toggle) and a "Talk time" legend (2+ speakers only; one voice gets no legend and no per-row name). **`kind="audio"` renders no stage**, and **`transport="band"`** (the audio default) drops the native `<video controls>` bar for a transport row on the band (play/pause, `now / total`, speed, mute + volume, fullscreen for video), so the band is the ONE timeline. A double-click on the picture toggles fullscreen (band transport; with native controls the browser owns the gesture), a buffering spinner shows while the media is waiting, and volume/mute survive a reload through `settingsStore` (else guarded localStorage). **Every intelligence prop is optional** — a bare mp4 still gets the scrubber, hover-time card, wheel volume and the full keyboard map (Space/K, arrows/J/L, vol/mute, −/= speed + Backspace reset, </> frame step, [/] chapters, B bookmark, t theater, ? help overlay), and each pane lights up as its data arrives (nothing shifts layout when diarization lands late). Wheel means volume over the video (shift = seek, alt = speed; a wheel/keyboard volume change also flashes a clear volume OSD — glyph, level bar and %, "Muted" at 0, opacity-only fade) and zoom over the band; Shift+wheel over the transcript steps line by line (a plain wheel over the transcript scrolls the pane natively — the page is never trapped); `/` opens an in-transcript search (only when a transcript exists and focus is not in a text field; the omnibar yields): matches highlight in place with a live `n / m` count (`aria-live`), Enter/Shift+Enter walk next/previous (seek + the line follows), Esc closes the search first in the Esc layering. Right-click a transcript line (or `Menu` / `Shift+F10` on a focused one) for the **line menu** — Play from here, Copy text, Copy text with timestamp (`[0:02] text`), Copy link at this time when `buildTimeLink` supplies the URL builder (without it the item is absent; `?t=` shape via `buildTimeUrl`); Escape closes and returns focus to the line, the player's keys are dead while it floats, and in fill theater Esc closes the menu without leaving the theater. Internals live in `src/lib/video/` (band/keys/rate/deepLink/settings + contracts for audio/night/presence — see that README; gap audit vs the extension: `VIDEO-PLAYER-GAP-AUDIT.md`). | `src`, `kind?` (`'video' \| 'audio'`), `transport?` (`'native' \| 'band'`), `chapters?`, `speakers?`, `transcript?`, `bookmarks?`, `waveform?`, `talkTime?`, `startAt?`, `poster?`, `captionsSrc?`, `crossorigin?` (`'anonymous'` — what frame-reading features need), `syncUrlTime?` (`?t=` deep-link, opt-in), `transcriptPosition?` (`'below' \| 'side'`), `transcriptMaxHeight?` (pane bound — default `min(40vh, 28rem)` below, the player's height beside; the active-line follow scrolls only the pane; only a gesture that actually moves the PANE pauses it — a wheel that scrolls the page doesn't — behind a "Resume following" pill, and it resumes by itself on seek / line or chapter click / play after pause / `followResumeMs` (6000) idle while playing; a hovered chapter fills its whole span in the theme accent with full-height edges), `followResumeMs?`, `transcriptLineMenu?` (true — right-click/`Menu`/`Shift+F10` on a transcript line opens the line menu; `false` leaves transcript lines to the browser's own context menu), `buildTimeLink?` ((sec) → URL for "Copy link at this time"; the host owns the address shape), `theater?` (bindable — theater mode, NOT fullscreen: control-bar button + `t`, the HOST lays the page out full-width; a side transcript moves below; remembered per viewer as the `player.theater` setting via `settingsStore`, else guarded localStorage), `theaterMode?` (`'width'` default — the host widens its page \| `'fill'` — the player becomes a fixed full-viewport dialog: `role="dialog"` + `aria-modal`, page scroll locked, Tab trapped, Esc/the button exits and returns focus, largest 16:9 stage that fits, transcript beside it ≥64rem else below, z from `--jatui-z-theater`; a remembered theater never fills on load, only a viewer gesture), `theaterButton?` (true), `onTheaterChange?`, `onTimeUpdate?`, `onAddBookmark?`, `onBookmarkContext?` |
|
|
181
181
|
| `MediaCapture` | One-tap audio/selfie-video/screen(+mic) recording — a host-agnostic PRIMITIVE. Owns permission requests, MIME negotiation, the record/stop state machine, live self-preview, and mixes screen+mic audio into one track via AudioContext. Never uploads anything — hands the finished clip to `onCapture` as a Blob and the host decides what to do with it. `onCapture` may be async; the component shows "processing" until it resolves. | `onCapture: (result: MediaCaptureResult) => void \| Promise<void>`, `modes?` (default all three), `maxDurationMs?` (default 120000), `label?`, `hint?`, `compact?`, `disabled?` |
|
|
182
182
|
| `MultimodalCaptureField` | "Upload a file, or record yourself going over it" as one input surface — composes `FileDropzone` (capped to one file) with `MediaCapture`. File and recording are independent (either, both, or neither); neither half uploads anything, same "validate and hand back" contract as `FileDropzone`. | `onFileSelected?`, `onFileRemoved?`, `onCapture?`, `onRecordingRemoved?`, `accept?`, `maxFileSize?`, `modes?`, `maxDurationMs?`, `file?`/`fileUrl?`/`fileName?`, `recordingSummary?` |
|
|
183
183
|
| `ListenControl` | "Listen to this page" — a compact pill that expands into a TWO-ROW playback bar over the host's PER-SENTENCE audio (TTS): row 1 = play/pause · chapter chip + current sentence (full remaining width) · ONE settings pill ("1.25× · Sarah" → speed 0.75–2× + voice menu) · close; row 2 = prev/next sentence and −15s/+15s · a document-wide progress bar with chapter ticks (a hatched tail marks the not-yet-synthesized part), time, close. The host supplies an async `source` (sentences/paragraphs with ids → audio URLs, streamed page by page so playback starts on the first chunk while the rest synthesizes; pages merge by id so late URLs fill in) and gets `onSegmentChange(id)` at every boundary for highlight + scroll-follow. Media Session (lock-screen/headset controls, one document-wide position), keyboard scoped to the player (Space/K, ←/→ sentences, Shift+←/→ ±15s, [ / ] speed, Escape), speed+voice persisted in localStorage, `bind:api` for hosts wiring their own global keys, reduced-motion aware, token-only theming, never overflows its host (border-box, `max-width: 100%`, `min-width: 0` chain), fixed sticky mini-bar (same two rows) on narrow screens. Internals in `src/lib/listen/` (one timeline across per-sentence files — `buildTimeline` offsets; see `video/` for the doctrine). Demo synthesizes real WAVs locally. | `source`, `voices?`, `voiceId?` (bindable), `chapters?`, `title?`, `triggerLabel?`, `open?` (bindable), `preload?`, `estimateSec?`, `storageKey?`, `onSegmentChange?`, `onOpenChange?`, `api?` (bindable), `class?`, `children?` (custom trigger) |
|
|
@@ -198,6 +198,7 @@
|
|
|
198
198
|
type VideoAdSpan
|
|
199
199
|
} from '../video/rate';
|
|
200
200
|
import { readTimeParam, WRITE_THROTTLE_MS, writeTimeParam } from '../video/deepLink';
|
|
201
|
+
import { isContextMenuKey, lineTextWithTime } from '../video/lineMenu';
|
|
201
202
|
import {
|
|
202
203
|
biasCells,
|
|
203
204
|
biasGradient,
|
|
@@ -275,6 +276,9 @@
|
|
|
275
276
|
type VideoSettingsStore
|
|
276
277
|
} from '../video/settings';
|
|
277
278
|
import SettingsPanel from '../video/settings/SettingsPanel.svelte';
|
|
279
|
+
import ContextMenu from './ContextMenu.svelte';
|
|
280
|
+
import type { ContextMenuItem } from './ContextMenu.types';
|
|
281
|
+
import { copyText } from '../utils/clipboard';
|
|
278
282
|
|
|
279
283
|
let {
|
|
280
284
|
src,
|
|
@@ -301,6 +305,8 @@
|
|
|
301
305
|
transcriptPosition = 'below',
|
|
302
306
|
transcriptMaxHeight,
|
|
303
307
|
followResumeMs = FOLLOW_RESUME_IDLE_MS,
|
|
308
|
+
transcriptLineMenu = true,
|
|
309
|
+
buildTimeLink = undefined,
|
|
304
310
|
theater = $bindable(false),
|
|
305
311
|
theaterMode = 'width',
|
|
306
312
|
theaterButton = true,
|
|
@@ -485,6 +491,24 @@
|
|
|
485
491
|
* play after pause or the "Resume following" pill bring it back).
|
|
486
492
|
*/
|
|
487
493
|
followResumeMs?: number;
|
|
494
|
+
/**
|
|
495
|
+
* Right-click a transcript line (or Context-menu / Shift+F10 on a
|
|
496
|
+
* focused one) for the line's verbs (extension-library parity,
|
|
497
|
+
* library.js:8458): Play from here, Copy text, Copy text with
|
|
498
|
+
* timestamp — and, when `buildTimeLink` is supplied, Copy link at this
|
|
499
|
+
* time. Default on: the menu only exists where a transcript exists.
|
|
500
|
+
* Pass `false` to leave transcript lines to the browser's own context
|
|
501
|
+
* menu.
|
|
502
|
+
*/
|
|
503
|
+
transcriptLineMenu?: boolean;
|
|
504
|
+
/**
|
|
505
|
+
* Builds the shareable URL for "Copy link at this time" — the HOST
|
|
506
|
+
* supplies it because only the host knows its page's address shape
|
|
507
|
+
* (MOS builds `?t=90s` on its video page; `buildTimeUrl` in
|
|
508
|
+
* video/deepLink.ts is exactly that builder). Without it the item is
|
|
509
|
+
* simply not on the menu — the player never guesses a URL.
|
|
510
|
+
*/
|
|
511
|
+
buildTimeLink?: (sec: number) => string;
|
|
488
512
|
/**
|
|
489
513
|
* Theater mode (jatui-ee2ty) — bindable. NOT fullscreen: the player
|
|
490
514
|
* only says it wants the page's width; the HOST lays its page out
|
|
@@ -971,6 +995,15 @@
|
|
|
971
995
|
let searchMatchIdx = $state(-1);
|
|
972
996
|
let searchInputEl: HTMLInputElement | undefined = $state();
|
|
973
997
|
|
|
998
|
+
// ── Transcript line menu (jatui-3wl5x) ─────────────────────────────
|
|
999
|
+
// Right-click (or Context-menu / Shift+F10 on a focused row) opens the
|
|
1000
|
+
// line's verb menu (extension library.js:8458). ONE panel serves the
|
|
1001
|
+
// whole pane: opening re-points it at the row; closing clears the row.
|
|
1002
|
+
let lineMenuOpen = $state(false);
|
|
1003
|
+
let lineMenuX = $state(0);
|
|
1004
|
+
let lineMenuY = $state(0);
|
|
1005
|
+
let lineMenuSeg = $state<VideoTranscriptSegment | null>(null);
|
|
1006
|
+
|
|
974
1007
|
let zoomTimer: ReturnType<typeof setTimeout> | null = null;
|
|
975
1008
|
let osdTimer: ReturnType<typeof setTimeout> | null = null;
|
|
976
1009
|
|
|
@@ -3125,7 +3158,12 @@
|
|
|
3125
3158
|
!keysOpen &&
|
|
3126
3159
|
!searchOpen &&
|
|
3127
3160
|
!settingsOpen &&
|
|
3128
|
-
!isFullscreen
|
|
3161
|
+
!isFullscreen &&
|
|
3162
|
+
// The line menu (jatui-3wl5x) owns Escape while it is open — the
|
|
3163
|
+
// menu closes itself, the theater must not leave with it. The
|
|
3164
|
+
// panel's own handler may already have closed it by the time this
|
|
3165
|
+
// bubble-phase handler runs, hence the target check too.
|
|
3166
|
+
!(lineMenuOpen || (e.target instanceof Element && e.target.closest('[role="menu"]')))
|
|
3129
3167
|
) {
|
|
3130
3168
|
e.preventDefault();
|
|
3131
3169
|
toggleTheater();
|
|
@@ -3146,6 +3184,13 @@
|
|
|
3146
3184
|
// Space/Enter on a focused button activates THAT button (play, a
|
|
3147
3185
|
// speaker chip, a transcript row); toggling playback too would undo it.
|
|
3148
3186
|
if (tag === 'BUTTON' && (e.key === ' ' || e.key === 'Enter')) return;
|
|
3187
|
+
// The transcript line menu (jatui-3wl5x) owns the keyboard while it is
|
|
3188
|
+
// open: arrows/Enter drive it (its panel preventDefaults what it uses,
|
|
3189
|
+
// which the defaultPrevented guard above already yields to), Escape
|
|
3190
|
+
// closes it, and nothing else drives the player underneath a floating
|
|
3191
|
+
// menu. Tab is the shared ContextMenu's own behaviour (focus moves out
|
|
3192
|
+
// with the menu still up; click/Escape dismiss it).
|
|
3193
|
+
if (lineMenuOpen) return;
|
|
3149
3194
|
// mos-777hj — the chapter rail owns j/k (listNav) and Enter/Space (row
|
|
3150
3195
|
// activation) while focus is inside it; without this guard a rail `k`
|
|
3151
3196
|
// would also toggle play/pause and a rail `j` also seek ±5s.
|
|
@@ -3443,6 +3488,70 @@
|
|
|
3443
3488
|
seekTo(seg.startSec);
|
|
3444
3489
|
}
|
|
3445
3490
|
|
|
3491
|
+
// ── Transcript line menu (jatui-3wl5x, extension library.js:8458) ────
|
|
3492
|
+
/** Right-click a line: the menu opens at the cursor. The row takes focus
|
|
3493
|
+
* first — right-click alone doesn't move focus in every engine, and
|
|
3494
|
+
* giving it the row means the menu's focus-return lands back on the
|
|
3495
|
+
* line the menu was about (preventScroll: the row is already on screen). */
|
|
3496
|
+
function onRowContextMenu(seg: VideoTranscriptSegment, e: MouseEvent) {
|
|
3497
|
+
if (!transcriptLineMenu) return; // the browser's own menu stays
|
|
3498
|
+
e.preventDefault();
|
|
3499
|
+
rowRefs[seg.id]?.focus({ preventScroll: true });
|
|
3500
|
+
openLineMenu(seg, e.clientX, e.clientY);
|
|
3501
|
+
}
|
|
3502
|
+
/** Context-menu key / Shift+F10 on a focused line: the same menu, anchored
|
|
3503
|
+
* to the row (there is no cursor to place it at). */
|
|
3504
|
+
function onRowMenuKey(seg: VideoTranscriptSegment, e: KeyboardEvent) {
|
|
3505
|
+
if (!transcriptLineMenu || !isContextMenuKey(e)) return;
|
|
3506
|
+
e.preventDefault();
|
|
3507
|
+
const rect = rowRefs[seg.id]?.getBoundingClientRect();
|
|
3508
|
+
openLineMenu(seg, rect?.left ?? 0, rect ? rect.bottom : 0);
|
|
3509
|
+
}
|
|
3510
|
+
function openLineMenu(seg: VideoTranscriptSegment, x: number, y: number) {
|
|
3511
|
+
lineMenuSeg = seg;
|
|
3512
|
+
lineMenuX = x;
|
|
3513
|
+
lineMenuY = y;
|
|
3514
|
+
lineMenuOpen = true;
|
|
3515
|
+
}
|
|
3516
|
+
/** "Play from here": seek to the line's start and play — the verb the
|
|
3517
|
+
* click can't offer (a plain line click only seeks, never unpauses). */
|
|
3518
|
+
function playFromLine(seg: VideoTranscriptSegment) {
|
|
3519
|
+
seekTo(seg.startSec);
|
|
3520
|
+
const v = videoEl;
|
|
3521
|
+
if (v && v.paused) void v.play().catch(() => {});
|
|
3522
|
+
}
|
|
3523
|
+
/** One copy verb for all the payloads; the OSD and the polite region say
|
|
3524
|
+
* what happened — a clipboard write is otherwise invisible. */
|
|
3525
|
+
async function copyLine(label: string, payload: string) {
|
|
3526
|
+
if (!payload) return;
|
|
3527
|
+
const ok = await copyText(payload);
|
|
3528
|
+
flashOsd(ok ? 'Copied' : 'Copy failed');
|
|
3529
|
+
announce(ok ? `${label} copied` : 'Copy failed');
|
|
3530
|
+
}
|
|
3531
|
+
/** The menu's items for the row it is open on. `buildTimeLink` decides
|
|
3532
|
+
* whether the link verb exists at all (the host owns the URL shape). */
|
|
3533
|
+
const lineMenuItems = $derived.by<ContextMenuItem[]>(() => {
|
|
3534
|
+
const seg = lineMenuSeg;
|
|
3535
|
+
if (!seg) return [];
|
|
3536
|
+
const items: ContextMenuItem[] = [
|
|
3537
|
+
{ label: 'Play from here', icon: '▶', onSelect: () => playFromLine(seg) },
|
|
3538
|
+
{ label: 'Copy text', icon: '⧉', onSelect: () => copyLine('Copy text', seg.text) },
|
|
3539
|
+
{
|
|
3540
|
+
label: 'Copy text with timestamp',
|
|
3541
|
+
icon: '⧉',
|
|
3542
|
+
onSelect: () => copyLine('Copy text with timestamp', lineTextWithTime(seg.text, seg.startSec, useHours))
|
|
3543
|
+
}
|
|
3544
|
+
];
|
|
3545
|
+
if (buildTimeLink) {
|
|
3546
|
+
items.push({
|
|
3547
|
+
label: 'Copy link at this time',
|
|
3548
|
+
icon: '🔗',
|
|
3549
|
+
onSelect: () => copyLine('Copy link at this time', buildTimeLink(seg.startSec))
|
|
3550
|
+
});
|
|
3551
|
+
}
|
|
3552
|
+
return items;
|
|
3553
|
+
});
|
|
3554
|
+
|
|
3446
3555
|
// ── Transcript search (jatui-xmtnv) ───────────────────────────────────
|
|
3447
3556
|
/** `/`: open the box (showing the transcript if it was hidden) and hand
|
|
3448
3557
|
* it focus once it mounts — the effect below lands it. */
|
|
@@ -4297,6 +4406,8 @@
|
|
|
4297
4406
|
: filterDims(speakerKeyOf(seg), false, soloKey, mutedKeys)}
|
|
4298
4407
|
use:transcriptRowAction={seg}
|
|
4299
4408
|
onclick={() => onRowClick(seg)}
|
|
4409
|
+
oncontextmenu={(e) => onRowContextMenu(seg, e)}
|
|
4410
|
+
onkeydown={(e) => onRowMenuKey(seg, e)}
|
|
4300
4411
|
>
|
|
4301
4412
|
<span class="vp-row-time">{formatTime(seg.startSec, useHours)}</span>
|
|
4302
4413
|
{#if multiSpeaker}
|
|
@@ -4328,6 +4439,8 @@
|
|
|
4328
4439
|
: filterDims(speakerKeyOf(seg), false, soloKey, mutedKeys)}
|
|
4329
4440
|
use:transcriptRowAction={seg}
|
|
4330
4441
|
onclick={() => onRowClick(seg)}
|
|
4442
|
+
oncontextmenu={(e) => onRowContextMenu(seg, e)}
|
|
4443
|
+
onkeydown={(e) => onRowMenuKey(seg, e)}
|
|
4331
4444
|
>
|
|
4332
4445
|
<span class="vp-row-time">{formatTime(seg.startSec, useHours)}</span>
|
|
4333
4446
|
{#if multiSpeaker}
|
|
@@ -4394,6 +4507,22 @@
|
|
|
4394
4507
|
persisted={!!settingsStore}
|
|
4395
4508
|
/>
|
|
4396
4509
|
{/if}
|
|
4510
|
+
|
|
4511
|
+
{#if transcriptLineMenu && sortedSegments.length > 0}
|
|
4512
|
+
<!-- Transcript line menu (jatui-3wl5x, extension library.js:8458): ONE
|
|
4513
|
+
panel serves the whole pane, re-pointed at the row it was opened
|
|
4514
|
+
from. Rendered inside the root so the fill-theater Tab trap keeps
|
|
4515
|
+
cycling through its items; position:fixed places it at the cursor
|
|
4516
|
+
(or the focused row) wherever this spot sits in the layout. -->
|
|
4517
|
+
<ContextMenu
|
|
4518
|
+
bind:open={lineMenuOpen}
|
|
4519
|
+
x={lineMenuX}
|
|
4520
|
+
y={lineMenuY}
|
|
4521
|
+
items={lineMenuItems}
|
|
4522
|
+
header={lineMenuSeg ? formatTime(lineMenuSeg.startSec, useHours) : undefined}
|
|
4523
|
+
onClose={() => (lineMenuSeg = null)}
|
|
4524
|
+
/>
|
|
4525
|
+
{/if}
|
|
4397
4526
|
</div>
|
|
4398
4527
|
|
|
4399
4528
|
<style>
|
|
@@ -191,6 +191,24 @@ type $$ComponentProps = {
|
|
|
191
191
|
* play after pause or the "Resume following" pill bring it back).
|
|
192
192
|
*/
|
|
193
193
|
followResumeMs?: number;
|
|
194
|
+
/**
|
|
195
|
+
* Right-click a transcript line (or Context-menu / Shift+F10 on a
|
|
196
|
+
* focused one) for the line's verbs (extension-library parity,
|
|
197
|
+
* library.js:8458): Play from here, Copy text, Copy text with
|
|
198
|
+
* timestamp — and, when `buildTimeLink` is supplied, Copy link at this
|
|
199
|
+
* time. Default on: the menu only exists where a transcript exists.
|
|
200
|
+
* Pass `false` to leave transcript lines to the browser's own context
|
|
201
|
+
* menu.
|
|
202
|
+
*/
|
|
203
|
+
transcriptLineMenu?: boolean;
|
|
204
|
+
/**
|
|
205
|
+
* Builds the shareable URL for "Copy link at this time" — the HOST
|
|
206
|
+
* supplies it because only the host knows its page's address shape
|
|
207
|
+
* (MOS builds `?t=90s` on its video page; `buildTimeUrl` in
|
|
208
|
+
* video/deepLink.ts is exactly that builder). Without it the item is
|
|
209
|
+
* simply not on the menu — the player never guesses a URL.
|
|
210
|
+
*/
|
|
211
|
+
buildTimeLink?: (sec: number) => string;
|
|
194
212
|
/**
|
|
195
213
|
* Theater mode (jatui-ee2ty) — bindable. NOT fullscreen: the player
|
|
196
214
|
* only says it wants the page's width; the HOST lays its page out
|
package/dist/video/README.md
CHANGED
|
@@ -19,7 +19,9 @@ props + callback surfaces.
|
|
|
19
19
|
| `band/` | **.30** (done) | `window.ts` — the zoom-window math (moved out of band.ts, re-exported there; shared by every band layer without an import cycle). `axis.ts` — one-axis four-state arbitration (`resolveAxis`, `BandState`, the churn-guarded microtask `createBandReporter`), `playheadX` (the one "where you are" mapping — ≤0.2px agreement) and `overviewBracket`. `luma.ts` — the light track (`VideoLumaTrack`, the sidecar verbatim) → ribbon (`v² × 0.55`, max-per-interval). `markers.ts` — `VideoMarker` comments (clustered dots) + like heat. |
|
|
20
20
|
| `keys.ts` | .28 (done) | The keyboard map — `PLAYER_KEYS` (the `?` overlay renders it), `chapterJump`, `FRAME_STEP_SEC`. Every key that exists is in the map; a key not in the map doesn't exist. |
|
|
21
21
|
| `rate.ts` + `rate/` | .28 contract — **.32** (done) | `rate.ts` is the barrel. `compose.ts` — the contract (`composeRate`, clamp [0.0625, 16], `shouldWriteRate`'s 0.001 deadband with an exact return to base), the `SPEED_STEPS` staircase, `formatSpeedLabel`. `speaker.ts` — props → `advisors/speaker-rate`; manual rates keyed by NAME always beat measured. `silence.ts` — waveform → `advisors/smart-speed` map + boost + the OSD saving claim. `ads.ts` — `adSpans` ∪ `ad: true` turns; skip only when PLAYBACK runs into an ad, never when a seek lands in one. `tick.ts` — the component's own 120ms loop's one-tick decision. None of them write `playbackRate`; they advise. |
|
|
22
|
-
| `deepLink.ts` | .28 (done) | `?t=` parse/format/read/write (replaceState, throttled, other params preserved). |
|
|
22
|
+
| `deepLink.ts` | .28 (done) | `?t=` parse/format/read/write (replaceState, throttled, other params preserved) + `buildTimeUrl` (jatui-3wl5x) — the `?t=` URL builder hosts pass as `buildTimeLink` (MOS's "copy link at this time" shape). |
|
|
23
|
+
| `lineMenu.ts` | **jatui-3wl5x** | The transcript line right-click menu's pure pieces: `isContextMenuKey` (the keyboard's right-click — Context-menu key or Shift+F10) and `lineTextWithTime` (`[1:23] text`, the row's own clock). The component owns the DOM — which row opened it, cursor vs row anchoring, focus return — and the menu itself is the shared `ContextMenu`. |
|
|
24
|
+
| `transcriptSearch.ts` | **jatui-xmtnv** (E25/E57) | The transcript search + wheel line-scrub pure helpers: `searchHits` (case-insensitive, non-overlapping), `highlightRowHtml` (`<mark>`s around matches, everything else escaped — host transcript text is never markup), `pickMatch` (Enter walks matches from the playhead, wrapping), `lineScrubTarget` + `transcriptWheelDecision`/`effectiveWheelDelta` (Shift+wheel steps a line per ~100px notch; a plain wheel is never claimed). The component owns the box, focus, Esc layering and the `n / m` count. |
|
|
23
25
|
| `settings.ts` + `settings/` | **.35** (done) — knobs added by .30–.34 | `settings.ts` is the barrel. `registry.ts` — `VIDEO_SETTINGS`: id, group, label, **mandatory hint**, the prop it drives, the availability gate (`when`); `coerce` never throws (hostile values, throwing `valueOf`, Symbols → undefined). Labels/hints are the single source for the panel AND a host help page. `store.ts` — the host-supplied `VideoSettingsStore` (`get`/`set`, either async; jatui never persists on its own) and `createSettingsSession`: coerced reads, a choice made on screen beats a late read, per-id debounced writes flushed on close. `panel.ts` — `panelGroups` (rows only for features this player has — omitted, never disabled), value formatting, radio stepping. `SettingsPanel.svelte` — the generated dialog (switch / radio group / range, focus trap, Esc/`,` close, focus handed back). `remembered.ts` (jatui-xz6so) — remembered MEDIA state (E12: volume + mute survive a reload), coerced like settings and persisted through the same store split (`settingsStore`, else guarded localStorage) but deliberately never panel rows: remembering is not configuring. In the component a stored choice overlays its prop but never creates a presence-gated feature; `night`/`nightDepth` are written through their bindings and `N`/the moon wheel are saved back. The one permission ask is `sleep.desktopIdle` turned on, from that click. A knob lands WITH its reader. |
|
|
24
26
|
| `audio.ts` + `audio/` | **.31** (done) | `audio.ts` is the barrel. `rider.ts` — pure: RMS dBFS (silence → null), the 12 dB relative-gated loud estimate (rise 0.08 / forget 0.01), want-gain clamped 0.3–1.35, the asymmetric ride (down 0.15 / up 0.02), limiter constants + `compressorMakeupDb`/`limiterTrim` (DynamicsCompressor's automatic makeup, cancelled by a trim node), `routeSafety` (only blob:/data: or a `crossorigin` load — a same-origin URL may redirect cross-origin and route to SILENCE). `graph.ts` — `createAudioSession`: source → gain → quiet chain → limiter → trim → destination, analyser on the source. Context first, route last: the element is taken only when a feature is on, the source is route-safe and `ctx.state === 'running'` (gestures resume + retry; never navigator.userActivation). After routing it watches: an unsafe src or bit-exact zeros while audibly playing ⇒ `broken` (`unsafe-src` / `silent`) via `onBroken`. A construction throw is a permanent `build-failed` (context closed if the element wasn't taken; kept with source → destination if it was). Level off cancels before it sets; limiter moves are ramped. The element never enters the module — the component passes a `route` closure. |
|
|
25
27
|
| `night.ts` + `night/` | **.33** (done) | `night.ts` is the barrel. `meter.ts` — 32×18 frame bytes → Rec.709 luma + blown share, asymmetric smoothing, bias-light cells from the same bytes. `levels.ts` — luma → brightness/saturation/warmth/vignette off `NIGHT_FLOOR` 0.34, blind fixed level, the light-theme surround. `clock.ts` — local-clock `auto` window + the `N` auto/on/off cycle (no hyprsunset). `track.ts` — prediction (only ever adds dimming), flash intervals, `composeNight` (darker at once, brighter released per tick — never a jump). |
|
package/dist/video/deepLink.d.ts
CHANGED
|
@@ -34,6 +34,15 @@ export declare function readTimeParam(url?: string | URL | null): number | null;
|
|
|
34
34
|
* history API, same second) so the caller can skip the replaceState.
|
|
35
35
|
*/
|
|
36
36
|
export declare function writeTimeParam(sec: number, url?: string | URL | null): boolean;
|
|
37
|
+
/**
|
|
38
|
+
* Build the shareable URL for a time: the base URL (default: the current
|
|
39
|
+
* page) with its `t` param set — every other query param preserved. This is
|
|
40
|
+
* the builder MOS's "copy link at this time" uses (`?t=90s`) and the shape
|
|
41
|
+
* hosts pass as VideoPlayer's `buildTimeLink` prop. Null when there is no
|
|
42
|
+
* URL to build on (SSR, garbage base) — a builder that can't build returns
|
|
43
|
+
* nothing rather than throwing.
|
|
44
|
+
*/
|
|
45
|
+
export declare function buildTimeUrl(sec: number, url?: string | URL | null): string | null;
|
|
37
46
|
/** Strip the `t` param (e.g. when the host unmounts the player opt-in). */
|
|
38
47
|
export declare function clearTimeParam(url?: string | URL | null): boolean;
|
|
39
48
|
/** Throttle for URL writes: at most one replaceState per WRITE_THROTTLE_MS. */
|
package/dist/video/deepLink.js
CHANGED
|
@@ -88,6 +88,26 @@ export function writeTimeParam(sec, url = typeof location !== 'undefined' ? loca
|
|
|
88
88
|
history.replaceState(history.state, '', u);
|
|
89
89
|
return true;
|
|
90
90
|
}
|
|
91
|
+
/**
|
|
92
|
+
* Build the shareable URL for a time: the base URL (default: the current
|
|
93
|
+
* page) with its `t` param set — every other query param preserved. This is
|
|
94
|
+
* the builder MOS's "copy link at this time" uses (`?t=90s`) and the shape
|
|
95
|
+
* hosts pass as VideoPlayer's `buildTimeLink` prop. Null when there is no
|
|
96
|
+
* URL to build on (SSR, garbage base) — a builder that can't build returns
|
|
97
|
+
* nothing rather than throwing.
|
|
98
|
+
*/
|
|
99
|
+
export function buildTimeUrl(sec, url = typeof location !== 'undefined' ? location.href : null) {
|
|
100
|
+
if (!url)
|
|
101
|
+
return null;
|
|
102
|
+
try {
|
|
103
|
+
const u = new URL(String(url), typeof location !== 'undefined' ? location.href : undefined);
|
|
104
|
+
u.searchParams.set('t', formatTimeParam(sec));
|
|
105
|
+
return u.toString();
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
91
111
|
/** Strip the `t` param (e.g. when the host unmounts the player opt-in). */
|
|
92
112
|
export function clearTimeParam(url = typeof location !== 'undefined' ? location.href : null) {
|
|
93
113
|
if (typeof history === 'undefined' || typeof history.replaceState !== 'function')
|
package/dist/video/keys.js
CHANGED
|
@@ -36,6 +36,10 @@ export const PLAYER_KEYS = [
|
|
|
36
36
|
{ keys: 't', description: 'Theater mode on / off', when: 'theater' },
|
|
37
37
|
{ keys: 'Shift+T', description: 'Cycle transcript layout: side / below / hidden', when: 'transcript' },
|
|
38
38
|
{ keys: '/', description: 'Search transcript', when: 'transcript' },
|
|
39
|
+
// jatui-3wl5x — right-click a line (or the keyboard's right-click on a
|
|
40
|
+
// focused one) for the line menu: play from here, copy text / with time /
|
|
41
|
+
// link at time (the link verb only when the host supplies buildTimeLink).
|
|
42
|
+
{ keys: 'Menu / Shift+F10', description: 'On a transcript line: the line menu', when: 'transcript' },
|
|
39
43
|
// Shift+wheel over the transcript pane steps PLAYBACK line by line —
|
|
40
44
|
// a plain wheel there scrolls the pane natively and is never claimed.
|
|
41
45
|
{ keys: 'Shift+wheel', description: 'Over the transcript: steps line by line', when: 'transcript' },
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Transcript line right-click menu (jatui-3wl5x — extension-library parity,
|
|
3
|
+
* library.js:8458): right-clicking a transcript line (or Context-menu /
|
|
4
|
+
* Shift+F10 on a focused one) offers the line's verbs — Play from here,
|
|
5
|
+
* Copy text, Copy text with timestamp, and — when the host supplies a URL
|
|
6
|
+
* builder — Copy link at this time (MOS builds `?t=`; `buildTimeUrl` in
|
|
7
|
+
* deepLink.ts is that builder for the host's `buildTimeLink` prop).
|
|
8
|
+
*
|
|
9
|
+
* The pure pieces live here so they stay node-testable; the component owns
|
|
10
|
+
* the DOM: which row opened the menu, where it opens, focus return.
|
|
11
|
+
*/
|
|
12
|
+
/**
|
|
13
|
+
* The context-menu key (`e.key === 'ContextMenu'`) or Shift+F10 — the
|
|
14
|
+
* keyboard's right-click. Engines dispatch one or the other depending on
|
|
15
|
+
* the keyboard; plain F10 stays the browser's own menu-bar key.
|
|
16
|
+
*/
|
|
17
|
+
export declare function isContextMenuKey(e: {
|
|
18
|
+
key: string;
|
|
19
|
+
shiftKey: boolean;
|
|
20
|
+
}): boolean;
|
|
21
|
+
/**
|
|
22
|
+
* A transcript line quoted with its timestamp — `[1:23] The line text`.
|
|
23
|
+
* The time renders in the row's own clock (hours form on long videos), so
|
|
24
|
+
* what lands on the clipboard is exactly what the viewer read on screen.
|
|
25
|
+
*/
|
|
26
|
+
export declare function lineTextWithTime(text: string, startSec: number, useHours: boolean): string;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Transcript line right-click menu (jatui-3wl5x — extension-library parity,
|
|
3
|
+
* library.js:8458): right-clicking a transcript line (or Context-menu /
|
|
4
|
+
* Shift+F10 on a focused one) offers the line's verbs — Play from here,
|
|
5
|
+
* Copy text, Copy text with timestamp, and — when the host supplies a URL
|
|
6
|
+
* builder — Copy link at this time (MOS builds `?t=`; `buildTimeUrl` in
|
|
7
|
+
* deepLink.ts is that builder for the host's `buildTimeLink` prop).
|
|
8
|
+
*
|
|
9
|
+
* The pure pieces live here so they stay node-testable; the component owns
|
|
10
|
+
* the DOM: which row opened the menu, where it opens, focus return.
|
|
11
|
+
*/
|
|
12
|
+
import { formatTime } from './time';
|
|
13
|
+
/**
|
|
14
|
+
* The context-menu key (`e.key === 'ContextMenu'`) or Shift+F10 — the
|
|
15
|
+
* keyboard's right-click. Engines dispatch one or the other depending on
|
|
16
|
+
* the keyboard; plain F10 stays the browser's own menu-bar key.
|
|
17
|
+
*/
|
|
18
|
+
export function isContextMenuKey(e) {
|
|
19
|
+
return e.key === 'ContextMenu' || (e.key === 'F10' && e.shiftKey);
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* A transcript line quoted with its timestamp — `[1:23] The line text`.
|
|
23
|
+
* The time renders in the row's own clock (hours form on long videos), so
|
|
24
|
+
* what lands on the clipboard is exactly what the viewer read on screen.
|
|
25
|
+
*/
|
|
26
|
+
export function lineTextWithTime(text, startSec, useHours) {
|
|
27
|
+
return `[${formatTime(startSec, useHours)}] ${text}`;
|
|
28
|
+
}
|