feat(web): PillNav bar mode, fullscreen, local STT + mic button

PillNav overhaul:
- Dropdown-as-bar: theme/AI/sync/user menus render as horizontal
  bars in the bottom stack (PillDropdownBar) instead of floating
  popovers. New onOpenBar/activeBarId props on PillNavigation.
- iconOnly pills: tags/search/workbench-tabs pills show only icons.
  Home pill removed. New iconOnly flag on PillNavItem.
- Segmented toggle groups: items sharing a `group` id render as a
  single segmented pill (e.g. Light/Dark/System triple).
- Fullscreen mode: press "f" to hide all bottom chrome, Esc to exit.
- QuickInputBar + bottom bar visibility toggles via new pills.
- Progress ring on AI trigger pill during model download
  (conic-gradient ::after, follows pill border-radius).

@mana/local-stt — new package for browser-local speech-to-text:
- Whisper models via transformers.js v4 (WebGPU + WASM fallback)
- Same Web Worker architecture as @mana/local-llm
- Two models: Whisper Tiny (150 MB) and Whisper Small (950 MB)
- Reactive Svelte 5 bindings (getLocalSttStatus, loadLocalStt, transcribe)

Voice-to-text integration:
- useLocalStt() composable: mic capture via AudioContext +
  ScriptProcessor, resample to 16kHz mono, feed into Whisper worker
- Mic button in QuickInputBar (leftAction slot) with
  recording/loading/transcribing states + pulse animation
- Transcribed text injected into InputBar via new injectedText prop
- STT model selector in AI bar alongside LLM tier controls

Also: vite.config.ts server.fs.allow expanded to monorepo root
so workspace package workers resolve in dev.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Till JS 2026-04-12 16:05:43 +02:00
parent 8c2f9306e9
commit 3deee755b3
24 changed files with 2145 additions and 28 deletions

View file

@ -76,6 +76,9 @@
locale?: string;
/** Use 'static' when inside a flex container (bottom-stack pattern). Default: 'fixed'. */
positioning?: 'fixed' | 'static';
/** Externally injected text (e.g. from voice input). When this changes
* to a non-empty string, the input bar's query is set and focused. */
injectedText?: string;
}
let {
@ -106,6 +109,7 @@
highlightPatterns,
locale = 'de',
positioning = 'fixed',
injectedText,
}: Props = $props();
// Use settings for autoFocus
@ -125,6 +129,18 @@
// Whether search has been explicitly triggered in deferred mode
let searchTriggered = $state(false);
// External text injection (e.g. from voice-to-text). When the prop
// changes to a new non-empty value, set the search query and focus.
let lastInjected = '';
$effect(() => {
if (injectedText && injectedText !== lastInjected) {
lastInjected = injectedText;
searchQuery = injectedText;
// Focus the input so the user sees and can edit the text
requestAnimationFrame(() => inputElement?.focus());
}
});
// Context menu state
let contextMenuVisible = $state(false);
let contextMenuX = $state(0);