@joewinke/jatui 0.35.0 → 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.
@@ -39,9 +39,32 @@
39
39
  * <KeyboardShortcutsOverlay {registry}>
40
40
  * {#snippet extra()}<div>…custom footer…</div>{/snippet}
41
41
  * </KeyboardShortcutsOverlay>
42
+ *
43
+ * ── Host / contributor mode ───────────────────────────────────────────
44
+ *
45
+ * An app shell can mount ONE global host overlay; every other instance
46
+ * on the page then contributes its sections to the host instead of
47
+ * self-rendering, so exactly one `?` overlay exists no matter how many
48
+ * surfaces mount the component:
49
+ *
50
+ * // App shell (layout.svelte):
51
+ * <KeyboardShortcutsOverlay mode="host" />
52
+ *
53
+ * // A page with its own shortcut set:
54
+ * <KeyboardShortcutsOverlay {sections} sectionTitle="Inbox" />
55
+ *
56
+ * `mode` defaults to `'auto'`: the instance contributes to the host when
57
+ * one is mounted, and self-renders when there isn't (the portal /
58
+ * contract-page fallback). Force self-rendering with
59
+ * `mode="standalone"`.
42
60
  */
43
61
  import type { Snippet } from 'svelte';
44
62
  import type { ShortcutRegistry } from '../utils/keyboardShortcutRegistry';
63
+ import {
64
+ keyboardShortcutHub,
65
+ type ShortcutContribution,
66
+ type ShortcutSectionLike
67
+ } from '../utils/keyboardShortcutHub';
45
68
 
46
69
  export interface KeyboardShortcut {
47
70
  key: string;
@@ -67,7 +90,27 @@
67
90
  extraSections,
68
91
  extra,
69
92
  title = 'Keyboard Shortcuts',
70
- open = $bindable(false)
93
+ open = $bindable(false),
94
+ /**
95
+ * Host / contributor mode.
96
+ * - `'host'`: this instance IS the global overlay — it registers
97
+ * with the hub and aggregates contributions from other instances.
98
+ * - `'auto'` (default): contribute to the host when one exists,
99
+ * otherwise self-render (the portal / contract-page fallback).
100
+ * - `'standalone'`: never defer, always self-render.
101
+ */
102
+ mode = 'auto',
103
+ /**
104
+ * Label under which this instance's sections appear in the host
105
+ * panel when contributing. Ignored when self-rendering.
106
+ */
107
+ sectionTitle = 'Page shortcuts',
108
+ /**
109
+ * Show the closed-state "press ?" hint pill (bottom-right).
110
+ * `showHint={false}` opts out — e.g. on contributor instances that
111
+ * defer to the host's own hint.
112
+ */
113
+ showHint = true
71
114
  }: {
72
115
  shortcuts?: KeyboardShortcut[];
73
116
  /**
@@ -97,6 +140,9 @@
97
140
  extra?: Snippet;
98
141
  title?: string;
99
142
  open?: boolean;
143
+ mode?: 'auto' | 'host' | 'standalone';
144
+ sectionTitle?: string;
145
+ showHint?: boolean;
100
146
  } = $props();
101
147
 
102
148
  let isOpen = $state(open);
@@ -108,19 +154,103 @@
108
154
  open = isOpen;
109
155
  });
110
156
 
111
- // Registry sections are snapshotted on open — BEFORE pushing the overlay's
112
- // own modal context, which would otherwise hide every page context.
157
+ // ── Registry → sections ─────────────────────────────────────────────
158
+ // Registry sections are kept live via `registry.subscribe()` so the host
159
+ // panel always reflects the current context stack. The overlay pushes its
160
+ // own modal context while open, which hides the page contexts underneath —
161
+ // that push is what suppresses page shortcuts behind the modal.
113
162
  let registrySections = $state<ShortcutSection[] | null>(null);
114
163
  $effect(() => {
115
- if (!isOpen || !registry) return;
164
+ if (!registry) return;
116
165
  registrySections = registry.toSections();
166
+ return registry.subscribe(() => {
167
+ registrySections = registry.toSections();
168
+ });
169
+ });
170
+ $effect(() => {
171
+ if (!isOpen || !registry) return;
117
172
  return registry.pushContext('keyboard-shortcuts-overlay', { modal: true });
118
173
  });
119
174
 
120
- const shownSections = $derived(
175
+ // ── Hub wiring ──────────────────────────────────────────────────────
176
+ let hubHasHost = $state(keyboardShortcutHub.hasHost);
177
+ let hubContributions = $state<ShortcutContribution[]>(keyboardShortcutHub.getContributions());
178
+ $effect(() => {
179
+ return keyboardShortcutHub.subscribe(() => {
180
+ hubHasHost = keyboardShortcutHub.hasHost;
181
+ hubContributions = keyboardShortcutHub.getContributions();
182
+ });
183
+ });
184
+
185
+ // A 'host' instance registers itself with the hub while mounted.
186
+ $effect(() => {
187
+ if (mode !== 'host') return;
188
+ return keyboardShortcutHub.registerHost({
189
+ open: () => {
190
+ isOpen = true;
191
+ },
192
+ toggle: () => {
193
+ isOpen = !isOpen;
194
+ }
195
+ });
196
+ });
197
+
198
+ // Whether this instance self-renders or defers to the host.
199
+ const isHost = $derived(mode === 'host');
200
+ const isContributor = $derived(mode !== 'host' && mode !== 'standalone' && hubHasHost);
201
+
202
+ // This instance's own sections (precedence: sections > registry > null).
203
+ const ownSections = $derived(
121
204
  sections && sections.length > 0 ? sections : registry ? registrySections : null
122
205
  );
123
206
 
207
+ // Contributing instances hand their sections to the hub. `extraSections`
208
+ // ride along too — they belong to the page, not the overlay itself.
209
+ const contributionId = `kso-${Math.random().toString(36).slice(2, 10)}`;
210
+ $effect(() => {
211
+ if (!isContributor) return;
212
+ const primary = ownSections ?? [];
213
+ if (primary.length === 0 && (!extraSections || extraSections.length === 0)) return;
214
+ const contribution: ShortcutContribution = {
215
+ id: contributionId,
216
+ sectionTitle,
217
+ sections: primary,
218
+ registrySections: extraSections ?? []
219
+ };
220
+ return keyboardShortcutHub.contribute(contribution);
221
+ });
222
+
223
+ // Contributing instances forward external `open` requests to the host —
224
+ // they can't open themselves.
225
+ $effect(() => {
226
+ if (!isContributor) return;
227
+ if (!open) return;
228
+ keyboardShortcutHub.requestOpen();
229
+ isOpen = false;
230
+ });
231
+
232
+ function flattenSections(secs: ShortcutSectionLike[]): SectionEntry[] {
233
+ return secs.flatMap((s) => s.shortcuts);
234
+ }
235
+
236
+ // Host panel content: own sections plus every contributor's sections,
237
+ // each grouped under its sectionTitle.
238
+ const hostSections = $derived.by(() => {
239
+ if (!isHost) return null;
240
+ const primary = ownSections ?? (shortcuts.length > 0 ? [{ title, shortcuts }] : []);
241
+ const contributed: ShortcutSection[] = hubContributions.flatMap((c) => [
242
+ { title: c.sectionTitle, shortcuts: flattenSections(c.sections) },
243
+ ...(c.registrySections ?? []).map((s) => ({ title: s.title, shortcuts: s.shortcuts }))
244
+ ]);
245
+ return [...primary, ...contributed];
246
+ });
247
+
248
+ // What actually renders in the panel.
249
+ const shownSections = $derived(isHost ? hostSections : ownSections);
250
+ const shownFlatShortcuts = $derived(
251
+ isHost || (shownSections && shownSections.length > 0) ? [] : shortcuts
252
+ );
253
+
124
254
  // Track which key label is currently depressed
125
255
  let depressedKey = $state<string | null>(null);
126
256
  let depressTimer: ReturnType<typeof setTimeout> | null = null;
@@ -143,6 +273,8 @@
143
273
  if (e.key === '?') {
144
274
  // `?` is always Shift+/. Don't trigger while typing — would swallow the char.
145
275
  if (isTypingTarget(e.target)) return;
276
+ // A contributor doesn't own `?` — the host's own listener handles it.
277
+ if (isContributor) return;
146
278
  e.preventDefault();
147
279
  isOpen = !isOpen;
148
280
  return;
@@ -236,7 +368,7 @@
236
368
  {/each}
237
369
  {/each}
238
370
  {:else}
239
- {#each shortcuts as { key, description }, i (key + '::' + i)}
371
+ {#each shownFlatShortcuts as { key, description }, i (key + '::' + i)}
240
372
  <div class="kso-row">
241
373
  <kbd
242
374
  class="kso-key"
@@ -279,6 +411,13 @@
279
411
  </div>
280
412
  </div>
281
413
  </div>
414
+ {:else if showHint && !isContributor}
415
+ <!-- Closed-state hint — press ? to open. Contributors that defer to a
416
+ host stay silent: the host renders its own hint. -->
417
+ <div class="kso-hint" aria-hidden="true">
418
+ <kbd class="kso-hint-key">?</kbd>
419
+ <span class="kso-hint-text">for shortcuts</span>
420
+ </div>
282
421
  {/if}
283
422
 
284
423
  <style>
@@ -419,6 +558,39 @@
419
558
  border-color: oklch(0.45 0.06 240);
420
559
  }
421
560
 
561
+ .kso-hint {
562
+ position: fixed;
563
+ right: 1rem;
564
+ bottom: 1rem;
565
+ z-index: 50;
566
+ display: flex;
567
+ align-items: center;
568
+ gap: 0.45rem;
569
+ padding: 0.3rem 0.65rem;
570
+ background: oklch(0.18 0.02 250 / 0.85);
571
+ border: 1px solid oklch(0.30 0.03 250);
572
+ border-radius: 0.5rem;
573
+ color: oklch(0.85 0.02 250);
574
+ font-size: 0.78rem;
575
+ pointer-events: none;
576
+ animation: kso-fade 0.15s ease-out;
577
+ }
578
+
579
+ .kso-hint-key {
580
+ font-family: ui-monospace, 'SF Mono', Menlo, Consolas, monospace;
581
+ font-size: 0.72rem;
582
+ background: oklch(0.25 0.02 250);
583
+ border: 1px solid oklch(0.35 0.02 250);
584
+ border-bottom-width: 2px;
585
+ border-radius: 0.3rem;
586
+ padding: 0.05rem 0.45rem;
587
+ color: oklch(0.93 0.02 250);
588
+ }
589
+
590
+ .kso-hint-text {
591
+ color: oklch(0.72 0.02 250);
592
+ }
593
+
422
594
  @keyframes kso-fade {
423
595
  from {
424
596
  opacity: 0;
@@ -430,7 +602,8 @@
430
602
 
431
603
  @media (prefers-reduced-motion: reduce) {
432
604
  .kso-backdrop,
433
- .kso-panel {
605
+ .kso-panel,
606
+ .kso-hint {
434
607
  animation: none !important;
435
608
  }
436
609
  .kso-key {
@@ -38,6 +38,24 @@
38
38
  * <KeyboardShortcutsOverlay {registry}>
39
39
  * {#snippet extra()}<div>…custom footer…</div>{/snippet}
40
40
  * </KeyboardShortcutsOverlay>
41
+ *
42
+ * ── Host / contributor mode ───────────────────────────────────────────
43
+ *
44
+ * An app shell can mount ONE global host overlay; every other instance
45
+ * on the page then contributes its sections to the host instead of
46
+ * self-rendering, so exactly one `?` overlay exists no matter how many
47
+ * surfaces mount the component:
48
+ *
49
+ * // App shell (layout.svelte):
50
+ * <KeyboardShortcutsOverlay mode="host" />
51
+ *
52
+ * // A page with its own shortcut set:
53
+ * <KeyboardShortcutsOverlay {sections} sectionTitle="Inbox" />
54
+ *
55
+ * `mode` defaults to `'auto'`: the instance contributes to the host when
56
+ * one is mounted, and self-renders when there isn't (the portal /
57
+ * contract-page fallback). Force self-rendering with
58
+ * `mode="standalone"`.
41
59
  */
42
60
  import type { Snippet } from 'svelte';
43
61
  import type { ShortcutRegistry } from '../utils/keyboardShortcutRegistry';
@@ -84,6 +102,9 @@ type $$ComponentProps = {
84
102
  extra?: Snippet;
85
103
  title?: string;
86
104
  open?: boolean;
105
+ mode?: 'auto' | 'host' | 'standalone';
106
+ sectionTitle?: string;
107
+ showHint?: boolean;
87
108
  };
88
109
  declare const KeyboardShortcutsOverlay: import("svelte").Component<$$ComponentProps, {}, "open">;
89
110
  type KeyboardShortcutsOverlay = ReturnType<typeof KeyboardShortcutsOverlay>;
@@ -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 on hover; requires `onRemove`. */
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 opacity-0 transition-opacity group-hover:opacity-100"
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 on hover; requires `onRemove`. */
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';
@@ -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';
@@ -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. */