feat(ui): accessible text-size (shortcuts + header) + locally-generated capture path (0.3.15-rc.7) - #130
Merged
Merged
Conversation
…ed capture path (0.3.15-rc.7) Two follow-ups to rc.6 bundled because they touch the same chrome / Capture surfaces. Closes notes#127 + #126. ## notes#127 — text-size on the editing surface Aaron's feedback: "usually I'm in the middle of typing a note when I want to increase the size." Settings-only is wrong UX. - Keyboard shortcuts: Cmd+= / Cmd+Plus / Cmd+- / Cmd+0. Bound once at the app root via `TextSizeShortcutsMount` so the listener doesn't double-bind when the mobile menu mounts a second `TextSizeControl`. Ignored when Shift, Alt, or no modifier is held. - Header chrome: new `TextSizeControl` ("Aa" button + popover). Three options with ✓ on the active row. Same persist + apply path as the Settings dropdown via `lib/text-size.ts`. Lives on both desktop and mobile menu so phones (no keyboard) keep the path. - Same-tab sync: `CustomEvent("notes:text-size-change")` keeps the popover's active row in sync when a shortcut or sibling control changes the size (the `storage` event only fires cross-tab). - New helpers `nextTextSize` / `previousTextSize` mirror `nextTheme`'s shape in `theme.ts`. Direction is explicit at the call site. ## notes#126 — capture path generated locally + pre-filled Aaron's feedback: today's "vault auto-assigns" framing is wrong; vault doesn't have a path-generation workflow, the rule lives in Notes. Make it honest by surfacing the generated path on entry. - `quickPath()` helper in `lib/capture/recorder.ts` next to `memoPath()`. Same `<root>/YYYY/MM-DD/HH-MM-SS` shape under `Notes/` so quickly-captured text and voice memos read as parallel concepts. - Capture's `pathOverride` state is now seeded with `quickPath()` on mount. The operator sees the generated path the moment they expand More fields — no more invisible auto-assign magic. Empty path (operator-cleared) preserves the rc.6 escape valve: text falls to vault-picks, audio-only falls to `memoPath()`. ## Behavior changes worth flagging - Text-only captures now land at `Notes/<date>/<time>` by default (was: vault-assigned). Same shape, just Notes-side. - Audio captures with no override now also land under `Notes/` (was: `Memos/`). Clearing the input restores the old `Memos/` rule. The escape valve is pinned by a new test. - Combined text+voice captures now land at `Notes/<date>/<time>` (was: vault-assigned). ## Tests - `text-size.test.ts`: +2 (cycle direction helpers). - `TextSizeControl.test.tsx`: +12 (button + popover, keyboard handlers, same-tab sync between shortcut and popover). - `recorder.test.ts`: +1 (quickPath shape). - `Capture.test.tsx`: +2 (pre-fill is editable and saved; audio-only memoPath fallback when path cleared). Existing assertions updated where pre-fill changed observable behavior. Gates: 81 test files / 741 tests pass (+17 from this PR; was 724 in rc.6 + F1 fold). typecheck clean, lint clean, build green (1563 KiB precache). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 tasks
unforced
added a commit
that referenced
this pull request
May 13, 2026
…d) + path-collision fix (0.3.15-rc.8) Closes notes#126 (reshaped scope per Aaron's design conversation). Builds on rc.7's `quickPath()` pre-fill (PR #130) with the confirmed `capture/*` classification model plus three reviewer follow-ups bundled. ## What landed **Hierarchical capture tags (notes#126 reshape):** - New file `src/lib/vault/schema.ts` declaring `NOTES_REQUIRED_SCHEMA`: - `capture` (parent, no parent_names) - `capture/text` (parent_names: ["capture"]) - `capture/voice` (parent_names: ["capture"]) First instance of patterns#57 (surface-declares-required-schema). - `DEFAULT_TAG_ROLES.captureText` → `"capture/text"`, `DEFAULT_TAG_ROLES.captureVoice` → `"capture/voice"`. Existing vaults preserve their stored values — no force-migrate. **Idempotent schema-ensure:** - New `VaultClient.updateTag(name, { description, parent_names })` — PUT /api/tags/:name, field-merged vault-side. - New `src/lib/vault/schema-ensure.ts` exposes `ensureNotesSchema()`. Per-vault per-session module-level ref guard so repeated captures don't hammer the vault. Failure rolls back the guard so next capture retries; success is silent (it's plumbing, not user- actionable — notes#129 will add the audit UI). - Capture wires `ensureNotesSchema(activeVault.id, client)` as a fire-and-forget call inside save(). Doesn't block the save or surface failures. **Option (d) bundled** (closed PR #131 absorbed here): - `pathOverride.trim() || generatedPathRef.current` resolves to the mount-time generated value when input is empty. Never falls back to vault-picks. The rc.6 `memoPath()` audio-only fallback is unreachable and dropped — one canonical Notes-side rule. - Placeholder text updated: "(blank → vault picks)" → "(blank → uses generated path)". **Path-collision fix** (raised in #130 review): - `reset()` after successful save now regenerates `quickPath()` and updates the input — but only when the operator hasn't manually edited (`pathEditedRef`). User-typed paths survive across captures on the same mount; auto-generated paths roll forward in time. ## Behavior changes - Text captures: default tag is `capture/text` instead of `quick`. - Voice captures: default tag is `capture/voice` instead of `voice`. - Existing vaults with stored values are NOT migrated — their `quick`/`voice` choices stand. Schema-ensure still writes the parent + parent_names rows so hierarchy queries work either way. - Audio captures with cleared path no longer go to `Memos/` — they land under `Notes/` like text captures (single canonical rule). - Two captures within the same wall-clock second no longer collide on the auto-generated path. ## Tests - 6 new in `schema-ensure.test.ts`: declaration-order PUTs, parent-before-children, per-session per-vault idempotence, multi-vault independence, retry-after-failure, swallow-doesn't- throw. - 2 new in `Capture.test.tsx`: regen-on-reset-when-unedited (with fake-timer `setSystemTime` + shouldAdvanceTime for waitFor), preserve-user-edit-across-reset. - 4 existing tests flipped for the default rename + option (d): - `"quick"`/`"voice"` → `"capture/text"`/`"capture/voice"` in tag assertions. - "Empty path → omits path" → "Empty path reverts to generated". - "Audio-only cleared path → memoPath" → "Audio-only cleared path → generated path". - `Capture.test.tsx` adds `vi.mock("@/lib/vault/schema-ensure")` so capture tests don't hit the real PUT (covered by the dedicated schema-ensure tests). ## Not in scope - Full Settings audit UI + connect-time banner → notes#129. - Per-vault path-template customization → notes#128. Gates: 82 test files / 749 tests pass (+8 from rc.7's 741). typecheck clean, lint clean, build green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
unforced
added a commit
that referenced
this pull request
May 13, 2026
…d) + path-collision fix (0.3.15-rc.8) (#132) Closes notes#126 (reshaped scope per Aaron's design conversation). Builds on rc.7's `quickPath()` pre-fill (PR #130) with the confirmed `capture/*` classification model plus three reviewer follow-ups bundled. ## What landed **Hierarchical capture tags (notes#126 reshape):** - New file `src/lib/vault/schema.ts` declaring `NOTES_REQUIRED_SCHEMA`: - `capture` (parent, no parent_names) - `capture/text` (parent_names: ["capture"]) - `capture/voice` (parent_names: ["capture"]) First instance of patterns#57 (surface-declares-required-schema). - `DEFAULT_TAG_ROLES.captureText` → `"capture/text"`, `DEFAULT_TAG_ROLES.captureVoice` → `"capture/voice"`. Existing vaults preserve their stored values — no force-migrate. **Idempotent schema-ensure:** - New `VaultClient.updateTag(name, { description, parent_names })` — PUT /api/tags/:name, field-merged vault-side. - New `src/lib/vault/schema-ensure.ts` exposes `ensureNotesSchema()`. Per-vault per-session module-level ref guard so repeated captures don't hammer the vault. Failure rolls back the guard so next capture retries; success is silent (it's plumbing, not user- actionable — notes#129 will add the audit UI). - Capture wires `ensureNotesSchema(activeVault.id, client)` as a fire-and-forget call inside save(). Doesn't block the save or surface failures. **Option (d) bundled** (closed PR #131 absorbed here): - `pathOverride.trim() || generatedPathRef.current` resolves to the mount-time generated value when input is empty. Never falls back to vault-picks. The rc.6 `memoPath()` audio-only fallback is unreachable and dropped — one canonical Notes-side rule. - Placeholder text updated: "(blank → vault picks)" → "(blank → uses generated path)". **Path-collision fix** (raised in #130 review): - `reset()` after successful save now regenerates `quickPath()` and updates the input — but only when the operator hasn't manually edited (`pathEditedRef`). User-typed paths survive across captures on the same mount; auto-generated paths roll forward in time. ## Behavior changes - Text captures: default tag is `capture/text` instead of `quick`. - Voice captures: default tag is `capture/voice` instead of `voice`. - Existing vaults with stored values are NOT migrated — their `quick`/`voice` choices stand. Schema-ensure still writes the parent + parent_names rows so hierarchy queries work either way. - Audio captures with cleared path no longer go to `Memos/` — they land under `Notes/` like text captures (single canonical rule). - Two captures within the same wall-clock second no longer collide on the auto-generated path. ## Tests - 6 new in `schema-ensure.test.ts`: declaration-order PUTs, parent-before-children, per-session per-vault idempotence, multi-vault independence, retry-after-failure, swallow-doesn't- throw. - 2 new in `Capture.test.tsx`: regen-on-reset-when-unedited (with fake-timer `setSystemTime` + shouldAdvanceTime for waitFor), preserve-user-edit-across-reset. - 4 existing tests flipped for the default rename + option (d): - `"quick"`/`"voice"` → `"capture/text"`/`"capture/voice"` in tag assertions. - "Empty path → omits path" → "Empty path reverts to generated". - "Audio-only cleared path → memoPath" → "Audio-only cleared path → generated path". - `Capture.test.tsx` adds `vi.mock("@/lib/vault/schema-ensure")` so capture tests don't hit the real PUT (covered by the dedicated schema-ensure tests). ## Not in scope - Full Settings audit UI + connect-time banner → notes#129. - Per-vault path-template customization → notes#128. Gates: 82 test files / 749 tests pass (+8 from rc.7's 741). 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 follow-ups bundled per the dispatch brief — both touch the same chrome / Capture surfaces. Closes #127 + #126.
#127 — text-size accessible from the editing surface
Aaron flagged: "usually I'm in the middle of typing a note when I want to increase the size" — Settings-only is wrong UX.
Shipped:
TextSizeShortcutsMountso the listener doesn't double-bind when the mobile menu mounts a secondTextSizeControl. Ignored when Shift/Alt held or no modifier.TextSizeControl("Aa" button + popover) next to existing chrome (Install / Theme). Three options with a ✓ on the active row. Lives on both desktop and mobile menu so phones (no keyboard) keep the path.CustomEvent("notes:text-size-change")keeps the popover's active row in sync when a shortcut or sibling control changes the size (thestorageevent only fires cross-tab).nextTextSize/previousTextSizeinlib/text-size.ts, mirroringnextTheme's shape. Direction is explicit at the call site.#126 — Capture path generated locally + pre-filled
Aaron flagged: today's "vault auto-assigns" framing is wrong — vault doesn't have a path-generation workflow, the rule lives in Notes. Make it honest.
Shipped:
quickPath()helper next tomemoPath()inlib/capture/recorder.ts. Same<root>/YYYY/MM-DD/HH-MM-SSshape underNotes/so quickly-captured text and voice memos read as parallel concepts.pathOverridestate is seeded withquickPath()on mount. Operator sees the generated path the moment they expand More fields. They can accept, edit, or clear.memoPath()). Pinned by a new test.Behavior changes (worth flagging for review)
Notes/<date>/<time>by default (was: vault-assigned).Notes/(was:Memos/). Clearing the input restores theMemos/rule.Notes/<date>/<time>(was: vault-assigned).If Aaron wants audio captures to keep going to
Memos/by default (regardless of the pre-fill being non-empty), the fix is a phase-aware pre-fill — pre-fill switches tomemoPath()when audio is staged AND the user hasn't manually edited. Flag in review if that's the preference.Design call (re-flagging from my ack)
The brief said "My lean is in-More-fields pre-filled — keeps the focused-textarea UX, but the path is no longer hidden", while issue #126 acceptance reads "Pre-fill the path input visibly (not behind 'More fields')". I went with the brief's lean (in-More-fields pre-filled) because the textarea-first UX in audit §3 #12 is north-star. If you want it surfaced above the textarea as an always-visible slim affordance instead, flag it in review and I'll move it.
Patterns check
TextSizeControl+TextSizeShortcutsMountcollocate as one feature module — the visible affordance and the global listener are the same concept, even though they have to be mounted at different DOM positions.CustomEventpattern is new for this codebase — adds explicit comment about whystoragealone isn't enough. Mirror of cross-tab sync but for same-tab; not a clash with existing patterns.quickPath()mirrorsmemoPath()shape exactly so the two helpers are interchangeable for future "configurable path rule" work (notes#126 mentions per-vault override as a future).Tests
text-size.test.ts: +2 (cycle direction helpers).TextSizeControl.test.tsx: +12 (button + popover behavior, keyboard handlers across Cmd+= / Cmd+Plus / Cmd+- / Cmd+0 / Shift-rejection / no-modifier-rejection, same-tab sync between shortcut and popover).recorder.test.ts: +1 (quickPath shape).Capture.test.tsx: +2 (pre-fill is editable + saved; audio-only memoPath escape valve when path cleared). Existing assertions updated where pre-fill changed observable behavior.Full gate: 81 test files / 741 tests pass (+17 from this PR; was 724 in rc.6 + F1 fold). Typecheck clean, lint clean, build green (1563 KiB precache).
End-to-end smoke
parachute restart notesclean. Bundle shipsTextSizeControl,quickPath, the new keymap handler. Browser test plan below.Test plan
/capture. Cmd+0 resets.+in markdown tables)./capture→ expand More fields → Path input is pre-filled withNotes/<today>/<time>. Edit to a custom path → save → new note lands at the custom path. Clear path → save text-only → vault picks (rc.6 behavior). Clear path → record audio only → memo lands underMemos/<date>(rc.6 escape valve)./new→ still renders NoteNew (heavy form with attachments) — untouched.🤖 Generated with Claude Code