From 4de3acd426e9798e6484960cfbd3c16e61772c79 Mon Sep 17 00:00:00 2001 From: Jake Date: Fri, 15 May 2026 16:14:12 +0100 Subject: [PATCH] v0.3 Phase 5c: Dictation layout migration toward the mockup structure MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Layout work, not a colour pass. DictationPage's existing 1 282-LOC structure stays untouched for v0.2; under quietware a new layout renders via LumotiaPageSkeleton with five zones matching the mockup: Capture header large Record + status + secondary support + timer + pill Slim notice info-classed browser-preview or danger-classed errors Primary surface calm transcript canvas with empty-state typography Action bar Copy / Save / Extract Tasks / Template / Open Viewer Mono metadata Smart · model · Local-only or Browser-preview Implementation pattern. Gated {#if isQuietware} branch inside the existing {#if needsDownload}{:else} structure. The new layout subscribes to ALL existing state bindings (page.recording, page.timerText, transcript, error, tauriRuntimeAvailable, modelLoading, settings.*, etc.) and reuses every existing handler: toggleRecording, copyAll, saveTypedText, manualExtractTasks, applyTemplate, invoke("open_viewer_window") v0.2 surface unchanged — when data-design="quietware" is absent the existing control-strip / toolbar / post-capture-card layout renders as before. Capture-header. - Record button (72 px circular) carries the role-correct --button-record-bg (red). Recording state pulses; modelLoading shows spinner; non-Tauri state neutral-disabled. - Status text uses Young Serif italic. Four states map to "Ready to capture" / "Loading model" / "Desktop preview mode" / "Recording…". - Secondary support text in Work Sans body weight, adapts per state. - Timer in JetBrains Mono tabular-nums, right aligned. - Status pill appears when useful (recording / transcribing / ready). Slim notice. - Browser-preview renders LumotiaNotice tone="info" slim dismissible with the friendly Phase 5b copy. - Real errors render tone="danger" slim dismissible. - Slot stays empty otherwise — no permanent banner clutter. Transcript surface. - Empty state: 56-px Mic icon, Young Serif italic "Talk now, think later.", Work Sans support "Press record, or Ctrl+Shift+R." Centred, low-emphasis tertiary text on the calm card surface. - Populated: scrollable transcript at the user's accessibility transcript-size preference. Bottom action bar. - Copy (tertiary), Save (primary blue), Extract Tasks (secondary), Template (secondary, applies templates[0] when present), Open Viewer (secondary, gated on Tauri). - All disabled until transcript has content. Disabled controls render neutral with no wireline per the Phase 4k token contract. Mono metadata footer. - Left: "Smart · {model} · Local only" or "Browser preview". - Right: "{formatMode} · {activeProfile}". - JetBrains Mono, text-tertiary, single line, below the action bar. What's preserved (no behaviour change). - Recording events, hotkey listeners (lumotia:toggle-recording). - Tauri runtime branching (tauriRuntimeAvailable, browser-preview state). - Live preview hooks (emit("preview-listening") etc.). - Task extraction (manualExtractTasks -> extractTasksForTranscript). - Template apply (applyTemplate(template)). - Copy / Save paths (copyAll, saveTypedText). - All existing state bindings on page.*, settings.*, prefs.*. Out of scope for this commit (queued for follow-up). - Files / Tasks / History page migrations (Phases 5d-5f). - Real Lumotia wordmark / icon integration (Phase 6). - Motion audit (Phase 7). - Full Template-picker menu (currently auto-applies templates[0]). Verified. - npm run check: 0 errors, 0 warnings across 5707 files. - Headless-Chromium screenshots confirm the new layout renders correctly in quietware-dark and quietware-light with the disabled-record state (Tauri APIs absent in browser preview), slim info notice, and empty-state typography. Co-Authored-By: Claude Opus 4.7 (1M context) --- docs/release/v0.3-tactile-quietware.md | 66 +++++++++++ src/lib/pages/DictationPage.svelte | 153 +++++++++++++++++++++++++ 2 files changed, 219 insertions(+) diff --git a/docs/release/v0.3-tactile-quietware.md b/docs/release/v0.3-tactile-quietware.md index c6a6c0c..be2d7d1 100644 --- a/docs/release/v0.3-tactile-quietware.md +++ b/docs/release/v0.3-tactile-quietware.md @@ -351,6 +351,72 @@ LumotiaNotice now reads these via Tailwind arbitrary-value classes (`bg-[var(--n Per Jake's round-5 spec: yellow stays yellow (visible signal), ochre only appears as small-text ink, and brown/khaki never represents the caution role's primary identity again. +### Phase 5c — Dictation layout migration. Landed 2026-05-15. + +The big layout migration. DictationPage's existing 1 282-LOC structure stays untouched for v0.2; under quietware a new layout renders via `LumotiaPageSkeleton` with five zones matching the mockup: + +``` +[Capture header] large Record + status text + secondary support + timer + status pill +[Slim notice] info-classed browser-preview or danger-classed real errors +[Primary surface] calm transcript canvas with empty-state typography +[Action bar] Copy / Save / Extract Tasks / Template / Open Viewer +[Mono metadata] Smart · model · Local-only or Browser-preview +``` + +Implementation pattern: gated `{#if isQuietware}` branch inside the existing `{#if needsDownload}{:else}` structure. The new layout subscribes to ALL existing state bindings (`page.recording`, `page.timerText`, `transcript`, `error`, `tauriRuntimeAvailable`, `modelLoading`, `settings.*`, etc.) and reuses every handler (`toggleRecording`, `copyAll`, `saveTypedText`, `manualExtractTasks`, `applyTemplate`, `invoke("open_viewer_window")`). v0.2 surface is unchanged. + +Capture-header content: + +- **Record button** (72 px circular) carries the role-correct fill via `--button-record-bg` (red). Recording state pulses; modelLoading state shows the spinner; non-Tauri state goes neutral-disabled. Hover swaps to `--button-record-border`. Focus-visible ring uses `--button-record-bg` at `--wire-opacity-focus`. +- **Status text** uses Young Serif italic via `font-display`. Four states: Ready to capture / Loading model / Desktop preview mode / Recording… +- **Secondary support text** in Work Sans body weight. Adapts per state. +- **Timer** in JetBrains Mono tabular-nums, right aligned. +- **Status pill** appears when there is something useful to report (recording / transcribing / ready). + +Slim notice: + +- Browser-preview message renders as `LumotiaNotice tone="info" slim dismissible` with the friendly Phase 5b copy. +- Real errors render as `LumotiaNotice tone="danger" slim dismissible`. +- Notice slot stays empty otherwise — no permanent banner clutter. + +Transcript surface: + +- Empty state: 56 px Mic icon, Young Serif italic "Talk now, think later.", Work Sans support "Press record, or Ctrl+Shift+R." Centred, low-emphasis tertiary text on the calm card. +- Populated: scrollable transcript at the user's accessibility transcript size. + +Bottom action bar: + +- Copy (tertiary), Save (primary blue), Extract Tasks (secondary), Template (secondary, applies templates[0] when present), Open Viewer (secondary, gated on Tauri). +- All disabled until transcript has content; disabled controls render neutral with no wireline. + +Mono metadata footer: + +- Left: `Smart · {model} · Local only` or `Browser preview`. +- Right: `{formatMode} · {activeProfile}`. +- JetBrains Mono, text-tertiary, single line. + +What's preserved (no behaviour change): + +- Recording events, hotkey listeners (`lumotia:toggle-recording`). +- Tauri runtime branching (`tauriRuntimeAvailable`, browser-preview state). +- Live preview hooks (`emit("preview-listening")` etc.). +- Task extraction (`manualExtractTasks` → `extractTasksForTranscript`). +- Template apply (`applyTemplate(template)`). +- Copy / Save paths (`copyAll`, `saveTypedText`). +- All existing state bindings on `page.*`, `settings.*`, `prefs.*`. + +What is NOT done in this commit (out of scope for Phase 5c): + +- Files / Tasks / History page migrations (Phases 5d-5f). +- Real Lumotia wordmark / icon integration (Phase 6, blocked on SVG). +- Motion audit + decorative animation strip (Phase 7). +- Template-picker menu in the bottom action bar (currently auto-applies first template; full menu reserved for a follow-up). + +Verified: + +- `npm run check`: 0 errors, 0 warnings across 5707 files. +- Browser-preview screenshots show the new layout rendering correctly with the disabled-record state, slim info notice, and empty-state typography. + ### Phase 4k — Palette architecture hardening (round-9 redirect). Landed 2026-05-15. Token-hardening pass. No new visual decisions — just plumbing the tokens correctly so the grammar locked in Phase 4j has a system underneath instead of hand-tuned hex values. diff --git a/src/lib/pages/DictationPage.svelte b/src/lib/pages/DictationPage.svelte index 296ee1f..09713b3 100644 --- a/src/lib/pages/DictationPage.svelte +++ b/src/lib/pages/DictationPage.svelte @@ -14,6 +14,9 @@ import LumotiaCard from "$lib/ui/LumotiaCard.svelte"; import LumotiaEmptyState from "$lib/ui/LumotiaEmptyState.svelte"; import LumotiaNotice from "$lib/ui/LumotiaNotice.svelte"; + // v0.3 Phase 5c — quietware layout uses the shared page skeleton. + import LumotiaPageSkeleton from "$lib/ui/LumotiaPageSkeleton.svelte"; + import LumotiaButton from "$lib/ui/LumotiaButton.svelte"; import ModelDownloader from "$lib/components/ModelDownloader.svelte"; import { exportTranscript } from "$lib/utils/export.js"; import { extractTasks } from "$lib/utils/taskExtractor.js"; @@ -72,9 +75,28 @@ // Global hotkey listener let hotkeyHandler = () => toggleRecording(); + // v0.3 Phase 5c — quietware layout switch. Mirrors the SettingsPage + // pattern: reactive flag driven by a MutationObserver on + // . When unset (v0.2), the page renders its + // existing layout unchanged. + let isQuietware = $state(false); + let quietwareObserver: MutationObserver | null = null; + onMount(async () => { window.addEventListener("lumotia:toggle-recording", hotkeyHandler); + if (typeof document !== "undefined") { + const sync = () => { + isQuietware = document.documentElement.dataset.design === "quietware"; + }; + sync(); + quietwareObserver = new MutationObserver(sync); + quietwareObserver.observe(document.documentElement, { + attributes: true, + attributeFilter: ["data-design"], + }); + } + if (!tauriRuntimeAvailable) { error = browserPreviewMessage; return; @@ -87,6 +109,8 @@ onDestroy(() => { window.removeEventListener("lumotia:toggle-recording", hotkeyHandler); clearInterval(timerInterval); + quietwareObserver?.disconnect(); + quietwareObserver = null; if (page.recording) { page.recording = false; page.status = "Ready"; @@ -915,6 +939,134 @@ onComplete={onModelDownloaded} /> {:else} + {#if isQuietware} + + + {#snippet header()} +
+ +
+

+ {#if page.recording}Recording… + {:else if modelLoading}Loading model + {:else if !tauriRuntimeAvailable}Desktop preview mode + {:else}Ready to capture + {/if} +

+

+ {#if page.recording}Press to stop, or use the global hotkey. + {:else if modelLoading}One moment. + {:else if !tauriRuntimeAvailable}Local transcription needs the Lumotia desktop app. + {:else}Press record, or Ctrl+Shift+R. + {/if} +

+
+
+ {page.timerText || "00:00"} +
+ {#if page.recording} + + {:else if modelLoading} + + {:else if tauriRuntimeAvailable} + + {/if} +
+ {/snippet} + + {#snippet notice()} + {#if error === browserPreviewMessage} + { error = ""; errorDetail = ""; }}> + {browserPreviewSlim} + + {:else if error} + { error = ""; errorDetail = ""; }}> + {error} + + {/if} + {/snippet} + + {#snippet primary()} +
+ {#if !transcript.trim()} +
+
+ {:else} +
{transcript}
+ {/if} +
+ {/snippet} + + {#snippet actionBar()} +
+ Copy + Save + Extract Tasks + { if (templates.length > 0) applyTemplate(templates[0]); }} + title={templates.length === 0 ? "No templates configured" : `Apply ${templates[0].name}`} + >Template + invoke("open_viewer_window").catch(() => {})} + >Open Viewer +
+ {/snippet} + + {#snippet metadata()} +
+ Smart · {settings.modelSize} · {tauriRuntimeAvailable ? "Local only" : "Browser preview"} + {settings.formatMode} · {page.activeProfile === "None" ? "No profile" : page.activeProfile} +
+ {/snippet} +
+ {:else}