refactor(kon): dictation page layout — compact control strip, toolbar, more transcript space

- Replaced tall header (pt-6 pb-4 with 28px timer + label stack) with 56px control strip
- Control strip: 40px record button + waveform placeholder + 20px timer + status badge + task toggle
- Waveform placeholder shows animated bars during recording, hint text at idle
- Actions moved to dedicated toolbar row (justify-end) below control strip
- Status footer (word count, format mode, profile) moved inside transcript Card
- Insert-at-cursor indicator relocated to status footer
- Page container remains flex-col h-full; transcript area fills all remaining space

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
jake
2026-03-17 02:28:46 +00:00
parent 4d962adea6
commit ac0aeff676

View File

@@ -504,11 +504,11 @@
{#if needsDownload} {#if needsDownload}
<ModelDownloader modelSize={settings.modelSize.toLowerCase()} onComplete={onModelDownloaded} /> <ModelDownloader modelSize={settings.modelSize.toLowerCase()} onComplete={onModelDownloaded} />
{:else} {:else}
<!-- Header --> <!-- Control strip -->
<div class="flex items-center gap-5 px-7 pt-6 pb-4"> <div class="flex items-center gap-3 px-5 h-[56px] border-b border-border-subtle flex-shrink-0">
<!-- Record button --> <!-- Record button -->
<button <button
class="relative flex items-center justify-center w-[56px] h-[56px] min-w-[56px] flex-shrink-0 rounded-full text-white class="relative flex items-center justify-center w-[40px] h-[40px] min-w-[40px] flex-shrink-0 rounded-full text-white
active:scale-[0.93] transition-all duration-200 active:scale-[0.93] transition-all duration-200
{page.recording {page.recording
? 'bg-danger animate-pulse-warm' ? 'bg-danger animate-pulse-warm'
@@ -520,125 +520,145 @@
aria-label={page.recording ? "Stop recording" : "Start recording"} aria-label={page.recording ? "Stop recording" : "Start recording"}
> >
{#if page.recording} {#if page.recording}
<span class="w-[18px] h-[18px] rounded-[4px] bg-white"></span> <span class="w-[14px] h-[14px] rounded-[3px] bg-white"></span>
{:else if modelLoading} {:else if modelLoading}
<svg class="w-5 h-5 animate-spin" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"> <svg class="w-4 h-4 animate-spin" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
<path d="M12 2a10 10 0 0 1 10 10" stroke-linecap="round" /> <path d="M12 2a10 10 0 0 1 10 10" stroke-linecap="round" />
</svg> </svg>
{:else} {:else}
<span class="w-[18px] h-[18px] rounded-full bg-white/90"></span> <span class="w-[14px] h-[14px] rounded-full bg-white/90"></span>
{/if} {/if}
</button> </button>
<!-- Timer + label --> <!-- Waveform placeholder (Phase 2: live visualiser) -->
<div class="flex flex-col flex-shrink-0"> <div class="flex-1 min-h-[32px] rounded-md border border-border-subtle bg-bg-elevated/40 flex items-center px-3">
<span class="text-[28px] font-bold text-text tabular-nums tracking-tight leading-none" style="font-variant-numeric: tabular-nums;"> {#if page.recording}
{page.timerText} <span class="flex items-end gap-[3px] h-[18px]">
</span> {#each [0.4, 0.8, 0.5, 1, 0.6, 0.9, 0.4, 0.7, 0.5, 0.8, 0.3, 0.6] as h, i}
<span class="text-[11px] text-text-tertiary mt-1"> <span
{#if page.recording} class="w-[2px] rounded-full bg-danger/70 animate-pulse-soft"
<span class="inline-flex items-center gap-1.5"> style="height: {h * 100}%; animation-delay: {i * 60}ms"
<span class="w-[6px] h-[6px] rounded-full bg-danger animate-pulse-soft"></span> ></span>
<span class="font-medium text-danger tracking-wide">REC</span> {/each}
{#if insertPos >= 0} </span>
<span class="text-text-tertiary ml-1">inserting at cursor</span> {:else}
{/if} <span class="text-[11px] text-text-tertiary">
</span> {#if modelLoading}
{:else if modelLoading} Loading model...
<span class="text-warning">Loading Whisper model...</span> {:else if saved}
{:else if saved} <span class="text-success animate-fade-in">
<span class="text-success animate-fade-in"> Saved{#if extractedCount > 0} · {extractedCount} task{extractedCount === 1 ? '' : 's'} extracted{/if}
Saved to history{#if extractedCount > 0} · {extractedCount} task{extractedCount === 1 ? '' : 's'} extracted{/if} </span>
</span> {:else}
{:else} Press record or <kbd class="px-1 py-0.5 rounded bg-bg-elevated text-[10px] text-text-tertiary border border-border-subtle">Ctrl+Shift+R</kbd>
Press record or <kbd class="px-1 py-0.5 rounded bg-bg-elevated text-[10px] text-text-tertiary border border-border-subtle">Ctrl+Shift+R</kbd> {/if}
{/if} </span>
</span> {/if}
</div> </div>
<div class="flex-1"></div> <!-- Timer -->
<span class="text-[20px] font-bold text-text tabular-nums tracking-tight leading-none flex-shrink-0" style="font-variant-numeric: tabular-nums;">
{page.timerText}
</span>
<!-- Actions --> <!-- Status indicator -->
<div class="flex gap-0.5 flex-shrink-0"> <span class="text-[11px] text-text-tertiary flex-shrink-0 min-w-[60px] text-right">
<!-- Template selector --> {#if page.recording}
{#if templates.length > 0} <span class="inline-flex items-center gap-1.5">
<div class="relative"> <span class="w-[6px] h-[6px] rounded-full bg-danger animate-pulse-soft"></span>
<button <span class="font-medium text-danger tracking-wide">REC</span>
class="btn-md rounded-lg text-text-secondary hover:bg-hover hover:text-text flex-shrink-0 whitespace-nowrap" </span>
onclick={() => { showTemplateMenu = !showTemplateMenu; showExportMenu = false; }} {:else if modelLoading}
>Template</button> <span class="text-warning">Loading</span>
{#if showTemplateMenu} {:else}
<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-[160px]"> <span class="text-success">Ready</span>
{#each templates as template}
<button
class="w-full text-left btn-md text-text-secondary hover:bg-hover hover:text-text
{activeTemplate === template.name ? 'text-accent' : ''}"
onclick={() => applyTemplate(template)}
>{template.name}</button>
{/each}
</div>
{/if}
</div>
{/if} {/if}
<button </span>
class="btn-md rounded-lg flex-shrink-0 whitespace-nowrap {aiProcessing ? 'text-warning' : 'text-accent hover:bg-accent/10 hover:text-accent font-medium'}"
onclick={manualExtractTasks} <!-- Separator -->
disabled={aiProcessing || !transcript.trim()} <span class="w-px h-4 bg-border-subtle flex-shrink-0"></span>
>{aiProcessing ? "Extracting..." : "Extract Tasks"}</button>
<span class="w-px h-4 bg-border-subtle flex-shrink-0"></span> <!-- Task sidebar toggle -->
<button <button
class="btn-md rounded-lg text-text-secondary hover:bg-hover hover:text-text flex-shrink-0 whitespace-nowrap class="relative px-2 py-1.5 rounded-lg flex-shrink-0
{!transcript.trim() ? 'opacity-40 cursor-default' : ''}" {page.taskSidebarOpen ? 'bg-accent/10' : 'hover:bg-hover'}"
onclick={saveTypedText} onclick={() => page.taskSidebarOpen = !page.taskSidebarOpen}
disabled={!transcript.trim()} aria-label="Toggle task sidebar"
>Save</button> >
<button <svg class="w-4 h-4 {page.taskSidebarOpen ? 'text-accent' : 'text-text-tertiary'}" viewBox="0 0 24 24" fill="currentColor">
class="btn-md rounded-lg text-text-secondary hover:bg-hover hover:text-text flex-shrink-0 whitespace-nowrap" <path d="M9 11l3 3L22 4M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11" />
onclick={copyAll} </svg>
>Copy</button> {#if taskCount > 0}
<button <span class="absolute -top-0.5 -right-0.5 text-[9px] px-1 py-0 rounded-full bg-accent text-white font-medium min-w-[14px] text-center leading-[14px]">
class="btn-md rounded-lg text-text-secondary hover:bg-hover hover:text-text flex-shrink-0 whitespace-nowrap" {taskCount}
onclick={clearTranscript} </span>
>Clear</button> {/if}
<div class="relative flex-shrink-0"> </button>
</div>
<!-- Toolbar -->
<div class="flex items-center justify-end gap-1 px-5 py-2 border-b border-border-subtle flex-shrink-0">
<!-- Template selector -->
{#if templates.length > 0}
<div class="relative">
<button <button
class="btn-md rounded-lg text-text-secondary hover:bg-hover hover:text-text whitespace-nowrap" class="btn-md rounded-lg text-text-secondary hover:bg-hover hover:text-text flex-shrink-0 whitespace-nowrap"
onclick={() => { showExportMenu = !showExportMenu; showTemplateMenu = false; }} onclick={() => { showTemplateMenu = !showTemplateMenu; showExportMenu = false; }}
>Export</button> >Template</button>
{#if showExportMenu} {#if showTemplateMenu}
<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]"> <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-[160px]">
{#each [["txt", "Plain Text"], ["md", "Markdown"], ["csv", "CSV"], ["html", "HTML"], ["srt", "SRT Subtitles"], ["vtt", "WebVTT"]] as [fmt, label]} {#each templates as template}
<button <button
class="w-full text-left btn-md text-text-secondary hover:bg-hover hover:text-text" class="w-full text-left btn-md text-text-secondary hover:bg-hover hover:text-text
onclick={() => handleExport(fmt)} {activeTemplate === template.name ? 'text-accent' : ''}"
>{label}</button> onclick={() => applyTemplate(template)}
>{template.name}</button>
{/each} {/each}
</div> </div>
{/if} {/if}
</div> </div>
<span class="w-px h-4 bg-border-subtle flex-shrink-0"></span> {/if}
<!-- Task sidebar toggle --> <button
class="btn-md rounded-lg flex-shrink-0 whitespace-nowrap {aiProcessing ? 'text-warning' : 'text-accent hover:bg-accent/10 hover:text-accent font-medium'}"
onclick={manualExtractTasks}
disabled={aiProcessing || !transcript.trim()}
>{aiProcessing ? "Extracting..." : "Extract Tasks"}</button>
<span class="w-px h-4 bg-border-subtle flex-shrink-0"></span>
<button
class="btn-md rounded-lg text-text-secondary hover:bg-hover hover:text-text flex-shrink-0 whitespace-nowrap
{!transcript.trim() ? 'opacity-40 cursor-default' : ''}"
onclick={saveTypedText}
disabled={!transcript.trim()}
>Save</button>
<button
class="btn-md rounded-lg text-text-secondary hover:bg-hover hover:text-text flex-shrink-0 whitespace-nowrap"
onclick={copyAll}
>Copy</button>
<button
class="btn-md rounded-lg text-text-secondary hover:bg-hover hover:text-text flex-shrink-0 whitespace-nowrap"
onclick={clearTranscript}
>Clear</button>
<div class="relative flex-shrink-0">
<button <button
class="relative px-2 py-1.5 rounded-lg flex-shrink-0 class="btn-md rounded-lg text-text-secondary hover:bg-hover hover:text-text whitespace-nowrap"
{page.taskSidebarOpen ? 'bg-accent/10' : 'hover:bg-hover'}" onclick={() => { showExportMenu = !showExportMenu; showTemplateMenu = false; }}
onclick={() => page.taskSidebarOpen = !page.taskSidebarOpen} >Export</button>
aria-label="Toggle task sidebar" {#if showExportMenu}
> <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]">
<svg class="w-4 h-4 {page.taskSidebarOpen ? 'text-accent' : 'text-text-tertiary'}" viewBox="0 0 24 24" fill="currentColor"> {#each [["txt", "Plain Text"], ["md", "Markdown"], ["csv", "CSV"], ["html", "HTML"], ["srt", "SRT Subtitles"], ["vtt", "WebVTT"]] as [fmt, label]}
<path d="M9 11l3 3L22 4M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11" /> <button
</svg> class="w-full text-left btn-md text-text-secondary hover:bg-hover hover:text-text"
{#if taskCount > 0} onclick={() => handleExport(fmt)}
<span class="absolute -top-0.5 -right-0.5 text-[9px] px-1 py-0 rounded-full bg-accent text-white font-medium min-w-[14px] text-center leading-[14px]"> >{label}</button>
{taskCount} {/each}
</span> </div>
{/if} {/if}
</button>
</div> </div>
</div> </div>
<!-- Template indicator --> <!-- Template indicator -->
{#if activeTemplate} {#if activeTemplate}
<div class="px-7 pb-2 animate-fade-in"> <div class="px-5 pt-2 animate-fade-in flex-shrink-0">
<div class="flex items-center gap-2 px-4 py-1.5 rounded-lg bg-accent-subtle border border-accent/20"> <div class="flex items-center gap-2 px-4 py-1.5 rounded-lg bg-accent-subtle border border-accent/20">
<span class="text-[11px] text-accent font-medium">Template: {activeTemplate}</span> <span class="text-[11px] text-accent font-medium">Template: {activeTemplate}</span>
<span class="text-[11px] text-text-tertiary">Click a section, then record to fill it</span> <span class="text-[11px] text-text-tertiary">Click a section, then record to fill it</span>
@@ -650,7 +670,7 @@
<!-- Error --> <!-- Error -->
{#if error} {#if error}
<div class="px-7 pb-2 animate-fade-in"> <div class="px-5 pt-2 animate-fade-in flex-shrink-0">
<div class="px-4 py-2 rounded-lg bg-danger/10 border border-danger/20 text-[12px] text-danger"> <div class="px-4 py-2 rounded-lg bg-danger/10 border border-danger/20 text-[12px] text-danger">
{error} {error}
</div> </div>
@@ -658,12 +678,12 @@
{/if} {/if}
<!-- Transcript area --> <!-- Transcript area -->
<div class="flex-1 px-7 pb-3 min-h-0"> <div class="flex-1 px-5 pt-3 pb-3 min-h-0">
<Card classes="h-full flex flex-col"> <Card classes="h-full flex flex-col">
<textarea <textarea
bind:this={textareaEl} bind:this={textareaEl}
class="font-transcript flex-1 w-full bg-transparent text-text p-6 class="font-transcript flex-1 w-full bg-transparent text-text p-6
resize-none focus:outline-none placeholder:text-text-tertiary" resize-none focus:outline-none placeholder:text-text-tertiary min-h-0"
placeholder={activeTemplate ? "Click a section above, then press record..." : "Your words will appear here..."} placeholder={activeTemplate ? "Click a section above, then press record..." : "Your words will appear here..."}
bind:value={transcript} bind:value={transcript}
onclick={() => { showExportMenu = false; showTemplateMenu = false; }} onclick={() => { showExportMenu = false; showTemplateMenu = false; }}
@@ -671,23 +691,30 @@
aria-label="Transcript" aria-label="Transcript"
aria-live="polite" aria-live="polite"
></textarea> ></textarea>
</Card>
</div>
<!-- Bottom bar --> <!-- Status footer (inside transcript card) -->
<div class="flex items-center px-7 pb-5"> <div class="flex items-center justify-between px-6 pb-3 pt-1 flex-shrink-0">
<span class="text-[11px] text-text-tertiary"> <span class="text-[11px] text-text-tertiary">
{#if wordCount > 0} {#if wordCount > 0}
{wordCount} {wordCount === 1 ? 'word' : 'words'} {wordCount} {wordCount === 1 ? 'word' : 'words'}
{/if} {:else}
</span> 0 words
<div class="flex-1"></div> {/if}
{#if aiStatus} {#if insertPos >= 0 && page.recording}
<span class="text-[11px] text-accent animate-fade-in mr-3">{aiStatus}</span> · <span class="text-accent">inserting at cursor</span>
{/if} {/if}
<span class="text-[11px] text-text-tertiary"> </span>
{settings.formatMode} · {page.activeProfile === "None" ? "No profile" : page.activeProfile} <div class="flex items-center gap-2">
</span> {#if aiStatus}
<span class="text-[11px] text-accent animate-fade-in">{aiStatus}</span>
<span class="text-[11px] text-text-tertiary">·</span>
{/if}
<span class="text-[11px] text-text-tertiary">
{settings.formatMode} · {page.activeProfile === "None" ? "No profile" : page.activeProfile}
</span>
</div>
</div>
</Card>
</div> </div>
{/if} {/if}
</div> </div>