From 0c8a7eb8e9668ff8aee004148f517fe471e9b17f Mon Sep 17 00:00:00 2001 From: Leonard Roussard Date: Fri, 29 May 2026 13:24:09 +0200 Subject: [PATCH 1/5] fix(ui): center toggle knob, surface hidden widget during STT, polish UI - Toggle: center the knob via flexbox padding instead of magic offsets, stop the shadow being clipped, add focus-visible ring + reduced-motion - Widget: show the hidden island while recording and restore the configured visibility on idle; respect widgetVisible at startup - Polish: tabular-nums stats, balanced headings, font smoothing, global reduced-motion, animated Select dropdown with focus-visible Co-Authored-By: Claude Opus 4.8 (1M context) --- src-tauri/src/agent/pipeline.rs | 34 +++++++++++++++++++ src-tauri/src/lib.rs | 15 +++++++-- src/dashboard/stats/stats-row.tsx | 2 +- src/index.css | 45 +++++++++++++++++++++++++ src/shared/components/select/select.tsx | 11 +++--- src/shared/components/toggle.tsx | 28 ++++++++++----- 6 files changed, 119 insertions(+), 16 deletions(-) diff --git a/src-tauri/src/agent/pipeline.rs b/src-tauri/src/agent/pipeline.rs index feaf03e..4080c30 100644 --- a/src-tauri/src/agent/pipeline.rs +++ b/src-tauri/src/agent/pipeline.rs @@ -485,9 +485,43 @@ fn hot_swap_llm(app_handle: &AppHandle, model_id: &str) -> Result<(), AppError> } fn emit_state(app_handle: &AppHandle, state: &str) -> Result<(), tauri::Error> { + // The widget must be visible while it is actively reflecting a recording, + // even when the user has hidden it. We force it on when recording begins + // and restore the configured visibility once we settle back to idle. + match state { + "recording" => show_widget(app_handle), + "idle" => restore_widget_visibility(app_handle), + _ => {} + } app_handle.emit("agent-state-changed", serde_json::json!({ "state": state })) } +/// Force the widget window visible (used when recording starts). +fn show_widget(app_handle: &AppHandle) { + if let Some(widget) = app_handle.get_webview_window("widget") { + let _ = widget.show(); + } +} + +/// Restore the widget to the user's configured visibility. Called when the +/// pipeline returns to idle, so a hidden widget that we surfaced for recording +/// gets hidden again. +fn restore_widget_visibility(app_handle: &AppHandle) { + let state = app_handle.state::(); + let visible = state + .db + .lock() + .ok() + .and_then(|db| db.get_config("widgetVisible").ok().flatten()) + .map_or(true, |v| v == "true"); + + if !visible { + if let Some(widget) = app_handle.get_webview_window("widget") { + let _ = widget.hide(); + } + } +} + fn parse_retention_bytes(value: &str) -> Option { match value { "100mb" => Some(104_857_600), diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 746db49..3345cc4 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -176,7 +176,7 @@ pub fn run() { if onboarding_done { if let Some(widget) = app.get_webview_window("widget") { - let (position, size) = { + let (position, size, visible) = { let db = state.db.lock().unwrap(); let pos = db .get_config("widgetPosition") @@ -188,11 +188,20 @@ pub fn run() { .ok() .flatten() .unwrap_or_else(|| "medium".to_string()); - (pos, sz) + let vis = db + .get_config("widgetVisible") + .ok() + .flatten() + .map_or(true, |v| v == "true"); + (pos, sz, vis) }; let _ = window::widget::apply_position_and_size(&widget, &position, &size); - let _ = widget.show(); + // Respect the user's hide preference at startup. When hidden, + // the pipeline still surfaces the widget during recording. + if visible { + let _ = widget.show(); + } } // Load STT model in background — but only if the user is on diff --git a/src/dashboard/stats/stats-row.tsx b/src/dashboard/stats/stats-row.tsx index 73fee36..9c7b78a 100644 --- a/src/dashboard/stats/stats-row.tsx +++ b/src/dashboard/stats/stats-row.tsx @@ -26,7 +26,7 @@ export default function StatsRow() {
{items.map((stat) => ( -
+
{stat.value}
{stat.label}
diff --git a/src/index.css b/src/index.css index 1bbebb5..6c7fd36 100644 --- a/src/index.css +++ b/src/index.css @@ -28,12 +28,57 @@ font-size: 14px; line-height: 1.5; -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + text-rendering: optimizeLegibility; } #root { height: 100%; } + /* Tighter, balanced headings — large text wants negative tracking. */ + h1, + h2, + h3 { + text-wrap: balance; + letter-spacing: -0.011em; + } + + /* Dynamic figures (stats, counters, durations) must not jitter as they + change width. Opt in with `tabular-nums`. */ + .tabular-nums { + font-variant-numeric: tabular-nums; + } + + /* Subtle entrance for popovers/menus — origin-aware, ease-out. */ + @keyframes pop-in { + from { + opacity: 0; + transform: scale(0.96) translateY(-2px); + } + to { + opacity: 1; + transform: scale(1) translateY(0); + } + } + + .animate-pop-in { + animation: pop-in 0.14s cubic-bezier(0.16, 1, 0.3, 1); + transform-origin: top; + } + + /* Respect the user's motion preference everywhere by default. */ + @media (prefers-reduced-motion: reduce) { + *, + *::before, + *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + } + } + #root[data-window="main"] { background-color: var(--color-bg-primary); border-radius: 12px; diff --git a/src/shared/components/select/select.tsx b/src/shared/components/select/select.tsx index 908f968..da196b8 100644 --- a/src/shared/components/select/select.tsx +++ b/src/shared/components/select/select.tsx @@ -47,7 +47,9 @@ export default function Select({ type="button" onClick={() => setOpen(!open)} className={cn( - "flex items-center gap-2 bg-bg-secondary border border-border rounded-lg px-3 py-1.5 text-sm text-text-primary transition-colors hover:border-text-tertiary", + "flex items-center gap-2 bg-bg-secondary border border-border rounded-lg px-3 py-1.5 text-sm text-text-primary", + "transition-colors duration-150 ease-out motion-reduce:transition-none hover:border-text-tertiary", + "focus-visible:outline-none focus-visible:border-accent focus-visible:ring-2 focus-visible:ring-accent/20", fullWidth && "w-full justify-between h-9", open && "border-accent ring-2 ring-accent/20", )} @@ -67,8 +69,8 @@ export default function Select({ {open && (
{options.map((opt) => ( @@ -80,7 +82,8 @@ export default function Select({ setOpen(false); }} className={cn( - "w-full text-left px-3 py-2 rounded-md text-sm transition-colors flex items-start gap-2", + "w-full text-left px-3 py-2 rounded-md text-sm flex items-start gap-2", + "transition-colors duration-150 ease-out motion-reduce:transition-none", opt.value === value ? "bg-accent-subtle text-accent" : "text-text-primary hover:bg-bg-tertiary", diff --git a/src/shared/components/toggle.tsx b/src/shared/components/toggle.tsx index 408cec3..505f12b 100644 --- a/src/shared/components/toggle.tsx +++ b/src/shared/components/toggle.tsx @@ -9,28 +9,40 @@ interface ToggleProps { export default function Toggle({ checked, onChange, label, disabled }: ToggleProps) { return ( -