feat(ui): unified capture surface + view-level text-size control (0.3.15-rc.6) - #123
Merged
Conversation
….15-rc.6) Two items from design/2026-05-12-notes-ui-audit.md §3 bundled per the audit's sequencing note: #12 (unify capture) + #11 (view text-size). Both north-star "Apple-Notes-grade ease" items. ### Unified capture surface (audit §3 item #12) Adds polish to Capture; preserves NoteNew at /new for the file-drop attachment-on-create path so this PR ships clean without losing the working attachment flow. Capture is the canonical quick-path; NoteNew remains the heavy-editor escape hatch one click away. - **More fields panel** — collapsible <details> in Capture exposing a path override + summary input. Closed by default (textarea stays the no-friction focus); operators who need to set an explicit path (e.g. capturing into "Daily/2026-05-12") get the form without leaving Capture. Empty path = "vault auto-assigns"; empty summary = "no metadata.summary". Path override wins over the audio-only memo auto-path. - **Inactivity autosave** — fires save() after 5 seconds of editing inactivity, in addition to the existing unmount-flush. Skipped while audio is staged (manual Capture click only), while recording/saving, and while body is empty. Hardcoded 5s per brief. - **Escape hatch** — "Need to attach a file? Open the full editor" link in the More-fields panel points at /new (NoteNew, kept). Cmd+K keeps both "New note" + "Capture" entries for discoverability. ### View-level text-size control (audit §3 item #11) - **lib/text-size.ts** — mirrors lib/theme.ts shape. Three steps (Default / Larger / Largest); per-device localStorage at `notes:textSize`; applied via `data-text-size` attribute on <html>. - **styles/index.css** — `--font-size-prose` + `--font-size-editor` CSS variables on :root with two `:root[data-text-size="…"]` overrides. `.prose-note` reads the prose one; CodeMirror reads the editor one (its hardcoded `fontSize: "14px"` swapped for `var(--font-size-editor, 14px)`). - **Settings TextSizeSection** — three radio buttons; applies + persists in one motion. Markdown on disk unaffected — pure view preference. - **App.tsx** — applies the stored size on mount so the preference takes effect on every route, not just Settings. ### Tests - 4 new in text-size.test.ts (round-trip, default handling, data-attribute application, labels). - 8 new in Capture.test.tsx (disclosure closed by default; path + summary override payload shape; empty path → no `path` in payload; path override wins over audio memo auto-path; autosave-after-5s; edit-resets-timer; empty content no-autosave; audio-staged suppresses autosave). ### Hardening - Unmount-flush enqueue now swallows IDB teardown rejections — the SyncProvider closes its handle in the same tick as Capture's unmount-flush, documented race in SyncProvider.tsx:60. No user-visible surface to report failures during nav-away anyway. ### Follow-ups not in this PR - Attachments in Capture's quick flow (file-drop staging against the enqueue path) — currently only via /new. Will file as separate issue. - captureText role tag applied in NoteNew (today only Capture applies the role). Small consistency win — separate issue. Gates: 80 test files / 722 tests pass (+12 from this PR; was 710 in #122). Typecheck clean, lint clean, build green (1561 KiB precache). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Reviewer F1 (critical, data-loss) — savingRef was never reset after a successful save. With autosave, the user stays mounted after a save, so: - The next autosave timer's `if (savingRef.current) return` always bailed. - The unmount-flush's same guard always bailed. Net effect: type thought 1, autosave fires + succeeds, type thought 2, close tab → thought 2 silently lost. Fix: one line after `reset()` in the success path. Pre-autosave the manual Capture click was the only entry point and a fresh mount handled the reset implicitly; with the 5s timer the mount is reused across saves. Two regression tests: - "second autosave after first fires (savingRef releases on success)" asserts two distinct saves land in the queue from one mount. - "unmount-flush after a successful autosave still flushes new typed content" asserts post-autosave drafts aren't dropped on nav-away. Uses the existing Toggler pattern so the SyncProvider's IDB handle stays open while the unmount-flush enqueue runs. Reviewer F2 (polish) — add `@vitest-environment jsdom` pragma to text-size.test.ts. The vitest config already defaults to jsdom but the pragma makes the dependency explicit for stray `bun test` invocations. Reviewer F3 (polish) — Settings TextSizeSection used useEffect to read localStorage after the first render, causing a visible flash from "Default" to the stored value. Switched to a lazy initializer so the correct value is rendered on first paint. Reviewer F4 (skipped) — the <details>/<summary> test pattern is already documented inline. No change needed. Gates: 80 test files / 724 tests pass (+2 from F1 regression tests), typecheck clean, lint clean, build green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Two items from
design/2026-05-12-notes-ui-audit.md§3 bundled per the audit's sequencing note ("text-size cheap to ship alongside unified surface if the surface PR is open anyway"). Both north-star "Apple-Notes-grade ease" items.Summary
Item #12 — Unified capture surface (capture polish, not full collapse).
Capturewith path + summary override inputs. Closed by default — textarea stays the no-friction focus./new(NoteNew, preserved). Cmd+K keeps both entries for discoverability.Item #11 — View-level text-size knob.
lib/text-size.tsmirrorslib/theme.tsshape: three steps (Default / Larger / Largest); per-device localStorage atnotes:textSize; applied viadata-text-sizeattribute on<html>.--font-size-prose+--font-size-editorCSS variables;.prose-notereads the prose one, CodeMirror reads the editor one (swapped its hardcoded14pxforvar(--font-size-editor, 14px)).TextSizeSectionin Settings — three radio buttons.Scope refinement vs the brief
The brief assumed three separate creation routes (
/new, TextCapture, MemoCapture). Reality onmain:feat/unified-capture-89already shipped, so TextCapture and MemoCapture are folded intoCapture.tsx. The remaining duality was/capturevs/new, where/new(NoteNew) is the only path with attachment drop/paste/picker on create (per PR #8 history).This PR delivers the audit's "Capture canonical" framing while preserving
/new's attachment-rich path as a one-click escape hatch. The trade-off is keeping a working feature working over fully collapsing to one route. Two follow-up issues will be filed:captureTextrole tag wired into NoteNew (consistency win; S).Patterns check
lib/text-size.tsis a faithful mirror oflib/theme.ts— same read/write/apply trio, same "default sentinel removes attribute" pattern. Easy to find via cross-reference.<details>for the disclosure — no new component, no JS state management beyondopen. Matches the codebase's preference for native elements with light hooks.save()function; no duplication of the enqueue branching logic.Tests
12 new tests (4 + 8). Full suite: 80 test files / 722 tests pass (was 710 in #122). Typecheck clean, lint clean, build green.
Behavioral coverage:
text-size.test.ts: round-trip storage + default fallback,applyTextSizeattribute behavior, labels.Capture.test.tsx— More fields panel: closed by default; path+summary override payload shape; empty path → nopathin payload; path override wins over audio memo auto-path.Capture.test.tsx— Autosave: fires after 5s; further edits reset the timer; empty content doesn't fire; staged audio suppresses autosave.End-to-end smoke
parachute restart notes→ portal serves200 OKat/notes/.More fields,data-text-size,--font-size-editorall present in the built JS/CSS.Test plan
/captureloads → textarea focused, no More-fields visible by default. Click "More fields" → path + summary inputs visible./new→ NoteNew still renders (attachment drag-drop, file picker, paste — all still work).🤖 Generated with Claude Code