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}
<ModelDownloader modelSize={settings.modelSize.toLowerCase()} onComplete={onModelDownloaded} />
{:else}
<!-- Header -->
<div class="flex items-center gap-5 px-7 pt-6 pb-4">
<!-- Control strip -->
<div class="flex items-center gap-3 px-5 h-[56px] border-b border-border-subtle flex-shrink-0">
<!-- Record 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
{page.recording
? 'bg-danger animate-pulse-warm'
@@ -520,125 +520,145 @@
aria-label={page.recording ? "Stop recording" : "Start 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}
<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" />
</svg>
{: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}
</button>
<!-- Timer + label -->
<div class="flex flex-col flex-shrink-0">
<span class="text-[28px] font-bold text-text tabular-nums tracking-tight leading-none" style="font-variant-numeric: tabular-nums;">
{page.timerText}
</span>
<span class="text-[11px] text-text-tertiary mt-1">
{#if page.recording}
<span class="inline-flex items-center gap-1.5">
<span class="w-[6px] h-[6px] rounded-full bg-danger animate-pulse-soft"></span>
<span class="font-medium text-danger tracking-wide">REC</span>
{#if insertPos >= 0}
<span class="text-text-tertiary ml-1">inserting at cursor</span>
{/if}
</span>
{:else if modelLoading}
<span class="text-warning">Loading Whisper model...</span>
{:else if saved}
<span class="text-success animate-fade-in">
Saved to history{#if extractedCount > 0} · {extractedCount} task{extractedCount === 1 ? '' : 's'} extracted{/if}
</span>
{: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>
{/if}
</span>
<!-- Waveform placeholder (Phase 2: live visualiser) -->
<div class="flex-1 min-h-[32px] rounded-md border border-border-subtle bg-bg-elevated/40 flex items-center px-3">
{#if page.recording}
<span class="flex items-end gap-[3px] h-[18px]">
{#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="w-[2px] rounded-full bg-danger/70 animate-pulse-soft"
style="height: {h * 100}%; animation-delay: {i * 60}ms"
></span>
{/each}
</span>
{:else}
<span class="text-[11px] text-text-tertiary">
{#if modelLoading}
Loading model...
{:else if saved}
<span class="text-success animate-fade-in">
Saved{#if extractedCount > 0} · {extractedCount} task{extractedCount === 1 ? '' : 's'} extracted{/if}
</span>
{: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>
{/if}
</span>
{/if}
</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 -->
<div class="flex gap-0.5 flex-shrink-0">
<!-- Template selector -->
{#if templates.length > 0}
<div class="relative">
<button
class="btn-md rounded-lg text-text-secondary hover:bg-hover hover:text-text flex-shrink-0 whitespace-nowrap"
onclick={() => { showTemplateMenu = !showTemplateMenu; showExportMenu = false; }}
>Template</button>
{#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-[160px]">
{#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>
<!-- Status indicator -->
<span class="text-[11px] text-text-tertiary flex-shrink-0 min-w-[60px] text-right">
{#if page.recording}
<span class="inline-flex items-center gap-1.5">
<span class="w-[6px] h-[6px] rounded-full bg-danger animate-pulse-soft"></span>
<span class="font-medium text-danger tracking-wide">REC</span>
</span>
{:else if modelLoading}
<span class="text-warning">Loading</span>
{:else}
<span class="text-success">Ready</span>
{/if}
<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">
</span>
<!-- Separator -->
<span class="w-px h-4 bg-border-subtle flex-shrink-0"></span>
<!-- Task sidebar toggle -->
<button
class="relative px-2 py-1.5 rounded-lg flex-shrink-0
{page.taskSidebarOpen ? 'bg-accent/10' : 'hover:bg-hover'}"
onclick={() => page.taskSidebarOpen = !page.taskSidebarOpen}
aria-label="Toggle task sidebar"
>
<svg class="w-4 h-4 {page.taskSidebarOpen ? 'text-accent' : 'text-text-tertiary'}" viewBox="0 0 24 24" fill="currentColor">
<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" />
</svg>
{#if taskCount > 0}
<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]">
{taskCount}
</span>
{/if}
</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
class="btn-md rounded-lg text-text-secondary hover:bg-hover hover:text-text whitespace-nowrap"
onclick={() => { showExportMenu = !showExportMenu; showTemplateMenu = false; }}
>Export</button>
{#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]">
{#each [["txt", "Plain Text"], ["md", "Markdown"], ["csv", "CSV"], ["html", "HTML"], ["srt", "SRT Subtitles"], ["vtt", "WebVTT"]] as [fmt, label]}
class="btn-md rounded-lg text-text-secondary hover:bg-hover hover:text-text flex-shrink-0 whitespace-nowrap"
onclick={() => { showTemplateMenu = !showTemplateMenu; showExportMenu = false; }}
>Template</button>
{#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-[160px]">
{#each templates as template}
<button
class="w-full text-left btn-md text-text-secondary hover:bg-hover hover:text-text"
onclick={() => handleExport(fmt)}
>{label}</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>
<span class="w-px h-4 bg-border-subtle flex-shrink-0"></span>
<!-- Task sidebar toggle -->
{/if}
<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
class="relative px-2 py-1.5 rounded-lg flex-shrink-0
{page.taskSidebarOpen ? 'bg-accent/10' : 'hover:bg-hover'}"
onclick={() => page.taskSidebarOpen = !page.taskSidebarOpen}
aria-label="Toggle task sidebar"
>
<svg class="w-4 h-4 {page.taskSidebarOpen ? 'text-accent' : 'text-text-tertiary'}" viewBox="0 0 24 24" fill="currentColor">
<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" />
</svg>
{#if taskCount > 0}
<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]">
{taskCount}
</span>
{/if}
</button>
class="btn-md rounded-lg text-text-secondary hover:bg-hover hover:text-text whitespace-nowrap"
onclick={() => { showExportMenu = !showExportMenu; showTemplateMenu = false; }}
>Export</button>
{#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]">
{#each [["txt", "Plain Text"], ["md", "Markdown"], ["csv", "CSV"], ["html", "HTML"], ["srt", "SRT Subtitles"], ["vtt", "WebVTT"]] as [fmt, label]}
<button
class="w-full text-left btn-md text-text-secondary hover:bg-hover hover:text-text"
onclick={() => handleExport(fmt)}
>{label}</button>
{/each}
</div>
{/if}
</div>
</div>
<!-- Template indicator -->
{#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">
<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>
@@ -650,7 +670,7 @@
<!-- 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">
{error}
</div>
@@ -658,12 +678,12 @@
{/if}
<!-- 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">
<textarea
bind:this={textareaEl}
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..."}
bind:value={transcript}
onclick={() => { showExportMenu = false; showTemplateMenu = false; }}
@@ -671,23 +691,30 @@
aria-label="Transcript"
aria-live="polite"
></textarea>
</Card>
</div>
<!-- Bottom bar -->
<div class="flex items-center px-7 pb-5">
<span class="text-[11px] text-text-tertiary">
{#if wordCount > 0}
{wordCount} {wordCount === 1 ? 'word' : 'words'}
{/if}
</span>
<div class="flex-1"></div>
{#if aiStatus}
<span class="text-[11px] text-accent animate-fade-in mr-3">{aiStatus}</span>
{/if}
<span class="text-[11px] text-text-tertiary">
{settings.formatMode} · {page.activeProfile === "None" ? "No profile" : page.activeProfile}
</span>
<!-- Status footer (inside transcript card) -->
<div class="flex items-center justify-between px-6 pb-3 pt-1 flex-shrink-0">
<span class="text-[11px] text-text-tertiary">
{#if wordCount > 0}
{wordCount} {wordCount === 1 ? 'word' : 'words'}
{:else}
0 words
{/if}
{#if insertPos >= 0 && page.recording}
· <span class="text-accent">inserting at cursor</span>
{/if}
</span>
<div class="flex items-center gap-2">
{#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>
{/if}
</div>