@joewinke/jatui 0.23.0 → 0.25.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +32 -2
- package/dist/components/AudioRecorder.svelte +84 -304
- package/dist/components/AudioRecorder.svelte.d.ts +1 -7
- package/dist/components/BulkActionBar.svelte +40 -0
- package/dist/components/BulkActionBar.svelte.d.ts +9 -0
- package/dist/components/ConfirmDialog.svelte +39 -35
- package/dist/components/CopyButton.svelte +5 -2
- package/dist/components/FloatingActionBar.svelte +55 -1
- package/dist/components/FloatingActionBar.svelte.d.ts +8 -1
- package/dist/components/InputDialog.svelte +49 -38
- package/dist/components/KeyboardShortcutsOverlay.svelte +54 -0
- package/dist/components/KeyboardShortcutsOverlay.svelte.d.ts +30 -0
- package/dist/components/bulkActionBarKeys.d.ts +40 -0
- package/dist/components/bulkActionBarKeys.js +38 -0
- package/dist/components/linked-columns/LinkedColumns.svelte +10 -22
- package/dist/components/linked-columns/LinkedColumns.svelte.d.ts +1 -9
- package/dist/components/linked-columns/types.d.ts +9 -0
- package/dist/components/linked-columns/types.js +1 -0
- package/dist/components/replay/ChapterTimeline.svelte +43 -25
- package/dist/components/replay/ChapterTimeline.svelte.d.ts +26 -1
- package/dist/components/session-nav/transcriptModel.d.ts +46 -0
- package/dist/components/session-nav/transcriptModel.js +143 -19
- package/dist/components/toast/ToastItem.svelte +47 -2
- package/dist/index.d.ts +8 -5
- package/dist/index.js +7 -2
- package/dist/stores/toast.d.ts +8 -0
- package/dist/stores/toast.js +2 -0
- package/dist/utils/audioRecorder.d.ts +28 -0
- package/dist/utils/audioRecorder.js +58 -0
- package/dist/utils/audioRecorderCore.d.ts +132 -0
- package/dist/utils/audioRecorderCore.js +447 -0
- package/dist/utils/clipboard.d.ts +25 -0
- package/dist/utils/clipboard.js +59 -0
- package/dist/utils/dateFormatters.d.ts +11 -2
- package/dist/utils/dateFormatters.js +22 -2
- package/package.json +1 -1
|
@@ -41,10 +41,36 @@ function parseSignalBody(body) {
|
|
|
41
41
|
if (!m)
|
|
42
42
|
return null;
|
|
43
43
|
let label = '';
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
44
|
+
let payload = null;
|
|
45
|
+
// Try to extract and parse the JSON payload from the signal call
|
|
46
|
+
const jsonMatch = body.match(/jat-signal\s+\w+\s+'(\{[\s\S]*?\})'\s*$/) ||
|
|
47
|
+
body.match(/jat-signal\s+\w+\s+"(\{[\s\S]*?\})"\s*$/) ||
|
|
48
|
+
body.match(/jat-signal\s+\w+\s+(\{[\s\S]*?\})\s*$/);
|
|
49
|
+
if (jsonMatch) {
|
|
50
|
+
try {
|
|
51
|
+
payload = JSON.parse(jsonMatch[1]);
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
// ignore parse errors — payload stays null
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
if (payload) {
|
|
58
|
+
// Extract label from full payload
|
|
59
|
+
const p = payload;
|
|
60
|
+
const candidate = (Array.isArray(p.summary) ? p.summary[0] : undefined) ??
|
|
61
|
+
(typeof p.approach === 'string' ? p.approach : undefined) ??
|
|
62
|
+
(typeof p.taskTitle === 'string' ? p.taskTitle : undefined) ??
|
|
63
|
+
(typeof p.question === 'string' ? p.question : undefined) ??
|
|
64
|
+
(typeof p.currentStep === 'string' ? p.currentStep : undefined);
|
|
65
|
+
if (candidate)
|
|
66
|
+
label = candidate.slice(0, 90);
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
const pick = body.match(/"(?:approach|question|reviewFocus|currentStep|summary|reason|taskTitle)"\s*:\s*"([^"]{3,90})/);
|
|
70
|
+
if (pick)
|
|
71
|
+
label = pick[1];
|
|
72
|
+
}
|
|
73
|
+
return { rawType: m[1], label, payload };
|
|
48
74
|
}
|
|
49
75
|
/** Build the linked model from rendered transcript lines. */
|
|
50
76
|
export function buildLinkedSession(lines) {
|
|
@@ -64,7 +90,14 @@ export function buildLinkedSession(lines) {
|
|
|
64
90
|
} // blank ends a block
|
|
65
91
|
if (line.startsWith('› ')) {
|
|
66
92
|
flush();
|
|
67
|
-
|
|
93
|
+
const headline = line.slice(2);
|
|
94
|
+
// Claude Code's TUI draws a blank "› " input box while idle between
|
|
95
|
+
// turns; a capture that lands mid-frame leaks that empty prompt line
|
|
96
|
+
// into the transcript. Treat it like a blank line (below) rather than
|
|
97
|
+
// a zero-content prompt item — it never carries real content.
|
|
98
|
+
if (!headline.trim())
|
|
99
|
+
continue;
|
|
100
|
+
cur = { kind: 'prompt', headline, detail: [], srcLines: 1 };
|
|
68
101
|
continue;
|
|
69
102
|
}
|
|
70
103
|
if (line.startsWith('⏺ ')) {
|
|
@@ -73,7 +106,7 @@ export function buildLinkedSession(lines) {
|
|
|
73
106
|
if (sig) {
|
|
74
107
|
// signal emission = boundary; consume it, don't render as an item
|
|
75
108
|
flush();
|
|
76
|
-
emits.push({ afterIdx: items.length - 1, rawType: sig.rawType, label: sig.label });
|
|
109
|
+
emits.push({ afterIdx: items.length - 1, rawType: sig.rawType, label: sig.label, payload: sig.payload });
|
|
77
110
|
continue;
|
|
78
111
|
}
|
|
79
112
|
flush();
|
|
@@ -113,13 +146,33 @@ export function buildLinkedSession(lines) {
|
|
|
113
146
|
}
|
|
114
147
|
flush();
|
|
115
148
|
// Build signal nodes from emission boundaries.
|
|
149
|
+
// Ownership is FORWARD: each signal owns the items that come AFTER it, up to
|
|
150
|
+
// (but not including) the next signal's emission point. The first signal also
|
|
151
|
+
// absorbs any preamble items that appear before it.
|
|
152
|
+
//
|
|
153
|
+
// Let B[i] = emits[i].afterIdx = last item index before signal[i].
|
|
154
|
+
//
|
|
155
|
+
// Partition (non-overlapping):
|
|
156
|
+
// sig[0] → [0 .. B[1]] preamble + first forward region
|
|
157
|
+
// sig[i>0] → [B[i]+1 .. B[i+1]] region after sig[i], before sig[i+1]
|
|
158
|
+
// sig[last] → [B[last]+1 .. N-1] region after last signal
|
|
159
|
+
//
|
|
160
|
+
// This ensures output printed AFTER a signal emission (e.g. the READY FOR
|
|
161
|
+
// REVIEW banner after `jat-signal review`) is attributed to that signal, not
|
|
162
|
+
// the subsequent one.
|
|
116
163
|
const signals = [];
|
|
117
|
-
|
|
164
|
+
const N = items.length;
|
|
118
165
|
emits.forEach((e, i) => {
|
|
119
|
-
|
|
120
|
-
|
|
166
|
+
// rowStart: first signal starts at 0 (absorbs preamble items before it);
|
|
167
|
+
// later signals start at the item immediately after this signal's emission.
|
|
168
|
+
const rowStart = i === 0 ? 0 : e.afterIdx + 1;
|
|
169
|
+
// naturalEnd: extends to the next signal's pre-boundary, or to the last item.
|
|
170
|
+
const naturalEnd = i + 1 < emits.length ? emits[i + 1].afterIdx : N - 1;
|
|
171
|
+
// rowEnd: capped at N-1 (never out of bounds); may be < rowStart when this
|
|
172
|
+
// signal has no items after it (e.g. last line is the signal itself).
|
|
173
|
+
const rowEnd = Math.min(naturalEnd, N - 1);
|
|
121
174
|
let lineCount = 0;
|
|
122
|
-
for (let k = rowStart; k <= rowEnd && k <
|
|
175
|
+
for (let k = rowStart; k <= rowEnd && k < N; k++)
|
|
123
176
|
lineCount += items[k].srcLines;
|
|
124
177
|
signals.push({
|
|
125
178
|
id: `sig-${i}`,
|
|
@@ -135,9 +188,9 @@ export function buildLinkedSession(lines) {
|
|
|
135
188
|
filesTouched: [],
|
|
136
189
|
commits: [],
|
|
137
190
|
linesAdded: 0,
|
|
138
|
-
linesRemoved: 0
|
|
191
|
+
linesRemoved: 0,
|
|
192
|
+
payload: e.payload
|
|
139
193
|
});
|
|
140
|
-
prevBoundary = e.afterIdx;
|
|
141
194
|
});
|
|
142
195
|
for (const sig of signals) {
|
|
143
196
|
for (let k = sig.rowStart; k <= sig.rowEnd && k < items.length; k++) {
|
|
@@ -168,9 +221,11 @@ export function buildLinkedSessionFromWork(blocks, items) {
|
|
|
168
221
|
headline: it.headline,
|
|
169
222
|
detail: it.detail,
|
|
170
223
|
srcLines: it.srcLines,
|
|
171
|
-
signalId: null // filled in below
|
|
224
|
+
signalId: null, // filled in below
|
|
225
|
+
timestamp: it.timestamp ?? null
|
|
172
226
|
}));
|
|
173
|
-
// Map WorkBlock → SignalNode
|
|
227
|
+
// Map WorkBlock → SignalNode. `isLive` is seeded from the server's open flag
|
|
228
|
+
// (read defensively — jat-difet adds it; may be undefined on older payloads).
|
|
174
229
|
const signals = blocks.map((blk) => ({
|
|
175
230
|
id: blk.id,
|
|
176
231
|
state: rawTypeToState(blk.signal.type),
|
|
@@ -185,11 +240,80 @@ export function buildLinkedSessionFromWork(blocks, items) {
|
|
|
185
240
|
filesTouched: blk.filesTouched,
|
|
186
241
|
commits: blk.commits ?? [],
|
|
187
242
|
linesAdded: blk.linesAdded ?? 0,
|
|
188
|
-
linesRemoved: blk.linesRemoved ?? 0
|
|
243
|
+
linesRemoved: blk.linesRemoved ?? 0,
|
|
244
|
+
payload: blk.signal.payload,
|
|
245
|
+
isLive: blk.open === true
|
|
189
246
|
}));
|
|
190
|
-
|
|
247
|
+
const N = transcriptItems.length;
|
|
248
|
+
// Helper: synthesize the live node owning the open in-progress turn.
|
|
249
|
+
const makeLiveNode = (start, end, label) => {
|
|
250
|
+
let lineCount = 0;
|
|
251
|
+
for (let k = start; k <= end && k < N; k++)
|
|
252
|
+
lineCount += transcriptItems[k].srcLines;
|
|
253
|
+
return {
|
|
254
|
+
id: 'sig-live',
|
|
255
|
+
state: rawTypeToState('working'),
|
|
256
|
+
rawType: 'working',
|
|
257
|
+
label,
|
|
258
|
+
rowStart: start,
|
|
259
|
+
rowEnd: end,
|
|
260
|
+
lineCount: Math.max(1, lineCount),
|
|
261
|
+
blockId: null,
|
|
262
|
+
summary: null,
|
|
263
|
+
filesTouched: [],
|
|
264
|
+
commits: [],
|
|
265
|
+
linesAdded: 0,
|
|
266
|
+
linesRemoved: 0,
|
|
267
|
+
payload: null,
|
|
268
|
+
isLive: true
|
|
269
|
+
};
|
|
270
|
+
};
|
|
271
|
+
if (N > 0) {
|
|
272
|
+
if (signals.length === 0) {
|
|
273
|
+
// No blocks but live items exist → the whole transcript is the open
|
|
274
|
+
// turn. Give it a single live node so nothing is left unsignaled.
|
|
275
|
+
signals.push(makeLiveNode(0, N - 1, 'current'));
|
|
276
|
+
}
|
|
277
|
+
else {
|
|
278
|
+
// (#1 gap-fill) Guarantee a contiguous [0..N-1] partition so EVERY
|
|
279
|
+
// item ends up owning a signal:
|
|
280
|
+
// (a) first signal absorbs any preamble before its range
|
|
281
|
+
signals[0].rowStart = 0;
|
|
282
|
+
// (b) extend each signal's rowEnd to close the gap before the next
|
|
283
|
+
// signal's range (no-op when ranges already touch/overlap)
|
|
284
|
+
for (let i = 0; i < signals.length - 1; i++) {
|
|
285
|
+
const nextStart = signals[i + 1].rowStart;
|
|
286
|
+
if (signals[i].rowEnd < nextStart - 1) {
|
|
287
|
+
signals[i].rowEnd = nextStart - 1;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
// (#2 live node) Trailing items past the last block's range are the
|
|
291
|
+
// OPEN in-progress turn — they must NOT be folded into the previous
|
|
292
|
+
// signal (e.g. a 'review'). Honor a server-marked open block first;
|
|
293
|
+
// otherwise synthesize a dedicated live node.
|
|
294
|
+
const lastSig = signals[signals.length - 1];
|
|
295
|
+
const lastBlockEnd = lastSig.rowEnd;
|
|
296
|
+
if (lastSig.isLive) {
|
|
297
|
+
// Server already marked the last block open → IT is the live node;
|
|
298
|
+
// extend it to absorb any trailing items.
|
|
299
|
+
lastSig.rowEnd = N - 1;
|
|
300
|
+
}
|
|
301
|
+
else if (lastBlockEnd < N - 1) {
|
|
302
|
+
// Synthesize a live node owning [lastBlockEnd+1 .. N-1]. Label from
|
|
303
|
+
// the live task title when available, else a neutral 'current'.
|
|
304
|
+
const p = lastSig.payload;
|
|
305
|
+
const title = p && typeof p.taskTitle === 'string' ? p.taskTitle.slice(0, 90) : null;
|
|
306
|
+
signals.push(makeLiveNode(lastBlockEnd + 1, N - 1, title ?? 'current'));
|
|
307
|
+
}
|
|
308
|
+
// else: the last block already reaches N-1 — nothing trailing.
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
// Assign signalId to transcript items via (gap-filled) ownership ranges.
|
|
312
|
+
// Clamp to bounds; later signals (incl. the live node) win on any overlap.
|
|
191
313
|
for (const sig of signals) {
|
|
192
|
-
|
|
314
|
+
const start = Math.max(0, sig.rowStart);
|
|
315
|
+
const end = Math.min(sig.rowEnd, N - 1);
|
|
316
|
+
for (let k = start; k <= end; k++) {
|
|
193
317
|
transcriptItems[k].signalId = sig.id;
|
|
194
318
|
}
|
|
195
319
|
}
|
|
@@ -202,7 +326,7 @@ export function buildLinkedSessionFromWork(blocks, items) {
|
|
|
202
326
|
*/
|
|
203
327
|
export const SAMPLE_TRANSCRIPT_LINES = [
|
|
204
328
|
'',
|
|
205
|
-
'› /jat
|
|
329
|
+
'› /jat-start GentleCoast jat-7h2k',
|
|
206
330
|
'⏺ Bash(get-current-session-id)',
|
|
207
331
|
' ⎿ 4f9c1a2e-7b3d-4c8a-9e1f-2a6b8c0d4e5f',
|
|
208
332
|
'⏺ Bash(jat-signal starting \'{"agentName":"GentleCoast","taskTitle":"Add password reset flow"}\')',
|
|
@@ -256,7 +380,7 @@ export const SAMPLE_TRANSCRIPT_LINES = [
|
|
|
256
380
|
'',
|
|
257
381
|
'🔍 READY FOR REVIEW: jat-7h2k — password reset flow implemented, tested, rate-limited.',
|
|
258
382
|
'',
|
|
259
|
-
'› /jat
|
|
383
|
+
'› /jat-complete',
|
|
260
384
|
'⏺ Bash(jat-step committing --task jat-7h2k)',
|
|
261
385
|
' ⎿ Committed: feat(jat-7h2k): add password reset flow',
|
|
262
386
|
'⏺ Bash(jat-signal complete \'{"taskId":"jat-7h2k","summary":"Shipped password reset flow"}\')',
|
|
@@ -151,7 +151,11 @@
|
|
|
151
151
|
}
|
|
152
152
|
}
|
|
153
153
|
|
|
154
|
-
//
|
|
154
|
+
// Collapsible details (toast.details)
|
|
155
|
+
let detailsOpen = $state(false);
|
|
156
|
+
const detailsId = $derived(`toast-details-${toast.id}`);
|
|
157
|
+
|
|
158
|
+
// DaisyUI alert class per type
|
|
155
159
|
const alertClass: Record<string, string> = {
|
|
156
160
|
info: 'alert-info',
|
|
157
161
|
success: 'alert-success',
|
|
@@ -223,7 +227,40 @@
|
|
|
223
227
|
></span>
|
|
224
228
|
{/if}
|
|
225
229
|
|
|
226
|
-
<
|
|
230
|
+
<div class="flex-1 min-w-0">
|
|
231
|
+
<span class="text-sm">{toast.message}</span>
|
|
232
|
+
|
|
233
|
+
{#if toast.details}
|
|
234
|
+
<button
|
|
235
|
+
type="button"
|
|
236
|
+
class="toast-details-toggle btn btn-ghost btn-xs mt-0.5 h-auto min-h-0 gap-1 px-0 font-semibold opacity-70"
|
|
237
|
+
aria-expanded={detailsOpen}
|
|
238
|
+
aria-controls={detailsId}
|
|
239
|
+
onclick={() => (detailsOpen = !detailsOpen)}
|
|
240
|
+
>
|
|
241
|
+
<svg
|
|
242
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
243
|
+
class="size-3"
|
|
244
|
+
class:rotate-90={detailsOpen}
|
|
245
|
+
viewBox="0 0 24 24"
|
|
246
|
+
fill="none"
|
|
247
|
+
stroke="currentColor"
|
|
248
|
+
stroke-width="2.5"
|
|
249
|
+
stroke-linecap="round"
|
|
250
|
+
stroke-linejoin="round"
|
|
251
|
+
>
|
|
252
|
+
<path d="m9 18 6-6-6-6" />
|
|
253
|
+
</svg>
|
|
254
|
+
Details
|
|
255
|
+
</button>
|
|
256
|
+
{#if detailsOpen}
|
|
257
|
+
<pre
|
|
258
|
+
id={detailsId}
|
|
259
|
+
class="toast-details mt-1 max-h-40 overflow-auto rounded border border-current/20 bg-base-content/5 p-2 font-mono text-xs whitespace-pre-wrap break-words"
|
|
260
|
+
>{toast.details}</pre>
|
|
261
|
+
{/if}
|
|
262
|
+
{/if}
|
|
263
|
+
</div>
|
|
227
264
|
|
|
228
265
|
{#if toast.onUndo}
|
|
229
266
|
<button
|
|
@@ -274,7 +311,15 @@
|
|
|
274
311
|
to { transform: scaleX(0); }
|
|
275
312
|
}
|
|
276
313
|
|
|
314
|
+
.toast-details-toggle svg {
|
|
315
|
+
transition: transform var(--motion-fast) var(--motion-glide);
|
|
316
|
+
}
|
|
317
|
+
|
|
277
318
|
@media (prefers-reduced-motion: reduce) {
|
|
319
|
+
.toast-details-toggle svg {
|
|
320
|
+
transition: none;
|
|
321
|
+
}
|
|
322
|
+
|
|
278
323
|
.toast-progress {
|
|
279
324
|
animation: none;
|
|
280
325
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -6,9 +6,9 @@ export { collapse } from './actions/collapse';
|
|
|
6
6
|
export type { CollapseOptions } from './actions/collapse';
|
|
7
7
|
export { default as Collapse } from './components/Collapse.svelte';
|
|
8
8
|
export { default as LinkedColumns } from './components/linked-columns/LinkedColumns.svelte';
|
|
9
|
-
export type { ColumnLink } from './components/linked-columns/
|
|
9
|
+
export type { ColumnLink } from './components/linked-columns/types.js';
|
|
10
10
|
export { default as ChapterTimeline } from './components/replay/ChapterTimeline.svelte';
|
|
11
|
-
export type { ChapterBlock } from './components/replay/ChapterTimeline.svelte';
|
|
11
|
+
export type { ChapterBlock, ChapterStateVisual } from './components/replay/ChapterTimeline.svelte';
|
|
12
12
|
export { default as TranscriptPlayer } from './components/TranscriptPlayer.svelte';
|
|
13
13
|
export type { TranscriptSegment, TranscriptBookmark, TranscriptChapter, TranscriptSpeakerTalkTime, } from './components/TranscriptPlayer.svelte';
|
|
14
14
|
export { default as VideoPlayer } from './components/VideoPlayer.svelte';
|
|
@@ -25,7 +25,7 @@ export { default as Flythrough } from './components/Flythrough.svelte';
|
|
|
25
25
|
export { default as LessonEmbed } from './components/LessonEmbed.svelte';
|
|
26
26
|
export type { Collection, CollectionCut, CollectionBeat, } from './components/LessonEmbed.svelte';
|
|
27
27
|
export { buildLinkedSession, buildLinkedSessionFromWork, rawTypeToState, sampleLinkedSession, SAMPLE_TRANSCRIPT_LINES, } from './components/session-nav/transcriptModel';
|
|
28
|
-
export type { ItemKind, TranscriptItem, FileTouchedEntry, SignalNode, LinkedSession, WorkBlockClient, WorkTranscriptItemClient, } from './components/session-nav/transcriptModel';
|
|
28
|
+
export type { ItemKind, TranscriptItem, FileTouchedEntry, SignalNode, LinkedSession, WorkBlockClient, WorkTranscriptItemClient, MarkerTone, TimelineMarkerSpec, TimelineMarker, } from './components/session-nav/transcriptModel';
|
|
29
29
|
export { default as UserAvatar } from './components/UserAvatar.svelte';
|
|
30
30
|
export { default as SidebarUserFooter } from './components/SidebarUserFooter.svelte';
|
|
31
31
|
export { default as TaskTypeIcon } from './components/TaskTypeIcon.svelte';
|
|
@@ -83,7 +83,9 @@ export { default as ResizablePanel } from './components/ResizablePanel.svelte';
|
|
|
83
83
|
export { default as VoicePlayer } from './components/VoicePlayer.svelte';
|
|
84
84
|
export { default as SpeechForm } from './components/SpeechForm.svelte';
|
|
85
85
|
export { default as AudioRecorder } from './components/AudioRecorder.svelte';
|
|
86
|
-
export type { AudioRecordingResult } from './
|
|
86
|
+
export type { AudioRecordingResult } from './utils/audioRecorderCore';
|
|
87
|
+
export { createAudioRecorder } from './utils/audioRecorder';
|
|
88
|
+
export type { AudioRecorder, AudioRecorderCore, AudioRecorderError, AudioRecorderErrorKind, AudioRecorderFrame, AudioRecorderMode, AudioRecorderOptions, AudioRecorderPhase, AudioRecorderSnapshot } from './utils/audioRecorderCore';
|
|
87
89
|
export { default as ThemeSelector } from './components/ThemeSelector.svelte';
|
|
88
90
|
export { default as Sparkline } from './components/Sparkline.svelte';
|
|
89
91
|
export { default as BurndownChart } from './components/BurndownChart.svelte';
|
|
@@ -128,7 +130,7 @@ export type { Milestone, MilestoneStatus } from './types/milestone';
|
|
|
128
130
|
export type { LocationType, BookingStatus, OverrideType, Host, EventType, WeeklyAvailability, DateOverride, Booking, BookingAttendee, TimeSlot, BookingFormData, AvailabilityCounts } from './types/booking';
|
|
129
131
|
export { DAYS_OF_WEEK, DAYS_SHORT, LOCATION_LABELS, LOCATION_ICONS } from './types/booking';
|
|
130
132
|
export type { ImageVariantSpec, ImageProcessingSpec, VariantResult, VariantState, } from './types/smartImageEditor';
|
|
131
|
-
export { normalizeTimestamp, parseTimestamp, formatRelativeTime, formatFullDate, formatShortDate, formatLastActivity, formatDate, getTimeSinceMs, getTimeSinceMinutes, isWithinMinutes, formatDateTime, taskAge, pacingColor, type PacingLevel, type PacingResult, } from './utils/dateFormatters';
|
|
133
|
+
export { normalizeTimestamp, parseTimestamp, formatRelativeTime, formatFullDate, formatShortDate, formatLastActivity, formatDate, getTimeSinceMs, getTimeSinceMinutes, isWithinMinutes, formatDateTime, taskAge, pacingColor, formatPacingDuration, type PacingLevel, type PacingResult, type PacingOptions, } from './utils/dateFormatters';
|
|
132
134
|
export { statusColor, priorityColor, typeIcon, statusLabel, } from './utils/taskUtils';
|
|
133
135
|
export { stripPhoneNumber, formatPhoneNumber, isCompletePhoneNumber, isValidPhoneNumber, updatePhoneInput } from './utils/phoneFormat';
|
|
134
136
|
export { toE164, formatPhoneForDisplay, isValidPhone, listCountries, DEFAULT_COUNTRY, PRIORITY_COUNTRIES, type CountryOption, } from './utils/phone';
|
|
@@ -198,6 +200,7 @@ export type { GlideOrigin, GlideParams } from './transitions/glide';
|
|
|
198
200
|
export { themeWipe } from './utils/themeWipe';
|
|
199
201
|
export { viewTransition } from './utils/viewTransition';
|
|
200
202
|
export { play as playSound, registerSound, isSoundEnabled, setSoundEnabled } from './utils/sound';
|
|
203
|
+
export { copyText, readClipboardText } from './utils/clipboard';
|
|
201
204
|
export { splitText } from './utils/splitText';
|
|
202
205
|
export type { SplitBy, SplitTextOptions, SplitTextResult } from './utils/splitText';
|
|
203
206
|
export { default as ToastHost } from './components/toast/ToastHost.svelte';
|
package/dist/index.js
CHANGED
|
@@ -44,7 +44,8 @@ export { qrMatrix, qrCanvas } from './video/advisors/qr';
|
|
|
44
44
|
// LessonEmbed renders a demo-assemble collection.json (cut picker + player).
|
|
45
45
|
export { default as Flythrough } from './components/Flythrough.svelte';
|
|
46
46
|
export { default as LessonEmbed } from './components/LessonEmbed.svelte';
|
|
47
|
-
// Data models — Session transcript (from JAT IDE, jat-jj79f.16 G1
|
|
47
|
+
// Data models — Session transcript (from JAT IDE, jat-jj79f.16 G1; resynced
|
|
48
|
+
// with the IDE's payload/timestamp/live-node/F13-marker model in jatui-ucmkz)
|
|
48
49
|
export { buildLinkedSession, buildLinkedSessionFromWork, rawTypeToState, sampleLinkedSession, SAMPLE_TRANSCRIPT_LINES, } from './components/session-nav/transcriptModel';
|
|
49
50
|
// Components — Universal
|
|
50
51
|
export { default as UserAvatar } from './components/UserAvatar.svelte';
|
|
@@ -102,6 +103,8 @@ export { default as ResizablePanel } from './components/ResizablePanel.svelte';
|
|
|
102
103
|
export { default as VoicePlayer } from './components/VoicePlayer.svelte';
|
|
103
104
|
export { default as SpeechForm } from './components/SpeechForm.svelte';
|
|
104
105
|
export { default as AudioRecorder } from './components/AudioRecorder.svelte';
|
|
106
|
+
// Headless audio capture — the factory behind AudioRecorder (jatui-is2ow.8)
|
|
107
|
+
export { createAudioRecorder } from './utils/audioRecorder';
|
|
105
108
|
// Components — from Headcount
|
|
106
109
|
export { default as ThemeSelector } from './components/ThemeSelector.svelte';
|
|
107
110
|
export { default as Sparkline } from './components/Sparkline.svelte';
|
|
@@ -137,7 +140,7 @@ export { default as AvailabilityModal } from './components/AvailabilityModal.sve
|
|
|
137
140
|
export { default as BookingForm } from './components/BookingForm.svelte';
|
|
138
141
|
export { DAYS_OF_WEEK, DAYS_SHORT, LOCATION_LABELS, LOCATION_ICONS } from './types/booking';
|
|
139
142
|
// Utilities — date formatting
|
|
140
|
-
export { normalizeTimestamp, parseTimestamp, formatRelativeTime, formatFullDate, formatShortDate, formatLastActivity, formatDate, getTimeSinceMs, getTimeSinceMinutes, isWithinMinutes, formatDateTime, taskAge, pacingColor, } from './utils/dateFormatters';
|
|
143
|
+
export { normalizeTimestamp, parseTimestamp, formatRelativeTime, formatFullDate, formatShortDate, formatLastActivity, formatDate, getTimeSinceMs, getTimeSinceMinutes, isWithinMinutes, formatDateTime, taskAge, pacingColor, formatPacingDuration, } from './utils/dateFormatters';
|
|
141
144
|
// Utilities — task display helpers
|
|
142
145
|
export { statusColor, priorityColor, typeIcon, statusLabel, } from './utils/taskUtils';
|
|
143
146
|
// Utilities — phone formatting (legacy US-only)
|
|
@@ -224,6 +227,8 @@ export { themeWipe } from './utils/themeWipe';
|
|
|
224
227
|
export { viewTransition } from './utils/viewTransition';
|
|
225
228
|
// Sound kit (functional feedback, opt-in)
|
|
226
229
|
export { play as playSound, registerSound, isSoundEnabled, setSoundEnabled } from './utils/sound';
|
|
230
|
+
// Clipboard (secure-context aware: execCommand fallback for plain-HTTP origins)
|
|
231
|
+
export { copyText, readClipboardText } from './utils/clipboard';
|
|
227
232
|
// Text utilities
|
|
228
233
|
export { splitText } from './utils/splitText';
|
|
229
234
|
// Toast system
|
package/dist/stores/toast.d.ts
CHANGED
|
@@ -2,6 +2,12 @@ export type ToastType = 'info' | 'success' | 'warning' | 'error';
|
|
|
2
2
|
export type ToastOptions = {
|
|
3
3
|
type?: ToastType;
|
|
4
4
|
message: string;
|
|
5
|
+
/** Extended detail text (e.g. a stack trace or error chain) — rendered by
|
|
6
|
+
* ToastHost behind a collapsible "Details" toggle. Multiline-safe. */
|
|
7
|
+
details?: string;
|
|
8
|
+
/** Arbitrary structured context carried on the Toast for programmatic
|
|
9
|
+
* access (getToasts()); not rendered by ToastHost. */
|
|
10
|
+
meta?: Record<string, unknown>;
|
|
5
11
|
action?: {
|
|
6
12
|
label: string;
|
|
7
13
|
onClick: () => void;
|
|
@@ -13,6 +19,8 @@ export type Toast = {
|
|
|
13
19
|
id: number;
|
|
14
20
|
type: ToastType;
|
|
15
21
|
message: string;
|
|
22
|
+
details?: string;
|
|
23
|
+
meta?: Record<string, unknown>;
|
|
16
24
|
action?: {
|
|
17
25
|
label: string;
|
|
18
26
|
onClick: () => void;
|
package/dist/stores/toast.js
CHANGED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { AudioRecorderMode, AudioRecorderOptions, AudioRecorderPhase, AudioRecorderSnapshot } from './audioRecorderCore';
|
|
2
|
+
export type { AudioRecorderError, AudioRecorderErrorKind, AudioRecorderFrame, AudioRecorderMode, AudioRecorderOptions, AudioRecorderPhase, AudioRecordingResult } from './audioRecorderCore';
|
|
3
|
+
export interface AudioRecorder {
|
|
4
|
+
/** False when the platform has no getUserMedia/MediaRecorder (SSR, Node). */
|
|
5
|
+
readonly supported: boolean;
|
|
6
|
+
readonly mode: AudioRecorderMode;
|
|
7
|
+
readonly phase: AudioRecorderPhase;
|
|
8
|
+
/** True while recording is suspended (pause()). */
|
|
9
|
+
readonly paused: boolean;
|
|
10
|
+
/** 0–100 live mic level. */
|
|
11
|
+
readonly level: number;
|
|
12
|
+
/** Bytes captured so far. */
|
|
13
|
+
readonly sizeBytes: number;
|
|
14
|
+
/** Pause-adjusted wall-clock elapsed ms. */
|
|
15
|
+
readonly elapsedMs: number;
|
|
16
|
+
/** Last error, or null. */
|
|
17
|
+
readonly error: AudioRecorderSnapshot['error'];
|
|
18
|
+
start(): Promise<void>;
|
|
19
|
+
stop(): void;
|
|
20
|
+
pause(): void;
|
|
21
|
+
resume(): void;
|
|
22
|
+
cancel(): void;
|
|
23
|
+
/** Return a finished/errored recorder to idle so the host can record again. */
|
|
24
|
+
reset(): void;
|
|
25
|
+
/** Final teardown — releases mic + timers. Call from onDestroy. */
|
|
26
|
+
destroy(): void;
|
|
27
|
+
}
|
|
28
|
+
export declare function createAudioRecorder(options?: AudioRecorderOptions): AudioRecorder;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/* audioRecorder.svelte.js generated by Svelte v5.53.11 */
|
|
2
|
+
import * as $ from 'svelte/internal/client';
|
|
3
|
+
import { createAudioRecorderCore } from './audioRecorderCore';
|
|
4
|
+
|
|
5
|
+
export function createAudioRecorder(options = {}) {
|
|
6
|
+
const core = createAudioRecorderCore(options);
|
|
7
|
+
const snap = $.proxy(core.snapshot());
|
|
8
|
+
|
|
9
|
+
core.subscribe((s) => {
|
|
10
|
+
snap.phase = s.phase;
|
|
11
|
+
snap.paused = s.paused;
|
|
12
|
+
snap.level = s.level;
|
|
13
|
+
snap.sizeBytes = s.sizeBytes;
|
|
14
|
+
snap.elapsedMs = s.elapsedMs;
|
|
15
|
+
snap.error = s.error;
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
return {
|
|
19
|
+
get supported() {
|
|
20
|
+
return core.supported;
|
|
21
|
+
},
|
|
22
|
+
|
|
23
|
+
get mode() {
|
|
24
|
+
return core.mode;
|
|
25
|
+
},
|
|
26
|
+
|
|
27
|
+
get phase() {
|
|
28
|
+
return snap.phase;
|
|
29
|
+
},
|
|
30
|
+
|
|
31
|
+
get paused() {
|
|
32
|
+
return snap.paused;
|
|
33
|
+
},
|
|
34
|
+
|
|
35
|
+
get level() {
|
|
36
|
+
return snap.level;
|
|
37
|
+
},
|
|
38
|
+
|
|
39
|
+
get sizeBytes() {
|
|
40
|
+
return snap.sizeBytes;
|
|
41
|
+
},
|
|
42
|
+
|
|
43
|
+
get elapsedMs() {
|
|
44
|
+
return snap.elapsedMs;
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
get error() {
|
|
48
|
+
return snap.error;
|
|
49
|
+
},
|
|
50
|
+
start: () => core.start(),
|
|
51
|
+
stop: () => core.stop(),
|
|
52
|
+
pause: () => core.pause(),
|
|
53
|
+
resume: () => core.resume(),
|
|
54
|
+
cancel: () => core.cancel(),
|
|
55
|
+
reset: () => core.reset(),
|
|
56
|
+
destroy: () => core.destroy()
|
|
57
|
+
};
|
|
58
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* createAudioRecorderCore — headless audio-capture primitive (jatui-is2ow.8).
|
|
3
|
+
*
|
|
4
|
+
* The capture pipeline of `AudioRecorder.svelte` (permission request, MIME
|
|
5
|
+
* negotiation, MediaRecorder state machine, pause-aware wall-clock timing,
|
|
6
|
+
* AnalyserNode level meter, blob out) factored into a dependency-injected,
|
|
7
|
+
* DOM-free core so any host can drive it from its own UI — mic buttons,
|
|
8
|
+
* push-to-talk keys, overlays — without importing the component.
|
|
9
|
+
*
|
|
10
|
+
* NO transcription here by design: hosts that transcribe (the jat IDE's
|
|
11
|
+
* VoiceMicButton/VoiceInput, wave 3.5 of the adoption audit jat-1alfp) keep
|
|
12
|
+
* their POST-local capture shape and consume `onStop`'s blob payload.
|
|
13
|
+
*
|
|
14
|
+
* Two layers:
|
|
15
|
+
* • this plain module — every knob injectable via `deps` (tests drive a fake
|
|
16
|
+
* MediaRecorder backend in node:test, no DOM);
|
|
17
|
+
* • `audioRecorder.svelte.ts` — the `createAudioRecorder()` rune wrapper
|
|
18
|
+
* that wires the core's snapshot into `$state` so Svelte 5 templates react
|
|
19
|
+
* to it. Components and app code import the wrapper; tests import this.
|
|
20
|
+
*
|
|
21
|
+
* Modes:
|
|
22
|
+
* • `'toggle'` (default) — click-style: `start()` begins, `stop()` ends.
|
|
23
|
+
* • `'push'` — hold-to-talk: the host wires pointerdown → `start()`,
|
|
24
|
+
* pointerup → `stop()`. A `stop()` that arrives while the mic stream is
|
|
25
|
+
* still being acquired (a very quick tap) CANCELS — no blob, no error —
|
|
26
|
+
* matching the jat IDE VoiceMicButton's pointerup-before-stream behavior.
|
|
27
|
+
*
|
|
28
|
+
* Phases: idle → starting → recording ⇄(pause/resume) → processing → done,
|
|
29
|
+
* with `cancel()` from recording returning straight to idle and `reset()`
|
|
30
|
+
* returning a finished/errored recorder to idle so the host can go again.
|
|
31
|
+
*
|
|
32
|
+
* Timer discipline (lesson from MediaCapture, mos-ow4q4.37): elapsed time is
|
|
33
|
+
* derived from the wall clock minus accumulated pause, never an incremented
|
|
34
|
+
* counter — a throttled background tab can't freeze it.
|
|
35
|
+
*/
|
|
36
|
+
export type AudioRecorderPhase = 'idle' | 'starting' | 'recording' | 'processing' | 'done';
|
|
37
|
+
export type AudioRecorderMode = 'toggle' | 'push';
|
|
38
|
+
/** Why a recording attempt failed. */
|
|
39
|
+
export type AudioRecorderErrorKind = 'unsupported' | 'denied' | 'no-mic' | 'start-failed' | 'too-short' | 'handler-failed';
|
|
40
|
+
export interface AudioRecorderError {
|
|
41
|
+
kind: AudioRecorderErrorKind;
|
|
42
|
+
message: string;
|
|
43
|
+
}
|
|
44
|
+
/** What a finished recording hands back to the host — mirrors MediaCaptureResult's shape for the audio kind. */
|
|
45
|
+
export interface AudioRecordingResult {
|
|
46
|
+
blob: Blob;
|
|
47
|
+
mimeType: string;
|
|
48
|
+
durationSeconds: number;
|
|
49
|
+
sizeBytes: number;
|
|
50
|
+
}
|
|
51
|
+
/** One animation frame of analyser data, for hosts that render a visualization. */
|
|
52
|
+
export interface AudioRecorderFrame {
|
|
53
|
+
/** 0–100 RMS level (same value exposed reactively as `level`). */
|
|
54
|
+
level: number;
|
|
55
|
+
/** Frequency-domain bytes (from getByteFrequencyData). */
|
|
56
|
+
frequency: Uint8Array;
|
|
57
|
+
/** Time-domain bytes (from getByteTimeDomainData) — raw PCM-ish shape. */
|
|
58
|
+
time: Uint8Array;
|
|
59
|
+
}
|
|
60
|
+
export interface AudioRecorderOptions {
|
|
61
|
+
/** `'toggle'` (default) or `'push'` (hold-to-talk — see module header). */
|
|
62
|
+
mode?: AudioRecorderMode;
|
|
63
|
+
/** Auto-stop ceiling in ms. 0 (default) records until stopped manually. */
|
|
64
|
+
maxDurationMs?: number;
|
|
65
|
+
/** Fired once the mic stream is live and recording has started. */
|
|
66
|
+
onStart?: () => void;
|
|
67
|
+
onPause?: () => void;
|
|
68
|
+
onResume?: () => void;
|
|
69
|
+
/** Fired with the finished recording. May return a Promise — the recorder
|
|
70
|
+
* stays in "processing" until it settles, so hosts can upload/transcribe
|
|
71
|
+
* without the user re-triggering a recording. */
|
|
72
|
+
onStop?: (result: AudioRecordingResult) => void | Promise<void>;
|
|
73
|
+
/** Fired when the recording is cancelled — no blob is produced. */
|
|
74
|
+
onCancel?: () => void;
|
|
75
|
+
/** Fired on recorder errors (permission denied, no mic, start failure, …). */
|
|
76
|
+
onError?: (error: AudioRecorderError) => void;
|
|
77
|
+
/** Fired every animation frame while recording — analyser data for hosts
|
|
78
|
+
* that draw their own waveform/level UI. Not fired while paused. */
|
|
79
|
+
onFrame?: (frame: AudioRecorderFrame) => void;
|
|
80
|
+
}
|
|
81
|
+
/** 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
|
+
export interface AudioRecorderDeps {
|
|
83
|
+
getUserMedia?: (constraints?: MediaStreamConstraints) => Promise<MediaStream>;
|
|
84
|
+
MediaRecorder?: any;
|
|
85
|
+
AudioContext?: any;
|
|
86
|
+
requestAnimationFrame?: (cb: (t: number) => void) => number;
|
|
87
|
+
cancelAnimationFrame?: (id: number) => void;
|
|
88
|
+
setTimeout?: typeof setTimeout;
|
|
89
|
+
clearTimeout?: typeof clearTimeout;
|
|
90
|
+
setInterval?: typeof setInterval;
|
|
91
|
+
clearInterval?: typeof clearInterval;
|
|
92
|
+
/** Wall clock — injectable so tests can advance time deterministically. */
|
|
93
|
+
now?: () => number;
|
|
94
|
+
}
|
|
95
|
+
export interface AudioRecorderSnapshot {
|
|
96
|
+
phase: AudioRecorderPhase;
|
|
97
|
+
/** True while recording is suspended (pause()). */
|
|
98
|
+
paused: boolean;
|
|
99
|
+
/** 0–100 live mic level. */
|
|
100
|
+
level: number;
|
|
101
|
+
/** Bytes captured so far. */
|
|
102
|
+
sizeBytes: number;
|
|
103
|
+
/** Pause-adjusted wall-clock elapsed ms. */
|
|
104
|
+
elapsedMs: number;
|
|
105
|
+
/** Last error, or null. */
|
|
106
|
+
error: AudioRecorderError | null;
|
|
107
|
+
}
|
|
108
|
+
export interface AudioRecorderCore {
|
|
109
|
+
/** False when the platform has no getUserMedia/MediaRecorder (SSR, Node). */
|
|
110
|
+
readonly supported: boolean;
|
|
111
|
+
readonly mode: AudioRecorderMode;
|
|
112
|
+
/** Current reactive-independent snapshot (the rune wrapper copies this into $state). */
|
|
113
|
+
snapshot(): AudioRecorderSnapshot;
|
|
114
|
+
/** Subscribe to snapshot changes. Returns an unsubscribe fn. */
|
|
115
|
+
subscribe(fn: (snapshot: AudioRecorderSnapshot) => void): () => void;
|
|
116
|
+
start(): Promise<void>;
|
|
117
|
+
stop(): void;
|
|
118
|
+
pause(): void;
|
|
119
|
+
resume(): void;
|
|
120
|
+
cancel(): void;
|
|
121
|
+
/** Return a finished/errored recorder to idle so the host can record again. */
|
|
122
|
+
reset(): void;
|
|
123
|
+
/** Final teardown — releases mic + timers. Safe to call from any phase. */
|
|
124
|
+
destroy(): void;
|
|
125
|
+
}
|
|
126
|
+
/** First supported recording MIME type, or '' when none are. */
|
|
127
|
+
export declare function pickAudioMimeType(isSupported?: (type: string) => boolean): string;
|
|
128
|
+
/** RMS of the time-domain data scaled to the recorder's 0–100 meter range. */
|
|
129
|
+
export declare function computeAudioLevel(timeData: ArrayLike<number> | null): number;
|
|
130
|
+
/** Elapsed from the wall clock, pause-adjusted — see module header note. */
|
|
131
|
+
export declare function computeElapsedMs(startedAt: number, pausedTotalMs: number, pauseStartedAt: number, now: number): number;
|
|
132
|
+
export declare function createAudioRecorderCore(options?: AudioRecorderOptions, deps?: AudioRecorderDeps): AudioRecorderCore;
|