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:
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user