@joewinke/jatui 0.35.0 → 0.36.2
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 -4
- 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 +455 -152
- package/dist/components/ListenControl.svelte.d.ts +12 -3
- package/dist/components/ThumbnailGrid.svelte +34 -7
- package/dist/components/ThumbnailGrid.svelte.d.ts +17 -1
- package/dist/index.d.ts +5 -3
- package/dist/index.js +3 -2
- 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
|
@@ -2,9 +2,10 @@ export type { ListenSegment, ListenChapter, ListenVoice, ListenSegmentPage, List
|
|
|
2
2
|
/**
|
|
3
3
|
* ListenControl — "listen to this page" as one compact control.
|
|
4
4
|
*
|
|
5
|
-
* A small pill that expands into a playback bar: play/pause,
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* A small pill that expands into a two-row playback bar: play/pause, the
|
|
6
|
+
* current sentence, one speed (0.75–2×) + voice menu, close; then
|
|
7
|
+
* previous/next sentence, −15s/+15s, a document-wide progress bar with
|
|
8
|
+
* chapter/section ticks, and time. Built for hosts
|
|
8
9
|
* that synthesize audio per sentence (TTS): the host supplies an async
|
|
9
10
|
* SEGMENT SOURCE — sentences/paragraphs with ids → audio URLs — and the
|
|
10
11
|
* player starts on the first chunk while the rest synthesizes, hatching
|
|
@@ -22,6 +23,14 @@ export type { ListenSegment, ListenChapter, ListenVoice, ListenSegmentPage, List
|
|
|
22
23
|
* reports them) lay the timeline out immediately; unknown ones are
|
|
23
24
|
* measured on load and ESTIMATED until then, self-correcting as they land.
|
|
24
25
|
*
|
|
26
|
+
* ── Layout: two rows (jatui-zl9dp) ──────────────────────────────────────
|
|
27
|
+
* Row 1 is what you're hearing: play/pause, the chapter chip + current
|
|
28
|
+
* sentence (it gets ALL the remaining width), ONE settings pill folding
|
|
29
|
+
* speed + voice into a small menu, close. Row 2 is where you are: prev
|
|
30
|
+
* sentence, −15s, +15s, next sentence, then the progress bar and the
|
|
31
|
+
* clock. The bar is `box-sizing: border-box` + `max-width: 100%` with
|
|
32
|
+
* `min-width: 0` down the flex chain, so it never overflows its host.
|
|
33
|
+
*
|
|
25
34
|
* The transport row borrows VideoPlayer's `kind="audio"` / `transport=
|
|
26
35
|
* "band"` shape (round primary play, ghost transport buttons, tabular
|
|
27
36
|
* clock) and reuses its pure time formatting (`video/time.ts`); the
|
|
@@ -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. */
|
package/dist/index.d.ts
CHANGED
|
@@ -93,7 +93,7 @@ export { default as ResizablePanel } from './components/ResizablePanel.svelte';
|
|
|
93
93
|
export { default as VoicePlayer } from './components/VoicePlayer.svelte';
|
|
94
94
|
export { default as SpeechForm } from './components/SpeechForm.svelte';
|
|
95
95
|
export { default as AudioRecorder } from './components/AudioRecorder.svelte';
|
|
96
|
-
export type { AudioRecordingResult } from './utils/audioRecorderCore';
|
|
96
|
+
export type { AudioRecordingResult, AudioRecorderChunk } from './utils/audioRecorderCore';
|
|
97
97
|
export { createAudioRecorder } from './utils/audioRecorder';
|
|
98
98
|
export type { AudioRecorder, AudioRecorderCore, AudioRecorderError, AudioRecorderErrorKind, AudioRecorderFrame, AudioRecorderMode, AudioRecorderOptions, AudioRecorderPhase, AudioRecorderSnapshot } from './utils/audioRecorderCore';
|
|
99
99
|
export { default as ThemeSelector } from './components/ThemeSelector.svelte';
|
|
@@ -172,7 +172,8 @@ export { default as PipelineColumn } from './components/pipeline/PipelineColumn.
|
|
|
172
172
|
export { default as PipelineCard } from './components/pipeline/PipelineCard.svelte';
|
|
173
173
|
export type { PipelineStage, StageColors, PipelineItem, PipelineMetric, StageMetrics, PipelineViewMode, PipelineTableColumn, PipelineDragConfig, PipelineConfig, PipelineStageChangeEvent, PipelineItemClickEvent } from './types/pipeline';
|
|
174
174
|
export { DEFAULT_DRAG_CONFIG, DEFAULT_PIPELINE_CONFIG } from './types/pipeline';
|
|
175
|
-
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';
|
|
176
177
|
export { default as CommentThread } from './components/CommentThread.svelte';
|
|
177
178
|
export type { Comment, CommentEntityType, CommentVisibility, CommentCreateInput, CommentCallbacks } from './types/comments';
|
|
178
179
|
export { COMMENT_ENTITY_TYPES, COMMENT_VISIBILITIES } from './types/comments';
|
|
@@ -187,6 +188,7 @@ export type { MarkupTool, Point, MarkupShape, ArrowShape, RectangleShape, Ellips
|
|
|
187
188
|
export type { Annotation, AnnotationProfile, AnnotationThread, AnnotationCallbacks, } from './types/annotation';
|
|
188
189
|
export { default as KeyboardShortcutsOverlay } from './components/KeyboardShortcutsOverlay.svelte';
|
|
189
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';
|
|
190
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';
|
|
191
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';
|
|
192
194
|
export { default as MapView } from './components/MapView.svelte';
|
|
@@ -200,7 +202,7 @@ export { staggerEntrance } from './actions/staggerEntrance';
|
|
|
200
202
|
export type { StaggerEntranceOptions } from './actions/staggerEntrance';
|
|
201
203
|
export { attention } from './actions/attention';
|
|
202
204
|
export type { AttentionEffect, AttentionOptions } from './actions/attention';
|
|
203
|
-
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';
|
|
204
206
|
export type { RevealOptions, RevealVariant } from './actions/reveal';
|
|
205
207
|
export { default as ThumbnailGrid } from './components/ThumbnailGrid.svelte';
|
|
206
208
|
export type { ThumbnailItem } from './components/ThumbnailGrid.svelte';
|
package/dist/index.js
CHANGED
|
@@ -204,7 +204,7 @@ export { default as PipelineColumn } from './components/pipeline/PipelineColumn.
|
|
|
204
204
|
export { default as PipelineCard } from './components/pipeline/PipelineCard.svelte';
|
|
205
205
|
export { DEFAULT_DRAG_CONFIG, DEFAULT_PIPELINE_CONFIG } from './types/pipeline';
|
|
206
206
|
// Utilities — mention parsing
|
|
207
|
-
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';
|
|
208
208
|
// Components — Comments (universal threaded comments)
|
|
209
209
|
export { default as CommentThread } from './components/CommentThread.svelte';
|
|
210
210
|
export { COMMENT_ENTITY_TYPES, COMMENT_VISIBILITIES } from './types/comments';
|
|
@@ -220,6 +220,7 @@ export { default as AnnotationPanel } from './components/AnnotationPanel.svelte'
|
|
|
220
220
|
export { MARKUP_COLORS, DEFAULT_STROKE_WIDTH, renderShape, renderAllShapes, nextShapeId, } from './components/markup';
|
|
221
221
|
// Components — Keyboard Navigation
|
|
222
222
|
export { default as KeyboardShortcutsOverlay } from './components/KeyboardShortcutsOverlay.svelte';
|
|
223
|
+
export { keyboardShortcutHub, KEYBOARD_SHORTCUT_HUB_ID, } from './utils/keyboardShortcutHub';
|
|
223
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';
|
|
224
225
|
// Components — Google Maps (DispatchMap fork + location primitives)
|
|
225
226
|
export { default as MapView } from './components/MapView.svelte';
|
|
@@ -243,7 +244,7 @@ export { attention } from './actions/attention';
|
|
|
243
244
|
// Scroll-reveal (jatui-m4uz3.3, chimaro port): IntersectionObserver
|
|
244
245
|
// scroll-reveal whose variants live in styles/motion.css as jatui-reveal-*
|
|
245
246
|
// classes on the --motion-* tokens — no shipped animista.css.
|
|
246
|
-
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';
|
|
247
248
|
// Thumbnail grid (jatui-m4uz3.3, chimaro port): generic {id,url,alt} strip
|
|
248
249
|
// with overflow counter, remove/add affordances and a cell snippet override.
|
|
249
250
|
export { default as ThumbnailGrid } from './components/ThumbnailGrid.svelte';
|
package/dist/motion/index.d.ts
CHANGED
|
@@ -13,5 +13,5 @@ export { createMotionLevelStore } from './store';
|
|
|
13
13
|
export type { MotionState, MotionLevelStore, MotionLevelStoreOptions } from './store';
|
|
14
14
|
export { glide } from '../transitions/glide';
|
|
15
15
|
export type { GlideOrigin, GlideParams } from '../transitions/glide';
|
|
16
|
-
export { reveal, REVEAL_HIDDEN_CLASS, REVEAL_BASE_CLASS } from '../actions/reveal';
|
|
16
|
+
export { reveal, REVEAL_HIDDEN_CLASS, REVEAL_BASE_CLASS, REVEAL_OFF_SELECTOR, REVEAL_MAX_DELAY_SECONDS } from '../actions/reveal';
|
|
17
17
|
export type { RevealOptions, RevealVariant } from '../actions/reveal';
|
package/dist/motion/index.js
CHANGED
|
@@ -8,4 +8,4 @@ export { cubicBezier, motionEasing } from './easing';
|
|
|
8
8
|
export { motionFade, motionScale, motionSlide, resolveMotionTiming } from './transitions';
|
|
9
9
|
export { createMotionLevelStore } from './store';
|
|
10
10
|
export { glide } from '../transitions/glide';
|
|
11
|
-
export { reveal, REVEAL_HIDDEN_CLASS, REVEAL_BASE_CLASS } from '../actions/reveal';
|
|
11
|
+
export { reveal, REVEAL_HIDDEN_CLASS, REVEAL_BASE_CLASS, REVEAL_OFF_SELECTOR, REVEAL_MAX_DELAY_SECONDS } from '../actions/reveal';
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { AudioRecorderMode, AudioRecorderOptions, AudioRecorderPhase, AudioRecorderSnapshot } from './audioRecorderCore';
|
|
2
|
-
export type { AudioRecorderError, AudioRecorderErrorKind, AudioRecorderFrame, AudioRecorderMode, AudioRecorderOptions, AudioRecorderPhase, AudioRecordingResult } from './audioRecorderCore';
|
|
2
|
+
export type { AudioRecorderChunk, AudioRecorderError, AudioRecorderErrorKind, AudioRecorderFrame, AudioRecorderMode, AudioRecorderOptions, AudioRecorderPhase, AudioRecordingResult } from './audioRecorderCore';
|
|
3
3
|
export interface AudioRecorder {
|
|
4
4
|
/** False when the platform has no getUserMedia/MediaRecorder (SSR, Node). */
|
|
5
5
|
readonly supported: boolean;
|
|
@@ -15,6 +15,11 @@ export interface AudioRecorder {
|
|
|
15
15
|
readonly elapsedMs: number;
|
|
16
16
|
/** Last error, or null. */
|
|
17
17
|
readonly error: AudioRecorderSnapshot['error'];
|
|
18
|
+
/** Live mic MediaStream while the recorder holds it (starting/recording),
|
|
19
|
+
* else null (jatui-ft2ot). Reactive — read it in templates/effects. For
|
|
20
|
+
* hosts that attach the raw stream (WebRTC, a custom MediaRecorder with
|
|
21
|
+
* requestData()-driven streaming). */
|
|
22
|
+
readonly stream: MediaStream | null;
|
|
18
23
|
start(): Promise<void>;
|
|
19
24
|
stop(): void;
|
|
20
25
|
pause(): void;
|
|
@@ -13,6 +13,7 @@ export function createAudioRecorder(options = {}) {
|
|
|
13
13
|
snap.sizeBytes = s.sizeBytes;
|
|
14
14
|
snap.elapsedMs = s.elapsedMs;
|
|
15
15
|
snap.error = s.error;
|
|
16
|
+
snap.stream = s.stream;
|
|
16
17
|
});
|
|
17
18
|
|
|
18
19
|
return {
|
|
@@ -47,6 +48,10 @@ export function createAudioRecorder(options = {}) {
|
|
|
47
48
|
get error() {
|
|
48
49
|
return snap.error;
|
|
49
50
|
},
|
|
51
|
+
|
|
52
|
+
get stream() {
|
|
53
|
+
return snap.stream;
|
|
54
|
+
},
|
|
50
55
|
start: () => core.start(),
|
|
51
56
|
stop: () => core.stop(),
|
|
52
57
|
pause: () => core.pause(),
|
|
@@ -57,11 +57,32 @@ export interface AudioRecorderFrame {
|
|
|
57
57
|
/** Time-domain bytes (from getByteTimeDomainData) — raw PCM-ish shape. */
|
|
58
58
|
time: Uint8Array;
|
|
59
59
|
}
|
|
60
|
+
/** One MediaRecorder timeslice of audio, handed to `onChunk` as it arrives
|
|
61
|
+
* (jatui-ft2ot). The take keeps accumulating for `onStop` regardless — hosts
|
|
62
|
+
* that buffer chunks (restartable dictation, live transcription) consume
|
|
63
|
+
* both streams independently. */
|
|
64
|
+
export interface AudioRecorderChunk {
|
|
65
|
+
/** The chunk blob — one MediaRecorder `dataavailable` payload. */
|
|
66
|
+
blob: Blob;
|
|
67
|
+
/** MIME type of the recording (same value `onStop` reports). */
|
|
68
|
+
mimeType: string;
|
|
69
|
+
/** Cumulative bytes captured so far in this take (all chunks, not just this one). */
|
|
70
|
+
sizeBytes: number;
|
|
71
|
+
/** Pause-adjusted elapsed ms at the moment this chunk arrived. */
|
|
72
|
+
elapsedMs: number;
|
|
73
|
+
/** 0-based index of this chunk within the take. Resets on a new take. */
|
|
74
|
+
index: number;
|
|
75
|
+
}
|
|
60
76
|
export interface AudioRecorderOptions {
|
|
61
77
|
/** `'toggle'` (default) or `'push'` (hold-to-talk — see module header). */
|
|
62
78
|
mode?: AudioRecorderMode;
|
|
63
79
|
/** Auto-stop ceiling in ms. 0 (default) records until stopped manually. */
|
|
64
80
|
maxDurationMs?: number;
|
|
81
|
+
/** Timeslice in ms passed to `MediaRecorder.start()` — chunks are emitted
|
|
82
|
+
* at least this often and delivered to `onChunk` (jatui-ft2ot). Default
|
|
83
|
+
* 1000 (the historical behavior). 0 records as a single blob until stop
|
|
84
|
+
* (no intermediate chunking). */
|
|
85
|
+
timesliceMs?: number;
|
|
65
86
|
/** Fired once the mic stream is live and recording has started. */
|
|
66
87
|
onStart?: () => void;
|
|
67
88
|
onPause?: () => void;
|
|
@@ -77,6 +98,12 @@ export interface AudioRecorderOptions {
|
|
|
77
98
|
/** Fired every animation frame while recording — analyser data for hosts
|
|
78
99
|
* that draw their own waveform/level UI. Not fired while paused. */
|
|
79
100
|
onFrame?: (frame: AudioRecorderFrame) => void;
|
|
101
|
+
/** Fired per MediaRecorder chunk as it arrives (every `timesliceMs`, plus
|
|
102
|
+
* a final shorter chunk at stop). The take still accumulates for `onStop` —
|
|
103
|
+
* hosts get the full take AND a live chunk stream (restartable dictation,
|
|
104
|
+
* streaming transcription). A throwing handler reports `handler-failed`
|
|
105
|
+
* but does NOT stop the recording. */
|
|
106
|
+
onChunk?: (chunk: AudioRecorderChunk) => void;
|
|
80
107
|
}
|
|
81
108
|
/** Injectable platform bindings — defaults resolve from globalThis lazily at factory time, so Node (no MediaRecorder) degrades to `supported: false` instead of throwing at import. */
|
|
82
109
|
export interface AudioRecorderDeps {
|
|
@@ -104,11 +131,18 @@ export interface AudioRecorderSnapshot {
|
|
|
104
131
|
elapsedMs: number;
|
|
105
132
|
/** Last error, or null. */
|
|
106
133
|
error: AudioRecorderError | null;
|
|
134
|
+
/** Live mic MediaStream while the recorder holds it (starting/recording),
|
|
135
|
+
* else null (jatui-ft2ot). For hosts that attach the raw stream — WebRTC,
|
|
136
|
+
* a custom MediaRecorder with requestData()-driven streaming. */
|
|
137
|
+
stream: MediaStream | null;
|
|
107
138
|
}
|
|
108
139
|
export interface AudioRecorderCore {
|
|
109
140
|
/** False when the platform has no getUserMedia/MediaRecorder (SSR, Node). */
|
|
110
141
|
readonly supported: boolean;
|
|
111
142
|
readonly mode: AudioRecorderMode;
|
|
143
|
+
/** Live mic MediaStream while the recorder holds it, else null — same
|
|
144
|
+
* value as `snapshot().stream`, for hosts that read it imperatively. */
|
|
145
|
+
readonly stream: MediaStream | null;
|
|
112
146
|
/** Current reactive-independent snapshot (the rune wrapper copies this into $state). */
|
|
113
147
|
snapshot(): AudioRecorderSnapshot;
|
|
114
148
|
/** Subscribe to snapshot changes. Returns an unsubscribe fn. */
|
|
@@ -61,7 +61,7 @@ export function computeElapsedMs(startedAt, pausedTotalMs, pauseStartedAt, now)
|
|
|
61
61
|
return Math.max(0, now - startedAt - pausedMs);
|
|
62
62
|
}
|
|
63
63
|
export function createAudioRecorderCore(options = {}, deps = {}) {
|
|
64
|
-
const { mode = 'toggle', maxDurationMs = 0, onStart, onPause, onResume, onStop, onCancel, onError, onFrame } = options;
|
|
64
|
+
const { mode = 'toggle', maxDurationMs = 0, timesliceMs = 1000, onStart, onPause, onResume, onStop, onCancel, onError, onFrame, onChunk } = options;
|
|
65
65
|
// Lazy globalThis defaults — resolved per factory call, never at import time.
|
|
66
66
|
const g = (v) => v;
|
|
67
67
|
const getUserMedia = deps.getUserMedia ??
|
|
@@ -87,6 +87,8 @@ export function createAudioRecorderCore(options = {}, deps = {}) {
|
|
|
87
87
|
let level = 0;
|
|
88
88
|
let sizeBytes = 0;
|
|
89
89
|
let error = null;
|
|
90
|
+
/** 0-based index of the next chunk in the current take. */
|
|
91
|
+
let chunkIndex = 0;
|
|
90
92
|
// Recorder internals
|
|
91
93
|
let recorder = null;
|
|
92
94
|
let stream = null;
|
|
@@ -105,7 +107,15 @@ export function createAudioRecorderCore(options = {}, deps = {}) {
|
|
|
105
107
|
let pendingCancel = false;
|
|
106
108
|
const listeners = new Set();
|
|
107
109
|
function snapshot() {
|
|
108
|
-
return {
|
|
110
|
+
return {
|
|
111
|
+
phase,
|
|
112
|
+
paused,
|
|
113
|
+
level,
|
|
114
|
+
sizeBytes,
|
|
115
|
+
elapsedMs: computeElapsedMs(startedAt, pausedTotalMs, pauseStartedAt, now()),
|
|
116
|
+
error,
|
|
117
|
+
stream
|
|
118
|
+
};
|
|
109
119
|
}
|
|
110
120
|
function notify() {
|
|
111
121
|
const s = snapshot();
|
|
@@ -273,11 +283,32 @@ export function createAudioRecorderCore(options = {}, deps = {}) {
|
|
|
273
283
|
}
|
|
274
284
|
chunks = [];
|
|
275
285
|
sizeBytes = 0;
|
|
286
|
+
chunkIndex = 0;
|
|
276
287
|
recorder.ondataavailable = (e) => {
|
|
277
288
|
if (e.data && e.data.size > 0) {
|
|
278
|
-
|
|
279
|
-
|
|
289
|
+
const blob = e.data;
|
|
290
|
+
chunks.push(blob);
|
|
291
|
+
sizeBytes += blob.size;
|
|
292
|
+
const chunk = {
|
|
293
|
+
blob,
|
|
294
|
+
mimeType,
|
|
295
|
+
sizeBytes,
|
|
296
|
+
elapsedMs: elapsed(),
|
|
297
|
+
index: chunkIndex++
|
|
298
|
+
};
|
|
280
299
|
notify();
|
|
300
|
+
if (onChunk) {
|
|
301
|
+
try {
|
|
302
|
+
onChunk(chunk);
|
|
303
|
+
}
|
|
304
|
+
catch (err) {
|
|
305
|
+
// A throwing chunk handler must not lose the take — report and keep capturing.
|
|
306
|
+
fail({
|
|
307
|
+
kind: 'handler-failed',
|
|
308
|
+
message: `Chunk kept, but the onChunk handler failed: ${err instanceof Error ? err.message : String(err)}`
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
}
|
|
281
312
|
}
|
|
282
313
|
};
|
|
283
314
|
recorder.onstop = () => {
|
|
@@ -300,7 +331,7 @@ export function createAudioRecorderCore(options = {}, deps = {}) {
|
|
|
300
331
|
analyser = null;
|
|
301
332
|
}
|
|
302
333
|
}
|
|
303
|
-
recorder.start(
|
|
334
|
+
recorder.start(timesliceMs > 0 ? timesliceMs : undefined);
|
|
304
335
|
startedAt = now();
|
|
305
336
|
pausedTotalMs = 0;
|
|
306
337
|
pauseStartedAt = 0;
|
|
@@ -381,7 +412,10 @@ export function createAudioRecorderCore(options = {}, deps = {}) {
|
|
|
381
412
|
if (wasCancelled)
|
|
382
413
|
return;
|
|
383
414
|
if (chunks.length === 0) {
|
|
384
|
-
// Zero chunks (e.g. stopped before any data arrived) — fail loud, not a
|
|
415
|
+
// Zero chunks (e.g. stopped before any data arrived) — fail loud, not a
|
|
416
|
+
// silent no-op. Release the mic + audio graph like every other exit, so
|
|
417
|
+
// a failed take can't leave a hot microphone behind.
|
|
418
|
+
teardown();
|
|
385
419
|
phase = 'idle';
|
|
386
420
|
notify();
|
|
387
421
|
fail({ kind: 'too-short', message: 'Recording was too short — nothing was captured. Try again.' });
|
|
@@ -434,6 +468,9 @@ export function createAudioRecorderCore(options = {}, deps = {}) {
|
|
|
434
468
|
return supported;
|
|
435
469
|
},
|
|
436
470
|
mode,
|
|
471
|
+
get stream() {
|
|
472
|
+
return stream;
|
|
473
|
+
},
|
|
437
474
|
snapshot,
|
|
438
475
|
subscribe,
|
|
439
476
|
start,
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Keyboard shortcut hub — the "one overlay" coordination layer for
|
|
3
|
+
* KeyboardShortcutsOverlay's host/contributor mode.
|
|
4
|
+
*
|
|
5
|
+
* Page-level overlay instances usually self-render (the pre-existing
|
|
6
|
+
* non-host fallback path). But an app shell can mount ONE "global" host
|
|
7
|
+
* overlay; every other overlay on the page then becomes a contributor:
|
|
8
|
+
* instead of self-rendering it contributes its sections (under a
|
|
9
|
+
* `sectionTitle`) to the host's panel and forwards its `open` requests to
|
|
10
|
+
* the host, so exactly one `?`-overlay exists no matter how many surfaces
|
|
11
|
+
* mount the component.
|
|
12
|
+
*
|
|
13
|
+
* Pure TypeScript (no Svelte), subscriber-based like ShortcutRegistry —
|
|
14
|
+
* reactivity lives in the overlay components, which re-read on notify.
|
|
15
|
+
*
|
|
16
|
+
* Usage (app shell):
|
|
17
|
+
*
|
|
18
|
+
* <KeyboardShortcutsOverlay mode="host" /> // owns the `?` toggle
|
|
19
|
+
*
|
|
20
|
+
* Usage (page / portal / contract page):
|
|
21
|
+
*
|
|
22
|
+
* <KeyboardShortcutsOverlay
|
|
23
|
+
* {sections}
|
|
24
|
+
* sectionTitle="Inbox"
|
|
25
|
+
* showHint={false}
|
|
26
|
+
* />
|
|
27
|
+
* // self-renders when no host exists; contributes when one does
|
|
28
|
+
*/
|
|
29
|
+
export interface ShortcutContribution {
|
|
30
|
+
/** Stable id for the contributing surface (page route, portal name…). */
|
|
31
|
+
id: string;
|
|
32
|
+
/** Label under which the contributed sections appear in the host panel. */
|
|
33
|
+
sectionTitle: string;
|
|
34
|
+
/** Contributed sections (same shape as KeyboardShortcutsOverlay's `sections`). */
|
|
35
|
+
sections: ShortcutSectionLike[];
|
|
36
|
+
/** Registry snapshot (context-keyed sections) — optional alternative to `sections`. */
|
|
37
|
+
registrySections?: ShortcutSectionLike[];
|
|
38
|
+
}
|
|
39
|
+
export interface ShortcutSectionLike {
|
|
40
|
+
title: string;
|
|
41
|
+
shortcuts: {
|
|
42
|
+
key: string;
|
|
43
|
+
description: string;
|
|
44
|
+
color?: string;
|
|
45
|
+
}[];
|
|
46
|
+
}
|
|
47
|
+
export interface KeyboardShortcutHost {
|
|
48
|
+
/** Toggle the host overlay open/closed. */
|
|
49
|
+
toggle: () => void;
|
|
50
|
+
/** Open the host overlay (idempotent while already open). */
|
|
51
|
+
open: () => void;
|
|
52
|
+
}
|
|
53
|
+
declare class KeyboardShortcutHub {
|
|
54
|
+
private host;
|
|
55
|
+
private contributions;
|
|
56
|
+
private listeners;
|
|
57
|
+
/** True when a host overlay is mounted (contributing instances defer to it). */
|
|
58
|
+
get hasHost(): boolean;
|
|
59
|
+
/** Mount the host. Returns the unmount function. */
|
|
60
|
+
registerHost(host: KeyboardShortcutHost): () => void;
|
|
61
|
+
/** Contribute sections from a page-level overlay instance. Returns unregister. */
|
|
62
|
+
contribute(contribution: ShortcutContribution): () => void;
|
|
63
|
+
/** Contributed sections in registration order, labelled by sectionTitle. */
|
|
64
|
+
getContributions(): ShortcutContribution[];
|
|
65
|
+
/** Ask the host overlay to open. No-op when there is no host. */
|
|
66
|
+
requestOpen(): boolean;
|
|
67
|
+
/** Subscribe to hub changes (host mount/unmount, contributions). */
|
|
68
|
+
subscribe(listener: () => void): () => void;
|
|
69
|
+
private notify;
|
|
70
|
+
}
|
|
71
|
+
export declare const keyboardShortcutHub: KeyboardShortcutHub;
|
|
72
|
+
export declare const KEYBOARD_SHORTCUT_HUB_ID = "keyboard-shortcut-hub";
|
|
73
|
+
export {};
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Keyboard shortcut hub — the "one overlay" coordination layer for
|
|
3
|
+
* KeyboardShortcutsOverlay's host/contributor mode.
|
|
4
|
+
*
|
|
5
|
+
* Page-level overlay instances usually self-render (the pre-existing
|
|
6
|
+
* non-host fallback path). But an app shell can mount ONE "global" host
|
|
7
|
+
* overlay; every other overlay on the page then becomes a contributor:
|
|
8
|
+
* instead of self-rendering it contributes its sections (under a
|
|
9
|
+
* `sectionTitle`) to the host's panel and forwards its `open` requests to
|
|
10
|
+
* the host, so exactly one `?`-overlay exists no matter how many surfaces
|
|
11
|
+
* mount the component.
|
|
12
|
+
*
|
|
13
|
+
* Pure TypeScript (no Svelte), subscriber-based like ShortcutRegistry —
|
|
14
|
+
* reactivity lives in the overlay components, which re-read on notify.
|
|
15
|
+
*
|
|
16
|
+
* Usage (app shell):
|
|
17
|
+
*
|
|
18
|
+
* <KeyboardShortcutsOverlay mode="host" /> // owns the `?` toggle
|
|
19
|
+
*
|
|
20
|
+
* Usage (page / portal / contract page):
|
|
21
|
+
*
|
|
22
|
+
* <KeyboardShortcutsOverlay
|
|
23
|
+
* {sections}
|
|
24
|
+
* sectionTitle="Inbox"
|
|
25
|
+
* showHint={false}
|
|
26
|
+
* />
|
|
27
|
+
* // self-renders when no host exists; contributes when one does
|
|
28
|
+
*/
|
|
29
|
+
const HUB_ID = 'keyboard-shortcut-hub';
|
|
30
|
+
class KeyboardShortcutHub {
|
|
31
|
+
host = null;
|
|
32
|
+
contributions = new Map();
|
|
33
|
+
listeners = new Set();
|
|
34
|
+
/** True when a host overlay is mounted (contributing instances defer to it). */
|
|
35
|
+
get hasHost() {
|
|
36
|
+
return this.host !== null;
|
|
37
|
+
}
|
|
38
|
+
/** Mount the host. Returns the unmount function. */
|
|
39
|
+
registerHost(host) {
|
|
40
|
+
if (this.host && this.host !== host) {
|
|
41
|
+
// Two hosts — the later one wins (last-mounted). Warn loudly: a
|
|
42
|
+
// page accidentally mounting a second global overlay is a bug.
|
|
43
|
+
console.warn('[keyboardShortcutHub] replacing an existing host overlay — did you mount the global overlay twice?');
|
|
44
|
+
}
|
|
45
|
+
this.host = host;
|
|
46
|
+
this.notify();
|
|
47
|
+
return () => {
|
|
48
|
+
if (this.host === host) {
|
|
49
|
+
this.host = null;
|
|
50
|
+
this.notify();
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
/** Contribute sections from a page-level overlay instance. Returns unregister. */
|
|
55
|
+
contribute(contribution) {
|
|
56
|
+
this.contributions.set(contribution.id, contribution);
|
|
57
|
+
this.notify();
|
|
58
|
+
return () => {
|
|
59
|
+
if (this.contributions.get(contribution.id) === contribution) {
|
|
60
|
+
this.contributions.delete(contribution.id);
|
|
61
|
+
this.notify();
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
/** Contributed sections in registration order, labelled by sectionTitle. */
|
|
66
|
+
getContributions() {
|
|
67
|
+
return [...this.contributions.values()];
|
|
68
|
+
}
|
|
69
|
+
/** Ask the host overlay to open. No-op when there is no host. */
|
|
70
|
+
requestOpen() {
|
|
71
|
+
if (!this.host)
|
|
72
|
+
return false;
|
|
73
|
+
this.host.open();
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
/** Subscribe to hub changes (host mount/unmount, contributions). */
|
|
77
|
+
subscribe(listener) {
|
|
78
|
+
this.listeners.add(listener);
|
|
79
|
+
return () => {
|
|
80
|
+
this.listeners.delete(listener);
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
notify() {
|
|
84
|
+
for (const l of this.listeners)
|
|
85
|
+
l();
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
export const keyboardShortcutHub = new KeyboardShortcutHub();
|
|
89
|
+
export const KEYBOARD_SHORTCUT_HUB_ID = HUB_ID;
|