@joewinke/jatui 0.34.2 → 0.36.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 +9 -3
- package/dist/actions/reveal.d.ts +72 -11
- package/dist/actions/reveal.js +103 -14
- package/dist/components/AudioRecorder.svelte +23 -1
- package/dist/components/AudioRecorder.svelte.d.ts +13 -1
- package/dist/components/BulkActionBar.svelte +17 -2
- package/dist/components/BulkActionBar.svelte.d.ts +5 -0
- package/dist/components/Drawer.svelte +20 -1
- package/dist/components/Drawer.svelte.d.ts +11 -0
- package/dist/components/FloatingActionBar.svelte +26 -11
- package/dist/components/FloatingActionBar.svelte.d.ts +5 -0
- package/dist/components/KeyboardShortcutsOverlay.svelte +180 -7
- package/dist/components/KeyboardShortcutsOverlay.svelte.d.ts +21 -0
- package/dist/components/ListenControl.svelte +1064 -0
- package/dist/components/ListenControl.svelte.d.ts +87 -0
- package/dist/components/ThumbnailGrid.svelte +34 -7
- package/dist/components/ThumbnailGrid.svelte.d.ts +17 -1
- package/dist/components/table/DataTable.svelte.d.ts +2 -2
- package/dist/index.d.ts +9 -3
- package/dist/index.js +9 -2
- package/dist/listen/keys.d.ts +36 -0
- package/dist/listen/keys.js +68 -0
- package/dist/listen/mediaSession.d.ts +90 -0
- package/dist/listen/mediaSession.js +108 -0
- package/dist/listen/prefs.d.ts +49 -0
- package/dist/listen/prefs.js +97 -0
- package/dist/listen/segments.d.ts +101 -0
- package/dist/listen/segments.js +224 -0
- package/dist/listen/types.d.ts +81 -0
- package/dist/listen/types.js +7 -0
- package/dist/motion/index.d.ts +1 -1
- package/dist/motion/index.js +1 -1
- package/dist/utils/audioRecorder.d.ts +6 -1
- package/dist/utils/audioRecorder.js +5 -0
- package/dist/utils/audioRecorderCore.d.ts +34 -0
- package/dist/utils/audioRecorderCore.js +43 -6
- package/dist/utils/keyboardShortcutHub.d.ts +73 -0
- package/dist/utils/keyboardShortcutHub.js +89 -0
- package/dist/utils/mentionParser.d.ts +44 -2
- package/dist/utils/mentionParser.js +84 -4
- package/package.json +2 -1
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
export type { ListenSegment, ListenChapter, ListenVoice, ListenSegmentPage, ListenSegmentSource, ListenStatus, ListenApi } from '../listen/types';
|
|
2
|
+
/**
|
|
3
|
+
* ListenControl — "listen to this page" as one compact control.
|
|
4
|
+
*
|
|
5
|
+
* A small pill that expands into a playback bar: play/pause, −15s/+15s and
|
|
6
|
+
* previous/next sentence, speed (0.75–2×), a voice picker, a document-wide
|
|
7
|
+
* progress bar with chapter/section ticks, time, close. Built for hosts
|
|
8
|
+
* that synthesize audio per sentence (TTS): the host supplies an async
|
|
9
|
+
* SEGMENT SOURCE — sentences/paragraphs with ids → audio URLs — and the
|
|
10
|
+
* player starts on the first chunk while the rest synthesizes, hatching
|
|
11
|
+
* the not-yet-real tail of the progress bar. Every segment boundary fires
|
|
12
|
+
* `onSegmentChange(id)` so the host can highlight + scroll-follow.
|
|
13
|
+
*
|
|
14
|
+
* ── One timeline across per-sentence files ──────────────────────────────
|
|
15
|
+
* A "listen" player is a playlist, not one media element. Each segment has
|
|
16
|
+
* its own URL, so the engine walks segments sequentially through ONE
|
|
17
|
+
* hidden `<audio>` element and reports ONE document-wide time: the
|
|
18
|
+
* elapsed clock, the progress fill, the ticks, a ±15s skip that crosses a
|
|
19
|
+
* boundary and a seek from the slider all map through `buildTimeline`'s
|
|
20
|
+
* offsets (listen/segments.ts) — the same one-axis discipline as the
|
|
21
|
+
* VideoPlayer band. Durations the host knows (`durationSec`, TTS usually
|
|
22
|
+
* reports them) lay the timeline out immediately; unknown ones are
|
|
23
|
+
* measured on load and ESTIMATED until then, self-correcting as they land.
|
|
24
|
+
*
|
|
25
|
+
* The transport row borrows VideoPlayer's `kind="audio"` / `transport=
|
|
26
|
+
* "band"` shape (round primary play, ghost transport buttons, tabular
|
|
27
|
+
* clock) and reuses its pure time formatting (`video/time.ts`); the
|
|
28
|
+
* per-segment playlist engine is what doesn't fit inside VideoPlayer and
|
|
29
|
+
* lives here instead.
|
|
30
|
+
*
|
|
31
|
+
* ── Media Session ───────────────────────────────────────────────────────
|
|
32
|
+
* Lock-screen/headset/OS controls are wired while open: metadata (the
|
|
33
|
+
* `title` prop + current voice), play/pause, previous/next track (the
|
|
34
|
+
* sentence skip), seekbackward/seekforward/seekto, and ONE document-wide
|
|
35
|
+
* position state. Feature-detected end to end — nothing runs where the
|
|
36
|
+
* API is missing (listen/mediaSession.ts).
|
|
37
|
+
*
|
|
38
|
+
* ── Scope honesty ───────────────────────────────────────────────────────
|
|
39
|
+
* The keyboard map (Space/K, ←/→ sentences, Shift+←/→ ±15s, [ / ] speed,
|
|
40
|
+
* Escape) fires only while focus is inside the player — a library
|
|
41
|
+
* component must not hijack the host page's keys. Hosts wanting global
|
|
42
|
+
* keys bind `api` and wire their own handler with the same actions.
|
|
43
|
+
*
|
|
44
|
+
* Theming is token-only (DaisyUI semantic vars); motion is reduced-motion
|
|
45
|
+
* aware; everything is labelled; on narrow screens the expanded bar
|
|
46
|
+
* becomes a fixed sticky mini-bar at the bottom.
|
|
47
|
+
*/
|
|
48
|
+
import { type Snippet } from 'svelte';
|
|
49
|
+
import type { ListenApi, ListenChapter, ListenSegment, ListenSegmentSource, ListenVoice } from '../listen/types';
|
|
50
|
+
interface Props {
|
|
51
|
+
/**
|
|
52
|
+
* Host-supplied async segment source (sentences/paragraphs with ids →
|
|
53
|
+
* audio). Called per voice; pages arrive as chunks synthesize and are
|
|
54
|
+
* merged by id, so a later page can fill a segment's `url` in late.
|
|
55
|
+
* Pass a STABLE reference — a fresh inline arrow re-runs the source.
|
|
56
|
+
*/
|
|
57
|
+
source: ListenSegmentSource;
|
|
58
|
+
/** The voices the host offers. Empty ⇒ no picker (one implicit voice, ''). */
|
|
59
|
+
voices?: ListenVoice[];
|
|
60
|
+
/** The picked voice (bindable). Default: the first voice, persisted to storage. */
|
|
61
|
+
voiceId?: string;
|
|
62
|
+
/** Sections for the progress ticks + the caption's chapter label. */
|
|
63
|
+
chapters?: ListenChapter[];
|
|
64
|
+
/** Document title — lock-screen metadata + the bar's aria-label. */
|
|
65
|
+
title?: string;
|
|
66
|
+
/** The pill's label. Default `Listen`. */
|
|
67
|
+
triggerLabel?: string;
|
|
68
|
+
/** Expanded state (bindable). */
|
|
69
|
+
open?: boolean;
|
|
70
|
+
/** Load the source on mount instead of waiting for the first open (TTS cost is the host's call). */
|
|
71
|
+
preload?: boolean;
|
|
72
|
+
/** Estimate for a segment whose duration isn't known yet. Default 3s. */
|
|
73
|
+
estimateSec?: number;
|
|
74
|
+
/** localStorage key prefix for speed/voice. Default `jatui.listen` (→ `jatui.listen:prefs`). */
|
|
75
|
+
storageKey?: string;
|
|
76
|
+
/** Segment-boundary hook — host-side highlight + scroll-follow. */
|
|
77
|
+
onSegmentChange?: (id: string | null, segment: ListenSegment | null) => void;
|
|
78
|
+
onOpenChange?: (open: boolean) => void;
|
|
79
|
+
/** Instance API for hosts wiring their own global keys / external controls. */
|
|
80
|
+
api?: ListenApi;
|
|
81
|
+
class?: string;
|
|
82
|
+
/** Custom trigger content (replaces the headphones+label default). */
|
|
83
|
+
children?: Snippet;
|
|
84
|
+
}
|
|
85
|
+
declare const ListenControl: import("svelte").Component<Props, {}, "open" | "voiceId" | "api">;
|
|
86
|
+
type ListenControl = ReturnType<typeof ListenControl>;
|
|
87
|
+
export default ListenControl;
|
|
@@ -21,12 +21,28 @@
|
|
|
21
21
|
max?: number;
|
|
22
22
|
/** Tile size. Default 'md'. */
|
|
23
23
|
size?: 'xs' | 'sm' | 'md' | 'lg';
|
|
24
|
-
/** Show remove (×) buttons
|
|
24
|
+
/** Show remove (×) buttons; requires `onRemove`. Visibility follows `removeMode`. */
|
|
25
25
|
removable?: boolean;
|
|
26
|
+
/**
|
|
27
|
+
* When the remove × is visible. 'hover' (default) reveals it on
|
|
28
|
+
* hover/focus and keeps it visible on coarse (touch) pointers — a
|
|
29
|
+
* hover-only control is unusable on touch (the ContextMenuTrigger
|
|
30
|
+
* pattern). 'always' pins it visible for touch-first flows.
|
|
31
|
+
*/
|
|
32
|
+
removeMode?: 'hover' | 'always';
|
|
26
33
|
/** Show the dashed add tile at the end; requires `onAdd`. */
|
|
27
34
|
addable?: boolean;
|
|
28
35
|
/** Tooltip / aria-label for the add tile. Default 'Add items'. */
|
|
29
36
|
addLabel?: string;
|
|
37
|
+
/**
|
|
38
|
+
* Tailwind grid-cols classes for a responsive tile grid, e.g.
|
|
39
|
+
* 'grid-cols-2 sm:grid-cols-4'. When set, the root renders as a CSS grid
|
|
40
|
+
* (`grid {gridCols} gap-2`) instead of the fixed flex-wrap strip, and
|
|
41
|
+
* tiles stretch to fill their column (height still follows `size`).
|
|
42
|
+
*/
|
|
43
|
+
gridCols?: string;
|
|
44
|
+
/** Extra classes for the root container, composed after the layout classes. */
|
|
45
|
+
class?: string;
|
|
30
46
|
/** Fired when a thumbnail is clicked. */
|
|
31
47
|
onSelect?: (item: ThumbnailItem) => void;
|
|
32
48
|
/** Fired when a remove (×) button is clicked. */
|
|
@@ -46,12 +62,15 @@
|
|
|
46
62
|
max = 8,
|
|
47
63
|
size = 'md',
|
|
48
64
|
removable = false,
|
|
65
|
+
removeMode = 'hover',
|
|
49
66
|
addable = false,
|
|
50
67
|
addLabel = 'Add items',
|
|
68
|
+
gridCols,
|
|
51
69
|
onSelect,
|
|
52
70
|
onRemove,
|
|
53
71
|
onAdd,
|
|
54
|
-
cell
|
|
72
|
+
cell,
|
|
73
|
+
class: className = ''
|
|
55
74
|
}: Props = $props();
|
|
56
75
|
|
|
57
76
|
const sizeClasses: Record<NonNullable<Props['size']>, string> = {
|
|
@@ -63,18 +82,26 @@
|
|
|
63
82
|
|
|
64
83
|
const visible = $derived(items.slice(0, max));
|
|
65
84
|
const overflow = $derived(Math.max(0, items.length - max));
|
|
85
|
+
/** Grid mode when the caller supplies grid-cols classes; tiles fill their column. */
|
|
86
|
+
const isGrid = $derived(Boolean(gridCols));
|
|
87
|
+
const fillClass = $derived(isGrid ? ' w-full' : '');
|
|
88
|
+
const removeVisibility = $derived(
|
|
89
|
+
removeMode === 'always'
|
|
90
|
+
? 'opacity-100'
|
|
91
|
+
: 'opacity-0 transition-opacity group-hover:opacity-100 focus-visible:opacity-100 pointer-coarse:opacity-100'
|
|
92
|
+
);
|
|
66
93
|
|
|
67
94
|
function shortLabel(alt: string): string {
|
|
68
95
|
return alt.length > 14 ? `${alt.slice(0, 14)}…` : alt;
|
|
69
96
|
}
|
|
70
97
|
</script>
|
|
71
98
|
|
|
72
|
-
<div class="flex flex-wrap items-center gap-2">
|
|
99
|
+
<div class="{isGrid ? 'grid gap-2' : 'flex flex-wrap items-center gap-2'} {gridCols ?? ''} {className}">
|
|
73
100
|
{#each visible as item (item.id)}
|
|
74
101
|
<div class="group relative">
|
|
75
102
|
<button
|
|
76
103
|
type="button"
|
|
77
|
-
class="{sizeClasses[size]} cursor-pointer overflow-hidden rounded border transition-all duration-200
|
|
104
|
+
class="{sizeClasses[size]}{fillClass} cursor-pointer overflow-hidden rounded border transition-all duration-200
|
|
78
105
|
{removable ? 'border-2 border-accent' : 'border-base-300 hover:border-primary'}"
|
|
79
106
|
onclick={() => onSelect?.(item)}
|
|
80
107
|
title={item.alt}
|
|
@@ -96,7 +123,7 @@
|
|
|
96
123
|
{#if removable && onRemove}
|
|
97
124
|
<button
|
|
98
125
|
type="button"
|
|
99
|
-
class="absolute -top-1 -right-1 z-10 flex h-4 w-4 items-center justify-center rounded-full bg-error text-error-content
|
|
126
|
+
class="absolute -top-1 -right-1 z-10 flex h-4 w-4 items-center justify-center rounded-full bg-error text-error-content {removeVisibility}"
|
|
100
127
|
onclick={(e) => {
|
|
101
128
|
e.stopPropagation();
|
|
102
129
|
onRemove(item);
|
|
@@ -114,7 +141,7 @@
|
|
|
114
141
|
|
|
115
142
|
{#if overflow > 0}
|
|
116
143
|
<div
|
|
117
|
-
class="{sizeClasses[size]} flex items-center justify-center rounded border border-base-300 bg-base-200"
|
|
144
|
+
class="{sizeClasses[size]}{fillClass} flex items-center justify-center rounded border border-base-300 bg-base-200"
|
|
118
145
|
title="{overflow} more item{overflow === 1 ? '' : 's'}"
|
|
119
146
|
>
|
|
120
147
|
<span class="text-xs opacity-70">+{overflow}</span>
|
|
@@ -124,7 +151,7 @@
|
|
|
124
151
|
{#if addable && onAdd}
|
|
125
152
|
<button
|
|
126
153
|
type="button"
|
|
127
|
-
class="{sizeClasses[size]} flex scale-90 cursor-pointer items-center justify-center rounded border-2 border-dashed border-primary/30 bg-primary/5 transition-all duration-200 hover:border-primary/50 hover:bg-primary/10"
|
|
154
|
+
class="{sizeClasses[size]}{fillClass} flex scale-90 cursor-pointer items-center justify-center rounded border-2 border-dashed border-primary/30 bg-primary/5 transition-all duration-200 hover:border-primary/50 hover:bg-primary/10"
|
|
128
155
|
onclick={onAdd}
|
|
129
156
|
title={addLabel}
|
|
130
157
|
aria-label={addLabel}
|
|
@@ -18,12 +18,28 @@ interface Props {
|
|
|
18
18
|
max?: number;
|
|
19
19
|
/** Tile size. Default 'md'. */
|
|
20
20
|
size?: 'xs' | 'sm' | 'md' | 'lg';
|
|
21
|
-
/** Show remove (×) buttons
|
|
21
|
+
/** Show remove (×) buttons; requires `onRemove`. Visibility follows `removeMode`. */
|
|
22
22
|
removable?: boolean;
|
|
23
|
+
/**
|
|
24
|
+
* When the remove × is visible. 'hover' (default) reveals it on
|
|
25
|
+
* hover/focus and keeps it visible on coarse (touch) pointers — a
|
|
26
|
+
* hover-only control is unusable on touch (the ContextMenuTrigger
|
|
27
|
+
* pattern). 'always' pins it visible for touch-first flows.
|
|
28
|
+
*/
|
|
29
|
+
removeMode?: 'hover' | 'always';
|
|
23
30
|
/** Show the dashed add tile at the end; requires `onAdd`. */
|
|
24
31
|
addable?: boolean;
|
|
25
32
|
/** Tooltip / aria-label for the add tile. Default 'Add items'. */
|
|
26
33
|
addLabel?: string;
|
|
34
|
+
/**
|
|
35
|
+
* Tailwind grid-cols classes for a responsive tile grid, e.g.
|
|
36
|
+
* 'grid-cols-2 sm:grid-cols-4'. When set, the root renders as a CSS grid
|
|
37
|
+
* (`grid {gridCols} gap-2`) instead of the fixed flex-wrap strip, and
|
|
38
|
+
* tiles stretch to fill their column (height still follows `size`).
|
|
39
|
+
*/
|
|
40
|
+
gridCols?: string;
|
|
41
|
+
/** Extra classes for the root container, composed after the layout classes. */
|
|
42
|
+
class?: string;
|
|
27
43
|
/** Fired when a thumbnail is clicked. */
|
|
28
44
|
onSelect?: (item: ThumbnailItem) => void;
|
|
29
45
|
/** Fired when a remove (×) button is clicked. */
|
|
@@ -2,7 +2,7 @@ import type { DataTableProps } from './types.js';
|
|
|
2
2
|
declare function $$render<TData>(): {
|
|
3
3
|
props: DataTableProps<TData>;
|
|
4
4
|
exports: {};
|
|
5
|
-
bindings: "
|
|
5
|
+
bindings: "api" | "collapsed" | "sorting";
|
|
6
6
|
slots: {};
|
|
7
7
|
events: {};
|
|
8
8
|
};
|
|
@@ -10,7 +10,7 @@ declare class __sveltets_Render<TData> {
|
|
|
10
10
|
props(): ReturnType<typeof $$render<TData>>['props'];
|
|
11
11
|
events(): ReturnType<typeof $$render<TData>>['events'];
|
|
12
12
|
slots(): ReturnType<typeof $$render<TData>>['slots'];
|
|
13
|
-
bindings(): "
|
|
13
|
+
bindings(): "api" | "collapsed" | "sorting";
|
|
14
14
|
exports(): {};
|
|
15
15
|
}
|
|
16
16
|
interface $$IsomorphicComponent {
|
package/dist/index.d.ts
CHANGED
|
@@ -18,6 +18,10 @@ export type { ChapterBlock, ChapterStateVisual } from './components/replay/Chapt
|
|
|
18
18
|
export { default as TranscriptPlayer } from './components/TranscriptPlayer.svelte';
|
|
19
19
|
export type { TranscriptSegment, TranscriptBookmark, TranscriptChapter, TranscriptSpeakerTalkTime, } from './components/TranscriptPlayer.svelte';
|
|
20
20
|
export { default as VideoPlayer } from './components/VideoPlayer.svelte';
|
|
21
|
+
export { default as ListenControl } from './components/ListenControl.svelte';
|
|
22
|
+
export type { ListenSegment, ListenChapter, ListenVoice, ListenSegmentPage, ListenSegmentSource, ListenStatus, ListenApi } from './listen/types';
|
|
23
|
+
export { LISTEN_KEYS } from './listen/keys';
|
|
24
|
+
export { LISTEN_SPEEDS, SPEED_FLOOR, SPEED_CEIL, clampSpeed, stepSpeed, formatListenSpeed } from './listen/prefs';
|
|
21
25
|
export type { VideoChapter, VideoSpeakerTurn, VideoTranscriptSegment, VideoBookmark, VideoWaveform, VideoSpeakerTalkTime, VideoLumaTrack, VideoMarker, BandState, BandAxis, NightMode, NightWindow, AudioState, AudioStatus, AudioBrokenReason, VideoAdSpan, RateReport, WindDownState, WindDownPhase, PresenceSource, VideoResumeStore, FlipSource, } from './components/VideoPlayer.svelte';
|
|
22
26
|
export { speakerRateMap, toRateTurns, toRateSegments } from './video/rate';
|
|
23
27
|
export { idleDetectionSupported, idleDetectionPermission, requestIdleDetection, localResumeStore, flipOpen, flipClose, clearPoster, type IdlePermission } from './video/presence';
|
|
@@ -89,7 +93,7 @@ export { default as ResizablePanel } from './components/ResizablePanel.svelte';
|
|
|
89
93
|
export { default as VoicePlayer } from './components/VoicePlayer.svelte';
|
|
90
94
|
export { default as SpeechForm } from './components/SpeechForm.svelte';
|
|
91
95
|
export { default as AudioRecorder } from './components/AudioRecorder.svelte';
|
|
92
|
-
export type { AudioRecordingResult } from './utils/audioRecorderCore';
|
|
96
|
+
export type { AudioRecordingResult, AudioRecorderChunk } from './utils/audioRecorderCore';
|
|
93
97
|
export { createAudioRecorder } from './utils/audioRecorder';
|
|
94
98
|
export type { AudioRecorder, AudioRecorderCore, AudioRecorderError, AudioRecorderErrorKind, AudioRecorderFrame, AudioRecorderMode, AudioRecorderOptions, AudioRecorderPhase, AudioRecorderSnapshot } from './utils/audioRecorderCore';
|
|
95
99
|
export { default as ThemeSelector } from './components/ThemeSelector.svelte';
|
|
@@ -168,7 +172,8 @@ export { default as PipelineColumn } from './components/pipeline/PipelineColumn.
|
|
|
168
172
|
export { default as PipelineCard } from './components/pipeline/PipelineCard.svelte';
|
|
169
173
|
export type { PipelineStage, StageColors, PipelineItem, PipelineMetric, StageMetrics, PipelineViewMode, PipelineTableColumn, PipelineDragConfig, PipelineConfig, PipelineStageChangeEvent, PipelineItemClickEvent } from './types/pipeline';
|
|
170
174
|
export { DEFAULT_DRAG_CONFIG, DEFAULT_PIPELINE_CONFIG } from './types/pipeline';
|
|
171
|
-
export { parseMentions, renderMentionsAsHTML, createMentionString, extractPlainText, findMentionAtCursor, findPartialMention, replaceMentionInContent, escapeHTML, countMentions, setMentionUrlGenerator } from './utils/mentionParser';
|
|
175
|
+
export { parseMentions, renderMentionsAsHTML, createMentionString, extractPlainText, findMentionAtCursor, findPartialMention, replaceMentionInContent, escapeHTML, countMentions, hasMentions, setMentionUrlGenerator, registerMentionType, extractAgentMentions, getUniqueMentionsByType, getMentionedUserIds, AGENT_MENTION_TYPE } from './utils/mentionParser';
|
|
176
|
+
export type { MentionMatch, MentionUrlGenerator, MentionTypeConfig } from './utils/mentionParser';
|
|
172
177
|
export { default as CommentThread } from './components/CommentThread.svelte';
|
|
173
178
|
export type { Comment, CommentEntityType, CommentVisibility, CommentCreateInput, CommentCallbacks } from './types/comments';
|
|
174
179
|
export { COMMENT_ENTITY_TYPES, COMMENT_VISIBILITIES } from './types/comments';
|
|
@@ -183,6 +188,7 @@ export type { MarkupTool, Point, MarkupShape, ArrowShape, RectangleShape, Ellips
|
|
|
183
188
|
export type { Annotation, AnnotationProfile, AnnotationThread, AnnotationCallbacks, } from './types/annotation';
|
|
184
189
|
export { default as KeyboardShortcutsOverlay } from './components/KeyboardShortcutsOverlay.svelte';
|
|
185
190
|
export type { KeyboardShortcut, SectionEntry as KeyboardShortcutSectionEntry, ShortcutSection as KeyboardShortcutSection, } from './components/KeyboardShortcutsOverlay.svelte';
|
|
191
|
+
export { keyboardShortcutHub, KEYBOARD_SHORTCUT_HUB_ID, type KeyboardShortcutHost, type ShortcutContribution, type ShortcutSectionLike, } from './utils/keyboardShortcutHub';
|
|
186
192
|
export { createShortcutRegistry, getShortcutRegistry, formatShortcut, parseCombo as parseShortcutCombo, isTypingTarget, detectPlatform as detectShortcutPlatform, SIDEBAR_TOGGLE_KEY, normalizeCombo, comboFromEvent, validateCombo, localStorageBindingStore, BROWSER_RESERVED_COMBOS, } from './utils/keyboardShortcutRegistry';
|
|
187
193
|
export type { ShortcutRegistry, ShortcutDefinition, ShortcutModifier, ShortcutPlatform, PushContextOptions as ShortcutContextOptions, RegistrySection as ShortcutRegistrySection, ToSectionsOptions as ShortcutSectionsOptions, ShortcutRegistryOptions, ShortcutEventTarget, ShortcutDeclaration, BindingOverrides, BindingStore, BindingInfo, ShortcutConflict, ShortcutConflictSeverity, FindConflictsTarget, AllConflictsOptions as ShortcutAllConflictsOptions, ReservedCombo, RebindResult, RebindOptions as ShortcutRebindOptions, RejectedBinding, RawShortcutHandler, ModifierState as ShortcutModifierState, } from './utils/keyboardShortcutRegistry';
|
|
188
194
|
export { default as MapView } from './components/MapView.svelte';
|
|
@@ -196,7 +202,7 @@ export { staggerEntrance } from './actions/staggerEntrance';
|
|
|
196
202
|
export type { StaggerEntranceOptions } from './actions/staggerEntrance';
|
|
197
203
|
export { attention } from './actions/attention';
|
|
198
204
|
export type { AttentionEffect, AttentionOptions } from './actions/attention';
|
|
199
|
-
export { reveal, REVEAL_HIDDEN_CLASS, REVEAL_BASE_CLASS } from './actions/reveal';
|
|
205
|
+
export { reveal, REVEAL_HIDDEN_CLASS, REVEAL_BASE_CLASS, REVEAL_OFF_SELECTOR, REVEAL_MAX_DELAY_SECONDS } from './actions/reveal';
|
|
200
206
|
export type { RevealOptions, RevealVariant } from './actions/reveal';
|
|
201
207
|
export { default as ThumbnailGrid } from './components/ThumbnailGrid.svelte';
|
|
202
208
|
export type { ThumbnailItem } from './components/ThumbnailGrid.svelte';
|
package/dist/index.js
CHANGED
|
@@ -33,6 +33,12 @@ export { default as TranscriptPlayer } from './components/TranscriptPlayer.svelt
|
|
|
33
33
|
// optional so a bare mp4 still gets the full playback UX. TranscriptPlayer
|
|
34
34
|
// stays exported unchanged — meetings already depends on it.
|
|
35
35
|
export { default as VideoPlayer } from './components/VideoPlayer.svelte';
|
|
36
|
+
// ListenControl (jatui-eev2n) — "listen to this page" playback over the host's
|
|
37
|
+
// per-sentence audio source. The component re-exports its types too; these
|
|
38
|
+
// are the same names, so either import site works.
|
|
39
|
+
export { default as ListenControl } from './components/ListenControl.svelte';
|
|
40
|
+
export { LISTEN_KEYS } from './listen/keys';
|
|
41
|
+
export { LISTEN_SPEEDS, SPEED_FLOOR, SPEED_CEIL, clampSpeed, stepSpeed, formatListenSpeed } from './listen/prefs';
|
|
36
42
|
// Rate composer (mos-5lzns.32): the same name → rate table the player applies
|
|
37
43
|
// under `speakerPace` (manual beats measured), for a host drawing its own
|
|
38
44
|
// per-speaker speed UI.
|
|
@@ -198,7 +204,7 @@ export { default as PipelineColumn } from './components/pipeline/PipelineColumn.
|
|
|
198
204
|
export { default as PipelineCard } from './components/pipeline/PipelineCard.svelte';
|
|
199
205
|
export { DEFAULT_DRAG_CONFIG, DEFAULT_PIPELINE_CONFIG } from './types/pipeline';
|
|
200
206
|
// Utilities — mention parsing
|
|
201
|
-
export { parseMentions, renderMentionsAsHTML, createMentionString, extractPlainText, findMentionAtCursor, findPartialMention, replaceMentionInContent, escapeHTML, countMentions, setMentionUrlGenerator } from './utils/mentionParser';
|
|
207
|
+
export { parseMentions, renderMentionsAsHTML, createMentionString, extractPlainText, findMentionAtCursor, findPartialMention, replaceMentionInContent, escapeHTML, countMentions, hasMentions, setMentionUrlGenerator, registerMentionType, extractAgentMentions, getUniqueMentionsByType, getMentionedUserIds, AGENT_MENTION_TYPE } from './utils/mentionParser';
|
|
202
208
|
// Components — Comments (universal threaded comments)
|
|
203
209
|
export { default as CommentThread } from './components/CommentThread.svelte';
|
|
204
210
|
export { COMMENT_ENTITY_TYPES, COMMENT_VISIBILITIES } from './types/comments';
|
|
@@ -214,6 +220,7 @@ export { default as AnnotationPanel } from './components/AnnotationPanel.svelte'
|
|
|
214
220
|
export { MARKUP_COLORS, DEFAULT_STROKE_WIDTH, renderShape, renderAllShapes, nextShapeId, } from './components/markup';
|
|
215
221
|
// Components — Keyboard Navigation
|
|
216
222
|
export { default as KeyboardShortcutsOverlay } from './components/KeyboardShortcutsOverlay.svelte';
|
|
223
|
+
export { keyboardShortcutHub, KEYBOARD_SHORTCUT_HUB_ID, } from './utils/keyboardShortcutHub';
|
|
217
224
|
export { createShortcutRegistry, getShortcutRegistry, formatShortcut, parseCombo as parseShortcutCombo, isTypingTarget, detectPlatform as detectShortcutPlatform, SIDEBAR_TOGGLE_KEY, normalizeCombo, comboFromEvent, validateCombo, localStorageBindingStore, BROWSER_RESERVED_COMBOS, } from './utils/keyboardShortcutRegistry';
|
|
218
225
|
// Components — Google Maps (DispatchMap fork + location primitives)
|
|
219
226
|
export { default as MapView } from './components/MapView.svelte';
|
|
@@ -237,7 +244,7 @@ export { attention } from './actions/attention';
|
|
|
237
244
|
// Scroll-reveal (jatui-m4uz3.3, chimaro port): IntersectionObserver
|
|
238
245
|
// scroll-reveal whose variants live in styles/motion.css as jatui-reveal-*
|
|
239
246
|
// classes on the --motion-* tokens — no shipped animista.css.
|
|
240
|
-
export { reveal, REVEAL_HIDDEN_CLASS, REVEAL_BASE_CLASS } from './actions/reveal';
|
|
247
|
+
export { reveal, REVEAL_HIDDEN_CLASS, REVEAL_BASE_CLASS, REVEAL_OFF_SELECTOR, REVEAL_MAX_DELAY_SECONDS } from './actions/reveal';
|
|
241
248
|
// Thumbnail grid (jatui-m4uz3.3, chimaro port): generic {id,url,alt} strip
|
|
242
249
|
// with overflow counter, remove/add affordances and a cell snippet override.
|
|
243
250
|
export { default as ThumbnailGrid } from './components/ThumbnailGrid.svelte';
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ListenControl keyboard map (jatui-eev2n) — the same single-source discipline
|
|
3
|
+
* as `video/keys.ts`: a key that exists is in the map, a key in the map
|
|
4
|
+
* exists. The component's keydown handler and any host help surface both read
|
|
5
|
+
* LISTEN_KEYS.
|
|
6
|
+
*
|
|
7
|
+
* Map parity: Space/K play-pause, ←/→ prev/next SENTENCE (a reading player's
|
|
8
|
+
* primary navigation — the segments ARE the sentences), Shift+←/→ the ±15s
|
|
9
|
+
* skip, [ / ] speed down/up, Escape close. Deliberately NOT bound globally:
|
|
10
|
+
* the map fires only while focus is inside the player (a library component
|
|
11
|
+
* must not hijack the host page's keys — hosts wanting global keys wire the
|
|
12
|
+
* exported `ListenApi` themselves).
|
|
13
|
+
*/
|
|
14
|
+
export interface ListenKeyRow {
|
|
15
|
+
/** Keycap label — `/`-separated alternates, as shown in a help table. */
|
|
16
|
+
keys: string;
|
|
17
|
+
description: string;
|
|
18
|
+
}
|
|
19
|
+
export declare const LISTEN_KEYS: ListenKeyRow[];
|
|
20
|
+
export type ListenKeyAction = 'toggle' | 'prev' | 'next' | 'back' | 'forward' | 'slower' | 'faster' | 'close';
|
|
21
|
+
/**
|
|
22
|
+
* One keydown → one action (or null). Pure: takes the two KeyboardEvent
|
|
23
|
+
* fields the map needs, so a synthetic event in node tests resolves the same
|
|
24
|
+
* way a real one does.
|
|
25
|
+
*/
|
|
26
|
+
export declare function resolveListenKey(event: {
|
|
27
|
+
key: string;
|
|
28
|
+
shiftKey?: boolean;
|
|
29
|
+
}): ListenKeyAction | null;
|
|
30
|
+
/**
|
|
31
|
+
* Targets whose keys already mean something native — a focused button gets
|
|
32
|
+
* Space as its activation key, the progress `<input type=range>` owns the
|
|
33
|
+
* arrows, a `<select>` owns typeahead. The component skips the map for these
|
|
34
|
+
* (Escape excepted — closing the player outranks every widget).
|
|
35
|
+
*/
|
|
36
|
+
export declare function isNativeControlTarget(target: EventTarget | null): boolean;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ListenControl keyboard map (jatui-eev2n) — the same single-source discipline
|
|
3
|
+
* as `video/keys.ts`: a key that exists is in the map, a key in the map
|
|
4
|
+
* exists. The component's keydown handler and any host help surface both read
|
|
5
|
+
* LISTEN_KEYS.
|
|
6
|
+
*
|
|
7
|
+
* Map parity: Space/K play-pause, ←/→ prev/next SENTENCE (a reading player's
|
|
8
|
+
* primary navigation — the segments ARE the sentences), Shift+←/→ the ±15s
|
|
9
|
+
* skip, [ / ] speed down/up, Escape close. Deliberately NOT bound globally:
|
|
10
|
+
* the map fires only while focus is inside the player (a library component
|
|
11
|
+
* must not hijack the host page's keys — hosts wanting global keys wire the
|
|
12
|
+
* exported `ListenApi` themselves).
|
|
13
|
+
*/
|
|
14
|
+
import { isTypingTarget } from '../utils/keyboardShortcutRegistry';
|
|
15
|
+
export const LISTEN_KEYS = [
|
|
16
|
+
{ keys: 'Space / K', description: 'Play / pause' },
|
|
17
|
+
{ keys: '← / →', description: 'Previous / next sentence' },
|
|
18
|
+
{ keys: 'Shift + ← / →', description: 'Skip ∓15s' },
|
|
19
|
+
{ keys: '[ / ]', description: 'Speed down / up (0.75–2×)' },
|
|
20
|
+
{ keys: 'Escape', description: 'Close the player' }
|
|
21
|
+
];
|
|
22
|
+
/**
|
|
23
|
+
* One keydown → one action (or null). Pure: takes the two KeyboardEvent
|
|
24
|
+
* fields the map needs, so a synthetic event in node tests resolves the same
|
|
25
|
+
* way a real one does.
|
|
26
|
+
*/
|
|
27
|
+
export function resolveListenKey(event) {
|
|
28
|
+
const key = typeof event?.key === 'string' ? event.key : '';
|
|
29
|
+
const shift = event?.shiftKey === true;
|
|
30
|
+
switch (key) {
|
|
31
|
+
case ' ':
|
|
32
|
+
case 'Spacebar': // legacy IE/Edge spelling
|
|
33
|
+
return shift ? null : 'toggle';
|
|
34
|
+
case 'k':
|
|
35
|
+
case 'K':
|
|
36
|
+
return 'toggle';
|
|
37
|
+
case 'ArrowLeft':
|
|
38
|
+
return shift ? 'back' : 'prev';
|
|
39
|
+
case 'ArrowRight':
|
|
40
|
+
return shift ? 'forward' : 'next';
|
|
41
|
+
case '[':
|
|
42
|
+
return 'slower';
|
|
43
|
+
case ']':
|
|
44
|
+
return 'faster';
|
|
45
|
+
case 'Escape':
|
|
46
|
+
return 'close';
|
|
47
|
+
default:
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Targets whose keys already mean something native — a focused button gets
|
|
53
|
+
* Space as its activation key, the progress `<input type=range>` owns the
|
|
54
|
+
* arrows, a `<select>` owns typeahead. The component skips the map for these
|
|
55
|
+
* (Escape excepted — closing the player outranks every widget).
|
|
56
|
+
*/
|
|
57
|
+
export function isNativeControlTarget(target) {
|
|
58
|
+
if (isTypingTarget(target))
|
|
59
|
+
return true;
|
|
60
|
+
// Duck-typed (not `instanceof`) so node tests without DOM globals work.
|
|
61
|
+
// `<input type=range>` (the progress bar) is not a typing target but still
|
|
62
|
+
// owns its own arrow keys natively — as does any other input.
|
|
63
|
+
const el = target;
|
|
64
|
+
if (typeof el?.tagName !== 'string')
|
|
65
|
+
return false;
|
|
66
|
+
const tag = el.tagName.toUpperCase();
|
|
67
|
+
return tag === 'BUTTON' || tag === 'INPUT';
|
|
68
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ListenControl Media Session glue (jatui-eev2n) — lock-screen / headset /
|
|
3
|
+
* OS-media controls for a page whose audio is a SEQUENCE of per-sentence
|
|
4
|
+
* files. The browser only ever sees one session; this module keeps it fed:
|
|
5
|
+
*
|
|
6
|
+
* · metadata — the document title + the current voice (artist);
|
|
7
|
+
* · action handlers — play/pause, previous/next track (sentence),
|
|
8
|
+
* seekbackward/seekforward (the ±15s skip), seekto;
|
|
9
|
+
* · position state — ONE document-wide position, throttled by the component
|
|
10
|
+
* (`navigator.mediaSession.setPositionState` throws on an invalid
|
|
11
|
+
* duration/rate combination, so every write is guarded).
|
|
12
|
+
*
|
|
13
|
+
* Feature-detected end to end: everywhere the API is missing (every non-secure
|
|
14
|
+
* context, older Safari, jsdom, node) these are silent no-ops. Pure enough to
|
|
15
|
+
* drive from a fake session in node tests — the DOM `MediaSession` type is
|
|
16
|
+
* deliberately mirrored structurally, not imported.
|
|
17
|
+
*/
|
|
18
|
+
/** The slice of `navigator.mediaSession` ListenControl uses. */
|
|
19
|
+
export interface MediaSessionLike {
|
|
20
|
+
metadata: {
|
|
21
|
+
title?: string;
|
|
22
|
+
artist?: string;
|
|
23
|
+
album?: string;
|
|
24
|
+
artwork?: {
|
|
25
|
+
src: string;
|
|
26
|
+
sizes?: string;
|
|
27
|
+
type?: string;
|
|
28
|
+
}[];
|
|
29
|
+
} | null;
|
|
30
|
+
setActionHandler(action: string, handler: ((details: {
|
|
31
|
+
seekOffset?: number;
|
|
32
|
+
seekTime?: number;
|
|
33
|
+
fastSeek?: boolean;
|
|
34
|
+
}) => void) | null): void;
|
|
35
|
+
setPositionState(state: {
|
|
36
|
+
duration: number;
|
|
37
|
+
playbackRate?: number;
|
|
38
|
+
position?: number;
|
|
39
|
+
}): void;
|
|
40
|
+
}
|
|
41
|
+
/** Navigator-like lookup — injectable so node tests can pass a fake. */
|
|
42
|
+
export type NavigatorLike = {
|
|
43
|
+
mediaSession?: MediaSessionLike;
|
|
44
|
+
} | null | undefined;
|
|
45
|
+
/** The session, or null where the Media Session API doesn't exist. */
|
|
46
|
+
export declare function getMediaSession(nav?: NavigatorLike): MediaSessionLike | null;
|
|
47
|
+
export interface ListenMediaMeta {
|
|
48
|
+
title: string;
|
|
49
|
+
artist?: string;
|
|
50
|
+
album?: string;
|
|
51
|
+
/** Optional cover art URL(s) — host-supplied. */
|
|
52
|
+
artwork?: string[];
|
|
53
|
+
}
|
|
54
|
+
/** Best-effort metadata write. No-op without a session; never throws. */
|
|
55
|
+
export declare function setListenMetadata(session: MediaSessionLike | null, meta: ListenMediaMeta): void;
|
|
56
|
+
/**
|
|
57
|
+
* Wire the transport actions. Each handler is registered on its own try/catch:
|
|
58
|
+
* browsers reject actions they don't implement individually, and one refusal
|
|
59
|
+
* must not cost the rest. A null handler value clears that action.
|
|
60
|
+
*/
|
|
61
|
+
export declare function setListenActions(session: MediaSessionLike | null, handlers: {
|
|
62
|
+
play?: (() => void) | null;
|
|
63
|
+
pause?: (() => void) | null;
|
|
64
|
+
previoustrack?: (() => void) | null;
|
|
65
|
+
nexttrack?: (() => void) | null;
|
|
66
|
+
seekbackward?: ((details: {
|
|
67
|
+
seekOffset?: number;
|
|
68
|
+
}) => void) | null;
|
|
69
|
+
seekforward?: ((details: {
|
|
70
|
+
seekOffset?: number;
|
|
71
|
+
}) => void) | null;
|
|
72
|
+
seekto?: ((details: {
|
|
73
|
+
seekTime?: number;
|
|
74
|
+
fastSeek?: boolean;
|
|
75
|
+
}) => void) | null;
|
|
76
|
+
stop?: (() => void) | null;
|
|
77
|
+
}): void;
|
|
78
|
+
/** Hand the lock-screen back: null metadata, null handlers. */
|
|
79
|
+
export declare function clearListenSession(session: MediaSessionLike | null): void;
|
|
80
|
+
/**
|
|
81
|
+
* One document-wide position write. Guards the two ways this throws: no
|
|
82
|
+
* session, and an invalid state (negative/NaN duration, position outside it,
|
|
83
|
+
* a non-positive rate) — `setPositionState` rejects those with an exception,
|
|
84
|
+
* which must never surface from a timeupdate.
|
|
85
|
+
*/
|
|
86
|
+
export declare function updateListenPosition(session: MediaSessionLike | null, state: {
|
|
87
|
+
duration: number;
|
|
88
|
+
position: number;
|
|
89
|
+
playbackRate: number;
|
|
90
|
+
}): void;
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ListenControl Media Session glue (jatui-eev2n) — lock-screen / headset /
|
|
3
|
+
* OS-media controls for a page whose audio is a SEQUENCE of per-sentence
|
|
4
|
+
* files. The browser only ever sees one session; this module keeps it fed:
|
|
5
|
+
*
|
|
6
|
+
* · metadata — the document title + the current voice (artist);
|
|
7
|
+
* · action handlers — play/pause, previous/next track (sentence),
|
|
8
|
+
* seekbackward/seekforward (the ±15s skip), seekto;
|
|
9
|
+
* · position state — ONE document-wide position, throttled by the component
|
|
10
|
+
* (`navigator.mediaSession.setPositionState` throws on an invalid
|
|
11
|
+
* duration/rate combination, so every write is guarded).
|
|
12
|
+
*
|
|
13
|
+
* Feature-detected end to end: everywhere the API is missing (every non-secure
|
|
14
|
+
* context, older Safari, jsdom, node) these are silent no-ops. Pure enough to
|
|
15
|
+
* drive from a fake session in node tests — the DOM `MediaSession` type is
|
|
16
|
+
* deliberately mirrored structurally, not imported.
|
|
17
|
+
*/
|
|
18
|
+
/** The session, or null where the Media Session API doesn't exist. */
|
|
19
|
+
export function getMediaSession(nav = typeof navigator === 'undefined' ? null : navigator) {
|
|
20
|
+
const session = nav?.mediaSession;
|
|
21
|
+
if (!session || typeof session.setActionHandler !== 'function')
|
|
22
|
+
return null;
|
|
23
|
+
return session;
|
|
24
|
+
}
|
|
25
|
+
/** Best-effort metadata write. No-op without a session; never throws. */
|
|
26
|
+
export function setListenMetadata(session, meta) {
|
|
27
|
+
if (!session)
|
|
28
|
+
return;
|
|
29
|
+
try {
|
|
30
|
+
const plain = {
|
|
31
|
+
title: meta.title,
|
|
32
|
+
artist: meta.artist ?? '',
|
|
33
|
+
album: meta.album ?? '',
|
|
34
|
+
...(meta.artwork && meta.artwork.length
|
|
35
|
+
? { artwork: meta.artwork.map((src) => ({ src, sizes: '512x512' })) }
|
|
36
|
+
: {})
|
|
37
|
+
};
|
|
38
|
+
// `MediaMetadata` is browser-global; node/fake sessions get the plain shape.
|
|
39
|
+
const Ctor = typeof MediaMetadata === 'undefined' ? null : MediaMetadata;
|
|
40
|
+
session.metadata = Ctor ? new Ctor(plain) : plain;
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
/* hostile metadata — keep playing without lock-screen info */
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Wire the transport actions. Each handler is registered on its own try/catch:
|
|
48
|
+
* browsers reject actions they don't implement individually, and one refusal
|
|
49
|
+
* must not cost the rest. A null handler value clears that action.
|
|
50
|
+
*/
|
|
51
|
+
export function setListenActions(session, handlers) {
|
|
52
|
+
if (!session)
|
|
53
|
+
return;
|
|
54
|
+
for (const [action, handler] of Object.entries(handlers)) {
|
|
55
|
+
if (handler === undefined)
|
|
56
|
+
continue;
|
|
57
|
+
try {
|
|
58
|
+
session.setActionHandler(action, handler);
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
/* this browser doesn't implement that action */
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/** Hand the lock-screen back: null metadata, null handlers. */
|
|
66
|
+
export function clearListenSession(session) {
|
|
67
|
+
if (!session)
|
|
68
|
+
return;
|
|
69
|
+
try {
|
|
70
|
+
session.metadata = null;
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
/* ignore */
|
|
74
|
+
}
|
|
75
|
+
setListenActions(session, {
|
|
76
|
+
play: null,
|
|
77
|
+
pause: null,
|
|
78
|
+
previoustrack: null,
|
|
79
|
+
nexttrack: null,
|
|
80
|
+
seekbackward: null,
|
|
81
|
+
seekforward: null,
|
|
82
|
+
seekto: null,
|
|
83
|
+
stop: null
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* One document-wide position write. Guards the two ways this throws: no
|
|
88
|
+
* session, and an invalid state (negative/NaN duration, position outside it,
|
|
89
|
+
* a non-positive rate) — `setPositionState` rejects those with an exception,
|
|
90
|
+
* which must never surface from a timeupdate.
|
|
91
|
+
*/
|
|
92
|
+
export function updateListenPosition(session, state) {
|
|
93
|
+
if (!session)
|
|
94
|
+
return;
|
|
95
|
+
const { duration, position, playbackRate } = state;
|
|
96
|
+
if (!Number.isFinite(duration) || duration <= 0)
|
|
97
|
+
return;
|
|
98
|
+
if (!Number.isFinite(position) || position < 0 || position > duration)
|
|
99
|
+
return;
|
|
100
|
+
if (!Number.isFinite(playbackRate) || playbackRate <= 0)
|
|
101
|
+
return;
|
|
102
|
+
try {
|
|
103
|
+
session.setPositionState({ duration, position, playbackRate });
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
/* raced with a source change — the next timeupdate rewrites it */
|
|
107
|
+
}
|
|
108
|
+
}
|