v0.3 Phase 5c: Dictation layout migration toward the mockup structure
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) <noreply@anthropic.com>
This commit is contained in:
@@ -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.
|
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.
|
### 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.
|
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.
|
||||||
|
|||||||
@@ -14,6 +14,9 @@
|
|||||||
import LumotiaCard from "$lib/ui/LumotiaCard.svelte";
|
import LumotiaCard from "$lib/ui/LumotiaCard.svelte";
|
||||||
import LumotiaEmptyState from "$lib/ui/LumotiaEmptyState.svelte";
|
import LumotiaEmptyState from "$lib/ui/LumotiaEmptyState.svelte";
|
||||||
import LumotiaNotice from "$lib/ui/LumotiaNotice.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 ModelDownloader from "$lib/components/ModelDownloader.svelte";
|
||||||
import { exportTranscript } from "$lib/utils/export.js";
|
import { exportTranscript } from "$lib/utils/export.js";
|
||||||
import { extractTasks } from "$lib/utils/taskExtractor.js";
|
import { extractTasks } from "$lib/utils/taskExtractor.js";
|
||||||
@@ -72,9 +75,28 @@
|
|||||||
// Global hotkey listener
|
// Global hotkey listener
|
||||||
let hotkeyHandler = () => toggleRecording();
|
let hotkeyHandler = () => toggleRecording();
|
||||||
|
|
||||||
|
// v0.3 Phase 5c — quietware layout switch. Mirrors the SettingsPage
|
||||||
|
// pattern: reactive flag driven by a MutationObserver on
|
||||||
|
// <html data-design>. When unset (v0.2), the page renders its
|
||||||
|
// existing layout unchanged.
|
||||||
|
let isQuietware = $state(false);
|
||||||
|
let quietwareObserver: MutationObserver | null = null;
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
window.addEventListener("lumotia:toggle-recording", hotkeyHandler);
|
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) {
|
if (!tauriRuntimeAvailable) {
|
||||||
error = browserPreviewMessage;
|
error = browserPreviewMessage;
|
||||||
return;
|
return;
|
||||||
@@ -87,6 +109,8 @@
|
|||||||
onDestroy(() => {
|
onDestroy(() => {
|
||||||
window.removeEventListener("lumotia:toggle-recording", hotkeyHandler);
|
window.removeEventListener("lumotia:toggle-recording", hotkeyHandler);
|
||||||
clearInterval(timerInterval);
|
clearInterval(timerInterval);
|
||||||
|
quietwareObserver?.disconnect();
|
||||||
|
quietwareObserver = null;
|
||||||
if (page.recording) {
|
if (page.recording) {
|
||||||
page.recording = false;
|
page.recording = false;
|
||||||
page.status = "Ready";
|
page.status = "Ready";
|
||||||
@@ -914,6 +938,134 @@
|
|||||||
modelSize={settings.modelSize.toLowerCase()}
|
modelSize={settings.modelSize.toLowerCase()}
|
||||||
onComplete={onModelDownloaded}
|
onComplete={onModelDownloaded}
|
||||||
/>
|
/>
|
||||||
|
{:else}
|
||||||
|
{#if isQuietware}
|
||||||
|
<!-- =================================================================
|
||||||
|
v0.3 Phase 5c — quietware Dictation layout. Uses LumotiaPageSkeleton
|
||||||
|
and reuses every state binding and handler from the existing v0.2
|
||||||
|
layout below. Behaviour is identical; only the structure changes:
|
||||||
|
|
||||||
|
Capture header large Record + status + secondary + timer + pill
|
||||||
|
Slim notice info-classed browser-preview or error
|
||||||
|
Primary surface calm transcript canvas with empty-state typography
|
||||||
|
Bottom action Copy / Save / Extract Tasks / Template / Open Viewer
|
||||||
|
Mono footer Smart · model · Local only
|
||||||
|
|
||||||
|
The v0.2 path stays unchanged for non-quietware users.
|
||||||
|
================================================================= -->
|
||||||
|
<LumotiaPageSkeleton primaryBleed>
|
||||||
|
{#snippet header()}
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<button
|
||||||
|
class="relative flex items-center justify-center w-[72px] h-[72px] rounded-full shrink-0
|
||||||
|
text-[var(--button-record-fg)]
|
||||||
|
focus-visible:outline-none focus-visible:ring-[var(--wire-width-focus,2px)]
|
||||||
|
focus-visible:ring-[var(--button-record-bg)] focus-visible:ring-offset-2
|
||||||
|
{page.recording
|
||||||
|
? 'bg-danger animate-pulse-warm'
|
||||||
|
: modelLoading
|
||||||
|
? 'bg-warning opacity-60 cursor-wait'
|
||||||
|
: !tauriRuntimeAvailable
|
||||||
|
? 'bg-bg-elevated text-text-tertiary cursor-not-allowed'
|
||||||
|
: 'bg-[var(--button-record-bg)] hover:bg-[var(--button-record-border)]'}"
|
||||||
|
onclick={toggleRecording}
|
||||||
|
disabled={modelLoading || !tauriRuntimeAvailable}
|
||||||
|
aria-label={page.recording ? "Stop recording" : "Start recording"}
|
||||||
|
>
|
||||||
|
{#if page.recording}
|
||||||
|
<span class="w-[20px] h-[20px] rounded-[3px] bg-white"></span>
|
||||||
|
{:else if modelLoading}
|
||||||
|
<Loader2 size={24} class="animate-spin" aria-hidden="true" />
|
||||||
|
{:else}
|
||||||
|
<span class="w-[20px] h-[20px] rounded-full bg-white/90"></span>
|
||||||
|
{/if}
|
||||||
|
</button>
|
||||||
|
<div class="flex-1 min-w-0">
|
||||||
|
<h1 class="font-display italic text-[22px] text-text leading-tight">
|
||||||
|
{#if page.recording}Recording…
|
||||||
|
{:else if modelLoading}Loading model
|
||||||
|
{:else if !tauriRuntimeAvailable}Desktop preview mode
|
||||||
|
{:else}Ready to capture
|
||||||
|
{/if}
|
||||||
|
</h1>
|
||||||
|
<p class="text-[13px] text-text-secondary mt-0.5">
|
||||||
|
{#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}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="text-[20px] font-mono tabular-nums text-text-secondary shrink-0">
|
||||||
|
{page.timerText || "00:00"}
|
||||||
|
</div>
|
||||||
|
{#if page.recording}
|
||||||
|
<LumotiaStatusPill status="recording" />
|
||||||
|
{:else if modelLoading}
|
||||||
|
<LumotiaStatusPill status="transcribing" />
|
||||||
|
{:else if tauriRuntimeAvailable}
|
||||||
|
<LumotiaStatusPill status="ready" />
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/snippet}
|
||||||
|
|
||||||
|
{#snippet notice()}
|
||||||
|
{#if error === browserPreviewMessage}
|
||||||
|
<LumotiaNotice tone="info" slim dismissible onDismiss={() => { error = ""; errorDetail = ""; }}>
|
||||||
|
{browserPreviewSlim}
|
||||||
|
</LumotiaNotice>
|
||||||
|
{:else if error}
|
||||||
|
<LumotiaNotice tone="danger" slim dismissible onDismiss={() => { error = ""; errorDetail = ""; }}>
|
||||||
|
{error}
|
||||||
|
</LumotiaNotice>
|
||||||
|
{/if}
|
||||||
|
{/snippet}
|
||||||
|
|
||||||
|
{#snippet primary()}
|
||||||
|
<div class="h-full flex flex-col px-8 py-6">
|
||||||
|
{#if !transcript.trim()}
|
||||||
|
<div class="flex-1 flex flex-col items-center justify-center text-center gap-4 text-text-tertiary">
|
||||||
|
<Mic size={56} class="opacity-50" aria-hidden="true" />
|
||||||
|
<p class="font-display italic text-[28px] text-text">Talk now, think later.</p>
|
||||||
|
<p class="text-[14px] text-text-secondary">Press record, or Ctrl+Shift+R.</p>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div
|
||||||
|
class="flex-1 overflow-auto whitespace-pre-wrap text-text leading-relaxed"
|
||||||
|
style="--text-transcript: {prefs.accessibility.transcriptSize}px; font-size: var(--text-transcript)"
|
||||||
|
>{transcript}</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/snippet}
|
||||||
|
|
||||||
|
{#snippet actionBar()}
|
||||||
|
<div class="flex items-center gap-2 justify-end">
|
||||||
|
<LumotiaButton variant="tertiary" size="sm" onclick={copyAll} disabled={!transcript.trim()}>Copy</LumotiaButton>
|
||||||
|
<LumotiaButton variant="primary" size="sm" onclick={saveTypedText} disabled={!transcript.trim()}>Save</LumotiaButton>
|
||||||
|
<LumotiaButton variant="secondary" size="sm" onclick={manualExtractTasks} disabled={!transcript.trim()}>Extract Tasks</LumotiaButton>
|
||||||
|
<LumotiaButton
|
||||||
|
variant="secondary"
|
||||||
|
size="sm"
|
||||||
|
disabled={templates.length === 0 || !transcript.trim()}
|
||||||
|
onclick={() => { if (templates.length > 0) applyTemplate(templates[0]); }}
|
||||||
|
title={templates.length === 0 ? "No templates configured" : `Apply ${templates[0].name}`}
|
||||||
|
>Template</LumotiaButton>
|
||||||
|
<LumotiaButton
|
||||||
|
variant="secondary"
|
||||||
|
size="sm"
|
||||||
|
disabled={!tauriRuntimeAvailable}
|
||||||
|
onclick={() => invoke("open_viewer_window").catch(() => {})}
|
||||||
|
>Open Viewer</LumotiaButton>
|
||||||
|
</div>
|
||||||
|
{/snippet}
|
||||||
|
|
||||||
|
{#snippet metadata()}
|
||||||
|
<div class="flex items-center justify-between gap-4">
|
||||||
|
<span>Smart · {settings.modelSize} · {tauriRuntimeAvailable ? "Local only" : "Browser preview"}</span>
|
||||||
|
<span>{settings.formatMode} · {page.activeProfile === "None" ? "No profile" : page.activeProfile}</span>
|
||||||
|
</div>
|
||||||
|
{/snippet}
|
||||||
|
</LumotiaPageSkeleton>
|
||||||
{:else}
|
{:else}
|
||||||
<!-- Control strip -->
|
<!-- Control strip -->
|
||||||
<div class="flex items-center gap-3 px-5 h-[56px] border-b border-border-subtle flex-shrink-0">
|
<div class="flex items-center gap-3 px-5 h-[56px] border-b border-border-subtle flex-shrink-0">
|
||||||
@@ -1279,4 +1431,5 @@
|
|||||||
</LumotiaCard>
|
</LumotiaCard>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user