diff --git a/CLAUDE.md b/CLAUDE.md index 2ed340b..cec92c7 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -14,8 +14,10 @@ Read these IN ORDER before writing any code in a new phase: 1. **`docs/PRD.md`** — what we're building and why. Authoritative for scope, non-goals, acceptance criteria. The product contract. 2. **`docs/TECH_SPEC.md`** — how. Prescriptive — every architectural decision lives here (file layout, threading, P/Invoke, naming, dependencies). -3. **`docs/ROADMAP.md`** — what's deferred past v1.0. "Phase 2+", "deferred to ROADMAP", or any v1.1+ heading = **NOT in scope right now.** -4. **`docs/PROCESS.md`** — the gate-driven workflow. Follow it exactly. +3. **`docs/APP_DESIGN.md`** — full visual + interaction spec for every user-facing surface (pill, MainWindow, onboarding, tray). Tokens, components, layouts. Use it for Phase 9+ UI. +4. **`docs/PILL_DESIGN.md`** — pill-specific spec, including the **§10 hover-extend override** (close + settings buttons on hover). Where it conflicts with APP_DESIGN §2 (click-through), **PILL_DESIGN §10 wins** — user-confirmed override. +5. **`docs/ROADMAP.md`** — what's deferred past v1.0. "Phase 2+", "deferred to ROADMAP", or any v1.1+ heading = **NOT in scope right now.** +6. **`docs/PROCESS.md`** — the gate-driven workflow. Follow it exactly. **Conflict resolution:** PRD §scope decisions outrank TECH_SPEC §implementation decisions outrank anything else. If you find a contradiction between docs, surface it before picking a side. Never quietly resolve a conflict. @@ -100,7 +102,12 @@ When a gate forces a deliberate deviation from TECH_SPEC's literal text, log it - **`src/KusPus.Audio/AudioRecorder.cs`** — same CA1848/CA1873 suppression as the other layers (start/stop/cap logging only). - **`src/KusPus.Audio/IAudioRecorder.cs`** — exposes `event EventHandler? DefaultDeviceChanged` rather than the spec §14 prose "surface error in pill" (the spec describes a behaviour, not a mechanism). Event lets the Coordinator subscribe in Phase 6 and translate to pill state. - **`src/KusPus.Audio/AudioRecorder.cs` (REVERTED 2026-05-16)** — Phase 4 originally used `MediaFoundationResampler` instead of the spec §14 `WdlResamplingSampleProvider` chain. Phase 6 manual smoke uncovered why MF was the wrong choice: it pads the output buffer with zeros when the source has no fresh data, so the worker writes target-format bytes at SSD speed (~500× realtime) until the disk fills. Reverted to the spec-prescribed `ToSampleProvider → ToMono → WdlResamplingSampleProvider(16k) → SampleToWaveProvider16` pull-based chain — WDL returns 0 when source is empty, so the worker correctly stalls. -- **`src/KusPus.App/FloatingPillWindow.xaml.cs`** — **PRD G4 deviation (intentional, dev-only):** pill is visible at ALL times during this dev window, showing the current FSM state ("KusPus (idle)", "Armed…", "Recording…", "Transcribing…", "Cancelled"). PRD G4 says "visible only when in use" and the spec'd behaviour returns in Phase 8 alongside acrylic + visualizer + fade animations. Always-visible is intentional now so the developer can verify the FSM is reaching the right states without parsing logs. +- **`src/KusPus.App/FloatingPillWindow.xaml.cs`** — **PRD G4 deviation (user-requested, ongoing):** pill stays visible between dictations, showing an "Idle" content state (faint SVG icon + "KusPus" label). PRD G4 + APP_DESIGN §2.4 say no idle pill — user explicitly wants always-visible until the Settings modal exposes the close path through the tray. Will revert to the spec'd hidden-when-not-in-use behaviour once Phase 9's tray "Preferences…" / "Quit" items make discoverability acceptable. +- **`src/KusPus.App/FloatingPillWindow.xaml{,.cs}` — APP_DESIGN §2 / PILL_DESIGN.md fully implemented in Phase 8.** 200×56 Mica surface, 8 px DWM rounded corners, §3.1 dark gradient, §3.3 hairline border + drop shadow + inner highlight, five-state machine (Hidden/Recording/Transcribing/Confirmed/Error), §4.2 damped-target visualizer (20 bars × 3 px × 4 px gap, real audio via `IAudioRecorder.Levels`), 14 px ¾-arc spinner, 136 × 1.5 mint accent line with per-state opacity, 120 / 150 ms motion. `WS_EX_NOACTIVATE | WS_EX_TOOLWINDOW` for focus-proof + hidden-from-shell. Per-monitor DPI math via `MonitorFromWindow` + `GetDpiForMonitor`. +- **`src/KusPus.App/FloatingPillWindow.xaml{,.cs}` — `PILL_DESIGN.md §10` hover-extend override (user-requested):** pill width animates 200 → 280 on hover, revealing Settings (placeholder) + Close buttons. `WS_EX_TRANSPARENT` therefore NOT applied (overrides APP_DESIGN §2.1 / §8.3 click-through). Drag-anywhere-except-buttons via `Window.DragMove` with `VisualTreeHelper`-based button-source detection. +- **Draggable pill + hybrid-sticky multi-monitor (beyond spec, user-requested):** session-only `Dictionary` keyed by `MONITORINFOEX.szDevice` remembers per-monitor positions. On `Armed`/`Recording` transitions, `EnsurePillOnForegroundMonitor` jumps the pill to the focused window's monitor (at remembered or default position). Dictionary cleared every fresh process start per user spec. +- **`src/KusPus.App/AppCoordinator.cs` + `src/KusPus.Core/State/CoordinatorSnapshot.cs`** — added `PostPasteInfo (Pasted, TargetApp, ErrorReason)` on the snapshot. `AppCoordinator.EmitPostPasteSnapshot` fires one extra snapshot from `DeliverAsync` / `HandleFailureAsync` so the pill can render Confirmed (1 s) or Error (2 s) per APP_DESIGN §2.4. +- **`src/KusPus.App/KusPus.App.csproj`** — adds `SharpVectors.Wpf 1.8.5` so the pill Idle state can load `icons/icon.svg` via `` rather than hand-converted XAML. Same SVG file is the single source for the tray, taskbar, Task Manager, and .exe icon (via `tools/IconBuilder` → `icons/icon.ico`). SVG `viewBox` tightened to `272 246 480 480` so the 5-bar content fills ~94 % of every rendered frame instead of ~44 %. - **`src/KusPus.Audio/AudioRecorder.cs`** — RMS is computed on the worker thread over each 1/15s output frame rather than "in the WASAPI callback over 20 chunks per buffer" per §14. User-visible behaviour is identical (20-channel array at 15 Hz). The worker location avoids doing math on the capture thread (NAudio holds a lock that delays the next callback). - **`src/KusPus.Audio/RecordedFile.cs`** — adds `bool CappedAtLimit = false` so Phase 6 can surface the spec §14 "Recording capped at 50 min" pill message. Optional ctor parameter; pre-existing callers continue to compile. - **`test/KusPus.Native.Tests/`** — 5th test project (spec §1 lists 3). Pure-helper coverage for `PasteEngine` (terminal-detect, friendly-name, SendInput payload) and `HotkeyEngine.ProcessKey` chord state machine. LL-hook itself and end-to-end paste are Phase 6 manual smoke per §33. @@ -110,15 +117,39 @@ When a gate forces a deliberate deviation from TECH_SPEC's literal text, log it - **`src/KusPus.Native/PasteEngine.cs`** — `IClipboardWriter` injection point. Spec §16 calls `Clipboard.SetText` directly, which requires WPF in the Native project. Decoupling via an interface keeps `KusPus.Native` free of WPF deps; the WPF impl ships in Phase 6's `KusPus.App` composition root. - **`src/KusPus.Native/JobObjectContainer.cs`** — owns the Job Object handle. Spec §15 says "created in AppCoordinator and injected into WhisperRunner via DI"; in practice the handle lives in this class which implements `IProcessContainer` and is injected as the `WhisperRunner._onProcessStarted` callback. Functionally equivalent. - **`src/KusPus.Native/HotkeyEngine.cs`** — Spec §13 step 6 prescribes a `Channel` between the LL callback and the subject. Real impl emits directly to `Subject` AFTER releasing the state lock, which avoids the deadlock-under-callback risk without the channel allocation/dispatch overhead. At v1's input rate (a handful of events per chord activation) this is well within the `LowLevelHooksTimeout` budget. Revisit if the watchdog observes timeouts. +- **`src/KusPus.Native/HotkeyEngine.cs` — LWin keyup is NOT consumed** (spec §13 says it should be; that turned out to be a bug). The Ctrl-tap injection still runs (AHK `#MenuMaskKey` idiom — masks the Start menu) but the real LWin keyup is allowed to reach the OS so its internal "Win is held" state clears. Consuming the keyup left Win stuck-down — every later `SendInput(Ctrl+V)` from PasteEngine read as `Win+Ctrl+V` (opens Action Center / Quick Settings) and every later keystroke became a `Win+key` system shortcut. Validated by AutoHotkey community docs + PowerToys issue #35345 / #18175. **Spec §13 needs an update** when the user is reviewing — flagged in conversation, not edited here per CLAUDE.md rule. - **`src/KusPus.Whisper/WhisperRunner.cs`** — empty `expectedWhisperSha256` now skips the integrity check (dev mode). Phase 12 release builds populate the SHA from `installer/payload/whisper/SHA256SUMS`. -- **`src/KusPus.App/`** — Phase 6 milestone code. Several v1-only simplifications, all to be revisited in their planned phases: - - Pill: plain `Topmost`/click-through/`WS_EX_NOACTIVATE` window with a single status text; no acrylic, no animations, no visualizer (Phase 8). Position math uses `System.Windows.Forms.Screen.FromPoint` (good enough for one-monitor dev; Phase 8 switches to `MonitorFromPoint` + `GetDpiForMonitor` for per-monitor DPI). - - No MainWindow yet (Phase 9). Tray menu has Toggle + Quit only. - - Single-instance "bring main to front" broadcast deferred until MainWindow exists (Phase 9). Second launch just exits silently for now. - - `UseWindowsForms=true` alongside `UseWPF=true` to get `NotifyIcon`-style tray and `Screen.FromPoint`. Triggers ambiguous-`Application` references — App uses `using WpfApplication = System.Windows.Application` and qualifies the partial-class base type. +- **`src/KusPus.App/`** — Phase 6 milestone code. Several v1-only simplifications: + - Pill: built out in Phase 8 per `docs/PILL_DESIGN.md` + APP_DESIGN §2. The Phase 6 plain-Topmost/text-only pill is gone; see the Phase 8 entries above for the current state. + - MainWindow under construction (Phase 9 — starting now). Tray menu has Toggle + Quit until 9A adds Preferences / History items. + - Single-instance "bring main to front" broadcast still deferred until the WndProc message handler lands with MainWindow. Second launch exits silently for now. + - `UseWindowsForms=true` alongside `UseWPF=true` to get `NotifyIcon`-style tray and `Screen.FromPoint`. Triggers ambiguous-`Application` / `Button` / `Rectangle` / `Color` / `Point` references — App uses local aliases (e.g. `WpfRectangle`, `WpfPoint`) and fully qualifies where needed. - `KUSPUS_WHISPER_DIR` and `KUSPUS_WHISPER_SHA256` env vars override the default `{app}\whisper` path + skip-integrity-check dev mode. Phase 12 installer sets these from real values. - - Sentry, autostart registry write, onboarding modal — all deferred to Phases 10/11. + - Sentry, autostart registry write, onboarding modal — deferred to Phases 10/11; autostart toggle UI lands in Phase 9 General tab. - `CA1001` suppressed on `App` (it owns disposable fields but inherits from `System.Windows.Application` which isn't `IDisposable`; cleanup happens in `OnExit`). +- **`src/KusPus.Core/Networking/EgressPolicy.cs` + `src/KusPus.App/EgressAllowlistHandler.cs` (Phase 11)** — Sentry's regional ingest hosts (`o.ingest.de.sentry.io`, `o.ingest.us.sentry.io`) are accepted in addition to the spec-literal `ingest.sentry.io`. Recognised by "host ends in `.sentry.io` AND contains an `ingest` label" — covers every documented region without enumerating them. **PRD §10.2 needs an update** to mention regional ingest; flagged here per CLAUDE.md rule. Driver: the user-supplied EU DSN. +- **`src/KusPus.App/CrashReporter.cs` (Phase 11)** — embeds the project's EU Sentry DSN as `internal const string DefaultDsn` with `KUSPUS_SENTRY_DSN` env-var override. Spec §19 says "``"; const-in-source is functionally equivalent for the friends-only audience and avoids a MSBuild property indirection. DSNs are not secrets per Sentry's own docs. +- **`src/KusPus.App/CrashReporter.cs` (Phase 11)** — Sentry's own HTTP transport is routed through `EgressAllowlistHandler` via `SentryOptions.CreateHttpMessageHandler`. Without this, PRD §10.2's "All HttpClient instances in the codebase route through a single factory" promise would be unmet for the Sentry SDK. Belt-and-suspenders with `ShutdownSdk()` on Offline-Mode flips so an in-flight Sentry upload also gets blocked. +- **`src/KusPus.App/CrashReporter.cs` (Phase 11)** — breadcrumb scrubbing runs in `SetBeforeBreadcrumb`, not inside `BeforeSend`. Sentry 5.0 exposes `SentryEvent.Breadcrumbs` as `IReadOnlyCollection`, so mutation from inside `BeforeSend` is impossible. Scrubbing at add-time is the supported hook and matches the spec intent. +- **`src/KusPus.Core/Telemetry/CrashScrubber.cs` (Phase 11)** — env-var ordering is `TEMP → LOCALAPPDATA → APPDATA → USERPROFILE`. On Windows, `%TEMP%` is `%LOCALAPPDATA%\Temp` so the more-specific prefix must win, otherwise `%TEMP%`-rooted paths get prefixed as `%LOCALAPPDATA%\Temp\…` and lose the most-specific signal. Same logic for LOCALAPPDATA/APPDATA before USERPROFILE. +- **`src/KusPus.Core/Telemetry/CrashScrubber.cs` (Phase 11)** — `ScrubString` does mid-string replacement (free-form text where the path is embedded in a sentence), `ScrubPath` is start-anchored (fields whose value IS a path — stack-frame `AbsolutePath`, `FileName`). Two functions because mid-string substring replacement would corrupt a stack frame's relative-path field that legitimately contains `\Temp\` as a directory component. +- **`tools/verify-egress-allowlist.ps1` — NOT implemented in Phase 11.** Phase 11 enforces the allowlist at runtime via `EgressAllowlistHandler`; the static-analysis pre-commit script is a belt-and-suspenders dev-loop tool the spec calls out. Deferred to a follow-up; the existing stub still throws on invocation. +- **`KusPus.App.Tests` project — NOT added in Phase 11.** `BeforeSend` (internal static in `CrashReporter`) has no direct unit tests; coverage comes via `CrashScrubber` tests + manual smoke. Adding a 6th test project for one internal-static is more scaffolding than value at v1's audience size. + +### Dogfood-driven UX deviations (2026-05-17 session) + +- **`src/KusPus.App/FloatingPillWindow.xaml{,.cs}` — Pin is now a "compact-mode + position lock" toggle, not just "latch open"** (user-requested). Pin click: contract pill to 200×56 + slide dock back + keep pin button always visible at angle=0 (mint-tinted). While pinned, hover only swaps SVG↔visualizer content (no resize, no dock); position drag is disabled. `CompactRecordButton` (top-LEFT corner, 18×18 + Radius=4 matching the right cluster) is the persistent record affordance in pinned mode. Cursor flips `SizeAll`↔`Arrow` on pin to telegraph the lock. APP_DESIGN §2.4 / PILL_DESIGN §10 originally framed pin as "latch open" — flagged for spec update. +- **`src/KusPus.App/FloatingPillWindow.xaml{,.cs}` — Record-toggle nudge popup** (10s timer, sole dismissal path). Earlier 6s timer was a red herring: a `TransitionTo`'s "dismiss-on-Recording" rule fired within ~ms of click since the FSM moves to Recording immediately after `ToggleFromTray`. That rule is now removed — comment in `Render()` explains. Nudge says "Click into your text field" so the user knows the paste goes wherever focus is at transcribe-end. +- **`src/KusPus.App/FloatingPillWindow.xaml.cs` — `SetRecordToggleAction(Action)`** added. App.OnStartup wires `_coordinator.ToggleFromTray`. Per user spec the toggle does NOT auto-capture a foreground HWND — paste lands wherever focus is at transcribe-end. Tap-mode label is **"Toggle Recording [BETA]"** on both the dock button and tray menu. +- **`src/KusPus.App/TrayManager.cs` + `src/KusPus.App/TrayMenuWindow.xaml{,.cs}` — Custom WPF tray right-click menu** replaces the WinForms `ContextMenuStrip`. Matches user-supplied `Tray_light.png`/`Tray_dark.png`: rounded `Surface` card + drop shadow + design-system tokens. KusPus header with state-aware "Version 1.0.0 · {Idle|Recording|Transcribing}" subtitle, "Toggle Recording [BETA]" with hotkey keycap (live-bound to PrefsStore.Hotkey), "Active model: {name}" with chevron (opens models tab), Preferences, History, red Quit. Shows at cursor on right-click; closes on `Deactivated`. `WS_EX_TOOLWINDOW` hides from Alt-Tab/taskbar. +- **`icons/icon-{idle,recording,error}.{svg,ico}` + `src/KusPus.App/TrayManager.cs` — State-aware tray icons.** Recording overlays a red dot + glow on the bars; Error overlays a red warning triangle. `tools/IconBuilder` generates the multi-resolution .ico files from each SVG. `TrayManager` subscribes to `AppCoordinator.State` and swaps `NotifyIcon.Icon` based on FSM state (treating a failed `PostPaste` snapshot as Error for its hold duration). +- **`src/KusPus.Core/Settings/AppSettings.cs` — Default `UiSettings.Theme = "dark"`** (was `"auto"`). User dogfood feedback: light theme is still in beta polish, so new installs land on the polished dark surface. `MainWindow.xaml` Light radio shows `"Light [BETA]"` with tooltip explaining beta state. `DefaultSettingsTests` assertion updated. **APP_DESIGN.md theme defaults section needs an update** — flagged for spec revision. +- **`src/KusPus.App/OnboardingWindow.xaml{,.cs}` — Step 6 (Try it) replaced fake `SimulatedSentences` with real `IAudioRecorder` + `IWhisperRunner` pipeline.** 5s countdown record → transcribe with active model → render actual transcript (or error). Threaded audio/whisper/models services through the OnboardingWindow constructor + both call sites (`App.OnStartup` + `MainWindow.OnRerunOnboarding`). Surfaces broken-mic / missing-model failures during setup instead of after onboarding finishes. +- **`src/KusPus.App/OnboardingWindow.xaml{,.cs}` — Step 3 adds `OnbInputDeviceCombo`** (the mic chooser). Writes to the same `PrefsStore.Audio.InputDeviceId` field that Preferences → Audio uses, so the selection persists until the user changes it from either surface. `ResolveOnbMicDevice` mirrors `MainWindow.ResolveLevelMeterDevice` — falls back to OS default if the saved id doesn't resolve. Live meter card re-binds when the user changes selection. +- **`src/KusPus.App/Styles/Tokens.xaml` — added `Icon.Glyph=11` + `Icon.Chevron=9` double tokens** for unified icon sizing across pill / tray / dock chrome. Replaces literal `FontSize="10/11/8"` scattered across button glyphs. Bound via `{StaticResource Icon.Glyph}` / `{StaticResource Icon.Chevron}`. +- **`src/KusPus.App/FloatingPillWindow.xaml` — Pill drop shadow softened** to `ShadowDepth=0 BlurRadius=14 Opacity=0.25` (was `Depth=2 Blur=32 Opacity=0.45`) per UX audit Fix #9. Omnidirectional soft halo so the shadow doesn't bleed onto the dock when the drawer is open. Inner `PillInnerHighlight` Rectangle removed per Fix #7 — only existed on the pill (not the dock), created a seam at the drawer junction. All chrome gutters standardised at 6 px per Fix #10. +- **`src/KusPus.App/FloatingPillWindow.xaml` — `MicChooserButton` hover** swapped from `Opacity=1.4` (silent no-op — WPF clamps at 1) to `Background={DynamicResource SurfaceElevated}` via Setter. Real, theme-aware lift. +- **`docs/ROADMAP.md` — added R1.2-10 long-mode chunk-on-VAD streaming entry** (2026-05-17). User dogfood feedback asked for continuous "speak-pause-paste" loop; researched 3 architectures (sliding-window vs chunk-on-VAD vs library-binding) and recommended chunk-on-VAD on a second hotkey ("Option B"). Deferred to v1.2 — ~2 weeks build + 1 week dogfood, too large for the pre-v1 polish window. Entry includes 8-cluster plan + risk register + latency expectations. Append to this list (don't replace) when a new deviation lands. diff --git a/docs/APP_DESIGN.md b/docs/APP_DESIGN.md new file mode 100644 index 0000000..5d2f4cb --- /dev/null +++ b/docs/APP_DESIGN.md @@ -0,0 +1,1774 @@ +\# KusPus — Full Design Specification + + + +> This document is the single source of truth for the KusPus visual + interaction system. Hand it to a designer, an image generator, or another AI to reproduce the design across all surfaces. It is implementation-agnostic — translate the tokens and components into whatever framework you build with. + + + +\--- + + + +\## 0. Product Summary + + + +\*\*KusPus\*\* is a lightweight, local-first \*\*Windows dictation utility\*\*. + + + +> Press a hotkey. Speak. The transcript pastes into whatever app has focus. + + + +\- \*\*Audience.\*\* Power users, developers, writers — people who type all day and would rather not. + +\- \*\*Stack constraint.\*\* Native Windows 11 (Mica/Acrylic). Windows 10 supported as a fallback. + +\- \*\*No cloud.\*\* No accounts, no browser UI, no telemetry by default. + +\- \*\*Surface count.\*\* Three user-facing surfaces total: the \*\*floating pill\*\*, the \*\*main window\*\*, and the \*\*first-launch onboarding\*\*. Plus a \*\*tray icon + menu\*\*. + + + +The brand temperament is \*\*quiet, instant, native, calm, voice.\*\* If a decision would make the app feel louder than the OS shell around it, the decision is wrong. + + + +\--- + + + +\## 1. Brand Identity + + + +\### 1.1 The mark + + + +The KusPus mark is a \*\*voice stack\*\*: five vertical pearly-mint pills arranged in a \*\*center-heavy envelope silhouette\*\* — tallest in the middle, shorter at the edges. Each bar has a faint mint glow ellipse underneath, suggesting reflected light from the visualizer in the product. + + + +\*\*Geometry (in a 1024 × 1024 artboard):\*\* + + + +| Bar | x | y | width | height | corner radius | + +|---|---|---|---|---|---| + +| 1 (outer) | 288 | 402 | 64 | 146 | 32 | + +| 2 | 384 | 332 | 64 | 286 | 32 | + +| 3 (center) | 480 | 260 | 64 | 424 | 32 | + +| 4 | 576 | 332 | 64 | 286 | 32 | + +| 5 (outer) | 672 | 402 | 64 | 146 | 32 | + + + +\- \*\*Fill:\*\* vertical gradient `#F5F5F2` (top) → `#EEF4EE` (72 %) → `#CDEFD9` (bottom). Pearly, not pure white. + +\- \*\*Glow:\*\* under each bar, a horizontal ellipse filled `#4DDBA6` at 18–20 % opacity, blurred 10 px stdDev, color-matrixed to mint. Glow ellipses sit slightly \*below\* the bar baseline. + +\- \*\*Container:\*\* rounded square with corner radius \*\*22 % of the side\*\*. Background `#1C1C1E` on dark, `#F3F3F3` on light. + + + +\### 1.2 The wordmark + + + +\- Spelled \*\*`KusPus`\*\* — capital K, capital P. \*\*Mid-string cap is intentional\*\*; never flatten to `Kuspus` or `KUSPUS`. + +\- \*\*Typeface:\*\* Segoe UI Variable, weight 600. Fallback: Segoe UI 600, then Inter 600. + +\- \*\*Tracking:\*\* −0.5 % to −1 %. + +\- \*\*Color:\*\* primary text token (white on dark, near-black on light) — \*\*never mint\*\*. + +\- \*\*Lockup:\*\* when paired with the icon horizontally, the gap between them = 0.5 × the icon side. + +\- \*\*Optional motif:\*\* a 1.5 px mint underline beneath the wordmark, \*\*136 px wide\*\*, gradient-faded at both ends — same accent stroke used over the pill's visualizer. + + + +\### 1.3 Color palette + + + +| Token | Value | Where used | + +|---|---|---| + +| \*\*Mint\*\* \*(primary brand)\* | `#4DDBA6` | Accent line, selected states, brand highlights, success affirmations | + +| Mint glow | `rgba(77,219,166,0.22)` | Soft outer glows behind mint elements | + +| Charcoal | `#1E1E1E` | Pill surface fill (dark, effective at 90 % opacity) | + +| App background (dark) | `#202020` | Main window body | + +| Surface (dark) | `#2A2A2C` | Cards, inputs in dark mode | + +| Surface elevated (dark) | `#323234` | Hover state, dropdowns | + +| App background (light) | `#F3F3F3` | Main window body (light) | + +| Surface (light) | `#FFFFFF` | Cards in light mode | + +| Error red | `#EF5350` | Error states, danger buttons | + +| Success | `#4CAF50` | OK indicators in history, mic detect | + +| Warning amber | `#FFB74D` | Shortcut conflict warnings | + + + +\*\*Rules:\*\* + +\- Mint is the \*\*only saturated color in normal flow\*\*. If you find yourself reaching for a second brand color, stop. + +\- Error red is reserved for \*\*the error state only\*\* — not for emphasis, not for delete buttons-at-rest. A "Delete" button is ghost-styled until destructive intent is being shown (e.g., a confirmation modal). + +\- Never invent gradients beyond the icon's pearly-mint gradient and the pill's surface gradient. No marketing-style hero gradients anywhere. + + + +\### 1.4 Type system + + + +Single family — \*\*Segoe UI Variable\*\* (fallback chain: `"Segoe UI Variable", "Segoe UI", -apple-system, system-ui, sans-serif`). One monospace family for code/keys: `ui-monospace, "Cascadia Mono", "Cascadia Code", "SF Mono", Menlo, monospace`. + + + +| Role | Size | Weight | Letter-spacing | Notes | + +|---|---|---|---|---| + +| Hero / wordmark | 56 px | 600 | −0.03 em | Brand only | + +| Window title | — | — | — | Use system title bar | + +| Step title (onboarding) | 22 px | 600 | −0.02 em | | + +| Section title | 13 px | 600 | −0.005 em | Inside main window | + +| Row title | 13 px | 500 | −0.005 em | Inside list cards | + +| Body | 13 px | 500 | −0.005 em | Default copy | + +| Body small | 12.5 px | 500 | −0.005 em | Error pill text | + +| Subtitle / secondary | 11.5–12 px | 400 | 0 | Helper copy | + +| Micro-label (`RECORDING`) | 9.5 px | 500 | 0.08 em uppercase | Under visualizer | + +| Eyebrow | 10.5 px | 600 | 0.1 em uppercase | Onboarding step counter | + +| Mono (keys, timestamps) | 10–14 px | 500 | 0 | Cascadia Mono / monospace | + + + +\--- + + + +\## 2. Surface 1 · The Floating Pill + + + +99 % of daily interaction. Visible only when the app is in a dictation cycle; otherwise the OS shouldn't show it exists. + + + +\### 2.1 Anatomy + + + +| Property | Value | + +|---|---| + +| Width | \*\*200 px\*\* logical (fixed across all states) | + +| Height | \*\*56 px\*\* | + +| Corner radius | \*\*8 px\*\* — Windows 11 surface rounding, \*\*not iOS super-ellipse\*\* | + +| Material | Mica/Acrylic on Win11; solid translucent fill on Win10 | + +| Position | Bottom-center of the active monitor, \*\*40 px above the taskbar\*\* | + +| Z-order | Always-on-top | + +| Interaction | \*\*Click-through.\*\* Mouse events pass through; keyboard focus never moves | + +| Taskbar / Alt-Tab | Hidden from both | + +| Inner horizontal padding | 32 px each side (content track = 136 px) | + +| Shadow (dark) | `0 12 32 rgba(0,0,0,0.45) + 0 2 6 rgba(0,0,0,0.35) + 1 px inset border` | + +| Shadow (light) | `0 12 32 rgba(0,0,0,0.14) + 0 2 6 rgba(0,0,0,0.08) + 1 px inset border` | + +| Top inner highlight | 1 px line, `rgba(255,255,255,0.05)` dark / `rgba(255,255,255,0.7)` light | + + + +\### 2.2 The accent line + + + +A thin glowing horizontal stroke at the top edge — the only chrome on the pill. + + + +| Property | Value | + +|---|---| + +| Width | \*\*136 px\*\* (hugs the visualizer) | + +| Height | 1.5 px | + +| Position | Centered, flush to top edge (`top: 0`) | + +| Fill | Horizontal gradient `transparent → accent (50%) → transparent` | + +| Glow (recording/error) | Outer blur `0 0 12 px {accent}@80%` + offset `0 2 14 px {accent}@40%` | + +| Opacity by state | `recording` 1.0 · `error` 1.0 · `transcribing` 0.55 · `confirmed` 0.40 · `hidden` 0 | + +| Color | Mint by default (`#4DDBA6`); \*\*shifts to `#FF4D4F` (error red) instantly — no fade\*\* | + + + +\### 2.3 The visualizer + + + +\*\*This is the message\*\* when recording. No text is needed; the visualizer carries the meaning. + + + +| Property | Value | + +|---|---| + +| Bar count | 20 | + +| Bar width | 3 px | + +| Bar corner radius | 2 px | + +| Bar gap | 4 px | + +| Total track width | \*\*136 px\*\* (20 × 3 + 19 × 4) | + +| Track height | 28 px | + +| Bar height range | 4 px (idle) ↔ 26 px (max) | + +| Active bar fill | `rgba(255,255,255,0.92)` (dark) / `rgba(20,20,20,0.85)` (light) | + +| Idle bar fill | `rgba(255,255,255,0.28)` (dark) / `rgba(20,20,20,0.25)` (light) | + +| Per-bar glow | When level > 0.7, add `0 0 6 px {accent}@33%` shadow | + + + +\#### Motion model (language-agnostic pseudocode) + + + +``` + +every animation frame: + + dt = clamp(now - lastFrame, 0, 64ms) / 1000 + + + + if (recording and now > nextTargetAt): + + nextTargetAt = now + random(90..150) ms + + # voice envelope — louder in center, softer at edges + + speak = 0.55 + sin(now / 380) \* 0.25 + random(-0.125, +0.125) + + for each bar i in 0..19: + + center\_weight = 1 - abs(i - 9.5) / 9.5 # 1.0 at center, 0 at edges + + base = 0.18 + center\_weight \* 0.45 + + jitter = random(-0.30, +0.70) \* 0.55 # asymmetric — favors loud + + target\[i] = clamp(base \* speak + jitter, 0.05, 1.0) + + + + if (not recording): + + target\[i] = 0.05 for all bars + + + + # damped approach (per-bar rate so they don't move in lockstep) + + for each bar i: + + rate = recording ? (14 + (i % 3) \* 3) : 6 + + k = 1 - exp(-rate \* dt) + + level\[i] += (target\[i] - level\[i]) \* k + + + + # render + + bar\_height\[i] = 4 + level\[i] \* 22 + +``` + + + +Replace `target\[i]` with real-amplitude × center\_weight × jitter if you have live audio data; keep the damping pass. + + + +\### 2.4 The five states + + + +\#### 1 · Hidden + +Nothing on screen. Window not rendered (or fully transparent + non-interactive). There is \*\*no "idle" pill\*\*. + + + +\#### 2 · Recording + +\- Dark/light surface as theme dictates. + +\- Mint accent line at full opacity + glow. + +\- 20-bar visualizer running (motion model above). + +\- 4 px below the visualizer: micro-label \*\*`RECORDING`\*\* in 9.5 px uppercase, letter-spacing 0.08 em, muted-text color. + + + +\#### 3 · Transcribing + +\- Surface unchanged. + +\- Accent line opacity drops to 0.55. + +\- Center content: \*\*14 px spinner\*\* (1.5 px stroke, \~270° dash arc, 0.9 s linear rotation) + \*\*`Transcribing…`\*\* in 13 px / 500 / primary-text-color. 8 px gap. + + + +\#### 4 · Paste confirmed + +\- Surface unchanged. + +\- Accent line opacity drops to 0.40. + +\- Visualizer \*\*continues to run\*\* but at \*\*35 % opacity\*\* with a \*\*radial mask\*\* punching a transparent hole through the middle: + + + +``` + +mask-image: radial-gradient( + + ellipse 60% 140% at 50% 50%, + + transparent 0%, + + transparent 35%, + + rgba(0,0,0,0.55) 60%, + + black 88% + +); + +``` + + + +\- Text \*\*`Pasted into `\*\* fades in (200 ms, opacity 0→1 + translateY 2→0). `` is \*\*bold 600\*\*, the rest is \*\*500\*\*. 13 px. + +\- Holds 1 s, then the whole pill fades out (120 ms). + + + +\#### 5 · Error + +\- Surface unchanged. + +\- Accent line \*\*shifts to red `#FF4D4F` instantly\*\* — no fade on color. + +\- Center content: a 5 × 5 px red dot with a soft red glow + brief error text (12.5 px / 500). 8 px gap. + +\- Holds \~2 s, then the pill fades out. + +\- Copy must fit the 136 px content track at 12.5 px. Truncate with `…`. Approved short forms: `Microphone blocked`, `Disk full`, `No internet`, `Window closed`, `Paste failed`, `Model not loaded`. + + + +\### 2.5 Pill motion language + + + +| Transition | Duration | Curve | Notes | + +|---|---|---|---| + +| Appear (hidden → any) | 120 ms | ease-out | Opacity 0 → 1. No scale, no slide, no bounce. | + +| Disappear (any → hidden) | 120 ms | ease-in | Opacity 1 → 0. | + +| State content crossfade | 150 ms | ease | Old content fades out, new fades in. | + +| Confirmation choreography | 200 + 1000 + 120 ms | — | See §2.6 | + +| Error accent color shift | \*\*0 ms\*\* | — | Instant — no fade on the color, only on appearance. | + + + +\### 2.6 Confirmation choreography + + + +``` + +t = 0 transcribing showing + +t = 0 transcript ready → switch to confirmed state + +t = 0 visualizer ghosts to 35 % opacity, mask applies, accent line drops to 0.4 + +t = 0..200 "Pasted into " text fades in (opacity 0→1, translateY 2→0) + +t = 1000 begin pill fade-out (120 ms) + +t = 1120 pill fully hidden + +``` + + + +\### 2.7 Reduced motion + +Respect the OS reduced-motion preference: hold all bars at the mean level (\~0.45), no animation. All fades become instant. + + + +\--- + + + +\## 3. Surface 2 · The Main Window + + + +Opened intentionally from the tray. Closing hides (does not quit). + + + +\### 3.1 Window chrome + + + +| Property | Value | + +|---|---| + +| Default size | 880 × 620 | + +| Minimum size | 820 × 600 | + +| Resizable | Yes | + +| Title bar | \*\*System chrome\*\* — no custom title bar. Show app icon (16 px, monochrome) + "KusPus" wordmark on the left of the bar. | + +| Title bar height | 32 px | + +| Title bar background | `#1A1A1C` (dark) / `#EAEAEC` (light) | + +| Min / Max / Close | Native chrome behavior. Close button shows `#E81123` hover background. | + +| Closing | \*\*Hides the window\*\* — quit is only via the tray menu. | + +| Corner radius | 8 px on the outer window | + + + +\### 3.2 Layout + + + +Two-pane: \*\*left sidebar (200 px, fixed) + right content area (fills)\*\*. + + + +\#### Sidebar + +\- Background: `#1B1B1D` (dark) / `#ECECEE` (light). + +\- Vertical 1 px right divider (`rgba(255,255,255,0.06)` dark / `rgba(0,0,0,0.06)` light). + +\- Padding: 14 px vertical, 10 px horizontal. + +\- 6 tab buttons stacked vertically. Each: + + - 9 px × 12 px padding, 6 px corner radius, 12 px icon + label gap. + + - Icon (16 px, line-style, 1.4 px stroke). + + - Label in 12.5 px / 500. + + - \*\*Selected:\*\* elevated background (`#2A2A2C` dark / `#FFFFFF` light), icon turns mint (`#4DDBA6`), label gets full primary text color, soft `0 1 2 rgba(0,0,0,0.08)` shadow. + + - \*\*Selected accent stripe:\*\* 3 × 18 px mint pill, 2 px corner radius, sitting just left of the sidebar's inner padding. + + - \*\*Hover (non-selected):\*\* `rgba(255,255,255,0.05)` (dark) / `rgba(0,0,0,0.04)` (light). + +\- \*\*Sidebar footer pill:\*\* small status row at the very bottom of the sidebar — mint dot + `Idle · tiny.en` + monospaced hotkey glyph `⌃⊞`. + + + +\#### Content area + +\- Padding: 28 px top / 36 px sides / 32 px bottom. + +\- Vertical scroll only. + +\- Background: `#202020` (dark) / `#F3F3F3` (light). + + + +\### 3.3 The six tabs + + + +The order is fixed: \*\*General → Audio → Models → History → Privacy → About.\*\* + + + +\#### Tab 1 · General + + + +The most-changed settings. Three sections: + + + +1\. \*\*Hotkey\*\* \*(hero control)\* + + - Big tappable card (\~440 px wide). 20 × 22 px padding, 10 px corner radius, 1.5 px border (solid when idle, \*\*dashed mint when listening\*\*). + + - Eyebrow: `HOTKEY` uppercase 10.5 px, mint when listening. + + - Each chord key rendered as a \*\*monospaced keycap\*\*: 7 × 14 px padding, 6 px corner radius, surface background, 1 px strong border, faint `0 1 0 rgba(0,0,0,0.2)` bottom shadow. + + - `+` separator between keys in muted-text color. + + - Default chord: \*\*`LCtrl + LWin`\*\*. + + - \*\*Conflict warning row:\*\* amber dot + bold "Shortcut conflict." + explanation. Appears inline below the picker when the chord clashes with a Windows shortcut. + + + +2\. \*\*Startup\*\* + + - One row card: title `Launch KusPus when I sign in` + subtitle, with a toggle on the right. Default \*\*OFF\*\*. + + + +3\. \*\*Appearance\*\* + + - One row card: title `Theme` + 3-way segmented control `Auto / Light / Dark`. Default `Auto`. + + + +\#### Tab 2 · Audio + + + +1\. \*\*Input device\*\* — one row: device label + native-styled `