feat(dictation): surface "Talk now, think later" empty state and hide post-transcript actions

Reduces first-run cognitive load. Save / Copy / Clear / Export now appear only
once the user has a transcript or is recording, sliding in via animate-fade-in
(brand --duration-ui + ease-out-quart). Template and Extract Tasks remain
always-visible because they shape transcription rather than acting on completed
content.

Empty state now leads with the catchphrase as a brand statement
(font-display italic 28px) with the hotkey hint demoted to body-font tertiary.
EmptyState gains an optional headline prop; callers without it render unchanged.
This commit is contained in:
2026-05-07 10:49:37 +01:00
parent d2f64a231d
commit 5778696140
2 changed files with 45 additions and 35 deletions

View File

@@ -1,12 +1,17 @@
<script> <script>
let { icon: Icon = null, message = '', actionLabel = '', onAction = null } = $props(); let { icon: Icon = null, headline = '', message = '', actionLabel = '', onAction = null } = $props();
</script> </script>
<div class="flex flex-col items-center justify-center gap-4 py-16 text-text-secondary"> <div class="flex flex-col items-center justify-center gap-3 py-16 text-text-secondary">
{#if Icon} {#if Icon}
<Icon size={48} strokeWidth={1} class="opacity-40" aria-hidden="true" /> <Icon size={48} strokeWidth={1} class="opacity-40 mb-1" aria-hidden="true" />
{/if}
{#if headline}
<p class="font-display italic text-[28px] text-text text-center leading-tight">{headline}</p>
{/if}
{#if message}
<p class="text-center max-w-xs leading-relaxed {headline ? 'text-text-tertiary' : ''}">{message}</p>
{/if} {/if}
<p class="text-center max-w-xs leading-relaxed">{message}</p>
{#if actionLabel && onAction} {#if actionLabel && onAction}
<button onclick={onAction} <button onclick={onAction}
class="px-4 py-2 rounded-lg bg-accent text-bg text-sm font-medium hover:bg-accent-hover" class="px-4 py-2 rounded-lg bg-accent text-bg text-sm font-medium hover:bg-accent-hover"

View File

@@ -705,6 +705,7 @@
} }
let taskCount = $derived(tasks.filter((t) => !t.done).length); let taskCount = $derived(tasks.filter((t) => !t.done).length);
let hasTranscript = $derived(transcript.trim().length > 0);
let wordCount = $derived.by(() => { let wordCount = $derived.by(() => {
const trimmed = transcript.trim(); const trimmed = transcript.trim();
@@ -937,37 +938,41 @@
onclick={manualExtractTasks} onclick={manualExtractTasks}
disabled={aiProcessing || !transcript.trim()} disabled={aiProcessing || !transcript.trim()}
>{aiProcessing ? "Extracting..." : "Extract Tasks"}</button> >{aiProcessing ? "Extracting..." : "Extract Tasks"}</button>
<span class="w-px h-4 bg-border-subtle flex-shrink-0"></span> {#if hasTranscript || page.recording}
<button <div class="flex items-center gap-1 animate-fade-in">
class="btn-md rounded-lg text-text-secondary hover:bg-hover hover:text-text flex-shrink-0 whitespace-nowrap <span class="w-px h-4 bg-border-subtle flex-shrink-0"></span>
{!transcript.trim() ? 'opacity-40 cursor-default' : ''}" <button
onclick={saveTypedText} class="btn-md rounded-lg text-text-secondary hover:bg-hover hover:text-text flex-shrink-0 whitespace-nowrap
disabled={!transcript.trim()} {!transcript.trim() ? 'opacity-40 cursor-default' : ''}"
>Save</button> onclick={saveTypedText}
<button disabled={!transcript.trim()}
class="btn-md rounded-lg text-text-secondary hover:bg-hover hover:text-text flex-shrink-0 whitespace-nowrap" >Save</button>
onclick={copyAll} <button
>Copy</button> class="btn-md rounded-lg text-text-secondary hover:bg-hover hover:text-text flex-shrink-0 whitespace-nowrap"
<button onclick={copyAll}
class="btn-md rounded-lg text-text-secondary hover:bg-hover hover:text-text flex-shrink-0 whitespace-nowrap" >Copy</button>
onclick={clearTranscript} <button
>Clear</button> class="btn-md rounded-lg text-text-secondary hover:bg-hover hover:text-text flex-shrink-0 whitespace-nowrap"
<div class="relative flex-shrink-0"> onclick={clearTranscript}
<button >Clear</button>
class="btn-md rounded-lg text-text-secondary hover:bg-hover hover:text-text whitespace-nowrap" <div class="relative flex-shrink-0">
onclick={() => { showExportMenu = !showExportMenu; showTemplateMenu = false; }} <button
>Export</button> class="btn-md rounded-lg text-text-secondary hover:bg-hover hover:text-text whitespace-nowrap"
{#if showExportMenu} onclick={() => { showExportMenu = !showExportMenu; showTemplateMenu = false; }}
<div class="absolute right-0 top-full mt-1 bg-bg-card border border-border rounded-lg shadow-lg py-1 z-10 animate-fade-in min-w-[120px]"> >Export</button>
{#each [["txt", "Plain Text"], ["md", "Markdown"], ["csv", "CSV"], ["html", "HTML"], ["srt", "SRT Subtitles"], ["vtt", "WebVTT"]] as [fmt, label]} {#if showExportMenu}
<button <div class="absolute right-0 top-full mt-1 bg-bg-card border border-border rounded-lg shadow-lg py-1 z-10 animate-fade-in min-w-[120px]">
class="w-full text-left btn-md text-text-secondary hover:bg-hover hover:text-text" {#each [["txt", "Plain Text"], ["md", "Markdown"], ["csv", "CSV"], ["html", "HTML"], ["srt", "SRT Subtitles"], ["vtt", "WebVTT"]] as [fmt, label]}
onclick={() => handleExport(fmt)} <button
>{label}</button> class="w-full text-left btn-md text-text-secondary hover:bg-hover hover:text-text"
{/each} onclick={() => handleExport(fmt)}
>{label}</button>
{/each}
</div>
{/if}
</div> </div>
{/if} </div>
</div> {/if}
</div> </div>
<!-- Template indicator --> <!-- Template indicator -->
@@ -1008,7 +1013,7 @@
</div> </div>
{:else if !transcript.trim() && !page.recording && !transcribing} {:else if !transcript.trim() && !page.recording && !transcribing}
<div class="flex-1 flex items-center justify-center"> <div class="flex-1 flex items-center justify-center">
<EmptyState icon={Mic} message={`Press record or ${settings.globalHotkey}`} /> <EmptyState icon={Mic} headline="Talk now, think later." message={`Press record, or ${settings.globalHotkey}.`} />
</div> </div>
<textarea <textarea
bind:this={textareaEl} bind:this={textareaEl}