Skip to content

feat(ui): unified capture surface + view-level text-size control (0.3.15-rc.6) - #123

Merged
unforced merged 2 commits into
mainfrom
ag-unforced-dev
May 13, 2026
Merged

feat(ui): unified capture surface + view-level text-size control (0.3.15-rc.6)#123
unforced merged 2 commits into
mainfrom
ag-unforced-dev

Conversation

@unforced

Copy link
Copy Markdown
Contributor

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).

  • Adds collapsible "More fields" panel to Capture with path + summary override inputs. Closed by default — textarea stays the no-friction focus.
  • 5-second inactivity autosave (in addition to the existing unmount-flush). Skipped while audio staged, while recording/saving, while body empty.
  • "Need to attach a file? Open the full editor" link in More-fields panel points at /new (NoteNew, preserved). Cmd+K keeps both entries for discoverability.

Item #11 — View-level text-size knob.

  • 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>.
  • --font-size-prose + --font-size-editor CSS variables; .prose-note reads the prose one, CodeMirror reads the editor one (swapped its hardcoded 14px for var(--font-size-editor, 14px)).
  • New TextSizeSection in Settings — three radio buttons.

Scope refinement vs the brief

The brief assumed three separate creation routes (/new, TextCapture, MemoCapture). Reality on main: feat/unified-capture-89 already shipped, so TextCapture and MemoCapture are folded into Capture.tsx. The remaining duality was /capture vs /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:

  1. Attachments in Capture's quick flow (file-drop staging against the enqueue path; M).
  2. captureText role tag wired into NoteNew (consistency win; S).

Patterns check

  • lib/text-size.ts is a faithful mirror of lib/theme.ts — same read/write/apply trio, same "default sentinel removes attribute" pattern. Easy to find via cross-reference.
  • More-fields panel uses native <details> for the disclosure — no new component, no JS state management beyond open. Matches the codebase's preference for native elements with light hooks.
  • Autosave timer + unmount-flush are independent paths over the same 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, applyTextSize attribute behavior, labels.
  • Capture.test.tsx — More fields panel: closed by default; path+summary override payload shape; empty path → no path in 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 serves 200 OK at /notes/.
  • Bundle inspection: More fields, data-text-size, --font-size-editor all present in the built JS/CSS.
  • Manual browser smoke is in the test plan — the test suite covers the state-machine behaviors but not the actual visual polish (which Aaron will see on the dev install).

Test plan

  • /capture loads → textarea focused, no More-fields visible by default. Click "More fields" → path + summary inputs visible.
  • Type a path override + body + click Capture → note saves with the override path.
  • Type a body, walk away for 5+ seconds → toast confirms auto-save (no manual Capture click needed).
  • Hold-to-record audio → autosave does NOT fire (manual click required).
  • Settings → Text size → switch to Larger → editor + read views scale up live. Switch to Largest → scale up more. Reload page → preference persists.
  • /new → NoteNew still renders (attachment drag-drop, file picker, paste — all still work).

🤖 Generated with Claude Code

unforced and others added 2 commits May 12, 2026 17:54
….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>
@unforced
unforced merged commit 11ff2a6 into main May 13, 2026
@unforced
unforced deleted the ag-unforced-dev branch May 13, 2026 00:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant